diff --git a/docs/EVENTS_MAPS_AUDIT_AND_ROADMAP.md b/docs/EVENTS_MAPS_AUDIT_AND_ROADMAP.md deleted file mode 100644 index 0cfc3546..00000000 --- a/docs/EVENTS_MAPS_AUDIT_AND_ROADMAP.md +++ /dev/null @@ -1,199 +0,0 @@ -# Posimai Events / Maps 監査と完成ロードマップ - -**目的**: 単体ミニマルアプリとしての現状整理・改善点の優先度付け・Maps/Events どちらを先に「完全体」にするかの方針。 -**前提**: 機能を積み増ししすぎず、シンプル&スタイリッシュを維持。既存アプリ(Feed / Brain / Reader)との整合性・Lucide アイコン統一を重視する。 - ---- - -## 1. デザインシステム共通チェック(全アプリ) - -| 項目 | Feed | Brain | Reader | Maps | Events | -|------|------|-------|--------|------|--------| -| ヘッダー高さ 52px | ○ | ○ (--topbar-h) | - | 要確認 | ○ | -| Inter フォント | ○ | ○ | - | 要確認 | ○ | -| :root / [data-theme="light"] | ○ | ○ | - | ○ | ○ | -| --bg, --surface, --text, --accent, --radius: 8px | ○ | ○ | - | ○ | ○ | -| Lucide のみ(絵文字・他アイコンなし) | ○ | ○ | - | ○ | **要修正** | -| PWA manifest + theme-color | ○ | ○ | - | ○ | ○ | -| テーマ切替(sun/moon アイコン) | ○ | ○ | - | ○ | ○ | - -**結論**: Events の「絞り込み」シート内の一部アイコン名が Lucide にない可能性あり(後述)。それ以外はおおむね統一されている。 - ---- - -## 2. Posimai Events — 現状と改善点 - -### 2.1 実装済み(維持する) - -| 項目 | 内容 | -|------|------| -| **レイアウト** | 52px ヘッダー、タブ(今日/今週/すべて)、リスト+日付バッジ、ボトムシート詳細 | -| **データ** | モック JSON + localStorage キャッシュ/オフライン表示、Synology API URL 対応 | -| **パーソナライズ** | 興味タグ・対象者タグ・オプション(無料/申込不要/屋外・屋内)の絞り込み、設定は localStorage で永続化 | -| **連携** | Brain 保存(URL+title で開く)、詳細リンク(外部)、場所→Google Maps 検索 | -| **PWA** | manifest.json, sw.js, インストール可能、オフラインでキャッシュ表示 | -| **テーマ** | ダーク/ライト切替、Events 専用アクセント #6EE7B7 | -| **Lucide** | ヘッダー・カード・シート・トーストで `data-lucide` を一貫使用 | - -### 2.2 改善点(優先度順) - -#### 優先度:高(見た目・一貫性・信頼性) - -| # | 内容 | 理由 | 工数目安 | -|---|------|------|----------| -| E1 | **Lucide アイコン名の検証・修正** | 絞り込みタグで `beer`, `glass-water`, `circle-dollar-sign`, `door-open`, `hand-heart` 等を使用。Lucide にない/名前違いがあると表示欠けやコンソールエラーの原因になる。 | 0.5h | -| E2 | **API フォールバック順の明確化** | 現状「Synology URL 優先 → 失敗時 Vercel /api/events」だが、Synology 未構築時は初回から失敗する。Vercel をデフォルトにし、設定で Synology URL を差し替える方が運用しやすい。 | 0.5h | -| E3 | **空状態・エラー時の文言統一** | 「この条件のイベントは見つかりません」等、Feed/Brain に近いトーンで簡潔に。 | 0.25h | - -#### 優先度:中(UX の磨き) - -| # | 内容 | 理由 | 工数目安 | -|---|------|------|----------| -| E4 | **絞り込みシートの「適用」でシートを閉じる** | 現状どおりで問題なし。閉じたあとトーストで「絞り込みを適用しました」と出しているのでそのまま維持でよい。 | - | -| E5 | **カードのタップ領域** | カード全体タップで詳細、右端アイコンは別アクション。`event.stopPropagation` が効いているか確認。 | 0.25h | -| E6 | **キーボード・スクリーンリーダー** | ボタンに `aria-label`、シートに `aria-modal="true"` を追加し、既存アプリと水準を揃える。 | 0.5h | - -#### 優先度:低(将来) - -| # | 内容 | 理由 | -|---|------|------| -| E7 | **API URL の設定 UI** | 開発者向け。設定パネルや localStorage のキー説明を README に記載すれば十分。UI は必須ではない。 | -| E8 | **Maps 連携** | 「イベント場所→Maps で周辺飲食店」は、Maps 側で URL パラメータが固まってから実装。現状は Google Maps リンクで代替。 | - -### 2.3 意図的にスコープ外(ミニマル維持) - -- イベントの編集・削除(管理画面は別) -- 複数地域・複数カレンダー -- プッシュ通知 -- ログイン・ユーザー別データ(単体アプリのまま) - ---- - -## 3. Posimai Maps — 現状と改善点 - -### 3.1 実装済み(OVERVIEW 等より) - -| 項目 | 内容 | -|------|------| -| **コア** | Google Places API (New) searchNearby、最大20件、半径・タイプ・ランキング対応、moveend 再検索、0件時 UX | -| **UI** | 地図右下に現在地・ズーム、ヘッダーに人気順/近い順、カテゴリタブ、店舗詳細パネル、テーマ切替 | -| **連携** | Brain 保存、Google マップ連携、Carto タイル(ダーク/ライト) | -| **PWA** | manifest, sw.js | -| **Lucide** | locate, sun, utensils, x, map-pin, phone, sticky-note, clock 等を利用 | - -### 3.2 改善点(優先度順) - -#### 優先度:高(負債解消・一貫性) - -| # | 内容 | 理由 | 工数目安 | -|---|------|------|----------| -| M1 | **ラジアル UI の扱いを確定** | CRITICAL_ACTION_PLAN 通り「削除」か「保留(触らない)」を決定。削除なら関連 JS/CSS を削り、スライダーのみで完成形に。 | 0.5h | -| M2 | **位置情報オプション** | `enableHighAccuracy: true`, `timeout: 10000`, `maximumAge` の調整(OVERVIEW 1.3 の推奨)。 | 0.25h | -| M3 | **スライダー見た目** | トラック 1.5px・ポインタ 16px リング型(未適用なら実施)。 | 0.25h | -| M4 | **Lucide の徹底** | 地図コントロール・リスト・詳細パネルで Lucide 以外のアイコンやインライン SVG が残っていれば Lucide に統一。 | 0.5h | - -#### 優先度:中 - -| # | 内容 | 理由 | -|---|------|------| -| M5 | **カテゴリと API の連携** | タブ選択で `includedTypes` を変え、カテゴリごとに最大20件を取得(OVERVIEW 推奨)。 | -| M6 | **取得幅の設定** | 歯車で範囲(m)を設定・保存する場合は、既存「1.5km 固定」を維持しつつオプションとして追加。 | - -#### 優先度:低 - -| # | 内容 | -|---|------| -| M7 | **20件超の表示** | 複数回検索の併合は、需要とコストを見てから。 | -| M8 | **営業時間なしの表記** | 詳細パネルで「営業時間未登録」等の文言を仕様化。 | - -### 3.3 意図的にスコープ外(Phase 1 完全体の範囲外) - -- ラジアルの「改良」(削除 or 保留のみ) -- Phase 2 の SNS/Web 営業時間補完 -- レビュー・写真の表示(API 次第) - ---- - -## 4. Lucide アイコン統一チェック - -### Events で使用しているアイコン名(絞り込みタグ含む) - -| 用途 | 指定名 | Lucide での存在 | -|------|--------|-----------------| -| 日本酒 | wine | ○ あり | -| クラフトビール | beer | △ **要確認**(無ければ `wine` や `glass-water` で代替) | -| ワイン・お酒 | glass-water | ○ あり | -| ボランティア | hand-heart | ○ あり | -| 無料 | circle-dollar-sign | ○ あり | -| 申込不要 | door-open | ○ あり | - -**対応**: ビルドまたはブラウザで実際に表示を確認し、存在しない名前だけ `lucide.dev` で正式名に差し替える。 - -### 全アプリ共通ルール - -- アイコンは **Lucide のみ**。絵文字・Font Awesome・インライン SVG は使わない。 -- サイズは `width`/`height` または CSS で統一(例: ヘッダー 18px、リスト 14px)。 -- 色は `color: var(--text2)` / `var(--accent)` 等、テーマ変数に合わせる。 - ---- - -## 5. Maps と Events のどちらを先に「完全体」にするか - -### 判定の観点 - -| 観点 | Maps | Events | -|------|------|--------| -| **バックエンド** | 既存(Google Places API + Vercel serverless)で完結。Synology 不要。 | 本番データは Synology(n8n + PostgreSQL)が前提。未構築ならモックのまま。 | -| **負債** | ラジアルの削除/保留の決定が未了。 | なし(新規プロトタイプ)。 | -| **スコープ** | 検索・表示・連携まで一通り実装済み。あとは整理と仕様確定。 | UI/UX と絞り込みはある。実データは n8n+DB 構築が必要。 | -| **ユーザー価値** | 「今空いている飲食店」は単体で価値が明確。 | 「自分用イベント一覧」はデータが入って初めて価値が立つ。 | -| **工数** | ラジアル処理+位置情報+スライダー+Lucide で **1〜2h** 程度。 | Lucide 修正+API デフォルト+軽微 UX で **1h 以内**。Synology 側は別計画。 | - -### 推奨方針:**Maps を先に「完全体」にする** - -1. **完了の定義がはっきりしている** - 「ラジアル削除 or 保留」「位置情報・スライダー・Lucide 統一」をやれば、Phase 1 として完成と宣言しやすい。 - -2. **バックエンドに依存しない** - Maps は既存 API のまま完成形にできる。Events の完全体は Synology の n8n + DB が前提で、工数と前提が大きい。 - -3. **1アプリずつ仕上げる** - まず Maps を「完成」にし、そのあと Events は「UI/UX とクライアント側は完成 → データは Synology 構築で実化」と段階を分けられる。 - -4. **Events は「見た目と動きは完成、データはモック or 任意 Synology」** - Lucide 修正・API デフォルト・アクセシビリティだけ整え、単体ミニマルアプリとして完成とする。実データは別タスク(n8n + Synology API)で対応。 - -### ロードマップ(案) - -``` -Phase A: Maps 完全体(1〜2h) - M1 ラジアル削除 or 保留確定 → 実装 - M2 位置情報オプション - M3 スライダー見た目(未適用なら) - M4 Lucide 徹底 - -Phase B: Events クライアント完成(1h 以内) - E1 Lucide アイコン名検証・修正 - E2 API フォールバック順(Vercel デフォルト) - E3 空状態・エラー文言 - E6 アクセシビリティ(aria) - -Phase C: ドキュメント・計画の更新 - CRITICAL_ACTION_PLAN に「Maps 完全体済み」「Events クライアント完成」を反映 - Events の実データ(n8n + Synology)は別ロードマップで計画 -``` - ---- - -## 6. まとめ - -| アプリ | 現状 | 完全体の定義 | 優先度 | -|--------|------|--------------|--------| -| **Maps** | 機能はほぼ揃っている。ラジアル・位置情報・スライダー・Lucide の整理が残り。 | ラジアル処理済み・位置情報推奨値・スライダー見た目・Lucide 統一。Phase 1 として宣言。 | **先に完了** | -| **Events** | UI/UX・絞り込み・PWA・Brain 連携まで実装。モック or Synology で表示。 | クライアント: Lucide 修正・API デフォルト・文言・aria。実データは n8n+Synology で別対応。 | **Maps の次** | - -- **シンプル&スタイリッシュ**: 新機能の追加は行わず、上記の「高」と「中」の改善に絞る。 -- **整合性**: ヘッダー 52px、Inter、CSS 変数、Lucide のみ、PWA、テーマ切替は全アプリで揃える。 -- **完成の切り分け**: Maps = このドキュメントの Phase A 完了で「完全体」。Events = Phase B 完了で「クライアント完成」とし、データは別計画とする。 - -この方針で進めれば、機能を積み増しせず、シンプルな完成形を維持できます。 diff --git a/docs/POSIMAI_SYSTEM_OVERVIEW.md b/docs/POSIMAI_SYSTEM_OVERVIEW.md deleted file mode 100644 index f50dc817..00000000 --- a/docs/POSIMAI_SYSTEM_OVERVIEW.md +++ /dev/null @@ -1,401 +0,0 @@ -# Posimai System Overview - Brain/Reader/Feed連携 - -## システム全体像 - -``` -┌──────────────────────────────────────────────────────────────────┐ -│ Posimai Ecosystem │ -└──────────────────────────────────────────────────────────────────┘ - -┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ -│ Posimai Feed │ │ Posimai Reader │ │ Posimai Brain │ -│ (RSS集約) │─────▶│ (広告除去) │─────▶│ (AI分析・保存) │ -└─────────────────┘ └─────────────────┘ └─────────────────┘ - Vercel Vercel Vercel + Synology -``` - ---- - -## 🔄 データフロー(現在の実装) - -### Phase 1: Feed → Reader -``` -[Posimai Feed] - │ - │ ① RSSフィードから記事一覧を取得 - │ - タイトル - │ - URL - │ - 概要 - │ - ▼ -[ユーザー] 気になる記事をタップ - │ - ▼ -[Posimai Reader] - │ ② URLを受け取る - │ ③ Readability.jsで広告除去 - │ - タイトル - │ - 本文全文(クリーン) - │ - 画像 - │ - ▼ -[ユーザー] 記事を読む -``` - -### Phase 2: Reader → Brain(現在の実装) -``` -[Posimai Reader] - │ - │ ④ 「Brainに保存」ボタンをクリック - │ - ▼ -[POST /api/save] ← ⚠️ 現状はURLのみ送信 - { - "url": "https://example.com/article" - } - │ - ▼ -[Brain API Server - Synology] - │ - │ ⑤ OGPメタデータをフェッチ - │ - title (og:title) - │ - desc (og:description) ← ⚠️ 短い! - │ - ogImage - │ - favicon - │ - ▼ - │ ⑥ Gemini AI分析 - │ Input: title + desc(短い概要のみ) - │ Output: - │ - summary (3文要約) - │ - topics (最大2個) - │ - readingTime (推定読了時間) - │ - ▼ -[PostgreSQL - Synology] - 保存されるデータ: - ├─ url - ├─ title - ├─ summary (AI生成) - ├─ topics (AI生成) - ├─ source (抽出元) - ├─ reading_time (AI推定) - ├─ favicon - ├─ og_image - └─ status (inbox/favorite/shared) - - ⚠️ 保存されないデータ: - └─ 広告除去済みの本文全文 -``` - -### Phase 3: Brain UI表示 -``` -[Posimai Brain - Vercel] - │ - │ ⑦ GET /api/articles でデータ取得 - │ - ▼ -[記事一覧表示] - ├─ タイトル - ├─ AI要約(3文) - ├─ トピックタグ - ├─ ソース - └─ 読了時間 - - ⚠️ 表示できないデータ: - └─ 本文全文(保存されていないため) -``` - ---- - -## 🔴 現在の問題点 - -### 1. **AI分析の精度が低い** -- OGPの`description`は短い(50-150文字程度) -- 本文全文を使っていないため、詳細な内容を把握できない -- トピック分類が不正確になる可能性 - -### 2. **本文が保存されていない** -- Brain側で記事を読み直せない -- 元の記事が削除されたら読めなくなる -- もう一度Readerを開く必要がある - -### 3. **Geminiトークンの無駄遣い** -- Readerで既に本文全文を取得済み -- それを捨てて、BrainでOGPを再フェッチ -- 二重の通信が発生 - ---- - -## ✅ 改善案:本文全文保存フロー - -### 新しいデータフロー -``` -[Posimai Reader] - │ - │ ④ 「Brainに保存」ボタンをクリック - │ - ▼ -[POST /api/save] ← ✅ 本文全文も送信 - { - "url": "https://example.com/article", - "title": "記事タイトル", - "full_text": "広告除去済みの本文全文...", ← NEW! - "images": ["https://...", ...] ← NEW! - } - │ - ▼ -[Brain API Server] - │ - │ ⑤ Gemini AI分析 - │ Input: title + full_text(本文全文!)← 改善! - │ Output: - │ - summary (より正確な3文要約) - │ - topics (より正確な分類) - │ - readingTime (本文の長さから計算) - │ - ▼ -[PostgreSQL] - 保存されるデータ: - ├─ url - ├─ title - ├─ full_text ← NEW! 本文全文 - ├─ summary (AI生成) - ├─ topics (AI生成) - ├─ source - ├─ reading_time - ├─ favicon - ├─ og_image - └─ status -``` - -### Brain UI表示(改善後) -``` -[記事一覧] - クリック - │ - ▼ -[記事詳細モーダル] ← NEW! - ├─ タイトル - ├─ AI要約 - ├─ トピックタグ - ├─ 元URL(リンク) - ├─ 保存日時 - └─ 本文全文(広告除去済み)← NEW! - └─ Readerと同じ見た目で表示 -``` - ---- - -## 📊 実装工数見積もり - -### 1. **本文全文保存機能** (1-2時間) -- [ ] DB: `articles`テーブルに`full_text TEXT`カラム追加 -- [ ] Reader: 保存時に本文をPOST -- [ ] Brain API: 本文を受け取って保存 -- [ ] Brain API: AI分析時に本文を使用 - -### 2. **Brain UI: 記事詳細表示** (2-3時間) -- [ ] モーダルまたは詳細ページ作成 -- [ ] 本文のマークダウン/HTML表示 -- [ ] レスポンシブ対応 - -### 3. **Reader UI改善** (1-2時間) -- [ ] 読むボタンをURL右側に配置(スマホ対応) -- [ ] 本文内の長いURLを`word-break: break-all`で折り返し -- [ ] フッター要素削除ロジック強化 - -### 4. **連携フロー全体のテスト** (1時間) -- [ ] Feed → Reader → Brain の一連の流れを確認 -- [ ] 各種エラーハンドリング - -**合計: 5-8時間** - ---- - -## 🎯 優先順位 - -### 🔴 高(今すぐやるべき) -1. **本文全文保存機能** - データロスを防ぐ -2. **Reader UI改善(読むボタン配置)** - UX向上 - -### 🟡 中(近日中に) -3. **Brain UI: 記事詳細表示** - 利便性向上 -4. **フッター要素削除** - 読みやすさ向上 - -### 🟢 低(余裕があれば) -5. **Feed連携強化** - RSSからの直接保存 - ---- - -## 🛠️ 技術詳細 - -### DB Schema(改善後) -```sql -ALTER TABLE articles ADD COLUMN IF NOT EXISTS full_text TEXT; -ALTER TABLE articles ADD COLUMN IF NOT EXISTS images TEXT[]; -``` - -### Reader → Brain API呼び出し(改善後) -```javascript -// posimai-reader/script.js -async function saveToBrain(article) { - const response = await fetch('https://posimai-lab.tail72e846.ts.net/brain/api/save', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${API_KEY}` - }, - body: JSON.stringify({ - url: article.url, - title: article.title, - full_text: article.textContent, // NEW! - images: article.images // NEW! - }) - }); - return response.json(); -} -``` - -### Brain API(改善後) -```javascript -// posimai-api/server.js -r.post('/save', authMiddleware, async (req, res) => { - const { url, title, full_text, images } = req.body || {}; - - // AI分析時に本文全文を使用 - const ai = await analyzeWithGemini(title, full_text || meta.desc, url); - - // DBに本文も保存 - await pool.query(` - INSERT INTO articles - (user_id, url, title, full_text, summary, topics, ...) - VALUES ($1,$2,$3,$4,$5,$6,...) - `, [req.userId, url, title, full_text, ai.summary, ai.topics, ...]); -}); -``` - ---- - -## Posimai Maps - Phase 1 調査(エンジニア・PM・UX 観点) - -### Phase 1 のスコープ(現状・2026年3月時点) - -- 現在地または地図中心を起点に Google Places API (New) の **searchNearby** で飲食店を検索 -- 取得件数 **最大20件**(API上限)、半径 **500m〜5km ユーザー選択可**、**6類型**(restaurant, bar, cafe, meal_takeaway, bakery, meal_delivery) -- 営業時間は **regularOpeningHours** / **weekdayDescriptions** で表示し、**JST** で「今」を算出 -- **地図 moveend で再検索**(デバウンス・150m 以上移動時)、**0件時は再検索・範囲を広げる** UI、**人気順/近い順** 選択済み -- 取得幅・現在地は **地図右下(ズーム付近)** に配置(Google Maps 風) -- ダーク/ライトテーマ、カテゴリフィルター、Brain 保存・Google マップ連携まで実装済み - -### Phase 1 の残課題 - -| 観点 | 課題 | 影響 | -|------|------|------| -| **エンジニア** | **取得件数が20件で固定**(API の maxResultCount 上限) | 繁華街などでは「周辺の飲食店」のごく一部しか表示されない | -| **エンジニア** | **半径が1500m固定**(フロントで `radius=1500` のみ送信) | サーバーは 500–5000m を受け付けるが未使用。ユーザーが範囲を変えられない | -| **エンジニア** | **includedTypes が4種類のみ**(bakery, meal_delivery 等なし) | ベーカリー・デリバリー専門など、型が違う実在店が検索結果に含まれない可能性 | -| **エンジニア** | **rankPreference 未指定**(デフォルト POPULARITY) | 「近い順」で見たいニーズに未対応 | -| **PM** | **地図を動かしても再検索しない** | 表示は初回+「現在地」ボタンのみ。エリアを変えても結果が更新されず網羅性が体感しづらい | -| **PM** | **0件時の明示的な再検索手段がない** | トーストのみ。リトライや「範囲を広げる」などの導線がない | -| **UX** | **営業時間なし店舗を「営業中」としてカウント**(`periods` なしなら `open = true`) | ヘッダーの「N 営業中」が実態より多くなりうる。詳細では「営業時間不明」と表示される | - -### 実在する飲食店の「網羅性」 - -**結論: 現状では「網羅的」には取得できていません。** - -- **件数**: 1回の検索で **最大20件** のみ。同一エリアに50店あっても20店しか出ない。 -- **範囲**: 半径 **1500m** 固定のため、それ以遠の店は初回から対象外。 -- **種類**: **restaurant, bar, cafe, meal_takeaway** のみ。Google の Table A にある **bakery, meal_delivery** などはリクエストに含めておらず、これらが primary type の店は返ってこない可能性がある(restaurant の下位の sushi_restaurant 等は `restaurant` 指定で取得可能)。 -- **データソース**: あくまで **Google に登録されている店舗** のみ。未登録・営業時間が未登録の店は「営業時間不明」または検索結果に含まれない。 -- **地図移動**: 地図をドラッグして別エリアに移っても **再検索されない** ため、表示は「最初の中心+現在地ボタン」の2パターンのみ。 - -「世界中の実在する飲食店を漏れなく」という意味での網羅は、Phase 1 の前提(1 API 呼び出し・20件・固定半径)では達成されていません。 -「現在地付近の、Google に載っている飲食店の一部を、最大20件まで見る」というスコープでは動作しています。 - -### 推奨アクション(Phase 1 改善)— 多くは実装済み - -- 網羅性の体感向上(moveend 再検索・デバウンス)— **済** -- 取得幅の拡大(bakery / meal_delivery、半径選択)— **済** -- ランキング選択(人気順/近い順)— **済** -- 0件時の再検索・範囲を広げる UX — **済** -- 営業中カウントの「不明」別表示 — 要望により **廃止**(「N 営業中」のみ表示) - ---- - -## Posimai Maps - 残タスクの推奨ステップ(PM・UX 観点) - -優先度と影響範囲を踏まえた実行順の目安です。 - -| 順 | フェーズ | タスク | 目的・効果 | 工数目安 | -|----|----------|--------|------------|----------| -| 1 | **安定化** | 本番デプロイ・動作確認 | 地図右下コントロール・スライダー・ヘッダー表示の確認 | 0.5h | -| 2 | **検証** | 20件上限の体感テスト | 繁華街で「足りない」と感じるかユーザーテストし、次フェーズの要不要を判断 | 1h | -| 3 | **価値向上** | 複数回検索で20件超を表示(オプション) | 中心を少しずらして複数回 searchNearby を叩き、重複除いて最大40〜60件表示。API コスト・レート制限に注意 | 2–3h | -| 4 | **信頼性** | 営業時間なし店舗の扱いを仕様化 | 現状「営業中」に含めて表示。必要なら詳細パネルで「営業時間未登録」を明示するなど軽い文言整備 | 0.5h | -| 5 | **Phase 2 検討** | SNS/Web からの営業時間補完 | Jina + LLM で公式サイト・Instagram 等から営業時間をパースし補完。設計・コスト・精度の検討から | 設計 2h〜 | - -**運用方針の提案** - -- **まず 1〜2 で現行 Phase 1 を固め、3 は「20件で不足」という声が出てから着手**するのが安全です。 -- **4 は軽い文言・表示の整理**で、5 は別フェーズとしてロードマップに記載し、必要に応じて着手する形を推奨します。 - -### 地図右下ゾーン(右手親指・スマホ)のUX方針 - -- **配置順(上→下)**: 現在地 → ズーム(+-)。取得幅は廃止し検索は 1.5km 固定。地図を動かすとその中心で再検索される。 -- **ラジアル・タイムセレクター(サムゾーン最適化)**: スマホ表示のみ(640px 以下)。**二重円弧(Concentric Arcs)+回転リール(Rotary Wheel)**方式。 - - 画面右下角を支点に、左上向きの四分の一円(90度)だけが露出。外側円弧で曜日(日月火…)、内側円弧で時刻(00–23)を選択。 - - なぞると仮想リールが回転し、**弧の中央(約45度位置)**に来た項目が選択される。曜日・時刻とも無限ループ(土→日、23→00)。 - - **最終洗練**: (1) **常時ガイド** — 二重の円弧(トラック)は未操作時も opacity 0.1 で常時表示。(2) **フェードイン** — タッチ開始時のみ、目盛り(曜日・時刻ラベル)と中央の現在値が opacity 1 で表示。(3) **スナップ** — 指を離すと最も近い曜日/時刻が弧の中央に吸着し、transform の transition(0.15s)で「カチッ」と止まる。選択値は #6EE7B7、それ以外は #4B5563。指離し時に短いハプティック(3ms)。 -- **PC**: 時刻は横スライダーのみ。ラジアル切替タブは非表示。 -- **今後の検討**: ラジアルと地図コントロールが同じ右下を共有するため、表示時にボタン縮小/アイコンのみなど、領域の兼ね合いをユーザーテストで調整可能。 - -**テーマ切替・地図タイルについて**: テーマ切替は実装済みで、ダーク/ライトの両方で動作します。テーマ切替は Posimai Feed と同様にボタン枠なしの Lucide アイコンのみ表示。地図タイルも**両モードで存在**します。ダーク時は Carto `dark_all`、ライト時は Carto `voyager_labels_under`(Voyager ラベル付き)を使用しています。 - -**ヘッダー「人気順/近い順」について**: リスト表示の有無とは無関係です。Google Places API の **検索結果の並び順**(`rankPreference`)を切り替えるためのものです。**人気順**=POPULARITY(口コミ・人気でソート)、**近い順**=DISTANCE(現在地からの距離でソート)。地図上のピンがこの順で返ってくるため、どちらの基準で周辺店舗を見るかを選べるようにする目的で追加しています。 - -**カテゴリタグについて**: **現在地周辺の検索結果(currentPlaces)に含まれる店舗のカテゴリだけ**がタグとして表示されます。検索のたびに `initCategoryBar()` が `getCategories()` で「今取得した店舗の categoryLabel のユニーク一覧」を元にタグを再生成するため、エリアや範囲を変えるとタグの種類・順序が変わり、安定しないように見えます。固定のカテゴリリストにするか、前回結果とマージしてタグを安定させるかは今後の検討事項です。 - -**取得幅について**: 取得幅プルダウンは削除し、**1500m(1.5km)固定**にしています。**100m は狭すぎます** — 市街地でも 100m 圏内の飲食店は 0〜2 件程度になり「見つかりません」が多くなります。500m〜2km が実用域で、1.5km は「徒歩で選びやすい範囲」の目安です。UI は「現在地」「ズーム」のみとし、空状態の「範囲を広げて検索」は「地図を動かして検索」に変更し、地図ドラッグで再検索される案内にしています。 - -**20店舗制限と「20件の精度」について**: **searchNearby の maxResultCount は 1〜20 が上限**(API仕様)です。20件のうち「欲しいもの」に絞るには次のようにできます。 -- **カテゴリで絞る(API側)**: リクエスト時に `includedTypes` を限定すると、**そのタイプだけを最大20件**取得できます。例: `['cafe']` のみ → カフェ20件、`['bar']` のみ → 居酒屋・バー系20件。現状は複数タイプをまとめて取得しているため、カテゴリタブで「カフェ」を選んでも表示側のフィルタのみです。**「カフェだけ20件」「居酒屋だけ20件」**にしたい場合は、検索時に `includedTypes` を1種類に絞る実装にすると、20件枠をそのジャンルに集中させられます。 -- **高評価で絞る**: searchNearby には **「評価でフィルタ」するパラメータはありません**。取得後に `places.rating` を利用する場合は FieldMask に rating を追加(Enterprise SKU・請求増)し、**クライアント側で20件を評価順に並べ替え・表示**する形になります。「高評価だけ20件」を API に頼ることはできず、あくまで「取れた20件のなかで評価の高い順に表示」です。 -- **まとめ**: 20件制限を活かすなら、(1) **カテゴリを API で絞る**(カフェ20件・居酒屋20件など)が最も効果的。(2) 評価は **取得オプションに rating を足し、クライアントでソート** するのが現実的です。 - -**取得幅を歯車で設定するUI・カテゴリでAPI検索するUI(助言・未実装)** - -- **取得幅を歯車ダイアログで設定する案** - - **メリット**: 普段はシンプルな地図UIのままにでき、必要な人だけ「検索範囲」を変えられる。設定値を保存して次回以降の初期値にすれば、ユーザーごとの好み(狭い範囲で選びたい/広めで探したい)を満たせる。 - - **注意点**: (1) 歯車は「設定」のメタファーなので、ヘッダーか地図コントロール近くに1つにまとめるのがよい。(2) 数値だけ(例: 500〜5000m)にすると単位を誤解しやすいので、「500m」「1km」「1.5km」などプリセット+任意入力の併用か、スライダー+ラベル表示が分かりやすい。(3) 極端な値(100m/10km)は0件や負荷になりやすいので、最小・最大をAPI仕様(500m〜50km)に合わせて制限し、推奨は 500〜3000m と説明しておくと安心。 - - **結論**: ニーズはあるので、**歯車で「検索範囲」を設定し、その値を保存して初期値にする**のは妥当。実装時は「設定」を開く導線を1箇所にし、範囲の上限・下限と単位表示をはっきりさせることを推奨。 - -- **カテゴリ選択でAPI検索する案(選択カテゴリごとに20件取得)** - - **ご認識の通り**、選択したカテゴリで `includedTypes` を絞って検索すれば「カフェ20件」「居酒屋20件」のように**ジャンルごとに最大20件**を精度高く取れる。別カテゴリを選んだらその都度、同じ中心・半径で `includedTypes` を変えて再検索すれば、**カテゴリごとに20件の精度の高い結果**が得られる。 - - **設計の選択肢**: - - **A. カテゴリ切り替え=その場で再検索**: 「カフェ」タップ → 即座に `includedTypes: ['cafe']` で再検索して最大20件表示。「居酒屋」タップ → 同様に再検索。地図上のピンは**常に「今選んでいるカテゴリの最大20件」**だけ。シンプルで「今見ているカテゴリに集中できる」。 - - **B. 複数カテゴリを溜めて表示**: 「カフェ」で20件取得 → 表示。「居酒屋」でさらに20件取得 → 既存のカフェ20件と合わせて40件表示(重複除く)。カテゴリを増やすほどリクエストとピンが増える。20件制限は「カテゴリごと」なので、**カテゴリ数×20件**まで増やせるが、リクエスト数・料金・地図の混雑が増える。 - - **推奨**: **まずは A(選択カテゴリ=その場で再検索・表示はそのカテゴリの最大20件のみ)**がよい。理由: (1) 動作が分かりやすい (2) リクエストは「タブ切り替え時のみ」で済む (3) 地図が混雑しにくい。「すべて」タブのときだけ複数タイプをまとめて取得する現行に近い形にし、「カフェ」「居酒屋」など単一カテゴリを選んだときだけ `includedTypes` を1種類に絞る、という役割分担が扱いやすい。 - - **カテゴリリスト**: 現在は「検索結果に含まれたラベルのユニーク」でタブを出しているため、初回は「すべて」しか出ない。**「カフェ」「居酒屋」「レストラン」など、API の `includedTypes` と対応した固定リストを並べ、選択時にその type で再検索する**形にすると、初回からカテゴリで精度の高い20件検索ができる。 - -- **まとめ(実装前の助言)** - - 取得幅: 歯車でダイアログを開き、範囲(m)を設定・保存して初期値にする案は**採用してよい**。単位と min/max を明示し、プリセットかスライダーで分かりやすくする。 - - カテゴリ: **選択したカテゴリでAPIを絞り、そのカテゴリで20件取得する**のは、20件制限を活かすうえで有効。まずは「タブ=そのカテゴリで再検索し、表示はその20件のみ」(上記A)を推奨。カテゴリは `includedTypes` と対応した固定リストにすると初回から使える。 - -### 地図視認性と現在地ピン(批判的レビューと実装) - -- **地図ラベルの視認性**: 本アプリは **Leaflet + Carto ラスタータイル** のため、Google Maps の `StyledMapType` や `featureType`/`elementType` による「ラベル塗り・縁取り・不透明度」のコード制御は**不可**。可能な対応は「タイルセットの差し替え」のみ。 - - **実装**: ライトテーマ時に `light_all` から **Carto Voyager(ラベル付き)**(`rastertiles/voyager_labels_under`)へ変更。地名・道路のコントラストが強く、Google マップに近い読みやすさを優先。 - - ダークテーマは従来どおり `dark_all`(Voyager 相当のダークは現状なし)。 -- **現在地ピン**: 店舗ピン(青磁 #6EE7B7)と明確に区別するため **Posimai Brain テーマカラー(#3B82F6)** に変更。外側に青い光の輪のパルスアニメーションを付与し、一目で「自分」が分かるようにした。 -- **技術的注意**: ラスタータイルのため「POI の重み付け」「情報の引き算」はタイル提供側の仕様に依存。将来的にベクタータイル(Mapbox GL 等)へ切り替える場合に、ラベル優先度・ハロの制御が可能になる。 - ---- - -## 📝 次のアクション(一元化) - -**次に何をすべきか・優先順位・実施済みの管理は、すべて次の 1 ファイルに集約しています。全 AI(Cursor/Claude/Gemini 等)は実装前にここを参照してください。** - -- **CRITICAL_ACTION_PLAN.md** … 次ステップの優先順位・Claude 批判的レビューの精査結果・参照ドキュメント一覧・実施済み更新ルール - -OVERVIEW 上の「次のアクション」の詳細(Brain 本文保存・Reader UI・Maps ラジアル扱い・Scan 等)は上記に記載されています。新規タスクや優先度変更があった場合も、CRITICAL_ACTION_PLAN.md を更新してから実装に進んでください。 diff --git a/docs/synology/.env.example b/docs/synology/.env.example deleted file mode 100644 index e742bc9a..00000000 --- a/docs/synology/.env.example +++ /dev/null @@ -1,23 +0,0 @@ -# ============================================ -# posimai_lab Docker Compose — 環境変数 -# ============================================ -# このファイルを .env という名前でコピーして実際の値を入力してください。 -# .env は絶対に git commit しないこと! -# -# Synology での配置場所: -# /volume1/docker/posimai_lab/.env - -# PostgreSQL パスワード(Gitea と posimai_api で共用) -DB_PASS=ここに実際のDBパスワードを入力 - -# Gitea の公開ドメイン(Tailscale ホスト名) -GITEA_DOMAIN=posimai-lab.tail72e846.ts.net - -# Gemini API キー -GEMINI_API_KEY=ここに実際のGemini APIキーを入力 - -# posimai_api の認証キー(カンマ区切り: key:username 形式) -API_KEYS=ここに実際のAPIキー一覧を入力 - -# CORS 許可オリジン(カンマ区切り) -ALLOWED_ORIGINS=https://posimai-brain.vercel.app,https://posimai-reader.vercel.app,https://posimai-feed.vercel.app,http://localhost:3000 diff --git a/docs/synology/SYNOLOGY_PHASE1_DEPLOY.md b/docs/synology/SYNOLOGY_PHASE1_DEPLOY.md deleted file mode 100644 index c276a90a..00000000 --- a/docs/synology/SYNOLOGY_PHASE1_DEPLOY.md +++ /dev/null @@ -1,249 +0,0 @@ -# Phase 1: Brain 本文全文保存 — Synology 側 実施手順(コピペ用) - -**目的**: Reader から送られた本文を Brain API が受け取り DB に保存し、AI 分析で本文を使うようにする。 - -**前提**: Synology NAS に SSH で接続できること。Brain API と PostgreSQL がすでに動いていること。 - -**補足**: Synology の「Container Manager」は従来の「Docker」パッケージの後継です。中身は同じなので、SSH では `docker` コマンドが使えます。 - ---- - -## あなたの環境(docker ps の結果)について - -| コンテナ名 | イメージ | 用途の目安 | -|---------------|-----------------|--------------------| -| **posimai_api** | node:20-slim (8090) | **Brain API** の可能性が高い | -| **gitea_db** | postgres:15 | **Gitea 用** PostgreSQL(Brain 用ではない) | -| その他 | gitea, mcp_server, uptime-kuma 等 | - | - -**Brain 専用の PostgreSQL コンテナは一覧にありません。** - -- **posimai_api** が Brain API で、その中で **SQLite** や **別の DB** を使っている場合は、手順 1 の「PostgreSQL に接続」は**そのままでは使えません**。 -- まず **posimai_api(Brain API)のコードや設定**を確認し、「どの DB に接続しているか」を把握する必要があります。 - -**進め方の目安** - -1. **Brain 用に PostgreSQL を別コンテナで用意している場合** - → そのコンテナ名で手順 1 の「1-3. PostgreSQL に接続」を実行し、手順 1 → 2 の順で実施。 - -2. **Brain が SQLite など PostgreSQL 以外を使っている場合** - → **手順 1 はいったん飛ばし、手順 2(API のコード修正)だけ先に実施**してください。 - 保存処理で `full_text` を扱うようにし、使っている DB のスキーマに `full_text` カラム(または相当の項目)を後から追加する形になります。 - (SQLite なら、posimai_api のプロジェクト内のマイグレーションや、DB ファイルを開いて `ALTER TABLE` する方法などがあります。) - -3. **どこに DB があるか分からない場合** - posimai_api の設定ファイル(例: `.env` / `config.js` / 環境変数)や、ソース内の `pool` / `createPool` / `sqlite` などの記述を確認すると、接続先が分かります。 - ---- - -## 手順 1: データベースにカラムを追加する(Brain が PostgreSQL を使っている場合のみ) - -### 1-1. Synology に SSH 接続 - -```bash -ssh admin@posimai-lab.tail72e846.ts.net -``` -(パスワードを聞かれたら入力。ユーザー名が `mai` の場合は `ssh mai@posimai-lab.tail72e846.ts.net`) - -### 1-2. コンテナ名を確認する(重要) - -手順書の `posimai-brain-postgres` は**例**です。お使いの環境ではコンテナ名が違う場合があります。 - -**方法A: コマンドで一覧を表示** - -```bash -docker ps -``` - -表示された一覧の「NAMES」列で、PostgreSQL のコンテナ名を確認してください(例: `postgres` / `brain-postgres` / `synology-postgres` など)。 - -**方法B: DSM の Container Manager で確認** - -1. DSM にログイン → **Container Manager** を開く -2. 「コンテナ」一覧で、PostgreSQL または Brain 用 DB のコンテナを探す -3. コンテナ名(名前の列)をメモする - -**次の 1-3 で使うコマンドの `コンテナ名` を、上で確認した名前に置き換えてください。** - -### 1-3. PostgreSQL に接続 - -**Container Manager(Docker)で PostgreSQL を動かしている場合:** - -```bash -docker exec -it コンテナ名 psql -U brain_user -d brain_db -``` - -- `コンテナ名` を 1-2 で確認した名前に置き換えます。 -- 例: コンテナ名が `postgres` なら - `docker exec -it postgres psql -U brain_user -d brain_db` -- ユーザー名・DB 名(`brain_user` / `brain_db`)が違う場合は、実際の設定に合わせて変更してください。 - -**PostgreSQL を Docker 以外で動かしている場合(NAS に直接インストール等):** - -```bash -psql -U brain_user -d brain_db -``` - -### 1-4. 以下の SQL をそのままコピーして貼り付け、Enter で実行 - -```sql --- full_text カラムを追加(本文全文) -ALTER TABLE articles ADD COLUMN IF NOT EXISTS full_text TEXT; - --- オプション: 画像用(将来用) -ALTER TABLE articles ADD COLUMN IF NOT EXISTS images TEXT[]; - --- 更新日時用 -ALTER TABLE articles ADD COLUMN IF NOT EXISTS updated_at TIMESTAMP DEFAULT NOW(); - --- 確認(articles の構造が表示されればOK) -\d articles -``` - -`\d articles` の結果に `full_text` が含まれていれば成功です。 - -### 1-5. PostgreSQL を終了 - -```bash -\q -``` - ---- - -## 手順 2: Brain API のコードを修正する - -**posimai_api** が Brain API の場合、そのコンテナで動いているコード(NAS 上でマウントしているフォルダ内の `server.js` など)を編集します。 - -Brain API の `server.js`(またはルーターを定義しているファイル)を編集します。 - -**編集する場所の目安:** -- `POST /save` または `POST /brain/api/save` を処理しているブロック -- `GET /articles` または `GET /brain/api/articles` を処理しているブロック - -### 2-1. POST /save の置き換え - -**いまの `post('/save', ...)` ~ そのブロックの終わりまで** を、以下で**まるごと置き換え**してください。 - -```javascript -// POST /save — Reader から url, title, content, source を受け取り本文を保存 -app.post('/save', authMiddleware, async (req, res) => { - const { url, title, content, source } = req.body || {}; - if (!url || !title) { - return res.status(400).json({ error: 'Missing required fields: url and title' }); - } - try { - let fullText = content || null; - let meta = {}; - if (!fullText || fullText.trim().length === 0) { - meta = await fetchOGP(url); // 既存の OGP 取得関数 - fullText = meta.desc || ''; - } else { - meta = await fetchOGP(url); - } - const ai = await analyzeWithGemini(title, fullText, url); // 第2引数を fullText に変更 - const result = await pool.query(` - INSERT INTO articles (user_id, url, title, full_text, summary, topics, source, reading_time, favicon, og_image, status, created_at, updated_at) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, 'inbox', NOW(), NOW()) - ON CONFLICT (user_id, url) DO UPDATE SET - title = EXCLUDED.title, - full_text = EXCLUDED.full_text, - summary = EXCLUDED.summary, - topics = EXCLUDED.topics, - reading_time = EXCLUDED.reading_time, - favicon = EXCLUDED.favicon, - og_image = EXCLUDED.og_image, - updated_at = NOW() - RETURNING id - `, [req.userId, url, title, fullText, ai.summary, ai.topics, source || 'reader', ai.readingTime, meta.favicon || null, meta.ogImage || null]); - const articleId = result.rows[0].id; - return res.json({ success: true, articleId, fullTextSaved: !!fullText, textLength: fullText?.length || 0 }); - } catch (error) { - console.error('[Brain API] Save failed:', error); - return res.status(500).json({ error: 'Failed to save article', message: error.message }); - } -}); -``` - -**あわせて確認すること:** -- `analyzeWithGemini` の**第2引数**を、これまでの「短い説明」ではなく **本文全文(fullText)** に変更する。 -- `fetchOGP` はそのまま利用して問題ありません。 - -### 2-2. GET /articles の置き換え - -**いまの `get('/articles', ...)` ~ そのブロックの終わりまで** を、以下で**まるごと置き換え**してください。 - -```javascript -// GET /articles — 一覧取得時に full_text も返す(Brain UI で本文表示するため) -app.get('/articles', authMiddleware, async (req, res) => { - try { - const { rows } = await pool.query(` - SELECT id, url, title, full_text, summary, topics, source, reading_time, favicon, og_image, status, created_at, updated_at - FROM articles - WHERE user_id = $1 - ORDER BY created_at DESC - `, [req.userId]); - return res.json({ - articles: rows.map(row => ({ - id: row.id, - url: row.url, - title: row.title, - fullText: row.full_text, - summary: row.summary, - topics: row.topics, - source: row.source, - readingTime: row.reading_time, - favicon: row.favicon, - ogImage: row.og_image, - status: row.status, - createdAt: row.created_at, - updatedAt: row.updated_at - })) - }); - } catch (error) { - console.error('[Brain API] Failed to fetch articles:', error); - return res.status(500).json({ error: 'Failed to fetch articles', message: error.message }); - } -}); -``` - -**注意:** お使いのコードが `app` ではなく `router` の場合は、上記の `app.post` / `app.get` を `router.post` / `router.get` に読み替えてください。 - ---- - -## 手順 3: API サーバーを再起動する - -**Docker で Brain API を動かしている場合:** - -```bash -docker restart posimai-brain-api -docker logs -f posimai-brain-api -``` - -ログにエラーが出ていなければ成功です。`Ctrl+C` でログ表示を終了できます。 - -**Docker を使っていない場合:** -Brain API を起動している方法(systemd や手動実行など)に合わせて再起動してください。 - ---- - -## 手順 4: 動作確認 - -1. **Reader** で記事を開き、「Brainに保存」を押す。 -2. ブラウザの開発者ツール(F12)の「ネットワーク」で、`/save` へのレスポンスを確認する。 - - `fullTextSaved: true` かつ `textLength` が数百以上なら、本文が送れて保存されています。 -3. **Brain** の一覧を開き、該当記事の「本文を読む」が出ていれば、API と DB の対応は問題ありません。 - ---- - -## うまくいかないとき - -- **「column full_text does not exist」** - → 手順 1 の SQL が実行されていないか、別の DB を見ている可能性があります。もう一度 1-2~1-4 を実行してください。 -- **「analyzeWithGemini is not a function」** - → `analyzeWithGemini` の定義で、第2引数が「本文」になるように変更してください(従来の短い説明用引数を fullText に変更)。 -- **Reader から保存しても Brain に反映されない** - → Synology の Brain API のログ(`docker logs posimai-brain-api`)でエラーが出ていないか確認してください。 - ---- - -**次のステップ:** Brain のフロント(Vercel など)は、すでに「本文を読む」で保存済み本文を表示できるように修正済みです。Synology 側の上記対応が完了すれば Phase 1 は完了です。 diff --git a/docs/synology/SYNOLOGY_SAVE_FLOW_DIAGRAM.md b/docs/synology/SYNOLOGY_SAVE_FLOW_DIAGRAM.md deleted file mode 100644 index 4d479492..00000000 --- a/docs/synology/SYNOLOGY_SAVE_FLOW_DIAGRAM.md +++ /dev/null @@ -1,525 +0,0 @@ -# Synology 保存機能 — 構成とフロー図解 - -## 全体構成図 - -``` -┌─────────────────────────────────────────────────────────────────────┐ -│ Posimai エコシステム │ -├─────────────────────────────────────────────────────────────────────┤ -│ │ -│ ┌────────────┐ ┌────────────┐ ┌────────────┐ ┌─────────────┐ │ -│ │ Reader │ │ Feed │ │ Brain │ │ Maps │ │ -│ │ (Vercel) │ │ (Vercel) │ │ (Vercel) │ │ (Vercel) │ │ -│ │ │ │ │ │ │ │ │ │ -│ │ 記事閲覧 │ │ RSS収集 │ │ 一覧表示 │ │ 位置情報 │ │ -│ └─────┬──────┘ └─────┬──────┘ └─────┬──────┘ └─────────────┘ │ -│ │ │ │ │ -│ │ 「Brainに保存」ボタン │ │ -│ └────────────────┴────────────────┘ │ -│ │ │ -│ │ POST /brain/api/save │ -│ │ { url, title, content, source } │ -│ ▼ │ -│ ┌────────────────────────────────────────────┐ │ -│ │ Synology NAS (Home Server) │ │ -│ │ Tailscale: posimai-lab.tail72e846.ts.net │ │ -│ ├────────────────────────────────────────────┤ │ -│ │ ┌──────────────────────────────────────┐ │ │ -│ │ │ Brain API (Node.js + Express) │ │ │ -│ │ │ Port: 8090 │ │ │ -│ │ │ Container: posimai_api │ │ │ -│ │ │ │ │ │ -│ │ │ 1. 認証(JWT/APIキー) │ │ │ -│ │ │ 2. 本文処理(content || OGP取得) │ │ │ -│ │ │ 3. Gemini AI 分析 ──────────────┐ │ │ │ -│ │ │ 4. PostgreSQL保存 │ │ │ │ -│ │ └───────────────────────────┬────────┘ │ │ │ -│ │ │ │ │ │ -│ │ ┌───────────────────────────▼──────────┐ │ │ -│ │ │ PostgreSQL Database │ │ │ -│ │ │ Port: 5432 │ │ │ -│ │ │ Container: (別コンテナまたはSQLite) │ │ │ -│ │ │ │ │ │ -│ │ │ Table: articles │ │ │ -│ │ │ ・id, user_id, url, title │ │ │ -│ │ │ ・full_text (本文全文) ← Phase 1 │ │ │ -│ │ │ ・summary (AI要約) │ │ │ -│ │ │ ・topics (AIトピック抽出) │ │ │ -│ │ │ ・reading_time (読了時間) │ │ │ -│ │ │ ・favicon, og_image │ │ │ -│ │ │ ・status (inbox/favorite/archived) │ │ │ -│ │ └──────────────────────────────────────┘ │ │ -│ │ │ │ -│ │ ┌──────────────────────────────────────┐ │ │ -│ │ │ Gemini API (External) │ │ │ -│ │ │ Model: gemini-2.0-flash-exp │ │ │ -│ │ │ │ │ │ -│ │ │ Input: title + full_text (5000文字)│ │ │ -│ │ │ Output: │ │ │ -│ │ │ ・summary (3文の要約) │ │ │ -│ │ │ ・topics (2つのテーマ) │ │ │ -│ │ │ ・readingTime (推定分数) │ │ │ -│ │ └──────────────────────────────────────┘ │ │ -│ └────────────────────────────────────────────┘ │ -│ │ -└─────────────────────────────────────────────────────────────────────┘ -``` - ---- - -## 保存フロー詳細(シーケンス図) - -### 1. Reader/Feed → Brain 保存フロー - -``` -┌─────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────┐ -│ Reader │ │ Synology │ │ PostgreSQL │ │ Gemini │ -│ (Vercel)│ │ Brain API │ │ Database │ │ API │ -└────┬────┘ └──────┬───────┘ └──────┬───────┘ └────┬─────┘ - │ │ │ │ - │ 1. ユーザーが │ │ │ - │ 「Brainに保存」 │ │ │ - │ ボタンをクリック │ │ │ - │ │ │ │ - │ 2. POST /save │ │ │ - │ { │ │ │ - │ url: "...", │ │ │ - │ title: "...", │ │ │ - │ content: "...", │ (本文全文) │ │ - │ source: "reader"│ │ │ - │ } │ │ │ - ├───────────────────>│ │ │ - │ │ │ │ - │ │ 3. 認証チェック │ │ - │ │ (JWT/APIキー) │ │ - │ │ │ │ - │ │ 4. 本文取得 │ │ - │ │ ・content あり │ │ - │ │ → そのまま使用 │ │ - │ │ ・content なし │ │ - │ │ → fetchOGP() │ │ - │ │ (OGP description │ │ - │ │ を代替使用) │ │ - │ │ │ │ - │ │ 5. AI分析リクエスト │ │ - │ │ analyzeWithGemini( │ │ - │ │ title, │ │ - │ │ fullText, │ │ - │ │ url │ │ - │ │ ) │ │ - │ ├───────────────────────────────────────────>│ - │ │ │ │ - │ │ │ 6. AI処理 │ - │ │ │ ・要約生成 │ - │ │ │ ・トピック抽出 │ - │ │ │ ・読了時間推定 │ - │ │ │ (5000文字制限) │ - │ │ │ │ - │ │ 7. AI分析結果 │ │ - │ │ { │ │ - │ │ summary: "...", │ │ - │ │ topics: [...], │ │ - │ │ readingTime: 5 │ │ - │ │ } │ │ - │ │<──────────────────────────────────────────┤ - │ │ │ │ - │ │ 8. DB保存 │ │ - │ │ INSERT INTO articles │ │ - │ │ (user_id, url, │ │ - │ │ title, full_text, │ ← Phase 1 新規 │ - │ │ summary, topics, │ │ - │ │ source, ...) │ │ - │ │ ON CONFLICT UPDATE │ (重複時は更新) │ - │ ├──────────────────────>│ │ - │ │ │ │ - │ │ 9. 保存完了 │ │ - │ │<──────────────────────┤ │ - │ │ │ │ - │ 10. レスポンス │ │ │ - │ { │ │ │ - │ success: true, │ │ │ - │ articleId: 123, │ │ │ - │ fullTextSaved: │ │ │ - │ true, │ │ │ - │ textLength: 2500│ │ │ - │ } │ │ │ - │<───────────────────┤ │ │ - │ │ │ │ - │ 11. UI フィードバック │ │ - │ ・ボタンを緑色に │ │ │ - │ ・「保存済み」表示 │ │ │ - │ │ │ │ -``` - ---- - -### 2. Brain 一覧表示フロー - -``` -┌─────────┐ ┌──────────────┐ ┌──────────────┐ -│ Brain │ │ Synology │ │ PostgreSQL │ -│ (Vercel)│ │ Brain API │ │ Database │ -└────┬────┘ └──────┬───────┘ └──────┬───────┘ - │ │ │ - │ 1. ページを開く │ │ - │ (初回ロード) │ │ - │ │ │ - │ 2. GET /articles │ │ - │ Authorization: │ │ - │ Bearer pk_xxx │ │ - ├───────────────────>│ │ - │ │ │ - │ │ 3. 認証チェック │ - │ │ │ - │ │ 4. 記事一覧取得 │ - │ │ SELECT id, url, │ - │ │ title, full_text, │ ← Phase 1 追加 - │ │ summary, topics, │ - │ │ ... │ - │ │ FROM articles │ - │ │ WHERE user_id = $1 │ - │ │ ORDER BY │ - │ │ created_at DESC │ - │ ├──────────────────────>│ - │ │ │ - │ │ 5. 記事データ │ - │ │<──────────────────────┤ - │ │ │ - │ 6. JSON レスポンス│ │ - │ { │ │ - │ articles: [ │ │ - │ { │ │ - │ id: 123, │ │ - │ url: "...", │ │ - │ title: "...│ │ - │ fullText: │ ← Phase 1 新規 │ - │ "...", │ (本文表示用) │ - │ summary: │ │ - │ "...", │ │ - │ topics: [...│ │ - │ } │ │ - │ ] │ │ - │ } │ │ - │<───────────────────┤ │ - │ │ │ - │ 7. UI レンダリング│ │ - │ ・カード一覧表示 │ │ - │ ・クリックで詳細 │ │ - │ モーダル表示 │ │ - │ │ │ -``` - ---- - -### 3. Brain 本文表示フロー(詳細モーダル) - -``` -┌─────────┐ ┌──────────────┐ -│ Brain │ │ Memory │ -│ UI │ │ Cache │ -└────┬────┘ └──────┬───────┘ - │ │ - │ 1. カードをクリック│ - │ openArticleDetail │ - │ Modal(article) │ - │ │ - │ 2. キャッシュ確認 │ - │ fullTextCache │ - │ .has(article.id)│ - ├───────────────────>│ - │ │ - │ 3a. キャッシュHIT │ - │ (2回目以降) │ - │<───────────────────┤ - │ fullText を即表示 │ - │ (APIコールなし) │ - │ │ - │ 3b. キャッシュMISS│ - │ (初回) │ - │ │ - │ 4. article.fullText│ - │ がすでに含まれて │ - │ いる場合 │ - │ → 直接表示 │ - │ (Phase 1で対応済み)│ - │ │ - │ 5. Markdown変換 │ - │ renderMarkdown( │ - │ fullText │ - │ ) │ - │ ・# →