Skip to content

feat: 라우트별 lazy import 도입 및 splitting 적용#196

Merged
ff1451 merged 3 commits intodevelopfrom
perf/01-route-splitting
Mar 20, 2026
Merged

feat: 라우트별 lazy import 도입 및 splitting 적용#196
ff1451 merged 3 commits intodevelopfrom
perf/01-route-splitting

Conversation

@ff1451
Copy link
Collaborator

@ff1451 ff1451 commented Mar 20, 2026

하나의 chunk로 묶여있던 페이지 코드들을 lazy import로 분리합니다

Summary by CodeRabbit

릴리스 노트

  • 새로운 기능

    • 라우트별 비동기 로딩 지원 및 로딩 폴백 컴포넌트 추가로 경로 전환 중 UX 향상
  • 성능 개선

    • 라우트 레벨 코드 분할로 초기 로드 시간 단축
    • 번들 분석 빌드 스크립트와 시각화 도구 추가
  • UI 개선

    • 헤더·로그인 화면의 이미지 및 플레이스홀더 시각 일관성 개선
    • 캐러셀과 추천 카드의 이미지 로딩 우선순위 조정
  • 버그 수정

    • 인증 초기화·사용자 하이드레이션의 동시성·상태 일관성 개선
    • 인증 초기화 중 공개 경로의 불필요한 렌더 차단 해소

@coderabbitai
Copy link

coderabbitai bot commented Mar 20, 2026

Walkthrough

라우팅 트리를 React.lazy와 Suspense로 감싸고 라우트 수준 코드 분할을 적용했습니다. 다수의 정적 페이지 임포트를 동적 import로 교체했고, 비동기 로드 중 표시되는 RouteLoadingFallback 컴포넌트를 추가했습니다. Layout의 Suspense fallback 표기와 일부 CSS 변수 참조 표기가 조정되었고, auth 초기화/수화(hydration) 로직의 중복 호출 방지 및 토큰 검사 흐름이 개선되었습니다. 그 외 여러 UI 컴포넌트에서 이미지 로딩/우선순위, 알림/카운트 훅, 홈용 공지 훅 등 기능 파일이 추가·변경되었습니다.

Possibly related PRs

  • PR 135 — src/stores/authStore.ts의 초기화/수화(hydration) 흐름과 중복 호출 방지(logic)를 변경해 직접적으로 동일 파일을 수정합니다.
  • PR 177 — InfoHeader, NotificationBell, Home 관련 훅/컴포넌트(예: CouncilNoticeSection, RecommendedClubCard, InfiniteClubCarousel) 등 동일 파일군을 변경해 코드 수준 충돌 가능성이 높습니다.
  • PR 189 — src/App.tsx 및 라우팅·헤더·채팅 관련 컴포넌트 변경과 겹쳐 라우트 선언 및 관련 컴포넌트의 상호작용에 직접적인 연관이 있습니다.
🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR 제목이 주요 변경사항을 명확하게 요약하고 있습니다. 라우트별 lazy import 도입과 코드 splitting 적용이 정확히 반영되어 있습니다.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch perf/01-route-splitting
📝 Coding Plan
  • Generate coding plan for human review comments

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

@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

Caution

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

⚠️ Outside diff range comments (1)
src/components/layout/index.tsx (1)

31-42: ⚠️ Potential issue | 🟡 Minor

Suspensemain 안으로 옮겨서 로딩 중 레이아웃 일관성 유지하기

현재 Outlet suspend 시 Suspense fallback이 전체 main을 대체하면서 contentClassName, 헤더 padding, pb-19 여백이 모두 사라집니다. src/App.tsx에서 contentClassName="bg-indigo-0"를 넘기는 라우트에서 로딩 중 배경색이 갑자기 바뀌는 문제가 발생합니다.

Suspensemain 안쪽으로 이동하면 레이아웃 클래스가 유지되어 안정적입니다.

