Skip to content

Comments

feat: 홈페이지 레이아웃 및 리스트 페이지 #155

Merged
Seojunhwan merged 12 commits intomainfrom
feat/main-layout
Aug 21, 2025
Merged

feat: 홈페이지 레이아웃 및 리스트 페이지 #155
Seojunhwan merged 12 commits intomainfrom
feat/main-layout

Conversation

@Seojunhwan
Copy link
Member

@Seojunhwan Seojunhwan commented Aug 21, 2025

✅ 이슈 번호

close x


🪄 작업 내용 (변경 사항)

  • 홈페이지 레이아웃 변경
  • 리스트 페이지 내용 변경 및 디자인 변경

📸 스크린샷

81498

💡 설명


🗣️ 리뷰어에게 전달 사항

덕분에 편하게 만들었어유,,

📍 트러블 슈팅

Summary by CodeRabbit

  • 신기능

    • 홈에 매장 리스트 섹션 추가: 카테고리 선택과 칩 필터(분위기/편의/지역)로 매장 필터링 지원, 매장 목록을 카테고리 기반으로 지연 로드.
    • 매장 목록 썸네일을 단일 이미지로 간소화.
    • 최근 응원 카드에 태그 배지 표시(최대 2개 +N) 및 응원 데이터에 다중 이미지(images)와 tags 필드 추가, CTA 라벨을 ‘응원 전체보기’로 변경.
  • 스타일

    • 최근 응원 카드 높이 유동화(최소/최대, 오버플로우 숨김), 카드 이미지 object-fit 적용 및 이미지 테두리 반경 일관화, 카테고리 영역 하단 보더 추가.
  • 기타

    • 기존 정적 카테고리 상수 삭제.

@vercel
Copy link
Contributor

vercel bot commented Aug 21, 2025

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

Project Deployment Preview Comments Updated (UTC)
26th-web-team-1-fe Ready Ready Preview Comment Aug 21, 2025 2:55pm

@coderabbitai
Copy link

coderabbitai bot commented Aug 21, 2025

Walkthrough

타입 변경(cheer: imageUrl → images[], tags 추가), 상점 API·쿼리에 tag·location 필터 도입, RecentCheers 컴포넌트의 태그 표시 및 이미지 소스 변경, 홈에 신규 StoreList 섹션 추가, 스토어 리스트 필터링 로직 적용 및 이미지 렌더 단순화, 일부 스타일 조정 및 정적 상수 삭제.

Changes

Cohort / File(s) Summary
Cheer 타입 확장
src/app/(home)/_api/cheer/cheer.types.ts
Cheer에서 imageUrl 제거, images: ImageResponse[]tags: string[] 추가; ImageResponse 임포트 추가.
Shop API & Queries 필터 추가
src/app/(home)/_api/shop/shop.api.ts, src/app/(home)/_api/shop/shop.queries.ts
getStores와 쿼리 옵션에 tag?: string[], location?: string[] 파라미터 추가; 배열을 CSV로 직렬화해 쿼리파라미터로 전송; 쿼리 키에 tag·location 포함.
RecentCheers UI 업데이트
src/app/(home)/_components/RecentCheers/RecentCheers.tsx, .../RecentCheers.css.ts
다중 태그 렌더링(최대 2개 + 초과 카운트), cheer 이미지 소스 images[0]?.url 사용, CTA 라벨/링크 일부 변경, 이미지 objectFit CSS로 이동, 카드 높이 동적화·오버플로우 클리핑.
홈 StoreList 섹션 추가
src/app/(home)/_components/StoreList/StoreList.tsx, .../StoreList/index.ts, src/app/(home)/_components/index.ts, src/app/(home)/page.tsx
신규 StoreList 컴포넌트 추가 및 배럴/공개 export, 홈 페이지에 StoreList 렌더링 및 간격/데코 스페이서 조정.
스토어 리스트 필터링 및 스타일 변경
src/app/(store)/stores/(list)/_components/StoreList/StoreList.tsx, .../StoreList/StoreList.css.ts
useChipFilter 기반 필터 적용해 쿼리 옵션(tag·location 포함) 확장, 이미지 캐러셀 제거·단일 썸네일로 단순화, wrapper 및 relative 포지셔닝 추가, 코너 반경 단순화, 일부 export 제거.
정적 상수 삭제
src/app/(store)/constants/storeCategory.constants.ts
STORE_CATEGORIES 파일 삭제(정적 카테고리 배열 제거).
FoodCategory 스타일 업데이트
src/components/ui/FoodCategory/FoodCategory.css.ts
컨테이너에 하단 보더(borderBottom) 추가.

Sequence Diagram(s)

