Skip to content

Conversation

@leeleeleeleejun
Copy link
Member

@leeleeleeleejun leeleeleeleejun commented Aug 17, 2025

#️⃣연관된 이슈

📝작업 내용

메인 화면 ui 구현 및 MSW를 활용한 API로직 함께 구현

  • MSW 를 활용해 미리 api의 요청 관련 로직까지 함께 구현했습니다.

  • CLIENT_PATH 추가하여 클라이언트 라우팅 경로를 상수로 관리 0fa1c3c

  • BottomNavigationTabItem 컴포넌트 추가 cea3ddc

  • TanstackQuery의 Key를 관리하는 방법으로 queryOptions을 활용한 Query factory를 도입했습니다.

    // example
    const userKeys = {
      all: ['users'] as const,
      detail: (id: number) => [...userKeys.all, 'detail', id] as const,
    }
    
    const userQueries = {
      detail: (id: number) => ({
        queryKey: userKeys.detail(id),
        queryFn: () => fetchUser(id),
        staleTime: 10 * 60 * 1000,
      }),
    }
  • 런타임에서도 api 요청 등 타입 검증 가능하도록 zod를 도입했습니다.

  • HydrationBoundaryPage 컴포넌트에서 타입 안정성 향상을 위해 문제로 쿼리 옵션을 prefetch 파라미터로 변경했습니다.
    359ed03 c00e778

    // before
     const queries = [useCategoryQueries.list()]
     
     <HydrationBoundaryPage queries={queries}>
       <Categories />
     </HydrationBoundaryPage>
    
    // after
     <HydrationBoundaryPage
        prefetch={async (queryClient) => {
          await queryClient.prefetchQuery(useCategoryQueries.list())
          await queryClient.prefetchQuery(usePlaceQueries.rankingList('likes'))
        }}
      >
        <Categories />
     </HydrationBoundaryPage>

스크린샷 (선택)

image

💬리뷰 요구사항(선택)

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

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

Summary by CodeRabbit

  • New Features

    • 홈 화면 개편: 카테고리 그리드, 자동재생 배너, 찜/조회수 랭킹 리스트, 장소 리스트 아이템, 하단 내비게이션 등 UI 컴포넌트 추가
    • 검색바·헤더 및 주요 레이아웃 구성 적용
  • Refactor

    • 서버 프리패치·하이드레이션 흐름 단순화로 초기 로딩 일관성 개선
    • 칩 컴포넌트 API 및 검색바 사용성 개선
  • Style

    • 전체 배경색 밝은 톤으로 업데이트
  • Chores

    • keen-slider 및 zod 의존성 추가, 목(모의) API에 장소 랭킹 엔드포인트 추가

- Chip.stories.tsx 함께 수정
- 기본 컴포넌트 div로 변경
@leeleeleeleejun leeleeleeleejun self-assigned this Aug 17, 2025
@leeleeleeleejun leeleeleeleejun added ⚙️ Chore 환경 셋팅 🎨 Style 스타일 관련 ✨ Feature 기능 구현 labels Aug 17, 2025
@leeleeleeleejun leeleeleeleejun linked an issue Aug 17, 2025 that may be closed by this pull request
2 tasks
@coderabbitai
Copy link

coderabbitai bot commented Aug 17, 2025

Walkthrough

HydrationBoundaryPage가 쿼리 리스트 대신 단일 prefetch 콜백 API로 변경되었고, 카테고리·장소 관련 Zod 스키마·서비스·쿼리, 메인 페이지 UI(배너, 카테고리, 랭킹, 하단 내비) 및 MSW 목 데이터/핸들러와 UI 라이브러리(Chip API 변경, Divider 추가 등)가 추가되었습니다.

Changes

