diff --git a/app/Helpers/ApiResponse.php b/app/Helpers/ApiResponse.php index 0b35274..b809809 100644 --- a/app/Helpers/ApiResponse.php +++ b/app/Helpers/ApiResponse.php @@ -4,6 +4,7 @@ use Illuminate\Support\Facades\DB; use Illuminate\Http\JsonResponse; +use Symfony\Component\HttpKernel\Exception\HttpException; class ApiResponse { @@ -159,6 +160,16 @@ public static function handle( return self::success($data, $responseTitle.' 성공', $debug); } catch (\Throwable $e) { + + // HttpException 계열은 상태코드/메시지를 그대로 반영 + if ($e instanceof HttpException) { + return self::error( + $e->getMessage() ?: ($responseTitle.' 실패'), + $e->getStatusCode(), + ['details' => config('app.debug') ? $e->getTraceAsString() : null] + ); + } + return self::error($responseTitle.' 실패', 500, [ 'details' => $e->getMessage(), ]); diff --git a/app/Http/Controllers/Api/V1/DepartmentController.php b/app/Http/Controllers/Api/V1/DepartmentController.php index d50d3d3..c761b14 100644 --- a/app/Http/Controllers/Api/V1/DepartmentController.php +++ b/app/Http/Controllers/Api/V1/DepartmentController.php @@ -26,11 +26,12 @@ public function store(Request $request) } // GET /v1/departments/{id} - public function show($id, Request $request) + public function show(int $id, Request $request, DepartmentService $service) { - return ApiResponse::handle(function () use ($id, $request) { - return DepartmentService::show((int)$id, $request->all()); - }, '부서 단건 조회'); + return \App\Helpers\ApiResponse::handle( + fn() => $service->show($id, $request->all()), + '부서 단건 조회' + ); } // PATCH /v1/departments/{id} diff --git a/app/Services/DepartmentService.php b/app/Services/DepartmentService.php index fe52e88..d8522f8 100644 --- a/app/Services/DepartmentService.php +++ b/app/Services/DepartmentService.php @@ -10,12 +10,12 @@ use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Validator; -class DepartmentService +class DepartmentService extends Service { /** - * 공통 검증 헬퍼: 실패 시 JsonResponse 반환 + * 공통 검증 헬퍼: 실패 시 ['error'=>..., 'code'=>...] 형태로 반환 */ - private static function v(array $params, array $rules) + protected function v(array $params, array $rules) { $v = Validator::make($params, $rules); if ($v->fails()) { @@ -25,15 +25,16 @@ private static function v(array $params, array $rules) } /** 목록 */ - public static function index(array $params) + public function index(array $params) { - $p = self::v($params, [ + $p = $this->v($params, [ 'q' => 'nullable|string|max:100', 'is_active' => 'nullable|in:0,1', 'page' => 'nullable|integer|min:1', 'per_page' => 'nullable|integer|min:1|max:200', ]); if ($p instanceof JsonResponse) return $p; + if (isset($p['error'])) return $p; $q = Department::query(); @@ -52,14 +53,16 @@ public static function index(array $params) $perPage = $p['per_page'] ?? 20; $page = $p['page'] ?? null; - // 페이징 객체는 'result'로 반환 return $q->paginate($perPage, ['*'], 'page', $page); } /** 생성 */ - public static function store(array $params) + public function store(array $params) { - $p = self::v($params, [ + // 테넌트 강제가 필요하면 아래 라인 사용: + // $this->tenantIdOrFail(); + + $p = $this->v($params, [ 'code' => 'nullable|string|max:50', 'name' => 'required|string|max:100', 'description' => 'nullable|string|max:255', @@ -68,6 +71,7 @@ public static function store(array $params) 'created_by' => 'nullable|integer|min:1', ]); if ($p instanceof JsonResponse) return $p; + if (isset($p['error'])) return $p; if (!empty($p['code'])) { $exists = Department::query()->where('code', $p['code'])->exists(); @@ -88,23 +92,23 @@ public static function store(array $params) } /** 단건 */ - public static function show(int $id, array $params) + public function show(int $id, array $params = []) { if (!$id) return ['error' => 'id가 필요합니다.', 'code' => 400]; - $res = Department::query()->where('id', $id)->first(); - if (empty($res['data'])) { + $res = Department::query()->find($id); + if (!$res) { return ['error' => '부서를 찾을 수 없습니다.', 'code' => 404]; } return $res; } /** 수정 */ - public static function update(int $id, array $params) + public function update(int $id, array $params) { if (!$id) return ['error' => 'id가 필요합니다.', 'code' => 400]; - $p = self::v($params, [ + $p = $this->v($params, [ 'code' => 'nullable|string|max:50', 'name' => 'nullable|string|max:100', 'description' => 'nullable|string|max:255', @@ -113,6 +117,7 @@ public static function update(int $id, array $params) 'updated_by' => 'nullable|integer|min:1', ]); if ($p instanceof JsonResponse) return $p; + if (isset($p['error'])) return $p; $dept = Department::query()->find($id); if (!$dept) return ['error' => '부서를 찾을 수 없습니다.', 'code' => 404]; @@ -138,14 +143,15 @@ public static function update(int $id, array $params) } /** 삭제(soft) */ - public static function destroy(int $id, array $params) + public function destroy(int $id, array $params) { if (!$id) return ['error' => 'id가 필요합니다.', 'code' => 400]; - $p = self::v($params, [ + $p = $this->v($params, [ 'deleted_by' => 'nullable|integer|min:1', ]); if ($p instanceof JsonResponse) return $p; + if (isset($p['error'])) return $p; $dept = Department::query()->find($id); if (!$dept) return ['error' => '부서를 찾을 수 없습니다.', 'code' => 404]; @@ -160,13 +166,14 @@ public static function destroy(int $id, array $params) } /** 부서 사용자 목록 */ - public static function listUsers(int $deptId, array $params) + public function listUsers(int $deptId, array $params) { - $p = self::v($params, [ + $p = $this->v($params, [ 'page' => 'nullable|integer|min:1', 'per_page' => 'nullable|integer|min:1|max:200', ]); if ($p instanceof JsonResponse) return $p; + if (isset($p['error'])) return $p; $dept = Department::query()->find($deptId); if (!$dept) return ['error' => '부서를 찾을 수 없습니다.', 'code' => 404]; @@ -181,14 +188,15 @@ public static function listUsers(int $deptId, array $params) } /** 사용자 배정 (단건) */ - public static function attachUser(int $deptId, array $params) + public function attachUser(int $deptId, array $params) { - $p = self::v($params, [ + $p = $this->v($params, [ 'user_id' => 'required|integer|min:1', 'is_primary' => 'nullable|in:0,1', 'joined_at' => 'nullable|date', ]); if ($p instanceof JsonResponse) return $p; + if (isset($p['error'])) return $p; $dept = Department::query()->find($deptId); if (!$dept) return ['error' => '부서를 찾을 수 없습니다.', 'code' => 404]; @@ -227,14 +235,12 @@ public static function attachUser(int $deptId, array $params) return ['department_id' => $dept->id, 'user_id' => $p['user_id']]; }); - // 트랜잭션 내부에서 에러 응답이 나올 수 있으므로 분기 if ($result instanceof JsonResponse) return $result; - return $result; } /** 사용자 제거(soft) */ - public static function detachUser(int $deptId, int $userId, array $params) + public function detachUser(int $deptId, int $userId, array $params) { $du = DepartmentUser::whereNull('deleted_at') ->where('department_id', $deptId) @@ -252,12 +258,13 @@ public static function detachUser(int $deptId, int $userId, array $params) } /** 주부서 설정/해제 */ - public static function setPrimary(int $deptId, int $userId, array $params) + public function setPrimary(int $deptId, int $userId, array $params) { - $p = self::v($params, [ + $p = $this->v($params, [ 'is_primary' => 'required|in:0,1', ]); if ($p instanceof JsonResponse) return $p; + if (isset($p['error'])) return $p; $result = DB::transaction(function () use ($deptId, $userId, $p) { $du = DepartmentUser::whereNull('deleted_at') @@ -282,20 +289,20 @@ public static function setPrimary(int $deptId, int $userId, array $params) }); if ($result instanceof JsonResponse) return $result; - return $result; } /** 부서 권한 목록 */ - public static function listPermissions(int $deptId, array $params) + public function listPermissions(int $deptId, array $params) { - $p = self::v($params, [ + $p = $this->v($params, [ 'menu_id' => 'nullable|integer|min:1', 'is_allowed' => 'nullable|in:0,1', 'page' => 'nullable|integer|min:1', 'per_page' => 'nullable|integer|min:1|max:200', ]); if ($p instanceof JsonResponse) return $p; + if (isset($p['error'])) return $p; $dept = Department::query()->find($deptId); if (!$dept) return ['error' => '부서를 찾을 수 없습니다.', 'code' => 404]; @@ -316,14 +323,15 @@ public static function listPermissions(int $deptId, array $params) } /** 권한 부여/차단 upsert */ - public static function upsertPermission(int $deptId, array $params) + public function upsertPermission(int $deptId, array $params) { - $p = self::v($params, [ + $p = $this->v($params, [ 'permission_id' => 'required|integer|min:1', 'menu_id' => 'nullable|integer|min:1', 'is_allowed' => 'nullable|in:0,1', ]); if ($p instanceof JsonResponse) return $p; + if (isset($p['error'])) return $p; $dept = Department::query()->find($deptId); if (!$dept) return ['error' => '부서를 찾을 수 없습니다.', 'code' => 404]; @@ -340,16 +348,17 @@ public static function upsertPermission(int $deptId, array $params) $model->save(); // 변경 후 목록 반환 - return self::listPermissions($deptId, []); + return $this->listPermissions($deptId, []); } /** 권한 제거 (menu_id 없으면 전체 제거) */ - public static function revokePermission(int $deptId, int $permissionId, array $params) + public function revokePermission(int $deptId, int $permissionId, array $params) { - $p = self::v($params, [ + $p = $this->v($params, [ 'menu_id' => 'nullable|integer|min:1', ]); if ($p instanceof JsonResponse) return $p; + if (isset($p['error'])) return $p; $q = DepartmentPermission::whereNull('deleted_at') ->where('department_id', $deptId) diff --git a/app/Services/Service.php b/app/Services/Service.php new file mode 100644 index 0000000..49e19ca --- /dev/null +++ b/app/Services/Service.php @@ -0,0 +1,37 @@ +tenantIdOrNull(); + if (!$id) { + // ko/error.php 의 'tenant_id' 키 사용 + throw new BadRequestHttpException(__('error.tenant_id')); + } + return $id; + } + + /** (선택) API 사용자 ID 필요할 때 401로 던지고 싶다면 */ + protected function apiUserIdOrFail(): int + { + $uid = app('api_user'); + if (!$uid) { + // Handler에서 AuthenticationException은 401로 처리 중 + throw new AuthenticationException(__('auth.unauthenticated')); + } + return (int) $uid; + } +} diff --git a/lang/ko/error.php b/lang/ko/error.php new file mode 100644 index 0000000..8688894 --- /dev/null +++ b/lang/ko/error.php @@ -0,0 +1,7 @@ + '활성 테넌트 없음', +]; diff --git a/routes/api.php b/routes/api.php index 9b1ff81..57628da 100644 --- a/routes/api.php +++ b/routes/api.php @@ -182,34 +182,33 @@ // 테넌트 필드 설정 Route::prefix('fields')->group(function () { - Route::get ('', [TenantFieldSettingController::class, 'index']); // 목록(효과값) - Route::put ('/bulk', [TenantFieldSettingController::class, 'bulkUpsert']); // 대량 저장 - Route::patch ('/{key}', [TenantFieldSettingController::class, 'updateOne']); // 단건 수정 + Route::get ('', [TenantFieldSettingController::class, 'index'])->name('v1.fields.index'); // 필드 설정 목록(전역+테넌트 병합 효과값) + Route::put ('/bulk', [TenantFieldSettingController::class, 'bulkUpsert'])->name('v1.fields.bulk'); // 필드 설정 대량 저장(트랜잭션 처리) + Route::patch ('/{key}', [TenantFieldSettingController::class, 'updateOne'])->name('v1.fields.update'); // 필드 설정 단건 수정/업데이트 }); // 옵션 그룹/값 Route::prefix('opt-groups')->group(function () { - Route::get ('', [TenantOptionGroupController::class, 'index']); - Route::post ('', [TenantOptionGroupController::class, 'store']); - Route::get ('/{id}', [TenantOptionGroupController::class, 'show']); - Route::patch ('/{id}', [TenantOptionGroupController::class, 'update']); - Route::delete('/{id}', [TenantOptionGroupController::class, 'destroy']); - - Route::get ('/{gid}/values', [TenantOptionValueController::class, 'index']); - Route::post ('/{gid}/values', [TenantOptionValueController::class, 'store']); - Route::get ('/{gid}/values/{id}', [TenantOptionValueController::class, 'show']); - Route::patch ('/{gid}/values/{id}', [TenantOptionValueController::class, 'update']); - Route::delete ('/{gid}/values/{id}', [TenantOptionValueController::class, 'destroy']); - Route::patch ('/{gid}/values/reorder', [TenantOptionValueController::class, 'reorder']); // [{id,sort_order}] + Route::get ('', [TenantOptionGroupController::class, 'index'])->name('v1.opt-groups.index'); // 옵션 그룹 목록 + Route::post ('', [TenantOptionGroupController::class, 'store'])->name('v1.opt-groups.store'); // 옵션 그룹 생성 + Route::get ('/{id}', [TenantOptionGroupController::class, 'show'])->name('v1.opt-groups.show'); // 옵션 그룹 단건 조회 + Route::patch ('/{id}', [TenantOptionGroupController::class, 'update'])->name('v1.opt-groups.update'); // 옵션 그룹 수정 + Route::delete('/{id}', [TenantOptionGroupController::class, 'destroy'])->name('v1.opt-groups.destroy'); // 옵션 그룹 삭제 + Route::get ('/{gid}/values', [TenantOptionValueController::class, 'index'])->name('v1.opt-groups.values.index'); // 옵션 값 목록 + Route::post ('/{gid}/values', [TenantOptionValueController::class, 'store'])->name('v1.opt-groups.values.store'); // 옵션 값 생성 + Route::get ('/{gid}/values/{id}', [TenantOptionValueController::class, 'show'])->name('v1.opt-groups.values.show'); // 옵션 값 단건 조회 + Route::patch ('/{gid}/values/{id}', [TenantOptionValueController::class, 'update'])->name('v1.opt-groups.values.update'); // 옵션 값 수정 + Route::delete('/{gid}/values/{id}', [TenantOptionValueController::class, 'destroy'])->name('v1.opt-groups.values.destroy'); // 옵션 값 삭제 + Route::patch ('/{gid}/values/reorder', [TenantOptionValueController::class, 'reorder'])->name('v1.opt-groups.values.reorder'); // 옵션 값 정렬순서 재배치 }); // 회원 프로필(테넌트 기준) Route::prefix('profiles')->group(function () { - Route::get ('', [TenantUserProfileController::class, 'index']); // 목록 - Route::get ('/{userId}', [TenantUserProfileController::class, 'show']); // 단건 - Route::patch ('/{userId}', [TenantUserProfileController::class, 'update']); // 수정(관리자) - Route::get ('/me', [TenantUserProfileController::class, 'me']); // 내 프로필 - Route::patch ('/me', [TenantUserProfileController::class, 'updateMe']); // 내 정보 수정 + Route::get ('', [TenantUserProfileController::class, 'index'])->name('v1.profiles.index'); // 프로필 목록(테넌트 기준) + Route::get ('/{userId}', [TenantUserProfileController::class, 'show'])->name('v1.profiles.show'); // 특정 사용자 프로필 조회 + Route::patch ('/{userId}', [TenantUserProfileController::class, 'update'])->name('v1.profiles.update'); // 특정 사용자 프로필 수정(관리자) + Route::get ('/me', [TenantUserProfileController::class, 'me'])->name('v1.profiles.me'); // 내 프로필 조회 + Route::patch ('/me', [TenantUserProfileController::class, 'updateMe'])->name('v1.profiles.me.update'); // 내 프로필 수정 }); });