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

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

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

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

24 KiB

Settings API 레퍼런스

소유: plugin sirsoft-gdpr · 생성: php artisan api:docgen (실측 기반). @generated 블록은 재생성 시 갱신되며, 사람이 작성한 설명은 보존됩니다.


TL;DR (5초 요약)

1. 이 문서는 실제 API 호출로 실측한 Settings 엔드포인트 레퍼런스입니다
2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 요청 예시(raw HTTP) + 실측 응답 필드 표 + 응답 예시(envelope)
3. 응답 필드의 예시값·응답 예시 JSON 은 실제 호출 응답에서 관측된 값입니다
4. 갱신: 코드 변경 후 php artisan api:docgen 재실행
5. 설명(TODO) 칸은 사람이 채웁니다

GET /api/plugins/sirsoft-gdpr/admin/settings

  • 라우트명: api.plugins.sirsoft-gdpr.admin.settings.show
  • 컨트롤러: Plugins\Sirsoft\Gdpr\Http\Controllers\Admin\GdprAdminSettingsController@show
  • 인증/권한: auth:sanctum + permission:sirsoft-gdpr.privacy.view

요청 파라미터

요청 파라미터 없음.

요청 예시

GET /api/plugins/sirsoft-gdpr/admin/settings HTTP/1.1
Host: api.example.com
Accept: application/json
Authorization: Bearer {YOUR_TOKEN}

응답 필드 (data 내부)

단건 응답: data 객체의 필드.

