Skip to content

[release] FE v1.1.17#1060

Merged
seongwon030 merged 41 commits intomainfrom
develop-fe
Jan 18, 2026
Merged

[release] FE v1.1.17#1060
seongwon030 merged 41 commits intomainfrom
develop-fe

Conversation

@seongwon030
Copy link
Member

@seongwon030 seongwon030 commented Jan 18, 2026

#️⃣연관된 이슈

ex) #이슈번호, #이슈번호

📝작업 내용

이번 PR에서 작업한 내용을 간략히 설명해주세요(이미지/동영상 첨부 가능)

중점적으로 리뷰받고 싶은 부분(선택)

리뷰어가 특별히 봐주었으면 하는 부분이 있다면 작성해주세요

ex) 메서드 XXX의 이름을 더 잘 짓고 싶은데 혹시 좋은 명칭이 있을까요?

논의하고 싶은 부분(선택)

논의하고 싶은 부분이 있다면 작성해주세요.

🫡 참고사항

Summary by CodeRabbit

  • New Features

    • 이미지 업로드/관리, 인증, 신청서·동아리 관련 프론트엔드 API와 React Query 훅 추가
    • 중앙화된 queryKeys 추가로 데이터 캐싱 키 일관화
    • 신청서 선택 모달과 관련된 사용자 흐름 개선
  • Refactor

    • API/훅 구조 통합 및 공통 에러 처리 유틸 도입
    • Storybook 메타데이터(autodocs/argTypes) 보강으로 문서화 개선
  • Chores

    • 경로/임포트 정리, 코드 포맷팅 일관화
    • Netlify SPA fallback 리디렉트 제거

✏️ Tip: You can customize this high-level summary in your review settings.

- Mixpanel 관련 훅을 hooks/Mixpanel/ 폴더로 이동
- Scroll 관련 훅을 hooks/Scroll/ 폴더로 이동
- Application 관련 훅을 hooks/Application/ 폴더로 이동
- useValidateAnswers는 순수 함수이므로 utils로 이동
- 모든 import 경로 업데이트
…(applicants, application, auth, club, image) - apis/utils/apiHelpers.ts 추가하여 공통 에러 처리 로직 분리 - handleResponse와 withErrorHandling으로 중복 코드 제거 - 커스텀 에러 메시지 선택적 지원으로 기존 동작 유지 - 모든 API에 일관된 에러 처리 패턴 적용 - 43개 파일의 import 경로 업데이트

- 24개 API 파일을 5개 도메인별 파일로 통합 (applicants, application, auth, club, image)
- apis/utils/apiHelpers.ts 추가하여 공통 에러 처리 로직 분리
- handleResponse와 withErrorHandling으로 중복 코드 제거
- 커스텀 에러 메시지 선택적 지원으로 기존 동작 유지
- 모든 API에 일관된 에러 처리 패턴 적용
- 43개 파일의 import 경로 업데이트
- API 함수에 사용자 친화적인 한국어 에러 메시지 추가
- queryKeys를 constants/queryKeys.ts로 분리 및 factory 패턴 적용
- hooks/queries → hooks/Queries 폴더 구조 정리
- apiHelpers/handleResponse: 빈 응답, JSON 파싱 실패, 잘못된 Content-Type에 대한 예외 처리 강화
- auth/getClubIdByToken: 응답 데이터 내 clubId 유효성 검사 로직 추가
- club/getClubDetail: 응답 데이터 내 club 객체 유효성 검사 로직 추가
- club/getClubList: 리스트 데이터 Null 체크 및 기본값(Fallback) 처리 추가
- App.tsx: QueryClient 전역 설정 추가 (staleTime: 60s, retry: 1)
- Hooks:
  - 개별 훅 내 중복된 retry, staleTime 옵션 제거
  - invalidateQueries 로직을 컴포넌트에서 훅 내부 onSuccess로 이동하여 응집도 향상
  - 훅 내부 alert 제거 및 console.error로 로깅 전환 (SoC 준수)
- Components:
  - mutate 호출 시 onError/onSuccess 콜백을 통해 사용자 알림(alert) 처리하도록 수정
