instructor page
This commit is contained in:
@@ -13,7 +13,7 @@ const NAV_ITEMS = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const INSTRUCTOR_NAV_ITEMS = [
|
const INSTRUCTOR_NAV_ITEMS = [
|
||||||
{ label: "강좌 현황", href: "/admin/courses" },
|
{ label: "강좌 현황", href: "/instructor/courses" },
|
||||||
{ label: "학습 자료실", href: "/admin/resources" },
|
{ label: "학습 자료실", href: "/admin/resources" },
|
||||||
{ label: "공지사항", href: "/admin/notices" },
|
{ label: "공지사항", href: "/admin/notices" },
|
||||||
];
|
];
|
||||||
@@ -28,7 +28,7 @@ export default function NavBar() {
|
|||||||
const userButtonRef = useRef<HTMLButtonElement | null>(null);
|
const userButtonRef = useRef<HTMLButtonElement | null>(null);
|
||||||
const hideCenterNav = /^\/[^/]+\/review$/.test(pathname);
|
const hideCenterNav = /^\/[^/]+\/review$/.test(pathname);
|
||||||
const isAdminPage = pathname.startsWith('/admin');
|
const isAdminPage = pathname.startsWith('/admin');
|
||||||
const isInstructorPage = pathname === '/instructor';
|
const isInstructorPage = pathname.startsWith('/instructor');
|
||||||
|
|
||||||
// 사용자 정보 가져오기 및 비활성화 계정 체크
|
// 사용자 정보 가져오기 및 비활성화 계정 체크
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -162,6 +162,12 @@ export default function NavBar() {
|
|||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
<Link
|
||||||
|
href="/admin"
|
||||||
|
className={["px-4 py-2 text-[16px] font-semibold text-white"].join(" ")}
|
||||||
|
>
|
||||||
|
관리자페이지
|
||||||
|
</Link>
|
||||||
</nav>
|
</nav>
|
||||||
)}
|
)}
|
||||||
{!hideCenterNav && !isAdminPage && !isInstructorPage && (
|
{!hideCenterNav && !isAdminPage && !isInstructorPage && (
|
||||||
|
|||||||
749
src/app/instructor/courses/page.tsx
Normal file
749
src/app/instructor/courses/page.tsx
Normal file
@@ -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 (
|
||||||
|
<svg
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
className={className}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M4 6L8 10L12 6"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 검색 아이콘 컴포넌트
|
||||||
|
function SearchIcon({ className }: { className?: string }) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
className={className}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M9 17C13.4183 17 17 13.4183 17 9C17 4.58172 13.4183 1 9 1C4.58172 1 1 4.58172 1 9C1 13.4183 4.58172 17 9 17Z"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M19 19L14.65 14.65"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 태그 컴포넌트
|
||||||
|
function StatusTag({ text, type = 'default', color = 'primary' }: { text: string; type?: 'default' | 'emphasis'; color?: 'primary' | 'gray' }) {
|
||||||
|
if (type === 'default' && color === 'primary') {
|
||||||
|
return (
|
||||||
|
<div className="inline-flex items-center justify-center h-[20px] px-[4px] rounded-[4px] bg-[#ecf0ff]">
|
||||||
|
<span className="text-[13px] font-semibold leading-[1.4] text-[#384fbf] whitespace-nowrap">
|
||||||
|
{text}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (type === 'default' && color === 'gray') {
|
||||||
|
return (
|
||||||
|
<div className="inline-flex items-center justify-center h-[20px] px-[4px] rounded-[4px] bg-[#f1f3f5]">
|
||||||
|
<span className="text-[13px] font-semibold leading-[1.4] text-[#4c5561] whitespace-nowrap">
|
||||||
|
{text}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className="inline-flex items-center justify-center h-[20px] px-[4px] rounded-[4px] bg-[#ecf0ff]">
|
||||||
|
<span className="text-[13px] font-semibold leading-[1.4] text-[#384fbf] whitespace-nowrap">
|
||||||
|
{text}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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<string>('');
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
|
|
||||||
|
// 필터 상태
|
||||||
|
const [selectedCourse, setSelectedCourse] = useState<string>('all');
|
||||||
|
const [selectedSubmissionStatus, setSelectedSubmissionStatus] = useState<string>('all');
|
||||||
|
const [selectedCompletionStatus, setSelectedCompletionStatus] = useState<string>('all');
|
||||||
|
const [searchQuery, setSearchQuery] = useState<string>('');
|
||||||
|
|
||||||
|
// 드롭다운 열림 상태
|
||||||
|
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<LearnerProgress[]>([]);
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<div className="bg-white min-h-screen flex flex-col">
|
||||||
|
<div className="flex-1 max-w-[1440px] w-full mx-auto px-0">
|
||||||
|
<div className="flex flex-col gap-[10px] h-[100px] items-start px-[32px]">
|
||||||
|
<h1 className="text-[24px] font-bold leading-[1.5] text-[#1b2027]">
|
||||||
|
강좌 현황
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-[16px] px-[32px] py-[32px]">
|
||||||
|
{/* 필터 및 검색 영역 */}
|
||||||
|
<div className="flex items-end justify-between gap-[16px]">
|
||||||
|
<div className="flex gap-[8px] items-end">
|
||||||
|
{/* 교육 과정 드롭다운 */}
|
||||||
|
<div className="flex flex-col gap-[4px] relative dropdown-container">
|
||||||
|
<label className="text-[14px] font-semibold leading-[1.5] text-[#6c7682]">
|
||||||
|
교육 과정
|
||||||
|
</label>
|
||||||
|
<div className="relative">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setIsCourseDropdownOpen(!isCourseDropdownOpen);
|
||||||
|
setIsSubmissionDropdownOpen(false);
|
||||||
|
setIsCompletionDropdownOpen(false);
|
||||||
|
}}
|
||||||
|
className="bg-white border border-[#dee1e6] rounded-[8px] h-[40px] px-[12px] py-[6px] flex items-center justify-between gap-[8px] w-[400px]"
|
||||||
|
>
|
||||||
|
<span className="text-[15px] font-normal leading-[1.5] text-[#6c7682] flex-1 text-left">
|
||||||
|
{selectedCourse === 'all' ? '선택 안함' : courses.find(c => c.id === selectedCourse)?.name || '선택 안함'}
|
||||||
|
</span>
|
||||||
|
<ArrowDownIcon className="size-[16px] text-[#6c7682] shrink-0" />
|
||||||
|
</button>
|
||||||
|
{isCourseDropdownOpen && (
|
||||||
|
<div className="absolute top-full left-0 mt-1 bg-white border border-[#dee1e6] rounded-[8px] shadow-lg z-50 w-[400px] max-h-[200px] overflow-y-auto">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedCourse('all');
|
||||||
|
setIsCourseDropdownOpen(false);
|
||||||
|
}}
|
||||||
|
className="w-full px-[12px] py-[8px] text-left text-[15px] font-normal leading-[1.5] text-[#1b2027] hover:bg-[#f1f3f5]"
|
||||||
|
>
|
||||||
|
선택 안함
|
||||||
|
</button>
|
||||||
|
{courses.map((course) => (
|
||||||
|
<button
|
||||||
|
key={course.id}
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedCourse(course.name);
|
||||||
|
setIsCourseDropdownOpen(false);
|
||||||
|
}}
|
||||||
|
className="w-full px-[12px] py-[8px] text-left text-[15px] font-normal leading-[1.5] text-[#1b2027] hover:bg-[#f1f3f5]"
|
||||||
|
>
|
||||||
|
{course.name}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 문제 제출 여부 드롭다운 */}
|
||||||
|
<div className="flex flex-col gap-[4px] relative dropdown-container">
|
||||||
|
<label className="text-[14px] font-semibold leading-[1.5] text-[#6c7682]">
|
||||||
|
문제 제출 여부
|
||||||
|
</label>
|
||||||
|
<div className="relative">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setIsSubmissionDropdownOpen(!isSubmissionDropdownOpen);
|
||||||
|
setIsCourseDropdownOpen(false);
|
||||||
|
setIsCompletionDropdownOpen(false);
|
||||||
|
}}
|
||||||
|
className="bg-white border border-[#dee1e6] rounded-[8px] h-[40px] px-[12px] py-[6px] flex items-center justify-between gap-[8px] w-[190px]"
|
||||||
|
>
|
||||||
|
<span className="text-[15px] font-normal leading-[1.5] text-[#6c7682] flex-1 text-left">
|
||||||
|
{selectedSubmissionStatus === 'all' ? '모든 상태' : selectedSubmissionStatus === 'submitted' ? '제출' : '미제출'}
|
||||||
|
</span>
|
||||||
|
<ArrowDownIcon className="size-[16px] text-[#6c7682] shrink-0" />
|
||||||
|
</button>
|
||||||
|
{isSubmissionDropdownOpen && (
|
||||||
|
<div className="absolute top-full left-0 mt-1 bg-white border border-[#dee1e6] rounded-[8px] shadow-lg z-50 w-[190px]">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedSubmissionStatus('all');
|
||||||
|
setIsSubmissionDropdownOpen(false);
|
||||||
|
}}
|
||||||
|
className="w-full px-[12px] py-[8px] text-left text-[15px] font-normal leading-[1.5] text-[#1b2027] hover:bg-[#f1f3f5]"
|
||||||
|
>
|
||||||
|
모든 상태
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedSubmissionStatus('submitted');
|
||||||
|
setIsSubmissionDropdownOpen(false);
|
||||||
|
}}
|
||||||
|
className="w-full px-[12px] py-[8px] text-left text-[15px] font-normal leading-[1.5] text-[#1b2027] hover:bg-[#f1f3f5]"
|
||||||
|
>
|
||||||
|
제출
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedSubmissionStatus('not-submitted');
|
||||||
|
setIsSubmissionDropdownOpen(false);
|
||||||
|
}}
|
||||||
|
className="w-full px-[12px] py-[8px] text-left text-[15px] font-normal leading-[1.5] text-[#1b2027] hover:bg-[#f1f3f5]"
|
||||||
|
>
|
||||||
|
미제출
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 수료 여부 드롭다운 */}
|
||||||
|
<div className="flex flex-col gap-[4px] relative dropdown-container">
|
||||||
|
<label className="text-[14px] font-semibold leading-[1.5] text-[#6c7682]">
|
||||||
|
수료 여부
|
||||||
|
</label>
|
||||||
|
<div className="relative">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setIsCompletionDropdownOpen(!isCompletionDropdownOpen);
|
||||||
|
setIsCourseDropdownOpen(false);
|
||||||
|
setIsSubmissionDropdownOpen(false);
|
||||||
|
}}
|
||||||
|
className="bg-white border border-[#dee1e6] rounded-[8px] h-[40px] px-[12px] py-[6px] flex items-center justify-between gap-[8px] w-[190px]"
|
||||||
|
>
|
||||||
|
<span className="text-[15px] font-normal leading-[1.5] text-[#6c7682] flex-1 text-left">
|
||||||
|
{selectedCompletionStatus === 'all' ? '모든 상태' : selectedCompletionStatus === 'completed' ? '완료' : '미완료'}
|
||||||
|
</span>
|
||||||
|
<ArrowDownIcon className="size-[16px] text-[#6c7682] shrink-0" />
|
||||||
|
</button>
|
||||||
|
{isCompletionDropdownOpen && (
|
||||||
|
<div className="absolute top-full left-0 mt-1 bg-white border border-[#dee1e6] rounded-[8px] shadow-lg z-50 w-[190px]">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedCompletionStatus('all');
|
||||||
|
setIsCompletionDropdownOpen(false);
|
||||||
|
}}
|
||||||
|
className="w-full px-[12px] py-[8px] text-left text-[15px] font-normal leading-[1.5] text-[#1b2027] hover:bg-[#f1f3f5]"
|
||||||
|
>
|
||||||
|
모든 상태
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedCompletionStatus('completed');
|
||||||
|
setIsCompletionDropdownOpen(false);
|
||||||
|
}}
|
||||||
|
className="w-full px-[12px] py-[8px] text-left text-[15px] font-normal leading-[1.5] text-[#1b2027] hover:bg-[#f1f3f5]"
|
||||||
|
>
|
||||||
|
완료
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedCompletionStatus('not-completed');
|
||||||
|
setIsCompletionDropdownOpen(false);
|
||||||
|
}}
|
||||||
|
className="w-full px-[12px] py-[8px] text-left text-[15px] font-normal leading-[1.5] text-[#1b2027] hover:bg-[#f1f3f5]"
|
||||||
|
>
|
||||||
|
미완료
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 검색바 */}
|
||||||
|
<div className="bg-white border border-[#dee1e6] rounded-[8px] h-[40px] px-[12px] py-[8px] flex items-center gap-[10px] w-[240px]">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="학습자명으로 검색"
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
className="flex-1 text-[16px] font-normal leading-[1.5] text-[#b1b8c0] outline-none placeholder:text-[#b1b8c0]"
|
||||||
|
/>
|
||||||
|
<SearchIcon className="size-[20px] text-[#b1b8c0] shrink-0" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 테이블 영역 */}
|
||||||
|
<div className="border border-[#dee1e6] rounded-[8px] overflow-hidden">
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="min-h-[400px] flex items-center justify-center">
|
||||||
|
<p className="text-[16px] font-medium text-[#333c47]">로딩 중...</p>
|
||||||
|
</div>
|
||||||
|
) : filteredData.length === 0 ? (
|
||||||
|
<div className="min-h-[400px] flex items-center justify-center">
|
||||||
|
<p className="text-[16px] font-medium text-[#333c47]">데이터가 없습니다.</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{/* 테이블 헤더 */}
|
||||||
|
<div className="bg-gray-50 h-[48px] flex items-center border-b border-[#dee1e6]">
|
||||||
|
<div className="flex-1 border-r border-[#dee1e6] px-[16px] py-[12px]">
|
||||||
|
<span className="text-[14px] font-semibold leading-[1.5] text-[#4c5561]">교육 과정명</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 border-r border-[#dee1e6] px-[16px] py-[12px]">
|
||||||
|
<span className="text-[14px] font-semibold leading-[1.5] text-[#4c5561]">강좌명</span>
|
||||||
|
</div>
|
||||||
|
<div className="w-[140px] border-r border-[#dee1e6] px-[16px] py-[12px]">
|
||||||
|
<span className="text-[14px] font-semibold leading-[1.5] text-[#4c5561]">학습자명</span>
|
||||||
|
</div>
|
||||||
|
<div className="w-[140px] border-r border-[#dee1e6] px-[16px] py-[12px]">
|
||||||
|
<span className="text-[14px] font-semibold leading-[1.5] text-[#4c5561]">가입일</span>
|
||||||
|
</div>
|
||||||
|
<div className="w-[140px] border-r border-[#dee1e6] px-[16px] py-[12px]">
|
||||||
|
<span className="text-[14px] font-semibold leading-[1.5] text-[#4c5561]">마지막 수강일</span>
|
||||||
|
</div>
|
||||||
|
<div className="w-[76px] border-r border-[#dee1e6] px-[16px] py-[12px] text-center">
|
||||||
|
<span className="text-[14px] font-semibold leading-[1.5] text-[#4c5561]">진도율</span>
|
||||||
|
</div>
|
||||||
|
<div className="w-[112px] border-r border-[#dee1e6] px-[16px] py-[12px] text-center">
|
||||||
|
<span className="text-[14px] font-semibold leading-[1.5] text-[#4c5561]">문제 제출 여부</span>
|
||||||
|
</div>
|
||||||
|
<div className="w-[84px] border-r border-[#dee1e6] px-[16px] py-[12px] text-center">
|
||||||
|
<span className="text-[14px] font-semibold leading-[1.5] text-[#4c5561]">평가 점수</span>
|
||||||
|
</div>
|
||||||
|
<div className="w-[84px] px-[16px] py-[12px] text-center">
|
||||||
|
<span className="text-[14px] font-semibold leading-[1.5] text-[#4c5561]">수료 여부</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 테이블 바디 */}
|
||||||
|
<div className="bg-white">
|
||||||
|
{paginatedData.map((item) => (
|
||||||
|
<div
|
||||||
|
key={item.id}
|
||||||
|
className="h-[48px] flex items-center border-b border-[#dee1e6] last:border-b-0"
|
||||||
|
>
|
||||||
|
<div className="flex-1 border-r border-[#dee1e6] px-[16px] py-[12px]">
|
||||||
|
<span className="text-[15px] font-medium leading-[1.5] text-[#1b2027]">{item.courseName}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 border-r border-[#dee1e6] px-[16px] py-[12px]">
|
||||||
|
<span className="text-[15px] font-medium leading-[1.5] text-[#1b2027]">{item.lessonName}</span>
|
||||||
|
</div>
|
||||||
|
<div className="w-[140px] border-r border-[#dee1e6] px-[16px] py-[12px]">
|
||||||
|
<span className="text-[15px] font-medium leading-[1.5] text-[#1b2027]">{item.learnerName}</span>
|
||||||
|
</div>
|
||||||
|
<div className="w-[140px] border-r border-[#dee1e6] px-[16px] py-[12px]">
|
||||||
|
<span className="text-[15px] font-medium leading-[1.5] text-[#1b2027]">{item.enrollmentDate}</span>
|
||||||
|
</div>
|
||||||
|
<div className="w-[140px] border-r border-[#dee1e6] px-[16px] py-[12px]">
|
||||||
|
<span className="text-[15px] font-medium leading-[1.5] text-[#1b2027]">{item.lastStudyDate}</span>
|
||||||
|
</div>
|
||||||
|
<div className="w-[76px] border-r border-[#dee1e6] px-[16px] py-[12px] text-center">
|
||||||
|
<span className="text-[15px] font-medium leading-[1.5] text-[#1b2027]">{item.progressRate}%</span>
|
||||||
|
</div>
|
||||||
|
<div className="w-[112px] border-r border-[#dee1e6] px-[16px] py-[12px] text-center">
|
||||||
|
{item.hasSubmitted ? (
|
||||||
|
<StatusTag text="제출" type="default" color="primary" />
|
||||||
|
) : (
|
||||||
|
<StatusTag text="미제출" type="default" color="gray" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="w-[84px] border-r border-[#dee1e6] px-[16px] py-[12px] text-center">
|
||||||
|
<span className="text-[15px] font-medium leading-[1.5] text-[#1b2027]">
|
||||||
|
{item.score !== null ? `${item.score}점` : '-'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="w-[84px] px-[16px] py-[12px] text-center">
|
||||||
|
{item.isCompleted ? (
|
||||||
|
<StatusTag text="완료" type="default" color="primary" />
|
||||||
|
) : (
|
||||||
|
<StatusTag text="미완료" type="default" color="gray" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 페이지네이션 */}
|
||||||
|
{filteredData.length > ITEMS_PER_PAGE && (
|
||||||
|
<div className="flex items-center justify-center gap-[8px] pt-[32px]">
|
||||||
|
{/* First */}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setCurrentPage(1)}
|
||||||
|
disabled={currentPage === 1}
|
||||||
|
className="flex items-center justify-center rounded-full size-[32px] p-[8.615px] text-[#333c47] disabled:opacity-40 cursor-pointer disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
<div className="relative flex items-center justify-center w-full h-full">
|
||||||
|
<ChevronDownSvg width={14.8} height={14.8} className="rotate-90 absolute left-[2px]" />
|
||||||
|
<ChevronDownSvg width={14.8} height={14.8} className="rotate-90 absolute right-[2px]" />
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Prev */}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setCurrentPage((p) => Math.max(1, p - 1))}
|
||||||
|
disabled={currentPage === 1}
|
||||||
|
className="flex items-center justify-center rounded-full size-[32px] p-[8.615px] text-[#333c47] disabled:opacity-40 cursor-pointer disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
<ChevronDownSvg width={14.8} height={14.8} className="rotate-90" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Page Numbers */}
|
||||||
|
{Array.from({ length: Math.min(10, totalPages) }, (_, i) => {
|
||||||
|
const pageNum = i + 1;
|
||||||
|
const isActive = pageNum === currentPage;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={pageNum}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setCurrentPage(pageNum)}
|
||||||
|
className={`flex items-center justify-center rounded-full size-[32px] text-[16px] leading-[1.4] text-[#333c47] cursor-pointer ${
|
||||||
|
isActive ? 'bg-[#ecf0ff]' : 'bg-white'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{pageNum}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
{/* Next */}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setCurrentPage((p) => Math.min(totalPages, p + 1))}
|
||||||
|
disabled={currentPage === totalPages}
|
||||||
|
className="flex items-center justify-center rounded-full size-[32px] p-[8.615px] text-[#333c47] disabled:opacity-40 cursor-pointer disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
<ChevronDownSvg width={14.8} height={14.8} className="-rotate-90" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Last */}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setCurrentPage(totalPages)}
|
||||||
|
disabled={currentPage === totalPages}
|
||||||
|
className="flex items-center justify-center rounded-full size-[32px] p-[8.615px] text-[#333c47] disabled:opacity-40 cursor-pointer disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
<div className="relative flex items-center justify-center w-full h-full">
|
||||||
|
<ChevronDownSvg width={14.8} height={14.8} className="-rotate-90 absolute left-[2px]" />
|
||||||
|
<ChevronDownSvg width={14.8} height={14.8} className="-rotate-90 absolute right-[2px]" />
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -259,7 +259,7 @@ export default function InstructorPage() {
|
|||||||
강좌별 상세 내역
|
강좌별 상세 내역
|
||||||
</h2>
|
</h2>
|
||||||
<Link
|
<Link
|
||||||
href="/admin/courses"
|
href="/instructor/courses"
|
||||||
className="flex items-center gap-[2px] text-[14px] font-medium text-[#6c7682]"
|
className="flex items-center gap-[2px] text-[14px] font-medium text-[#6c7682]"
|
||||||
>
|
>
|
||||||
<span>전체보기</span>
|
<span>전체보기</span>
|
||||||
|
|||||||
Reference in New Issue
Block a user