o2o-site-AEO/solution/backend/router/v1/local/local.py
김성경 01824a7a91 [feat] solution,admin,postgres-init: 주변정보를 업장 좌표 기준으로 — 시군구 캐시는 "가까운 곳"을 못 준다
지역 정보(맛집·관광지·축제)는 행정구역 코드(local_contents.region_code) 단위로 캐시돼
"그 시군구에 있는 것"을 줬다. 양양군 업장 옆 5km 속초 관광지는 빠지고 같은 군 반대편
30km 맛집이 붙는 구조라, 캔버스의 지역 정보 섹션은 늘 "준비 중"이었다. 업장 좌표로
TourAPI 를 직접 물어 업장 단위(place_contents)에 담고, 캔버스는 스크린샷으로 받은
형식(도보 시간 필터 + 카드 캐러셀)으로 통일했다.
2026-09-08 11:46:03 +09:00

78 lines
3.8 KiB
Python

import uuid
from fastapi import APIRouter, Depends, Query
from common.enums import LocalContentStatus
from common.models.gmodel import UserInfo
from router.v1.validator.dependencies import RequireOwner, RemoveNoneResponse
from services.local_content_service import LocalContentService
from .protocol import (
ReqHidePlaceContent, ReqPublishLocalContent, ReqUpdateLocalContent,
ResLocalContentList, ResLocalGuide, ResPlaceContentList, ResSyncPlace, ResWeather,
)
router = APIRouter(prefix="/v1/admin/local-content", tags=["LocalContent"])
weather_router = APIRouter(prefix="/v1/local", tags=["LocalContent"])
@weather_router.get("/weather", response_model=ResWeather, summary="현재 날씨 조회 (cache-aside)")
async def get_weather(
region_code: str = Query(min_length=1, max_length=10),
latitude: float = Query(ge=-90, le=90),
longitude: float = Query(ge=-180, le=180),
service: LocalContentService = Depends(),
):
return RemoveNoneResponse(await service.get_weather(region_code, latitude, longitude))
@weather_router.get("/guide", response_model=ResLocalGuide, summary="업장 주변 가이드(맛집·명소·축제·코스) — 에디터 캔버스용")
async def get_guide(
place_id: uuid.UUID = Query(),
service: LocalContentService = Depends(),
):
"""날씨와 같은 공개 조회다 — 운영자가 숨기지 않은 공공데이터만 나가므로 인증을 요구하지 않는다."""
return RemoveNoneResponse(await service.get_guide(place_id))
@router.get("/place/{place_id}", response_model=ResPlaceContentList, summary="업장 주변정보 목록 (숨김 포함)")
async def list_place_contents(place_id: uuid.UUID, service: LocalContentService = Depends(), _user=Depends(RequireOwner)):
return RemoveNoneResponse(await service.list_place_contents(place_id))
@router.post("/place/{place_id}/sync", response_model=ResSyncPlace, summary="업장 주변정보 재수집 (TourAPI 반경)")
async def sync_place(place_id: uuid.UUID, service: LocalContentService = Depends(), _user=Depends(RequireOwner)):
"""빌드가 매번 하는 것과 같은 수집을 운영자가 직접 누른다. 무인 갱신(스케줄러)은 아직 없다."""
return RemoveNoneResponse(await service.sync_place_by_id(place_id))
@router.post("/place-content/{place_content_id}/hide", response_model=ResPlaceContentList, summary="주변정보 숨김/해제")
async def hide_place_content(
place_content_id: uuid.UUID, req: ReqHidePlaceContent,
service: LocalContentService = Depends(), _user=Depends(RequireOwner),
):
"""숨긴 항목은 재수집이 되살리지 않는다. 다음 빌드부터 발행본에서 빠진다."""
return RemoveNoneResponse(await service.set_hidden(place_content_id, req.hidden))
@router.get("", response_model=ResLocalContentList)
async def list_contents(
status: LocalContentStatus | None = Query(None), region_code: str | None = Query(None),
service: LocalContentService = Depends(), _user: UserInfo = Depends(RequireOwner),
):
return RemoveNoneResponse(await service.list(status.value if status else None, region_code))
@router.post("/publish", response_model=ResLocalContentList)
async def publish(req: ReqPublishLocalContent, service: LocalContentService = Depends(), user=Depends(RequireOwner)):
return RemoveNoneResponse(await service.publish(req.content_ids, user.user_id))
@router.patch("/{content_id}", response_model=ResLocalContentList)
async def update_content(content_id: str, req: ReqUpdateLocalContent, service: LocalContentService = Depends(), _user=Depends(RequireOwner)):
return RemoveNoneResponse(await service.update(content_id, req))
@router.post("/{content_id}/end", response_model=ResLocalContentList)
async def end_content(content_id: str, service: LocalContentService = Depends(), _user=Depends(RequireOwner)):
return RemoveNoneResponse(await service.end(content_id))