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.
백엔드 개발 가이드
그누보드7 백엔드 개발을 위한 종합 가이드입니다.
핵심 원칙
필수: FormRequest + Custom Rule 사용 (Service에 검증 로직 금지)
필수: FormRequest + Custom Rule 패턴 사용
✅ 필수: __() 함수를 사용한 다국어 처리
✅ 필수: 상태/타입/분류는 Enum으로 정의
API 레퍼런스
엔드포인트별 요청 파라미터·응답 필드·요청/응답 예시는 api/README.md 에 있습니다. 공통 규약(Bearer 토큰 인증, 응답 봉투, 페이지네이션, 401/403/422/428 에러)도 그 문서 상단에 정리되어 있습니다. 확장(모듈·플러그인)이 소유한 API 문서 목록도 같은 문서에서 찾을 수 있습니다.
문서 작성·갱신 규정은 api-documentation.md 를 참고하세요.
문서 목록
| 문서 | 제목 | 핵심 내용 |
|---|---|---|
| activity-log-hooks.md | 활동 로그 훅 레퍼런스 (Activity Log Hooks Reference) | 코어 66훅 + 확장 132훅 = 총 198훅 (확장별 목록은 그 확장이 소유) |
| activity-log.md | 활동 로그 시스템 (Activity Log System) | Monolog 기반: Service 훅 → Listener → Log::channel... |
| admin-settings-access.md | Admin 환경설정 값 접근 (g7_core_settings vs config()) |
동기화 SSoT: storage/app/settings/*.json → Setting... |
| api-documentation.md | API 레퍼런스 문서 규정 (API Documentation) | 모든 API 엔드포인트는 레퍼런스 문서 필수 — 메서드/URI/파라미터/응답 필드 +... |
| api-resources.md | API 리소스 | Resource: BaseApiResource 상속 필수 / Collection: B... |
| authentication.md | 인증 및 세션 처리 | Laravel Sanctum 토큰 전용 인증 (Bearer 토큰만 사용) |
| benchmark.md | 성능 계측 시스템 (Benchmark) | g7:bench 가 4축(list/screen/write/batch)을 잰다 — ... |
| broadcasting.md | Broadcasting (실시간 이벤트) | Laravel Reverb 사용 (WebSocket) |
| console-confirm.md | 콘솔 yes/no 프롬프트 (ConsoleConfirm) | 콘솔 커맨드의 yes/no 프롬프트는 $this->unifiedConfirm() 사용... |
| controllers.md | 컨트롤러 계층 구조 | AdminBaseController / AuthBaseController / Publ... |
| core-config.md | 코어 설정 (config/core.php) | config/core.php = 코어 권한/역할/메뉴/메일템플릿의 SSoT (Sing... |
| core-update-system.md | 코어 업데이트 시스템 (Core Update System) | 코어 업그레이드 스텝: upgrades/ 디렉토리 (프로젝트 루트), 네임스페이스 A... |
| data-sync-helpers.md | 데이터 동기화 Helper (Data Sync Helpers) | 모든 데이터 동기화는 Service/Seeder 가 Helper 를 호출해 수행 (직... |
| dto.md | DTO (Data Transfer Object) 사용 규칙 | DTO 두 패턴 — Value Object(불변 1회 전달) vs Data Carri... |
| enum.md | Enum 사용 규칙 | 상태/타입/분류 = Enum 필수 (PHP 8.1+ Backed Enum) |
| exceptions.md | Custom Exception 다국어 처리 | 예외 메시지 하드코딩 금지 → __() 함수 필수 |
| geoip.md | GeoIP 시스템 (MaxMind GeoLite2) | MaxMind GeoLite2-City DB 기반 IP → 타임존 감지 (SetTim... |
| identity-messages.md | 본인인증 메시지 템플릿 시스템 (Identity Messages) | 알림 시스템(notification_*)과 완전 분리된 IDV 전용 템플릿 인프라 |
| identity-policies.md | 본인인증 정책 시스템 (Identity Policies) | - |
| identity-providers.md | IDV Provider 작성 가이드 (Identity Verification Providers) | VerificationProviderInterface 구현 + IdentityProv... |
| language-pack-service.md | LanguagePackService (백엔드 Service 레이어) | LanguagePackService 가 install/activate/deactiva... |
| middleware.md | 미들웨어 등록 규칙 | 인증 필요 미들웨어 → 전역 등록 금지! |
| notification-system.md | 알림 시스템 (Notification System) | GenericNotification 범용 클래스 1개로 모든 알림 처리 (개별 클래스... |
| pagination.md | 대용량 목록 페이지네이션 (Pagination) | 총 건수만 상한을 받는다 — 상한 이하면 정확, 초과면 "이상"(total_relat... |
| response-helper.md | API 응답 규칙 (ResponseHelper) | 모든 API 응답은 ResponseHelper 사용 |
| reverse-proxy.md | 리버스 프록시 환경 (Reverse Proxy) | 프록시 뒤에서는 요청이 스스로 스킴·IP 를 증명하지 못한다 — 신뢰할 프록시를 지정... |
| routing.md | 라우트 네이밍 및 경로 | 모든 라우트는 name() 필수: ->name('api.users.index') |
| search-system.md | Scout 검색 엔진 시스템 (Search System) | Laravel Scout + DatabaseFulltextEngine: MySQL F... |
| seo-system.md | SEO 페이지 생성기 시스템 (SEO Page Generator) | SeoMiddleware: 봇 요청 감지 → ?locale= 파라미터 해석 → Seo... |
| service-provider.md | 서비스 프로바이더 안전성 | DB 접근 전 .env 파일 존재 확인 필수 |
| service-repository.md | Service-Repository 패턴 | RepositoryInterface 주입 필수 (구체 클래스 직접 주입 금지) |
| settings-multilingual-enrichment.md | Settings 카탈로그 다국어 자동 보강 | settings JSON 의 다국어 카탈로그 라벨(_cached_name 등)은 카탈... |
| static-asset-publishing.md | 부트스트랩 리소스 정적 게시 (Static Asset Publishing) | 게시물: public/build/ext/{cache_version}/ — 수명주기 이... |
| translatable-seeders.md | 다국어 시더 인터페이스 (Translatable Seeders) | 다국어 JSON 컬럼(name 등)을 시드하는 확장 entity 시더는 Transla... |
| user-overrides.md | 사용자 수정 보존 (HasUserOverrides Trait) | 모델에 use HasUserOverrides; + `protected array ... |
| validation.md | 검증 (Validation) | 필수: FormRequest에서 검증 (Service에 검증 로직 배치 금지) |
아키텍처 개요
계층 분리
Controller → Request → Service → Repository → Model
컨트롤러 계층 구조
BaseApiController (최상위)
├── AdminBaseController (관리자 전용)
├── AuthBaseController (인증된 사용자)
└── PublicBaseController (공개 API)
Service-Repository 패턴
// Service에서 훅 실행
HookManager::doAction('module.entity.before_create', $data);
$data = HookManager::applyFilters('module.entity.filter_data', $data);
$result = $this->repository->create($data);
HookManager::doAction('module.entity.after_create', $result);
파사드 사용 규칙
// ✅ DO: 파사드 앞 역슬래시 제거
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Auth;
Log::info('메시지');
Auth::user();
// ❌ DON'T: 역슬래시 사용
\Log::info('메시지');
auth()->user();
핵심 규칙 요약
1. 검증 로직 위치
- ❌ Service 클래스에서 검증 금지
- ✅ FormRequest에서 기본 검증
- ✅ Custom Rule에서 복잡한 검증
2. 다국어 처리
- ❌ 하드코딩된 메시지 금지
- ✅
__()함수 사용 필수 - ✅
lang/ko/,lang/en/파일 관리
3. Enum 사용
- ❌ 문자열/숫자 상수 직접 사용 금지
- ✅ Backed Enum 정의
- ✅ 타입 힌트 활용
4. 미들웨어 등록
- ❌ 인증이 필요한 미들웨어를 글로벌 등록 금지
- ✅ 그룹별 적절한 위치에 등록
- ✅ 실행 순서 고려
5. 서비스 프로바이더 안전성
- ❌ .env 없이 실패하는 코드 금지
- ✅ 환경 검증 후 로직 실행
- ✅ 인스톨러 안정성 확보
6. 외부 HTTP 호출
- ❌
file_get_contents($url)/fopen($url, ...)/stream_context_create([...])로 원격 URL 직접 호출 금지 - ✅
Illuminate\Support\Facades\Http(Laravel Http 파사드) 사용 - ✅ GitHub 연동은
App\Extension\Helpers\GithubHelper재사용 - 이유: 공유 호스팅은
allow_url_fopen=Off설정인 경우가 많아 URL 스트림 래퍼 기반 호출이 전부 실패합니다. Http 파사드는 cURL 기반이라 해당 설정의 영향을 받지 않습니다.
빠른 참조
자주 사용하는 클래스
| 클래스 | 위치 | 용도 |
|---|---|---|
| ResponseHelper | app/Helpers/ResponseHelper.php |
API 응답 표준화 |
| BaseApiResource | app/Http/Resources/BaseApiResource.php |
API 리소스 기본 클래스 |
| AdminBaseController | app/Http/Controllers/Api/Admin/AdminBaseController.php |
관리자 컨트롤러 |
훅 네이밍 규칙
[vendor-module].[entity].[action]_[timing]
예시:
sirsoft-ecommerce.product.before_create
sirsoft-ecommerce.product.after_update
sirsoft-ecommerce.product.filter_create_data
관련 문서
- AGENTS.md - 프로젝트 개발 가이드
- database-guide.md - 데이터베이스 규칙
- extension/ - 확장 시스템 (훅, 모듈, 플러그인)
- testing-guide.md - 테스트 규칙