#24 技術解説 Chrome拡張機能「Gmap口コミAnalytics」技術解説

全体アーキテクチャ

以下の4つの枠組みで、この拡張機能の技術的な仕組みを言語化します。


1️⃣ 「何のために」を言語化する(目的の抽象化)

A. ポップアップUI(popup.html/js)

何のために存在するのか
ユーザーがGoogle Mapsを閲覧している最中に、分析を開始するための「入り口」を用意する仕組み。

ユーザーの困り
Google Mapsの場所ページを開いても、口コミの良し悪しが一目ではわかりません。100件以上あれば尚更。手作業で星の分布を数えたり、キーワードを探したりする手間が発生します。

現実世界の比喩
飲食店の口コミ一覧表を見ているのに、「この店の強みは?弱点は?」を判断するまでに全部読まないといけない。ポップアップはその場で「分析ボタン一つで、別紙の分析レポートを自動作成してくれる秘書」です。


B. スクレイピング機能(content.js)

何のために存在するのか
Google Mapsの場所ページから、見えている画面の奥にある全ての口コミデータを自動的に取得する仕組み。

ユーザーの困り
Google Mapsの口コミは「最新順に表示」できても、全件をまとめてダウンロードできません。スクロールして「もっと見る」ボタンを何度も何度も手作業で押す必要があります。

現実世界の比喩
アマゾン倉庫の棚から、お客さんが欲しい商品を全て集めるロボット。人間が「この商品ください」と言ったら、スクロールして・箱から出して・リストにまとめて差し出す。それがcontent.js。


C. ダッシュボード表示(dashboard.html/js)

何のために存在するのか
取得した生の口コミデータを、7つの視点で自動分析し、意思決定に使える「レポート」に変換する仕組み。

ユーザーの困り
200件の口コミテキストを渡されても、「この店の本当の評判は?」という経営判断に使えません。「星4と星2の割合は?」「接客の満足度は?」といった構造化された情報が必要です。

現実世界の比喩
医者が患者の症状(口コミ)を聞いて、問診票・体温・血液検査・レントゲン・脳波などの「7つの視点」で診断し、最終的に「病名」と「治療方針」を提示する。ダッシュボードはそのカルテです。


2️⃣ データの「ビフォー・アフター」を追う(データフロー)

✅ 全体フロー図

【ビフォー:ユーザーの現在状態】
Google Mapsページ
    ↓
    └─→ スクロール見える範囲:3~5件の口コミ
        もっと見るボタン:何度も手作業

【処理】
popupアイコンクリック
    ↓
content.jsが起動:ページの言語を読み込む
    ↓
自動スクロール・「もっと見る」ボタンを自動で何度も押す
    ↓
全ての口コミのHTMLを抽出:星・日付・テキスト・著者名など
    ↓
JSON形式に正規化:機械が処理しやすい形に変換
    ↓
chrome.storage.localに一時保存
    ↓
新タブで dashboard.html を開く

【アフター:ユーザーが手にする情報】
分析ダッシュボード
    ├─ 平均評価:4.2 ⭐
    ├─ 最近3ヶ月:上昇傾向 📈
    ├─ 星の分布:二極化している ⚠️
    ├─ 頻出キーワード:「美味しい」「待たされ」
    ├─ 5大要素の満足度グラフ
    ├─ 月別投稿頻度チャート
    ├─ ローカルガイド比率:20%
    └─ 返信率:75%(低評価への対応)

詳細フロー(ファイル別)

⚙️ popup.js のフロー

【入力】
 └─ ユーザーが拡張アイコンをクリック
    └─ 現在のタブURL(例:google.com/maps/place/...)

【処理】
 1. URLが「Google Maps場所ページ」かどうかを判定
    └─ 正規表現で検証:/google\.(com|co\.jp)\/maps\/place/
 
 2. 正規表現マッチ → content.jsを該当タブに注入
    └─ chrome.scripting.executeScript()で動的に読み込み
 
 3. ポートを開いて content.js と通信を確立
    └─ chrome.tabs.connect()で双方向パイプ作成
 
 4. 「スクレイピング開始」ボタンをユーザーが押す
    └─ port.postMessage() で content.js に命令を送信

 5. content.js から「進捗」メッセージを受信
    └─ プログレスバーを段階的に更新(0% → 100%)

 6. content.js から「完了」メッセージを受信
    └─ JSON形式の分析データ(口コミ配列など)

 7. データを chrome.storage.local に保存
    └─ chrome.storage.local.set({ analysisData: data })

 8. 新しいタブで dashboard.html を開く
    └─ chrome.tabs.create({ url: 'dashboard.html' })

