Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
34 changes: 22 additions & 12 deletions apps/web/app/categories/[id]/CategoryDetailPage.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client'

import { useState } from 'react'
import { usePathname } from 'next/navigation'
import { useSuspenseQuery } from '@tanstack/react-query'
import { useCategoryQueries } from '@/_apis/queries/category'
import { Icon } from '@repo/ui/components/Icon'
Expand All @@ -9,21 +9,24 @@ import { Header } from '@repo/ui/components/Header'
import { Flex } from '@repo/ui/components/Layout'
import { HeaderBackButton } from '@/_components/HeaderBackButton'
import { RowCategories, Places } from './_components'
import { Suspense, useEffect } from 'react'
import { Spinner } from '@heroui/react'

type Props = {
initId: string
}

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

export const CategoryDetailPage = () => {
const { data: categories } = useSuspenseQuery(useCategoryQueries.list())
const activeCategory = categories.find((category) => category.id === id)
const activeCategoryId = usePathname().split('/')[2] || '0'
const activeCategory = categories.find(
(category) => category.id === activeCategoryId,
)

const setIdFunc = (id: string) => {
setId(id)
window.history.replaceState(null, '', `/categories/${id}`)
}
Comment on lines 22 to 24
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Next.js 라우터 사용을 고려하세요.

window.history.replaceState를 직접 사용하는 것은 Next.js의 클라이언트 사이드 라우팅을 우회합니다. 이는 프리페칭, 스크롤 복원 등의 Next.js 기능과 충돌할 수 있습니다.

다음 스크립트를 실행하여 Next.js router를 사용하는 다른 예시가 있는지 확인하세요:

next/navigationuseRouter를 사용하는 것이 더 적절할 수 있습니다:

import { useRouter } from 'next/navigation'

const router = useRouter()
const setIdFunc = (id: string) => {
  router.replace(`/categories/${id}`)
}

🏁 Script executed:

#!/bin/bash
# Next.js useRouter 사용 패턴 검색
rg -n "useRouter|router\.push|router\.replace" --type=tsx --type=ts -C 3

Length of output: 27


새로운 스크립트를 실행하여 코드베이스를 재검증하겠습니다.


---

🏁 Script executed:

```shell
#!/bin/bash
# TypeScript/TSX 파일에서 router 패턴 검색 (파일 타입 제약 제거)
rg -n "useRouter|router\.push|router\.replace|window\.history" -C 3 --glob="*.tsx" --glob="*.ts"

Length of output: 4609


🏁 Script executed:

#!/bin/bash
# 해당 파일 확인
fd "CategoryDetailPage.tsx" -type f

Length of output: 170


🏁 Script executed:

#!/bin/bash
# 파일 내용 확인
find . -name "CategoryDetailPage.tsx" -type f -exec cat {} \;

Length of output: 1746


Next.js 라우터 사용으로 변경하세요.

이 파일은 이미 'use client' 선언이 있고 next/navigation에서 usePathname을 임포트하고 있습니다. 코드베이스의 다른 컴포넌트들(HeaderHomeButton.tsx, HeaderBackButton.tsx 등)도 useRouter를 사용하여 네비게이션을 처리하고 있습니다.

window.history.replaceState를 직접 사용하면 Next.js의 클라이언트 사이드 라우팅 기능(프리페칭, 스크롤 복원 등)이 제대로 작동하지 않을 수 있습니다.

다음과 같이 수정하세요:

import { useRouter } from 'next/navigation'

const router = useRouter()
const setIdFunc = (id: string) => {
  router.replace(`/categories/${id}`)
}
🤖 Prompt for AI Agents
In apps/web/app/categories/[id]/CategoryDetailPage.tsx around lines 22 to 24,
replace the direct window.history.replaceState usage with Next.js client router
navigation: import and call useRouter from next/navigation, get router via
useRouter() in the component, and change setIdFunc to call
router.replace(`/categories/${id}`) so routing uses Next.js client-side
navigation and preserves prefetching/scroll restoration.


useEffect(() => {
document.title = `공주대 맛집 | ${activeCategory?.name}`
}, [activeCategory])
Comment on lines +26 to +28
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

document.title 직접 조작을 제거하세요.

page.tsx에서 generateMetadata를 통해 메타데이터를 이미 관리하고 있으므로, 클라이언트 컴포넌트에서 document.title을 직접 설정하는 것은 불필요하며 Next.js의 메타데이터 시스템과 충돌할 수 있습니다.

다음과 같이 useEffect를 제거하세요:

-  useEffect(() => {
-    document.title = `공주대 맛집 | ${activeCategory?.name}`
-  }, [activeCategory])
-
📝 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
useEffect(() => {
document.title = `공주대 맛집 | ${activeCategory?.name}`
}, [activeCategory])
🤖 Prompt for AI Agents
In apps/web/app/categories/[id]/CategoryDetailPage.tsx around lines 26 to 28,
remove the client-side useEffect that sets document.title (document.title = `공주대
맛집 | ${activeCategory?.name}`) because page.tsx already manages metadata via
generateMetadata; delete the entire useEffect block and any imports or hooks
that are only used for that effect so the component relies on Next.js metadata
instead.


return (
<>
<Header
Expand All @@ -38,8 +41,15 @@ export const CategoryDetailPage = ({ initId }: Props) => {
}
className={'border-b-1 border-gray-50'}
/>
<RowCategories id={id} categories={categories} setIdFunc={setIdFunc} />
<Places id={id} />
<RowCategories
id={activeCategoryId}
categories={categories}
setIdFunc={setIdFunc}
/>
{/*Todo: 맛집 리스트 스켈레톤으로 변경하기*/}
<Suspense fallback={<Spinner className={'my-auto'} />}>
<Places id={activeCategoryId} />
</Suspense>
</>
)
}
28 changes: 25 additions & 3 deletions apps/web/app/categories/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,39 @@
import type { Metadata } from 'next'
import { HydrationBoundaryPage } from '@/HydrationBoundaryPage'
import { useCategoryQueries } from '@/_apis/queries/category'
import { CategoryDetailPage } from '@/categories/[id]/CategoryDetailPage'
import { getCategories } from '@/_apis/services/category'

const Page = async ({ params }: { params: Promise<{ id: string }> }) => {
const { id } = await params
export async function generateMetadata({
params,
}: {
params: Promise<{ id: string }>
}): Promise<Metadata> {
const { id: categoryId } = await params

const data = await getCategories()
const categoryName = data?.find(({ id }) => id === categoryId)?.name

return {
title: `${categoryName}`,
description: `공주대학교 근처 ${categoryName} 맛집`,
openGraph: {
title: `공주대 맛집 | ${categoryName}`,
description: `공주대학교 근처 ${categoryName} 맛집`,
locale: 'ko-KR',
type: 'website',
},
}
}
Comment on lines +7 to +27
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

generateMetadata에 에러 핸들링을 추가하세요.

카테고리를 찾지 못했을 때를 대비한 에러 핸들링이 필요합니다. categoryNameundefined일 경우 메타데이터에 "undefined"가 포함될 수 있습니다.

다음과 같이 폴백을 추가하세요:

 export async function generateMetadata({
   params,
 }: {
   params: Promise<{ id: string }>
 }): Promise<Metadata> {
   const { id: categoryId } = await params
 
   const data = await getCategories()
-  const categoryName = data?.find(({ id }) => id === categoryId)?.name
+  const categoryName = data?.find(({ id }) => id === categoryId)?.name || '카테고리'
 
   return {
     title: `${categoryName}`,
     description: `공주대학교 근처 ${categoryName} 맛집`,
     openGraph: {
       title: `공주대 맛집 | ${categoryName}`,
       description: `공주대학교 근처 ${categoryName} 맛집`,
       locale: 'ko-KR',
       type: 'website',
     },
   }
 }
🤖 Prompt for AI Agents
In apps/web/app/categories/[id]/page.tsx around lines 7 to 27, generateMetadata
currently assumes categoryName exists and may insert "undefined" into metadata;
update the function to handle missing categories by checking if categoryName is
undefined and either (a) use a safe fallback string (e.g., '카테고리' or 'Unknown')
when building title/description/openGraph, or (b) throw a notFound()/404 so the
page metadata isn't generated for a non-existent category; implement one of
these branches and ensure all metadata fields use the fallback/handled value.


const Page = () => {
return (
<HydrationBoundaryPage
prefetch={async (queryClient) => {
await queryClient.prefetchQuery(useCategoryQueries.list())
}}
>
<CategoryDetailPage initId={id} />
<CategoryDetailPage />
</HydrationBoundaryPage>
)
}
Comment on lines +29 to 39
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

HydrationBoundary를 사용하는 페이지에 dynamic 설정이 누락되었습니다.

PR 설명에 따르면 HydrationBoundary를 사용하는 페이지에 export const dynamic = 'force-dynamic'을 추가하여 빌드 타임 API 요청을 방지해야 합니다. 이 설정이 없으면 Next.js가 SSG로 빌드 시 MSW가 실행되지 않아 500 에러가 발생할 수 있습니다.

다음과 같이 dynamic export를 추가하세요:

 import type { Metadata } from 'next'
 import { HydrationBoundaryPage } from '@/HydrationBoundaryPage'
 import { useCategoryQueries } from '@/_apis/queries/category'
 import { CategoryDetailPage } from '@/categories/[id]/CategoryDetailPage'
 import { getCategories } from '@/_apis/services/category'
 
+export const dynamic = 'force-dynamic'
+
 export async function generateMetadata({

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In apps/web/app/categories/[id]/page.tsx around lines 29 to 39, the page using
HydrationBoundaryPage is missing the Next.js dynamic export required to force
dynamic rendering; add a top-level export: export const dynamic =
'force-dynamic' in this file (placed at module scope, above the component or
exports) so build-time SSG is disabled and MSW-backed API calls run at request
time to avoid the 500 error.

Expand Down
18 changes: 18 additions & 0 deletions apps/web/app/events/food-slot/page.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,27 @@
import type { Metadata } from 'next'
import { HydrationBoundaryPage } from '@/HydrationBoundaryPage'
import { useCategoryQueries } from '@/_apis/queries/category'
import FoodSlotMachine from './FoodSlotMachine'
import { Header } from '@repo/ui/components/Header'
import { HeaderBackButton } from '@/_components/HeaderBackButton'

export const dynamic = 'force-dynamic'

const pageTitle = '오늘 뭐 먹지?'
const pageDescription =
"선택 장애가 고민인가요? '오늘 뭐 먹지?' 슬롯머신으로 공주대 근처 맛집 메뉴를 추천받아보세요!"

export const metadata: Metadata = {
title: pageTitle,
description: pageDescription,
openGraph: {
title: `공주대 맛집 | ${pageTitle}`,
description: pageDescription,
locale: 'ko-KR',
type: 'website',
},
}

const Page = async () => {
return (
<>
Expand Down
2 changes: 2 additions & 0 deletions apps/web/app/events/lucky-draw/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import { HydrationBoundaryPage } from '@/HydrationBoundaryPage'
import { useEventQueries } from '@/_apis/queries/event'
import { InfoPopover } from './_components/InfoPopover'

export const dynamic = 'force-dynamic'

const Page = () => {
return (
<HydrationBoundaryPage
Expand Down
Binary file modified apps/web/app/favicon.ico
Binary file not shown.
21 changes: 19 additions & 2 deletions apps/web/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,26 @@ import { Column } from '@repo/ui/components/Layout'
import { NaverMapProvider } from '@/NaverMapProvider'
import { HeroProvider } from '@/HeroProvider'

const SITE_URL = new URL('https://knu-matzip.vercel.app')

export const metadata: Metadata = {
title: 'Create Turborepo',
description: 'Generated by create turbo',
metadataBase: SITE_URL,
title: {
template: '공주대 맛집 | %s',
default: '공주대 맛집',
},
description: '공주대학교 캠퍼스(천안, 공주, 예산) 근처 맛집을 찾아보세요.',
openGraph: {
title: '공주대 맛집',
description: '공주대학교 캠퍼스별 맛집 정보',
locale: 'ko-KR',
siteName: '공주대 맛집',
url: '/',
type: 'website',
},
// verification: {
// google: "",
// },
}

const pretendard = localFont({
Expand Down
17 changes: 17 additions & 0 deletions apps/web/app/likes/page.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,27 @@
import type { Metadata } from 'next'
import { usePlaceQueries } from '@/_apis/queries/place'
import { OnlyLeftHeader } from '@repo/ui/components/Header'
import { VerticalScrollArea } from '@repo/ui/components/Layout'
import { HydrationBoundaryPage } from '@/HydrationBoundaryPage'
import { LikePlacesList } from './_components/LikePlacesList'
import { BottomNavigation } from '@/_components/BottomNavigation'

export const dynamic = 'force-dynamic'

const pageTitle = '찜 맛집'
const pageDescription = '내가 직접 고른 공주대 주변 찜 맛집 리스트!'

export const metadata: Metadata = {
title: pageTitle,
description: pageDescription,
openGraph: {
title: `${pageTitle} | 공주대 맛집`,
description: pageDescription,
locale: 'ko-KR',
type: 'website',
},
}

const Page = () => {
return (
<HydrationBoundaryPage
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { UserMarker, PlaceMarker } from './_components/Marker'
import { CurrentLocationButton } from './_components/CurrentLocationButton'
import { PreviewPlace } from './_components/PreviewPlace'

export const MapPage = () => {
const MapComponent = () => {
const [map, setMap] = useState<naver.maps.Map | null>(null)
const [isCenteredOnUser, setIsCenteredOnUser] = useState(false)
const [currentBounds, setCurrentBounds] = useState<MapBounds | null>(null)
Expand Down Expand Up @@ -102,3 +102,5 @@ export const MapPage = () => {
</>
)
}

export default MapComponent
10 changes: 10 additions & 0 deletions apps/web/app/map/MapContainer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
'use client'

import dynamic from 'next/dynamic'

const MapComponent = dynamic(() => import('./MapComponent'), {
ssr: false,
loading: () => <div>지도 로딩 중...</div>,
})

export const MapContainer = () => <MapComponent />
19 changes: 17 additions & 2 deletions apps/web/app/map/page.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,27 @@
import { OnlyLeftHeader } from '@repo/ui/components/Header'
import { BottomNavigation } from '@/_components/BottomNavigation'
import { MapPage } from '@/map/MapPage'
import { MapContainer } from './MapContainer'
import type { Metadata } from 'next'

const pageTitle = '주변 맛집'
const pageDescription = '지도로 내 근처 공주대 주변 맛집의 위치를 확인하세요.'

export const metadata: Metadata = {
title: pageTitle,
description: pageDescription,
openGraph: {
title: `${pageTitle} | 공주대 맛집`,
description: pageDescription,
locale: 'ko-KR',
type: 'website',
},
}

const Page = () => {
return (
<>
<OnlyLeftHeader icon={'markerWithMap'} name={'주변 맛집'} />
<MapPage />
<MapContainer />
<BottomNavigation />
</>
)
Expand Down
2 changes: 2 additions & 0 deletions apps/web/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import {
LuckyDrawBanner,
} from '@/_components/eventBanners'

export const dynamic = 'force-dynamic'

export default function Page() {
return (
<HydrationBoundaryPage
Expand Down
25 changes: 25 additions & 0 deletions apps/web/app/places/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,31 @@
import type { Metadata } from 'next'
import { usePlaceQueries } from '@/_apis/queries/place'
import { HydrationBoundaryPage } from '@/HydrationBoundaryPage'
import { PlaceDetailPage } from './PlaceDetailPage'
import { getPlaceDetail } from '@/_apis/services/place'

export async function generateMetadata({
params,
}: {
params: Promise<{ id: string }>
}): Promise<Metadata> {
const { id } = await params

const data = await getPlaceDetail(id)
const { placeName } = data
const pageDescription = `공주대학교 근처 ${placeName} 맛집 상세 정보`

return {
title: `${placeName}`,
description: pageDescription,
openGraph: {
title: `공주대 맛집 | ${placeName}`,
description: pageDescription,
locale: 'ko-KR',
type: 'website',
},
}
}
Comment on lines +7 to +28
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

메타데이터 생성이 올바르게 구현되었습니다.

generateMetadata에서 getPlaceDetail을 호출하고, Page 컴포넌트에서 usePlaceQueries.detail로 동일한 데이터를 프리페치하는 것이 중복처럼 보일 수 있으나, Next.js 15는 동일한 요청을 자동으로 중복 제거(dedupe)하므로 실제로는 한 번만 호출됩니다. 이는 Next.js의 권장 패턴입니다.

다만, export const dynamic = 'force-dynamic'이 이 파일에 없는데, HydrationBoundaryPage를 사용하는 다른 페이지들과 일관성을 위해 추가를 고려해보세요.

다음 스크립트로 HydrationBoundaryPage를 사용하면서 dynamic export가 없는 페이지를 확인하세요:


🏁 Script executed:

#!/bin/bash
# HydrationBoundaryPage를 사용하지만 dynamic export가 없는 파일 찾기
for file in $(rg -l "HydrationBoundaryPage" --type=tsx --type=ts apps/web/app); do
  if ! grep -q "export const dynamic" "$file"; then
    echo "Missing dynamic export: $file"
  fi
done

Length of output: 27


고마워요. 스크립트에 문제가 있네요. rg의 파일 타입을 수정하겠습니다.


---

🏁 Script executed:

```shell
#!/bin/bash
# tsx/ts 파일에서 HydrationBoundaryPage를 사용하는 모든 파일 찾기
# 그리고 export const dynamic 여부 확인

echo "=== Files using HydrationBoundaryPage ==="
rg "HydrationBoundaryPage" -t typescript apps/web/app --files-with-matches

echo ""
echo "=== Checking for dynamic export in places/[id]/page.tsx ==="
if grep -q "export const dynamic" apps/web/app/places/\[id\]/page.tsx; then
  echo "✓ Has dynamic export"
else
  echo "✗ Missing dynamic export"
fi

echo ""
echo "=== All HydrationBoundaryPage files with dynamic export status ==="
for file in $(rg "HydrationBoundaryPage" -t typescript apps/web/app --files-with-matches); do
  if grep -q "export const dynamic" "$file"; then
    echo "✓ $file"
  else
    echo "✗ $file"
  fi
done

Length of output: 265


파일 타입 지정 문제가 있네요. 다른 방식으로 접근하겠습니다.


🏁 Script executed:

#!/bin/bash
# 파일 타입 필터링 없이 HydrationBoundaryPage 검색
echo "=== Files using HydrationBoundaryPage ==="
rg "HydrationBoundaryPage" apps/web/app -l

echo ""
echo "=== Current file status ==="
echo "File: apps/web/app/places/[id]/page.tsx"
if grep -q "export const dynamic" "apps/web/app/places/[id]/page.tsx"; then
  echo "Dynamic export: ✓ Present"
else
  echo "Dynamic export: ✗ Missing"
fi

echo ""
echo "=== Checking all identified files for dynamic export ==="
rg "HydrationBoundaryPage" apps/web/app -l | while read file; do
  if grep -q "export const dynamic" "$file"; then
    echo "✓ HAS: $file"
  else
    echo "✗ LACKS: $file"
  fi
done

Length of output: 877


export const dynamic = 'force-dynamic' 추가 필요

검증 결과, apps/web/app/places/[id]/page.tsx는 실제로 export const dynamic 선언이 누락되어 있습니다. HydrationBoundaryPage를 사용하는 다른 페이지들을 확인한 결과:

  • 동적 선언 있음: page.tsx, requests/page.tsx, requests/[id]/page.tsx, likes/page.tsx, events/food-slot/page.tsx, events/lucky-draw/page.tsx (6개)
  • 동적 선언 없음: places/[id]/page.tsx, categories/[id]/page.tsx (2개)

대부분의 HydrationBoundaryPage 사용 페이지가 동적 선언을 포함하고 있으므로, 일관성 유지를 위해 export const dynamic = 'force-dynamic'을 이 파일에 추가하세요.

🤖 Prompt for AI Agents
In apps/web/app/places/[id]/page.tsx around lines 7 to 28, add the missing
export const dynamic = 'force-dynamic' declaration to match other
HydrationBoundaryPage files; insert this export near the top of the module
(before the generateMetadata export) so the page is treated as dynamic and
consistent with the other pages using hydration boundaries.


const Page = async ({ params }: { params: Promise<{ id: string }> }) => {
const { id } = await params
Expand Down
2 changes: 2 additions & 0 deletions apps/web/app/requests/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { RequestDetailPage } from './RequestDetailPage'
import { HydrationBoundaryPage } from '@/HydrationBoundaryPage'
import { useRequestQueries } from '@/_apis/queries/request'

export const dynamic = 'force-dynamic'

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

Expand Down
4 changes: 3 additions & 1 deletion apps/web/app/requests/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ import { useRequestQueries } from '@/_apis/queries/request'
import { HydrationBoundaryPage } from '@/HydrationBoundaryPage'
import { RequestPlacesList } from './_components/RequestPlacesList'

const Page = () => {
export const dynamic = 'force-dynamic'

const Page = async () => {
return (
<HydrationBoundaryPage
prefetch={async (queryClient) => {
Expand Down