Cohort / File(s) Summary
SSR/Hydration 개편
apps/web/app/HydrationBoundaryPage.tsx
Public API 변경: queries: QueryConfig[]prefetch: (queryClient: QueryClient) => Promise<void>; 내부 prefetch 로직을 개별 prefetchQuery 호출에서 단일 await prefetch(queryClient) 호출로 교체.
메인 페이지 구성 업데이트
apps/web/app/page.tsx, apps/web/app/layout.tsx
메인 페이지를 HydrationBoundaryPage 기반으로 재구성, 서버 프리패치(callback) 추가, 레이아웃·배경색 및 Keen Slider CSS 임포트 추가.
쿼리·서비스·스키마 (카테고리)
apps/web/app/_apis/queries/category.ts, apps/web/app/_apis/services/category.ts, apps/web/app/_apis/schemas/category.ts
CategoryQueryKeys 및 useCategoryQueries.list 추가, getCategories 서비스 추가, CategorySchema 및 Category 타입(Zod) 추가.
쿼리·서비스·스키마 (장소)
apps/web/app/_apis/queries/place.ts, apps/web/app/_apis/services/place.ts, apps/web/app/_apis/schemas/place.ts
PlaceQueryKeys 및 usePlaceQueries.rankingList 추가, getRankingPlaces 서비스 추가, BasePlace/RankingPlace 스키마·타입 및 RankingPlaceSort 정의.
경로 상수
apps/web/app/_constants/path.ts
API_PATH에 RANKING(sort) 추가, CLIENT_PATH에 여러 클라이언트 라우트(동적 포함) 추가.
메인 UI 컴포넌트 추가
apps/web/app/_components/Banner/*, .../BottomNavigation/*, .../Categories/*, .../PlaceListItem/*, .../RankingPlaceList/*, .../SubTitle/*
Banner, BottomNavigation(및 TabItem), Categories/CategoryItem, PlaceListItem, RankingPlaceList(및 MostLiked/MostViews), SubTitle 등 신규 컴포넌트와 인덱스 배럴 추가.
MSW 목 데이터/핸들러
apps/web/app/_mocks/data/place.ts, apps/web/app/_mocks/handlers/placeHandlers.ts, apps/web/app/_mocks/handlers/index.ts
RankingPlaces 목 데이터 추가, RANKING likes/views 엔드포인트 핸들러 추가 및 handlers 배열에 통합.
UI 라이브러리: Chip 리팩터
packages/ui/src/components/Chip/Chip.tsx, .../Chip.stories.tsx
Chip API를 chipType에서 icon: IconType, label: string으로 변경; 스토리도 데이터 기반으로 수정(사용법 변경).
UI 라이브러리: 신규/수정 컴포넌트
packages/ui/src/components/Divider/*, packages/ui/src/components/SearchBar/SearchBar.tsx, packages/ui/src/components/Header/Header.tsx, packages/ui/src/components/Icon/index.tsx
Divider 컴포넌트 추가 및 배럴, SearchBar에 className prop 추가, Header에서 제목을 h1으로 명시, Icon 모듈에 IconType 타입 재노출.
의존성 변경
apps/web/package.json
keen-sliderzod 의존성 추가.
스토리/테스트 보완
packages/ui/.../Chip.stories.tsx
Chip 스토리들을 신규 데이터 기반으로 갱신.

Sequence Diagram(s)

sequenceDiagram
  participant Page as app/page.tsx
  participant HBP as HydrationBoundaryPage
  participant QC as QueryClient
  participant PrefetchFn as prefetch(callback)
  participant RQ as ReactQuery(dehydrate)

  Page->>HBP: render with prefetch callback
  HBP->>QC: create QueryClient
  HBP->>PrefetchFn: await prefetch(QC)
  PrefetchFn->>QC: prefetchQuery(...) or multiple fetches
  HBP->>RQ: dehydrate(QC)
  RQ-->>Page: dehydrated state embedded for hydration
Loading
sequenceDiagram
  participant UI as Component (Categories/MostViews/MostLiked)
  participant RQ as React Query
  participant Svc as Service (getCategories/getRankingPlaces)
  participant HTTP as axiosInstance
  participant API as Server/MSW
  participant Zod as Zod.parse

  UI->>RQ: useSuspenseQuery(queryOptions)
  RQ->>Svc: call queryFn
  Svc->>HTTP: GET API_PATH...
  HTTP->>API: request
  API-->>HTTP: JSON response
  HTTP-->>Svc: response.data
  Svc->>Zod: ZodSchema.parse(response.data)
  Zod-->>Svc: validated data
  Svc-->>RQ: return data
  RQ-->>UI: render with data
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

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/#12

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

@leeleeleeleejun leeleeleeleejun marked this pull request as ready for review August 17, 2025 13:57
@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.

Actionable comments posted: 7

🧹 Nitpick comments (30)
apps/web/app/_components/Banner/Banner.tsx (3)

3-3: keen-slider CSS 전역 임포트와 중복됩니다 — 한 곳으로 모으는 것을 권장

layout.tsx(Line 3)에서 이미 전역으로 CSS를 불러오고 있어, 여기서의 중복 임포트는 불필요합니다. 전역 임포트만 유지하는 편이 예측 가능성이 높습니다.

아래처럼 컴포넌트 내 CSS 임포트를 제거해 주세요.

-import 'keen-slider/keen-slider.min.css'

6-9: 접근성(ARIA) 강화: carousel 역할/라벨링 추가 제안

스크린 리더 친화적 카루셀로 인식되도록 role/roledescription/label을 제공하는 것을 권장합니다. 라벨을 props로 주입할 수 있도록 하면 재사용성이 높습니다.

아래처럼 ariaLabel을 추가하고 컨테이너에 ARIA 속성을 부여하는 것을 제안합니다.

 type Props = {
   contents: React.ReactNode[]
   minHeight?: number
+  ariaLabel?: string
 }
-export const Banner = ({ contents, minHeight = 150 }: Props) => {
+export const Banner = ({ contents, minHeight = 150, ariaLabel = '배너' }: Props) => {
-    <div ref={sliderRef} className={'keen-slider'} style={{ minHeight }}>
+    <div
+      ref={sliderRef}
+      className={'keen-slider'}
+      style={{ minHeight }}
+      role='region'
+      aria-roledescription='carousel'
+      aria-label={ariaLabel}
+      aria-live='polite'
+    >

Also applies to: 33-33, 70-77


73-76: index 기반 key는 정적 배열이 아닐 경우 재정렬 시 리렌더링 비용 증가 가능

콘텐츠가 동적으로 바뀌거나 순서가 바뀔 수 있다면, 가능한 안정적인 key를 사용하는 편이 좋습니다. 현재 정적 사용이 보장된다면 무시해도 됩니다.

apps/web/app/layout.tsx (1)

3-3: keen-slider CSS 전역 임포트는 적절. 컴포넌트 내 중복 임포트 제거 권장

전역 스타일로 한 곳(layout)에서만 불러오면 충돌/중복을 줄일 수 있습니다. Banner.tsx(Line 3)의 CSS 임포트는 제거해 주세요.

apps/web/app/_components/Banner/index.tsx (1)

1-1: 확장자 니트픽: JSX가 없다면 .ts로 변경 검토

해당 파일은 단순 re-export만 수행하므로 .tsx 대신 .ts 사용을 고려해도 좋습니다. 빌드 속도 및 도구 인식 측면에서 약간의 이점이 있습니다.

apps/web/app/_components/RankingPlaceList/MostViewsPlaces/MostViewsPlaces.tsx (1)

7-11: 가독성: data를 의미 있는 이름으로 변경 제안

data가 RankingPlace[]임을 바로 드러내면 이후 사용처에서 의미 파악이 빠릅니다.

아래처럼 변수명을 구체화하면 읽기 쉽습니다.

-export const MostViewsPlaces = () => {
-  const { data } = useSuspenseQuery(usePlaceQueries.rankingList('views'))
-
-  return <RankingPlaceList title={'오늘의 맛집'} icon={'fire'} places={data} />
+export const MostViewsPlaces = () => {
+  const { data: places } = useSuspenseQuery(
+    usePlaceQueries.rankingList('views'),
+  )
+
+  return (
+    <RankingPlaceList title={'오늘의 맛집'} icon={'fire'} places={places} />
+  )
 }
apps/web/app/_apis/services/category.ts (2)

3-3: import 경로 일관성 제안

동일한 모듈 계층에서 alias(@/...)를 사용 중이므로, schemas도 alias로 맞추면 일관성이 좋아집니다.

-import { CategorySchema, Category } from '../schemas/category'
+import { CategorySchema, Category } from '@/_apis/schemas/category'

5-8: 요청 취소(AbortSignal)·타이핑 개선으로 DX/안정성 향상 제안

React Query는 queryFn에 AbortSignal을 전달합니다. 서비스 레이어에서 signal을 받아 axios에 넘기면 탭 전환 등에서 불필요한 네트워크 낭비를 줄일 수 있습니다. 또한 axios 응답 제네릭을 unknown으로 두고 zod로 단언하는 패턴이 안전합니다.

아래 변경을 고려해주세요:

-export const getCategories = async (): Promise<Category[]> => {
-  const { data } = await axiosInstance.get(API_PATH.CATEGORY)
-  return CategorySchema.array().parse(data)
-}
+export const getCategories = async (signal?: AbortSignal): Promise<Category[]> => {
+  const { data } = await axiosInstance.get<unknown>(API_PATH.CATEGORY, { signal })
+  return CategorySchema.array().parse(data)
+}

그리고 쿼리 팩토리(useCategoryQueries)가 있다면 queryFn에서 전달된 signal을 서비스에 넘겨주세요(참고용, 외부 파일):

// apps/web/app/_apis/queries/category.ts (예시)
export const useCategoryQueries = {
  list: () =>
    queryOptions({
      queryKey: CategoryQueryKeys.list(),
      queryFn: ({ signal }) => getCategories(signal),
      // 필요 시 staleTime 등 추가
    }),
}
packages/ui/src/components/Divider/Divider.tsx (1)

1-5: forwardRef와 기본 hr props 전달로 재사용성/테스트 용이성 개선 제안

현재 className만 받도록 고정되어 있어 data-testid, aria-* 등 기본 hr 속성을 전달할 수 없습니다. 또한 border 전역 지정 대신 상단 보더만 지정하면 목적을 더 명확히 표현할 수 있습니다.

아래처럼 수정하면 좋겠습니다.

-import { cn } from '../../utils/cn'
+import { forwardRef, type ComponentPropsWithoutRef } from 'react'
+import { cn } from '../../utils/cn'
 
-export const Divider = ({ className }: { className?: string }) => (
-  <hr className={cn('ui:border-[#EFF1F3] ui:border-[4px]', className)} />
-)
+export type DividerProps = ComponentPropsWithoutRef<'hr'>
+
+export const Divider = forwardRef<HTMLHRElement, DividerProps>(
+  ({ className, ...props }, ref) => (
+    <hr
+      ref={ref}
+      className={cn('ui:border-0 ui:border-t-[4px] ui:border-[#EFF1F3]', className)}
+      {...props}
+    />
+  ),
+)
apps/web/app/_components/Categories/Categories.tsx (1)

11-18: 시맨틱 마크업으로 접근성 향상 제안(ul/li 사용)

카테고리 목록은 시맨틱하게 리스트로 표현하는 편이 스크린리더 호환성에 유리합니다. 그리드 레이아웃은 그대로 유지 가능합니다.

-  return (
-    <div
-      className={cn('grid grid-flow-row grid-cols-5 gap-y-6', 'px-7.5 py-2.5')}
-    >
-      {categories.map((category) => (
-        <CategoryItem key={category.id} {...category} />
-      ))}
-    </div>
-  )
+  return (
+    <ul
+      className={cn('grid grid-flow-row grid-cols-5 gap-y-6', 'px-7.5 py-2.5')}
+      role='list'
+    >
+      {categories.map((category) => (
+        <li key={category.id}>
+          <CategoryItem {...category} />
+        </li>
+      ))}
+    </ul>
+  )
apps/web/app/_apis/schemas/category.ts (1)

5-5: id 파싱 유연성 개선 제안

실제 API가 문자열 숫자를 반환해도 수용하려면 z.coerce.number()를 사용해 변환 후 string으로 바꾸는 편이 견고합니다.

-  id: z.number().transform(String),
+  id: z.coerce.number().transform(String),
apps/web/app/_components/SubTitle/SubTitle.tsx (1)

10-16: 아이콘 접근성 미세 개선

아이콘은 타이틀 텍스트가 동일 정보를 제공하므로 스크린리더에서 중복되지 않도록 aria-hidden을 권장합니다.

-    <Icon type={icon} size={30} />
+    <Icon type={icon} size={30} aria-hidden />
packages/ui/src/components/SearchBar/SearchBar.tsx (1)

6-12: 재사용성 향상을 위한 잔여(props) 전달 지원 제안

외부에서 aria-label, onClick 등 추가 속성을 붙이고 싶을 수 있어요. 앵커() 속성들을 그대로 전달받아 Flex에 스프레드로 넘기면 재사용성이 올라갑니다.

아래처럼 타입과 구현을 보완할 수 있습니다.

-export const SearchBar = ({
-  href,
-  className,
-}: {
-  href: string
-  className?: string
-}) => {
+type SearchBarProps = {
+  href: string
+  className?: string
+} & ComponentPropsWithoutRef<'a'>
+
+export const SearchBar = ({ href, className, ...rest }: SearchBarProps) => {
   return (
     <Flex
       as='a'
       href={href}
       className={cn(
         'ui:border ui:border-gray-200',
         'ui:rounded-xl',
         'ui:p-3.5',
         'ui:items-center',
         'ui:gap-2',
         className,
       )}
-      aria-label={'검색 페이지로 이동'}
+      aria-label={'검색 페이지로 이동'}
+      {...rest}
     >

추가로 필요한 import:

import type { ComponentPropsWithoutRef } from 'react'

Also applies to: 14-25

apps/web/app/_apis/services/place.ts (1)

12-13: 미세 개선: axios 응답 제네릭을 unknown으로 선언해 any 누수 차단

parse 이전 단계에서 data 타입을 unknown으로 고정하면, zod가 유일한 신뢰 경로가 되어 타입 안전성이 더 좋아집니다.

-  const { data } = await axiosInstance.get(API_PATH.RANKING(sort))
+  const { data } = await axiosInstance.get<unknown>(API_PATH.RANKING(sort))
   return RankingPlaceSchema.array().parse(data)
apps/web/app/_components/Categories/CategoryItem.tsx (1)

8-12: 내부 라우팅 시 next/link 사용 고려

앱 환경(Next.js)에서 내부 네비게이션은 a 태그 대신 next/link를 쓰면 풀 리로드 없이 전환되어 UX가 개선됩니다. Column의 as가 컴포넌트도 허용한다면 Link로 교체하거나, Link로 감싸는 방식을 고려해주세요.

예시:

import Link from 'next/link'

<Link href={CLIENT_PATH.CATEGORY_DETAIL(id)} className='inline-block'>
  <Column className='items-center gap-1'>
    {/* ... */}
  </Column>
</Link>
apps/web/app/_apis/queries/category.ts (2)

