Skip to content

모임 선택 화면에 기존 모임 선택 기능 추가#10

Merged
yoouyeon merged 26 commits intodevelopfrom
feat/9-add-meeting-settlement
Feb 17, 2026
Merged

모임 선택 화면에 기존 모임 선택 기능 추가#10
yoouyeon merged 26 commits intodevelopfrom
feat/9-add-meeting-settlement

Conversation

@yoouyeon
Copy link
Contributor

@yoouyeon yoouyeon commented Dec 14, 2025

카카오 로그인 작업 브랜치 기반으로 작업한거라 앞선 PR(#8) 머지된 후에 리뷰 요청 드리겠습니다!

💻 작업 내용

  • 모임 선택 페이지에서 기존 모임을 선택할 수 있도록 했습니다.
  • 기존 모임 조회 API를 연결할 준비를 해두었습니다. (mock api를 사용하고 있습니다!)
  • 지출을 의미하는 단어가 bill과 expense 두가지로 혼용되고 있어서 expense로 통일했습니다.
  • 모임 토큰을 localstorage에 저장하는 대신 url query param으로 받아서 사용하는 방식으로 수정했습니다.
    📍 이건 localstorage에 토큰 저장하던 것들을 차차 수정해야 한다고 얘기 나눴던 기억이 있어서 변경한건데, 만약 localstorage 저장 방식을 유지해야 한다면 수정하겠습니다!

  • 네임칩 컴포넌트를 추가했습니다! 기존 칩 컴포넌트와 variant가 달라서 새로 만들었는데요.
    예전에 컴포넌트 정리했던 것에서 컴포넌트가 달라진 부분이 있는 것 같아서 이것도 UI 작업 하면서 수정하면 좋을 것 같습니당!!

Screen_Shot 2025-12-14 19 55 51 Screen_Shot 2025-12-14 19 59 02

📸 스크린샷

기존 모임이 없을 때 기존 모임이 있을 때
Screen_Shot 2025-12-14 19 41 57 Screen_Shot 2025-12-14 19 40 58

Summary by CodeRabbit

  • New Features

    • '청구(Bill)' 명칭을 '지출(Expense)'으로 전면 전환 — 관련 화면·버튼·페이지가 업데이트됨.
    • 그룹 목록 조회, 그룹 생성 동선, 빈 상태(Empty) UI 및 그룹별 바로가기 추가.
    • NameChip 컴포넌트 도입으로 멤버 표시가 시각적으로 개선됨.
    • 멤버별 결제 상태(결제 여부·타임스탬프) 표시 추가.
  • Improvements

    • 보호된 라우팅과 생성 페이지 가드로 내비게이션 흐름 및 접근 제어 강화.
    • 그룹 데이터 로딩 흐름과 오류 처리 개선, 로딩 상태 표시 추가.

- 모임 생성 페이지로 이동하는 버튼
- 비어있음 표시 버튼
- 기존 모임 선택 버튼
객체 메소드 구조에서 개별 함수로 분리함
fsd 작업하면서 지출 용어를 bill에서 expense로 변경하기로 결정함.
변경하지 못했던 부분을 수정
#7
- group token을 url 파라미터로 받도록 라우트 수정
- loader에서 그룹 토큰만 확인하도록 수정 (로그인 여부는 상위 로더에서 확인)
- loader 코드 위치를 페이지 가까이로 이동 (재사용 가능성 낮기 때문)
group 데이터에 대한 쿼리 관리를 편하게 하기 위해서 group/api 디렉토리에 한 파일에 정의해둠
직관적인 이름으로 변경하고, 재사용을 위해 entities 디렉토리로 위치 변경
- 로더 함수 이름 변경
- 모임 토큰 유효성 확인용 모임 데이터 페칭 로직 추가
- 모임 데이터와 토큰을 반환하도록 수정
- groupToken을 사용한 데이터 페칭 로직 제거
- defaultFormValue 계산 시 groupData를 참조하도록 수정
axios의 params 옵션을 사용하여 자동으로 URL 인코딩되도록 수정
버튼 모양이지만 클릭 동작을 쓰지 않는 컴포넌트이므로 Button 대신 Box라는 이름을 씀
@coderabbitai
Copy link

coderabbitai bot commented Dec 14, 2025

📝 Walkthrough

Walkthrough

앱 전반에서 "Bill"을 "Expense"로 리네임하고 라우팅·쿼리 클라이언트·그룹 API·타입·목(mock) 데이터 및 일부 로더/훅/스타일을 재구성했습니다. 새로운 SelectGroup UI·NameChip 컴포넌트와 CreateExpense 페이지 가드가 추가되었습니다.

Changes

Cohort / File(s) Summary
라우터·경로
src/app/Router.tsx, src/shared/config/route.ts, src/shared/lib/generateShareLink.ts
Bill 관련 라우트·상수들을 Expense로 교체하고 Router에 protected route/guards, Suspense 경계 및 NotFound 추가. generateShareLink 내부 경로도 expense로 변경.
앱 진입·쿼리 클라이언트
src/app/App.tsx, src/shared/api/queryClient.ts
App에서 setupQueryClient 호출 제거. setupQueryClient API 삭제 및 queryClient 초기화에 defaultOptions(queries/mutations.throwOnError=true) 인라인 설정.
그룹 API·쿼리 훅
src/entities/group/api/group.ts, src/entities/group/api/groupQueries.ts
기존 default export 객체 제거, getGroupList/getGroupDetail/createGroup 도입 및 React Query 훅 useGetGroupList·useGetGroupDetail 추가.
그룹 타입·목 데이터 및 MSW
src/entities/group/model/group.type.ts, src/mocks/handlers/group.ts, src/pages/selectGroup/SelectGroupPage.stories.tsx
Group.id: string 추가; mock 그룹 데이터 멤버에 profile, isPaid, paidAt 필드 추가; MSW에 GET /api/v1/groups 엔드포인트 및 스토리 mock 변경(상대경로).
가드 로더·인증 정리
src/pages/CreateExpensePage/lib/createExpensePageGuardLoader.ts, src/entities/auth/lib/getGroupManagerAuth.ts
Create Expense용 guard loader 추가(그룹토큰 검증·리다이렉트). 기존 getGroupManagerAuth 파일 삭제.
Bill → Expense 대규모 리네임
src/pages/CreateExpensePage/*, src/pages/expenseDetail/*, src/features/expense-management/lib/createExpenseFunnel.type.ts, src/pages/*Step/*, src/pages/billDetail/index.ts, src/pages/createBill/index.ts
컴포넌트·페이지·타입·funnel id 등 Bill → Expense로 명칭 일괄 변경. 관련 타입·onEdit 시그니처·바렐(export) 정리 및 일부 re-export 제거.
폼 리스트 스타일명 변경
src/pages/createExpenseStep/..., src/pages/addExpenseStep/..., src/pages/editExpenseStep/...
스타일 export 명칭 BillFormListExpenseFormList로 변경하고 사용처 업데이트.
SelectGroup 페이지 및 UI 추가
src/pages/selectGroup/SelectGroupPage.tsx, src/pages/selectGroup/ui/*, src/pages/selectGroup/SelectGroupPage.styles.ts
로컬 상태·기존 스타일 파일 일부 제거, useGetGroupList로 데이터 기반 렌더링 전환. CreateGroupLinkButton, GroupLinkButton, EmptyBox 컴포넌트 및 바렐 추가. 기존 스타일 일부 삭제.
그 외 훅/바렐 정리
src/features/group-creation/api/useGetGroupBasicInfo.ts, src/features/group-creation/api/usePostCreateGroup.ts
useGetGroupBasicInfo 삭제. 그룹 생성 훅에서 group.postcreateGroup 호출로 변경.
그룹 데이터 사용으로 로직 조정
src/features/expense-management/lib/useAddExpenseFormArray.ts, src/pages/memberSetup/MemberSetupPage.tsx
로컬 비동기 상태 제거, 로더/쿼리에서 제공하는 groupData 사용. MemberSetupPage는 useGetGroupDetail 사용 및 403 처리 추가, 생성 버튼이 expense 경로로 이동.
UI 컴포넌트 추가: NameChip
src/shared/ui/NameChip/*, .storybook/main.ts
NameChip 컴포넌트(스타일·타입·스토리) 추가. Storybook viteFinal로 import.meta.env 조정(환경값 오버라이드).
기타 경로 참조 교체
src/features/character-management/..., src/pages/shareStep/ShareStepPage.tsx, src/pages/expenseDetail/ui/*
경로 네임스와 import 경로 일부 교체(예: characterShare, expenseDetail, ExpenseMemberItem 경로).

Possibly related PRs

Suggested labels: 🔨 Refactor, 🎨 Design

Poem

🐇
청구서는 벗고 비용이란 이름 불러왔네
길을 바꾸고 문을 걸어 가드가 지켜주니
이름표(Chip) 반짝이며 친구들 모였고
토큰 들고 폴짝폴짝 새 페이지로 가자
당근 하나로 축하해요 🥕✨

🚥 Pre-merge checks | ✅ 1
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/9-add-meeting-settlement

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.

@github-actions
Copy link

Storybook preview : https://6867dc9b2bbd1b3d76b92f4a-xciegopkwi.chromatic.com/

@yoouyeon
Copy link
Contributor Author

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Dec 14, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@yoouyeon yoouyeon added the ✨ Feature 신규 기능 추가 label Dec 14, 2025
@coderabbitai coderabbitai bot changed the title @coderabbitai 제목 인증·API 리팩터링 및 그룹 선택 페이지 재구축 Dec 14, 2025
@yoouyeon yoouyeon changed the title 인증·API 리팩터링 및 그룹 선택 페이지 재구축 모임 선택 화면에 기존 모임 선택 기능 추가 Dec 14, 2025
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: 3

Caution

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

⚠️ Outside diff range comments (6)
src/features/expense-management/ui/FormCard/index.tsx (1)

114-119: 날짜 파싱/타임존/빈 값에서 Invalid Date 가능성 있어요.
new Date('yyyy-MM-dd')는 UTC 파싱이라 로컬 타임존에서 날짜가 하루 밀릴 수 있고, field.value가 비어있으면 Invalid Date가 됩니다. date-fns로 명시 파싱/가드 추천합니다.

-import { format } from 'date-fns';
+import { format, parse } from 'date-fns';

                 <ExpenseDatePicker
-                  selected={new Date(field.value)}
-                  onChange={(date) =>
-                    field.onChange(format(date || new Date(), 'yyyy-MM-dd'))
-                  }
+                  selected={
+                    field.value
+                      ? parse(field.value, 'yyyy-MM-dd', new Date())
+                      : undefined
+                  }
+                  onChange={(date) =>
+                    field.onChange(date ? format(date, 'yyyy-MM-dd') : '')
+                  }
                 />
src/entities/auth/api/auth.ts (1)

4-9: 모의 응답과 인터페이스의 타입 불일치를 수정하세요.

GuestTokenData 인터페이스의 expiredAt 필드는 Date 타입으로 정의되어 있지만, src/mocks/handlers/auth.ts/api/v1/user/guest/token 엔드포인트 모의 응답에서는 문자열('2025-03-06 14:17:26')로 반환되고 있습니다. isMember 필드는 올바르게 포함되어 있습니다.

인터페이스의 expiredAtstring 타입으로 변경하거나, 모의 응답에서 Date 객체로 변환하여 타입 일관성을 맞춰주세요.

src/pages/expenseDetail/ExpenseDetailPage.tsx (1)

44-44: localStorage에서 groupToken 사용을 제거하세요.

PR에서 모임 토큰을 URL 쿼리 파라미터로 전달하도록 변경했지만, 현재 코드에는 localStorage.removeItem('groupToken')이 여전히 남아 있습니다. 코드베이스 전체를 확인한 결과, localStorage.getItem('groupToken')은 어디에도 존재하지 않습니다.

groupToken은 URL 파라미터의 라우터 로더(groupTokenUrlLoader)에서만 추출되어 사용되므로, localStorage 저장소와 제거 로직은 더 이상 필요하지 않습니다. 이를 정리하기 위해 다음 두 곳의 localStorage.removeItem('groupToken') 호출을 제거하세요:

  • src/pages/expenseDetail/ExpenseDetailPage.tsx (라인 44)
  • src/pages/createExpenseStep/CreateExpenseStepPage.tsx (라인 53)

또한 그룹 생성 성공 시 저장하는 localStorage.setItem('groupToken', response?.groupToken) (src/features/group-creation/api/usePostCreateGroup.ts)도 제거해야 합니다.

src/pages/memberSetup/MemberSetupPage.tsx (1)

71-71: reverse()는 원본 배열을 변경하며, 매 렌더링마다 호출됩니다.

Array.prototype.reverse()는 원본 배열을 직접 변경(mutate)하고 매 렌더링마다 호출되어 멤버 순서가 계속 뒤집힙니다. toReversed() 또는 스프레드 연산자를 사용하세요.

-          members={data.members.reverse() || []}
+          members={[...data.members].reverse() || []}
src/mocks/handlers/group.ts (1)

97-112: request.json()을 await하지 않아 디버그 로그가 의도대로 동작하지 않습니다.

주석상 "body를 잘 전송했는지 확인용"이므로, handler를 async로 변경하고 await를 추가해야 합니다. 현재 상태에서는 console.log가 Promise 객체를 출력하게 됩니다.

-  http.post('/api/v1/group', ({ request }) => {
+  http.post('/api/v1/group', async ({ request }) => {
     const isMocked = request.headers.get('X-Mock-Request');
     if (!isMocked || isMocked !== 'true') return passthrough();
 
     const accessToken = request.headers.get('Authorization');
     if (!accessToken) {
       return HttpResponse.json({ error: 'Unauthorized' }, { status: 401 });
     }
     /** body를 잘 전송했는지 확인용 */
-    const body = request.json();
+    const body = await request.json();
     console.log('response body:', body);
src/features/expense-management/lib/useAddExpenseFormArray.ts (1)

27-49: groupData null 체크 누락으로 런타임 에러 발생 가능

defaultValues async 함수 내부(Line 39)에서 groupData.members에 직접 접근하고 있지만, Line 53의 useMemo와 달리 null 체크가 없습니다. 만약 loader가 예외 상황에서 groupData를 반환하지 않으면 Cannot read properties of undefined (reading 'members') 에러가 발생할 수 있습니다.

     defaultValues: async () => {
       // 기본 데이터가 있는 경우 (ex. 수정)
       if (initialExpense) {
         return {
           expenses: [initialExpense],
         };
       }
       // 기본 데이터가 없는 경우 (ex. 추가)
+      if (!groupData) {
+        return { expenses: [defaultValues] };
+      }
       return {
         expenses: [
           {
             ...defaultValues,
             memberExpenses: groupData.members.map((member) => ({
♻️ Duplicate comments (2)
src/pages/editExpenseStep/EditExpenseStepPage.styles.ts (1)

3-14: 동일한 코드 중복.

이 파일의 ExpenseFormListAddExpenseStepPage.styles.tsCreateExpenseStepPage.styles.ts에서도 동일하게 정의되어 있습니다. 이전 파일 리뷰에서 제안한 대로 공통 위치로 추출을 권장합니다.

src/pages/createExpenseStep/CreateExpenseStepPage.styles.ts (1)

3-14: 동일한 코드 중복.

이 파일의 ExpenseFormList도 앞서 리뷰한 두 파일과 동일합니다. 세 파일 모두에서 공통 스타일을 공유 위치로 추출하는 것을 권장합니다.

🧹 Nitpick comments (16)
src/pages/selectGroup/ui/GroupLinkButton.styles.ts (1)

4-15: 스타일 컴포넌트 구현이 적절합니다.

CreateGroupLinkButton.styles.ts와 유사한 구조를 가지고 있지만, 배경색과 패딩 값의 차이는 각 버튼의 시각적 위계를 구분하기 위한 의도적인 디자인으로 보입니다.

향후 버튼 스타일이 더 추가될 경우, 공통 속성을 추상화한 기본 LinkButton 컴포넌트를 만들고 variant prop으로 스타일을 구분하는 것을 고려해볼 수 있습니다.

src/pages/addExpenseStep/AddExpenseStepPage.styles.ts (1)

3-14: 코드 중복 제거를 고려해주세요.

ExpenseFormList 스타일 컴포넌트가 세 개의 파일에서 동일하게 정의되어 있습니다:

  • src/pages/addExpenseStep/AddExpenseStepPage.styles.ts
  • src/pages/createExpenseStep/CreateExpenseStepPage.styles.ts
  • src/pages/editExpenseStep/EditExpenseStepPage.styles.ts

공통 스타일을 src/shared/ui 또는 src/pages/shared/styles 같은 공유 위치로 추출하여 중복을 제거하는 것을 권장합니다.

다음과 같이 리팩터링할 수 있습니다:

새 파일 src/shared/ui/ExpenseFormList/index.styles.ts 생성:

import styled from 'styled-components';

export const ExpenseFormList = styled.form`
  display: flex;
  flex-direction: column;
  gap: ${({ theme }) => theme.unit[24]};
  width: 100%;
  flex: 1 0 0;
  overflow-y: auto;
  padding: ${({ theme }) =>
    `0 ${theme.unit[20]} ${theme.unit[24]} ${theme.unit[20]}`};
  background: ${({ theme }) =>
    theme.color.semantic.background.normal.alternative};
`;

각 페이지 스타일 파일에서:

export { ExpenseFormList } from '@/shared/ui/ExpenseFormList/index.styles';
src/pages/selectGroup/ui/CreateGroupLinkButton.styles.ts (1)

4-13: 코드 중복 가능성을 검토하세요.

GroupLinkButton.styles.ts에도 동일한 이름의 LinkButton 스타일 컴포넌트가 존재합니다. 두 컴포넌트는 배경색과 테두리만 다르고 나머지 스타일이 유사합니다. variant prop을 받는 단일 스타일 컴포넌트로 통합하는 것을 고려해보세요.

예시 통합 방식:

export const LinkButton = styled(Link)<{ variant?: 'primary' | 'default' }>`
  width: 100%;
  @media (min-width: 600px) {
    font-size: 22px;
  }
  background-color: ${({ theme, variant }) =>
    variant === 'primary'
      ? theme.color.semantic.orange.default
      : theme.color.semantic.background.normal.default};
  border-radius: ${({ theme }) => theme.radius.default};
  ${({ variant, theme }) =>
    variant !== 'primary' &&
    `border: 1px solid ${theme.color.semantic.border.default};`}
  padding: ${({ theme }) => `${theme.unit[16]} ${theme.unit[8]}`};
  cursor: pointer;
`;
src/pages/selectGroup/ui/CreateGroupLinkButton.tsx (1)

14-14: gap 값의 일관성을 확인하세요.

gap={4}는 숫자 리터럴을 직접 사용하고 있습니다. 다른 테마 값들과의 일관성을 위해 gap={theme.unit[4]} 또는 유사한 형태로 변경하는 것이 좋을 수 있습니다.

src/entities/auth/lib/kakaoLogin.ts (1)

12-14: 모든 쿼리 파라미터에 URL 인코딩 적용 권장

state 파라미터는 encodeURIComponent를 사용하고 있지만, client_idredirect_uri도 인코딩하는 것이 안전합니다. URLSearchParams를 사용하면 자동으로 인코딩됩니다.

 function kakaoLogin(url?: string) {
   const defaultRedirectUrl = window.location.origin;
   const redirectUrl = url || defaultRedirectUrl;

-  window.location.href = `https://kauth.kakao.com/oauth/authorize?client_id=${KAKAO_CLIENT_ID}&redirect_uri=${KAKAO_REDIRECT_URI}&response_type=code&state=${encodeURIComponent(
-    redirectUrl
-  )}`;
+  const params = new URLSearchParams({
+    client_id: KAKAO_CLIENT_ID,
+    redirect_uri: KAKAO_REDIRECT_URI,
+    response_type: 'code',
+    state: redirectUrl,
+  });
+  window.location.href = `https://kauth.kakao.com/oauth/authorize?${params.toString()}`;
 }
src/shared/api/queryClient.ts (1)

3-18: QueryCache 생성자를 통해 onError 핸들러를 설정하는 것을 권장합니다.

queryClient.getQueryCache().config.onError를 직접 수정하는 방식은 공식 문서에서 권장하지 않으며, 향후 버전에서 동작이 변경될 수 있습니다. QueryCacheMutationCache 생성자 옵션을 사용하세요.

-import { QueryClient } from '@tanstack/react-query';
+import { QueryClient, QueryCache, MutationCache } from '@tanstack/react-query';

-export const queryClient = new QueryClient();
-
-export const setupQueryClient = (
+export const createQueryClient = (
   handleQueryError: (error: Error) => void,
   handleMutationError: (error: Error) => void
-) => {
-  queryClient.setDefaultOptions({
-    mutations: {
-      onError: handleMutationError,
-      throwOnError: true, // 기본적으로 RouteErrorBoundary로 에러를 던집니다.
-    },
-    queries: {
-      throwOnError: true, // 기본적으로 RouteErrorBoundary로 에러를 던집니다.
+) =>
+  new QueryClient({
+    queryCache: new QueryCache({
+      onError: handleQueryError,
+    }),
+    mutationCache: new MutationCache({
+      onError: handleMutationError,
+    }),
+    defaultOptions: {
+      mutations: {
+        throwOnError: true,
+      },
+      queries: {
+        throwOnError: true,
+      },
     },
   });
-  queryClient.getQueryCache().config.onError = handleQueryError;
-};
src/pages/CreateExpensePage/lib/createExpensePageGuardLoader.ts (2)

24-27: 에러 메시지 추가 권장

throw new Error()에 메시지가 없으면 디버깅 시 원인 파악이 어렵습니다.

     // 모임 정보가 없으면 리다이렉트
     if (!groupData) {
-      throw new Error();
+      throw new Error('Group data not found');
     }

30-39: 에러 핸들링 로직 LGTM

401/404 에러 시 selectGroup으로 리다이렉트하고, 그 외 에러는 ErrorBoundary로 전파하는 구조가 적절합니다.

다만, BoundaryError에 원본 에러 정보를 포함하면 디버깅에 도움이 됩니다.

     // 그 외에는 에러를 그대로 던진다
-    throw new BoundaryError({});
+    throw new BoundaryError({ cause: error });
src/entities/group/api/groupQueries.ts (2)

5-16: 파라미터 기본값 설정으로 사용성 개선 권장

매 호출 시 errorHandlersignoreBoundaryErrors를 명시해야 하는 현재 구조는 사용이 번거롭습니다.

 export const useGetGroupList = (
-  errorHandlers: ErrorHandlers,
-  ignoreBoundaryErrors: IgnoreBoundaryErrors
+  errorHandlers: ErrorHandlers = {},
+  ignoreBoundaryErrors: IgnoreBoundaryErrors = []
 ) => {

이렇게 하면 useGetGroupList()로 간단히 호출할 수 있습니다.


18-30: groupToken 유효성에 따른 쿼리 활성화 제어 권장

groupToken이 빈 문자열이거나 없는 경우에도 쿼리가 실행될 수 있습니다.

 export const useGetGroupDetail = (
   groupToken: string,
-  errorHandlers: ErrorHandlers,
-  ignoreBoundaryErrors: IgnoreBoundaryErrors
+  errorHandlers: ErrorHandlers = {},
+  ignoreBoundaryErrors: IgnoreBoundaryErrors = []
 ) => {
   const query = useQueryWithHandlers({
     queryKey: ['groupDetail', groupToken],
     queryFn: () => getGroupDetail(groupToken),
     errorHandlers,
     ignoreBoundaryErrors,
+    enabled: !!groupToken,
   });
   return query;
 };
src/pages/selectGroup/SelectGroupPage.tsx (1)

17-19: groupList가 없을 때 사용자 피드백 개선 권장

groupListundefined인 경우 null을 반환하면 사용자에게 아무것도 표시되지 않습니다. 에러 상태나 재시도 옵션을 제공하는 것이 UX에 더 좋습니다.

   if (!groupList) {
-    return null;
+    return <div>데이터를 불러올 수 없습니다.</div>;
   }

또는 useGetGroupList에서 반환하는 isError, error 상태를 활용하여 에러 UI를 표시하는 것을 고려해 주세요.

src/entities/group/api/group.ts (2)

21-27: createGroup는 반환 타입을 명시하면 호출부 DX가 좋아집니다.
응답 shape를 이미 알고 있으니 Promise<{ groupToken: string }>로 고정하는 편이 안전합니다. (mock을 쓰는 중이면 useMock도 동일하게)

-export const createGroup = async (groupData: CreateGroupData) => {
+export const createGroup = async (
+  groupData: CreateGroupData
+): Promise<{ groupToken: string }> => {
   const response = await axiosInstance.post<{ groupToken: string }>(
     '/group',
     groupData
   );
   return response.data;
 };

36-49: query param은 문자열 보간 대신 params로 인코딩 보장하는 게 안전합니다.
groupToken에 특수문자가 포함되면 깨질 수 있어서 axios의 params를 권합니다.

 export const putGroupAccount = async ({
   accountData,
   groupToken,
 }: {
   accountData: AccountVariable;
   groupToken: string;
 }) => {
-  const response = await axiosInstance.put(
-    `/group/account?groupToken=${groupToken}`,
-    accountData
-  );
+  const response = await axiosInstance.put('/group/account', accountData, {
+    params: { groupToken },
+  });
   return response.data;
 };
@@
 export const getGroupHeader = (
   groupToken: string
 ): Promise<GroupHeaderResponse> => {
   return axiosInstance
-    .get(`/group/header?groupToken=${groupToken}`)
+    .get('/group/header', { params: { groupToken } })
     .then((res) => res.data);
 };
src/app/Router.tsx (2)

52-114: createBrowserRouter를 컴포넌트 외부로 이동 권장

createBrowserRouterAppRouter 함수 내부에서 호출되고 있어, 매 렌더링마다 새로운 router 인스턴스가 생성됩니다. 이는 불필요한 재생성과 잠재적인 상태 손실을 야기할 수 있습니다.

+const router = createBrowserRouter([
+  {
+    id: 'root',
+    // ... router config
+  },
+]);
+
 function AppRouter() {
-  const router = createBrowserRouter([
-    // ...
-  ]);
-
   return <RouterProvider router={router} />;
 }

58-60: 로딩 fallback UI 개선 고려

현재 <div>loading...</div>로 단순 처리되어 있습니다. 추후 디자인 시스템의 로딩 컴포넌트나 스켈레톤 UI로 교체하면 사용자 경험이 향상됩니다.

src/pages/CreateExpensePage/CreateExpensePage.tsx (1)

36-41: eslint-disable 주석 정리 고려

react/no-unstable-nested-components 경고를 억제하는 주석이 여러 곳에 반복됩니다. @use-funnel 라이브러리 사용 패턴상 필요할 수 있지만, 해당 규칙을 파일 레벨에서 비활성화하거나, step 컴포넌트들을 별도 변수로 추출하면 코드가 더 깔끔해질 수 있습니다.

파일 상단에서 규칙을 비활성화하는 방법:

/* eslint-disable react/no-unstable-nested-components */

Also applies to: 72-75

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 739f485 and b891c23.

📒 Files selected for processing (54)
  • .github/PULL_REQUEST_TEMPLATE.md (1 hunks)
  • src/app/App.tsx (1 hunks)
  • src/app/Router.tsx (3 hunks)
  • src/entities/auth/api/auth.ts (2 hunks)
  • src/entities/auth/lib/checkAuth.ts (1 hunks)
  • src/entities/auth/lib/getGroupManagerAuth.ts (0 hunks)
  • src/entities/auth/lib/kakaoLogin.ts (1 hunks)
  • src/entities/group/api/group.ts (1 hunks)
  • src/entities/group/api/groupQueries.ts (1 hunks)
  • src/entities/group/model/group.type.ts (1 hunks)
  • src/features/character-management/ui/CharacterBottomSheet/index.tsx (1 hunks)
  • src/features/expense-management/lib/createExpenseFunnel.type.ts (1 hunks)
  • src/features/expense-management/lib/useAddExpenseFormArray.ts (3 hunks)
  • src/features/expense-management/ui/FormCard/index.tsx (2 hunks)
  • src/features/group-creation/api/useGetGroupBasicInfo.ts (0 hunks)
  • src/features/group-creation/api/usePostCreateGroup.ts (2 hunks)
  • src/mocks/handlers/auth.ts (1 hunks)
  • src/mocks/handlers/group.ts (1 hunks)
  • src/pages/CreateExpensePage/CreateExpensePage.tsx (5 hunks)
  • src/pages/CreateExpensePage/index.ts (1 hunks)
  • src/pages/CreateExpensePage/lib/createExpensePageGuardLoader.ts (1 hunks)
  • src/pages/addExpenseStep/AddExpenseStepPage.styles.ts (1 hunks)
  • src/pages/addExpenseStep/AddExpenseStepPage.tsx (1 hunks)
  • src/pages/billDetail/index.ts (0 hunks)
  • src/pages/confirmStep/ConfirmStepPage.tsx (1 hunks)
  • src/pages/confirmStep/ui/ExpenseCard/index.tsx (1 hunks)
  • src/pages/confirmStep/ui/ExpenseCardList/index.tsx (1 hunks)
  • src/pages/createBill/index.ts (0 hunks)
  • src/pages/createExpenseStep/CreateExpenseStepPage.styles.ts (1 hunks)
  • src/pages/createExpenseStep/CreateExpenseStepPage.tsx (2 hunks)
  • src/pages/editExpenseStep/EditExpenseStepPage.styles.ts (1 hunks)
  • src/pages/editExpenseStep/EditExpenseStepPage.tsx (2 hunks)
  • src/pages/expenseDetail/ExpenseDetailPage.tsx (2 hunks)
  • src/pages/expenseDetail/index.ts (1 hunks)
  • src/pages/expenseDetail/ui/ExpenseMembers/index.tsx (1 hunks)
  • src/pages/login/LoginPage.tsx (2 hunks)
  • src/pages/memberSetup/MemberSetupPage.tsx (3 hunks)
  • src/pages/selectGroup/SelectGroupPage.styles.ts (0 hunks)
  • src/pages/selectGroup/SelectGroupPage.tsx (2 hunks)
  • src/pages/selectGroup/ui/CreateGroupLinkButton.styles.ts (1 hunks)
  • src/pages/selectGroup/ui/CreateGroupLinkButton.tsx (1 hunks)
  • src/pages/selectGroup/ui/EmptyBox.styles.ts (1 hunks)
  • src/pages/selectGroup/ui/EmptyBox.tsx (1 hunks)
  • src/pages/selectGroup/ui/GroupLinkButton.styles.ts (1 hunks)
  • src/pages/selectGroup/ui/GroupLinkButton.tsx (1 hunks)
  • src/pages/selectGroup/ui/index.ts (1 hunks)
  • src/pages/shareStep/ShareStepPage.tsx (1 hunks)
  • src/shared/api/queryClient.ts (1 hunks)
  • src/shared/config/route.ts (1 hunks)
  • src/shared/lib/generateShareLink.ts (1 hunks)
  • src/shared/ui/NameChip/index.stories.tsx (1 hunks)
  • src/shared/ui/NameChip/index.styles.ts (1 hunks)
  • src/shared/ui/NameChip/index.tsx (1 hunks)
  • src/shared/ui/NameChip/index.type.ts (1 hunks)
💤 Files with no reviewable changes (5)
  • src/pages/createBill/index.ts
  • src/entities/auth/lib/getGroupManagerAuth.ts
  • src/pages/billDetail/index.ts
  • src/features/group-creation/api/useGetGroupBasicInfo.ts
  • src/pages/selectGroup/SelectGroupPage.styles.ts
🧰 Additional context used
🧠 Learnings (4)
📚 Learning: 2025-09-21T08:03:27.190Z
Learnt from: yoouyeon
Repo: moddo-kr/moddo-frontend PR: 7
File: src/features/expense-management/ui/FormField/index.tsx:11-13
Timestamp: 2025-09-21T08:03:27.190Z
Learning: src/shared/ui/Button/index.tsx (Line 20), src/shared/ui/Text/index.tsx (Line 35), src/shared/ui/Input/index.tsx (Line 59) 파일들에는 모두 export default가 존재한다. default import 방식 (import Button from '@/shared/ui/Button' 등)이 올바르게 작동한다.

Applied to files:

  • src/pages/selectGroup/ui/CreateGroupLinkButton.tsx
  • src/pages/selectGroup/ui/index.ts
  • src/pages/CreateExpensePage/index.ts
  • src/shared/ui/NameChip/index.styles.ts
  • src/pages/expenseDetail/index.ts
  • src/pages/confirmStep/ConfirmStepPage.tsx
  • src/pages/selectGroup/ui/GroupLinkButton.tsx
  • src/pages/editExpenseStep/EditExpenseStepPage.tsx
  • src/pages/selectGroup/ui/GroupLinkButton.styles.ts
  • src/pages/selectGroup/ui/CreateGroupLinkButton.styles.ts
  • src/features/expense-management/ui/FormCard/index.tsx
  • src/pages/selectGroup/ui/EmptyBox.tsx
  • src/pages/login/LoginPage.tsx
📚 Learning: 2025-09-21T08:03:27.190Z
Learnt from: yoouyeon
Repo: moddo-kr/moddo-frontend PR: 7
File: src/features/expense-management/ui/FormField/index.tsx:11-13
Timestamp: 2025-09-21T08:03:27.190Z
Learning: src/shared/ui/Button/index.tsx, src/shared/ui/Text/index.tsx, src/shared/ui/Input/index.tsx 파일들에는 모두 export default가 존재한다. default import 방식 (import Button from '@/shared/ui/Button' 등)이 올바르게 작동한다.

Applied to files:

  • src/pages/selectGroup/ui/CreateGroupLinkButton.tsx
  • src/pages/selectGroup/ui/index.ts
  • src/pages/CreateExpensePage/index.ts
  • src/pages/expenseDetail/index.ts
  • src/pages/editExpenseStep/EditExpenseStepPage.tsx
  • src/pages/selectGroup/ui/GroupLinkButton.styles.ts
  • src/pages/selectGroup/ui/CreateGroupLinkButton.styles.ts
  • src/features/expense-management/ui/FormCard/index.tsx
  • src/pages/selectGroup/ui/EmptyBox.tsx
📚 Learning: 2025-09-21T07:58:28.422Z
Learnt from: yoouyeon
Repo: moddo-kr/moddo-frontend PR: 7
File: src/features/expense-management/ui/NumberInput/index.tsx:2-2
Timestamp: 2025-09-21T07:58:28.422Z
Learning: src/shared/ui/Text/index.tsx 파일에는 default export가 존재한다 (export default Text;). import Text from '@/shared/ui/Text' 구문이 올바르게 작동한다.

Applied to files:

  • src/pages/selectGroup/ui/index.ts
  • src/pages/CreateExpensePage/index.ts
  • src/pages/expenseDetail/index.ts
📚 Learning: 2025-09-21T07:58:28.422Z
Learnt from: yoouyeon
Repo: moddo-kr/moddo-frontend PR: 7
File: src/features/expense-management/ui/NumberInput/index.tsx:2-2
Timestamp: 2025-09-21T07:58:28.422Z
Learning: src/shared/ui/Text/index.tsx 파일에는 export default Text; 가 35번째 줄에 존재한다. import Text from '@/shared/ui/Text' 구문이 올바르게 작동한다.

Applied to files:

  • src/pages/selectGroup/ui/index.ts
  • src/pages/expenseDetail/index.ts
🧬 Code graph analysis (30)
src/pages/editExpenseStep/EditExpenseStepPage.styles.ts (2)
src/pages/addExpenseStep/AddExpenseStepPage.styles.ts (1)
  • ExpenseFormList (3-14)
src/pages/createExpenseStep/CreateExpenseStepPage.styles.ts (1)
  • ExpenseFormList (3-14)
src/pages/createExpenseStep/CreateExpenseStepPage.styles.ts (2)
src/pages/addExpenseStep/AddExpenseStepPage.styles.ts (1)
  • ExpenseFormList (3-14)
src/pages/editExpenseStep/EditExpenseStepPage.styles.ts (1)
  • ExpenseFormList (3-14)
src/entities/auth/lib/checkAuth.ts (2)
src/shared/api/queryClient.ts (1)
  • queryClient (3-3)
src/entities/auth/api/auth.ts (1)
  • getAuth (18-23)
src/entities/group/api/groupQueries.ts (1)
src/entities/group/api/group.ts (2)
  • getGroupList (9-12)
  • getGroupDetail (14-19)
src/shared/ui/NameChip/index.styles.ts (2)
src/shared/ui/NameChip/index.type.ts (2)
  • NameChipVariant (3-3)
  • NameChipSize (4-4)
src/shared/ui/Text/index.styles.tsx (1)
  • TextVariant (6-12)
src/features/character-management/ui/CharacterBottomSheet/index.tsx (1)
src/shared/config/route.ts (1)
  • ROUTE (3-12)
src/pages/confirmStep/ui/ExpenseCardList/index.tsx (2)
src/entities/expense/model/expense.type.ts (1)
  • Expense (36-48)
src/features/expense-management/lib/createExpenseFunnel.type.ts (1)
  • EditExpenseContext (3-6)
src/pages/confirmStep/ConfirmStepPage.tsx (1)
src/features/expense-management/lib/createExpenseFunnel.type.ts (1)
  • EditExpenseContext (3-6)
src/shared/ui/NameChip/index.tsx (1)
src/shared/ui/NameChip/index.type.ts (2)
  • NameChipVariant (3-3)
  • NameChipSize (4-4)
src/pages/memberSetup/MemberSetupPage.tsx (2)
src/entities/group/api/groupQueries.ts (1)
  • useGetGroupDetail (18-30)
src/shared/config/route.ts (1)
  • ROUTE (3-12)
src/shared/ui/NameChip/index.stories.tsx (1)
src/shared/ui/NameChip/index.type.ts (2)
  • nameChipVariants (1-1)
  • nameChipSizes (2-2)
src/entities/group/api/group.ts (1)
src/entities/group/model/group.type.ts (2)
  • Group (3-7)
  • CreateGroupData (14-17)
src/pages/addExpenseStep/AddExpenseStepPage.styles.ts (2)
src/pages/createExpenseStep/CreateExpenseStepPage.styles.ts (1)
  • ExpenseFormList (3-14)
src/pages/editExpenseStep/EditExpenseStepPage.styles.ts (1)
  • ExpenseFormList (3-14)
src/pages/selectGroup/ui/GroupLinkButton.tsx (3)
src/entities/group/model/group.type.ts (1)
  • Group (3-7)
src/shared/config/route.ts (1)
  • ROUTE (3-12)
src/shared/ui/Text/index.styles.tsx (1)
  • Text (20-25)
src/pages/editExpenseStep/EditExpenseStepPage.tsx (2)
src/features/expense-management/lib/createExpenseFunnel.type.ts (1)
  • EditExpenseContext (3-6)
src/features/expense-management/ui/FormCard/index.styles.ts (1)
  • FormCard (7-16)
src/pages/selectGroup/ui/GroupLinkButton.styles.ts (1)
src/pages/selectGroup/ui/CreateGroupLinkButton.styles.ts (1)
  • LinkButton (4-13)
src/features/expense-management/lib/createExpenseFunnel.type.ts (1)
src/entities/expense/model/expense.type.ts (1)
  • SingleExpenseForm (25-25)
src/pages/CreateExpensePage/CreateExpensePage.tsx (1)
src/features/expense-management/lib/createExpenseFunnel.type.ts (3)
  • ExpenseStepContext (8-12)
  • EditExpenseStepContext (14-18)
  • EditExpenseContext (3-6)
src/pages/selectGroup/ui/CreateGroupLinkButton.styles.ts (1)
src/pages/selectGroup/ui/GroupLinkButton.styles.ts (1)
  • LinkButton (4-15)
src/pages/CreateExpensePage/lib/createExpensePageGuardLoader.ts (2)
src/shared/config/route.ts (1)
  • ROUTE (3-12)
src/entities/group/api/group.ts (1)
  • getGroupDetail (14-19)
src/features/group-creation/api/usePostCreateGroup.ts (1)
src/entities/group/api/group.ts (1)
  • createGroup (21-27)
src/pages/selectGroup/SelectGroupPage.tsx (2)
src/entities/group/api/groupQueries.ts (1)
  • useGetGroupList (5-16)
src/pages/selectGroup/ui/EmptyBox.styles.ts (1)
  • EmptyBox (3-14)
src/app/App.tsx (1)
src/shared/api/queryClient.ts (1)
  • setupQueryClient (5-19)
src/pages/confirmStep/ui/ExpenseCard/index.tsx (2)
src/entities/expense/model/expense.type.ts (1)
  • Expense (36-48)
src/features/expense-management/lib/createExpenseFunnel.type.ts (1)
  • EditExpenseContext (3-6)
src/pages/shareStep/ShareStepPage.tsx (1)
src/shared/config/route.ts (1)
  • ROUTE (3-12)
src/mocks/handlers/group.ts (1)
src/entities/group/model/group.type.ts (1)
  • Group (3-7)
src/pages/selectGroup/ui/EmptyBox.tsx (2)
src/pages/selectGroup/ui/EmptyBox.styles.ts (1)
  • EmptyBox (3-14)
src/shared/ui/Text/index.styles.tsx (1)
  • Text (20-25)
src/app/Router.tsx (2)
src/entities/expense/model/expense.type.ts (1)
  • ExpenseDetail (62-68)
src/shared/config/route.ts (1)
  • ROUTE (3-12)
src/entities/auth/api/auth.ts (1)
public/mockServiceWorker.js (1)
  • response (125-125)
src/features/expense-management/lib/useAddExpenseFormArray.ts (1)
src/entities/group/model/group.type.ts (1)
  • Group (3-7)
🔇 Additional comments (42)
.github/PULL_REQUEST_TEMPLATE.md (1)

1-2: 좋은 도움말 추가입니다.

CodeRabbit의 자동 제목 생성 기능을 안내하는 주석이 명확하고 기여자들에게 도움이 될 것 같습니다. 템플릿 구조와 기존 내용에 영향을 주지 않으면서 유용한 정보를 추가했습니다.

src/features/expense-management/ui/FormCard/index.tsx (1)

11-12: 네이밍/용어 통일 방향은 좋습니다(ExpenseDatePicker로 교체).

@/shared/ui/DatePicker는 default export를 유지하며, DatePickerProps 인터페이스가 react-datepicker v7.6.0의 기본 prop 시그니처(selected?: Date | null, onChange?: (date: Date | null) => void)와 정확히 일치합니다. 폼의 date 필드는 항상 'yyyy-MM-dd' 형식의 유효한 문자열로 초기화되므로 런타임 에러 리스크도 없습니다.

src/pages/expenseDetail/index.ts (1)

1-1: LGTM!

표준 barrel export 패턴을 따르고 있으며, ExpenseDetailPage를 명확하게 노출합니다.

src/features/character-management/ui/CharacterBottomSheet/index.tsx (1)

56-56: 라우트 업데이트가 올바릅니다.

bill→expense 용어 표준화에 맞춰 ROUTE.characterShare로 정확하게 변경되었습니다.

src/pages/createExpenseStep/CreateExpenseStepPage.tsx (1)

95-108: LGTM!

BillFormList에서 ExpenseFormList로의 리네이밍이 전체 bill→expense 용어 표준화와 일관성 있게 적용되었습니다.

src/pages/expenseDetail/ui/ExpenseMembers/index.tsx (1)

2-2: LGTM!

디렉토리 구조 변경에 맞춰 import 경로가 정확하게 업데이트되었습니다.

src/pages/confirmStep/ui/ExpenseCardList/index.tsx (1)

4-11: LGTM!

EditBillContext에서 EditExpenseContext로의 타입 변경이 import와 prop signature 모두에 일관되게 적용되었습니다.

src/features/group-creation/api/usePostCreateGroup.ts (1)

2-11: LGTM!

group API의 리팩터링이 올바르게 반영되었습니다. 객체 메서드에서 독립 함수로의 변경은 테스트 가능성과 코드 명확성을 향상시킵니다.

src/pages/shareStep/ShareStepPage.tsx (1)

60-60: LGTM!

정산 내역 확인 버튼의 라우트가 ROUTE.expenseDetail로 정확하게 업데이트되었습니다.

src/mocks/handlers/auth.ts (1)

14-24: LGTM!

기존 guest token 핸들러와 동일한 패턴을 따르고 있으며, 인증 체크 mock 응답 구조도 적절합니다.

src/pages/login/LoginPage.tsx (1)

13-13: LGTM!

카카오 로그인 기능이 적절하게 통합되었습니다. 기존 placeholder를 실제 kakaoLogin() 호출로 교체하여 OAuth 플로우가 정상적으로 작동할 것으로 보입니다.

Also applies to: 24-24

src/pages/confirmStep/ui/ExpenseCard/index.tsx (1)

7-7: LGTM!

EditBillContext에서 EditExpenseContext로의 타입 변경이 프로젝트 전반의 Bill→Expense 용어 통일과 일치합니다. 타입 이름만 변경되었고 로직은 동일하게 유지됩니다.

Also applies to: 13-13

src/pages/selectGroup/ui/EmptyBox.tsx (1)

1-29: LGTM!

간단하고 명확한 empty state 컴포넌트입니다. 기존 UI 패턴을 잘 따르고 있으며 구현이 적절합니다.

src/entities/group/model/group.type.ts (1)

4-4: Group 인터페이스의 id: string 필수 속성 추가는 코드베이스 전체에서 올바르게 반영되었습니다.

검증 결과:

  • 모든 mock 데이터(src/mocks/handlers/group.ts)에서 Group 객체가 id 필드를 포함합니다.
  • SelectGroupPage에서 key={group.id}로 id 필드를 사용하고 있습니다.
  • GroupLinkButton에서 const { groupName, members, id: groupToken } = group;으로 id를 정상적으로 구조분해합니다.
  • getGroupList, getGroupDetail API 함수들이 반환하는 데이터는 모두 id 필드를 포함합니다.
  • breaking change에 해당하는 불완전한 Group 객체 인스턴스는 발견되지 않았습니다.
src/pages/editExpenseStep/EditExpenseStepPage.tsx (1)

11-11: 일관된 리팩터링이 잘 적용되었습니다.

Bill → Expense로의 용어 변경이 타입 정의(EditExpenseContext)와 UI 컴포넌트(ExpenseFormList)에 일관되게 적용되었습니다.

Also applies to: 19-19, 82-86

src/pages/CreateExpensePage/index.ts (1)

1-1: LGTM!

표준 배럴 패턴이 올바르게 적용되었습니다.

src/pages/expenseDetail/ExpenseDetailPage.tsx (1)

22-22: 컴포넌트 이름 변경이 일관되게 적용되었습니다.

BillDetailPage에서 ExpenseDetailPage로의 리네이밍이 올바르게 적용되었습니다.

Also applies to: 109-109

src/shared/lib/generateShareLink.ts (1)

5-5: 라우트 업데이트가 올바르게 적용되었습니다.

ROUTE.billDetail에서 ROUTE.expenseDetail로의 변경이 일관된 리팩터링의 일부로 적절하게 적용되었습니다.

src/pages/selectGroup/ui/CreateGroupLinkButton.tsx (1)

7-23: 컴포넌트 구현이 잘 되었습니다.

접근성을 고려한 aria-label 속성이 포함되어 있고, 테마 기반 스타일링이 일관되게 적용되었습니다.

src/features/expense-management/lib/createExpenseFunnel.type.ts (1)

3-18: 타입 리네이밍이 일관되게 적용되었습니다.

EditBillContextEditExpenseContext, BillStepContextExpenseStepContext, EditBillStepContextEditExpenseStepContext로의 변경이 타입 구조 변경 없이 일관되게 적용되었습니다.

src/entities/auth/api/auth.ts (1)

18-23: 새로운 인증 확인 함수가 추가되었습니다.

getAuth 함수가 추가되어 사용자 인증 상태를 확인할 수 있게 되었습니다.

Line 20의 useMock: true 플래그가 의도적인지 확인해주세요. 이 설정이 프로덕션 환경에서도 유지되어야 하는지, 아니면 개발 중 임시로 사용하는 것인지 명확히 해주세요.

src/pages/selectGroup/ui/EmptyBox.styles.ts (1)

3-14: 미디어 쿼리 내 font-size에 대한 기본값 누락 가능성

@media (min-width: 600px)에서만 font-size: 22px가 설정되어 있고, 600px 미만 화면에서는 기본 font-size가 정의되어 있지 않습니다. 의도된 동작인지 확인해 주세요. 작은 화면에서도 일관된 폰트 크기가 필요하다면 기본값을 추가하는 것이 좋습니다.

 export const EmptyBox = styled.div`
   width: 100%;
+  font-size: 18px; // 또는 적절한 기본값
   @media (min-width: 600px) {
     font-size: 22px;
   }
src/pages/addExpenseStep/AddExpenseStepPage.tsx (1)

49-53: LGTM!

PR 목표에 맞게 BillFormList에서 ExpenseFormList로 일관되게 변경되었습니다.

src/pages/selectGroup/ui/index.ts (1)

1-3: LGTM!

Barrel export 패턴이 깔끔하게 적용되었습니다. CreateGroupLinkButton, EmptyBox, GroupLinkButton 컴포넌트를 일관된 방식으로 export하고 있습니다.

src/shared/ui/NameChip/index.styles.ts (1)

10-36: LGTM!

테마 토큰과 TextVariant 헬퍼를 활용한 깔끔한 구현입니다. $variant$size에 따른 스타일 분기가 명확합니다.

기본 variant(inherit 반환)가 의도된 동작인지만 확인해 주세요. 부모 스타일을 상속받는 유연한 사용을 위한 것이라면 문제없습니다.

src/pages/memberSetup/MemberSetupPage.tsx (2)

30-43: 에러 핸들러 구현이 적절합니다.

403 에러 핸들링과 BoundaryError 사용이 잘 구현되어 있습니다. NOTE 주석에서 실제 API가 500을 반환한다고 언급하고 있는데, API가 수정되면 해당 주석을 업데이트하거나 제거해 주세요.


78-80: LGTM!

generatePath를 사용한 동적 라우트 생성이 올바르게 구현되었습니다.

src/shared/ui/NameChip/index.stories.tsx (1)

1-76: LGTM!

Storybook 스토리 파일이 잘 구성되어 있습니다. Showcase 스토리가 모든 variant와 size 조합을 테이블로 보여주어 컴포넌트 문서화에 유용합니다.

src/shared/ui/NameChip/index.tsx (1)

1-18: LGTM!

NameChip 컴포넌트가 깔끔하게 구현되었습니다. styled-components의 transient props 패턴($variant, $size)을 올바르게 사용하고 있으며, 타입 정의와 기본값 설정이 적절합니다.

src/pages/selectGroup/ui/GroupLinkButton.tsx (2)

9-12: group.idgroupToken으로 사용하는 것이 의도된 것인지 확인해 주세요.

Group 타입에는 idgroupName 필드가 있지만, 라우트 파라미터는 groupToken을 기대합니다. idgroupToken으로 별칭 지정하는 것이 의도된 것이라면, Group 타입에 token 필드를 추가하거나 주석을 추가하여 이 매핑이 명확하도록 하는 것을 고려해 주세요.


16-22: LGTM!

members 배열에 대한 optional chaining과 조건부 variant 로직이 적절합니다.

src/pages/confirmStep/ConfirmStepPage.tsx (1)

9-9: LGTM!

Bill → Expense 용어 통일 작업의 일환으로 타입명 변경이 올바르게 적용되었습니다. EditExpenseContext 타입 정의와 일치합니다.

Also applies to: 17-17

src/pages/selectGroup/SelectGroupPage.tsx (1)

55-64: 데이터 기반 렌더링 LGTM

CreateGroupLinkButton과 조건부 GroupLinkButton / EmptyBox 렌더링 구조가 깔끔합니다. 기존 상태 기반 UI에서 데이터 기반으로 잘 리팩터링되었습니다.

src/app/App.tsx (1)

16-17: useApiError 호출 방식으로 인한 불필요한 의존성 재생성 제거

useApiErrorhandleError는 이미 useCallback으로 메모이제이션되어 있습니다. 다만, 매 렌더링마다 빈 객체 {}를 새로 전달하면 handlers 의존성이 변경되어 handleError 참조가 계속 갱신됩니다.

useApiError를 컴포넌트 외부에서 호출하거나 useMemo로 감싸서 인수를 안정화하세요:

const errorHandlers = useMemo(() => ({}), []);
const { handleError: handleQueryError } = useApiError(errorHandlers);
const { handleError: handleMutationError } = useApiError(errorHandlers);

또는 errorHandlers 인수를 생략하는 경우 기본값을 안정화하는 방안을 고려하세요.

src/shared/ui/NameChip/index.type.ts (1)

1-4: 타입/상수 조합 패턴이 깔끔합니다.
as const + union 타입 추출로 variant/size가 타입 안정적으로 고정되어서, 컴포넌트/스타일/스토리에서 오타 방지에 도움이 됩니다.

src/mocks/handlers/group.ts (1)

5-66: dummyGroups의 멤버 필드/ID 타입이 Member 정의와 일치하는지 확인 필요.
dummyGroups: Group[]로 강타입을 준 건 좋은데, members[].idnumber(1,2,3…)로 들어가 있어서 Member.idstring인 설계라면 곧바로 타입/런타임 불일치가 납니다. src/entities/group/model/group.type.tsMember 인터페이스와 정합성만 한 번 확인해주세요.

src/entities/group/api/group.ts (1)

9-12: mock 사용 흐름이면 getGroupDetailuseMock 전달을 고려해주세요.
현재 MSW handler 쪽은 getIsMocked(request)로 passthrough를 결정하는데, getGroupDetail()에는 useMock이 없어 로컬에서 실서버로 튈 가능성이 있어 보여요(의도라면 OK).

 export const getGroupDetail = async (groupToken: string): Promise<Group> => {
   const response = await axiosInstance.get('/group', {
     params: { groupToken },
+    useMock: true,
   });
   return response.data;
 };

Also applies to: 14-19

src/entities/auth/lib/checkAuth.ts (1)

9-28: 가드 로직을 “토큰 존재”에서 “유저 조회”로 바꾼 방향은 좋습니다.
다만 ensureQueryData 옵션(gcTime 포함)과 redirect() 반환 패턴이 현재 사용 중인 TanStack Query / react-router 버전 조합에서 의도대로 동작하는지(특히 loader/guard 호출 컨텍스트)만 확인 부탁드립니다.

src/shared/config/route.ts (1)

7-11: 라우트 키 rename이 완벽하게 완료되었습니다.

제거된 ROUTE.createBill, ROUTE.billDetail, ROUTE.billDetailCharacterShare 참조는 남아있지 않으며, 구 경로인 /create-bill, /bill-detail 하드코딩도 확인되지 않습니다. 모든 ROUTE 참조가 새로운 키(expenseDetail, createExpense, characterShare 등)로 일관되게 마이그레이션되었으므로 별도 조치 불필요합니다.

src/features/expense-management/lib/useAddExpenseFormArray.ts (1)

73-78: LGTM!

groupInfogroupData로 매핑하여 반환하는 방식은 기존 API와의 하위 호환성을 유지하면서 loader 기반 데이터로 전환한 좋은 접근입니다.

src/app/Router.tsx (1)

97-107: expenseDetailcharacterShare 라우트가 protected 블록 외부에 위치

TODO 주석에 명시된 대로, 현재 이 라우트들은 checkAuth 보호 없이 groupTokenUrlLoader만 사용하고 있습니다. 의도된 동작인지 확인이 필요합니다. 인증이 필요한 페이지라면 protected 블록 내부로 이동해야 합니다.

src/pages/CreateExpensePage/CreateExpensePage.tsx (1)

1-6: LGTM!

Bill에서 Expense로의 명명 변경이 일관성 있게 적용되었습니다. useFunnel 제네릭 타입, funnel id, 그리고 컨텍스트 타입들이 모두 올바르게 업데이트되었습니다.

Also applies to: 15-32

@yoouyeon yoouyeon linked an issue Dec 26, 2025 that may be closed by this pull request
4 tasks
QueryClient 설정을 렌더 이후에 주입하던 방식을 제거
#10 (comment) 리뷰 반영
- 모듈 로드 시점 에러로 인한 앱 전체 크래시를 방지하기 위함
- #10 (comment) 리뷰 반영
@github-actions
Copy link

Storybook preview : https://6867dc9b2bbd1b3d76b92f4a-ifsdirkcpi.chromatic.com/

@moddo-kr moddo-kr deleted a comment from github-actions bot Dec 28, 2025
@yoouyeon yoouyeon marked this pull request as ready for review February 16, 2026 02:53
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

🤖 Fix all issues with AI agents
In `@src/pages/selectGroup/SelectGroupPage.tsx`:
- Around line 10-18: The component currently returns null when groupList is
undefined and isLoading is false, causing a blank screen on fetch failures;
update SelectGroupPage to destructure isError and error from useGetGroupList
(e.g., const { data: groupList, isLoading, isError, error } =
useGetGroupList(...)), and render an error UI or message when isError is true
(or when error exists) instead of returning null — include a user-friendly
message and optionally retry controls so failures are visible and recoverable.

@moddo-kr moddo-kr deleted a comment from coderabbitai bot Feb 17, 2026
@moddo-kr moddo-kr deleted a comment from coderabbitai bot Feb 17, 2026
- axios instance 기본 설정에서 환경변수가 없을 때는 상대경로로 요청을 보내도록 설정
- storybook 환경에서 server url 환경변수를 빈 문자열로 설정
- story들의 msw 모킹 url 수정
@github-actions
Copy link

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 (4)
src/pages/expenseDetail/ui/ExpenseTimeHeader/index.stories.tsx (1)

16-16: QueryClient가 모듈 스코프에서 생성되어 스토리 간 캐시가 공유됩니다.

현재 변경 사항과 직접 관련은 없지만, QueryClient를 decorator 내부에서 생성하면 스토리 간 쿼리 캐시 오염을 방지할 수 있습니다.

♻️ 제안: decorator 내부에서 QueryClient 생성
-const queryClient = new QueryClient();
-
 const meta: Meta<typeof ExpenseTimeHeader> = {
   // ...
   decorators: [
     (Story) => {
+      const queryClient = new QueryClient();
       const mockRouter = createMemoryRouter([
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/expenseDetail/ui/ExpenseTimeHeader/index.stories.tsx` at line 16,
The QueryClient is being created at module scope (const queryClient = new
QueryClient()) which causes cache sharing across stories; remove the
module-level QueryClient and instead instantiate a new QueryClient inside the
Storybook decorator (e.g., within the decorator function that wraps the Story)
so each story gets its own QueryClient instance; update any providers or
wrappers that reference queryClient to use the locally created instance inside
the decorator to avoid inter-story cache pollution.
src/shared/api/axios.ts (1)

13-13: Authorization 헤더에 문자열 "null"이 전송될 수 있습니다.

localStorage.getItem('accessToken')null을 반환하면 템플릿 리터럴에 의해 "null" 문자열이 됩니다. 요청 인터셉터(Line 22-25)는 accessToken이 truthy일 때만 덮어쓰므로, 토큰이 없는 상태에서는 Authorization: null 헤더가 그대로 전송됩니다.

이번 PR의 변경 범위는 아니지만, 향후 수정을 권장합니다.

♻️ 제안
   headers: {
     'Content-Type': 'application/json',
-    Authorization: `${localStorage.getItem('accessToken')}`,
+    ...(localStorage.getItem('accessToken') && {
+      Authorization: localStorage.getItem('accessToken')!,
+    }),
   },

또는 초기 헤더에서 Authorization을 제거하고 인터셉터에서만 설정하는 방식도 고려할 수 있습니다.

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

In `@src/shared/api/axios.ts` at line 13, 현재 axios 인스턴스의 기본 헤더에 Authorization:
`${localStorage.getItem('accessToken')}`를 설정하면 accessToken이 null일 때 문자열 "null"이
전송됩니다; 수정 방법은 기본 헤더에서 Authorization을 제거하거나 빈값 대신 조건부로 설정하고(예: only set when
localStorage.getItem('accessToken') is truthy) 요청 인터셉터(request interceptor used
around lines 22-25)에서만 Authorization 헤더를 설정하도록 변경하는 것입니다 — 즉, remove the static
Authorization entry in the axios.create/default headers and ensure the
interceptor (the function that currently overwrites headers at lines ~22-25)
sets headers.authorization only when a real token exists.
src/pages/selectGroup/SelectGroupPage.stories.tsx (2)

16-16: QueryClient가 모듈 레벨에서 생성되어 스토리 간 캐시가 공유됩니다.

여러 스토리가 추가될 경우 테스트 간 데이터가 오염될 수 있습니다. 데코레이터 내부에서 생성하거나 staleTime: 0 등의 옵션을 고려해 보세요. 현재 스토리가 하나뿐이라 당장 문제는 없지만 참고 부탁드립니다.

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

In `@src/pages/selectGroup/SelectGroupPage.stories.tsx` at line 16, 현재 모듈 레벨에 생성된
QueryClient (const queryClient = new QueryClient()) 때문에 스토리 간 캐시가 공유되어 테스트 오염이
발생할 수 있습니다; 이를 해결하려면 모듈 레벨 인스턴스 대신 스토리 데코레이터나 각 스토리 렌더 시점에 새로운 QueryClient를
생성하고(예: 데코레이터 내부에서 new QueryClient(...)) QueryClientProvider로 감싸 사용하거나, 불변성을
확보하기 위해 new QueryClient({ defaultOptions: { queries: { staleTime: 0 } } })처럼 기본
옵션을 설정해 캐시 유효성을 즉시 만료시키도록 변경하세요; 관련 식별자는 queryClient, QueryClient 및
QueryClientProvider입니다.

26-26: 디버그용 console.log가 남아 있습니다.

머지 전에 제거해 주세요.

♻️ 수정 제안
        http.get('/api/v1/groups', () => {
-          console.log('msw handler called');
          const result: Group[] = [
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/selectGroup/SelectGroupPage.stories.tsx` at line 26, Remove the
leftover debug console.log call in the msw handler inside
SelectGroupPage.stories.tsx; locate the MSW request handler or mock setup where
"console.log('msw handler called')" is present and delete that statement (no
replacement needed), keeping the rest of the handler and story definitions
unchanged.
🤖 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/expenseDetail/ui/ExpenseTimeHeader/index.stories.tsx`:
- Line 16: The QueryClient is being created at module scope (const queryClient =
new QueryClient()) which causes cache sharing across stories; remove the
module-level QueryClient and instead instantiate a new QueryClient inside the
Storybook decorator (e.g., within the decorator function that wraps the Story)
so each story gets its own QueryClient instance; update any providers or
wrappers that reference queryClient to use the locally created instance inside
the decorator to avoid inter-story cache pollution.

In `@src/pages/selectGroup/SelectGroupPage.stories.tsx`:
- Line 16: 현재 모듈 레벨에 생성된 QueryClient (const queryClient = new QueryClient()) 때문에
스토리 간 캐시가 공유되어 테스트 오염이 발생할 수 있습니다; 이를 해결하려면 모듈 레벨 인스턴스 대신 스토리 데코레이터나 각 스토리 렌더
시점에 새로운 QueryClient를 생성하고(예: 데코레이터 내부에서 new QueryClient(...))
QueryClientProvider로 감싸 사용하거나, 불변성을 확보하기 위해 new QueryClient({ defaultOptions: {
queries: { staleTime: 0 } } })처럼 기본 옵션을 설정해 캐시 유효성을 즉시 만료시키도록 변경하세요; 관련 식별자는
queryClient, QueryClient 및 QueryClientProvider입니다.
- Line 26: Remove the leftover debug console.log call in the msw handler inside
SelectGroupPage.stories.tsx; locate the MSW request handler or mock setup where
"console.log('msw handler called')" is present and delete that statement (no
replacement needed), keeping the rest of the handler and story definitions
unchanged.

In `@src/shared/api/axios.ts`:
- Line 13: 현재 axios 인스턴스의 기본 헤더에 Authorization:
`${localStorage.getItem('accessToken')}`를 설정하면 accessToken이 null일 때 문자열 "null"이
전송됩니다; 수정 방법은 기본 헤더에서 Authorization을 제거하거나 빈값 대신 조건부로 설정하고(예: only set when
localStorage.getItem('accessToken') is truthy) 요청 인터셉터(request interceptor used
around lines 22-25)에서만 Authorization 헤더를 설정하도록 변경하는 것입니다 — 즉, remove the static
Authorization entry in the axios.create/default headers and ensure the
interceptor (the function that currently overwrites headers at lines ~22-25)
sets headers.authorization only when a real token exists.

@yoouyeon yoouyeon merged commit d5ebf43 into develop Feb 17, 2026
2 checks passed
@yoouyeon yoouyeon deleted the feat/9-add-meeting-settlement branch February 17, 2026 12:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

✨ Feature 신규 기능 추가

Projects

None yet

Development

Successfully merging this pull request may close these issues.

기존 모임에서 정산 생성 기능 추가

2 participants