LINE Friends Tracker — システム解説書

LINE 公式アカウントの友だち数を毎日自動取得し、Web ダッシュボードと Lark 通知で可視化するシステムの構成・処理内容・データベース・運用情報を網羅したスタッフ向け資料。

プロジェクト名
line-friend-tracker
対象アカウント
新さきの海外不動産 / 旧さきの海外不動産
Web URL
https://line-friend-tracker.vercel.app/
リポジトリ
github.com/sugiura-7338/line-friend-tracker
資料作成日
2026-05-16
対象バージョン
commit 8d689f7 時点

プロジェクト概要

目的

LINE 公式アカウント(L ステップ・UTAGE 接続)の友だち数(累計追加・有効友だち・累計ブロック)の推移を毎日自動で記録し、ブラウザでいつでも確認できる状態にする。あわせて、業務チャットツール Lark のグループへ毎朝集計結果を通知する。

解決したい課題

提供している機能

機能場所概要
日次データ自動取得Windows PC(バッチ)毎日 11:00 (JST) に LINE API を叩き、前日分のデータを保存
ダッシュボードWeb2 アカウントの KPI とグラフを一覧表示
グラフ画面Web有効友だち数・累計追加数の推移を期間指定で比較表示
アカウント詳細Web各アカウントの数値・グラフ・日次データ表を表示
全期間レポートWebPDF 印刷向けの月末スナップショット表。ブラウザの「PDF として保存」で出力可能
Lark 通知Lark グループ毎朝、アカウント別の推移カードを配信。月曜日はログイン情報も付加
スタッフ管理Web(管理)ダッシュボード閲覧ユーザーの追加・編集・パスワードリセット
外部連携設定Web(管理)Lark 通知先 Webhook URL と署名シークレットの編集・テスト送信

監視対象アカウント

Slug名称接続元表示色
saki新さきの海外不動産L ステップ#3b82f6(青)
utage旧さきの海外不動産UTAGE#10b981(緑)

システムアーキテクチャ

全体構成

[Windows PC] ── 毎日 11:00 (JST) ──┐ ├─ ingest (Python) │ │ LINE API → Neon Postgres │ 自動実行 │ Vercel /api/revalidate を叩く │ (タスクスケジューラ) │ Lark Webhook へ通知 │ │ │ └────────────────────────────────┘ │ ▼ [Neon Postgres (us-east-1)] ▲ │ SELECT │ [Vercel: Next.js (web)] ▲ │ HTTPS + Cookie 認証 │ [ブラウザ] (閲覧スタッフ)

3 つの主要コンポーネント

区分技術スタック役割
ingest Python 3.11 / requests / psycopg / python-dotenv 毎日のデータ取得・保存・通知。Windows タスクスケジューラから起動
web Next.js 16 (App Router) / TypeScript / Drizzle ORM / Recharts / Tailwind CSS ダッシュボード・グラフ・管理画面・レポート出力
database Neon Postgres(us-east-1 リージョン) アカウントマスタ・日次スナップショット・スタッフ・アプリ設定の永続化

設計上の主要な選択

項目選択理由
データ取得日常に前日のみ当日は集計途中で値が変動するため、確定した日のみを保存
実行タイミング毎朝 11:00 (JST)朝の業務確認に間に合わせる
ストレージNeon Postgres時系列・SQL 集計に強く、Vercel との親和性が高い
Web フレームワークNext.js (App Router)サーバー側で DB を直接 SELECT してダッシュボードを高速表示
認証独自 JWT(Cookie ベース)外部サービス依存を避け、メール+パスワード方式で 7 日セッション
通知Lark Webhook (HMAC-SHA256 署名)既存業務ツール。アカウント別カード形式で一目で状況を把握
LINE API 認証短期チャネルアクセストークンL ステップ・UTAGE の長期トークンと干渉しないため毎回新規発行

コンポーネント詳細

ingest(Python・Windows ローカル実行)

