// 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 MathUnit { common prob_stat calculus geometry } enum SubscriptionTier { free pro school } enum OrgType { school academy } enum OrgRole { admin teacher student } enum AssignmentStatus { active closed draft } // ─── Models ─────────────────────────────────────────────────────── model User { id Int @id @default(autoincrement()) email String @unique password String nickname String avatarUrl String? persona Persona @default(mid) currentGrade Int? targetGrade Int? targetExamYear Int? focusSubjects Json? focusUnits Json? bojHandle String? @db.VarChar(32) reviewIntensity ReviewIntensity @default(moderate) onboardedAt DateTime? subscriptionTier SubscriptionTier @default(free) subscriptionUntil DateTime? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt subjects Subject[] studyLogs StudyLog[] psBookmarks PsBookmark[] psSyncState PsSyncState? reviewSchedules ReviewSchedule[] skillSnapshots SkillSnapshot[] organizationMembers OrganizationMember[] classMemberships ClassMember[] teacherClasses Class[] @relation("teacherClasses") assignmentSubmissions AssignmentSubmission[] @@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) psProblemId Int? psProblem PsProblem? @relation(fields: [psProblemId], references: [id], onDelete: SetNull) title String difficulty Float baseCorrectRate Float? result StudyResult chosenAnswer Int? memo String? @db.Text studiedAt DateTime @default(now()) timeSpent Int? reviewSchedules ReviewSchedule[] @@index([userId, studiedAt]) @@index([subjectId]) @@index([tagId]) @@index([problemId]) @@index([psProblemId]) @@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 mathUnit MathUnit? sourceUrl String? /// 듣기 음원 파일 경로 배열 (data/kice/ 상대경로). /// 예: ["2026/listening/audios/01_track.mp3", ...] audioUrls Json? createdAt DateTime @default(now()) problems Problem[] passages Passage[] assignments Assignment[] @@unique([year, examType, subjectName, mathUnit]) @@map("problem_sets") } model Organization { id Int @id @default(autoincrement()) name String type OrgType @default(academy) inviteCode String @unique @db.VarChar(12) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt members OrganizationMember[] classes Class[] @@map("organizations") } model OrganizationMember { id Int @id @default(autoincrement()) userId Int user User @relation(fields: [userId], references: [id], onDelete: Cascade) organizationId Int organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) role OrgRole @default(student) joinedAt DateTime @default(now()) @@unique([userId, organizationId]) @@index([organizationId]) @@map("organization_members") } model Class { id Int @id @default(autoincrement()) name String organizationId Int organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) teacherId Int teacher User @relation("teacherClasses", fields: [teacherId], references: [id]) createdAt DateTime @default(now()) members ClassMember[] assignments Assignment[] @@index([organizationId]) @@index([teacherId]) @@map("classes") } model ClassMember { id Int @id @default(autoincrement()) userId Int user User @relation(fields: [userId], references: [id], onDelete: Cascade) classId Int class Class @relation(fields: [classId], references: [id], onDelete: Cascade) joinedAt DateTime @default(now()) @@unique([userId, classId]) @@index([classId]) @@map("class_members") } model Assignment { id Int @id @default(autoincrement()) title String description String? @db.Text classId Int class Class @relation(fields: [classId], references: [id], onDelete: Cascade) problemSetId Int problemSet ProblemSet @relation(fields: [problemSetId], references: [id]) status AssignmentStatus @default(active) dueDate DateTime? createdAt DateTime @default(now()) submissions AssignmentSubmission[] @@index([classId]) @@index([problemSetId]) @@map("assignments") } model AssignmentSubmission { id Int @id @default(autoincrement()) userId Int user User @relation(fields: [userId], references: [id], onDelete: Cascade) assignmentId Int assignment Assignment @relation(fields: [assignmentId], references: [id], onDelete: Cascade) score Float? totalProblems Int? correctCount Int? completedAt DateTime? studyLogIds Json? createdAt DateTime @default(now()) @@unique([userId, assignmentId]) @@index([assignmentId]) @@map("assignment_submissions") } 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? /// 렌더링된 문제 이미지 URL (수학/도표형 문제 대응) imageUrl String? /// 렌더링된 원본 페이지 이미지 URL pageImageUrl String? /// 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 imageUrl String? 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") } model PsProblem { id Int @id @default(autoincrement()) bojId Int @unique title String titleKo String? level Int tags Json? acceptedUserCount Int? averageTries Float? solvedacUpdatedAt DateTime? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt bookmarks PsBookmark[] studyLogs StudyLog[] @@index([level]) @@map("ps_problems") } model PsBookmark { id Int @id @default(autoincrement()) userId Int user User @relation(fields: [userId], references: [id], onDelete: Cascade) psProblemId Int psProblem PsProblem @relation(fields: [psProblemId], references: [id], onDelete: Cascade) memo String? createdAt DateTime @default(now()) @@unique([userId, psProblemId]) @@index([userId]) @@map("ps_bookmarks") } model PsSyncState { id Int @id @default(autoincrement()) userId Int @unique user User @relation(fields: [userId], references: [id], onDelete: Cascade) lastSyncedAt DateTime? lastSolvedCount Int @default(0) lastTier Int? updatedAt DateTime @updatedAt @@map("ps_sync_states") }