번들 확장 20개 전부에 AGENTS.md · README.md · docs/ 를 채우고, 코어가 들고 있던 확장 소유 문서 두 갈래를 그 확장으로 옮긴다. 번들 템플릿의 컴포넌트· 핸들러·레이아웃 상세와 확장이 구독하는 활동 로그 훅 목록이 그 대상이며, 코어에는 총계와 링크만 남아 확장이 기능을 늘릴 때 코어 문서를 고쳐야 하던 역방향 의존이 사라진다. 전수 완비를 확인하고 강제를 조인다 — 문서 동반 룰을 대상 목록 없는 error 로 승격하고, 검사 스크립트가 문서 미보유를 실패로 올리며, 미채움 마커 baseline 을 0 으로 기록한다. 한쪽만 조이면 "새 확장이 문서 없이 들어와도 초록" 인 상태가 남는데 그 결과는 이상 0건과 구분되지 않는다. 집필 과정에서 드러난 생성기 결함 셋을 함께 고친다. 스케줄 주기 열이 계약 키를 읽지 않아 모든 확장에서 '-' 였고, 네임스페이스를 붙인 핸들러 등록 키가 수집에서 통째로 빠졌으며, README 골격이 폐기된 히어로 배지를 계속 찍어내고 있었다. 셋 다 산출물이 아니라 원천이 틀린 것이라, 가드의 모집단에 생성기 출력 자체를 넣어 다음 확장이 같은 상태로 태어나는 경로를 막는다.
6.0 KiB
마케팅 동의 — 데이터 모델
모델·소유 테이블·마이그레이션·Enum · 진입점: AGENTS.md
모델
| 모델 | 테이블 | fillable | 관계 | 특성 |
|---|---|---|---|---|
MarketingConsent |
user_marketing_consents |
7 | user→User | - |
MarketingConsentHistory |
user_marketing_consent_histories |
5 | user→User | - |
두 모델의 역할이 상태와 이력으로 갈립니다.
MarketingConsent— "지금 어떤가". 회원 × 동의 항목(consent_key) 하나가 한 행이며, 현재 동의 여부(is_consented) · 동의/철회 시각 · 누적 동의 횟수(consent_count) · 마지막 변경 출처(last_source)를 갖습니다. EAV 구조이므로 항목이 늘어도 스키마는 그대로입니다.MarketingConsentHistory— "어떻게 여기까지 왔는가". 변경 한 건이 한 행이며 행위 (action) · 출처(source) · IP(ip_address)를 남깁니다.
둘을 나눈 이유는 조회 성질이 다르기 때문입니다. 현재 상태는 화면을 그릴 때마다 읽히므로 회원당 항목 수만큼만 있어야 하고, 이력은 계속 쌓이지만 평소에는 읽히지 않습니다. 한 테이블에 두면 "현재 상태" 조회가 이력 전체를 훑게 됩니다.
consent_count 는 이력에서 세도 되는 값이지만 상태에 함께 둡니다 — 이 값을 보려고 이력
테이블을 조회하게 하면 화면 조회가 이력 크기에 묶입니다. 대신 상태와 이력을 같은 트랜잭션에서
갱신해야 둘이 어긋나지 않습니다.
소유 테이블
| 테이블 | 모델 |
|---|---|
user_marketing_consent_histories |
MarketingConsentHistory |
user_marketing_consents |
MarketingConsent |
두 테이블 모두 user_ 로 시작합니다 — 이 플러그인의 데이터가 회원에 종속된다는 뜻이며,
회원이 사라지면 함께 사라져야 합니다.
그 정리는 DB CASCADE 가 아니라 core.user.before_delete 훅이 합니다. 코어 회원 삭제는
이 플러그인의 테이블을 알지 못하므로, 이 구독이 빠지면 고아 행이 조용히 쌓입니다. 반대로
CASCADE 로 처리하면 훅 발행과 이력 처리가 통째로 건너뛰어집니다.
이력 테이블에는 인덱스 추가 마이그레이션이 따로 있습니다(2026_04_01_000003). 이력은 계속
쌓이는 테이블이라 회원별·채널별 조회가 인덱스를 타야 합니다.
마이그레이션
마이그레이션 3개.
| 파일 | 생성 테이블 | 변경 테이블 | down() |
|---|---|---|---|
2026_04_01_000001_create_user_marketing_consents_table.php |
user_marketing_consents |
user_marketing_consents |
✅ |
2026_04_01_000002_create_user_marketing_consent_histories_table.php |
user_marketing_consent_histories |
user_marketing_consent_histories |
✅ |
2026_04_01_000003_add_indexes_to_user_marketing_consent_histories_table.php |
- | user_marketing_consent_histories |
✅ |
3개입니다 — 상태 테이블 · 이력 테이블 · 이력 인덱스.
항목이 늘어도 마이그레이션이 필요 없는 것이 이 설계의 목표입니다. 채널을 추가하려는데 마이그레이션을 쓰고 있다면 EAV 구조를 벗어나고 있다는 신호이므로, 그 변경을 설정으로 표현할 수 없는지 먼저 검토합니다.
새 컬럼을 더할 때 초기 create_* 파일을 고치지 않습니다 — 이미 설치된 사이트는 그 파일을
다시 실행하지 않으므로 반영되지 않으며, 기존 행을 손봐야 하는 변경은 upgrades/ 의 업그레이드
스텝 백필이 함께 필요합니다. 한국어 comment 와 down() 은 필수입니다.
Enum
Enum 이 없습니다.
없습니다. 동의는 참/거짓 하나이고 항목 목록은 설정 데이터라 코드의 닫힌 어휘가 아닙니다 — Enum 으로 만들면 채널 추가가 다시 배포 작업이 됩니다.
닫힌 어휘가 하나 있긴 합니다: 이력의 source(admin / profile)와 action. 이 값들은
detectSource() 와 서비스가 문자열로 다루는데, 새 변경 경로가 늘어 분기가 생기기 시작하면
그때 Enum 으로 올리는 것이 맞습니다. 지금은 판정 지점이 한 곳뿐이라 어휘가 갈라질 여지가
없습니다.
Repository
| 클래스 | 종류 | 설명 |
|---|---|---|
MarketingConsentRepository |
구현 | 마케팅 동의 Repository 구현체 |
MarketingConsentRepositoryInterface |
인터페이스 | 마케팅 동의 Repository 인터페이스 |
MarketingConsentRepository 하나이며 인터페이스를 통해 주입됩니다(구체 클래스 타입힌트 금지).
상태와 이력을 한 Repository 가 함께 다룹니다. 둘이 같은 트랜잭션에서 갱신되어야 하는데 Repository 를 나누면 그 원자성을 호출부가 조립하게 되고, 조립을 빠뜨린 경로에서 상태만 바뀌고 이력이 없는 행이 생깁니다.
회원 삭제 정리(deleteByUserId)도 여기 있습니다. 이 메서드는 core.user.before_delete 에서만
호출되며, 두 테이블을 함께 지웁니다.