-
Notifications
You must be signed in to change notification settings - Fork 3
[Release] v1.0.9 #627
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Release] v1.0.9 #627
Changes from all commits
229a341
b29c835
78b3402
73c3381
808d00e
6f7264e
2519381
2b6f5cd
cf6cf25
95a6005
b507b1d
14a4bcd
7853a0c
307f2f0
c431434
cbc1850
99f7032
c926e54
1129623
5382f19
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,20 @@ | ||||||||||||||||||||||||||||||||||||||||||||
| import API_BASE_URL from '@/constants/api'; | ||||||||||||||||||||||||||||||||||||||||||||
| import { secureFetch } from '../auth/secureFetch'; | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| const getClubApplicants = async (clubId: string) => { | ||||||||||||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion 반환 타입을 명시하고 도메인 타입을 사용하세요 동일 계층 API 함수들과의 일관성을 위해 명시적 반환 타입을 지정하고, applicants 도메인 타입을 재사용하는 편이 안전합니다. 예시(정확한 타입명은 -const getClubApplicants = async (clubId: string) => {
+import type { ApplicantsData } from '@/types/applicants';
+const getClubApplicants = async (clubId: string): Promise<ApplicantsData[]> => {📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||
| 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; | ||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+7
to
+13
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion 에러 응답 JSON 파싱 실패 대비 및 메시지 폴백 추가 서버가 비-JSON 응답을 돌려줄 경우 [security] - 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;
+ if (!response.ok) {
+ console.error(`Failed to fetch: ${response.status} ${response.statusText}`);
+ let message = `요청 실패: ${response.status} ${response.statusText}`;
+ try {
+ const errBody = await response.json();
+ if (errBody?.message) message = errBody.message;
+ } catch {
+ // ignore JSON parse error
+ }
+ throw new Error(message);
+ }
+
+ const result = await response.json();
+ return result.data;참고: 공용 API 유틸 층에서 콘솔 에러를 직접 찍는 대신(숨은 부작용), 호출 측에서 로깅/토스트를 책임지도록 위임하는 것도 고려해볼 만합니다. 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||||||||||||||||||||||||||
| console.error('Error fetching club applicants', error); | ||||||||||||||||||||||||||||||||||||||||||||
| throw error; | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| export default getClubApplicants; | ||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -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, | ||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+4
to
+10
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 빈 clubId로 인한 불필요/오류 요청 방지 및 타입 강화 필요 현재 clubId가 빈 문자열일 때도 쿼리가 실행되어 API 오류 가능성이 있습니다. enabled 가드를 추가하고 반환 타입을 명시하세요. -import getClubApplicants from "@/apis/applicants/getClubApplicants"
-import { useQuery } from "@tanstack/react-query"
+import getClubApplicants from "@/apis/applicants/getClubApplicants"
+import { useQuery } from "@tanstack/react-query"
+import { ApplicantsInfo } from "@/types/applicants"
-export const useGetApplicants = (clubId: string) => {
- return useQuery({
+export const useGetApplicants = (clubId: string) => {
+ return useQuery<ApplicantsInfo, Error>({
queryKey: ['clubApplicants', clubId],
queryFn: () => getClubApplicants(clubId),
+ enabled: Boolean(clubId),
retry: false,
})
}참고: 기존 호출부(PrivateRoute) 변경 없이 안전해집니다. enabled가 false면 네트워크 호출이 발생하지 않습니다. 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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]); | ||
|
Comment on lines
+19
to
+23
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 의존성 누락 및 이전 동아리 데이터 잔존 위험
-useEffect(() => {
- if (clubId && applicantsData) {
- setApplicantsData(applicantsData);
- }
-}, [clubId, applicantsData]);
+useEffect(() => {
+ if (!clubId) {
+ setApplicantsData(null);
+ return;
+ }
+ if (applicantsData) {
+ setApplicantsData(applicantsData);
+ }
+}, [clubId, applicantsData, setApplicantsData]);🤖 Prompt for AI Agents |
||
|
|
||
| if (isLoading) return <Spinner />; | ||
| if (!isAuthenticated) return <Navigate to='/admin/login' replace />; | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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(); | ||
| } | ||
|
Comment on lines
+44
to
+46
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 치명적: HttpOnly 쿠키 환경에서 로그아웃 API가 건너뛰어질 수 있습니다
[security] 아래와 같이 수정하면 네트워크/서버 오류와 무관하게 로컬 상태는 정리되고, 쿠키 제거는 가능한 경우에만 서버가 처리합니다. - if (document.cookie.split(';').some((cookie) => cookie.trim().startsWith('refreshToken='))) {
- await logout();
- }
+ await logout().catch(() => {
+ // 서버 쿠키/세션 정리 실패는 무시하고 클라이언트 상태를 우선 정리
+ });추가적으로, 보다 견고하게 하려면 토큰 제거 및 이동을 finally로 옮기는 것도 고려해 주세요. try {
await logout();
} catch {
// noop
} finally {
localStorage.removeItem('accessToken');
navigate('/admin/login', { replace: true });
}🤖 Prompt for AI Agents |
||
| localStorage.removeItem('accessToken'); | ||
| navigate('/admin/login', { replace: true }); | ||
| } catch (error) { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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(); | ||
|
|
||
|
Comment on lines
+15
to
+18
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion URL 파라미터 명칭이 혼란을 유발합니다
🤖 Prompt for AI Agents |
||
| // 지원서 질문 목록 fetch | ||
| const { data: formData, isLoading, isError } = useGetApplication(clubId!); | ||
|
|
||
|
Comment on lines
+20
to
+21
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
🤖 Prompt for AI Agents |
||
| if (!applicantsData) { | ||
| return <div>지원자 데이터를 불러올 수 없습니다.</div>; | ||
| } | ||
| if (isLoading) return <Spinner />; | ||
| if (isError || !formData) return <div>지원서 정보를 불러올 수 없습니다.</div>; | ||
|
|
||
| // questionId로 지원자 찾기 | ||
| const applicant = applicantsData.applicants.find( | ||
| (a) => a.id === questionId | ||
| ); | ||
| if (!applicant) { | ||
| return <div>해당 지원자를 찾을 수 없습니다.</div>; | ||
| } | ||
|
|
||
| // 답변 매핑 함수 | ||
| const getAnswerByQuestionId = (qId: number) => { | ||
| return applicant.answers | ||
| .filter((ans) => ans.id === qId) | ||
| .map((ans) => ans.value); | ||
| }; | ||
|
|
||
| return ( | ||
| <> | ||
| <Header /> | ||
| <PageContainer style={{ paddingTop: '80px' }}> | ||
| {/* FormTitle과 백아이콘을 한 줄에 배치 */} | ||
| <div | ||
| style={{ | ||
| position: 'sticky', | ||
| top: 25, | ||
| zIndex: 10, | ||
| background: '#fff', | ||
| display: 'flex', | ||
| alignItems: 'center', | ||
| gap: 12, | ||
| marginBottom: 16, | ||
| }} | ||
| > | ||
| <button | ||
| onClick={() => navigate(-1)} | ||
| style={{ background: 'none', border: 'none', cursor: 'pointer', padding: 0 }} | ||
| aria-label="뒤로가기" | ||
| > | ||
| <img src={backButtonIcon} alt="뒤로가기" style={{ width: 16, height: 16 }} /> | ||
| </button> | ||
| </div> | ||
| {/* 커서 고정 */} | ||
| <Styled.QuestionsWrapper style={{ cursor: 'default' }}> | ||
| {formData.questions.map((q: import('@/types/application').Question, i: number) => ( | ||
| <QuestionContainer key={q.id} hasError={false}> | ||
| <QuestionAnswerer | ||
| question={q} | ||
| selectedAnswers={getAnswerByQuestionId(q.id)} | ||
| onChange={() => {}} | ||
| /> | ||
| </QuestionContainer> | ||
| ))} | ||
| </Styled.QuestionsWrapper> | ||
| </PageContainer> | ||
| </> | ||
| ); | ||
| }; | ||
|
|
||
| export default ApplicantDetailPage; | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
URL 파라미터 명 확인 필요:
:questionId→:applicantId?개별 지원자 상세를 보여주는 페이지라면 파라미터명이
questionId보다는applicantId(또는 유사 도메인명)가 자연스럽습니다. 내부에서useParams로 어떤 키를 읽는지 확인해 주세요. 불일치 시 상세 페이지가 파라미터를 못 읽습니다.다음 스크립트로 상세 페이지의 파라미터 사용을 점검할 수 있습니다(레포 루트에서 실행):
수정이 필요하다면:
🏁 Script executed:
Length of output: 2998
파라미터명 일관성 확인 및 도메인 용어 구체화 제안
현재
Route와useParams모두:questionId/questionId로 일관되게 쓰이고 있어 런타임 이슈는 없으나, 컴포넌트명이ApplicantDetailPage임을 감안하면 파라미터명은applicantId가 더 적합합니다. 아래 파일을 중심으로 네이밍을 통일해주세요.• frontend/src/App.tsx
• frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantDetailPage.tsx
• (링크 생성부) frontend/src/pages/AdminPage/tabs/ApplicantsTab.tsx
예시 diff:
navigate()호출부의 템플릿 리터럴도 동일하게applicants/${applicantId}로 수정 필요합니다.📝 Committable suggestion
🤖 Prompt for AI Agents