fix: 회원가입 메뉴 생성 오류 수정 및 검증 에러 처리 개선

주요 변경사항:
- MenusStep.php: 존재하지 않는 컬럼(code, route_name, depth, description) 제거
- MenusStep.php: 실제 DB 스키마 컬럼(hidden, is_external, external_url) 추가
- RecipeRegistry.php: MenusStep 비활성화 (하이브리드 메뉴 생성 방식 도입)
- Handler.php: ValidationException 처리 개선 (실제 에러 메시지 표시, 422 상태 코드)

기술 세부사항:
- 하이브리드 접근: TenantBootstrapper(데이터) + MenuBootstrapService(메뉴)
- HTTP 상태 코드 표준화: 422 Unprocessable Entity (validation 실패)
- 실제 검증 에러 메시지 반환: errors 객체에 필드별 에러 정보 포함
This commit is contained in:
2025-11-10 09:35:43 +09:00
parent 9ba6e8d833
commit 92c60ff39f
4 changed files with 258 additions and 11 deletions

View File

@@ -1,5 +1,245 @@
# SAM API 저장소 작업 현황
## 2025-11-10 (일) - 회원가입 메뉴 생성 오류 수정 및 검증 에러 처리 개선
### 주요 작업
- **MenusStep 컬럼 오류 수정**: 존재하지 않는 컬럼(code, route_name, depth, description) 제거
- **하이브리드 메뉴 생성 방식 도입**: TenantBootstrapper에서 MenusStep 비활성화, MenuBootstrapService 활용
- **ValidationException 처리 개선**: 실제 검증 에러 메시지 표시 (422 상태 코드)
### 수정된 파일:
- `app/Services/TenantBootstrap/Steps/MenusStep.php` - 실제 DB 스키마에 맞게 컬럼 수정
- `app/Services/TenantBootstrap/RecipeRegistry.php` - MenusStep 비활성화 (주석 처리)
- `app/Exceptions/Handler.php` - ValidationException 처리 로직 개선
### 작업 내용:
#### 1. 문제 분석
**증상:**
```
SQLSTATE[42S22]: Column not found: 1054 Unknown column 'code' in 'field list'
SQL: insert into `menus` (..., `code`, `route_name`, `depth`, `description`, ...)
```
**원인:**
- `TenantObserver`가 Tenant 생성 시 자동으로 `TenantBootstrapper::bootstrap()` 호출
- `MenusStep.php`가 실제 DB에 없는 컬럼(`code`, `route_name`, `depth`, `description`) 사용 시도
- `RegisterService.php``MenuBootstrapService::createDefaultMenus()`와 중복 실행
**쿼리 과다 실행:**
- 메뉴 9개 생성 시 272개 쿼리 실행
- MenuObserver가 메뉴당 7개 권한 자동 생성 (view/create/update/delete/approve/export/manage)
- 중복 메뉴 생성 + 중복 권한 생성
#### 2. MenusStep.php 수정
**Before (잘못된 컬럼):**
```php
$newId = DB::table('menus')->insertGetId([
'tenant_id' => $tenantId,
'parent_id' => $newParentId,
'name' => $menu->name,
'code' => $menu->code ?? null, // ❌ 존재하지 않음
'icon' => $menu->icon ?? null,
'url' => $menu->url ?? null,
'route_name' => $menu->route_name ?? null, // ❌ 존재하지 않음
'sort_order' => $menu->sort_order ?? 0,
'is_active' => $menu->is_active ?? 1,
'depth' => $menu->depth ?? 0, // ❌ 존재하지 않음
'description' => $menu->description ?? null, // ❌ 존재하지 않음
'created_at' => now(),
'updated_at' => now(),
]);
```
**After (실제 DB 스키마):**
```php
$newId = DB::table('menus')->insertGetId([
'tenant_id' => $tenantId,
'parent_id' => $newParentId,
'name' => $menu->name,
'icon' => $menu->icon ?? null,
'url' => $menu->url ?? null,
'sort_order' => $menu->sort_order ?? 0,
'is_active' => $menu->is_active ?? 1,
'hidden' => $menu->hidden ?? 0, // ✅ 실제 컬럼
'is_external' => $menu->is_external ?? 0, // ✅ 실제 컬럼
'external_url' => $menu->external_url ?? null, // ✅ 실제 컬럼
'created_at' => now(),
'updated_at' => now(),
]);
```
**실제 DB 컬럼:**
```sql
id, tenant_id, parent_id, name, url, is_active, sort_order,
hidden, is_external, external_url, icon,
created_at, updated_at, created_by, updated_by, deleted_by, deleted_at
```
#### 3. 하이브리드 메뉴 생성 방식 도입
**배경:**
- **Option A**: TenantBootstrapper (글로벌 메뉴 복제, DB 의존)
- **Option B**: MenuBootstrapService (코드 기반, Git 버전 관리)
**선택: 하이브리드 접근** (Best Practice)
```
TenantObserver → TenantBootstrapper
├─ CapabilityProfilesStep ✅ (유지)
├─ CategoriesStep ✅ (유지)
├─ MenusStep ❌ (비활성화)
└─ SettingsStep ✅ (유지)
RegisterService → MenuBootstrapService ✅ (메뉴 생성)
```
**장점:**
- ✅ 메뉴 구조가 코드로 명확하게 정의됨 (`MenuBootstrapService.php`)
- ✅ Git으로 버전 관리 가능
- ✅ 새 메뉴 추가가 간단 (코드만 수정)
- ✅ 글로벌 메뉴 DB 데이터 불필요
- ✅ 부트스트랩 시스템 장점 유지 (CapabilityProfiles, Categories, Settings)
**RecipeRegistry.php 수정:**
```php
default => [ // STANDARD
new CapabilityProfilesStep,
new CategoriesStep,
// new MenusStep, // Disabled: Use MenuBootstrapService in RegisterService instead
new SettingsStep,
],
```
#### 4. ValidationException 처리 개선
**문제:**
```php
// Before - 모든 validation 에러를 "필수 파라미터 누락"으로 변환
if (
$exception instanceof ValidationException ||
$exception instanceof BadRequestHttpException
) {
return response()->json([
'success' => false,
'message' => '필수 파라미터 누락', // ❌ 실제 에러 메시지 손실
'data' => null,
], 400);
}
```
**증상:**
- Slack: "이메일은(는) 이미 사용 중입니다" (실제 에러)
- API 응답: "필수 파라미터 누락" (잘못된 메시지)
**수정:**
```php
// After - 실제 검증 에러 메시지 표시
if ($exception instanceof ValidationException) {
return response()->json([
'success' => false,
'message' => '입력값 검증 실패',
'data' => [
'errors' => $exception->errors(), // ✅ 실제 에러 정보
],
], 422); // ✅ 표준 validation 실패 코드
}
if ($exception instanceof BadRequestHttpException) {
return response()->json([
'success' => false,
'message' => '잘못된 요청',
'data' => null,
], 400);
}
```
**개선 효과:**
```json
// Before
{
"success": false,
"message": "필수 파라미터 누락",
"data": null
}
// After
{
"success": false,
"message": "입력값 검증 실패",
"data": {
"errors": {
"email": ["이메일은(는) 이미 사용 중입니다."],
"user_id": ["사용자 아이디은(는) 이미 사용 중입니다."]
}
}
}
```
### 기술 세부사항:
#### 메뉴 생성 방식 비교
**TenantBootstrapper + MenusStep (기존):**
- 장점: 체계적인 부트스트랩 시스템, 레시피 기반 확장
- 단점: 글로벌 메뉴 DB 데이터 필요, Git 버전 관리 불가, 메뉴 추가 시 DB 수정 필요
**MenuBootstrapService (새 방식):**
- 장점: 코드 기반, Git 버전 관리, 메뉴 추가 간단
- 단점: 부트스트랩 시스템과 분리
**하이브리드 (선택):**
- 데이터 부트스트랩(CapabilityProfiles, Categories, Settings)은 TenantBootstrapper 사용
- 메뉴 생성은 코드 기반 MenuBootstrapService 사용
- 양쪽 장점 활용
#### HTTP 상태 코드 표준화
- **422 Unprocessable Entity**: Validation 실패 (표준)
- **400 Bad Request**: 잘못된 요청 형식
- **401 Unauthorized**: 인증 실패
- **403 Forbidden**: 권한 없음
- **404 Not Found**: 리소스 없음
- **500 Internal Server Error**: 서버 에러
### SAM API Development Rules 준수:
**Service-First 아키텍처:**
- MenuBootstrapService에 메뉴 생성 로직
**멀티테넌시:**
- Tenant context 명시적 설정
- BelongsToTenant 스코프 활용
**코드 품질:**
- 실제 DB 스키마와 일치
- 명확한 주석 (비활성화 이유 설명)
**에러 처리:**
- 표준 HTTP 상태 코드
- 실제 검증 에러 메시지 표시
### 예상 효과:
1. **회원가입 정상 동작**: SQL 에러 해결
2. **쿼리 최적화**: 272개 → 약 100개 (중복 제거)
3. **유지보수 편의성**: 코드 기반 메뉴 관리
4. **명확한 에러 메시지**: 사용자가 정확한 문제 파악 가능
### 다음 작업:
- [x] MenusStep.php 컬럼 수정
- [x] RecipeRegistry.php MenusStep 비활성화
- [x] Handler.php ValidationException 처리 개선
- [x] 캐시 클리어
- [x] 회원가입 API 테스트 (성공/실패 케이스)
### Git 커밋:
- 커밋 메시지: `fix: 회원가입 메뉴 생성 오류 수정 및 검증 에러 처리 개선`
---
## 2025-11-06 (수) - Login API 응답 개선 (사용자/테넌트/메뉴 정보 포함)
### 주요 작업

