'I', 'name' => '매출액', 'type' => 'sum', 'category' => 'revenue', 'sub_categories' => ['sales_revenue']], ['code' => 'II', 'name' => '매출원가', 'type' => 'sum', 'category' => 'expense', 'sub_categories' => ['cogs', 'construction_cost']], ['code' => 'III', 'name' => '매출총이익', 'type' => 'calc', 'formula' => 'I - II'], ['code' => 'IV', 'name' => '판매비와관리비', 'type' => 'sum', 'category' => 'expense', 'sub_categories' => ['selling_admin']], ['code' => 'V', 'name' => '영업이익', 'type' => 'calc', 'formula' => 'III - IV'], ['code' => 'VI', 'name' => '영업외수익', 'type' => 'sum', 'category' => 'revenue', 'sub_categories' => ['other_revenue']], ['code' => 'VII', 'name' => '영업외비용', 'type' => 'sum', 'category' => 'expense', 'sub_categories' => ['other_expense'], 'exclude_codes' => ['99800', '99900']], ['code' => 'VIII', 'name' => '법인세비용차감전순이익', 'type' => 'calc', 'formula' => 'V + VI - VII'], ['code' => 'IX', 'name' => '법인세비용', 'type' => 'sum', 'category' => 'expense', 'tax_codes' => ['99800', '99900']], ['code' => 'X', 'name' => '당기순이익', 'type' => 'calc', 'formula' => 'VIII - IX'], ]; /** * 손익계산서 페이지 */ public function index(Request $request) { if ($request->header('HX-Request')) { return response('', 200)->header('HX-Redirect', route('finance.income-statement')); } return view('finance.income-statement'); } /** * 손익계산서 데이터 조회 */ public function data(Request $request): JsonResponse { $tenantId = session('selected_tenant_id', 1); $startDate = $request->input('start_date', now()->startOfYear()->format('Y-m-d')); $endDate = $request->input('end_date', now()->endOfYear()->format('Y-m-d')); $unit = $request->input('unit', 'won'); // 전기 기간 계산 (전년 동기) $prevStartDate = date('Y-m-d', strtotime($startDate.' -1 year')); $prevEndDate = date('Y-m-d', strtotime($endDate.' -1 year')); // 당기/전기 계정별 합계 $currentSums = $this->getAccountSums($tenantId, $startDate, $endDate); $previousSums = $this->getAccountSums($tenantId, $prevStartDate, $prevEndDate); $accountCodes = AccountCode::where('tenant_id', $tenantId) ->where('is_active', true) ->whereIn('category', ['revenue', 'expense']) ->orderBy('sort_order') ->orderBy('code') ->get(); $sections = $this->buildSections($accountCodes, $currentSums, $previousSums, $unit); // 기수 계산 $currentYear = (int) date('Y', strtotime($endDate)); $fiscalYear = $this->getFiscalYear($tenantId, $currentYear); return response()->json([ 'period' => [ 'current' => [ 'start' => $startDate, 'end' => $endDate, 'label' => "제 {$fiscalYear} (당)기", ], 'previous' => [ 'start' => $prevStartDate, 'end' => $prevEndDate, 'label' => '제 '.($fiscalYear - 1).' (전)기', ], ], 'unit' => $unit, 'sections' => $sections, ]); } /** * 계정코드별 차변/대변 합계 조회 */ private function getAccountSums(int $tenantId, string $startDate, string $endDate): array { // 일반전표 $journalSums = DB::table('journal_entry_lines as jel') ->join('journal_entries as je', 'je.id', '=', 'jel.journal_entry_id') ->where('jel.tenant_id', $tenantId) ->whereBetween('je.entry_date', [$startDate, $endDate]) ->whereNull('je.deleted_at') ->groupBy('jel.account_code') ->select([ 'jel.account_code', DB::raw('SUM(jel.debit_amount) as debit'), DB::raw('SUM(jel.credit_amount) as credit'), ]) ->get(); // 홈택스 $hometaxSums = DB::table('hometax_invoice_journals as hij') ->join('hometax_invoices as hi', 'hi.id', '=', 'hij.hometax_invoice_id') ->where('hij.tenant_id', $tenantId) ->whereBetween('hij.write_date', [$startDate, $endDate]) ->whereNull('hi.deleted_at') ->groupBy('hij.account_code') ->select([ 'hij.account_code', DB::raw('SUM(hij.debit_amount) as debit'), DB::raw('SUM(hij.credit_amount) as credit'), ]) ->get(); $result = []; foreach ($journalSums as $row) { $result[$row->account_code] = [ 'debit' => (int) $row->debit, 'credit' => (int) $row->credit, ]; } foreach ($hometaxSums as $row) { if (! isset($result[$row->account_code])) { $result[$row->account_code] = ['debit' => 0, 'credit' => 0]; } $result[$row->account_code]['debit'] += (int) $row->debit; $result[$row->account_code]['credit'] += (int) $row->credit; } return $result; } /** * 계산 공식 평가 (I - II, III - IV 등) */ private function evaluateFormula(string $formula, array $values): array { $current = 0; $previous = 0; $parts = preg_split('/\s*([+-])\s*/', $formula, -1, PREG_SPLIT_DELIM_CAPTURE); $sign = 1; foreach ($parts as $part) { $part = trim($part); if ($part === '+') { $sign = 1; } elseif ($part === '-') { $sign = -1; } elseif (isset($values[$part])) { $current += $sign * $values[$part]['current']; $previous += $sign * $values[$part]['previous']; $sign = 1; } } return compact('current', 'previous'); } /** * 금액 단위 변환 */ private function applyUnitDivisor(array $sections, int $divisor): array { foreach ($sections as &$section) { $section['current_amount'] = (int) round($section['current_amount'] / $divisor); $section['previous_amount'] = (int) round($section['previous_amount'] / $divisor); if (! empty($section['items'])) { foreach ($section['items'] as &$item) { $item['current'] = (int) round($item['current'] / $divisor); $item['previous'] = (int) round($item['previous'] / $divisor); } unset($item); } } unset($section); return $sections; } /** * 기수 계산 (코드브릿지엑스 설립: 2025-09-13, 1기 = 2025년) */ private function getFiscalYear(int $tenantId, int $currentYear): int { $baseYear = 2024; // 2025 - 2024 = 1기 return $currentYear - $baseYear; } /** * 월별 손익계산서 조회 */ public function monthly(Request $request): JsonResponse { $tenantId = session('selected_tenant_id', 1); $year = (int) $request->input('year', now()->year); $unit = $request->input('unit', 'won'); $accountCodes = AccountCode::where('tenant_id', $tenantId) ->where('is_active', true) ->whereIn('category', ['revenue', 'expense']) ->orderBy('sort_order') ->orderBy('code') ->get(); $months = []; for ($m = 1; $m <= 12; $m++) { $startDate = sprintf('%04d-%02d-01', $year, $m); $endDate = date('Y-m-t', strtotime($startDate)); // 미래 월은 건너뜀 if (strtotime($startDate) > time()) { break; } $sums = $this->getAccountSums($tenantId, $startDate, $endDate); $sections = $this->buildSections($accountCodes, $sums, $sums, $unit, true); $months[] = [ 'month' => sprintf('%02d', $m), 'label' => $m.'월', 'sections' => $sections, ]; } $fiscalYear = $this->getFiscalYear($tenantId, $year); return response()->json([ 'year' => $year, 'fiscal_year' => $fiscalYear, 'fiscal_label' => "제 {$fiscalYear} 기", 'unit' => $unit, 'months' => $months, ]); } /** * 섹션 조립 공통 로직 */ private function buildSections($accountCodes, array $currentSums, array $previousSums, string $unit, bool $currentOnly = false): array { $sections = []; $calcValues = []; foreach (self::PL_STRUCTURE as $item) { $section = [ 'code' => $item['code'], 'name' => $item['name'], 'current_amount' => 0, 'previous_amount' => 0, 'items' => [], 'is_calculated' => $item['type'] === 'calc', ]; if ($item['type'] === 'sum') { $relatedAccounts = $accountCodes->filter(function ($ac) use ($item) { if (! empty($item['tax_codes'])) { return in_array($ac->code, $item['tax_codes']); } $subCategories = $item['sub_categories'] ?? []; if ($ac->category !== $item['category'] || ! in_array($ac->sub_category, $subCategories)) { return false; } if (! empty($item['exclude_codes']) && in_array($ac->code, $item['exclude_codes'])) { return false; } return true; }); $currentTotal = 0; $previousTotal = 0; foreach ($relatedAccounts as $ac) { $curDebit = $currentSums[$ac->code]['debit'] ?? 0; $curCredit = $currentSums[$ac->code]['credit'] ?? 0; if ($ac->category === 'revenue') { $curAmount = $curCredit - $curDebit; } else { $curAmount = $curDebit - $curCredit; } $prevAmount = 0; if (! $currentOnly) { $prevDebit = $previousSums[$ac->code]['debit'] ?? 0; $prevCredit = $previousSums[$ac->code]['credit'] ?? 0; $prevAmount = $ac->category === 'revenue' ? ($prevCredit - $prevDebit) : ($prevDebit - $prevCredit); } if ($curAmount != 0 || $prevAmount != 0) { $section['items'][] = [ 'code' => $ac->code, 'name' => $ac->name, 'current' => $curAmount, 'previous' => $prevAmount, ]; } $currentTotal += $curAmount; $previousTotal += $prevAmount; } $section['current_amount'] = $currentTotal; $section['previous_amount'] = $previousTotal; $calcValues[$item['code']] = ['current' => $currentTotal, 'previous' => $previousTotal]; } elseif ($item['type'] === 'calc') { $amounts = $this->evaluateFormula($item['formula'], $calcValues); $section['current_amount'] = $amounts['current']; $section['previous_amount'] = $amounts['previous']; $calcValues[$item['code']] = $amounts; } $sections[] = $section; } $divisor = match ($unit) { 'thousand' => 1000, 'million' => 1000000, default => 1, }; if ($divisor > 1) { $sections = $this->applyUnitDivisor($sections, $divisor); } return $sections; } }