4-9: 쿼리 키 구조 깔끔합니다. 다만 items의 categoryId 타입 정합성 확인 부탁드립니다.

현재 items(categoryId: string)로 선언되어 있는데, 모킹/스키마 상 카테고리(또는 장소의 카테고리) id가 number인 경우가 많습니다. 사용처와 스키마에 맞춰 number | string 혹은 일관된 단일 타입으로 맞추는 것을 권장합니다.

적용 예시:

-  items: (categoryId: string) =>
+  items: (categoryId: number | string) =>
     [...CategoryQueryKeys.all(), 'items', categoryId] as const,

11-17: queryOptions 팩토리 패턴 좋습니다. 기본 캐싱/리패치 정책 추가를 제안합니다.

초기 화면에서 빈번히 재요청되는 것을 줄이려면 staleTime/refetchOnWindowFocus 등을 기본값으로 넣어두는 것이 유용합니다. 또한 네이밍 관점에서 이 객체는 훅이 아니므로 useCategoryQueries 대신 categoryQueries와 같은 이름이 혼동을 줄입니다(선택).

캐싱 파라미터 추가 예시:

   list: () =>
     queryOptions({
       queryKey: CategoryQueryKeys.list(),
       queryFn: getCategories,
+      staleTime: 5 * 60 * 1000, // 5분
+      refetchOnWindowFocus: false,
     }),

네이밍 변경은 범위가 커질 수 있으니 추후 일괄 리팩토링 시 고려해 주세요.

apps/web/app/_components/BottomNavigation/BottomNavigation.tsx (2)

5-11: 센터 액션(label이 빈 문자열) 접근성 이슈: aria-label 제공 권장

가운데 탭은 텍스트 라벨이 없어 보조기기에서 이름 없는 링크로 인식될 수 있습니다. aria-label을 추가해 접근성을 보완해 주세요.

이 파일에서의 적용 예시(중앙 탭에만 ariaLabel 추가):

 const tabs: TabItemProps[] = [
   { path: 'MAIN', label: '메인', icon: 'home' },
   { path: 'MAP', label: '주변 맛집', icon: 'map' },
-  { path: 'PLACE_NEW', label: '', icon: 'circlePlus', iconSize: 50 },
+  { path: 'PLACE_NEW', label: '', icon: 'circlePlus', iconSize: 50, ariaLabel: '장소 등록' },
   { path: 'LIKES', label: '찜', icon: 'navHeart' },
   { path: 'PROFILE', label: '내 정보', icon: 'navUser' },
 ]

참고: TabItem 컴포넌트에도 ariaLabel을 전달해 Link에 적용해야 합니다(다른 파일 변경 예시는 아래).

추가 변경(다른 파일: apps/web/app/_components/BottomNavigation/TabItem.tsx):

// 타입에 ariaLabel 추가
export type TabItemProps = {
  path: keyof Pick<typeof CLIENT_PATH, 'MAIN' | 'MAP' | 'LIKES' | 'PROFILE' | 'PLACE_NEW'>
  icon: IconType
  iconSize?: number
  label?: string
  ariaLabel?: string
}

// Link에 aria-label 연결
export const TabItem = ({ path, label, ariaLabel, icon, iconSize = 26 }: TabItemProps) => {
  const pathname = usePathname()
  const href = CLIENT_PATH[path]
  const active = pathname === href

  return (
    <Link
      href={href}
      className='w-12.5 flex flex-col items-center'
      aria-label={label || ariaLabel}
    >
      <Icon
        type={icon}
        size={iconSize}
        color={active ? '--color-main' : '--color-gray-200'}
      />
      {label && (
        <Text
          fontWeight='normal'
          className={cn('text-[10px]', active ? 'text-main' : 'text-gray-200')}
        >
          {label}
        </Text>
      )}
    </Link>
  )
}

15-23: nav 요소에 접근성 이름(aria-label) 부여 추천

내비게이션의 역할을 명확히 하기 위해 aria-label을 추가하면 좋습니다.

   return (
     <JustifyBetween
       as={'nav'}
+      aria-label='하단 내비게이션'
       className={cn(
         'border-t-1',
         'border-gray-50',
         'h-15',
         'mt-auto',
         'px-5 py-2.5',
       )}
     >
apps/web/app/_mocks/handlers/placeHandlers.ts (1)

11-18: 핸들러 중복 제거로 간결화 + 확장 용이성 개선 제안

likes, views 두 케이스가 동일 로직이므로 매핑으로 생성하면 중복이 줄고 향후 정렬 기준 추가도 쉬워집니다.

-export const PlaceHandlers = [
-  http.get(addBaseUrl(API_PATH.RANKING('likes')), () => {
-    return HttpResponse.json(RankingPlaces)
-  }),
-  http.get(addBaseUrl(API_PATH.RANKING('views')), () => {
-    return HttpResponse.json(RankingPlaces)
-  }),
-]
+const sorts = ['likes', 'views'] as const
+export const PlaceHandlers = sorts.map((sort) =>
+  http.get(addBaseUrl(API_PATH.RANKING(sort)), () => {
+    return HttpResponse.json(RankingPlaces)
+  }),
+)

또한 네트워크 지연/에러 시나리오를 간단히 추가해 두면 UI 회귀 테스트에 도움이 됩니다(선택).

예시(지연/에러 케이스 추가):

import { http, HttpResponse, delay } from 'msw'

http.get(addBaseUrl(API_PATH.RANKING('likes')), async () => {
  await delay(300) // 300ms 지연
  return HttpResponse.json(RankingPlaces)
})

http.get(addBaseUrl(API_PATH.RANKING('views')), async () => {
  // 에러 테스트가 필요하다면 조건부로 500 반환 등도 가능
  // return new HttpResponse('Internal Error', { status: 500 })
  return HttpResponse.json(RankingPlaces)
})
apps/web/app/_components/PlaceListItem/PlaceListItem.tsx (2)

33-35: 주소 텍스트 길이 제한(line-clamp)으로 레이아웃 안정성 향상 제안

긴 주소가 레이아웃을 밀지 않도록 한 줄로 클램프하거나 말줄임 처리하면 리스트 안정성이 좋아집니다.

-      <Text variant={'caption2'} className={'text-gray-300'}>
+      <Text variant={'caption2'} className={'text-gray-300 line-clamp-1'}>
         {address}
       </Text>

21-43: 리스트 아이템 전체 영역을 링크로 감싸는 TODO 진행 제안

현재 TODO 주석이 있어 상세 페이지 이동을 고려 중이신 것으로 보입니다. 클릭 가능 영역을 항목 전체로 넓혀 모바일 UX를 개선하는 편을 권장합니다.

예시(개념 코드):

import Link from 'next/link'
import { CLIENT_PATH } from '@/_constants/path'

export const PlaceListItem = ({ placeId, ...props }: Props) => {
  const href = CLIENT_PATH.PLACE_DETAIL(String(placeId)) // 라우트 시그니처에 맞춰 변환
  return (
    <li className='...'>
      <Link href={href} className='block py-2.5 pb-4'>
        {/* 기존 내용 (제목/아이콘/주소/태그) */}
      </Link>
    </li>
  )
}

필요하시면 라우팅 시그니처(예: /places/:id)에 맞춰 링크 생성/타입 보강까지 함께 도와드릴게요.

apps/web/app/_constants/path.ts (1)

5-6: 동적 파라미터/쿼리 인코딩 및 상수 리터럴화로 안전성·타입 정확도 개선 제안

  • 쿼리값(sort)과 경로 파라미터(id)에 encodeURIComponent를 적용해 잠재적 특수문자 이슈를 예방하는 것이 안전합니다.
  • 객체를 as const로 리터럴화하면 키가 리터럴 타입으로 고정되어 오타를 줄이고, 호출부 타입 추론이 더 정확해집니다.

아래와 같이 수정을 제안드립니다.

 export const API_PATH = {
   CATEGORY: '/categories',
-  RANKING: (sort: RankingPlaceSort) => `/places/ranking?sort=${sort}`,
-}
+  RANKING: (sort: RankingPlaceSort) =>
+    `/places/ranking?sort=${encodeURIComponent(sort)}`,
+} as const

 export const CLIENT_PATH = {
   MAIN: '/',
   MAP: '/map',
   PLACE_NEW: '/places/new',
   PLACE_SEARCH: '/places/search',
-  PLACE_DETAIL: (id: string | number) => `/places/${id}`,
-  CATEGORY_DETAIL: (id: string | number) => `/categories/${id}`,
+  PLACE_DETAIL: (id: string | number) =>
+    `/places/${encodeURIComponent(String(id))}`,
+  CATEGORY_DETAIL: (id: string | number) =>
+    `/categories/${encodeURIComponent(String(id))}`,
   LIKES: '/likes',
   PROFILE: '/profile',
-}
+} as const

Also applies to: 8-17

packages/ui/src/components/Chip/Chip.stories.tsx (1)

55-63: Storybook에선 console.log 대신 action 애드온 사용 권장

스토리 상호작용 로깅은 콘솔 출력보다 '@storybook/addon-actions' 사용이 일반적입니다. 아래처럼 바꾸면 Docs/Actions 패널에서 클릭 이벤트를 확인할 수 있습니다.

+import { action } from '@storybook/addon-actions'
 
 ...
 
         <Chip
           key={category.id}
           icon={category.icon}
           label={category.label}
-          onToggle={() => {
-            console.log(`${category.label} 클릭됨!`)
-          }}
+          onToggle={() => action('chip-click')(category.label)}
         />