【出力】
 └─ dashboard.html がロードされ、分析ダッシュボード表示開始

⚙️ content.js のフロー

【入力】
 └─ popup.js から port.postMessage({ action: 'startScraping', maxReviews: 50 })

【処理】
 1. 「クチコミ」タブをクリック(リビューパネルを開く)
    └─ aria-label属性で該当ボタンを探す
    └─ element.click()で自動クリック

 2. 「新しい順」に並べ替え(最新レビューから取得)
    └─ ソートドロップダウン → 「新しい順」をクリック

 3. リビューパネルのスクロールコンテナを特定
    └─ DOM構造:.m6QErb.DxyBCb クラスを探す

 4. [ループ開始] 指定件数まで、またはスクロール停止まで
    
    a) 「もっと見る」ボタンを全て自動クリック
       └─ element.click()で拡張テキストを表示
    
    b) 現在表示中の全レビューを抽出・解析
       └─ querySelector()で各要素を探す
       └─ 星:.kvMYJc の aria-label から数字を抽出
       └─ テキスト:.wiI7pd から全文取得
       └─ 日付:.rsqaWe から「2 か月前」などを取得
       └─ 著者:.d4r55 から名前を取得
       └─ ローカルガイド:.RfnDt に「ローカルガイド」という文字があるか
    
    c) 重複排除:同じ著者&同じ日時のレビューは除外
    
    d) 進捗をポップアップに送信
       └─ port.postMessage({ type: 'progress', loaded: 10, total: 50 })
    
    e) スクロール速度制限(2秒待機)
       └─ DOMが新しい要素をロードするのを待つ
    
    f) スクロールが止まった? → カウンター+1
       └─ 6回連続でスクロール停止なら終了
 
 5. [ループ終了]

 6. 場所情報(名前・住所・カテゴリ・総評点)を再度取得
    └─ このタイミングで取得理由:スクレイピング中に更新されている可能性

 7. 全データを JSON 形式でまとめる
    {
      place: { name, rating, totalReviews, address, category },
      reviews: [ 
        { authorName, stars, date, text, isLocalGuide, photoCount, ownerResponse }
      ],
      scrapedAt: ISO8601タイムスタンプ
    }

 8. 完了信号を popup.js に送信
    └─ port.postMessage({ type: 'complete', data: {...} })

【出力】
 └─ JSON形式の構造化データ(popup.js がこれを受け取る)

⚙️ dashboard.js のフロー

【入力】
 └─ chrome.storage.local から分析データを読み込み
    └─ const { analysisData } = await chrome.storage.local.get('analysisData')

【処理】

 ≪ステップ 1: 定量的指標の分析 ≫
 reviews配列の全要素に対して:
   └─ 星の合計を数える → 平均値を計算
   └─ 星の分布を集計:[1つ星の件数, 2つ星...5つ星の件数]
   └─ 直近3ヶ月のレビューのみ抽出 → 平均値を再計算
   └─ 月ごとにグループ化 → 投稿頻度をチャート化
   └─ 「星5と星1の件数」 > 「星2~4の件数」? → 二極化判定

 ≪ステップ 2: 定性的内容(キーワード)の分析 ≫
 全レビューテキストに対して:
   └─ 定義済みのポジティブキーワード配列と照らし合わせ
   └─ マッチしたキーワードの出現回数をカウント
   └─ 同じく、ネガティブキーワードも集計
   └─ 出現順でソート(多い順)
   
   └─ 具体的エピソード検出:
      テキスト長 > 40字 かつ (著者名敬称 || 動作動詞 || 心遣い表現) → 抽出
   
   └─ 期待値ギャップ検出:
      (期待関連キーワード AND ギャップ関連キーワード)→ 抽出

 ≪ステップ 3: 5大要素の分析 ≫
 5カテゴリ [商品, 接客, 価格, 空間, 利便性] に対して、各カテゴリごと:
   └─ 該当キーワードを含むレビューを抽出
   └─ そのうち、ポジティブキーワードも含む件数をカウント
   └─ 満足率 = ポジティブ件数 / 該当件数 × 100%
   └─ 該当レビュー内の星の平均値も計算

 ≪ステップ 4: 時間軸トレンド ≫
 月ごと・曜日ごとのグループ化:
   └─ キーの抽出:「2024-01」「平日」「土日」など
   └─ 各グループの平均スコアを計算
   
   └─ 改善検出:
      「リニューアル」「新メニュー」など関連キーワードのレビューを抽出

 ≪ステップ 5: 投稿者属性 ≫
   └─ isLocalGuide === true のレビュー数をカウント
   └─ hasPhotos === true のレビュー数をカウント
   └─ 各グループの平均スコアを計算(ローカルガイド vs 一般)

 ≪ステップ 6: 競合比較 ≫
   └─ 「他の店」「比べ」などのキーワードを含むレビューを抽出
   └─ 「ここだけ」「唯一」などのキーワード+高評価レビューを強み として抽出
   └─ 「この辺り」などのキーワード+低評価レビューをエリア課題として抽出

 ≪ステップ 7: 運営対応力 ≫
   └─ ownerResponse フィールドが null でない件数 / 全体 = 返信率
   └─ 低評価(星1~2)のうち、返信がある件数を別途集計
   └─ 返信文の最初30文字が重複 → 定型文の傾向を判定
   └─ 返信サンプルとして最初の3件を抽出

