diff --git a/src/app/mocks/repositories/applicant.ts b/src/app/mocks/repositories/applicant.ts index ce318d71..090699eb 100644 --- a/src/app/mocks/repositories/applicant.ts +++ b/src/app/mocks/repositories/applicant.ts @@ -1,7 +1,7 @@ import type { DetailApplication } from '@/pages/admin/ApplicationDetail/types/detailApplication'; import type { ApplicantData } from '@/pages/admin/Dashboard/types/dashboard'; -const applicants: ApplicantData[] = [ +const applicants: Omit[] = [ { applicantId: 1, name: '김동글', diff --git a/src/app/mocks/repositories/dashboard.ts b/src/app/mocks/repositories/dashboard.ts index ed89c62a..899fa09e 100644 --- a/src/app/mocks/repositories/dashboard.ts +++ b/src/app/mocks/repositories/dashboard.ts @@ -47,7 +47,6 @@ const MOCK_APPLICANTS: ApplicantData[] = [ availableTimes: ['14:00', '15:00', '16:00'], }, ], - interviewSchedule: MOCK_INTERVIEW_SCHEDULE, }, { applicantId: 2, @@ -57,14 +56,13 @@ const MOCK_APPLICANTS: ApplicantData[] = [ phoneNumber: '010-2345-6789', email: 'test2@example.com', status: 'APPROVED', - confirmedTime: undefined, + confirmedTime: null, interviewInfo: [ { interviewDate: '2026-09-02', availableTimes: ['10:00', '11:00'], }, ], - interviewSchedule: MOCK_INTERVIEW_SCHEDULE, }, { applicantId: 3, @@ -91,6 +89,7 @@ export const dashboardRepository = { getApplicants: (): ApplicantsApiResponse => { return { + interviewRequired: true, applicants: MOCK_APPLICANTS, interviewSchedule: MOCK_INTERVIEW_SCHEDULE, message: null, diff --git a/src/pages/admin/Dashboard/components/ApplicantListSection/List/ApplicantList/index.styled.ts b/src/pages/admin/Dashboard/components/ApplicantListSection/List/ApplicantList/index.styled.ts index 8b3196c6..3bb332b1 100644 --- a/src/pages/admin/Dashboard/components/ApplicantListSection/List/ApplicantList/index.styled.ts +++ b/src/pages/admin/Dashboard/components/ApplicantListSection/List/ApplicantList/index.styled.ts @@ -4,9 +4,10 @@ export const Container = styled.div({ width: '100%', }); -export const ApplicantInfoCategoryList = styled.div` +export const ApplicantInfoCategoryList = styled.div<{ hasInterview: boolean }>` display: grid; - grid-template-columns: 1fr 1fr 1fr 1.2fr 1.5fr 1fr 1.2fr; + grid-template-columns: ${({ hasInterview }) => + '1fr 1fr 1fr 1.2fr 1.5fr 1fr' + (hasInterview ? '1.2fr' : '')}; background-color: #f9fbfc; border-bottom: 1.8px solid ${({ theme }) => theme.colors.gray100}; padding: 1.7rem 0 1.5rem 0; @@ -16,7 +17,8 @@ export const ApplicantInfoCategoryList = styled.div` } @media (max-width: 1200px) { - grid-template-columns: 1fr 1fr 1fr 1.5fr 1fr 1.2fr; + grid-template-columns: ${({ hasInterview }) => + '1fr 1fr 1fr 1.5fr 1fr' + (hasInterview ? '1.2fr' : '')}; & > div:nth-of-type(4) { display: none; @@ -24,7 +26,8 @@ export const ApplicantInfoCategoryList = styled.div` } @media (max-width: ${({ theme }) => theme.breakpoints.web}) { - grid-template-columns: 1fr 1fr 1.5fr 1fr 1.2fr; + grid-template-columns: ${({ hasInterview }) => + '1fr 1fr 1.5fr 1fr' + (hasInterview ? '1.2fr' : '')} & > div:nth-of-type(3) { display: none; @@ -32,7 +35,7 @@ export const ApplicantInfoCategoryList = styled.div` } @media (max-width: 768px) { - grid-template-columns: 1fr 1.5fr 1fr 1.2fr; + grid-template-columns: ${({ hasInterview }) => '1fr 1.5fr 1fr' + (hasInterview ? '1.2fr' : '')}; & > div:nth-of-type(2) { display: none; diff --git a/src/pages/admin/Dashboard/components/ApplicantListSection/List/ApplicantList/index.tsx b/src/pages/admin/Dashboard/components/ApplicantListSection/List/ApplicantList/index.tsx index 7cfea9f1..94457353 100644 --- a/src/pages/admin/Dashboard/components/ApplicantListSection/List/ApplicantList/index.tsx +++ b/src/pages/admin/Dashboard/components/ApplicantListSection/List/ApplicantList/index.tsx @@ -1,4 +1,4 @@ -import { useCallback } from 'react'; +import { useCallback, useMemo } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; import { useApplicants } from '@/pages/admin/Dashboard/hooks/useApplicants'; import { STAGE_LABEL } from '@/pages/admin/Dashboard/utils/labelMap'; @@ -16,19 +16,8 @@ type Props = { stage: ApplicationStage; }; -const INFO_CATEGORY: ApplicateInfoCategory[] = [ - '이름', - '학번', - '학과', - '전화번호', - '이메일', - '결과', - '면접 시간', -]; - export const ApplicantList = ({ filterOption, stage }: Props) => { const { clubId } = useParams(); - const navigate = useNavigate(); const apiStage = STAGE_LABEL[stage]; @@ -37,8 +26,15 @@ export const ApplicantList = ({ filterOption, stage }: Props) => { data: applicants, isLoading, error, + interviewSchedule, + interviewRequired, } = useApplicants(Number(clubId), apiStage, filterOption); + const categories = useMemo(() => { + const base: ApplicateInfoCategory[] = ['이름', '학번', '학과', '전화번호', '이메일', '결과']; + return interviewRequired ? [...base, '면접 시간'] : base; + }, [interviewRequired]); + const handleItemClick = useCallback( (applicantId: number) => { navigate(`/admin/clubs/${clubId}/applicants/${applicantId}`); @@ -51,11 +47,12 @@ export const ApplicantList = ({ filterOption, stage }: Props) => { return ( - - {INFO_CATEGORY.map((category) => ( + + {categories.map((category) => ( {category} ))} + {applicants.length > 0 ? ( applicants.map((applicant) => ( @@ -70,7 +67,8 @@ export const ApplicantList = ({ filterOption, stage }: Props) => { status={applicant.status} confirmedTime={applicant.confirmedTime} interviewInfo={applicant.interviewInfo} - interviewSchedule={applicant.interviewSchedule} + interviewSchedule={interviewSchedule} + interviewRequired={interviewRequired} onClick={handleItemClick} /> )) diff --git a/src/pages/admin/Dashboard/components/ApplicantListSection/List/ApplicantListItem/InterviewTimeContentModal.tsx b/src/pages/admin/Dashboard/components/ApplicantListSection/List/ApplicantListItem/InterviewTimeContentModal.tsx index a7c14b2e..26bf9917 100644 --- a/src/pages/admin/Dashboard/components/ApplicantListSection/List/ApplicantListItem/InterviewTimeContentModal.tsx +++ b/src/pages/admin/Dashboard/components/ApplicantListSection/List/ApplicantListItem/InterviewTimeContentModal.tsx @@ -1,39 +1,55 @@ +import { Text } from '@/shared/components/Text'; import { formatDateWithoutYear } from '@/shared/utils/dateUtils'; import * as S from './InterviewTimeContentModal.styled'; import type { InterviewInfo, InterviewSchedule } from '@/pages/admin/Dashboard/types/dashboard'; type Props = { - interviewInfo?: InterviewInfo[]; - interviewSchedule?: InterviewSchedule[]; + interviewInfo: InterviewInfo[]; + interviewSchedule: InterviewSchedule[]; }; export const InterviewTimeContentModal = ({ interviewInfo, interviewSchedule }: Props) => { return ( - {interviewSchedule?.map((schedule) => ( - - {formatDateWithoutYear(schedule.date)} - - {schedule.slots.map((slot) => ( - - {slot.time} - ({slot.assignedCount}명 선택) - - ))} - - - ))} + {interviewSchedule?.length ? ( + <> + {interviewSchedule?.map((schedule) => ( + + {formatDateWithoutYear(schedule.date)} + + {schedule.slots.map((slot) => ( + + {slot.time} + ({slot.assignedCount}명 선택) + + ))} + + + ))} + + ) : ( + + ⚠️ 면접 일정을 먼저 등록해주세요 + + )} - 지원자 면접 희망 시간대 - {interviewInfo?.map((info) => ( - - {formatDateWithoutYear(info.interviewDate)} - {info.availableTimes.join(', ')} - - ))} + {interviewInfo?.length ? ( + <> + {interviewInfo?.map((info) => ( + + {formatDateWithoutYear(info.interviewDate)} + {info.availableTimes.join(', ')} + + ))} + + ) : ( + + ⚠️ 지원자가 선택한 시간이 없습니다 + + )} ); diff --git a/src/pages/admin/Dashboard/components/ApplicantListSection/List/ApplicantListItem/index.styled.ts b/src/pages/admin/Dashboard/components/ApplicantListSection/List/ApplicantListItem/index.styled.ts index 0b6fcd2e..f2b66f8b 100644 --- a/src/pages/admin/Dashboard/components/ApplicantListSection/List/ApplicantListItem/index.styled.ts +++ b/src/pages/admin/Dashboard/components/ApplicantListSection/List/ApplicantListItem/index.styled.ts @@ -1,9 +1,10 @@ import styled from '@emotion/styled'; import type { ApplicantData } from '@/pages/admin/Dashboard/types/dashboard'; -export const ItemWrapper = styled.div` +export const ItemWrapper = styled.div<{ hasInterview: boolean }>` display: grid; - grid-template-columns: 1fr 1fr 1fr 1.2fr 1.5fr 1fr 1.2fr; + grid-template-columns: ${({ hasInterview }) => + '1fr 1fr 1fr 1.2fr 1.5fr 1fr' + (hasInterview ? ' 1.2fr' : '')}; gap: 1rem; align-items: center; padding: 1.5rem 0; @@ -19,7 +20,8 @@ export const ItemWrapper = styled.div` } @media (max-width: 1200px) { - grid-template-columns: 1fr 1fr 1fr 1.5fr 1fr 1.2fr; + grid-template-columns: ${({ hasInterview }) => + '1fr 1fr 1fr 1.5fr 1fr' + (hasInterview ? ' 1.2fr' : '')}; & > p:nth-of-type(4) { display: none; @@ -27,7 +29,8 @@ export const ItemWrapper = styled.div` } @media (max-width: ${({ theme }) => theme.breakpoints.web}) { - grid-template-columns: 1fr 1fr 1.5fr 1fr 1.2fr; + grid-template-columns: ${({ hasInterview }) => + '1fr 1fr 1.5fr 1fr' + (hasInterview ? ' 1.2fr' : '')}; & > p:nth-of-type(3) { display: none; @@ -35,7 +38,8 @@ export const ItemWrapper = styled.div` } @media (max-width: 768px) { - grid-template-columns: 1fr 1.5fr 1fr 1.2fr; + grid-template-columns: ${({ hasInterview }) => + '1fr 1.5fr 1fr' + (hasInterview ? ' 1.2fr' : '')}; & > p:nth-of-type(2) { display: none; diff --git a/src/pages/admin/Dashboard/components/ApplicantListSection/List/ApplicantListItem/index.tsx b/src/pages/admin/Dashboard/components/ApplicantListSection/List/ApplicantListItem/index.tsx index 68f46dc3..9c91957f 100644 --- a/src/pages/admin/Dashboard/components/ApplicantListSection/List/ApplicantListItem/index.tsx +++ b/src/pages/admin/Dashboard/components/ApplicantListSection/List/ApplicantListItem/index.tsx @@ -1,14 +1,16 @@ import React, { useRef } from 'react'; import { STATUS_LABEL } from '@/pages/admin/Dashboard/utils/labelMap'; -// import { Modal } from '@/shared/components/Modal'; +import { Modal } from '@/shared/components/Modal'; import { Text } from '@/shared/components/Text'; -// import { useModal } from '@/shared/hooks/useModal'; +import { useModal } from '@/shared/hooks/useModal'; import { formatDateTime } from '@/shared/utils/dateUtils'; import * as S from './index.styled'; -// import { InterviewTimeContentModal } from './InterviewTimeContentModal'; -import type { ApplicantData } from '@/pages/admin/Dashboard/types/dashboard'; +import { InterviewTimeContentModal } from './InterviewTimeContentModal'; +import type { ApplicantData, InterviewSchedule } from '@/pages/admin/Dashboard/types/dashboard'; type Props = ApplicantData & { + interviewSchedule: InterviewSchedule[]; + interviewRequired: boolean; onClick: (id: number) => void; }; @@ -21,28 +23,28 @@ export const ApplicantListItem = React.memo(function ApplicantListItem({ email, status, confirmedTime, - // interviewInfo, - // interviewSchedule, + interviewInfo, + interviewSchedule, + interviewRequired, onClick, }: Props) { - // const { isOpen, openModal, closeModal } = useModal(); + const { isOpen, openModal, closeModal } = useModal(); const timeSetterRef = useRef(null); const handleTimeSetterClick = (e: React.MouseEvent) => { e.stopPropagation(); - // openModal(); + openModal(); }; - return ( <> - onClick(applicantId)}> + onClick(applicantId)}> {name || '-'} {studentId || '-'} {department || '-'} {phoneNumber || '-'} {email || '-'} {STATUS_LABEL[status] || '-'} - {status === 'APPROVED' && ( + {interviewRequired && status === 'APPROVED' && ( {confirmedTime ? ( @@ -56,7 +58,7 @@ export const ApplicantListItem = React.memo(function ApplicantListItem({ )} - {/* - */} + ); }); diff --git a/src/pages/admin/Dashboard/hooks/useApplicants.ts b/src/pages/admin/Dashboard/hooks/useApplicants.ts index 65344454..c858d81e 100644 --- a/src/pages/admin/Dashboard/hooks/useApplicants.ts +++ b/src/pages/admin/Dashboard/hooks/useApplicants.ts @@ -6,11 +6,14 @@ import type { ApplicantsApiResponse, ApplicationFilterOption, ApplicantCounts, + InterviewSchedule, } from '@/pages/admin/Dashboard/types/dashboard'; import type { UseApiQueryResult } from '@/shared/types/useApiQueryResult'; export interface ExtendedUseApiQueryResult extends UseApiQueryResult { counts: ApplicantCounts; + interviewRequired: boolean; + interviewSchedule: InterviewSchedule[]; } export const useApplicants = ( @@ -60,5 +63,7 @@ export const useApplicants = ( isLoading, error, counts, + interviewRequired: responseData?.interviewRequired ?? false, + interviewSchedule: responseData?.interviewSchedule || [], }; }; diff --git a/src/pages/admin/Dashboard/types/dashboard.ts b/src/pages/admin/Dashboard/types/dashboard.ts index 2fa7ae63..0327b59f 100644 --- a/src/pages/admin/Dashboard/types/dashboard.ts +++ b/src/pages/admin/Dashboard/types/dashboard.ts @@ -30,9 +30,8 @@ export type ApplicantData = { phoneNumber: string; email: string; status: ApplicationStatus; - confirmedTime?: string; - interviewInfo?: InterviewInfo[]; - interviewSchedule?: InterviewSchedule[]; + confirmedTime: string | null; + interviewInfo: InterviewInfo[]; }; export type DashboardSummary = { @@ -63,6 +62,7 @@ export type InterviewSchedule = { }; export type ApplicantsApiResponse = { + interviewRequired: boolean; applicants: ApplicantData[]; interviewSchedule: InterviewSchedule[]; message: string | null;