fix : Base Service 추가 및 다국어 설정, DepartmentService 서비스를 static에서 인스턴스로 변경
- 모든 서비스를 인스턴스구조로 변경예정 * 규모가 커질수록 → 인스턴스(=DI) 설계가 유리 * 작고 단순한 유틸/순수 함수만 스태틱으로 유지 * DI/모킹/테스트 쉬움 (서비스 교체·부분 테스트 가능) * 의존성 교체 쉬움 (Repo/캐시/로거…) * 상태·컨텍스트 주입 명확 (테넌트/유저/트랜잭션)
This commit is contained in:
@@ -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(),
|
||||
]);
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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)
|
||||
|
||||
37
app/Services/Service.php
Normal file
37
app/Services/Service.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
namespace App\Services;
|
||||
|
||||
use Illuminate\Auth\AuthenticationException;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
|
||||
abstract class Service
|
||||
{
|
||||
/** 활성 테넌트 ID(없으면 null) */
|
||||
protected function tenantIdOrNull(): ?int
|
||||
{
|
||||
$id = app('tenant_id');
|
||||
return $id ? (int) $id : null;
|
||||
}
|
||||
|
||||
/** 활성 테넌트 ID(없으면 400 Bad Request + i18n 메시지로 예외) */
|
||||
protected function tenantId(): int
|
||||
{
|
||||
$id = $this->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;
|
||||
}
|
||||
}
|
||||
7
lang/ko/error.php
Normal file
7
lang/ko/error.php
Normal file
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
return [
|
||||
'tenant_id' => '활성 테넌트 없음',
|
||||
];
|
||||
@@ -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'); // 내 프로필 수정
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user