필드 타입 실측 예시값 용도/설명
settings object {"privacy_policy_slug":"privacy","legal_entity_name":"","… GDPR 플러그인의 관리자 설정 전체 객체. 정책 메타데이터(slug/법인명/저장 위치), 배너 설정, 쿠키 카테고리 카탈로그, 차단 도메인을 담으며 cookie_categories 등 JSON 필드는 디코드되어 객체/배열로 노출됩니다

응답 예시

HTTP/1.1 200
{
    "success": true,
    "message": "성공적으로 처리되었습니다.",
    "data": {
        "settings": {
            "privacy_policy_slug": "privacy",
            "legal_entity_name": "",
            "data_storage_location": "",
            "banner_enabled": true,
            "banner_position": "bottom_bar",
            "blocked_domains": {
                "functional": [
                    "*.crisp.chat",
                    "widget.intercom.io"
                ],
                "analytics": [
                    "google-analytics.com"
                ],
                "marketing": [
                    "facebook.com"
                ]
            },
            "cookie_categories": [
                {
                    "key": "necessary",
                    "required": true,
                    "label": {
                        "ko": "필수 쿠키",
                        "en": "Strictly Necessary"
                    },
                    "description": {
                        "ko": "세션·CSRF·로그인 토큰, 장바구니 식별자, 사용자가 직접 고른 언어 설정과 화면 테마, 쿠키 동의 기록 등 사이트 운영에 반드시 필요한 항목입니다. 비활성화할 수 없습니다.",
                        "en": "Strictly necessary for site operation: session/CSRF/auth tokens, shopping basket identifier, user-selected language preference and display theme, cookie consent record. Cannot be disabled."
                    }
                },
                {
                    "key": "functional",
                    "required": false,
                    "label": {
                        "ko": "기능 쿠키",
                        "en": "Functional"
                    },
                    "description": {
                        "ko": "사용자 선호도(표시 통화 등)를 기억하는 쿠키입니다. 거부 시 매 방문마다 기본값으로 표시됩니다.",
                        "en": "Cookies that remember user preferences such as display currency. If declined, defaults are used on every visit."
                    }
                },
                {
                    "key": "analytics",
                    "required": false,
                    "label": {
                        "ko": "분석 쿠키",
                        "en": "Analytics"
                    },
                    "description": {
                        "ko": "방문자가 사이트를 어떻게 이용하는지 익명으로 측정해 더 나은 서비스를 만드는 데 사용됩니다. (예: Google Analytics, Hotjar)",
                        "en": "Used to anonymously measure how visitors use the site so we can improve it. (e.g. Google Analytics, Hotjar)"
                    }
                },
                {
                    "key": "marketing",
                    "required": false,
                    "label": {
                        "ko": "마케팅 쿠키",
                        "en": "Marketing"
                    },
                    "description": {
                        "ko": "관심사에 맞는 광고를 보여주거나, 광고가 얼마나 효과적이었는지 측정하는 데 사용됩니다. SNS 영상 임베드 등도 포함됩니다. (예: Facebook 픽셀, Google 광고, YouTube 영상)",
                        "en": "Used to show ads relevant to your interests, measure ad performance, and embed social media content. (e.g. Facebook Pixel, Google Ads, YouTube embeds)"
                    }
                }
            ]
        }
    }
}

에러 응답

상태코드 의미 발생 조건
401 Unauthenticated 유효한 Bearer 토큰이 없거나 만료된 경우
403 Forbidden 요구 권한(sirsoft-gdpr.privacy.view)이 없는 경우

설명 GDPR 플러그인의 관리자 설정 전체를 반환해 관리자 설정 화면 폼에 바인딩합니다. auth:sanctum과 sirsoft-gdpr.privacy.view 권한이 필요합니다. cookie_categories 같은 JSON 필드는 디코드하여 객체/배열로 노출합니다. 조회 전용이며 정책 버전 발행 등 부수 효과는 없습니다.

추가 응답 필드

필드 타입 예시값 용도/설명
settings.necessary_storage_allowlist object {"localStorage":["g7_locale","g7_filters_*"],"sessionStorage":[],"cookie":["laravel_maintenance"]} 기능 쿠키 미동의 상태에서도 저장이 허용되는 항목(운영자 편집 대상). 저장소 구분(localStorage/sessionStorage/cookie)별 문자열 배열이며, 끝의 * 는 앞부분 매칭입니다
settings.necessary_storage_locked object {"localStorage":["auth_token"],"sessionStorage":[],"cookie":["XSRF-TOKEN","laravel_session","gdpr_session"]} 지울 수 없는 잠금 항목. 설정이 아니라 코드가 정하므로 저장 요청에 담아 보내도 반영되지 않으며, 판정에는 언제나 운영자 목록과 합쳐 적용됩니다. 세션 쿠키 이름은 session.cookie 설정에서 런타임 해석됩니다
default_blocked_domains_preview object {"functional":["*.crisp.chat"],"analytics":["google-analytics.com"],"marketing":[]} 출하 기본 차단 도메인 카탈로그. 관리자 화면 TagInput 의 자동완성 추천 출처입니다
default_necessary_allowlist_preview object {"localStorage":["g7_locale","g7_color_scheme"],"sessionStorage":[],"cookie":["laravel_maintenance"]} 출하 기본 허용목록 카탈로그. 위와 같이 추천 출처이며 잠금 항목은 담기지 않습니다

두 *_preview 는 settings 바깥, data 최상위에 실립니다.

PUT /api/plugins/sirsoft-gdpr/admin/settings

  • 라우트명: api.plugins.sirsoft-gdpr.admin.settings.update
  • 컨트롤러: Plugins\Sirsoft\Gdpr\Http\Controllers\Admin\GdprAdminSettingsController@update
  • 인증/권한: auth:sanctum + permission:sirsoft-gdpr.privacy.update

요청 파라미터

이름 위치 타입 필수 허용값 용도
privacy_policy_slug body string 아니오 max 100 개인정보 처리방침 페이지 slug(소문자·숫자·하이픈만). 비면 처리방침 링크가 비활성화되며 배너/마이페이지의 방침 링크 대상이 됩니다
legal_entity_name body string 아니오 max 200 법인명 / 운영 주체. 쿠키 배너와 마이페이지에 표시되는 사이트 운영 주체 이름입니다 (예: (주)회사명)
data_storage_location body string 아니오 max 200 데이터 저장 위치 표기(GDPR Art.13(1)(f)/PIPA 국가 단위 안내용). IP/CIDR·클라우드 리전 코드 등 보안 민감 식별자는 검증에서 차단됩니다
banner_enabled body boolean 아니오 — 쿠키 배너와 자동 차단 엔진의 통합 활성 토글. true일 때만 배너 노출과 도메인 기반 차단이 동작합니다
banner_position body string 아니오 bottom_bar, bottom_left_popup, bottom_right_popup, centered_modal 쿠키 배너 노출 위치(하단 바/좌하단 팝업/우하단 팝업/중앙 모달)
cookie_categories body string 아니오 — 쿠키 카테고리 카탈로그(JSON 문자열 또는 배열). 각 항목의 key는 necessary/functional/analytics/marketing 중 하나이며 label.ko/label.en이 필수입니다
blocked_domains body array 아니오 — 카테고리별 차단 도메인 패턴 배열(necessary 제외). FQDN 및 *. 와일드카드 prefix만 허용되며 textarea 줄바꿈 입력도 배열로 정규화됩니다

요청 예시

PUT /api/plugins/sirsoft-gdpr/admin/settings HTTP/1.1
Host: api.example.com
Accept: application/json
Authorization: Bearer {YOUR_TOKEN}
Content-Type: application/json

{
    "privacy_policy_slug": "example-key",
    "legal_entity_name": "예시 이름",
    "data_storage_location": "예시값",
    "banner_enabled": true,
    "banner_position": "bottom_bar",
    "cookie_categories": "예시값",
    "blocked_domains": [
        "예시값"
    ]
}

응답 필드 (data 내부)

단건 응답: data 객체의 필드.

필드 타입 실측 예시값 용도/설명
settings object {"privacy_policy_slug":"privacy","banner_enabled":true,…} 저장 직후 다시 로드한 GDPR 플러그인 설정 전체 객체(GET 응답의 settings 와 동일 형태). 저장 결과를 관리자 폼에 되반영하는 데 사용합니다. cookie_categories 는 DB 에 JSON 문자열로 저장되지만 응답에서는 디코드되어 배열로 노출됩니다
settings.privacy_policy_slug string privacy 저장된 개인정보 처리방침 페이지 slug
settings.legal_entity_name string (주)회사명 저장된 법인명 / 운영 주체
settings.data_storage_location string 대한민국 저장된 데이터 저장 위치 표기(국가 단위)
settings.banner_enabled boolean true 저장된 쿠키 배너·자동 차단 통합 활성 토글
settings.banner_position string bottom_bar 저장된 배너 노출 위치(bottom_bar / bottom_left_popup / bottom_right_popup / centered_modal)
settings.cookie_categories array [{"key":"necessary","required":true,"label":{"ko":"필수 쿠키",…}}] 저장된 쿠키 카테고리 카탈로그 배열(디코드된 상태). 각 항목은 key, required, 다국어 label/description
settings.blocked_domains object {"functional":["*.crisp.chat"],"analytics":[],"marketing":[]} 저장된 카테고리별 차단 도메인 패턴(키→도메인 배열, necessary 제외)

응답 예시

HTTP/1.1 200
{
    "success": true,
    "message": "설정이 저장되었습니다.",
    "data": {
        "settings": {
            "privacy_policy_slug": "privacy",
            "legal_entity_name": "(주)회사명",
            "data_storage_location": "대한민국",
            "banner_enabled": true,
            "banner_position": "bottom_bar",
            "cookie_categories": [
                {
                    "key": "necessary",
                    "required": true,
                    "label": {
                        "ko": "필수 쿠키",
                        "en": "Strictly Necessary"
                    },
                    "description": {
                        "ko": "세션·CSRF·로그인 토큰, 장바구니 식별자 등 사이트 운영에 반드시 필요한 항목입니다. 비활성화할 수 없습니다.",
                        "en": "Strictly necessary for site operation. Cannot be disabled."
                    }
                },
                {
                    "key": "analytics",
                    "required": false,
                    "label": {
                        "ko": "분석 쿠키",
                        "en": "Analytics"
                    },
                    "description": {
                        "ko": "방문자가 사이트를 어떻게 이용하는지 익명으로 측정합니다.",
                        "en": "Used to anonymously measure how visitors use the site."
                    }
                }
            ],
            "blocked_domains": {
                "analytics": [
                    "google-analytics.com"
                ]
            }
        }
    }
}

참고 위 표에 정의되지 않은 요청 필드는 검증 대상에서 제외되며(FormRequest::validated() 결과만 사용), 저장 로직에 전달되지 않습니다.

에러 응답

상태코드 의미 발생 조건
401 Unauthenticated 유효한 Bearer 토큰이 없거나 만료된 경우
403 Forbidden 요구 권한(sirsoft-gdpr.privacy.update)이 없는 경우
422 Unprocessable Entity 요청 파라미터가 검증 규칙을 위반한 경우 (error.errors 에 필드별 메시지)

설명 검증된 GDPR 관리자 설정을 저장합니다. auth:sanctum과 sirsoft-gdpr.privacy.update 권한이 필요합니다. 설정만 저장할 뿐 정책 버전은 자동 발행되지 않으며, 재동의가 필요한 변경이라면 운영자가 「+ 새 버전 발행」을 별도로 눌러야 합니다. 배너 문구·위치, 쿠키 카테고리, 차단 도메인, 필수 저장 항목 허용목록 등을 갱신하는 쓰기 엔드포인트입니다.

추가 요청 파라미터

파라미터 위치 타입 필수 기본값 설명
necessary_storage_allowlist body object 아니오 — 저장소 구분별 허용 항목 목록. 키는 localStorage / sessionStorage / cookie 셋만 허용되며(그 외는 422), 값은 문자열 배열입니다. 항목은 영문·숫자와 _ . : @ + - 로 이루어지고 최대 128자이며, 끝에 * 를 하나 붙이면 앞부분 매칭이 됩니다(* 를 앞이나 중간에 두면 422). textarea 줄바꿈 문자열로 보내도 배열로 정규화됩니다

키를 보내지 않으면 기존 저장값을 그대로 둡니다. 빈 객체({})를 보내면 세 구분이 모두 빈 배열로 저장되어 잠금 항목만 남습니다.

검증 실패 시 에러 키는 necessary_storage_allowlist.{구분}.{인덱스} 형태이고, 알 수 없는 구분은 necessary_storage_allowlist.{구분} 으로 보고됩니다.

necessary_storage_locked 는 요청에 담아도 저장되지 않습니다 — 잠금 항목은 코드가 정하며 API 로 지울 수 없습니다.

GET /api/plugins/sirsoft-gdpr/settings

  • 라우트명: api.plugins.sirsoft-gdpr.settings
  • 컨트롤러: Plugins\Sirsoft\Gdpr\Http\Controllers\Public\GdprSettingsController@show
  • 인증/권한: 공개 (인증 불필요)

요청 파라미터

요청 파라미터 없음.

요청 예시

GET /api/plugins/sirsoft-gdpr/settings HTTP/1.1
Host: api.example.com
Accept: application/json

응답 필드 (data 내부)

단건 응답: data 객체의 필드.

필드 타입 실측 예시값 용도/설명
cookie_policy_version string 10 현재 발행된 최신 정책 버전 문자열(gdpr_policy_versions 테이블이 SSoT, 최신 row의 version 정수). 게스트/회원 동의 시점 비교 및 배너 재노출 판정에 사용됩니다
privacy_policy_slug string privacy 개인정보 처리방침 페이지 slug. 설정이 비어 있으면 null로 반환됩니다
privacy_policy_available boolean true 처리방침 slug가 설정되어 링크 노출이 가능한지 여부. slug가 비면 false입니다
legal_entity_name string `` 개인정보 처리 법인/사업자명. 미설정 시 빈 문자열입니다
data_storage_location string `` 데이터 저장 위치 표기 문자열(국가 단위 안내). 미설정 시 빈 문자열입니다
banner_enabled boolean true 쿠키 배너와 자동 차단의 통합 활성 여부. true일 때만 게스트에게 배너가 노출되고 도메인 차단 엔진이 동작합니다
banner_position string bottom_bar 쿠키 배너 노출 위치(bottom_bar / bottom_left_popup / bottom_right_popup / centered_modal)
cookie_categories array [{"key":"necessary","required":true,"label":{"ko":"필수 쿠키"… 배너·마이페이지 렌더링용 쿠키 카테고리 카탈로그 배열. 각 항목은 key, required(필수 여부), 다국어 label/description을 담습니다
blocked_domains object {"functional":["*.crisp.chat","widget.intercom.io"],"anal… 현재 설정된 카테고리별 차단 도메인 패턴(키→도메인 배열). 게스트도 차단이 동작해야 하므로 공개 응답에 노출되며 본 컨트롤러가 노출 SSoT입니다
default_blocked_domains_preview object {"functional":["*.crisp.chat","client.crisp.chat","*.inte… 플러그인 기본 차단 도메인 카탈로그 미리보기(변경 불가 기본값). 관리자가 커스텀 차단 목록을 편집할 때 참고용 기본 패턴을 보여줍니다

응답 예시

HTTP/1.1 200
{
    "success": true,
    "message": "성공적으로 처리되었습니다.",
    "data": {
        "cookie_policy_version": "1",
        "privacy_policy_slug": "privacy",
        "privacy_policy_available": true,
        "legal_entity_name": "",
        "data_storage_location": "",
        "banner_enabled": true,
        "banner_position": "bottom_bar",
        "cookie_categories": [
            {
                "key": "necessary",
                "required": true,
                "label": {
                    "ko": "필수 쿠키",
                    "en": "Strictly Necessary"
                },
                "description": {
                    "ko": "세션·CSRF·로그인 토큰, 장바구니 식별자, 사용자가 직접 고른 언어 설정과 화면 테마, 쿠키 동의 기록 등 사이트 운영에 반드시 필요한 항목입니다. 비활성화할 수 없습니다.",
                    "en": "Strictly necessary for site operation: session/CSRF/auth tokens, shopping basket identifier, user-selected language preference and display theme, cookie consent record. Cannot be disabled."
                }
            },
            {
                "key": "functional",
                "required": false,
                "label": {
                    "ko": "기능 쿠키",
                    "en": "Functional"
                },
                "description": {
                    "ko": "사용자 선호도(표시 통화 등)를 기억하는 쿠키입니다. 거부 시 매 방문마다 기본값으로 표시됩니다.",
                    "en": "Cookies that remember user preferences such as display currency. If declined, defaults are used on every visit."
                }
            },
            {
                "key": "analytics",
                "required": false,
                "label": {
                    "ko": "분석 쿠키",
                    "en": "Analytics"
                },
                "description": {
                    "ko": "방문자가 사이트를 어떻게 이용하는지 익명으로 측정해 더 나은 서비스를 만드는 데 사용됩니다. (예: Google Analytics, Hotjar)",
                    "en": "Used to anonymously measure how visitors use the site so we can improve it. (e.g. Google Analytics, Hotjar)"
                }
            },
            {
                "key": "marketing",
                "required": false,
                "label": {
                    "ko": "마케팅 쿠키",
                    "en": "Marketing"
                },
                "description": {
                    "ko": "관심사에 맞는 광고를 보여주거나, 광고가 얼마나 효과적이었는지 측정하는 데 사용됩니다. SNS 영상 임베드 등도 포함됩니다. (예: Facebook 픽셀, Google 광고, YouTube 영상)",
                    "en": "Used to show ads relevant to your interests, measure ad performance, and embed social media content. (e.g. Facebook Pixel, Google Ads, YouTube embeds)"
                }
            }
        ],
        "blocked_domains": {
            "functional": [
                "*.crisp.chat",
                "widget.intercom.io"
            ],
            "analytics": [
                "google-analytics.com"
            ],
            "marketing": [
                "facebook.com"
            ]
        },
        "default_blocked_domains_preview": {
            "functional": [
                "*.crisp.chat",
                "client.crisp.chat",
                "*.intercom.io",
                "widget.intercom.io",
                "*.tawk.to",
                "embed.tawk.to",
                "cdn.weglot.com",
                "*.weglot.com",
                "*.usercentrics.eu"
            ],
            "analytics": [
                "google-analytics.com",
                "*.google-analytics.com",
                "googletagmanager.com",
                "*.googletagmanager.com",
                "ssl.google-analytics.com",
                "*.hotjar.com",
                "static.hotjar.com",
                "*.mixpanel.com",
                "cdn.mxpnl.com",
                "*.amplitude.com",
                "cdn.amplitude.com",
                "*.segment.io",
                "*.segment.com",
                "wcs.naver.net",
                "wcs.naver.com",
                "*.beusable.net"
            ],
            "marketing": [
                "facebook.net",
                "connect.facebook.net",
                "facebook.com",
                "*.facebook.com",
                "doubleclick.net",
                "*.doubleclick.net",
                "googleadservices.com",
                "googlesyndication.com",
                "ads.google.com",
                "*.criteo.com",
                "static.criteo.net",
                "*.adnxs.com",
                "*.taboola.com",
                "cdn.taboola.com",
                "*.outbrain.com",
                "*.kakao.com",
                "analytics.ad.daum.net",
                "platform.twitter.com",
                "*.twitter.com",
                "platform.linkedin.com",
                "*.linkedin.com"
            ]
        }
    }
}

에러 응답

에러 없음 (공개 조회 — 요청 파라미터가 없고 컨트롤러/서비스가 도메인 예외를 던지지 않습니다. 설정 미저장 시에도 기본값으로 200 응답합니다).

설명 게스트도 접근할 수 있는 완전 공개 엔드포인트로, 인증 없이 호출됩니다. 공개 쿠키 동의 배너와 마이페이지 카드 렌더링에 필요한 쿠키 카테고리·배너 설정·차단 도메인과 현재 cookie_policy_version(정책 버전 서비스 기준)을 반환합니다. 차단 도메인은 게스트도 차단이 동작해야 하므로 공개 응답에 노출되며, 이 컨트롤러가 응답 노출의 기준(SSoT)입니다. 조회 전용입니다.