Skip to content

[배포] 이미지 전처리, 광고 기능, 하단바 리디자인, 인앱 알림 페이지 및 토스트 프로덕션 배포#230

Open
ff1451 wants to merge 29 commits intomainfrom
develop
Open

[배포] 이미지 전처리, 광고 기능, 하단바 리디자인, 인앱 알림 페이지 및 토스트 프로덕션 배포#230
ff1451 wants to merge 29 commits intomainfrom
develop

Conversation

@ff1451
Copy link
Copy Markdown
Collaborator

@ff1451 ff1451 commented Mar 27, 2026

Summary by CodeRabbit

  • 새 기능

    • 알림 페이지 추가 및 실시간 인앱 알림 토스트(스트림) 지원
    • 채팅·클럽 목록에 광고 카드 표시 및 광고 클릭 추적
    • 하단 네비게이션에 알림·채팅 탭 추가
  • 개선 사항

    • 알림 읽음 처리와 언릿 카운트 동기화 개선
    • 헤더/서브페이지 헤더 표기·네비게이션 개선
    • 레이아웃·뷰포트 잠금 및 키보드/안전영역 처리 개선 (스크롤/오버레이 안정화)

ff1451 added 11 commits March 22, 2026 04:31
* feat: 전처리 로직 및 WebWorker 구현

* feat: 전처리 적용 및 preview 동시성 제어 로직 추가

* refactor: 리뷰 반영
* hotfix: 하단바 너비 수정

* chore: 불필요한 값 제거

* refactor: 고정 gap 제거
* chore: asset 추가

* feat: 하단바 리디자인 반영 및 레이아웃 수정
* refactor: 광고 개수 측정 시기 변경 및 기본값 제거

* feat: 스켈레톤 UI 추가
* feat: 알림 API 및 스트림 기반 추가

* feat: 인앱 알림 레이어 추가

* feat: 알림 페이지 및 헤더 진입 구현

* fix: 알림 스트림 401 재시도 조건 정리

* fix: 알림 목록 이동 차단 제거

* refactor: 알림 공용 훅 위치 정리

* fix: 알림 재연결 캐시 동기화 추가

* fix: 알림 목록 토스트 큐 누적 방지

* fix: 알림 읽음 카운트 감소 조건 보강
* chore: pwa용 이미지 제거

* refactor: auth 도메인 쿼리와 뮤테이션 정리

* refactor: council과 schedule 조회 훅 정리

* refactor: chat과 notification 캐시 처리 정리

* refactor: club 조회와 지원 플로우 정리

* refactor: manager 도메인 캐시 처리 정리

* refactor: studyTime 도메인 쿼리와 뮤테이션 정리

* refactor: 광고와 업로드 도메인 훅 정리
* refactor: auth와 user myInfo 훅 정리

* refactor: club과 schedule 조회 훅 정리

* refactor: chat과 notification 훅 구조 정리

* refactor: club 지원 뮤테이션 훅 정리

* refactor: manager 뮤테이션 훅 구조 정리

* refactor: mutation 훅 cache 정리

* refactor: 컨벤션 통일

* refactor: isRead 조건 정리

* fix: 채팅 스크롤 문제 수정

* refactor: 불필요한 코드 제거
* chore: 가공용 safeArea 변수 선언

* refactor: 고정 패딩 값 수정 및 safeArea 적용 변경

* feat: 키보드 활성화 감지 및 safeArea 적용 여부 기능 추가

* refactor: 매직넘버 상수화 및 가로모드 처리
* refactor: 채팅 viewport 훅 네이밍 정리

* refactor: viewport 높이 잠금 훅 적용 시점 조정
* refactor: 채팅 viewport 훅 네이밍 정리

* refactor: viewport 높이 잠금 훅 적용 시점 조정

* fix: 채팅 화면 스크롤 잠금으로 키보드 흔들림 완화
@ff1451 ff1451 self-assigned this Mar 27, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 27, 2026

Note

Currently processing new changes in this PR. This may take a few minutes, please wait...

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 09c162c7-789a-40c3-beed-f870e6c2c03c

📥 Commits

Reviewing files that changed from the base of the PR and between 7da1524 and ce31ca9.

📒 Files selected for processing (8)
  • src/apis/studyTime/queries.ts
  • src/pages/Auth/SignUp/components/AgreementArrow.tsx
  • src/pages/Manager/ManagedClubProfile/index.tsx
  • src/pages/Manager/ManagedMemberList/index.tsx
  • src/pages/Manager/ManagedRecruitmentWrite/index.tsx
  • src/utils/ts/imagePreprocessor.ts
  • src/utils/ts/notification.ts
  • src/utils/ts/promise.ts
 ________________________________________________________________________________________________________________________
< I've finally learned what 'upward compatible' means. It means we get to keep all our old mistakes. - Dennie van Tassel >
 ------------------------------------------------------------------------------------------------------------------------
  \
   \   (\__/)
       (•ㅅ•)
       /   づ

Walkthrough

