diff --git a/public/imgs/talk.png b/public/imgs/talk.png new file mode 100644 index 0000000..a4c3654 Binary files /dev/null and b/public/imgs/talk.png differ diff --git a/src/app/NavBar.tsx b/src/app/NavBar.tsx index 1757d65..5af8a86 100644 --- a/src/app/NavBar.tsx +++ b/src/app/NavBar.tsx @@ -4,6 +4,7 @@ import Link from "next/link"; import { useEffect, useRef, useState } from "react"; import { usePathname } from "next/navigation"; import MainLogoSvg from "./svgs/mainlogosvg"; +import ChevronDownSvg from "./svgs/chevrondownsvg"; const NAV_ITEMS = [ { label: "교육 과정 목록", href: "/menu" }, @@ -77,12 +78,14 @@ export default function NavBar() { onClick={() => setIsUserMenuOpen((v) => !v)} aria-haspopup="menu" aria-expanded={isUserMenuOpen} - className="flex items-center gap-1 px-4 py-2 text-[16px] font-semibold text-white" + className="flex items-center gap-1 px-4 py-2 text-[16px] font-semibold text-white cursor-pointer" > 김이름 - - - + {isUserMenuOpen && (
- + +
+ + {/* body */} +
+
+

+ 회원 탈퇴 시 유의사항을 확인해주세요. +

+
+

- 탈퇴 후에도 재가입은 가능합니다.

+

- 수강 및 학습 이력이 모두 삭제되며, 복구가 불가능합니다.

+

- 수강 서비스 이용 권한이 즉시 종료됩니다.

+
+
+
+ + {/* footer */} +
+ + +
+ + + ); +} + + diff --git a/src/app/menu/ChangePasswordModal.tsx b/src/app/menu/ChangePasswordModal.tsx index 765a1ae..9804073 100644 --- a/src/app/menu/ChangePasswordModal.tsx +++ b/src/app/menu/ChangePasswordModal.tsx @@ -1,27 +1,74 @@ 'use client'; -import { useState } from "react"; +import { useEffect, useState } from "react"; +import ModalCloseSvg from "../svgs/closexsvg"; type Props = { open: boolean; onClose: () => void; - onSubmit?: (payload: { email: string; code: string; newPassword: string }) => void; + onSubmit?: (payload: { email: string; code?: string; newPassword: string }) => void; + showVerification?: boolean; + devVerificationState?: 'initial' | 'sent' | 'verified' | 'failed'; }; -export default function ChangePasswordModal({ open, onClose, onSubmit }: Props) { +export default function ChangePasswordModal({ open, onClose, onSubmit, showVerification = false, devVerificationState }: 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); // 인증번호 오류 등 + const [requireCode, setRequireCode] = useState(showVerification); + const [isCodeSent, setIsCodeSent] = useState(showVerification); + const canConfirm = code.trim().length > 0; + const [isVerified, setIsVerified] = useState(false); + const hasError = !!error; + + // 외부에서 전달된 개발모드 상태(devVerificationState)에 따라 UI 동기화 + useEffect(() => { + if (!devVerificationState) return; + switch (devVerificationState) { + case 'initial': + setRequireCode(false); + setIsCodeSent(false); + setCode(""); + setError(null); + setIsVerified(false); + break; + case 'sent': + setRequireCode(true); + setIsCodeSent(true); + setCode(""); + setError(null); + setIsVerified(false); + break; + case 'verified': + setRequireCode(true); + setIsCodeSent(true); + setCode("123456"); + setError(null); + setIsVerified(true); + break; + case 'failed': + setRequireCode(true); + setIsCodeSent(true); + setCode(""); + setError("올바르지 않은 인증번호입니다. 인증번호를 확인해주세요."); + setIsVerified(false); + break; + default: + break; + } + }, [devVerificationState]); if (!open) return null; const handleSubmit = () => { setError(null); - if (!code) { - setError("인증번호를 입력해 주세요."); - return; + if (requireCode) { + if (!code) { + setError("인증번호를 입력해 주세요."); + return; + } } if (!newPassword || !confirmPassword) { setError("새 비밀번호를 입력해 주세요."); @@ -31,7 +78,7 @@ export default function ChangePasswordModal({ open, onClose, onSubmit }: Props) setError("새 비밀번호가 일치하지 않습니다."); return; } - onSubmit?.({ email, code, newPassword }); + onSubmit?.({ email, code: requireCode ? code : undefined, newPassword }); onClose(); }; @@ -49,11 +96,9 @@ export default function ChangePasswordModal({ open, onClose, onSubmit }: Props) type="button" aria-label="닫기" onClick={onClose} - className="inline-flex size-6 items-center justify-center text-[#333c47] hover:opacity-80" + className="inline-flex size-6 items-center justify-center text-[#333c47] hover:opacity-80 cursor-pointer" > - - - + @@ -66,47 +111,75 @@ export default function ChangePasswordModal({ open, onClose, onSubmit }: Props) type="email" value={email} onChange={(e) => 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" + className={[ + "h-10 flex-1 rounded-[8px] border border-[#dee1e6] px-3 text-[16px] leading-[1.5] text-[#333c47] placeholder-[#b1b8c0] outline-none", + hasError ? "bg-white" : isCodeSent ? "bg-neutral-50" : "bg-white", + ].join(" ")} 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자리" - /> - + {requireCode ? ( +
+
인증번호
+
+ 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="인증번호를 입력해 주세요." + /> + +
+ {isCodeSent && !hasError && !isVerified ? ( +
+

+ 인증 확인을 위해 작성한 이메일로 인증번호를 발송했습니다. +

+

+ 이메일을 확인해 주세요. +

+
+ ) : null} + {error ? ( +

+ {error} +

+ ) : null}
- {error ? ( -

- {error} -

- ) : null} -
+ ) : 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" + disabled={!isVerified} + className={[ + "h-10 rounded-[8px] border border-[#dee1e6] px-3 text-[16px] leading-[1.5] text-[#333c47] placeholder-[#b1b8c0] outline-none", + isVerified ? "bg-white" : "bg-neutral-50", + ].join(" ")} placeholder="새 비밀번호" />
@@ -118,7 +191,11 @@ export default function ChangePasswordModal({ open, onClose, onSubmit }: Props) type="password" value={confirmPassword} onChange={(e) => 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" + disabled={!isVerified} + className={[ + "h-10 rounded-[8px] border border-[#dee1e6] px-3 text-[16px] leading-[1.5] text-[#333c47] placeholder-[#b1b8c0] outline-none", + isVerified ? "bg-white" : "bg-neutral-50", + ].join(" ")} placeholder="새 비밀번호 확인" />
@@ -129,14 +206,14 @@ export default function ChangePasswordModal({ open, onClose, onSubmit }: Props) diff --git a/src/app/menu/PasswordChangeDoneModal.tsx b/src/app/menu/PasswordChangeDoneModal.tsx new file mode 100644 index 0000000..6e0602b --- /dev/null +++ b/src/app/menu/PasswordChangeDoneModal.tsx @@ -0,0 +1,56 @@ +'use client'; + +import Link from "next/link"; +import ModalCloseSvg from "../svgs/closexsvg"; + +type Props = { + open: boolean; + onClose: () => void; +}; + +export default function PasswordChangeDoneModal({ open, onClose }: Props) { + if (!open) return null; + + return ( +
+
+
+ +
+ +
+
+

+ 비밀번호 변경이 완료됐습니다. +

+

+ 새로운 비밀번호로 다시 로그인 해주세요. +

+
+
+ +
+ + 로그인 + +
+
+
+ ); +} + + diff --git a/src/app/menu/account/MenuAccountOption.tsx b/src/app/menu/account/MenuAccountOption.tsx new file mode 100644 index 0000000..4db8cc1 --- /dev/null +++ b/src/app/menu/account/MenuAccountOption.tsx @@ -0,0 +1,151 @@ +'use client'; + +import { useState } from "react"; + +type VerificationState = 'initial' | 'sent' | 'verified' | 'failed' | 'changed'; + +type MenuAccountOptionProps = { + verificationState: VerificationState; + setVerificationState: (state: VerificationState) => void; + deleteOpen?: boolean; + setDeleteOpen?: (open: boolean) => void; +}; + +export default function MenuAccountOption({ + verificationState, + setVerificationState, + deleteOpen, + setDeleteOpen, +}: MenuAccountOptionProps) { + const [isOpen, setIsOpen] = useState(false); + + const itemClass = (active: boolean) => + [ + "relative inline-flex h-6 w-11 items-center rounded-full transition-colors", + active ? "bg-blue-600" : "bg-gray-300", + ].join(" "); + + const knobClass = (active: boolean) => + [ + "inline-block h-5 w-5 transform rounded-full bg-white transition", + active ? "translate-x-5" : "translate-x-1", + ].join(" "); + + const is = { + initial: verificationState === 'initial', + sent: verificationState === 'sent', + verified: verificationState === 'verified', + failed: verificationState === 'failed', + changed: verificationState === 'changed', + }; + + return ( +
+ + {isOpen && ( +
+
+ +
+
+

현재 상태: {verificationState}

+
+
    +
  • +

    초기 상태

    + +
  • +
  • +

    인증번호 전송 상태

    + +
  • +
  • +

    인증 완료 상태

    + +
  • +
  • +

    인증 실패 상태

    + +
  • +
+
+
  • +

    비밀번호 변경 완료 상태

    + +
  • +
  • +

    회원 탈퇴 모달

    + +
  • +
    +
    +
    +
    + )} +
    + ); +} + + diff --git a/src/app/menu/account/page.tsx b/src/app/menu/account/page.tsx index 47cfb20..1431200 100644 --- a/src/app/menu/account/page.tsx +++ b/src/app/menu/account/page.tsx @@ -1,10 +1,23 @@ 'use client'; -import { useState } from "react"; +import { useEffect, useState } from "react"; import ChangePasswordModal from "../ChangePasswordModal"; +import PasswordChangeDoneModal from "../PasswordChangeDoneModal"; +import AccountDeleteModal from "../AccountDeleteModal"; +import MenuAccountOption from "@/app/menu/account/MenuAccountOption"; + +type VerificationState = 'initial' | 'sent' | 'verified' | 'failed' | 'changed'; export default function AccountPage() { const [open, setOpen] = useState(false); + const [verificationState, setVerificationState] = useState('initial'); + const [doneOpen, setDoneOpen] = useState(false); + const [deleteOpen, setDeleteOpen] = useState(false); + + // 개발 옵션에서 'changed'로 전환하면 완료 모달 표시 + useEffect(() => { + setDoneOpen(verificationState === 'changed'); + }, [verificationState]); return (
    @@ -40,7 +53,11 @@ export default function AccountPage() {
    -
    @@ -52,6 +69,28 @@ export default function AccountPage() { onSubmit={() => { // TODO: integrate API }} + devVerificationState={verificationState} + /> + + + + setDoneOpen(false)} + /> + + setDeleteOpen(false)} + onConfirm={() => { + // TODO: 탈퇴 API 연동 + setDeleteOpen(false); + }} />
    ); diff --git a/src/app/menu/courses/CourseCard.tsx b/src/app/menu/courses/CourseCard.tsx new file mode 100644 index 0000000..100309b --- /dev/null +++ b/src/app/menu/courses/CourseCard.tsx @@ -0,0 +1,131 @@ +'use client'; + +import Image from "next/image"; +import { useState } from "react"; + +type Lesson = { + id: string; + title: string; + durationMin: number; + progressPct: number; // 0~100 + isCompleted: boolean; +}; + +type Course = { + id: string; + title: string; + description: string; + thumbnail: string; + status: "전체" | "수강 예정" | "수강중" | "수강 완료"; + progressPct: number; + lessons: Lesson[]; +}; + +function ProgressBar({ value }: { value: number }) { + const pct = Math.max(0, Math.min(100, value)); + return ( +
    +
    +
    + ); +} + +export default function CourseCard({ course }: { course: Course }) { + const [open, setOpen] = useState(false); + + return ( +
    +
    +
    + +
    +
    +
    + + {course.status} + +

    + {course.title} +

    +
    +

    {course.description}

    +
    + + + 진행률 {course.progressPct}% + +
    +
    +
    + + +
    +
    + + {open ? ( +
    +
      + {course.lessons.map((lesson, idx) => ( +
    • +
      +
      +
      + + {idx + 1}. + +

      + {lesson.title} +

      +
      +
      + + + {lesson.progressPct}% · {lesson.durationMin}분 + +
      +
      +
      + {lesson.isCompleted ? ( + + 수료 + + ) : ( + + )} +
      +
      +
    • + ))} +
    +
    + ) : null} +
    + ); +} + + diff --git a/src/app/menu/courses/CourseGridItem.tsx b/src/app/menu/courses/CourseGridItem.tsx new file mode 100644 index 0000000..bc7b2da --- /dev/null +++ b/src/app/menu/courses/CourseGridItem.tsx @@ -0,0 +1,61 @@ +'use client'; + +import Image from "next/image"; + +export type CourseGridItemProps = { + id: string; + thumbnail: string; + title: string; + isNew?: boolean; + category?: string; // ex) 안전, 공정 등 + meta?: string; // ex) "VOD · 총 6강 · 4시간 20분" +}; + +export default function CourseGridItem({ + id, + thumbnail, + title, + isNew, + category, + meta, +}: CourseGridItemProps) { + return ( +
  • +
    + +
    +
    +
    + {category ? ( + + {category} + + ) : null} + {isNew ? ( + + NEW + + ) : null} +
    +

    + {title} +

    + {meta ? ( +

    {meta}

    + ) : null} +
    +
  • + ); +} + + diff --git a/src/app/menu/courses/page.tsx b/src/app/menu/courses/page.tsx index c54bede..991ee26 100644 --- a/src/app/menu/courses/page.tsx +++ b/src/app/menu/courses/page.tsx @@ -1,14 +1,179 @@ +'use client'; + +import { useMemo, useState } from "react"; +import CourseCard from "./CourseCard"; + +type CourseStatus = "전체" | "수강 예정" | "수강중" | "수강 완료"; + +type Lesson = { + id: string; + title: string; + durationMin: number; + progressPct: number; // 0~100 + isCompleted: boolean; +}; + +type Course = { + id: string; + title: string; + description: string; + thumbnail: string; + status: CourseStatus; + progressPct: number; + lessons: Lesson[]; +}; + +const MOCK_COURSES: Course[] = [ + { + id: "c1", + title: "원자재 출입 전 계동", + description: + "원자재 이송/보관 기준을 기반으로 가이드하며, 일련의 단계 별 강의에서 세부적인 작업 지침을 다룹니다.", + thumbnail: "/imgs/talk.png", + status: "수강중", + progressPct: 80, + lessons: [ + { id: "c1l1", title: "1. 운반과 기준 및 방출 절차", durationMin: 12, progressPct: 100, isCompleted: true }, + { id: "c1l2", title: "2. 라벨링 원칙과 현장 케어", durationMin: 9, progressPct: 100, isCompleted: true }, + { id: "c1l3", title: "3. 배치/현황 기록 및 문서 관리", durationMin: 15, progressPct: 60, isCompleted: false }, + { id: "c1l4", title: "4. 보관 적재 기준 점검", durationMin: 8, progressPct: 0, isCompleted: false }, + { id: "c1l5", title: "5. 입고 검사 방법 (AQL) 및 유의점", durationMin: 11, progressPct: 0, isCompleted: false }, + { id: "c1l6", title: "6. 장비 사용, 손질 및 일지 필기", durationMin: 13, progressPct: 0, isCompleted: false }, + ], + }, + { + id: "c2", + title: "학점과 평가", + description: + "학점과 평가 항목 기준을 가이드하며, 일과 관련된 기본 평가 체계와 피드백 처리 방법에 대해 배웁니다.", + thumbnail: "/imgs/talk.png", + status: "수강 완료", + progressPct: 100, + lessons: [ + { id: "c2l1", title: "평가 기준 이해", durationMin: 10, progressPct: 100, isCompleted: true }, + ], + }, + { + id: "c3", + title: "부서간 연협", + description: + "부서간 협업 절차와 기록 기준을 가이드하며, 일련 단계 별 협업에서 생길 수 있는 리스크 관리법을 다룹니다.", + thumbnail: "/imgs/talk.png", + status: "수강중", + progressPct: 60, + lessons: [ + { id: "c3l1", title: "의사소통 원칙", durationMin: 9, progressPct: 100, isCompleted: true }, + { id: "c3l2", title: "문서 공유와 승인", durationMin: 14, progressPct: 30, isCompleted: false }, + ], + }, + { + id: "c4", + title: "작업별 방사선의 이해", + description: + "작업별 방사선 안전 기준을 가이드하며, 일과 관련된 위험과 보호 장비 선택법을 배웁니다.", + thumbnail: "/imgs/talk.png", + status: "수강 예정", + progressPct: 0, + lessons: [ + { id: "c4l1", title: "기초 이론", durationMin: 12, progressPct: 0, isCompleted: false }, + ], + }, +]; + +const TABS: CourseStatus[] = ["전체", "수강 예정", "수강중", "수강 완료"]; + export default function CoursesPage() { - return ( -
    -
    -

    내 강좌실

    -
    -
    -

    콘텐츠 준비 중입니다.

    -
    -
    - ); + const [activeTab, setActiveTab] = useState("전체"); + + const countsByStatus = useMemo(() => { + return { + 전체: MOCK_COURSES.length, + "수강 예정": MOCK_COURSES.filter((c) => c.status === "수강 예정").length, + 수강중: MOCK_COURSES.filter((c) => c.status === "수강중").length, + "수강 완료": MOCK_COURSES.filter((c) => c.status === "수강 완료").length, + }; + }, []); + + const filtered = useMemo(() => { + if (activeTab === "전체") return MOCK_COURSES; + return MOCK_COURSES.filter((c) => c.status === activeTab); + }, [activeTab]); + + return ( +
    +
    +

    내 강좌실

    +
    + +
    +
    +
      + {TABS.map((tab) => { + const isActive = activeTab === tab; + return ( +
    • + +
    • + ); + })} +
    +
    +
    + +
    +
    + {filtered.map((course) => ( + + ))} +
    + + {/* pagination */} +
    + + {[1, 2, 3].map((p) => ( + + ))} + +
    +
    +
    + ); } diff --git a/src/app/menu/layout.tsx b/src/app/menu/layout.tsx index c457839..41cb205 100644 --- a/src/app/menu/layout.tsx +++ b/src/app/menu/layout.tsx @@ -3,7 +3,7 @@ import MenuSidebar from "./MenuSidebar"; export default function MenuLayout({ children }: { children: ReactNode }) { return ( -
    +
    diff --git a/src/app/menu/results/page.tsx b/src/app/menu/results/page.tsx index bf31bc8..eec1766 100644 --- a/src/app/menu/results/page.tsx +++ b/src/app/menu/results/page.tsx @@ -1,12 +1,118 @@ +type ResultRow = { + programTitle: string; + courseTitle: string; + completedAt: string; + score: string; + instructor: string; + feedbackLink?: string; + certificateLink?: string; +}; + +const mockResults: ResultRow[] = [ + { + programTitle: "XR 안전 기본 과정", + courseTitle: "{강좌명}", + completedAt: "2025-09-10", + score: "-", + instructor: "이공필", + }, + { + programTitle: "건설 현장 안전 실무", + courseTitle: "{강좌명}", + completedAt: "2025-09-10", + score: "70 / 100", + instructor: "이공필", + feedbackLink: "#", + certificateLink: "#", + }, + { + programTitle: "전기 설비 위험성 평가", + courseTitle: "{강좌명}", + completedAt: "2025-09-10", + score: "70 / 100", + instructor: "이공필", + feedbackLink: "#", + certificateLink: "#", + }, +]; + export default function ResultsPage() { return (

    학습 결과

    -
    -

    콘텐츠 준비 중입니다.

    -
    + +
    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + {mockResults.map((row, idx) => ( + + + + + + + + + + ))} + +
    교육 과정명강좌명수강 완료일평가 점수(점)평가 강사피드백수료증
    + {row.programTitle} + + {row.courseTitle} + + {row.completedAt} + + {row.score} + + {row.instructor} + + {row.feedbackLink ? ( + + 확인하기 + + ) : ( + "-" + )} + + {row.certificateLink ? ( + + 확인하기 + + ) : ( + "-" + )} +
    +
    +
    +
    ); } diff --git a/src/app/page.tsx b/src/app/page.tsx index d87ac11..68f2e71 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -3,10 +3,12 @@ import { useEffect, useMemo, useRef, useState } from 'react'; import MainLogoSvg from './svgs/mainlogosvg'; +import ChevronDownSvg from './svgs/chevrondownsvg'; export default function Home() { const containerRef = useRef(null); const [currentIndex, setCurrentIndex] = useState(0); + const [isNameActive, setIsNameActive] = useState(false); // 코스, 공지사항 더미 데이터 const courseCards = useMemo( @@ -16,71 +18,61 @@ export default function Home() { id: 'c1', title: '원자력 운영 기초', meta: 'VOD • 초급 • 4시간 20분', - image: - 'https://images.unsplash.com/photo-1555949963-ff9fe0c870eb?q=80&w=1200&auto=format&fit=crop', + image: 'https://picsum.photos/seed/xrlms-c1/1200/800', }, { id: 'c2', title: '반도체', meta: 'VOD • 중급 • 3시간 10분', - image: - 'https://images.unsplash.com/photo-1581092921461-eab62e97a780?q=80&w=1200&auto=format&fit=crop', + image: 'https://picsum.photos/seed/xrlms-c2/1200/800', }, { id: 'c3', title: '방사선 안전', meta: 'VOD • 중급 • 4시간 20분', - image: - 'https://images.unsplash.com/photo-1581090464777-f3220bbe1b8b?q=80&w=1200&auto=format&fit=crop', + image: 'https://picsum.photos/seed/xrlms-c3/1200/800', }, { id: 'c4', title: '방사선 폐기물', meta: 'VOD • 중급 • 4시간 20분', - image: - 'https://images.unsplash.com/photo-1581091220351-5a6a4e6f22c1?q=80&w=1200&auto=format&fit=crop', + image: 'https://picsum.photos/seed/xrlms-c4/1200/800', }, { id: 'c5', title: '원자력 운전 개론', meta: 'VOD • 초급 • 3시간 00분', - image: - 'https://images.unsplash.com/photo-1581090463520-5d09f3c456d2?q=80&w=1200&auto=format&fit=crop', + image: 'https://picsum.photos/seed/xrlms-c5/1200/800', }, { id: 'c6', title: '안전 표지와 표준', meta: 'VOD • 초급 • 2시간 40분', - image: - 'https://images.unsplash.com/photo-1470167290877-7d5d3446de4c?q=80&w=1200&auto=format&fit=crop', + image: 'https://picsum.photos/seed/xrlms-c6/1200/800', }, { id: 'c7', title: '발전소 운영', meta: 'VOD • 중급 • 4시간 20분', - image: - 'https://images.unsplash.com/photo-1581092160562-40aa08e78837?q=80&w=1200&auto=format&fit=crop', + image: 'https://picsum.photos/seed/xrlms-c7/1200/800', }, { id: 'c8', title: '방사선 안전 실습', meta: 'VOD • 중급 • 3시간 30분', - image: - 'https://images.unsplash.com/photo-1581093458791-9d181f5842fd?q=80&w=1200&auto=format&fit=crop', + image: 'https://picsum.photos/seed/xrlms-c8/1200/800', }, { id: 'c9', title: '실험실 안전', meta: 'VOD • 초급 • 2시간 10분', - image: - 'https://images.unsplash.com/photo-1559757175-08c6d5b3f4b4?q=80&w=1200&auto=format&fit=crop', + image: 'https://picsum.photos/seed/xrlms-c9/1200/800', }, { id: 'c10', title: '기초 장비 운용', meta: 'VOD • 초급 • 2시간 50분', - image: - 'https://images.unsplash.com/photo-1581092338398-16e5b28a2b13?q=80&w=1200&auto=format&fit=crop', + image: 'https://picsum.photos/seed/xrlms-c10/1200/800', }, ] as Array<{ id: string; title: string; meta: string; image: string }>, [] @@ -104,22 +96,19 @@ export default function Home() { id: 1, title: '시스템 점검 안내', description: '11월 10일 새벽 2시~4시 시스템 점검이 진행됩니다.', - imageSrc: - 'https://images.unsplash.com/photo-1581091226825-a6a2a5aee158?q=80&w=1600&auto=format&fit=crop', + imageSrc: 'https://picsum.photos/seed/xrlms-slide1/1600/900', }, { id: 2, title: '신규 과정 오픈', description: '최신 커리큘럼으로 업스킬링하세요.', - imageSrc: - 'https://images.unsplash.com/photo-1550602921-a0d9a4d5b1a9?q=80&w=1600&auto=format&fit=crop', + imageSrc: 'https://picsum.photos/seed/xrlms-slide2/1600/900', }, { id: 3, title: '수강 이벤트', description: '이번 달 수강 혜택을 확인해보세요.', - imageSrc: - 'https://images.unsplash.com/photo-1545235617-9465d2a55698?q=80&w=1600&auto=format&fit=crop', + imageSrc: 'https://picsum.photos/seed/xrlms-slide3/1600/900', }, ], [] @@ -152,29 +141,41 @@ export default function Home() { const handlePrev = () => scrollToIndex(currentIndex - 1); const handleNext = () => scrollToIndex(currentIndex + 1); + const handleNameClick = () => { + setIsNameActive((prev) => !prev); + }; + return (
    {/* 메인 컨테이너 */}
    -
    +
    {/* 배너 + 사이드 */} -
    +
    {/* 배너 */} -
    +
    {slides.map((slide) => ( -
    - {slide.title} -
    -
    -
    {slide.title}
    -
    +
    + {slide.title} { + const t = e.currentTarget as HTMLImageElement; + if (!t.src.includes('picsum.photos')) t.src = 'https://picsum.photos/seed/xrlms-fallback-slide/1600/900'; + }} + /> +
    +
    +
    {slide.title}
    +
    {slide.description}
    @@ -188,27 +189,28 @@ export default function Home() { onClick={handlePrev} aria-label="이전 배너" disabled={currentIndex <= 0} - className="absolute left-2 top-1/2 -translate-y-1/2 w-9 h-9 rounded-full bg-white/90 text-[#1B2027] border border-[#DEE1E6] flex items-center justify-center cursor-pointer shadow disabled:opacity-60" + className="absolute left-4 top-1/2 -translate-y-1/2 size-12 rounded-full bg-white/20 text-white flex items-center justify-center cursor-pointer disabled:opacity-60" > - ‹ + + + {/* 인디케이터 */} -
    +
    {slides.map((_, idx) => ( - + ))}
    @@ -217,12 +219,24 @@ export default function Home() { {/* 사이드 패널 (피그마 디자인 적용) */}
    - {/* 푸터 */} - + {/* 전역 Footer는 layout.tsx에서 렌더링됩니다. */}
    ); } diff --git a/src/app/resources/page.tsx b/src/app/resources/page.tsx new file mode 100644 index 0000000..ac04d01 --- /dev/null +++ b/src/app/resources/page.tsx @@ -0,0 +1,153 @@ +'use client'; + +type ResourceRow = { + id: number; + title: string; + date: string; + views: number; + writer: string; + hasAttachment?: boolean; +}; + +const rows: ResourceRow[] = [ + { + id: 6, + title: '방사선과 물질의 상호작용 관련 학습 자료', + date: '2025-06-28', + views: 1230, + writer: '강민재', + hasAttachment: true, + }, + { + id: 5, + title: '감마선과 베타선의 특성 및 차이 분석 자료', + date: '2025-06-28', + views: 594, + writer: '강민재', + }, + { + id: 4, + title: '방사선량 단위(Sv, Gy) 비교 및 계산 예제', + date: '2025-06-28', + views: 1230, + writer: '강민재', + }, + { + id: 3, + title: '의료 영상 촬영 시 방사선 안전 수칙 가이드', + date: '2025-06-28', + views: 1230, + writer: '강민재', + }, + { + id: 2, + title: 'X선 발생 원리 및 특성에 대한 이해 자료', + date: '2025-06-28', + views: 1230, + writer: '강민재', + }, + { + id: 1, + title: '방사선의 기초 개념과 물질과의 상호작용 정리 자료', + date: '2025-06-28', + views: 1230, + writer: '강민재', + }, +]; + +export default function ResourcesPage() { + return ( +
    +
    +
    + {/* 헤더 영역 */} +
    +

    + 학습 자료실 +

    +
    + + {/* 본문 영역 */} +
    + {/* 총 건수 */} +
    +

    + 총 {rows.length}건 +

    +
    + + {/* 표 */} +
    + {/* 헤더 */} +
    +
    + 번호 +
    +
    + 제목 +
    +
    + 게시일 +
    +
    + 조회수 +
    +
    등록자
    +
    + + {/* 바디 */} +
    + {rows.map((r) => ( +
    +
    + {r.id} +
    +
    + {r.title} + {r.hasAttachment && ( + + + + )} +
    +
    + {r.date} +
    +
    + {r.views.toLocaleString()} +
    +
    {r.writer}
    +
    + ))} +
    +
    +
    +
    +
    +
    + ); +} + + diff --git a/src/app/svgs/chevrondownsvg.tsx b/src/app/svgs/chevrondownsvg.tsx new file mode 100644 index 0000000..9bea139 --- /dev/null +++ b/src/app/svgs/chevrondownsvg.tsx @@ -0,0 +1,28 @@ +export default function ChevronDownSvg( + { + width = 24, + height = 24, + className = '', + }: { width?: number | string; height?: number | string; className?: string } +): JSX.Element { + return ( + + + + ); +} + + diff --git a/src/app/svgs/closexsvg.tsx b/src/app/svgs/closexsvg.tsx new file mode 100644 index 0000000..49226eb --- /dev/null +++ b/src/app/svgs/closexsvg.tsx @@ -0,0 +1,16 @@ +"use client"; + +export default function ModalCloseSvg() { + return ( + + + + ); +} + +