Files
reloop-v2/backend/prisma/schema.prisma
reloop 871ca89da0 feat(phase6): BM tiers + Landing + Pricing + 평가원 기출 ProblemSet — tracks 2/4
트랙 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
  이름과 매칭. 결과/메모/시간만 수동 입력.
2026-04-12 01:26:35 +09:00

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")
}