관리자 계정 설정, 관리자 페이지 작업1
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
124
app/api/curriculums/[id]/route.ts
Normal file
124
app/api/curriculums/[id]/route.ts
Normal 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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
113
app/api/curriculums/route.ts
Normal file
113
app/api/curriculums/route.ts
Normal 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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
127
app/api/lectures/[id]/route.ts
Normal file
127
app/api/lectures/[id]/route.ts
Normal 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
148
app/api/lectures/route.ts
Normal 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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
152
app/api/user-lectures/[id]/route.ts
Normal file
152
app/api/user-lectures/[id]/route.ts
Normal 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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
182
app/api/user-lectures/route.ts
Normal file
182
app/api/user-lectures/route.ts
Normal 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
59
app/api/users/route.ts
Normal 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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
223
app/page.tsx
223
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 <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
35
lib/auth.ts
Normal 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
14
lib/prisma.ts
Normal 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;
|
||||
|
||||
@@ -97,8 +97,3 @@ model UserLecture {
|
||||
@@unique([userId, lectureId]) // 한 사용자는 같은 강좌를 중복 수강할 수 없음
|
||||
@@map("user_lectures")
|
||||
}
|
||||
|
||||
model Test {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
7
public/svg/logout.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user