제안 수정
-      <Suspense fallback={<RouteLoadingFallback />}>
-        <main
-          className={cn(
-            'bg-background box-border flex min-h-0 flex-1 flex-col overflow-y-auto overscroll-contain [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden',
-            hasHeader && (isInfoHeader ? 'pt-15' : isManagerHeader ? 'pt-(--manager-header-height)' : 'pt-11'),
-            showBottomNav && 'pb-19',
-            contentClassName
-          )}
-        >
-          <Outlet />
-        </main>
-      </Suspense>
+      <main
+        className={cn(
+          'bg-background box-border flex min-h-0 flex-1 flex-col overflow-y-auto overscroll-contain [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden',
+          hasHeader && (isInfoHeader ? 'pt-15' : isManagerHeader ? 'pt-(--manager-header-height)' : 'pt-11'),
+          showBottomNav && 'pb-19',
+          contentClassName
+        )}
+      >
+        <Suspense fallback={<RouteLoadingFallback />}>
+          <Outlet />
+        </Suspense>
+      </main>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/layout/index.tsx` around lines 31 - 42, The Suspense fallback
currently wraps the entire main element causing layout classes
(contentClassName, header paddings computed from
hasHeader/isInfoHeader/isManagerHeader, and showBottomNav pb-19) to be replaced
during loading; move the Suspense so main always renders and wrap only the
Outlet with Suspense (using RouteLoadingFallback) so the main DOM and its
className computation remain intact while Outlet suspends.
🧹 Nitpick comments (1)
src/App.tsx (1)

14-49: lazy import도 @/ 별칭으로 통일해 주세요.

이 블록만 상대 경로를 쓰고 있어서 파일 이동이나 폴더 정리 때 깨지기 쉽습니다. 정적 import와 동일하게 @/pages/...로 맞추는 편이 유지보수에 유리합니다.

📦 예시
-const ConfirmStep = lazy(() => import('./pages/Auth/SignUp/ConfirmStep'));
+const ConfirmStep = lazy(() => import('@/pages/Auth/SignUp/ConfirmStep'));

As per coding guidelines, @/* alias를 상대 경로 대신 사용해야 합니다.

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

In `@src/App.tsx` around lines 14 - 49, The lazy imports for components like
ConfirmStep, FinishStep, NameStep, StudentIdStep, TermStep, UniversityStep,
ChatListPage, ChatRoom, ApplicationPage, ApplyCompletePage, ClubFeePage,
ClubDetail, ClubList, ClubSearch, CouncilDetail, CouncilNotice, GuidePage,
LicensePage, MarketingPolicyPage, PrivacyPolicyPage, TermsPage, ManagedAccount,
ManagedApplicationDetail, ManagedApplicationList, ManagedClubDetail,
ManagedClubInfo, ManagedClubList, ManagedMemberApplicationDetail,
ManagedMemberList, ManagedRecruitment, ManagedRecruitmentForm,
ManagedRecruitmentWrite, Schedule, Timer, MyPage, and Profile use relative paths
and should be switched to the project alias; update each lazy(() =>
import('./pages/...')) to use the alias form lazy(() => import('@/pages/...'))
so all imports match the alias-based convention and avoid breakage during
refactors.
🤖 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/components/common/RouteLoadingFallback.tsx`:
- Around line 9-18: The loading markup in RouteLoadingFallback lacks readable
text for screen readers; update the component (RouteLoadingFallback) so the
visible spinner div has aria-hidden="true" (e.g., the element with className
"h-8 w-8 animate-spin ...") and add a visually-hidden / sr-only text node inside
the role="status" container (e.g., "Loading…" or "로딩 중…") so screen readers
announce the status while the spinner is hidden from assistive tech.

---

Outside diff comments:
In `@src/components/layout/index.tsx`:
- Around line 31-42: The Suspense fallback currently wraps the entire main
element causing layout classes (contentClassName, header paddings computed from
hasHeader/isInfoHeader/isManagerHeader, and showBottomNav pb-19) to be replaced
during loading; move the Suspense so main always renders and wrap only the
Outlet with Suspense (using RouteLoadingFallback) so the main DOM and its
className computation remain intact while Outlet suspends.