sequenceDiagram
  actor User
  participant UI as StoreList (홈)
  participant Query as storesQueryOptions
  participant API as getStores
  participant Server as HTTP Server

  User->>UI: 카테고리/필터 선택
  UI->>Query: queryKey(size, category, tag[], location[])
  Query->>API: getStores({ size, category, tag, location })
  API->>Server: GET /shops?size=&category=&tag=csv&location=csv
  Server-->>API: StoresResponse
  API-->>Query: StoresResponse
  Query-->>UI: 데이터 렌더
Loading
sequenceDiagram
  participant RecentCheers as RecentCheers
  participant Data as Cheer
  participant Tags as ALL_TAGS

  Data-->>RecentCheers: { images[], tags[] }
  RecentCheers->>Tags: 태그 이름 매칭(ALL_TAGS)
  Tags-->>RecentCheers: 선택된 태그(최대 2 + 나머지 수)
  Note over RecentCheers: 첫 이미지(url) 사용해 썸네일 표시
Loading
sequenceDiagram
  participant StoreListComp as StoreList (스토어 영역)
  participant Filter as useChipFilter
  participant Query as storesQueryOptions

  StoreListComp->>Filter: 선택 필터 읽기
  Filter-->>StoreListComp: { tag[], location[], ... }
  StoreListComp->>Query: options({ tag, location, category })
  Note over StoreListComp: 이미지 1장만 표시, 더보기 제거
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested labels

✨ feature

Poem

토끼가 깡총: 태그를 묶고 사진들을 늘어놓네,
필터는 소곤, 리스트는 쫑쫑 걸음,
홈에 새 섹션 반짝이고,
응원엔 다중 사진·태그 춤추네,
코드는 당근 향기, 개발은 한 입 🥕

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/main-layout

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@github-actions
Copy link

github-actions bot commented Aug 21, 2025

🎨 Storybook Preview: https://685a32a1c0bbd269fdb67af4-qnybxncrjs.chromatic.com/
🔗 Chromatic Build: https://www.chromatic.com/build?appId=685a32a1c0bbd269fdb67af4&number=243
🕖 Updated at: 2025년 08월 21일 23시 54분 13초

wkdtnqls0506
wkdtnqls0506 previously approved these changes Aug 21, 2025
Copy link
Contributor

@wkdtnqls0506 wkdtnqls0506 left a comment

Choose a reason for hiding this comment

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

ㅋㅋㅋㅋㅋㅋㅋㅋㅋ빠르시다!! 저는 준환님 덕분에 FilterBottomSheet 편안히 썼슴다 ㅎㅎ
수고하셨어요~!!

Comment on lines +5 to +6
import { at, chunk, compact } from "es-toolkit";
import { isEmpty } from "es-toolkit/compat";
Copy link
Contributor

Choose a reason for hiding this comment

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

신기하고 편리한 것들이 정말 많군유 @@

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

🧹 Nitpick comments (19)
src/components/ui/FoodCategory/FoodCategory.css.ts (1)

11-12: 하단 보더 추가 LGTM. 다만 의미 토큰 사용과 스크롤바 숨김의 크로스 브라우저 보완을 제안합니다.

  • 디자인 토큰 일관성: colors.coolNeutral 대신 semantic.border.grayLight 사용이 더 일관적입니다.
  • 스크롤바 숨김: WebKit 전용 처리만 있어 Firefox/구형 Edge(Trident)에서 스크롤바가 노출될 수 있습니다.

다음 diff로 테마 일관성을 맞출 수 있습니다(선택 사항):

-  borderBottom: `1px solid ${colors.coolNeutral[97]}`,
+  borderBottom: `1px solid ${semantic.border.grayLight}`,

선택적으로, 아래 속성들을 container 스타일에 추가해 크로스 브라우저 스크롤바 숨김을 보완하세요(선택 사항):

// container 스타일 객체 상단 레벨에 추가
msOverflowStyle: "none", // IE/Legacy Edge
scrollbarWidth: "none",  // Firefox
src/app/(home)/_components/RecentCheers/RecentCheers.css.ts (1)

63-63: objectFit: cover 적용 LGTM

썸네일 크롭 일관성 향상에 적절합니다. 필요 시 중앙 크롭을 보장하려면 objectPosition: "center" 추가를 고려해도 됩니다(선택).

src/app/(home)/_api/shop/shop.api.ts (2)

21-23: toCSV 중복 호출 제거로 가독성/미세 성능 개선

같은 값에 대해 toCSV를 두 번 호출하고 있습니다. 한 번만 계산해 재사용하면 명확합니다.

-  const toCSV = (v?: string | string[]) =>
-    Array.isArray(v) ? v.filter(Boolean).join(",") : v || undefined;
+  const toCSV = (v?: string | string[]) =>
+    Array.isArray(v) ? v.filter(Boolean).join(",") : v || undefined;

   return await http
     .get("api/shops", {
       searchParams: {
         size,
         ...(category ? { category } : {}),
-        ...(toCSV(tag) ? { tag: toCSV(tag) } : {}),
-        ...(toCSV(location) ? { location: toCSV(location) } : {}),
+        ...(() => {
+          const tagCSV = toCSV(tag);
+          return tagCSV ? { tag: tagCSV } : {};
+        })(),
+        ...(() => {
+          const locationCSV = toCSV(location);
+          return locationCSV ? { location: locationCSV } : {};
+        })(),
       },
     })

