213 lines
7.2 KiB
TypeScript
213 lines
7.2 KiB
TypeScript
|
|
'use client';
|
||
|
|
|
||
|
|
import { useEffect, useState } from 'react';
|
||
|
|
import { useParams, useRouter } from 'next/navigation';
|
||
|
|
import Image from 'next/image';
|
||
|
|
import apiService from '../../lib/apiService';
|
||
|
|
|
||
|
|
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[];
|
||
|
|
};
|
||
|
|
|
||
|
|
export default function CourseDetailPage() {
|
||
|
|
const params = useParams();
|
||
|
|
const router = useRouter();
|
||
|
|
const [course, setCourse] = useState<CourseDetail | null>(null);
|
||
|
|
const [loading, setLoading] = useState(true);
|
||
|
|
const [error, setError] = useState<string | null>(null);
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
const fetchCourse = async () => {
|
||
|
|
if (!params?.id) return;
|
||
|
|
|
||
|
|
try {
|
||
|
|
setLoading(true);
|
||
|
|
setError(null);
|
||
|
|
const response = await apiService.getLecture(params.id as string);
|
||
|
|
|
||
|
|
// API 응답 데이터를 CourseDetail 타입으로 변환
|
||
|
|
const data = response.data;
|
||
|
|
|
||
|
|
// API 응답 구조에 맞게 데이터 매핑
|
||
|
|
// 실제 API 응답 구조에 따라 조정 필요
|
||
|
|
const courseDetail: CourseDetail = {
|
||
|
|
id: String(data.id || params.id),
|
||
|
|
status: data.status || "수강 예정",
|
||
|
|
title: data.title || data.lectureName || '',
|
||
|
|
goal: data.objective || data.goal || '',
|
||
|
|
method: data.method || '',
|
||
|
|
summary: data.summary || `VOD · 총 ${data.lessons?.length || 0}강`,
|
||
|
|
submitSummary: data.submitSummary || '',
|
||
|
|
thumbnail: data.thumbnail || data.imageKey || data.imageUrl || '/imgs/talk.png',
|
||
|
|
lessons: (data.lessons || []).map((lesson: any, index: number) => ({
|
||
|
|
id: String(lesson.id || lesson.lessonId || index + 1),
|
||
|
|
title: `${index + 1}. ${lesson.title || lesson.lessonName || ''}`,
|
||
|
|
duration: lesson.duration || '00:00',
|
||
|
|
state: lesson.isCompleted ? "제출완료" : "제출대기",
|
||
|
|
action: lesson.isCompleted ? "복습하기" : (index === 0 ? "수강하기" : "이어서 수강하기"),
|
||
|
|
})),
|
||
|
|
};
|
||
|
|
|
||
|
|
setCourse(courseDetail);
|
||
|
|
} catch (err) {
|
||
|
|
console.error('강좌 조회 실패:', err);
|
||
|
|
setError(err instanceof Error ? err.message : '강좌를 불러오는데 실패했습니다.');
|
||
|
|
} finally {
|
||
|
|
setLoading(false);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
fetchCourse();
|
||
|
|
}, [params?.id]);
|
||
|
|
|
||
|
|
if (loading) {
|
||
|
|
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="flex items-center justify-center py-20">
|
||
|
|
<p className="text-[16px] text-[#8c95a1]">로딩 중...</p>
|
||
|
|
</div>
|
||
|
|
</section>
|
||
|
|
</main>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (error || !course) {
|
||
|
|
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="flex flex-col items-center justify-center py-20">
|
||
|
|
<p className="text-[16px] text-red-500 mb-4">{error || '강좌를 찾을 수 없습니다.'}</p>
|
||
|
|
<button
|
||
|
|
type="button"
|
||
|
|
onClick={() => router.push('/course-list')}
|
||
|
|
className="px-4 py-2 rounded-[6px] bg-primary text-white text-[14px] font-medium"
|
||
|
|
>
|
||
|
|
강좌 목록으로 돌아가기
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
</section>
|
||
|
|
</main>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
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={course.thumbnail}
|
||
|
|
alt={course.title}
|
||
|
|
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]">
|
||
|
|
{course.status}
|
||
|
|
</span>
|
||
|
|
<h2 className="text-[18px] font-semibold leading-[1.5] text-[#333c47]">{course.title}</h2>
|
||
|
|
</div>
|
||
|
|
<div className="mt-3 space-y-1">
|
||
|
|
<p className="text-[15px] leading-[1.5] text-[#333c47]">
|
||
|
|
<span className="font-medium">학습 목표:</span> {course.goal}
|
||
|
|
</p>
|
||
|
|
<p className="text-[15px] leading-[1.5] text-[#333c47]">
|
||
|
|
<span className="font-medium">학습 방법:</span> {course.method}
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
<div className="mt-3 flex items-center gap-5 text-[13px] leading-[1.4] text-[#8c95a1]">
|
||
|
|
<span>{course.summary}</span>
|
||
|
|
{course.submitSummary && <span>{course.submitSummary}</span>}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* 차시 리스트 */}
|
||
|
|
<div className="mt-6 space-y-2">
|
||
|
|
{course.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>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|