/** * 구조화 데이터 ↔ 화면 값 대조. **★ 절대규칙 3 을 코드로 강제하는 자리다.** * * JSON-LD 는 AI 검색이 읽는 값이고 화면은 사람이 읽는 값이다. 둘이 다르면 * '검색엔진에만 다른 말을 하는' 상태가 된다 — 클로킹으로 취급될 수 있고, 무엇보다 거짓이다. * * ★ 왜 여기(렌더러 안)에 있나 * 예전에는 백엔드가 **자기만의 HTML 을 따로 구워** 그걸 대조했다. 방문자가 보는 페이지는 * 이 렌더러가 굽는데, 검사는 아무도 안 보는 페이지에 대고 하고 있었다 — 두 렌더러가 * 어긋나는 순간 게이트는 통과인데 실제 페이지는 다른 내용이 된다. * 검사는 **나갈 바로 그 HTML** 에 대고 해야 의미가 있다. * * ★ 검사 방식 * JSON-LD 의 모든 스칼라 값이 화면 텍스트(또는 src/href)에 나타나는지 본다. * 나타나지 않으면 "화면에 없는 것을 구조화 데이터가 주장하고 있다" 는 뜻이고, 그게 거짓이다. */ import type {Json} from './jsonld'; /** 화면에 텍스트로 나타나지 않는 구조·메타 값. 대조 대상에서 제외한다. */ const STRUCTURAL = new Set([ '@context', '@type', '@id', 'priceCurrency', // ★ ㎡의 ISO 코드('MTK'). priceCurrency 와 같은 성격이라 화면에 나올 값이 아니다 — // 빠져 있어서 객실 면적(room_size)이 있는 사업장은 전부 발행이 막혔다(실측: 가은채 객실 12개). // ★ 사람이 읽는 단위 표기(`unitText`)는 여기 넣지 않는다 — 그건 화면에 있어야 하는 말이다. 'unitCode', // ★ units.length 로 만든 파생 카운트다. 화면에는 객실 카드가 그 개수만큼 있을 뿐 // '12' 라는 숫자가 글자로 있지는 않다. 객실이 2~3개인 사업장만 우연히 통과했다. 'numberOfRooms', 'addressCountry', 'inLanguage', // 좌표는 지도 핀용 메타지 본문에 쓸 값이 아니다. 대신 payload 와 직접 대조한다(verifyGeo). 'latitude', 'longitude', ]); /** 이름과 값을 따로 대조하는 서브트리. 일반 순회에서는 건너뛴다. */ const SPECIAL_SUBTREE = new Set(['amenityFeature', 'makesOffer', 'geo']); /** * **페이지를 설명하는** 노드. 본문 대조 대상이 아니다. * * ★ 이게 왜 필요한가 * WebPage 의 name·description·datePublished 는 `