Files
Gnuboard7/docs/backend/api/README.md
T
HeuJung 50007d5cc6 fix(auth): 2단계 인증을 켠 사이트의 로그인 흐름 구현
2단계 인증은 7.0.6 에서 서버측이 갖춰졌지만 인증번호를 입력할 화면이 어느 버전에도
없었다. 그래서 그 설정을 켠 사이트는 관리자를 포함한 전원이 로그인할 수 없었다.

원인은 `POST /api/auth/login` 이 조건에 따라 **다른 형태의 200** 을 돌려준다는 것이다.
평소에는 `{token, user}` 지만 2단계 인증이 켜져 있으면 `{two_factor_required,
challenge_id, ...}` 를 돌려준다. 프론트는 앞의 형태만 선언하고 `response.data.user.language`
를 바로 읽었으므로 그 자리에서 TypeError 가 났고, 영문 원문이 로그인 화면에 그대로 노출됐다.
서버는 정상 응답했으므로 서버 로그에는 아무 흔적도 남지 않는다.

이어서 `setToken(undefined)` 가 `localStorage` 에 문자열 `"undefined"` 를 남겼다.
이 값은 truthy 라 이후 모든 요청이 `Bearer undefined` 로 나가 401 이 되고, 사용자에게는
「세션이 만료되었습니다」로 보인다. 관리자 로그인은 한발 더 나가 `null->isAdmin` 으로
500 이 되어, 설정을 되돌릴 수단까지 함께 사라졌다.

## 구현

- 로그인 응답을 판별 유니온(`LoginResult`)으로 표현하고, 형태를 판별한 뒤에 읽는다.
 `ApiClient.setToken` 은 비어 있지 않은 문자열만 저장한다.
- 사용자·관리자 로그인 화면에 인증번호 입력 단계를 추가했다. 같은 카드 안에서 넘어가며
 「인증번호 다시 받기」와 「처음부터」를 제공한다. 관리자 판정은 코드 확인에 성공한 뒤에
 수행하고, 거부할 때는 그 직전에 발급된 토큰을 회수한다.
- 재발송(`login/two-factor/resend`)은 기존 challenge 를 취소하고 새로 발행한다. 유효한
 코드를 여러 개 살려 두면 대입 시도의 표적이 넓어진다.
- 인증번호를 보내지 못하면 401 이 아니라 503 으로 답한다. 자격 증명은 올바른데 401 로
 뭉개면 사용자는 비밀번호를 의심하며 같은 시도를 반복하고, 운영자는 메일 설정이 깨진
 사실을 알 방법이 없다.
- 공개 본인인증 경로(`identity/verify`·`cancel`)가 로그인 목적의 challenge 를 소진하지
 못하도록 403 게이트를 세웠다. 소진되면 그 challenge 로 영영 로그인할 수 없다.
- 로그인 시도 제한 429 응답이 다국어 문구를 싣도록 했다(종전에는 프레임워크 기본 영문).
- 다국어 파라미터에서 파이프 표현식이 평가되지 않아 「유효시간 까지」처럼 값이 빠지던
 문제를 함께 고쳤다. 같은 결함이 문의 목록 화면에도 있었다.

## 이번 점검에서 함께 고친 것

- 계정 잠금(423)·발송 실패(503) 응답이 사용자·관리자 컨트롤러에 동일하게 복제돼 있었고
 그 주석 자신은 "단일 지점에서 만든다" 고 적혀 있었다. 페이로드에 필드가 하나 추가되면
 한쪽만 따라가 같은 실패를 두 화면이 다르게 안내하게 된다 — 트레이트로 통합했다.
- 테스트가 개발자 자신의 사이트 설정을 읽고 있었다. 2단계 인증을 켜 둔 환경에서는 로그인
 성공을 전제한 테스트가 503 으로 깨지는데 실패 메시지가 원인을 가리키지도 않는다.
 같은 결함군을 위해 이미 존재하던 단일 지점에 그 축을 추가했다.

## 버전

코어 7.0.11 · sirsoft-basic 1.1.4 · sirsoft-admin_basic 1.0.9 ·
번들 일본어팩 3종 · 템플릿 엔진 engine-v1.65.0.
2026-09-07 17:08:14 +09:00

16 KiB

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
    }
}

총 건수 정확도 (대용량 목록)

