# STD-PARTNER-JSONLD-FILL v1.0

> # 📐 STD-PARTNER-JSONLD-FILL v1.0 — 사이트 풍부 schema 적재 표준 > Track : `T-PARTNER-JSONLD-FILL` > 작성 : 2026-05-10 KST | 검증 사례: bizspring.co.kr 70 URL > 적용 

## 핵심 요약


# 📐 STD-PARTNER-JSONLD-FILL v1.0 — 사이트 풍부 schema 적재 표준

> Track    : `T-PARTNER-JSONLD-FILL`
> 작성     : 2026-05-10 KST | 검증 사례: bizspring.co.kr 70 URL
> 적용     : 모든 GEOcare 등록 사이트 (PREMIUM_GEO 파트너 사이트 포함)
> 짝 표준  : STD-USER-FLOW v1.0, STD-SITE-GEO-FLOW v1.1, STD-DEPLOY v1.1, STD-EDGE-01

---

## 0. 적용 시점

다음 중 하나라도 해당 시 본 표준 적용:
- 사이트가 GEOcare에 등록되고 EEAT 분석 완료
- 사이트가 schema.geocare.ai EF v5 호출 사용 시작
- PREMIUM_GEO 파트너 사이트 (Brand Hub bmp.ai 운영)

## 1. 모델 — 3 SSOT (UPSERT 통일)

```
┌──────────────────────────────────────────────────────────────┐
│ ① gp_geoex_site_jsonld    (UNIQUE site_domain)               │
│   - Organization + WebSite (도메인 단위, 1행)                  │
│   - 메인 페이지 정보 흡수 가능                                  │
├──────────────────────────────────────────────────────────────┤
│ ② gp_geoex_brand_jsonld   (UNIQUE url_prefix)                │
│   - Brand × N (멀티브랜드 사이트는 여러 행)                      │
│   - URL prefix longest match로 결정                            │
├──────────────────────────────────────────────────────────────┤
│ ③ gp_geoex_eeat_analysis  (UNIQUE site+url, 기존 테이블)       │
│   - 신규 컬럼 3개:                                              │
│     • generated_jsonld (jsonb)        ← 페이지 schema array     │
│     • jsonld_schema_types (text[])                             │
│     • jsonld_generated_at (timestamptz)                        │
│   - WebPage / FAQPage / Article 등 모든 page-level             │
└──────────────────────────────────────────────────────────────┘

[송출 EF]
EF v5 (geobh-jsonld) → RPC v3 (get_jsonld_by_url_v3)
  → 3 SSOT 병합 → @graph 단일 응답 (?pretty=1 옵션)

[배제]
gp_geoex_jsonld_log
  - Read-only archive (이력 테이블)
  - 신규 INSERT 금지 (가비지 발생 패턴)
  - dup_ratio 2.43 vs eeat_analysis 1.00 (UPSERT 모범)
```

## 2. 적용 절차 (신규 사이트)

### 2-1. siteGEO 적재 (1회, 도메인 단위)

```sql
INSERT INTO gp_geoex_site_jsonld (site_domain, organization_schema, website_schema, source_kpi, created_by)
VALUES (
  'https://example.com',
  jsonb_build_object(
    '@id', 'https://example.com/#organization',
    '@type', 'Organization',
    'name', '브랜드명',
    'alternateName', jsonb_build_array('alias1', 'alias2'),
    'url', 'https://example.com',
    'description', '...',
    'knowsAbout', jsonb_build_array('핵심키워드1', '핵심키워드2'),
    'makesOffer', jsonb_build_array(
      jsonb_build_object('@type', 'Offer', 'itemOffered', jsonb_build_object('@type', 'Service', 'name', '제품1'))
    )
  ),
  jsonb_build_object(
    '@id', 'https://example.com/#website',
    '@type', 'WebSite',
    'url', 'https://example.com',
    'name', '...',
    'inLanguage', 'ko-KR',
    'publisher', jsonb_build_object('@id', 'https://example.com/#organization'),
    'potentialAction', jsonb_build_object(
      '@type', 'SearchAction',
      'target', jsonb_build_object('@type', 'EntryPoint', 'urlTemplate', 'https://example.com/search?q={search_term_string}'),
      'query-input', 'required name=search_term_string'
    )
  ),
  'manual',
  'OPS or Claude'
)
ON CONFLICT (site_domain) DO UPDATE 
  SET organization_schema = EXCLUDED.organization_schema,
      website_schema = EXCLUDED.website_schema;
```

### 2-2. brandGEO 적재 (브랜드별 1회)

