user-web
Jester の管理画面(レース/ブログ管理ダッシュボード)です。 レースと記事の管理、過去ブログの取り込みと AI リライト、ブログ・カテゴリ・NGワードの管理、 および記事・ブログのプレビューをブラウザから操作できます。
技術スタック
| 項目 | 内容 |
|---|---|
| ビルド | Vite(bun run dev / bun run build。出力先は out/) |
| フレームワーク | React 19 + react-router-dom v7(クライアントのみの SPA。サーバーランタイムは持たない) |
| スタイル | Tailwind CSS v4(@tailwindcss/vite) |
| UI 部品 | Radix UI |
| API 呼び出し | ブラウザの fetch(src/lib/api.ts) |
| デザイン | oddspark-static-pages から移植したコンポーネント・SCSS(src/oddspark/) |
Next.js ではない
もとは Next.js アプリでしたが、Vite + React Router の SPA に移行しました。 サーバーコンポーネント・サーバーアクションはありません。
環境変数
| 変数名 | 説明 |
|---|---|
VITE_API_BASE_URL |
odds_poc_app(App Lambda)のベース URL |
VITE_MEDIA_BASE_URL |
メディア配信用の CloudFront ドメイン |
ディレクトリ構成
jester-user-web/
├── index.html
├── vite.config.ts
└── src/
├── main.tsx # エントリ(BrowserRouter)
├── App.tsx # ルート定義
├── pages/ # 1ルート = 1コンポーネント
├── components/ # 共有 UI・管理画面レイアウト
├── lib/
│ ├── api.ts # API 呼び出し(取得・更新)
│ ├── types.ts # API レスポンスの型
│ ├── race-config.ts # 競技種別とパスセグメントの対応
│ └── race-result/ # レース結果の整形
└── oddspark/ # oddspark から移植したデザイン資産
画面構成
管理画面(サイドバー付きレイアウト)
| 画面 | パス | 概要 |
|---|---|---|
| 競馬レース一覧 | /horse-racings |
レースの一覧・記事生成の起点 |
| オートレース一覧 | /auto-racings |
同上 |
| 競輪レース一覧 | /bicycle-racings |
同上 |
| レース詳細 | /:raceType/detail?id=... |
レース結果の確認・記事の生成・紐づく記事の一覧 |
| レース記事詳細 | /:raceType/detail/article?id=... |
記事本文の確認・編集 |
| 生成失敗一覧 | /articles/failed |
status=failed の記事一覧・追加指示つき再生成 |
| ブログ一覧 | /blogs |
生成済みブログの一覧・絞り込み |
| ブログ詳細 | /blogs/detail?id=... |
本文・タイトル・カテゴリの編集、サムネイル生成、関連情報生成、削除 |
| 過去ブログ一覧 | /old-blogs |
過去ブログの一覧・取り込み |
| 過去ブログ詳細 | /old-blogs/detail?id=... |
カテゴリの更新・AI リライトの起点 |
| カテゴリ一覧 | /categories |
カテゴリの作成・更新・削除 |
| NGワード一覧 | /ng-words |
NGワードの作成・更新・削除 |
ルート(/)へアクセスすると、先頭の競技種別(競馬レース一覧)へリダイレクトします。
プレビュー(管理画面 UI なし・別タブで開く)
| 画面 | パス | 概要 |
|---|---|---|
| レース結果ページ | /articles/preview?raceId=... |
記事サマリーを埋め込んだレース結果ページの実表示 |
| ブログ記事ページ | /blogs/preview?id=... |
ブログの実表示 |
| リライト前ブログ | /blogs/preview/original?id=... |
同じ記事体裁のまま、本文だけリライト前に差し替えた比較用表示 |
プレビューは oddspark から移植したグローバル CSS(リセット含む)を取り込むため、 管理画面側のスタイルと分離するよう遅延読み込みしています。
詳細ページの ID はクエリ文字列
詳細ページは ID をパスパラメータではなくクエリ文字列(?id=...)で受け取ります。
競技種別3つ(horse-racings / auto-racings / bicycle-racings)は /:raceType ルートを共有します。
競技種別の扱い
URL のパスセグメントとバックエンドの競技種別は src/lib/race-config.ts で対応づけます。
| kind | パス | racingType(API) |
表示名 | 出走者番号 |
|---|---|---|---|---|
horse |
/horse-racings |
horse_racing |
競馬レース | horse_number |
auto |
/auto-racings |
auto_racing |
オートレース | car_number |
bicycle |
/bicycle-racings |
bicycle_racing |
競輪レース | car_number |
レース一覧は GET /v1/races?type={racingType} を呼びます。
race_id は競技をまたいで一意のため、詳細取得(GET /v1/races/{race_id})では種別を指定しません。
使用 API
レース・記事
| 操作 | API |
|---|---|
| レース一覧 | GET /v1/races?type=... |
| レース詳細 | GET /v1/races/{race_id} |
| レースに紐づく記事 | GET /v1/articles?race_id=... / GET /v1/races/{race_id}/articles/{article_id} |
| 生成失敗記事の一覧 | GET /v1/articles?status=failed |
| 記事の AI 生成 | POST /v1/articles/generation |
| 生成失敗記事の再生成 | POST /v1/articles/{article_id}/regeneration |
| 記事本文の更新 | PUT /v1/articles/{article_id} |
ブログ・過去ブログ
| 操作 | API |
|---|---|
| ブログ一覧 | GET /v1/blogs(race_id / race_type / category_id / origin / status / name / id で絞り込み) |
| ブログ詳細 | GET /v1/blogs/{blog_id} |
| ブログの更新(本文・タイトル・カテゴリ) | PUT /v1/blogs/{blog_id} |
| ブログの削除 | DELETE /v1/blogs/{blog_id} |
| サムネイル生成 | POST /v1/blogs/{blog_id}/thumbnail_generate |
| 関連情報の生成 | POST /v1/blogs/{blog_id}/related_generate |
| 過去ブログ一覧・詳細 | GET /v1/old_blogs / GET /v1/old_blogs/{old_blog_id} |
| 過去ブログの取り込み | POST /v1/old_blogs |
| 過去ブログのカテゴリ更新 | PUT /v1/old_blogs/{old_blog_id} |
| AI リライトの開始 | POST /v1/old_blogs/{old_blog_id}/rewrite |
マスタ
| 操作 | API |
|---|---|
| カテゴリ | GET / POST / PUT / DELETE /v1/categories |
| NGワード | GET / POST / PUT / DELETE /v1/ng_words |
過去ブログの取り込み
過去ブログの取り込みは、scripts/scraping_blog.py(jester-backend)が出力した
スクレイピング済み JSON ファイルを複数選択でアップロードする方式です。
画面側でファイルを1件ずつ読み込み、次のようにマッピングして POST /v1/old_blogs を呼びます。
| JSON のキー | リクエストのフィールド |
|---|---|
title |
name |
body_html |
body_html |
race_type |
race_type |
blog_type |
blog_type |
category |
category |
url |
source_url |
published_at |
published_at(「2026年5月10日」形式・ISO 8601 の両方を UNIX ミリ秒へ変換) |
title / body_html が無いファイルはスキップし、処理後に成功件数と失敗理由をまとめて表示します。
非同期処理の扱い
記事生成・ブログリライト・サムネイル生成は 202 Accepted ですぐ返り、
バックグラウンドで処理が進みます。画面は状態をポーリングせず、再読み込みで状態を確認します。
| 対象 | 完了の見え方 |
|---|---|
| 記事生成 | status が generating → draft(失敗時は failed) |
| ブログリライト・書き下ろし | status が generating → draft(失敗時はレコードごと消える) |
| サムネイル生成 | ブログ本文にサムネイル画像が挿入される |
関連情報の生成(related_generate)だけは同期処理で、その場で更新後のブログが返ります。
エラー表示
FastAPI はエラー理由を detail に入れて返します。
src/lib/api.ts の ApiError がこれを message へ引き上げるため、
「このレースは記事を生成できません: …」のようなバックエンドの理由をそのまま画面に表示できます。
デプロイ
bun run build で out/ に静的ファイルを出力し、GitHub Actions(.github/workflows/dev-deploy.yml)が
main への push で S3 + CloudFront へ同期します。
SPA のためのフォールバック設定が必要
クライアントルーティングの SPA のため、CloudFront 側で未知パスを index.html(200)に
フォールバックさせる必要があります(403/404 のカスタムエラーレスポンスで /index.html を返す)。
設定が無いとディープリンクが 404 になります。