diff --git a/apps/admin/apis/study/studyApi.ts b/apps/admin/apis/study/studyApi.ts index 7105f882..bbd4d1b8 100644 --- a/apps/admin/apis/study/studyApi.ts +++ b/apps/admin/apis/study/studyApi.ts @@ -9,6 +9,7 @@ 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 { StudyAnnouncementType } from "types/entities/study"; import type { StudyListApiResponseDto } from "../../types/dtos/studyList"; @@ -148,4 +149,14 @@ export const studyApi = { ); return response.data; }, + getStudyStudents: async (studyId: number) => { + const response = await fetcher.get( + `/mentor/studies/${studyId}/students`, + { + next: { tags: [tags.students] }, + cache: "force-cache", + } + ); + return response.data; + }, }; diff --git a/apps/admin/app/@modal/(.)participants/page.tsx b/apps/admin/app/@modal/(.)participants/page.tsx deleted file mode 100644 index b90748d3..00000000 --- a/apps/admin/app/@modal/(.)participants/page.tsx +++ /dev/null @@ -1,23 +0,0 @@ -"use client"; - -import { Flex } from "@styled-system/jsx"; -import { Modal } from "@wow-class/ui"; -import { useModalRoute } from "@wow-class/ui/hooks"; -import Button from "wowds-ui/Button"; - -const TestModal = () => { - const { onClose } = useModalRoute(); - - return ( - - - - - - - ); -}; - -export default TestModal; diff --git a/apps/admin/app/@modal/default.tsx b/apps/admin/app/@modal/default.tsx deleted file mode 100644 index 395785b9..00000000 --- a/apps/admin/app/@modal/default.tsx +++ /dev/null @@ -1,5 +0,0 @@ -const Default = () => { - return null; -}; - -export default Default; diff --git a/apps/admin/app/layout.tsx b/apps/admin/app/layout.tsx index 7b198a7b..958856e0 100644 --- a/apps/admin/app/layout.tsx +++ b/apps/admin/app/layout.tsx @@ -56,6 +56,12 @@ const RootLayout = ({ limit={1} /> + {children} {modal} diff --git a/apps/admin/app/participants/page.tsx b/apps/admin/app/participants/page.tsx deleted file mode 100644 index 19a3c50b..00000000 --- a/apps/admin/app/participants/page.tsx +++ /dev/null @@ -1,5 +0,0 @@ -const Participants = () => { - return
Participants
; -}; - -export default Participants; diff --git a/apps/admin/app/students/_components/StudentList.tsx b/apps/admin/app/students/_components/StudentList.tsx new file mode 100644 index 00000000..652ef925 --- /dev/null +++ b/apps/admin/app/students/_components/StudentList.tsx @@ -0,0 +1,50 @@ +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 new file mode 100644 index 00000000..d4f00256 --- /dev/null +++ b/apps/admin/app/students/_components/StudentListItem.tsx @@ -0,0 +1,51 @@ +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/StudentsHeader.tsx b/apps/admin/app/students/_components/StudentsHeader.tsx new file mode 100644 index 00000000..3ab763dd --- /dev/null +++ b/apps/admin/app/students/_components/StudentsHeader.tsx @@ -0,0 +1,28 @@ +import { Text } from "@wow-class/ui"; +import ItemSeparator from "components/ItemSeparator"; +import type { CSSProperties } from "react"; +import type { StudyListApiResponseDto } from "types/dtos/studyList"; + +import StudyDropDown from "./StudyDropDown"; + +const StudentsHeader = ({ + studyList, +}: { + studyList: StudyListApiResponseDto[]; +}) => { + return ( + + 수강생 관리 + + + ); +}; + +const titleStyle: CSSProperties = { + display: "flex", + alignItems: "center", + gap: "0.75rem", + whiteSpace: "nowrap", +}; + +export default StudentsHeader; diff --git a/apps/admin/app/students/_components/StudyDropDown/DropDownTrigger.tsx b/apps/admin/app/students/_components/StudyDropDown/DropDownTrigger.tsx new file mode 100644 index 00000000..89b8013b --- /dev/null +++ b/apps/admin/app/students/_components/StudyDropDown/DropDownTrigger.tsx @@ -0,0 +1,19 @@ +import { Flex } from "@styled-system/jsx"; +import { DownArrow } from "wowds-icons"; + +const DropDownTrigger = () => { + return ( + + + + ); +}; + +export default DropDownTrigger; diff --git a/apps/admin/app/students/_components/StudyDropDown/index.tsx b/apps/admin/app/students/_components/StudyDropDown/index.tsx new file mode 100644 index 00000000..d19a55a9 --- /dev/null +++ b/apps/admin/app/students/_components/StudyDropDown/index.tsx @@ -0,0 +1,53 @@ +import { Flex } from "@styled-system/jsx"; +import { useAtom } from "jotai"; +import type { ReactNode } from "react"; +import type { StudyListApiResponseDto } from "types/dtos/studyList"; +import DropDown from "wowds-ui/DropDown"; +import DropDownOption from "wowds-ui/DropDownOption"; + +import { studyAtom } from "../../_contexts/StudyProvider"; +import DropDownTrigger from "./DropDownTrigger"; + +const StudyDropDown = ({ + studyList, +}: { + studyList: StudyListApiResponseDto[]; +}) => { + const [study, setStudy] = useAtom(studyAtom); + + if (!study) return null; + + return ( + + {study.title} + + + } + onChange={(value: { selectedValue: string; selectedText: ReactNode }) => { + setStudy({ + studyId: +value.selectedValue, + title: value.selectedText, + }); + }} + > + {studyList.map((study: StudyListApiResponseDto) => ( + + ))} + + ); +}; + +export default StudyDropDown; diff --git a/apps/admin/app/students/_contexts/StudyProvider.tsx b/apps/admin/app/students/_contexts/StudyProvider.tsx new file mode 100644 index 00000000..484a8224 --- /dev/null +++ b/apps/admin/app/students/_contexts/StudyProvider.tsx @@ -0,0 +1,18 @@ +"use client"; + +import { atom, createStore, Provider } from "jotai"; +import type { PropsWithChildren, ReactNode } from "react"; + +const studyIdStore = createStore(); + +export type StudyAtomprops = { + studyId: number; + title: ReactNode; +}; + +export const studyAtom = atom(); +studyIdStore.set(studyAtom, undefined); + +export const StudyProvider = ({ children }: PropsWithChildren) => { + return {children}; +}; diff --git a/apps/admin/app/students/layout.tsx b/apps/admin/app/students/layout.tsx new file mode 100644 index 00000000..5b02ec4c --- /dev/null +++ b/apps/admin/app/students/layout.tsx @@ -0,0 +1,30 @@ +import { css } from "@styled-system/css"; +import { styled } from "@styled-system/jsx"; +import Navbar from "components/Navbar"; + +import { StudyProvider } from "./_contexts/StudyProvider"; + +const StudentsLayout = ({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) => { + return ( + + + {children} + + ); +}; + +export default StudentsLayout; + +const StudentsLayoutStyle = css({ + display: "flex", + flexDirection: "column", + gap: "sm", + height: "100vh", + overflow: "scroll", + width: "100%", + padding: "54px 101px", +}); diff --git a/apps/admin/app/students/page.tsx b/apps/admin/app/students/page.tsx new file mode 100644 index 00000000..7d6c5cbc --- /dev/null +++ b/apps/admin/app/students/page.tsx @@ -0,0 +1,49 @@ +"use client"; + +import { Flex } from "@styled-system/jsx"; +import { Text } from "@wow-class/ui"; +import { studyApi } from "apis/study/studyApi"; +import useFetchStudents from "hooks/fetch/useFetchStudents"; +import { useAtom } from "jotai"; +import { useEffect, useState } from "react"; +import type { StudyListApiResponseDto } from "types/dtos/studyList"; +import isAdmin from "utils/isAdmin"; + +import StudentList from "./_components/StudentList"; +import StudentsHeader from "./_components/StudentsHeader"; +import { studyAtom } from "./_contexts/StudyProvider"; + +const StudentsPage = () => { + const [studyList, setStudyList] = useState(); + const [selectedStudy, setSelectedStudy] = useAtom(studyAtom); + + useEffect(() => { + const fetchData = async () => { + const adminStatus = await isAdmin(); + if (adminStatus) { + const data = adminStatus + ? await studyApi.getStudyList() + : await studyApi.getMyStudyList(); + + if (data && data.length && data[0]) { + setStudyList(data); + setSelectedStudy({ studyId: data[0].studyId, title: data[0].title }); + } + } + }; + + fetchData(); + }, [setSelectedStudy]); + + const student = useFetchStudents(selectedStudy); + if (!studyList) return 담당한 스터디가 없어요.; + + return ( + + + + + ); +}; + +export default StudentsPage; diff --git a/apps/admin/components/Navbar.tsx b/apps/admin/components/Navbar.tsx index c69807d2..25c77cec 100644 --- a/apps/admin/components/Navbar.tsx +++ b/apps/admin/components/Navbar.tsx @@ -37,12 +37,12 @@ const Navbar = async () => { }; }), }, - // { - // href: "participants", - // imageUrl: participantImageUrl, - // alt: "participant-icon", - // name: "수강생 관리", - // }, + { + href: "/students", + imageUrl: participantImageUrl, + alt: "participant-icon", + name: "수강생 관리", + }, ]; return ( diff --git a/apps/admin/constants/tags.ts b/apps/admin/constants/tags.ts index 268bf0de..73d8eb94 100644 --- a/apps/admin/constants/tags.ts +++ b/apps/admin/constants/tags.ts @@ -8,4 +8,5 @@ export const enum tags { announcements = "announcements", memberList = "memberList", attendances = "attendances", + students = "students", } diff --git a/apps/admin/hooks/fetch/useFetchStudents.ts b/apps/admin/hooks/fetch/useFetchStudents.ts new file mode 100644 index 00000000..c35eecdb --- /dev/null +++ b/apps/admin/hooks/fetch/useFetchStudents.ts @@ -0,0 +1,28 @@ +import { studyApi } from "apis/study/studyApi"; +import { useEffect, useState } from "react"; +import type { StudyStudentApiResponseDto } from "types/dtos/studyStudent"; + +import type { StudyAtomprops } from "@/students/_contexts/StudyProvider"; + +const useFetchStudents = ( + study: StudyAtomprops | undefined +): { studentList: StudyStudentApiResponseDto[] | [] } => { + const [studentList, setStudentList] = useState< + StudyStudentApiResponseDto[] | [] + >([]); + + useEffect(() => { + const fetchStudentsData = async () => { + if (study) { + const studentsData = await studyApi.getStudyStudents(study.studyId); + if (studentsData) setStudentList(studentsData); + } + }; + + fetchStudentsData(); + }, [study]); + + return { studentList }; +}; + +export default useFetchStudents; diff --git a/apps/admin/middleware.ts b/apps/admin/middleware.ts index f32cfe32..ba9b1a28 100644 --- a/apps/admin/middleware.ts +++ b/apps/admin/middleware.ts @@ -5,7 +5,7 @@ import { cookies } from "next/headers"; import { NextResponse } from "next/server"; import setExpireTime from "utils/setExpireTime"; export const config = { - matcher: ["/studies/:path*", "/participants/:path*"], + matcher: ["/studies/:path*", "/students/:path*"], }; const middleware = async () => { diff --git a/apps/admin/types/dtos/studyStudent.ts b/apps/admin/types/dtos/studyStudent.ts new file mode 100644 index 00000000..f9867939 --- /dev/null +++ b/apps/admin/types/dtos/studyStudent.ts @@ -0,0 +1,8 @@ +export interface StudyStudentApiResponseDto { + memberId: number; + name: string; + studentId: string; + discordUsername: string; + nickname: string; + githubLink: string; +} diff --git a/packages/utils/src/fetcher/index.ts b/packages/utils/src/fetcher/index.ts index 00e69839..451775a3 100644 --- a/packages/utils/src/fetcher/index.ts +++ b/packages/utils/src/fetcher/index.ts @@ -106,7 +106,6 @@ class Fetcher { let response: ApiResponse = await fetch(fullUrl, fetchOptions); const data = await this.parseJsonResponse(response); - await this.handleError(response, data); response = await this.interceptResponse(response);