Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
058f4a3
feat: 다중 지원서 모달용 옵션 타입 정의
suhyun113 Oct 11, 2025
7c94b70
feat: 다중 지원서 옵션 목 데이터 추가
suhyun113 Oct 11, 2025
7dcb97b
feat: 다중 지원서 옵션 핸들러 추가
suhyun113 Oct 11, 2025
fef1c3e
feat: 다중 지원서 옵션 목록 조회 api 추가
suhyun113 Oct 11, 2025
d762de6
fix: mock 데이터 키 및 응답 스키마 형식 수정
suhyun113 Oct 11, 2025
8bba6d7
refacter: mock 서버 상수 분리
suhyun113 Oct 11, 2025
87d1757
feat: 모달 공통 컴포넌트 추가
suhyun113 Oct 12, 2025
4440bb8
chore: react-remove-scroll 의존성 추가
suhyun113 Oct 12, 2025
01d9bc3
feat: RemoveScroll 적용
suhyun113 Oct 12, 2025
62aeef4
style: 공통 모달 컴포넌트 스타일 추가
suhyun113 Oct 12, 2025
53ff2b3
feat: 지원서 선택 모달 컴포넌트 추가
suhyun113 Oct 12, 2025
40a5556
style: 지원서 선택 모달 컴포넌트 스타일 추가
suhyun113 Oct 12, 2025
402e906
chore: 목 서버 주소로 임시 변경
suhyun113 Oct 12, 2025
97cd642
feat: 목 데이터 추가
suhyun113 Oct 12, 2025
df2de81
feat: 동아리 지원하기 버튼에 지원서 개수별 동작 분기 추가
suhyun113 Oct 12, 2025
a5f1853
chore: 불필요한 import 제거
suhyun113 Oct 12, 2025
e52fee6
fix: 서버 주소 변경
suhyun113 Nov 2, 2025
73f4db1
feat: 타입 수정
suhyun113 Nov 7, 2025
ab9d09b
feat: 주소 변경 및 반환 형태 변환
suhyun113 Nov 7, 2025
92221f4
feat: 주소 변경 및 파라미터 추가
suhyun113 Nov 7, 2025
f390eb0
feat: 다중 지원서 api 파일명 변경
suhyun113 Nov 7, 2025
66eea33
feat: 예외 처리 보강 및 삼항 연산자 제거
suhyun113 Nov 7, 2025
c3c6d4a
feat: 예외 처리 보강
suhyun113 Nov 7, 2025
919bf61
chore: 파일 이름 수정
suhyun113 Nov 8, 2025
b3d860e
feat: 파라미터 이름 수정
suhyun113 Nov 8, 2025
7fd2b26
feat: 다중 지원서 분기 로직 수정
suhyun113 Nov 8, 2025
e7bfaa1
feat: applicationFormId 추가
suhyun113 Nov 9, 2025
4108f88
fix: 라우터 문제 해결 및 조건 오류 수정 및 사용하지 않는 import 제거
suhyun113 Nov 9, 2025
c6709b0
fix: 목업 파일 임시 수정
suhyun113 Nov 9, 2025
0d39d6b
feat: 모달 onBackdropClick 속성 추가
suhyun113 Nov 9, 2025
ca1462a
fix: 삼항연산자 관심사 분리
suhyun113 Nov 9, 2025
ad58cc7
fix: handleOverlayClick 이동
suhyun113 Nov 9, 2025
f446b1b
fix: 라이브러리 제거 및 useEffect 사용
suhyun113 Nov 9, 2025
c159a23
fix: 모달 닫히지 않도록 수정
suhyun113 Nov 9, 2025
428b637
fix: react-remove-scroll 라이브러리 제거
suhyun113 Nov 9, 2025
bca239a
fix: 모달 열림 후 스크롤 안 되는 문제 수정
suhyun113 Nov 9, 2025
bba7f92
fix: 외부 지원서 분기 오류 수정
suhyun113 Nov 9, 2025
ce3f273
fix: early return 방식으로 수정
suhyun113 Nov 9, 2025
e642dac
fix: 훅 규칙 위반 문제 해결
suhyun113 Nov 9, 2025
a70dbbf
fix: 불필요한 alert 제거
suhyun113 Nov 9, 2025
2014acb
fix: 화면 크기에 따른 모달 크기 수정
suhyun113 Nov 9, 2025
e20db39
fix: 불필요한 매핑 수정
suhyun113 Nov 10, 2025
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
2 changes: 1 addition & 1 deletion frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ const App = () => {
}
/>
<Route
path='/application/:clubId'
path='/application/:clubId/:applicationFormId'
element={<ApplicationFormPage />}
/>
<Route path='/club-union' element={<ClubUnionPage />} />
Expand Down
13 changes: 9 additions & 4 deletions frontend/src/apis/application/getApplication.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import API_BASE_URL from '@/constants/api';

