diff --git a/apps/web/app/HydrationBoundaryPage.tsx b/apps/web/app/HydrationBoundaryPage.tsx index 335af0f..6926a62 100644 --- a/apps/web/app/HydrationBoundaryPage.tsx +++ b/apps/web/app/HydrationBoundaryPage.tsx @@ -19,7 +19,7 @@ import { ReactNode } from 'react' * { * await queryClient.prefetchQuery(useCategoryQueries.list()) - * await queryClient.prefetchQuery(usePlaceQueries.rankingList('likes')) + * await queryClient.prefetchQuery(usePlaceQueries.byRanking('likes')) * }} * > * diff --git a/apps/web/app/_apis/queries/place.ts b/apps/web/app/_apis/queries/place.ts index 4553381..66f634d 100644 --- a/apps/web/app/_apis/queries/place.ts +++ b/apps/web/app/_apis/queries/place.ts @@ -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), }), } diff --git a/apps/web/app/_apis/schemas/place.ts b/apps/web/app/_apis/schemas/place.ts index 8615bd4..98914ad 100644 --- a/apps/web/app/_apis/schemas/place.ts +++ b/apps/web/app/_apis/schemas/place.ts @@ -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 -export type RankingPlace = z.infer diff --git a/apps/web/app/_apis/services/place.ts b/apps/web/app/_apis/services/place.ts index 3221457..86c7c07 100644 --- a/apps/web/app/_apis/services/place.ts +++ b/apps/web/app/_apis/services/place.ts @@ -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 => { - const { data } = await axiosInstance.get(API_PATH.RANKING(sort)) - return RankingPlaceSchema.array().parse(data) +): Promise => { + const { data } = await axiosInstance.get(API_PATH.PLACES.BY_RANKING(sort)) + return BasePlaceSchema.array().parse(data) +} + +export const getPlacesByCategory = async (id: string): Promise => { + const { data } = await axiosInstance.get(API_PATH.PLACES.BY_CATEGORY(id)) + return BasePlaceSchema.array().parse(data) } diff --git a/apps/web/app/_components/HeaderBackButton/HeaderBackButton.tsx b/apps/web/app/_components/HeaderBackButton/HeaderBackButton.tsx new file mode 100644 index 0000000..69cc008 --- /dev/null +++ b/apps/web/app/_components/HeaderBackButton/HeaderBackButton.tsx @@ -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 ( + + ) +} diff --git a/apps/web/app/_components/HeaderBackButton/index.tsx b/apps/web/app/_components/HeaderBackButton/index.tsx new file mode 100644 index 0000000..c4129ae --- /dev/null +++ b/apps/web/app/_components/HeaderBackButton/index.tsx @@ -0,0 +1 @@ +export { HeaderBackButton } from './HeaderBackButton' diff --git a/apps/web/app/_components/RankingPlaceList/MostLikedPlaces/index.tsx b/apps/web/app/_components/RankingPlaceList/MostLikedPlaces/index.tsx deleted file mode 100644 index 7dd8614..0000000 --- a/apps/web/app/_components/RankingPlaceList/MostLikedPlaces/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export { MostLikedPlaces } from './MostLikedPlaces' diff --git a/apps/web/app/_components/RankingPlaceList/MostLikedPlaces/MostLikedPlaces.tsx b/apps/web/app/_components/RankingPlaceList/MostLikesPlaces/MostLikesPlaces.tsx similarity index 72% rename from apps/web/app/_components/RankingPlaceList/MostLikedPlaces/MostLikedPlaces.tsx rename to apps/web/app/_components/RankingPlaceList/MostLikesPlaces/MostLikesPlaces.tsx index 58221fe..8efeb80 100644 --- a/apps/web/app/_components/RankingPlaceList/MostLikedPlaces/MostLikedPlaces.tsx +++ b/apps/web/app/_components/RankingPlaceList/MostLikesPlaces/MostLikesPlaces.tsx @@ -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 ( diff --git a/apps/web/app/_components/RankingPlaceList/MostLikesPlaces/index.tsx b/apps/web/app/_components/RankingPlaceList/MostLikesPlaces/index.tsx new file mode 100644 index 0000000..e57fea4 --- /dev/null +++ b/apps/web/app/_components/RankingPlaceList/MostLikesPlaces/index.tsx @@ -0,0 +1 @@ +export { MostLikesPlaces } from './MostLikesPlaces' diff --git a/apps/web/app/_components/RankingPlaceList/MostViewsPlaces/MostViewsPlaces.tsx b/apps/web/app/_components/RankingPlaceList/MostViewsPlaces/MostViewsPlaces.tsx index 1d964e0..a2c51d2 100644 --- a/apps/web/app/_components/RankingPlaceList/MostViewsPlaces/MostViewsPlaces.tsx +++ b/apps/web/app/_components/RankingPlaceList/MostViewsPlaces/MostViewsPlaces.tsx @@ -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 } diff --git a/apps/web/app/_components/RankingPlaceList/index.tsx b/apps/web/app/_components/RankingPlaceList/index.tsx index 2205676..fa5f639 100644 --- a/apps/web/app/_components/RankingPlaceList/index.tsx +++ b/apps/web/app/_components/RankingPlaceList/index.tsx @@ -1,3 +1,3 @@ export { RankingPlaceList } from './RankingPlaceList' export { MostViewsPlaces } from './MostViewsPlaces' -export { MostLikedPlaces } from './MostLikedPlaces' +export { MostLikesPlaces } from './MostLikesPlaces' diff --git a/apps/web/app/_constants/path.ts b/apps/web/app/_constants/path.ts index 933821b..15cc004 100644 --- a/apps/web/app/_constants/path.ts +++ b/apps/web/app/_constants/path.ts @@ -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 = { diff --git a/apps/web/app/_mocks/data/place.ts b/apps/web/app/_mocks/data/place.ts index ca18748..4d056ad 100644 --- a/apps/web/app/_mocks/data/place.ts +++ b/apps/web/app/_mocks/data/place.ts @@ -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' }, @@ -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' }, @@ -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' }], }, diff --git a/apps/web/app/_mocks/handlers/placeHandlers.ts b/apps/web/app/_mocks/handlers/placeHandlers.ts index 1299b78..3d073fa 100644 --- a/apps/web/app/_mocks/handlers/placeHandlers.ts +++ b/apps/web/app/_mocks/handlers/placeHandlers.ts @@ -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) }), ] diff --git a/apps/web/app/_template/template.tsx b/apps/web/app/_template/template.tsx new file mode 100644 index 0000000..33ed2b5 --- /dev/null +++ b/apps/web/app/_template/template.tsx @@ -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 ( + + {children} + + ) +} diff --git a/apps/web/app/categories/[id]/CategoryDetailPage.tsx b/apps/web/app/categories/[id]/CategoryDetailPage.tsx new file mode 100644 index 0000000..c087e51 --- /dev/null +++ b/apps/web/app/categories/[id]/CategoryDetailPage.tsx @@ -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) + } + + return ( + <> +
} + center={ + + + + {activeCategory?.name} + + + } + className={'border-b-1 border-gray-50'} + /> + + + + + + ) +} diff --git a/apps/web/app/categories/[id]/_components/Places/Places.tsx b/apps/web/app/categories/[id]/_components/Places/Places.tsx new file mode 100644 index 0000000..22c4dd4 --- /dev/null +++ b/apps/web/app/categories/[id]/_components/Places/Places.tsx @@ -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 ( +
    + {places.map((place, index) => ( + + ))} +
+ ) +} diff --git a/apps/web/app/categories/[id]/_components/Places/index.tsx b/apps/web/app/categories/[id]/_components/Places/index.tsx new file mode 100644 index 0000000..d7cb449 --- /dev/null +++ b/apps/web/app/categories/[id]/_components/Places/index.tsx @@ -0,0 +1 @@ +export { Places } from './Places' diff --git a/apps/web/app/categories/[id]/_components/RowCategories/CategoryItem.tsx b/apps/web/app/categories/[id]/_components/RowCategories/CategoryItem.tsx new file mode 100644 index 0000000..d47832f --- /dev/null +++ b/apps/web/app/categories/[id]/_components/RowCategories/CategoryItem.tsx @@ -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 ( + + + + {name} + +
+
+ ) +} diff --git a/apps/web/app/categories/[id]/_components/RowCategories/RowCategories.tsx b/apps/web/app/categories/[id]/_components/RowCategories/RowCategories.tsx new file mode 100644 index 0000000..884a103 --- /dev/null +++ b/apps/web/app/categories/[id]/_components/RowCategories/RowCategories.tsx @@ -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 ( +
+ + {categories.map((category) => ( + { + setIdFunc(category.id) + }} + /> + ))} + + +
+ ) +} + +const ScrollHintGradient = () => ( +
+) diff --git a/apps/web/app/categories/[id]/_components/RowCategories/index.tsx b/apps/web/app/categories/[id]/_components/RowCategories/index.tsx new file mode 100644 index 0000000..c0b7517 --- /dev/null +++ b/apps/web/app/categories/[id]/_components/RowCategories/index.tsx @@ -0,0 +1 @@ +export { RowCategories } from './RowCategories' diff --git a/apps/web/app/categories/[id]/_components/index.tsx b/apps/web/app/categories/[id]/_components/index.tsx new file mode 100644 index 0000000..02dc702 --- /dev/null +++ b/apps/web/app/categories/[id]/_components/index.tsx @@ -0,0 +1,2 @@ +export * from './Places' +export * from './RowCategories' diff --git a/apps/web/app/categories/[id]/page.tsx b/apps/web/app/categories/[id]/page.tsx new file mode 100644 index 0000000..fa707ac --- /dev/null +++ b/apps/web/app/categories/[id]/page.tsx @@ -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 ( + { + await queryClient.prefetchQuery(useCategoryQueries.list()) + await queryClient.prefetchQuery(usePlaceQueries.byCategory(id)) + }} + > + + + ) +} +export default Page diff --git a/apps/web/app/categories/template.tsx b/apps/web/app/categories/template.tsx new file mode 100644 index 0000000..23b9a5a --- /dev/null +++ b/apps/web/app/categories/template.tsx @@ -0,0 +1,2 @@ +import Template from '@/_template/template' +export default Template diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx index 0c52fbc..f5d10b0 100644 --- a/apps/web/app/layout.tsx +++ b/apps/web/app/layout.tsx @@ -32,7 +32,7 @@ export default async function RootLayout({
- + {children}
diff --git a/apps/web/app/page.tsx b/apps/web/app/page.tsx index 5b9516e..9f0f1f8 100644 --- a/apps/web/app/page.tsx +++ b/apps/web/app/page.tsx @@ -9,7 +9,7 @@ import { SearchBar } from '@repo/ui/components/SearchBar' import { Column } from '@repo/ui/components/Layout' import { Banner } from '@/_components/Banner' import { - MostLikedPlaces, + MostLikesPlaces, MostViewsPlaces, } from '@/_components/RankingPlaceList' import { Divider } from '@repo/ui/components/Divider' @@ -19,8 +19,8 @@ export default function Page() { { await queryClient.prefetchQuery(useCategoryQueries.list()) - await queryClient.prefetchQuery(usePlaceQueries.rankingList('likes')) - await queryClient.prefetchQuery(usePlaceQueries.rankingList('views')) + await queryClient.prefetchQuery(usePlaceQueries.byRanking('likes')) + await queryClient.prefetchQuery(usePlaceQueries.byRanking('views')) }} > @@ -28,7 +28,7 @@ export default function Page() { - + diff --git a/packages/ui/src/components/Header/Header.tsx b/packages/ui/src/components/Header/Header.tsx index 914c6f2..69e739e 100644 --- a/packages/ui/src/components/Header/Header.tsx +++ b/packages/ui/src/components/Header/Header.tsx @@ -1,18 +1,22 @@ -import { Flex, JustifyBetween } from '../Layout' import type { ReactNode } from 'react' -import { Icon } from '../Icon' import { Text } from '../Text' -import type { IconType } from '../Icon/IconMap' +import { Icon, type IconType } from '../Icon' +import { Flex, JustifyBetween } from '../Layout' +import { cn } from '../../utils/cn' type Props = { left?: ReactNode center?: ReactNode right?: ReactNode + className?: string } -export const Header = ({ left, center, right }: Props) => { +export const Header = ({ left, center, right, className }: Props) => { return ( - + {left} {center} {right ??
{left}
}