라우팅과 레이아웃을 확장하고 React Query 설정을 대규모로 중앙화했습니다. App에 lazy 로딩된 NotificationsPage와 최상위 /chats 경로를 추가하고 기존 채팅 라우팅을 단순화했습니다. 다수의 API 타입/엔티티(광고, 알림 등)와 관련된 apis/* 모듈(queries/mutations/index)들과 이를 사용하는 훅들이 추가·리팩토링되었으며, SSE 기반 인박스 알림 스트림, 인앱 토스트 및 캐시 갱신 유틸리티가 도입되었습니다. 레이아웃(헤더/바텀네비), 뷰포트 잠금/키보드 처리, 이미지 전처리 및 광고 삽입 로직도 변경되었습니다.

Possibly related PRs

  • #219: 도메인별 TanStack Query 쿼리/뮤테이션 옵션 팩토리 추가 및 호출부 마이그레이션 — 본 PR과 동일한 query/mutation 중앙화 작업과 강한 코드 수준 연관.
  • #217: 인박스 알림 전체 기능(NotificationsPage, SSE 스트림, 캐시 헬퍼, InboxNotificationLayer) 구현 — 알림 관련 파일·흐름이 직접적으로 겹칩니다.
  • #200: 광고 엔터티/쿼리/훅 및 UI(AdvertisementCard, useAdvertisementInterval, useAdvertisements) 도입 및 목록에 광고 삽입 — 채팅/클럽 목록 광고 삽입 코드와 직접 중첩됩니다.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch develop

ff1451 added 5 commits March 27, 2026 22:11
* fix: 채팅 화면 상단 고정 깨짐과 빈 공간 잔류 수정

* refactor: 라우트 조건 수정

* fix: 문서 스크롤 위치 감지 보강

* refactor: 입력 요소 판별 유틸과 스크롤 주석 정리
* fix: 채팅 문서 스크롤 제스처 차단

* fix: 입력 요소 터치 동작 예외 처리
* fix: 키보드 활성화 시 채팅 하단 정렬 유지

* refactor: 채팅 리사이즈 관찰 안정화
Copy link
Copy Markdown

@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: 8

Caution

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

⚠️ Outside diff range comments (1)
src/pages/Chat/ChatRoom.tsx (1)

82-84: ⚠️ Potential issue | 🟡 Minor

chatRoomId undefined 처리 필요

useParamschatRoomId가 undefined일 수 있어 Number(chatRoomId)NaN을 반환할 수 있습니다.

🛡️ 제안
 function ChatRoom() {
   const { chatRoomId } = useParams();
+  if (!chatRoomId) return null;
   const { sendMessage, chatMessages, fetchNextPage, hasNextPage, isFetchingNextPage, chatRoomList, isSendingMessage } =
     useChat(Number(chatRoomId));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/Chat/ChatRoom.tsx` around lines 82 - 84, The code currently passes
Number(chatRoomId) into useChat which will produce NaN if useParams() returned
undefined for chatRoomId; update ChatRoom.tsx to validate and normalize
chatRoomId before calling useChat (e.g., check that chatRoomId from useParams()
is a defined numeric string, parse it with parseInt or Number only after the
check, or provide a safe fallback id or early return UI when chatRoomId is
missing). Ensure the change touches the lines using useParams, the chatRoomId
variable, and the useChat(...) call so useChat always receives a valid number.
🧹 Nitpick comments (45)
src/apis/inquiry/mutations.ts (1)

2-2: 상대 경로 import 대신 @/* 별칭을 사용해주세요.

from '.'는 현재 컨벤션과 맞지 않습니다. @/apis/inquiry 형태로 변경하는 게 좋습니다.

As per coding guidelines **/*.{ts,tsx}: Use @/* alias for import paths instead of relative paths.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/apis/inquiry/mutations.ts` at line 2, The import in mutations.ts uses a
relative path; replace "import { postInquiry } from '.';" with the project alias
form so it follows the guideline (use the "@/..." alias); update the import to
reference the inquiry module via the alias (e.g., import { postInquiry } from
'@/apis/inquiry') making sure to update any other similar imports in the file
that reference the inquiry index.
src/apis/studyTime/mutations.ts (1)

2-3: 상대 경로 대신 path alias 사용 권장

♻️ 수정 제안
-import type { StopTimerRequest } from './entity';
-import { startStudyTimer, stopStudyTimer } from '.';
+import type { StopTimerRequest } from '@/apis/studyTime/entity';
+import { startStudyTimer, stopStudyTimer } from '@/apis/studyTime';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/apis/studyTime/mutations.ts` around lines 2 - 3, The imports in this
module use relative paths; replace them with your project path alias to keep
imports consistent and resilient—change "import type { StopTimerRequest } from
'./entity';" to use the configured alias (e.g. import type { StopTimerRequest }
from '@/apis/studyTime/entity';) and change "import { startStudyTimer,
stopStudyTimer } from '.';" to the alias form that explicitly imports
startStudyTimer and stopStudyTimer from the studyTime index (e.g. import {
startStudyTimer, stopStudyTimer } from '@/apis/studyTime';) ensuring the symbol
names StopTimerRequest, startStudyTimer, and stopStudyTimer remain unchanged.
src/apis/studyTime/hooks.ts (1)

3-4: 상대 경로 대신 path alias 사용 권장

♻️ 수정 제안
-import { studyTimeMutations } from './mutations';
-import { studyTimeQueryKeys } from './queries';
+import { studyTimeMutations } from '@/apis/studyTime/mutations';
+import { studyTimeQueryKeys } from '@/apis/studyTime/queries';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/apis/studyTime/hooks.ts` around lines 3 - 4, Replace the relative imports
for studyTimeMutations and studyTimeQueryKeys in hooks.ts with your project's
path aliases (e.g., use `@/`... style aliases) so imports use the configured alias
instead of a relative path; update the import lines that bring in
studyTimeMutations and studyTimeQueryKeys to the alias form and verify
TypeScript/tsconfig paths and build tooling resolve them.
src/pages/Timer/hooks/useStudyTimer.ts (1)

2-2: 상대 경로 대신 path alias 사용 권장

♻️ 수정 제안
-import { useStudyTime } from './useStudyTime';
+import { useStudyTime } from '@/pages/Timer/hooks/useStudyTime';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/Timer/hooks/useStudyTimer.ts` at line 2, In useStudyTimer.ts
replace the relative import of the useStudyTime hook (import { useStudyTime }
from './useStudyTime') with the project path-alias form your codebase uses (e.g.
import from the alias root like `@/`... or src/...) so all imports are consistent
with path-alias configuration; update the import statement to reference the
alias and ensure your tsconfig/webpack alias entry is respected and tests/build
still resolve the module.
src/apis/studyTime/queries.ts (1)

2-3: 상대 경로 대신 path alias 사용 권장

코딩 가이드라인에 따라 @/* alias를 사용해주세요.

♻️ 수정 제안
-import type { StudyRankingParams } from './entity';
-import { getMyStudyTimeRanking, getStudyTimeRanking, getStudyTimeSummary } from '.';
+import type { StudyRankingParams } from '@/apis/studyTime/entity';
+import { getMyStudyTimeRanking, getStudyTimeRanking, getStudyTimeSummary } from '@/apis/studyTime';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/apis/studyTime/queries.ts` around lines 2 - 3, Replace the relative
import paths with the project path alias: change the import of
StudyRankingParams from './entity' to '@/apis/studyTime/entity' (or the
appropriate aliased module) and change the imports of getMyStudyTimeRanking,
getStudyTimeRanking, getStudyTimeSummary from '.' to '@/apis/studyTime' (or the
aliased entrypoint); update the import statements that reference
StudyRankingParams, getMyStudyTimeRanking, getStudyTimeRanking, and
getStudyTimeSummary to use the `@/`* alias so they follow the coding guideline.
src/pages/Schedule/index.tsx (1)

31-34: useQuery vs useSuspenseQuery 사용 검토

코딩 가이드라인에서 useSuspenseQuery 사용을 권장하고 있습니다. 현재 enabled 옵션과 함께 useQuery를 사용 중인데, 이 페이지가 route entry이고 schedules ?? [] 폴백이 있어 의도적인 선택으로 보입니다.

useSuspenseQuery로 전환할 경우 Suspense boundary가 필요하며, 초기 로딩 상태 처리 방식이 달라집니다. 현재 구현이 의도된 것이라면 무시해도 됩니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/Schedule/index.tsx` around lines 31 - 34, The code uses
useQuery({...scheduleQueries.monthly({ year, month }), enabled: Boolean(year &&
month)}) which the review flags because project guidelines prefer
useSuspenseQuery for route-entry data; either replace useQuery with
useSuspenseQuery and remove the enabled gating (wrapping this page in a Suspense
boundary and handling loading via Suspense/Fallback), or keep useQuery but add a
brief comment by the useQuery line referencing why schedules ?? [] fallback and
the enabled flag are intentional; locate the call by the useQuery invocation and
scheduleQueries.monthly({ year, month }) to apply the change.
src/apis/club/queries.ts (1)

89-101: pageParam = 1 기본값 중복

Line 92의 pageParam = 1은 Line 93의 initialPageParam: 1과 중복됩니다. 제거해도 동작에 영향 없습니다.

♻️ 중복 기본값 제거
  infiniteList: (params: ClubInfiniteListParams) =>
    infiniteQueryOptions({
      queryKey: clubQueryKeys.infinite.list(params),
-     queryFn: ({ pageParam = 1 }) => getClubs(buildClubListRequest(params, pageParam)),
+     queryFn: ({ pageParam }) => getClubs(buildClubListRequest(params, pageParam)),
      initialPageParam: 1,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/apis/club/queries.ts` around lines 89 - 101, The default pageParam in the
infiniteList query is duplicated: remove the inline default in the queryFn
parameter and rely on initialPageParam instead; specifically, update the
infiniteList definition so queryFn uses ({ pageParam }) =>
getClubs(buildClubListRequest(params, pageParam)) (keeping initialPageParam: 1
and getNextPageParam logic intact) to avoid redundant defaults while preserving
behavior in infiniteQueryOptions and buildClubListRequest.
src/apis/chat/queries.ts (1)

18-31: chatRoomId! 비널 단언 사용 확인

enabled: Boolean(chatRoomId)로 인해 queryFnchatRoomId가 있을 때만 실행되므로 안전합니다. 다만 skipToken 패턴을 고려해볼 수 있습니다.

♻️ skipToken 사용 대안 (optional)
+import { infiniteQueryOptions, queryOptions, skipToken } from '@tanstack/react-query';
...
  messages: (chatRoomId?: number, limit = 20) =>
    infiniteQueryOptions({
      queryKey: chatRoomId ? chatQueryKeys.messages(chatRoomId) : chatQueryKeys.disabledMessages(),
-     queryFn: ({ pageParam }) =>
-       getChatMessages({
-         chatRoomId: chatRoomId!,
-         page: pageParam,
-         limit,
-       }),
+     queryFn: chatRoomId
+       ? ({ pageParam }) => getChatMessages({ chatRoomId, page: pageParam, limit })
+       : skipToken,
      initialPageParam: 1,
      getNextPageParam: (lastPage: ChatMessagesResponse) =>
        lastPage.currentPage < lastPage.totalPage ? lastPage.currentPage + 1 : undefined,
-     enabled: Boolean(chatRoomId),
    }),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/apis/chat/queries.ts` around lines 18 - 31, The current messages factory
uses a non-null assertion chatRoomId! inside queryFn even though enabled:
Boolean(chatRoomId) prevents execution; replace the assertion with a safe guard
or React Query's skipToken pattern so the queryFn never receives
undefined—specifically update messages (the infiniteQueryOptions call) to only
call getChatMessages when chatRoomId is defined (or use a skipToken-style
queryKey and early-return/no-op queryFn), and keep the queryKey logic using
chatQueryKeys.messages(chatRoomId) / chatQueryKeys.disabledMessages() and the
enabled flag consistent to avoid the chatRoomId! assertion.
src/apis/notification/index.ts (1)

2-2: 상대 경로 대신 path alias 사용 권장

코딩 가이드라인에 따라 @/* path alias를 사용해주세요.

♻️ 수정 제안
-import { apiClient } from '../client';
+import { apiClient } from '@/apis/client';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/apis/notification/index.ts` at line 2, Replace the relative import in
src/apis/notification/index.ts with the project path alias: change the import of
apiClient from "../client" to use the "@/apis/client" alias (i.e. import {
apiClient } from '@/apis/client'), ensuring the apiClient symbol remains
referenced unchanged.
src/pages/Manager/ManagedMemberApplicationDetail/index.tsx (1)

7-11: params 변환 시 NaN 가능성

Number(params.clubId)Number(params.userId)undefined일 경우 NaN이 됩니다. 라우트 설정상 항상 존재한다면 괜찮지만, 방어적 처리를 고려해볼 수 있습니다.

🔧 방어적 처리 예시
 const clubId = Number(params.clubId);
 const userId = Number(params.userId);
+
+if (Number.isNaN(clubId) || Number.isNaN(userId)) {
+  throw new Error('Invalid route parameters');
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/Manager/ManagedMemberApplicationDetail/index.tsx` around lines 7 -
11, The code converts route params directly with Number(params.clubId) and
Number(params.userId) which yields NaN if params are undefined; update the
component to defensively parse and validate params from useParams() (e.g., use
parseInt with radix or Number.isInteger checks) before calling
managedClubQueries.memberApplicationDetail; if parsing fails, handle it early
(return a fallback UI, throw a controlled error, or navigate away) so
useSuspenseQuery isn't invoked with NaN values and functions like clubId,
userId, useSuspenseQuery, and managedClubQueries.memberApplicationDetail only
receive validated numeric IDs.
src/apis/notification/mutations.ts (1)

2-2: 상대 경로 대신 경로 별칭 사용 필요

코딩 가이드라인에 따라 @/* 경로 별칭을 사용해주세요.

🔧 수정 제안
-import { markInboxNotificationAsRead } from '.';
+import { markInboxNotificationAsRead } from '@/apis/notification';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/apis/notification/mutations.ts` at line 2, Replace the relative import
for markInboxNotificationAsRead with the project path-alias form using `@/`*;
locate the import statement that currently reads import {
markInboxNotificationAsRead } from '.' and change it to use the alias (e.g.,
import { markInboxNotificationAsRead } from '@/...') so it complies with the
codebase guideline for path aliases and resolves from the notification module
symbol.
src/apis/club/managedQueries.ts (2)

17-17: 상대 경로 대신 경로 별칭 사용 필요

🔧 수정 제안
-} from '.';
+} from '@/apis/club';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/apis/club/managedQueries.ts` at line 17, Replace the relative import
"from '.'" in managedQueries.ts with the project's configured path alias to
avoid fragile relative paths; locate the import statement that ends with " }
from '.';" and update it to use the TypeScript/webpack alias (e.g.,
"@/apis/club" or the project's specified alias) so the module resolution uses
the alias configured in tsconfig/paths, then run a build/type-check to ensure
the new alias resolves correctly.

63-90: queryFn에서 null 반환 시 타입 주의

infiniteQueryOptions에서 queryFnnull을 반환할 수 있어 getNextPageParam에서 lastPagenull일 수 있습니다. 현재 !lastPage 체크로 잘 처리되어 있지만, 반환 타입이 명시적이지 않습니다.

타입 안전성을 위해 반환 타입을 명시하거나, 소비하는 쪽에서 null 케이스를 인지하도록 문서화하는 것을 권장합니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/apis/club/managedQueries.ts` around lines 63 - 90, The queryFn used in
applications (inside infiniteQueryOptions with key
managedClubQueryKeys.applicationsInfinite) can return null on
NOT_FOUND_CLUB_RECRUITMENT, so explicitly type the queryFn/infiniteQueryOptions
return type to Promise<YourResponseType | null> (the type returned by
getManagedClubApplications) so callers know null is possible; update the
generic/type annotation around queryFn or infiniteQueryOptions to include | null
and keep getNextPageParam (referencing getNextPageParam and lastPage) handling
the null case as-is.
src/utils/ts/notification.ts (1)

52-54: 타입 단언 대신 타입 가드 고려

notification.type as NotificationInboxType 타입 단언이 사용되었습니다. notification.type이 이미 NotificationInboxType이라면 단언이 불필요하고, 아니라면 런타임에 예상치 못한 동작이 발생할 수 있습니다.

현재 ?? DEFAULT_NOTIFICATION_PRESENTATION fallback이 있어 안전하지만, 타입 정의를 확인해보시기 바랍니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/utils/ts/notification.ts` around lines 52 - 54, The use of a type
assertion in getInboxNotificationPresentation hides whether notification.type is
actually a valid NotificationInboxType; replace the assertion with a runtime
type guard (e.g., implement isNotificationInboxType(value): value is
NotificationInboxType) and use it to look up NOTIFICATION_PRESENTATIONS, falling
back to DEFAULT_NOTIFICATION_PRESENTATION when the guard returns false; ensure
types InboxNotification, NotificationInboxType, NOTIFICATION_PRESENTATIONS,
DEFAULT_NOTIFICATION_PRESENTATION and the new isNotificationInboxType are
referenced so the change is localized to getInboxNotificationPresentation.
src/pages/Manager/ManagedMemberList/index.tsx (1)

74-77: 하드코딩된 색상 값을 테마 토큰으로 대체 고려

#A5B3C1, #69BFDF, #C6CFD8 등 여러 하드코딩된 색상이 사용되고 있습니다. src/styles/theme.css의 색상 토큰 사용을 권장합니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/Manager/ManagedMemberList/index.tsx` around lines 74 - 77, Replace
hardcoded hex color literals (e.g., border-[`#A5B3C1`], and other occurrences like
`#69BFDF`, `#C6CFD8`) with the corresponding theme tokens from src/styles/theme.css;
update the className usages in this file (notably the container element with
className including border-[`#A5B3C1`] and any nearby elements/icons such as
RoleSelectorArrowDownIcon) to use the theme token names (e.g.,
border-[var(--token-name)] or the utility classes that map to those tokens) so
colors derive from the centralized theme.
src/components/layout/Header/headerConfig.ts (1)

46-47: 비교 연산자 일관성 확인 필요

Line 46에서 == 대신 ===를 사용하면 다른 config 항목들과 일관성이 유지됩니다.

제안
-    match: (pathname) => pathname == '/schedule',
+    match: (pathname) => pathname === '/schedule',
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/layout/Header/headerConfig.ts` around lines 46 - 47, The match
function uses a loose equality operator; update the arrow function match to use
strict equality (===) when comparing pathname to '/schedule' to maintain
consistency with other config entries (look for the match: (pathname) =>
pathname == '/schedule' definition in headerConfig and change to ===).
src/pages/Club/ClubDetail/components/ClubRecruitment.tsx (1)

91-93: 이미지 key로 index 사용

이미지 목록이 정적이라면 괜찮지만, 이미지에 고유 ID(image.id 또는 image.url)가 있다면 그것을 key로 사용하는 것이 더 안전합니다.

제안
-            {clubRecruitment.images.map((image, index) => (
-              <img key={index} src={image.url} alt={`모집 공고 이미지 ${index + 1}`} className="w-full rounded-sm" />
+            {clubRecruitment.images.map((image) => (
+              <img key={image.url} src={image.url} alt="모집 공고 이미지" className="w-full rounded-sm" />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/Club/ClubDetail/components/ClubRecruitment.tsx` around lines 91 -
93, The map over clubRecruitment.images uses the array index as the React key
which can cause rendering issues; change the key to a stable unique identifier
such as image.id or image.url in the map callback (the mapping that renders
<img> inside the clubRecruitment.images.map in ClubRecruitment.tsx) so each
<img> uses a consistent unique key instead of index.
src/pages/Club/ClubDetail/index.tsx (1)

3-3: cn() 유틸리티 사용 권장

코딩 가이드라인에 따르면 Tailwind 클래스 병합 시 cn() 유틸리티를 사용해야 합니다. clsx 직접 사용 대신 cn()으로 통일하면 좋겠습니다.

제안
-import clsx from 'clsx';
+import { cn } from '@/utils/ts/cn';
 className={cn(
-              clsx(
-                'text-sub2 h-[38px] border-b-[1.6px] px-3 transition-colors',
-                currentTab === tab.key ? 'border-[`#69BFDF`] text-[`#69BFDF`]' : 'border-transparent text-indigo-300'
-              )
+              'text-sub2 h-[38px] border-b-[1.6px] px-3 transition-colors',
+              currentTab === tab.key ? 'border-[`#69BFDF`] text-[`#69BFDF`]' : 'border-transparent text-indigo-300'
             )}

As per coding guidelines: "Use cn() utility from src/utils/ts/cn.ts to merge Tailwind CSS classes"

Also applies to: 74-77

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/Club/ClubDetail/index.tsx` at line 3, Replace the direct use of the
clsx utility with the project's cn() utility: change the import of clsx to
import cn from the cn utility and update all usages like className={clsx(...)}
to className={cn(...)} (including the occurrences referenced around lines
74-77); ensure the import references the cn utility (src/utils/ts/cn.ts) and
preserve any conditional class logic when converting each clsx call to cn().
src/pages/Auth/SignUp/FinishStep.tsx (1)

11-13: 불필요한 null 체크

useSuspenseQuery는 데이터가 준비될 때까지 Suspense로 렌더링을 지연시키므로, 컴포넌트가 렌더링될 때 myInfo는 항상 존재합니다. 이 null 체크는 실행되지 않는 코드입니다.

제안
 function FinishStep() {
   const navigate = useNavigate();
   const { data: myInfo } = useSuspenseQuery(authQueries.myInfo());
   const setUser = useAuthStore((state) => state.setUser);

-  if (!myInfo) {
-    return null;
-  }
-
   const handleStart = () => {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/Auth/SignUp/FinishStep.tsx` around lines 11 - 13, Remove the
redundant null check for myInfo in the FinishStep component: because myInfo is
loaded via useSuspenseQuery (or a suspense-backed hook) it will always be
present when the component renders, so delete the if (!myInfo) return null;
block and any dead branches depending on it; update any imports/logic in
FinishStep that only existed to guard that branch and ensure the component
relies directly on myInfo returned by the suspense query.
src/pages/Club/ClubDetail/components/ClubMember.tsx (3)

64-66: useQuery 대신 useSuspenseQuery 사용 검토 필요

다른 컴포넌트들(ClubAccount, CouncilDetail)은 useSuspenseQuery를 사용하고 있는데, 이 컴포넌트만 useQuery를 사용하고 있습니다. 일관성을 위해 useSuspenseQuery로 변경하는 것이 좋습니다.

또한 Line 66에서 clubMembers?.clubMembers.lengthclubMembersundefined일 때 런타임 에러가 발생할 수 있습니다.

♻️ 수정 제안
-  const { data: clubMembers } = useQuery(clubQueries.members(Number(clubId)));
+  const { data: clubMembers } = useSuspenseQuery(clubQueries.members(Number(clubId)));
   const members = clubMembers?.clubMembers ?? [];
-  const totalMembers = clubMembers?.clubMembers.length ?? memberCount;
+  const totalMembers = clubMembers?.clubMembers?.length ?? memberCount;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/Club/ClubDetail/components/ClubMember.tsx` around lines 64 - 66,
Replace useQuery with useSuspenseQuery for consistency (call
useSuspenseQuery(clubQueries.members(Number(clubId)))) and keep the same
destructuring name clubMembers; compute members as clubMembers?.clubMembers ??
[] and compute totalMembers safely using optional chaining and a fallback like
clubMembers?.clubMembers?.length ?? memberCount so accessing .length cannot
throw; update any related imports from useQuery to useSuspenseQuery and adjust
variable usage in ClubMember component accordingly.

14-18: 하드코딩된 색상값 → 테마 토큰 사용 검토

#69BFDF, #F6DE8C, #FFB8B8 등 하드코딩된 색상값이 사용되고 있습니다. 가능하다면 src/styles/theme.css의 색상 토큰을 사용하는 것이 좋습니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/Club/ClubDetail/components/ClubMember.tsx` around lines 14 - 18,
Replace the hardcoded hex values in POSITION_BADGE_STYLES with theme tokens (CSS
variables or theme constants) from your theme (e.g., values defined in
src/styles/theme.css) so the badge colors follow the app theme; update the map
entries in POSITION_BADGE_STYLES (referencing POSITION_BADGE_STYLES and
PositionType) to use the appropriate token strings (like "var(--token-name)" or
imported theme constant) instead of '#69BFDF', '#F6DE8C', '#FFB8B8', and ensure
the type remains Partial<Record<PositionType, string>> and any components that
consume these values still receive the token string.

1-2: cn() 유틸리티 사용 권장

코딩 가이드라인에 따르면 Tailwind 클래스 병합 시 clsx 대신 @/utils/ts/cn.tscn() 유틸리티를 사용해야 합니다.

♻️ 수정 제안
-import { useQuery } from '@tanstack/react-query';
-import clsx from 'clsx';
+import { useSuspenseQuery } from '@tanstack/react-query';
 import { useParams } from 'react-router-dom';
 import type { ClubMember, PositionType } from '@/apis/club/entity';
 import { clubQueries } from '@/apis/club/queries';
+import { cn } from '@/utils/ts/cn';

As per coding guidelines: "Use cn() utility from src/utils/ts/cn.ts to merge Tailwind CSS classes"

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/Club/ClubDetail/components/ClubMember.tsx` around lines 1 - 2, The
file imports and uses the third-party clsx utility; replace that with your
project's cn() helper: remove the import of clsx and add an import for cn from
the local utility (src/utils/ts/cn.ts or '@/utils/ts/cn'), then update all
usages of clsx(...) in this module (e.g., within the ClubMember component and
any className expressions) to call cn(...) instead so Tailwind classes are
merged via the project's standard helper.
src/pages/Council/CouncilDetail/index.tsx (2)

28-30: useSuspenseQuery 사용 시 불필요한 null 체크

useSuspenseQuery는 데이터가 로드될 때까지 suspend하므로, councilInfonull인 경우는 발생하지 않습니다. 이 guard는 dead code입니다.

♻️ 수정 제안
   const { data: councilInfo } = useSuspenseQuery(councilQueries.info());
 
-  if (!councilInfo) {
-    return <div>잘못된 경로입니다</div>;
-  }
-
   return (
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/Council/CouncilDetail/index.tsx` around lines 28 - 30, Remove the
dead null-guard around councilInfo that returns the Korean error div; because
councilInfo is provided by useSuspenseQuery and the component suspends until
data exists, delete the if (!councilInfo) { return <div>잘못된 경로입니다</div>; } block
(in CouncilDetail/index.tsx) and adjust any local code that assumed a nullable
councilInfo type (e.g., refine prop/type annotations or remove unnecessary
non-null assertions) so the component uses councilInfo as a present value.

3-3: cn() 유틸리티 사용 권장

Line 50에서 clsx를 직접 사용하고 있습니다. 코딩 가이드라인에 따라 cn()을 사용하세요.

As per coding guidelines: "Use cn() utility from src/utils/ts/cn.ts to merge Tailwind CSS classes"

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/Council/CouncilDetail/index.tsx` at line 3, Replace direct usage of
the clsx utility in the CouncilDetail component with the project's cn() utility:
remove the existing import of clsx and import the cn function from the shared
utility (cn from src/utils/ts/cn.ts), then change all calls to clsx(...) to
cn(...). Target the import statement that currently brings in clsx and every
occurrence of the clsx symbol in CouncilDetail/index.tsx so the component uses
cn(...) consistently.
src/pages/Club/ClubDetail/components/ClubAccount.tsx (1)

6-8: clubId undefined 처리 검토

useParams()clubIdundefined일 경우 Number(undefined)NaN을 반환합니다. 라우트 구조상 항상 존재한다면 괜찮지만, 방어적 코딩을 위해 검증을 추가하는 것도 고려해보세요.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/Club/ClubDetail/components/ClubAccount.tsx` around lines 6 - 8, The
ClubAccount component uses useParams() and passes Number(clubId) into
useSuspenseQuery(clubQueries.fee(...)) which will become NaN if clubId is
undefined; update ClubAccount to validate clubId (from useParams) before calling
useSuspenseQuery — e.g., check that clubId is a defined string and that
Number(clubId) is a valid number (or use parseInt and !isNaN), and handle the
invalid case by returning a fallback UI, throwing a descriptive error, or
redirecting, so that clubQueries.fee is only called with a valid numeric id.
src/pages/Manager/ManagedRecruitment/index.tsx (1)

48-49: Typography 토큰 사용 권장

text-sm, text-xs 대신 text-sub3, text-cap1 등 시맨틱 typography 유틸리티 사용을 고려해주세요.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/Manager/ManagedRecruitment/index.tsx` around lines 48 - 49, Replace
the raw utility font-size classes on the title and content nodes with the
semantic typography tokens: change the div rendering {title} currently using
"text-sm" to the semantic token (e.g., "text-sub3") and change the div rendering
{content} currently using "text-xs" to the semantic token (e.g., "text-cap1"),
keeping existing leading and color classes (leading-[1.6], text-indigo-700/300)
intact; update the two divs that render title and content in the
ManagedRecruitment component accordingly so they use the project's semantic
typography utilities.
src/pages/Manager/ManagedApplicationList/index.tsx (1)

15-15: Path alias 사용 권장

코딩 가이드라인에 따라 상대 경로 대신 @/* 경로 alias를 사용해주세요.

♻️ 제안
-import { useGetManagedApplications } from '../hooks/useManagedApplications';
+import { useGetManagedApplications } from '@/pages/Manager/hooks/useManagedApplications';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/Manager/ManagedApplicationList/index.tsx` at line 15, 현재 파일의 상대경로
import of useGetManagedApplications uses "../hooks/useManagedApplications";
replace that relative import with the project path alias form (use the "@/..."
alias) so the module import targets the same hooks module
(useManagedApplications) via the alias instead of a relative path; update the
import statement that references useGetManagedApplications to use the
"@/…/useManagedApplications" alias form accordingly.
src/pages/Club/ClubList/components/ClubCard.tsx (1)

35-61: 하드코딩된 색상값 대신 theme.css 토큰 사용 권장

#FEF3C7, #B45309 등 하드코딩된 색상값이 있습니다. 코딩 가이드라인에 따르면 src/styles/theme.css의 색상 토큰을 우선 사용해야 합니다.

해당 상태별 색상이 테마에 없다면 CSS 변수로 추가하는 것을 고려해주세요.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/Club/ClubList/components/ClubCard.tsx` around lines 35 - 61, The
clubTag construction in ClubCard (the self-invoking that sets clubTag: ClubTag |
null) uses hardcoded hex colors (e.g., '#FEF3C7', '#B45309', '#ECFDF5',
'#047857', '#EFF6FF', '#1D4ED8'); replace those literal values with the theme
CSS tokens (CSS variables) defined in src/styles/theme.css (e.g., use
"var(--<token-name>)") so bgColor and textColor return token references instead
of hex values; if the exact token does not exist for a given state
(isPendingApproval, isAlwaysRecruiting, status === 'ONGOING'), add appropriately
named tokens to theme.css and then reference them from ClubCard (and ensure the
ClubTag type accepts string CSS variables).
src/pages/Manager/ManagedRecruitmentForm/index.tsx (1)

4-4: cn() 유틸리티 사용 권장

twMerge를 직접 import하기보다 이미 import된 cn() 유틸리티를 일관되게 사용하는 것이 좋습니다. cn()clsxtailwind-merge를 함께 처리합니다.

♻️ 제안된 수정
-import { twMerge } from 'tailwind-merge';

Line 128, 137의 twMerge 호출을 cn()으로 대체:

-<div className={twMerge(sectionCardStyle, 'items-center py-10 text-center')}>
+<div className={cn(sectionCardStyle, 'items-center py-10 text-center')}>

As per coding guidelines: "Use cn() utility from src/utils/ts/cn.ts to merge Tailwind CSS classes"

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/Manager/ManagedRecruitmentForm/index.tsx` at line 4, Replace direct
usage of twMerge with the project's cn() utility: remove the twMerge import and
import cn() instead, then swap the two twMerge(...) calls inside the
ManagedRecruitmentForm component for cn(...) so clsx + tailwind-merge behavior
is preserved (the twMerge usages at the two className merge sites in
ManagedRecruitmentForm should be updated to call cn()).
src/components/layout/Header/components/SubpageHeader.tsx (1)

30-35: 서브페이지 title은 heading으로 두는 편이 낫습니다.

지금 구조에선 실제 페이지 제목이 span이라 스크린리더 heading 탐색에서 빠집니다. 태그만 h1으로 바꿔도 접근성이 좋아집니다.

예시
-        <span className="text-sub1 truncate text-indigo-700">{title}</span>
+        <h1 className="text-sub1 truncate text-indigo-700">{title}</h1>
As per coding guidelines "접근성(aria-*, role, 키보드 탐색)이 적절히 처리되는지".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/layout/Header/components/SubpageHeader.tsx` around lines 30 -
35, The page title is rendered as a span which prevents screen readers from
discovering it as a heading; update the SubpageHeader so the element showing
title (currently the span that uses title, className and truncate styles,
adjacent to the back button and ChevronLeftIcon and inside the smartBack
onClick) is a proper heading element (e.g., h1 or appropriate semantic level)
while keeping the existing className, truncation styling and layout so screen
readers can find the page heading.
src/apis/advertisement/queries.ts (1)

1-2: 새 API query 모듈은 alias import로 통일해주세요.

from '.'는 이 레이어의 import 규칙에서 바로 벗어납니다. @/apis/advertisement/... 형태로 맞춰두면 이동/분리 때 추적이 훨씬 쉽습니다.

As per coding guidelines "Use path alias @/* for imports instead of relative paths".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/apis/advertisement/queries.ts` around lines 1 - 2, The import of
getAdvertisements uses a relative alias ("from '.'"); update it to use the
project path alias instead (e.g. import { getAdvertisements } from
'@/apis/advertisement') so it follows the "Use path alias `@/`*" rule; locate the
import of getAdvertisements in the queries module and replace the source string
with the alias, keeping the named import intact and ensuring the module
resolution still builds.
src/components/notification/InAppNotificationToast.tsx (1)

45-60: 새 토스트에 raw 스타일 값이 너무 많습니다.

bg-[rgba(...)], text-[16px], text-[13px], text-[#5A6B7F]처럼 직접 값을 넣으면 이번 컴포넌트만 테마 토큰 체계에서 빠집니다. 배경/텍스트/CTA typography는 src/styles/theme.css의 semantic color와 text-body*/text-cap* 토큰으로 맞춰두는 편이 이후 수정이 훨씬 쉽습니다.