const getApplication = async (clubId: string) => {
const getApplication = async (clubId: string, applicationFormId: string) => {
try {
const response = await fetch(`${API_BASE_URL}/api/club/${clubId}/apply`);
const response = await fetch(`${API_BASE_URL}/api/club/${clubId}/apply/${applicationFormId}`);
if (!response.ok) {
console.error(`Failed to fetch: ${response.statusText}`);
throw new Error((await response.json()).message);
let message = response.statusText;
try {
const errorData = await response.json();
if (errorData?.message) message = errorData.message;
} catch {}
console.error(`Failed to fetch: ${message}`);
throw new Error(message);
}

const result = await response.json();
Expand Down
28 changes: 28 additions & 0 deletions frontend/src/apis/application/getApplicationOptions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import API_BASE_URL from "@/constants/api";

const getApplicationOptions = async (clubId: string) => {
try {
const response = await fetch(`${API_BASE_URL}/api/club/${clubId}/apply`);
if (!response.ok) {
let message = response.statusText;
try {
const errorData = await response.json();
if (errorData?.message) message = errorData.message;
} catch {}
console.error(`Failed to fetch options: ${message}`);
throw new Error(message);
}

const result = await response.json();
let forms: Array<{ id: string; title: string }> = [];
if (result && result.data && Array.isArray(result.data.forms)) {
forms = result.data.forms;
}
return forms;
} catch (error) {
console.error('지원서 옵션 조회 중 오류가 발생했습니다.', error);
throw error;
}
Comment on lines 4 to 25
Copy link
Member

Choose a reason for hiding this comment

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

api마다 매번 try/catch를 하고, 또 에러를 던지는 식으로 구현을 해 왔는데,

이러면 작성해야 할 코드양도 많아지고 에러를 어디서 받는지 알 수 없더라고요.

discussion에서 api 구조에 대해 같이 얘기해보면 좋을 것 같아요

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

좋은 피드백 감사합니다! 말씀하신 부분 공감돼요.
구현할때, 어떻게 하는게 좋을지 몰라서 기존 방식을 최대한 따라갔거든요.
API마다 try/catch로 처리하다 보니 중복 코드가 많고, 에러 핸들링 위치도 애매한 것 같더라구요.

저도 공통 에러 핸들링 로직이나 API 유틸을 따로 만들어서 관리하는 방식으로 개선하면 좋을 것 같아요. discussion에서 구조를 같이 정리해보면 좋겠네요!

};

export default getApplicationOptions;
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import styled from 'styled-components';

export const EmptyMessage = styled.div`
padding: 16px 8px;
color: #9D9D9D;
text-align: center;
font-weight: 600;
`;
Comment on lines +3 to +8
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

매직 컬러 값을 상수로 추출하세요.

Line 5의 #9D9D9D는 매직 값입니다. 코딩 가이드라인에 따라 명명된 상수로 대체해야 합니다.

색상 테마 파일을 생성하거나 기존 테마 파일을 사용하세요:

// theme/colors.ts 또는 유사한 파일에서
export const COLORS = {
  textSecondary: '#9D9D9D',
  // ...
} as const;

그런 다음 스타일에서 사용:

+import { COLORS } from '@/theme/colors';
+
 export const EmptyMessage = styled.div`
   padding: 16px 8px;
-  color: #9D9D9D;
+  color: ${COLORS.textSecondary};
   text-align: center;
   font-weight: 600;
 `;
🤖 Prompt for AI Agents
In frontend/src/components/application/modals/ApplicationSelectModal.styles.ts
around lines 3 to 8, the color literal `#9D9D9D` is a magic value; extract it
into a theme/colors file (e.g., create or update theme/colors.ts exporting a
COLORS object with textSecondary: '#9D9D9D' as const) and replace the literal
with an imported COLORS.textSecondary in the styled component; ensure the new
file is exported and imported with the correct relative path.


export const List = styled.div`
display: grid;
gap: 16px;
`;

export const OptionButton = styled.button`
width: 100%;
padding: 18px 20px;
border-radius: 10px;
border: 1px solid #DCDCDC;
background: #fff;
font-weight: 600;
font-size: 16px;
cursor: pointer;
transition: background-color .15s ease, color .15s ease, border-color .15s ease;

&:hover {
background: #ff7a00;
color: #fff;
border-color: #ff7a00;
}
`;
Comment on lines +15 to +31
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

매직 컬러 값을 상수로 추출하세요.

Lines 19, 27, 29에 하드코딩된 색상 값들(#DCDCDC, #ff7a00)은 매직 값입니다. 코딩 가이드라인에 따라 명명된 상수로 대체해야 합니다.

색상 테마 파일을 생성하거나 기존 테마 파일을 사용하세요:

// theme/colors.ts 또는 유사한 파일에서
export const COLORS = {
  borderGray: '#DCDCDC',
  primary: '#ff7a00',
  white: '#fff',
  // ...
} as const;

그런 다음 스타일에서 사용:

+import { COLORS } from '@/theme/colors';
+
 export const OptionButton = styled.button`
   width: 100%;
   padding: 18px 20px;
   border-radius: 10px;
-  border: 1px solid #DCDCDC;
-  background: #fff;
+  border: 1px solid ${COLORS.borderGray};
+  background: ${COLORS.white};
   font-weight: 600;
   font-size: 16px;
   cursor: pointer;
   transition: background-color .15s ease, color .15s ease, border-color .15s ease;

   &:hover {
-    background: #ff7a00;
-    color: #fff;
-    border-color: #ff7a00;
+    background: ${COLORS.primary};
+    color: ${COLORS.white};
+    border-color: ${COLORS.primary};
   }
 `;
🤖 Prompt for AI Agents
In frontend/src/components/application/modals/ApplicationSelectModal.styles.ts
around lines 15 to 31, replace the hardcoded color literals (#DCDCDC, #ff7a00,
#fff) with named constants from a shared theme/colors file (create one as
theme/colors.ts exporting a COLORS object if it doesn't exist). Import the
COLORS constants at the top of this file and use them for border, background and
hover colors (e.g., COLORS.borderGray, COLORS.primary, COLORS.white) so no magic
color values remain in the styled component.

Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import Modal from "@/components/common/Modal/Modal";
import * as Styled from './ApplicationSelectModal.styles';
import { ApplicationForm } from "@/types/application";

export interface ApplicationSelectModalProps {
isOpen: boolean;
onClose: () => void;
options: ApplicationForm[];
onSelect: (option: ApplicationForm) => void;
onBackdropClick?: () => boolean | void;
}

interface OptionsListProps {
options: ApplicationForm[];
onSelect: (option: ApplicationForm) => void;
}

const OptionsList = ({ options, onSelect }: OptionsListProps) => {
if (options.length === 0) {
return <Styled.EmptyMessage>지원 가능한 분야가 없습니다.</Styled.EmptyMessage>;
}

return (
<Styled.List>
{options.map((option) => (
<Styled.OptionButton key={option.id} onClick={() => {onSelect(option);}}>
{option.title}
</Styled.OptionButton>
))}
</Styled.List>
)
};

const ApplicationSelectModal = ({ isOpen, onClose, options, onSelect, onBackdropClick }: ApplicationSelectModalProps) => {
const handleOverlayClick = () => {
return false;
};

return (
<Modal
isOpen={isOpen}
onClose={onClose}
title="지원 분야 선택"
description="지원할 분야를 선택해주세요"
onBackdropClick={handleOverlayClick}
>
<OptionsList options={options} onSelect={onSelect} />
</Modal>
);
};

export default ApplicationSelectModal;
60 changes: 60 additions & 0 deletions frontend/src/components/common/Modal/Modal.styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { media } from '@/styles/mediaQuery';
import styled from 'styled-components';

export const Overlay = styled.div<{ isOpen: boolean }>`
position: fixed;
inset: 0;
z-index: 1000;
background: rgba(0,0,0, ${({ isOpen }) => (isOpen ? 0.45 : 0)});
display: grid;
place-items: center;
padding: 24px;
transition: background-color .2s ease;
`;

export const Container = styled.div<{ isOpen: boolean }>`
max-width: 500px;
width: 100%;
max-height: 90vh;
background: #fff;
border-radius: 10px;
overflow: hidden;
box-shadow: ${({ isOpen }) => (isOpen ? '0 18px 44px rgba(0,0,0,.22)' : 'none')};
transition: transform .2s ease, box-shadow .2s ease;
`;

export const Header = styled.div`
padding: 30px;
border-bottom: 1px solid #DCDCDC;
display: flex;
align-items: center;
`;

export const Title = styled.h3`
font-size: 20px;
font-weight: 800;
flex: 1;
text-align: left;
`;

export const IconButton = styled.button`
border: none;
background: transparent;
font-size: 20px;
font-weight: 800;
color: #9D9D9D;
line-height: 1;
cursor: pointer;
`;

export const Description = styled.p`
padding: 20px 32px 0px;
text-align: left;
color: #9D9D9D;
font-weight: 600;
`;

export const Body = styled.div`
padding: 16px 30px 30px;
overflow: auto;
`;
40 changes: 40 additions & 0 deletions frontend/src/components/common/Modal/Modal.tsx
Copy link
Contributor

Choose a reason for hiding this comment

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

모달 컴포넌트 생성 수고하셨습니다.
다른 두분의 말씀대로 생각해야할 부분이 참많은거같네요.
또한 더 사용성 좋은 모달은 만들기위해 내부를 커스터마이징할 수 있게 추후 컴파운드패턴 형식으로 변경한다면 재사용성을 갖출 수 있지않을까 생각이듭니다

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

공통 컴포넌트를 만드는 것에는 생각보다 많은 고민이 필요한 것 같습니다.
사용성 좋은 모달을 고려해보고 피드백 받은 것을 기반으로 다음에 구현할때 참고하겠습니다.

현재 모달을 더 좋은 방향으로 변경해도 좋을 것 같네요!
컴파운드패턴 형식! 공부해보겠습니다.

Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { MouseEvent, ReactNode, useEffect } from "react";
import * as Styled from './Modal.styles';

export interface ModalProps {
isOpen: boolean;
onClose: () => void;
title: string;
description?: string;
children?: ReactNode;
onBackdropClick?: () => boolean | void;
}
Comment on lines 4 to 11
Copy link
Member

Choose a reason for hiding this comment

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

바깥쪽을 클릭했을 때의 핸들러 또한 추가하면 어떨까요?

모달에서 바깥쪽을 클릭했을 때의 동작도 각 모달마다 다를 수 있다고 생각합니다!

backDropClick: () => void;

Copy link
Collaborator Author

@suhyun113 suhyun113 Nov 9, 2025

Choose a reason for hiding this comment

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

말씀해주신 부분 반영해서 onBackdropClick에 boolean 제어를 추가했습니다!
덕분에 다양한 동작에도 대응할 수 있을 것 같아요.
좋은 피드백 감사합니다 :)

0d39d6b ad58cc7 c159a23 에서 작업 진행했습니다.

Comment on lines +4 to +11
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

title 속성 타입 불일치

title은 필수 속성(string)으로 정의되었지만, 28번 줄에서 조건부 렌더링(title &&)을 사용하고 있습니다. 빈 문자열을 허용하려면 타입을 선택적으로 변경하거나, 항상 타이틀을 표시하도록 로직을 수정해야 합니다.

다음 중 하나를 적용하세요:

옵션 1: title을 선택적 속성으로 변경

-  title: string;
+  title?: string;

옵션 2: 조건부 렌더링 제거 (title이 항상 제공된다면)

-  {title && <Styled.Title>{title}</Styled.Title>}
+  <Styled.Title>{title}</Styled.Title>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export interface ModalProps {
isOpen: boolean;
onClose: () => void;
title: string;
description?: string;
children?: ReactNode;
onBackdropClick?: () => boolean | void;
}
export interface ModalProps {
isOpen: boolean;
onClose: () => void;
title?: string;
description?: string;
children?: ReactNode;
onBackdropClick?: () => boolean | void;
}
🤖 Prompt for AI Agents
In frontend/src/components/common/Modal/Modal.tsx around lines 5 to 12, the
ModalProps defines title as a required string but the component uses conditional
rendering (title && ...) at line 28; either make title optional in the type or
always render the title. Fix by choosing one: Option A — change title: string to
title?: string in ModalProps and keep the conditional render; Option B — remove
the conditional rendering and always render the title (ensuring callers always
pass a non-empty string).



const Modal = ({ isOpen, onClose, title, description, children, onBackdropClick }: ModalProps) => {
useEffect(() => {
if (isOpen) {
document.body.style.overflow = 'hidden';
}
return () => {
document.body.style.overflow = '';
};
}, [isOpen]);

if (!isOpen) return null;

return (
<Styled.Overlay isOpen={isOpen} onClick={onBackdropClick} aria-modal="true">
<Styled.Container isOpen={isOpen} onClick={(e: MouseEvent<HTMLDivElement>) => e.stopPropagation()}>
<Styled.Header>
{title && <Styled.Title>{title}</Styled.Title>}
<Styled.IconButton aria-label="close" type="button" onClick={onClose}>✕</Styled.IconButton>
</Styled.Header>
{description && <Styled.Description>{description}</Styled.Description>}
<Styled.Body>{children}</Styled.Body>
</Styled.Container>
</Styled.Overlay>
);
}

export default Modal;
11 changes: 10 additions & 1 deletion frontend/src/context/AdminClubContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ 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>(
Expand All @@ -19,9 +21,16 @@ export const AdminClubProvider = ({
}) => {
const [clubId, setClubId] = useState<string | null>(null);
const [applicantsData, setApplicantsData] = useState<ApplicantsInfo | null>(null);
const [applicationFormId, setApplicationFormId] = useState<string | null>(null);

return (
<AdminClubContext.Provider value={{ clubId, setClubId, applicantsData, setApplicantsData }}>
<AdminClubContext.Provider
value={{
clubId, setClubId,
applicantsData, setApplicantsData,
applicationFormId, setApplicationFormId,
}}
>
{children}
</AdminClubContext.Provider>
);
Expand Down
8 changes: 4 additions & 4 deletions frontend/src/hooks/queries/application/useGetApplication.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { useQuery } from '@tanstack/react-query';
import getApplication from '@/apis/application/getApplication';

export const useGetApplication = (clubId: string) => {
export const useGetApplication = (clubId?: string | null, applicationFormId?: string | null) => {
return useQuery({
queryKey: ['applicationForm', clubId],
queryFn: () => getApplication(clubId),
queryKey: ['applicationForm', clubId, applicationFormId],
queryFn: () => getApplication(clubId!, applicationFormId!),
retry: false,
enabled: !!clubId,
enabled: !!clubId && !!applicationFormId,
});
};
25 changes: 21 additions & 4 deletions frontend/src/mocks/api/apply.ts
Copy link
Contributor

Choose a reason for hiding this comment

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

목 데이터 써주신거 좋네요~

Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { http, HttpResponse } from 'msw';
import { mockData } from '../data/mockData';
import { mockData, mockOptions } from '../data/mockData';
import { API_BASE } from '../constants/clubApi';
import { validateClubId } from '../utils/validateClubId';
import { ERROR_MESSAGE } from '../constants/error';
Expand All @@ -24,9 +24,14 @@ export const applyHandlers = [

return HttpResponse.json(
{
clubId,
form_title: mockData.title,
questions: mockData.questions,
status: '200',
message: 'OK',
data: {
clubId,
title: mockData.title,
description: mockData.description,
questions: mockData.questions,
},
},
{ status: 200 },
);
Expand Down Expand Up @@ -69,4 +74,16 @@ export const applyHandlers = [
{ status: 200 },
);
}),

http.get(`${API_BASE}/:clubId/applications`, ({ params }) => {
const clubId = String(params.clubId);
if (!validateClubId(clubId)) {
return HttpResponse.json(
{ message: ERROR_MESSAGE.INVALID_CLUB_ID },
{ status: 400 },
);
}
const list = mockOptions[clubId] ?? [];
return HttpResponse.json({data: list}, {status: 200});
}),
Comment on lines +78 to +88
Copy link
Member

Choose a reason for hiding this comment

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

👍

];
5 changes: 5 additions & 0 deletions frontend/src/mocks/constants/clubApi.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
export const API_BASE = 'http://localhost/api/club';

export const CLUB_ID = '67e54ae51cfd27718dd40be6';

export const CLUB_IVF = '67ee2ca3b35e3c267e3c248d';
export const CLUB_BOB = '67e54ae51cfd27718dd40bea';
export const CLUB_BACK = '67e54ae51cfd27718dd40bd8';
export const CLUB_TEST = '67ebf9f75c8623081055881c';
22 changes: 21 additions & 1 deletion frontend/src/mocks/data/mockData.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ApplicationFormData } from '@/types/application';
import { ApplicationFormData, ApplicationForm } from '@/types/application';
import { CLUB_BACK, CLUB_BOB, CLUB_IVF, CLUB_TEST } from '../constants/clubApi';

type QuestionType =
| 'CHOICE'
Expand Down Expand Up @@ -121,3 +122,22 @@ export const mockData: ApplicationFormData = {
},
],
};

export const mockOptions: Record<string, ApplicationForm[]> = {
/*보블리스*/
[CLUB_BOB]: [
{ id: 'string101', title: '개발자로 지원하기' },
{ id: 'string103', title: '기획자로 지원하기' },
],
/*IVF*/
[CLUB_IVF]: [
{ id: 'string201', title: '선수로 지원하기' },
{ id: 'string202', title: '매니저로 지원하기' },
],
/*백경예술연구회*/
[CLUB_BACK]: [
{ id: 'string301', title: '백경예술연구회 지원하기' },
],
/*테스트*/
[CLUB_TEST]: [],
};
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,9 @@ const ApplicantDetailPage = () => {
const [applicantStatus, setApplicantStatus] = useState<ApplicationStatus>(
ApplicationStatus.SUBMITTED,
);
const { applicantsData, clubId } = useAdminClubContext();
const { applicantsData, clubId, applicationFormId } = useAdminClubContext();

const { data: formData, isLoading, isError } = useGetApplication(clubId!);
const { data: formData, isLoading, isError } = useGetApplication(clubId!, applicationFormId!);
const { mutate: updateApplicant } = useUpdateApplicant(clubId!);
Comment on lines +41 to 44
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 | 🟠 Major

Non-null assertion 사용으로 인한 잠재적 런타임 에러를 방지하세요.

Line 43에서 clubId!applicationFormId!로 non-null assertion을 사용하고 있습니다. 컨텍스트에서 가져온 이 값들이 null일 수 있으므로, assertion은 타입 체크를 우회하여 런타임 에러를 발생시킬 수 있습니다.

다음 diff를 적용하여 안전하게 처리하세요:

-  const { applicantsData, clubId, applicationFormId } = useAdminClubContext();
-
-  const { data: formData, isLoading, isError } = useGetApplication(clubId!, applicationFormId!);
-  const { mutate: updateApplicant } = useUpdateApplicant(clubId!);
+  const { applicantsData, clubId, applicationFormId } = useAdminClubContext();
+
+  const { data: formData, isLoading, isError } = useGetApplication(
+    clubId || '', 
+    applicationFormId || ''
+  );
+  const { mutate: updateApplicant } = useUpdateApplicant(clubId || '');

또는 early return으로 guard를 추가하세요:

   const { applicantsData, clubId, applicationFormId } = useAdminClubContext();
+
+  if (!clubId || !applicationFormId) {
+    return <div>클럽 정보를 불러올 수 없습니다.</div>;
+  }
 
-  const { data: formData, isLoading, isError } = useGetApplication(clubId!, applicationFormId!);
+  const { data: formData, isLoading, isError } = useGetApplication(clubId, applicationFormId);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const { applicantsData, clubId, applicationFormId } = useAdminClubContext();
const { data: formData, isLoading, isError } = useGetApplication(clubId!);
const { data: formData, isLoading, isError } = useGetApplication(clubId!, applicationFormId!);
const { mutate: updateApplicant } = useUpdateApplicant(clubId!);
const { applicantsData, clubId, applicationFormId } = useAdminClubContext();
const { data: formData, isLoading, isError } = useGetApplication(
clubId || '',
applicationFormId || ''
);
const { mutate: updateApplicant } = useUpdateApplicant(clubId || '');
🤖 Prompt for AI Agents
In
frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantDetailPage/ApplicantDetailPage.tsx
around lines 41–44, avoid using non-null assertions for clubId and
applicationFormId; instead guard their presence before invoking hooks or make
the hooks conditional. Fix by checking if clubId and applicationFormId are
defined (early return a loading/empty state or render fallback) before calling
useGetApplication/useUpdateApplicant, or call the hooks with safe arguments and
an enabled flag (e.g., pass undefined/nullable ids and add { enabled: !!clubId
&& !!applicationFormId } to the query hook and only initialize the mutation when
clubId exists). Ensure no hooks are conditionally called inside render — move
the early return above hook usage or use the hooks with the enabled pattern so
runtime null assertions are removed.


const applicantIndex =
Expand Down Expand Up @@ -75,7 +75,7 @@ const ApplicantDetailPage = () => {
},
]);
}, 400),
[clubId, questionId],
[clubId, questionId, updateApplicant],
);

if (!applicantsData) {
Expand Down
Loading
Loading