blob: 2468a45962b27cf8b4771bfc5dccc9d331f5b8ea (
plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
|
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## 概要
バンドとアーティストの情報管理サイト。React Router v7 (SSR) + SQLite + Tailwind CSS v4。
## コマンド
```bash
npm run dev # 開発サーバー (http://localhost:5173)
npm run build # プロダクションビルド
npm run typecheck # react-router typegen + tsc
```
## 重要ファイル
- `app/lib/db.server.ts` — DB 接続・スキーマ初期化・全 CRUD 関数 (すべての SQL はここに集約)
- `app/lib/constants.ts` — `LINK_TYPES` / `ARTIST_ROLES` など `as const` 定数・型
- `app/routes.ts` — ルート定義
- `app/root.tsx` — ルートレイアウト (nav バー・グローバルエラーバウンダリ含む)
- `app/routes/` — 各ページのルートファイル
## DB スキーマ
SQLite (`whois.db`)。テーブル: `bands`, `band_links`, `artists`, `artist_links`, `band_artists`, `band_revisions`, `artist_revisions`。
- バンドとアーティストは N:M 関係 (`band_artists` 中間テーブル、`order_index` で順序管理)
- `band_artists.role` はカンマ区切り文字列 (`"Vocal, Guitar"`) で複数ロールを保持。ロード時に `.split(", ").filter(Boolean)` で配列へ戻す
- 編集のたびに `*_revisions` テーブルへ完全な状態の JSON スナップショット + 更新メッセージ + IP を記録 (差分ではなく全量)
- 全 PK は `crypto.randomUUID()` で生成した UUID (TEXT 型)
- `foreign_keys = ON` / `journal_mode = WAL` を常に設定
## ルートファイルの命名
`routes.ts` で明示的に設定しているため、ファイル名はルート構造と無関係。
| ファイル | URL |
|---|---|
| `band-by-uuid.tsx` | `/bands/of/:uuid` |
| `band-by-slug.tsx` | `/bands/named/:slug` (UUID へリダイレクト) |
| `band-new.tsx` | `/bands/new` |
| `band-edit.tsx` | `/bands/of/:uuid/edit` |
| `band-history.tsx` | `/bands/of/:uuid/history` |
| artist 系も同様 | `/artists/...` |
slug-only ルート (`band-by-slug.tsx` 等) は `loader` だけを持ち、コンポーネントは `export default function() { return null; }` のみ。
## コーディング規約
- `loader` / `action` のみ `~/lib/db.server` をインポートする (クライアントバンドルに含めないため)
- フォームの動的フィールド (リンク・メンバー) は React `useState` で管理し、hidden input に JSON シリアライズして送信。サーバー側は `JSON.parse((fd.get("field") as string) || "[]")` でデシリアライズ
- slug は自動生成 (バンド名/アーティスト名から `toSlug()` 関数) かつ手動上書き可能。日本語などの Unicode 文字を保持するため正規表現で ASCII 以外も許容
- IP アドレスは `x-forwarded-for` → `x-real-ip` の順で取得 (nginx リバースプロキシ環境)
### action のエラーハンドリングパターン
```ts
// バリデーション失敗 → エラーオブジェクトを return (リダイレクトしない)
const errors: Record<string, string> = {};
if (!name) errors.name = "必須です";
if (Object.keys(errors).length > 0) return { errors };
// UNIQUE 制約違反はキャッチして専用エラーを返す
try {
createBand(...);
} catch (e) {
if (e instanceof Error && e.message.includes("UNIQUE constraint failed")) {
return { errors: { slug: "既に使われています" } };
}
throw e;
}
// 成功時は UUID ルートへリダイレクト
return redirect(`/bands/of/${id}`);
```
コンポーネントは `useActionData<typeof action>()` でエラーを受け取り、各フィールド直下に表示する。
### 404 の throw パターン
```ts
import { data } from "react-router";
if (!band) throw data("Not found", { status: 404 });
```
グローバルエラーバウンダリ (`root.tsx`) が `isRouteErrorResponse(error)` で処理する。
## デプロイ
```bash
git push hetzner master
```
`server-setup.sh` で設定した post-receive フックが自動で `npm ci && npm run build` して PM2 を再起動する。
|