As per coding guidelines "Prioritize color tokens from src/styles/theme.css (indigo-, blue-, background, primary, etc.)" and "Use typography tokens (text-h1 through text-h5, text-sub1 through text-sub4, text-body1 through text-body3, text-cap1 through text-cap2) from src/styles/theme.css".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/notification/InAppNotificationToast.tsx` around lines 45 - 60,
The toast uses raw style values; update InAppNotificationToast to use theme
tokens instead: replace bg-[rgba(...)] with the appropriate semantic background
token (e.g., background or surface token), replace the message paragraph's
text-[16px] font class with a typography token like text-body1 or text-body2 and
remove raw leading/whitespace overrides, and replace the button's text-[13px]
and text-[`#5A6B7F`] with a caption/cta typography token (e.g.,
text-cap1/text-cap2) and a semantic color token (e.g., primary or text-muted)
while keeping existing classes like rounded and padding; adjust className on the
root element, the <p> that renders {message}, and the action <button> (identify
these by InAppNotificationToast, the message variable, and onAction) so the
component conforms to src/styles/theme.css tokens.
src/components/layout/Header/presentation.ts (1)

1-3: 상대 경로 대신 @/* 별칭으로 통일해주세요.

새 파일인데도 내부 import가 상대 경로로 들어와 있습니다. 이 파일도 @/components/layout/Header/... 형태로 맞춰두면 이후 이동이나 리팩터링 때 경로 수정 범위가 줄어듭니다.

As per coding guidelines "Use path alias @/* for imports instead of relative paths"

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/layout/Header/presentation.ts` around lines 1 - 3, Imports in
presentation.ts use relative paths; update them to the project path alias form
(e.g. "@/components/layout/Header/...") so they match the repo convention.
Replace the three imports (DEFAULT_HEADER_TYPE, HEADER_CONFIGS from
'./headerConfig'; ROUTE_TITLES from './routeTitles'; and HeaderType from
'./types') to use the `@/`* alias pointing to their respective files under
components/layout/Header, keeping the same imported symbols and types.
src/components/layout/BottomNav/index.tsx (1)

88-91: 하단바 border 색상은 토큰으로 맞춰주세요.

새 레이아웃 코드에 hex 값이 직접 들어와 있습니다. 테마 일관성을 위해 여기서도 border-text-100 같은 토큰을 쓰는 편이 안전합니다.

수정 예시
     <nav
       ref={navRef}
-      className="fixed right-0 bottom-0 left-0 z-20 rounded-t-[20px] border-t border-[`#e0e0e0`] bg-white"
+      className="fixed right-0 bottom-0 left-0 z-20 rounded-t-[20px] border-t border-text-100 bg-white"
     >
As per coding guidelines "Prioritize color tokens from `src/styles/theme.css` (indigo-*, blue-*, background, primary, etc.)"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/layout/BottomNav/index.tsx` around lines 88 - 91, Replace the
hard-coded hex border color in the BottomNav component's nav element (the JSX
with ref={navRef} and className="... border-t border-[`#e0e0e0`] ...") with the
theme token class (e.g., use border-text-100 or the appropriate token from
src/styles/theme.css) so the className no longer contains border-[`#e0e0e0`] but
the token class; update the className string in the nav JSX in
src/components/layout/BottomNav/index.tsx accordingly.
src/pages/Manager/ManagedRecruitmentWrite/index.tsx (2)

4-4: twMerge 대신 cn() 유틸리티 사용 권장

코딩 가이드라인에 따라 @/utils/ts/cn.tscn() 유틸리티를 사용해주세요. cn()clsxtailwind-merge를 결합한 유틸리티입니다.

-import { twMerge } from 'tailwind-merge';
+import { cn } from '@/utils/ts/cn';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/Manager/ManagedRecruitmentWrite/index.tsx` at line 4, Replace the
tailwind-merge import and usages with the project's combined utility: remove the
import of twMerge and any calls to twMerge(...) in ManagedRecruitmentWrite and
instead import cn from '@/utils/ts/cn' and call cn(...) where merging/classname
composition is needed; ensure any existing clsx-like arguments passed to twMerge
remain compatible with cn() and update all occurrences of the symbol twMerge to
cn to keep behavior consistent.

49-305: 페이지 로직을 hooks/ 디렉토리로 분리 고려

코딩 가이드라인에 따르면 페이지 로직은 페이지 전용 hooks/ 서브디렉토리로 분리하는 것이 권장됩니다. 현재 컴포넌트의 복잡도가 높으므로, 이미지 처리 로직이나 폼 상태 관리 등을 커스텀 훅으로 분리하면 가독성이 향상될 것입니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/Manager/ManagedRecruitmentWrite/index.tsx` around lines 49 - 305,
Extract the page logic from the ManagedRecruitmentWrite component into reusable
hooks under a hooks/ subdirectory: create at least one hook for image
preparation/management (pull out handleImageSelect, handlePrevImage,
handleNextImage, handleDeleteImage, handleImageClick, isPreparingImages, images,
currentImageIndex, setImages, setCurrentImageIndex and related
URL.revokeObjectURL handling) and another hook for form/recruitment state (pull
out
startDate/endDate/startTime/endTime/isAlwaysRecruiting/content/isRecruitmentEnabled,
applyExistingRecruitment, handleReset, handleContentChange, validation helpers
like getDateErrorMessage/hasDateError, and the submit flow in handleSubmit
including uploadImage, saveRecruitment and patchSettings). Keep
ManagedRecruitmentWrite as a thin component that uses useImageManagement() and
useRecruitmentForm(clubIdNumber, existingRecruitment, clubSettings, location,
navigate, showToast) hooks, moving side effects (useEffect blocks that
initialize from existingRecruitment and clubSettings) into the appropriate hooks
so the component only renders UI and delegates logic to these named hooks.
src/apis/auth/queries.ts (1)

8-8: signupPrefill 쿼리 키가 다른 키들과 일관성이 없습니다

다른 쿼리 키들은 authQueryKeys.all을 spread하는데, signupPrefill만 독립적인 배열을 사용합니다. 이로 인해 authQueryKeys.all 기반 invalidation 시 signupPrefill은 제외될 수 있습니다.

의도된 설계라면 무시해도 됩니다.

-  signupPrefill: () => ['signup', 'prefill'] as const,
+  signupPrefill: () => [...authQueryKeys.all, 'signupPrefill'] as const,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/apis/auth/queries.ts` at line 8, The signupPrefill query key is
inconsistent with others and should include authQueryKeys.all so it participates
in broad invalidation; update the signupPrefill factory (signupPrefill) to
return an array that spreads authQueryKeys.all and then 'prefill' (e.g.
[...authQueryKeys.all, 'prefill'] as const) so it matches the pattern used by
the other keys.
src/utils/ts/viewport.ts (1)

17-17: 매직 넘버 상수화 고려

80이 뷰포트 컨텍스트 변경 임계값으로 사용되는데, KEYBOARD_OPEN_THRESHOLD_PX처럼 명명된 상수로 추출하면 의도가 더 명확해집니다.

+const VIEWPORT_CONTEXT_CHANGE_THRESHOLD_PX = 80;
+
 // inside setViewportHeight:
-    const hasViewportContextChanged = Math.abs(restingViewportWidth - w) > 80;
+    const hasViewportContextChanged = Math.abs(restingViewportWidth - w) > VIEWPORT_CONTEXT_CHANGE_THRESHOLD_PX;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/utils/ts/viewport.ts` at line 17, Extract the magic number 80 into a
named constant (e.g., KEYBOARD_OPEN_THRESHOLD_PX or
VIEWPORT_CONTEXT_CHANGE_THRESHOLD_PX) and replace the literal in the
hasViewportContextChanged calculation (Math.abs(restingViewportWidth - w) > 80)
with that constant; declare the constant near related viewport utilities so
intent is clear and update any nearby comments or tests that rely on this
threshold.
src/components/layout/hooks/useLayoutElements.ts (1)

2-2: 상대 경로 대신 @/ 경로 alias 사용 권장

코딩 가이드라인에 따라 @/* 경로 alias를 사용해주세요.

-import { useLayoutBottomOverlayInset } from './useLayoutBottomOverlayInset';
+import { useLayoutBottomOverlayInset } from '@/components/layout/hooks/useLayoutBottomOverlayInset';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/layout/hooks/useLayoutElements.ts` at line 2, 현재 파일에서
useLayoutBottomOverlayInset을 상대경로로 import하고 있으니 코딩 가이드라인에 맞춰 경로 alias로 변경하세요: 교체
대상은 import { useLayoutBottomOverlayInset } from
'./useLayoutBottomOverlayInset';이며 이를 프로젝트의 `@/`* alias 규칙에 맞는 경로(예:
'@/components/layout/hooks/useLayoutBottomOverlayInset')로 바꿔 사용하도록 수정하세요.
src/pages/Chat/hooks/useChat.ts (1)

4-8: import 경로에서 .ts 확장자를 제거하세요.

TypeScript/ESM 컨벤션상 import 경로에 확장자를 명시하지 않습니다.

♻️ 수정 제안
 import {
   useCreateChatRoomMutation,
   useSendChatMessageMutation,
   useToggleChatMuteMutation,
-} from '@/pages/Chat/hooks/useChatMutations.ts';
+} from '@/pages/Chat/hooks/useChatMutations';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/Chat/hooks/useChat.ts` around lines 4 - 8, The import statement in
useChat.ts incorrectly includes the .ts extension; update the import to remove
the extension so it reads from '@/pages/Chat/hooks/useChatMutations' (without
'.ts'), ensuring the three identifiers useCreateChatRoomMutation,
useSendChatMessageMutation, and useToggleChatMuteMutation are imported from that
path.
src/pages/Notifications/index.tsx (1)

71-74: 타이포그래피 토큰 사용을 검토하세요.

text-[16px], text-[14px] 등 하드코딩된 폰트 크기 대신 text-body1, text-body2 등 시맨틱 타이포그래피 토큰을 우선 검토해 주세요.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/Notifications/index.tsx` around lines 71 - 74, Replace the
hardcoded font-size utility classes on the notification text elements with
semantic typography tokens: change the p element rendering notification.title
(currently using "text-[16px] ... font-semibold") to use the semantic token
(e.g., "text-body1 font-semibold ...") and change the p that renders
getInboxNotificationMessage(notification) (currently "text-[14px] ...
font-medium") to use "text-body2 font-medium ..."; ensure any additional utility
classes (truncate, leading, color) remain, and confirm the design system tokens
text-body1/text-body2 exist or map to the intended sizes in the global CSS/
tailwind config.
src/utils/hooks/useAdvertisementInterval.ts (2)

97-97: 의존성 배열에 RefObject 포함 여부 검토.

firstItemRef, secondItemRef, mainRef, bottomNavRef는 RefObject로 참조가 안정적입니다. React에서 ref 객체 자체는 변경되지 않으므로 의존성 배열에서 제외해도 됩니다. 단, ESLint exhaustive-deps 규칙 때문에 포함했다면 현재 구현도 괜찮습니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/utils/hooks/useAdvertisementInterval.ts` at line 97, 요약:
useAdvertisementInterval 훅의 useEffect 의존성 배열에 불필요한 RefObject들(firstItemRef,
secondItemRef, mainRef, bottomNavRef)이 포함되어 있습니다; 수정하려면 useAdvertisementInterval
내부의 해당 useEffect에서 ref 객체(self)들을 의존성 배열에서 제거하고 대신 실제 변화에 따라 반응해야 하는 값들(예:
enabled, itemCount)만 남기세요; 만약 ESLint의 exhaustive-deps 경고가 발생하면 ref.current 같은 실제
참조 값을 명시하거나 규칙 비활성화 주석을 사용해(`// eslint-disable-next-line
react-hooks/exhaustive-deps`) 처리하면 됩니다.

50-53: 계산식 가독성 개선을 검토하세요.

Math.max(1, Math.max(2, Math.floor(...)) - 1) 에서 외부 Math.max(1, ...) 는 내부 Math.max(2, ...) - 1이 최소 1을 보장하므로 중복입니다.

♻️ 수정 제안
     const nextItemsPerAdvertisement =
       availableHeight <= 0 || slotHeight <= 0
         ? DEFAULT_ITEMS_PER_ADVERTISEMENT
-        : Math.max(1, Math.max(2, Math.floor((availableHeight + gap) / slotHeight)) - 1);
+        : Math.max(2, Math.floor((availableHeight + gap) / slotHeight)) - 1;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/utils/hooks/useAdvertisementInterval.ts` around lines 50 - 53, The
computation for nextItemsPerAdvertisement is using a redundant outer Math.max;
replace the ternary's right-hand expression with Math.max(2,
Math.floor((availableHeight + gap) / slotHeight)) - 1 so the inner Math.max(2,
...) already guarantees the minimum before subtracting 1. Update the assignment
of nextItemsPerAdvertisement in useAdvertisementInterval.ts to use that
simplified expression and remove the outer Math.max(1, ...).
src/pages/Club/ClubList/components/AdvertisementCard.tsx (1)

25-31: 하드코딩된 색상 값 대신 디자인 토큰 사용을 검토하세요.

#1E2C3F, #EAF6FB, #2F6E83 등 하드코딩된 색상이 있습니다. 가능하다면 src/styles/theme.css의 색상 토큰 사용을 권장합니다. 광고 카드가 의도적으로 다른 스타일이라면 현재 구현도 괜찮습니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/Club/ClubList/components/AdvertisementCard.tsx` around lines 25 -
31, AdvertisementCard contains hardcoded hex colors for the title, badge
background and icon (see the JSX that renders advertisement.title,
advertisement.description and the badge with CircleWarningIcon); replace those
literals with your design tokens from theme (use the CSS variables or
token-based utility classes instead of text-[`#1E2C3F`], bg-[`#EAF6FB`],
text-[`#2F6E83`] and the inline style={{ color: '#2F6E83' }} on
CircleWarningIcon), by switching the className/value to the corresponding token
names (or var(--token-name)) and ensuring the tokens are available where the
component is used so the visual remains identical but driven by theme variables.
src/utils/ts/imagePreprocessor.ts (1)

137-146: Worker 응답 timeout 부재로 Promise가 무한 대기할 수 있습니다.

Worker가 응답하지 않으면 Promise가 영원히 resolve/reject되지 않아 UI가 멈출 수 있습니다. timeout 추가를 권장합니다.

♻️ timeout 추가 제안
+const WORKER_TIMEOUT_MS = 30000;
+
 async function runWorkerPreprocess(file: File, options: PrepareImageFileOptions) {
   // ... existing code ...
 
   return new Promise<ImagePreprocessResponse | null>((resolve, reject) => {
+    const timeoutId = setTimeout(() => {
+      pendingImagePreprocessRequests.delete(id);
+      resolve(null); // fallback to main thread
+    }, WORKER_TIMEOUT_MS);
+
     try {
-      pendingImagePreprocessRequests.set(id, { reject, resolve });
+      pendingImagePreprocessRequests.set(id, {
+        reject: (error) => {
+          clearTimeout(timeoutId);
+          reject(error);
+        },
+        resolve: (value) => {
+          clearTimeout(timeoutId);
+          resolve(value);
+        },
+      });
       worker.postMessage(requestPayload);
     } catch {
+      clearTimeout(timeoutId);
       pendingImagePreprocessRequests.delete(id);
       resetImagePreprocessWorker();
       resolve(null);
     }
   });
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/utils/ts/imagePreprocessor.ts` around lines 137 - 146, The Promise
created for image preprocessing can hang indefinitely if the worker never
responds; add a timeout (e.g., setTimeout) when creating the Promise in this
block so that after a configurable delay you clean up
pendingImagePreprocessRequests for the given id, call
resetImagePreprocessWorker(), and resolve(null) to avoid UI hang. Store the
timeout handle alongside the { resolve, reject } entry
(pendingImagePreprocessRequests.set(id, { resolve, reject, timeoutHandle })) and
ensure any existing resolution/rejection path that handles worker responses
clears that timeout so you don't leak timers; reference
pendingImagePreprocessRequests, resetImagePreprocessWorker, worker.postMessage,
id, requestPayload, and ImagePreprocessResponse when implementing this change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 288f3f65-652e-4f26-a4a7-069b87b1e4b2

📥 Commits

Reviewing files that changed from the base of the PR and between b846b34 and b75ced4.

⛔ Files ignored due to path filters (17)
  • public/apple-touch-icon-180x180.png is excluded by !**/*.png, !public/** and included by **
  • public/maskable-icon-512x512.png is excluded by !**/*.png, !public/** and included by **
  • public/pwa-192x192.png is excluded by !**/*.png, !public/** and included by **
  • public/pwa-512x512.png is excluded by !**/*.png, !public/** and included by **
  • public/pwa-64x64.png is excluded by !**/*.png, !public/** and included by **
  • src/assets/image/bottom-nav-home.png is excluded by !**/*.png, !src/assets/** and included by **
  • src/assets/image/chat-cat-header.png is excluded by !**/*.png, !src/assets/** and included by **
  • src/assets/image/notification-toast-approved.png is excluded by !**/*.png, !src/assets/** and included by **
  • src/assets/image/notification-toast-general.png is excluded by !**/*.png, !src/assets/** and included by **
  • src/assets/svg/bottom-nav-chat.svg is excluded by !**/*.svg, !src/assets/** and included by **
  • src/assets/svg/bottom-nav-clubs.svg is excluded by !**/*.svg, !src/assets/** and included by **
  • src/assets/svg/bottom-nav-mypage.svg is excluded by !**/*.svg, !src/assets/** and included by **
  • src/assets/svg/bottom-nav-timer.svg is excluded by !**/*.svg, !src/assets/** and included by **
  • src/assets/svg/chat-icon.svg is excluded by !**/*.svg, !src/assets/** and included by **
  • src/assets/svg/notifications.svg is excluded by !**/*.svg, !src/assets/** and included by **
  • src/assets/svg/person-icon.svg is excluded by !**/*.svg, !src/assets/** and included by **
  • src/assets/svg/unread-notification.svg is excluded by !**/*.svg, !src/assets/** and included by **
📒 Files selected for processing (141)
  • src/App.tsx
  • src/apis/advertisement/entity.ts
  • src/apis/advertisement/index.ts
  • src/apis/advertisement/mutations.ts
  • src/apis/advertisement/queries.ts
  • src/apis/auth/index.ts
  • src/apis/auth/mutations.ts
  • src/apis/auth/queries.ts
  • src/apis/chat/mutations.ts
  • src/apis/chat/queries.ts
  • src/apis/club/managedMutations.ts
  • src/apis/club/managedQueries.ts
  • src/apis/club/mutations.ts
  • src/apis/club/queries.ts
  • src/apis/council/queries.ts
  • src/apis/inquiry/mutations.ts
  • src/apis/notification/cache.ts
  • src/apis/notification/entity.ts
  • src/apis/notification/index.ts
  • src/apis/notification/mutations.ts
  • src/apis/notification/queries.ts
  • src/apis/schedule/queries.ts
  • src/apis/studyTime/hooks.ts
  • src/apis/studyTime/mutations.ts
  • src/apis/studyTime/queries.ts
  • src/apis/university/queries.ts
  • src/apis/upload/mutations.ts
  • src/components/common/Portal.tsx
  • src/components/layout/BottomNav/index.tsx
  • src/components/layout/Header/components/InfoHeader.tsx
  • src/components/layout/Header/components/ManagerHeader.tsx
  • src/components/layout/Header/components/NotificationBell.tsx
  • src/components/layout/Header/components/SubpageHeader.tsx
  • src/components/layout/Header/constants.ts
  • src/components/layout/Header/headerConfig.ts
  • src/components/layout/Header/index.tsx
  • src/components/layout/Header/presentation.ts
  • src/components/layout/Header/routeTitles.ts
  • src/components/layout/Header/types.ts
  • src/components/layout/hooks/useLayoutBottomOverlayInset.ts
  • src/components/layout/hooks/useLayoutElements.ts
  • src/components/layout/index.tsx
  • src/components/layout/layoutMetrics.ts
  • src/components/notification/InAppNotificationToast.tsx
  • src/components/notification/InboxNotificationLayer.tsx
  • src/components/notification/hooks/useInboxNotificationMutations.ts
  • src/components/notification/hooks/useInboxNotificationStream.ts
  • src/contexts/useLayoutElementsContext.tsx
  • src/index.css
  • src/pages/Auth/SignUp/ConfirmStep.tsx
  • src/pages/Auth/SignUp/FinishStep.tsx
  • src/pages/Auth/SignUp/StudentIdStep.tsx
  • src/pages/Auth/SignUp/UniversityStep.tsx
  • src/pages/Auth/SignUp/hooks/useInquiry.ts
  • src/pages/Auth/SignUp/hooks/useSignup.ts
  • src/pages/Auth/SignUp/hooks/useSignupPrefill.ts
  • src/pages/Auth/SignUp/hooks/useUniversity.ts
  • src/pages/Chat/ChatRoom.tsx
  • src/pages/Chat/hooks/useChat.ts
  • src/pages/Chat/hooks/useChatMutations.ts
  • src/pages/Chat/hooks/useUnreadChatCount.ts
  • src/pages/Chat/index.tsx
  • src/pages/Club/Application/applyCompletePage.tsx
  • src/pages/Club/Application/clubFeePage.tsx
  • src/pages/Club/Application/hooks/useApplyToClub.ts
  • src/pages/Club/Application/hooks/useClubApply.ts
  • src/pages/Club/Application/hooks/useGetClubFee.ts
  • src/pages/Club/ClubDetail/components/ClubAccount.tsx
  • src/pages/Club/ClubDetail/components/ClubMember.tsx
  • src/pages/Club/ClubDetail/components/ClubRecruitment.tsx
  • src/pages/Club/ClubDetail/hooks/useCouncilNotices.ts
  • src/pages/Club/ClubDetail/hooks/useGetClubDetail.ts
  • src/pages/Club/ClubDetail/hooks/useGetClubMembers.ts
  • src/pages/Club/ClubDetail/hooks/useGetClubRecruitment.ts
  • src/pages/Club/ClubDetail/index.tsx
  • src/pages/Club/ClubList/components/AdvertisementCard.tsx
  • src/pages/Club/ClubList/components/ClubCard.tsx
  • src/pages/Club/ClubList/hooks/useGetClubs.ts
  • src/pages/Club/ClubList/index.tsx
  • src/pages/Club/ClubSearch/index.tsx
  • src/pages/Council/CouncilDetail/components/CouncilNotice.tsx
  • src/pages/Council/CouncilDetail/hooks/useGetCouncilInfo.ts
  • src/pages/Council/CouncilDetail/index.tsx
  • src/pages/Council/CouncilNotice/hooks/useCouncilNoticeDetail.ts
  • src/pages/Home/components/CouncilNoticeSection.tsx
  • src/pages/Home/components/HomeClubSection.tsx
  • src/pages/Home/components/MiniSchedulePreview.tsx
  • src/pages/Home/hooks/useGetHomeClubs.ts
  • src/pages/Home/hooks/useGetHomeCouncilNotices.ts
  • src/pages/Home/hooks/useGetNotificationToken.ts
  • src/pages/Home/hooks/useGetScheduleList.ts
  • src/pages/Home/hooks/useGetUpComingSchedule.ts
  • src/pages/Manager/ManagedAccount/index.tsx
  • src/pages/Manager/ManagedApplicationDetail/index.tsx
  • src/pages/Manager/ManagedApplicationList/index.tsx
  • src/pages/Manager/ManagedClubList/index.tsx
  • src/pages/Manager/ManagedClubProfile/index.tsx
  • src/pages/Manager/ManagedMemberApplicationDetail/index.tsx
  • src/pages/Manager/ManagedMemberList/index.tsx
  • src/pages/Manager/ManagedRecruitment/index.tsx
  • src/pages/Manager/ManagedRecruitmentForm/index.tsx
  • src/pages/Manager/ManagedRecruitmentWrite/index.tsx
  • src/pages/Manager/hooks/useManagedApplicationMutations.ts
  • src/pages/Manager/hooks/useManagedApplications.ts
  • src/pages/Manager/hooks/useManagedClubMutations.ts
  • src/pages/Manager/hooks/useManagedClubs.ts
  • src/pages/Manager/hooks/useManagedFee.ts
  • src/pages/Manager/hooks/useManagedMemberApplications.ts
  • src/pages/Manager/hooks/useManagedMemberMutations.ts
  • src/pages/Manager/hooks/useManagedMembers.ts
  • src/pages/Manager/hooks/useManagedRecruitment.ts
  • src/pages/Manager/hooks/useManagedSettings.ts
  • src/pages/Notifications/index.tsx
  • src/pages/Schedule/components/ScheduleDetail.tsx
  • src/pages/Schedule/hooks/useGetSchedules.ts
  • src/pages/Schedule/index.tsx
  • src/pages/Timer/hooks/useStudyTime.ts
  • src/pages/Timer/hooks/useStudyTimeRanking.ts
  • src/pages/Timer/hooks/useStudyTimer.ts
  • src/pages/User/MyPage/components/UserInfoCard.tsx
  • src/pages/User/MyPage/hooks/useLogout.ts
  • src/pages/User/MyPage/hooks/useWithdraw.ts
  • src/pages/User/MyPage/index.tsx
  • src/pages/User/Profile/hooks/useMyInfo.ts
  • src/pages/User/Profile/hooks/useOAuthLinks.ts
  • src/pages/User/Profile/index.tsx
  • src/pages/User/hooks/useAdminChatMutation.ts
  • src/stores/authStore.ts
  • src/utils/hooks/useAdvertisementInterval.ts
  • src/utils/hooks/useAdvertisements.ts
  • src/utils/hooks/useKeyboardHeight.ts
  • src/utils/hooks/useUploadImage.ts
  • src/utils/hooks/useViewportHeight.ts
  • src/utils/hooks/useViewportHeightLock.ts
  • src/utils/ts/accessToken.ts
  • src/utils/ts/dom.ts
  • src/utils/ts/imagePreprocessor.ts
  • src/utils/ts/imagePreprocessor.worker.ts
  • src/utils/ts/notification.ts
  • src/utils/ts/promise.ts
  • src/utils/ts/viewport.ts
💤 Files with no reviewable changes (25)
  • src/pages/Auth/SignUp/hooks/useSignup.ts
  • src/pages/Club/ClubDetail/hooks/useGetClubMembers.ts
  • src/pages/Auth/SignUp/hooks/useInquiry.ts
  • src/pages/Auth/SignUp/hooks/useSignupPrefill.ts
  • src/pages/Auth/SignUp/hooks/useUniversity.ts
  • src/pages/Club/Application/hooks/useGetClubFee.ts
  • src/pages/Club/ClubDetail/hooks/useCouncilNotices.ts
  • src/pages/Club/ClubDetail/hooks/useGetClubDetail.ts
  • src/pages/Club/ClubList/hooks/useGetClubs.ts
  • src/pages/Club/ClubDetail/hooks/useGetClubRecruitment.ts
  • src/pages/Council/CouncilDetail/hooks/useGetCouncilInfo.ts
  • src/pages/Home/hooks/useGetUpComingSchedule.ts
  • src/pages/Home/hooks/useGetScheduleList.ts
  • src/pages/Home/hooks/useGetHomeCouncilNotices.ts
  • src/pages/Home/hooks/useGetNotificationToken.ts
  • src/pages/Schedule/hooks/useGetSchedules.ts
  • src/pages/Manager/hooks/useManagedSettings.ts
  • src/pages/Manager/hooks/useManagedMemberApplications.ts
  • src/pages/Manager/hooks/useManagedRecruitment.ts
  • src/pages/Manager/hooks/useManagedClubs.ts
  • src/utils/hooks/useKeyboardHeight.ts
  • src/pages/User/Profile/hooks/useMyInfo.ts
  • src/utils/hooks/useViewportHeight.ts
  • src/pages/Manager/hooks/useManagedFee.ts
  • src/pages/Manager/hooks/useManagedMembers.ts

Comment on lines 12 to +13
const { clubId } = useParams<{ clubId: string }>();
const { data: settings } = useGetClubSettings(Number(clubId));
const { data: settings } = useQuery(managedClubQueries.settings(Number(clubId)));
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

clubId가 undefined일 경우 NaN 전달 가능성

useParams에서 clubId가 undefined일 수 있습니다. Number(undefined)NaN을 반환하므로 쿼리에 잘못된 값이 전달될 수 있습니다.

🛡️ 제안
 function ManagedRecruitment() {
   const { clubId } = useParams<{ clubId: string }>();
+  if (!clubId) return null; // 또는 적절한 에러 처리
   const { data: settings } = useQuery(managedClubQueries.settings(Number(clubId)));
📝 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 { clubId } = useParams<{ clubId: string }>();
const { data: settings } = useGetClubSettings(Number(clubId));
const { data: settings } = useQuery(managedClubQueries.settings(Number(clubId)));
const { clubId } = useParams<{ clubId: string }>();
if (!clubId) return null; // 또는 적절한 에러 처리
const { data: settings } = useQuery(managedClubQueries.settings(Number(clubId)));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/Manager/ManagedRecruitment/index.tsx` around lines 12 - 13, The
code calls Number(clubId) directly which can pass NaN when clubId is undefined;
update to parse and guard before querying: read clubId from useParams, compute
const parsedClubId = clubId ? Number(clubId) : undefined (or use parseInt where
appropriate), then call useQuery(managedClubQueries.settings(parsedClubId as
number), { enabled: !!parsedClubId }) so managedClubQueries.settings and
useQuery are only executed when a valid numeric clubId exists; reference the
symbols useParams, clubId, Number(clubId), managedClubQueries.settings, and
useQuery when making this change.

Copy link
Copy Markdown

@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: 1

🧹 Nitpick comments (2)
src/pages/Chat/hooks/useChatRoomScroll.ts (2)

117-130: ResizeObserver 조건부 생성 시 타입 명시 권장

resizeObserver 변수의 타입이 ResizeObserver | undefined로 추론되지만, 명시적 타입을 추가하면 가독성이 향상됩니다.

♻️ 타입 명시 예시
-    const resizeObserver =
-      typeof ResizeObserver !== 'undefined'
-        ? new ResizeObserver(() => {
+    const resizeObserver: ResizeObserver | undefined =
+      typeof ResizeObserver !== 'undefined'
+        ? new ResizeObserver(() => {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/Chat/hooks/useChatRoomScroll.ts` around lines 117 - 130, The
ResizeObserver created in useChatRoomScroll is currently relying on type
inference; explicitly annotate the resizeObserver variable as ResizeObserver |
undefined to improve readability and intent. Locate the declaration named
resizeObserver in the useChatRoomScroll hook and change its declaration to
include the explicit type annotation (ResizeObserver | undefined), keeping the
conditional creation logic intact so the runtime behavior does not change.

111-137: ResizeObserver 구현 양호, 중복 scroll 호출 가능성 확인 필요

SSR 환경 체크(typeof ResizeObserver !== 'undefined)와 requestAnimationFrame 사용이 적절합니다.

다만, ChatRoom.tsxhandleSubmit에서 메시지 전송 후 scrollToBottom()을 직접 호출하고, 동시에 textarea 축소로 인해 ResizeObserver가 트리거되어 scrollToBottom()이 중복 호출될 수 있습니다. 기능상 idempotent하여 문제는 없지만, 불필요한 연산입니다.

♻️ (선택적) 중복 호출 방지를 위한 debounce 또는 flag 패턴

간단한 방법으로 isScrollingRef를 추가하여 중복 호출을 방지할 수 있습니다:

+ const isScrollingRef = useRef(false);

  const scrollToBottom = useCallback(() => {
    const container = scrollContainerRef.current;
    if (!container) return;
+   if (isScrollingRef.current) return;
+   isScrollingRef.current = true;

    container.scrollTop = container.scrollHeight;
    isNearBottomRef.current = true;
+   
+   requestAnimationFrame(() => {
+     isScrollingRef.current = false;
+   });
  }, []);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/Chat/hooks/useChatRoomScroll.ts` around lines 111 - 137, The
ResizeObserver callback and ChatRoom.tsx's handleSubmit can both call
scrollToBottom causing duplicate calls; add a guard (e.g., isScrollingRef)
checked and set before invoking scrollToBottom so subsequent callers skip while
a scroll is in progress, and clear the flag after the scroll completes (use
requestAnimationFrame or a small timeout). Update the ResizeObserver logic in
useLayoutEffect (where previousClientHeightRef, isNearBottomRef,
chatMessagesLengthRef, scrollToBottom, chatRoomId are referenced) to check
isScrollingRef before scheduling requestAnimationFrame(scrollToBottom), and
update handleSubmit to set/check the same isScrollingRef around its direct call
to scrollToBottom; alternatively implement a lightweight debounce around
scrollToBottom if preferred.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/pages/Auth/SignUp/components/AgreementArrow.tsx`:
- Around line 33-35: The Link in AgreementArrow.tsx renders only a RightIcon so
screen readers lack context; update the <Link to={route} state={state}> (the
Link wrapping RightIcon/RightIcon component) to include an accessible name by
adding an appropriate aria-label (or aria-labelledby) that describes the link
target (e.g., "View agreement" or similar) and ensure the label is localized if
needed; keep the visual content as the icon and do not change styling or
navigation logic.

---

Nitpick comments:
In `@src/pages/Chat/hooks/useChatRoomScroll.ts`:
- Around line 117-130: The ResizeObserver created in useChatRoomScroll is
currently relying on type inference; explicitly annotate the resizeObserver
variable as ResizeObserver | undefined to improve readability and intent. Locate
the declaration named resizeObserver in the useChatRoomScroll hook and change
its declaration to include the explicit type annotation (ResizeObserver |
undefined), keeping the conditional creation logic intact so the runtime
behavior does not change.
- Around line 111-137: The ResizeObserver callback and ChatRoom.tsx's
handleSubmit can both call scrollToBottom causing duplicate calls; add a guard
(e.g., isScrollingRef) checked and set before invoking scrollToBottom so
subsequent callers skip while a scroll is in progress, and clear the flag after
the scroll completes (use requestAnimationFrame or a small timeout). Update the
ResizeObserver logic in useLayoutEffect (where previousClientHeightRef,
isNearBottomRef, chatMessagesLengthRef, scrollToBottom, chatRoomId are
referenced) to check isScrollingRef before scheduling
requestAnimationFrame(scrollToBottom), and update handleSubmit to set/check the
same isScrollingRef around its direct call to scrollToBottom; alternatively
implement a lightweight debounce around scrollToBottom if preferred.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 9ffda348-a183-4dd1-8040-22a401fcd4a7

📥 Commits

Reviewing files that changed from the base of the PR and between b75ced4 and 488168b.

📒 Files selected for processing (5)
  • src/pages/Auth/SignUp/TermStep.tsx
  • src/pages/Auth/SignUp/components/AgreementArrow.tsx
  • src/pages/Chat/hooks/useChatRoomScroll.ts
  • src/pages/User/MyPage/index.tsx
  • src/utils/hooks/useSmartBack.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/pages/User/MyPage/index.tsx

Copy link
Copy Markdown

@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: 8

🧹 Nitpick comments (4)
src/apis/notification/index.ts (1)

41-46: 반환 타입 명시 필요

다른 함수들과 달리 markInboxNotificationAsRead는 반환 타입이 명시되어 있지 않아 Promise<unknown>으로 추론됩니다. 코딩 가이드라인에 따라 응답 타입을 명시해주세요.

♻️ 타입 명시 제안
-export const markInboxNotificationAsRead = async (notificationId: number) => {
-  const response = await apiClient.patch(`notifications/inbox/${notificationId}/read`, {
+export const markInboxNotificationAsRead = async (notificationId: number): Promise<void> => {
+  await apiClient.patch<void>(`notifications/inbox/${notificationId}/read`, {
     requiresAuth: true,
   });
-  return response;
 };

또는 서버에서 응답 데이터가 있다면 entity.ts에 타입을 정의하고 사용해주세요.

As per coding guidelines: "요청/응답 타입이 명시되어 있는지" 및 "Define API response types in the domain-specific entity.ts file"

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/apis/notification/index.ts` around lines 41 - 46,
markInboxNotificationAsRead currently lacks an explicit return type so
TypeScript infers Promise<unknown>; update it to a concrete response type by
defining/using a domain response in entity.ts (e.g., NotificationReadResponse or
ApiResponse<void> if no payload) and annotate the function as returning
Promise<ThatType>, then pass that type as the generic to apiClient.patch and
import the type into src/apis/notification/index.ts so
markInboxNotificationAsRead and its apiClient.patch call are strongly typed.
src/utils/ts/imagePreprocessor.ts (1)

161-173: Timeout 핸들러에서 다른 pending request까지 null로 settle하는 로직 검토 필요

한 요청이 timeout되면 worker를 종료하고 모든 다른 pending request도 null로 resolve합니다. 의도된 동작일 수 있으나, worker가 단순히 느린 경우(예: 대용량 이미지)에는 다른 요청들까지 불필요하게 실패할 수 있습니다.

worker 종료 후 다른 요청들은 어차피 응답을 받지 못하므로 현재 로직이 맞지만, timeout된 요청만 실패시키고 worker를 유지하는 방식도 고려해볼 수 있습니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/utils/ts/imagePreprocessor.ts` around lines 161 - 173, The timeout
handler currently takes the timed-out request via
takePendingImagePreprocessRequest(id), calls resetImagePreprocessWorker(),
resolves that request with null, and then calls
settlePendingImagePreprocessRequests to resolve ALL other pending requests to
null; change this so the handler only fails the timed-out request: remove or
avoid calling settlePendingImagePreprocessRequests and do not tear down the
worker unless you explicitly want to terminate it; keep only the logic that
retrieves currentRequest, resolve it with null, and optionally decide separately
whether to call resetImagePreprocessWorker() (if you choose to terminate the
worker) so other pending requests remain pending and can still complete.
src/utils/hooks/useAdvertisementInterval.ts (1)

60-97: cleanup 및 이벤트 리스너 관리 잘 되어있습니다.

  • cancelAnimationFrame(frameId) 호출로 pending frame 정리
  • ResizeObserver.disconnect() 호출
  • 모든 event listener 제거
  • ResizeObserver feature detection도 적절합니다.

다만, itemCount가 dependency array에 포함되어 있지만 effect 내부에서 직접 사용되지 않습니다. 의도적으로 아이템 수 변경 시 재측정을 트리거하는 것으로 보이는데, 간단한 주석이 있으면 의도가 명확해질 것 같습니다.

💡 의도를 명확히 하는 주석 추가
-  }, [bottomNavRef, enabled, firstItemRef, itemCount, mainRef, secondItemRef]);
+  // itemCount: 아이템 수 변경 시 재측정 트리거 목적으로 포함
+  }, [bottomNavRef, enabled, firstItemRef, itemCount, mainRef, secondItemRef]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/utils/hooks/useAdvertisementInterval.ts` around lines 60 - 97, The
useLayoutEffect in useAdvertisementInterval includes itemCount in its dependency
array but does not reference it inside the effect; add a short explanatory
comment next to the effect or near the dependency array (or inside the effect)
stating that itemCount is included intentionally to force re-measurement when
the number of items changes (so the measurement logic like
measureAdvertisementInterval and observedElements updates run), referencing
useAdvertisementInterval, measureAdvertisementInterval, and the itemCount
dependency so future readers understand this is deliberate and not an accidental
leftover.
src/pages/Auth/SignUp/FinishStep.tsx (1)

8-14: Step 페이지 비즈니스 로직을 전용 훅으로 분리해 주세요.

useSuspenseQuery 조회 + setUser + navigate가 페이지 컴포넌트에 직접 있어, Step 페이지 패턴 대비 응집도가 낮습니다. src/pages/Auth/SignUp/hooks/useFinishStep.ts로 분리하는 편이 좋습니다.

♻️ 분리 예시
-  const navigate = useNavigate();
-  const { data: myInfo } = useSuspenseQuery(authQueries.myInfo());
-  const setUser = useAuthStore((state) => state.setUser);
-
-  const handleStart = () => {
-    setUser(myInfo);
-    navigate('/guide');
-  };
+  const { myInfo, handleStart } = useFinishStep();

As per coding guidelines: src/pages/**/*.{ts,tsx}: Separate page-specific logic into page-dedicated hooks/ directories.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/Auth/SignUp/FinishStep.tsx` around lines 8 - 14, Extract the page
logic into a new hook src/pages/Auth/SignUp/hooks/useFinishStep.ts: move the
useSuspenseQuery(authQueries.myInfo()), the useAuthStore((s)=>s.setUser) call
and useNavigate() into that hook, have it expose the fetched myInfo (or null)
and a start/handleStart function that calls setUser(myInfo) and
navigate('/guide'); then update FinishStep.tsx to import and use useFinishStep
(replacing inline useSuspenseQuery, setUser, navigate and handleStart) so the
page component only handles UI rendering.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/apis/studyTime/queries.ts`:
- Around line 21-22: The myRanking query key currently includes params.type
which causes refetches when the UI tab (type) changes even though queryFn only
uses params.sort; remove params.type from the key so it becomes
[...studyTimeQueryKeys.all, 'myRanking', params.sort] as const in the myRanking
function (keep MyStudyTimeRankingKeyParams unchanged), and ensure the query's
queryFn still reads params.sort and the select function continues to receive
params.type for client-side filtering (no change to select usage).