ローカル PC のタスクスケジューラから毎日起動される Python バッチ。配置場所は C:\AItools\AI_cursor_data\01_APP\line-friend-tracker\ingest

ファイル構成

ingest/
├── .env                    # 機密情報(gitignore)
├── accounts.json           # アカウント定義
├── requirements.txt        # Python 依存
├── src/
│   ├── config.py           # .env + accounts.json + DB から設定を読み込む
│   ├── fetch.py            # LINE API クライアント
│   ├── store.py            # Neon Postgres への書き込み
│   ├── notify.py           # Lark Webhook 通知(インタラクティブカード)
│   ├── revalidate.py       # Vercel ISR の再生成トリガー
│   └── main.py             # 統合エントリ
├── scripts/
│   ├── run_daily.ps1       # タスクスケジューラから呼ばれるラッパー
│   ├── install_scheduler.ps1   # スケジューラ登録(初回のみ)
│   ├── uninstall_scheduler.ps1 # スケジューラ解除
│   ├── init_db.py          # DB スキーマ初期化(sql/*.sql を全適用)
│   ├── migrate_db.py       # 旧 Neon DB から新 DB へのデータ移行(一度きり)
│   ├── seed_app_settings.py # .env の Lark 設定値を app_settings へ移植
│   ├── import_csv.py       # 過去 Excel データ取り込み(移行時のみ)
│   └── test-api.ps1        # LINE API の動作確認用
├── sql/
│   ├── 001_init.sql        # accounts / daily_snapshots
│   ├── 002_auth.sql        # staff / password_reset_tokens
│   ├── 003_preferences.sql # staff.preferences カラム追加
│   ├── 004_staff_last_login.sql # staff.last_login_at カラム追加
│   └── 005_app_settings.sql # app_settings テーブル
└── data/
    └── logs/YYYY-MM-DD.log # 日次の実行ログ

主要モジュールの責務

ファイル関数処理
config.pyload().env と accounts.json を読み、DB から app_settings の Lark 設定を引いて Config オブジェクトを返す
fetch.pyissue_token()LINE API /v2/oauth/accessToken で短期アクセストークンを発行
fetch.pyfetch_insight(token, date)/v2/bot/insight/followers で対象日の集計値を取得
store.pyupsert_snapshot(...)前日比 (daily_added/daily_blocked/daily_net) を計算しつつ daily_snapshots に UPSERT
notify.pybuild_card(...)Lark のインタラクティブカード JSON を構築。月曜日のみログイン情報を付加
notify.pysend_card(...)HMAC-SHA256 で署名し Webhook へ POST
revalidate.pytrigger(...)Vercel の /api/revalidate を叩いてキャッシュを破棄
main.pymain()上記を順に呼び出し、サマリを標準出力に書き出す

web(Next.js・Vercel デプロイ)

Vercel 上で動作する Next.js アプリ。URL は https://line-friend-tracker.vercel.app/

ディレクトリ構成(src 配下)

web/src/
├── app/
│   ├── layout.tsx                  # ルートレイアウト
│   ├── page.tsx                    # / → /dashboard リダイレクト
│   ├── login/page.tsx              # ログイン画面
│   ├── api/
│   │   ├── auth/login/route.ts     # ログイン処理
│   │   ├── auth/logout/route.ts    # ログアウト処理
│   │   └── revalidate/route.ts     # ingest からのキャッシュ再生成
│   └── (app)/                      # 認証必須エリア
│       ├── layout.tsx              # サイドバー付きレイアウト
│       ├── dashboard/page.tsx
│       ├── graphs/page.tsx
│       ├── accounts/[slug]/page.tsx
│       ├── reports/[slug]/page.tsx
│       ├── settings/page.tsx       # 個人設定
│       ├── staff/page.tsx          # スタッフ一覧(admin)
│       └── integrations/page.tsx   # 外部連携設定(admin)
├── components/
│   ├── app-sidebar.tsx
│   ├── charts/
│   │   ├── trend-chart.tsx
│   │   └── daily-changes-chart.tsx
│   ├── period-control.tsx
│   └── ui/                         # shadcn ベースの汎用 UI
└── lib/
    ├── auth/
    │   ├── jwt.ts                  # JWT 発行/検証
    │   ├── password.ts             # bcrypt ハッシュ
    │   ├── session.ts              # requireSession / requireRole
    │   └── staff.ts                # スタッフ DB ヘルパー
    ├── db/
    │   ├── client.ts               # Drizzle + Neon HTTP クライアント
    │   ├── schema.ts               # 全テーブルの Drizzle スキーマ
    │   ├── queries.ts              # 再利用クエリ
    │   └── seed-admin.ts           # 初期 admin 作成スクリプト
    ├── preferences.ts              # ユーザー UI 設定の型と定数
    ├── period.ts                   # 期間指定ロジック
    └── utils.ts

