Files
reloop-v2/backend/prisma/schema.prisma
reloop 58719e329e feat: 문제풀이 UI 통일 + PDF→문제집 + 통계탭 삭제
- 통계 탭 삭제 (프론트 페이지/차트/네비 항목, 백엔드 stats 모듈은 대시보드용으로 유지)
- PDF 문제 분리 → 문제집 생성으로 변경 (ProblemSet + Problem 레코드)
  - ProblemSet에 sourceType, uploadedByUserId 추가
  - POST /problem-sets/create-from-pdf 엔드포인트
  - GET /problem-sets/my-uploads, DELETE /problem-sets/:id
  - exams 페이지에 '내가 만든 문제집' 섹션
- 문제 풀이 UI 통일
  - DrawingCanvas 공통 컴포넌트 추출 (필기모드)
  - 복습 페이지에 필기모드(캔버스+펜색상) 추가
  - 다시풀기 페이지에 필기모드+메모 추가
  - 처음풀기 페이지 난이도 슬라이더→3버튼(H/M/E)+SM-2 연동

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-17 10:17:12 +09:00

438 lines
12 KiB
Plaintext

// 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[]
uploadedProblemSets ProblemSet[] @relation("uploadedProblemSets")
@@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?
correctAnswer Int? // 정답 번호 (1~5), PDF 해설지에서 추출 또는 수동 입력
imageUrl String? // 사용자 업로드 PDF에서 크롭된 문제 이미지 경로
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" | "user-upload"
examType String
year Int
subjectName String
mathUnit MathUnit?
sourceUrl String?
/// 듣기 음원 파일 경로 배열 (data/kice/ 상대경로).
/// 예: ["2026/listening/audios/01_track.mp3", ...]
audioUrls Json?
/// "kice" | "user-upload" — 기출 vs 사용자 업로드 구분
sourceType String @default("kice")
/// 사용자 업로드인 경우 업로드한 사용자
uploadedByUserId Int?
uploadedBy User? @relation("uploadedProblemSets", fields: [uploadedByUserId], references: [id], onDelete: SetNull)
createdAt DateTime @default(now())
problems Problem[]
passages Passage[]
assignments Assignment[]
@@unique([year, examType, subjectName, mathUnit])
@@index([uploadedByUserId])
@@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")
}