apps/web/app/page.tsx (1)

19-24: SSR 프리패치 최적화: 병렬 prefetch + 'views' 랭킹도 함께 선로딩 권장

현재 likes 랭킹만 프리패치합니다. 페이지에서 MostViewsPlaces도 렌더링하므로 'views'도 함께 프리패치하면 초기 로딩 스피너/레이아웃 시프트를 줄일 수 있습니다. 또한 Promise.all로 병렬화하면 서버 시간도 단축됩니다.

-    <HydrationBoundaryPage
-      prefetch={async (queryClient) => {
-        await queryClient.prefetchQuery(useCategoryQueries.list())
-        await queryClient.prefetchQuery(usePlaceQueries.rankingList('likes'))
-      }}
-    >
+    <HydrationBoundaryPage
+      prefetch={async (queryClient) => {
+        await Promise.all([
+          queryClient.prefetchQuery(useCategoryQueries.list()),
+          queryClient.prefetchQuery(usePlaceQueries.rankingList('likes')),
+          queryClient.prefetchQuery(usePlaceQueries.rankingList('views')),
+        ])
+      }}
+    >
packages/ui/src/components/Chip/Chip.tsx (1)

33-35: JSDoc와 실제 동작이 어긋납니다

JSDoc에는 "클릭 시 내부 상태 isActive를 토글하며, onToggle 콜백을 실행"이라고 되어 있으나, 현재 구현은 onToggle이 없으면 토글이 일어나지 않습니다. 위 코멘트의 리팩터로 문서와 구현을 일치시킬 수 있습니다.

apps/web/app/_mocks/data/place.ts (1)

1-38: 모킹 데이터 스키마 정합성 강화 제안

모킹 데이터는 런타임 zod 파서로 검증되겠지만, 컴파일 타임에서도 스키마 입력 형태(z.input)에 부합하는지 확인하면 조기 오류 발견에 유용합니다. placeId가 number(→transform으로 string)인 점을 고려해, RankingPlace 타입이 아닌 RankingPlaceSchema의 입력 타입으로 satisfy 하는 방식을 권장합니다.

적용 제안:

+import { z } from 'zod'
+import { RankingPlaceSchema } from '@/_apis/schemas/place'
+
-export const RankingPlaces = [
+export const RankingPlaces = [
   {
     placeId: 15,
     placeName: '우돈탄 다산본점',
     address: '경기 남양주시 다산중앙로82번길 25',
     isLiked: true,
     likeCount: 5,
     categories: [
       { id: 3, name: '한식', iconKey: 'korean' },
       { id: 14, name: '고기·구이', iconKey: 'meat' },
     ],
     tags: [
       { id: 2, name: '혼밥하기 좋은', iconKey: 'fingerUp' },
       { id: 5, name: '가성비 좋은', iconKey: 'calculator' },
     ],
   },
   {
     placeId: 21,
     placeName: '김밥천국',
     address: '서울특별시 강남구 테헤란로 100',
     isLiked: false,
     likeCount: 5,
     categories: [
       { id: 4, name: '분식', iconKey: 'bunsik' },
       { id: 3, name: '한식', iconKey: 'korean' },
     ],
     tags: [{ id: 7, name: '분위기 좋은', iconKey: 'blingBling' }],
   },
   {
     placeId: 2,
     placeName: '짬뽕집',
     address: '충남 천안시 서북구 테헤란로 100',
     isLiked: false,
     likeCount: 5,
     categories: [{ id: 4, name: '중식', iconKey: 'chinese' }],
     tags: [{ id: 7, name: '분위기 좋은', iconKey: 'blingBling' }],
   },
-]
+ ] satisfies z.input<typeof RankingPlaceSchema>[]
apps/web/app/_apis/schemas/place.ts (1)

12-12: RankingPlaceSort를 상수 집합으로 노출하면 재사용성이 좋아집니다

API 파라미터 선택 UI 등에서 옵션 배열이 필요할 수 있습니다. 상수 배열과 타입을 함께 제공하는 패턴을 고려해 보세요.

예시:

export const RankingPlaceSorts = ['views', 'likes'] as const
export type RankingPlaceSort = (typeof RankingPlaceSorts)[number]
apps/web/app/_components/BottomNavigation/TabItem.tsx (2)

8-8: IconType는 타입 전용 import로 가져와 번들 오버헤드를 피하세요

런타임 심볼이 아닌 타입이므로 import type을 사용해 트리셰이킹/번들 안전성을 확보하는 것이 좋습니다. 다른 파일(Chip.tsx)과의 일관성도 맞출 수 있습니다.

-import { Icon, IconType } from '@repo/ui/components/Icon'
+import { Icon, type IconType } from '@repo/ui/components/Icon'

20-24: 활성 탭 판정과 접근성 개선 + CSS 변수 전달 방식 점검

  • 활성 판정을 pathname === href로만 하면 하위 경로(/profile/settings 등)에서 비활성으로 보일 수 있습니다. 메인('/')를 제외하고 하위 경로 포함 여부도 고려해 주세요.
  • Link에 aria-current를 부여해 접근성을 개선하세요.
  • label이 없는 경우를 대비해 aria-label도 설정해 주면 좋습니다.
 export const TabItem = ({ path, label, icon, iconSize = 26 }: TabItemProps) => {
   const pathname = usePathname()
   const href = CLIENT_PATH[path]
-  const active = pathname === href
+  const active = pathname === href || (href !== '/' && pathname.startsWith(`${href}/`))

   return (
-    <Link href={href} className={'w-12.5 flex flex-col items-center'}>
+    <Link
+      href={href}
+      className={'w-12.5 flex flex-col items-center'}
+      aria-current={active ? 'page' : undefined}
+      aria-label={label || path}
+    >
       <Icon
         type={icon}
         size={iconSize}
-        color={active ? '--color-main' : '--color-gray-200'}
+        color={active ? 'var(--color-main)' : 'var(--color-gray-200)'}
       />
       {label && (
         <Text
           fontWeight={'normal'}
           className={cn('text-[10px]', active ? 'text-main' : 'text-gray-200')}
         >
           {label}
         </Text>
       )}
     </Link>
   )
 }

Also applies to: 26-41

📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 0e84bbe and 3a899cc.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (39)
  • apps/web/app/HydrationBoundaryPage.tsx (1 hunks)
  • apps/web/app/_apis/queries/category.ts (1 hunks)
  • apps/web/app/_apis/queries/place.ts (1 hunks)
  • apps/web/app/_apis/schemas/category.ts (1 hunks)
  • apps/web/app/_apis/schemas/place.ts (1 hunks)
  • apps/web/app/_apis/services/category.ts (1 hunks)
  • apps/web/app/_apis/services/place.ts (1 hunks)
  • apps/web/app/_components/Banner/Banner.tsx (1 hunks)
  • apps/web/app/_components/Banner/index.tsx (1 hunks)
  • apps/web/app/_components/BottomNavigation/BottomNavigation.tsx (1 hunks)
  • apps/web/app/_components/BottomNavigation/TabItem.tsx (1 hunks)
  • apps/web/app/_components/BottomNavigation/index.tsx (1 hunks)
  • apps/web/app/_components/Categories/Categories.tsx (1 hunks)
  • apps/web/app/_components/Categories/CategoryItem.tsx (1 hunks)
  • apps/web/app/_components/Categories/index.tsx (1 hunks)
  • apps/web/app/_components/PlaceListItem/PlaceListItem.tsx (1 hunks)
  • apps/web/app/_components/PlaceListItem/index.tsx (1 hunks)
  • apps/web/app/_components/RankingPlaceList/MostLikedPlaces/MostLikedPlaces.tsx (1 hunks)
  • apps/web/app/_components/RankingPlaceList/MostLikedPlaces/index.tsx (1 hunks)
  • apps/web/app/_components/RankingPlaceList/MostViewsPlaces/MostViewsPlaces.tsx (1 hunks)
  • apps/web/app/_components/RankingPlaceList/MostViewsPlaces/index.tsx (1 hunks)
  • apps/web/app/_components/RankingPlaceList/RankingPlaceList.tsx (1 hunks)
  • apps/web/app/_components/RankingPlaceList/index.tsx (1 hunks)
  • apps/web/app/_components/SubTitle/SubTitle.tsx (1 hunks)
  • apps/web/app/_components/SubTitle/index.tsx (1 hunks)
  • apps/web/app/_constants/path.ts (1 hunks)
  • apps/web/app/_mocks/data/place.ts (1 hunks)
  • apps/web/app/_mocks/handlers/index.ts (1 hunks)
  • apps/web/app/_mocks/handlers/placeHandlers.ts (1 hunks)
  • apps/web/app/layout.tsx (2 hunks)
  • apps/web/app/page.tsx (1 hunks)
  • apps/web/package.json (1 hunks)
  • packages/ui/src/components/Chip/Chip.stories.tsx (2 hunks)
  • packages/ui/src/components/Chip/Chip.tsx (3 hunks)
  • packages/ui/src/components/Divider/Divider.tsx (1 hunks)
  • packages/ui/src/components/Divider/index.tsx (1 hunks)
  • packages/ui/src/components/Header/Header.tsx (1 hunks)
  • packages/ui/src/components/Icon/index.tsx (1 hunks)
  • packages/ui/src/components/SearchBar/SearchBar.tsx (2 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (26)
packages/ui/src/components/Icon/index.tsx (2)
packages/ui/src/components/Icon/Icon.tsx (1)
  • Icon (40-51)
packages/ui/src/components/Icon/Icon.stories.tsx (1)
  • type (30-38)
apps/web/app/_components/RankingPlaceList/MostViewsPlaces/MostViewsPlaces.tsx (3)
apps/web/app/_apis/queries/place.ts (1)
  • usePlaceQueries (11-17)
apps/web/app/_components/RankingPlaceList/RankingPlaceList.tsx (1)
  • RankingPlaceList (13-28)
apps/web/app/QueryClientProvider.tsx (1)
  • QueryProvider (40-57)
packages/ui/src/components/Divider/Divider.tsx (2)
packages/ui/src/utils/cn.ts (1)
  • cn (19-21)
packages/ui/src/components/Layout/Column/Column.tsx (1)
  • restProps (4-16)
apps/web/app/_apis/schemas/category.ts (1)
packages/ui/src/components/Icon/IconMap.ts (1)
  • IconList (101-101)
apps/web/app/_components/Categories/Categories.tsx (4)
apps/web/app/_apis/queries/category.ts (1)
  • useCategoryQueries (11-17)
packages/ui/src/utils/cn.ts (1)
  • cn (19-21)
apps/web/app/_mocks/data/category.ts (1)
  • category (1-17)
apps/web/app/_components/Categories/CategoryItem.tsx (1)
  • CategoryItem (7-23)
apps/web/app/_components/BottomNavigation/BottomNavigation.tsx (2)
apps/web/app/_components/BottomNavigation/TabItem.tsx (2)
  • TabItemProps (10-18)
  • TabItem (20-42)
packages/ui/src/utils/cn.ts (1)
  • cn (19-21)
apps/web/app/_components/RankingPlaceList/MostLikedPlaces/MostLikedPlaces.tsx (3)
apps/web/app/_apis/queries/place.ts (1)
  • usePlaceQueries (11-17)
apps/web/app/_components/RankingPlaceList/RankingPlaceList.tsx (1)
  • RankingPlaceList (13-28)
apps/web/app/QueryClientProvider.tsx (1)
  • QueryProvider (40-57)
apps/web/app/_apis/services/category.ts (2)
apps/web/app/_apis/schemas/category.ts (2)
  • Category (10-10)
  • CategorySchema (4-8)
apps/web/app/_constants/path.ts (1)
  • API_PATH (3-6)
apps/web/app/_mocks/handlers/placeHandlers.ts (2)
apps/web/app/_constants/path.ts (1)
  • API_PATH (3-6)
apps/web/app/_mocks/data/place.ts (1)
  • RankingPlaces (1-38)
packages/ui/src/components/Header/Header.tsx (2)
packages/ui/src/components/Header/Header.stories.tsx (3)
  • Box (39-46)
  • Box (51-55)
  • Box (26-34)
packages/ui/src/components/Text/Text.stories.tsx (1)
  • div (63-76)
apps/web/app/_apis/services/place.ts (2)
apps/web/app/_apis/schemas/place.ts (3)
  • RankingPlaceSort (12-12)
  • RankingPlace (20-20)
  • RankingPlaceSchema (14-17)
apps/web/app/_constants/path.ts (1)
  • API_PATH (3-6)
apps/web/app/_components/Categories/CategoryItem.tsx (3)
apps/web/app/_apis/schemas/category.ts (1)
  • Category (10-10)
apps/web/app/_constants/path.ts (1)
  • CLIENT_PATH (8-17)
packages/ui/src/components/Icon/Icon.stories.tsx (2)
  • Column (28-40)
  • type (30-38)
apps/web/app/_components/RankingPlaceList/RankingPlaceList.tsx (1)
apps/web/app/_apis/schemas/place.ts (1)
  • RankingPlace (20-20)
apps/web/app/_apis/schemas/place.ts (1)
apps/web/app/_apis/schemas/category.ts (1)
  • CategorySchema (4-8)
apps/web/app/_components/SubTitle/SubTitle.tsx (3)
packages/ui/src/components/Header/Header.stories.tsx (1)
  • Box (26-34)
packages/ui/src/components/Text/Text.stories.tsx (1)
  • div (63-76)
packages/ui/src/components/Icon/assets/icons/navigation/heart.tsx (1)
  • props (3-34)
apps/web/app/_components/BottomNavigation/TabItem.tsx (4)
apps/web/app/_constants/path.ts (1)
  • CLIENT_PATH (8-17)
packages/ui/src/utils/cn.ts (1)
  • cn (19-21)
packages/ui/src/components/Icon/assets/icons/navigation/map.tsx (1)
  • props (3-22)
packages/ui/src/components/Icon/assets/icons/navigation/home.tsx (1)
  • props (4-23)
apps/web/app/_components/PlaceListItem/PlaceListItem.tsx (6)
apps/web/app/_apis/schemas/place.ts (1)
  • BasePlace (19-19)
packages/ui/src/utils/cn.ts (1)
  • cn (19-21)
packages/ui/src/components/Icon/assets/icons/header/markerWithMap.tsx (1)
  • props (3-49)
packages/ui/src/components/Icon/assets/icons/navigation/map.tsx (1)
  • props (3-22)
packages/ui/src/components/Icon/assets/icons/marker.tsx (1)
  • props (3-34)
packages/ui/src/components/Icon/assets/icons/navigation/home.tsx (1)
  • props (4-23)
apps/web/app/page.tsx (4)
apps/web/app/HydrationBoundaryPage.tsx (1)
  • HydrationBoundaryPage (29-44)
apps/web/app/_apis/queries/category.ts (1)
  • useCategoryQueries (11-17)
apps/web/app/_apis/queries/place.ts (1)
  • usePlaceQueries (11-17)
apps/web/app/_constants/path.ts (1)
  • CLIENT_PATH (8-17)
apps/web/app/_constants/path.ts (1)
apps/web/app/_apis/schemas/place.ts (1)
  • RankingPlaceSort (12-12)
apps/web/app/HydrationBoundaryPage.tsx (1)
apps/web/app/QueryClientProvider.tsx (1)
  • QueryProvider (40-57)
apps/web/app/_apis/queries/place.ts (3)
apps/web/app/_apis/schemas/place.ts (1)
  • RankingPlaceSort (12-12)
apps/web/app/_apis/services/place.ts (1)
  • getRankingPlaces (9-14)
apps/web/app/QueryClientProvider.tsx (1)
  • QueryProvider (40-57)
apps/web/app/_apis/queries/category.ts (3)
apps/web/app/_apis/services/category.ts (1)
  • getCategories (5-8)
apps/web/app/QueryClientProvider.tsx (1)
  • QueryProvider (40-57)
apps/web/app/_mocks/handlers/categoryHandlers.ts (1)
  • HttpResponse (12-14)
packages/ui/src/components/SearchBar/SearchBar.tsx (2)
packages/ui/src/components/Text/Text.tsx (1)
  • restProps (115-138)
packages/ui/src/components/Icon/assets/icons/arrowRight.tsx (1)
  • props (3-24)
apps/web/app/_mocks/handlers/index.ts (2)
apps/web/app/_mocks/handlers/categoryHandlers.ts (2)
  • CategoryHandlers (11-15)
  • path (7-9)
apps/web/app/_mocks/handlers/placeHandlers.ts (1)
  • PlaceHandlers (11-18)
packages/ui/src/components/Chip/Chip.tsx (3)
packages/ui/src/polymorphics.ts (1)
  • PolymorphicComponentProps (9-12)
packages/ui/src/components/Icon/assets/icons/menu/chinese.tsx (1)
  • props (3-205)
packages/ui/src/components/Icon/assets/icons/tag/blingBling.tsx (1)
  • props (3-41)
packages/ui/src/components/Chip/Chip.stories.tsx (2)
packages/ui/src/components/Chip/Chip.tsx (1)
  • Chip (45-86)
packages/ui/src/components/Icon/Icon.stories.tsx (2)
  • Column (28-40)
  • type (30-38)
🔇 Additional comments (28)
packages/ui/src/components/Header/Header.tsx (1)

32-34: h1 적용으로 시맨틱/A11y 개선은 좋습니다. 다만 페이지 내 h1 중복만 확인해주세요.

헤더 타이틀을 h1으로 명시해 준 점은 적절합니다. 다만 페이지 본문 내 다른 h1과의 중복 여부(특히 섹션 헤더나 카드 컴포넌트 내 제목 등)를 한번 점검해 주세요. 중복 시 스크린 리더 내 탐색 경험이 저하될 수 있습니다.

apps/web/package.json (2)

24-24: keen-slider 의존성 추가 적절

배너 슬라이더 도입에 필요한 최소 의존성 추가로 보이며, Next.js 앱 라우터 환경에서도 문제 없습니다. 아래 CSS 중복 임포트만 정리하면 됩니다.


29-29: zod v4.0.17이 정상 배포됨 — 버전 수정 불필요

검증 결과, npm view zod version 명령으로 zod 최신 버전이 4.0.17임이 확인되었고, npm view zod@4로도 4.0.0부터 4.0.17까지 정상 조회됩니다.
따라서 apps/web/package.json

-    "zod": "^4.0.17",
+    "zod": "^4.0.17",

는 유효한 설정이며, 변경할 필요가 없습니다.

Likely an incorrect or invalid review comment.

apps/web/app/_components/Categories/index.tsx (1)

1-1: 배럴 파일 구성 LGTM

컴포넌트 공개 경로를 단일화하여 사용성/가독성이 좋아졌습니다.

apps/web/app/layout.tsx (1)

34-34: 배경 컬러 톤 변경 LGTM

전반 UI 톤과 메인 화면 구성에 맞춘 배경 컬러 변경으로 시각적 일관성이 좋아졌습니다.

packages/ui/src/components/Divider/index.tsx (1)

1-1: 배럴 re-export 추가 LGTM

공용 API 표면을 정리하는 방향으로 적절합니다. 다른 컴포넌트들과 일관된 import 경로 제공에 도움이 됩니다.

apps/web/app/_components/SubTitle/index.tsx (1)

1-1: 배럴 re-export 추가 LGTM

폴더 배럴을 통한 공개 API 정리가 일관되고, 사용성 향상에 유효합니다.

apps/web/app/_components/RankingPlaceList/MostViewsPlaces/index.tsx (1)

1-1: 배럴 re-export 추가 LGTM

상위 배럴(예: RankingPlaceList/index.tsx)와의 조합을 고려했을 때 import 경로 단순화에 도움이 됩니다.

packages/ui/src/components/Icon/index.tsx (1)

2-2: 타입 전용 재수출 적절합니다

export type 사용으로 런타임 번들에 영향 없이 타입만 노출되어 트리 셰이킹과 번들 최적화에 유리합니다. 아이콘 관련 타입을 외부에서 활용하기 좋아졌습니다.

apps/web/app/_components/PlaceListItem/index.tsx (1)

1-1: 배럴(인덱스) 재노출 LGTM

디렉터리 단위 import가 가능해져 사용성이 좋아졌습니다. 네이밍 일관성도 유지됩니다.

apps/web/app/_components/BottomNavigation/index.tsx (1)

1-1: 배럴(인덱스) 재노출 LGTM

컴포넌트 공개 표면을 단일 진입점으로 모아 일관성을 높였습니다.

apps/web/app/_components/RankingPlaceList/MostViewsPlaces/MostViewsPlaces.tsx (1)

1-1: 'use client' 지시자 적절

클라이언트 훅(useSuspenseQuery) 사용을 위해 필요한 선언입니다.

apps/web/app/_components/RankingPlaceList/MostLikedPlaces/index.tsx (1)

1-1: 배럴(인덱스) 재노출 LGTM

디렉터리 공개 패턴을 다른 컴포넌트들과 일관되게 맞췄습니다.

apps/web/app/_components/RankingPlaceList/MostLikedPlaces/MostLikedPlaces.tsx (1)

7-12: 패턴 일관성 좋습니다.

Query Factory 패턴과 Suspense 연동이 깔끔하게 적용되어 있고, 재사용 컴포넌트(RankingPlaceList) 구성도 적절합니다.

packages/ui/src/components/SearchBar/SearchBar.tsx (1)

6-12: className 주입 지원 추가 굿 — 사용 편의성 향상

  • props에 className을 열어주고, cn의 마지막 인자로 전달한 방식이 적절합니다. 외부에서 스타일 오버라이드/확장하기 용이합니다.
  • 기존 동작 변화도 없고, 타입 선언도 명확합니다.

Also applies to: 17-24

apps/web/app/_apis/services/place.ts (1)

9-14: zod로 런타임 검증까지 포함된 서비스 함수 구성 적절

  • axios로 가져온 응답을 RankingPlaceSchema.array().parse로 검증하는 흐름이 안정적입니다.
  • API_PATH.RANKING(sort) 설계도 타입 안전하게 연결되어 있습니다.
apps/web/app/_components/RankingPlaceList/index.tsx (1)

1-3: 배럴(Barrel) export로 공개 API 정리된 점 좋습니다

  • 소비자 입장에서 import 경로 단순화에 도움이 됩니다.
apps/web/app/_components/Categories/CategoryItem.tsx (1)

7-22: 컴포넌트 구조와 타입 결합 적절 — UI/경로 상수 연계도 명확

  • Category 타입의 필드를 그대로 받아 사용하는 형태가 간결합니다.
  • CLIENT_PATH와의 연결이 명시적이고, Icon/Text 조합 및 레이아웃도 일관적입니다.
apps/web/app/_components/RankingPlaceList/RankingPlaceList.tsx (2)

13-28: 리스트 렌더링 및 마지막 아이템 border 제어 로직 깔끔

  • place.placeId를 키로 사용하는 점, 마지막 아이템만 border 제거하는 조건 모두 적절합니다.
  • SubTitle과의 결합도 명확합니다.

17-25: PlaceListItem은 <li> 요소를 반환하므로 시맨틱이 유효합니다.

PlaceListItem 내부 구현을 확인한 결과, Column as={'li'}로 루트 JSX가 <li>로 렌더링됩니다.

  • 파일: apps/web/app/_components/PlaceListItem/PlaceListItem.tsx (Column as='li')

따라서 <ul> 안에 PlaceListItem을 직접 렌더링해도 시맨틱 위반이 아니며, 별도로 <li>로 감싸지 않아도 됩니다.

apps/web/app/_components/BottomNavigation/BottomNavigation.tsx (1)

13-30: 구성 및 구현 전반 LGTM

탭 구성, 레이아웃 및 스타일링이 명확하고 일관적입니다.

apps/web/app/_mocks/handlers/index.ts (1)

1-4: MSW 핸들러 통합 및 경로 정리 LGTM

카테고리/장소 핸들러의 합치기가 명확하며, 확장에 유리합니다.

apps/web/app/_components/PlaceListItem/PlaceListItem.tsx (1)

19-20: 카테고리 아이콘 폴백 키 검증 요청

'logo'가 실제 아이콘 키에 존재하는지 확인 부탁드립니다. 미존재 시 빌드 타임/런타임 오류가 발생할 수 있어 공통 폴백(예: 'marker')을 고려해 주세요.

packages/ui/src/components/Chip/Chip.stories.tsx (1)

11-31: 아이콘 키 정의 확인 완료

fingerUp, calculator, blingBling, waiter가 모두 iconMap의 키로 정의되어 있으며
export type IconType = keyof typeof iconMap에 포함되어 있어 타입/런타임 미ismatch가 없습니다.

• packages/ui/src/components/Icon/IconMap.ts (lines 74–77): iconMap에 각 키 매핑
• packages/ui/src/components/Icon/IconMap.ts (line 100): IconType에 포함됨

apps/web/app/_apis/queries/place.ts (1)

5-9: 쿼리 키 구조와 옵션 팩토리 패턴이 깔끔하게 정리되어 있습니다

  • 키 스코프가 ['place', 'ranking', sort]로 분리되어 캐싱/무효화 전략에 유리합니다.
  • queryOptions 사용으로 호출부 일관성이 확보됩니다.

Also applies to: 11-17

apps/web/app/HydrationBoundaryPage.tsx (1)

31-38: 새 prefetch 콜백 기반 API가 간결하고 의도가 명확합니다

  • 서버에서 QueryClient를 만들고 dehydrate하는 흐름이 표준적이며, 호출부 유연성이 좋아졌습니다.
  • 주석/예제가 현재 사용 방식과 일치합니다.
apps/web/app/_mocks/data/place.ts (2)

1-38: 모킹 데이터 구성 적절함

UI 데모 목적의 최소 필드 구성이 스키마와 일관되며, 카테고리/태그 아이콘 키도 명확합니다. MSW 핸들러와의 연결을 고려한 형태로 LGTM입니다.


1-38: 모든 iconKey가 유효합니다
다음 키들이 아이콘 에셋 디렉토리에서 정상적으로 확인되었습니다:

  • korean
  • meat
  • bunsik
  • blingBling
  • fingerUp
  • calculator
  • chinese

Comment on lines +5 to +8
id: z.number().transform(String),
name: z.string(),
iconKey: z.enum(IconList),
})
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