매칭이 아주 많을 수 있는 목록(검색 등)은 총 건수를 상한까지만 셉니다. 그런 목록은 위 필드에 더해 정확도를 함께 내보내며, 세지 않은 값을 정확한 것처럼 말하지 않습니다.

필드 타입 의미
total_relation string exact(정확) 또는 at_least(그 이상)
total_is_exact boolean 총 건수가 정확한지 여부
result_cap integer|null 집계에 적용된 상한 (무제한이면 null)

상한을 넘긴 경우 동작은 이렇습니다.

  • total 은 상한값이며 그 이상이라는 뜻입니다 (화면은 "10,000건 이상" 으로 표기)
  • last_page 는 null 입니다 — 총 건수를 알아야 계산되는 유일한 값이라 계산할 수 없습니다
  • has_more_pages 는 그대로 정확합니다. 다음 페이지 이동은 끝까지 열려 있습니다

즉 상한에 걸려도 막히는 것은 마지막 페이지 점프 하나뿐입니다.

단순형·커서형 응답

총 건수를 아예 세지 않는 목록(simplePaginate)은 total 과 last_page 를 내보내지 않습니다. 커서 방식 목록은 대신 next_cursor / prev_cursor 를 실어 보냅니다. 없는 필드를 0 이나 1 로 채우지 않으므로, 화면은 필드 존재 여부로 목록의 종류를 구분할 수 있습니다.

상한·커서 규약 상세: pagination.md

일부 목록은 data.abilities 에 컬렉션 레벨 권한(can_create, can_delete 등)을 함께 반환합니다. 화면의 버튼 노출 여부를 이 값으로 판정하세요.

공통 에러 상태코드

상태코드 의미 발생 조건
401 Unauthenticated 유효한 Bearer 토큰이 없거나 만료된 경우
403 Forbidden 엔드포인트가 요구하는 권한이 없는 경우
404 Not Found 대상 리소스가 없거나 접근 범위를 벗어난 경우
422 Unprocessable Entity 요청 파라미터가 검증 규칙을 위반한 경우 (errors 에 필드별 메시지)
428 Precondition Required 본인인증(IDV)이 선행되어야 하는 경우

확장(모듈·플러그인)이 제공하는 엔드포인트(/api/modules/{id}/…, /api/plugins/{id}/…)는 그 확장이 활성 상태일 때만 존재합니다. 비활성화·제거된 확장의 엔드포인트는 404 를 반환하며, 이는 권한 문제가 아니라 라우트가 등록되지 않은 상태입니다. 확장을 업데이트하는 동안에도 잠시 같은 상태가 됩니다.

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 을 하드코딩한 서드파티 확장이 깨집니다.

CSS 응답의 상대 참조 치환

