./knowledge-base/docs/改修要件定義書_時代項目シンプル化_v1.0.md

# 時代考証データベース 改修要件定義書 — 時代項目シンプル化 v1.0

---

## 📋 目次

1. [改修概要](#1-改修概要)
2. [データモデル変更](#2-データモデル変更)
3. [バックエンド改修](#3-バックエンド改修)
4. [フロントエンド改修](#4-フロントエンド改修)
5. [データ移行](#5-データ移行)
6. [影響範囲サマリ](#6-影響範囲サマリ)

---

## 1. 改修概要

### 1.1 背景・目的

現行システムでは、ドキュメントに付与される「時代」メタ項目が**西暦年の範囲**として格納される複雑な仕組みになっている。
具体的には、固定ラベル(`user_era_labels`)と展開年リスト(`user_era_years`)の2フィールドに分散しており、
フロントエンド〜バックエンド〜DBの各層で相互変換処理が必要だった。

本改修ではこれを廃止し、**9種類の固定ラベル文字列の配列** 1フィールドに統一することで、
実装の複雑性を大幅に削減し、メンテナンス性を向上させる。

### 1.2 新しい時代ラベル定義(全9種)

| ID                 | ラベル             | 備考 |
| ------------------ | ------------------ | ---- |
| `muromachi_before` | 室町以前           |      |
| `edo_early`        | 江戸時代前期       |      |
| `edo_late`         | 江戸時代後期       |      |
| `bakumatsu`        | 幕末               |      |
| `meiji`            | 明治               |      |
| `taisho`           | 大正               |      |
| `showa_prewar`     | 昭和(戦前・戦中) |      |
| `showa_postwar`    | 昭和(戦後)       |      |
| `heisei_reiwa`     | 平成~             |      |

### 1.3 基本方針

- アップロード時に上記9ラベルから**複数選択**できる
- AIによる自動判定も引き続き利用可能。AIも上記9ラベルから**複数**返却できる
- 既存の西暦年ベース仕様(`user_era_years` 等)は完全廃止。移行スクリプトで変換後、旧フィールドを削除する
- 旧仕様・新仕様の資料が混在することは想定しない。すべて新仕様で統一する

---

## 2. データモデル変更

### 2.1 DynamoDB フィールド変更

**対象テーブル**: `historical-research-processing-status`

#### 廃止フィールド

| フィールド名      | 型               | 内容                                        |
| ----------------- | ---------------- | ------------------------------------------- |
| `user_era_labels` | `string[]`       | 固定3ラベル(室町以前・江戸前期・江戸後期) |
| `user_era_years`  | `number[]`       | 1853年以降を年単位で展開したリスト          |
| `user_era_start`  | `number \| null` | 展開年の最小値                              |
| `user_era_end`    | `number \| null` | 展開年の最大値                              |

#### 新設フィールド

| フィールド名 | 型         | 内容                                                      |
| ------------ | ---------- | --------------------------------------------------------- |
| `era_labels` | `string[]` | 9ラベルから選択した文字列の配列(例: `["明治", "大正"]`) |

### 2.2 OpenSearch インデックス フィールド変更

**対象インデックス**: `historical-research-pages`

#### 廃止フィールド

| フィールド名      | 型        |
| ----------------- | --------- |
| `user_era_labels` | `keyword` |
| `user_era_years`  | `integer` |
| `user_era_start`  | `integer` |
| `user_era_end`    | `integer` |

#### 新設フィールド

| フィールド名 | 型        | 備考                                           |
| ------------ | --------- | ---------------------------------------------- |
| `era_labels` | `keyword` | 複数値フィールド(配列をそのままインデックス) |

### 2.3 era-list.json 変更

#### 現行仕様

```json
[
  {
    "id": "muromachi_before",
    "label": "室町以前",
    "year_range": { "start": 0, "end": 1599 },
    "is_fixed_label": true
  },
  ...
]
```

#### 改修後仕様(シンプル化)

```json
[
  { "id": "muromachi_before", "label": "室町以前" },
  { "id": "edo_early", "label": "江戸時代前期" },
  { "id": "edo_late", "label": "江戸時代後期" },
  { "id": "bakumatsu", "label": "幕末" },
  { "id": "meiji", "label": "明治" },
  { "id": "taisho", "label": "大正" },
  { "id": "showa_prewar", "label": "昭和(戦前・戦中)" },
  { "id": "showa_postwar", "label": "昭和(戦後)" },
  { "id": "heisei_reiwa", "label": "平成~" }
]
```

---

## 3. バックエンド改修

### 3.1 共通削除処理

以下の関数は `upload-handler`・`pdf-splitter`・`image-analyzer`・`maintenance-handler` の4 Lambdaに重複して存在するため、すべて削除する。

| 関数名                                     | 内容                                                               |
| ------------------------------------------ | ------------------------------------------------------------------ |
| `extract_era_fields(era_entries)`          | `era_entries` → `user_era_labels` / `user_era_years` 変換          |
| `ai_era_to_entries(ai_era_text, era_list)` | AI自由記述テキスト → `era_entries` 変換(`image-analyzer`のみ)    |
| `_years_to_era_labels(years)`              | 年リスト → ラベル配列変換(`search-api`のみ)                      |
| `_build_era_entries_from_stored(item)`     | 保存済みデータ → `era_entries` 逆変換(`maintenance-handler`のみ) |

### 3.2 upload-handler Lambda

**ファイル**: `lambda/upload-handler/handler.py`

#### 変更点

| 項目                                   | 現行                                               | 改修後                                        |
| -------------------------------------- | -------------------------------------------------- | --------------------------------------------- |
| リクエストボディのera項目              | `era_entries: list[dict]`(ラベル+年範囲の構造体) | `era_labels: list[str]`(ラベル文字列の配列) |
| `ai_era_mode=false` 時のバリデーション | `era_entries` が空なら400エラー                    | `era_labels` が空なら400エラー                |
| DynamoDB保存                           | `extract_era_fields()` を呼び出して変換後に保存    | `era_labels` をそのまま保存                   |

#### 削除関数

- `extract_era_fields()`

### 3.3 pdf-splitter Lambda

**ファイル**: `lambda/pdf-splitter/handler.py`

#### 変更点

| 項目                          | 現行                                                                  | 改修後                       |
| ----------------------------- | --------------------------------------------------------------------- | ---------------------------- |
| メタデータから取得するera項目 | `era_entries`(構造体リスト)                                         | `era_labels`(文字列リスト) |
| DynamoDB保存フィールド        | `user_era_labels`, `user_era_years`, `user_era_start`, `user_era_end` | `era_labels` のみ            |
| OpenSearch保存フィールド      | 同上                                                                  | `era_labels` のみ            |

#### 削除関数

- `extract_era_fields()`

### 3.4 image-analyzer Lambda

**ファイル**: `lambda/image-analyzer/handler.py`

#### 変更点①:AI判定結果の受け取り

| 項目             | 現行                                                     | 改修後                                                 |
| ---------------- | -------------------------------------------------------- | ------------------------------------------------------ |
| AIへの出力指示   | `ai_era: string`(自由記述例: "昭和40年代(1965年〜)") | `ai_era_labels: string[]`(9ラベルの列挙から複数選択) |
| AI出力の変換処理 | `ai_era_to_entries()` → `extract_era_fields()` の2段変換 | 変換不要(`ai_era_labels` をそのまま利用)             |

#### 変更点②:ドキュメント保存フィールド

| 現行                                                                  | 改修後            |
| --------------------------------------------------------------------- | ----------------- |
| `user_era_labels`, `user_era_years`, `user_era_start`, `user_era_end` | `era_labels` のみ |

#### 変更点③:プロンプト内のera説明文

`ai_era_mode=true` の場合のプロンプト内変数 `{era}` が現行では `"不明(AIが判断します)"` となっているが、
改修後は `"AIが以下の9ラベルから判断します:室町以前/江戸時代前期/江戸時代後期/幕末/明治/大正/昭和(戦前・戦中)/昭和(戦後)/平成~"` に変更する。

#### 削除関数

- `extract_era_fields()`
- `ai_era_to_entries()`

### 3.5 maintenance-handler Lambda

**ファイル**: `lambda/maintenance-handler/handler.py`

#### 変更点

| 項目                               | 現行                                                                  | 改修後                      |
| ---------------------------------- | --------------------------------------------------------------------- | --------------------------- |
| PUT エンドポイントの受付フィールド | `era_entries: list[dict]`                                             | `era_labels: list[str]`     |
| DynamoDB更新フィールド             | `user_era_labels`, `user_era_years`, `user_era_start`, `user_era_end` | `era_labels` のみ           |
| OpenSearch更新フィールド           | 同上                                                                  | `era_labels` のみ           |
| 既存データ読み込み時の変換         | `_build_era_entries_from_stored()` で `era_entries` に変換            | `era_labels` をそのまま返す |

#### 削除関数

- `_extract_era_fields()`
- `_build_era_entries_from_stored()`

### 3.6 search-api Lambda

**ファイル**: `lambda/search-api/handler.py`

#### 変更点①:eraフィルタークエリ

| 項目             | 現行                                                          | 改修後                                     |
| ---------------- | ------------------------------------------------------------- | ------------------------------------------ |
| ラベル一致       | `user_era_labels.keyword` への `term` クエリ                  | `era_labels` への `term` クエリ            |
| 年範囲フィルター | `user_era_years` への `terms` + `user_era_start/end` のレンジ | 廃止(`era_labels` の `terms` クエリのみ) |
| 複数選択時の条件 | 単一値のみ対応                                                | `terms` クエリ(OR条件、複数値対応)       |
| 未設定フィルター | `user_era_labels` も `user_era_years` も存在しない条件        | `era_labels` が存在しない条件              |

#### 変更点②:検索結果の era フィールド組み立て

| 現行                                                              | 改修後                      |
| ----------------------------------------------------------------- | --------------------------- |
| `user_era_labels` + `_years_to_era_labels(user_era_years)` を合成 | `era_labels` をそのまま返す |

#### 削除関数

- `_years_to_era_labels()`

### 3.7 AI JSON スキーマ変更

**ファイル**: `prompts/image-analysis/json-schema.json`

#### 変更点

```json
// 現行
"ai_era": {
  "type": "string",
  "description": "AIが判定した時代(例: 昭和40年代(1965年~1974年))"
}

// 改修後
"ai_era_labels": {
  "type": "array",
  "items": {
    "type": "string",
    "enum": [
      "室町以前", "江戸時代前期", "江戸時代後期",
      "幕末", "明治", "大正",
      "昭和(戦前・戦中)", "昭和(戦後)", "平成~"
    ]
  },
  "description": "AIが判定した時代ラベル。上記のenum値から該当するものをすべて選択してください。"
}
```

---

## 4. フロントエンド改修

### 4.1 型定義 (`types/index.ts`)

#### 廃止する型・フィールド

| 型                            | フィールド/内容                                                               |
| ----------------------------- | ----------------------------------------------------------------------------- |
| `EraEntry` インターフェース   | `{ label: string; start_year: number; end_year: number }` — 完全廃止          |
| `EraMaster` インターフェース  | `year_range`, `is_fixed_label` フィールドを削除(`id`, `label` のみに簡略化) |
| `PageMetadata.era_entries`    | `era_labels: string[]` に変更                                                 |
| `SearchResultItem.era_labels` | `string[]` に維持(フィールド名変更なし)                                     |
| `SearchResultItem.era_years`  | 廃止                                                                          |
| `SearchResultItem.era_start`  | 廃止                                                                          |
| `SearchResultItem.era_end`    | 廃止                                                                          |

#### 追加・変更する型

```typescript
// 改修後の EraMaster
export interface EraMaster {
  id: string;
  label: string;
}

// 改修後の PageMetadata(era関連)
export interface PageMetadata {
  era_labels: string[]; // 変更: era_entries → era_labels
  ai_era_mode?: boolean; // 変更なし
  // ... その他フィールド
}

// 改修後の SearchResultItem(era関連)
export interface SearchResultItem {
  era_labels: string[]; // 変更なし(中身がシンプル化)
  // era_years, era_start, era_end を削除
  // ... その他フィールド
}
```

### 4.2 eraUtils.ts 変更

**ファイル**: `frontend/src/utils/eraUtils.ts`

#### 廃止するもの

- `GENGOU_LIST` 定数(元号→西暦オフセットリスト)
- `ERA_YEAR_BASED_LIST` 定数(年範囲付き時代リスト)
- 年変換関連ユーティリティ関数すべて

#### 残すもの

```typescript
// 9ラベルの定数配列(これが唯一のラベル定義)
export const ERA_LABELS = [
  "室町以前",
  "江戸時代前期",
  "江戸時代後期",
  "幕末",
  "明治",
  "大正",
  "昭和(戦前・戦中)",
  "昭和(戦後)",
  "平成~",
] as const;

export type EraLabel = (typeof ERA_LABELS)[number];
```

### 4.3 EraSelector.tsx 完全書き換え

**ファイル**: `frontend/src/components/EraSelector.tsx`

#### 現行コンポーネントの問題点

- 固定ラベル3択 + 「それ以降」展開 + 年入力フィールドという複雑な2段UI
- `EraEntry[]`(ラベル+年範囲構造体)を状態として管理
- initEraStateFromStored / initEraStateFromEntries の2種類の初期化関数

#### 改修後のコンポーネント仕様

```typescript
interface Props {
  value: string[]; // 選択済みのラベル配列
  onChange: (labels: string[]) => void;
  disabled?: boolean;
}
```

- **UI**: `ERA_LABELS` の9項目をチェックボックスリストで表示
- 複数選択可(制限なし)
- 年入力フィールドは完全廃止
- `initEraFromLabels(storedLabels: string[]): string[]` のシンプルな初期化関数のみ

#### 廃止するエクスポート

- `ERA_SELECTION_OPTIONS` 定数
- `initEraStateFromStored()` 関数
- `initEraStateFromEntries()` 関数

### 4.4 UploadPage.tsx 変更

**ファイル**: `frontend/src/pages/UploadPage.tsx`

| 項目                     | 現行                                          | 改修後                             |
| ------------------------ | --------------------------------------------- | ---------------------------------- |
| era状態                  | `eraEntries: EraEntry[]`                      | `eraLabels: string[]`              |
| EraSelector props        | `initialEntries` / `onChange` が `EraEntry[]` | `value` / `onChange` が `string[]` |
| buildMetadata の era項目 | `era_entries: eraEntries`                     | `era_labels: eraLabels`            |
| バリデーション           | `eraEntries.length > 0`                       | `eraLabels.length > 0`             |

### 4.5 MaintenancePage.tsx 変更

**ファイル**: `frontend/src/pages/MaintenancePage.tsx`

| 項目               | 現行                                                               | 改修後                             |
| ------------------ | ------------------------------------------------------------------ | ---------------------------------- |
| 編集状態のera項目  | `era_entries: EraEntry[]`                                          | `era_labels: string[]`             |
| EraSelector props  | `initialEntries` / `onChange` が `EraEntry[]`                      | `value` / `onChange` が `string[]` |
| APIペイロード      | `era_entries: edit.era_entries`                                    | `era_labels: edit.era_labels`      |
| 既存データ読み込み | `_build_era_entries_from_stored()` の結果を `era_entries` にセット | `era_labels` をそのままセット      |

### 4.6 BrowseLists.tsx 変更

**ファイル**: `frontend/src/components/BrowseLists.tsx`

#### eraフィルターUIの変更

| 項目           | 現行                       | 改修後                               |
| -------------- | -------------------------- | ------------------------------------ |
| フィルター形式 | ドロップダウン(単一選択) | チェックボックス or Chip(複数選択) |
| 選択条件       | 単一ラベル絞り込み         | 複数ラベル OR 条件絞り込み           |
| 状態型         | `selectedEra: string`      | `selectedEras: string[]`             |