diff --git a/lang-packs/_bundled/g7-plugin-sirsoft-gdpr-ja/CHANGELOG.md b/lang-packs/_bundled/g7-plugin-sirsoft-gdpr-ja/CHANGELOG.md index 325a801a..f3eca31d 100644 --- a/lang-packs/_bundled/g7-plugin-sirsoft-gdpr-ja/CHANGELOG.md +++ b/lang-packs/_bundled/g7-plugin-sirsoft-gdpr-ja/CHANGELOG.md @@ -10,3 +10,8 @@ - 쿠키 동의 배너·약관·데이터 요청 등 사용자 화면 프론트엔드 텍스트 일본어 번역 추가 — 관련 화면이 일본어 로케일에서 자연스럽게 표시됩니다. - GDPR 플러그인(sirsoft-gdpr)의 일본어 번들 언어팩 초기 제공 +- 쿠키 거부 기능 및 마이페이지 「쿠키 동의 관리」 재설계에 따른 신규 문구(상태 배지, 동의·철회 확인 모달, 거부 안내 등) 일본어 번역 추가 + +### Changed + +- 초기 토글 방식 설계에서 쓰였던 미사용 문구(항상 켜짐/켜짐/꺼짐) 번역 제거 diff --git a/lang-packs/_bundled/g7-plugin-sirsoft-gdpr-ja/backend/ja/messages.php b/lang-packs/_bundled/g7-plugin-sirsoft-gdpr-ja/backend/ja/messages.php index e39ea4b7..7aa37c5f 100644 --- a/lang-packs/_bundled/g7-plugin-sirsoft-gdpr-ja/backend/ja/messages.php +++ b/lang-packs/_bundled/g7-plugin-sirsoft-gdpr-ja/backend/ja/messages.php @@ -4,13 +4,14 @@ return [ 'consent' => [ 'granted' => '同意が保存されました。', 'granted_again' => '同意が更新されました。', - 'revoked' => '同意が撤回されました。', - 'renew_all_success' => 'アクティブな項目 {renewed} 個を新しいポリシーバージョンに更新しました。', + 'rejected_saved' => '同意せずに続行します。選択項目は使用されません。', + 'revoked' => '同意が取り消されました。', + 'renew_all_success' => '有効な項目 {renewed} 個を新しいポリシーバージョンに更新しました。', 'granting' => '同意中...', - 'revoking' => '撤回中...', + 'revoking' => '取り消し中...', 'renewing' => '更新中...', 'already_same_state' => '既に同じ状態です。', - 'required_cannot_revoke' => '必須項目は撤回できません。', + 'required_cannot_revoke' => '必須項目は取り消すことができません。', 'invalid_key' => '無効な同意項目です。', 'category_cookie_necessary' => '必須クッキー', 'category_cookie_functional' => '機能クッキー', @@ -18,35 +19,35 @@ return [ 'category_cookie_marketing' => 'マーケティングクッキー', ], 'blocked_domains' => [ - 'invalid_format' => '正しいドメイン形式ではありません。(FQDNのみサポート、*.example.comワイルドカード可能)', - 'invalid_format_functional' => '機能カテゴリ — 正しいドメイン形式ではありません。(FQDNのみサポート、*.example.comワイルドカード可能)', - 'invalid_format_analytics' => '分析カテゴリ — 正しいドメイン形式ではありません。(FQDNのみサポート、*.example.comワイルドカード可能)', - 'invalid_format_marketing' => 'マーケティングカテゴリ — 正しいドメイン形式ではありません。(FQDNのみサポート、*.example.comワイルドカード可能)', - 'too_long' => 'ドメインは253文字を超えることはできません。', - 'too_long_functional' => '機能カテゴリ — ドメインは253文字を超えることはできません。', - 'too_long_analytics' => '分析カテゴリ — ドメインは253文字を超えることはできません。', - 'too_long_marketing' => 'マーケティングカテゴリ — ドメインは253文字を超えることはできません。', - 'must_be_array' => 'カテゴリ別ドメイン一覧は配列である必要があります。', + 'invalid_format' => '正しいドメイン形式ではありません。(FQDN のみサポート、*.example.com ワイルドカード対応)', + 'invalid_format_functional' => '機能カテゴリ — 正しいドメイン形式ではありません。(FQDN のみサポート、*.example.com ワイルドカード対応)', + 'invalid_format_analytics' => '分析カテゴリ — 正しいドメイン形式ではありません。(FQDN のみサポート、*.example.com ワイルドカード対応)', + 'invalid_format_marketing' => 'マーケティングカテゴリ — 正しいドメイン形式ではありません。(FQDN のみサポート、*.example.com ワイルドカード対応)', + 'too_long' => 'ドメインは 253 文字を超えることができません。', + 'too_long_functional' => '機能カテゴリ — ドメインは 253 文字を超えることができません。', + 'too_long_analytics' => '分析カテゴリ — ドメインは 253 文字を超えることができません。', + 'too_long_marketing' => 'マーケティングカテゴリ — ドメインは 253 文字を超えることができません。', + 'must_be_array' => 'カテゴリごとのドメインリストは配列である必要があります。', ], 'settings' => [ 'saved' => '設定が保存されました。', 'title' => 'GDPR 設定', - 'description' => 'クッキー同意バナーとマイページ同意管理カードの動作を管理者が制御します。', + 'description' => 'クッキー同意バナーと マイページ同意管理カードの動作を管理者が制御します。', 'section' => [ 'operator' => '運営主体 / プライバシーポリシー', - 'operator_desc' => 'クッキーバナー·ポリシーページに表示される運営主体、データ保存位置、ポリシーページスラッグを登録します。(GDPR Art.13 透明性義務)', + 'operator_desc' => 'クッキーバナー・ポリシーページに表示される運営主体、データ保存場所、ポリシーページスラッグを登録します。(GDPR Art.13 透明性義務)', 'cookie_banner' => 'クッキー同意バナー', 'cookie_banner_desc' => '訪問者に表示されるクッキー同意バナーの表示/位置/カテゴリ/ポリシーバージョンを一箇所で設定します。', 'cookie_banner_settings' => 'バナー表示設定', 'cookie_categories' => 'クッキーカテゴリ', 'cookie_policy_version' => 'クッキーポリシーバージョン', - 'auto_blocking' => '自動ブロックポリシー', - 'auto_blocking_desc' => 'クッキーバナーの表示が ON のときは、以下のカテゴリ別ブロックドメイン一覧の外部トラッキングリソースがユーザーの同意前まで自動ブロックされます。(個別トグルなし — バナー表示と共に自動作動)', + 'auto_blocking' => '自動ブロック設定', + 'auto_blocking_desc' => 'クッキーバナー表示が ON の場合、下記のカテゴリごとのブロックドメインリストの外部トラッキングリソースがユーザー同意前まで自動ブロックされます。(別途トグルなし — バナー表示と共に自動稼働)', ], 'nav' => [ 'operator' => '運営情報', 'cookie_banner' => 'クッキーバナー', - 'auto_blocking_policy' => '自動ブロックポリシー', + 'auto_blocking_policy' => '自動ブロック設定', ], 'status' => [ 'enabled' => '有効化', @@ -58,28 +59,28 @@ return [ 'cookie_policy_version' => [ 'label' => 'ポリシーバージョン', 'hint' => '管理者が直接入力せず、システムが自動管理します。 -• カテゴリ追加/削除、カテゴリ意味変更、ポリシーページ変更 → 新バージョン自動公開 + すべての会員再同意 -• ドメイン追加/削除、ラベル変更など単純変更 → 公開なし', +• カテゴリの追加/削除、カテゴリの意味の変更、ポリシーページの変更 → 新バージョン自動公開 + すべての会員に再同意 +• ドメインの追加/削除、ラベル変更など単純な変更 → 公開なし', ], 'privacy_policy_slug' => [ 'label' => 'プライバシーポリシーページスラッグ', - 'hint' => 'ページプラグインに登録されたプライバシーポリシーページのスラッグです。空の場合、クッキーバナーのポリシーリンクは表示されません。', + 'hint' => 'ページプラグインに登録されたプライバシーポリシーページのスラッグ。空の場合、クッキーバナーのポリシーリンクが表示されません。', 'placeholder' => '例: privacy-policy', ], 'legal_entity_name' => [ 'label' => '法人名 / 運営主体', - 'hint' => 'クッキーバナーとマイページに表示されるサイト運営主体名です。', - 'placeholder' => '例: ○○会社', + 'hint' => 'クッキーバナーと マイページに表示されるサイト運営主体名です。', + 'placeholder' => '例: (株)会社名', ], 'data_storage_location' => [ - 'label' => 'データ保存位置', - 'hint' => 'ユーザーに案内するデータ保存国を入力してください。例:「日本」「米国 (AWS)」。セキュリティ上、IPアドレス、クラウドリージョンコード(例: ap-northeast-2)、データセンターアドレスは入力しないでください。', - 'placeholder' => '例: 日本 (自社データセンター) / 米国 (AWS)', - 'sensitive_format' => 'データ保存位置に IPアドレス、CIDR、クラウドリージョンコード(例: ap-northeast-2) は入力できません。国名(例: 日本、米国) で表記してください。', + 'label' => 'データ保存場所', + 'hint' => 'ユーザーに通知するデータ保存国を入力してください。例: "日本"、"アメリカ (AWS)"。IP アドレス、クラウドリージョンコード(例: ap-northeast-2)、データセンターアドレスはセキュリティ上入力しないでください。', + 'placeholder' => '例: 日本 (自社データセンター) / アメリカ (AWS)', + 'sensitive_format' => 'データ保存場所に IP アドレス、CIDR、クラウドリージョンコード(例: ap-northeast-2) は入力できません。国名(例: 日本、アメリカ) で記載してください。', ], 'banner_enabled' => [ 'label' => 'クッキーバナー表示', - 'hint' => '有効にするとクッキー同意バナー表示 + 同意前外部トラッキング自動ブロック + マイページ同意管理カードが一括有効になります。(GDPR Art.6 「同意前処理禁止」の強制メカニズムであるブロックを個別トグルでオフにできないよう単一トグルに統合)', + 'hint' => '有効化すると、クッキー同意バナー表示 + 同意前の外部トラッキング自動ブロック + マイページ同意管理カードが一括有効化されます。(GDPR Art.6 "同意前処理禁止" の強制メカニズムであるブロックを個別トグルで無効化できないよう、単一トグルに統合)', ], 'banner_position' => [ 'label' => 'バナー位置', @@ -90,12 +91,12 @@ return [ 'option_centered_modal' => '中央モーダル', ], 'blocked_domains' => [ - 'warnings_title' => 'お知らせ', - 'auto_blocking_off_hint' => '自動ブロックはクッキーバナー表示が ON のときのみ作動します。「クッキーバナー」カードでクッキーバナー表示トグルをまず有効にしてください。', - 'warning_visual_break' => '外部フォント·スタイルシートは分類に注意してください。誤ってブロックするとページが静かに壊れる可能性があります。', + 'warnings_title' => '注意', + 'auto_blocking_off_hint' => '自動ブロックはクッキーバナー表示が ON の場合のみ稼働します。「クッキーバナー」カードでクッキーバナー表示トグルをまずオンにしてください。', + 'warning_visual_break' => '外部フォント・スタイルシートは分類に注意してください。誤ってブロックするとページが静かに破損する可能性があります。', 'warning_policy_bump' => 'ブロックドメインを変更した後、クッキーポリシーバージョンを上げてください。既存の同意者が新しいドメインにも同意したものとして処理される問題を防ぎます。', - 'warning_domain_format' => 'ドメイン形式: ドット1個以上含むドメイン。ワイルドカード *.example.com 可能。シングルラベル(localhost)および韓国語ドメイン(xn-- 変換)非サポート。', - 'tag_input_placeholder' => 'ドメイン入力後 Enter (例: google-analytics.com, *.hotjar.com)', + 'warning_domain_format' => 'ドメイン形式: ドット 1 個以上を含むドメイン。ワイルドカード *.example.com 対応。シングルラベル(localhost) および多言語ドメイン(xn-- 変換) は未対応。', + 'tag_input_placeholder' => 'ドメイン入力後 Enter (例: google-analytics.com、*.hotjar.com)', 'tag_no_options' => '推奨ドメインはありません。直接入力してください。', 'category' => [ 'functional' => '機能カテゴリブロックドメイン', @@ -104,13 +105,13 @@ return [ ], ], 'cookie_categories' => [ - 'description' => '4種類のクッキーカテゴリの用途とブロック対象ツール例は、各カードの[詳細を表示]からご確認いただけます。', + 'description' => 'クッキーカテゴリ 4 種類の用途とブロックツール例は、各カードの [情報を展開] で確認できます。', 'category_required_badge' => '必須', - 'category_optional_badge' => '任意', + 'category_optional_badge' => '選択', ], ], 'save_success' => '設定が保存されました。', - 'save_error' => '設定の保存中にエラーが発生しました。', + 'save_error' => '設定保存中にエラーが発生しました。', 'validation_error' => '入力値を確認してください。', 'policy_version' => [ 'current_label' => '現在のバージョン', @@ -122,7 +123,7 @@ return [ 'publishing' => '公開中...', 'change_type' => [ 'material' => '再同意トリガー', - 'non_material' => '軽微な変更', + 'non_material' => '簡単な変更', 'initial' => '初期公開', ], 'history_modal' => [ @@ -142,27 +143,27 @@ return [ 'snapshot_view_short' => '表示', 'material_modal' => [ 'title' => 'ポリシーバージョン公開 + 再同意トリガー', - 'body' => '次の変更により、ポリシーバージョンが自動公開され、すべての会員の再同意トリガーが発生します。', - 'effect_bump' => 'ポリシーバージョン自動公開(単調増加)', - 'effect_reconsent' => 'すべての会員の次回訪問時に再同意画面を表示', - 'effect_history' => '履歴モーダルに新しいバージョンの行を追加', - 'memo_label' => '変更理由(必須)', - 'memo_placeholder' => '例:分析カテゴリ新設(Hotjar導入)', + 'body' => '次の変更は、ポリシーバージョンの自動公開とすべての会員の再同意トリガーを発生させます。', + 'effect_bump' => 'ポリシーバージョンの自動公開(単調増加)', + 'effect_reconsent' => 'すべての会員の次回訪問時に再同意画面が表示されます', + 'effect_history' => '履歴モーダルに新しいバージョン行を追加', + 'memo_label' => '変更理由(必須)', + 'memo_placeholder' => '例:分析カテゴリ新設(Hotjar導入)', 'confirm' => '新バージョン公開後に保存', ], 'snapshot_modal' => [ 'title' => 'ポリシーバージョン v:version 本文', - 'description' => '公開時点の設定のカテゴリ一覧、ポリシー本文スラッグ、ブロック対象ドメインなど全体の本文です。DPOが同意紛争時の証拠資料として活用します。', + 'description' => '公開時点での設定のカテゴリリスト・ポリシー本文スラッグ・ブロックドメイン等、全体の本文です。DPO が同意紛争時の立証資料として活用します。', 'section_categories' => 'クッキーカテゴリ', 'section_privacy_policy' => 'ポリシー本文スラッグ', - 'section_blocked_domains' => 'ブロック対象ドメインカタログ', + 'section_blocked_domains' => 'ブロックドメインカタログ', 'category_required_badge' => '必須', - 'no_categories' => '登録されたカテゴリはありません。', - 'no_blocked_domains' => '登録されたブロック対象ドメインはありません。', + 'no_categories' => '登録されたカテゴリがありません。', + 'no_blocked_domains' => '登録されたブロックドメインがありません。', 'view_button' => 'v:version 本文を表示', 'close' => '閉じる', 'open_policy_page' => '本文ページを開く', - 'no_policy_slug' => '登録されたポリシー本文ページはありません。', + 'no_policy_slug' => '登録されたポリシー本文ページがありません。', ], 'not_found' => '該当するポリシーバージョンが見つかりません。', ], @@ -170,16 +171,16 @@ return [ 'admin' => [ 'consent_log' => [ 'title' => 'GDPR同意履歴', - 'description' => '会員・ゲストの同意・撤回の変更履歴を照会します。IP、User-Agentなどの監査情報も表示されます。', + 'description' => '会員・ゲストの同意・撤回変更履歴を照会します。IP·User-Agent等の監査情報が一緒に表示されます。', 'refresh' => '更新', - 'empty' => '照会された同意履歴はありません。', + 'empty' => '照会された同意履歴がありません。', 'clear_filters' => 'フィルターをリセット', 'guest_label' => 'ゲスト', - 'view_member_tooltip' => '会員詳細ページへ移動', + 'view_member_tooltip' => '会員詳細ページに移動', 'search_placeholder' => 'メールアドレスまたはセッションIDで検索', 'search_button' => '検索', 'search_type' => [ - 'email' => 'メールアドレス', + 'email' => 'メール', 'session_id' => 'セッションID', ], 'filter' => [ @@ -191,6 +192,7 @@ return [ 'action' => [ 'granted' => '同意', 'revoked' => '撤回', + 'rejected' => '拒否', ], 'source' => [ 'banner' => 'バナー', @@ -212,7 +214,7 @@ return [ 'categories_label' => 'カテゴリスナップショット', 'categories_hint' => '同意時点で会員がカテゴリ別に示した意思です。ポリシー本文が更新されても、この記録は不変として保存されます。', 'snapshot_granted' => '同意', - 'snapshot_revoked' => '拒否', + 'snapshot_revoked' => '非同意', ], ], ], @@ -221,29 +223,42 @@ return [ 'title' => 'クッキー同意管理', 'description' => 'サイトが使用中のクッキー項目に対する同意を変更または撤回できます。', 'section_consents' => '私の同意状況', - 'section_consents_hint' => '同意・撤回時点に応じて即座に反映されます。必須項目は撤回することはできません。', + 'section_consents_hint' => '同意/撤回時点に従ってすぐに反映されます。必須項目は撤回できません。', 'col_consent_key' => '同意項目', 'col_consented' => 'ステータス', 'col_consented_at' => '同意日', 'col_revoked_at' => '撤回日', 'col_policy_version' => 'ポリシーバージョン', - 'col_action' => '操作', + 'col_action' => '作業', 'consented' => '同意', - 'revoked' => '撤回', - 'revoke' => '撤回', + 'revoked' => '取消', + 'rejected_label' => '拒否', + 'status' => [ + 'granted' => '同意', + 'consented' => '同意', + 'revoked' => '取消', + 'rejected' => '拒否', + ], + 'badge' => [ + 'required' => '常に適用', + 'consented' => '同意しました', + 'rejected' => '拒否しました', + 'unset' => '未設定', + ], + 'revoke' => '取消', 'grant' => '同意', 'grant_again' => '再度同意', 'required_label' => '必須', - 'required_hint' => 'サービス提供に必須のため撤回できません。', - 'revoke_confirm_title' => '同意撤回確認', - 'revoke_confirm_message' => 'この項目を撤回されますか?同意を撤回するとその カテゴリ関連機能が制限される可能性があります。', - 'no_consents' => '有効な同意項目がありません。', + 'required_hint' => 'サービス提供に必須であるため、取消することはできません。', + 'revoke_confirm_title' => '同意取消確認', + 'revoke_confirm_message' => 'このアイテムを取消しますか? 同意を取消すると、該当するカテゴリーの関連機能が制限される可能性があります。', + 'no_consents' => 'アクティブな同意項目がありません。', 'needs_renewal' => 'クッキーポリシーが変更されました。同意を更新してください。', 'needs_renewal_with_version' => 'クッキーポリシーがバージョン {version} に変更されました。選択項目の同意を再度確認してください。', - 'needs_renewal_hint' => '必須クッキーはサイト運営に不可欠な項目のため、ポリシー変更に関わらず常に適用されます。', + 'needs_renewal_hint' => '必須クッキーはサイト運営に必ず必要な項目のため、ポリシー変更の有無に関わらず常に適用されます。', 'btn_renew_all' => '現在の同意更新', 'btn_renew_this_item' => 'ポリシー更新', - 'renew_all_success' => '有効な項目 {renewed} 個を新しいポリシーバージョンに更新しました。', + 'renew_all_success' => 'アクティブな項目 {renewed} 個を新しいポリシーバージョンに更新しました。', ], ], 'common' => [ @@ -254,15 +269,15 @@ return [ 're_appear_reason' => 'クッキーポリシーがバージョン :version に変更されたため、同意を再度確認します。', 'consent_required_hint' => '拒否した項目に関連する機能が制限される可能性があります。', 'btn_renew_consent' => '現在の同意更新', - 'title' => 'クッキー使用ガイド', - 'description' => '本サイトはユーザー体験の改善と分析のためクッキーを使用します。項目別に同意の可否を選択できます。', - 'accept_all' => 'すべて同意', - 'reject_all' => '必須のみ使用', - 'preferences' => '環境設定', - 'save' => '選択的同意', + 'title' => 'クッキー使用のお知らせ', + 'description' => '本サイトはユーザーエクスペリエンスの改善と分析のためクッキーを使用します。項目ごとに同意の有無を選択できます。', + 'accept_all' => 'すべてに同意', + 'reject_continue' => '同意しないで続ける', + 'preferences' => '選択同意', + 'save' => '選択を保存', 'policy_link' => 'プライバシーポリシー', 'preferences_title' => 'クッキー環境設定', - 'preferences_description' => 'カテゴリ別のクッキー使用を個別に同意/撤回できます。必須カテゴリは無効にできません。', + 'preferences_description' => 'カテゴリー別のクッキー使用を個別に同意・取消できます。必須カテゴリーはオフにできません。', 'category_required_badge' => '必須', 'view_policy_label' => '変更されたポリシー本文を表示', ], diff --git a/lang-packs/_bundled/g7-plugin-sirsoft-gdpr-ja/frontend/ja.json b/lang-packs/_bundled/g7-plugin-sirsoft-gdpr-ja/frontend/ja.json index d1201c7e..359f2fd7 100644 --- a/lang-packs/_bundled/g7-plugin-sirsoft-gdpr-ja/frontend/ja.json +++ b/lang-packs/_bundled/g7-plugin-sirsoft-gdpr-ja/frontend/ja.json @@ -15,6 +15,7 @@ "consent": { "granted": "同意が保存されました。", "granted_again": "同意が更新されました。", + "rejected_saved": "同意しないで続行します。選択項目は使用されません。", "revoked": "同意が撤回されました。", "renew_all_success": "アクティブな項目 {renewed} 個を新しいポリシーバージョンに更新しました。", "granting": "同意中...", @@ -29,30 +30,30 @@ "category_cookie_marketing": "マーケティングクッキー" }, "blocked_domains": { - "invalid_format": "正しいドメイン形式ではありません。(FQDN のみサポート、*.example.com ワイルドカード可)", - "invalid_format_functional": "機能カテゴリ — 正しいドメイン形式ではありません。(FQDN のみサポート、*.example.com ワイルドカード可)", - "invalid_format_analytics": "分析カテゴリ — 正しいドメイン形式ではありません。(FQDN のみサポート、*.example.com ワイルドカード可)", - "invalid_format_marketing": "マーケティングカテゴリ — 正しいドメイン形式ではありません。(FQDN のみサポート、*.example.com ワイルドカード可)", + "invalid_format": "正しいドメイン形式ではありません。(FQDNのみ対応、*.example.com ワイルドカード可能)", + "invalid_format_functional": "機能カテゴリ — 正しいドメイン形式ではありません。(FQDNのみ対応、*.example.com ワイルドカード可能)", + "invalid_format_analytics": "分析カテゴリ — 正しいドメイン形式ではありません。(FQDNのみ対応、*.example.com ワイルドカード可能)", + "invalid_format_marketing": "マーケティングカテゴリ — 正しいドメイン形式ではありません。(FQDNのみ対応、*.example.com ワイルドカード可能)", "too_long": "ドメインは253文字を超えることはできません。", "too_long_functional": "機能カテゴリ — ドメインは253文字を超えることはできません。", "too_long_analytics": "分析カテゴリ — ドメインは253文字を超えることはできません。", "too_long_marketing": "マーケティングカテゴリ — ドメインは253文字を超えることはできません。", - "must_be_array": "カテゴリ別ドメイン一覧は配列である必要があります。" + "must_be_array": "カテゴリ別ドメインリストは配列である必要があります。" }, "settings": { "saved": "設定が保存されました。", "title": "GDPR設定", - "description": "クッキー同意バナーとマイページ同意管理カードの動作を運営者が制御します。", + "description": "クッキー同意バナーと本人ページ同意管理カードの動作を運営者が制御します。", "section": { - "operator": "運営者 / プライバシーポリシー", - "operator_desc": "クッキーバナー·ポリシーページに表示される運営者、データ保存場所、ポリシーページスラッグを登録します。", + "operator": "運営主体·プライバシーポリシー", + "operator_desc": "クッキーバナー·ポリシーページに表示される運営主体、データ保管位置、ポリシーページスラッグを登録します。", "cookie_banner": "クッキー同意バナー", - "cookie_banner_desc": "訪問者に表示されるクッキー同意バナーの表示·位置·カテゴリ·ポリシーバージョンを一箇所で設定します。", + "cookie_banner_desc": "訪問者に表示されるクッキー同意バナーの表示/位置/カテゴリ/ポリシーバージョンを一か所で設定します。", "cookie_banner_settings": "バナー表示設定", "cookie_categories": "クッキーカテゴリ", "cookie_policy_version": "クッキーポリシーバージョン", "auto_blocking": "自動ブロックポリシー", - "auto_blocking_desc": "クッキーバナーが表示されると、カテゴリ別ドメイン一覧の外部追跡リソースがユーザー同意前まで自動ブロックされます。" + "auto_blocking_desc": "クッキーバナーが表示されると、カテゴリ別ドメインリストの外部追跡リソースがユーザーの同意前まで自動ブロックされます。" }, "nav": { "operator": "運営情報", @@ -68,29 +69,29 @@ "fields": { "cookie_policy_version": { "label": "ポリシーバージョン", - "hint": "ポリシー本文、カテゴリの意味、委託者·データ保管情報が変わったら「+ 新しいバージョンを公開」をクリックしてください。公開と同時にすべての会員が次回訪問時に再同意を進めます。" + "hint": "ポリシー本文、カテゴリの意味、委託者·データ保管情報が変わった場合は「+ 新規バージョン公開」をクリックしてください。公開直後にすべての会員が次回訪問時に再同意を進めます。" }, "privacy_policy_slug": { "label": "プライバシーポリシーページスラッグ", - "hint": "ページプラグインに登録された処理方針ページのスラッグ。空の場合、クッキーバナーのポリシーリンクは表示されません。", + "hint": "ページプラグインに登録された処理方針ページのスラッグ。空白の場合、クッキーバナーのポリシーリンクが表示されません。", "placeholder": "例: privacy-policy" }, "legal_entity_name": { - "label": "法人名 / 運営者", - "hint": "クッキーバナーとマイページに表示されるサイト運営者名です。", + "label": "法人名·運営主体", + "hint": "クッキーバナーと本人ページに表示されるサイト運営主体の名前です。", "placeholder": "例: (株)会社名" }, "data_storage_location": { - "label": "データ保存場所", - "hint": "ユーザーに案内するデータ保存国を入力してください。例: \"大韓民国\"、\"アメリカ (AWS)\"。IPアドレス、クラウドリージョンコード(例: ap-northeast-2)、データセンターアドレスはセキュリティ上入力しないでください。", - "placeholder": "例: 大韓民国 (自社データセンター) / アメリカ (AWS)" + "label": "データ保管位置", + "hint": "ユーザーに案内するデータ保管国を入力してください。例: \"大韓民国\"、\"米国 (AWS)\"。IPアドレス、クラウドリージョンコード(例: ap-northeast-2)、データセンターアドレスはセキュリティ上入力しないでください。", + "placeholder": "例: 大韓民国 (自社データセンター) / 米国 (AWS)" }, "banner_enabled": { "label": "クッキーバナー表示", - "hint": "有効にするとクッキーバナー表示、同意前外部追跡自動ブロック、マイページ同意管理カードが一緒に開始されます。" + "hint": "有効化時にクッキーバナー表示、同意前外部追跡自動ブロック、本人ページ同意管理カードが一緒に開始されます。" }, "banner_position": { - "label": "バナーの位置", + "label": "バナー位置", "hint": "クッキーバナーが画面に表示される位置を選択します。", "option_bottom_bar": "下部 (全幅)", "option_bottom_left_popup": "左下ポップアップ", @@ -98,75 +99,75 @@ "option_centered_modal": "中央モーダル" }, "blocked_domains": { - "warnings_title": "注意", - "auto_blocking_off_hint": "自動ブロックはクッキーバナー表示が ON の場合のみ機能します。「クッキーバナー」カードでクッキーバナー表示トグルをまず有効にしてください。", + "warnings_title": "案内", + "auto_blocking_off_hint": "自動ブロックはクッキーバナー表示がONの時のみ動作します。「クッキーバナー」カードでクッキーバナー表示トグルをまず有効化してください。", "warning_visual_break": "外部フォント·スタイルシートは分類に注意してください。誤ってブロックするとページが静かに壊れる可能性があります。", "warning_policy_bump": "ブロックドメインを変更した後、クッキーポリシーバージョンを上げてください。既存の同意者が新しいドメインにも同意したものとして処理される問題を防ぎます。", - "warning_domain_format": "ドメイン形式: example.com、*.example.com (ワイルドカード) 可能。localhost のようなドットがない名前や日本語ドメインはサポートされていません。", - "tag_input_placeholder": "ドメイン入力後 Enter (例: google-analytics.com、*.hotjar.com)", + "warning_domain_format": "ドメイン形式: example.com、*.example.com (ワイルドカード) 可能。localhostのように点がない名前や韓国語ドメインはサポートされていません。", + "tag_input_placeholder": "ドメイン入力後、Enter キー (例: google-analytics.com、*.hotjar.com)", "tag_no_options": "推奨ドメインがありません。直接入力してください。", "category": { "functional": "機能カテゴリブロックドメイン", "analytics": "分析カテゴリブロックドメイン", - "marketing": "マーケティングカテゴリブロックドメイン" + "marketing": "マーケティング カテゴリ 차단 도메인" } }, "cookie_categories": { - "description": "クッキーカテゴリ4種の用途とブロックツール例は、各カードの[情報を表示]で確認できます。", + "description": "クッキー カテゴリ 4種の用途と차단 도구 例は各カードの【情報を展開】で確認できます。", "category_required_badge": "必須", "category_optional_badge": "選択", "info": { - "expand": "情報を表示", - "collapse": "情報を非表示", - "description_label": "ユーザー案内文言", - "scope_label": "自動ブロック対象", - "tools_label": "代表的なツール例", + "expand": "情報を展開", + "collapse": "情報を閉じる", + "description_label": "ユーザー案内文句", + "scope_label": "自動차단 対象", + "tools_label": "代表 ツール例", "necessary": { - "scope": "自動ブロックしません。セッション・ログイントークン、カート識別子、ユーザーが登録時に選択した言語設定、クッキー同意記録など、サイト動作に必須の項目は常に許可されます。", - "tools": "セッションID、認証トークン、CSRFトークン、カート識別子、多言語設定など(追加設定不要)" + "scope": "自動차단しません。セッション·ログイン トークン、カート識別子、ユーザーが会員登録 時に選択した言語 設定、クッキー 同意 記録など サイト動作に必ず必要な項目は常に許可されます。", + "tools": "セッション ID、認証 トークン、CSRF トークン、カート識別子、多言語 設定など(別途設定 不要)" }, "functional": { - "scope": "「自動ブロックポリシー」タブの機能カテゴリドメイン一覧に登録された外部リソースが同意前までブロックされます。また、ダークモード・通貨設定など、ユーザー利便性の保存も同意後のみ保管されます。", - "tools": "カスタマーサポートチャットボット(Crisp、Intercom、Tawk.to)、多言語自動翻訳ウィジェット、ユーザー設定同期サービスなど" + "scope": "「自動차단 ポリシー」タブの機能 カテゴリ ドメイン リストに登録された外部リソースが同意前まで自動차断されます。また ダークモード·通貨 選好のような ユーザー 利便性 保存も同意後にのみ保管されます。", + "tools": "顧客サポート チャットボット(Crisp、Intercom、Tawk.to)、多言語 自動翻訳 ウィジェット、ユーザー設定 同期 サービスなど" }, "analytics": { - "scope": "「自動ブロックポリシー」タブの分析カテゴリドメイン一覧に登録された外部分析スクリプト・埋め込み・トラッキングピクセルが同意前までブロックされます。同意すると即座に復元され動作します。", - "tools": "Google Analytics 4(GA4)、Hotjar、Microsoft Clarity、Mixpanel、Matomo、ネイバープレミアムログ分析など" + "scope": "「自動차단 ポリシー」タブの分析 カテゴリ ドメイン リストに登録された外部分析 スクリプト·埋め込み·トラッキング ピクセルが同意前まで自動차단されます。同意すると即座に復元されて作動します。", + "tools": "Google Analytics 4(GA4)、Hotjar、Microsoft Clarity、Mixpanel、Matomo、ネイバー プレミアム ログ分析など" }, "marketing": { - "scope": "「自動ブロックポリシー」タブのマーケティングカテゴリドメイン一覧に登録された外部広告・埋め込み・トラッキングピクセルが同意前までブロックされます。同意すると即座に復元され動作します。", - "tools": "Meta Pixel(Facebook)、Google広告リマーケティング、カカオピクセル、YouTube動画埋め込みなど" + "scope": "「自動차단 ポリシー」タブのマーケティング カテゴリ ドメイン リストに登録された外部広告·埋め込み·トラッキング ピクセルが同意前まで自動차단されます。同意すると即座に復元されて作動します。", + "tools": "Meta ピクセル(Facebook)、Google 広告 リマーケティング、カカオ ピクセル、YouTube 動画 埋め込みなど" } } } }, "save_success": "設定が保存されました。", - "save_error": "設定保存中にエラーが発生しました。", + "save_error": "設定 保存 中 エラーが発生しました。", "validation_error": "入力値を確認してください。", - "unsaved_changes": "保存されていない変更があります。", + "unsaved_changes": "保存されていない変更事項があります。", "policy_version": { - "current_label": "現在のバージョン", - "published_at_label": "公開日時", + "current_label": "現在バージョン", + "published_at_label": "公開 日時", "publisher_label": "公開者", "publisher_system": "システム", - "memo_label": "変更理由", - "publish_button": "新しいバージョンを公開", - "history_toggle_show": "履歴を表示", + "memo_label": "変更 理由", + "publish_button": "新 バージョン 公開", + "history_toggle_show": "履歴を見る", "history_toggle_hide": "履歴を閉じる", - "history_empty": "公開されたポリシーバージョンがありません。", - "publish_success": "新しいポリシーバージョンが公開されました。すべての会員が次回訪問時に再同意を進めます。", - "publishing": "公開中...", - "guide_title": "いつ新しいバージョンを公開しますか?", - "guide_body": "ブロックドメイン変更、UI文言修正などの運用上の変更は新しいバージョン公開が必要ありません。", + "history_empty": "公開された ポリシー バージョンがありません。", + "publish_success": "新しい ポリシー バージョンが公開されました。すべての会員が次回 訪問時に再同意を進めます。", + "publishing": "公開 中...", + "guide_title": "いつ新しい バージョンを公開しますか?", + "guide_body": "차断 ドメイン 変更、UI 文句 修正のような運用上の変更は新しい バージョン 公開が必要ではありません。", "change_type": { - "material": "再同意トリガー", - "non_material": "シンプルな変更", - "initial": "初期公開" + "material": "再同意 トリガー", + "non_material": "単純 変更", + "initial": "初期 公開" }, "history": { "col": { "version": "バージョン", - "created_at": "公開日時", + "created_at": "公開 日時", "publisher": "公開者", "change_type": "種類", "memo": "理由", @@ -175,57 +176,58 @@ }, "snapshot_view_short": "表示", "material_modal": { - "title": "新しいポリシーバージョン公開 + 再同意トリガー", - "body": "「新しいバージョンを公開」をクリックすると即座に公開され、すべてのユーザーが次回訪問時に再同意する必要があります。", - "effect_bump": "ポリシーバージョン単調増加(例:v2 → v3)", - "effect_reconsent": "すべての会員が次回訪問時に再同意画面を表示します", - "effect_history": "履歴表に新しいバージョン行が追加されます", - "memo_label": "変更理由(必須)", - "memo_placeholder": "例:約款ページ本文修正 / 委託運用者変更", - "confirm": "新しいバージョンを公開" + "title": "新 ポリシー バージョン 公開 + 再同意 トリガー", + "body": "「新 バージョン 公開」を押すと即座に公開され、すべての ユーザーが次回 訪問時に再同意する必要があります。", + "effect_bump": "ポリシー バージョン 単調 増加(例:v2 → v3)", + "effect_reconsent": "すべての会員が次回 訪問時に再同意 画面を見ることになります", + "effect_history": "履歴 表に新しい バージョン row が追加されます", + "memo_label": "変更 理由(必須)", + "memo_placeholder": "例:規約 ページ 本文 修正/委託 運営者 変更", + "confirm": "新 バージョン 公開" }, "snapshot_modal": { - "title": "ポリシーバージョン v{version} 本文", - "description": "公開時点の設定のカテゴリ一覧・ポリシー本文スラッグ・ブロックドメインなど全体の本文です。DPOが同意紛争時に立証資料として活用します。", - "section_categories": "クッキーカテゴリ", - "section_privacy_policy": "ポリシー本文スラッグ", - "section_blocked_domains": "ブロックドメインカタログ", + "editor_label": "ポリシー バージョン 本文を見る", + "title": "ポリシー バージョン v{version} 本文", + "description": "公開 時点 settings のカテゴリ リスト·ポリシー 本文 スラッグ·차단 ドメインなど全体 本文です。DPO が同意 紛争時に立証 資料として活用します。", + "section_categories": "クッキー カテゴリ", + "section_privacy_policy": "ポリシー 本文 スラッグ", + "section_blocked_domains": "차단 ドメイン カタログ", "category_required_badge": "必須", "no_categories": "登録されたカテゴリがありません。", - "no_blocked_domains": "登録されたブロックドメインがありません。", - "view_button": "v{version} 本文を表示", + "no_blocked_domains": "登録された차단 ドメインがありません。", + "view_button": "v{version} 本文を見る", "close": "閉じる", - "open_policy_page": "本文ページを開く", - "no_policy_slug": "登録されたポリシー本文ページがありません。", - "editor_label": "ポリシーバージョンの本文を表示" + "open_policy_page": "本文 ページを開く", + "no_policy_slug": "登録されたポリシー 本文 ページがありません。" }, - "not_found": "該当するポリシーバージョンが見つかりません。" + "not_found": "該当するポリシー バージョンが見つかりません。" } }, "admin": { "consent_log": { - "title": "GDPR同意履歴", - "description": "会員・ゲストの同意/撤回変更履歴を照会します。IP・User-Agentなど監査情報が一緒に公開されます。", + "title": "GDPR 同意 履歴", + "description": "会員·ゲストの同意/撤回 変更 履歴を照会します。IP·User-Agent などの監査 情報が合わせて露出されます。", "refresh": "更新", - "empty": "照会された同意履歴がありません。", - "clear_filters": "フィルターを初期化", + "empty": "照会された同意 履歴がありません。", + "clear_filters": "フィルター 初期化", "guest_label": "ゲスト", - "view_member_tooltip": "会員詳細ページに移動", - "search_placeholder": "メールアドレスまたはセッションIDで検索", + "view_member_tooltip": "会員 詳細 ページへ移動", + "search_placeholder": "メールアドレス または セッション ID で検索", "search_button": "検索", "search_type": { "email": "メール", - "session_id": "セッションID" + "session_id": "セッション ID" }, "filter": { - "consent_key_label": "同意項目", + "consent_key_label": "同意 項目", "action_label": "アクション", "source_label": "出典", "option_all": "すべて" }, "action": { "granted": "同意", - "revoked": "撤回" + "revoked": "撤回", + "rejected": "拒否" }, "source": { "banner": "バナー", @@ -244,19 +246,19 @@ "detail": { "session_id_label": "セッション ID", "user_agent_label": "User-Agent", - "categories_label": "カテゴリスナップショット", - "categories_hint": "同意時点で会員がカテゴリ別に表示した意思です。ポリシー本文が更新されても、この記録は不変として保存されます。", + "categories_label": "カテゴリースナップショット", + "categories_hint": "同意時点で会員がカテゴリー別に表示した意思です。ポリシー本文が更新されてもこの記録は不変として保存されます。", "snapshot_granted": "同意", - "snapshot_revoked": "拒否" + "snapshot_revoked": "未同意" } } }, "mypage": { "privacy": { "title": "クッキー同意管理", - "description": "サイトが使用中のクッキー項目に対する同意を変更または撤回できます。", + "description": "サイトが使用中のクッキー項目について同意を変更または撤回できます。", "section_consents": "私の同意状況", - "section_consents_hint": "同意・撤回時点に応じて即座に反映されます。必須項目は撤回できません。", + "section_consents_hint": "同意/撤回時点に応じて即座に反映されます。必須項目は撤回できません。", "col_consent_key": "同意項目", "col_consented": "ステータス", "col_consented_at": "同意日", @@ -265,17 +267,26 @@ "col_action": "操作", "consented": "同意", "revoked": "撤回", + "rejected_label": "拒否", "revoke": "撤回", "grant": "同意", "grant_again": "再度同意", + "label_suffix": "{label} クッキー", + "locked_label": "必須", + "enable_all": "すべてに同意", + "enable_all_hint": "同意していない選択項目があります。一度にすべてに同意できます。", + "enable_all_success": "選択項目にすべて同意しました。", "required_label": "必須", "required_hint": "サービス提供に必須のため撤回できません。", + "grant_confirm_title": "クッキー同意確認", + "grant_confirm_message": "このクッキーに同意しますか?同意すると該当カテゴリーのクッキーが使用され、いつでも再度撤回できます。", "revoke_confirm_title": "同意撤回確認", - "revoke_confirm_message": "この項目を撤回しますか?同意を撤回すると、該当カテゴリ関連機能が制限される可能性があります。", + "revoke_confirm_message": "この項目を撤回しますか?同意を撤回すると該当カテゴリー関連機能が制限される可能性があります。", + "revoke_target_consented_at": "同意 · {date}", "no_consents": "有効な同意項目がありません。", "needs_renewal": "クッキーポリシーが変更されました。同意を更新してください。", "needs_renewal_with_version": "クッキーポリシーがバージョン {version} に変更されました。選択項目の同意を再度確認してください。", - "needs_renewal_hint": "必須クッキーはサイト運用に不可欠な項目のため、ポリシー変更に関わらず常に適用されます。", + "needs_renewal_hint": "必須クッキーはサイト運営に必ず必要な項目のためポリシー変更と無関係に常に適用されます。", "btn_renew_all": "現在の同意更新", "btn_renew_this_item": "ポリシー更新", "renew_all_success": "有効項目 {renewed} 個を新しいポリシーバージョンに更新しました。", @@ -289,18 +300,18 @@ "refresh": "更新" }, "banner": { - "re_appear_reason": "クッキーポリシーがバージョン {version} に変更されたため、同意を再度確認します。", - "consent_required_hint": "拒否した項目と関連する機能が制限される可能性があります。", + "re_appear_reason": "クッキーポリシーがバージョン {version} に変更されたため同意を再度確認します。", + "consent_required_hint": "拒否した項目に関連する機能が制限される可能性があります。", "btn_renew_consent": "現在の同意更新", - "title": "クッキー使用のご案内", - "description": "本サイトはユーザー体験の向上と分析のためクッキーを使用しています。項目別に同意の有無を選択できます。", - "accept_all": "すべて同意", - "reject_all": "必須のみ使用", - "preferences": "環境設定", - "save": "選択同意", + "title": "クッキー使用のお知らせ", + "description": "本サイトはユーザー体験の改善と分析のためクッキーを使用しています。項目ごとに同意の有無を選択できます。", + "accept_all": "すべてに同意", + "reject_continue": "同意せずに続行", + "preferences": "選択同意", + "save": "選択を保存", "policy_link": "プライバシーポリシー", "preferences_title": "クッキー環境設定", - "preferences_description": "カテゴリ別クッキー使用を個別に同意・撤回できます。必須カテゴリはオフにできません。", + "preferences_description": "カテゴリー別クッキー使用を個別に同意/撤回できます。必須カテゴリーはオフにできません。", "category_required_badge": "必須", "view_policy_label": "変更されたポリシー本文を表示", "operator_and_storage_line": "本サイトは {entity} が運営しており、データは {location} に保存されます", diff --git a/lang-packs/_bundled/g7-plugin-sirsoft-gdpr-ja/seed/manifest.json b/lang-packs/_bundled/g7-plugin-sirsoft-gdpr-ja/seed/manifest.json index e43f637c..ce39d6b7 100644 --- a/lang-packs/_bundled/g7-plugin-sirsoft-gdpr-ja/seed/manifest.json +++ b/lang-packs/_bundled/g7-plugin-sirsoft-gdpr-ja/seed/manifest.json @@ -1,4 +1,4 @@ { "name": "GDPR (一般データ保護規則)", - "description": "クッキー同意バナー、データ保護責任者(DPO)情報の公開、データ閲覧・削除リクエスト処理を提供するGDPR対応プラグインです。" + "description": "クッキー同意バナー、自動ブロック、同意履歴保存、マイページ同意撤回を提供するGDPR対応プラグインです。" } diff --git a/lang-packs/_bundled/g7-plugin-sirsoft-gdpr-ja/seed/permissions.json b/lang-packs/_bundled/g7-plugin-sirsoft-gdpr-ja/seed/permissions.json index d92d684e..997e9d92 100644 --- a/lang-packs/_bundled/g7-plugin-sirsoft-gdpr-ja/seed/permissions.json +++ b/lang-packs/_bundled/g7-plugin-sirsoft-gdpr-ja/seed/permissions.json @@ -1,18 +1,18 @@ { "sirsoft-gdpr": { - "name": "GDPR(一般データ保護規則)", - "description": "GDPRプラグインが提供する権限" + "name": "GDPR (一般データ保護規則)", + "description": "GDPR プラグインが提供する権限" }, "sirsoft-gdpr.privacy": { "name": "個人情報保護", - "description": "GDPRドメイン権限(閲覧·設定)" + "description": "GDPR ドメイン権限 (閲覧·設定)" }, "sirsoft-gdpr.privacy.view": { "name": "個人情報閲覧", - "description": "同意履歴·GDPR設定閲覧(監査·モニタリング)" + "description": "同意履歴·GDPR 設定閲覧 (監査·モニタリング)" }, "sirsoft-gdpr.privacy.update": { "name": "個人情報設定変更", - "description": "クッキーカテゴリ·ポリシーバージョンなどGDPRプラグイン設定変更" + "description": "クッキーカテゴリ·ポリシーバージョンなど GDPR プラグイン設定変更" } } diff --git a/lang-packs/_bundled/g7-plugin-sirsoft-gdpr-ja/seed/roles.json b/lang-packs/_bundled/g7-plugin-sirsoft-gdpr-ja/seed/roles.json index e70c39ea..9b4c5c61 100644 --- a/lang-packs/_bundled/g7-plugin-sirsoft-gdpr-ja/seed/roles.json +++ b/lang-packs/_bundled/g7-plugin-sirsoft-gdpr-ja/seed/roles.json @@ -1,6 +1,6 @@ { "sirsoft-gdpr.privacy": { "name": "個人情報管理者", - "description": "GDPRドメイン運営権限(設定・同意履歴閲覧)" + "description": "GDPR ドメイン運用権限 (設定·同意履歴閲覧)" } } diff --git a/plugins/_bundled/sirsoft-gdpr/docs/api/consent.md b/plugins/_bundled/sirsoft-gdpr/docs/api/consent.md index 90a5047b..e214db83 100644 --- a/plugins/_bundled/sirsoft-gdpr/docs/api/consent.md +++ b/plugins/_bundled/sirsoft-gdpr/docs/api/consent.md @@ -248,7 +248,7 @@ _단건 응답: `data` 객체의 필드._ | user_id | integer | `166` | user 식별자 (연관 리소스 참조) | | needs_renewal | boolean | `false` | 회원의 활성 동의 중 옛 정책 버전인 항목이 있어 재동의가 필요한지 여부. 마이페이지가 「전체 다시 동의」 안내를 노출할지 판단합니다 | | current_policy_version | string | `10` | 현재 발행된 최신 정책 버전 문자열. 각 동의 항목의 `policy_version`과 비교해 항목별 갱신 필요 여부를 계산하는 기준입니다 | -| consents | array | `[{"id":null,"consent_key":"cookie_necessary","consent_lab…` | 카탈로그의 모든 쿠키 카테고리와 회원 status를 합친 동의 매트릭스. 항목마다 다국어 라벨(`consent_label`), 필수 여부(`is_required`), 현재 동의 상태(`is_consented`), 명시적 거부 여부(`is_rejected`)와 거부 일시(`rejected_at`), 철회/재동의 가능 여부(`can_revoke`/`can_grant`), 항목별 갱신 필요(`needs_renewal_this_item`)를 담아 한 화면에서 철회·재동의·신규 동의를 처리하게 합니다. `is_rejected=true`는 마이페이지에서 「거부」 배지로 표시됩니다 | +| consents | array | `[{"id":null,"consent_key":"cookie_necessary","consent_lab…` | 카탈로그의 모든 쿠키 카테고리와 회원 status를 합친 동의 매트릭스. 항목마다 다국어 라벨(`consent_label`), 필수 여부(`is_required`), 현재 동의 상태(`is_consented`), 명시적 거부 여부(`is_rejected`)와 거부 일시(`rejected_at`), 철회/재동의 가능 여부(`can_revoke`/`can_grant`), 항목별 갱신 필요(`needs_renewal_this_item`)를 담아 한 화면에서 철회·재동의·신규 동의를 처리하게 합니다. `is_rejected=true`는 마이페이지에서 「거부」 배지로 표시됩니다. 각 항목은 마이페이지 표시용 상태 파생 필드도 함께 내려줍니다 — `status`(`required`/`consented`/`revoked`/`rejected`/`none`), 상태 배지 문구(`status_badge_label`, 다국어 — 필수=항상 적용 / 동의함 / 거부함 / 미설정), 동의한 항목의 표시 동사·날짜(`status_label`·`status_at_formatted` — 동의 상태(required·consented)만 '동의' + 동의일을 내려주고, 거부·철회·미설정은 둘 다 null) | **응답 예시** diff --git a/plugins/_bundled/sirsoft-gdpr/lang/en/messages.php b/plugins/_bundled/sirsoft-gdpr/lang/en/messages.php index 47ea17c5..965f5d4a 100644 --- a/plugins/_bundled/sirsoft-gdpr/lang/en/messages.php +++ b/plugins/_bundled/sirsoft-gdpr/lang/en/messages.php @@ -231,6 +231,20 @@ return [ 'consented' => 'Consented', 'revoked' => 'Revoked', 'rejected_label' => 'Rejected', + // Issue #430 — per-item status verb chosen by the Resource (status_label) from status. + 'status' => [ + 'granted' => 'Consented', + 'consented' => 'Consented', + 'revoked' => 'Revoked', + 'rejected' => 'Rejected', + ], + // Issue #430 (button redesign) — status badge text. Resource(status_badge_label). + 'badge' => [ + 'required' => 'Always on', + 'consented' => 'Consented', + 'rejected' => 'Declined', + 'unset' => 'Not set', + ], 'revoke' => 'Revoke', 'grant' => 'Consent', 'grant_again' => 'Consent Again', diff --git a/plugins/_bundled/sirsoft-gdpr/lang/ko/messages.php b/plugins/_bundled/sirsoft-gdpr/lang/ko/messages.php index f5329dfa..4b617c90 100644 --- a/plugins/_bundled/sirsoft-gdpr/lang/ko/messages.php +++ b/plugins/_bundled/sirsoft-gdpr/lang/ko/messages.php @@ -231,6 +231,20 @@ return [ 'consented' => '동의', 'revoked' => '철회', 'rejected_label' => '거부', + // 이슈 #430 — 각 동의 항목 상태별 표시 동사. Resource(status_label)가 status 로 골라 내려준다. + 'status' => [ + 'granted' => '동의', + 'consented' => '동의', + 'revoked' => '철회', + 'rejected' => '거부', + ], + // 이슈 #430 (버튼 재설계) — 상태 배지 문구. Resource(status_badge_label). + 'badge' => [ + 'required' => '항상 적용', + 'consented' => '동의함', + 'rejected' => '거부함', + 'unset' => '미설정', + ], 'revoke' => '철회', 'grant' => '동의', 'grant_again' => '다시 동의', diff --git a/plugins/_bundled/sirsoft-gdpr/resources/extensions/cookie_banner.json b/plugins/_bundled/sirsoft-gdpr/resources/extensions/cookie_banner.json index b7e8057a..ad14f89a 100644 --- a/plugins/_bundled/sirsoft-gdpr/resources/extensions/cookie_banner.json +++ b/plugins/_bundled/sirsoft-gdpr/resources/extensions/cookie_banner.json @@ -370,7 +370,7 @@ "name": "Div", "if": "{{!!gdprPublicSettings?.data?.legal_entity_name || !!gdprPublicSettings?.data?.data_storage_location}}", "props": { - "className": "mt-3 pt-2 border-t border-gray-100 dark:border-gray-700 text-tertiary" + "className": "mt-3 pt-2 border-t border-gray-100 dark:border-gray-700 text-xs text-gray-500 dark:text-gray-400" }, "children": [ { diff --git a/plugins/_bundled/sirsoft-gdpr/resources/extensions/mypage_privacy_tab.json b/plugins/_bundled/sirsoft-gdpr/resources/extensions/mypage_privacy_tab.json index 1f5d46a0..8f5ca815 100644 --- a/plugins/_bundled/sirsoft-gdpr/resources/extensions/mypage_privacy_tab.json +++ b/plugins/_bundled/sirsoft-gdpr/resources/extensions/mypage_privacy_tab.json @@ -54,7 +54,7 @@ { "type": "basic", "name": "P", - "props": { "className": "text-body" }, + "props": { "className": "text-sm text-gray-700 dark:text-gray-300" }, "text": "$t:sirsoft-gdpr.mypage.privacy.revoke_confirm_message" }, { @@ -73,8 +73,16 @@ "type": "basic", "name": "Span", "if": "{{!!_global.revokeTargetDescription}}", - "props": { "className": "block text-tertiary mt-0.5 leading-relaxed" }, + "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 ?? ''}}" } ] }, @@ -89,7 +97,7 @@ "props": { "type": "button", "disabled": "{{_local.isRevoking}}", - "className": "btn btn-secondary disabled:opacity-50" + "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" } @@ -105,7 +113,7 @@ "props": { "type": "button", "disabled": "{{_local.isRevoking || !_global.revokeTargetKey}}", - "className": "px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed" + "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": [ { @@ -158,6 +166,128 @@ ] } ] + }, + { + "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": [ @@ -189,7 +319,7 @@ { "type": "basic", "name": "H2", - "props": { "className": "section-heading-md" }, + "props": { "className": "text-base font-semibold text-gray-900 dark:text-white" }, "text": "$t:sirsoft-gdpr.mypage.privacy.title" } ] @@ -200,7 +330,7 @@ "type": "basic", "name": "Div", "if": "{{!!gdprPublicSettings?.data?.legal_entity_name || !!gdprPublicSettings?.data?.data_storage_location}}", - "props": { "className": "mb-4 text-tertiary" }, + "props": { "className": "mb-4 text-xs text-gray-500 dark:text-gray-400" }, "children": [ { "type": "basic", @@ -233,7 +363,7 @@ { "type": "basic", "name": "Div", - "props": { "className": "flex-center gap-3 flex-1" }, + "props": { "className": "flex items-center gap-3 flex-1" }, "children": [ { "type": "basic", @@ -249,14 +379,14 @@ "type": "basic", "name": "P", "comment": "현재 정책 버전을 노출하여 사용자가 어느 버전에 동의하는지 인지 (#23 — informed consent). G7 표준 $t:key|param={{value}} 토큰 사용 — lang 파일의 {version} placeholder 가 치환됨.", - "props": { "className": "text-warning-soft" }, + "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-warning-soft" }, + "props": { "className": "mt-1 text-xs text-yellow-700 dark:text-yellow-300" }, "text": "$t:sirsoft-gdpr.mypage.privacy.needs_renewal_hint" } ] @@ -266,7 +396,7 @@ { "type": "basic", "name": "Div", - "props": { "className": "flex-center flex-wrap gap-2 sm:flex-shrink-0" }, + "props": { "className": "flex items-center flex-wrap gap-2 sm:flex-shrink-0" }, "children": [ { "type": "basic", @@ -317,24 +447,24 @@ "id": "gdpr_section_consents", "type": "basic", "name": "Div", - "comment": "내 동의 현황 — 회원 활성 동의 (status) 기반", - "props": { "className": "toggle-group" }, + "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 bg-gray-50 dark:bg-gray-700/50" }, + "props": { "className": "px-4 py-3 bg-gray-50 dark:bg-gray-700/50" }, "children": [ { "type": "basic", "name": "H3", - "props": { "className": "text-primary text-sm font-semibold" }, + "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": "form-hint" }, + "props": { "className": "text-xs text-gray-500 dark:text-gray-400 mt-1" }, "text": "$t:sirsoft-gdpr.mypage.privacy.section_consents_hint" } ] @@ -343,11 +473,11 @@ "type": "basic", "name": "Div", "if": "{{(gdprMeConsents?.data?.consents ?? []).length === 0}}", - "props": { "className": "py-10 text-center text-label-subtle" }, + "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) 로 한 번에 처리.", + "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}}", @@ -404,7 +534,7 @@ ] }, { - "comment": "토글 리스트 (이슈 #430 재설계) — 업계 표준 CMP(OneTrust/Cookiebot) 패턴: 필수=잠긴 '항상 켜짐', 선택=on/off 스위치. 동의일 컬럼은 사용자 화면에서 제거(감사 로그/관리자 화면에만 존재). 스위치는 Checkbox/Toggle 컴포넌트 부재로 클릭 가능한 Div 로 구현 — 필수는 클릭 액션 없이 시각적 잠금.", + "comment": "항목 리스트 (이슈 #430 재설계) — 상태 배지 + 동의/철회 버튼 방식. 필수=상태 배지 '항상 적용' + 잠금 버튼(액션 없음), 선택=상태 배지(동의함/거부함/미설정) + 동의·철회 버튼(확인 모달 경유). 동의일 컬럼은 사용자 화면에서 제거(동의한 항목만 라벨 아래 날짜 표시, 감사 로그 전체는 관리자 화면에만 존재). 초기 설계였던 on/off 스위치는 user 템플릿 스타일 회귀 + PO 지시로 배지+버튼 방식으로 대체됨(2026-07-21).", "type": "basic", "name": "Div", "if": "{{(gdprMeConsents?.data?.consents ?? []).length > 0}}", @@ -418,7 +548,7 @@ { "type": "basic", "name": "Div", - "props": { "className": "flex items-center justify-between gap-4 px-4 py-3" }, + "props": { "className": "flex items-center justify-between gap-4 px-4 py-4" }, "children": [ { "type": "basic", @@ -429,13 +559,14 @@ { "type": "basic", "name": "Div", - "props": { "className": "flex items-center gap-2" }, + "props": { "className": "flex items-center gap-2 flex-wrap" }, "children": [ { + "comment": "라벨 뒤에 '쿠키' 접미사 (이슈 #430) — label_suffix='{label} 쿠키'. 운영자 카탈로그 라벨이 '기능' 이어도 화면엔 '기능 쿠키' 로 노출.", "type": "basic", "name": "Span", - "props": { "className": "text-primary font-medium text-sm" }, - "text": "{{consent?.consent_label ?? consent?.consent_key ?? ''}}" + "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", @@ -443,6 +574,13 @@ "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 ?? ''}}" } ] }, @@ -450,129 +588,106 @@ "type": "basic", "name": "Span", "if": "{{!!consent?.consent_description}}", - "props": { "className": "block text-tertiary mt-0.5 text-xs leading-relaxed" }, + "props": { "className": "block text-xs text-gray-500 dark:text-gray-400 mt-1 leading-relaxed" }, "text": "{{consent?.consent_description ?? ''}}" + }, + { + "comment": "이슈 #430 (3안) — 라벨 아래 이력 줄: '날짜 + 동의'. 상태는 이미 라벨 옆 배지가 말하므로 여기선 시점만 강조. 서버가 동의한 항목만 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 text-gray-400 dark:text-gray-500" }, + "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 gap-2" }, + "props": { "className": "shrink-0 flex items-center" }, "children": [ { - "comment": "필수 카테고리 — 잠긴 '항상 켜짐' 스위치. 클릭 액션 없음(철회 불가, ePrivacy Art.5(3) 면제). 파란 채움 + 오른쪽 knob + not-allowed 커서로 잠금 표현.", + "comment": "필수 — 잠금 버튼 (비활성, 액션 없음). ePrivacy Art.5(3) 면제.", "type": "basic", - "name": "Div", + "name": "Button", "if": "{{consent?.is_required === true}}", - "props": { "className": "flex items-center gap-2" }, + "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": "Span", - "props": { "className": "text-xs font-medium text-gray-500 dark:text-gray-400" }, - "text": "$t:sirsoft-gdpr.mypage.privacy.always_on" - }, - { - "type": "basic", - "name": "Div", - "props": { "className": "relative inline-flex h-6 w-11 items-center rounded-full bg-blue-600/60 dark:bg-blue-500/60 cursor-not-allowed" }, - "children": [ - { "type": "basic", "name": "Icon", "props": { "name": "lock", "size": "xs", "className": "absolute left-1.5 text-white/80" } }, - { "type": "basic", "name": "Span", "props": { "className": "inline-block h-4 w-4 translate-x-6 rounded-full bg-white shadow transition-transform" } } - ] - } + { "type": "basic", "name": "Icon", "props": { "name": "lock", "size": "xs" } }, + { "type": "basic", "name": "Span", "text": "$t:sirsoft-gdpr.mypage.privacy.locked_label" } ] }, { - "comment": "선택형 카테고리 — on/off 스위치. 켜짐(is_consented=true)이면 녹색+revoke 확인 모달, 꺼짐이면 회색+grant 즉시 호출. grantingKey 로 해당 행만 처리 중 표시.", + "comment": "동의함(선택) — 철회 버튼. 클릭 시 철회 확인 모달 (즉시 철회 아님, 오조작 방지).", "type": "basic", - "name": "Div", - "if": "{{consent?.is_required !== true}}", - "props": { "className": "flex items-center gap-2" }, - "children": [ + "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": "basic", - "name": "Span", - "props": { "className": "text-xs font-medium {{consent?.is_consented ? 'text-green-600 dark:text-green-400' : 'text-gray-400 dark:text-gray-500'}}" }, - "text": "{{consent?.is_consented ? '$t:sirsoft-gdpr.mypage.privacy.toggle_on' : '$t:sirsoft-gdpr.mypage.privacy.toggle_off'}}" - }, - { - "comment": "켜짐 상태 스위치 — 클릭 시 철회 확인 모달. 녹색 채움 + knob 오른쪽.", - "type": "basic", - "name": "Button", - "if": "{{consent?.is_consented === true}}", - "props": { - "type": "button", - "aria-label": "{{consent?.consent_label ?? consent?.consent_key}}", - "className": "relative inline-flex h-6 w-11 items-center rounded-full bg-green-500 dark:bg-green-600 transition-colors focus:outline-none focus:ring-2 focus:ring-green-400 focus:ring-offset-1" - }, + "type": "click", + "handler": "sequence", "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 ?? ''}}" - } - }, - { "handler": "openModal", "target": "revoke_confirm_modal" } - ] - } - ], - "children": [ - { "type": "basic", "name": "Span", "props": { "className": "inline-block h-4 w-4 translate-x-6 rounded-full bg-white shadow transition-transform" } } - ] - }, - { - "comment": "꺼짐 상태 스위치 — 클릭 시 즉시 grant. 회색 채움 + knob 왼쪽. 정책 갱신 항목도 별도 색 분기 없이 grant 로 통합(정책 갱신도 결국 동의 표명, grant 는 항상 현재 정책 버전 기록).", - "type": "basic", - "name": "Button", - "if": "{{consent?.is_consented !== true}}", - "props": { - "type": "button", - "aria-label": "{{consent?.consent_label ?? consent?.consent_key}}", - "disabled": "{{_local.grantingKey === consent?.consent_key}}", - "className": "relative inline-flex h-6 w-11 items-center rounded-full bg-gray-300 dark:bg-gray-600 transition-colors focus:outline-none focus:ring-2 focus:ring-blue-400 focus:ring-offset-1 disabled:opacity-50 disabled:cursor-wait" - }, - "actions": [ - { - "type": "click", - "handler": "sequence", - "actions": [ - { "handler": "setState", "params": { "target": "local", "grantingKey": "{{consent?.consent_key}}" } }, - { - "handler": "apiCall", - "auth_required": true, - "target": "/api/plugins/sirsoft-gdpr/consent/grant", - "params": { - "method": "POST", - "body": "{{{ consent_key: consent?.consent_key }}}" - }, - "onSuccess": [ - { "handler": "setState", "params": { "target": "local", "grantingKey": null } }, - { "handler": "refetchDataSource", "params": { "dataSourceId": "gdprMeConsents" } }, - { "handler": "refetchDataSource", "params": { "dataSourceId": "gdprMyConsents" } }, - { "handler": "sirsoft-gdpr.syncConsent" }, - { "handler": "toast", "params": { "type": "success", "message": "$t:sirsoft-gdpr.consent.granted_again" } } - ], - "onError": [ - { "handler": "setState", "params": { "target": "local", "grantingKey": null } }, - { "handler": "toast", "params": { "type": "error", "message": "{{error?.message ?? 'Error'}}" } } - ] - } - ] - } - ], - "children": [ - { "type": "basic", "name": "Span", "props": { "className": "inline-block h-4 w-4 translate-x-1 rounded-full bg-white shadow transition-transform" } } + "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" } ] } ] diff --git a/plugins/_bundled/sirsoft-gdpr/resources/js/__tests__/layouts/cookie_banner.test.tsx b/plugins/_bundled/sirsoft-gdpr/resources/js/__tests__/layouts/cookie_banner.test.tsx index 8cfc2227..c7b344be 100644 --- a/plugins/_bundled/sirsoft-gdpr/resources/js/__tests__/layouts/cookie_banner.test.tsx +++ b/plugins/_bundled/sirsoft-gdpr/resources/js/__tests__/layouts/cookie_banner.test.tsx @@ -528,11 +528,14 @@ describe('extensions/cookie_banner.json — 쿠키 동의 배너', () => { expect(text).toContain('sirsoft-gdpr.banner.storage_only_line|location='); }); - it('푸터 라인은 시각적 무게 최소화 — 작은 보조 텍스트 시맨틱 + 상단 구분선', () => { + it('푸터 라인은 시각적 무게 최소화 — 작은 보조 텍스트 (공통 Tailwind 코어 클래스) + 상단 구분선', () => { // 동의 행위 자체에 시선이 가도록 운영 주체는 부드러운 보조 정보로 처리. - // 작은 회색 텍스트 톤은 표준 시맨틱(.text-tertiary)으로 통일 (#399). + // 이슈 #430 — 배너는 user 페이지에 렌더되므로 admin 전용 시맨틱(.text-tertiary)은 미해석. + // 동등한 공통 Tailwind 코어 클래스로 치환 (회귀 수정). const className = (footer?.props as { className?: string })?.className ?? ''; - expect(className).toContain('text-tertiary'); + expect(className).toContain('text-xs'); + expect(className).toContain('text-gray-500'); + expect(className).not.toContain('text-tertiary'); expect(className).toContain('border-t'); }); }); diff --git a/plugins/_bundled/sirsoft-gdpr/resources/js/__tests__/layouts/mypage_privacy_tab.test.tsx b/plugins/_bundled/sirsoft-gdpr/resources/js/__tests__/layouts/mypage_privacy_tab.test.tsx index 6fba4b7f..2d452db5 100644 --- a/plugins/_bundled/sirsoft-gdpr/resources/js/__tests__/layouts/mypage_privacy_tab.test.tsx +++ b/plugins/_bundled/sirsoft-gdpr/resources/js/__tests__/layouts/mypage_privacy_tab.test.tsx @@ -143,10 +143,13 @@ describe('extensions/mypage_privacy_tab.json — 마이페이지 GDPR 동의 매 expect(text).toContain('sirsoft-gdpr.mypage.privacy.storage_only_meta|location='); }); - it('시각적 무게 최소화 — 작은 보조 텍스트 시맨틱', () => { - // 작은 회색 텍스트 톤은 표준 시맨틱(.text-tertiary)으로 통일 (#399). + it('시각적 무게 최소화 — 작은 보조 텍스트 (공통 Tailwind 코어 클래스)', () => { + // 이슈 #430 — 이 카드는 user 페이지(sirsoft-basic)에 렌더되므로 admin 전용 시맨틱(.text-tertiary)은 + // CSS 미해석. 동등한 공통 Tailwind 코어 클래스로 치환 (회귀 수정). const className = (meta?.props as { className?: string })?.className ?? ''; - expect(className).toContain('text-tertiary'); + expect(className).toContain('text-xs'); + expect(className).toContain('text-gray-500'); + expect(className).not.toContain('text-tertiary'); }); }); @@ -157,6 +160,21 @@ describe('extensions/mypage_privacy_tab.json — 마이페이지 GDPR 동의 매 expect(modal?.name).toBe('Modal'); }); + it('이슈 #430 — grant_confirm_modal(동의 확인 모달)이 modals 배열에 포함', () => { + const modal = (root.modals ?? []).find((m) => m.id === 'grant_confirm_modal'); + expect(modal).toBeTruthy(); + expect(modal?.name).toBe('Modal'); + const text = serializeForSearch(modal); + // 동의 확인 문구 + grant API + 대상 _global.grantTargetKey + expect(text).toContain('mypage.privacy.grant_confirm_message'); + expect(text).toContain('"target": "/api/plugins/sirsoft-gdpr/consent/grant"'); + expect(text).toContain('_global.grantTargetKey'); + // 확인 후 refetch + syncConsent + closeModal + expect(text).toContain('"dataSourceId": "gdprMeConsents"'); + expect(text).toContain('"handler": "sirsoft-gdpr.syncConsent"'); + expect(text).toContain('"handler": "closeModal"'); + }); + it('제거된 데이터 요청·자가 취소 모달은 존재하지 않는다', () => { const ids = (root.modals ?? []).map((m) => m.id); expect(ids).not.toContain('gdpr_data_request_modal'); @@ -203,6 +221,8 @@ describe('extensions/mypage_privacy_tab.json — 마이페이지 GDPR 동의 매 // raw key (cookie_functional 등) 대신 사용자 친화 description 을 모달에 표시하기 위해 // 행 핸들러에서 consent_description 을 _global 에 세팅한다. expect(text).toContain('"revokeTargetDescription": "{{consent?.consent_description ?? \'\'}}"'); + // 이슈 #430 — 철회 모달에서 마지막 동의일을 보여주기 위해 status_at_formatted 를 _global 로 전달. + expect(text).toContain('"revokeTargetDate": "{{consent?.status_at_formatted ?? \'\'}}"'); }); it('동의 항목 컬럼이 라벨 우선 표시 (consent_label fallback consent_key)', () => { @@ -233,6 +253,13 @@ describe('extensions/mypage_privacy_tab.json — 마이페이지 GDPR 동의 매 expect(text).not.toContain('font-mono break-all'); }); + it('이슈 #430 — 모달에 철회 대상의 마지막 동의일 노출 (revokeTargetDate 있을 때만)', () => { + const text = serializeForSearch(modal); + // 사용자가 언제 동의한 것을 철회하는지 인지하도록 마지막 동의일을 표시. + expect(text).toContain('_global.revokeTargetDate'); + expect(text).toContain('sirsoft-gdpr.mypage.privacy.revoke_target_consented_at|date='); + }); + it('확인 버튼 → POST /consent/revoke + onSuccess 에 refetch + closeModal + toast', () => { const text = serializeForSearch(modal); expect(text).toContain('"target": "/api/plugins/sirsoft-gdpr/consent/revoke"'); @@ -270,77 +297,102 @@ describe('extensions/mypage_privacy_tab.json — 마이페이지 GDPR 동의 매 /** * @scenario entry=reject, subject=member, category=required - * @effects mypage_no_consent_date_column_for_user + * @effects mypage_status_date_line_per_row */ - it('사용자 화면에 동의일 컬럼이 없다 (감사/관리자 전용) — col_consented_at / col_action 헤더 제거', () => { - // 재설계 회귀 가드: 표 헤더(col_consent_key/col_consented/col_consented_at/col_action) 전면 제거. - // 동의일은 GDPR 감사 로그·관리자 화면에만 존재하고 사용자 토글 화면엔 노출하지 않음. + it('옛 표 헤더 컬럼 방식은 미사용 (col_consented_at / col_action 헤더 없음)', () => { + // 회귀 가드: 재설계로 표 헤더(col_consent_key/col_consented/col_consented_at/col_action) 방식은 제거. + // 날짜는 컬럼이 아니라 각 행의 상태별 한 줄(status_at_formatted)로 노출한다. const text = serializeForSearch(section); expect(text).not.toContain('mypage.privacy.col_consented_at'); expect(text).not.toContain('mypage.privacy.col_action'); - // 행 단위 consented_at 바인딩도 사용자 화면에서 제거 - expect(text).not.toContain('consent?.consented_at'); + }); + + /** + * @scenario entry=reject, subject=member, category=optional + * @effects mypage_status_date_line_consented_only + */ + it('이슈 #430 — 동의한 항목만 날짜 줄 노출 (서버가 동의 상태에만 status_at_formatted 제공)', () => { + // PO 결정(2026-07-20): 날짜는 "동의한 항목만". 거부·철회·미설정은 서버가 status_at_formatted 를 + // null 로 내려주므로 날짜 줄이 렌더되지 않는다. 프론트는 status_label && status_at_formatted 가 둘 다 + // 있을 때만 날짜 줄을 그린다. + const text = serializeForSearch(section); + expect(text).toContain('consent?.status_label'); + expect(text).toContain('consent?.status_at_formatted'); + // 날짜 줄은 둘 다 있을 때만 (거부/미설정은 null → 미노출) + expect(text).toContain('!!consent?.status_label && !!consent?.status_at_formatted'); }); /** * @scenario entry=reject, subject=member, category=required - * @effects mypage_required_shows_locked_always_on_toggle + * @effects mypage_required_shows_locked_button */ - it('필수 카테고리 — 잠긴 「항상 켜짐」 토글 (액션 없음, lock 아이콘 + not-allowed)', () => { + it('필수 카테고리 — 상태 배지 + 잠금 버튼 (액션 없음, lock 아이콘 + not-allowed)', () => { const text = serializeForSearch(section); expect(text).toContain('consent?.is_required === true'); - expect(text).toContain('mypage.privacy.always_on'); - // 잠금 시각 표현: lock 아이콘 + not-allowed 커서 + // 상태 배지는 서버 status_badge_label 사용 + expect(text).toContain('consent?.status_badge_label'); + // 잠금 버튼: lock 아이콘 + not-allowed + locked_label expect(text).toContain('"name": "lock"'); expect(text).toContain('cursor-not-allowed'); - // 필수 행에는 required_label 배지도 노출 + expect(text).toContain('mypage.privacy.locked_label'); + // 필수 행 라벨 옆 required_label 배지도 유지 expect(text).toContain('mypage.privacy.required_label'); }); /** * @scenario entry=reject, subject=member, category=optional - * @effects mypage_optional_shows_on_off_toggle + * @effects mypage_optional_consented_shows_revoke_button */ - it('선택형 켜짐(is_consented) — 녹색 토글 + 「켜짐」 라벨, 클릭 시 철회 확인 모달', () => { + it('선택형 동의함 — 철회 버튼, 클릭 시 철회 확인 모달 (즉시 철회 아님)', () => { const text = serializeForSearch(section); - expect(text).toContain('consent?.is_consented === true'); - expect(text).toContain('mypage.privacy.toggle_on'); - // 켜짐 토글은 녹색 - expect(text).toContain('bg-green-500'); - // 클릭 시 철회 확인 모달 오픈 (즉시 철회 아님 — 오조작 방지) + expect(text).toContain('consent?.is_required !== true && consent?.is_consented === true'); + expect(text).toContain('mypage.privacy.revoke'); + // 클릭 시 철회 확인 모달 오픈 + 대상 키를 global 로 세팅 expect(text).toContain('"target": "revoke_confirm_modal"'); + expect(text).toContain('"revokeTargetKey"'); }); - it('선택형 꺼짐(!is_consented) — 회색 토글 + 「꺼짐」 라벨, 클릭 시 즉시 grant', () => { + /** + * @scenario entry=reject, subject=member, category=optional + * @effects mypage_optional_unset_shows_grant_button + */ + it('선택형 거부함/미설정 — 동의 버튼, 클릭 시 동의 확인 모달 (즉시 grant 아님)', () => { const text = serializeForSearch(section); - expect(text).toContain('consent?.is_consented !== true'); - expect(text).toContain('mypage.privacy.toggle_off'); - // 꺼짐 토글은 회색 - expect(text).toContain('bg-gray-300'); - // 클릭 시 POST /consent/grant - expect(text).toContain('"target": "/api/plugins/sirsoft-gdpr/consent/grant"'); + expect(text).toContain('consent?.is_required !== true && consent?.is_consented !== true'); + expect(text).toContain('mypage.privacy.grant'); + // 토글 즉시 grant 가 아니라 동의 확인 모달 경유 (grant_confirm_modal) + expect(text).toContain('"target": "grant_confirm_modal"'); + expect(text).toContain('grantTargetKey'); }); - it('grant onSuccess 에서 refetch + syncConsent + 토스트 호출', () => { - // 회귀 가드: 꺼짐→켜짐 시 interceptor functionalConsented 즉시 갱신 (동의 즉시성). + it('이슈 #430 (3안) — 상태 배지는 라벨 줄에, 날짜 줄은 날짜+동의 순서 (배지↔날짜 동의 중복 제거)', () => { const text = serializeForSearch(section); - expect(text).toContain('"dataSourceId": "gdprMeConsents"'); - const matches = text.match(/"handler":\s*"sirsoft-gdpr\.syncConsent"/g) ?? []; - expect(matches.length).toBeGreaterThanOrEqual(1); + // 배지가 라벨 옆 flex-wrap 줄에 위치 (라벨 label_suffix 와 같은 컨테이너) + expect(text).toContain('items-center gap-2 flex-wrap'); + expect(text).toContain('consent?.status_badge_label'); + // 날짜 줄은 날짜가 먼저, 그 뒤에 동의 라벨 (동사 중복 회피 — 상태는 배지가 말함) + const idxDate = text.indexOf('status_at_formatted'); + const idxLabelAfter = text.indexOf('status_label', idxDate); + expect(idxDate).toBeGreaterThan(-1); + expect(idxLabelAfter).toBeGreaterThan(idxDate); }); - it('꺼짐 토글에 grantingKey 비교 disabled — 처리 중 중복 클릭 차단', () => { + it('「켜짐/꺼짐」 토글 표현 및 스위치 마크업이 부활하지 않는다 (회귀 가드)', () => { + // 토글 제거 회귀 가드: user 템플릿 w-11 safelist 붕괴 + '켜짐/꺼짐' 표현 어색함으로 버튼 전환. const text = serializeForSearch(section); - expect(text).toContain("_local.grantingKey === consent?.consent_key"); + expect(text).not.toContain('mypage.privacy.toggle_on'); + expect(text).not.toContain('mypage.privacy.toggle_off'); + // 스위치 knob 마크업(translate-x-6 / translate-x-1 + rounded-full 스위치 트랙)이 없어야 함 + expect(text).not.toContain('h-6 w-11'); }); /** * @scenario entry=reject, subject=member, category=optional * @effects mypage_enable_all_bar_when_optional_off */ - it('꺼진 선택 항목이 있을 때만 「모두 켜기」 바 노출 + POST /consent/cookie 로 일괄 동의', () => { + it('동의 안 한 선택 항목이 있을 때만 「모두 동의」 바 노출 + POST /consent/cookie 로 일괄 동의', () => { const text = serializeForSearch(section); - // 노출 조건: 선택형(is_required !== true) 중 꺼진(is_consented !== true) 항목 존재 + // 노출 조건: 선택형(is_required !== true) 중 미동의(is_consented !== true) 항목 존재 expect(text).toContain("c?.is_required !== true && c?.is_consented !== true"); expect(text).toContain('mypage.privacy.enable_all'); expect(text).toContain('mypage.privacy.enable_all_hint'); @@ -350,12 +402,15 @@ describe('extensions/mypage_privacy_tab.json — 마이페이지 GDPR 동의 매 expect(text).toContain('mypage.privacy.enable_all_success'); }); - it('제거된 표 배지/버튼 구조가 부활하지 않는다 (회귀 가드)', () => { - // 재설계로 상태 배지(bg-amber-100)·철회 빨간 버튼(bg-red-600 버튼)·can_grant/can_revoke 분기가 - // 토글로 대체됨. can_grant/can_revoke 메타는 서버 Resource 에 남아있으나 레이아웃은 미참조. + it('grant/revoke onSuccess 에서 refetch + syncConsent 호출 (동의 즉시성)', () => { + // 회귀 가드: 동의/철회 시 interceptor 즉시 갱신 — 모달의 sequence onSuccess 에 syncConsent 존재. + const text = serializeForSearch(root); + const matches = text.match(/"handler":\s*"sirsoft-gdpr\.syncConsent"/g) ?? []; + expect(matches.length).toBeGreaterThanOrEqual(2); + }); + + it('제거된 amber 갱신 분기 / rejected_label 이 부활하지 않는다 (회귀 가드)', () => { const text = serializeForSearch(section); - expect(text).not.toContain('consent?.can_revoke === true'); - expect(text).not.toContain('consent?.can_grant === true'); expect(text).not.toContain('mypage.privacy.rejected_label'); expect(text).not.toContain('needs_renewal_this_item'); }); diff --git a/plugins/_bundled/sirsoft-gdpr/resources/lang/en.json b/plugins/_bundled/sirsoft-gdpr/resources/lang/en.json index ae46dac7..ec4ab41b 100644 --- a/plugins/_bundled/sirsoft-gdpr/resources/lang/en.json +++ b/plugins/_bundled/sirsoft-gdpr/resources/lang/en.json @@ -271,16 +271,18 @@ "revoke": "Revoke", "grant": "Consent", "grant_again": "Consent Again", - "always_on": "Always On", - "toggle_on": "On", - "toggle_off": "Off", - "enable_all": "Enable All", - "enable_all_hint": "You have optional items turned off. Enable them all at once.", - "enable_all_success": "All optional items have been enabled.", + "label_suffix": "{label} cookies", + "locked_label": "Required", + "enable_all": "Consent to all", + "enable_all_hint": "You have optional items you haven't consented to. Consent to them all at once.", + "enable_all_success": "You have consented to all optional items.", "required_label": "Required", "required_hint": "This item is required for service operation and cannot be revoked.", + "grant_confirm_title": "Confirm Cookie Consent", + "grant_confirm_message": "Do you want to consent to this cookie? Once consented, this category's cookies will be used, and you can revoke it anytime.", "revoke_confirm_title": "Revoke Consent", "revoke_confirm_message": "Are you sure you want to revoke this consent? Revoking consent may limit features related to that category.", + "revoke_target_consented_at": "Consented · {date}", "no_consents": "No active consent items.", "needs_renewal": "The cookie policy has been updated. Please renew your consent.", "needs_renewal_with_version": "The cookie policy has been updated to version {version}. Please review the optional consent items.", diff --git a/plugins/_bundled/sirsoft-gdpr/resources/lang/ko.json b/plugins/_bundled/sirsoft-gdpr/resources/lang/ko.json index e31291fb..1c7253f2 100644 --- a/plugins/_bundled/sirsoft-gdpr/resources/lang/ko.json +++ b/plugins/_bundled/sirsoft-gdpr/resources/lang/ko.json @@ -271,16 +271,18 @@ "revoke": "철회", "grant": "동의", "grant_again": "다시 동의", - "always_on": "항상 켜짐", - "toggle_on": "켜짐", - "toggle_off": "꺼짐", - "enable_all": "모두 켜기", - "enable_all_hint": "동의하지 않은 선택 항목이 있습니다. 한 번에 모두 켤 수 있습니다.", - "enable_all_success": "선택 항목을 모두 켰습니다.", + "label_suffix": "{label} 쿠키", + "locked_label": "필수", + "enable_all": "모두 동의", + "enable_all_hint": "동의하지 않은 선택 항목이 있습니다. 한 번에 모두 동의할 수 있습니다.", + "enable_all_success": "선택 항목에 모두 동의했습니다.", "required_label": "필수", "required_hint": "서비스 제공에 필수적이라 철회할 수 없습니다.", + "grant_confirm_title": "쿠키 동의 확인", + "grant_confirm_message": "이 쿠키에 동의하시겠습니까? 동의하면 해당 카테고리의 쿠키가 사용되며, 언제든 다시 철회할 수 있습니다.", "revoke_confirm_title": "동의 철회 확인", "revoke_confirm_message": "이 항목을 철회하시겠습니까? 동의를 철회하면 해당 카테고리 관련 기능이 제한될 수 있습니다.", + "revoke_target_consented_at": "동의 · {date}", "no_consents": "활성 동의 항목이 없습니다.", "needs_renewal": "쿠키 정책이 변경되었습니다. 동의를 갱신해주세요.", "needs_renewal_with_version": "쿠키 정책이 버전 {version} 으로 변경되었습니다. 선택 항목의 동의를 다시 확인해주세요.", diff --git a/plugins/_bundled/sirsoft-gdpr/src/Http/Resources/GdprUserConsentResource.php b/plugins/_bundled/sirsoft-gdpr/src/Http/Resources/GdprUserConsentResource.php index 05b51579..42c3ee4e 100644 --- a/plugins/_bundled/sirsoft-gdpr/src/Http/Resources/GdprUserConsentResource.php +++ b/plugins/_bundled/sirsoft-gdpr/src/Http/Resources/GdprUserConsentResource.php @@ -39,6 +39,39 @@ class GdprUserConsentResource extends BaseApiResource // 선택형 항목 중 정책 버전이 옛 버전인지 — 필수는 ePrivacy Art.5(3) 면제로 갱신 대상 아님. $needsRenewalThisItem = ! $isRequired && $isConsented && $policyVersion !== '' && $policyVersion !== $currentPolicyVersion; + // 이슈 #430 — 마이페이지 「내 동의 현황」 각 행의 상태를 서버가 판정한다. + // 상태 판정: 필수 → required(동의), 선택 동의중 → consented(동의), + // 선택 명시 거부 → rejected(거부), 선택 철회 이력 → revoked(철회), 그 외(신규 미선택) → none. + if ($isRequired) { + $status = 'required'; + } elseif ($isConsented) { + $status = 'consented'; + } elseif ((bool) $this->is_rejected) { + $status = 'rejected'; + } elseif ($this->revoked_at !== null) { + $status = 'revoked'; + } else { + $status = 'none'; + } + // 표시 날짜/라벨은 "동의한 항목만" 노출한다 (PO 결정, 2026-07-20). + // 동의 상태(required·consented)만 동의일 + '동의' 라벨을 내려주고, + // 거부·철회·미선택은 status_label·status_at_formatted 를 null 로 두어 프론트가 날짜 줄을 렌더하지 않는다. + // (거부일·철회일은 사용자 화면 확인 가치가 낮고, 감사 로그·관리자 화면에서 확인 가능) + $isConsentState = $status === 'required' || $status === 'consented'; + $statusRawDate = $isConsentState ? $this->consented_at : null; + $statusLabel = $isConsentState + ? __('sirsoft-gdpr::messages.mypage.privacy.status.granted') + : null; + // 이슈 #430 (버튼 재설계) — 상태 배지 문구. 토글 대신 배지+단일버튼 UI 에서 + // 오른쪽 배지에 노출한다. 철회 이력(revoked)과 신규 미선택(none)은 '미설정'으로 통합. + $statusBadgeKey = match ($status) { + 'required' => 'required', + 'consented' => 'consented', + 'rejected' => 'rejected', + default => 'unset', // revoked, none + }; + $statusBadgeLabel = __('sirsoft-gdpr::messages.mypage.privacy.badge.'.$statusBadgeKey); + return [ 'id' => $this->id, 'consent_key' => $consentKey, @@ -56,6 +89,13 @@ class GdprUserConsentResource extends BaseApiResource // 비활성 (철회 또는 신규) → 다시 동의 (기존 동작). 'can_grant' => ! $isRequired && (! $isConsented || $needsRenewalThisItem), 'needs_renewal_this_item' => $needsRenewalThisItem, + // 이슈 #430 — 상태별 단일 표시 필드. 프론트는 status 로 색만 분기하고 나머지는 그대로 렌더. + 'status' => $status, + 'status_label' => $statusLabel, + 'status_badge_label' => $statusBadgeLabel, + 'status_at_formatted' => $statusRawDate !== null + ? $this->formatDateTimeStringForUser($statusRawDate) + : null, 'consented_at' => $this->formatDateTimeStringForUser($this->consented_at), 'revoked_at' => $this->formatDateTimeStringForUser($this->revoked_at), 'consent_count' => (int) $this->consent_count, diff --git a/plugins/_bundled/sirsoft-gdpr/src/Services/CookieCategoryService.php b/plugins/_bundled/sirsoft-gdpr/src/Services/CookieCategoryService.php index 7f5b397f..141be05a 100644 --- a/plugins/_bundled/sirsoft-gdpr/src/Services/CookieCategoryService.php +++ b/plugins/_bundled/sirsoft-gdpr/src/Services/CookieCategoryService.php @@ -157,9 +157,10 @@ class CookieCategoryService /** * consent_key 에 대응하는 사용자 친화 설명을 현재 locale 기준으로 반환합니다. * - * 마이페이지 「내 동의 현황」 의 동의 항목 컬럼에서 영문 식별자 대신 회원이 - * 즉시 이해할 수 있는 한 줄 설명을 노출하기 위함. 카탈로그에 매칭되는 카테고리가 - * 없거나 설명이 비어 있으면 null 반환 — 호출 측이 fallback 처리. + * 설명(description)은 관리자 설정 화면에 편집 UI 가 없어(라벨만 편집 가능) 운영자가 + * 카테고리를 저장하면 description 이 유실된다. 따라서 설명은 관리자 설정(getCategories)이 + * 아니라 항상 코드 기본값(getDefaultCategories)에서 가져온다 — 운영자 라벨 커스텀과 + * 무관하게 마이페이지·배너에 일관된 설명이 노출된다 (이슈 #430). 기본값에도 없으면 null. * * @param string $consentKey 동의 항목 키 (예: cookie_analytics) * @return string|null @@ -170,7 +171,7 @@ class CookieCategoryService $locale = (string) app()->getLocale(); $fallbackLocale = (string) config('app.fallback_locale', 'en'); - foreach ($this->getCategories() as $cat) { + foreach ($this->getDefaultCategories() as $cat) { if (($cat['key'] ?? null) !== $bareKey) { continue; } diff --git a/plugins/_bundled/sirsoft-gdpr/tests/Unit/Http/Resources/GdprUserConsentResourceTest.php b/plugins/_bundled/sirsoft-gdpr/tests/Unit/Http/Resources/GdprUserConsentResourceTest.php new file mode 100644 index 00000000..d642424d --- /dev/null +++ b/plugins/_bundled/sirsoft-gdpr/tests/Unit/Http/Resources/GdprUserConsentResourceTest.php @@ -0,0 +1,124 @@ + + */ + private function toArray(GdprUserConsent $consent): array + { + return (new GdprUserConsentResource($consent))->toArray(Request::create('/')); + } + + public function test_required_category_maps_to_required_status_with_consented_date(): void + { + $consent = new GdprUserConsent([ + 'consent_key' => 'cookie_necessary', + 'is_consented' => true, + 'is_rejected' => false, + 'consented_at' => '2026-07-14 09:00:00', + 'policy_version' => '1', + ]); + + $result = $this->toArray($consent); + + $this->assertSame('required', $result['status']); + // 필수는 '동의' 라벨을 granted 키로 공유. + $this->assertSame(__('sirsoft-gdpr::messages.mypage.privacy.status.granted'), $result['status_label']); + $this->assertNotNull($result['status_at_formatted']); + $this->assertSame(__('sirsoft-gdpr::messages.mypage.privacy.badge.required'), $result['status_badge_label']); + } + + public function test_consented_optional_maps_to_consented_status_with_consented_date(): void + { + $consent = new GdprUserConsent([ + 'consent_key' => 'cookie_functional', + 'is_consented' => true, + 'is_rejected' => false, + 'consented_at' => '2026-07-14 09:00:00', + 'policy_version' => '1', + ]); + + $result = $this->toArray($consent); + + $this->assertSame('consented', $result['status']); + $this->assertSame(__('sirsoft-gdpr::messages.mypage.privacy.status.consented'), $result['status_label']); + $this->assertNotNull($result['status_at_formatted']); + $this->assertSame(__('sirsoft-gdpr::messages.mypage.privacy.badge.consented'), $result['status_badge_label']); + } + + public function test_rejected_optional_maps_to_rejected_status_with_no_date_label(): void + { + // PO 결정(2026-07-20) — 날짜/라벨은 "동의한 항목만" 노출. 거부는 상태만 rejected 로 내려주고 + // status_label·status_at_formatted 는 null (프론트가 날짜 줄을 렌더하지 않음). + $consent = new GdprUserConsent([ + 'consent_key' => 'cookie_analytics', + 'is_consented' => false, + 'is_rejected' => true, + 'rejected_at' => '2026-07-10 08:00:00', + 'policy_version' => '1', + ]); + + $result = $this->toArray($consent); + + $this->assertSame('rejected', $result['status']); + $this->assertNull($result['status_label']); + $this->assertNull($result['status_at_formatted']); + $this->assertSame(__('sirsoft-gdpr::messages.mypage.privacy.badge.rejected'), $result['status_badge_label']); + } + + public function test_revoked_optional_maps_to_revoked_status_with_no_date_label(): void + { + // 철회도 거부와 동일 — 동의 상태가 아니므로 날짜/라벨 미노출. + $consent = new GdprUserConsent([ + 'consent_key' => 'cookie_marketing', + 'is_consented' => false, + 'is_rejected' => false, + 'revoked_at' => '2026-07-12 07:00:00', + 'policy_version' => '1', + ]); + + $result = $this->toArray($consent); + + $this->assertSame('revoked', $result['status']); + $this->assertNull($result['status_label']); + $this->assertNull($result['status_at_formatted']); + // 철회는 신규 미선택과 함께 '미설정'(unset) 배지로 통합. + $this->assertSame(__('sirsoft-gdpr::messages.mypage.privacy.badge.unset'), $result['status_badge_label']); + } + + public function test_new_untouched_optional_maps_to_none_with_null_label_and_date(): void + { + $consent = new GdprUserConsent([ + 'consent_key' => 'cookie_analytics', + 'is_consented' => false, + 'is_rejected' => false, + 'policy_version' => '1', + ]); + + $result = $this->toArray($consent); + + $this->assertSame('none', $result['status']); + // 신규 미선택은 라벨/날짜 모두 미노출 → 프론트가 날짜 줄 자체를 렌더하지 않음. + $this->assertNull($result['status_label']); + $this->assertNull($result['status_at_formatted']); + $this->assertSame(__('sirsoft-gdpr::messages.mypage.privacy.badge.unset'), $result['status_badge_label']); + } +} diff --git a/plugins/_bundled/sirsoft-gdpr/tests/Unit/Services/CookieCategoryServiceTest.php b/plugins/_bundled/sirsoft-gdpr/tests/Unit/Services/CookieCategoryServiceTest.php index f3817d85..d8c4c3a8 100644 --- a/plugins/_bundled/sirsoft-gdpr/tests/Unit/Services/CookieCategoryServiceTest.php +++ b/plugins/_bundled/sirsoft-gdpr/tests/Unit/Services/CookieCategoryServiceTest.php @@ -162,4 +162,37 @@ class CookieCategoryServiceTest extends PluginTestCase app()->setLocale('ko'); $this->assertNull($this->makeService()->getLabelForKey('cookie_analytics')); } + + public function test_get_description_uses_default_even_when_admin_setting_omits_description(): void + { + // 이슈 #430 회귀 가드: 관리자 설정 화면에 설명 편집 UI 가 없어, 운영자가 라벨만 저장하면 + // cookie_categories 에 description 이 유실된다. 설명은 관리자 설정이 아니라 항상 코드 + // 기본값에서 가져와야 한다 — 아래처럼 description 이 없는 설정이 저장돼도 기본 설명이 나온다. + $this->mockSettings([ + 'cookie_categories' => json_encode([ + ['key' => 'functional', 'required' => false, 'label' => ['ko' => '기능', 'en' => 'Functional']], + ]), + ]); + + app()->setLocale('ko'); + $desc = $this->makeService()->getDescriptionForKey('cookie_functional'); + $this->assertNotNull($desc); + $this->assertNotSame('', $desc); + // 라벨은 운영자 커스텀('기능')을 유지하되, 설명은 코드 기본값에서 온다. + $this->assertSame('기능', $this->makeService()->getLabelForKey('cookie_functional')); + } + + public function test_get_description_ignores_admin_custom_description(): void + { + // 설명은 관리자 설정이 아니라 코드 기본값 SSoT — 운영자가 설정에 다른 설명을 넣어도 + // 코드 기본 설명이 우선한다 (설명 편집 UI 부재 정합). + $this->mockSettings([ + 'cookie_categories' => json_encode([ + ['key' => 'functional', 'required' => false, 'label' => ['ko' => '기능'], 'description' => ['ko' => '운영자가 임의로 넣은 설명']], + ]), + ]); + + app()->setLocale('ko'); + $this->assertNotSame('운영자가 임의로 넣은 설명', $this->makeService()->getDescriptionForKey('cookie_functional')); + } } diff --git a/plugins/_bundled/sirsoft-gdpr/tests/scenarios/cookie-rejection.yaml b/plugins/_bundled/sirsoft-gdpr/tests/scenarios/cookie-rejection.yaml index 099238ec..0ae5287f 100644 --- a/plugins/_bundled/sirsoft-gdpr/tests/scenarios/cookie-rejection.yaml +++ b/plugins/_bundled/sirsoft-gdpr/tests/scenarios/cookie-rejection.yaml @@ -73,10 +73,11 @@ effects: - consent_action_from_decision_granted_revoked_rejected # value/isRejection → granted/revoked/rejected 3분기 # 화면 표기 (레이아웃 렌더링) - - mypage_required_shows_locked_always_on_toggle # 마이페이지 필수 = 잠긴 '항상 켜짐' 토글 (액션/동의일 없음) - - mypage_optional_shows_on_off_toggle # 마이페이지 선택 = on/off 토글 (켜짐 녹색→철회, 꺼짐 회색→동의) - - mypage_no_consent_date_column_for_user # 사용자 화면에 동의일 컬럼 미노출 (감사/관리자 전용) - - mypage_enable_all_bar_when_optional_off # 꺼진 선택 항목 있을 때 '모두 켜기' 바 노출 + - mypage_required_shows_locked_button # 마이페이지 필수 = 상태 배지 + 잠금 버튼 (액션 없음) + - mypage_optional_consented_shows_revoke_button # 마이페이지 선택 동의함 = 철회 버튼 → 철회 확인 모달 + - mypage_optional_unset_shows_grant_button # 마이페이지 선택 거부함/미설정 = 동의 버튼 → 동의 확인 모달 + - mypage_status_date_line_consented_only # 동의한 항목만 날짜 줄 노출 (서버 status_at_formatted, 거부/미설정은 null) + - mypage_enable_all_bar_when_optional_off # 미동의 선택 항목 있을 때 '모두 동의' 바 노출 - admin_log_action_badge_rejected_amber # 관리자 동의이력 rejected 배지(amber) - admin_log_filter_rejected_checkbox # 관리자 [거부] 필터 체크박스 - banner_reject_button_sends_intent_reject # 배너 거부 버튼이 intent:'reject' 전송