도메인별 스키마(company·place·fact·local·site·job)를 걷어내고 public 한 벌로 폈다.
스키마 한정자가 붙은 순간부터 ORM·raw SQL·테스트 픽스처가 각자 그 이름을 들고 다녀야 했다.
- 공용 콘텐츠를 한 테이블로 되돌린다. spots·region_stories 를 따로 파 놓고 보니
같은 성격이 세 곳으로 갈라져 있었다 — `area_contents` 가 처음부터 content_type 으로
종류를 가르는 설계였고 그걸 쓰면 됐다. 관계(거리·숨김)만 `place_area_refs` 로 남긴다.
- migrations/ + scripts/migrate.py: `init.sql` 은 **DB 를 처음 만들 때만** 돈다. 파일에
컬럼을 더해도 이미 데이터가 든 DB 에는 반영되지 않는다 — 실제로 TourAPI 가 주변 정보를
받아 와도 저장할 곳이 없어 축제·맛집이 0건이었고, 화면에는 "그냥 안 나오는 것" 으로만 보였다.
DECISIONS.md 가 예고한 그대로다("운영 DB 가 생기는 순간 다시 필요해진다").
Alembic 을 쓰지 않는 이유는 스키마 정의가 이미 두 곳(ORM·init.sql)이라 세 번째를
더하면 어긋날 자리가 하나 더 생기기 때문이다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
78 lines
3.8 KiB
Python
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))
|