Compare commits

...

21 Commits

Author SHA1 Message Date
koreacomp5
dae09ff2e7 mota1 2025-11-15 22:28:31 +09:00
wallace
0af1028b7b 시드 만들었음음 2025-11-13 18:07:45 +09:00
wallace
efdcde8b12 페이지네이션 아이콘 추가가 2025-11-13 15:53:07 +09:00
wallace
a7cad39b3c admin 페이지 메인 수정정 2025-11-13 15:32:58 +09:00
wallace
16d6cb5b30 admin 임시 리스트 생성성 2025-11-13 15:28:45 +09:00
wallace
8403684df3 관리자 계정 설정, 관리자 페이지 작업1 2025-11-13 15:02:43 +09:00
wallace
c81f218a2b 공지사항 페이지 추가1 2025-11-12 20:49:07 +09:00
wallace
9d3ab9f8d8 env 허용1 2025-11-12 10:37:04 +09:00
wallace
e8f4094de6 헤더 버튼 활성화화 2025-11-11 20:01:37 +09:00
wallace
94a186de59 메인 수정정 2025-11-11 11:57:34 +09:00
wallace
137edf2d0d prisma 성공~ 2025-11-11 11:41:08 +09:00
wallace
6600241bdb prizma 삭제 2025-11-11 10:46:00 +09:00
wallace
218e915e37 prizma 이상한거거 2025-11-11 10:42:56 +09:00
wallace
512acfb921 프리즈마 세팅팅 2025-11-10 21:57:28 +09:00
wallace
9a11a8afd1 관리자 페이지 교육과정 등록, 비밀번호 재설정 생성 2025-11-10 21:20:09 +09:00
wallace
efd5ab915e 내 강좌실 생성11 2025-11-10 20:13:55 +09:00
wallace
6a9cd7c1e4 아이디 찾기11 2025-11-10 19:48:54 +09:00
wallace
3d226a65f7 회원가입 icon 정리리 2025-11-10 11:34:06 +09:00
wallace
c40a40bf58 메인페이지 작업시작작 2025-11-10 10:54:07 +09:00
9be57a56b4 인증번호 발송 및 확인 2025-11-08 13:55:27 +09:00
5335c24b5a 회원가입 폼 수정1 2025-11-08 13:17:20 +09:00
48 changed files with 6611 additions and 526 deletions

12
.env Normal file
View 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
View File

@@ -16,7 +16,6 @@
# next.js # next.js
/.next/ /.next/
/out/ /out/
# production # production
/build /build
@@ -31,7 +30,7 @@ yarn-error.log*
.pnpm-debug.log* .pnpm-debug.log*
# env files (can opt-in for committing if needed) # env files (can opt-in for committing if needed)
.env* # .env*
# vercel # vercel
.vercel .vercel
@@ -39,3 +38,17 @@ yarn-error.log*
# typescript # typescript
*.tsbuildinfo *.tsbuildinfo
next-env.d.ts 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
View 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
View 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
View 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
View 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>
);
}

View File

