Files
Gnuboard7/docs/backend/api-documentation.md
T
HeuJung 5ca8f636e6 docs(readme): README 영문 기본화 + 한국어판 분리 + 커뮤니티 기여자 하네스
글로벌 방문자에게 한국어 README 는 완전한 이탈 요인인 반면 한국 방문자에게
영문 README 는 한 클릭 불편이라, 비대칭 비용에 맞춰 영문을 기본값으로 둔다.
한국어판은 이력을 보존하도록 git mv 로 옮기고 본문은 그대로 둔다.

커뮤니티 기여자 목록은 공개 CHANGELOG 의 제보자 표기에서 도출한다. 손으로
옮겨 적으면 이슈가 쌓일수록 누락·중복이 생기므로 생성기와 판정기를 한
스크립트로 두고, 세션 종료 시 두 README 와 CHANGELOG 의 불일치를 검사한다.

README 가 두 벌이 되면서 버전 뱃지 축도 둘이 됐다. 한 축이 빠져도 나머지가
통과시켜 초록이 누락을 감추므로, 각 축이 독립적으로 red 를 내는지를
합성 저장소로 고정한다.
2026-08-11 15:50:34 +09:00

30 KiB
Raw Blame History

API 레퍼런스 문서 규정 (API Documentation)

관련 문서: routing.md | api-resources.md | response-helper.md | validation.md


TL;DR (5초 요약)

1. 모든 API 엔드포인트는 레퍼런스 문서 필수 — 메서드/URI/파라미터/응답 필드 + 요청·응답 예시 전수 기재
2. 위치: 코어 = docs/backend/api/, 확장 = {modules|plugins}/_bundled/{id}/docs/api/
3. 생성: php artisan api:docgen — 코드에서 추출한 스캐폴딩 + 사람이 서술 보강 (순수 수기 금지)
4. 추출 불가분(훅 주입 파라미터·동적 응답)은 <!-- TODO --> 마커 남기고 사람이 채움
5. src/routes/api.php 가 없는 web 전용 확장(브라우저 콜백)은 문서 대상 아님
6. Swagger/OpenAPI 도구 미사용 — 마크다운 레퍼런스 전용

목차

  1. 왜 이 규정이 필요한가
  2. 문서 위치 규칙
  3. 표준 문서 포맷
  4. 생성 커맨드 api:docgen
  5. 문서 갱신 의무
  6. 체크리스트

왜 이 규정이 필요한가

