This commit is contained in:
mota
2025-11-18 09:09:09 +09:00
parent 4f7b98dffb
commit 4dc8304a1d
22 changed files with 1497 additions and 107 deletions

View File

@@ -15,7 +15,7 @@ const learningItems: NavItem[] = [
const accountItems: NavItem[] = [
{ label: "내 정보 수정", href: "/menu/account" },
{ label: "로그아웃", href: "/logout" },
{ label: "로그아웃", href: "/login" },
];
export default function MenuSidebar() {

View File

@@ -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>

View File

@@ -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>

View 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>
);
}

View File

@@ -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() {
);
}

View File

@@ -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="이전 페이지"
>
&#x2039;
</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="다음 페이지"
>
&#x203A;
</button>
</div>
</div>
</main>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View File

@@ -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>
);
}