【出力】
 └─ 7つの分析オブジェクト:
    {
      quant: { avg, dist, avg3, monthly, isPolarized },
      qual: { posHits, negHits, episodes, gaps },
      five: [ { name, satisfaction, mentions, avgStars } × 5 ],
      time: { monthlyData, dayPatterns, improvementMentions },
      reviewer: { localGuideRate, photoRate, lgAvg },
      comp: { compMentions, strengths, areaIssues },
      owner: { rate, negRate, isTemplate, samples }
    }
 
 └─ これらを HTML にレンダリング:
    └─ renderSummary() 総合サマリー表示
    └─ renderQuantitative() 定量的グラフ描画
    └─ ... × 7 セクション
    └─ DOM に追記し、ユーザーがブラウザで閲覧可能に

3️⃣ 「なぜその順序なのか」を解き明かす(制御構造)

🔄 popup.js の実行順序と分岐

1【最初】ページ読み込み完了
   └─ DOMContentLoaded イベント発火

2【判定】現在のタブ情報を取得
   ├─ 成功した?
   │  └─ YES → 次へ
   │  └─ NO  → エラー表示して終了 ⛔

3【判定】タブのURLが Google Maps 形式か?
   ├─ マッチした(google.com/maps/place)?
   │  └─ YES → ポップアップにUI表示 ✅
   │  └─ NO  → 「Google Mapsページを開いてください」と表示

4【準備】content.js を注入(初回のみ実行)
   └─ chrome.scripting.executeScript()
   └─ 既に注入済み or 失敗してもエラーにしない(二重実行防止)

5【接続】popup ↔ content 間に通信パイプを開く
   └─ chrome.tabs.connect()
   └─ ポート名 'gmap-scraper' で識別

6【待機】ユーザーが「分析開始」ボタンをクリックするのを待つ
   └─ イベントリスナー:startBtn.addEventListener('click', ...)

7【送信】content.js に「スクレイピング開始」命令
   └─ port.postMessage({ action: 'startScraping', maxReviews: 50 })

8【受け取り】content.js から「進捗」メッセージが来る度に
   ├─ プログレスバー更新
   ├─ ステータステキスト更新
   └─ 待機継続

9【判定】「完了」メッセージが来た?
   ├─ YES → ステップ10へ
   ├─ NO(エラー)→ エラー表示、ボタン再有効化

10【保存】データを chrome.storage.local に保存
   └─ chrome.storage.local.set({ analysisData: data })

11【遷移】新しいタブで dashboard.html を開く
   └─ chrome.tabs.create({ url: 'dashboard.html' })

12【完了】ユーザーが新タブで分析結果を閲覧開始

