# Phase A 緊急修正計画(v1.0.10) ## 🎯 目的 ユーザー体験を著しく阻害している3つの問題を緊急修正 --- ## 📋 修正タスク一覧 ### A1. 酒向タイプ診断「おすすめを見る」ボタン修正 #### 現状の問題 - ボタン名: 「おすすめを見る」 - 実際の動作: 診断結果を保存するだけ - **ユーザーの期待**: おすすめの日本酒が表示される - **実際**: SnackBarで「保存しました」メッセージのみ → **混乱を招く** #### 修正方針 **オプション1**: ボタン名を現状の動作に合わせる(最小限の変更) ```dart // lib/widgets/mbti/mbti_result_card.dart:170 // Before: label: const Text("おすすめを見る"), // After: label: const Text("診断結果を保存"), ``` **オプション2**: おすすめ機能を簡易実装する(時間がある場合) ```dart // lib/screens/placeholders/sommelier_screen.dart:438-446 onShowRecommendations: () { Navigator.pop(dialogContext); ref.read(userProfileProvider.notifier).setSakePersonaMbti(result.type.code); if (!mounted) return; // Filter recommendations based on MBTI type final recommendations = MBTIType.types[result.type.code]?.recommendedStyles ?? ''; final allItems = ref.read(allSakeItemsProvider).value ?? []; final matchedItems = allItems.where((item) { // Simple matching: check if flavor tags or type match recommendations return item.hiddenSpecs.type?.contains(recommendations) ?? false || item.hiddenSpecs.flavorTags.any((tag) => recommendations.contains(tag)); }).toList(); Navigator.of(context).push(MaterialPageRoute( builder: (_) => RecommendationsScreen( mbtiType: result.type.code, recommendations: matchedItems, ), )); }, ``` **推奨**: **オプション1**(ボタン名変更のみ) - 理由: 最小限の変更で混乱を即座に解消 - 将来的に本格的なレコメンド機能を実装する際、ボタンを「おすすめを見る」に戻せばOK #### ファイル - `lib/widgets/mbti/mbti_result_card.dart` (1箇所のみ) --- ### A2. SnackBar Duration 統一 #### 現状の問題 | 箇所 | Duration | 問題 | |------|----------|------| | 解析エラー | **未指定** | ずっと表示され続ける | | API制限 | **未指定** | ずっと表示され続ける | | Draft保存 | 5秒 | OK(やや長い) | | 登録成功 | 4秒/6秒 | バラバラ | | ギャラリー保存失敗 | 1秒 | 短すぎる | #### 修正方針 **すべてのSnackBarに明示的なdurationを指定** #### 基準 ```dart // 成功系(通常) Duration(seconds: 3) // 成功系(重要情報あり: バッジ、レベルアップ等) Duration(seconds: 4) // 警告系 Duration(seconds: 4) // エラー系 Duration(seconds: 5) ``` #### 修正箇所リスト ##### camera_screen.dart ```dart // 1. API利用制限エラー (Line 183-186) ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('AI利用制限中です。あと${remaining.inSeconds}秒お待ちください。'), duration: const Duration(seconds: 5), // 追加 backgroundColor: appColors.error, // 追加 ), ); // 2. ギャラリー保存失敗 (Line 230-232) ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('ギャラリー保存に失敗しました: $e'), duration: const Duration(seconds: 4), // 変更(1秒→4秒) backgroundColor: appColors.warning, // 追加 ), ); // 3. 解析エラー (Line 286-288) ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('解析エラー: $e'), duration: const Duration(seconds: 5), // 追加 backgroundColor: appColors.error, // 追加 ), ); ``` ##### pending_analysis_screen.dart ```dart // 1. オフライン警告 (Line 63-71) ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('オフライン状態です。インターネット接続を確認してください。'), backgroundColor: Colors.red, duration: Duration(seconds: 4), // 追加 ), ); // 2. 一括解析成功 (Line 105) ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('✅ すべてのDraft($successCount件)を解析しました!'), backgroundColor: Colors.green, duration: const Duration(seconds: 3), // 追加 ), ); // 3. Draft削除成功 (Line 209) ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Draftを削除しました'), duration: Duration(seconds: 3), // 追加 ), ); // 4. Draft削除エラー (Line 213) ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('削除エラー: $e'), duration: const Duration(seconds: 5), // 追加 ), ); // 5. 全件削除成功 (Line ~260頃、正確な行番号は要確認) ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('$count件のDraftを削除しました'), duration: const Duration(seconds: 3), // 追加 ), ); ``` ##### sommelier_screen.dart ```dart // 1. 診断結果保存 (Line 444-446) ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('「${result.type.title}」として診断結果を保存しました!'), duration: const Duration(seconds: 3), // 追加 ), ); // 2. データ不足エラー (Line 393-398) ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('データ不足です!あと${5 - sakeList.length}本の記録が必要です。'), backgroundColor: Theme.of(context).colorScheme.error, duration: const Duration(seconds: 4), // 追加 ), ); ``` #### ファイル - `lib/screens/camera_screen.dart` (3箇所) - `lib/screens/pending_analysis_screen.dart` (5箇所) - `lib/screens/placeholders/sommelier_screen.dart` (2箇所) **合計**: 10箇所 --- ### A3. camera_screen.dart のテーマカラー修正 #### 現状の問題 1. **撮影後ダイアログ**: `AppTheme.posimaiBlue` 固定 → 和モダンテーマ無視 2. **オフラインSnackBar**: `Colors.orange` 固定 → テーマカラー無視 3. **経験値SnackBar**: `Colors.yellow`, `Colors.greenAccent` 固定 → テーマカラー無視 #### 修正方針 **すべてテーマカラー(`appColors.*`)を使用** ただし、以下の配慮が必要: - **オフライン警告**: オレンジは視覚的に「警告」を意味するため、テーマに関係なくオレンジ系を維持 - **経験値/バッジ**: ゲーミフィケーション演出なので、黄色・緑は維持してもOK(ただし、ダークモードで視認性確保) #### 修正内容 ##### 1. 撮影後ダイアログ (Line 301-328) ```dart Future _handleCapturedImage(String imagePath, {bool fromGallery = false}) async { // ... final appColors = Theme.of(context).extension()!; // 追加 await showDialog( context: context, barrierDismissible: false, builder: (ctx) => AlertDialog( title: Text(fromGallery ? '画像を読み込みました' : '写真を保存しました'), content: const Text('さらに別の面も撮影・追加すると、\nAI解析の精度がアップします!'), actions: [ OutlinedButton( onPressed: () { Navigator.of(context).pop(); _analyzeImages(); }, style: OutlinedButton.styleFrom( foregroundColor: appColors.brandPrimary, // 追加 side: BorderSide(color: appColors.brandPrimary), // 追加 ), child: const Text('解析開始'), ), FilledButton( onPressed: () { Navigator.of(context).pop(); }, style: FilledButton.styleFrom( backgroundColor: appColors.brandPrimary, // 変更(AppTheme.posimaiBlue → appColors.brandPrimary) foregroundColor: Colors.white, ), child: const Text('さらに追加'), ), ], ), ); } ``` ##### 2. オフラインSnackBar (Line 110-131) **Option A**: テーマカラー適用(推奨) ```dart // appColorsにwarningカラーがない場合は、固定色を調整 final warningColor = Theme.of(context).brightness == Brightness.dark ? Colors.orange.shade700 // ダークモード: 濃いオレンジ : Colors.orange.shade600; // ライトモード: 通常オレンジ ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon(LucideIcons.wifiOff, color: Colors.white, size: 16), // 白に変更(視認性) SizedBox(width: 8), Text('オフライン検知', style: TextStyle(fontWeight: FontWeight.bold, color: Colors.white)), ], ), SizedBox(height: 4), Text('写真を「解析待ち」として保存しました。', style: TextStyle(color: Colors.white)), Text('オンライン復帰後、ホーム画面から解析できます。', style: TextStyle(color: Colors.white)), ], ), duration: Duration(seconds: 5), backgroundColor: warningColor, // 変更 ), ); ``` **Option B**: 現状維持(オレンジ固定) - 警告色は universal なので、テーマに関わらず `Colors.orange` でOK - その場合は変更不要 **推奨**: **Option B**(現状維持) - オレンジは「注意・警告」の universal color - テーマ変更の影響を受けない方が視認性が高い ##### 3. 経験値SnackBar (Line 264-273) **Option A**: テーマカラー適用 ```dart final isDark = Theme.of(context).brightness == Brightness.dark; ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: messageWidgets, ), duration: Duration(seconds: newBadges.isNotEmpty ? 6 : 4), backgroundColor: isDark ? Colors.grey.shade800 : null, // ダークモード対応 ), ); // アイコン色も調整 Row( children: [ Icon(LucideIcons.sparkles, color: isDark ? Colors.yellow.shade300 : Colors.yellow, // ダークモード: 薄い黄色 size: 16), // ... ], ) ``` **Option B**: 現状維持(黄色・緑固定) - ゲーミフィケーション演出は色が重要 - 黄色=経験値、緑=バッジ は直感的 **推奨**: **Option B**(現状維持)、ただし**ダークモード対応のみ追加** ```dart final isDark = Theme.of(context).brightness == Brightness.dark; Icon(LucideIcons.sparkles, color: isDark ? Colors.yellow.shade300 : Colors.yellow, size: 16), // ... Text('バッジ獲得: ${badge.name}', style: TextStyle( fontWeight: FontWeight.bold, color: isDark ? Colors.green.shade300 : Colors.greenAccent, )), ``` #### ファイル - `lib/screens/camera_screen.dart` (2箇所: ダイアログ、経験値SnackBar) --- ## 📊 修正サマリー | タスク | 優先度 | 修正箇所数 | 推定時間 | |--------|--------|------------|----------| | A1. ボタン名修正 | 🔥 最高 | 1箇所 | 5分 | | A2. SnackBar Duration | 🔥 最高 | 10箇所 | 30分 | | A3. テーマカラー修正 | 🔥 高 | 2箇所 | 20分 | | **合計** | - | **13箇所** | **約1時間** | --- ## ✅ テストチェックリスト ### A1. ボタン名修正 - [ ] 酒向タイプ診断を実行 - [ ] 結果ダイアログのボタンが「診断結果を保存」になっているか確認 - [ ] ボタン押下 → SnackBarで「保存しました」メッセージ表示 - [ ] ダイアログが閉じることを確認 ### A2. SnackBar Duration - [ ] カメラ撮影 → API制限エラー → 5秒で自動的に消える - [ ] オフライン撮影 → Draft保存 → 5秒で消える - [ ] 解析エラー発生 → 5秒で消える - [ ] Draft一括解析成功 → 3秒で消える - [ ] Draft削除 → 3秒で消える ### A3. テーマカラー修正 - [ ] **爽やかテーマ**: 撮影後ダイアログのボタンが青色 - [ ] **和モダンテーマ**: 撮影後ダイアログのボタンが和モダン色(赤茶系) - [ ] ライトモード: 経験値SnackBar の黄色が視認可能 - [ ] ダークモード: 経験値SnackBar の黄色が薄くなり視認可能 --- ## 🚀 リリース手順 ### 1. 修正実装 ```bash # ブランチ作成(任意) git checkout -b fix/phase-a-v1.0.10 # 修正実施 # ... コード編集 ... # ビルドテスト flutter analyze flutter build apk --release --dart-define=IS_PRO_VERSION=false ``` ### 2. バージョン更新 ```yaml # pubspec.yaml version: 1.0.10+18 ``` ### 3. リリースビルド ```bash # Lite版 flutter build apk --release --dart-define=IS_PRO_VERSION=false cp build/app/outputs/flutter-apk/app-release.apk ponshu-room-lite-v1.0.10-release.apk # Pro版 flutter build apk --release --dart-define=IS_PRO_VERSION=true cp build/app/outputs/flutter-apk/app-release.apk ponshu-room-pro-v1.0.10-release.apk ``` ### 4. リリースノート作成 `RELEASE_NOTES_v1.0.10.md` を作成(テンプレートは v1.0.9 参照) --- ## 📝 注意事項 ### AppColors 拡張が必要な場合 もし `appColors.warning` や `appColors.success` が存在しない場合: ```dart // lib/theme/app_colors.dart に追加 extension AppColors on ThemeData { // ... 既存のプロパティ ... Color get warning => brightness == Brightness.dark ? Colors.orange.shade700 : Colors.orange.shade600; Color get success => brightness == Brightness.dark ? Colors.green.shade700 : Colors.green.shade600; Color get error => colorScheme.error; } ``` --- ## 🔮 将来機能バックログ(Phase D以降) ### D1. おすすめ日本酒機能(さけのわAPI連携) **概要**: MBTI診断結果に基づいて、さけのわDBのAPIと連携してパーソナライズされたおすすめを表示 **実装イメージ**: 1. ユーザーのMBTI診断タイプを取得 2. さけのわAPI で該当タイプに合う特性(甘口/辛口、香り高い、etc.)で検索 3. 検索結果から上位10件を表示 4. ユーザーがタップで詳細閲覧・お気に入り登録可能 **技術的要件**: - さけのわAPI仕様確認(認証、レート制限) - APIキーの安全な管理 - キャッシュ戦略(毎回APIを叩かない) - オフライン時のフォールバック **優先度**: 🔶 中 **推定実装時間**: 4-6時間 --- ### D2. 酒蔵マップの制覇率代替指標 **背景**: 47都道府県の制覇率は心理的ハードルが高く(沖縄、九州の日本酒はレア)、達成感を感じにくい **代替案**: 1. **飲んだ酒蔵数**: 日本には1000以上の酒蔵 → コレクション性 2. **出会った銘柄数**: シンプルな種類カウント(バッジとも連動) 3. **味わいマップ埋め率**: 甘口/辛口、濃醇/淡麗の4象限分布チャート 4. **好みの発見率**: ★4以上の銘柄比率 **推奨**: **飲んだ酒蔵数** + **味わいマップ埋め率** の2つを追加 **技術的要件**: - 酒蔵名の正規化(表記ゆれ対策) - 味わいマップの座標計算ロジック - UI設計(マップ画面に統合) **優先度**: 🔶 中 **推定実装時間**: 3-4時間 --- ### D3. ギャラリー画像の圧縮・永続化 **背景**: Antigravityコードレビューで発見された画像最適化の不備 **現状の問題**: 1. **カメラ撮影**: ✅ 圧縮済み(1024px / 85% JPEG) 2. **ギャラリー選択**: ❌ 未対応 - `XFile.path`(image_pickerの一時キャッシュ)をそのままDBに保存 - 圧縮処理なし - 原寸大の画像をGemini APIに送信 **3つのリスク**: | リスク | 深刻度 | 内容 | |--------|--------|------| | **永続化リスク** | 🔴 高 | OSのキャッシュクリーンアップで画像リンク切れの可能性 | | **API/通信コスト** | 🟡 中 | 数MBの高画質画像を送信 → 解析時間増・モバイル通信量増大 | | **アプリ容量** | 🟡 中 | 表示に不要な高解像度で保存 → 端末容量圧迫 | **修正方針**: ```dart // lib/screens/camera_screen.dart:264-280 Future _pickFromGallery() async { final picker = ImagePicker(); final List images = await picker.pickMultiImage(); if (images.isNotEmpty && mounted) { // 🔧 修正: ギャラリー画像も圧縮 + Documents領域にコピー final directory = await getApplicationDocumentsDirectory(); for (var img in images) { // 1. 一時パスから圧縮 final finalPath = join(directory.path, '${const Uuid().v4()}.jpg'); final compressedPath = await ImageCompressionService.compressForGemini( img.path, targetPath: finalPath ); // 2. 圧縮済み永続パスを追加 _capturedImages.add(compressedPath); } // 既存の通知処理... } } ``` **期待効果**: - ✅ 画像リンク切れ防止(永続的なDocuments領域に保存) - ✅ API送信サイズ削減(カメラ撮影と同じ1024px / 85% JPEG) - ✅ 端末容量節約 **技術的要件**: - ImageCompressionService の再利用(既存コード) - 一時ファイルの削除処理(オプション) - エラーハンドリング(圧縮失敗時のフォールバック) **検証項目**: - [ ] ギャラリーから大容量画像(5MB以上)を選択 → 圧縮されてDB保存 - [ ] アプリ再起動後も画像が正常に表示される - [ ] 圧縮後のファイルサイズがカメラ撮影時と同等(数百KB程度) **優先度**: 🔶 中(現在は機能的に動作しているが、データ安定性に問題) **推定実装時間**: 30分 **発見者**: Antigravity(コードレビュアー) **報告日**: 2026年1月31日 --- **計画作成日**: 2026年1月31日 **最終更新日**: 2026年1月31日(Phase C完了後、D3追加) **対象バージョン**: v1.0.10 **推定実装時間**: Phase A-C約2時間 / Phase D以降7.5-10.5時間 **推定テスト時間**: 約30分