month; $year = $date->year; return [ 'today_issue' => $this->getTodayIssue($date), 'monthly_expense' => $this->getMonthlyExpense($year, $month), 'card_management' => $this->getCardManagement($year, $month), 'entertainment' => $this->getEntertainment($year, $month), 'welfare' => $this->getWelfare($year, $month), 'receivable' => $this->getReceivable($year, $month), 'debt_collection' => $this->getDebtCollection(), ]; } /** * 오늘의 이슈 - 현재 사용자가 결재할 수 있는 대기 문서 */ protected function getTodayIssue(Carbon $date): array { $tenantId = $this->tenantId(); $userId = $this->apiUserId(); // 현재 사용자가 결재자인 대기 문서만 조회 $pendingApprovals = Approval::where('tenant_id', $tenantId) ->pending() ->whereHas('steps', function ($q) use ($userId) { $q->where('approver_id', $userId) ->where('status', ApprovalStep::STATUS_PENDING); }) ->with(['form', 'drafter']) ->orderBy('drafted_at', 'desc') ->limit(10) ->get(); $categories = ['전체필터']; $items = []; foreach ($pendingApprovals as $approval) { $category = $approval->form?->name ?? '결재요청'; if (! in_array($category, $categories)) { $categories[] = $category; } $items[] = [ 'id' => (string) $approval->id, 'category' => $category, 'description' => $approval->title, 'requires_approval' => true, 'time' => $approval->drafted_at?->format('H:i') ?? '', ]; } return [ 'filter_options' => $categories, 'items' => $items, ]; } /** * 당월 예상 지출 내역 */ 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) ->selectRaw('transaction_type, SUM(amount) as total') ->groupBy('transaction_type') ->get() ->keyBy('transaction_type'); // 전월 데이터 $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'); // 실제 데이터 추출 $purchaseTotal = (float) ($expenses['purchase']->total ?? 0); $cardTotal = (float) ($expenses['card']->total ?? 0); $billTotal = (float) ($expenses['bill']->total ?? 0); $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' => $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 = []; $changeRate = $lastTotalExpense > 0 ? (($totalExpense - $lastTotalExpense) / $lastTotalExpense) * 100 : 0; if ($changeRate > 15) { $checkPoints[] = [ 'id' => 'expense-cp-1', 'type' => 'warning', 'message' => '이번 달 예상 지출이', 'highlight' => '전월 대비 '.number_format($changeRate, 0).'% 증가했습니다. 매입 비용 증가가 주요 원인입니다.', ]; } elseif ($changeRate < -5) { $checkPoints[] = [ 'id' => 'expense-cp-1', 'type' => 'success', 'message' => '이번 달 예상 지출이', 'highlight' => '전월 대비 '.number_format(abs($changeRate), 0).'% 감소했습니다.', ]; } else { $checkPoints[] = [ 'id' => 'expense-cp-1', 'type' => 'info', 'message' => '이번 달 예상 지출이', 'highlight' => '전월과 비슷한 수준입니다.', ]; } return [ 'cards' => $cards, 'check_points' => $checkPoints, ]; } /** * 카드/가지급금 관리 */ protected function getCardManagement(int $year, int $month): array { $tenantId = $this->tenantId(); // 가지급금 (suspense) $suspenseTotal = ExpectedExpense::where('tenant_id', $tenantId) ->where('transaction_type', 'suspense') ->where('payment_status', '!=', 'paid') ->sum('amount'); // 선급금 (advance) $advanceTotal = ExpectedExpense::where('tenant_id', $tenantId) ->where('transaction_type', 'advance') ->where('payment_status', '!=', 'paid') ->sum('amount'); // 이번 달 카드 사용액 (가상 - 실제로는 CardTransaction 등 필요) $monthlyCardUsage = ExpectedExpense::where('tenant_id', $tenantId) ->whereIn('transaction_type', ['suspense', 'advance']) ->whereYear('expected_payment_date', $year) ->whereMonth('expected_payment_date', $month) ->sum('amount'); // 미정산 금액 $unsettledTotal = ExpectedExpense::where('tenant_id', $tenantId) ->whereIn('transaction_type', ['suspense', 'advance']) ->where('payment_status', 'pending') ->sum('amount'); $cards = [ ['id' => 'card-1', 'label' => '가지급금 잔액', 'amount' => (float) $suspenseTotal], ['id' => 'card-2', 'label' => '선급금 잔액', 'amount' => (float) $advanceTotal], ['id' => 'card-3', 'label' => '이번 달 카드 사용액', 'amount' => (float) $monthlyCardUsage], ['id' => 'card-4', 'label' => '미정산 금액', 'amount' => (float) $unsettledTotal], ]; $checkPoints = []; if ($suspenseTotal > 10000000) { $checkPoints[] = [ 'id' => 'card-cp-1', 'type' => 'warning', 'message' => '가지급금이 1,000만원을 초과했습니다.', 'highlight' => '정산이 필요합니다.', ]; } if ($unsettledTotal > 0) { $checkPoints[] = [ 'id' => 'card-cp-2', 'type' => 'info', 'message' => '미정산 금액이 '.number_format($unsettledTotal).'원 있습니다.', ]; } return [ 'cards' => $cards, 'check_points' => $checkPoints, ]; } /** * 접대비 현황 */ 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) ->where('transaction_type', 'other') ->sum('amount'); $yearlyTotal = ExpectedExpense::where('tenant_id', $tenantId) ->whereYear('expected_payment_date', $year) ->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) $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 = []; if ($usageRate >= 100) { $overAmount = $yearlyUsage - $annualLimit; $checkPoints[] = [ 'id' => 'ent-cp-1', 'type' => 'error', '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' => '여유 있게 운영 중입니다.', ]; } return [ 'cards' => $cards, 'check_points' => $checkPoints, ]; } /** * 복리후생비 현황 */ protected function getWelfare(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) ->whereIn('transaction_type', ['salary', 'insurance']) ->sum('amount'); $salaryTotal = ExpectedExpense::where('tenant_id', $tenantId) ->whereYear('expected_payment_date', $year) ->whereMonth('expected_payment_date', $month) ->where('transaction_type', 'salary') ->sum('amount'); $insuranceTotal = ExpectedExpense::where('tenant_id', $tenantId) ->whereYear('expected_payment_date', $year) ->whereMonth('expected_payment_date', $month) ->where('transaction_type', 'insurance') ->sum('amount'); $yearlyTotal = ExpectedExpense::where('tenant_id', $tenantId) ->whereYear('expected_payment_date', $year) ->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) $totalLimit], ['id' => 'wf-2', 'label' => '누적 복리후생비 사용', 'amount' => $yearlyWelfare], ['id' => 'wf-3', 'label' => '이번 달 복리후생비', 'amount' => $monthlyWelfare], ['id' => 'wf-4', 'label' => '1인당 월 복리후생비', 'amount' => $perPersonMonthly], ]; $checkPoints = []; // 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 [ 'cards' => $cards, 'check_points' => $checkPoints, ]; } /** * 미수금 현황 */ protected function getReceivable(int $year, int $month): array { $tenantId = $this->tenantId(); // 총 미수금 (거래처별) $totalReceivable = Client::where('tenant_id', $tenantId) ->where('is_active', true) ->sum('outstanding_balance'); // 이번 달 입금 $monthlyDeposit = Deposit::where('tenant_id', $tenantId) ->whereYear('deposit_date', $year) ->whereMonth('deposit_date', $month) ->sum('amount'); // 오늘 입금 $todayDeposit = Deposit::where('tenant_id', $tenantId) ->whereDate('deposit_date', Carbon::today()) ->sum('amount'); // 연체 미수금 (credit_limit 초과 거래처) $overdueReceivable = Client::where('tenant_id', $tenantId) ->where('is_active', true) ->whereColumn('outstanding_balance', '>', 'credit_limit') ->sum('outstanding_balance'); $cards = [ [ 'id' => 'rcv-1', 'label' => '총 미수금', 'amount' => (float) $totalReceivable, 'sub_amount' => (float) $overdueReceivable, 'sub_label' => '한도초과', ], [ 'id' => 'rcv-2', 'label' => '이번 달 입금', 'amount' => (float) $monthlyDeposit, ], [ 'id' => 'rcv-3', 'label' => '오늘 입금', 'amount' => (float) $todayDeposit, ], [ 'id' => 'rcv-4', 'label' => '연체 미수금', 'amount' => (float) $overdueReceivable, ], ]; $checkPoints = []; // 한도 초과 거래처 $overdueClients = Client::where('tenant_id', $tenantId) ->where('is_active', true) ->whereColumn('outstanding_balance', '>', 'credit_limit') ->select('name', 'outstanding_balance') ->limit(3) ->get(); foreach ($overdueClients as $client) { $checkPoints[] = [ 'id' => 'rcv-cp-'.($client->id ?? uniqid()), 'type' => 'error', 'message' => $client->name.'의 미수금이 한도를 초과했습니다.', 'highlight' => '거래 주의가 필요합니다.', ]; } return [ 'cards' => $cards, 'check_points' => $checkPoints, 'has_detail_button' => true, 'detail_button_label' => '거래처별 미수금 현황', 'detail_button_path' => '/accounting/receivables-status', ]; } /** * 채권추심 현황 */ protected function getDebtCollection(): array { $tenantId = $this->tenantId(); // 추심중 건수 및 금액 $collectingData = BadDebt::where('tenant_id', $tenantId) ->collecting() ->selectRaw('COUNT(*) as count, SUM(debt_amount) as total') ->first(); // 법적조치 건수 및 금액 $legalData = BadDebt::where('tenant_id', $tenantId) ->legalAction() ->selectRaw('COUNT(*) as count, SUM(debt_amount) as total') ->first(); // 회수완료 금액 (이번 년도) $recoveredTotal = BadDebt::where('tenant_id', $tenantId) ->recovered() ->whereYear('closed_at', Carbon::now()->year) ->sum('debt_amount'); // 대손처리 금액 $badDebtTotal = BadDebt::where('tenant_id', $tenantId) ->badDebt() ->sum('debt_amount'); $cards = [ ['id' => 'debt-1', 'label' => '추심중', 'amount' => (float) ($collectingData->total ?? 0)], ['id' => 'debt-2', 'label' => '법적조치 진행', 'amount' => (float) ($legalData->total ?? 0)], ['id' => 'debt-3', 'label' => '올해 회수 완료', 'amount' => (float) $recoveredTotal], ['id' => 'debt-4', 'label' => '대손처리 금액', 'amount' => (float) $badDebtTotal], ]; $checkPoints = []; $collectingCount = $collectingData->count ?? 0; $legalCount = $legalData->count ?? 0; if ($collectingCount > 0) { $checkPoints[] = [ 'id' => 'debt-cp-1', 'type' => 'info', 'message' => '현재 추심 진행 중인 건이 '.$collectingCount.'건 있습니다.', ]; } if ($legalCount > 0) { $checkPoints[] = [ 'id' => 'debt-cp-2', 'type' => 'warning', 'message' => '법적조치 진행 중인 건이 '.$legalCount.'건 있습니다.', ]; } return [ 'cards' => $cards, 'check_points' => $checkPoints, ]; } }