主要なライブラリ

ライブラリ用途
next 16App Router・サーバーコンポーネント
@neondatabase/serverlessVercel から Neon に HTTP 接続
drizzle-orm型安全な SQL クエリ
recharts折れ線・エリアグラフ描画
joseJWT 発行・検証
bcryptjsパスワードハッシュ
tailwindcss 4スタイリング
lucide-reactアイコン
sonnerトースト通知

database(Neon Postgres)

Vercel と同じ AWS リージョン us-east-1 に配置。Pooled / Unpooled の 2 種類の接続文字列を用途別に使い分ける。

接続種別用途
Pooled (DATABASE_URL)Web 側(サーバーレス環境からの短時間クエリ)
Unpooled (DATABASE_URL_UNPOOLED)マイグレーション・長時間接続(ingest 側のスクリプト)

日次処理フロー

毎日 11:00 (JST) に Windows タスクスケジューラから scripts\run_daily.ps1 が起動し、以下を実行する。

11:00:00 Windows タスクスケジューラ起動 │ └─► run_daily.ps1 │ ├─ ログファイル data/logs/YYYY-MM-DD.log を作成 └─ .venv/Scripts/python.exe -m src.main を呼び出し │ ▼ [src.main] │ ├─ Step 1: config.load() │ ├─ .env から DATABASE_URL / LINE 認証情報 │ ├─ accounts.json から監視対象リスト │ └─ app_settings から Lark Webhook URL/シークレット │ ├─ Step 2: 各アカウントについて以下を繰り返す │ ├─ fetch.issue_token() → アクセストークン発行 │ ├─ fetch.fetch_insight() → 前日分の指標取得 │ ├─ if status != 'ready': │ │ → スキップ(失敗リストに追加) │ └─ store.upsert_snapshot() → DB に UPSERT │ (前日との差分も計算) │ ├─ Step 3: Vercel のキャッシュ再生成 │ └─ revalidate.trigger() │ POST /api/revalidate?token=... │ ├─ Step 4: Lark 通知 │ ├─ if 成功アカウントあり: │ │ for each account: │ │ notify.build_card() → notify.send_card() │ │ (月曜日のみログイン情報を付加) │ └─ else: │ notify.send_text() で失敗通知を送る │ └─ Step 5: 終了コード 0: 全アカウント成功 1: 1 件以上失敗
耐障害設計: Vercel revalidate と Lark 通知は best-effort(失敗しても全体は失敗扱いにしない)。データ取得・保存(Step 2)の成否のみが終了コードを決定する。

失敗時の挙動

データベース詳細

