MCPcopy Index your code
hub / github.com/Sallyn0225/seatview

github.com/Sallyn0225/seatview @main

Chat with this repo
repository ↗ · DeepWiki ↗ · + Follow
511 symbols 1,204 edges 106 files 34 documented · 7%
What it actually does AI analysis from the code graph — generated when you open this
loading…
README

SeatView

Website License: Apache 2.0 CI

简体中文 | English | 日本語 | 한국어

抽選・チケット争奪戦・開演前の座席確認——その席から実際に何が見えるのかを確かめよう。 リアル座席ビュー · 真实视角图集 —— 内部コードネーム seatmap-real

SeatView は、日本(および一部海外)のライブ会場のリアルな座席ビュー写真を集約します。ユーザーは会場公式の座席表上に自分の席をマークし、その位置から撮った実写を投稿。他のユーザーは座席表のマーカーをクリックすると、その席のリアルな視界を Lightbox でプレビューでき、1 枚の視点写真への直接リンクも共有できます。抽選やチケット争奪、開演前の座席確認のときに、より賢い判断ができます。

閲覧、投稿、匿名評価はすべて SeatView への登録不要です。投稿は IP レート制限 + Cloudflare Turnstile で不正利用を防ぎ、評価は会場 + ソルト付き IP hash で重複排除とレート制限を行います。コメントは giscus で GitHub Discussions に接続します。スタック全体が Cloudflare のみで動作します:Workers(SSR + 静的アセット)+ D1 + KV + R2。

公開サイト 👉 seat.genchi.top

プレビュー · 機能 · 技術スタック · クイックスタート · デプロイ · 仕組み · プロジェクト構成 · コントリビュート

プレビュー

以下のスクリーンショットは公開サイト seat.genchi.top(簡体字中国語 · ライトテーマ)から取得したものです。

SeatView トップページ

コア体験はたった二段階——座席表で席をタップ → その席のリアルな視界を見る

① 座席表のマーカー ② リアルビュー Lightbox
会場の座席表とマーカー 座席のリアルビュー Lightbox
会場公式の座席表(複数レイヤー / 複数エリアの切り替え対応)上で、他ユーザーがマークした座席ポイントを表示。隣接するポイントは自動で集約し件数を表示します。 マーカーやサムネイルをクリックすると、その席の実写 + 座席番号 / テキスト説明を Lightbox で表示します。
③ 会場の全投稿(ウォーターフォール) 見たい会場(クラウドソーシング一時置き場) ダークテーマ
会場投稿のウォーターフォール 見たい会場の一時置き場 ダークテーマの座席表
座席表の下に、その会場のリアルビュー投稿の全件を masonry ウォーターフォールで表示します。 見たい会場がない?名前を書けば、他のユーザーが +1 で賛同できます。 ライト / ダーク / システム追従の三段階テーマ切り替え。

機能

  • 都道府県別ブラウズ —— 左側の会場ツリーは日本の行政区画でグループ化・折りたたみ可能。Fuse.js のクライアントサイドあいまい検索で、中国語 / 日本語 / ローマ字のエイリアスもヒットします。
  • 座席表マーキング —— 会場公式の座席表(複数レイヤー / 複数エリアの tag 切り替え対応)上で、他ユーザーがマークした座席ポイントを表示。隣接するポイントは自動で集約し件数を表示します。
  • リアルビュー Lightbox —— マーカーをクリックすると、その席の実写 + 座席番号 / テキスト説明を表示。下部の「近くの座席」ストリップで同一クラスタの隣席を横スクロールでき、「座席表で位置を確認」で座席表上の対応するマーカーへ戻ります。Lightbox は現在の写真を ?photo= として URL に反映し、共有ボタンで会場 / エリア文脈つきの deep link をコピーできます。下部のウォーターフォールでその会場の全投稿を表示します。
  • 会場の投稿数表示 —— 会場タイトル下に現在のエリアと会場全体の写真数を表示し、座席図エリアの切り替えや新規投稿後に即時同期します。
  • 会場コメントと評価 —— 会場タイトル付近の控えめな入口に総合点 / 評価数を表示し、右側のドロワーを開きます。上部は匿名4項目の 1〜5 星評価(見やすさ、音響、周辺の便利さ、アクセス。再評価はスコア変更)、下部は venue:<id> に厳密マッピングされた giscus コメントで、言語パスや座席表タブをまたいで同じ議論を共有します。
  • 登録不要の投稿 —— マーク(全画面ズームで精密に配置するモードあり)→ 画像選択 → クライアント側で WebP に圧縮(EXIF 除去)→ HMAC ticket の二段階コミット。未完了のステップはインライン案内で誘導し、全工程で IP レート制限 + Turnstile による不正対策。
  • 多言語 i18n —— /zh /ja /en /ko の 4 プレフィックスルーティング、ルート直下 /Accept-Language で自動リダイレクト(zh / ja は対等な二軸、en / ko はアクセシビリティのための翻訳レイヤー)。
  • 会場のクラウドソーシング —— サイト内「見たい会場」一時置き場で +1(公開票数 + 日次レート制限 + 同名の重複排除)、または GitHub PR で会場 JSON を直接投稿。
  • メンテナー管理画面 —— /admin は Cloudflare Access のエッジ認証で保護、投稿のソフト削除に対応。
  • SEO と AI への発見性 —— すべてのページが canonical + 4 ロケールの hreflang(x-default を含む)を出力。会場ページは MusicVenue(評価が十分にある場合は aggregateRating 付き)/ BreadcrumbList / 座席写真の ImageGallery 構造化データを注入し、トップページは WebSite / Organization を注入します。サイトルートでは /sitemap.xml(ロケール × パス + hreflang の代替)と /llms.txt(AI 向けのプレーンテキスト会場インデックス)を配信し、価値の低いページ(一時置き場 / 管理画面)は noindex,follow を付与します。

