diff --git a/src/app/admin/lessons/[id]/edit/page.tsx b/src/app/admin/lessons/[id]/edit/page.tsx new file mode 100644 index 0000000..479d726 --- /dev/null +++ b/src/app/admin/lessons/[id]/edit/page.tsx @@ -0,0 +1,969 @@ +'use client'; + +import { useState, useRef, useEffect, useMemo } from "react"; +import { useParams, useRouter } from "next/navigation"; +import AdminSidebar from "@/app/components/AdminSidebar"; +import DropdownIcon from "@/app/svgs/dropdownicon"; +import BackArrowSvg from "@/app/svgs/backarrow"; +import { getCourses, type Course } from "@/app/admin/courses/mockData"; +import CloseXOSvg from "@/app/svgs/closexo"; +import apiService from "@/app/lib/apiService"; + +export default function LessonEditPage() { + const params = useParams(); + const router = useRouter(); + const [loading, setLoading] = useState(true); + const [isSaving, setIsSaving] = useState(false); + const [isDropdownOpen, setIsDropdownOpen] = useState(false); + const dropdownRef = useRef(null); + const [courses, setCourses] = useState([]); + const [showToast, setShowToast] = useState(false); + + // 폼 상태 + const [selectedCourse, setSelectedCourse] = useState(""); + const [lessonName, setLessonName] = useState(""); + const [learningGoal, setLearningGoal] = useState(""); + const [courseVideoCount, setCourseVideoCount] = useState(0); + const [courseVideoFiles, setCourseVideoFiles] = useState([]); + const [courseVideoFileObjects, setCourseVideoFileObjects] = useState([]); + const [existingVideoFiles, setExistingVideoFiles] = useState>([]); + const [vrContentCount, setVrContentCount] = useState(0); + const [vrContentFiles, setVrContentFiles] = useState([]); + const [vrContentFileObjects, setVrContentFileObjects] = useState([]); + const [existingVrFiles, setExistingVrFiles] = useState>([]); + const [questionFileCount, setQuestionFileCount] = useState(0); + const [questionFileObject, setQuestionFileObject] = useState(null); + const [existingQuestionFile, setExistingQuestionFile] = useState<{fileName: string, fileKey?: string} | null>(null); + + // 원본 데이터 저장 (변경사항 비교용) + const [originalData, setOriginalData] = useState<{ + title?: string; + objective?: string; + videoUrl?: string; + webglUrl?: string; + csvUrl?: string; + } | null>(null); + + // 에러 상태 + const [errors, setErrors] = useState<{ + selectedCourse?: string; + lessonName?: string; + learningGoal?: string; + }>({}); + + // 교육과정 옵션 + const courseOptions = useMemo(() => { + return courses.map(course => ({ + id: course.id, + name: course.courseName, + })); + }, [courses]); + + // 교육과정 목록 로드 + useEffect(() => { + const loadCourses = async () => { + try { + const data = await getCourses(); + setCourses(data); + } catch (error) { + console.error('교육과정 목록 로드 오류:', error); + } + }; + loadCourses(); + }, []); + + // 강좌 데이터 로드 + useEffect(() => { + const loadLecture = async () => { + if (!params?.id) return; + + try { + setLoading(true); + + // sessionStorage에서 저장된 강좌 데이터 가져오기 + let data: any = null; + const storedData = typeof window !== 'undefined' ? sessionStorage.getItem('selectedLecture') : null; + + if (storedData) { + try { + data = JSON.parse(storedData); + } catch (e) { + console.error('저장된 데이터 파싱 실패:', e); + } + } + + // sessionStorage에 데이터가 없으면 API에서 직접 가져오기 + if (!data) { + try { + const response = await apiService.getLecture(params.id as string); + data = response.data; + } catch (err) { + console.error('강좌 조회 실패:', err); + // getLecture 실패 시 getLectures로 재시도 + try { + const listResponse = await apiService.getLectures(); + const lectures = Array.isArray(listResponse.data) + ? listResponse.data + : listResponse.data?.items || listResponse.data?.lectures || listResponse.data?.data || []; + + data = lectures.find((l: any) => String(l.id || l.lectureId) === params.id); + } catch (listErr) { + console.error('강좌 리스트 조회 실패:', listErr); + } + } + } + + if (!data) { + throw new Error('강좌를 찾을 수 없습니다.'); + } + + // 원본 데이터 저장 + const original = { + title: data.title || data.lectureName || '', + objective: data.objective || data.goal || '', + videoUrl: data.videoUrl || undefined, + webglUrl: data.webglUrl || data.vrUrl || undefined, + csvUrl: data.csvUrl || (data.csvKey ? `csv/${data.csvKey}` : undefined), + }; + setOriginalData(original); + + // 폼에 데이터 채우기 + if (data.subjectId || data.subject_id) { + setSelectedCourse(String(data.subjectId || data.subject_id)); + } + setLessonName(original.title); + setLearningGoal(original.objective); + + // 기존 비디오 파일 + if (data.videoUrl || data.videoKey) { + const videoFiles = []; + if (data.videoUrl) { + videoFiles.push({ + fileName: data.videoFileName || '강좌영상.mp4', + fileKey: data.videoKey, + url: data.videoUrl, + }); + } + if (data.videoFiles && Array.isArray(data.videoFiles)) { + data.videoFiles.forEach((vf: any) => { + videoFiles.push({ + fileName: vf.fileName || vf.name || '강좌영상.mp4', + fileKey: vf.fileKey || vf.key, + url: vf.url || vf.videoUrl, + }); + }); + } + setExistingVideoFiles(videoFiles); + setCourseVideoCount(videoFiles.length); + } + + // 기존 VR 파일 + if (data.webglUrl || data.vrUrl || data.webglKey || data.vrKey) { + const vrFiles = []; + if (data.webglUrl || data.vrUrl) { + vrFiles.push({ + fileName: data.vrFileName || data.webglFileName || 'VR_콘텐츠.zip', + fileKey: data.webglKey || data.vrKey, + url: data.webglUrl || data.vrUrl, + }); + } + setExistingVrFiles(vrFiles); + setVrContentCount(vrFiles.length); + } + + // 기존 평가 문제 파일 + if (data.csvKey || data.csvUrl) { + setExistingQuestionFile({ + fileName: '평가문제.csv', + fileKey: data.csvKey, + }); + setQuestionFileCount(1); + } + } catch (err) { + console.error('강좌 로드 실패:', err); + + // 기본값 설정 (에러 발생 시에도 폼이 작동하도록) + const defaultOriginal = { + title: '', + objective: '', + videoUrl: undefined, + webglUrl: undefined, + csvUrl: undefined, + }; + setOriginalData(defaultOriginal); + + alert('강좌를 불러오는데 실패했습니다. 페이지를 새로고침하거나 다시 시도해주세요.'); + // 에러 발생 시에도 페이지는 유지 (사용자가 수동으로 데이터 입력 가능) + } finally { + setLoading(false); + } + }; + + loadLecture(); + }, [params?.id, router]); + + // 외부 클릭 시 드롭다운 닫기 + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if ( + dropdownRef.current && + !dropdownRef.current.contains(event.target as Node) + ) { + setIsDropdownOpen(false); + } + }; + + if (isDropdownOpen) { + document.addEventListener("mousedown", handleClickOutside); + } + + return () => { + document.removeEventListener("mousedown", handleClickOutside); + }; + }, [isDropdownOpen]); + + // 토스트 자동 닫기 + useEffect(() => { + if (showToast) { + const timer = setTimeout(() => { + setShowToast(false); + }, 3000); // 3초 후 자동 닫기 + + return () => clearTimeout(timer); + } + }, [showToast]); + + const handleBackClick = () => { + router.push(`/admin/lessons/${params.id}`); + }; + + const handleSaveClick = async () => { + // 유효성 검사 + const newErrors: { + selectedCourse?: string; + lessonName?: string; + learningGoal?: string; + } = {}; + + if (!selectedCourse) { + newErrors.selectedCourse = "교육과정을 선택해 주세요."; + } + if (!lessonName.trim()) { + newErrors.lessonName = "강좌명을 입력해 주세요."; + } + if (!learningGoal.trim()) { + newErrors.learningGoal = "학습 목표를 입력해 주세요."; + } + + setErrors(newErrors); + + if (Object.keys(newErrors).length > 0) { + return; + } + + if (isSaving) return; + + setIsSaving(true); + + try { + if (!originalData) { + alert('데이터를 불러오는 중입니다. 잠시 후 다시 시도해주세요.'); + return; + } + + // 새로 업로드된 파일들 처리 + let videoUrl: string | undefined; + let webglUrl: string | undefined; + let csvUrl: string | undefined; + + // 강좌 영상 업로드 (새 파일이 있는 경우) + if (courseVideoFileObjects.length > 0) { + try { + const uploadResponse = await apiService.uploadFiles(courseVideoFileObjects); + // 다중 파일 업로드 응답 처리 (실제 API 응답 구조에 맞게 조정 필요) + if (uploadResponse.data && uploadResponse.data.length > 0) { + videoUrl = uploadResponse.data[0].url || uploadResponse.data[0].fileKey; + } + } catch (error) { + console.error('강좌 영상 업로드 실패:', error); + throw new Error('강좌 영상 업로드에 실패했습니다.'); + } + } else if (existingVideoFiles.length > 0 && existingVideoFiles[0].url) { + // 기존 파일 URL 유지 + videoUrl = existingVideoFiles[0].url; + } + + // VR 콘텐츠 업로드 (새 파일이 있는 경우) + if (vrContentFileObjects.length > 0) { + try { + const uploadResponse = await apiService.uploadFiles(vrContentFileObjects); + if (uploadResponse.data && uploadResponse.data.length > 0) { + webglUrl = uploadResponse.data[0].url || uploadResponse.data[0].fileKey; + } + } catch (error) { + console.error('VR 콘텐츠 업로드 실패:', error); + throw new Error('VR 콘텐츠 업로드에 실패했습니다.'); + } + } else if (existingVrFiles.length > 0 && existingVrFiles[0].url) { + // 기존 파일 URL 유지 + webglUrl = existingVrFiles[0].url; + } + + // 학습 평가 문제 업로드 (새 파일이 있는 경우) + if (questionFileObject) { + try { + const uploadResponse = await apiService.uploadFile(questionFileObject); + const fileKey = uploadResponse.data?.key || uploadResponse.data?.fileKey || uploadResponse.data?.csvKey; + if (fileKey) { + csvUrl = `csv/${fileKey}`; + } else if (uploadResponse.data?.url) { + csvUrl = uploadResponse.data.url; + } + } catch (error) { + console.error('학습 평가 문제 업로드 실패:', error); + throw new Error('학습 평가 문제 업로드에 실패했습니다.'); + } + } else if (existingQuestionFile?.fileKey) { + // 기존 파일 URL 유지 + csvUrl = `csv/${existingQuestionFile.fileKey}`; + } else if (originalData.csvUrl) { + // 원본 데이터의 csvUrl 유지 + csvUrl = originalData.csvUrl; + } + + // 현재 값들 + const currentTitle = lessonName.trim(); + const currentObjective = learningGoal.trim(); + + // 변경된 항목만 포함하는 request body 생성 + const requestBody: { + title?: string; + objective?: string; + videoUrl?: string; + webglUrl?: string; + csvUrl?: string; + } = {}; + + // 변경된 항목만 추가 + if (currentTitle !== originalData.title) { + requestBody.title = currentTitle; + } + if (currentObjective !== originalData.objective) { + requestBody.objective = currentObjective; + } + + // videoUrl 변경사항 체크 (원본에 있었는데 삭제된 경우도 포함) + const originalVideoUrl = originalData.videoUrl; + if (videoUrl !== originalVideoUrl) { + // 원본에 있었는데 현재 없는 경우 (삭제된 경우) 빈 문자열로 처리 + if (originalVideoUrl && !videoUrl) { + requestBody.videoUrl = ''; + } else if (videoUrl) { + requestBody.videoUrl = videoUrl; + } + } + + // webglUrl 변경사항 체크 + const originalWebglUrl = originalData.webglUrl; + if (webglUrl !== originalWebglUrl) { + // 원본에 있었는데 현재 없는 경우 (삭제된 경우) 빈 문자열로 처리 + if (originalWebglUrl && !webglUrl) { + requestBody.webglUrl = ''; + } else if (webglUrl) { + requestBody.webglUrl = webglUrl; + } + } + + // csvUrl 변경사항 체크 + const originalCsvUrl = originalData.csvUrl; + if (csvUrl !== originalCsvUrl) { + // 원본에 있었는데 현재 없는 경우 (삭제된 경우) 빈 문자열로 처리 + if (originalCsvUrl && !csvUrl) { + requestBody.csvUrl = ''; + } else if (csvUrl) { + requestBody.csvUrl = csvUrl; + } + } + + // 변경사항이 없으면 알림 + if (Object.keys(requestBody).length === 0) { + alert('변경된 내용이 없습니다.'); + setIsSaving(false); + return; + } + + // 강좌 수정 API 호출 (PATCH /lectures/{id}) + await apiService.updateLecture(params.id as string, requestBody); + + // 성공 시 토스트 표시 + setShowToast(true); + + // 토스트 표시 후 상세 페이지로 이동 + setTimeout(() => { + router.push(`/admin/lessons/${params.id}`); + }, 1500); + } catch (error) { + console.error('강좌 수정 실패:', error); + const errorMessage = error instanceof Error ? error.message : '강좌 수정 중 오류가 발생했습니다.'; + alert(errorMessage); + } finally { + setIsSaving(false); + } + }; + + if (loading) { + return ( +
+
+
+
+ +
+
+
+
+
+ +

+ 강좌 수정 +

+
+
+
+
+

로딩 중...

+
+
+
+
+
+
+
+ ); + } + + return ( +
+
+
+
+ +
+
+
+
+ {/* 헤더 */} +
+
+ +

+ 강좌 수정 +

+
+
+ + {/* 폼 콘텐츠 */} +
+
+ {/* 강좌 정보 */} +
+

+ 강좌 정보 +

+
+ {/* 교육 과정 */} +
+ +
+ + {isDropdownOpen && ( +
+ {courseOptions.map((course, index) => ( + + ))} +
+ )} +
+ {errors.selectedCourse && ( +

+ {errors.selectedCourse} +

+ )} +
+ + {/* 강좌명 */} +
+ + { + setLessonName(e.target.value); + if (errors.lessonName) { + setErrors(prev => ({ ...prev, lessonName: undefined })); + } + }} + placeholder="강좌명을 입력해 주세요." + className={`h-[40px] px-[12px] py-[8px] border rounded-[8px] bg-white text-[16px] font-normal leading-[1.5] text-[#1b2027] placeholder:text-[#b1b8c0] focus:outline-none focus:shadow-[inset_0_0_0_1px_#333c47] ${ + errors.lessonName ? 'border-[#e63946]' : 'border-[#dee1e6]' + }`} + /> + {errors.lessonName && ( +

+ {errors.lessonName} +

+ )} +
+ + {/* 학습 목표 */} +
+ +
+