```sql
INSERT INTO gp_geoex_brand_jsonld (site_domain, brand_settings_id, url_prefix, brand_name, brand_schema, source_kpi)
SELECT 
  '<host>',
  bs.id,
  bs.site_domain,
  bs.brand_name,
  jsonb_build_object(
    '@id', rtrim(bs.site_domain, '/') || '/#brand',
    '@type', 'Brand',
    'name', bs.brand_name,
    'alternateName', COALESCE(to_jsonb(bs.brand_aliases), '[]'::jsonb),
    'parentOrganization', jsonb_build_object('@id', '<host>/#organization'),
    'description', '...',
    'knowsAbout', COALESCE(to_jsonb(bs.brand_keywords), '[]'::jsonb),
    'makesOffer', CASE 
      WHEN bs.product_names IS NOT NULL AND array_length(bs.product_names, 1) > 0 
      THEN (SELECT jsonb_agg(jsonb_build_object('@type', 'Offer', 'itemOffered', jsonb_build_object('@type', 'Service', 'name', p))) FROM unnest(bs.product_names) p)
      ELSE '[]'::jsonb
    END
  ),
  'manual'
FROM gp_geoex_brand_settings bs
WHERE bs.site_domain LIKE '<host>%'
ON CONFLICT (url_prefix) DO UPDATE
  SET brand_schema = EXCLUDED.brand_schema,
      brand_name = EXCLUDED.brand_name;
```

### 2-3. URL-specific UPSERT (eeat_analysis 활용, 일괄)

```sql
WITH urls AS (
  SELECT DISTINCT ON (ee.url) 
    ee.id, ee.url, ee.site_domain,
    ua.title, ua.page_type, ua.total_score
  FROM gp_geoex_eeat_analysis ee
  LEFT JOIN gp_geoex_url_analysis ua 
    ON ua.url = ee.url AND ua.site_domain LIKE '<host>%'
  WHERE ee.site_domain LIKE '<host>%'
  ORDER BY ee.url, ua.total_score DESC NULLS LAST
),
url_brand AS (
  SELECT u.*, br.brand_schema, br.url_prefix
  FROM urls u
  LEFT JOIN LATERAL (
    SELECT brand_schema, url_prefix
    FROM gp_geoex_brand_jsonld
    WHERE (u.url LIKE url_prefix || '%') OR (u.url = rtrim(url_prefix, '/'))
    ORDER BY length(url_prefix) DESC LIMIT 1
  ) br ON true
)
UPDATE gp_geoex_eeat_analysis ee
SET 
  generated_jsonld = jsonb_build_array(
    jsonb_build_object(
      '@id', ub.url || '#webpage',
      '@type', 'WebPage',
      'url', ub.url,
      'name', COALESCE(NULLIF(ub.title, ''), '<host_name>'),
      'description', COALESCE(ub.brand_schema->>'description', '<fallback>'),
      'isPartOf', jsonb_build_object('@id', '<host>/#website'),
      'about', jsonb_build_object('@id', COALESCE(ub.brand_schema->>'@id', '<host>/#organization')),
      'inLanguage', 'ko-KR'
    )
  ),
  jsonld_schema_types = ARRAY['WebPage'],
  jsonld_generated_at = NOW()
FROM url_brand ub
WHERE ee.id = ub.id;
```

## 3. 검증 4단계

| 단계 | 검증 | SQL/명령 |
|:-:|---|---|
| 1 | 3 SSOT 적재 카운트 | `SELECT COUNT FROM site_jsonld / brand_jsonld / eeat_analysis WHERE generated_jsonld IS NOT NULL` |
| 2 | RPC v3 호출 (5+ URL 샘플) | `SELECT * FROM get_jsonld_by_url_v3('<sample_url>')` |
| 3 | EF v5 직접 호출 | `curl "schema.geocare.ai/functions/v1/geobh-jsonld?url=<url>&pretty=1"` |
| 4 | 사이트 view-source (PHP 캐시 만료 후) | 브라우저 view-source — `<script type="application/ld+json">` 풍부 schema |

## 4. 환각 방지 정책 (G-02 ABSOLUTE RULES)

본 표준 적용 시 다음 금지:
- ❌ FAQPage `acceptedAnswer`에 `eeat_analysis.suggested_faqs`의 `answer_direction` 직접 사용 (질문은 있으나 답변 본문 부재 → 환각 위험)
- ❌ Article `author`/`datePublished` 추정 (정보 부재 시 OMIT OVER FAKE)
- ❌ Service `provider`/`offers` 추정 (제품/가격 정보 부재)

본 표준은 **WebPage 기본 schema만 일괄 자동 적재**. 페이지 단위 풍부 schema(FAQPage/Article 등)는 별 트랙 (사람 또는 LLM 검수 후).

## 5. 변경 시 영향 (Cross-Room)