技術スタック

レイヤー 採用 説明
フロントエンドフレームワーク Astro 6.4 + React 19.2 Islands 大部分は静的化、インタラクティブなコンポーネントは React
デプロイアダプター @astrojs/cloudflare v13.7 Astro 6 は Cloudflare Pages 非対応となり、全面的に Workers を使用(SSR + 静的アセットを同一 Worker で)
ランタイムバインディング import { env } from "cloudflare:workers" Astro v6 は Astro.locals.runtime.env を削除。型は src/env.d.tsCloudflare.Env を参照
スタイリング Tailwind v4.3(Vite プラグイン @tailwindcss/vite 独立した tailwind.config なし。デザイントークンは src/styles/global.css に記述
UI コンポーネント すべて手書きDESIGN.md のトークンに準拠) components.json は存在するが、UI は shadcn/ui 生成ではない
アイコン lucide-react
検索 Fuse.js(クライアント側で全件) 会場 ≤ 200、バンドル内の全件検索でレイテンシゼロ
データベース Cloudflare D1 + Drizzle ORM photos / staging / photo corrections / venue ratings。schema は src/server/db/schema.ts。マイグレーションは drizzle-kit generate
レート制限 Cloudflare KVRATE_LIMIT 投稿、一時置き場、座席番号修正、評価の日次カウントとクールダウン。TTL で自動失効
画像ストレージ Cloudflare R2BUCKET バインディング直接書き込み、presigned URL ではない
ボット対策 Cloudflare Turnstile 二段階:フロントエンド token → バックエンド siteverify
コメント giscus + @giscus/react GitHub Discussions を使用。コメントドロワーは初回オープン時だけ遅延ロードし、サイトのライト / ダークテーマに追従
匿名評価 D1 集約テーブル + React island 4項目の 1〜5 星評価。venue_id + ip_hash で重複排除し、venue_rating_agg から次元別集約を読み取り
画像処理 browser-image-compression 長辺 1920px / WebP / EXIF 除去 / 約 500KB
Lightbox yet-another-react-lightbox v3
ウォーターフォール react-photo-album(masonry)
座席表のズーム react-zoom-pan-pinch v4.0 setTransform / resetTransform でプログラム的にズーム
i18n Astro 組み込み i18n ルーティング /zh /ja /en /ko の 4 プレフィックス、ルート直下は 302
SEO / 構造化データ 手書き JSON-LD + hreflang + sitemap / llms.txt src/lib/seo/(純粋関数 + 単体テスト):canonical / 4 ロケールの hreflang / MusicVenue·Breadcrumb·ImageGallery JSON-LD / /sitemap.xml / /llms.txt
ULID 自前実装src/server/id.ts ulid パッケージは不使用(import 時に detectPrng() が workerd で例外を投げるため)

[!NOTE] いくつかの実装は初期の PRD / research の記述と意図的に異なります。本リポジトリを正とします。詳しくは 仕組み → 主要な実装上のトレードオフ を参照。

クイックスタート

[!IMPORTANT] 前提:Node ≥ 22.12(Astro 6 の要件)。

# 1. 依存関係をインストール
npm install