---

Nitpick comments:
In `@src/App.tsx`:
- Around line 14-49: The lazy imports for components like ConfirmStep,
FinishStep, NameStep, StudentIdStep, TermStep, UniversityStep, ChatListPage,
ChatRoom, ApplicationPage, ApplyCompletePage, ClubFeePage, ClubDetail, ClubList,
ClubSearch, CouncilDetail, CouncilNotice, GuidePage, LicensePage,
MarketingPolicyPage, PrivacyPolicyPage, TermsPage, ManagedAccount,
ManagedApplicationDetail, ManagedApplicationList, ManagedClubDetail,
ManagedClubInfo, ManagedClubList, ManagedMemberApplicationDetail,
ManagedMemberList, ManagedRecruitment, ManagedRecruitmentForm,
ManagedRecruitmentWrite, Schedule, Timer, MyPage, and Profile use relative paths
and should be switched to the project alias; update each lazy(() =>
import('./pages/...')) to use the alias form lazy(() => import('@/pages/...'))
so all imports match the alias-based convention and avoid breakage during
refactors.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 2881528b-3ee8-4684-8c6f-2a36ea64ab4e

📥 Commits

Reviewing files that changed from the base of the PR and between ff0f3a1 and c3a21fb.

📒 Files selected for processing (3)
  • src/App.tsx
  • src/components/common/RouteLoadingFallback.tsx
  • src/components/layout/index.tsx

Comment on lines +9 to +18
<div
role="status"
aria-live="polite"
className={cn(
'text-body3 flex w-full items-center justify-center gap-3 text-indigo-400',
fullScreen ? 'h-(--viewport-height)' : 'min-h-full flex-1 py-10'
)}
>
<div className="h-8 w-8 animate-spin rounded-full border-4 border-indigo-200 border-t-indigo-600" />
</div>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

로딩 상태가 스크린리더에 전달되지 않습니다.

role="status"는 들어갔지만 읽을 텍스트가 없어서 보조기기에는 상태 변화가 거의 전달되지 않습니다. sr-only 문구를 넣고 스피너는 aria-hidden="true"로 숨겨 주세요.

♿ 제안 수정
     <div
       role="status"
       aria-live="polite"
       className={cn(
         'text-body3 flex w-full items-center justify-center gap-3 text-indigo-400',
         fullScreen ? 'h-(--viewport-height)' : 'min-h-full flex-1 py-10'
       )}
     >
-      <div className="h-8 w-8 animate-spin rounded-full border-4 border-indigo-200 border-t-indigo-600" />
+      <div
+        aria-hidden="true"
+        className="h-8 w-8 animate-spin rounded-full border-4 border-indigo-200 border-t-indigo-600"
+      />
+      <span className="sr-only">페이지를 불러오는 중</span>
     </div>

As per coding guidelines, 접근성(aria-*, role, 키보드 탐색)이 적절히 처리되어야 합니다.

