diff --git a/src/app/admin/courses/CourseRegistrationModal.tsx b/src/app/admin/courses/CourseRegistrationModal.tsx index c834bc7..1205ba0 100644 --- a/src/app/admin/courses/CourseRegistrationModal.tsx +++ b/src/app/admin/courses/CourseRegistrationModal.tsx @@ -4,7 +4,7 @@ import React, { useState, useRef, useEffect, useMemo } from "react"; import ModalCloseSvg from "@/app/svgs/closexsvg"; import DropdownIcon from "@/app/svgs/dropdownicon"; import CloseXOSvg from "@/app/svgs/closexo"; -import { getInstructors, type UserRow } from "@/app/admin/id/mockData"; +import { type UserRow } from "@/app/admin/id/mockData"; import { type Course } from "./mockData"; type Props = { @@ -24,6 +24,7 @@ export default function CourseRegistrationModal({ open, onClose, onSave, onDelet const [selectedImage, setSelectedImage] = useState(null); const [previewUrl, setPreviewUrl] = useState(null); const [isDragging, setIsDragging] = useState(false); + const [isSaving, setIsSaving] = useState(false); const dropdownRef = useRef(null); const modalRef = useRef(null); const fileInputRef = useRef(null); @@ -32,24 +33,105 @@ export default function CourseRegistrationModal({ open, onClose, onSave, onDelet const [instructors, setInstructors] = useState([]); const [isLoadingInstructors, setIsLoadingInstructors] = useState(false); - useEffect(() => { - async function loadInstructors() { - setIsLoadingInstructors(true); - try { - const data = await getInstructors(); - setInstructors(data); - } catch (error) { - console.error('강사 목록 로드 오류:', error); - setInstructors([]); - } finally { - setIsLoadingInstructors(false); - } - } + // 강사 목록 로드 함수 + const loadInstructors = async () => { + if (isLoadingInstructors) return; // 이미 로딩 중이면 중복 호출 방지 - if (open) { - loadInstructors(); + setIsLoadingInstructors(true); + try { + const token = localStorage.getItem('token') || document.cookie + .split('; ') + .find(row => row.startsWith('token=')) + ?.split('=')[1]; + + // 외부 API 호출 + const baseUrl = process.env.NEXT_PUBLIC_API_BASE_URL + ? `${process.env.NEXT_PUBLIC_API_BASE_URL}/admin/users/compact` + : 'https://hrdi.coconutmeet.net/admin/users/compact'; + + // 쿼리 파라미터 추가: type=ADMIN + const apiUrl = new URL(baseUrl); + apiUrl.searchParams.set('type', 'ADMIN'); + + const response = await fetch(apiUrl.toString(), { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + ...(token && { Authorization: `Bearer ${token}` }), + }, + }); + + if (!response.ok) { + throw new Error(`강사 목록을 가져오는데 실패했습니다. (${response.status})`); + } + + const data = await response.json(); + + // API 응답이 배열이 아닌 경우 처리 (예: { items: [...] } 형태) + let usersArray: any[] = []; + if (Array.isArray(data)) { + usersArray = data; + } else if (data && typeof data === 'object') { + usersArray = data.items || data.users || data.data || data.list || []; + } + + // 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]; + } + }; + + // 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, + }; + }) + : []; + + setInstructors(transformedUsers); + } catch (error) { + console.error('강사 목록 로드 오류:', error); + setInstructors([]); + } finally { + setIsLoadingInstructors(false); } - }, [open]); + }; // 선택된 강사 정보 const selectedInstructor = useMemo(() => { @@ -60,11 +142,8 @@ export default function CourseRegistrationModal({ open, onClose, onSave, onDelet useEffect(() => { if (open && editingCourse) { setCourseName(editingCourse.courseName); - // 강사명으로 instructorId 찾기 - const instructor = instructors.find(inst => inst.name === editingCourse.instructorName); - if (instructor) { - setInstructorId(instructor.id); - } + // 수정 모드일 때 강사 목록 자동 로드 + loadInstructors(); } else if (!open) { setCourseName(""); setInstructorId(""); @@ -74,6 +153,17 @@ export default function CourseRegistrationModal({ open, onClose, onSave, onDelet setSelectedImage(null); setPreviewUrl(null); setIsDragging(false); + setInstructors([]); // 모달 닫을 때 강사 목록 초기화 + } + }, [open, editingCourse]); + + // instructors가 로드된 후 editingCourse의 강사 찾기 + useEffect(() => { + if (open && editingCourse && instructors.length > 0) { + const instructor = instructors.find(inst => inst.name === editingCourse.instructorName); + if (instructor) { + setInstructorId(instructor.id); + } } }, [open, editingCourse, instructors]); @@ -103,7 +193,7 @@ export default function CourseRegistrationModal({ open, onClose, onSave, onDelet }; // 저장 버튼 클릭 핸들러 - const handleSave = () => { + const handleSave = async () => { const nextErrors: Record = {}; if (!courseName.trim()) { @@ -119,8 +209,184 @@ export default function CourseRegistrationModal({ open, onClose, onSave, onDelet return; } - if (onSave && selectedInstructor) { - onSave(courseName.trim(), selectedInstructor.name); + if (isSaving) return; // 이미 저장 중이면 중복 호출 방지 + + // selectedInstructor가 없으면 종료 + if (!selectedInstructor) { + return; + } + + setIsSaving(true); + setErrors((prev) => { + const next = { ...prev }; + delete next.submit; + return next; + }); + + 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: '이미지 업로드에 실패했습니다. 이미지 없이 계속 진행됩니다.', + })); + // 이미지 업로드 실패해도 계속 진행 (선택사항) + } + } catch (uploadError) { + const errorMessage = uploadError instanceof Error ? uploadError.message : '이미지 업로드 중 오류가 발생했습니다.'; + console.error('이미지 업로드 오류:', errorMessage); + setErrors((prev) => ({ + ...prev, + image: '이미지 업로드 중 오류가 발생했습니다. 이미지 없이 계속 진행됩니다.', + })); + // 이미지 업로드 오류 발생해도 계속 진행 (선택사항) + } + } + + // /subjects API 호출 + const requestBody: { + title: string; + instructor: string; + imageKey?: string | null; + } = { + 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 파싱 실패 시 기본 메시지 사용 + } + console.error('과목 등록 실패:', errorMessage); + setErrors({ submit: errorMessage }); + setIsSaving(false); + return; + } + + // 성공 시 onSave 콜백 호출 + if (onSave && selectedInstructor) { + onSave(courseName.trim(), selectedInstructor.name); + } + } catch (error) { + const errorMessage = error instanceof Error ? error.message : '네트워크 오류가 발생했습니다.'; + console.error('과목 등록 오류:', errorMessage); + setErrors({ submit: errorMessage }); + } finally { + setIsSaving(false); } }; @@ -253,10 +519,10 @@ export default function CourseRegistrationModal({ open, onClose, onSave, onDelet className="relative z-10 shadow-xl" onClick={handleModalClick} > -
+
{/* Header */}
-

+

{editingCourse ? "교육과정 수정" : "과목 등록"}

{isDropdownOpen && ( -
- {instructors.length === 0 ? ( -
+
+ {isLoadingInstructors ? ( +
+ 로딩 중... +
+ ) : instructors.length === 0 ? ( +
등록된 강사가 없습니다.
) : ( @@ -348,10 +625,10 @@ export default function CourseRegistrationModal({ open, onClose, onSave, onDelet }); } }} - className={`w-full px-3 py-2 text-left text-[16px] font-normal leading-[1.5] hover:bg-[#f1f3f5] transition-colors cursor-pointer ${ + className={`w-full px-3 py-2 text-left text-[16px] font-normal leading-normal hover:bg-[var(--color-bg-gray-light)] transition-colors cursor-pointer ${ instructorId === instructor.id - ? "bg-[#ecf0ff] text-[#1f2b91] font-semibold" - : "text-[#1b2027]" + ? "bg-[var(--color-bg-primary-light)] text-[var(--color-active-button)] font-semibold" + : "text-[var(--color-text-title)]" } ${ index === 0 ? "rounded-t-[8px]" : "" } ${ @@ -366,7 +643,7 @@ export default function CourseRegistrationModal({ open, onClose, onSave, onDelet )}
{errors.instructor && ( -

{errors.instructor}

+

{errors.instructor}

)}
@@ -374,10 +651,10 @@ export default function CourseRegistrationModal({ open, onClose, onSave, onDelet
-
@@ -399,8 +676,8 @@ export default function CourseRegistrationModal({ open, onClose, onSave, onDelet isDragging ? "bg-blue-50 border-blue-300" : previewUrl - ? "bg-white border-[#dee1e6]" - : "bg-gray-50 border-[#dee1e6] hover:bg-gray-100" + ? "bg-white border-[var(--color-neutral-40)]" + : "bg-gray-50 border-[var(--color-neutral-40)] hover:bg-gray-100" }`} > {previewUrl ? ( @@ -424,7 +701,7 @@ export default function CourseRegistrationModal({ open, onClose, onSave, onDelet
-

+

클릭하여 이미지 변경

@@ -442,7 +719,7 @@ export default function CourseRegistrationModal({ open, onClose, onSave, onDelet >
-

+

(클릭하여 이미지 업로드) 미첨부 시 기본 이미지가 노출됩니다. @@ -460,12 +737,19 @@ export default function CourseRegistrationModal({ open, onClose, onSave, onDelet )}

{errors.image && ( -

{errors.image}

+

{errors.image}

)}
+ {/* 에러 메시지 표시 */} + {errors.submit && ( +
+

{errors.submit}

+
+ )} + {/* Actions Container */}
@@ -473,7 +757,7 @@ export default function CourseRegistrationModal({ open, onClose, onSave, onDelet @@ -481,16 +765,17 @@ export default function CourseRegistrationModal({ open, onClose, onSave, onDelet
@@ -507,10 +792,10 @@ export default function CourseRegistrationModal({ open, onClose, onSave, onDelet />
-

+

교육과정을 삭제하시겠습니까?

-

+

삭제된 교육과정은 복구할 수 없습니다.
정말 삭제하시겠습니까? @@ -520,14 +805,14 @@ export default function CourseRegistrationModal({ open, onClose, onSave, onDelet diff --git a/src/app/admin/courses/mockData.ts b/src/app/admin/courses/mockData.ts index 433a698..2d8ca70 100644 --- a/src/app/admin/courses/mockData.ts +++ b/src/app/admin/courses/mockData.ts @@ -7,100 +7,80 @@ export type Course = { hasLessons: boolean; // 강좌포함여부 }; -// TODO: 나중에 DB에서 가져오도록 변경 -export const MOCK_CURRENT_USER = "관리자"; // 현재 로그인한 사용자 이름 +// 과목 리스트 조회 API +export async function getCourses(): Promise { + try { + const token = typeof window !== 'undefined' + ? (localStorage.getItem('token') || document.cookie + .split('; ') + .find(row => row.startsWith('token=')) + ?.split('=')[1]) + : null; -// TODO: 이 부분도 나중에는 db에서 받아오도록 변경 예정 -// 임시 데이터 - 기본 강사명 목록 (getInstructors는 async이므로 모듈 레벨에서 사용 불가) -const defaultInstructorNames = [ - "최예준", - "정시우", - "임건우", - "송윤서", - "김민수", - "정대현", -]; + const baseUrl = process.env.NEXT_PUBLIC_API_BASE_URL + ? process.env.NEXT_PUBLIC_API_BASE_URL + : 'https://hrdi.coconutmeet.net'; -export const MOCK_COURSES: Course[] = [ - { - id: "1", - courseName: "웹 개발 기초", - instructorName: defaultInstructorNames[0] || "최예준", - createdAt: "2024-01-15", - createdBy: MOCK_CURRENT_USER, - hasLessons: false, - }, - { - id: "2", - courseName: "React 실전 프로젝트", - instructorName: defaultInstructorNames[1] || "정시우", - createdAt: "2024-02-20", - createdBy: MOCK_CURRENT_USER, - hasLessons: false, - }, - { - id: "3", - courseName: "데이터베이스 설계", - instructorName: defaultInstructorNames[2] || "임건우", - createdAt: "2024-03-10", - createdBy: MOCK_CURRENT_USER, - hasLessons: false, - }, - { - id: "4", - courseName: "Node.js 백엔드 개발", - instructorName: defaultInstructorNames[3] || "송윤서", - createdAt: "2024-03-25", - createdBy: MOCK_CURRENT_USER, - hasLessons: false, - }, - { - id: "5", - courseName: "TypeScript 마스터", - instructorName: defaultInstructorNames[4] || "김민수", - createdAt: "2024-04-05", - createdBy: MOCK_CURRENT_USER, - hasLessons: false, - }, - { - id: "6", - courseName: "UI/UX 디자인 기초", - instructorName: defaultInstructorNames[5] || "정대현", - createdAt: "2024-04-18", - createdBy: MOCK_CURRENT_USER, - hasLessons: false, - }, - { - id: "7", - courseName: "모바일 앱 개발", - instructorName: defaultInstructorNames[0] || "최예준", - createdAt: "2024-05-02", - createdBy: MOCK_CURRENT_USER, - hasLessons: false, - }, - { - id: "8", - courseName: "클라우드 인프라", - instructorName: defaultInstructorNames[1] || "정시우", - createdAt: "2024-05-15", - createdBy: MOCK_CURRENT_USER, - hasLessons: false, - }, - { - id: "9", - courseName: "머신러닝 입문", - instructorName: defaultInstructorNames[2] || "임건우", - createdAt: "2024-06-01", - createdBy: MOCK_CURRENT_USER, - hasLessons: false, - }, - { - id: "10", - courseName: "DevOps 실무", - instructorName: defaultInstructorNames[3] || "송윤서", - createdAt: "2024-06-20", - createdBy: MOCK_CURRENT_USER, - hasLessons: false, - }, -]; + const apiUrl = `${baseUrl}/subjects`; + const response = await fetch(apiUrl, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + ...(token && { Authorization: `Bearer ${token}` }), + }, + }); + + if (!response.ok) { + // 404 에러는 리소스가 없는 것이므로 빈 배열 반환 + if (response.status === 404) { + console.warn('⚠️ [getCourses] 과목 리스트를 찾을 수 없습니다 (404)'); + return []; + } + + const errorText = await response.text(); + console.error('❌ [getCourses] API 에러 응답:', errorText); + throw new Error(`과목 리스트 조회 실패 (${response.status})`); + } + + const data = await response.json(); + + // 디버깅: API 응답 구조 확인 + console.log('🔍 [getCourses] API 원본 응답:', data); + console.log('🔍 [getCourses] 응답 타입:', Array.isArray(data) ? '배열' : typeof data); + + // API 응답이 배열이 아닌 경우 처리 (예: { items: [...] } 형태) + let coursesArray: any[] = []; + if (Array.isArray(data)) { + coursesArray = data; + } else if (data && typeof data === 'object') { + // 더 많은 가능한 필드명 확인 + coursesArray = data.items || data.courses || data.data || data.list || data.subjects || data.subjectList || []; + } + + console.log('🔍 [getCourses] 변환 전 배열:', coursesArray); + console.log('🔍 [getCourses] 배열 길이:', coursesArray.length); + + // API 응답 데이터를 Course 형식으로 변환 + const transformedCourses: Course[] = coursesArray.map((item: any) => { + const transformed = { + id: String(item.id || item.subjectId || item.subject_id || ''), + 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), + }; + console.log('🔍 [getCourses] 변환된 항목:', transformed); + return transformed; + }); + + console.log('🔍 [getCourses] 최종 변환된 배열:', transformedCourses); + console.log('🔍 [getCourses] 최종 배열 길이:', transformedCourses.length); + + return transformedCourses; + } catch (error) { + console.error('과목 리스트 조회 오류:', error); + return []; + } +} diff --git a/src/app/admin/courses/page.tsx b/src/app/admin/courses/page.tsx index 5648148..aa2c581 100644 --- a/src/app/admin/courses/page.tsx +++ b/src/app/admin/courses/page.tsx @@ -1,17 +1,38 @@ 'use client'; -import { useState, useMemo } from "react"; +import { useState, useMemo, useEffect } from "react"; import AdminSidebar from "@/app/components/AdminSidebar"; import CourseRegistrationModal from "./CourseRegistrationModal"; import ChevronDownSvg from "@/app/svgs/chevrondownsvg"; -import { MOCK_COURSES, MOCK_CURRENT_USER, type Course } from "./mockData"; +import { getCourses, type Course } from "./mockData"; export default function AdminCoursesPage() { - const [courses, setCourses] = useState(MOCK_COURSES); + const [courses, setCourses] = useState([]); + const [isLoading, setIsLoading] = useState(true); const [isModalOpen, setIsModalOpen] = useState(false); const [editingCourse, setEditingCourse] = useState(null); const [currentPage, setCurrentPage] = useState(1); const [showToast, setShowToast] = useState(false); + + // API에서 과목 리스트 가져오기 + useEffect(() => { + async function fetchCourses() { + try { + setIsLoading(true); + const data = await getCourses(); + console.log('📋 [AdminCoursesPage] 받은 데이터:', data); + console.log('📋 [AdminCoursesPage] 데이터 개수:', data.length); + setCourses(data); + } catch (error) { + console.error('과목 리스트 로드 오류:', error); + setCourses([]); + } finally { + setIsLoading(false); + } + } + + fetchCourses(); + }, []); const totalCount = useMemo(() => courses.length, [courses]); @@ -30,28 +51,36 @@ export default function AdminCoursesPage() { return sortedCourses.slice(startIndex, endIndex); }, [sortedCourses, currentPage]); - const handleSaveCourse = (courseName: string, instructorName: string) => { + const handleSaveCourse = async (courseName: string, instructorName: string) => { if (editingCourse) { - // 수정 모드 + // 수정 모드 - TODO: API 호출로 변경 필요 setCourses(prev => prev.map(course => course.id === editingCourse.id ? { ...course, courseName, instructorName } : course )); } else { - // 등록 모드 + // 등록 모드 - TODO: API 호출로 변경 필요 const newCourse: Course = { id: String(Date.now()), courseName, instructorName, createdAt: new Date().toISOString().split('T')[0], - createdBy: MOCK_CURRENT_USER, + createdBy: '', // API에서 받아오도록 변경 필요 hasLessons: false, // 기본값: 미포함 }; setCourses(prev => [...prev, newCourse]); } setIsModalOpen(false); setEditingCourse(null); + + // 저장 후 리스트 새로고침 + try { + const data = await getCourses(); + setCourses(data); + } catch (error) { + console.error('과목 리스트 새로고침 오류:', error); + } }; const handleRowClick = (course: Course) => { @@ -69,14 +98,23 @@ export default function AdminCoursesPage() { setIsModalOpen(true); }; - const handleDeleteCourse = () => { + const handleDeleteCourse = async () => { if (editingCourse) { + // TODO: API 호출로 삭제 처리 필요 setCourses(prev => prev.filter(course => course.id !== editingCourse.id)); setEditingCourse(null); setShowToast(true); setTimeout(() => { setShowToast(false); }, 3000); + + // 삭제 후 리스트 새로고침 + try { + const data = await getCourses(); + setCourses(data); + } catch (error) { + console.error('과목 리스트 새로고침 오류:', error); + } } }; @@ -115,7 +153,13 @@ export default function AdminCoursesPage() { {/* 콘텐츠 영역 */}

- {courses.length === 0 ? ( + {isLoading ? ( +
+

+ 로딩 중... +

+
+ ) : courses.length === 0 ? (

등록된 교육과정이 없습니다. @@ -128,18 +172,16 @@ export default function AdminCoursesPage() {

- - - - - + + + + - @@ -159,9 +201,6 @@ export default function AdminCoursesPage() { -
교육과정명 강사명 생성일등록자 강좌포함여부
{course.createdAt} - {course.createdBy} - {course.hasLessons ? (
diff --git a/src/app/admin/id/mockData.ts b/src/app/admin/id/mockData.ts index ed8124b..fd57d3e 100644 --- a/src/app/admin/id/mockData.ts +++ b/src/app/admin/id/mockData.ts @@ -26,9 +26,9 @@ export async function getInstructors(): Promise { ? `${process.env.NEXT_PUBLIC_API_BASE_URL}/admin/users/compact` : 'https://hrdi.coconutmeet.net/admin/users/compact'; - // 쿼리 파라미터 추가: type=STUDENT, limit=10 + // 쿼리 파라미터 추가: type=ADMIN, limit=10 const apiUrl = new URL(baseUrl); - apiUrl.searchParams.set('type', 'STUDENT'); + apiUrl.searchParams.set('type', 'ADMIN'); apiUrl.searchParams.set('limit', '10'); console.log('🔍 [getInstructors] API 호출 정보:', { @@ -110,7 +110,7 @@ export async function getInstructors(): Promise { return transformed; }) - .filter((user: UserRow) => user.role === 'instructor' && user.status === 'active') + .filter((user: UserRow) => user.role === 'admin' && user.status === 'active') : []; console.log('✅ [getInstructors] 변환된 강사 데이터:', { diff --git a/src/app/admin/lessons/page.tsx b/src/app/admin/lessons/page.tsx index 91d8b1e..f4e62b2 100644 --- a/src/app/admin/lessons/page.tsx +++ b/src/app/admin/lessons/page.tsx @@ -5,7 +5,7 @@ import AdminSidebar from "@/app/components/AdminSidebar"; import ChevronDownSvg from "@/app/svgs/chevrondownsvg"; import DropdownIcon from "@/app/svgs/dropdownicon"; import BackArrowSvg from "@/app/svgs/backarrow"; -import { MOCK_COURSES, MOCK_CURRENT_USER } from "@/app/admin/courses/mockData"; +import { getCourses, type Course } from "@/app/admin/courses/mockData"; import CloseXOSvg from "@/app/svgs/closexo"; type Lesson = { @@ -24,6 +24,8 @@ export default function AdminLessonsPage() { const [isRegistrationMode, setIsRegistrationMode] = useState(false); const [isDropdownOpen, setIsDropdownOpen] = useState(false); const dropdownRef = useRef(null); + const [courses, setCourses] = useState([]); + const [currentUser, setCurrentUser] = useState("관리자"); // 등록 폼 상태 const [selectedCourse, setSelectedCourse] = useState(""); @@ -35,6 +37,60 @@ export default function AdminLessonsPage() { const [vrContentFiles, setVrContentFiles] = useState([]); const [questionFileCount, setQuestionFileCount] = useState(0); + // 교육과정 목록 가져오기 + useEffect(() => { + async function fetchCourses() { + try { + const data = await getCourses(); + setCourses(data); + } catch (error) { + console.error('교육과정 목록 로드 오류:', error); + setCourses([]); + } + } + fetchCourses(); + }, []); + + // 현재 사용자 정보 가져오기 + useEffect(() => { + async function fetchCurrentUser() { + try { + const token = typeof window !== 'undefined' + ? (localStorage.getItem('token') || document.cookie + .split('; ') + .find(row => row.startsWith('token=')) + ?.split('=')[1]) + : null; + + if (!token) { + return; + } + + const apiUrl = process.env.NEXT_PUBLIC_API_BASE_URL + ? `${process.env.NEXT_PUBLIC_API_BASE_URL}/auth/me` + : 'https://hrdi.coconutmeet.net/auth/me'; + + const response = await fetch(apiUrl, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}`, + }, + }); + + if (response.ok) { + const data = await response.json(); + if (data.name) { + setCurrentUser(data.name); + } + } + } catch (error) { + console.error('사용자 정보 조회 오류:', error); + } + } + fetchCurrentUser(); + }, []); + const totalCount = useMemo(() => lessons.length, [lessons]); const ITEMS_PER_PAGE = 10; @@ -52,13 +108,13 @@ export default function AdminLessonsPage() { return sortedLessons.slice(startIndex, endIndex); }, [sortedLessons, currentPage]); - // 교육과정 옵션 - mockData에서 가져오기 + // 교육과정 옵션 const courseOptions = useMemo(() => - MOCK_COURSES.map(course => ({ + courses.map(course => ({ id: course.id, name: course.courseName })) - , []); + , [courses]); // 외부 클릭 시 드롭다운 닫기 useEffect(() => { @@ -129,7 +185,7 @@ export default function AdminLessonsPage() { lessonName, attachments, questionCount: questionFileCount, - createdBy: MOCK_CURRENT_USER, + createdBy: currentUser, createdAt: new Date().toISOString().split('T')[0], };