o2o-site-AEO/backend/router/v1/media/protocol.py
Mina Choi 6784e59ca5 최초 커밋 — 기존 코드 전체 + 문서 체계 신설
git 저장소가 없어 히스토리·협업 기반이 아예 없던 상태를 연다.
함께 문서를 재편했다. 그동안 문서가 있어도 "이 제품이 뭘 푸는가"와
"어떻게 도는가"를 담은 문서가 없어서, 목표 문장이 backend/frontend
README 두 곳에 복붙돼 있었다 — 상위 문서가 없어 아래로 샌 것이다.

신설
  README.md               레포 진입점 + 문서 지도 + 문서 규칙 4가지
  AGENTS.md               에이전트·신규 합류자용 함정 목록과 규약
                          (CLAUDE.md 는 여기로 걸린 심볼릭 링크)
  docs/PRODUCT.md         제품 정의 — 문제·사용자·원칙·**non-goals**·성공 기준
  docs/ARCHITECTURE.md    payload 경계·발행 파이프라인·서빙 결정·앱 분리 설계

이동
  backend/docs/DECISIONS.md → docs/DECISIONS.md
    백엔드만의 결정이 아니다. 게다가 코드 주석 ~25곳이 이미
    `docs/DECISIONS.md` 로 적고 있어 레포 루트 기준으로는 그게 맞다.

갱신
  docs/DEPLOY.md          서빙 결정 반영 — nginx 정적 서빙이 지금 경로(3절),
                          Azure 는 나중에 켤 때(4절)로 분리
  docs/ARCHITECTURE.md    사이트 = 한 장(2026-08-31) 구조 반영
  docs/COLLECTION_SEO_AEO_FLOW.md
                          robots.txt·sitemap.xml 은 오리진 루트에만 굽는다는 점 명시
  frontend/site/scripts/prerender.ts
                          헤더 주석의 렌더 보고서 경로가 실제(422줄)와 달라 수정

.gitignore
  ★ CLAUDE.md 를 더 이상 무시하지 않는다. 에이전트 지침은 팀과 모든
    에이전트가 공유하는 규약이라 커밋해야 한다 — 무시하면 클론한 사람이
    "배포 후 republish_all.py 필수" 같은 함정을 전달받지 못한다.
    개인용 오버라이드는 ~/.claude/CLAUDE.md 에 둔다.
2026-08-31 13:57:59 +09:00

53 lines
2.7 KiB
Python

import uuid
from datetime import datetime
from decimal import Decimal
from typing import Optional
from pydantic import ConfigDict
from common.enums import MediaStatus, SourceType
from common.models.gmodel import Res_WebPacketProtocol, WebPacketProtocol
# 라우터 폴더마다 protocol.py 를 두고 Req_/Res_ 를 정의한다 (protocol 규약).
class MediaProtocol(WebPacketProtocol):
pass
class MediaData(WebPacketProtocol):
"""사진 1건.
★ source_type 과 origin_url 을 반드시 함께 내려보낸다 — 크롤링 이미지의 재게시 권리가
아직 미결이라(docs/DECISIONS.md 1-2), 결론이 '불가'로 나면 발행에서 source_type = CRAWL 을
통째로 제외해야 한다. 화면이 출처를 모르면 무엇이 빠질지도 미리 보여줄 수 없다.
origin_url 은 그때 '이 사진은 어디서 왔는가'를 증명하는 유일한 근거다."""
model_config = ConfigDict(from_attributes=True)
media_id: uuid.UUID
place_id: uuid.UUID
unit_id: Optional[uuid.UUID] = None # 객실·메뉴 사진이면 연결
url: str # 우리가 보관하는 접근 URL
origin_url: Optional[str] = None # ★ 수집 원본 이미지 URL — 권리 판단의 근거
source_type: SourceType # ★ OWNER 업로드 / CRAWL 수집 — 발행 필터 키
source_url: Optional[str] = None # 수집한 페이지 URL
label: Optional[str] = None # Vision 분류 라벨 (예: "A동 침실")
alt_text: Optional[str] = None # Vision 생성 alt
vision_confidence: Optional[Decimal] = None # 0.000~1.000
status: MediaStatus # PENDING_REVIEW 는 사람 확인 큐에 남아 있다는 뜻
width: Optional[int] = None
height: Optional[int] = None
sort_order: int = 0
created_at: Optional[datetime] = None
# DB 컬럼이 아니라 계산값이다 — '지금 발행하면 이 사진이 사이트에 실리는가'.
# 판단 기준을 services/snapshot.py 와 똑같이 맞춘다(승인 + alt 있음). 화면이 "왜 이 사진은
# 안 나오나"를 사장님에게 설명할 수 있어야 하는데, 그 답이 상태 하나로는 안 나오기 때문이다.
publishable: bool = False
class Res_MediaList(Res_WebPacketProtocol):
media: list[MediaData] = []
publishable: int = 0 # ★ 실제로 사이트에 나갈 수 있는 사진 수(승인 + alt 있음)
pending_review: int = 0 # 사람 확인 큐에 남은 사진 수(관리 화면 배지)
crawled: int = 0 # ★ 재게시 권리 미결(1-2) — 결론이 '불가'면 통째로 빠질 사진 수