// ReLoop v2 schema — persona forgetting curve model // Generated fresh; old FSRS fields dropped. DB reset via `prisma migrate reset`. // // NOTE: After editing tag-relation onDelete rules, run: // prisma migrate dev --name relax_tag_cascade // on the Dev VM before deploying. This migrates SkillSnapshot.tagId to nullable // and sets both SkillSnapshot and StudyLog tag FKs to ON DELETE SET NULL. generator client { provider = "prisma-client-js" } datasource db { provider = "mysql" url = env("DATABASE_URL") } // ─── Enums ──────────────────────────────────────────────────────── enum Persona { senior mid junior crammer } enum ReviewIntensity { strict moderate relaxed } enum StudyResult { correct incorrect partial } enum ReviewStatus { pending done skipped expired } enum SubscriptionTier { free pro school } // ─── Models ─────────────────────────────────────────────────────── model User { id Int @id @default(autoincrement()) email String @unique password String nickname String persona Persona @default(mid) currentGrade Int? targetGrade Int? reviewIntensity ReviewIntensity @default(moderate) onboardedAt DateTime? subscriptionTier SubscriptionTier @default(free) subscriptionUntil DateTime? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt subjects Subject[] studyLogs StudyLog[] reviewSchedules ReviewSchedule[] skillSnapshots SkillSnapshot[] @@map("users") } model Subject { id Int @id @default(autoincrement()) name String color String @default("#6366f1") userId Int user User @relation(fields: [userId], references: [id], onDelete: Cascade) createdAt DateTime @default(now()) tags Tag[] studyLogs StudyLog[] @@unique([userId, name]) @@index([userId]) @@map("subjects") } model Tag { id Int @id @default(autoincrement()) name String subjectId Int subject Subject @relation(fields: [subjectId], references: [id], onDelete: Cascade) createdAt DateTime @default(now()) studyLogs StudyLog[] skillSnapshots SkillSnapshot[] @@unique([subjectId, name]) @@index([subjectId]) @@map("tags") } model StudyLog { id Int @id @default(autoincrement()) userId Int user User @relation(fields: [userId], references: [id], onDelete: Cascade) subjectId Int subject Subject @relation(fields: [subjectId], references: [id]) tagId Int? tag Tag? @relation(fields: [tagId], references: [id], onDelete: SetNull) problemId Int? problem Problem? @relation(fields: [problemId], references: [id], onDelete: SetNull) title String difficulty Float baseCorrectRate Float? result StudyResult memo String? @db.Text studiedAt DateTime @default(now()) timeSpent Int? reviewSchedules ReviewSchedule[] @@index([userId, studiedAt]) @@index([subjectId]) @@index([tagId]) @@index([problemId]) @@map("study_logs") } model ReviewSchedule { id Int @id @default(autoincrement()) userId Int user User @relation(fields: [userId], references: [id], onDelete: Cascade) studyLogId Int studyLog StudyLog @relation(fields: [studyLogId], references: [id], onDelete: Cascade) scheduledAt DateTime reviewedAt DateTime? result StudyResult? iteration Int @default(0) predictedP Float? status ReviewStatus @default(pending) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([userId, status, scheduledAt]) @@index([studyLogId]) @@map("review_schedules") } /// 평가원/수능/모의평가 등 공개 기출 문제를 담는 컨테이너. /// Subject 와 별개로 존재 — 모든 사용자가 공유하며 읽기 전용. model ProblemSet { id Int @id @default(autoincrement()) title String /// examType 허용값: "sat" | "mock-june" | "mock-sept" | "academy" examType String year Int subjectName String sourceUrl String? /// 영어 과목의 듣기 음원 파일 경로 배열 (data/kice/ 상대경로). /// 예: ["2026/영어/영어영역듣기평가음원/01_문제 01.mp3", ...] audioUrls Json? createdAt DateTime @default(now()) problems Problem[] passages Passage[] @@unique([year, examType, subjectName]) @@map("problem_sets") } model Problem { id Int @id @default(autoincrement()) problemSetId Int problemSet ProblemSet @relation(fields: [problemSetId], references: [id], onDelete: Cascade) number Int title String difficulty Float baseCorrectRate Float? topic String? /// KICE 파싱 결과 — 본문 텍스트 (선택지 제외) bodyText String? @db.Text /// KICE 파싱 결과 — 선택지 객체 { "1": "...", "2": "...", "3": "...", "4": "...", "5": "..." } choices Json? /// KICE 파싱 결과 — 정답 번호 1..5 answerNumber Int? /// 공통 지문 FK (국어 독서/문학, 영어 지문, 사탐 자료 등) passageId Int? passage Passage? @relation(fields: [passageId], references: [id], onDelete: SetNull) /// 파싱 엣지케이스 플래그 — 수동 보정 대상 needsReview Boolean @default(false) createdAt DateTime @default(now()) studyLogs StudyLog[] @@unique([problemSetId, number]) @@index([problemSetId]) @@index([passageId]) @@map("problems") } /// 평가원 기출의 [N~M] 공통 지문을 담는 컨테이너. /// 여러 Problem 이 같은 Passage 를 공유할 수 있음. model Passage { id Int @id @default(autoincrement()) problemSetId Int problemSet ProblemSet @relation(fields: [problemSetId], references: [id], onDelete: Cascade) startNumber Int endNumber Int bodyText String @db.Text createdAt DateTime @default(now()) problems Problem[] @@index([problemSetId]) @@map("passages") } model SkillSnapshot { id Int @id @default(autoincrement()) userId Int user User @relation(fields: [userId], references: [id], onDelete: Cascade) tagId Int? tag Tag? @relation(fields: [tagId], references: [id], onDelete: SetNull) s0 Float lastUpdatedAt DateTime @default(now()) sampleCount Int @default(0) @@unique([userId, tagId]) @@index([userId]) @@map("skill_snapshots") }