Compare commits
37 Commits
2cc57f8a3d
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dae09ff2e7 | ||
|
|
0af1028b7b | ||
|
|
efdcde8b12 | ||
|
|
a7cad39b3c | ||
|
|
16d6cb5b30 | ||
|
|
8403684df3 | ||
|
|
c81f218a2b | ||
|
|
9d3ab9f8d8 | ||
|
|
e8f4094de6 | ||
|
|
94a186de59 | ||
|
|
137edf2d0d | ||
|
|
6600241bdb | ||
|
|
218e915e37 | ||
|
|
512acfb921 | ||
|
|
9a11a8afd1 | ||
|
|
efd5ab915e | ||
|
|
6a9cd7c1e4 | ||
|
|
3d226a65f7 | ||
|
|
c40a40bf58 | ||
| 9be57a56b4 | |||
| 5335c24b5a | |||
|
|
c913c2d1f6 | ||
|
|
b439d56744 | ||
|
|
86a9e1e17f | ||
|
|
7639c027e3 | ||
|
|
c524626a00 | ||
|
|
4663dd5ba6 | ||
|
|
ef82f69edc | ||
|
|
bfee0c4717 | ||
| 33d5d900fc | |||
| 6720653d3f | |||
| 30353521a1 | |||
| 8d79468bf8 | |||
| a96ee0fb60 | |||
| 679bc77403 | |||
| b2103a73e6 | |||
| 5af20e2105 |
2
.cursor/commands/ck.md
Normal file
2
.cursor/commands/ck.md
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
방금 수행한 작업 체크
|
||||||
|
ㄴ
|
||||||
1
.cursor/commands/cm.md
Normal file
1
.cursor/commands/cm.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
지금 수행한 작업 메시지 넣고 커밋s
|
||||||
@@ -1 +1 @@
|
|||||||
안녕?
|
다음 작업 수행
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
# 로그인 페이지 작업 리스트
|
|
||||||
0. [x] 로그인 페이지 생성
|
|
||||||
레이아웃 중앙 정렬 설정
|
|
||||||
상단 로고 추가
|
|
||||||
아이디 입력폼 추가
|
|
||||||
한행에 아이디 기억하기 체크박스 추가
|
|
||||||
한행에 자동로그인 체크박스 추가
|
|
||||||
로그인 버튼 추가
|
|
||||||
한행으로 회원가입 버튼, 아이디찾기 버튼, 비밀번호 재설정 버튼 추가
|
|
||||||
최하단에 카피라이트 추가
|
|
||||||
|
|
||||||
6
.cursor/work/loginpagework.md
Normal file
6
.cursor/work/loginpagework.md
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
1. 네비게이션바
|
||||||
|
-
|
||||||
|
2. fo
|
||||||
|
- oter
|
||||||
|
2. 메인을 플렉스로 vertical방향으로 3개로 구분
|
||||||
|
-
|
||||||
38
.cursor/work/mainpagework.md
Normal file
38
.cursor/work/mainpagework.md
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
[✅] 1. **전체 페이지 수직 분할 (기본 구조)**
|
||||||
|
* `<body>` 또는 최상위 wrapper div에 `display: flex`와 `flex-direction: column`을 적용합니다.
|
||||||
|
* 이는 페이지 전체를 **[헤더]**, **[메인 콘텐츠]**, **[푸터]** 3개의 큰 영역으로 수직 정렬하기 위함입니다.
|
||||||
|
* `min-height: 100vh`를 설정하여 콘텐츠가 적어도 화면 전체 높이를 차지하도록 합니다.
|
||||||
|
|
||||||
|
[✅] 2. **헤더 (Navigation Bar) 영역**
|
||||||
|
* `display: flex`, `flex-direction: row`, `justify-content: space-between`을 사용하여 **로고/메뉴 그룹**과 **사용자 메뉴(내 강의실, 프로필) 그룹**을 양쪽 끝으로 배치합니다.
|
||||||
|
* 로고/메뉴 그룹 내부도 `display: flex`, `align-items: center`로 로고와 메뉴 항목들을 정렬합니다.
|
||||||
|
* **[반응형]** 모바일 화면(@media 쿼리 사용)에서는 메뉴 항목들을 숨기고 햄버거 메뉴 아이콘을 표시하도록 처리합니다.
|
||||||
|
|
||||||
|
[] 3. **메인 콘텐츠 상단 (Hero + Sidebar) 영역**
|
||||||
|
* `main` 태그 내부에 `display: flex`, `flex-direction: row`를 가진 컨테이너를 생성합니다.
|
||||||
|
* 이 컨테이너는 **Hero 배너(좌측)**와 **사이드바(우측)** 두 부분으로 가로 분할됩니다.
|
||||||
|
* **[반응형]** 모바일 화면에서는 `flex-direction: column`으로 변경하여 사이드바가 Hero 배너 하단으로 이동하도록 순서를 조정합니다.
|
||||||
|
|
||||||
|
[] 4. **Hero 배너 (메인 좌측)**
|
||||||
|
* Hero 배너 영역에 `flex-grow: 1`을 설정하여, 사이드바의 너비를 제외한 나머지 공간을 모두 차지하도록 합니다.
|
||||||
|
* 배너 컨테이너에 `position: relative`를, "미래 에너지, 안전의 길을 열다" 텍스트 박스에 `position: absolute`를 적용하여 텍스트를 이미지 위에 오버레이합니다.
|
||||||
|
|
||||||
|
[] 5. **우측 사이드바 (나의 수강현황, 최근 수강 과목)**
|
||||||
|
* 사이드바 영역에 `flex-basis` 또는 `width`를 사용해 고정된 너비(예: `300px`)를 지정합니다.
|
||||||
|
* '나의 수강현황'과 '최근 수강 과목' 두 섹션은 `display: flex`, `flex-direction: column`으로 수직 정렬합니다.
|
||||||
|
* '나의 수강현황' 내부의 4개 통계(수강중, 수강완료 등)는 `display: flex`, `justify-content: space-around`로 균등하게 배치합니다.
|
||||||
|
|
||||||
|
[] 6. **전체 과목 리스트 (메인 중단)**
|
||||||
|
* "전체 과목" 타이틀과 "지도보기" 링크를 `display: flex`, `justify-content: space-between`으로 배치합니다.
|
||||||
|
* 과목 카드 리스트는 `display: flex`, `flex-wrap: wrap`을 설정하여 카드들이 자동으로 다음 줄로 넘어가도록 합니다.
|
||||||
|
* 각 과목 카드에는 `flex-basis`를 사용해 너비를 지정합니다. (예: `flex-basis: 24%`, 4개 배치. `gap`으로 간격 조절)
|
||||||
|
* **[반응형]** 화면 크기에 따라 `flex-basis` 값을 변경합니다. (예: 태블릿 `48%`, 모바일 `100%`)
|
||||||
|
|
||||||
|
[] 7. **공지사항 (메인 하단)**
|
||||||
|
* "공지사항" 타이틀과 "더보기" 링크를 `display: flex`, `justify-content: space-between`으로 배치합니다.
|
||||||
|
* 각 공지사항 항목(카테고리, 제목, 날짜) 역시 `display: flex`, `justify-content: space-between`을 사용하여 깔끔하게 정렬합니다.
|
||||||
|
|
||||||
|
[] 8. **푸터 (Footer) 영역**
|
||||||
|
* `display: flex`, `flex-direction: row`, `justify-content: space-between`을 사용하여 **로고/주소/고객센터 정보** 그룹과 **저작권** 텍스트를 배치합니다.
|
||||||
|
* `flex-wrap: wrap`을 추가하여 화면이 좁아질 때 유연하게 대응합니다.
|
||||||
|
* **[반응형]** 모바일에서는 `flex-direction: column`으로 변경하고 `align-items: center` 또는 `flex-start`로 정렬을 변경할 수 있습니다.
|
||||||
12
.env
Normal file
12
.env
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# Environment variables declared in this file are NOT automatically loaded by Prisma.
|
||||||
|
# Please add `import "dotenv/config";` to your `prisma.config.ts` file, or use the Prisma CLI with Bun
|
||||||
|
# to load environment variables from .env files: https://pris.ly/prisma-config-env-vars.
|
||||||
|
|
||||||
|
# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB.
|
||||||
|
# See the documentation for all the connection string options: https://pris.ly/d/connection-strings
|
||||||
|
|
||||||
|
# The following `prisma+postgres` URL is similar to the URL produced by running a local Prisma Postgres
|
||||||
|
# server with the `prisma dev` CLI command, when not choosing any non-default ports or settings. The API key, unlike the
|
||||||
|
# one found in a remote Prisma Postgres URL, does not contain any sensitive information.
|
||||||
|
|
||||||
|
DATABASE_URL="prisma+postgres://localhost:51213/?api_key=eyJkYXRhYmFzZVVybCI6InBvc3RncmVzOi8vcG9zdGdyZXM6cG9zdGdyZXNAbG9jYWxob3N0OjUxMjE0L3RlbXBsYXRlMT9zc2xtb2RlPWRpc2FibGUmY29ubmVjdGlvbl9saW1pdD0xJmNvbm5lY3RfdGltZW91dD0wJm1heF9pZGxlX2Nvbm5lY3Rpb25fbGlmZXRpbWU9MCZwb29sX3RpbWVvdXQ9MCZzaW5nbGVfdXNlX2Nvbm5lY3Rpb25zPXRydWUmc29ja2V0X3RpbWVvdXQ9MCIsIm5hbWUiOiJkZWZhdWx0Iiwic2hhZG93RGF0YWJhc2VVcmwiOiJwb3N0Z3JlczovL3Bvc3RncmVzOnBvc3RncmVzQGxvY2FsaG9zdDo1MTIxNS90ZW1wbGF0ZTE_c3NsbW9kZT1kaXNhYmxlJmNvbm5lY3Rpb25fbGltaXQ9MSZjb25uZWN0X3RpbWVvdXQ9MCZtYXhfaWRsZV9jb25uZWN0aW9uX2xpZmV0aW1lPTAmcG9vbF90aW1lb3V0PTAmc2luZ2xlX3VzZV9jb25uZWN0aW9ucz10cnVlJnNvY2tldF90aW1lb3V0PTAifQ"
|
||||||
17
.gitignore
vendored
17
.gitignore
vendored
@@ -16,7 +16,6 @@
|
|||||||
# next.js
|
# next.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
462
app/admin_home/page.tsx
Normal file
@@ -0,0 +1,462 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import { isAdminLoggedIn } from '../../lib/auth';
|
||||||
|
import LoginPage from '../login/page';
|
||||||
|
import Logout from '../../public/svg/logout';
|
||||||
|
|
||||||
|
const imgArrowDisabled = "http://localhost:3845/assets/6edcb2defc36a2bf4a05a3abe53b8da3d42b2cb4.svg";
|
||||||
|
const imgArrowDefault = "http://localhost:3845/assets/ad0cb4418492f1b020bb38a2ff038a331294ce87.svg";
|
||||||
|
const imgArrowNext = "http://localhost:3845/assets/6328cf96ee1169c1425c2ce55e7a2dcca0374508.svg";
|
||||||
|
|
||||||
|
interface User {
|
||||||
|
id: number;
|
||||||
|
joinDate: string;
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
role: string;
|
||||||
|
accountStatus: string;
|
||||||
|
accountManagement: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type PaginationMove = "Previous" | "Next";
|
||||||
|
type PaginationStatus = "Default" | "Disabled";
|
||||||
|
|
||||||
|
function PaginationBtnMove({ status = "Default", move = "Previous" }: { status?: PaginationStatus; move?: PaginationMove }) {
|
||||||
|
const isDisabled = status === "Disabled";
|
||||||
|
const isNext = move === "Next";
|
||||||
|
|
||||||
|
if (isDisabled && isNext) {
|
||||||
|
return (
|
||||||
|
<div className="border border-[#eeeeee] border-solid relative rounded-[8px] shrink-0 size-[30px]">
|
||||||
|
<div className="overflow-clip relative rounded-[inherit] size-[30px]">
|
||||||
|
<div className="absolute flex items-center justify-center left-[5px] size-[20px] top-[5px]">
|
||||||
|
<div className="flex-none rotate-[180deg]">
|
||||||
|
<div className="overflow-clip relative size-[20px]">
|
||||||
|
<div className="absolute flex inset-[15.63%_30.21%] items-center justify-center">
|
||||||
|
<div className="flex-none h-[16.5px] rotate-[180deg] w-[9.5px]">
|
||||||
|
<div className="relative size-full">
|
||||||
|
<div className="absolute inset-0">
|
||||||
|
<img alt="" className="block max-w-none size-full" style={{ filter: 'opacity(0.88) brightness(0) saturate(100%) invert(88%)', opacity: 1 }} src={imgArrowNext} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isDisabled && !isNext) {
|
||||||
|
return (
|
||||||
|
<div className="border border-[#eeeeee] border-solid relative rounded-[8px] shrink-0 size-[30px]">
|
||||||
|
<div className="overflow-clip relative rounded-[inherit] size-[30px]">
|
||||||
|
<div className="absolute flex items-center justify-center left-[5px] size-[20px] top-[5px]">
|
||||||
|
<div className="flex-none rotate-[180deg]">
|
||||||
|
<div className="overflow-clip relative size-[20px]">
|
||||||
|
<div className="absolute inset-[15.63%_30.21%]">
|
||||||
|
<div className="absolute inset-0">
|
||||||
|
<img alt="" className="block max-w-none size-full" style={{ filter: 'opacity(0.88) brightness(0) saturate(100%) invert(88%)', opacity: 1 }} src={imgArrowDefault} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isNext) {
|
||||||
|
return (
|
||||||
|
<div className="border border-[#eeeeee] border-solid relative rounded-[8px] shrink-0 size-[30px] cursor-pointer hover:bg-gray-50 transition-colors">
|
||||||
|
<div className="overflow-clip relative rounded-[inherit] size-[30px]">
|
||||||
|
<div className="absolute flex items-center justify-center left-[5px] size-[20px] top-[5px]">
|
||||||
|
<div className="flex-none rotate-[180deg]">
|
||||||
|
<div className="overflow-clip relative size-[20px]">
|
||||||
|
<div className="absolute flex inset-[15.63%_30.21%] items-center justify-center">
|
||||||
|
<div className="flex-none h-[16.5px] rotate-[180deg] w-[9.5px]">
|
||||||
|
<div className="relative size-full">
|
||||||
|
<div className="absolute inset-0">
|
||||||
|
<img alt="" className="block max-w-none size-full" src={imgArrowNext} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="border border-[#eeeeee] border-solid relative rounded-[8px] shrink-0 size-[30px] cursor-pointer hover:bg-gray-50 transition-colors">
|
||||||
|
<div className="overflow-clip relative rounded-[inherit] size-[30px]">
|
||||||
|
<div className="absolute flex items-center justify-center left-[5px] size-[20px] top-[5px]">
|
||||||
|
<div className="flex-none rotate-[180deg]">
|
||||||
|
<div className="overflow-clip relative size-[20px]">
|
||||||
|
<div className="absolute inset-[15.63%_30.21%]">
|
||||||
|
<div className="absolute inset-0">
|
||||||
|
<img alt="" className="block max-w-none size-full" src={imgArrowDefault} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AdminHomePage() {
|
||||||
|
const router = useRouter();
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
||||||
|
const [selectedTab, setSelectedTab] = useState<'전체' | '학습자' | '강사' | '운영자'>('전체');
|
||||||
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
|
|
||||||
|
// 샘플 사용자 데이터
|
||||||
|
const [users, setUsers] = useState<User[]>([
|
||||||
|
{ id: 39, joinDate: '2026-01-15', name: '홍길동', email: 'hong@example.com', role: '학습자', accountStatus: '활성', accountManagement: '관리' },
|
||||||
|
{ id: 38, joinDate: '2026-01-14', name: '김철수', email: 'kim@example.com', role: '강사', accountStatus: '활성', accountManagement: '관리' },
|
||||||
|
{ id: 37, joinDate: '2026-01-13', name: '이영희', email: 'lee@example.com', role: '학습자', accountStatus: '활성', accountManagement: '관리' },
|
||||||
|
{ id: 36, joinDate: '2026-01-12', name: '박민수', email: 'park@example.com', role: '운영자', accountStatus: '활성', accountManagement: '관리' },
|
||||||
|
{ id: 35, joinDate: '2026-01-11', name: '최지영', email: 'choi@example.com', role: '학습자', accountStatus: '활성', accountManagement: '관리' },
|
||||||
|
{ id: 34, joinDate: '2026-01-10', name: '정대현', email: 'jung@example.com', role: '강사', accountStatus: '활성', accountManagement: '관리' },
|
||||||
|
{ id: 33, joinDate: '2026-01-09', name: '강미영', email: 'kang@example.com', role: '학습자', accountStatus: '활성', accountManagement: '관리' },
|
||||||
|
{ id: 32, joinDate: '2026-01-08', name: '윤성호', email: 'yoon@example.com', role: '학습자', accountStatus: '활성', accountManagement: '관리' },
|
||||||
|
{ id: 31, joinDate: '2026-01-07', name: '임수진', email: 'lim@example.com', role: '강사', accountStatus: '활성', accountManagement: '관리' },
|
||||||
|
{ id: 30, joinDate: '2026-01-06', name: '한지훈', email: 'han@example.com', role: '학습자', accountStatus: '활성', accountManagement: '관리' },
|
||||||
|
{ id: 29, joinDate: '2026-01-05', name: '송민경', email: 'song@example.com', role: '학습자', accountStatus: '활성', accountManagement: '관리' },
|
||||||
|
{ id: 28, joinDate: '2026-01-04', name: '오준혁', email: 'oh@example.com', role: '운영자', accountStatus: '활성', accountManagement: '관리' },
|
||||||
|
{ id: 27, joinDate: '2026-01-03', name: '류현우', email: 'ryu@example.com', role: '학습자', accountStatus: '활성', accountManagement: '관리' },
|
||||||
|
{ id: 26, joinDate: '2026-01-02', name: '신동욱', email: 'shin@example.com', role: '강사', accountStatus: '활성', accountManagement: '관리' },
|
||||||
|
{ id: 25, joinDate: '2026-01-01', name: '조은서', email: 'cho@example.com', role: '학습자', accountStatus: '활성', accountManagement: '관리' },
|
||||||
|
{ id: 24, joinDate: '2025-12-31', name: '배성민', email: 'bae@example.com', role: '학습자', accountStatus: '활성', accountManagement: '관리' },
|
||||||
|
{ id: 23, joinDate: '2025-12-30', name: '전혜진', email: 'jeon@example.com', role: '강사', accountStatus: '활성', accountManagement: '관리' },
|
||||||
|
{ id: 22, joinDate: '2025-12-29', name: '남궁준', email: 'namgung@example.com', role: '학습자', accountStatus: '활성', accountManagement: '관리' },
|
||||||
|
{ id: 21, joinDate: '2025-12-28', name: '서아름', email: 'seo@example.com', role: '학습자', accountStatus: '활성', accountManagement: '관리' },
|
||||||
|
{ id: 20, joinDate: '2025-12-27', name: '권태영', email: 'kwon@example.com', role: '운영자', accountStatus: '활성', accountManagement: '관리' },
|
||||||
|
]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// 관리자 인증 확인
|
||||||
|
const checkAuth = () => {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
const isAdmin = isAdminLoggedIn();
|
||||||
|
setIsAuthenticated(isAdmin);
|
||||||
|
setIsLoading(false);
|
||||||
|
|
||||||
|
if (!isAdmin) {
|
||||||
|
// 인증되지 않은 경우 로그인 페이지로 리다이렉트
|
||||||
|
router.push('/login');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
checkAuth();
|
||||||
|
}, [router]);
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return null; // 로딩 중
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isAuthenticated) {
|
||||||
|
return <LoginPage />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const itemsPerPage = 13;
|
||||||
|
const totalPages = Math.ceil(users.length / itemsPerPage);
|
||||||
|
const pageNumbers = Array.from({ length: totalPages }, (_, i) => i + 1);
|
||||||
|
const startIndex = (currentPage - 1) * itemsPerPage;
|
||||||
|
const endIndex = startIndex + itemsPerPage;
|
||||||
|
const currentUsers = users.slice(startIndex, endIndex);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-white relative size-full min-h-screen">
|
||||||
|
{/* 사이드바 */}
|
||||||
|
<div className="absolute bg-white border-r border-[#eeeeee] border-solid box-border content-stretch flex flex-col gap-[45px] items-center left-0 min-h-[1080px] pb-8 pt-[30px] px-0 top-0 w-[250px]">
|
||||||
|
{/* 로고 */}
|
||||||
|
<button
|
||||||
|
onClick={() => router.push('/')}
|
||||||
|
className="h-[102px] relative shrink-0 w-[99px] cursor-pointer hover:opacity-80 transition-opacity"
|
||||||
|
>
|
||||||
|
<div className="absolute inset-0 flex items-center justify-center overflow-hidden pointer-events-none">
|
||||||
|
<img alt="로고" className="h-full w-full object-contain" src="/logo.svg" />
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
{/* 메뉴 */}
|
||||||
|
<div className="box-border content-stretch flex flex-col items-center pb-0 pt-4 px-0 relative shrink-0 w-[250px]">
|
||||||
|
<div className="box-border content-stretch flex flex-col gap-2 items-start p-3 relative shrink-0 w-full">
|
||||||
|
<button className="bg-[#f7f7f7] box-border content-stretch flex gap-2 h-[34px] items-center pl-2 pr-2 py-1 relative rounded-lg shrink-0 w-[226px] cursor-pointer">
|
||||||
|
<div className="basis-0 content-stretch flex gap-2 grow items-center min-h-px min-w-px relative shrink-0 pl-2">
|
||||||
|
<p className="[white-space-collapse:collapse] basis-0 font-medium grow leading-[1.6] min-h-px min-w-px not-italic overflow-ellipsis overflow-hidden relative shrink-0 text-[14px] text-[#404040] text-left text-nowrap">
|
||||||
|
권한 설정
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => router.push('/admin_lecture1')}
|
||||||
|
className="box-border content-stretch flex gap-2 h-[34px] items-center pl-2 pr-2 py-1 relative rounded-lg shrink-0 w-[226px] cursor-pointer hover:bg-gray-50 transition-colors"
|
||||||
|
>
|
||||||
|
<div className="basis-0 content-stretch flex gap-2 grow items-center min-h-px min-w-px relative shrink-0 pl-2">
|
||||||
|
<p className="[white-space-collapse:collapse] basis-0 font-medium grow leading-[1.6] min-h-px min-w-px not-italic overflow-ellipsis overflow-hidden relative shrink-0 text-[14px] text-[#404040] text-left text-nowrap">
|
||||||
|
교육 과정 관리
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => router.push('/admin_lecture2')}
|
||||||
|
className="box-border content-stretch flex gap-2 h-[34px] items-center pl-2 pr-2 py-1 relative rounded-lg shrink-0 w-[226px] cursor-pointer hover:bg-gray-50 transition-colors"
|
||||||
|
>
|
||||||
|
<div className="basis-0 content-stretch flex gap-2 grow items-center min-h-px min-w-px relative shrink-0 pl-2">
|
||||||
|
<p className="[white-space-collapse:collapse] basis-0 font-medium grow leading-[1.6] min-h-px min-w-px not-italic overflow-ellipsis overflow-hidden relative shrink-0 text-[14px] text-[#404040] text-left text-nowrap">
|
||||||
|
강좌 관리
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
<button className="box-border content-stretch flex gap-2 h-[34px] items-center pl-2 pr-2 py-1 relative rounded-lg shrink-0 w-[226px] cursor-pointer hover:bg-gray-50 transition-colors">
|
||||||
|
<div className="basis-0 content-stretch flex gap-2 grow items-center min-h-px min-w-px relative shrink-0 pl-2">
|
||||||
|
<p className="[white-space-collapse:collapse] basis-0 font-medium grow leading-[1.6] min-h-px min-w-px not-italic overflow-ellipsis overflow-hidden relative shrink-0 text-[14px] text-[#404040] text-left text-nowrap">
|
||||||
|
문제 은행 관리
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
<button className="box-border content-stretch flex gap-2 h-[34px] items-center pl-2 pr-2 py-1 relative rounded-lg shrink-0 w-[226px] cursor-pointer hover:bg-gray-50 transition-colors">
|
||||||
|
<div className="basis-0 content-stretch flex gap-2 grow items-center min-h-px min-w-px relative shrink-0 pl-2">
|
||||||
|
<p className="[white-space-collapse:collapse] basis-0 font-medium grow leading-[1.6] min-h-px min-w-px not-italic overflow-ellipsis overflow-hidden relative shrink-0 text-[14px] text-[#404040] text-left text-nowrap">
|
||||||
|
수료증 발급
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
<button className="box-border content-stretch flex gap-2 h-[34px] items-center pl-2 pr-2 py-1 relative rounded-lg shrink-0 w-[226px] cursor-pointer hover:bg-gray-50 transition-colors">
|
||||||
|
<div className="basis-0 content-stretch flex gap-2 grow items-center min-h-px min-w-px relative shrink-0 pl-2">
|
||||||
|
<p className="[white-space-collapse:collapse] basis-0 font-medium grow leading-[1.6] min-h-px min-w-px not-italic overflow-ellipsis overflow-hidden relative shrink-0 text-[14px] text-[#404040] text-left text-nowrap">
|
||||||
|
공지사항
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
<button className="box-border content-stretch flex gap-2 h-[34px] items-center pl-2 pr-2 py-1 relative rounded-lg shrink-0 w-[226px] cursor-pointer hover:bg-gray-50 transition-colors">
|
||||||
|
<div className="basis-0 content-stretch flex gap-2 grow items-center min-h-px min-w-px relative shrink-0 pl-2">
|
||||||
|
<p className="[white-space-collapse:collapse] basis-0 font-medium grow leading-[1.6] min-h-px min-w-px not-italic overflow-ellipsis overflow-hidden relative shrink-0 text-[14px] text-[#404040] text-left text-nowrap">
|
||||||
|
학습 자료실
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
<button className="box-border content-stretch flex gap-2 h-[34px] items-center pl-2 pr-2 py-1 relative rounded-lg shrink-0 w-[226px] cursor-pointer hover:bg-gray-50 transition-colors">
|
||||||
|
<div className="basis-0 content-stretch flex gap-2 grow items-center min-h-px min-w-px relative shrink-0 pl-2">
|
||||||
|
<p className="[white-space-collapse:collapse] basis-0 font-medium grow leading-[1.6] min-h-px min-w-px not-italic overflow-ellipsis overflow-hidden relative shrink-0 text-[14px] text-[#404040] text-left text-nowrap">
|
||||||
|
로그/접속 기록
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
<button className="box-border content-stretch flex gap-2 h-[34px] items-center pl-2 pr-2 py-1 relative rounded-lg shrink-0 w-[226px] cursor-pointer hover:bg-gray-50 transition-colors">
|
||||||
|
<div className="basis-0 content-stretch flex gap-2 grow items-center min-h-px min-w-px relative shrink-0 pl-2">
|
||||||
|
<p className="[white-space-collapse:collapse] basis-0 font-medium grow leading-[1.6] min-h-px min-w-px not-italic overflow-ellipsis overflow-hidden relative shrink-0 text-[14px] text-[#404040] text-left text-nowrap">
|
||||||
|
배너 관리
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* 로그아웃 */}
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
localStorage.removeItem('isLoggedIn');
|
||||||
|
localStorage.removeItem('isAdminLoggedIn');
|
||||||
|
router.push('/login');
|
||||||
|
}}
|
||||||
|
className="content-stretch flex gap-[9px] h-[36px] items-center relative shrink-0 cursor-pointer hover:opacity-80 transition-opacity"
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-center relative shrink-0">
|
||||||
|
<div className="flex-none rotate-[180deg] scale-y-[-100%]">
|
||||||
|
<div className="h-[23.12px] relative w-[22px]">
|
||||||
|
<Logout />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="box-border content-stretch flex gap-[10px] items-center justify-center px-[10px] py-[5px] relative shrink-0">
|
||||||
|
<p className="font-medium leading-[1.6] not-italic relative shrink-0 text-[16px] text-[#404040] text-nowrap whitespace-pre">
|
||||||
|
로그아웃
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 메인 콘텐츠 */}
|
||||||
|
<div className="absolute left-[250px] top-0 right-0 min-h-screen">
|
||||||
|
{/* 페이지 타이틀 탭 */}
|
||||||
|
<div className="absolute content-stretch flex gap-[24px] items-center left-[48px] top-[45px]">
|
||||||
|
<button
|
||||||
|
onClick={() => setSelectedTab('전체')}
|
||||||
|
className={`border-b-[2px] border-solid box-border content-stretch flex gap-[150px] items-center relative shrink-0 ${selectedTab === '전체' ? 'border-[#2b82e8]' : 'border-transparent'}`}
|
||||||
|
>
|
||||||
|
<div className="box-border content-stretch flex gap-[10px] items-center justify-center p-[10px] relative shrink-0">
|
||||||
|
<p className="font-bold leading-[normal] not-italic relative shrink-0 text-[#515151] text-[18px] text-nowrap whitespace-pre">
|
||||||
|
전체
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setSelectedTab('학습자')}
|
||||||
|
className={`border-b-[2px] border-solid box-border content-stretch flex gap-[150px] items-center relative shrink-0 ${selectedTab === '학습자' ? 'border-[#2b82e8]' : 'border-transparent'}`}
|
||||||
|
>
|
||||||
|
<div className="box-border content-stretch flex gap-[10px] items-center justify-center p-[10px] relative shrink-0">
|
||||||
|
<p className="font-bold leading-[normal] not-italic relative shrink-0 text-[#515151] text-[18px] text-nowrap whitespace-pre">
|
||||||
|
학습자
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setSelectedTab('강사')}
|
||||||
|
className={`border-b-[2px] border-solid box-border content-stretch flex gap-[150px] items-center relative shrink-0 ${selectedTab === '강사' ? 'border-[#2b82e8]' : 'border-transparent'}`}
|
||||||
|
>
|
||||||
|
<div className="box-border content-stretch flex gap-[10px] items-center justify-center p-[10px] relative shrink-0">
|
||||||
|
<p className="font-bold leading-[normal] not-italic relative shrink-0 text-[#515151] text-[18px] text-nowrap whitespace-pre">
|
||||||
|
강사
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setSelectedTab('운영자')}
|
||||||
|
className={`border-b-[2px] border-solid box-border content-stretch flex gap-[150px] items-center relative shrink-0 ${selectedTab === '운영자' ? 'border-[#2b82e8]' : 'border-transparent'}`}
|
||||||
|
>
|
||||||
|
<div className="box-border content-stretch flex gap-[10px] items-center justify-center p-[10px] relative shrink-0">
|
||||||
|
<p className="font-bold leading-[normal] not-italic relative shrink-0 text-[#515151] text-[18px] text-nowrap whitespace-pre">
|
||||||
|
운영자
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 사용자 목록 테이블 */}
|
||||||
|
<div className="absolute content-stretch flex flex-col items-start left-[48px] right-[101px] top-[135px] h-[671px] overflow-hidden">
|
||||||
|
{/* 테이블 헤더 */}
|
||||||
|
<div className="bg-[rgba(235,247,255,0.5)] content-stretch flex h-[41px] items-center relative shrink-0 w-full">
|
||||||
|
<div className="content-stretch flex items-center relative shrink-0 w-full">
|
||||||
|
<div className="basis-0 border-[0.5px_0px_0.5px_0.5px] border-[#b9b9b9] border-solid box-border content-stretch flex gap-[10px] grow items-center min-h-px min-w-px p-[10px] relative shrink-0">
|
||||||
|
<div className="basis-0 flex flex-col font-bold grow justify-center leading-[0] min-h-px min-w-px not-italic relative shrink-0 text-[18px] text-[#515151]">
|
||||||
|
<p className="leading-[normal]">가입일</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="basis-0 border-[0.5px_0px_0.5px_0.5px] border-[#b9b9b9] border-solid box-border content-stretch flex gap-[10px] grow items-center min-h-px min-w-px p-[10px] relative shrink-0">
|
||||||
|
<div className="basis-0 flex flex-col font-bold grow justify-center leading-[0] min-h-px min-w-px not-italic relative shrink-0 text-[18px] text-[#515151]">
|
||||||
|
<p className="leading-[normal]">사용자명</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="basis-0 border-[0.5px_0px_0.5px_0.5px] border-[#b9b9b9] border-solid box-border content-stretch flex gap-[10px] grow items-center min-h-px min-w-px p-[10px] relative shrink-0">
|
||||||
|
<div className="basis-0 flex flex-col font-bold grow justify-center leading-[0] min-h-px min-w-px not-italic relative shrink-0 text-[18px] text-[#515151]">
|
||||||
|
<p className="leading-[normal]">아이디(이메일)</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="basis-0 border-[0.5px_0px_0.5px_0.5px] border-[#b9b9b9] border-solid box-border content-stretch flex gap-[10px] grow items-center min-h-px min-w-px p-[10px] relative shrink-0">
|
||||||
|
<div className="basis-0 flex flex-col font-bold grow justify-center leading-[0] min-h-px min-w-px not-italic relative shrink-0 text-[18px] text-[#515151]">
|
||||||
|
<p className="leading-[normal]">권한</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="basis-0 border-[0.5px_0px_0.5px_0.5px] border-[#b9b9b9] border-solid box-border content-stretch flex gap-[10px] grow items-center min-h-px min-w-px p-[10px] relative shrink-0">
|
||||||
|
<div className="basis-0 flex flex-col font-bold grow justify-center leading-[0] min-h-px min-w-px not-italic relative shrink-0 text-[18px] text-[#515151]">
|
||||||
|
<p className="leading-[normal]">계정 상태</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="basis-0 border-[0.5px_0px_0.5px_0.5px] border-[#b9b9b9] border-solid box-border content-stretch flex gap-[10px] grow items-center min-h-px min-w-px p-[10px] relative shrink-0">
|
||||||
|
<div className="basis-0 flex flex-col font-bold grow justify-center leading-[0] min-h-px min-w-px not-italic relative shrink-0 text-[18px] text-[#515151]">
|
||||||
|
<p className="leading-[normal]">계정 관리</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 테이블 바디 */}
|
||||||
|
{users.length === 0 ? (
|
||||||
|
<div className="relative w-full flex items-center justify-center" style={{ minHeight: '400px' }}>
|
||||||
|
<div className="content-stretch flex flex-col gap-[16px] items-center relative shrink-0">
|
||||||
|
<div className="content-stretch flex flex-col gap-[2px] items-center relative shrink-0">
|
||||||
|
<p className="font-medium leading-[1.6] not-italic relative shrink-0 text-[16px] text-[#969696] text-nowrap whitespace-pre">
|
||||||
|
존재하는 계정이 없어요.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="content-stretch flex flex-col items-start relative shrink-0 w-full max-h-[630px] overflow-y-auto">
|
||||||
|
{currentUsers.map((user) => (
|
||||||
|
<div key={user.id} className="bg-white content-stretch flex items-center relative shrink-0 w-full">
|
||||||
|
<div className="content-stretch flex items-center relative shrink-0 w-full">
|
||||||
|
<div className="basis-0 border-[0.5px_0px_0.5px_0.5px] border-[#b9b9b9] border-solid box-border content-stretch flex gap-[10px] grow items-center min-h-px min-w-px p-[10px] relative shrink-0">
|
||||||
|
<div className="basis-0 flex flex-col font-medium grow justify-center leading-[0] min-h-px min-w-px not-italic relative shrink-0 text-[18px] text-[#515151]">
|
||||||
|
<p className="leading-[normal]">{user.joinDate}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="basis-0 border-[0.5px_0px_0.5px_0.5px] border-[#b9b9b9] border-solid box-border content-stretch flex gap-[10px] grow items-center min-h-px min-w-px p-[10px] relative shrink-0">
|
||||||
|
<div className="basis-0 flex flex-col font-medium grow justify-center leading-[0] min-h-px min-w-px not-italic relative shrink-0 text-[18px] text-[#515151]">
|
||||||
|
<p className="leading-[normal]">{user.name}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="basis-0 border-[0.5px_0px_0.5px_0.5px] border-[#b9b9b9] border-solid box-border content-stretch flex gap-[10px] grow items-center min-h-px min-w-px p-[10px] relative shrink-0">
|
||||||
|
<div className="basis-0 flex flex-col font-medium grow justify-center leading-[0] min-h-px min-w-px not-italic relative shrink-0 text-[18px] text-[#515151]">
|
||||||
|
<p className="leading-[normal]">{user.email}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="basis-0 border-[0.5px_0px_0.5px_0.5px] border-[#b9b9b9] border-solid box-border content-stretch flex gap-[10px] grow items-center min-h-px min-w-px p-[10px] relative shrink-0">
|
||||||
|
<div className="basis-0 flex flex-col font-medium grow justify-center leading-[0] min-h-px min-w-px not-italic relative shrink-0 text-[18px] text-[#515151]">
|
||||||
|
<p className="leading-[normal]">{user.role}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="basis-0 border-[0.5px_0px_0.5px_0.5px] border-[#b9b9b9] border-solid box-border content-stretch flex gap-[10px] grow items-center min-h-px min-w-px p-[10px] relative shrink-0">
|
||||||
|
<div className="basis-0 flex flex-col font-medium grow justify-center leading-[0] min-h-px min-w-px not-italic relative shrink-0 text-[18px] text-[#515151]">
|
||||||
|
<p className="leading-[normal]">{user.accountStatus}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="basis-0 border-[0.5px_0px_0.5px_0.5px] border-[#b9b9b9] border-solid box-border content-stretch flex gap-[10px] grow items-center min-h-px min-w-px p-[10px] relative shrink-0">
|
||||||
|
<div className="basis-0 flex flex-col font-medium grow justify-center leading-[0] min-h-px min-w-px not-italic relative shrink-0 text-[18px] text-[#515151]">
|
||||||
|
<p className="leading-[normal]">{user.accountManagement}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 페이지네이션 */}
|
||||||
|
{users.length > 0 && (
|
||||||
|
<div className="absolute content-stretch flex gap-2 items-center justify-center left-[48px] right-[101px] top-[909px]">
|
||||||
|
<button onClick={() => setCurrentPage(Math.max(1, currentPage - 1))}>
|
||||||
|
<PaginationBtnMove status={currentPage === 1 ? "Disabled" : "Default"} move="Previous" />
|
||||||
|
</button>
|
||||||
|
<div className="content-stretch flex gap-2 items-center relative shrink-0">
|
||||||
|
{pageNumbers.map((pageNum) => (
|
||||||
|
<button
|
||||||
|
key={pageNum}
|
||||||
|
onClick={() => setCurrentPage(pageNum)}
|
||||||
|
className={`relative rounded-[8px] shrink-0 size-[30px] cursor-pointer transition-colors ${currentPage === pageNum
|
||||||
|
? 'bg-[#1d1d1d]'
|
||||||
|
: 'border border-[#eeeeee] border-solid hover:bg-gray-50'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="overflow-clip relative rounded-[inherit] size-[30px]">
|
||||||
|
<div className={`absolute flex flex-col font-medium justify-center leading-[0] left-[15.5px] not-italic text-[14px] text-center text-nowrap top-[15px] translate-x-[-50%] translate-y-[-50%] ${currentPage === pageNum ? 'text-white' : 'text-[#515151]'
|
||||||
|
}`}>
|
||||||
|
<p className="leading-[1.6] whitespace-pre">{pageNum}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<button onClick={() => setCurrentPage(Math.min(totalPages, currentPage + 1))}>
|
||||||
|
<PaginationBtnMove status={currentPage === totalPages ? "Disabled" : "Default"} move="Next" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
644
app/admin_lecture1/page.tsx
Normal file
644
app/admin_lecture1/page.tsx
Normal file
@@ -0,0 +1,644 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import { isAdminLoggedIn } from '../../lib/auth';
|
||||||
|
import LoginPage from '../login/page';
|
||||||
|
import Close from '../../public/svg/close';
|
||||||
|
import Logout from '../../public/svg/logout';
|
||||||
|
|
||||||
|
const imgLogo = "http://localhost:3845/assets/89fda8e949171025b1232bae70fc9d442e4e70c8.png";
|
||||||
|
const imgArrowDisabled = "http://localhost:3845/assets/6edcb2defc36a2bf4a05a3abe53b8da3d42b2cb4.svg";
|
||||||
|
const imgArrowDefault = "http://localhost:3845/assets/ad0cb4418492f1b020bb38a2ff038a331294ce87.svg";
|
||||||
|
const imgArrowNext = "http://localhost:3845/assets/6328cf96ee1169c1425c2ce55e7a2dcca0374508.svg";
|
||||||
|
const imgLogout = "http://localhost:3845/assets/2277a58aa2f582fd42132439745c5bb051f322c2.svg";
|
||||||
|
const imgClose = "http://localhost:3845/assets/51206896b81ebe68b2d80b5b603e576227587007.svg";
|
||||||
|
const imgCloseX = "http://localhost:3845/assets/91a45a4f2e47cd854574f32ee6ac1b84e12807cf.svg";
|
||||||
|
const imgMingcuteDownLine = "http://localhost:3845/assets/a50ea05576e2bcccf052b5be6750edaf476ad4d4.svg";
|
||||||
|
|
||||||
|
interface Course {
|
||||||
|
id: number;
|
||||||
|
courseName: string;
|
||||||
|
instructorName: string;
|
||||||
|
isIncluded: string;
|
||||||
|
createdAt: string;
|
||||||
|
registrar: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type PaginationMove = "Previous" | "Next";
|
||||||
|
type PaginationStatus = "Default" | "Disabled";
|
||||||
|
|
||||||
|
function PaginationBtnMove({ status = "Default", move = "Previous" }: { status?: PaginationStatus; move?: PaginationMove }) {
|
||||||
|
const isDisabled = status === "Disabled";
|
||||||
|
const isNext = move === "Next";
|
||||||
|
|
||||||
|
if (isDisabled && isNext) {
|
||||||
|
// Disabled Next 버튼
|
||||||
|
return (
|
||||||
|
<div className="border border-[#eeeeee] border-solid relative rounded-[8px] shrink-0 size-[30px]">
|
||||||
|
<div className="overflow-clip relative rounded-[inherit] size-[30px]">
|
||||||
|
<div className="absolute flex items-center justify-center left-[5px] size-[20px] top-[5px]">
|
||||||
|
<div className="flex-none rotate-[180deg]">
|
||||||
|
<div className="overflow-clip relative size-[20px]">
|
||||||
|
<div className="absolute flex inset-[15.63%_30.21%] items-center justify-center">
|
||||||
|
<div className="flex-none h-[16.5px] rotate-[180deg] w-[9.5px]">
|
||||||
|
<div className="relative size-full">
|
||||||
|
<div className="absolute inset-0">
|
||||||
|
<img alt="" className="block max-w-none size-full" style={{ filter: 'opacity(0.88) brightness(0) saturate(100%) invert(88%)', opacity: 1 }} src={imgArrowNext} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isDisabled && !isNext) {
|
||||||
|
// Disabled Previous 버튼
|
||||||
|
return (
|
||||||
|
<div className="border border-[#eeeeee] border-solid relative rounded-[8px] shrink-0 size-[30px]">
|
||||||
|
<div className="overflow-clip relative rounded-[inherit] size-[30px]">
|
||||||
|
<div className="absolute flex items-center justify-center left-[5px] size-[20px] top-[5px]">
|
||||||
|
<div className="flex-none rotate-[180deg]">
|
||||||
|
<div className="overflow-clip relative size-[20px]">
|
||||||
|
<div className="absolute inset-[15.63%_30.21%]">
|
||||||
|
<div className="absolute inset-0">
|
||||||
|
<img alt="" className="block max-w-none size-full" style={{ filter: 'opacity(0.88) brightness(0) saturate(100%) invert(88%)', opacity: 1 }} src={imgArrowDefault} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isNext) {
|
||||||
|
// Default Next 버튼
|
||||||
|
return (
|
||||||
|
<div className="border border-[#eeeeee] border-solid relative rounded-[8px] shrink-0 size-[30px] cursor-pointer hover:bg-gray-50 transition-colors">
|
||||||
|
<div className="overflow-clip relative rounded-[inherit] size-[30px]">
|
||||||
|
<div className="absolute flex items-center justify-center left-[5px] size-[20px] top-[5px]">
|
||||||
|
<div className="flex-none rotate-[180deg]">
|
||||||
|
<div className="overflow-clip relative size-[20px]">
|
||||||
|
<div className="absolute flex inset-[15.63%_30.21%] items-center justify-center">
|
||||||
|
<div className="flex-none h-[16.5px] rotate-[180deg] w-[9.5px]">
|
||||||
|
<div className="relative size-full">
|
||||||
|
<div className="absolute inset-0">
|
||||||
|
<img alt="" className="block max-w-none size-full" src={imgArrowNext} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default Previous 버튼
|
||||||
|
return (
|
||||||
|
<div className="border border-[#eeeeee] border-solid relative rounded-[8px] shrink-0 size-[30px] cursor-pointer hover:bg-gray-50 transition-colors">
|
||||||
|
<div className="overflow-clip relative rounded-[inherit] size-[30px]">
|
||||||
|
<div className="absolute flex items-center justify-center left-[5px] size-[20px] top-[5px]">
|
||||||
|
<div className="flex-none rotate-[180deg]">
|
||||||
|
<div className="overflow-clip relative size-[20px]">
|
||||||
|
<div className="absolute inset-[15.63%_30.21%]">
|
||||||
|
<div className="absolute inset-0">
|
||||||
|
<img alt="" className="block max-w-none size-full" src={imgArrowDefault} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AdminLecture1Page() {
|
||||||
|
const router = useRouter();
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
||||||
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
|
const [courseName, setCourseName] = useState('');
|
||||||
|
const [instructorName, setInstructorName] = useState('');
|
||||||
|
const [showInstructorDropdown, setShowInstructorDropdown] = useState(false);
|
||||||
|
const [nextId, setNextId] = useState(14);
|
||||||
|
|
||||||
|
// 샘플 강좌 데이터
|
||||||
|
const [courses, setCourses] = useState<Course[]>([
|
||||||
|
{ id: 39, courseName: '방사선 적용', instructorName: '김강사', isIncluded: '포함', createdAt: '2026-01-15', registrar: '이철수' },
|
||||||
|
{ id: 38, courseName: '방사선 적용', instructorName: '김강사', isIncluded: '포함', createdAt: '2026-01-15', registrar: '이철수' },
|
||||||
|
{ id: 37, courseName: '방사선 적용', instructorName: '김강사', isIncluded: '포함', createdAt: '2026-01-15', registrar: '이철수' },
|
||||||
|
{ id: 36, courseName: '방사선 적용', instructorName: '김강사', isIncluded: '포함', createdAt: '2026-01-15', registrar: '이철수' },
|
||||||
|
{ id: 35, courseName: '방사선 적용', instructorName: '김강사', isIncluded: '포함', createdAt: '2026-01-15', registrar: '이철수' },
|
||||||
|
{ id: 34, courseName: '방사선 적용', instructorName: '김강사', isIncluded: '포함', createdAt: '2026-01-15', registrar: '이철수' },
|
||||||
|
{ id: 33, courseName: '방사선 적용', instructorName: '김강사', isIncluded: '포함', createdAt: '2026-01-15', registrar: '이철수' },
|
||||||
|
{ id: 32, courseName: '방사선 적용', instructorName: '김강사', isIncluded: '포함', createdAt: '2026-01-15', registrar: '이철수' },
|
||||||
|
{ id: 31, courseName: '방사선 적용', instructorName: '김강사', isIncluded: '포함', createdAt: '2026-01-15', registrar: '이철수' },
|
||||||
|
{ id: 30, courseName: '방사선 적용', instructorName: '김강사', isIncluded: '포함', createdAt: '2026-01-15', registrar: '이철수' },
|
||||||
|
{ id: 29, courseName: '방사선 적용', instructorName: '김강사', isIncluded: '포함', createdAt: '2026-01-15', registrar: '이철수' },
|
||||||
|
{ id: 28, courseName: '방사선 적용', instructorName: '김강사', isIncluded: '포함', createdAt: '2026-01-15', registrar: '이철수' },
|
||||||
|
{ id: 27, courseName: '방사선 적용', instructorName: '김강사', isIncluded: '포함', createdAt: '2026-01-15', registrar: '이철수' },
|
||||||
|
{ id: 26, courseName: '방사선 원리', instructorName: '김강사', isIncluded: '포함', createdAt: '2025-12-11', registrar: '김등록' },
|
||||||
|
{ id: 25, courseName: '방사선 원리', instructorName: '김강사', isIncluded: '포함', createdAt: '2025-12-11', registrar: '김등록' },
|
||||||
|
{ id: 24, courseName: '방사선 적용', instructorName: '김강사', isIncluded: '포함', createdAt: '2026-01-15', registrar: '이철수' },
|
||||||
|
{ id: 23, courseName: '방사선 적용', instructorName: '김강사', isIncluded: '포함', createdAt: '2026-01-15', registrar: '이철수' },
|
||||||
|
{ id: 22, courseName: '방사선 적용', instructorName: '김강사', isIncluded: '포함', createdAt: '2026-01-15', registrar: '이철수' },
|
||||||
|
{ id: 21, courseName: '방사선 적용', instructorName: '김강사', isIncluded: '포함', createdAt: '2026-01-15', registrar: '이철수' },
|
||||||
|
{ id: 20, courseName: '방사선 적용', instructorName: '김강사', isIncluded: '포함', createdAt: '2026-01-15', registrar: '이철수' },
|
||||||
|
{ id: 19, courseName: '방사선 적용', instructorName: '김강사', isIncluded: '포함', createdAt: '2026-01-15', registrar: '이철수' },
|
||||||
|
{ id: 18, courseName: '방사선 적용', instructorName: '김강사', isIncluded: '포함', createdAt: '2026-01-15', registrar: '이철수' },
|
||||||
|
{ id: 17, courseName: '방사선 적용', instructorName: '김강사', isIncluded: '포함', createdAt: '2026-01-15', registrar: '이철수' },
|
||||||
|
{ id: 16, courseName: '방사선 적용', instructorName: '김강사', isIncluded: '포함', createdAt: '2026-01-15', registrar: '이철수' },
|
||||||
|
{ id: 15, courseName: '방사선 적용', instructorName: '김강사', isIncluded: '포함', createdAt: '2026-01-15', registrar: '이철수' },
|
||||||
|
{ id: 14, courseName: '방사선 적용', instructorName: '김강사', isIncluded: '포함', createdAt: '2026-01-15', registrar: '이철수' },
|
||||||
|
{ id: 13, courseName: '방사선 원리', instructorName: '김강사', isIncluded: '포함', createdAt: '2025-12-11', registrar: '김등록' },
|
||||||
|
{ id: 12, courseName: '방사선 원리', instructorName: '김강사', isIncluded: '포함', createdAt: '2025-12-11', registrar: '김등록' },
|
||||||
|
{ id: 11, courseName: '방사선 적용', instructorName: '김강사', isIncluded: '포함', createdAt: '2026-01-15', registrar: '이철수' },
|
||||||
|
{ id: 10, courseName: '방사선 적용', instructorName: '김강사', isIncluded: '포함', createdAt: '2026-01-15', registrar: '이철수' },
|
||||||
|
{ id: 9, courseName: '방사선 적용', instructorName: '김강사', isIncluded: '포함', createdAt: '2026-01-15', registrar: '이철수' },
|
||||||
|
{ id: 8, courseName: '방사선 적용', instructorName: '김강사', isIncluded: '포함', createdAt: '2026-01-15', registrar: '이철수' },
|
||||||
|
{ id: 7, courseName: '방사선 적용', instructorName: '김강사', isIncluded: '포함', createdAt: '2026-01-15', registrar: '이철수' },
|
||||||
|
{ id: 6, courseName: '방사선 적용', instructorName: '김강사', isIncluded: '포함', createdAt: '2026-01-15', registrar: '이철수' },
|
||||||
|
{ id: 5, courseName: '방사선 적용', instructorName: '김강사', isIncluded: '포함', createdAt: '2026-01-15', registrar: '이철수' },
|
||||||
|
{ id: 4, courseName: '방사선 적용', instructorName: '김강사', isIncluded: '포함', createdAt: '2026-01-15', registrar: '이철수' },
|
||||||
|
{ id: 3, courseName: '방사선 적용', instructorName: '김강사', isIncluded: '포함', createdAt: '2026-01-15', registrar: '이철수' },
|
||||||
|
{ id: 2, courseName: '방사선 적용', instructorName: '김강사', isIncluded: '포함', createdAt: '2026-01-15', registrar: '이철수' },
|
||||||
|
{ id: 1, courseName: '방사선 적용', instructorName: '김강사', isIncluded: '포함', createdAt: '2026-01-15', registrar: '이철수' },
|
||||||
|
]);
|
||||||
|
|
||||||
|
const instructors = ['김강사', '이강사', '박강사', '최강사'];
|
||||||
|
|
||||||
|
// 관리자 인증 확인
|
||||||
|
useEffect(() => {
|
||||||
|
const checkAuth = () => {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
const isAdmin = isAdminLoggedIn();
|
||||||
|
setIsAuthenticated(isAdmin);
|
||||||
|
setIsLoading(false);
|
||||||
|
|
||||||
|
if (!isAdmin) {
|
||||||
|
// 인증되지 않은 경우 로그인 페이지로 리다이렉트
|
||||||
|
router.push('/login');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
checkAuth();
|
||||||
|
}, [router]);
|
||||||
|
|
||||||
|
const handleOpenModal = () => {
|
||||||
|
setIsModalOpen(true);
|
||||||
|
setCourseName('');
|
||||||
|
setInstructorName('');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCloseModal = () => {
|
||||||
|
setIsModalOpen(false);
|
||||||
|
setCourseName('');
|
||||||
|
setInstructorName('');
|
||||||
|
setShowInstructorDropdown(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
if (!courseName.trim() || !instructorName.trim()) {
|
||||||
|
alert('교육 과정명과 강사를 입력해 주세요.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const today = new Date();
|
||||||
|
const formattedDate = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`;
|
||||||
|
|
||||||
|
const newCourse: Course = {
|
||||||
|
id: nextId,
|
||||||
|
courseName: courseName.trim(),
|
||||||
|
instructorName: instructorName.trim(),
|
||||||
|
isIncluded: '미포함',
|
||||||
|
createdAt: formattedDate,
|
||||||
|
registrar: '관리자',
|
||||||
|
};
|
||||||
|
|
||||||
|
const updatedCourses = [...courses, newCourse];
|
||||||
|
setCourses(updatedCourses);
|
||||||
|
setNextId(nextId + 1);
|
||||||
|
|
||||||
|
// 13개를 넘어가면 해당 페이지로 이동
|
||||||
|
const newTotalPages = Math.ceil(updatedCourses.length / 13);
|
||||||
|
if (newTotalPages > 1 && updatedCourses.length > 13) {
|
||||||
|
setCurrentPage(newTotalPages);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleCloseModal();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSelectInstructor = (instructor: string) => {
|
||||||
|
setInstructorName(instructor);
|
||||||
|
setShowInstructorDropdown(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const itemsPerPage = 13;
|
||||||
|
const totalPages = Math.ceil(courses.length / itemsPerPage);
|
||||||
|
const pageNumbers = Array.from({ length: totalPages }, (_, i) => i + 1);
|
||||||
|
const startIndex = (currentPage - 1) * itemsPerPage;
|
||||||
|
const endIndex = startIndex + itemsPerPage;
|
||||||
|
const currentCourses = courses.slice(startIndex, endIndex);
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return null; // 로딩 중
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isAuthenticated) {
|
||||||
|
return <LoginPage />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-white relative size-full min-h-screen">
|
||||||
|
{/* 사이드바 */}
|
||||||
|
<div className="absolute bg-white border-r border-[#eeeeee] border-solid box-border content-stretch flex flex-col gap-[45px] items-center left-0 min-h-[1080px] pb-8 pt-[30px] px-0 top-0 w-[250px]">
|
||||||
|
{/* 로고 */}
|
||||||
|
<button
|
||||||
|
onClick={() => router.push('/')}
|
||||||
|
className="h-[102px] relative shrink-0 w-[99px] cursor-pointer hover:opacity-80 transition-opacity"
|
||||||
|
>
|
||||||
|
<div className="absolute inset-0 flex items-center justify-center overflow-hidden pointer-events-none">
|
||||||
|
<img alt="로고" className="h-full w-full object-contain" src="/logo.svg" />
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
{/* 메뉴 */}
|
||||||
|
<div className="box-border content-stretch flex flex-col items-center pb-0 pt-4 px-0 relative shrink-0 w-[250px]">
|
||||||
|
<div className="box-border content-stretch flex flex-col gap-2 items-start p-3 relative shrink-0 w-full">
|
||||||
|
<button
|
||||||
|
onClick={() => router.push('/admin_home')}
|
||||||
|
className="box-border content-stretch flex gap-2 h-[34px] items-center pl-2 pr-2 py-1 relative rounded-lg shrink-0 w-[226px] cursor-pointer hover:bg-gray-50 transition-colors"
|
||||||
|
>
|
||||||
|
<div className="basis-0 content-stretch flex gap-2 grow items-center min-h-px min-w-px relative shrink-0 pl-2">
|
||||||
|
<p className="[white-space-collapse:collapse] basis-0 font-medium grow leading-[1.6] min-h-px min-w-px not-italic overflow-ellipsis overflow-hidden relative shrink-0 text-[14px] text-[#404040] text-left text-nowrap">
|
||||||
|
권한 설정
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
<button className="bg-[#f7f7f7] box-border content-stretch flex gap-2 h-[34px] items-center pl-2 pr-2 py-1 relative rounded-lg shrink-0 w-[226px] cursor-pointer">
|
||||||
|
<div className="basis-0 content-stretch flex gap-2 grow items-center min-h-px min-w-px relative shrink-0 pl-2">
|
||||||
|
<p className="[white-space-collapse:collapse] basis-0 font-medium grow leading-[1.6] min-h-px min-w-px not-italic overflow-ellipsis overflow-hidden relative shrink-0 text-[14px] text-[#404040] text-left text-nowrap">
|
||||||
|
교육 과정 관리
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => router.push('/admin_lecture2')}
|
||||||
|
className="box-border content-stretch flex gap-2 h-[34px] items-center pl-2 pr-2 py-1 relative rounded-lg shrink-0 w-[226px] cursor-pointer hover:bg-gray-50 transition-colors"
|
||||||
|
>
|
||||||
|
<div className="basis-0 content-stretch flex gap-2 grow items-center min-h-px min-w-px relative shrink-0 pl-2">
|
||||||
|
<p className="[white-space-collapse:collapse] basis-0 font-medium grow leading-[1.6] min-h-px min-w-px not-italic overflow-ellipsis overflow-hidden relative shrink-0 text-[14px] text-[#404040] text-left text-nowrap">
|
||||||
|
강좌 관리
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
<button className="box-border content-stretch flex gap-2 h-[34px] items-center pl-2 pr-2 py-1 relative rounded-lg shrink-0 w-[226px] cursor-pointer hover:bg-gray-50 transition-colors">
|
||||||
|
<div className="basis-0 content-stretch flex gap-2 grow items-center min-h-px min-w-px relative shrink-0 pl-2">
|
||||||
|
<p className="[white-space-collapse:collapse] basis-0 font-medium grow leading-[1.6] min-h-px min-w-px not-italic overflow-ellipsis overflow-hidden relative shrink-0 text-[14px] text-[#404040] text-left text-nowrap">
|
||||||
|
문제 은행 관리
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
<button className="box-border content-stretch flex gap-2 h-[34px] items-center pl-2 pr-2 py-1 relative rounded-lg shrink-0 w-[226px] cursor-pointer hover:bg-gray-50 transition-colors">
|
||||||
|
<div className="basis-0 content-stretch flex gap-2 grow items-center min-h-px min-w-px relative shrink-0 pl-2">
|
||||||
|
<p className="[white-space-collapse:collapse] basis-0 font-medium grow leading-[1.6] min-h-px min-w-px not-italic overflow-ellipsis overflow-hidden relative shrink-0 text-[14px] text-[#404040] text-left text-nowrap">
|
||||||
|
수료증 발급
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
<button className="box-border content-stretch flex gap-2 h-[34px] items-center pl-2 pr-2 py-1 relative rounded-lg shrink-0 w-[226px] cursor-pointer hover:bg-gray-50 transition-colors">
|
||||||
|
<div className="basis-0 content-stretch flex gap-2 grow items-center min-h-px min-w-px relative shrink-0 pl-2">
|
||||||
|
<p className="[white-space-collapse:collapse] basis-0 font-medium grow leading-[1.6] min-h-px min-w-px not-italic overflow-ellipsis overflow-hidden relative shrink-0 text-[14px] text-[#404040] text-left text-nowrap">
|
||||||
|
공지사항
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
<button className="box-border content-stretch flex gap-2 h-[34px] items-center pl-2 pr-2 py-1 relative rounded-lg shrink-0 w-[226px] cursor-pointer hover:bg-gray-50 transition-colors">
|
||||||
|
<div className="basis-0 content-stretch flex gap-2 grow items-center min-h-px min-w-px relative shrink-0 pl-2">
|
||||||
|
<p className="[white-space-collapse:collapse] basis-0 font-medium grow leading-[1.6] min-h-px min-w-px not-italic overflow-ellipsis overflow-hidden relative shrink-0 text-[14px] text-[#404040] text-left text-nowrap">
|
||||||
|
학습 자료실
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
<button className="box-border content-stretch flex gap-2 h-[34px] items-center pl-2 pr-2 py-1 relative rounded-lg shrink-0 w-[226px] cursor-pointer hover:bg-gray-50 transition-colors">
|
||||||
|
<div className="basis-0 content-stretch flex gap-2 grow items-center min-h-px min-w-px relative shrink-0 pl-2">
|
||||||
|
<p className="[white-space-collapse:collapse] basis-0 font-medium grow leading-[1.6] min-h-px min-w-px not-italic overflow-ellipsis overflow-hidden relative shrink-0 text-[14px] text-[#404040] text-left text-nowrap">
|
||||||
|
로그/접속 기록
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
<button className="box-border content-stretch flex gap-2 h-[34px] items-center pl-2 pr-2 py-1 relative rounded-lg shrink-0 w-[226px] cursor-pointer hover:bg-gray-50 transition-colors">
|
||||||
|
<div className="basis-0 content-stretch flex gap-2 grow items-center min-h-px min-w-px relative shrink-0 pl-2">
|
||||||
|
<p className="[white-space-collapse:collapse] basis-0 font-medium grow leading-[1.6] min-h-px min-w-px not-italic overflow-ellipsis overflow-hidden relative shrink-0 text-[14px] text-[#404040] text-left text-nowrap">
|
||||||
|
배너 관리
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* 로그아웃 */}
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
localStorage.removeItem('isLoggedIn');
|
||||||
|
localStorage.removeItem('isAdminLoggedIn');
|
||||||
|
router.push('/login');
|
||||||
|
}}
|
||||||
|
className="content-stretch flex gap-[9px] h-[36px] items-center relative shrink-0 cursor-pointer hover:opacity-80 transition-opacity"
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-center relative shrink-0">
|
||||||
|
<div className="flex-none rotate-[180deg] scale-y-[-100%]">
|
||||||
|
<div className="h-[23.12px] relative w-[22px]">
|
||||||
|
<Logout />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="box-border content-stretch flex gap-[10px] items-center justify-center px-[10px] py-[5px] relative shrink-0">
|
||||||
|
<p className="font-medium leading-[1.6] not-italic relative shrink-0 text-[16px] text-[#404040] text-nowrap whitespace-pre">
|
||||||
|
로그아웃
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 메인 콘텐츠 */}
|
||||||
|
<div className="absolute left-[250px] top-0 right-0 min-h-screen">
|
||||||
|
{/* 헤더 */}
|
||||||
|
<div className="absolute content-stretch flex gap-[24px] items-center left-[48px] top-[45px]">
|
||||||
|
<div className="border-[#2b82e8] border-b-[2px] border-solid box-border content-stretch flex gap-[150px] items-center relative shrink-0">
|
||||||
|
<div className="box-border content-stretch flex gap-[10px] items-center justify-center p-[10px] relative shrink-0">
|
||||||
|
<p className="font-bold leading-[normal] not-italic relative shrink-0 text-[#515151] text-[18px] text-nowrap whitespace-pre">
|
||||||
|
교육 과정 관리
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 교육 과정 등록 버튼 */}
|
||||||
|
<button
|
||||||
|
onClick={handleOpenModal}
|
||||||
|
className="absolute bg-[#160e0e] box-border content-stretch flex gap-[10px] items-center justify-center right-[101px] p-[10px] rounded-[10px] top-[72px] w-[167px] cursor-pointer hover:bg-[#2a1f1f] transition-colors"
|
||||||
|
>
|
||||||
|
<p className="font-bold leading-[1.6] not-italic relative shrink-0 text-[18px] text-nowrap text-white whitespace-pre">
|
||||||
|
교육 과정 등록
|
||||||
|
</p>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* 테이블 */}
|
||||||
|
<div className="absolute content-stretch flex flex-col items-start left-[48px] right-[101px] top-[186px] h-[671px] overflow-hidden">
|
||||||
|
{/* 테이블 헤더 */}
|
||||||
|
<div className="bg-[#f5fbff] content-stretch flex h-[41px] items-center relative shrink-0 w-full">
|
||||||
|
<div className="content-stretch flex items-center relative shrink-0 w-full">
|
||||||
|
<div className="basis-0 border-[0.5px_0.5px_0.5px_0.5px] border-[#b9b9b9] border-solid box-border content-stretch flex gap-[10px] grow items-center min-h-px min-w-px p-[10px] relative shrink-0">
|
||||||
|
<div className="basis-0 flex flex-col font-bold grow justify-center leading-[0] min-h-px min-w-px not-italic relative shrink-0 text-[18px] text-[#515151]">
|
||||||
|
<p className="leading-[normal]">교육 과정명</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="basis-0 border-[0.5px_0.5px_0.5px_0px] border-[#b9b9b9] border-solid box-border content-stretch flex gap-[10px] grow items-center min-h-px min-w-px p-[10px] relative shrink-0">
|
||||||
|
<div className="basis-0 flex flex-col font-bold grow justify-center leading-[0] min-h-px min-w-px not-italic relative shrink-0 text-[18px] text-[#515151]">
|
||||||
|
<p className="leading-[normal]">강사명</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="basis-0 border-[0.5px_0.5px_0.5px_0px] border-[#b9b9b9] border-solid box-border content-stretch flex gap-[10px] grow items-center min-h-px min-w-px p-[10px] relative shrink-0">
|
||||||
|
<div className="basis-0 flex flex-col font-bold grow justify-center leading-[0] min-h-px min-w-px not-italic relative shrink-0 text-[18px] text-[#515151]">
|
||||||
|
<p className="leading-[normal]">강좌 포함 여부</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="basis-0 border-[0.5px_0.5px_0.5px_0px] border-[#b9b9b9] border-solid box-border content-stretch flex gap-[10px] grow items-center min-h-px min-w-px p-[10px] relative shrink-0">
|
||||||
|
<div className="basis-0 flex flex-col font-bold grow justify-center leading-[0] min-h-px min-w-px not-italic relative shrink-0 text-[18px] text-[#515151]">
|
||||||
|
<p className="leading-[normal]">생성일</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="basis-0 border-[0.5px_0.5px_0.5px_0px] border-[#b9b9b9] border-solid box-border content-stretch flex gap-[10px] grow items-center min-h-px min-w-px p-[10px] relative shrink-0">
|
||||||
|
<div className="basis-0 flex flex-col font-bold grow justify-center leading-[0] min-h-px min-w-px not-italic relative shrink-0 text-[18px] text-[#515151]">
|
||||||
|
<p className="leading-[normal]">등록 담당자</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 테이블 바디 */}
|
||||||
|
<div className="content-stretch flex flex-col items-start relative shrink-0 w-full max-h-[630px] overflow-y-auto">
|
||||||
|
{currentCourses.map((course) => (
|
||||||
|
<div key={course.id} className="bg-white content-stretch flex items-center relative shrink-0 w-full">
|
||||||
|
<div className="content-stretch flex items-center relative shrink-0 w-full">
|
||||||
|
<div className="basis-0 border-[0.5px_0.5px_0.5px_0.5px] border-[#b9b9b9] border-solid box-border content-stretch flex gap-[10px] grow items-center min-h-px min-w-px p-[10px] relative shrink-0">
|
||||||
|
<div className="basis-0 flex flex-col font-medium grow justify-center leading-[0] min-h-px min-w-px not-italic relative shrink-0 text-[18px] text-[#515151]">
|
||||||
|
<p className="leading-[normal]">{course.courseName}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="basis-0 border-[0.5px_0.5px_0.5px_0px] border-[#b9b9b9] border-solid box-border content-stretch flex gap-[10px] grow items-center min-h-px min-w-px p-[10px] relative shrink-0">
|
||||||
|
<div className="basis-0 flex flex-col font-medium grow justify-center leading-[0] min-h-px min-w-px not-italic relative shrink-0 text-[18px] text-[#515151]">
|
||||||
|
<p className="leading-[normal]">{course.instructorName}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="basis-0 border-[0.5px_0.5px_0.5px_0px] border-[#b9b9b9] border-solid box-border content-stretch flex gap-[10px] grow items-center min-h-px min-w-px p-[10px] relative shrink-0">
|
||||||
|
<div className="basis-0 flex flex-col font-medium grow justify-center leading-[0] min-h-px min-w-px not-italic relative shrink-0 text-[18px] text-[#515151]">
|
||||||
|
<p className="leading-[normal]">{course.isIncluded}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="basis-0 border-[0.5px_0.5px_0.5px_0px] border-[#b9b9b9] border-solid box-border content-stretch flex gap-[10px] grow items-center min-h-px min-w-px p-[10px] relative shrink-0">
|
||||||
|
<div className="basis-0 flex flex-col font-medium grow justify-center leading-[0] min-h-px min-w-px not-italic relative shrink-0 text-[18px] text-[#515151]">
|
||||||
|
<p className="leading-[normal]">{course.createdAt}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="basis-0 border-[0.5px_0.5px_0.5px_0px] border-[#b9b9b9] border-solid box-border content-stretch flex gap-[10px] grow items-center min-h-px min-w-px p-[10px] relative shrink-0">
|
||||||
|
<div className="basis-0 flex flex-col font-medium grow justify-center leading-[0] min-h-px min-w-px not-italic relative shrink-0 text-[18px] text-[#515151]">
|
||||||
|
<p className="leading-[normal]">{course.registrar}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 페이지네이션 */}
|
||||||
|
<div className="absolute content-stretch flex gap-2 items-center justify-center left-[48px] right-[101px] top-[909px]">
|
||||||
|
<button onClick={() => setCurrentPage(Math.max(1, currentPage - 1))}>
|
||||||
|
<PaginationBtnMove status={currentPage === 1 ? "Disabled" : "Default"} move="Previous" />
|
||||||
|
</button>
|
||||||
|
<div className="content-stretch flex gap-2 items-center relative shrink-0">
|
||||||
|
{pageNumbers.map((pageNum) => (
|
||||||
|
<button
|
||||||
|
key={pageNum}
|
||||||
|
onClick={() => setCurrentPage(pageNum)}
|
||||||
|
className={`relative rounded-[8px] shrink-0 size-[30px] cursor-pointer transition-colors ${currentPage === pageNum
|
||||||
|
? 'bg-[#1d1d1d]'
|
||||||
|
: 'border border-[#eeeeee] border-solid hover:bg-gray-50'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="overflow-clip relative rounded-[inherit] size-[30px]">
|
||||||
|
<div className={`absolute flex flex-col font-medium justify-center leading-[0] left-[15.5px] not-italic text-[14px] text-center text-nowrap top-[15px] translate-x-[-50%] translate-y-[-50%] ${currentPage === pageNum ? 'text-white' : 'text-[#515151]'
|
||||||
|
}`}>
|
||||||
|
<p className="leading-[1.6] whitespace-pre">{pageNum}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<button onClick={() => setCurrentPage(Math.min(totalPages, currentPage + 1))}>
|
||||||
|
<PaginationBtnMove status={currentPage === totalPages ? "Disabled" : "Default"} move="Next" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 모달 팝업 */}
|
||||||
|
{isModalOpen && (
|
||||||
|
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||||
|
{/* 배경 오버레이 */}
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 bg-[rgba(0,0,0,0.6)]"
|
||||||
|
onClick={handleCloseModal}
|
||||||
|
/>
|
||||||
|
{/* 모달 컨텐츠 */}
|
||||||
|
<div
|
||||||
|
className="relative bg-[#f7f7f7] h-[504px] overflow-clip shadow-[0px_4px_4px_0px_rgba(0,0,0,0.25)] w-[683px]"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
{/* 닫기 버튼 */}
|
||||||
|
<button
|
||||||
|
onClick={handleCloseModal}
|
||||||
|
className="absolute left-[622px] overflow-clip size-[30px] top-[41px] cursor-pointer hover:opacity-80 transition-opacity z-10"
|
||||||
|
>
|
||||||
|
<div className="absolute inset-0 pointer-events-none flex items-center justify-center">
|
||||||
|
<Close />
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* 저장 버튼 */}
|
||||||
|
<button
|
||||||
|
onClick={handleSave}
|
||||||
|
className="absolute bg-[#599ded] box-border content-stretch flex gap-[10px] h-[50px] items-center justify-center left-[358px] p-[10px] rounded-[10px] top-[414px] w-[264px] cursor-pointer hover:bg-[#4a8ddc] transition-colors"
|
||||||
|
>
|
||||||
|
<p className="font-medium leading-[normal] not-italic relative shrink-0 text-[18px] text-nowrap text-white whitespace-pre">
|
||||||
|
저장
|
||||||
|
</p>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* 취소 버튼 */}
|
||||||
|
<button
|
||||||
|
onClick={handleCloseModal}
|
||||||
|
className="absolute bg-[#eeeeee] box-border content-stretch flex gap-[10px] h-[50px] items-center justify-center left-[62px] p-[10px] rounded-[10px] top-[414px] w-[264px] cursor-pointer hover:bg-[#e0e0e0] transition-colors"
|
||||||
|
>
|
||||||
|
<p className="font-medium leading-[normal] not-italic relative shrink-0 text-[#515151] text-[18px] text-nowrap whitespace-pre">
|
||||||
|
취소
|
||||||
|
</p>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* 폼 컨텐츠 */}
|
||||||
|
<div className="absolute content-stretch flex flex-col gap-[20px] items-start left-[31px] top-[41px] w-[621px]">
|
||||||
|
<div className="content-stretch flex flex-col gap-[20px] items-start relative shrink-0 w-[621px]">
|
||||||
|
{/* 제목 */}
|
||||||
|
<div className="content-stretch flex gap-[10px] items-center relative shrink-0 w-full">
|
||||||
|
<div className="flex flex-col font-bold justify-center leading-[0] not-italic relative shrink-0 text-[20px] text-[#404040] text-nowrap">
|
||||||
|
<p className="leading-[1.6] whitespace-pre">교육 과정 등록</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 교육 과정명 */}
|
||||||
|
<div className="content-stretch flex flex-col gap-[20px] items-start relative shrink-0 w-full">
|
||||||
|
<div className="content-stretch flex gap-[16px] items-center relative shrink-0 w-full">
|
||||||
|
<div className="content-stretch flex gap-[10px] items-center relative shrink-0 w-[177px]">
|
||||||
|
<p className="font-medium leading-[normal] not-italic relative shrink-0 text-[#515151] text-[18px] text-nowrap whitespace-pre">
|
||||||
|
교육 과정명<span className="text-[#e61a1a]"> *</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={courseName}
|
||||||
|
onChange={(e) => setCourseName(e.target.value)}
|
||||||
|
placeholder="교육 과정명을 입력해 주세요."
|
||||||
|
className="basis-0 border border-[#b9b9b9] border-solid box-border content-stretch flex gap-[10px] grow items-center justify-center min-h-px min-w-px p-[10px] relative rounded-[8px] shrink-0 bg-white text-[16px] text-[#515151] focus:outline-none focus:border-[#2b82e8]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 강사 */}
|
||||||
|
<div className="content-stretch flex flex-col gap-[20px] items-start relative shrink-0 w-full">
|
||||||
|
<div className="content-stretch flex gap-[16px] items-center relative shrink-0 w-full">
|
||||||
|
<div className="content-stretch flex gap-[10px] items-center relative shrink-0 w-[177px]">
|
||||||
|
<p className="font-medium leading-[normal] not-italic relative shrink-0 text-[#515151] text-[18px] text-nowrap whitespace-pre">
|
||||||
|
강사<span className="text-[#e61a1a]"> *</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="basis-0 relative grow min-h-px min-w-px shrink-0">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowInstructorDropdown(!showInstructorDropdown)}
|
||||||
|
className="basis-0 border border-[#b9b9b9] border-solid box-border content-stretch flex gap-[10px] grow items-center justify-between min-h-px min-w-px p-[10px] relative rounded-[8px] shrink-0 w-full bg-white cursor-pointer hover:border-[#2b82e8] transition-colors"
|
||||||
|
>
|
||||||
|
<p className={`basis-0 font-medium grow leading-[1.6] min-h-px min-w-px not-italic relative shrink-0 text-left text-[16px] ${instructorName ? 'text-[#515151]' : 'text-[#b9b9b9]'}`}>
|
||||||
|
{instructorName || '강사를 선택해 주세요.'}
|
||||||
|
</p>
|
||||||
|
<div className="overflow-clip relative shrink-0 size-[24px]">
|
||||||
|
<img alt="" className="block max-w-none size-full" src={imgMingcuteDownLine} />
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
{showInstructorDropdown && (
|
||||||
|
<div className="absolute top-full left-0 right-0 mt-1 bg-white box-border content-stretch flex flex-col gap-[4px] items-start p-[8px] rounded-[8px] shadow-[0px_8px_16px_-2px_rgba(27,33,44,0.12)] z-10">
|
||||||
|
{instructors.map((instructor) => (
|
||||||
|
<button
|
||||||
|
key={instructor}
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleSelectInstructor(instructor)}
|
||||||
|
className={`box-border content-stretch flex gap-[10px] h-[38px] items-center overflow-clip p-[8px] relative rounded-[8px] shrink-0 w-full transition-colors ${instructorName === instructor ? 'bg-[#e3effc]' : 'bg-white hover:bg-[#f7f7f7]'}`}
|
||||||
|
>
|
||||||
|
<div className="basis-0 flex flex-col font-normal grow justify-center leading-[0] min-h-px min-w-px not-italic relative shrink-0 text-[14px] text-[#515151]">
|
||||||
|
<p className="leading-[1.6]">{instructor}</p>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 교육 과정 이미지 */}
|
||||||
|
<div className="content-stretch flex flex-col gap-[20px] items-start relative shrink-0 w-full">
|
||||||
|
<div className="content-stretch flex gap-[20px] items-center relative shrink-0 w-full">
|
||||||
|
<div className="basis-0 content-stretch flex gap-[16px] grow items-center min-h-px min-w-px relative shrink-0">
|
||||||
|
<div className="content-stretch flex gap-[10px] items-center relative shrink-0 w-[177px]">
|
||||||
|
<p className="font-medium leading-[normal] not-italic relative shrink-0 text-[#515151] text-[18px] text-nowrap whitespace-pre">
|
||||||
|
교육 과정 이미지
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="content-stretch flex gap-[16px] items-center relative shrink-0">
|
||||||
|
<button className="bg-[#2b82e8] box-border content-stretch flex gap-2 h-[36px] items-center justify-center overflow-clip px-5 py-[13px] relative rounded-[8px] shrink-0 w-[114px] cursor-pointer hover:bg-[#1f6fc4] transition-colors">
|
||||||
|
<p className="font-medium leading-[1.6] not-italic relative shrink-0 text-[14px] text-white text-center text-nowrap whitespace-pre">
|
||||||
|
첨부
|
||||||
|
</p>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-[#f7f7f7] border border-[#eeeeee] border-solid box-border content-stretch flex flex-col gap-4 items-center px-[10px] py-[20px] relative rounded-[10px] shrink-0 w-full">
|
||||||
|
<div className="content-stretch flex flex-col gap-[2px] items-center relative shrink-0">
|
||||||
|
<div className="font-medium leading-[1.6] not-italic relative shrink-0 text-[#b9b9b9] text-[16px] text-center text-nowrap whitespace-pre">
|
||||||
|
<p className="mb-0">30MB 미만의 교육 과정 이미지를 첨부해 주세요.</p>
|
||||||
|
<p>첨부하지 않을 경우 기본 이미지가 노출돼요.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
436
app/admin_lecture2/page.tsx
Normal file
436
app/admin_lecture2/page.tsx
Normal file
@@ -0,0 +1,436 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import { isAdminLoggedIn } from '../../lib/auth';
|
||||||
|
import LoginPage from '../login/page';
|
||||||
|
import Logout from '../../public/svg/logout';
|
||||||
|
|
||||||
|
const imgArrowDisabled = "http://localhost:3845/assets/6edcb2defc36a2bf4a05a3abe53b8da3d42b2cb4.svg";
|
||||||
|
const imgArrowDefault = "http://localhost:3845/assets/ad0cb4418492f1b020bb38a2ff038a331294ce87.svg";
|
||||||
|
const imgArrowNext = "http://localhost:3845/assets/6328cf96ee1169c1425c2ce55e7a2dcca0374508.svg";
|
||||||
|
|
||||||
|
type PaginationMove = "Previous" | "Next";
|
||||||
|
type PaginationStatus = "Default" | "Disabled";
|
||||||
|
|
||||||
|
function PaginationBtnMove({ status = "Default", move = "Previous" }: { status?: PaginationStatus; move?: PaginationMove }) {
|
||||||
|
const isDisabled = status === "Disabled";
|
||||||
|
const isNext = move === "Next";
|
||||||
|
|
||||||
|
if (isDisabled && isNext) {
|
||||||
|
return (
|
||||||
|
<div className="border border-[#eeeeee] border-solid relative rounded-[8px] shrink-0 size-[30px]">
|
||||||
|
<div className="overflow-clip relative rounded-[inherit] size-[30px]">
|
||||||
|
<div className="absolute flex items-center justify-center left-[5px] size-[20px] top-[5px]">
|
||||||
|
<div className="flex-none rotate-[180deg]">
|
||||||
|
<div className="overflow-clip relative size-[20px]">
|
||||||
|
<div className="absolute flex inset-[15.63%_30.21%] items-center justify-center">
|
||||||
|
<div className="flex-none h-[16.5px] rotate-[180deg] w-[9.5px]">
|
||||||
|
<div className="relative size-full">
|
||||||
|
<div className="absolute inset-0">
|
||||||
|
<img alt="" className="block max-w-none size-full" style={{ filter: 'opacity(0.88) brightness(0) saturate(100%) invert(88%)', opacity: 1 }} src={imgArrowNext} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isDisabled && !isNext) {
|
||||||
|
return (
|
||||||
|
<div className="border border-[#eeeeee] border-solid relative rounded-[8px] shrink-0 size-[30px]">
|
||||||
|
<div className="overflow-clip relative rounded-[inherit] size-[30px]">
|
||||||
|
<div className="absolute flex items-center justify-center left-[5px] size-[20px] top-[5px]">
|
||||||
|
<div className="flex-none rotate-[180deg]">
|
||||||
|
<div className="overflow-clip relative size-[20px]">
|
||||||
|
<div className="absolute inset-[15.63%_30.21%]">
|
||||||
|
<div className="absolute inset-0">
|
||||||
|
<img alt="" className="block max-w-none size-full" style={{ filter: 'opacity(0.88) brightness(0) saturate(100%) invert(88%)', opacity: 1 }} src={imgArrowDefault} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isNext) {
|
||||||
|
return (
|
||||||
|
<div className="border border-[#eeeeee] border-solid relative rounded-[8px] shrink-0 size-[30px] cursor-pointer hover:bg-gray-50 transition-colors">
|
||||||
|
<div className="overflow-clip relative rounded-[inherit] size-[30px]">
|
||||||
|
<div className="absolute flex items-center justify-center left-[5px] size-[20px] top-[5px]">
|
||||||
|
<div className="flex-none rotate-[180deg]">
|
||||||
|
<div className="overflow-clip relative size-[20px]">
|
||||||
|
<div className="absolute flex inset-[15.63%_30.21%] items-center justify-center">
|
||||||
|
<div className="flex-none h-[16.5px] rotate-[180deg] w-[9.5px]">
|
||||||
|
<div className="relative size-full">
|
||||||
|
<div className="absolute inset-0">
|
||||||
|
<img alt="" className="block max-w-none size-full" src={imgArrowNext} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="border border-[#eeeeee] border-solid relative rounded-[8px] shrink-0 size-[30px] cursor-pointer hover:bg-gray-50 transition-colors">
|
||||||
|
<div className="overflow-clip relative rounded-[inherit] size-[30px]">
|
||||||
|
<div className="absolute flex items-center justify-center left-[5px] size-[20px] top-[5px]">
|
||||||
|
<div className="flex-none rotate-[180deg]">
|
||||||
|
<div className="overflow-clip relative size-[20px]">
|
||||||
|
<div className="absolute inset-[15.63%_30.21%]">
|
||||||
|
<div className="absolute inset-0">
|
||||||
|
<img alt="" className="block max-w-none size-full" src={imgArrowDefault} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Lecture {
|
||||||
|
id: number;
|
||||||
|
courseName: string;
|
||||||
|
lectureName: string;
|
||||||
|
attachedFile: string;
|
||||||
|
questionCount: number;
|
||||||
|
registrar: string;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AdminLecture2Page() {
|
||||||
|
const router = useRouter();
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
||||||
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
|
const [lectures, setLectures] = useState<Lecture[]>([
|
||||||
|
{ id: 39, courseName: '방사선 적용', lectureName: '방사선 기본 원리', attachedFile: '파일1.pdf', questionCount: 10, registrar: '이철수', createdAt: '2026-01-15' },
|
||||||
|
{ id: 38, courseName: '방사선 적용', lectureName: '방사선 안전 관리', attachedFile: '파일2.pdf', questionCount: 15, registrar: '이철수', createdAt: '2026-01-14' },
|
||||||
|
{ id: 37, courseName: '방사선 원리', lectureName: '방사선 측정 기법', attachedFile: '파일3.pdf', questionCount: 12, registrar: '김등록', createdAt: '2026-01-13' },
|
||||||
|
{ id: 36, courseName: '방사선 적용', lectureName: '방사선 장비 사용법', attachedFile: '파일4.pdf', questionCount: 8, registrar: '이철수', createdAt: '2026-01-12' },
|
||||||
|
{ id: 35, courseName: '방사선 원리', lectureName: '방사선 물리학 기초', attachedFile: '파일5.pdf', questionCount: 20, registrar: '김등록', createdAt: '2026-01-11' },
|
||||||
|
{ id: 34, courseName: '방사선 적용', lectureName: '방사선 보호 장비', attachedFile: '파일6.pdf', questionCount: 14, registrar: '이철수', createdAt: '2026-01-10' },
|
||||||
|
{ id: 33, courseName: '방사선 적용', lectureName: '방사선 응용 실습', attachedFile: '파일7.pdf', questionCount: 16, registrar: '이철수', createdAt: '2026-01-09' },
|
||||||
|
{ id: 32, courseName: '방사선 원리', lectureName: '방사선 화학 반응', attachedFile: '파일8.pdf', questionCount: 11, registrar: '김등록', createdAt: '2026-01-08' },
|
||||||
|
{ id: 31, courseName: '방사선 적용', lectureName: '방사선 의료 응용', attachedFile: '파일9.pdf', questionCount: 18, registrar: '이철수', createdAt: '2026-01-07' },
|
||||||
|
{ id: 30, courseName: '방사선 적용', lectureName: '방사선 산업 응용', attachedFile: '파일10.pdf', questionCount: 13, registrar: '이철수', createdAt: '2026-01-06' },
|
||||||
|
{ id: 29, courseName: '방사선 원리', lectureName: '방사선 생물학', attachedFile: '파일11.pdf', questionCount: 17, registrar: '김등록', createdAt: '2026-01-05' },
|
||||||
|
{ id: 28, courseName: '방사선 적용', lectureName: '방사선 환경 모니터링', attachedFile: '파일12.pdf', questionCount: 9, registrar: '이철수', createdAt: '2026-01-04' },
|
||||||
|
{ id: 27, courseName: '방사선 적용', lectureName: '방사선 검사 기법', attachedFile: '파일13.pdf', questionCount: 19, registrar: '이철수', createdAt: '2026-01-03' },
|
||||||
|
{ id: 26, courseName: '방사선 원리', lectureName: '방사선 에너지 전달', attachedFile: '파일14.pdf', questionCount: 21, registrar: '김등록', createdAt: '2026-01-02' },
|
||||||
|
{ id: 25, courseName: '방사선 적용', lectureName: '방사선 처리 기술', attachedFile: '파일15.pdf', questionCount: 7, registrar: '이철수', createdAt: '2026-01-01' },
|
||||||
|
{ id: 24, courseName: '방사선 적용', lectureName: '방사선 품질 관리', attachedFile: '파일16.pdf', questionCount: 22, registrar: '이철수', createdAt: '2025-12-31' },
|
||||||
|
{ id: 23, courseName: '방사선 원리', lectureName: '방사선 방어 이론', attachedFile: '파일17.pdf', questionCount: 6, registrar: '김등록', createdAt: '2025-12-30' },
|
||||||
|
{ id: 22, courseName: '방사선 적용', lectureName: '방사선 계측법', attachedFile: '파일18.pdf', questionCount: 24, registrar: '이철수', createdAt: '2025-12-29' },
|
||||||
|
{ id: 21, courseName: '방사선 적용', lectureName: '방사선 안전 규정', attachedFile: '파일19.pdf', questionCount: 5, registrar: '이철수', createdAt: '2025-12-28' },
|
||||||
|
{ id: 20, courseName: '방사선 원리', lectureName: '방사선 핵물리학', attachedFile: '파일20.pdf', questionCount: 25, registrar: '김등록', createdAt: '2025-12-27' },
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 관리자 인증 확인
|
||||||
|
useEffect(() => {
|
||||||
|
const checkAuth = () => {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
const isAdmin = isAdminLoggedIn();
|
||||||
|
setIsAuthenticated(isAdmin);
|
||||||
|
setIsLoading(false);
|
||||||
|
|
||||||
|
if (!isAdmin) {
|
||||||
|
// 인증되지 않은 경우 로그인 페이지로 리다이렉트
|
||||||
|
router.push('/login');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
checkAuth();
|
||||||
|
}, [router]);
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return null; // 로딩 중
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isAuthenticated) {
|
||||||
|
return <LoginPage />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const itemsPerPage = 13;
|
||||||
|
const totalPages = Math.ceil(lectures.length / itemsPerPage);
|
||||||
|
const pageNumbers = Array.from({ length: totalPages }, (_, i) => i + 1);
|
||||||
|
const startIndex = (currentPage - 1) * itemsPerPage;
|
||||||
|
const endIndex = startIndex + itemsPerPage;
|
||||||
|
const currentLectures = lectures.slice(startIndex, endIndex);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-white relative size-full min-h-screen">
|
||||||
|
{/* 사이드바 */}
|
||||||
|
<div className="absolute bg-white border-r border-[#eeeeee] border-solid box-border content-stretch flex flex-col gap-[45px] items-center left-0 min-h-[1080px] pb-8 pt-[30px] px-0 top-0 w-[250px]">
|
||||||
|
{/* 로고 */}
|
||||||
|
<button
|
||||||
|
onClick={() => router.push('/')}
|
||||||
|
className="h-[102px] relative shrink-0 w-[99px] cursor-pointer hover:opacity-80 transition-opacity"
|
||||||
|
>
|
||||||
|
<div className="absolute inset-0 flex items-center justify-center overflow-hidden pointer-events-none">
|
||||||
|
<img alt="로고" className="h-full w-full object-contain" src="/logo.svg" />
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
{/* 메뉴 */}
|
||||||
|
<div className="box-border content-stretch flex flex-col items-center pb-0 pt-4 px-0 relative shrink-0 w-[250px]">
|
||||||
|
<div className="box-border content-stretch flex flex-col gap-2 items-start p-3 relative shrink-0 w-full">
|
||||||
|
<button
|
||||||
|
onClick={() => router.push('/admin_home')}
|
||||||
|
className="box-border content-stretch flex gap-2 h-[34px] items-center pl-2 pr-2 py-1 relative rounded-lg shrink-0 w-[226px] cursor-pointer hover:bg-gray-50 transition-colors"
|
||||||
|
>
|
||||||
|
<div className="basis-0 content-stretch flex gap-2 grow items-center min-h-px min-w-px relative shrink-0 pl-2">
|
||||||
|
<p className="[white-space-collapse:collapse] basis-0 font-medium grow leading-[1.6] min-h-px min-w-px not-italic overflow-ellipsis overflow-hidden relative shrink-0 text-[14px] text-[#404040] text-left text-nowrap">
|
||||||
|
권한 설정
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => router.push('/admin_lecture1')}
|
||||||
|
className="box-border content-stretch flex gap-2 h-[34px] items-center pl-2 pr-2 py-1 relative rounded-lg shrink-0 w-[226px] cursor-pointer hover:bg-gray-50 transition-colors"
|
||||||
|
>
|
||||||
|
<div className="basis-0 content-stretch flex gap-2 grow items-center min-h-px min-w-px relative shrink-0 pl-2">
|
||||||
|
<p className="[white-space-collapse:collapse] basis-0 font-medium grow leading-[1.6] min-h-px min-w-px not-italic overflow-ellipsis overflow-hidden relative shrink-0 text-[14px] text-[#404040] text-left text-nowrap">
|
||||||
|
교육 과정 관리
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
<button className="bg-[#f7f7f7] box-border content-stretch flex gap-2 h-[34px] items-center pl-2 pr-2 py-1 relative rounded-lg shrink-0 w-[226px] cursor-pointer">
|
||||||
|
<div className="basis-0 content-stretch flex gap-2 grow items-center min-h-px min-w-px relative shrink-0 pl-2">
|
||||||
|
<p className="[white-space-collapse:collapse] basis-0 font-medium grow leading-[1.6] min-h-px min-w-px not-italic overflow-ellipsis overflow-hidden relative shrink-0 text-[14px] text-[#404040] text-left text-nowrap">
|
||||||
|
강좌 관리
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
<button className="box-border content-stretch flex gap-2 h-[34px] items-center pl-2 pr-2 py-1 relative rounded-lg shrink-0 w-[226px] cursor-pointer hover:bg-gray-50 transition-colors">
|
||||||
|
<div className="basis-0 content-stretch flex gap-2 grow items-center min-h-px min-w-px relative shrink-0 pl-2">
|
||||||
|
<p className="[white-space-collapse:collapse] basis-0 font-medium grow leading-[1.6] min-h-px min-w-px not-italic overflow-ellipsis overflow-hidden relative shrink-0 text-[14px] text-[#404040] text-left text-nowrap">
|
||||||
|
문제 은행 관리
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
<button className="box-border content-stretch flex gap-2 h-[34px] items-center pl-2 pr-2 py-1 relative rounded-lg shrink-0 w-[226px] cursor-pointer hover:bg-gray-50 transition-colors">
|
||||||
|
<div className="basis-0 content-stretch flex gap-2 grow items-center min-h-px min-w-px relative shrink-0 pl-2">
|
||||||
|
<p className="[white-space-collapse:collapse] basis-0 font-medium grow leading-[1.6] min-h-px min-w-px not-italic overflow-ellipsis overflow-hidden relative shrink-0 text-[14px] text-[#404040] text-left text-nowrap">
|
||||||
|
수료증 발급
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
<button className="box-border content-stretch flex gap-2 h-[34px] items-center pl-2 pr-2 py-1 relative rounded-lg shrink-0 w-[226px] cursor-pointer hover:bg-gray-50 transition-colors">
|
||||||
|
<div className="basis-0 content-stretch flex gap-2 grow items-center min-h-px min-w-px relative shrink-0 pl-2">
|
||||||
|
<p className="[white-space-collapse:collapse] basis-0 font-medium grow leading-[1.6] min-h-px min-w-px not-italic overflow-ellipsis overflow-hidden relative shrink-0 text-[14px] text-[#404040] text-left text-nowrap">
|
||||||
|
공지사항
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
<button className="box-border content-stretch flex gap-2 h-[34px] items-center pl-2 pr-2 py-1 relative rounded-lg shrink-0 w-[226px] cursor-pointer hover:bg-gray-50 transition-colors">
|
||||||
|
<div className="basis-0 content-stretch flex gap-2 grow items-center min-h-px min-w-px relative shrink-0 pl-2">
|
||||||
|
<p className="[white-space-collapse:collapse] basis-0 font-medium grow leading-[1.6] min-h-px min-w-px not-italic overflow-ellipsis overflow-hidden relative shrink-0 text-[14px] text-[#404040] text-left text-nowrap">
|
||||||
|
학습 자료실
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
<button className="box-border content-stretch flex gap-2 h-[34px] items-center pl-2 pr-2 py-1 relative rounded-lg shrink-0 w-[226px] cursor-pointer hover:bg-gray-50 transition-colors">
|
||||||
|
<div className="basis-0 content-stretch flex gap-2 grow items-center min-h-px min-w-px relative shrink-0 pl-2">
|
||||||
|
<p className="[white-space-collapse:collapse] basis-0 font-medium grow leading-[1.6] min-h-px min-w-px not-italic overflow-ellipsis overflow-hidden relative shrink-0 text-[14px] text-[#404040] text-left text-nowrap">
|
||||||
|
로그/접속 기록
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
<button className="box-border content-stretch flex gap-2 h-[34px] items-center pl-2 pr-2 py-1 relative rounded-lg shrink-0 w-[226px] cursor-pointer hover:bg-gray-50 transition-colors">
|
||||||
|
<div className="basis-0 content-stretch flex gap-2 grow items-center min-h-px min-w-px relative shrink-0 pl-2">
|
||||||
|
<p className="[white-space-collapse:collapse] basis-0 font-medium grow leading-[1.6] min-h-px min-w-px not-italic overflow-ellipsis overflow-hidden relative shrink-0 text-[14px] text-[#404040] text-left text-nowrap">
|
||||||
|
배너 관리
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* 로그아웃 */}
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
localStorage.removeItem('isLoggedIn');
|
||||||
|
localStorage.removeItem('isAdminLoggedIn');
|
||||||
|
router.push('/login');
|
||||||
|
}}
|
||||||
|
className="content-stretch flex gap-[9px] h-[36px] items-center relative shrink-0 cursor-pointer hover:opacity-80 transition-opacity"
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-center relative shrink-0">
|
||||||
|
<div className="flex-none rotate-[180deg] scale-y-[-100%]">
|
||||||
|
<div className="h-[23.12px] relative w-[22px]">
|
||||||
|
<Logout />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="box-border content-stretch flex gap-[10px] items-center justify-center px-[10px] py-[5px] relative shrink-0">
|
||||||
|
<p className="font-medium leading-[1.6] not-italic relative shrink-0 text-[16px] text-[#404040] text-nowrap whitespace-pre">
|
||||||
|
로그아웃
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 메인 콘텐츠 */}
|
||||||
|
<div className="absolute left-[250px] top-0 right-0 min-h-screen">
|
||||||
|
{/* 헤더 */}
|
||||||
|
<div className="absolute content-stretch flex gap-[24px] items-center left-[48px] top-[45px]">
|
||||||
|
<div className="border-[#2b82e8] border-b-[2px] border-solid box-border content-stretch flex gap-[150px] items-center relative shrink-0">
|
||||||
|
<div className="box-border content-stretch flex gap-[10px] items-center justify-center p-[10px] relative shrink-0">
|
||||||
|
<p className="font-bold leading-[normal] not-italic relative shrink-0 text-[#515151] text-[18px] text-nowrap whitespace-pre">
|
||||||
|
강좌 관리
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 강좌 등록 버튼 */}
|
||||||
|
<button
|
||||||
|
className="absolute bg-[#160e0e] box-border content-stretch flex gap-[10px] items-center justify-center right-[101px] p-[10px] rounded-[10px] top-[72px] w-[167px] cursor-pointer hover:bg-[#2a1f1f] transition-colors"
|
||||||
|
>
|
||||||
|
<p className="font-bold leading-[1.6] not-italic relative shrink-0 text-[18px] text-nowrap text-white whitespace-pre">
|
||||||
|
강좌 등록
|
||||||
|
</p>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* 테이블 */}
|
||||||
|
<div className="absolute content-stretch flex flex-col items-start left-[48px] right-[101px] top-[186px] h-[671px] overflow-hidden">
|
||||||
|
{/* 테이블 헤더 */}
|
||||||
|
<div className="bg-[rgba(235,247,255,0.5)] content-stretch flex h-[41px] items-center relative shrink-0 w-full">
|
||||||
|
<div className="content-stretch flex items-center relative shrink-0 w-full">
|
||||||
|
<div className="basis-0 border-[0.5px_0px_0.5px_0.5px] border-[#b9b9b9] border-solid box-border content-stretch flex gap-[10px] grow items-center min-h-px min-w-px p-[10px] relative shrink-0">
|
||||||
|
<div className="basis-0 flex flex-col font-bold grow justify-center leading-[0] min-h-px min-w-px not-italic relative shrink-0 text-[18px] text-[#515151]">
|
||||||
|
<p className="leading-[normal]">교육 과정명</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="basis-0 border-[0.5px_0px_0.5px_0.5px] border-[#b9b9b9] border-solid box-border content-stretch flex gap-[10px] grow items-center min-h-px min-w-px p-[10px] relative shrink-0">
|
||||||
|
<div className="basis-0 flex flex-col font-bold grow justify-center leading-[0] min-h-px min-w-px not-italic relative shrink-0 text-[18px] text-[#515151]">
|
||||||
|
<p className="leading-[normal]">강좌명</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="basis-0 border-[0.5px_0px_0.5px_0.5px] border-[#b9b9b9] border-solid box-border content-stretch flex gap-[10px] grow items-center min-h-px min-w-px p-[10px] relative shrink-0">
|
||||||
|
<div className="basis-0 flex flex-col font-bold grow justify-center leading-[0] min-h-px min-w-px not-italic relative shrink-0 text-[18px] text-[#515151]">
|
||||||
|
<p className="leading-[normal]">첨부 파일</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="basis-0 border-[0.5px_0px_0.5px_0.5px] border-[#b9b9b9] border-solid box-border content-stretch flex gap-[10px] grow items-center min-h-px min-w-px p-[10px] relative shrink-0">
|
||||||
|
<div className="basis-0 flex flex-col font-bold grow justify-center leading-[0] min-h-px min-w-px not-italic relative shrink-0 text-[18px] text-[#515151]">
|
||||||
|
<p className="leading-[normal]">학습 평가 문제 수</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="basis-0 border-[0.5px_0px_0.5px_0.5px] border-[#b9b9b9] border-solid box-border content-stretch flex gap-[10px] grow items-center min-h-px min-w-px p-[10px] relative shrink-0">
|
||||||
|
<div className="basis-0 flex flex-col font-medium grow justify-center leading-[0] min-h-px min-w-px not-italic relative shrink-0 text-[18px] text-[#515151]">
|
||||||
|
<p className="leading-[normal]">등록자</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="basis-0 border-[0.5px_0px_0.5px_0.5px] border-[#b9b9b9] border-solid box-border content-stretch flex gap-[10px] grow items-center min-h-px min-w-px p-[10px] relative shrink-0">
|
||||||
|
<div className="basis-0 flex flex-col font-medium grow justify-center leading-[0] min-h-px min-w-px not-italic relative shrink-0 text-[18px] text-[#515151]">
|
||||||
|
<p className="leading-[normal]">등록일</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 테이블 바디 */}
|
||||||
|
{lectures.length === 0 ? (
|
||||||
|
<div className="relative w-full flex items-center justify-center" style={{ minHeight: '400px' }}>
|
||||||
|
<div className="content-stretch flex flex-col gap-[16px] items-center relative shrink-0">
|
||||||
|
<div className="content-stretch flex flex-col gap-[2px] items-center relative shrink-0">
|
||||||
|
<p className="font-medium leading-[1.6] not-italic relative shrink-0 text-[16px] text-[#404040] text-nowrap whitespace-pre">
|
||||||
|
교육 과정을 추가해 주세요.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="content-stretch flex flex-col items-start relative shrink-0 w-full max-h-[630px] overflow-y-auto">
|
||||||
|
{currentLectures.map((lecture) => (
|
||||||
|
<div key={lecture.id} className="bg-white content-stretch flex items-center relative shrink-0 w-full">
|
||||||
|
<div className="content-stretch flex items-center relative shrink-0 w-full">
|
||||||
|
<div className="basis-0 border-[0.5px_0px_0.5px_0.5px] border-[#b9b9b9] border-solid box-border content-stretch flex gap-[10px] grow items-center min-h-px min-w-px p-[10px] relative shrink-0">
|
||||||
|
<div className="basis-0 flex flex-col font-medium grow justify-center leading-[0] min-h-px min-w-px not-italic relative shrink-0 text-[18px] text-[#515151]">
|
||||||
|
<p className="leading-[normal]">{lecture.courseName}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="basis-0 border-[0.5px_0px_0.5px_0.5px] border-[#b9b9b9] border-solid box-border content-stretch flex gap-[10px] grow items-center min-h-px min-w-px p-[10px] relative shrink-0">
|
||||||
|
<div className="basis-0 flex flex-col font-medium grow justify-center leading-[0] min-h-px min-w-px not-italic relative shrink-0 text-[18px] text-[#515151]">
|
||||||
|
<p className="leading-[normal]">{lecture.lectureName}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="basis-0 border-[0.5px_0px_0.5px_0.5px] border-[#b9b9b9] border-solid box-border content-stretch flex gap-[10px] grow items-center min-h-px min-w-px p-[10px] relative shrink-0">
|
||||||
|
<div className="basis-0 flex flex-col font-medium grow justify-center leading-[0] min-h-px min-w-px not-italic relative shrink-0 text-[18px] text-[#515151]">
|
||||||
|
<p className="leading-[normal]">{lecture.attachedFile}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="basis-0 border-[0.5px_0px_0.5px_0.5px] border-[#b9b9b9] border-solid box-border content-stretch flex gap-[10px] grow items-center min-h-px min-w-px p-[10px] relative shrink-0">
|
||||||
|
<div className="basis-0 flex flex-col font-medium grow justify-center leading-[0] min-h-px min-w-px not-italic relative shrink-0 text-[18px] text-[#515151]">
|
||||||
|
<p className="leading-[normal]">{lecture.questionCount}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="basis-0 border-[0.5px_0px_0.5px_0.5px] border-[#b9b9b9] border-solid box-border content-stretch flex gap-[10px] grow items-center min-h-px min-w-px p-[10px] relative shrink-0">
|
||||||
|
<div className="basis-0 flex flex-col font-medium grow justify-center leading-[0] min-h-px min-w-px not-italic relative shrink-0 text-[18px] text-[#515151]">
|
||||||
|
<p className="leading-[normal]">{lecture.registrar}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="basis-0 border-[0.5px_0px_0.5px_0.5px] border-[#b9b9b9] border-solid box-border content-stretch flex gap-[10px] grow items-center min-h-px min-w-px p-[10px] relative shrink-0">
|
||||||
|
<div className="basis-0 flex flex-col font-medium grow justify-center leading-[0] min-h-px min-w-px not-italic relative shrink-0 text-[18px] text-[#515151]">
|
||||||
|
<p className="leading-[normal]">{lecture.createdAt}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 페이지네이션 */}
|
||||||
|
{lectures.length > 0 && (
|
||||||
|
<div className="absolute content-stretch flex gap-2 items-center justify-center left-[48px] right-[101px] top-[909px]">
|
||||||
|
<button onClick={() => setCurrentPage(Math.max(1, currentPage - 1))}>
|
||||||
|
<PaginationBtnMove status={currentPage === 1 ? "Disabled" : "Default"} move="Previous" />
|
||||||
|
</button>
|
||||||
|
<div className="content-stretch flex gap-2 items-center relative shrink-0">
|
||||||
|
{pageNumbers.map((pageNum) => (
|
||||||
|
<button
|
||||||
|
key={pageNum}
|
||||||
|
onClick={() => setCurrentPage(pageNum)}
|
||||||
|
className={`relative rounded-[8px] shrink-0 size-[30px] cursor-pointer transition-colors ${currentPage === pageNum
|
||||||
|
? 'bg-[#1d1d1d]'
|
||||||
|
: 'border border-[#eeeeee] border-solid hover:bg-gray-50'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="overflow-clip relative rounded-[inherit] size-[30px]">
|
||||||
|
<div className={`absolute flex flex-col font-medium justify-center leading-[0] left-[15.5px] not-italic text-[14px] text-center text-nowrap top-[15px] translate-x-[-50%] translate-y-[-50%] ${currentPage === pageNum ? 'text-white' : 'text-[#515151]'
|
||||||
|
}`}>
|
||||||
|
<p className="leading-[1.6] whitespace-pre">{pageNum}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<button onClick={() => setCurrentPage(Math.min(totalPages, currentPage + 1))}>
|
||||||
|
<PaginationBtnMove status={currentPage === totalPages ? "Disabled" : "Default"} move="Next" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
182
app/announcement/page.tsx
Normal file
182
app/announcement/page.tsx
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import Image from 'next/image';
|
||||||
|
import Header from '../components/Header';
|
||||||
|
import logo from '../logo.svg';
|
||||||
|
|
||||||
|
const imgLine2 = "http://localhost:3845/assets/6ee8cf4ebb6bc2adb14aab8c9940b3002c20af35.svg";
|
||||||
|
const imgFooterLogo = "http://localhost:3845/assets/89fda8e949171025b1232bae70fc9d442e4e70c8.png";
|
||||||
|
|
||||||
|
export default function AnnouncementPage() {
|
||||||
|
const router = useRouter();
|
||||||
|
const [isLoggedIn, setIsLoggedIn] = useState(false);
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const [announcements, setAnnouncements] = useState<any[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// 로그인 상태 확인
|
||||||
|
const loginStatus = localStorage.getItem('isLoggedIn') === 'true';
|
||||||
|
setIsLoggedIn(loginStatus);
|
||||||
|
setIsLoading(false);
|
||||||
|
|
||||||
|
// TODO: DB에서 공지사항 목록 가져오기
|
||||||
|
// const fetchAnnouncements = async () => {
|
||||||
|
// const response = await fetch('/api/announcements');
|
||||||
|
// const data = await response.json();
|
||||||
|
// setAnnouncements(data);
|
||||||
|
// };
|
||||||
|
// fetchAnnouncements();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return null; // 로딩 중
|
||||||
|
}
|
||||||
|
|
||||||
|
// 로그인되지 않았으면 로그인 페이지로 리다이렉트
|
||||||
|
if (!isLoggedIn) {
|
||||||
|
router.push('/login');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-white relative min-h-screen w-full">
|
||||||
|
{/* 헤더 */}
|
||||||
|
<Header activePage="announcement" />
|
||||||
|
|
||||||
|
{/* 구분선 */}
|
||||||
|
<div className="absolute h-0 left-1/2 top-[150px] translate-x-[-50%] w-[1920px]">
|
||||||
|
<div className="absolute bottom-0 left-0 right-0 top-[-1px]">
|
||||||
|
<img alt="" className="block max-w-none size-full" src={imgLine2} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 페이지 제목 */}
|
||||||
|
<p className="absolute font-bold leading-[normal] left-[calc(18.75%-11.44px)] not-italic text-[#515151] text-[24px] text-nowrap top-[239px] whitespace-pre">
|
||||||
|
공지사항 ({announcements.length}건)
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* 테이블 컨테이너 */}
|
||||||
|
<div className="absolute content-stretch flex flex-col items-start left-[calc(18.75%-7.44px)] top-[344px] w-[1330px]">
|
||||||
|
{/* 테이블 헤더 */}
|
||||||
|
<div className="bg-[rgba(235,247,255,0.5)] content-stretch flex h-[41px] items-center relative shrink-0 w-full">
|
||||||
|
<div className="content-stretch flex items-center relative shrink-0 w-full">
|
||||||
|
<div className="basis-0 border-[0.5px] border-[#b9b9b9] border-solid box-border content-stretch flex gap-[10px] grow items-center min-h-px min-w-px p-[10px] relative shrink-0">
|
||||||
|
<div className="basis-0 flex flex-col font-bold grow justify-center leading-[0] min-h-px min-w-px not-italic relative shrink-0 text-[18px] text-[#515151]">
|
||||||
|
<p className="leading-[normal]">번호</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="basis-0 border-[0.5px] border-[#b9b9b9] border-solid box-border content-stretch flex gap-[10px] grow items-center min-h-px min-w-px p-[10px] relative shrink-0">
|
||||||
|
<div className="basis-0 flex flex-col font-bold grow justify-center leading-[0] min-h-px min-w-px not-italic relative shrink-0 text-[18px] text-[#515151]">
|
||||||
|
<p className="leading-[normal]">제목</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="basis-0 border-[0.5px] border-[#b9b9b9] border-solid box-border content-stretch flex gap-[10px] grow items-center min-h-px min-w-px p-[10px] relative shrink-0">
|
||||||
|
<div className="basis-0 flex flex-col font-bold grow justify-center leading-[0] min-h-px min-w-px not-italic relative shrink-0 text-[18px] text-[#515151]">
|
||||||
|
<p className="leading-[normal]">게시일</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="basis-0 border-[0.5px] border-[#b9b9b9] border-solid box-border content-stretch flex gap-[10px] grow items-center min-h-px min-w-px p-[10px] relative shrink-0">
|
||||||
|
<div className="basis-0 flex flex-col font-bold grow justify-center leading-[0] min-h-px min-w-px not-italic relative shrink-0 text-[18px] text-[#515151]">
|
||||||
|
<p className="leading-[normal]">작성자</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="basis-0 border-[0.5px] border-[#b9b9b9] border-solid box-border content-stretch flex gap-[10px] grow items-center min-h-px min-w-px p-[10px] relative shrink-0">
|
||||||
|
<div className="basis-0 flex flex-col font-bold grow justify-center leading-[0] min-h-px min-w-px not-italic relative shrink-0 text-[18px] text-[#515151]">
|
||||||
|
<p className="leading-[normal]">조회수</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 테이블 내용 */}
|
||||||
|
{announcements.length > 0 ? (
|
||||||
|
<div className="content-stretch flex flex-col items-start relative shrink-0 w-full">
|
||||||
|
{announcements.map((announcement) => (
|
||||||
|
<div key={announcement.id} className="content-stretch flex items-center relative shrink-0 w-full border-[0.5px] border-[#b9b9b9] border-solid">
|
||||||
|
<div className="basis-0 border-[0.5px] border-[#b9b9b9] border-solid box-border content-stretch flex gap-[10px] grow items-center min-h-px min-w-px p-[10px] relative shrink-0">
|
||||||
|
<p className="font-medium leading-[normal] not-italic relative shrink-0 text-[#515151] text-[18px]">
|
||||||
|
{announcement.id}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="basis-0 border-[0.5px] border-[#b9b9b9] border-solid box-border content-stretch flex gap-[10px] grow items-center min-h-px min-w-px p-[10px] relative shrink-0">
|
||||||
|
<p className="font-medium leading-[normal] not-italic relative shrink-0 text-[#515151] text-[18px]">
|
||||||
|
{announcement.title}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="basis-0 border-[0.5px] border-[#b9b9b9] border-solid box-border content-stretch flex gap-[10px] grow items-center min-h-px min-w-px p-[10px] relative shrink-0">
|
||||||
|
<p className="font-medium leading-[normal] not-italic relative shrink-0 text-[#515151] text-[18px]">
|
||||||
|
{announcement.createdAt ? new Date(announcement.createdAt).toLocaleDateString('ko-KR') : '-'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="basis-0 border-[0.5px] border-[#b9b9b9] border-solid box-border content-stretch flex gap-[10px] grow items-center min-h-px min-w-px p-[10px] relative shrink-0">
|
||||||
|
<p className="font-medium leading-[normal] not-italic relative shrink-0 text-[#515151] text-[18px]">
|
||||||
|
{announcement.author || '-'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="basis-0 border-[0.5px] border-[#b9b9b9] border-solid box-border content-stretch flex gap-[10px] grow items-center min-h-px min-w-px p-[10px] relative shrink-0">
|
||||||
|
<p className="font-medium leading-[normal] not-italic relative shrink-0 text-[#515151] text-[18px]">
|
||||||
|
{announcement.views || 0}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="content-stretch flex items-center justify-center relative shrink-0 w-full py-[40px] border-[0.5px] border-[#b9b9b9] border-solid">
|
||||||
|
<p className="font-medium leading-[normal] not-italic relative shrink-0 text-[#b9b9b9] text-[18px]">
|
||||||
|
등록된 공지사항이 없습니다.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 푸터 */}
|
||||||
|
<footer className="absolute bg-[#f7f7f7] box-border content-stretch flex flex-col gap-[10px] h-[225px] items-start left-0 px-[243px] py-[39px] top-[855px] w-[1920px]">
|
||||||
|
<div className="content-stretch flex gap-[49px] items-center relative shrink-0">
|
||||||
|
<div className="h-[74px] relative shrink-0 w-[72px]">
|
||||||
|
<Image
|
||||||
|
src={logo}
|
||||||
|
alt="로고"
|
||||||
|
className="w-full h-full object-contain"
|
||||||
|
width={72}
|
||||||
|
height={74}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="content-stretch flex flex-col gap-[5px] items-start relative shrink-0 w-[479px]">
|
||||||
|
<div className="content-stretch flex gap-[27px] items-center relative shrink-0 w-full">
|
||||||
|
<div className="box-border content-stretch flex gap-[10px] items-center justify-center p-[10px] relative shrink-0">
|
||||||
|
<p className="font-bold leading-[normal] not-italic relative shrink-0 text-[#515151] text-[18px] text-nowrap whitespace-pre">
|
||||||
|
이용 약관
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="box-border content-stretch flex gap-[10px] items-center justify-center p-[10px] relative shrink-0">
|
||||||
|
<p className="font-bold leading-[normal] not-italic relative shrink-0 text-[#515151] text-[18px] text-nowrap whitespace-pre">
|
||||||
|
개인정보처리방침
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="box-border content-stretch flex gap-[10px] items-center justify-center p-[10px] relative shrink-0">
|
||||||
|
<p className="font-bold leading-[normal] not-italic relative shrink-0 text-[#515151] text-[18px] text-nowrap whitespace-pre">
|
||||||
|
고객센터
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="box-border content-stretch flex flex-col font-medium gap-[10px] items-center justify-center leading-[normal] not-italic p-[10px] relative shrink-0 text-[#515151] text-[16px]">
|
||||||
|
<p className="relative shrink-0 w-[400px]">
|
||||||
|
(12345) 서울특별시 광진구 구의동 123-12(구의타워1)
|
||||||
|
</p>
|
||||||
|
<p className="relative shrink-0 w-[400px]">
|
||||||
|
문의: 1234-1234 (평일 09:00 ~ 18:00)
|
||||||
|
</p>
|
||||||
|
<p className="relative shrink-0 w-[400px]">
|
||||||
|
이메일: qwer1234@go.or.kr
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
124
app/api/curriculums/[id]/route.ts
Normal file
124
app/api/curriculums/[id]/route.ts
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
import { prisma } from '@/lib/prisma';
|
||||||
|
|
||||||
|
// GET: 특정 교육 과정 조회
|
||||||
|
export async function GET(
|
||||||
|
request: NextRequest,
|
||||||
|
{ params }: { params: { id: string } }
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const curriculum = await prisma.curriculum.findUnique({
|
||||||
|
where: { id: params.id },
|
||||||
|
include: {
|
||||||
|
lectures: {
|
||||||
|
orderBy: {
|
||||||
|
registeredAt: 'desc',
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
registrant: {
|
||||||
|
select: {
|
||||||
|
name: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!curriculum) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: '교육 과정을 찾을 수 없습니다.' },
|
||||||
|
{ status: 404 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
data: {
|
||||||
|
id: curriculum.id,
|
||||||
|
courseName: curriculum.title,
|
||||||
|
instructorId: curriculum.instructorId,
|
||||||
|
thumbnailImage: curriculum.thumbnailImage,
|
||||||
|
createdAt: curriculum.createdAt.toISOString().split('T')[0],
|
||||||
|
lectures: curriculum.lectures.map((lecture) => ({
|
||||||
|
id: lecture.id,
|
||||||
|
lectureName: lecture.title,
|
||||||
|
attachedFile: lecture.attachmentFile || '없음',
|
||||||
|
questionCount: lecture.evaluationQuestionCount,
|
||||||
|
registrar: lecture.registrant.name || '알 수 없음',
|
||||||
|
createdAt: lecture.registeredAt.toISOString().split('T')[0],
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching curriculum:', error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: '교육 과정을 불러오는 중 오류가 발생했습니다.' },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PUT: 교육 과정 수정
|
||||||
|
export async function PUT(
|
||||||
|
request: NextRequest,
|
||||||
|
{ params }: { params: { id: string } }
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const body = await request.json();
|
||||||
|
const { title, instructorId, thumbnailImage } = body;
|
||||||
|
|
||||||
|
const curriculum = await prisma.curriculum.update({
|
||||||
|
where: { id: params.id },
|
||||||
|
data: {
|
||||||
|
...(title && { title }),
|
||||||
|
...(instructorId && { instructorId }),
|
||||||
|
...(thumbnailImage !== undefined && { thumbnailImage }),
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
lectures: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
data: {
|
||||||
|
id: curriculum.id,
|
||||||
|
courseName: curriculum.title,
|
||||||
|
instructorId: curriculum.instructorId,
|
||||||
|
thumbnailImage: curriculum.thumbnailImage,
|
||||||
|
createdAt: curriculum.createdAt.toISOString().split('T')[0],
|
||||||
|
lectureCount: curriculum.lectures.length,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error updating curriculum:', error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: '교육 과정 수정 중 오류가 발생했습니다.' },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DELETE: 교육 과정 삭제
|
||||||
|
export async function DELETE(
|
||||||
|
request: NextRequest,
|
||||||
|
{ params }: { params: { id: string } }
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
await prisma.curriculum.delete({
|
||||||
|
where: { id: params.id },
|
||||||
|
});
|
||||||
|
|
||||||
|
return NextResponse.json({ message: '교육 과정이 삭제되었습니다.' });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error deleting curriculum:', error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: '교육 과정 삭제 중 오류가 발생했습니다.' },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
113
app/api/curriculums/route.ts
Normal file
113
app/api/curriculums/route.ts
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
import { prisma } from '@/lib/prisma';
|
||||||
|
|
||||||
|
// GET: 교육 과정 목록 조회
|
||||||
|
export async function GET(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const searchParams = request.nextUrl.searchParams;
|
||||||
|
const page = parseInt(searchParams.get('page') || '1');
|
||||||
|
const limit = parseInt(searchParams.get('limit') || '13');
|
||||||
|
const skip = (page - 1) * limit;
|
||||||
|
|
||||||
|
const [curriculums, total] = await Promise.all([
|
||||||
|
prisma.curriculum.findMany({
|
||||||
|
skip,
|
||||||
|
take: limit,
|
||||||
|
orderBy: {
|
||||||
|
createdAt: 'desc',
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
lectures: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
prisma.curriculum.count(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
data: curriculums.map((curriculum) => ({
|
||||||
|
id: curriculum.id,
|
||||||
|
courseName: curriculum.title,
|
||||||
|
instructorId: curriculum.instructorId,
|
||||||
|
thumbnailImage: curriculum.thumbnailImage,
|
||||||
|
createdAt: curriculum.createdAt.toISOString().split('T')[0],
|
||||||
|
lectureCount: curriculum.lectures.length,
|
||||||
|
})),
|
||||||
|
pagination: {
|
||||||
|
page,
|
||||||
|
limit,
|
||||||
|
total,
|
||||||
|
totalPages: Math.ceil(total / limit),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching curriculums:', error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: '교육 과정 목록을 불러오는 중 오류가 발생했습니다.' },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST: 교육 과정 생성
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const body = await request.json();
|
||||||
|
const { title, instructorId, thumbnailImage } = body;
|
||||||
|
|
||||||
|
if (!title || !instructorId) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: '교육 과정명과 강사 ID는 필수입니다.' },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 강사 존재 확인
|
||||||
|
const instructor = await prisma.user.findUnique({
|
||||||
|
where: { id: instructorId },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!instructor || instructor.role !== 'INSTRUCTOR') {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: '유효한 강사를 선택해주세요.' },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const curriculum = await prisma.curriculum.create({
|
||||||
|
data: {
|
||||||
|
title,
|
||||||
|
instructorId,
|
||||||
|
thumbnailImage: thumbnailImage || null,
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
lectures: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
data: {
|
||||||
|
id: curriculum.id,
|
||||||
|
courseName: curriculum.title,
|
||||||
|
instructorId: curriculum.instructorId,
|
||||||
|
thumbnailImage: curriculum.thumbnailImage,
|
||||||
|
createdAt: curriculum.createdAt.toISOString().split('T')[0],
|
||||||
|
lectureCount: curriculum.lectures.length,
|
||||||
|
},
|
||||||
|
}, { status: 201 });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error creating curriculum:', error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: '교육 과정 생성 중 오류가 발생했습니다.' },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
127
app/api/lectures/[id]/route.ts
Normal file
127
app/api/lectures/[id]/route.ts
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
import { prisma } from '@/lib/prisma';
|
||||||
|
|
||||||
|
// GET: 특정 강좌 조회
|
||||||
|
export async function GET(
|
||||||
|
request: NextRequest,
|
||||||
|
{ params }: { params: { id: string } }
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const lecture = await prisma.lecture.findUnique({
|
||||||
|
where: { id: params.id },
|
||||||
|
include: {
|
||||||
|
curriculum: {
|
||||||
|
select: {
|
||||||
|
title: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
registrant: {
|
||||||
|
select: {
|
||||||
|
name: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!lecture) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: '강좌를 찾을 수 없습니다.' },
|
||||||
|
{ status: 404 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
data: {
|
||||||
|
id: lecture.id,
|
||||||
|
courseName: lecture.curriculum.title,
|
||||||
|
lectureName: lecture.title,
|
||||||
|
attachedFile: lecture.attachmentFile || '없음',
|
||||||
|
questionCount: lecture.evaluationQuestionCount,
|
||||||
|
registrar: lecture.registrant.name || '알 수 없음',
|
||||||
|
createdAt: lecture.registeredAt.toISOString().split('T')[0],
|
||||||
|
curriculumId: lecture.curriculumId,
|
||||||
|
thumbnailImage: lecture.thumbnailImage,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching lecture:', error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: '강좌를 불러오는 중 오류가 발생했습니다.' },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PUT: 강좌 수정
|
||||||
|
export async function PUT(
|
||||||
|
request: NextRequest,
|
||||||
|
{ params }: { params: { id: string } }
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const body = await request.json();
|
||||||
|
const { title, attachmentFile, evaluationQuestionCount, thumbnailImage } = body;
|
||||||
|
|
||||||
|
const lecture = await prisma.lecture.update({
|
||||||
|
where: { id: params.id },
|
||||||
|
data: {
|
||||||
|
...(title && { title }),
|
||||||
|
...(attachmentFile !== undefined && { attachmentFile }),
|
||||||
|
...(evaluationQuestionCount !== undefined && { evaluationQuestionCount }),
|
||||||
|
...(thumbnailImage !== undefined && { thumbnailImage }),
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
curriculum: {
|
||||||
|
select: {
|
||||||
|
title: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
registrant: {
|
||||||
|
select: {
|
||||||
|
name: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
data: {
|
||||||
|
id: lecture.id,
|
||||||
|
courseName: lecture.curriculum.title,
|
||||||
|
lectureName: lecture.title,
|
||||||
|
attachedFile: lecture.attachmentFile || '없음',
|
||||||
|
questionCount: lecture.evaluationQuestionCount,
|
||||||
|
registrar: lecture.registrant.name || '알 수 없음',
|
||||||
|
createdAt: lecture.registeredAt.toISOString().split('T')[0],
|
||||||
|
curriculumId: lecture.curriculumId,
|
||||||
|
thumbnailImage: lecture.thumbnailImage,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error updating lecture:', error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: '강좌 수정 중 오류가 발생했습니다.' },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DELETE: 강좌 삭제
|
||||||
|
export async function DELETE(
|
||||||
|
request: NextRequest,
|
||||||
|
{ params }: { params: { id: string } }
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
await prisma.lecture.delete({
|
||||||
|
where: { id: params.id },
|
||||||
|
});
|
||||||
|
|
||||||
|
return NextResponse.json({ message: '강좌가 삭제되었습니다.' });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error deleting lecture:', error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: '강좌 삭제 중 오류가 발생했습니다.' },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
148
app/api/lectures/route.ts
Normal file
148
app/api/lectures/route.ts
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
import { prisma } from '@/lib/prisma';
|
||||||
|
|
||||||
|
// GET: 강좌 목록 조회
|
||||||
|
export async function GET(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const searchParams = request.nextUrl.searchParams;
|
||||||
|
const page = parseInt(searchParams.get('page') || '1');
|
||||||
|
const limit = parseInt(searchParams.get('limit') || '13');
|
||||||
|
const curriculumId = searchParams.get('curriculumId');
|
||||||
|
const skip = (page - 1) * limit;
|
||||||
|
|
||||||
|
const where = curriculumId ? { curriculumId } : {};
|
||||||
|
|
||||||
|
const [lectures, total] = await Promise.all([
|
||||||
|
prisma.lecture.findMany({
|
||||||
|
where,
|
||||||
|
skip,
|
||||||
|
take: limit,
|
||||||
|
orderBy: {
|
||||||
|
registeredAt: 'desc',
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
curriculum: {
|
||||||
|
select: {
|
||||||
|
title: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
registrant: {
|
||||||
|
select: {
|
||||||
|
name: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
prisma.lecture.count({ where }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
data: lectures.map((lecture) => ({
|
||||||
|
id: lecture.id,
|
||||||
|
courseName: lecture.curriculum.title,
|
||||||
|
lectureName: lecture.title,
|
||||||
|
attachedFile: lecture.attachmentFile || '없음',
|
||||||
|
questionCount: lecture.evaluationQuestionCount,
|
||||||
|
registrar: lecture.registrant.name || '알 수 없음',
|
||||||
|
createdAt: lecture.registeredAt.toISOString().split('T')[0],
|
||||||
|
curriculumId: lecture.curriculumId,
|
||||||
|
thumbnailImage: lecture.thumbnailImage,
|
||||||
|
})),
|
||||||
|
pagination: {
|
||||||
|
page,
|
||||||
|
limit,
|
||||||
|
total,
|
||||||
|
totalPages: Math.ceil(total / limit),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching lectures:', error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: '강좌 목록을 불러오는 중 오류가 발생했습니다.' },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST: 강좌 생성
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const body = await request.json();
|
||||||
|
const { title, curriculumId, registrantId, attachmentFile, evaluationQuestionCount, thumbnailImage } = body;
|
||||||
|
|
||||||
|
if (!title || !curriculumId || !registrantId) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: '강좌명, 교육 과정 ID, 등록자 ID는 필수입니다.' },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 교육 과정 존재 확인
|
||||||
|
const curriculum = await prisma.curriculum.findUnique({
|
||||||
|
where: { id: curriculumId },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!curriculum) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: '유효한 교육 과정을 선택해주세요.' },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 등록자 존재 확인
|
||||||
|
const registrant = await prisma.user.findUnique({
|
||||||
|
where: { id: registrantId },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!registrant) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: '유효한 등록자를 선택해주세요.' },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const lecture = await prisma.lecture.create({
|
||||||
|
data: {
|
||||||
|
title,
|
||||||
|
curriculumId,
|
||||||
|
registrantId,
|
||||||
|
attachmentFile: attachmentFile || null,
|
||||||
|
evaluationQuestionCount: evaluationQuestionCount || 0,
|
||||||
|
thumbnailImage: thumbnailImage || null,
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
curriculum: {
|
||||||
|
select: {
|
||||||
|
title: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
registrant: {
|
||||||
|
select: {
|
||||||
|
name: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
data: {
|
||||||
|
id: lecture.id,
|
||||||
|
courseName: lecture.curriculum.title,
|
||||||
|
lectureName: lecture.title,
|
||||||
|
attachedFile: lecture.attachmentFile || '없음',
|
||||||
|
questionCount: lecture.evaluationQuestionCount,
|
||||||
|
registrar: lecture.registrant.name || '알 수 없음',
|
||||||
|
createdAt: lecture.registeredAt.toISOString().split('T')[0],
|
||||||
|
curriculumId: lecture.curriculumId,
|
||||||
|
thumbnailImage: lecture.thumbnailImage,
|
||||||
|
},
|
||||||
|
}, { status: 201 });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error creating lecture:', error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: '강좌 생성 중 오류가 발생했습니다.' },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
152
app/api/user-lectures/[id]/route.ts
Normal file
152
app/api/user-lectures/[id]/route.ts
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
import { prisma } from '@/lib/prisma';
|
||||||
|
|
||||||
|
// GET: 특정 수강 관계 조회
|
||||||
|
export async function GET(
|
||||||
|
request: NextRequest,
|
||||||
|
{ params }: { params: { id: string } }
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const userLecture = await prisma.userLecture.findUnique({
|
||||||
|
where: { id: params.id },
|
||||||
|
include: {
|
||||||
|
user: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
email: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
lecture: {
|
||||||
|
include: {
|
||||||
|
curriculum: {
|
||||||
|
select: {
|
||||||
|
title: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!userLecture) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: '수강 관계를 찾을 수 없습니다.' },
|
||||||
|
{ status: 404 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
data: {
|
||||||
|
id: userLecture.id,
|
||||||
|
userId: userLecture.userId,
|
||||||
|
userName: userLecture.user.name,
|
||||||
|
userEmail: userLecture.user.email,
|
||||||
|
lectureId: userLecture.lectureId,
|
||||||
|
lectureName: userLecture.lecture.title,
|
||||||
|
courseName: userLecture.lecture.curriculum.title,
|
||||||
|
enrolledAt: userLecture.enrolledAt.toISOString().split('T')[0],
|
||||||
|
completedAt: userLecture.completedAt ? userLecture.completedAt.toISOString().split('T')[0] : null,
|
||||||
|
isCompleted: userLecture.isCompleted,
|
||||||
|
progress: userLecture.progress,
|
||||||
|
score: userLecture.score,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching user lecture:', error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: '수강 관계를 불러오는 중 오류가 발생했습니다.' },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PUT: 수강 관계 수정 (진행률, 완료 여부, 점수 등)
|
||||||
|
export async function PUT(
|
||||||
|
request: NextRequest,
|
||||||
|
{ params }: { params: { id: string } }
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const body = await request.json();
|
||||||
|
const { progress, isCompleted, score } = body;
|
||||||
|
|
||||||
|
const updateData: any = {};
|
||||||
|
if (progress !== undefined) updateData.progress = Math.max(0, Math.min(100, progress));
|
||||||
|
if (isCompleted !== undefined) {
|
||||||
|
updateData.isCompleted = isCompleted;
|
||||||
|
if (isCompleted && !body.completedAt) {
|
||||||
|
updateData.completedAt = new Date();
|
||||||
|
} else if (!isCompleted) {
|
||||||
|
updateData.completedAt = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (score !== undefined) updateData.score = score;
|
||||||
|
|
||||||
|
const userLecture = await prisma.userLecture.update({
|
||||||
|
where: { id: params.id },
|
||||||
|
data: updateData,
|
||||||
|
include: {
|
||||||
|
user: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
email: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
lecture: {
|
||||||
|
include: {
|
||||||
|
curriculum: {
|
||||||
|
select: {
|
||||||
|
title: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
data: {
|
||||||
|
id: userLecture.id,
|
||||||
|
userId: userLecture.userId,
|
||||||
|
userName: userLecture.user.name,
|
||||||
|
userEmail: userLecture.user.email,
|
||||||
|
lectureId: userLecture.lectureId,
|
||||||
|
lectureName: userLecture.lecture.title,
|
||||||
|
courseName: userLecture.lecture.curriculum.title,
|
||||||
|
enrolledAt: userLecture.enrolledAt.toISOString().split('T')[0],
|
||||||
|
completedAt: userLecture.completedAt ? userLecture.completedAt.toISOString().split('T')[0] : null,
|
||||||
|
isCompleted: userLecture.isCompleted,
|
||||||
|
progress: userLecture.progress,
|
||||||
|
score: userLecture.score,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error updating user lecture:', error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: '수강 관계 수정 중 오류가 발생했습니다.' },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DELETE: 수강 관계 삭제 (수강 취소)
|
||||||
|
export async function DELETE(
|
||||||
|
request: NextRequest,
|
||||||
|
{ params }: { params: { id: string } }
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
await prisma.userLecture.delete({
|
||||||
|
where: { id: params.id },
|
||||||
|
});
|
||||||
|
|
||||||
|
return NextResponse.json({ message: '수강이 취소되었습니다.' });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error deleting user lecture:', error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: '수강 취소 중 오류가 발생했습니다.' },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
182
app/api/user-lectures/route.ts
Normal file
182
app/api/user-lectures/route.ts
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
import { prisma } from '@/lib/prisma';
|
||||||
|
|
||||||
|
// GET: 사용자-강좌 수강 관계 목록 조회
|
||||||
|
export async function GET(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const searchParams = request.nextUrl.searchParams;
|
||||||
|
const userId = searchParams.get('userId');
|
||||||
|
const lectureId = searchParams.get('lectureId');
|
||||||
|
const page = parseInt(searchParams.get('page') || '1');
|
||||||
|
const limit = parseInt(searchParams.get('limit') || '100');
|
||||||
|
const skip = (page - 1) * limit;
|
||||||
|
|
||||||
|
const where: any = {};
|
||||||
|
if (userId) where.userId = userId;
|
||||||
|
if (lectureId) where.lectureId = lectureId;
|
||||||
|
|
||||||
|
const [userLectures, total] = await Promise.all([
|
||||||
|
prisma.userLecture.findMany({
|
||||||
|
where,
|
||||||
|
skip,
|
||||||
|
take: limit,
|
||||||
|
orderBy: {
|
||||||
|
enrolledAt: 'desc',
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
user: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
email: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
lecture: {
|
||||||
|
include: {
|
||||||
|
curriculum: {
|
||||||
|
select: {
|
||||||
|
title: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
prisma.userLecture.count({ where }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
data: userLectures.map((ul) => ({
|
||||||
|
id: ul.id,
|
||||||
|
userId: ul.userId,
|
||||||
|
userName: ul.user.name,
|
||||||
|
userEmail: ul.user.email,
|
||||||
|
lectureId: ul.lectureId,
|
||||||
|
lectureName: ul.lecture.title,
|
||||||
|
courseName: ul.lecture.curriculum.title,
|
||||||
|
enrolledAt: ul.enrolledAt.toISOString().split('T')[0],
|
||||||
|
completedAt: ul.completedAt ? ul.completedAt.toISOString().split('T')[0] : null,
|
||||||
|
isCompleted: ul.isCompleted,
|
||||||
|
progress: ul.progress,
|
||||||
|
score: ul.score,
|
||||||
|
})),
|
||||||
|
pagination: {
|
||||||
|
page,
|
||||||
|
limit,
|
||||||
|
total,
|
||||||
|
totalPages: Math.ceil(total / limit),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching user lectures:', error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: '수강 관계 목록을 불러오는 중 오류가 발생했습니다.' },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST: 사용자-강좌 수강 관계 생성 (수강 신청)
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const body = await request.json();
|
||||||
|
const { userId, lectureId } = body;
|
||||||
|
|
||||||
|
if (!userId || !lectureId) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: '사용자 ID와 강좌 ID는 필수입니다.' },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 사용자 존재 확인
|
||||||
|
const user = await prisma.user.findUnique({
|
||||||
|
where: { id: userId },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: '유효한 사용자를 선택해주세요.' },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 강좌 존재 확인
|
||||||
|
const lecture = await prisma.lecture.findUnique({
|
||||||
|
where: { id: lectureId },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!lecture) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: '유효한 강좌를 선택해주세요.' },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 이미 수강 중인지 확인
|
||||||
|
const existing = await prisma.userLecture.findUnique({
|
||||||
|
where: {
|
||||||
|
userId_lectureId: {
|
||||||
|
userId,
|
||||||
|
lectureId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: '이미 수강 중인 강좌입니다.' },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const userLecture = await prisma.userLecture.create({
|
||||||
|
data: {
|
||||||
|
userId,
|
||||||
|
lectureId,
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
user: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
email: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
lecture: {
|
||||||
|
include: {
|
||||||
|
curriculum: {
|
||||||
|
select: {
|
||||||
|
title: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
data: {
|
||||||
|
id: userLecture.id,
|
||||||
|
userId: userLecture.userId,
|
||||||
|
userName: userLecture.user.name,
|
||||||
|
userEmail: userLecture.user.email,
|
||||||
|
lectureId: userLecture.lectureId,
|
||||||
|
lectureName: userLecture.lecture.title,
|
||||||
|
courseName: userLecture.lecture.curriculum.title,
|
||||||
|
enrolledAt: userLecture.enrolledAt.toISOString().split('T')[0],
|
||||||
|
completedAt: userLecture.completedAt ? userLecture.completedAt.toISOString().split('T')[0] : null,
|
||||||
|
isCompleted: userLecture.isCompleted,
|
||||||
|
progress: userLecture.progress,
|
||||||
|
score: userLecture.score,
|
||||||
|
},
|
||||||
|
}, { status: 201 });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error creating user lecture:', error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: '수강 신청 중 오류가 발생했습니다.' },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
59
app/api/users/route.ts
Normal file
59
app/api/users/route.ts
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
import { prisma } from '@/lib/prisma';
|
||||||
|
|
||||||
|
// GET: 사용자 목록 조회 (강사 목록 포함)
|
||||||
|
export async function GET(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const searchParams = request.nextUrl.searchParams;
|
||||||
|
const role = searchParams.get('role'); // 'INSTRUCTOR', 'ADMIN', 'STUDENT' 등
|
||||||
|
const page = parseInt(searchParams.get('page') || '1');
|
||||||
|
const limit = parseInt(searchParams.get('limit') || '100');
|
||||||
|
const skip = (page - 1) * limit;
|
||||||
|
|
||||||
|
const where = role ? { role: role as any } : {};
|
||||||
|
|
||||||
|
const [users, total] = await Promise.all([
|
||||||
|
prisma.user.findMany({
|
||||||
|
where,
|
||||||
|
skip,
|
||||||
|
take: limit,
|
||||||
|
orderBy: {
|
||||||
|
createdAt: 'desc',
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
email: true,
|
||||||
|
name: true,
|
||||||
|
role: true,
|
||||||
|
isActive: true,
|
||||||
|
createdAt: true,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
prisma.user.count({ where }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
data: users.map((user) => ({
|
||||||
|
id: user.id,
|
||||||
|
email: user.email,
|
||||||
|
name: user.name,
|
||||||
|
role: user.role,
|
||||||
|
isActive: user.isActive,
|
||||||
|
createdAt: user.createdAt.toISOString().split('T')[0],
|
||||||
|
})),
|
||||||
|
pagination: {
|
||||||
|
page,
|
||||||
|
limit,
|
||||||
|
total,
|
||||||
|
totalPages: Math.ceil(total / limit),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching users:', error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: '사용자 목록을 불러오는 중 오류가 발생했습니다.' },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
110
app/components/Header.tsx
Normal file
110
app/components/Header.tsx
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import Image from 'next/image';
|
||||||
|
import logo from '../logo.svg';
|
||||||
|
|
||||||
|
type ActivePage = 'home' | 'lecturelist' | 'studydata' | 'announcement' | null;
|
||||||
|
|
||||||
|
interface HeaderProps {
|
||||||
|
activePage?: ActivePage;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Header({ activePage = null }: HeaderProps) {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const handleNoticeClick = () => {
|
||||||
|
router.push('/announcement');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLogout = () => {
|
||||||
|
localStorage.removeItem('isLoggedIn');
|
||||||
|
router.push('/');
|
||||||
|
};
|
||||||
|
|
||||||
|
const getMenuTextColor = (page: 'lecturelist' | 'studydata' | 'notice') => {
|
||||||
|
if (page === 'lecturelist' && activePage === 'lecturelist') {
|
||||||
|
return 'text-[#1669ca]';
|
||||||
|
}
|
||||||
|
if (page === 'studydata' && activePage === 'studydata') {
|
||||||
|
return 'text-[#1669ca]';
|
||||||
|
}
|
||||||
|
if (page === 'notice' && activePage === 'announcement') {
|
||||||
|
return 'text-[#1669ca]';
|
||||||
|
}
|
||||||
|
return 'text-[#515151] group-hover:text-blue-500';
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<header className="absolute content-stretch flex items-center justify-between left-[332px] top-[43px] w-[1332px]">
|
||||||
|
<div className="content-stretch flex gap-[99px] items-center relative shrink-0">
|
||||||
|
{/* 로고 */}
|
||||||
|
<button
|
||||||
|
onClick={() => router.push('/')}
|
||||||
|
className="h-[74px] relative shrink-0 w-[72px] cursor-pointer"
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
src={logo}
|
||||||
|
alt="로고"
|
||||||
|
className="w-full h-full object-contain"
|
||||||
|
width={72}
|
||||||
|
height={74}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
{/* 메뉴 */}
|
||||||
|
<div className="content-stretch flex gap-[24px] items-center relative shrink-0">
|
||||||
|
<button
|
||||||
|
onClick={() => router.push('/lecturelist')}
|
||||||
|
className="content-stretch flex gap-[150px] items-center relative shrink-0 cursor-pointer group transition-colors"
|
||||||
|
>
|
||||||
|
<div className="box-border content-stretch flex gap-[10px] items-center justify-center p-[10px] relative shrink-0">
|
||||||
|
<p className={`font-bold leading-[normal] not-italic relative shrink-0 text-[24px] text-nowrap whitespace-pre transition-colors ${getMenuTextColor('lecturelist')}`}>
|
||||||
|
교육 과정 목록
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => router.push('/studydata')}
|
||||||
|
className="content-stretch flex gap-[150px] items-center relative shrink-0 cursor-pointer group transition-colors"
|
||||||
|
>
|
||||||
|
<div className="box-border content-stretch flex gap-[10px] items-center justify-center p-[10px] relative shrink-0">
|
||||||
|
<p className={`font-bold leading-[normal] not-italic relative shrink-0 text-[24px] text-nowrap whitespace-pre transition-colors ${getMenuTextColor('studydata')}`}>
|
||||||
|
학습 자료실
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleNoticeClick}
|
||||||
|
className="content-stretch flex gap-[150px] items-center relative shrink-0 cursor-pointer group transition-colors"
|
||||||
|
>
|
||||||
|
<div className="box-border content-stretch flex gap-[10px] items-center justify-center p-[10px] relative shrink-0">
|
||||||
|
<p className={`font-bold leading-[normal] not-italic relative shrink-0 text-[24px] text-nowrap whitespace-pre transition-colors ${getMenuTextColor('notice')}`}>
|
||||||
|
공지사항
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* 사용자 메뉴 */}
|
||||||
|
<div className="content-stretch flex gap-[20px] items-center relative shrink-0">
|
||||||
|
<button
|
||||||
|
onClick={() => router.push('/myinfo')}
|
||||||
|
className="box-border content-stretch flex gap-[10px] items-center justify-center p-[10px] relative shrink-0 cursor-pointer group transition-colors"
|
||||||
|
>
|
||||||
|
<p className="font-medium leading-[normal] not-italic relative shrink-0 text-[#515151] text-[18px] text-nowrap whitespace-pre group-hover:text-blue-500 transition-colors">
|
||||||
|
내 강좌실
|
||||||
|
</p>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleLogout}
|
||||||
|
className="box-border content-stretch flex gap-[10px] items-center justify-center p-[10px] relative shrink-0 cursor-pointer group transition-colors"
|
||||||
|
>
|
||||||
|
<p className="font-medium leading-[normal] not-italic relative shrink-0 text-[#515151] text-[18px] text-nowrap whitespace-pre group-hover:text-blue-500 transition-colors">
|
||||||
|
로그아웃
|
||||||
|
</p>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -3,11 +3,23 @@
|
|||||||
:root {
|
:root {
|
||||||
--background: #ffffff;
|
--background: #ffffff;
|
||||||
--foreground: #171717;
|
--foreground: #171717;
|
||||||
|
|
||||||
|
/* 테마 컬러 */
|
||||||
|
--color-primary: #3b82f6; /* Primary - 파란색 (메인 액션) */
|
||||||
|
--color-secondary: #8b5cf6; /* Secondary - 보라색 (보조 액션) */
|
||||||
|
--color-success: #10b981; /* Success - 초록색 (성공 상태) */
|
||||||
|
--color-error: #ef4444; /* Error - 빨간색 (에러/경고) */
|
||||||
|
--color-neutral: #6b7280; /* Neutral - 회색 (중립/보조) */
|
||||||
}
|
}
|
||||||
|
|
||||||
@theme inline {
|
@theme inline {
|
||||||
--color-background: var(--background);
|
--color-background: var(--background);
|
||||||
--color-foreground: var(--foreground);
|
--color-foreground: var(--foreground);
|
||||||
|
--color-primary: var(--color-primary);
|
||||||
|
--color-secondary: var(--color-secondary);
|
||||||
|
--color-success: var(--color-success);
|
||||||
|
--color-error: var(--color-error);
|
||||||
|
--color-neutral: var(--color-neutral);
|
||||||
--font-sans: var(--font-geist-sans);
|
--font-sans: var(--font-geist-sans);
|
||||||
--font-mono: var(--font-geist-mono);
|
--font-mono: var(--font-geist-mono);
|
||||||
}
|
}
|
||||||
@@ -20,7 +32,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
height: 100vh;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 100vh;
|
||||||
background: var(--background);
|
background: var(--background);
|
||||||
color: var(--foreground);
|
color: var(--foreground);
|
||||||
font-family: Arial, Helvetica, sans-serif;
|
font-family: Arial, Helvetica, sans-serif;
|
||||||
|
|||||||
134
app/idfind/page.tsx
Normal file
134
app/idfind/page.tsx
Normal 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
163
app/lecturelist/page.tsx
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import Image from 'next/image';
|
||||||
|
import Header from '../components/Header';
|
||||||
|
import logo from '../logo.svg';
|
||||||
|
|
||||||
|
const imgLine2 = "http://localhost:3845/assets/6ee8cf4ebb6bc2adb14aab8c9940b3002c20af35.svg";
|
||||||
|
|
||||||
|
export default function 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,79 +1,264 @@
|
|||||||
export default function LoginPage() {
|
"use client";
|
||||||
|
|
||||||
|
import Link from 'next/link';
|
||||||
|
import Image from 'next/image';
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import logo from '../logo.svg';
|
||||||
|
import CheckboxOff from '../../public/svg/checkbox_off';
|
||||||
|
import CheckboxOn from '../../public/svg/checkbox_on';
|
||||||
|
|
||||||
|
type CheckboxProps = {
|
||||||
|
checked: boolean;
|
||||||
|
onChange: () => void;
|
||||||
|
label: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function Checkbox({ checked, onChange, label }: CheckboxProps) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex items-center justify-center">
|
<div className="flex items-center gap-2.5">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onChange}
|
||||||
|
className="relative w-[18px] h-[18px] flex items-center justify-center"
|
||||||
|
>
|
||||||
|
{checked ? <CheckboxOn /> : <CheckboxOff />}
|
||||||
|
</button>
|
||||||
|
<span className="text-sm text-[#515151] leading-[1.6]">{label}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function LoginPage() {
|
||||||
|
const router = useRouter();
|
||||||
|
const [username, setUsername] = useState('');
|
||||||
|
const [password, setPassword] = useState('');
|
||||||
|
const [rememberId, setRememberId] = useState(false);
|
||||||
|
const [autoLogin, setAutoLogin] = useState(false);
|
||||||
|
const [usernameError, setUsernameError] = useState('');
|
||||||
|
const [passwordError, setPasswordError] = useState('');
|
||||||
|
const [showErrorPopup, setShowErrorPopup] = useState(false);
|
||||||
|
|
||||||
|
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setUsernameError('');
|
||||||
|
setPasswordError('');
|
||||||
|
|
||||||
|
// 아이디와 비밀번호가 비어있는지 확인
|
||||||
|
const isUsernameEmpty = !username.trim();
|
||||||
|
const isPasswordEmpty = !password.trim();
|
||||||
|
|
||||||
|
if (isUsernameEmpty) {
|
||||||
|
setUsernameError('이메일을 입력해 주세요.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isPasswordEmpty) {
|
||||||
|
setPasswordError('비밀번호를 입력해 주세요.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 둘 중 하나라도 비어있으면 검증 중단
|
||||||
|
if (isUsernameEmpty || isPasswordEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 관리자 계정 검증
|
||||||
|
if (username === 'admin' && password === '1234') {
|
||||||
|
// 관리자 로그인 성공
|
||||||
|
localStorage.setItem('isAdminLoggedIn', 'true');
|
||||||
|
localStorage.setItem('isLoggedIn', 'true');
|
||||||
|
// 아이디 기억하기 체크 시 아이디 저장
|
||||||
|
if (rememberId) {
|
||||||
|
localStorage.setItem('rememberedUsername', username);
|
||||||
|
} else {
|
||||||
|
localStorage.removeItem('rememberedUsername');
|
||||||
|
}
|
||||||
|
// 루트 경로로 이동 (관리자 페이지 표시)
|
||||||
|
window.location.href = '/';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 일반 사용자 계정 검증
|
||||||
|
if (username === 'qwre@naver.com' && password === '1234') {
|
||||||
|
// 로그인 성공
|
||||||
|
localStorage.setItem('isLoggedIn', 'true');
|
||||||
|
localStorage.removeItem('isAdminLoggedIn'); // 일반 사용자는 관리자 플래그 제거
|
||||||
|
// 아이디 기억하기 체크 시 아이디 저장
|
||||||
|
if (rememberId) {
|
||||||
|
localStorage.setItem('rememberedUsername', username);
|
||||||
|
} else {
|
||||||
|
localStorage.removeItem('rememberedUsername');
|
||||||
|
}
|
||||||
|
// 루트 경로로 이동 (루트 페이지에서 로그인 상태를 확인하여 메인 페이지 표시)
|
||||||
|
window.location.href = '/';
|
||||||
|
} else {
|
||||||
|
// 로그인 실패 - 팝업 표시
|
||||||
|
setShowErrorPopup(true);
|
||||||
|
setUsernameError('아이디 또는 비밀번호가 올바르지 않습니다.');
|
||||||
|
setPasswordError('아이디 또는 비밀번호가 올바르지 않습니다.');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 아이디 기억하기 기능 - 컴포넌트 마운트 시 저장된 아이디 불러오기
|
||||||
|
useEffect(() => {
|
||||||
|
const rememberedUsername = localStorage.getItem('rememberedUsername');
|
||||||
|
if (rememberedUsername) {
|
||||||
|
setUsername(rememberedUsername);
|
||||||
|
setRememberId(true);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen flex justify-center relative pt-[178px]">
|
||||||
<div className="w-full max-w-md">
|
<div className="w-full max-w-md">
|
||||||
{/* 상단 로고 */}
|
{/* 상단 로고 */}
|
||||||
<div className="mb-8 text-center">
|
<div className="mb-8 text-center">
|
||||||
<h1 className="text-2xl font-bold">Logo</h1>
|
<Image
|
||||||
|
src={logo}
|
||||||
|
alt="Logo"
|
||||||
|
width={179}
|
||||||
|
height={185}
|
||||||
|
className="mx-auto"
|
||||||
|
priority
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 로그인 폼 */}
|
{/* 로그인 폼 */}
|
||||||
<form className="space-y-4">
|
<form className="space-y-4" onSubmit={handleSubmit}>
|
||||||
{/* 아이디 입력폼 */}
|
{/* 아이디 입력폼 */}
|
||||||
<div>
|
<div className="flex flex-col gap-2">
|
||||||
<label htmlFor="username" className="block text-sm font-medium mb-1">
|
<div className="flex flex-col gap-1">
|
||||||
아이디
|
<input
|
||||||
</label>
|
type="text"
|
||||||
<input
|
id="username"
|
||||||
type="text"
|
value={username}
|
||||||
id="username"
|
onChange={(e) => {
|
||||||
className="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
setUsername(e.target.value);
|
||||||
placeholder="아이디를 입력하세요"
|
setUsernameError('');
|
||||||
/>
|
}}
|
||||||
|
className={`w-full px-4 py-2.5 border rounded-[8px] focus:outline-none ${usernameError
|
||||||
|
? 'border-[#e61a1a] border-solid'
|
||||||
|
: 'border-gray-300 focus:ring-2 focus:ring-blue-500'
|
||||||
|
}`}
|
||||||
|
placeholder="아이디(이메일)"
|
||||||
|
/>
|
||||||
|
{usernameError && (
|
||||||
|
<p className="text-[#e61a1a] text-xs leading-normal whitespace-pre">
|
||||||
|
{usernameError}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 비밀번호 입력폼 */}
|
{/* 비밀번호 입력폼 */}
|
||||||
<div>
|
<div className="flex flex-col gap-2">
|
||||||
<label htmlFor="password" className="block text-sm font-medium mb-1">
|
<div className="flex flex-col gap-1">
|
||||||
비밀번호
|
<input
|
||||||
</label>
|
type="password"
|
||||||
<input
|
id="password"
|
||||||
type="password"
|
value={password}
|
||||||
id="password"
|
onChange={(e) => {
|
||||||
className="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
setPassword(e.target.value);
|
||||||
placeholder="비밀번호를 입력하세요"
|
setPasswordError('');
|
||||||
/>
|
}}
|
||||||
|
className={`w-full px-4 py-2.5 border rounded-[8px] focus:outline-none ${passwordError
|
||||||
|
? 'border-[#e61a1a] border-solid'
|
||||||
|
: 'border-gray-300 focus:ring-2 focus:ring-blue-500'
|
||||||
|
}`}
|
||||||
|
placeholder="비밀번호를 입력하세요"
|
||||||
|
/>
|
||||||
|
{passwordError && (
|
||||||
|
<p className="text-[#e61a1a] text-xs leading-normal whitespace-pre">
|
||||||
|
{passwordError}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 체크박스 */}
|
{/* 체크박스 */}
|
||||||
<div className="flex items-center space-x-4">
|
<div className="flex items-center gap-[46px] justify-start">
|
||||||
<label className="flex items-center">
|
<Checkbox
|
||||||
<input type="checkbox" className="mr-2" />
|
checked={rememberId}
|
||||||
<span className="text-sm">아이디 기억하기</span>
|
onChange={() => setRememberId(!rememberId)}
|
||||||
</label>
|
label="아이디 기억하기"
|
||||||
<label className="flex items-center">
|
/>
|
||||||
<input type="checkbox" className="mr-2" />
|
<Checkbox
|
||||||
<span className="text-sm">자동로그인</span>
|
checked={autoLogin}
|
||||||
</label>
|
onChange={() => setAutoLogin(!autoLogin)}
|
||||||
|
label="자동 로그인"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 로그인 버튼 */}
|
{/* 로그인 버튼 */}
|
||||||
<button
|
<div className="mt-[54px]">
|
||||||
type="submit"
|
<button
|
||||||
className="w-full py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
type="submit"
|
||||||
>
|
className="w-full h-[52px] bg-blue-500 text-white rounded-md hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500 flex items-center justify-center"
|
||||||
로그인
|
>
|
||||||
</button>
|
로그인
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
{/* 하단 링크 버튼들 */}
|
{/* 하단 링크 버튼들 */}
|
||||||
<div className="mt-4 flex justify-center space-x-4">
|
<div className="mt-4 flex items-center justify-center gap-2.5">
|
||||||
<button className="text-sm text-gray-600 hover:text-gray-800">
|
<Link
|
||||||
|
href="/register"
|
||||||
|
className="text-sm text-gray-600 hover:text-gray-800"
|
||||||
|
>
|
||||||
회원가입
|
회원가입
|
||||||
</button>
|
</Link>
|
||||||
<button className="text-sm text-gray-600 hover:text-gray-800">
|
<Image
|
||||||
|
src="/Divider.svg"
|
||||||
|
alt=""
|
||||||
|
width={1}
|
||||||
|
height={12}
|
||||||
|
className="h-3"
|
||||||
|
/>
|
||||||
|
<Link
|
||||||
|
href="/idfind"
|
||||||
|
className="text-sm text-gray-600 hover:text-gray-800"
|
||||||
|
>
|
||||||
아이디찾기
|
아이디찾기
|
||||||
</button>
|
</Link>
|
||||||
|
<Image
|
||||||
|
src="/Divider.svg"
|
||||||
|
alt=""
|
||||||
|
width={1}
|
||||||
|
height={12}
|
||||||
|
className="h-3"
|
||||||
|
/>
|
||||||
<button className="text-sm text-gray-600 hover:text-gray-800">
|
<button className="text-sm text-gray-600 hover:text-gray-800">
|
||||||
비밀번호 재설정
|
비밀번호 재설정
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 카피라이트 */}
|
{/* 카피라이트 */}
|
||||||
<div className="mt-8 text-center text-sm text-gray-500">
|
<div className="absolute bottom-[40px] left-0 right-0 text-center text-sm text-gray-500">
|
||||||
© 2024 XR LMS. All rights reserved.
|
© 2024 XR LMS. All rights reserved.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 에러 팝업 */}
|
||||||
|
{showErrorPopup && (
|
||||||
|
<div className="fixed inset-0 flex items-center justify-center z-50">
|
||||||
|
<div className="bg-white rounded-[16px] shadow-[0px_4px_4px_0px_rgba(0,0,0,0.25)] max-w-md w-full mx-4">
|
||||||
|
<div className="flex flex-col gap-3 p-6">
|
||||||
|
<div className="text-sm text-black text-center leading-[1.6]">
|
||||||
|
<p className="mb-0">아이디 또는 비밀번호가 일치하지 않아요.</p>
|
||||||
|
<p>확인 후 다시 시도해 주세요.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center w-full border-t border-gray-200">
|
||||||
|
<button
|
||||||
|
onClick={() => setShowErrorPopup(false)}
|
||||||
|
className="w-full h-12 text-sm text-[#404040] hover:bg-gray-50 focus:outline-none rounded-[16px]"
|
||||||
|
>
|
||||||
|
확인
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
9
app/logo.svg
Normal file
9
app/logo.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 368 KiB |
240
app/myinfo/page.tsx
Normal file
240
app/myinfo/page.tsx
Normal 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
370
app/mylecture/page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
217
app/page.tsx
217
app/page.tsx
@@ -1,9 +1,218 @@
|
|||||||
import Image from "next/image";
|
"use client";
|
||||||
|
|
||||||
export default function Home() {
|
import { useEffect, useState } from 'react';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import Image from 'next/image';
|
||||||
|
import { isAdminLoggedIn } from '../lib/auth';
|
||||||
|
import LoginPage from './login/page';
|
||||||
|
import Header from './components/Header';
|
||||||
|
|
||||||
|
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() {
|
||||||
|
const router = useRouter();
|
||||||
|
const [isLoggedIn, setIsLoggedIn] = useState(false);
|
||||||
|
const [isAdmin, setIsAdmin] = useState(false);
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const [currentHeroSlide, setCurrentHeroSlide] = useState(0);
|
||||||
|
|
||||||
|
// 임시 데이터 - 실제로는 API에서 가져올 데이터
|
||||||
|
const [courses, setCourses] = useState([
|
||||||
|
{ 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(() => {
|
||||||
|
// 로그인 상태 확인
|
||||||
|
const loginStatus = localStorage.getItem('isLoggedIn') === 'true';
|
||||||
|
const adminStatus = isAdminLoggedIn();
|
||||||
|
setIsLoggedIn(loginStatus);
|
||||||
|
setIsAdmin(adminStatus);
|
||||||
|
setIsLoading(false);
|
||||||
|
|
||||||
|
// 관리자일 경우 admin_home 페이지로 리다이렉트
|
||||||
|
if (adminStatus) {
|
||||||
|
router.push('/admin_home');
|
||||||
|
}
|
||||||
|
}, [router]);
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return null; // 로딩 중
|
||||||
|
}
|
||||||
|
|
||||||
|
// 로그인되지 않았으면 로그인 페이지 표시
|
||||||
|
if (!isLoggedIn) {
|
||||||
|
return <LoginPage />;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 관리자일 경우 리다이렉트 중
|
||||||
|
if (isAdmin) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 일반 사용자일 경우 기존 메인 페이지 표시
|
||||||
return (
|
return (
|
||||||
<div className="h-screen- bg-red-500">
|
<div className="bg-white relative min-h-screen w-full pb-[199px]">
|
||||||
homepage
|
{/* 헤더 */}
|
||||||
|
<Header activePage="home" />
|
||||||
|
|
||||||
|
{/* 구분선 */}
|
||||||
|
<div className="absolute h-0 left-1/2 top-[150px] translate-x-[-50%] w-[1920px]">
|
||||||
|
<div className="absolute bottom-0 left-0 right-0 top-[-1px]">
|
||||||
|
<img alt="" className="block max-w-none size-full" src={imgLine2} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Hero 배너 */}
|
||||||
|
<div className="absolute h-[402px] left-[-1px] top-[272px] w-full 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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
64
app/pages/page.tsx
Normal file
64
app/pages/page.tsx
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import Link from "next/link";
|
||||||
|
import path from "path";
|
||||||
|
import { promises as fs } from "fs";
|
||||||
|
|
||||||
|
const APP_DIR = path.join(process.cwd(), "app");
|
||||||
|
|
||||||
|
async function collectRoutes(rootDir: string): Promise<string[]> {
|
||||||
|
const routes: string[] = [];
|
||||||
|
|
||||||
|
async function walk(relativeDir: string) {
|
||||||
|
const absoluteDir = path.join(rootDir, relativeDir);
|
||||||
|
const entries = await fs.readdir(absoluteDir, { withFileTypes: true });
|
||||||
|
const names = entries.map((e) => e.name);
|
||||||
|
|
||||||
|
if (names.includes("page.tsx")) {
|
||||||
|
const routePath =
|
||||||
|
relativeDir === "" ? "/" : `/${relativeDir.replace(/\\\\/g, "/")}`;
|
||||||
|
routes.push(routePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (!entry.isDirectory()) continue;
|
||||||
|
if (["api", "components"].includes(entry.name)) continue;
|
||||||
|
if (entry.name.startsWith("(")) continue;
|
||||||
|
if (entry.name.startsWith("_")) continue;
|
||||||
|
if (entry.name === "node_modules") continue;
|
||||||
|
|
||||||
|
await walk(path.join(relativeDir, entry.name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await walk("");
|
||||||
|
|
||||||
|
routes.sort((a, b) => {
|
||||||
|
if (a === "/" && b !== "/") return -1;
|
||||||
|
if (b === "/" && a !== "/") return 1;
|
||||||
|
return a.localeCompare(b);
|
||||||
|
});
|
||||||
|
|
||||||
|
return routes;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function Pages() {
|
||||||
|
const routes = await collectRoutes(APP_DIR);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main style={{ maxWidth: 800, margin: "0 auto", padding: "24px" }}>
|
||||||
|
<h1 style={{ fontSize: 24, fontWeight: 700, marginBottom: 16 }}>
|
||||||
|
모든 페이지 링크
|
||||||
|
</h1>
|
||||||
|
<ul style={{ display: "grid", gap: 8, listStyle: "none", padding: 0 }}>
|
||||||
|
{routes.map((route) => (
|
||||||
|
<li key={route}>
|
||||||
|
<Link href={route} style={{ color: "#2563eb" }}>
|
||||||
|
{route}
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
253
app/pwfind/page.tsx
Normal file
253
app/pwfind/page.tsx
Normal file
@@ -0,0 +1,253 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import Link from 'next/link';
|
||||||
|
|
||||||
|
export default function PasswordFindPage() {
|
||||||
|
const router = useRouter();
|
||||||
|
const [email, setEmail] = useState('');
|
||||||
|
const [newPassword, setNewPassword] = useState('');
|
||||||
|
const [confirmPassword, setConfirmPassword] = useState('');
|
||||||
|
const [emailError, setEmailError] = useState('');
|
||||||
|
const [newPasswordError, setNewPasswordError] = useState('');
|
||||||
|
const [confirmPasswordError, setConfirmPasswordError] = useState('');
|
||||||
|
const [isCodeSent, setIsCodeSent] = useState(false);
|
||||||
|
|
||||||
|
const handleEmailChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const value = e.target.value;
|
||||||
|
setEmail(value);
|
||||||
|
if (emailError) {
|
||||||
|
setEmailError('');
|
||||||
|
}
|
||||||
|
// 이메일이 변경되면 인증 상태 초기화
|
||||||
|
if (isCodeSent) {
|
||||||
|
setIsCodeSent(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNewPasswordChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const value = e.target.value;
|
||||||
|
setNewPassword(value);
|
||||||
|
if (newPasswordError) {
|
||||||
|
setNewPasswordError('');
|
||||||
|
}
|
||||||
|
// 새 비밀번호가 변경되면 확인 비밀번호도 다시 검증
|
||||||
|
if (confirmPassword && value !== confirmPassword) {
|
||||||
|
setConfirmPasswordError('비밀번호와 일치하지 않아요.');
|
||||||
|
} else if (confirmPassword && value === confirmPassword) {
|
||||||
|
setConfirmPasswordError('');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConfirmPasswordChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const value = e.target.value;
|
||||||
|
setConfirmPassword(value);
|
||||||
|
if (confirmPasswordError) {
|
||||||
|
setConfirmPasswordError('');
|
||||||
|
}
|
||||||
|
// 비밀번호 확인 실시간 검증
|
||||||
|
if (value && newPassword && value !== newPassword) {
|
||||||
|
setConfirmPasswordError('비밀번호와 일치하지 않아요.');
|
||||||
|
} else if (value && newPassword && value === newPassword) {
|
||||||
|
setConfirmPasswordError('');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateEmail = (email: string) => {
|
||||||
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||||
|
return emailRegex.test(email);
|
||||||
|
};
|
||||||
|
|
||||||
|
const validatePassword = (password: string) => {
|
||||||
|
return password.length >= 8 && password.length <= 16 && /[a-zA-Z]/.test(password) && /[0-9]/.test(password);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSendCode = () => {
|
||||||
|
if (!email.trim()) {
|
||||||
|
setEmailError('이메일을 입력해 주세요.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!validateEmail(email)) {
|
||||||
|
setEmailError('올바른 이메일을 입력해주세요.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// TODO: 인증번호 발송 API 호출
|
||||||
|
setIsCodeSent(true);
|
||||||
|
setEmailError('');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setEmailError('');
|
||||||
|
setNewPasswordError('');
|
||||||
|
setConfirmPasswordError('');
|
||||||
|
|
||||||
|
let isValid = true;
|
||||||
|
|
||||||
|
if (!email.trim()) {
|
||||||
|
setEmailError('이메일을 입력해 주세요.');
|
||||||
|
isValid = false;
|
||||||
|
} else if (!validateEmail(email)) {
|
||||||
|
setEmailError('올바른 이메일을 입력해주세요.');
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isCodeSent) {
|
||||||
|
setEmailError('인증번호를 발송해주세요.');
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!newPassword.trim()) {
|
||||||
|
setNewPasswordError('새 비밀번호를 입력해 주세요.');
|
||||||
|
isValid = false;
|
||||||
|
} else if (!validatePassword(newPassword)) {
|
||||||
|
setNewPasswordError('비밀번호는 8~16자의 영문/숫자를 포함해야 합니다.');
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!confirmPassword.trim()) {
|
||||||
|
setConfirmPasswordError('새 비밀번호 확인을 입력해 주세요.');
|
||||||
|
isValid = false;
|
||||||
|
} else if (newPassword !== confirmPassword) {
|
||||||
|
setConfirmPasswordError('비밀번호와 일치하지 않아요.');
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isValid) {
|
||||||
|
// TODO: 비밀번호 재설정 API 호출
|
||||||
|
router.push('/login');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const canSubmit = email.trim() !== '' &&
|
||||||
|
newPassword.trim() !== '' &&
|
||||||
|
confirmPassword.trim() !== '' &&
|
||||||
|
isCodeSent &&
|
||||||
|
newPassword === confirmPassword &&
|
||||||
|
validatePassword(newPassword);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative flex flex-col min-h-screen bg-white">
|
||||||
|
{/* 메인 콘텐츠 영역 - 카피라이트와 브라우저 최상단 사이의 중앙 */}
|
||||||
|
<div className="flex-1 flex flex-col items-center justify-center pb-[100px]">
|
||||||
|
{/* 제목 */}
|
||||||
|
<h1 className="font-bold text-[#1D1D1D] text-[32px] leading-tight">
|
||||||
|
비밀번호 재설정
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
{/* 안내 문구 */}
|
||||||
|
<p className="text-[#515151] text-[18px] font-medium leading-normal mt-[40px]">
|
||||||
|
비밀번호 재설정을 위해 아래 정보를 입력해 주세요.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* 입력 폼 */}
|
||||||
|
<form
|
||||||
|
className="w-[829px] border border-[#b9b9b9] rounded-[8px] mt-[40px] px-[31px] py-[29px]"
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
>
|
||||||
|
<div className="flex flex-col gap-[20px]">
|
||||||
|
{/* 이메일(아이디) + 인증번호 발송 */}
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div className="flex gap-[10px] items-center justify-between h-[42px]">
|
||||||
|
<span className="text-[#515151] text-[18px] font-medium w-[177px]">
|
||||||
|
이메일(아이디)
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
value={email}
|
||||||
|
onChange={handleEmailChange}
|
||||||
|
className={`h-[42px] px-[10px] text-[18px] font-medium rounded-[8px] border placeholder:text-[#b9b9b9] bg-white w-[401px] focus:border-[#1669CA] focus:outline-none ${emailError ? 'border-[#E85D5D] text-[#E85D5D]' : 'border-[#b9b9b9] text-[#515151]'}`}
|
||||||
|
placeholder="이메일을 입력해 주세요."
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleSendCode}
|
||||||
|
className="bg-[#2B82E8] rounded-[10px] px-[10px] text-[18px] font-medium text-white h-[43px] w-[158px] transition hover:bg-[#1669ca] flex items-center justify-center"
|
||||||
|
>
|
||||||
|
인증번호 발송
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{emailError && (
|
||||||
|
<p className="text-[13px] text-[#E85D5D] ml-[187px] mt-[16px]">{emailError}</p>
|
||||||
|
)}
|
||||||
|
{isCodeSent && !emailError && (
|
||||||
|
<p className="text-[#1669ca] text-[12px] font-medium ml-[187px] mt-[16px]">
|
||||||
|
인증번호가 발송되었습니다. 이메일을 확인해 주세요.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 새 비밀번호 */}
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div className="flex gap-[16px] items-center h-[42px]">
|
||||||
|
<span className="text-[#515151] text-[18px] font-medium w-[177px]">
|
||||||
|
새 비밀번호
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={newPassword}
|
||||||
|
onChange={handleNewPasswordChange}
|
||||||
|
className={`flex-1 h-[42px] px-[10px] text-[18px] font-medium rounded-[8px] border placeholder:text-[#b9b9b9] bg-white focus:border-[#1669CA] focus:outline-none ${newPasswordError ? 'border-[#E85D5D] text-[#E85D5D]' : 'border-[#b9b9b9] text-[#515151]'}`}
|
||||||
|
placeholder="새 비밀번호를 입력해 주세요."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{newPasswordError && (
|
||||||
|
<p className="text-[13px] text-[#E85D5D] ml-[193px] mt-[16px]">{newPasswordError}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 새 비밀번호 확인 */}
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div className="flex gap-[16px] items-center h-[42px]">
|
||||||
|
<span className="text-[#515151] text-[18px] font-medium w-[177px]">
|
||||||
|
새 비밀번호 확인
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={confirmPassword}
|
||||||
|
onChange={handleConfirmPasswordChange}
|
||||||
|
className={`flex-1 h-[42px] px-[10px] text-[18px] font-medium rounded-[8px] border placeholder:text-[#b9b9b9] bg-white focus:border-[#1669CA] focus:outline-none ${confirmPasswordError ? 'border-[#E85D5D] text-[#E85D5D]' : 'border-[#b9b9b9] text-[#515151]'}`}
|
||||||
|
placeholder="새 비밀번호를 다시 입력해 주세요."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{confirmPasswordError && (
|
||||||
|
<p className="text-[13px] text-[#E85D5D] ml-[193px] mt-[16px]">{confirmPasswordError}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{/* 버튼 영역 */}
|
||||||
|
<div className="mt-[54px] flex gap-[21px]">
|
||||||
|
<Link
|
||||||
|
href="/login"
|
||||||
|
className="border border-[#1669ca] bg-white rounded-[10px] h-[55px] w-[334px] flex items-center justify-center font-medium text-[#515151] text-[18px] hover:bg-[#EDF4FC] transition"
|
||||||
|
>
|
||||||
|
돌아가기
|
||||||
|
</Link>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleSubmit}
|
||||||
|
disabled={!canSubmit}
|
||||||
|
className={`rounded-[10px] h-[55px] w-[334px] font-medium text-[18px] transition flex items-center justify-center
|
||||||
|
${canSubmit
|
||||||
|
? 'bg-[#2B82E8] text-white hover:bg-[#1669ca]'
|
||||||
|
: 'bg-[#b9b9b9] text-white cursor-not-allowed'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
비밀번호 재설정
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 카피라이트 */}
|
||||||
|
<footer className="absolute bottom-[51.5px] left-1/2 -translate-x-1/2 flex flex-col items-center">
|
||||||
|
<p className="text-[16px] text-[rgba(0,0,0,0.55)] leading-[1.45] font-medium tracking-[-0.08px]">
|
||||||
|
Copyright ⓒ 2025 XL LMS. All rights reserved
|
||||||
|
</p>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
630
app/register/page.tsx
Normal file
630
app/register/page.tsx
Normal file
@@ -0,0 +1,630 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import Link from 'next/link';
|
||||||
|
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 imgFormkitRadio = "http://localhost:3845/assets/ea30a9a80d95ced4bfb1174d3a8475a4a1dbbabb.svg";
|
||||||
|
const imgAkarIconsRadio = "http://localhost:3845/assets/d772bd292f6dfddfcbd42cc1f22aa796ed671b11.svg";
|
||||||
|
const imgLsiconDownFilled = "http://localhost:3845/assets/1c65a7143b6e9a0eee4b0878c33198a22da091cf.svg";
|
||||||
|
|
||||||
|
export default function RegisterPage() {
|
||||||
|
const router = useRouter();
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
name: '',
|
||||||
|
phone: '',
|
||||||
|
email: '',
|
||||||
|
password: '',
|
||||||
|
passwordConfirm: '',
|
||||||
|
gender: 'male',
|
||||||
|
birthYear: '',
|
||||||
|
birthMonth: '',
|
||||||
|
birthDay: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const [agreements, setAgreements] = useState({
|
||||||
|
all: false,
|
||||||
|
age14: false,
|
||||||
|
terms: false,
|
||||||
|
privacy: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [errors, setErrors] = useState({
|
||||||
|
name: '',
|
||||||
|
phone: '',
|
||||||
|
email: '',
|
||||||
|
password: '',
|
||||||
|
passwordConfirm: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
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 prevValue = formData[name as keyof typeof formData];
|
||||||
|
setFormData((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[name]: value,
|
||||||
|
}));
|
||||||
|
if (errors[name as keyof typeof errors]) {
|
||||||
|
setErrors((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[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>) => {
|
||||||
|
let value = e.target.value.replace(/[^\d]/g, '');
|
||||||
|
if (value.length > 11) value = value.slice(0, 11);
|
||||||
|
setFormData((prev) => ({
|
||||||
|
...prev,
|
||||||
|
phone: value,
|
||||||
|
}));
|
||||||
|
if (errors.phone) {
|
||||||
|
setErrors((prev) => ({
|
||||||
|
...prev,
|
||||||
|
phone: '',
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAgreementChange = (key: keyof typeof agreements) => {
|
||||||
|
if (key === 'all') {
|
||||||
|
const newValue = !agreements.all;
|
||||||
|
setAgreements({
|
||||||
|
all: newValue,
|
||||||
|
age14: newValue,
|
||||||
|
terms: newValue,
|
||||||
|
privacy: newValue,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const newAgreements = {
|
||||||
|
...agreements,
|
||||||
|
[key]: !agreements[key],
|
||||||
|
};
|
||||||
|
setAgreements({
|
||||||
|
...newAgreements,
|
||||||
|
all: newAgreements.age14 && newAgreements.terms && newAgreements.privacy,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleGenderChange = (gender: 'male' | 'female') => {
|
||||||
|
setFormData((prev) => ({
|
||||||
|
...prev,
|
||||||
|
gender,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
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 validatePhone = (phone: string) => {
|
||||||
|
if (phone === '') return true;
|
||||||
|
const phoneRegex = /^010\d{8}$/;
|
||||||
|
return phoneRegex.test(phone.replace(/[^\d]/g, ''));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSendVerificationCode = () => {
|
||||||
|
if (!formData.email || !validateEmail(formData.email)) {
|
||||||
|
setErrors((prev) => ({
|
||||||
|
...prev,
|
||||||
|
email: '올바른 이메일을 입력해주세요.',
|
||||||
|
}));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// TODO: 인증번호 전송 API 호출
|
||||||
|
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 newErrors = {
|
||||||
|
name: '',
|
||||||
|
phone: '',
|
||||||
|
email: '',
|
||||||
|
password: '',
|
||||||
|
passwordConfirm: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
let isValid = true;
|
||||||
|
|
||||||
|
if (formData.name.trim() === '') {
|
||||||
|
newErrors.name = '이름을 입력해주세요.';
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (formData.phone && !validatePhone(formData.phone)) {
|
||||||
|
newErrors.phone = '올바른 전화번호 형식이 아닙니다.';
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (formData.email.trim() === '') {
|
||||||
|
newErrors.email = '이메일을 입력해주세요.';
|
||||||
|
isValid = false;
|
||||||
|
} else if (!validateEmail(formData.email)) {
|
||||||
|
newErrors.email = '올바른 이메일 형식이 아닙니다.';
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (formData.password === '') {
|
||||||
|
newErrors.password = '비밀번호를 입력해주세요.';
|
||||||
|
isValid = false;
|
||||||
|
} else if (!validatePassword(formData.password)) {
|
||||||
|
newErrors.password = '비밀번호는 8~16자의 영문/숫자를 포함해야 합니다.';
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (formData.passwordConfirm === '') {
|
||||||
|
newErrors.passwordConfirm = '비밀번호 확인을 입력해주세요.';
|
||||||
|
isValid = false;
|
||||||
|
} else if (formData.password !== formData.passwordConfirm) {
|
||||||
|
newErrors.passwordConfirm = '비밀번호가 일치하지 않습니다.';
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
setErrors(newErrors);
|
||||||
|
return isValid;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!agreements.age14 || !agreements.terms || !agreements.privacy) {
|
||||||
|
alert('필수 약관에 동의해주세요.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (validateForm()) {
|
||||||
|
router.push('/registercomplete');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const canProceed =
|
||||||
|
formData.name.trim() !== '' &&
|
||||||
|
formData.email.trim() !== '' &&
|
||||||
|
formData.password !== '' &&
|
||||||
|
formData.passwordConfirm !== '' &&
|
||||||
|
agreements.age14 &&
|
||||||
|
agreements.terms &&
|
||||||
|
agreements.privacy;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative min-h-screen flex flex-col items-center bg-white pb-[100px]">
|
||||||
|
{/* 제목 */}
|
||||||
|
<h1 className="font-bold text-[#1D1D1D] text-[32px] leading-tight mt-[100px]">
|
||||||
|
회원가입
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
{/* 회원정보 입력 폼 */}
|
||||||
|
<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 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"
|
||||||
|
name="name"
|
||||||
|
value={formData.name}
|
||||||
|
onChange={handleChange}
|
||||||
|
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="이름을 입력해 주세요."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{errors.name && (
|
||||||
|
<p className="text-[13px] text-[#E85D5D] ml-[193px] mt-[16px]">{errors.name}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 휴대폰 */}
|
||||||
|
<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"
|
||||||
|
name="phone"
|
||||||
|
value={formData.phone}
|
||||||
|
onChange={handlePhoneChange}
|
||||||
|
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>
|
||||||
|
{errors.phone && (
|
||||||
|
<p className="text-[13px] text-[#E85D5D] ml-[193px] mt-[16px]">{errors.phone}</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="email"
|
||||||
|
name="email"
|
||||||
|
value={formData.email}
|
||||||
|
onChange={handleChange}
|
||||||
|
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="이메일을 입력해 주세요."
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleSendVerificationCode}
|
||||||
|
className="bg-[#2B82E8] rounded-[10px] px-[10px] text-[18px] font-medium text-white h-[42px] w-[158px] transition hover:bg-[#1669ca]"
|
||||||
|
>
|
||||||
|
{isVerificationSent ? '인증번호 재발송' : '인증번호 발송'}
|
||||||
|
</button>
|
||||||
|
</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 flex-col">
|
||||||
|
<div className="flex gap-[16px] items-center h-[41px]">
|
||||||
|
<span className="text-[#515151] text-[18px] font-medium w-[177px]">
|
||||||
|
비밀번호
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
name="password"
|
||||||
|
value={formData.password}
|
||||||
|
onChange={handleChange}
|
||||||
|
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자의 영문/숫자/특수문자를 조합해서 입력해 주세요."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{errors.password && (
|
||||||
|
<p className="text-[13px] text-[#E85D5D] ml-[193px] mt-[16px]">{errors.password}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 비밀번호 확인 */}
|
||||||
|
<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="password"
|
||||||
|
name="passwordConfirm"
|
||||||
|
value={formData.passwordConfirm}
|
||||||
|
onChange={handleChange}
|
||||||
|
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="비밀번호를 다시 입력해 주세요."
|
||||||
|
/>
|
||||||
|
</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 className="flex gap-[16px] items-center h-[24px]">
|
||||||
|
<span className="text-[#515151] text-[18px] font-medium w-[177px]">
|
||||||
|
성별
|
||||||
|
</span>
|
||||||
|
<div className="flex gap-[14px] items-center">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="flex items-center gap-[10px]"
|
||||||
|
onClick={() => handleGenderChange('male')}
|
||||||
|
>
|
||||||
|
{formData.gender === 'male' ? <RadioOn /> : <RadioOff />}
|
||||||
|
<span className="text-[#515151] text-[14px] font-medium leading-[1.6]">
|
||||||
|
남성
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="flex items-center gap-[10px]"
|
||||||
|
onClick={() => handleGenderChange('female')}
|
||||||
|
>
|
||||||
|
{formData.gender === 'female' ? <RadioOn /> : <RadioOff />}
|
||||||
|
<span className="text-[#515151] text-[14px] font-medium leading-[1.6]">
|
||||||
|
여성
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 생년월일 */}
|
||||||
|
<div className="flex gap-[16px] items-center h-[41px]">
|
||||||
|
<span className="text-[#515151] text-[18px] font-medium w-[177px]">
|
||||||
|
생년월일
|
||||||
|
</span>
|
||||||
|
<div className="flex gap-[8px] flex-1">
|
||||||
|
<div className="relative flex-1">
|
||||||
|
<select
|
||||||
|
name="birthYear"
|
||||||
|
value={formData.birthYear}
|
||||||
|
onChange={handleChange}
|
||||||
|
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 className="relative flex-1">
|
||||||
|
<select
|
||||||
|
name="birthMonth"
|
||||||
|
value={formData.birthMonth}
|
||||||
|
onChange={handleChange}
|
||||||
|
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 className="relative flex-1">
|
||||||
|
<select
|
||||||
|
name="birthDay"
|
||||||
|
value={formData.birthDay}
|
||||||
|
onChange={handleChange}
|
||||||
|
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>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{/* 약관 동의 */}
|
||||||
|
<div className="mt-[46px] w-[829px] border border-[#b9b9b9] rounded-[8px] h-[221px] relative">
|
||||||
|
<div className="px-[31px] py-[29px]">
|
||||||
|
{/* 전체동의 */}
|
||||||
|
<div
|
||||||
|
className="flex items-center gap-[16px] cursor-pointer select-none mb-[17px]"
|
||||||
|
onClick={() => handleAgreementChange('all')}
|
||||||
|
>
|
||||||
|
{agreements.all ? <CheckOn /> : <CheckOff />}
|
||||||
|
<span className="font-bold text-[20px] text-[#515151]">
|
||||||
|
모든 항목에 동의합니다.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 개별 동의들 */}
|
||||||
|
<div className="flex flex-col gap-[17px]">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-[16px] cursor-pointer" onClick={() => handleAgreementChange('age14')}>
|
||||||
|
{agreements.age14 ? <CheckOn /> : <CheckOff />}
|
||||||
|
<span className="text-[#515151] text-[16px] font-medium leading-[1.6]">
|
||||||
|
만 14세 이상입니다. (필수)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
전체
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-[16px] cursor-pointer" onClick={() => handleAgreementChange('terms')}>
|
||||||
|
{agreements.terms ? <CheckOn /> : <CheckOff />}
|
||||||
|
<span className="text-[#515151] text-[16px] font-medium leading-[1.6]">
|
||||||
|
이용 약관 동의 (필수)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
전체
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-[16px] cursor-pointer" onClick={() => handleAgreementChange('privacy')}>
|
||||||
|
{agreements.privacy ? <CheckOn /> : <CheckOff />}
|
||||||
|
<span className="text-[#515151] text-[16px] font-medium leading-[1.6]">
|
||||||
|
개인정보 수집 및 이용 동의 (필수)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
전체
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 버튼 영역 */}
|
||||||
|
<div className="mt-[52px] mb-[137px] w-[829px] flex gap-[21px] justify-center">
|
||||||
|
<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={!canProceed}
|
||||||
|
className={`rounded-[10px] h-[55px] w-[334px] font-medium text-[18px] transition flex items-center justify-center
|
||||||
|
${canProceed
|
||||||
|
? 'bg-[#2B82E8] text-white hover:bg-[#1669ca]'
|
||||||
|
: 'bg-[#b9b9b9] text-white cursor-not-allowed'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
회원가입 완료
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 카피라이트 */}
|
||||||
|
<footer className="absolute bottom-[78px] 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
36
app/registercomplete/page.tsx
Normal file
36
app/registercomplete/page.tsx
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import Link from 'next/link';
|
||||||
|
|
||||||
|
export default function RegisterCompletePage() {
|
||||||
|
return (
|
||||||
|
<div className="relative min-h-screen flex flex-col items-center bg-white">
|
||||||
|
{/* 메인 콘텐츠 영역 - 카피라이트와 최상단 사이 중앙 */}
|
||||||
|
<div className="flex-1 flex flex-col justify-center items-center w-full">
|
||||||
|
{/* 완료 메시지 */}
|
||||||
|
<div className="flex flex-col gap-[33px] items-center">
|
||||||
|
<p className="font-bold text-[#515151] text-[32px] leading-normal text-center">
|
||||||
|
회원가입을 완료했어요.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 로그인 버튼 */}
|
||||||
|
<div className="mt-[46px]">
|
||||||
|
<Link
|
||||||
|
href="/login"
|
||||||
|
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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 카피라이트 */}
|
||||||
|
<footer className="absolute bottom-[51.5px] left-1/2 -translate-x-1/2 flex flex-col items-center">
|
||||||
|
<p className="text-[16px] text-[rgba(0,0,0,0.55)] leading-[1.45] font-medium tracking-[-0.08px]">
|
||||||
|
Copyright ⓒ 2025 XL LMS. All rights reserved
|
||||||
|
</p>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
163
app/studydata/page.tsx
Normal file
163
app/studydata/page.tsx
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import Image from 'next/image';
|
||||||
|
import Header from '../components/Header';
|
||||||
|
import logo from '../logo.svg';
|
||||||
|
|
||||||
|
const imgLine2 = "http://localhost:3845/assets/6ee8cf4ebb6bc2adb14aab8c9940b3002c20af35.svg";
|
||||||
|
|
||||||
|
export default function StudyDataPage() {
|
||||||
|
const router = useRouter();
|
||||||
|
const [isLoggedIn, setIsLoggedIn] = useState(false);
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const [curriculums, setCurriculums] = useState<any[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// 로그인 상태 확인
|
||||||
|
const loginStatus = localStorage.getItem('isLoggedIn') === 'true';
|
||||||
|
setIsLoggedIn(loginStatus);
|
||||||
|
setIsLoading(false);
|
||||||
|
|
||||||
|
// TODO: DB에서 교육 과정 목록 가져오기
|
||||||
|
// const fetchCurriculums = async () => {
|
||||||
|
// const response = await fetch('/api/curriculums');
|
||||||
|
// const data = await response.json();
|
||||||
|
// setCurriculums(data);
|
||||||
|
// };
|
||||||
|
// fetchCurriculums();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return null; // 로딩 중
|
||||||
|
}
|
||||||
|
|
||||||
|
// 로그인되지 않았으면 로그인 페이지로 리다이렉트
|
||||||
|
if (!isLoggedIn) {
|
||||||
|
router.push('/login');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-white relative min-h-screen w-full pb-[199px]">
|
||||||
|
{/* 헤더 */}
|
||||||
|
<Header activePage="studydata" />
|
||||||
|
|
||||||
|
{/* 구분선 */}
|
||||||
|
<div className="absolute h-0 left-1/2 top-[150px] translate-x-[-50%] w-[1920px]">
|
||||||
|
<div className="absolute bottom-0 left-0 right-0 top-[-1px]">
|
||||||
|
<img alt="" className="block max-w-none size-full" src={imgLine2} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 메인 콘텐츠 */}
|
||||||
|
<div className="absolute content-stretch flex flex-col items-start left-[calc(6.25%+79px)] top-[270px] w-[1521px]">
|
||||||
|
{/* 페이지 제목 */}
|
||||||
|
<div className="content-stretch flex items-center relative shrink-0 mb-[20px]">
|
||||||
|
<p className="font-bold leading-[normal] not-italic relative shrink-0 text-[#515151] text-[28px]">
|
||||||
|
학습 자료 ({curriculums.length}건)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 테이블 헤더 */}
|
||||||
|
<div className="bg-[rgba(235,247,255,0.5)] content-stretch flex h-[41px] items-center relative shrink-0 w-full">
|
||||||
|
<div className="content-stretch flex items-center relative shrink-0 w-full">
|
||||||
|
<div className="basis-0 border-[0.5px_0px_0.5px_0.5px] border-[#b9b9b9] border-solid box-border content-stretch flex gap-[10px] grow items-center min-h-px min-w-px p-[10px] relative shrink-0">
|
||||||
|
<div className="basis-0 flex flex-col font-bold grow justify-center leading-[0] min-h-px min-w-px not-italic relative shrink-0 text-[18px] text-[#515151]">
|
||||||
|
<p className="leading-[normal]">제목</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="basis-0 border-[0.5px_0px_0.5px_0.5px] border-[#b9b9b9] border-solid box-border content-stretch flex gap-[10px] grow items-center min-h-px min-w-px p-[10px] relative shrink-0">
|
||||||
|
<div className="basis-0 flex flex-col font-bold grow justify-center leading-[0] min-h-px min-w-px not-italic relative shrink-0 text-[18px] text-[#515151]">
|
||||||
|
<p className="leading-[normal]">등록자</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="basis-0 border-[0.5px_0px_0.5px_0.5px] border-[#b9b9b9] border-solid box-border content-stretch flex gap-[10px] grow items-center min-h-px min-w-px p-[10px] relative shrink-0">
|
||||||
|
<div className="basis-0 flex flex-col font-bold grow justify-center leading-[0] min-h-px min-w-px not-italic relative shrink-0 text-[18px] text-[#515151]">
|
||||||
|
<p className="leading-[normal]">등록일</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 테이블 내용 */}
|
||||||
|
{curriculums.length > 0 ? (
|
||||||
|
<div className="content-stretch flex flex-col items-start relative shrink-0 w-full">
|
||||||
|
{curriculums.map((curriculum) => (
|
||||||
|
<div key={curriculum.id} className="content-stretch flex items-center relative shrink-0 w-full border-[0.5px] border-[#b9b9b9] border-solid">
|
||||||
|
<div className="basis-0 border-[0.5px_0px_0.5px_0.5px] border-[#b9b9b9] border-solid box-border content-stretch flex gap-[10px] grow items-center min-h-px min-w-px p-[10px] relative shrink-0">
|
||||||
|
<p className="font-medium leading-[normal] not-italic relative shrink-0 text-[#515151] text-[18px]">
|
||||||
|
{curriculum.title}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="basis-0 border-[0.5px_0px_0.5px_0.5px] border-[#b9b9b9] border-solid box-border content-stretch flex gap-[10px] grow items-center min-h-px min-w-px p-[10px] relative shrink-0">
|
||||||
|
<p className="font-medium leading-[normal] not-italic relative shrink-0 text-[#515151] text-[18px]">
|
||||||
|
{curriculum.instructorName || '-'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="basis-0 border-[0.5px_0px_0.5px_0.5px] border-[#b9b9b9] border-solid box-border content-stretch flex gap-[10px] grow items-center min-h-px min-w-px p-[10px] relative shrink-0">
|
||||||
|
<p className="font-medium leading-[normal] not-italic relative shrink-0 text-[#515151] text-[18px]">
|
||||||
|
{curriculum.createdAt ? new Date(curriculum.createdAt).toLocaleDateString('ko-KR') : '-'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="content-stretch flex items-center justify-center relative shrink-0 w-full py-[40px]">
|
||||||
|
<p className="font-medium leading-[normal] not-italic relative shrink-0 text-[#b9b9b9] text-[18px]">
|
||||||
|
등록된 학습 자료가 없습니다.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 푸터 */}
|
||||||
|
<footer className="absolute bg-[#f7f7f7] box-border content-stretch flex flex-col gap-[10px] h-[225px] items-start left-0 px-[243px] py-[39px] top-[855px] w-[1920px]">
|
||||||
|
<div className="content-stretch flex gap-[49px] items-center relative shrink-0">
|
||||||
|
<div className="h-[74px] relative shrink-0 w-[72px]">
|
||||||
|
<Image
|
||||||
|
src={logo}
|
||||||
|
alt="로고"
|
||||||
|
className="w-full h-full object-contain"
|
||||||
|
width={72}
|
||||||
|
height={74}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="content-stretch flex flex-col gap-[5px] items-start relative shrink-0 w-[479px]">
|
||||||
|
<div className="content-stretch flex gap-[27px] items-center relative shrink-0 w-full">
|
||||||
|
<div className="box-border content-stretch flex gap-[10px] items-center justify-center p-[10px] relative shrink-0">
|
||||||
|
<p className="font-bold leading-[normal] not-italic relative shrink-0 text-[#515151] text-[18px] text-nowrap whitespace-pre">
|
||||||
|
이용 약관
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="box-border content-stretch flex gap-[10px] items-center justify-center p-[10px] relative shrink-0">
|
||||||
|
<p className="font-bold leading-[normal] not-italic relative shrink-0 text-[#515151] text-[18px] text-nowrap whitespace-pre">
|
||||||
|
개인정보처리방침
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="box-border content-stretch flex gap-[10px] items-center justify-center p-[10px] relative shrink-0">
|
||||||
|
<p className="font-bold leading-[normal] not-italic relative shrink-0 text-[#515151] text-[18px] text-nowrap whitespace-pre">
|
||||||
|
고객센터
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="box-border content-stretch flex flex-col font-medium gap-[10px] items-center justify-center leading-[normal] not-italic p-[10px] relative shrink-0 text-[#515151] text-[16px]">
|
||||||
|
<p className="relative shrink-0 w-[400px]">
|
||||||
|
(12345) 서울특별시 광진구 구의동 123-12(구의타워1)
|
||||||
|
</p>
|
||||||
|
<p className="relative shrink-0 w-[400px]">
|
||||||
|
문의: 1234-1234 (평일 09:00 ~ 18:00)
|
||||||
|
</p>
|
||||||
|
<p className="relative shrink-0 w-[400px]">
|
||||||
|
이메일: qwer1234@go.or.kr
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
35
lib/auth.ts
Normal file
35
lib/auth.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
/**
|
||||||
|
* 관리자 인증 유틸리티 함수
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 관리자 로그인 상태 확인
|
||||||
|
* @returns 관리자 로그인 여부
|
||||||
|
*/
|
||||||
|
export function isAdminLoggedIn(): boolean {
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return localStorage.getItem('isAdminLoggedIn') === 'true';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 관리자 인증 체크 및 리다이렉트
|
||||||
|
* 인증되지 않은 경우 로그인 페이지로 이동
|
||||||
|
*/
|
||||||
|
export function requireAdminAuth(): boolean {
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isAdmin = isAdminLoggedIn();
|
||||||
|
|
||||||
|
if (!isAdmin) {
|
||||||
|
// 관리자 인증되지 않은 경우 로그인 페이지로 리다이렉트
|
||||||
|
window.location.href = '/login';
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
14
lib/prisma.ts
Normal file
14
lib/prisma.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { PrismaClient } from './generated/prisma/client';
|
||||||
|
|
||||||
|
const globalForPrisma = globalThis as unknown as {
|
||||||
|
prisma: PrismaClient | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const prisma =
|
||||||
|
globalForPrisma.prisma ??
|
||||||
|
new PrismaClient({
|
||||||
|
log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma;
|
||||||
|
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
import type { NextConfig } from "next";
|
import type { NextConfig } from "next";
|
||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
/* config options here */
|
images: {
|
||||||
|
unoptimized: false,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default nextConfig;
|
||||||
|
|||||||
1429
package-lock.json
generated
1429
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
36
package.json
36
package.json
@@ -6,21 +6,37 @@
|
|||||||
"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",
|
||||||
"react": "19.2.0",
|
"react": "19.2.0",
|
||||||
"react-dom": "19.2.0",
|
"react-dom": "19.2.0"
|
||||||
"next": "16.0.1"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "^5",
|
|
||||||
"@types/node": "^20",
|
|
||||||
"@types/react": "^19",
|
|
||||||
"@types/react-dom": "^19",
|
|
||||||
"@tailwindcss/postcss": "^4",
|
"@tailwindcss/postcss": "^4",
|
||||||
"tailwindcss": "^4",
|
"@types/node": "20.19.24",
|
||||||
|
"@types/react": "19.2.2",
|
||||||
|
"@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",
|
||||||
|
"tsx": "^4.19.2",
|
||||||
|
"typescript": "5.9.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
14
prisma.config.ts
Normal file
14
prisma.config.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { defineConfig, env } from "prisma/config";
|
||||||
|
import { config } from "dotenv";
|
||||||
|
config();
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
schema: "prisma/schema.prisma",
|
||||||
|
migrations: {
|
||||||
|
path: "prisma/migrations",
|
||||||
|
},
|
||||||
|
engine: "classic",
|
||||||
|
datasource: {
|
||||||
|
url: env("DATABASE_URL"),
|
||||||
|
},
|
||||||
|
});
|
||||||
BIN
prisma/prisma/dev.db
Normal file
BIN
prisma/prisma/dev.db
Normal file
Binary file not shown.
99
prisma/schema.prisma
Normal file
99
prisma/schema.prisma
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
// This is your Prisma schema file,
|
||||||
|
// learn more about it in the docs: https://pris.ly/d/prisma-schema
|
||||||
|
|
||||||
|
// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?
|
||||||
|
// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init
|
||||||
|
|
||||||
|
// This is your Prisma schema file,
|
||||||
|
// learn more about it in the docs: https://pris.ly/d/prisma-schema
|
||||||
|
|
||||||
|
generator client {
|
||||||
|
provider = "prisma-client"
|
||||||
|
output = "../lib/generated/prisma"
|
||||||
|
}
|
||||||
|
|
||||||
|
datasource db {
|
||||||
|
provider = "sqlite"
|
||||||
|
url = "file:./prisma/dev.db"
|
||||||
|
}
|
||||||
|
|
||||||
|
// 사용자 권한 Enum
|
||||||
|
enum UserRole {
|
||||||
|
STUDENT // 학습자
|
||||||
|
INSTRUCTOR // 강사
|
||||||
|
ADMIN // 관리자
|
||||||
|
}
|
||||||
|
|
||||||
|
// User 모델
|
||||||
|
model User {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
email String @unique
|
||||||
|
name String?
|
||||||
|
password String
|
||||||
|
phone String?
|
||||||
|
gender String?
|
||||||
|
birthYear Int?
|
||||||
|
birthMonth Int?
|
||||||
|
birthDay Int?
|
||||||
|
role UserRole @default(STUDENT) // 권한 (기본값: 학습자)
|
||||||
|
isActive Boolean @default(true) // 계정 활성화 여부 (true: 활성화, false: 비활성화)
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
registeredLectures Lecture[] @relation("Registrant") // 등록한 강좌 목록
|
||||||
|
enrolledLectures UserLecture[] // 수강 중인 강좌 목록
|
||||||
|
|
||||||
|
@@map("users")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 교육 과정 관리
|
||||||
|
model Curriculum {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
title String // 과정 제목
|
||||||
|
thumbnailImage String? // 썸네일 이미지 경로
|
||||||
|
instructorId String // 강사 ID (User와의 관계)
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
lectures Lecture[] // 강좌 목록 (1:N 관계)
|
||||||
|
|
||||||
|
@@map("curriculums")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 강좌 관리
|
||||||
|
model Lecture {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
title String // 강좌명
|
||||||
|
thumbnailImage String? // 썸네일 이미지 경로
|
||||||
|
attachmentFile String? // 첨부 파일 경로
|
||||||
|
evaluationQuestionCount Int @default(0) // 학습 평가 문제 수
|
||||||
|
curriculumId String // 교육 과정 ID (Curriculum과의 관계)
|
||||||
|
registrantId String // 등록자 ID (User와의 관계)
|
||||||
|
registeredAt DateTime @default(now()) // 등록일
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
curriculum Curriculum @relation(fields: [curriculumId], references: [id], onDelete: Cascade)
|
||||||
|
registrant User @relation("Registrant", fields: [registrantId], references: [id])
|
||||||
|
enrolledUsers UserLecture[] // 수강 중인 사용자 목록
|
||||||
|
|
||||||
|
@@map("lectures")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 사용자-강좌 수강 관계 (다대다 관계)
|
||||||
|
model UserLecture {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
userId String // 사용자 ID
|
||||||
|
lectureId String // 강좌 ID
|
||||||
|
enrolledAt DateTime @default(now()) // 수강 시작일
|
||||||
|
completedAt DateTime? // 수강 완료일
|
||||||
|
isCompleted Boolean @default(false) // 수강 완료 여부
|
||||||
|
progress Int @default(0) // 수강 진행률 (0-100)
|
||||||
|
score Int? // 평가 점수
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
lecture Lecture @relation(fields: [lectureId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
@@unique([userId, lectureId]) // 한 사용자는 같은 강좌를 중복 수강할 수 없음
|
||||||
|
@@map("user_lectures")
|
||||||
|
}
|
||||||
126
prisma/seed.ts
Normal file
126
prisma/seed.ts
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
import { PrismaClient } from '../lib/generated/prisma/client';
|
||||||
|
import bcrypt from 'bcryptjs';
|
||||||
|
|
||||||
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
console.log('🌱 시드 데이터 생성 시작...');
|
||||||
|
|
||||||
|
// 기존 데이터 삭제 (순서 중요: 외래키 관계 고려)
|
||||||
|
console.log('🗑️ 기존 데이터 삭제 중...');
|
||||||
|
await prisma.userLecture.deleteMany();
|
||||||
|
await prisma.lecture.deleteMany();
|
||||||
|
await prisma.curriculum.deleteMany();
|
||||||
|
await prisma.user.deleteMany();
|
||||||
|
|
||||||
|
// 비밀번호 해시 (기본 비밀번호: password123)
|
||||||
|
const hashedPassword = await bcrypt.hash('password123', 10);
|
||||||
|
|
||||||
|
// 1. 사용자 데이터 생성
|
||||||
|
console.log('👥 사용자 데이터 생성 중...');
|
||||||
|
const users = await prisma.user.createMany({
|
||||||
|
data: [
|
||||||
|
// 운영자
|
||||||
|
{ email: 'park@example.com', name: '박민수', password: hashedPassword, role: 'ADMIN', isActive: true },
|
||||||
|
{ email: 'oh@example.com', name: '오준혁', password: hashedPassword, role: 'ADMIN', isActive: true },
|
||||||
|
{ email: 'kwon@example.com', name: '권태영', password: hashedPassword, role: 'ADMIN', isActive: true },
|
||||||
|
|
||||||
|
// 강사
|
||||||
|
{ email: 'kim@example.com', name: '김철수', password: hashedPassword, role: 'INSTRUCTOR', isActive: true },
|
||||||
|
{ email: 'jung@example.com', name: '정대현', password: hashedPassword, role: 'INSTRUCTOR', isActive: true },
|
||||||
|
{ email: 'lim@example.com', name: '임수진', password: hashedPassword, role: 'INSTRUCTOR', isActive: true },
|
||||||
|
{ email: 'shin@example.com', name: '신동욱', password: hashedPassword, role: 'INSTRUCTOR', isActive: true },
|
||||||
|
{ email: 'jeon@example.com', name: '전혜진', password: hashedPassword, role: 'INSTRUCTOR', isActive: true },
|
||||||
|
|
||||||
|
// 학습자
|
||||||
|
{ email: 'hong@example.com', name: '홍길동', password: hashedPassword, role: 'STUDENT', isActive: true },
|
||||||
|
{ email: 'lee@example.com', name: '이영희', password: hashedPassword, role: 'STUDENT', isActive: true },
|
||||||
|
{ email: 'choi@example.com', name: '최지영', password: hashedPassword, role: 'STUDENT', isActive: true },
|
||||||
|
{ email: 'kang@example.com', name: '강미영', password: hashedPassword, role: 'STUDENT', isActive: true },
|
||||||
|
{ email: 'yoon@example.com', name: '윤성호', password: hashedPassword, role: 'STUDENT', isActive: true },
|
||||||
|
{ email: 'han@example.com', name: '한지훈', password: hashedPassword, role: 'STUDENT', isActive: true },
|
||||||
|
{ email: 'song@example.com', name: '송민경', password: hashedPassword, role: 'STUDENT', isActive: true },
|
||||||
|
{ email: 'ryu@example.com', name: '류현우', password: hashedPassword, role: 'STUDENT', isActive: true },
|
||||||
|
{ email: 'cho@example.com', name: '조은서', password: hashedPassword, role: 'STUDENT', isActive: true },
|
||||||
|
{ email: 'bae@example.com', name: '배성민', password: hashedPassword, role: 'STUDENT', isActive: true },
|
||||||
|
{ email: 'namgung@example.com', name: '남궁준', password: hashedPassword, role: 'STUDENT', isActive: true },
|
||||||
|
{ email: 'seo@example.com', name: '서아름', password: hashedPassword, role: 'STUDENT', isActive: true },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`✅ ${users.count}명의 사용자 생성 완료`);
|
||||||
|
|
||||||
|
// 사용자 ID 조회 (강사와 등록자로 사용)
|
||||||
|
const instructorKim = await prisma.user.findUnique({ where: { email: 'kim@example.com' } });
|
||||||
|
const registrarLee = await prisma.user.findUnique({ where: { email: 'lee@example.com' } });
|
||||||
|
const registrarPark = await prisma.user.findUnique({ where: { email: 'park@example.com' } });
|
||||||
|
const registrarKim = await prisma.user.findUnique({ where: { email: 'kim@example.com' } });
|
||||||
|
|
||||||
|
if (!instructorKim || !registrarLee || !registrarPark || !registrarKim) {
|
||||||
|
throw new Error('필수 사용자를 찾을 수 없습니다.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 교육 과정 데이터 생성
|
||||||
|
console.log('📚 교육 과정 데이터 생성 중...');
|
||||||
|
const curriculum1 = await prisma.curriculum.create({
|
||||||
|
data: {
|
||||||
|
title: '방사선 적용',
|
||||||
|
thumbnailImage: null,
|
||||||
|
instructorId: instructorKim.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const curriculum2 = await prisma.curriculum.create({
|
||||||
|
data: {
|
||||||
|
title: '방사선 원리',
|
||||||
|
thumbnailImage: null,
|
||||||
|
instructorId: instructorKim.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('✅ 교육 과정 생성 완료');
|
||||||
|
|
||||||
|
// 3. 강좌 데이터 생성
|
||||||
|
console.log('📖 강좌 데이터 생성 중...');
|
||||||
|
const lectures = await prisma.lecture.createMany({
|
||||||
|
data: [
|
||||||
|
// 방사선 적용 강좌들
|
||||||
|
{ title: '방사선 기본 원리', attachmentFile: '파일1.pdf', evaluationQuestionCount: 10, curriculumId: curriculum1.id, registrantId: registrarLee.id },
|
||||||
|
{ title: '방사선 안전 관리', attachmentFile: '파일2.pdf', evaluationQuestionCount: 15, curriculumId: curriculum1.id, registrantId: registrarLee.id },
|
||||||
|
{ title: '방사선 장비 사용법', attachmentFile: '파일4.pdf', evaluationQuestionCount: 8, curriculumId: curriculum1.id, registrantId: registrarLee.id },
|
||||||
|
{ title: '방사선 보호 장비', attachmentFile: '파일6.pdf', evaluationQuestionCount: 14, curriculumId: curriculum1.id, registrantId: registrarLee.id },
|
||||||
|
{ title: '방사선 응용 실습', attachmentFile: '파일7.pdf', evaluationQuestionCount: 16, curriculumId: curriculum1.id, registrantId: registrarLee.id },
|
||||||
|
{ title: '방사선 의료 응용', attachmentFile: '파일9.pdf', evaluationQuestionCount: 18, curriculumId: curriculum1.id, registrantId: registrarLee.id },
|
||||||
|
{ title: '방사선 산업 응용', attachmentFile: '파일10.pdf', evaluationQuestionCount: 13, curriculumId: curriculum1.id, registrantId: registrarLee.id },
|
||||||
|
{ title: '방사선 환경 모니터링', attachmentFile: '파일12.pdf', evaluationQuestionCount: 9, curriculumId: curriculum1.id, registrantId: registrarLee.id },
|
||||||
|
{ title: '방사선 검사 기법', attachmentFile: '파일13.pdf', evaluationQuestionCount: 19, curriculumId: curriculum1.id, registrantId: registrarLee.id },
|
||||||
|
{ title: '방사선 처리 기술', attachmentFile: '파일15.pdf', evaluationQuestionCount: 7, curriculumId: curriculum1.id, registrantId: registrarLee.id },
|
||||||
|
{ title: '방사선 품질 관리', attachmentFile: '파일16.pdf', evaluationQuestionCount: 22, curriculumId: curriculum1.id, registrantId: registrarLee.id },
|
||||||
|
{ title: '방사선 계측법', attachmentFile: '파일18.pdf', evaluationQuestionCount: 24, curriculumId: curriculum1.id, registrantId: registrarLee.id },
|
||||||
|
{ title: '방사선 안전 규정', attachmentFile: '파일19.pdf', evaluationQuestionCount: 5, curriculumId: curriculum1.id, registrantId: registrarLee.id },
|
||||||
|
|
||||||
|
// 방사선 원리 강좌들
|
||||||
|
{ title: '방사선 측정 기법', attachmentFile: '파일3.pdf', evaluationQuestionCount: 12, curriculumId: curriculum2.id, registrantId: registrarKim.id },
|
||||||
|
{ title: '방사선 물리학 기초', attachmentFile: '파일5.pdf', evaluationQuestionCount: 20, curriculumId: curriculum2.id, registrantId: registrarKim.id },
|
||||||
|
{ title: '방사선 화학 반응', attachmentFile: '파일8.pdf', evaluationQuestionCount: 11, curriculumId: curriculum2.id, registrantId: registrarKim.id },
|
||||||
|
{ title: '방사선 생물학', attachmentFile: '파일11.pdf', evaluationQuestionCount: 17, curriculumId: curriculum2.id, registrantId: registrarKim.id },
|
||||||
|
{ title: '방사선 에너지 전달', attachmentFile: '파일14.pdf', evaluationQuestionCount: 21, curriculumId: curriculum2.id, registrantId: registrarKim.id },
|
||||||
|
{ title: '방사선 방어 이론', attachmentFile: '파일17.pdf', evaluationQuestionCount: 6, curriculumId: curriculum2.id, registrantId: registrarKim.id },
|
||||||
|
{ title: '방사선 핵물리학', attachmentFile: '파일20.pdf', evaluationQuestionCount: 25, curriculumId: curriculum2.id, registrantId: registrarKim.id },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`✅ ${lectures.count}개의 강좌 생성 완료`);
|
||||||
|
|
||||||
|
console.log('🎉 시드 데이터 생성 완료!');
|
||||||
|
}
|
||||||
|
|
||||||
|
main()
|
||||||
|
.catch((e) => {
|
||||||
|
console.error('❌ 시드 데이터 생성 실패:', e);
|
||||||
|
process.exit(1);
|
||||||
|
})
|
||||||
|
.finally(async () => {
|
||||||
|
await prisma.$disconnect();
|
||||||
|
});
|
||||||
|
|
||||||
3
public/Divider.svg
Normal file
3
public/Divider.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="2" height="11" viewBox="0 0 2 11" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M1.125 10.875H0V0H1.125V10.875Z" fill="#515151"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 160 B |
3
public/chevron.svg
Normal file
3
public/chevron.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M18.7071 8.29289C19.0976 8.68342 19.0976 9.31658 18.7071 9.70711L12.7071 15.7071C12.3166 16.0976 11.6834 16.0976 11.2929 15.7071L5.29289 9.70711C4.90237 9.31658 4.90237 8.68342 5.29289 8.29289C5.68342 7.90237 6.31658 7.90237 6.70711 8.29289L12 13.5858L17.2929 8.29289C17.6834 7.90237 18.3166 7.90237 18.7071 8.29289Z" fill="#5F5F5F"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 487 B |
BIN
public/hero.png
Normal file
BIN
public/hero.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 713 KiB |
9
public/logo.svg
Normal file
9
public/logo.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 368 KiB |
7
public/svg/check_off.tsx
Normal file
7
public/svg/check_off.tsx
Normal 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
7
public/svg/check_on.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
7
public/svg/checkbox_off.tsx
Normal file
7
public/svg/checkbox_off.tsx
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export default function CheckboxOff() {
|
||||||
|
return (
|
||||||
|
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect x="0.5" y="0.5" width="17" height="17" rx="3.5" stroke="#B9B9B9" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
15
public/svg/checkbox_on.tsx
Normal file
15
public/svg/checkbox_on.tsx
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
export default function CheckboxOn() {
|
||||||
|
return (
|
||||||
|
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect width="18" height="18" rx="4" fill="#515151" />
|
||||||
|
<g clipPath="url(#clip0_40000058_4306)">
|
||||||
|
<path d="M7.49999 11.0849L5.41499 8.99992L4.70499 9.70492L7.49999 12.4999L13.5 6.49992L12.795 5.79492L7.49999 11.0849Z" fill="white" />
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_40000058_4306">
|
||||||
|
<rect width="12" height="12" fill="white" transform="translate(3 3)" />
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
15
public/svg/chevron_middle.tsx
Normal file
15
public/svg/chevron_middle.tsx
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
export default function ChevronMiddle() {
|
||||||
|
return (
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g clipPath="url(#clip0_40000054_6401)">
|
||||||
|
<path fillRule="evenodd" clipRule="evenodd" d="M12.7083 15.7073C12.5208 15.8948 12.2665 16.0001 12.0013 16.0001C11.7362 16.0001 11.4818 15.8948 11.2943 15.7073L5.63732 10.0503C5.54181 9.9581 5.46563 9.84775 5.41322 9.72575C5.36081 9.60374 5.33322 9.47252 5.33207 9.33974C5.33092 9.20696 5.35622 9.07529 5.4065 8.95239C5.45678 8.82949 5.53103 8.71784 5.62492 8.62395C5.71882 8.53006 5.83047 8.4558 5.95337 8.40552C6.07626 8.35524 6.20794 8.32994 6.34072 8.33109C6.4735 8.33225 6.60472 8.35983 6.72672 8.41224C6.84873 8.46465 6.95907 8.54083 7.05132 8.63634L12.0013 13.5863L16.9513 8.63634C17.1399 8.45418 17.3925 8.35339 17.6547 8.35567C17.9169 8.35795 18.1677 8.46312 18.3531 8.64852C18.5385 8.83393 18.6437 9.08474 18.646 9.34694C18.6483 9.60914 18.5475 9.86174 18.3653 10.0503L12.7083 15.7073Z" fill="#515151" />
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_40000054_6401">
|
||||||
|
<rect width="24" height="24" fill="white" />
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
);
|
||||||
|
}
|
||||||
7
public/svg/chevron_small.tsx
Normal file
7
public/svg/chevron_small.tsx
Normal 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
14
public/svg/close.tsx
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
export default function Close() {
|
||||||
|
return (
|
||||||
|
<svg width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g clipPath="url(#clip0_40000054_6405)">
|
||||||
|
<path d="M20.3033 22.0714L15 16.7681L9.6967 22.0714L7.92893 20.3036L13.2322 15.0003L7.92893 9.69704L9.6967 7.92928L15 13.2326L20.3033 7.92928L22.0711 9.69704L16.7678 15.0003L22.0711 20.3036L20.3033 22.0714Z" fill="#515151" />
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_40000054_6405">
|
||||||
|
<rect width="30" height="30" fill="white" />
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
7
public/svg/logout.tsx
Normal file
7
public/svg/logout.tsx
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export default function Logout() {
|
||||||
|
return (
|
||||||
|
<svg width="22" height="24" viewBox="0 0 22 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M22 23.1204V0H11V2.56893H19.5556V20.5514H11V23.1204H22ZM6.11111 17.9825L7.79167 16.12L4.675 12.8447H14.6667V10.2757H4.675L7.79167 7.00033L6.11111 5.13786L2.38419e-07 11.5602L6.11111 17.9825Z" fill="#606060" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
8
public/svg/pagenav_leftoff.tsx
Normal file
8
public/svg/pagenav_leftoff.tsx
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
export default function PageNavLeftOff() {
|
||||||
|
return (
|
||||||
|
<svg width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M11.2002 0.5H18.7998C21.8166 0.5 23.2067 0.506341 24.3174 0.867188C26.6007 1.60907 28.3909 3.39933 29.1328 5.68262C29.4937 6.79334 29.5 8.1834 29.5 11.2002V18.7998C29.5 21.8166 29.4937 23.2067 29.1328 24.3174C28.3909 26.6007 26.6007 28.3909 24.3174 29.1328C23.2067 29.4937 21.8166 29.5 18.7998 29.5H11.2002C8.1834 29.5 6.79334 29.4937 5.68262 29.1328C3.39933 28.3909 1.60907 26.6007 0.867188 24.3174C0.506341 23.2067 0.5 21.8166 0.5 18.7998V11.2002C0.5 8.1834 0.506341 6.79334 0.867188 5.68262C1.60907 3.39933 3.39933 1.60907 5.68262 0.867188C6.79334 0.506341 8.1834 0.5 11.2002 0.5Z" stroke="#EEEEEE" />
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M18.7002 21.5193C18.3199 21.9522 17.6592 21.9961 17.2246 21.6173L11.9708 17.0382C10.7328 15.9592 10.7328 14.0408 11.9708 12.9618L17.2246 8.38272C17.6592 8.00388 18.3199 8.04776 18.7002 8.48071C19.0805 8.91367 19.0365 9.57175 18.6018 9.95059L13.3481 14.5296C13.0624 14.7786 13.0624 15.2213 13.3481 15.4703L18.6018 20.0494C19.0365 20.4282 19.0805 21.0863 18.7002 21.5193Z" fill="#E1E1E1" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
8
public/svg/pagenav_lefton.tsx
Normal file
8
public/svg/pagenav_lefton.tsx
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
export default function PageNavLeftOn() {
|
||||||
|
return (
|
||||||
|
<svg width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M11.2002 0.5H18.7998C21.8166 0.5 23.2067 0.506341 24.3174 0.867188C26.6007 1.60907 28.3909 3.39933 29.1328 5.68262C29.4937 6.79334 29.5 8.1834 29.5 11.2002V18.7998C29.5 21.8166 29.4937 23.2067 29.1328 24.3174C28.3909 26.6007 26.6007 28.3909 24.3174 29.1328C23.2067 29.4937 21.8166 29.5 18.7998 29.5H11.2002C8.1834 29.5 6.79334 29.4937 5.68262 29.1328C3.39933 28.3909 1.60907 26.6007 0.867188 24.3174C0.506341 23.2067 0.5 21.8166 0.5 18.7998V11.2002C0.5 8.1834 0.506341 6.79334 0.867188 5.68262C1.60907 3.39933 3.39933 1.60907 5.68262 0.867188C6.79334 0.506341 8.1834 0.5 11.2002 0.5Z" stroke="#EEEEEE" />
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M18.7002 21.5193C18.3199 21.9522 17.6592 21.9961 17.2246 21.6173L11.9708 17.0382C10.7328 15.9592 10.7328 14.0408 11.9708 12.9618L17.2246 8.38272C17.6592 8.00388 18.3199 8.04776 18.7002 8.48071C19.0805 8.91367 19.0365 9.57175 18.6018 9.95059L13.3481 14.5296C13.0624 14.7786 13.0624 15.2213 13.3481 15.4703L18.6018 20.0494C19.0365 20.4282 19.0805 21.0863 18.7002 21.5193Z" fill="#515151" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
8
public/svg/pagenav_rightoff.tsx
Normal file
8
public/svg/pagenav_rightoff.tsx
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
export default function PageNavRightOff() {
|
||||||
|
return (
|
||||||
|
<svg width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M11.2002 0.5H18.7998C21.8166 0.5 23.2067 0.506341 24.3174 0.867188C26.6007 1.60907 28.3909 3.39933 29.1328 5.68262C29.4937 6.79334 29.5 8.1834 29.5 11.2002V18.7998C29.5 21.8166 29.4937 23.2067 29.1328 24.3174C28.3909 26.6007 26.6007 28.3909 24.3174 29.1328C23.2067 29.4937 21.8166 29.5 18.7998 29.5H11.2002C8.1834 29.5 6.79334 29.4937 5.68262 29.1328C3.39933 28.3909 1.60907 26.6007 0.867188 24.3174C0.506341 23.2067 0.5 21.8166 0.5 18.7998V11.2002C0.5 8.1834 0.506341 6.79334 0.867188 5.68262C1.60907 3.39933 3.39933 1.60907 5.68262 0.867188C6.79334 0.506341 8.1834 0.5 11.2002 0.5Z" stroke="#EEEEEE" />
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.2998 8.48074C11.6801 8.04778 12.3408 8.00391 12.7754 8.38275L18.0292 12.9618C19.2672 14.0408 19.2672 15.9592 18.0292 17.0382L12.7754 21.6173C12.3408 21.9961 11.6801 21.9522 11.2998 21.5193C10.9195 21.0863 10.9635 20.4282 11.3982 20.0494L16.6519 15.4704C16.9376 15.2214 16.9376 14.7787 16.6519 14.5297L11.3982 9.95061C10.9635 9.57178 10.9195 8.91369 11.2998 8.48074Z" fill="#E1E1E1" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
8
public/svg/pagenav_righton.tsx
Normal file
8
public/svg/pagenav_righton.tsx
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
export default function PageNavRightOn() {
|
||||||
|
return (
|
||||||
|
<svg width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M11.2002 0.5H18.7998C21.8166 0.5 23.2067 0.506341 24.3174 0.867188C26.6007 1.60907 28.3909 3.39933 29.1328 5.68262C29.4937 6.79334 29.5 8.1834 29.5 11.2002V18.7998C29.5 21.8166 29.4937 23.2067 29.1328 24.3174C28.3909 26.6007 26.6007 28.3909 24.3174 29.1328C23.2067 29.4937 21.8166 29.5 18.7998 29.5H11.2002C8.1834 29.5 6.79334 29.4937 5.68262 29.1328C3.39933 28.3909 1.60907 26.6007 0.867188 24.3174C0.506341 23.2067 0.5 21.8166 0.5 18.7998V11.2002C0.5 8.1834 0.506341 6.79334 0.867188 5.68262C1.60907 3.39933 3.39933 1.60907 5.68262 0.867188C6.79334 0.506341 8.1834 0.5 11.2002 0.5Z" stroke="#EEEEEE" />
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.2998 8.48074C11.6801 8.04778 12.3408 8.00391 12.7754 8.38275L18.0292 12.9618C19.2672 14.0408 19.2672 15.9592 18.0292 17.0382L12.7754 21.6173C12.3408 21.9961 11.6801 21.9522 11.2998 21.5193C10.9194 21.0863 10.9635 20.4282 11.3981 20.0494L16.6519 15.4704C16.9376 15.2214 16.9376 14.7787 16.6519 14.5297L11.3982 9.95062C10.9635 9.57178 10.9195 8.91369 11.2998 8.48074Z" fill="#515151" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
7
public/svg/radio_off.tsx
Normal file
7
public/svg/radio_off.tsx
Normal 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
8
public/svg/radio_on.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,7 +1,11 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ES2017",
|
"target": "ES2017",
|
||||||
"lib": ["dom", "dom.iterable", "esnext"],
|
"lib": [
|
||||||
|
"dom",
|
||||||
|
"dom.iterable",
|
||||||
|
"esnext"
|
||||||
|
],
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
@@ -19,7 +23,9 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["./*"]
|
"@/*": [
|
||||||
|
"./*"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
@@ -30,5 +36,7 @@
|
|||||||
".next/dev/types/**/*.ts",
|
".next/dev/types/**/*.ts",
|
||||||
"**/*.mts"
|
"**/*.mts"
|
||||||
],
|
],
|
||||||
"exclude": ["node_modules"]
|
"exclude": [
|
||||||
|
"node_modules"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user