# 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 を更新してから実装に進んでください。