From 43ec44b9c3eaf3e5f3edba9b49cd17d503915342 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A1=B0=EC=9E=AC=EC=A4=91?= <126754298+m-a-king@users.noreply.github.com> Date: Tue, 25 Aug 2026 14:48:32 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20OG=20=EA=B0=80=EA=B2=A9=EC=9D=84=20?= =?UTF-8?q?=EA=B5=AC=ED=98=95=20og:price=20=EB=84=A4=EC=9E=84=EC=8A=A4?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=8A=A4=EC=97=90=EC=84=9C=EB=8F=84=20?= =?UTF-8?q?=EC=9D=BD=EB=8A=94=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Shopify 계열은 표준 product:price:* 대신 구형 og:price:* 만 내보내 OG 경로가 가격을 놓쳤고, 최상위 노드가 ProductGroup 이라 JSON-LD 경로도 함께 미스했다. 두 경로가 다 미스해 매 요청이 Gemini fallback 으로 가면서 같은 사이트의 상품명이 요청마다 갈렸다 (prod 토너먼트 1350 의 20건 전부 SERVER_LLM) - 표준 태그를 먼저 보고 없을 때만 구형으로 폴백한다. amount 와 currency 를 각각 폴백해, 통화 태그만 단독으로 있는 페이지를 no_data 아닌 missing_field 로 가르는 기존 규칙이 구형 네임스페이스에서도 유지된다 - JSON-LD 의 ProductGroup 인식 추가는 택하지 않았다 - 지원하면 JSON-LD 가 우선순위상 먼저 이겨 이름이 색상 없는 한글로 바뀌고, 같은 상품의 다른 색상이 한 토너먼트에 올라왔을 때 구분이 사라진다 - og:price:amount 의 천단위 콤마는 기존 PRICE_NOISE 가 이미 제거해 추가 처리를 넣지 않았다. 통화는 extractor 가 보내는 Accept-Language 헤더로 KRW 가 정상 수신되는 것을 실측해 회귀 위험이 없음을 확인했다 --- .../structured/StructuredDataExtractor.java | 17 +++++++-- .../StructuredDataExtractorTest.java | 36 +++++++++++++++++++ 2 files changed, 50 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/depromeet/piki/extractor/extraction/structured/StructuredDataExtractor.java b/src/main/java/com/depromeet/piki/extractor/extraction/structured/StructuredDataExtractor.java index ee88019..1783494 100644 --- a/src/main/java/com/depromeet/piki/extractor/extraction/structured/StructuredDataExtractor.java +++ b/src/main/java/com/depromeet/piki/extractor/extraction/structured/StructuredDataExtractor.java @@ -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); } @@ -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) { diff --git a/src/test/java/com/depromeet/piki/extractor/extraction/structured/StructuredDataExtractorTest.java b/src/test/java/com/depromeet/piki/extractor/extraction/structured/StructuredDataExtractorTest.java index 0171533..17d9de0 100644 --- a/src/test/java/com/depromeet/piki/extractor/extraction/structured/StructuredDataExtractorTest.java +++ b/src/test/java/com/depromeet/piki/extractor/extraction/structured/StructuredDataExtractorTest.java @@ -301,6 +301,42 @@ void extractsFromOpenGraph() { assertEquals("KRW", snapshot.currency()); } + @Test + @DisplayName("표준 가격 태그가 없고 구형 og_price 네임스페이스만 있으면 콤마 낀 가격을 뽑는다") + void extractsFromLegacyOgPriceNamespace() { + String 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 = """ + + + + + + + """; + + 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() {