diff --git a/CURRENT_WORKS.md b/CURRENT_WORKS.md index e7a0141..3696d42 100644 --- a/CURRENT_WORKS.md +++ b/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 응답 개선 (사용자/테넌트/메뉴 정보 포함) ### 주요 작업 diff --git a/app/Exceptions/Handler.php b/app/Exceptions/Handler.php index 0e34db7..f19b124 100644 --- a/app/Exceptions/Handler.php +++ b/app/Exceptions/Handler.php @@ -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); } diff --git a/app/Services/TenantBootstrap/RecipeRegistry.php b/app/Services/TenantBootstrap/RecipeRegistry.php index a007213..9ca16eb 100644 --- a/app/Services/TenantBootstrap/RecipeRegistry.php +++ b/app/Services/TenantBootstrap/RecipeRegistry.php @@ -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, ], }; diff --git a/app/Services/TenantBootstrap/Steps/MenusStep.php b/app/Services/TenantBootstrap/Steps/MenusStep.php index 3c3e851..235f748 100644 --- a/app/Services/TenantBootstrap/Steps/MenusStep.php +++ b/app/Services/TenantBootstrap/Steps/MenusStep.php @@ -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(), ]);