Compare commits
15 Commits
efd5ab915e
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dae09ff2e7 | ||
|
|
0af1028b7b | ||
|
|
efdcde8b12 | ||
|
|
a7cad39b3c | ||
|
|
16d6cb5b30 | ||
|
|
8403684df3 | ||
|
|
c81f218a2b | ||
|
|
9d3ab9f8d8 | ||
|
|
e8f4094de6 | ||
|
|
94a186de59 | ||
|
|
137edf2d0d | ||
|
|
6600241bdb | ||
|
|
218e915e37 | ||
|
|
512acfb921 | ||
|
|
9a11a8afd1 |
12
.env
Normal file
12
.env
Normal file
@@ -0,0 +1,12 @@
|
||||
# Environment variables declared in this file are NOT automatically loaded by Prisma.
|
||||
# Please add `import "dotenv/config";` to your `prisma.config.ts` file, or use the Prisma CLI with Bun
|
||||
# to load environment variables from .env files: https://pris.ly/prisma-config-env-vars.
|
||||
|
||||
# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB.
|
||||
# See the documentation for all the connection string options: https://pris.ly/d/connection-strings
|
||||
|
||||
# The following `prisma+postgres` URL is similar to the URL produced by running a local Prisma Postgres
|
||||
# server with the `prisma dev` CLI command, when not choosing any non-default ports or settings. The API key, unlike the
|
||||
# one found in a remote Prisma Postgres URL, does not contain any sensitive information.
|
||||
|
||||
DATABASE_URL="prisma+postgres://localhost:51213/?api_key=eyJkYXRhYmFzZVVybCI6InBvc3RncmVzOi8vcG9zdGdyZXM6cG9zdGdyZXNAbG9jYWxob3N0OjUxMjE0L3RlbXBsYXRlMT9zc2xtb2RlPWRpc2FibGUmY29ubmVjdGlvbl9saW1pdD0xJmNvbm5lY3RfdGltZW91dD0wJm1heF9pZGxlX2Nvbm5lY3Rpb25fbGlmZXRpbWU9MCZwb29sX3RpbWVvdXQ9MCZzaW5nbGVfdXNlX2Nvbm5lY3Rpb25zPXRydWUmc29ja2V0X3RpbWVvdXQ9MCIsIm5hbWUiOiJkZWZhdWx0Iiwic2hhZG93RGF0YWJhc2VVcmwiOiJwb3N0Z3JlczovL3Bvc3RncmVzOnBvc3RncmVzQGxvY2FsaG9zdDo1MTIxNS90ZW1wbGF0ZTE_c3NsbW9kZT1kaXNhYmxlJmNvbm5lY3Rpb25fbGltaXQ9MSZjb25uZWN0X3RpbWVvdXQ9MCZtYXhfaWRsZV9jb25uZWN0aW9uX2xpZmV0aW1lPTAmcG9vbF90aW1lb3V0PTAmc2luZ2xlX3VzZV9jb25uZWN0aW9ucz10cnVlJnNvY2tldF90aW1lb3V0PTAifQ"
|
||||
17
.gitignore
vendored
17
.gitignore
vendored
@@ -16,7 +16,6 @@
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
@@ -31,7 +30,7 @@ yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# env files (can opt-in for committing if needed)
|
||||
.env*
|
||||
# .env*
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
@@ -39,3 +38,17 @@ yarn-error.log*
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
|
||||
/lib/generated/prisma
|
||||
|
||||
/lib/generated/prisma
|
||||
|
||||
/lib/generated/prisma
|
||||
|
||||
/lib/generated/prisma
|
||||
|
||||
/lib/generated/prisma
|
||||
|
||||
/lib/generated/prisma
|
||||
|
||||
/lib/generated/prisma
|
||||
|
||||
462
app/admin_home/page.tsx
Normal file
462
app/admin_home/page.tsx
Normal file
@@ -0,0 +1,462 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { isAdminLoggedIn } from '../../lib/auth';
|
||||
import LoginPage from '../login/page';
|
||||
import Logout from '../../public/svg/logout';
|
||||
|
||||
const imgArrowDisabled = "http://localhost:3845/assets/6edcb2defc36a2bf4a05a3abe53b8da3d42b2cb4.svg";
|
||||
const imgArrowDefault = "http://localhost:3845/assets/ad0cb4418492f1b020bb38a2ff038a331294ce87.svg";
|
||||
const imgArrowNext = "http://localhost:3845/assets/6328cf96ee1169c1425c2ce55e7a2dcca0374508.svg";
|
||||
|
||||
interface User {
|
||||
id: number;
|
||||
joinDate: string;
|
||||
name: string;
|
||||
email: string;
|
||||
role: string;
|
||||
accountStatus: string;
|
||||
accountManagement: string;
|
||||
}
|
||||
|
||||
type PaginationMove = "Previous" | "Next";
|
||||
type PaginationStatus = "Default" | "Disabled";
|
||||
|
||||
function PaginationBtnMove({ status = "Default", move = "Previous" }: { status?: PaginationStatus; move?: PaginationMove }) {
|
||||
const isDisabled = status === "Disabled";
|
||||
const isNext = move === "Next";
|
||||
|
||||
if (isDisabled && isNext) {
|
||||
return (
|
||||
<div className="border border-[#eeeeee] border-solid relative rounded-[8px] shrink-0 size-[30px]">
|
||||
<div className="overflow-clip relative rounded-[inherit] size-[30px]">
|
||||
<div className="absolute flex items-center justify-center left-[5px] size-[20px] top-[5px]">
|
||||
<div className="flex-none rotate-[180deg]">
|
||||
<div className="overflow-clip relative size-[20px]">
|
||||
<div className="absolute flex inset-[15.63%_30.21%] items-center justify-center">
|
||||
<div className="flex-none h-[16.5px] rotate-[180deg] w-[9.5px]">
|
||||
<div className="relative size-full">
|
||||
<div className="absolute inset-0">
|
||||
<img alt="" className="block max-w-none size-full" style={{ filter: 'opacity(0.88) brightness(0) saturate(100%) invert(88%)', opacity: 1 }} src={imgArrowNext} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isDisabled && !isNext) {
|
||||
return (
|
||||
<div className="border border-[#eeeeee] border-solid relative rounded-[8px] shrink-0 size-[30px]">
|
||||
<div className="overflow-clip relative rounded-[inherit] size-[30px]">
|
||||
<div className="absolute flex items-center justify-center left-[5px] size-[20px] top-[5px]">
|
||||
<div className="flex-none rotate-[180deg]">
|
||||
<div className="overflow-clip relative size-[20px]">
|
||||
<div className="absolute inset-[15.63%_30.21%]">
|
||||
<div className="absolute inset-0">
|
||||
<img alt="" className="block max-w-none size-full" style={{ filter: 'opacity(0.88) brightness(0) saturate(100%) invert(88%)', opacity: 1 }} src={imgArrowDefault} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isNext) {
|
||||
return (
|
||||
<div className="border border-[#eeeeee] border-solid relative rounded-[8px] shrink-0 size-[30px] cursor-pointer hover:bg-gray-50 transition-colors">
|
||||
<div className="overflow-clip relative rounded-[inherit] size-[30px]">
|
||||
<div className="absolute flex items-center justify-center left-[5px] size-[20px] top-[5px]">
|
||||
<div className="flex-none rotate-[180deg]">
|
||||
<div className="overflow-clip relative size-[20px]">
|
||||
<div className="absolute flex inset-[15.63%_30.21%] items-center justify-center">
|
||||
<div className="flex-none h-[16.5px] rotate-[180deg] w-[9.5px]">
|
||||
<div className="relative size-full">
|
||||
<div className="absolute inset-0">
|
||||
<img alt="" className="block max-w-none size-full" src={imgArrowNext} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="border border-[#eeeeee] border-solid relative rounded-[8px] shrink-0 size-[30px] cursor-pointer hover:bg-gray-50 transition-colors">
|
||||
<div className="overflow-clip relative rounded-[inherit] size-[30px]">
|
||||
<div className="absolute flex items-center justify-center left-[5px] size-[20px] top-[5px]">
|
||||
<div className="flex-none rotate-[180deg]">
|
||||
<div className="overflow-clip relative size-[20px]">
|
||||
<div className="absolute inset-[15.63%_30.21%]">
|
||||
<div className="absolute inset-0">
|
||||
<img alt="" className="block max-w-none size-full" src={imgArrowDefault} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function AdminHomePage() {
|
||||
const router = useRouter();
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
||||
const [selectedTab, setSelectedTab] = useState<'전체' | '학습자' | '강사' | '운영자'>('전체');
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
|
||||
// 샘플 사용자 데이터
|
||||
const [users, setUsers] = useState<User[]>([
|
||||
{ id: 39, joinDate: '2026-01-15', name: '홍길동', email: 'hong@example.com', role: '학습자', accountStatus: '활성', accountManagement: '관리' },
|
||||
{ id: 38, joinDate: '2026-01-14', name: '김철수', email: 'kim@example.com', role: '강사', accountStatus: '활성', accountManagement: '관리' },
|
||||
{ id: 37, joinDate: '2026-01-13', name: '이영희', email: 'lee@example.com', role: '학습자', accountStatus: '활성', accountManagement: '관리' },
|
||||
{ id: 36, joinDate: '2026-01-12', name: '박민수', email: 'park@example.com', role: '운영자', accountStatus: '활성', accountManagement: '관리' },
|
||||
{ id: 35, joinDate: '2026-01-11', name: '최지영', email: 'choi@example.com', role: '학습자', accountStatus: '활성', accountManagement: '관리' },
|
||||
{ id: 34, joinDate: '2026-01-10', name: '정대현', email: 'jung@example.com', role: '강사', accountStatus: '활성', accountManagement: '관리' },
|
||||
{ id: 33, joinDate: '2026-01-09', name: '강미영', email: 'kang@example.com', role: '학습자', accountStatus: '활성', accountManagement: '관리' },
|
||||
{ id: 32, joinDate: '2026-01-08', name: '윤성호', email: 'yoon@example.com', role: '학습자', accountStatus: '활성', accountManagement: '관리' },
|
||||
{ id: 31, joinDate: '2026-01-07', name: '임수진', email: 'lim@example.com', role: '강사', accountStatus: '활성', accountManagement: '관리' },
|
||||
{ id: 30, joinDate: '2026-01-06', name: '한지훈', email: 'han@example.com', role: '학습자', accountStatus: '활성', accountManagement: '관리' },
|
||||
{ id: 29, joinDate: '2026-01-05', name: '송민경', email: 'song@example.com', role: '학습자', accountStatus: '활성', accountManagement: '관리' },
|
||||
{ id: 28, joinDate: '2026-01-04', name: '오준혁', email: 'oh@example.com', role: '운영자', accountStatus: '활성', accountManagement: '관리' },
|
||||
{ id: 27, joinDate: '2026-01-03', name: '류현우', email: 'ryu@example.com', role: '학습자', accountStatus: '활성', accountManagement: '관리' },
|
||||
{ id: 26, joinDate: '2026-01-02', name: '신동욱', email: 'shin@example.com', role: '강사', accountStatus: '활성', accountManagement: '관리' },
|
||||
{ id: 25, joinDate: '2026-01-01', name: '조은서', email: 'cho@example.com', role: '학습자', accountStatus: '활성', accountManagement: '관리' },
|
||||
{ id: 24, joinDate: '2025-12-31', name: '배성민', email: 'bae@example.com', role: '학습자', accountStatus: '활성', accountManagement: '관리' },
|
||||
{ id: 23, joinDate: '2025-12-30', name: '전혜진', email: 'jeon@example.com', role: '강사', accountStatus: '활성', accountManagement: '관리' },
|
||||
{ id: 22, joinDate: '2025-12-29', name: '남궁준', email: 'namgung@example.com', role: '학습자', accountStatus: '활성', accountManagement: '관리' },
|
||||
{ id: 21, joinDate: '2025-12-28', name: '서아름', email: 'seo@example.com', role: '학습자', accountStatus: '활성', accountManagement: '관리' },
|
||||
{ id: 20, joinDate: '2025-12-27', name: '권태영', email: 'kwon@example.com', role: '운영자', accountStatus: '활성', accountManagement: '관리' },
|
||||
]);
|
||||
|
||||
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 />;
|
||||
}
|
||||
|
||||
const itemsPerPage = 13;
|
||||
const totalPages = Math.ceil(users.length / itemsPerPage);
|
||||
const pageNumbers = Array.from({ length: totalPages }, (_, i) => i + 1);
|
||||
const startIndex = (currentPage - 1) * itemsPerPage;
|
||||
const endIndex = startIndex + itemsPerPage;
|
||||
const currentUsers = users.slice(startIndex, endIndex);
|
||||
|
||||
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>
|
||||
{/* 로그아웃 */}
|
||||
<button
|
||||
onClick={() => {
|
||||
localStorage.removeItem('isLoggedIn');
|
||||
localStorage.removeItem('isAdminLoggedIn');
|
||||
router.push('/login');
|
||||
}}
|
||||
className="content-stretch flex gap-[9px] h-[36px] items-center relative shrink-0 cursor-pointer hover:opacity-80 transition-opacity"
|
||||
>
|
||||
<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>
|
||||
<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 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-[#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>
|
||||
<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] h-[671px] overflow-hidden">
|
||||
{/* 테이블 헤더 */}
|
||||
<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>
|
||||
|
||||
{/* 테이블 바디 */}
|
||||
{users.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-[#969696] text-nowrap whitespace-pre">
|
||||
존재하는 계정이 없어요.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="content-stretch flex flex-col items-start relative shrink-0 w-full max-h-[630px] overflow-y-auto">
|
||||
{currentUsers.map((user) => (
|
||||
<div key={user.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]">{user.joinDate}</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]">{user.name}</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]">{user.email}</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]">{user.role}</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]">{user.accountStatus}</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]">{user.accountManagement}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 페이지네이션 */}
|
||||
{users.length > 0 && (
|
||||
<div className="absolute content-stretch flex gap-2 items-center justify-center left-[48px] right-[101px] top-[909px]">
|
||||
<button onClick={() => setCurrentPage(Math.max(1, currentPage - 1))}>
|
||||
<PaginationBtnMove status={currentPage === 1 ? "Disabled" : "Default"} move="Previous" />
|
||||
</button>
|
||||
<div className="content-stretch flex gap-2 items-center relative shrink-0">
|
||||
{pageNumbers.map((pageNum) => (
|
||||
<button
|
||||
key={pageNum}
|
||||
onClick={() => setCurrentPage(pageNum)}
|
||||
className={`relative rounded-[8px] shrink-0 size-[30px] cursor-pointer transition-colors ${currentPage === pageNum
|
||||
? 'bg-[#1d1d1d]'
|
||||
: 'border border-[#eeeeee] border-solid hover:bg-gray-50'
|
||||
}`}
|
||||
>
|
||||
<div className="overflow-clip relative rounded-[inherit] size-[30px]">
|
||||
<div className={`absolute flex flex-col font-medium justify-center leading-[0] left-[15.5px] not-italic text-[14px] text-center text-nowrap top-[15px] translate-x-[-50%] translate-y-[-50%] ${currentPage === pageNum ? 'text-white' : 'text-[#515151]'
|
||||
}`}>
|
||||
<p className="leading-[1.6] whitespace-pre">{pageNum}</p>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<button onClick={() => setCurrentPage(Math.min(totalPages, currentPage + 1))}>
|
||||
<PaginationBtnMove status={currentPage === totalPages ? "Disabled" : "Default"} move="Next" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
644
app/admin_lecture1/page.tsx
Normal file
644
app/admin_lecture1/page.tsx
Normal file
@@ -0,0 +1,644 @@
|
||||
"use client";
|
||||
|
||||
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";
|
||||
const imgArrowDefault = "http://localhost:3845/assets/ad0cb4418492f1b020bb38a2ff038a331294ce87.svg";
|
||||
const imgArrowNext = "http://localhost:3845/assets/6328cf96ee1169c1425c2ce55e7a2dcca0374508.svg";
|
||||
const imgLogout = "http://localhost:3845/assets/2277a58aa2f582fd42132439745c5bb051f322c2.svg";
|
||||
const imgClose = "http://localhost:3845/assets/51206896b81ebe68b2d80b5b603e576227587007.svg";
|
||||
const imgCloseX = "http://localhost:3845/assets/91a45a4f2e47cd854574f32ee6ac1b84e12807cf.svg";
|
||||
const imgMingcuteDownLine = "http://localhost:3845/assets/a50ea05576e2bcccf052b5be6750edaf476ad4d4.svg";
|
||||
|
||||
interface Course {
|
||||
id: number;
|
||||
courseName: string;
|
||||
instructorName: string;
|
||||
isIncluded: string;
|
||||
createdAt: string;
|
||||
registrar: string;
|
||||
}
|
||||
|
||||
type PaginationMove = "Previous" | "Next";
|
||||
type PaginationStatus = "Default" | "Disabled";
|
||||
|
||||
function PaginationBtnMove({ status = "Default", move = "Previous" }: { status?: PaginationStatus; move?: PaginationMove }) {
|
||||
const isDisabled = status === "Disabled";
|
||||
const isNext = move === "Next";
|
||||
|
||||
if (isDisabled && isNext) {
|
||||
// Disabled Next 버튼
|
||||
return (
|
||||
<div className="border border-[#eeeeee] border-solid relative rounded-[8px] shrink-0 size-[30px]">
|
||||
<div className="overflow-clip relative rounded-[inherit] size-[30px]">
|
||||
<div className="absolute flex items-center justify-center left-[5px] size-[20px] top-[5px]">
|
||||
<div className="flex-none rotate-[180deg]">
|
||||
<div className="overflow-clip relative size-[20px]">
|
||||
<div className="absolute flex inset-[15.63%_30.21%] items-center justify-center">
|
||||
<div className="flex-none h-[16.5px] rotate-[180deg] w-[9.5px]">
|
||||
<div className="relative size-full">
|
||||
<div className="absolute inset-0">
|
||||
<img alt="" className="block max-w-none size-full" style={{ filter: 'opacity(0.88) brightness(0) saturate(100%) invert(88%)', opacity: 1 }} src={imgArrowNext} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isDisabled && !isNext) {
|
||||
// Disabled Previous 버튼
|
||||
return (
|
||||
<div className="border border-[#eeeeee] border-solid relative rounded-[8px] shrink-0 size-[30px]">
|
||||
<div className="overflow-clip relative rounded-[inherit] size-[30px]">
|
||||
<div className="absolute flex items-center justify-center left-[5px] size-[20px] top-[5px]">
|
||||
<div className="flex-none rotate-[180deg]">
|
||||
<div className="overflow-clip relative size-[20px]">
|
||||
<div className="absolute inset-[15.63%_30.21%]">
|
||||
<div className="absolute inset-0">
|
||||
<img alt="" className="block max-w-none size-full" style={{ filter: 'opacity(0.88) brightness(0) saturate(100%) invert(88%)', opacity: 1 }} src={imgArrowDefault} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isNext) {
|
||||
// Default Next 버튼
|
||||
return (
|
||||
<div className="border border-[#eeeeee] border-solid relative rounded-[8px] shrink-0 size-[30px] cursor-pointer hover:bg-gray-50 transition-colors">
|
||||
<div className="overflow-clip relative rounded-[inherit] size-[30px]">
|
||||
<div className="absolute flex items-center justify-center left-[5px] size-[20px] top-[5px]">
|
||||
<div className="flex-none rotate-[180deg]">
|
||||
<div className="overflow-clip relative size-[20px]">
|
||||
<div className="absolute flex inset-[15.63%_30.21%] items-center justify-center">
|
||||
<div className="flex-none h-[16.5px] rotate-[180deg] w-[9.5px]">
|
||||
<div className="relative size-full">
|
||||
<div className="absolute inset-0">
|
||||
<img alt="" className="block max-w-none size-full" src={imgArrowNext} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Default Previous 버튼
|
||||
return (
|
||||
<div className="border border-[#eeeeee] border-solid relative rounded-[8px] shrink-0 size-[30px] cursor-pointer hover:bg-gray-50 transition-colors">
|
||||
<div className="overflow-clip relative rounded-[inherit] size-[30px]">
|
||||
<div className="absolute flex items-center justify-center left-[5px] size-[20px] top-[5px]">
|
||||
<div className="flex-none rotate-[180deg]">
|
||||
<div className="overflow-clip relative size-[20px]">
|
||||
<div className="absolute inset-[15.63%_30.21%]">
|
||||
<div className="absolute inset-0">
|
||||
<img alt="" className="block max-w-none size-full" src={imgArrowDefault} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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('');
|
||||
const [instructorName, setInstructorName] = useState('');
|
||||
const [showInstructorDropdown, setShowInstructorDropdown] = useState(false);
|
||||
const [nextId, setNextId] = useState(14);
|
||||
|
||||
// 샘플 강좌 데이터
|
||||
const [courses, setCourses] = useState<Course[]>([
|
||||
{ id: 39, courseName: '방사선 적용', instructorName: '김강사', isIncluded: '포함', createdAt: '2026-01-15', registrar: '이철수' },
|
||||
{ id: 38, courseName: '방사선 적용', instructorName: '김강사', isIncluded: '포함', createdAt: '2026-01-15', registrar: '이철수' },
|
||||
{ id: 37, courseName: '방사선 적용', instructorName: '김강사', isIncluded: '포함', createdAt: '2026-01-15', registrar: '이철수' },
|
||||
{ id: 36, courseName: '방사선 적용', instructorName: '김강사', isIncluded: '포함', createdAt: '2026-01-15', registrar: '이철수' },
|
||||
{ id: 35, courseName: '방사선 적용', instructorName: '김강사', isIncluded: '포함', createdAt: '2026-01-15', registrar: '이철수' },
|
||||
{ id: 34, courseName: '방사선 적용', instructorName: '김강사', isIncluded: '포함', createdAt: '2026-01-15', registrar: '이철수' },
|
||||
{ id: 33, courseName: '방사선 적용', instructorName: '김강사', isIncluded: '포함', createdAt: '2026-01-15', registrar: '이철수' },
|
||||
{ id: 32, courseName: '방사선 적용', instructorName: '김강사', isIncluded: '포함', createdAt: '2026-01-15', registrar: '이철수' },
|
||||
{ id: 31, courseName: '방사선 적용', instructorName: '김강사', isIncluded: '포함', createdAt: '2026-01-15', registrar: '이철수' },
|
||||
{ id: 30, courseName: '방사선 적용', instructorName: '김강사', isIncluded: '포함', createdAt: '2026-01-15', registrar: '이철수' },
|
||||
{ id: 29, courseName: '방사선 적용', instructorName: '김강사', isIncluded: '포함', createdAt: '2026-01-15', registrar: '이철수' },
|
||||
{ id: 28, courseName: '방사선 적용', instructorName: '김강사', isIncluded: '포함', createdAt: '2026-01-15', registrar: '이철수' },
|
||||
{ id: 27, courseName: '방사선 적용', instructorName: '김강사', isIncluded: '포함', createdAt: '2026-01-15', registrar: '이철수' },
|
||||
{ id: 26, courseName: '방사선 원리', instructorName: '김강사', isIncluded: '포함', createdAt: '2025-12-11', registrar: '김등록' },
|
||||
{ id: 25, courseName: '방사선 원리', instructorName: '김강사', isIncluded: '포함', createdAt: '2025-12-11', registrar: '김등록' },
|
||||
{ id: 24, courseName: '방사선 적용', instructorName: '김강사', isIncluded: '포함', createdAt: '2026-01-15', registrar: '이철수' },
|
||||
{ id: 23, courseName: '방사선 적용', instructorName: '김강사', isIncluded: '포함', createdAt: '2026-01-15', registrar: '이철수' },
|
||||
{ id: 22, courseName: '방사선 적용', instructorName: '김강사', isIncluded: '포함', createdAt: '2026-01-15', registrar: '이철수' },
|
||||
{ id: 21, courseName: '방사선 적용', instructorName: '김강사', isIncluded: '포함', createdAt: '2026-01-15', registrar: '이철수' },
|
||||
{ id: 20, courseName: '방사선 적용', instructorName: '김강사', isIncluded: '포함', createdAt: '2026-01-15', registrar: '이철수' },
|
||||
{ id: 19, courseName: '방사선 적용', instructorName: '김강사', isIncluded: '포함', createdAt: '2026-01-15', registrar: '이철수' },
|
||||
{ id: 18, courseName: '방사선 적용', instructorName: '김강사', isIncluded: '포함', createdAt: '2026-01-15', registrar: '이철수' },
|
||||
{ id: 17, courseName: '방사선 적용', instructorName: '김강사', isIncluded: '포함', createdAt: '2026-01-15', registrar: '이철수' },
|
||||
{ id: 16, courseName: '방사선 적용', instructorName: '김강사', isIncluded: '포함', createdAt: '2026-01-15', registrar: '이철수' },
|
||||
{ id: 15, courseName: '방사선 적용', instructorName: '김강사', isIncluded: '포함', createdAt: '2026-01-15', registrar: '이철수' },
|
||||
{ id: 14, courseName: '방사선 적용', instructorName: '김강사', isIncluded: '포함', createdAt: '2026-01-15', registrar: '이철수' },
|
||||
{ id: 13, courseName: '방사선 원리', instructorName: '김강사', isIncluded: '포함', createdAt: '2025-12-11', registrar: '김등록' },
|
||||
{ id: 12, courseName: '방사선 원리', instructorName: '김강사', isIncluded: '포함', createdAt: '2025-12-11', registrar: '김등록' },
|
||||
{ id: 11, courseName: '방사선 적용', instructorName: '김강사', isIncluded: '포함', createdAt: '2026-01-15', registrar: '이철수' },
|
||||
{ id: 10, courseName: '방사선 적용', instructorName: '김강사', isIncluded: '포함', createdAt: '2026-01-15', registrar: '이철수' },
|
||||
{ id: 9, courseName: '방사선 적용', instructorName: '김강사', isIncluded: '포함', createdAt: '2026-01-15', registrar: '이철수' },
|
||||
{ id: 8, courseName: '방사선 적용', instructorName: '김강사', isIncluded: '포함', createdAt: '2026-01-15', registrar: '이철수' },
|
||||
{ id: 7, courseName: '방사선 적용', instructorName: '김강사', isIncluded: '포함', createdAt: '2026-01-15', registrar: '이철수' },
|
||||
{ id: 6, courseName: '방사선 적용', instructorName: '김강사', isIncluded: '포함', createdAt: '2026-01-15', registrar: '이철수' },
|
||||
{ id: 5, courseName: '방사선 적용', instructorName: '김강사', isIncluded: '포함', createdAt: '2026-01-15', registrar: '이철수' },
|
||||
{ id: 4, courseName: '방사선 적용', instructorName: '김강사', isIncluded: '포함', createdAt: '2026-01-15', registrar: '이철수' },
|
||||
{ id: 3, courseName: '방사선 적용', instructorName: '김강사', isIncluded: '포함', createdAt: '2026-01-15', registrar: '이철수' },
|
||||
{ id: 2, courseName: '방사선 적용', instructorName: '김강사', isIncluded: '포함', createdAt: '2026-01-15', registrar: '이철수' },
|
||||
{ id: 1, courseName: '방사선 적용', instructorName: '김강사', isIncluded: '포함', createdAt: '2026-01-15', registrar: '이철수' },
|
||||
]);
|
||||
|
||||
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('');
|
||||
setInstructorName('');
|
||||
};
|
||||
|
||||
const handleCloseModal = () => {
|
||||
setIsModalOpen(false);
|
||||
setCourseName('');
|
||||
setInstructorName('');
|
||||
setShowInstructorDropdown(false);
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
if (!courseName.trim() || !instructorName.trim()) {
|
||||
alert('교육 과정명과 강사를 입력해 주세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
const today = new Date();
|
||||
const formattedDate = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`;
|
||||
|
||||
const newCourse: Course = {
|
||||
id: nextId,
|
||||
courseName: courseName.trim(),
|
||||
instructorName: instructorName.trim(),
|
||||
isIncluded: '미포함',
|
||||
createdAt: formattedDate,
|
||||
registrar: '관리자',
|
||||
};
|
||||
|
||||
const updatedCourses = [...courses, newCourse];
|
||||
setCourses(updatedCourses);
|
||||
setNextId(nextId + 1);
|
||||
|
||||
// 13개를 넘어가면 해당 페이지로 이동
|
||||
const newTotalPages = Math.ceil(updatedCourses.length / 13);
|
||||
if (newTotalPages > 1 && updatedCourses.length > 13) {
|
||||
setCurrentPage(newTotalPages);
|
||||
}
|
||||
|
||||
handleCloseModal();
|
||||
};
|
||||
|
||||
const handleSelectInstructor = (instructor: string) => {
|
||||
setInstructorName(instructor);
|
||||
setShowInstructorDropdown(false);
|
||||
};
|
||||
|
||||
const itemsPerPage = 13;
|
||||
const totalPages = Math.ceil(courses.length / itemsPerPage);
|
||||
const pageNumbers = Array.from({ length: totalPages }, (_, i) => i + 1);
|
||||
const startIndex = (currentPage - 1) * itemsPerPage;
|
||||
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">
|
||||
{/* 사이드바 */}
|
||||
<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
|
||||
onClick={() => router.push('/admin_home')}
|
||||
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
|
||||
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>
|
||||
{/* 로그아웃 */}
|
||||
<button
|
||||
onClick={() => {
|
||||
localStorage.removeItem('isLoggedIn');
|
||||
localStorage.removeItem('isAdminLoggedIn');
|
||||
router.push('/login');
|
||||
}}
|
||||
className="content-stretch flex gap-[9px] h-[36px] items-center relative shrink-0 cursor-pointer hover:opacity-80 transition-opacity"
|
||||
>
|
||||
<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>
|
||||
<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 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
|
||||
onClick={handleOpenModal}
|
||||
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] h-[671px] overflow-hidden">
|
||||
{/* 테이블 헤더 */}
|
||||
<div className="bg-[#f5fbff] 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_0.5px_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_0.5px_0.5px_0px] 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_0.5px_0.5px_0px] 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_0.5px_0.5px_0px] 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_0.5px_0.5px_0px] 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="content-stretch flex flex-col items-start relative shrink-0 w-full max-h-[630px] overflow-y-auto">
|
||||
{currentCourses.map((course) => (
|
||||
<div key={course.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_0.5px_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]">{course.courseName}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="basis-0 border-[0.5px_0.5px_0.5px_0px] 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]">{course.instructorName}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="basis-0 border-[0.5px_0.5px_0.5px_0px] 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]">{course.isIncluded}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="basis-0 border-[0.5px_0.5px_0.5px_0px] 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]">{course.createdAt}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="basis-0 border-[0.5px_0.5px_0.5px_0px] 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]">{course.registrar}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 페이지네이션 */}
|
||||
<div className="absolute content-stretch flex gap-2 items-center justify-center left-[48px] right-[101px] top-[909px]">
|
||||
<button onClick={() => setCurrentPage(Math.max(1, currentPage - 1))}>
|
||||
<PaginationBtnMove status={currentPage === 1 ? "Disabled" : "Default"} move="Previous" />
|
||||
</button>
|
||||
<div className="content-stretch flex gap-2 items-center relative shrink-0">
|
||||
{pageNumbers.map((pageNum) => (
|
||||
<button
|
||||
key={pageNum}
|
||||
onClick={() => setCurrentPage(pageNum)}
|
||||
className={`relative rounded-[8px] shrink-0 size-[30px] cursor-pointer transition-colors ${currentPage === pageNum
|
||||
? 'bg-[#1d1d1d]'
|
||||
: 'border border-[#eeeeee] border-solid hover:bg-gray-50'
|
||||
}`}
|
||||
>
|
||||
<div className="overflow-clip relative rounded-[inherit] size-[30px]">
|
||||
<div className={`absolute flex flex-col font-medium justify-center leading-[0] left-[15.5px] not-italic text-[14px] text-center text-nowrap top-[15px] translate-x-[-50%] translate-y-[-50%] ${currentPage === pageNum ? 'text-white' : 'text-[#515151]'
|
||||
}`}>
|
||||
<p className="leading-[1.6] whitespace-pre">{pageNum}</p>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<button onClick={() => setCurrentPage(Math.min(totalPages, currentPage + 1))}>
|
||||
<PaginationBtnMove status={currentPage === totalPages ? "Disabled" : "Default"} move="Next" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 모달 팝업 */}
|
||||
{isModalOpen && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||
{/* 배경 오버레이 */}
|
||||
<div
|
||||
className="absolute inset-0 bg-[rgba(0,0,0,0.6)]"
|
||||
onClick={handleCloseModal}
|
||||
/>
|
||||
{/* 모달 컨텐츠 */}
|
||||
<div
|
||||
className="relative bg-[#f7f7f7] h-[504px] overflow-clip shadow-[0px_4px_4px_0px_rgba(0,0,0,0.25)] w-[683px]"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* 닫기 버튼 */}
|
||||
<button
|
||||
onClick={handleCloseModal}
|
||||
className="absolute left-[622px] overflow-clip size-[30px] top-[41px] cursor-pointer hover:opacity-80 transition-opacity z-10"
|
||||
>
|
||||
<div className="absolute inset-0 pointer-events-none flex items-center justify-center">
|
||||
<Close />
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{/* 저장 버튼 */}
|
||||
<button
|
||||
onClick={handleSave}
|
||||
className="absolute bg-[#599ded] box-border content-stretch flex gap-[10px] h-[50px] items-center justify-center left-[358px] p-[10px] rounded-[10px] top-[414px] w-[264px] cursor-pointer hover:bg-[#4a8ddc] transition-colors"
|
||||
>
|
||||
<p className="font-medium leading-[normal] not-italic relative shrink-0 text-[18px] text-nowrap text-white whitespace-pre">
|
||||
저장
|
||||
</p>
|
||||
</button>
|
||||
|
||||
{/* 취소 버튼 */}
|
||||
<button
|
||||
onClick={handleCloseModal}
|
||||
className="absolute bg-[#eeeeee] box-border content-stretch flex gap-[10px] h-[50px] items-center justify-center left-[62px] p-[10px] rounded-[10px] top-[414px] w-[264px] cursor-pointer hover:bg-[#e0e0e0] transition-colors"
|
||||
>
|
||||
<p className="font-medium leading-[normal] not-italic relative shrink-0 text-[#515151] text-[18px] text-nowrap whitespace-pre">
|
||||
취소
|
||||
</p>
|
||||
</button>
|
||||
|
||||
{/* 폼 컨텐츠 */}
|
||||
<div className="absolute content-stretch flex flex-col gap-[20px] items-start left-[31px] top-[41px] w-[621px]">
|
||||
<div className="content-stretch flex flex-col gap-[20px] items-start relative shrink-0 w-[621px]">
|
||||
{/* 제목 */}
|
||||
<div className="content-stretch flex gap-[10px] items-center relative shrink-0 w-full">
|
||||
<div className="flex flex-col font-bold justify-center leading-[0] not-italic relative shrink-0 text-[20px] text-[#404040] text-nowrap">
|
||||
<p className="leading-[1.6] whitespace-pre">교육 과정 등록</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 교육 과정명 */}
|
||||
<div className="content-stretch flex flex-col gap-[20px] items-start relative shrink-0 w-full">
|
||||
<div className="content-stretch flex gap-[16px] items-center relative shrink-0 w-full">
|
||||
<div className="content-stretch flex gap-[10px] items-center relative shrink-0 w-[177px]">
|
||||
<p className="font-medium leading-[normal] not-italic relative shrink-0 text-[#515151] text-[18px] text-nowrap whitespace-pre">
|
||||
교육 과정명<span className="text-[#e61a1a]"> *</span>
|
||||
</p>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
value={courseName}
|
||||
onChange={(e) => setCourseName(e.target.value)}
|
||||
placeholder="교육 과정명을 입력해 주세요."
|
||||
className="basis-0 border border-[#b9b9b9] border-solid box-border content-stretch flex gap-[10px] grow items-center justify-center min-h-px min-w-px p-[10px] relative rounded-[8px] shrink-0 bg-white text-[16px] text-[#515151] focus:outline-none focus:border-[#2b82e8]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 강사 */}
|
||||
<div className="content-stretch flex flex-col gap-[20px] items-start relative shrink-0 w-full">
|
||||
<div className="content-stretch flex gap-[16px] items-center relative shrink-0 w-full">
|
||||
<div className="content-stretch flex gap-[10px] items-center relative shrink-0 w-[177px]">
|
||||
<p className="font-medium leading-[normal] not-italic relative shrink-0 text-[#515151] text-[18px] text-nowrap whitespace-pre">
|
||||
강사<span className="text-[#e61a1a]"> *</span>
|
||||
</p>
|
||||
</div>
|
||||
<div className="basis-0 relative grow min-h-px min-w-px shrink-0">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowInstructorDropdown(!showInstructorDropdown)}
|
||||
className="basis-0 border border-[#b9b9b9] border-solid box-border content-stretch flex gap-[10px] grow items-center justify-between min-h-px min-w-px p-[10px] relative rounded-[8px] shrink-0 w-full bg-white cursor-pointer hover:border-[#2b82e8] transition-colors"
|
||||
>
|
||||
<p className={`basis-0 font-medium grow leading-[1.6] min-h-px min-w-px not-italic relative shrink-0 text-left text-[16px] ${instructorName ? 'text-[#515151]' : 'text-[#b9b9b9]'}`}>
|
||||
{instructorName || '강사를 선택해 주세요.'}
|
||||
</p>
|
||||
<div className="overflow-clip relative shrink-0 size-[24px]">
|
||||
<img alt="" className="block max-w-none size-full" src={imgMingcuteDownLine} />
|
||||
</div>
|
||||
</button>
|
||||
{showInstructorDropdown && (
|
||||
<div className="absolute top-full left-0 right-0 mt-1 bg-white box-border content-stretch flex flex-col gap-[4px] items-start p-[8px] rounded-[8px] shadow-[0px_8px_16px_-2px_rgba(27,33,44,0.12)] z-10">
|
||||
{instructors.map((instructor) => (
|
||||
<button
|
||||
key={instructor}
|
||||
type="button"
|
||||
onClick={() => handleSelectInstructor(instructor)}
|
||||
className={`box-border content-stretch flex gap-[10px] h-[38px] items-center overflow-clip p-[8px] relative rounded-[8px] shrink-0 w-full transition-colors ${instructorName === instructor ? 'bg-[#e3effc]' : 'bg-white hover:bg-[#f7f7f7]'}`}
|
||||
>
|
||||
<div className="basis-0 flex flex-col font-normal grow justify-center leading-[0] min-h-px min-w-px not-italic relative shrink-0 text-[14px] text-[#515151]">
|
||||
<p className="leading-[1.6]">{instructor}</p>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 교육 과정 이미지 */}
|
||||
<div className="content-stretch flex flex-col gap-[20px] items-start relative shrink-0 w-full">
|
||||
<div className="content-stretch flex gap-[20px] items-center relative shrink-0 w-full">
|
||||
<div className="basis-0 content-stretch flex gap-[16px] grow items-center min-h-px min-w-px relative shrink-0">
|
||||
<div className="content-stretch flex gap-[10px] items-center relative shrink-0 w-[177px]">
|
||||
<p className="font-medium leading-[normal] not-italic relative shrink-0 text-[#515151] text-[18px] text-nowrap whitespace-pre">
|
||||
교육 과정 이미지
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="content-stretch flex gap-[16px] items-center relative shrink-0">
|
||||
<button className="bg-[#2b82e8] box-border content-stretch flex gap-2 h-[36px] items-center justify-center overflow-clip px-5 py-[13px] relative rounded-[8px] shrink-0 w-[114px] cursor-pointer hover:bg-[#1f6fc4] transition-colors">
|
||||
<p className="font-medium leading-[1.6] not-italic relative shrink-0 text-[14px] text-white text-center text-nowrap whitespace-pre">
|
||||
첨부
|
||||
</p>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-[#f7f7f7] border border-[#eeeeee] border-solid box-border content-stretch flex flex-col gap-4 items-center px-[10px] py-[20px] relative rounded-[10px] shrink-0 w-full">
|
||||
<div className="content-stretch flex flex-col gap-[2px] items-center relative shrink-0">
|
||||
<div className="font-medium leading-[1.6] not-italic relative shrink-0 text-[#b9b9b9] text-[16px] text-center text-nowrap whitespace-pre">
|
||||
<p className="mb-0">30MB 미만의 교육 과정 이미지를 첨부해 주세요.</p>
|
||||
<p>첨부하지 않을 경우 기본 이미지가 노출돼요.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
436
app/admin_lecture2/page.tsx
Normal file
436
app/admin_lecture2/page.tsx
Normal file
@@ -0,0 +1,436 @@
|
||||
"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';
|
||||
|
||||
const imgArrowDisabled = "http://localhost:3845/assets/6edcb2defc36a2bf4a05a3abe53b8da3d42b2cb4.svg";
|
||||
const imgArrowDefault = "http://localhost:3845/assets/ad0cb4418492f1b020bb38a2ff038a331294ce87.svg";
|
||||
const imgArrowNext = "http://localhost:3845/assets/6328cf96ee1169c1425c2ce55e7a2dcca0374508.svg";
|
||||
|
||||
type PaginationMove = "Previous" | "Next";
|
||||
type PaginationStatus = "Default" | "Disabled";
|
||||
|
||||
function PaginationBtnMove({ status = "Default", move = "Previous" }: { status?: PaginationStatus; move?: PaginationMove }) {
|
||||
const isDisabled = status === "Disabled";
|
||||
const isNext = move === "Next";
|
||||
|
||||
if (isDisabled && isNext) {
|
||||
return (
|
||||
<div className="border border-[#eeeeee] border-solid relative rounded-[8px] shrink-0 size-[30px]">
|
||||
<div className="overflow-clip relative rounded-[inherit] size-[30px]">
|
||||
<div className="absolute flex items-center justify-center left-[5px] size-[20px] top-[5px]">
|
||||
<div className="flex-none rotate-[180deg]">
|
||||
<div className="overflow-clip relative size-[20px]">
|
||||
<div className="absolute flex inset-[15.63%_30.21%] items-center justify-center">
|
||||
<div className="flex-none h-[16.5px] rotate-[180deg] w-[9.5px]">
|
||||
<div className="relative size-full">
|
||||
<div className="absolute inset-0">
|
||||
<img alt="" className="block max-w-none size-full" style={{ filter: 'opacity(0.88) brightness(0) saturate(100%) invert(88%)', opacity: 1 }} src={imgArrowNext} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isDisabled && !isNext) {
|
||||
return (
|
||||
<div className="border border-[#eeeeee] border-solid relative rounded-[8px] shrink-0 size-[30px]">
|
||||
<div className="overflow-clip relative rounded-[inherit] size-[30px]">
|
||||
<div className="absolute flex items-center justify-center left-[5px] size-[20px] top-[5px]">
|
||||
<div className="flex-none rotate-[180deg]">
|
||||
<div className="overflow-clip relative size-[20px]">
|
||||
<div className="absolute inset-[15.63%_30.21%]">
|
||||
<div className="absolute inset-0">
|
||||
<img alt="" className="block max-w-none size-full" style={{ filter: 'opacity(0.88) brightness(0) saturate(100%) invert(88%)', opacity: 1 }} src={imgArrowDefault} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isNext) {
|
||||
return (
|
||||
<div className="border border-[#eeeeee] border-solid relative rounded-[8px] shrink-0 size-[30px] cursor-pointer hover:bg-gray-50 transition-colors">
|
||||
<div className="overflow-clip relative rounded-[inherit] size-[30px]">
|
||||
<div className="absolute flex items-center justify-center left-[5px] size-[20px] top-[5px]">
|
||||
<div className="flex-none rotate-[180deg]">
|
||||
<div className="overflow-clip relative size-[20px]">
|
||||
<div className="absolute flex inset-[15.63%_30.21%] items-center justify-center">
|
||||
<div className="flex-none h-[16.5px] rotate-[180deg] w-[9.5px]">
|
||||
<div className="relative size-full">
|
||||
<div className="absolute inset-0">
|
||||
<img alt="" className="block max-w-none size-full" src={imgArrowNext} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="border border-[#eeeeee] border-solid relative rounded-[8px] shrink-0 size-[30px] cursor-pointer hover:bg-gray-50 transition-colors">
|
||||
<div className="overflow-clip relative rounded-[inherit] size-[30px]">
|
||||
<div className="absolute flex items-center justify-center left-[5px] size-[20px] top-[5px]">
|
||||
<div className="flex-none rotate-[180deg]">
|
||||
<div className="overflow-clip relative size-[20px]">
|
||||
<div className="absolute inset-[15.63%_30.21%]">
|
||||
<div className="absolute inset-0">
|
||||
<img alt="" className="block max-w-none size-full" src={imgArrowDefault} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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 [currentPage, setCurrentPage] = useState(1);
|
||||
const [lectures, setLectures] = useState<Lecture[]>([
|
||||
{ id: 39, courseName: '방사선 적용', lectureName: '방사선 기본 원리', attachedFile: '파일1.pdf', questionCount: 10, registrar: '이철수', createdAt: '2026-01-15' },
|
||||
{ id: 38, courseName: '방사선 적용', lectureName: '방사선 안전 관리', attachedFile: '파일2.pdf', questionCount: 15, registrar: '이철수', createdAt: '2026-01-14' },
|
||||
{ id: 37, courseName: '방사선 원리', lectureName: '방사선 측정 기법', attachedFile: '파일3.pdf', questionCount: 12, registrar: '김등록', createdAt: '2026-01-13' },
|
||||
{ id: 36, courseName: '방사선 적용', lectureName: '방사선 장비 사용법', attachedFile: '파일4.pdf', questionCount: 8, registrar: '이철수', createdAt: '2026-01-12' },
|
||||
{ id: 35, courseName: '방사선 원리', lectureName: '방사선 물리학 기초', attachedFile: '파일5.pdf', questionCount: 20, registrar: '김등록', createdAt: '2026-01-11' },
|
||||
{ id: 34, courseName: '방사선 적용', lectureName: '방사선 보호 장비', attachedFile: '파일6.pdf', questionCount: 14, registrar: '이철수', createdAt: '2026-01-10' },
|
||||
{ id: 33, courseName: '방사선 적용', lectureName: '방사선 응용 실습', attachedFile: '파일7.pdf', questionCount: 16, registrar: '이철수', createdAt: '2026-01-09' },
|
||||
{ id: 32, courseName: '방사선 원리', lectureName: '방사선 화학 반응', attachedFile: '파일8.pdf', questionCount: 11, registrar: '김등록', createdAt: '2026-01-08' },
|
||||
{ id: 31, courseName: '방사선 적용', lectureName: '방사선 의료 응용', attachedFile: '파일9.pdf', questionCount: 18, registrar: '이철수', createdAt: '2026-01-07' },
|
||||
{ id: 30, courseName: '방사선 적용', lectureName: '방사선 산업 응용', attachedFile: '파일10.pdf', questionCount: 13, registrar: '이철수', createdAt: '2026-01-06' },
|
||||
{ id: 29, courseName: '방사선 원리', lectureName: '방사선 생물학', attachedFile: '파일11.pdf', questionCount: 17, registrar: '김등록', createdAt: '2026-01-05' },
|
||||
{ id: 28, courseName: '방사선 적용', lectureName: '방사선 환경 모니터링', attachedFile: '파일12.pdf', questionCount: 9, registrar: '이철수', createdAt: '2026-01-04' },
|
||||
{ id: 27, courseName: '방사선 적용', lectureName: '방사선 검사 기법', attachedFile: '파일13.pdf', questionCount: 19, registrar: '이철수', createdAt: '2026-01-03' },
|
||||
{ id: 26, courseName: '방사선 원리', lectureName: '방사선 에너지 전달', attachedFile: '파일14.pdf', questionCount: 21, registrar: '김등록', createdAt: '2026-01-02' },
|
||||
{ id: 25, courseName: '방사선 적용', lectureName: '방사선 처리 기술', attachedFile: '파일15.pdf', questionCount: 7, registrar: '이철수', createdAt: '2026-01-01' },
|
||||
{ id: 24, courseName: '방사선 적용', lectureName: '방사선 품질 관리', attachedFile: '파일16.pdf', questionCount: 22, registrar: '이철수', createdAt: '2025-12-31' },
|
||||
{ id: 23, courseName: '방사선 원리', lectureName: '방사선 방어 이론', attachedFile: '파일17.pdf', questionCount: 6, registrar: '김등록', createdAt: '2025-12-30' },
|
||||
{ id: 22, courseName: '방사선 적용', lectureName: '방사선 계측법', attachedFile: '파일18.pdf', questionCount: 24, registrar: '이철수', createdAt: '2025-12-29' },
|
||||
{ id: 21, courseName: '방사선 적용', lectureName: '방사선 안전 규정', attachedFile: '파일19.pdf', questionCount: 5, registrar: '이철수', createdAt: '2025-12-28' },
|
||||
{ id: 20, courseName: '방사선 원리', lectureName: '방사선 핵물리학', attachedFile: '파일20.pdf', questionCount: 25, registrar: '김등록', createdAt: '2025-12-27' },
|
||||
]);
|
||||
|
||||
// 관리자 인증 확인
|
||||
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 />;
|
||||
}
|
||||
|
||||
const itemsPerPage = 13;
|
||||
const totalPages = Math.ceil(lectures.length / itemsPerPage);
|
||||
const pageNumbers = Array.from({ length: totalPages }, (_, i) => i + 1);
|
||||
const startIndex = (currentPage - 1) * itemsPerPage;
|
||||
const endIndex = startIndex + itemsPerPage;
|
||||
const currentLectures = lectures.slice(startIndex, endIndex);
|
||||
|
||||
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
|
||||
onClick={() => router.push('/admin_home')}
|
||||
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>
|
||||
{/* 로그아웃 */}
|
||||
<button
|
||||
onClick={() => {
|
||||
localStorage.removeItem('isLoggedIn');
|
||||
localStorage.removeItem('isAdminLoggedIn');
|
||||
router.push('/login');
|
||||
}}
|
||||
className="content-stretch flex gap-[9px] h-[36px] items-center relative shrink-0 cursor-pointer hover:opacity-80 transition-opacity"
|
||||
>
|
||||
<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>
|
||||
<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 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] h-[671px] overflow-hidden">
|
||||
{/* 테이블 헤더 */}
|
||||
<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 max-h-[630px] overflow-y-auto">
|
||||
{currentLectures.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>
|
||||
|
||||
{/* 페이지네이션 */}
|
||||
{lectures.length > 0 && (
|
||||
<div className="absolute content-stretch flex gap-2 items-center justify-center left-[48px] right-[101px] top-[909px]">
|
||||
<button onClick={() => setCurrentPage(Math.max(1, currentPage - 1))}>
|
||||
<PaginationBtnMove status={currentPage === 1 ? "Disabled" : "Default"} move="Previous" />
|
||||
</button>
|
||||
<div className="content-stretch flex gap-2 items-center relative shrink-0">
|
||||
{pageNumbers.map((pageNum) => (
|
||||
<button
|
||||
key={pageNum}
|
||||
onClick={() => setCurrentPage(pageNum)}
|
||||
className={`relative rounded-[8px] shrink-0 size-[30px] cursor-pointer transition-colors ${currentPage === pageNum
|
||||
? 'bg-[#1d1d1d]'
|
||||
: 'border border-[#eeeeee] border-solid hover:bg-gray-50'
|
||||
}`}
|
||||
>
|
||||
<div className="overflow-clip relative rounded-[inherit] size-[30px]">
|
||||
<div className={`absolute flex flex-col font-medium justify-center leading-[0] left-[15.5px] not-italic text-[14px] text-center text-nowrap top-[15px] translate-x-[-50%] translate-y-[-50%] ${currentPage === pageNum ? 'text-white' : 'text-[#515151]'
|
||||
}`}>
|
||||
<p className="leading-[1.6] whitespace-pre">{pageNum}</p>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<button onClick={() => setCurrentPage(Math.min(totalPages, currentPage + 1))}>
|
||||
<PaginationBtnMove status={currentPage === totalPages ? "Disabled" : "Default"} move="Next" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
182
app/announcement/page.tsx
Normal file
182
app/announcement/page.tsx
Normal file
@@ -0,0 +1,182 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import Image from 'next/image';
|
||||
import Header from '../components/Header';
|
||||
import logo from '../logo.svg';
|
||||
|
||||
const imgLine2 = "http://localhost:3845/assets/6ee8cf4ebb6bc2adb14aab8c9940b3002c20af35.svg";
|
||||
const imgFooterLogo = "http://localhost:3845/assets/89fda8e949171025b1232bae70fc9d442e4e70c8.png";
|
||||
|
||||
export default function AnnouncementPage() {
|
||||
const router = useRouter();
|
||||
const [isLoggedIn, setIsLoggedIn] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [announcements, setAnnouncements] = useState<any[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
// 로그인 상태 확인
|
||||
const loginStatus = localStorage.getItem('isLoggedIn') === 'true';
|
||||
setIsLoggedIn(loginStatus);
|
||||
setIsLoading(false);
|
||||
|
||||
// TODO: DB에서 공지사항 목록 가져오기
|
||||
// const fetchAnnouncements = async () => {
|
||||
// const response = await fetch('/api/announcements');
|
||||
// const data = await response.json();
|
||||
// setAnnouncements(data);
|
||||
// };
|
||||
// fetchAnnouncements();
|
||||
}, []);
|
||||
|
||||
if (isLoading) {
|
||||
return null; // 로딩 중
|
||||
}
|
||||
|
||||
// 로그인되지 않았으면 로그인 페이지로 리다이렉트
|
||||
if (!isLoggedIn) {
|
||||
router.push('/login');
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-white relative min-h-screen w-full">
|
||||
{/* 헤더 */}
|
||||
<Header activePage="announcement" />
|
||||
|
||||
{/* 구분선 */}
|
||||
<div className="absolute h-0 left-1/2 top-[150px] translate-x-[-50%] w-[1920px]">
|
||||
<div className="absolute bottom-0 left-0 right-0 top-[-1px]">
|
||||
<img alt="" className="block max-w-none size-full" src={imgLine2} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 페이지 제목 */}
|
||||
<p className="absolute font-bold leading-[normal] left-[calc(18.75%-11.44px)] not-italic text-[#515151] text-[24px] text-nowrap top-[239px] whitespace-pre">
|
||||
공지사항 ({announcements.length}건)
|
||||
</p>
|
||||
|
||||
{/* 테이블 컨테이너 */}
|
||||
<div className="absolute content-stretch flex flex-col items-start left-[calc(18.75%-7.44px)] top-[344px] w-[1330px]">
|
||||
{/* 테이블 헤더 */}
|
||||
<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] 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] 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] 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] 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] 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>
|
||||
|
||||
{/* 테이블 내용 */}
|
||||
{announcements.length > 0 ? (
|
||||
<div className="content-stretch flex flex-col items-start relative shrink-0 w-full">
|
||||
{announcements.map((announcement) => (
|
||||
<div key={announcement.id} className="content-stretch flex items-center relative shrink-0 w-full border-[0.5px] border-[#b9b9b9] border-solid">
|
||||
<div className="basis-0 border-[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">
|
||||
<p className="font-medium leading-[normal] not-italic relative shrink-0 text-[#515151] text-[18px]">
|
||||
{announcement.id}
|
||||
</p>
|
||||
</div>
|
||||
<div className="basis-0 border-[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">
|
||||
<p className="font-medium leading-[normal] not-italic relative shrink-0 text-[#515151] text-[18px]">
|
||||
{announcement.title}
|
||||
</p>
|
||||
</div>
|
||||
<div className="basis-0 border-[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">
|
||||
<p className="font-medium leading-[normal] not-italic relative shrink-0 text-[#515151] text-[18px]">
|
||||
{announcement.createdAt ? new Date(announcement.createdAt).toLocaleDateString('ko-KR') : '-'}
|
||||
</p>
|
||||
</div>
|
||||
<div className="basis-0 border-[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">
|
||||
<p className="font-medium leading-[normal] not-italic relative shrink-0 text-[#515151] text-[18px]">
|
||||
{announcement.author || '-'}
|
||||
</p>
|
||||
</div>
|
||||
<div className="basis-0 border-[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">
|
||||
<p className="font-medium leading-[normal] not-italic relative shrink-0 text-[#515151] text-[18px]">
|
||||
{announcement.views || 0}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="content-stretch flex items-center justify-center relative shrink-0 w-full py-[40px] border-[0.5px] border-[#b9b9b9] border-solid">
|
||||
<p className="font-medium leading-[normal] not-italic relative shrink-0 text-[#b9b9b9] text-[18px]">
|
||||
등록된 공지사항이 없습니다.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 푸터 */}
|
||||
<footer className="absolute bg-[#f7f7f7] box-border content-stretch flex flex-col gap-[10px] h-[225px] items-start left-0 px-[243px] py-[39px] top-[855px] w-[1920px]">
|
||||
<div className="content-stretch flex gap-[49px] items-center relative shrink-0">
|
||||
<div className="h-[74px] relative shrink-0 w-[72px]">
|
||||
<Image
|
||||
src={logo}
|
||||
alt="로고"
|
||||
className="w-full h-full object-contain"
|
||||
width={72}
|
||||
height={74}
|
||||
/>
|
||||
</div>
|
||||
<div className="content-stretch flex flex-col gap-[5px] items-start relative shrink-0 w-[479px]">
|
||||
<div className="content-stretch flex gap-[27px] items-center relative shrink-0 w-full">
|
||||
<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 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 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 className="box-border content-stretch flex flex-col font-medium gap-[10px] items-center justify-center leading-[normal] not-italic p-[10px] relative shrink-0 text-[#515151] text-[16px]">
|
||||
<p className="relative shrink-0 w-[400px]">
|
||||
(12345) 서울특별시 광진구 구의동 123-12(구의타워1)
|
||||
</p>
|
||||
<p className="relative shrink-0 w-[400px]">
|
||||
문의: 1234-1234 (평일 09:00 ~ 18:00)
|
||||
</p>
|
||||
<p className="relative shrink-0 w-[400px]">
|
||||
이메일: qwer1234@go.or.kr
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
110
app/components/Header.tsx
Normal file
110
app/components/Header.tsx
Normal file
@@ -0,0 +1,110 @@
|
||||
"use client";
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
import Image from 'next/image';
|
||||
import logo from '../logo.svg';
|
||||
|
||||
type ActivePage = 'home' | 'lecturelist' | 'studydata' | 'announcement' | null;
|
||||
|
||||
interface HeaderProps {
|
||||
activePage?: ActivePage;
|
||||
}
|
||||
|
||||
export default function Header({ activePage = null }: HeaderProps) {
|
||||
const router = useRouter();
|
||||
|
||||
const handleNoticeClick = () => {
|
||||
router.push('/announcement');
|
||||
};
|
||||
|
||||
const handleLogout = () => {
|
||||
localStorage.removeItem('isLoggedIn');
|
||||
router.push('/');
|
||||
};
|
||||
|
||||
const getMenuTextColor = (page: 'lecturelist' | 'studydata' | 'notice') => {
|
||||
if (page === 'lecturelist' && activePage === 'lecturelist') {
|
||||
return 'text-[#1669ca]';
|
||||
}
|
||||
if (page === 'studydata' && activePage === 'studydata') {
|
||||
return 'text-[#1669ca]';
|
||||
}
|
||||
if (page === 'notice' && activePage === 'announcement') {
|
||||
return 'text-[#1669ca]';
|
||||
}
|
||||
return 'text-[#515151] group-hover:text-blue-500';
|
||||
};
|
||||
|
||||
return (
|
||||
<header className="absolute content-stretch flex items-center justify-between left-[332px] top-[43px] w-[1332px]">
|
||||
<div className="content-stretch flex gap-[99px] items-center relative shrink-0">
|
||||
{/* 로고 */}
|
||||
<button
|
||||
onClick={() => router.push('/')}
|
||||
className="h-[74px] relative shrink-0 w-[72px] cursor-pointer"
|
||||
>
|
||||
<Image
|
||||
src={logo}
|
||||
alt="로고"
|
||||
className="w-full h-full object-contain"
|
||||
width={72}
|
||||
height={74}
|
||||
/>
|
||||
</button>
|
||||
{/* 메뉴 */}
|
||||
<div className="content-stretch flex gap-[24px] items-center relative shrink-0">
|
||||
<button
|
||||
onClick={() => router.push('/lecturelist')}
|
||||
className="content-stretch flex gap-[150px] items-center relative shrink-0 cursor-pointer group transition-colors"
|
||||
>
|
||||
<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-[24px] text-nowrap whitespace-pre transition-colors ${getMenuTextColor('lecturelist')}`}>
|
||||
교육 과정 목록
|
||||
</p>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => router.push('/studydata')}
|
||||
className="content-stretch flex gap-[150px] items-center relative shrink-0 cursor-pointer group transition-colors"
|
||||
>
|
||||
<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-[24px] text-nowrap whitespace-pre transition-colors ${getMenuTextColor('studydata')}`}>
|
||||
학습 자료실
|
||||
</p>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
onClick={handleNoticeClick}
|
||||
className="content-stretch flex gap-[150px] items-center relative shrink-0 cursor-pointer group transition-colors"
|
||||
>
|
||||
<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-[24px] text-nowrap whitespace-pre transition-colors ${getMenuTextColor('notice')}`}>
|
||||
공지사항
|
||||
</p>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/* 사용자 메뉴 */}
|
||||
<div className="content-stretch flex gap-[20px] items-center relative shrink-0">
|
||||
<button
|
||||
onClick={() => router.push('/myinfo')}
|
||||
className="box-border content-stretch flex gap-[10px] items-center justify-center p-[10px] relative shrink-0 cursor-pointer group transition-colors"
|
||||
>
|
||||
<p className="font-medium leading-[normal] not-italic relative shrink-0 text-[#515151] text-[18px] text-nowrap whitespace-pre group-hover:text-blue-500 transition-colors">
|
||||
내 강좌실
|
||||
</p>
|
||||
</button>
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="box-border content-stretch flex gap-[10px] items-center justify-center p-[10px] relative shrink-0 cursor-pointer group transition-colors"
|
||||
>
|
||||
<p className="font-medium leading-[normal] not-italic relative shrink-0 text-[#515151] text-[18px] text-nowrap whitespace-pre group-hover:text-blue-500 transition-colors">
|
||||
로그아웃
|
||||
</p>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import Image from 'next/image';
|
||||
import Header from '../components/Header';
|
||||
import logo from '../logo.svg';
|
||||
|
||||
const imgLine2 = "http://localhost:3845/assets/6ee8cf4ebb6bc2adb14aab8c9940b3002c20af35.svg";
|
||||
|
||||
export default function LectureListPage() {
|
||||
const router = useRouter();
|
||||
const [isLoggedIn, setIsLoggedIn] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [curriculums, setCurriculums] = useState<any[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
// 로그인 상태 확인
|
||||
const loginStatus = localStorage.getItem('isLoggedIn') === 'true';
|
||||
setIsLoggedIn(loginStatus);
|
||||
setIsLoading(false);
|
||||
|
||||
// TODO: DB에서 교육 과정 목록 가져오기
|
||||
// const fetchCurriculums = async () => {
|
||||
// const response = await fetch('/api/curriculums');
|
||||
// const data = await response.json();
|
||||
// setCurriculums(data);
|
||||
// };
|
||||
// fetchCurriculums();
|
||||
}, []);
|
||||
|
||||
if (isLoading) {
|
||||
return null; // 로딩 중
|
||||
}
|
||||
|
||||
// 로그인되지 않았으면 로그인 페이지로 리다이렉트
|
||||
if (!isLoggedIn) {
|
||||
router.push('/login');
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-white relative min-h-screen w-full pb-[199px]">
|
||||
{/* 헤더 */}
|
||||
<Header activePage="lecturelist" />
|
||||
|
||||
{/* 구분선 */}
|
||||
<div className="absolute h-0 left-1/2 top-[150px] translate-x-[-50%] w-[1920px]">
|
||||
<div className="absolute bottom-0 left-0 right-0 top-[-1px]">
|
||||
<img alt="" className="block max-w-none size-full" src={imgLine2} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 메인 콘텐츠 */}
|
||||
<div className="absolute content-stretch flex flex-col items-start left-[calc(6.25%+79px)] top-[270px] w-[1521px]">
|
||||
{/* 페이지 제목 */}
|
||||
<div className="content-stretch flex items-center relative shrink-0 mb-[20px]">
|
||||
<p className="font-bold leading-[normal] not-italic relative shrink-0 text-[#515151] text-[28px]">
|
||||
교육 과정 목록 ({curriculums.length}건)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 테이블 헤더 */}
|
||||
<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>
|
||||
</div>
|
||||
|
||||
{/* 테이블 내용 */}
|
||||
{curriculums.length > 0 ? (
|
||||
<div className="content-stretch flex flex-col items-start relative shrink-0 w-full">
|
||||
{curriculums.map((curriculum) => (
|
||||
<div key={curriculum.id} className="content-stretch flex items-center relative shrink-0 w-full border-[0.5px] border-[#b9b9b9] border-solid">
|
||||
<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">
|
||||
<p className="font-medium leading-[normal] not-italic relative shrink-0 text-[#515151] text-[18px]">
|
||||
{curriculum.title}
|
||||
</p>
|
||||
</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">
|
||||
<p className="font-medium leading-[normal] not-italic relative shrink-0 text-[#515151] text-[18px]">
|
||||
{curriculum.instructorName || '-'}
|
||||
</p>
|
||||
</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">
|
||||
<p className="font-medium leading-[normal] not-italic relative shrink-0 text-[#515151] text-[18px]">
|
||||
{curriculum.createdAt ? new Date(curriculum.createdAt).toLocaleDateString('ko-KR') : '-'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="content-stretch flex items-center justify-center relative shrink-0 w-full py-[40px]">
|
||||
<p className="font-medium leading-[normal] not-italic relative shrink-0 text-[#b9b9b9] text-[18px]">
|
||||
등록된 교육 과정이 없습니다.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 푸터 */}
|
||||
<footer className="absolute bg-[#f7f7f7] box-border content-stretch flex flex-col gap-[10px] h-[225px] items-start left-0 px-[243px] py-[39px] top-[855px] w-[1920px]">
|
||||
<div className="content-stretch flex gap-[49px] items-center relative shrink-0">
|
||||
<div className="h-[74px] relative shrink-0 w-[72px]">
|
||||
<Image
|
||||
src={logo}
|
||||
alt="로고"
|
||||
className="w-full h-full object-contain"
|
||||
width={72}
|
||||
height={74}
|
||||
/>
|
||||
</div>
|
||||
<div className="content-stretch flex flex-col gap-[5px] items-start relative shrink-0 w-[479px]">
|
||||
<div className="content-stretch flex gap-[27px] items-center relative shrink-0 w-full">
|
||||
<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 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 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 className="box-border content-stretch flex flex-col font-medium gap-[10px] items-center justify-center leading-[normal] not-italic p-[10px] relative shrink-0 text-[#515151] text-[16px]">
|
||||
<p className="relative shrink-0 w-[400px]">
|
||||
(12345) 서울특별시 광진구 구의동 123-12(구의타워1)
|
||||
</p>
|
||||
<p className="relative shrink-0 w-[400px]">
|
||||
문의: 1234-1234 (평일 09:00 ~ 18:00)
|
||||
</p>
|
||||
<p className="relative shrink-0 w-[400px]">
|
||||
이메일: qwer1234@go.or.kr
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@ import Image from 'next/image';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import logo from '../logo.svg';
|
||||
|
||||
const checkIcon = "http://localhost:3845/assets/68720b08a673d8b68ae6482d642eeab286c9462b.svg";
|
||||
import CheckboxOff from '../../public/svg/checkbox_off';
|
||||
import CheckboxOn from '../../public/svg/checkbox_on';
|
||||
|
||||
type CheckboxProps = {
|
||||
checked: boolean;
|
||||
@@ -20,18 +20,9 @@ function Checkbox({ checked, onChange, label }: CheckboxProps) {
|
||||
<button
|
||||
type="button"
|
||||
onClick={onChange}
|
||||
className="relative w-[18px] h-[18px] rounded-[4px]"
|
||||
className="relative w-[18px] h-[18px] flex items-center justify-center"
|
||||
>
|
||||
{checked ? (
|
||||
<>
|
||||
<div className="absolute bg-[#515151] left-0 rounded-[4px] w-[18px] h-[18px] top-0" />
|
||||
<div className="absolute left-[3px] w-3 h-3 top-[3px]">
|
||||
<img alt="" className="block max-w-none w-full h-full" src={checkIcon} />
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="absolute border border-[#b9b9b9] border-solid left-0 rounded-[4px] w-[18px] h-[18px] top-0" />
|
||||
)}
|
||||
{checked ? <CheckboxOn /> : <CheckboxOff />}
|
||||
</button>
|
||||
<span className="text-sm text-[#515151] leading-[1.6]">{label}</span>
|
||||
</div>
|
||||
@@ -70,10 +61,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);
|
||||
|
||||
@@ -34,11 +34,14 @@ export default function MyLecturePage() {
|
||||
<header className="absolute content-stretch flex items-center justify-between left-[calc(12.5%+91px)] top-[43px] w-[1332px]">
|
||||
<div className="content-stretch flex gap-[99px] items-center relative shrink-0">
|
||||
{/* 로고 */}
|
||||
<div className="h-[74px] relative shrink-0 w-[72px]">
|
||||
<button
|
||||
onClick={() => router.push('/')}
|
||||
className="h-[74px] relative shrink-0 w-[72px] cursor-pointer"
|
||||
>
|
||||
<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={imgImage2} />
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
{/* 메뉴 */}
|
||||
<div className="content-stretch flex gap-[24px] items-center relative shrink-0">
|
||||
<div className="content-stretch flex gap-[150px] items-center relative shrink-0">
|
||||
|
||||
@@ -94,11 +94,14 @@ export default function MyLecturePage() {
|
||||
<header className="absolute content-stretch flex items-center justify-between left-[calc(12.5%+91px)] top-[43px] w-[1332px]">
|
||||
<div className="content-stretch flex gap-[99px] items-center relative shrink-0">
|
||||
{/* 로고 */}
|
||||
<div className="h-[74px] relative shrink-0 w-[72px]">
|
||||
<button
|
||||
onClick={() => router.push('/')}
|
||||
className="h-[74px] relative shrink-0 w-[72px] cursor-pointer"
|
||||
>
|
||||
<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={imgImage2} />
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
{/* 메뉴 */}
|
||||
<div className="content-stretch flex gap-[24px] items-center relative shrink-0">
|
||||
<div className="content-stretch flex gap-[150px] items-center relative shrink-0">
|
||||
@@ -320,7 +323,7 @@ export default function MyLecturePage() {
|
||||
</div>
|
||||
|
||||
{/* 푸터 */}
|
||||
<footer className="absolute bg-[#f7f7f7] box-border content-stretch flex flex-col gap-[10px] h-[225px] items-start left-0 px-[243px] py-[39px] top-[1226px] w-full">
|
||||
<footer className="absolute bg-[#f7f7f7] box-border content-stretch flex flex-col mt-[111px] gap-[10px] h-[225px] items-start left-0 px-[243px] py-[39px] top-[1226px] w-full">
|
||||
<div className="content-stretch flex gap-[49px] items-center relative shrink-0">
|
||||
{/* 로고 */}
|
||||
<div className="h-[74px] relative shrink-0 w-[72px]">
|
||||
|
||||
116
app/page.tsx
116
app/page.tsx
@@ -3,8 +3,9 @@
|
||||
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 logo from './logo.svg';
|
||||
import Header from './components/Header';
|
||||
|
||||
const imgImage2 = "http://localhost:3845/assets/89fda8e949171025b1232bae70fc9d442e4e70c8.png";
|
||||
const imgImage7 = "http://localhost:3845/assets/a4e4d09643b890b56084560cc24d6e532a03487b.png";
|
||||
@@ -16,7 +17,9 @@ 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);
|
||||
|
||||
// 임시 데이터 - 실제로는 API에서 가져올 데이터
|
||||
const [courses, setCourses] = useState([
|
||||
@@ -52,9 +55,16 @@ export default function HomePage() {
|
||||
useEffect(() => {
|
||||
// 로그인 상태 확인
|
||||
const loginStatus = localStorage.getItem('isLoggedIn') === 'true';
|
||||
const adminStatus = isAdminLoggedIn();
|
||||
setIsLoggedIn(loginStatus);
|
||||
setIsAdmin(adminStatus);
|
||||
setIsLoading(false);
|
||||
}, []);
|
||||
|
||||
// 관리자일 경우 admin_home 페이지로 리다이렉트
|
||||
if (adminStatus) {
|
||||
router.push('/admin_home');
|
||||
}
|
||||
}, [router]);
|
||||
|
||||
if (isLoading) {
|
||||
return null; // 로딩 중
|
||||
@@ -65,80 +75,27 @@ export default function HomePage() {
|
||||
return <LoginPage />;
|
||||
}
|
||||
|
||||
// 로그인되었으면 메인 페이지 표시
|
||||
// 관리자일 경우 리다이렉트 중
|
||||
if (isAdmin) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 일반 사용자일 경우 기존 메인 페이지 표시
|
||||
return (
|
||||
<div className="bg-white relative min-h-screen w-full">
|
||||
<div className="bg-white relative min-h-screen w-full pb-[199px]">
|
||||
{/* 헤더 */}
|
||||
<header className="absolute content-stretch flex items-center justify-between left-[calc(12.5%+91.375px)] top-[43px] w-[1332px]">
|
||||
<div className="content-stretch flex gap-[99px] items-center relative shrink-0">
|
||||
{/* 로고 */}
|
||||
<div className="h-[74px] relative shrink-0 w-[72px]">
|
||||
<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={imgImage2} />
|
||||
</div>
|
||||
</div>
|
||||
{/* 메뉴 */}
|
||||
<div className="content-stretch flex gap-[24px] items-center relative shrink-0">
|
||||
<div className="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-[24px] text-nowrap whitespace-pre">
|
||||
교육 과정 목록
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="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-[24px] text-nowrap whitespace-pre">
|
||||
학습 자료실
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="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-[24px] text-nowrap whitespace-pre">
|
||||
공지사항
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* 사용자 메뉴 */}
|
||||
<div className="content-stretch flex gap-[20px] items-center relative shrink-0">
|
||||
<button
|
||||
onClick={() => router.push('/myinfo')}
|
||||
className="box-border content-stretch flex gap-[10px] items-center justify-center p-[10px] relative shrink-0 cursor-pointer group transition-colors"
|
||||
>
|
||||
<p className="font-medium leading-[normal] not-italic relative shrink-0 text-[#515151] text-[18px] text-nowrap whitespace-pre group-hover:text-blue-500 transition-colors">
|
||||
내 강좌실
|
||||
</p>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
localStorage.removeItem('isLoggedIn');
|
||||
setIsLoggedIn(false);
|
||||
router.push('/');
|
||||
}}
|
||||
className="box-border content-stretch flex gap-[10px] items-center justify-center p-[10px] relative shrink-0 cursor-pointer group transition-colors"
|
||||
>
|
||||
<p className="font-medium leading-[normal] not-italic relative shrink-0 text-[#515151] text-[18px] text-nowrap whitespace-pre group-hover:text-blue-500 transition-colors">
|
||||
로그아웃
|
||||
</p>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
<Header activePage="home" />
|
||||
|
||||
{/* 구분선 */}
|
||||
<div className="absolute h-0 left-[calc(50%+0.5px)] top-[150px] translate-x-[-50%] w-full">
|
||||
<div className="absolute h-0 left-1/2 top-[150px] translate-x-[-50%] w-[1920px]">
|
||||
<div className="absolute bottom-0 left-0 right-0 top-[-1px]">
|
||||
<img alt="" className="block max-w-none size-full" src={imgLine2} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Hero 배너 */}
|
||||
<div className="absolute h-[402px] left-[-1px] top-[272px] w-full">
|
||||
<div className="absolute inset-0 overflow-hidden pointer-events-none">
|
||||
<img alt="" className="absolute h-[257.2%] left-[-0.04%] max-w-none top-0 w-[100.07%]" src={imgImage7} />
|
||||
</div>
|
||||
<div className="absolute h-[402px] left-[-1px] top-[272px] w-full bg-[#b9b9b9]">
|
||||
{/* TODO: DB에서 이미지를 가져와서 표시 */}
|
||||
</div>
|
||||
|
||||
{/* 전체 교육 과정 (4개) */}
|
||||
@@ -152,9 +109,17 @@ export default function HomePage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 지도보기 아이콘 */}
|
||||
<div className="absolute h-[17px] left-[calc(43.75%+28.313px)] top-[692px] w-[128px]">
|
||||
<img alt="" className="block max-w-none size-full" src={imgFrame2616314} />
|
||||
{/* Hero 배너 인디케이터 버튼 */}
|
||||
<div className="absolute h-[17px] left-[calc(43.75%+28.313px)] top-[692px] w-[128px] flex gap-[20px] items-center justify-center">
|
||||
{[0, 1, 2, 3].map((index) => (
|
||||
<button
|
||||
key={index}
|
||||
onClick={() => setCurrentHeroSlide(index)}
|
||||
className={`h-[17px] w-[17px] rounded-full transition-colors ${currentHeroSlide === index ? 'bg-[#050404]' : 'bg-[#D9D9D9]'
|
||||
}`}
|
||||
aria-label={`Hero 배너 ${index + 1}번 슬라이드로 이동`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 교육 과정 카드들 - 데이터가 있을 때 */}
|
||||
@@ -162,10 +127,12 @@ export default function HomePage() {
|
||||
<div className="absolute content-stretch flex gap-[23px] items-center left-[43px] top-[817px]">
|
||||
{courses.map((course) => (
|
||||
<div key={course.id} className="content-stretch flex flex-col gap-[18px] h-[326px] items-start relative shrink-0 w-[437px]">
|
||||
<div className="h-[253px] relative shrink-0 w-full">
|
||||
<div aria-hidden="true" className="absolute inset-0 pointer-events-none">
|
||||
<img alt="" className="absolute max-w-none object-50%-50% object-cover size-full" src={course.image} />
|
||||
</div>
|
||||
<div className="h-[253px] relative shrink-0 w-full bg-[#b9b9b9]">
|
||||
{course.image ? (
|
||||
<div aria-hidden="true" className="absolute inset-0 pointer-events-none">
|
||||
<img alt="" className="absolute max-w-none object-50%-50% object-cover size-full" src={course.image} />
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="content-stretch flex flex-col gap-[10px] items-start relative shrink-0 w-[392px]">
|
||||
<p className="font-bold leading-[normal] not-italic relative shrink-0 text-[#515151] text-[20px] w-full">
|
||||
@@ -243,6 +210,9 @@ export default function HomePage() {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 공지사항 하단 여백 (공지사항 최하단: 1213 + 제목영역 + gap + 박스높이 + 199px) */}
|
||||
<div className="absolute left-0 top-[1967px] w-full h-[199px]" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
64
app/pages/page.tsx
Normal file
64
app/pages/page.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
import Link from "next/link";
|
||||
import path from "path";
|
||||
import { promises as fs } from "fs";
|
||||
|
||||
const APP_DIR = path.join(process.cwd(), "app");
|
||||
|
||||
async function collectRoutes(rootDir: string): Promise<string[]> {
|
||||
const routes: string[] = [];
|
||||
|
||||
async function walk(relativeDir: string) {
|
||||
const absoluteDir = path.join(rootDir, relativeDir);
|
||||
const entries = await fs.readdir(absoluteDir, { withFileTypes: true });
|
||||
const names = entries.map((e) => e.name);
|
||||
|
||||
if (names.includes("page.tsx")) {
|
||||
const routePath =
|
||||
relativeDir === "" ? "/" : `/${relativeDir.replace(/\\\\/g, "/")}`;
|
||||
routes.push(routePath);
|
||||
}
|
||||
|
||||
for (const entry of entries) {
|
||||
if (!entry.isDirectory()) continue;
|
||||
if (["api", "components"].includes(entry.name)) continue;
|
||||
if (entry.name.startsWith("(")) continue;
|
||||
if (entry.name.startsWith("_")) continue;
|
||||
if (entry.name === "node_modules") continue;
|
||||
|
||||
await walk(path.join(relativeDir, entry.name));
|
||||
}
|
||||
}
|
||||
|
||||
await walk("");
|
||||
|
||||
routes.sort((a, b) => {
|
||||
if (a === "/" && b !== "/") return -1;
|
||||
if (b === "/" && a !== "/") return 1;
|
||||
return a.localeCompare(b);
|
||||
});
|
||||
|
||||
return routes;
|
||||
}
|
||||
|
||||
export default async function Pages() {
|
||||
const routes = await collectRoutes(APP_DIR);
|
||||
|
||||
return (
|
||||
<main style={{ maxWidth: 800, margin: "0 auto", padding: "24px" }}>
|
||||
<h1 style={{ fontSize: 24, fontWeight: 700, marginBottom: 16 }}>
|
||||
모든 페이지 링크
|
||||
</h1>
|
||||
<ul style={{ display: "grid", gap: 8, listStyle: "none", padding: 0 }}>
|
||||
{routes.map((route) => (
|
||||
<li key={route}>
|
||||
<Link href={route} style={{ color: "#2563eb" }}>
|
||||
{route}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
253
app/pwfind/page.tsx
Normal file
253
app/pwfind/page.tsx
Normal file
@@ -0,0 +1,253 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import Link from 'next/link';
|
||||
|
||||
export default function PasswordFindPage() {
|
||||
const router = useRouter();
|
||||
const [email, setEmail] = useState('');
|
||||
const [newPassword, setNewPassword] = useState('');
|
||||
const [confirmPassword, setConfirmPassword] = useState('');
|
||||
const [emailError, setEmailError] = useState('');
|
||||
const [newPasswordError, setNewPasswordError] = useState('');
|
||||
const [confirmPasswordError, setConfirmPasswordError] = useState('');
|
||||
const [isCodeSent, setIsCodeSent] = useState(false);
|
||||
|
||||
const handleEmailChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = e.target.value;
|
||||
setEmail(value);
|
||||
if (emailError) {
|
||||
setEmailError('');
|
||||
}
|
||||
// 이메일이 변경되면 인증 상태 초기화
|
||||
if (isCodeSent) {
|
||||
setIsCodeSent(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleNewPasswordChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = e.target.value;
|
||||
setNewPassword(value);
|
||||
if (newPasswordError) {
|
||||
setNewPasswordError('');
|
||||
}
|
||||
// 새 비밀번호가 변경되면 확인 비밀번호도 다시 검증
|
||||
if (confirmPassword && value !== confirmPassword) {
|
||||
setConfirmPasswordError('비밀번호와 일치하지 않아요.');
|
||||
} else if (confirmPassword && value === confirmPassword) {
|
||||
setConfirmPasswordError('');
|
||||
}
|
||||
};
|
||||
|
||||
const handleConfirmPasswordChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = e.target.value;
|
||||
setConfirmPassword(value);
|
||||
if (confirmPasswordError) {
|
||||
setConfirmPasswordError('');
|
||||
}
|
||||
// 비밀번호 확인 실시간 검증
|
||||
if (value && newPassword && value !== newPassword) {
|
||||
setConfirmPasswordError('비밀번호와 일치하지 않아요.');
|
||||
} else if (value && newPassword && value === newPassword) {
|
||||
setConfirmPasswordError('');
|
||||
}
|
||||
};
|
||||
|
||||
const validateEmail = (email: string) => {
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
return emailRegex.test(email);
|
||||
};
|
||||
|
||||
const validatePassword = (password: string) => {
|
||||
return password.length >= 8 && password.length <= 16 && /[a-zA-Z]/.test(password) && /[0-9]/.test(password);
|
||||
};
|
||||
|
||||
const handleSendCode = () => {
|
||||
if (!email.trim()) {
|
||||
setEmailError('이메일을 입력해 주세요.');
|
||||
return;
|
||||
}
|
||||
if (!validateEmail(email)) {
|
||||
setEmailError('올바른 이메일을 입력해주세요.');
|
||||
return;
|
||||
}
|
||||
// TODO: 인증번호 발송 API 호출
|
||||
setIsCodeSent(true);
|
||||
setEmailError('');
|
||||
};
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setEmailError('');
|
||||
setNewPasswordError('');
|
||||
setConfirmPasswordError('');
|
||||
|
||||
let isValid = true;
|
||||
|
||||
if (!email.trim()) {
|
||||
setEmailError('이메일을 입력해 주세요.');
|
||||
isValid = false;
|
||||
} else if (!validateEmail(email)) {
|
||||
setEmailError('올바른 이메일을 입력해주세요.');
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
if (!isCodeSent) {
|
||||
setEmailError('인증번호를 발송해주세요.');
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
if (!newPassword.trim()) {
|
||||
setNewPasswordError('새 비밀번호를 입력해 주세요.');
|
||||
isValid = false;
|
||||
} else if (!validatePassword(newPassword)) {
|
||||
setNewPasswordError('비밀번호는 8~16자의 영문/숫자를 포함해야 합니다.');
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
if (!confirmPassword.trim()) {
|
||||
setConfirmPasswordError('새 비밀번호 확인을 입력해 주세요.');
|
||||
isValid = false;
|
||||
} else if (newPassword !== confirmPassword) {
|
||||
setConfirmPasswordError('비밀번호와 일치하지 않아요.');
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
if (isValid) {
|
||||
// TODO: 비밀번호 재설정 API 호출
|
||||
router.push('/login');
|
||||
}
|
||||
};
|
||||
|
||||
const canSubmit = email.trim() !== '' &&
|
||||
newPassword.trim() !== '' &&
|
||||
confirmPassword.trim() !== '' &&
|
||||
isCodeSent &&
|
||||
newPassword === confirmPassword &&
|
||||
validatePassword(newPassword);
|
||||
|
||||
return (
|
||||
<div className="relative flex flex-col min-h-screen bg-white">
|
||||
{/* 메인 콘텐츠 영역 - 카피라이트와 브라우저 최상단 사이의 중앙 */}
|
||||
<div className="flex-1 flex flex-col items-center justify-center pb-[100px]">
|
||||
{/* 제목 */}
|
||||
<h1 className="font-bold text-[#1D1D1D] text-[32px] leading-tight">
|
||||
비밀번호 재설정
|
||||
</h1>
|
||||
|
||||
{/* 안내 문구 */}
|
||||
<p className="text-[#515151] text-[18px] font-medium leading-normal mt-[40px]">
|
||||
비밀번호 재설정을 위해 아래 정보를 입력해 주세요.
|
||||
</p>
|
||||
|
||||
{/* 입력 폼 */}
|
||||
<form
|
||||
className="w-[829px] border border-[#b9b9b9] rounded-[8px] mt-[40px] px-[31px] py-[29px]"
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<div className="flex flex-col gap-[20px]">
|
||||
{/* 이메일(아이디) + 인증번호 발송 */}
|
||||
<div className="flex flex-col">
|
||||
<div className="flex gap-[10px] items-center justify-between h-[42px]">
|
||||
<span className="text-[#515151] text-[18px] font-medium w-[177px]">
|
||||
이메일(아이디)
|
||||
</span>
|
||||
<input
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={handleEmailChange}
|
||||
className={`h-[42px] px-[10px] text-[18px] font-medium rounded-[8px] border placeholder:text-[#b9b9b9] bg-white w-[401px] focus:border-[#1669CA] focus:outline-none ${emailError ? 'border-[#E85D5D] text-[#E85D5D]' : 'border-[#b9b9b9] text-[#515151]'}`}
|
||||
placeholder="이메일을 입력해 주세요."
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleSendCode}
|
||||
className="bg-[#2B82E8] rounded-[10px] px-[10px] text-[18px] font-medium text-white h-[43px] w-[158px] transition hover:bg-[#1669ca] flex items-center justify-center"
|
||||
>
|
||||
인증번호 발송
|
||||
</button>
|
||||
</div>
|
||||
{emailError && (
|
||||
<p className="text-[13px] text-[#E85D5D] ml-[187px] mt-[16px]">{emailError}</p>
|
||||
)}
|
||||
{isCodeSent && !emailError && (
|
||||
<p className="text-[#1669ca] text-[12px] font-medium ml-[187px] mt-[16px]">
|
||||
인증번호가 발송되었습니다. 이메일을 확인해 주세요.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 새 비밀번호 */}
|
||||
<div className="flex flex-col">
|
||||
<div className="flex gap-[16px] items-center h-[42px]">
|
||||
<span className="text-[#515151] text-[18px] font-medium w-[177px]">
|
||||
새 비밀번호
|
||||
</span>
|
||||
<input
|
||||
type="password"
|
||||
value={newPassword}
|
||||
onChange={handleNewPasswordChange}
|
||||
className={`flex-1 h-[42px] px-[10px] text-[18px] font-medium rounded-[8px] border placeholder:text-[#b9b9b9] bg-white focus:border-[#1669CA] focus:outline-none ${newPasswordError ? 'border-[#E85D5D] text-[#E85D5D]' : 'border-[#b9b9b9] text-[#515151]'}`}
|
||||
placeholder="새 비밀번호를 입력해 주세요."
|
||||
/>
|
||||
</div>
|
||||
{newPasswordError && (
|
||||
<p className="text-[13px] text-[#E85D5D] ml-[193px] mt-[16px]">{newPasswordError}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 새 비밀번호 확인 */}
|
||||
<div className="flex flex-col">
|
||||
<div className="flex gap-[16px] items-center h-[42px]">
|
||||
<span className="text-[#515151] text-[18px] font-medium w-[177px]">
|
||||
새 비밀번호 확인
|
||||
</span>
|
||||
<input
|
||||
type="password"
|
||||
value={confirmPassword}
|
||||
onChange={handleConfirmPasswordChange}
|
||||
className={`flex-1 h-[42px] px-[10px] text-[18px] font-medium rounded-[8px] border placeholder:text-[#b9b9b9] bg-white focus:border-[#1669CA] focus:outline-none ${confirmPasswordError ? 'border-[#E85D5D] text-[#E85D5D]' : 'border-[#b9b9b9] text-[#515151]'}`}
|
||||
placeholder="새 비밀번호를 다시 입력해 주세요."
|
||||
/>
|
||||
</div>
|
||||
{confirmPasswordError && (
|
||||
<p className="text-[13px] text-[#E85D5D] ml-[193px] mt-[16px]">{confirmPasswordError}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{/* 버튼 영역 */}
|
||||
<div className="mt-[54px] flex gap-[21px]">
|
||||
<Link
|
||||
href="/login"
|
||||
className="border border-[#1669ca] bg-white rounded-[10px] h-[55px] w-[334px] flex items-center justify-center font-medium text-[#515151] text-[18px] hover:bg-[#EDF4FC] transition"
|
||||
>
|
||||
돌아가기
|
||||
</Link>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleSubmit}
|
||||
disabled={!canSubmit}
|
||||
className={`rounded-[10px] h-[55px] w-[334px] font-medium text-[18px] transition flex items-center justify-center
|
||||
${canSubmit
|
||||
? 'bg-[#2B82E8] text-white hover:bg-[#1669ca]'
|
||||
: 'bg-[#b9b9b9] text-white cursor-not-allowed'
|
||||
}`}
|
||||
>
|
||||
비밀번호 재설정
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 카피라이트 */}
|
||||
<footer className="absolute bottom-[51.5px] left-1/2 -translate-x-1/2 flex flex-col items-center">
|
||||
<p className="text-[16px] text-[rgba(0,0,0,0.55)] leading-[1.45] font-medium tracking-[-0.08px]">
|
||||
Copyright ⓒ 2025 XL LMS. All rights reserved
|
||||
</p>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
163
app/studydata/page.tsx
Normal file
163
app/studydata/page.tsx
Normal file
@@ -0,0 +1,163 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import Image from 'next/image';
|
||||
import Header from '../components/Header';
|
||||
import logo from '../logo.svg';
|
||||
|
||||
const imgLine2 = "http://localhost:3845/assets/6ee8cf4ebb6bc2adb14aab8c9940b3002c20af35.svg";
|
||||
|
||||
export default function StudyDataPage() {
|
||||
const router = useRouter();
|
||||
const [isLoggedIn, setIsLoggedIn] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [curriculums, setCurriculums] = useState<any[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
// 로그인 상태 확인
|
||||
const loginStatus = localStorage.getItem('isLoggedIn') === 'true';
|
||||
setIsLoggedIn(loginStatus);
|
||||
setIsLoading(false);
|
||||
|
||||
// TODO: DB에서 교육 과정 목록 가져오기
|
||||
// const fetchCurriculums = async () => {
|
||||
// const response = await fetch('/api/curriculums');
|
||||
// const data = await response.json();
|
||||
// setCurriculums(data);
|
||||
// };
|
||||
// fetchCurriculums();
|
||||
}, []);
|
||||
|
||||
if (isLoading) {
|
||||
return null; // 로딩 중
|
||||
}
|
||||
|
||||
// 로그인되지 않았으면 로그인 페이지로 리다이렉트
|
||||
if (!isLoggedIn) {
|
||||
router.push('/login');
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-white relative min-h-screen w-full pb-[199px]">
|
||||
{/* 헤더 */}
|
||||
<Header activePage="studydata" />
|
||||
|
||||
{/* 구분선 */}
|
||||
<div className="absolute h-0 left-1/2 top-[150px] translate-x-[-50%] w-[1920px]">
|
||||
<div className="absolute bottom-0 left-0 right-0 top-[-1px]">
|
||||
<img alt="" className="block max-w-none size-full" src={imgLine2} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 메인 콘텐츠 */}
|
||||
<div className="absolute content-stretch flex flex-col items-start left-[calc(6.25%+79px)] top-[270px] w-[1521px]">
|
||||
{/* 페이지 제목 */}
|
||||
<div className="content-stretch flex items-center relative shrink-0 mb-[20px]">
|
||||
<p className="font-bold leading-[normal] not-italic relative shrink-0 text-[#515151] text-[28px]">
|
||||
학습 자료 ({curriculums.length}건)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 테이블 헤더 */}
|
||||
<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>
|
||||
</div>
|
||||
|
||||
{/* 테이블 내용 */}
|
||||
{curriculums.length > 0 ? (
|
||||
<div className="content-stretch flex flex-col items-start relative shrink-0 w-full">
|
||||
{curriculums.map((curriculum) => (
|
||||
<div key={curriculum.id} className="content-stretch flex items-center relative shrink-0 w-full border-[0.5px] border-[#b9b9b9] border-solid">
|
||||
<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">
|
||||
<p className="font-medium leading-[normal] not-italic relative shrink-0 text-[#515151] text-[18px]">
|
||||
{curriculum.title}
|
||||
</p>
|
||||
</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">
|
||||
<p className="font-medium leading-[normal] not-italic relative shrink-0 text-[#515151] text-[18px]">
|
||||
{curriculum.instructorName || '-'}
|
||||
</p>
|
||||
</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">
|
||||
<p className="font-medium leading-[normal] not-italic relative shrink-0 text-[#515151] text-[18px]">
|
||||
{curriculum.createdAt ? new Date(curriculum.createdAt).toLocaleDateString('ko-KR') : '-'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="content-stretch flex items-center justify-center relative shrink-0 w-full py-[40px]">
|
||||
<p className="font-medium leading-[normal] not-italic relative shrink-0 text-[#b9b9b9] text-[18px]">
|
||||
등록된 학습 자료가 없습니다.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 푸터 */}
|
||||
<footer className="absolute bg-[#f7f7f7] box-border content-stretch flex flex-col gap-[10px] h-[225px] items-start left-0 px-[243px] py-[39px] top-[855px] w-[1920px]">
|
||||
<div className="content-stretch flex gap-[49px] items-center relative shrink-0">
|
||||
<div className="h-[74px] relative shrink-0 w-[72px]">
|
||||
<Image
|
||||
src={logo}
|
||||
alt="로고"
|
||||
className="w-full h-full object-contain"
|
||||
width={72}
|
||||
height={74}
|
||||
/>
|
||||
</div>
|
||||
<div className="content-stretch flex flex-col gap-[5px] items-start relative shrink-0 w-[479px]">
|
||||
<div className="content-stretch flex gap-[27px] items-center relative shrink-0 w-full">
|
||||
<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 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 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 className="box-border content-stretch flex flex-col font-medium gap-[10px] items-center justify-center leading-[normal] not-italic p-[10px] relative shrink-0 text-[#515151] text-[16px]">
|
||||
<p className="relative shrink-0 w-[400px]">
|
||||
(12345) 서울특별시 광진구 구의동 123-12(구의타워1)
|
||||
</p>
|
||||
<p className="relative shrink-0 w-[400px]">
|
||||
문의: 1234-1234 (평일 09:00 ~ 18:00)
|
||||
</p>
|
||||
<p className="relative shrink-0 w-[400px]">
|
||||
이메일: qwer1234@go.or.kr
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
1412
package-lock.json
generated
1412
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
20
package.json
20
package.json
@@ -6,9 +6,23 @@
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "eslint"
|
||||
"lint": "eslint",
|
||||
"generate": "prisma generate",
|
||||
"push": "prisma db push",
|
||||
"migrate": "prisma migrate dev",
|
||||
"studio": "prisma studio",
|
||||
"seed": "npx tsx prisma/seed.ts"
|
||||
},
|
||||
"prisma": {
|
||||
"seed": "npx tsx prisma/seed.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@prisma/client": "^6.19.0",
|
||||
"@types/bcryptjs": "^2.4.6",
|
||||
"@types/better-sqlite3": "^7.6.13",
|
||||
"bcryptjs": "^3.0.3",
|
||||
"better-sqlite3": "^12.4.1",
|
||||
"dotenv": "^17.2.3",
|
||||
"next": "16.0.1",
|
||||
"react": "19.2.0",
|
||||
"react-dom": "19.2.0"
|
||||
@@ -20,7 +34,9 @@
|
||||
"@types/react-dom": "^19",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "16.0.1",
|
||||
"prisma": "^6.19.0",
|
||||
"tailwindcss": "^4",
|
||||
"tsx": "^4.19.2",
|
||||
"typescript": "5.9.3"
|
||||
}
|
||||
}
|
||||
}
|
||||
14
prisma.config.ts
Normal file
14
prisma.config.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { defineConfig, env } from "prisma/config";
|
||||
import { config } from "dotenv";
|
||||
config();
|
||||
|
||||
export default defineConfig({
|
||||
schema: "prisma/schema.prisma",
|
||||
migrations: {
|
||||
path: "prisma/migrations",
|
||||
},
|
||||
engine: "classic",
|
||||
datasource: {
|
||||
url: env("DATABASE_URL"),
|
||||
},
|
||||
});
|
||||
BIN
prisma/prisma/dev.db
Normal file
BIN
prisma/prisma/dev.db
Normal file
Binary file not shown.
99
prisma/schema.prisma
Normal file
99
prisma/schema.prisma
Normal file
@@ -0,0 +1,99 @@
|
||||
// This is your Prisma schema file,
|
||||
// learn more about it in the docs: https://pris.ly/d/prisma-schema
|
||||
|
||||
// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?
|
||||
// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init
|
||||
|
||||
// This is your Prisma schema file,
|
||||
// learn more about it in the docs: https://pris.ly/d/prisma-schema
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client"
|
||||
output = "../lib/generated/prisma"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "sqlite"
|
||||
url = "file:./prisma/dev.db"
|
||||
}
|
||||
|
||||
// 사용자 권한 Enum
|
||||
enum UserRole {
|
||||
STUDENT // 학습자
|
||||
INSTRUCTOR // 강사
|
||||
ADMIN // 관리자
|
||||
}
|
||||
|
||||
// User 모델
|
||||
model User {
|
||||
id String @id @default(cuid())
|
||||
email String @unique
|
||||
name String?
|
||||
password String
|
||||
phone String?
|
||||
gender String?
|
||||
birthYear Int?
|
||||
birthMonth Int?
|
||||
birthDay Int?
|
||||
role UserRole @default(STUDENT) // 권한 (기본값: 학습자)
|
||||
isActive Boolean @default(true) // 계정 활성화 여부 (true: 활성화, false: 비활성화)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
registeredLectures Lecture[] @relation("Registrant") // 등록한 강좌 목록
|
||||
enrolledLectures UserLecture[] // 수강 중인 강좌 목록
|
||||
|
||||
@@map("users")
|
||||
}
|
||||
|
||||
// 교육 과정 관리
|
||||
model Curriculum {
|
||||
id String @id @default(cuid())
|
||||
title String // 과정 제목
|
||||
thumbnailImage String? // 썸네일 이미지 경로
|
||||
instructorId String // 강사 ID (User와의 관계)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
lectures Lecture[] // 강좌 목록 (1:N 관계)
|
||||
|
||||
@@map("curriculums")
|
||||
}
|
||||
|
||||
// 강좌 관리
|
||||
model Lecture {
|
||||
id String @id @default(cuid())
|
||||
title String // 강좌명
|
||||
thumbnailImage String? // 썸네일 이미지 경로
|
||||
attachmentFile String? // 첨부 파일 경로
|
||||
evaluationQuestionCount Int @default(0) // 학습 평가 문제 수
|
||||
curriculumId String // 교육 과정 ID (Curriculum과의 관계)
|
||||
registrantId String // 등록자 ID (User와의 관계)
|
||||
registeredAt DateTime @default(now()) // 등록일
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
curriculum Curriculum @relation(fields: [curriculumId], references: [id], onDelete: Cascade)
|
||||
registrant User @relation("Registrant", fields: [registrantId], references: [id])
|
||||
enrolledUsers UserLecture[] // 수강 중인 사용자 목록
|
||||
|
||||
@@map("lectures")
|
||||
}
|
||||
|
||||
// 사용자-강좌 수강 관계 (다대다 관계)
|
||||
model UserLecture {
|
||||
id String @id @default(cuid())
|
||||
userId String // 사용자 ID
|
||||
lectureId String // 강좌 ID
|
||||
enrolledAt DateTime @default(now()) // 수강 시작일
|
||||
completedAt DateTime? // 수강 완료일
|
||||
isCompleted Boolean @default(false) // 수강 완료 여부
|
||||
progress Int @default(0) // 수강 진행률 (0-100)
|
||||
score Int? // 평가 점수
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
lecture Lecture @relation(fields: [lectureId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([userId, lectureId]) // 한 사용자는 같은 강좌를 중복 수강할 수 없음
|
||||
@@map("user_lectures")
|
||||
}
|
||||
126
prisma/seed.ts
Normal file
126
prisma/seed.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
import { PrismaClient } from '../lib/generated/prisma/client';
|
||||
import bcrypt from 'bcryptjs';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
async function main() {
|
||||
console.log('🌱 시드 데이터 생성 시작...');
|
||||
|
||||
// 기존 데이터 삭제 (순서 중요: 외래키 관계 고려)
|
||||
console.log('🗑️ 기존 데이터 삭제 중...');
|
||||
await prisma.userLecture.deleteMany();
|
||||
await prisma.lecture.deleteMany();
|
||||
await prisma.curriculum.deleteMany();
|
||||
await prisma.user.deleteMany();
|
||||
|
||||
// 비밀번호 해시 (기본 비밀번호: password123)
|
||||
const hashedPassword = await bcrypt.hash('password123', 10);
|
||||
|
||||
// 1. 사용자 데이터 생성
|
||||
console.log('👥 사용자 데이터 생성 중...');
|
||||
const users = await prisma.user.createMany({
|
||||
data: [
|
||||
// 운영자
|
||||
{ email: 'park@example.com', name: '박민수', password: hashedPassword, role: 'ADMIN', isActive: true },
|
||||
{ email: 'oh@example.com', name: '오준혁', password: hashedPassword, role: 'ADMIN', isActive: true },
|
||||
{ email: 'kwon@example.com', name: '권태영', password: hashedPassword, role: 'ADMIN', isActive: true },
|
||||
|
||||
// 강사
|
||||
{ email: 'kim@example.com', name: '김철수', password: hashedPassword, role: 'INSTRUCTOR', isActive: true },
|
||||
{ email: 'jung@example.com', name: '정대현', password: hashedPassword, role: 'INSTRUCTOR', isActive: true },
|
||||
{ email: 'lim@example.com', name: '임수진', password: hashedPassword, role: 'INSTRUCTOR', isActive: true },
|
||||
{ email: 'shin@example.com', name: '신동욱', password: hashedPassword, role: 'INSTRUCTOR', isActive: true },
|
||||
{ email: 'jeon@example.com', name: '전혜진', password: hashedPassword, role: 'INSTRUCTOR', isActive: true },
|
||||
|
||||
// 학습자
|
||||
{ email: 'hong@example.com', name: '홍길동', password: hashedPassword, role: 'STUDENT', isActive: true },
|
||||
{ email: 'lee@example.com', name: '이영희', password: hashedPassword, role: 'STUDENT', isActive: true },
|
||||
{ email: 'choi@example.com', name: '최지영', password: hashedPassword, role: 'STUDENT', isActive: true },
|
||||
{ email: 'kang@example.com', name: '강미영', password: hashedPassword, role: 'STUDENT', isActive: true },
|
||||
{ email: 'yoon@example.com', name: '윤성호', password: hashedPassword, role: 'STUDENT', isActive: true },
|
||||
{ email: 'han@example.com', name: '한지훈', password: hashedPassword, role: 'STUDENT', isActive: true },
|
||||
{ email: 'song@example.com', name: '송민경', password: hashedPassword, role: 'STUDENT', isActive: true },
|
||||
{ email: 'ryu@example.com', name: '류현우', password: hashedPassword, role: 'STUDENT', isActive: true },
|
||||
{ email: 'cho@example.com', name: '조은서', password: hashedPassword, role: 'STUDENT', isActive: true },
|
||||
{ email: 'bae@example.com', name: '배성민', password: hashedPassword, role: 'STUDENT', isActive: true },
|
||||
{ email: 'namgung@example.com', name: '남궁준', password: hashedPassword, role: 'STUDENT', isActive: true },
|
||||
{ email: 'seo@example.com', name: '서아름', password: hashedPassword, role: 'STUDENT', isActive: true },
|
||||
],
|
||||
});
|
||||
|
||||
console.log(`✅ ${users.count}명의 사용자 생성 완료`);
|
||||
|
||||
// 사용자 ID 조회 (강사와 등록자로 사용)
|
||||
const instructorKim = await prisma.user.findUnique({ where: { email: 'kim@example.com' } });
|
||||
const registrarLee = await prisma.user.findUnique({ where: { email: 'lee@example.com' } });
|
||||
const registrarPark = await prisma.user.findUnique({ where: { email: 'park@example.com' } });
|
||||
const registrarKim = await prisma.user.findUnique({ where: { email: 'kim@example.com' } });
|
||||
|
||||
if (!instructorKim || !registrarLee || !registrarPark || !registrarKim) {
|
||||
throw new Error('필수 사용자를 찾을 수 없습니다.');
|
||||
}
|
||||
|
||||
// 2. 교육 과정 데이터 생성
|
||||
console.log('📚 교육 과정 데이터 생성 중...');
|
||||
const curriculum1 = await prisma.curriculum.create({
|
||||
data: {
|
||||
title: '방사선 적용',
|
||||
thumbnailImage: null,
|
||||
instructorId: instructorKim.id,
|
||||
},
|
||||
});
|
||||
|
||||
const curriculum2 = await prisma.curriculum.create({
|
||||
data: {
|
||||
title: '방사선 원리',
|
||||
thumbnailImage: null,
|
||||
instructorId: instructorKim.id,
|
||||
},
|
||||
});
|
||||
|
||||
console.log('✅ 교육 과정 생성 완료');
|
||||
|
||||
// 3. 강좌 데이터 생성
|
||||
console.log('📖 강좌 데이터 생성 중...');
|
||||
const lectures = await prisma.lecture.createMany({
|
||||
data: [
|
||||
// 방사선 적용 강좌들
|
||||
{ title: '방사선 기본 원리', attachmentFile: '파일1.pdf', evaluationQuestionCount: 10, curriculumId: curriculum1.id, registrantId: registrarLee.id },
|
||||
{ title: '방사선 안전 관리', attachmentFile: '파일2.pdf', evaluationQuestionCount: 15, curriculumId: curriculum1.id, registrantId: registrarLee.id },
|
||||
{ title: '방사선 장비 사용법', attachmentFile: '파일4.pdf', evaluationQuestionCount: 8, curriculumId: curriculum1.id, registrantId: registrarLee.id },
|
||||
{ title: '방사선 보호 장비', attachmentFile: '파일6.pdf', evaluationQuestionCount: 14, curriculumId: curriculum1.id, registrantId: registrarLee.id },
|
||||
{ title: '방사선 응용 실습', attachmentFile: '파일7.pdf', evaluationQuestionCount: 16, curriculumId: curriculum1.id, registrantId: registrarLee.id },
|
||||
{ title: '방사선 의료 응용', attachmentFile: '파일9.pdf', evaluationQuestionCount: 18, curriculumId: curriculum1.id, registrantId: registrarLee.id },
|
||||
{ title: '방사선 산업 응용', attachmentFile: '파일10.pdf', evaluationQuestionCount: 13, curriculumId: curriculum1.id, registrantId: registrarLee.id },
|
||||
{ title: '방사선 환경 모니터링', attachmentFile: '파일12.pdf', evaluationQuestionCount: 9, curriculumId: curriculum1.id, registrantId: registrarLee.id },
|
||||
{ title: '방사선 검사 기법', attachmentFile: '파일13.pdf', evaluationQuestionCount: 19, curriculumId: curriculum1.id, registrantId: registrarLee.id },
|
||||
{ title: '방사선 처리 기술', attachmentFile: '파일15.pdf', evaluationQuestionCount: 7, curriculumId: curriculum1.id, registrantId: registrarLee.id },
|
||||
{ title: '방사선 품질 관리', attachmentFile: '파일16.pdf', evaluationQuestionCount: 22, curriculumId: curriculum1.id, registrantId: registrarLee.id },
|
||||
{ title: '방사선 계측법', attachmentFile: '파일18.pdf', evaluationQuestionCount: 24, curriculumId: curriculum1.id, registrantId: registrarLee.id },
|
||||
{ title: '방사선 안전 규정', attachmentFile: '파일19.pdf', evaluationQuestionCount: 5, curriculumId: curriculum1.id, registrantId: registrarLee.id },
|
||||
|
||||
// 방사선 원리 강좌들
|
||||
{ title: '방사선 측정 기법', attachmentFile: '파일3.pdf', evaluationQuestionCount: 12, curriculumId: curriculum2.id, registrantId: registrarKim.id },
|
||||
{ title: '방사선 물리학 기초', attachmentFile: '파일5.pdf', evaluationQuestionCount: 20, curriculumId: curriculum2.id, registrantId: registrarKim.id },
|
||||
{ title: '방사선 화학 반응', attachmentFile: '파일8.pdf', evaluationQuestionCount: 11, curriculumId: curriculum2.id, registrantId: registrarKim.id },
|
||||
{ title: '방사선 생물학', attachmentFile: '파일11.pdf', evaluationQuestionCount: 17, curriculumId: curriculum2.id, registrantId: registrarKim.id },
|
||||
{ title: '방사선 에너지 전달', attachmentFile: '파일14.pdf', evaluationQuestionCount: 21, curriculumId: curriculum2.id, registrantId: registrarKim.id },
|
||||
{ title: '방사선 방어 이론', attachmentFile: '파일17.pdf', evaluationQuestionCount: 6, curriculumId: curriculum2.id, registrantId: registrarKim.id },
|
||||
{ title: '방사선 핵물리학', attachmentFile: '파일20.pdf', evaluationQuestionCount: 25, curriculumId: curriculum2.id, registrantId: registrarKim.id },
|
||||
],
|
||||
});
|
||||
|
||||
console.log(`✅ ${lectures.count}개의 강좌 생성 완료`);
|
||||
|
||||
console.log('🎉 시드 데이터 생성 완료!');
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((e) => {
|
||||
console.error('❌ 시드 데이터 생성 실패:', e);
|
||||
process.exit(1);
|
||||
})
|
||||
.finally(async () => {
|
||||
await prisma.$disconnect();
|
||||
});
|
||||
|
||||
7
public/svg/checkbox_off.tsx
Normal file
7
public/svg/checkbox_off.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
export default function CheckboxOff() {
|
||||
return (
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="0.5" y="0.5" width="17" height="17" rx="3.5" stroke="#B9B9B9" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
15
public/svg/checkbox_on.tsx
Normal file
15
public/svg/checkbox_on.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
export default function CheckboxOn() {
|
||||
return (
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="18" height="18" rx="4" fill="#515151" />
|
||||
<g clipPath="url(#clip0_40000058_4306)">
|
||||
<path d="M7.49999 11.0849L5.41499 8.99992L4.70499 9.70492L7.49999 12.4999L13.5 6.49992L12.795 5.79492L7.49999 11.0849Z" fill="white" />
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_40000058_4306">
|
||||
<rect width="12" height="12" fill="white" transform="translate(3 3)" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
15
public/svg/chevron_middle.tsx
Normal file
15
public/svg/chevron_middle.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
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 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">
|
||||
<rect width="24" height="24" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
14
public/svg/close.tsx
Normal file
14
public/svg/close.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
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 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>
|
||||
<clipPath id="clip0_40000054_6405">
|
||||
<rect width="30" height="30" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
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>
|
||||
);
|
||||
}
|
||||
8
public/svg/pagenav_leftoff.tsx
Normal file
8
public/svg/pagenav_leftoff.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
export default function PageNavLeftOff() {
|
||||
return (
|
||||
<svg width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11.2002 0.5H18.7998C21.8166 0.5 23.2067 0.506341 24.3174 0.867188C26.6007 1.60907 28.3909 3.39933 29.1328 5.68262C29.4937 6.79334 29.5 8.1834 29.5 11.2002V18.7998C29.5 21.8166 29.4937 23.2067 29.1328 24.3174C28.3909 26.6007 26.6007 28.3909 24.3174 29.1328C23.2067 29.4937 21.8166 29.5 18.7998 29.5H11.2002C8.1834 29.5 6.79334 29.4937 5.68262 29.1328C3.39933 28.3909 1.60907 26.6007 0.867188 24.3174C0.506341 23.2067 0.5 21.8166 0.5 18.7998V11.2002C0.5 8.1834 0.506341 6.79334 0.867188 5.68262C1.60907 3.39933 3.39933 1.60907 5.68262 0.867188C6.79334 0.506341 8.1834 0.5 11.2002 0.5Z" stroke="#EEEEEE" />
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M18.7002 21.5193C18.3199 21.9522 17.6592 21.9961 17.2246 21.6173L11.9708 17.0382C10.7328 15.9592 10.7328 14.0408 11.9708 12.9618L17.2246 8.38272C17.6592 8.00388 18.3199 8.04776 18.7002 8.48071C19.0805 8.91367 19.0365 9.57175 18.6018 9.95059L13.3481 14.5296C13.0624 14.7786 13.0624 15.2213 13.3481 15.4703L18.6018 20.0494C19.0365 20.4282 19.0805 21.0863 18.7002 21.5193Z" fill="#E1E1E1" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
8
public/svg/pagenav_lefton.tsx
Normal file
8
public/svg/pagenav_lefton.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
export default function PageNavLeftOn() {
|
||||
return (
|
||||
<svg width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11.2002 0.5H18.7998C21.8166 0.5 23.2067 0.506341 24.3174 0.867188C26.6007 1.60907 28.3909 3.39933 29.1328 5.68262C29.4937 6.79334 29.5 8.1834 29.5 11.2002V18.7998C29.5 21.8166 29.4937 23.2067 29.1328 24.3174C28.3909 26.6007 26.6007 28.3909 24.3174 29.1328C23.2067 29.4937 21.8166 29.5 18.7998 29.5H11.2002C8.1834 29.5 6.79334 29.4937 5.68262 29.1328C3.39933 28.3909 1.60907 26.6007 0.867188 24.3174C0.506341 23.2067 0.5 21.8166 0.5 18.7998V11.2002C0.5 8.1834 0.506341 6.79334 0.867188 5.68262C1.60907 3.39933 3.39933 1.60907 5.68262 0.867188C6.79334 0.506341 8.1834 0.5 11.2002 0.5Z" stroke="#EEEEEE" />
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M18.7002 21.5193C18.3199 21.9522 17.6592 21.9961 17.2246 21.6173L11.9708 17.0382C10.7328 15.9592 10.7328 14.0408 11.9708 12.9618L17.2246 8.38272C17.6592 8.00388 18.3199 8.04776 18.7002 8.48071C19.0805 8.91367 19.0365 9.57175 18.6018 9.95059L13.3481 14.5296C13.0624 14.7786 13.0624 15.2213 13.3481 15.4703L18.6018 20.0494C19.0365 20.4282 19.0805 21.0863 18.7002 21.5193Z" fill="#515151" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
8
public/svg/pagenav_rightoff.tsx
Normal file
8
public/svg/pagenav_rightoff.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
export default function PageNavRightOff() {
|
||||
return (
|
||||
<svg width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11.2002 0.5H18.7998C21.8166 0.5 23.2067 0.506341 24.3174 0.867188C26.6007 1.60907 28.3909 3.39933 29.1328 5.68262C29.4937 6.79334 29.5 8.1834 29.5 11.2002V18.7998C29.5 21.8166 29.4937 23.2067 29.1328 24.3174C28.3909 26.6007 26.6007 28.3909 24.3174 29.1328C23.2067 29.4937 21.8166 29.5 18.7998 29.5H11.2002C8.1834 29.5 6.79334 29.4937 5.68262 29.1328C3.39933 28.3909 1.60907 26.6007 0.867188 24.3174C0.506341 23.2067 0.5 21.8166 0.5 18.7998V11.2002C0.5 8.1834 0.506341 6.79334 0.867188 5.68262C1.60907 3.39933 3.39933 1.60907 5.68262 0.867188C6.79334 0.506341 8.1834 0.5 11.2002 0.5Z" stroke="#EEEEEE" />
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.2998 8.48074C11.6801 8.04778 12.3408 8.00391 12.7754 8.38275L18.0292 12.9618C19.2672 14.0408 19.2672 15.9592 18.0292 17.0382L12.7754 21.6173C12.3408 21.9961 11.6801 21.9522 11.2998 21.5193C10.9195 21.0863 10.9635 20.4282 11.3982 20.0494L16.6519 15.4704C16.9376 15.2214 16.9376 14.7787 16.6519 14.5297L11.3982 9.95061C10.9635 9.57178 10.9195 8.91369 11.2998 8.48074Z" fill="#E1E1E1" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
8
public/svg/pagenav_righton.tsx
Normal file
8
public/svg/pagenav_righton.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
export default function PageNavRightOn() {
|
||||
return (
|
||||
<svg width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11.2002 0.5H18.7998C21.8166 0.5 23.2067 0.506341 24.3174 0.867188C26.6007 1.60907 28.3909 3.39933 29.1328 5.68262C29.4937 6.79334 29.5 8.1834 29.5 11.2002V18.7998C29.5 21.8166 29.4937 23.2067 29.1328 24.3174C28.3909 26.6007 26.6007 28.3909 24.3174 29.1328C23.2067 29.4937 21.8166 29.5 18.7998 29.5H11.2002C8.1834 29.5 6.79334 29.4937 5.68262 29.1328C3.39933 28.3909 1.60907 26.6007 0.867188 24.3174C0.506341 23.2067 0.5 21.8166 0.5 18.7998V11.2002C0.5 8.1834 0.506341 6.79334 0.867188 5.68262C1.60907 3.39933 3.39933 1.60907 5.68262 0.867188C6.79334 0.506341 8.1834 0.5 11.2002 0.5Z" stroke="#EEEEEE" />
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.2998 8.48074C11.6801 8.04778 12.3408 8.00391 12.7754 8.38275L18.0292 12.9618C19.2672 14.0408 19.2672 15.9592 18.0292 17.0382L12.7754 21.6173C12.3408 21.9961 11.6801 21.9522 11.2998 21.5193C10.9194 21.0863 10.9635 20.4282 11.3981 20.0494L16.6519 15.4704C16.9376 15.2214 16.9376 14.7787 16.6519 14.5297L11.3982 9.95062C10.9635 9.57178 10.9195 8.91369 11.2998 8.48074Z" fill="#515151" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user