Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions src/app/mocks/repositories/dashboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@ const MOCK_APPLICANTS: ApplicantData[] = [
availableTimes: ['14:00', '15:00', '16:00'],
},
],
interviewSchedule: MOCK_INTERVIEW_SCHEDULE,
},
{
applicantId: 2,
Expand All @@ -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,
Expand All @@ -91,6 +89,7 @@ export const dashboardRepository = {

getApplicants: (): ApplicantsApiResponse => {
return {
interviewRequired: true,
applicants: MOCK_APPLICANTS,
interviewSchedule: MOCK_INTERVIEW_SCHEDULE,
message: null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 }) =>
hasInterview ? '1fr 1fr 1fr 1.2fr 1.5fr 1fr 1.2fr' : '1fr 1fr 1fr 1.2fr 1.5fr 1fr'};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

grid-template-columns를 설정하는 로직이 여러 미디어 쿼리에서 반복되고 있습니다. 이로 인해 코드가 장황해지고 유지보수가 어려워질 수 있습니다. 기본 컬럼 문자열에 인터뷰 컬럼을 조건부로 추가하는 방식으로 리팩토링하면 코드를 더 간결하고 명확하게 만들 수 있습니다. 아래 제안을 이 파일의 다른 미디어 쿼리들과 src/pages/admin/Dashboard/components/ApplicantListSection/List/ApplicantListItem/index.styled.ts 파일에도 동일하게 적용하는 것을 고려해보세요.

Suggested change
grid-template-columns: ${({ hasInterview }) =>
hasInterview ? '1fr 1fr 1fr 1.2fr 1.5fr 1fr 1.2fr' : '1fr 1fr 1fr 1.2fr 1.5fr 1fr'};
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;
Expand All @@ -16,23 +17,26 @@ export const ApplicantInfoCategoryList = styled.div`
}

@media (max-width: 1200px) {
grid-template-columns: 1fr 1fr 1fr 1.5fr 1fr 1.2fr;
grid-template-columns: ${({ hasInterview }) =>
hasInterview ? '1fr 1fr 1fr 1.5fr 1fr 1.2fr' : '1fr 1fr 1fr 1.5fr 1fr'};

& > div:nth-of-type(4) {
display: none;
}
}

@media (max-width: ${({ theme }) => theme.breakpoints.web}) {
grid-template-columns: 1fr 1fr 1.5fr 1fr 1.2fr;
grid-template-columns: ${({ hasInterview }) =>
hasInterview ? '1fr 1fr 1.5fr 1fr 1.2fr' : '1fr 1fr 1.5fr 1fr'};

& > div:nth-of-type(3) {
display: none;
}
}

@media (max-width: 768px) {
grid-template-columns: 1fr 1.5fr 1fr 1.2fr;
grid-template-columns: ${({ hasInterview }) =>
hasInterview ? '1fr 1.5fr 1fr 1.2fr' : '1fr 1.5fr 1fr'};

& > div:nth-of-type(2) {
display: none;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand All @@ -37,8 +26,15 @@ export const ApplicantList = ({ filterOption, stage }: Props) => {
data: applicants,
isLoading,
error,
interviewSchedule,
interviewRequired,
} = useApplicants(Number(clubId), apiStage, filterOption);

const categories = () => {
const base: ApplicateInfoCategory[] = ['이름', '학번', '학과', '전화번호', '이메일', '결과'];
return interviewRequired ? [...base, '면접 시간'] : base;
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

categories 함수는 컴포넌트가 렌더링될 때마다 재생성됩니다. 사소한 성능 저하일 수 있지만, useMemo 훅을 사용하여 interviewRequired 값이 변경될 때만 categories 배열이 재생성되도록 메모이제이션하는 것이 좋습니다.

이 제안을 적용하려면 react에서 useMemo를 import하고, JSX 내부의 categories().map 호출을 categories.map으로 변경해야 합니다.

Suggested change
const categories = () => {
const base: ApplicateInfoCategory[] = ['이름', '학번', '학과', '전화번호', '이메일', '결과'];
return interviewRequired ? [...base, '면접 시간'] : base;
};
const categories = useMemo(() => {
const base: ApplicateInfoCategory[] = ['이름', '학번', '학과', '전화번호', '이메일', '결과'];
return interviewRequired ? [...base, '면접 시간'] : base;
}, [interviewRequired]);


const handleItemClick = useCallback(
(applicantId: number) => {
navigate(`/admin/clubs/${clubId}/applicants/${applicantId}`);
Expand All @@ -51,11 +47,12 @@ export const ApplicantList = ({ filterOption, stage }: Props) => {

return (
<S.Container>
<S.ApplicantInfoCategoryList>
{INFO_CATEGORY.map((category) => (
<S.ApplicantInfoCategoryList hasInterview={interviewRequired}>
{categories().map((category) => (
<S.CategoryText key={category}>{category}</S.CategoryText>
))}
</S.ApplicantInfoCategoryList>

<S.ApplicantInfoDataList>
{applicants.length > 0 ? (
applicants.map((applicant) => (
Expand All @@ -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}
/>
))
Expand Down
Original file line number Diff line number Diff line change
@@ -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 (
<S.Container>
<S.Section>
{interviewSchedule?.map((schedule) => (
<S.ScheduleRow key={schedule.date}>
<S.ScheduleDateLabel>{formatDateWithoutYear(schedule.date)}</S.ScheduleDateLabel>
<S.SlotsContainer>
{schedule.slots.map((slot) => (
<S.TimeSlot key={slot.time}>
<S.SlotTime>{slot.time}</S.SlotTime>
<S.SlotCount>({slot.assignedCount}명 선택)</S.SlotCount>
</S.TimeSlot>
))}
</S.SlotsContainer>
</S.ScheduleRow>
))}
{interviewSchedule?.length ? (
<>
{interviewSchedule?.map((schedule) => (
<S.ScheduleRow key={schedule.date}>
<S.ScheduleDateLabel>{formatDateWithoutYear(schedule.date)}</S.ScheduleDateLabel>
<S.SlotsContainer>
{schedule.slots.map((slot) => (
<S.TimeSlot key={slot.time}>
<S.SlotTime>{slot.time}</S.SlotTime>
<S.SlotCount>({slot.assignedCount}명 선택)</S.SlotCount>
</S.TimeSlot>
))}
</S.SlotsContainer>
</S.ScheduleRow>
))}
</>
) : (
<Text color='#595959' size='sm'>
⚠️ 면접 일정을 먼저 등록해주세요
</Text>
)}
</S.Section>

<S.Section>
<S.SectionTitle>지원자 면접 희망 시간대</S.SectionTitle>
{interviewInfo?.map((info) => (
<S.AvailableTimesRow key={info.interviewDate}>
<S.DateLabel>{formatDateWithoutYear(info.interviewDate)}</S.DateLabel>
<S.AvailableTimes>{info.availableTimes.join(', ')}</S.AvailableTimes>
</S.AvailableTimesRow>
))}
{interviewInfo?.length ? (
<>
{interviewInfo?.map((info) => (
<S.AvailableTimesRow key={info.interviewDate}>
<S.DateLabel>{formatDateWithoutYear(info.interviewDate)}</S.DateLabel>
<S.AvailableTimes>{info.availableTimes.join(', ')}</S.AvailableTimes>
</S.AvailableTimesRow>
))}
</>
) : (
<Text color='#595959' size='sm'>
⚠️ 지원자가 선택한 시간이 없습니다
</Text>
)}
</S.Section>
</S.Container>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -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 }) =>
hasInterview ? '1fr 1fr 1fr 1.2fr 1.5fr 1fr 1.2fr' : '1fr 1fr 1fr 1.2fr 1.5fr 1fr'};
gap: 1rem;
align-items: center;
padding: 1.5rem 0;
Expand All @@ -19,23 +20,26 @@ export const ItemWrapper = styled.div`
}

