import uuid from datetime import date, datetime from typing import Any, Optional from pydantic import ConfigDict from common.enums import ( BuildStatus, JobStatus, PlaceCategory, PlaceStatus, PublishAction, PublishRejectReason, PublishResult, SiteStatus, ) from common.models.gmodel import Res_PageProtocol, Res_WebPacketProtocol, WebPacketProtocol class SiteProtocol(WebPacketProtocol): pass class Req_StartBuild(SiteProtocol): """정적 빌드 시작.""" publish: bool = False class Res_StartBuild(Res_WebPacketProtocol): job_id: Optional[uuid.UUID] = None status: Optional[JobStatus] = None created: bool = True class Req_Rollback(SiteProtocol): """재굽기가 아니다 — 대상 버전이 디스크에 아직 있으면 심볼릭 링크만 돌린다.""" target_version: int class SiteVersionData(WebPacketProtocol): model_config = ConfigDict(from_attributes=True) site_version_id: uuid.UUID version: int build_status: BuildStatus unique_content_count: int = 0 build_error: Optional[str] = None built_at: Optional[datetime] = None created_at: Optional[datetime] = None class SiteData(WebPacketProtocol): model_config = ConfigDict(from_attributes=True) site_id: uuid.UUID place_id: uuid.UUID status: SiteStatus domain: Optional[str] = None # 사장님이 고른 템플릿. template_id: Optional[str] = None # 저장된 디자인(색·서체·섹션). theme: Optional[dict[str, Any]] = None current_version_id: Optional[uuid.UUID] = None published_at: Optional[datetime] = None # 발행 썸네일(Azure Blob 공개 URL). thumbnail_url: Optional[str] = None class MySiteData(WebPacketProtocol): """내 사이트 목록의 한 줄 — 사업장(place) + 사이트(site).""" place_id: uuid.UUID name: str category: PlaceCategory place_status: PlaceStatus road_address: Optional[str] = None created_at: Optional[datetime] = None site_id: Optional[uuid.UUID] = None status: Optional[SiteStatus] = None domain: Optional[str] = None template_id: Optional[str] = None published_at: Optional[datetime] = None # 목록 카드의 그림. thumbnail_url: Optional[str] = None # 단건과 같은 규칙 — 노출값이 마지막 빌드보다 나중에 바뀌었으면 재발행 대상이다. needs_rebuild: bool = False class Res_MySites(Res_PageProtocol): sites: list[MySiteData] = [] class PublishLogData(WebPacketProtocol): model_config = ConfigDict(from_attributes=True) publish_log_id: uuid.UUID action: PublishAction result: PublishResult reject_reason: Optional[PublishRejectReason] = None detail: Optional[Any] = None created_at: Optional[datetime] = None class Req_SiteTemplate(SiteProtocol): """템플릿 선택 저장. 업종 허용 목록(solution/shared/src/data/templates.json)에 없는 id는 거절한다.""" template_id: str = "" class Req_SiteTheme(SiteProtocol): """색·섹션 저장.""" theme: dict[str, Any] = {} class RenderStatusData(WebPacketProtocol): """정적 페이지가 실제로 구워졌는지.""" # PENDING(아직) · STALE(낡음) · OK · FAILED state: str = "PENDING" rendered_at: Optional[str] = None rendered_version: Optional[int] = None error: Optional[str] = None class Res_Site(Res_WebPacketProtocol): site: Optional[SiteData] = None current_version: Optional[SiteVersionData] = None # 노출값이 바뀐 뒤 다시 빌드하지 않았으면 True — 개별 재빌드 대상이라는 표시. needs_rebuild: bool = False # 빌드(DB)와 렌더(정적 파일)는 다른 단계다. render: RenderStatusData = RenderStatusData() class Res_SiteVersions(Res_WebPacketProtocol): versions: list[SiteVersionData] = [] class Res_PublishLogs(Res_WebPacketProtocol): logs: list[PublishLogData] = [] class AuditCheckData(WebPacketProtocol): id: str group: str label: str points: int earned: int status: str detail: str recommendation: Optional[str] = None class Res_SeoAudit(Res_WebPacketProtocol): seo_score: int = 0 aeo_score: int = 0 overall_score: int = 0 checks: list[AuditCheckData] = [] summary: dict[str, Any] = {} visibility: dict[str, int] = {} disclaimer: str = "" class Req_SiteSlug(SiteProtocol): """사이트 주소(네임스페이스) 예약.""" slug: str = "" class Res_SlugCheck(Res_WebPacketProtocol): """주소 사용 가능 확인. UI 가 타이핑 중에 호출한다.""" available: bool = False # 불가 사유(services/site_slug 의 REASON_*): INVALID_LENGTH / INVALID_FORMAT / RESERVED / TAKEN. reason: Optional[str] = None # 이름 자체는 멀쩡한데 못 쓰는 경우(예약어·중복)에만 대안을 하나 준다. suggestion: Optional[str] = None class Res_SiteSlug(Res_WebPacketProtocol): """주소 저장 결과. 거부됐으면 왜/대안을 check 와 같은 코드로 돌려준다.""" site: Optional[SiteData] = None reason: Optional[str] = None suggestion: Optional[str] = None class Req_SiteStatus(SiteProtocol): """발행 상태 전이.""" action: PublishAction class ShowcaseItem(WebPacketProtocol): """랜딩 쇼케이스 카드 한 장.""" name: str category: PlaceCategory # "강원특별자치도 양양군" 수준. region: Optional[str] = None # 발행 주소. url: str # 없으면 화면이 글자 카드로 떨어진다(썸네일은 발행의 부수 효과라 실패할 수 있다). thumbnail_url: Optional[str] = None class Res_Showcase(Res_WebPacketProtocol): items: list[ShowcaseItem] = [] class PostData(WebPacketProtocol): """미니 블로그 글 하나 — 빌더 앱 '이번 달 생성된 글' 목록 카드.""" model_config = ConfigDict(from_attributes=True) post_id: uuid.UUID body: str topic_kind: int status: int scheduled_date: Optional[date] = None created_at: Optional[datetime] = None sent_at: Optional[datetime] = None approved_at: Optional[datetime] = None published_at: Optional[datetime] = None # 화면은 발행완료/발행실패만 보여준다(발행 전 상태는 안 보여준다) — 승인됐는데 BUILD 잡이 dead-letter 로 끝났을 때만 true(PostService._latest_build_failed). build_failed: bool = False class Res_MyPosts(Res_WebPacketProtocol): posts: list[PostData] = [] class Req_EditPost(SiteProtocol): """수정하고 그대로 승인 — 로그인 세션 버전(메일 없이 목록에서 바로 고칠 때).""" body: str class Res_GenerateNow(Res_WebPacketProtocol): """즉시 생성 결과 — 사장님이 고른 구간(시작~끝) 중 몇 일을 채웠는지.""" requested: int = 0 created: int = 0 class GenerationBatch(WebPacketProtocol): """생성 회차 하나 — 같은 스윕에서 한 번에 만들어진 글 묶음(post_crud.generation_batches).""" created_at: datetime count: int model: Optional[str] = None class Res_GenerationHistory(Res_WebPacketProtocol): batches: list[GenerationBatch] = [] class Res_GenerateOne(Res_WebPacketProtocol): """개별 생성 결과 — 달력에서 빈 날짜 하나를 콕 집어 만들었을 때.""" post: Optional[PostData] = None