z.enum에 동적 배열(IconList) 전달은 TS 타입 오류 발생 가능성이 큽니다

IconListstring[]으로 추론되어 z.enum의 튜플 요구사항을 만족하지 못합니다. 현재 구현은 빌드 타임에 TS 에러가 날 가능성이 높습니다.

아래처럼 z.custom<IconType>로 검증을 수행하면 런타임 검증과 타입 추론(IconType)을 동시에 만족합니다.

-import { z } from 'zod'
-import { IconList } from '@repo/ui/components/Icon/IconMap'
+import { z } from 'zod'
+import { IconList } from '@repo/ui/components/Icon/IconMap'
+import type { IconType } from '@repo/ui/components/Icon'
 
 export const CategorySchema = z.object({
   id: z.number().transform(String),
   name: z.string(),
-  iconKey: z.enum(IconList),
+  iconKey: z.custom<IconType>(
+    (v) => typeof v === 'string' && (IconList as readonly string[]).includes(v),
+    { message: '유효하지 않은 iconKey 입니다.' },
+  ),
 })

참고 대안:

  • UI 쪽에서 IconListreadonly [IconType, ...IconType[]] 튜플로 보장하도록 내보내면 z.enum(IconList) 사용이 가능합니다. 다만 현재 Object.keys 기반 구현에서는 튜플 보장이 어려워 위 방식이 안전합니다.
