diff --git a/src/app/NavBar.tsx b/src/app/NavBar.tsx index e999dfa..4314ddc 100644 --- a/src/app/NavBar.tsx +++ b/src/app/NavBar.tsx @@ -13,7 +13,7 @@ const NAV_ITEMS = [ ]; const INSTRUCTOR_NAV_ITEMS = [ - { label: "강좌 현황", href: "/admin/courses" }, + { label: "강좌 현황", href: "/instructor/courses" }, { label: "학습 자료실", href: "/admin/resources" }, { label: "공지사항", href: "/admin/notices" }, ]; @@ -28,7 +28,7 @@ export default function NavBar() { const userButtonRef = useRef(null); const hideCenterNav = /^\/[^/]+\/review$/.test(pathname); const isAdminPage = pathname.startsWith('/admin'); - const isInstructorPage = pathname === '/instructor'; + const isInstructorPage = pathname.startsWith('/instructor'); // 사용자 정보 가져오기 및 비활성화 계정 체크 useEffect(() => { @@ -162,6 +162,12 @@ export default function NavBar() { ); })} + + 관리자페이지 + )} {!hideCenterNav && !isAdminPage && !isInstructorPage && ( diff --git a/src/app/instructor/courses/page.tsx b/src/app/instructor/courses/page.tsx new file mode 100644 index 0000000..2d99f88 --- /dev/null +++ b/src/app/instructor/courses/page.tsx @@ -0,0 +1,749 @@ +'use client'; + +import { useEffect, useState, useMemo } from 'react'; +import { useRouter } from 'next/navigation'; +import ChevronDownSvg from '../../svgs/chevrondownsvg'; + +// 드롭다운 아이콘 컴포넌트 +function ArrowDownIcon({ className }: { className?: string }) { + return ( + + + + ); +} + +// 검색 아이콘 컴포넌트 +function SearchIcon({ className }: { className?: string }) { + return ( + + + + + ); +} + +// 태그 컴포넌트 +function StatusTag({ text, type = 'default', color = 'primary' }: { text: string; type?: 'default' | 'emphasis'; color?: 'primary' | 'gray' }) { + if (type === 'default' && color === 'primary') { + return ( +
+ + {text} + +
+ ); + } + if (type === 'default' && color === 'gray') { + return ( +
+ + {text} + +
+ ); + } + return ( +
+ + {text} + +
+ ); +} + +type LearnerProgress = { + id: string; + courseName: string; + lessonName: string; + learnerName: string; + enrollmentDate: string; + lastStudyDate: string; + progressRate: number; + hasSubmitted: boolean; + score: number | null; + isCompleted: boolean; +}; + +export default function InstructorCoursesPage() { + const router = useRouter(); + const [userRole, setUserRole] = useState(''); + const [isLoading, setIsLoading] = useState(true); + const [currentPage, setCurrentPage] = useState(1); + + // 필터 상태 + const [selectedCourse, setSelectedCourse] = useState('all'); + const [selectedSubmissionStatus, setSelectedSubmissionStatus] = useState('all'); + const [selectedCompletionStatus, setSelectedCompletionStatus] = useState('all'); + const [searchQuery, setSearchQuery] = useState(''); + + // 드롭다운 열림 상태 + const [isCourseDropdownOpen, setIsCourseDropdownOpen] = useState(false); + const [isSubmissionDropdownOpen, setIsSubmissionDropdownOpen] = useState(false); + const [isCompletionDropdownOpen, setIsCompletionDropdownOpen] = useState(false); + + // 데이터 + const [courses, setCourses] = useState<{ id: string; name: string }[]>([]); + const [learnerProgress, setLearnerProgress] = useState([]); + + const ITEMS_PER_PAGE = 10; + + // 사용자 정보 및 권한 확인 + useEffect(() => { + let isMounted = true; + + async function fetchUserInfo() { + try { + const localStorageToken = localStorage.getItem('token'); + const cookieToken = document.cookie + .split('; ') + .find(row => row.startsWith('token=')) + ?.split('=')[1]; + + const token = localStorageToken || cookieToken; + + if (!token) { + router.push('/login'); + 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) { + if (response.status === 401) { + localStorage.removeItem('token'); + document.cookie = 'token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;'; + if (isMounted) { + router.push('/login'); + } + } + return; + } + + const data = await response.json(); + + if (isMounted) { + const role = data.role || data.userRole || ''; + setUserRole(role); + + // admin이 아니면 접근 불가 + if (role !== 'ADMIN' && role !== 'admin') { + router.push('/'); + return; + } + } + } catch (error) { + console.error('사용자 정보 조회 오류:', error); + if (isMounted) { + router.push('/login'); + } + } + } + + fetchUserInfo(); + + return () => { + isMounted = false; + }; + }, [router]); + + // 교육 과정 목록 가져오기 + useEffect(() => { + async function fetchCourses() { + try { + const token = localStorage.getItem('token') || document.cookie + .split('; ') + .find(row => row.startsWith('token=')) + ?.split('=')[1]; + + const apiUrl = process.env.NEXT_PUBLIC_API_BASE_URL + ? `${process.env.NEXT_PUBLIC_API_BASE_URL}/subjects` + : 'https://hrdi.coconutmeet.net/subjects'; + + const response = await fetch(apiUrl, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + ...(token && { Authorization: `Bearer ${token}` }), + }, + }); + + if (response.ok) { + const data = await response.json(); + const coursesArray = Array.isArray(data) ? data : (data.items || data.courses || data.data || []); + setCourses(coursesArray.map((item: any) => ({ + id: String(item.id || item.subjectId || ''), + name: item.courseName || item.name || item.subjectName || '', + }))); + } + } catch (error) { + console.error('교육 과정 목록 조회 오류:', error); + } + } + + fetchCourses(); + }, []); + + // 학습자 진행 상황 데이터 (더미 데이터 - 실제 API로 교체 필요) + useEffect(() => { + async function fetchLearnerProgress() { + try { + setIsLoading(true); + // TODO: 실제 API 호출로 교체 + // 현재는 더미 데이터 사용 + const dummyData: LearnerProgress[] = [ + { + id: '1', + courseName: '방사선 관리', + lessonName: '{강좌명}', + learnerName: '김하늘', + enrollmentDate: '2025-09-10', + lastStudyDate: '2025-09-10', + progressRate: 100, + hasSubmitted: true, + score: 100, + isCompleted: true, + }, + { + id: '2', + courseName: '방사선 관리', + lessonName: '{강좌명}', + learnerName: '김하늘', + enrollmentDate: '2025-09-10', + lastStudyDate: '2025-09-10', + progressRate: 100, + hasSubmitted: true, + score: 100, + isCompleted: true, + }, + { + id: '3', + courseName: '방사선 관리', + lessonName: '{강좌명}', + learnerName: '김하늘', + enrollmentDate: '2025-09-10', + lastStudyDate: '2025-09-10', + progressRate: 100, + hasSubmitted: true, + score: 100, + isCompleted: true, + }, + { + id: '4', + courseName: '원자로 운전 및 계통', + lessonName: '6. 원자로 시동, 운전 및 정지 절차', + learnerName: '김하늘', + enrollmentDate: '2025-09-10', + lastStudyDate: '2025-09-10', + progressRate: 100, + hasSubmitted: true, + score: 60, + isCompleted: true, + }, + { + id: '5', + courseName: '방사선 관리', + lessonName: '{강좌명}', + learnerName: '김하늘', + enrollmentDate: '2025-09-10', + lastStudyDate: '2025-09-10', + progressRate: 100, + hasSubmitted: true, + score: 30, + isCompleted: false, + }, + { + id: '6', + courseName: '방사선 관리', + lessonName: '{강좌명}', + learnerName: '김하늘', + enrollmentDate: '2025-09-10', + lastStudyDate: '2025-09-10', + progressRate: 100, + hasSubmitted: true, + score: 30, + isCompleted: false, + }, + { + id: '7', + courseName: '방사선 관리', + lessonName: '{강좌명}', + learnerName: '김하늘', + enrollmentDate: '2025-09-10', + lastStudyDate: '2025-09-10', + progressRate: 100, + hasSubmitted: false, + score: null, + isCompleted: false, + }, + ]; + setLearnerProgress(dummyData); + } catch (error) { + console.error('학습자 진행 상황 조회 오류:', error); + } finally { + setIsLoading(false); + } + } + + fetchLearnerProgress(); + }, []); + + // 필터링된 데이터 + const filteredData = useMemo(() => { + return learnerProgress.filter((item) => { + // 교육 과정 필터 + if (selectedCourse !== 'all' && item.courseName !== selectedCourse) { + return false; + } + + // 문제 제출 여부 필터 + if (selectedSubmissionStatus === 'submitted' && !item.hasSubmitted) { + return false; + } + if (selectedSubmissionStatus === 'not-submitted' && item.hasSubmitted) { + return false; + } + + // 수료 여부 필터 + if (selectedCompletionStatus === 'completed' && !item.isCompleted) { + return false; + } + if (selectedCompletionStatus === 'not-completed' && item.isCompleted) { + return false; + } + + // 검색어 필터 + if (searchQuery && !item.learnerName.toLowerCase().includes(searchQuery.toLowerCase())) { + return false; + } + + return true; + }); + }, [learnerProgress, selectedCourse, selectedSubmissionStatus, selectedCompletionStatus, searchQuery]); + + // 페이지네이션 + const totalPages = Math.ceil(filteredData.length / ITEMS_PER_PAGE); + const paginatedData = useMemo(() => { + const startIndex = (currentPage - 1) * ITEMS_PER_PAGE; + const endIndex = startIndex + ITEMS_PER_PAGE; + return filteredData.slice(startIndex, endIndex); + }, [filteredData, currentPage]); + + // 드롭다운 외부 클릭 감지 + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + const target = event.target as HTMLElement; + if (!target.closest('.dropdown-container')) { + setIsCourseDropdownOpen(false); + setIsSubmissionDropdownOpen(false); + setIsCompletionDropdownOpen(false); + } + }; + + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, []); + + return ( +
+
+
+

+ 강좌 현황 +

+
+ +
+ {/* 필터 및 검색 영역 */} +
+
+ {/* 교육 과정 드롭다운 */} +
+ +
+ + {isCourseDropdownOpen && ( +
+ + {courses.map((course) => ( + + ))} +
+ )} +
+
+ + {/* 문제 제출 여부 드롭다운 */} +
+ +
+ + {isSubmissionDropdownOpen && ( +
+ + + +
+ )} +
+
+ + {/* 수료 여부 드롭다운 */} +
+ +
+ + {isCompletionDropdownOpen && ( +
+ + + +
+ )} +
+
+
+ + {/* 검색바 */} +
+ setSearchQuery(e.target.value)} + className="flex-1 text-[16px] font-normal leading-[1.5] text-[#b1b8c0] outline-none placeholder:text-[#b1b8c0]" + /> + +
+
+ + {/* 테이블 영역 */} +
+ {isLoading ? ( +
+

로딩 중...

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

데이터가 없습니다.

+
+ ) : ( + <> + {/* 테이블 헤더 */} +
+
+ 교육 과정명 +
+
+ 강좌명 +
+
+ 학습자명 +
+
+ 가입일 +
+
+ 마지막 수강일 +
+
+ 진도율 +
+
+ 문제 제출 여부 +
+
+ 평가 점수 +
+
+ 수료 여부 +
+
+ + {/* 테이블 바디 */} +
+ {paginatedData.map((item) => ( +
+
+ {item.courseName} +
+
+ {item.lessonName} +
+
+ {item.learnerName} +
+
+ {item.enrollmentDate} +
+
+ {item.lastStudyDate} +
+
+ {item.progressRate}% +
+
+ {item.hasSubmitted ? ( + + ) : ( + + )} +
+
+ + {item.score !== null ? `${item.score}점` : '-'} + +
+
+ {item.isCompleted ? ( + + ) : ( + + )} +
+
+ ))} +
+ + )} +
+ + {/* 페이지네이션 */} + {filteredData.length > ITEMS_PER_PAGE && ( +
+ {/* First */} + + + {/* Prev */} + + + {/* Page Numbers */} + {Array.from({ length: Math.min(10, totalPages) }, (_, i) => { + const pageNum = i + 1; + const isActive = pageNum === currentPage; + return ( + + ); + })} + + {/* Next */} + + + {/* Last */} + +
+ )} +
+
+
+ ); +} + diff --git a/src/app/instructor/page.tsx b/src/app/instructor/page.tsx index 5bed73c..6404dbb 100644 --- a/src/app/instructor/page.tsx +++ b/src/app/instructor/page.tsx @@ -259,7 +259,7 @@ export default function InstructorPage() { 강좌별 상세 내역 전체보기