diff --git a/src/app/admin/courses/CourseRegistrationModal.tsx b/src/app/admin/courses/CourseRegistrationModal.tsx index cb21ebc..5360e69 100644 --- a/src/app/admin/courses/CourseRegistrationModal.tsx +++ b/src/app/admin/courses/CourseRegistrationModal.tsx @@ -42,8 +42,8 @@ export default function CourseRegistrationModal({ open, onClose, onSave, onDelet setIsLoadingInstructors(true); try { - // 외부 API 호출 - const response = await apiService.getUsersCompact(); + // 외부 API 호출 - type이 'ADMIN'인 사용자만 조회 + const response = await apiService.getUsersCompact({ type: 'ADMIN', limit: 10 }); const data = response.data; // API 응답이 배열이 아닌 경우 처리 (예: { items: [...] } 형태) @@ -57,50 +57,50 @@ export default function CourseRegistrationModal({ open, onClose, onSave, onDelet // API 응답 데이터를 UserRow 형식으로 변환 const transformedUsers: UserRow[] = usersArray.length > 0 ? usersArray.map((user: any) => { - // 가입일을 YYYY-MM-DD 형식으로 변환 - const formatDate = (dateString: string | null | undefined): string => { - if (!dateString) return new Date().toISOString().split('T')[0]; - try { - const date = new Date(dateString); - return date.toISOString().split('T')[0]; - } catch { - return new Date().toISOString().split('T')[0]; + // 가입일을 YYYY-MM-DD 형식으로 변환 + const formatDate = (dateString: string | null | undefined): string => { + if (!dateString) return new Date().toISOString().split('T')[0]; + try { + const date = new Date(dateString); + return date.toISOString().split('T')[0]; + } catch { + return new Date().toISOString().split('T')[0]; + } + }; + + // null 값을 명시적으로 처리 + const getValue = (value: any, fallback: string = '-') => { + if (value === null || value === undefined) return fallback; + if (typeof value === 'string' && value.trim() === '') return fallback; + return String(value); + }; + + // status가 "ACTIVE"이면 활성화, 아니면 비활성화 + const accountStatus: 'active' | 'inactive' = + user.status === 'ACTIVE' || user.status === 'active' ? 'active' : 'inactive'; + + // role 데이터 처리 + let userRole: 'learner' | 'instructor' | 'admin' = 'learner'; // 기본값 + if (user.role) { + const roleLower = String(user.role).toLowerCase(); + if (roleLower === 'instructor' || roleLower === '강사') { + userRole = 'instructor'; + } else if (roleLower === 'admin' || roleLower === '관리자') { + userRole = 'admin'; + } else { + userRole = 'learner'; + } } - }; - - // null 값을 명시적으로 처리 - const getValue = (value: any, fallback: string = '-') => { - if (value === null || value === undefined) return fallback; - if (typeof value === 'string' && value.trim() === '') return fallback; - return String(value); - }; - - // status가 "ACTIVE"이면 활성화, 아니면 비활성화 - const accountStatus: 'active' | 'inactive' = - user.status === 'ACTIVE' || user.status === 'active' ? 'active' : 'inactive'; - - // role 데이터 처리 - let userRole: 'learner' | 'instructor' | 'admin' = 'learner'; // 기본값 - if (user.role) { - const roleLower = String(user.role).toLowerCase(); - if (roleLower === 'instructor' || roleLower === '강사') { - userRole = 'instructor'; - } else if (roleLower === 'admin' || roleLower === '관리자') { - userRole = 'admin'; - } else { - userRole = 'learner'; - } - } - - return { - id: String(user.id || user.userId || Math.random()), - joinDate: formatDate(user.createdAt || user.joinDate || user.join_date), - name: getValue(user.name || user.userName, '-'), - email: getValue(user.email || user.userEmail, '-'), - role: userRole, - status: accountStatus, - }; - }) + + return { + id: String(user.id || user.userId || Math.random()), + joinDate: formatDate(user.createdAt || user.joinDate || user.join_date), + name: getValue(user.name || user.userName, '-'), + email: getValue(user.email || user.userEmail, '-'), + role: userRole, + status: accountStatus, + }; + }) : []; setInstructors(transformedUsers); @@ -322,10 +322,21 @@ export default function CourseRegistrationModal({ open, onClose, onSave, onDelet } else { // 등록 모드: POST /subjects try { - await apiService.createSubject({ - courseName: courseName.trim(), - instructorName: selectedInstructor.name, - }); + const createRequestBody: { + title: string; + instructor: string; + imageKey?: string; + } = { + title: courseName.trim(), + instructor: selectedInstructor.name, + }; + + // imageKey 처리: 등록 모드에서 새 이미지가 있으면 포함 + if (imageKey) { + createRequestBody.imageKey = imageKey; + } + + await apiService.createSubject(createRequestBody); // 성공 시 onSave 콜백 호출 및 모달 닫기 if (onSave && selectedInstructor) { diff --git a/src/app/admin/lessons/[id]/edit/page.tsx b/src/app/admin/lessons/[id]/edit/page.tsx index 3f93a36..43be6fd 100644 --- a/src/app/admin/lessons/[id]/edit/page.tsx +++ b/src/app/admin/lessons/[id]/edit/page.tsx @@ -41,6 +41,11 @@ export default function LessonEditPage() { const [csvHeaders, setCsvHeaders] = useState([]); const [csvRows, setCsvRows] = useState([]); + // 파일 교체 확인 모달 상태 + const [isFileReplaceModalOpen, setIsFileReplaceModalOpen] = useState(false); + const [pendingFiles, setPendingFiles] = useState([]); + const [pendingFileType, setPendingFileType] = useState<'video' | 'vr' | 'csv' | null>(null); + // 원본 데이터 저장 (변경사항 비교용) const [originalData, setOriginalData] = useState<{ title?: string; @@ -243,6 +248,251 @@ export default function LessonEditPage() { router.push(`/admin/lessons/${params.id}`); }; + // 파일 업로드 함수 + const handleVideoFileUpload = async (validFiles: File[]) => { + try { + let fileKeys: string[] = []; + + // 파일이 1개인 경우 uploadFile 사용 + if (validFiles.length === 1) { + const uploadResponse = await apiService.uploadFile(validFiles[0]); + + // 응답에서 fileKey 추출 + const fileKey = uploadResponse.data?.fileKey + || uploadResponse.data?.key + || uploadResponse.data?.id + || uploadResponse.data?.fileId; + + if (fileKey) { + fileKeys = [fileKey]; + } else { + throw new Error('파일 업로드는 완료되었지만 fileKey를 받지 못했습니다.'); + } + } else { + // 파일이 2개 이상인 경우 uploadFiles 사용 + const uploadResponse = await apiService.uploadFiles(validFiles); + + // 응답에서 fileKey 배열 추출 + if (uploadResponse.data?.results && Array.isArray(uploadResponse.data.results)) { + uploadResponse.data.results.forEach((result: any) => { + if (result.ok && result.fileKey) { + fileKeys.push(result.fileKey); + } else if (result.fileKey) { + fileKeys.push(result.fileKey); + } + }); + } else if (Array.isArray(uploadResponse.data)) { + // 응답이 배열인 경우 + fileKeys = uploadResponse.data.map((item: any) => + item.fileKey || item.key || item.id || item.fileId + ).filter(Boolean); + } else if (uploadResponse.data?.fileKeys && Array.isArray(uploadResponse.data.fileKeys)) { + fileKeys = uploadResponse.data.fileKeys; + } + } + + if (fileKeys.length > 0) { + // 새로 첨부한 파일로 교체 (기존 새로 첨부한 파일은 삭제, 서버에 저장된 기존 파일은 유지) + setCourseVideoFiles(validFiles.map(f => f.name)); + setCourseVideoFileObjects(validFiles); + setCourseVideoFileKeys(fileKeys); + setCourseVideoCount(existingVideoFiles.length + fileKeys.length); + } else { + throw new Error('파일 업로드는 완료되었지만 fileKey를 받지 못했습니다.'); + } + } catch (error) { + console.error('강좌 영상 업로드 실패:', error); + alert('파일 업로드에 실패했습니다. 다시 시도해주세요.'); + } + }; + + const handleVrFileUpload = async (validFiles: File[]) => { + try { + let fileKeys: string[] = []; + + // 파일이 1개인 경우 uploadFile 사용 + if (validFiles.length === 1) { + const uploadResponse = await apiService.uploadFile(validFiles[0]); + + // 응답에서 fileKey 추출 + const fileKey = uploadResponse.data?.fileKey + || uploadResponse.data?.key + || uploadResponse.data?.id + || uploadResponse.data?.fileId; + + if (fileKey) { + fileKeys = [fileKey]; + } else { + throw new Error('파일 업로드는 완료되었지만 fileKey를 받지 못했습니다.'); + } + } else { + // 파일이 2개 이상인 경우 uploadFiles 사용 + const uploadResponse = await apiService.uploadFiles(validFiles); + + // 응답에서 fileKey 배열 추출 + if (uploadResponse.data?.results && Array.isArray(uploadResponse.data.results)) { + uploadResponse.data.results.forEach((result: any) => { + if (result.ok && result.fileKey) { + fileKeys.push(result.fileKey); + } else if (result.fileKey) { + fileKeys.push(result.fileKey); + } + }); + } else if (Array.isArray(uploadResponse.data)) { + // 응답이 배열인 경우 + fileKeys = uploadResponse.data.map((item: any) => + item.fileKey || item.key || item.id || item.fileId + ).filter(Boolean); + } else if (uploadResponse.data?.fileKeys && Array.isArray(uploadResponse.data.fileKeys)) { + fileKeys = uploadResponse.data.fileKeys; + } + } + + if (fileKeys.length > 0) { + // 새로 첨부한 파일로 교체 (기존 새로 첨부한 파일은 삭제, 서버에 저장된 기존 파일은 유지) + setVrContentFiles(validFiles.map(f => f.name)); + setVrContentFileObjects(validFiles); + setVrContentFileKeys(fileKeys); + setVrContentCount(existingVrFiles.length + fileKeys.length); + } else { + throw new Error('파일 업로드는 완료되었지만 fileKey를 받지 못했습니다.'); + } + } catch (error) { + console.error('VR 콘텐츠 업로드 실패:', error); + alert('파일 업로드에 실패했습니다. 다시 시도해주세요.'); + } + }; + + const handleCsvFileUpload = async (file: File) => { + try { + // CSV 파일 파싱 + const reader = new FileReader(); + reader.onload = (event) => { + const text = event.target?.result as string; + if (!text) return; + + 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]; + + 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(text); + + if (parsed.length === 0) { + alert('CSV 파일이 비어있습니다.'); + return; + } + + // 첫 번째 줄을 헤더로 사용 + const headers = parsed[0]; + const rows = parsed.slice(1); + + setCsvHeaders(headers); + setCsvRows(rows); + setCsvData(parsed); + } catch (parseError) { + console.error('CSV 파싱 오류:', parseError); + alert('CSV 파일을 읽는 중 오류가 발생했습니다.'); + } + }; + + reader.onerror = () => { + alert('파일을 읽는 중 오류가 발생했습니다.'); + }; + + reader.readAsText(file, 'UTF-8'); + + // 단일 파일 업로드 + const uploadResponse = await apiService.uploadFile(file); + + // 응답에서 fileKey 추출 + const fileKey = uploadResponse.data?.fileKey + || uploadResponse.data?.key + || uploadResponse.data?.id + || uploadResponse.data?.fileId + || uploadResponse.data?.imageKey + || (uploadResponse.data?.data && (uploadResponse.data.data.key || uploadResponse.data.data.fileKey)) + || null; + + if (fileKey) { + // 이전 파일 삭제하고 새 파일로 교체 + setQuestionFileObject(file); + setQuestionFileKey(fileKey); + setQuestionFileCount(1); + setExistingQuestionFile(null); + } else { + throw new Error('파일 업로드는 완료되었지만 fileKey를 받지 못했습니다.'); + } + } catch (error) { + console.error('학습 평가 문제 업로드 실패:', error); + alert('파일 업로드에 실패했습니다. 다시 시도해주세요.'); + } + }; + + // 파일 교체 확인 모달 핸들러 + const handleFileReplaceConfirm = async () => { + if (!pendingFiles.length || !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]); + } + + setPendingFiles([]); + setPendingFileType(null); + }; + + const handleFileReplaceCancel = () => { + setIsFileReplaceModalOpen(false); + setPendingFiles([]); + setPendingFileType(null); + }; + const handleSaveClick = async () => { // 유효성 검사 const newErrors: { @@ -666,32 +916,17 @@ export default function LessonEditPage() { } if (validFiles.length > 0) { - try { - // 다중 파일 업로드 - const uploadResponse = await apiService.uploadFiles(validFiles); - - // 응답에서 fileKey 배열 추출 - const fileKeys: string[] = []; - if (uploadResponse.data?.results && Array.isArray(uploadResponse.data.results)) { - uploadResponse.data.results.forEach((result: any) => { - if (result.ok && result.fileKey) { - fileKeys.push(result.fileKey); - } - }); - } - - if (fileKeys.length > 0) { - setCourseVideoFiles(prev => [...prev, ...validFiles.map(f => f.name)]); - setCourseVideoFileObjects(prev => [...prev, ...validFiles]); - setCourseVideoFileKeys(prev => [...prev, ...fileKeys]); - setCourseVideoCount(prev => prev + validFiles.length); - } else { - throw new Error('파일 업로드는 완료되었지만 fileKey를 받지 못했습니다.'); - } - } catch (error) { - console.error('강좌 영상 업로드 실패:', error); - alert('파일 업로드에 실패했습니다. 다시 시도해주세요.'); + // 새로 첨부한 파일이 있으면 확인 모달 표시 + if (courseVideoFiles.length > 0 || courseVideoFileKeys.length > 0) { + setPendingFiles(validFiles); + setPendingFileType('video'); + setIsFileReplaceModalOpen(true); + e.target.value = ''; + return; } + + // 새로 첨부한 파일이 없으면 바로 업로드 + handleVideoFileUpload(validFiles); } e.target.value = ''; }} @@ -825,32 +1060,17 @@ export default function LessonEditPage() { } if (validFiles.length > 0) { - try { - // 다중 파일 업로드 - const uploadResponse = await apiService.uploadFiles(validFiles); - - // 응답에서 fileKey 배열 추출 - const fileKeys: string[] = []; - if (uploadResponse.data?.results && Array.isArray(uploadResponse.data.results)) { - uploadResponse.data.results.forEach((result: any) => { - if (result.ok && result.fileKey) { - fileKeys.push(result.fileKey); - } - }); - } - - if (fileKeys.length > 0) { - setVrContentFiles(prev => [...prev, ...validFiles.map(f => f.name)]); - setVrContentFileObjects(prev => [...prev, ...validFiles]); - setVrContentFileKeys(prev => [...prev, ...fileKeys]); - setVrContentCount(prev => prev + validFiles.length); - } else { - throw new Error('파일 업로드는 완료되었지만 fileKey를 받지 못했습니다.'); - } - } catch (error) { - console.error('VR 콘텐츠 업로드 실패:', error); - alert('파일 업로드에 실패했습니다. 다시 시도해주세요.'); + // 새로 첨부한 파일이 있으면 확인 모달 표시 + if (vrContentFiles.length > 0 || vrContentFileKeys.length > 0) { + setPendingFiles(validFiles); + setPendingFileType('vr'); + setIsFileReplaceModalOpen(true); + e.target.value = ''; + return; } + + // 새로 첨부한 파일이 없으면 바로 업로드 + handleVrFileUpload(validFiles); } e.target.value = ''; }} @@ -1024,38 +1244,17 @@ export default function LessonEditPage() { reader.readAsText(file, 'UTF-8'); - // 단일 파일 업로드 - const uploadResponse = await apiService.uploadFile(file); - - // 응답에서 fileKey 추출 - let fileKey: string | null = null; - if (uploadResponse.data?.fileKey) { - fileKey = uploadResponse.data.fileKey; - } else if (uploadResponse.data?.key) { - fileKey = uploadResponse.data.key; - } else if (uploadResponse.data?.id) { - fileKey = uploadResponse.data.id; - } else if (uploadResponse.data?.imageKey) { - fileKey = uploadResponse.data.imageKey; - } else if (uploadResponse.data?.fileId) { - fileKey = uploadResponse.data.fileId; - } else if (uploadResponse.data?.data && (uploadResponse.data.data.key || uploadResponse.data.data.fileKey)) { - fileKey = uploadResponse.data.data.key || uploadResponse.data.data.fileKey; - } else if (uploadResponse.data?.results && Array.isArray(uploadResponse.data.results) && uploadResponse.data.results.length > 0) { - const result = uploadResponse.data.results[0]; - if (result.ok && result.fileKey) { - fileKey = result.fileKey; - } + // 기존 파일이 있으면 확인 모달 표시 + if (questionFileObject || questionFileKey) { + setPendingFiles([file]); + setPendingFileType('csv'); + setIsFileReplaceModalOpen(true); + e.target.value = ''; + return; } - if (fileKey) { - setQuestionFileObject(file); - setQuestionFileKey(fileKey); - setQuestionFileCount(1); - setExistingQuestionFile(null); - } else { - throw new Error('파일 업로드는 완료되었지만 fileKey를 받지 못했습니다.'); - } + // 기존 파일이 없으면 바로 업로드 + await handleCsvFileUpload(file); } catch (error) { console.error('학습 평가 문제 업로드 실패:', error); alert('파일 업로드에 실패했습니다. 다시 시도해주세요.'); @@ -1203,6 +1402,52 @@ export default function LessonEditPage() { )} + + {/* 파일 교체 확인 모달 */} + {isFileReplaceModalOpen && ( +
+ + )}
); } diff --git a/src/app/admin/lessons/page.tsx b/src/app/admin/lessons/page.tsx index 64f33e2..d0f56cf 100644 --- a/src/app/admin/lessons/page.tsx +++ b/src/app/admin/lessons/page.tsx @@ -51,6 +51,11 @@ export default function AdminLessonsPage() { const [csvHeaders, setCsvHeaders] = useState([]); const [csvRows, setCsvRows] = useState([]); + // 파일 교체 확인 모달 상태 + const [isFileReplaceModalOpen, setIsFileReplaceModalOpen] = useState(false); + const [pendingFiles, setPendingFiles] = useState([]); + const [pendingFileType, setPendingFileType] = useState<'video' | 'vr' | 'csv' | null>(null); + // 에러 상태 const [errors, setErrors] = useState<{ selectedCourse?: string; @@ -245,6 +250,250 @@ export default function AdminLessonsPage() { setIsRegistrationMode(true); }; + // 파일 업로드 함수 + const handleVideoFileUpload = async (validFiles: File[]) => { + try { + let fileKeys: string[] = []; + + // 파일이 1개인 경우 uploadFile 사용 + if (validFiles.length === 1) { + const uploadResponse = await apiService.uploadFile(validFiles[0]); + + // 응답에서 fileKey 추출 + const fileKey = uploadResponse.data?.fileKey + || uploadResponse.data?.key + || uploadResponse.data?.id + || uploadResponse.data?.fileId; + + if (fileKey) { + fileKeys = [fileKey]; + } else { + throw new Error('파일 업로드는 완료되었지만 fileKey를 받지 못했습니다.'); + } + } else { + // 파일이 2개 이상인 경우 uploadFiles 사용 + const uploadResponse = await apiService.uploadFiles(validFiles); + + // 응답에서 fileKey 배열 추출 + if (uploadResponse.data?.results && Array.isArray(uploadResponse.data.results)) { + uploadResponse.data.results.forEach((result: any) => { + if (result.ok && result.fileKey) { + fileKeys.push(result.fileKey); + } else if (result.fileKey) { + fileKeys.push(result.fileKey); + } + }); + } else if (Array.isArray(uploadResponse.data)) { + // 응답이 배열인 경우 + fileKeys = uploadResponse.data.map((item: any) => + item.fileKey || item.key || item.id || item.fileId + ).filter(Boolean); + } else if (uploadResponse.data?.fileKeys && Array.isArray(uploadResponse.data.fileKeys)) { + fileKeys = uploadResponse.data.fileKeys; + } + } + + if (fileKeys.length > 0) { + // 이전 파일 삭제하고 새 파일로 교체 + setCourseVideoFiles(validFiles.map(f => f.name)); + setCourseVideoFileObjects(validFiles); + setCourseVideoFileKeys(fileKeys); + setCourseVideoCount(fileKeys.length); + } else { + throw new Error('파일 업로드는 완료되었지만 fileKey를 받지 못했습니다.'); + } + } catch (error) { + console.error('강좌 영상 업로드 실패:', error); + alert('파일 업로드에 실패했습니다. 다시 시도해주세요.'); + } + }; + + const handleVrFileUpload = async (validFiles: File[]) => { + try { + let fileKeys: string[] = []; + + // 파일이 1개인 경우 uploadFile 사용 + if (validFiles.length === 1) { + const uploadResponse = await apiService.uploadFile(validFiles[0]); + + // 응답에서 fileKey 추출 + const fileKey = uploadResponse.data?.fileKey + || uploadResponse.data?.key + || uploadResponse.data?.id + || uploadResponse.data?.fileId; + + if (fileKey) { + fileKeys = [fileKey]; + } else { + throw new Error('파일 업로드는 완료되었지만 fileKey를 받지 못했습니다.'); + } + } else { + // 파일이 2개 이상인 경우 uploadFiles 사용 + const uploadResponse = await apiService.uploadFiles(validFiles); + + // 응답에서 fileKey 배열 추출 + if (uploadResponse.data?.results && Array.isArray(uploadResponse.data.results)) { + uploadResponse.data.results.forEach((result: any) => { + if (result.ok && result.fileKey) { + fileKeys.push(result.fileKey); + } else if (result.fileKey) { + fileKeys.push(result.fileKey); + } + }); + } else if (Array.isArray(uploadResponse.data)) { + // 응답이 배열인 경우 + fileKeys = uploadResponse.data.map((item: any) => + item.fileKey || item.key || item.id || item.fileId + ).filter(Boolean); + } else if (uploadResponse.data?.fileKeys && Array.isArray(uploadResponse.data.fileKeys)) { + fileKeys = uploadResponse.data.fileKeys; + } + } + + if (fileKeys.length > 0) { + // 이전 파일 삭제하고 새 파일로 교체 + setVrContentFiles(validFiles.map(f => f.name)); + setVrContentFileObjects(validFiles); + setVrContentFileKeys(fileKeys); + setVrContentCount(fileKeys.length); + } else { + throw new Error('파일 업로드는 완료되었지만 fileKey를 받지 못했습니다.'); + } + } catch (error) { + console.error('VR 콘텐츠 업로드 실패:', error); + alert('파일 업로드에 실패했습니다. 다시 시도해주세요.'); + } + }; + + const handleCsvFileUpload = async (file: File) => { + try { + // CSV 파일 파싱 + const reader = new FileReader(); + reader.onload = (event) => { + const text = event.target?.result as string; + if (!text) return; + + 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]; + + 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(text); + + if (parsed.length === 0) { + alert('CSV 파일이 비어있습니다.'); + return; + } + + // 첫 번째 줄을 헤더로 사용 + const headers = parsed[0]; + const rows = parsed.slice(1); + + setCsvHeaders(headers); + setCsvRows(rows); + setCsvData(parsed); + } catch (parseError) { + console.error('CSV 파싱 오류:', parseError); + alert('CSV 파일을 읽는 중 오류가 발생했습니다.'); + } + }; + + reader.onerror = () => { + alert('파일을 읽는 중 오류가 발생했습니다.'); + }; + + reader.readAsText(file, 'UTF-8'); + + // 단일 파일 업로드 + const uploadResponse = await apiService.uploadFile(file); + + // 응답에서 fileKey 추출 + const fileKey = uploadResponse.data?.fileKey + || uploadResponse.data?.key + || uploadResponse.data?.id + || uploadResponse.data?.fileId + || uploadResponse.data?.imageKey + || (uploadResponse.data?.data && (uploadResponse.data.data.key || uploadResponse.data.data.fileKey)) + || null; + + if (fileKey) { + // 이전 파일 삭제하고 새 파일로 교체 + setQuestionFileObject(file); + setQuestionFileKey(fileKey); + setQuestionFileCount(1); + } else { + throw new Error('파일 업로드는 완료되었지만 fileKey를 받지 못했습니다.'); + } + } catch (error) { + console.error('학습 평가 문제 업로드 실패:', error); + alert('파일 업로드에 실패했습니다. 다시 시도해주세요.'); + } + }; + + // 파일 교체 확인 모달 핸들러 + const handleFileReplaceConfirm = async () => { + if (!pendingFiles.length || !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]); + } + + setPendingFiles([]); + setPendingFileType(null); + }; + + const handleFileReplaceCancel = () => { + setIsFileReplaceModalOpen(false); + setPendingFiles([]); + setPendingFileType(null); + }; + const handleSaveClick = async () => { // 유효성 검사 const newErrors: { @@ -643,32 +892,17 @@ export default function AdminLessonsPage() { } if (validFiles.length > 0) { - try { - // 다중 파일 업로드 - const uploadResponse = await apiService.uploadFiles(validFiles); - - // 응답에서 fileKey 배열 추출 - const fileKeys: string[] = []; - if (uploadResponse.data?.results && Array.isArray(uploadResponse.data.results)) { - uploadResponse.data.results.forEach((result: any) => { - if (result.ok && result.fileKey) { - fileKeys.push(result.fileKey); - } - }); - } - - if (fileKeys.length > 0) { - setCourseVideoFiles(prev => [...prev, ...validFiles.map(f => f.name)]); - setCourseVideoFileObjects(prev => [...prev, ...validFiles]); - setCourseVideoFileKeys(prev => [...prev, ...fileKeys]); - setCourseVideoCount(prev => prev + validFiles.length); - } else { - throw new Error('파일 업로드는 완료되었지만 fileKey를 받지 못했습니다.'); - } - } catch (error) { - console.error('강좌 영상 업로드 실패:', error); - alert('파일 업로드에 실패했습니다. 다시 시도해주세요.'); + // 기존 파일이 있으면 확인 모달 표시 + if (courseVideoFiles.length > 0 || courseVideoFileKeys.length > 0) { + setPendingFiles(validFiles); + setPendingFileType('video'); + setIsFileReplaceModalOpen(true); + e.target.value = ''; + return; } + + // 기존 파일이 없으면 바로 업로드 + handleVideoFileUpload(validFiles); } // input 초기화 (같은 파일 다시 선택 가능하도록) e.target.value = ''; @@ -782,32 +1016,17 @@ export default function AdminLessonsPage() { } if (validFiles.length > 0) { - try { - // 다중 파일 업로드 - const uploadResponse = await apiService.uploadFiles(validFiles); - - // 응답에서 fileKey 배열 추출 - const fileKeys: string[] = []; - if (uploadResponse.data?.results && Array.isArray(uploadResponse.data.results)) { - uploadResponse.data.results.forEach((result: any) => { - if (result.ok && result.fileKey) { - fileKeys.push(result.fileKey); - } - }); - } - - if (fileKeys.length > 0) { - setVrContentFiles(prev => [...prev, ...validFiles.map(f => f.name)]); - setVrContentFileObjects(prev => [...prev, ...validFiles]); - setVrContentFileKeys(prev => [...prev, ...fileKeys]); - setVrContentCount(prev => prev + validFiles.length); - } else { - throw new Error('파일 업로드는 완료되었지만 fileKey를 받지 못했습니다.'); - } - } catch (error) { - console.error('VR 콘텐츠 업로드 실패:', error); - alert('파일 업로드에 실패했습니다. 다시 시도해주세요.'); + // 기존 파일이 있으면 확인 모달 표시 + if (vrContentFiles.length > 0 || vrContentFileKeys.length > 0) { + setPendingFiles(validFiles); + setPendingFileType('vr'); + setIsFileReplaceModalOpen(true); + e.target.value = ''; + return; } + + // 기존 파일이 없으면 바로 업로드 + handleVrFileUpload(validFiles); } // input 초기화 (같은 파일 다시 선택 가능하도록) e.target.value = ''; @@ -967,37 +1186,17 @@ export default function AdminLessonsPage() { reader.readAsText(file, 'UTF-8'); - // 단일 파일 업로드 - const uploadResponse = await apiService.uploadFile(file); - - // 응답에서 fileKey 추출 - let fileKey: string | null = null; - if (uploadResponse.data?.fileKey) { - fileKey = uploadResponse.data.fileKey; - } else if (uploadResponse.data?.key) { - fileKey = uploadResponse.data.key; - } else if (uploadResponse.data?.id) { - fileKey = uploadResponse.data.id; - } else if (uploadResponse.data?.imageKey) { - fileKey = uploadResponse.data.imageKey; - } else if (uploadResponse.data?.fileId) { - fileKey = uploadResponse.data.fileId; - } else if (uploadResponse.data?.data && (uploadResponse.data.data.key || uploadResponse.data.data.fileKey)) { - fileKey = uploadResponse.data.data.key || uploadResponse.data.data.fileKey; - } else if (uploadResponse.data?.results && Array.isArray(uploadResponse.data.results) && uploadResponse.data.results.length > 0) { - const result = uploadResponse.data.results[0]; - if (result.ok && result.fileKey) { - fileKey = result.fileKey; - } + // 기존 파일이 있으면 확인 모달 표시 + if (questionFileObject || questionFileKey) { + setPendingFiles([file]); + setPendingFileType('csv'); + setIsFileReplaceModalOpen(true); + e.target.value = ''; + return; } - if (fileKey) { - setQuestionFileObject(file); - setQuestionFileKey(fileKey); - setQuestionFileCount(1); - } else { - throw new Error('파일 업로드는 완료되었지만 fileKey를 받지 못했습니다.'); - } + // 기존 파일이 없으면 바로 업로드 + await handleCsvFileUpload(file); } catch (error) { console.error('학습 평가 문제 업로드 실패:', error); alert('파일 업로드에 실패했습니다. 다시 시도해주세요.'); @@ -1335,6 +1534,52 @@ export default function AdminLessonsPage() { )} + + {/* 파일 교체 확인 모달 */} + {isFileReplaceModalOpen && ( +
+ + )}
); } diff --git a/src/app/admin/notices/[id]/page.tsx b/src/app/admin/notices/[id]/page.tsx index 15a893c..7e2b5e2 100644 --- a/src/app/admin/notices/[id]/page.tsx +++ b/src/app/admin/notices/[id]/page.tsx @@ -24,6 +24,7 @@ export default function AdminNoticeDetailPage() { const [attachments, setAttachments] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + const [isDeleting, setIsDeleting] = useState(false); useEffect(() => { async function fetchNotice() { @@ -298,16 +299,34 @@ export default function AdminNoticeDetailPage() {