Files
HeuJung 1124fad027 feat(gdpr): 필수 저장 항목 허용목록을 운영자 설정으로 이관
허용목록이 플러그인 코드 상수라, 새 확장이 저장 키를 도입할 때마다 이 플러그인을
고쳐야 했다. 고치지 않으면 그 키는 방문마다 파기되는데 예외도 로그도 남지 않아
운영자에게는 "설정이 저장되지 않는다" 는 증상만 보인다.

허용목록을 운영자 설정(necessary_storage_allowlist)으로 옮기고, 관리자 환경설정에
브라우저 저장소·세션 저장소·쿠키 세 카드를 두어 직접 편집하게 했다. 확장은 GDPR 을
알 필요가 없고 GDPR 도 확장을 알 필요가 없다 — 연결은 운영자가 화면에서 한다.
저장 키를 플러그인이 관측·수집하지 않는 것도 같은 이유다: 관측 자체가 추적이다.

판정은 서버(미들웨어)와 클라이언트 셋(인터셉터 둘 + 정리기)이 같은 설정을 같은 매칭
규칙으로 읽는다. 잠금 항목(로그인 토큰·CSRF·세션 쿠키·동의 기록)만 설정 밖에 두고
판정 시점에 합집합으로 얹는다 — 설정에 담기면 저장 요청 한 번으로 지워져 잠금이
아니게 된다. 기설치본은 업그레이드 스텝이 현재 카탈로그를 설정 파일에 못박되,
운영자가 이미 편집했으면 그대로 둔다.

함께 고친 선재 결함 셋:
- 차단 도메인 추천 목록이 응답에 실리지 않아 자동완성이 동작하지 않던 문제
- 세션 쿠키 이름을 기본값에서 바꾼 사이트에서 브라우저 측 목록의 그 항목이 죽던 문제
- 쿠키 목록에만 앞부분 매칭(`이름_*`)이 적용되지 않던 문제
2026-09-03 22:18:18 +09:00

652 lines
23 KiB
JSON

