18 KiB
Phase A 緊急修正計画(v1.0.10)
🎯 目的
ユーザー体験を著しく阻害している3つの問題を緊急修正
📋 修正タスク一覧
A1. 酒向タイプ診断「おすすめを見る」ボタン修正
現状の問題
- ボタン名: 「おすすめを見る」
- 実際の動作: 診断結果を保存するだけ
- ユーザーの期待: おすすめの日本酒が表示される
- 実際: SnackBarで「保存しました」メッセージのみ → 混乱を招く
修正方針
オプション1: ボタン名を現状の動作に合わせる(最小限の変更)
// lib/widgets/mbti/mbti_result_card.dart:170
// Before:
label: const Text("おすすめを見る"),
// After:
label: const Text("診断結果を保存"),
オプション2: おすすめ機能を簡易実装する(時間がある場合)
// 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を指定
基準
// 成功系(通常)
Duration(seconds: 3)
// 成功系(重要情報あり: バッジ、レベルアップ等)
Duration(seconds: 4)
// 警告系
Duration(seconds: 4)
// エラー系
Duration(seconds: 5)
修正箇所リスト
camera_screen.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
// 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
// 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 のテーマカラー修正
現状の問題
- 撮影後ダイアログ:
AppTheme.posimaiBlue固定 → 和モダンテーマ無視 - オフラインSnackBar:
Colors.orange固定 → テーマカラー無視 - 経験値SnackBar:
Colors.yellow,Colors.greenAccent固定 → テーマカラー無視
修正方針
すべてテーマカラー(appColors.*)を使用
ただし、以下の配慮が必要:
- オフライン警告: オレンジは視覚的に「警告」を意味するため、テーマに関係なくオレンジ系を維持
- 経験値/バッジ: ゲーミフィケーション演出なので、黄色・緑は維持してもOK(ただし、ダークモードで視認性確保)
修正内容
1. 撮影後ダイアログ (Line 301-328)
Future<void> _handleCapturedImage(String imagePath, {bool fromGallery = false}) async {
// ...
final appColors = Theme.of(context).extension<AppColors>()!; // 追加
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: テーマカラー適用(推奨)
// 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: テーマカラー適用
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(現状維持)、ただしダークモード対応のみ追加
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. 修正実装
# ブランチ作成(任意)
git checkout -b fix/phase-a-v1.0.10
# 修正実施
# ... コード編集 ...
# ビルドテスト
flutter analyze
flutter build apk --release --dart-define=IS_PRO_VERSION=false
2. バージョン更新
# pubspec.yaml
version: 1.0.10+18
3. リリースビルド
# 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 が存在しない場合:
// 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と連携してパーソナライズされたおすすめを表示
実装イメージ:
- ユーザーのMBTI診断タイプを取得
- さけのわAPI で該当タイプに合う特性(甘口/辛口、香り高い、etc.)で検索
- 検索結果から上位10件を表示
- ユーザーがタップで詳細閲覧・お気に入り登録可能
技術的要件:
- さけのわAPI仕様確認(認証、レート制限)
- APIキーの安全な管理
- キャッシュ戦略(毎回APIを叩かない)
- オフライン時のフォールバック
優先度: 🔶 中 推定実装時間: 4-6時間
D2. 酒蔵マップの制覇率代替指標
背景: 47都道府県の制覇率は心理的ハードルが高く(沖縄、九州の日本酒はレア)、達成感を感じにくい
代替案:
- 飲んだ酒蔵数: 日本には1000以上の酒蔵 → コレクション性
- 出会った銘柄数: シンプルな種類カウント(バッジとも連動)
- 味わいマップ埋め率: 甘口/辛口、濃醇/淡麗の4象限分布チャート
- 好みの発見率: ★4以上の銘柄比率
推奨: 飲んだ酒蔵数 + 味わいマップ埋め率 の2つを追加
技術的要件:
- 酒蔵名の正規化(表記ゆれ対策)
- 味わいマップの座標計算ロジック
- UI設計(マップ画面に統合)
優先度: 🔶 中 推定実装時間: 3-4時間
D3. ギャラリー画像の圧縮・永続化
背景: Antigravityコードレビューで発見された画像最適化の不備
現状の問題:
- カメラ撮影: ✅ 圧縮済み(1024px / 85% JPEG)
- ギャラリー選択: ❌ 未対応
XFile.path(image_pickerの一時キャッシュ)をそのままDBに保存- 圧縮処理なし
- 原寸大の画像をGemini APIに送信
3つのリスク:
| リスク | 深刻度 | 内容 |
|---|---|---|
| 永続化リスク | 🔴 高 | OSのキャッシュクリーンアップで画像リンク切れの可能性 |
| API/通信コスト | 🟡 中 | 数MBの高画質画像を送信 → 解析時間増・モバイル通信量増大 |
| アプリ容量 | 🟡 中 | 表示に不要な高解像度で保存 → 端末容量圧迫 |
修正方針:
// lib/screens/camera_screen.dart:264-280
Future<void> _pickFromGallery() async {
final picker = ImagePicker();
final List<XFile> 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分