From 9197fe66f71c96c3c6145830b4185f0ccf12a8c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B6=8C=ED=98=81=EC=84=B1?= Date: Thu, 22 Jan 2026 22:40:00 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EA=B0=80=EC=A7=80=EA=B8=89=EA=B8=88=20?= =?UTF-8?q?=EB=8C=80=EC=8B=9C=EB=B3=B4=EB=93=9C=20API=20=EA=B0=9C=EB=B0=9C?= =?UTF-8?q?=20(Phase=201.2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - LoanService.dashboard() 메서드 추가 (요약 + 목록) - LoanController.dashboard() 액션 추가 - GET /api/v1/loans/dashboard 라우트 등록 - Swagger LoanDashboard 스키마 및 엔드포인트 문서화 Co-Authored-By: Claude --- .../Controllers/Api/V1/LoanController.php | 10 ++++ app/Services/LoanService.php | 60 +++++++++++++++++++ app/Swagger/v1/LoanApi.php | 57 ++++++++++++++++++ routes/api.php | 8 ++- 4 files changed, 132 insertions(+), 3 deletions(-) diff --git a/app/Http/Controllers/Api/V1/LoanController.php b/app/Http/Controllers/Api/V1/LoanController.php index 9ef4889..19d56c0 100644 --- a/app/Http/Controllers/Api/V1/LoanController.php +++ b/app/Http/Controllers/Api/V1/LoanController.php @@ -39,6 +39,16 @@ public function summary(LoanIndexRequest $request): JsonResponse return ApiResponse::handle('message.fetched', $result); } + /** + * 가지급금 대시보드 + */ + public function dashboard(): JsonResponse + { + $result = $this->loanService->dashboard(); + + return ApiResponse::handle('message.fetched', $result); + } + /** * 가지급금 등록 */ diff --git a/app/Services/LoanService.php b/app/Services/LoanService.php index 98d052d..bf6a009 100644 --- a/app/Services/LoanService.php +++ b/app/Services/LoanService.php @@ -362,6 +362,66 @@ public function calculateInterest(int $year, ?int $userId = null): array ]; } + /** + * 가지급금 대시보드 데이터 + * + * CEO 대시보드 카드/가지급금 관리 섹션(cm2) 모달용 데이터 제공 + * + * @return array{ + * summary: array{ + * total_outstanding: float, + * recognized_interest: float, + * outstanding_count: int + * }, + * loans: array + * } + */ + public function dashboard(): array + { + $tenantId = $this->tenantId(); + $currentYear = now()->year; + + // 1. Summary 데이터 + $summaryData = $this->summary(); + + // 2. 인정이자 계산 (현재 연도 기준) + $interestData = $this->calculateInterest($currentYear); + $recognizedInterest = $interestData['summary']['total_recognized_interest'] ?? 0; + + // 3. 가지급금 목록 (최근 10건, 미정산 우선) + $loans = Loan::query() + ->where('tenant_id', $tenantId) + ->with(['user:id,name,email', 'withdrawal']) + ->orderByRaw('CASE WHEN status = ? THEN 0 WHEN status = ? THEN 1 ELSE 2 END', [ + Loan::STATUS_OUTSTANDING, + Loan::STATUS_PARTIAL, + ]) + ->orderByDesc('loan_date') + ->limit(10) + ->get() + ->map(function ($loan) { + return [ + 'id' => $loan->id, + 'loan_date' => $loan->loan_date->format('Y-m-d'), + 'user_name' => $loan->user?->name ?? '미지정', + 'category' => $loan->withdrawal_id ? '카드' : '계좌', + 'amount' => (float) $loan->amount, + 'status' => $loan->status, + 'content' => $loan->purpose ?? '', + ]; + }) + ->toArray(); + + return [ + 'summary' => [ + 'total_outstanding' => (float) $summaryData['total_outstanding'], + 'recognized_interest' => (float) $recognizedInterest, + 'outstanding_count' => (int) $summaryData['outstanding_count'], + ], + 'loans' => $loans, + ]; + } + /** * 인정이자 리포트 (연도별 요약) */ diff --git a/app/Swagger/v1/LoanApi.php b/app/Swagger/v1/LoanApi.php index ac5f33b..6800bc3 100644 --- a/app/Swagger/v1/LoanApi.php +++ b/app/Swagger/v1/LoanApi.php @@ -171,6 +171,33 @@ * description="전체 합계" * ) * ) + * + * @OA\Schema( + * schema="LoanDashboard", + * type="object", + * description="가지급금 대시보드 응답", + * + * @OA\Property(property="summary", type="object", + * @OA\Property(property="total_outstanding", type="number", format="float", example=20000000, description="미정산 가지급금 총액"), + * @OA\Property(property="recognized_interest", type="number", format="float", example=920000, description="인정이자 (연 4.6%)"), + * @OA\Property(property="outstanding_count", type="integer", example=5, description="미정산 건수"), + * description="요약 정보" + * ), + * @OA\Property(property="loans", type="array", + * + * @OA\Items( + * + * @OA\Property(property="id", type="integer", example=1, description="가지급금 ID"), + * @OA\Property(property="loan_date", type="string", format="date", example="2025-01-15", description="지급일"), + * @OA\Property(property="user_name", type="string", example="홍길동", description="수령자명"), + * @OA\Property(property="category", type="string", example="카드", enum={"카드","계좌"}, description="구분"), + * @OA\Property(property="amount", type="number", format="float", example=5000000, description="금액"), + * @OA\Property(property="status", type="string", example="outstanding", enum={"outstanding","settled","partial"}, description="상태"), + * @OA\Property(property="content", type="string", example="출장 경비", description="내용/목적") + * ), + * description="최근 가지급금 목록 (미정산 우선, 최대 10건)" + * ) + * ) */ class LoanApi { @@ -290,6 +317,36 @@ public function store() {} */ public function summary() {} + /** + * @OA\Get( + * path="/api/v1/loans/dashboard", + * tags={"Loans"}, + * summary="가지급금 대시보드 조회", + * description="CEO 대시보드 카드/가지급금 관리 섹션(cm2) 모달용 데이터를 조회합니다. 요약 정보와 최근 가지급금 목록을 반환합니다.", + * security={{"ApiKeyAuth":{}},{"BearerAuth":{}}}, + * + * @OA\Response( + * response=200, + * description="조회 성공", + * + * @OA\JsonContent( + * allOf={ + * + * @OA\Schema(ref="#/components/schemas/ApiResponse"), + * @OA\Schema( + * + * @OA\Property(property="data", ref="#/components/schemas/LoanDashboard") + * ) + * } + * ) + * ), + * + * @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), + * @OA\Response(response=500, description="서버 에러", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")) + * ) + */ + public function dashboard() {} + /** * @OA\Post( * path="/api/v1/loans/calculate-interest", diff --git a/routes/api.php b/routes/api.php index d6cd86c..8a2b749 100644 --- a/routes/api.php +++ b/routes/api.php @@ -98,19 +98,19 @@ use App\Http\Controllers\Api\V1\SiteController; use App\Http\Controllers\Api\V1\StatusBoardController; use App\Http\Controllers\Api\V1\StockController; -use App\Http\Controllers\Api\V1\TodayIssueController; use App\Http\Controllers\Api\V1\SubscriptionController; use App\Http\Controllers\Api\V1\SystemBoardController; use App\Http\Controllers\Api\V1\SystemPostController; -// 설계 전용 (디자인 네임스페이스) use App\Http\Controllers\Api\V1\TaxInvoiceController; +// 설계 전용 (디자인 네임스페이스) use App\Http\Controllers\Api\V1\TenantController; use App\Http\Controllers\Api\V1\TenantFieldSettingController; use App\Http\Controllers\Api\V1\TenantOptionGroupController; use App\Http\Controllers\Api\V1\TenantOptionValueController; -// 모델셋 관리 (견적 시스템) use App\Http\Controllers\Api\V1\TenantStatFieldController; +// 모델셋 관리 (견적 시스템) use App\Http\Controllers\Api\V1\TenantUserProfileController; +use App\Http\Controllers\Api\V1\TodayIssueController; use App\Http\Controllers\Api\V1\UserController; use App\Http\Controllers\Api\V1\UserInvitationController; use App\Http\Controllers\Api\V1\UserRoleController; @@ -575,6 +575,7 @@ Route::get('', [LoanController::class, 'index'])->name('v1.loans.index'); Route::post('', [LoanController::class, 'store'])->name('v1.loans.store'); Route::get('/summary', [LoanController::class, 'summary'])->name('v1.loans.summary'); + Route::get('/dashboard', [LoanController::class, 'dashboard'])->name('v1.loans.dashboard'); Route::post('/calculate-interest', [LoanController::class, 'calculateInterest'])->name('v1.loans.calculate-interest'); Route::get('/interest-report/{year}', [LoanController::class, 'interestReport'])->whereNumber('year')->name('v1.loans.interest-report'); Route::get('/{id}', [LoanController::class, 'show'])->whereNumber('id')->name('v1.loans.show'); @@ -807,6 +808,7 @@ Route::get('', [BillController::class, 'index'])->name('v1.bills.index'); Route::post('', [BillController::class, 'store'])->name('v1.bills.store'); Route::get('/summary', [BillController::class, 'summary'])->name('v1.bills.summary'); + Route::get('/dashboard-detail', [BillController::class, 'dashboardDetail'])->name('v1.bills.dashboard-detail'); Route::get('/{id}', [BillController::class, 'show'])->whereNumber('id')->name('v1.bills.show'); Route::put('/{id}', [BillController::class, 'update'])->whereNumber('id')->name('v1.bills.update'); Route::delete('/{id}', [BillController::class, 'destroy'])->whereNumber('id')->name('v1.bills.destroy');