📝 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
<div
role="status"
aria-live="polite"
className={cn(
'text-body3 flex w-full items-center justify-center gap-3 text-indigo-400',
fullScreen ? 'h-(--viewport-height)' : 'min-h-full flex-1 py-10'
)}
>
<div className="h-8 w-8 animate-spin rounded-full border-4 border-indigo-200 border-t-indigo-600" />
</div>
<div
role="status"
aria-live="polite"
className={cn(
'text-body3 flex w-full items-center justify-center gap-3 text-indigo-400',
fullScreen ? 'h-(--viewport-height)' : 'min-h-full flex-1 py-10'
)}
>
<div
aria-hidden="true"
className="h-8 w-8 animate-spin rounded-full border-4 border-indigo-200 border-t-indigo-600"
/>
<span className="sr-only">페이지를 불러오는 중</span>
</div>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/common/RouteLoadingFallback.tsx` around lines 9 - 18, The
loading markup in RouteLoadingFallback lacks readable text for screen readers;
update the component (RouteLoadingFallback) so the visible spinner div has
aria-hidden="true" (e.g., the element with className "h-8 w-8 animate-spin ...")
and add a visually-hidden / sr-only text node inside the role="status" container
(e.g., "Loading…" or "로딩 중…") so screen readers announce the status while the
spinner is hidden from assistive tech.

* refactor: 인증 불필요 페이지 검증 제거

* refactor: 인증 필요 페이지 로직 수정

* Perf/03 home data request shaping (#198)

* refactor: 채팅 hook 분리 및 호출 시점 정리

* refactor: 총동 공지사항 조회 수 수정

* Perf/04 home lighthouse (#199)

* chore: svg -> png 변환 및 리사이징

* chore: 번들 분석 라이브러리 추가

* refactor: 캐러셀 이미지 지연 로드 적요
Copy link

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

🧹 Nitpick comments (2)
src/pages/Home/components/InfiniteClubCarousel.tsx (1)

58-59: 같은 우선순위 계산을 한 번만 수행하면 가독성이 좋아집니다.

같은 isPriorityImage(...) 호출이 중복되어 있어 한 번 계산한 값을 재사용하는 편이 더 명확합니다.

리팩터링 예시
           {displayClubs.map(({ club, key }, index) => {
             const isDuplicate = shouldLoop && (index < clubs.length || index >= clubs.length * 2);
+            const shouldPrioritizeImage = isPriorityImage(index, clubs.length, shouldLoop);

             return (
               <div
                 key={key}
                 className={cn('shrink-0 py-1', shouldCenterCard ? 'snap-center' : 'snap-start px-[3px]')}
                 style={{ width: `${CLUB_CARD_WIDTH}px` }}
               >
                 <RecommendedClubCard
                   club={club}
                   className="w-full"
                   ariaHidden={isDuplicate}
-                  imageFetchPriority={isPriorityImage(index, clubs.length, shouldLoop) ? 'auto' : 'low'}
-                  imageLoading={isPriorityImage(index, clubs.length, shouldLoop) ? 'eager' : 'lazy'}
+                  imageFetchPriority={shouldPrioritizeImage ? 'auto' : 'low'}
+                  imageLoading={shouldPrioritizeImage ? 'eager' : 'lazy'}
                   tabIndex={isDuplicate ? -1 : 0}
                 />
               </div>
             );
           })}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/Home/components/InfiniteClubCarousel.tsx` around lines 58 - 59,
Compute the result of isPriorityImage(index, clubs.length, shouldLoop) once and
reuse it for both props to improve readability: call isPriorityImage(...) into a
local const (e.g., isPriority) near where index is in scope, then use that
boolean to set imageFetchPriority ('auto'/'low') and imageLoading
('eager'/'lazy') instead of calling isPriorityImage twice; update the component
that renders the image (the element assigning imageFetchPriority and
imageLoading) to reference this const.
src/stores/authStore.ts (1)

34-64: hydrateUserinitialize 외부로 분리하면 가독성이 향상됩니다.

현재 hydrateUserinitialize 내부에 정의되어 있어 매 호출마다 새 클로저가 생성됩니다. 모듈 레벨 hydrateUserPromise로 중복 호출은 방지되지만, 스토어 외부 또는 상단에 분리하면 구조가 더 명확해집니다.

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