優先順位の理由

  • なぜ「ページ判定」を最初に?
    Google Maps以外のページなら、以後の全ての処理が無駄になるため。「駅名を確認してから乗車券を買う」のと同じ。
  • なぜ「content注入」の前に「ポート接続」をしない?
    content.js が注入されていなければ、ポート接続に失敗するから。「スタッフを呼ぶ前に、相手がその場所にいるか確認する」のと同じ。
  • なぜ「プログレス表示」を長時間続ける?
    スクレイピングは2~5分かかるため、ユーザーに「進行中」という安心感を与えるため。反応しないアプリは、ユーザーに「フリーズしているのでは?」という不安を与える。
  • なぜ「storage保存」の後に「タブ作成」?
    dashboard.html が読み込まれる際、storage.local からデータを読む。そのため、データが先に保存されていないと dashboard は空のままになる。

🔄 content.js の実行順序と分岐

1【初期化】このスクリプトが既に実行済みか判定
   ├─ window._gmapAnalyticsInit === true ?
   │  └─ YES → 以後の全処理をスキップ(二重実行防止)
   │  └─ NO  → フラグを立てて続行
   └─ 理由:popup が複数回接続しても、スクリプトは一度だけ実行したい

2【待機】popup からの接続(port)を待つ
   └─ chrome.runtime.onConnect.addListener()
   └─ ポート名の確認:name === 'gmap-scraper' か?

3【受信】popup から「getPlaceInfo」メッセージ
   └─ 場所名・住所・カテゴリなどを抽出して返信

4【受信】popup から「startScraping」メッセージ
   ├─ 【ステップ 4-1】リビュータブをクリック
   │  └─ document.querySelector() で該当ボタンを探す
   │  └─ なぜ? Google Maps は複数のタブ(情報、写真、クチコミなど)を持つ
   │  └─ クチコミ以外のタブに口コミデータはない
   │
   ├─ 【ステップ 4-2】「新しい順」に並べ替え
   │  └─ ドロップダウンメニューを開く
   │  └─ 「新しい順」オプションを選択
   │  └─ なぜ? 最新の評価傾向を反映したいから。古い口コミだけでは現在の評判がわからない
   │
   ├─ 【ステップ 4-3】スクロールコンテナを特定
   │  └─ Google Maps の DOM 構造から .m6QErb.DxyBCb を探す
   │  └─ なぜ? このコンテナをスクロールしないと、下の口コミは読み込まれない
   │
   ├─ 【ステップ 4-4】[ループ開始] maxReviews に達するか、スクロール停止まで
   │  │
   │  ├─ 4-4-a) 「もっと見る」ボタンを全てクリック
   │  │  └─ 長いテキストがある場合、ボタンが表示される
   │  │  └─ クリックすると全文が表示される
   │  │
   │  ├─ 4-4-b) 全レビュー要素を抽出
   │  │  └─ document.querySelectorAll('.jftiEf') で全口コミコンテナを取得
   │  │
   │  ├─ 4-4-c) 各レビューから個別情報を抽出
   │  │  ├─ 星:スクリーンリーダー用の aria-label から数字を正規表現で取得
   │  │  │  └─ なぜ aria-label? 画面上の視覚的な「★★★★☆」は画像の場合があり、テキスト化しにくいから
   │  │  ├─ テキスト:.wiI7pd の innerText を取得
   │  │  ├─ 日付:.rsqaWe から「2か月前」などの相対日付を取得
   │  │  │  └─ 後で parseRelativeDate() で実日付に変換
   │  │  ├─ 著者名:.d4r55 から取得
   │  │  ├─ ローカルガイド:.RfnDt に「ローカルガイド」テキストがあるか判定
   │  │  ├─ 写真数:.KtCyie 要素の個数を数える
   │  │  └─ オーナー返信:.CDe7pd から返信テキスト&日付を取得(あれば)
   │  │
   │  ├─ 4-4-d) 重複排除
   │  │  └─ 既に reviews 配列に同じレビューがあれば追加しない
   │  │  └─ なぜ? スクロール中に同じレビューが複数回表示される可能性があるから
   │  │
   │  ├─ 4-4-e) 進捗を popup に報告
   │  │  └─ port.postMessage({ type: 'progress', loaded: 100, total: 200 })
   │  │
   │  ├─ 4-4-f) 目標件数に達した? → ループ終了
   │  │  └─ reviews.length >= maxReviews なら break
   │  │
   │  ├─ 4-4-g) スクロール速度制限
   │  │  └─ 2秒待機(sleep(2000))
   │  │  └─ なぜ待つ? DOMが新しい要素をロードするのに時間がかかるから。
   │  │  └─ 待たずにスクロールすると、キャッシュから古いデータを読む
   │  │
   │  ├─ 4-4-h) スクロール位置を最下部に
   │  │  └─ scrollContainer.scrollTop = scrollContainer.scrollHeight
   │  │
   │  ├─ 4-4-i) スクロール後の高さが変わった?
   │  │  ├─ 変わった → 新しい要素が読み込まれた、ループ続行
   │  │  └─ 変わらない → スタレカウント+1
   │  │  └─ なぜ? スクロール停止=「もう新しい口コミがない」の合図
   │  │
   │  └─ 4-4-j) スタレカウント >= 6 ? → ループ終了
   │     └─ 6回連続でスクロール停止なら、確実に全口コミを取得したと判定
   │
   ├─ 【ステップ 4-5】場所情報を再度取得
   │  └─ スクレイピング中に場所情報が更新されている可能性があるから
   │
   ├─ 【ステップ 4-6】全データをまとめて JSON 化
   │  └─ { place: {...}, reviews: [...], scrapedAt: ... }
   │
   └─ 【ステップ 4-7】popup に「完了」を通知
      └─ port.postMessage({ type: 'complete', data: {...} })