Also applies to: 29-31


13-20: JSDoc 파라미터 주석 업데이트 필요

함수 시그니처에 tag/location이 추가되었으나 상단 주석에 반영되지 않았습니다. 공개 API인 만큼 주석 동기화를 권장합니다.

src/app/(home)/_api/shop/shop.queries.ts (2)

7-13: 쿼리 키 안정성 향상: 필터 배열 정렬/정규화하여 캐시 키 난립 방지

동일 의미의 필터라도 순서가 다르면 다른 queryKey가 생성됩니다. tag/location을 정렬/정규화해서 키 안정성을 높이세요.

 export const storesQueryKeys = {
   all: ["stores"] as const,
-  size: (
-    size: number,
-    category?: string,
-    tag?: string[],
-    location?: string[]
-  ) => [...storesQueryKeys.all, size, category, tag, location] as const,
+  size: (
+    size: number,
+    category?: string,
+    tag?: string[],
+    location?: string[]
+  ) => {
+    const norm = (a?: string[]) => (a && a.length ? [...a].filter(Boolean).sort() : undefined);
+    return [...storesQueryKeys.all, size, category, norm(tag), norm(location)] as const;
+  },
 };

27-29: API 호출 파라미터도 정렬/정규화하여 서버 캐시/로그 일관성 확보

queryKey만 정규화하면 캐시 일관성은 좋아지지만, 서버에는 순서가 다른 CSV가 전달될 수 있습니다. API 호출에도 동일 정규화를 적용해두면 운영 관점에서 유리합니다.

-  queryOptions({
-    queryKey: storesQueryKeys.size(size, category, tag, location),
-    queryFn: () => getStores({ size, category, tag, location }),
-  });
+  queryOptions({
+    queryKey: storesQueryKeys.size(size, category, tag, location),
+    queryFn: () => {
+      const norm = (a?: string[]) => (a && a.length ? [...a].filter(Boolean).sort() : undefined);
+      return getStores({ size, category, tag: norm(tag), location: norm(location) });
+    },
+  });
src/app/(home)/page.tsx (1)

23-28: 구분선은 Spacer 스타일 대신 전용 컴포넌트(Separator/Divider) 사용 고려

시맨틱/재사용성 측면에서 색 있는 12px 바를 Spacer 인라인 스타일로 만드는 것보다, 얇은 구분선 전용 컴포넌트(예: <Divider height={12} color={...} />)를 두는 편이 의도 전달과 유지보수에 유리합니다. 현 구조로도 문제는 없으나, 디자인 토큰/테마 변경 시 일괄치환이 쉬운 형태로 추상화를 제안합니다.

src/app/(home)/_components/StoreList/StoreList.tsx (1)

29-31: 서스펜스 fallback 추가로 로딩 UX 개선 제안

현재 <Suspense>fallback이 없어 초기 로딩 동안 공백이 발생합니다. 간단한 여백/스켈레톤을 추가하면 체감 UX가 좋아집니다.

다음처럼 최소 fallback을 지정할 수 있습니다.

-      <Suspense>
+      <Suspense fallback={<Spacer size={16} />}>
         <StoreListComponent category={selectedCategory.name} />
       </Suspense>
src/app/(store)/stores/(list)/_components/StoreList/StoreList.tsx (2)

41-49: 대량 렌더링에 대한 페이징/가상화 고려

size: 50으로 한 번에 렌더링합니다. 디바이스/네트워크 상황에 따라 이미지 서브쿼리(N개)와 맞물려 비용이 커질 수 있어, 무한 스크롤(페이지네이션) 또는 리스트 가상화 도입을 장기적으로 고려해 주세요.


31-37: 정렬된 배열로 캐시 키 안정성 보장 권장

useSuspenseQuery 호출부에 전달하는 tag/location 배열은 선택 순서에 따라 달라질 수 있어, 동일한 필터 조합임에도 서로 다른 queryKey가 생성되어 캐시 미스나 불필요한 리패치가 발생할 수 있습니다. 호출 직전에 배열을 정렬하여 순서 독립성을 보장하는 것을 권장합니다.

• 대상 위치

  • 파일: src/app/(store)/stores/(list)/_components/StoreList/StoreList.tsx
    라인 31–37 (useSuspenseQuery 인자)

• 수정 제안 (옵션)

   } = useSuspenseQuery(
     storesQueryOptions({
       size: 50,
       category,
-      tag: [...selectedFilters.atmosphereTags, ...selectedFilters.utilityTags],
-      location: selectedFilters.locations,
+      tag: [
+        ...selectedFilters.atmosphereTags,
+        ...selectedFilters.utilityTags,
+      ].sort(),
+      location: [...selectedFilters.locations].sort(),
     })
   );