본 표준 변경 시:
- GEOCARE Snapshot 갱신
- PREMIUM_GEO 통보 (bmp.ai 파트너 사이트 영향)
- SALES 통보 (영업 자료 갱신)

## 6. 검증 사례

| 사이트 | 적재 | 검증 |
|---|---|:-:|
| bizspring.co.kr | site (1) + brand (3) + eeat_analysis (69 URL) | ✅ EF v5 6 시나리오 ALL PASS (5/10) |

## 7. RPC v3 정의 본문 (재현 가능)

```sql
CREATE OR REPLACE FUNCTION get_jsonld_by_url_v3(p_url TEXT)
RETURNS TABLE (
  cached BOOLEAN,
  data JSONB
) AS $$
DECLARE
  v_url_norm TEXT;
  v_site_domain TEXT;
  v_org JSONB;
  v_website JSONB;
  v_brand JSONB;
  v_page JSONB;
  v_graph JSONB;
BEGIN
  v_url_norm := CASE 
    WHEN p_url ~ '^https?://[^/]+/?$' THEN p_url
    ELSE rtrim(p_url, '/')
  END;
  v_site_domain := substring(p_url FROM '^(https?://[^/]+)');
  
  SELECT organization_schema, website_schema 
    INTO v_org, v_website
  FROM gp_geoex_site_jsonld 
  WHERE site_domain = v_site_domain LIMIT 1;
  
  SELECT brand_schema INTO v_brand
  FROM gp_geoex_brand_jsonld
  WHERE (p_url LIKE url_prefix || '%') OR (p_url = rtrim(url_prefix, '/'))
  ORDER BY length(url_prefix) DESC LIMIT 1;
  
  SELECT generated_jsonld INTO v_page
  FROM gp_geoex_eeat_analysis
  WHERE url IN (p_url, v_url_norm, v_url_norm || '/')
    AND generated_jsonld IS NOT NULL
    AND jsonb_typeof(generated_jsonld) = 'array'
  ORDER BY jsonld_generated_at DESC NULLS LAST LIMIT 1;
  
  v_graph := '[]'::jsonb;
  IF v_org IS NOT NULL THEN v_graph := v_graph || jsonb_build_array(v_org); END IF;
  IF v_website IS NOT NULL THEN v_graph := v_graph || jsonb_build_array(v_website); END IF;
  IF v_brand IS NOT NULL THEN v_graph := v_graph || jsonb_build_array(v_brand); END IF;
  IF v_page IS NOT NULL THEN v_graph := v_graph || v_page; END IF;
  
  IF jsonb_typeof(v_graph) = 'array' AND v_graph != '[]'::jsonb THEN
    RETURN QUERY SELECT 
      (v_page IS NOT NULL),
      jsonb_build_array(jsonb_build_object(
        '@context', 'https://schema.org',
        '@graph', v_graph
      ));
  END IF;
END;
$$ LANGUAGE plpgsql STABLE;

GRANT EXECUTE ON FUNCTION get_jsonld_by_url_v3(TEXT) TO anon, authenticated, service_role;
```

## 8. EF v5 관련

- 라이브: `https://schema.geocare.ai/functions/v1/geobh-jsonld?url=<URL>&pretty=1`
- 응답 메타: `ef_version: v5, rpc_version: v3, sources: ['site_jsonld', 'brand_jsonld', 'eeat_analysis.generated_jsonld']`
- 백업 (롤백): KHub `8ce966ce` (EF v2 코드 — RPC v1.1)
- 배포 방식: Supabase Management API multipart (학습 #L25~L27)

## 9. 적용 워크플로우 (신규 사이트별)

```
1. 사이트가 GEOcare 등록 + EEAT 분석 완료 확인
2. brand_settings 등록 확인 (멀티브랜드면 N행)
3. site_jsonld INSERT (도메인 1회)
4. brand_jsonld INSERT (브랜드별)
5. eeat_analysis UPSERT (URL 일괄)
6. 검증 4단계 완료
7. (사이트 측) v2.10 PHP 박혀 있으면 캐시 만료 1h 후 라이브
   또는 즉시: 캐시 디렉터리 클리어 + view-source 재호출
```

## 10. 변경 이력

| 일자 | 버전 | 변경 |
|---|---|---|
| 2026-05-10 | v1.0 | 초판. T-BIZSPRING-JSONLD-FILL 검증 사례 기반. 3 SSOT UPSERT 모델. EF v5 / RPC v3. |


## FAQ


---
출처: https://bizspring.ai/kb/ref/khub/geocare-std-partner-jsonld-fill-v1-0 · 최종 갱신 2026-08-24T11:53:44.997497+00:00
