diff --git a/frontend/src/api/dashboard.ts b/frontend/src/api/dashboard.ts index fe896c9ba..a348b3ddc 100644 --- a/frontend/src/api/dashboard.ts +++ b/frontend/src/api/dashboard.ts @@ -4,6 +4,30 @@ import { convertParamsToQueryString } from './utils'; import ApiError from './ApiError'; const dashboardApis = { + get: async ({ dashboardId }: { dashboardId: string }) => { + const queryParams = { + clubId: dashboardId, + }; + + const response = await fetch(`${DASHBOARDS}?${convertParamsToQueryString(queryParams)}`, { + headers: { + Accept: 'application/json', + }, + }); + + if (!response.ok) { + throw new ApiError({ + method: 'GET', + statusCode: response.status, + message: '공고 리스트의 정보를 불러오지 못했습니다.', + }); + } + + const data = await response.json(); + + return data; + }, + create: async ({ clubId, dashboardFormInfo }: { clubId: number; dashboardFormInfo: DashboardFormInfo }) => { const queryParams = { clubId: String(clubId), diff --git a/frontend/src/api/process.ts b/frontend/src/api/process.ts index 83862ac9f..b064da0cc 100644 --- a/frontend/src/api/process.ts +++ b/frontend/src/api/process.ts @@ -3,8 +3,8 @@ import { createParams } from './utils'; import ApiError from './ApiError'; const processApis = { - get: async ({ id }: { id: number }) => { - const response = await fetch(`${PROCESSES}?${createParams({ dashboard_id: String(id) })}`, { + get: async ({ id }: { id: string }) => { + const response = await fetch(`${PROCESSES}?${createParams({ dashboard_id: id })}`, { headers: { Accept: 'application/json', }, diff --git a/frontend/src/components/appModal/ApplicantBaseInfo/index.tsx b/frontend/src/components/appModal/ApplicantBaseInfo/index.tsx index 90d4d160a..c0d830382 100644 --- a/frontend/src/components/appModal/ApplicantBaseInfo/index.tsx +++ b/frontend/src/components/appModal/ApplicantBaseInfo/index.tsx @@ -1,3 +1,5 @@ +import { useParams } from 'react-router-dom'; + import Dropdown from '@components/common/Dropdown'; import Button from '@components/common/Button'; import useProcess from '@hooks/useProcess'; @@ -14,7 +16,8 @@ interface ApplicantBaseInfoProps { export default function ApplicantBaseInfo({ applicantId }: ApplicantBaseInfoProps) { const { data: applicantBaseDetail } = specificApplicant.useGetBaseInfo({ applicantId }); const { mutate: rejectMutate } = specificApplicant.useRejectApplicant(); - const { processList } = useProcess(); + const { dashboardId, postId } = useParams() as { dashboardId: string; postId: string }; + const { processList } = useProcess({ dashboardId, postId }); const { moveApplicantProcess } = useApplicant({ applicantId }); const { close } = useModal(); diff --git a/frontend/src/components/dashboard/ProcessColumn/index.tsx b/frontend/src/components/dashboard/ProcessColumn/index.tsx index 5d64d4806..321ec1170 100644 --- a/frontend/src/components/dashboard/ProcessColumn/index.tsx +++ b/frontend/src/components/dashboard/ProcessColumn/index.tsx @@ -1,3 +1,5 @@ +import { useParams } from 'react-router-dom'; + import { useSpecificApplicantId } from '@contexts/SpecificApplicnatIdContext'; import { Process } from '@customTypes/process'; import useProcess from '@hooks/useProcess'; @@ -12,7 +14,8 @@ interface ProcessColumnProps { } export default function ProcessColumn({ process }: ProcessColumnProps) { - const { processList } = useProcess(); + const { dashboardId, postId } = useParams() as { dashboardId: string; postId: string }; + const { processList } = useProcess({ dashboardId, postId }); const { moveApplicantProcess } = useApplicant({}); const { setApplicantId } = useSpecificApplicantId(); const { open } = useModal(); diff --git a/frontend/src/hooks/useGetDashboards/index.tsx b/frontend/src/hooks/useGetDashboards/index.tsx new file mode 100644 index 000000000..1e72dae45 --- /dev/null +++ b/frontend/src/hooks/useGetDashboards/index.tsx @@ -0,0 +1,17 @@ +import dashboardApis from '@api/dashboard'; +import type { Club } from '@customTypes/dashboard'; +import QUERY_KEYS from '@hooks/queryKeys'; +import { useQuery } from '@tanstack/react-query'; + +interface UseGetDashboardsProps { + dashboardId: string; +} + +export default function useGetDashboards({ dashboardId }: UseGetDashboardsProps) { + const { data, error, isLoading } = useQuery({ + queryKey: [QUERY_KEYS.DASHBOARD, dashboardId], + queryFn: () => dashboardApis.get({ dashboardId }), + }); + + return { data, error, isLoading }; +} diff --git a/frontend/src/hooks/useProcess/index.ts b/frontend/src/hooks/useProcess/index.ts index d522d2aad..ea66903e4 100644 --- a/frontend/src/hooks/useProcess/index.ts +++ b/frontend/src/hooks/useProcess/index.ts @@ -3,7 +3,6 @@ import { useQuery } from '@tanstack/react-query'; import type { Process } from '@customTypes/process'; import processApis from '@api/process'; -import { DASHBOARD_ID } from '@constants/constants'; import QUERY_KEYS from '@hooks/queryKeys'; interface SimpleProcess { @@ -11,6 +10,11 @@ interface SimpleProcess { processId: number; } +interface UseProcessProps { + dashboardId: string; + postId: string; +} + interface UseProcessReturn { processes: Process[]; processList: SimpleProcess[]; @@ -18,10 +22,10 @@ interface UseProcessReturn { isLoading: boolean; } -export default function useProcess(): UseProcessReturn { +export default function useProcess({ dashboardId, postId }: UseProcessProps): UseProcessReturn { const { data, error, isLoading } = useQuery<{ processes: Process[] }>({ - queryKey: [QUERY_KEYS.DASHBOARD, DASHBOARD_ID], - queryFn: () => processApis.get({ id: DASHBOARD_ID }), + queryKey: [QUERY_KEYS.DASHBOARD, dashboardId, postId], + queryFn: () => processApis.get({ id: postId }), }); const processes = data?.processes || []; diff --git a/frontend/src/hooks/useProcess/useProcess.test.tsx b/frontend/src/hooks/useProcess/useProcess.test.tsx index 899580988..4ea4502c8 100644 --- a/frontend/src/hooks/useProcess/useProcess.test.tsx +++ b/frontend/src/hooks/useProcess/useProcess.test.tsx @@ -11,7 +11,7 @@ const wrapper = ({ children }: PropsWithChildren) => ( describe('useProcess', () => { it('should return processes and processNameList when data is loaded', async () => { - const { result } = renderHook(() => useProcess(), { wrapper }); + const { result } = renderHook(() => useProcess({ dashboardId: '1', postId: '1' }), { wrapper }); await waitFor(() => expect(result.current.isLoading).toBe(false)); diff --git a/frontend/src/mocks/dashboardList.json b/frontend/src/mocks/dashboardList.json new file mode 100644 index 000000000..325c3c939 --- /dev/null +++ b/frontend/src/mocks/dashboardList.json @@ -0,0 +1,44 @@ +{ + "clubName": "크루루", + "dashboards": [ + { + "dashboardId": "1", + "title": "프론트엔드 7기 모집", + "stats": { + "accept": 3, + "fail": 3, + "inProgress": 9, + "total": 15 + }, + "postUrl": "https://www.cruru.kr/123543920/recruit", + "startDate": "1900-01-21T00:00:00", + "endDate": "2024-07-24T18:00:00" + }, + { + "dashboardId": "2", + "title": "백엔드 7기 모집", + "stats": { + "accept": 3, + "fail": 3, + "inProgress": 9, + "total": 15 + }, + "postUrl": "https://www.cruru.kr/98765101/recruit", + "startDate": "1900-01-21T00:00:00", + "endDate": "2024-07-24T20:30:00" + }, + { + "dashboardId": "3", + "title": "안드로이드 7기 모집", + "stats": { + "accept": 3, + "fail": 3, + "inProgress": 9, + "total": 15 + }, + "postUrl": "https://www.cruru.kr/7777890/recruit", + "startDate": "1900-01-21T00:00:00", + "endDate": "2024-07-24T21:00:00" + } + ] +} diff --git a/frontend/src/mocks/handlers/dashboardHandlers.ts b/frontend/src/mocks/handlers/dashboardHandlers.ts index 045ef74a4..6224383f3 100644 --- a/frontend/src/mocks/handlers/dashboardHandlers.ts +++ b/frontend/src/mocks/handlers/dashboardHandlers.ts @@ -1,7 +1,8 @@ -import { http } from 'msw'; +import { http, HttpResponse } from 'msw'; import { DASHBOARDS } from '@api/endPoint'; import { DashboardFormInfo } from '@customTypes/dashboard'; +import DASHBOARD_LIST from '../dashboardList.json'; const dashboardHandlers = [ http.post(DASHBOARDS, async ({ request }) => { @@ -21,6 +22,20 @@ const dashboardHandlers = [ statusText: 'Created', }); }), + + http.get(DASHBOARDS, ({ request }) => { + const url = new URL(request.url); + const clubId = url.searchParams.get('clubId'); + + if (!clubId) { + return new Response(null, { + status: 400, + statusText: 'The request Param is missing required information.', + }); + } + + return HttpResponse.json(DASHBOARD_LIST); + }), ]; export default dashboardHandlers; diff --git a/frontend/src/pages/DashBoardList/DashboardList.stories.tsx b/frontend/src/pages/DashBoardList/DashboardList.stories.tsx new file mode 100644 index 000000000..c838d49fb --- /dev/null +++ b/frontend/src/pages/DashBoardList/DashboardList.stories.tsx @@ -0,0 +1,36 @@ +import { reactRouterParameters } from 'storybook-addon-remix-react-router'; +import type { Meta, StoryObj } from '@storybook/react'; +import DashboardList from '.'; + +const meta: Meta = { + title: 'Components/DashboardList', + component: DashboardList, + parameters: { + layout: 'centered', + docs: { + description: { + component: 'DashboardList 컴포넌트는 여러 대시보드를 리스트 형태로 보여줍니다.', + }, + }, + reactRouter: reactRouterParameters({ + location: { + pathParams: { dashboardId: '1' }, + }, + routing: { path: '/dashboardId/:dashboardId' }, + }), + }, + tags: ['autodocs'], +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + decorators: [ + (Child) => ( +
+ +
+ ), + ], +}; diff --git a/frontend/src/pages/DashBoardList/index.tsx b/frontend/src/pages/DashBoardList/index.tsx index 668b25943..b13282193 100644 --- a/frontend/src/pages/DashBoardList/index.tsx +++ b/frontend/src/pages/DashBoardList/index.tsx @@ -1,10 +1,38 @@ +import RecruitmentCard from '@components/recruitment/RecruitmentCard'; +import { useNavigate, useParams } from 'react-router-dom'; +import useGetDashboards from '@hooks/useGetDashboards'; +import S from './style'; + export default function DashboardList() { + const { dashboardId } = useParams() as { dashboardId: string }; + const { data } = useGetDashboards({ dashboardId }); + const navigate = useNavigate(); + + const handleCardClick = (postId: number) => { + navigate(`/dashboard/${dashboardId}/${postId}`); + }; + return ( -
- List - {/* - TODO: 여기에 PostList를 넣어주세요!!!!!! - */} -
+ + {data?.clubName} + + {data?.dashboards.map((dashboard) => ( + postId로 변경 + dashboardId={Number(dashboard.dashboardId)} + title={dashboard.title} + postStats={dashboard.stats} + startDate={dashboard.startDate} + endDate={dashboard.endDate} + onClick={handleCardClick} + /> + ))} + navigate(`/dashboard/${dashboardId}/create`)}> +
+
+ 새 공고 추가 +
+
+
); } diff --git a/frontend/src/pages/DashBoardList/style.ts b/frontend/src/pages/DashBoardList/style.ts new file mode 100644 index 000000000..1b01a7d72 --- /dev/null +++ b/frontend/src/pages/DashBoardList/style.ts @@ -0,0 +1,58 @@ +import styled from '@emotion/styled'; + +const Container = styled.div` + display: flex; + flex-direction: column; + padding: 3.8rem 2.4rem; + gap: 2.4rem; +`; + +const Title = styled.h1` + ${({ theme }) => theme.typography.heading[700]} + padding-bottom: 2.4rem; + border-bottom: 1px solid ${({ theme }) => theme.baseColors.grayscale[300]}; +`; + +const CardGrid = styled.div` + display: grid; + grid-template-columns: repeat(auto-fill, 37.8rem); + gap: 2.4rem; +`; + +const AddCard = styled.div` + display: flex; + height: 20rem; + + flex-direction: column; + align-items: center; + justify-content: center; + + border: 0.2rem dashed ${({ theme }) => theme.baseColors.grayscale[600]}; + border-radius: 0.5rem; + padding: 2rem; + + cursor: pointer; + text-align: center; + + &:hover { + background-color: ${({ theme }) => theme.baseColors.grayscale[200]}; + } + + div { + font-size: 6rem; + color: ${({ theme }) => theme.colors.brand.primary}; + } + span { + ${({ theme }) => theme.typography.common.large} + color: ${({ theme }) => theme.baseColors.grayscale[800]}; + } +`; + +const S = { + Container, + Title, + CardGrid, + AddCard, +}; + +export default S; diff --git a/frontend/src/pages/Dashboard/index.tsx b/frontend/src/pages/Dashboard/index.tsx index 89bc16be6..3e2f24638 100644 --- a/frontend/src/pages/Dashboard/index.tsx +++ b/frontend/src/pages/Dashboard/index.tsx @@ -1,6 +1,8 @@ +import { useParams } from 'react-router-dom'; + +import ProcessManageBoard from '@components/processManagement/ProcessManageBoard'; import Tab from '@components/common/Tab'; import KanbanBoard from '@components/dashboard/KanbanBoard'; -import ProcessManageBoard from '@components/processManagement/ProcessManageBoard'; import useTab from '@components/common/Tab/useTab'; import useProcess from '@hooks/useProcess'; @@ -13,7 +15,8 @@ import S from './style'; export type DashboardTabItems = '지원자 관리' | '모집 과정 관리'; export default function Dashboard() { - const { processes } = useProcess(); + const { dashboardId, postId } = useParams() as { dashboardId: string; postId: string }; + const { processes } = useProcess({ dashboardId, postId }); const { currentMenu, moveTab } = useTab({ defaultValue: '지원자 관리' }); diff --git a/frontend/src/pages/DashboardLayout/index.tsx b/frontend/src/pages/DashboardLayout/index.tsx index 149689654..06fe91029 100644 --- a/frontend/src/pages/DashboardLayout/index.tsx +++ b/frontend/src/pages/DashboardLayout/index.tsx @@ -1,14 +1,25 @@ +import useGetDashboards from '@hooks/useGetDashboards'; import DashboardSidebar from '@components/dashboard/DashboardSidebar'; -import { Outlet } from 'react-router-dom'; +import { Outlet, useParams } from 'react-router-dom'; import S from './style'; export default function DashboardLayout() { - const options = [{ text: '프론트엔드 7기 모집', isSelected: true, postId: 1 }]; + const { dashboardId, postId } = useParams() as { dashboardId: string; postId: string }; + const { data, isLoading } = useGetDashboards({ dashboardId }); + + if (isLoading) return
Loading...
; + if (!data) return
something wrong
; + + const titleList = data.dashboards.map(({ title, dashboardId: postId2 }) => ({ + text: title, + isSelected: !!postId && postId === postId2, + postId: Number(postId2), + })); return ( - + diff --git a/frontend/src/types/dashboard.ts b/frontend/src/types/dashboard.ts index b24a70729..e3cf5070f 100644 --- a/frontend/src/types/dashboard.ts +++ b/frontend/src/types/dashboard.ts @@ -23,3 +23,24 @@ export interface QuestionOption { choice: string; orderIndex: number; } + +interface Stats { + accept: number; + fail: number; + inProgress: number; + total: number; +} + +interface Dashboard { + dashboardId: string; + title: string; + stats: Stats; + postUrl: string; + startDate: string; + endDate: string; +} + +export interface Club { + clubName: string; + dashboards: Dashboard[]; +}