ponshu-room-lite/docs/archive/PHASE_A_FIX_PLAN.md

561 lines
18 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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分