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 saveDraft(List photoPaths) async { try { final box = Hive.box('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> getPendingDrafts() async { try { final box = Hive.box('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 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 analyzeDraft(dynamic itemKey) async { final box = Hive.box('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> 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 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 deleteDraft(dynamic itemKey) async { final box = Hive.box('sake_items'); final item = box.get(itemKey); if (item != null && item.isPendingAnalysis) { await box.delete(itemKey); debugPrint('🗑️ Draft deleted: $itemKey'); } } /// すべてのDraftを削除 /// /// Returns: 削除件数 static Future 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; } }