📝 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
id: z.number().transform(String),
name: z.string(),
iconKey: z.enum(IconList),
})
// apps/web/app/_apis/schemas/category.ts
import { z } from 'zod'
import { IconList } from '@repo/ui/components/Icon/IconMap'
import type { IconType } from '@repo/ui/components/Icon'
export const CategorySchema = z.object({
id: z.number().transform(String),
name: z.string(),
iconKey: z.custom<IconType>(
(v) => typeof v === 'string' && (IconList as readonly string[]).includes(v),
{ message: '유효하지 않은 iconKey 입니다.' },
),
})
🤖 Prompt for AI Agents
In apps/web/app/_apis/schemas/category.ts around lines 5 to 8, z.enum(IconList)
is unsafe because IconList is inferred as string[] not a readonly tuple and will
cause TypeScript errors; replace the z.enum usage with z.custom<IconType> that
performs a runtime check asserting the value is a string and is included in
IconList (use IconList.includes(value)) and ensure IconType is imported/defined
so z.custom is correctly typed; alternatively export IconList as a readonly
tuple on the UI side if you prefer to keep z.enum.

Comment on lines +40 to +61
let timeout: ReturnType<typeof setTimeout>
let mouseOver = false
function clearNextTimeout() {
clearTimeout(timeout)
}
function nextTimeout() {
clearTimeout(timeout)
if (mouseOver) return
timeout = setTimeout(() => {
slider.next()
}, 2000)
}
slider.on('created', () => {
slider.container.addEventListener('mouseover', () => {
mouseOver = true
clearNextTimeout()
})
slider.container.addEventListener('mouseout', () => {
mouseOver = false
nextTimeout()
})
nextTimeout()
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

타이머 초기화 안전성 및 이벤트 리스너 정리 누락 — 메모리 누수/경합 방지 리팩터 권장

  • timeout이 초기화 전 clearTimeout 대상이 될 수 있습니다. null 가드가 안전합니다.
  • addEventListener로 등록한 핸들러는 destroyed 시점에 removeEventListener로 해제해야 합니다.
  • mouseout 대신 mouseleave(버블링 없음)를 사용하면 자식 엘리먼트로 이동 시 불필요한 pause/resume 이벤트가 줄어듭니다.

아래와 같이 보완을 제안합니다.

-        let timeout: ReturnType<typeof setTimeout>
+        let timeout: ReturnType<typeof setTimeout> | null = null
         let mouseOver = false
         function clearNextTimeout() {
-          clearTimeout(timeout)
+          if (timeout !== null) {
+            clearTimeout(timeout)
+            timeout = null
+          }
         }
         function nextTimeout() {
-          clearTimeout(timeout)
+          clearNextTimeout()
           if (mouseOver) return
-          timeout = setTimeout(() => {
+          timeout = setTimeout(() => {
             slider.next()
           }, 2000)
         }
-        slider.on('created', () => {
-          slider.container.addEventListener('mouseover', () => {
-            mouseOver = true
-            clearNextTimeout()
-          })
-          slider.container.addEventListener('mouseout', () => {
-            mouseOver = false
-            nextTimeout()
-          })
-          nextTimeout()
-        })
+        const onMouseEnter = () => {
+          mouseOver = true
+          clearNextTimeout()
+        }
+        const onMouseLeave = () => {
+          mouseOver = false
+          nextTimeout()
+        }
+        slider.on('created', () => {
+          slider.container.addEventListener('mouseenter', onMouseEnter)
+          slider.container.addEventListener('mouseleave', onMouseLeave)
+          nextTimeout()
+        })
         slider.on('dragStarted', clearNextTimeout)
         slider.on('animationEnded', nextTimeout)
         slider.on('updated', nextTimeout)
+        slider.on('destroyed', () => {
+          slider.container.removeEventListener('mouseenter', onMouseEnter)
+          slider.container.removeEventListener('mouseleave', onMouseLeave)
+          clearNextTimeout()
+        })

Also applies to: 63-66

🤖 Prompt for AI Agents
In apps/web/app/_components/Banner/Banner.tsx around lines 40 to 61 (also
applies to 63-66): the current timer and mouse event handling can cause unsafe
clearTimeout calls, missing listener cleanup and noisy mouseout events. Guard
clearTimeout by checking timeout !== undefined/null before clearing, replace
'mouseout' with 'mouseleave' to avoid bubbling issues, store the event handler
functions in variables so you can call removeEventListener with the exact
references, and in the slider destroyed/detached handler clear the timeout (with
a null guard) and remove the registered mouseenter/mouseleave listeners to
prevent leaks and race conditions.

Comment on lines +27 to +31
<Icon
type={icon}
size={iconSize}
color={active ? '--color-main' : '--color-gray-200'}
/>
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Icon color에 CSS 변수를 직접 전달할 때의 처리 로직 확인 필요

현재 color={ '--color-main' } 형태로 전달하고 있는데, 아이콘 컴포넌트가 이를 var(--color-main)로 래핑하지 않는다면 브라우저가 유효한 색상으로 인식하지 못합니다. 위 제안처럼 var(--color-*) 형태로 넘기거나, Icon 내부에서 -- 프리픽스를 감지해 var(...)로 변환하는 로직이 있는지 확인 부탁드립니다.

레포에서 Icon 컴포넌트의 color 처리 로직을 확인하는 스크립트:


🏁 Script executed:

#!/bin/bash
set -e

echo "Icon 컴포넌트에서 color 처리 로직 탐색"
rg -n -C2 "export.*Icon(?!List)" packages/ui/src/components/Icon || true
rg -n -C2 "color\??:\s*string" packages/ui/src/components/Icon || true
rg -n -C2 "var\\(--" packages/ui/src/components/Icon || true

echo "아이콘 에셋에서 color prop 사용 패턴 탐색(fill={color})"
rg -n -C2 "fill=\{color" packages/ui/src/components/Icon/assets/icons || true

Length of output: 12403


Icon 컴포넌트가 CSS 변수 문자열을 처리하지 않으므로 --color-* 대신 반드시 var(--color-*) 형태로 전달하거나, Icon 내부에서 -- 접두사를 감지해 var(...)로 감싸주는 로직을 추가해야 합니다.

  • TabItem.tsx (apps/web/app/_components/BottomNavigation/TabItem.tsx:27–31)
    color={active ? '--color-main' : '--color-gray-200'}
    • 즉시 color={active ? 'var(--color-main)' : 'var(--color-gray-200)'}로 변경

  • 또는 Icon 컴포넌트 수정 (packages/ui/src/components/Icon/**/*.tsx)
    color prop 값이 --로 시작하면 내부에서 var(${color})로 자동 변환하는 유틸 로직 추가

위 두 가지 중 하나를 선택해 적용해주세요.

🤖 Prompt for AI Agents
apps/web/app/_components/BottomNavigation/TabItem.tsx lines 27–31: the Icon
component is receiving raw CSS variable names like "--color-main" which it
doesn't handle; update the color prop to pass CSS variables using var(...) by
changing color={active ? '--color-main' : '--color-gray-200'} to color={active ?
'var(--color-main)' : 'var(--color-gray-200)'}; alternatively (if you prefer
central handling) modify the Icon component in packages/ui to detect values
starting with "--" and wrap them as var(<value>) before applying, but for a
minimal fix update TabItem to use var(...) as shown.

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

Actionable comments posted: 1

♻️ Duplicate comments (3)
packages/ui/src/components/Chip/Chip.tsx (3)

56-61: 클릭 토글이 onToggle 유무에 종속되고, 소비자 onClick이 무시됩니다 → 내부 토글은 항상 수행 + 외부 onClick 병합 호출로 수정 권장

  • 현재 구현은 onToggle이 없으면 isActive가 토글되지 않아 문서(라인 26)와 불일치합니다.
  • 또한 {...restProps} 뒤에 onClick을 배치하여 내부 onClick이 소비자의 onClick을 덮어쓰고, 소비자 onClick이 호출되지 않습니다.

아래처럼 내부 토글을 항상 수행하고, 외부 onClick과 onToggle을 병합 호출하도록 수정해 주세요. 이 변경은 라인 53-55의 사전 분해(별도 코멘트)와 함께 적용되어야 합니다.

-  const onClick = () => {
-    if (onToggle) {
-      onToggle()
-      setIsActive((prev) => !prev)
-    }
-  }
+  // 내부 토글은 항상 수행하고, 외부 onClick과 onToggle은 병합 호출
+  const handleClick: MouseEventHandler = (e) => {
+    // 외부 onClick 먼저 호출
+    onClickProp?.(e)
+    // 내부 상태 토글
+    setIsActive((prev) => !prev)
+    // onToggle 콜백은 선택적으로 호출
+    onToggle?.()
+  }
-      {...restProps}
-      onClick={onClick}
+      aria-pressed={isActive}
+      {...rest}
+      onClick={handleClick}

Also applies to: 77-78


2-2: 타입 보강: MouseEventHandler import 추가

위 병합 구현에 필요한 MouseEventHandler 타입을 import 해주세요.

-import type { ElementType, JSX, PropsWithChildren } from 'react'
+import type { ElementType, JSX, PropsWithChildren, MouseEventHandler } from 'react'

53-55: 외부 onClick 분리하여 병합 호출 준비

소비자 onClick을 병합 호출하려면 restProps에서 onClick을 분리해야 합니다. 아래와 같이 분해해 주세요.

   const Component = as || 'div'
   const [isActive, setIsActive] = useState(false)
+
+  // 외부 onClick과 내부 토글 병합을 위해 분리
+  const { onClick: onClickProp, ...rest } = restProps as {
+    onClick?: MouseEventHandler
+  } & typeof restProps
🧹 Nitpick comments (5)
packages/ui/src/components/Chip/Chip.tsx (2)

63-79: a11y 강화: button일 때 type="button" 지정, div 기본값 유지 시 역할/키보드 접근성 보강 권장

  • 폴리모픽으로 button으로 렌더링되는 경우, 의도치 않은 form submit을 막기 위해 type="button"을 명시하는 것이 안전합니다.
  • 기본이 'div'인 현재 설계에서는 클릭 가능한 역할임을 노출(role="button")하고 키보드(Enter/Space) 조작을 지원하는 것이 접근성 측면에서 필요합니다. 최소한 aria-pressed는 이미 위 코멘트 diff에 포함했습니다.

type 지정은 아래처럼 간단히 추가할 수 있습니다.

     <Component
       className={cn(
         'ui:w-fit',
         'ui:bg-gray-50',
         'ui:rounded-full',
         'ui:flex',
         'ui:gap-1',
         'ui:px-2.5 ui:py-1',
         'ui:items-center',
         'ui:border-2 ui:border-gray-50',
         { 'ui:border-blue': isActive },
         className,
       )}
-      {...restProps}
-      onClick={onClick}
+      type={Component === 'button' ? 'button' : undefined}
+      {...rest}
+      onClick={handleClick}
     >

div 기본을 유지하면서 키보드 접근성까지 보완하려면, 추가로 아래 속성/핸들러를 권장합니다.

+      role={Component === 'button' ? undefined : 'button'}
+      tabIndex={Component === 'button' ? undefined : 0}
+      onKeyDown={(e) => {
+        if (e.key === 'Enter' || e.key === ' ') {
+          e.preventDefault()
+          handleClick(e as any)
+        }
+      }}

29-29: 문서와 구현은 일치하나(기본값: 'div'), 상호작용 컴포넌트의 기본 태그로는 button이 더 안전할 수 있음

현재 JSDoc과 구현 모두 기본을 'div'로 두고 있어 일관성은 좋습니다. 다만 상호작용/토글 컴포넌트 특성상 기본을 'button'으로 두면 a11y, 키보드 접근성, 폼 내 오동작 방지(type='button')까지 한 번에 해결됩니다. 현 설계를 유지하더라도 앞선 코멘트의 role/tabIndex/onKeyDown 보강은 권장드립니다.

apps/web/app/page.tsx (3)

20-24: 프리패치 병렬화로 SSR 지연 줄이기

세 개의 쿼리를 순차로 prefetch 중입니다. Promise.all로 병렬화하면 TTFB를 줄이고 서버 자원 사용도 효율화됩니다.

-      prefetch={async (queryClient) => {
-        await queryClient.prefetchQuery(useCategoryQueries.list())
-        await queryClient.prefetchQuery(usePlaceQueries.rankingList('likes'))
-        await queryClient.prefetchQuery(usePlaceQueries.rankingList('views'))
-      }}
+      prefetch={async (queryClient) => {
+        await Promise.all([
+          queryClient.prefetchQuery(useCategoryQueries.list()),
+          queryClient.prefetchQuery(usePlaceQueries.rankingList('likes')),
+          queryClient.prefetchQuery(usePlaceQueries.rankingList('views')),
+        ])
+      }}

28-34: 콘텐츠 스크롤 안정화: flex-1, min-h-0 추가 권장

헤더/바텀 네비가 고정된 레이아웃에서 중간 영역에 스크롤을 주려면, 해당 컨테이너가 flex 컨텍스트에서 공간을 차지하도록 flex-1과 min-h-0을 부여하는 것이 안전합니다. Column이 flex 컨테이너라면 아래와 같이 적용을 권장합니다.

-      <Column className={'scrollbar-hide gap-4 overflow-auto'}>
+      <Column className={'scrollbar-hide gap-4 overflow-auto flex-1 min-h-0'}>

30-30: 배너 contents 하드코딩 제거 (nit)

더미 숫자 배열은 의도가 불명확합니다. 상수로 분리하거나 데이터 소스에서 주입받도록 하면 교체/테스트가 쉬워집니다.

-        <Banner contents={[1, 2, 3, 4]} />
+        <Banner contents={HOME_BANNER_CONTENTS} />

추가(이 파일 상단 인근에 배치 가능):

// 페이지 전용 샘플/상수
const HOME_BANNER_CONTENTS = [1, 2, 3, 4] as const
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 3a899cc and 381ec61.

📒 Files selected for processing (3)
  • apps/web/app/_apis/schemas/place.ts (1 hunks)
  • apps/web/app/page.tsx (1 hunks)
  • packages/ui/src/components/Chip/Chip.tsx (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/web/app/_apis/schemas/place.ts
🧰 Additional context used
🧬 Code Graph Analysis (2)
apps/web/app/page.tsx (5)
apps/web/app/HydrationBoundaryPage.tsx (2)
  • HydrationBoundaryPage (29-44)
  • queries (38-58)
apps/web/app/_apis/queries/category.ts (1)
  • useCategoryQueries (11-17)
apps/web/app/_apis/queries/place.ts (1)
  • usePlaceQueries (11-17)
apps/web/app/_constants/path.ts (1)
  • CLIENT_PATH (8-17)
apps/web/app/layout.tsx (1)
  • RootLayout (21-43)
packages/ui/src/components/Chip/Chip.tsx (2)
packages/ui/src/polymorphics.ts (1)
  • PolymorphicComponentProps (9-12)
packages/ui/src/components/Chip/Chip.stories.tsx (4)
  • chipType (35-43)
  • Flex (26-45)
  • console (39-41)
  • Flex (15-22)
⏰ 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: chromatic-deployment
🔇 Additional comments (2)
packages/ui/src/components/Chip/Chip.tsx (1)

12-14: Props 마이그레이션 완료: chipTypeicon/label 사용흔적 없음

  • *.ts/*.tsx 전체 검색 결과, chipType prop은 전혀 발견되지 않음
  • 스토리(packages/ui/.../Chip.stories.tsx)와 앱 코드(apps/web/.../PlaceListItem.tsx) 모두 이미 icon/label 사용으로 마이그레이션 완료

추가 조치 불필요합니다.

apps/web/app/page.tsx (1)

26-36: 전반적으로 깔끔한 적용: HydrationBoundaryPage + 프리패치 + UI 구성 LGTM

HydrationBoundaryPage의 prefetch 콜백 활용, QueryFactory(queryOptions) 기반 프리패치, 메인 화면 컴포지션(헤더/검색/카테고리/배너/랭킹/바텀 네비) 구성 모두 일관되고 명확합니다.

Comment on lines +31 to +33
<MostLikedPlaces />
<Divider />
<MostViewsPlaces />
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

랭킹 섹션 접근성 확인(제목/aria-label)

MostLikedPlaces / MostViewsPlaces가 연속 배치되어 있어 보조기기에서 섹션 구분을 위해 heading(예: h2/h3) 또는 aria-label이 노출되는지 점검이 필요합니다. 아래 스크립트로 컴포넌트 내에 heading/aria 속성이 존재하는지 빠르게 확인할 수 있습니다.


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 랭킹 컴포넌트 파일 탐색
fd -a -t f -E node_modules -E dist -E build -E .next \
  -g '**/Most*Places*.tsx' | while read -r f; do
  echo "=== Checking: $f ==="
  # heading/aria 존재 여부 확인
  rg -n -C2 -e '<h[2-6]\b' -e 'role\s*=\s*["'"'"']heading["'"'"']' \
     -e 'aria-label\s*=' -e 'aria-labelledby\s*=' "$f" || true
done

Length of output: 1041


접근성 강화: 랭킹 섹션에 헤딩(h2~h6) 또는 aria-label/aria-labelledby 속성 추가 필요

스크립트 확인 결과, 두 컴포넌트에 <h2>~<h6> 태그나 aria-label/aria-labelledby 속성이 전혀 없어 보조기기에서 섹션을 구분할 수 없습니다. 아래 파일들에 반드시 제목 또는 ARIA 속성을 추가해 주세요.

  • apps/web/app/_components/RankingPlaceList/MostLikedPlaces/MostLikedPlaces.tsx
  • apps/web/app/_components/RankingPlaceList/MostViewsPlaces/MostViewsPlaces.tsx

예시 변경안:

- <MostLikedPlaces />
+ <section aria-labelledby="most-liked-places-title">
+   <h2 id="most-liked-places-title">가장 인기 있는 장소</h2>
+   <MostLikedPlaces />
+ </section>

또는

- <MostViewsPlaces />
+ <MostViewsPlaces aria-label="가장 많이 조회된 장소" />
📝 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
<MostLikedPlaces />
<Divider />
<MostViewsPlaces />
<section aria-labelledby="most-liked-places-title">
<h2 id="most-liked-places-title">가장 인기 있는 장소</h2>
<MostLikedPlaces />
</section>
<Divider />
<MostViewsPlaces aria-label="가장 많이 조회된 장소" />
🤖 Prompt for AI Agents
In apps/web/app/page.tsx around lines 31-33 and specifically in the components
apps/web/app/_components/RankingPlaceList/MostLikedPlaces/MostLikedPlaces.tsx
and
apps/web/app/_components/RankingPlaceList/MostViewsPlaces/MostViewsPlaces.tsx,
the rendered ranking sections lack any heading (h2–h6) or
aria-label/aria-labelledby, which prevents assistive tech from distinguishing
sections; fix by adding a semantic heading element (e.g., an <h2> with
appropriate text) at the top of each component or, if visual design prevents
visible headings, add an aria-label or aria-labelledby on the top-level
container (or role="region" with aria-label) with a clear descriptive label like
"Most liked places" / "Most viewed places" so screen readers can identify the
sections.

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

Labels

⚙️ Chore 환경 셋팅 ✨ Feature 기능 구현 🎨 Style 스타일 관련

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature] 메인 화면 ui 구현

2 participants