From eb7871133dba303e18864fad4a52009c7bcd8534 Mon Sep 17 00:00:00 2001 From: wallace Date: Sat, 29 Nov 2025 15:40:39 +0900 Subject: [PATCH] =?UTF-8?q?=EA=B3=B5=EC=A7=80=EC=82=AC=ED=95=AD=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=EB=93=B1=EB=A1=9D=20=EB=93=B111?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/admin/courses/page.tsx | 79 ++- src/app/admin/lessons/[id]/edit/page.tsx | 451 ++++++++++------- src/app/admin/lessons/[id]/page.tsx | 533 ++++++++++++++++---- src/app/admin/lessons/page.tsx | 322 ++++++++++-- src/app/admin/notices/NoticeCancelModal.tsx | 65 +++ src/app/admin/notices/NoticeDeleteModal.tsx | 70 +++ src/app/admin/notices/[id]/edit/page.tsx | 441 ++++++++++++++++ src/app/admin/notices/[id]/page.tsx | 92 +++- src/app/admin/notices/page.tsx | 20 +- src/app/lib/apiService.ts | 2 +- 10 files changed, 1663 insertions(+), 412 deletions(-) create mode 100644 src/app/admin/notices/NoticeCancelModal.tsx create mode 100644 src/app/admin/notices/NoticeDeleteModal.tsx create mode 100644 src/app/admin/notices/[id]/edit/page.tsx diff --git a/src/app/admin/courses/page.tsx b/src/app/admin/courses/page.tsx index 3ea88db..0e87c95 100644 --- a/src/app/admin/courses/page.tsx +++ b/src/app/admin/courses/page.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useState, useMemo, useEffect } from "react"; +import { useState, useMemo, useEffect, useRef } from "react"; import AdminSidebar from "@/app/components/AdminSidebar"; import CourseRegistrationModal from "./CourseRegistrationModal"; import ChevronDownSvg from "@/app/svgs/chevrondownsvg"; @@ -31,6 +31,8 @@ export default function AdminCoursesPage() { const [editingCourse, setEditingCourse] = useState(null); const [currentPage, setCurrentPage] = useState(1); const [showToast, setShowToast] = useState(false); + const prevModalOpenRef = useRef(false); + const shouldRefreshRef = useRef(false); // API에서 과목 리스트 가져오기 useEffect(() => { @@ -70,35 +72,9 @@ export default function AdminCoursesPage() { }, [sortedCourses, currentPage]); const handleSaveCourse = async (courseName: string, instructorName: string) => { - if (editingCourse) { - // 수정 모드 - TODO: API 호출로 변경 필요 - setCourses(prev => prev.map(course => - course.id === editingCourse.id - ? { ...course, courseName, instructorName } - : course - )); - } else { - // 등록 모드 - TODO: API 호출로 변경 필요 - const newCourse: Course = { - id: String(Date.now()), - courseName, - instructorName, - createdAt: new Date().toISOString().split('T')[0], - createdBy: '', // API에서 받아오도록 변경 필요 - hasLessons: false, // 기본값: 미포함 - }; - setCourses(prev => [...prev, newCourse]); - } + shouldRefreshRef.current = true; // 새로고침 플래그 설정 setIsModalOpen(false); setEditingCourse(null); - - // 저장 후 리스트 새로고침 - try { - const data = await getCourses(); - setCourses(data); - } catch (error) { - console.error('과목 리스트 새로고침 오류:', error); - } }; const handleRowClick = (course: Course) => { @@ -117,25 +93,38 @@ export default function AdminCoursesPage() { }; const handleDeleteCourse = async () => { - if (editingCourse) { - // TODO: API 호출로 삭제 처리 필요 - setCourses(prev => prev.filter(course => course.id !== editingCourse.id)); - setEditingCourse(null); - setShowToast(true); - setTimeout(() => { - setShowToast(false); - }, 3000); - - // 삭제 후 리스트 새로고침 - try { - const data = await getCourses(); - setCourses(data); - } catch (error) { - console.error('과목 리스트 새로고침 오류:', error); - } - } + shouldRefreshRef.current = true; // 새로고침 플래그 설정 + setEditingCourse(null); + setShowToast(true); + setTimeout(() => { + setShowToast(false); + }, 3000); }; + // 모달이 닫힌 후 리스트 새로고침 + useEffect(() => { + // 모달이 열렸다가 닫힐 때, 그리고 새로고침 플래그가 설정되어 있을 때만 새로고침 + if (prevModalOpenRef.current && !isModalOpen && shouldRefreshRef.current) { + shouldRefreshRef.current = false; // 플래그 리셋 + + async function refreshList() { + try { + setIsLoading(true); + const data = await getCourses(); + console.log('📋 [AdminCoursesPage] 새로고침된 데이터:', data); + console.log('📋 [AdminCoursesPage] 새로고침된 데이터 개수:', data.length); + setCourses(data); + } catch (error) { + console.error('과목 리스트 새로고침 오류:', error); + } finally { + setIsLoading(false); + } + } + refreshList(); + } + prevModalOpenRef.current = isModalOpen; + }, [isModalOpen]); + return (
{/* 메인 레이아웃 */} diff --git a/src/app/admin/lessons/[id]/edit/page.tsx b/src/app/admin/lessons/[id]/edit/page.tsx index 43be6fd..f6d95ed 100644 --- a/src/app/admin/lessons/[id]/edit/page.tsx +++ b/src/app/admin/lessons/[id]/edit/page.tsx @@ -16,6 +16,9 @@ export default function LessonEditPage() { const [isSaving, setIsSaving] = useState(false); const [isDropdownOpen, setIsDropdownOpen] = useState(false); const dropdownRef = useRef(null); + const videoFileInputRef = useRef(null); + const vrFileInputRef = useRef(null); + const csvFileInputRef = useRef(null); const [courses, setCourses] = useState([]); const [showToast, setShowToast] = useState(false); @@ -184,11 +187,134 @@ export default function LessonEditPage() { // 기존 평가 문제 파일 if (data.csvKey || data.csvUrl) { + const csvFileKey = data.csvKey || (data.csvUrl && data.csvUrl.startsWith('csv/') ? data.csvUrl.substring(4) : data.csvUrl); + setExistingQuestionFile({ fileName: data.csvFileName || data.csv_file_name || data.csvName || '평가문제.csv', - fileKey: data.csvKey, + fileKey: csvFileKey, }); setQuestionFileCount(1); + + // CSV 파일 다운로드 및 파싱 + if (data.csvUrl || data.csvKey) { + try { + const baseURL = process.env.NEXT_PUBLIC_API_BASE_URL || 'https://hrdi.coconutmeet.net'; + const token = typeof window !== 'undefined' ? localStorage.getItem('token') : null; + + let fileKey: string | null = null; + + // csvUrl에서 fileKey 추출 + if (data.csvUrl) { + // 전체 URL인 경우 fileKey 추출 + if (data.csvUrl.startsWith('http://') || data.csvUrl.startsWith('https://')) { + // URL에서 /api/files/ 이후 부분을 fileKey로 사용 + const filesIndex = data.csvUrl.indexOf('/api/files/'); + if (filesIndex !== -1) { + const extractedKey = data.csvUrl.substring(filesIndex + '/api/files/'.length); + // URL 디코딩 + fileKey = decodeURIComponent(extractedKey); + } else { + // URL에서 마지막 경로를 fileKey로 사용 + const urlParts = data.csvUrl.split('/'); + const lastPart = urlParts[urlParts.length - 1]; + if (lastPart) { + fileKey = decodeURIComponent(lastPart); + } + } + } else if (data.csvUrl.startsWith('csv/')) { + // "csv/" 접두사 제거 + fileKey = data.csvUrl.substring(4); + } else { + // 그 외의 경우 fileKey로 사용 + fileKey = data.csvUrl; + } + } else if (data.csvKey) { + // csvKey가 있으면 fileKey로 사용 + fileKey = data.csvKey; + } + + if (!fileKey) { + return; // fileKey가 없으면 종료 + } + + // /api/files/{fileKey} 형태로 요청 + const csvUrl = `${baseURL}/api/files/${encodeURIComponent(fileKey as string)}`; + + const csvResponse = await fetch(csvUrl, { + method: 'GET', + headers: { + ...(token && { Authorization: `Bearer ${token}` }), + }, + }); + + // 404 에러는 파일이 없는 것으로 간주하고 조용히 처리 + if (csvResponse.status === 404) { + console.warn('CSV 파일을 찾을 수 없습니다:', data.csvUrl || data.csvKey); + } else if (csvResponse.ok) { + const csvText = await csvResponse.text(); + + // CSV 파싱 함수 + const parseCsv = (csvText: string): string[][] => { + const lines: string[][] = []; + let currentLine: string[] = []; + let currentField = ''; + let inQuotes = false; + + for (let i = 0; i < csvText.length; i++) { + const char = csvText[i]; + const nextChar = csvText[i + 1]; + + if (char === '"') { + if (inQuotes && nextChar === '"') { + currentField += '"'; + i++; + } else { + inQuotes = !inQuotes; + } + } else if (char === ',' && !inQuotes) { + currentLine.push(currentField.trim()); + currentField = ''; + } else if ((char === '\n' || char === '\r') && !inQuotes) { + if (char === '\r' && nextChar === '\n') { + i++; + } + if (currentField || currentLine.length > 0) { + currentLine.push(currentField.trim()); + lines.push(currentLine); + currentLine = []; + currentField = ''; + } + } else { + currentField += char; + } + } + + if (currentField || currentLine.length > 0) { + currentLine.push(currentField.trim()); + lines.push(currentLine); + } + + return lines; + }; + + const parsed = parseCsv(csvText); + + if (parsed.length > 0) { + const headers = parsed[0]; + const rows = parsed.slice(1); + + setCsvHeaders(headers); + setCsvRows(rows); + setCsvData(parsed); + } + } else if (!csvResponse.ok) { + console.error(`CSV 파일을 가져오는데 실패했습니다. (${csvResponse.status})`); + } + } catch (csvError) { + console.error('CSV 파일 파싱 실패:', csvError); + // CSV 파싱 실패해도 계속 진행 + } + } } } catch (err) { console.error('강좌 로드 실패:', err); @@ -365,82 +491,94 @@ export default function LessonEditPage() { const handleCsvFileUpload = async (file: File) => { try { - // CSV 파일 파싱 - const reader = new FileReader(); - reader.onload = (event) => { - const text = event.target?.result as string; - if (!text) return; + // CSV 파일 파싱 (Promise로 감싸서 파일 읽기 완료 대기) + const parseCsvFile = (): Promise => { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + + reader.onload = (event) => { + const text = event.target?.result as string; + if (!text) { + reject(new Error('파일을 읽을 수 없습니다.')); + return; + } - try { - // CSV 파싱 함수 - const parseCsv = (csvText: string): string[][] => { - const lines: string[][] = []; - let currentLine: string[] = []; - let currentField = ''; - let inQuotes = false; + try { + // CSV 파싱 함수 + const parseCsv = (csvText: string): string[][] => { + const lines: string[][] = []; + let currentLine: string[] = []; + let currentField = ''; + let inQuotes = false; - for (let i = 0; i < csvText.length; i++) { - const char = csvText[i]; - const nextChar = csvText[i + 1]; + for (let i = 0; i < csvText.length; i++) { + const char = csvText[i]; + const nextChar = csvText[i + 1]; - if (char === '"') { - if (inQuotes && nextChar === '"') { - currentField += '"'; - i++; - } else { - inQuotes = !inQuotes; - } - } else if (char === ',' && !inQuotes) { - currentLine.push(currentField.trim()); - currentField = ''; - } else if ((char === '\n' || char === '\r') && !inQuotes) { - if (char === '\r' && nextChar === '\n') { - i++; + if (char === '"') { + if (inQuotes && nextChar === '"') { + currentField += '"'; + i++; + } else { + inQuotes = !inQuotes; + } + } else if (char === ',' && !inQuotes) { + currentLine.push(currentField.trim()); + currentField = ''; + } else if ((char === '\n' || char === '\r') && !inQuotes) { + if (char === '\r' && nextChar === '\n') { + i++; + } + if (currentField || currentLine.length > 0) { + currentLine.push(currentField.trim()); + lines.push(currentLine); + currentLine = []; + currentField = ''; + } + } else { + currentField += char; + } } + if (currentField || currentLine.length > 0) { currentLine.push(currentField.trim()); lines.push(currentLine); - currentLine = []; - currentField = ''; } - } else { - currentField += char; + + return lines; + }; + + const parsed = parseCsv(text); + + if (parsed.length === 0) { + reject(new Error('CSV 파일이 비어있습니다.')); + return; } - } - if (currentField || currentLine.length > 0) { - currentLine.push(currentField.trim()); - lines.push(currentLine); + resolve(parsed); + } catch (parseError) { + reject(new Error('CSV 파일을 읽는 중 오류가 발생했습니다.')); } - - return lines; }; - const parsed = parseCsv(text); - - if (parsed.length === 0) { - alert('CSV 파일이 비어있습니다.'); - return; - } + reader.onerror = () => { + reject(new Error('파일을 읽는 중 오류가 발생했습니다.')); + }; - // 첫 번째 줄을 헤더로 사용 - const headers = parsed[0]; - const rows = parsed.slice(1); - - setCsvHeaders(headers); - setCsvRows(rows); - setCsvData(parsed); - } catch (parseError) { - console.error('CSV 파싱 오류:', parseError); - alert('CSV 파일을 읽는 중 오류가 발생했습니다.'); - } + reader.readAsText(file, 'UTF-8'); + }); }; - reader.onerror = () => { - alert('파일을 읽는 중 오류가 발생했습니다.'); - }; + // CSV 파일 파싱 + const parsed = await parseCsvFile(); + + // 첫 번째 줄을 헤더로 사용 + const headers = parsed[0]; + const rows = parsed.slice(1); - reader.readAsText(file, 'UTF-8'); + setCsvHeaders(headers); + setCsvRows(rows); + setCsvData(parsed); // 단일 파일 업로드 const uploadResponse = await apiService.uploadFile(file); @@ -465,22 +603,24 @@ export default function LessonEditPage() { } } catch (error) { console.error('학습 평가 문제 업로드 실패:', error); - alert('파일 업로드에 실패했습니다. 다시 시도해주세요.'); + const errorMessage = error instanceof Error ? error.message : '파일 업로드에 실패했습니다. 다시 시도해주세요.'; + alert(errorMessage); } }; // 파일 교체 확인 모달 핸들러 - const handleFileReplaceConfirm = async () => { - if (!pendingFiles.length || !pendingFileType) return; + const handleFileReplaceConfirm = () => { + if (!pendingFileType) return; setIsFileReplaceModalOpen(false); - if (pendingFileType === 'video') { - await handleVideoFileUpload(pendingFiles); - } else if (pendingFileType === 'vr') { - await handleVrFileUpload(pendingFiles); - } else if (pendingFileType === 'csv' && pendingFiles.length > 0) { - await handleCsvFileUpload(pendingFiles[0]); + // 파일 선택 다이얼로그 열기 + if (pendingFileType === 'video' && videoFileInputRef.current) { + videoFileInputRef.current.click(); + } else if (pendingFileType === 'vr' && vrFileInputRef.current) { + vrFileInputRef.current.click(); + } else if (pendingFileType === 'csv' && csvFileInputRef.current) { + csvFileInputRef.current.click(); } setPendingFiles([]); @@ -634,9 +774,9 @@ export default function LessonEditPage() { // 성공 시 토스트 표시 setShowToast(true); - // 토스트 표시 후 상세 페이지로 이동 + // 토스트 표시 후 상세 페이지로 이동 (새로고침하여 최신 데이터 표시) setTimeout(() => { - router.push(`/admin/lessons/${params.id}`); + router.push(`/admin/lessons/${params.id}?refresh=${Date.now()}`); }, 1500); } catch (error) { console.error('강좌 수정 실패:', error); @@ -858,9 +998,21 @@ export default function LessonEditPage() { 30MB 미만 파일
-