In `@src/pages/Manager/ManagedClubProfile/index.tsx`:
- Around line 120-125: The catch block currently wipes the selected image state;
instead, modify the catch handler in the image preprocessing flow so it only
resets the file input value and shows the toast, but preserves the current
preview/file state: remove or avoid calling
clearLocalPreviewUrl(localPreviewUrlRef), setImageFile(null) and
setImagePreview(''), keep e.target.value = '' and showToast('이미지 처리에 실패했습니다') so
the existing server image or previously chosen local preview remains visible.

In `@src/pages/Manager/ManagedMemberList/index.tsx`:
- Around line 232-236: The current useChangeManagedMemberPositionMutation call
(mutateAsync: changeMemberPosition with invalidateOnSuccess: false) disables
per-request invalidation and only relies on a single success-path invalidate,
which leaves the member list stale on partial failures; update the mutation
usage so that cache invalidation for the member list runs in the
onSettled/finally path (or call the single final invalidate unconditionally
after all position-change requests complete), ensuring changeMemberPosition
requests always trigger the final invalidate regardless of individual request
success or failure (apply the same fix to the other occurrence around lines
428-434).

In `@src/pages/Manager/ManagedRecruitmentForm/index.tsx`:
- Around line 37-38: The success UX for the "enableAfterSave" path is being
shown immediately after updateQuestions succeeds, which can mislead if
patchSettings (or the enableAfterSave update) fails; change the flow so that
after calling updateQuestions (useUpdateManagedClubQuestionsMutation ->
updateQuestions) you do NOT show the success toast or trigger navigation until
patchSettings (usePatchManagedClubSettingsMutation -> patchSettings) finishes
successfully; instead, chain the calls or await patchSettings inside the
updateQuestions onSuccess handler (or handle both promises) and only call the
toast/navigation when patchSettings resolves successfully, while handling and
reporting errors from patchSettings (including the enableAfterSave case)
appropriately.

