diff --git a/.gitignore b/.gitignore
index 5ef6a52..f390d12 100644
--- a/.gitignore
+++ b/.gitignore
@@ -39,3 +39,5 @@ yarn-error.log*
# typescript
*.tsbuildinfo
next-env.d.ts
+
+/src/generated/prisma
diff --git a/API_USAGE.md b/API_USAGE.md
new file mode 100644
index 0000000..77a73a6
--- /dev/null
+++ b/API_USAGE.md
@@ -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 (
+
+ );
+}
+```
+
+### 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 (
+
+
사용자 목록
+
+ {users.map((user) => (
+ -
+ {user.name} ({user.email}) - {user.role}
+
+ ))}
+
+
+ );
+}
+```
+
+### 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)
+
diff --git a/package-lock.json b/package-lock.json
index 91823a6..66c4564 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,7 +8,10 @@
"name": "xrlms",
"version": "0.1.0",
"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"
},
@@ -19,6 +22,7 @@
"@types/react": "^19",
"@types/react-dom": "^19",
"tailwindcss": "^4",
+ "tsx": "^4.20.6",
"typescript": "^5"
}
},
@@ -208,6 +212,448 @@
"tslib": "^2.4.0"
}
},
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
+ "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
+ "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
+ "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
+ "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
+ "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
+ "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
+ "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
+ "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
+ "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
+ "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
+ "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
+ "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
+ "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
+ "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
+ "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
+ "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
+ "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
+ "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz",
+ "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
+ "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
+ "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
+ "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
+ "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/@img/colour": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz",
@@ -858,6 +1304,91 @@
"node": ">= 10"
}
},
+ "node_modules/@prisma/client": {
+ "version": "6.19.0",
+ "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.19.0.tgz",
+ "integrity": "sha512-QXFT+N/bva/QI2qoXmjBzL7D6aliPffIwP+81AdTGq0FXDoLxLkWivGMawG8iM5B9BKfxLIXxfWWAF6wbuJU6g==",
+ "hasInstallScript": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18"
+ },
+ "peerDependencies": {
+ "prisma": "*",
+ "typescript": ">=5.1.0"
+ },
+ "peerDependenciesMeta": {
+ "prisma": {
+ "optional": true
+ },
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@prisma/config": {
+ "version": "6.19.0",
+ "resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.19.0.tgz",
+ "integrity": "sha512-zwCayme+NzI/WfrvFEtkFhhOaZb/hI+X8TTjzjJ252VbPxAl2hWHK5NMczmnG9sXck2lsXrxIZuK524E25UNmg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "c12": "3.1.0",
+ "deepmerge-ts": "7.1.5",
+ "effect": "3.18.4",
+ "empathic": "2.0.0"
+ }
+ },
+ "node_modules/@prisma/debug": {
+ "version": "6.19.0",
+ "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.19.0.tgz",
+ "integrity": "sha512-8hAdGG7JmxrzFcTzXZajlQCidX0XNkMJkpqtfbLV54wC6LSSX6Vni25W/G+nAANwLnZ2TmwkfIuWetA7jJxJFA==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/@prisma/engines": {
+ "version": "6.19.0",
+ "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.19.0.tgz",
+ "integrity": "sha512-pMRJ+1S6NVdXoB8QJAPIGpKZevFjxhKt0paCkRDTZiczKb7F4yTgRP8M4JdVkpQwmaD4EoJf6qA+p61godDokw==",
+ "hasInstallScript": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@prisma/debug": "6.19.0",
+ "@prisma/engines-version": "6.19.0-26.2ba551f319ab1df4bc874a89965d8b3641056773",
+ "@prisma/fetch-engine": "6.19.0",
+ "@prisma/get-platform": "6.19.0"
+ }
+ },
+ "node_modules/@prisma/engines-version": {
+ "version": "6.19.0-26.2ba551f319ab1df4bc874a89965d8b3641056773",
+ "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.19.0-26.2ba551f319ab1df4bc874a89965d8b3641056773.tgz",
+ "integrity": "sha512-gV7uOBQfAFlWDvPJdQxMT1aSRur3a0EkU/6cfbAC5isV67tKDWUrPauyaHNpB+wN1ebM4A9jn/f4gH+3iHSYSQ==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/@prisma/fetch-engine": {
+ "version": "6.19.0",
+ "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.19.0.tgz",
+ "integrity": "sha512-OOx2Lda0DGrZ1rodADT06ZGqHzr7HY7LNMaFE2Vp8dp146uJld58sRuasdX0OiwpHgl8SqDTUKHNUyzEq7pDdQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@prisma/debug": "6.19.0",
+ "@prisma/engines-version": "6.19.0-26.2ba551f319ab1df4bc874a89965d8b3641056773",
+ "@prisma/get-platform": "6.19.0"
+ }
+ },
+ "node_modules/@prisma/get-platform": {
+ "version": "6.19.0",
+ "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.19.0.tgz",
+ "integrity": "sha512-ym85WDO2yDhC3fIXHWYpG3kVMBA49cL1XD2GCsCF8xbwoy2OkDQY44gEbAt2X46IQ4Apq9H6g0Ex1iFfPqEkHA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@prisma/debug": "6.19.0"
+ }
+ },
+ "node_modules/@standard-schema/spec": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz",
+ "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==",
+ "license": "MIT"
+ },
"node_modules/@swc/helpers": {
"version": "0.5.15",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
@@ -1168,6 +1699,46 @@
"@types/react": "^19.2.0"
}
},
+ "node_modules/c12": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/c12/-/c12-3.1.0.tgz",
+ "integrity": "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw==",
+ "license": "MIT",
+ "dependencies": {
+ "chokidar": "^4.0.3",
+ "confbox": "^0.2.2",
+ "defu": "^6.1.4",
+ "dotenv": "^16.6.1",
+ "exsolve": "^1.0.7",
+ "giget": "^2.0.0",
+ "jiti": "^2.4.2",
+ "ohash": "^2.0.11",
+ "pathe": "^2.0.3",
+ "perfect-debounce": "^1.0.0",
+ "pkg-types": "^2.2.0",
+ "rc9": "^2.1.2"
+ },
+ "peerDependencies": {
+ "magicast": "^0.3.5"
+ },
+ "peerDependenciesMeta": {
+ "magicast": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/c12/node_modules/dotenv": {
+ "version": "16.6.1",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
+ "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://dotenvx.com"
+ }
+ },
"node_modules/caniuse-lite": {
"version": "1.0.30001754",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001754.tgz",
@@ -1188,12 +1759,51 @@
],
"license": "CC-BY-4.0"
},
+ "node_modules/chokidar": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
+ "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
+ "license": "MIT",
+ "dependencies": {
+ "readdirp": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 14.16.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
+ "node_modules/citty": {
+ "version": "0.1.6",
+ "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz",
+ "integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==",
+ "license": "MIT",
+ "dependencies": {
+ "consola": "^3.2.3"
+ }
+ },
"node_modules/client-only": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
"license": "MIT"
},
+ "node_modules/confbox": {
+ "version": "0.2.2",
+ "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz",
+ "integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==",
+ "license": "MIT"
+ },
+ "node_modules/consola": {
+ "version": "3.4.2",
+ "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz",
+ "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==",
+ "license": "MIT",
+ "engines": {
+ "node": "^14.18.0 || >=16.10.0"
+ }
+ },
"node_modules/csstype": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.1.tgz",
@@ -1201,6 +1811,27 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/deepmerge-ts": {
+ "version": "7.1.5",
+ "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz",
+ "integrity": "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "node_modules/defu": {
+ "version": "6.1.4",
+ "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz",
+ "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==",
+ "license": "MIT"
+ },
+ "node_modules/destr": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz",
+ "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==",
+ "license": "MIT"
+ },
"node_modules/detect-libc": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
@@ -1211,6 +1842,37 @@
"node": ">=8"
}
},
+ "node_modules/dotenv": {
+ "version": "17.2.3",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz",
+ "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://dotenvx.com"
+ }
+ },
+ "node_modules/effect": {
+ "version": "3.18.4",
+ "resolved": "https://registry.npmjs.org/effect/-/effect-3.18.4.tgz",
+ "integrity": "sha512-b1LXQJLe9D11wfnOKAk3PKxuqYshQ0Heez+y5pnkd3jLj1yx9QhM72zZ9uUrOQyNvrs2GZZd/3maL0ZV18YuDA==",
+ "license": "MIT",
+ "dependencies": {
+ "@standard-schema/spec": "^1.0.0",
+ "fast-check": "^3.23.1"
+ }
+ },
+ "node_modules/empathic": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.0.tgz",
+ "integrity": "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=14"
+ }
+ },
"node_modules/enhanced-resolve": {
"version": "5.18.3",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz",
@@ -1225,6 +1887,121 @@
"node": ">=10.13.0"
}
},
+ "node_modules/esbuild": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
+ "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.25.12",
+ "@esbuild/android-arm": "0.25.12",
+ "@esbuild/android-arm64": "0.25.12",
+ "@esbuild/android-x64": "0.25.12",
+ "@esbuild/darwin-arm64": "0.25.12",
+ "@esbuild/darwin-x64": "0.25.12",
+ "@esbuild/freebsd-arm64": "0.25.12",
+ "@esbuild/freebsd-x64": "0.25.12",
+ "@esbuild/linux-arm": "0.25.12",
+ "@esbuild/linux-arm64": "0.25.12",
+ "@esbuild/linux-ia32": "0.25.12",
+ "@esbuild/linux-loong64": "0.25.12",
+ "@esbuild/linux-mips64el": "0.25.12",
+ "@esbuild/linux-ppc64": "0.25.12",
+ "@esbuild/linux-riscv64": "0.25.12",
+ "@esbuild/linux-s390x": "0.25.12",
+ "@esbuild/linux-x64": "0.25.12",
+ "@esbuild/netbsd-arm64": "0.25.12",
+ "@esbuild/netbsd-x64": "0.25.12",
+ "@esbuild/openbsd-arm64": "0.25.12",
+ "@esbuild/openbsd-x64": "0.25.12",
+ "@esbuild/openharmony-arm64": "0.25.12",
+ "@esbuild/sunos-x64": "0.25.12",
+ "@esbuild/win32-arm64": "0.25.12",
+ "@esbuild/win32-ia32": "0.25.12",
+ "@esbuild/win32-x64": "0.25.12"
+ }
+ },
+ "node_modules/exsolve": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz",
+ "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==",
+ "license": "MIT"
+ },
+ "node_modules/fast-check": {
+ "version": "3.23.2",
+ "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz",
+ "integrity": "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/dubzzz"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fast-check"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "pure-rand": "^6.1.0"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/get-tsconfig": {
+ "version": "4.13.0",
+ "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz",
+ "integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "resolve-pkg-maps": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
+ }
+ },
+ "node_modules/giget": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/giget/-/giget-2.0.0.tgz",
+ "integrity": "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==",
+ "license": "MIT",
+ "dependencies": {
+ "citty": "^0.1.6",
+ "consola": "^3.4.0",
+ "defu": "^6.1.4",
+ "node-fetch-native": "^1.6.6",
+ "nypm": "^0.6.0",
+ "pathe": "^2.0.3"
+ },
+ "bin": {
+ "giget": "dist/cli.mjs"
+ }
+ },
"node_modules/graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
@@ -1236,7 +2013,6 @@
"version": "2.6.1",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
"integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==",
- "dev": true,
"license": "MIT",
"bin": {
"jiti": "lib/jiti-cli.mjs"
@@ -1611,12 +2387,66 @@
"node": "^10 || ^12 || >=14"
}
},
+ "node_modules/node-fetch-native": {
+ "version": "1.6.7",
+ "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz",
+ "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==",
+ "license": "MIT"
+ },
+ "node_modules/nypm": {
+ "version": "0.6.2",
+ "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.2.tgz",
+ "integrity": "sha512-7eM+hpOtrKrBDCh7Ypu2lJ9Z7PNZBdi/8AT3AX8xoCj43BBVHD0hPSTEvMtkMpfs8FCqBGhxB+uToIQimA111g==",
+ "license": "MIT",
+ "dependencies": {
+ "citty": "^0.1.6",
+ "consola": "^3.4.2",
+ "pathe": "^2.0.3",
+ "pkg-types": "^2.3.0",
+ "tinyexec": "^1.0.1"
+ },
+ "bin": {
+ "nypm": "dist/cli.mjs"
+ },
+ "engines": {
+ "node": "^14.16.0 || >=16.10.0"
+ }
+ },
+ "node_modules/ohash": {
+ "version": "2.0.11",
+ "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz",
+ "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==",
+ "license": "MIT"
+ },
+ "node_modules/pathe": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
+ "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
+ "license": "MIT"
+ },
+ "node_modules/perfect-debounce": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz",
+ "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==",
+ "license": "MIT"
+ },
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"license": "ISC"
},
+ "node_modules/pkg-types": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz",
+ "integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==",
+ "license": "MIT",
+ "dependencies": {
+ "confbox": "^0.2.2",
+ "exsolve": "^1.0.7",
+ "pathe": "^2.0.3"
+ }
+ },
"node_modules/postcss": {
"version": "8.5.6",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
@@ -1646,6 +2476,57 @@
"node": "^10 || ^12 || >=14"
}
},
+ "node_modules/prisma": {
+ "version": "6.19.0",
+ "resolved": "https://registry.npmjs.org/prisma/-/prisma-6.19.0.tgz",
+ "integrity": "sha512-F3eX7K+tWpkbhl3l4+VkFtrwJlLXbAM+f9jolgoUZbFcm1DgHZ4cq9AgVEgUym2au5Ad/TDLN8lg83D+M10ycw==",
+ "hasInstallScript": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@prisma/config": "6.19.0",
+ "@prisma/engines": "6.19.0"
+ },
+ "bin": {
+ "prisma": "build/index.js"
+ },
+ "engines": {
+ "node": ">=18.18"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.1.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/pure-rand": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz",
+ "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/dubzzz"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fast-check"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/rc9": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz",
+ "integrity": "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==",
+ "license": "MIT",
+ "dependencies": {
+ "defu": "^6.1.4",
+ "destr": "^2.0.3"
+ }
+ },
"node_modules/react": {
"version": "19.2.0",
"resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
@@ -1667,6 +2548,29 @@
"react": "^19.2.0"
}
},
+ "node_modules/readdirp": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
+ "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14.18.0"
+ },
+ "funding": {
+ "type": "individual",
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
+ "node_modules/resolve-pkg-maps": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
+ "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
+ }
+ },
"node_modules/scheduler": {
"version": "0.27.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
@@ -1784,17 +2688,46 @@
"url": "https://opencollective.com/webpack"
}
},
+ "node_modules/tinyexec": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz",
+ "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
+ "node_modules/tsx": {
+ "version": "4.20.6",
+ "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.6.tgz",
+ "integrity": "sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "~0.25.0",
+ "get-tsconfig": "^4.7.5"
+ },
+ "bin": {
+ "tsx": "dist/cli.mjs"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ }
+ },
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
- "dev": true,
+ "devOptional": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
diff --git a/package.json b/package.json
index 9dd1f54..93f8a75 100644
--- a/package.json
+++ b/package.json
@@ -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"
}
}
diff --git a/prisma.config.ts b/prisma.config.ts
new file mode 100644
index 0000000..410cb59
--- /dev/null
+++ b/prisma.config.ts
@@ -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"),
+ },
+});
diff --git a/prisma/README.md b/prisma/README.md
new file mode 100644
index 0000000..f7927c5
--- /dev/null
+++ b/prisma/README.md
@@ -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)
+
diff --git a/prisma/migrations/20251121074818_new/migration.sql b/prisma/migrations/20251121074818_new/migration.sql
new file mode 100644
index 0000000..e0ede73
--- /dev/null
+++ b/prisma/migrations/20251121074818_new/migration.sql
@@ -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;
diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml
new file mode 100644
index 0000000..044d57c
--- /dev/null
+++ b/prisma/migrations/migration_lock.toml
@@ -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"
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
new file mode 100644
index 0000000..2728af9
--- /dev/null
+++ b/prisma/schema.prisma
@@ -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")
+}
diff --git a/prisma/seed.ts b/prisma/seed.ts
new file mode 100644
index 0000000..68c8a26
--- /dev/null
+++ b/prisma/seed.ts
@@ -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();
+
+ 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();
+
+ 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();
+ });
+
diff --git a/src/app/api/README.md b/src/app/api/README.md
new file mode 100644
index 0000000..9fba067
--- /dev/null
+++ b/src/app/api/README.md
@@ -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 추가
+
diff --git a/src/app/api/courses/route.ts b/src/app/api/courses/route.ts
new file mode 100644
index 0000000..b2407ce
--- /dev/null
+++ b/src/app/api/courses/route.ts
@@ -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 }
+ );
+ }
+}
+
diff --git a/src/app/api/examples.ts b/src/app/api/examples.ts
new file mode 100644
index 0000000..0ecbfe7
--- /dev/null
+++ b/src/app/api/examples.ts
@@ -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(
+ apiCall: () => Promise
+): 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);
+// }
+
diff --git a/src/app/api/lessons/route.ts b/src/app/api/lessons/route.ts
new file mode 100644
index 0000000..8bf4010
--- /dev/null
+++ b/src/app/api/lessons/route.ts
@@ -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 }
+ );
+ }
+}
+
diff --git a/src/app/api/notices/route.ts b/src/app/api/notices/route.ts
new file mode 100644
index 0000000..544aa02
--- /dev/null
+++ b/src/app/api/notices/route.ts
@@ -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 }
+ );
+ }
+}
+
diff --git a/src/app/api/users/route.ts b/src/app/api/users/route.ts
new file mode 100644
index 0000000..91bbecb
--- /dev/null
+++ b/src/app/api/users/route.ts
@@ -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 }
+ );
+ }
+}
+
diff --git a/src/lib/prisma.ts b/src/lib/prisma.ts
new file mode 100644
index 0000000..0eebf59
--- /dev/null
+++ b/src/lib/prisma.ts
@@ -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;
+