From 5ac83399b363e41159dce164cdbf8d8b55c43f2c 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 23:18:33 +0900 Subject: [PATCH] =?UTF-8?q?feat(API):=20CEO=20=EB=8C=80=EC=8B=9C=EB=B3=B4?= =?UTF-8?q?=EB=93=9C=20=EC=A7=80=EC=B6=9C=EB=82=B4=EC=97=AD=20=EC=83=81?= =?UTF-8?q?=EC=84=B8=20=EC=A1=B0=ED=9A=8C=20API=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - BillService: 발행어음 대시보드 상세 조회 (me3 모달용) - WelfareService: 직원 수/급여 총액 조회 로직 개선 (salaries 테이블 기반) - BillApi/ExpectedExpenseApi: Swagger 스키마 추가 Co-Authored-By: Claude Opus 4.5 --- .../Controllers/Api/V1/BillController.php | 10 ++ app/Services/BillService.php | 112 ++++++++++++++++++ app/Services/WelfareService.php | 105 +++++++++++----- app/Swagger/v1/BillApi.php | 80 +++++++++++++ app/Swagger/v1/ExpectedExpenseApi.php | 61 ++++++++++ 5 files changed, 336 insertions(+), 32 deletions(-) diff --git a/app/Http/Controllers/Api/V1/BillController.php b/app/Http/Controllers/Api/V1/BillController.php index 0f9a831..1e9692c 100644 --- a/app/Http/Controllers/Api/V1/BillController.php +++ b/app/Http/Controllers/Api/V1/BillController.php @@ -109,4 +109,14 @@ public function summary(Request $request) return ApiResponse::success($summary, __('message.fetched')); } + + /** + * 대시보드 상세 조회 (CEO 대시보드 당월 예상 지출내역 me3 모달용) + */ + public function dashboardDetail() + { + $data = $this->service->dashboardDetail(); + + return ApiResponse::success($data, __('message.fetched')); + } } diff --git a/app/Services/BillService.php b/app/Services/BillService.php index 8fcddc9..b2d1512 100644 --- a/app/Services/BillService.php +++ b/app/Services/BillService.php @@ -328,6 +328,118 @@ public function summary(array $params): array ]; } + /** + * 발행어음 대시보드 상세 조회 (CEO 대시보드 당월 예상 지출내역 me3 모달용) + * + * @return array{ + * summary: array{current_month_total: float, previous_month_total: float, change_rate: float}, + * monthly_trend: array, + * by_vendor: array, + * items: array + * } + */ + public function dashboardDetail(): array + { + $tenantId = $this->tenantId(); + + // 현재 월 범위 + $currentMonthStart = now()->startOfMonth()->toDateString(); + $currentMonthEnd = now()->endOfMonth()->toDateString(); + + // 전월 범위 + $previousMonthStart = now()->subMonth()->startOfMonth()->toDateString(); + $previousMonthEnd = now()->subMonth()->endOfMonth()->toDateString(); + + // 1. 요약 정보 (발행어음 기준) + $currentMonthTotal = Bill::query() + ->where('tenant_id', $tenantId) + ->where('bill_type', 'issued') + ->whereBetween('issue_date', [$currentMonthStart, $currentMonthEnd]) + ->sum('amount'); + + $previousMonthTotal = Bill::query() + ->where('tenant_id', $tenantId) + ->where('bill_type', 'issued') + ->whereBetween('issue_date', [$previousMonthStart, $previousMonthEnd]) + ->sum('amount'); + + $changeRate = $previousMonthTotal > 0 + ? round((($currentMonthTotal - $previousMonthTotal) / $previousMonthTotal) * 100, 1) + : 0; + + // 2. 월별 추이 (최근 7개월) + $monthlyTrend = []; + for ($i = 6; $i >= 0; $i--) { + $monthStart = now()->subMonths($i)->startOfMonth(); + $monthEnd = now()->subMonths($i)->endOfMonth(); + + $amount = Bill::query() + ->where('tenant_id', $tenantId) + ->where('bill_type', 'issued') + ->whereBetween('issue_date', [$monthStart->toDateString(), $monthEnd->toDateString()]) + ->sum('amount'); + + $monthlyTrend[] = [ + 'month' => $monthStart->format('Y-m'), + 'amount' => (float) $amount, + ]; + } + + // 3. 거래처별 분포 (현재 월) + $byVendorRaw = Bill::query() + ->where('tenant_id', $tenantId) + ->where('bill_type', 'issued') + ->whereBetween('issue_date', [$currentMonthStart, $currentMonthEnd]) + ->select( + DB::raw('COALESCE(client_name, (SELECT name FROM clients WHERE clients.id = bills.client_id)) as vendor_name'), + DB::raw('SUM(amount) as amount') + ) + ->groupBy('client_id', 'client_name') + ->orderByDesc('amount') + ->get(); + + $totalAmount = $byVendorRaw->sum('amount'); + $byVendor = $byVendorRaw->map(function ($item) use ($totalAmount) { + return [ + 'vendor_name' => $item->vendor_name ?? '미지정', + 'amount' => (float) $item->amount, + 'ratio' => $totalAmount > 0 ? round(($item->amount / $totalAmount) * 100, 1) : 0, + ]; + })->toArray(); + + // 4. 발행어음 목록 (현재 월) + $items = Bill::query() + ->where('tenant_id', $tenantId) + ->where('bill_type', 'issued') + ->whereBetween('issue_date', [$currentMonthStart, $currentMonthEnd]) + ->with(['client:id,name']) + ->orderBy('maturity_date', 'asc') + ->get() + ->map(function ($bill) { + return [ + 'id' => $bill->id, + 'vendor_name' => $bill->client?->name ?? $bill->client_name ?? '-', + 'issue_date' => $bill->issue_date->format('Y-m-d'), + 'maturity_date' => $bill->maturity_date->format('Y-m-d'), + 'amount' => (float) $bill->amount, + 'status' => $bill->status, + 'status_label' => Bill::ISSUED_STATUSES[$bill->status] ?? $bill->status, + ]; + }) + ->toArray(); + + return [ + 'summary' => [ + 'current_month_total' => (float) $currentMonthTotal, + 'previous_month_total' => (float) $previousMonthTotal, + 'change_rate' => $changeRate, + ], + 'monthly_trend' => $monthlyTrend, + 'by_vendor' => $byVendor, + 'items' => $items, + ]; + } + /** * 어음번호 자동 생성 */ diff --git a/app/Services/WelfareService.php b/app/Services/WelfareService.php index 0ccee06..c17aa54 100644 --- a/app/Services/WelfareService.php +++ b/app/Services/WelfareService.php @@ -124,10 +124,25 @@ public function getSummary( } /** - * 직원 수 조회 + * 직원 수 조회 (급여 대상 직원 기준) + * + * salaries 테이블에서 유니크한 employee_id 수를 카운트합니다. + * 급여 데이터가 없으면 user_tenants 테이블에서 조회합니다. */ private function getEmployeeCount(int $tenantId): int { + // 1차: salaries 테이블에서 급여 대상 직원 수 조회 + $count = DB::table('salaries') + ->where('tenant_id', $tenantId) + ->whereNull('deleted_at') + ->distinct('employee_id') + ->count('employee_id'); + + if ($count > 0) { + return $count; + } + + // 2차: salaries 데이터가 없으면 user_tenants에서 조회 $count = DB::table('users') ->join('user_tenants', 'users.id', '=', 'user_tenants.user_id') ->where('user_tenants.tenant_id', $tenantId) @@ -135,33 +150,70 @@ private function getEmployeeCount(int $tenantId): int ->whereNull('users.deleted_at') ->count(); - return $count ?: 50; // 임시 기본값 + return $count ?: 0; } /** * 연간 급여 총액 조회 + * + * salaries 테이블에서 해당 연도의 base_salary 합계를 조회합니다. + * 연간 데이터가 부족한 경우, 최근 월 데이터를 12배하여 추정합니다. */ private function getTotalSalary(int $tenantId, int $year): float { - // TODO: 실제 급여 테이블에서 조회 - // payroll 또는 salary_histories에서 연간 급여 합계 - return 2000000000; // 임시 기본값 (20억) + // 해당 연도의 급여 합계 조회 + $yearlyTotal = DB::table('salaries') + ->where('tenant_id', $tenantId) + ->where('year', $year) + ->whereNull('deleted_at') + ->sum('base_salary'); + + if ($yearlyTotal > 0) { + // 데이터가 있는 월 수 확인 + $monthCount = DB::table('salaries') + ->where('tenant_id', $tenantId) + ->where('year', $year) + ->whereNull('deleted_at') + ->distinct('month') + ->count('month'); + + // 연간 추정 (데이터가 일부 월만 있을 경우 12개월로 환산) + if ($monthCount > 0 && $monthCount < 12) { + return ($yearlyTotal / $monthCount) * 12; + } + + return $yearlyTotal; + } + + // 해당 연도 데이터가 없으면 최근 월 데이터로 추정 + $latestMonth = DB::table('salaries') + ->where('tenant_id', $tenantId) + ->whereNull('deleted_at') + ->sum('base_salary'); + + $employeeCount = $this->getEmployeeCount($tenantId); + if ($latestMonth > 0 && $employeeCount > 0) { + // 최근 월 급여를 12배하여 연간 추정 + return ($latestMonth / $employeeCount) * $employeeCount * 12; + } + + return 0; } /** * 복리후생비 사용액 조회 + * + * expense_accounts 테이블에서 welfare 타입 지출액을 조회합니다. + * 데이터가 없으면 0을 반환합니다. */ private function getUsedAmount(int $tenantId, string $startDate, string $endDate): float { - // TODO: 실제 복리후생비 계정과목에서 조회 - $amount = DB::table('expense_accounts') + return DB::table('expense_accounts') ->where('tenant_id', $tenantId) ->where('account_type', 'welfare') ->whereBetween('expense_date', [$startDate, $endDate]) ->whereNull('deleted_at') - ->sum('amount'); - - return $amount ?: 5123000; // 임시 기본값 + ->sum('amount') ?: 0; } /** @@ -291,12 +343,18 @@ public function getDetail( /** * 복리후생비 계정 잔액 조회 + * + * 해당 연도의 복리후생비 계정 지출액을 조회합니다. + * 데이터가 없으면 0을 반환합니다. */ private function getAccountBalance(int $tenantId, int $year): float { - // TODO: 실제 계정 잔액 조회 로직 구현 - // 예: accounting_accounts에서 복리후생비 계정 잔액 조회 - return 3123000; // 임시 기본값 + return DB::table('expense_accounts') + ->where('tenant_id', $tenantId) + ->where('account_type', 'welfare') + ->whereYear('expense_date', $year) + ->whereNull('deleted_at') + ->sum('amount') ?: 0; } /** @@ -361,16 +419,7 @@ private function getCategoryDistribution(int $tenantId, string $startDate, strin ]; } - // 데이터가 없는 경우 기본값 반환 - if (empty($result)) { - $result = [ - ['category' => 'meal', 'label' => '식비', 'amount' => 55000000, 'ratio' => 55], - ['category' => 'health_check', 'label' => '건강검진', 'amount' => 25000000, 'ratio' => 25], - ['category' => 'congratulation', 'label' => '경조사비', 'amount' => 10000000, 'ratio' => 10], - ['category' => 'other', 'label' => '기타', 'amount' => 10000000, 'ratio' => 10], - ]; - } - + // 데이터가 없는 경우 빈 배열 반환 (mock 데이터 제거) return $result; } @@ -420,15 +469,7 @@ private function getTransactions(int $tenantId, string $startDate, string $endDa ]; } - // 데이터가 없는 경우 기본값 반환 - if (empty($result)) { - $result = [ - ['id' => 1, 'card_name' => '카드명', 'user_name' => '홍길동', 'expense_date' => '2025-12-12 12:12', 'vendor_name' => '가맹점명', 'amount' => 1000000, 'sub_type' => 'meal', 'sub_type_label' => '식비'], - ['id' => 2, 'card_name' => '카드명', 'user_name' => '홍길동', 'expense_date' => '2025-12-12 12:12', 'vendor_name' => '가맹점명', 'amount' => 1200000, 'sub_type' => 'health_check', 'sub_type_label' => '건강검진'], - ['id' => 3, 'card_name' => '카드명', 'user_name' => '홍길동', 'expense_date' => '2025-12-12 12:12', 'vendor_name' => '가맹점명', 'amount' => 1500000, 'sub_type' => 'congratulation', 'sub_type_label' => '경조사비'], - ]; - } - + // 데이터가 없는 경우 빈 배열 반환 (mock 데이터 제거) return $result; } diff --git a/app/Swagger/v1/BillApi.php b/app/Swagger/v1/BillApi.php index 5357c1b..1a7e264 100644 --- a/app/Swagger/v1/BillApi.php +++ b/app/Swagger/v1/BillApi.php @@ -164,6 +164,62 @@ * ), * @OA\Property(property="maturity_alert_amount", type="number", example=10000000, description="만기 임박 금액 (7일 이내)") * ) + * + * @OA\Schema( + * schema="BillDashboardDetail", + * description="발행어음 대시보드 상세 (CEO 대시보드 당월 예상 지출내역 me3 모달용)", + * type="object", + * + * @OA\Property( + * property="summary", + * type="object", + * description="요약 정보", + * @OA\Property(property="current_month_total", type="number", example=50000000, description="당월 발행어음 합계"), + * @OA\Property(property="previous_month_total", type="number", example=45000000, description="전월 발행어음 합계"), + * @OA\Property(property="change_rate", type="number", example=11.1, description="전월 대비 증감율 (%)") + * ), + * @OA\Property( + * property="monthly_trend", + * type="array", + * description="월별 추이 (최근 7개월)", + * + * @OA\Items( + * type="object", + * + * @OA\Property(property="month", type="string", example="2025-07", description="년-월"), + * @OA\Property(property="amount", type="number", example=42000000, description="해당 월 발행어음 합계") + * ) + * ), + * @OA\Property( + * property="by_vendor", + * type="array", + * description="거래처별 분포 (당월)", + * + * @OA\Items( + * type="object", + * + * @OA\Property(property="vendor_name", type="string", example="(주)삼성전자", description="거래처명"), + * @OA\Property(property="amount", type="number", example=25000000, description="금액"), + * @OA\Property(property="percentage", type="number", example=50.0, description="비율 (%)") + * ) + * ), + * @OA\Property( + * property="items", + * type="array", + * description="발행어음 목록 (당월)", + * + * @OA\Items( + * type="object", + * + * @OA\Property(property="id", type="integer", example=1), + * @OA\Property(property="vendor", type="string", example="(주)삼성전자", description="거래처명"), + * @OA\Property(property="issue_date", type="string", format="date", example="2025-01-05", description="발행일"), + * @OA\Property(property="due_date", type="string", format="date", example="2025-04-05", description="만기일"), + * @OA\Property(property="amount", type="number", example=10000000, description="금액"), + * @OA\Property(property="status", type="string", example="stored", description="상태") + * ) + * ) + * ) */ class BillApi { @@ -396,4 +452,28 @@ public function updateStatus() {} * ) */ public function summary() {} + + /** + * @OA\Get( + * path="/api/v1/bills/dashboard-detail", + * operationId="getBillDashboardDetail", + * tags={"Bills"}, + * summary="발행어음 대시보드 상세 조회", + * description="CEO 대시보드 당월 예상 지출내역 me3 모달용 상세 데이터를 조회합니다. 요약 정보, 월별 추이, 거래처별 분포, 발행어음 목록을 제공합니다.", + * security={{"ApiKeyAuth": {}}, {"BearerAuth": {}}}, + * + * @OA\Response( + * response=200, + * description="성공", + * + * @OA\JsonContent( + * + * @OA\Property(property="success", type="boolean", example=true), + * @OA\Property(property="message", type="string", example="데이터를 조회했습니다."), + * @OA\Property(property="data", ref="#/components/schemas/BillDashboardDetail") + * ) + * ) + * ) + */ + public function dashboardDetail() {} } diff --git a/app/Swagger/v1/ExpectedExpenseApi.php b/app/Swagger/v1/ExpectedExpenseApi.php index adfeb0e..6647bf6 100644 --- a/app/Swagger/v1/ExpectedExpenseApi.php +++ b/app/Swagger/v1/ExpectedExpenseApi.php @@ -107,6 +107,44 @@ * @OA\Property(property="by_transaction_type", type="object"), * @OA\Property(property="by_month", type="object") * ) + * + * @OA\Schema( + * schema="ExpectedExpenseDashboardDetail", + * type="object", + * description="CEO 대시보드 당월 예상 지출내역 me4 모달용 데이터", + * + * @OA\Property( + * property="summary", + * type="object", + * @OA\Property(property="current_month_total", type="number", format="float", example=5000000, description="당월 지출예상 합계"), + * @OA\Property(property="previous_month_total", type="number", format="float", example=4500000, description="전월 지출예상 합계"), + * @OA\Property(property="change_rate", type="number", format="float", example=11.1, description="전월대비 증감률(%)"), + * @OA\Property(property="pending_balance", type="number", format="float", example=3000000, description="미지급 잔액") + * ), + * @OA\Property( + * property="items", + * type="array", + * description="당월 지출예상 목록", + * + * @OA\Items( + * type="object", + * + * @OA\Property(property="id", type="integer", example=1), + * @OA\Property(property="payment_date", type="string", format="date", example="2026-01-15"), + * @OA\Property(property="item", type="string", example="월 임대료"), + * @OA\Property(property="amount", type="number", format="float", example=1500000), + * @OA\Property(property="vendor", type="string", example="(주)빌딩관리"), + * @OA\Property(property="account", type="string", example="임차료"), + * @OA\Property(property="status", type="string", example="pending") + * ) + * ), + * @OA\Property( + * property="footer_summary", + * type="object", + * @OA\Property(property="total_amount", type="number", format="float", example=5000000), + * @OA\Property(property="total_count", type="integer", example=8) + * ) + * ) */ class ExpectedExpenseApi { @@ -341,4 +379,27 @@ public function updateExpectedPaymentDate() {} * ) */ public function summary() {} + + /** + * @OA\Get( + * path="/api/v1/expected-expenses/dashboard-detail", + * summary="대시보드 상세 조회 (CEO 대시보드 당월 예상 지출내역 me4 모달용)", + * description="당월 지출예상 요약, 상세 목록을 제공합니다. me4(지출예상) 모달에서 사용됩니다.", + * tags={"ExpectedExpense"}, + * security={{"ApiKeyAuth": {}}, {"BearerAuth": {}}}, + * + * @OA\Response( + * response=200, + * description="성공", + * + * @OA\JsonContent( + * + * @OA\Property(property="success", type="boolean", example=true), + * @OA\Property(property="message", type="string"), + * @OA\Property(property="data", ref="#/components/schemas/ExpectedExpenseDashboardDetail") + * ) + * ) + * ) + */ + public function dashboardDetail() {} }