From 4dc8304a1dcf0e015ff37f2bf9b5163795a9d1a6 Mon Sep 17 00:00:00 2001 From: mota Date: Tue, 18 Nov 2025 09:09:09 +0900 Subject: [PATCH] =?UTF-8?q?=EB=AF=80=ED=95=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- next.config.ts | 8 + src/app/NavBar.tsx | 8 +- src/app/components/Footer.tsx | 2 +- src/app/course-list/page.tsx | 62 ++++ src/app/menu/MenuSidebar.tsx | 2 +- src/app/menu/courses/CourseCard.tsx | 100 +++-- src/app/menu/courses/CourseGridItem.tsx | 4 +- src/app/menu/courses/[courseId]/page.tsx | 138 +++++++ src/app/menu/courses/page.tsx | 51 ++- src/app/menu/page.tsx | 92 ++++- src/app/menu/results/CertificateModal.tsx | 23 ++ src/app/menu/results/FeedbackModal.tsx | 44 +++ .../menu/results/FigmaCertificateContent.tsx | 346 ++++++++++++++++++ src/app/menu/results/FigmaFeedbackContent.tsx | 122 ++++++ src/app/menu/results/page.tsx | 54 ++- src/app/notices/[id]/page.tsx | 109 ++++++ src/app/notices/page.tsx | 124 +++++++ src/app/resources/[id]/page.tsx | 194 ++++++++++ src/app/resources/page.tsx | 44 ++- src/app/svgs/backcirclesvg.tsx | 46 +++ src/app/svgs/chevrondownsvg.tsx | 2 +- src/app/svgs/paperclipsvg.tsx | 29 ++ 22 files changed, 1497 insertions(+), 107 deletions(-) create mode 100644 src/app/course-list/page.tsx create mode 100644 src/app/menu/courses/[courseId]/page.tsx create mode 100644 src/app/menu/results/CertificateModal.tsx create mode 100644 src/app/menu/results/FeedbackModal.tsx create mode 100644 src/app/menu/results/FigmaCertificateContent.tsx create mode 100644 src/app/menu/results/FigmaFeedbackContent.tsx create mode 100644 src/app/notices/[id]/page.tsx create mode 100644 src/app/notices/page.tsx create mode 100644 src/app/resources/[id]/page.tsx create mode 100644 src/app/svgs/backcirclesvg.tsx create mode 100644 src/app/svgs/paperclipsvg.tsx diff --git a/next.config.ts b/next.config.ts index e9ffa30..fabeeff 100644 --- a/next.config.ts +++ b/next.config.ts @@ -2,6 +2,14 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { /* config options here */ + images: { + remotePatterns: [ + { + protocol: "https", + hostname: "picsum.photos", + }, + ], + }, }; export default nextConfig; diff --git a/src/app/NavBar.tsx b/src/app/NavBar.tsx index 5af8a86..cc5c25d 100644 --- a/src/app/NavBar.tsx +++ b/src/app/NavBar.tsx @@ -7,7 +7,7 @@ import MainLogoSvg from "./svgs/mainlogosvg"; import ChevronDownSvg from "./svgs/chevrondownsvg"; const NAV_ITEMS = [ - { label: "교육 과정 목록", href: "/menu" }, + { label: "교육 과정 목록", href: "/course-list" }, { label: "학습 자료실", href: "/resources" }, { label: "공지사항", href: "/notices" }, ]; @@ -52,14 +52,12 @@ export default function NavBar() {
- + 내 강좌실 +
+ + ))} + + + + ); +} + + diff --git a/src/app/menu/MenuSidebar.tsx b/src/app/menu/MenuSidebar.tsx index 17d583b..836ebc0 100644 --- a/src/app/menu/MenuSidebar.tsx +++ b/src/app/menu/MenuSidebar.tsx @@ -15,7 +15,7 @@ const learningItems: NavItem[] = [ const accountItems: NavItem[] = [ { label: "내 정보 수정", href: "/menu/account" }, - { label: "로그아웃", href: "/logout" }, + { label: "로그아웃", href: "/login" }, ]; export default function MenuSidebar() { diff --git a/src/app/menu/courses/CourseCard.tsx b/src/app/menu/courses/CourseCard.tsx index 100309b..39b0442 100644 --- a/src/app/menu/courses/CourseCard.tsx +++ b/src/app/menu/courses/CourseCard.tsx @@ -2,6 +2,7 @@ import Image from "next/image"; import { useState } from "react"; +import ChevronDownSvg from "../../svgs/chevrondownsvg"; type Lesson = { id: string; @@ -24,7 +25,7 @@ type Course = { function ProgressBar({ value }: { value: number }) { const pct = Math.max(0, Math.min(100, value)); return ( -
+
sum + (l.durationMin || 0), 0); + const totalHours = Math.floor(totalMinutes / 60); + const restMinutes = totalMinutes % 60; + const firstIncomplete = course.lessons.find((l) => !l.isCompleted)?.id; + const cardClassName = [ + "rounded-xl border bg-white shadow-[0_2px_8px_rgba(0,0,0,0.02)]", + open ? "border-[#384fbf]" : "border-[#ecf0ff]", + ].join(" "); + + const formatDuration = (m: number) => { + const minutes = String(m).padStart(2, "0"); + const seconds = String((m * 7) % 60).padStart(2, "0"); + return `${minutes}:${seconds}`; + }; return ( -
+
- +
@@ -52,32 +75,31 @@ export default function CourseCard({ course }: { course: Course }) {

{course.description}

-
+
+

+ VOD · 총 {course.lessons.length}강 · {totalHours}시간 {restMinutes}분 +

+

+ 진행도 {course.progressPct}% +

+
+
- - 진행률 {course.progressPct}% -
-
@@ -86,7 +108,7 @@ export default function CourseCard({ course }: { course: Course }) {
    {course.lessons.map((lesson, idx) => ( -
  • +
  • @@ -98,23 +120,41 @@ export default function CourseCard({ course }: { course: Course }) {

    - - - {lesson.progressPct}% · {lesson.durationMin}분 + + {formatDuration(lesson.durationMin)}
    + {lesson.isCompleted ? ( - - 수료 - + ) : ( )}
    diff --git a/src/app/menu/courses/CourseGridItem.tsx b/src/app/menu/courses/CourseGridItem.tsx index bc7b2da..b10713b 100644 --- a/src/app/menu/courses/CourseGridItem.tsx +++ b/src/app/menu/courses/CourseGridItem.tsx @@ -22,7 +22,7 @@ export default function CourseGridItem({ return (
  • diff --git a/src/app/menu/courses/[courseId]/page.tsx b/src/app/menu/courses/[courseId]/page.tsx new file mode 100644 index 0000000..b5b2c3f --- /dev/null +++ b/src/app/menu/courses/[courseId]/page.tsx @@ -0,0 +1,138 @@ +'use client'; + +import Image from "next/image"; + +type Lesson = { + id: string; + title: string; + duration: string; // "12:46" 형식 + state: "제출완료" | "제출대기"; + action: "복습하기" | "이어서 수강하기" | "수강하기"; +}; + +type CourseDetail = { + id: string; + status: "수강 중" | "수강 예정" | "수강 완료"; + title: string; + goal: string; + method: string; + summary: string; // VOD · 총 n강 · n시간 n분 + submitSummary: string; // 학습 제출 n/n + thumbnail: string; + lessons: Lesson[]; +}; + +const MOCK_DETAIL: CourseDetail = { + id: "c1", + status: "수강 중", + title: "원자로 운전 및 계통", + goal: + "원자로 운전 원리와 주요 계통의 구조 및 기능을 이해하고, 실제 운전 상황을 가상 환경에서 체험하여 문제 해결 능력을 기른다.", + method: + "강좌 동영상을 통해 이론을 학습한 후, XR 실습을 통해 원자로 운전 및 계통 제어 과정을 체험한다. 이후 문제 풀이를 통해 학습 내용을 점검하며 평가를 받아 학습 성취도를 확인한다.", + summary: "VOD · 총 6강 · 4시간 20분", + submitSummary: "학습 제출 3/6", + thumbnail: "https://picsum.photos/seed/course-detail/584/318", + lessons: [ + { id: "l1", title: "1. 원자로 기초 및 핵분열 원리", duration: "12:46", state: "제출완료", action: "복습하기" }, + { id: "l2", title: "2. 제어봉 및 원자로 출력 제어", duration: "18:23", state: "제출완료", action: "복습하기" }, + { id: "l3", title: "3. 터빈-발전기 계통 및 전력 생산", duration: "13:47", state: "제출완료", action: "복습하기" }, + { id: "l4", title: "4. 보조 계통 및 안전 계통", duration: "13:47", state: "제출대기", action: "복습하기" }, + { id: "l5", title: "5. 원자로 냉각재 계통 (RCS) 및 열수력", duration: "08:11", state: "제출대기", action: "이어서 수강하기" }, + { id: "l6", title: "6. 원자로 시동, 운전 및 정지 절차", duration: "13:47", state: "제출대기", action: "수강하기" }, + ], +}; + +export default function CourseDetailPage() { + const c = MOCK_DETAIL; + return ( +
    +
    +

    교육 과정 상세보기

    +
    + +
    +
    + {/* 상단 소개 카드 */} +
    +
    + +
    +
    +
    + + {c.status} + +

    {c.title}

    +
    +
    +

    + 학습 목표: {c.goal} +

    +

    + 학습 방법: {c.method} +

    +
    +
    + {c.summary} + {c.submitSummary} +
    +
    +
    + + {/* 차시 리스트 */} +
    + {c.lessons.map((l) => { + const isSubmitted = l.state === "제출완료"; + const submitBtnStyle = + l.state === "제출완료" + ? "border border-transparent text-[#384fbf]" + : "border " + (l.action === "이어서 수강하기" || l.action === "수강하기" ? "border-[#b1b8c0]" : "border-[#8c95a1]"); + const rightBtnStyle = + l.action === "이어서 수강하기" + ? "bg-[#ecf0ff] text-[#384fbf]" + : l.action === "수강하기" + ? "bg-[#ecf0ff] text-[#384fbf]" + : "bg-[#f1f3f5] text-[#4c5561]"; + return ( +
    +
    +
    +

    {l.title}

    +
    +

    {l.duration}

    +
    +
    +
    + + +
    +
    +
    + ); + })} +
    +
    +
    +
    + ); +} + + diff --git a/src/app/menu/courses/page.tsx b/src/app/menu/courses/page.tsx index 991ee26..7401dcf 100644 --- a/src/app/menu/courses/page.tsx +++ b/src/app/menu/courses/page.tsx @@ -9,7 +9,7 @@ type Lesson = { id: string; title: string; durationMin: number; - progressPct: number; // 0~100 + progressPct: number; isCompleted: boolean; }; @@ -23,41 +23,41 @@ type Course = { lessons: Lesson[]; }; -const MOCK_COURSES: Course[] = [ +const COURSES: Course[] = [ { id: "c1", - title: "원자재 출입 전 계동", + 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: "c1l1", title: "1. 원자로 기초 및 핵분열 원리", durationMin: 12, progressPct: 100, isCompleted: true }, + { id: "c1l2", title: "2. 제어봉 및 원자로 출력 제어", durationMin: 18, progressPct: 100, isCompleted: true }, + { id: "c1l3", title: "3. 터빈-발전기 계통 및 전력 생산", durationMin: 13, progressPct: 60, isCompleted: false }, + { id: "c1l4", title: "4. 보조 계통 및 안전 계통", durationMin: 13, progressPct: 0, isCompleted: false }, + { id: "c1l5", title: "5. 원자로 냉각재 계통 (RCS) 및 열수력", durationMin: 8, progressPct: 0, isCompleted: false }, + { id: "c1l6", title: "6. 원자로 시동, 운전 및 정지 절차", durationMin: 13, progressPct: 0, isCompleted: false }, ], }, { id: "c2", - title: "학점과 평가", + title: "확률론", description: - "학점과 평가 항목 기준을 가이드하며, 일과 관련된 기본 평가 체계와 피드백 처리 방법에 대해 배웁니다.", + "확률과 통계의 주요 개념을 가이드하며, 일련 단계 실습을 기반으로 문제 해결력을 기릅니다.", thumbnail: "/imgs/talk.png", status: "수강 완료", progressPct: 100, lessons: [ - { id: "c2l1", title: "평가 기준 이해", durationMin: 10, progressPct: 100, isCompleted: true }, + { id: "c2l1", title: "기초 개념", durationMin: 10, progressPct: 100, isCompleted: true }, ], }, { id: "c3", - title: "부서간 연협", + title: "부서간 협업", description: - "부서간 협업 절차와 기록 기준을 가이드하며, 일련 단계 별 협업에서 생길 수 있는 리스크 관리법을 다룹니다.", + "부서간 협업 절차와 기록 기준을 가이드하며, 협업 중 생길 수 있는 리스크 관리법을 다룹니다.", thumbnail: "/imgs/talk.png", status: "수강중", progressPct: 60, @@ -68,15 +68,13 @@ const MOCK_COURSES: Course[] = [ }, { id: "c4", - title: "작업별 방사선의 이해", + title: "방사선의 이해", description: - "작업별 방사선 안전 기준을 가이드하며, 일과 관련된 위험과 보호 장비 선택법을 배웁니다.", + "방사선 안전 기준을 가이드하며, 일과 관련된 위험과 보호 장비 선택법을 배웁니다.", thumbnail: "/imgs/talk.png", status: "수강 예정", progressPct: 0, - lessons: [ - { id: "c4l1", title: "기초 이론", durationMin: 12, progressPct: 0, isCompleted: false }, - ], + lessons: [{ id: "c4l1", title: "기초 이론", durationMin: 12, progressPct: 0, isCompleted: false }], }, ]; @@ -87,16 +85,16 @@ export default function CoursesPage() { 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, + 전체: COURSES.length, + "수강 예정": COURSES.filter((c) => c.status === "수강 예정").length, + 수강중: COURSES.filter((c) => c.status === "수강중").length, + "수강 완료": COURSES.filter((c) => c.status === "수강 완료").length, }; }, []); const filtered = useMemo(() => { - if (activeTab === "전체") return MOCK_COURSES; - return MOCK_COURSES.filter((c) => c.status === activeTab); + if (activeTab === "전체") return COURSES; + return COURSES.filter((c) => c.status === activeTab); }, [activeTab]); return ( @@ -176,4 +174,3 @@ export default function CoursesPage() { ); } - diff --git a/src/app/menu/page.tsx b/src/app/menu/page.tsx index e532782..d08cc44 100644 --- a/src/app/menu/page.tsx +++ b/src/app/menu/page.tsx @@ -1,12 +1,86 @@ +'use client'; + +import CourseGridItem, { CourseGridItemProps } from "./courses/CourseGridItem"; + +type CatalogCourse = CourseGridItemProps & { + category: string | undefined; +}; + +// 안정적인 랜덤 이미지를 위해 시드 기반 썸네일 풀 구성 +const THUMBS = Array.from({ length: 16 }).map((_, i) => `https://picsum.photos/seed/xrlms-${i}/640/400`); + +const CATALOG: CatalogCourse[] = Array.from({ length: 20 }).map((_, i) => ({ + id: `cat-${i + 1}`, + title: + i % 5 === 0 + ? "방사선 안전" + : i % 5 === 1 + ? "원자재 운전 및 계동" + : i % 5 === 2 + ? "학점과 평가" + : i % 5 === 3 + ? "방사선 불가물" + : "현장 운전 및 계동", + category: i % 6 === 0 ? "추천" : undefined, + meta: "VOD · 총 6강 · 4시간 20분", + thumbnail: THUMBS[i % THUMBS.length], + isNew: i % 9 === 0, +})); + export default function MenuPage() { - return ( -
    -

    교육 과정 목록

    -

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

    -
    - ); + return ( +
    +
    +

    교육 과정 목록

    +
    + +
    +

    총 {CATALOG.length}건

    +
      + {CATALOG.map((course) => ( + + ))} +
    + + {/* pagination */} +
    + + {[1, 2, 3].map((p) => ( + + ))} + +
    +
    +
    + ); } - diff --git a/src/app/menu/results/CertificateModal.tsx b/src/app/menu/results/CertificateModal.tsx new file mode 100644 index 0000000..e880434 --- /dev/null +++ b/src/app/menu/results/CertificateModal.tsx @@ -0,0 +1,23 @@ +"use client"; + +import React from "react"; +import FigmaCertificateContent from "./FigmaCertificateContent"; + +type Props = { + open: boolean; + onClose: () => void; +}; + +export default function CertificateModal({ open, onClose }: Props) { + if (!open) return null; + return ( +
    + + ); +} + + diff --git a/src/app/menu/results/FeedbackModal.tsx b/src/app/menu/results/FeedbackModal.tsx new file mode 100644 index 0000000..5ae61bb --- /dev/null +++ b/src/app/menu/results/FeedbackModal.tsx @@ -0,0 +1,44 @@ +"use client"; + +import React from "react"; +import FigmaFeedbackContent from "./FigmaFeedbackContent"; + +type Props = { + open: boolean; + onClose: () => void; + learnerName?: string; + instructorName?: string; + scoreText?: string; +}; + +export default function FeedbackModal({ + open, + onClose, + learnerName, + instructorName, + scoreText, +}: Props) { + if (!open) return null; + return ( +
    + + ); +} + + diff --git a/src/app/menu/results/FigmaCertificateContent.tsx b/src/app/menu/results/FigmaCertificateContent.tsx new file mode 100644 index 0000000..348c20b --- /dev/null +++ b/src/app/menu/results/FigmaCertificateContent.tsx @@ -0,0 +1,346 @@ +/* eslint-disable @next/next/no-img-element */ +"use client"; + +import React from "react"; + +type Props = { + onClose: () => void; +}; + +const imgImage1 = "http://localhost:3845/assets/89fda8e949171025b1232bae70fc9d442e4e70c8.png"; +const imgContainer = "http://localhost:3845/assets/d04df6bb7fe1bd29946d04be9442029bca1503b0.png"; +const img = "http://localhost:3845/assets/7adf9a5e43b6c9e5f9bee6adfee64e85eabac44a.svg"; +const img1 = "http://localhost:3845/assets/9e3b52939dbaa99088659a82db437772ef1ad40e.svg"; + +export default function FigmaCertificateContent({ onClose }: Props) { + return ( +
    +
    +
    +

    수료증

    +
    + +
    + +
    +
    +

    + 제 2025-N0055L3 +

    +

    {`수 료 증`}

    +
    + +
    +
    +

    + {`소 속 :`} +

    +
    +
    +

    + XR LMS +

    +
    +
    +
    + +
    +

    + {`성 명 :`} +

    +
    +
    +

    + 김하늘 +

    +
    +
    +
    + +
    +

    + 생 년 월 일 : +

    +
    +
    +

    + 1994-10-17 +

    +
    +
    +
    + +
    +

    + 교 육 과 정 : +

    +
    +
    +

    + (2025년) 방사선작업종사자 직장교육(신규) 9월 +

    +
    +
    +
    + +
    +

    + 교 육 기 간 : +

    +
    +
    +

    + 2025-09-01 ~ 2025-09-30, 4시간 +

    +
    +
    +
    + +
    +

    + 수 료 일 자 : +

    +
    +
    +

    + 2025-09-26 +

    +
    +
    +
    +
    + +
    +
    +

    위 사람은 우리 협회가 진행한

    +

    + 『(2025년) 방사선작업종사자 직장교육(신규)_9월』 + + 과정을 수료하였으므로 이 수료증을 수여함. +

    +
    +

    + 2025년 10월 21일 +

    +
    +
    +
    + +
    +
    +
    +

    XR LMS

    +
    +
    +
    + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + +
    +
    + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    + +
    +
    + + +
    +
    +
    + ); +} + + diff --git a/src/app/menu/results/FigmaFeedbackContent.tsx b/src/app/menu/results/FigmaFeedbackContent.tsx new file mode 100644 index 0000000..8bc92e0 --- /dev/null +++ b/src/app/menu/results/FigmaFeedbackContent.tsx @@ -0,0 +1,122 @@ +/* eslint-disable @next/next/no-img-element */ +"use client"; + +import React from "react"; + +type Props = { + onClose: () => void; + learnerName?: string; + instructorName?: string; + scoreText?: string; +}; + +const img = "http://localhost:3845/assets/7adf9a5e43b6c9e5f9bee6adfee64e85eabac44a.svg"; +const img1 = "http://localhost:3845/assets/498f1d9877c6da3dadf581f98114a7f15bfc6769.svg"; + +export default function FigmaFeedbackContent({ + onClose, + learnerName = "{학습자명}", + instructorName = "이공필", + scoreText = "{점수}", +}: Props) { + return ( +
    +
    +
    +

    {`${learnerName}님의 피드백`}

    +
    + +
    +
    +
    +
    +

    + 평가 강사: {instructorName} +

    +
    +

    + {`- ${learnerName}님, 총점 ${scoreText}점으로 합격하셨습니다. 학습 흐름이 안정적이며 이해도가 좋습니다.`} +

    +
    +
    +
    +
    + +
    +
    +
    + ); +} + + diff --git a/src/app/menu/results/page.tsx b/src/app/menu/results/page.tsx index eec1766..655a74d 100644 --- a/src/app/menu/results/page.tsx +++ b/src/app/menu/results/page.tsx @@ -1,3 +1,9 @@ +"use client"; + +import { useState } from "react"; +import FeedbackModal from "./FeedbackModal"; +import CertificateModal from "./CertificateModal"; + type ResultRow = { programTitle: string; courseTitle: string; @@ -11,14 +17,14 @@ type ResultRow = { const mockResults: ResultRow[] = [ { programTitle: "XR 안전 기본 과정", - courseTitle: "{강좌명}", + courseTitle: "기본 안전 교육", completedAt: "2025-09-10", score: "-", instructor: "이공필", }, { programTitle: "건설 현장 안전 실무", - courseTitle: "{강좌명}", + courseTitle: "비계 설치 점검", completedAt: "2025-09-10", score: "70 / 100", instructor: "이공필", @@ -27,7 +33,7 @@ const mockResults: ResultRow[] = [ }, { programTitle: "전기 설비 위험성 평가", - courseTitle: "{강좌명}", + courseTitle: "분전반 작업 안전", completedAt: "2025-09-10", score: "70 / 100", instructor: "이공필", @@ -37,6 +43,19 @@ const mockResults: ResultRow[] = [ ]; export default function ResultsPage() { + const [isFeedbackOpen, setIsFeedbackOpen] = useState(false); + const [selected, setSelected] = useState(null); + const [isCertOpen, setIsCertOpen] = useState(false); + + function openFeedback(row: ResultRow) { + setSelected(row); + setIsFeedbackOpen(true); + } + function openCertificate(row: ResultRow) { + setSelected(row); + setIsCertOpen(true); + } + return (
    @@ -71,7 +90,7 @@ export default function ResultsPage() { {mockResults.map((row, idx) => ( {row.programTitle} @@ -90,18 +109,26 @@ export default function ResultsPage() { {row.feedbackLink ? ( - + ) : ( "-" )} {row.certificateLink ? ( - + ) : ( "-" )} @@ -113,6 +140,17 @@ export default function ResultsPage() {
    + setIsFeedbackOpen(false)} + learnerName="홍길동" + instructorName={selected?.instructor} + scoreText={selected?.score?.split(" / ")[0] ?? "0"} + /> + setIsCertOpen(false)} + /> ); } diff --git a/src/app/notices/[id]/page.tsx b/src/app/notices/[id]/page.tsx new file mode 100644 index 0000000..0a28ff6 --- /dev/null +++ b/src/app/notices/[id]/page.tsx @@ -0,0 +1,109 @@ +import Link from 'next/link'; +import { notFound } from 'next/navigation'; +import BackCircleSvg from '../../svgs/backcirclesvg'; + +type NoticeItem = { + id: number; + title: string; + date: string; + views: number; + writer: string; + content: string[]; +}; + +const DATA: NoticeItem[] = [ + { + id: 2, + title: '공지사항 제목이 노출돼요', + date: '2025-09-10', + views: 1230, + writer: '문지호', + content: [ + '사이트 이용 관련 주요 변경 사항을 안내드립니다.', + '변경되는 내용은 공지일자로부터 즉시 적용됩니다.', + ], + }, + { + id: 1, + title: '📢 방사선학 온라인 강의 수강 안내 및 필수 공지', + date: '2025-06-28', + views: 594, + writer: '문지호', + content: [ + '온라인 강의 수강 방법과 필수 확인 사항을 안내드립니다.', + '수강 기간 및 출석, 과제 제출 관련 정책을 반드시 확인해 주세요.', + ], + }, +]; + +export default async function NoticeDetailPage({ + params, +}: { + params: Promise<{ id: string }>; +}) { + const { id } = await params; + const numericId = Number(id); + const item = DATA.find((r) => r.id === numericId); + if (!item) return notFound(); + + return ( +
    +
    +
    + {/* 상단 타이틀 */} +
    + + + +

    + 공지사항 상세 +

    +
    + + {/* 카드 */} +
    +
    + {/* 헤더 */} +
    +

    + {item.title} +

    +
    + 작성자 + {item.writer} + + 게시일 + {item.date} + + 조회수 + {item.views.toLocaleString()} +
    +
    + + {/* 구분선 */} +
    + + {/* 본문 */} +
    +
    + {item.content.map((p, idx) => ( +

    + {p} +

    + ))} +
    +
    +
    +
    +
    +
    +
    + ); +} + + + diff --git a/src/app/notices/page.tsx b/src/app/notices/page.tsx new file mode 100644 index 0000000..95d5d04 --- /dev/null +++ b/src/app/notices/page.tsx @@ -0,0 +1,124 @@ +'use client'; + +import { useRouter } from 'next/navigation'; +import PaperClipSvg from '../svgs/paperclipsvg'; + +type NoticeRow = { + id: number; + title: string; + date: string; + views: number; + writer: string; + hasAttachment?: boolean; +}; + +const rows: NoticeRow[] = [ + { + id: 2, + title: '공지사항 제목이 노출돼요', + date: '2025-09-10', + views: 1230, + writer: '문지호', + }, + { + id: 1, + title: '📢 방사선학 온라인 강의 수강 안내 및 필수 공지', + date: '2025-06-28', + views: 594, + writer: '문지호', + hasAttachment: true, + }, +]; + +export default function NoticesPage() { + const router = useRouter(); + + return ( +
    +
    +
    + {/* 헤더 영역 */} +
    +

    + 공지사항 +

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

    + 총 {rows.length}건 +

    +
    + + {/* 표 */} +
    + {/* 헤더 */} +
    +
    + 번호 +
    +
    + 제목 +
    +
    + 게시일 +
    +
    + 조회수 +
    +
    작성자
    +
    + + {/* 바디 */} +
    + {rows.map((r) => ( +
    router.push(`/notices/${r.id}`)} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + router.push(`/notices/${r.id}`); + } + }} + className={[ + 'grid grid-cols-[57px_1fr_140px_140px_140px] h-[48px] text-[15px] font-medium text-[#1B2027] border-t border-[#DEE1E6] hover:bg-[rgba(236,240,255,0.5)] cursor-pointer', + ].join(' ')} + > +
    + {r.id} +
    +
    + {r.title} + {r.hasAttachment && ( + + )} +
    +
    + {r.date} +
    +
    + {r.views.toLocaleString()} +
    +
    {r.writer}
    +
    + ))} +
    +
    +
    +
    +
    +
    + ); +} + + + diff --git a/src/app/resources/[id]/page.tsx b/src/app/resources/[id]/page.tsx new file mode 100644 index 0000000..2070c95 --- /dev/null +++ b/src/app/resources/[id]/page.tsx @@ -0,0 +1,194 @@ +import Link from 'next/link'; +import { notFound } from 'next/navigation'; +import PaperClipSvg from '../../svgs/paperclipsvg'; +import BackCircleSvg from '../../svgs/backcirclesvg'; + +type ResourceRow = { + id: number; + title: string; + date: string; + views: number; + writer: string; + content: string[]; + attachments?: Array<{ name: string; size: string; url: string }>; +}; + +const DATA: ResourceRow[] = [ + { + id: 6, + title: '방사선과 물질의 상호작용 관련 학습 자료', + date: '2025-06-28', + views: 1230, + writer: '강민재', + content: [ + '방사선(Radiation)이 물질 속을 지나갈 때 발생하는 다양한 상호작용(Interaction)의 기본적인 원리를 이해합니다.', + '상호작용의 원리는 방사선 측정, 방사선 이용(의료, 산업), 방사선 차폐 등 방사선 관련 분야의 기본이 됨을 인식합니다.', + '방사선의 종류(광자, 하전입자, 중성자 등) 및 에너지에 따라 물질과의 상호작용 형태가 어떻게 달라지는지 학습합니다.', + ], + attachments: [ + { + name: '[PPT] 방사선-물질 상호작용의 3가지 유형.pptx', + size: '796.35 KB', + url: '#', + }, + ], + }, + { + id: 5, + title: '감마선과 베타선의 특성 및 차이 분석 자료', + date: '2025-06-28', + views: 594, + writer: '강민재', + content: [ + '감마선과 베타선의 발생 원리, 물질과의 상호작용 차이를 비교합니다.', + '차폐 설계 시 고려해야 할 변수들을 사례와 함께 설명합니다.', + ], + }, + { + id: 4, + title: '방사선량 단위(Sv, Gy) 비교 및 계산 예제', + date: '2025-06-28', + views: 1230, + writer: '강민재', + content: ['방사선량 단위 변환 및 예제 계산을 통해 실무 감각을 익힙니다.'], + }, + { + id: 3, + title: '의료 영상 촬영 시 방사선 안전 수칙 가이드', + date: '2025-06-28', + views: 1230, + writer: '강민재', + content: ['촬영 환경에서의 방사선 안전 수칙을 체크리스트 형태로 정리합니다.'], + }, + { + id: 2, + title: 'X선 발생 원리 및 특성에 대한 이해 자료', + date: '2025-06-28', + views: 1230, + writer: '강민재', + content: ['X선의 발생 원리와 에너지 스펙트럼의 특성을 개관합니다.'], + }, + { + id: 1, + title: '방사선의 기초 개념과 물질과의 상호작용 정리 자료', + date: '2025-06-28', + views: 1230, + writer: '강민재', + content: ['방사선 기초 개념을 한눈에 정리한 입문용 자료입니다.'], + }, +]; + +export default async function ResourceDetailPage({ + params, +}: { + params: Promise<{ id: string }>; +}) { + const { id } = await params; + const numericId = Number(id); + const item = DATA.find((r) => r.id === numericId); + if (!item) return notFound(); + + return ( +
    +
    +
    + {/* 상단 타이틀 */} +
    + + + +

    + 학습 자료 상세 +

    +
    + + {/* 카드 */} +
    +
    + {/* 헤더 */} +
    +

    + {item.title} +

    +
    + 작성자 + {item.writer} + + 게시일 + {item.date} + + 조회수 + {item.views.toLocaleString()} +
    +
    + + {/* 구분선 */} +
    + + {/* 본문 */} +
    +
    + {item.content.map((p, idx) => ( +

    + {p} +

    + ))} +
    +
    + + {/* 첨부 파일 */} + {item.attachments?.length ? ( +
    +
    + 첨부 파일 +
    + {item.attachments.map((f, idx) => ( +
    +
    + +
    +
    + + {f.name} + + + {f.size} + +
    + + + + + 다운로드 + +
    + ))} +
    + ) : null} +
    +
    +
    +
    +
    + ); +} + + diff --git a/src/app/resources/page.tsx b/src/app/resources/page.tsx index ac04d01..1ad76c1 100644 --- a/src/app/resources/page.tsx +++ b/src/app/resources/page.tsx @@ -1,5 +1,8 @@ 'use client'; +import { useRouter } from 'next/navigation'; +import PaperClipSvg from '../svgs/paperclipsvg'; + type ResourceRow = { id: number; title: string; @@ -56,6 +59,8 @@ const rows: ResourceRow[] = [ ]; export default function ResourcesPage() { + const router = useRouter(); + return (
    @@ -80,19 +85,19 @@ export default function ResourcesPage() {
    {/* 헤더 */}
    -
    +
    번호
    -
    +
    제목
    -
    +
    게시일
    -
    +
    조회수
    -
    등록자
    +
    등록자
    {/* 바디 */} @@ -100,9 +105,17 @@ export default function ResourcesPage() { {rows.map((r) => (
    router.push(`/resources/${r.id}`)} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + router.push(`/resources/${r.id}`); + } + }} className={[ - 'grid grid-cols-[57px_1fr_140px_140px_140px] h-[48px] text-[15px] font-medium text-[#1B2027] border-t border-[#DEE1E6]', - r.id === rows[0].id ? 'bg-[rgba(236,240,255,0.5)]' : '', + 'grid grid-cols-[57px_1fr_140px_140px_140px] h-[48px] text-[15px] font-medium text-[#1B2027] border-t border-[#DEE1E6] hover:bg-[rgba(236,240,255,0.5)] cursor-pointer', ].join(' ')} >
    @@ -114,22 +127,7 @@ export default function ResourcesPage() { > {r.title} {r.hasAttachment && ( - - - + )}
    diff --git a/src/app/svgs/backcirclesvg.tsx b/src/app/svgs/backcirclesvg.tsx new file mode 100644 index 0000000..ee969f3 --- /dev/null +++ b/src/app/svgs/backcirclesvg.tsx @@ -0,0 +1,46 @@ +export default function BackCircleSvg( + { + width = 32, + height = 32, + className = '', + }: { width?: number | string; height?: number | string; className?: string } +): JSX.Element { + return ( + + + + + + ); +} + + diff --git a/src/app/svgs/chevrondownsvg.tsx b/src/app/svgs/chevrondownsvg.tsx index 9bea139..a33c186 100644 --- a/src/app/svgs/chevrondownsvg.tsx +++ b/src/app/svgs/chevrondownsvg.tsx @@ -19,7 +19,7 @@ export default function ChevronDownSvg( fillRule="evenodd" clipRule="evenodd" d="M18.7071 8.29277C19.0976 8.6833 19.0976 9.31646 18.7071 9.70698L12.7071 15.707C12.3166 16.0975 11.6834 16.0975 11.2929 15.707L5.29289 9.70699C4.90237 9.31646 4.90237 8.6833 5.29289 8.29277C5.68342 7.90225 6.31658 7.90225 6.70711 8.29277L12 13.5857L17.2929 8.29277C17.6834 7.90225 18.3166 7.90225 18.7071 8.29277Z" - fill="white" + fill="currentColor" /> ); diff --git a/src/app/svgs/paperclipsvg.tsx b/src/app/svgs/paperclipsvg.tsx new file mode 100644 index 0000000..3f2c2af --- /dev/null +++ b/src/app/svgs/paperclipsvg.tsx @@ -0,0 +1,29 @@ +export default function PaperClipSvg( + { + width = 16, + height = 16, + className = '', + }: { width?: number | string; height?: number | string; className?: string } +): JSX.Element { + return ( + + + + ); +} + +