diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 4807b07e3..71c0e5e0d 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -17,6 +17,8 @@ import LoginTab from '@/pages/AdminPage/auth/LoginTab/LoginTab';
import PrivateRoute from '@/pages/AdminPage/auth/PrivateRoute/PrivateRoute';
import PhotoEditTab from '@/pages/AdminPage/tabs/PhotoEditTab/PhotoEditTab';
import ApplicationFormPage from './pages/ApplicationFormPage/ApplicationFormPage';
+import ApplicantsTab from './pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab';
+import ApplicantDetailPage from './pages/AdminPage/tabs/ApplicantsTab/ApplicantDetailPage';
const queryClient = new QueryClient();
@@ -76,6 +78,14 @@ const App = () => {
path='application-edit'
element={}
/>
+ }
+ />
+ }
+ />
diff --git a/frontend/src/apis/applicants/getClubApplicants.ts b/frontend/src/apis/applicants/getClubApplicants.ts
new file mode 100644
index 000000000..4d71c43ec
--- /dev/null
+++ b/frontend/src/apis/applicants/getClubApplicants.ts
@@ -0,0 +1,20 @@
+import API_BASE_URL from '@/constants/api';
+import { secureFetch } from '../auth/secureFetch';
+
+const getClubApplicants = async (clubId: string) => {
+ try {
+ const response = await secureFetch(`${API_BASE_URL}/api/club/${clubId}/apply/info`);
+ if (!response.ok) {
+ console.error(`Failed to fetch: ${response.statusText}`)
+ throw new Error((await response.json()).message);
+ }
+
+ const result = await response.json();
+ return result.data;
+ } catch (error) {
+ console.error('Error fetching club applicants', error);
+ throw error;
+ }
+};
+
+export default getClubApplicants;
diff --git a/frontend/src/components/common/SearchBox/SearchBox.tsx b/frontend/src/components/common/SearchBox/SearchBox.tsx
index 5d98a9091..be9c5917f 100644
--- a/frontend/src/components/common/SearchBox/SearchBox.tsx
+++ b/frontend/src/components/common/SearchBox/SearchBox.tsx
@@ -1,5 +1,6 @@
import { useRef, useState } from 'react';
import { useSearch } from '@/context/SearchContext';
+import { useCategory } from '@/context/CategoryContext';
import useMixpanelTrack from '@/hooks/useMixpanelTrack';
import * as Styled from './SearchBox.styles';
import SearchIcon from '@/assets/images/icons/search_button_icon.svg';
@@ -7,7 +8,8 @@ import { useLocation, useNavigate } from 'react-router-dom';
const SearchBox = () => {
const [isSearchBoxClicked, setIsSearchBoxClicked] = useState(false);
- const { setKeyword, inputValue, setInputValue } = useSearch();
+ const { setKeyword, inputValue, setInputValue, setIsSearching } = useSearch();
+ const { setSelectedCategory } = useCategory();
const trackEvent = useMixpanelTrack();
const navigate = useNavigate();
const location = useLocation();
@@ -23,6 +25,8 @@ const SearchBox = () => {
const handleSearch = () => {
redirectToHome();
setKeyword(inputValue);
+ setSelectedCategory('all');
+ setIsSearching(true);
inputRef.current?.blur();
@@ -40,7 +44,8 @@ const SearchBox = () => {
return (
+ onSubmit={handleSubmit}
+ >
{
+ aria-label='검색'
+ >
diff --git a/frontend/src/context/AdminClubContext.tsx b/frontend/src/context/AdminClubContext.tsx
index 23c526eb6..aaf87f2cc 100644
--- a/frontend/src/context/AdminClubContext.tsx
+++ b/frontend/src/context/AdminClubContext.tsx
@@ -1,12 +1,15 @@
import { createContext, useContext, useState } from 'react';
+import { ApplicantsInfo } from '@/types/applicants';
interface AdminClubContextType {
clubId: string | null;
setClubId: (id: string | null) => void;
+ applicantsData: ApplicantsInfo | null;
+ setApplicantsData: (data: ApplicantsInfo | null) => void;
}
const AdminClubContext = createContext(
- undefined,
+ undefined
);
export const AdminClubProvider = ({
@@ -15,9 +18,10 @@ export const AdminClubProvider = ({
children: React.ReactNode;
}) => {
const [clubId, setClubId] = useState(null);
+ const [applicantsData, setApplicantsData] = useState(null);
return (
-
+
{children}
);
diff --git a/frontend/src/context/SearchContext.tsx b/frontend/src/context/SearchContext.tsx
index d20af1f39..24296326f 100644
--- a/frontend/src/context/SearchContext.tsx
+++ b/frontend/src/context/SearchContext.tsx
@@ -5,6 +5,8 @@ interface SearchContextType {
setKeyword: (keyword: string) => void;
inputValue: string;
setInputValue: (value: string) => void;
+ isSearching: boolean;
+ setIsSearching: (isSearching: boolean) => void;
}
interface SearchProviderProps {
@@ -16,6 +18,7 @@ const SearchContext = createContext(undefined);
export const SearchProvider = ({ children }: SearchProviderProps) => {
const [keyword, setKeyword] = useState('');
const [inputValue, setInputValue] = useState('');
+ const [isSearching, setIsSearching] = useState(false);
return (
{
setKeyword,
inputValue,
setInputValue,
- }}>
+ isSearching,
+ setIsSearching,
+ }}
+ >
{children}
);
diff --git a/frontend/src/hooks/queries/applicants/useGetApplicants.ts b/frontend/src/hooks/queries/applicants/useGetApplicants.ts
new file mode 100644
index 000000000..38c7da2f6
--- /dev/null
+++ b/frontend/src/hooks/queries/applicants/useGetApplicants.ts
@@ -0,0 +1,10 @@
+import getClubApplicants from "@/apis/applicants/getClubApplicants"
+import { useQuery } from "@tanstack/react-query"
+
+export const useGetApplicants = (clubId: string) => {
+ return useQuery({
+ queryKey: ['clubApplicants', clubId],
+ queryFn: () => getClubApplicants(clubId),
+ retry: false,
+ })
+}
\ No newline at end of file
diff --git a/frontend/src/pages/AdminPage/auth/PrivateRoute/PrivateRoute.tsx b/frontend/src/pages/AdminPage/auth/PrivateRoute/PrivateRoute.tsx
index 2c787f3f0..79563ed5e 100644
--- a/frontend/src/pages/AdminPage/auth/PrivateRoute/PrivateRoute.tsx
+++ b/frontend/src/pages/AdminPage/auth/PrivateRoute/PrivateRoute.tsx
@@ -3,15 +3,25 @@ import useAuth from '@/hooks/useAuth';
import { Navigate } from 'react-router-dom';
import { useAdminClubContext } from '@/context/AdminClubContext';
import Spinner from '@/components/common/Spinner/Spinner';
+import { useGetApplicants } from '@/hooks/queries/applicants/useGetApplicants';
const PrivateRoute = ({ children }: { children: React.ReactNode }) => {
const { isLoading, isAuthenticated, clubId } = useAuth();
- const { setClubId } = useAdminClubContext();
+ const { setClubId, setApplicantsData } = useAdminClubContext();
+ const { data: applicantsData } = useGetApplicants(clubId ?? '');
useEffect(() => {
- if (clubId) setClubId(clubId);
+ if (clubId) {
+ setClubId(clubId);
+ }
}, [clubId, setClubId]);
+ useEffect(() => {
+ if (clubId && applicantsData) {
+ setApplicantsData(applicantsData);
+ }
+ }, [clubId, applicantsData]);
+
if (isLoading) return ;
if (!isAuthenticated) return ;
diff --git a/frontend/src/pages/AdminPage/components/SideBar/SideBar.styles.ts b/frontend/src/pages/AdminPage/components/SideBar/SideBar.styles.ts
index 14911debc..26530f5e8 100644
--- a/frontend/src/pages/AdminPage/components/SideBar/SideBar.styles.ts
+++ b/frontend/src/pages/AdminPage/components/SideBar/SideBar.styles.ts
@@ -7,7 +7,6 @@ export const SidebarWrapper = styled.aside`
word-wrap: break-word;
overflow-wrap: break-word;
white-space: normal;
-
width: 168px;
`;
diff --git a/frontend/src/pages/AdminPage/components/SideBar/SideBar.tsx b/frontend/src/pages/AdminPage/components/SideBar/SideBar.tsx
index f160cd25a..24f8270e0 100644
--- a/frontend/src/pages/AdminPage/components/SideBar/SideBar.tsx
+++ b/frontend/src/pages/AdminPage/components/SideBar/SideBar.tsx
@@ -14,7 +14,8 @@ const tabs = [
{ label: '기본 정보 수정', path: '/admin/club-info' },
{ label: '모집 정보 수정', path: '/admin/recruit-edit' },
{ label: '활동 사진 수정', path: '/admin/photo-edit' },
- { label: '지원 관리', path: '/admin/application-edit' },
+ { label: '지원서 관리', path: '/admin/application-edit' },
+ { label: '지원자 관리', path: '/admin/applicants' },
{ label: '계정 관리', path: '/admin/account-edit' },
];
@@ -40,7 +41,9 @@ const SideBar = ({ clubLogo, clubName }: SideBarProps) => {
if (!confirmed) return;
try {
- await logout();
+ if (document.cookie.split(';').some((cookie) => cookie.trim().startsWith('refreshToken='))) {
+ await logout();
+ }
localStorage.removeItem('accessToken');
navigate('/admin/login', { replace: true });
} catch (error) {
diff --git a/frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantDetailPage.tsx b/frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantDetailPage.tsx
new file mode 100644
index 000000000..32395e9b7
--- /dev/null
+++ b/frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantDetailPage.tsx
@@ -0,0 +1,85 @@
+import React from 'react';
+import { useParams, useNavigate } from 'react-router-dom';
+import { useAdminClubContext } from '@/context/AdminClubContext';
+import Header from '@/components/common/Header/Header';
+import { PageContainer } from '@/styles/PageContainer.styles';
+import * as Styled from '@/pages/ApplicationFormPage/ApplicationFormPage.styles';
+import QuestionContainer from '@/pages/ApplicationFormPage/components/QuestionContainer/QuestionContainer';
+import QuestionAnswerer from '@/pages/ApplicationFormPage/components/QuestionAnswerer/QuestionAnswerer';
+import { useGetApplication } from '@/hooks/queries/application/useGetApplication';
+import Spinner from '@/components/common/Spinner/Spinner';
+import backButtonIcon from '@/assets/images/icons/back_button_icon.svg';
+
+
+const ApplicantDetailPage = () => {
+ const { questionId } = useParams<{ questionId: string }>();
+ const navigate = useNavigate();
+ const { applicantsData, clubId } = useAdminClubContext();
+
+ // 지원서 질문 목록 fetch
+ const { data: formData, isLoading, isError } = useGetApplication(clubId!);
+
+ if (!applicantsData) {
+ return 지원자 데이터를 불러올 수 없습니다.
;
+ }
+ if (isLoading) return ;
+ if (isError || !formData) return 지원서 정보를 불러올 수 없습니다.
;
+
+ // questionId로 지원자 찾기
+ const applicant = applicantsData.applicants.find(
+ (a) => a.id === questionId
+ );
+ if (!applicant) {
+ return 해당 지원자를 찾을 수 없습니다.
;
+ }
+
+ // 답변 매핑 함수
+ const getAnswerByQuestionId = (qId: number) => {
+ return applicant.answers
+ .filter((ans) => ans.id === qId)
+ .map((ans) => ans.value);
+ };
+
+ return (
+ <>
+
+
+ {/* FormTitle과 백아이콘을 한 줄에 배치 */}
+
+
+
+ {/* 커서 고정 */}
+
+ {formData.questions.map((q: import('@/types/application').Question, i: number) => (
+
+ {}}
+ />
+
+ ))}
+
+
+ >
+ );
+};
+
+export default ApplicantDetailPage;
\ No newline at end of file
diff --git a/frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.styles.tsx b/frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.styles.tsx
new file mode 100644
index 000000000..b2bbe8a0e
--- /dev/null
+++ b/frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.styles.tsx
@@ -0,0 +1,136 @@
+import styled from 'styled-components';
+
+export const ApplicationHeader = styled.div`
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: 32px;
+`;
+
+export const ApplicationTitle = styled.h2`
+ font-size: 28px;
+ font-weight: 700;
+ margin: 0;
+`;
+
+export const SemesterSelect = styled.select`
+ padding: 8px 16px;
+ border-radius: 8px;
+ border: 1px solid #ddd;
+ background: #fff;
+ font-size: 16px;
+`;
+
+// 지원현황
+export const SummaryWrapper = styled.div`
+ display: flex;
+ gap: 12px;
+ margin-bottom: 40px;
+`;
+
+export const SummaryCard = styled.div<{ bgColor: string }>`
+ flex: 1;
+ background: ${({ bgColor }) => bgColor};
+ border-radius: 10px;
+ padding: 32px 0;
+ text-align: center;
+`;
+
+export const SummaryLabel = styled.div`
+ font-size: 18px;
+ color: #888;
+`;
+
+export const SummaryValue = styled.div`
+ font-size: 40px;
+ font-weight: 700;
+ margin-top: 8px;
+`;
+
+export const SummaryPeople = styled.span`
+ font-size: 20px;
+ font-weight: 400;
+ margin-left: 2px;
+`;
+
+// 지원자 목록 스타일
+export const ApplicantListWrapper = styled.div``;
+
+export const ApplicantListTitle = styled.h2`
+ font-size: 28px;
+ font-weight: 700;
+ margin-bottom: 24px;
+`;
+
+export const ApplicantListHeader = styled.div`
+ display: flex;
+ align-items: center;
+ margin-bottom: 16px;
+ gap: 8px;
+`;
+
+export const ApplicantFilterSelect = styled.select`
+ padding: 8px 16px;
+ border-radius: 8px;
+ border: 1px solid #ddd;
+ background: #fff;
+ font-size: 16px;
+`;
+
+export const ApplicantSearchBox = styled.input`
+ margin-left: auto;
+ padding: 8px 16px;
+ border-radius: 8px;
+ border: 1px solid #ddd;
+ width: 240px;
+ font-size: 16px;
+`;
+
+export const ApplicantTable = styled.table`
+ width: 100%;
+ border-collapse: collapse;
+ background: #fff;
+`;
+
+export const ApplicantTableHeaderWrapper = styled.thead`
+ background: #fafafa;
+`;
+
+export const ApplicantTableHeader = styled.th`
+ background: #fafafa;
+ padding: 12px 8px;
+ font-size: 16px;
+ font-weight: 500;
+ color: #888;
+ text-align: center;
+`;
+
+export const ApplicantTableRow = styled.tr`
+ border-bottom: 1px solid #f0f0f0;
+ &:hover {
+ background: #f7faff;
+ }
+ text-align: center;
+`;
+
+export const ApplicantTableCol = styled.td`
+ padding: 12px 8px;
+ font-size: 16px;
+`;
+
+export const ApplicantStatusBadge = styled.span<{ status: string }>`
+ display: inline-block;
+ border-radius: 8px;
+ padding: 4px 12px;
+ font-weight: 500;
+ font-size: 15px;
+ background: ${({ status }) =>
+ status === '서류검토'
+ ? '#E6F4FB'
+ : status === '면접예정'
+ ? '#E6FBF0'
+ : status === '합격'
+ ? '#F5F5F5'
+ : '#eee'};
+ color: ${({ status }) => (status === '합격' ? '#888' : '#222')};
+`;
diff --git a/frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx b/frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx
new file mode 100644
index 000000000..907ba36a7
--- /dev/null
+++ b/frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx
@@ -0,0 +1,153 @@
+import { useAdminClubContext } from '@/context/AdminClubContext';
+import { Applicant } from '@/types/applicants';
+import React from 'react';
+import * as Styled from './ApplicantsTab.styles';
+import { useNavigate } from 'react-router-dom';
+
+function applicationStatusMapping(status: Applicant['status']): string {
+ switch (status) {
+ case 'DRAFT':
+ case 'SUBMITTED':
+ case 'SCREENING':
+ return '서류검토';
+ case 'SCREENING_PASSED':
+ case 'INTERVIEW_SCHEDULED':
+ case 'INTERVIEW_IN_PROGRESS':
+ return '면접예정';
+ case 'INTERVIEW_PASSED':
+ case 'OFFERED':
+ case 'ACCEPTED':
+ return '합격';
+ default:
+ return '';
+ }
+}
+
+const ApplicantsTab = () => {
+ const navigate = useNavigate();
+ const { clubId, applicantsData } = useAdminClubContext();
+ if (!clubId) return null;
+
+ return (
+ <>
+
+ 지원 현황
+ {/*
+
+ ...다른 학기 */
+ /*{' '}
+ */}
+
+
+
+
+ 전체 지원자 수
+
+ {applicantsData?.total}
+ 명
+
+
+
+ 서류 검토 필요
+
+ {applicantsData?.reviewRequired}
+ 명
+
+
+
+ 면접 예정
+
+ {applicantsData?.scheduledInterview}
+ 명
+
+
+
+ 합격
+
+ {applicantsData?.accepted}
+ 명
+
+
+
+
+
+ 지원자 목록
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 현재상태
+
+
+ 이름
+
+ 메모
+
+ 제출날짜
+
+
+
+
+ {applicantsData?.applicants.map(
+ (item: Applicant, index: number) => (
+
+ navigate(`/admin/applicants/${item.id}`)
+ }
+ >
+
+ ) =>
+ e.stopPropagation()
+ }
+ />
+
+
+ {applicationStatusMapping(item.status)}
+
+
+ {item.answers[0].value}
+
+
+ {item.memo}
+
+
+ {
+ // createdAt을 yyyy-mm-dd 형식으로 변환
+ // 임시로.. 나중에 변경해야함
+ (() => {
+ const date = new Date(item.createdAt);
+ const year = date.getFullYear();
+ const month = String(date.getMonth() + 1).padStart(2, '0');
+ const day = String(date.getDate()).padStart(2, '0');
+ return `${year}-${month}-${day}`;
+ })()
+ }
+
+
+ ),
+ )}
+
+
+
+ >
+ );
+};
+
+export default ApplicantsTab;
diff --git a/frontend/src/pages/ClubDetailPage/components/InfoBox/InfoBox.tsx b/frontend/src/pages/ClubDetailPage/components/InfoBox/InfoBox.tsx
index a922e1194..104ffe404 100644
--- a/frontend/src/pages/ClubDetailPage/components/InfoBox/InfoBox.tsx
+++ b/frontend/src/pages/ClubDetailPage/components/InfoBox/InfoBox.tsx
@@ -40,7 +40,12 @@ const InfoBox = ({ sectionRefs, clubDetail }: InfoBoxProps) => {
{ label: '전화번호', value: clubDetail.presidentPhoneNumber },
{
label: 'SNS',
- render: ,
+ render: (
+
+ ),
},
],
refIndex: INFOTABS_SCROLL_INDEX.CLUB_INFO_TAB,
diff --git a/frontend/src/pages/ClubDetailPage/components/ShareButton/ShareButton.tsx b/frontend/src/pages/ClubDetailPage/components/ShareButton/ShareButton.tsx
index eea5ea29e..935ab62ba 100644
--- a/frontend/src/pages/ClubDetailPage/components/ShareButton/ShareButton.tsx
+++ b/frontend/src/pages/ClubDetailPage/components/ShareButton/ShareButton.tsx
@@ -43,7 +43,7 @@ const ShareButton = ({ clubId }: ShareButtonProps) => {
},
],
});
- trackEvent(`${clubDetail.name} 공유하기 버튼 클릭`);
+ trackEvent('공유하기 버튼 클릭', { clubName: clubDetail.name });
};
return (
diff --git a/frontend/src/pages/ClubDetailPage/components/SnsLinkIcons/SnsLinkIcons.tsx b/frontend/src/pages/ClubDetailPage/components/SnsLinkIcons/SnsLinkIcons.tsx
index 103cb50f0..4ca845bc1 100644
--- a/frontend/src/pages/ClubDetailPage/components/SnsLinkIcons/SnsLinkIcons.tsx
+++ b/frontend/src/pages/ClubDetailPage/components/SnsLinkIcons/SnsLinkIcons.tsx
@@ -2,12 +2,16 @@ import React from 'react';
import * as Styled from './SnsLinkIcons.styles';
import { SNS_CONFIG } from '@/constants/snsConfig';
import { SNSPlatform } from '@/types/club';
+import useMixpanelTrack from '@/hooks/useMixpanelTrack';
interface SnsLinkIconsProps {
apiSocialLinks: Partial>;
+ clubName?: string;
}
-const SnsLinkIcons = ({ apiSocialLinks }: SnsLinkIconsProps) => {
+const SnsLinkIcons = ({ apiSocialLinks, clubName }: SnsLinkIconsProps) => {
+ const trackEvent = useMixpanelTrack();
+
if (!apiSocialLinks) return null;
return (
@@ -21,6 +25,12 @@ const SnsLinkIcons = ({ apiSocialLinks }: SnsLinkIconsProps) => {
href={url}
target='_blank'
rel='noreferrer'
+ onClick={() =>
+ trackEvent('sns링크 버튼 클릭', {
+ platform,
+ clubName,
+ })
+ }
>
diff --git a/frontend/src/pages/MainPage/MainPage.tsx b/frontend/src/pages/MainPage/MainPage.tsx
index c49dae1be..0f8857380 100644
--- a/frontend/src/pages/MainPage/MainPage.tsx
+++ b/frontend/src/pages/MainPage/MainPage.tsx
@@ -21,15 +21,16 @@ const MainPage = () => {
const [isFilterActive, setIsFilterActive] = useState(false);
const { selectedCategory, setSelectedCategory } = useCategory();
- const { keyword } = useSearch();
+ const { keyword, isSearching } = useSearch();
const recruitmentStatus = isFilterActive ? 'OPEN' : 'all';
const division = 'all';
+ const searchCategory = isSearching ? 'all' : selectedCategory;
const {
data: clubs,
error,
isLoading,
- } = useGetCardList(keyword, recruitmentStatus, division, selectedCategory);
+ } = useGetCardList(keyword, recruitmentStatus, division, searchCategory);
const isEmpty = !isLoading && (!clubs || clubs.length === 0);
const hasData = clubs && clubs.length > 0;
diff --git a/frontend/src/pages/MainPage/components/CategoryButtonList/CategoryButtonList.tsx b/frontend/src/pages/MainPage/components/CategoryButtonList/CategoryButtonList.tsx
index cdaeafaff..b5a568108 100644
--- a/frontend/src/pages/MainPage/components/CategoryButtonList/CategoryButtonList.tsx
+++ b/frontend/src/pages/MainPage/components/CategoryButtonList/CategoryButtonList.tsx
@@ -58,7 +58,7 @@ const clubCategories: Category[] = [
];
const CategoryButtonList = () => {
- const { setKeyword, setInputValue } = useSearch();
+ const { setKeyword, setInputValue, setIsSearching } = useSearch();
const { setSelectedCategory } = useCategory();
const handleCategoryClick = (category: Category) => {
@@ -71,9 +71,9 @@ const CategoryButtonList = () => {
setKeyword('');
setInputValue('');
+ setIsSearching(false);
setSelectedCategory(category.id);
-
};
return (
diff --git a/frontend/src/types/applicants.ts b/frontend/src/types/applicants.ts
new file mode 100644
index 000000000..b91fec312
--- /dev/null
+++ b/frontend/src/types/applicants.ts
@@ -0,0 +1,33 @@
+import { AnswerItem } from "./application";
+
+export enum ApplicationStatus {
+ DRAFT = 'DRAFT', // 작성 중
+ SUBMITTED = 'SUBMITTED', // 제출 완료
+ SCREENING = 'SCREENING', // 서류 심사 중
+ SCREENING_PASSED = 'SCREENING_PASSED', // 서류 통과
+ SCREENING_FAILED = 'SCREENING_FAILED', // 서류 탈락
+ INTERVIEW_SCHEDULED = 'INTERVIEW_SCHEDULED', // 면접 일정 확정
+ INTERVIEW_IN_PROGRESS = 'INTERVIEW_IN_PROGRESS', // 면접 진행 중
+ INTERVIEW_PASSED = 'INTERVIEW_PASSED', // 면접 통과
+ INTERVIEW_FAILED = 'INTERVIEW_FAILED', // 면접 탈락
+ OFFERED = 'OFFERED', // 최종 합격 제안
+ ACCEPTED = 'ACCEPTED', // 제안 수락
+ DECLINED = 'DECLINED', // 제안 거절
+ CANCELED_BY_APPLICANT = 'CANCELED_BY_APPLICANT', // 지원자 자진 철회
+}
+
+export interface ApplicantsInfo {
+ total: number;
+ reviewRequired: number;
+ scheduledInterview: number;
+ accepted: number;
+ applicants: Applicant[]
+}
+
+export interface Applicant {
+ id: string;
+ status: ApplicationStatus;
+ answers: AnswerItem[]
+ memo: string;
+ createdAt: string;
+}
\ No newline at end of file