diff --git a/app/admin_home/page.tsx b/app/admin_home/page.tsx index e69de29..1ce030d 100644 --- a/app/admin_home/page.tsx +++ b/app/admin_home/page.tsx @@ -0,0 +1,49 @@ +"use client"; + +import { useEffect, useState } from 'react'; +import { useRouter } from 'next/navigation'; +import { isAdminLoggedIn } from '../../lib/auth'; +import LoginPage from '../login/page'; + +export default function AdminHomePage() { + const router = useRouter(); + const [isLoading, setIsLoading] = useState(true); + const [isAuthenticated, setIsAuthenticated] = useState(false); + + useEffect(() => { + // 관리자 인증 확인 + const checkAuth = () => { + if (typeof window !== 'undefined') { + const isAdmin = isAdminLoggedIn(); + setIsAuthenticated(isAdmin); + setIsLoading(false); + + if (!isAdmin) { + // 인증되지 않은 경우 로그인 페이지로 리다이렉트 + router.push('/login'); + } + } + }; + + checkAuth(); + }, [router]); + + if (isLoading) { + return null; // 로딩 중 + } + + if (!isAuthenticated) { + return ; + } + + return ( +
+
+
+

관리자 홈

+

관리자 페이지에 오신 것을 환영합니다.

+
+
+
+ ); +} diff --git a/app/admin_lecture1/page.tsx b/app/admin_lecture1/page.tsx index 920f421..fdd72d6 100644 --- a/app/admin_lecture1/page.tsx +++ b/app/admin_lecture1/page.tsx @@ -1,8 +1,11 @@ "use client"; -import { useState } from 'react'; +import { useState, useEffect } from 'react'; import { useRouter } from 'next/navigation'; +import { isAdminLoggedIn } from '../../lib/auth'; +import LoginPage from '../login/page'; import Close from '../../public/svg/close'; +import Logout from '../../public/svg/logout'; const imgLogo = "http://localhost:3845/assets/89fda8e949171025b1232bae70fc9d442e4e70c8.png"; const imgArrowDisabled = "http://localhost:3845/assets/6edcb2defc36a2bf4a05a3abe53b8da3d42b2cb4.svg"; @@ -122,6 +125,8 @@ function PaginationBtnMove({ status = "Default", move = "Previous" }: { status?: export default function AdminLecture1Page() { const router = useRouter(); + const [isLoading, setIsLoading] = useState(true); + const [isAuthenticated, setIsAuthenticated] = useState(false); const [currentPage, setCurrentPage] = useState(1); const [isModalOpen, setIsModalOpen] = useState(false); const [courseName, setCourseName] = useState(''); @@ -174,6 +179,24 @@ export default function AdminLecture1Page() { const instructors = ['김강사', '이강사', '박강사', '최강사']; + // 관리자 인증 확인 + useEffect(() => { + const checkAuth = () => { + if (typeof window !== 'undefined') { + const isAdmin = isAdminLoggedIn(); + setIsAuthenticated(isAdmin); + setIsLoading(false); + + if (!isAdmin) { + // 인증되지 않은 경우 로그인 페이지로 리다이렉트 + router.push('/login'); + } + } + }; + + checkAuth(); + }, [router]); + const handleOpenModal = () => { setIsModalOpen(true); setCourseName(''); @@ -230,6 +253,14 @@ export default function AdminLecture1Page() { const endIndex = startIndex + itemsPerPage; const currentCourses = courses.slice(startIndex, endIndex); + if (isLoading) { + return null; // 로딩 중 + } + + if (!isAuthenticated) { + return ; + } + return (
{/* 사이드바 */} @@ -239,8 +270,8 @@ export default function AdminLecture1Page() { onClick={() => router.push('/')} className="h-[102px] relative shrink-0 w-[99px] cursor-pointer hover:opacity-80 transition-opacity" > -
- +
+ 로고
{/* 메뉴 */} @@ -260,7 +291,10 @@ export default function AdminLecture1Page() {

- + {/* 메뉴 */} +
+
+ + + + + + + + + +
+
+ {/* 로그아웃 */} +
+
+
+
+ +
+
+
+ +
+
+ + {/* 메인 콘텐츠 */} +
+ {/* 헤더 */} +
+
+
+

+ 강좌 관리 +

+
+
+
+ + {/* 강좌 등록 버튼 */} + + + {/* 테이블 */} +
+ {/* 테이블 헤더 */} +
+
+
+
+

교육 과정명

+
+
+
+
+

강좌명

+
+
+
+
+

첨부 파일

+
+
+
+
+

학습 평가 문제 수

+
+
+
+
+

등록자

+
+
+
+
+

등록일

+
+
+
+
+ + {/* 빈 상태 또는 테이블 바디 */} + {lectures.length === 0 ? ( +
+
+
+

+ 교육 과정을 추가해 주세요. +

+
+
+
+ ) : ( +
+ {lectures.map((lecture) => ( +
+
+
+
+

{lecture.courseName}

+
+
+
+
+

{lecture.lectureName}

+
+
+
+
+

{lecture.attachedFile}

+
+
+
+
+

{lecture.questionCount}

+
+
+
+
+

{lecture.registrar}

+
+
+
+
+

{lecture.createdAt}

+
+
+
+
+ ))} +
+ )} +
+
+ + ); +} + diff --git a/app/api/curriculums/[id]/route.ts b/app/api/curriculums/[id]/route.ts new file mode 100644 index 0000000..217d77a --- /dev/null +++ b/app/api/curriculums/[id]/route.ts @@ -0,0 +1,124 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; + +// GET: 특정 교육 과정 조회 +export async function GET( + request: NextRequest, + { params }: { params: { id: string } } +) { + try { + const curriculum = await prisma.curriculum.findUnique({ + where: { id: params.id }, + include: { + lectures: { + orderBy: { + registeredAt: 'desc', + }, + include: { + registrant: { + select: { + name: true, + }, + }, + }, + }, + }, + }); + + if (!curriculum) { + return NextResponse.json( + { error: '교육 과정을 찾을 수 없습니다.' }, + { status: 404 } + ); + } + + return NextResponse.json({ + data: { + id: curriculum.id, + courseName: curriculum.title, + instructorId: curriculum.instructorId, + thumbnailImage: curriculum.thumbnailImage, + createdAt: curriculum.createdAt.toISOString().split('T')[0], + lectures: curriculum.lectures.map((lecture) => ({ + id: lecture.id, + lectureName: lecture.title, + attachedFile: lecture.attachmentFile || '없음', + questionCount: lecture.evaluationQuestionCount, + registrar: lecture.registrant.name || '알 수 없음', + createdAt: lecture.registeredAt.toISOString().split('T')[0], + })), + }, + }); + } catch (error) { + console.error('Error fetching curriculum:', error); + return NextResponse.json( + { error: '교육 과정을 불러오는 중 오류가 발생했습니다.' }, + { status: 500 } + ); + } +} + +// PUT: 교육 과정 수정 +export async function PUT( + request: NextRequest, + { params }: { params: { id: string } } +) { + try { + const body = await request.json(); + const { title, instructorId, thumbnailImage } = body; + + const curriculum = await prisma.curriculum.update({ + where: { id: params.id }, + data: { + ...(title && { title }), + ...(instructorId && { instructorId }), + ...(thumbnailImage !== undefined && { thumbnailImage }), + }, + include: { + lectures: { + select: { + id: true, + }, + }, + }, + }); + + return NextResponse.json({ + data: { + id: curriculum.id, + courseName: curriculum.title, + instructorId: curriculum.instructorId, + thumbnailImage: curriculum.thumbnailImage, + createdAt: curriculum.createdAt.toISOString().split('T')[0], + lectureCount: curriculum.lectures.length, + }, + }); + } catch (error) { + console.error('Error updating curriculum:', error); + return NextResponse.json( + { error: '교육 과정 수정 중 오류가 발생했습니다.' }, + { status: 500 } + ); + } +} + +// DELETE: 교육 과정 삭제 +export async function DELETE( + request: NextRequest, + { params }: { params: { id: string } } +) { + try { + await prisma.curriculum.delete({ + where: { id: params.id }, + }); + + return NextResponse.json({ message: '교육 과정이 삭제되었습니다.' }); + } catch (error) { + console.error('Error deleting curriculum:', error); + return NextResponse.json( + { error: '교육 과정 삭제 중 오류가 발생했습니다.' }, + { status: 500 } + ); + } +} + diff --git a/app/api/curriculums/route.ts b/app/api/curriculums/route.ts new file mode 100644 index 0000000..1a3945a --- /dev/null +++ b/app/api/curriculums/route.ts @@ -0,0 +1,113 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; + +// GET: 교육 과정 목록 조회 +export async function GET(request: NextRequest) { + try { + const searchParams = request.nextUrl.searchParams; + const page = parseInt(searchParams.get('page') || '1'); + const limit = parseInt(searchParams.get('limit') || '13'); + const skip = (page - 1) * limit; + + const [curriculums, total] = await Promise.all([ + prisma.curriculum.findMany({ + skip, + take: limit, + orderBy: { + createdAt: 'desc', + }, + include: { + lectures: { + select: { + id: true, + }, + }, + }, + }), + prisma.curriculum.count(), + ]); + + return NextResponse.json({ + data: curriculums.map((curriculum) => ({ + id: curriculum.id, + courseName: curriculum.title, + instructorId: curriculum.instructorId, + thumbnailImage: curriculum.thumbnailImage, + createdAt: curriculum.createdAt.toISOString().split('T')[0], + lectureCount: curriculum.lectures.length, + })), + pagination: { + page, + limit, + total, + totalPages: Math.ceil(total / limit), + }, + }); + } 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, thumbnailImage } = body; + + if (!title || !instructorId) { + return NextResponse.json( + { error: '교육 과정명과 강사 ID는 필수입니다.' }, + { status: 400 } + ); + } + + // 강사 존재 확인 + const instructor = await prisma.user.findUnique({ + where: { id: instructorId }, + }); + + if (!instructor || instructor.role !== 'INSTRUCTOR') { + return NextResponse.json( + { error: '유효한 강사를 선택해주세요.' }, + { status: 400 } + ); + } + + const curriculum = await prisma.curriculum.create({ + data: { + title, + instructorId, + thumbnailImage: thumbnailImage || null, + }, + include: { + lectures: { + select: { + id: true, + }, + }, + }, + }); + + return NextResponse.json({ + data: { + id: curriculum.id, + courseName: curriculum.title, + instructorId: curriculum.instructorId, + thumbnailImage: curriculum.thumbnailImage, + createdAt: curriculum.createdAt.toISOString().split('T')[0], + lectureCount: curriculum.lectures.length, + }, + }, { status: 201 }); + } catch (error) { + console.error('Error creating curriculum:', error); + return NextResponse.json( + { error: '교육 과정 생성 중 오류가 발생했습니다.' }, + { status: 500 } + ); + } +} + diff --git a/app/api/lectures/[id]/route.ts b/app/api/lectures/[id]/route.ts new file mode 100644 index 0000000..bde48a4 --- /dev/null +++ b/app/api/lectures/[id]/route.ts @@ -0,0 +1,127 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; + +// GET: 특정 강좌 조회 +export async function GET( + request: NextRequest, + { params }: { params: { id: string } } +) { + try { + const lecture = await prisma.lecture.findUnique({ + where: { id: params.id }, + include: { + curriculum: { + select: { + title: true, + }, + }, + registrant: { + select: { + name: true, + }, + }, + }, + }); + + if (!lecture) { + return NextResponse.json( + { error: '강좌를 찾을 수 없습니다.' }, + { status: 404 } + ); + } + + return NextResponse.json({ + data: { + id: lecture.id, + courseName: lecture.curriculum.title, + lectureName: lecture.title, + attachedFile: lecture.attachmentFile || '없음', + questionCount: lecture.evaluationQuestionCount, + registrar: lecture.registrant.name || '알 수 없음', + createdAt: lecture.registeredAt.toISOString().split('T')[0], + curriculumId: lecture.curriculumId, + thumbnailImage: lecture.thumbnailImage, + }, + }); + } catch (error) { + console.error('Error fetching lecture:', error); + return NextResponse.json( + { error: '강좌를 불러오는 중 오류가 발생했습니다.' }, + { status: 500 } + ); + } +} + +// PUT: 강좌 수정 +export async function PUT( + request: NextRequest, + { params }: { params: { id: string } } +) { + try { + const body = await request.json(); + const { title, attachmentFile, evaluationQuestionCount, thumbnailImage } = body; + + const lecture = await prisma.lecture.update({ + where: { id: params.id }, + data: { + ...(title && { title }), + ...(attachmentFile !== undefined && { attachmentFile }), + ...(evaluationQuestionCount !== undefined && { evaluationQuestionCount }), + ...(thumbnailImage !== undefined && { thumbnailImage }), + }, + include: { + curriculum: { + select: { + title: true, + }, + }, + registrant: { + select: { + name: true, + }, + }, + }, + }); + + return NextResponse.json({ + data: { + id: lecture.id, + courseName: lecture.curriculum.title, + lectureName: lecture.title, + attachedFile: lecture.attachmentFile || '없음', + questionCount: lecture.evaluationQuestionCount, + registrar: lecture.registrant.name || '알 수 없음', + createdAt: lecture.registeredAt.toISOString().split('T')[0], + curriculumId: lecture.curriculumId, + thumbnailImage: lecture.thumbnailImage, + }, + }); + } catch (error) { + console.error('Error updating lecture:', error); + return NextResponse.json( + { error: '강좌 수정 중 오류가 발생했습니다.' }, + { status: 500 } + ); + } +} + +// DELETE: 강좌 삭제 +export async function DELETE( + request: NextRequest, + { params }: { params: { id: string } } +) { + try { + await prisma.lecture.delete({ + where: { id: params.id }, + }); + + return NextResponse.json({ message: '강좌가 삭제되었습니다.' }); + } catch (error) { + console.error('Error deleting lecture:', 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..fcbd1ba --- /dev/null +++ b/app/api/lectures/route.ts @@ -0,0 +1,148 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; + +// GET: 강좌 목록 조회 +export async function GET(request: NextRequest) { + try { + const searchParams = request.nextUrl.searchParams; + const page = parseInt(searchParams.get('page') || '1'); + const limit = parseInt(searchParams.get('limit') || '13'); + const curriculumId = searchParams.get('curriculumId'); + const skip = (page - 1) * limit; + + const where = curriculumId ? { curriculumId } : {}; + + const [lectures, total] = await Promise.all([ + prisma.lecture.findMany({ + where, + skip, + take: limit, + orderBy: { + registeredAt: 'desc', + }, + include: { + curriculum: { + select: { + title: true, + }, + }, + registrant: { + select: { + name: true, + }, + }, + }, + }), + prisma.lecture.count({ where }), + ]); + + return NextResponse.json({ + data: lectures.map((lecture) => ({ + id: lecture.id, + courseName: lecture.curriculum.title, + lectureName: lecture.title, + attachedFile: lecture.attachmentFile || '없음', + questionCount: lecture.evaluationQuestionCount, + registrar: lecture.registrant.name || '알 수 없음', + createdAt: lecture.registeredAt.toISOString().split('T')[0], + curriculumId: lecture.curriculumId, + thumbnailImage: lecture.thumbnailImage, + })), + pagination: { + page, + limit, + total, + totalPages: Math.ceil(total / limit), + }, + }); + } 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, curriculumId, registrantId, attachmentFile, evaluationQuestionCount, thumbnailImage } = 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: 400 } + ); + } + + // 등록자 존재 확인 + const registrant = await prisma.user.findUnique({ + where: { id: registrantId }, + }); + + if (!registrant) { + return NextResponse.json( + { error: '유효한 등록자를 선택해주세요.' }, + { status: 400 } + ); + } + + const lecture = await prisma.lecture.create({ + data: { + title, + curriculumId, + registrantId, + attachmentFile: attachmentFile || null, + evaluationQuestionCount: evaluationQuestionCount || 0, + thumbnailImage: thumbnailImage || null, + }, + include: { + curriculum: { + select: { + title: true, + }, + }, + registrant: { + select: { + name: true, + }, + }, + }, + }); + + return NextResponse.json({ + data: { + id: lecture.id, + courseName: lecture.curriculum.title, + lectureName: lecture.title, + attachedFile: lecture.attachmentFile || '없음', + questionCount: lecture.evaluationQuestionCount, + registrar: lecture.registrant.name || '알 수 없음', + createdAt: lecture.registeredAt.toISOString().split('T')[0], + curriculumId: lecture.curriculumId, + thumbnailImage: lecture.thumbnailImage, + }, + }, { status: 201 }); + } catch (error) { + console.error('Error creating lecture:', error); + return NextResponse.json( + { error: '강좌 생성 중 오류가 발생했습니다.' }, + { status: 500 } + ); + } +} + diff --git a/app/api/test/route.ts b/app/api/test/route.ts deleted file mode 100644 index b48bff5..0000000 --- a/app/api/test/route.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { PrismaClient } from "@/lib/generated/prisma/client"; -import { NextResponse } from "next/server"; - -const prisma = new PrismaClient(); - -export async function GET() { - try { - const tests = await prisma.test.findMany(); - return NextResponse.json(tests); - } catch (error) { - console.error(error); - return NextResponse.json({ error: "Failed to fetch tests." }, { status: 500 }); - } -} - -export async function POST(req: Request) { - try { - const body = await req.json(); - const { name } = body; - if (!name) { - return NextResponse.json({ error: "Name is required." }, { status: 400 }); - } - const test = await prisma.test.create({ - data: { name }, - }); - return NextResponse.json(test, { status: 201 }); - } catch (error) { - return NextResponse.json({ error: "Failed to create test." }, { status: 500 }); - } -} diff --git a/app/api/user-lectures/[id]/route.ts b/app/api/user-lectures/[id]/route.ts new file mode 100644 index 0000000..bf87e17 --- /dev/null +++ b/app/api/user-lectures/[id]/route.ts @@ -0,0 +1,152 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; + +// GET: 특정 수강 관계 조회 +export async function GET( + request: NextRequest, + { params }: { params: { id: string } } +) { + try { + const userLecture = await prisma.userLecture.findUnique({ + where: { id: params.id }, + include: { + user: { + select: { + id: true, + name: true, + email: true, + }, + }, + lecture: { + include: { + curriculum: { + select: { + title: true, + }, + }, + }, + }, + }, + }); + + if (!userLecture) { + return NextResponse.json( + { error: '수강 관계를 찾을 수 없습니다.' }, + { status: 404 } + ); + } + + return NextResponse.json({ + data: { + id: userLecture.id, + userId: userLecture.userId, + userName: userLecture.user.name, + userEmail: userLecture.user.email, + lectureId: userLecture.lectureId, + lectureName: userLecture.lecture.title, + courseName: userLecture.lecture.curriculum.title, + enrolledAt: userLecture.enrolledAt.toISOString().split('T')[0], + completedAt: userLecture.completedAt ? userLecture.completedAt.toISOString().split('T')[0] : null, + isCompleted: userLecture.isCompleted, + progress: userLecture.progress, + score: userLecture.score, + }, + }); + } catch (error) { + console.error('Error fetching user lecture:', error); + return NextResponse.json( + { error: '수강 관계를 불러오는 중 오류가 발생했습니다.' }, + { status: 500 } + ); + } +} + +// PUT: 수강 관계 수정 (진행률, 완료 여부, 점수 등) +export async function PUT( + request: NextRequest, + { params }: { params: { id: string } } +) { + try { + const body = await request.json(); + const { progress, isCompleted, score } = body; + + const updateData: any = {}; + if (progress !== undefined) updateData.progress = Math.max(0, Math.min(100, progress)); + if (isCompleted !== undefined) { + updateData.isCompleted = isCompleted; + if (isCompleted && !body.completedAt) { + updateData.completedAt = new Date(); + } else if (!isCompleted) { + updateData.completedAt = null; + } + } + if (score !== undefined) updateData.score = score; + + const userLecture = await prisma.userLecture.update({ + where: { id: params.id }, + data: updateData, + include: { + user: { + select: { + id: true, + name: true, + email: true, + }, + }, + lecture: { + include: { + curriculum: { + select: { + title: true, + }, + }, + }, + }, + }, + }); + + return NextResponse.json({ + data: { + id: userLecture.id, + userId: userLecture.userId, + userName: userLecture.user.name, + userEmail: userLecture.user.email, + lectureId: userLecture.lectureId, + lectureName: userLecture.lecture.title, + courseName: userLecture.lecture.curriculum.title, + enrolledAt: userLecture.enrolledAt.toISOString().split('T')[0], + completedAt: userLecture.completedAt ? userLecture.completedAt.toISOString().split('T')[0] : null, + isCompleted: userLecture.isCompleted, + progress: userLecture.progress, + score: userLecture.score, + }, + }); + } catch (error) { + console.error('Error updating user lecture:', error); + return NextResponse.json( + { error: '수강 관계 수정 중 오류가 발생했습니다.' }, + { status: 500 } + ); + } +} + +// DELETE: 수강 관계 삭제 (수강 취소) +export async function DELETE( + request: NextRequest, + { params }: { params: { id: string } } +) { + try { + await prisma.userLecture.delete({ + where: { id: params.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/user-lectures/route.ts b/app/api/user-lectures/route.ts new file mode 100644 index 0000000..7e86af5 --- /dev/null +++ b/app/api/user-lectures/route.ts @@ -0,0 +1,182 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; + +// GET: 사용자-강좌 수강 관계 목록 조회 +export async function GET(request: NextRequest) { + try { + const searchParams = request.nextUrl.searchParams; + const userId = searchParams.get('userId'); + const lectureId = searchParams.get('lectureId'); + const page = parseInt(searchParams.get('page') || '1'); + const limit = parseInt(searchParams.get('limit') || '100'); + const skip = (page - 1) * limit; + + const where: any = {}; + if (userId) where.userId = userId; + if (lectureId) where.lectureId = lectureId; + + const [userLectures, total] = await Promise.all([ + prisma.userLecture.findMany({ + where, + skip, + take: limit, + orderBy: { + enrolledAt: 'desc', + }, + include: { + user: { + select: { + id: true, + name: true, + email: true, + }, + }, + lecture: { + include: { + curriculum: { + select: { + title: true, + }, + }, + }, + }, + }, + }), + prisma.userLecture.count({ where }), + ]); + + return NextResponse.json({ + data: userLectures.map((ul) => ({ + id: ul.id, + userId: ul.userId, + userName: ul.user.name, + userEmail: ul.user.email, + lectureId: ul.lectureId, + lectureName: ul.lecture.title, + courseName: ul.lecture.curriculum.title, + enrolledAt: ul.enrolledAt.toISOString().split('T')[0], + completedAt: ul.completedAt ? ul.completedAt.toISOString().split('T')[0] : null, + isCompleted: ul.isCompleted, + progress: ul.progress, + score: ul.score, + })), + pagination: { + page, + limit, + total, + totalPages: Math.ceil(total / limit), + }, + }); + } 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: 400 } + ); + } + + // 강좌 존재 확인 + const lecture = await prisma.lecture.findUnique({ + where: { id: lectureId }, + }); + + if (!lecture) { + return NextResponse.json( + { error: '유효한 강좌를 선택해주세요.' }, + { status: 400 } + ); + } + + // 이미 수강 중인지 확인 + const existing = await prisma.userLecture.findUnique({ + where: { + userId_lectureId: { + userId, + lectureId, + }, + }, + }); + + if (existing) { + return NextResponse.json( + { error: '이미 수강 중인 강좌입니다.' }, + { status: 400 } + ); + } + + const userLecture = await prisma.userLecture.create({ + data: { + userId, + lectureId, + }, + include: { + user: { + select: { + id: true, + name: true, + email: true, + }, + }, + lecture: { + include: { + curriculum: { + select: { + title: true, + }, + }, + }, + }, + }, + }); + + return NextResponse.json({ + data: { + id: userLecture.id, + userId: userLecture.userId, + userName: userLecture.user.name, + userEmail: userLecture.user.email, + lectureId: userLecture.lectureId, + lectureName: userLecture.lecture.title, + courseName: userLecture.lecture.curriculum.title, + enrolledAt: userLecture.enrolledAt.toISOString().split('T')[0], + completedAt: userLecture.completedAt ? userLecture.completedAt.toISOString().split('T')[0] : null, + isCompleted: userLecture.isCompleted, + progress: userLecture.progress, + score: userLecture.score, + }, + }, { status: 201 }); + } catch (error) { + console.error('Error creating user lecture:', 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..0d5f80f --- /dev/null +++ b/app/api/users/route.ts @@ -0,0 +1,59 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; + +// GET: 사용자 목록 조회 (강사 목록 포함) +export async function GET(request: NextRequest) { + try { + const searchParams = request.nextUrl.searchParams; + const role = searchParams.get('role'); // 'INSTRUCTOR', 'ADMIN', 'STUDENT' 등 + const page = parseInt(searchParams.get('page') || '1'); + const limit = parseInt(searchParams.get('limit') || '100'); + const skip = (page - 1) * limit; + + const where = role ? { role: role as any } : {}; + + const [users, total] = await Promise.all([ + prisma.user.findMany({ + where, + skip, + take: limit, + orderBy: { + createdAt: 'desc', + }, + select: { + id: true, + email: true, + name: true, + role: true, + isActive: true, + createdAt: true, + }, + }), + prisma.user.count({ where }), + ]); + + return NextResponse.json({ + data: users.map((user) => ({ + id: user.id, + email: user.email, + name: user.name, + role: user.role, + isActive: user.isActive, + createdAt: user.createdAt.toISOString().split('T')[0], + })), + pagination: { + page, + limit, + total, + totalPages: Math.ceil(total / limit), + }, + }); + } catch (error) { + console.error('Error fetching users:', error); + return NextResponse.json( + { error: '사용자 목록을 불러오는 중 오류가 발생했습니다.' }, + { status: 500 } + ); + } +} + diff --git a/app/login/page.tsx b/app/login/page.tsx index 957625d..678065b 100644 --- a/app/login/page.tsx +++ b/app/login/page.tsx @@ -70,10 +70,27 @@ export default function LoginPage() { return; } - // 아이디와 비밀번호 검증 + // 관리자 계정 검증 + if (username === 'admin' && password === '1234') { + // 관리자 로그인 성공 + localStorage.setItem('isAdminLoggedIn', 'true'); + localStorage.setItem('isLoggedIn', 'true'); + // 아이디 기억하기 체크 시 아이디 저장 + if (rememberId) { + localStorage.setItem('rememberedUsername', username); + } else { + localStorage.removeItem('rememberedUsername'); + } + // 루트 경로로 이동 (관리자 페이지 표시) + window.location.href = '/'; + return; + } + + // 일반 사용자 계정 검증 if (username === 'qwre@naver.com' && password === '1234') { // 로그인 성공 localStorage.setItem('isLoggedIn', 'true'); + localStorage.removeItem('isAdminLoggedIn'); // 일반 사용자는 관리자 플래그 제거 // 아이디 기억하기 체크 시 아이디 저장 if (rememberId) { localStorage.setItem('rememberedUsername', username); diff --git a/app/page.tsx b/app/page.tsx index 23e1170..af19961 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -3,8 +3,10 @@ import { useEffect, useState } from 'react'; import { useRouter } from 'next/navigation'; import Image from 'next/image'; +import { isAdminLoggedIn } from '../lib/auth'; import LoginPage from './login/page'; import Header from './components/Header'; +import Logout from '../public/svg/logout'; const imgImage2 = "http://localhost:3845/assets/89fda8e949171025b1232bae70fc9d442e4e70c8.png"; const imgImage7 = "http://localhost:3845/assets/a4e4d09643b890b56084560cc24d6e532a03487b.png"; @@ -16,8 +18,10 @@ const imgRectangle1738 = "http://localhost:3845/assets/50e850999bbdd551763a187d4 export default function HomePage() { const router = useRouter(); const [isLoggedIn, setIsLoggedIn] = useState(false); + const [isAdmin, setIsAdmin] = useState(false); const [isLoading, setIsLoading] = useState(true); const [currentHeroSlide, setCurrentHeroSlide] = useState(0); + const [selectedTab, setSelectedTab] = useState<'전체' | '학습자' | '강사' | '운영자'>('전체'); // 임시 데이터 - 실제로는 API에서 가져올 데이터 const [courses, setCourses] = useState([ @@ -53,7 +57,9 @@ export default function HomePage() { useEffect(() => { // 로그인 상태 확인 const loginStatus = localStorage.getItem('isLoggedIn') === 'true'; + const adminStatus = isAdminLoggedIn(); setIsLoggedIn(loginStatus); + setIsAdmin(adminStatus); setIsLoading(false); }, []); @@ -66,7 +72,222 @@ export default function HomePage() { return ; } - // 로그인되었으면 메인 페이지 표시 + // 관리자일 경우 권한 설정 페이지 표시 + if (isAdmin) { + return ( +
+ {/* 사이드바 */} +
+ {/* 로고 */} + + {/* 메뉴 */} +
+
+ + + + + + + + + +
+
+ {/* 로그아웃 */} +
+
+
+
+ +
+
+
+ +
+
+ + {/* 메인 콘텐츠 */} +
+ {/* 페이지 타이틀 탭 */} +
+ + + + +
+ + {/* 사용자 목록 테이블 */} +
+ {/* 테이블 헤더 */} +
+
+
+
+

가입일

+
+
+
+
+

사용자명

+
+
+
+
+

아이디(이메일)

+
+
+
+
+

권한

+
+
+
+
+

계정 상태

+
+
+
+
+

계정 관리

+
+
+
+
+ + {/* 빈 상태 메시지 */} +
+
+
+

+ 존재하는 계정이 없어요. +

+
+
+
+
+
+
+ ); + } + + // 일반 사용자일 경우 기존 메인 페이지 표시 return (
{/* 헤더 */} diff --git a/lib/auth.ts b/lib/auth.ts new file mode 100644 index 0000000..2c556be --- /dev/null +++ b/lib/auth.ts @@ -0,0 +1,35 @@ +/** + * 관리자 인증 유틸리티 함수 + */ + +/** + * 관리자 로그인 상태 확인 + * @returns 관리자 로그인 여부 + */ +export function isAdminLoggedIn(): boolean { + if (typeof window === 'undefined') { + return false; + } + return localStorage.getItem('isAdminLoggedIn') === 'true'; +} + +/** + * 관리자 인증 체크 및 리다이렉트 + * 인증되지 않은 경우 로그인 페이지로 이동 + */ +export function requireAdminAuth(): boolean { + if (typeof window === 'undefined') { + return false; + } + + const isAdmin = isAdminLoggedIn(); + + if (!isAdmin) { + // 관리자 인증되지 않은 경우 로그인 페이지로 리다이렉트 + window.location.href = '/login'; + return false; + } + + return true; +} + diff --git a/lib/prisma.ts b/lib/prisma.ts new file mode 100644 index 0000000..c91cf74 --- /dev/null +++ b/lib/prisma.ts @@ -0,0 +1,14 @@ +import { PrismaClient } from './generated/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; + diff --git a/prisma/schema.prisma b/prisma/schema.prisma index d28102f..7e7647d 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -97,8 +97,3 @@ model UserLecture { @@unique([userId, lectureId]) // 한 사용자는 같은 강좌를 중복 수강할 수 없음 @@map("user_lectures") } - -model Test { - id String @id @default(cuid()) - name String -} diff --git a/public/svg/chevron_middle.tsx b/public/svg/chevron_middle.tsx index e4c4a6d..830e840 100644 --- a/public/svg/chevron_middle.tsx +++ b/public/svg/chevron_middle.tsx @@ -1,8 +1,8 @@ export default function ChevronMiddle() { return ( - - + + diff --git a/public/svg/chevron_small.tsx b/public/svg/chevron_small.tsx index 7eef87a..9fc66f0 100644 --- a/public/svg/chevron_small.tsx +++ b/public/svg/chevron_small.tsx @@ -1,7 +1,7 @@ export default function ChevronSmall() { return ( - + ); } diff --git a/public/svg/close.tsx b/public/svg/close.tsx index e85917a..e22388b 100644 --- a/public/svg/close.tsx +++ b/public/svg/close.tsx @@ -1,7 +1,7 @@ export default function Close() { return ( - + diff --git a/public/svg/logout.tsx b/public/svg/logout.tsx new file mode 100644 index 0000000..046f851 --- /dev/null +++ b/public/svg/logout.tsx @@ -0,0 +1,7 @@ +export default function Logout() { + return ( + + + + ); +}