# LINEセミナー検索システム仕様書

## 1. 目的

ログインが必要なセミナーサイトから全国のセミナー情報を定期取得し、LINE公式アカウントから利用者が日付、地域、カテゴリなどを指定して検索できるようにする。

## 2. 採用方針

1. セミナーサイト取得とLINE応答を分離する。
2. LINE Webhookは常にDBを読む。Webhook処理中にセミナーサイトへアクセスしない。
3. 取得元の認証情報はソースコード、JSON、ログに保存しない。
4. 既存JSONを取得証跡として残し、DBへの取込は冪等にする。
5. 検索結果が多い場合は、LINEのメッセージ制限を超えないようページングする。

## 3. 全体構成

```text
定期実行（cron / scheduler）
        │
        ▼
collector ── Playwright ── 全厚済セミナーサイト
        │
        ├── docs/json/YYYY-MM.json（証跡）
        ▼
importer ──────────────── PostgreSQL（本番正本）
                                │
LINE Platform ─ Webhook API ───┘
        │
        ▼
LINE Messaging API（返信）
```

### 推奨インフラ

- API：HTTPSで公開したFastAPI、Flask、または同等のWeb API
- DB：PostgreSQL。本番以外ではSQLiteでも可
- 定期取得：cron、GitHub Actions、AWS EventBridgeなど
- 秘密情報：環境変数またはAWS Secrets Manager
- 監視：取得件数、失敗回数、DB接続、Webhookエラーを通知

## 4. 機能要件

### 4.1 セミナー取得

- 対象月を`YYYY-MM`で指定できる。
- 通常ログインまたは保存済みブラウザセッションで認証する。
- 検索結果を既存JSON形式に合わせて保存する。
- セミナーIDをキーにDBへ新規登録または更新する。
- 取得できなかった月は既存の正常データを削除しない。
- 同一セミナーを複数回取得しても重複レコードを作らない。
- 取得件数が前回比で大きく減った場合は、即時に掲載終了扱いにせず警告にする。

### 4.2 LINE Webhook

- LINEからのWebhook署名を検証する。
- 未知のイベント、再送イベント、対象外メッセージを安全に処理する。
- 受信後3秒以内を目標にHTTP 200を返す。
- 重い検索や返信処理は必要に応じてキューへ渡す。
- LINEユーザーIDはDBに保存する場合、利用目的と保存期間を定める。

### 4.3 検索

MVPで対応する検索条件：

| 条件 | 例 |
|---|---|
| 対象月 | `2027-09` |
| 日付 | `2027-09-10` |
| 都道府県 | `東京都` |
| 市区町村・住所 | `新宿` |
| カテゴリ | `BE`、`BT`、`BS` |
| 講師 | 講師名の一部 |
| 状態 | 受付中、開催予定、開催済 |

MVPの入力形式：

```text
検索 2027-09 東京 BE
検索 2027-09-10 大阪
講師 徳野
ヘルプ
```

自然言語入力は、まず正規表現と辞書で解釈する。解釈できない場合は候補形式を案内し、曖昧な条件で勝手に検索しない。

### 4.4 応答

検索結果は、開催日時、カテゴリ、都道府県、市区町村、会場、受付状況、詳細URLを表示する。

```text
2027年9月10日（金）19:00〜21:00
BE｜東京都新宿区
会場：〇〇会館
受付：受付中
詳細：https://seminar.zenko-sai.or.jp/seminar/12345
```

- 初回返信は最大5件を表示する。
- 6件以上の場合は「次の5件」ボタンを表示する。
- 結果0件の場合は、検索条件を表示して条件変更を案内する。
- 外部サイトの詳細URLを表示し、LINE内で申込処理は行わない。
- 同じWebhookイベントに対して二重返信しない。

## 5. DB論理設計

### 5.1 `seminars`

| カラム | 型 | 必須 | 説明 |
|---|---|---:|---|
| `id` | UUID/ bigint | ○ | 内部主キー |
| `source_seminar_id` | varchar | ○ | 取得元セミナーID、UNIQUE |
| `detail_url` | text | ○ | 詳細URL |
| `category_code` | integer |  | カテゴリコード |
| `category_label` | varchar |  | BE、BTなど |
| `title` | text |  | タイトル |
| `starts_at` | timestamptz | ○ | 開始日時 |
| `ends_at` | timestamptz |  | 終了日時 |
| `date_raw` | varchar |  | 元の日付 |
| `prefecture` | varchar |  | 都道府県 |
| `address_1` | text |  | 市区町村等 |
| `address_2` | text |  | 番地等 |
| `venue_name` | text |  | 会場 |
| `capacity` | integer |  | 定員 |
| `fee_raw` | text |  | 料金原文 |
| `availability_raw` | text |  | 空席・受付状態原文 |
| `status_code` | varchar |  | 元状態コード |
| `organizer_display` | text |  | 主催者表示 |
| `format_raw` | varchar |  | 開催形式原文 |
| `is_active` | boolean | ○ | 現在掲載中か |
| `first_seen_at` | timestamptz | ○ | 初回取得日時 |
| `last_seen_at` | timestamptz | ○ | 最終取得日時 |
| `raw_json` | jsonb |  | 元レコード |

