Skip to content

Commit 82d5870

Browse files
authored
Merge pull request #423 from kakao-tech-campus-3rd-step3/feat/interview-required-column#420
[FEAT] 면접 진행 여부에 따른 지원자 목록 컬럼 조건부 노출 (#420)
2 parents 9e46e9c + 781dff7 commit 82d5870

File tree

9 files changed

+94
-67
lines changed

9 files changed

+94
-67
lines changed

src/app/mocks/repositories/applicant.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { DetailApplication } from '@/pages/admin/ApplicationDetail/types/detailApplication';
22
import type { ApplicantData } from '@/pages/admin/Dashboard/types/dashboard';
33

4-
const applicants: ApplicantData[] = [
4+
const applicants: Omit<ApplicantData, 'confirmedTime' | 'interviewInfo'>[] = [
55
{
66
applicantId: 1,
77
name: '김동글',

src/app/mocks/repositories/dashboard.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,6 @@ const MOCK_APPLICANTS: ApplicantData[] = [
4747
availableTimes: ['14:00', '15:00', '16:00'],
4848
},
4949
],
50-
interviewSchedule: MOCK_INTERVIEW_SCHEDULE,
5150
},
5251
{
5352
applicantId: 2,
@@ -57,14 +56,13 @@ const MOCK_APPLICANTS: ApplicantData[] = [
5756
phoneNumber: '010-2345-6789',
5857
email: 'test2@example.com',
5958
status: 'APPROVED',
60-
confirmedTime: undefined,
59+
confirmedTime: null,
6160
interviewInfo: [
6261
{
6362
interviewDate: '2026-09-02',
6463
availableTimes: ['10:00', '11:00'],
6564
},
6665
],
67-
interviewSchedule: MOCK_INTERVIEW_SCHEDULE,
6866
},
6967
{
7068
applicantId: 3,
@@ -91,6 +89,7 @@ export const dashboardRepository = {
9189

9290
getApplicants: (): ApplicantsApiResponse => {
9391
return {
92+
interviewRequired: true,
9493
applicants: MOCK_APPLICANTS,
9594
interviewSchedule: MOCK_INTERVIEW_SCHEDULE,
9695
message: null,

src/pages/admin/Dashboard/components/ApplicantListSection/List/ApplicantList/index.styled.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@ export const Container = styled.div({
44
width: '100%',
55
});
66

7-
export const ApplicantInfoCategoryList = styled.div`
7+
export const ApplicantInfoCategoryList = styled.div<{ hasInterview: boolean }>`
88
display: grid;
9-
grid-template-columns: 1fr 1fr 1fr 1.2fr 1.5fr 1fr 1.2fr;
9+
grid-template-columns: ${({ hasInterview }) =>
10+
'1fr 1fr 1fr 1.2fr 1.5fr 1fr' + (hasInterview ? '1.2fr' : '')};
1011
background-color: #f9fbfc;
1112
border-bottom: 1.8px solid ${({ theme }) => theme.colors.gray100};
1213
padding: 1.7rem 0 1.5rem 0;
@@ -16,23 +17,25 @@ export const ApplicantInfoCategoryList = styled.div`
1617
}
1718
1819
@media (max-width: 1200px) {
19-
grid-template-columns: 1fr 1fr 1fr 1.5fr 1fr 1.2fr;
20+
grid-template-columns: ${({ hasInterview }) =>
21+
'1fr 1fr 1fr 1.5fr 1fr' + (hasInterview ? '1.2fr' : '')};
2022
2123
& > div:nth-of-type(4) {
2224
display: none;
2325
}
2426
}
2527
2628
@media (max-width: ${({ theme }) => theme.breakpoints.web}) {
27-
grid-template-columns: 1fr 1fr 1.5fr 1fr 1.2fr;
29+
grid-template-columns: ${({ hasInterview }) =>
30+
'1fr 1fr 1.5fr 1fr' + (hasInterview ? '1.2fr' : '')}
2831
2932
& > div:nth-of-type(3) {
3033
display: none;
3134
}
3235
}
3336
3437
@media (max-width: 768px) {
35-
grid-template-columns: 1fr 1.5fr 1fr 1.2fr;
38+
grid-template-columns: ${({ hasInterview }) => '1fr 1.5fr 1fr' + (hasInterview ? '1.2fr' : '')};
3639
3740
& > div:nth-of-type(2) {
3841
display: none;

src/pages/admin/Dashboard/components/ApplicantListSection/List/ApplicantList/index.tsx

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useCallback } from 'react';
1+
import { useCallback, useMemo } from 'react';
22
import { useNavigate, useParams } from 'react-router-dom';
33
import { useApplicants } from '@/pages/admin/Dashboard/hooks/useApplicants';
44
import { STAGE_LABEL } from '@/pages/admin/Dashboard/utils/labelMap';
@@ -16,19 +16,8 @@ type Props = {
1616
stage: ApplicationStage;
1717
};
1818

19-
const INFO_CATEGORY: ApplicateInfoCategory[] = [
20-
'이름',
21-
'학번',
22-
'학과',
23-
'전화번호',
24-
'이메일',
25-
'결과',
26-
'면접 시간',
27-
];
28-
2919
export const ApplicantList = ({ filterOption, stage }: Props) => {
3020
const { clubId } = useParams();
31-
3221
const navigate = useNavigate();
3322

3423
const apiStage = STAGE_LABEL[stage];
@@ -37,8 +26,15 @@ export const ApplicantList = ({ filterOption, stage }: Props) => {
3726
data: applicants,
3827
isLoading,
3928
error,
29+
interviewSchedule,
30+
interviewRequired,
4031
} = useApplicants(Number(clubId), apiStage, filterOption);
4132

33+
const categories = useMemo(() => {
34+
const base: ApplicateInfoCategory[] = ['이름', '학번', '학과', '전화번호', '이메일', '결과'];
35+
return interviewRequired ? [...base, '면접 시간'] : base;
36+
}, [interviewRequired]);
37+
4238
const handleItemClick = useCallback(
4339
(applicantId: number) => {
4440
navigate(`/admin/clubs/${clubId}/applicants/${applicantId}`);
@@ -51,11 +47,12 @@ export const ApplicantList = ({ filterOption, stage }: Props) => {
5147

5248
return (
5349
<S.Container>
54-
<S.ApplicantInfoCategoryList>
55-
{INFO_CATEGORY.map((category) => (
50+
<S.ApplicantInfoCategoryList hasInterview={interviewRequired}>
51+
{categories.map((category) => (
5652
<S.CategoryText key={category}>{category}</S.CategoryText>
5753
))}
5854
</S.ApplicantInfoCategoryList>
55+
5956
<S.ApplicantInfoDataList>
6057
{applicants.length > 0 ? (
6158
applicants.map((applicant) => (
@@ -70,7 +67,8 @@ export const ApplicantList = ({ filterOption, stage }: Props) => {
7067
status={applicant.status}
7168
confirmedTime={applicant.confirmedTime}
7269
interviewInfo={applicant.interviewInfo}
73-
interviewSchedule={applicant.interviewSchedule}
70+
interviewSchedule={interviewSchedule}
71+
interviewRequired={interviewRequired}
7472
onClick={handleItemClick}
7573
/>
7674
))

src/pages/admin/Dashboard/components/ApplicantListSection/List/ApplicantListItem/InterviewTimeContentModal.tsx

Lines changed: 38 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,55 @@
1+
import { Text } from '@/shared/components/Text';
12
import { formatDateWithoutYear } from '@/shared/utils/dateUtils';
23
import * as S from './InterviewTimeContentModal.styled';
34
import type { InterviewInfo, InterviewSchedule } from '@/pages/admin/Dashboard/types/dashboard';
45

56
type Props = {
6-
interviewInfo?: InterviewInfo[];
7-
interviewSchedule?: InterviewSchedule[];
7+
interviewInfo: InterviewInfo[];
8+
interviewSchedule: InterviewSchedule[];
89
};
910

1011
export const InterviewTimeContentModal = ({ interviewInfo, interviewSchedule }: Props) => {
1112
return (
1213
<S.Container>
1314
<S.Section>
14-
{interviewSchedule?.map((schedule) => (
15-
<S.ScheduleRow key={schedule.date}>
16-
<S.ScheduleDateLabel>{formatDateWithoutYear(schedule.date)}</S.ScheduleDateLabel>
17-
<S.SlotsContainer>
18-
{schedule.slots.map((slot) => (
19-
<S.TimeSlot key={slot.time}>
20-
<S.SlotTime>{slot.time}</S.SlotTime>
21-
<S.SlotCount>({slot.assignedCount}명 선택)</S.SlotCount>
22-
</S.TimeSlot>
23-
))}
24-
</S.SlotsContainer>
25-
</S.ScheduleRow>
26-
))}
15+
{interviewSchedule?.length ? (
16+
<>
17+
{interviewSchedule?.map((schedule) => (
18+
<S.ScheduleRow key={schedule.date}>
19+
<S.ScheduleDateLabel>{formatDateWithoutYear(schedule.date)}</S.ScheduleDateLabel>
20+
<S.SlotsContainer>
21+
{schedule.slots.map((slot) => (
22+
<S.TimeSlot key={slot.time}>
23+
<S.SlotTime>{slot.time}</S.SlotTime>
24+
<S.SlotCount>({slot.assignedCount}명 선택)</S.SlotCount>
25+
</S.TimeSlot>
26+
))}
27+
</S.SlotsContainer>
28+
</S.ScheduleRow>
29+
))}
30+
</>
31+
) : (
32+
<Text color='#595959' size='sm'>
33+
⚠️ 면접 일정을 먼저 등록해주세요
34+
</Text>
35+
)}
2736
</S.Section>
28-
2937
<S.Section>
3038
<S.SectionTitle>지원자 면접 희망 시간대</S.SectionTitle>
31-
{interviewInfo?.map((info) => (
32-
<S.AvailableTimesRow key={info.interviewDate}>
33-
<S.DateLabel>{formatDateWithoutYear(info.interviewDate)}</S.DateLabel>
34-
<S.AvailableTimes>{info.availableTimes.join(', ')}</S.AvailableTimes>
35-
</S.AvailableTimesRow>
36-
))}
39+
{interviewInfo?.length ? (
40+
<>
41+
{interviewInfo?.map((info) => (
42+
<S.AvailableTimesRow key={info.interviewDate}>
43+
<S.DateLabel>{formatDateWithoutYear(info.interviewDate)}</S.DateLabel>
44+
<S.AvailableTimes>{info.availableTimes.join(', ')}</S.AvailableTimes>
45+
</S.AvailableTimesRow>
46+
))}
47+
</>
48+
) : (
49+
<Text color='#595959' size='sm'>
50+
⚠️ 지원자가 선택한 시간이 없습니다
51+
</Text>
52+
)}
3753
</S.Section>
3854
</S.Container>
3955
);

src/pages/admin/Dashboard/components/ApplicantListSection/List/ApplicantListItem/index.styled.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import styled from '@emotion/styled';
22
import type { ApplicantData } from '@/pages/admin/Dashboard/types/dashboard';
33

4-
export const ItemWrapper = styled.div`
4+
export const ItemWrapper = styled.div<{ hasInterview: boolean }>`
55
display: grid;
6-
grid-template-columns: 1fr 1fr 1fr 1.2fr 1.5fr 1fr 1.2fr;
6+
grid-template-columns: ${({ hasInterview }) =>
7+
'1fr 1fr 1fr 1.2fr 1.5fr 1fr' + (hasInterview ? ' 1.2fr' : '')};
78
gap: 1rem;
89
align-items: center;
910
padding: 1.5rem 0;
@@ -19,23 +20,26 @@ export const ItemWrapper = styled.div`
1920
}
2021
2122
@media (max-width: 1200px) {
22-
grid-template-columns: 1fr 1fr 1fr 1.5fr 1fr 1.2fr;
23+
grid-template-columns: ${({ hasInterview }) =>
24+
'1fr 1fr 1fr 1.5fr 1fr' + (hasInterview ? ' 1.2fr' : '')};
2325
2426
& > p:nth-of-type(4) {
2527
display: none;
2628
}
2729
}
2830
2931
@media (max-width: ${({ theme }) => theme.breakpoints.web}) {
30-
grid-template-columns: 1fr 1fr 1.5fr 1fr 1.2fr;
32+
grid-template-columns: ${({ hasInterview }) =>
33+
'1fr 1fr 1.5fr 1fr' + (hasInterview ? ' 1.2fr' : '')};
3134
3235
& > p:nth-of-type(3) {
3336
display: none;
3437
}
3538
}
3639
3740
@media (max-width: 768px) {
38-
grid-template-columns: 1fr 1.5fr 1fr 1.2fr;
41+
grid-template-columns: ${({ hasInterview }) =>
42+
'1fr 1.5fr 1fr' + (hasInterview ? ' 1.2fr' : '')};
3943
4044
& > p:nth-of-type(2) {
4145
display: none;

src/pages/admin/Dashboard/components/ApplicantListSection/List/ApplicantListItem/index.tsx

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
import React, { useRef } from 'react';
22
import { STATUS_LABEL } from '@/pages/admin/Dashboard/utils/labelMap';
3-
// import { Modal } from '@/shared/components/Modal';
3+
import { Modal } from '@/shared/components/Modal';
44
import { Text } from '@/shared/components/Text';
5-
// import { useModal } from '@/shared/hooks/useModal';
5+
import { useModal } from '@/shared/hooks/useModal';
66
import { formatDateTime } from '@/shared/utils/dateUtils';
77
import * as S from './index.styled';
8-
// import { InterviewTimeContentModal } from './InterviewTimeContentModal';
9-
import type { ApplicantData } from '@/pages/admin/Dashboard/types/dashboard';
8+
import { InterviewTimeContentModal } from './InterviewTimeContentModal';
9+
import type { ApplicantData, InterviewSchedule } from '@/pages/admin/Dashboard/types/dashboard';
1010

1111
type Props = ApplicantData & {
12+
interviewSchedule: InterviewSchedule[];
13+
interviewRequired: boolean;
1214
onClick: (id: number) => void;
1315
};
1416

@@ -21,28 +23,28 @@ export const ApplicantListItem = React.memo(function ApplicantListItem({
2123
email,
2224
status,
2325
confirmedTime,
24-
// interviewInfo,
25-
// interviewSchedule,
26+
interviewInfo,
27+
interviewSchedule,
28+
interviewRequired,
2629
onClick,
2730
}: Props) {
28-
// const { isOpen, openModal, closeModal } = useModal();
31+
const { isOpen, openModal, closeModal } = useModal();
2932
const timeSetterRef = useRef<HTMLButtonElement>(null);
3033

3134
const handleTimeSetterClick = (e: React.MouseEvent) => {
3235
e.stopPropagation();
33-
// openModal();
36+
openModal();
3437
};
35-
3638
return (
3739
<>
38-
<S.ItemWrapper onClick={() => onClick(applicantId)}>
40+
<S.ItemWrapper hasInterview={interviewRequired} onClick={() => onClick(applicantId)}>
3941
<S.InfoText>{name || '-'}</S.InfoText>
4042
<S.InfoText>{studentId || '-'}</S.InfoText>
4143
<S.InfoText>{department || '-'}</S.InfoText>
4244
<S.InfoText>{phoneNumber || '-'}</S.InfoText>
4345
<S.InfoText>{email || '-'}</S.InfoText>
4446
<S.StatusBadge status={status}>{STATUS_LABEL[status] || '-'}</S.StatusBadge>
45-
{status === 'APPROVED' && (
47+
{interviewRequired && status === 'APPROVED' && (
4648
<S.InfoText>
4749
{confirmedTime ? (
4850
<Text color='#8C8C8C' size='lg'>
@@ -56,7 +58,7 @@ export const ApplicantListItem = React.memo(function ApplicantListItem({
5658
</S.InfoText>
5759
)}
5860
</S.ItemWrapper>
59-
{/* <Modal
61+
<Modal
6062
isOpen={isOpen}
6163
onClose={closeModal}
6264
title='동아리 면접 공지 일정'
@@ -68,7 +70,7 @@ export const ApplicantListItem = React.memo(function ApplicantListItem({
6870
interviewInfo={interviewInfo}
6971
interviewSchedule={interviewSchedule}
7072
/>
71-
</Modal> */}
73+
</Modal>
7274
</>
7375
);
7476
});

src/pages/admin/Dashboard/hooks/useApplicants.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,14 @@ import type {
66
ApplicantsApiResponse,
77
ApplicationFilterOption,
88
ApplicantCounts,
9+
InterviewSchedule,
910
} from '@/pages/admin/Dashboard/types/dashboard';
1011
import type { UseApiQueryResult } from '@/shared/types/useApiQueryResult';
1112

1213
export interface ExtendedUseApiQueryResult<T> extends UseApiQueryResult<T> {
1314
counts: ApplicantCounts;
15+
interviewRequired: boolean;
16+
interviewSchedule: InterviewSchedule[];
1417
}
1518

1619
export const useApplicants = (
@@ -60,5 +63,7 @@ export const useApplicants = (
6063
isLoading,
6164
error,
6265
counts,
66+
interviewRequired: responseData?.interviewRequired ?? false,
67+
interviewSchedule: responseData?.interviewSchedule || [],
6368
};
6469
};

src/pages/admin/Dashboard/types/dashboard.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,8 @@ export type ApplicantData = {
3030
phoneNumber: string;
3131
email: string;
3232
status: ApplicationStatus;
33-
confirmedTime?: string;
34-
interviewInfo?: InterviewInfo[];
35-
interviewSchedule?: InterviewSchedule[];
33+
confirmedTime: string | null;
34+
interviewInfo: InterviewInfo[];
3635
};
3736

3837
export type DashboardSummary = {
@@ -63,6 +62,7 @@ export type InterviewSchedule = {
6362
};
6463

6564
export type ApplicantsApiResponse = {
65+
interviewRequired: boolean;
6666
applicants: ApplicantData[];
6767
interviewSchedule: InterviewSchedule[];
6868
message: string | null;

0 commit comments

Comments
 (0)