5【終了】ポート接続を閉じる(自動)
   └─ popup が受け取ったら、popup が dashboard.html を開く

優先順位の理由

  • なぜ「二重実行防止」を最初に?
    popup が複数回クリックされると、content.js が複数回実行される。その度にリスナーが増え、メモリリークや二重処理につながるから。
  • なぜ「リビュータブ切り替え」を最初に、「取得」の前に?
    Google Maps のデフォルトは「情報」タブ。クチコミタブを開かないと、DOM に口コミ要素がない。
  • なぜ「並べ替え」をすぐ?
    古い順で取得して分析すると、「3年前の悪い評判に基づいた分析」になる。経営判断に使えない。
  • なぜ「sleep 2秒」を入れる?
    Google Maps は遅延読み込み。スクロールしてから2秒かけて新要素をレンダリングする。待たずに読むと、スクロール前の古いDOM を読む。
  • なぜ「重複排除」をループ内でする?
    スクロール中に、一度表示されたレビューが画面上部に戻ってくることがある。排除しないと、同じレビューが何度も集計されて、統計が歪む。
  • なぜ「スタレカウント 6回」の判定?
    1度目は偽陽性(たまたまロード中)の可能性があるから、複数回確認してから終了判定。これが「6回」の理由は、経験的な閾値。

🔄 dashboard.js の分析の順序と理由

1【最初】データ読み込み
   └─ chrome.storage.local から analysisData を取得
   └─ なぜここで? 分析に必要な全データが揃うのはここで初めて

2【定量的指標】最初に計算
   ├─ 理由:以後の全分析の「ベースラインスコア」になるから
   ├─ 平均スコア、星の分布、二極化判定
   └─ これらがないと、他の分析結果の「良い/悪い」が判断できない

3【定性的分析】次に実行
   ├─ 理由:テキストマイニングは計算コストが高いから、早期に実行
   ├─ キーワード集計、エピソード抽出
   └─ その後の「5大要素」分析で、このキーワード情報を再利用

4【5大要素分析】定性の後に実行
   ├─ 理由:「商品」「接客」などのカテゴリキーワードを確定させてから
   │        各カテゴリの満足率を計算したいから
   └─ 定性分析で抽出したキーワードを「どのカテゴリに属するか」で分類

5【時間軸分析】中盤で実行
   ├─ 理由:月ごと・曜日ごとのグループ化に時間がかかるから
   │        計算重たい処理は早めに始めたい
   └─ また「時系列グラフ」を描画するには、複数月のデータが必要

6【投稿者属性分析】相対的に軽い処理だから後回し
   ├─ 理由:isLocalGuide フラグをカウントするだけ、計算が簡単
   └─ しかし他の分析より優先度は低い

7【競合比較】やや軽い処理だから後回し
   ├─ 理由:「他の店」「比べ」などのキーワードマッチングだけ
   └─ 定性分析で既に全テキストを読み込んでいるので、追加コスト少なめ

8【運営対応力】最後に分析
   ├─ 理由:最も優先度低い
   ├─ 返信率の集計も、定型文判定も、計算が軽い
   └─ これはオマケ情報なので、最後でいい

9【レンダリング】分析の後に一括実行
   └─ 全ての分析結果が揃ってから、HTML描画を開始
   └─ なぜ最後? 分析中に DOM を触ると、再レンダリングでブラウザが重くなるから

