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:
240
CURRENT_WORKS.md
240
CURRENT_WORKS.md
@@ -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 응답 개선 (사용자/테넌트/메뉴 정보 포함)
|
||||
|
||||
### 주요 작업
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
],
|
||||
};
|
||||
|
||||
@@ -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(),
|
||||
]);
|
||||
|
||||
Reference in New Issue
Block a user