db만 구축했는데 안되면 되돌리셈

This commit is contained in:
2025-11-21 17:03:55 +09:00
parent 24f17b1dd1
commit 6a546b6fcd
17 changed files with 3499 additions and 3 deletions

162
prisma/README.md Normal file
View File

@@ -0,0 +1,162 @@
# Prisma 데이터베이스 관리 가이드
## 📋 개요
이 문서는 Prisma를 사용한 데이터베이스 스키마 관리 및 마이그레이션 워크플로우를 설명합니다.
## 🚀 개발 환경 워크플로우
### 1. 스키마 변경 후 데이터베이스 최신화
#### 방법 A: 마이그레이션 생성 및 적용 (권장)
```bash
# 1. 스키마 변경 (prisma/schema.prisma 수정)
# 2. 마이그레이션 생성 및 적용
npm run db:migrate
# 마이그레이션 이름을 지정하려면:
npx prisma migrate dev --name add_new_field
```
이 명령어는:
- 마이그레이션 파일 생성
- 데이터베이스에 마이그레이션 적용
- Prisma Client 자동 재생성
#### 방법 B: 개발 중 빠른 프로토타이핑 (데이터 손실 가능)
```bash
# 스키마 변경 후 즉시 적용 (마이그레이션 파일 생성 안 함)
npm run db:push
```
⚠️ **주의**: `db:push`는 프로덕션 환경에서는 사용하지 마세요!
### 2. Seed 데이터 재실행
```bash
# Seed 데이터 삽입
npm run db:seed
# 또는 마이그레이션 리셋 후 자동으로 seed 실행
npm run db:migrate:reset
```
### 3. 전체 초기화 (개발 환경 전용)
```bash
# 데이터베이스 완전히 리셋 + 마이그레이션 재적용 + seed 실행
npm run db:migrate:reset
```
⚠️ **주의**: 모든 데이터가 삭제됩니다!
## 📦 프로덕션 환경 워크플로우
### 1. 마이그레이션 배포
```bash
# 생성된 마이그레이션 파일들을 프로덕션 DB에 적용
npm run db:migrate:deploy
```
이 명령어는:
- 마이그레이션 파일만 적용 (새로운 마이그레이션 생성 안 함)
- 안전하게 프로덕션에 적용 가능
### 2. Prisma Client 재생성
```bash
npm run db:generate
```
## 🔄 일반적인 워크플로우 시나리오
### 시나리오 1: 새로운 필드 추가
```bash
# 1. schema.prisma에 필드 추가
# 예: model User { ... newField String? }
# 2. 마이그레이션 생성 및 적용
npm run db:migrate --name add_new_field
# 3. (선택) Seed 데이터 업데이트 필요 시
npm run db:seed
```
### 시나리오 2: 관계 추가/변경
```bash
# 1. schema.prisma에 관계 추가
# 2. 마이그레이션 생성
npm run db:migrate --name add_relation
# 3. 기존 데이터 마이그레이션 필요 시 수동으로 처리
```
### 시나리오 3: 개발 중 스키마 실험
```bash
# 빠르게 스키마 변경 테스트 (마이그레이션 파일 생성 안 함)
npm run db:push
# 만족스러우면 마이그레이션 생성
npm run db:migrate --name experimental_changes
```
## 🛠️ 유용한 명령어
### Prisma Studio (데이터베이스 GUI)
```bash
npm run db:studio
```
브라우저에서 데이터베이스를 시각적으로 확인하고 편집할 수 있습니다.
### 기존 데이터베이스에서 스키마 가져오기
```bash
npm run db:pull
```
기존 데이터베이스 구조를 분석하여 `schema.prisma`를 생성합니다.
## 📝 마이그레이션 파일 관리
- 마이그레이션 파일은 `prisma/migrations/` 폴더에 저장됩니다
- 각 마이그레이션은 타임스탬프와 이름으로 식별됩니다
- 마이그레이션 파일은 Git에 커밋해야 합니다
- 팀원들과 마이그레이션을 공유하여 동일한 스키마를 유지합니다
## ⚠️ 주의사항
1. **프로덕션 환경**에서는 절대 `db:push``db:migrate:reset`을 사용하지 마세요
2. **마이그레이션 파일**은 항상 Git에 커밋하세요
3. **스키마 변경 전**에 백업을 권장합니다
4. **Seed 데이터**는 개발 환경에서만 사용하세요
## 🔍 문제 해결
### 마이그레이션 충돌 시
```bash
# 마이그레이션 상태 확인
npx prisma migrate status
# 문제 해결 후
npm run db:migrate:resolve
```
### Prisma Client가 최신이 아닐 때
```bash
npm run db:generate
```
## 📚 추가 리소스
- [Prisma 마이그레이션 가이드](https://www.prisma.io/docs/concepts/components/prisma-migrate)
- [Prisma CLI 참조](https://www.prisma.io/docs/reference/api-reference/command-reference)

View File

@@ -0,0 +1,311 @@
-- CreateEnum
CREATE TYPE "UserRole" AS ENUM ('LEARNER', 'ADMIN');
-- CreateEnum
CREATE TYPE "UserStatus" AS ENUM ('ACTIVE', 'INACTIVE');
-- CreateEnum
CREATE TYPE "QuestionType" AS ENUM ('MULTIPLE_CHOICE', 'SHORT_ANSWER', 'ESSAY');
-- CreateTable
CREATE TABLE "users" (
"id" TEXT NOT NULL,
"email" TEXT NOT NULL,
"password" TEXT NOT NULL,
"name" TEXT NOT NULL,
"phone" TEXT,
"gender" TEXT,
"birthdate" TIMESTAMP(3),
"role" "UserRole" NOT NULL DEFAULT 'LEARNER',
"status" "UserStatus" NOT NULL DEFAULT 'ACTIVE',
"joinDate" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "users_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "courses" (
"id" TEXT NOT NULL,
"courseName" TEXT NOT NULL,
"instructorId" TEXT NOT NULL,
"createdById" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "courses_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "lessons" (
"id" TEXT NOT NULL,
"courseId" TEXT NOT NULL,
"lessonName" TEXT NOT NULL,
"learningGoal" TEXT,
"createdById" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "lessons_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "lesson_videos" (
"id" TEXT NOT NULL,
"lessonId" TEXT NOT NULL,
"fileName" TEXT NOT NULL,
"filePath" TEXT NOT NULL,
"fileSize" INTEGER NOT NULL,
"order" INTEGER NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "lesson_videos_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "lesson_vr_contents" (
"id" TEXT NOT NULL,
"lessonId" TEXT NOT NULL,
"fileName" TEXT NOT NULL,
"filePath" TEXT NOT NULL,
"fileSize" INTEGER NOT NULL,
"order" INTEGER NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "lesson_vr_contents_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "lesson_attachments" (
"id" TEXT NOT NULL,
"lessonId" TEXT NOT NULL,
"fileName" TEXT NOT NULL,
"filePath" TEXT NOT NULL,
"fileSize" INTEGER NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "lesson_attachments_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "questions" (
"id" TEXT NOT NULL,
"lessonId" TEXT,
"question" TEXT NOT NULL,
"type" "QuestionType" NOT NULL DEFAULT 'MULTIPLE_CHOICE',
"options" JSONB,
"correctAnswer" TEXT NOT NULL,
"explanation" TEXT,
"points" INTEGER NOT NULL DEFAULT 1,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "questions_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "notices" (
"id" TEXT NOT NULL,
"title" TEXT NOT NULL,
"content" TEXT NOT NULL,
"writerId" TEXT NOT NULL,
"views" INTEGER NOT NULL DEFAULT 0,
"hasAttachment" BOOLEAN NOT NULL DEFAULT false,
"date" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "notices_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "notice_attachments" (
"id" TEXT NOT NULL,
"noticeId" TEXT NOT NULL,
"fileName" TEXT NOT NULL,
"filePath" TEXT NOT NULL,
"fileSize" INTEGER NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "notice_attachments_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "resources" (
"id" TEXT NOT NULL,
"title" TEXT NOT NULL,
"description" TEXT,
"filePath" TEXT,
"fileName" TEXT,
"fileSize" INTEGER,
"category" TEXT,
"views" INTEGER NOT NULL DEFAULT 0,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "resources_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "enrollments" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"lessonId" TEXT NOT NULL,
"progress" INTEGER NOT NULL DEFAULT 0,
"completedAt" TIMESTAMP(3),
"enrolledAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "enrollments_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "certificates" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"lessonId" TEXT,
"courseId" TEXT,
"verificationKey" TEXT NOT NULL,
"issuedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "certificates_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "logs" (
"id" TEXT NOT NULL,
"userId" TEXT,
"action" TEXT NOT NULL,
"ipAddress" TEXT,
"userAgent" TEXT,
"details" JSONB,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "logs_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "users_email_key" ON "users"("email");
-- CreateIndex
CREATE INDEX "users_email_idx" ON "users"("email");
-- CreateIndex
CREATE INDEX "users_role_idx" ON "users"("role");
-- CreateIndex
CREATE INDEX "users_status_idx" ON "users"("status");
-- CreateIndex
CREATE INDEX "courses_instructorId_idx" ON "courses"("instructorId");
-- CreateIndex
CREATE INDEX "courses_createdById_idx" ON "courses"("createdById");
-- CreateIndex
CREATE INDEX "courses_createdAt_idx" ON "courses"("createdAt");
-- CreateIndex
CREATE INDEX "lessons_courseId_idx" ON "lessons"("courseId");
-- CreateIndex
CREATE INDEX "lessons_createdById_idx" ON "lessons"("createdById");
-- CreateIndex
CREATE INDEX "lessons_createdAt_idx" ON "lessons"("createdAt");
-- CreateIndex
CREATE INDEX "lesson_videos_lessonId_idx" ON "lesson_videos"("lessonId");
-- CreateIndex
CREATE INDEX "lesson_vr_contents_lessonId_idx" ON "lesson_vr_contents"("lessonId");
-- CreateIndex
CREATE INDEX "lesson_attachments_lessonId_idx" ON "lesson_attachments"("lessonId");
-- CreateIndex
CREATE INDEX "questions_lessonId_idx" ON "questions"("lessonId");
-- CreateIndex
CREATE INDEX "notices_writerId_idx" ON "notices"("writerId");
-- CreateIndex
CREATE INDEX "notices_date_idx" ON "notices"("date");
-- CreateIndex
CREATE UNIQUE INDEX "notice_attachments_noticeId_key" ON "notice_attachments"("noticeId");
-- CreateIndex
CREATE INDEX "resources_category_idx" ON "resources"("category");
-- CreateIndex
CREATE INDEX "resources_createdAt_idx" ON "resources"("createdAt");
-- CreateIndex
CREATE INDEX "enrollments_userId_idx" ON "enrollments"("userId");
-- CreateIndex
CREATE INDEX "enrollments_lessonId_idx" ON "enrollments"("lessonId");
-- CreateIndex
CREATE UNIQUE INDEX "enrollments_userId_lessonId_key" ON "enrollments"("userId", "lessonId");
-- CreateIndex
CREATE UNIQUE INDEX "certificates_verificationKey_key" ON "certificates"("verificationKey");
-- CreateIndex
CREATE INDEX "certificates_userId_idx" ON "certificates"("userId");
-- CreateIndex
CREATE INDEX "certificates_verificationKey_idx" ON "certificates"("verificationKey");
-- CreateIndex
CREATE INDEX "logs_userId_idx" ON "logs"("userId");
-- CreateIndex
CREATE INDEX "logs_action_idx" ON "logs"("action");
-- CreateIndex
CREATE INDEX "logs_createdAt_idx" ON "logs"("createdAt");
-- AddForeignKey
ALTER TABLE "courses" ADD CONSTRAINT "courses_instructorId_fkey" FOREIGN KEY ("instructorId") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "courses" ADD CONSTRAINT "courses_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "lessons" ADD CONSTRAINT "lessons_courseId_fkey" FOREIGN KEY ("courseId") REFERENCES "courses"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "lessons" ADD CONSTRAINT "lessons_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "lesson_videos" ADD CONSTRAINT "lesson_videos_lessonId_fkey" FOREIGN KEY ("lessonId") REFERENCES "lessons"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "lesson_vr_contents" ADD CONSTRAINT "lesson_vr_contents_lessonId_fkey" FOREIGN KEY ("lessonId") REFERENCES "lessons"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "lesson_attachments" ADD CONSTRAINT "lesson_attachments_lessonId_fkey" FOREIGN KEY ("lessonId") REFERENCES "lessons"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "questions" ADD CONSTRAINT "questions_lessonId_fkey" FOREIGN KEY ("lessonId") REFERENCES "lessons"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "notices" ADD CONSTRAINT "notices_writerId_fkey" FOREIGN KEY ("writerId") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "notice_attachments" ADD CONSTRAINT "notice_attachments_noticeId_fkey" FOREIGN KEY ("noticeId") REFERENCES "notices"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "enrollments" ADD CONSTRAINT "enrollments_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "enrollments" ADD CONSTRAINT "enrollments_lessonId_fkey" FOREIGN KEY ("lessonId") REFERENCES "lessons"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "certificates" ADD CONSTRAINT "certificates_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "logs" ADD CONSTRAINT "logs_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE;

View File

@@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (e.g., Git)
provider = "postgresql"

313
prisma/schema.prisma Normal file
View File

@@ -0,0 +1,313 @@
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?
// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
// ============================================
// 사용자 관련 모델
// ============================================
/// 사용자 (User)
/// 권한 설정 페이지, 로그인/회원가입에서 사용
model User {
id String @id @default(uuid())
email String @unique // 이메일 (아이디로 사용)
password String // 비밀번호 (해시화되어 저장)
name String // 성명
phone String? // 휴대폰 번호
gender String? // 성별 (M/F)
birthdate DateTime? // 생년월일
role UserRole @default(LEARNER) // 권한: 학습자, 관리자
status UserStatus @default(ACTIVE) // 계정 상태: 활성화, 비활성화
joinDate DateTime @default(now()) // 가입일
// 관계
createdCourses Course[] @relation("CourseCreator")
instructedCourses Course[] @relation("CourseInstructor")
createdLessons Lesson[] @relation("LessonCreator")
createdNotices Notice[] @relation("NoticeWriter")
enrollments Enrollment[] // 수강 등록
certificates Certificate[] // 수료증
logs Log[] // 로그 기록
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([email])
@@index([role])
@@index([status])
@@map("users")
}
enum UserRole {
LEARNER // 학습자
ADMIN // 관리자 (강사 권한 포함)
}
enum UserStatus {
ACTIVE // 활성화
INACTIVE // 비활성화
}
// ============================================
// 교육과정 관련 모델
// ============================================
/// 교육과정 (Course)
/// 교육과정 관리 페이지에서 사용
model Course {
id String @id @default(uuid())
courseName String // 교육과정명
instructorId String // 강사 ID (User의 ADMIN 역할)
instructor User @relation("CourseInstructor", fields: [instructorId], references: [id])
createdById String // 등록자 ID
createdBy User @relation("CourseCreator", fields: [createdById], references: [id])
createdAt DateTime @default(now()) // 생성일
// 관계
lessons Lesson[] // 강좌 목록
updatedAt DateTime @updatedAt
@@index([instructorId])
@@index([createdById])
@@index([createdAt])
@@map("courses")
}
/// 강좌 (Lesson)
/// 강좌 관리 페이지에서 사용
model Lesson {
id String @id @default(uuid())
courseId String // 교육과정 ID
course Course @relation(fields: [courseId], references: [id], onDelete: Cascade)
lessonName String // 강좌명
learningGoal String? @db.Text // 학습 목표 (최대 1000자)
createdById String // 등록자 ID
createdBy User @relation("LessonCreator", fields: [createdById], references: [id])
createdAt DateTime @default(now()) // 등록일
// 관계
videos LessonVideo[] // 강좌 영상
vrContents LessonVRContent[] // VR 콘텐츠
questions Question[] // 학습 평가 문제
attachments LessonAttachment[] // 첨부파일
enrollments Enrollment[] // 수강 등록
updatedAt DateTime @updatedAt
@@index([courseId])
@@index([createdById])
@@index([createdAt])
@@map("lessons")
}
/// 강좌 영상 (LessonVideo)
/// 강좌 등록 시 첨부되는 영상 파일 (최대 10개, 30MB 미만)
model LessonVideo {
id String @id @default(uuid())
lessonId String // 강좌 ID
lesson Lesson @relation(fields: [lessonId], references: [id], onDelete: Cascade)
fileName String // 파일명
filePath String // 파일 저장 경로
fileSize Int // 파일 크기 (bytes)
order Int // 순서
createdAt DateTime @default(now())
@@index([lessonId])
@@map("lesson_videos")
}
/// VR 콘텐츠 (LessonVRContent)
/// 강좌 등록 시 첨부되는 VR 콘텐츠 파일 (최대 10개, 30MB 미만)
model LessonVRContent {
id String @id @default(uuid())
lessonId String // 강좌 ID
lesson Lesson @relation(fields: [lessonId], references: [id], onDelete: Cascade)
fileName String // 파일명
filePath String // 파일 저장 경로
fileSize Int // 파일 크기 (bytes)
order Int // 순서
createdAt DateTime @default(now())
@@index([lessonId])
@@map("lesson_vr_contents")
}
/// 강좌 첨부파일 (LessonAttachment)
/// 강좌 관련 기타 첨부파일
model LessonAttachment {
id String @id @default(uuid())
lessonId String // 강좌 ID
lesson Lesson @relation(fields: [lessonId], references: [id], onDelete: Cascade)
fileName String // 파일명
filePath String // 파일 저장 경로
fileSize Int // 파일 크기 (bytes)
createdAt DateTime @default(now())
@@index([lessonId])
@@map("lesson_attachments")
}
// ============================================
// 학습 평가 관련 모델
// ============================================
/// 문제 (Question)
/// 문제 은행 페이지에서 사용, 강좌별 학습 평가 문제
model Question {
id String @id @default(uuid())
lessonId String? // 강좌 ID (선택적, 문제 은행에만 있을 수도 있음)
lesson Lesson? @relation(fields: [lessonId], references: [id], onDelete: SetNull)
question String @db.Text // 문제 내용
type QuestionType @default(MULTIPLE_CHOICE) // 문제 유형
options Json? // 선택지 (객관식인 경우)
correctAnswer String @db.Text // 정답
explanation String? @db.Text // 해설
points Int @default(1) // 배점
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([lessonId])
@@map("questions")
}
enum QuestionType {
MULTIPLE_CHOICE // 객관식
SHORT_ANSWER // 단답형
ESSAY // 서술형
}
// ============================================
// 공지사항 관련 모델
// ============================================
/// 공지사항 (Notice)
/// 공지사항 관리 페이지에서 사용
model Notice {
id String @id @default(uuid())
title String // 제목
content String @db.Text // 내용 (최대 1000자)
writerId String // 작성자 ID
writer User @relation("NoticeWriter", fields: [writerId], references: [id])
views Int @default(0) // 조회수
hasAttachment Boolean @default(false) // 첨부파일 여부
date DateTime @default(now()) // 게시일
// 관계
attachment NoticeAttachment? // 첨부파일
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([writerId])
@@index([date])
@@map("notices")
}
/// 공지사항 첨부파일 (NoticeAttachment)
/// 공지사항에 첨부되는 파일 (최대 1개, 30MB 미만)
model NoticeAttachment {
id String @id @default(uuid())
noticeId String @unique // 공지사항 ID
notice Notice @relation(fields: [noticeId], references: [id], onDelete: Cascade)
fileName String // 파일명
filePath String // 파일 저장 경로
fileSize Int // 파일 크기 (bytes)
createdAt DateTime @default(now())
@@map("notice_attachments")
}
// ============================================
// 학습 자료 관련 모델
// ============================================
/// 학습 자료 (Resource)
/// 학습 자료실 페이지에서 사용
model Resource {
id String @id @default(uuid())
title String // 제목
description String? @db.Text // 설명
filePath String? // 파일 경로 (파일이 있는 경우)
fileName String? // 파일명
fileSize Int? // 파일 크기 (bytes)
category String? // 카테고리
views Int @default(0) // 조회수
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([category])
@@index([createdAt])
@@map("resources")
}
// ============================================
// 수강 및 수료 관련 모델
// ============================================
/// 수강 등록 (Enrollment)
/// 사용자가 강좌를 수강하는 관계
model Enrollment {
id String @id @default(uuid())
userId String // 사용자 ID
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
lessonId String // 강좌 ID
lesson Lesson @relation(fields: [lessonId], references: [id], onDelete: Cascade)
progress Int @default(0) // 학습 진행률 (0-100)
completedAt DateTime? // 완료일
enrolledAt DateTime @default(now()) // 등록일
@@unique([userId, lessonId])
@@index([userId])
@@index([lessonId])
@@map("enrollments")
}
/// 수료증 (Certificate)
/// 수료증 발급/검증키 관리 페이지에서 사용
model Certificate {
id String @id @default(uuid())
userId String // 사용자 ID
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
lessonId String? // 강좌 ID (강좌 완료 시 발급)
courseId String? // 교육과정 ID (과정 완료 시 발급)
verificationKey String @unique // 검증 키
issuedAt DateTime @default(now()) // 발급일
@@index([userId])
@@index([verificationKey])
@@map("certificates")
}
// ============================================
// 로그 관련 모델
// ============================================
/// 로그 (Log)
/// 로그/접속 기록 페이지에서 사용
model Log {
id String @id @default(uuid())
userId String? // 사용자 ID (로그인한 경우)
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
action String // 액션 (예: LOGIN, LOGOUT, VIEW_LESSON, etc.)
ipAddress String? // IP 주소
userAgent String? // User Agent
details Json? // 추가 상세 정보
createdAt DateTime @default(now()) // 기록 시간
@@index([userId])
@@index([action])
@@index([createdAt])
@@map("logs")
}

205
prisma/seed.ts Normal file
View File

@@ -0,0 +1,205 @@
import { PrismaClient, UserRole, UserStatus } from '@prisma/client';
const prisma = new PrismaClient();
// 간단한 비밀번호 해시 함수 (실제 프로덕션에서는 bcrypt 사용 권장)
function hashPassword(password: string): string {
// 개발 환경용 간단한 해시 (실제로는 bcrypt 사용)
// 모든 사용자의 기본 비밀번호는 "password123"으로 설정
return '$2a$10$placeholder_hash_for_development_only';
}
async function main() {
console.log('🌱 Seeding database...');
// 1. 사용자 데이터 생성
const mockUsers = [
{ id: "1", joinDate: "2024-01-15", name: "김민준", email: "user1@example.com", role: "learner", status: "active" },
{ id: "2", joinDate: "2024-01-20", name: "이서준", email: "user2@example.com", role: "learner", status: "active" },
{ id: "3", joinDate: "2024-02-05", name: "박도윤", email: "user3@example.com", role: "learner", status: "inactive" },
{ id: "4", joinDate: "2024-02-10", name: "최예준", email: "user4@example.com", role: "instructor", status: "active" },
{ id: "5", joinDate: "2024-02-15", name: "정시우", email: "user5@example.com", role: "instructor", status: "active" },
{ id: "6", joinDate: "2024-02-20", name: "강하준", email: "user6@example.com", role: "learner", status: "active" },
{ id: "7", joinDate: "2024-03-01", name: "조주원", email: "user7@example.com", role: "admin", status: "active" },
{ id: "8", joinDate: "2024-03-05", name: "윤지호", email: "user8@example.com", role: "learner", status: "active" },
{ id: "9", joinDate: "2024-03-10", name: "장준서", email: "user9@example.com", role: "learner", status: "inactive" },
{ id: "10", joinDate: "2024-03-15", name: "임건우", email: "user10@example.com", role: "instructor", status: "active" },
{ id: "11", joinDate: "2024-03-20", name: "한서연", email: "user11@example.com", role: "learner", status: "active" },
{ id: "12", joinDate: "2024-04-01", name: "오서윤", email: "user12@example.com", role: "learner", status: "active" },
{ id: "13", joinDate: "2024-04-05", name: "서지우", email: "user13@example.com", role: "instructor", status: "inactive" },
{ id: "14", joinDate: "2024-04-10", name: "신서현", email: "user14@example.com", role: "learner", status: "active" },
{ id: "15", joinDate: "2024-04-15", name: "권민서", email: "user15@example.com", role: "admin", status: "active" },
{ id: "16", joinDate: "2024-04-20", name: "황하은", email: "user16@example.com", role: "learner", status: "active" },
{ id: "17", joinDate: "2024-05-01", name: "안예은", email: "user17@example.com", role: "learner", status: "inactive" },
{ id: "18", joinDate: "2024-05-05", name: "송윤서", email: "user18@example.com", role: "instructor", status: "active" },
{ id: "19", joinDate: "2024-05-10", name: "전채원", email: "user19@example.com", role: "learner", status: "active" },
{ id: "20", joinDate: "2024-05-15", name: "홍지원", email: "user20@example.com", role: "learner", status: "active" },
{ id: "21", joinDate: "2024-05-20", name: "김민수", email: "user21@example.com", role: "instructor", status: "active" },
{ id: "22", joinDate: "2024-06-01", name: "이영희", email: "user22@example.com", role: "learner", status: "active" },
{ id: "23", joinDate: "2024-06-05", name: "박철수", email: "user23@example.com", role: "learner", status: "inactive" },
{ id: "24", joinDate: "2024-06-10", name: "최수진", email: "user24@example.com", role: "admin", status: "active" },
{ id: "25", joinDate: "2024-06-15", name: "정대현", email: "user25@example.com", role: "instructor", status: "active" },
{ id: "26", joinDate: "2024-06-20", name: "강미영", email: "user26@example.com", role: "learner", status: "active" },
{ id: "27", joinDate: "2024-07-01", name: "조성호", email: "user27@example.com", role: "learner", status: "active" },
{ id: "28", joinDate: "2024-07-05", name: "윤지은", email: "user28@example.com", role: "instructor", status: "inactive" },
{ id: "29", joinDate: "2024-07-10", name: "장현우", email: "user29@example.com", role: "learner", status: "active" },
{ id: "30", joinDate: "2024-07-15", name: "임소영", email: "user30@example.com", role: "learner", status: "active" },
];
// 사용자 생성 및 ID 매핑 저장
const userMap = new Map<string, string>();
for (const userData of mockUsers) {
const role = userData.role === 'instructor' ? UserRole.ADMIN :
userData.role === 'admin' ? UserRole.ADMIN :
UserRole.LEARNER;
const status = userData.status === 'active' ? UserStatus.ACTIVE : UserStatus.INACTIVE;
const user = await prisma.user.upsert({
where: { email: userData.email },
update: {},
create: {
email: userData.email,
password: hashPassword('password123'), // 기본 비밀번호
name: userData.name,
role,
status,
joinDate: new Date(userData.joinDate),
},
});
userMap.set(userData.id, user.id);
}
console.log(`✅ Created ${userMap.size} users`);
// 관리자 계정 찾기 (공지사항 작성자용)
const adminUsers = await prisma.user.findMany({
where: { role: UserRole.ADMIN, status: UserStatus.ACTIVE },
});
const defaultAdmin = adminUsers[0];
// 2. 교육과정 데이터 생성
const mockCourses = [
{ id: "1", courseName: "웹 개발 기초", instructorName: "최예준", createdAt: "2024-01-15", createdBy: "관리자" },
{ id: "2", courseName: "React 실전 프로젝트", instructorName: "정시우", createdAt: "2024-02-20", createdBy: "관리자" },
{ id: "3", courseName: "데이터베이스 설계", instructorName: "임건우", createdAt: "2024-03-10", createdBy: "관리자" },
{ id: "4", courseName: "Node.js 백엔드 개발", instructorName: "송윤서", createdAt: "2024-03-25", createdBy: "관리자" },
{ id: "5", courseName: "TypeScript 마스터", instructorName: "김민수", createdAt: "2024-04-05", createdBy: "관리자" },
{ id: "6", courseName: "UI/UX 디자인 기초", instructorName: "정대현", createdAt: "2024-04-18", createdBy: "관리자" },
{ id: "7", courseName: "모바일 앱 개발", instructorName: "최예준", createdAt: "2024-05-02", createdBy: "관리자" },
{ id: "8", courseName: "클라우드 인프라", instructorName: "정시우", createdAt: "2024-05-15", createdBy: "관리자" },
{ id: "9", courseName: "머신러닝 입문", instructorName: "임건우", createdAt: "2024-06-01", createdBy: "관리자" },
{ id: "10", courseName: "DevOps 실무", instructorName: "송윤서", createdAt: "2024-06-20", createdBy: "관리자" },
];
const courseMap = new Map<string, string>();
for (const courseData of mockCourses) {
// 강사 이름으로 사용자 찾기
const instructor = await prisma.user.findFirst({
where: { name: courseData.instructorName, role: UserRole.ADMIN },
});
if (!instructor) {
console.warn(`⚠️ Instructor not found: ${courseData.instructorName}`);
continue;
}
const course = await prisma.course.create({
data: {
courseName: courseData.courseName,
instructorId: instructor.id,
createdById: defaultAdmin?.id || instructor.id,
createdAt: new Date(courseData.createdAt),
},
});
courseMap.set(courseData.id, course.id);
}
console.log(`✅ Created ${courseMap.size} courses`);
// 3. 공지사항 데이터 생성
const mockNotices = [
{
id: 2,
title: '공지사항 제목이 노출돼요',
date: '2025-09-10',
views: 1230,
writer: '문지호',
content: [
'사이트 이용 관련 주요 변경 사항을 안내드립니다.',
'변경되는 내용은 공지일자로부터 즉시 적용됩니다.',
],
},
{
id: 1,
title: '📢 방사선학 온라인 강의 수강 안내 및 필수 공지',
date: '2025-06-28',
views: 594,
writer: '문지호',
hasAttachment: true,
content: [
'온라인 강의 수강 방법과 필수 확인 사항을 안내드립니다.',
'수강 기간 및 출석, 과제 제출 관련 정책을 반드시 확인해 주세요.',
],
},
];
// 공지사항 작성자 찾기 또는 생성
let noticeWriter = await prisma.user.findFirst({
where: { name: '문지호' },
});
if (!noticeWriter) {
noticeWriter = await prisma.user.create({
data: {
email: 'munjih@example.com',
password: hashPassword('password123'),
name: '문지호',
role: UserRole.ADMIN,
status: UserStatus.ACTIVE,
},
});
}
for (const noticeData of mockNotices) {
const notice = await prisma.notice.create({
data: {
title: noticeData.title,
content: noticeData.content?.join('\n') || '',
writerId: noticeWriter.id,
views: noticeData.views,
hasAttachment: noticeData.hasAttachment || false,
date: new Date(noticeData.date),
},
});
// 첨부파일이 있는 경우
if (noticeData.hasAttachment) {
await prisma.noticeAttachment.create({
data: {
noticeId: notice.id,
fileName: '공지사항_첨부파일.pdf',
filePath: '/uploads/notices/sample.pdf',
fileSize: 1024000, // 1MB
},
});
}
}
console.log(`✅ Created ${mockNotices.length} notices`);
console.log('🎉 Seeding completed!');
}
main()
.catch((e) => {
console.error('❌ Error seeding database:', e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});