ponshu-room-lite/lib/services/draft_service.dart

272 lines
8.3 KiB
Dart
Raw Permalink Normal View History

import 'package:flutter/foundation.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:uuid/uuid.dart';
import '../models/sake_item.dart';
import 'gemini_service.dart';
/// Draft解析待ちアイテム管理サービス
///
/// Phase 1緊急対応: オフライン時に撮影した写真を一時保存し、
/// オンライン復帰時に自動解析する機能を提供します。
///
/// 使用フロー:
/// 1. オフライン時: saveDraft() で写真を一時保存
/// 2. オンライン復帰時: getPendingDrafts() で一覧取得
/// 3. analyzeDraft() で個別解析、または analyzeAllDrafts() で一括解析
class DraftService {
static const _uuid = Uuid();
/// Draft解析待ちアイテムを保存
///
/// オフライン時にカメラで撮影した写真を一時保存します。
/// 写真パスはそのまま保持され、後でAI解析時に使用されます。
///
/// [photoPaths] 保存した写真の絶対パスリスト(ギャラリーに既に保存済みであること)
///
/// Returns: 作成されたDraft SakeItemのkey
///
/// Usage:
/// ```dart
/// final draftKey = await DraftService.saveDraft([imagePath1, imagePath2]);
/// ```
static Future<String> saveDraft(List<String> photoPaths) async {
try {
final box = Hive.box<SakeItem>('sake_items');
// 🔧 FIX: 最初の画像をdraftPhotoPathに、全画像をimagePathsに保存
final firstPhotoPath = photoPaths.isNotEmpty ? photoPaths.first : '';
// Draft用の仮データを作成
final draftItem = SakeItem(
id: _uuid.v4(),
isPendingAnalysis: true,
draftPhotoPath: firstPhotoPath,
displayData: DisplayData(
name: '解析待ち',
brewery: '---',
prefecture: '---',
imagePaths: photoPaths, // 🔧 FIX: すべての画像を保存
rating: null,
),
hiddenSpecs: HiddenSpecs(
description: 'オフライン時に撮影された写真です。オンライン復帰後に自動解析されます。',
tasteStats: {},
flavorTags: [],
),
userData: UserData(
isFavorite: false,
isUserEdited: false,
markup: 3.0,
),
gamification: Gamification(ponPoints: 0),
metadata: Metadata(
createdAt: DateTime.now(),
aiConfidence: null,
),
);
await box.add(draftItem);
debugPrint('📝 Draft saved: ${draftItem.id}');
return draftItem.id;
} catch (e) {
debugPrint('⚠️ Draft save error: $e');
rethrow;
}
}
/// すべての解析待ちDraftを取得
///
/// Returns: 解析待ちアイテムのリスト
///
/// Usage:
/// ```dart
/// final drafts = await DraftService.getPendingDrafts();
/// print('未解析: ${drafts.length}件');
/// ```
static Future<List<SakeItem>> getPendingDrafts() async {
try {
final box = Hive.box<SakeItem>('sake_items');
final allItems = box.values.toList();
final pendingDrafts = allItems.where((item) => item.isPendingAnalysis).toList();
debugPrint('📋 Pending drafts: ${pendingDrafts.length}');
return pendingDrafts;
} catch (e) {
debugPrint('⚠️ Get pending drafts error: $e');
return [];
}
}
/// 解析待ちDraftの件数を取得
///
/// Returns: 解析待ちアイテム数
static Future<int> getPendingCount() async {
final drafts = await getPendingDrafts();
return drafts.length;
}
/// 特定のDraftを解析し、正式なSakeItemに変換
///
/// [itemKey] DraftアイテムのHive key
///
/// Returns: 解析結果のSakeAnalysisResult
///
/// Throws: AI解析エラー、ネットワークエラーなど
///
/// Usage:
/// ```dart
/// try {
/// final result = await DraftService.analyzeDraft(itemKey);
/// print('解析完了: ${result.name}');
/// } catch (e) {
/// print('解析失敗: $e');
/// }
/// ```
static Future<SakeAnalysisResult> analyzeDraft(dynamic itemKey) async {
final box = Hive.box<SakeItem>('sake_items');
final item = box.get(itemKey);
if (item == null) {
throw Exception('Draft item not found: $itemKey');
}
if (!item.isPendingAnalysis) {
throw Exception('Item is not a draft: $itemKey');
}
final photoPath = item.draftPhotoPath;
if (photoPath == null || photoPath.isEmpty) {
throw Exception('Draft has no photo path: $itemKey');
}
debugPrint('🔍 Analyzing draft: $itemKey (${item.displayData.displayName})');
// 🔧 FIX: 複数画像がある場合はすべて使用
final imagePaths = item.displayData.imagePaths;
final pathsToAnalyze = imagePaths.isNotEmpty ? imagePaths : [photoPath];
// Gemini Vision API呼び出し
final geminiService = GeminiService();
final result = await geminiService.analyzeSakeLabel(pathsToAnalyze);
debugPrint('✅ Analysis completed: ${result.name}');
// Draftを正式なアイテムに更新
final updatedItem = item.copyWith(
name: result.name,
brand: result.brand,
prefecture: result.prefecture,
description: result.description,
catchCopy: result.catchCopy,
flavorTags: result.flavorTags,
tasteStats: result.tasteStats,
confidenceScore: result.confidenceScore,
// 新規フィールド
specificDesignation: result.type,
alcoholContent: result.alcoholContent,
polishingRatio: result.polishingRatio,
sakeMeterValue: result.sakeMeterValue,
riceVariety: result.riceVariety,
yeast: result.yeast,
manufacturingYearMonth: result.manufacturingYearMonth,
// Draft状態を解除
isPendingAnalysis: false,
draftPhotoPath: null,
);
// await updatedItem.save(); // Error: This object is currently not in a box.
await box.put(itemKey, updatedItem);
debugPrint('💾 Draft updated to normal item: $itemKey');
return result;
}
/// すべての解析待ちDraftを一括解析
///
/// Returns: {成功件数, 失敗件数, エラーメッセージリスト}
///
/// Usage:
/// ```dart
/// final result = await DraftService.analyzeAllDrafts((progress, total) {
/// print('進捗: $progress / $total');
/// });
/// print('成功: ${result['success']}, 失敗: ${result['failed']}');
/// ```
static Future<Map<String, dynamic>> analyzeAllDrafts({
Function(int progress, int total)? onProgress,
}) async {
final drafts = await getPendingDrafts();
final total = drafts.length;
if (total == 0) {
debugPrint('📭 No pending drafts to analyze');
return {'success': 0, 'failed': 0, 'errors': []};
}
int successCount = 0;
int failedCount = 0;
final List<String> errors = [];
debugPrint('🚀 Analyzing $total drafts...');
for (int i = 0; i < total; i++) {
final draft = drafts[i];
final itemKey = draft.key;
try {
onProgress?.call(i + 1, total);
await analyzeDraft(itemKey);
successCount++;
debugPrint('✅ [$i+1/$total] Success: ${draft.displayData.displayName}');
} catch (e) {
failedCount++;
final errorMsg = '[$i+1/$total] ${draft.displayData.displayName}: $e';
errors.add(errorMsg);
debugPrint('$errorMsg');
// 失敗してもループは継続他のDraftを解析
}
}
debugPrint('🎉 Batch analysis completed: $successCount成功, $failedCount失敗');
return {
'success': successCount,
'failed': failedCount,
'errors': errors,
};
}
/// Draft を削除(解析を諦める場合)
///
/// [itemKey] DraftアイテムのHive key
///
/// Usage:
/// ```dart
/// await DraftService.deleteDraft(itemKey);
/// ```
static Future<void> deleteDraft(dynamic itemKey) async {
final box = Hive.box<SakeItem>('sake_items');
final item = box.get(itemKey);
if (item != null && item.isPendingAnalysis) {
await box.delete(itemKey);
debugPrint('🗑️ Draft deleted: $itemKey');
}
}
/// すべてのDraftを削除
///
/// Returns: 削除件数
static Future<int> deleteAllDrafts() async {
final drafts = await getPendingDrafts();
final count = drafts.length;
for (final draft in drafts) {
await deleteDraft(draft.key);
}
debugPrint('🗑️ All drafts deleted: $count件');
return count;
}
}