4️⃣ 依存関係を「家系図」にする(コンポーネント構成)

📦 外部依存関係

Chrome拡張機能(Gmap口コミAnalytics)
│
├─ 【親】 Google Chrome ブラウザ
│  ├─ chrome.tabs API
│  │  └─ tabs.query() : 現在のタブ情報取得
│  │  └─ tabs.connect() : content.js との通信ポート確立
│  │  └─ tabs.create() : 新しいタブ作成
│  │
│  ├─ chrome.scripting API
│  │  └─ executeScript() : content.js をターゲットタブに注入
│  │
│  ├─ chrome.runtime API
│  │  └─ onConnect : ポート接続リスナー
│  │  └─ sendMessage, postMessage : IPC通信
│  │
│  ├─ chrome.storage API
│  │  └─ storage.local.get/set() : 拡張機能のローカルストレージ
│  │
│  └─ DOM API(全てのスクリプトで利用)
│     ├─ document.querySelector/querySelectorAll
│     ├─ element.click()
│     ├─ element.textContent / innerHTML
│     └─ ... etc.
│
├─ 【親】 Google Maps ウェブサイト
│  ├─ HTML/CSS/JavaScript(Google Maps のページ構造)
│  │  └─ DOM構造 : .m6QErb, .jftiEf, .wiI7pd などのクラス名
│  │  └─ これらは Google が制御していて、更新時に変わる可能性あり
│  │  └─ 👉 **脆弱性:拡張機能の保守が必要になる可能性**
│  │
│  └─ 非同期読み込み機構(Lazy Loading)
│     └─ スクロール時に段階的にレビューをロード
│     └─ content.js の「sleep 2秒待機」はこれに対応するため

🏗️ 内部コンポーネント構成

ユーザー
 │
 └─ [popup.html 画面]
    │  (UI:ボタン、スライダー、プログレスバー)
    │
    ├─ popup.js (ロジック層)
    │  ├─ 機能①:Google Maps 判定
    │  │  └─ chrome.tabs.query() を呼び出す(Chrome API の子)
    │  │
    │  ├─ 機能②:content.js 注入
    │  │  └─ chrome.scripting.executeScript() を呼び出す(Chrome API の子)
    │  │
    │  ├─ 機能③:ポート接続・通信管理
    │  │  └─ chrome.tabs.connect() で content.js と繋ぐ(Chrome API の子)
    │  │  └─ port.postMessage() で送信(Chrome API の子)
    │  │  └─ port.onMessage.addListener() で受信(Chrome API の子)
    │  │
    │  └─ 機能④:ダッシュボード起動
    │     └─ chrome.tabs.create() で新タブ作成(Chrome API の子)
    │     └─ chrome.storage.local.set() でデータ保存(Chrome API の子)
    │
    └─ popup.css (見た目)
       └─ グラデーション、カード、ボタンスタイル

─────────────────────────────────────

[Google Maps ページ]
 │
 └─ content.js (ページ内で実行される)
    │
    ├─ 機能①:Google Maps の DOM 解析
    │  └─ document.querySelector() で要素取得
    │  └─ 依存:Google Maps の HTML 構造
    │     👉 脆弱性:Google が DOM 構造を変更すると動作しなくなる
    │
    ├─ 機能②:自動操作(クリック・スクロール)
    │  ├─ element.click() でボタンクリック
    │  └─ scrollContainer.scrollTop でスクロール
    │
    ├─ 機能③:データ抽出
    │  ├─ element.textContent でテキスト取得
    │  ├─ element.getAttribute() で属性値取得
    │  └─ 正規表現で日付・数字を抽出
    │
    ├─ 機能④:ポート通信(popup.js との双方向)
    │  ├─ chrome.runtime.onConnect() でリスナー
    │  ├─ port.postMessage() で送信(進捗・完了報告)
    │  └─ port.onMessage() で受信(命令待機)
    │
    └─ 機能⑤:タイミング制御
       └─ sleep() 関数で遅延読み込み待機

─────────────────────────────────────