# 2. ローカルのシークレットを準備(デフォルトは Cloudflare ドキュメントの「常に通過」Turnstile テスト key、オフラインで動作確認可能)
cp .dev.vars.example .dev.vars

# 3.(任意)imageUrl が .svg を指す新規会場のプレースホルダー座席表を生成。収録済み会場の座席表はリポジトリに同梱
npm run gen:seatmaps

# 4. ローカル D1 を初期化(マイグレーション適用)
npm run db:migrate:local

# 5. デモのマーカーを生成して投入(座席表 / ウォーターフォール / Lightbox にコンテンツを用意)
npm run gen:seed && npm run db:seed:local

# 6. 開発サーバーを起動
npm run dev        # ページ開発専用、最速の HMR(D1/KV/R2 バインディングと API は利用不可)
# または
npm run preview    # フル機能(バインディング + API、miniflare 経由)

[!TIP] UI を書く・スタイルを調整するには npm run dev(最速)。アップロード / 一時置き場 / 管理画面など Cloudflare バインディングに依存する機能を連携テストするには npm run preview(先に astro build、続いて wrangler dev がビルド成果物 dist/server/wrangler.json を指し、ローカルでは miniflare が D1/KV/R2 を提供)。

[!WARNING] ルートの wrangler.jsonc に対して直接 wrangler dev を実行しないでください(つまり -c なし):ルート設定はバインディングを宣言するだけで main/assetsありません。そのためビルド成果物ではなくアダプターのソースエントリが起動し、すべてのページの SSR がリテラルの [object Object] を返してしまいます。npm run preview / npm run deploy は正しい設定を指すよう既に設定済みです。

すべての npm スクリプト

