feat: [hr] 사원 연봉 등록 시 급여 산정 테이블 추가

- 고정연장근로수당 산정 계산 로직 구현 (기본급, 통상시급, 고정OT 자동 계산)
- 수정 모드에서 실시간 급여 산정 미리보기 테이블
- 조회 모드에서 요약/상세 급여 산정 내역 표시
- 식대, 월 고정연장근로시간 입력 필드 추가
- 계산 결과를 salary_info에 저장하여 급여관리에서 활용 가능
This commit is contained in:
김보곤
2026-03-12 14:34:21 +09:00
parent 26acd0e07b
commit de6ef7472a
3 changed files with 372 additions and 77 deletions

View File

@@ -61,6 +61,8 @@ public function update(Request $request, int $id): JsonResponse
$validated = $request->validate([
'annual_salary' => 'nullable|integer|min:0',
'meal_allowance' => 'nullable|integer|min:0|max:1000000',
'fixed_overtime_hours' => 'nullable|integer|min:0|max:100',
'effective_date' => 'nullable|date',
'notes' => 'nullable|string|max:500',
]);

View File

@@ -180,12 +180,24 @@ public function setJsonExtraValue(string $key, mixed $value): void
public function getSalaryInfo(): array
{
return $this->json_extra['salary_info'] ?? [
$defaults = [
'annual_salary' => null,
'effective_date' => null,
'notes' => null,
'fixed_overtime_hours' => null,
'meal_allowance' => 200000,
'monthly_salary' => null,
'base_salary' => null,
'fixed_overtime_pay' => null,
'hourly_wage' => null,
'monthly_work_hours' => 209,
'overtime_multiplier' => 1.5,
'history' => [],
];
$data = $this->json_extra['salary_info'] ?? [];
return array_merge($defaults, $data);
}
public function setSalaryInfo(array $data): void
@@ -197,6 +209,10 @@ public function setSalaryInfo(array $data): void
if ($current['annual_salary'] !== null) {
$history[] = [
'annual_salary' => $current['annual_salary'],
'fixed_overtime_hours' => $current['fixed_overtime_hours'],
'meal_allowance' => $current['meal_allowance'],
'base_salary' => $current['base_salary'],
'fixed_overtime_pay' => $current['fixed_overtime_pay'],
'effective_date' => $current['effective_date'],
'notes' => $current['notes'],
'recorded_at' => now()->format('Y-m-d H:i:s'),
@@ -204,12 +220,56 @@ public function setSalaryInfo(array $data): void
];
}
$this->setJsonExtraValue('salary_info', [
'annual_salary' => $data['annual_salary'] ?? null,
$annualSalary = $data['annual_salary'] ?? null;
$mealAllowance = $data['meal_allowance'] ?? 200000;
$fixedOvertimeHours = $data['fixed_overtime_hours'] ?? null;
$breakdown = $this->calculateSalaryBreakdown($annualSalary, $mealAllowance, $fixedOvertimeHours);
$this->setJsonExtraValue('salary_info', array_merge([
'annual_salary' => $annualSalary,
'effective_date' => $data['effective_date'] ?? null,
'notes' => $data['notes'] ?? null,
'history' => $history,
]);
'fixed_overtime_hours' => $fixedOvertimeHours,
'meal_allowance' => $mealAllowance,
], $breakdown, ['history' => $history]));
}
/**
* 급여 산정 계산
*
* 공식: (기본급 + 식대) = 월급여 × 209 / (209 + 고정연장근로시간 × 1.5)
*/
private function calculateSalaryBreakdown(?int $annualSalary, int $mealAllowance, ?int $fixedOvertimeHours): array
{
$monthlyWorkHours = 209;
$overtimeMultiplier = 1.5;
if (! $annualSalary) {
return [
'monthly_salary' => null,
'base_salary' => null,
'fixed_overtime_pay' => null,
'hourly_wage' => null,
'monthly_work_hours' => $monthlyWorkHours,
'overtime_multiplier' => $overtimeMultiplier,
];
}
$monthlySalary = (int) round($annualSalary / 12);
$otFactor = ($fixedOvertimeHours ?? 0) * $overtimeMultiplier;
$basePlusMeal = (int) round($monthlySalary * $monthlyWorkHours / ($monthlyWorkHours + $otFactor));
$baseSalary = $basePlusMeal - $mealAllowance;
$hourlyWage = (int) floor($basePlusMeal / $monthlyWorkHours);
$fixedOvertimePay = $monthlySalary - $baseSalary - $mealAllowance;
return [
'monthly_salary' => $monthlySalary,
'base_salary' => $baseSalary,
'fixed_overtime_pay' => $fixedOvertimePay,
'hourly_wage' => $hourlyWage,
'monthly_work_hours' => $monthlyWorkHours,
'overtime_multiplier' => $overtimeMultiplier,
];
}
/**

View File

@@ -26,51 +26,245 @@ class="inline-flex items-center gap-1 px-3 py-1.5 text-sm font-medium rounded-lg
</button>
</div>
{{-- ============================================================ --}}
{{-- 조회 모드 --}}
<div x-show="!editing" class="divide-y divide-gray-100">
<div class="px-6 py-3 flex" style="flex-wrap: wrap;">
<div class="shrink-0 text-sm font-medium text-gray-500" style="width: 140px;">연봉</div>
<div class="text-sm text-gray-900 font-semibold">
<template x-if="salaryData.annual_salary !== null && salaryData.annual_salary !== ''">
<span x-text="Number(salaryData.annual_salary).toLocaleString() + '원'"></span>
</template>
<template x-if="salaryData.annual_salary === null || salaryData.annual_salary === ''">
<span class="text-gray-400 font-normal">미입력</span>
</template>
{{-- ============================================================ --}}
<div x-show="!editing">
{{-- 기본 연봉 정보 --}}
<div class="divide-y divide-gray-100">
<div class="px-6 py-3 flex" style="flex-wrap: wrap;">
<div class="shrink-0 text-sm font-medium text-gray-500" style="width: 140px;">연봉</div>
<div class="text-sm text-gray-900 font-semibold">
<template x-if="salaryData.annual_salary">
<span x-text="Number(salaryData.annual_salary).toLocaleString() + '원'"></span>
</template>
<template x-if="!salaryData.annual_salary">
<span class="text-gray-400 font-normal">미입력</span>
</template>
</div>
</div>
<div class="px-6 py-3 flex" style="flex-wrap: wrap;">
<div class="shrink-0 text-sm font-medium text-gray-500" style="width: 140px;">적용일</div>
<div class="text-sm text-gray-900" x-text="salaryData.effective_date || '-'"></div>
</div>
<div class="px-6 py-3 flex" style="flex-wrap: wrap;">
<div class="shrink-0 text-sm font-medium text-gray-500" style="width: 140px;">비고</div>
<div class="text-sm text-gray-900" x-text="salaryData.notes || '-'"></div>
</div>
</div>
<div class="px-6 py-3 flex" style="flex-wrap: wrap;">
<div class="shrink-0 text-sm font-medium text-gray-500" style="width: 140px;">적용일</div>
<div class="text-sm text-gray-900" x-text="salaryData.effective_date || '-'"></div>
</div>
<div class="px-6 py-3 flex" style="flex-wrap: wrap;">
<div class="shrink-0 text-sm font-medium text-gray-500" style="width: 140px;">비고</div>
<div class="text-sm text-gray-900" x-text="salaryData.notes || '-'"></div>
</div>
{{-- 급여 산정 테이블 (저장된 데이터가 있을 ) --}}
<template x-if="salaryData.annual_salary && salaryData.fixed_overtime_hours != null">
<div class="border-t border-gray-200">
<div class="px-6 py-3 bg-blue-50">
<span class="text-sm font-semibold text-blue-700">급여 산정 내역</span>
</div>
{{-- 요약 테이블 --}}
<div class="px-6 pt-4">
<div class="overflow-x-auto">
<table class="w-full text-sm border border-gray-300">
<thead>
<tr class="bg-gray-100 text-gray-600">
<th class="border border-gray-300 px-3 py-2 text-center font-medium">총급여(연봉)</th>
<th class="border border-gray-300 px-3 py-2 text-center font-medium">기본급</th>
<th class="border border-gray-300 px-3 py-2 text-center font-medium">식대</th>
<th class="border border-gray-300 px-3 py-2 text-center font-medium">고정연장근로수당</th>
<th class="border border-gray-300 px-3 py-2 text-center font-medium bg-blue-50">합계()</th>
</tr>
</thead>
<tbody>
<tr class="text-gray-900">
<td class="border border-gray-300 px-3 py-2 text-right" x-text="Number(salaryData.annual_salary).toLocaleString()"></td>
<td class="border border-gray-300 px-3 py-2 text-right font-medium" x-text="Number(salaryData.base_salary || 0).toLocaleString()"></td>
<td class="border border-gray-300 px-3 py-2 text-right" x-text="Number(salaryData.meal_allowance || 200000).toLocaleString()"></td>
<td class="border border-gray-300 px-3 py-2 text-right font-medium" x-text="Number(salaryData.fixed_overtime_pay || 0).toLocaleString()"></td>
<td class="border border-gray-300 px-3 py-2 text-right font-bold bg-blue-50" x-text="Number(salaryData.monthly_salary || 0).toLocaleString()"></td>
</tr>
</tbody>
</table>
</div>
</div>
{{-- 상세 산정 내역 --}}
<div class="px-6 py-4">
<div class="overflow-x-auto">
<table class="w-full text-sm border border-gray-300">
<tbody>
<tr>
<td class="border border-gray-300 px-3 py-2 bg-gray-50 font-medium text-gray-700" style="width: 170px;">월급여</td>
<td class="border border-gray-300 px-3 py-2 text-right" style="width: 130px;" x-text="Number(salaryData.monthly_salary || 0).toLocaleString()"></td>
<td class="border border-gray-300 px-3 py-2 text-gray-500 text-xs"></td>
</tr>
<tr>
<td class="border border-gray-300 px-3 py-2 bg-gray-50 font-medium text-gray-700">기본급</td>
<td class="border border-gray-300 px-3 py-2 text-right" x-text="Number(salaryData.base_salary || 0).toLocaleString()"></td>
<td class="border border-gray-300 px-3 py-2 text-gray-500 text-xs"></td>
</tr>
<tr>
<td class="border border-gray-300 px-3 py-2 bg-gray-50 font-medium text-gray-700">식대</td>
<td class="border border-gray-300 px-3 py-2 text-right" x-text="Number(salaryData.meal_allowance || 200000).toLocaleString()"></td>
<td class="border border-gray-300 px-3 py-2 text-gray-500 text-xs">고정값</td>
</tr>
<tr>
<td class="border border-gray-300 px-3 py-2 bg-gray-50 font-medium text-gray-700"> 근로시간</td>
<td class="border border-gray-300 px-3 py-2 text-right" x-text="salaryData.monthly_work_hours || 209"></td>
<td class="border border-gray-300 px-3 py-2 text-gray-500 text-xs">고정값</td>
</tr>
<tr>
<td class="border border-gray-300 px-3 py-2 bg-gray-50 font-medium text-gray-700"> 고정연장근로시간</td>
<td class="border border-gray-300 px-3 py-2 text-right font-medium text-blue-700" x-text="salaryData.fixed_overtime_hours"></td>
<td class="border border-gray-300 px-3 py-2 text-gray-500 text-xs">변동값</td>
</tr>
<tr>
<td class="border border-gray-300 px-3 py-2 bg-gray-50 font-medium text-gray-700">통상시급</td>
<td class="border border-gray-300 px-3 py-2 text-right" x-text="Number(salaryData.hourly_wage || 0).toLocaleString()"></td>
<td class="border border-gray-300 px-3 py-2 text-gray-500 text-xs">(기본급+식대) / 209, 연차수당 계산시 적용시급</td>
</tr>
<tr>
<td class="border border-gray-300 px-3 py-2 bg-gray-50 font-medium text-gray-700">연장근로수당배수</td>
<td class="border border-gray-300 px-3 py-2 text-right" x-text="salaryData.overtime_multiplier || 1.5"></td>
<td class="border border-gray-300 px-3 py-2 text-gray-500 text-xs"></td>
</tr>
<tr class="bg-amber-50">
<td class="border border-gray-300 px-3 py-2 font-semibold text-gray-800" style="background: rgb(254 243 199);">고정연장근로수당</td>
<td class="border border-gray-300 px-3 py-2 text-right font-bold" x-text="Number(salaryData.fixed_overtime_pay || 0).toLocaleString()"></td>
<td class="border border-gray-300 px-3 py-2 text-gray-500 text-xs">통상시급 x 월고정연장근로시간 x 연장근로수당배수</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</template>
</div>
{{-- ============================================================ --}}
{{-- 수정 모드 --}}
{{-- ============================================================ --}}
<div x-show="editing" class="p-6 space-y-4" x-cloak>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">연봉 ()</label>
<input type="text" inputmode="numeric"
:value="formatNumber(form.annual_salary)"
@input="form.annual_salary = parseNumber($event.target.value); $event.target.value = formatNumber(form.annual_salary)"
placeholder="예: 50,000,000"
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
<p class="text-xs text-gray-400 mt-1" x-show="form.annual_salary > 0"
x-text="'월 약 ' + Math.round(form.annual_salary / 12).toLocaleString() + '원'"></p>
{{-- 입력 필드 --}}
<div class="grid gap-4" style="grid-template-columns: repeat(3, 1fr);">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">연봉 ()</label>
<input type="text" inputmode="numeric"
:value="formatNumber(form.annual_salary)"
@input="form.annual_salary = parseNumber($event.target.value); $event.target.value = formatNumber(form.annual_salary)"
placeholder="예: 50,000,000"
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">식대 ()</label>
<input type="text" inputmode="numeric"
:value="formatNumber(form.meal_allowance)"
@input="form.meal_allowance = parseNumber($event.target.value); $event.target.value = formatNumber(form.meal_allowance)"
placeholder="200,000"
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
<p class="text-xs text-gray-400 mt-1">비과세 한도: 200,000</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1"> 고정연장근로시간</label>
<input type="number" x-model.number="form.fixed_overtime_hours"
placeholder="예: 22"
min="0" max="100" step="1"
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
<p class="text-xs text-gray-400 mt-1">변동값 (사원별 상이)</p>
</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">적용일</label>
<input type="date" x-model="form.effective_date"
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">비고</label>
<textarea x-model="form.notes" rows="2" maxlength="500" placeholder="메모 (선택)"
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"></textarea>
<div class="grid gap-4" style="grid-template-columns: 1fr 2fr;">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">적용일</label>
<input type="date" x-model="form.effective_date"
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">비고</label>
<textarea x-model="form.notes" rows="1" maxlength="500" placeholder="메모 (선택)"
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"></textarea>
</div>
</div>
{{-- 실시간 급여 산정 미리보기 --}}
<template x-if="form.annual_salary > 0">
<div class="border border-blue-200 rounded-lg overflow-hidden">
<div class="px-4 py-2 bg-blue-50 text-sm font-semibold text-blue-700">급여 산정 미리보기</div>
<div class="p-4 space-y-3">
{{-- 요약 테이블 --}}
<div class="overflow-x-auto">
<table class="w-full text-sm border border-gray-300">
<thead>
<tr class="bg-gray-100 text-gray-600">
<th class="border border-gray-300 px-3 py-2 text-center font-medium">총급여(연봉)</th>
<th class="border border-gray-300 px-3 py-2 text-center font-medium">기본급</th>
<th class="border border-gray-300 px-3 py-2 text-center font-medium">식대</th>
<th class="border border-gray-300 px-3 py-2 text-center font-medium">고정연장근로수당</th>
<th class="border border-gray-300 px-3 py-2 text-center font-medium bg-blue-50">합계()</th>
</tr>
</thead>
<tbody>
<tr class="text-gray-900">
<td class="border border-gray-300 px-3 py-2 text-right" x-text="formatNumber(form.annual_salary)"></td>
<td class="border border-gray-300 px-3 py-2 text-right font-medium" x-text="formatNumber(calcBaseSalary)"></td>
<td class="border border-gray-300 px-3 py-2 text-right" x-text="formatNumber(form.meal_allowance)"></td>
<td class="border border-gray-300 px-3 py-2 text-right font-medium" x-text="formatNumber(calcFixedOvertimePay)"></td>
<td class="border border-gray-300 px-3 py-2 text-right font-bold bg-blue-50" x-text="formatNumber(calcMonthlySalary)"></td>
</tr>
</tbody>
</table>
</div>
{{-- 상세 산정 내역 --}}
<div class="overflow-x-auto">
<table class="w-full text-sm border border-gray-300">
<tbody>
<tr>
<td class="border border-gray-300 px-3 py-2 bg-gray-50 font-medium text-gray-700" style="width: 170px;">월급여</td>
<td class="border border-gray-300 px-3 py-2 text-right" style="width: 130px;" x-text="formatNumber(calcMonthlySalary)"></td>
<td class="border border-gray-300 px-3 py-2 text-gray-500 text-xs"></td>
</tr>
<tr>
<td class="border border-gray-300 px-3 py-2 bg-gray-50 font-medium text-gray-700">기본급</td>
<td class="border border-gray-300 px-3 py-2 text-right" x-text="formatNumber(calcBaseSalary)"></td>
<td class="border border-gray-300 px-3 py-2 text-gray-500 text-xs"></td>
</tr>
<tr>
<td class="border border-gray-300 px-3 py-2 bg-gray-50 font-medium text-gray-700">식대</td>
<td class="border border-gray-300 px-3 py-2 text-right" x-text="formatNumber(form.meal_allowance)"></td>
<td class="border border-gray-300 px-3 py-2 text-gray-500 text-xs">고정값</td>
</tr>
<tr>
<td class="border border-gray-300 px-3 py-2 bg-gray-50 font-medium text-gray-700"> 근로시간</td>
<td class="border border-gray-300 px-3 py-2 text-right">209</td>
<td class="border border-gray-300 px-3 py-2 text-gray-500 text-xs">고정값</td>
</tr>
<tr>
<td class="border border-gray-300 px-3 py-2 bg-gray-50 font-medium text-gray-700"> 고정연장근로시간</td>
<td class="border border-gray-300 px-3 py-2 text-right font-medium text-blue-700" x-text="form.fixed_overtime_hours || 0"></td>
<td class="border border-gray-300 px-3 py-2 text-gray-500 text-xs">변동값</td>
</tr>
<tr>
<td class="border border-gray-300 px-3 py-2 bg-gray-50 font-medium text-gray-700">통상시급</td>
<td class="border border-gray-300 px-3 py-2 text-right" x-text="formatNumber(calcHourlyWage)"></td>
<td class="border border-gray-300 px-3 py-2 text-gray-500 text-xs">(기본급+식대) / 209, 연차수당 계산시 적용시급</td>
</tr>
<tr>
<td class="border border-gray-300 px-3 py-2 bg-gray-50 font-medium text-gray-700">연장근로수당배수</td>
<td class="border border-gray-300 px-3 py-2 text-right">1.5</td>
<td class="border border-gray-300 px-3 py-2 text-gray-500 text-xs"></td>
</tr>
<tr class="bg-amber-50">
<td class="border border-gray-300 px-3 py-2 font-semibold text-gray-800" style="background: rgb(254 243 199);">고정연장근로수당</td>
<td class="border border-gray-300 px-3 py-2 text-right font-bold" x-text="formatNumber(calcFixedOvertimePay)"></td>
<td class="border border-gray-300 px-3 py-2 text-gray-500 text-xs">통상시급 x 월고정연장근로시간 x 연장근로수당배수</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</template>
{{-- 버튼 --}}
<div class="flex justify-end gap-2 pt-2">
<button type="button" @click="editing = false"
class="px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors text-sm">
@@ -84,7 +278,9 @@ class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-
</div>
</div>
{{-- ============================================================ --}}
{{-- 연봉 변경 이력 --}}
{{-- ============================================================ --}}
<template x-if="salaryData.history && salaryData.history.length > 0">
<div class="border-t border-gray-200">
<div class="px-6 py-3 bg-gray-50">
@@ -96,6 +292,7 @@ class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-
<thead>
<tr class="border-b border-gray-200 text-gray-500">
<th class="text-left py-2 pr-3 font-medium">연봉</th>
<th class="text-left py-2 pr-3 font-medium">고정OT시간</th>
<th class="text-left py-2 pr-3 font-medium">적용일</th>
<th class="text-left py-2 pr-3 font-medium">비고</th>
<th class="text-left py-2 pr-3 font-medium">기록일</th>
@@ -107,6 +304,7 @@ class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-
<template x-for="(item, idx) in [...salaryData.history].reverse()" :key="idx">
<tr class="text-gray-900">
<td class="py-2 pr-3 font-medium" x-text="item.annual_salary ? Number(item.annual_salary).toLocaleString() + '원' : '-'"></td>
<td class="py-2 pr-3" x-text="item.fixed_overtime_hours != null ? item.fixed_overtime_hours + 'h' : '-'"></td>
<td class="py-2 pr-3" x-text="item.effective_date || '-'"></td>
<td class="py-2 pr-3" x-text="item.notes || '-'"></td>
<td class="py-2 pr-3 text-gray-500" x-text="item.recorded_at || '-'"></td>
@@ -134,17 +332,47 @@ class="inline-flex items-center justify-center w-7 h-7 rounded text-red-400 hove
@push('scripts')
<script>
function salaryManager() {
const MONTHLY_WORK_HOURS = 209;
const OVERTIME_MULTIPLIER = 1.5;
const initialData = @json($salaryInfo);
return {
editing: false,
saving: false,
salaryData: initialData,
form: {
annual_salary: initialData.annual_salary || '',
meal_allowance: initialData.meal_allowance ?? 200000,
fixed_overtime_hours: initialData.fixed_overtime_hours ?? '',
effective_date: initialData.effective_date || '',
notes: initialData.notes || '',
},
// === 계산 프로퍼티 (실시간 미리보기용) ===
get calcMonthlySalary() {
if (!this.form.annual_salary) return 0;
return Math.round(this.form.annual_salary / 12);
},
get calcOtFactor() {
return (this.form.fixed_overtime_hours || 0) * OVERTIME_MULTIPLIER;
},
get calcBasePlusMeal() {
if (!this.calcMonthlySalary) return 0;
return Math.round(this.calcMonthlySalary * MONTHLY_WORK_HOURS / (MONTHLY_WORK_HOURS + this.calcOtFactor));
},
get calcBaseSalary() {
return this.calcBasePlusMeal - (this.form.meal_allowance || 0);
},
get calcHourlyWage() {
if (!this.calcBasePlusMeal) return 0;
return Math.floor(this.calcBasePlusMeal / MONTHLY_WORK_HOURS);
},
get calcFixedOvertimePay() {
if (!this.calcMonthlySalary) return 0;
return this.calcMonthlySalary - this.calcBaseSalary - (this.form.meal_allowance || 0);
},
// === 유틸리티 ===
formatNumber(val) {
if (!val && val !== 0) return '';
return Number(val).toLocaleString();
@@ -154,6 +382,47 @@ function salaryManager() {
return isNaN(num) ? '' : num;
},
// === API 호출 ===
async saveSalary() {
this.saving = true;
try {
const res = await fetch('{{ route("api.admin.hr.employees.salary.update", $employee->id) }}', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': '{{ csrf_token() }}',
'Accept': 'application/json',
'X-Requested-With': 'XMLHttpRequest',
},
body: JSON.stringify({
annual_salary: this.form.annual_salary ? parseInt(this.form.annual_salary) : null,
meal_allowance: this.form.meal_allowance ? parseInt(this.form.meal_allowance) : 200000,
fixed_overtime_hours: this.form.fixed_overtime_hours !== '' ? parseInt(this.form.fixed_overtime_hours) : null,
effective_date: this.form.effective_date || null,
notes: this.form.notes || null,
}),
});
const json = await res.json();
if (json.success) {
this.salaryData = json.data;
this.form.annual_salary = json.data.annual_salary || '';
this.form.meal_allowance = json.data.meal_allowance ?? 200000;
this.form.fixed_overtime_hours = json.data.fixed_overtime_hours ?? '';
this.form.effective_date = json.data.effective_date || '';
this.form.notes = json.data.notes || '';
this.editing = false;
showToast(json.message, 'success');
} else {
showToast(json.message || '저장에 실패했습니다.', 'error');
}
} catch (e) {
showToast('연봉 정보 저장 중 오류가 발생했습니다.', 'error');
} finally {
this.saving = false;
}
},
async deleteHistory(originalIndex) {
if (!confirm('이 연봉 이력을 삭제하시겠습니까?')) return;
try {
@@ -176,42 +445,6 @@ function salaryManager() {
showToast('이력 삭제 중 오류가 발생했습니다.', 'error');
}
},
async saveSalary() {
this.saving = true;
try {
const res = await fetch('{{ route("api.admin.hr.employees.salary.update", $employee->id) }}', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': '{{ csrf_token() }}',
'Accept': 'application/json',
'X-Requested-With': 'XMLHttpRequest',
},
body: JSON.stringify({
annual_salary: this.form.annual_salary ? parseInt(this.form.annual_salary) : null,
effective_date: this.form.effective_date || null,
notes: this.form.notes || null,
}),
});
const json = await res.json();
if (json.success) {
this.salaryData = json.data;
this.form.annual_salary = json.data.annual_salary || '';
this.form.effective_date = json.data.effective_date || '';
this.form.notes = json.data.notes || '';
this.editing = false;
showToast(json.message, 'success');
} else {
showToast(json.message || '저장에 실패했습니다.', 'error');
}
} catch (e) {
showToast('연봉 정보 저장 중 오류가 발생했습니다.', 'error');
} finally {
this.saving = false;
}
},
};
}
</script>