instructor page
This commit is contained in:
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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user