# 通知設計書 39-画像アップロード成功

## 概要

本ドキュメントは、InvenTreeモバイルアプリケーションにおける「画像アップロード成功」通知の設計仕様を定義するものです。この通知は、パーツ画像のアップロードが正常に完了した際にユーザーに表示されるスナックバー通知です。

### 本通知の処理概要

この通知は、ユーザーがパーツに対して画像をアップロード（または更新）する際、クロッピング処理を経てサーバーへの画像送信が成功した場合にフィードバックを提供する機能です。

**業務上の目的・背景**：パーツ画像はInvenTreeにおいて重要な視覚的識別情報であり、部品の検索や確認に活用されます。画像アップロードは選択、クロッピング、送信という複数のステップを経るため、最終的な成功確認をユーザーに明確に伝える必要があります。

**通知の送信タイミング**：`PartImageWidget._processImageWithCropping()` メソッドでクロッピング後の画像をサーバーにアップロードした後、`part.uploadImage()` がtrueを返した場合に通知が発生します。

**通知の受信者**：通知は、アプリで画像アップロード操作を実行している現在のユーザーに対してのみ表示されます。

**通知内容の概要**：国際化対応されたメッセージ「Image uploaded」（または相当するローカライズ文字列）がスナックバーとして画面下部に表示されます。成功を示すライトグリーンの背景とチェックアイコンが付加されます。

**期待されるアクション**：ユーザーは画像アップロードが成功したことを確認し、パーツ詳細画面で新しい画像を確認することが期待されます。

## 通知種別

アプリ内通知（スナックバー）

## 送信仕様

### 基本情報

| 項目 | 内容 |
|-----|------|
| 送信方式 | 同期 |
| 優先度 | 中 |
| リトライ | なし |

### 送信先決定ロジック

アプリの現在アクティブなユーザーに対してのみ表示されます。編集権限（canEdit）を持つユーザーのみがアップロード機能にアクセスできます。

## 通知テンプレート

### スナックバー通知の場合

| 項目 | 内容 |
|-----|------|
| 背景色 | ライトグリーン（Colors.lightGreen） |
| アイコン | TablerIcons.circle_check |
| 表示時間 | 5秒 |
| 閉じるボタン | あり |

### 本文テンプレート

```
{L10().uploadSuccess}
```

実際の表示例（英語）：
```
Image uploaded
```

### 添付ファイル

該当なし

## テンプレート変数

| 変数名 | 説明 | データ取得元 | 必須 |
|--------|------|-------------|-----|
| uploadSuccess | アップロード成功メッセージ | L10()国際化リソース | Yes |

## 送信トリガー・条件

### トリガー一覧

| トリガー種別 | トリガーイベント | 送信条件 | 説明 |
|------------|----------------|---------|------|
| 画面操作 | _processImageWithCropping() | result == true | パーツ画像アップロードの成功時 |

### 送信抑止条件

| 条件 | 説明 |
|-----|------|
| アップロード失敗 | uploadImage()がfalseを返した場合は失敗通知が表示される |
| クロッピングキャンセル | ユーザーがクロッピングダイアログをキャンセルした場合 |

## 処理フロー

### 送信フロー

```mermaid
flowchart TD
    A[画像アップロードボタン押下] --> B[FilePickerDialog.pickFile]
    B --> C{ファイル選択?}
    C -->|なし| D[処理終了]
    C -->|あり| E[_processImageWithCropping呼び出し]
    E --> F[画像読み込み]
    F --> G[クロッピングダイアログ表示]
    G --> H{クロッピング確定?}
    H -->|キャンセル| I[元画像使用]
    H -->|確定| J[クロッピング画像使用]
    I --> K[一時ファイル作成]
    J --> K
    K --> L[part.uploadImage実行]
    L --> M{アップロード成功?}
    M -->|成功| N[showSnackIcon success: true]
    M -->|失敗| O[showSnackIcon success: false]
    N --> P[一時ファイル削除]
    O --> P
    P --> Q[refresh画面更新]
```

## データベース参照・更新仕様

### 参照テーブル一覧

該当なし（クライアントサイドのみの処理）

### 更新テーブル一覧

サーバー側でPartテーブルのimageフィールドが更新される

## エラー処理

### エラーケース一覧

| エラー種別 | 発生条件 | 対処方法 |
|----------|---------|---------|
| アップロード失敗 | サーバーエラー、ネットワークエラー | 失敗通知（No.40）を表示 |
| 画像処理エラー | クロッピングやファイル操作で例外発生 | エラー通知を表示 |
| コンテキスト不在 | Flutterコンテキストが無効 | 通知をスキップ |

### リトライ仕様

