Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
918ab33
feat: Layout 컴포넌트의 Column 너비를 450px에서 전체 너비로 변경
leeleeleeleejun Aug 18, 2025
9235f37
feat: Header 컴포넌트에 className prop 추가 및 적용
leeleeleeleejun Aug 18, 2025
757c324
feat: categories 하위 폴더 Template 컴포넌트 추가
leeleeleeleejun Aug 18, 2025
659a5e7
feat: HeaderBackButton 컴포넌트 추가
leeleeleeleejun Aug 18, 2025
cfd5bcd
feat: CategoryItem 및 RowCategories 컴포넌트 추가
leeleeleeleejun Aug 18, 2025
24f133f
feat: CategoryDetailPage 및 페이지 컴포넌트 추가
leeleeleeleejun Aug 18, 2025
d19d9d5
feat: categories/[id]/_components/index 파일 추가
leeleeleeleejun Aug 18, 2025
caea68e
refactor: API_PATH의 PLACES 관련 path 구조 변경
leeleeleeleejun Aug 18, 2025
94a6305
refactor: RankingPlaces 데이터 타입 BasePlaces로 통일
leeleeleeleejun Aug 18, 2025
38891f5
refactor: 일관성을 위해 rankingList 쿼리를 byRanking으로 변경
leeleeleeleejun Aug 18, 2025
9283547
feat: 카테고리별 장소 조회 기능 및 관련 컴포넌트 추가
leeleeleeleejun Aug 18, 2025
b91df60
refactor: 일관성을 위해 MostLikedPlaces를 MostLikesPlaces로 변경
leeleeleeleejun Aug 18, 2025
2eb50eb
refactor: CategoryDetailPage에서 activeCategory 이름에 패딩 추가
leeleeleeleejun Aug 18, 2025
ea337a7
refactor: RowCategories 컴포넌트에 pointer-events-none 클래스 추가
leeleeleeleejun Aug 18, 2025
d1467df
feat: prefetchQuery에 usePlaceQueries.byCategory 추가
leeleeleeleejun Aug 18, 2025
c4c19a2
feat: HydrationBoundaryPage의 jsDoc example 수정
leeleeleeleejun Aug 18, 2025
d256334
fix: remove slash to API_PATH.BY_CATEGORY
leeleeleeleejun Aug 18, 2025
8b7abc5
feat: export Places component from index.tsx
leeleeleeleejun Aug 18, 2025
4b96f5b
fix: activeCategory 조회 시 filter에서 find로 변경
leeleeleeleejun Aug 18, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/web/app/HydrationBoundaryPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { ReactNode } from 'react'
* <HydrationBoundaryPage
* prefetch={async (queryClient) => {
* await queryClient.prefetchQuery(useCategoryQueries.list())
* await queryClient.prefetchQuery(usePlaceQueries.rankingList('likes'))
* await queryClient.prefetchQuery(usePlaceQueries.byRanking('likes'))
* }}
* >
* <Categories />
Expand Down
18 changes: 13 additions & 5 deletions apps/web/app/_apis/queries/place.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,25 @@
import { queryOptions } from '@tanstack/react-query'
import { RankingPlaceSort } from '@/_apis/schemas/place'
import { getRankingPlaces } from '@/_apis/services/place'
import { getPlacesByCategory, getPlacesByRanking } from '@/_apis/services/place'

export const PlaceQueryKeys = {
all: () => ['place'] as const,
rankingList: (sort: RankingPlaceSort) =>
byRanking: (sort: RankingPlaceSort) =>
[...PlaceQueryKeys.all(), 'ranking', sort] as const,
byCategory: (id: string) =>
[...PlaceQueryKeys.all(), 'category', id] as const,
}

