여러 줄 주석이 설명보다 경위(예전·실측·지적)를 적고 있어 읽는 사람이 결론을 찾기 어려웠다. - ts·tsx·js·mjs·css·py 478개: 여러 줄 주석은 첫 문장 한 줄로, 과거형·날짜 문장은 삭제 - 주석 위치는 TypeScript 파서·파이썬 tokenize/ast 로 찾는다 — 문자열 안의 # · /* 는 건드리지 않는다 - eslint·ts·noqa·type: ignore 같은 지시 주석은 그대로 둔다 파이썬 275개 정리 전후 AST 동일, TS 298개 주석 뺀 토큰 동일(빈 JSX 주석 10곳만 차이). site·frontend·admin tsc, site vitest 105 passed Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
230 lines
6.9 KiB
Python
230 lines
6.9 KiB
Python
import uuid
|
|
from datetime import datetime
|
|
from decimal import Decimal
|
|
from typing import Optional
|
|
|
|
from pydantic import ConfigDict
|
|
|
|
from common.enums import ExternalPlaceSource, JobStatus, LinkChannel, PlaceCategory, PlaceStatus, SourceType
|
|
from common.models.gmodel import Res_PageProtocol, Res_WebPacketProtocol, WebPacketProtocol
|
|
|
|
|
|
# 라우터 폴더마다 protocol.py 를 두고 Req_/Res_ 를 정의한다 (protocol 규약).
|
|
class PlaceProtocol(WebPacketProtocol):
|
|
pass
|
|
|
|
|
|
class Req_CreatePlace(PlaceProtocol):
|
|
# 상호명 하나로 시작한다.
|
|
name: str = ""
|
|
category: PlaceCategory = PlaceCategory.LODGING
|
|
# 주인은 받지 않는다 — 토큰이 정한다(place_service.create_place).
|
|
|
|
|
|
class Req_VerifyPlace(PlaceProtocol):
|
|
"""카카오 로컬 조회 결과를 사업장에 박제한다(동일 업소 확정)."""
|
|
|
|
source: ExternalPlaceSource = ExternalPlaceSource.NAVER
|
|
external_place_id: str = ""
|
|
# 외부 장소 DB 가 함께 준 업체 홈페이지 URL.
|
|
place_url: Optional[str] = None
|
|
road_address: Optional[str] = None
|
|
address: Optional[str] = None
|
|
phone: Optional[str] = None
|
|
latitude: Optional[Decimal] = None
|
|
longitude: Optional[Decimal] = None
|
|
region_code: Optional[str] = None
|
|
# 외부 장소 DB 의 분류 문자열(후보의 category_name).
|
|
category_name: Optional[str] = None
|
|
|
|
|
|
class Req_VerifyPlaceByUrl(PlaceProtocol):
|
|
"""네이버 플레이스 URL 하나로 동일 업소를 확정한다."""
|
|
|
|
url: str = ""
|
|
|
|
# 같은 가게를 이미 갖고 있을 때 그 사업장으로 이어붙일지.
|
|
reuse_existing: bool = True
|
|
|
|
|
|
class Req_UpdatePlace(PlaceProtocol):
|
|
# 주인은 못 바꾼다(위 Req_CreatePlace 주석).
|
|
name: Optional[str] = None
|
|
status: Optional[PlaceStatus] = None
|
|
# 미니 블로그 승인 메일 수신 주소.
|
|
notify_email: Optional[str] = None
|
|
|
|
|
|
class Req_CreateUnit(PlaceProtocol):
|
|
name: str = ""
|
|
sort_order: int = 0
|
|
|
|
|
|
class Req_CreateLink(PlaceProtocol):
|
|
channel: LinkChannel = LinkChannel.ETC
|
|
url: str = ""
|
|
title: Optional[str] = None
|
|
discovered_by: SourceType = SourceType.OWNER
|
|
|
|
|
|
class PlaceData(WebPacketProtocol):
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
place_id: uuid.UUID
|
|
name: str
|
|
category: PlaceCategory
|
|
status: PlaceStatus
|
|
owner_user_id: Optional[uuid.UUID] = None
|
|
external_source: Optional[ExternalPlaceSource] = None
|
|
external_place_id: Optional[str] = None
|
|
road_address: Optional[str] = None
|
|
address: Optional[str] = None
|
|
phone: Optional[str] = None
|
|
latitude: Optional[Decimal] = None
|
|
longitude: Optional[Decimal] = None
|
|
region_code: Optional[str] = None
|
|
verified_at: Optional[datetime] = None
|
|
content_updated_at: Optional[datetime] = None # 노출값 변경 시각 — 개별 재빌드 대상 판별
|
|
notify_email: Optional[str] = None # 미니 블로그 승인 메일 수신 주소.
|
|
created_at: Optional[datetime] = None
|
|
|
|
|
|
class UnitData(WebPacketProtocol):
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
unit_id: uuid.UUID
|
|
place_id: uuid.UUID
|
|
name: str
|
|
sort_order: int = 0
|
|
|
|
|
|
class LinkData(WebPacketProtocol):
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
link_id: uuid.UUID
|
|
place_id: uuid.UUID
|
|
channel: LinkChannel
|
|
url: str
|
|
title: Optional[str] = None
|
|
discovered_by: SourceType
|
|
discovered_at: Optional[datetime] = None
|
|
confirmed_at: Optional[datetime] = None # NULL = 크롤링 대상 아님
|
|
|
|
|
|
class Res_PlaceList(Res_PageProtocol):
|
|
places: list[PlaceData] = []
|
|
|
|
|
|
class Res_Place(Res_WebPacketProtocol):
|
|
place: Optional[PlaceData] = None
|
|
|
|
|
|
class Res_UnitList(Res_WebPacketProtocol):
|
|
units: list[UnitData] = []
|
|
|
|
|
|
class Res_Unit(Res_WebPacketProtocol):
|
|
unit: Optional[UnitData] = None
|
|
|
|
|
|
class Res_LinkList(Res_WebPacketProtocol):
|
|
links: list[LinkData] = []
|
|
confirmed: int = 0 # 크롤링 가능한 링크 수
|
|
|
|
|
|
class Res_Link(Res_WebPacketProtocol):
|
|
link: Optional[LinkData] = None
|
|
|
|
|
|
class Req_StartCollect(PlaceProtocol):
|
|
"""수집 시작."""
|
|
|
|
# 확정된 채널 URL 만 크롤링한다.
|
|
link_ids: list[uuid.UUID] = []
|
|
# 이미 확보한 fact 를 다시 긁을지.
|
|
force: bool = False
|
|
# 상호·주소로 공개 채널 URL 을 Perplexity 에서 추가 탐색할지.
|
|
discover_channels: bool = False
|
|
|
|
|
|
class Res_StartCollect(Res_WebPacketProtocol):
|
|
"""수집 잡 적재 결과."""
|
|
|
|
job_id: Optional[uuid.UUID] = None
|
|
status: Optional[JobStatus] = None
|
|
created: bool = True
|
|
confirmed_links: int = 0
|
|
|
|
|
|
class Req_StartVision(PlaceProtocol):
|
|
"""사진 분석 시작."""
|
|
|
|
# 이미 분석된 사진도 다시 태울지.
|
|
force: bool = False
|
|
|
|
|
|
class Res_StartVision(Res_WebPacketProtocol):
|
|
job_id: Optional[uuid.UUID] = None
|
|
status: Optional[JobStatus] = None
|
|
created: bool = True
|
|
pending_media: int = 0 # 분석 대상 사진 수
|
|
|
|
|
|
# 동일 업소 후보 (UI 가 사람에게 고르게 한다)
|
|
class PlaceCandidate(WebPacketProtocol):
|
|
"""외부 장소 DB 에서 찾은 후보 1건."""
|
|
|
|
external_place_id: Optional[str] = None # 네이버는 안 준다
|
|
name: str = ""
|
|
road_address: Optional[str] = None
|
|
address: Optional[str] = None
|
|
phone: Optional[str] = None # 네이버는 안 준다
|
|
latitude: Optional[Decimal] = None
|
|
longitude: Optional[Decimal] = None
|
|
category_name: Optional[str] = None
|
|
place_url: Optional[str] = None # 업체 홈페이지 — 확정 시 공식 채널로 등록된다
|
|
naver_place_url: Optional[str] = None # 자동 발견한 네이버 플레이스.
|
|
|
|
|
|
class Res_VerifyCandidates(Res_WebPacketProtocol):
|
|
"""동일 업소 후보 목록."""
|
|
|
|
source: Optional[ExternalPlaceSource] = None
|
|
outcome: str = "" # matched | ambiguous | no_candidate
|
|
reason: Optional[str] = None # 판정 근거(name_exact / name_duplicate / name_no_exact …)
|
|
auto_selectable: bool = False # True 면 UI 가 '이거 맞나요?' 한 번만 물어도 된다
|
|
candidates: list[PlaceCandidate] = []
|
|
|
|
|
|
# 공개 상호명 검색 (랜딩 첫 화면)
|
|
class PlaceSearchItem(WebPacketProtocol):
|
|
"""공개 검색 결과 1건."""
|
|
|
|
name: str = ""
|
|
road_address: Optional[str] = None
|
|
category_name: Optional[str] = None # 외부 DB 의 분류 문자열(예: "숙박>펜션")
|
|
# 추정 업종.
|
|
category: Optional[PlaceCategory] = None
|
|
# 자동으로 찾은 네이버 플레이스 주소.
|
|
naver_place_url: Optional[str] = None
|
|
|
|
|
|
class Res_PlaceSearch(Res_WebPacketProtocol):
|
|
"""상호명 공개 검색 결과."""
|
|
|
|
source: Optional[ExternalPlaceSource] = None
|
|
items: list[PlaceSearchItem] = []
|
|
|
|
|
|
class Req_StartCopy(PlaceProtocol):
|
|
"""소개문·FAQ 생성 시작."""
|
|
|
|
resume: bool = False
|
|
|
|
|
|
class Res_StartCopy(Res_WebPacketProtocol):
|
|
job_id: Optional[uuid.UUID] = None
|
|
status: Optional[JobStatus] = None
|
|
created: bool = True
|
|
grounded_facts: int = 0 # 근거로 쓸 수 있는 확인된 fact 수
|