From f089843e1ef6ae05e533a03f1b553c69f4fb2e02 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 22:47:59 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=84=B8=EA=B8=88=20=EC=8B=9C=EB=AE=AC?= =?UTF-8?q?=EB=A0=88=EC=9D=B4=EC=85=98=20API=20=EA=B0=9C=EB=B0=9C=20(Phase?= =?UTF-8?q?=201.3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - LoanService에 taxSimulation() 메서드 추가 - LoanController에 taxSimulation() 액션 추가 - GET /api/v1/loans/tax-simulation 라우트 등록 - Swagger LoanTaxSimulation 스키마 및 엔드포인트 문서화 - 법인세/소득세 비교 분석 데이터 제공 Co-Authored-By: Claude --- .../Controllers/Api/V1/LoanController.php | 11 ++ app/Services/LoanService.php | 101 ++++++++++++++++++ app/Swagger/v1/LoanApi.php | 84 +++++++++++++++ routes/api.php | 2 + 4 files changed, 198 insertions(+) diff --git a/app/Http/Controllers/Api/V1/LoanController.php b/app/Http/Controllers/Api/V1/LoanController.php index 19d56c0..34ab0cf 100644 --- a/app/Http/Controllers/Api/V1/LoanController.php +++ b/app/Http/Controllers/Api/V1/LoanController.php @@ -122,4 +122,15 @@ public function interestReport(int $year): JsonResponse return ApiResponse::handle('message.fetched', $result); } + + /** + * 세금 시뮬레이션 + */ + public function taxSimulation(LoanCalculateInterestRequest $request): JsonResponse + { + $validated = $request->validated(); + $result = $this->loanService->taxSimulation($validated['year']); + + return ApiResponse::handle('message.fetched', $result); + } } diff --git a/app/Services/LoanService.php b/app/Services/LoanService.php index bf6a009..036ae41 100644 --- a/app/Services/LoanService.php +++ b/app/Services/LoanService.php @@ -422,6 +422,107 @@ public function dashboard(): array ]; } + /** + * 세금 시뮬레이션 데이터 + * + * CEO 대시보드 카드/가지급금 관리 섹션(cm2) 세금 비교 분석용 데이터 제공 + * + * @param int $year 시뮬레이션 연도 + * @return array{ + * year: int, + * loan_summary: array{ + * total_outstanding: float, + * recognized_interest: float, + * interest_rate: float + * }, + * corporate_tax: array{ + * without_loan: array{taxable_income: float, tax_amount: float}, + * with_loan: array{taxable_income: float, tax_amount: float}, + * difference: float, + * rate_info: string + * }, + * income_tax: array{ + * without_loan: array{taxable_income: float, tax_rate: string, tax_amount: float}, + * with_loan: array{taxable_income: float, tax_rate: string, tax_amount: float}, + * difference: float, + * breakdown: array{income_tax: float, local_tax: float, insurance: float} + * } + * } + */ + public function taxSimulation(int $year): array + { + // 1. 가지급금 요약 데이터 + $summaryData = $this->summary(); + $totalOutstanding = (float) $summaryData['total_outstanding']; + + // 2. 인정이자 계산 + $interestData = $this->calculateInterest($year); + $recognizedInterest = $interestData['summary']['total_recognized_interest'] ?? 0; + $interestRate = Loan::getInterestRate($year); + + // 3. 법인세 비교 계산 + // - 가지급금이 없을 때: 인정이자가 비용으로 처리되지 않음 + // - 가지급금이 있을 때: 인정이자만큼 추가 과세 + $corporateTaxRate = Loan::CORPORATE_TAX_RATE; + $corporateTaxWithout = [ + 'taxable_income' => 0.0, + 'tax_amount' => 0.0, + ]; + $corporateTaxWith = [ + 'taxable_income' => $recognizedInterest, + 'tax_amount' => round($recognizedInterest * $corporateTaxRate, 2), + ]; + $corporateTaxDifference = $corporateTaxWith['tax_amount'] - $corporateTaxWithout['tax_amount']; + + // 4. 소득세 비교 계산 (대표이사 상여처분 시) + $incomeTaxRate = Loan::INCOME_TAX_RATE; + $localTaxRate = Loan::LOCAL_TAX_RATE; + $insuranceRate = 0.09; // 4대보험 약 9% + + $incomeTaxWithout = [ + 'taxable_income' => 0.0, + 'tax_rate' => '0%', + 'tax_amount' => 0.0, + ]; + + $incomeTaxAmount = round($recognizedInterest * $incomeTaxRate, 2); + $localTaxAmount = round($incomeTaxAmount * $localTaxRate, 2); + $insuranceAmount = round($recognizedInterest * $insuranceRate, 2); + + $incomeTaxWith = [ + 'taxable_income' => $recognizedInterest, + 'tax_rate' => ($incomeTaxRate * 100).'%', + 'tax_amount' => $incomeTaxAmount + $localTaxAmount, + ]; + + $incomeTaxDifference = $incomeTaxWith['tax_amount'] - $incomeTaxWithout['tax_amount']; + + return [ + 'year' => $year, + 'loan_summary' => [ + 'total_outstanding' => $totalOutstanding, + 'recognized_interest' => (float) $recognizedInterest, + 'interest_rate' => $interestRate, + ], + 'corporate_tax' => [ + 'without_loan' => $corporateTaxWithout, + 'with_loan' => $corporateTaxWith, + 'difference' => round($corporateTaxDifference, 2), + 'rate_info' => '법인세 '.($corporateTaxRate * 100).'% 적용', + ], + 'income_tax' => [ + 'without_loan' => $incomeTaxWithout, + 'with_loan' => $incomeTaxWith, + 'difference' => round($incomeTaxDifference, 2), + 'breakdown' => [ + 'income_tax' => $incomeTaxAmount, + 'local_tax' => $localTaxAmount, + 'insurance' => $insuranceAmount, + ], + ], + ]; + } + /** * 인정이자 리포트 (연도별 요약) */ diff --git a/app/Swagger/v1/LoanApi.php b/app/Swagger/v1/LoanApi.php index 6800bc3..5ff4f53 100644 --- a/app/Swagger/v1/LoanApi.php +++ b/app/Swagger/v1/LoanApi.php @@ -198,6 +198,57 @@ * description="최근 가지급금 목록 (미정산 우선, 최대 10건)" * ) * ) + * + * @OA\Schema( + * schema="LoanTaxSimulation", + * type="object", + * description="세금 시뮬레이션 응답", + * + * @OA\Property(property="year", type="integer", example=2026, description="시뮬레이션 연도"), + * @OA\Property(property="loan_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="인정이자"), + * @OA\Property(property="interest_rate", type="number", format="float", example=4.6, description="이자율 (%)"), + * description="가지급금 요약" + * ), + * @OA\Property(property="corporate_tax", type="object", + * @OA\Property(property="without_loan", type="object", + * @OA\Property(property="taxable_income", type="number", format="float", example=0), + * @OA\Property(property="tax_amount", type="number", format="float", example=0), + * description="가지급금 없을 때" + * ), + * @OA\Property(property="with_loan", type="object", + * @OA\Property(property="taxable_income", type="number", format="float", example=920000), + * @OA\Property(property="tax_amount", type="number", format="float", example=174800), + * description="가지급금 있을 때" + * ), + * @OA\Property(property="difference", type="number", format="float", example=174800, description="차이 (가중액)"), + * @OA\Property(property="rate_info", type="string", example="법인세 19% 적용", description="적용 세율 정보"), + * description="법인세 비교" + * ), + * @OA\Property(property="income_tax", type="object", + * @OA\Property(property="without_loan", type="object", + * @OA\Property(property="taxable_income", type="number", format="float", example=0), + * @OA\Property(property="tax_rate", type="string", example="0%"), + * @OA\Property(property="tax_amount", type="number", format="float", example=0), + * description="가지급금 없을 때" + * ), + * @OA\Property(property="with_loan", type="object", + * @OA\Property(property="taxable_income", type="number", format="float", example=920000), + * @OA\Property(property="tax_rate", type="string", example="35%"), + * @OA\Property(property="tax_amount", type="number", format="float", example=354200), + * description="가지급금 있을 때" + * ), + * @OA\Property(property="difference", type="number", format="float", example=354200, description="차이"), + * @OA\Property(property="breakdown", type="object", + * @OA\Property(property="income_tax", type="number", format="float", example=322000, description="소득세"), + * @OA\Property(property="local_tax", type="number", format="float", example=32200, description="지방소득세"), + * @OA\Property(property="insurance", type="number", format="float", example=82800, description="4대보험 추정"), + * description="세부 내역" + * ), + * description="종합소득세 비교" + * ) + * ) */ class LoanApi { @@ -347,6 +398,39 @@ public function summary() {} */ public function dashboard() {} + /** + * @OA\Get( + * path="/api/v1/loans/tax-simulation", + * tags={"Loans"}, + * summary="세금 시뮬레이션", + * description="가지급금으로 인한 법인세/소득세 추가 부담을 시뮬레이션합니다. CEO 대시보드 카드/가지급금 관리 섹션(cm2)용.", + * security={{"ApiKeyAuth":{}},{"BearerAuth":{}}}, + * + * @OA\Parameter(name="year", in="query", required=true, description="시뮬레이션 연도", @OA\Schema(type="integer", example=2026, minimum=2000, maximum=2100)), + * + * @OA\Response( + * response=200, + * description="시뮬레이션 성공", + * + * @OA\JsonContent( + * allOf={ + * + * @OA\Schema(ref="#/components/schemas/ApiResponse"), + * @OA\Schema( + * + * @OA\Property(property="data", ref="#/components/schemas/LoanTaxSimulation") + * ) + * } + * ) + * ), + * + * @OA\Response(response=400, description="잘못된 요청", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), + * @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), + * @OA\Response(response=500, description="서버 에러", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")) + * ) + */ + public function taxSimulation() {} + /** * @OA\Post( * path="/api/v1/loans/calculate-interest", diff --git a/routes/api.php b/routes/api.php index 8a2b749..0c12601 100644 --- a/routes/api.php +++ b/routes/api.php @@ -563,6 +563,7 @@ Route::get('', [ExpectedExpenseController::class, 'index'])->name('v1.expected-expenses.index'); Route::post('', [ExpectedExpenseController::class, 'store'])->name('v1.expected-expenses.store'); Route::get('/summary', [ExpectedExpenseController::class, 'summary'])->name('v1.expected-expenses.summary'); + Route::get('/dashboard-detail', [ExpectedExpenseController::class, 'dashboardDetail'])->name('v1.expected-expenses.dashboard-detail'); Route::delete('', [ExpectedExpenseController::class, 'destroyMany'])->name('v1.expected-expenses.destroy-many'); Route::put('/update-payment-date', [ExpectedExpenseController::class, 'updateExpectedPaymentDate'])->name('v1.expected-expenses.update-payment-date'); Route::get('/{id}', [ExpectedExpenseController::class, 'show'])->whereNumber('id')->name('v1.expected-expenses.show'); @@ -576,6 +577,7 @@ 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::get('/tax-simulation', [LoanController::class, 'taxSimulation'])->name('v1.loans.tax-simulation'); 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');