👌
【開発ログ】学習カードのUIとDB設計(Next.js + Prisma)
現在、自分の子どものために教育用WEBアプリを作っています。
前回
- 認証(ログイン)と保護ルートを実装
- ダッシュボードの簡単なレイアウト & UI を作成

今回
- 学習カード用のダミーデータを作成
- 最小限のUI(LearningCard)を実装
- 学習用データベース(Prisma)を設計
① DB 設計(Prisma)
学習カードを作成するため、単語(Word) と 画像(Image) を 1:1 で結びつけるスキーマにしました。音声はTTSで生成する方針のため、DBには持たせません。
// 画像
model Image {
id String @id @default(cuid())
imageUrl String // Firebase Storage ダウンロードURL
storagePath String @unique // images/{uid}/{cuid}.jpg
contentType String?
createdAt DateTime @default(now())
// 所有者
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
// Word 側の imageId が参照する(片側ユニーク → 実質 1:1)
word Word?
}
// 単語
enum WordStatus {
draft
published
}
model Word {
id String @id @default(cuid())
koSurface String
jaSurface String
tags String[] @default([])
status WordStatus @default(draft)
createdAt DateTime @default(now())
// 代表画像(null可。設定時は一意 → 1:1)
imageId String? @unique
image Image? @relation(fields: [imageId], references: [id])
// 所有者
userId String
user User @relation(fields: [userId], references: [id])
// 同一ユーザー内での重複登録を防止
@@unique([userId, jaSurface])
@@unique([userId, koSurface])
// よく使う絞り込み用
@@index([userId, status, createdAt])
}
設計のポイント
-
Word と Image は 1 対 1
Word.imageIdを@uniqueにすることで、設定時は 1:1 を保証。画像未設定(null)は許可。 -
ユーザー単位の一意制約
@@unique([userId, jaSurface])/@@unique([userId, koSurface])により、同じユーザー内ではユニーク、ユーザーが違えば重複OK。 -
音声はDBに保存しない
まずは Web Speech API の TTS を使用。必要ならキャッシュを別テーブルで追加予定。
② Type とコンポーネント
-
WordエンティティはDBの完全形。 - UI には必要最小限の
LearningCardDataを渡す構成に分離。
// types/lesson.ts
export interface LearningCardData {
id: string; // = word.id
ko: string; // = word.koSurface
ja: string; // = word.jaSurface
imgUrl: string; // = word.image?.imageUrl ?? placeholder
tags: string[];
status: "draft" | "published";
}

③ フォルダ構成(現状)
web/
└── src/
├── app/
│ └── dashboard/
│ └── page.tsx // ダッシュボードページ
│
├── components/
│ └── learning/
│ ├── LearningCard.tsx // 単一カードコンポーネント
│ └── LearningList.tsx // カード一覧(ダミーデータを map)
│
├── lib/
│ └── mock/
│ └── lessons.ts // 学習カード用ダミーデータ
│
└── types/
├── lesson.ts // Lesson 型定義
└── word.ts // Word 型定義
悩んだこと
- 単語テーブルに持たせる最小情報の切り分け
- 音声データをDBに保存するか → 初期はTTS採用
-
typeとinterfaceの使い分け
学んだこと
-
@@unique([userId, jaSurface])により、同一ユーザー内での重複登録を抑止できる。
@unique jaSurfaceにすると全ユーザーに跨って一意になってしまうので要注意。 -
typeとinterfaceの使い分け(私の指針)-
UIコンポーネントの Props →
interface(拡張しやすく、IDE補完が分かりやすい) -
DB戻り値から派生したユーティリティ型 →
type(Union/Tuple等に強い)
-
UIコンポーネントの Props →
| type | interface | |
|---|---|---|
| 拡張 |
&(交差型) |
extends |
| 同名再定義 | できない(エラー) | マージされる(宣言合体) |
| Union / Tuple | できる | ほぼできない |
| クラス適用 |
implements 可能だが非推奨気味 |
implements と相性良 |
次回やること
- Prisma + API 経由で実データ読み込み
- 画像アップロード(Firebase Storage)
- 公開/下書き切り替え(Word.status "draft" | "published";)
- LessonCardの機能やデザイン補強
Discussion