| 項目 | 内容 |
|-----|------|
| リトライ回数 | 0（ユーザーによる手動再試行） |
| リトライ間隔 | 該当なし |
| リトライ対象エラー | 該当なし |

## 配信設定

### レート制限

| 項目 | 内容 |
|-----|------|
| 1分あたり上限 | 制限なし |
| 1日あたり上限 | 制限なし |

### 配信時間帯

制限なし（24時間対応）

## セキュリティ考慮事項

- アップロードされた画像ファイル名は通知に含まれない
- 編集権限を持つユーザーのみがアップロード機能を利用可能
- 一時ファイルはアップロード後に削除される
- 認証済みユーザーのみがアプリを使用可能

## 備考

- 画像アップロード前にクロッピング機能が提供され、ユーザーは画像の一部を選択可能
- クロッピングをキャンセルした場合は元の画像がそのままアップロードされる
- 一時ファイルは `getTemporaryDirectory()` に作成され、アップロード後に削除される
- 国際化対応により、アプリの言語設定に応じたメッセージが表示される

---

## コードリーディングガイド

本通知を理解するために参照すべきファイルと、推奨する読み解き順序を以下に示す。

### 推奨読解順序

#### Step 1: データ構造を理解する

まず、InvenTreePartクラスとuploadImage()メソッドを理解することが重要。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | part.dart | `lib/inventree/part.dart` | InvenTreePartクラス、uploadImage()メソッド |

**読解のコツ**: uploadImage()はbool値を返し、成功/失敗を示す

#### Step 2: エントリーポイントを理解する

_processImageWithCropping()メソッドでの画像処理と通知を確認。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | part_image_widget.dart | `lib/widget/part/part_image_widget.dart` | _processImageWithCropping()メソッド（行37-92） |

**主要処理フロー**:
1. **行38-39**: 画像ファイルの読み込み
2. **行42-62**: クロッピングダイアログの表示
3. **行64-66**: クロッピング結果の適用
4. **行69-72**: 一時ファイルの作成
5. **行75**: `part.uploadImage(tempFile)` でアップロード実行
6. **行78-80**: 一時ファイルの削除
7. **行82-86**: 結果に応じてshowSnackIcon呼び出し
8. **行85**: `showSnackIcon(L10().uploadSuccess, success: true)` - 成功時
9. **行88**: refresh(context)で画面更新

#### Step 3: アップロードボタンのトリガーを理解する

appBarActions()でのアップロードボタンを確認。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | part_image_widget.dart | `lib/widget/part/part_image_widget.dart` | appBarActions()メソッド（行136-164） |

### プログラム呼び出し階層図

```
PartImageWidget  [lib/widget/part/part_image_widget.dart]
    │
    ├─ appBarActions()  [行136]
    │      └─ IconButton(file_upload)
    │             └─ FilePickerDialog.pickFile()
    │                    └─ _processImageWithCropping()呼び出し
    │
    └─ _processImageWithCropping()  [行37]
           │
           ├─ imageFile.readAsBytes()  [行39]
           │
           ├─ showDialog (ImageCropperWidget)  [行42]
           │      └─ クロッピングUI
           │
           ├─ tempFile.writeAsBytes()  [行72]
           │      └─ 一時ファイル作成
           │
           ├─ part.uploadImage()  [行75]
           │      └─ API uploadFile()
           │
           ├─ tempFile.delete()  [行78-80]
           │
           ├─ showSnackIcon()  [行85 or 83]
           │      └─ L10().uploadSuccess or L10().uploadFailed
           │
           └─ refresh()  [行88]
```

### データフロー図

```
[入力]                     [処理]                              [出力]

画像ファイル       ───▶  _processImageWithCropping      ───▶  スナックバー表示
                          ├─ 画像読み込み                      │
                          ├─ クロッピング                      └─ 「Image uploaded」
                          ├─ 一時ファイル作成                     メッセージ
                          ├─ uploadImage
                          ├─ 一時ファイル削除
                          └─ refresh
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| part_image_widget.dart | `lib/widget/part/part_image_widget.dart` | ソース | PartImageWidget、_processImageWithCropping()メソッド |
| part.dart | `lib/inventree/part.dart` | ソース | InvenTreePartクラス、uploadImage()メソッド |
| image_cropper.dart | `lib/widget/part/image_cropper.dart` | ソース | ImageCropperWidget |
| api.dart | `lib/api.dart` | ソース | uploadFile()メソッド |
| snacks.dart | `lib/widget/snacks.dart` | ソース | showSnackIcon関数の実装 |
| l10.dart | `lib/l10.dart` | ソース | 国際化ヘルパー関数L10() |
| fields.dart | `lib/widget/fields.dart` | ソース | FilePickerDialog |
