Location Service Guide
Architecture Overview
apps/location/
├── domain/ # Pure domain layer
│ ├── entities/ # NormalizedSite
│ ├── value_objects/ # Coordinates
│ ├── enums/ # StoreCategory, PickupCategory
│ └── exceptions/ # LocationNotFoundError
│
├── application/
│ ├── nearby/
│ │ ├── dto/ # LocationEntryDTO, SuggestEntryDTO, LocationDetailDTO
│ │ ├── ports/ # LocationReader (ABC)
│ │ ├── queries/ # GetNearbyCentersQuery, SearchByKeywordQuery, etc.
│ │ └── services/ # CategoryClassifierService, LocationEntryBuilder, ZoomPolicyService
│ └── ports/ # KakaoLocalClientPort (ABC)
│
├── infrastructure/
│ ├── integrations/kakao/ # KakaoLocalHttpClient
│ ├── persistence_postgres/ # SqlaLocationReader
│ └── observability/ # OpenTelemetry
│
├── presentation/http/
│ ├── controllers/ # location.py (endpoints)
│ ├── schemas/ # LocationEntry, SuggestEntry, LocationDetail
│ └── errors/ # Exception handlers
│
└── setup/
├── config.py # Settings (pydantic-settings)
├── dependencies.py # FastAPI DI wiring
└── database.py # SQLAlchemy async session
API Endpoints
| Method | Path | Description | Response |
|---|---|---|---|
| GET | /api/v1/locations/centers | Nearby centers | list[LocationEntry] |
| GET | /api/v1/locations/search | Keyword search (Kakao + DB hybrid) | list[LocationEntry] |
| GET | /api/v1/locations/suggest | Autocomplete suggestions | list[SuggestEntry] |
| GET | /api/v1/locations/centers/{id} | Center detail | LocationDetail |
Key Patterns
1. Hybrid Search Flow (SearchByKeywordQuery)
User Query → Kakao keyword search → Extract coordinates
↓
DB spatial query (earth_distance/Haversine)
↓
Merge DB + Kakao results
↓
Deduplicate (50m threshold)
↓
Return unified LocationEntryDTO[]
2. Category Classification
CategoryClassifierService.classify(site, metadata) analyzes:
- •
positn_intdc_cn(introduction text) - •
positn_nm(name) - •
metadata.clctItemCn(collection items)
Returns: (StoreCategory, list[PickupCategory])
Priority order: REFILL_ZERO > CAFE_BAKERY > VEGAN_DINING > UPCYCLE_RECYCLE > BOOK_WORKSHOP > MARKET_MART > LODGING > PUBLIC_DROPBOX > GENERAL
3. Distance Calculation Fallback
try:
PostGIS earth_distance (accurate, requires extension)
except DBAPIError:
Haversine formula (fallback, pure SQL math)
4. Kakao API Integration
- •Port:
KakaoLocalClientPort(application/ports/) - •Adapter:
KakaoLocalHttpClient(infrastructure/integrations/kakao/) - •Singleton via
get_kakao_client()in dependencies.py - •Graceful degradation: returns None if
LOCATION_KAKAO_REST_API_KEYnot set
5. DTO ↔ Schema Mapping
Application layer returns DTOs (dataclasses), presentation layer maps to Pydantic schemas:
Query.execute() → LocationEntryDTO (application layer) Controller → LocationEntry (presentation schema, Pydantic)
Environment Variables
| Variable | Default | Description |
|---|---|---|
LOCATION_DATABASE_URL | postgresql+asyncpg://... | DB connection |
LOCATION_KAKAO_REST_API_KEY | "" (disabled) | Kakao REST API key |
LOCATION_KAKAO_API_TIMEOUT | 5.0 | Kakao API timeout (seconds) |
LOCATION_OTEL_ENABLED | False | OpenTelemetry tracing |
LOCATION_REDIS_URL | redis://localhost:6379/5 | Redis for metrics cache |
Domain Enums
StoreCategory
refill_zero, cafe_bakery, vegan_dining, upcycle_recycle, book_workshop, market_mart, lodging, public_dropbox, general
PickupCategory
clear_pet, can, paper, glass, plastic, vinyl, styrofoam, clothing, electronics, fluorescent, battery, medicine, cooking_oil, general
Testing
pytest apps/location/tests/ -v
Test structure:
- •
test_domain.py— Entities, Value Objects, Enums (no mocks) - •
test_services.py— CategoryClassifier, LocationEntryBuilder, ZoomPolicy - •
test_queries.py— GetNearbyCenters, SearchByKeyword, Suggest, CenterDetail (mocked ports) - •
test_controllers.py— HTTP parameter validation, endpoint routing
Frontend Integration
Frontend uses:
- •
VITE_KAKAO_MAP_API_KEY— Kakao Maps JS SDK (client-side, different from backend key) - •
VITE_API_BASE_URL— Backend API base URL - •API client:
src/api/services/map/map.service.ts - •Types:
src/api/services/map/map.type.ts
Common Issues
- •Kakao features disabled: Check
LOCATION_KAKAO_REST_API_KEYis set - •earth_distance not available: Falls back to Haversine (no PostGIS extension)
- •Negative IDs in search results: Kakao-only results use negative IDs (-1, -2, ...)
- •Silent category fallback: Unknown items default to
GENERAL