- API 컨트롤러 store/update/destroy에 try-catch 추가 - debug 모드에서 상세 에러 메시지 포함 응답 - create/edit 뷰에 showToast 기반 에러 표시 추가 - 422 validation 에러 필드별 메시지 표시 - 500 서버 에러 시 사용자 친화적 메시지 표시
245 lines
13 KiB
PHP
245 lines
13 KiB
PHP
@extends('layouts.app')
|
|
|
|
@section('title', '사원 수정')
|
|
|
|
@section('content')
|
|
<div class="container mx-auto px-4 py-6 max-w-2xl">
|
|
{{-- 페이지 헤더 --}}
|
|
<div class="mb-6">
|
|
<a href="{{ route('hr.employees.index') }}" class="text-sm text-gray-500 hover:text-gray-700 inline-flex items-center gap-1 mb-2">
|
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/>
|
|
</svg>
|
|
사원 목록으로
|
|
</a>
|
|
<h1 class="text-2xl font-bold text-gray-800">사원 수정</h1>
|
|
</div>
|
|
|
|
{{-- 수정 폼 --}}
|
|
<div class="bg-white rounded-lg shadow-sm p-6">
|
|
<form id="employeeForm"
|
|
hx-put="{{ route('api.admin.hr.employees.update', $employee->id) }}"
|
|
hx-headers='{"X-CSRF-TOKEN": "{{ csrf_token() }}", "Accept": "application/json"}'
|
|
hx-target="#form-message"
|
|
hx-swap="innerHTML"
|
|
class="space-y-6">
|
|
|
|
<div id="form-message"></div>
|
|
|
|
{{-- 기본 정보 --}}
|
|
<div class="border-b border-gray-200 pb-4 mb-4">
|
|
<h2 class="text-lg font-semibold text-gray-700">기본 정보</h2>
|
|
</div>
|
|
|
|
{{-- 이름 --}}
|
|
<div>
|
|
<label for="name" class="block text-sm font-medium text-gray-700 mb-1">
|
|
이름 <span class="text-red-500">*</span>
|
|
</label>
|
|
<input type="text" name="name" id="name" required
|
|
value="{{ $employee->user?->name }}"
|
|
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 for="display_name" class="block text-sm font-medium text-gray-700 mb-1">표시 이름</label>
|
|
<input type="text" name="display_name" id="display_name"
|
|
value="{{ $employee->display_name }}"
|
|
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">
|
|
</div>
|
|
|
|
{{-- 사번 --}}
|
|
<div>
|
|
<label for="employee_code" class="block text-sm font-medium text-gray-700 mb-1">사번</label>
|
|
<input type="text" name="employee_code" id="employee_code"
|
|
value="{{ $employee->employee_code }}"
|
|
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 class="flex gap-4" style="flex-wrap: wrap;">
|
|
<div style="flex: 1 1 200px;">
|
|
<label for="email" class="block text-sm font-medium text-gray-700 mb-1">이메일</label>
|
|
<input type="email" name="email" id="email"
|
|
value="{{ $employee->user?->email }}"
|
|
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 style="flex: 1 1 200px;">
|
|
<label for="phone" class="block text-sm font-medium text-gray-700 mb-1">연락처</label>
|
|
<input type="text" name="phone" id="phone"
|
|
value="{{ $employee->user?->phone }}"
|
|
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>
|
|
|
|
{{-- 근무 정보 --}}
|
|
<div class="border-b border-gray-200 pb-4 mb-4 mt-8">
|
|
<h2 class="text-lg font-semibold text-gray-700">근무 정보</h2>
|
|
</div>
|
|
|
|
{{-- 부서 --}}
|
|
<div>
|
|
<label for="department_id" class="block text-sm font-medium text-gray-700 mb-1">부서</label>
|
|
<select name="department_id" id="department_id"
|
|
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
|
<option value="">선택하세요</option>
|
|
@foreach($departments as $dept)
|
|
<option value="{{ $dept->id }}" {{ $employee->department_id == $dept->id ? 'selected' : '' }}>
|
|
{{ $dept->name }}
|
|
</option>
|
|
@endforeach
|
|
</select>
|
|
</div>
|
|
|
|
{{-- 직급 / 직책 --}}
|
|
<div class="flex gap-4" style="flex-wrap: wrap;">
|
|
<div style="flex: 1 1 200px;">
|
|
<label class="block text-sm font-medium text-gray-700 mb-1">직급</label>
|
|
<div class="flex gap-2">
|
|
<select name="position_key" id="position_key"
|
|
class="flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
|
<option value="">선택하세요</option>
|
|
@foreach($ranks as $rank)
|
|
<option value="{{ $rank->key }}" {{ $employee->position_key === $rank->key ? 'selected' : '' }}>
|
|
{{ $rank->name }}
|
|
</option>
|
|
@endforeach
|
|
</select>
|
|
<button type="button" onclick="openPositionModal('rank')"
|
|
class="shrink-0 w-9 h-9 flex items-center justify-center border border-gray-300 rounded-lg text-gray-500 hover:bg-blue-50 hover:text-blue-600 hover:border-blue-300 transition-colors"
|
|
title="직급 추가">
|
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div style="flex: 1 1 200px;">
|
|
<label class="block text-sm font-medium text-gray-700 mb-1">직책</label>
|
|
<div class="flex gap-2">
|
|
<select name="job_title_key" id="job_title_key"
|
|
class="flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
|
<option value="">선택하세요</option>
|
|
@foreach($titles as $title)
|
|
<option value="{{ $title->key }}" {{ $employee->job_title_key === $title->key ? 'selected' : '' }}>
|
|
{{ $title->name }}
|
|
</option>
|
|
@endforeach
|
|
</select>
|
|
<button type="button" onclick="openPositionModal('title')"
|
|
class="shrink-0 w-9 h-9 flex items-center justify-center border border-gray-300 rounded-lg text-gray-500 hover:bg-blue-50 hover:text-blue-600 hover:border-blue-300 transition-colors"
|
|
title="직책 추가">
|
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{{-- 입사일 / 상태 --}}
|
|
<div class="flex gap-4" style="flex-wrap: wrap;">
|
|
<div style="flex: 1 1 200px;">
|
|
<label for="hire_date" class="block text-sm font-medium text-gray-700 mb-1">입사일</label>
|
|
<input type="date" name="hire_date" id="hire_date"
|
|
value="{{ $employee->hire_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 style="flex: 1 1 200px;">
|
|
<label for="employee_status" class="block text-sm font-medium text-gray-700 mb-1">재직상태</label>
|
|
<select name="employee_status" id="employee_status"
|
|
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
|
<option value="active" {{ $employee->employee_status === 'active' ? 'selected' : '' }}>재직</option>
|
|
<option value="leave" {{ $employee->employee_status === 'leave' ? 'selected' : '' }}>휴직</option>
|
|
<option value="resigned" {{ $employee->employee_status === 'resigned' ? 'selected' : '' }}>퇴직</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
{{-- 주소 --}}
|
|
<div>
|
|
<label for="address" class="block text-sm font-medium text-gray-700 mb-1">주소</label>
|
|
<input type="text" name="address" id="address"
|
|
value="{{ $employee->address }}"
|
|
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 for="emergency_contact" class="block text-sm font-medium text-gray-700 mb-1">비상연락처</label>
|
|
<input type="text" name="emergency_contact" id="emergency_contact"
|
|
value="{{ $employee->emergency_contact }}"
|
|
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 class="flex justify-end gap-3 pt-4 border-t">
|
|
<a href="{{ route('hr.employees.show', $employee->id) }}"
|
|
class="px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors">
|
|
취소
|
|
</a>
|
|
<button type="submit"
|
|
class="px-6 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors">
|
|
저장
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
{{-- 직급/직책 추가 모달 --}}
|
|
@include('hr.employees.partials.position-add-modal')
|
|
@endsection
|
|
|
|
@push('scripts')
|
|
<script>
|
|
document.body.addEventListener('htmx:afterRequest', function(event) {
|
|
if (event.detail.elt.id !== 'employeeForm') return;
|
|
|
|
const xhr = event.detail.xhr;
|
|
try {
|
|
const response = JSON.parse(xhr.responseText);
|
|
|
|
if (response.success) {
|
|
showToast(response.message || '사원 정보가 수정되었습니다.', 'success');
|
|
window.location.href = '{{ route('hr.employees.show', $employee->id) }}';
|
|
return;
|
|
}
|
|
|
|
let msg = response.message || '저장에 실패했습니다.';
|
|
if (response.error) msg += '\n' + response.error;
|
|
showToast(msg, 'error', 5000);
|
|
} catch (e) {
|
|
if (!event.detail.successful) {
|
|
showToast('서버 오류가 발생했습니다. (HTTP ' + xhr.status + ')', 'error', 5000);
|
|
}
|
|
}
|
|
});
|
|
|
|
document.body.addEventListener('htmx:responseError', function(event) {
|
|
if (event.detail.elt.id !== 'employeeForm') return;
|
|
|
|
const xhr = event.detail.xhr;
|
|
try {
|
|
const response = JSON.parse(xhr.responseText);
|
|
|
|
if (xhr.status === 422 && response.errors) {
|
|
const messages = [];
|
|
for (const field in response.errors) {
|
|
messages.push(response.errors[field].join(', '));
|
|
}
|
|
showToast(messages.join('\n'), 'error', 6000);
|
|
} else {
|
|
let msg = response.message || '오류가 발생했습니다.';
|
|
if (response.error) msg += '\n' + response.error;
|
|
showToast(msg, 'error', 5000);
|
|
}
|
|
} catch (e) {
|
|
showToast('서버 오류가 발생했습니다. (HTTP ' + xhr.status + ')', 'error', 5000);
|
|
}
|
|
});
|
|
</script>
|
|
@endpush
|