feat: 가지급금 대시보드 API 개발 (Phase 1.2)

- LoanService.dashboard() 메서드 추가 (요약 + 목록)
- LoanController.dashboard() 액션 추가
- GET /api/v1/loans/dashboard 라우트 등록
- Swagger LoanDashboard 스키마 및 엔드포인트 문서화

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-01-22 22:40:00 +09:00
parent 161b353b1c
commit 9197fe66f7
4 changed files with 132 additions and 3 deletions

View File

@@ -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);
}
/**
* 가지급금 등록
*/

View File

@@ -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,
];
}
/**
* 인정이자 리포트 (연도별 요약)
*/

View File

@@ -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",

View File

@@ -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');