From 0fa1c3c1d073d2a78f186d1f27f82606eaed400e Mon Sep 17 00:00:00 2001 From: leeleeleeleejun Date: Sun, 17 Aug 2025 14:11:13 +0900 Subject: [PATCH 01/27] =?UTF-8?q?feat:=20CLIENT=5FPATH=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=ED=95=98=EC=97=AC=20=ED=81=B4=EB=9D=BC=EC=9D=B4?= =?UTF-8?q?=EC=96=B8=ED=8A=B8=20=EB=9D=BC=EC=9A=B0=ED=8C=85=20=EA=B2=BD?= =?UTF-8?q?=EB=A1=9C=20=EC=A0=95=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/app/_constants/path.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/apps/web/app/_constants/path.ts b/apps/web/app/_constants/path.ts index a4a0fe9..ce9af8c 100644 --- a/apps/web/app/_constants/path.ts +++ b/apps/web/app/_constants/path.ts @@ -1,3 +1,14 @@ export const API_PATH = { CATEGORY: '/categories', } + +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}`, + LIKES: '/likes', + PROFILE: '/profile', +} From d8f9d98aa09ff924c3c598001df56c9e6842265e Mon Sep 17 00:00:00 2001 From: leeleeleeleejun Date: Sun, 17 Aug 2025 14:23:14 +0900 Subject: [PATCH 02/27] =?UTF-8?q?feat:=20@repo/components/Icon=EC=97=90?= =?UTF-8?q?=EC=84=9C=20IconType=20=ED=83=80=EC=9E=85=EB=8F=84=20=EB=82=B4?= =?UTF-8?q?=EB=B3=B4=EB=82=B4=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/ui/src/components/Icon/index.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/ui/src/components/Icon/index.tsx b/packages/ui/src/components/Icon/index.tsx index d78603a..1f86e4b 100644 --- a/packages/ui/src/components/Icon/index.tsx +++ b/packages/ui/src/components/Icon/index.tsx @@ -1 +1,2 @@ export { Icon } from './Icon' +export type { IconType } from './IconMap' From cea3ddcca1d09f544d21acac0eadae31a6ee2884 Mon Sep 17 00:00:00 2001 From: leeleeleeleejun Date: Sun, 17 Aug 2025 14:24:33 +0900 Subject: [PATCH 03/27] =?UTF-8?q?feat:=20BottomNavigation=20=EB=B0=8F=20Ta?= =?UTF-8?q?bItem=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../BottomNavigation/BottomNavigation.tsx | 30 +++++++++++++ .../_components/BottomNavigation/TabItem.tsx | 42 +++++++++++++++++++ .../_components/BottomNavigation/index.tsx | 1 + 3 files changed, 73 insertions(+) create mode 100644 apps/web/app/_components/BottomNavigation/BottomNavigation.tsx create mode 100644 apps/web/app/_components/BottomNavigation/TabItem.tsx create mode 100644 apps/web/app/_components/BottomNavigation/index.tsx diff --git a/apps/web/app/_components/BottomNavigation/BottomNavigation.tsx b/apps/web/app/_components/BottomNavigation/BottomNavigation.tsx new file mode 100644 index 0000000..2d1b133 --- /dev/null +++ b/apps/web/app/_components/BottomNavigation/BottomNavigation.tsx @@ -0,0 +1,30 @@ +import { TabItem, type TabItemProps } from './TabItem' +import { cn } from '@repo/ui/utils/cn' +import { JustifyBetween } from '@repo/ui/components/Layout' + +const tabs: TabItemProps[] = [ + { path: 'MAIN', label: '메인', icon: 'home' }, + { path: 'MAP', label: '주변 맛집', icon: 'map' }, + { path: 'PLACE_NEW', label: '', icon: 'circlePlus', iconSize: 50 }, + { path: 'LIKES', label: '찜', icon: 'navHeart' }, + { path: 'PROFILE', label: '내 정보', icon: 'navUser' }, +] + +export const BottomNavigation = () => { + return ( + + {tabs.map((tab: TabItemProps) => ( + + ))} + + ) +} diff --git a/apps/web/app/_components/BottomNavigation/TabItem.tsx b/apps/web/app/_components/BottomNavigation/TabItem.tsx new file mode 100644 index 0000000..08b8764 --- /dev/null +++ b/apps/web/app/_components/BottomNavigation/TabItem.tsx @@ -0,0 +1,42 @@ +'use client' + +import Link from 'next/link' +import { usePathname } from 'next/navigation' +import { CLIENT_PATH } from '@/_constants/path' +import { cn } from '@repo/ui/utils/cn' +import { Text } from '@repo/ui/components/Text' +import { Icon, IconType } from '@repo/ui/components/Icon' + +export type TabItemProps = { + path: keyof Pick< + typeof CLIENT_PATH, + 'MAIN' | 'MAP' | 'LIKES' | 'PROFILE' | 'PLACE_NEW' + > + icon: IconType + iconSize?: number + label?: string +} + +export const TabItem = ({ path, label, icon, iconSize = 26 }: TabItemProps) => { + const pathname = usePathname() + const href = CLIENT_PATH[path] + const active = pathname === href + + return ( + + + {label && ( + + {label} + + )} + + ) +} diff --git a/apps/web/app/_components/BottomNavigation/index.tsx b/apps/web/app/_components/BottomNavigation/index.tsx new file mode 100644 index 0000000..ae9b6be --- /dev/null +++ b/apps/web/app/_components/BottomNavigation/index.tsx @@ -0,0 +1 @@ +export { BottomNavigation } from './BottomNavigation' From 8e99bd1fb5c788f671d98835f853b4a78c692f4a Mon Sep 17 00:00:00 2001 From: leeleeleeleejun Date: Sun, 17 Aug 2025 15:49:25 +0900 Subject: [PATCH 04/27] =?UTF-8?q?feat:=20SearchBar=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=EC=97=90=20className=20prop=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/ui/src/components/SearchBar/SearchBar.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/ui/src/components/SearchBar/SearchBar.tsx b/packages/ui/src/components/SearchBar/SearchBar.tsx index 0ec8bbe..dfe6b4b 100644 --- a/packages/ui/src/components/SearchBar/SearchBar.tsx +++ b/packages/ui/src/components/SearchBar/SearchBar.tsx @@ -3,7 +3,13 @@ import { Flex } from '../Layout' import { cn } from '../../utils/cn' import { Text } from '../Text' -export const SearchBar = ({ href }: { href: string }) => { +export const SearchBar = ({ + href, + className, +}: { + href: string + className?: string +}) => { return ( { 'ui:p-3.5', 'ui:items-center', 'ui:gap-2', + className, )} aria-label={'검색 페이지로 이동'} > From 45bd18d9301f1cd617576fef076bed1e54247cfa Mon Sep 17 00:00:00 2001 From: leeleeleeleejun Date: Sun, 17 Aug 2025 16:02:49 +0900 Subject: [PATCH 05/27] =?UTF-8?q?chore:=20zod=20=ED=8C=A8=ED=82=A4?= =?UTF-8?q?=EC=A7=80=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/package.json | 1 + pnpm-lock.yaml | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/apps/web/package.json b/apps/web/package.json index 20f9d4c..853a1e8 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -25,6 +25,7 @@ "next": "^15.4.2", "react": "^19.1.0", "react-dom": "^19.1.0", + "zod": "^4.0.17", "zustand": "^5.0.7" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6a93304..537ed87 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -141,6 +141,9 @@ importers: react-dom: specifier: ^19.1.0 version: 19.1.0(react@19.1.0) + zod: + specifier: ^4.0.17 + version: 4.0.17 zustand: specifier: ^5.0.7 version: 5.0.7(@types/react@19.1.0)(react@19.1.0) @@ -10747,6 +10750,10 @@ packages: engines: {node: '>=18'} dev: true + /zod@4.0.17: + resolution: {integrity: sha512-1PHjlYRevNxxdy2JZ8JcNAw7rX8V9P1AKkP+x/xZfxB0K5FYfuV+Ug6P/6NVSR2jHQ+FzDDoDHS04nYUsOIyLQ==} + dev: false + /zustand@5.0.7(@types/react@19.1.0)(react@19.1.0): resolution: {integrity: sha512-Ot6uqHDW/O2VdYsKLLU8GQu8sCOM1LcoE8RwvLv9uuRT9s6SOHCKs0ZEOhxg+I1Ld+A1Q5lwx+UlKXXUoCZITg==} engines: {node: '>=12.20.0'} From 359ed037c23f0fab50e3d7beeb209198ac913c1e Mon Sep 17 00:00:00 2001 From: leeleeleeleejun Date: Sun, 17 Aug 2025 16:08:49 +0900 Subject: [PATCH 06/27] =?UTF-8?q?feat:=20HydrationBoundaryPage=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=EC=9D=98=20=EC=BF=BC=EB=A6=AC=20?= =?UTF-8?q?=EA=B5=AC=EC=84=B1=20=ED=83=80=EC=9E=85=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?=EB=B0=8F=20=EC=A3=BC=EC=84=9D=20=EA=B0=9C=EC=84=A0=20-=20query?= =?UTF-8?q?Options=EC=9D=98=20Query=20factory=20=ED=98=95=ED=83=9C?= =?UTF-8?q?=EB=A1=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/app/HydrationBoundaryPage.tsx | 49 ++++++++++---------------- 1 file changed, 19 insertions(+), 30 deletions(-) diff --git a/apps/web/app/HydrationBoundaryPage.tsx b/apps/web/app/HydrationBoundaryPage.tsx index 9f208cf..d086c12 100644 --- a/apps/web/app/HydrationBoundaryPage.tsx +++ b/apps/web/app/HydrationBoundaryPage.tsx @@ -2,53 +2,42 @@ import { dehydrate, HydrationBoundary, QueryClient, + UseQueryOptions, } from '@tanstack/react-query' import { ReactNode } from 'react' /** - * React Query 쿼리 구성 타입 + * 서버 컴포넌트에서 React Query 쿼리를 미리 요청(prefetch)하고, + * 클라이언트로 전달하여 초기 데이터를 사용할 수 있도록 해주는 컴포넌트입니다. * - * @property queryKey - React Query에서 사용할 쿼리 키 - * @property queryFn - 데이터를 가져오는 비동기 함수 - */ -type QueryConfig = { - queryKey: string[] - queryFn: () => Promise -} - -/** - * 서버 컴포넌트에서 React Query 쿼리를 미리 요청(prefetch)한 뒤, - * dehydrate 상태를 클라이언트에 전달하기 위한 컴포넌트. + * @param queries - prefetch할 React Query 옵션 배열. `queryOptions()` 반환값을 전달하는 것이 안전합니다. + * @param children - HydrationBoundary로 감쌀 React 노드 + * + * @returns HydrationBoundary로 감싼 children * * @example * ```tsx - * - * + * const queries = [useCategoryQueries.list()] + * + * + * * * ``` - * - * @param queries - 사전 요청할 쿼리들의 배열 - * @param children - HydrationBoundary로 감쌀 React 노드 */ -export const HydrationBoundaryPage = async ({ +export const HydrationBoundaryPage = async < + TQueryFnData, + TError, + TData, + TQueryKey extends readonly unknown[], +>({ queries, children, }: { - queries: QueryConfig[] + queries: UseQueryOptions[] children: ReactNode }) => { const queryClient = new QueryClient() - - await Promise.all( - queries.map(({ queryKey, queryFn }) => - queryClient.prefetchQuery({ queryKey, queryFn }), - ), - ) + await Promise.all(queries.map((query) => queryClient.prefetchQuery(query))) return ( From 33bd5ea1be2715f3f634e76927e7ff27e0745faf Mon Sep 17 00:00:00 2001 From: leeleeleeleejun Date: Sun, 17 Aug 2025 16:10:02 +0900 Subject: [PATCH 07/27] =?UTF-8?q?feat:=20=EC=B9=B4=ED=85=8C=EA=B3=A0?= =?UTF-8?q?=EB=A6=AC=20api=20=EA=B4=80=EB=A0=A8=20=EC=BF=BC=EB=A6=AC=20?= =?UTF-8?q?=EB=B0=8F=20=EC=8A=A4=ED=82=A4=EB=A7=88=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/app/_apis/queries/category.ts | 17 +++++++++++++++++ apps/web/app/_apis/schemas/category.ts | 10 ++++++++++ apps/web/app/_apis/services/category.ts | 8 ++++++++ 3 files changed, 35 insertions(+) create mode 100644 apps/web/app/_apis/queries/category.ts create mode 100644 apps/web/app/_apis/schemas/category.ts create mode 100644 apps/web/app/_apis/services/category.ts diff --git a/apps/web/app/_apis/queries/category.ts b/apps/web/app/_apis/queries/category.ts new file mode 100644 index 0000000..83519e2 --- /dev/null +++ b/apps/web/app/_apis/queries/category.ts @@ -0,0 +1,17 @@ +import { queryOptions } from '@tanstack/react-query' +import { getCategories } from '@/_apis/services/category' + +export const CategoryQueryKeys = { + all: () => ['category'] as const, + list: () => [...CategoryQueryKeys.all(), 'list'] as const, + items: (categoryId: string) => + [...CategoryQueryKeys.all(), 'items', categoryId] as const, +} + +export const useCategoryQueries = { + list: () => + queryOptions({ + queryKey: CategoryQueryKeys.list(), + queryFn: getCategories, + }), +} diff --git a/apps/web/app/_apis/schemas/category.ts b/apps/web/app/_apis/schemas/category.ts new file mode 100644 index 0000000..292973f --- /dev/null +++ b/apps/web/app/_apis/schemas/category.ts @@ -0,0 +1,10 @@ +import { z } from 'zod' +import { IconList } from '@repo/ui/components/Icon/IconMap' + +export const CategorySchema = z.object({ + id: z.number().transform(String), + name: z.string(), + iconKey: z.enum(IconList), +}) + +export type Category = z.infer diff --git a/apps/web/app/_apis/services/category.ts b/apps/web/app/_apis/services/category.ts new file mode 100644 index 0000000..0e84cce --- /dev/null +++ b/apps/web/app/_apis/services/category.ts @@ -0,0 +1,8 @@ +import axiosInstance from '@/_lib/axiosInstance' +import { API_PATH } from '@/_constants/path' +import { CategorySchema, Category } from '../schemas/category' + +export const getCategories = async (): Promise => { + const { data } = await axiosInstance.get(API_PATH.CATEGORY) + return CategorySchema.array().parse(data) +} From ecb34f12fb6bc78e622884662cb0ccedac0b745f Mon Sep 17 00:00:00 2001 From: leeleeleeleejun Date: Sun, 17 Aug 2025 16:10:43 +0900 Subject: [PATCH 08/27] =?UTF-8?q?feat:=20=EB=A9=94=EC=9D=B8=ED=99=94?= =?UTF-8?q?=EB=A9=B4=20=EC=B9=B4=ED=85=8C=EA=B3=A0=EB=A6=AC=20=EB=AA=A9?= =?UTF-8?q?=EB=A1=9D=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/_components/Categories/Categories.tsx | 20 ++++++++++++++++ .../_components/Categories/CategoryItem.tsx | 23 +++++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 apps/web/app/_components/Categories/Categories.tsx create mode 100644 apps/web/app/_components/Categories/CategoryItem.tsx diff --git a/apps/web/app/_components/Categories/Categories.tsx b/apps/web/app/_components/Categories/Categories.tsx new file mode 100644 index 0000000..d68260f --- /dev/null +++ b/apps/web/app/_components/Categories/Categories.tsx @@ -0,0 +1,20 @@ +'use client' + +import { useSuspenseQuery } from '@tanstack/react-query' +import { useCategoryQueries } from '@/_apis/queries/category' +import { cn } from '@repo/ui/utils/cn' +import { CategoryItem } from './CategoryItem' + +export default function Categories() { + const { data: categories } = useSuspenseQuery(useCategoryQueries.list()) + + return ( +
+ {categories.map((category) => ( + + ))} +
+ ) +} diff --git a/apps/web/app/_components/Categories/CategoryItem.tsx b/apps/web/app/_components/Categories/CategoryItem.tsx new file mode 100644 index 0000000..0d3d406 --- /dev/null +++ b/apps/web/app/_components/Categories/CategoryItem.tsx @@ -0,0 +1,23 @@ +import { CLIENT_PATH } from '@/_constants/path' +import { Category } from '@/_apis/schemas/category' +import { Icon } from '@repo/ui/components/Icon' +import { Text } from '@repo/ui/components/Text' +import { Column } from '@repo/ui/components/Layout' + +export const CategoryItem = ({ id, name, iconKey }: Category) => ( + + + + {name} + + +) From 3751ed0b4c0c7a82b4aaf59aa85d66404cce5c0d Mon Sep 17 00:00:00 2001 From: leeleeleeleejun Date: Sun, 17 Aug 2025 17:14:53 +0900 Subject: [PATCH 09/27] =?UTF-8?q?feat:=20Categories=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=EB=A5=BC=20=ED=99=94=EC=82=B4=ED=91=9C=20?= =?UTF-8?q?=ED=95=A8=EC=88=98=EB=A1=9C=20=EB=B3=80=EA=B2=BD=ED=95=98?= =?UTF-8?q?=EA=B3=A0=20index=20=ED=8C=8C=EC=9D=BC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/app/_components/Categories/Categories.tsx | 2 +- apps/web/app/_components/Categories/index.tsx | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 apps/web/app/_components/Categories/index.tsx diff --git a/apps/web/app/_components/Categories/Categories.tsx b/apps/web/app/_components/Categories/Categories.tsx index d68260f..5954357 100644 --- a/apps/web/app/_components/Categories/Categories.tsx +++ b/apps/web/app/_components/Categories/Categories.tsx @@ -5,7 +5,7 @@ import { useCategoryQueries } from '@/_apis/queries/category' import { cn } from '@repo/ui/utils/cn' import { CategoryItem } from './CategoryItem' -export default function Categories() { +export const Categories = () => { const { data: categories } = useSuspenseQuery(useCategoryQueries.list()) return ( diff --git a/apps/web/app/_components/Categories/index.tsx b/apps/web/app/_components/Categories/index.tsx new file mode 100644 index 0000000..fac3ddb --- /dev/null +++ b/apps/web/app/_components/Categories/index.tsx @@ -0,0 +1 @@ +export { Categories } from './Categories' From 5550d75534d9b534f6932ebfb5bc43a8cd646438 Mon Sep 17 00:00:00 2001 From: leeleeleeleejun Date: Sun, 17 Aug 2025 17:20:15 +0900 Subject: [PATCH 10/27] =?UTF-8?q?chore:=20keen-slider=20=ED=8C=A8=ED=82=A4?= =?UTF-8?q?=EC=A7=80=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/package.json | 1 + pnpm-lock.yaml | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/apps/web/package.json b/apps/web/package.json index 853a1e8..c599a0e 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -21,6 +21,7 @@ "@tanstack/react-query": "^5.84.1", "@tanstack/react-query-devtools": "^5.84.1", "axios": "^1.11.0", + "keen-slider": "^6.8.6", "motion": "^12.23.12", "next": "^15.4.2", "react": "^19.1.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 537ed87..ebff357 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -129,6 +129,9 @@ importers: axios: specifier: ^1.11.0 version: 1.11.0 + keen-slider: + specifier: ^6.8.6 + version: 6.8.6 motion: specifier: ^12.23.12 version: 12.23.12(react-dom@19.1.0)(react@19.1.0) @@ -7535,6 +7538,10 @@ packages: object.values: 1.2.1 dev: true + /keen-slider@6.8.6: + resolution: {integrity: sha512-dcEQ7GDBpCjUQA8XZeWh3oBBLLmyn8aoeIQFGL/NTVkoEOsmlnXqA4QykUm/SncolAZYGsEk/PfUhLZ7mwMM2w==} + dev: false + /keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} dependencies: From 479abb25b2957dde52be8ae17c882b008724a40a Mon Sep 17 00:00:00 2001 From: leeleeleeleejun Date: Sun, 17 Aug 2025 17:43:47 +0900 Subject: [PATCH 11/27] =?UTF-8?q?feat:=20Banner=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EC=8A=AC?= =?UTF-8?q?=EB=9D=BC=EC=9D=B4=EB=8D=94=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/app/_components/Banner/Banner.tsx | 58 ++++++++++++++++++++++ apps/web/app/_components/Banner/index.tsx | 1 + apps/web/app/layout.tsx | 1 + 3 files changed, 60 insertions(+) create mode 100644 apps/web/app/_components/Banner/Banner.tsx create mode 100644 apps/web/app/_components/Banner/index.tsx diff --git a/apps/web/app/_components/Banner/Banner.tsx b/apps/web/app/_components/Banner/Banner.tsx new file mode 100644 index 0000000..cdb0dd0 --- /dev/null +++ b/apps/web/app/_components/Banner/Banner.tsx @@ -0,0 +1,58 @@ +'use client' + +import 'keen-slider/keen-slider.min.css' +import { useKeenSlider } from 'keen-slider/react' + +type Props = { + contents: React.ReactNode[] +} + +export const Banner = ({ contents }: Props) => { + const [sliderRef] = useKeenSlider( + { + loop: true, + }, + [ + (slider) => { + let timeout: ReturnType + 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() + }) + slider.on('dragStarted', clearNextTimeout) + slider.on('animationEnded', nextTimeout) + slider.on('updated', nextTimeout) + }, + ], + ) + + return ( + <> +
+ {contents.map((content, index) => ( +
+ {content} +
+ ))} +
+ + ) +} diff --git a/apps/web/app/_components/Banner/index.tsx b/apps/web/app/_components/Banner/index.tsx new file mode 100644 index 0000000..1a83a85 --- /dev/null +++ b/apps/web/app/_components/Banner/index.tsx @@ -0,0 +1 @@ +export { Banner } from './Banner' diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx index 84b9428..41cf46c 100644 --- a/apps/web/app/layout.tsx +++ b/apps/web/app/layout.tsx @@ -1,5 +1,6 @@ import '@repo/ui/styles.css' import './globals.css' +import 'keen-slider/keen-slider.min.css' import type { Metadata } from 'next' import QueryProvider from './QueryClientProvider' import localFont from 'next/font/local' From c70d5f9bb95b045c75de858192ea1a41a122589c Mon Sep 17 00:00:00 2001 From: leeleeleeleejun Date: Sun, 17 Aug 2025 17:44:25 +0900 Subject: [PATCH 12/27] =?UTF-8?q?feat:=20OnlyLeftHeader=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=EC=97=90=EC=84=9C=20Text=20?= =?UTF-8?q?=EC=9A=94=EC=86=8C=EB=A5=BC=20h1=20=ED=83=9C=EA=B7=B8=EB=A1=9C?= =?UTF-8?q?=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/ui/src/components/Header/Header.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/ui/src/components/Header/Header.tsx b/packages/ui/src/components/Header/Header.tsx index bf0461d..914c6f2 100644 --- a/packages/ui/src/components/Header/Header.tsx +++ b/packages/ui/src/components/Header/Header.tsx @@ -29,6 +29,8 @@ export const OnlyLeftHeader = ({ }) => ( - {name} + + {name} + ) From 1ee478231ae041f86061bb0bb94176bd4fdef80e Mon Sep 17 00:00:00 2001 From: leeleeleeleejun Date: Sun, 17 Aug 2025 20:58:03 +0900 Subject: [PATCH 13/27] =?UTF-8?q?refactor:=20Chip=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=EC=97=90=EC=84=9C=20chipType=EC=9D=84=20icon?= =?UTF-8?q?=EA=B3=BC=20label=EB=A1=9C=20=EB=B3=80=EA=B2=BD=20-=20Chip.stor?= =?UTF-8?q?ies.tsx=20=ED=95=A8=EA=BB=98=20=EC=88=98=EC=A0=95=20-=20?= =?UTF-8?q?=EA=B8=B0=EB=B3=B8=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20div?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/src/components/Chip/Chip.stories.tsx | 51 +++++++++++++------ packages/ui/src/components/Chip/Chip.tsx | 39 ++++---------- 2 files changed, 47 insertions(+), 43 deletions(-) diff --git a/packages/ui/src/components/Chip/Chip.stories.tsx b/packages/ui/src/components/Chip/Chip.stories.tsx index d57c3f1..94f1c25 100644 --- a/packages/ui/src/components/Chip/Chip.stories.tsx +++ b/packages/ui/src/components/Chip/Chip.stories.tsx @@ -1,6 +1,34 @@ import type { Meta, StoryObj } from '@storybook/nextjs' import { Chip } from './Chip' import { Flex } from '../Layout' +import { IconType } from '../Icon' + +const CHIP_TAGS: { + id: number + label: string + icon: IconType +}[] = [ + { + id: 1, + label: '혼밥하기 좋은', + icon: 'fingerUp', + }, + { + id: 2, + label: '가성비 좋은', + icon: 'calculator', + }, + { + id: 3, + label: '분위기 좋은', + icon: 'blingBling', + }, + { + id: 4, + label: '친절해요', + icon: 'waiter', + }, +] const meta: Meta = { title: 'Components/Chip', @@ -14,10 +42,9 @@ type Story = StoryObj export const Default: Story = { render: () => ( - - - - + {CHIP_TAGS.map((category) => ( + + ))} ), } @@ -25,19 +52,13 @@ export const Default: Story = { export const ClickableChips: Story = { render: () => ( - {( - [ - 'SOLO_FRIENDLY', - 'VALUE_FOR_MONEY', - 'GOOD_AMBIENCE', - 'KIND_SERVICE', - ] as const - ).map((chipType) => ( + {CHIP_TAGS.map((category) => ( { - console.log(`${chipType} 클릭됨!`) + console.log(`${category.label} 클릭됨!`) }} /> ))} diff --git a/packages/ui/src/components/Chip/Chip.tsx b/packages/ui/src/components/Chip/Chip.tsx index 55bc627..4397e7c 100644 --- a/packages/ui/src/components/Chip/Chip.tsx +++ b/packages/ui/src/components/Chip/Chip.tsx @@ -3,34 +3,14 @@ import type { ElementType, JSX, PropsWithChildren } from 'react' import type { PolymorphicComponentProps } from '../../polymorphics' import { useState } from 'react' import { cn } from '../../utils/cn' -import { Icon } from '../Icon' +import { Icon, type IconType } from '../Icon' import { Text } from '../Text' -const CHIP_TAGS = { - SOLO_FRIENDLY: { - label: '혼밥하기 좋은', - icon: 'fingerUp', - }, - VALUE_FOR_MONEY: { - label: '가성비 좋은', - icon: 'calculator', - }, - GOOD_AMBIENCE: { - label: '분위기 좋은', - icon: 'blingBling', - }, - KIND_SERVICE: { - label: '친절해요', - icon: 'waiter', - }, -} as const - -type ChipTagKey = keyof typeof CHIP_TAGS - export type ChipProps = PolymorphicComponentProps< C, { - chipType: ChipTagKey + icon: IconType + label: string onToggle?: () => void } > @@ -50,24 +30,27 @@ export type ChipType = ( * * @param as 렌더링할 HTML 태그 또는 컴포넌트 * @param className 추가 CSS 클래스 - * @param chipType 표시할 Chip 타입 + * @param icon 표시할 아이콘 타입 + * @param label Chip에 표시할 텍스트 라벨 * @param onToggle 클릭 시 실행할 콜백 함수 * @param restProps 나머지 Props * * @returns 렌더링된 Chip 요소 * * @example - * console.log('클릭됨')} /> + * ```tsx + * console.log('클릭됨')} /> + * ``` */ export const Chip: ChipType = ({ as, className, - chipType, + icon, + label, onToggle, ...restProps }) => { - const Component = as || 'button' - const { icon, label } = CHIP_TAGS[chipType] + const Component = as || 'div' const [isActive, setIsActive] = useState(false) const onClick = () => { From 33ff257fbc49aa8e456645f941dd6c45c05769cd Mon Sep 17 00:00:00 2001 From: leeleeleeleejun Date: Sun, 17 Aug 2025 21:25:56 +0900 Subject: [PATCH 14/27] =?UTF-8?q?feat:=20Divider=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20index=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=EC=97=90=EC=84=9C=20=EB=82=B4=EB=B3=B4?= =?UTF-8?q?=EB=82=B4=EA=B8=B0=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/ui/src/components/Divider/Divider.tsx | 5 +++++ packages/ui/src/components/Divider/index.tsx | 1 + 2 files changed, 6 insertions(+) create mode 100644 packages/ui/src/components/Divider/Divider.tsx create mode 100644 packages/ui/src/components/Divider/index.tsx diff --git a/packages/ui/src/components/Divider/Divider.tsx b/packages/ui/src/components/Divider/Divider.tsx new file mode 100644 index 0000000..39a0ec8 --- /dev/null +++ b/packages/ui/src/components/Divider/Divider.tsx @@ -0,0 +1,5 @@ +import { cn } from '../../utils/cn' + +export const Divider = ({ className }: { className?: string }) => ( +
+) diff --git a/packages/ui/src/components/Divider/index.tsx b/packages/ui/src/components/Divider/index.tsx new file mode 100644 index 0000000..465ccc7 --- /dev/null +++ b/packages/ui/src/components/Divider/index.tsx @@ -0,0 +1 @@ +export { Divider } from './Divider' From 991540b23ca06545a78e39db8623ebb7ed181b39 Mon Sep 17 00:00:00 2001 From: leeleeleeleejun Date: Sun, 17 Aug 2025 21:45:45 +0900 Subject: [PATCH 15/27] =?UTF-8?q?feat:=20Banner=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=EC=97=90=20=EC=B5=9C=EC=86=8C=20=EB=86=92?= =?UTF-8?q?=EC=9D=B4=20=EC=86=8D=EC=84=B1=20=EC=B6=94=EA=B0=80=20=EB=B0=8F?= =?UTF-8?q?=20jsdoc=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/app/_components/Banner/Banner.tsx | 41 ++++++++++++++++------ 1 file changed, 31 insertions(+), 10 deletions(-) diff --git a/apps/web/app/_components/Banner/Banner.tsx b/apps/web/app/_components/Banner/Banner.tsx index cdb0dd0..f668009 100644 --- a/apps/web/app/_components/Banner/Banner.tsx +++ b/apps/web/app/_components/Banner/Banner.tsx @@ -5,9 +5,32 @@ import { useKeenSlider } from 'keen-slider/react' type Props = { contents: React.ReactNode[] + minHeight?: number } -export const Banner = ({ contents }: Props) => { +/** + * Banner 컴포넌트 + * + * - 여러 콘텐츠를 순차적으로 보여주는 슬라이더 배너입니다. + * - `keen-slider`를 기반으로 자동 재생(loop) 기능을 제공합니다. + * - 마우스를 올리면 자동 재생이 일시 정지되고, 마우스를 치우면 다시 재생됩니다. + * + * @param contents 렌더링할 React 노드 배열 (각각의 배너 콘텐츠) + * @param minHeight 배너의 최소 높이(px). 기본값은 150입니다. + * + * @example + * ```tsx + * 배너 1, + *
배너 2
, + *
배너 3
, + * ]} + * minHeight={200} + * /> + * ``` + */ +export const Banner = ({ contents, minHeight = 150 }: Props) => { const [sliderRef] = useKeenSlider( { loop: true, @@ -45,14 +68,12 @@ export const Banner = ({ contents }: Props) => { ) return ( - <> -
- {contents.map((content, index) => ( -
- {content} -
- ))} -
- +
+ {contents.map((content, index) => ( +
+ {content} +
+ ))} +
) } From c00e778597806ba6683b6658b57c672a25d9f6da Mon Sep 17 00:00:00 2001 From: leeleeleeleejun Date: Sun, 17 Aug 2025 21:47:13 +0900 Subject: [PATCH 16/27] =?UTF-8?q?feat:=20HydrationBoundaryPage=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=EC=97=90=EC=84=9C=20prefetch=20?= =?UTF-8?q?=ED=8C=8C=EB=9D=BC=EB=AF=B8=ED=84=B0=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?=EB=B0=8F=20=EC=BF=BC=EB=A6=AC=20=EC=98=B5=EC=85=98=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/app/HydrationBoundaryPage.tsx | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/apps/web/app/HydrationBoundaryPage.tsx b/apps/web/app/HydrationBoundaryPage.tsx index d086c12..335af0f 100644 --- a/apps/web/app/HydrationBoundaryPage.tsx +++ b/apps/web/app/HydrationBoundaryPage.tsx @@ -2,7 +2,6 @@ import { dehydrate, HydrationBoundary, QueryClient, - UseQueryOptions, } from '@tanstack/react-query' import { ReactNode } from 'react' @@ -10,34 +9,32 @@ import { ReactNode } from 'react' * 서버 컴포넌트에서 React Query 쿼리를 미리 요청(prefetch)하고, * 클라이언트로 전달하여 초기 데이터를 사용할 수 있도록 해주는 컴포넌트입니다. * - * @param queries - prefetch할 React Query 옵션 배열. `queryOptions()` 반환값을 전달하는 것이 안전합니다. * @param children - HydrationBoundary로 감쌀 React 노드 + * @param prefetch - 서버에서 실행할 prefetch 함수. QueryClient를 받아서 필요한 쿼리를 모두 prefetch하도록 구현합니다. * * @returns HydrationBoundary로 감싼 children * * @example * ```tsx - * const queries = [useCategoryQueries.list()] - * - * + * { + * await queryClient.prefetchQuery(useCategoryQueries.list()) + * await queryClient.prefetchQuery(usePlaceQueries.rankingList('likes')) + * }} + * > * * * ``` */ -export const HydrationBoundaryPage = async < - TQueryFnData, - TError, - TData, - TQueryKey extends readonly unknown[], ->({ - queries, +export const HydrationBoundaryPage = async ({ children, + prefetch, }: { - queries: UseQueryOptions[] children: ReactNode + prefetch: (queryClient: QueryClient) => Promise }) => { const queryClient = new QueryClient() - await Promise.all(queries.map((query) => queryClient.prefetchQuery(query))) + await prefetch(queryClient) return ( From 03a8dda932b7cf9ef6a645b48197da3a62b835eb Mon Sep 17 00:00:00 2001 From: leeleeleeleejun Date: Sun, 17 Aug 2025 21:55:04 +0900 Subject: [PATCH 17/27] =?UTF-8?q?feat:=20RankingPlace=20=EA=B4=80=EB=A0=A8?= =?UTF-8?q?=20=ED=95=B8=EB=93=A4=EB=9F=AC=20=EB=B0=8F=20=EC=BF=BC=EB=A6=AC?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80,=20API=20=EA=B2=BD=EB=A1=9C=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20-=20Mock=20Data=20=EB=B0=8F=20=ED=95=B8=EB=93=A4?= =?UTF-8?q?=EB=9F=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/app/_apis/queries/place.ts | 17 +++++++++ apps/web/app/_apis/schemas/place.ts | 20 ++++++++++ apps/web/app/_apis/services/place.ts | 14 +++++++ apps/web/app/_constants/path.ts | 3 ++ apps/web/app/_mocks/data/place.ts | 38 +++++++++++++++++++ apps/web/app/_mocks/handlers/index.ts | 5 ++- apps/web/app/_mocks/handlers/placeHandlers.ts | 18 +++++++++ 7 files changed, 113 insertions(+), 2 deletions(-) create mode 100644 apps/web/app/_apis/queries/place.ts create mode 100644 apps/web/app/_apis/schemas/place.ts create mode 100644 apps/web/app/_apis/services/place.ts create mode 100644 apps/web/app/_mocks/data/place.ts create mode 100644 apps/web/app/_mocks/handlers/placeHandlers.ts diff --git a/apps/web/app/_apis/queries/place.ts b/apps/web/app/_apis/queries/place.ts new file mode 100644 index 0000000..4553381 --- /dev/null +++ b/apps/web/app/_apis/queries/place.ts @@ -0,0 +1,17 @@ +import { queryOptions } from '@tanstack/react-query' +import { RankingPlaceSort } from '@/_apis/schemas/place' +import { getRankingPlaces } from '@/_apis/services/place' + +export const PlaceQueryKeys = { + all: () => ['place'] as const, + rankingList: (sort: RankingPlaceSort) => + [...PlaceQueryKeys.all(), 'ranking', sort] as const, +} + +export const usePlaceQueries = { + rankingList: (sort: RankingPlaceSort) => + queryOptions({ + queryKey: PlaceQueryKeys.rankingList(sort), + queryFn: () => getRankingPlaces(sort), + }), +} diff --git a/apps/web/app/_apis/schemas/place.ts b/apps/web/app/_apis/schemas/place.ts new file mode 100644 index 0000000..410ebaf --- /dev/null +++ b/apps/web/app/_apis/schemas/place.ts @@ -0,0 +1,20 @@ +import { z } from 'zod' +import { CategorySchema } from '@/_apis/schemas/category' + +export const BasePlaceSchema = z.object({ + placeId: z.number().transform(String), + placeName: z.string(), + address: z.string(), + categories: z.array(CategorySchema), + tags: z.array(CategorySchema), +}) + +export type RankingPlaceSort = 'views' | 'likes' + +export const RankingPlaceSchema = BasePlaceSchema.extend({ + isLiked: z.boolean(), + likeCount: z.number(), +}) + +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 new file mode 100644 index 0000000..3221457 --- /dev/null +++ b/apps/web/app/_apis/services/place.ts @@ -0,0 +1,14 @@ +import axiosInstance from '@/_lib/axiosInstance' +import { API_PATH } from '@/_constants/path' +import { + type RankingPlace, + type RankingPlaceSort, + RankingPlaceSchema, +} from '../schemas/place' + +export const getRankingPlaces = async ( + sort: RankingPlaceSort, +): Promise => { + const { data } = await axiosInstance.get(API_PATH.RANKING(sort)) + return RankingPlaceSchema.array().parse(data) +} diff --git a/apps/web/app/_constants/path.ts b/apps/web/app/_constants/path.ts index ce9af8c..933821b 100644 --- a/apps/web/app/_constants/path.ts +++ b/apps/web/app/_constants/path.ts @@ -1,5 +1,8 @@ +import { RankingPlaceSort } from '@/_apis/schemas/place' + export const API_PATH = { CATEGORY: '/categories', + 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 new file mode 100644 index 0000000..ca18748 --- /dev/null +++ b/apps/web/app/_mocks/data/place.ts @@ -0,0 +1,38 @@ +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' }], + }, +] diff --git a/apps/web/app/_mocks/handlers/index.ts b/apps/web/app/_mocks/handlers/index.ts index 31e9e0b..152e181 100644 --- a/apps/web/app/_mocks/handlers/index.ts +++ b/apps/web/app/_mocks/handlers/index.ts @@ -1,3 +1,4 @@ -import { CategoryHandlers } from '@/_mocks/handlers/categoryHandlers' +import { CategoryHandlers } from './categoryHandlers' +import { PlaceHandlers } from './placeHandlers' -export const handlers = [...CategoryHandlers] +export const handlers = [...CategoryHandlers, ...PlaceHandlers] diff --git a/apps/web/app/_mocks/handlers/placeHandlers.ts b/apps/web/app/_mocks/handlers/placeHandlers.ts new file mode 100644 index 0000000..1299b78 --- /dev/null +++ b/apps/web/app/_mocks/handlers/placeHandlers.ts @@ -0,0 +1,18 @@ +import { http, HttpResponse } from 'msw' +import { API_PATH } from '@/_constants/path' +import { RankingPlaces } from '../data/place' + +const BASE_URL = process.env.NEXT_PUBLIC_API_URL || '' + +const addBaseUrl = (path: string) => { + return `${BASE_URL}${path}` +} + +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) + }), +] From e72bfcaa80f7d52af2b3269f493d49376bfacdcc Mon Sep 17 00:00:00 2001 From: leeleeleeleejun Date: Sun, 17 Aug 2025 22:00:18 +0900 Subject: [PATCH 18/27] =?UTF-8?q?feat:=20Chip=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=EC=97=90=EC=84=9C=20=EC=95=84=EC=9D=B4?= =?UTF-8?q?=EC=BD=98=20=ED=81=AC=EA=B8=B0=EB=A5=BC=2014=EC=97=90=EC=84=9C?= =?UTF-8?q?=2016=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/ui/src/components/Chip/Chip.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ui/src/components/Chip/Chip.tsx b/packages/ui/src/components/Chip/Chip.tsx index 4397e7c..bdf4fc7 100644 --- a/packages/ui/src/components/Chip/Chip.tsx +++ b/packages/ui/src/components/Chip/Chip.tsx @@ -77,7 +77,7 @@ export const Chip: ChipType = ({ onClick={onClick} {...restProps} > - + {label} From 746f1b45af495233e4add21d46acfd639c74678c Mon Sep 17 00:00:00 2001 From: leeleeleeleejun Date: Sun, 17 Aug 2025 22:00:56 +0900 Subject: [PATCH 19/27] =?UTF-8?q?feat:=20SubTitle=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/app/_components/SubTitle/SubTitle.tsx | 17 +++++++++++++++++ apps/web/app/_components/SubTitle/index.tsx | 1 + 2 files changed, 18 insertions(+) create mode 100644 apps/web/app/_components/SubTitle/SubTitle.tsx create mode 100644 apps/web/app/_components/SubTitle/index.tsx diff --git a/apps/web/app/_components/SubTitle/SubTitle.tsx b/apps/web/app/_components/SubTitle/SubTitle.tsx new file mode 100644 index 0000000..73137c8 --- /dev/null +++ b/apps/web/app/_components/SubTitle/SubTitle.tsx @@ -0,0 +1,17 @@ +import { Flex } from '@repo/ui/components/Layout' +import { Icon, IconType } from '@repo/ui/components/Icon' +import { Text } from '@repo/ui/components/Text' + +type Props = { + icon: IconType + title: string +} + +export const SubTitle = ({ icon, title }: Props) => ( + + + + {title} + + +) diff --git a/apps/web/app/_components/SubTitle/index.tsx b/apps/web/app/_components/SubTitle/index.tsx new file mode 100644 index 0000000..3cc0cc3 --- /dev/null +++ b/apps/web/app/_components/SubTitle/index.tsx @@ -0,0 +1 @@ +export { SubTitle } from './SubTitle' From 511b92f0030aa3dd7d86ca633952d2910d2a6563 Mon Sep 17 00:00:00 2001 From: leeleeleeleejun Date: Sun, 17 Aug 2025 22:07:50 +0900 Subject: [PATCH 20/27] =?UTF-8?q?feat:=20PlaceListItem=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../PlaceListItem/PlaceListItem.tsx | 45 +++++++++++++++++++ .../app/_components/PlaceListItem/index.tsx | 1 + 2 files changed, 46 insertions(+) create mode 100644 apps/web/app/_components/PlaceListItem/PlaceListItem.tsx create mode 100644 apps/web/app/_components/PlaceListItem/index.tsx diff --git a/apps/web/app/_components/PlaceListItem/PlaceListItem.tsx b/apps/web/app/_components/PlaceListItem/PlaceListItem.tsx new file mode 100644 index 0000000..8fdd968 --- /dev/null +++ b/apps/web/app/_components/PlaceListItem/PlaceListItem.tsx @@ -0,0 +1,45 @@ +import { Text } from '@repo/ui/components/Text' +import { Column, Flex } from '@repo/ui/components/Layout' +import { Icon } from '@repo/ui/components/Icon' +import { BasePlace } from '@/_apis/schemas/place' +import { Chip } from '@repo/ui/components/Chip' +import { cn } from '@repo/ui/utils/cn' + +type Props = { + showBorder?: boolean +} & BasePlace + +export const PlaceListItem = ({ + placeName, + address, + categories, + tags, + showBorder = true, +}: Props) => { + const mainCategoryIcon = categories[0]?.iconKey || 'logo' + + return ( + + {/*Todo: Link 태그로 감싸기 -> 상세페이지로 이동*/} + + {placeName} + + + + {address} + + {tags.length > 0 && ( + + {tags.map((tag) => ( + + ))} + + )} + + ) +} diff --git a/apps/web/app/_components/PlaceListItem/index.tsx b/apps/web/app/_components/PlaceListItem/index.tsx new file mode 100644 index 0000000..257da74 --- /dev/null +++ b/apps/web/app/_components/PlaceListItem/index.tsx @@ -0,0 +1 @@ +export { PlaceListItem } from './PlaceListItem' From fd8f7408e17c6e2710b3f5e6b58533f6ba2464f2 Mon Sep 17 00:00:00 2001 From: leeleeleeleejun Date: Sun, 17 Aug 2025 22:08:55 +0900 Subject: [PATCH 21/27] =?UTF-8?q?feat:=20MostLikedPlaces=20=EB=B0=8F=20Mos?= =?UTF-8?q?tViewsPlaces=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20=EB=B0=8F=20RankingPlaceList=20=ED=86=B5=ED=95=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../MostLikedPlaces/MostLikedPlaces.tsx | 13 +++++++++ .../MostLikedPlaces/index.tsx | 1 + .../MostViewsPlaces/MostViewsPlaces.tsx | 11 ++++++++ .../MostViewsPlaces/index.tsx | 1 + .../RankingPlaceList/RankingPlaceList.tsx | 28 +++++++++++++++++++ .../_components/RankingPlaceList/index.tsx | 3 ++ 6 files changed, 57 insertions(+) create mode 100644 apps/web/app/_components/RankingPlaceList/MostLikedPlaces/MostLikedPlaces.tsx create mode 100644 apps/web/app/_components/RankingPlaceList/MostLikedPlaces/index.tsx create mode 100644 apps/web/app/_components/RankingPlaceList/MostViewsPlaces/MostViewsPlaces.tsx create mode 100644 apps/web/app/_components/RankingPlaceList/MostViewsPlaces/index.tsx create mode 100644 apps/web/app/_components/RankingPlaceList/RankingPlaceList.tsx create mode 100644 apps/web/app/_components/RankingPlaceList/index.tsx diff --git a/apps/web/app/_components/RankingPlaceList/MostLikedPlaces/MostLikedPlaces.tsx b/apps/web/app/_components/RankingPlaceList/MostLikedPlaces/MostLikedPlaces.tsx new file mode 100644 index 0000000..58221fe --- /dev/null +++ b/apps/web/app/_components/RankingPlaceList/MostLikedPlaces/MostLikedPlaces.tsx @@ -0,0 +1,13 @@ +'use client' + +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')) + + return ( + + ) +} diff --git a/apps/web/app/_components/RankingPlaceList/MostLikedPlaces/index.tsx b/apps/web/app/_components/RankingPlaceList/MostLikedPlaces/index.tsx new file mode 100644 index 0000000..7dd8614 --- /dev/null +++ b/apps/web/app/_components/RankingPlaceList/MostLikedPlaces/index.tsx @@ -0,0 +1 @@ +export { MostLikedPlaces } from './MostLikedPlaces' diff --git a/apps/web/app/_components/RankingPlaceList/MostViewsPlaces/MostViewsPlaces.tsx b/apps/web/app/_components/RankingPlaceList/MostViewsPlaces/MostViewsPlaces.tsx new file mode 100644 index 0000000..1d964e0 --- /dev/null +++ b/apps/web/app/_components/RankingPlaceList/MostViewsPlaces/MostViewsPlaces.tsx @@ -0,0 +1,11 @@ +'use client' + +import { useSuspenseQuery } from '@tanstack/react-query' +import { usePlaceQueries } from '@/_apis/queries/place' +import { RankingPlaceList } from '@/_components/RankingPlaceList' + +export const MostViewsPlaces = () => { + const { data } = useSuspenseQuery(usePlaceQueries.rankingList('views')) + + return +} diff --git a/apps/web/app/_components/RankingPlaceList/MostViewsPlaces/index.tsx b/apps/web/app/_components/RankingPlaceList/MostViewsPlaces/index.tsx new file mode 100644 index 0000000..4bb7554 --- /dev/null +++ b/apps/web/app/_components/RankingPlaceList/MostViewsPlaces/index.tsx @@ -0,0 +1 @@ +export { MostViewsPlaces } from './MostViewsPlaces' diff --git a/apps/web/app/_components/RankingPlaceList/RankingPlaceList.tsx b/apps/web/app/_components/RankingPlaceList/RankingPlaceList.tsx new file mode 100644 index 0000000..6233739 --- /dev/null +++ b/apps/web/app/_components/RankingPlaceList/RankingPlaceList.tsx @@ -0,0 +1,28 @@ +import { Column } from '@repo/ui/components/Layout' +import { IconType } from '@repo/ui/components/Icon' +import { SubTitle } from '@/_components/SubTitle' +import { RankingPlace } from '@/_apis/schemas/place' +import { PlaceListItem } from '@/_components/PlaceListItem' + +type Props = { + title: string + icon: IconType + places: RankingPlace[] +} + +export const RankingPlaceList = ({ title, icon, places }: Props) => { + return ( + + +
    + {places.map((place, index) => ( + + ))} +
+
+ ) +} diff --git a/apps/web/app/_components/RankingPlaceList/index.tsx b/apps/web/app/_components/RankingPlaceList/index.tsx new file mode 100644 index 0000000..2205676 --- /dev/null +++ b/apps/web/app/_components/RankingPlaceList/index.tsx @@ -0,0 +1,3 @@ +export { RankingPlaceList } from './RankingPlaceList' +export { MostViewsPlaces } from './MostViewsPlaces' +export { MostLikedPlaces } from './MostLikedPlaces' From 0321ce62d33764a0b8ee0ddbba43dc2a0ba74685 Mon Sep 17 00:00:00 2001 From: leeleeleeleejun Date: Sun, 17 Aug 2025 22:09:48 +0900 Subject: [PATCH 22/27] =?UTF-8?q?feat:=20=EB=A9=94=EC=9D=B8=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EA=B5=AC=EC=84=B1=20=EC=9A=94=EC=86=8C=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20?= =?UTF-8?q?=ED=94=84=EB=A6=AC=ED=8C=A8=EC=B9=AD=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/app/page.tsx | 40 ++++++++++++++++++++++++++++++++-------- 1 file changed, 32 insertions(+), 8 deletions(-) diff --git a/apps/web/app/page.tsx b/apps/web/app/page.tsx index ec0ff95..5ca836d 100644 --- a/apps/web/app/page.tsx +++ b/apps/web/app/page.tsx @@ -1,13 +1,37 @@ -import { Flex } from '@repo/ui/components/Layout/Flex' +import { CLIENT_PATH } from '@/_constants/path' +import { useCategoryQueries } from '@/_apis/queries/category' +import { usePlaceQueries } from '@/_apis/queries/place' +import { HydrationBoundaryPage } from '@/HydrationBoundaryPage' +import { Categories } from '@/_components/Categories' +import { BottomNavigation } from '@/_components/BottomNavigation' +import { OnlyLeftHeader } from '@repo/ui/components/Header' +import { SearchBar } from '@repo/ui/components/SearchBar' +import { Column } from '@repo/ui/components/Layout' +import { Banner } from '@/_components/Banner' +import { + MostLikedPlaces, + MostViewsPlaces, +} from '@/_components/RankingPlaceList' +import { Divider } from '@repo/ui/components/Divider' export default function Page() { return ( - <> - -
gkldf
-
gkldf
-
gkldf
-
- + { + await queryClient.prefetchQuery(useCategoryQueries.list()) + await queryClient.prefetchQuery(usePlaceQueries.rankingList('likes')) + }} + > + + + + + + + + + + + ) } From 3a899cce99673918707be475e439d7f4b8de84a6 Mon Sep 17 00:00:00 2001 From: leeleeleeleejun Date: Sun, 17 Aug 2025 22:13:27 +0900 Subject: [PATCH 23/27] =?UTF-8?q?feat:=20Layout=20=EB=B0=B0=EA=B2=BD?= =?UTF-8?q?=EC=83=89=EC=9D=84=20=ED=9A=8C=EC=83=89=EC=97=90=EC=84=9C=20#FE?= =?UTF-8?q?FCF9=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/app/layout.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx index 41cf46c..0c52fbc 100644 --- a/apps/web/app/layout.tsx +++ b/apps/web/app/layout.tsx @@ -31,7 +31,7 @@ export default async function RootLayout({ -
+
{children} From 56ee87e7683553d0abe651b16d30dce192a44a25 Mon Sep 17 00:00:00 2001 From: leeleeleeleejun Date: Sun, 17 Aug 2025 23:24:58 +0900 Subject: [PATCH 24/27] =?UTF-8?q?feat:=20likeCount=20=EC=8A=A4=ED=82=A4?= =?UTF-8?q?=EB=A7=88=EB=A5=BC=20=EC=9D=8C=EC=88=98=20=EB=98=90=EB=8A=94=20?= =?UTF-8?q?=EC=86=8C=EC=88=98=20=EB=B0=A9=EC=A7=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/app/_apis/schemas/place.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/app/_apis/schemas/place.ts b/apps/web/app/_apis/schemas/place.ts index 410ebaf..8615bd4 100644 --- a/apps/web/app/_apis/schemas/place.ts +++ b/apps/web/app/_apis/schemas/place.ts @@ -13,7 +13,7 @@ export type RankingPlaceSort = 'views' | 'likes' export const RankingPlaceSchema = BasePlaceSchema.extend({ isLiked: z.boolean(), - likeCount: z.number(), + likeCount: z.number().int().nonnegative(), }) export type BasePlace = z.infer From f96510128d4a9ba1534d662079f57128d246f0a4 Mon Sep 17 00:00:00 2001 From: leeleeleeleejun Date: Sun, 17 Aug 2025 23:26:43 +0900 Subject: [PATCH 25/27] =?UTF-8?q?feat:=20prefetchQuery=EC=97=90=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=EC=88=98=20=EB=9E=AD=ED=82=B9=20=EB=A6=AC?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=ED=8F=AC=ED=95=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/app/page.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/web/app/page.tsx b/apps/web/app/page.tsx index 5ca836d..5b9516e 100644 --- a/apps/web/app/page.tsx +++ b/apps/web/app/page.tsx @@ -20,6 +20,7 @@ export default function Page() { prefetch={async (queryClient) => { await queryClient.prefetchQuery(useCategoryQueries.list()) await queryClient.prefetchQuery(usePlaceQueries.rankingList('likes')) + await queryClient.prefetchQuery(usePlaceQueries.rankingList('views')) }} > From 2fa54016489f5f89ae2a718e9f4a14b54a8bb48e Mon Sep 17 00:00:00 2001 From: leeleeleeleejun Date: Sun, 17 Aug 2025 23:29:00 +0900 Subject: [PATCH 26/27] =?UTF-8?q?feat:=20Chip=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=EC=97=90=EC=84=9C=20onClick=20=EC=9D=B4?= =?UTF-8?q?=EB=B2=A4=ED=8A=B8=20=ED=95=B8=EB=93=A4=EB=9F=AC=20=EC=9C=84?= =?UTF-8?q?=EC=B9=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/ui/src/components/Chip/Chip.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ui/src/components/Chip/Chip.tsx b/packages/ui/src/components/Chip/Chip.tsx index bdf4fc7..7026bde 100644 --- a/packages/ui/src/components/Chip/Chip.tsx +++ b/packages/ui/src/components/Chip/Chip.tsx @@ -74,8 +74,8 @@ export const Chip: ChipType = ({ { 'ui:border-blue': isActive }, className, )} - onClick={onClick} {...restProps} + onClick={onClick} > From 381ec61bfaf8d2d7519f4320393a79afd8ef31d0 Mon Sep 17 00:00:00 2001 From: leeleeleeleejun Date: Sun, 17 Aug 2025 23:30:57 +0900 Subject: [PATCH 27/27] =?UTF-8?q?feat:=20Chip=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=EC=9D=98=20=EA=B8=B0=EB=B3=B8=20HTML=20?= =?UTF-8?q?=ED=83=9C=EA=B7=B8=20=ED=83=80=EC=9E=85=EC=9D=84=20'button'?= =?UTF-8?q?=EC=97=90=EC=84=9C=20'div'=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/ui/src/components/Chip/Chip.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ui/src/components/Chip/Chip.tsx b/packages/ui/src/components/Chip/Chip.tsx index 7026bde..fdba583 100644 --- a/packages/ui/src/components/Chip/Chip.tsx +++ b/packages/ui/src/components/Chip/Chip.tsx @@ -15,7 +15,7 @@ export type ChipProps = PolymorphicComponentProps< } > -export type ChipType = ( +export type ChipType = ( props: PropsWithChildren>, ) => JSX.Element @@ -26,7 +26,7 @@ export type ChipType = ( * - 클릭 시 내부 상태 `isActive`를 토글하며, `onToggle` 콜백을 실행합니다. * - 다양한 HTML 요소(`as` prop)를 지정하여 렌더링할 수 있습니다. * - * @template C 렌더링할 HTML 태그 타입 (기본값: 'button') + * @template C 렌더링할 HTML 태그 타입 (기본값: 'div') * * @param as 렌더링할 HTML 태그 또는 컴포넌트 * @param className 추가 CSS 클래스