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

2
.gitignore vendored
View File

@@ -39,3 +39,5 @@ yarn-error.log*
# typescript
*.tsbuildinfo
next-env.d.ts
/src/generated/prisma

565
API_USAGE.md Normal file
View File

@@ -0,0 +1,565 @@
# API 사용 가이드
이 문서는 데이터베이스에 데이터를 생성하는 API의 사용 방법을 설명합니다.
## 📋 목차
1. [기본 설정](#기본-설정)
2. [사용자 API](#1-사용자-api)
3. [교육과정 API](#2-교육과정-api)
4. [강좌 API](#3-강좌-api)
5. [공지사항 API](#4-공지사항-api)
6. [에러 처리](#에러-처리)
7. [실전 예제](#실전-예제)
---
## 기본 설정
### 환경 변수
`.env` 파일에 데이터베이스 연결 정보가 설정되어 있어야 합니다:
```env
DATABASE_URL="postgresql://user:password@localhost:5432/dbname"
```
### API 기본 URL
- 개발 환경: `http://localhost:3000/api`
- 프로덕션: `https://your-domain.com/api`
---
## 1. 사용자 API
### POST /api/users - 사용자 생성
새로운 사용자를 생성합니다.
#### 요청
```typescript
const response = await fetch('/api/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
email: 'user@example.com',
password: 'hashed_password_here', // 실제로는 해시화된 비밀번호
name: '홍길동',
phone: '010-1234-5678', // 선택사항
gender: 'M', // 선택사항: 'M' 또는 'F'
birthdate: '1990-01-01', // 선택사항: YYYY-MM-DD 형식
role: 'LEARNER', // 선택사항: 'LEARNER' 또는 'ADMIN' (기본값: 'LEARNER')
status: 'ACTIVE', // 선택사항: 'ACTIVE' 또는 'INACTIVE' (기본값: 'ACTIVE')
}),
});
const data = await response.json();
```
#### 성공 응답 (201)
```json
{
"message": "사용자가 성공적으로 생성되었습니다.",
"user": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"email": "user@example.com",
"name": "홍길동",
"phone": "010-1234-5678",
"gender": "M",
"birthdate": "1990-01-01T00:00:00.000Z",
"role": "LEARNER",
"status": "ACTIVE",
"joinDate": "2024-11-21T00:00:00.000Z",
"createdAt": "2024-11-21T00:00:00.000Z",
"updatedAt": "2024-11-21T00:00:00.000Z"
}
}
```
#### 에러 응답
**400 Bad Request** - 필수 필드 누락
```json
{
"error": "이메일, 비밀번호, 이름은 필수입니다."
}
```
**409 Conflict** - 이메일 중복
```json
{
"error": "이미 존재하는 이메일입니다."
}
```
### GET /api/users - 사용자 목록 조회
사용자 목록을 조회합니다. 필터링 및 페이지네이션을 지원합니다.
#### 요청
```typescript
// 전체 사용자 조회
const response = await fetch('/api/users');
// 필터링 및 페이지네이션
const response = await fetch('/api/users?role=LEARNER&status=ACTIVE&page=1&limit=10');
const data = await response.json();
```
#### 쿼리 파라미터
- `role` (선택): `LEARNER` 또는 `ADMIN`
- `status` (선택): `ACTIVE` 또는 `INACTIVE`
- `page` (선택): 페이지 번호 (기본값: 1)
- `limit` (선택): 페이지당 항목 수 (기본값: 10)
#### 성공 응답 (200)
```json
{
"users": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"email": "user@example.com",
"name": "홍길동",
"role": "LEARNER",
"status": "ACTIVE",
...
}
],
"pagination": {
"page": 1,
"limit": 10,
"total": 30,
"totalPages": 3
}
}
```
---
## 2. 교육과정 API
### POST /api/courses - 교육과정 생성
새로운 교육과정을 생성합니다.
#### 요청
```typescript
const response = await fetch('/api/courses', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
courseName: '웹 개발 기초',
instructorId: 'instructor-uuid-here', // 필수: 강사(ADMIN 역할)의 ID
createdById: 'admin-uuid-here', // 선택사항: 등록자 ID (기본값: instructorId)
}),
});
const data = await response.json();
```
#### 성공 응답 (201)
```json
{
"message": "교육과정이 성공적으로 생성되었습니다.",
"course": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"courseName": "웹 개발 기초",
"instructorId": "instructor-uuid",
"createdById": "admin-uuid",
"createdAt": "2024-11-21T00:00:00.000Z",
"instructor": {
"id": "instructor-uuid",
"name": "최예준",
"email": "instructor@example.com"
},
"createdBy": {
"id": "admin-uuid",
"name": "관리자"
}
}
}
```
#### 에러 응답
**400 Bad Request** - 필수 필드 누락
```json
{
"error": "교육과정명과 강사 ID는 필수입니다."
}
```
**404 Not Found** - 강사를 찾을 수 없음
```json
{
"error": "강사를 찾을 수 없습니다."
}
```
### GET /api/courses - 교육과정 목록 조회
교육과정 목록을 조회합니다.
#### 요청
```typescript
// 전체 교육과정 조회
const response = await fetch('/api/courses');
// 특정 강사의 교육과정 조회
const response = await fetch('/api/courses?instructorId=instructor-uuid&page=1&limit=10');
const data = await response.json();
```
#### 쿼리 파라미터
- `instructorId` (선택): 강사 ID로 필터링
- `page` (선택): 페이지 번호 (기본값: 1)
- `limit` (선택): 페이지당 항목 수 (기본값: 10)
---
## 3. 강좌 API
### POST /api/lessons - 강좌 생성
새로운 강좌를 생성합니다.
#### 요청
```typescript
const response = await fetch('/api/lessons', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
courseId: 'course-uuid-here', // 필수: 교육과정 ID
lessonName: 'HTML 기초', // 필수: 강좌명
learningGoal: 'HTML의 기본 문법을 이해하고 활용할 수 있다.', // 선택사항: 학습 목표
createdById: 'admin-uuid-here', // 선택사항: 등록자 ID
}),
});
const data = await response.json();
```
#### 성공 응답 (201)
```json
{
"message": "강좌가 성공적으로 생성되었습니다.",
"lesson": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"courseId": "course-uuid",
"lessonName": "HTML 기초",
"learningGoal": "HTML의 기본 문법을 이해하고 활용할 수 있다.",
"createdAt": "2024-11-21T00:00:00.000Z",
"course": {
"id": "course-uuid",
"courseName": "웹 개발 기초"
},
"createdBy": {
"id": "admin-uuid",
"name": "관리자"
},
"_count": {
"videos": 0,
"vrContents": 0,
"questions": 0
}
}
}
```
### GET /api/lessons - 강좌 목록 조회
강좌 목록을 조회합니다.
#### 요청
```typescript
// 전체 강좌 조회
const response = await fetch('/api/lessons');
// 특정 교육과정의 강좌 조회
const response = await fetch('/api/lessons?courseId=course-uuid&page=1&limit=10');
const data = await response.json();
```
---
## 4. 공지사항 API
### POST /api/notices - 공지사항 생성
새로운 공지사항을 생성합니다.
#### 요청
```typescript
const response = await fetch('/api/notices', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
title: '공지사항 제목',
content: '공지사항 내용입니다.\n여러 줄로 작성할 수 있습니다.',
writerId: 'admin-uuid-here', // 필수: 작성자 ID
hasAttachment: false, // 선택사항: 첨부파일 여부 (기본값: false)
}),
});
const data = await response.json();
```
#### 성공 응답 (201)
```json
{
"message": "공지사항이 성공적으로 생성되었습니다.",
"notice": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"title": "공지사항 제목",
"content": "공지사항 내용입니다.\n여러 줄로 작성할 수 있습니다.",
"writerId": "admin-uuid",
"views": 0,
"hasAttachment": false,
"date": "2024-11-21T00:00:00.000Z",
"writer": {
"id": "admin-uuid",
"name": "관리자",
"email": "admin@example.com"
}
}
}
```
### GET /api/notices - 공지사항 목록 조회
공지사항 목록을 조회합니다.
#### 요청
```typescript
// 전체 공지사항 조회
const response = await fetch('/api/notices');
// 특정 작성자의 공지사항 조회
const response = await fetch('/api/notices?writerId=admin-uuid&page=1&limit=10');
const data = await response.json();
```
---
## 에러 처리
모든 API는 일관된 에러 응답 형식을 사용합니다:
```typescript
try {
const response = await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(userData),
});
if (!response.ok) {
const error = await response.json();
console.error('에러:', error.error);
// 에러 처리 로직
return;
}
const data = await response.json();
console.log('성공:', data);
} catch (error) {
console.error('네트워크 오류:', error);
}
```
### HTTP 상태 코드
- `200` - 성공 (GET 요청)
- `201` - 생성 성공 (POST 요청)
- `400` - 잘못된 요청 (필수 필드 누락 등)
- `404` - 리소스를 찾을 수 없음
- `409` - 충돌 (중복 데이터 등)
- `500` - 서버 오류
---
## 실전 예제
### React 컴포넌트에서 사용하기
```typescript
'use client';
import { useState } from 'react';
export default function CreateUserForm() {
const [formData, setFormData] = useState({
email: '',
password: '',
name: '',
role: 'LEARNER',
});
const [loading, setLoading] = useState(false);
const [message, setMessage] = useState('');
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
setMessage('');
try {
const response = await fetch('/api/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(formData),
});
const data = await response.json();
if (!response.ok) {
setMessage(`오류: ${data.error}`);
return;
}
setMessage('사용자가 성공적으로 생성되었습니다!');
// 폼 초기화
setFormData({ email: '', password: '', name: '', role: 'LEARNER' });
} catch (error) {
setMessage('네트워크 오류가 발생했습니다.');
} finally {
setLoading(false);
}
};
return (
<form onSubmit={handleSubmit}>
<input
type="email"
placeholder="이메일"
value={formData.email}
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
required
/>
<input
type="password"
placeholder="비밀번호"
value={formData.password}
onChange={(e) => setFormData({ ...formData, password: e.target.value })}
required
/>
<input
type="text"
placeholder="이름"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
required
/>
<select
value={formData.role}
onChange={(e) => setFormData({ ...formData, role: e.target.value })}
>
<option value="LEARNER">학습자</option>
<option value="ADMIN">관리자</option>
</select>
<button type="submit" disabled={loading}>
{loading ? '생성 중...' : '사용자 생성'}
</button>
{message && <p>{message}</p>}
</form>
);
}
```
### Server Component에서 사용하기
```typescript
// app/admin/users/page.tsx
import { prisma } from '@/lib/prisma';
export default async function UsersPage() {
const users = await prisma.user.findMany({
take: 10,
orderBy: { createdAt: 'desc' },
});
return (
<div>
<h1>사용자 목록</h1>
<ul>
{users.map((user) => (
<li key={user.id}>
{user.name} ({user.email}) - {user.role}
</li>
))}
</ul>
</div>
);
}
```
### cURL로 테스트하기
```bash
# 사용자 생성
curl -X POST http://localhost:3000/api/users \
-H "Content-Type: application/json" \
-d '{
"email": "test@example.com",
"password": "test123",
"name": "테스트 사용자",
"role": "LEARNER"
}'
# 사용자 목록 조회
curl http://localhost:3000/api/users?role=LEARNER&page=1&limit=10
# 교육과정 생성
curl -X POST http://localhost:3000/api/courses \
-H "Content-Type: application/json" \
-d '{
"courseName": "웹 개발 기초",
"instructorId": "instructor-uuid-here"
}'
```
---
## 🔒 보안 고려사항
1. **비밀번호 해시화**: 실제 프로덕션에서는 bcrypt 등을 사용하여 비밀번호를 해시화해야 합니다.
2. **인증/인가**: 현재 API는 인증이 없습니다. 프로덕션에서는 JWT 또는 세션 기반 인증을 추가해야 합니다.
3. **입력 검증**: 클라이언트 측 검증 외에도 서버 측 검증이 필요합니다.
4. **CORS 설정**: 필요시 CORS 설정을 추가해야 합니다.
---
## 📚 추가 리소스
- [Next.js API Routes 문서](https://nextjs.org/docs/app/building-your-application/routing/route-handlers)
- [Prisma Client 문서](https://www.prisma.io/docs/concepts/components/prisma-client)

937
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -7,10 +7,24 @@
"build": "next build",
"start": "next start",
"lint": "biome check",
"format": "biome format --write"
"format": "biome format --write",
"db:generate": "prisma generate",
"db:migrate": "prisma migrate dev",
"db:migrate:deploy": "prisma migrate deploy",
"db:migrate:reset": "prisma migrate reset",
"db:seed": "tsx prisma/seed.ts",
"db:studio": "prisma studio",
"db:push": "prisma db push",
"db:pull": "prisma db pull"
},
"prisma": {
"seed": "tsx prisma/seed.ts"
},
"dependencies": {
"@prisma/client": "^6.19.0",
"dotenv": "^17.2.3",
"next": "16.0.3",
"prisma": "^6.19.0",
"react": "19.2.0",
"react-dom": "19.2.0"
},
@@ -21,6 +35,7 @@
"@types/react": "^19",
"@types/react-dom": "^19",
"tailwindcss": "^4",
"tsx": "^4.20.6",
"typescript": "^5"
}
}

13
prisma.config.ts Normal file
View File

@@ -0,0 +1,13 @@
import { defineConfig, env } from "prisma/config";
import "dotenv/config";
export default defineConfig({
schema: "prisma/schema.prisma",
migrations: {
path: "prisma/migrations",
},
engine: "classic",
datasource: {
url: env("DATABASE_URL"),
},
});

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();
});

211
src/app/api/README.md Normal file
View File

@@ -0,0 +1,211 @@
# API 엔드포인트 문서
이 문서는 데이터베이스에 데이터를 생성하는 API 엔드포인트를 설명합니다.
## 📋 API 목록
### 1. 사용자 API (`/api/users`)
#### POST - 사용자 생성
```bash
POST /api/users
Content-Type: application/json
{
"email": "user@example.com",
"password": "hashed_password",
"name": "홍길동",
"phone": "010-1234-5678",
"gender": "M",
"birthdate": "1990-01-01",
"role": "LEARNER", // 또는 "ADMIN"
"status": "ACTIVE" // 또는 "INACTIVE"
}
```
**응답:**
```json
{
"message": "사용자가 성공적으로 생성되었습니다.",
"user": {
"id": "uuid",
"email": "user@example.com",
"name": "홍길동",
...
}
}
```
#### GET - 사용자 목록 조회
```bash
GET /api/users?role=LEARNER&status=ACTIVE&page=1&limit=10
```
**쿼리 파라미터:**
- `role`: 필터링할 역할 (LEARNER, ADMIN)
- `status`: 필터링할 상태 (ACTIVE, INACTIVE)
- `page`: 페이지 번호 (기본값: 1)
- `limit`: 페이지당 항목 수 (기본값: 10)
---
### 2. 교육과정 API (`/api/courses`)
#### POST - 교육과정 생성
```bash
POST /api/courses
Content-Type: application/json
{
"courseName": "웹 개발 기초",
"instructorId": "instructor_uuid",
"createdById": "admin_uuid" // 선택사항, 기본값: instructorId
}
```
**응답:**
```json
{
"message": "교육과정이 성공적으로 생성되었습니다.",
"course": {
"id": "uuid",
"courseName": "웹 개발 기초",
"instructor": { ... },
"createdBy": { ... }
}
}
```
#### GET - 교육과정 목록 조회
```bash
GET /api/courses?instructorId=uuid&page=1&limit=10
```
---
### 3. 강좌 API (`/api/lessons`)
#### POST - 강좌 생성
```bash
POST /api/lessons
Content-Type: application/json
{
"courseId": "course_uuid",
"lessonName": "HTML 기초",
"learningGoal": "HTML의 기본 문법을 이해하고 활용할 수 있다.",
"createdById": "admin_uuid" // 선택사항
}
```
**응답:**
```json
{
"message": "강좌가 성공적으로 생성되었습니다.",
"lesson": {
"id": "uuid",
"lessonName": "HTML 기초",
"course": { ... },
"createdBy": { ... }
}
}
```
#### GET - 강좌 목록 조회
```bash
GET /api/lessons?courseId=uuid&page=1&limit=10
```
---
### 4. 공지사항 API (`/api/notices`)
#### POST - 공지사항 생성
```bash
POST /api/notices
Content-Type: application/json
{
"title": "공지사항 제목",
"content": "공지사항 내용",
"writerId": "admin_uuid",
"hasAttachment": false // 선택사항
}
```
**응답:**
```json
{
"message": "공지사항이 성공적으로 생성되었습니다.",
"notice": {
"id": "uuid",
"title": "공지사항 제목",
"content": "공지사항 내용",
"writer": { ... }
}
}
```
#### GET - 공지사항 목록 조회
```bash
GET /api/notices?writerId=uuid&page=1&limit=10
```
---
## 🔧 사용 예시
### JavaScript/TypeScript (fetch)
```typescript
// 사용자 생성
const response = await fetch('/api/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
email: 'user@example.com',
password: 'hashed_password',
name: '홍길동',
role: 'LEARNER',
}),
});
const data = await response.json();
console.log(data);
```
### cURL
```bash
# 사용자 생성
curl -X POST http://localhost:3000/api/users \
-H "Content-Type: application/json" \
-d '{
"email": "user@example.com",
"password": "hashed_password",
"name": "홍길동",
"role": "LEARNER"
}'
```
---
## ⚠️ 주의사항
1. **비밀번호 해시화**: 실제 프로덕션에서는 비밀번호를 해시화하여 저장해야 합니다.
2. **인증/인가**: 현재 API는 인증이 없습니다. 프로덕션에서는 JWT나 세션 기반 인증을 추가해야 합니다.
3. **에러 처리**: 모든 API는 적절한 에러 응답을 반환합니다.
4. **데이터 검증**: 필수 필드 검증이 포함되어 있습니다.
---
## 📝 다음 단계
- [ ] 인증 미들웨어 추가
- [ ] 비밀번호 해시화 로직 추가
- [ ] 파일 업로드 API 추가 (공지사항 첨부파일 등)
- [ ] 수정/삭제 API 추가
- [ ] 상세 조회 API 추가

View File

@@ -0,0 +1,126 @@
import { NextRequest, NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';
// 교육과정 생성
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const { courseName, instructorId, createdById } = body;
// 필수 필드 검증
if (!courseName || !instructorId) {
return NextResponse.json(
{ error: '교육과정명과 강사 ID는 필수입니다.' },
{ status: 400 }
);
}
// 강사 존재 확인
const instructor = await prisma.user.findUnique({
where: { id: instructorId },
});
if (!instructor) {
return NextResponse.json(
{ error: '강사를 찾을 수 없습니다.' },
{ status: 404 }
);
}
// 교육과정 생성
const course = await prisma.course.create({
data: {
courseName,
instructorId,
createdById: createdById || instructorId,
},
include: {
instructor: {
select: {
id: true,
name: true,
email: true,
},
},
createdBy: {
select: {
id: true,
name: true,
},
},
},
});
return NextResponse.json(
{ message: '교육과정이 성공적으로 생성되었습니다.', course },
{ status: 201 }
);
} catch (error) {
console.error('교육과정 생성 오류:', error);
return NextResponse.json(
{ error: '교육과정 생성 중 오류가 발생했습니다.' },
{ status: 500 }
);
}
}
// 교육과정 목록 조회
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url);
const instructorId = searchParams.get('instructorId');
const page = parseInt(searchParams.get('page') || '1');
const limit = parseInt(searchParams.get('limit') || '10');
const skip = (page - 1) * limit;
const where: any = {};
if (instructorId) where.instructorId = instructorId;
const [courses, total] = await Promise.all([
prisma.course.findMany({
where,
skip,
take: limit,
orderBy: { createdAt: 'desc' },
include: {
instructor: {
select: {
id: true,
name: true,
email: true,
},
},
createdBy: {
select: {
id: true,
name: true,
},
},
_count: {
select: {
lessons: true,
},
},
},
}),
prisma.course.count({ where }),
]);
return NextResponse.json({
courses,
pagination: {
page,
limit,
total,
totalPages: Math.ceil(total / limit),
},
});
} catch (error) {
console.error('교육과정 조회 오류:', error);
return NextResponse.json(
{ error: '교육과정 조회 중 오류가 발생했습니다.' },
{ status: 500 }
);
}
}

