diff --git a/.gitignore b/.gitignore index 5ef6a52..62fcee8 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,5 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts + +/lib/generated/prisma diff --git a/app/api/curriculums/route.ts b/app/api/curriculums/route.ts new file mode 100644 index 0000000..dd6f012 --- /dev/null +++ b/app/api/curriculums/route.ts @@ -0,0 +1,161 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; + +// GET - 교육 과정 목록 조회 또는 단일 교육 과정 조회 +export async function GET(request: NextRequest) { + try { + const { searchParams } = new URL(request.url); + const id = searchParams.get('id'); + const instructorId = searchParams.get('instructorId'); + + if (id) { + // 단일 교육 과정 조회 + const curriculum = await prisma.curriculum.findUnique({ + where: { id }, + include: { + lectures: { + include: { + registrant: { + select: { + id: true, + name: true, + email: true, + }, + }, + }, + }, + }, + }); + + if (!curriculum) { + return NextResponse.json({ error: '교육 과정을 찾을 수 없습니다.' }, { status: 404 }); + } + + return NextResponse.json(curriculum); + } + + // 전체 교육 과정 목록 조회 + let where: any = {}; + if (instructorId) { + where.instructorId = instructorId; + } + + const curriculums = await prisma.curriculum.findMany({ + where, + include: { + lectures: { + select: { + id: true, + title: true, + }, + }, + }, + orderBy: { + createdAt: 'desc', + }, + }); + + return NextResponse.json(curriculums); + } catch (error) { + console.error('Error fetching curriculums:', error); + return NextResponse.json({ error: '교육 과정 조회 중 오류가 발생했습니다.' }, { status: 500 }); + } +} + +// POST - 교육 과정 생성 +export async function POST(request: NextRequest) { + try { + const body = await request.json(); + const { title, instructorId } = body; + + if (!title || !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 curriculum = await prisma.curriculum.create({ + data: { + title, + instructorId, + }, + include: { + lectures: true, + }, + }); + + return NextResponse.json(curriculum, { status: 201 }); + } catch (error) { + console.error('Error creating curriculum:', error); + return NextResponse.json({ error: '교육 과정 생성 중 오류가 발생했습니다.' }, { status: 500 }); + } +} + +// PUT - 교육 과정 수정 +export async function PUT(request: NextRequest) { + try { + const body = await request.json(); + const { id, title, instructorId } = body; + + if (!id) { + return NextResponse.json({ error: '교육 과정 ID는 필수입니다.' }, { status: 400 }); + } + + const updateData: any = {}; + if (title !== undefined) updateData.title = title; + if (instructorId !== undefined) { + // 강사 존재 확인 + const instructor = await prisma.user.findUnique({ + where: { id: instructorId }, + }); + + if (!instructor) { + return NextResponse.json({ error: '강사를 찾을 수 없습니다.' }, { status: 404 }); + } + + updateData.instructorId = instructorId; + } + + const curriculum = await prisma.curriculum.update({ + where: { id }, + data: updateData, + include: { + lectures: true, + }, + }); + + return NextResponse.json(curriculum); + } catch (error) { + console.error('Error updating curriculum:', error); + return NextResponse.json({ error: '교육 과정 수정 중 오류가 발생했습니다.' }, { status: 500 }); + } +} + +// DELETE - 교육 과정 삭제 +export async function DELETE(request: NextRequest) { + try { + const { searchParams } = new URL(request.url); + const id = searchParams.get('id'); + + if (!id) { + return NextResponse.json({ error: '교육 과정 ID는 필수입니다.' }, { status: 400 }); + } + + await prisma.curriculum.delete({ + where: { id }, + }); + + return NextResponse.json({ message: '교육 과정이 삭제되었습니다.' }); + } catch (error) { + console.error('Error deleting curriculum:', error); + return NextResponse.json({ error: '교육 과정 삭제 중 오류가 발생했습니다.' }, { status: 500 }); + } +} + diff --git a/app/api/lectures/route.ts b/app/api/lectures/route.ts new file mode 100644 index 0000000..d0b3762 --- /dev/null +++ b/app/api/lectures/route.ts @@ -0,0 +1,220 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; + +// GET - 강좌 목록 조회 또는 단일 강좌 조회 +export async function GET(request: NextRequest) { + try { + const { searchParams } = new URL(request.url); + const id = searchParams.get('id'); + const curriculumId = searchParams.get('curriculumId'); + const registrantId = searchParams.get('registrantId'); + + if (id) { + // 단일 강좌 조회 + const lecture = await prisma.lecture.findUnique({ + where: { id }, + include: { + curriculum: true, + registrant: { + select: { + id: true, + name: true, + email: true, + }, + }, + enrolledUsers: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + }, + }, + }, + }, + }, + }); + + if (!lecture) { + return NextResponse.json({ error: '강좌를 찾을 수 없습니다.' }, { status: 404 }); + } + + return NextResponse.json(lecture); + } + + // 전체 강좌 목록 조회 + let where: any = {}; + if (curriculumId) { + where.curriculumId = curriculumId; + } + if (registrantId) { + where.registrantId = registrantId; + } + + const lectures = await prisma.lecture.findMany({ + where, + include: { + curriculum: { + select: { + id: true, + title: true, + }, + }, + registrant: { + select: { + id: true, + name: true, + email: true, + }, + }, + }, + orderBy: { + createdAt: 'desc', + }, + }); + + return NextResponse.json(lectures); + } catch (error) { + console.error('Error fetching lectures:', error); + return NextResponse.json({ error: '강좌 조회 중 오류가 발생했습니다.' }, { status: 500 }); + } +} + +// POST - 강좌 생성 +export async function POST(request: NextRequest) { + try { + const body = await request.json(); + const { title, attachmentFile, evaluationQuestionCount, curriculumId, registrantId } = body; + + if (!title || !curriculumId || !registrantId) { + return NextResponse.json({ error: '강좌명, 교육 과정 ID, 등록자 ID는 필수입니다.' }, { status: 400 }); + } + + // 교육 과정 존재 확인 + const curriculum = await prisma.curriculum.findUnique({ + where: { id: curriculumId }, + }); + + if (!curriculum) { + return NextResponse.json({ error: '교육 과정을 찾을 수 없습니다.' }, { status: 404 }); + } + + // 등록자 존재 확인 + const registrant = await prisma.user.findUnique({ + where: { id: registrantId }, + }); + + if (!registrant) { + return NextResponse.json({ error: '등록자를 찾을 수 없습니다.' }, { status: 404 }); + } + + const lecture = await prisma.lecture.create({ + data: { + title, + attachmentFile: attachmentFile || null, + evaluationQuestionCount: evaluationQuestionCount || 0, + curriculumId, + registrantId, + }, + include: { + curriculum: true, + registrant: { + select: { + id: true, + name: true, + email: true, + }, + }, + }, + }); + + return NextResponse.json(lecture, { status: 201 }); + } catch (error) { + console.error('Error creating lecture:', error); + return NextResponse.json({ error: '강좌 생성 중 오류가 발생했습니다.' }, { status: 500 }); + } +} + +// PUT - 강좌 수정 +export async function PUT(request: NextRequest) { + try { + const body = await request.json(); + const { id, title, attachmentFile, evaluationQuestionCount, curriculumId, registrantId } = body; + + if (!id) { + return NextResponse.json({ error: '강좌 ID는 필수입니다.' }, { status: 400 }); + } + + const updateData: any = {}; + if (title !== undefined) updateData.title = title; + if (attachmentFile !== undefined) updateData.attachmentFile = attachmentFile; + if (evaluationQuestionCount !== undefined) updateData.evaluationQuestionCount = parseInt(evaluationQuestionCount); + if (curriculumId !== undefined) { + // 교육 과정 존재 확인 + const curriculum = await prisma.curriculum.findUnique({ + where: { id: curriculumId }, + }); + + if (!curriculum) { + return NextResponse.json({ error: '교육 과정을 찾을 수 없습니다.' }, { status: 404 }); + } + + updateData.curriculumId = curriculumId; + } + if (registrantId !== undefined) { + // 등록자 존재 확인 + const registrant = await prisma.user.findUnique({ + where: { id: registrantId }, + }); + + if (!registrant) { + return NextResponse.json({ error: '등록자를 찾을 수 없습니다.' }, { status: 404 }); + } + + updateData.registrantId = registrantId; + } + + const lecture = await prisma.lecture.update({ + where: { id }, + data: updateData, + include: { + curriculum: true, + registrant: { + select: { + id: true, + name: true, + email: true, + }, + }, + }, + }); + + return NextResponse.json(lecture); + } catch (error) { + console.error('Error updating lecture:', error); + return NextResponse.json({ error: '강좌 수정 중 오류가 발생했습니다.' }, { status: 500 }); + } +} + +// DELETE - 강좌 삭제 +export async function DELETE(request: NextRequest) { + try { + const { searchParams } = new URL(request.url); + const id = searchParams.get('id'); + + if (!id) { + return NextResponse.json({ error: '강좌 ID는 필수입니다.' }, { status: 400 }); + } + + await prisma.lecture.delete({ + where: { id }, + }); + + return NextResponse.json({ message: '강좌가 삭제되었습니다.' }); + } catch (error) { + console.error('Error deleting lecture:', error); + return NextResponse.json({ error: '강좌 삭제 중 오류가 발생했습니다.' }, { status: 500 }); + } +} + diff --git a/app/api/user-lectures/route.ts b/app/api/user-lectures/route.ts new file mode 100644 index 0000000..c89f160 --- /dev/null +++ b/app/api/user-lectures/route.ts @@ -0,0 +1,234 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; + +// GET - 수강 목록 조회 또는 단일 수강 조회 +export async function GET(request: NextRequest) { + try { + const { searchParams } = new URL(request.url); + const id = searchParams.get('id'); + const userId = searchParams.get('userId'); + const lectureId = searchParams.get('lectureId'); + + if (id) { + // 단일 수강 조회 + const userLecture = await prisma.userLecture.findUnique({ + where: { id }, + include: { + user: { + select: { + id: true, + name: true, + email: true, + }, + }, + lecture: { + include: { + curriculum: true, + }, + }, + }, + }); + + if (!userLecture) { + return NextResponse.json({ error: '수강 정보를 찾을 수 없습니다.' }, { status: 404 }); + } + + return NextResponse.json(userLecture); + } + + // 수강 목록 조회 + let where: any = {}; + if (userId) { + where.userId = userId; + } + if (lectureId) { + where.lectureId = lectureId; + } + + const userLectures = await prisma.userLecture.findMany({ + where, + include: { + user: { + select: { + id: true, + name: true, + email: true, + }, + }, + lecture: { + include: { + curriculum: { + select: { + id: true, + title: true, + }, + }, + }, + }, + }, + orderBy: { + enrolledAt: 'desc', + }, + }); + + return NextResponse.json(userLectures); + } catch (error) { + console.error('Error fetching user lectures:', error); + return NextResponse.json({ error: '수강 정보 조회 중 오류가 발생했습니다.' }, { status: 500 }); + } +} + +// POST - 수강 등록 +export async function POST(request: NextRequest) { + try { + const body = await request.json(); + const { userId, lectureId } = body; + + if (!userId || !lectureId) { + return NextResponse.json({ error: '사용자 ID와 강좌 ID는 필수입니다.' }, { status: 400 }); + } + + // 사용자 존재 확인 + const user = await prisma.user.findUnique({ + where: { id: userId }, + }); + + if (!user) { + return NextResponse.json({ error: '사용자를 찾을 수 없습니다.' }, { status: 404 }); + } + + // 강좌 존재 확인 + const lecture = await prisma.lecture.findUnique({ + where: { id: lectureId }, + }); + + if (!lecture) { + return NextResponse.json({ error: '강좌를 찾을 수 없습니다.' }, { status: 404 }); + } + + // 이미 수강 중인지 확인 + const existingEnrollment = await prisma.userLecture.findUnique({ + where: { + userId_lectureId: { + userId, + lectureId, + }, + }, + }); + + if (existingEnrollment) { + return NextResponse.json({ error: '이미 수강 중인 강좌입니다.' }, { status: 409 }); + } + + // 수강 등록 + const userLecture = await prisma.userLecture.create({ + data: { + userId, + lectureId, + progress: 0, + isCompleted: false, + }, + include: { + user: { + select: { + id: true, + name: true, + email: true, + }, + }, + lecture: { + include: { + curriculum: true, + }, + }, + }, + }); + + return NextResponse.json(userLecture, { status: 201 }); + } catch (error) { + console.error('Error enrolling lecture:', error); + return NextResponse.json({ error: '수강 등록 중 오류가 발생했습니다.' }, { status: 500 }); + } +} + +// PUT - 수강 정보 수정 (진행률, 완료 여부 등) +export async function PUT(request: NextRequest) { + try { + const body = await request.json(); + const { id, progress, isCompleted, score } = body; + + if (!id) { + return NextResponse.json({ error: '수강 ID는 필수입니다.' }, { status: 400 }); + } + + const updateData: any = {}; + if (progress !== undefined) { + const progressValue = parseInt(progress); + if (progressValue < 0 || progressValue > 100) { + return NextResponse.json({ error: '진행률은 0-100 사이여야 합니다.' }, { status: 400 }); + } + updateData.progress = progressValue; + } + if (isCompleted !== undefined) { + updateData.isCompleted = isCompleted; + if (isCompleted && !updateData.completedAt) { + updateData.completedAt = new Date(); + } else if (!isCompleted) { + updateData.completedAt = null; + } + } + if (score !== undefined) { + const scoreValue = parseInt(score); + if (scoreValue < 0 || scoreValue > 100) { + return NextResponse.json({ error: '점수는 0-100 사이여야 합니다.' }, { status: 400 }); + } + updateData.score = scoreValue; + } + + const userLecture = await prisma.userLecture.update({ + where: { id }, + data: updateData, + include: { + user: { + select: { + id: true, + name: true, + email: true, + }, + }, + lecture: { + include: { + curriculum: true, + }, + }, + }, + }); + + return NextResponse.json(userLecture); + } catch (error) { + console.error('Error updating user lecture:', error); + return NextResponse.json({ error: '수강 정보 수정 중 오류가 발생했습니다.' }, { status: 500 }); + } +} + +// DELETE - 수강 취소 +export async function DELETE(request: NextRequest) { + try { + const { searchParams } = new URL(request.url); + const id = searchParams.get('id'); + + if (!id) { + return NextResponse.json({ error: '수강 ID는 필수입니다.' }, { status: 400 }); + } + + await prisma.userLecture.delete({ + where: { id }, + }); + + return NextResponse.json({ message: '수강이 취소되었습니다.' }); + } catch (error) { + console.error('Error deleting user lecture:', error); + return NextResponse.json({ error: '수강 취소 중 오류가 발생했습니다.' }, { status: 500 }); + } +} + diff --git a/app/api/users/login/route.ts b/app/api/users/login/route.ts new file mode 100644 index 0000000..39f4a4c --- /dev/null +++ b/app/api/users/login/route.ts @@ -0,0 +1,48 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; +import bcrypt from 'bcryptjs'; + +// POST - 로그인 +export async function POST(request: NextRequest) { + try { + const body = await request.json(); + const { email, password } = body; + + if (!email || !password) { + return NextResponse.json({ error: '이메일과 비밀번호를 입력해주세요.' }, { status: 400 }); + } + + // 사용자 조회 + const user = await prisma.user.findUnique({ + where: { email }, + }); + + if (!user) { + return NextResponse.json({ error: '아이디 또는 비밀번호가 올바르지 않습니다.' }, { status: 401 }); + } + + // 계정 활성화 확인 + if (!user.isActive) { + return NextResponse.json({ error: '비활성화된 계정입니다.' }, { status: 403 }); + } + + // 비밀번호 확인 + const isPasswordValid = await bcrypt.compare(password, user.password); + + if (!isPasswordValid) { + return NextResponse.json({ error: '아이디 또는 비밀번호가 올바르지 않습니다.' }, { status: 401 }); + } + + // 비밀번호 제외하고 사용자 정보 반환 + const { password: _, ...userWithoutPassword } = user; + + return NextResponse.json({ + user: userWithoutPassword, + message: '로그인 성공', + }); + } catch (error) { + console.error('Error during login:', error); + return NextResponse.json({ error: '로그인 중 오류가 발생했습니다.' }, { status: 500 }); + } +} + diff --git a/app/api/users/route.ts b/app/api/users/route.ts new file mode 100644 index 0000000..8dfe143 --- /dev/null +++ b/app/api/users/route.ts @@ -0,0 +1,187 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; +import bcrypt from 'bcryptjs'; + +// GET - 사용자 목록 조회 또는 단일 사용자 조회 +export async function GET(request: NextRequest) { + try { + const { searchParams } = new URL(request.url); + const id = searchParams.get('id'); + const email = searchParams.get('email'); + + if (id) { + // 단일 사용자 조회 + const user = await prisma.user.findUnique({ + where: { id }, + include: { + enrolledLectures: { + include: { + lecture: true, + }, + }, + }, + }); + + if (!user) { + return NextResponse.json({ error: '사용자를 찾을 수 없습니다.' }, { status: 404 }); + } + + // 비밀번호 제외 + const { password, ...userWithoutPassword } = user; + return NextResponse.json(userWithoutPassword); + } + + if (email) { + // 이메일로 사용자 조회 + const user = await prisma.user.findUnique({ + where: { email }, + }); + + if (!user) { + return NextResponse.json({ error: '사용자를 찾을 수 없습니다.' }, { status: 404 }); + } + + const { password, ...userWithoutPassword } = user; + return NextResponse.json(userWithoutPassword); + } + + // 전체 사용자 목록 조회 + const users = await prisma.user.findMany({ + select: { + id: true, + email: true, + name: true, + phone: true, + gender: true, + role: true, + isActive: true, + createdAt: true, + updatedAt: true, + }, + }); + + return NextResponse.json(users); + } catch (error) { + console.error('Error fetching users:', error); + return NextResponse.json({ error: '사용자 조회 중 오류가 발생했습니다.' }, { status: 500 }); + } +} + +// POST - 사용자 생성 (회원가입) +export async function POST(request: NextRequest) { + try { + const body = await request.json(); + const { email, password, name, phone, gender, birthYear, birthMonth, birthDay, role } = body; + + // 필수 필드 검증 + if (!email || !password) { + return NextResponse.json({ error: '이메일과 비밀번호는 필수입니다.' }, { status: 400 }); + } + + // 이메일 중복 확인 + const existingUser = await prisma.user.findUnique({ + where: { email }, + }); + + if (existingUser) { + return NextResponse.json({ error: '이미 등록된 이메일입니다.' }, { status: 409 }); + } + + // 비밀번호 해시화 + const hashedPassword = await bcrypt.hash(password, 10); + + // 사용자 생성 + const user = await prisma.user.create({ + data: { + email, + password: hashedPassword, + name: name || null, + phone: phone || null, + gender: gender || null, + birthYear: birthYear ? parseInt(birthYear) : null, + birthMonth: birthMonth ? parseInt(birthMonth) : null, + birthDay: birthDay ? parseInt(birthDay) : null, + role: role || 'STUDENT', + }, + select: { + id: true, + email: true, + name: true, + phone: true, + gender: true, + role: true, + isActive: true, + createdAt: true, + }, + }); + + return NextResponse.json(user, { status: 201 }); + } catch (error) { + console.error('Error creating user:', error); + return NextResponse.json({ error: '사용자 생성 중 오류가 발생했습니다.' }, { status: 500 }); + } +} + +// PUT - 사용자 정보 수정 +export async function PUT(request: NextRequest) { + try { + const body = await request.json(); + const { id, name, phone, gender, birthYear, birthMonth, birthDay, role, isActive } = body; + + if (!id) { + return NextResponse.json({ error: '사용자 ID는 필수입니다.' }, { status: 400 }); + } + + const updateData: any = {}; + if (name !== undefined) updateData.name = name; + if (phone !== undefined) updateData.phone = phone; + if (gender !== undefined) updateData.gender = gender; + if (birthYear !== undefined) updateData.birthYear = birthYear ? parseInt(birthYear) : null; + if (birthMonth !== undefined) updateData.birthMonth = birthMonth ? parseInt(birthMonth) : null; + if (birthDay !== undefined) updateData.birthDay = birthDay ? parseInt(birthDay) : null; + if (role !== undefined) updateData.role = role; + if (isActive !== undefined) updateData.isActive = isActive; + + const user = await prisma.user.update({ + where: { id }, + data: updateData, + select: { + id: true, + email: true, + name: true, + phone: true, + gender: true, + role: true, + isActive: true, + updatedAt: true, + }, + }); + + return NextResponse.json(user); + } catch (error) { + console.error('Error updating user:', error); + return NextResponse.json({ error: '사용자 정보 수정 중 오류가 발생했습니다.' }, { status: 500 }); + } +} + +// DELETE - 사용자 삭제 +export async function DELETE(request: NextRequest) { + try { + const { searchParams } = new URL(request.url); + const id = searchParams.get('id'); + + if (!id) { + return NextResponse.json({ error: '사용자 ID는 필수입니다.' }, { status: 400 }); + } + + await prisma.user.delete({ + where: { id }, + }); + + return NextResponse.json({ message: '사용자가 삭제되었습니다.' }); + } catch (error) { + console.error('Error deleting user:', error); + return NextResponse.json({ error: '사용자 삭제 중 오류가 발생했습니다.' }, { status: 500 }); + } +} + diff --git a/lib/prisma.ts b/lib/prisma.ts new file mode 100644 index 0000000..eb5d383 --- /dev/null +++ b/lib/prisma.ts @@ -0,0 +1,14 @@ +import { PrismaClient } from './generated/prisma'; + +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; + diff --git a/package-lock.json b/package-lock.json index 856d4c8..999e1f9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,9 +8,14 @@ "name": "xr_lms", "version": "0.1.0", "dependencies": { + "@prisma/client": "^6.19.0", + "@types/bcryptjs": "^2.4.6", "@types/better-sqlite3": "^7.6.13", + "bcryptjs": "^3.0.3", "better-sqlite3": "^12.4.1", + "dotenv": "^17.2.3", "next": "16.0.1", + "prisma": "^6.19.0", "react": "19.2.0", "react-dom": "19.2.0" }, @@ -1190,6 +1195,85 @@ "node": ">=12.4.0" } }, + "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/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", @@ -1197,6 +1281,12 @@ "dev": true, "license": "MIT" }, + "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", @@ -1488,6 +1578,12 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/bcryptjs": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.6.tgz", + "integrity": "sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==", + "license": "MIT" + }, "node_modules/@types/better-sqlite3": { "version": "7.6.13", "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz", @@ -2440,6 +2536,15 @@ "baseline-browser-mapping": "dist/cli.js" } }, + "node_modules/bcryptjs": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz", + "integrity": "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==", + "license": "BSD-3-Clause", + "bin": { + "bcrypt": "bin/bcrypt" + } + }, "node_modules/better-sqlite3": { "version": "12.4.1", "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.4.1.tgz", @@ -2556,6 +2661,46 @@ "ieee754": "^1.1.13" } }, + "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/call-bind": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", @@ -2653,12 +2798,36 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "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/chownr": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", "license": "ISC" }, + "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", @@ -2692,6 +2861,21 @@ "dev": true, "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/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -2831,6 +3015,15 @@ "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/define-data-property": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", @@ -2867,6 +3060,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "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", @@ -2889,6 +3094,18 @@ "node": ">=0.10.0" } }, + "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/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -2904,6 +3121,16 @@ "node": ">= 0.4" } }, + "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/electron-to-chromium": { "version": "1.5.243", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.243.tgz", @@ -2918,6 +3145,15 @@ "dev": true, "license": "MIT" }, + "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/end-of-stream": { "version": "1.4.5", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", @@ -3574,6 +3810,34 @@ "node": ">=6" } }, + "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/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -3858,6 +4122,23 @@ "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/github-from-package": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", @@ -4580,7 +4861,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" @@ -5262,6 +5542,12 @@ "node": ">=10" } }, + "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/node-releases": { "version": "2.0.26", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.26.tgz", @@ -5269,6 +5555,25 @@ "dev": true, "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/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -5392,6 +5697,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "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/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -5509,6 +5820,18 @@ "dev": true, "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", @@ -5528,6 +5851,17 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "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/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -5603,6 +5937,31 @@ "node": ">= 0.8.0" } }, + "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/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -5635,6 +5994,22 @@ "node": ">=6" } }, + "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/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -5680,6 +6055,16 @@ "node": ">=0.10.0" } }, + "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", @@ -5722,6 +6107,19 @@ "node": ">= 6" } }, + "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/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -6455,6 +6853,15 @@ "node": ">=6" } }, + "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/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -6668,7 +7075,7 @@ "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 ad36b3d..fa5a56a 100644 --- a/package.json +++ b/package.json @@ -6,12 +6,21 @@ "dev": "next dev", "build": "next build", "start": "next start", - "lint": "eslint" + "lint": "eslint", + "db:generate": "prisma generate", + "db:push": "prisma db push", + "db:migrate": "prisma migrate dev", + "db:studio": "prisma studio" }, "dependencies": { + "@prisma/client": "^6.19.0", + "@types/bcryptjs": "^2.4.6", "@types/better-sqlite3": "^7.6.13", + "bcryptjs": "^3.0.3", "better-sqlite3": "^12.4.1", + "dotenv": "^17.2.3", "next": "16.0.1", + "prisma": "^6.19.0", "react": "19.2.0", "react-dom": "19.2.0" }, diff --git a/prisma.config.ts b/prisma.config.ts new file mode 100644 index 0000000..08d49c7 --- /dev/null +++ b/prisma.config.ts @@ -0,0 +1,24 @@ +import { config } from "dotenv"; +import { defineConfig } from "prisma/config"; +import { resolve } from "path"; +import { existsSync } from "fs"; + +// .env 파일을 명시적으로 로드 +const envPath = resolve(process.cwd(), ".env"); +if (existsSync(envPath)) { + config({ path: envPath }); +} + +// .env 파일에서 DATABASE_URL을 읽어옵니다 (기본값: file:./dev.db) +const databaseUrl = process.env.DATABASE_URL || "file:./dev.db"; + +export default defineConfig({ + schema: "prisma/schema.prisma", + migrations: { + path: "prisma/migrations", + }, + engine: "classic", + datasource: { + url: databaseUrl, + }, +}); diff --git a/prisma/dev.db b/prisma/dev.db new file mode 100644 index 0000000..0fc148b Binary files /dev/null and b/prisma/dev.db differ diff --git a/prisma/schema.prisma b/prisma/schema.prisma new file mode 100644 index 0000000..d6e5ed4 --- /dev/null +++ b/prisma/schema.prisma @@ -0,0 +1,91 @@ +// This is your Prisma schema file, +// learn more about it in the docs: https://pris.ly/d/prisma-schema + +generator client { + provider = "prisma-client" + output = "../lib/generated/prisma" +} + +datasource db { + provider = "sqlite" + url = env("DATABASE_URL") +} + +// 사용자 권한 Enum +enum UserRole { + STUDENT // 학습자 + INSTRUCTOR // 강사 + ADMIN // 관리자 +} + +// User 모델 +model User { + id String @id @default(cuid()) + email String @unique + name String? + password String + phone String? + gender String? + birthYear Int? + birthMonth Int? + birthDay Int? + role UserRole @default(STUDENT) // 권한 (기본값: 학습자) + isActive Boolean @default(true) // 계정 활성화 여부 (true: 활성화, false: 비활성화) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + registeredLectures Lecture[] @relation("Registrant") // 등록한 강좌 목록 + enrolledLectures UserLecture[] // 수강 중인 강좌 목록 + + @@map("users") +} + +// 교육 과정 관리 +model Curriculum { + id String @id @default(cuid()) + title String // 과정 제목 + instructorId String // 강사 ID (User와의 관계) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + lectures Lecture[] // 강좌 목록 (1:N 관계) + + @@map("curriculums") +} + +// 강좌 관리 +model Lecture { + id String @id @default(cuid()) + title String // 강좌명 + attachmentFile String? // 첨부 파일 경로 + evaluationQuestionCount Int @default(0) // 학습 평가 문제 수 + curriculumId String // 교육 과정 ID (Curriculum과의 관계) + registrantId String // 등록자 ID (User와의 관계) + registeredAt DateTime @default(now()) // 등록일 + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + curriculum Curriculum @relation(fields: [curriculumId], references: [id], onDelete: Cascade) + registrant User @relation("Registrant", fields: [registrantId], references: [id]) + enrolledUsers UserLecture[] // 수강 중인 사용자 목록 + + @@map("lectures") +} + +// 사용자-강좌 수강 관계 (다대다 관계) +model UserLecture { + id String @id @default(cuid()) + userId String // 사용자 ID + lectureId String // 강좌 ID + enrolledAt DateTime @default(now()) // 수강 시작일 + completedAt DateTime? // 수강 완료일 + isCompleted Boolean @default(false) // 수강 완료 여부 + progress Int @default(0) // 수강 진행률 (0-100) + score Int? // 평가 점수 + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + lecture Lecture @relation(fields: [lectureId], references: [id], onDelete: Cascade) + + @@unique([userId, lectureId]) // 한 사용자는 같은 강좌를 중복 수강할 수 없음 + @@map("user_lectures") +}