๐ŸŽจ 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 ์ ์šฉ ๋ถ€๋ถ„:
      • ์‚ฌ์šฉ์ž ์œ„์น˜ ๊ธฐ๋ฐ˜ ๋™์  ์ฝ˜ํ…์ธ 
      • ์‹ค์‹œ๊ฐ„ ์ง€๋„ ์ธํ„ฐ๋ž™์…˜
      • ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ๋ฐ ํ•„ํ„ฐ๋ง
  • /places/[id] : SSR + CSR ํ˜ผํ•ฉ
    • SSR ์ ์šฉ ๋ถ€๋ถ„:
      • ์žฅ์†Œ ๊ธฐ๋ณธ ์ •๋ณด (์ด๋ฆ„, ์ฃผ์†Œ, ์„ค๋ช… ๋“ฑ)
      • ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ๋ฐ SEO ์ตœ์ ํ™”
      • ์ดˆ๊ธฐ ๋ฐฉ๋ฌธ ๊ธฐ๋ก ๋กœ๋“œ
    • CSR ์ ์šฉ ๋ถ€๋ถ„:
      • ์‹ค์‹œ๊ฐ„ ์œ„์น˜ ํ™•์ธ
      • ๋ถ๋งˆํฌ ํ† ๊ธ€
      • ๋ฌดํ•œ ์Šคํฌ๋กค ๋ฐฉ๋ฌธ ๊ธฐ๋ก
      • ๊ธฐ๋ก ์ž‘์„ฑ ๋ฒ„ํŠผ ์ƒํƒœ

ํด๋” ๊ตฌ์กฐ

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 ํ˜ธ์ถœ

๊ณตํ†ต ์ปดํฌ๋„ŒํŠธ

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 ์‚ฌ์šฉ
    • ์Šค๋ƒ… ํฌ์ธํŠธ ์ƒํƒœ
    • ์Šคํฌ๋กค ์œ„์น˜ ์ €์žฅ
    • ์ด์ „ ๋†’์ด ๋ณต์›
  • ์ปค์Šคํ…€ ํ›…:

    • useBottomSheetSnapManagement: ์Šค๋ƒ… ํฌ์ธํŠธ ๊ด€๋ฆฌ
    • useScrollRestoration: ์Šคํฌ๋กค ์œ„์น˜ ๋ณต์›
    • useGlobalFocusHandler: ํฌ์ปค์Šค ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ
  • ์Šคํƒ€์ผ๋ง:

    • Vaul ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๊ธฐ๋ฐ˜
    • ์ƒ๋‹จ ๋“œ๋ž˜๊ทธ ํ•ธ๋“ค
    • ์ตœ๋Œ€ ๋„ˆ๋น„ 430px

PlacePinBottomSheet : ์žฅ์†Œ ํ•€ ๋ฐ”ํ…€์‹œํŠธ ์ปดํฌ๋„ŒํŠธ

  • ์—ญํ• : ์„ ํƒ๋œ ์žฅ์†Œ ์ •๋ณด ํ‘œ์‹œ

  • Props & Interface:

    interface PlaceBottomSheetProps {
      onOpenChange: (open: boolean) => void;
      place: Place | null;
    }
    
    
  • ๊ธฐ๋Šฅ:

    • ์žฅ์†Œ ๊ธฐ๋ณธ ์ •๋ณด ํ‘œ์‹œ
    • ํ‚ค์›Œ๋“œ ํ‘œ์‹œ
    • ๋‹ซ๊ธฐ ๊ธฐ๋Šฅ
    • ์ƒ์„ธ ๋ณด๊ธฐ ๋ฒ„ํŠผ
  • ์ƒํƒœ ๊ด€๋ฆฌ:

    • Zustand bottomSheetStore์—์„œ opened ์ƒํƒœ ํ™•์ธ
    • ๋‹ซ๊ธฐ ์‹œ opened ์ƒํƒœ๋ฅผ 'list'๋กœ ๋ณ€๊ฒฝ
  • ์Šคํƒ€์ผ๋ง:

    • ๋ชจ๋‹ฌ ์˜ค๋ฒ„๋ ˆ์ด
    • ํ•˜๋‹จ ๊ณ ์ • ์œ„์น˜
    • ๋‘ฅ๊ทผ ์ƒ๋‹จ ๋ชจ์„œ๋ฆฌ

PlaceItem : ์žฅ์†Œ ์•„์ดํ…œ ์ปดํฌ๋„ŒํŠธ

  • ์—ญํ• : ๊ฐœ๋ณ„ ์žฅ์†Œ ์นด๋“œ

  • Props & Interface:

    interface PlaceItemProps {
      id: number;
      name: string;
      thumbnail: string;
      keywords: string[];
      isClickable?: boolean;
      isDetailButton?: boolean;
    }
    
    
  • ๊ธฐ๋Šฅ:

    • ์ด๋ฏธ์ง€ ํ‘œ์‹œ (์—†์„ ๊ฒฝ์šฐ ํ”Œ๋ ˆ์ด์Šคํ™€๋”)
    • ์žฅ์†Œ๋ช…, ์นดํ…Œ๊ณ ๋ฆฌ
    • ํด๋ฆญ ์‹œ ์ƒ์„ธ ํŽ˜์ด์ง€ ์ด๋™
    • ์ƒ์„ธ ๋ณด๊ธฐ ๋ฒ„ํŠผ (์„ ํƒ์ )
  • ์ƒํƒœ ๊ด€๋ฆฌ:

    • Next.js useRouter ์‚ฌ์šฉ
    • ํด๋ฆญ ์‹œ /places/${id}๋กœ ๋ผ์šฐํŒ…
  • ์Šคํƒ€์ผ๋ง:

    • ์ด๋ฏธ์ง€ + ์ •๋ณด ๊ฐ€๋กœ ๋ ˆ์ด์•„์›ƒ
    • 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

๋ฐ์ดํ„ฐ ํ๋ฆ„

  1. ๊ฒ€์ƒ‰ ํ”Œ๋กœ์šฐ:
    • SearchBar โ†’ ๊ฒ€์ƒ‰์–ด ์ž…๋ ฅ
    • โ†’ /search ํŽ˜์ด์ง€ ์ด๋™
    • โ†’ searchPlaces API ํ˜ธ์ถœ
    • โ†’ Map + PlaceListBottomSheet ๋ Œ๋”๋ง
  2. ๋งˆ์ปค ํด๋ฆญ ํ”Œ๋กœ์šฐ:
    • Map ๋งˆ์ปค ํด๋ฆญ
    • โ†’ bottomSheetStore ์ƒํƒœ ๋ณ€๊ฒฝ
    • โ†’ PlacePinBottomSheet ์—ด๋ฆผ
    • โ†’ PlaceItem ํ‘œ์‹œ
  3. ์ƒ์„ธ ํŽ˜์ด์ง€ ํ”Œ๋กœ์šฐ:
    • 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 ์ตœ์ ํ™” ๊ตฌํ˜„
  • ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ์ •๋ ฌ
  • ์ง€๋„ ํด๋Ÿฌ์Šคํ„ฐ๋ง