Map Feature Guide
File Structure
code
src/
├── pages/Map/
│ └── Map.tsx # Main map page (state management)
│
├── components/map/
│ ├── MapSearchBar.tsx # Search input + autocomplete dropdown
│ ├── MapCard.tsx # Location card in bottom sheet
│ ├── MapDetailSheet.tsx # Location detail bottom sheet
│ ├── MapView.tsx # Kakao Map markers + overlays
│ ├── MapFloatingView.tsx # Source toggle (keco/zerowaste/all)
│ └── bottomSheet/
│ ├── MapBottomSheet.tsx # Bottom sheet container
│ ├── StoreCategoryFilter.tsx # Store category filter UI
│ └── FilterContent.tsx # Pickup category filter
│
├── api/services/map/
│ ├── map.service.ts # API client (MapService class)
│ ├── map.type.ts # TypeScript types + STORE_CATEGORIES
│ └── map.queries.ts # React Query options
│
└── hooks/
└── useKakaoLoaderOrigin.tsx # Kakao Maps SDK loader
Data Flow
code
Map.tsx (state owner) ├── useQuery(MapQueries.getLocations) → MapService.getLocations() ├── MapSearchBar → MapService.suggestPlaces() / searchLocations() ├── MapBottomSheet → MapCard[] → handleSetSelectedId() ├── MapDetailSheet → MapService.getLocationDetail() ├── MapView → kakao.maps.Map + MapMarker[] └── StoreCategoryFilter → selectedStoreCategories state
Key State
typescript
// Map.tsx
const [center, setCenter] = useState({ lat, lng }); // Map center
const [mapZoom, setMapZoom] = useState(3); // Zoom level
const [selectedId, setSelectedId] = useState<number | null>(null);
const [selectedStoreCategories, setSelectedStoreCategories] = useState<string[]>([]);
const [searchResults, setSearchResults] = useState<LocationListResponse | null>(null);
const [toggle, setToggle] = useState<ToggleType>('all'); // Source filter
API Types
typescript
// LocationListRequest
{ lat, lon, radius?, zoom?, store_category?, pickup_category? }
// LocationListItemResponse
{ id, name, source, road_address?, latitude, longitude, distance_km?,
distance_text?, store_category?, is_open?, is_holiday?,
start_time?, end_time?, phone?, pickup_categories?, place_url?, kakao_place_id? }
// SuggestEntry
{ place_name, address, latitude, longitude, place_url? }
// LocationDetailResponse
{ id, name, source, road_address?, lot_address?, latitude?, longitude?,
store_category, pickup_categories, phone?, place_url?, kakao_place_id?,
collection_items?, introduction? }
Store Categories
typescript
const STORE_CATEGORIES = [
{ key: 'refill_zero', label: '제로웨이스트' },
{ key: 'cafe_bakery', label: '카페/베이커리' },
{ key: 'vegan_dining', label: '비건/식당' },
{ key: 'upcycle_recycle', label: '업사이클/재활용' },
{ key: 'book_workshop', label: '도서관/공방' },
{ key: 'market_mart', label: '마트/시장' },
{ key: 'lodging', label: '숙박' },
{ key: 'public_dropbox', label: '무인 수거함' },
{ key: 'general', label: '기타' },
];
Environment Variables
| Variable | Description |
|---|---|
VITE_KAKAO_MAP_API_KEY | Kakao Maps JavaScript SDK key |
VITE_API_BASE_URL | Backend API base URL |
Kakao Map Integration
- •SDK loader:
useKakaoLoaderOriginhook (loadsreact-kakao-maps-sdk) - •Map component:
<Map>fromreact-kakao-maps-sdk - •Markers:
<MapMarker>with custom overlay - •Pan/zoom events:
onCenterChanged,onZoomChanged
Common Patterns
Search flow
- •User types in MapSearchBar
- •Debounce (300ms) →
MapService.suggestPlaces(q) - •Show dropdown with SuggestEntry[]
- •User selects → pan map to coordinates +
MapService.searchLocations()
Filter flow
- •User opens StoreCategoryFilter
- •Toggle categories → temporary state
- •"결과보기" button →
setSelectedStoreCategories() - •useEffect triggers
refetch()with newstore_categoryparam
Detail flow
- •User taps MapCard
- •
setSelectedId(id)→ MapDetailSheet opens - •
MapService.getLocationDetail(id)→ show detail
Directions
typescript
// Opens Kakao Map directions in new tab
window.open(`https://map.kakao.com/link/to/${name},${lat},${lng}`)
Known Issues
- •Query key caching:
MapQueries.getLocationsuses static query key — different map positions return cached data - •Multiple refetch effects: Three
useEffecthooks callrefetch()with different conditions - •MapView marker key: Uses array index instead of item ID
- •Silent error handling: MapDetailSheet/MapSearchBar swallow API errors