272
src/app/api/examples.ts Normal file
View File

@@ -0,0 +1,272 @@
/**
* API 사용 예제 모음
*
* 이 파일은 API를 사용하는 다양한 예제를 제공합니다.
* 실제 프로젝트에서 참고하여 사용하세요.
*/
// ============================================
// 1. 사용자 API 예제
// ============================================
/**
* 사용자 생성 예제
*/
export async function createUserExample() {
const response = await fetch('/api/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
email: 'newuser@example.com',
password: 'hashed_password_here', // 실제로는 bcrypt로 해시화
name: '홍길동',
phone: '010-1234-5678',
gender: 'M',
birthdate: '1990-01-01',
role: 'LEARNER',
status: 'ACTIVE',
}),
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error);
}
const data = await response.json();
return data.user;
}
/**
* 사용자 목록 조회 예제 (필터링)
*/
export async function getUsersExample() {
const params = new URLSearchParams({
role: 'LEARNER',
status: 'ACTIVE',
page: '1',
limit: '10',
});
const response = await fetch(`/api/users?${params.toString()}`);
const data = await response.json();
return data;
}
// ============================================
// 2. 교육과정 API 예제
// ============================================
/**
* 교육과정 생성 예제
*/
export async function createCourseExample(instructorId: string) {
const response = await fetch('/api/courses', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
courseName: '웹 개발 기초',
instructorId: instructorId,
createdById: instructorId, // 선택사항
}),
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error);
}
const data = await response.json();
return data.course;
}
/**
* 교육과정 목록 조회 예제
*/
export async function getCoursesExample(instructorId?: string) {
const params = new URLSearchParams();
if (instructorId) {
params.append('instructorId', instructorId);
}
params.append('page', '1');
params.append('limit', '10');
const response = await fetch(`/api/courses?${params.toString()}`);
const data = await response.json();
return data;
}
// ============================================
// 3. 강좌 API 예제
// ============================================
/**
* 강좌 생성 예제
*/
export async function createLessonExample(courseId: string) {
const response = await fetch('/api/lessons', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
courseId: courseId,
lessonName: 'HTML 기초',
learningGoal: 'HTML의 기본 문법을 이해하고 활용할 수 있다.',
createdById: undefined, // 선택사항
}),
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error);
}
const data = await response.json();
return data.lesson;
}
/**
* 강좌 목록 조회 예제
*/
export async function getLessonsExample(courseId?: string) {
const params = new URLSearchParams();
if (courseId) {
params.append('courseId', courseId);
}
params.append('page', '1');
params.append('limit', '10');
const response = await fetch(`/api/lessons?${params.toString()}`);
const data = await response.json();
return data;
}
// ============================================
// 4. 공지사항 API 예제
// ============================================
/**
* 공지사항 생성 예제
*/
export async function createNoticeExample(writerId: string) {
const response = await fetch('/api/notices', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
title: '공지사항 제목',
content: '공지사항 내용입니다.\n여러 줄로 작성할 수 있습니다.',
writerId: writerId,
hasAttachment: false,
}),
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error);
}
const data = await response.json();
return data.notice;
}
/**
* 공지사항 목록 조회 예제
*/
export async function getNoticesExample(writerId?: string) {
const params = new URLSearchParams();
if (writerId) {
params.append('writerId', writerId);
}
params.append('page', '1');
params.append('limit', '10');
const response = await fetch(`/api/notices?${params.toString()}`);
const data = await response.json();
return data;
}
// ============================================
// 5. 통합 예제 - 전체 워크플로우
// ============================================
/**
* 전체 워크플로우 예제
* 1. 관리자 사용자 생성
* 2. 교육과정 생성
* 3. 강좌 생성
* 4. 공지사항 생성
*/
export async function completeWorkflowExample() {
try {
// 1. 관리자 사용자 생성
const adminUser = await createUserExample();
console.log('관리자 생성:', adminUser);
// 2. 교육과정 생성
const course = await createCourseExample(adminUser.id);
console.log('교육과정 생성:', course);
// 3. 강좌 생성
const lesson = await createLessonExample(course.id);
console.log('강좌 생성:', lesson);
// 4. 공지사항 생성
const notice = await createNoticeExample(adminUser.id);
console.log('공지사항 생성:', notice);
return {
admin: adminUser,
course,
lesson,
notice,
};
} catch (error) {
console.error('워크플로우 실행 중 오류:', error);
throw error;
}
}
// ============================================
// 6. 에러 처리 예제
// ============================================
/**
* 에러 처리를 포함한 안전한 API 호출 예제
*/
export async function safeApiCall<T>(
apiCall: () => Promise<Response>
): Promise<{ data?: T; error?: string }> {
try {
const response = await apiCall();
if (!response.ok) {
const errorData = await response.json();
return { error: errorData.error || '알 수 없는 오류가 발생했습니다.' };
}
const data = await response.json();
return { data: data as T };
} catch (error) {
return {
error: error instanceof Error ? error.message : '네트워크 오류가 발생했습니다.',
};
}
}
// 사용 예시:
// const result = await safeApiCall(() =>
// fetch('/api/users', { method: 'POST', ... })
// );
// if (result.error) {
// console.error(result.error);
// } else {
// console.log(result.data);
// }

