번들 확장 README 제목이 manifest 확장명 그대로(「게시판」)라 그 문서만 연
사람이 그누보드7의 확장인지, 모듈인지 템플릿인지 알 수 없었다.
README · AGENTS.md · docs/README.md 진입 문서 60개의 제목을
「그누보드7 {확장명} {유형}」 으로 바꾸고, 조립 규칙을 ExtensionInventory::docTitle
한 곳에 두어 골격 생성기와 계약 테스트가 같은 헬퍼를 쓰게 했다.
확장명이 이미 유형으로 끝나면 겹쳐 붙이지 않는다.
공개 규정에는 번들 확장 전용 관례임을 주어로 명시하고 제3자 확장에는
요구하지 않는다는 문장을 두었다. 언어팩은 README 가 없어 대상 밖이다.
20 KiB
확장 개발자 문서 (Extension Documentation)
번들 확장이 갖추는
AGENTS.md·README.md·docs/**의 역할 경계, 골격, 자동 생성 규약, 갱신 의무.
TL;DR (5초 요약)
1. 확장마다 AGENTS.md(개발자·에이전트용) + README.md(사람용) + docs/(상세) 를 갖는다
2. 같은 사실은 한쪽만 SSoT — 소개·기능·요구사항은 README, 설계 의도·확장점·동반 의무는 AGENTS
3. 코드에서 실측되는 표는 `php artisan ext:docgen` 이 @generated 블록 안쪽만 교체한다
4. 블록 밖 전부가 사람 영역 — 생성기는 그 자리를 절대 건드리지 않으며 파괴적 재생성 플래그가 없다
5. 사람만 쓸 수 있는 다섯 자리는 TODO 마커로 남는다 (의도 · 흐름 · 금지패턴 · 사용방법 · 트러블슈팅)
목차
1. 왜 두 문서인가
docs/api/** 는 "엔드포인트가 무엇을 받고 무엇을 돌려주는가" 만 답한다. 확장을 수정하려는 사람이 실제로 필요로 하는 것은 그 앞의 질문이다 — 왜 이렇게 설계됐는가 / 어디를 확장해야 하는가 / 무엇을 건드리면 안 되는가.
그 답이 코드 안에만 있으면 세 가지가 생긴다.
- 확장을 수정할 때마다
src/전체를 훑어 구조를 재발견한다. - 확장이 발행하는 훅이 코드 안에만 있어 확장점이 사실상 비공개가 된다.
- 확장 저장소만 받은 제3자에게는 참고할 문서가
CHANGELOG.md뿐이다.
독자가 둘이므로 문서도 둘이다. 도입을 검토하고 운영하는 사람은 "무엇을 해결해 주는가" 를 묻고, 확장을 고치는 사람은 "어디를 어떻게 건드리는가" 를 묻는다. 한 문서에 섞으면 양쪽 모두에게 길어진다.
2. 파일 배치
{ext}/
├─ AGENTS.md 에이전트·확장개발자 진입점
├─ README.md 사람(도입검토자·운영자) 진입점 — 한국어
├─ CHANGELOG.md 변경 이력
└─ docs/
├─ README.md 문서 통합 목차 + 실측 집계
├─ architecture.md 설계 의도 · 계층 지도 · 디렉토리 맵
├─ extension-points.md 발행/구독 훅 · 미들웨어 · 채널 · 스케줄 [모듈·플러그인]
├─ data-model.md 모델 · 소유 테이블 · 마이그레이션 · Enum [모듈·플러그인]
├─ settings.md 설정 스키마 · 권한 · 메뉴 · 라우트 · 의존 [모듈·플러그인]
├─ frontend.md 레이아웃 · 핸들러 · 전역 진입점 · 에셋 [모듈·플러그인]
├─ components.md 제공 컴포넌트 [템플릿]
├─ layouts.md 레이아웃 목록 · 라우트 매핑 [템플릿]
├─ handlers.md 템플릿 전용 핸들러 · 부트스트랩 [템플릿]
├─ editor-spec.md 레이아웃 편집기 선언 · 프리뷰 샘플 커버리지
└─ api/ API 레퍼런스 (별도 체계)
템플릿은 API·모델·훅 축이 사실상 비므로 골격이 다르다. ext:docgen 이 manifest 유형을 보고 골격을 고르므로 유형을 직접 지정할 필요가 없다.
docs/editor-spec.md 만은 세 유형 공통이다. 편집기 스펙(editor-spec.json)을 두지 않는 확장에도 문서를 두는 것은, 미보유가 정상일 수 있고 그 정상 여부를 적을 자리가 필요하기 때문이다 — "이 확장은 왜 스펙이 없어도 되는가 / 언제 필요해지는가" 가 어디에도 없으면 다음 사람이 그 부재를 누락으로 오해하거나 반대로 필요한 시점을 놓친다.
이 문서들은 {ext}/ 안에 있으므로 릴리즈 페이로드에 실리는 공개 배포물이다. 확장 저장소를 받은 사람이 그대로 읽는다.
3. 역할 경계와 SSoT
같은 사실은 한쪽만 SSoT 로 두고 반대쪽은 링크한다. 다만 두 문서 모두 자기 독자에게는 자족적이어야 한다 — "저쪽을 보라" 만 남기면 어느 쪽도 읽히지 않는다.
| README.md (사람) | AGENTS.md (에이전트·확장개발자) |
|---|---|
| 확장 소개 · 해결하는 문제 (SSoT) | 개발 의도 · 설계 원칙 (SSoT) |
| 핵심 기능 목록 (SSoT) | 아키텍처 · 계층 지도 · 디렉토리 맵 |
| 동작 방식 다이어그램 (운영자 눈높이) | 핵심 흐름 (코드 경로 · 계층 통과 순서) |
| 요구사항 · 의존성 · 연동 확장 (SSoT) | 도메인 모델 · 소유 테이블 요약 |
| 설치 · 활성화 | 확장점: 발행 훅 · 구독 훅 · 필터 (SSoT) |
| 관리자 설정 화면 사용법 (템플릿은 이 자리에 제공 컴포넌트 요약) | 라우트 · 권한 · 설정 스키마 요약 |
| 운영 트러블슈팅 | 프론트 진입점 · 레이아웃 · 핸들러 |
| 라이선스 | 수정 시 동반 의무 체크리스트 |
필수 섹션 (유형별)
ext:docgen --check 가 요구하는 헤딩이다. 낱말이 본문 어딘가에 있는 것으로는 충족되지 않고 헤딩이어야 한다.
| 문서 | 필수 섹션 |
|---|---|
AGENTS.md |
TL;DR (5초 요약) · 1. 이 확장은 무엇인가 · 2. 디렉토리 지도 · 3. 핵심 흐름 · 4. 확장점 · 5. 수정 시 동반 의무 · 6. 금지 패턴 · 7. 테스트 실행 · 8. 문서 목차 |
README.md |
소개 · 주요 기능 · 동작 방식 · 요구 사항 · 설치 · 관리자 설정(템플릿은 제공 컴포넌트) · 사용 방법 · 다른 확장과의 연동 · 문서 · 트러블슈팅 · 변경 이력 · 라이선스 |
docs/README.md |
문서 목차 |
docs/architecture.md |
설계 의도 · 계층 지도 · 디렉토리 |
docs/extension-points.md (모듈·플러그인) |
발행 훅 · 구독 훅 · 훅 리스너 · 레이아웃 확장 · 미들웨어 · 브로드캐스트 채널 · 스케줄 · 알림 정의 |
docs/data-model.md (모듈·플러그인) |
모델 · 소유 테이블 · 마이그레이션 · Enum · Repository |
docs/settings.md (모듈·플러그인) |
설정 스키마 · 권한 · 메뉴 · 라우트 · 의존 관계 |
docs/frontend.md (모듈·플러그인) |
레이아웃 · 액션 핸들러 · 전역 진입점 · 에셋 |
docs/components.md (템플릿) |
제공 컴포넌트 |
docs/layouts.md (템플릿) |
레이아웃 목록 · 라우트 매핑 |
docs/handlers.md (템플릿) |
템플릿 전용 핸들러 · 부트스트랩 |
docs/editor-spec.md |
선언 요약 · 선언 블록 · 컴포넌트 팔레트 · 샘플 데이터와 페이지 상태 · 수정 시 동반 의무 |
목록의 SSoT 는 생성기이므로, 직접 옮겨 적기보다 ext:docgen --init 이 만든 골격에서 시작하는 편이 어긋나지 않는다.
AGENTS.md 의 5. 수정 시 동반 의무
이 절이 문서의 실효성 핵심이다. 코어 횡단 규정 중 그 확장에 실제로 걸리는 것만 추린다. 전부 나열하면 체크리스트 전체가 형식적으로 읽히고, 정작 걸리는 항목이 묻힌다.
예를 들어 이커머스는 통화 스냅샷과 주문 통화 전 사슬이, 게시판은 비밀글 게이트의 하위 리소스 재적용이, 결제 플러그인은 청구 금액 계약과 샌드박스 실호출이 그 자리에 온다.
ext:docgen --init 이 확장이 실제로 보유한 표면(마이그레이션 · 발행 훅 · 라우트 · 레이아웃 · 빌드 산출물 · 다국어)만 골라 초안을 만든다. 사람은 거기에 그 확장 고유의 항목을 보탠다.
시각 자료
스크린샷을 두지 않고 mermaid 다이어그램과 표로 대체한다. 이미지 파일이 없으므로 릴리즈 용량이 늘지 않고, UI 가 바뀔 때마다 다시 촬영할 의무도 없다. GitHub 이 mermaid 를 네이티브로 렌더한다.
mermaid 문법 오류는 렌더 시점에만 드러나므로, 새 형식을 도입할 때는 실제 렌더를 눈으로 확인한다.
README 첫 화면
확장명은 히어로 이미지 배지가 아니라 평범한 H1 제목으로 적는다. 확장은 서로 대등하게 병렬로
존재하는 구성요소이고, 각자가 코어와 같은 히어로 브랜딩을 달면 그 확장 하나가 독립 프로젝트인
것처럼 보인다. @generated:badges 블록의 버전·유형·코어 제약·라이선스 배지는 manifest 에서
오는 정보 표시이므로 그대로 둔다.
번들 확장의 제목은 확장명만이 아니라 「그누보드7 {확장명} {유형}」 이다 — 「그누보드7 페이지
모듈」·「그누보드7 Basic 템플릿」·「그누보드7 GDPR 플러그인」. 확장명만 제목으로 두면(# 페이지)
그 문서만 연 사람이 이것이 그누보드7의 확장인지, 모듈인지 템플릿인지 알 수 없다. 확장명이 이미
유형으로 끝나면(「Hello 모듈」) 유형을 겹쳐 붙이지 않는다. 진입 문서 세 곳이 같은 제목을 쓴다 —
README.md 는 그 제목 그대로, AGENTS.md 는 — 에이전트 가이드, docs/README.md 는
개발자 문서 를 뒤에 붙인다. docs/ 안의 하위 문서(아키텍처·데이터 모델 등)는 이미 상위
문서 안에 있으므로 확장명만 쓴다.
제3자 확장에는 이 표기를 요구하지 않는다. 확장의 이름과 브랜딩은 그 저작자의 것이다.
ext:docgen --init 이 같은 형식을 기본 제목으로 깔아 주지만 바꿔도 된다 — 제목만 보고도
그누보드7의 확장이며 어떤 유형인지 알 수 있으면 충분하다.
# 그누보드7 페이지 모듈
**그누보드7 모듈 · sirsoft-page**
정적 페이지(정보/정책/안내) 관리 모듈
제목 조립 규칙의 단일 출처는 골격 생성기가 쓰는 헬퍼이고, 계약 테스트가 번들 확장 전수와 생성기 출력을 같은 헬퍼로 대조한다 — 규칙을 문서·생성기·검사에 각각 적으면 한쪽만 고쳐 어긋난다.
4. 자동 생성 블록 규약
생성기는 마커 안쪽만 쓴다.
<!-- @generated:hooks-published START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
| 훅 이름 | 유형 | 발행 위치 |
| ... |
<!-- @generated:hooks-published END -->
<!-- @intent START -->
이 훅은 ... (사람이 쓰는 영역 — 생성기가 건드리지 않는다)
<!-- @intent END -->
블록 밖 전부가 사람 영역이다. 계약은 세 가지다.
- 블록 안쪽 교체가 유일한 쓰기 동작이다. 그래서
--force같은 파괴적 플래그가 없다. 신규 파일 생성만--init으로 분리되어 있고, 그 경로도 기존 파일을 덮어쓰지 않는다. - 문서에 없는 블록 키는 주입하지 않는다. 생성기가 임의 위치에 표를 끼워 넣으면 사람이 잡아 둔 문서 구조가 흔들린다. 대신 누락으로 보고하여 사람이 마커 놓을 자리를 정한다.
- 재실행은 멱등이다. 같은 코드 상태에서 두 번 돌리면 한 글자도 바뀌지 않는다.
@intent 블록은 사람 영역임을 눈에 띄게 표시하는 장치일 뿐이며, 생성기는 @generated 마커만 찾는다.
블록 목록
| 블록 키 | 내용 | 출처 |
|---|---|---|
badges |
버전 · 유형 · 코어 제약 · 라이선스 · 의존 배지 | manifest |
requirements |
코어 버전 · PHP · 의존 확장 · 외부 호스트 | manifest · composer.json |
install |
설치 · 활성화 · 업데이트 커맨드 | manifest |
integrations · dependencies |
정방향/역방향 의존 확장 | 번들 전수 교차 스캔 |
docs-index · doc-toc |
문서 목차와 작성 상태 | 파일 존재 여부 |
stats |
훅 · 구독 훅 · 라우트 · 모델 · 테이블 · 마이그레이션 · 레이아웃 · 핸들러 8지표 실측 집계 | 전 수집기 |
directory-map |
경로별 역할과 수정 시 절차 | 디렉토리 구조 |
extension-points-summary |
확장점 종류별 개수와 상세 링크 | 훅·선언형 표면 |
hooks-published · hooks-subscribed · listeners |
발행/구독 훅과 리스너 | 소스 스캔 |
layout-extensions · middleware · channels · schedules · notifications |
선언형 확장점 | 진입 클래스 getter |
models · tables · migrations · enums · repositories |
데이터 모델 | 소스 파싱 |
settings-schema · settings-summary · permissions · menus · routes |
설정 스키마 · README 용 설정 요약(키·의미·기본값) · 권한 · 메뉴 · 라우트 | 진입 클래스 getter |
layouts · handlers · frontend-entry · assets |
프론트 표면 | 레이아웃/TS 스캔 |
components · layout-map |
템플릿 컴포넌트·라우트 매핑 | 컴포넌트 스캔 · routes.json |
test-commands |
테스트 종류별 개수와 실행 명령 | 테스트 경로 스캔 |
5. 미채움 마커
생성기가 채울 수 없는 자리 — 코드에서 실측되지 않는 왜 — 에는 다섯 종류의 마커가 남는다.
| 마커 | 자리 | 주 위치 |
|---|---|---|
TODO: 의도 |
설계 의도 · 소개 · 주요 기능 | AGENTS 1, README 소개·주요 기능 |
TODO: 흐름 |
핵심 흐름 · 동작 방식 다이어그램 | AGENTS 3, README 동작 방식 |
TODO: 금지패턴 |
금지 패턴 표 | AGENTS 6 |
TODO: 사용방법 |
운영자 사용 시나리오 | README 사용 방법 |
TODO: 트러블슈팅 |
증상 → 원인 → 조치 | README 트러블슈팅 |
마커 종류를 다섯으로 고정하는 이유는 잔량을 집계할 수 있게 하기 위해서다. 자유 문구로 남기면 어느 자리가 비었는지 셀 수 없다.
의도 와 흐름 은 두 문서 모두에 나타난다 — 같은 축을 다른 독자에게 서술하는 자리이기 때문이다. 나머지 셋은 한쪽에만 있다.
골격을 만든 직후에는 마커가 반드시 존재하며, 그 상태로 커밋하는 것이 정상 흐름이다. 결함이 아니라 집필 진행 상태다. 다만 잔량이 늘어나는 것은 회귀다.
6. ext:docgen 사용법
php artisan ext:docgen
{--scope=all : all | module:{id} | plugin:{id} | template:{id}}
{--init : 문서가 없는 확장에 골격 파일 생성 (기존 파일은 건너뜀)}
{--check : 생성하지 않고 누락·드리프트만 리포트}
{--json : 기계 판독 출력}
{--dry-run : 대상과 실측 집계만 출력}
전형적인 흐름:
# 1. 대상과 실측 규모 확인
php artisan ext:docgen --scope=module:sirsoft-board --dry-run
# 2. 골격 생성 (없는 문서만)
php artisan ext:docgen --scope=module:sirsoft-board --init
# 3. TODO 마커 자리를 코드 근거로 채운다 (사람)
# 4. 표면을 고친 뒤 자동 생성 블록 갱신
php artisan ext:docgen --scope=module:sirsoft-board
# 5. 문서와 코드가 어긋나지 않는지 확인
php artisan ext:docgen --check
작업 위치는 언제나 _bundled 다. 활성 디렉토리 반영은 update 커맨드로만 한다 (문서만 바뀌었다면 빌드는 불필요하다).
php artisan {module|plugin|template}:update {id} --force
수집 대상은 _bundled 소스다
선언형 표면(라우트 · 권한 · 메뉴 · 훅 리스너 · 설정 스키마 등)은 진입 클래스의 getter 를 실제로 호출해 읽는다. 정규식으로 소스를 긁는 방식과 달리 상속 기본값까지 정확히 반영된다.
같은 확장이 활성 디렉토리에서 이미 부팅되어 있으면 PHP 는 같은 클래스를 다시 정의할 수 없으므로, 진입 클래스명만 바꿔 _bundled 파일을 메모리에 다시 읽는다. 그래서 활성 디렉토리에 반영하기 전이라도 _bundled 의 변경이 문서에 그대로 나타난다.
getter 하나가 실패해도 나머지 수집은 계속되며, 실패 사유가 리포트에 드러난다 — 조용한 누락이 없다.
7. 갱신 의무
확장 표면을 바꾸면 같은 작업 단위에서 문서를 함께 갱신한다.
| 바꾼 것 | 해야 할 것 |
|---|---|
| 발행 훅 추가 · 이름 변경 | ext:docgen 재실행 — 다른 확장이 잡는 계약이 바뀐다 |
| 라우트 · 권한 · 메뉴 · 설정 스키마 | ext:docgen 재실행 |
| 모델 · 마이그레이션 | ext:docgen 재실행 + 업그레이드 스텝 |
| 레이아웃 · 핸들러 · 에셋 | ext:docgen 재실행 |
| 설계 방침 · 계층 구조 | 블록 밖 서술을 직접 갱신 (생성기가 채우지 못한다) |
| 새로 발견한 오용 | 6. 금지 패턴 에 행 추가 |
자동 생성 블록이 아무리 정확해도 블록 밖 서술이 낡으면 문서 전체가 의심스러워진다. 생성기 재실행은 갱신의 절반이다.
신규 확장
새 확장을 만들면 php artisan ext:docgen --scope={type}:{id} --init 로 문서 골격을 먼저 만든다. 확장이 문서를 갖고 태어나야 하며, 만들자마자 TODO 마커 자리를 채우는 것이 첫 작업이다.
8. 체크리스트
□ AGENTS.md · README.md · docs/ 필수 문서가 유형에 맞게 있는가?
□ 필수 섹션 헤딩이 모두 있는가?
□ 자동 생성 블록 마커가 모두 있는가?
□ `ext:docgen --check` 가 드리프트 0 인가?
□ TODO 마커 다섯 자리를 코드 근거로 채웠는가? (추측 서술 금지)
□ mermaid 다이어그램이 실제로 렌더되는가? (눈으로 확인)
□ 배지 값이 manifest 와 일치하는가? (생성기가 채우므로 직접 쓰지 않는다)
□ README 와 AGENTS 가 같은 사실을 각자 서술하고 있지는 않은가? (SSoT 한쪽 + 링크)
□ `5. 수정 시 동반 의무` 가 이 확장에 실제로 걸리는 것만 담고 있는가?
□ 버전 상향 시 CHANGELOG 에 기재했는가?
내부 작업 단계·심사 어휘를 남기지 않는다
확장은 각자 자기 저장소로 배포되고, 그것만 내려받은 개발자에게는 그 문서가 유일한 안내다. 거기에 내부 작업 맥락이 남으면 읽는 쪽은 해석할 근거가 없다.
| 남기지 않는다 | 대신 |
|---|---|
작업 단계 (Phase 3, 1차 작업, 다음 단계에서 추가) |
지금 무엇이 있는지만 적는다. 계획은 문서가 아니라 이슈가 담는다 |
심사 판정 (— 정당, 타당함, 검토 결과 통과) |
판정이 아니라 사실을 적는다 |
작업 이력 (이번 라운드에서, 재검 결과) |
결과만 남긴다 |
| 항목 ID 나열로 규모를 설명 | 개수와 목록은 생성기가 실측해 싣는다 |
이 문구들의 공통점은 쓸 때는 정확했다가 나중에 거짓이 된다는 것이다. "다음 단계에서 추가" 라고 적어 둔 항목이 실제로 들어온 뒤에도 문구는 그대로 남고, 오류도 경고도 나지 않는다. 문서가 자기 자신을 반박한 채 계속 읽힐 뿐이다.
코드에서 실측되는 부분은 생성기가 유지하므로 이 문제가 없다. 위험한 자리는 사람이 쓴 자유 텍스트이며, 그중에서도 생성기가 문서로 옮겨 싣는 필드가 가장 위험하다 — 한 곳의 낡음이 여러 문서로 퍼진다. 그래서 확장 문서의 편집기 스펙 한 줄 요약은 스펙 파일의 설명을 옮기지 않고 실측에서 만든다.
관련 문서
- module-basics.md — 모듈 개발 기초
- plugin-development.md — 플러그인 개발 가이드
- template-basics.md — 템플릿 시스템 기초
- hooks.md — 훅 시스템 (발행/구독 규약)
- changelog-rules.md — CHANGELOG 규칙
- ../backend/api-documentation.md — API 레퍼런스 문서 규정