In `@src/pages/Manager/ManagedRecruitmentWrite/index.tsx`:
- Around line 73-74: 현재 saveRecruitment 성공 직후 바로 성공 토스트 및 이동을 하고 있어 이후 호출되는
patchSettings 실패 시에도 사용자에게 성공으로 보입니다; 변경할 곳은 saveRecruitment와 patchSettings 호출
흐름(관련 훅: useUpsertManagedClubRecruitmentMutation,
usePatchManagedClubSettingsMutation, 함수/변수: saveRecruitment, patchSettings,
isPending, isSettingsPending). 수정 방법: saveRecruitment가 성공하면 즉시 토스트/이동하지 말고
patchSettings를 호출하고 그 결과를 기다려서 둘 다 성공했을 때만 성공 토스트와 네비게이션을 실행하도록 체이닝하거나 Promise
기반으로 병합 처리하고, patchSettings 실패 시 해당 에러를 적절히 노출(토스트/폼 에러)하도록 처리하세요; 또한
isPending/isSettingsPending 상태를 이용해 로딩 UI를 적절히 유지하세요.

In `@src/utils/ts/notification.ts`:
- Around line 80-88: normalizeInboxNotificationPath currently allows
protocol-relative or external URLs like "//evil.com" or "http://..." which can
cause open-redirects; update the function to only accept internal paths by
rejecting any input that (a) is empty after trim, (b) starts with "//", (c)
contains "://" anywhere, or (d) matches a URI scheme pattern (e.g.,
/^[a-zA-Z][\w+.-]*:/). If any of those checks match, return null; otherwise
ensure the returned value starts with a single leading slash (prepend "/" if
missing). Reference the function normalizeInboxNotificationPath and apply these
validation checks before returning the normalized path.
- Around line 52-53: The type guard isNotificationInboxType currently uses the
`in` operator which checks the prototype chain and can return true for inherited
keys; change it to use Object.hasOwn(NOTIFICATION_PRESENTATIONS, value) so only
own properties are considered. Locate the isNotificationInboxType function and
replace the `value in NOTIFICATION_PRESENTATIONS` check with an Object.hasOwn
call referencing NOTIFICATION_PRESENTATIONS and value to make the guard
accurate.