View File

@@ -0,0 +1,127 @@
import { NextRequest, NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';
// 강좌 생성
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const { courseId, lessonName, learningGoal, createdById } = body;
// 필수 필드 검증
if (!courseId || !lessonName) {
return NextResponse.json(
{ error: '교육과정 ID와 강좌명은 필수입니다.' },
{ status: 400 }
);
}
// 교육과정 존재 확인
const course = await prisma.course.findUnique({
where: { id: courseId },
});
if (!course) {
return NextResponse.json(
{ error: '교육과정을 찾을 수 없습니다.' },
{ status: 404 }
);
}
// 강좌 생성
const lesson = await prisma.lesson.create({
data: {
courseId,
lessonName,
learningGoal: learningGoal || null,
createdById: createdById || course.createdById,
},
include: {
course: {
select: {
id: true,
courseName: true,
},
},
createdBy: {
select: {
id: true,
name: true,
},
},
},
});
return NextResponse.json(
{ message: '강좌가 성공적으로 생성되었습니다.', lesson },
{ status: 201 }
);
} catch (error) {
console.error('강좌 생성 오류:', error);
return NextResponse.json(
{ error: '강좌 생성 중 오류가 발생했습니다.' },
{ status: 500 }
);
}
}
// 강좌 목록 조회
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url);
const courseId = searchParams.get('courseId');
const page = parseInt(searchParams.get('page') || '1');
const limit = parseInt(searchParams.get('limit') || '10');
const skip = (page - 1) * limit;
const where: any = {};
if (courseId) where.courseId = courseId;
const [lessons, total] = await Promise.all([
prisma.lesson.findMany({
where,
skip,
take: limit,
orderBy: { createdAt: 'desc' },
include: {
course: {
select: {
id: true,
courseName: true,
},
},
createdBy: {
select: {
id: true,
name: true,
},
},
_count: {
select: {
videos: true,
vrContents: true,
questions: true,
},
},
},
}),
prisma.lesson.count({ where }),
]);
return NextResponse.json({
lessons,
pagination: {
page,
limit,
total,
totalPages: Math.ceil(total / limit),
},
});
} catch (error) {
console.error('강좌 조회 오류:', error);
return NextResponse.json(
{ error: '강좌 조회 중 오류가 발생했습니다.' },
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,109 @@
import { NextRequest, NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';
// 공지사항 생성
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const { title, content, writerId, hasAttachment } = body;
// 필수 필드 검증
if (!title || !content || !writerId) {
return NextResponse.json(
{ error: '제목, 내용, 작성자 ID는 필수입니다.' },
{ status: 400 }
);
}
// 작성자 존재 확인
const writer = await prisma.user.findUnique({
where: { id: writerId },
});
if (!writer) {
return NextResponse.json(
{ error: '작성자를 찾을 수 없습니다.' },
{ status: 404 }
);
}
// 공지사항 생성
const notice = await prisma.notice.create({
data: {
title,
content,
writerId,
hasAttachment: hasAttachment || false,
},
include: {
writer: {
select: {
id: true,
name: true,
email: true,
},
},
},
});
return NextResponse.json(
{ message: '공지사항이 성공적으로 생성되었습니다.', notice },
{ status: 201 }
);
} catch (error) {
console.error('공지사항 생성 오류:', error);
return NextResponse.json(
{ error: '공지사항 생성 중 오류가 발생했습니다.' },
{ status: 500 }
);
}
}
// 공지사항 목록 조회
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url);
const writerId = searchParams.get('writerId');
const page = parseInt(searchParams.get('page') || '1');
const limit = parseInt(searchParams.get('limit') || '10');
const skip = (page - 1) * limit;
const where: any = {};
if (writerId) where.writerId = writerId;
const [notices, total] = await Promise.all([
prisma.notice.findMany({
where,
skip,
take: limit,
orderBy: { date: 'desc' },
include: {
writer: {
select: {
id: true,
name: true,
},
},
},
}),
prisma.notice.count({ where }),
]);
return NextResponse.json({
notices,
pagination: {
page,
limit,
total,
totalPages: Math.ceil(total / limit),
},
});
} catch (error) {
console.error('공지사항 조회 오류:', error);
return NextResponse.json(
{ error: '공지사항 조회 중 오류가 발생했습니다.' },
{ status: 500 }
);
}
}