### 5.2 関連テーブル

- `seminar_lecturers(seminar_id, lecturer_name, lecturer_url)`
- `seminar_registrations(seminar_id, apply_start_at, apply_end_at, lottery_at, fcfs_start_at, reservation_end_at)`
- `collection_runs(id, target_month, started_at, finished_at, status, observed_count, error_message)`
- `line_users(line_user_id, first_seen_at, last_seen_at, blocked_at)`
- `line_queries(id, line_user_id, query_text, parsed_query_json, result_count, created_at)`

### 5.3 インデックス

`starts_at`、`prefecture`、`category_label`、`is_active`にインデックスを作成する。講師名と住所は必要に応じて全文検索インデックスを追加する。

## 6. 取得・取込フロー

1. 対象月を決定する。
2. ログインセッションを検証する。
3. セミナー検索を実行する。
4. 一覧と詳細情報を取得する。
5. `captured_at`付きJSONを一時ファイルへ書き込む。
6. JSONの必須項目と重複を検証する。
7. DBトランザクション内でupsertする。
8. 今回観測されなかった既存データは、同じ対象範囲を正常取得できた場合のみ`is_active=false`にする。
9. `collection_runs`を成功で終了する。
10. 件数異常、認証失敗、部分取得は警告または失敗として記録する。

## 7. LINE処理フロー

1. `POST /line/webhook`で受信する。
2. `X-Line-Signature`をHMAC-SHA256で検証する。
3. イベントIDを冪等性キーとして重複を確認する。
4. テキストを検索コマンドへ変換する。
5. DBから最大5件を取得する。
6. LINE返信形式に変換する。
7. Reply Tokenで返信する。
8. 処理結果を監査ログに残す（本文・署名・トークンは秘匿）。

## 8. セキュリティ・個人情報

- Webhookは署名検証に失敗した場合、処理せず`400`を返す。
- 管理用取得エンドポイントを外部公開する場合は、別認証とIP制限を設ける。
- LINEチャネルアクセストークン、チャネルシークレット、取得元ID・パスワードはSecret管理する。
- DBは公開ネットワークから直接接続できないようにする。
- `line_user_id`は必要最小限のみ保存し、削除依頼に対応できるようにする。
- エラー返信には内部URL、SQL、認証情報、スタックトレースを含めない。

## 9. 障害・再実行

| 障害 | 動作 |
|---|---|
| 取得元ログイン失敗 | 既存DBを維持し、管理者へ通知 |
| 取得件数が異常に少ない | DB更新を保留し、警告 |
| DB停止 | Webhookは一時エラー、取得側は再試行 |
| LINE返信失敗 | イベントIDと結果を記録し、再送可能にする |
| 途中終了 | 次回実行時に同月を安全に再取得 |

## 10. 受入条件

- 正しいLINE署名のWebhookにHTTP 200を返せる。
- 不正署名を拒否できる。
- `検索 2027-09 東京 BE`でDB検索され、結果がLINEに返る。
- 0件、1件、5件、6件以上を正しく表示できる。
- 同一Webhookの再送で二重返信・二重記録が発生しない。
- 同一JSONを2回取り込んでもDB件数が増えない。
- 取得元ログイン失敗時に既存データが消えない。
- パスワード、チャネルシークレット、アクセストークンがログに出ない。
- 月別JSONとDBの代表件数・代表レコードが一致する。
- 主要処理に自動テストがある。

## 11. 段階導入

### Phase 1：取得・DB

既存取得CLI、JSON検証、PostgreSQL upsert、取得ログを実装する。

### Phase 2：LINE固定コマンド

Webhook署名検証、`検索`、`ヘルプ`、ページング、エラー応答を実装する。

### Phase 3：運用強化

定期実行、異常件数通知、管理者向け再取得、監視、バックアップを追加する。

### Phase 4：自然言語検索

辞書・正規表現で対応範囲を広げ、必要性が確認できた場合のみLLMによる条件抽出を導入する。LLMには認証情報や不要な個人情報を渡さない。
