๐จ 3๋จ๊ณ : ์ฅ์ ๋๋ฉ์ธ ํ ํฌ์คํ - 100-hours-a-week/7-team-ddb-wiki GitHub Wiki
๋ฌธ์ ๊ด๋ฆฌ
์์ฑ์ผ | 2025-05-30 |
---|---|
๋ฒ์ | 1.1 |
์์ฑ์ | suzy.kang (๊ฐ์์ง) |
๊ฒํ ์ | kevin |
์ํ | ์๋ฃ |
๋ณ๊ฒฝ ์ด๋ ฅ
๋ฒ์ | ๋ ์ง | ์์ฑ์ | ๊ฒํ ์ | ๋ณ๊ฒฝ๋ด์ฉ |
---|---|---|---|---|
1.0 | 2025-04-25 | suzy | kevin | ์ด์ |
1.1 | 2025-05-30 | suzy | kevin | MVP ๊ฐ๋ฐ ์ดํ ๋ฌธ์ ๋๊ธฐํ |
๋ฐฐ๊ฒฝ (Background)
ํ๋ก์ ํธ ๋ชฉํ (Objective)
- ์ฌ์ฉ์ ์์น ๊ธฐ๋ฐ ์ฅ์ ๊ฒ์ ๋ฐ ์ถ์ฒ ๊ธฐ๋ฅ ์ ๊ณต
- ์ง๊ด์ ์ธ ์ง๋ ์ธํฐํ์ด์ค๋ก ์ฅ์ ํ์ ๊ฒฝํ ๊ฐ์
- ์ฅ์ ์ ๋ณด์ ํจ์จ์ ์ธ ํ์ ๋ฐ ๋ถ๋งํฌ ๊ธฐ๋ฅ ๊ตฌํ
- ๋ฐํ ์ํธ๋ฅผ ํ์ฉํ ๋ชจ๋ฐ์ผ ์ต์ ํ UX ์ ๊ณต
๋ชฉํ๊ฐ ์๋ ๊ฒ (Non-goals)
- ์ค์๊ฐ ์ฅ์ ์ ๋ณด ์ ๋ฐ์ดํธ
- ์ฅ์ ๋ฆฌ๋ทฐ ๋ฐ ํ์ ์์คํ
- ์ฅ์ ์์ฝ ์์คํ
์ค๊ณ ๋ฐ ๊ธฐ์ ์๋ฃ (Architecture and Technical Documentation)
์ํคํ ์ฒ ๊ฐ์ (Architecture Overview)
ํ๋ ์์ํฌ/๋ผ์ด๋ธ๋ฌ๋ฆฌ
- Next.js (v15.3.0) + React (v19.1.0)
- App Router ๊ธฐ๋ฐ ๋ผ์ฐํ
- SSR/ISR ํ์ฉ์ผ๋ก SEO ์ต์ ํ
- React 19์ use() ํ ํ์ฉํ ๋ฐ์ดํฐ ํ์นญ
์ํ ๊ด๋ฆฌ
- Zustand (v5.0.3)
- ๋ฐํ
์ํธ ์ํ ๊ด๋ฆฌ (
bottomSheetStore
) - ์๋ฒ ์ปดํฌ๋ํธ ํธํ์ฑ
- ์ง๊ด์ ์ธ API๋ก ๋น ๋ฅธ ๊ฐ๋ฐ ๊ฐ๋ฅ
- ๋ฐํ
์ํธ ์ํ ๊ด๋ฆฌ (
UI/์คํ์ผ๋ง
- Tailwind CSS (v4.1)
- ์ ํธ๋ฆฌํฐ ํผ์คํธ ์ ๊ทผ์ผ๋ก ๋น ๋ฅธ ๊ฐ๋ฐ
- ์ปค์คํ ํ ๋ง๋ก ๋ธ๋๋ ๋์์ธ ์์คํ ๊ตฌ์ถ
- shadcn/ui
- Tailwind ๊ธฐ๋ฐ ์ปดํฌ๋ํธ
- ํ์ํ ์ปดํฌ๋ํธ๋ง ๊ฐ์ ธ์ ๋ฒ๋ค ํฌ๊ธฐ ์ต์ ํ
- Vaul
- ๋ชจ๋ฐ์ผ ์ต์ ํ ๋ฐํ ์ํธ ๋ผ์ด๋ธ๋ฌ๋ฆฌ
- ํฐ์น ์ ์ค์ฒ ์ง์
๋ฐ์ดํฐ ๊ด๋ฆฌ
- React use() + TanStack Query (v5)
- ์๋ฒ ์ปดํฌ๋ํธ: use() ํ ์ผ๋ก SEO ์ต์ ํ
- ํด๋ผ์ด์ธํธ: TanStack Query๋ก ๋ณต์กํ ์ํ ๊ด๋ฆฌ
- Zod (v3.24.3)
- ํ์ ์คํฌ๋ฆฝํธ ์ฐ๋
- ์๋ฒ-ํด๋ผ์ด์ธํธ ์คํค๋ง ๊ณต์
ํผ ์ํ ๊ด๋ฆฌ
- react-hook-form (v7.51.2)
- ๊ฐ๋ฒผ์ด ๋ฒ๋ค ์ฌ์ด์ฆ๋ก ๋ชจ๋ฐ์ผ ์นํ์
- Zod๊ณผ ํตํฉํด ์ ๊ตํ ์ ํจ์ฑ ๊ฒ์ฌ ๊ฐ๋ฅ
- Server Action๊ณผ ๋ณํ ์ฌ์ฉ ๊ฐ๋ฅ (ํ์ ์ SSR + ๋น ๋ฅธ UX ๋ชจ๋ ํ๋ณด)
์ง๋ ์๋น์ค
- Kakao Maps SDK (v2.7.5)
- ๊ตญ๋ด ์ฌ์ฉ์ ์นํ์ UI
- ์ปค์คํ ๋ง์ปค ์ง์
- ์ง๋ ์์ญ ์ ํ ๊ธฐ๋ฅ
๋น๋ ๋๊ตฌ
- Turbopack(v1.5.3) + webpack (v5)
- Next.js 15 ๋ด์ฅ ๋น๋ ๋๊ตฌ ( Turbopack : ๊ฐ๋ฐ ํ๊ฒฝ, Webpack : ํ๋ก๋์ ํ๊ฒฝ )
- ๋น ๋ฅธ ๊ฐ๋ฐ ํ๊ฒฝ๊ณผ HMR ์ง์
SSR / ISR / SSG ์ ์ฉ ๋ฒ์
/
: SSR + CSR ํผํฉ- SSR ์ ์ฉ ๋ถ๋ถ:
- ์ด๊ธฐ ์นดํ ๊ณ ๋ฆฌ ๋ชฉ๋ก ๋ก๋
- ๊ธฐ๋ณธ ์ง๋ ์ค์
- ๋ฉํ๋ฐ์ดํฐ ๋ฐ SEO ์ต์ ํ
- CSR ์ ์ฉ ๋ถ๋ถ:
- ์ฌ์ฉ์ ์์น ๊ธฐ๋ฐ ๋์ ์ฝํ ์ธ
- ์ค์๊ฐ ์ง๋ ์ธํฐ๋์
- ๊ฒ์ ๊ฒฐ๊ณผ ๋ฐ ํํฐ๋ง
- SSR ์ ์ฉ ๋ถ๋ถ:
/places/[id]
: SSR + CSR ํผํฉ- SSR ์ ์ฉ ๋ถ๋ถ:
- ์ฅ์ ๊ธฐ๋ณธ ์ ๋ณด (์ด๋ฆ, ์ฃผ์, ์ค๋ช ๋ฑ)
- ๋ฉํ๋ฐ์ดํฐ ๋ฐ SEO ์ต์ ํ
- ์ด๊ธฐ ๋ฐฉ๋ฌธ ๊ธฐ๋ก ๋ก๋
- CSR ์ ์ฉ ๋ถ๋ถ:
- ์ค์๊ฐ ์์น ํ์ธ
- ๋ถ๋งํฌ ํ ๊ธ
- ๋ฌดํ ์คํฌ๋กค ๋ฐฉ๋ฌธ ๊ธฐ๋ก
- ๊ธฐ๋ก ์์ฑ ๋ฒํผ ์ํ
- SSR ์ ์ฉ ๋ถ๋ถ:
ํด๋ ๊ตฌ์กฐ
src/
โโโ app/(main)/(places)/
โ โโโ page.tsx # ๋ฉ์ธ ํ์ด์ง
โ โโโ places/[id]/page.tsx # ์ฅ์ ์์ธ ํ์ด์ง
โ โโโ search/ # ๊ฒ์ ๊ฒฐ๊ณผ ํ์ด์ง
โ โโโ page.tsx
โ โโโ layout.tsx
โโโ features/place/
โโโ api/ # API ํจ์
โ โโโ getCategories.ts
โ โโโ getPlaceDetail.ts
โ โโโ searchPlaces.ts
โโโ components/ # UI ์ปดํฌ๋ํธ
โ โโโ category-list/
โ โโโ keyword-list/
โ โโโ map/
โ โโโ place-basic-info/
โ โโโ place-item/
โ โโโ place-list-bottom-sheet/
โ โโโ place-menu/
โ โโโ place-open-hours/
โ โโโ place-pin-bottom-sheet/
โ โโโ search-bar/
โ โโโ search-result-bar/
โโโ constants/ # ์์
โโโ hooks/ # ์ปค์คํ
ํ
โ โโโ useBottomSheetSnapManagement.ts
โ โโโ useGlobalFocusHandler.ts
โ โโโ usePlaceToast.ts
โ โโโ useScrollRestoration.ts
โ โโโ useSearchBar.ts
โโโ stores/ # Zustand ์คํ ์ด
โ โโโ bottomSheetStore.ts
โโโ types/ # ํ์
์ ์
โ โโโ bottomSheetStore.ts
โ โโโ place.ts
โโโ utils/ # ์ ํธ๋ฆฌํฐ ํจ์
โโโ map.ts
์ฃผ์ ํ์ด์ง
/
: ๋ฉ์ธ ํ์ด์ง
- ์ฃผ์๊ธฐ๋ฅ:
- ์ฅ์ ๊ฒ์ (์ต์ 1์, ์ต๋ 25์)
- ์ง๋ ๊ธฐ๋ฐ ์ฅ์ ํ์
- ์นดํ ๊ณ ๋ฆฌ๋ณ ์ฅ์ ํํฐ๋ง
- ์ด๊ธฐ ๋น ์ํ ํ์
- ์ฌ์ฉ ์ปดํฌ๋ํธ:
SearchBar
- ๊ฒ์ ์ ๋ ฅMap
- ์นด์นด์ค๋งตCategoryList
- ์นดํ ๊ณ ๋ฆฌ ๋ฆฌ์คํธ
- ํน์ง:
- ๊ฒ์์ด ์
๋ ฅ ์
/search
ํ์ด์ง๋ก ์ด๋ - ์นดํ ๊ณ ๋ฆฌ ์ ํ ์ ํด๋น ์นดํ ๊ณ ๋ฆฌ๋ก ๊ฒ์
- ๊ฒ์์ด ์
๋ ฅ ์
/search
: ๊ฒ์ ๊ฒฐ๊ณผ ํ์ด์ง
- ์ฃผ์๊ธฐ๋ฅ:
- ๊ฒ์ ๊ฒฐ๊ณผ ์ง๋ ํ์
- ๋ฐํ ์ํธ๋ก ์ฅ์ ๋ชฉ๋ก ํ์
- ๋ง์ปค ํด๋ฆญ ์ ์ฅ์ ํ ๋ฐํ ์ํธ ํ์
- ์์น ๊ธฐ๋ฐ ๊ฒ์ (ํ์ฌ ์์น๋ ํ๊ต ์ ์คํ์ด์ค๋ก ๊ณ ์ )
- ์ฌ์ฉ ์ปดํฌ๋ํธ:
Map
- ๊ฒ์๋ ์ฅ์ ๋ง์ปค ํ์PlaceListBottomSheet
- ์ฅ์ ๋ชฉ๋ก ๋ฐํ ์ํธPlacePinBottomSheet
- ๊ฐ๋ณ ์ฅ์ ๋ฐํ ์ํธ
- ๋ฐ์ดํฐ ๋ก๋ฉ:
searchPlaces
API ํธ์ถ- TanStack Query๋ก ์บ์ฑ
/places/[id]
: ์ฅ์ ์์ธ ํ์ด์ง
- ์ฃผ์๊ธฐ๋ฅ:
- ์ฅ์ ๊ธฐ๋ณธ ์ ๋ณด ํ์
- ์์ ์๊ฐ ํ์
- ๋ฉ๋ด ์ ๋ณด ํ์
- ํค๋ ๋ค๋ก๊ฐ๊ธฐ ๋ฒํผ
- ์ฌ์ฉ ์ปดํฌ๋ํธ:
PlaceBasicInfo
- ์ฅ์ ๊ธฐ๋ณธ ์ ๋ณดPlaceOpenHours
- ์์ ์๊ฐPlaceMenu
- ๋ฉ๋ด ์ ๋ณด
- ๋ฐ์ดํฐ ๋ก๋ฉ:
- SSR๋ก
getPlaceDetail
API ํธ์ถ
- SSR๋ก
๊ณตํต ์ปดํฌ๋ํธ
SearchBar
: ๊ฒ์ ์
๋ ฅ ์ปดํฌ๋ํธ
-
์ญํ : ์ฅ์ ๊ฒ์์ด ์ ๋ ฅ ๋ฐ ์ ์ถ
-
Props & Interface:
interface SearchBarProps { initialQuery?: string; onSearch?: (query: string) => void; placeholder?: string; className?: string; }
-
๊ธฐ๋ฅ:
- ์ต์ 1์ ์ ๋ ฅ ๊ฒ์ฆ
- ์ต๋ 25์ ์ ํ
- ์ํฐํค ๊ฒ์ ์ง์
- ํ ์คํธ ๋ฉ์์ง ํ์
- ๊ฒ์ ์
/search
ํ์ด์ง๋ก ๋ผ์ฐํ - ์์น ์ ๋ณด ํฌํจํ ์ฟผ๋ฆฌ ํ๋ผ๋ฏธํฐ ์ ๋ฌ
-
์ํ ๊ด๋ฆฌ:
useSearchBar
์ปค์คํ ํ ์ฌ์ฉ- ๊ฒ์์ด ์ํ ๋ฐ ์ ํจ์ฑ ๊ฒ์ฆ
- ๊ฒ์ ์ ๋ฐํ ์ํธ ์ํ ์ด๊ธฐํ
-
์คํ์ผ๋ง:
- ๋ฅ๊ทผ ๋ชจ์๋ฆฌ ๊ฒ์ ๋ฐ
- ํธ๋ฒ/ํฌ์ปค์ค ์ ๊ทธ๋ฆผ์ ํจ๊ณผ
- ๊ฒ์ ์์ด์ฝ ๋ฒํผ
Map
: ์นด์นด์ค๋งต ์ปดํฌ๋ํธ
-
์ญํ : ์นด์นด์ค๋งต ํ์ ๋ฐ ๋ง์ปค ๊ด๋ฆฌ
-
Props & Interface:
interface MapProps { places: Place[]; }
-
๊ธฐ๋ฅ:
- ์ฌ์ฉ์ ์์น ๋ง์ปค ํ์ (ํ์ฌ ์์น)
- ์ฅ์ ๋ง์ปค ํ์
- ๋ง์ปค ํด๋ฆญ ์ด๋ฒคํธ ์ฒ๋ฆฌ
- ์ง๋ ์์ญ ์ ํ (๋ฐ๊ฒฝ 1km)
- ์์ญ ๋ฒ์ด๋จ ํ ์คํธ ์๋ฆผ
- ๋ง์ปค ์ ํ ์ํ ๊ด๋ฆฌ
-
์ํ ๊ด๋ฆฌ:
- ์ง๋ ์ธ์คํด์ค ref ๊ด๋ฆฌ
- ๋ง์ปค ํด๋ฆญ ์ ๋ฐํ ์ํธ ์ํ ๋ณ๊ฒฝ
- ์ ํ๋ ์ฅ์ ID ์ ์ฅ
-
์คํ์ผ๋ง:
- ์ ์ฒด ํ๋ฉด ์ง๋
- ์ปค์คํ ๋ง์ปค ์ด๋ฏธ์ง
- ์ ํ๋ ๋ง์ปค ํ์ด๋ผ์ดํธ
CategoryList
: ์นดํ
๊ณ ๋ฆฌ ๋ฆฌ์คํธ ์ปดํฌ๋ํธ
- ์ญํ : ์นดํ ๊ณ ๋ฆฌ ๊ธฐ๋ฐ ๊ฒ์
- ๊ธฐ๋ฅ:
- ์นดํ ๊ณ ๋ฆฌ ๋ชฉ๋ก ํ์
- ์ ํ๋ ์นดํ ๊ณ ๋ฆฌ ํ์ด๋ผ์ดํธ
- ๊ฐ๋ก ์คํฌ๋กค
- ์นดํ ๊ณ ๋ฆฌ ํด๋ฆญ ์ ๊ฒ์
- ๋ฐ์ดํฐ ํ์นญ:
- ์๋ฒ ์ปดํฌ๋ํธ๋ก
getCategories
API ํธ์ถ - SSR๋ก ์นดํ ๊ณ ๋ฆฌ ๋ฐ์ดํฐ ๋ก๋
- ์๋ฒ ์ปดํฌ๋ํธ๋ก
- ์คํ์ผ๋ง:
- ๊ฐ๋ก ์คํฌ๋กค ์ปจํ ์ด๋
- ์คํฌ๋กค๋ฐ ์จ๊น
- ์นดํ ๊ณ ๋ฆฌ ์นฉ ์คํ์ผ
PlaceListBottomSheet
: ์ฅ์ ๋ชฉ๋ก ๋ฐํ
์ํธ ์ปดํฌ๋ํธ
-
์ญํ : ์ฅ์ ๋ชฉ๋ก์ ๋ฐํ ์ํธ๋ก ํ์
-
Props & Interface:
interface PlaceListProps { places: Place[]; }
-
๊ธฐ๋ฅ:
- ๋๋๊ทธ ์ ์ค์ฒ ์ง์
- ๋์ด ์ค๋ ํฌ์ธํธ
- ์คํฌ๋กค ๋ณต์
- ์ฅ์ ์์ดํ ํด๋ฆญ
- ๋์ด ์ํ ์ ์ฅ ๋ฐ ๋ณต์
-
์ํ ๊ด๋ฆฌ:
- Zustand
bottomSheetStore
์ฌ์ฉ - ์ค๋ ํฌ์ธํธ ์ํ
- ์คํฌ๋กค ์์น ์ ์ฅ
- ์ด์ ๋์ด ๋ณต์
- Zustand
-
์ปค์คํ ํ :
useBottomSheetSnapManagement
: ์ค๋ ํฌ์ธํธ ๊ด๋ฆฌuseScrollRestoration
: ์คํฌ๋กค ์์น ๋ณต์useGlobalFocusHandler
: ํฌ์ปค์ค ์ด๋ฒคํธ ์ฒ๋ฆฌ
-
์คํ์ผ๋ง:
- Vaul ๋ผ์ด๋ธ๋ฌ๋ฆฌ ๊ธฐ๋ฐ
- ์๋จ ๋๋๊ทธ ํธ๋ค
- ์ต๋ ๋๋น 430px
PlacePinBottomSheet
: ์ฅ์ ํ ๋ฐํ
์ํธ ์ปดํฌ๋ํธ
-
์ญํ : ์ ํ๋ ์ฅ์ ์ ๋ณด ํ์
-
Props & Interface:
interface PlaceBottomSheetProps { onOpenChange: (open: boolean) => void; place: Place | null; }
-
๊ธฐ๋ฅ:
- ์ฅ์ ๊ธฐ๋ณธ ์ ๋ณด ํ์
- ํค์๋ ํ์
- ๋ซ๊ธฐ ๊ธฐ๋ฅ
- ์์ธ ๋ณด๊ธฐ ๋ฒํผ
-
์ํ ๊ด๋ฆฌ:
- Zustand
bottomSheetStore
์์opened
์ํ ํ์ธ - ๋ซ๊ธฐ ์
opened
์ํ๋ฅผ 'list'๋ก ๋ณ๊ฒฝ
- Zustand
-
์คํ์ผ๋ง:
- ๋ชจ๋ฌ ์ค๋ฒ๋ ์ด
- ํ๋จ ๊ณ ์ ์์น
- ๋ฅ๊ทผ ์๋จ ๋ชจ์๋ฆฌ
PlaceItem
: ์ฅ์ ์์ดํ
์ปดํฌ๋ํธ
-
์ญํ : ๊ฐ๋ณ ์ฅ์ ์นด๋
-
Props & Interface:
interface PlaceItemProps { id: number; name: string; thumbnail: string; keywords: string[]; isClickable?: boolean; isDetailButton?: boolean; }
-
๊ธฐ๋ฅ:
- ์ด๋ฏธ์ง ํ์ (์์ ๊ฒฝ์ฐ ํ๋ ์ด์คํ๋)
- ์ฅ์๋ช , ์นดํ ๊ณ ๋ฆฌ
- ํด๋ฆญ ์ ์์ธ ํ์ด์ง ์ด๋
- ์์ธ ๋ณด๊ธฐ ๋ฒํผ (์ ํ์ )
-
์ํ ๊ด๋ฆฌ:
- Next.js
useRouter
์ฌ์ฉ - ํด๋ฆญ ์
/places/${id}
๋ก ๋ผ์ฐํ
- Next.js
-
์คํ์ผ๋ง:
- ์ด๋ฏธ์ง + ์ ๋ณด ๊ฐ๋ก ๋ ์ด์์
- 96x96px ์ด๋ฏธ์ง ํฌ๊ธฐ
- ํค์๋ ๋ฆฌ์คํธ ํฌํจ
PlaceBasicInfo
: ์ฅ์ ๊ธฐ๋ณธ ์ ๋ณด ์ปดํฌ๋ํธ
-
์ญํ : ์ฅ์ ์์ธ ์ ๋ณด
-
Props & Interface:
interface PlaceBasicInfoProps { placeBasicInfo: Omit<PlaceDetail, 'menu' | 'opening_hours'>; }
-
๊ธฐ๋ฅ:
- ์ด๋ฏธ์ง ๊ฐค๋ฌ๋ฆฌ (1:1 ๋น์จ)
- ๊ธฐ๋ณธ ์ ๋ณด ํ์ (์ด๋ฆ, ์ฃผ์, ์ ํ๋ฒํธ)
- ํค์๋ ๋ฆฌ์คํธ
- ์ค๋ช ํ ์คํธ
-
์คํ์ผ๋ง:
- ์ ์ฌ๊ฐํ ์ด๋ฏธ์ง
- ํค์๋ ํ๊ทธ ์คํ์ผ
- ์ ๋ณด ๊ณ์ธต ๊ตฌ์กฐ
PlaceOpenHours
: ์์
์๊ฐ ์ปดํฌ๋ํธ
-
์ญํ : ์์ ์๊ฐ ์ ๋ณด
-
Props & Interface:
interface PlaceOpenHoursProps { openHours: OpeningHours; }
-
๊ธฐ๋ฅ:
- ํ์ฌ ์์ ์ํ
- ์์ผ๋ณ ์๊ฐ ํ์
- ํด๋ฌด์ผ ํ์
-
์คํ์ผ๋ง:
- ์์ ์ํ ๋ฐฐ์ง
- ์์ผ๋ณ ๋ชฉ๋ก
PlaceMenu
: ๋ฉ๋ด ์ ๋ณด ์ปดํฌ๋ํธ
-
์ญํ : ๋ฉ๋ด ์ ๋ณด
-
Props & Interface:
interface PlaceMenuProps { menu: MenuItem[]; }
-
๊ธฐ๋ฅ:
- ๋ฉ๋ด ์ด๋ฏธ์ง
- ๋ฉ๋ด๋ช ๋ฐ ๊ฐ๊ฒฉ
- ์ด๋ฏธ์ง ์์ ๊ฒฝ์ฐ ํ๋ ์ด์คํ๋
-
์คํ์ผ๋ง:
- ๊ทธ๋ฆฌ๋ ๋ ์ด์์
- ๋ฉ๋ด ์นด๋ ์คํ์ผ
KeywordList
: ํค์๋ ๋ฆฌ์คํธ ์ปดํฌ๋ํธ
-
์ญํ : ํค์๋ ํ๊ทธ ํ์
-
Props & Interface:
interface KeywordListProps { keywords: string[]; }
-
๊ธฐ๋ฅ:
- ํค์๋ ๋ฐฐ์ด์ ํ๊ทธ๋ก ํ์
- ๊ฐ๋ก ์คํฌ๋กค (ํ์์)
-
์คํ์ผ๋ง:
- ๋ฅ๊ทผ ํ๊ทธ ์คํ์ผ
- ํ์ ๋ฐฐ๊ฒฝ
- ๊ฐ๊ฒฉ ์กฐ์
์ปดํฌ๋ํธ ๊ฐ ๊ด๊ณ
graph TD
A[SearchBar] -->|๊ฒ์ ์ฟผ๋ฆฌ| B[Search Page]
B --> C[Map]
B --> D[PlaceListBottomSheet]
C -->|๋ง์ปค ํด๋ฆญ| E[PlacePinBottomSheet]
D -->|์ฅ์ ํด๋ฆญ| F[PlaceItem]
E --> F
F -->|์์ธ ๋ณด๊ธฐ| G[Place Detail Page]
G --> H[PlaceBasicInfo]
G --> I[PlaceOpenHours]
G --> J[PlaceMenu]
K[CategoryList] -->|์นดํ
๊ณ ๋ฆฌ ์ ํ| B
๋ฐ์ดํฐ ํ๋ฆ
- ๊ฒ์ ํ๋ก์ฐ:
SearchBar
โ ๊ฒ์์ด ์ ๋ ฅ- โ
/search
ํ์ด์ง ์ด๋ - โ
searchPlaces
API ํธ์ถ - โ
Map
+PlaceListBottomSheet
๋ ๋๋ง
- ๋ง์ปค ํด๋ฆญ ํ๋ก์ฐ:
Map
๋ง์ปค ํด๋ฆญ- โ
bottomSheetStore
์ํ ๋ณ๊ฒฝ - โ
PlacePinBottomSheet
์ด๋ฆผ - โ
PlaceItem
ํ์
- ์์ธ ํ์ด์ง ํ๋ก์ฐ:
PlaceItem
ํด๋ฆญ/๋ฒํผ- โ
/places/[id]
์ด๋ - โ
getPlaceDetail
API ํธ์ถ - โ ์์ธ ์ ๋ณด ์ปดํฌ๋ํธ ๋ ๋๋ง
์ํ ๊ด๋ฆฌ
BottomSheetStore (Zustand)
interface BottomSheetState {
opened: 'list' | 'pin';
prevSnap: number | string | null;
scrollY: number;
lastPlaceId: number | null;
setOpened: (opened: 'list' | 'pin') => void;
setPrevSnap: (snap: number | string | null) => void;
setScrollY: (scrollY: number) => void;
setLastPlaceId: (placeId: number | null) => void;
resetForNewSearch: () => void;
}
Local State
SearchBar
: ๊ฒ์์ด ์ ๋ ฅMap
: ์ง๋ ์ธ์คํด์คPlaceListBottomSheet
: ์ค๋ ํฌ์ธํธ
API ์ฐ๋
๊ตฌํ๋ API
GET /api/v1/categories
- ์นดํ ๊ณ ๋ฆฌ ๋ชฉ๋กGET /api/v1/places/search
- ์ฅ์ ๊ฒ์GET /api/v1/places/{id}
- ์ฅ์ ์์ธ
ํ์ ์ ์
// ์์น ์ ๋ณดinterface Location {
type: 'Point';
coordinates: [number, number];// [๊ฒฝ๋, ์๋] - GeoJSON ํ์ค
}
// ์ฅ์ ๊ธฐ๋ณธ ์ ๋ณดinterface Place {
id: number;
name: string;
thumbnail: string;
distance: string;// ๊ฑฐ๋ฆฌ (๋ฏธํฐ ๋จ์)moment_count: string;// ๋ฐฉ๋ฌธ ๊ธฐ๋ก ์keywords: string[];// ํค์๋ ๋ชฉ๋กlocation: Location;// ์์น ์ ๋ณด
}
// ์ฅ์ ์์ธ ์ ๋ณดinterface PlaceDetail {
id: number;
name: string;
address: string | null;
thumbnail: string | null;
location: Location;
keywords: string[];
description: string;
phone: string | null;
menu?: Menu[];
opening_hours: OpenHours;
}
// ์์
์๊ฐ ์ ๋ณดinterface OpenHours {
status:
| '์์
์ค'
| '์์
์ข
๋ฃ'
| '๋ธ๋ ์ดํฌ ํ์'
| 'ํด๋ฌด์ผ'
| '์์
์ ๋ณด ์์'
| '์์
์ฌ๋ถ ํ์ธ ํ์';
schedules: {
day: 'mon' | 'tue' | 'wed' | 'thu' | 'fri' | 'sat' | 'sun';
hours: string | null;
break_time: string | null;
}[];
}
// ๋ฉ๋ด ์ ๋ณดinterface Menu {
name: string;
price: number | null;
}
API ์์ฒญ/์๋ต ํ์
// ์นดํ
๊ณ ๋ฆฌ ๋ชฉ๋ก ์๋ต
interface CategoriesResponse {
categories: string[];
}
// ์ฅ์ ๊ฒ์ ์์ฒญinterface SearchPlacesParams {
lat: string;// ์๋lng: string;// ๊ฒฝ๋query: string | null;// ๊ฒ์์ดcategory: string | null;// ์นดํ
๊ณ ๋ฆฌ
}
// ์ฅ์ ๊ฒ์ ์๋ตinterface SearchPlacesResponse {
total: number;// ์ ์ฒด ๊ฒฐ๊ณผ ์places: Place[];// ์ฅ์ ๋ชฉ๋ก
}
์ํ ๊ด๋ฆฌ ํ์
// ๋ฐํ
์ํธ ์ํtype BottomSheetType = 'list' | 'pin';
interface BottomSheetState {
opened: BottomSheetType;
prevSnap: number | string | null;
scrollY: number;
lastPlaceId: number | null;
setOpened: (opened: BottomSheetType) => void;
setPrevSnap: (snap: number | string | null) => void;
setScrollY: (scrollY: number) => void;
setLastPlaceId: (placeId: number | null) => void;
resetForNewSearch: () => void;
}
API ํธ์ถ ์์
// ์นดํ
๊ณ ๋ฆฌ ๋ชฉ๋ก ์กฐํconst { categories } = await getCategories();
// ์ฅ์ ๊ฒ์const searchResults = await searchPlaces({
lat: '37.401179',
lng: '127.108047',
query: '์นดํ',
category: null,
});
// ์ฅ์ ์์ธ ์กฐํconst placeDetail = await getPlaceDetail('1');
์ปค์คํ ํ
useSearchBar
- ๊ฒ์์ด ์ํ ๊ด๋ฆฌ
- ๊ฒ์ ์คํ ๋ก์ง
- ์ ํจ์ฑ ๊ฒ์ฆ
useBottomSheetSnapManagement
- ๋ฐํ ์ํธ ๋์ด ๊ด๋ฆฌ
- ์ค๋ ํฌ์ธํธ ์ ์ด
useScrollRestoration
- ์คํฌ๋กค ์์น ์ ์ฅ/๋ณต์
- ํ์ด์ง ์ด๋ ์ ์์น ์ ์ง
usePlaceToast
- ํ ์คํธ ๋ฉ์์ง ํ์
- ์๋ฌ/์ฑ๊ณต ๋ฉ์์ง
useGlobalFocusHandler
- ์ ์ญ ํฌ์ปค์ค ์ด๋ฒคํธ
- ๋ฐํ ์ํธ ์ ๋ ฅ ๋ฌธ์ ํด๊ฒฐ
์ฑ๋ฅ ์ต์ ํ
- TanStack Query ์บ์ฑ
staleTime: 10๋ถ
refetchOnWindowFocus: false
- ์ด๋ฏธ์ง ์ต์ ํ
- Next.js Image ์ปดํฌ๋ํธ
- lazy loading
ํฅํ ๊ณํ
- ๋ถ๋งํฌ ๊ธฐ๋ฅ ๊ตฌํ
- ๋ฐฉ๋ฌธ ๊ธฐ๋ก ๊ธฐ๋ฅ
- ์ต์ ํ ๋ ๋ ๋๋ง ๊ตฌํ
- SEO ์ต์ ํ ๊ตฌํ
- ๊ฒ์ ๊ฒฐ๊ณผ ์ ๋ ฌ
- ์ง๋ ํด๋ฌ์คํฐ๋ง