사진 쪽은 MediaService 에 list_media 하나뿐이었다 — 쓰기 경로가 아예 없었고
빌더 화면에서도 보기만 됐다. 서비스·라우터부터 열고 도구를 붙였다.
- crud: set_sort_order / service: hide_media · set_primary
- POST .../media/{id}/hide · /primary — ★ 에이전트 전용 뒷문을 만들지 않는다.
그러면 빌더 화면이 그 기능을 못 쓰고 나중에 붙일 때 로직이 두 벌이 된다
- 도구 셋: list_photos(READ) · hide_photo · set_primary_photo(REVERSIBLE)
★ 대표 사진에 별도 칸을 두지 않았다. primary_media 가 '첫 장' 을 쓰고 목록이
ORDER BY sort_order 라, 지정은 sort_order 를 가장 작게 내리는 일이다 —
칸을 따로 두면 규칙이 둘이 되어 검색 결과의 그림과 화면 첫 장이 갈린다.
★ 내려도 지우지 않는다(REJECTED). origin_url·source_type 이 남아야 재게시
권리(DECISIONS 1-2) 결론이 났을 때 되짚을 수 있다.
★★ 업로드·교체는 만들지 않았다 — 미결 사항을 코드가 먼저 푸는 자리다.
테스트가 레지스트리에 upload·replace 가 없는지 실제로 검사한다.
test_agent_runtime 32 passed(사진 6건 추가).
전체 871 passed / 53 failed — 53 은 이번 변경 전과 동일. npm run lint 통과
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
67 lines
3.0 KiB
Python
67 lines
3.0 KiB
Python
from typing import Optional
|
|
from uuid import UUID
|
|
|
|
from fastapi import APIRouter, Depends, Query
|
|
|
|
from common.models.gmodel import UserInfo
|
|
from router.v1.validator.dependencies import IsValidAccessToken, RemoveNoneResponse
|
|
from services.media_service import MediaService
|
|
from .protocol import Res_MediaList
|
|
|
|
# 사진 라우터. 사업장(place_id) 하위 리소스이며, 회사 스코프는 service 가 사업장 조회로 강제한다.
|
|
router = APIRouter(prefix="/v1/place/{place_id}/media", tags=["Media"], responses={404: {"description": "Not found"}})
|
|
|
|
|
|
@router.get(
|
|
path="/list",
|
|
response_model=Res_MediaList,
|
|
summary="사진 목록",
|
|
description="publishable_only=true 면 ★ 사이트에 실제로 나갈 수 있는 것(승인 + alt 있음)만 — "
|
|
"빌드 스냅샷과 같은 기준이다. unit_id 를 주면 그 객실·메뉴 사진만. "
|
|
"★ 응답에는 항상 source_type(owner/crawl)과 origin_url 이 실린다 — "
|
|
"크롤링 이미지 재게시 권리가 미결이라(docs/DECISIONS.md 1-2) 결론에 따라 "
|
|
"CRAWL 을 통째로 걸러내야 하고, 화면이 출처를 모르면 그 판단을 못 한다.",
|
|
)
|
|
async def list_media(
|
|
place_id: UUID,
|
|
service: MediaService = Depends(),
|
|
user_info: UserInfo = Depends(IsValidAccessToken),
|
|
unit_id: Optional[UUID] = Query(None, description="객실·메뉴 단위 사진만"),
|
|
publishable_only: bool = Query(False),
|
|
):
|
|
return RemoveNoneResponse(await service.list_media(user_info, str(place_id), unit_id, publishable_only))
|
|
|
|
|
|
@router.post(
|
|
path="/{media_id}/hide",
|
|
response_model=Res_MediaList,
|
|
summary="사진 내리기",
|
|
description="그 사진을 REJECTED 로 내려 발행본에서 뺀다. ★ 지우지 않는다 — origin_url·source_type 이 "
|
|
"남아 있어야 재게시 권리(docs/DECISIONS.md 1-2) 결론이 났을 때 되짚을 수 있고, "
|
|
"잘못 내렸을 때 되돌릴 수도 있다. 응답은 갱신된 목록이다.",
|
|
)
|
|
async def hide_media(
|
|
place_id: UUID,
|
|
media_id: UUID,
|
|
service: MediaService = Depends(),
|
|
user_info: UserInfo = Depends(IsValidAccessToken),
|
|
):
|
|
return RemoveNoneResponse(await service.hide_media(user_info, str(place_id), str(media_id)))
|
|
|
|
|
|
@router.post(
|
|
path="/{media_id}/primary",
|
|
response_model=Res_MediaList,
|
|
summary="대표 사진 지정",
|
|
description="목록 맨 앞으로 올린다. ★ 대표 사진은 별도 칸이 아니라 **목록의 첫 장**이다"
|
|
"(site_payload.primary_media) — 칸을 따로 두면 검색 결과에 뜨는 그림과 화면 첫 장이 갈린다. "
|
|
"객실·메뉴 전용 사진(unit_id 가 있는 것)은 대표가 될 수 없다.",
|
|
)
|
|
async def set_primary_media(
|
|
place_id: UUID,
|
|
media_id: UUID,
|
|
service: MediaService = Depends(),
|
|
user_info: UserInfo = Depends(IsValidAccessToken),
|
|
):
|
|
return RemoveNoneResponse(await service.set_primary(user_info, str(place_id), str(media_id)))
|