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

18 KiB
Raw Permalink Blame History

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 のテーマカラー修正

現状の問題

  1. 撮影後ダイアログ: AppTheme.posimaiBlue 固定 → 和モダンテーマ無視
  2. オフラインSnackBar: Colors.orange 固定 → テーマカラー無視
  3. 経験値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.warningappColors.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と連携してパーソナライズされたおすすめを表示

実装イメージ:

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