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:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 인정이자 리포트 (연도별 요약)
|
||||
*/
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user