From 0452ca2c287c279f003b29371ed871a43b361a10 Mon Sep 17 00:00:00 2001 From: koreacomp5 Date: Tue, 18 Nov 2025 06:19:26 +0900 Subject: [PATCH] =?UTF-8?q?=E3=84=B1=E3=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/NavBar.tsx | 114 ++++++++ src/app/find-id/FindIdDevOption.tsx | 82 ------ src/app/find-id/FindIdOption.tsx | 185 +++++-------- src/app/find-id/page.tsx | 124 ++++++++- src/app/layout.tsx | 2 + src/app/menu/ChangePasswordModal.tsx | 149 +++++++++++ src/app/menu/MenuSidebar.tsx | 70 +++++ src/app/menu/account/page.tsx | 60 +++++ src/app/menu/courses/page.tsx | 14 + src/app/menu/layout.tsx | 15 ++ src/app/menu/page.tsx | 12 + src/app/menu/results/page.tsx | 14 + src/app/page.tsx | 378 ++++++++++++++++++++++++++- src/app/register/RegisterDone.tsx | 4 +- src/app/register/RegisterOption.tsx | 2 +- src/app/svgs/mainlogosvg.tsx | 6 +- 16 files changed, 1012 insertions(+), 219 deletions(-) create mode 100644 src/app/NavBar.tsx delete mode 100644 src/app/find-id/FindIdDevOption.tsx create mode 100644 src/app/menu/ChangePasswordModal.tsx create mode 100644 src/app/menu/MenuSidebar.tsx create mode 100644 src/app/menu/account/page.tsx create mode 100644 src/app/menu/courses/page.tsx create mode 100644 src/app/menu/layout.tsx create mode 100644 src/app/menu/page.tsx create mode 100644 src/app/menu/results/page.tsx diff --git a/src/app/NavBar.tsx b/src/app/NavBar.tsx new file mode 100644 index 0000000..1757d65 --- /dev/null +++ b/src/app/NavBar.tsx @@ -0,0 +1,114 @@ +'use client'; + +import Link from "next/link"; +import { useEffect, useRef, useState } from "react"; +import { usePathname } from "next/navigation"; +import MainLogoSvg from "./svgs/mainlogosvg"; + +const NAV_ITEMS = [ + { label: "교육 과정 목록", href: "/menu" }, + { label: "학습 자료실", href: "/resources" }, + { label: "공지사항", href: "/notices" }, +]; + +export default function NavBar() { + const pathname = usePathname(); + const [isUserMenuOpen, setIsUserMenuOpen] = useState(false); + const userMenuRef = useRef(null); + const userButtonRef = useRef(null); + + useEffect(() => { + if (!isUserMenuOpen) return; + const onDown = (e: MouseEvent) => { + const t = e.target as Node; + if ( + userMenuRef.current && + !userMenuRef.current.contains(t) && + userButtonRef.current && + !userButtonRef.current.contains(t) + ) { + setIsUserMenuOpen(false); + } + }; + const onKey = (e: KeyboardEvent) => { + if (e.key === "Escape") setIsUserMenuOpen(false); + }; + document.addEventListener("mousedown", onDown); + document.addEventListener("keydown", onKey); + return () => { + document.removeEventListener("mousedown", onDown); + document.removeEventListener("keydown", onKey); + }; + }, [isUserMenuOpen]); + + return ( +
+
+
+ + + XR LMS + + +
+
+ + 내 강좌실 + + + {isUserMenuOpen && ( +
+ + +
+ )} +
+
+
+ ); +} + + diff --git a/src/app/find-id/FindIdDevOption.tsx b/src/app/find-id/FindIdDevOption.tsx deleted file mode 100644 index ca29738..0000000 --- a/src/app/find-id/FindIdDevOption.tsx +++ /dev/null @@ -1,82 +0,0 @@ -"use client"; - -import React, { useState } from "react"; - -type FindIdDevOptionProps = { - doneEnabled?: boolean; - setDoneEnabled?: (enabled: boolean) => void; - failedEnabled?: boolean; - setFailedEnabled?: (enabled: boolean) => void; -}; - -export default function FindIdDevOption({ - doneEnabled, - setDoneEnabled, - failedEnabled, - setFailedEnabled, -}: FindIdDevOptionProps) { - const [isOpen, setIsOpen] = useState(false); - - return ( -
- - {isOpen && ( -
- -
    -
  • -

    find-id done overlay

    - -
  • -
  • -

    find-id failed overlay

    - -
  • -
-
-
- )} - - ); -} - - diff --git a/src/app/find-id/FindIdOption.tsx b/src/app/find-id/FindIdOption.tsx index 9aa7998..ca29738 100644 --- a/src/app/find-id/FindIdOption.tsx +++ b/src/app/find-id/FindIdOption.tsx @@ -1,131 +1,82 @@ "use client"; -import React, { useMemo, useState } from "react"; -import LoginInputSvg from "@/app/svgs/inputformx"; -import Link from "next/link"; +import React, { useState } from "react"; -type FindIdOptionProps = { - onOpenDone: (userId?: string) => void; - onOpenFailed: () => void; +type FindIdDevOptionProps = { + doneEnabled?: boolean; + setDoneEnabled?: (enabled: boolean) => void; + failedEnabled?: boolean; + setFailedEnabled?: (enabled: boolean) => void; }; -export default function FindIdOption({ onOpenDone, onOpenFailed }: FindIdOptionProps) { - const [name, setName] = useState(""); - const [phone, setPhone] = useState(""); - const [focused, setFocused] = useState>({}); - const [errors, setErrors] = useState>({}); - - const isNameValid = useMemo(() => name.trim().length > 0, [name]); - const isPhoneValid = useMemo(() => /^\d{9,11}$/.test(phone), [phone]); - const canSubmit = useMemo(() => isNameValid && isPhoneValid, [isNameValid, isPhoneValid]); - - function validateAll() { - const next: Record = {}; - if (!isNameValid) next.name = "이름을 입력해 주세요."; - if (!isPhoneValid) next.phone = "휴대폰 번호를 숫자만 9~11자리로 입력해 주세요."; - setErrors(next); - return Object.keys(next).length === 0; - } - - function handleSubmit(e: React.FormEvent) { - e.preventDefault(); - if (!validateAll()) return; - const mockUserId = `${name.trim()}@example.com`; - onOpenDone(mockUserId); - } +export default function FindIdDevOption({ + doneEnabled, + setDoneEnabled, + failedEnabled, + setFailedEnabled, +}: FindIdDevOptionProps) { + const [isOpen, setIsOpen] = useState(false); return ( -
- + - )} + + {isOpen && ( +
+ +
    +
  • +

    find-id done overlay

    + +
  • +
  • +

    find-id failed overlay

    + +
  • +
- {errors.name &&

{errors.name}

}
- -
- -
- setPhone(e.target.value.replace(/[^0-9]/g, ""))} - onFocus={() => setFocused((p) => ({ ...p, phone: true }))} - onBlur={() => setFocused((p) => ({ ...p, phone: false }))} - placeholder="-없이 입력해 주세요." - className="h-[40px] px-[12px] py-[7px] w-full rounded-[8px] border border-neutral-40 focus:outline-none focus:border-neutral-700 text-[18px] text-neutral-700 placeholder:text-input-placeholder-text pr-[40px]" - /> - {phone.trim().length > 0 && focused.phone && ( - - )} -
- {errors.phone &&

{errors.phone}

} -
- - - - - + )} ); } + diff --git a/src/app/find-id/page.tsx b/src/app/find-id/page.tsx index 32dd77b..25c81c1 100644 --- a/src/app/find-id/page.tsx +++ b/src/app/find-id/page.tsx @@ -1,15 +1,39 @@ "use client"; -import { useState } from "react"; +import React, { useMemo, useState } from "react"; import IdFindDone from "./IdFindDone"; import IdFindFailed from "./IdFindFailed"; import FindIdOption from "./FindIdOption"; -import FindIdDevOption from "./FindIdDevOption"; +import LoginInputSvg from "@/app/svgs/inputformx"; export default function FindIdPage() { const [isDoneOpen, setIsDoneOpen] = useState(false); const [isFailedOpen, setIsFailedOpen] = useState(false); const [foundUserId, setFoundUserId] = useState(undefined); + const [name, setName] = useState(""); + const [phone, setPhone] = useState(""); + const [focused, setFocused] = useState>({}); + const [errors, setErrors] = useState>({}); + + const isNameValid = useMemo(() => name.trim().length > 0, [name]); + const isPhoneValid = useMemo(() => /^\d{9,11}$/.test(phone), [phone]); + const canSubmit = useMemo(() => isNameValid && isPhoneValid, [isNameValid, isPhoneValid]); + + function validateAll() { + const next: Record = {}; + if (!isNameValid) next.name = "이름을 입력해 주세요."; + if (!isPhoneValid) next.phone = "휴대폰 번호를 숫자만 9~11자리로 입력해 주세요."; + setErrors(next); + return Object.keys(next).length === 0; + } + + function handleSubmit(e: React.FormEvent) { + e.preventDefault(); + if (!validateAll()) return; + const mockUserId = `${name.trim()}@example.com`; + setFoundUserId(mockUserId); + setIsDoneOpen(true); + } return (
@@ -23,17 +47,93 @@ export default function FindIdPage() { onClose={() => setIsFailedOpen(false)} /> - { - setFoundUserId(id); - setIsDoneOpen(true); - }} - onOpenFailed={() => { - setIsFailedOpen(true); - }} - /> +
+ +
+
+ 아이디 찾기 +
+

+ 가입 시 등록한 회원정보를 입력해 주세요. +

+
- +
+ +
+ setName(e.target.value)} + onFocus={() => setFocused((p) => ({ ...p, name: true }))} + onBlur={() => setFocused((p) => ({ ...p, name: false }))} + placeholder="이름을 입력해 주세요." + className="h-[40px] px-[12px] py-[7px] w-full rounded-[8px] border border-neutral-40 focus:outline-none focus:border-neutral-700 text-[18px] text-neutral-700 placeholder:text-input-placeholder-text pr-[40px]" + /> + {name.trim().length > 0 && focused.name && ( + + )} +
+ {errors.name &&

{errors.name}

} +
+ +
+ +
+ setPhone(e.target.value.replace(/[^0-9]/g, ""))} + onFocus={() => setFocused((p) => ({ ...p, phone: true }))} + onBlur={() => setFocused((p) => ({ ...p, phone: false }))} + placeholder="-없이 입력해 주세요." + className="h-[40px] px-[12px] py-[7px] w-full rounded-[8px] border border-neutral-40 focus:outline-none focus:border-neutral-700 text-[18px] text-neutral-700 placeholder:text-input-placeholder-text pr-[40px]" + /> + {phone.trim().length > 0 && focused.phone && ( + + )} +
+ {errors.phone &&

{errors.phone}

} +
+ + + + + +
+ + + {children} diff --git a/src/app/menu/ChangePasswordModal.tsx b/src/app/menu/ChangePasswordModal.tsx new file mode 100644 index 0000000..765a1ae --- /dev/null +++ b/src/app/menu/ChangePasswordModal.tsx @@ -0,0 +1,149 @@ +'use client'; + +import { useState } from "react"; + +type Props = { + open: boolean; + onClose: () => void; + onSubmit?: (payload: { email: string; code: string; newPassword: string }) => void; +}; + +export default function ChangePasswordModal({ open, onClose, onSubmit }: Props) { + const [email, setEmail] = useState("xrlms2025@gmail.com"); + const [code, setCode] = useState(""); + const [newPassword, setNewPassword] = useState(""); + const [confirmPassword, setConfirmPassword] = useState(""); + const [error, setError] = useState(null); // 인증번호 오류 등 + + if (!open) return null; + + const handleSubmit = () => { + setError(null); + if (!code) { + setError("인증번호를 입력해 주세요."); + return; + } + if (!newPassword || !confirmPassword) { + setError("새 비밀번호를 입력해 주세요."); + return; + } + if (newPassword !== confirmPassword) { + setError("새 비밀번호가 일치하지 않습니다."); + return; + } + onSubmit?.({ email, code, newPassword }); + onClose(); + }; + + return ( +
+
+ {/* header */} +
+

비밀번호 변경

+ +
+ + {/* body */} +
+
+ +
+ setEmail(e.target.value)} + className="h-10 flex-1 rounded-[8px] border border-[#dee1e6] bg-white px-3 text-[16px] leading-[1.5] text-[#333c47] placeholder-[#b1b8c0] outline-none" + placeholder="이메일" + /> + +
+
+
+
인증번호
+
+ setCode(e.target.value)} + className="h-10 flex-1 rounded-[8px] border border-[#dee1e6] bg-white px-3 text-[16px] leading-[1.5] text-[#333c47] placeholder-[#b1b8c0] outline-none" + placeholder="인증번호 6자리" + /> + +
+ {error ? ( +

+ {error} +

+ ) : null} +
+
+ + setNewPassword(e.target.value)} + className="h-10 rounded-[8px] border border-[#dee1e6] bg-neutral-50 px-3 text-[16px] leading-[1.5] text-[#333c47] placeholder-[#b1b8c0] outline-none" + placeholder="새 비밀번호" + /> +
+
+ + setConfirmPassword(e.target.value)} + className="h-10 rounded-[8px] border border-[#dee1e6] bg-neutral-50 px-3 text-[16px] leading-[1.5] text-[#333c47] placeholder-[#b1b8c0] outline-none" + placeholder="새 비밀번호 확인" + /> +
+
+ + {/* footer */} +
+ + +
+
+
+ ); +} + + diff --git a/src/app/menu/MenuSidebar.tsx b/src/app/menu/MenuSidebar.tsx new file mode 100644 index 0000000..17d583b --- /dev/null +++ b/src/app/menu/MenuSidebar.tsx @@ -0,0 +1,70 @@ +'use client'; + +import Link from "next/link"; +import { usePathname } from "next/navigation"; + +type NavItem = { + label: string; + href: string; +}; + +const learningItems: NavItem[] = [ + { label: "내 강좌실", href: "/menu/courses" }, + { label: "학습 결과", href: "/menu/results" }, +]; + +const accountItems: NavItem[] = [ + { label: "내 정보 수정", href: "/menu/account" }, + { label: "로그아웃", href: "/logout" }, +]; + +export default function MenuSidebar() { + const pathname = usePathname(); + + const renderItem = (item: NavItem) => { + const isActive = + pathname === item.href || (item.href !== "/logout" && pathname.startsWith(item.href)); + return ( + + + {item.label} + + + ); + }; + + return ( + + ); +} + + diff --git a/src/app/menu/account/page.tsx b/src/app/menu/account/page.tsx new file mode 100644 index 0000000..47cfb20 --- /dev/null +++ b/src/app/menu/account/page.tsx @@ -0,0 +1,60 @@ +'use client'; + +import { useState } from "react"; +import ChangePasswordModal from "../ChangePasswordModal"; + +export default function AccountPage() { + const [open, setOpen] = useState(false); + + return ( +
+
+

내 정보 수정

+
+
+
+
+ +
+ skyblue@edu.com +
+
+
+ +
+
+ ●●●●●●●●●● +
+ +
+
+
+
+ +
+
+ + setOpen(false)} + onSubmit={() => { + // TODO: integrate API + }} + /> +
+ ); +} + + diff --git a/src/app/menu/courses/page.tsx b/src/app/menu/courses/page.tsx new file mode 100644 index 0000000..c54bede --- /dev/null +++ b/src/app/menu/courses/page.tsx @@ -0,0 +1,14 @@ +export default function CoursesPage() { + return ( +
+
+

내 강좌실

+
+
+

콘텐츠 준비 중입니다.

+
+
+ ); +} + + diff --git a/src/app/menu/layout.tsx b/src/app/menu/layout.tsx new file mode 100644 index 0000000..c457839 --- /dev/null +++ b/src/app/menu/layout.tsx @@ -0,0 +1,15 @@ +import type { ReactNode } from "react"; +import MenuSidebar from "./MenuSidebar"; + +export default function MenuLayout({ children }: { children: ReactNode }) { + return ( +
+ +
{children}
+
+ ); +} + + diff --git a/src/app/menu/page.tsx b/src/app/menu/page.tsx new file mode 100644 index 0000000..e532782 --- /dev/null +++ b/src/app/menu/page.tsx @@ -0,0 +1,12 @@ +export default function MenuPage() { + return ( +
+

교육 과정 목록

+

+ 메뉴 페이지 준비 중입니다. +

+
+ ); +} + + diff --git a/src/app/menu/results/page.tsx b/src/app/menu/results/page.tsx new file mode 100644 index 0000000..bf31bc8 --- /dev/null +++ b/src/app/menu/results/page.tsx @@ -0,0 +1,14 @@ +export default function ResultsPage() { + return ( +
+
+

학습 결과

+
+
+

콘텐츠 준비 중입니다.

+
+
+ ); +} + + diff --git a/src/app/page.tsx b/src/app/page.tsx index aa23646..d87ac11 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,7 +1,379 @@ -export default function Home() { - return ( -
+/* eslint-disable @next/next/no-img-element */ +'use client'; +import { useEffect, useMemo, useRef, useState } from 'react'; +import MainLogoSvg from './svgs/mainlogosvg'; + +export default function Home() { + const containerRef = useRef(null); + const [currentIndex, setCurrentIndex] = useState(0); + + // 코스, 공지사항 더미 데이터 + const courseCards = useMemo( + () => + [ + { + id: 'c1', + title: '원자력 운영 기초', + meta: 'VOD • 초급 • 4시간 20분', + image: + 'https://images.unsplash.com/photo-1555949963-ff9fe0c870eb?q=80&w=1200&auto=format&fit=crop', + }, + { + id: 'c2', + title: '반도체', + meta: 'VOD • 중급 • 3시간 10분', + image: + 'https://images.unsplash.com/photo-1581092921461-eab62e97a780?q=80&w=1200&auto=format&fit=crop', + }, + { + id: 'c3', + title: '방사선 안전', + meta: 'VOD • 중급 • 4시간 20분', + image: + 'https://images.unsplash.com/photo-1581090464777-f3220bbe1b8b?q=80&w=1200&auto=format&fit=crop', + }, + { + id: 'c4', + title: '방사선 폐기물', + meta: 'VOD • 중급 • 4시간 20분', + image: + 'https://images.unsplash.com/photo-1581091220351-5a6a4e6f22c1?q=80&w=1200&auto=format&fit=crop', + }, + { + id: 'c5', + title: '원자력 운전 개론', + meta: 'VOD • 초급 • 3시간 00분', + image: + 'https://images.unsplash.com/photo-1581090463520-5d09f3c456d2?q=80&w=1200&auto=format&fit=crop', + }, + { + id: 'c6', + title: '안전 표지와 표준', + meta: 'VOD • 초급 • 2시간 40분', + image: + 'https://images.unsplash.com/photo-1470167290877-7d5d3446de4c?q=80&w=1200&auto=format&fit=crop', + }, + { + id: 'c7', + title: '발전소 운영', + meta: 'VOD • 중급 • 4시간 20분', + image: + 'https://images.unsplash.com/photo-1581092160562-40aa08e78837?q=80&w=1200&auto=format&fit=crop', + }, + { + id: 'c8', + title: '방사선 안전 실습', + meta: 'VOD • 중급 • 3시간 30분', + image: + 'https://images.unsplash.com/photo-1581093458791-9d181f5842fd?q=80&w=1200&auto=format&fit=crop', + }, + { + id: 'c9', + title: '실험실 안전', + meta: 'VOD • 초급 • 2시간 10분', + image: + 'https://images.unsplash.com/photo-1559757175-08c6d5b3f4b4?q=80&w=1200&auto=format&fit=crop', + }, + { + id: 'c10', + title: '기초 장비 운용', + meta: 'VOD • 초급 • 2시간 50분', + image: + 'https://images.unsplash.com/photo-1581092338398-16e5b28a2b13?q=80&w=1200&auto=format&fit=crop', + }, + ] as Array<{ id: string; title: string; meta: string; image: string }>, + [] + ); + const noticeRows = useMemo( + () => + [ + { id: 5, title: '(공지)시스템 개선이 완료되었...', date: '2025-09-10', views: 1320, writer: '운영팀' }, + { id: 4, title: '(공지)서버 점검 안내(9/10 새벽)', date: '2025-09-10', views: 1210, writer: '운영팀' }, + { id: 3, title: '(공지)서비스 개선 안내', date: '2025-09-10', views: 1230, writer: '운영팀' }, + { id: 2, title: '(공지)시장점검 공지', date: '2025-09-10', views: 1320, writer: '관리자' }, + { id: 1, title: '뉴: 봉사시간 안내 및 한눈에 보는 현황 정리', date: '2025-08-28', views: 594, writer: '운영팀' }, + ], + [] + ); + + // NOTE: 실제 이미지 자산 연결 시 해당 src를 교체하세요. + const slides = useMemo( + () => [ + { + id: 1, + title: '시스템 점검 안내', + description: '11월 10일 새벽 2시~4시 시스템 점검이 진행됩니다.', + imageSrc: + 'https://images.unsplash.com/photo-1581091226825-a6a2a5aee158?q=80&w=1600&auto=format&fit=crop', + }, + { + id: 2, + title: '신규 과정 오픈', + description: '최신 커리큘럼으로 업스킬링하세요.', + imageSrc: + 'https://images.unsplash.com/photo-1550602921-a0d9a4d5b1a9?q=80&w=1600&auto=format&fit=crop', + }, + { + id: 3, + title: '수강 이벤트', + description: '이번 달 수강 혜택을 확인해보세요.', + imageSrc: + 'https://images.unsplash.com/photo-1545235617-9465d2a55698?q=80&w=1600&auto=format&fit=crop', + }, + ], + [] + ); + + useEffect(() => { + const containerEl = containerRef.current; + if (!containerEl) return; + + const handleScroll = () => { + const width = containerEl.clientWidth; + const index = Math.round(containerEl.scrollLeft / Math.max(width, 1)); + setCurrentIndex(index); + }; + + containerEl.addEventListener('scroll', handleScroll, { passive: true }); + return () => { + containerEl.removeEventListener('scroll', handleScroll); + }; + }, []); + + const scrollToIndex = (index: number) => { + const containerEl = containerRef.current; + if (!containerEl) return; + const clamped = Math.max(0, Math.min(index, slides.length - 1)); + const width = containerEl.clientWidth; + containerEl.scrollTo({ left: clamped * width, behavior: 'smooth' }); + }; + + const handlePrev = () => scrollToIndex(currentIndex - 1); + const handleNext = () => scrollToIndex(currentIndex + 1); + + return ( +
+
+ {/* 메인 컨테이너 */} +
+
+ {/* 배너 + 사이드 */} +
+ {/* 배너 */} +
+
+
+ + {slides.map((slide) => ( +
+ {slide.title} +
+
+
{slide.title}
+
+ {slide.description} +
+
+
+ ))} +
+ + {/* 좌/우 내비게이션 버튼 */} + + + + {/* 인디케이터 */} +
+ {slides.map((_, idx) => ( + + ))} +
+
+
+ + {/* 사이드 패널 (피그마 디자인 적용) */} + +
+ + {/* 교육 과정 */} +
+
+
+

+ 교육 과정 +

+ 총 28건 +
+ + 전체보기 › + +
+ +
+ {courseCards.map((c) => ( +
+
+ {c.title} +
+
+
+ {c.title} +
+
{c.meta}
+
+
+ ))} +
+
+ + {/* 공지사항 */} +
+
+
+

+ 공지사항 +

+ 총 102건 +
+ + 전체보기 › + +
+ +
+
+ {['번호', '제목', '작성일', '조회수', '작성자'].map((h) => ( +
+ {h} +
+ ))} +
+ +
+ {noticeRows.map((r, idx) => ( +
+
{r.id}
+
+ {r.title} +
+
{r.date}
+
{r.views}
+
{r.writer}
+
+ ))} +
+
+
+
+
+
+ + {/* 푸터 */} +
); } diff --git a/src/app/register/RegisterDone.tsx b/src/app/register/RegisterDone.tsx index 5c43ce7..b87ef0f 100644 --- a/src/app/register/RegisterDone.tsx +++ b/src/app/register/RegisterDone.tsx @@ -55,7 +55,7 @@ export default function RegisterDone({ @@ -69,7 +69,7 @@ export default function RegisterDone({