LINE Friends Tracker — システム解説書
LINE 公式アカウントの友だち数を毎日自動取得し、Web ダッシュボードと Lark 通知で可視化するシステムの構成・処理内容・データベース・運用情報を網羅したスタッフ向け資料。
プロジェクト概要
目的
LINE 公式アカウント(L ステップ・UTAGE 接続)の友だち数(累計追加・有効友だち・累計ブロック)の推移を毎日自動で記録し、ブラウザでいつでも確認できる状態にする。あわせて、業務チャットツール Lark のグループへ毎朝集計結果を通知する。
解決したい課題
- 従来は Excel に手作業で記録しており、転記ミスや作業負荷が大きかった
- L ステップ・UTAGE のダッシュボードを個別に開かないと現状が把握できなかった
- 過去推移を時系列で振り返れる形式の資料が手元にあると望ましい
提供している機能
| 機能 | 場所 | 概要 |
|---|---|---|
| 日次データ自動取得 | Windows PC(バッチ) | 毎日 11:00 (JST) に LINE API を叩き、前日分のデータを保存 |
| ダッシュボード | Web | 2 アカウントの KPI とグラフを一覧表示 |
| グラフ画面 | Web | 有効友だち数・累計追加数の推移を期間指定で比較表示 |
| アカウント詳細 | Web | 各アカウントの数値・グラフ・日次データ表を表示 |
| 全期間レポート | Web | PDF 印刷向けの月末スナップショット表。ブラウザの「PDF として保存」で出力可能 |
| Lark 通知 | Lark グループ | 毎朝、アカウント別の推移カードを配信。月曜日はログイン情報も付加 |
| スタッフ管理 | Web(管理) | ダッシュボード閲覧ユーザーの追加・編集・パスワードリセット |
| 外部連携設定 | Web(管理) | Lark 通知先 Webhook URL と署名シークレットの編集・テスト送信 |
監視対象アカウント
| Slug | 名称 | 接続元 | 表示色 |
|---|---|---|---|
saki | 新さきの海外不動産 | L ステップ | #3b82f6(青) |
utage | 旧さきの海外不動産 | UTAGE | #10b981(緑) |
システムアーキテクチャ
全体構成
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.py | load() | .env と accounts.json を読み、DB から app_settings の Lark 設定を引いて Config オブジェクトを返す |
fetch.py | issue_token() | LINE API /v2/oauth/accessToken で短期アクセストークンを発行 |
fetch.py | fetch_insight(token, date) | /v2/bot/insight/followers で対象日の集計値を取得 |
store.py | upsert_snapshot(...) | 前日比 (daily_added/daily_blocked/daily_net) を計算しつつ daily_snapshots に UPSERT |
notify.py | build_card(...) | Lark のインタラクティブカード JSON を構築。月曜日のみログイン情報を付加 |
notify.py | send_card(...) | HMAC-SHA256 で署名し Webhook へ POST |
revalidate.py | trigger(...) | Vercel の /api/revalidate を叩いてキャッシュを破棄 |
main.py | main() | 上記を順に呼び出し、サマリを標準出力に書き出す |
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 16 | App Router・サーバーコンポーネント |
@neondatabase/serverless | Vercel から Neon に HTTP 接続 |
drizzle-orm | 型安全な SQL クエリ |
recharts | 折れ線・エリアグラフ描画 |
jose | JWT 発行・検証 |
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 が起動し、以下を実行する。
失敗時の挙動
- LINE API が
status: "unready"を返した場合(前日データがまだ確定していない)、そのアカウントは SKIP され、Lark には通知されない - 全アカウントが失敗した場合のみ、Lark にプレーンテキストで失敗通知を送る
- ログは
ingest/data/logs/YYYY-MM-DD.logに追記される
データベース詳細
5 テーブル構成。スキーマ定義は Drizzle ORM(web/src/lib/db/schema.ts)と SQL マイグレーション(ingest/sql/*.sql)の両方に存在し、内容は揃えてある。
テーブル一覧と関係
accounts — LINE 公式アカウントマスタ
| カラム | 型 | 制約 | 説明 |
|---|---|---|---|
id | SERIAL | PK | 連番 |
slug | VARCHAR(50) | UNIQUE NOT NULL | URL/識別子(saki / utage) |
name | VARCHAR(100) | NOT NULL | 表示名 |
description | VARCHAR(200) | 備考(接続元など) | |
line_channel_id | VARCHAR(20) | UNIQUE NOT NULL | LINE Messaging API のチャネル ID |
display_color | VARCHAR(7) | UI で使う色(#3b82f6 等) | |
display_order | INTEGER | DEFAULT 0 | サイドバー表示順 |
is_active | BOOLEAN | DEFAULT TRUE | FALSE にすると取得対象から除外 |
created_at | TIMESTAMPTZ | DEFAULT NOW() | |
updated_at | TIMESTAMPTZ | DEFAULT NOW() |
ingest/.env 側で環境変数として管理する設計。daily_snapshots — 日次スナップショット
| カラム | 型 | 制約 | 説明 |
|---|---|---|---|
account_id | INTEGER | PK / FK→accounts | 対象アカウント(CASCADE 削除) |
date | DATE | PK | 集計対象日(前日) |
followers | INTEGER | NOT NULL | 累計友だち追加数(LINE API の followers) |
active_friends | INTEGER | NOT NULL | 有効友だち数(targetedReaches) |
blocks | INTEGER | NOT NULL | 累計ブロック数(blocks) |
daily_added | INTEGER | NULL 可 | 当日新規追加数(= followers の前日差) |
daily_blocked | INTEGER | NULL 可 | 当日ブロック数 |
daily_net | INTEGER | NULL 可 | 前日比(有効友だち数の差分) |
fetched_at | TIMESTAMPTZ | DEFAULT NOW() | レコード取得時刻 |
インデックス: (date DESC) と (account_id, date DESC) の 2 本。
staff — ダッシュボード閲覧ユーザー
| カラム | 型 | 説明 |
|---|---|---|
id | UUID PK | 自動生成 |
email | VARCHAR(255) UNIQUE | ログイン ID |
password_hash | TEXT | bcrypt ハッシュ |
display_name | VARCHAR(100) | 表示名(任意) |
role | VARCHAR(20) | admin または viewer |
is_active | BOOLEAN | FALSE でログイン不可 |
preferences | JSONB | 個人 UI 設定(グラフスタイル等) |
last_login_at | TIMESTAMPTZ | 最終ログイン日時 |
created_by | UUID | 作成者 staff.id |
created_at / updated_at | TIMESTAMPTZ |
password_reset_tokens — パスワードリセット用ワンタイムトークン
UUID 主キー、staff_id 外部キー、token_hash(送信した平文トークンの SHA ハッシュ)、expires_at、used_at(NULL のものが有効)。短期間で使い捨てる前提。
app_settings — 管理画面で編集可能な key/value 設定
| カラム | 型 | 説明 |
|---|---|---|
key | VARCHAR(64) PK | 設定キー |
value | TEXT | 値(空文字 = 未設定) |
updated_at | TIMESTAMPTZ | 最終更新時刻 |
updated_by | UUID → staff.id | 最終更新者(SET NULL) |
現在登録されているキー
| key | 用途 |
|---|---|
lark_webhook_url | Lark 通知先 Webhook URL |
lark_signing_secret | Lark Webhook の署名検証用シークレット |
主要画面の説明
ダッシュボード /dashboard
ログイン直後の画面。2 アカウントの最新指標(有効友だち・前日比・7 日純増など)と直近のミニグラフを並べて表示。
グラフ /graphs
2 アカウントを 1 つのグラフに重ねて表示。期間セレクタ(直近 30 日 / 90 日 / 全期間 / カスタム)で範囲を切り替え可能。
アカウント詳細 /accounts/[slug]
アカウント別の KPI カード、累計推移グラフ、日次差分グラフ、データ表。期間セレクタで範囲調整可能。右上に「レポート出力」ボタンあり。
全期間レポート /reports/[slug]
PDF 出力を前提とした印刷向けレイアウト。
- 全期間(実行月を除く完了月のみ)の累計追加・有効友だち・累計ブロック 3 系列エリアグラフ
- 月末日スナップショット表(行数 18 超で 2 列横並びレイアウト)
- 右上の「印刷 / PDF保存」ボタンでブラウザの印刷ダイアログを開く
- 印刷時はサイドバー等が非表示になり、A4 縦に最適化された専用チャートで描画される
個人設定 /settings
ログインユーザーごとに保存される UI 設定。現在は「グラフ表示スタイル」(linear / monotone / step / area)の選択肢を提供。
スタッフ一覧 /staff(admin のみ)
ダッシュボード閲覧ユーザーの管理。追加・編集・無効化・パスワードリセットが可能。新規追加時とリセット時に初回パスワードが 1 度だけ表示される。
外部連携 /integrations(admin のみ)
Lark 通知の宛先設定。Webhook URL と署名シークレットを編集できる。シークレットはマスク表示で、変更時のみ新しい値を入力する。「テスト送信」ボタンで現在保存中の設定で動作確認可能。
認証と権限
認証方式
独自実装の Cookie ベース認証。外部 IdP(Clerk 等)には依存していない。
- ログイン: メールアドレス + パスワード
- パスワードハッシュ:
bcryptjs - セッション:
joseで署名した JWT を Cookie に格納(有効期間 7 日) - サインアップ機能なし。新規ユーザーは admin が
/staff画面から作成する
権限ロール
| ロール | 閲覧 | 管理メニュー |
|---|---|---|
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_SECRET | saki アカウントの LINE API 認証情報 |
LINE_UTAGE_CHANNEL_ID / LINE_UTAGE_CHANNEL_SECRET | utage アカウントの LINE API 認証情報 |
DATABASE_URL | Neon Postgres プール接続文字列 |
DATABASE_URL_UNPOOLED | Neon 直接接続文字列(マイグレーション用) |
REVALIDATE_TOKEN | Vercel /api/revalidate の認証トークン |
DASHBOARD_URL | Lark カードに載せるダッシュボード URL |
LARK_LOGIN_EMAIL / LARK_LOGIN_PASSWORD | 月曜カードに付加するログイン情報 |
.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_URL | Neon プール接続文字列 |
DATABASE_URL_UNPOOLED | Drizzle Kit でのスキーマ更新用 |
JWT_SECRET | JWT 署名鍵(最低 32 文字) |
REVALIDATE_TOKEN | ingest からの再生成リクエスト検証 |
開発・運用情報
ローカル開発環境(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
デプロイ
| 対象 | 方法 |
|---|---|
| web | main ブランチへの push で Vercel が自動デプロイ |
| ingest | Windows PC 上の git pull で更新(デプロイ自動化なし) |
| DB スキーマ | ingest\scripts\init_db.py または web/npm run db:push |
定期実行(Windows タスクスケジューラ)
登録
powershell -ExecutionPolicy Bypass -File ingest\scripts\install_scheduler.ps1
- タスク名:
LineFriendTracker - 初回実行: 翌日 11:00(インストール当日の意図せぬ実行を避ける)
- 失敗時は 5 分間隔で 2 回まで再試行
- バッテリー駆動でも実行可、最大実行時間 30 分
状態確認
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 に追記される。標準出力・エラー出力の両方を含む。
運用上の注意
- Windows PC がスリープ・シャットダウン中は実行されない。当日の朝 PC が起動しているか確認しておく
- LINE API は前日データの確定タイミングが不定。
status: "unready"が返ることがあるが、翌日の実行で自動的にリカバリされる(古い日付の UPSERT は許容) - Lark の通知先グループを変える場合、ソースコードや
.envではなく /integrations 画面から変更する - 新しい LINE 公式アカウントを監視対象に追加する場合、
accounts.jsonへの追記とaccountsテーブルへの INSERT、対応する.envへのチャネル情報追加が必要
主要な機能追加履歴
git の main ブランチに記録された主要な変更(新しいものが上)。
| 時期 | commit | 内容 |
|---|---|---|
| 2026-05 | 8d689f7 | レポートグラフの軸ラベル・当月除外フィルタを追加 |
| 2026-05 | 2876d10 | レポート印刷時に固定サイズのチャート版を併設し PDF を A4 横幅にフィット |
| 2026-05 | 4c0b6b8 | アカウント別の月次レポート印刷ページを新設 |
| 2026-05 | bca047d | 外部連携設定 /integrations を新設し Lark 設定を DB 管理に移行 |
| 2026-05 | cd09585 | Lark 通知をアカウント別インタラクティブカードに変更 |
| 2026-05 | 049e18a | アカウント表にブロック率を併記 |
| 2026-05 | afcf0fa | スタッフ一覧に最終ログイン日時を表示 |
| 2026-05 | 68e1ee6 | モバイル対応のハンバーガーメニューレイアウト |
| 2026-05 | d906a26 | 個人 UI 設定とグラフスタイル選択画面 |
| 2026-05 | 3c63d45 | ダッシュボードのクエリ統合とローディング UI |
| 2026-05 | 7a4d3e9 | Neon DB を Singapore → us-east-1 にリージョン移行 |
完全な履歴は git log で確認可能。
関連URL・参照先
| 項目 | 場所 |
|---|---|
| 本番 Web | https://line-friend-tracker.vercel.app/ |
| GitHub リポジトリ | github.com/sugiura-7338/line-friend-tracker |
| Vercel プロジェクト | Vercel ダッシュボードで line-friend-tracker を検索 |
| Neon Postgres | Neon コンソール(リージョン: us-east-1、ホスト: ep-delicate-king-ammdrk40.c-5.us-east-1.aws.neon.tech) |
| LINE Developers | developers.line.biz/console |
| Lark カスタム Bot 設定 | 各 Lark グループの設定 → ボット → カスタム Bot |
| ingest 配置先 | C:\AItools\AI_cursor_data\01_APP\line-friend-tracker\ingest(Windows ローカル) |
関連内部ドキュメント
README.md— プロジェクトの一行サマリingest/README.md— ingest 側のセットアップ手順docs/architecture.md— アーキテクチャ早見表(このドキュメントの簡易版)web/AGENTS.md— Next.js 16 を扱う際の注意事項メモ
本資料の対象コミット: 8d689f7(2026-05-16 時点)。資料更新時はファイル末尾のこの行と冒頭のメタ情報の更新日を併せて変更してください。