{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$comment": "레이아웃 편집기 스펙 — GDPR 플러그인 도메인 sampleData. 관리자 레이아웃(plugin_settings / gdpr_consent_log + 정책 버전 모달)의 data_source ID 전수 스캔 기반: gdprSettings / gdprPolicyVersionCurrent / gdprPolicyVersionHistory / gdprPolicyVersionSnapshot / gdprConsentLog. 응답 shape 는 각 레이아웃의 실제 바인딩 경로(gdprSettings.data.* / gdprPolicyVersion*.data.data.* / gdprConsentLog.data.data[] + pagination) 역추론. 편집기 캔버스 프리뷰에서 generic 폴백 차단용 mock.",
"pluginId": "sirsoft-gdpr",
"version": "1.0.0",
"description": "GDPR 플러그인 레이아웃 편집기 샘플 데이터 (쿠키 동의 설정 / 동의 이력 / 개인정보 정책 버전).",
"sampleData": {
"byDataSourceId": {
"gdprSettings": {
"data": {
"is_blocking_enabled": true,
"auto_block": true,
"respect_dnt": true,
"banner_position": "bottom",
"show_settings_button": true,
"consent_version": 3,
"consent_expiry_days": 365,
"privacy_policy_slug": "privacy",
"privacy_policy_url": "/privacy",
"geo_targeting": false,
"geo_countries": [
"KR",
"JP"
],
"test_mode": false,
"log_consents": true,
"blocked_domains": [
"www.google-analytics.com",
"connect.facebook.net"
],
"cookie_categories": [
{
"key": "necessary",
"name": "필수 쿠키",
"description": "사이트 기본 동작에 반드시 필요한 쿠키입니다.",
"cookies": [
"session",
"csrf_token"
],
"required": true,
"enabled": true
},
{
"key": "analytics",
"name": "분석 쿠키",
"description": "방문 통계를 수집해 서비스를 개선합니다.",
"cookies": [
"_ga",
"_gid"
],
"required": false,
"enabled": true
},
{
"key": "marketing",
"name": "마케팅 쿠키",
"description": "관심사 기반 광고 제공에 사용됩니다.",
"cookies": [
"_fbp"
],
"required": false,
"enabled": false
}
],
"default_blocked_domains_preview": {
"analytics": [
"www.google-analytics.com",
"stats.g.doubleclick.net"
],
"marketing": [
"connect.facebook.net",
"www.googleadservices.com"
]
},
"default_necessary_allowlist_preview": {
"localStorage": [
"g7_locale",
"g7_color_scheme",
"g7_filters_*"
],
"sessionStorage": [
"g7:sirsoft-pay_kginicis:pendingClose"
],
"cookie": [
"laravel_maintenance"
]
},
"settings": {
"necessary_storage_locked": {
"localStorage": [
"auth_token"
],
"sessionStorage": [],
"cookie": [
"XSRF-TOKEN",
"laravel_session",
"gdpr_session"
]
}
}
}
},
"gdprPolicyVersionCurrent": {
"data": {
"data": {
"version": 3,
"created_at": "2026-05-20 14:30:00",
"change_type": "material",
"memo": "쿠키 분류 항목 추가 및 보관 기간 변경",
"publisher": {
"name": "관리자",
"email": "admin@example.com"
}
}
}
},
"gdprPolicyVersionHistory": {
"data": {
"data": [
{
"version": 3,
"created_at": "2026-05-20 14:30:00",
"change_type": "material",
"memo": "쿠키 분류 항목 추가 및 보관 기간 변경",
"publisher": {
"name": "관리자",
"email": "admin@example.com"
}
},
{
"version": 2,
"created_at": "2026-03-11 09:05:00",
"change_type": "non_material",
"memo": "문구 다듬기",
"publisher": {
"name": "운영자",
"email": "manager@example.com"
}
},
{
"version": 1,
"created_at": "2026-01-02 10:00:00",
"change_type": "initial",
"memo": "최초 발행",
"publisher": {
"name": "관리자",
"email": "admin@example.com"
}
}
]
}
},
"gdprPolicyVersionSnapshot": {
"data": {
"data": {
"version": 2,
"created_at": "2026-03-11 09:05:00",
"change_type": "non_material",
"memo": "문구 다듬기",
"publisher": {
"name": "운영자",
"email": "manager@example.com"
},
"snapshot": {
"privacy_policy_slug": "privacy",
"cookie_categories": [
{
"key": "necessary",
"name": "필수 쿠키",
"required": true,
"enabled": true
},
{
"key": "analytics",
"name": "분석 쿠키",
"required": false,
"enabled": true
}
]
},
"blocked_domains_list": [
"www.google-analytics.com",
"connect.facebook.net"
]
}
}
},
"gdprConsentLog": {
"data": {
"data": [
{
"id": 1,
"created_at": "2026-05-29 18:22:10",
"ip_address": "203.0.113.10",
"consent_given": true,
"categories": [
"necessary",
"analytics"
],
"policy_version": 3
},
{
"id": 2,
"created_at": "2026-05-29 17:01:42",
"ip_address": "203.0.113.55",
"consent_given": false,
"categories": [
"necessary"
],
"policy_version": 3
},
{
"id": 3,
"created_at": "2026-05-28 11:40:05",
"ip_address": "198.51.100.7",
"consent_given": true,
"categories": [
"necessary",
"analytics",
"marketing"
],
"policy_version": 2
}
],
"pagination": {
"current_page": 1,
"last_page": 4,
"per_page": 20,
"total": 73
}
}
},
"gdprPublicSettings": {
"data": {
"cookie_policy_version": "1",
"privacy_policy_slug": "privacy",
"privacy_policy_available": true,
"legal_entity_name": "",
"data_storage_location": "",
"banner_enabled": true,
"banner_position": "bottom_bar",
"cookie_categories": [
{
"key": "necessary",
"required": true,
"label": {
"ko": "필수 쿠키",
"en": "Strictly Necessary"
},
"description": {
"ko": "세션·CSRF·로그인 토큰, 장바구니 식별자, 사용자가 직접 고른 언어 설정과 화면 테마, 쿠키 동의 기록 등 사이트 운영에 반드시 필요한 항목입니다. 비활성화할 수 없습니다.",
"en": "Strictly necessary for site operation: session/CSRF/auth tokens, shopping basket identifier, user-selected language preference and display theme, cookie consent record. Cannot be disabled."
}
},
{
"key": "functional",
"required": false,
"label": {
"ko": "기능 쿠키",
"en": "Functional"
},
"description": {
"ko": "사용자 선호도(표시 통화 등)를 기억하는 쿠키입니다. 거부 시 매 방문마다 기본값으로 표시됩니다.",
"en": "Cookies that remember user preferences such as display currency. If declined, defaults are used on every visit."
}
},
{
"key": "analytics",
"required": false,
"label": {
"ko": "분석 쿠키",
"en": "Analytics"
},
"description": {
"ko": "방문자가 사이트를 어떻게 이용하는지 익명으로 측정해 더 나은 서비스를 만드는 데 사용됩니다. (예: Google Analytics, Hotjar)",
"en": "Used to anonymously measure how visitors use the site so we can improve it. (e.g. Google Analytics, Hotjar)"
}
},
{
"key": "marketing",
"required": false,
"label": {
"ko": "마케팅 쿠키",
"en": "Marketing"
},
"description": {
"ko": "관심사에 맞는 광고를 보여주거나, 광고가 얼마나 효과적이었는지 측정하는 데 사용됩니다. SNS 영상 임베드 등도 포함됩니다. (예: Facebook 픽셀, Google 광고, YouTube 영상)",
"en": "Used to show ads relevant to your interests, measure ad performance, and embed social media content. (e.g. Facebook Pixel, Google Ads, YouTube embeds)"
}
}
],
"blocked_domains": {
"functional": [
"*.crisp.chat",
"client.crisp.chat",
"*.intercom.io",
"widget.intercom.io",
"*.tawk.to",
"embed.tawk.to",
"cdn.weglot.com",
"*.weglot.com",
"*.usercentrics.eu"
],
"analytics": [
"google-analytics.com",
"*.google-analytics.com",
"googletagmanager.com",
"*.googletagmanager.com",
"ssl.google-analytics.com",
"*.hotjar.com",
"static.hotjar.com",
"*.mixpanel.com",
"cdn.mxpnl.com",
"*.amplitude.com",
"cdn.amplitude.com",
"*.segment.io",
"*.segment.com",
"wcs.naver.net",
"wcs.naver.com",
"*.beusable.net"
],
"marketing": [
"facebook.net",
"connect.facebook.net",
"facebook.com",
"*.facebook.com",
"doubleclick.net",
"*.doubleclick.net",
"googleadservices.com",
"googlesyndication.com",
"ads.google.com",
"*.criteo.com",
"static.criteo.net",
"*.adnxs.com",
"*.taboola.com",
"cdn.taboola.com",
"*.outbrain.com",
"*.kakao.com",
"analytics.ad.daum.net",
"platform.twitter.com",
"*.twitter.com",
"platform.linkedin.com",
"*.linkedin.com"
]
},
"default_blocked_domains_preview": {
"functional": [
"*.crisp.chat",
"client.crisp.chat",
"*.intercom.io",
"widget.intercom.io",
"*.tawk.to",
"embed.tawk.to",
"cdn.weglot.com",
"*.weglot.com",
"*.usercentrics.eu"
],
"analytics": [
"google-analytics.com",
"*.google-analytics.com",
"googletagmanager.com",
"*.googletagmanager.com",
"ssl.google-analytics.com",
"*.hotjar.com",
"static.hotjar.com",
"*.mixpanel.com",
"cdn.mxpnl.com",
"*.amplitude.com",
"cdn.amplitude.com",
"*.segment.io",
"*.segment.com",
"wcs.naver.net",
"wcs.naver.com",
"*.beusable.net"
],
"marketing": [
"facebook.net",
"connect.facebook.net",
"facebook.com",
"*.facebook.com",
"doubleclick.net",
"*.doubleclick.net",
"googleadservices.com",
"googlesyndication.com",
"ads.google.com",
"*.criteo.com",
"static.criteo.net",
"*.adnxs.com",
"*.taboola.com",
"cdn.taboola.com",
"*.outbrain.com",
"*.kakao.com",
"analytics.ad.daum.net",
"platform.twitter.com",
"*.twitter.com",
"platform.linkedin.com",
"*.linkedin.com"
]
}
}
},
"gdprMyConsent": {
"data": {
"has_consented": true,
"consents": {
"analytics": true,
"functional": true,
"marketing": true,
"necessary": true
},
"needs_renewal": false,
"current_policy_version": "1",
"is_member": true
}
},
"gdprMeConsents": {
"data": {
"user_id": 1,
"needs_renewal": false,
"current_policy_version": "1",
"consents": [
{
"id": 1,
"consent_key": "cookie_necessary",
"consent_label": "필수 쿠키",
"consent_description": "세션·CSRF·로그인 토큰, 장바구니 식별자, 사용자가 직접 고른 언어 설정과 화면 테마, 쿠키 동의 기록 등 사이트 운영에 반드시 필요한 항목입니다. 비활성화할 수 없습니다.",
"consent_category": "cookie",
"is_required": true,
"is_consented": true,
"can_revoke": false,
"can_grant": false,
"needs_renewal_this_item": false,
"consented_at": "2026-05-24 09:32:38",
"revoked_at": null,
"consent_count": 1,
"policy_version": "1",
"last_source": "banner",
"created_at": "2026-05-24 09:32:38",
"updated_at": "2026-05-24 09:32:38"
},
{
"id": 2,
"consent_key": "cookie_functional",
"consent_label": "기능 쿠키",
"consent_description": "사용자 선호도(표시 통화 등)를 기억하는 쿠키입니다. 거부 시 매 방문마다 기본값으로 표시됩니다.",
"consent_category": "cookie",
"is_required": false,
"is_consented": true,
"can_revoke": true,
"can_grant": false,
"needs_renewal_this_item": false,
"consented_at": "2026-05-24 09:32:38",
"revoked_at": null,
"consent_count": 1,
"policy_version": "1",
"last_source": "banner",
"created_at": "2026-05-24 09:32:38",
"updated_at": "2026-05-24 09:32:38"
},
{
"id": 3,
"consent_key": "cookie_analytics",
"consent_label": "분석 쿠키",
"consent_description": "방문자가 사이트를 어떻게 이용하는지 익명으로 측정해 더 나은 서비스를 만드는 데 사용됩니다. (예: Google Analytics, Hotjar)",
"consent_category": "cookie",
"is_required": false,
"is_consented": true,
"can_revoke": true,
"can_grant": false,
"needs_renewal_this_item": false,
"consented_at": "2026-05-24 09:32:38",
"revoked_at": null,
"consent_count": 1,
"policy_version": "1",
"last_source": "banner",
"created_at": "2026-05-24 09:32:38",
"updated_at": "2026-05-24 09:32:38"
},
{
"id": 4,
"consent_key": "cookie_marketing",
"consent_label": "마케팅 쿠키",
"consent_description": "관심사에 맞는 광고를 보여주거나, 광고가 얼마나 효과적이었는지 측정하는 데 사용됩니다. SNS 영상 임베드 등도 포함됩니다. (예: Facebook 픽셀, Google 광고, YouTube 영상)",
"consent_category": "cookie",
"is_required": false,
"is_consented": true,
"can_revoke": true,
"can_grant": false,
"needs_renewal_this_item": false,
"consented_at": "2026-05-24 09:32:38",
"revoked_at": null,
"consent_count": 1,
"policy_version": "1",
"last_source": "banner",
"created_at": "2026-05-24 09:32:38",
"updated_at": "2026-05-24 09:32:38"
}
]
}
},
"gdprMyConsents": {
"data": [
{
"id": 1,
"consent_key": "cookie_necessary",
"consent_label": "필수 쿠키",
"consent_description": "세션·CSRF·로그인 토큰, 장바구니 식별자, 사용자가 직접 고른 언어 설정과 화면 테마, 쿠키 동의 기록 등 사이트 운영에 반드시 필요한 항목입니다. 비활성화할 수 없습니다.",
"consent_category": "cookie",
"is_required": true,
"is_consented": true,
"can_revoke": false,
"can_grant": false,
"needs_renewal_this_item": false,
"consented_at": "2026-05-24 09:32:38",
"revoked_at": null,
"consent_count": 1,
"policy_version": "1",
"last_source": "banner",
"created_at": "2026-05-24 09:32:38",
"updated_at": "2026-05-24 09:32:38"
},
{
"id": 2,
"consent_key": "cookie_functional",
"consent_label": "기능 쿠키",
"consent_description": "사용자 선호도(표시 통화 등)를 기억하는 쿠키입니다. 거부 시 매 방문마다 기본값으로 표시됩니다.",
"consent_category": "cookie",
"is_required": false,
"is_consented": true,
"can_revoke": true,
"can_grant": false,
"needs_renewal_this_item": false,
"consented_at": "2026-05-24 09:32:38",
"revoked_at": null,
"consent_count": 1,
"policy_version": "1",
"last_source": "banner",
"created_at": "2026-05-24 09:32:38",
"updated_at": "2026-05-24 09:32:38"
},
{
"id": 3,
"consent_key": "cookie_analytics",
"consent_label": "분석 쿠키",
"consent_description": "방문자가 사이트를 어떻게 이용하는지 익명으로 측정해 더 나은 서비스를 만드는 데 사용됩니다. (예: Google Analytics, Hotjar)",
"consent_category": "cookie",
"is_required": false,
"is_consented": true,
"can_revoke": true,
"can_grant": false,
"needs_renewal_this_item": false,
"consented_at": "2026-05-24 09:32:38",
"revoked_at": null,
"consent_count": 1,
"policy_version": "1",
"last_source": "banner",
"created_at": "2026-05-24 09:32:38",
"updated_at": "2026-05-24 09:32:38"
},
{
"id": 4,
"consent_key": "cookie_marketing",
"consent_label": "마케팅 쿠키",
"consent_description": "관심사에 맞는 광고를 보여주거나, 광고가 얼마나 효과적이었는지 측정하는 데 사용됩니다. SNS 영상 임베드 등도 포함됩니다. (예: Facebook 픽셀, Google 광고, YouTube 영상)",
"consent_category": "cookie",
"is_required": false,
"is_consented": true,
"can_revoke": true,
"can_grant": false,
"needs_renewal_this_item": false,
"consented_at": "2026-05-24 09:32:38",
"revoked_at": null,
"consent_count": 1,
"policy_version": "1",
"last_source": "banner",
"created_at": "2026-05-24 09:32:38",
"updated_at": "2026-05-24 09:32:38"
}
]
}
}
},
"states": {
"comment": "GDPR 동의 로그 목록 유무.",
"groups": [
{
"comment": "consent-log — gdprConsentLog 데이터소스 빈 배열 시 빈 상태.",
"scope": {
"kind": "route",
"match": "*/admin/plugins/sirsoft-gdpr/consent-log"
},
"items": [
{
"id": "with_data",
"label": "$t:editor.state.with_data",
"default": true
},
{
"id": "empty_data",
"label": "$t:editor.state.empty_data",
"sampleDataOverrides": {
"byDataSourceId": {
"gdprConsentLog": {
"data": {
"data": [],
"meta": {
"total": 0,
"per_page": 20,
"current_page": 1
}
}
}
}
}
}
]
},
{
"comment": " states 전수 — 쿠키 배너(user_global_overlay EP 주입 조각). 배너 루트 if 가 gdprMyConsent.has_consented !== true 를 요구하는데 기본 샘플은 has_consented=true 라 미렌더. 미동의 사용자 샘플(base 샘플과 동일 shape 통째 교체)로 배너를 노출한다. default 항목은 템플릿 _user_base 그룹이 소유(concat — 중복 default 금지).",
"scope": {
"kind": "base",
"match": "_user_base"
},
"items": [
{
"id": "gdpr_cookie_banner_visible",
"label": "$t:editor.state.gdpr_cookie_banner_visible",
"sampleDataOverrides": {
"byDataSourceId": {
"gdprMyConsent": {
"data": {
"has_consented": false,
"consents": {
"analytics": false,
"functional": false,
"marketing": false,
"necessary": true
},
"needs_renewal": false,
"current_policy_version": "1",
"is_member": false
}
}
}
}
}
]
}
]
}
}