In `@src/utils/ts/promise.ts`:
- Around line 28-35: After awaiting iteratee(items[currentIndex], currentIndex)
a worker may return after another worker has set isAborted=true; update the
worker code (the block around iteratee, results, onResolved) to check isAborted
immediately after the await and before assigning results[currentIndex] or
calling onResolved, and skip those steps if isAborted is true; keep the existing
catch that sets isAborted=true and rethrows so new failures abort others, but
prevent in-flight completions from mutating results or invoking onResolved
(symbols to update: iteratee, items, currentIndex, results, onResolved,
isAborted).

---

Nitpick comments:
In `@src/apis/notification/index.ts`:
- Around line 41-46: markInboxNotificationAsRead currently lacks an explicit
return type so TypeScript infers Promise<unknown>; update it to a concrete
response type by defining/using a domain response in entity.ts (e.g.,
NotificationReadResponse or ApiResponse<void> if no payload) and annotate the
function as returning Promise<ThatType>, then pass that type as the generic to
apiClient.patch and import the type into src/apis/notification/index.ts so
markInboxNotificationAsRead and its apiClient.patch call are strongly typed.

In `@src/pages/Auth/SignUp/FinishStep.tsx`:
- Around line 8-14: Extract the page logic into a new hook
src/pages/Auth/SignUp/hooks/useFinishStep.ts: move the
useSuspenseQuery(authQueries.myInfo()), the useAuthStore((s)=>s.setUser) call
and useNavigate() into that hook, have it expose the fetched myInfo (or null)
and a start/handleStart function that calls setUser(myInfo) and
navigate('/guide'); then update FinishStep.tsx to import and use useFinishStep
(replacing inline useSuspenseQuery, setUser, navigate and handleStart) so the
page component only handles UI rendering.