@@ -0,0 +1,124 @@
import { NextRequest, NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';
// GET: 특정 교육 과정 조회
export async function GET(
request: NextRequest,
{ params }: { params: { id: string } }
) {
try {
const curriculum = await prisma.curriculum.findUnique({
where: { id: params.id },
include: {
lectures: {
orderBy: {
registeredAt: 'desc',
},
include: {
registrant: {
select: {
name: true,
},
},
},
},
},
});
if (!curriculum) {
return NextResponse.json(
{ error: '교육 과정을 찾을 수 없습니다.' },
{ status: 404 }
);
}
return NextResponse.json({
data: {
id: curriculum.id,
courseName: curriculum.title,
instructorId: curriculum.instructorId,
thumbnailImage: curriculum.thumbnailImage,
createdAt: curriculum.createdAt.toISOString().split('T')[0],
lectures: curriculum.lectures.map((lecture) => ({
id: lecture.id,
lectureName: lecture.title,
attachedFile: lecture.attachmentFile || '없음',
questionCount: lecture.evaluationQuestionCount,
registrar: lecture.registrant.name || '알 수 없음',
createdAt: lecture.registeredAt.toISOString().split('T')[0],
})),
},
});
} catch (error) {
console.error('Error fetching curriculum:', error);
return NextResponse.json(
{ error: '교육 과정을 불러오는 중 오류가 발생했습니다.' },
{ status: 500 }
);
}
}
// PUT: 교육 과정 수정
export async function PUT(
request: NextRequest,
{ params }: { params: { id: string } }
) {
try {
const body = await request.json();
const { title, instructorId, thumbnailImage } = body;
const curriculum = await prisma.curriculum.update({
where: { id: params.id },
data: {
...(title && { title }),
...(instructorId && { instructorId }),
...(thumbnailImage !== undefined && { thumbnailImage }),
},
include: {
lectures: {
select: {
id: true,
},
},
},
});
return NextResponse.json({
data: {
id: curriculum.id,
courseName: curriculum.title,
instructorId: curriculum.instructorId,
thumbnailImage: curriculum.thumbnailImage,
createdAt: curriculum.createdAt.toISOString().split('T')[0],
lectureCount: curriculum.lectures.length,
},
});
} catch (error) {
console.error('Error updating curriculum:', error);
return NextResponse.json(
{ error: '교육 과정 수정 중 오류가 발생했습니다.' },
{ status: 500 }
);
}
}
// DELETE: 교육 과정 삭제
export async function DELETE(
request: NextRequest,
{ params }: { params: { id: string } }
) {
try {
await prisma.curriculum.delete({
where: { id: params.id },
});
return NextResponse.json({ message: '교육 과정이 삭제되었습니다.' });
} catch (error) {
console.error('Error deleting curriculum:', error);
return NextResponse.json(
{ error: '교육 과정 삭제 중 오류가 발생했습니다.' },
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,113 @@
import { NextRequest, NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';
// GET: 교육 과정 목록 조회
export async function GET(request: NextRequest) {
try {
const searchParams = request.nextUrl.searchParams;
const page = parseInt(searchParams.get('page') || '1');
const limit = parseInt(searchParams.get('limit') || '13');
const skip = (page - 1) * limit;
const [curriculums, total] = await Promise.all([
prisma.curriculum.findMany({
skip,
take: limit,
orderBy: {
createdAt: 'desc',
},
include: {
lectures: {
select: {
id: true,
},
},
},
}),
prisma.curriculum.count(),
]);
return NextResponse.json({
data: curriculums.map((curriculum) => ({
id: curriculum.id,
courseName: curriculum.title,
instructorId: curriculum.instructorId,
thumbnailImage: curriculum.thumbnailImage,
createdAt: curriculum.createdAt.toISOString().split('T')[0],
lectureCount: curriculum.lectures.length,
})),
pagination: {
page,
limit,
total,
totalPages: Math.ceil(total / limit),
},
});
} catch (error) {
console.error('Error fetching curriculums:', error);
return NextResponse.json(
{ error: '교육 과정 목록을 불러오는 중 오류가 발생했습니다.' },
{ status: 500 }
);
}
}
// POST: 교육 과정 생성
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const { title, instructorId, thumbnailImage } = body;
if (!title || !instructorId) {
return NextResponse.json(
{ error: '교육 과정명과 강사 ID는 필수입니다.' },
{ status: 400 }
);
}
// 강사 존재 확인
const instructor = await prisma.user.findUnique({
where: { id: instructorId },
});
if (!instructor || instructor.role !== 'INSTRUCTOR') {
return NextResponse.json(
{ error: '유효한 강사를 선택해주세요.' },
{ status: 400 }
);
}
const curriculum = await prisma.curriculum.create({
data: {
title,
instructorId,
thumbnailImage: thumbnailImage || null,
},
include: {
lectures: {
select: {
id: true,
},
},
},
});
return NextResponse.json({
data: {
id: curriculum.id,
courseName: curriculum.title,
instructorId: curriculum.instructorId,
thumbnailImage: curriculum.thumbnailImage,
createdAt: curriculum.createdAt.toISOString().split('T')[0],
lectureCount: curriculum.lectures.length,
},
}, { status: 201 });
} catch (error) {
console.error('Error creating curriculum:', error);
return NextResponse.json(
{ error: '교육 과정 생성 중 오류가 발생했습니다.' },
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,127 @@
import { NextRequest, NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';
// GET: 특정 강좌 조회
export async function GET(
request: NextRequest,
{ params }: { params: { id: string } }
) {
try {
const lecture = await prisma.lecture.findUnique({
where: { id: params.id },
include: {
curriculum: {
select: {
title: true,
},
},
registrant: {
select: {
name: true,
},
},
},
});
if (!lecture) {
return NextResponse.json(
{ error: '강좌를 찾을 수 없습니다.' },
{ status: 404 }
);
}
return NextResponse.json({
data: {
id: lecture.id,
courseName: lecture.curriculum.title,
lectureName: lecture.title,
attachedFile: lecture.attachmentFile || '없음',
questionCount: lecture.evaluationQuestionCount,
registrar: lecture.registrant.name || '알 수 없음',
createdAt: lecture.registeredAt.toISOString().split('T')[0],
curriculumId: lecture.curriculumId,
thumbnailImage: lecture.thumbnailImage,
},
});
} catch (error) {
console.error('Error fetching lecture:', error);
return NextResponse.json(
{ error: '강좌를 불러오는 중 오류가 발생했습니다.' },
{ status: 500 }
);
}
}
// PUT: 강좌 수정
export async function PUT(
request: NextRequest,
{ params }: { params: { id: string } }
) {
try {
const body = await request.json();
const { title, attachmentFile, evaluationQuestionCount, thumbnailImage } = body;
const lecture = await prisma.lecture.update({
where: { id: params.id },
data: {
...(title && { title }),
...(attachmentFile !== undefined && { attachmentFile }),
...(evaluationQuestionCount !== undefined && { evaluationQuestionCount }),
...(thumbnailImage !== undefined && { thumbnailImage }),
},
include: {
curriculum: {
select: {
title: true,
},
},
registrant: {
select: {
name: true,
},
},
},
});
return NextResponse.json({
data: {
id: lecture.id,
courseName: lecture.curriculum.title,
lectureName: lecture.title,
attachedFile: lecture.attachmentFile || '없음',
questionCount: lecture.evaluationQuestionCount,
registrar: lecture.registrant.name || '알 수 없음',
createdAt: lecture.registeredAt.toISOString().split('T')[0],
curriculumId: lecture.curriculumId,
thumbnailImage: lecture.thumbnailImage,
},
});
} catch (error) {
console.error('Error updating lecture:', error);
return NextResponse.json(
{ error: '강좌 수정 중 오류가 발생했습니다.' },
{ status: 500 }
);
}
}
// DELETE: 강좌 삭제
export async function DELETE(
request: NextRequest,
{ params }: { params: { id: string } }
) {
try {
await prisma.lecture.delete({
where: { id: params.id },
});
return NextResponse.json({ message: '강좌가 삭제되었습니다.' });
} catch (error) {
console.error('Error deleting lecture:', error);
return NextResponse.json(
{ error: '강좌 삭제 중 오류가 발생했습니다.' },
{ status: 500 }
);
}
}

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

@@ -0,0 +1,148 @@
import { NextRequest, NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';
// GET: 강좌 목록 조회
export async function GET(request: NextRequest) {
try {
const searchParams = request.nextUrl.searchParams;
const page = parseInt(searchParams.get('page') || '1');
const limit = parseInt(searchParams.get('limit') || '13');
const curriculumId = searchParams.get('curriculumId');
const skip = (page - 1) * limit;
const where = curriculumId ? { curriculumId } : {};
const [lectures, total] = await Promise.all([
prisma.lecture.findMany({
where,
skip,
take: limit,
orderBy: {
registeredAt: 'desc',
},
include: {
curriculum: {
select: {
title: true,
},
},
registrant: {
select: {
name: true,
},
},
},
}),
prisma.lecture.count({ where }),
]);
return NextResponse.json({
data: lectures.map((lecture) => ({
id: lecture.id,
courseName: lecture.curriculum.title,
lectureName: lecture.title,
attachedFile: lecture.attachmentFile || '없음',
questionCount: lecture.evaluationQuestionCount,
registrar: lecture.registrant.name || '알 수 없음',
createdAt: lecture.registeredAt.toISOString().split('T')[0],
curriculumId: lecture.curriculumId,
thumbnailImage: lecture.thumbnailImage,
})),
pagination: {
page,
limit,
total,
totalPages: Math.ceil(total / limit),
},
});
} catch (error) {
console.error('Error fetching lectures:', error);
return NextResponse.json(
{ error: '강좌 목록을 불러오는 중 오류가 발생했습니다.' },
{ status: 500 }
);
}
}
// POST: 강좌 생성
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const { title, curriculumId, registrantId, attachmentFile, evaluationQuestionCount, thumbnailImage } = body;
if (!title || !curriculumId || !registrantId) {
return NextResponse.json(
{ error: '강좌명, 교육 과정 ID, 등록자 ID는 필수입니다.' },
{ status: 400 }
);
}
// 교육 과정 존재 확인
const curriculum = await prisma.curriculum.findUnique({
where: { id: curriculumId },
});
if (!curriculum) {
return NextResponse.json(
{ error: '유효한 교육 과정을 선택해주세요.' },
{ status: 400 }
);
}
// 등록자 존재 확인
const registrant = await prisma.user.findUnique({
where: { id: registrantId },
});
if (!registrant) {
return NextResponse.json(
{ error: '유효한 등록자를 선택해주세요.' },
{ status: 400 }
);
}
const lecture = await prisma.lecture.create({
data: {
title,
curriculumId,
registrantId,
attachmentFile: attachmentFile || null,
evaluationQuestionCount: evaluationQuestionCount || 0,
thumbnailImage: thumbnailImage || null,
},
include: {
curriculum: {
select: {
title: true,
},
},
registrant: {
select: {
name: true,
},
},
},
});
return NextResponse.json({
data: {
id: lecture.id,
courseName: lecture.curriculum.title,
lectureName: lecture.title,
attachedFile: lecture.attachmentFile || '없음',
questionCount: lecture.evaluationQuestionCount,
registrar: lecture.registrant.name || '알 수 없음',
createdAt: lecture.registeredAt.toISOString().split('T')[0],
curriculumId: lecture.curriculumId,
thumbnailImage: lecture.thumbnailImage,
},
}, { status: 201 });
} catch (error) {
console.error('Error creating lecture:', error);
return NextResponse.json(
{ error: '강좌 생성 중 오류가 발생했습니다.' },
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,152 @@
import { NextRequest, NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';
// GET: 특정 수강 관계 조회
export async function GET(
request: NextRequest,
{ params }: { params: { id: string } }
) {
try {
const userLecture = await prisma.userLecture.findUnique({
where: { id: params.id },
include: {
user: {
select: {
id: true,
name: true,
email: true,
},
},
lecture: {
include: {
curriculum: {
select: {
title: true,
},
},
},
},
},
});
if (!userLecture) {
return NextResponse.json(
{ error: '수강 관계를 찾을 수 없습니다.' },
{ status: 404 }
);
}
return NextResponse.json({
data: {
id: userLecture.id,
userId: userLecture.userId,
userName: userLecture.user.name,
userEmail: userLecture.user.email,
lectureId: userLecture.lectureId,
lectureName: userLecture.lecture.title,
courseName: userLecture.lecture.curriculum.title,
enrolledAt: userLecture.enrolledAt.toISOString().split('T')[0],
completedAt: userLecture.completedAt ? userLecture.completedAt.toISOString().split('T')[0] : null,
isCompleted: userLecture.isCompleted,
progress: userLecture.progress,
score: userLecture.score,
},
});
} catch (error) {
console.error('Error fetching user lecture:', error);
return NextResponse.json(
{ error: '수강 관계를 불러오는 중 오류가 발생했습니다.' },
{ status: 500 }
);
}
}
// PUT: 수강 관계 수정 (진행률, 완료 여부, 점수 등)
export async function PUT(
request: NextRequest,
{ params }: { params: { id: string } }
) {
try {
const body = await request.json();
const { progress, isCompleted, score } = body;
const updateData: any = {};
if (progress !== undefined) updateData.progress = Math.max(0, Math.min(100, progress));
if (isCompleted !== undefined) {
updateData.isCompleted = isCompleted;
if (isCompleted && !body.completedAt) {
updateData.completedAt = new Date();
} else if (!isCompleted) {
updateData.completedAt = null;
}
}
if (score !== undefined) updateData.score = score;
const userLecture = await prisma.userLecture.update({
where: { id: params.id },
data: updateData,
include: {
user: {
select: {
id: true,
name: true,
email: true,
},
},
lecture: {
include: {
curriculum: {
select: {
title: true,
},
},
},
},
},
});
return NextResponse.json({
data: {
id: userLecture.id,
userId: userLecture.userId,
userName: userLecture.user.name,
userEmail: userLecture.user.email,
lectureId: userLecture.lectureId,
lectureName: userLecture.lecture.title,
courseName: userLecture.lecture.curriculum.title,
enrolledAt: userLecture.enrolledAt.toISOString().split('T')[0],
completedAt: userLecture.completedAt ? userLecture.completedAt.toISOString().split('T')[0] : null,
isCompleted: userLecture.isCompleted,
progress: userLecture.progress,
score: userLecture.score,
},
});
} catch (error) {
console.error('Error updating user lecture:', error);
return NextResponse.json(
{ error: '수강 관계 수정 중 오류가 발생했습니다.' },
{ status: 500 }
);
}
}
// DELETE: 수강 관계 삭제 (수강 취소)
export async function DELETE(
request: NextRequest,
{ params }: { params: { id: string } }
) {
try {
await prisma.userLecture.delete({
where: { id: params.id },
});
return NextResponse.json({ message: '수강이 취소되었습니다.' });
} catch (error) {
console.error('Error deleting user lecture:', error);
return NextResponse.json(
{ error: '수강 취소 중 오류가 발생했습니다.' },
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,182 @@
import { NextRequest, NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';
// GET: 사용자-강좌 수강 관계 목록 조회
export async function GET(request: NextRequest) {
try {
const searchParams = request.nextUrl.searchParams;
const userId = searchParams.get('userId');
const lectureId = searchParams.get('lectureId');
const page = parseInt(searchParams.get('page') || '1');
const limit = parseInt(searchParams.get('limit') || '100');
const skip = (page - 1) * limit;
const where: any = {};
if (userId) where.userId = userId;
if (lectureId) where.lectureId = lectureId;
const [userLectures, total] = await Promise.all([
prisma.userLecture.findMany({
where,
skip,
take: limit,
orderBy: {
enrolledAt: 'desc',
},
include: {
user: {
select: {
id: true,
name: true,
email: true,
},
},
lecture: {
include: {
curriculum: {
select: {
title: true,
},
},
},
},
},
}),
prisma.userLecture.count({ where }),
]);
return NextResponse.json({
data: userLectures.map((ul) => ({
id: ul.id,
userId: ul.userId,
userName: ul.user.name,
userEmail: ul.user.email,
lectureId: ul.lectureId,
lectureName: ul.lecture.title,
courseName: ul.lecture.curriculum.title,
enrolledAt: ul.enrolledAt.toISOString().split('T')[0],
completedAt: ul.completedAt ? ul.completedAt.toISOString().split('T')[0] : null,
isCompleted: ul.isCompleted,
progress: ul.progress,
score: ul.score,
})),
pagination: {
page,
limit,
total,
totalPages: Math.ceil(total / limit),
},
});
} catch (error) {
console.error('Error fetching user lectures:', error);
return NextResponse.json(
{ error: '수강 관계 목록을 불러오는 중 오류가 발생했습니다.' },
{ status: 500 }
);
}
}
// POST: 사용자-강좌 수강 관계 생성 (수강 신청)
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const { userId, lectureId } = body;
if (!userId || !lectureId) {
return NextResponse.json(
{ error: '사용자 ID와 강좌 ID는 필수입니다.' },
{ status: 400 }
);
}
// 사용자 존재 확인
const user = await prisma.user.findUnique({
where: { id: userId },
});
if (!user) {
return NextResponse.json(
{ error: '유효한 사용자를 선택해주세요.' },
{ status: 400 }
);
}
// 강좌 존재 확인
const lecture = await prisma.lecture.findUnique({
where: { id: lectureId },
});
if (!lecture) {
return NextResponse.json(
{ error: '유효한 강좌를 선택해주세요.' },
{ status: 400 }
);
}
// 이미 수강 중인지 확인
const existing = await prisma.userLecture.findUnique({
where: {
userId_lectureId: {
userId,
lectureId,
},
},
});
if (existing) {
return NextResponse.json(
{ error: '이미 수강 중인 강좌입니다.' },
{ status: 400 }
);
}
const userLecture = await prisma.userLecture.create({
data: {
userId,
lectureId,
},
include: {
user: {
select: {
id: true,
name: true,
email: true,
},
},
lecture: {
include: {
curriculum: {
select: {
title: true,
},
},
},
},
},
});
return NextResponse.json({
data: {
id: userLecture.id,
userId: userLecture.userId,
userName: userLecture.user.name,
userEmail: userLecture.user.email,
lectureId: userLecture.lectureId,
lectureName: userLecture.lecture.title,
courseName: userLecture.lecture.curriculum.title,
enrolledAt: userLecture.enrolledAt.toISOString().split('T')[0],
completedAt: userLecture.completedAt ? userLecture.completedAt.toISOString().split('T')[0] : null,
isCompleted: userLecture.isCompleted,
progress: userLecture.progress,
score: userLecture.score,
},
}, { status: 201 });
} catch (error) {
console.error('Error creating user lecture:', error);
return NextResponse.json(
{ error: '수강 신청 중 오류가 발생했습니다.' },
{ status: 500 }
);
}
}

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

@@ -0,0 +1,59 @@
import { NextRequest, NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';
// GET: 사용자 목록 조회 (강사 목록 포함)
export async function GET(request: NextRequest) {
try {
const searchParams = request.nextUrl.searchParams;
const role = searchParams.get('role'); // 'INSTRUCTOR', 'ADMIN', 'STUDENT' 등
const page = parseInt(searchParams.get('page') || '1');
const limit = parseInt(searchParams.get('limit') || '100');
const skip = (page - 1) * limit;
const where = role ? { role: role as any } : {};
const [users, total] = await Promise.all([
prisma.user.findMany({
where,
skip,
take: limit,
orderBy: {
createdAt: 'desc',
},
select: {
id: true,
email: true,
name: true,
role: true,
isActive: true,
createdAt: true,
},
}),
prisma.user.count({ where }),
]);
return NextResponse.json({
data: users.map((user) => ({
id: user.id,
email: user.email,
name: user.name,
role: user.role,
isActive: user.isActive,
createdAt: user.createdAt.toISOString().split('T')[0],
})),
pagination: {
page,
limit,
total,
totalPages: Math.ceil(total / limit),
},
});
} catch (error) {
console.error('Error fetching users:', error);
return NextResponse.json(
{ error: '사용자 목록을 불러오는 중 오류가 발생했습니다.' },
{ status: 500 }
);
}
}

110
app/components/Header.tsx Normal file
View 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>
);
}

134
app/idfind/page.tsx Normal file
View File

@@ -0,0 +1,134 @@
'use client';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
export default function IdFindPage() {
const router = useRouter();
const [name, setName] = useState('');
const [phone, setPhone] = useState('');
const [nameError, setNameError] = useState('');
const [phoneError, setPhoneError] = useState('');
const handleNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
let value = e.target.value;
setName(value);
if (nameError) {
setNameError('');
}
};
const handlePhoneChange = (e: React.ChangeEvent<HTMLInputElement>) => {
let value = e.target.value.replace(/[^\d]/g, '');
if (value.length > 11) value = value.slice(0, 11);
setPhone(value);
if (phoneError) {
setPhoneError('');
}
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
setNameError('');
setPhoneError('');
let isValid = true;
if (!name.trim()) {
setNameError('이름을 입력해주세요.');
isValid = false;
}
if (!phone.trim()) {
setPhoneError('휴대폰 번호를 입력해주세요.');
isValid = false;
} else if (phone.length !== 11) {
setPhoneError('올바른 휴대폰 번호를 입력해주세요.');
isValid = false;
}
if (isValid) {
// TODO: 아이디 찾기 API 호출
// 임시로 다음 단계로 이동
router.push('/idfind/result');
}
};
return (
<div className="relative flex flex-col items-center bg-white pb-[100px]">
{/* 제목 */}
<h1 className="font-bold text-[#1D1D1D] text-[32px] leading-tight mt-[292px]">
</h1>
{/* 입력 폼 */}
<form
className="w-[664px] border border-[#b9b9b9] rounded-[8px] mt-[40px] px-[88px] py-[48px]"
onSubmit={handleSubmit}
>
<p className="text-[#515151] text-[16px] font-medium leading-[1.6] text-center mb-[36px]">
.
</p>
<div className="flex flex-col gap-[20px]">
{/* 이름 */}
<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="text"
value={name}
onChange={handleNameChange}
className={`h-[42px] px-[10px] text-[18px] font-medium rounded-[8px] border border-[#b9b9b9] placeholder:text-[#b9b9b9] bg-white w-[313px] ${nameError ? 'border-[#E85D5D] text-[#E85D5D]' : 'text-[#515151]'}`}
placeholder="이름을 입력해 주세요."
/>
</div>
{nameError && (
<p className="text-[13px] text-[#E85D5D] ml-[193px] mt-[16px]">{nameError}</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="text"
value={phone}
onChange={handlePhoneChange}
maxLength={11}
className={`h-[42px] px-[10px] text-[18px] font-medium rounded-[8px] border border-[#b9b9b9] placeholder:text-[#b9b9b9] bg-white w-[313px] ${phoneError ? 'border-[#E85D5D] text-[#E85D5D]' : 'text-[#515151]'}`}
placeholder="숫자만 입력해 주세요."
/>
</div>
{phoneError && (
<p className="text-[13px] text-[#E85D5D] ml-[193px] mt-[16px]">{phoneError}</p>
)}
</div>
</div>
</form>
{/* 다음 버튼 */}
<div className="mt-[54px] mb-[228px]">
<button
type="button"
onClick={handleSubmit}
className="bg-[#2B82E8] rounded-[10px] h-[55px] w-[334px] flex items-center justify-center font-medium text-white text-[18px] hover:bg-[#1669ca] transition"
>
</button>
</div>
{/* 카피라이트 */}
<footer className="absolute bottom-[40px] 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/lecturelist/page.tsx Normal file
View 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 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>
);
}

View File

@@ -5,8 +5,8 @@ import Image from 'next/image';
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import logo from '../logo.svg'; import logo from '../logo.svg';
import CheckboxOff from '../../public/svg/checkbox_off';
const checkIcon = "http://localhost:3845/assets/68720b08a673d8b68ae6482d642eeab286c9462b.svg"; import CheckboxOn from '../../public/svg/checkbox_on';
type CheckboxProps = { type CheckboxProps = {
checked: boolean; checked: boolean;
@@ -20,18 +20,9 @@ function Checkbox({ checked, onChange, label }: CheckboxProps) {
<button <button
type="button" type="button"
onClick={onChange} onClick={onChange}
className="relative w-[18px] h-[18px] rounded-[4px]" className="relative w-[18px] h-[18px] flex items-center justify-center"
> >
{checked ? ( {checked ? <CheckboxOn /> : <CheckboxOff />}
<>
<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" />
)}
</button> </button>
<span className="text-sm text-[#515151] leading-[1.6]">{label}</span> <span className="text-sm text-[#515151] leading-[1.6]">{label}</span>
</div> </div>
@@ -70,10 +61,27 @@ export default function LoginPage() {
return; return;
} }
// 아이디와 비밀번호 검증 // 관리자 계정 검증
if (username === 'admin' && password === '1234') { 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.setItem('isLoggedIn', 'true');
localStorage.removeItem('isAdminLoggedIn'); // 일반 사용자는 관리자 플래그 제거
// 아이디 기억하기 체크 시 아이디 저장 // 아이디 기억하기 체크 시 아이디 저장
if (rememberId) { if (rememberId) {
localStorage.setItem('rememberedUsername', username); localStorage.setItem('rememberedUsername', username);
@@ -206,9 +214,12 @@ export default function LoginPage() {
height={12} height={12}
className="h-3" className="h-3"
/> />
<button className="text-sm text-gray-600 hover:text-gray-800"> <Link
href="/idfind"
className="text-sm text-gray-600 hover:text-gray-800"
>
</button> </Link>
<Image <Image
src="/Divider.svg" src="/Divider.svg"
alt="" alt=""

240
app/myinfo/page.tsx Normal file
View File

@@ -0,0 +1,240 @@
"use client";
import { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
import LoginPage from '../login/page';
const imgImage2 = "http://localhost:3845/assets/89fda8e949171025b1232bae70fc9d442e4e70c8.png";
const imgLine2 = "http://localhost:3845/assets/6ee8cf4ebb6bc2adb14aab8c9940b3002c20af35.svg";
export default function MyLecturePage() {
const router = useRouter();
const [isLoggedIn, setIsLoggedIn] = useState(false);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
// 로그인 상태 확인
const loginStatus = localStorage.getItem('isLoggedIn') === 'true';
setIsLoggedIn(loginStatus);
setIsLoading(false);
}, []);
if (isLoading) {
return null; // 로딩 중
}
// 로그인되지 않았으면 로그인 페이지 표시
if (!isLoggedIn) {
return <LoginPage />;
}
return (
<div className="bg-white relative min-h-screen w-full">
{/* 헤더 */}
<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">
{/* 로고 */}
<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>
</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">
<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">
<div className="box-border content-stretch flex gap-[10px] items-center justify-center p-[10px] relative shrink-0">
<p className="font-medium leading-[normal] not-italic relative shrink-0 text-[#1669ca] text-[18px] text-nowrap whitespace-pre">
</p>
</div>
<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>
{/* 구분선 */}
<div className="absolute h-0 left-1/2 top-[150px] translate-x-[-50%] w-full">
<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 box-border content-stretch flex flex-col items-center left-[37px] pb-0 pt-4 px-0 top-[192px] 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('/mylecture')}
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('/mylecture')}
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"
>
<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">
<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">
<div className="basis-0 content-stretch flex gap-2 grow items-center min-h-px min-w-px relative shrink-0 pl-2">
<p className="[white-space-collapse:collapse] basis-0 font-medium grow leading-[1.6] min-h-px min-w-px not-italic overflow-ellipsis overflow-hidden relative shrink-0 text-[14px] text-[#404040] text-left text-nowrap">
</p>
</div>
</button>
</div>
</div>
{/* 메인 콘텐츠 - 내 정보 수정 */}
<div className="absolute content-stretch flex flex-col gap-6 items-start left-[calc(31.25%+35px)] top-[243px] w-[784px]">
<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-[24px] text-[#1d1d1d] text-nowrap">
<p className="leading-[1.6] whitespace-pre"> </p>
</div>
</div>
<div className="content-stretch flex flex-col gap-4 items-start relative shrink-0">
{/* 아이디 */}
<div className="content-stretch flex flex-col gap-4 items-start relative shrink-0 w-[789px]">
<div className="bg-white border border-[#b9b9b9] border-solid box-border content-stretch flex gap-[138px] items-center justify-center p-5 relative rounded-2xl shrink-0 w-full">
<p className="font-medium leading-[normal] not-italic relative shrink-0 text-[14px] text-[#515151] text-nowrap whitespace-pre">
</p>
<div className="flex flex-row items-center self-stretch">
<div className="bg-white box-border content-stretch flex gap-[10px] h-full items-center overflow-clip px-2 py-[10px] relative rounded-lg shrink-0 w-[559px]">
<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-[#b9b9b9] text-[14px] text-nowrap">
qwer1234@naver.com
</p>
</div>
</div>
</div>
</div>
{/* 비밀번호 변경 */}
<div className="content-stretch flex flex-col gap-4 items-start relative shrink-0 w-[789px]">
<div className="bg-white border border-[#b9b9b9] border-solid box-border content-stretch flex gap-[109px] items-center p-5 relative rounded-2xl shrink-0 w-full">
<p className="font-medium leading-[normal] not-italic relative shrink-0 text-[14px] text-[#515151] text-nowrap whitespace-pre">
</p>
<div className="box-border content-stretch flex gap-2 items-center px-0 py-[9px] relative shrink-0">
<button className="bg-white border border-[#b9b9b9] border-solid box-border content-stretch flex gap-[10px] items-center justify-center px-2 py-1 relative rounded-md shrink-0 w-[256px] cursor-pointer hover:bg-gray-50 transition-colors">
<p className="font-medium leading-[1.6] not-italic relative shrink-0 text-[14px] text-black text-center text-nowrap whitespace-pre">
</p>
</button>
</div>
</div>
</div>
{/* 회원 탈퇴 */}
<div className="content-stretch flex flex-col gap-4 items-start relative shrink-0 w-[789px]">
<div className="bg-white border border-[#b9b9b9] border-solid box-border content-stretch flex gap-[109px] items-center p-5 relative rounded-2xl shrink-0 w-full">
<p className="font-medium leading-[normal] not-italic relative shrink-0 text-[14px] text-[#515151] w-[77px]">
</p>
<div className="box-border content-stretch flex gap-2 items-center px-0 py-[9px] relative shrink-0">
<button className="bg-white border border-[#b9b9b9] border-solid box-border content-stretch flex gap-[10px] items-center justify-center px-2 py-1 relative rounded-md shrink-0 w-[256px] cursor-pointer hover:bg-gray-50 transition-colors">
<p className="font-medium leading-[1.6] not-italic relative shrink-0 text-[14px] text-black text-center text-nowrap whitespace-pre">
</p>
</button>
</div>
</div>
</div>
</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-[854px] w-full">
<div className="content-stretch flex gap-[49px] 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 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>
);
}

370
app/mylecture/page.tsx Normal file
View File

@@ -0,0 +1,370 @@
"use client";
import { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
import LoginPage from '../login/page';
import Image from 'next/image';
import logo from '../logo.svg';
import RadioOff from '../../public/svg/radio_off';
import RadioOn from '../../public/svg/radio_on';
const imgImage2 = "http://localhost:3845/assets/89fda8e949171025b1232bae70fc9d442e4e70c8.png";
const imgLine2 = "http://localhost:3845/assets/6ee8cf4ebb6bc2adb14aab8c9940b3002c20af35.svg";
type CourseStatus = 'all' | 'completed' | 'in-progress';
interface Course {
id: number;
title: string;
progress: number;
score: number;
maxScore: number;
status: 'in-progress' | 'completed' | 'not-started';
}
export default function MyLecturePage() {
const router = useRouter();
const [isLoggedIn, setIsLoggedIn] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const [filter, setFilter] = useState<CourseStatus>('all');
// 샘플 강좌 데이터
const [courses] = useState<Course[]>([
{
id: 1,
title: '원자로 기본 원리와 주료 계동 원리',
progress: 10,
score: 0,
maxScore: 100,
status: 'in-progress',
},
{
id: 2,
title: '핵연료 제조 공정 및 특성',
progress: 10,
score: 0,
maxScore: 100,
status: 'in-progress',
},
{
id: 3,
title: '핵분열과 핵연료 주기 이해',
progress: 10,
score: 0,
maxScore: 100,
status: 'completed',
},
{
id: 4,
title: '핵연료 성능 평가와 열수력 해석',
progress: 10,
score: 0,
maxScore: 100,
status: 'not-started',
},
]);
useEffect(() => {
// 로그인 상태 확인
const loginStatus = localStorage.getItem('isLoggedIn') === 'true';
setIsLoggedIn(loginStatus);
setIsLoading(false);
}, []);
if (isLoading) {
return null; // 로딩 중
}
// 로그인되지 않았으면 로그인 페이지 표시
if (!isLoggedIn) {
return <LoginPage />;
}
// 필터에 따른 강좌 필터링
const filteredCourses = courses.filter((course) => {
if (filter === 'all') return true;
if (filter === 'completed') return course.status === 'completed';
if (filter === 'in-progress') return course.status === 'in-progress';
return true;
});
return (
<div className="bg-white relative min-h-screen w-full">
{/* 헤더 */}
<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">
{/* 로고 */}
<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>
</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">
<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">
<div className="box-border content-stretch flex gap-[10px] items-center justify-center p-[10px] relative shrink-0">
<p className="font-medium leading-[normal] not-italic relative shrink-0 text-[#1669ca] text-[18px] text-nowrap whitespace-pre">
</p>
</div>
<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>
{/* 구분선 */}
<div className="absolute h-0 left-1/2 top-[150px] translate-x-[-50%] w-full">
<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 box-border content-stretch flex flex-col items-center left-[37px] pb-0 pt-4 px-0 top-[192px] 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('/myinfo')}
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"
>
<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('/mylecture')}
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">
<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">
<div className="basis-0 content-stretch flex gap-2 grow items-center min-h-px min-w-px relative shrink-0 pl-2">
<p className="[white-space-collapse:collapse] basis-0 font-medium grow leading-[1.6] min-h-px min-w-px not-italic overflow-ellipsis overflow-hidden relative shrink-0 text-[14px] text-[#404040] text-left text-nowrap">
</p>
</div>
</button>
</div>
</div>
{/* 메인 콘텐츠 - 내 강좌실 */}
<div className="absolute content-stretch flex flex-col gap-[32px] items-start justify-end left-[calc(12.5%+101px)] top-[196px]">
{/* 필터 섹션 */}
<div className="content-stretch flex gap-[20px] items-center relative shrink-0">
<div className="content-stretch flex flex-col gap-[20px] items-start relative shrink-0 w-[717px]">
<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-[28px] text-nowrap whitespace-pre">
</p>
</div>
<div className="content-stretch flex gap-[20px] items-center relative shrink-0">
<button
onClick={() => setFilter('all')}
className="content-stretch flex gap-[10px] items-center relative shrink-0 cursor-pointer"
>
<div className="relative shrink-0 size-[25px]">
{filter === 'all' ? <RadioOn /> : <RadioOff />}
</div>
<p className="font-medium leading-[normal] not-italic relative shrink-0 text-[#515151] text-[18px] text-nowrap whitespace-pre">
</p>
</button>
<button
onClick={() => setFilter('completed')}
className="content-stretch flex gap-[10px] items-center relative shrink-0 cursor-pointer"
>
<div className="relative shrink-0 size-[24px]">
{filter === 'completed' ? <RadioOn /> : <RadioOff />}
</div>
<p className="font-medium leading-[normal] not-italic relative shrink-0 text-[#515151] text-[18px] text-nowrap whitespace-pre">
</p>
</button>
<button
onClick={() => setFilter('in-progress')}
className="content-stretch flex gap-[10px] items-center relative shrink-0 cursor-pointer"
>
<div className="relative shrink-0 size-[24px]">
{filter === 'in-progress' ? <RadioOn /> : <RadioOff />}
</div>
<p className="font-medium leading-[normal] not-italic relative shrink-0 text-[#515151] text-[18px] text-nowrap whitespace-pre">
</p>
</button>
</div>
</div>
</div>
{/* 강좌 리스트 */}
<div className="border border-[#b9b9b9] border-solid box-border content-stretch flex flex-col gap-[30px] items-start p-[20px] relative rounded-[10px] shrink-0 w-[1347px]">
{filteredCourses.map((course, index) => (
<div key={course.id}>
<div className="content-stretch flex gap-[26px] items-center relative shrink-0 w-full">
{/* 썸네일 */}
<div className="bg-[#f7f7f7] box-border content-stretch flex gap-[10px] h-[165px] items-center justify-center px-[20px] py-[10px] relative rounded-[10px] shrink-0 w-[165px]">
<p className="font-bold leading-[normal] not-italic relative shrink-0 text-[#b9b9b9] text-[24px] text-nowrap whitespace-pre">
</p>
</div>
{/* 강좌 정보 */}
<div className="basis-0 content-stretch flex flex-col grow items-start justify-center min-h-px min-w-px relative shrink-0">
<div className="box-border content-stretch flex gap-[133px] items-center justify-center p-[10px] relative shrink-0 w-full">
<div className="content-stretch flex flex-col gap-[10px] items-start leading-[normal] not-italic relative shrink-0 w-[392px]">
<p className="font-bold relative shrink-0 text-[#515151] text-[20px] w-full">
{course.title}
</p>
<p className="font-medium relative shrink-0 text-[#b9b9b9] text-[18px] w-full">
: {course.progress}%
</p>
<p className="font-medium relative shrink-0 text-[#b9b9b9] text-[18px] w-full">
: {course.score} / {course.maxScore}
</p>
</div>
{/* 액션 버튼들 */}
<div className="content-stretch flex gap-[10px] items-center justify-end relative shrink-0">
{/* 상태 표시 */}
<div className="box-border content-stretch flex gap-[10px] h-[50px] items-center justify-center p-[10px] relative rounded-[10px] shrink-0 w-[264px]">
<p className="font-bold leading-[normal] not-italic relative shrink-0 text-[#f25200] text-[18px] text-nowrap whitespace-pre">
{course.status === 'in-progress' && '수강중'}
{course.status === 'completed' && '수강 완료'}
{course.status === 'not-started' && '수강 전'}
</p>
</div>
{/* 수강 취소 버튼 */}
{course.status !== 'completed' && (
<button className="border border-[#2b82e8] border-solid box-border content-stretch flex gap-[10px] h-[50px] items-center justify-center p-[10px] relative rounded-[10px] shrink-0 w-[124px] cursor-pointer hover:bg-blue-50 transition-colors">
<p className="font-medium leading-[normal] not-italic relative shrink-0 text-[#515151] text-[18px] text-nowrap whitespace-pre">
</p>
</button>
)}
{/* 이어서 수강 / 수강하기 버튼 */}
{course.status === 'in-progress' && (
<button className="bg-[#599ded] box-border content-stretch flex gap-[10px] h-[50px] items-center justify-center p-[10px] relative rounded-[10px] shrink-0 w-[124px] 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>
)}
{course.status === 'not-started' && (
<button className="bg-[#599ded] box-border content-stretch flex gap-[10px] h-[50px] items-center justify-center p-[10px] relative rounded-[10px] shrink-0 w-[124px] 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>
)}
{course.status === 'completed' && (
<button className="bg-[#599ded] box-border content-stretch flex gap-[10px] h-[50px] items-center justify-center p-[10px] relative rounded-[10px] shrink-0 w-[124px] 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>
)}
</div>
</div>
</div>
</div>
{/* 구분선 */}
{index < filteredCourses.length - 1 && (
<div className="bg-[#eeeeee] h-px shrink-0 w-full mt-[30px]" />
)}
</div>
))}
</div>
</div>
{/* 푸터 */}
<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]">
<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 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>
);
}

View File

@@ -2,19 +2,69 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import Image from 'next/image';
import { isAdminLoggedIn } from '../lib/auth';
import LoginPage from './login/page'; import LoginPage from './login/page';
import Header from './components/Header';
const imgImage2 = "http://localhost:3845/assets/89fda8e949171025b1232bae70fc9d442e4e70c8.png";
const imgImage7 = "http://localhost:3845/assets/a4e4d09643b890b56084560cc24d6e532a03487b.png";
const imgLine2 = "http://localhost:3845/assets/6ee8cf4ebb6bc2adb14aab8c9940b3002c20af35.svg";
const imgFrame2616314 = "http://localhost:3845/assets/17f021e324ee315bdf2fe96554a2260813957042.svg";
const imgRectangle1737 = "http://localhost:3845/assets/ae523ea10901c105fdbfda27ed21dd658fc4a7c2.png";
const imgRectangle1738 = "http://localhost:3845/assets/50e850999bbdd551763a187d402169c28ffecec5.png";
export default function HomePage() { export default function HomePage() {
const router = useRouter(); const router = useRouter();
const [isLoggedIn, setIsLoggedIn] = useState(false); const [isLoggedIn, setIsLoggedIn] = useState(false);
const [isAdmin, setIsAdmin] = useState(false);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [currentHeroSlide, setCurrentHeroSlide] = useState(0);
// 임시 데이터 - 실제로는 API에서 가져올 데이터
const [courses, setCourses] = useState([
{ id: 1, title: '원자로 운전 및 계통', image: imgRectangle1737 },
{ id: 2, title: '핵 연료', image: imgRectangle1738 },
{ id: 3, title: '방사선 안전', image: imgRectangle1737 },
{ id: 4, title: '방사성 폐기물', image: imgRectangle1737 },
]);
const [myCourses, setMyCourses] = useState([
{ id: 1, title: '원자로 기본 원리와 주요 계통 이해 - 이론 1', date: '2025-12-12(화)' },
{ id: 2, title: '원자로 기동 및 정상운전 절차 - 이론2', date: '2025-12-12(화)' },
{ id: 3, title: '비상 시 운전원 조치와 안전 계통 운용', date: '2025-12-12(화)' },
{ id: 4, title: '운전 사례 분석 및 시뮬레이션 실습', date: '2025-12-12(화)' },
{ id: 5, title: '핵분열과 핵연로 주기 이해', date: '2025-12-12(화)' },
{ id: 6, title: '핵연료 제조 공정 및 특성', date: '2025-12-12(화)' },
{ id: 7, title: '핵연로 성능 평가와 열수력 해석', date: '2025-12-12(화)' },
]);
const [notices, setNotices] = useState([
{ id: 1, title: '[점검] 방사선 폐기 VR 실습 서버 정기점검 안내', date: '2025-12-12(화)' },
{ id: 2, title: '[점검] 방사선 폐기 VR 실습 서버 정기점검 안내', date: '2025-12-12(화)' },
{ id: 3, title: '[점검] 방사선 폐기 VR 실습 서버 정기점검 안내', date: '2025-12-12(화)' },
{ id: 4, title: '[점검] 방사선 폐기 VR 실습 서버 정기점검 안내', date: '2025-12-12(화)' },
{ id: 5, title: '[점검] 방사선 폐기 VR 실습 서버 정기점검 안내', date: '2025-12-12(화)' },
{ id: 6, title: '[점검] 방사선 폐기 VR 실습 서버 정기점검 안내', date: '2025-12-12(화)' },
{ id: 7, title: '[점검] 방사선 폐기 VR 실습 서버 정기점검 안내', date: '2025-12-12(화)' },
{ id: 8, title: '[점검] 방사선 폐기 VR 실습 서버 정기점검 안내', date: '2025-12-12(화)' },
{ id: 9, title: '[점검] 방사선 폐기 VR 실습 서버 정기점검 안내', date: '2025-12-12(화)' },
{ id: 10, title: '[점검] 방사선 폐기 VR 실습 서버 정기점검 안내', date: '2025-12-12(화)' },
]);
useEffect(() => { useEffect(() => {
// 로그인 상태 확인 // 로그인 상태 확인
const loginStatus = localStorage.getItem('isLoggedIn') === 'true'; const loginStatus = localStorage.getItem('isLoggedIn') === 'true';
const adminStatus = isAdminLoggedIn();
setIsLoggedIn(loginStatus); setIsLoggedIn(loginStatus);
setIsAdmin(adminStatus);
setIsLoading(false); setIsLoading(false);
}, []);
// 관리자일 경우 admin_home 페이지로 리다이렉트
if (adminStatus) {
router.push('/admin_home');
}
}, [router]);
if (isLoading) { if (isLoading) {
return null; // 로딩 중 return null; // 로딩 중
@@ -25,24 +75,144 @@ export default function HomePage() {
return <LoginPage />; return <LoginPage />;
} }
// 로그인되었으면 메인 페이지 표시 // 관리자일 경우 리다이렉트 중
return ( if (isAdmin) {
<div className="min-h-screen bg-gray-50"> return null;
<div className="container mx-auto px-4 py-8"> }
<h1 className="text-3xl font-bold mb-4"> </h1>
<p className="text-gray-600"> .</p>
{/* 로그아웃 버튼 */} // 일반 사용자일 경우 기존 메인 페이지 표시
<button return (
onClick={() => { <div className="bg-white relative min-h-screen w-full pb-[199px]">
localStorage.removeItem('isLoggedIn'); {/* 헤더 */}
router.push('/'); <Header activePage="home" />
}}
className="mt-4 px-4 py-2 bg-red-500 text-white rounded-md hover:bg-red-600" {/* 구분선 */}
> <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]">
</button> <img alt="" className="block max-w-none size-full" src={imgLine2} />
</div> </div>
</div> </div>
{/* Hero 배너 */}
<div className="absolute h-[402px] left-[-1px] top-[272px] w-full bg-[#b9b9b9]">
{/* TODO: DB에서 이미지를 가져와서 표시 */}
</div>
{/* 전체 교육 과정 (4개) */}
<div className="absolute content-stretch flex flex-col gap-[7px] items-start justify-end left-[43px] top-[748px] w-[1435px]">
<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-[28px] text-nowrap whitespace-pre">
({courses.length})
</p>
</div>
</div>
</div>
{/* 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>
{/* 교육 과정 카드들 - 데이터가 있을 때 */}
{courses.length > 0 ? (
<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 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">
{course.title}
</p>
</div>
</div>
))}
</div>
) : (
/* 등록된 교육 과정이 없습니다 메시지 - 데이터가 없을 때 */
<p className="absolute font-medium leading-[normal] left-[calc(43.75%+122.563px)] not-italic text-[#b9b9b9] text-[18px] text-center top-[964px] translate-x-[-50%] w-[268.5px]">
.
</p>
)}
{/* 나의 수강 강좌 목록 */}
<div className="absolute content-stretch flex flex-col gap-[7px] items-start justify-end left-[43px] top-[1213px] w-[866px]">
<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-[28px] text-nowrap whitespace-pre">
({myCourses.length})
</p>
</div>
</div>
<div className="border border-[#b9b9b9] border-solid box-border content-stretch flex flex-col items-start p-[20px] relative rounded-[10px] shrink-0 w-full">
{myCourses.length > 0 ? (
myCourses.map((course) => (
<div key={course.id} className="box-border content-stretch flex font-medium gap-[269px] items-center justify-center leading-[normal] not-italic p-[10px] relative shrink-0 text-[#515151] text-[18px] w-full">
<p className="relative shrink-0 w-[342px]">
{course.title}
</p>
<p className="basis-0 grow min-h-px min-w-px relative shrink-0 text-right">
{course.date}
</p>
</div>
))
) : (
<div className="box-border content-stretch flex gap-[269px] items-center justify-center p-[10px] relative shrink-0 w-full">
<p className="basis-0 font-medium grow leading-[normal] min-h-px min-w-px not-italic relative shrink-0 text-[#b9b9b9] text-[18px]">
.
</p>
</div>
)}
</div>
</div>
{/* 공지사항 */}
<div className="absolute content-stretch flex flex-col gap-[7px] items-start justify-end left-[calc(50%-9.5px)] top-[1213px] w-[914px]">
<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-[28px] text-nowrap whitespace-pre">
</p>
</div>
</div>
<div className="border border-[#b9b9b9] border-solid box-border content-stretch flex flex-col h-[498px] items-start p-[20px] relative rounded-[10px] shrink-0 w-full">
{notices.length > 0 ? (
notices.map((notice) => (
<div key={notice.id} className="box-border content-stretch flex font-medium items-center justify-between leading-[normal] not-italic p-[10px] relative shrink-0 text-[#515151] text-[18px] w-full">
<p className="basis-0 grow min-h-px min-w-px relative shrink-0">
{notice.title}
</p>
<p className="basis-0 grow min-h-px min-w-px relative shrink-0 text-right">
{notice.date}
</p>
</div>
))
) : (
<div className="box-border content-stretch flex gap-[269px] items-center justify-center p-[10px] relative shrink-0 w-full">
<p className="basis-0 font-medium grow leading-[normal] min-h-px min-w-px not-italic relative shrink-0 text-[#b9b9b9] text-[18px]">
.
</p>
</div>
)}
</div>
</div>
{/* 공지사항 하단 여백 (공지사항 최하단: 1213 + 제목영역 + gap + 박스높이 + 199px) */}
<div className="absolute left-0 top-[1967px] w-full h-[199px]" />
</div>
); );
} }

64
app/pages/page.tsx Normal file
View 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
View 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>
);
}

View File

@@ -3,6 +3,11 @@
import { useState } from 'react'; import { useState } from 'react';
import Link from 'next/link'; import Link from 'next/link';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import RadioOff from '../../public/svg/radio_off';
import RadioOn from '../../public/svg/radio_on';
import CheckOff from '../../public/svg/check_off';
import CheckOn from '../../public/svg/check_on';
import ChevronSmall from '../../public/svg/chevron_small';
const imgRiCheckboxCircleLine = "http://localhost:3845/assets/e4c498605e2559d2764a3112ae9a9019e6ad798e.svg"; const imgRiCheckboxCircleLine = "http://localhost:3845/assets/e4c498605e2559d2764a3112ae9a9019e6ad798e.svg";
const imgFormkitRadio = "http://localhost:3845/assets/ea30a9a80d95ced4bfb1174d3a8475a4a1dbbabb.svg"; const imgFormkitRadio = "http://localhost:3845/assets/ea30a9a80d95ced4bfb1174d3a8475a4a1dbbabb.svg";
@@ -38,8 +43,15 @@ export default function RegisterPage() {
passwordConfirm: '', passwordConfirm: '',
}); });
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => { const [verificationCode, setVerificationCode] = useState('');
const [isVerificationSent, setIsVerificationSent] = useState(false);
const [isVerificationComplete, setIsVerificationComplete] = useState(false);
const [verificationError, setVerificationError] = useState('');
const [activeSelect, setActiveSelect] = useState<string | null>(null);
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
const { name, value } = e.target; const { name, value } = e.target;
const prevValue = formData[name as keyof typeof formData];
setFormData((prev) => ({ setFormData((prev) => ({
...prev, ...prev,
[name]: value, [name]: value,
@@ -50,6 +62,38 @@ export default function RegisterPage() {
[name]: '', [name]: '',
})); }));
} }
// 이메일이 실제로 변경되었을 때만 인증 상태 초기화
if (name === 'email' && prevValue !== value && isVerificationSent) {
setIsVerificationSent(false);
setVerificationCode('');
setIsVerificationComplete(false);
}
// 비밀번호 확인 실시간 검증
if (name === 'passwordConfirm' && value && formData.password && value !== formData.password) {
setErrors((prev) => ({
...prev,
passwordConfirm: '비밀번호와 일치하지 않아요.',
}));
} else if (name === 'passwordConfirm' && value && formData.password && value === formData.password) {
setErrors((prev) => ({
...prev,
passwordConfirm: '',
}));
}
// 비밀번호가 변경되면 비밀번호 확인도 다시 검증
if (name === 'password' && formData.passwordConfirm) {
if (formData.passwordConfirm !== value) {
setErrors((prev) => ({
...prev,
passwordConfirm: '비밀번호와 일치하지 않아요.',
}));
} else {
setErrors((prev) => ({
...prev,
passwordConfirm: '',
}));
}
}
}; };
const handlePhoneChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handlePhoneChange = (e: React.ChangeEvent<HTMLInputElement>) => {
@@ -119,7 +163,33 @@ export default function RegisterPage() {
return; return;
} }
// TODO: 인증번호 전송 API 호출 // TODO: 인증번호 전송 API 호출
alert('인증번호가 전송되었습니다.'); setIsVerificationSent(true);
setVerificationCode('');
setIsVerificationComplete(false);
setVerificationError('');
};
const handleVerificationCodeChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setVerificationCode(e.target.value);
// 인증번호 입력 시 에러 초기화
if (verificationError) {
setVerificationError('');
}
};
const handleVerifyCode = () => {
if (!verificationCode.trim()) {
return;
}
// TODO: 인증번호 확인 API 호출
// API 연동 전까지 맞는 인증번호는 123456
if (verificationCode.trim() === '123456') {
setIsVerificationComplete(true);
setVerificationError('');
} else {
// 인증번호가 틀렸을 때 에러 메시지 표시
setVerificationError('인증번호가 잘못되었습니다.');
}
}; };
const validateForm = () => { const validateForm = () => {
@@ -192,296 +262,369 @@ export default function RegisterPage() {
agreements.privacy; agreements.privacy;
return ( return (
<div className="bg-white relative min-h-screen flex flex-col items-center"> <div className="relative min-h-screen flex flex-col items-center bg-white pb-[100px]">
{/* 제목 */} {/* 제목 */}
<p className="font-bold text-[#1d1d1d] text-[32px] leading-normal mt-[146px] mb-0"> <h1 className="font-bold text-[#1D1D1D] text-[32px] leading-tight mt-[100px]">
</p> </h1>
{/* 회원정보 입력 폼 */} {/* 회원정보 입력 폼 */}
<div className="mt-[160px] w-[767px] flex flex-col gap-[61px]"> <form
className="w-[829px] border border-[#b9b9b9] rounded-[8px] mt-[56px] px-[31px] py-[29px]"
autoComplete="off"
onSubmit={handleSubmit}
>
<div className="flex flex-col gap-[20px]">
{/* 이름 */} {/* 이름 */}
<div className="flex items-center gap-[10px]"> <div className="flex flex-col">
<p className="font-medium text-[#515151] text-[18px] leading-normal w-[177px]"> <div className="flex gap-[16px] items-center h-[41px]">
<span className="text-[#515151] text-[18px] font-medium w-[177px]">
</p> </span>
<div className={`border border-solid box-border flex items-center p-[10px] rounded-[8px] flex-1 ${errors.name ? 'border-red-500' : 'border-[#b9b9b9]'}`}>
<input <input
type="text" type="text"
name="name" name="name"
value={formData.name} value={formData.name}
onChange={handleChange} onChange={handleChange}
className="flex-1 outline-none font-medium text-[18px] text-[#1d1d1d] placeholder:text-[#b9b9b9]" 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 ${errors.name ? 'border-[#E85D5D] text-[#E85D5D]' : 'border-[#b9b9b9] text-[#515151]'}`}
placeholder="이름을 입력해 주세요." placeholder="이름을 입력해 주세요."
/> />
</div> </div>
{errors.name && (
<p className="text-[13px] text-[#E85D5D] ml-[193px] mt-[16px]">{errors.name}</p>
)}
</div> </div>
{/* 휴대폰 */} {/* 휴대폰 */}
<div className="flex items-center gap-[10px]"> <div className="flex flex-col">
<p className="font-medium text-[#515151] text-[18px] leading-normal w-[177px]"> <div className="flex gap-[16px] items-center h-[41px]">
<span className="text-[#515151] text-[18px] font-medium w-[177px]">
</p> </span>
<div className={`border border-solid box-border flex items-center p-[10px] rounded-[8px] flex-1 ${errors.phone ? 'border-red-500' : 'border-[#b9b9b9]'}`}>
<input <input
type="text" type="text"
name="phone" name="phone"
value={formData.phone} value={formData.phone}
onChange={handlePhoneChange} onChange={handlePhoneChange}
className="flex-1 outline-none font-medium text-[18px] text-[#1d1d1d] placeholder:text-[#b9b9b9]"
placeholder="-없이 입력해 주세요."
maxLength={11} maxLength={11}
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 ${errors.phone ? 'border-[#E85D5D] text-[#E85D5D]' : 'border-[#b9b9b9] text-[#515151]'}`}
placeholder="-없이 입력해 주세요."
/> />
</div> </div>
{errors.phone && (
<p className="text-[13px] text-[#E85D5D] ml-[193px] mt-[16px]">{errors.phone}</p>
)}
</div> </div>
{/* 이메일(아이디) */} {/* 이메일(아이디) + 인증번호 발송 */}
<div className="flex items-center gap-[10px]"> <div className="flex flex-col">
<p className="font-medium text-[#515151] text-[18px] leading-normal w-[177px]"> <div className="flex gap-[16px] items-center h-[42px]">
<span className="text-[#515151] text-[18px] font-medium w-[177px]">
() ()
</p> </span>
<div className={`border border-solid box-border flex items-center p-[10px] rounded-[8px] ${errors.email ? 'border-red-500' : 'border-[#b9b9b9]'} w-[401px]`}>
<input <input
type="email" type="email"
name="email" name="email"
value={formData.email} value={formData.email}
onChange={handleChange} onChange={handleChange}
className="flex-1 outline-none font-medium text-[18px] text-[#1d1d1d] placeholder:text-[#b9b9b9]" 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 ${errors.email ? 'border-[#E85D5D] text-[#E85D5D]' : 'border-[#b9b9b9] text-[#515151]'}`}
placeholder="이메일을 입력해 주세요." placeholder="이메일을 입력해 주세요."
/> />
</div>
<button <button
type="button" type="button"
onClick={handleSendVerificationCode} onClick={handleSendVerificationCode}
className="bg-[#2b82e8] box-border flex items-center justify-center p-[10px] rounded-[10px] w-[158px] h-[43px]" className="bg-[#2B82E8] rounded-[10px] px-[10px] text-[18px] font-medium text-white h-[42px] w-[158px] transition hover:bg-[#1669ca]"
> >
<p className="font-medium text-[18px] text-white text-center"> {isVerificationSent ? '인증번호 재발송' : '인증번호 발송'}
</p>
</button> </button>
</div> </div>
{errors.email && (
<p className="text-[13px] text-[#E85D5D] ml-[193px] mt-[16px]">{errors.email}</p>
)}
</div>
{/* 인증번호 입력 폼 */}
{isVerificationSent && (
<div className="flex flex-col">
<div className="flex gap-[16px] items-center h-[41px]">
<span className="text-[#515151] text-[18px] font-medium w-[177px]">
</span>
<input
type="text"
value={verificationCode}
onChange={handleVerificationCodeChange}
className="h-[42px] px-[10px] text-[18px] font-medium rounded-[8px] border border-[#1669ca] text-[#515151] bg-white w-[401px] focus:border-[#1669CA] focus:outline-none"
placeholder="인증번호를 입력해 주세요."
/>
<button
type="button"
onClick={handleVerifyCode}
disabled={!verificationCode.trim() || isVerificationComplete}
className={`rounded-[10px] h-[43px] w-[158px] text-[18px] font-medium transition flex items-center justify-center
${verificationCode.trim() && !isVerificationComplete
? 'bg-[#2B82E8] text-white hover:bg-[#1669ca]'
: 'bg-[#b9b9b9] text-white cursor-not-allowed'
}`}
>
</button>
</div>
{verificationError && (
<p className="text-[13px] text-[#E85D5D] ml-[193px] mt-[16px]">{verificationError}</p>
)}
{!isVerificationComplete && !verificationError && (
<div className="flex items-center ml-[193px] mt-[16px]">
<p className="text-[#1669ca] text-[12px] font-medium leading-[14px] max-w-[575px]">
.<br /> .
</p>
</div>
)}
{isVerificationComplete && (
<div className="flex items-center ml-[193px] mt-[16px]">
<p className="text-[#1669ca] text-[12px] font-medium">
.
</p>
</div>
)}
</div>
)}
{/* 비밀번호 */} {/* 비밀번호 */}
<div className="flex items-center gap-[10px]"> <div className="flex flex-col">
<p className="font-medium text-[#515151] text-[18px] leading-normal w-[177px]"> <div className="flex gap-[16px] items-center h-[41px]">
<span className="text-[#515151] text-[18px] font-medium w-[177px]">
</p> </span>
<div className={`border border-solid box-border flex items-center p-[10px] rounded-[8px] flex-1 ${errors.password ? 'border-red-500' : 'border-[#b9b9b9]'}`}>
<input <input
type="password" type="password"
name="password" name="password"
value={formData.password} value={formData.password}
onChange={handleChange} onChange={handleChange}
className="flex-1 outline-none font-medium text-[18px] text-[#1d1d1d] placeholder:text-[#b9b9b9]" 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 ${errors.password ? 'border-[#E85D5D] text-[#E85D5D]' : 'border-[#b9b9b9] text-[#515151]'}`}
placeholder="8~16자의 영문/숫자/특수문자를 조합해서 입력해 주세요." placeholder="8~16자의 영문/숫자/특수문자를 조합해서 입력해 주세요."
/> />
</div> </div>
{errors.password && (
<p className="text-[13px] text-[#E85D5D] ml-[193px] mt-[16px]">{errors.password}</p>
)}
</div> </div>
{/* 비밀번호 확인 */} {/* 비밀번호 확인 */}
<div className="flex items-center gap-[10px]"> <div className="flex flex-col">
<p className="font-medium text-[#515151] text-[18px] leading-normal w-[177px]"> <div className="flex gap-[16px] items-center h-[41px]">
<span className="text-[#515151] text-[18px] font-medium w-[177px]">
</p> </span>
<div className={`border border-solid box-border flex items-center p-[10px] rounded-[8px] flex-1 ${errors.passwordConfirm ? 'border-red-500' : 'border-[#b9b9b9]'}`}>
<input <input
type="password" type="password"
name="passwordConfirm" name="passwordConfirm"
value={formData.passwordConfirm} value={formData.passwordConfirm}
onChange={handleChange} onChange={handleChange}
className="flex-1 outline-none font-medium text-[18px] text-[#1d1d1d] placeholder:text-[#b9b9b9]" className={`flex-1 h-[42px] px-[10px] text-[18px] font-medium rounded-[8px] border placeholder:text-[#b9b9b9] bg-white text-[#000000] focus:outline-none ${formData.passwordConfirm && formData.password && formData.passwordConfirm !== formData.password
? 'border-[#E61A1A] focus:border-[#E61A1A]'
: errors.passwordConfirm
? 'border-[#E85D5D] focus:border-[#1669CA]'
: 'border-[#b9b9b9] focus:border-[#1669CA]'
}`}
placeholder="비밀번호를 다시 입력해 주세요." placeholder="비밀번호를 다시 입력해 주세요."
/> />
</div> </div>
{formData.passwordConfirm && formData.password && formData.passwordConfirm !== formData.password && (
<p className="text-[13px] text-[#E61A1A] ml-[193px] mt-[16px]"> .</p>
)}
{errors.passwordConfirm && formData.passwordConfirm === formData.password && (
<p className="text-[13px] text-[#E85D5D] ml-[193px] mt-[16px]">{errors.passwordConfirm}</p>
)}
</div> </div>
{/* 성별 */} {/* 성별 */}
<div className="flex items-start gap-[14px]"> <div className="flex gap-[16px] items-center h-[24px]">
<p className="font-medium text-[#515151] text-[18px] leading-normal w-[177px]"> <span className="text-[#515151] text-[18px] font-medium w-[177px]">
</p> </span>
<div className="flex gap-[14px] items-center"> <div className="flex gap-[14px] items-center">
<div className="flex gap-[10px] items-center cursor-pointer" onClick={() => handleGenderChange('male')}> <button
<div className="relative w-[24px] h-[24px] shrink-0"> type="button"
{formData.gender === 'male' ? ( className="flex items-center gap-[10px]"
<img alt="" className="block max-w-none w-full h-full" src={imgFormkitRadio} /> onClick={() => handleGenderChange('male')}
) : ( >
<img alt="" className="block max-w-none w-full h-full" src={imgAkarIconsRadio} /> {formData.gender === 'male' ? <RadioOn /> : <RadioOff />}
)} <span className="text-[#515151] text-[14px] font-medium leading-[1.6]">
</div>
<p className="font-medium text-[14px] text-[#515151] leading-[1.6]"></p> </span>
</div> </button>
<div className="flex gap-[10px] items-center cursor-pointer" onClick={() => handleGenderChange('female')}> <button
<div className="relative w-[24px] h-[24px] shrink-0"> type="button"
{formData.gender === 'female' ? ( className="flex items-center gap-[10px]"
<img alt="" className="block max-w-none w-full h-full" src={imgFormkitRadio} /> onClick={() => handleGenderChange('female')}
) : ( >
<img alt="" className="block max-w-none w-full h-full" src={imgAkarIconsRadio} /> {formData.gender === 'female' ? <RadioOn /> : <RadioOff />}
)} <span className="text-[#515151] text-[14px] font-medium leading-[1.6]">
</div>
<p className="font-medium text-[14px] text-[#515151] leading-[1.6]"></p> </span>
</div> </button>
</div> </div>
</div> </div>
{/* 생년월일 */} {/* 생년월일 */}
<div className="flex items-center gap-[10px]"> <div className="flex gap-[16px] items-center h-[41px]">
<p className="font-medium text-[#515151] text-[18px] leading-normal w-[177px]"> <span className="text-[#515151] text-[18px] font-medium w-[177px]">
</p> </span>
<div className="flex gap-[10px]"> <div className="flex gap-[8px] flex-1">
<div className="border border-[#b9b9b9] border-solid box-border flex items-center justify-between p-[10px] rounded-[8px] w-[180.667px] cursor-pointer"> <div className="relative flex-1">
<p className="font-medium text-[18px] text-[#b9b9b9]"> <select
{formData.birthYear || '년도'} name="birthYear"
</p> value={formData.birthYear}
<div className="relative w-[16px] h-[16px] shrink-0"> onChange={handleChange}
<img alt="" className="block max-w-none w-full h-full" src={imgLsiconDownFilled} /> onFocus={() => setActiveSelect('birthYear')}
onBlur={() => setActiveSelect(null)}
className="w-full h-[42px] px-[10px] pr-[30px] text-[18px] font-medium border border-[#b9b9b9] rounded-[8px] bg-white text-[#515151] appearance-none focus:border-[#1669CA] focus:outline-none"
style={{ color: formData.birthYear ? '#515151' : '#b9b9b9' }}
>
<option value="" style={{ color: '#b9b9b9' }}></option>
{[...Array(100)].map((_, idx) => {
const year = new Date().getFullYear() - idx;
return <option key={year} value={year} style={{ color: '#515151' }}>{year}</option>
})}
</select>
<div className={`absolute right-[10px] top-1/2 -translate-y-1/2 pointer-events-none transition-transform ${activeSelect === 'birthYear' ? 'rotate-180' : ''}`}>
<ChevronSmall />
</div> </div>
</div> </div>
<div className="border border-[#b9b9b9] border-solid box-border flex items-center justify-between p-[10px] rounded-[8px] w-[180.667px] cursor-pointer"> <div className="relative flex-1">
<p className="font-medium text-[18px] text-[#b9b9b9]"> <select
{formData.birthMonth || '월'} name="birthMonth"
</p> value={formData.birthMonth}
<div className="relative w-[16px] h-[16px] shrink-0"> onChange={handleChange}
<img alt="" className="block max-w-none w-full h-full" src={imgLsiconDownFilled} /> onFocus={() => setActiveSelect('birthMonth')}
onBlur={() => setActiveSelect(null)}
className="w-full h-[42px] px-[10px] pr-[30px] text-[18px] font-medium border border-[#b9b9b9] rounded-[8px] bg-white text-[#515151] appearance-none focus:border-[#1669CA] focus:outline-none"
style={{ color: formData.birthMonth ? '#515151' : '#b9b9b9' }}
>
<option value="" style={{ color: '#b9b9b9' }}></option>
{[...Array(12)].map((_, idx) => (
<option key={idx + 1} value={idx + 1} style={{ color: '#515151' }}>{idx + 1}</option>
))}
</select>
<div className={`absolute right-[10px] top-1/2 -translate-y-1/2 pointer-events-none transition-transform ${activeSelect === 'birthMonth' ? 'rotate-180' : ''}`}>
<ChevronSmall />
</div> </div>
</div> </div>
<div className="border border-[#b9b9b9] border-solid box-border flex items-center justify-between p-[10px] rounded-[8px] w-[180.667px] cursor-pointer"> <div className="relative flex-1">
<p className="font-medium text-[18px] text-[#b9b9b9]"> <select
{formData.birthDay || '일'} name="birthDay"
</p> value={formData.birthDay}
<div className="relative w-[16px] h-[16px] shrink-0"> onChange={handleChange}
<img alt="" className="block max-w-none w-full h-full" src={imgLsiconDownFilled} /> onFocus={() => setActiveSelect('birthDay')}
onBlur={() => setActiveSelect(null)}
className="w-full h-[42px] px-[10px] pr-[30px] text-[18px] font-medium border border-[#b9b9b9] rounded-[8px] bg-white text-[#515151] appearance-none focus:border-[#1669CA] focus:outline-none"
style={{ color: formData.birthDay ? '#515151' : '#b9b9b9' }}
>
<option value="" style={{ color: '#b9b9b9' }}></option>
{[...Array(31)].map((_, idx) => (
<option key={idx + 1} value={idx + 1} style={{ color: '#515151' }}>{idx + 1}</option>
))}
</select>
<div className={`absolute right-[10px] top-1/2 -translate-y-1/2 pointer-events-none transition-transform ${activeSelect === 'birthDay' ? 'rotate-180' : ''}`}>
<ChevronSmall />
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</form>
{/* 약관 동의 */} {/* 약관 동의 */}
<div className="mt-[100px] border border-[#b9b9b9] border-solid rounded-[8px] w-[829px]"> <div className="mt-[46px] w-[829px] border border-[#b9b9b9] rounded-[8px] h-[221px] relative">
<div className="p-[29px] flex flex-col gap-[17px]"> <div className="px-[31px] py-[29px]">
{/* 전체동의 */} {/* 전체동의 */}
<div className="flex items-center justify-between"> <div
<div className="flex gap-[16px] items-end cursor-pointer" onClick={() => handleAgreementChange('all')}> className="flex items-center gap-[16px] cursor-pointer select-none mb-[17px]"
<div className="relative w-[24px] h-[24px] shrink-0"> onClick={() => handleAgreementChange('all')}
{agreements.all ? ( >
<div className="w-[24px] h-[24px] rounded-full border-2 border-[#1669ca] bg-[#1669ca] flex items-center justify-center"> {agreements.all ? <CheckOn /> : <CheckOff />}
<div className="w-[8px] h-[8px] rounded-full bg-white" /> <span className="font-bold text-[20px] text-[#515151]">
</div>
) : (
<div className="w-[24px] h-[24px] rounded-full border-2 border-[#b9b9b9]" />
)}
</div>
<p className="font-bold text-[#515151] text-[20px] leading-normal">
. .
</p> </span>
</div>
</div> </div>
{/* 개별 약관 */} {/* 개별 동의들 */}
<div className="flex flex-col gap-[17px]"> <div className="flex flex-col gap-[17px]">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex gap-[16px] items-end cursor-pointer" onClick={() => handleAgreementChange('age14')}> <div className="flex items-center gap-[16px] cursor-pointer" onClick={() => handleAgreementChange('age14')}>
<div className="relative w-[24px] h-[24px] shrink-0"> {agreements.age14 ? <CheckOn /> : <CheckOff />}
{agreements.age14 ? ( <span className="text-[#515151] text-[16px] font-medium leading-[1.6]">
<div className="w-[24px] h-[24px] rounded-full border-2 border-[#1669ca] bg-[#1669ca] flex items-center justify-center">
<div className="w-[8px] h-[8px] rounded-full bg-white" />
</div>
) : (
<div className="w-[24px] h-[24px] rounded-full border-2 border-[#b9b9b9]" />
)}
</div>
<p className="font-medium text-[#515151] text-[16px] leading-[1.6]">
14 . () 14 . ()
</p> </span>
</div> </div>
<div className="border border-[#eeeeee] border-solid box-border flex items-center justify-center p-[10px] rounded-[10px] w-[90px] h-[27px] cursor-pointer hover:bg-gray-50"> <button
<p className="font-medium text-[#1669ca] text-[12px] leading-normal"> type="button"
className="border border-[#eeeeee] rounded-[10px] px-[10px] py-[10px] h-[27px] flex items-center justify-center text-[#1669ca] text-[12px] font-medium"
>
</p> </button>
</div> </div>
</div>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex gap-[16px] items-end cursor-pointer" onClick={() => handleAgreementChange('terms')}> <div className="flex items-center gap-[16px] cursor-pointer" onClick={() => handleAgreementChange('terms')}>
<div className="relative w-[24px] h-[24px] shrink-0"> {agreements.terms ? <CheckOn /> : <CheckOff />}
{agreements.terms ? ( <span className="text-[#515151] text-[16px] font-medium leading-[1.6]">
<div className="w-[24px] h-[24px] rounded-full border-2 border-[#1669ca] bg-[#1669ca] flex items-center justify-center">
<div className="w-[8px] h-[8px] rounded-full bg-white" />
</div>
) : (
<div className="w-[24px] h-[24px] rounded-full border-2 border-[#b9b9b9]" />
)}
</div>
<p className="font-medium text-[#515151] text-[16px] leading-[1.6]">
() ()
</p> </span>
</div> </div>
<div className="border border-[#eeeeee] border-solid box-border flex items-center justify-center p-[10px] rounded-[10px] w-[90px] h-[27px] cursor-pointer hover:bg-gray-50"> <button
<p className="font-medium text-[#1669ca] text-[12px] leading-normal"> type="button"
className="border border-[#eeeeee] rounded-[10px] px-[10px] py-[10px] h-[27px] flex items-center justify-center text-[#1669ca] text-[12px] font-medium"
>
</p> </button>
</div> </div>
</div>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex gap-[16px] items-end cursor-pointer" onClick={() => handleAgreementChange('privacy')}> <div className="flex items-center gap-[16px] cursor-pointer" onClick={() => handleAgreementChange('privacy')}>
<div className="relative w-[24px] h-[24px] shrink-0"> {agreements.privacy ? <CheckOn /> : <CheckOff />}
{agreements.privacy ? ( <span className="text-[#515151] text-[16px] font-medium leading-[1.6]">
<div className="w-[24px] h-[24px] rounded-full border-2 border-[#1669ca] bg-[#1669ca] flex items-center justify-center">
<div className="w-[8px] h-[8px] rounded-full bg-white" />
</div>
) : (
<div className="w-[24px] h-[24px] rounded-full border-2 border-[#b9b9b9]" />
)}
</div>
<p className="font-medium text-[#515151] text-[16px] leading-[1.6]">
() ()
</p> </span>
</div> </div>
<div className="border border-[#eeeeee] border-solid box-border flex items-center justify-center p-[10px] rounded-[10px] w-[90px] h-[27px] cursor-pointer hover:bg-gray-50"> <button
<p className="font-medium text-[#1669ca] text-[12px] leading-normal"> type="button"
className="border border-[#eeeeee] rounded-[10px] px-[10px] py-[10px] h-[27px] flex items-center justify-center text-[#1669ca] text-[12px] font-medium"
>
</p> </button>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{/* 버튼 영역 */} {/* 버튼 영역 */}
<div className="mt-[50px] flex gap-[21px] items-center"> <div className="mt-[52px] mb-[137px] w-[829px] flex gap-[21px] justify-center">
<Link <Link
href="/registeragreement" href="/login"
className="border border-[#1669ca] border-solid box-border flex items-center justify-center p-[10px] rounded-[10px] w-[334px] h-[55px]" 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"
> >
<p className="font-medium text-[#515151] text-[18px] leading-normal">
</p>
</Link> </Link>
<button <button
type="button" type="button"
onClick={handleSubmit} onClick={handleSubmit}
disabled={!canProceed} disabled={!canProceed}
className={`box-border flex items-center justify-center p-[10px] rounded-[10px] w-[334px] h-[55px] ${canProceed className={`rounded-[10px] h-[55px] w-[334px] font-medium text-[18px] transition flex items-center justify-center
? 'bg-[#2b82e8] hover:bg-[#1669ca]' ${canProceed
: 'bg-[#b9b9b9] cursor-not-allowed' ? 'bg-[#2B82E8] text-white hover:bg-[#1669ca]'
: 'bg-[#b9b9b9] text-white cursor-not-allowed'
}`} }`}
> >
<p className={`font-medium text-[18px] leading-normal ${canProceed ? 'text-white' : 'text-white'
}`}>
</p>
</button> </button>
</div> </div>
{/* 카피라이트 */} {/* 카피라이트 */}
<div className="absolute bottom-[89.5px] flex flex-col justify-center left-1/2 -translate-x-1/2 translate-y-1/2"> <footer className="absolute bottom-[78px] left-1/2 -translate-x-1/2 flex flex-col items-center">
<p className="font-medium text-[16px] text-[rgba(0,0,0,0.55)] leading-[1.45] tracking-[-0.08px]"> <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 Copyright 2025 XL LMS. All rights reserved
</p> </p>
</div> </footer>
</div> </div>
); );
} }

View File

@@ -1,172 +0,0 @@
'use client';
import { useState } from 'react';
import Link from 'next/link';
export default function RegisterAgreementPage() {
const [allAgreed, setAllAgreed] = useState(false);
const [ageAgreed, setAgeAgreed] = useState(false);
const [termsAgreed, setTermsAgreed] = useState(false);
const [privacyAgreed, setPrivacyAgreed] = useState(false);
// 전체 동의 핸들러
const handleAllAgreed = (checked: boolean) => {
setAllAgreed(checked);
setAgeAgreed(checked);
setTermsAgreed(checked);
setPrivacyAgreed(checked);
};
// 개별 체크박스 변경 시 전체 동의 상태 업데이트
const updateAllAgreed = () => {
if (ageAgreed && termsAgreed && privacyAgreed) {
setAllAgreed(true);
} else {
setAllAgreed(false);
}
};
// 개별 체크박스 핸들러
const handleAgeAgreed = (checked: boolean) => {
setAgeAgreed(checked);
updateAllAgreed();
};
const handleTermsAgreed = (checked: boolean) => {
setTermsAgreed(checked);
updateAllAgreed();
};
const handlePrivacyAgreed = (checked: boolean) => {
setPrivacyAgreed(checked);
updateAllAgreed();
};
// 다음 단계 진행 가능 여부
const canProceed = ageAgreed && termsAgreed && privacyAgreed;
return (
<div className="min-h-screen flex items-center justify-center bg-white">
<div className="w-full max-w-2xl px-4">
{/* 회원가입 카드 */}
<div className="bg-white rounded-lg shadow-lg p-8">
{/* 제목 */}
<h1 className="text-2xl font-bold text-center mb-8"></h1>
{/* 단계 표시 */}
<div className="flex items-center justify-center mb-8 text-sm">
<div className="flex items-center">
<span className="px-3 py-1 bg-blue-500 text-white rounded-md font-semibold">
01
</span>
<span className="mx-2 text-gray-400">&gt;</span>
<span className="px-3 py-1 text-gray-500">02 </span>
<span className="mx-2 text-gray-400">&gt;</span>
<span className="px-3 py-1 text-gray-500">03 </span>
</div>
</div>
{/* 약관 동의 섹션 */}
<div className="space-y-4">
{/* 전체 동의 */}
<div className="pb-4 border-b border-gray-300">
<label className="flex items-center cursor-pointer">
<input
type="checkbox"
checked={allAgreed}
onChange={(e) => handleAllAgreed(e.target.checked)}
className="w-5 h-5 mr-3 text-blue-500 rounded focus:ring-2 focus:ring-blue-500"
/>
<span className="text-base font-medium"> .</span>
</label>
</div>
{/* 개별 약관 */}
<div className="space-y-4">
{/* 만 14세 이상 */}
<div className="flex items-center justify-between">
<label className="flex items-center cursor-pointer flex-1">
<input
type="checkbox"
checked={ageAgreed}
onChange={(e) => handleAgeAgreed(e.target.checked)}
className="w-5 h-5 mr-3 text-blue-500 rounded focus:ring-2 focus:ring-blue-500"
/>
<span className="text-sm">
14 . <span className="text-red-500">()</span>
</span>
</label>
<button className="text-sm text-gray-500 hover:text-gray-700">
&gt;
</button>
</div>
{/* 이용 약관 */}
<div className="flex items-center justify-between">
<label className="flex items-center cursor-pointer flex-1">
<input
type="checkbox"
checked={termsAgreed}
onChange={(e) => handleTermsAgreed(e.target.checked)}
className="w-5 h-5 mr-3 text-blue-500 rounded focus:ring-2 focus:ring-blue-500"
/>
<span className="text-sm">
<span className="text-red-500">()</span>
</span>
</label>
<button className="text-sm text-gray-500 hover:text-gray-700">
&gt;
</button>
</div>
{/* 개인정보 수집 및 이용 */}
<div className="flex items-center justify-between">
<label className="flex items-center cursor-pointer flex-1">
<input
type="checkbox"
checked={privacyAgreed}
onChange={(e) => handlePrivacyAgreed(e.target.checked)}
className="w-5 h-5 mr-3 text-blue-500 rounded focus:ring-2 focus:ring-blue-500"
/>
<span className="text-sm">
<span className="text-red-500">()</span>
</span>
</label>
<button className="text-sm text-gray-500 hover:text-gray-700">
&gt;
</button>
</div>
</div>
</div>
{/* 버튼 영역 */}
<div className="mt-8 flex space-x-4">
<Link
href="/login"
className="flex-1 py-3 bg-gray-200 text-gray-700 rounded-md text-center hover:bg-gray-300 transition font-medium"
>
</Link>
<Link
href={canProceed ? '/register' : '#'}
className={`flex-1 py-3 rounded-md text-center font-medium transition ${
canProceed
? 'bg-blue-500 text-white hover:bg-blue-600'
: 'bg-gray-300 text-gray-500 cursor-not-allowed pointer-events-none'
}`}
onClick={(e) => !canProceed && e.preventDefault()}
>
</Link>
</div>
</div>
{/* 카피라이트 */}
<div className="mt-8 text-center text-sm text-gray-500">
Copyright © 2025 XL LMS. All rights reserved
</div>
</div>
</div>
);
}

View File

@@ -4,81 +4,33 @@ import Link from 'next/link';
export default function RegisterCompletePage() { export default function RegisterCompletePage() {
return ( return (
<div className="min-h-screen flex items-center justify-center bg-white"> <div className="relative min-h-screen flex flex-col items-center bg-white">
<div className="w-full max-w-2xl px-4"> {/* 메인 콘텐츠 영역 - 카피라이트와 최상단 사이 중앙 */}
{/* 회원가입 완료 카드 */} <div className="flex-1 flex flex-col justify-center items-center w-full">
<div className="bg-white rounded-lg shadow-lg p-8">
{/* 제목 */}
<h1 className="text-2xl font-bold text-center mb-8"></h1>
{/* 단계 표시 */}
<div className="flex items-center justify-center mb-8 text-sm">
<div className="flex items-center">
<span className="px-3 py-1 text-gray-500">01 </span>
<span className="mx-2 text-gray-400">&gt;</span>
<span className="px-3 py-1 text-gray-500">02 </span>
<span className="mx-2 text-gray-400">&gt;</span>
<span className="px-3 py-1 bg-blue-500 text-white rounded-md font-semibold">
03
</span>
</div>
</div>
{/* 완료 메시지 */} {/* 완료 메시지 */}
<div className="text-center py-12"> <div className="flex flex-col gap-[33px] items-center">
{/* 체크 아이콘 */} <p className="font-bold text-[#515151] text-[32px] leading-normal text-center">
<div className="mb-6 flex justify-center"> .
<div className="w-20 h-20 bg-green-100 rounded-full flex items-center justify-center">
<svg
className="w-12 h-12 text-green-500"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M5 13l4 4L19 7"
/>
</svg>
</div>
</div>
{/* 완료 메시지 */}
<h2 className="text-2xl font-bold mb-4 text-gray-800">
!
</h2>
<p className="text-gray-600 mb-8">
XL LMS에 .
<br />
.
</p> </p>
</div>
{/* 버튼 영역 */} {/* 로그인 버튼 */}
<div className="flex flex-col sm:flex-row gap-4 justify-center"> <div className="mt-[46px]">
<Link <Link
href="/login" href="/login"
className="px-8 py-3 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition font-medium text-center" className="bg-[#2B82E8] rounded-[10px] h-[55px] w-[334px] flex items-center justify-center font-medium text-white text-[18px] hover:bg-[#1669ca] transition"
> >
</Link> </Link>
<Link
href="/"
className="px-8 py-3 bg-gray-200 text-gray-700 rounded-md hover:bg-gray-300 transition font-medium text-center"
>
</Link>
</div>
</div> </div>
</div> </div>
{/* 카피라이트 */} {/* 카피라이트 */}
<div className="mt-8 text-center text-sm text-gray-500"> <footer className="absolute bottom-[51.5px] left-1/2 -translate-x-1/2 flex flex-col items-center">
Copyright © 2025 XL LMS. All rights reserved <p className="text-[16px] text-[rgba(0,0,0,0.55)] leading-[1.45] font-medium tracking-[-0.08px]">
</div> Copyright 2025 XL LMS. All rights reserved
</div> </p>
</footer>
</div> </div>
); );
} }

163
app/studydata/page.tsx Normal file
View 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
View File

@@ -0,0 +1,35 @@
/**
* 관리자 인증 유틸리티 함수
*/
/**
* 관리자 로그인 상태 확인
* @returns 관리자 로그인 여부
*/
export function isAdminLoggedIn(): boolean {
if (typeof window === 'undefined') {
return false;
}
return localStorage.getItem('isAdminLoggedIn') === 'true';
}
/**
* 관리자 인증 체크 및 리다이렉트
* 인증되지 않은 경우 로그인 페이지로 이동
*/
export function requireAdminAuth(): boolean {
if (typeof window === 'undefined') {
return false;
}
const isAdmin = isAdminLoggedIn();
if (!isAdmin) {
// 관리자 인증되지 않은 경우 로그인 페이지로 리다이렉트
window.location.href = '/login';
return false;
}
return true;
}

14
lib/prisma.ts Normal file
View File

@@ -0,0 +1,14 @@
import { PrismaClient } from './generated/prisma/client';
const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined;
};
export const prisma =
globalForPrisma.prisma ??
new PrismaClient({
log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'],
});
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma;

