인터뷰에서 방향만 정하고 남겨 뒀던 후속 5건을 구현하고, 커밋 전 종합 점검에서 발견한 결함 2건을 함께 고쳤다. 기능 도입: - 업로드 이미지 축소 — `upload.image_max_width` / `image_max_height` / `image_quality` 는 저장·검증만 되고 리사이즈 코드가 0건이었다. 비율 유지, 상한 이내면 재인코딩하지 않음(불필요한 화질 손실 방지), 한계 미설정이면 무동작(기존 사이트 이미지 보호). PNG·GIF·WebP 투명도 보존. 실패해도 업로드는 계속되고 원본이 저장된다. - 로그인 2단계 인증 — `security.two_factor_auth` 는 설정 항목만 있고 구현도 화면 컨트롤도 없었다. 코어 IDV 인프라를 재사용해 purpose `login` 을 추가하고, 비밀번호 확인 통과 후 challenge 만 발행한다(토큰 미발급 + logout). `POST /api/auth/login/two-factor` 가 코드를 확인해야 토큰이 나가며, 로그인과 같은 요청 제한을 적용한다. challenge 의 purpose 를 대조해 다른 흐름의 challenge 로 로그인하는 것을 막는다. 코드 발송이 실패하면 실패로 알린다 — 2단계를 건너뛰고 로그인시키면 통제가 조용히 열린다. 결함 수정: - 게시글 수정 시 사용자 경로가 `attachment_ids` 를 Service 로 넘기지 않아, 검증과 권한은 통과하고 200 을 받는데 첨부만 조용히 연결되지 않았다. 관리자 경로는 넘기고 있어 같은 요청이 화면에 따라 다르게 동작했다. - 설정 화면 라디오가 `pointer-events-none` + Label click 으로만 상태를 바꿔, 키보드 방향키(change) 조작 시 화면 표시와 저장될 값이 어긋났다. 동일 패턴 4개 레이아웃 12개 라디오에 change 액션을 부여하고 params 를 click 과 동일하게 고정한다. - 주문완료 화면이 결제수단 확인보다 영수증 조회를 앞세워, PG 를 거치지 않는 주문에서도 매번 404 요청이 나갔다. - 개별 게시판 폼의 숫자 항목에 경계값 바인딩이 없어 환경설정 화면과 안내가 달랐다. 커밋 전 점검에서 발견: - 2단계 인증이 반쪽으로 들어가 있었다. enum 에 case 만 추가하고 코어 purpose 레지스트리와 ko/en 라벨을 빠뜨려, 목적 목록 API 에서 통째로 빠지고 `hasPurpose` 가 false 였으며 라벨 대신 i18n 키 원문이 반환됐다. 관리자가 이 목적의 메시지 템플릿·정책을 만들 수 없는 상태였다. 세 곳을 enum 기준으로 전수 대조하는 회귀 테스트를 함께 둔다. - 업로드 이미지 축소가 적용되지 않은 8번째 서비스(레이아웃 편집기 첨부)가 있었다. 열거형 커버리지 테스트는 내 조사 결과를 green 으로 고정할 뿐 전수를 증명하지 못한다 — 같은 규약을 조건으로 표현한 audit 룰이 첫 실행에서 검출했다. 재발 방지: - audit 룰 `upload-service-image-resize` (신규 업로드 서비스의 축소 누락 검출) - 트러블슈팅 사례 20 (코어 설정이 일부 경로에만 적용되는 증상의 진단 절차) - `sirsoft-ckeditor5` 의 코어 최소 요구 버전을 7.0.6 으로 상향 (신설 클래스 의존) 공개 이슈: gnuboard/g7
API 레퍼런스 문서 목차
소유: 코어 · 생성:
php artisan api:docgen(실측 기반). 아래 표는 자동 생성됩니다. 각 문서를 열면 엔드포인트별 파라미터·응답·예시를 볼 수 있습니다.
G7 의 REST API 레퍼런스입니다. 도메인별 문서에는 엔드포인트마다 메서드·URI·인증/권한, 요청 파라미터 표, 응답 필드 표, 실제 호출로 관측한 요청·응답 예시가 실려 있습니다. 아래 공통 규약은 모든 엔드포인트에 동일하게 적용되므로 개별 문서에서 반복하지 않습니다.
문서 작성·갱신 규정은 api-documentation.md 를 참고하세요.
공통 규약
인증
Laravel Sanctum 의 Bearer 토큰 전용입니다(세션 쿠키 인증 미사용).
POST /api/auth/login 또는 POST /api/auth/admin/login 으로 토큰을 발급받아 모든 후속 요청에 실어 보냅니다.
GET /api/me HTTP/1.1
Host: api.example.com
Accept: application/json
Authorization: Bearer {YOUR_TOKEN}
각 문서의 인증/권한 줄은 다음 네 가지 중 하나입니다.
| 표기 | 의미 |
|---|---|
공개 (인증 불필요) |
토큰 없이 호출 가능 |
optional.sanctum |
토큰이 있으면 회원, 없으면 비회원으로 처리 (둘 다 접근 가능) |
auth:sanctum |
유효한 토큰 필수 (없거나 만료 시 401) |
auth:sanctum + admin + permission:{키} |
토큰 + 관리자 + 해당 권한 필요 (권한 부족 시 403) |
로케일
응답 메시지의 언어는 ① 로그인 사용자의 users.language → ② Accept-Language 헤더 →
③ 시스템 기본값(config('app.locale')) 순으로 결정됩니다. 지원 언어는 config('app.supported_locales') 를 따릅니다.
응답 봉투
성공·실패 모두 동일한 최상위 구조를 씁니다. 실제 페이로드는 항상 data 안에 들어갑니다.
{
"success": true,
"message": "요청이 성공했습니다.",
"data": { }
}
실패 시 success 는 false 이고, 검증 오류는 errors 에 필드별 메시지 배열로 담깁니다.
{
"success": false,
"message": "입력값이 올바르지 않습니다.",
"errors": {
"email": ["이메일 형식이 올바르지 않습니다."]
}
}
페이지네이션
목록 엔드포인트는 data.data[] 에 항목 배열을, data.pagination 에 페이지 정보를 담습니다.
요청은 page 와 per_page 쿼리 파라미터로 제어합니다.
{
"pagination": {
"current_page": 1,
"last_page": 4,
"per_page": 25,
"total": 87,
"from": 1,
"to": 25,
"has_more_pages": true
}
}
일부 목록은 data.abilities 에 컬렉션 레벨 권한(can_create, can_delete 등)을 함께 반환합니다.
화면의 버튼 노출 여부를 이 값으로 판정하세요.
공통 에러 상태코드
| 상태코드 | 의미 | 발생 조건 |
|---|---|---|
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 엔드포인트가 요구하는 권한이 없는 경우 |
| 404 | Not Found | 대상 리소스가 없거나 접근 범위를 벗어난 경우 |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (errors 에 필드별 메시지) |
| 428 | Precondition Required | 본인인증(IDV)이 선행되어야 하는 경우 |
428 응답은 error_code: "identity_verification_required" 와 함께 verification 객체를 반환합니다.
클라이언트는 이 값으로 본인인증 화면을 띄운 뒤 원래 요청을 재시도합니다.
{
"success": false,
"error_code": "identity_verification_required",
"message": "본인인증이 필요합니다.",
"verification": { }
}
자산 URL 이중 모드
정적 파일 확장자(.js / .css / .json)로 끝나는 동적 엔드포인트는 확장자 없는 형태를 함께 제공합니다.
아래 문서에 실린 URI 는 확장자 형태를 기준으로 표기하지만, 각 엔드포인트는 대응하는 확장자 없는 형태로도
동일한 응답·동일한 권한 가드로 호출할 수 있습니다.
이유는 서버 설정입니다. nginx/Apache 의 표준적 정적 최적화 블록은 URL 마지막 확장자로 분기하며,
nginx 에서 정규식 location 은 프리픽스 location 보다 먼저 매칭되므로 try_files ... /index.php 폴백이
실행될 기회가 없습니다. 그런 환경에서는 확장자 붙은 동적 응답이 PHP 에 도달하지 못하고 404 가 됩니다.
location ~* \.(js|css|json)$ { expires max; access_log off; }
| 확장자 형태 | 확장자 없는 형태 | 변환 규칙 |
|---|---|---|
/api/templates/{id}/routes.json |
/api/templates/{id}/routes |
접미사 제거 |
/api/layouts/{tpl}/{layout}.json |
/api/layouts/{tpl}/{layout} |
접미사 제거 |
/api/modules/bundle.js |
/api/modules/bundle/js |
접미사를 경로 세그먼트로 (js/css 구분이 필요) |
/api/templates/assets/{id}/js/a.js |
/api/templates/assets/{id}?file=js/a.js |
파일 경로를 file 쿼리로 (경로가 곧 파일명이라 제거 불가) |
file 쿼리 형태가 안전한 이유는 nginx 의 location 정규식이 쿼리스트링을 제외한 경로에만 매칭되기 때문입니다.
확장자 없는 형태에도 경로 탈출 방어와 확장자 화이트리스트가 동일하게 적용됩니다.
두 형태는 모두 영구 유지됩니다. 확장자 형태를 제거하면 URL 을 하드코딩한 서드파티 확장이 깨집니다.
어느 형태를 쓸지는 서버 환경에 따라 결정되며, 다음 프로브 엔드포인트로 판정합니다.
| 메서드 | URI | 인증/권한 | 설명 |
|---|---|---|---|
| GET | /api/system/asset-probe.js |
공개 (인증 불필요) | 확장자 형태 프로브 |
| GET | /api/system/asset-probe |
공개 (인증 불필요) | 대조군 |
두 URL 을 브라우저에서 쌍으로 요청합니다(서버측 loopback curl 은 vhost·프록시 체인을 우회해 오판합니다).
응답은 application/javascript 이며 본문에 매직 토큰 G7_ASSET_PROBE_OK 를 담습니다. DB 에 접근하지 않고
Cache-Control: no-store 로 캐시되지 않습니다.
asset-probe.js |
asset-probe |
판정 |
|---|---|---|
| 성공 | 성공 | 확장자 형태 사용 가능 |
| 실패 | 성공 | 정적 블록 가로채기 확정 — 확장자 없는 형태 사용 |
| 실패 | 실패 | 모드 문제가 아님 (PHP/라우팅 장애) |
성공 판정은 상태코드가 아니라 본문의 매직 토큰과 Content-Type 으로 합니다. 상태코드만 보면 "404 대신 200 + 에러 HTML" 이나 catch-all 200 페이지를 반환하는 설정에서 영원히 오판합니다.
코어 API 레퍼런스
- 문서 수: 35 · 엔드포인트 수: 291
| 문서 | 도메인 | 엔드포인트 |
|---|---|---|
| activity-logs.md | activity-logs |
3 |
| attachment.md | attachment |
1 |
| attachments.md | attachments |
4 |
| auth.md | auth |
15 |
| avatar.md | avatar |
2 |
| broadcasting.md | broadcasting |
1 |
| changelog.md | changelog |
1 |
| core-update.md | core-update |
2 |
| dashboard.md | dashboard |
5 |
| extensions.md | extensions |
3 |
| identity.md | identity |
27 |
| language-packs.md | language-packs |
15 |
| layouts.md | layouts |
2 |
| license.md | license |
1 |
| locales.md | locales |
1 |
| me.md | me |
3 |
| menus.md | menus |
10 |
| modules.md | modules |
25 |
| notification-channels.md | notification-channels |
1 |
| notification-definitions.md | notification-definitions |
5 |
| notification-logs.md | notification-logs |
3 |
| notification-templates.md | notification-templates |
4 |
| notifications.md | notifications |
14 |
| password.md | password |
1 |
| permissions.md | permissions |
1 |
| plugins.md | plugins |
27 |
| profile.md | profile |
4 |
| roles.md | roles |
7 |
| schedules.md | schedules |
12 |
| search.md | search |
1 |
| seo.md | seo |
5 |
| settings.md | settings |
15 |
| templates.md | templates |
57 |
| users.md | users |
12 |
| verify-password.md | verify-password |
1 |
확장 API 레퍼런스
각 확장이 자신의 API 문서를 소유합니다. 아래 표는 자동 생성됩니다.
- 확장 수: 10 · 엔드포인트 수: 384
| 확장 | 유형 | API 문서 목차 | 문서/엔드포인트 |
|---|---|---|---|
gnuboard7-hello_module |
모듈 | docs/api/ | 1 / 2 |
sirsoft-board |
모듈 | docs/api/ | 10 / 80 |
sirsoft-ecommerce |
모듈 | docs/api/ | 33 / 231 |
sirsoft-page |
모듈 | docs/api/ | 2 / 17 |
sirsoft-ckeditor5 |
플러그인 | docs/api/ | 2 / 2 |
sirsoft-gdpr |
플러그인 | docs/api/ | 4 / 15 |
sirsoft-marketing |
플러그인 | docs/api/ | 2 / 2 |
sirsoft-message_bizppurio |
플러그인 | docs/api/ | 6 / 12 |
sirsoft-pay_kginicis |
플러그인 | docs/api/ | 5 / 22 |
sirsoft-verification_kginicis |
플러그인 | docs/api/ | 1 / 1 |