5 テーブル構成。スキーマ定義は Drizzle ORM(web/src/lib/db/schema.ts)と SQL マイグレーション(ingest/sql/*.sql)の両方に存在し、内容は揃えてある。

テーブル一覧と関係

accounts (1) ──┐ │ 1:N ▼ daily_snapshots (N) staff (1) ──┬─ N ── password_reset_tokens │ └─ N ── app_settings.updated_by (SET NULL)

accounts — LINE 公式アカウントマスタ

カラム制約説明
idSERIALPK連番
slugVARCHAR(50)UNIQUE NOT NULLURL/識別子(saki / utage
nameVARCHAR(100)NOT NULL表示名
descriptionVARCHAR(200)備考(接続元など)
line_channel_idVARCHAR(20)UNIQUE NOT NULLLINE Messaging API のチャネル ID
display_colorVARCHAR(7)UI で使う色(#3b82f6 等)
display_orderINTEGERDEFAULT 0サイドバー表示順
is_activeBOOLEANDEFAULT TRUEFALSE にすると取得対象から除外
created_atTIMESTAMPTZDEFAULT NOW()
updated_atTIMESTAMPTZDEFAULT NOW()
チャネルシークレットは DB に保存しない。シークレットは ingest/.env 側で環境変数として管理する設計。

daily_snapshots — 日次スナップショット

カラム制約説明
account_idINTEGERPK / FK→accounts対象アカウント(CASCADE 削除)
dateDATEPK集計対象日(前日)
followersINTEGERNOT NULL累計友だち追加数(LINE API の followers
active_friendsINTEGERNOT NULL有効友だち数(targetedReaches
blocksINTEGERNOT NULL累計ブロック数(blocks
daily_addedINTEGERNULL 可当日新規追加数(= followers の前日差)
daily_blockedINTEGERNULL 可当日ブロック数
daily_netINTEGERNULL 可前日比(有効友だち数の差分)
fetched_atTIMESTAMPTZDEFAULT NOW()レコード取得時刻

インデックス: (date DESC)(account_id, date DESC) の 2 本。

staff — ダッシュボード閲覧ユーザー

カラム説明
idUUID PK自動生成
emailVARCHAR(255) UNIQUEログイン ID
password_hashTEXTbcrypt ハッシュ
display_nameVARCHAR(100)表示名(任意)
roleVARCHAR(20)admin または viewer
is_activeBOOLEANFALSE でログイン不可
preferencesJSONB個人 UI 設定(グラフスタイル等)
last_login_atTIMESTAMPTZ最終ログイン日時
created_byUUID作成者 staff.id
created_at / updated_atTIMESTAMPTZ

password_reset_tokens — パスワードリセット用ワンタイムトークン

UUID 主キー、staff_id 外部キー、token_hash(送信した平文トークンの SHA ハッシュ)、expires_atused_at(NULL のものが有効)。短期間で使い捨てる前提。

app_settings — 管理画面で編集可能な key/value 設定

カラム説明
keyVARCHAR(64) PK設定キー
valueTEXT値(空文字 = 未設定)
updated_atTIMESTAMPTZ最終更新時刻
updated_byUUID → staff.id最終更新者(SET NULL)

現在登録されているキー

key用途
lark_webhook_urlLark 通知先 Webhook URL
lark_signing_secretLark Webhook の署名検証用シークレット
これら 2 値は Web の /integrations 画面から admin が変更可能。ingest 側も DB から読むため、変更後は次回 11:00 の通知から新しい設定が反映される。

主要画面の説明

ダッシュボード /dashboard

ログイン直後の画面。2 アカウントの最新指標(有効友だち・前日比・7 日純増など)と直近のミニグラフを並べて表示。

グラフ /graphs

2 アカウントを 1 つのグラフに重ねて表示。期間セレクタ(直近 30 日 / 90 日 / 全期間 / カスタム)で範囲を切り替え可能。

アカウント詳細 /accounts/[slug]

アカウント別の KPI カード、累計推移グラフ、日次差分グラフ、データ表。期間セレクタで範囲調整可能。右上に「レポート出力」ボタンあり。

全期間レポート /reports/[slug]

PDF 出力を前提とした印刷向けレイアウト。

個人設定 /settings

ログインユーザーごとに保存される UI 設定。現在は「グラフ表示スタイル」(linear / monotone / step / area)の選択肢を提供。

スタッフ一覧 /staff(admin のみ)

ダッシュボード閲覧ユーザーの管理。追加・編集・無効化・パスワードリセットが可能。新規追加時とリセット時に初回パスワードが 1 度だけ表示される。

外部連携 /integrations(admin のみ)

Lark 通知の宛先設定。Webhook URL と署名シークレットを編集できる。シークレットはマスク表示で、変更時のみ新しい値を入力する。「テスト送信」ボタンで現在保存中の設定で動作確認可能。

認証と権限

認証方式

独自実装の Cookie ベース認証。外部 IdP(Clerk 等)には依存していない。

権限ロール

ロール閲覧管理メニュー
admin(管理者) 全画面 スタッフ一覧・外部連携の両方を表示/操作可能
viewer(閲覧者) ダッシュボード・グラフ・アカウント詳細・レポート・個人設定 表示されない(サイドバーに管理セクション自体が現れず、URL 直接アクセスも /dashboard へリダイレクト)

初期管理者の追加

初回セットアップ時のみ、ローカルから npm run seed:admin を実行することでメール+パスワードを指定して admin アカウントを作成可能。

外部サービス連携

LINE Messaging API 外部

エンドポイント用途
POST /v2/oauth/accessToken短期チャネルアクセストークン発行(client_credentials)
GET /v2/bot/insight/followers?date=YYYYMMDD対象日の友だち統計取得

短期トークンを毎回新規発行する方式なので、L ステップ・UTAGE が発行している長期トークンと干渉せず共存できる。

Lark Webhook 外部

カスタム Bot の Webhook に対して、HMAC-SHA256 で署名した JSON を POST する。

string_to_sign = "{timestamp}\n{signing_secret}"
signature      = base64( HMAC-SHA256( key=string_to_sign, msg=b"" ) )

POST body:
{
  "timestamp": "1700000000",
  "sign": "...",
  "msg_type": "interactive",
  "card": { ... }
}

Vercel Revalidate API 内部

ingest が DB 書き込み完了後に Vercel 側の ISR キャッシュを更新するために叩く内部 API。REVALIDATE_TOKEN でアクセス制御。

環境変数・設定

ingest 側(ingest/.env

キー用途
LINE_SAKI_CHANNEL_ID / LINE_SAKI_CHANNEL_SECRETsaki アカウントの LINE API 認証情報
LINE_UTAGE_CHANNEL_ID / LINE_UTAGE_CHANNEL_SECRETutage アカウントの LINE API 認証情報
DATABASE_URLNeon Postgres プール接続文字列
DATABASE_URL_UNPOOLEDNeon 直接接続文字列(マイグレーション用)
REVALIDATE_TOKENVercel /api/revalidate の認証トークン
DASHBOARD_URLLark カードに載せるダッシュボード URL
LARK_LOGIN_EMAIL / LARK_LOGIN_PASSWORD月曜カードに付加するログイン情報
Lark Webhook URL / 署名シークレット.env ではなく Neon の app_settings テーブルに保管。Web の /integrations 画面が単一の真実。

ingest 側(ingest/accounts.json

監視対象アカウントの定義。スキーマ:

{
  "accounts": [
    {
      "slug": "saki",
      "name": "新さきの海外不動産",
      "description": "Lステップ接続",
      "channel_id_env": "LINE_SAKI_CHANNEL_ID",
      "channel_secret_env": "LINE_SAKI_CHANNEL_SECRET",
      "display_color": "#3b82f6",
      "display_order": 1
    },
    { ... utage 側 ... }
  ]
}

web 側(Vercel 環境変数)

キー用途
DATABASE_URLNeon プール接続文字列
DATABASE_URL_UNPOOLEDDrizzle Kit でのスキーマ更新用
JWT_SECRETJWT 署名鍵(最低 32 文字)
REVALIDATE_TOKENingest からの再生成リクエスト検証

開発・運用情報

ローカル開発環境(ingest)

cd C:\AItools\AI_cursor_data\01_APP\line-friend-tracker\ingest

# 仮想環境
python -m venv .venv
.\.venv\Scripts\Activate.ps1

# 依存インストール
pip install -r requirements.txt

# 環境変数
Copy-Item .env.example .env
notepad .env   # 各値を埋める

# 動作確認(API のみ・DB/Lark には触れない)
powershell -File scripts\test-api.ps1

# 全工程の手動実行
python -m src.main
python -m src.main --no-notify        # Lark 通知をスキップ
python -m src.main --no-revalidate    # Vercel 再生成をスキップ
python -m src.main --date 2026-04-29  # 過去日のバックフィル

ローカル開発環境(web)

cd C:\AItools\AI_cursor_data\01_APP\line-friend-tracker\web

npm install

# ローカル DB に接続して開発サーバー起動
npm run dev                # http://localhost:3000

# 本番ビルドの確認
npm run build && npm run start

# DB マイグレーション関連
npm run db:generate        # Drizzle スキーマから SQL 生成
npm run db:push            # スキーマを直接 DB に反映
npm run db:studio          # Drizzle Studio で DB を覗く

# 初期 admin 追加
npm run seed:admin

デプロイ

対象方法
webmain ブランチへの push で Vercel が自動デプロイ
ingestWindows PC 上の git pull で更新(デプロイ自動化なし)
DB スキーマingest\scripts\init_db.py または web/npm run db:push

定期実行(Windows タスクスケジューラ)

登録

powershell -ExecutionPolicy Bypass -File ingest\scripts\install_scheduler.ps1

状態確認

Get-ScheduledTask -TaskName "LineFriendTracker" | Get-ScheduledTaskInfo
Get-ScheduledTask -TaskName "LineFriendTracker" | Start-ScheduledTask  # 手動実行

解除

powershell -ExecutionPolicy Bypass -File ingest\scripts\uninstall_scheduler.ps1

ログ

毎日の実行ログは ingest\data\logs\YYYY-MM-DD.log に追記される。標準出力・エラー出力の両方を含む。

運用上の注意

主要な機能追加履歴

git の main ブランチに記録された主要な変更(新しいものが上)。

時期commit内容
2026-058d689f7レポートグラフの軸ラベル・当月除外フィルタを追加
2026-052876d10レポート印刷時に固定サイズのチャート版を併設し PDF を A4 横幅にフィット
2026-054c0b6b8アカウント別の月次レポート印刷ページを新設
2026-05bca047d外部連携設定 /integrations を新設し Lark 設定を DB 管理に移行
2026-05cd09585Lark 通知をアカウント別インタラクティブカードに変更
2026-05049e18aアカウント表にブロック率を併記
2026-05afcf0faスタッフ一覧に最終ログイン日時を表示
2026-0568e1ee6モバイル対応のハンバーガーメニューレイアウト
2026-05d906a26個人 UI 設定とグラフスタイル選択画面
2026-053c63d45ダッシュボードのクエリ統合とローディング UI
2026-057a4d3e9Neon DB を Singapore → us-east-1 にリージョン移行

完全な履歴は git log で確認可能。

関連URL・参照先

項目場所
本番 Webhttps://line-friend-tracker.vercel.app/
GitHub リポジトリgithub.com/sugiura-7338/line-friend-tracker
Vercel プロジェクトVercel ダッシュボードで line-friend-tracker を検索
Neon PostgresNeon コンソール(リージョン: us-east-1、ホスト: ep-delicate-king-ammdrk40.c-5.us-east-1.aws.neon.tech
LINE Developersdevelopers.line.biz/console
Lark カスタム Bot 設定各 Lark グループの設定 → ボット → カスタム Bot
ingest 配置先C:\AItools\AI_cursor_data\01_APP\line-friend-tracker\ingest(Windows ローカル)

関連内部ドキュメント


本資料の対象コミット: 8d689f7(2026-05-16 時点)。資料更新時はファイル末尾のこの行と冒頭のメタ情報の更新日を併せて変更してください。