1412
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -6,9 +6,23 @@
"dev": "next dev", "dev": "next dev",
"build": "next build", "build": "next build",
"start": "next start", "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": { "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", "next": "16.0.1",
"react": "19.2.0", "react": "19.2.0",
"react-dom": "19.2.0" "react-dom": "19.2.0"
@@ -20,7 +34,9 @@
"@types/react-dom": "^19", "@types/react-dom": "^19",
"eslint": "^9", "eslint": "^9",
"eslint-config-next": "16.0.1", "eslint-config-next": "16.0.1",
"prisma": "^6.19.0",
"tailwindcss": "^4", "tailwindcss": "^4",
"tsx": "^4.19.2",
"typescript": "5.9.3" "typescript": "5.9.3"
} }
} }

14
prisma.config.ts Normal file
View 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

Binary file not shown.

99
prisma/schema.prisma Normal file
View 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
View 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/check_off.tsx Normal file
View File

@@ -0,0 +1,7 @@
export default function CheckOff() {
return (
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4 12C4 9.87827 4.84285 7.84344 6.34315 6.34315C7.84344 4.84285 9.87827 4 12 4C14.1217 4 16.1566 4.84285 17.6569 6.34315C19.1571 7.84344 20 9.87827 20 12C20 14.1217 19.1571 16.1566 17.6569 17.6569C16.1566 19.1571 14.1217 20 12 20C9.87827 20 7.84344 19.1571 6.34315 17.6569C4.84285 16.1566 4 14.1217 4 12ZM12 2C6.477 2 2 6.477 2 12C2 17.523 6.477 22 12 22C17.523 22 22 17.523 22 12C22 6.477 17.523 2 12 2Z" fill="#2C83E8" />
</svg>
);
}

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