115
src/app/api/users/route.ts Normal file
View File

@@ -0,0 +1,115 @@
import { NextRequest, NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';
import { UserRole, UserStatus } from '@prisma/client';
// 사용자 생성
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const { email, password, name, phone, gender, birthdate, role, status } = body;
// 필수 필드 검증
if (!email || !password || !name) {
return NextResponse.json(
{ error: '이메일, 비밀번호, 이름은 필수입니다.' },
{ status: 400 }
);
}
// 이메일 중복 확인
const existingUser = await prisma.user.findUnique({
where: { email },
});
if (existingUser) {
return NextResponse.json(
{ error: '이미 존재하는 이메일입니다.' },
{ status: 409 }
);
}
// 사용자 생성
const user = await prisma.user.create({
data: {
email,
password, // 실제로는 해시화된 비밀번호를 저장해야 합니다
name,
phone: phone || null,
gender: gender || null,
birthdate: birthdate ? new Date(birthdate) : null,
role: role || UserRole.LEARNER,
status: status || UserStatus.ACTIVE,
},
});
// 비밀번호 제외하고 반환
const { password: _, ...userWithoutPassword } = user;
return NextResponse.json(
{ message: '사용자가 성공적으로 생성되었습니다.', user: userWithoutPassword },
{ status: 201 }
);
} catch (error) {
console.error('사용자 생성 오류:', error);
return NextResponse.json(
{ error: '사용자 생성 중 오류가 발생했습니다.' },
{ status: 500 }
);
}
}
// 사용자 목록 조회
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url);
const role = searchParams.get('role');
const status = searchParams.get('status');
const page = parseInt(searchParams.get('page') || '1');
const limit = parseInt(searchParams.get('limit') || '10');
const skip = (page - 1) * limit;
const where: any = {};
if (role) where.role = role;
if (status) where.status = status;
const [users, total] = await Promise.all([
prisma.user.findMany({
where,
skip,
take: limit,
orderBy: { createdAt: 'desc' },
select: {
id: true,
email: true,
name: true,
phone: true,
gender: true,
birthdate: true,
role: true,
status: true,
joinDate: true,
createdAt: true,
updatedAt: true,
},
}),
prisma.user.count({ where }),
]);
return NextResponse.json({
users,
pagination: {
page,
limit,
total,
totalPages: Math.ceil(total / limit),
},
});
} catch (error) {
console.error('사용자 조회 오류:', error);
return NextResponse.json(
{ error: '사용자 조회 중 오류가 발생했습니다.' },
{ status: 500 }
);
}
}

14
src/lib/prisma.ts Normal file
View File

@@ -0,0 +1,14 @@
import { PrismaClient } from '@prisma/client';
const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined;
};
export const prisma =
globalForPrisma.prisma ??
new PrismaClient({
log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'],
});
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma;