-
Notifications
You must be signed in to change notification settings - Fork 1
Feat/#15 카테고리 상세페이지 화면 UI 구현 및 API 관련 query 로직 등 구현 #16
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
918ab33
9235f37
757c324
659a5e7
cfd5bcd
24f133f
d19d9d5
caea68e
94a6305
38891f5
9283547
b91df60
2eb50eb
ea337a7
d1467df
c4c19a2
d256334
8b7abc5
4b96f5b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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), | ||
| }), | ||
| } | ||
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 || trueLength of output: 1126 HydrationBoundaryPage.tsx에서 legacy 심볼(rankingList) 잔존 사용 확인
|
||
| 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> | ||
| ) | ||
| } |
| 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 |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export { MostLikesPlaces } from './MostLikesPlaces' |
| 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' |
| 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 || '' | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
|
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. BY_CATEGORY 핸들러가 categoryId=1에만 반응함 → 다른 카테고리 요청 실패 현재 핸들러가 아래처럼 - 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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||
| ] | ||||||||||||||||||||||||||||||||
| 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> | ||
| ) | ||
| } |
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Verification agent 🧩 Analysis chainRowCategories가 number를 콜백으로 전달하는 경우 대비: setIdFunc 시그니처 완화 RowCategories 내부에서 - const setIdFunc = (id: string) => {
- setId(id)
- }
+ const setIdFunc = (nextId: string | number) => {
+ setId(String(nextId))
+ }RowCategories의 추가 검증을 위해 아래 스크립트를 실행하여 🏁 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가 • 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위 수정 후 🤖 Prompt for AI Agents |
||
|
|
||
| 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> | ||
| </> | ||
| ) | ||
| } | ||
| 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> | ||
| ) | ||
| } |
| 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' |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| export * from './Places' | ||
| export * from './RowCategories' | ||
leeleeleeleejun marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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 |
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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 |
||
Uh oh!
There was an error while loading. Please reload this page.