diff --git a/apps/admin/apis/study/studyApi.ts b/apps/admin/apis/study/studyApi.ts index bbd4d1b8..0ca28ce8 100644 --- a/apps/admin/apis/study/studyApi.ts +++ b/apps/admin/apis/study/studyApi.ts @@ -9,7 +9,8 @@ import type { import type { AttendanceApiResponseDto } from "types/dtos/attendance"; import type { CurriculumApiResponseDto } from "types/dtos/curriculumList"; import type { StudyBasicInfoApiResponseDto } from "types/dtos/studyBasicInfo"; -import type { StudyStudentApiResponseDto } from "types/dtos/studyStudent"; +import type { PaginatedStudyStudentResponseDto } from "types/dtos/studyStudent"; +import type { PageableType } from "types/entities/page"; import type { StudyAnnouncementType } from "types/entities/study"; import type { StudyListApiResponseDto } from "../../types/dtos/studyList"; @@ -149,14 +150,26 @@ export const studyApi = { ); return response.data; }, - getStudyStudents: async (studyId: number) => { - const response = await fetcher.get( + getStudyStudents: async (studyId: number, pageable: PageableType) => { + const response = await fetcher.get( `/mentor/studies/${studyId}/students`, { next: { tags: [tags.students] }, cache: "force-cache", + }, + pageable + ); + return response.data; + }, + getStudyStudentsExcel: async (studyId: number) => { + const response = await fetcher.get( + `/mentor/studies/${studyId}/students/excel`, + { + next: { tags: [tags.studentsExcel] }, + cache: "force-cache", } ); + return response.data; }, }; diff --git a/apps/admin/app/students/_components/StudentFilter.tsx b/apps/admin/app/students/_components/StudentFilter.tsx new file mode 100644 index 00000000..af9a3c2e --- /dev/null +++ b/apps/admin/app/students/_components/StudentFilter.tsx @@ -0,0 +1,13 @@ +import { Flex } from "@styled-system/jsx"; +import Chip from "wowds-ui/Chip"; + +const StudentFilter = () => { + return ( + + + + + ); +}; + +export default StudentFilter; diff --git a/apps/admin/app/students/_components/StudentList.tsx b/apps/admin/app/students/_components/StudentList.tsx deleted file mode 100644 index 652ef925..00000000 --- a/apps/admin/app/students/_components/StudentList.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import { css } from "@styled-system/css"; -import { styled } from "@styled-system/jsx"; -import { Text } from "@wow-class/ui"; -import type { StudyStudentApiResponseDto } from "types/dtos/studyStudent"; - -import StudentListItem from "./StudentListItem"; - -const StudentList = ({ - studentList, -}: { - studentList: StudyStudentApiResponseDto[] | []; -}) => { - if (!studentList.length) return 스터디 수강생이 없어요.; - - return ( - - - - - 이름 - - - 학번 - - - 디스코드 사용자명 - - - 디스코드 닉네임 - - - 깃허브 링크 - - - - - {studentList.map((student) => ( - - ))} - - - ); -}; - -const tableThStyle = css({ - padding: "1rem", - textAlign: "left", -}); - -export default StudentList; diff --git a/apps/admin/app/students/_components/StudentListItem.tsx b/apps/admin/app/students/_components/StudentListItem.tsx deleted file mode 100644 index d4f00256..00000000 --- a/apps/admin/app/students/_components/StudentListItem.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import { css } from "@styled-system/css"; -import { styled } from "@styled-system/jsx"; -import { Text } from "@wow-class/ui"; -import Link from "next/link"; -import type { CSSProperties } from "react"; -import type { StudyStudentApiResponseDto } from "types/dtos/studyStudent"; -import TextButton from "wowds-ui/TextButton"; - -const StudentListItem = ({ - name, - studentId, - discordUsername, - nickname, - githubLink, -}: StudyStudentApiResponseDto) => { - return ( - - - {name} - - - {studentId} - - - {discordUsername} - - - {nickname} - - - - - - ); -}; - -const tableThStyle = css({ - padding: "1rem", -}); - -const textButtonStyle: CSSProperties = { - width: "fit-content", - padding: 0, -}; - -export default StudentListItem; diff --git a/apps/admin/app/students/_components/StudentPagination.tsx b/apps/admin/app/students/_components/StudentPagination.tsx new file mode 100644 index 00000000..5c40cd19 --- /dev/null +++ b/apps/admin/app/students/_components/StudentPagination.tsx @@ -0,0 +1,20 @@ +import type { PaginatedStudyStudentResponseDto } from "types/dtos/studyStudent"; +import Pagination from "wowds-ui/Pagination"; + +const StudentPagination = ({ + pageInfo, + handleClickChangePage, +}: { + pageInfo: Omit | null; + handleClickChangePage: (nextPage: number) => void; +}) => { + if (!pageInfo || !pageInfo.numberOfElements) return null; + return ( + + ); +}; + +export default StudentPagination; diff --git a/apps/admin/app/students/_components/StudentTable/StudentList.tsx b/apps/admin/app/students/_components/StudentTable/StudentList.tsx new file mode 100644 index 00000000..205ab035 --- /dev/null +++ b/apps/admin/app/students/_components/StudentTable/StudentList.tsx @@ -0,0 +1,51 @@ +import { Text } from "@wow-class/ui"; +import type { StudyStudentApiResponseDto } from "types/dtos/studyStudent"; +import Table from "wowds-ui/Table"; + +import StudentListItem from "./StudentListItem"; +import { StudyTasksThs } from "./StudyTasks"; + +const STUENT_INFO_LIST_BEFORE = [ + "수료", + "1차 우수회원", + "2차 우수회원", + "이름", + "학번", + "디스코드 사용자명", + "디스코드 닉네임", + "깃허브 링크", +]; + +const STUDENT_INFO_LIST_AFTER = ["출석률", "과제 수행률", "전체 수행정도"]; + +const StudentList = ({ + studentList, +}: { + studentList: StudyStudentApiResponseDto[] | []; +}) => { + if (!studentList) return null; + if (!studentList.length) return 스터디 수강생이 없어요.; + + return ( + + + {STUENT_INFO_LIST_BEFORE.map((info) => ( + {info} + ))} + {studentList[0] && } + {STUDENT_INFO_LIST_AFTER.map((info) => ( + {info} + ))} + + + {studentList.map((student) => ( + + + + ))} + +
+ ); +}; + +export default StudentList; diff --git a/apps/admin/app/students/_components/StudentTable/StudentListItem.tsx b/apps/admin/app/students/_components/StudentTable/StudentListItem.tsx new file mode 100644 index 00000000..9f058912 --- /dev/null +++ b/apps/admin/app/students/_components/StudentTable/StudentListItem.tsx @@ -0,0 +1,71 @@ +import { AwardIcon, StarCheckIcon, Text } from "@wow-class/ui"; +import Link from "next/link"; +import type { CSSProperties } from "react"; +import type { StudyStudentApiResponseDto } from "types/dtos/studyStudent"; +import { formatNumberToPercent } from "utils/formatNumber"; +import Table from "wowds-ui/Table"; +import TextButton from "wowds-ui/TextButton"; + +import { StudyTasksTds } from "./StudyTasks"; + +const StudentListItem = ({ + studyHistoryStatus, + isFirstRoundOutstandingStudent, + isSecondRoundOutstandingStudent, + name, + studentId, + discordUsername, + nickname, + githubLink, + studyTasks, + assignmentRate, + attendanceRate, +}: StudyStudentApiResponseDto) => { + return ( + <> + + + + + + + 1차 + + + + + + 2차 + + + {name} + {studentId} + {discordUsername} + {nickname} + + + + + {formatNumberToPercent(assignmentRate)} + {formatNumberToPercent(attendanceRate)} + + ); +}; + +const textButtonStyle: CSSProperties = { + width: "fit-content", + padding: 0, +}; + +const awardTextStyle: CSSProperties = { + display: "flex", + gap: "0.25rem", + alignItems: "center", +}; + +export default StudentListItem; diff --git a/apps/admin/app/students/_components/StudentTable/StudyTasks.tsx b/apps/admin/app/students/_components/StudentTable/StudyTasks.tsx new file mode 100644 index 00000000..87b59766 --- /dev/null +++ b/apps/admin/app/students/_components/StudentTable/StudyTasks.tsx @@ -0,0 +1,40 @@ +import type { StudyTaskResponseDto } from "types/dtos/studyStudent"; +import Table from "wowds-ui/Table"; + +import TaskTag from "./TaskTag"; + +export const StudyTasksThs = ({ + tasks, +}: { + tasks: ( + | StudyTaskResponseDto<"ASSIGNMENT"> + | StudyTaskResponseDto<"ATTENDANCE"> + )[]; +}) => { + return tasks.map((task) => { + const { week, taskType } = task; + return ( + + {taskType === "ATTENDANCE" ? `${week}주차 출석` : `${week}주차 과제`} + + ); + }); +}; + +export const StudyTasksTds = ({ + tasks, +}: { + tasks: ( + | StudyTaskResponseDto<"ASSIGNMENT"> + | StudyTaskResponseDto<"ATTENDANCE"> + )[]; +}) => { + return tasks.map((task) => { + const { week, taskType } = task; + return ( + + + + ); + }); +}; diff --git a/apps/admin/app/students/_components/StudentTable/TaskTag.tsx b/apps/admin/app/students/_components/StudentTable/TaskTag.tsx new file mode 100644 index 00000000..2d9425f2 --- /dev/null +++ b/apps/admin/app/students/_components/StudentTable/TaskTag.tsx @@ -0,0 +1,29 @@ +import { assignmentSubmissionStatusMap } from "constants/status/assignmentStatusMap"; +import { attendanceTaskStatusMap } from "constants/status/attendanceStatusMap"; +import type { StudyTaskResponseDto } from "types/dtos/studyStudent"; +import type { TaskType } from "types/entities/task"; +import Tag from "wowds-ui/Tag"; + +const TaskTag = ({ task }: { task: StudyTaskResponseDto }) => { + const formatTaskToTagInfo = () => { + if (task.taskType === "ATTENDANCE") { + return attendanceTaskStatusMap[task.attendanceStatus]; + } + if (task.taskType === "ASSIGNMENT") { + return assignmentSubmissionStatusMap[task.assignmentSubmissionStatus]; + } + return null; + }; + + const tagInfo = formatTaskToTagInfo(); + if (!tagInfo) return null; + const { tagText, tagColor } = tagInfo; + + return ( + + {tagText} + + ); +}; + +export default TaskTag; diff --git a/apps/admin/app/students/_components/StudentsHeader.tsx b/apps/admin/app/students/_components/StudentsHeader.tsx index 3ab763dd..10029b52 100644 --- a/apps/admin/app/students/_components/StudentsHeader.tsx +++ b/apps/admin/app/students/_components/StudentsHeader.tsx @@ -1,20 +1,55 @@ +import { Flex, styled } from "@styled-system/jsx"; import { Text } from "@wow-class/ui"; +import { studyApi } from "apis/study/studyApi"; import ItemSeparator from "components/ItemSeparator"; +import Image from "next/image"; import type { CSSProperties } from "react"; +import { useEffect, useState } from "react"; import type { StudyListApiResponseDto } from "types/dtos/studyList"; import StudyDropDown from "./StudyDropDown"; const StudentsHeader = ({ studyList, + studyId, + studentLength, }: { studyList: StudyListApiResponseDto[]; + studyId: number; + studentLength: number; }) => { + const [url, setUrl] = useState(""); + + useEffect(() => { + const fetchData = async () => { + const response = await studyApi.getStudyStudentsExcel(studyId); + const blob = new Blob([response], { + type: "application/vnd.ms-excel", + }); + const url = URL.createObjectURL(blob); + if (url) setUrl(url); + }; + + if (studentLength) fetchData(); + }, [studyId, studentLength]); + return ( - - 수강생 관리 - - + + + 수강생 관리 + + + {studyId && !!studentLength && ( + + 다운로드 + + )} + ); }; diff --git a/apps/admin/app/students/page.tsx b/apps/admin/app/students/page.tsx index 0d957d08..defba01f 100644 --- a/apps/admin/app/students/page.tsx +++ b/apps/admin/app/students/page.tsx @@ -9,8 +9,10 @@ import { useEffect, useState } from "react"; import type { StudyListApiResponseDto } from "types/dtos/studyList"; import isAdmin from "utils/isAdmin"; -import StudentList from "./_components/StudentList"; +import StudentFilter from "./_components/StudentFilter"; +import StudentPagination from "./_components/StudentPagination"; import StudentsHeader from "./_components/StudentsHeader"; +import StudentList from "./_components/StudentTable/StudentList"; import { studyAtom } from "./_contexts/StudyProvider"; const StudentsPage = () => { @@ -33,13 +35,31 @@ const StudentsPage = () => { fetchData(); }, [setSelectedStudy]); - const student = useFetchStudents(selectedStudy); + const [page, setPage] = useState(1); + const handleClickChangePage = (nextPage: number) => { + setPage(nextPage); + }; + + const { studentList, pageInfo } = useFetchStudents(selectedStudy, page); + if (!selectedStudy) return null; if (!studyList) return 담당한 스터디가 없어요.; return ( - - - + + + + {/* TODO: 페이지네이션 API 필터 추가 후 주석 해제 + {studentList.length ? : null} + */} + + ); }; diff --git a/apps/admin/app/studies/[studyId]/_components/assignment/AssignmentButtons.tsx b/apps/admin/app/studies/[studyId]/_components/assignment/AssignmentButtons.tsx index 166b45b6..5367eb4e 100644 --- a/apps/admin/app/studies/[studyId]/_components/assignment/AssignmentButtons.tsx +++ b/apps/admin/app/studies/[studyId]/_components/assignment/AssignmentButtons.tsx @@ -39,7 +39,7 @@ const AssignmentButtons = ({ ); } - if (assignmentStatus === "CANCELLED") { + if (assignmentStatus === "CANCELED") { return (