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はどう実装すべきですか?
|
||
```
|
||
|
||
---
|
||
|
||
**頑張ってください!「魔法のような体験」を一緒に作りましょう!🍶✨**
|