idonknow
This commit is contained in:
@@ -42,8 +42,8 @@ export default function CourseRegistrationModal({ open, onClose, onSave, onDelet
|
|||||||
|
|
||||||
setIsLoadingInstructors(true);
|
setIsLoadingInstructors(true);
|
||||||
try {
|
try {
|
||||||
// 외부 API 호출
|
// 외부 API 호출 - type이 'ADMIN'인 사용자만 조회
|
||||||
const response = await apiService.getUsersCompact();
|
const response = await apiService.getUsersCompact({ type: 'ADMIN', limit: 10 });
|
||||||
const data = response.data;
|
const data = response.data;
|
||||||
|
|
||||||
// API 응답이 배열이 아닌 경우 처리 (예: { items: [...] } 형태)
|
// API 응답이 배열이 아닌 경우 처리 (예: { items: [...] } 형태)
|
||||||
@@ -322,10 +322,21 @@ export default function CourseRegistrationModal({ open, onClose, onSave, onDelet
|
|||||||
} else {
|
} else {
|
||||||
// 등록 모드: POST /subjects
|
// 등록 모드: POST /subjects
|
||||||
try {
|
try {
|
||||||
await apiService.createSubject({
|
const createRequestBody: {
|
||||||
courseName: courseName.trim(),
|
title: string;
|
||||||
instructorName: selectedInstructor.name,
|
instructor: string;
|
||||||
});
|
imageKey?: string;
|
||||||
|
} = {
|
||||||
|
title: courseName.trim(),
|
||||||
|
instructor: selectedInstructor.name,
|
||||||
|
};
|
||||||
|
|
||||||
|
// imageKey 처리: 등록 모드에서 새 이미지가 있으면 포함
|
||||||
|
if (imageKey) {
|
||||||
|
createRequestBody.imageKey = imageKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
await apiService.createSubject(createRequestBody);
|
||||||
|
|
||||||
// 성공 시 onSave 콜백 호출 및 모달 닫기
|
// 성공 시 onSave 콜백 호출 및 모달 닫기
|
||||||
if (onSave && selectedInstructor) {
|
if (onSave && selectedInstructor) {
|
||||||
|
|||||||
@@ -41,6 +41,11 @@ export default function LessonEditPage() {
|
|||||||
const [csvHeaders, setCsvHeaders] = useState<string[]>([]);
|
const [csvHeaders, setCsvHeaders] = useState<string[]>([]);
|
||||||
const [csvRows, setCsvRows] = 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<{
|
const [originalData, setOriginalData] = useState<{
|
||||||
title?: string;
|
title?: string;
|
||||||
@@ -243,6 +248,251 @@ export default function LessonEditPage() {
|
|||||||
router.push(`/admin/lessons/${params.id}`);
|
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 handleSaveClick = async () => {
|
||||||
// 유효성 검사
|
// 유효성 검사
|
||||||
const newErrors: {
|
const newErrors: {
|
||||||
@@ -666,32 +916,17 @@ export default function LessonEditPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (validFiles.length > 0) {
|
if (validFiles.length > 0) {
|
||||||
try {
|
// 새로 첨부한 파일이 있으면 확인 모달 표시
|
||||||
// 다중 파일 업로드
|
if (courseVideoFiles.length > 0 || courseVideoFileKeys.length > 0) {
|
||||||
const uploadResponse = await apiService.uploadFiles(validFiles);
|
setPendingFiles(validFiles);
|
||||||
|
setPendingFileType('video');
|
||||||
// 응답에서 fileKey 배열 추출
|
setIsFileReplaceModalOpen(true);
|
||||||
const fileKeys: string[] = [];
|
e.target.value = '';
|
||||||
if (uploadResponse.data?.results && Array.isArray(uploadResponse.data.results)) {
|
return;
|
||||||
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)]);
|
handleVideoFileUpload(validFiles);
|
||||||
setCourseVideoFileObjects(prev => [...prev, ...validFiles]);
|
|
||||||
setCourseVideoFileKeys(prev => [...prev, ...fileKeys]);
|
|
||||||
setCourseVideoCount(prev => prev + validFiles.length);
|
|
||||||
} else {
|
|
||||||
throw new Error('파일 업로드는 완료되었지만 fileKey를 받지 못했습니다.');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('강좌 영상 업로드 실패:', error);
|
|
||||||
alert('파일 업로드에 실패했습니다. 다시 시도해주세요.');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
e.target.value = '';
|
e.target.value = '';
|
||||||
}}
|
}}
|
||||||
@@ -825,32 +1060,17 @@ export default function LessonEditPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (validFiles.length > 0) {
|
if (validFiles.length > 0) {
|
||||||
try {
|
// 새로 첨부한 파일이 있으면 확인 모달 표시
|
||||||
// 다중 파일 업로드
|
if (vrContentFiles.length > 0 || vrContentFileKeys.length > 0) {
|
||||||
const uploadResponse = await apiService.uploadFiles(validFiles);
|
setPendingFiles(validFiles);
|
||||||
|
setPendingFileType('vr');
|
||||||
// 응답에서 fileKey 배열 추출
|
setIsFileReplaceModalOpen(true);
|
||||||
const fileKeys: string[] = [];
|
e.target.value = '';
|
||||||
if (uploadResponse.data?.results && Array.isArray(uploadResponse.data.results)) {
|
return;
|
||||||
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)]);
|
handleVrFileUpload(validFiles);
|
||||||
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('파일 업로드에 실패했습니다. 다시 시도해주세요.');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
e.target.value = '';
|
e.target.value = '';
|
||||||
}}
|
}}
|
||||||
@@ -1024,38 +1244,17 @@ export default function LessonEditPage() {
|
|||||||
|
|
||||||
reader.readAsText(file, 'UTF-8');
|
reader.readAsText(file, 'UTF-8');
|
||||||
|
|
||||||
// 단일 파일 업로드
|
// 기존 파일이 있으면 확인 모달 표시
|
||||||
const uploadResponse = await apiService.uploadFile(file);
|
if (questionFileObject || questionFileKey) {
|
||||||
|
setPendingFiles([file]);
|
||||||
// 응답에서 fileKey 추출
|
setPendingFileType('csv');
|
||||||
let fileKey: string | null = null;
|
setIsFileReplaceModalOpen(true);
|
||||||
if (uploadResponse.data?.fileKey) {
|
e.target.value = '';
|
||||||
fileKey = uploadResponse.data.fileKey;
|
return;
|
||||||
} 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 (fileKey) {
|
// 기존 파일이 없으면 바로 업로드
|
||||||
setQuestionFileObject(file);
|
await handleCsvFileUpload(file);
|
||||||
setQuestionFileKey(fileKey);
|
|
||||||
setQuestionFileCount(1);
|
|
||||||
setExistingQuestionFile(null);
|
|
||||||
} else {
|
|
||||||
throw new Error('파일 업로드는 완료되었지만 fileKey를 받지 못했습니다.');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('학습 평가 문제 업로드 실패:', error);
|
console.error('학습 평가 문제 업로드 실패:', error);
|
||||||
alert('파일 업로드에 실패했습니다. 다시 시도해주세요.');
|
alert('파일 업로드에 실패했습니다. 다시 시도해주세요.');
|
||||||
@@ -1203,6 +1402,52 @@ export default function LessonEditPage() {
|
|||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,6 +51,11 @@ export default function AdminLessonsPage() {
|
|||||||
const [csvHeaders, setCsvHeaders] = useState<string[]>([]);
|
const [csvHeaders, setCsvHeaders] = useState<string[]>([]);
|
||||||
const [csvRows, setCsvRows] = 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 [errors, setErrors] = useState<{
|
const [errors, setErrors] = useState<{
|
||||||
selectedCourse?: string;
|
selectedCourse?: string;
|
||||||
@@ -245,6 +250,250 @@ export default function AdminLessonsPage() {
|
|||||||
setIsRegistrationMode(true);
|
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 handleSaveClick = async () => {
|
||||||
// 유효성 검사
|
// 유효성 검사
|
||||||
const newErrors: {
|
const newErrors: {
|
||||||
@@ -643,32 +892,17 @@ export default function AdminLessonsPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (validFiles.length > 0) {
|
if (validFiles.length > 0) {
|
||||||
try {
|
// 기존 파일이 있으면 확인 모달 표시
|
||||||
// 다중 파일 업로드
|
if (courseVideoFiles.length > 0 || courseVideoFileKeys.length > 0) {
|
||||||
const uploadResponse = await apiService.uploadFiles(validFiles);
|
setPendingFiles(validFiles);
|
||||||
|
setPendingFileType('video');
|
||||||
// 응답에서 fileKey 배열 추출
|
setIsFileReplaceModalOpen(true);
|
||||||
const fileKeys: string[] = [];
|
e.target.value = '';
|
||||||
if (uploadResponse.data?.results && Array.isArray(uploadResponse.data.results)) {
|
return;
|
||||||
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)]);
|
handleVideoFileUpload(validFiles);
|
||||||
setCourseVideoFileObjects(prev => [...prev, ...validFiles]);
|
|
||||||
setCourseVideoFileKeys(prev => [...prev, ...fileKeys]);
|
|
||||||
setCourseVideoCount(prev => prev + validFiles.length);
|
|
||||||
} else {
|
|
||||||
throw new Error('파일 업로드는 완료되었지만 fileKey를 받지 못했습니다.');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('강좌 영상 업로드 실패:', error);
|
|
||||||
alert('파일 업로드에 실패했습니다. 다시 시도해주세요.');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// input 초기화 (같은 파일 다시 선택 가능하도록)
|
// input 초기화 (같은 파일 다시 선택 가능하도록)
|
||||||
e.target.value = '';
|
e.target.value = '';
|
||||||
@@ -782,32 +1016,17 @@ export default function AdminLessonsPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (validFiles.length > 0) {
|
if (validFiles.length > 0) {
|
||||||
try {
|
// 기존 파일이 있으면 확인 모달 표시
|
||||||
// 다중 파일 업로드
|
if (vrContentFiles.length > 0 || vrContentFileKeys.length > 0) {
|
||||||
const uploadResponse = await apiService.uploadFiles(validFiles);
|
setPendingFiles(validFiles);
|
||||||
|
setPendingFileType('vr');
|
||||||
// 응답에서 fileKey 배열 추출
|
setIsFileReplaceModalOpen(true);
|
||||||
const fileKeys: string[] = [];
|
e.target.value = '';
|
||||||
if (uploadResponse.data?.results && Array.isArray(uploadResponse.data.results)) {
|
return;
|
||||||
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)]);
|
handleVrFileUpload(validFiles);
|
||||||
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('파일 업로드에 실패했습니다. 다시 시도해주세요.');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// input 초기화 (같은 파일 다시 선택 가능하도록)
|
// input 초기화 (같은 파일 다시 선택 가능하도록)
|
||||||
e.target.value = '';
|
e.target.value = '';
|
||||||
@@ -967,37 +1186,17 @@ export default function AdminLessonsPage() {
|
|||||||
|
|
||||||
reader.readAsText(file, 'UTF-8');
|
reader.readAsText(file, 'UTF-8');
|
||||||
|
|
||||||
// 단일 파일 업로드
|
// 기존 파일이 있으면 확인 모달 표시
|
||||||
const uploadResponse = await apiService.uploadFile(file);
|
if (questionFileObject || questionFileKey) {
|
||||||
|
setPendingFiles([file]);
|
||||||
// 응답에서 fileKey 추출
|
setPendingFileType('csv');
|
||||||
let fileKey: string | null = null;
|
setIsFileReplaceModalOpen(true);
|
||||||
if (uploadResponse.data?.fileKey) {
|
e.target.value = '';
|
||||||
fileKey = uploadResponse.data.fileKey;
|
return;
|
||||||
} 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 (fileKey) {
|
// 기존 파일이 없으면 바로 업로드
|
||||||
setQuestionFileObject(file);
|
await handleCsvFileUpload(file);
|
||||||
setQuestionFileKey(fileKey);
|
|
||||||
setQuestionFileCount(1);
|
|
||||||
} else {
|
|
||||||
throw new Error('파일 업로드는 완료되었지만 fileKey를 받지 못했습니다.');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('학습 평가 문제 업로드 실패:', error);
|
console.error('학습 평가 문제 업로드 실패:', error);
|
||||||
alert('파일 업로드에 실패했습니다. 다시 시도해주세요.');
|
alert('파일 업로드에 실패했습니다. 다시 시도해주세요.');
|
||||||
@@ -1335,6 +1534,52 @@ export default function AdminLessonsPage() {
|
|||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ export default function AdminNoticeDetailPage() {
|
|||||||
const [attachments, setAttachments] = useState<Attachment[]>([]);
|
const [attachments, setAttachments] = useState<Attachment[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [isDeleting, setIsDeleting] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function fetchNotice() {
|
async function fetchNotice() {
|
||||||
@@ -298,16 +299,34 @@ export default function AdminNoticeDetailPage() {
|
|||||||
<div className="flex items-center justify-end gap-[12px]">
|
<div className="flex items-center justify-end gap-[12px]">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={async () => {
|
||||||
if (confirm('정말 삭제하시겠습니까?')) {
|
if (!confirm('정말 삭제하시겠습니까?')) {
|
||||||
// TODO: 삭제 API 호출
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!params?.id) {
|
||||||
|
alert('공지사항 ID를 찾을 수 없습니다.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setIsDeleting(true);
|
||||||
|
await apiService.deleteNotice(params.id);
|
||||||
|
alert('공지사항이 삭제되었습니다.');
|
||||||
router.push('/admin/notices');
|
router.push('/admin/notices');
|
||||||
|
} catch (err) {
|
||||||
|
console.error('공지사항 삭제 오류:', err);
|
||||||
|
const errorMessage = err instanceof Error ? err.message : '공지사항 삭제에 실패했습니다.';
|
||||||
|
alert(errorMessage);
|
||||||
|
} finally {
|
||||||
|
setIsDeleting(false);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className="bg-[#FEF2F2] h-[48px] rounded-[10px] px-[8px] shrink-0 min-w-[80px] flex items-center justify-center hover:bg-[#FEE2E2] transition-colors"
|
disabled={isDeleting}
|
||||||
|
className="bg-[#FEF2F2] h-[48px] rounded-[10px] px-[8px] shrink-0 min-w-[80px] flex items-center justify-center hover:bg-[#FEE2E2] transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
>
|
>
|
||||||
<span className="text-[16px] font-semibold leading-[1.5] text-[#F64C4C] text-center">
|
<span className="text-[16px] font-semibold leading-[1.5] text-[#F64C4C] text-center">
|
||||||
삭제
|
{isDeleting ? '삭제 중...' : '삭제'}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ interface RequestConfig {
|
|||||||
headers?: Record<string, string>;
|
headers?: Record<string, string>;
|
||||||
body?: any;
|
body?: any;
|
||||||
timeout?: number;
|
timeout?: number;
|
||||||
|
params?: Record<string, string | number | undefined>;
|
||||||
}
|
}
|
||||||
|
|
||||||
class ApiService {
|
class ApiService {
|
||||||
@@ -96,10 +97,25 @@ class ApiService {
|
|||||||
method = 'GET',
|
method = 'GET',
|
||||||
headers = {},
|
headers = {},
|
||||||
body,
|
body,
|
||||||
timeout = this.defaultTimeout
|
timeout = this.defaultTimeout,
|
||||||
|
params
|
||||||
} = config;
|
} = config;
|
||||||
|
|
||||||
const url = endpoint.startsWith('http') ? endpoint : `${this.baseURL}${endpoint}`;
|
let url = endpoint.startsWith('http') ? endpoint : `${this.baseURL}${endpoint}`;
|
||||||
|
|
||||||
|
// 쿼리 파라미터 추가
|
||||||
|
if (params) {
|
||||||
|
const queryParams = new URLSearchParams();
|
||||||
|
Object.entries(params).forEach(([key, value]) => {
|
||||||
|
if (value !== undefined && value !== null) {
|
||||||
|
queryParams.append(key, String(value));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const queryString = queryParams.toString();
|
||||||
|
if (queryString) {
|
||||||
|
url += (url.includes('?') ? '&' : '?') + queryString;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// FormData 여부 확인
|
// FormData 여부 확인
|
||||||
const isFormData = body instanceof FormData;
|
const isFormData = body instanceof FormData;
|
||||||
@@ -271,8 +287,9 @@ class ApiService {
|
|||||||
* 과목 생성
|
* 과목 생성
|
||||||
*/
|
*/
|
||||||
async createSubject(subjectData: {
|
async createSubject(subjectData: {
|
||||||
courseName: string;
|
title: string;
|
||||||
instructorName: string;
|
instructor: string;
|
||||||
|
imageKey?: string;
|
||||||
}) {
|
}) {
|
||||||
return this.request('/subjects', {
|
return this.request('/subjects', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -342,8 +359,13 @@ class ApiService {
|
|||||||
/**
|
/**
|
||||||
* 회원 리스트 조회 (컴팩트) - 관리자 전용
|
* 회원 리스트 조회 (컴팩트) - 관리자 전용
|
||||||
*/
|
*/
|
||||||
async getUsersCompact() {
|
async getUsersCompact(params?: { type?: string; limit?: number }) {
|
||||||
return this.request('/admin/users/compact');
|
return this.request('/admin/users/compact', {
|
||||||
|
params: params ? {
|
||||||
|
...(params.type && { type: params.type }),
|
||||||
|
...(params.limit && { limit: params.limit }),
|
||||||
|
} : undefined,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user