This commit is contained in:
2025-11-29 14:27:01 +09:00
parent 91d14fdabf
commit 872a88866e
5 changed files with 761 additions and 219 deletions

View File

@@ -41,6 +41,11 @@ export default function LessonEditPage() {
const [csvHeaders, setCsvHeaders] = useState<string[]>([]);
const [csvRows, setCsvRows] = useState<string[][]>([]);
// 파일 교체 확인 모달 상태
const [isFileReplaceModalOpen, setIsFileReplaceModalOpen] = useState(false);
const [pendingFiles, setPendingFiles] = useState<File[]>([]);
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() {
</div>
</div>
)}
{/* 파일 교체 확인 모달 */}
{isFileReplaceModalOpen && (
<div className="fixed inset-0 z-[60] flex items-center justify-center">
<div
className="absolute inset-0 bg-black/40"
onClick={handleFileReplaceCancel}
aria-hidden="true"
/>
<div className="relative z-10 bg-white rounded-[8px] p-[24px] shadow-[0px_8px_20px_0px_rgba(0,0,0,0.06),0px_24px_60px_0px_rgba(0,0,0,0.12)] flex flex-col gap-[32px] items-end justify-end min-w-[500px]">
<div className="flex flex-col gap-[16px] items-start justify-center w-full">
<div className="flex gap-[8px] items-start w-full">
<p className="text-[18px] font-semibold leading-[1.5] text-[#333c47]">
.
</p>
</div>
<div className="flex gap-[8px] items-start w-full">
<div className="text-[15px] font-normal leading-[1.5] text-[#4c5561]">
<p className="mb-0"> .</p>
<p> ?</p>
</div>
</div>
</div>
<div className="flex gap-[8px] items-center justify-end shrink-0">
<button
type="button"
onClick={handleFileReplaceCancel}
className="bg-[#f1f3f5] h-[40px] rounded-[8px] px-[8px] flex items-center justify-center shrink-0 w-[80px] hover:bg-[#e9ecef] cursor-pointer transition-colors"
>
<p className="text-[16px] font-semibold leading-[1.5] text-[#4c5561] text-center whitespace-pre">
</p>
</button>
<button
type="button"
onClick={handleFileReplaceConfirm}
className="bg-[#1f2b91] h-[40px] rounded-[8px] px-[8px] flex items-center justify-center shrink-0 w-[80px] hover:bg-[#1a2478] cursor-pointer transition-colors"
>
<p className="text-[16px] font-semibold leading-[1.5] text-white text-center whitespace-pre">
</p>
</button>
</div>
</div>
</div>
)}
</div>
);
}