- Hooks: useUpdateApplicationStatus 훅 추가 및 useDuplicateApplication 적용
- ApplicationListTab: 상태 변경 로직을 훅으로 위임하고 누락된 onDuplicate 핸들러 연결
- ApplicantDetailPage: 지원자 정보 수정 시 에러 핸들링(onError) 추가
- ApplicantsTab: 코드 정리 및 라우팅 관련 수정
- APPLICATION_FORM.ts → applicationForm.ts
- CLUB_UNION_INFO.ts → clubUnionInfo.ts
- INITIAL_FORM_DATA.ts → initialFormData.ts
- 관련 import 경로 9개 파일 업데이트
- clubId/applicationFormId가 없을 때 queryKeys.application.all 대신 고유한 키 사용
- useGetApplicationlist와 캐시 키 충돌로 인한 타입 불일치 문제 해결
- enabled가 false일 때도 명확한 캐시 분리 보장
- error 체크를 clubDetail 체크보다 먼저 수행
- API 에러 발생 시 에러 메시지가 정상적으로 표시되도록 개선
- club.responses.ts 파일 삭제
- ClubSearchResponse를 club.ts로 이동
- 단일 사용처만 있는 타입의 불필요한 파일 분리 제거
- ClubFeed 컴포넌트 내부로 모달 상태 관리 로직 이동
- hooks/PhotoList/usePhotoModal.ts 파일 삭제
- 단일 사용처만 있는 불필요한 훅 추상화 제거
- feed 배열 변경 시 index가 범위를 벗어나지 않도록 useEffect 추가
- 빈 배열일 때 모달 자동 닫기 및 index 초기화
- PhotoModal에서 잘못된 배열 접근 방지
…OA-530

[feature] 앱 다운로드 배너 트래킹에 A/B 테스트 그룹 정보 추가
…ing-MOA-532

[refacotor] Tanstack Query 리팩토링 및 API/Hooks 구조 개선
@seongwon030 seongwon030 self-assigned this Jan 18, 2026
@seongwon030 seongwon030 added 💻 FE Frontend 📈 release 릴리즈 배포 labels Jan 18, 2026
@vercel
Copy link

vercel bot commented Jan 18, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Review Updated (UTC)
moadong Ready Ready Preview, Comment Jan 18, 2026 1:01pm

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 18, 2026

Warning

Rate limit exceeded

@seongwon030 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 5 minutes and 56 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between 4b1ba6c and 05d111c.

📒 Files selected for processing (3)
  • frontend/src/hooks/Queries/useClub.ts
  • frontend/src/pages/AdminPage/tabs/ClubInfoEditTab/ClubInfoEditTab.tsx
  • frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsx

Warning

.coderabbit.yaml has a parsing error

The CodeRabbit configuration file in this repository has a parsing error and default settings were used instead. Please fix the error(s) in the configuration file. You can initialize chat with CodeRabbit to get help with the configuration file.

💥 Parsing errors (1)
Validation error: Invalid regex pattern for base branch. Received: "**" at "reviews.auto_review.base_branches[0]"
⚙️ Configuration instructions
  • Please see the configuration documentation for more information.
  • You can also validate your configuration using the online YAML validator.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Walkthrough

API·훅 대규모 통합·재배치, API 응답/오류 처리 유틸 추가, React Query 훅 재구성, 이미지 업로드·인증·애플리케이션 API 통합, 다수의 import 경로 및 Storybook 메타 변경, Netlify SPA 리다이렉트 제거 등 구조적 리팩토링이 이루어졌습니다.

Changes

