692 lines
18 KiB
Markdown
692 lines
18 KiB
Markdown
|
|
# Antigravity向け - 新生ぽんるーむ実装プロンプト
|
|||
|
|
|
|||
|
|
**実装日**: 2025-12-29以降
|
|||
|
|
**実装担当**: Antigravity
|
|||
|
|
**サポート**: Claude (Anthropic)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 🚨 最重要指示
|
|||
|
|
|
|||
|
|
### ⛔ 絶対にやってはいけないこと
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
❌ 既存のWeb版(ponshu-room/lib/)からコードを1行もコピーしない
|
|||
|
|
❌ 既存プロジェクトを編集しない
|
|||
|
|
❌ Web版のUIを踏襲しない
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### ✅ やるべきこと
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
✅ 完全に新しいFlutterプロジェクトを作成
|
|||
|
|
✅ FINAL_REQUIREMENTS.mdに100%従う
|
|||
|
|
✅ スキャンアプリ(mai_quick_scan)の成功パターンを参考にする
|
|||
|
|
✅ 写真を主役にする
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 📱 プロジェクト作成(コピペして実行)
|
|||
|
|
|
|||
|
|
### ステップ1: 新規プロジェクト作成
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
# 親ディレクトリへ移動
|
|||
|
|
cd C:\Users\maita\posimai-project
|
|||
|
|
|
|||
|
|
# 完全に新しいFlutterプロジェクトを作成
|
|||
|
|
flutter create ponshu_room_reborn
|
|||
|
|
|
|||
|
|
cd ponshu_room_reborn
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### ステップ2: pubspec.yaml編集
|
|||
|
|
|
|||
|
|
**完全に置き換え**:
|
|||
|
|
|
|||
|
|
```yaml
|
|||
|
|
name: ponshu_room_reborn
|
|||
|
|
description: "Reborn Ponshu Room - My Digital Sake Cellar"
|
|||
|
|
publish_to: 'none'
|
|||
|
|
version: 2.0.0+1
|
|||
|
|
|
|||
|
|
environment:
|
|||
|
|
sdk: ^3.10.1
|
|||
|
|
|
|||
|
|
dependencies:
|
|||
|
|
flutter:
|
|||
|
|
sdk: flutter
|
|||
|
|
|
|||
|
|
# 状態管理(スキャンアプリと同じ)
|
|||
|
|
flutter_riverpod: ^2.6.1
|
|||
|
|
hooks_riverpod: ^2.6.1
|
|||
|
|
flutter_hooks: ^0.20.5
|
|||
|
|
|
|||
|
|
# ローカルDB
|
|||
|
|
hive: ^2.2.3
|
|||
|
|
hive_flutter: ^1.1.0
|
|||
|
|
|
|||
|
|
# AI解析
|
|||
|
|
google_generative_ai: ^0.4.7
|
|||
|
|
http: ^1.6.0
|
|||
|
|
|
|||
|
|
# カメラ・画像
|
|||
|
|
camera: ^0.11.0+2
|
|||
|
|
image_picker: ^1.2.1
|
|||
|
|
image: ^4.3.0
|
|||
|
|
|
|||
|
|
# UI
|
|||
|
|
google_fonts: ^6.2.1
|
|||
|
|
fl_chart: ^1.1.1
|
|||
|
|
|
|||
|
|
# 共有・保存
|
|||
|
|
share_plus: ^12.0.1
|
|||
|
|
path_provider: ^2.1.5
|
|||
|
|
|
|||
|
|
# その他
|
|||
|
|
intl: ^0.20.2
|
|||
|
|
package_info_plus: ^9.0.0
|
|||
|
|
url_launcher: ^6.3.2
|
|||
|
|
countries_world_map: ^1.3.0
|
|||
|
|
cupertino_icons: ^1.0.8
|
|||
|
|
|
|||
|
|
dev_dependencies:
|
|||
|
|
flutter_test:
|
|||
|
|
sdk: flutter
|
|||
|
|
flutter_lints: ^6.0.0
|
|||
|
|
hive_generator: ^2.0.1
|
|||
|
|
build_runner: ^2.4.13
|
|||
|
|
|
|||
|
|
flutter:
|
|||
|
|
uses-material-design: true
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
flutter pub get
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### ステップ3: Android設定
|
|||
|
|
|
|||
|
|
`android/app/build.gradle.kts` を編集:
|
|||
|
|
|
|||
|
|
```kotlin
|
|||
|
|
android {
|
|||
|
|
namespace = "com.posimai.ponshu_room"
|
|||
|
|
compileSdk = 36 // Android 15+ 対応
|
|||
|
|
|
|||
|
|
defaultConfig {
|
|||
|
|
applicationId = "com.posimai.ponshu_room"
|
|||
|
|
minSdk = 21
|
|||
|
|
targetSdk = 35
|
|||
|
|
versionCode = 1
|
|||
|
|
versionName = "2.0.0"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
compileOptions {
|
|||
|
|
sourceCompatibility = JavaVersion.VERSION_17
|
|||
|
|
targetCompatibility = JavaVersion.VERSION_17
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
kotlinOptions {
|
|||
|
|
jvmTarget = JavaVersion.VERSION_17.toString()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
buildTypes {
|
|||
|
|
release {
|
|||
|
|
signingConfig = signingConfigs.getByName("debug")
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 🎨 デザイン仕様(最重要)
|
|||
|
|
|
|||
|
|
### 写真を主役にする「ナロー・マージン」設計
|
|||
|
|
|
|||
|
|
#### ❌ 従来の余白設計(ダメな例)
|
|||
|
|
|
|||
|
|
```dart
|
|||
|
|
ListView.separated(
|
|||
|
|
padding: EdgeInsets.all(16), // ← 余白が大きすぎ
|
|||
|
|
separatorBuilder: (context, index) => SizedBox(height: 16), // ← 隙間が大きすぎ
|
|||
|
|
itemBuilder: (context, index) => SakeCard(...),
|
|||
|
|
)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**問題点**: 写真が小さくなる、迫力がない
|
|||
|
|
|
|||
|
|
#### ✅ 新生ぽんるーむの余白設計(正しい例)
|
|||
|
|
|
|||
|
|
```dart
|
|||
|
|
ListView.separated(
|
|||
|
|
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 12), // ← 最小限
|
|||
|
|
separatorBuilder: (context, index) => SizedBox(height: 4), // ← 細い隙間
|
|||
|
|
itemBuilder: (context, index) => SakeCard(...),
|
|||
|
|
)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**メリット**: 写真が大きい、迫力がある、Instagram的
|
|||
|
|
|
|||
|
|
### カードデザイン - 「フル幅カード」
|
|||
|
|
|
|||
|
|
```dart
|
|||
|
|
// lib/widgets/sake_card.dart
|
|||
|
|
class SakeCard extends StatelessWidget {
|
|||
|
|
final SakeItem item;
|
|||
|
|
|
|||
|
|
const SakeCard({required this.item});
|
|||
|
|
|
|||
|
|
@override
|
|||
|
|
Widget build(BuildContext context) {
|
|||
|
|
final screenWidth = MediaQuery.of(context).size.width;
|
|||
|
|
|
|||
|
|
return Card(
|
|||
|
|
margin: EdgeInsets.zero, // ← カード自体のマージンはゼロ
|
|||
|
|
elevation: 0,
|
|||
|
|
shape: RoundedRectangleBorder(
|
|||
|
|
borderRadius: BorderRadius.circular(0), // ← 角丸なし(フル幅)
|
|||
|
|
side: BorderSide(
|
|||
|
|
color: Color(0xFFE8E8E8),
|
|||
|
|
width: 0.5, // ← 繊細なボーダー
|
|||
|
|
),
|
|||
|
|
),
|
|||
|
|
child: Container(
|
|||
|
|
decoration: BoxDecoration(
|
|||
|
|
border: Border(
|
|||
|
|
bottom: BorderSide(
|
|||
|
|
color: Color(0xFFE8E8E8),
|
|||
|
|
width: 0.5,
|
|||
|
|
),
|
|||
|
|
),
|
|||
|
|
),
|
|||
|
|
child: Padding(
|
|||
|
|
padding: EdgeInsets.all(12), // ← カード内の余白は最小限
|
|||
|
|
child: Row(
|
|||
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|||
|
|
children: [
|
|||
|
|
// 左: 写真(できるだけ大きく)
|
|||
|
|
ClipRRect(
|
|||
|
|
borderRadius: BorderRadius.circular(8), // ← 写真だけ角丸
|
|||
|
|
child: Image.network(
|
|||
|
|
item.imagePath,
|
|||
|
|
width: 120, // ← 大きく!
|
|||
|
|
height: 120,
|
|||
|
|
fit: BoxFit.cover,
|
|||
|
|
),
|
|||
|
|
),
|
|||
|
|
SizedBox(width: 12),
|
|||
|
|
// 右: テキスト情報
|
|||
|
|
Expanded(
|
|||
|
|
child: Column(
|
|||
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|||
|
|
children: [
|
|||
|
|
// 銘柄名(明朝体、大きく)
|
|||
|
|
Text(
|
|||
|
|
item.brandName ?? '日本酒',
|
|||
|
|
style: GoogleFonts.notoSerifJp(
|
|||
|
|
fontSize: 18,
|
|||
|
|
fontWeight: FontWeight.w600,
|
|||
|
|
color: Color(0xFF1A1A1A),
|
|||
|
|
),
|
|||
|
|
maxLines: 2,
|
|||
|
|
overflow: TextOverflow.ellipsis,
|
|||
|
|
),
|
|||
|
|
SizedBox(height: 4),
|
|||
|
|
// 酒蔵・産地
|
|||
|
|
Text(
|
|||
|
|
'${item.breweryName ?? ''} | ${item.prefecture ?? ''}',
|
|||
|
|
style: GoogleFonts.notoSansJp(
|
|||
|
|
fontSize: 11,
|
|||
|
|
color: Color(0xFF8A8A8A),
|
|||
|
|
),
|
|||
|
|
),
|
|||
|
|
SizedBox(height: 8),
|
|||
|
|
// 評価
|
|||
|
|
if (item.rating != null)
|
|||
|
|
Row(
|
|||
|
|
children: List.generate(
|
|||
|
|
5,
|
|||
|
|
(index) => Icon(
|
|||
|
|
index < item.rating!.round()
|
|||
|
|
? Icons.star
|
|||
|
|
: Icons.star_border,
|
|||
|
|
size: 16,
|
|||
|
|
color: Color(0xFFFFC107),
|
|||
|
|
),
|
|||
|
|
),
|
|||
|
|
),
|
|||
|
|
SizedBox(height: 4),
|
|||
|
|
// 種類
|
|||
|
|
if (item.type != null)
|
|||
|
|
Text(
|
|||
|
|
item.type!,
|
|||
|
|
style: GoogleFonts.notoSansJp(
|
|||
|
|
fontSize: 11,
|
|||
|
|
color: Color(0xFF4A4A4A),
|
|||
|
|
),
|
|||
|
|
),
|
|||
|
|
// キャッチコピー
|
|||
|
|
if (item.catchCopy != null) ...[
|
|||
|
|
SizedBox(height: 6),
|
|||
|
|
Text(
|
|||
|
|
item.catchCopy!,
|
|||
|
|
style: GoogleFonts.notoSerifJp(
|
|||
|
|
fontSize: 12,
|
|||
|
|
fontStyle: FontStyle.italic,
|
|||
|
|
color: Color(0xFF376495),
|
|||
|
|
),
|
|||
|
|
maxLines: 2,
|
|||
|
|
overflow: TextOverflow.ellipsis,
|
|||
|
|
),
|
|||
|
|
],
|
|||
|
|
],
|
|||
|
|
),
|
|||
|
|
),
|
|||
|
|
],
|
|||
|
|
),
|
|||
|
|
),
|
|||
|
|
),
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**レイアウトイメージ**:
|
|||
|
|
```
|
|||
|
|
┌────────────────────────────────────┐
|
|||
|
|
│ [120x120] │ 獺祭 ← 明朝体18px
|
|||
|
|
│ 写真 │ 旭酒造 | 山口県 ← ゴシック体11px
|
|||
|
|
│ 角丸8px │ ⭐⭐⭐⭐⭐ 純米大吟醸
|
|||
|
|
│ │ "夜風と楽しみたい、淡麗な一滴"
|
|||
|
|
└────────────────────────────────────┘
|
|||
|
|
↑ 0.5pxのボーダーで区切り
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 🍶 Gemini 3.0統合(核心機能)
|
|||
|
|
|
|||
|
|
### APIキー設定
|
|||
|
|
|
|||
|
|
```dart
|
|||
|
|
// lib/secrets.dart
|
|||
|
|
class Secrets {
|
|||
|
|
static const String geminiApiKey = 'AIzaSyA2BSr16R2k0bHjSYcSUdmLoY8PKwaFts0';
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### リアルタイム実況付きGemini解析
|
|||
|
|
|
|||
|
|
```dart
|
|||
|
|
// lib/services/gemini_service.dart
|
|||
|
|
import 'dart:typed_data';
|
|||
|
|
import 'package:flutter/material.dart';
|
|||
|
|
import 'package:google_generative_ai/google_generative_ai.dart';
|
|||
|
|
import 'dart:convert';
|
|||
|
|
import '../secrets.dart';
|
|||
|
|
|
|||
|
|
class GeminiService {
|
|||
|
|
late final GenerativeModel _model;
|
|||
|
|
|
|||
|
|
GeminiService() {
|
|||
|
|
_model = GenerativeModel(
|
|||
|
|
model: 'gemini-2.5-flash-latest', // スキャンアプリで実績あり
|
|||
|
|
apiKey: Secrets.geminiApiKey,
|
|||
|
|
generationConfig: GenerationConfig(
|
|||
|
|
temperature: 0.1,
|
|||
|
|
),
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// リアルタイム実況付きで解析
|
|||
|
|
Stream<String> analyzeSakeLabelWithCommentary(Uint8List imageBytes) async* {
|
|||
|
|
// ステージ1: 開始
|
|||
|
|
yield 'ラベルを読んでいます...';
|
|||
|
|
await Future.delayed(Duration(milliseconds: 500));
|
|||
|
|
|
|||
|
|
// ステージ2: 解析中
|
|||
|
|
yield 'お酒の情報を確認しています...';
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
final prompt = '''
|
|||
|
|
この画像は日本酒のボトルまたはラベルの写真です。
|
|||
|
|
ラベルに書かれているテキスト情報を正確に読み取り、JSON形式で返してください。
|
|||
|
|
|
|||
|
|
【重要な指示】
|
|||
|
|
1. ラベルに明確に書かれている情報のみを抽出してください
|
|||
|
|
2. 推測や想像で値を入れないでください(読めない場合はnullまたは省略)
|
|||
|
|
3. 数値は必ず数字のみ(単位記号%などは除く)
|
|||
|
|
4. このお酒の印象を一言で表す「キャッチコピー」を自動生成してください
|
|||
|
|
|
|||
|
|
【出力フォーマット】
|
|||
|
|
{
|
|||
|
|
"brandName": "銘柄名",
|
|||
|
|
"type": "特定名称",
|
|||
|
|
"alcoholContent": 数値,
|
|||
|
|
"polishingRatio": 数値,
|
|||
|
|
"breweryName": "酒蔵名",
|
|||
|
|
"prefecture": "都道府県名",
|
|||
|
|
"catchCopy": "夜風と楽しみたい、淡麗な一滴"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
**JSON以外の余計な説明は一切不要です。JSONのみを出力してください。**
|
|||
|
|
''';
|
|||
|
|
|
|||
|
|
final content = [
|
|||
|
|
Content.multi([
|
|||
|
|
TextPart(prompt),
|
|||
|
|
DataPart('image/jpeg', imageBytes),
|
|||
|
|
])
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
final response = await _model.generateContent(content);
|
|||
|
|
final text = response.text ?? '';
|
|||
|
|
|
|||
|
|
// ステージ3: 都道府県を検出した場合
|
|||
|
|
if (text.contains('県')) {
|
|||
|
|
final prefectureMatch = RegExp(r'([^\s]+県)').firstMatch(text);
|
|||
|
|
if (prefectureMatch != null) {
|
|||
|
|
yield 'お、これは${prefectureMatch.group(1)}の銘柄ですね...';
|
|||
|
|
await Future.delayed(Duration(milliseconds: 300));
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ステージ4: データ整理中
|
|||
|
|
yield 'データを整理しています...';
|
|||
|
|
await Future.delayed(Duration(milliseconds: 300));
|
|||
|
|
|
|||
|
|
// JSON抽出
|
|||
|
|
final jsonMatch = RegExp(r'\{[\s\S]*\}').firstMatch(text);
|
|||
|
|
if (jsonMatch != null) {
|
|||
|
|
final jsonString = jsonMatch.group(0)!;
|
|||
|
|
final data = jsonDecode(jsonString);
|
|||
|
|
|
|||
|
|
// ステージ5: 完了
|
|||
|
|
yield '解析完了!';
|
|||
|
|
|
|||
|
|
// データを返す(特別な形式)
|
|||
|
|
yield 'DATA:$jsonString';
|
|||
|
|
} else {
|
|||
|
|
yield 'エラー: データを読み取れませんでした';
|
|||
|
|
}
|
|||
|
|
} catch (e) {
|
|||
|
|
yield 'エラー: $e';
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// シンプルな解析(実況なし)
|
|||
|
|
Future<Map<String, dynamic>?> analyzeSakeLabel(Uint8List imageBytes) async {
|
|||
|
|
try {
|
|||
|
|
final prompt = '''
|
|||
|
|
この画像は日本酒のボトルまたはラベルの写真です。
|
|||
|
|
ラベルに書かれているテキスト情報を正確に読み取り、JSON形式で返してください。
|
|||
|
|
|
|||
|
|
【重要な指示】
|
|||
|
|
1. ラベルに明確に書かれている情報のみを抽出してください
|
|||
|
|
2. 推測や想像で値を入れないでください(読めない場合はnullまたは省略)
|
|||
|
|
3. 数値は必ず数字のみ(単位記号%などは除く)
|
|||
|
|
4. このお酒の印象を一言で表す「キャッチコピー」を自動生成してください
|
|||
|
|
|
|||
|
|
【出力フォーマット】
|
|||
|
|
{
|
|||
|
|
"brandName": "銘柄名",
|
|||
|
|
"type": "特定名称",
|
|||
|
|
"alcoholContent": 数値,
|
|||
|
|
"polishingRatio": 数値,
|
|||
|
|
"breweryName": "酒蔵名",
|
|||
|
|
"prefecture": "都道府県名",
|
|||
|
|
"catchCopy": "夜風と楽しみたい、淡麗な一滴"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
**JSON以外の余計な説明は一切不要です。JSONのみを出力してください。**
|
|||
|
|
''';
|
|||
|
|
|
|||
|
|
final content = [
|
|||
|
|
Content.multi([
|
|||
|
|
TextPart(prompt),
|
|||
|
|
DataPart('image/jpeg', imageBytes),
|
|||
|
|
])
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
final response = await _model.generateContent(content);
|
|||
|
|
final text = response.text ?? '';
|
|||
|
|
|
|||
|
|
final jsonMatch = RegExp(r'\{[\s\S]*\}').firstMatch(text);
|
|||
|
|
if (jsonMatch != null) {
|
|||
|
|
final jsonString = jsonMatch.group(0)!;
|
|||
|
|
return jsonDecode(jsonString);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return null;
|
|||
|
|
} catch (e) {
|
|||
|
|
debugPrint('Gemini API Error: $e');
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### リアルタイム実況の使用例
|
|||
|
|
|
|||
|
|
```dart
|
|||
|
|
// lib/screens/input_screen.dart(一部)
|
|||
|
|
StreamBuilder<String>(
|
|||
|
|
stream: geminiService.analyzeSakeLabelWithCommentary(imageBytes),
|
|||
|
|
builder: (context, snapshot) {
|
|||
|
|
if (snapshot.hasData) {
|
|||
|
|
final message = snapshot.data!;
|
|||
|
|
|
|||
|
|
// データが返ってきた場合
|
|||
|
|
if (message.startsWith('DATA:')) {
|
|||
|
|
final jsonString = message.substring(5);
|
|||
|
|
final data = jsonDecode(jsonString);
|
|||
|
|
// フォームに自動入力
|
|||
|
|
_fillForm(data);
|
|||
|
|
return ResultForm(data: data);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 実況メッセージを表示
|
|||
|
|
return AnalyzingOverlay(currentStage: message);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return AnalyzingOverlay(currentStage: 'カメラを起動しています...');
|
|||
|
|
},
|
|||
|
|
)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 📦 データモデル
|
|||
|
|
|
|||
|
|
### SakeItem (Hive Model)
|
|||
|
|
|
|||
|
|
```dart
|
|||
|
|
// lib/models/sake_item.dart
|
|||
|
|
import 'package:hive/hive.dart';
|
|||
|
|
|
|||
|
|
part 'sake_item.g.dart';
|
|||
|
|
|
|||
|
|
@HiveType(typeId: 0)
|
|||
|
|
class SakeItem extends HiveObject {
|
|||
|
|
@HiveField(0)
|
|||
|
|
String? brandName;
|
|||
|
|
|
|||
|
|
@HiveField(1)
|
|||
|
|
String? breweryName;
|
|||
|
|
|
|||
|
|
@HiveField(2)
|
|||
|
|
String? prefecture;
|
|||
|
|
|
|||
|
|
@HiveField(3)
|
|||
|
|
String? type;
|
|||
|
|
|
|||
|
|
@HiveField(4)
|
|||
|
|
double? alcoholContent;
|
|||
|
|
|
|||
|
|
@HiveField(5)
|
|||
|
|
int? polishingRatio;
|
|||
|
|
|
|||
|
|
@HiveField(6)
|
|||
|
|
String imagePath;
|
|||
|
|
|
|||
|
|
@HiveField(7)
|
|||
|
|
List<String>? additionalImages;
|
|||
|
|
|
|||
|
|
@HiveField(8)
|
|||
|
|
double? rating;
|
|||
|
|
|
|||
|
|
@HiveField(9)
|
|||
|
|
String? memo;
|
|||
|
|
|
|||
|
|
@HiveField(10)
|
|||
|
|
List<String>? tags;
|
|||
|
|
|
|||
|
|
@HiveField(11)
|
|||
|
|
bool isFavorite;
|
|||
|
|
|
|||
|
|
@HiveField(12)
|
|||
|
|
bool isWishlist;
|
|||
|
|
|
|||
|
|
@HiveField(13)
|
|||
|
|
DateTime createdAt;
|
|||
|
|
|
|||
|
|
@HiveField(14)
|
|||
|
|
DateTime? updatedAt;
|
|||
|
|
|
|||
|
|
@HiveField(15)
|
|||
|
|
int? price;
|
|||
|
|
|
|||
|
|
@HiveField(16)
|
|||
|
|
int? volume;
|
|||
|
|
|
|||
|
|
@HiveField(17)
|
|||
|
|
String? catchCopy; // AIが生成したキャッチコピー
|
|||
|
|
|
|||
|
|
@HiveField(18)
|
|||
|
|
double? sweetnessScore; // -1.0(辛口)~ 1.0(甘口)
|
|||
|
|
|
|||
|
|
@HiveField(19)
|
|||
|
|
double? bodyScore; // -1.0(淡麗)~ 1.0(濃醇)
|
|||
|
|
|
|||
|
|
SakeItem({
|
|||
|
|
this.brandName,
|
|||
|
|
this.breweryName,
|
|||
|
|
this.prefecture,
|
|||
|
|
this.type,
|
|||
|
|
this.alcoholContent,
|
|||
|
|
this.polishingRatio,
|
|||
|
|
required this.imagePath,
|
|||
|
|
this.additionalImages,
|
|||
|
|
this.rating,
|
|||
|
|
this.memo,
|
|||
|
|
this.tags,
|
|||
|
|
this.isFavorite = false,
|
|||
|
|
this.isWishlist = false,
|
|||
|
|
required this.createdAt,
|
|||
|
|
this.updatedAt,
|
|||
|
|
this.price,
|
|||
|
|
this.volume,
|
|||
|
|
this.catchCopy,
|
|||
|
|
this.sweetnessScore,
|
|||
|
|
this.bodyScore,
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
# 生成コマンド
|
|||
|
|
flutter pub run build_runner build
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 🚀 実装チェックリスト
|
|||
|
|
|
|||
|
|
### Phase 1: MVP(5時間)
|
|||
|
|
|
|||
|
|
- [ ] プロジェクト作成(`flutter create ponshu_room_reborn`)
|
|||
|
|
- [ ] `pubspec.yaml` 設定
|
|||
|
|
- [ ] Android設定(compileSdk: 36, targetSdk: 35)
|
|||
|
|
- [ ] `lib/secrets.dart` 作成(APIキー)
|
|||
|
|
- [ ] `lib/models/sake_item.dart` 作成
|
|||
|
|
- [ ] `flutter pub run build_runner build`
|
|||
|
|
- [ ] `lib/theme/app_theme.dart` 作成(posimaiカラー)
|
|||
|
|
- [ ] `lib/main.dart` 作成(Hive初期化)
|
|||
|
|
- [ ] `lib/screens/home/home_screen.dart` 作成(4タブ)
|
|||
|
|
- [ ] `lib/services/gemini_service.dart` 作成(リアルタイム実況)
|
|||
|
|
- [ ] `lib/widgets/sake_card.dart` 作成(フル幅カード)
|
|||
|
|
- [ ] `lib/widgets/analyzing_overlay.dart` 作成(実況UI)
|
|||
|
|
- [ ] カメラ撮影機能
|
|||
|
|
- [ ] 入力フォーム
|
|||
|
|
- [ ] 詳細画面
|
|||
|
|
- [ ] **SafeArea対応** ← Android 15必須
|
|||
|
|
|
|||
|
|
### Phase 2: 「美録」UI洗練(3時間)
|
|||
|
|
|
|||
|
|
- [ ] 余白調整(padding: 8dp, separator: 4dp)
|
|||
|
|
- [ ] 明朝体×ゴシック体の適用
|
|||
|
|
- [ ] Hero遷移
|
|||
|
|
- [ ] Instagram用画像生成機能
|
|||
|
|
|
|||
|
|
### Phase 3: 「遊び心」機能拡張(4時間)
|
|||
|
|
|
|||
|
|
- [ ] フレーバー・マトリックス
|
|||
|
|
- [ ] 日本酒・制覇マップ + バッジ
|
|||
|
|
- [ ] AIソムリエ(質問例、チャット)
|
|||
|
|
- [ ] マイページ統計グラフ
|
|||
|
|
|
|||
|
|
### Phase 4: 共有機能(2時間)
|
|||
|
|
|
|||
|
|
- [ ] シンプルテキスト共有
|
|||
|
|
- [ ] Instagram用正方形画像生成
|
|||
|
|
- [ ] キャッチコピー付き共有
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## ✅ 完成基準
|
|||
|
|
|
|||
|
|
### MVP完成の定義
|
|||
|
|
- [ ] カメラで日本酒を撮影できる
|
|||
|
|
- [ ] Gemini 3.0でリアルタイム実況しながら解析
|
|||
|
|
- [ ] キャッチコピーが自動生成される
|
|||
|
|
- [ ] データをHiveに保存できる
|
|||
|
|
- [ ] フル幅カード(写真120x120)で表示
|
|||
|
|
- [ ] 詳細表示できる
|
|||
|
|
- [ ] SafeAreaで見切れない
|
|||
|
|
- [ ] Android 15 (Xiaomi 14T Pro) で動作する
|
|||
|
|
|
|||
|
|
### 最終完成の定義
|
|||
|
|
- [ ] すべての機能が動作
|
|||
|
|
- [ ] フレーバー・マトリックス表示
|
|||
|
|
- [ ] 日本酒・制覇マップ + バッジ
|
|||
|
|
- [ ] Instagram用正方形画像生成
|
|||
|
|
- [ ] 「雑誌のような」デザイン
|
|||
|
|
- [ ] 60fpsの滑らかな動作
|
|||
|
|
- [ ] 「魔法のような」心地よさ
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 📞 進捗報告
|
|||
|
|
|
|||
|
|
各フェーズ完了後に以下を報告してください:
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
Phase 1-1 完了: プロジェクト初期化
|
|||
|
|
Phase 1-2 完了: データモデル
|
|||
|
|
...
|
|||
|
|
|
|||
|
|
問題点:
|
|||
|
|
- XXXでエラーが発生しました
|
|||
|
|
- YYYの実装方法が不明です
|
|||
|
|
|
|||
|
|
質問:
|
|||
|
|
- ZZZはどう実装すべきですか?
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
**頑張ってください!「魔法のような体験」を一緒に作りましょう!🍶✨**
|