므하
This commit is contained in:
@@ -15,7 +15,7 @@ const learningItems: NavItem[] = [
|
||||
|
||||
const accountItems: NavItem[] = [
|
||||
{ label: "내 정보 수정", href: "/menu/account" },
|
||||
{ label: "로그아웃", href: "/logout" },
|
||||
{ label: "로그아웃", href: "/login" },
|
||||
];
|
||||
|
||||
export default function MenuSidebar() {
|
||||
|
||||
@@ -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 (
|
||||
<div className="relative h-2 w-full overflow-hidden rounded-full bg-[#ecf0ff]">
|
||||
<div className="relative h-1.5 w-full overflow-hidden rounded-full bg-[#ecf0ff]">
|
||||
<div
|
||||
className="h-full rounded-full bg-[#384fbf] transition-[width] duration-300 ease-out"
|
||||
style={{ width: `${pct}%` }}
|
||||
@@ -33,14 +34,36 @@ function ProgressBar({ value }: { value: number }) {
|
||||
);
|
||||
}
|
||||
|
||||
export default function CourseCard({ course }: { course: Course }) {
|
||||
const [open, setOpen] = useState(false);
|
||||
export default function CourseCard({ course, defaultOpen = false }: { course: Course; defaultOpen?: boolean }) {
|
||||
const [open, setOpen] = useState(defaultOpen);
|
||||
|
||||
const totalMinutes = course.lessons.reduce((sum, l) => 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 (
|
||||
<article className="rounded-xl border border-[#ecf0ff] bg-white shadow-[0_2px_8px_rgba(0,0,0,0.02)]">
|
||||
<article className={cardClassName}>
|
||||
<header className="flex items-center gap-4 p-4">
|
||||
<div className="relative h-[76px] w-[120px] overflow-hidden rounded-md bg-[#f1f3f5]">
|
||||
<Image src={course.thumbnail} alt="" fill sizes="120px" className="object-cover" />
|
||||
<Image
|
||||
src={`https://picsum.photos/seed/${encodeURIComponent(course.id)}/240/152`}
|
||||
alt=""
|
||||
fill
|
||||
sizes="120px"
|
||||
unoptimized
|
||||
className="object-cover"
|
||||
/>
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -52,32 +75,31 @@ export default function CourseCard({ course }: { course: Course }) {
|
||||
</h2>
|
||||
</div>
|
||||
<p className="mt-1 line-clamp-2 text-[14px] leading-[1.5] text-[#4c5561]">{course.description}</p>
|
||||
<div className="mt-2 flex items-center gap-3">
|
||||
<div className="mt-2 flex items-center justify-between gap-3">
|
||||
<p className="text-[13px] leading-[1.4] text-[#8c95a1]">
|
||||
VOD · 총 {course.lessons.length}강 · {totalHours}시간 {restMinutes}분
|
||||
</p>
|
||||
<p className="text-[13px] font-semibold leading-[1.4] text-[#384fbf]">
|
||||
진행도 {course.progressPct}%
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
<ProgressBar value={course.progressPct} />
|
||||
<span className="w-[72px] text-right text-[13px] font-medium leading-[1.4] text-[#6c7682]">
|
||||
진행률 {course.progressPct}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 self-start">
|
||||
<button
|
||||
type="button"
|
||||
className="rounded-md border border-[#dee1e6] px-3 py-2 text-[13px] font-semibold leading-[1.4] text-[#333c47] hover:bg-[#f9fafb]"
|
||||
>
|
||||
이어 학습하기
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
aria-expanded={open}
|
||||
onClick={() => setOpen((v) => !v)}
|
||||
className="flex h-8 w-8 items-center justify-center rounded-full border border-[#dee1e6] text-[#6c7682] hover:bg-[#f9fafb]"
|
||||
className="flex h-8 w-8 items-center justify-center text-[#6c7682] cursor-pointer"
|
||||
aria-label={open ? "접기" : "펼치기"}
|
||||
>
|
||||
<span
|
||||
className={["inline-block transition-transform", open ? "rotate-180" : "rotate-0"].join(" ")}
|
||||
>
|
||||
▾
|
||||
</span>
|
||||
<ChevronDownSvg
|
||||
width={16}
|
||||
height={16}
|
||||
className={["transition-transform", open ? "rotate-180" : "rotate-0"].join(" ")}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
@@ -86,7 +108,7 @@ export default function CourseCard({ course }: { course: Course }) {
|
||||
<div className="px-4 pb-4">
|
||||
<ul className="flex flex-col gap-2">
|
||||
{course.lessons.map((lesson, idx) => (
|
||||
<li key={lesson.id} className="rounded-lg border border-[#ecf0ff]">
|
||||
<li key={lesson.id} className="rounded-lg border border-[#ecf0ff] bg-white">
|
||||
<div className="flex items-center justify-between gap-4 px-4 py-3">
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-3">
|
||||
@@ -98,23 +120,41 @@ export default function CourseCard({ course }: { course: Course }) {
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-2 flex items-center gap-3">
|
||||
<ProgressBar value={lesson.progressPct} />
|
||||
<span className="w-[80px] text-right text-[13px] leading-[1.4] text-[#8c95a1]">
|
||||
{lesson.progressPct}% · {lesson.durationMin}분
|
||||
<span className="text-[13px] leading-[1.4] text-[#8c95a1]">
|
||||
{formatDuration(lesson.durationMin)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
className={[
|
||||
"rounded-md px-3 py-2 text-[14px] font-medium leading-[1.5]",
|
||||
lesson.isCompleted
|
||||
? "text-[#384fbf] border border-transparent bg-white"
|
||||
: "text-[#4c5561] border border-[#8c95a1] bg-white",
|
||||
].join(" ")}
|
||||
>
|
||||
{lesson.isCompleted ? "학습 제출 완료" : "학습 제출 하기"}
|
||||
</button>
|
||||
{lesson.isCompleted ? (
|
||||
<span className="rounded bg-[#e5f5ec] px-2 py-1 text-[12px] font-semibold leading-[1.4] text-[#0c9d61]">
|
||||
수료
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
className="rounded-md border border-[#dee1e6] px-3 py-2 text-[14px] font-medium leading-[1.5] text-[#4c5561] hover:bg-[#f9fafb]"
|
||||
>
|
||||
복습하기
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
className="rounded-md border border-[#dee1e6] px-3 py-2 text-[13px] font-semibold leading-[1.4] text-[#333c47] hover:bg-[#f9fafb]"
|
||||
className={[
|
||||
"rounded-md px-3 py-2 text-[14px] font-medium leading-[1.5]",
|
||||
lesson.id === firstIncomplete
|
||||
? "bg-[#ecf0ff] text-[#384fbf]"
|
||||
: "border border-[#dee1e6] text-[#4c5561] hover:bg-[#f9fafb]",
|
||||
].join(" ")}
|
||||
>
|
||||
학습하기
|
||||
{lesson.id === firstIncomplete ? "이어서 수강하기" : "수강하기"}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -22,7 +22,7 @@ export default function CourseGridItem({
|
||||
return (
|
||||
<li
|
||||
key={id}
|
||||
className="group flex w-full flex-col gap-2"
|
||||
className="flex w-full cursor-pointer flex-col gap-2"
|
||||
>
|
||||
<div className="relative aspect-[16/10] w-full overflow-hidden rounded-lg border border-[#ecf0ff] bg-[#f9fafb]">
|
||||
<Image
|
||||
@@ -30,7 +30,7 @@ export default function CourseGridItem({
|
||||
alt=""
|
||||
fill
|
||||
sizes="(max-width: 768px) 50vw, (max-width: 1200px) 25vw, 240px"
|
||||
className="object-cover transition-transform duration-300 group-hover:scale-[1.02]"
|
||||
className="object-cover"
|
||||
priority={false}
|
||||
/>
|
||||
</div>
|
||||
|
||||
138
src/app/menu/courses/[courseId]/page.tsx
Normal file
138
src/app/menu/courses/[courseId]/page.tsx
Normal file
@@ -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 (
|
||||
<main className="flex w-full flex-col">
|
||||
<div className="flex h-[100px] items-center px-8">
|
||||
<h1 className="text-[24px] font-bold leading-[1.5] text-[#1b2027]">교육 과정 상세보기</h1>
|
||||
</div>
|
||||
|
||||
<section className="px-8 pb-20">
|
||||
<div className="rounded-[8px] bg-white px-8 pb-20 pt-6">
|
||||
{/* 상단 소개 카드 */}
|
||||
<div className="flex gap-6 rounded-[8px] bg-[#f8f9fa] p-6">
|
||||
<div className="relative h-[159px] w-[292px] overflow-hidden rounded">
|
||||
<Image src={c.thumbnail} alt="" fill sizes="292px" className="object-cover" unoptimized />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="flex h-[27px] items-center gap-2">
|
||||
<span className="h-[20px] rounded-[4px] bg-[#e5f5ec] px-1.5 text-[13px] font-semibold leading-[1.4] text-[#0c9d61]">
|
||||
{c.status}
|
||||
</span>
|
||||
<h2 className="text-[18px] font-semibold leading-[1.5] text-[#333c47]">{c.title}</h2>
|
||||
</div>
|
||||
<div className="mt-3 space-y-1">
|
||||
<p className="text-[15px] leading-[1.5] text-[#333c47]">
|
||||
<span className="font-medium">학습 목표:</span> {c.goal}
|
||||
</p>
|
||||
<p className="text-[15px] leading-[1.5] text-[#333c47]">
|
||||
<span className="font-medium">학습 방법:</span> {c.method}
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-3 flex items-center gap-5 text-[13px] leading-[1.4] text-[#8c95a1]">
|
||||
<span>{c.summary}</span>
|
||||
<span>{c.submitSummary}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 차시 리스트 */}
|
||||
<div className="mt-6 space-y-2">
|
||||
{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 (
|
||||
<div key={l.id} className="rounded-[8px] border border-[#dee1e6] bg-white">
|
||||
<div className="flex items-center justify-between gap-4 rounded-[8px] px-6 py-4">
|
||||
<div className="min-w-0">
|
||||
<p className="text-[16px] font-semibold leading-[1.5] text-[#333c47]">{l.title}</p>
|
||||
<div className="mt-1 flex items-center gap-3">
|
||||
<p className="w-[40px] text-[13px] leading-[1.4] text-[#8c95a1]">{l.duration}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
className={[
|
||||
"h-8 rounded-[6px] px-4 text-[14px] font-medium leading-[1.5]",
|
||||
"bg-white",
|
||||
submitBtnStyle,
|
||||
].join(" ")}
|
||||
>
|
||||
{isSubmitted ? "학습 제출 완료" : "학습 제출 하기"}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={[
|
||||
"h-8 rounded-[6px] px-4 text-[14px] font-medium leading-[1.5]",
|
||||
rightBtnStyle,
|
||||
].join(" ")}
|
||||
>
|
||||
{l.action}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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() {
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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 (
|
||||
<main className="mx-auto w-full max-w-[1440px] px-8 py-8">
|
||||
<h1 className="text-[24px] font-bold leading-[1.5] text-[#1b2027]">교육 과정 목록</h1>
|
||||
<p className="mt-6 text-[16px] leading-[1.5] text-[#4c5561]">
|
||||
메뉴 페이지 준비 중입니다.
|
||||
</p>
|
||||
</main>
|
||||
);
|
||||
return (
|
||||
<main className="w-full">
|
||||
<div className="flex h-[88px] items-center px-8">
|
||||
<h1 className="text-[24px] font-bold leading-[1.5] text-[#1b2027]">교육 과정 목록</h1>
|
||||
</div>
|
||||
|
||||
<div className="px-8 pb-16 pt-2">
|
||||
<p className="mb-4 text-[13px] leading-[1.4] text-[#8c95a1]">총 {CATALOG.length}건</p>
|
||||
<ul className="grid grid-cols-2 gap-x-5 gap-y-6 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5">
|
||||
{CATALOG.map((course) => (
|
||||
<CourseGridItem
|
||||
key={course.id}
|
||||
id={course.id}
|
||||
title={course.title}
|
||||
category={course.category}
|
||||
meta={course.meta}
|
||||
thumbnail={course.thumbnail}
|
||||
isNew={course.isNew}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
{/* pagination */}
|
||||
<div className="mt-10 flex items-center justify-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
className="flex h-8 w-8 items-center justify-center rounded-full text-[#8c95a1] hover:bg-[#f1f3f5]"
|
||||
aria-label="이전 페이지"
|
||||
>
|
||||
‹
|
||||
</button>
|
||||
{[1, 2, 3].map((p) => (
|
||||
<button
|
||||
key={p}
|
||||
type="button"
|
||||
className={[
|
||||
"flex h-8 w-8 items-center justify-center rounded-full text-[13px] leading-[1.4]",
|
||||
p === 1 ? "bg-[#1f2b91] text-white" : "text-[#4c5561] hover:bg-[#f1f3f5]",
|
||||
].join(" ")}
|
||||
>
|
||||
{p}
|
||||
</button>
|
||||
))}
|
||||
<button
|
||||
type="button"
|
||||
className="flex h-8 w-8 items-center justify-center rounded-full text-[#8c95a1] hover:bg-[#f1f3f5]"
|
||||
aria-label="다음 페이지"
|
||||
>
|
||||
›
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
23
src/app/menu/results/CertificateModal.tsx
Normal file
23
src/app/menu/results/CertificateModal.tsx
Normal file
@@ -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 (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||
<div className="absolute inset-0 bg-black/40" onClick={onClose} aria-hidden="true" />
|
||||
<div className="relative z-10 shadow-xl">
|
||||
<FigmaCertificateContent onClose={onClose} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
44
src/app/menu/results/FeedbackModal.tsx
Normal file
44
src/app/menu/results/FeedbackModal.tsx
Normal file
@@ -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 (
|
||||
<div
|
||||
className="fixed inset-0 z-50 flex items-center justify-center"
|
||||
aria-hidden={!open}
|
||||
>
|
||||
<div
|
||||
className="absolute inset-0 bg-black/40"
|
||||
onClick={onClose}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<div className="relative z-10 shadow-xl">
|
||||
<FigmaFeedbackContent
|
||||
onClose={onClose}
|
||||
learnerName={learnerName}
|
||||
instructorName={instructorName}
|
||||
scoreText={scoreText}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
346
src/app/menu/results/FigmaCertificateContent.tsx
Normal file
346
src/app/menu/results/FigmaCertificateContent.tsx
Normal file
@@ -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 (
|
||||
<div
|
||||
className="bg-white border border-[#dee1e6] border-solid box-border content-stretch flex flex-col items-center relative rounded-[12px] w-[720px] max-w-[95vw]"
|
||||
data-name="Frame"
|
||||
data-node-id="388:21934"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
>
|
||||
<div
|
||||
className="box-border content-stretch flex gap-[10px] items-center overflow-clip p-[24px] relative shrink-0 w-full"
|
||||
data-name="header"
|
||||
data-node-id="388:21935"
|
||||
>
|
||||
<div
|
||||
className="basis-0 flex flex-col font-['Pretendard:Bold',sans-serif] grow h-[32px] justify-center leading-[0] min-h-px min-w-px not-italic relative shrink-0 text-[#333c47] text-[20px]"
|
||||
data-node-id="388:21937"
|
||||
>
|
||||
<p className="leading-[1.5]">수료증</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="닫기"
|
||||
onClick={onClose}
|
||||
className="overflow-clip relative shrink-0 size-[24px]"
|
||||
data-name="x-close-lg"
|
||||
data-node-id="388:21938"
|
||||
>
|
||||
<div
|
||||
className="absolute left-[calc(50%-0.02px)] size-[15.167px] top-[calc(50%-0.02px)] translate-x-[-50%] translate-y-[-50%]"
|
||||
data-name="Icon (Stroke)"
|
||||
data-node-id="I388:21938;1422:243"
|
||||
>
|
||||
<div
|
||||
className="absolute inset-0"
|
||||
style={{ "--fill-0": "rgba(51, 60, 71, 1)" } as React.CSSProperties}
|
||||
>
|
||||
<img alt="" className="block max-w-none size-full" src={img} />
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="bg-white border-[#1f2b91] border-[6px] border-solid h-[714px] relative rounded-[12px] shrink-0 w-[506px]"
|
||||
data-name="Container"
|
||||
data-node-id="388:22388"
|
||||
>
|
||||
<div
|
||||
className="absolute content-stretch flex flex-col gap-[8px] items-center justify-center left-[46px] not-italic text-center text-nowrap top-[92px] w-[418px] whitespace-pre"
|
||||
data-node-id="388:22389"
|
||||
>
|
||||
<p
|
||||
className="font-['Pretendard:Regular',sans-serif] leading-[1.4] relative shrink-0 text-[#6c7682] text-[13px]"
|
||||
data-node-id="388:22390"
|
||||
>
|
||||
제 2025-N0055L3
|
||||
</p>
|
||||
<p
|
||||
className="font-['Pretendard:SemiBold',sans-serif] leading-[1.5] relative shrink-0 text-[#333c47] text-[40px]"
|
||||
data-node-id="388:22391"
|
||||
>{`수 료 증`}</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="absolute content-stretch flex flex-col gap-[4px] items-start left-[46px] top-[216px] w-[418px]"
|
||||
data-node-id="388:22392"
|
||||
>
|
||||
<div
|
||||
className="content-stretch flex h-[24px] items-start relative shrink-0 w-full"
|
||||
data-name="Container"
|
||||
data-node-id="388:22393"
|
||||
>
|
||||
<p className="font-['Pretendard:Medium',sans-serif] leading-[1.5] not-italic relative shrink-0 text-[#333c47] text-[15px] w-[100px] whitespace-pre-wrap">
|
||||
{`소 속 :`}
|
||||
</p>
|
||||
<div className="h-[24px] relative shrink-0 w-[170.297px]" data-name="Paragraph">
|
||||
<div className="bg-clip-padding border-0 border-[transparent] border-solid box-border h-[24px] relative w-[170.297px]">
|
||||
<p className="absolute font-['Pretendard:Medium',sans-serif] leading-[1.5] left-0 not-italic text-[#333c47] text-[15px] text-nowrap top-[-1px] whitespace-pre">
|
||||
XR LMS
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="content-stretch flex h-[24px] items-start relative shrink-0 w-full" data-name="Container">
|
||||
<p className="font-['Pretendard:Medium',sans-serif] leading-[1.5] not-italic relative shrink-0 text-[#333c47] text-[15px] w-[100px] whitespace-pre-wrap">
|
||||
{`성 명 :`}
|
||||
</p>
|
||||
<div className="h-[24px] relative shrink-0 w-[41.531px]" data-name="Paragraph">
|
||||
<div className="bg-clip-padding border-0 border-[transparent] border-solid box-border h-[24px] relative w-[41.531px]">
|
||||
<p className="absolute font-['Pretendard:Medium',sans-serif] leading-[1.5] left-0 not-italic text-[#333c47] text-[15px] text-nowrap top-[-1px] whitespace-pre">
|
||||
김하늘
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="content-stretch flex h-[24px] items-start relative shrink-0 w-full" data-name="Container">
|
||||
<p className="font-['Pretendard:Medium',sans-serif] leading-[1.5] not-italic relative shrink-0 text-[#333c47] text-[15px] w-[100px]">
|
||||
생 년 월 일 :
|
||||
</p>
|
||||
<div className="h-[24px] relative shrink-0 w-[78.281px]" data-name="Paragraph">
|
||||
<div className="bg-clip-padding border-0 border-[transparent] border-solid box-border h-[24px] relative w-[78.281px]">
|
||||
<p className="absolute font-['Pretendard:Medium',sans-serif] leading-[1.5] left-0 not-italic text-[#333c47] text-[15px] text-nowrap top-[-1px] whitespace-pre">
|
||||
1994-10-17
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="content-stretch flex h-[24px] items-start relative shrink-0 w-full" data-name="Container">
|
||||
<p className="font-['Pretendard:Medium',sans-serif] leading-[1.5] not-italic relative shrink-0 text-[#333c47] text-[15px] w-[100px]">
|
||||
교 육 과 정 :
|
||||
</p>
|
||||
<div className="basis-0 grow h-[24px] min-h-px min-w-px relative shrink-0" data-name="Paragraph">
|
||||
<div className="bg-clip-padding border-0 border-[transparent] border-solid box-border h-[24px] relative w-full">
|
||||
<p className="absolute font-['Pretendard:Medium',sans-serif] leading-[1.5] left-0 not-italic text-[#333c47] text-[15px] text-nowrap top-[-1px] whitespace-pre">
|
||||
(2025년) 방사선작업종사자 직장교육(신규) 9월
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="content-stretch flex h-[24px] items-start relative shrink-0 w-full" data-name="Container">
|
||||
<p className="font-['Pretendard:Medium',sans-serif] leading-[1.5] not-italic relative shrink-0 text-[#333c47] text-[15px] w-[100px]">
|
||||
교 육 기 간 :
|
||||
</p>
|
||||
<div className="h-[24px] relative shrink-0 w-[229.328px]" data-name="Paragraph">
|
||||
<div className="bg-clip-padding border-0 border-[transparent] border-solid box-border h-[24px] relative w-[229.328px]">
|
||||
<p className="absolute font-['Pretendard:Medium',sans-serif] leading-[1.5] left-0 not-italic text-[#333c47] text-[15px] text-nowrap top-[-1px] whitespace-pre">
|
||||
2025-09-01 ~ 2025-09-30, 4시간
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="content-stretch flex h-[24px] items-start relative shrink-0 w-full" data-name="Container">
|
||||
<p className="font-['Pretendard:Medium',sans-serif] leading-[1.5] not-italic relative shrink-0 text-[#333c47] text-[15px] w-[100px]">
|
||||
수 료 일 자 :
|
||||
</p>
|
||||
<div className="h-[24px] relative shrink-0 w-[84.219px]" data-name="Paragraph">
|
||||
<div className="bg-clip-padding border-0 border-[transparent] border-solid box-border h-[24px] relative w-[84.219px]">
|
||||
<p className="absolute font-['Pretendard:Medium',sans-serif] leading-[1.5] left-0 not-italic text-[#333c47] text-[15px] text-nowrap top-[-1px] whitespace-pre">
|
||||
2025-09-26
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="absolute content-stretch flex flex-col gap-[24px] items-center left-[94.5px] top-[428px]"
|
||||
data-node-id="388:22423"
|
||||
>
|
||||
<div className="font-['Pretendard:Medium',sans-serif] leading-[1.5] not-italic relative shrink-0 text-[#333c47] text-[16px] text-center text-nowrap whitespace-pre">
|
||||
<p className="mb-0">위 사람은 우리 협회가 진행한</p>
|
||||
<p>
|
||||
『(2025년) 방사선작업종사자 직장교육(신규)_9월』
|
||||
<br aria-hidden="true" />
|
||||
과정을 수료하였으므로 이 수료증을 수여함.
|
||||
</p>
|
||||
</div>
|
||||
<p className="font-['Pretendard:Medium',sans-serif] leading-[1.5] not-italic relative shrink-0 text-[#4c5561] text-[16px] text-center text-nowrap whitespace-pre">
|
||||
2025년 10월 21일
|
||||
</p>
|
||||
<div className="content-stretch flex gap-[8px] items-center relative shrink-0" data-name="logo">
|
||||
<div className="h-[36px] relative shrink-0 w-[46.703px]" data-name="image 1">
|
||||
<div className="absolute inset-0 overflow-hidden pointer-events-none">
|
||||
<img alt="" className="absolute h-[390.71%] left-[-100%] max-w-none top-[-132.02%] w-[301.18%]" src={imgImage1} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col font-['Pretendard:ExtraBold',sans-serif] justify-center leading-[0] not-italic relative shrink-0 text-[#333c47] text-[24px] text-nowrap">
|
||||
<p className="leading-[1.45] whitespace-pre">XR LMS</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="absolute contents left-[26px] top-[662px]">
|
||||
<div className="absolute flex h-[28px] items-center justify-center left-[26px] top-[662px] w-[40px]">
|
||||
<div className="flex-none scale-y-[-100%]">
|
||||
<div className="content-stretch flex flex-col gap-[4px] h-[28px] items-start relative w-[40px]">
|
||||
<div className="h-[12px] relative shrink-0 w-[40px]">
|
||||
<div className="content-stretch flex gap-[4px] h-[12px] items-start relative w-[40px]">
|
||||
<div
|
||||
className="relative shrink-0 size-[12px]"
|
||||
style={{ backgroundImage: "linear-gradient(rgb(31, 43, 145) 0%, rgb(56, 79, 191) 100%), linear-gradient(90deg, rgb(56, 79, 191) 0%, rgb(56, 79, 191) 100%)" }}
|
||||
>
|
||||
<div className="bg-clip-padding border-0 border-[transparent] border-solid box-border size-[12px]" />
|
||||
</div>
|
||||
<div
|
||||
className="relative shrink-0 size-[12px]"
|
||||
style={{ backgroundImage: "linear-gradient(rgb(31, 43, 145) 0%, rgb(56, 79, 191) 100%), linear-gradient(90deg, rgb(56, 79, 191) 0%, rgb(56, 79, 191) 100%)" }}
|
||||
>
|
||||
<div className="bg-clip-padding border-0 border-[transparent] border-solid box-border size-[12px]" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="basis-0 grow min-h-px min-w-px relative shrink-0 w-[12px]"
|
||||
style={{ backgroundImage: "linear-gradient(rgb(31, 43, 145) 0%, rgb(56, 79, 191) 100%), linear-gradient(90deg, rgb(56, 79, 191) 0%, rgb(56, 79, 191) 100%)" }}
|
||||
>
|
||||
<div className="bg-clip-padding border-0 border-[transparent] border-solid box-border h-full w-[12px]" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute flex h-[28px] items-center justify-center left-[444px] top-[662px] w-[40px]">
|
||||
<div className="flex-none scale-y-[-100%]">
|
||||
<div className="content-stretch flex flex-col gap-[4px] h-[28px] items-end relative w-[40px]">
|
||||
<div className="h-[12px] relative shrink-0 w-[28px]">
|
||||
<div className="content-stretch flex gap-[4px] h-[12px] items-start relative w-[28px]">
|
||||
<div
|
||||
className="relative shrink-0 size-[12px]"
|
||||
style={{ backgroundImage: "linear-gradient(rgb(31, 43, 145) 0%, rgb(56, 79, 191) 100%), linear-gradient(90deg, rgb(56, 79, 191) 0%, rgb(56, 79, 191) 100%)" }}
|
||||
>
|
||||
<div className="bg-clip-padding border-0 border-[transparent] border-solid box-border size-[12px]" />
|
||||
</div>
|
||||
<div
|
||||
className="basis-0 grow h-[12px] min-h-px min-w-px relative shrink-0"
|
||||
style={{ backgroundImage: "linear-gradient(rgb(31, 43, 145) 0%, rgb(56, 79, 191) 100%), linear-gradient(90deg, rgb(56, 79, 191) 0%, rgb(56, 79, 191) 100%)" }}
|
||||
>
|
||||
<div className="bg-clip-padding border-0 border-[transparent] border-solid box-border h-[12px] w-full" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="basis-0 grow min-h-px min-w-px relative shrink-0 w-[12px]"
|
||||
style={{ backgroundImage: "linear-gradient(rgb(31, 43, 145) 0%, rgb(56, 79, 191) 100%), linear-gradient(90deg, rgb(56, 79, 191) 0%, rgb(56, 79, 191) 100%)" }}
|
||||
>
|
||||
<div className="bg-clip-padding border-0 border-[transparent] border-solid box-border h-full w-[12px]" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute h-[2px] left-[86px] top-[690px] w-[338px]">
|
||||
<img alt="" className="absolute inset-0 max-w-none object-50%-50% object-cover pointer-events-none size-full" src={imgContainer} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="absolute contents left-[24px] top-[22px]">
|
||||
<div className="absolute content-stretch flex flex-col gap-[4px] h-[28px] items-start left-[24px] top-[24px] w-[40px]">
|
||||
<div className="h-[12px] relative shrink-0 w-[40px]">
|
||||
<div className="content-stretch flex gap-[4px] h-[12px] items-start relative w-[40px]">
|
||||
<div
|
||||
className="relative shrink-0 size-[12px]"
|
||||
style={{ backgroundImage: "linear-gradient(rgb(31, 43, 145) 0%, rgb(56, 79, 191) 100%), linear-gradient(90deg, rgb(56, 79, 191) 0%, rgb(56, 79, 191) 100%)" }}
|
||||
>
|
||||
<div className="bg-clip-padding border-0 border-[transparent] border-solid box-border size-[12px]" />
|
||||
</div>
|
||||
<div
|
||||
className="relative shrink-0 size-[12px]"
|
||||
style={{ backgroundImage: "linear-gradient(rgb(31, 43, 145) 0%, rgb(56, 79, 191) 100%), linear-gradient(90deg, rgb(56, 79, 191) 0%, rgb(56, 79, 191) 100%)" }}
|
||||
>
|
||||
<div className="bg-clip-padding border-0 border-[transparent] border-solid box-border size-[12px]" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="basis-0 grow min-h-px min-w-px relative shrink-0 w-[12px]"
|
||||
style={{ backgroundImage: "linear-gradient(rgb(31, 43, 145) 0%, rgb(56, 79, 191) 100%), linear-gradient(90deg, rgb(56, 79, 191) 0%, rgb(56, 79, 191) 100%)" }}
|
||||
>
|
||||
<div className="bg-clip-padding border-0 border-[transparent] border-solid box-border h-full w-[12px]" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute content-stretch flex flex-col gap-[4px] h-[28px] items-end left-[442px] top-[24px] w-[40px]">
|
||||
<div className="h-[12px] relative shrink-0 w-[28px]">
|
||||
<div className="content-stretch flex gap-[4px] h-[12px] items-start relative w-[28px]">
|
||||
<div
|
||||
className="relative shrink-0 size-[12px]"
|
||||
style={{ backgroundImage: "linear-gradient(rgb(31, 43, 145) 0%, rgb(56, 79, 191) 100%), linear-gradient(90deg, rgb(56, 79, 191) 0%, rgb(56, 79, 191) 100%)" }}
|
||||
>
|
||||
<div className="bg-clip-padding border-0 border-[transparent] border-solid box-border size-[12px]" />
|
||||
</div>
|
||||
<div
|
||||
className="basis-0 grow h-[12px] min-h-px min-w-px relative shrink-0"
|
||||
style={{ backgroundImage: "linear-gradient(rgb(31, 43, 145) 0%, rgb(56, 79, 191) 100%), linear-gradient(90deg, rgb(56, 79, 191) 0%, rgb(56, 79, 191) 100%)" }}
|
||||
>
|
||||
<div className="bg-clip-padding border-0 border-[transparent] border-solid box-border h-[12px] w-full" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="basis-0 grow min-h-px min-w-px relative shrink-0 w-[12px]"
|
||||
style={{ backgroundImage: "linear-gradient(rgb(31, 43, 145) 0%, rgb(56, 79, 191) 100%), linear-gradient(90deg, rgb(56, 79, 191) 0%, rgb(56, 79, 191) 100%)" }}
|
||||
>
|
||||
<div className="bg-clip-padding border-0 border-[transparent] border-solid box-border h-full w-[12px]" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute h-[2px] left-[84px] top-[22px] w-[338px]">
|
||||
<img alt="" className="absolute inset-0 max-w-none object-50%-50% object-cover pointer-events-none size-full" src={imgContainer} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="box-border content-stretch flex flex-col gap-[32px] h-[96px] items-center p-[24px] relative shrink-0 w-full"
|
||||
data-node-id="388:22061"
|
||||
>
|
||||
<div
|
||||
className="basis-0 content-stretch flex gap-[12px] grow items-start justify-center min-h-px min-w-px overflow-clip relative shrink-0 w-full"
|
||||
data-name="Actions"
|
||||
data-node-id="388:22062"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className="bg-white border border-[#8c95a1] border-solid box-border content-stretch flex gap-[4px] h-[48px] items-center justify-center px-[16px] py-[3px] relative rounded-[10px] shrink-0 w-[136px]"
|
||||
>
|
||||
<div className="relative shrink-0 size-[20px]" data-name="Interface, Essential/download-arrow">
|
||||
<div className="absolute inset-[-0.02%]">
|
||||
<img alt="" className="block max-w-none size-full" src={img1} />
|
||||
</div>
|
||||
</div>
|
||||
<p className="font-['Pretendard:SemiBold',sans-serif] leading-[1.5] not-italic relative shrink-0 text-[#4c5561] text-[16px] text-center text-nowrap whitespace-pre">
|
||||
다운로드
|
||||
</p>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="bg-[#ecf0ff] box-border content-stretch flex gap-[4px] h-[48px] items-center justify-center px-[16px] py-[8px] relative rounded-[10px] shrink-0 w-[136px]"
|
||||
>
|
||||
<p className="font-['Pretendard:SemiBold',sans-serif] leading-[1.5] not-italic relative shrink-0 text-[#384fbf] text-[16px] text-center text-nowrap whitespace-pre">
|
||||
출력
|
||||
</p>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
122
src/app/menu/results/FigmaFeedbackContent.tsx
Normal file
122
src/app/menu/results/FigmaFeedbackContent.tsx
Normal file
@@ -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 (
|
||||
<div
|
||||
className="bg-white border border-[#dee1e6] border-solid box-border content-stretch flex flex-col items-center relative rounded-[12px] w-[640px] max-w-[90vw]"
|
||||
data-name="Frame"
|
||||
data-node-id="270:9120"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
>
|
||||
<div
|
||||
className="box-border content-stretch flex gap-[10px] items-center overflow-clip p-[24px] relative shrink-0 w-full"
|
||||
data-name="header"
|
||||
data-node-id="270:9121"
|
||||
>
|
||||
<div
|
||||
className="basis-0 flex flex-col font-['Pretendard:Bold',sans-serif] grow h-[32px] justify-center leading-[0] min-h-px min-w-px not-italic relative shrink-0 text-[#333c47] text-[20px]"
|
||||
data-node-id="270:9123"
|
||||
>
|
||||
<p className="leading-[1.5]">{`${learnerName}님의 피드백`}</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="닫기"
|
||||
onClick={onClose}
|
||||
className="overflow-clip relative shrink-0 size-[24px]"
|
||||
data-name="x-close-lg"
|
||||
data-node-id="270:9124"
|
||||
>
|
||||
<div
|
||||
className="absolute left-[calc(50%-0.02px)] size-[15.167px] top-[calc(50%-0.02px)] translate-x-[-50%] translate-y-[-50%]"
|
||||
data-name="Icon (Stroke)"
|
||||
data-node-id="I270:9124;1422:243"
|
||||
>
|
||||
<div
|
||||
className="absolute inset-0"
|
||||
style={{ "--fill-0": "rgba(51, 60, 71, 1)" } as React.CSSProperties}
|
||||
>
|
||||
<img alt="" className="block max-w-none size-full" src={img} />
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
className="box-border content-stretch flex flex-col gap-[16px] items-center px-[24px] py-0 relative shrink-0 w-full"
|
||||
data-node-id="270:9126"
|
||||
>
|
||||
<div
|
||||
className="bg-gray-50 border border-[#dee1e6] border-solid box-border content-stretch flex flex-col gap-[8px] items-start justify-center p-[24px] relative rounded-[16px] shrink-0 w-full"
|
||||
data-node-id="270:9127"
|
||||
>
|
||||
<div
|
||||
className="content-stretch flex gap-[10px] items-center justify-center relative shrink-0"
|
||||
data-node-id="270:9128"
|
||||
>
|
||||
<p
|
||||
className="font-['Pretendard:Bold',sans-serif] h-[23px] leading-[1.5] not-italic relative shrink-0 text-[#4c5561] text-[15px] w-full"
|
||||
data-node-id="270:9129"
|
||||
>
|
||||
평가 강사: {instructorName}
|
||||
</p>
|
||||
</div>
|
||||
<p
|
||||
className="font-['Pretendard:Regular',sans-serif] leading-[1.5] not-italic relative shrink-0 text-[#4c5561] text-[15px] whitespace-pre-wrap"
|
||||
data-node-id="270:9130"
|
||||
>
|
||||
{`- ${learnerName}님, 총점 ${scoreText}점으로 합격하셨습니다. 학습 흐름이 안정적이며 이해도가 좋습니다.`}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="box-border content-stretch flex flex-col gap-[32px] h-[96px] items-center p-[24px] relative shrink-0 w-full"
|
||||
data-node-id="270:9131"
|
||||
>
|
||||
<div
|
||||
className="basis-0 content-stretch flex gap-[12px] grow items-start justify-center min-h-px min-w-px overflow-clip relative shrink-0 w-full"
|
||||
data-name="Actions"
|
||||
data-node-id="270:9132"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="bg-[#1f2b91] box-border content-stretch flex flex-col h-[48px] items-center justify-center px-[16px] py-0 relative rounded-[10px] shrink-0 w-[284px] cursor-pointer"
|
||||
data-name="Primary"
|
||||
data-node-id="270:9134"
|
||||
>
|
||||
<div className="h-0 relative shrink-0 w-[82px]" data-name="Min Width" data-node-id="I270:9134;4356:4636">
|
||||
<img alt="" className="block max-w-none size-full" src={img1} />
|
||||
</div>
|
||||
<p
|
||||
className="font-['Pretendard:SemiBold',sans-serif] leading-[1.5] not-italic relative shrink-0 text-[16px] text-center text-nowrap text-white whitespace-pre"
|
||||
data-node-id="I270:9134;4356:4637"
|
||||
>
|
||||
확인
|
||||
</p>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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<ResultRow | null>(null);
|
||||
const [isCertOpen, setIsCertOpen] = useState(false);
|
||||
|
||||
function openFeedback(row: ResultRow) {
|
||||
setSelected(row);
|
||||
setIsFeedbackOpen(true);
|
||||
}
|
||||
function openCertificate(row: ResultRow) {
|
||||
setSelected(row);
|
||||
setIsCertOpen(true);
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="flex w-full flex-col">
|
||||
<div className="flex h-[100px] items-center px-8">
|
||||
@@ -71,7 +90,7 @@ export default function ResultsPage() {
|
||||
{mockResults.map((row, idx) => (
|
||||
<tr
|
||||
key={`${row.programTitle}-${idx}`}
|
||||
className={idx === 1 ? "h-12 bg-[rgba(236,240,255,0.5)]" : "h-12"}
|
||||
className="h-12"
|
||||
>
|
||||
<td className="border-t border-r border-[#dee1e6] px-4 text-[15px] leading-[1.5] text-[#1b2027]">
|
||||
{row.programTitle}
|
||||
@@ -90,18 +109,26 @@ export default function ResultsPage() {
|
||||
</td>
|
||||
<td className="border-t border-r border-[#dee1e6] px-4 text-center text-[15px] leading-[1.5] text-[#1b2027]">
|
||||
{row.feedbackLink ? (
|
||||
<a href={row.feedbackLink} className="text-[12px] text-blue-500 underline underline-offset-[3px]">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => openFeedback(row)}
|
||||
className="text-[12px] text-blue-500 underline underline-offset-[3px] cursor-pointer"
|
||||
>
|
||||
확인하기
|
||||
</a>
|
||||
</button>
|
||||
) : (
|
||||
"-"
|
||||
)}
|
||||
</td>
|
||||
<td className="border-t border-[#dee1e6] px-4 text-center text-[15px] leading-[1.5] text-[#1b2027]">
|
||||
{row.certificateLink ? (
|
||||
<a href={row.certificateLink} className="text-[12px] text-blue-500 underline underline-offset-[3px]">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => openCertificate(row)}
|
||||
className="text-[12px] text-blue-500 underline underline-offset-[3px] cursor-pointer"
|
||||
>
|
||||
확인하기
|
||||
</a>
|
||||
</button>
|
||||
) : (
|
||||
"-"
|
||||
)}
|
||||
@@ -113,6 +140,17 @@ export default function ResultsPage() {
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<FeedbackModal
|
||||
open={isFeedbackOpen}
|
||||
onClose={() => setIsFeedbackOpen(false)}
|
||||
learnerName="홍길동"
|
||||
instructorName={selected?.instructor}
|
||||
scoreText={selected?.score?.split(" / ")[0] ?? "0"}
|
||||
/>
|
||||
<CertificateModal
|
||||
open={isCertOpen}
|
||||
onClose={() => setIsCertOpen(false)}
|
||||
/>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user