feat(lps): 네이버 몰별 최저가 분해(by_mall) — 오픈마켓 가시화
네이버 오픈API 결과엔 G마켓·옥션·11번가 등이 이미 mallName 으로 들어오는데 최저가 1건만 쓰고 몰 정보를 버리고 있었다. summarize_by_mall() 로 매칭 후보를 (소스, 판매몰)별 최저가로 축약해 result.by_mall(가격 오름차순)에 노출한다. 추가 요청 0건으로 오픈마켓 몰별 최저가를 확보 — 별도 크롤러 불필요. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
4863425ca5
commit
313d882df4
@ -51,6 +51,25 @@ def apply_filters(
|
||||
return cur, stages
|
||||
|
||||
|
||||
def summarize_by_mall(products: list[NormalizedProduct]) -> list[dict]:
|
||||
"""같은상품 매칭 후보를 (소스, 판매몰)별 최저가로 분해한다.
|
||||
네이버 결과엔 G마켓·옥션·11번가 등이 mall_name 으로 이미 들어오므로,
|
||||
추가 요청 없이 '오픈마켓 몰별 최저가'를 노출할 수 있다(가격 오름차순).
|
||||
네이버 가격비교(mall_name='네이버')는 여러 판매자 중 최저가 롤업이다."""
|
||||
best: dict[tuple[str, str], NormalizedProduct] = {}
|
||||
for p in products:
|
||||
key = (p.source, (p.mall_name or "기타"))
|
||||
cur = best.get(key)
|
||||
if cur is None or p.price < cur.price:
|
||||
best[key] = p
|
||||
rows = sorted(best.values(), key=lambda p: p.price)
|
||||
return [{
|
||||
"source": p.source, "mall_name": p.mall_name, "price": p.price,
|
||||
"shipping_fee": p.shipping_fee, "shipping_type": p.shipping_type,
|
||||
"name": p.name, "detail_url": p.detail_url,
|
||||
} for p in rows]
|
||||
|
||||
|
||||
def rank_result(products: list[NormalizedProduct], total_found: int, stages: list[dict], top_n: int = 5) -> dict:
|
||||
"""최저가순 정렬 + top-N + 결과 봉투 조립(top_n STAGE 포함)."""
|
||||
ranked = sorted(products, key=lambda p: p.price)
|
||||
@ -61,6 +80,7 @@ def rank_result(products: list[NormalizedProduct], total_found: int, stages: lis
|
||||
"kept": len(ranked),
|
||||
"lowest": top[0].model_dump() if top else None,
|
||||
"top": [p.model_dump() for p in top],
|
||||
"by_mall": summarize_by_mall(ranked),
|
||||
"stages": stages,
|
||||
}
|
||||
|
||||
|
||||
@ -6,7 +6,7 @@ from services.search.contract import NormalizedProduct
|
||||
from services.search.coupang.parser import parse_search_html
|
||||
from services.pipeline.filters import filter_by_price_band, filter_out_malls
|
||||
from services.pipeline.outliers import remove_price_outliers
|
||||
from services.pipeline.core import run_price_pipeline
|
||||
from services.pipeline.core import run_price_pipeline, summarize_by_mall
|
||||
|
||||
FIXTURE = Path(__file__).parent / "fixtures" / "coupang_search.html"
|
||||
|
||||
@ -56,6 +56,30 @@ def test_stages_recorded():
|
||||
assert r["stages"][-1]["out"] == 2 # top_n 결과 수
|
||||
|
||||
|
||||
def _pm(source, mall, price):
|
||||
return NormalizedProduct(source=source, name=f"{mall}상품", price=price, mall_name=mall)
|
||||
|
||||
|
||||
def test_summarize_by_mall_lowest_per_mall_sorted():
|
||||
# 같은 몰 여러 건 → 몰별 최저가만, 전체 가격 오름차순
|
||||
prods = [
|
||||
_pm("naver", "G마켓", 12000), _pm("naver", "G마켓", 11000),
|
||||
_pm("naver", "11번가", 10500), _pm("naver", "네이버", 9800),
|
||||
_pm("coupang", "쿠팡", 10200),
|
||||
]
|
||||
rows = summarize_by_mall(prods)
|
||||
# 몰별 최저가 1건씩, 전체 가격 오름차순
|
||||
assert [(r["mall_name"], r["price"]) for r in rows] == [
|
||||
("네이버", 9800), ("쿠팡", 10200), ("11번가", 10500), ("G마켓", 11000),
|
||||
]
|
||||
|
||||
|
||||
def test_summarize_by_mall_in_result():
|
||||
r = run_price_pipeline([_pm("naver", "11번가", 5000), _pm("naver", "G마켓", 6000)], remove_outliers=False)
|
||||
malls = {row["mall_name"] for row in r["by_mall"]}
|
||||
assert malls == {"11번가", "G마켓"}
|
||||
|
||||
|
||||
def test_pipeline_on_real_fixture():
|
||||
products = parse_search_html(FIXTURE.read_text())
|
||||
r = run_price_pipeline(products, remove_outliers=False, top_n=3)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user