관리자 계정 설정, 관리자 페이지 작업1

This commit is contained in:
wallace
2025-11-13 15:02:43 +09:00
parent c81f218a2b
commit 8403684df3
20 changed files with 1571 additions and 46 deletions

View File

@@ -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 <LoginPage />;
}
return (
<div className="bg-white relative size-full min-h-screen">
<div className="flex items-center justify-center min-h-screen">
<div className="text-center">
<h1 className="text-2xl font-bold text-[#404040] mb-4"> </h1>
<p className="text-[#515151]"> .</p>
</div>
</div>
</div>
);
}

View File

@@ -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 <LoginPage />;
}
return (
<div className="bg-white relative size-full min-h-screen">
{/* 사이드바 */}
@@ -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"
>
<div className="absolute inset-0 overflow-hidden pointer-events-none">
<img alt="" className="absolute h-[291.74%] left-[-100%] max-w-none top-[-95.73%] w-[301.18%]" src={imgLogo} />
<div className="absolute inset-0 flex items-center justify-center overflow-hidden pointer-events-none">
<img alt="로고" className="h-full w-full object-contain" src="/logo.svg" />
</div>
</button>
{/* 메뉴 */}
@@ -260,7 +291,10 @@ export default function AdminLecture1Page() {
</p>
</div>
</button>
<button className="box-border content-stretch flex gap-2 h-[34px] items-center pl-2 pr-2 py-1 relative rounded-lg shrink-0 w-[226px] cursor-pointer hover:bg-gray-50 transition-colors">
<button
onClick={() => router.push('/admin_lecture2')}
className="box-border content-stretch flex gap-2 h-[34px] items-center pl-2 pr-2 py-1 relative rounded-lg shrink-0 w-[226px] cursor-pointer hover:bg-gray-50 transition-colors"
>
<div className="basis-0 content-stretch flex gap-2 grow items-center min-h-px min-w-px relative shrink-0 pl-2">
<p className="[white-space-collapse:collapse] basis-0 font-medium grow leading-[1.6] min-h-px min-w-px not-italic overflow-ellipsis overflow-hidden relative shrink-0 text-[14px] text-[#404040] text-left text-nowrap">
@@ -316,13 +350,14 @@ export default function AdminLecture1Page() {
<div className="flex items-center justify-center relative shrink-0">
<div className="flex-none rotate-[180deg] scale-y-[-100%]">
<div className="h-[23.12px] relative w-[22px]">
<img alt="" className="block max-w-none size-full" src={imgLogout} />
<Logout />
</div>
</div>
</div>
<button
onClick={() => {
localStorage.removeItem('isLoggedIn');
localStorage.removeItem('isAdminLoggedIn');
router.push('/');
}}
className="content-stretch flex h-[36px] items-center justify-between relative shrink-0 w-[76px] cursor-pointer hover:opacity-80 transition-opacity"

View File

@@ -0,0 +1,277 @@
"use client";
import { useState, useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { isAdminLoggedIn } from '../../lib/auth';
import LoginPage from '../login/page';
import Logout from '../../public/svg/logout';
interface Lecture {
id: number;
courseName: string;
lectureName: string;
attachedFile: string;
questionCount: number;
registrar: string;
createdAt: string;
}
export default function AdminLecture2Page() {
const router = useRouter();
const [isLoading, setIsLoading] = useState(true);
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [lectures, setLectures] = useState<Lecture[]>([]);
// 관리자 인증 확인
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 <LoginPage />;
}
return (
<div className="bg-white relative size-full min-h-screen">
{/* 사이드바 */}
<div className="absolute bg-white border-r border-[#eeeeee] border-solid box-border content-stretch flex flex-col gap-[45px] items-center left-0 min-h-[1080px] pb-8 pt-[30px] px-0 top-0 w-[250px]">
{/* 로고 */}
<button
onClick={() => router.push('/')}
className="h-[102px] relative shrink-0 w-[99px] cursor-pointer hover:opacity-80 transition-opacity"
>
<div className="absolute inset-0 flex items-center justify-center overflow-hidden pointer-events-none">
<img alt="로고" className="h-full w-full object-contain" src="/logo.svg" />
</div>
</button>
{/* 메뉴 */}
<div className="box-border content-stretch flex flex-col items-center pb-0 pt-4 px-0 relative shrink-0 w-[250px]">
<div className="box-border content-stretch flex flex-col gap-2 items-start p-3 relative shrink-0 w-full">
<button className="box-border content-stretch flex gap-2 h-[34px] items-center pl-2 pr-2 py-1 relative rounded-lg shrink-0 w-[226px] cursor-pointer hover:bg-gray-50 transition-colors">
<div className="basis-0 content-stretch flex gap-2 grow items-center min-h-px min-w-px relative shrink-0 pl-2">
<p className="[white-space-collapse:collapse] basis-0 font-medium grow leading-[1.6] min-h-px min-w-px not-italic overflow-ellipsis overflow-hidden relative shrink-0 text-[14px] text-[#404040] text-left text-nowrap">
</p>
</div>
</button>
<button
onClick={() => router.push('/admin_lecture1')}
className="box-border content-stretch flex gap-2 h-[34px] items-center pl-2 pr-2 py-1 relative rounded-lg shrink-0 w-[226px] cursor-pointer hover:bg-gray-50 transition-colors"
>
<div className="basis-0 content-stretch flex gap-2 grow items-center min-h-px min-w-px relative shrink-0 pl-2">
<p className="[white-space-collapse:collapse] basis-0 font-medium grow leading-[1.6] min-h-px min-w-px not-italic overflow-ellipsis overflow-hidden relative shrink-0 text-[14px] text-[#404040] text-left text-nowrap">
</p>
</div>
</button>
<button className="bg-[#f7f7f7] box-border content-stretch flex gap-2 h-[34px] items-center pl-2 pr-2 py-1 relative rounded-lg shrink-0 w-[226px] cursor-pointer">
<div className="basis-0 content-stretch flex gap-2 grow items-center min-h-px min-w-px relative shrink-0 pl-2">
<p className="[white-space-collapse:collapse] basis-0 font-medium grow leading-[1.6] min-h-px min-w-px not-italic overflow-ellipsis overflow-hidden relative shrink-0 text-[14px] text-[#404040] text-left text-nowrap">
</p>
</div>
</button>
<button className="box-border content-stretch flex gap-2 h-[34px] items-center pl-2 pr-2 py-1 relative rounded-lg shrink-0 w-[226px] cursor-pointer hover:bg-gray-50 transition-colors">
<div className="basis-0 content-stretch flex gap-2 grow items-center min-h-px min-w-px relative shrink-0 pl-2">
<p className="[white-space-collapse:collapse] basis-0 font-medium grow leading-[1.6] min-h-px min-w-px not-italic overflow-ellipsis overflow-hidden relative shrink-0 text-[14px] text-[#404040] text-left text-nowrap">
</p>
</div>
</button>
<button className="box-border content-stretch flex gap-2 h-[34px] items-center pl-2 pr-2 py-1 relative rounded-lg shrink-0 w-[226px] cursor-pointer hover:bg-gray-50 transition-colors">
<div className="basis-0 content-stretch flex gap-2 grow items-center min-h-px min-w-px relative shrink-0 pl-2">
<p className="[white-space-collapse:collapse] basis-0 font-medium grow leading-[1.6] min-h-px min-w-px not-italic overflow-ellipsis overflow-hidden relative shrink-0 text-[14px] text-[#404040] text-left text-nowrap">
</p>
</div>
</button>
<button className="box-border content-stretch flex gap-2 h-[34px] items-center pl-2 pr-2 py-1 relative rounded-lg shrink-0 w-[226px] cursor-pointer hover:bg-gray-50 transition-colors">
<div className="basis-0 content-stretch flex gap-2 grow items-center min-h-px min-w-px relative shrink-0 pl-2">
<p className="[white-space-collapse:collapse] basis-0 font-medium grow leading-[1.6] min-h-px min-w-px not-italic overflow-ellipsis overflow-hidden relative shrink-0 text-[14px] text-[#404040] text-left text-nowrap">
</p>
</div>
</button>
<button className="box-border content-stretch flex gap-2 h-[34px] items-center pl-2 pr-2 py-1 relative rounded-lg shrink-0 w-[226px] cursor-pointer hover:bg-gray-50 transition-colors">
<div className="basis-0 content-stretch flex gap-2 grow items-center min-h-px min-w-px relative shrink-0 pl-2">
<p className="[white-space-collapse:collapse] basis-0 font-medium grow leading-[1.6] min-h-px min-w-px not-italic overflow-ellipsis overflow-hidden relative shrink-0 text-[14px] text-[#404040] text-left text-nowrap">
</p>
</div>
</button>
<button className="box-border content-stretch flex gap-2 h-[34px] items-center pl-2 pr-2 py-1 relative rounded-lg shrink-0 w-[226px] cursor-pointer hover:bg-gray-50 transition-colors">
<div className="basis-0 content-stretch flex gap-2 grow items-center min-h-px min-w-px relative shrink-0 pl-2">
<p className="[white-space-collapse:collapse] basis-0 font-medium grow leading-[1.6] min-h-px min-w-px not-italic overflow-ellipsis overflow-hidden relative shrink-0 text-[14px] text-[#404040] text-left text-nowrap">
/
</p>
</div>
</button>
<button className="box-border content-stretch flex gap-2 h-[34px] items-center pl-2 pr-2 py-1 relative rounded-lg shrink-0 w-[226px] cursor-pointer hover:bg-gray-50 transition-colors">
<div className="basis-0 content-stretch flex gap-2 grow items-center min-h-px min-w-px relative shrink-0 pl-2">
<p className="[white-space-collapse:collapse] basis-0 font-medium grow leading-[1.6] min-h-px min-w-px not-italic overflow-ellipsis overflow-hidden relative shrink-0 text-[14px] text-[#404040] text-left text-nowrap">
</p>
</div>
</button>
</div>
</div>
{/* 로그아웃 */}
<div className="content-stretch flex gap-[9px] items-center relative shrink-0">
<div className="flex items-center justify-center relative shrink-0">
<div className="flex-none rotate-[180deg] scale-y-[-100%]">
<div className="h-[23.12px] relative w-[22px]">
<Logout />
</div>
</div>
</div>
<button
onClick={() => {
localStorage.removeItem('isLoggedIn');
localStorage.removeItem('isAdminLoggedIn');
router.push('/');
}}
className="content-stretch flex h-[36px] items-center justify-between relative shrink-0 w-[76px] cursor-pointer hover:opacity-80 transition-opacity"
>
<div className="box-border content-stretch flex gap-[10px] items-center justify-center px-[10px] py-[5px] relative shrink-0">
<p className="font-medium leading-[1.6] not-italic relative shrink-0 text-[16px] text-[#404040] text-nowrap whitespace-pre">
</p>
</div>
</button>
</div>
</div>
{/* 메인 콘텐츠 */}
<div className="absolute left-[250px] top-0 right-0 min-h-screen">
{/* 헤더 */}
<div className="absolute content-stretch flex gap-[24px] items-center left-[48px] top-[45px]">
<div className="border-[#2b82e8] border-b-[2px] border-solid box-border content-stretch flex gap-[150px] items-center relative shrink-0">
<div className="box-border content-stretch flex gap-[10px] items-center justify-center p-[10px] relative shrink-0">
<p className="font-bold leading-[normal] not-italic relative shrink-0 text-[#515151] text-[18px] text-nowrap whitespace-pre">
</p>
</div>
</div>
</div>
{/* 강좌 등록 버튼 */}
<button
className="absolute bg-[#160e0e] box-border content-stretch flex gap-[10px] items-center justify-center right-[101px] p-[10px] rounded-[10px] top-[72px] w-[167px] cursor-pointer hover:bg-[#2a1f1f] transition-colors"
>
<p className="font-bold leading-[1.6] not-italic relative shrink-0 text-[18px] text-nowrap text-white whitespace-pre">
</p>
</button>
{/* 테이블 */}
<div className="absolute content-stretch flex flex-col items-start left-[48px] right-[101px] top-[186px]">
{/* 테이블 헤더 */}
<div className="bg-[rgba(235,247,255,0.5)] content-stretch flex h-[41px] items-center relative shrink-0 w-full">
<div className="content-stretch flex items-center relative shrink-0 w-full">
<div className="basis-0 border-[0.5px_0px_0.5px_0.5px] border-[#b9b9b9] border-solid box-border content-stretch flex gap-[10px] grow items-center min-h-px min-w-px p-[10px] relative shrink-0">
<div className="basis-0 flex flex-col font-bold grow justify-center leading-[0] min-h-px min-w-px not-italic relative shrink-0 text-[18px] text-[#515151]">
<p className="leading-[normal]"> </p>
</div>
</div>
<div className="basis-0 border-[0.5px_0px_0.5px_0.5px] border-[#b9b9b9] border-solid box-border content-stretch flex gap-[10px] grow items-center min-h-px min-w-px p-[10px] relative shrink-0">
<div className="basis-0 flex flex-col font-bold grow justify-center leading-[0] min-h-px min-w-px not-italic relative shrink-0 text-[18px] text-[#515151]">
<p className="leading-[normal]"></p>
</div>
</div>
<div className="basis-0 border-[0.5px_0px_0.5px_0.5px] border-[#b9b9b9] border-solid box-border content-stretch flex gap-[10px] grow items-center min-h-px min-w-px p-[10px] relative shrink-0">
<div className="basis-0 flex flex-col font-bold grow justify-center leading-[0] min-h-px min-w-px not-italic relative shrink-0 text-[18px] text-[#515151]">
<p className="leading-[normal]"> </p>
</div>
</div>
<div className="basis-0 border-[0.5px_0px_0.5px_0.5px] border-[#b9b9b9] border-solid box-border content-stretch flex gap-[10px] grow items-center min-h-px min-w-px p-[10px] relative shrink-0">
<div className="basis-0 flex flex-col font-bold grow justify-center leading-[0] min-h-px min-w-px not-italic relative shrink-0 text-[18px] text-[#515151]">
<p className="leading-[normal]"> </p>
</div>
</div>
<div className="basis-0 border-[0.5px_0px_0.5px_0.5px] border-[#b9b9b9] border-solid box-border content-stretch flex gap-[10px] grow items-center min-h-px min-w-px p-[10px] relative shrink-0">
<div className="basis-0 flex flex-col font-medium grow justify-center leading-[0] min-h-px min-w-px not-italic relative shrink-0 text-[18px] text-[#515151]">
<p className="leading-[normal]"></p>
</div>
</div>
<div className="basis-0 border-[0.5px_0px_0.5px_0.5px] border-[#b9b9b9] border-solid box-border content-stretch flex gap-[10px] grow items-center min-h-px min-w-px p-[10px] relative shrink-0">
<div className="basis-0 flex flex-col font-medium grow justify-center leading-[0] min-h-px min-w-px not-italic relative shrink-0 text-[18px] text-[#515151]">
<p className="leading-[normal]"></p>
</div>
</div>
</div>
</div>
{/* 빈 상태 또는 테이블 바디 */}
{lectures.length === 0 ? (
<div className="relative w-full flex items-center justify-center" style={{ minHeight: '400px' }}>
<div className="content-stretch flex flex-col gap-[16px] items-center relative shrink-0">
<div className="content-stretch flex flex-col gap-[2px] items-center relative shrink-0">
<p className="font-medium leading-[1.6] not-italic relative shrink-0 text-[16px] text-[#404040] text-nowrap whitespace-pre">
.
</p>
</div>
</div>
</div>
) : (
<div className="content-stretch flex flex-col items-start relative shrink-0 w-full">
{lectures.map((lecture) => (
<div key={lecture.id} className="bg-white content-stretch flex items-center relative shrink-0 w-full">
<div className="content-stretch flex items-center relative shrink-0 w-full">
<div className="basis-0 border-[0.5px_0px_0.5px_0.5px] border-[#b9b9b9] border-solid box-border content-stretch flex gap-[10px] grow items-center min-h-px min-w-px p-[10px] relative shrink-0">
<div className="basis-0 flex flex-col font-medium grow justify-center leading-[0] min-h-px min-w-px not-italic relative shrink-0 text-[18px] text-[#515151]">
<p className="leading-[normal]">{lecture.courseName}</p>
</div>
</div>
<div className="basis-0 border-[0.5px_0px_0.5px_0.5px] border-[#b9b9b9] border-solid box-border content-stretch flex gap-[10px] grow items-center min-h-px min-w-px p-[10px] relative shrink-0">
<div className="basis-0 flex flex-col font-medium grow justify-center leading-[0] min-h-px min-w-px not-italic relative shrink-0 text-[18px] text-[#515151]">
<p className="leading-[normal]">{lecture.lectureName}</p>
</div>
</div>
<div className="basis-0 border-[0.5px_0px_0.5px_0.5px] border-[#b9b9b9] border-solid box-border content-stretch flex gap-[10px] grow items-center min-h-px min-w-px p-[10px] relative shrink-0">
<div className="basis-0 flex flex-col font-medium grow justify-center leading-[0] min-h-px min-w-px not-italic relative shrink-0 text-[18px] text-[#515151]">
<p className="leading-[normal]">{lecture.attachedFile}</p>
</div>
</div>
<div className="basis-0 border-[0.5px_0px_0.5px_0.5px] border-[#b9b9b9] border-solid box-border content-stretch flex gap-[10px] grow items-center min-h-px min-w-px p-[10px] relative shrink-0">
<div className="basis-0 flex flex-col font-medium grow justify-center leading-[0] min-h-px min-w-px not-italic relative shrink-0 text-[18px] text-[#515151]">
<p className="leading-[normal]">{lecture.questionCount}</p>
</div>
</div>
<div className="basis-0 border-[0.5px_0px_0.5px_0.5px] border-[#b9b9b9] border-solid box-border content-stretch flex gap-[10px] grow items-center min-h-px min-w-px p-[10px] relative shrink-0">
<div className="basis-0 flex flex-col font-medium grow justify-center leading-[0] min-h-px min-w-px not-italic relative shrink-0 text-[18px] text-[#515151]">
<p className="leading-[normal]">{lecture.registrar}</p>
</div>
</div>
<div className="basis-0 border-[0.5px_0px_0.5px_0.5px] border-[#b9b9b9] border-solid box-border content-stretch flex gap-[10px] grow items-center min-h-px min-w-px p-[10px] relative shrink-0">
<div className="basis-0 flex flex-col font-medium grow justify-center leading-[0] min-h-px min-w-px not-italic relative shrink-0 text-[18px] text-[#515151]">
<p className="leading-[normal]">{lecture.createdAt}</p>
</div>
</div>
</div>
</div>
))}
</div>
)}
</div>
</div>
</div>
);
}

View File

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

View File

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

View File

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

148
app/api/lectures/route.ts Normal file
View File

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

View File

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

View File

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

View File

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

59
app/api/users/route.ts Normal file
View File

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

View File

@@ -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);

View File

@@ -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 <LoginPage />;
}
// 로그인되었으면 메인 페이지 표시
// 관리자일 경우 권한 설정 페이지 표시
if (isAdmin) {
return (
<div className="bg-white relative size-full min-h-screen">
{/* 사이드바 */}
<div className="absolute bg-white border-r border-[#eeeeee] border-solid box-border content-stretch flex flex-col gap-[45px] items-center left-0 min-h-[1080px] pb-8 pt-[30px] px-0 top-0 w-[250px]">
{/* 로고 */}
<button
onClick={() => router.push('/')}
className="h-[102px] relative shrink-0 w-[99px] cursor-pointer hover:opacity-80 transition-opacity"
>
<div className="absolute inset-0 flex items-center justify-center overflow-hidden pointer-events-none">
<img alt="로고" className="h-full w-full object-contain" src="/logo.svg" />
</div>
</button>
{/* 메뉴 */}
<div className="box-border content-stretch flex flex-col items-center pb-0 pt-4 px-0 relative shrink-0 w-[250px]">
<div className="box-border content-stretch flex flex-col gap-2 items-start p-3 relative shrink-0 w-full">
<button className="bg-[#f7f7f7] box-border content-stretch flex gap-2 h-[34px] items-center pl-2 pr-2 py-1 relative rounded-lg shrink-0 w-[226px] cursor-pointer">
<div className="basis-0 content-stretch flex gap-2 grow items-center min-h-px min-w-px relative shrink-0 pl-2">
<p className="[white-space-collapse:collapse] basis-0 font-medium grow leading-[1.6] min-h-px min-w-px not-italic overflow-ellipsis overflow-hidden relative shrink-0 text-[14px] text-[#404040] text-left text-nowrap">
</p>
</div>
</button>
<button
onClick={() => router.push('/admin_lecture1')}
className="box-border content-stretch flex gap-2 h-[34px] items-center pl-2 pr-2 py-1 relative rounded-lg shrink-0 w-[226px] cursor-pointer hover:bg-gray-50 transition-colors"
>
<div className="basis-0 content-stretch flex gap-2 grow items-center min-h-px min-w-px relative shrink-0 pl-2">
<p className="[white-space-collapse:collapse] basis-0 font-medium grow leading-[1.6] min-h-px min-w-px not-italic overflow-ellipsis overflow-hidden relative shrink-0 text-[14px] text-[#404040] text-left text-nowrap">
</p>
</div>
</button>
<button
onClick={() => router.push('/admin_lecture2')}
className="box-border content-stretch flex gap-2 h-[34px] items-center pl-2 pr-2 py-1 relative rounded-lg shrink-0 w-[226px] cursor-pointer hover:bg-gray-50 transition-colors"
>
<div className="basis-0 content-stretch flex gap-2 grow items-center min-h-px min-w-px relative shrink-0 pl-2">
<p className="[white-space-collapse:collapse] basis-0 font-medium grow leading-[1.6] min-h-px min-w-px not-italic overflow-ellipsis overflow-hidden relative shrink-0 text-[14px] text-[#404040] text-left text-nowrap">
</p>
</div>
</button>
<button className="box-border content-stretch flex gap-2 h-[34px] items-center pl-2 pr-2 py-1 relative rounded-lg shrink-0 w-[226px] cursor-pointer hover:bg-gray-50 transition-colors">
<div className="basis-0 content-stretch flex gap-2 grow items-center min-h-px min-w-px relative shrink-0 pl-2">
<p className="[white-space-collapse:collapse] basis-0 font-medium grow leading-[1.6] min-h-px min-w-px not-italic overflow-ellipsis overflow-hidden relative shrink-0 text-[14px] text-[#404040] text-left text-nowrap">
</p>
</div>
</button>
<button className="box-border content-stretch flex gap-2 h-[34px] items-center pl-2 pr-2 py-1 relative rounded-lg shrink-0 w-[226px] cursor-pointer hover:bg-gray-50 transition-colors">
<div className="basis-0 content-stretch flex gap-2 grow items-center min-h-px min-w-px relative shrink-0 pl-2">
<p className="[white-space-collapse:collapse] basis-0 font-medium grow leading-[1.6] min-h-px min-w-px not-italic overflow-ellipsis overflow-hidden relative shrink-0 text-[14px] text-[#404040] text-left text-nowrap">
</p>
</div>
</button>
<button className="box-border content-stretch flex gap-2 h-[34px] items-center pl-2 pr-2 py-1 relative rounded-lg shrink-0 w-[226px] cursor-pointer hover:bg-gray-50 transition-colors">
<div className="basis-0 content-stretch flex gap-2 grow items-center min-h-px min-w-px relative shrink-0 pl-2">
<p className="[white-space-collapse:collapse] basis-0 font-medium grow leading-[1.6] min-h-px min-w-px not-italic overflow-ellipsis overflow-hidden relative shrink-0 text-[14px] text-[#404040] text-left text-nowrap">
</p>
</div>
</button>
<button className="box-border content-stretch flex gap-2 h-[34px] items-center pl-2 pr-2 py-1 relative rounded-lg shrink-0 w-[226px] cursor-pointer hover:bg-gray-50 transition-colors">
<div className="basis-0 content-stretch flex gap-2 grow items-center min-h-px min-w-px relative shrink-0 pl-2">
<p className="[white-space-collapse:collapse] basis-0 font-medium grow leading-[1.6] min-h-px min-w-px not-italic overflow-ellipsis overflow-hidden relative shrink-0 text-[14px] text-[#404040] text-left text-nowrap">
</p>
</div>
</button>
<button className="box-border content-stretch flex gap-2 h-[34px] items-center pl-2 pr-2 py-1 relative rounded-lg shrink-0 w-[226px] cursor-pointer hover:bg-gray-50 transition-colors">
<div className="basis-0 content-stretch flex gap-2 grow items-center min-h-px min-w-px relative shrink-0 pl-2">
<p className="[white-space-collapse:collapse] basis-0 font-medium grow leading-[1.6] min-h-px min-w-px not-italic overflow-ellipsis overflow-hidden relative shrink-0 text-[14px] text-[#404040] text-left text-nowrap">
/
</p>
</div>
</button>
<button className="box-border content-stretch flex gap-2 h-[34px] items-center pl-2 pr-2 py-1 relative rounded-lg shrink-0 w-[226px] cursor-pointer hover:bg-gray-50 transition-colors">
<div className="basis-0 content-stretch flex gap-2 grow items-center min-h-px min-w-px relative shrink-0 pl-2">
<p className="[white-space-collapse:collapse] basis-0 font-medium grow leading-[1.6] min-h-px min-w-px not-italic overflow-ellipsis overflow-hidden relative shrink-0 text-[14px] text-[#404040] text-left text-nowrap">
</p>
</div>
</button>
</div>
</div>
{/* 로그아웃 */}
<div className="content-stretch flex gap-[9px] items-center relative shrink-0">
<div className="flex items-center justify-center relative shrink-0">
<div className="flex-none rotate-[180deg] scale-y-[-100%]">
<div className="h-[23.12px] relative w-[22px]">
<Logout />
</div>
</div>
</div>
<button
onClick={() => {
localStorage.removeItem('isLoggedIn');
localStorage.removeItem('isAdminLoggedIn');
router.push('/');
}}
className="content-stretch flex h-[36px] items-center justify-between relative shrink-0 w-[76px] cursor-pointer hover:opacity-80 transition-opacity"
>
<div className="box-border content-stretch flex gap-[10px] items-center justify-center px-[10px] py-[5px] relative shrink-0">
<p className="font-medium leading-[1.6] not-italic relative shrink-0 text-[16px] text-[#404040] text-nowrap whitespace-pre">
</p>
</div>
</button>
</div>
</div>
{/* 메인 콘텐츠 */}
<div className="absolute left-[250px] top-0 right-0 min-h-screen">
{/* 페이지 타이틀 탭 */}
<div className="absolute content-stretch flex gap-[24px] items-center left-[48px] top-[45px]">
<button
onClick={() => setSelectedTab('전체')}
className={`border-b-[2px] border-solid box-border content-stretch flex gap-[150px] items-center relative shrink-0 ${selectedTab === '전체' ? 'border-[#2b82e8]' : 'border-transparent'}`}
>
<div className="box-border content-stretch flex gap-[10px] items-center justify-center p-[10px] relative shrink-0">
<p className={`font-bold leading-[normal] not-italic relative shrink-0 text-[18px] text-nowrap whitespace-pre ${selectedTab === '전체' ? 'text-[#515151]' : 'text-[#515151]'}`}>
</p>
</div>
</button>
<button
onClick={() => setSelectedTab('학습자')}
className={`border-b-[2px] border-solid box-border content-stretch flex gap-[150px] items-center relative shrink-0 ${selectedTab === '학습자' ? 'border-[#2b82e8]' : 'border-transparent'}`}
>
<div className="box-border content-stretch flex gap-[10px] items-center justify-center p-[10px] relative shrink-0">
<p className="font-bold leading-[normal] not-italic relative shrink-0 text-[#515151] text-[18px] text-nowrap whitespace-pre">
</p>
</div>
</button>
<button
onClick={() => setSelectedTab('강사')}
className={`border-b-[2px] border-solid box-border content-stretch flex gap-[150px] items-center relative shrink-0 ${selectedTab === '강사' ? 'border-[#2b82e8]' : 'border-transparent'}`}
>
<div className="box-border content-stretch flex gap-[10px] items-center justify-center p-[10px] relative shrink-0">
<p className="font-bold leading-[normal] not-italic relative shrink-0 text-[#515151] text-[18px] text-nowrap whitespace-pre">
</p>
</div>
</button>
<button
onClick={() => setSelectedTab('운영자')}
className={`border-b-[2px] border-solid box-border content-stretch flex gap-[150px] items-center relative shrink-0 ${selectedTab === '운영자' ? 'border-[#2b82e8]' : 'border-transparent'}`}
>
<div className="box-border content-stretch flex gap-[10px] items-center justify-center p-[10px] relative shrink-0">
<p className="font-bold leading-[normal] not-italic relative shrink-0 text-[#515151] text-[18px] text-nowrap whitespace-pre">
</p>
</div>
</button>
</div>
{/* 사용자 목록 테이블 */}
<div className="absolute content-stretch flex flex-col items-start left-[48px] right-[101px] top-[135px]">
{/* 테이블 헤더 */}
<div className="bg-[rgba(235,247,255,0.5)] content-stretch flex h-[41px] items-center relative shrink-0 w-full">
<div className="content-stretch flex items-center relative shrink-0 w-full">
<div className="basis-0 border-[0.5px_0px_0.5px_0.5px] border-[#b9b9b9] border-solid box-border content-stretch flex gap-[10px] grow items-center min-h-px min-w-px p-[10px] relative shrink-0">
<div className="basis-0 flex flex-col font-bold grow justify-center leading-[0] min-h-px min-w-px not-italic relative shrink-0 text-[18px] text-[#515151]">
<p className="leading-[normal]"></p>
</div>
</div>
<div className="basis-0 border-[0.5px_0px_0.5px_0.5px] border-[#b9b9b9] border-solid box-border content-stretch flex gap-[10px] grow items-center min-h-px min-w-px p-[10px] relative shrink-0">
<div className="basis-0 flex flex-col font-bold grow justify-center leading-[0] min-h-px min-w-px not-italic relative shrink-0 text-[18px] text-[#515151]">
<p className="leading-[normal]"></p>
</div>
</div>
<div className="basis-0 border-[0.5px_0px_0.5px_0.5px] border-[#b9b9b9] border-solid box-border content-stretch flex gap-[10px] grow items-center min-h-px min-w-px p-[10px] relative shrink-0">
<div className="basis-0 flex flex-col font-bold grow justify-center leading-[0] min-h-px min-w-px not-italic relative shrink-0 text-[18px] text-[#515151]">
<p className="leading-[normal]">()</p>
</div>
</div>
<div className="basis-0 border-[0.5px_0px_0.5px_0.5px] border-[#b9b9b9] border-solid box-border content-stretch flex gap-[10px] grow items-center min-h-px min-w-px p-[10px] relative shrink-0">
<div className="basis-0 flex flex-col font-bold grow justify-center leading-[0] min-h-px min-w-px not-italic relative shrink-0 text-[18px] text-[#515151]">
<p className="leading-[normal]"></p>
</div>
</div>
<div className="basis-0 border-[0.5px_0px_0.5px_0.5px] border-[#b9b9b9] border-solid box-border content-stretch flex gap-[10px] grow items-center min-h-px min-w-px p-[10px] relative shrink-0">
<div className="basis-0 flex flex-col font-bold grow justify-center leading-[0] min-h-px min-w-px not-italic relative shrink-0 text-[18px] text-[#515151]">
<p className="leading-[normal]"> </p>
</div>
</div>
<div className="basis-0 border-[0.5px_0px_0.5px_0.5px] border-[#b9b9b9] border-solid box-border content-stretch flex gap-[10px] grow items-center min-h-px min-w-px p-[10px] relative shrink-0">
<div className="basis-0 flex flex-col font-bold grow justify-center leading-[0] min-h-px min-w-px not-italic relative shrink-0 text-[18px] text-[#515151]">
<p className="leading-[normal]"> </p>
</div>
</div>
</div>
</div>
{/* 빈 상태 메시지 */}
<div className="relative w-full flex items-center justify-center" style={{ minHeight: '400px' }}>
<div className="content-stretch flex flex-col gap-[16px] items-center relative shrink-0">
<div className="content-stretch flex flex-col gap-[2px] items-center relative shrink-0">
<p className="font-medium leading-[1.6] not-italic relative shrink-0 text-[16px] text-[#969696] text-nowrap whitespace-pre">
.
</p>
</div>
</div>
</div>
</div>
</div>
</div>
);
}
// 일반 사용자일 경우 기존 메인 페이지 표시
return (
<div className="bg-white relative min-h-screen w-full pb-[199px]">
{/* 헤더 */}

35
lib/auth.ts Normal file
View File

@@ -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;
}

14
lib/prisma.ts Normal file
View File

@@ -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;

View File

@@ -97,8 +97,3 @@ model UserLecture {
@@unique([userId, lectureId]) // 한 사용자는 같은 강좌를 중복 수강할 수 없음
@@map("user_lectures")
}
model Test {
id String @id @default(cuid())
name String
}

View File

@@ -1,8 +1,8 @@
export default function ChevronMiddle() {
return (
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_40000054_6401)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.7083 15.7073C12.5208 15.8948 12.2665 16.0001 12.0013 16.0001C11.7362 16.0001 11.4818 15.8948 11.2943 15.7073L5.63732 10.0503C5.54181 9.9581 5.46563 9.84775 5.41322 9.72575C5.36081 9.60374 5.33322 9.47252 5.33207 9.33974C5.33092 9.20696 5.35622 9.07529 5.4065 8.95239C5.45678 8.82949 5.53103 8.71784 5.62492 8.62395C5.71882 8.53006 5.83047 8.4558 5.95337 8.40552C6.07626 8.35524 6.20794 8.32994 6.34072 8.33109C6.4735 8.33225 6.60472 8.35983 6.72672 8.41224C6.84873 8.46465 6.95907 8.54083 7.05132 8.63634L12.0013 13.5863L16.9513 8.63634C17.1399 8.45418 17.3925 8.35339 17.6547 8.35567C17.9169 8.35795 18.1677 8.46312 18.3531 8.64852C18.5385 8.83393 18.6437 9.08474 18.646 9.34694C18.6483 9.60914 18.5475 9.86174 18.3653 10.0503L12.7083 15.7073Z" fill="#515151" />
<g clipPath="url(#clip0_40000054_6401)">
<path fillRule="evenodd" clipRule="evenodd" d="M12.7083 15.7073C12.5208 15.8948 12.2665 16.0001 12.0013 16.0001C11.7362 16.0001 11.4818 15.8948 11.2943 15.7073L5.63732 10.0503C5.54181 9.9581 5.46563 9.84775 5.41322 9.72575C5.36081 9.60374 5.33322 9.47252 5.33207 9.33974C5.33092 9.20696 5.35622 9.07529 5.4065 8.95239C5.45678 8.82949 5.53103 8.71784 5.62492 8.62395C5.71882 8.53006 5.83047 8.4558 5.95337 8.40552C6.07626 8.35524 6.20794 8.32994 6.34072 8.33109C6.4735 8.33225 6.60472 8.35983 6.72672 8.41224C6.84873 8.46465 6.95907 8.54083 7.05132 8.63634L12.0013 13.5863L16.9513 8.63634C17.1399 8.45418 17.3925 8.35339 17.6547 8.35567C17.9169 8.35795 18.1677 8.46312 18.3531 8.64852C18.5385 8.83393 18.6437 9.08474 18.646 9.34694C18.6483 9.60914 18.5475 9.86174 18.3653 10.0503L12.7083 15.7073Z" fill="#515151" />
</g>
<defs>
<clipPath id="clip0_40000054_6401">

View File

@@ -1,7 +1,7 @@
export default function ChevronSmall() {
return (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M8 10.2075L11.854 6.35448L11.147 5.64648L8 8.79348L4.854 5.64648L4.146 6.35448L8 10.2075Z" fill="#000000" />
<path fillRule="evenodd" clipRule="evenodd" d="M8 10.2075L11.854 6.35448L11.147 5.64648L8 8.79348L4.854 5.64648L4.146 6.35448L8 10.2075Z" fill="#000000" />
</svg>
);
}

View File

@@ -1,7 +1,7 @@
export default function Close() {
return (
<svg width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_40000054_6405)">
<g clipPath="url(#clip0_40000054_6405)">
<path d="M20.3033 22.0714L15 16.7681L9.6967 22.0714L7.92893 20.3036L13.2322 15.0003L7.92893 9.69704L9.6967 7.92928L15 13.2326L20.3033 7.92928L22.0711 9.69704L16.7678 15.0003L22.0711 20.3036L20.3033 22.0714Z" fill="#515151" />
</g>
<defs>

7
public/svg/logout.tsx Normal file
View File

@@ -0,0 +1,7 @@
export default function Logout() {
return (
<svg width="22" height="24" viewBox="0 0 22 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M22 23.1204V0H11V2.56893H19.5556V20.5514H11V23.1204H22ZM6.11111 17.9825L7.79167 16.12L4.675 12.8447H14.6667V10.2757H4.675L7.79167 7.00033L6.11111 5.13786L2.38419e-07 11.5602L6.11111 17.9825Z" fill="#606060" />
</svg>
);
}