Cohort / File(s) 변경 사항 요약
통합된 API 모듈
frontend/src/apis/auth.ts, frontend/src/apis/applicants.ts, frontend/src/apis/application.ts, frontend/src/apis/club.ts, frontend/src/apis/image.ts
여러 개별 엔드포인트 파일을 단일 도메인 파일로 병합하여 관련 함수들을 내보냄 (인증, 지원자, 지원서 CRUD·상태관리, 클럽, 이미지 업로드 등).
삭제된 개별 API 파일
frontend/src/apis/auth/*, frontend/src/apis/application/*, frontend/src/apis/applicants/*, frontend/src/apis/image/*, frontend/src/apis/getClubDetail.ts, frontend/src/apis/getClubList.ts, frontend/src/apis/updateClub*.ts
기존 per-endpoint 파일들 제거(기능이 통합 모듈로 이동).
API 유틸리티 추가
frontend/src/apis/utils/apiHelpers.ts, frontend/src/constants/queryKeys.ts
handleResponsewithErrorHandling 도입 및 React Query용 중앙화된 queryKeys 추가.
훅 재배치/통합 (새 경로)
frontend/src/hooks/Queries/* (예: useApplicants.ts, useApplication.ts, useClub.ts, useClubImages.ts, useClubCover.ts)
기존 hooks/queries/* 파일들을 통합·이관하고 훅들을 재구성(쿼리 무효화/선택자 포함).
삭제된 훅/기능 훅 파일들
frontend/src/hooks/queries/*, frontend/src/hooks/PhotoList/*, frontend/src/hooks/PhotoModal/*, frontend/src/hooks/InfoTabs/useAutoScroll.ts
여러 개별 훅 파일 삭제(동일 기능이 통합 모듈로 대체되었거나 로컬 상태로 전환).
Mixpanel · Scroll 훅 경로 변경
frontend/src/hooks/Mixpanel/*, frontend/src/hooks/Scroll/*
Mixpanel 및 Scroll 관련 훅을 전용 디렉토리로 이동하고 다수 import 경로 갱신.
App 및 QueryClient 설정 변경
frontend/src/App.tsx
QueryClient 기본 옵션 변경 (staleTime=60s, queries.retry=1, mutations.retry=0) 및 ScrollToTop import 경로 조정.
Netlify 설정 제거
frontend/netlify.toml
SPA catch-all 리다이렉트 규칙 제거.
이미지 업로드 API 통합
frontend/src/apis/image.ts
presigned 업로드→storage PUT→완료 호출 흐름을 통합한 API 제공 (cover/feed/logo), 업로드 헬퍼 포함.
컴포넌트·스토리북·스타일 import 경로 일괄 업데이트
frontend/src/components/**, frontend/src/pages/**, frontend/.storybook/preview.ts, frontend/.prettierrc, frontend/config/vite.config.ts
대량의 import 경로 변경(상수/훅/대소문자 표준화), Storybook meta/argTypes 확장, 포맷 정리.
타입·상수 변경
frontend/src/types/club.ts, frontend/src/constants/*
ClubSearchResponse 타입 추가/이동, photoLayout 상수 삭제, APPLICATION_FORM → applicationForm 경로 표준화.

Sequence Diagram(s)

sequenceDiagram
    participant UI as Client(UI)
    participant API as Frontend API module (image.ts)
    participant Storage as Presigned Storage (S3/GCS)
    participant Query as React Query (cache)

    rect rgba(220,240,255,0.5)
    UI->>API: coverApi.getUploadUrl(clubId, fileName, contentType)
    API-->>UI: PresignedData{ presignedUrl, finalUrl, clubId }
    end

    rect rgba(220,255,220,0.5)
    UI->>Storage: PUT presignedUrl (file)
    Storage-->>UI: 200/OK
    end

    rect rgba(255,240,220,0.5)
    UI->>API: coverApi.completeUpload(clubId, finalUrl)
    API-->>UI: 200/ok
    API->>Query: invalidate queryKeys.club.detail(clubId)
    Query-->>UI: refetch/update UI
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • lepitaaar
  • oesnuj
🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 제목 '[release] FE v1.1.17'은 릴리스 버전을 명확히 명시하고 있어 주요 변경사항의 의도를 잘 전달합니다.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
frontend/src/pages/AdminPage/components/QuestionBuilder/QuestionBuilder.tsx (1)

30-36: React Hooks 규칙 위반: 조건부 반환 이후에 훅이 호출됨

Line 30-32의 early return이 Line 34-38의 useState와 Line 40-46의 useEffect 호출 전에 위치해 있습니다. 이는 React의 Hooks 규칙을 위반하여 typeQUESTION_LABEL_MAP에 없을 때 훅 호출 순서가 달라지면서 런타임 오류가 발생할 수 있습니다.

🐛 훅을 조건부 반환 전으로 이동하는 수정 제안
 const QuestionBuilder = ({
   id,
   title,
   description,
   options,
   items,
   type,
   readOnly,
   onTitleChange,
   onItemsChange,
   onDescriptionChange,
   onTypeChange,
   onRequiredChange,
   onRemoveQuestion,
 }: QuestionBuilderProps) => {
-  if (!(type in QUESTION_LABEL_MAP)) {
-    return null;
-  }
-
   const [selectionType, setSelectionType] = useState<'single' | 'multi'>(
     type === 'MULTI_CHOICE' ? 'multi' : 'single',
   );

   const [isDropdownOpen, setIsDropdownOpen] = useState(false);

   useEffect(() => {
     if (type === 'MULTI_CHOICE') {
       setSelectionType('multi');
     } else if (type === 'CHOICE') {
       setSelectionType('single');
     }
   }, [type]);

+  if (!(type in QUESTION_LABEL_MAP)) {
+    return null;
+  }
+
   const renderFieldByQuestionType = () => {
frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantDetailPage/ApplicantDetailPage.tsx (1)

67-95: 지원자 ID 누락 시 업데이트 요청 방지 필요
questionId가 라우트 파라미터라 undefined 가능성이 있어, 업데이트 전에 가드가 있으면 안전합니다.

✅ 제안 수정
       debounce((memo, status) => {
         function isApplicationStatus(v: unknown): v is ApplicationStatus {
           return (
             typeof v === 'string' &&
             Object.values(ApplicationStatus).includes(v as ApplicationStatus)
           );
         }

         if (typeof memo !== 'string') return;
         if (!isApplicationStatus(status)) return;
+        if (!questionId) return;

         updateApplicant(
           [
             {
               memo,
               status,
               applicantId: questionId,
             },
           ],
frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsx (1)

63-68: 지원서 옵션 0개일 때 사용자 피드백 필요
현재는 아무 반응 없이 종료되어 UX가 끊길 수 있습니다. 간단한 안내를 추가하면 좋습니다.

✅ 제안 수정
       const forms = await getApplicationOptions(clubId);

       if (forms.length <= 0) {
+        alert('현재 지원 가능한 지원서가 없습니다.');
         return;
       }
🤖 Fix all issues with AI agents
In `@frontend/src/apis/application.ts`:
- Around line 152-171: The updateApplicationStatus function is converting
ApplicationFormItem.status (uppercase strings) into a boolean and sending {
active: newStatus }, which mismatches ApplicationFormData.active (lowercase
string union); confirm the backend contract and then either (A) if the backend
expects a boolean, change the ApplicationFormData.active type to boolean across
types/interfaces, or (B) if the backend expects the lowercase status string,
update updateApplicationStatus to map currentStatus
('ACTIVE'|'PUBLISHED'|'UNPUBLISHED') to the corresponding lowercase value
('active'|'published'|'unpublished') and send that string in the request body
(ensure mapping logic is placed in updateApplicationStatus and that
ApplicationFormData.active stays the lowercase union).

In `@frontend/src/apis/auth.ts`:
- Around line 31-46: The logout function currently skips the server logout when
localStorage has no accessToken and doesn't clear the token after a successful
server logout; update logout (and its withErrorHandling wrapper usage) so the
server call to `${API_BASE_URL}/auth/user/logout` is always attempted (do not
early-return when accessToken is missing) and ensure the accessToken is removed
from localStorage in a finally block after the fetch/handleResponse completes or
errors; keep credentials: 'include' for cookie clearing and still propagate
errors via withErrorHandling while guaranteeing
localStorage.removeItem('accessToken') runs.

In `@frontend/src/apis/utils/apiHelpers.ts`:
- Around line 1-38: handleResponse currently returns only result.data which
drops unwrapped JSON responses (e.g. POST /apply); modify the JSON
parsing/fallback so that after parsing (in the try block that sets const result
= JSON.parse(text)) you return result.data if it exists (result && result.data
!== undefined), otherwise return the entire parsed result (and for
non-object/primitives just return result) so both wrapped ({ data: ... }) and
unwrapped responses are preserved; keep the existing error handling and
content-type/content-length checks in handleResponse.

In `@frontend/src/components/common/CustomDropDown/CustomDropDown.stories.tsx`:
- Around line 81-104: The story uses the hardcoded OPTIONS constant when
computing selectedLabel and rendering items, so Storybook controls
(args.options) don't affect the UI; update selectedLabel to derive from
args.options (e.g., find matching label from args.options) and replace the map
over OPTIONS inside the CustomDropDown.Menu with a map over args.options (ensure
type assertion matches the existing cast used for the options prop), adjusting
references to selected and option.value/option.label in CustomDropDown.Item to
use the items from args.options.

In `@frontend/src/hooks/Queries/useClub.ts`:
- Around line 79-81: The onError handler inside the useClub hook currently logs
"Error updating club detail:" which is a typo for the update hook; update the
error message in the onError callback to something accurate (e.g., "Error
updating club details:" or "Error updating club:") within the useClub hook's
update mutation (refer to the onError callback in useClub) so the log reflects
the correct operation and wording.

In `@frontend/src/pages/AdminPage/tabs/ApplicationListTab/ApplicationListTab.tsx`:
- Line 24: The cache invalidation uses the wrong query key string so the list
fetched by useGetApplicationList (which relies on queryKeys.application.all)
won't refresh; update the invalidateQueries call to use the same query key used
by useGetApplicationList (queryKeys.application.all) wherever
invalidateQueries({ queryKey: ['applicationForm'] }) is invoked so the cache and
refetch behavior match (search for useGetApplicationList and invalidateQueries
in ApplicationListTab.tsx and replace the mismatched queryKey accordingly).
🧹 Nitpick comments (13)
frontend/src/pages/ClubDetailPage/components/ClubProfileCard/ClubProfileCard.styles.ts (1)

165-189: 변경 사항 승인 - 순수 포맷팅 변경입니다.

hex 컬러 값의 대소문자 변경(#009CF6#009cf6)과 미디어 쿼리 블록 사이의 공백 정리는 기능적 영향이 없습니다. CSS hex 값은 대소문자를 구분하지 않습니다.

선택적 개선 제안: #009cf6 하드코딩 대신 colors 테마 토큰 사용을 고려해 보세요. 파일 내 다른 스타일들과 일관성을 유지하고, 향후 테마 변경 시 유지보수가 용이해집니다.

frontend/config/vite.config.ts (1)

2-2: 사용하지 않는 import를 제거하세요.

visualizer를 import했지만 설정에서 사용하지 않고 있습니다. 번들 분석이 필요한 경우 사용하시고, 그렇지 않다면 제거하는 것이 좋습니다.

♻️ 제안된 수정
-import { visualizer } from 'rollup-plugin-visualizer';
frontend/src/components/common/SearchField/SearchField.stories.tsx (1)

44-116: render 함수 중복을 줄이기 위한 공통 헬퍼 추출을 고려해보세요.

세 개의 스토리(Default, WithValue, CustomPlaceholder) 모두 동일한 render 함수를 사용하고 있습니다. 공통 render 함수를 추출하면 코드 중복을 줄일 수 있습니다.

♻️ 공통 render 함수 추출 제안
+const renderSearchField = (args: React.ComponentProps<typeof SearchField>) => {
+  const [value, setValue] = useState(args.value);
+
+  return (
+    <SearchField
+      {...args}
+      value={value}
+      onChange={(newValue) => {
+        setValue(newValue);
+        args.onChange(newValue);
+      }}
+    />
+  );
+};
+
 export const Default: Story = {
   args: {
     value: '',
     placeholder: '동아리 이름을 입력하세요',
     autoBlur: true,
     onChange: () => {},
     onSubmit: () => {},
   },
-  render: (args) => {
-    const [value, setValue] = useState(args.value);
-
-    return (
-      <SearchField
-        {...args}
-        value={value}
-        onChange={(newValue) => {
-          setValue(newValue);
-          args.onChange(newValue);
-        }}
-      />
-    );
-  },
+  render: renderSearchField,
 };

WithValue와 CustomPlaceholder 스토리에도 동일하게 적용할 수 있습니다.

frontend/src/pages/MainPage/components/Banner/Banner.tsx (1)

8-12: AB 테스트 유틸은 별도 모듈로 분리 고려해 주세요.
컴포넌트 파일에서 유틸을 가져오면 의존성 방향이 모호해질 수 있어, utils/abTest 같은 공용 위치로 이동해두면 재사용과 유지보수에 더 유리합니다.

frontend/src/pages/AdminPage/tabs/RecruitEditTab/components/Calendar/Calendar.tsx (1)

97-97: any 타입 사용에 대한 개선 제안

onChangeRaw 이벤트 핸들러에서 any 타입 대신 React.SyntheticEvent를 사용하면 타입 안전성을 높일 수 있습니다.

♻️ 타입 개선 제안
-        onChangeRaw={(e: any) => e.preventDefault()}
+        onChangeRaw={(e: React.SyntheticEvent) => e.preventDefault()}
frontend/src/pages/ClubDetailPage/components/ClubFeed/ClubFeed.tsx (1)

41-42: 변수 섀도잉 발생: index가 상태 변수와 map 콜백 파라미터에서 중복 사용됨.

Line 12에서 선언된 상태 변수 index와 Line 41의 map 콜백 파라미터 index가 동일한 이름을 사용하고 있습니다. 현재 동작에는 문제가 없지만, 향후 유지보수 시 혼란을 야기할 수 있습니다.

♻️ 변수명 변경 제안
-          {feed.map((f, index) => (
-            <Styled.PhotoItem key={`${f}-${index}`} onClick={() => open(index)}>
+          {feed.map((f, i) => (
+            <Styled.PhotoItem key={`${f}-${i}`} onClick={() => open(i)}>
               <Styled.PhotoImage
                 src={f}
-                alt={`활동사진 ${index + 1}`}
+                alt={`활동사진 ${i + 1}`}
                 loading='lazy'
               />
             </Styled.PhotoItem>
           ))}
frontend/src/pages/ClubDetailPage/components/PhotoModal/PhotoModal.styles.ts (1)

216-217: 디자인 시스템 일관성: 하드코딩된 색상값 대신 colors 토큰 사용 권장.

Line 217에서 #ddd 하드코딩 대신 프로젝트의 colors 테마 토큰을 사용하면 디자인 시스템과의 일관성이 향상됩니다.

♻️ 색상 토큰 사용 제안
   &:hover {
     border-color: ${({ isActive }) =>
-      isActive ? colors.primary[900] : '#ddd'};
+      isActive ? colors.primary[900] : colors.gray[400]};
   }
frontend/src/pages/AdminPage/auth/LoginTab/LoginTab.tsx (1)

47-51: 이벤트 트래킹 위치 확인 필요

trackEventfinally 블록 외부에서 호출되어, 만약 setLoading(false) 이후에 예외가 발생하면 이벤트가 누락될 수 있습니다. "LOGIN_BUTTON_CLICKED" 이벤트가 버튼 클릭 자체를 추적하는 것이라면 finally 블록 내부나 함수 시작 부분으로 이동하는 것이 더 안정적입니다.

♻️ 제안된 수정
    } finally {
      setLoading(false);
+     trackEvent(ADMIN_EVENT.LOGIN_BUTTON_CLICKED);
    }
-   trackEvent(ADMIN_EVENT.LOGIN_BUTTON_CLICKED);
  };
frontend/src/apis/image.ts (2)

10-13: FeedUploadRequest 인터페이스 export 고려

FeedUploadRequest 인터페이스가 내부에서만 사용되고 있지만, 이 API를 호출하는 컴포넌트에서 타입 안정성을 위해 해당 인터페이스가 필요할 수 있습니다.

♻️ 제안된 수정
-interface FeedUploadRequest {
+export interface FeedUploadRequest {
   fileName: string;
   contentType: string;
 }

31-81: coverApi와 logoApi 간 코드 중복

coverApilogoApi가 거의 동일한 구조(getUploadUrl, completeUpload, delete)를 가지고 있습니다. 현재 상태로도 동작하지만, 향후 유지보수를 위해 팩토리 함수로 추상화하는 것을 고려해볼 수 있습니다.

♻️ 팩토리 패턴 예시
const createImageApi = (type: 'cover' | 'logo') => ({
  getUploadUrl: async (clubId: string, fileName: string, contentType: string): Promise<PresignedData> => {
    return withErrorHandling(async () => {
      const response = await secureFetch(
        `${API_BASE_URL}/api/club/${clubId}/${type}/upload-url`,
        {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ fileName, contentType }),
        },
      );
      return handleResponse(response, `${type} 업로드 URL 생성 실패 : ${response.status}`);
    }, `${type} 업로드 URL 생성 중 오류 발생`);
  },
  // ... completeUpload, delete 동일 패턴
});

export const coverApi = createImageApi('cover');
export const logoApi = createImageApi('logo');

Also applies to: 121-171

frontend/src/pages/AdminPage/tabs/PhotoEditTab/PhotoEditTab.tsx (1)

90-106: 삭제 실패 시 로컬 상태 불일치 가능성 확인 필요
현재는 상태를 먼저 갱신하고 실패 시 알림만 표시합니다. 서버 업데이트가 실패하면 화면과 서버 상태가 불일치할 수 있으니 롤백 또는 재조회가 있는지 확인 부탁드립니다.

🔁 롤백을 추가하는 예시
   const deleteImage = (index: number) => {
     if (isLoading) return;
 
-    const newList = imageList.filter((_, i) => i !== index);
-    setImageList(newList);
+    const prevList = imageList;
+    const newList = prevList.filter((_, i) => i !== index);
+    setImageList(newList);
 
     updateFeed(
       {
         clubId: clubDetail.id,
         urls: newList,
       },
       {
         onError: () => {
+          setImageList(prevList);
           alert('이미지 삭제에 실패했어요. 다시 시도해주세요!');
         },
       },
     );
   };
frontend/src/components/common/InputField/InputField.stories.tsx (1)

80-231: 스토리 렌더 로직 중복은 DRY 가능 (선택)
모든 스토리가 동일한 useState + onChange/onClear 패턴을 반복합니다. 공통 render 헬퍼로 중복을 줄일 수 있습니다.

frontend/src/hooks/Queries/useClub.ts (1)

15-16: 동일 모듈의 중복 default import 정리 제안
같은 파일에서 두 번 default import되어 의미가 혼동됩니다. 하나로 통일하면 가독성이 좋아집니다.

♻️ 정리 예시
-import convertToDriveUrl from '@/utils/convertGoogleDriveUrl';
-import convertGoogleDriveUrl from '@/utils/convertGoogleDriveUrl';
+import convertGoogleDriveUrl from '@/utils/convertGoogleDriveUrl';
@@
-        logo: convertToDriveUrl(club.logo),
+        logo: convertGoogleDriveUrl(club.logo),

Copy link
Collaborator

@suhyun113 suhyun113 left a comment

Choose a reason for hiding this comment

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

릴리즈 갑시다아

- wrapped({ data: {...} })와 unwrapped({ clubId, message })
응답 형식을 모두 처리하도록 handleResponse 수정
- 하드코딩된 OPTIONS 대신 args.options 사용하여
Storybook Controls에서 옵션 변경 시 UI에 즉시 반영되도록 수정
- detail -> description
- 수동 API 호출 대신 훅 사용으로 중복된 invalidateQueries 로직 제거
- window.open() 대신 동적 a 태그 생성 방식으로 변경
- iOS Safari의 비동기 컨텍스트 팝업 차단 문제 해결
- 코드 간소화 (복잡한 fallback 로직 제거)
- API 호출 전에 미리 빈 창을 열어 사용자 제스처 컨텍스트 유지
- API 응답 후 외부 지원서인 경우 URL 주입
- 내부 지원서이거나 에러 발생 시 열린 창 자동 닫기
This reverts commit 17a31b9.
- handleResponse 제네릭 타입 도입
- null/undefined 데이터 검증 강화
- API 함수 반환 타입 명시
…able-MOA-534

[fix] 외부지원서 리다이렉트 방식 window.location.href로 변경
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsx (1)

90-93: 모달 열기 전에 에러 처리 추가 필요

에러 발생 시 모달을 열면 applicationOptions가 비어있어 "지원 가능한 분야가 없습니다"라는 메시지만 표시됩니다. 사용자가 API 오류인지 실제 지원서가 없는 상태인지 구분할 수 없는 UX 문제가 있습니다. 에러 발생 시 모달을 열지 않고 에러 알림을 표시하거나, 모달 내에서 구분된 에러 상태를 안내하는 것이 좋습니다.

🧹 Nitpick comments (1)
frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsx (1)

30-56: 내부 폼에서 불필요한 빈 탭이 열릴 수 있어요.
현재 모든 경로에서 window.open을 먼저 호출해 내부 폼에서도 잠깐 빈 탭이 열렸다 닫힐 수 있습니다. 외부 여부를 미리 알 수 있다면 외부 폼일 때만 새 탭을 여는 분기 고려해 주세요.

…ing-MOA-532

[Refactor] hooks 리팩토링 PR에 대한 추가수정
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Fix all issues with AI agents
In `@frontend/src/apis/application.ts`:
- Around line 29-52: Replace the English error label passed to withErrorHandling
in deleteApplication with a Korean message to match other functions: change the
second argument in deleteApplication (currently 'Error fetching delete
application') to a Korean string such as '지원서 삭제 중 오류 발생:' so it uses the same
withErrorHandling pattern as createApplication; locate the deleteApplication
function and update that argument accordingly.

In `@frontend/src/apis/image.ts`:
- Around line 136-157: The handleResponse error messages inside logoApi
functions are missing the "로고" prefix; update the failure messages in
getUploadUrl (the handleResponse call that currently reads `업로드 URL 생성 실패 :
${response.status}`) and in completeUpload (the handleResponse call that
currently reads `업로드 완료 처리 실패 : ${response.status}`) to include the "로고" prefix
(e.g., `로고 업로드 URL 생성 실패 : ...` and `로고 업로드 완료 처리 실패 : ...`) so they match the
outer withErrorHandling messages and the coverApi naming convention.

In `@frontend/src/hooks/Queries/useClub.ts`:
- Around line 15-16: There are duplicate imports of the same module under two
names (convertToDriveUrl and convertGoogleDriveUrl); remove the redundant import
and keep a single import (convertGoogleDriveUrl) from
'@/utils/convertGoogleDriveUrl', and update any usages that reference
convertToDriveUrl to call convertGoogleDriveUrl instead (e.g., replace
convertToDriveUrl(...) with convertGoogleDriveUrl(...)).
- Around line 85-103: The mutation expects updatedData to include an id so
onSuccess can invalidate the cache, but callers omit it; update the callers
(e.g., in ClubInfoEditTab and ClubIntroEditTab) to include clubDetail.id (or the
current club id) in the object passed to updateClubDetail so
useUpdateClubDetail's onSuccess sees variables.id and
queryClient.invalidateQueries(queryKeys.club.detail(variables.id)) runs
correctly; verify the objects constructed before calling updateClubDetail
include the id field.
♻️ Duplicate comments (2)
frontend/src/apis/auth.ts (1)

34-48: 로그아웃 시 토큰 정리와 서버 로그아웃 보장 필요
현재는 accessToken이 없으면 서버 로그아웃을 호출하지 않고, 로컬 토큰도 정리되지 않습니다. 세션/쿠키 정리 누락과 stale 토큰 사용 리스크가 있습니다.

🛠️ 수정 제안
 export const logout = async (): Promise<void> => {
   return withErrorHandling(async () => {
-    const accessToken = localStorage.getItem('accessToken');
-
-    if (!accessToken) {
-      return;
-    }
-
-    const response = await fetch(`${API_BASE_URL}/auth/user/logout`, {
-      method: 'GET',
-      credentials: 'include',
-    });
-
-    await handleResponse(response, '로그아웃에 실패하였습니다.');
+    try {
+      const response = await fetch(`${API_BASE_URL}/auth/user/logout`, {
+        method: 'GET',
+        credentials: 'include',
+      });
+      await handleResponse(response, '로그아웃에 실패하였습니다.');
+    } finally {
+      localStorage.removeItem('accessToken');
+    }
   }, '로그아웃 중 오류 발생');
 };
frontend/src/apis/application.ts (1)

154-173: active 필드 타입 불일치 확인

currentStatus를 boolean으로 변환하여 { active: newStatus }로 전송하고 있으나, ApplicationFormData.active 타입은 'active' | 'published' | 'unpublished' (소문자 문자열)로 정의되어 있습니다. 백엔드가 실제로 boolean을 기대하는지 확인이 필요합니다.

🧹 Nitpick comments (4)
frontend/src/pages/AdminPage/tabs/ApplicationListTab/ApplicationListTab.tsx (1)

135-145: Styled component를 컴포넌트 외부로 이동하세요.

ActiveListBodyActiveApplicationRow가 컴포넌트 함수 내부에서 정의되어 있어 매 렌더링마다 새로운 styled component가 생성됩니다. 이는 불필요한 재생성과 스타일 재계산을 유발하며, React의 reconciliation 성능에도 영향을 줍니다.

♻️ 제안 수정안

파일 상단(컴포넌트 외부)으로 이동:

+const ActiveListBody = styled(Styled.ApplicationList)`
+  border-top-left-radius: 0;
+`;
+
+const ActiveApplicationRow = styled(ApplicationRowItem)`
+  &:hover {
+    background-color: `#f2f2f2`;
+    &:first-child {
+      border-top-right-radius: 20px;
+    }
+  }
+`;
+
 const ApplicationListTab = () => {
   const navigate = useNavigate();
   // ...
-
-  const ActiveListBody = styled(Styled.ApplicationList)`
-    border-top-left-radius: 0;
-  `;
-  const ActiveApplicationRow = styled(ApplicationRowItem)`
-    &:hover {
-      background-color: `#f2f2f2`;
-      &:first-child {
-        border-top-right-radius: 20px;
-      }
-    }
-  `;
frontend/src/apis/image.ts (1)

5-13: 인터페이스 export 고려

PresignedDataFeedUploadRequest 인터페이스가 export되지 않았습니다. 이 API를 사용하는 훅이나 컴포넌트에서 타입 안전성을 위해 해당 타입들이 필요할 수 있습니다.

♻️ 인터페이스 export 제안
-interface PresignedData {
+export interface PresignedData {
   presignedUrl: string;
   finalUrl: string;
 }

-interface FeedUploadRequest {
+export interface FeedUploadRequest {
   fileName: string;
   contentType: string;
 }
frontend/src/apis/club.ts (1)

40-50: 에러 메시지 마침표 불일치

Line 40의 handleResponse 메시지는 마침표가 있지만(실패했습니다.), Line 50의 withErrorHandling 메시지는 마침표가 없습니다(실패했습니다). 일관성을 위해 통일해주세요.

♻️ 수정 제안
-  }, '클럽 데이터를 불러오는데 실패했습니다');
+  }, '클럽 데이터를 불러오는데 실패했습니다.');
frontend/src/hooks/Queries/useClub.ts (1)

25-40: 불필요한 타입 단언

Line 28에서 clubId as string은 이미 clubIdstring 타입으로 선언되어 있으므로 불필요합니다.

♻️ 수정 제안
-    queryFn: () => getClubDetail(clubId as string),
+    queryFn: () => getClubDetail(clubId),

- window.open() 방식에서 window.location.href로 변경
- API 호출 전 빈 창을 미리 여는 로직 제거
- 외부 지원서는 현재 탭에서 열리도록 개선
@seongwon030 seongwon030 merged commit 53bdef4 into main Jan 18, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

💻 FE Frontend 📈 release 릴리즈 배포

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

Comments