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