In `@src/stores/authStore.ts` around lines 34 - 64, The hydrateUser function is
currently defined inside initialize causing a new closure on each call; extract
hydrateUser to module scope (outside initialize) while keeping the shared
hydrateUserPromise variable at module level, and update references to
getMyInfo(), get(), set(), and window.ReactNativeWebView so behavior is
identical; ensure hydrateUser still checks get().accessToken against the passed
nextAccessToken, posts the LOGIN_COMPLETE message, handles bridge errors, and
resets hydrateUserPromise in finally, and update initialize to call the newly
moved hydrateUser(nextAccessToken).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@package.json`:
- Line 10: Update package.json to use cross-env for cross-platform environment
variables by adding cross-env to devDependencies and changing the
"build:analyze:staging" script to prefix ANALYZE=true with cross-env (i.e.,
"cross-env ANALYZE=true ..."); ensure you add a .env.example at project root
containing VITE_API_PATH (e.g., VITE_API_PATH=http://localhost:3000/api) because
src/apis/client.ts expects VITE_API_PATH; also add or document a .env.staging
file (or ensure .env.staging is created) for the --mode staging build so the
build has the required staging env vars.

In `@src/components/layout/Header/components/InfoHeader.tsx`:
- Around line 13-18: The component InfoHeader.tsx is using a non-existent token
`bg-indigo-10`; add the missing token and switch the usage: define
`--color-indigo-5: `#f4f6f9`` in your theme.css and replace `bg-indigo-10` in
InfoHeader.tsx with `bg-indigo-5` (or update any className that references
`bg-indigo-10`) so the background color uses the newly defined
`--color-indigo-5` token.
- Around line 22-23: In InfoHeader.tsx, the second skeleton div uses a
non-existent CSS token "bg-indigo-10"; replace that class with a defined token
(e.g., "bg-indigo-5" or another valid token in theme.css such as "bg-indigo-25")
so the element uses an existing color variable—update the className string on
the element containing "bg-indigo-10 h-4 w-36 animate-pulse rounded" to use the
valid token.

In `@src/pages/Chat/hooks/useUnreadChatCount.ts`:
- Around line 37-43: The hook useUnreadChatCount.ts uses useQuery with
chatQueryKeys.rooms() but sets a different staleTime/refetchInterval than the
existing hook useChat.ts, which can cause unexpected cache behavior; update
useUnreadChatCount.ts so its useQuery options (staleTime and refetchInterval)
match the ones used by useChat.ts, or change the query key to a dedicated one
(e.g., chatQueryKeys.unreadRooms()) and keep distinct timings—locate the
useQuery call in useUnreadChatCount.ts and either copy the exact option values
from useChat.ts or replace chatQueryKeys.rooms() with a unique key to avoid
option collisions.

In `@src/pages/Home/components/InfiniteClubCarousel.tsx`:
- Around line 12-20: isPriorityImage currently only prioritizes images around
the computed middleSegmentStartIndex when shouldLoop is true, which can miss the
actual initially visible cards before scroll correction; update isPriorityImage
to also include the initial visible range so first-paint images get high
priority. Modify the function (isPriorityImage, parameters index, clubsLength,
shouldLoop and local middleSegmentStartIndex) to compute a visible window (e.g.,
include indices < 2 or a configurable visibleCount) and return true if index
falls within either the looped middleSegmentStartIndex ± N OR the initial
visible range (index < visibleCount), ensuring the initial on-screen cards are
marked priority on first render.

In `@src/pages/Home/hooks/useGetHomeCouncilNotices.ts`:
- Line 3: The Home hook currently imports councilQueryKeys from the
CouncilDetail page hook (useGetCouncilInfo), creating a route-to-route
dependency; extract councilQueryKeys into a shared domain module (e.g., create a
new module named something like src/apis/council/queryKeys.ts) and export the
same symbol (councilQueryKeys) there, then update useGetHomeCouncilNotices.ts
and the CouncilDetail hook (useGetCouncilInfo) to import councilQueryKeys from
that new shared module instead of from the page hook to avoid chunk coupling.

In `@src/stores/authStore.ts`:
- Around line 29-32: The initialization currently sets isAuthenticated based
solely on user (the user variable) which can mark the app authenticated even if
accessToken is expired; update the logic in the auth initialization (where set({
isAuthenticated: true, isLoading: false }) is called) to validate the token or
attempt a refresh before marking authenticated: check the stored accessToken
expiry (or call an existing refresh method such as
refreshAccessToken/refreshToken or an apiClient.refresh flow) and only set
isAuthenticated true after a successful validation/refresh, otherwise clear
user/auth state and set isAuthenticated false; reference the user variable, the
accessToken/refreshToken storage, and the set(...) call to locate where to
implement this change.

---

Nitpick comments:
In `@src/pages/Home/components/InfiniteClubCarousel.tsx`:
- Around line 58-59: Compute the result of isPriorityImage(index, clubs.length,
shouldLoop) once and reuse it for both props to improve readability: call
isPriorityImage(...) into a local const (e.g., isPriority) near where index is
in scope, then use that boolean to set imageFetchPriority ('auto'/'low') and
imageLoading ('eager'/'lazy') instead of calling isPriorityImage twice; update
the component that renders the image (the element assigning imageFetchPriority
and imageLoading) to reference this const.

In `@src/stores/authStore.ts`:
- Around line 34-64: The hydrateUser function is currently defined inside
initialize causing a new closure on each call; extract hydrateUser to module
scope (outside initialize) while keeping the shared hydrateUserPromise variable
at module level, and update references to getMyInfo(), get(), set(), and
window.ReactNativeWebView so behavior is identical; ensure hydrateUser still
checks get().accessToken against the passed nextAccessToken, posts the
LOGIN_COMPLETE message, handles bridge errors, and resets hydrateUserPromise in
finally, and update initialize to call the newly moved
hydrateUser(nextAccessToken).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: f66597d5-2e75-4366-bba5-237f7dde2ad4

📥 Commits

Reviewing files that changed from the base of the PR and between c3a21fb and 2e67ebd.

⛔ Files ignored due to path filters (4)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml, !pnpm-lock.yaml and included by **
  • src/assets/image/chat-cat-header.png is excluded by !**/*.png, !src/assets/** and included by **
  • src/assets/image/chat-cat-login.png is excluded by !**/*.png, !src/assets/** and included by **
  • src/assets/svg/chat-cat.svg is excluded by !**/*.svg, !src/assets/** and included by **
📒 Files selected for processing (13)
  • package.json
  • src/components/auth/AuthGuard.tsx
  • src/components/layout/Header/components/InfoHeader.tsx
  • src/components/layout/Header/components/NotificationBell.tsx
  • src/pages/Auth/Login/index.tsx
  • src/pages/Chat/hooks/useUnreadChatCount.ts
  • src/pages/Council/CouncilDetail/hooks/useGetCouncilInfo.ts
  • src/pages/Home/components/CouncilNoticeSection.tsx
  • src/pages/Home/components/InfiniteClubCarousel.tsx
  • src/pages/Home/components/RecommendedClubCard.tsx
  • src/pages/Home/hooks/useGetHomeCouncilNotices.ts
  • src/stores/authStore.ts
  • vite.config.ts
✅ Files skipped from review due to trivial changes (1)
  • src/pages/Council/CouncilDetail/hooks/useGetCouncilInfo.ts

"scripts": {
"start": "vite",
"build": "tsc -b && vite build",
"build:analyze:staging": "ANALYZE=true tsc -b && ANALYZE=true vite build --mode staging",
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# .env.staging 파일 존재 여부 확인
fd -t f '.env.staging' --max-depth 1

# 또는 .env 관련 파일 목록 확인
ls -la .env* 2>/dev/null || echo "No .env files found in root"

Repository: BCSDLab/KONECT_FRONT_END

Length of output: 95


🏁 Script executed:

# 1. .env.example 파일 확인
fd -t f '.env.example' 

# 2. package.json에서 cross-env 존재 여부 확인
grep -E '"cross-env"|dependencies|devDependencies' package.json | head -20

# 3. src/apis/client.ts에서 VITE_API_PATH 사용 확인
fd -t f 'client.ts' src/apis/ && cat src/apis/client.ts | grep -A 5 -B 5 'VITE_API_PATH'

Repository: BCSDLab/KONECT_FRONT_END

Length of output: 507


🏁 Script executed:

# .env.example 파일 내용 확인
cat .env.example

# package.json의 전체 scripts와 devDependencies 확인
cat package.json | head -60

Repository: BCSDLab/KONECT_FRONT_END

Length of output: 1763


크로스 플랫폼 호환성을 위해 cross-env 사용 필수

ANALYZE=true 문법은 Windows에서 동작하지 않습니다. cross-env 패키지를 devDependencies에 추가하고 적용해야 합니다.

또한 --mode staging 사용 시 필요한 .env.staging 파일이 없고, 더 중요하게는 src/apis/client.ts에서 필수로 요구하는 VITE_API_PATH 환경변수를 정의할 .env.example 파일이 존재하지 않습니다.

♻️ 수정 예시

1. cross-env 적용

-    "build:analyze:staging": "ANALYZE=true tsc -b && ANALYZE=true vite build --mode staging",
+    "build:analyze:staging": "cross-env ANALYZE=true tsc -b && cross-env ANALYZE=true vite build --mode staging",

2. devDependencies에 cross-env 추가

+    "cross-env": "^7.0.3",

3. .env.example 생성 (프로젝트 루트)

VITE_API_PATH=http://localhost:3000/api
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@package.json` at line 10, Update package.json to use cross-env for
cross-platform environment variables by adding cross-env to devDependencies and
changing the "build:analyze:staging" script to prefix ANALYZE=true with
cross-env (i.e., "cross-env ANALYZE=true ..."); ensure you add a .env.example at
project root containing VITE_API_PATH (e.g.,
VITE_API_PATH=http://localhost:3000/api) because src/apis/client.ts expects
VITE_API_PATH; also add or document a .env.staging file (or ensure .env.staging
is created) for the --mode staging build so the build has the required staging
env vars.

Comment on lines +37 to +43
const { data } = useQuery({
queryKey: chatQueryKeys.rooms(),
queryFn: getChatRooms,
enabled: isEnabled,
staleTime: UNREAD_CHAT_COUNT_REFETCH_INTERVAL,
refetchInterval: isEnabled ? UNREAD_CHAT_COUNT_REFETCH_INTERVAL : false,
});
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# useChat.ts의 쿼리 설정 확인
ast-grep --pattern $'useQuery({
  queryKey: chatQueryKeys.rooms(),
  $$$
})'

