@zalkera/client 0.9.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -0
- package/contracts/aeo-surface-guarantees.json +189 -0
- package/dist/index.cjs +15 -13
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +32 -1
- package/dist/index.d.ts +32 -1
- package/dist/index.js +15 -13
- package/dist/index.js.map +1 -1
- package/llms.txt +44 -3
- package/package.json +5 -3
package/README.md
CHANGED
|
@@ -59,6 +59,16 @@ await zalkera.submitInquiry({name, email, subject, message}, {clientIp});
|
|
|
59
59
|
- 설치 후 경로: `node_modules/@zalkera/client/llms.txt`.
|
|
60
60
|
- 사용법: 이 파일을 AI 컨텍스트로 주고 "상품·장바구니·결제 페이지를 만들어줘".
|
|
61
61
|
|
|
62
|
+
## AEO 보장표 (`contracts/aeo-surface-guarantees.json`)
|
|
63
|
+
|
|
64
|
+
- `llms.txt` §5.1 이 말하는 산출물 규범의 **기계 판독본**. 설치 후 경로:
|
|
65
|
+
`node_modules/@zalkera/client/contracts/aeo-surface-guarantees.json`
|
|
66
|
+
패키지 하위 경로로도 열린다 — `require("@zalkera/client/contracts/aeo-surface-guarantees.json")`,
|
|
67
|
+
ESM 은 `import … with { type: "json" }`(실측 확인).
|
|
68
|
+
- 이 표를 읽어 **개시된 사이트를 크롤해** 판정하는 검사기가 스토어프론트 템플릿의
|
|
69
|
+
`scripts/check-aeo-surfaces.mjs` 다. 소스가 아니라 산출물을 재므로 스택·디자인을 가리지 않는다.
|
|
70
|
+
- 정본은 잘커라 백엔드에 있고 여기 실린 것은 그 **발행 산출물**이다(발행 전 기계 대조).
|
|
71
|
+
|
|
62
72
|
## API 스펙 (메서드)
|
|
63
73
|
|
|
64
74
|
### 콘텐츠·공개(무인증)
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$comment": "AEO 보장 표면 계약 정본(memo119 §1.0·§4.2 · T4a). 형제 계약 section-vocabulary.json 과 같은 거처·같은 관례(contractRev·기계 대조·사람 주석은 이식에서 진다)를 따른다. 어휘가 '레이아웃 규격'이 아니라 '유형별 최소 AEO/SEO 보장 계약'이라는 재정의(오너 정정 B)의 기계 형태다. 구조화 데이터에는 생김새가 없어 디자인 자유를 하나도 깎지 않고 바닥만 깔 수 있다 — 이 파일이 그 바닥이다.",
|
|
3
|
+
"contractRev": 1,
|
|
4
|
+
|
|
5
|
+
"conventions": {
|
|
6
|
+
"canonicalHere": "이 파일이 정본이다. zalkera-client/llms.txt §5.1 은 같은 계약의 **사람/AI용 운반체**이고, 대조는 zalkera-client/scripts/sync-aeo-guarantees.mjs 가 기계로 센다. 채널이 늘어도 사본을 늘리지 않는다 — AGENTS.md·콘솔 문서·MCP 는 참조 링크만 든다(memo119 §5-13).",
|
|
7
|
+
"floorNotCeiling": "보장은 바닥이지 천장이 아니다. 이 표를 못 채우는 템플릿도 만들 수 있고 적재될 수 있다 — 못 거는 것은 **그 카테고리 간판**뿐이고, 그것도 결함 판정이 아니라 진열 축의 사실 판정이다(§4.1·오너 정정 A).",
|
|
8
|
+
"whereTheWeightIs": "보장의 무게는 **라우트**에 있다. 현행 보장(Product·Organization·BlogPosting·BreadcrumbList)은 라우트·렌더러의 속성이고, 섹션이 직접 산출하는 것은 FAQPage 하나다. **섹션 어휘를 0개 쓰는 사이트도 라우트 보장은 똑같이 받는다**(§1.0-a).",
|
|
9
|
+
"verdictFromArtifact": "판정 지점은 소스가 아니라 **산출물**이다(오너 정정 C). '이 섹션을 써라'는 자유를 깎지만 '개시된 페이지에서 이 그래프가 나오는가'는 아무것도 깎지 않는다. 검사기는 storefront-template/scripts/check-aeo-surfaces.mjs 이고, 실행 자리는 promote 전 스모크 개시다.",
|
|
10
|
+
"requiredVsPlanned": "`required` 는 **이 rev 가 지금 강제하는 것**이고, `planned` 는 같은 카테고리의 목표 표면 중 **아직 우리 본보기도 못 내는 것**이다. planned 는 게이트가 아니라 T6 작업 지시서이고, 검사기는 이를 실패가 아니라 PLANNED_MISSING 으로 보고한다. `realized` 는 **그 지시가 실제로 이행됐는가**를 적는 별개 축이다 — 이행됐다고 자동으로 required 가 되지는 않는다. required 로 올리는 것은 이 표의 rev 상향이고, 그 시점은 **이행분이 개시된 산출물에 실제로 나타나는 발행 단위**다 — 코드 병합이 아니다(판정 지점이 산출물이므로 잣대의 상향 시점도 산출물이어야 한다. 병합만으로 표를 올리면 아직 재빌드 안 된 팩·구 SDK 로 빌드된 팩이 통과 못 할 요구를 받는다). rev 2 가 그 실례다: `openingHours` 는 저장 자리(백엔드 컬럼)만 섰고 `@zalkera/client` 0.9.0 미발행·템플릿 소비 0 이라, rev 2 는 T6 병합이 아니라 **client 발행 + 본보기 팩 재빌드**가 끝난 단위에서만 설 수 있다. 이 분리가 없으면 둘 중 하나를 해야 한다 — 규범을 낮추거나(보장이 거짓말이 된다), 오늘 아무도 못 거는 표를 강제하거나(카탈로그가 빈 선반이 된다).",
|
|
11
|
+
"growth": "표는 자란다. rev 상향 시 **기존 등재물의 소급 탈락은 금지**다 — 구 rev 판정은 판정 당시 rev 를 병기한 배지로 유효하게 지속되고, 신 rev 요구는 신규 등재·재promote 부터 건다. 순수 라벨 카테고리가 나중에 이 표에 편입되면 기존 등재물은 유예한다(§1.0-c). 조용한 소급은 이 설계가 죽이려던 조용한 실패의 재생산이다.",
|
|
12
|
+
"publication": "`published` 는 그 표면의 규범이 **공개 문서(llms.txt §5.1)에 이미 나가 있는가**다. false = 내부 정본에만 있다 = 아직 공표 전. 내부 정본이 앞서 있는 것은 무해하다(작업 지시서다). 그러나 **공표가 본보기를 앞지르면 안 된다** — '레시피가 본보기를 앞지르지 않는다'(§1.5-a). false→true 뒤집기는 T6 그린과 같은 발행 단위 이후이고(T4b), 대조 스크립트가 양방향으로 센다: published 인데 §5.1 에 없으면 실패, published 가 아닌데 §5.1 에 이미 있으면 **역방향 실패**(공표가 표를 앞질렀다).",
|
|
13
|
+
"categoryStorage": "카테고리는 `theme.category` 자유 문자열이다 — enum 으로 못박지 않는다(memo119 §5-6). **여기 항목이 있는 이름만** 게이트가 걸리고, 없는 이름은 순수 라벨이다(예: '미니멀'). 값 이름을 업종어가 아니라 유형어로 두는 이유는 형제 계약의 명명 규약과 같다: 업종어를 박으면 재사용이 죽는다(DOCTOR_INTRO 교훈). 그래서 예약 칸은 `BEAUTY` 가 아니라 `BOOKING` 이다 — 예약을 파는 것은 뷰티만이 아니다."
|
|
14
|
+
},
|
|
15
|
+
|
|
16
|
+
"routes": {
|
|
17
|
+
"$comment": "표면이 가리키는 라우트 어휘. LITERAL 은 그 경로 자체가 있어야 하고, INSTANCE 는 그 패턴에 맞는 **실물 한 건**을 크롤에서 찾아 검사한다(패턴 자체는 응답하지 않는다). 없는 라우트는 red 이지 스킵이 아니다 — 라우트 부재가 곧 보장 부재다.",
|
|
18
|
+
"$notASurface": "`/policies` 는 여기 없다. 그 페이지는 JSON-LD 를 **0건** 낸다 — `parsePolicies`(정책 문구 파서)만 import 할 뿐 `<JsonLd>` 를 렌더하지 않는다. 파일명 grep 이 `JsonLd` 를 문자열로 잡아 \"산출한다\"로 잘못 읽히는 자리라 명시해 둔다(실제로 그 오독이 한 번 났다). `PostalAddress` 의 거처는 `/` 의 `organizationJsonLd` 안이고 그것도 address 가 설정됐을 때만 나간다.",
|
|
19
|
+
"home": {"kind": "LITERAL", "path": "/"},
|
|
20
|
+
"cmsPage": {"kind": "INSTANCE", "pattern": "/{slug}", "note": "콘솔·시드가 만든 고정 페이지(src/app/[slug]/page.tsx). 예약 세그먼트(products·blog·c·policies·contact 등)는 인스턴스 후보에서 뺀다."},
|
|
21
|
+
"productList": {"kind": "LITERAL", "path": "/products", "note": "**T6-ⓑ 가 신설했다**(src/app/products/page.tsx · ItemList+BreadcrumbList · sitemap 등재). 레시피는 그 전부터 있었다 — llms.txt §4.1 이 이 라우트를 그린다. 개시된 사이트에 나타나는 것은 템플릿 병합 뒤 **그 팩을 다시 빌드·개시한 다음**이므로, 낡은 산출물을 크롤하면 여전히 부재로 잡힌다 — 그건 라우트 부재가 아니라 산출물이 낡은 것이다."},
|
|
22
|
+
"productDetail": {"kind": "INSTANCE", "pattern": "/products/{slug}"},
|
|
23
|
+
"productCategory": {"kind": "INSTANCE", "pattern": "/c/{slug}", "note": "**오늘 부재. 그런데 막는 것은 라우트가 아니라 데이터층이다**(2026-07-28 실측 · 설계 §0-6 에 없던 사실). 경로는 발명하지 않았다 — llms.txt §4.1 레시피가 카테고리 링크를 `/c/{slug}` 로 이미 그리고 있다. 진짜 선행은 아래 `product-category-collectionpage` 를 보라: `product_category_map` 에 **행을 넣는 경로가 전 레포 0건**이라 라우트를 지어도 어느 카테고리든 상품 0건이 된다."},
|
|
24
|
+
"blogList": {"kind": "LITERAL", "path": "/blog"},
|
|
25
|
+
"blogDetail": {"kind": "INSTANCE", "pattern": "/blog/{slug}"}
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
"siteWide": {
|
|
29
|
+
"$comment": "카테고리 무관 최소 기계 가독. 라우트별 그래프가 아무리 훌륭해도 크롤러가 목록을 못 받으면 발견되지 않는다.",
|
|
30
|
+
"requirements": [
|
|
31
|
+
{
|
|
32
|
+
"id": "robots",
|
|
33
|
+
"check": "ROBOTS_PRESENT",
|
|
34
|
+
"published": true,
|
|
35
|
+
"llmsMarker": "`sitemap.ts`·`robots.ts` 필수",
|
|
36
|
+
"why": "공개 카탈로그를 열고 세션·쓰기 경로만 막는다. **AI 크롤러(GPTBot·ClaudeBot 등)를 막지 않는다** — 발견 경로를 우리 손으로 닫는 일이다."
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"id": "sitemap",
|
|
40
|
+
"check": "SITEMAP_PRESENT",
|
|
41
|
+
"published": true,
|
|
42
|
+
"llmsMarker": "`sitemap.ts`·`robots.ts` 필수",
|
|
43
|
+
"why": "존재하는 공개 라우트만 싣는다."
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"id": "sitemap-covers-required-routes",
|
|
47
|
+
"check": "SITEMAP_COVERS",
|
|
48
|
+
"published": false,
|
|
49
|
+
"publishTranche": "T4b",
|
|
50
|
+
"llmsMarker": "sitemap 은 그 카테고리의 보장 라우트를 전부 싣는다",
|
|
51
|
+
"realized": "PARTIAL",
|
|
52
|
+
"realizedNote": "T6 이 /products 를 sitemap 에 등재했다. /blog 목록은 아직이고 카테고리는 라우트 자체가 없다 — 그래서 절반이다.",
|
|
53
|
+
"why": "현행 §5.1 의 sitemap 지시는 '홈·상품 상세·콘텐츠'라 **목록·카테고리를 모른다**(§0-11 ⓐ). 목록 라우트가 생겨도 sitemap 이 모르면 보장이 반쪽이다 — 쇼핑몰 칸이 못 서는 균열과 같은 뿌리다. 표에는 지금 넣고(작업 지시서), 공표는 본보기가 실제로 그렇게 된 뒤다."
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
"id": "absolute-urls",
|
|
57
|
+
"check": "ABSOLUTE_URLS_IN_JSONLD",
|
|
58
|
+
"published": true,
|
|
59
|
+
"llmsMarker": "절대 URL",
|
|
60
|
+
"why": "JSON-LD 의 url·item 이 상대경로면 크롤러가 해석하지 못한다."
|
|
61
|
+
}
|
|
62
|
+
]
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
"negative": {
|
|
66
|
+
"$comment": "**의도적 부정 보장** — 빠뜨린 것이 아니라 결정이다. 검사기가 이것들을 '누락'으로 잡으면 안 되고, 오히려 **나타나면 실패**로 잡아야 한다.",
|
|
67
|
+
"forbiddenTypes": [
|
|
68
|
+
{
|
|
69
|
+
"type": "Review",
|
|
70
|
+
"why": "자사 사이트의 자사 후기에 별점을 붙이는 것은 self-serving reviews 정책 위반이라 제재 대상이다(memo102 §2.3). TESTIMONIALS 섹션이 jsonLd 를 안 내는 이유가 이것이다 — 어휘 12종 중 jsonLd 비-null 이 FAQ_LIST 하나뿐인 것은 미완성이 아니라 판단이다."
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"type": "AggregateRating",
|
|
74
|
+
"scope": "ROOT_ONLY",
|
|
75
|
+
"why": "Product 안에 중첩된 AggregateRating 은 실제 후기 집계라 정상이다(productJsonLd 가 reviewCount>0 일 때만 낸다). 금지 대상은 **루트 노드로 선 AggregateRating** — 상품에 붙지 않은 별점은 자사 별점 선언이다."
|
|
76
|
+
}
|
|
77
|
+
]
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
"categories": [
|
|
81
|
+
{
|
|
82
|
+
"category": "MARKETING",
|
|
83
|
+
"label": "비즈니스 홍보",
|
|
84
|
+
"shipState": "SHIPPABLE",
|
|
85
|
+
"required": [
|
|
86
|
+
{"id": "home-organization", "route": "home", "jsonLd": ["Organization", "LocalBusiness", "BeautySalon"], "ssr": true, "published": true, "llmsMarker": "홈 = `Organization`", "why": "사이트 주체가 누구인지 기계가 읽는 자리. 홈에 1회만 낸다."},
|
|
87
|
+
{"id": "cms-page-ssr", "route": "cmsPage", "jsonLd": [], "ssr": true, "published": true, "llmsMarker": "**ISR 유지.**", "why": "고정 페이지 본문이 JS 실행 없이 응답 본문에 실려야 한다. 이것이 없으면 그래프를 아무리 잘 내도 답변 엔진이 인용할 본문이 없다."}
|
|
88
|
+
],
|
|
89
|
+
"planned": [
|
|
90
|
+
{"id": "cms-page-webpage", "route": "cmsPage", "jsonLd": ["WebPage"], "tranche": "T6-ⓐ", "realized": "DONE", "realizedNote": "T6 이 CMS 라우트에 WebPage+BreadcrumbList 를 얹었다(0건→2종).", "why": "CMS 서브페이지의 JSON-LD 는 오늘 0건이다(src/app/[slug]/page.tsx 에 JsonLd 호출 없음). 홈만 그래프를 갖고 서브페이지가 비어 있는 것이 마케팅 사이트의 실제 구멍이다."},
|
|
91
|
+
{"id": "cms-page-breadcrumb", "route": "cmsPage", "jsonLd": ["BreadcrumbList"], "tranche": "T6-ⓐ", "realized": "DONE", "realizedNote": "T6-ⓐ 와 같은 커밋.", "why": "§4.2 가 이 칸의 목표 표면에 BreadcrumbList 를 넣었는데, 오늘 BreadcrumbList 는 상품·블로그 상세에만 있다. 순수 마케팅 사이트에는 그 두 라우트가 없으므로 CMS 페이지가 유일한 거처다."}
|
|
92
|
+
],
|
|
93
|
+
"why": "표면이 전부 홈·고정 페이지라 오늘의 렌더러로 성립한다. 시드도 오늘 이 칸을 채울 수 있다(page·page_section·menu·theme_colors)."
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
"category": "PORTFOLIO",
|
|
97
|
+
"label": "포트폴리오",
|
|
98
|
+
"shipState": "SHIPPABLE",
|
|
99
|
+
"sameAs": "MARKETING",
|
|
100
|
+
"required": [
|
|
101
|
+
{"id": "home-organization", "route": "home", "jsonLd": ["Organization", "LocalBusiness", "BeautySalon"], "ssr": true, "published": true, "llmsMarker": "홈 = `Organization`", "why": "MARKETING 과 같다."},
|
|
102
|
+
{"id": "cms-page-ssr", "route": "cmsPage", "jsonLd": [], "ssr": true, "published": true, "llmsMarker": "**ISR 유지.**", "why": "MARKETING 과 같다."}
|
|
103
|
+
],
|
|
104
|
+
"planned": [
|
|
105
|
+
{"id": "cms-page-webpage", "route": "cmsPage", "jsonLd": ["WebPage"], "tranche": "T6-ⓐ", "why": "MARKETING 과 같다."},
|
|
106
|
+
{"id": "cms-page-breadcrumb", "route": "cmsPage", "jsonLd": ["BreadcrumbList"], "tranche": "T6-ⓐ", "why": "MARKETING 과 같다."}
|
|
107
|
+
],
|
|
108
|
+
"why": "보장 표면이 MARKETING 과 같다. 그런데도 별 항목으로 두는 이유는 **진열 라벨이 다르기 때문**이다 — 표면이 같다고 칸을 합치면 카탈로그 탐색에서 '포트폴리오'를 찾는 사람이 못 찾는다. 같은 표면을 두 번 적는 비용은 대조 스크립트가 감당한다."
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
"category": "EVENT",
|
|
112
|
+
"label": "이벤트·프로젝트",
|
|
113
|
+
"shipState": "SHIPPABLE",
|
|
114
|
+
"required": [
|
|
115
|
+
{"id": "home-organization", "route": "home", "jsonLd": ["Organization", "LocalBusiness", "BeautySalon"], "ssr": true, "published": true, "llmsMarker": "홈 = `Organization`", "why": "MARKETING 과 같다."},
|
|
116
|
+
{"id": "cms-page-ssr", "route": "cmsPage", "jsonLd": [], "ssr": true, "published": true, "llmsMarker": "**ISR 유지.**", "why": "MARKETING 과 같다."}
|
|
117
|
+
],
|
|
118
|
+
"conditional": [
|
|
119
|
+
{"id": "faq-page", "route": "any", "jsonLd": ["FAQPage"], "when": "SECTION_PRESENT", "section": "FAQ_LIST", "published": false, "publishTranche": "T4b", "why": "**어휘 12종 중 유일하게 섹션이 직접 산출하는 그래프**(FaqListSection.tsx:36). 조건부인 이유: 크롤한 HTML 만 보고는 'FAQ_LIST 를 안 썼다'와 'FAQ_LIST 를 썼는데 그래프가 안 나온다'를 구분할 수 없다. 그래서 검사기는 FAQPage 가 보이면 통과로, 안 보이면 SKIPPED 로 적고 **실패로 세지 않는다** — 못 세는 것을 센 척하지 않는다."}
|
|
120
|
+
],
|
|
121
|
+
"planned": [
|
|
122
|
+
{"id": "cms-page-webpage", "route": "cmsPage", "jsonLd": ["WebPage"], "tranche": "T6-ⓐ", "why": "MARKETING 과 같다."},
|
|
123
|
+
{"id": "cms-page-breadcrumb", "route": "cmsPage", "jsonLd": ["BreadcrumbList"], "tranche": "T6-ⓐ", "why": "MARKETING 과 같다."}
|
|
124
|
+
],
|
|
125
|
+
"why": "MARKETING 표면 + FAQ 섹션을 쓸 때의 FAQPage."
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
"category": "BOOKING",
|
|
129
|
+
"label": "예약",
|
|
130
|
+
"shipState": "SHIPPABLE",
|
|
131
|
+
"shipNote": "**축소 보장표로 등재된 상태다**(§4.2). ItemList·openingHours 를 지금 required 로 올리면 오늘 아무 팩도 이 칸을 못 걸고, 낮춰 적으면 보장이 거짓이 된다. 그래서 지금 참인 것만 강제하고 나머지는 planned 로 rev 관리한다 — rev 2 에서 required 로 올라간다. **그 rev 2 는 T6 병합 시점이 아니다**: openingHours 는 저장 자리만 서 있고 `@zalkera/client` 0.9.0(타입 추가)이 미발행이라 템플릿이 그 값을 소비하지 못해 그래프에 아무것도 안 나간다. rev 2 가 서는 단위는 **client 발행 + 본보기 팩 재빌드**다(conventions.requiredVsPlanned).",
|
|
132
|
+
"required": [
|
|
133
|
+
{"id": "home-localbusiness", "route": "home", "jsonLd": ["LocalBusiness", "BeautySalon", "HealthAndBeautyBusiness"], "ssr": true, "published": false, "publishTranche": "T4b", "why": "예약을 파는 곳은 **물리 점포**다. Organization 으로 두면 '어디로 가면 되는가'가 기계에 안 읽힌다. organizationJsonLd 는 config.businessType==BEAUTY 일 때 BeautySalon 으로 좁힌다 — 즉 이 요구는 렌더러가 아니라 **테넌트 설정**이 만족시킨다."},
|
|
134
|
+
{"id": "service-detail-product", "route": "productDetail", "jsonLd": ["Product"], "requiredChildren": ["Offer"], "ssr": true, "published": true, "llmsMarker": "상품 상세 = `Product` + variant 마다 `Offer`", "why": "시술 한 건이 상품 한 행이다. 가격이 Offer 로 나가야 '얼마인가'가 답변 엔진에 읽힌다. **이 표면은 시드가 상품을 만들 수 있어야 빈 약속이 아니다**(T1 의 products 시드가 전제)."}
|
|
135
|
+
],
|
|
136
|
+
"planned": [
|
|
137
|
+
{"id": "service-menu-itemlist", "route": "any", "jsonLd": ["ItemList"], "tranche": "T6-ⓒ", "realized": "DONE", "realizedNote": "T6 이 SERVICE_MENU 의 ItemList 산출을 구현했고 정본 section-vocabulary.json 의 jsonLd 열이 null→ItemList(contractRev 2)로 함께 올라갔다. 여기서 required 로 올리는 것은 이 표의 **rev 2** 이고, 그 단위는 T6 병합이 아니라 **본보기 팩을 다시 빌드해 개시한 뒤**다 — 같은 rev 의 openingHours 는 거기에 더해 client 발행까지 필요하다.", "why": "시술 목록의 정위치는 **별도 라우트가 아니라 홈의 SERVICE_MENU 섹션**이다(FAQ_LIST↔FAQPage 선례). 뷰티 사이트에 /products 목록 라우트를 강제하는 것은 과잉이라 비대칭을 이렇게 해소했다(§4.2 W-F5). 구현 시 section-vocabulary.json 의 SERVICE_MENU jsonLd 열이 null→\"ItemList\" 로 올라가고 그 rev 가 동반된다."},
|
|
138
|
+
{"id": "opening-hours", "route": "home", "jsonLdProperty": "openingHours", "tranche": "T6-ⓓ", "realized": "PARTIAL", "realizedNote": "**절반이다.** 저장 자리는 섰다(백엔드 site_config 확장 + 전체교체 경로 4곳). 템플릿 렌더는 @zalkera/client 0.9.0 발행 뒤라 아직 못 나간다(발행은 2FA 라 오너 몫). 저장 자리만으로는 그래프가 안 나가므로 이 항목이 여전히 red 로 잡히는 것이 맞다.", "why": "**전 레포에 저장할 필드 자체가 없다** — 렌더러 문제가 아니라 스키마 문제다. 거처는 예약 척추 위저드의 영업시간 입력(§2.4)이고, site_config 확장 마이그레이션이 선행한다."}
|
|
139
|
+
],
|
|
140
|
+
"why": "§4.2 예약 칸. 상세는 지금 나가고, 목록(ItemList)·영업시간은 T6 이후다."
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
"category": "COMMERCE",
|
|
144
|
+
"label": "쇼핑몰",
|
|
145
|
+
"shipState": "BLOCKED",
|
|
146
|
+
"blockedReason": "**DON'T-SHIP**(memo119 §5-11). 없는 표면을 카탈로그 간판으로 거는 것은 빈 선반이다.",
|
|
147
|
+
"unblockGate": "**정정(2026-07-28) — 'T6-ⓑ 완료 후 그린'은 거짓이다. T6-ⓑ 는 끝났는데 게이트는 안 열린다.** 목록 라우트(`/products`+ItemList)는 실제로 지어졌지만 카테고리 라우트는 못 지었고, 이유가 라우트 부재가 아니었다: **카테고리 데이터층이 없다.** 진짜 선행 조건은 상품↔카테고리 매핑을 실제로 채우고 읽는 층이다 — 도메인 모델·쓰기 경로·공개 API 필터·SDK 파라미터. 그것이 선 뒤에 라우트를 짓고, 이 검사기가 우리 팩에서 그린이면 shipState 를 SHIPPABLE 로·published 를 true 로 함께 뒤집는다.",
|
|
148
|
+
"unblockOwner": "이 사실은 설계 §0-6(라우트 부재만 기록)에 없다 — memo119 §4.2 쇼핑몰 칸의 선행 조건 서술을 갱신할지는 설계가 판정한다.",
|
|
149
|
+
"required": [
|
|
150
|
+
{"id": "product-detail", "route": "productDetail", "jsonLd": ["Product"], "requiredChildren": ["Offer"], "ssr": true, "published": true, "llmsMarker": "상품 상세 = `Product` + variant 마다 `Offer`", "why": "오늘 유일하게 나가는 커머스 표면. variant 마다 Offer 를 낸다(항상 variant 단위 판매)."},
|
|
151
|
+
{"id": "product-detail-breadcrumb", "route": "productDetail", "jsonLd": ["BreadcrumbList"], "ssr": true, "published": true, "llmsMarker": "목록·상세엔 `BreadcrumbList`", "why": "검색결과에 '홈 > 상품 > 이름' 경로가 선다."},
|
|
152
|
+
{"id": "product-list-itemlist", "route": "productList", "jsonLd": ["ItemList"], "ssr": true, "published": true,
|
|
153
|
+
"llmsMarker": "목록 라우트도 그래프를 낸다 — `ItemList`", "publishTranche": "T4b", "realized": "DONE", "realizedNote": "T6 이 /products 목록 라우트를 신설했다(404→200 · ItemList+BreadcrumbList · sitemap 등재). 이 항목은 이제 통과 가능하다 — 그래도 COMMERCE 칸이 안 열리는 이유는 같은 칸의 CollectionPage 다.", "why": "**라우트 자체가 없다.** 목록이 없으면 '이 가게가 무엇을 파는가'를 기계가 한 번에 못 받는다 — 상세 N건을 개별로 발견해야 한다. 쇼핑몰 3표면 중 2개가 비는 이유가 여기서 시작한다."},
|
|
154
|
+
{"id": "product-category-collectionpage", "route": "productCategory", "jsonLd": ["CollectionPage"], "requiredChildren": ["ItemList"], "ssr": true, "published": false, "publishTranche": "T4b", "why": "오너가 든 세 표면(카테고리·상품 상세·상품 목록) 중 마지막 하나이고, **셋 중 유일하게 아직 안 선다**. 막는 것이 라우트가 아니라는 것이 실측의 요점이다(2026-07-28): `product_category_map` 은 V9 에 테이블로 존재하지만 **INSERT 경로가 전 레포 0건**이고(유일한 코드 접촉은 카테고리 삭제 가드의 COUNT 한 줄), 도메인 모델·공개 API 필터·SDK 파라미터가 전부 없다(`PublicProductSearchRequest` = productType·keyword 뿐이고 그 KDoc 이 매핑이 채워진 뒤 확장한다고 자인한다). 즉 지금 라우트를 지으면 **어느 카테고리를 열어도 상품 0건**이 나온다 — 그건 표면을 만든 것이 아니라 빈 선반을 하나 더 세운 것이다(§4.2 빈 선반 금지)."}
|
|
155
|
+
],
|
|
156
|
+
"why": "오너가 '쇼핑몰이라면 최소한 카테고리·상품 상세·상품 목록은 보장받아야 할 것 아니냐'고 든 그 세 표면이 이 칸의 정의다. 셋 중 하나만 나가고 있다."
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
"category": "BLOG",
|
|
160
|
+
"label": "블로그·미디어",
|
|
161
|
+
"shipState": "SHIPPABLE",
|
|
162
|
+
"seedReady": false,
|
|
163
|
+
"seedNote": "표면은 지금 서지만 **시드가 글을 못 만든다**(posts 키 없음·수요 실증 전 확장 금지·§5-5). 즉 '개시 즉시 완성'이 아니라 '개시 후 글을 쓰면 완성'이다. 카드 문구가 이 차이를 숨기면 안 된다.",
|
|
164
|
+
"required": [
|
|
165
|
+
{"id": "post-detail-blogposting", "route": "blogDetail", "jsonLd": ["BlogPosting"], "ssr": true, "published": false, "publishTranche": "T4b", "why": "글 하나가 답변 엔진에 인용되는 단위. 오늘 라이브다(blog/[slug]/page.tsx:55). §5.1 이 아직 이 규범을 안 적고 있을 뿐이라 published=false 다 — 본보기가 앞서고 레시피가 뒤따르는 방향이라 무해하다."},
|
|
166
|
+
{"id": "post-detail-breadcrumb", "route": "blogDetail", "jsonLd": ["BreadcrumbList"], "ssr": true, "published": true, "llmsMarker": "목록·상세엔 `BreadcrumbList`", "why": "상세 라우트의 경로 표시."},
|
|
167
|
+
{"id": "post-list-ssr", "route": "blogList", "jsonLd": [], "ssr": true, "published": true, "llmsMarker": "**ISR 유지.**", "why": "목록 라우트는 오늘 있다(force-static·revalidate 300)."}
|
|
168
|
+
],
|
|
169
|
+
"planned": [
|
|
170
|
+
{"id": "post-list-itemlist", "route": "blogList", "jsonLd": ["ItemList", "Blog"], "tranche": "T6", "realized": "DONE_DATA_PENDING", "realizedNote": "T6 이 /blog 목록에 BreadcrumbList 를 얹었다. ItemList 는 글이 0건이라 안 나간다 — **의도**다(negative 규율과 같다: 페이지에 없는 것을 JSON-LD 에 쓰지 않는다). 시드가 글을 못 만드는 한 이 항목의 그린은 데이터에 달려 있다.", "why": "목록 라우트는 있는데 그래프가 없다. 상품 목록과 같은 형태의 구멍이라 같이 메우는 것이 맞다."}
|
|
171
|
+
],
|
|
172
|
+
"why": "§4.2 블로그·미디어 칸. 표면은 지금·시드는 나중."
|
|
173
|
+
}
|
|
174
|
+
],
|
|
175
|
+
|
|
176
|
+
"notRegistered": [
|
|
177
|
+
{
|
|
178
|
+
"category": "COMMUNITY",
|
|
179
|
+
"label": "커뮤니티",
|
|
180
|
+
"why": "**등재하지 않는다**(§4.2 DON'T). 게시판 축 자체가 미래탐색이라 보장할 표면도 만들 데이터도 없다. 빈 항목으로 넣으면 이 파일이 '준비 중' 간판을 하나 더 세우는 셈이라, 침묵이 아니라 명시적 부재로 적는다."
|
|
181
|
+
}
|
|
182
|
+
],
|
|
183
|
+
|
|
184
|
+
"catalogRequirements": {
|
|
185
|
+
"$comment": "이 표를 게이트로 쓸 때 **카탈로그 UI 가 반드시 갖춰야 하는 것**(§4.1 W-F4). 계약 파일에 적는 이유: 이 요건이 빠지면 진열 게이트가 조용히 '부드러운 강제'로 변질돼 오너 정정 A(다양성 우선)를 뒤로 어긴다.",
|
|
186
|
+
"categoryAgnosticSurface": "카탈로그 탐색 UI 는 **카테고리 무관 표면(전체 목록·검색)을 반드시 갖는다.** 없으면 무카테고리 = 사실상 비가시가 되고, '어떤 템플릿이든 적재될 수 있다'가 말뿐이 된다.",
|
|
187
|
+
"noDefectWording": "미충족은 **결함이 아니라 다른 칸**이다. 카드 문구에 '~불가'·'미지원' 류 결함 어투를 쓰지 않는다(§3.9·오너 정정 D — 편집 축은 가격 단계이지 손실이 아니다)."
|
|
188
|
+
}
|
|
189
|
+
}
|
package/dist/index.cjs
CHANGED
|
@@ -291,25 +291,27 @@ function safeJsonParse(text) {
|
|
|
291
291
|
}
|
|
292
292
|
|
|
293
293
|
// src/sections.ts
|
|
294
|
-
var SECTION_CONTRACT_REV =
|
|
294
|
+
var SECTION_CONTRACT_REV = 3;
|
|
295
295
|
var SECTION_CONTRACT = [
|
|
296
296
|
// 뷰티(memo47) — append-only 계약상 불변
|
|
297
297
|
// SERVICE_MENU 의 ItemList 는 rev 2 에서 올라왔다(memo119 T6-ⓒ): 쇼핑몰 유형엔 상품 목록 표면을
|
|
298
298
|
// 요구하면서 예약 유형엔 시술 목록 표면이 없던 비대칭의 해소다. 뷰티 사이트에서 시술 목록의
|
|
299
299
|
// 정위치는 별도 라우트가 아니라 홈의 이 섹션이라, FAQ_LIST→FAQPage 와 같은 형태로 섹션이 직접 낸다.
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
{ type: "
|
|
303
|
-
{ type: "
|
|
300
|
+
// rev 3 에서 productIds 의 `?` 가 떨어졌다 — 목록을 내겠다고 선언한 섹션이 목록을 안 가리키는 상태가
|
|
301
|
+
// 계약상 성립하지 않게 됐다(BOOKING_CTA.productId 는 rev 1 부터 필수였다·비대칭 해소).
|
|
302
|
+
{ type: "SERVICE_MENU", vertical: "BEAUTY", jsonLd: "ItemList", requiredRefs: ["productIds"] },
|
|
303
|
+
{ type: "BEFORE_AFTER_GALLERY", vertical: "BEAUTY", jsonLd: null, requiredRefs: [] },
|
|
304
|
+
{ type: "BOOKING_CTA", vertical: "BEAUTY", jsonLd: null, requiredRefs: ["productId"] },
|
|
305
|
+
{ type: "DOCTOR_INTRO", vertical: "BEAUTY", jsonLd: null, requiredRefs: [] },
|
|
304
306
|
// 기업 마케팅(memo102 §2)
|
|
305
|
-
{ type: "HERO", vertical: "GENERAL", jsonLd: null },
|
|
306
|
-
{ type: "FEATURE_GRID", vertical: "GENERAL", jsonLd: null },
|
|
307
|
-
{ type: "TEXT_MEDIA", vertical: "GENERAL", jsonLd: null },
|
|
308
|
-
{ type: "LOGO_WALL", vertical: "GENERAL", jsonLd: null },
|
|
309
|
-
{ type: "STATS_BAND", vertical: "GENERAL", jsonLd: null },
|
|
310
|
-
{ type: "TESTIMONIALS", vertical: "GENERAL", jsonLd: null },
|
|
311
|
-
{ type: "FAQ_LIST", vertical: "GENERAL", jsonLd: "FAQPage" },
|
|
312
|
-
{ type: "LEAD_CTA", vertical: "GENERAL", jsonLd: null }
|
|
307
|
+
{ type: "HERO", vertical: "GENERAL", jsonLd: null, requiredRefs: [] },
|
|
308
|
+
{ type: "FEATURE_GRID", vertical: "GENERAL", jsonLd: null, requiredRefs: [] },
|
|
309
|
+
{ type: "TEXT_MEDIA", vertical: "GENERAL", jsonLd: null, requiredRefs: [] },
|
|
310
|
+
{ type: "LOGO_WALL", vertical: "GENERAL", jsonLd: null, requiredRefs: [] },
|
|
311
|
+
{ type: "STATS_BAND", vertical: "GENERAL", jsonLd: null, requiredRefs: [] },
|
|
312
|
+
{ type: "TESTIMONIALS", vertical: "GENERAL", jsonLd: null, requiredRefs: [] },
|
|
313
|
+
{ type: "FAQ_LIST", vertical: "GENERAL", jsonLd: "FAQPage", requiredRefs: [] },
|
|
314
|
+
{ type: "LEAD_CTA", vertical: "GENERAL", jsonLd: null, requiredRefs: [] }
|
|
313
315
|
];
|
|
314
316
|
function sectionsOfVertical(vertical) {
|
|
315
317
|
return SECTION_CONTRACT.filter((s) => s.vertical === vertical);
|