コマンド 役割
npm run dev astro dev、ページのホットリロード
npm run build astro build、Workers バンドルを dist/ に出力
npm run preview astro build 後に wrangler dev -c dist/server/wrangler.json、ビルド成果物 + バインディングをローカルで実行
npm test node --experimental-strip-types --test、純粋ロジックの単体テストを実行
npm run typecheck astro check、型チェック
npm run format / format:check Prettier フォーマット / チェック(CI は format:check
npm run db:generate drizzle-kit generate、schema からマイグレーションを生成
npm run db:migrate:local / :prod wrangler d1 migrations apply(ローカル / リモート)
npm run gen:seatmaps プレースホルダー座席表 SVG を生成
npm run gen:seed デモのシード SQL を生成
npm run db:seed:local デモのシードをローカル D1 に投入
npm run cf-typegen wrangler types、バインディングの型を生成
npm run deploy astro build && wrangler deploy -c dist/server/wrangler.json

Cloudflare へのデプロイ

リソースを一度作成し、返ってきた id を wrangler.jsonc に記入してから、マイグレーション + デプロイします。

# 1. D1 / KV / R2 リソースを作成
wrangler d1 create seatmap-real
wrangler kv namespace create RATE_LIMIT
wrangler kv namespace create SESSION       # Astro CF アダプターは SESSION KV バインディングを要求します
wrangler r2 bucket create seatmap-images

# 2. 返ってきた実際の id を wrangler.jsonc に記入(プレースホルダー YOUR_*):
#    d1_databases[0].database_id、kv_namespaces[].id(RATE_LIMIT と SESSION に各 1 つ)

# 3. リモート D1 にマイグレーションを適用
npm run db:migrate:prod

# 4. Turnstile の本番シークレットを設定(リポジトリにコミットしないこと)
wrangler secret put TURNSTILE_SECRET_KEY
#    site key は .env.production の PUBLIC_TURNSTILE_SITE_KEY に記入(さらに wrangler.jsonc の vars と同期)

# 5. giscus コメントを設定(公開リソース id、シークレットではありません)
#    GitHub repo で Discussions を有効化し、giscus App をインストールし、
#    "Venue Comments" category を作成してから PUBLIC_GISCUS_REPO / REPO_ID /
#    CATEGORY / CATEGORY_ID を .env.production に記入し、wrangler.jsonc vars と同期

# 6. デプロイ
npm run deploy

[!NOTE] 自動デプロイ (CD):設定が完了すると、main への push で GitHub Actions が自動的にビルドして Cloudflare にデプロイします(.github/workflows/ci.yml。すべてのチェックが通った後にのみデプロイ。Actions タブの「Run workflow」から手動トリガーも可能)。上記の npm run deploy は初回 / ローカルでの手動デプロイ用です。

一度きりの設定:Cloudflare ダッシュボードで「Edit Cloudflare Workers」テンプレートを使って API トークンを作成し(Account = 自分のアカウント、Zone = genchi.top。custom domain で権限不足になる場合は Zone → DNS: Edit も追加)、GitHub repo → Settings → Secrets and variables → Actions でリポジトリシークレット CLOUDFLARE_API_TOKEN として追加します。

CD は D1 マイグレーションを実行しません。schema を変更した場合は引き続き手動で npm run db:migrate:prod を実行してください。

[!IMPORTANT] メンテナー管理画面/admin + /api/admin/*)は Cloudflare Access (Zero Trust) によりエッジで保護されます:ダッシュボードの Zero Trust → Access → Applications で /*/admin/api/admin/* をカバーする self-hosted アプリを新規作成し、Allow → メンテナーのメールアドレスの policy を 1 つ追加します。Access が認証後に Cf-Access-Authenticated-User-Email を注入し、Worker はそのヘッダーを信頼します(src/server/admin-auth.ts)。匿名トラフィックは Worker に到達しません。本番では admin の環境変数は不要です。本番で DEV_ADMIN_EMAIL絶対に設定しないでください——SSO ゲートウェイを迂回してしまいます。

[!NOTE] 本リポジトリには既に日本 / 一部海外の会場データ 74 件(public/seatmaps/ 下に座席図画像 90 点)+ デモのマーカーが含まれています。本番の実際のマーカーは、ユーザーがアップロードフローを通じて D1 に書き込みます。npm run db:migrate:prod の再実行が必要なのは DB schema を変更したときだけで、フロントエンドのみの変更にマイグレーションは不要です。

仕組み

アップロードフロー(バインディング直接書き込み + HMAC ticket)

presigned URL によるクライアント直接アップロードではなく、sign + commit の二段階方式を採用し、D1 への書き込みが偽造不可能であること、そしてローカルの miniflare R2 だけで全工程を通しで動作確認できること(S3 認証情報 / バケット CORS 不要)を保証します:

  1. クライアントが座席表上でマークし画像を選択、browser-image-compression で約 500KB の WebP に圧縮(長辺 ≤ 1920、EXIF 除去)し、Turnstile を通過して token を取得します。
  2. POST /api/upload/sign —— Worker が Turnstile + 30s クールダウン + 1 日 10 件の上限(KV、キーはハッシュ化した IP)を検証し、書き込む全フィールド(venue / sub-map / 座標 / 座席番号 / ip_hash / image_key / 有効期限)をバインドした HMAC ticket を発行します。この時点では日次クォータを消費しません
  3. POST /api/upload/commit(multipart)—— クライアントが ticket + WebP のバイト列を送り返します。Worker は HMAC + 有効期限を再検証し、バイト列を BUCKET バインディング経由で R2 に書き込み、続いてticket 内のフィールドを使って(リクエストボディは信頼しない)D1 に挿入し、最後にようやく日次クォータを消費 + 30s クールダウンを開始します。
  4. ネットワークエラー / 5xx は自動リトライし、同じ ticket(および消費済みの Turnstile token)を再利用します。4xx(ticket 期限切れなど)はリトライしません。

ソフト削除:メンテナーが /admin で画像を削除すると、D1 が deleted_at をセット(公開クエリは deleted_at IS NULL でフィルタし、マーカー / カードが即座に消えます)し、R2 オブジェクトはゴミ箱に残るため復元できます。ゴミ箱の「完全に削除」だけが、R2 オブジェクトと D1 行をどちらも物理削除します。

環境変数 / バインディング

名称 種別 用途 ローカル 本番
DB D1 photos + staging_venues + photo_correction_requests + venue_ratings / venue_rating_agg miniflare 自動 wrangler.jsonc に実際の database_id を記入
BUCKET R2 アップロード画像のストレージ(バインディング直接書き込み) miniflare 自動 wrangler.jsoncbucket_name
RATE_LIMIT KV IP レート制限のカウント + クールダウン(TTL)。投稿 / 一時置き場 / 座席番号修正 / 評価を含む miniflare 自動 wrangler.jsonc に実際の KV id を記入
SESSION KV Astro CF アダプターの session API が要求するバインディング(実際には書き込まない) miniflare 自動 wrangler.jsonc に実際の KV id を記入
TURNSTILE_SECRET_KEY シークレット バックエンド siteverify / HMAC ticket / IP-hash salt .dev.vars(テスト secret) wrangler secret put
PUBLIC_TURNSTILE_SITE_KEY 公開 var フロントエンド Turnstile widget .env.development .env.production(+ wrangler.jsonc vars のランタイムコピー)
PUBLIC_R2_BASE_URL 公開 var アップロード画像 URL を組み立てる。空 → 同一オリジン /r2/<key> にフォールバック .env.development(空) .env.production(r2.dev / カスタムドメイン)
PUBLIC_SITE_URL 公開 var サイトのベース URL http://localhost:4321 本番ドメイン
PUBLIC_GISCUS_REPO / PUBLIC_GISCUS_REPO_ID / PUBLIC_GISCUS_CATEGORY / PUBLIC_GISCUS_CATEGORY_ID 公開 var giscus の会場コメント設定。必須値が空の場合、コメント欄は未開放状態を表示し、第三者リソースを読み込みません .env.development .env.production(+ wrangler.jsonc vars のランタイムコピー)
DEV_ADMIN_EMAIL ローカル限定 メンテナー identity の mock(Access エッジがない場合) .dev.vars(任意のメール) 絶対に設定しない(Cloudflare Access を使用)

[!NOTE] PUBLIC_* は Vite がビルド時.env* からクライアントバンドルへインライン化します(islands は import.meta.env.PUBLIC_* で読み取り)——wrangler.jsoncvars(これは Worker ランタイムにのみ届く)からではありません。2 つの仕組み、2 つのファイルなので、Turnstile / R2 / giscus などの公開値は同期を忘れないでください。R2 アップロードに S3 presigned 認証情報(R2_ACCESS_KEY_ID など)は不要で、Worker は BUCKET バインディング経由で直接書き込みます。

会場コメントと匿名評価

会場タイトル付近には VenueComments island がマウントされ、初期状態では控えめな入口(平均点 / 評価数 / コメント)だけを表示します。@giscus/react はドロワーを初めて開いたときだけ動的 import されます。閉じるときはアンマウントせず非表示にするため、再オープン時に giscus iframe が再読み込みされません。giscus は mapping="specific" + term="venue:<id>" を使うので、同じ会場は言語パスや座席表タブをまたいで 1 つの GitHub Discussions スレッドを共有します。テーマはシステムテーマではなく、サイトの html.dark クラスに追従します。PUBLIC_GISCUS_CATEGORY_ID など必須値が空の場合、コメント欄は「まだ利用できません」状態だけを表示し、第三者 script / iframe は読み込みません。

Extension points exported contracts — how you extend this code

GridAlbumPhoto (Interface)
AlbumPhoto carrying the original DTO so render fns reach seat_label / id.
src/islands/PhotoGrid.tsx
CloudflareEnv (Interface)
(no doc)
src/env.d.ts
SubMap (Interface)
(no doc)
src/types/venue.ts
LoadFailureProps (Interface)
(no doc)
src/components/LoadFailure.tsx
Messages (Interface)
(no doc)
src/i18n/locales/zh.ts
SubMapChangeDetail (Interface)
(no doc)
src/lib/submap.ts
Prefecture (Interface)
(no doc)
src/data/prefectures.ts
TurnstileResult (Interface)
(no doc)
src/server/turnstile.ts

Core symbols most depended-on inside this repo

cn
called by 58
src/lib/utils.ts
pref
called by 48
src/data/prefectures.ts
jsonError
called by 29
src/pages/api/admin/photos.ts
fillTemplate
called by 27
src/lib/format.ts
useLocale
called by 23
src/hooks/useLocale.ts
getDb
called by 17
src/server/db/index.ts
jsonError
called by 15
src/pages/api/admin/staging.ts
jsonError
called by 14
src/pages/api/upload/commit.ts

Shape

Function 360
Interface 123
Class 16
Method 12

Languages

TypeScript100%

Modules by API surface

src/server/photos.ts17 symbols
src/islands/Seatmap.tsx17 symbols
src/lib/admin.ts16 symbols
src/lib/admin-client.ts16 symbols
scripts/migrate-coords-frame-to-image.mjs16 symbols
src/server/ratings.ts15 symbols
src/lib/venue-rating.ts14 symbols
src/islands/AdminConsole.tsx13 symbols
src/islands/upload/UploadSheet.tsx12 symbols
src/islands/upload/DateField.tsx12 symbols
src/lib/seo/jsonld-core.ts11 symbols
src/islands/SeatViewLightbox.tsx11 symbols

For agents

$ claude mcp add seatview \
  -- python -m otcore.mcp_server <graph>

⬇ download graph artifact