In `@src/utils/hooks/useAdvertisementInterval.ts`:
- Around line 60-97: The useLayoutEffect in useAdvertisementInterval includes
itemCount in its dependency array but does not reference it inside the effect;
add a short explanatory comment next to the effect or near the dependency array
(or inside the effect) stating that itemCount is included intentionally to force
re-measurement when the number of items changes (so the measurement logic like
measureAdvertisementInterval and observedElements updates run), referencing
useAdvertisementInterval, measureAdvertisementInterval, and the itemCount
dependency so future readers understand this is deliberate and not an accidental
leftover.

In `@src/utils/ts/imagePreprocessor.ts`:
- Around line 161-173: The timeout handler currently takes the timed-out request
via takePendingImagePreprocessRequest(id), calls resetImagePreprocessWorker(),
resolves that request with null, and then calls
settlePendingImagePreprocessRequests to resolve ALL other pending requests to
null; change this so the handler only fails the timed-out request: remove or
avoid calling settlePendingImagePreprocessRequests and do not tear down the
worker unless you explicitly want to terminate it; keep only the logic that
retrieves currentRequest, resolve it with null, and optionally decide separately
whether to call resetImagePreprocessWorker() (if you choose to terminate the
worker) so other pending requests remain pending and can still complete.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: c1238209-1205-4761-b421-53dfc896a2f9