@media (max-width: 1200px) {
grid-template-columns: 1fr 1fr 1fr 1.5fr 1fr 1.2fr;
grid-template-columns: ${({ hasInterview }) =>
hasInterview ? '1fr 1fr 1fr 1.5fr 1fr 1.2fr' : '1fr 1fr 1fr 1.5fr 1fr'};

& > p:nth-of-type(4) {
display: none;
}
}

@media (max-width: ${({ theme }) => theme.breakpoints.web}) {
grid-template-columns: 1fr 1fr 1.5fr 1fr 1.2fr;
grid-template-columns: ${({ hasInterview }) =>
hasInterview ? '1fr 1fr 1.5fr 1fr 1.2fr' : '1fr 1fr 1.5fr 1fr'};

& > p:nth-of-type(3) {
display: none;
}
}

@media (max-width: 768px) {
grid-template-columns: 1fr 1.5fr 1fr 1.2fr;
grid-template-columns: ${({ hasInterview }) =>
hasInterview ? '1fr 1.5fr 1fr 1.2fr' : '1fr 1.5fr 1fr'};

& > p:nth-of-type(2) {
display: none;
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
};

Expand All @@ -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<HTMLButtonElement>(null);

const handleTimeSetterClick = (e: React.MouseEvent) => {
e.stopPropagation();
// openModal();
openModal();
};

return (
<>
<S.ItemWrapper onClick={() => onClick(applicantId)}>
<S.ItemWrapper hasInterview={interviewRequired} onClick={() => onClick(applicantId)}>
<S.InfoText>{name || '-'}</S.InfoText>
<S.InfoText>{studentId || '-'}</S.InfoText>
<S.InfoText>{department || '-'}</S.InfoText>
<S.InfoText>{phoneNumber || '-'}</S.InfoText>
<S.InfoText>{email || '-'}</S.InfoText>
<S.StatusBadge status={status}>{STATUS_LABEL[status] || '-'}</S.StatusBadge>
{status === 'APPROVED' && (
{interviewRequired && status === 'APPROVED' && (
<S.InfoText>
{confirmedTime ? (
<Text color='#8C8C8C' size='lg'>
Expand All @@ -56,7 +58,7 @@ export const ApplicantListItem = React.memo(function ApplicantListItem({
</S.InfoText>
)}
</S.ItemWrapper>
{/* <Modal
<Modal
isOpen={isOpen}
onClose={closeModal}
title='동아리 면접 공지 일정'
Expand All @@ -68,7 +70,7 @@ export const ApplicantListItem = React.memo(function ApplicantListItem({
interviewInfo={interviewInfo}
interviewSchedule={interviewSchedule}
/>
</Modal> */}
</Modal>
</>
);
});
5 changes: 5 additions & 0 deletions src/pages/admin/Dashboard/hooks/useApplicants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T> extends UseApiQueryResult<T> {
counts: ApplicantCounts;
interviewRequired: boolean;
interviewSchedule: InterviewSchedule[];
}

export const useApplicants = (
Expand Down Expand Up @@ -60,5 +63,7 @@ export const useApplicants = (
isLoading,
error,
counts,
interviewRequired: responseData?.interviewRequired ?? false,
interviewSchedule: responseData?.interviewSchedule || [],
};
};
6 changes: 3 additions & 3 deletions src/pages/admin/Dashboard/types/dashboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -63,6 +62,7 @@ export type InterviewSchedule = {
};

export type ApplicantsApiResponse = {
interviewRequired: boolean;
applicants: ApplicantData[];
interviewSchedule: InterviewSchedule[];
message: string | null;
Expand Down
Loading