"""네이버 쇼핑 API 응답 items → NormalizedProduct (순수 함수). 네트워크 무관 — 저장된/모의 JSON 으로 결정론적 테스트 가능. title 의 강조 태그·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