확장 자산 엔드포인트(/api/{templates|modules|plugins}/assets/…)가 CSS 를 서빙할 때는 본문 안의 상대 참조를 절대 자산 URL 로 바꿔 내보냅니다. 대상은 url(...) 과 @import "…" 이며, 절대 URL · 프로토콜 상대(//host/x) · data: 같은 스킴 참조는 원문 그대로 둡니다.

브라우저는 CSS 안의 상대 참조를 그 스타일시트 URL 의 디렉토리 기준으로 해석합니다. 확장자 없는 형태에서는 경로의 마지막 세그먼트가 확장 식별자이고 파일 경로가 쿼리에 있으므로 기준 디렉토리가 /api/{타입}/assets/ 로 잡히고, url('./woff2/f.woff2') 가 실재하지 않는 주소를 가리킵니다. 치환은 이 어긋남을 서빙 시점에 해소합니다.

치환된 본문은 두 가지 계약을 따릅니다.

  • ETag 는 내보내는 본문 기준입니다. 자산 URL 모드가 바뀌면 본문이 달라지므로 ETag 도 함께 바뀝니다.
  • 서브리소스는 CSS 가 받은 v 를 승계합니다. 캐시 버전이 오르면 두 계층이 같은 시점에 무효화됩니다.

CSS 가 아닌 자산은 바이트 그대로 서빙됩니다. 정적 게시본(/build/ext/{v}/…)은 웹서버가 직접 경로 형태로 서빙하므로 상대 해석이 원래 정상이며 이 경로를 타지 않습니다.

어느 형태를 쓸지는 서버 환경에 따라 결정되며, 다음 프로브 엔드포인트로 판정합니다.

메서드 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 페이지를 반환하는 설정에서 영원히 오판합니다.

보안 게이트 (KVE-2026 대응)

일부 관리 엔드포인트에는 표준 응답 외에 다음 보안 게이트가 적용됩니다(각 엔드포인트 표에는 별도 표기가 없어도 공통 적용).

  • 등급 상한 (KVE-2026-1919) — 사용자·역할 쓰기 경로:
    • PUT /api/admin/users/{user}, POST /api/admin/users/{user}/unlock: 비-슈퍼관리자 액터가 슈퍼 관리자 계정을 수정·잠금해제하려 하면 403 (exceptions.cannot_modify_super_admin).
    • PATCH /api/admin/users/bulk-status: 비-슈퍼관리자 액터가 포함시킨 슈퍼 관리자 대상은 일괄 처리에서 제외(요청은 200, 슈퍼 관리자 상태 불변).
    • POST /api/admin/roles, PUT /api/admin/roles/{role}: 비-슈퍼관리자 액터가 자신이 보유하지 않았거나 자신의 범위(scope)보다 넓은 권한을 부여하려 하면 403 (exceptions.cannot_grant_unheld_permission).
    • PUT /api/admin/roles/{role}, PATCH /api/admin/roles/{role}/toggle-status: 비-슈퍼관리자 액터가 코어/확장 소유 역할(예: admin)을 수정·토글하려 하면 403 (exceptions.cannot_modify_protected_role).
    • 슈퍼 관리자 액터의 동일 작업은 정상 수행됩니다.
  • 레이아웃 저장 표현식/URL 검증 (KVE-2026-1915) — 레이아웃 생성·수정(POST/PUT /api/admin/layouts*)의 content 검증:
    • {{...}}·computed·init_actions/actions 문자열 값에 위험 토큰이 있으면 422 (validation.layout.dangerous_expression). 차단 토큰은 프로토타입 체인 접근(.constructor/.__proto__/.prototype, ['constructor'], 원시 __proto__)·Function(·eval(·import( 입니다.
    • scripts[].src·data_sources[].endpoint 가 same-origin path-only(/ 시작)가 아니면 422 (validation.layout.external_resource_url). 단, 활성 확장(모듈·플러그인·템플릿)이 자기 manifest 의 trusted_script_hosts 로 선언한 호스트는 예외로 허용됩니다 — 이 목록은 확장 배포물이 정하며 요청으로 바꿀 수 없습니다.
    • 정상 표현식(조건·계산·목록 가공·화살표 함수·템플릿 리터럴·경로 조립)은 통과합니다.

코어 API 레퍼런스

  • 문서 수: 36 · 엔드포인트 수: 328
문서 도메인 엔드포인트
activity-logs.md activity-logs 3
attachment.md attachment 1
attachments.md attachments 4
auth.md auth 18
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 8
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 18
system.md system 2
templates.md templates 57
users.md users 12
verify-password.md verify-password 1

확장 API 레퍼런스

각 확장이 자신의 API 문서를 소유합니다. 아래 표는 자동 생성됩니다.

  • 확장 수: 14 · 엔드포인트 수: 428
확장 유형 API 문서 목차 문서/엔드포인트
gnuboard7-hello_module 모듈 docs/api/ 1 / 7
sirsoft-board 모듈 docs/api/ 10 / 80
sirsoft-ecommerce 모듈 docs/api/ 33 / 239
sirsoft-page 모듈 docs/api/ 2 / 17
sirsoft-ckeditor5 플러그인 docs/api/ 3 / 5
sirsoft-gdpr 플러그인 docs/api/ 4 / 15
sirsoft-marketing 플러그인 docs/api/ 2 / 2
sirsoft-message_bizppurio 플러그인 docs/api/ 6 / 21
sirsoft-pay_kginicis 플러그인 docs/api/ 5 / 34
sirsoft-pay_nhnkcp 플러그인 docs/api/ 0 / 0
sirsoft-pay_nicepayments 플러그인 docs/api/ 0 / 0
sirsoft-tosspayments 플러그인 docs/api/ 2 / 4
sirsoft-verification_kginicis 플러그인 docs/api/ 2 / 3
sirsoft-verification_nhnkcp 플러그인 docs/api/ 1 / 1