• 추가 확인 사항

  1. storesQueryKeys.size(size, category, tag, location) 내부에서는 현재 전달된 배열을 그대로 사용하고 별도 정렬/정규화 로직은 없습니다.
    – 필요하다면 storesQueryKeys.size 구현부에서 내부 정렬을 추가하거나, 호출부에서 정렬된 배열을 전달하는 방안 중 선택하세요.
  2. shop.api.ts 내부 toCSV 유틸(로컬 화살표 함수)은 배열을 필터링 후 join(",")만 수행하므로, 호출부에서 정렬된 배열을 넘기면 쿼리 문자열에도 정렬 순서가 유지됩니다.

위 변경은 필수는 아니지만, 캐시 키 안정성을 높이고 불필요한 refetch를 방지하는 좋은 관례입니다.

src/app/(home)/_components/RecentCheers/RecentCheers.tsx (6)

5-6: at/compact/isEmpty 의존 제거하고 slice/length로 단순화하세요

가시 태그 2개만 추리는 목적이라면 slice(0, 2)length 체크가 가장 읽기 쉽고 타입 안정적입니다. 번들 크기도 약간 줄어듭니다.

적용 예시:

- import { at, chunk, compact } from "es-toolkit";
- import { isEmpty } from "es-toolkit/compat";
+ import { chunk } from "es-toolkit";

- const selectedTags = ALL_TAGS.filter(tag => tags?.includes(tag.name));
-
- const visibleTags = compact(at(selectedTags, [0, 1]));
+ const selectedTags = ALL_TAGS.filter(tag => tags.includes(tag.name));
+ const visibleTags = selectedTags.slice(0, 2);

