db만 구축했는데 안되면 되돌리셈
This commit is contained in:
162
prisma/README.md
Normal file
162
prisma/README.md
Normal 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)
|
||||
|
||||
311
prisma/migrations/20251121074818_new/migration.sql
Normal file
311
prisma/migrations/20251121074818_new/migration.sql
Normal 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;
|
||||
3
prisma/migrations/migration_lock.toml
Normal file
3
prisma/migrations/migration_lock.toml
Normal 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
313
prisma/schema.prisma
Normal 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
205
prisma/seed.ts
Normal 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();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user