View File

@@ -58,14 +58,22 @@ public function render($request, Throwable $exception)
{
if ($request->expectsJson()) {
// 400 Bad Request (예: ValidationException)
if (
$exception instanceof ValidationException ||
$exception instanceof BadRequestHttpException
) {
// 422 Unprocessable Entity - Validation 실패
if ($exception instanceof ValidationException) {
return response()->json([
'success' => false,
'message' => '필수 파라미터 누락',
'message' => '입력값 검증 실패',
'data' => [
'errors' => $exception->errors(),
],
], 422);
}
// 400 Bad Request
if ($exception instanceof BadRequestHttpException) {
return response()->json([
'success' => false,
'message' => '잘못된 요청',
'data' => null,
], 400);
}

View File

@@ -22,7 +22,7 @@ public function steps(string $recipe = 'STANDARD'): array
default => [ // STANDARD
new CapabilityProfilesStep,
new CategoriesStep,
new MenusStep,
// new MenusStep, // Disabled: Use MenuBootstrapService in RegisterService instead
new SettingsStep,
],
};

View File

@@ -49,14 +49,13 @@ public function run(int $tenantId): void
'tenant_id' => $tenantId,
'parent_id' => $newParentId,
'name' => $menu->name,
'code' => $menu->code ?? null,
'icon' => $menu->icon ?? null,
'url' => $menu->url ?? null,
'route_name' => $menu->route_name ?? null,
'sort_order' => $menu->sort_order ?? 0,
'is_active' => $menu->is_active ?? 1,
'depth' => $menu->depth ?? 0,
'description' => $menu->description ?? null,
'hidden' => $menu->hidden ?? 0,
'is_external' => $menu->is_external ?? 0,
'external_url' => $menu->external_url ?? null,
'created_at' => now(),
'updated_at' => now(),
]);