Skip to content
25 changes: 12 additions & 13 deletions frontend/src/context/AdminClubContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,10 @@ interface AdminClubContextType {
setClubId: (id: string | null) => void;
applicantsData: ApplicantsInfo | null;
setApplicantsData: (data: ApplicantsInfo | null) => void;
applicationFormId: string | null;
setApplicationFormId: (id: string | null) => void;
}

const AdminClubContext = createContext<AdminClubContextType | undefined>(
undefined
undefined,
);

export const AdminClubProvider = ({
Expand All @@ -20,18 +18,19 @@ export const AdminClubProvider = ({
children: React.ReactNode;
}) => {
const [clubId, setClubId] = useState<string | null>(null);
const [applicantsData, setApplicantsData] = useState<ApplicantsInfo | null>(null);
const [applicationFormId, setApplicationFormId] = useState<string | null>(null);
const [applicantsData, setApplicantsData] = useState<ApplicantsInfo | null>(
null,
);

return (
<AdminClubContext.Provider value={{
clubId,
setClubId,
applicantsData,
setApplicantsData,
applicationFormId,
setApplicationFormId,
}}>
<AdminClubContext.Provider
value={{
clubId,
setClubId,
applicantsData,
setApplicantsData,
}}
>
{children}
</AdminClubContext.Provider>
);
Expand Down
4 changes: 4 additions & 0 deletions frontend/src/pages/AdminPage/AdminRoutes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ export default function AdminRoutes() {
<Route path='photo-edit' element={<PhotoEditTab />} />
<Route path='account-edit' element={<AccountEditTab />} />
<Route path='application-list' element={<ApplicationListTab />} />
<Route
path='application-list/:applicationFormId/edit'
element={<ApplicationEditTab />}
/>
<Route path='application-list/edit' element={<ApplicationEditTab />} />
<Route path='applicants-list' element={<ApplicantsListTab />} />
<Route
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,17 @@ const getStatusColor = (status: ApplicationStatus | undefined): string => {
};

const ApplicantDetailPage = () => {
const { questionId } = useParams<{ questionId: string }>();
const { questionId, applicationFormId } = useParams<{
questionId: string;
applicationFormId: string;
}>();

const navigate = useNavigate();
const [applicantMemo, setAppMemo] = useState('');
const [applicantStatus, setApplicantStatus] = useState<ApplicationStatus>(
ApplicationStatus.SUBMITTED,
);
const { applicantsData, clubId, applicationFormId } = useAdminClubContext();
const { applicantsData, clubId } = useAdminClubContext();

const applicantIndex =
applicantsData?.applicants.findIndex((a) => a.id === questionId) ?? -1;
Expand All @@ -49,7 +53,9 @@ const ApplicantDetailPage = () => {
isLoading,
isError,
} = useGetApplication(clubId!, applicationFormId ?? undefined);
const { mutate: updateApplicant } = useUpdateApplicant(applicationFormId ?? undefined);
const { mutate: updateApplicant } = useUpdateApplicant(
applicationFormId ?? undefined,
);

useEffect(() => {
if (applicant) {
Expand Down Expand Up @@ -144,7 +150,9 @@ const ApplicantDetailPage = () => {
<select
id='applicantSelect'
value={applicant.id}
onChange={(e) => navigate(`/admin/applicants-lsit/${e.target.value}`)}
onChange={(e) =>
navigate(`/admin/applicants-list/${e.target.value}`)
}
>
{applicantsData.applicants.map((a) => (
<option key={a.id} value={a.id}>
Expand Down Expand Up @@ -184,7 +192,7 @@ const ApplicantDetailPage = () => {

<Styled.ApplicantInfoContainer>
<Styled.QuestionsWrapper style={{ cursor: 'default' }}>
{formData.questions.map((q: Question, i: number) => (
{formData.questions?.map((q: Question, i: number) => (
<QuestionContainer key={q.id} hasError={false}>
<QuestionAnswerer
question={q}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,25 @@ import React, { useState, useRef, useEffect } from 'react';
import ApplicationMenu from '../../ApplicationListTab/ApplicationMenu';
import { useGetApplicationlist } from '@/hooks/queries/application/useGetApplicationlist';
import Spinner from '@/components/common/Spinner/Spinner';
import { useAdminClubContext } from '@/context/AdminClubContext';
// import { useDeleteApplication } from '@/hooks/queries/application/useDeleteApplication';
import { ApplicationFormItem, SemesterGroup } from '@/types/application';

const ApplicationListTab = () => {
const {data: allforms, isLoading, isError, error} = useGetApplicationlist();
const { data: allforms, isLoading, isError, error } = useGetApplicationlist();
const navigate = useNavigate();
const { setApplicationFormId } = useAdminClubContext();
// const { mutate: deleteApplication } = useDeleteApplication();

const handleGoToNewForm = () => {
setApplicationFormId(null);
navigate('/admin/application-list/edit');
};
const handleGoToDetailForm = (applicationFormId: string) => {
setApplicationFormId(applicationFormId);
navigate(`/admin/applicants-list/${applicationFormId}`);
}
};

/**
* @TODO
* 백엔드 지원서 삭제 API 구현 후 주석 해제
*/
// const handleDeleteApplication = (applicationFormId: string) => {
// // 사용자에게 재확인
// if (window.confirm('지원서 양식을 정말 삭제하시겠습니까?\n삭제된 양식은 복구할 수 없습니다.')) {
Expand Down Expand Up @@ -78,7 +78,10 @@ const ApplicationListTab = () => {
const formatDateTime = (dateTimeString: string) => {
const now = new Date();
const date = new Date(dateTimeString);
const isToday = now.getFullYear() === date.getFullYear() && now.getMonth() === date.getMonth() && now.getDate() === date.getDate();
const isToday =
now.getFullYear() === date.getFullYear() &&
now.getMonth() === date.getMonth() &&
now.getDate() === date.getDate();
if (isToday) {
// [오늘 날짜인 경우] 시간만 표시
return date.toLocaleString('ko-KR', {
Expand All @@ -105,49 +108,56 @@ const ApplicationListTab = () => {
</Styled.AddButton>
</Styled.Header>
{semesterGroups.map((group: SemesterGroup) => {
const semesterTermLabel = group.semesterTerm === 'FIRST' ? '1학기' : '2학기';
const semesterTermLabel =
group.semesterTerm === 'FIRST' ? '1학기' : '2학기';
const semesterTitle = `${group.semesterYear}년 ${semesterTermLabel}`;
return (
<Styled.ApplicationList key={semesterTitle}>
<Styled.ListHeader>
<Styled.SemesterTitle>{semesterTitle}</Styled.SemesterTitle>
<Styled.DateHeader>
<Styled.Separation_Bar />
최종 수정 날짜
</Styled.DateHeader>
</Styled.ListHeader>
{(group.forms.map((application: ApplicationFormItem) => {
const isActive = application.status === 'ACTIVE';
return (
<Styled.ApplicationRow key={application.id}>
<Styled.ApplicationTitle $active={isActive} onClick={() => handleGoToDetailForm(application.id)}>
{application.title}
</Styled.ApplicationTitle>
<Styled.ApplicationDatetable>
<Styled.ApplicationDate>
{formatDateTime(application.editedAt)}
</Styled.ApplicationDate>
<Styled.MoreButtonContainer
ref={openMenuId === application.id ? menuRef : null}
>
<Styled.MoreButton
onClick={(e) => handleMoreButtonClick(e, application.id)}
<Styled.ApplicationList key={semesterTitle}>
<Styled.ListHeader>
<Styled.SemesterTitle>{semesterTitle}</Styled.SemesterTitle>
<Styled.DateHeader>
<Styled.Separation_Bar />
최종 수정 날짜
</Styled.DateHeader>
</Styled.ListHeader>
{group.forms.map((application: ApplicationFormItem) => {
const isActive = application.status === 'ACTIVE';
return (
<Styled.ApplicationRow key={application.id}>
<Styled.ApplicationTitle
$active={isActive}
onClick={() => handleGoToDetailForm(application.id)}
>
<Styled.MoreButtonIcon src={Morebutton} />
</Styled.MoreButton>
{openMenuId === application.id && (
<ApplicationMenu
isActive={isActive}
// onDelete={() => handleDeleteApplication(application.id)}
/>
)}
</Styled.MoreButtonContainer>
</Styled.ApplicationDatetable>
</Styled.ApplicationRow>
);
}))}
</Styled.ApplicationList>
)})}
{application.title}
</Styled.ApplicationTitle>
<Styled.ApplicationDatetable>
<Styled.ApplicationDate>
{formatDateTime(application.editedAt)}
</Styled.ApplicationDate>
<Styled.MoreButtonContainer
ref={openMenuId === application.id ? menuRef : null}
>
<Styled.MoreButton
onClick={(e) =>
handleMoreButtonClick(e, application.id)
}
>
<Styled.MoreButtonIcon src={Morebutton} />
</Styled.MoreButton>
{openMenuId === application.id && (
<ApplicationMenu
isActive={isActive}
// onDelete={() => handleDeleteApplication(application.id)}
/>
)}
</Styled.MoreButtonContainer>
</Styled.ApplicationDatetable>
</Styled.ApplicationRow>
);
})}
</Styled.ApplicationList>
);
})}
</Styled.Container>
);
};
Expand Down
24 changes: 18 additions & 6 deletions frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { useAdminClubContext } from '@/context/AdminClubContext';
import { Applicant, ApplicationStatus } from '@/types/applicants';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import * as Styled from './ApplicantsTab.styles';
import { useNavigate } from 'react-router-dom';
import { useNavigate, useParams } from 'react-router-dom';
import { useDeleteApplicants } from '@/hooks/queries/applicants/useDeleteApplicants';
import SearchField from '@/components/common/SearchField/SearchField';
import mapStatusToGroup from '@/utils/mapStatusToGroup';
Expand All @@ -15,6 +15,8 @@ import { CustomDropDown } from '@/components/common/CustomDropDown/CustomDropDow
import { useGetApplicants } from '@/hooks/queries/applicants/useGetApplicants';

const ApplicantsTab = () => {
const { applicationFormId } = useParams<{ applicationFormId: string }>();

const statusOptions = AVAILABLE_STATUSES.map((status) => ({
value: status,
label: mapStatusToGroup(status).label,
Expand All @@ -36,8 +38,12 @@ const ApplicantsTab = () => {
] as const;

const navigate = useNavigate();
const { clubId, applicantsData, applicationFormId, setApplicantsData } = useAdminClubContext();
const { data: fetchData, isLoading, isError } = useGetApplicants(applicationFormId ?? '');
const { clubId, applicantsData, setApplicantsData } = useAdminClubContext();
const {
data: fetchData,
isLoading,
isError,
} = useGetApplicants(applicationFormId ?? '');
const [keyword, setKeyword] = useState('');
const [checkedItem, setCheckedItem] = useState<Map<string, boolean>>(
new Map(),
Expand All @@ -56,7 +62,7 @@ const ApplicantsTab = () => {
useEffect(() => {
if (fetchData) {
setApplicantsData(fetchData);
}
}
}, [fetchData, setApplicantsData]);

// 모든 드롭다운을 닫는 함수
Expand All @@ -67,7 +73,9 @@ const ApplicantsTab = () => {
if (isSortOpen) setIsSortOpen(false);
};
const { mutate: deleteApplicants } = useDeleteApplicants(clubId!);
const { mutate: updateDetailApplicants } = useUpdateApplicant(applicationFormId ?? '');
const { mutate: updateDetailApplicants } = useUpdateApplicant(
applicationFormId ?? '',
);
const dropdownRef = useRef<Array<HTMLDivElement | null>>([]);

const filteredApplicants = useMemo(() => {
Expand Down Expand Up @@ -479,7 +487,11 @@ const ApplicantsTab = () => {
{filteredApplicants.map((item: Applicant, index: number) => (
<Styled.ApplicantTableRow
key={index}
onClick={() => navigate(`/admin/applicants-list/${applicationFormId}/${item.id}`)}
onClick={() =>
navigate(
`/admin/applicants-list/${applicationFormId}/${item.id}`,
)
}
style={{ cursor: 'pointer' }}
>
<Styled.ApplicantTableCol>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,16 @@ import { updateApplication } from '@/apis/application/updateApplication';
import CustomTextArea from '@/components/common/CustomTextArea/CustomTextArea';
import { APPLICATION_FORM } from '@/constants/APPLICATION_FORM';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useNavigate } from 'react-router-dom';
import { useNavigate, useParams } from 'react-router-dom';
import Button from '@/components/common/Button/Button';

const ApplicationEditTab = () => {
const navigate = useNavigate();
const queryClient = useQueryClient();
const { clubId, applicationFormId: formId } = useAdminClubContext();
const { applicationFormId: formId } = useParams<{
applicationFormId?: string;
}>();
const { clubId } = useAdminClubContext();

Comment on lines 17 to 27
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# First, let's examine ApplicationEditTab.tsx around the mentioned lines
fd -e tsx -e ts "ApplicationEditTab.tsx" --type f | head -5

Repository: Moadong/moadong

Length of output: 134


🏁 Script executed:

#!/bin/bash
# Find and examine the ApplicationEditTab file with line numbers
find . -name "ApplicationEditTab.tsx" -type f 2>/dev/null

Repository: Moadong/moadong

Length of output: 136


🏁 Script executed:

#!/bin/bash
# Search more broadly for the file
rg "ApplicationEditTab" -l --type typescript

Repository: Moadong/moadong

Length of output: 93


🏁 Script executed:

#!/bin/bash
# List files in frontend pages
ls -la frontend/src/pages/AdminPage/tabs/ApplicationEditTab/ 2>/dev/null || echo "Path not found, searching..."
find frontend -name "*ApplicationEditTab*" -type f 2>/dev/null || echo "Not in frontend, searching project root"
find . -name "*ApplicationEditTab*" -type f 2>/dev/null | head -10

Repository: Moadong/moadong

Length of output: 644


🏁 Script executed:

#!/bin/bash
# Read the ApplicationEditTab.tsx file with line numbers
cat -n frontend/src/pages/AdminPage/tabs/ApplicationEditTab/ApplicationEditTab.tsx

Repository: Moadong/moadong

Length of output: 14342


🏁 Script executed:

#!/bin/bash
# Find the useGetApplication hook
find . -name "*useGetApplication*" -type f 2>/dev/null

Repository: Moadong/moadong

Length of output: 186


🏁 Script executed:

#!/bin/bash
# Read the useGetApplication hook
cat -n frontend/src/hooks/queries/application/useGetApplication.ts

Repository: Moadong/moadong

Length of output: 603


🏁 Script executed:

#!/bin/bash
# Search for ApplicantDetailPage
find . -name "*ApplicantDetailPage*" -type f 2>/dev/null

Repository: Moadong/moadong

Length of output: 252


🏁 Script executed:

#!/bin/bash
# Read ApplicantDetailPage.tsx and search for useGetApplication usage
rg -n "useGetApplication" frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantDetailPage/ApplicantDetailPage.tsx -A 3 -B 3

Repository: Moadong/moadong

Length of output: 762


🏁 Script executed:

#!/bin/bash
# Search for all useGetApplication calls in the codebase
rg "useGetApplication\(" -A 1 frontend/src --type ts --type tsx

Repository: Moadong/moadong

Length of output: 86


🏁 Script executed:

#!/bin/bash
# Search for all useGetApplication calls in the codebase
rg "useGetApplication\(" -A 1 frontend/src

Repository: Moadong/moadong

Length of output: 764


🏁 Script executed:

#!/bin/bash
# Check ApplicationFormPage for context
rg -n "useGetApplication" frontend/src/pages/ApplicationFormPage/ApplicationFormPage.tsx -B 5 -A 2

Repository: Moadong/moadong

Length of output: 797


🏁 Script executed:

#!/bin/bash
# Search for AdminRoutes to see if there's a create route without applicationFormId
find . -name "*AdminRoute*" -o -name "*admin*route*" | grep -i route

Repository: Moadong/moadong

Length of output: 105


🏁 Script executed:

#!/bin/bash
# Read AdminRoutes to check route definitions
cat -n frontend/src/pages/AdminPage/AdminRoutes.tsx

Repository: Moadong/moadong

Length of output: 2257


🏁 Script executed:

#!/bin/bash
# Search for mentions of application creation routes or formId defaults
rg "application.*create" frontend/src/pages/AdminPage -i -B 2 -A 2 | head -30

Repository: Moadong/moadong

Length of output: 1448


Based on my verification, I can now provide the rewritten review comment. The original review is correct and well-founded:

Pass formId directly or use undefined consistently with other files for semantic clarity

ApplicationEditTab currently uses formId ?? '' when calling useGetApplication, while ApplicantDetailPage uses applicationFormId ?? undefined. The codebase shows this pattern is inconsistent—ApplicantDetailPage (and the general pattern) prefers undefined to represent absence of a value.

Though the current enabled: !!clubId && !!applicationFormId condition in the hook treats both empty string and undefined identically (both prevent the query), using undefined is semantically clearer and safer for future changes. If the enabled condition or hook logic is modified, an empty string could unintentionally trigger API calls.

-  } = useGetApplication(clubId ?? undefined, formId ?? '');
+  } = useGetApplication(clubId ?? undefined, formId);

When a create route (without applicationFormId in the URL) is added to AdminRoutes, this pattern will already be in place. The hook's enabled condition will then properly gate the query.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In frontend/src/pages/AdminPage/tabs/ApplicationEditTab/ApplicationEditTab.tsx
around lines 17 to 27, the component currently passes formId ?? '' to
useGetApplication which uses an empty string to represent "no id" but the
codebase standard is to use undefined; change the call to pass formId directly
or use formId ?? undefined so absence is represented by undefined, and keep the
hook's enabled condition as !!clubId && !!applicationFormId (or adjust to check
undefined if necessary) to ensure consistent, semantically correct gating of the
query.

const {
data: existingFormData,
Expand Down
Loading