[ダッシュボード]
 │
 ├─ dashboard.html (UI 骨組み)
 │  └─ <section> で 7つ の分析エリアを定義
 │
 ├─ dashboard.css (見た目)
 │  ├─ グリッドレイアウト、カード、グラフ(CSS棒グラフ)
 │  └─ カラースキーム(Google 風のブルー・グリーン・イエロー)
 │
 └─ dashboard.js (ロジック+レンダリング)
    │
    ├─ 機能①:データ読み込み
    │  └─ chrome.storage.local.get('analysisData')(Chrome API の子)
    │
    ├─ 機能②:7つの分析エンジン
    │  ├─ analyzeQuantitative() : 平均・分布・トレンド
    │  ├─ analyzeQualitative() : キーワード抽出
    │  ├─ analyzeFiveElements() : 5カテゴリ満足度
    │  ├─ analyzeTimeTrends() : 月別・曜日別分析
    │  ├─ analyzeReviewerAttributes() : ローカルガイド・写真比率
    │  ├─ analyzeCompetitive() : 競合比較言及
    │  └─ analyzeOwnerResponse() : 返信率・質
    │
    ├─ 機能③:キーワード辞書(内製)
    │  └─ KW オブジェクト
    │     ├─ KW.positive : 100+ ポジティブワード
    │     ├─ KW.negative : 100+ ネガティブワード
    │     ├─ KW.product, KW.hospitality, ... : カテゴリ別キーワード
    │     └─ 👉 メンテナンス:言葉は時代で変わるため、定期更新が必要
    │
    ├─ 機能④:HTML レンダリング
    │  ├─ renderSummary() : サマリーカード
    │  ├─ renderQuantitative() : 定量的セクション
    │  ├─ ... × 7 : 各セクション用の描画関数
    │  └─ DOM に innerHTML で追記
    │
    └─ 機能⑤:ユーティリティ関数
       ├─ countKeywords() : テキスト内のキーワード出現回数
       ├─ sortedEntries() : オブジェクトをソート
       ├─ escHtml() : HTML エスケープ(XSS対策)
       └─ parseRelativeDate() : 「2か月前」を実日付に変換

─────────────────────────────────────

【データフロー】自分で完結している部分 vs 外部に頼っている部分

自分で完結:
  • キーワード抽出ロジック(KW辞書 + マッチング)
  • 5大要素の満足度算出
  • 統計計算(平均・分布・トレンド)
  • HTML 描画(素の JavaScript + CSS)

外部に頼っている(脆弱性):
  ✅ Chrome API :
    └─ 将来の Chrome バージョンで API が廃止される可能性
    └─ Manifest V3 への対応が必要(Manifest V2 は廃止予定)
  
  ✅ Google Maps の DOM 構造 :
    └─ 【最大のリスク】
    └─ Google が UI をアップデートすると、セレクタが変わる
    └─ 例:.jftiEf → .review-container に変更されたら、content.js は動作しなくなる
    └─ 対策:複数セレクタの fallback を用意(現在のコードは一部対応)

  ✅ 言語(日本語) :
    └─ キーワード辞書が日本語の時流に追従する必要あり
    └─ 新しい俗語が出ると、取りこぼしが発生
    └─ 例:「ぴえん」「推し活」などの新語への対応

📊 総括:依存関係マップ

最強固い(変わらない)
  ↑
  │ ┌─── 標準的な DOM API(querySelector など)
  │ │    (W3C 標準だから、ブラウザが廃止しない)
  │ │
  │ ├─── 標準的な JavaScript(正規表現、配列操作など)
  │ │    (ECMAScript 仕様に従っている)
  │ │
  │ └─── 自作のキーワード辞書
  │      (コントローラブルだが、メンテナンスコスト)
  │
  ├─── Chrome 拡張 API(tabs, runtime, storage)
  │    (比較的安定、Manifest V3 対応が必須)
  │
  └─── Google Maps の DOM 構造
       (最も脆弱性が高い)
       (Google が予告なく変更する可能性)
       
最も脆い(変わりやすい)
  ↓

🎯 まとめ

観点要点
1. 何のためにユーザーが 1クリック で、Google Maps の全口コミを自動スクレイピング&7観点の分析レポートを生成する「秘書」
2. データフローGoogle Maps(見える範囲)→ 自動スクロール取得 → JSON正規化 → 7つの分析エンジン → HTML可視化
3. 実行順序最初は「前提条件チェック」→「低コスト処理」→「計算重い処理」→「UI描画」の順で効率化
4. 依存関係Chrome API 依存(堅牢)← → Google Maps DOM 依存(脆弱)

Don`t copy text!