📥 Commits

Reviewing files that changed from the base of the PR and between 488168b and 7da1524.

📒 Files selected for processing (31)
  • src/apis/advertisement/queries.ts
  • src/apis/auth/queries.ts
  • src/apis/club/managedQueries.ts
  • src/apis/club/queries.ts
  • src/apis/inquiry/mutations.ts
  • src/apis/notification/index.ts
  • src/apis/notification/mutations.ts
  • src/apis/notification/queries.ts
  • src/apis/studyTime/hooks.ts
  • src/apis/studyTime/mutations.ts
  • src/apis/studyTime/queries.ts
  • src/components/layout/Header/components/SubpageHeader.tsx
  • src/components/layout/Header/headerConfig.ts
  • src/pages/Auth/SignUp/FinishStep.tsx
  • src/pages/Chat/hooks/useChat.ts
  • src/pages/Club/Application/clubFeePage.tsx
  • src/pages/Club/ClubDetail/components/ClubMember.tsx
  • src/pages/Club/ClubDetail/components/ClubRecruitment.tsx
  • src/pages/Council/CouncilDetail/index.tsx
  • src/pages/Manager/ManagedAccount/index.tsx
  • src/pages/Manager/ManagedApplicationList/index.tsx
  • src/pages/Manager/ManagedClubProfile/index.tsx
  • src/pages/Manager/ManagedMemberList/index.tsx
  • src/pages/Manager/ManagedRecruitmentForm/index.tsx
  • src/pages/Manager/ManagedRecruitmentWrite/index.tsx
  • src/pages/Timer/hooks/useStudyTimer.ts
  • src/utils/hooks/useAdvertisementInterval.ts
  • src/utils/ts/imagePreprocessor.ts
  • src/utils/ts/notification.ts
  • src/utils/ts/promise.ts
  • src/utils/ts/viewport.ts
✅ Files skipped from review due to trivial changes (3)
  • src/apis/advertisement/queries.ts
  • src/apis/auth/queries.ts
  • src/apis/notification/mutations.ts
🚧 Files skipped from review as they are similar to previous changes (16)
  • src/pages/Club/ClubDetail/components/ClubRecruitment.tsx
  • src/pages/Club/ClubDetail/components/ClubMember.tsx
  • src/components/layout/Header/headerConfig.ts
  • src/apis/inquiry/mutations.ts
  • src/pages/Timer/hooks/useStudyTimer.ts
  • src/pages/Manager/ManagedApplicationList/index.tsx
  • src/apis/studyTime/hooks.ts
  • src/pages/Council/CouncilDetail/index.tsx
  • src/components/layout/Header/components/SubpageHeader.tsx
  • src/apis/studyTime/mutations.ts
  • src/pages/Manager/ManagedAccount/index.tsx
  • src/utils/ts/viewport.ts
  • src/pages/Chat/hooks/useChat.ts
  • src/apis/notification/queries.ts
  • src/pages/Club/Application/clubFeePage.tsx
  • src/apis/club/queries.ts

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant