feat: 세금 시뮬레이션 API 개발 (Phase 1.3)

- LoanService에 taxSimulation() 메서드 추가
- LoanController에 taxSimulation() 액션 추가
- GET /api/v1/loans/tax-simulation 라우트 등록
- Swagger LoanTaxSimulation 스키마 및 엔드포인트 문서화
- 법인세/소득세 비교 분석 데이터 제공

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-01-22 22:47:59 +09:00
parent 7162fc2b46
commit f089843e1e
4 changed files with 198 additions and 0 deletions

View File

@@ -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);
}
}

View File

@@ -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,
],
],
];
}
/**
* 인정이자 리포트 (연도별 요약)
*/

View File

@@ -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",

View File

@@ -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');