Modern PHP User Group 2026-09 정기모임 설치 리뷰에서 접수된 제보 2건. 설치 완료·실패·중단·필수파일 안내는 페이지 최상단에 뜨는데, 진행 로그를 보느라 화면이 아래에 머물러 있으면 안내가 눈에 들어오지 않았다. 네 경로 모두 DOM 변경이 끝난 뒤 결과 섹션으로 부드럽게 이동시킨다(모션 최소화 설정 시 즉시 이동). 미인증 3화면의 테마 버튼은 setTheme 을 params.theme 으로 불렀는데 핸들러는 action.target 만 읽는다. 엔진에 상호 폴백이 없어 클릭이 콘솔 경고 한 줄만 남기고 아무 일도 하지 않았다. 로그인 이후 화면은 핸들러를 거치지 않는 ThemeToggle 을 쓰기 때문에 정상이었고, 그래서 이 세 화면에서만 나타났다. 같은 계약 불일치가 편집기 액션 레시피(admin 9·basic 3)와 두 템플릿 문서에도 있어 함께 고쳤다. 편집기로 만든 액션은 생성 즉시 no-op 이 되는데 오류가 남지 않는다. 정적 검사 레지스트리가 오히려 틀린 계약(params.theme 필수)을 강제하고 있어 올바른 형태를 막고 있었으므로 두 미러를 함께 정정했다. 테마를 고치는 과정에서 별개 결함이 드러났다. GDPR 스토리지 인터셉터가 기능 쿠키 미동의 상태에서 필수 목록 밖 저장을 부팅마다 파기하는데, 화면 테마가 그 목록에서 빠져 있었다. 저장소의 setItem 호출을 기계 도출해 대조한 결과 같은 이유로 사라지던 항목이 13건 더 있었다 — 비회원 주문 조회, 결제창 복귀 기록, 본인인증 복귀 기록, 관리자 화면 상태, 자산 주소 형식 캐시. 전량 필수로 분류하고, 동의 안내 문구가 "다크모드는 기능 쿠키" 라고 말하던 부분을 사실에 맞게 정정했다(기설치본의 저장된 문구는 업그레이드 스텝이 정정하며, 운영자가 고친 문구는 건드리지 않는다). 재발 방지는 허용목록을 직접 import 하고 모집단을 디렉토리 순회로 도출하는 커버리지 테스트가 맡는다 — 손으로 열거하지 않으므로 새 저장 키가 등재를 빠뜨리면 붉어진다.
21 KiB
Consent API 레퍼런스
소유: plugin
sirsoft-gdpr· 생성:php artisan api:docgen(실측 기반). @generated 블록은 재생성 시 갱신되며, 사람이 작성한 설명은 보존됩니다.
TL;DR (5초 요약)
1. 이 문서는 실제 API 호출로 실측한 Consent 엔드포인트 레퍼런스입니다
2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 요청 예시(raw HTTP) + 실측 응답 필드 표 + 응답 예시(envelope)
3. 응답 필드의 예시값·응답 예시 JSON 은 실제 호출 응답에서 관측된 값입니다
4. 갱신: 코드 변경 후 php artisan api:docgen 재실행
5. 설명(TODO) 칸은 사람이 채웁니다
POST /api/plugins/sirsoft-gdpr/consent/cookie
- 라우트명:
api.plugins.sirsoft-gdpr.consent.cookie - 컨트롤러:
Plugins\Sirsoft\Gdpr\Http\Controllers\Public\GdprCookieConsentController@store - 인증/권한:
optional.sanctum(선택적 인증: 회원/비회원 모두 접근)
요청 파라미터 (Body)
| 파라미터 | 타입 | 필수 | 설명 |
|---|---|---|---|
| consents | object | 예 | { "cookie_<카테고리키>": bool } 형태의 동의 매트릭스. 최소 1개 이상. 필수 카테고리에 false를 보내면 거부됩니다(422) |
| source | string | 예 | 변경 경로. banner / preference_center / register / mypage 중 하나 |
| intent | string | 아니오 | 명시적 거부 신호. reject 만 허용합니다. 전송 시 선택형 미동의 항목(false)을 is_rejected=true로 저장하고 history action=rejected로 기록합니다. 미전송 시 미동의는 revoked로 기록됩니다(기존 동작). 배너의 "동의하지 않고 계속하기" 버튼이 전송합니다 |
요청 예시
POST /api/plugins/sirsoft-gdpr/consent/cookie HTTP/1.1
Host: api.example.com
Accept: application/json
Content-Type: application/json
Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생략 가능)
{
"consents": {
"cookie_necessary": true,
"cookie_analytics": false,
"cookie_marketing": false
},
"source": "banner",
"intent": "reject"
}
응답 필드 (data 내부)
단건 응답: data 객체의 필드.
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
|---|---|---|---|
| user_id | integer|null | 6 |
동의를 저장한 회원의 식별자. 게스트 호출이면 null 입니다 |
| session_id | string|null | 9f1c1c1e-4b2a-4d0e-9d3f-1a2b3c4d5e6f |
게스트 호출 시 사용된 세션 식별자(gdpr_session 쿠키 값 또는 신규 발급 UUID). 회원 호출이면 null 입니다 |
| consents | object | {"cookie_necessary":true,"cookie_analytics":false} |
이번 요청으로 저장된 카테고리별 동의 값(요청 본문의 consents 를 그대로 반향). 키는 cookie_ 접두사가 붙은 consent_key, 값은 boolean 입니다 |
응답 예시
{
"success": true,
"message": "동의가 저장되었습니다.",
"data": {
"user_id": null,
"session_id": "9f1c1c1e-4b2a-4d0e-9d3f-1a2b3c4d5e6f",
"consents": {
"cookie_necessary": true,
"cookie_functional": true,
"cookie_analytics": false,
"cookie_marketing": false
}
}
}
에러 응답
| 상태코드 | 의미 | 발생 조건 |
|---|---|---|
| 422 | Validation Error | consents 누락/비배열/빈 배열, 값이 boolean 이 아님, source 가 banner|preference_center|register|mypage 외의 값, 카탈로그에 없는 consent_key 전달(유효하지 않은 동의 항목입니다.), 필수 카테고리를 false 로 전달(필수 항목은 철회할 수 없습니다.) |
설명 공개 쿠키 동의 배너에서 방문자가 선택한 카테고리별 동의를 저장합니다. optional.sanctum 라우트로 게스트와 회원 모두 호출할 수 있으며, 회원(sanctum 토큰 보유)이면 user_id 기준으로 status를 upsert하고 history를 남기고, 게스트면 session_id 기준으로 history를 기록합니다. 게스트가 처음 호출해 세션 식별자가 없으면 UUID에 HMAC-SHA256 서명을 붙인({uuid}|{서명}) gdpr_session 쿠키(1년, SameSite=Lax)를 응답에 발급해 첨부합니다 — 서명은 위조 방지 목적이며, 요청으로 들어온 값의 서명이 유효하지 않으면 신원 불명(새 게스트)으로 처리합니다. 동의 철회 시 실제 쿠키 파기는 이 엔드포인트가 아니라 클라이언트 정리기와 후속 응답의 CookieConsentMiddleware가 담당합니다.
GET /api/plugins/sirsoft-gdpr/consent/cookie/status
- 라우트명:
api.plugins.sirsoft-gdpr.consent.cookie.status - 컨트롤러:
Plugins\Sirsoft\Gdpr\Http\Controllers\Public\GdprCookieConsentController@status - 인증/권한:
optional.sanctum(선택적 인증: 회원/비회원 모두 접근)
요청 파라미터
요청 파라미터 없음.
요청 예시
GET /api/plugins/sirsoft-gdpr/consent/cookie/status HTTP/1.1
Host: api.example.com
Accept: application/json
Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생략 가능)
응답 필드 (data 내부)
단건 응답: data 객체의 필드.
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
|---|---|---|---|
| has_consented | boolean | false |
consented 여부 |
| consents | array | [] |
현재 방문자의 활성 쿠키 동의 목록(카테고리별 현재 선택 상태). 배너가 기존 동의를 반영해 토글 초기값을 그릴 때 사용합니다 |
| needs_renewal | boolean | false |
과거 동의 이력은 있으나 현재 정책 버전으로는 미동의인 상태(정책 갱신 후 재확인 필요). 동의 이력이 전혀 없는 신규 게스트는 false입니다 |
| current_policy_version | string | 10 |
현재 발행된 최신 정책 버전 문자열(정책 버전 서비스 기준). 방문자 동의 버전과 비교해 배너 재노출 여부를 판단합니다 |
| is_member | boolean | true |
member 여부 |
응답 예시
HTTP/1.1 200
{
"success": true,
"message": "성공적으로 처리되었습니다.",
"data": {
"has_consented": false,
"consents": [],
"needs_renewal": false,
"current_policy_version": "1",
"is_member": true
}
}
에러 응답
에러 응답 없음 — 공개 조회 엔드포인트입니다. 요청 파라미터가 없어 검증 실패(422)가 발생하지 않고, 컨트롤러/Service 가 도메인 예외를 던지지 않습니다. 미인증 호출도 게스트로 정상 처리되어 401 도 발생하지 않습니다.
설명 현재 방문자의 쿠키 동의 상태를 반환해 배너 재노출 여부를 판단하게 합니다. optional.sanctum 라우트로 회원이면 user_id 기준, 미인증이면 session_id 기준으로 조회합니다. has_consented는 현재 정책 버전으로 동의를 완료했는지, needs_renewal은 과거 동의는 있으나 현재 정책 버전으로는 미동의인 상태(정책 갱신 후 재확인 필요)를 나타내며 동의 이력이 전혀 없는 신규 게스트는 false입니다. is_member는 배너가 회원 전용 분기(기존 동의 유지 버튼 등)를 노출할지 결정하는 데 사용합니다.
POST /api/plugins/sirsoft-gdpr/consent/grant
- 라우트명:
api.plugins.sirsoft-gdpr.consent.grant - 컨트롤러:
Plugins\Sirsoft\Gdpr\Http\Controllers\User\GdprConsentController@grant - 인증/권한:
auth:sanctum
요청 파라미터
요청 파라미터 없음.
요청 예시
POST /api/plugins/sirsoft-gdpr/consent/grant HTTP/1.1
Host: api.example.com
Accept: application/json
Authorization: Bearer {YOUR_TOKEN}
응답 필드 (data 내부)
단건 응답: data 객체의 필드.
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
|---|---|---|---|
| consent_key | string | cookie_analytics |
이번 호출로 동의(true)가 부여된 동의 항목 키. 요청 본문의 consent_key 를 그대로 반향하며, 프론트가 해당 행만 갱신하는 데 사용합니다 |
응답 예시
{
"success": true,
"message": "동의가 갱신되었습니다.",
"data": {
"consent_key": "cookie_analytics"
}
}
에러 응답
| 상태코드 | 의미 | 발생 조건 |
|---|---|---|
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 422 | Validation Error | consent_key 누락/문자열 아님/50자 초과, 또는 쿠키 카테고리 카탈로그에 없는 키(유효하지 않은 동의 항목입니다.) |
설명 마이페이지 「내 동의 현황」에서 회원이 특정 동의 항목을 부여하거나 다시 동의할 때 호출합니다. auth:sanctum 인증이 필요하며, consent_key 하나를 대상으로 동의를 true로 갱신하고 source를 mypage로 기록합니다. GDPR Art.7(3)의 자유 변경권을 구현한 것으로, 철회했던 항목의 재동의와 신규 동의를 동일하게 처리하며 history 행이 남습니다. consent_key의 화이트리스트 검사는 FormRequest에서 수행합니다.
GET /api/plugins/sirsoft-gdpr/consent/history
- 라우트명:
api.plugins.sirsoft-gdpr.consent.history - 컨트롤러:
Plugins\Sirsoft\Gdpr\Http\Controllers\User\GdprConsentController@history - 인증/권한:
auth:sanctum
요청 파라미터
요청 파라미터 없음.
요청 예시
GET /api/plugins/sirsoft-gdpr/consent/history HTTP/1.1
Host: api.example.com
Accept: application/json
Authorization: Bearer {YOUR_TOKEN}
응답 필드 (data 내부)
단건 응답: data 객체의 필드.
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
|---|---|---|---|
| histories | array | [] |
회원 본인의 동의 변경 이력 배열. 각 항목은 consent_key, action(granted/revoked/rejected), source, policy_version, categories, created_at로 구성되며 부여/철회/거부 기록을 시간순으로 제공합니다. rejected는 배너에서 명시적으로 거부(동의하지 않고 계속하기)한 항목입니다 |
응답 예시
HTTP/1.1 200
{
"success": true,
"message": "성공적으로 처리되었습니다.",
"data": {
"histories": []
}
}
에러 응답
| 상태코드 | 의미 | 발생 조건 |
|---|---|---|
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
설명 회원 본인의 동의 이력을 반환해 마이페이지 감사 목적으로 표시합니다. auth:sanctum 인증이 필요하며, histories 배열로 동의 부여/철회 기록을 시간순으로 제공합니다. 조회 전용이므로 부수 효과는 없습니다.
GET /api/plugins/sirsoft-gdpr/consent/me
- 라우트명:
api.plugins.sirsoft-gdpr.consent.me - 컨트롤러:
Plugins\Sirsoft\Gdpr\Http\Controllers\User\GdprConsentController@me - 인증/권한:
auth:sanctum
요청 파라미터
요청 파라미터 없음.
요청 예시
GET /api/plugins/sirsoft-gdpr/consent/me HTTP/1.1
Host: api.example.com
Accept: application/json
Authorization: Bearer {YOUR_TOKEN}
응답 필드 (data 내부)
단건 응답: 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는 마이페이지에서 「거부」 배지로 표시됩니다. 각 항목은 마이페이지 표시용 상태 파생 필드도 함께 내려줍니다 — status(required/consented/revoked/rejected/none), 상태 배지 문구(status_badge_label, 다국어 — 필수=항상 적용 / 동의함 / 거부함 / 미설정), 동의한 항목의 표시 동사·날짜(status_label·status_at_formatted — 동의 상태(required·consented)만 '동의' + 동의일을 내려주고, 거부·철회·미설정은 둘 다 null) |
응답 예시
HTTP/1.1 200
{
"success": true,
"message": "성공적으로 처리되었습니다.",
"data": {
"user_id": 6,
"needs_renewal": false,
"current_policy_version": "1",
"consents": [
{
"id": null,
"consent_key": "cookie_necessary",
"consent_label": "필수 쿠키",
"consent_description": "세션·CSRF·로그인 토큰, 장바구니 식별자, 사용자가 직접 고른 언어 설정과 화면 테마, 쿠키 동의 기록 등 사이트 운영에 반드시 필요한 항목입니다. 비활성화할 수 없습니다.",
"consent_category": "necessary",
"is_required": true,
"is_consented": false,
"can_revoke": false,
"can_grant": false,
"needs_renewal_this_item": false,
"consented_at": null,
"revoked_at": null,
"consent_count": 0,
"policy_version": null,
"last_source": null
},
{
"id": null,
"consent_key": "cookie_functional",
"consent_label": "기능 쿠키",
"consent_description": "사용자 선호도(표시 통화 등)를 기억하는 쿠키입니다. 거부 시 매 방문마다 기본값으로 표시됩니다.",
"consent_category": "functional",
"is_required": false,
"is_consented": false,
"can_revoke": false,
"can_grant": true,
"needs_renewal_this_item": false,
"consented_at": null,
"revoked_at": null,
"consent_count": 0,
"policy_version": null,
"last_source": null
},
{
"id": null,
"consent_key": "cookie_analytics",
"consent_label": "분석 쿠키",
"consent_description": "방문자가 사이트를 어떻게 이용하는지 익명으로 측정해 더 나은 서비스를 만드는 데 사용됩니다. (예: Google Analytics, Hotjar)",
"consent_category": "analytics",
"is_required": false,
"is_consented": false,
"can_revoke": false,
"can_grant": true,
"needs_renewal_this_item": false,
"consented_at": null,
"revoked_at": null,
"consent_count": 0,
"policy_version": null,
"last_source": null
},
{
"id": null,
"consent_key": "cookie_marketing",
"consent_label": "마케팅 쿠키",
"consent_description": "관심사에 맞는 광고를 보여주거나, 광고가 얼마나 효과적이었는지 측정하는 데 사용됩니다. SNS 영상 임베드 등도 포함됩니다. (예: Facebook 픽셀, Google 광고, YouTube 영상)",
"consent_category": "marketing",
"is_required": false,
"is_consented": false,
"can_revoke": false,
"can_grant": true,
"needs_renewal_this_item": false,
"consented_at": null,
"revoked_at": null,
"consent_count": 0,
"policy_version": null,
"last_source": null
}
]
}
}
에러 응답
| 상태코드 | 의미 | 발생 조건 |
|---|---|---|
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
설명 마이페이지 「내 동의 현황」 화면의 데이터 소스로, 회원 본인의 전체 동의 매트릭스를 반환합니다. auth:sanctum 인증이 필요하며, 활성 동의만이 아니라 카탈로그의 모든 카테고리와 회원 상태를 합쳐 노출해 철회·재동의·신규 동의를 한 화면에서 처리하도록 합니다(Art.7(3) 대칭성). 함께 반환되는 needs_renewal과 current_policy_version으로 정책 버전 갱신 이후 재동의가 필요한지 판단합니다. 조회 전용입니다.
POST /api/plugins/sirsoft-gdpr/consent/renew-all
- 라우트명:
api.plugins.sirsoft-gdpr.consent.renew_all - 컨트롤러:
Plugins\Sirsoft\Gdpr\Http\Controllers\User\GdprConsentController@renewAll - 인증/권한:
auth:sanctum
요청 파라미터
요청 파라미터 없음.
요청 예시
POST /api/plugins/sirsoft-gdpr/consent/renew-all HTTP/1.1
Host: api.example.com
Accept: application/json
Authorization: Bearer {YOUR_TOKEN}
응답 필드 (data 내부)
단건 응답: data 객체의 필드.
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
|---|---|---|---|
| renewed | integer | 0 |
이번 호출로 현재 정책 버전으로 재동의 처리된 활성 선택형 동의 항목 수. 필수 쿠키와 이미 철회한 항목·이미 현재 버전인 항목은 제외되며, 프론트 toast 메시지의 {renewed} 보간값으로 사용됩니다 |
응답 예시
HTTP/1.1 200
{
"success": true,
"message": "활성 항목 {renewed} 개를 새 정책 버전으로 갱신했습니다.",
"data": {
"renewed": 0
}
}
에러 응답
| 상태코드 | 의미 | 발생 조건 |
|---|---|---|
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
설명 정책 버전이 갱신된 뒤 회원의 활성 선택형 동의를 현재 정책 버전으로 일괄 재동의 처리합니다. auth:sanctum 인증이 필요하며, 필수 쿠키와 이미 철회한 항목은 대상에서 제외됩니다. 의사 변경 없이 재동의 의사 표명으로 처리되어 갱신된 각 항목마다 action=granted history 행이 누적됩니다(Art.7(1) 입증 트레일). 응답에는 갱신 건수가 포함되어 프론트 toast 메시지에 사용됩니다.
POST /api/plugins/sirsoft-gdpr/consent/revoke
- 라우트명:
api.plugins.sirsoft-gdpr.consent.revoke - 컨트롤러:
Plugins\Sirsoft\Gdpr\Http\Controllers\User\GdprConsentController@revoke - 인증/권한:
auth:sanctum
요청 파라미터
요청 파라미터 없음.
요청 예시
POST /api/plugins/sirsoft-gdpr/consent/revoke HTTP/1.1
Host: api.example.com
Accept: application/json
Authorization: Bearer {YOUR_TOKEN}
응답 필드 (data 내부)
단건 응답: data 객체의 필드.
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
|---|---|---|---|
| consent_key | string | cookie_marketing |
이번 호출로 철회(false)된 동의 항목 키. 요청 본문의 consent_key 를 그대로 반향하며, 프론트가 해당 행만 갱신하는 데 사용합니다 |
응답 예시
{
"success": true,
"message": "동의가 철회되었습니다.",
"data": {
"consent_key": "cookie_marketing"
}
}
에러 응답
| 상태코드 | 의미 | 발생 조건 |
|---|---|---|
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 422 | Validation Error | consent_key 누락/문자열 아님/50자 초과, 카탈로그에 없는 키(유효하지 않은 동의 항목입니다.), 또는 필수(strictly necessary) 카테고리 철회 시도(필수 항목은 철회할 수 없습니다.) |
설명 마이페이지 「내 동의 현황」에서 회원이 특정 동의 항목을 철회할 때 호출합니다. auth:sanctum 인증이 필요하며, consent_key 하나를 대상으로 동의를 false로 갱신하고 source를 mypage로 기록하며 history 행을 남깁니다. consent_key의 화이트리스트 검사는 FormRequest에서 수행합니다.