Repository: BCSDLab/KONECT_FRONT_END

Length of output: 641


🏁 Script executed:

fd -type f -name "*useChat*" src/

Repository: BCSDLab/KONECT_FRONT_END

Length of output: 238


🏁 Script executed:

rg "chatQueryKeys\.rooms\(\)" src/ -A 5 -B 2

Repository: BCSDLab/KONECT_FRONT_END

Length of output: 2409


🏁 Script executed:

cat -n src/pages/Chat/hooks/useUnreadChatCount.ts | head -50

Repository: BCSDLab/KONECT_FRONT_END

Length of output: 1683


동일 쿼리 키의 옵션 불일치로 인한 캐시 동작 확인 필요

useChat.ts와 동일한 chatQueryKeys.rooms()을 사용하면서 staleTime 설정이 다릅니다. 두 훅이 동시에 활성화될 때 캐시 동작이 예기치 않게 동작할 수 있으니, 쿼리 옵션을 통일하거나 별도의 쿼리 키 사용을 검토해주세요.

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

In `@src/pages/Chat/hooks/useUnreadChatCount.ts` around lines 37 - 43, The hook
useUnreadChatCount.ts uses useQuery with chatQueryKeys.rooms() but sets a
different staleTime/refetchInterval than the existing hook useChat.ts, which can
cause unexpected cache behavior; update useUnreadChatCount.ts so its useQuery
options (staleTime and refetchInterval) match the ones used by useChat.ts, or
change the query key to a dedicated one (e.g., chatQueryKeys.unreadRooms()) and
keep distinct timings—locate the useQuery call in useUnreadChatCount.ts and
either copy the exact option values from useChat.ts or replace
chatQueryKeys.rooms() with a unique key to avoid option collisions.

