561 lines
18 KiB
Markdown
561 lines
18 KiB
Markdown
# 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<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**: テーマカラー適用(推奨)
|
||
```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<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分
|