트랙 2 — BM / Landing / Pricing - schema.prisma: SubscriptionTier enum(free/pro/school) + User.subscriptionTier + User.subscriptionUntil. Migration 20260411161837_bm_and_problemsets. - auth.service.ts / me.service.ts safeUser/toView 에 구독 필드 포함. - frontend/lib/api.ts MeUser 에 subscriptionTier/subscriptionUntil 추가. - app/page.tsx 전면 재작성: 토큰 있으면 /dashboard, 없으면 비로그인 Landing (히어로 + 왜 ReLoop 3카드 + 페르소나 4장 + 4단계 루프 + 가격 티저 + FAQ 4개 + footer). - app/pricing/page.tsx 신규: Free/Pro/School 3 티어 카드(Pro 강조 PopBadge), 월/년 cycle 토글, 기능 비교 테이블(데스크탑 전용), FAQ 5개, 바닥 CTA. Pro ₩7,900/월, 연간 ₩79,000(2개월 무료), School ₩20,000/월·30시트. - app/profile/page.tsx: 구독 섹션 + 티어 배지 + pricing 으로 가는 업그레이드 버튼. 트랙 4 — 평가원 기출 ProblemSet 데모 - schema.prisma: ProblemSet + Problem 신규 모델. StudyLog.problemId (optional, SetNull). 각 ProblemSet 은 (year, examType, subjectName) 복합 unique. - prisma/seed.ts: 2024학년도 수능 수학 샘플 10 문항 (1/5/8/11/13/15/20/22/ 28/30번) 대략치 난이도+정답률로 upsert. 실제 KICE 수치로 교체 가능하도록 주석 명시. - backend/src/problem-sets/: ProblemSetsModule + Service + Controller. GET /api/problem-sets (list, subject/year 필터) + GET /api/problem-sets/:id (문제 배열 포함). JwtAuthGuard 적용. - study-logs.service/controller: problemId optional 필드 수용. - frontend/lib/api.ts: ProblemSetSummary/ProblemSetDetail/Problem 타입. - frontend/app/study/page.tsx 재설계: 상단 탭 [직접 입력 | 문제집에서 가져오기]. 문제집 탭은 set 드롭다운 + 문제 리스트 클릭 → title/difficulty/baseCorrectRate 자동 채움 + subjectName 매핑으로 사용자 과목 자동 선택 + topic 을 tag 이름과 매칭. 결과/메모/시간만 수동 입력.
208 lines
5.5 KiB
Plaintext
208 lines
5.5 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 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 String // "sat" | "mock-june" | "mock-sept" | "academy" | ...
|
|
year Int
|
|
subjectName String
|
|
sourceUrl String?
|
|
createdAt DateTime @default(now())
|
|
|
|
problems Problem[]
|
|
|
|
@@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?
|
|
createdAt DateTime @default(now())
|
|
|
|
studyLogs StudyLog[]
|
|
|
|
@@unique([problemSetId, number])
|
|
@@index([problemSetId])
|
|
@@map("problems")
|
|
}
|
|
|
|
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")
|
|
}
|