idonknow
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -51,6 +51,11 @@ export default function AdminLessonsPage() {
|
||||
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 [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() {
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ export default function AdminNoticeDetailPage() {
|
||||
const [attachments, setAttachments] = useState<Attachment[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchNotice() {
|
||||
@@ -298,16 +299,34 @@ export default function AdminNoticeDetailPage() {
|
||||
<div className="flex items-center justify-end gap-[12px]">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (confirm('정말 삭제하시겠습니까?')) {
|
||||
// TODO: 삭제 API 호출
|
||||
onClick={async () => {
|
||||
if (!confirm('정말 삭제하시겠습니까?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!params?.id) {
|
||||
alert('공지사항 ID를 찾을 수 없습니다.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsDeleting(true);
|
||||
await apiService.deleteNotice(params.id);
|
||||
alert('공지사항이 삭제되었습니다.');
|
||||
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">
|
||||
삭제
|
||||
{isDeleting ? '삭제 중...' : '삭제'}
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
|
||||
@@ -14,6 +14,7 @@ interface RequestConfig {
|
||||
headers?: Record<string, string>;
|
||||
body?: any;
|
||||
timeout?: number;
|
||||
params?: Record<string, string | number | undefined>;
|
||||
}
|
||||
|
||||
class ApiService {
|
||||
@@ -96,10 +97,25 @@ class ApiService {
|
||||
method = 'GET',
|
||||
headers = {},
|
||||
body,
|
||||
timeout = this.defaultTimeout
|
||||
timeout = this.defaultTimeout,
|
||||
params
|
||||
} = 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 여부 확인
|
||||
const isFormData = body instanceof FormData;
|
||||
@@ -271,8 +287,9 @@ class ApiService {
|
||||
* 과목 생성
|
||||
*/
|
||||
async createSubject(subjectData: {
|
||||
courseName: string;
|
||||
instructorName: string;
|
||||
title: string;
|
||||
instructor: string;
|
||||
imageKey?: string;
|
||||
}) {
|
||||
return this.request('/subjects', {
|
||||
method: 'POST',
|
||||
@@ -342,8 +359,13 @@ class ApiService {
|
||||
/**
|
||||
* 회원 리스트 조회 (컴팩트) - 관리자 전용
|
||||
*/
|
||||
async getUsersCompact() {
|
||||
return this.request('/admin/users/compact');
|
||||
async getUsersCompact(params?: { type?: string; limit?: number }) {
|
||||
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