- {isEmpty(selectedTags) ? null : (
+ {selectedTags.length === 0 ? null : (

Also applies to: 114-117, 165-166


77-78: tags 타입과 사용 방식이 일치하지 않습니다

RecentSupportCardProps에서는 tags: string[]로 필수값인데, 필터링에서는 tags?.includes(...)로 옵셔널처럼 다루고 있습니다. 둘 중 하나로 일관성 있게 맞춰 주세요.

옵션 A(권장: 서버에서 빈 배열 보장):

- const selectedTags = ALL_TAGS.filter(tag => tags?.includes(tag.name));
+ const selectedTags = ALL_TAGS.filter(tag => tags.includes(tag.name));

옵션 B(API가 undefined를 반환할 수 있다면):

-   tags: string[];
+   tags?: string[];

그리고 호출부에서

-   tags={cheer.tags}
+   tags={cheer.tags ?? []}

Also applies to: 105-112, 114-114


82-90: 스켈레톤과 라우팅/문구가 불일치합니다

본 컴포넌트는 '/cheer' + "응원 전체보기"로 변경되었지만, 스켈레톤은 여전히 '/stores' + "가게 전체보기"를 가리킵니다. UX 혼선을 막기 위해 스켈레톤도 동일하게 맞춰 주세요.

스켈레톤 수정 예시(파일 하단, 안내용 추가 코드 블록):

-        <Link href='/stores'>
+        <Link href='/cheer'>
...
-            가게 전체보기
+            응원 전체보기

43-45: 빈 데이터일 때 refresh에서 NaN 인덱스가 발생할 수 있습니다

chunkedList.length === 0% 0으로 NaN이 되고, 이후 인덱싱이 전부 undefined가 됩니다. 데이터가 없을 때는 버튼을 비활성화하거나 no-op 처리하세요.

  const handleRefresh = () => {
-   setCurrentIndex((currentIndex + 1) % chunkedList.length);
+   if (chunkedList.length <= 1) return;
+   setCurrentIndex((currentIndex + 1) % chunkedList.length);
  };

또는 렌더 시 chunkedList.length <= 1이면 새로고침 버튼을 disabled로 두는 것도 방법입니다.


168-171: 태그 아이콘 Image의 도메인/경로 확인 필요

tag.iconUrl이 외부 도메인이라면 next.config.jsimages.domains에 등록되어야 합니다. 내부 정적 자원이라면 next/image 대신 정적 import(svg) 또는 <img> 사용이 더 가볍습니다(14px 아이콘).

대안:

  • 외부 호스트: next.config.js에 호스트 추가.
  • 내부 아이콘: import Icon from "@/assets/foo.svg"; <Icon .../> 또는 <img width={14} height={14} src={...} alt={...} />

165-175: ‘+N’ 더보기 칩에 접근성 힌트 추가 제안

화면 리더 사용자에게 의미를 전달하려면 aria-label이나 title을 추가하는 것이 좋습니다. 예: “태그 3개 더 있음”.

- {selectedTags.length > 2 && <Tag>+{selectedTags.length - 2}</Tag>}
+ {selectedTags.length > 2 && (
+   <Tag aria-label={`태그 ${selectedTags.length - 2}개 더 있음`} title={`태그 ${selectedTags.length - 2}개 더 있음`}>
+     +{selectedTags.length - 2}
+   </Tag>
+ )}
src/app/(store)/stores/(list)/_components/StoreList/StoreList.css.ts (3)

24-27: storeImagesContainerposition: relative 필요성 확인

자식에서 absolute 포지셔닝을 사용하지 않는다면 불필요할 수 있습니다. 의미 없는 stacking context 생성을 피하려면 제거를 고려해 주세요.


35-39: 이미지 라운딩/클리핑은 래퍼에 위임하세요

Next/Imagefill 또는 고정 크기로 사용할 때는 래퍼가 overflow: hiddenborderRadius를 가져가는 편이 안전합니다. 현재는 storeImageborderRadius가 있어 중복/미클리핑 이슈가 생길 수 있습니다.

권장 변경:

 export const storeImageWrapper = style({
   position: "relative",
   width: "100%",
   height: "168px",
+  overflow: "hidden",
+  borderRadius: radius[160],
 });
 
 export const storeImage = style({
   objectFit: "cover",
-  overflow: "hidden",
   flexShrink: 0,
-
-  borderRadius: radius[160],
+  // 필요 시: width/height 100% 또는 Next/Image의 fill 사용
 });

또한 storeImageoverflow: "hidden"은 불필요합니다(자식 클리핑은 래퍼가 담당).

Also applies to: 46-47


54-56: 빈 이미지 폭 고정(126px)로 인한 불균형 가능성

래퍼가 width: 100%인 구조라면 emptyImage도 동일하게 맞추는 편이 카드 간 레이아웃 왜곡을 줄입니다.

참고 변경(파일 외 영역 수정 예시):

-  width: "126px",
+  width: "100%",
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 226ef12 and 637ee90.

📒 Files selected for processing (13)
  • src/app/(home)/_api/cheer/cheer.types.ts (1 hunks)
  • src/app/(home)/_api/shop/shop.api.ts (1 hunks)
  • src/app/(home)/_api/shop/shop.queries.ts (1 hunks)
  • src/app/(home)/_components/RecentCheers/RecentCheers.css.ts (2 hunks)
  • src/app/(home)/_components/RecentCheers/RecentCheers.tsx (5 hunks)
  • src/app/(home)/_components/StoreList/StoreList.tsx (1 hunks)
  • src/app/(home)/_components/StoreList/index.ts (1 hunks)
  • src/app/(home)/_components/index.ts (1 hunks)
  • src/app/(home)/page.tsx (2 hunks)
  • src/app/(store)/constants/storeCategory.constants.ts (0 hunks)
  • src/app/(store)/stores/(list)/_components/StoreList/StoreList.css.ts (3 hunks)
  • src/app/(store)/stores/(list)/_components/StoreList/StoreList.tsx (3 hunks)
  • src/components/ui/FoodCategory/FoodCategory.css.ts (1 hunks)
💤 Files with no reviewable changes (1)
  • src/app/(store)/constants/storeCategory.constants.ts
🧰 Additional context used
📓 Path-based instructions (3)
{src/app/**/_components/**/*.tsx,src/components/**/*.tsx}

📄 CodeRabbit inference engine (.cursor/rules/nextjs-folder-structure.mdc)

Component files must use PascalCase naming (e.g., Button.tsx, DomainLayout.tsx).

Files:

  • src/app/(store)/stores/(list)/_components/StoreList/StoreList.tsx
  • src/app/(home)/_components/StoreList/StoreList.tsx
  • src/app/(home)/_components/RecentCheers/RecentCheers.tsx
src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/nextjs-folder-structure.mdc)

All files must use TypeScript.

Files:

  • src/app/(store)/stores/(list)/_components/StoreList/StoreList.tsx
  • src/components/ui/FoodCategory/FoodCategory.css.ts
  • src/app/(home)/_components/RecentCheers/RecentCheers.css.ts
  • src/app/(home)/_components/index.ts
  • src/app/(home)/_components/StoreList/StoreList.tsx
  • src/app/(home)/_api/cheer/cheer.types.ts
  • src/app/(home)/_components/StoreList/index.ts
  • src/app/(home)/_api/shop/shop.queries.ts
  • src/app/(store)/stores/(list)/_components/StoreList/StoreList.css.ts
  • src/app/(home)/page.tsx
  • src/app/(home)/_api/shop/shop.api.ts
  • src/app/(home)/_components/RecentCheers/RecentCheers.tsx
{src/styles/**/*.css.ts,src/app/**/_components/**/*.css.ts,src/components/**/*.css.ts}

📄 CodeRabbit inference engine (.cursor/rules/nextjs-folder-structure.mdc)

Style files must use camelCase naming with the .css.ts extension (e.g., Button.css.ts, theme.css.ts).

Files:

  • src/components/ui/FoodCategory/FoodCategory.css.ts
  • src/app/(home)/_components/RecentCheers/RecentCheers.css.ts
  • src/app/(store)/stores/(list)/_components/StoreList/StoreList.css.ts
🧬 Code graph analysis (7)
src/app/(store)/stores/(list)/_components/StoreList/StoreList.tsx (1)
src/app/(home)/_api/shop/shop.queries.ts (1)
  • storesQueryOptions (15-29)
src/app/(home)/_components/StoreList/StoreList.tsx (1)
src/hooks/useFoodCategory.ts (1)
  • useFoodCategory (38-79)
src/app/(home)/_api/cheer/cheer.types.ts (1)
src/types/image.types.ts (1)
  • ImageResponse (11-11)
src/app/(home)/_api/shop/shop.queries.ts (1)
src/app/(home)/_api/shop/shop.api.ts (1)
  • getStores (10-34)
src/app/(home)/page.tsx (2)
src/app/(home)/_components/index.ts (2)
  • RecentCheers (1-1)
  • StoreList (4-4)
src/app/(home)/_components/StoreList/StoreList.tsx (1)
  • StoreList (11-34)
src/app/(home)/_api/shop/shop.api.ts (2)
src/app/(home)/_api/shop/shop.types.ts (1)
  • StoresResponse (10-12)
src/lib/api/client.ts (1)
  • http (69-74)
src/app/(home)/_components/RecentCheers/RecentCheers.tsx (1)
src/constants/tag.constants.ts (1)
  • ALL_TAGS (84-84)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: test
🔇 Additional comments (13)
src/app/(home)/_api/cheer/cheer.types.ts (1)

1-2: Cheer.tags 하위 호환성 검증 필요

Cheer 타입에서 tags: string[]를 필수로 유지하면, 백엔드가 빈 배열을 반환하지 않거나 tags 필드를 누락했을 때 런타임 에러가 발생할 수 있습니다. 아래 사항을 확인 및 적용해주세요.

– cheer.types.ts
• 위치: src/app/(home)/_api/cheer/cheer.types.ts
• 현재 정의:
ts export type Cheer = { // … tags: string[]; };

cheer.tags 직접 사용 위치
• RecentCheers 컴포넌트
src/app/(home)/_components/RecentCheers/RecentCheers.tsx:77tags={cheer.tags}
• CheerCardTags 컴포넌트
src/app/(cheer)/cheer/_components/CheerCard/CheerCard.tsx:134<CheerCardTags tags={cheer.tags} />

추천 옵션(백엔드가 빈 배열을 항상 보장하지 않는 경우):

  1. 타입을 optional로 변경

    export type Cheer = {
      // …
    - tags: string[];
    + tags?: string[];
    };
  2. adapter 레이어에서 기본값 매핑

    function normalizeCheer(raw: RawCheer): Cheer {
      return {
        ...raw,
        tags: raw.tags ?? [],
      };
    }

위 두 옵션 중 하나를 적용하거나, 백엔드 스펙(빈 배열 보장 여부)을 우선 확인 후 진행해주세요.

src/app/(home)/_components/RecentCheers/RecentCheers.css.ts (1)

9-11: 카드 높이 동적화 + overflow 처리 LGTM

min/maxHeight와 overflow: hidden 적용으로 내용 길이 편차 대응이 좋아졌습니다. 내부 포커스 가능한 요소가 있다면 포커스 링이 잘리지 않는지만 한 번만 확인해 주세요.

src/app/(home)/_components/index.ts (1)

4-4: StoreList 배럴 노출 추가 LGTM

홈 컴포넌트 퍼블릭 API에 StoreList를 노출한 선택이 합리적입니다. 경로 ./StoreList는 내부 배럴(index.ts)을 바라보므로 트리셰이킹/경로 안정성 모두 괜찮습니다.

src/app/(home)/_components/StoreList/index.ts (1)

1-1: 단일 엔트리 배럴 구성 적절

StoreList 하위 모듈을 export *로 재노출하는 패턴이 상위 배럴과 잘 맞습니다. 추후 서브 컴포넌트가 추가되어도 import 경로가 안정적으로 유지됩니다.

src/app/(home)/page.tsx (2)

30-30: 홈 섹션에 StoreList 통합 👍

홈 탭에서 RecentCheers 아래에 스토어 리스트를 배치한 구성과 여백 조정이 자연스럽습니다. 상위 페이지는 클라이언트 컴포넌트이므로 서브트리의 서스펜스/쿼리 훅과도 호환됩니다.


6-6: colors.coolNeutral[98] 및 [96] 토큰 존재 확인 완료

colors.css.ts 파일 내에서 coolNeutral 스케일에 99, 98, 96 키가 정의되어 있어 colors.coolNeutral[98][96] 호출 시 런타임 오류가 없습니다. 해당 리뷰 코멘트는 해결되었습니다.

src/app/(home)/_components/StoreList/StoreList.tsx (1)

12-14: 빈 카테고리 문자열 처리 확인(캐시 키/쿼리 파라미터 영향)

selectedCategory.name이 빈 문자열일 수 있습니다. 이 값이 그대로 하위 StoreListComponentstoresQueryOptions에 전달되면,

  • 캐시 키에 ""가 포함되어 불필요한 변형 키가 생길 수 있고,
  • API 레이어에서 빈 문자열이 쿼리스트링에 실리면 서버에서 "전체"가 아닌 "빈 문자열"로 해석될 가능성이 있습니다.

현 구현이 “빈 문자열=전체”로 명시적으로 처리됨을 보장하는지 확인 부탁드립니다. 필요 시 아래 둘 중 하나로 정리하는 것을 권장합니다.

  1. 하위 컴포넌트 prop을 선택형으로 완화:
- export const StoreList = ({ category }: { category: string }) => {
+ export const StoreList = ({ category }: { category?: string }) => {
  1. 혹은 상위에서 undefined로 정규화:
- <StoreListComponent category={selectedCategory.name} />
+ <StoreListComponent category={selectedCategory.name || undefined} />

두 방법 중 하나만 택해 일관성 있게 적용해 주세요. 선택 2를 택하면 하위 타입도 함께 조정되어야 합니다.

Also applies to: 29-31

src/app/(store)/stores/(list)/_components/StoreList/StoreList.tsx (3)

117-117: 썸네일 선택 로직 간결화 👍

first(imageUrls)로 대표 이미지를 선택하고 Empty 상태를 분기하는 방식이 단순하고 명확합니다.


145-149: Empty 상태 컬러 토큰 검증

colors.coolNeutral[96] 토큰 존재 여부도 함께 확인 부탁드립니다(상위 파일에서 98 인덱스와 동일 이슈).


124-138: unoptimized 플래그 제거 가능
프로젝트의 next.config.ts 에 이미 images.remotePatterns로 원격 이미지 도메인이 등록되어 있으므로, unoptimized 옵션을 제거하고 기본 이미지 최적화를 그대로 사용할 수 있습니다.

검토 대상:

  • 파일: src/app/(store)/stores/(list)/_components/StoreList/StoreList.tsx
  • 위치: 124–138번째 줄

제거할 코드:

-                // TODO: 추후 제거
-                unoptimized

추가 안내:

  • priority={false}는 Next/Image의 기본값이므로 별도로 명시하지 않아도 무방합니다.
src/app/(home)/_components/RecentCheers/RecentCheers.tsx (2)

72-73: 새 스키마(images[0]?.url) 적용 👍 — 안전장치도 적절합니다

빈 문자열 처리 전에 조건부 렌더링으로 fallback을 제공하고 있어 런타임 경고를 잘 회피합니다. 이 변경은 문제 없어 보입니다.


17-17: 태그 시스템 연동을 위한 컴포넌트/상수 import 추가 LGTM

UI 구성요소와 상수 분리 방향 좋습니다. 추후 태그 체계 확장에도 유연합니다.

Also applies to: 20-20

src/app/(store)/stores/(list)/_components/StoreList/StoreList.css.ts (1)

61-62: emptyImage 라운딩 통일 👍

이미지와 동일한 radius로 UI 일관성이 좋아졌습니다.

@@ -0,0 +1,34 @@
import { Suspense } from "react";
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Client 전용 훅 사용 파일에 'use client' 누락 — 빌드 에러 가능

이 컴포넌트는 useFoodCategory를 통해 useRouter/useSearchParams/usePathname(Client 전용)를 사용합니다. 파일 최상단에 "use client" 지시어가 없으면 Server Component로 취급되어 Next.js 빌드 시 에러가 납니다.

아래 패치를 적용해 주세요(Line 1 상단에 추가):

+ "use client";
 import { Suspense } from "react";
📝 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
import { Suspense } from "react";
"use client";
import { Suspense } from "react";
🤖 Prompt for AI Agents
In src/app/(home)/_components/StoreList/StoreList.tsx around line 1, this
component uses client-only hooks (useRouter/useSearchParams/usePathname via
useFoodCategory) but lacks the "use client" directive; add the "use client"
directive as the very first line of the file so Next.js treats the file as a
Client Component and prevents build-time errors.

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

🧹 Nitpick comments (2)
src/app/(store)/stores/(list)/_components/StoreListGNB/StoreListGNB.tsx (1)

3-5: 타이틀 하드코딩 대신 props로 일반화해 재사용성/카피 변경 대응성 향상 제안

향후 i18n/A-B 테스트/카피 변경에 유연하게 대응하려면 기본값을 두고 title을 외부에서 주입하는 편이 좋습니다.

-export const StoreListGNB = () => {
-  return <GNB title='가게 모아보기' />;
-};
+type StoreListGNBProps = { title?: string };
+export const StoreListGNB = ({ title = "가게 모아보기" }: StoreListGNBProps) => {
+  return <GNB title={title} />;
+};
src/app/(store)/stores/(list)/page.tsx (1)

9-9: Spacer 임포트 OK. 다만 간격 스케일 일관성 확인 권장

현재 사용처(라인 32)에서 size={12}를 쓰고 있는데, 디자인 토큰이 4/8 스케일 기반이라면 12가 예외값일 수 있습니다. 팀 스케일과 맞는지 한 번만 확인 부탁드립니다.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 637ee90 and 0c3432f.

📒 Files selected for processing (2)
  • src/app/(store)/stores/(list)/_components/StoreListGNB/StoreListGNB.tsx (1 hunks)
  • src/app/(store)/stores/(list)/page.tsx (2 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/nextjs-folder-structure.mdc)

All files must use TypeScript.

Files:

  • src/app/(store)/stores/(list)/page.tsx
  • src/app/(store)/stores/(list)/_components/StoreListGNB/StoreListGNB.tsx
{src/app/**/_components/**/*.tsx,src/components/**/*.tsx}

📄 CodeRabbit inference engine (.cursor/rules/nextjs-folder-structure.mdc)

Component files must use PascalCase naming (e.g., Button.tsx, DomainLayout.tsx).

Files:

  • src/app/(store)/stores/(list)/_components/StoreListGNB/StoreListGNB.tsx
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: test
🔇 Additional comments (4)
src/app/(store)/stores/(list)/_components/StoreListGNB/StoreListGNB.tsx (2)

4-4: 간결화된 GNB 적용, 문제 없어 보입니다

  • 컴포넌트/파일 네이밍이 PascalCase를 준수하고, TSX 사용 요건도 충족합니다.
  • 서버 컴포넌트로서 문자열 props만 내려 안정적입니다.

4-4: StoreListGNB 내비게이션 액션 누락 확인 요청

  • src/app/(store)/stores/(list)/_components/StoreListGNB/StoreListGNB.tsx 에서
    return <GNB title='가게 모아보기' />;
    처럼 left/right addon 없이 GNB가 렌더링되고 있어, 뒤로가기 버튼(왼쪽)이나 프로필 진입(오른쪽)이 제거된 상태입니다.
  • 동일한 GNB 컴포넌트가 쓰이는 다른 화면들(SettingGNB, StoreRegisterGNB, StoreDetailGNB 등)에는 leftAddon·rightAddon으로 내비게이션 액션이 제공되고 있음을 확인했습니다.
  • 스토어 리스트 페이지가 루트 레이아웃의 BottomNavigation(하단 탭)을 통해 진입되는 화면인지, 아니면 독립적인 페이지인지 검토 부탁드립니다.
    • 만약 BottomNavigation이 적용되어 스토어 리스트 진입 경로가 탭 내에 있다면 프로필 진입은 하단 탭을 통해 여전히 보장됩니다.
    • BottomNavigation이 없는 구조라면, 별도의 뒤로가기 또는 프로필 버튼이 필요할 수 있습니다.

위 사항을 확인하신 후, 필요 시 StoreListGNBleftAddon·rightAddon을 추가하거나, 레이아웃 차원에서 하단 네비게이션 노출 여부를 검토해 주시면 감사하겠습니다.

src/app/(store)/stores/(list)/page.tsx (2)

5-5: ChipFilter 도입 자체는 좋습니다.

공유 컴포넌트로 분리해 재사용성을 높인 점 👍


31-35: 위 스크립트가 --type tsx를 인식하지 못해 실행 결과가 출력되지 않았습니다. 아래와 같이 파일 확장자 글로브(-g)를 사용해서 다시 한 번 확인 후 결과를 공유 부탁드립니다.

#!/bin/bash
# 1) Suspense import 원본 확인
rg -nP -C1 'from\s+"@suspensive/react"|from\s+"react"' -g '*.tsx'

# 2) ChipFilter 훅/프로바이더/컨텍스트 사용 여부 확인
rg -nP -C2 'useChipFilter\(|ChipFilterProvider|ChipFilterContext' -g '*.{ts,tsx}'

# 3) StoreList 컴포넌트 시그니처 확인 (category가 optional string인지)
rg -nP '^(export\s+)?(function|const)\s+StoreList' -C2 -g '*.{ts,tsx}'

위 결과를 토대로 리뷰 코멘트를 최종 확정하겠습니다.

@Seojunhwan Seojunhwan merged commit bb06fb3 into main Aug 21, 2025
8 checks passed
@Seojunhwan Seojunhwan deleted the feat/main-layout branch August 21, 2025 15:03
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.

2 participants