From 3988372ca41a6ae6a21900a124cbe16aa5b763b7 Mon Sep 17 00:00:00 2001 From: hskwon Date: Wed, 24 Dec 2025 19:29:15 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=ED=9C=B4=EA=B0=80=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=ED=98=84=ED=99=A9=20=EC=A0=84=EC=B2=B4=20=EC=A7=81=EC=9B=90=20?= =?UTF-8?q?=EB=AA=A9=EB=A1=9D=20API=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - GET /api/v1/leaves/balances 엔드포인트 추가 - LeaveService.getAllBalances() 메서드 구현 - TenantUserProfile 기준 전체 활성 직원 조회 - LeaveBalance 서브쿼리로 연차 정보 LEFT JOIN - 부서/검색/정렬 필터링 및 페이지네이션 지원 - User 모델에 tenantProfiles/tenantProfile 관계 추가 --- .../Controllers/Api/V1/LeaveController.php | 11 ++++ app/Models/Members/User.php | 18 ++++++ app/Services/LeaveService.php | 61 +++++++++++++++++++ routes/api.php | 1 + 4 files changed, 91 insertions(+) diff --git a/app/Http/Controllers/Api/V1/LeaveController.php b/app/Http/Controllers/Api/V1/LeaveController.php index 9ef9eaf..0a9b40f 100644 --- a/app/Http/Controllers/Api/V1/LeaveController.php +++ b/app/Http/Controllers/Api/V1/LeaveController.php @@ -105,6 +105,17 @@ public function cancel(int $id, Request $request): JsonResponse }, __('message.leave.cancelled')); } + /** + * 전체 직원 휴가 사용현황 목록 + * GET /v1/leaves/balances + */ + public function balances(Request $request): JsonResponse + { + return ApiResponse::handle(function () use ($request) { + return $this->service->getAllBalances($request->all()); + }, __('message.fetched')); + } + /** * 내 잔여 휴가 조회 * GET /v1/leaves/balance diff --git a/app/Models/Members/User.php b/app/Models/Members/User.php index 8c6b2c7..4be158f 100644 --- a/app/Models/Members/User.php +++ b/app/Models/Members/User.php @@ -4,6 +4,7 @@ use App\Models\Commons\File; use App\Models\Tenants\Tenant; +use App\Models\Tenants\TenantUserProfile; use App\Traits\ModelTrait; use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\SoftDeletes; @@ -77,4 +78,21 @@ public function tenantsMembership(): BelongsToMany ->withPivot(['is_active', 'is_default', 'joined_at', 'left_at', 'deleted_at']) ->wherePivotNull('deleted_at'); // 소프트삭제 제외 } + + /** + * 테넌트별 사용자 프로필 (전체) + */ + public function tenantProfiles() + { + return $this->hasMany(TenantUserProfile::class); + } + + /** + * 현재 테넌트의 사용자 프로필 + * 주의: 조회 시 tenant_id 조건 추가 필요 + */ + public function tenantProfile() + { + return $this->hasOne(TenantUserProfile::class); + } } diff --git a/app/Services/LeaveService.php b/app/Services/LeaveService.php index 070330c..458054b 100644 --- a/app/Services/LeaveService.php +++ b/app/Services/LeaveService.php @@ -4,6 +4,7 @@ use App\Models\Tenants\Leave; use App\Models\Tenants\LeaveBalance; +use App\Models\Tenants\TenantUserProfile; use Illuminate\Contracts\Pagination\LengthAwarePaginator; use Illuminate\Support\Facades\DB; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; @@ -307,6 +308,66 @@ public function cancel(int $id, ?string $reason = null): Leave }); } + /** + * 전체 직원 휴가 사용현황 목록 조회 + * TenantUserProfile 기준으로 전체 직원 조회 후 LeaveBalance LEFT JOIN + */ + public function getAllBalances(array $params): LengthAwarePaginator + { + $tenantId = $this->tenantId(); + $year = $params['year'] ?? now()->year; + + $query = TenantUserProfile::query() + ->where('tenant_id', $tenantId) + ->where('employee_status', 'active') + ->with([ + 'user:id,name,email', + 'department:id,name', + ]) + ->addSelect([ + 'tenant_user_profiles.*', + 'leave_balance_total' => LeaveBalance::selectRaw('total_days') + ->whereColumn('leave_balances.user_id', 'tenant_user_profiles.user_id') + ->where('leave_balances.tenant_id', $tenantId) + ->where('leave_balances.year', $year) + ->limit(1), + 'leave_balance_used' => LeaveBalance::selectRaw('used_days') + ->whereColumn('leave_balances.user_id', 'tenant_user_profiles.user_id') + ->where('leave_balances.tenant_id', $tenantId) + ->where('leave_balances.year', $year) + ->limit(1), + ]); + + // 부서 필터 + if (! empty($params['department_id'])) { + $query->where('department_id', $params['department_id']); + } + + // 검색 (사용자명) + if (! empty($params['search'])) { + $query->whereHas('user', function ($q) use ($params) { + $q->where('name', 'like', '%'.$params['search'].'%'); + }); + } + + // 정렬 + $sortBy = $params['sort_by'] ?? 'user_id'; + $sortDir = $params['sort_dir'] ?? 'asc'; + + if ($sortBy === 'user_id') { + $query->orderBy('user_id', $sortDir); + } elseif ($sortBy === 'department') { + $query->orderBy('department_id', $sortDir); + } else { + $query->orderBy($sortBy, $sortDir); + } + + // 페이지네이션 + $perPage = $params['per_page'] ?? 20; + + return $query->paginate($perPage); + } + /** * 내 잔여 휴가 조회 */ diff --git a/routes/api.php b/routes/api.php index 401c882..62eef4a 100644 --- a/routes/api.php +++ b/routes/api.php @@ -304,6 +304,7 @@ Route::prefix('leaves')->group(function () { Route::get('', [LeaveController::class, 'index'])->name('v1.leaves.index'); Route::post('', [LeaveController::class, 'store'])->name('v1.leaves.store'); + Route::get('/balances', [LeaveController::class, 'balances'])->name('v1.leaves.balances'); Route::get('/balance', [LeaveController::class, 'balance'])->name('v1.leaves.balance'); Route::get('/balance/{userId}', [LeaveController::class, 'userBalance'])->name('v1.leaves.userBalance'); Route::put('/balance', [LeaveController::class, 'setBalance'])->name('v1.leaves.setBalance');