Skip to content

Commit e64d970

Browse files
authored
Merge pull request #412 from kakao-tech-campus-3rd-step3/feat/interview-time-modal#411
[FEAT] 지원자 목록 면접 시간 컬럼 추가 및 시간 선택 모달 구현 (#411)
2 parents a2df82f + 23a5225 commit e64d970

File tree

16 files changed

+345
-60
lines changed

16 files changed

+345
-60
lines changed

src/app/mocks/repositories/dashboard.ts

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type {
22
DashboardSummary,
33
ApplicantsApiResponse,
44
ApplicantData,
5+
InterviewSchedule,
56
} from '@/pages/admin/Dashboard/types/dashboard';
67

78
const MOCK_DASHBOARD_SUMMARY: DashboardSummary = {
@@ -11,6 +12,25 @@ const MOCK_DASHBOARD_SUMMARY: DashboardSummary = {
1112
endDay: '2024-03-31',
1213
};
1314

15+
const MOCK_INTERVIEW_SCHEDULE: InterviewSchedule[] = [
16+
{
17+
date: '2026-09-02',
18+
slots: [
19+
{ time: '14:00', assignedCount: 1 },
20+
{ time: '15:00', assignedCount: 1 },
21+
{ time: '16:00', assignedCount: 0 },
22+
],
23+
},
24+
{
25+
date: '2026-09-03',
26+
slots: [
27+
{ time: '10:00', assignedCount: 1 },
28+
{ time: '11:00', assignedCount: 0 },
29+
{ time: '14:00', assignedCount: 0 },
30+
],
31+
},
32+
];
33+
1434
const MOCK_APPLICANTS: ApplicantData[] = [
1535
{
1636
applicantId: 1,
@@ -19,7 +39,15 @@ const MOCK_APPLICANTS: ApplicantData[] = [
1939
department: '컴퓨터공학과',
2040
phoneNumber: '010-1234-5678',
2141
email: 'test1@example.com',
22-
status: 'PENDING',
42+
status: 'APPROVED',
43+
confirmedTime: '2026-09-02T15:00:00',
44+
interviewInfo: [
45+
{
46+
interviewDate: '2026-09-02',
47+
availableTimes: ['14:00', '15:00', '16:00'],
48+
},
49+
],
50+
interviewSchedule: MOCK_INTERVIEW_SCHEDULE,
2351
},
2452
{
2553
applicantId: 2,
@@ -29,6 +57,14 @@ const MOCK_APPLICANTS: ApplicantData[] = [
2957
phoneNumber: '010-2345-6789',
3058
email: 'test2@example.com',
3159
status: 'APPROVED',
60+
confirmedTime: undefined,
61+
interviewInfo: [
62+
{
63+
interviewDate: '2026-09-02',
64+
availableTimes: ['10:00', '11:00'],
65+
},
66+
],
67+
interviewSchedule: MOCK_INTERVIEW_SCHEDULE,
3268
},
3369
{
3470
applicantId: 3,
@@ -38,6 +74,13 @@ const MOCK_APPLICANTS: ApplicantData[] = [
3874
phoneNumber: '010-3456-7890',
3975
email: 'test3@example.com',
4076
status: 'REJECTED',
77+
confirmedTime: '2026-09-03T10:00:00',
78+
interviewInfo: [
79+
{
80+
interviewDate: '2026-09-03',
81+
availableTimes: ['10:00', '11:00', '14:00'],
82+
},
83+
],
4184
},
4285
];
4386

@@ -49,6 +92,7 @@ export const dashboardRepository = {
4992
getApplicants: (): ApplicantsApiResponse => {
5093
return {
5194
applicants: MOCK_APPLICANTS,
95+
interviewSchedule: MOCK_INTERVIEW_SCHEDULE,
5296
message: null,
5397
};
5498
},

src/pages/admin/ApplicationDetail/Page.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import { ApplicantProfileSection } from './components/ApplicantProfileSection/in
66
import { ApplicantQuestionSection } from './components/ApplicationQuestionSection';
77
import { CommentSection } from './components/CommentSection';
88
import { useDetailApplications } from './hooks/useDetailApplication';
9-
109
import * as S from './index.styled';
1110

1211
export const ApplicationDetailPage = () => {

src/pages/admin/Dashboard/api/sentMessage.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import { apiInstance } from '@/app/api/initInstance';
2-
import { stageMap } from '../utils/stageMap';
2+
import { STAGE_LABEL } from '../utils/labelMap';
33
import type { ApplicationStage } from '@/pages/admin/Dashboard/types/dashboard';
44

55
export const sentMessage = async (
66
clubId: number,
77
message: string,
88
stage: ApplicationStage,
99
): Promise<void> => {
10-
const apiStage = stageMap[stage];
10+
const apiStage = STAGE_LABEL[stage];
1111

1212
try {
1313
await apiInstance.patch(`/clubs/${clubId}/club-apply-form/result?stage=${apiStage}`, {

src/pages/admin/Dashboard/components/ApplicantListSection/Filter/ApplicationFilter.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import styled from '@emotion/styled';
22
import { useApplicants } from '@/pages/admin/Dashboard/hooks/useApplicants';
3-
import { stageMap } from '@/pages/admin/Dashboard/utils/stageMap';
3+
import { STAGE_LABEL } from '@/pages/admin/Dashboard/utils/labelMap';
44
import { ApplicantFilterButton } from './ApplicationFilterButton';
55

66
import type {
@@ -16,7 +16,7 @@ export type Props = {
1616
};
1717

1818
export const ApplicationStatusFilter = ({ option, onOptionChange, stage, clubId }: Props) => {
19-
const apiStage = stageMap[stage];
19+
const apiStage = STAGE_LABEL[stage];
2020
const { counts } = useApplicants(clubId, apiStage);
2121

2222
return (

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

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ export const Container = styled.div({
66

77
export const ApplicantInfoCategoryList = styled.div`
88
display: grid;
9-
grid-template-columns: 1fr 1fr 1.5fr 1.5fr 2fr 1fr;
9+
grid-template-columns: 1fr 1fr 1fr 1.2fr 1.5fr 1fr 1.2fr;
1010
background-color: #f9fbfc;
1111
border-bottom: 1.8px solid ${({ theme }) => theme.colors.gray100};
1212
padding: 1.7rem 0 1.5rem 0;
@@ -16,29 +16,34 @@ export const ApplicantInfoCategoryList = styled.div`
1616
}
1717
1818
@media (max-width: 1200px) {
19-
grid-template-columns: 1fr 1fr 1.5fr 1.5fr 1fr;
20-
& > div:nth-of-type(5) {
19+
grid-template-columns: 1fr 1fr 1fr 1.5fr 1fr 1.2fr;
20+
21+
& > div:nth-of-type(4) {
2122
display: none;
2223
}
2324
}
2425
2526
@media (max-width: ${({ theme }) => theme.breakpoints.web}) {
26-
grid-template-columns: 1fr 1fr 1.5fr 1fr;
27-
& > div:nth-of-type(4) {
27+
grid-template-columns: 1fr 1fr 1.5fr 1fr 1.2fr;
28+
29+
& > div:nth-of-type(3) {
2830
display: none;
2931
}
3032
}
3133
3234
@media (max-width: 768px) {
33-
grid-template-columns: 1fr 1fr 1fr;
34-
& > div:nth-of-type(3) {
35+
grid-template-columns: 1fr 1.5fr 1fr 1.2fr;
36+
37+
& > div:nth-of-type(2) {
3538
display: none;
3639
}
3740
}
3841
3942
@media (max-width: ${({ theme }) => theme.breakpoints.mobile}) {
40-
grid-template-columns: 1.5fr 1fr;
41-
& > div:nth-of-type(2) {
43+
grid-template-columns: 1fr 1fr;
44+
45+
& > div:nth-of-type(5),
46+
& > div:nth-of-type(7) {
4247
display: none;
4348
}
4449
}

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

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import { useCallback } from 'react';
22
import { useNavigate, useParams } from 'react-router-dom';
33
import { useApplicants } from '@/pages/admin/Dashboard/hooks/useApplicants';
4-
import { stageMap } from '@/pages/admin/Dashboard/utils/stageMap';
4+
import { STAGE_LABEL } from '@/pages/admin/Dashboard/utils/labelMap';
55
import { LoadingSpinner } from '@/shared/components/LoadingSpinner';
66
import { ApplicantListItem } from '../ApplicantListItem';
77
import * as S from './index.styled';
88
import type {
9+
ApplicateInfoCategory,
910
ApplicationFilterOption,
1011
ApplicationStage,
1112
} from '@/pages/admin/Dashboard/types/dashboard';
@@ -15,22 +16,22 @@ type Props = {
1516
stage: ApplicationStage;
1617
};
1718

18-
type ApplicateInfoCategory = '이름' | '학번' | '학과' | '전화번호' | '이메일' | '결과';
1919
const INFO_CATEGORY: ApplicateInfoCategory[] = [
2020
'이름',
2121
'학번',
2222
'학과',
2323
'전화번호',
2424
'이메일',
2525
'결과',
26+
'면접 시간',
2627
];
2728

2829
export const ApplicantList = ({ filterOption, stage }: Props) => {
2930
const { clubId } = useParams();
3031

3132
const navigate = useNavigate();
3233

33-
const apiStage = stageMap[stage];
34+
const apiStage = STAGE_LABEL[stage];
3435

3536
const {
3637
data: applicants,
@@ -67,6 +68,9 @@ export const ApplicantList = ({ filterOption, stage }: Props) => {
6768
phoneNumber={applicant.phoneNumber}
6869
email={applicant.email}
6970
status={applicant.status}
71+
confirmedTime={applicant.confirmedTime}
72+
interviewInfo={applicant.interviewInfo}
73+
interviewSchedule={applicant.interviewSchedule}
7074
onClick={handleItemClick}
7175
/>
7276
))
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import styled from '@emotion/styled';
2+
3+
export const Container = styled.div({
4+
display: 'flex',
5+
flexDirection: 'column',
6+
gap: '1.8rem',
7+
});
8+
9+
export const Section = styled.div({
10+
display: 'flex',
11+
flexDirection: 'column',
12+
gap: '1rem',
13+
});
14+
15+
export const SectionTitle = styled.h3(({ theme }) => ({
16+
fontSize: theme.font.size.sm,
17+
fontWeight: 600,
18+
color: theme.colors.gray700,
19+
paddingBottom: '0.5rem',
20+
borderBottom: `1px solid ${theme.colors.gray200}`,
21+
}));
22+
23+
export const ScheduleRow = styled.div({
24+
display: 'flex',
25+
alignItems: 'flex-start',
26+
gap: '1rem',
27+
});
28+
export const ScheduleDateLabel = styled.span(({ theme }) => ({
29+
fontSize: theme.font.size.sm,
30+
fontWeight: 600,
31+
color: theme.colors.gray700,
32+
minWidth: '2.5rem',
33+
paddingTop: '0.5rem',
34+
}));
35+
36+
export const DateLabel = styled.span(({ theme }) => ({
37+
fontSize: theme.font.size.sm,
38+
fontWeight: 600,
39+
color: theme.colors.gray700,
40+
minWidth: '2.5rem',
41+
}));
42+
43+
export const SlotsContainer = styled.div({
44+
display: 'flex',
45+
flexWrap: 'wrap',
46+
gap: '0.5rem',
47+
flex: 1,
48+
});
49+
50+
export const TimeSlot = styled.button<{ $selected?: boolean }>(({ theme, $selected }) => ({
51+
display: 'flex',
52+
flexDirection: 'column',
53+
alignItems: 'center',
54+
padding: '0.5rem 0.75rem',
55+
borderRadius: theme.radius.md,
56+
border: `1px solid ${$selected ? theme.colors.primary : theme.colors.gray300}`,
57+
backgroundColor: $selected ? theme.colors.primary00 : theme.colors.bg,
58+
cursor: 'pointer',
59+
transition: 'all 0.2s',
60+
minWidth: '4rem',
61+
62+
'&:hover': {
63+
borderColor: theme.colors.primary,
64+
backgroundColor: theme.colors.primary00,
65+
},
66+
}));
67+
68+
export const SlotTime = styled.span(({ theme }) => ({
69+
fontSize: theme.font.size.sm,
70+
fontWeight: 500,
71+
color: theme.colors.gray800,
72+
}));
73+
74+
export const SlotCount = styled.span(({ theme }) => ({
75+
fontSize: theme.font.size.xs,
76+
color: theme.colors.gray500,
77+
marginTop: '0.125rem',
78+
}));
79+
80+
export const AvailableTimesRow = styled.div({
81+
display: 'flex',
82+
alignItems: 'center',
83+
gap: '1rem',
84+
});
85+
86+
export const AvailableTimes = styled.span(({ theme }) => ({
87+
fontSize: theme.font.size.sm,
88+
color: theme.colors.gray600,
89+
}));
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { formatDateWithoutYear } from '@/shared/utils/dateUtils';
2+
import * as S from './InterviewTimeContentModal.styled';
3+
import type { InterviewInfo, InterviewSchedule } from '@/pages/admin/Dashboard/types/dashboard';
4+
5+
type Props = {
6+
interviewInfo?: InterviewInfo[];
7+
interviewSchedule?: InterviewSchedule[];
8+
};
9+
10+
export const InterviewTimeContentModal = ({ interviewInfo, interviewSchedule }: Props) => {
11+
return (
12+
<S.Container>
13+
<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+
))}
27+
</S.Section>
28+
29+
<S.Section>
30+
<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+
))}
37+
</S.Section>
38+
</S.Container>
39+
);
40+
};

0 commit comments

Comments
 (0)