o2o-negosium-original/lps/services/search/naver/transform.py
민헌 61bc719e28 feat(lps): 배송비 유형 분류 — 쿠팡 배송신호 파싱 + 네이버 배송비제외 명시
NormalizedProduct 에 shipping_type(rocket|rocket_merchant|free|paid|None) 추가.
쿠팡 파서: 로켓 뱃지(img src)로 유형, '무료배송'/'배송비 X원' 텍스트로 금액 판별
(상품명 '무료배송' 오탐은 이름 제거 후 매칭). 네이버 lprice 는 배송비 제외
상품가라 배송 필드 None — 카탈로그 '배송비포함 최저가'와 다른 이유를 docs/api.md 에 명시.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 13:59:28 +09:00

53 lines
1.9 KiB
Python

"""네이버 쇼핑 API 응답 items → NormalizedProduct (순수 함수).
네트워크 무관 — 저장된/모의 JSON 으로 결정론적 테스트 가능.
title 의 <b> 강조 태그·HTML 엔티티를 제거하고, 가격비교(catalog) 페이지는 제외한다.
"""
import html
import re
from services.search.contract import NormalizedProduct
_TAG = re.compile(r"<[^>]+>")
def _clean(text: str) -> str:
return html.unescape(_TAG.sub("", text or "")).strip()
def transform_items(items: list[dict], source: str = "naver") -> list[NormalizedProduct]:
products: list[NormalizedProduct] = []
for it in items:
link = it.get("link", "") or ""
# 가격비교(catalog, productType=1) 페이지의 lprice 는 '여러 판매자 중 최저가'라
# 최저가 솔루션에는 오히려 핵심 신호 → 제외하지 않고 그대로 취한다.
# 단, lprice 는 **배송비 제외** 상품가(API 에 배송비 필드 없음) — 카탈로그 화면의
# '배송비포함 최저가'와 다를 수 있다. 카탈로그 크롤링은 WTM 캡차로 차단됨(2026-07 스파이크)
# → shipping_fee/shipping_type 은 None(미확인)으로 남긴다.
try:
price = int(it.get("lprice")) # lprice = 최저가
except (TypeError, ValueError):
continue
if price <= 0:
continue
name = _clean(it.get("title"))
if not name:
continue
products.append(
NormalizedProduct(
source=source,
name=name,
price=price,
image_url=it.get("image") or None,
detail_url=link or None,
mall_name=it.get("mallName") or None,
manufacturer=(it.get("maker") or it.get("brand")) or None,
external_id=str(it["productId"]) if it.get("productId") else None,
)
)
return products