G7 의 REST API 는 라우트 ->name() 규약은 있으나 엔드포인트별 공개 레퍼런스가 부재했다. 프론트엔드 (레이아웃 JSON data_sources)와 외부 통합 개발자가 소비하는 요청/응답 계약이 코드에만 존재해, 변경 시 소비처가 침묵 속에서 깨진다(이슈 #64 의 data_source auth_required 계약 변화 사고가 계기).

문서는 코드에서 추출한 스캐폴딩 + 사람이 채운 서술의 하이브리드로 유지한다. 674개 규모에서 완전 수기 문서는 반드시 drift 하고, 완전 자동 추출은 훅 주입 파라미터·동적 응답을 못 잡으므로 둘 다 단독으로는 불충분하다.


문서 위치 규칙

대상 문서 위치 예시
코어 docs/backend/api/{도메인}.md docs/backend/api/users.md
모듈 modules/_bundled/{id}/docs/api/{도메인}.md modules/_bundled/sirsoft-ecommerce/docs/api/products.md
플러그인 plugins/_bundled/{id}/docs/api/{도메인}.md plugins/_bundled/sirsoft-gdpr/docs/api/consents.md

확장 API 문서는 확장이 소유한다(코어에 모으지 않음). 확장을 배포/삭제하면 그 API 문서도 함께 이동한다.

도메인 그룹핑은 URI/라우트명 prefix 기준(api.admin.users.* → users.md)으로 커맨드가 자동 분류한다.

문서 대상이 아닌 확장 (web 전용 라우트)

확장의 api/ prefix 라우트는 src/routes/api.php 를 통해서만 등록된다 ({Plugin,Module}RouteServiceProvider 가 api.php → api/{type}/{id}, web.php → {type}/{id} 로 로드).

따라서 web.php 만 가진 확장은 API 레퍼런스 문서 대상이 아니다. 브라우저 리다이렉트/콜백 (PG 승인 결과 수신 등)은 JSON API 계약이 아니라 사용자 브라우저를 향한 302 응답이며, api:docgen --scope={type}:{id} 는 이 경우 범위 '...' 에 해당하는 API 라우트가 없습니다 로 응답한다.

확장 라우트 docs/api/
sirsoft-pay_kginicis api.php + web.php 있음 — api/ 엔드포인트 22건만 문서화 (web 콜백 9건 제외)
sirsoft-tosspayments web.php 없음 — PG 콜백 2건 전부 브라우저 리다이렉트
gnuboard7-hello_plugin web.php 없음

정적 검사는 src/routes/api.php 존재 여부로 이를 판정해, 해당 확장의 라우트/컨트롤러/FormRequest 변경에는 문서 동반을 요구하지 않는다. 확장에 api.php 를 추가하면 그 시점부터 자동으로 검사 대상이 되므로, 문서 생성을 함께 수행한다.

문서 목차와 발견성 (README.md 규약)

각 대상(코어·확장)의 API 문서 디렉토리에는 README.md 목차가 있어야 한다. api:docgen 이 도메인 파일 목록·엔드포인트 수를 담아 자동 생성한다(@generated 블록, 재생성 멱등, 블록 밖 사람 서술은 보존).

  • 코어: docs/backend/api/README.md
  • 확장: {modules|plugins}/_bundled/{id}/docs/api/README.md

코어 README 는 프로젝트 최상위 README 의 "API 레퍼런스" 진입점이다. 따라서 확장 목차와 달리 세 부분으로 구성된다. 이 문서(작성 규정)와 혼동되지 않도록 최상위 README 는 둘을 "API 레퍼런스" 와 "API 문서 작성 규정" 으로 분리해 링크한다. 최상위 README 는 README.md(영문)·README.ko.md(한국어) 두 벌이며 둘 다 이 진입점을 싣는다 — 한쪽에만 링크가 남으면 그 언어 사용자에게 API 문서가 도달하지 않는다.

구성 위치 소유
공통 규약 개요 (인증·응답 봉투·페이지네이션·에러) 헤더 인용 블록 뒤 ~ 첫 @generated 앞 사람 (재생성 시 원문 보존)
코어 도메인 목차 @generated:start:api-readme-index api:docgen
확장 API 목차 @generated:start:api-readme-extensions api:docgen (코어 README 전용)

개요를 첫 생성 블록 앞에 두는 이유는 목차 표보다 먼저 읽혀야 하기 때문이다. 확장 README 에는 확장 목차 블록을 넣지 않는다. --scope 를 좁혀 실행해도 이번 회차에 갱신하지 않는 블록과 사람 서술은 원문 그대로 보존된다.

확장 API 문서의 발견성은 이 목차를 통해 확보한다. api:docgen 과 코어 인덱스 생성기 (generate-docs-index.cjs) 는 확장명을 하드코딩하지 않고 {modules,plugins}/_bundled/*/docs/api/README.md 를 패턴 스캔해, 코어 README 의 "확장 API 레퍼런스" 표와 AGENTS.md·docs-index 에 자동 편입한다(동적 로딩 원칙 — 코어는 규약과 스캔 패턴만, 확장 이름은 파일 시스템에서 발견). 문서 수·엔드포인트 수는 각 README 의 집계 라인 (**문서 수**: N · **엔드포인트 수**: M)에서 읽으므로, 이 라인 형식을 바꾸면 두 스캐너를 함께 갱신한다.

처음 진입하는 개발자/AI 의 도달 경로:

README.md / README.ko.md "API 레퍼런스"  또는  AGENTS.md "API 레퍼런스 진입점" 표
  → docs/backend/api/README.md (공통 규약 + 코어 목차 + 확장 목차)
  → {도메인}.md  또는  {확장}/docs/api/README.md
  → 엔드포인트별 파라미터·응답·예시

표준 문서 포맷

엔드포인트 1개당 아래 6개 구성(헤더 · 요청 파라미터 · 요청 예시 · 응답 필드 · 응답 예시 · 에러 응답)을 따른다. <!-- @generated:start --> ~ <!-- @generated:end --> 사이는 api:docgen 이 재생성하는 추출 블록이며, 그 바깥의 사람 서술(**설명**)은 재생성 시 보존된다.

블록 안의 사람 작성분도 보존된다. 자동 생성은 비어 있는 자리를 채울 뿐 사람이 쓴 것을 덮어쓰지 않는다:

대상 보존 판정
파라미터 표 용도 셀 행 키(이름+위치)로 대조. 기존 값이 TODO 스텁이 아니면 유지
응답 필드 표 용도/설명 셀 행 키(필드)로 대조. 기존 값이 TODO 스텁이 아니면 유지
응답 필드 표 실측 예시값 열 같은 필드의 타입이 그대로면 기존 값 유지. 타입이 바뀌면(스펙 변경) 새 실측값 채택. 기존에 없던 신규 필드는 새 값 사용
응답 필드 본문 (실측 실패로 마커만 남은 자리) 사람이 채웠으면 유지
응답 예시 본문 실측 산출물에는 <!-- @probed --> 출처 마커가 붙는다. 마커가 없으면 사람 작성분으로 보고 유지
응답 예시 JSON (양쪽 다 실측 산출물) 응답의 키 구조가 같으면 기존 예시 유지. 필드 집합이 달라지면 새 실측 예시 채택
응답 예시·응답 필드 (이번 실측이 실패) 기존이 실측 산출물이면 그대로 유지 (마커로 덮지 않는다)
에러 응답 표 상태코드 키 단위 병합. 사람이 추가한 행(도메인 특이 에러)은 살리고, 자동 추론에만 있는 상태코드는 편입. 같은 상태코드는 기존 행 우선

@probed 마커가 필요한 이유: 실측은 1회 호출이 관측한 표본 하나뿐이라, 사람이 코드를 읽고 쓴 사실 (성공 케이스와 엣지 케이스를 함께 제시하는 등)보다 좁다. 실측 결과는 기본값이지 덮어쓰기 권한이 아니다.

재생성은 멱등해야 한다. 실측값은 호출 시점 DB 상태가 낳은 표본이라, 그대로 반영하면 코드가 하나도 바뀌지 않은 문서까지 재실행마다 diff 가 난다(id 예시값, updated_at, cache_version 등). 그래서 값이 아니라 스펙(필드 집합·타입)이 바뀐 경우에만 갱신한다. 같은 이유로 요청 예시의 path 파라미터는 실측 치환값(/brands/1)이 아니라 라우트 정의의 placeholder(/brands/{brand})로 고정한다 — 실측 성패에 따라 예시가 흔들리지 않아야 한다.

실측 실패(실측 제외: ...)는 "새로 관측하지 못했다"는 뜻이지 기존 관측이 무효라는 뜻이 아니다. 실측 성패는 호출 시점 데이터 유무에 좌우된다(예: DELETE /checkout 은 삭제할 체크아웃이 없으면 404). 따라서 실측 실패가 기존 실측 결과를 지우지 않는다.

nullable 필드는 표본에 값이 있느냐에 따라 관측 타입이 흔들린다(loggable_type 이 string ↔ null). null 관측은 타입 변경이 아니라 "이번 표본에 값이 없었다" 는 뜻이므로, 기존에 관측한 실제 값을 유지한다.

에러 응답 표는 라우트 메타에서 대표 상태코드를 자동 추론한다: 인증 필수(auth:sanctum)→401, admin/permission: 요구→403, FormRequest 검증 규칙 존재→422, path 파라미터 존재→404. optional.sanctum(선택 인증)은 401 을 유발하지 않는다. 도메인 특이 에러(409·429 등)는 사람이 보강한다.

요청 예시(raw HTTP 요청)와 응답 예시(envelope 전문 JSON)는 실제 호출을 재현할 수 있도록 실측 기반으로 방출된다(단계 6). 요청 예시는 curl 이 아니라 raw HTTP 요청(요청 라인 + 헤더)으로 표기해 응답 예시의 HTTP/1.1 {status} 상태줄과 대칭을 이룬다. 세부 규칙:

  • 요청 예시: 요청 라인({METHOD} {path} HTTP/1.1) + Host: + Accept: application/json, 인증 필요 시 Authorization: Bearer {YOUR_TOKEN}(실측 토큰 평문 유출 방지 마스킹). Host 는 실측 기준 URL(로컬 개발 호스트 등)을 노출하지 않고 공개 placeholder(api.example.com, RFC 2606 예약 도메인)로 마스킹한다.
    • 바디 메서드(POST/PUT/PATCH): Content-Type: application/json + 빈 줄 뒤 JSON 바디. 바디는 필수만이 아니라 전체 파라미터를 담고 값은 이름·타입 기반 현실적 예시값으로 채운다("string" placeholder 남발 금지). 허용값 in: 열거가 있으면 그 첫 값을 채택한다.
    • 파일 업로드(요청 파라미터 타입이 image/file): application/json 이 아니라 Content-Type: multipart/form-data; boundary=... 로 표기하고 각 파일 파트를 filename=+Content-Type 으로 나타낸다(JSON 으로는 파일 전송 불가).
    • GET/DELETE: query 파라미터는 URL 쿼리스트링(?a=..&b=..)으로 반영한다(바디 아님).
    • optional.sanctum(선택 인증) 엔드포인트는 Authorization 헤더에 "비회원은 생략 가능" 주석을 붙인다.
  • 응답 예시: HTTP/1.1 {status} 상태줄 + 실측 응답 body 전문({success, data, message, error} envelope). 목록 응답의 data.data[] 는 대표 2항목으로 절단하고 나머지는 ... (총 N건 중 2건 표시) 항목으로 대체한다. 응답에 섞여 나온 민감값(토큰·비밀번호·시크릿·API 키)은 {MASKED} 로 마스킹해 방출한다.
  • 쓰기 메서드 실측: GET/HEAD 는 외부 HTTP 로 read-only 실측하고, 쓰기(POST/PUT/PATCH/DELETE)는 DB 트랜잭션 안에서 in-process dispatch 후 롤백하여 응답 shape 만 관측한다(부수효과 미영속). 단, 부수효과가 롤백으로 되돌릴 수 없는 쓰기(확장 install/activate/update, 언어팩 설치, 코어 업데이트, 파일 업로드, 캐시/워밍업/생성 등 파일시스템·프로세스·외부 네트워크 접촉)는 실측에서 제외(side-effectful-write)하고 요청 예시만 정적 방출한다.
  • 실측 제외(부수효과 쓰기·바이너리·미치환 path 파라미터 등) 엔드포인트: 요청 예시는 파라미터 표 기반으로 정적 조립(raw HTTP 요청 골격), 응답 예시는 <!-- 실측 제외: {사유} — 응답 예시는 사람이 작성하세요. --> 마커로 남긴다.
  • 두 예시 블록은 전량 @generated 블록 내부에 있다. 이미 파라미터/응답 필드 표의 사람 서술이 채워진 문서에 예시만 추가할 때는 api:docgen --examples-only 로 표·서술을 건드리지 않고 예시 2블록만 in-place 삽입/치환한다(측정은 재수행하되 표는 불가침, 신규 엔드포인트 섹션은 만들지 못한다). 백필 커맨드(backfill-params/backfill-fields)도 예시 블록을 건드리지 않는다.

재생성이 지우지 못하는 것

전체 재생성(api:docgen)은 표를 통째로 다시 조립하지만, 이미 문서에 있던 사실을 지우지 않는다. "이번 실행이 알아내지 못했다" 는 "그 사실이 없다" 와 다르기 때문이다. 병합 단계가 다음을 보존한다.

대상 규칙
표의 설명 셀 기존 설명이 있으면 유지. 도구가 만드는 문구는 필드명에서 유추한 일반 문장이라 사람이 적은 도메인 사실보다 정보량이 적다. 값이 바뀌는 컬럼(타입·예시·허용값)은 그대로 재생성된다
엔드포인트 서술(**설명**) 생성 블록 뒤의 사람 서술을 원문 그대로 되살린다. 생성 마커가 없던 수기 섹션은 헤딩(### METHOD /uri)으로 찾아 보존한다
응답 필드 표 이번 실측이 빈 응답이라 표가 사라지는 경우 기존 표를 유지한다
에러 응답 구간 새 문구가 TODO 스텁일 때만 기존 서술을 이어받는다

설명을 갱신하려면 그 셀을 직접 고치거나 비운다 — 비운 셀은 다음 재생성이 채운다.

응답 예시 크기 상한

응답 예시는 pretty JSON 기준 8KB 를 넘지 않는다. 목록 형태(data.data[])는 대표 2건으로 자르고, 목록이 아닌 큰 응답(편집기 스펙·컴포넌트 카탈로그·라우트 목록 등)은 절단 강도를 단계적으로 올려 상한 이하로 줄인다. 잘린 자리에는 ... (총 N건 중 K건 표시) / (N개 키 생략, 총 M개) 표기를 남겨 무엇이 빠졌는지 문서에 드러낸다. 상한이 없으면 문서 한 파일이 수 MB 가 되어 리뷰·git·에디터가 모두 망가진다.

  • --examples-only 모드: 기존 문서의 각 엔드포인트 @generated 블록에 요청 예시(응답 필드 표 앞)와 응답 예시(에러 응답 표 앞)만 삽입한다. 이미 예시가 있으면 그 블록만 새 실측으로 치환(멱등, 실측 상대시간 필드는 재측정으로 값이 달라질 수 있음). 표·필드 설명·엔드포인트 서술은 전량 보존된다. 최초 스캐폴딩이 없는 신규 문서는 먼저 api:docgen 으로 생성한 뒤 서술을 채우고, 이후 예시는 --examples-only 로 유지한다.
### GET /api/admin/users
<!-- @generated:start:api.admin.users.index -->
- **라우트명**: `api.admin.users.index`
- **컨트롤러**: `App\Http\Controllers\Api\Admin\UserController@index`
- **인증/권한**: `auth:sanctum` + `admin` + `permission:admin,core.users.read`

**요청 파라미터**

| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 |
|------|------|------|------|--------|------|
| keyword | query | string | 아니오 | — | <!-- TODO: 용도 --> |
| status | query | string | 아니오 | `active`, `dormant`, `withdrawn` | <!-- TODO: 용도 --> |

> 이 엔드포인트는 확장이 파라미터를 추가할 수 있음 (`core.user.search_validation_rules`).

**요청 예시**

​```http
GET /api/admin/users HTTP/1.1
Host: example.com
Accept: application/json
Authorization: Bearer {YOUR_TOKEN}
​```

**응답 필드** (`data` 내부)

| 필드 | 타입 | 실측 예시값 | 용도/설명 |
|------|------|-------------|-----------|
| id | integer | `1` | <!-- TODO: 설명 --> |
| uuid | string | `a231747f-...` | <!-- TODO: 설명 --> |

**응답 예시**

​```http
HTTP/1.1 200
​```

​```json
{
    "success": true,
    "data": {
        "data": [
            { "id": 1, "uuid": "a231747f-..." },
            { "id": 2, "uuid": "b0f2..." },
            "... (총 42건 중 2건 표시)"
        ],
        "pagination": { "current_page": 1, "total": 42 }
    },
    "message": null,
    "error": null
}
​```

**에러 응답**

| 상태코드 | 의미 | 발생 조건 |
| --- | --- | --- |
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 요구 권한(`admin,core.users.read`)이 없는 경우 |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
<!-- @generated:end -->

**설명** <!-- 사람이 작성: 이 엔드포인트의 용도, 주의사항, 예시 시나리오 -->

응답 envelope 표준

모든 응답은 ResponseHelper 로 {success, data, message, error} 로 래핑된다(response-helper.md). 문서의 "응답 필드" 표는 이 envelope 의 data 내부 필드를 기재한다.

  • 목록 응답 pagination: BaseApiCollection::paginationMeta() → {current_page, last_page, per_page, total, from, to, has_more_pages}
  • 권한 메타: BaseApiResource::resourceMeta() → is_owner + abilities.can_*

파라미터 위치 판정

위치 판정 근거
path URI 의 {param} 세그먼트
query GET/DELETE 요청의 FormRequest rule
body POST/PUT/PATCH 요청의 FormRequest rule

허용값은 FormRequest rule 의 in:, max:, min:, Rule::in(...), boolean, date 등에서 유추한다.


생성 커맨드 api:docgen

# 코어 스캐폴딩 생성 (docs/backend/api/*.md)
php artisan api:docgen --scope=core

# 특정 확장 스캐폴딩 생성
php artisan api:docgen --scope=module:sirsoft-ecommerce
php artisan api:docgen --scope=plugin:sirsoft-gdpr

# 전체
php artisan api:docgen --scope=all

# 생성 없이 누락/drift 만 리포트 (하네스가 소비)
php artisan api:docgen --check

# 이미 서술이 채워진 문서에 요청/응답 예시 블록만 in-place 삽입 (표·서술 불가침, 재생성 금지)
php artisan api:docgen --scope=module:sirsoft-board --seed --base-url=https://example.dev --examples-only

# 생성될 대상만 미리보기
php artisan api:docgen --scope=core --dry-run

동작 (실측 기반):

  1. route:list --json 으로 API 라우트 전수 수집 (method·uri·name·middleware·action).
  2. name prefix 로 소유 확장 판별 (api.modules.{id}.* / api.plugins.{id}.* / 그 외 코어) → 출력 파일 라우팅.
  3. 컨트롤러 메서드의 FormRequest 타입힌트 → rules() 리플렉션 → 요청 파라미터 표 (타입·필수·허용값).
  4. 실측: 임시 Sanctum 토큰 발급 → 실제 요청 파라미터로 엔드포인트 호출 → 실제 응답 JSON 관측.
    • GET/HEAD: 실호출(read-only). 목록이 비면 최소 시드 데이터 자동 생성 후 재호출.
    • 쓰기(POST/PUT/PATCH/DELETE): DB 트랜잭션 내 실행 후 롤백(응답 shape 만 관측, 영속 안 함).
    • 외부 부수효과(결제 PG·외부 인증 콜백·메일)가 있는 라우트: allowlist 로 실호출 제외 → 정적+예시 대체.
  5. 실제 응답 JSON 의 키·타입·샘플값 → 응답 필드 표 + 응답 예시. @generated 블록만 갱신, 사람 서술 보존.
  6. 실측 후 임시 토큰·시드 데이터 정리.

한계 / 보강:

  • FormRequest 가 HookManager::applyFilters 로 규칙을 주입하는 경우(163개) 정적 리플렉션은 훅 주입분을 못 읽는다 → 커맨드가 훅 필터 존재 시 주석을 남기고 사람이 보강. 단 응답 필드는 실측이므로 훅으로 병합된 응답 필드까지 실제로 포착된다.
  • route:list(=RouteFacade::getRoutes())는 활성 확장만 노출한다. 명시 범위(module:{id}/plugin:{id})로 지정한 확장이 비활성/미설치여서 등록 라우트가 0건이면, 인벤토리가 그 확장의 번들 라우트 파일 ({modules|plugins}/_bundled/{id}/src/routes/api.php)을 프로바이더와 동일한 prefix (api/{modules|plugins}/{id})·name(api.{modules|plugins}.{id}.)·api 미들웨어 규약으로 로드해 정적 폴백 수집한다. 이때 실측(HTTP 호출)은 불가하므로 응답 필드는 <!-- 실측 제외 --> + 정적 추정으로 대체되며, 설치 후 --seed 실측으로 채운다. 폴백은 api/ 로 시작하는 라우트만 대상이므로 web(admin) 라우트는 자동 제외된다.
  • 실측이 불가한 라우트(외부 의존·allowlist 제외)는 <!-- 실측 제외: {사유} --> 마커 + 정적 추정으로 대체.

확장 실측 샘플 시더 (--seed)

--seed 는 상세 GET 실측 시 응답 필드가 null 로 관측되는 것을 줄이기 위해, 도메인 대표 엔티티에 완전한 샘플 레코드를 멱등 시드한다. 코어 도메인은 App\Support\ApiDoc\ApiDocSampleService 가 담당한다.

확장은 자신의 도메인 샘플을 확장이 소유한다. App\Contracts\ApiDoc\ApiDocSampleSeeder 를 구현한 클래스를 규약 위치 {확장 네임스페이스}\Support\ApiDoc\ApiDocSampleService (예: Modules\Sirsoft\Page\Support\ApiDoc\ApiDocSampleService, 파일은 src/Support/ApiDoc/)에 두면, api:docgen --scope=module:{id} --seed 실행 시 커맨드가 자동으로 발견해 코어 시드 뒤에 병합한다.

  • seed() 반환 맵의 키는 라우트 도메인 그룹명(pages 등), 값은 {model, key, value} (모델 FQCN·route key 이름·route key 값)이다.
  • 이 맵은 상세 GET 의 path 파라미터 치환에 쓰인다. 라우트-모델 바인딩이 없는 확장 패턴 (show(int $id))도, 파라미터명이 도메인의 단수 리소스명과 일치하면(pages/{page}) 이 맵으로 실측된다. {slug}·{hash}·{versionId} 처럼 route key 가 다른 문자열/보조 파라미터는 폴백하지 않고 실측 제외된다.
  • 확장에 새 PHP 클래스를 추가했으므로 _bundled 작업 후 {type}:update {id} --force 로 활성 디렉토리에 반영해야 오토로드된다.

샘플은 시스템 동작을 바꾸지 않는다

--seed 로 만든 샘플 레코드는 개발 DB 에 영구 잔존한다. 조회 예시를 만들기 위한 데이터일 뿐이므로, 전역 설정을 읽는 쿼리에 걸리는 조합으로 생성해서는 안 된다.

❌ 금지 ✅ 올바른 사용
LanguagePack::create(['scope' => 'core', 'status' => 'active', ...]) ['scope' => 'module', 'target_identifier' => '...', 'status' => 'installed']
실재하지 않는 로케일/통화/국가 코드를 샘플 값으로 사용 실재하는 번들 값만 사용 (ja 등)

scope=core + status=active 조합은 LanguagePackRepository::getActiveCoreLocales() 의 승격 쿼리와 일치한다. 걸리면 그 locale 이 config('app.supported_locales') / config('app.translatable_locales') 로 올라가 시스템 전역이 바뀐다. 그 아래에서 시드·저장되는 모든 다국어 데이터에 실재하지 않는 로케일 키가 박히고, 샘플을 비활성화한 뒤에는 그 키가 TranslatableField 검증을 통과하지 못해 해당 엔티티의 저장이 막힌다. 오염 시점과 증상 발현 시점이 떨어져 있어 원인 추적이 어렵다.

같은 원리로 is_default / status=active 같은 승격·활성 플래그를 샘플에 붙이지 않는다. 샘플 레코드를 만들기 전에 "이 조합이 전역 설정을 읽는 쿼리에 걸리는가" 를 확인한다.

자동 차단: 정적 검사 대상 (위반 시 차단).

설명 백필 커맨드 (api:docgen-backfill-*)

파라미터/응답 필드 표의 <!-- TODO: 설명 --> 셀 중 도메인 무관 공통 필드(페이지네이션·정렬·검색· 식별자·기간·토글·공통 파생 필드 등)는 전체 재생성 없이 in-place 로 자동 서술한다. 실측을 재수행하지 않아 빠르고, 실측 예시값도 그대로 둔다(멱등 — 채워진 셀·실측 예시값 불가침).

# 요청 파라미터 표 TODO 셀 백필 (SSoT: App\Support\ApiDoc\ParameterDescriber)
php artisan api:docgen-backfill-params

# 응답 필드 표 TODO 셀 백필 (SSoT: App\Support\ApiDoc\ResourceFieldDescriber)
php artisan api:docgen-backfill-fields
  • 도메인 종속 필드(status/type/code/category·훅 주입·조건부 필드)는 값 의미가 도메인마다 달라 자동 채우지 않고 TODO 로 남겨 사람이 컨트롤러/FormRequest/Resource 근거로 서술한다.
  • 새 공통 필드 규칙을 Describer 사전에 추가한 뒤 백필을 재실행하면 그 필드가 전 문서에서 자동 채워진다.

문서 갱신 의무

컨트롤러/라우트/FormRequest/Resource 를 추가·변경하면 대응 API 문서를 같은 변경 단위에서 갱신한다.

  • 트리거: app/Http/Controllers/**, routes/api.php, app/Http/Requests/**, app/Http/Resources/** (+ 확장 대응 경로) 편집.
  • 절차: 코드 변경 → api:docgen --scope=... 재실행 → @generated 블록 갱신 → 신규 TODO 서술 채움.
  • 검증: api:docgen --check 로 drift 0 확인. 정적 검사가 변경셋에 대응 문서 동반 여부를 검사한다. 강제 수준은 대상별로 다르다 — 문서가 완비된 대상은 차단, 진행 중 대상은 경고에 그친다. 코어(docs/backend/api/)는 2026-07-08 문서 완비로 차단 대상이 됐다. 즉 코어 API 표면(routes/api.php·app/Http/{Controllers,Requests,Resources}/**)을 변경하면서 코어 API 문서를 함께 갱신하지 않으면 그 변경은 통과하지 못한다. 나머지 확장은 문서 완비 시 순차로 차단 대상에 편입된다.
  • 면제: src/routes/api.php 가 없는 web 전용 확장은 문서 대상이 아니므로 검사 대상에서 제외된다 ("문서 대상이 아닌 확장" 참조).

체크리스트

□ 그 확장에 api/ 표면이 있는가? (src/routes/api.php 부재 = web 전용 → 문서 대상 아님)
□ 엔드포인트가 대응 위치(코어 docs/backend/api/ 또는 확장 docs/api/)에 문서화되었는가?
□ 요청 파라미터 표에 위치/타입/필수/허용값/용도가 모두 기재되었는가?
□ 요청 예시(raw HTTP 요청) 블록이 방출되었는가? (curl 금지, 인증 필요 시 Bearer {YOUR_TOKEN} 마스킹)
□ 응답 필드 표가 envelope 의 data 내부 기준으로 작성되었는가?
□ 응답 예시(envelope 전문 JSON) 블록이 방출되었는가? (목록은 2항목 절단)
□ 훅 주입 파라미터가 있으면 주석 + 사람 보강이 되었는가?
□ 미채움 마커 5종이 전수 0 인가? (아래 참조 — `TODO:` 만 세는 부분 집계 금지)
□ api:docgen --check 가 drift 0 인가?

미채움 마커 5종 (전수 0 이 완료 기준)

api:docgen 은 실측할 수 없는 자리에 "사람이 작성하세요" 마커를 남긴다. 실측 불가는 그 자리를 비워둘 사유가 아니다. 컨트롤러/Resource/FormRequest/Enum/lang 을 읽어 코드 근거로 채우는 것이 규정이다.

마커 채울 것 근거 소스
<!-- 실측 제외: {사유} — 응답 필드는 사람이 작성하세요. --> 응답 필드 표 Resource toArray() 의 키 전수 + 타입, Enum label()/variant()
<!-- 실측 제외: {사유} — 응답 예시는 사람이 작성하세요. --> 응답 예시 JSON 컨트롤러의 ResponseHelper::success/paginated/dataSource 호출 형태 + Resource + lang 의 message 문구
<!-- TODO: 용도 --> 요청 파라미터 설명 셀 FormRequest rules()/messages(), 설정 키는 관리자 설정 UI 다국어 라벨
<!-- TODO: 설명 --> 응답 필드 설명 셀 Resource toArray() 파생 로직, 모델 $casts/관계, 마이그레이션 한국어 comment
_대표 에러 없음 ... <!-- TODO: ... -->_ 도메인 특이 에러 표 컨트롤러의 abort()/예외 throw, Service 의 커스텀 Exception

도메인 무관하게 의미가 고정된 파라미터/필드는 문서에 손으로 쓰지 말고 ParameterDescriber/ ResourceFieldDescriber 사전에 등재한다 — 사전은 재생성에 영속하고 신규 문서에도 자동 적용된다. 반대로 도메인 특이 의미를 공통 사전에 넣지 않는다 (오설명이 전 문서로 퍼진다).

전수 집계는 5종 마커를 한 번에 세야 한다. TODO: 만 세면 그 바깥의 미채움(실측 제외 등)이 집계에서 빠져 부분 집계가 완료로 통과한다:

grep -rcE "실측 제외:|TODO: 용도|TODO: 설명|실측 응답에 필드 없음|대표 에러 없음" \
  --include=*.md docs/backend/api {modules,plugins}/_bundled/*/docs/api

관련 문서