Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -257,11 +257,12 @@ private record ResolvedPrice(String price, String currency) {
/**
* OG 가 이름·이미지는 주지만 가격을 JS state(window.__PRELOADED_STATE__ 등)에만 둔 SPA(예: 유니클로)를
* LLM 없이 추출하기 위한 특화 경로다 — 가격이 거대 state 깊숙이 있어 Gemini fallback 의 토큰 상한에
* 안 맞는 사이트를 파서가 직접 건진다. OG 표준 가격 태그가 있으면 불필요한 state 파싱을 하지 않는다.
* 안 맞는 사이트를 파서가 직접 건진다. OG 가격 태그가 있으면 불필요한 state 파싱을 하지 않는다.
*/
private ResolvedPrice resolvePrice(Document document) {
String ogCurrency = metaContent(document, "product:price:currency");
String ogAmount = metaContent(document, "product:price:amount");
// 구형 OG product 네임스페이스(og:price:*)만 내보내는 몰(Shopify 계열)이 있어 표준 태그 뒤에 폴백으로 둔다.
String ogCurrency = firstMetaContent(document, "product:price:currency", "og:price:currency");
String ogAmount = firstMetaContent(document, "product:price:amount", "og:price:amount");
if (ogAmount != null) {
return new ResolvedPrice(ogAmount, ogCurrency);
}
Expand Down Expand Up @@ -374,6 +375,16 @@ private JsonNode findPricesNode(JsonNode node) {
return null;
}

private String firstMetaContent(Document document, String... properties) {
for (String property : properties) {
String content = metaContent(document, property);
if (content != null) {
return content;
}
}
return null;
}

private String metaContent(Document document, String property) {
Element meta = document.selectFirst("meta[property=" + property + "]");
if (meta == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,42 @@ void extractsFromOpenGraph() {
assertEquals("KRW", snapshot.currency());
}

@Test
@DisplayName("표준 가격 태그가 없고 구형 og_price 네임스페이스만 있으면 콤마 낀 가격을 뽑는다")
void extractsFromLegacyOgPriceNamespace() {
String html = """
<html><head>
<meta property="og:title" content="Stream Pants (Archive) - Charcoal"/>
<meta property="og:image" content="https://cdn.example.com/pants.jpg"/>
<meta property="og:price:amount" content="680,000"/>
<meta property="og:price:currency" content="KRW"/>
</head><body></body></html>""";

ProductSnapshot snapshot = snapshotOrNull(extractor.extract(pageOf(html)));

assertEquals("Stream Pants (Archive) - Charcoal", snapshot.name());
assertEquals(680_000, snapshot.currentPrice());
assertEquals("KRW", snapshot.currency());
}

@Test
@DisplayName("표준 product_price 태그와 구형 og_price 가 함께 있으면 표준을 쓴다")
void prefersStandardPriceTagOverLegacyNamespace() {
String html = """
<html><head>
<meta property="og:title" content="두네임스페이스"/>
<meta property="product:price:amount" content="45000"/>
<meta property="product:price:currency" content="KRW"/>
<meta property="og:price:amount" content="99999"/>
<meta property="og:price:currency" content="JPY"/>
</head><body></body></html>""";

ProductSnapshot snapshot = snapshotOrNull(extractor.extract(pageOf(html)));

assertEquals(45_000, snapshot.currentPrice());
assertEquals("KRW", snapshot.currency());
}

@Test
@DisplayName("og 에 title 만 있고 가격이 없으면 missing_field 로 fallback 한다")
void openGraphTitleOnlyFallsBackToMissingField() {
Expand Down
Loading