# 🚨 Critical Bug Report: 画像誤削除問題 **発見日**: 2026-01-22 **影響**: ⚠️ ユーザーデータ消失 **ステータス**: ✅ 修正完了 --- ## 📊 問題の概要 ### 症状 - カード一覧・詳細画面で日本酒の写真が表示されない - ストレージ: 558MB → 409MB(Androidキャッシュクリア後) - 一部の画像ファイルが消失 ### 影響範囲 - **Critical**: ユーザーが撮影した日本酒の写真が削除された - **被害画像**: `_gallery.jpg`, `_compressed.jpg` を含むすべてのファイル - **被害者**: 一時ファイルクリーンアップを実行したユーザー --- ## 🔍 根本原因 ### バグのあるコード ```dart // lib/services/image_batch_compression_service.dart:157-192 (修正前) static Future<(int, int)> cleanupTempFiles() async { // ❌ 問題: getApplicationDocumentsDirectory() をスキャン(永続ファイルがある場所) final directory = await getApplicationDocumentsDirectory(); final dir = Directory(directory.path); await for (final entity in dir.list()) { if (entity is File) { final fileName = entity.path.split('/').last; // ❌ 問題: _compressed, _gallery を含むすべてのファイルを削除 if (fileName.contains('_compressed') || fileName.contains('_gallery')) { await entity.delete(); // ← 本物の画像を削除! } } } } ``` ### なぜ本物の画像が削除されたのか? #### 1. ギャラリー保存用一時ファイル ```dart // lib/screens/camera_screen.dart:225 (修正前) final String galleryPath = join(directory.path, '${const Uuid().v4()}_gallery.jpg'); // ↑ directory = getApplicationDocumentsDirectory() ``` - `_gallery.jpg` という名前で永続ディレクトリに保存 - 削除処理が失敗した場合、ファイルが残る - `cleanupTempFiles()` で削除される #### 2. 圧縮用一時ファイル ```dart // lib/services/image_compression_service.dart:96 (修正前) static Future _generateCompressedPath(String sourcePath) async { final directory = await getApplicationDocumentsDirectory(); return path.join(directory.path, '${fileName}_compressed$extension'); } ``` - `_compressed.jpg` という名前で永続ディレクトリに保存 - 一括圧縮で使用される - `cleanupTempFiles()` で削除される ### 設計ミス | ディレクトリ | 用途 | 実際の使い方(修正前) | |------------|------|---------------------| | `getApplicationDocumentsDirectory()` | **永続ファイル** | ✅ 本物の画像
❌ 一時ファイルも保存 | | `getTemporaryDirectory()` | **一時ファイル** | ❌ 使われていない | **問題点**: 永続ファイルと一時ファイルが同じディレクトリに混在 --- ## ✅ 修正内容 ### 修正1: `cleanupTempFiles()` のスキャン対象を変更 ```dart // lib/services/image_batch_compression_service.dart:157-192 (修正後) static Future<(int, int)> cleanupTempFiles() async { // ✅ 修正: getTemporaryDirectory() をスキャン(一時ファイルのみ) final directory = await getTemporaryDirectory(); final dir = Directory(directory.path); await for (final entity in dir.list()) { if (entity is File) { final fileName = entity.path.split('/').last; // ✅ 修正: 画像ファイルすべてを削除(一時ディレクトリ内のみ) if (fileName.endsWith('.jpg') || fileName.endsWith('.jpeg') || fileName.endsWith('.png')) { await entity.delete(); // ← 安全! } } } } ``` **変更点**: - `getApplicationDocumentsDirectory()` → `getTemporaryDirectory()` - `contains('_compressed')` → `endsWith('.jpg')`(全画像を削除でOK) ### 修正2: ギャラリー用一時ファイルを `getTemporaryDirectory()` へ ```dart // lib/screens/camera_screen.dart:222-231 (修正後) // ✅ 修正: 一時ファイルは getTemporaryDirectory() に保存 final tempDir = await getTemporaryDirectory(); final String galleryPath = join(tempDir.path, '${const Uuid().v4()}_gallery.jpg'); ``` ### 修正3: 圧縮用一時ファイルを `getTemporaryDirectory()` へ ```dart // lib/services/image_compression_service.dart:94-100 (修正後) static Future _generateCompressedPath(String sourcePath) async { // ✅ 修正: 一時ファイルは getTemporaryDirectory() に保存 final directory = await getTemporaryDirectory(); return path.join(directory.path, '${fileName}_compressed$extension'); } ``` --- ## 📊 修正後のディレクトリ構成 | ディレクトリ | 用途 | 保存されるファイル | |------------|------|------------------| | `getApplicationDocumentsDirectory()` | **永続ファイル** | ✅ 本物の画像(UUID.jpg)
✅ Hive DB | | `getTemporaryDirectory()` | **一時ファイル** | ✅ _gallery.jpg
✅ _compressed.jpg
✅ その他一時ファイル | **メリット**: - 一時ファイルクリーンアップで**絶対に本物の画像が削除されない** - ディレクトリ構成が明確 --- ## 🔒 再発防止策 ### 1. コーディング規約 ```dart // ✅ 永続ファイル(本物の画像) final directory = await getApplicationDocumentsDirectory(); final permanentPath = join(directory.path, '${const Uuid().v4()}.jpg'); // ✅ 一時ファイル(処理後に削除) final tempDirectory = await getTemporaryDirectory(); final tempPath = join(tempDirectory.path, '${const Uuid().v4()}_temp.jpg'); ``` ### 2. クリーンアップ関数のルール - **必ず `getTemporaryDirectory()` のみをスキャン** - `getApplicationDocumentsDirectory()` をスキャンしない ### 3. 一時ファイルの命名規則 - 接尾辞不要(ディレクトリで分離) - `getTemporaryDirectory()` 内のすべてのファイルは削除OK --- ## 🚨 ユーザーへの影響と対応 ### 影響を受けたユーザー - 一時ファイルクリーンアップを実行したユーザー - **症状**: 一部の日本酒写真が表示されない ### 復旧方法 ❌ **復旧不可能** - 削除された画像ファイルは復元できません - バックアップがない場合、データ損失 ### 対応策 1. ユーザーに謝罪 2. 削除された日本酒を再撮影してもらう 3. 今後はバックアップ機能を強化 --- ## 📋 修正ファイル一覧 1. `lib/services/image_batch_compression_service.dart` - `cleanupTempFiles()` を修正 - スキャン対象を `getTemporaryDirectory()` に変更 2. `lib/screens/camera_screen.dart` - ギャラリー用一時ファイルを `getTemporaryDirectory()` に保存 3. `lib/services/image_compression_service.dart` - `_generateCompressedPath()` を修正 - 一時ファイルを `getTemporaryDirectory()` に保存 --- ## ✅ テスト計画 ### 1. 一時ファイルクリーンアップのテスト 1. 日本酒を3枚撮影 2. 開発者メニュー → 一時ファイルクリーンアップ 3. **確認**: 日本酒の写真がすべて表示されることを確認 4. **確認**: ストレージ使用量が増加していないことを確認 ### 2. ストレージ構成の確認 ```bash # Android Debug Bridge (adb) で確認 adb shell run-as com.example.ponshu_room_lite ls /data/user/0/com.example.ponshu_room_lite/app_flutter/ # → 永続ファイルのみ(UUID.jpg) adb shell run-as com.example.ponshu_room_lite ls /data/user/0/com.example.ponshu_room_lite/cache/ # → 一時ファイルのみ(_gallery.jpg, _compressed.jpg) ``` ### 3. 一括圧縮のテスト 1. 既存画像を一括圧縮 2. **確認**: すべての日本酒の写真が表示されることを確認 3. **確認**: ストレージ使用量が削減されたことを確認 --- ## 📝 教訓 ### ❌ やってはいけないこと 1. 永続ファイルと一時ファイルを同じディレクトリに保存 2. ファイル名の接尾辞でファイルタイプを判定 3. `contains()` で部分一致検索して削除 ### ✅ やるべきこと 1. 永続ファイルと一時ファイルを別のディレクトリに保存 2. ディレクトリ構成で責任を分離 3. 削除前に慎重に確認 ### 🎯 今後の改善 1. バックアップ機能の強化(自動バックアップ) 2. 削除前の確認ダイアログ(ファイル一覧表示) 3. ユニットテスト追加(ファイル操作) --- **作成日**: 2026-01-22 **作成者**: Cursor AI **レビュアー**: 必要 **優先度**: 🔴 Critical