@@ -0,0 +1,7 @@
export default function CheckOn() {
return (
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4 12C4 9.87827 4.84285 7.84344 6.34315 6.34315C7.84344 4.84285 9.87827 4 12 4C14.1217 4 16.1566 4.84285 17.6569 6.34315C19.1571 7.84344 20 9.87827 20 12C20 14.1217 19.1571 16.1566 17.6569 17.6569C16.1566 19.1571 14.1217 20 12 20C9.87827 20 7.84344 19.1571 6.34315 17.6569C4.84285 16.1566 4 14.1217 4 12ZM12 2C6.477 2 2 6.477 2 12C2 17.523 6.477 22 12 22C17.523 22 22 17.523 22 12C22 6.477 17.523 2 12 2ZM17.457 9.457L16.043 8.043L11 13.086L8.207 10.293L6.793 11.707L11 15.914L17.457 9.457Z" fill="#2B82E8" />
</svg>
);
}

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

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

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

View File

@@ -0,0 +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 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
View 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
View File

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

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

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

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

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

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

@@ -0,0 +1,7 @@
export default function RadioOff() {
return (
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 12C3 10.8181 3.23279 9.64778 3.68508 8.55585C4.13738 7.46392 4.80031 6.47177 5.63604 5.63604C6.47177 4.80031 7.46392 4.13738 8.55585 3.68508C9.64778 3.23279 10.8181 3 12 3C13.1819 3 14.3522 3.23279 15.4442 3.68508C16.5361 4.13738 17.5282 4.80031 18.364 5.63604C19.1997 6.47177 19.8626 7.46392 20.3149 8.55585C20.7672 9.64778 21 10.8181 21 12C21 14.3869 20.0518 16.6761 18.364 18.364C16.6761 20.0518 14.3869 21 12 21C9.61305 21 7.32387 20.0518 5.63604 18.364C3.94821 16.6761 3 14.3869 3 12Z" stroke="#969696" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
);
}

8
public/svg/radio_on.tsx Normal file
View File

@@ -0,0 +1,8 @@
export default function RadioOn() {
return (
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 22.5C6.21 22.5 1.5 17.79 1.5 12C1.5 6.21 6.21 1.5 12 1.5C17.79 1.5 22.5 6.21 22.5 12C22.5 17.79 17.79 22.5 12 22.5ZM12 3C7.035 3 3 7.035 3 12C3 16.965 7.035 21 12 21C16.965 21 21 16.965 21 12C21 7.035 16.965 3 12 3Z" fill="#404040" />
<path d="M12 18C15.3137 18 18 15.3137 18 12C18 8.68629 15.3137 6 12 6C8.68629 6 6 8.68629 6 12C6 15.3137 8.68629 18 12 18Z" fill="#404040" />
</svg>
);
}