Files
Gnuboard7/plugins/_bundled/sirsoft-gdpr/resources/extensions/mypage_privacy_tab.json
T
chym1217 d5b6f4b457 fix(sirsoft-gdpr): 마이페이지 쿠키 동의 철회 시 철회일 미표시 수정
철회 이력(status=revoked)이 있는 항목에 한해 status_at_formatted·
status_label 을 채워 마이페이지에 철회일이 표시되도록 한다. 거부·
미설정 상태는 기존대로 미표시 유지 ( 확인).

동의/철회 날짜 줄을 초록/빨강으로 구분해 시각적으로 대등하게
강조한다 (배지·관리자 GDPR 동의 이력 화면과 동일 톤).
2026-07-30 10:42:03 +09:00

707 lines
55 KiB
JSON

{
"target_layout": "mypage/profile",
"data_sources": [
{
"comment": "회원 동의 이력 (history)",
"id": "gdprMyConsents",
"label_key": "$t:sirsoft-gdpr.editor.data_source.gdprMyConsents",
"type": "api",
"endpoint": "/api/plugins/sirsoft-gdpr/consent/history",
"method": "GET",
"auto_fetch": true,
"auth_required": true,
"loading_strategy": "progressive"
},
{
"comment": "회원 활성 동의 + 정책 버전 갱신 필요 여부",
"id": "gdprMeConsents",
"label_key": "$t:sirsoft-gdpr.editor.data_source.gdprMeConsents",
"type": "api",
"endpoint": "/api/plugins/sirsoft-gdpr/consent/me",
"method": "GET",
"auto_fetch": true,
"auth_required": true,
"loading_strategy": "progressive"
},
{
"comment": "공개 설정 — 운영 주체/저장 위치 메타 라인 표시용 (legal_entity_name, data_storage_location). 카드 헤더 하단 1줄 메타 정보로 노출되며, 둘 다 비어있으면 메타 라인 전체 숨김.",
"id": "gdprPublicSettings",
"label_key": "$t:sirsoft-gdpr.editor.data_source.gdprPublicSettings",
"type": "api",
"endpoint": "/api/plugins/sirsoft-gdpr/settings",
"method": "GET",
"auto_fetch": true,
"auth_required": false,
"loading_strategy": "progressive"
}
],
"modals": [
{
"id": "revoke_confirm_modal",
"type": "composite",
"name": "Modal",
"props": {
"title": "$t:sirsoft-gdpr.mypage.privacy.revoke_confirm_title",
"width": "min(420px, 95vw)",
"closeOnBackdropClick": true
},
"children": [
{
"type": "basic",
"name": "Div",
"props": { "className": "space-y-4" },
"children": [
{
"type": "basic",
"name": "P",
"props": { "className": "text-sm text-gray-700 dark:text-gray-300" },
"text": "$t:sirsoft-gdpr.mypage.privacy.revoke_confirm_message"
},
{
"comment": "철회 대상 표시 — 라벨 + 카테고리 친화 설명. raw key (cookie_functional 등) 는 사용자에게 무의미하여 노출하지 않음. 모달은 별도 컨텍스트라 _global 사용 필수 (부모 _local 미해석)",
"type": "basic",
"name": "Div",
"props": { "className": "rounded-lg bg-gray-50 dark:bg-gray-700 px-3 py-2 text-sm text-gray-700 dark:text-gray-200" },
"children": [
{
"type": "basic",
"name": "Span",
"props": { "className": "block font-medium" },
"text": "{{_global.revokeTargetLabel ?? _global.revokeTargetKey ?? ''}}"
},
{
"type": "basic",
"name": "Span",
"if": "{{!!_global.revokeTargetDescription}}",
"props": { "className": "block text-xs text-gray-500 dark:text-gray-400 mt-0.5 leading-relaxed" },
"text": "{{_global.revokeTargetDescription ?? ''}}"
},
{
"comment": "이슈 #430 — 철회 대상의 마지막 동의일. 사용자가 언제 동의한 것을 철회하는지 인지 (informed). status_at_formatted 를 revokeTargetDate 로 전달받음.",
"type": "basic",
"name": "Span",
"if": "{{!!_global.revokeTargetDate}}",
"props": { "className": "block text-xs text-green-600 dark:text-green-400 mt-1.5" },
"text": "$t:sirsoft-gdpr.mypage.privacy.revoke_target_consented_at|date={{_global.revokeTargetDate ?? ''}}"
}
]
},
{
"type": "basic",
"name": "Div",
"props": { "className": "flex justify-end gap-2" },
"children": [
{
"type": "basic",
"name": "Button",
"props": {
"type": "button",
"disabled": "{{_local.isRevoking}}",
"className": "px-4 py-1.5 text-sm border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors disabled:opacity-50"
},
"actions": [
{ "type": "click", "handler": "closeModal" }
],
"children": [
{ "type": "basic", "name": "Span", "text": "$t:common.cancel" }
]
},
{
"comment": "철회 대상은 _global 참조 (모달 컨텍스트 분리). isRevoking 은 모달 _local 로 충분 — 모달이 직접 set 하고 직접 read 하므로 컨텍스트 일관됨",
"type": "basic",
"name": "Button",
"props": {
"type": "button",
"disabled": "{{_local.isRevoking || !_global.revokeTargetKey}}",
"className": "px-4 py-1.5 text-sm bg-red-600 text-white rounded-lg hover:bg-red-700 font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
},
"actions": [
{
"type": "click",
"handler": "sequence",
"actions": [
{
"handler": "setState",
"params": { "target": "local", "isRevoking": true }
},
{
"handler": "apiCall",
"auth_required": true,
"target": "/api/plugins/sirsoft-gdpr/consent/revoke",
"params": {
"method": "POST",
"body": "{{{ consent_key: _global.revokeTargetKey }}}"
},
"onSuccess": [
{
"handler": "setState",
"params": { "target": "local", "isRevoking": false }
},
{
"handler": "setState",
"params": { "target": "global", "revokeTargetKey": null, "revokeTargetLabel": null, "revokeTargetDescription": null }
},
{ "handler": "refetchDataSource", "params": { "dataSourceId": "gdprMeConsents" } },
{ "handler": "refetchDataSource", "params": { "dataSourceId": "gdprMyConsents" } },
{ "handler": "sirsoft-gdpr.syncConsent" },
{ "handler": "closeModal" },
{ "handler": "toast", "params": { "type": "success", "message": "$t:sirsoft-gdpr.consent.revoked" } }
],
"onError": [
{ "handler": "setState", "params": { "target": "local", "isRevoking": false } },
{ "handler": "toast", "params": { "type": "error", "message": "{{error?.message ?? 'Error'}}" } }
]
}
]
}
],
"children": [
{ "type": "basic", "name": "Icon", "if": "{{_local.isRevoking}}", "props": { "name": "spinner", "size": "sm", "spin": true, "className": "mr-1" } },
{ "type": "basic", "name": "Span", "if": "{{!_local.isRevoking}}", "text": "$t:sirsoft-gdpr.mypage.privacy.revoke" },
{ "type": "basic", "name": "Span", "if": "{{_local.isRevoking}}", "text": "$t:sirsoft-gdpr.consent.revoking" }
]
}
]
}
]
}
]
},
{
"comment": "동의 확인 모달 (이슈 #430 버튼 재설계) — 거부/미설정 항목의 '동의' 버튼 클릭 시. 즉시 grant 대신 한 번 더 확인. 대상은 _global.grantTarget* (모달 컨텍스트 분리).",
"id": "grant_confirm_modal",
"type": "composite",
"name": "Modal",
"props": {
"title": "$t:sirsoft-gdpr.mypage.privacy.grant_confirm_title",
"width": "min(420px, 95vw)",
"closeOnBackdropClick": true
},
"children": [
{
"type": "basic",
"name": "Div",
"props": { "className": "space-y-4" },
"children": [
{
"type": "basic",
"name": "P",
"props": { "className": "text-sm text-gray-700 dark:text-gray-300" },
"text": "$t:sirsoft-gdpr.mypage.privacy.grant_confirm_message"
},
{
"type": "basic",
"name": "Div",
"props": { "className": "rounded-lg bg-gray-50 dark:bg-gray-700 px-3 py-2 text-sm text-gray-700 dark:text-gray-200" },
"children": [
{
"type": "basic",
"name": "Span",
"props": { "className": "block font-medium" },
"text": "$t:sirsoft-gdpr.mypage.privacy.label_suffix|label={{_global.grantTargetLabel ?? _global.grantTargetKey ?? ''}}"
},
{
"type": "basic",
"name": "Span",
"if": "{{!!_global.grantTargetDescription}}",
"props": { "className": "block text-xs text-gray-500 dark:text-gray-400 mt-0.5 leading-relaxed" },
"text": "{{_global.grantTargetDescription ?? ''}}"
}
]
},
{
"type": "basic",
"name": "Div",
"props": { "className": "flex justify-end gap-2" },
"children": [
{
"type": "basic",
"name": "Button",
"props": {
"type": "button",
"disabled": "{{_local.isGranting}}",
"className": "px-4 py-1.5 text-sm border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors disabled:opacity-50"
},
"actions": [
{ "type": "click", "handler": "closeModal" }
],
"children": [
{ "type": "basic", "name": "Span", "text": "$t:common.cancel" }
]
},
{
"type": "basic",
"name": "Button",
"props": {
"type": "button",
"disabled": "{{_local.isGranting || !_global.grantTargetKey}}",
"className": "px-4 py-1.5 text-sm bg-blue-600 text-white rounded-lg hover:bg-blue-700 font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
},
"actions": [
{
"type": "click",
"handler": "sequence",
"actions": [
{
"handler": "setState",
"params": { "target": "local", "isGranting": true }
},
{
"handler": "apiCall",
"auth_required": true,
"target": "/api/plugins/sirsoft-gdpr/consent/grant",
"params": {
"method": "POST",
"body": "{{{ consent_key: _global.grantTargetKey }}}"
},
"onSuccess": [
{
"handler": "setState",
"params": { "target": "local", "isGranting": false }
},
{
"handler": "setState",
"params": { "target": "global", "grantTargetKey": null, "grantTargetLabel": null, "grantTargetDescription": null }
},
{ "handler": "refetchDataSource", "params": { "dataSourceId": "gdprMeConsents" } },
{ "handler": "refetchDataSource", "params": { "dataSourceId": "gdprMyConsents" } },
{ "handler": "sirsoft-gdpr.syncConsent" },
{ "handler": "closeModal" },
{ "handler": "toast", "params": { "type": "success", "message": "$t:sirsoft-gdpr.consent.granted_again" } }
],
"onError": [
{ "handler": "setState", "params": { "target": "local", "isGranting": false } },
{ "handler": "toast", "params": { "type": "error", "message": "{{error?.message ?? 'Error'}}" } }
]
}
]
}
],
"children": [
{ "type": "basic", "name": "Icon", "if": "{{_local.isGranting}}", "props": { "name": "spinner", "size": "sm", "spin": true, "className": "mr-1" } },
{ "type": "basic", "name": "Span", "if": "{{!_local.isGranting}}", "text": "$t:sirsoft-gdpr.mypage.privacy.grant" },
{ "type": "basic", "name": "Span", "if": "{{_local.isGranting}}", "text": "$t:sirsoft-gdpr.consent.granting" }
]
}
]
}
]
}
]
}
],
"injections": [
{
"target_id": "profile_view_bio_card",
"position": "append",
"components": [
{
"id": "gdpr_mypage_privacy_card",
"type": "basic",
"name": "Div",
"comment": "마이페이지 프로필 페이지 — 자기소개/마케팅 카드 다음에 GDPR 동의 매트릭스 카드 (F-04). GDPR Art.7(3) 대칭성 의무: 동의 철회는 동의 부여만큼 쉬워야 함 → 회원에게 동의 데이터가 있을 때만 노출 (빈 카드 노출 방지). Art.7(3) 보장은 *기존 동의가 있는* 회원의 철회권 보장이므로 동의 데이터 없는 신규 회원은 카드 미노출이 정합. LayoutExtensionService 의 'append' 는 array_splice(targetIndex+1) 이라 priority 가 작은 확장이 먼저 삽입되고 나중 확장이 그 자리를 밀어내는 구조 — sirsoft-marketing(100) 다음에 우리가 오게 하려면 우리 priority 가 더 작아야 함. 90 으로 설정.",
"if": "{{(gdprMyConsents?.data?.length ?? 0) > 0 || (gdprMeConsents?.data?.consents?.length ?? 0) > 0}}",
"props": {
"className": "bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6"
},
"responsive": {
"portable": {
"props": { "className": "bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-4" }
}
},
"children": [
{
"id": "gdpr_card_header",
"type": "basic",
"name": "Div",
"props": { "className": "mb-4 pb-3 border-b border-gray-200 dark:border-gray-700" },
"children": [
{
"type": "basic",
"name": "H2",
"props": { "className": "text-base font-semibold text-gray-900 dark:text-white" },
"text": "$t:sirsoft-gdpr.mypage.privacy.title"
}
]
},
{
"comment": "운영 주체·데이터 저장 위치 메타 라인 — GDPR Art.13(1)(a)(f) 의무 고지를 마이페이지에서도 접근 가능하도록 보강. 카드 헤더(타이틀 + 구분선) 다음에 자연어 한 줄로 노출. 3-case 분기: (1) 둘 다 / (2) 운영자만 / (3) 저장위치만. 둘 다 비면 블록 전체 숨김.",
"id": "gdpr_card_operator_meta",
"type": "basic",
"name": "Div",
"if": "{{!!gdprPublicSettings?.data?.legal_entity_name || !!gdprPublicSettings?.data?.data_storage_location}}",
"props": { "className": "mb-4 text-xs text-gray-500 dark:text-gray-400" },
"children": [
{
"type": "basic",
"name": "P",
"if": "{{!!gdprPublicSettings?.data?.legal_entity_name && !!gdprPublicSettings?.data?.data_storage_location}}",
"text": "$t:sirsoft-gdpr.mypage.privacy.operator_and_storage_meta|entity={{gdprPublicSettings?.data?.legal_entity_name ?? ''}}|location={{gdprPublicSettings?.data?.data_storage_location ?? ''}}"
},
{
"type": "basic",
"name": "P",
"if": "{{!!gdprPublicSettings?.data?.legal_entity_name && !gdprPublicSettings?.data?.data_storage_location}}",
"text": "$t:sirsoft-gdpr.mypage.privacy.operator_only_meta|entity={{gdprPublicSettings?.data?.legal_entity_name ?? ''}}"
},
{
"type": "basic",
"name": "P",
"if": "{{!gdprPublicSettings?.data?.legal_entity_name && !!gdprPublicSettings?.data?.data_storage_location}}",
"text": "$t:sirsoft-gdpr.mypage.privacy.storage_only_meta|location={{gdprPublicSettings?.data?.data_storage_location ?? ''}}"
}
]
},
{
"id": "gdpr_needs_renewal_banner",
"type": "basic",
"name": "Div",
"if": "{{gdprMeConsents?.data?.needs_renewal === true}}",
"comment": "정책 갱신 안내 (#22 옵션 C) — 현재 정책 버전 노출 + 전체 다시 동의 액션. 필수 쿠키는 ePrivacy Art.5(3) 면제로 갱신 대상 아님을 hint 로 명시.",
"props": { "className": "mb-4 rounded-lg border border-amber-300 dark:border-amber-700 bg-amber-50 dark:bg-amber-900/20 px-4 py-3 flex flex-col gap-2 sm:flex-row sm:items-center" },
"children": [
{
"type": "basic",
"name": "Div",
"props": { "className": "flex items-center gap-3 flex-1" },
"children": [
{
"type": "basic",
"name": "Icon",
"props": { "name": "triangle-exclamation", "size": "sm", "className": "text-amber-600 dark:text-amber-400 flex-shrink-0" }
},
{
"type": "basic",
"name": "Div",
"props": { "className": "flex-1" },
"children": [
{
"type": "basic",
"name": "P",
"comment": "현재 정책 버전을 노출하여 사용자가 어느 버전에 동의하는지 인지 (#23 — informed consent). G7 표준 $t:key|param={{value}} 토큰 사용 — lang 파일의 {version} placeholder 가 치환됨.",
"props": { "className": "text-xs text-yellow-700 dark:text-yellow-300" },
"text": "$t:sirsoft-gdpr.mypage.privacy.needs_renewal_with_version|version={{gdprMeConsents?.data?.current_policy_version ?? '-'}}"
},
{
"type": "basic",
"name": "P",
"comment": "필수 쿠키는 ePrivacy Art.5(3) 면제로 정책 갱신 대상이 아님을 명시.",
"props": { "className": "mt-1 text-xs text-yellow-700 dark:text-yellow-300" },
"text": "$t:sirsoft-gdpr.mypage.privacy.needs_renewal_hint"
}
]
}
]
},
{
"type": "basic",
"name": "Div",
"props": { "className": "flex items-center flex-wrap gap-2 sm:flex-shrink-0" },
"children": [
{
"type": "basic",
"name": "Button",
"comment": "전체 항목 다시 동의 (#19) — 활성 선택형 동의를 새 버전으로 일괄 grant (재동의 의사 표명). isRenewingAll 플래그로 클릭 직후 spinner 노출 + 중복 클릭 차단.",
"props": {
"type": "button",
"disabled": "{{_local.isRenewingAll}}",
"className": "px-3 py-1.5 text-xs bg-amber-600 dark:bg-amber-700 text-white rounded hover:bg-amber-700 dark:hover:bg-amber-600 font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed inline-flex items-center"
},
"actions": [
{
"type": "click",
"handler": "sequence",
"actions": [
{ "handler": "setState", "params": { "target": "local", "isRenewingAll": true } },
{
"handler": "apiCall",
"auth_required": true,
"target": "/api/plugins/sirsoft-gdpr/consent/renew-all",
"params": { "method": "POST" },
"onSuccess": [
{ "handler": "setState", "params": { "target": "local", "isRenewingAll": false } },
{ "handler": "refetchDataSource", "params": { "dataSourceId": "gdprMeConsents" } },
{ "handler": "refetchDataSource", "params": { "dataSourceId": "gdprMyConsents" } },
{ "handler": "sirsoft-gdpr.syncConsent" },
{ "handler": "toast", "params": { "type": "success", "message": "$t:sirsoft-gdpr.mypage.privacy.renew_all_success|renewed={{response?.data?.renewed ?? 0}}" } }
],
"onError": [
{ "handler": "setState", "params": { "target": "local", "isRenewingAll": false } },
{ "handler": "toast", "params": { "type": "error", "message": "{{error?.message ?? 'Error'}}" } }
]
}
]
}
],
"children": [
{ "type": "basic", "name": "Icon", "if": "{{_local.isRenewingAll}}", "props": { "name": "spinner", "size": "xs", "spin": true, "className": "mr-1" } },
{ "type": "basic", "name": "Span", "if": "{{!_local.isRenewingAll}}", "text": "$t:sirsoft-gdpr.mypage.privacy.btn_renew_all" },
{ "type": "basic", "name": "Span", "if": "{{_local.isRenewingAll}}", "text": "$t:sirsoft-gdpr.consent.renewing" }
]
}
]
}
]
},
{
"id": "gdpr_section_consents",
"type": "basic",
"name": "Div",
"comment": "내 동의 현황 — 회원 활성 동의 (status) 기반. admin 전용 toggle-group 클래스 → 공통 border/rounded 로 치환 (user 템플릿 미해석 회귀 수정, 이슈 #430).",
"props": { "className": "border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden" },
"children": [
{
"type": "basic",
"name": "Div",
"props": { "className": "px-4 py-3 bg-gray-50 dark:bg-gray-700/50" },
"children": [
{
"type": "basic",
"name": "H3",
"props": { "className": "text-gray-900 dark:text-white text-sm font-semibold" },
"text": "$t:sirsoft-gdpr.mypage.privacy.section_consents"
},
{
"type": "basic",
"name": "P",
"props": { "className": "text-xs text-gray-500 dark:text-gray-400 mt-1" },
"text": "$t:sirsoft-gdpr.mypage.privacy.section_consents_hint"
}
]
},
{
"type": "basic",
"name": "Div",
"if": "{{(gdprMeConsents?.data?.consents ?? []).length === 0}}",
"props": { "className": "py-10 text-center text-sm text-gray-500 dark:text-gray-400" },
"text": "$t:sirsoft-gdpr.mypage.privacy.no_consents"
},
{
"comment": "「모두 동의」 일괄 동의 바 — 선택형(is_required=false) 중 미동의(is_consented=false) 항목이 하나라도 있을 때만 노출. 필수는 대상 외. renew-all 이 아니라 신규/재동의 축이므로 grant 를 항목별로 반복 호출하는 대신, 서버 renew-all 은 '이미 활성 항목의 정책 갱신' 전용이라 여기서는 각 미동의 선택형 항목을 grant 로 순차 처리한다. 다만 레이아웃 반복 호출은 표현이 어렵기에, 배너의 '모두 동의' 와 동일하게 consent/cookie(intent 없음, 전체 true) 로 한 번에 처리.",
"type": "basic",
"name": "Div",
"if": "{{(gdprMeConsents?.data?.consents ?? []).filter(c => c?.is_required !== true && c?.is_consented !== true).length > 0}}",
"props": { "className": "flex items-center justify-between gap-3 px-4 py-3 bg-blue-50 dark:bg-blue-900/20 border-b border-blue-100 dark:border-blue-800" },
"children": [
{
"type": "basic",
"name": "Span",
"props": { "className": "text-sm text-blue-800 dark:text-blue-200" },
"text": "$t:sirsoft-gdpr.mypage.privacy.enable_all_hint"
},
{
"type": "basic",
"name": "Button",
"props": {
"type": "button",
"disabled": "{{!!_local.enablingAll}}",
"className": "shrink-0 px-3 py-1.5 text-xs bg-blue-600 text-white rounded hover:bg-blue-700 font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed inline-flex items-center"
},
"actions": [
{
"type": "click",
"handler": "sequence",
"actions": [
{ "handler": "setState", "params": { "target": "local", "enablingAll": true } },
{
"handler": "apiCall",
"auth_required": true,
"target": "/api/plugins/sirsoft-gdpr/consent/cookie",
"params": {
"method": "POST",
"body": "{{{ source: 'mypage', consents: (gdprMeConsents?.data?.consents ?? []).reduce((acc, c) => (c?.is_required === true ? acc : { ...acc, [c.consent_key]: true }), {}) }}}"
},
"onSuccess": [
{ "handler": "setState", "params": { "target": "local", "enablingAll": null } },
{ "handler": "refetchDataSource", "params": { "dataSourceId": "gdprMeConsents" } },
{ "handler": "refetchDataSource", "params": { "dataSourceId": "gdprMyConsents" } },
{ "handler": "sirsoft-gdpr.syncConsent" },
{ "handler": "toast", "params": { "type": "success", "message": "$t:sirsoft-gdpr.mypage.privacy.enable_all_success" } }
],
"onError": [
{ "handler": "setState", "params": { "target": "local", "enablingAll": null } },
{ "handler": "toast", "params": { "type": "error", "message": "{{error?.message ?? 'Error'}}" } }
]
}
]
}
],
"children": [
{ "type": "basic", "name": "Icon", "if": "{{!!_local.enablingAll}}", "props": { "name": "spinner", "size": "xs", "spin": true, "className": "mr-1" } },
{ "type": "basic", "name": "Span", "text": "$t:sirsoft-gdpr.mypage.privacy.enable_all" }
]
}
]
},
{
"comment": "항목 리스트 (이슈 #430 재설계) — 상태 배지 + 동의/철회 버튼 방식. 필수=상태 배지 '항상 적용' + 잠금 버튼(액션 없음), 선택=상태 배지(동의함/거부함/미설정) + 동의·철회 버튼(확인 모달 경유). 동의일 컬럼은 사용자 화면에서 제거(동의한 항목만 라벨 아래 날짜 표시, 감사 로그 전체는 관리자 화면에만 존재). 초기 설계였던 on/off 스위치는 user 템플릿 스타일 회귀 + PO 지시로 배지+버튼 방식으로 대체됨(2026-07-21).",
"type": "basic",
"name": "Div",
"if": "{{(gdprMeConsents?.data?.consents ?? []).length > 0}}",
"props": { "className": "divide-y divide-gray-100 dark:divide-gray-700" },
"iteration": {
"source": "gdprMeConsents?.data?.consents ?? []",
"item_var": "consent",
"index_var": "consentIdx"
},
"children": [
{
"type": "basic",
"name": "Div",
"props": { "className": "flex items-center justify-between gap-4 px-4 py-4" },
"children": [
{
"type": "basic",
"name": "Div",
"comment": "라벨 + 사용자 친화 1줄 설명 (raw key 대신 카테고리 의미 노출).",
"props": { "className": "min-w-0 flex-1" },
"children": [
{
"type": "basic",
"name": "Div",
"props": { "className": "flex items-center gap-2 flex-wrap" },
"children": [
{
"comment": "라벨 뒤에 '쿠키' 접미사 (이슈 #430) — label_suffix='{label} 쿠키'. 운영자 카탈로그 라벨이 '기능' 이어도 화면엔 '기능 쿠키' 로 노출.",
"type": "basic",
"name": "Span",
"props": { "className": "text-gray-900 dark:text-white font-medium text-sm" },
"text": "$t:sirsoft-gdpr.mypage.privacy.label_suffix|label={{consent?.consent_label ?? consent?.consent_key ?? ''}}"
},
{
"type": "basic",
"name": "Span",
"if": "{{consent?.is_required === true}}",
"props": { "className": "inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-gray-100 dark:bg-gray-700 text-gray-500 dark:text-gray-400" },
"text": "$t:sirsoft-gdpr.mypage.privacy.required_label"
},
{
"comment": "이슈 #430 (3안) — 상태 배지를 라벨 옆으로. 서버 status_badge_label (필수=항상 적용 / 동의함 / 거부함 / 미설정). status 로 색 분기.",
"type": "basic",
"name": "Span",
"props": { "className": "inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-semibold {{consent?.status === 'consented' ? 'bg-green-50 text-green-700 dark:bg-green-900/30 dark:text-green-300' : (consent?.status === 'required' ? 'bg-blue-50 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300' : 'bg-gray-100 text-gray-500 dark:bg-gray-700 dark:text-gray-400')}}" },
"text": "{{consent?.status_badge_label ?? ''}}"
}
]
},
{
"type": "basic",
"name": "Span",
"if": "{{!!consent?.consent_description}}",
"props": { "className": "block text-xs text-gray-500 dark:text-gray-400 mt-1 leading-relaxed" },
"text": "{{consent?.consent_description ?? ''}}"
},
{
"comment": "이슈 #430 (3안) + #509 16번 — 라벨 아래 이력 줄: '날짜 + 동의/철회'. 동의=초록, 철회=빨강으로 둘 다 강조해 균형을 맞춘다 (배지의 동의함=green-700/green-300, 관리자 「GDPR 동의 이력」의 revoked=red 관례와 동일 톤). 서버가 동의·철회 항목만 status_label·status_at_formatted 를 채워 내려주므로(거부/미설정은 null) 둘 다 있을 때만 렌더.",
"type": "basic",
"name": "Div",
"if": "{{!!consent?.status_label && !!consent?.status_at_formatted}}",
"props": { "className": "mt-1.5 text-xs font-medium {{consent?.status === 'revoked' ? 'text-red-600 dark:text-red-400' : 'text-green-600 dark:text-green-400'}}" },
"children": [
{ "type": "basic", "name": "Span", "text": "{{consent?.status_at_formatted ?? ''}}" },
{ "type": "basic", "name": "Span", "props": { "className": "ml-1" }, "text": "{{consent?.status_label ?? ''}}" }
]
}
]
},
{
"comment": "이슈 #430 (3안) — 오른쪽 클러스터: 단일 액션 버튼만. 상태 배지는 라벨 옆으로 이동. 필수=잠금버튼(액션X), 동의함=철회버튼(철회 모달), 그 외=동의버튼(동의 모달).",
"type": "basic",
"name": "Div",
"props": { "className": "shrink-0 flex items-center" },
"children": [
{
"comment": "필수 — 잠금 버튼 (비활성, 액션 없음). ePrivacy Art.5(3) 면제.",
"type": "basic",
"name": "Button",
"if": "{{consent?.is_required === true}}",
"props": {
"type": "button",
"disabled": true,
"className": "inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium border border-gray-200 dark:border-gray-700 text-gray-400 dark:text-gray-500 cursor-not-allowed"
},
"children": [
{ "type": "basic", "name": "Icon", "props": { "name": "lock", "size": "xs" } },
{ "type": "basic", "name": "Span", "text": "$t:sirsoft-gdpr.mypage.privacy.locked_label" }
]
},
{
"comment": "동의함(선택) — 철회 버튼. 클릭 시 철회 확인 모달 (즉시 철회 아님, 오조작 방지).",
"type": "basic",
"name": "Button",
"if": "{{consent?.is_required !== true && consent?.is_consented === true}}",
"props": {
"type": "button",
"aria-label": "{{consent?.consent_label ?? consent?.consent_key}}",
"className": "inline-flex items-center px-3 py-1.5 rounded-lg text-xs font-semibold border border-red-200 dark:border-red-800 text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors"
},
"actions": [
{
"type": "click",
"handler": "sequence",
"actions": [
{
"handler": "setState",
"params": {
"target": "global",
"revokeTargetKey": "{{consent?.consent_key}}",
"revokeTargetLabel": "{{consent?.consent_label ?? consent?.consent_key}}",
"revokeTargetDescription": "{{consent?.consent_description ?? ''}}",
"revokeTargetDate": "{{consent?.status_at_formatted ?? ''}}"
}
},
{ "handler": "openModal", "target": "revoke_confirm_modal" }
]
}
],
"children": [
{ "type": "basic", "name": "Span", "text": "$t:sirsoft-gdpr.mypage.privacy.revoke" }
]
},
{
"comment": "거부함/미설정(선택) — 동의 버튼. 클릭 시 동의 확인 모달 (신규, 즉시 grant 아님).",
"type": "basic",
"name": "Button",
"if": "{{consent?.is_required !== true && consent?.is_consented !== true}}",
"props": {
"type": "button",
"aria-label": "{{consent?.consent_label ?? consent?.consent_key}}",
"className": "inline-flex items-center px-3 py-1.5 rounded-lg text-xs font-semibold bg-blue-600 text-white hover:bg-blue-700 transition-colors"
},
"actions": [
{
"type": "click",
"handler": "sequence",
"actions": [
{
"handler": "setState",
"params": {
"target": "global",
"grantTargetKey": "{{consent?.consent_key}}",
"grantTargetLabel": "{{consent?.consent_label ?? consent?.consent_key}}",
"grantTargetDescription": "{{consent?.consent_description ?? ''}}"
}
},
{ "handler": "openModal", "target": "grant_confirm_modal" }
]
}
],
"children": [
{ "type": "basic", "name": "Span", "text": "$t:sirsoft-gdpr.mypage.privacy.grant" }
]
}
]
}
]
}
]
}
]
}
]
}
]
}
],
"priority": 90
}