From eaec13e3863304267e44faaa08a665a2d6b32386 Mon Sep 17 00:00:00 2001 From: wallace Date: Mon, 24 Nov 2025 22:50:28 +0900 Subject: [PATCH] =?UTF-8?q?=EB=82=B4=EB=B6=80=20api=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- API_USAGE.md | 565 ------------------ .../20251121074818_new/migration.sql | 311 ---------- prisma/migrations/migration_lock.toml | 3 - src/app/api/README.md | 211 ------- src/app/api/courses/route.ts | 126 ---- src/app/api/examples.ts | 272 --------- src/app/api/lessons/route.ts | 127 ---- src/app/api/notices/route.ts | 109 ---- src/app/api/users/route.ts | 115 ---- src/app/find-id/page.tsx | 32 +- src/app/globals.css | 8 + src/app/login/page.tsx | 68 ++- src/app/register/RegisterForm.tsx | 36 +- src/lib/email-templates.ts | 222 +++++++ 14 files changed, 342 insertions(+), 1863 deletions(-) delete mode 100644 API_USAGE.md delete mode 100644 prisma/migrations/20251121074818_new/migration.sql delete mode 100644 prisma/migrations/migration_lock.toml delete mode 100644 src/app/api/README.md delete mode 100644 src/app/api/courses/route.ts delete mode 100644 src/app/api/examples.ts delete mode 100644 src/app/api/lessons/route.ts delete mode 100644 src/app/api/notices/route.ts delete mode 100644 src/app/api/users/route.ts create mode 100644 src/lib/email-templates.ts diff --git a/API_USAGE.md b/API_USAGE.md deleted file mode 100644 index 77a73a6..0000000 --- a/API_USAGE.md +++ /dev/null @@ -1,565 +0,0 @@ -# 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 ( -
- setFormData({ ...formData, email: e.target.value })} - required - /> - setFormData({ ...formData, password: e.target.value })} - required - /> - setFormData({ ...formData, name: e.target.value })} - required - /> - - - {message &&

{message}

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

사용자 목록

- -
- ); -} -``` - -### 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/prisma/migrations/20251121074818_new/migration.sql b/prisma/migrations/20251121074818_new/migration.sql deleted file mode 100644 index e0ede73..0000000 --- a/prisma/migrations/20251121074818_new/migration.sql +++ /dev/null @@ -1,311 +0,0 @@ --- 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 deleted file mode 100644 index 044d57c..0000000 --- a/prisma/migrations/migration_lock.toml +++ /dev/null @@ -1,3 +0,0 @@ -# Please do not edit this file manually -# It should be added in your version-control system (e.g., Git) -provider = "postgresql" diff --git a/src/app/api/README.md b/src/app/api/README.md deleted file mode 100644 index 9fba067..0000000 --- a/src/app/api/README.md +++ /dev/null @@ -1,211 +0,0 @@ -# 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 deleted file mode 100644 index b2407ce..0000000 --- a/src/app/api/courses/route.ts +++ /dev/null @@ -1,126 +0,0 @@ -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 deleted file mode 100644 index 0ecbfe7..0000000 --- a/src/app/api/examples.ts +++ /dev/null @@ -1,272 +0,0 @@ -/** - * 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 deleted file mode 100644 index 8bf4010..0000000 --- a/src/app/api/lessons/route.ts +++ /dev/null @@ -1,127 +0,0 @@ -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 deleted file mode 100644 index 544aa02..0000000 --- a/src/app/api/notices/route.ts +++ /dev/null @@ -1,109 +0,0 @@ -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 deleted file mode 100644 index 91bbecb..0000000 --- a/src/app/api/users/route.ts +++ /dev/null @@ -1,115 +0,0 @@ -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/app/find-id/page.tsx b/src/app/find-id/page.tsx index 25c81c1..e5a224b 100644 --- a/src/app/find-id/page.tsx +++ b/src/app/find-id/page.tsx @@ -27,12 +27,36 @@ export default function FindIdPage() { return Object.keys(next).length === 0; } - function handleSubmit(e: React.FormEvent) { + async function handleSubmit(e: React.FormEvent) { e.preventDefault(); if (!validateAll()) return; - const mockUserId = `${name.trim()}@example.com`; - setFoundUserId(mockUserId); - setIsDoneOpen(true); + + try { + const response = await fetch('https://hrdi.coconutmeet.net/auth/find-id', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + name: name, + phone: phone, + }), + }); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + console.error('아이디 찾기 실패:', errorData.error || response.statusText); + setIsFailedOpen(true); + return; + } + + const data = await response.json(); + setFoundUserId(data.email); + setIsDoneOpen(true); + } catch (error) { + console.error('아이디 찾기 오류:', error); + setIsFailedOpen(true); + } } return ( diff --git a/src/app/globals.css b/src/app/globals.css index 8541e68..f2cdd58 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -41,3 +41,11 @@ body { background: var(--background); color: var(--foreground); } + +button { + cursor: pointer; +} + +button:hover { + cursor: pointer; +} diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx index 1c4c529..7ae5cab 100644 --- a/src/app/login/page.tsx +++ b/src/app/login/page.tsx @@ -20,13 +20,57 @@ export default function LoginPage() { const [idError, setIdError] = useState(""); const [passwordError, setPasswordError] = useState(""); - function handleSubmit(e: React.FormEvent) { + async function handleSubmit(e: React.FormEvent) { e.preventDefault(); - // 실제 로그인 API 연동 전까지는 실패 모달을 노출합니다. - // API 연동 시 결과에 따라 성공/실패 분기에서 setIsLoginErrorOpen(true) 호출로 교체하세요. - // if (userId.trim().length > 0 && password.trim().length > 0) { - // setIsLoginErrorOpen(true); - // } + if (userId.trim().length === 0 || password.trim().length === 0) { + return; + } + + try { + const response = await fetch("https://hrdi.coconutmeet.net/auth/login", { + method: "POST", + headers: {"Content-Type": "application/json",}, + body: JSON.stringify({ + email: userId, + password: password + }) + }); + + if (!response.ok) { + let errorMessage = `로그인 실패 (${response.status})`; + try { + const errorData = await response.json(); + if (errorData.error) { + errorMessage = errorData.error; + } else if (errorData.message) { + errorMessage = errorData.message; + } else if (response.statusText) { + errorMessage = `${response.statusText} (${response.status})`; + } + } catch (parseError) { + if (response.statusText) { + errorMessage = `${response.statusText} (${response.status})`; + } + } + console.error("로그인 실패:", errorMessage); + setIsLoginErrorOpen(true); + return; + } + + const data = await response.json(); + console.log("로그인 성공:", data); + + // 로그인 성공 시 처리 (예: 토큰 저장, 리다이렉트 등) + // TODO: 성공 시 처리 로직 추가 (예: localStorage에 토큰 저장, 메인 페이지로 이동 등) + // if (data.token) { + // localStorage.setItem('token', data.token); + // window.location.href = '/menu'; + // } + } catch (error) { + const errorMessage = error instanceof Error ? error.message : "네트워크 오류가 발생했습니다."; + console.error("로그인 오류:", errorMessage); + setIsLoginErrorOpen(true); + } } return ( @@ -106,12 +150,12 @@ export default function LoginPage() { onBlur={() => setIsPasswordFocused(false)} placeholder="비밀번호" className=" - h-[40px] px-[12px] py-[7px] rounded-[8px] w-full border border-neutral-40 - focus:outline-none focus:ring-0 focus:ring-offset-0 focus:shadow-none - focus:appearance-none focus:border-neutral-700 - text-[18px] text-neutral-700 font-normal leading-[150%] placeholder:text-input-placeholder-text - pr-[40px] - " + h-[40px] px-[12px] py-[7px] rounded-[8px] w-full border border-neutral-40 + focus:outline-none focus:ring-0 focus:ring-offset-0 focus:shadow-none + focus:appearance-none focus:border-neutral-700 + text-[18px] text-neutral-700 font-normal leading-[150%] placeholder:text-input-placeholder-text + pr-[40px] + " /> {password.trim().length > 0 && isPasswordFocused && ( {errors.email &&

{errors.email}

} @@ -337,7 +357,7 @@ export default function RegisterForm({ onOpenDone, onOpenCodeError }: RegisterFo