@@ -0,0 +1,14 @@
import { useSuspenseQuery } from '@tanstack/react-query';
import { getCouncilNotice } from '@/apis/council';
import { councilQueryKeys } from '@/pages/Council/CouncilDetail/hooks/useGetCouncilInfo';
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

라우트 간 쿼리키 의존을 분리해주세요.

Line 3처럼 Home 훅이 CouncilDetail 페이지 훅 파일을 직접 참조하면, 라우트 분할 시 청크 결합이 생겨 성능 이점이 줄 수 있습니다. councilQueryKeys를 페이지 경로가 아닌 공유 도메인 모듈(예: @/apis/council/queryKeys)로 분리해 양쪽에서 공통 import 하세요.

예시 변경안
- import { councilQueryKeys } from '@/pages/Council/CouncilDetail/hooks/useGetCouncilInfo';
+ import { councilQueryKeys } from '@/apis/council/queryKeys';
// src/apis/council/queryKeys.ts
export const councilQueryKeys = {
  all: ['council'] as const,
  noticesPreview: (limit: number) => [...councilQueryKeys.all, 'noticesPreview', limit] as const,
};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/Home/hooks/useGetHomeCouncilNotices.ts` at line 3, The Home hook
currently imports councilQueryKeys from the CouncilDetail page hook
(useGetCouncilInfo), creating a route-to-route dependency; extract
councilQueryKeys into a shared domain module (e.g., create a new module named
something like src/apis/council/queryKeys.ts) and export the same symbol
(councilQueryKeys) there, then update useGetHomeCouncilNotices.ts and the
CouncilDetail hook (useGetCouncilInfo) to import councilQueryKeys from that new
shared module instead of from the page hook to avoid chunk coupling.

Copy link

@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.

🧹 Nitpick comments (1)
src/pages/Home/components/InfiniteClubCarousel.tsx (1)

66-67: LCP 최적화를 위해 fetchPriority="high" 고려해 볼 수 있습니다.

현재 'auto'도 동작하지만, 우선 로드가 필요한 이미지라면 'high'가 브라우저에게 더 명시적인 힌트를 줍니다.

선택적 수정 제안
-                  imageFetchPriority={isPriority ? 'auto' : 'low'}
+                  imageFetchPriority={isPriority ? 'high' : 'low'}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/Home/components/InfiniteClubCarousel.tsx` around lines 66 - 67, For
LCP optimization, change the image fetch priority when isPriority is true from
'auto' to 'high' by updating the imageFetchPriority prop used in
InfiniteClubCarousel (where imageFetchPriority currently uses isPriority ?
'auto' : 'low'); keep the fallback 'low' for non-priority images and leave
imageLoading (isPriority ? 'eager' : 'lazy') as-is so high-priority images get
both 'high' fetch priority and eager loading.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/pages/Home/components/InfiniteClubCarousel.tsx`:
- Around line 66-67: For LCP optimization, change the image fetch priority when
isPriority is true from 'auto' to 'high' by updating the imageFetchPriority prop
used in InfiniteClubCarousel (where imageFetchPriority currently uses isPriority
? 'auto' : 'low'); keep the fallback 'low' for non-priority images and leave
imageLoading (isPriority ? 'eager' : 'lazy') as-is so high-priority images get
both 'high' fetch priority and eager loading.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: e2ab5b5e-78b1-4b08-95dd-454acac88d34

📥 Commits

Reviewing files that changed from the base of the PR and between 2e67ebd and 2ba2c10.

📒 Files selected for processing (4)
  • src/components/layout/Header/components/InfoHeader.tsx
  • src/components/layout/index.tsx
  • src/pages/Home/components/InfiniteClubCarousel.tsx
  • src/stores/authStore.ts
✅ Files skipped from review due to trivial changes (1)
  • src/components/layout/Header/components/InfoHeader.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/stores/authStore.ts

@ff1451 ff1451 merged commit ce04c0f into develop Mar 20, 2026
2 checks passed
@ff1451 ff1451 self-assigned this Mar 20, 2026
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