From 561a4745e090058066dbdc883bc6066d1c150610 Mon Sep 17 00:00:00 2001 From: kent Date: Fri, 2 Jan 2026 11:15:05 +0900 Subject: [PATCH] =?UTF-8?q?feat(API):=20=EC=A2=85=ED=95=A9=EB=B6=84?= =?UTF-8?q?=EC=84=9D=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EB=8D=94=EB=AF=B8=20?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 당월 예상 지출: 매입/카드/발행어음 라벨로 변경, 전월 대비 증감률 표시 - 접대비 현황: 연간 한도 4,000만원, 더미 사용량 데이터 - 복리후생비 현황: 인당 20만원 기준, 더미 사용량 데이터 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- app/Services/ComprehensiveAnalysisService.php | 217 ++++++++++++++---- 1 file changed, 166 insertions(+), 51 deletions(-) diff --git a/app/Services/ComprehensiveAnalysisService.php b/app/Services/ComprehensiveAnalysisService.php index 0718f16..e6a275e 100644 --- a/app/Services/ComprehensiveAnalysisService.php +++ b/app/Services/ComprehensiveAnalysisService.php @@ -86,7 +86,7 @@ protected function getMonthlyExpense(int $year, int $month): array { $tenantId = $this->tenantId(); - // 이번 달 예상 지출 합계 + // 이번 달 예상 지출 합계 (유형별) $expenses = ExpectedExpense::where('tenant_id', $tenantId) ->whereYear('expected_payment_date', $year) ->whereMonth('expected_payment_date', $month) @@ -95,47 +95,105 @@ protected function getMonthlyExpense(int $year, int $month): array ->get() ->keyBy('transaction_type'); - // 미지급 금액 - $unpaidTotal = ExpectedExpense::where('tenant_id', $tenantId) - ->whereYear('expected_payment_date', $year) - ->whereMonth('expected_payment_date', $month) - ->where('payment_status', 'pending') - ->sum('amount'); + // 전월 데이터 + $lastMonth = $month === 1 ? 12 : $month - 1; + $lastYear = $month === 1 ? $year - 1 : $year; + $lastMonthExpenses = ExpectedExpense::where('tenant_id', $tenantId) + ->whereYear('expected_payment_date', $lastYear) + ->whereMonth('expected_payment_date', $lastMonth) + ->selectRaw('transaction_type, SUM(amount) as total') + ->groupBy('transaction_type') + ->get() + ->keyBy('transaction_type'); - // 지급 완료 금액 - $paidTotal = ExpectedExpense::where('tenant_id', $tenantId) - ->whereYear('expected_payment_date', $year) - ->whereMonth('expected_payment_date', $month) - ->where('payment_status', 'paid') - ->sum('amount'); + // 실제 데이터 추출 + $purchaseTotal = (float) ($expenses['purchase']->total ?? 0); + $cardTotal = (float) ($expenses['card']->total ?? 0); + $billTotal = (float) ($expenses['bill']->total ?? 0); - // 연체 금액 - $overdueTotal = ExpectedExpense::where('tenant_id', $tenantId) - ->where('expected_payment_date', '<', Carbon::today()) - ->where('payment_status', 'pending') - ->sum('amount'); + $lastPurchase = (float) ($lastMonthExpenses['purchase']->total ?? 0); + $lastCard = (float) ($lastMonthExpenses['card']->total ?? 0); + $lastBill = (float) ($lastMonthExpenses['bill']->total ?? 0); + + // 데이터가 없으면 더미 데이터 사용 + $hasData = $purchaseTotal > 0 || $cardTotal > 0 || $billTotal > 0; + + if (! $hasData) { + $purchaseTotal = 45000000; // 매입 4,500만원 + $cardTotal = 8500000; // 카드 850만원 + $billTotal = 12000000; // 발행어음 1,200만원 + $lastPurchase = 42000000; // 전월 매입 4,200만원 + $lastCard = 7800000; // 전월 카드 780만원 + $lastBill = 11500000; // 전월 발행어음 1,150만원 + } + + $totalExpense = $purchaseTotal + $cardTotal + $billTotal; + $lastTotalExpense = $lastPurchase + $lastCard + $lastBill; + + // 전월 대비 증감률 계산 + $calculateChange = function ($current, $previous) { + if ($previous == 0) { + return $current > 0 ? '+100%' : '0%'; + } + $change = (($current - $previous) / $previous) * 100; + + return ($change >= 0 ? '+' : '').number_format($change, 1).'%'; + }; $cards = [ - ['id' => 'expense-1', 'label' => '이번 달 예상 지출', 'amount' => (float) ($unpaidTotal + $paidTotal)], - ['id' => 'expense-2', 'label' => '미지급 금액', 'amount' => (float) $unpaidTotal], - ['id' => 'expense-3', 'label' => '지급 완료', 'amount' => (float) $paidTotal], - ['id' => 'expense-4', 'label' => '연체 금액', 'amount' => (float) $overdueTotal], + [ + 'id' => 'expense-1', + 'label' => '매입', + 'amount' => $purchaseTotal, + 'previous_amount' => $lastPurchase, + 'previous_label' => '전월 대비 '.$calculateChange($purchaseTotal, $lastPurchase), + ], + [ + 'id' => 'expense-2', + 'label' => '카드', + 'amount' => $cardTotal, + 'previous_amount' => $lastCard, + 'previous_label' => '전월 대비 '.$calculateChange($cardTotal, $lastCard), + ], + [ + 'id' => 'expense-3', + 'label' => '발행어음', + 'amount' => $billTotal, + 'previous_amount' => $lastBill, + 'previous_label' => '전월 대비 '.$calculateChange($billTotal, $lastBill), + ], + [ + 'id' => 'expense-4', + 'label' => '총 예상 지출 합계', + 'amount' => $totalExpense, + 'previous_amount' => $lastTotalExpense, + 'previous_label' => '전월 대비 '.$calculateChange($totalExpense, $lastTotalExpense), + ], ]; $checkPoints = []; - if ($overdueTotal > 0) { + $changeRate = $lastTotalExpense > 0 ? (($totalExpense - $lastTotalExpense) / $lastTotalExpense) * 100 : 0; + + if ($changeRate > 15) { $checkPoints[] = [ 'id' => 'expense-cp-1', 'type' => 'warning', - 'message' => '연체 중인 지출이 있습니다.', - 'highlight' => number_format($overdueTotal).'원', + 'message' => '이번 달 예상 지출이', + 'highlight' => '전월 대비 '.number_format($changeRate, 0).'% 증가했습니다. 매입 비용 증가가 주요 원인입니다.', ]; - } - if ($unpaidTotal > $paidTotal * 2) { + } elseif ($changeRate < -5) { $checkPoints[] = [ - 'id' => 'expense-cp-2', + 'id' => 'expense-cp-1', + 'type' => 'success', + 'message' => '이번 달 예상 지출이', + 'highlight' => '전월 대비 '.number_format(abs($changeRate), 0).'% 감소했습니다.', + ]; + } else { + $checkPoints[] = [ + 'id' => 'expense-cp-1', 'type' => 'info', - 'message' => '미지급 금액이 지급 완료 금액보다 많습니다.', + 'message' => '이번 달 예상 지출이', + 'highlight' => '전월과 비슷한 수준입니다.', ]; } @@ -215,7 +273,6 @@ protected function getEntertainment(int $year, int $month): array $tenantId = $this->tenantId(); // 실제로는 expense_category 등으로 접대비 분류 필요 - // 현재는 기본값 반환 $monthlyTotal = ExpectedExpense::where('tenant_id', $tenantId) ->whereYear('expected_payment_date', $year) ->whereMonth('expected_payment_date', $month) @@ -227,20 +284,44 @@ protected function getEntertainment(int $year, int $month): array ->where('transaction_type', 'other') ->sum('amount'); + // 실제 데이터가 없으면 더미 데이터 사용 + $annualLimit = 40000000; // 연간 한도 4,000만원 + $monthlyUsage = $monthlyTotal > 0 ? (float) $monthlyTotal : 2400000; // 이번 달 240만원 + $yearlyUsage = $yearlyTotal > 0 ? (float) $yearlyTotal : 24000000; // 연간 누계 2,400만원 + $remainingLimit = $annualLimit - $yearlyUsage; + $lastMonthUsage = 1850000; // 전월 185만원 (더미) + $cards = [ - ['id' => 'ent-1', 'label' => '이번 달 접대비', 'amount' => (float) ($monthlyTotal * 0.1)], - ['id' => 'ent-2', 'label' => '연간 접대비 누계', 'amount' => (float) ($yearlyTotal * 0.1)], + ['id' => 'ent-1', 'label' => '접대비 한도', 'amount' => (float) $annualLimit, 'sub_amount' => $yearlyUsage, 'sub_label' => '사용'], + ['id' => 'ent-2', 'label' => '접대비 사용액', 'amount' => $monthlyUsage], + ['id' => 'ent-3', 'label' => '한도 잔액', 'amount' => $remainingLimit], + ['id' => 'ent-4', 'label' => '전월 사용액', 'amount' => $lastMonthUsage], ]; + $usageRate = ($yearlyUsage / $annualLimit) * 100; $checkPoints = []; - // 접대비 한도 초과 경고 (예시) - $limit = 5000000; // 월 500만원 한도 - if ($monthlyTotal * 0.1 > $limit) { + + if ($usageRate >= 100) { + $overAmount = $yearlyUsage - $annualLimit; $checkPoints[] = [ 'id' => 'ent-cp-1', 'type' => 'error', - 'message' => '접대비가 월 한도를 초과했습니다.', - 'highlight' => number_format($limit).'원 초과', + 'message' => '접대비 한도 초과 '.number_format($overAmount).'원 발생.', + 'highlight' => '손금불산입되어 법인세 부담 증가합니다.', + ]; + } elseif ($usageRate >= 85) { + $checkPoints[] = [ + 'id' => 'ent-cp-1', + 'type' => 'warning', + 'message' => '접대비 '.round($usageRate).'% 도달. 연내 한도 '.number_format($remainingLimit).'원 잔액입니다.', + 'highlight' => '사용 계획에 집중해 주세요.', + ]; + } else { + $checkPoints[] = [ + 'id' => 'ent-cp-1', + 'type' => 'success', + 'message' => '접대비 사용 총 '.number_format($yearlyUsage).'원 / 한도 '.number_format($annualLimit).'원 ('.round($usageRate).'%).', + 'highlight' => '여유 있게 운영 중입니다.', ]; } @@ -281,24 +362,58 @@ protected function getWelfare(int $year, int $month): array ->whereIn('transaction_type', ['salary', 'insurance']) ->sum('amount'); + // 실제 데이터가 없으면 더미 데이터 사용 + $hasData = $monthlyTotal > 0 || $yearlyTotal > 0; + $employeeCount = 15; // 직원 수 (더미) + $perPersonLimit = 200000; // 1인당 월 20만원 한도 + + $totalLimit = $employeeCount * $perPersonLimit; // 총 한도 300만원 + $monthlyWelfare = $hasData ? (float) $monthlyTotal : 2700000; // 이번 달 270만원 + $yearlyWelfare = $hasData ? (float) $yearlyTotal : 32400000; // 연간 누계 3,240만원 + $perPersonMonthly = $monthlyWelfare / $employeeCount; // 1인당 18만원 + $cards = [ - ['id' => 'wf-1', 'label' => '이번 달 복리후생비', 'amount' => (float) $monthlyTotal], - ['id' => 'wf-2', 'label' => '급여 지출', 'amount' => (float) $salaryTotal], - ['id' => 'wf-3', 'label' => '보험료 지출', 'amount' => (float) $insuranceTotal], - ['id' => 'wf-4', 'label' => '연간 복리후생비 누계', 'amount' => (float) $yearlyTotal], + ['id' => 'wf-1', 'label' => '총 복리후생비 한도', 'amount' => (float) $totalLimit], + ['id' => 'wf-2', 'label' => '누적 복리후생비 사용', 'amount' => $yearlyWelfare], + ['id' => 'wf-3', 'label' => '이번 달 복리후생비', 'amount' => $monthlyWelfare], + ['id' => 'wf-4', 'label' => '1인당 월 복리후생비', 'amount' => $perPersonMonthly], ]; $checkPoints = []; - // 복리후생비 비율 경고 (예시) - if ($monthlyTotal > 0 && $salaryTotal > 0) { - $ratio = ($monthlyTotal / $salaryTotal) * 100; - if ($ratio < 5 || $ratio > 20) { - $checkPoints[] = [ - 'id' => 'wf-cp-1', - 'type' => 'warning', - 'message' => '복리후생비 비율이 정상 범위(5~20%)를 벗어났습니다.', - ]; - } + + // 1인당 복리후생비 기준 체크 + if ($perPersonMonthly >= 150000 && $perPersonMonthly <= 250000) { + $checkPoints[] = [ + 'id' => 'wf-cp-1', + 'type' => 'success', + 'message' => '1인당 월 복리후생비 '.number_format($perPersonMonthly).'원. 업계 평균(15~25만원) 내', + 'highlight' => '정상 운영 중입니다.', + ]; + } elseif ($perPersonMonthly > 250000) { + $checkPoints[] = [ + 'id' => 'wf-cp-1', + 'type' => 'warning', + 'message' => '1인당 월 복리후생비가 '.number_format($perPersonMonthly).'원으로', + 'highlight' => '업계 평균(15~25만원)을 초과했습니다.', + ]; + } else { + $checkPoints[] = [ + 'id' => 'wf-cp-1', + 'type' => 'info', + 'message' => '1인당 월 복리후생비가 '.number_format($perPersonMonthly).'원으로', + 'highlight' => '업계 평균보다 낮습니다.', + ]; + } + + // 식대 비과세 한도 체크 (더미 경고) + $mealAllowance = 250000; // 식대 25만원 (더미) + if ($mealAllowance > 200000) { + $checkPoints[] = [ + 'id' => 'wf-cp-2', + 'type' => 'warning', + 'message' => '식대가 월 '.number_format($mealAllowance).'원으로', + 'highlight' => '비과세 한도(20만원)를 초과했습니다. 초과분은 근로소득 과세됩니다.', + ]; } return [