o2o-negosium-original/lps/router/v1/lps/protocol.py
민헌 3a2c6e43d4 feat(lps): price_history 몰별 최저가 스냅샷(by_mall JSONB)
몰이 열린 집합이라 와이드 컬럼(gmarket_*, st11_* …) 대신 JSONB 한 컬럼으로 담는다
— 몰 추가 시 마이그레이션 0. naver/coupang/final 3선 컬럼은 그래프 하위호환 유지.

- models: price_history.by_mall JSONB 추가
- crud: record/list 에 by_mall 왕복(json.dumps + CAST jsonb)
- handler: _price_snapshot 에 summarize_by_mall 적재, final 은 소스무관 전체 최저로
- protocol/service: history API 응답에 by_mall 노출
- migrations/2026-07-09: 기존 dev DB 동기화용 ALTER(추적 파일)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 14:15:58 +09:00

68 lines
2.9 KiB
Python

"""LPS API 요청/응답 프로토콜. 커머스→오투오 검색요청 계약을 미러링한다."""
from typing import Optional
from pydantic import BaseModel, Field
from common.models.gmodel import Req_WebPacketProtocol, Res_WebPacketProtocol
class SearchItem(BaseModel):
"""검색 대상 상품 1건. 규격/모델/제조사는 매칭(향후 AI 유사도) 입력으로 함께 적재한다."""
product_code: str = Field(description="상품 식별 코드(커머스 기준). dedupe 키로도 사용")
product_name: str = Field(description="상품명")
job_type: str = Field("manual", description="요청 유형(우선순위): new_product|manual|partner|batch")
model: str = Field("", description="모델명")
specification: str = Field("", description="규격(용량/개입/수량 등)")
company: str = Field("", description="제조사/브랜드")
price: str = Field("", description="현재가(참고, 문자열)")
class Req_Search(Req_WebPacketProtocol):
data: list[SearchItem] = Field(description="검색 대상 상품 리스트")
class EnqueuedItem(BaseModel):
product_code: str
job_id: Optional[str] = Field(None, description="적재된 잡 ID. 활성 중복이면 None")
duplicated: bool = Field(False, description="활성 중복(PENDING/RUNNING)이라 스킵됐는지")
class Res_Search(Res_WebPacketProtocol):
accepted: int = Field(0, description="새로 적재된 잡 수(중복 제외)")
items: list[EnqueuedItem] = Field(default_factory=list)
class Res_JobStatus(Res_WebPacketProtocol):
job_id: Optional[str] = None
status: Optional[str] = Field(None, description="JobStatus 이름(PENDING/RUNNING/DONE/DEAD)")
attempts: Optional[int] = None
max_attempts: Optional[int] = None
output: Optional[dict] = Field(None, description="잡 결과(완료 시). 봉투 result 와 구분")
last_error: Optional[str] = None
class Res_QueueStats(Res_WebPacketProtocol):
counts: dict[str, int] = Field(default_factory=dict, description="상태별 잡 개수")
class PricePoint(BaseModel):
triggered_at: str = Field(description="관측 시각(X축)")
outcome: str
matched_count: Optional[int] = None
naver: Optional[int] = Field(None, description="네이버 최저가")
coupang: Optional[int] = Field(None, description="쿠팡 최저가")
final: Optional[int] = Field(None, description="전체 최저가(Y축)")
final_source: Optional[str] = None
naver_name: Optional[str] = None
naver_url: Optional[str] = None
coupang_name: Optional[str] = None
coupang_url: Optional[str] = None
by_mall: Optional[list[dict]] = Field(None, description="몰별 최저가 스냅샷(G마켓·옥션·11번가 등 포함)")
class Res_PriceHistory(Res_WebPacketProtocol):
product_code: Optional[str] = None
points: list[PricePoint] = Field(default_factory=list, description="시각 오름차순 스냅샷(그래프용)")