export const usePlaceQueries = {
rankingList: (sort: RankingPlaceSort) =>
byRanking: (sort: RankingPlaceSort) =>
queryOptions({
queryKey: PlaceQueryKeys.rankingList(sort),
queryFn: () => getRankingPlaces(sort),
queryKey: PlaceQueryKeys.byRanking(sort),
queryFn: () => getPlacesByRanking(sort),
}),

byCategory: (id: string) =>
queryOptions({
queryKey: PlaceQueryKeys.byCategory(id),
queryFn: () => getPlacesByCategory(id),
}),
}
6 changes: 0 additions & 6 deletions apps/web/app/_apis/schemas/place.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,4 @@ export const BasePlaceSchema = z.object({

export type RankingPlaceSort = 'views' | 'likes'

export const RankingPlaceSchema = BasePlaceSchema.extend({
isLiked: z.boolean(),
likeCount: z.number().int().nonnegative(),
})

export type BasePlace = z.infer<typeof BasePlaceSchema>
export type RankingPlace = z.infer<typeof RankingPlaceSchema>
17 changes: 11 additions & 6 deletions apps/web/app/_apis/services/place.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
import axiosInstance from '@/_lib/axiosInstance'
import { API_PATH } from '@/_constants/path'
import {
type RankingPlace,
type BasePlace,
type RankingPlaceSort,
RankingPlaceSchema,
BasePlaceSchema,
} from '../schemas/place'

export const getRankingPlaces = async (
export const getPlacesByRanking = async (
sort: RankingPlaceSort,
): Promise<RankingPlace[]> => {
const { data } = await axiosInstance.get(API_PATH.RANKING(sort))
return RankingPlaceSchema.array().parse(data)
): Promise<BasePlace[]> => {
const { data } = await axiosInstance.get(API_PATH.PLACES.BY_RANKING(sort))
return BasePlaceSchema.array().parse(data)
}

export const getPlacesByCategory = async (id: string): Promise<BasePlace[]> => {
const { data } = await axiosInstance.get(API_PATH.PLACES.BY_CATEGORY(id))
return BasePlaceSchema.array().parse(data)
}
Comment on lines 1 to 19
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

레거시 심볼 사용 여부 점검 제안: getRankingPlaces/rankingList 잔존 참조 확인

리네이밍이 광범위합니다. 빌드 타임에 놓친 잔존 참조가 없는지 확인 부탁드립니다.


🏁 Script executed:

#!/bin/bash
# 리네이밍 전 심볼/키 사용 흔적 탐색 스크립트
set -euo pipefail

echo "Searching for old service function name: getRankingPlaces"
rg -nP --glob '!**/node_modules/**' '\bgetRankingPlaces\b' -C2 || true

echo
echo "Searching for old query accessor: rankingList("
rg -nP --glob '!**/node_modules/**' '\brankingList\s*\(' -C3 || true

echo
echo "Searching for old API path namespace: API_PATH.RANKING"
rg -nP --glob '!**/node_modules/**' '\bAPI_PATH\.RANKING\b' -C2 || true

Length of output: 1126


HydrationBoundaryPage.tsx에서 legacy 심볼(rankingList) 잔존 사용 확인

  • apps/web/app/HydrationBoundaryPage.tsx:22
    usePlaceQueries.rankingList('likes') → 신규 쿼리 메서드명으로 변경 필요
  • getRankingPlacesAPI_PATH.RANKING 참조는 검색되지 않음

14 changes: 14 additions & 0 deletions apps/web/app/_components/HeaderBackButton/HeaderBackButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
'use client'

import { Icon } from '@repo/ui/components/Icon'
import { useRouter } from 'next/navigation'

export const HeaderBackButton = () => {
const { back } = useRouter()

return (
<button onClick={back}>
<Icon type={'arrowLeft'} size={24} />
</button>
)
}
1 change: 1 addition & 0 deletions apps/web/app/_components/HeaderBackButton/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { HeaderBackButton } from './HeaderBackButton'

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import { useSuspenseQuery } from '@tanstack/react-query'
import { usePlaceQueries } from '@/_apis/queries/place'
import { RankingPlaceList } from '@/_components/RankingPlaceList'

export const MostLikedPlaces = () => {
const { data } = useSuspenseQuery(usePlaceQueries.rankingList('likes'))
export const MostLikesPlaces = () => {
const { data } = useSuspenseQuery(usePlaceQueries.byRanking('likes'))

return (
<RankingPlaceList title={'찜많은 맛집'} icon={'fireHeart'} places={data} />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { MostLikesPlaces } from './MostLikesPlaces'
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { usePlaceQueries } from '@/_apis/queries/place'
import { RankingPlaceList } from '@/_components/RankingPlaceList'

export const MostViewsPlaces = () => {
const { data } = useSuspenseQuery(usePlaceQueries.rankingList('views'))
const { data } = useSuspenseQuery(usePlaceQueries.byRanking('views'))

return <RankingPlaceList title={'오늘의 맛집'} icon={'fire'} places={data} />
}
2 changes: 1 addition & 1 deletion apps/web/app/_components/RankingPlaceList/index.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export { RankingPlaceList } from './RankingPlaceList'
export { MostViewsPlaces } from './MostViewsPlaces'
export { MostLikedPlaces } from './MostLikedPlaces'
export { MostLikesPlaces } from './MostLikesPlaces'
5 changes: 4 additions & 1 deletion apps/web/app/_constants/path.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ import { RankingPlaceSort } from '@/_apis/schemas/place'

export const API_PATH = {
CATEGORY: '/categories',
RANKING: (sort: RankingPlaceSort) => `/places/ranking?sort=${sort}`,
PLACES: {
BY_CATEGORY: (id: string) => `/places?categoryId=${id}`,
BY_RANKING: (sort: RankingPlaceSort) => `/places/ranking?sort=${sort}`,
},
}

export const CLIENT_PATH = {
Expand Down
8 changes: 1 addition & 7 deletions apps/web/app/_mocks/data/place.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
export const RankingPlaces = [
export const Places = [
{
placeId: 15,
placeName: '우돈탄 다산본점',
address: '경기 남양주시 다산중앙로82번길 25',
isLiked: true,
likeCount: 5,
categories: [
{ id: 3, name: '한식', iconKey: 'korean' },
{ id: 14, name: '고기·구이', iconKey: 'meat' },
Expand All @@ -18,8 +16,6 @@ export const RankingPlaces = [
placeId: 21,
placeName: '김밥천국',
address: '서울특별시 강남구 테헤란로 100',
isLiked: false,
likeCount: 5,
categories: [
{ id: 4, name: '분식', iconKey: 'bunsik' },
{ id: 3, name: '한식', iconKey: 'korean' },
Expand All @@ -30,8 +26,6 @@ export const RankingPlaces = [
placeId: 2,
placeName: '짬뽕집',
address: '충남 천안시 서북구 테헤란로 100',
isLiked: false,
likeCount: 5,
categories: [{ id: 4, name: '중식', iconKey: 'chinese' }],
tags: [{ id: 7, name: '분위기 좋은', iconKey: 'blingBling' }],
},
Expand Down
13 changes: 8 additions & 5 deletions apps/web/app/_mocks/handlers/placeHandlers.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { http, HttpResponse } from 'msw'
import { API_PATH } from '@/_constants/path'
import { RankingPlaces } from '../data/place'
import { Places } from '../data/place'

const BASE_URL = process.env.NEXT_PUBLIC_API_URL || ''

Expand All @@ -9,10 +9,13 @@ const addBaseUrl = (path: string) => {
}

export const PlaceHandlers = [
http.get(addBaseUrl(API_PATH.RANKING('likes')), () => {
return HttpResponse.json(RankingPlaces)
http.get(addBaseUrl(API_PATH.PLACES.BY_RANKING('likes')), () => {
return HttpResponse.json(Places)
}),
http.get(addBaseUrl(API_PATH.RANKING('views')), () => {
return HttpResponse.json(RankingPlaces)
http.get(addBaseUrl(API_PATH.PLACES.BY_RANKING('views')), () => {
return HttpResponse.json(Places)
}),
http.get(addBaseUrl(API_PATH.PLACES.BY_CATEGORY('1')), () => {
return HttpResponse.json(Places)
}),
Comment on lines +18 to 20
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

BY_CATEGORY 핸들러가 categoryId=1에만 반응함 → 다른 카테고리 요청 실패

현재 핸들러가 ?categoryId=1에만 매칭되어, 1 이외의 카테고리 선택 시 404/미응답이 발생합니다. 쿼리 파라미터를 동적으로 파싱하도록 수정해야 개발/스토리북/테스트 환경에서 정상 동작합니다.

아래처럼 /places 경로로 매칭하고 categoryId를 파싱하여 응답을 생성하는 것을 제안합니다(간단히 필터링도 포함).

-  http.get(addBaseUrl(API_PATH.PLACES.BY_CATEGORY('1')), () => {
-    return HttpResponse.json(Places)
-  }),
+  http.get(addBaseUrl('/places'), ({ request }) => {
+    const url = new URL(request.url)
+    const categoryId = url.searchParams.get('categoryId')
+    // 간단한 필터링: 해당 카테고리를 포함하는 place만 반환
+    const filtered =
+      categoryId == null
+        ? Places
+        : Places.filter((p) =>
+            p.categories?.some((c) => String(c.id) === String(categoryId)),
+          )
+    return HttpResponse.json(filtered)
+  }),

참고: MSW의 경로 매칭은 쿼리 문자열 플레이스홀더를 지원하지 않아, 위와 같이 경로만 고정하고 쿼리를 직접 파싱하는 방식이 가장 견고합니다.

📝 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
http.get(addBaseUrl(API_PATH.PLACES.BY_CATEGORY('1')), () => {
return HttpResponse.json(Places)
}),
http.get(addBaseUrl('/places'), ({ request }) => {
const url = new URL(request.url)
const categoryId = url.searchParams.get('categoryId')
// 간단한 필터링: 해당 카테고리를 포함하는 place만 반환
const filtered =
categoryId == null
? Places
: Places.filter((p) =>
p.categories?.some((c) => String(c.id) === String(categoryId)),
)
return HttpResponse.json(filtered)
}),
🤖 Prompt for AI Agents
In apps/web/app/_mocks/handlers/placeHandlers.ts around lines 18-20, the current
handler matches only requests with categoryId=1; change it to match the /places
path (no query placeholder), read req.url.searchParams.get('categoryId'), parse
it (e.g., to number), filter the Places array by categoryId when present and
return HttpResponse.json(filteredPlaces); if categoryId is absent return the
full list (or empty per spec), and ensure the handler still supports other query
combinations and returns a proper 200 JSON response.

]
24 changes: 24 additions & 0 deletions apps/web/app/_template/template.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
'use client'
import { motion } from 'motion/react'
import { ReactNode } from 'react'

interface TemplateProps {
children: ReactNode
}

export default function Template({ children }: TemplateProps) {
return (
<motion.div
initial={{ x: 10, opacity: 0 }}
animate={{ x: 0, opacity: 1 }}
exit={{ x: -10, opacity: 0 }}
transition={{
type: 'tween',
ease: 'easeOut',
duration: 0.3,
}}
>
{children}
</motion.div>
)
}
47 changes: 47 additions & 0 deletions apps/web/app/categories/[id]/CategoryDetailPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
'use client'

import { useState } from 'react'
import { useSuspenseQuery } from '@tanstack/react-query'
import { useCategoryQueries } from '@/_apis/queries/category'
import { Icon } from '@repo/ui/components/Icon'
import { Text } from '@repo/ui/components/Text'
import { Header } from '@repo/ui/components/Header'
import { Column, Flex } from '@repo/ui/components/Layout'
import { HeaderBackButton } from '@/_components/HeaderBackButton'
import { RowCategories, Places } from './_components'

type Props = {
initId: string
}

export const CategoryDetailPage = ({ initId }: Props) => {
const [id, setId] = useState(initId)

const { data: categories } = useSuspenseQuery(useCategoryQueries.list())
const activeCategory = categories.find((category) => category.id === id)

const setIdFunc = (id: string) => {
setId(id)
}
Comment on lines +23 to +25
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

RowCategories가 number를 콜백으로 전달하는 경우 대비: setIdFunc 시그니처 완화

RowCategories 내부에서 category.id(number)를 바로 넘길 가능성이 높습니다. 현재 시그니처가 string으로 고정되어 있어 타입/런타임 불일치 위험이 있습니다. union으로 받아 안전하게 문자열로 변환하세요.

-  const setIdFunc = (id: string) => {
-    setId(id)
-  }
+  const setIdFunc = (nextId: string | number) => {
+    setId(String(nextId))
+  }

RowCategories의 setIdFunc 타입 정의가 (id: string | number) => void로 허용되는지 확인 부탁드립니다. 필요 시 해당 컴포넌트에도 동일한 시그니처 반영이 필요합니다.


추가 검증을 위해 아래 스크립트를 실행하여 CategoryDetailPage.tsx에서 RowCategories 사용 위치와 RowCategories 컴포넌트 정의(Props 시그니처)를 확인해주세요.


🏁 Script executed:

#!/bin/bash
# 1. CategoryDetailPage.tsx에서 RowCategories 사용 위치 및 전달된 setIdFunc 확인
rg -nC3 "RowCategories" --glob "*.tsx" "apps/web/app/categories/[id]/CategoryDetailPage.tsx"

# 2. RowCategories 컴포넌트 정의 및 Props 시그니처 검색
rg -nC3 "RowCategories" --glob "*.tsx"

Length of output: 2981


setIdFunc 시그니처 완화 및 안전한 문자열 변환 적용

RowCategories가 number 타입의 id를 넘길 수 있으므로, CategoryDetailPage와 RowCategories 컴포넌트 양쪽에서 setIdFunc 시그니처를 string | number로 완화하고 내부에서 String()으로 변환해 주세요.

• apps/web/app/categories/[id]/CategoryDetailPage.tsx (기존 23–25번 라인)

-  const setIdFunc = (id: string) => {
-    setId(id)
-  }
+  const setIdFunc = (nextId: string | number) => {
+    setId(String(nextId))
+  }

• apps/web/app/categories/[id]/_components/RowCategories/RowCategories.tsx (Props 정의)

-  setIdFunc: (id: string) => void
+  setIdFunc: (id: string | number) => void

위 수정 후 RowCategories 호출부(<CategoryDetailPage /> 안)에는 별도 변경 없이 안전하게 동작합니다.

🤖 Prompt for AI Agents
In apps/web/app/categories/[id]/CategoryDetailPage.tsx around lines 23–25, the
setIdFunc currently only accepts a string; relax its signature to accept string
| number and convert the incoming value to a string with String(id) before
calling setId. Also update the props definition in
apps/web/app/categories/[id]/_components/RowCategories/RowCategories.tsx to
expect setIdFunc: (id: string | number) => void so both components accept
numbers from RowCategories; keep the call site in CategoryDetailPage unchanged
because the internal String() conversion makes it safe.


return (
<>
<Header
left={<HeaderBackButton />}
center={
<Flex className={'gap-1.5'}>
<Icon type={activeCategory?.iconKey || 'logo'} />
<Text variant={'heading2'} className={'pr-6'}>
{activeCategory?.name}
</Text>
</Flex>
}
className={'border-b-1 border-gray-50'}
/>
<Column className={'gap-2.5 px-5 py-2.5'}>
<RowCategories id={id} categories={categories} setIdFunc={setIdFunc} />
<Places id={id} />
</Column>
</>
)
}
19 changes: 19 additions & 0 deletions apps/web/app/categories/[id]/_components/Places/Places.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { useSuspenseQuery } from '@tanstack/react-query'
import { usePlaceQueries } from '@/_apis/queries/place'
import { PlaceListItem } from '@/_components/PlaceListItem'

export const Places = ({ id }: { id: string }) => {
const { data: places } = useSuspenseQuery(usePlaceQueries.byCategory(id))

return (
<ul className={'px-3'}>
{places.map((place, index) => (
<PlaceListItem
key={place.placeId}
{...place}
showBorder={index !== places.length - 1}
/>
))}
</ul>
)
}
1 change: 1 addition & 0 deletions apps/web/app/categories/[id]/_components/Places/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { Places } from './Places'
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Column } from '@repo/ui/components/Layout'
import { Icon } from '@repo/ui/components/Icon'
import { Text } from '@repo/ui/components/Text'
import { cn } from '@repo/ui/utils/cn'
import type { Category } from '@/_apis/schemas/category'

type Props = {
category: Category
isActive: boolean
onClick: VoidFunction
}

export const CategoryItem = ({ category, isActive, onClick }: Props) => {
const { iconKey, name } = category

return (
<Column
as={'button'}
onClick={onClick}
className={'w-10 items-center gap-1'}
>
<Icon type={iconKey} size={26} />
<Text
fontSize={'xs'}
fontWeight={isActive ? 'semibold' : 'light'}
className={'text-nowrap'}
>
{name}
</Text>
<hr
className={cn('border-main w-full rounded-full border-2', {
invisible: !isActive,
})}
/>
</Column>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import type { Category } from '@/_apis/schemas/category'
import { Flex } from '@repo/ui/components/Layout'
import { CategoryItem } from './CategoryItem'
import { cn } from '@repo/ui/utils/cn'

type Props = {
categories: Category[]
id: string
setIdFunc: (id: string) => void
}

export const RowCategories = ({ id, categories, setIdFunc }: Props) => {
return (
<div className={'relative'}>
<Flex className={'scrollbar-hide gap-4 overflow-x-auto'}>
{categories.map((category) => (
<CategoryItem
key={category.id}
category={category}
isActive={id === category.id}
onClick={() => {
setIdFunc(category.id)
}}
/>
))}
</Flex>
<ScrollHintGradient />
</div>
)
}

const ScrollHintGradient = () => (
<div
className={cn(
'absolute',
'right-[-1px] top-0',
'h-full w-10',
'z-10',
'pointer-events-none',
'bg-gradient-to-l from-white to-transparent',
)}
/>
)
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { RowCategories } from './RowCategories'
2 changes: 2 additions & 0 deletions apps/web/app/categories/[id]/_components/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './Places'
export * from './RowCategories'
20 changes: 20 additions & 0 deletions apps/web/app/categories/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { HydrationBoundaryPage } from '@/HydrationBoundaryPage'
import { useCategoryQueries } from '@/_apis/queries/category'
import { CategoryDetailPage } from '@/categories/[id]/CategoryDetailPage'
import { usePlaceQueries } from '@/_apis/queries/place'

const Page = async ({ params }: { params: Promise<{ id: string }> }) => {
const { id } = await params

return (
<HydrationBoundaryPage
prefetch={async (queryClient) => {
await queryClient.prefetchQuery(useCategoryQueries.list())
await queryClient.prefetchQuery(usePlaceQueries.byCategory(id))
}}
>
<CategoryDetailPage initId={id} />
</HydrationBoundaryPage>
)
}
export default Page
2 changes: 2 additions & 0 deletions apps/web/app/categories/template.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import Template from '@/_template/template'
export default Template
Comment on lines +1 to +2
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' 누락 — Next App Router에서 컴파일 에러 위험

Client 컴포넌트(@/_template/template)를 Server 파일(categories/template.tsx)에서 그대로 기본 내보내기(re-export)하면, Next가 "use client" 지시자 누락으로 에러를 낼 수 있습니다. 이 파일 자체를 Client 컴포넌트로 표시해 주세요.

아래처럼 상단에 'use client'를 추가하면 해결됩니다.

+'use client'
 import Template from '@/_template/template'
 export default Template

또는 명시적 래퍼를 만드는 방법도 있습니다(참고):

'use client'
import TemplateImpl from '@/_template/template'
export default function Template({ children }: { children: React.ReactNode }) {
  return <TemplateImpl>{children}</TemplateImpl>
}
🤖 Prompt for AI Agents
In apps/web/app/categories/template.tsx lines 1-2, this server-file re-exports a
client component (@/_template/template) without the 'use client' directive which
can cause Next App Router compile errors; fix by marking this file as a client
component: add a single line 'use client' at the very top of
apps/web/app/categories/template.tsx before any imports, then keep the existing
import/export, or alternatively replace the re-export with an explicit client
wrapper component that imports the implementation and returns it (ensuring the
'use client' directive remains at the top).

Loading