교육과정, 강의등록 완료1
This commit is contained in:
@@ -6,6 +6,7 @@ import DropdownIcon from "@/app/svgs/dropdownicon";
|
||||
import CloseXOSvg from "@/app/svgs/closexo";
|
||||
import { type UserRow } from "@/app/admin/id/mockData";
|
||||
import { type Course } from "./mockData";
|
||||
import apiService from "@/app/lib/apiService";
|
||||
|
||||
type Props = {
|
||||
open: boolean;
|
||||
@@ -25,6 +26,8 @@ export default function CourseRegistrationModal({ open, onClose, onSave, onDelet
|
||||
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
const [isImageDeleted, setIsImageDeleted] = useState(false);
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
const modalRef = useRef<HTMLDivElement>(null);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
@@ -138,12 +141,47 @@ export default function CourseRegistrationModal({ open, onClose, onSave, onDelet
|
||||
return instructors.find(inst => inst.id === instructorId);
|
||||
}, [instructors, instructorId]);
|
||||
|
||||
// previewUrl 변경 시 이전 Blob URL 정리
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (previewUrl && previewUrl.startsWith('blob:')) {
|
||||
URL.revokeObjectURL(previewUrl);
|
||||
}
|
||||
};
|
||||
}, [previewUrl]);
|
||||
|
||||
// 수정 모드일 때 기존 데이터 채우기
|
||||
useEffect(() => {
|
||||
if (open && editingCourse) {
|
||||
setCourseName(editingCourse.courseName);
|
||||
// 수정 모드일 때 강사 목록 자동 로드
|
||||
loadInstructors();
|
||||
|
||||
// 수정 모드일 때 이미지 로드
|
||||
if (editingCourse.imageKey) {
|
||||
setIsImageDeleted(false); // 초기화
|
||||
setSelectedImage(null); // 새 이미지 선택 초기화
|
||||
const loadImage = async () => {
|
||||
try {
|
||||
const imageUrl = await apiService.getFile(editingCourse.imageKey!);
|
||||
// 이미지가 있으면 previewUrl 설정, 없으면 null
|
||||
if (imageUrl) {
|
||||
setPreviewUrl(imageUrl);
|
||||
} else {
|
||||
setPreviewUrl(null);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('이미지 로드 오류:', error);
|
||||
// 이미지 로드 실패 시 null로 설정
|
||||
setPreviewUrl(null);
|
||||
}
|
||||
};
|
||||
loadImage();
|
||||
} else {
|
||||
setIsImageDeleted(false); // 초기화
|
||||
setSelectedImage(null); // 새 이미지 선택 초기화
|
||||
setPreviewUrl(null); // 이미지가 없으면 명시적으로 null 설정
|
||||
}
|
||||
} else if (!open) {
|
||||
setCourseName("");
|
||||
setInstructorId("");
|
||||
@@ -153,6 +191,7 @@ export default function CourseRegistrationModal({ open, onClose, onSave, onDelet
|
||||
setSelectedImage(null);
|
||||
setPreviewUrl(null);
|
||||
setIsDragging(false);
|
||||
setIsImageDeleted(false);
|
||||
setInstructors([]); // 모달 닫을 때 강사 목록 초기화
|
||||
}
|
||||
}, [open, editingCourse]);
|
||||
@@ -224,106 +263,24 @@ export default function CourseRegistrationModal({ open, onClose, onSave, onDelet
|
||||
});
|
||||
|
||||
try {
|
||||
// 토큰 가져오기
|
||||
const token = localStorage.getItem('token') || document.cookie
|
||||
.split('; ')
|
||||
.find(row => row.startsWith('token='))
|
||||
?.split('=')[1];
|
||||
|
||||
// API base URL 설정
|
||||
const baseUrl = process.env.NEXT_PUBLIC_API_BASE_URL
|
||||
? process.env.NEXT_PUBLIC_API_BASE_URL
|
||||
: 'https://hrdi.coconutmeet.net';
|
||||
|
||||
let imageKey: string | null = null;
|
||||
|
||||
// 이미지가 있으면 먼저 업로드하여 imageKey 받기
|
||||
// 새 이미지가 선택된 경우 업로드
|
||||
if (selectedImage) {
|
||||
try {
|
||||
// 이미지 업로드 API 호출 - 일반적인 엔드포인트 경로 시도
|
||||
const possibleEndpoints = [
|
||||
`${baseUrl}/admin/files/upload`,
|
||||
`${baseUrl}/admin/files`,
|
||||
`${baseUrl}/admin/upload`,
|
||||
`${baseUrl}/admin/images/upload`,
|
||||
`${baseUrl}/admin/images`,
|
||||
`${baseUrl}/files/upload`,
|
||||
`${baseUrl}/files`,
|
||||
`${baseUrl}/upload`,
|
||||
`${baseUrl}/api/files/upload`,
|
||||
`${baseUrl}/api/upload`,
|
||||
`${baseUrl}/api/files`,
|
||||
];
|
||||
|
||||
let uploadResponse: Response | null = null;
|
||||
let lastError: Error | null = null;
|
||||
let uploadSuccess = false;
|
||||
let lastStatusCode: number | null = null;
|
||||
|
||||
// 여러 엔드포인트를 시도
|
||||
for (const uploadUrl of possibleEndpoints) {
|
||||
try {
|
||||
// 각 시도마다 새로운 FormData 생성 (FormData는 한 번만 사용 가능)
|
||||
const formData = new FormData();
|
||||
formData.append('file', selectedImage);
|
||||
// 일부 API는 'image' 필드명을 사용할 수 있음
|
||||
formData.append('image', selectedImage);
|
||||
|
||||
uploadResponse = await fetch(uploadUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
...(token && { Authorization: `Bearer ${token}` }),
|
||||
// Content-Type은 FormData 사용 시 자동으로 설정되므로 명시하지 않음
|
||||
},
|
||||
body: formData,
|
||||
});
|
||||
|
||||
lastStatusCode = uploadResponse.status;
|
||||
|
||||
if (uploadResponse.ok) {
|
||||
const uploadData = await uploadResponse.json();
|
||||
// 응답에서 imageKey 추출 (실제 응답 구조에 맞게 수정 필요)
|
||||
imageKey = uploadData.imageKey || uploadData.key || uploadData.id || uploadData.fileKey || uploadData.fileId || uploadData.data?.key || uploadData.data?.imageKey || null;
|
||||
uploadSuccess = true;
|
||||
break; // 성공하면 루프 종료
|
||||
} else if (uploadResponse.status !== 404) {
|
||||
// 404가 아닌 다른 에러면 해당 엔드포인트가 맞을 수 있으므로 에러 정보 저장
|
||||
try {
|
||||
const errorData = await uploadResponse.json();
|
||||
lastError = new Error(
|
||||
errorData.message || errorData.error || `이미지 업로드 실패: ${uploadResponse.status} ${uploadResponse.statusText}`
|
||||
);
|
||||
} catch {
|
||||
lastError = new Error(`이미지 업로드 실패: ${uploadResponse.status} ${uploadResponse.statusText}`);
|
||||
}
|
||||
// 401, 403 같은 인증/권한 오류는 더 이상 시도하지 않음
|
||||
if (uploadResponse.status === 401 || uploadResponse.status === 403) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (fetchError) {
|
||||
// 네트워크 에러 등은 무시하고 다음 엔드포인트 시도
|
||||
if (!lastError) {
|
||||
lastError = fetchError instanceof Error ? fetchError : new Error('네트워크 오류가 발생했습니다.');
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (!uploadSuccess) {
|
||||
// 모든 엔드포인트가 404를 반환한 경우와 다른 오류를 구분
|
||||
if (lastStatusCode === 404 || !lastStatusCode) {
|
||||
console.warn('이미지 업로드 엔드포인트를 찾을 수 없습니다. 이미지 없이 계속 진행합니다.');
|
||||
} else {
|
||||
const errorMessage = lastError?.message || '이미지 업로드에 실패했습니다.';
|
||||
console.error('이미지 업로드 실패:', errorMessage);
|
||||
}
|
||||
// 이미지 업로드 실패 시 사용자에게 알림 (경고 수준)
|
||||
setErrors((prev) => ({
|
||||
...prev,
|
||||
image: '이미지 업로드에 실패했습니다. 이미지 없이 계속 진행됩니다.',
|
||||
}));
|
||||
// 이미지 업로드 실패해도 계속 진행 (선택사항)
|
||||
const uploadResponse = await apiService.uploadFile(selectedImage);
|
||||
|
||||
// 응답에서 imageKey 추출
|
||||
// apiService.uploadFile은 ApiResponse<T> 형태로 반환하므로 uploadResponse.data가 실제 응답 데이터
|
||||
if (uploadResponse.data) {
|
||||
// 다양한 가능한 응답 구조 확인
|
||||
imageKey = uploadResponse.data.imageKey
|
||||
|| uploadResponse.data.key
|
||||
|| uploadResponse.data.id
|
||||
|| uploadResponse.data.fileKey
|
||||
|| uploadResponse.data.fileId
|
||||
|| (uploadResponse.data.data && (uploadResponse.data.data.imageKey || uploadResponse.data.data.key))
|
||||
|| null;
|
||||
}
|
||||
} catch (uploadError) {
|
||||
const errorMessage = uploadError instanceof Error ? uploadError.message : '이미지 업로드 중 오류가 발생했습니다.';
|
||||
@@ -336,54 +293,117 @@ export default function CourseRegistrationModal({ open, onClose, onSave, onDelet
|
||||
}
|
||||
}
|
||||
|
||||
// /subjects API 호출
|
||||
const token = localStorage.getItem('token') || document.cookie
|
||||
.split('; ')
|
||||
.find(row => row.startsWith('token='))
|
||||
?.split('=')[1];
|
||||
|
||||
const baseUrl = process.env.NEXT_PUBLIC_API_BASE_URL
|
||||
? process.env.NEXT_PUBLIC_API_BASE_URL
|
||||
: 'https://hrdi.coconutmeet.net';
|
||||
|
||||
const requestBody: {
|
||||
title: string;
|
||||
instructor: string;
|
||||
imageKey?: string | null;
|
||||
imageKey?: string;
|
||||
} = {
|
||||
title: courseName.trim(),
|
||||
instructor: selectedInstructor.name,
|
||||
};
|
||||
|
||||
if (imageKey) {
|
||||
requestBody.imageKey = imageKey;
|
||||
}
|
||||
|
||||
const response = await fetch(`${baseUrl}/subjects`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(token && { Authorization: `Bearer ${token}` }),
|
||||
},
|
||||
body: JSON.stringify(requestBody),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
let errorMessage = `과목 등록 실패 (${response.status})`;
|
||||
try {
|
||||
const errorData = await response.json();
|
||||
if (errorData.error) {
|
||||
errorMessage = errorData.error;
|
||||
} else if (errorData.message) {
|
||||
errorMessage = errorData.message;
|
||||
}
|
||||
} catch (parseError) {
|
||||
// JSON 파싱 실패 시 기본 메시지 사용
|
||||
// imageKey 처리: 수정 모드에서는 항상 명시적으로 설정
|
||||
if (editingCourse && editingCourse.id) {
|
||||
// 수정 모드: 이미지가 삭제된 경우 "null" 문자열, 새 이미지가 있으면 값, 기존 이미지 유지 시 기존 값, 없으면 "null"
|
||||
if (isImageDeleted) {
|
||||
// 이미지가 삭제된 경우 무조건 "null" 문자열로 설정
|
||||
requestBody.imageKey = "null";
|
||||
} else if (imageKey) {
|
||||
// 새 이미지가 업로드된 경우
|
||||
requestBody.imageKey = imageKey;
|
||||
} else if (editingCourse.imageKey) {
|
||||
// 기존 이미지를 유지하는 경우
|
||||
requestBody.imageKey = editingCourse.imageKey;
|
||||
} else {
|
||||
// 이미지가 없는 경우 "null" 문자열로 명시적으로 설정
|
||||
requestBody.imageKey = "null";
|
||||
}
|
||||
} else {
|
||||
// 등록 모드: 새 이미지가 있으면 값, 없으면 undefined (선택사항)
|
||||
if (imageKey) {
|
||||
requestBody.imageKey = imageKey;
|
||||
}
|
||||
console.error('과목 등록 실패:', errorMessage);
|
||||
setErrors({ submit: errorMessage });
|
||||
setIsSaving(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// 성공 시 onSave 콜백 호출
|
||||
if (onSave && selectedInstructor) {
|
||||
onSave(courseName.trim(), selectedInstructor.name);
|
||||
// 수정 모드인지 등록 모드인지 확인
|
||||
if (editingCourse && editingCourse.id) {
|
||||
// 수정 모드: PUT /subjects/{id}
|
||||
try {
|
||||
await apiService.updateSubject(editingCourse.id, requestBody);
|
||||
|
||||
// 성공 시 onSave 콜백 호출 및 모달 닫기
|
||||
if (onSave && selectedInstructor) {
|
||||
onSave(courseName.trim(), selectedInstructor.name);
|
||||
}
|
||||
onClose(); // 모달 닫기
|
||||
} catch (updateError) {
|
||||
const errorMessage = updateError instanceof Error ? updateError.message : '과목 수정 중 오류가 발생했습니다.';
|
||||
console.error('과목 수정 실패:', errorMessage);
|
||||
setErrors({ submit: errorMessage });
|
||||
setIsSaving(false);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// 등록 모드: POST /subjects
|
||||
const response = await fetch(`${baseUrl}/subjects`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(token && { Authorization: `Bearer ${token}` }),
|
||||
},
|
||||
body: JSON.stringify(requestBody),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
let errorMessage = `과목 등록 실패 (${response.status})`;
|
||||
try {
|
||||
const errorData = await response.json();
|
||||
if (errorData.error) {
|
||||
errorMessage = errorData.error;
|
||||
} else if (errorData.message) {
|
||||
errorMessage = errorData.message;
|
||||
}
|
||||
} catch (parseError) {
|
||||
// JSON 파싱 실패 시 기본 메시지 사용
|
||||
}
|
||||
console.error('과목 등록 실패:', errorMessage);
|
||||
setErrors({ submit: errorMessage });
|
||||
setIsSaving(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// 응답에서 id 추출하여 저장
|
||||
try {
|
||||
const responseData = await response.json();
|
||||
|
||||
// 응답에서 id 추출 (다양한 가능한 필드명 확인)
|
||||
const subjectId = responseData.id
|
||||
|| responseData.data?.id
|
||||
|| responseData.subjectId
|
||||
|| responseData.data?.subjectId
|
||||
|| null;
|
||||
} catch (parseError) {
|
||||
// 응답 파싱 실패 시 무시
|
||||
}
|
||||
|
||||
// 성공 시 onSave 콜백 호출 및 모달 닫기
|
||||
if (onSave && selectedInstructor) {
|
||||
onSave(courseName.trim(), selectedInstructor.name);
|
||||
}
|
||||
onClose(); // 모달 닫기
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : '네트워크 오류가 발생했습니다.';
|
||||
console.error('과목 등록 오류:', errorMessage);
|
||||
console.error('과목 저장 오류:', errorMessage);
|
||||
setErrors({ submit: errorMessage });
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
@@ -396,11 +416,37 @@ export default function CourseRegistrationModal({ open, onClose, onSave, onDelet
|
||||
};
|
||||
|
||||
// 삭제 확인 핸들러
|
||||
const handleDeleteConfirm = () => {
|
||||
if (onDelete) {
|
||||
onDelete();
|
||||
const handleDeleteConfirm = async () => {
|
||||
if (!editingCourse || !editingCourse.id) {
|
||||
console.error('삭제할 교육과정 정보가 없습니다.');
|
||||
setErrors({ submit: '삭제할 교육과정 정보가 없습니다.' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (isDeleting) return; // 이미 삭제 중이면 중복 호출 방지
|
||||
|
||||
setIsDeleting(true);
|
||||
setErrors((prev) => {
|
||||
const next = { ...prev };
|
||||
delete next.submit;
|
||||
return next;
|
||||
});
|
||||
|
||||
try {
|
||||
await apiService.deleteSubject(editingCourse.id);
|
||||
|
||||
// 성공 시 모달 닫기 및 콜백 호출
|
||||
setIsDeleteConfirmOpen(false);
|
||||
if (onDelete) {
|
||||
onDelete();
|
||||
}
|
||||
onClose();
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : '교육과정 삭제 중 오류가 발생했습니다.';
|
||||
console.error('교육과정 삭제 실패:', errorMessage);
|
||||
setErrors({ submit: errorMessage });
|
||||
} finally {
|
||||
setIsDeleting(false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -434,12 +480,18 @@ export default function CourseRegistrationModal({ open, onClose, onSave, onDelet
|
||||
}
|
||||
|
||||
setSelectedImage(file);
|
||||
setIsImageDeleted(false); // 새 이미지 선택 시 삭제 상태 해제
|
||||
setErrors((prev) => {
|
||||
const next = { ...prev };
|
||||
delete next.image;
|
||||
return next;
|
||||
});
|
||||
|
||||
// 기존 previewUrl이 Blob URL인 경우 메모리 해제
|
||||
if (previewUrl && previewUrl.startsWith('blob:')) {
|
||||
URL.revokeObjectURL(previewUrl);
|
||||
}
|
||||
|
||||
// 미리보기 URL 생성
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => {
|
||||
@@ -491,7 +543,12 @@ export default function CourseRegistrationModal({ open, onClose, onSave, onDelet
|
||||
const handleRemoveImage = (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
setSelectedImage(null);
|
||||
// previewUrl이 Blob URL인 경우 메모리 해제
|
||||
if (previewUrl && previewUrl.startsWith('blob:')) {
|
||||
URL.revokeObjectURL(previewUrl);
|
||||
}
|
||||
setPreviewUrl(null);
|
||||
setIsImageDeleted(true); // 이미지 삭제 상태 설정
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = "";
|
||||
}
|
||||
@@ -800,6 +857,9 @@ export default function CourseRegistrationModal({ open, onClose, onSave, onDelet
|
||||
<br />
|
||||
정말 삭제하시겠습니까?
|
||||
</p>
|
||||
{errors.submit && (
|
||||
<p className="text-[var(--color-error)] text-[13px] leading-tight">{errors.submit}</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-2 items-center justify-end">
|
||||
<button
|
||||
@@ -812,9 +872,10 @@ export default function CourseRegistrationModal({ open, onClose, onSave, onDelet
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleDeleteConfirm}
|
||||
className="h-[40px] px-4 rounded-[8px] bg-[#fef2f2] text-[16px] font-semibold leading-normal text-[var(--color-error)] hover:bg-[#fae6e6] cursor-pointer transition-colors"
|
||||
disabled={isDeleting}
|
||||
className="h-[40px] px-4 rounded-[8px] bg-[#fef2f2] text-[16px] font-semibold leading-normal text-[var(--color-error)] hover:bg-[#fae6e6] cursor-pointer transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
삭제하기
|
||||
{isDeleting ? '삭제 중...' : '삭제하기'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -7,39 +7,71 @@ export type Course = {
|
||||
createdAt: string; // 생성일 (YYYY-MM-DD)
|
||||
createdBy: string; // 등록자
|
||||
hasLessons: boolean; // 강좌포함여부
|
||||
imageKey?: string; // 이미지 키
|
||||
};
|
||||
|
||||
// 과목 리스트 조회 API
|
||||
export async function getCourses(): Promise<Course[]> {
|
||||
try {
|
||||
const response = await apiService.getSubjects();
|
||||
const data = response.data;
|
||||
// 교육과정과 강좌 리스트를 동시에 가져오기
|
||||
const [subjectsResponse, lecturesResponse] = await Promise.all([
|
||||
apiService.getSubjects(),
|
||||
apiService.getLectures().catch(() => ({ data: [] })), // 강좌 리스트 조회 실패 시 빈 배열 반환
|
||||
]);
|
||||
|
||||
const subjectsData = subjectsResponse.data;
|
||||
const lecturesData = lecturesResponse.data || [];
|
||||
|
||||
// 디버깅: API 응답 구조 확인
|
||||
console.log('🔍 [getCourses] API 원본 응답:', data);
|
||||
console.log('🔍 [getCourses] 응답 타입:', Array.isArray(data) ? '배열' : typeof data);
|
||||
console.log('🔍 [getCourses] API 원본 응답:', subjectsData);
|
||||
console.log('🔍 [getCourses] 응답 타입:', Array.isArray(subjectsData) ? '배열' : typeof subjectsData);
|
||||
console.log('🔍 [getCourses] 강좌 리스트:', lecturesData);
|
||||
|
||||
// API 응답이 배열이 아닌 경우 처리 (예: { items: [...] } 형태)
|
||||
let coursesArray: any[] = [];
|
||||
if (Array.isArray(data)) {
|
||||
coursesArray = data;
|
||||
} else if (data && typeof data === 'object') {
|
||||
if (Array.isArray(subjectsData)) {
|
||||
coursesArray = subjectsData;
|
||||
} else if (subjectsData && typeof subjectsData === 'object') {
|
||||
// 더 많은 가능한 필드명 확인
|
||||
coursesArray = data.items || data.courses || data.data || data.list || data.subjects || data.subjectList || [];
|
||||
coursesArray = subjectsData.items || subjectsData.courses || subjectsData.data || subjectsData.list || subjectsData.subjects || subjectsData.subjectList || [];
|
||||
}
|
||||
|
||||
// 강좌 리스트가 배열이 아닌 경우 처리
|
||||
let lecturesArray: any[] = [];
|
||||
if (Array.isArray(lecturesData)) {
|
||||
lecturesArray = lecturesData;
|
||||
} else if (lecturesData && typeof lecturesData === 'object') {
|
||||
lecturesArray = lecturesData.items || lecturesData.lectures || lecturesData.data || lecturesData.list || [];
|
||||
}
|
||||
|
||||
console.log('🔍 [getCourses] 변환 전 배열:', coursesArray);
|
||||
console.log('🔍 [getCourses] 배열 길이:', coursesArray.length);
|
||||
console.log('🔍 [getCourses] 강좌 배열 길이:', lecturesArray.length);
|
||||
|
||||
// 각 교육과정에 대해 강좌가 있는지 확인하기 위한 Set 생성
|
||||
const courseIdsWithLessons = new Set<string>();
|
||||
lecturesArray.forEach((lecture: any) => {
|
||||
const subjectId = String(lecture.subjectId || lecture.subject_id || '');
|
||||
if (subjectId) {
|
||||
courseIdsWithLessons.add(subjectId);
|
||||
}
|
||||
});
|
||||
|
||||
console.log('🔍 [getCourses] 강좌가 있는 교육과정 ID:', Array.from(courseIdsWithLessons));
|
||||
|
||||
// API 응답 데이터를 Course 형식으로 변환
|
||||
const transformedCourses: Course[] = coursesArray.map((item: any) => {
|
||||
const courseId = String(item.id || item.subjectId || item.subject_id || '');
|
||||
const hasLessons = courseIdsWithLessons.has(courseId);
|
||||
|
||||
const transformed = {
|
||||
id: String(item.id || item.subjectId || item.subject_id || ''),
|
||||
id: courseId,
|
||||
courseName: item.courseName || item.name || item.subjectName || item.subject_name || item.title || '',
|
||||
instructorName: item.instructorName || item.instructor || item.instructor_name || item.teacherName || '',
|
||||
createdAt: item.createdAt || item.createdDate || item.created_date || item.createdAt || '',
|
||||
createdBy: item.createdBy || item.creator || item.created_by || item.creatorName || '',
|
||||
hasLessons: item.hasLessons !== undefined ? item.hasLessons : (item.has_lessons !== undefined ? item.has_lessons : false),
|
||||
hasLessons: hasLessons,
|
||||
imageKey: item.imageKey || item.image_key || item.fileKey || item.file_key || undefined,
|
||||
};
|
||||
console.log('🔍 [getCourses] 변환된 항목:', transformed);
|
||||
return transformed;
|
||||
|
||||
@@ -6,6 +6,24 @@ import CourseRegistrationModal from "./CourseRegistrationModal";
|
||||
import ChevronDownSvg from "@/app/svgs/chevrondownsvg";
|
||||
import { getCourses, type Course } from "./mockData";
|
||||
|
||||
// 날짜를 yyyy-mm-dd 형식으로 변환하는 함수
|
||||
const formatDate = (dateString: string): string => {
|
||||
if (!dateString) return '';
|
||||
try {
|
||||
const date = new Date(dateString);
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
return `${year}-${month}-${day}`;
|
||||
} catch {
|
||||
// 이미 yyyy-mm-dd 형식이거나 파싱 실패 시 원본 반환
|
||||
if (dateString.includes('T')) {
|
||||
return dateString.split('T')[0];
|
||||
}
|
||||
return dateString;
|
||||
}
|
||||
};
|
||||
|
||||
export default function AdminCoursesPage() {
|
||||
const [courses, setCourses] = useState<Course[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
@@ -199,7 +217,7 @@ export default function AdminCoursesPage() {
|
||||
{course.instructorName}
|
||||
</td>
|
||||
<td className="border-t border-r border-[#dee1e6] px-4 text-[13px] leading-[1.5] text-[#1b2027] whitespace-nowrap">
|
||||
{course.createdAt}
|
||||
{formatDate(course.createdAt)}
|
||||
</td>
|
||||
<td className="border-t border-[#dee1e6] px-4 text-left text-[13px] leading-[1.5] text-[#1b2027] whitespace-nowrap">
|
||||
{course.hasLessons ? (
|
||||
|
||||
Reference in New Issue
Block a user