diff --git a/app/Http/Controllers/Api/Admin/HR/EmployeeController.php b/app/Http/Controllers/Api/Admin/HR/EmployeeController.php index 1de66c26..b1f22c13 100644 --- a/app/Http/Controllers/Api/Admin/HR/EmployeeController.php +++ b/app/Http/Controllers/Api/Admin/HR/EmployeeController.php @@ -3,10 +3,13 @@ namespace App\Http\Controllers\Api\Admin\HR; use App\Http\Controllers\Controller; +use App\Models\Boards\File; use App\Services\HR\EmployeeService; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Http\Response; +use Illuminate\Support\Facades\Storage; +use Illuminate\Support\Str; class EmployeeController extends Controller { @@ -88,6 +91,18 @@ public function store(Request $request): JsonResponse 'resign_date' => 'nullable|date', 'address' => 'nullable|string|max:200', 'emergency_contact' => 'nullable|string|max:100', + 'resident_number' => 'nullable|string|max:14', + 'bank_account.bank_code' => 'nullable|string|max:20', + 'bank_account.bank_name' => 'nullable|string|max:50', + 'bank_account.account_holder' => 'nullable|string|max:50', + 'bank_account.account_number' => 'nullable|string|max:30', + 'dependents' => 'nullable|array', + 'dependents.*.name' => 'required_with:dependents|string|max:50', + 'dependents.*.nationality' => 'nullable|string|in:korean,foreigner', + 'dependents.*.resident_number' => 'nullable|string|max:14', + 'dependents.*.relationship' => 'nullable|string|max:20', + 'dependents.*.is_disabled' => 'nullable|boolean', + 'dependents.*.is_dependent' => 'nullable|boolean', ]; // 신규 사용자일 때만 이메일 unique 검증 @@ -157,6 +172,18 @@ public function update(Request $request, int $id): JsonResponse 'resign_date' => 'nullable|date', 'address' => 'nullable|string|max:200', 'emergency_contact' => 'nullable|string|max:100', + 'resident_number' => 'nullable|string|max:14', + 'bank_account.bank_code' => 'nullable|string|max:20', + 'bank_account.bank_name' => 'nullable|string|max:50', + 'bank_account.account_holder' => 'nullable|string|max:50', + 'bank_account.account_number' => 'nullable|string|max:30', + 'dependents' => 'nullable|array', + 'dependents.*.name' => 'required_with:dependents|string|max:50', + 'dependents.*.nationality' => 'nullable|string|in:korean,foreigner', + 'dependents.*.resident_number' => 'nullable|string|max:14', + 'dependents.*.relationship' => 'nullable|string|max:20', + 'dependents.*.is_disabled' => 'nullable|boolean', + 'dependents.*.is_dependent' => 'nullable|boolean', ]); try { @@ -228,6 +255,101 @@ public function destroy(Request $request, int $id): JsonResponse|Response } } + /** + * 사원 첨부파일 업로드 + */ + public function uploadFile(Request $request, int $id): JsonResponse + { + $employee = $this->employeeService->getEmployeeById($id); + if (! $employee) { + return response()->json(['success' => false, 'message' => '사원 정보를 찾을 수 없습니다.'], 404); + } + + $request->validate([ + 'files' => 'required|array|max:10', + 'files.*' => 'file|max:20480', + ]); + + $tenantId = session('selected_tenant_id'); + $uploaded = []; + + foreach ($request->file('files') as $file) { + $storedName = Str::random(40).'.'.$file->getClientOriginalExtension(); + $path = "tenants/{$tenantId}/employees/{$employee->id}"; + $file->storeAs($path, $storedName, 'tenant'); + + $fileRecord = File::create([ + 'tenant_id' => $tenantId, + 'document_id' => $employee->id, + 'document_type' => 'employee_profile', + 'original_name' => $file->getClientOriginalName(), + 'stored_name' => $storedName, + 'file_path' => $path.'/'.$storedName, + 'mime_type' => $file->getMimeType(), + 'file_size' => $file->getSize(), + 'file_type' => strtolower($file->getClientOriginalExtension()), + 'uploaded_by' => auth()->id(), + 'created_by' => auth()->id(), + ]); + + $uploaded[] = $fileRecord; + } + + return response()->json([ + 'success' => true, + 'message' => count($uploaded).'개 파일이 업로드되었습니다.', + 'data' => $uploaded, + ], 201); + } + + /** + * 사원 첨부파일 삭제 + */ + public function deleteFile(int $id, int $fileId): JsonResponse + { + $tenantId = session('selected_tenant_id'); + + $file = File::where('id', $fileId) + ->where('document_id', $id) + ->where('document_type', 'employee_profile') + ->where('tenant_id', $tenantId) + ->first(); + + if (! $file) { + return response()->json(['success' => false, 'message' => '파일을 찾을 수 없습니다.'], 404); + } + + Storage::disk('tenant')->delete($file->file_path); + $file->delete(); + + return response()->json(['success' => true, 'message' => '파일이 삭제되었습니다.']); + } + + /** + * 사원 첨부파일 다운로드 + */ + public function downloadFile(int $id, int $fileId) + { + $tenantId = session('selected_tenant_id'); + + $file = File::where('id', $fileId) + ->where('document_id', $id) + ->where('document_type', 'employee_profile') + ->where('tenant_id', $tenantId) + ->first(); + + if (! $file) { + abort(404, '파일을 찾을 수 없습니다.'); + } + + $disk = Storage::disk('tenant'); + if (! $disk->exists($file->file_path)) { + abort(404, '파일이 서버에 존재하지 않습니다.'); + } + + return $disk->download($file->file_path, $file->original_name); + } + /** * 직급/직책 추가 */ diff --git a/app/Http/Controllers/HR/EmployeeController.php b/app/Http/Controllers/HR/EmployeeController.php index 73bc42bc..50a35108 100644 --- a/app/Http/Controllers/HR/EmployeeController.php +++ b/app/Http/Controllers/HR/EmployeeController.php @@ -3,6 +3,7 @@ namespace App\Http\Controllers\HR; use App\Http\Controllers\Controller; +use App\Models\Boards\File; use App\Services\HR\EmployeeService; use Illuminate\Contracts\View\View; @@ -39,6 +40,7 @@ public function create(): View 'departments' => $departments, 'ranks' => $ranks, 'titles' => $titles, + 'banks' => config('banks', []), ]); } @@ -53,8 +55,15 @@ public function show(int $id): View abort(404, '사원 정보를 찾을 수 없습니다.'); } + $files = File::where('document_type', 'employee_profile') + ->where('document_id', $employee->id) + ->where('tenant_id', session('selected_tenant_id')) + ->orderBy('created_at', 'desc') + ->get(); + return view('hr.employees.show', [ 'employee' => $employee, + 'files' => $files, ]); } @@ -73,11 +82,19 @@ public function edit(int $id): View $ranks = $this->employeeService->getPositions('rank'); $titles = $this->employeeService->getPositions('title'); + $files = File::where('document_type', 'employee_profile') + ->where('document_id', $employee->id) + ->where('tenant_id', session('selected_tenant_id')) + ->orderBy('created_at', 'desc') + ->get(); + return view('hr.employees.edit', [ 'employee' => $employee, 'departments' => $departments, 'ranks' => $ranks, 'titles' => $titles, + 'banks' => config('banks', []), + 'files' => $files, ]); } } diff --git a/app/Models/HR/Employee.php b/app/Models/HR/Employee.php index 9662e013..1a6a8633 100644 --- a/app/Models/HR/Employee.php +++ b/app/Models/HR/Employee.php @@ -87,6 +87,21 @@ public function getEmergencyContactAttribute(): ?string return $this->json_extra['emergency_contact'] ?? null; } + public function getResidentNumberAttribute(): ?string + { + return $this->json_extra['resident_number'] ?? null; + } + + public function getBankAccountAttribute(): ?array + { + return $this->json_extra['bank_account'] ?? null; + } + + public function getDependentsAttribute(): array + { + return $this->json_extra['dependents'] ?? []; + } + public function getPositionLabelAttribute(): ?string { if (! $this->position_key || ! $this->tenant_id) { diff --git a/app/Services/HR/EmployeeService.php b/app/Services/HR/EmployeeService.php index 69184f50..ac404342 100644 --- a/app/Services/HR/EmployeeService.php +++ b/app/Services/HR/EmployeeService.php @@ -205,17 +205,28 @@ public function createEmployee(array $data): Employee // json_extra 구성 $jsonExtra = []; - if (! empty($data['hire_date'])) { - $jsonExtra['hire_date'] = $data['hire_date']; + foreach (['hire_date', 'resign_date', 'address', 'emergency_contact', 'resident_number'] as $key) { + if (! empty($data[$key])) { + $jsonExtra[$key] = $data[$key]; + } } - if (! empty($data['resign_date'])) { - $jsonExtra['resign_date'] = $data['resign_date']; + + // 급여이체정보 (bank_account 객체) + if (! empty($data['bank_account']) && is_array($data['bank_account'])) { + $bankAccount = array_filter($data['bank_account'], fn ($v) => $v !== null && $v !== ''); + if (! empty($bankAccount)) { + $jsonExtra['bank_account'] = $bankAccount; + } } - if (! empty($data['address'])) { - $jsonExtra['address'] = $data['address']; - } - if (! empty($data['emergency_contact'])) { - $jsonExtra['emergency_contact'] = $data['emergency_contact']; + + // 부양가족 정보 (dependents 배열) + if (! empty($data['dependents']) && is_array($data['dependents'])) { + $dependents = array_values(array_filter($data['dependents'], function ($dep) { + return ! empty($dep['name']); + })); + if (! empty($dependents)) { + $jsonExtra['dependents'] = $dependents; + } } // Employee(TenantUserProfile) 생성 @@ -259,8 +270,8 @@ public function updateEmployee(int $id, array $data): ?Employee 'display_name' => $data['display_name'] ?? null, ], fn ($v) => $v !== null); - // json_extra 업데이트 - $jsonExtraKeys = ['hire_date', 'resign_date', 'address', 'emergency_contact', 'salary', 'bank_account']; + // json_extra 업데이트 (스칼라 값) + $jsonExtraKeys = ['hire_date', 'resign_date', 'address', 'emergency_contact', 'salary', 'resident_number']; $extra = $employee->json_extra ?? []; foreach ($jsonExtraKeys as $key) { if (array_key_exists($key, $data)) { @@ -271,6 +282,37 @@ public function updateEmployee(int $id, array $data): ?Employee } } } + + // 급여이체정보 (bank_account 객체) + if (array_key_exists('bank_account', $data)) { + if (! empty($data['bank_account']) && is_array($data['bank_account'])) { + $bankAccount = array_filter($data['bank_account'], fn ($v) => $v !== null && $v !== ''); + if (! empty($bankAccount)) { + $extra['bank_account'] = $bankAccount; + } else { + unset($extra['bank_account']); + } + } else { + unset($extra['bank_account']); + } + } + + // 부양가족 정보 (dependents 배열) + if (array_key_exists('dependents', $data)) { + if (! empty($data['dependents']) && is_array($data['dependents'])) { + $dependents = array_values(array_filter($data['dependents'], function ($dep) { + return ! empty($dep['name']); + })); + if (! empty($dependents)) { + $extra['dependents'] = $dependents; + } else { + unset($extra['dependents']); + } + } else { + unset($extra['dependents']); + } + } + $updateData['json_extra'] = ! empty($extra) ? $extra : null; $employee->update($updateData); diff --git a/config/banks.php b/config/banks.php new file mode 100644 index 00000000..82647ec4 --- /dev/null +++ b/config/banks.php @@ -0,0 +1,28 @@ + 'KB국민은행', + 'SHINHAN' => '신한은행', + 'WOORI' => '우리은행', + 'HANA' => '하나은행', + 'NH' => 'NH농협은행', + 'IBK' => 'IBK기업은행', + 'SC' => 'SC제일은행', + 'CITI' => '한국씨티은행', + 'KAKAO' => '카카오뱅크', + 'TOSS' => '토스뱅크', + 'KBANK' => '케이뱅크', + 'SUHYUP' => '수협은행', + 'BNK_BUSAN' => 'BNK부산은행', + 'BNK_GYEONGNAM' => 'BNK경남은행', + 'DGB' => 'DGB대구은행', + 'KWANGJU' => '광주은행', + 'JEONBUK' => '전북은행', + 'JEJU' => '제주은행', + 'SAEMAUL' => '새마을금고', + 'SHINHYUP' => '신협', + 'POST' => '우체국', + 'SANLIM' => '산림조합', + 'KDB' => 'KDB산업은행', + 'EXIMBANK' => '한국수출입은행', +]; diff --git a/resources/views/hr/employees/create.blade.php b/resources/views/hr/employees/create.blade.php index b4b8e644..fa9d3192 100644 --- a/resources/views/hr/employees/create.blade.php +++ b/resources/views/hr/employees/create.blade.php @@ -125,6 +125,15 @@ class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:rin + {{-- 주민등록번호 --}} +
+ + +
+ {{-- 근무 정보 --}}

근무 정보

@@ -223,6 +232,114 @@ class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:rin class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
+ {{-- 급여이체정보 --}} +
+

급여이체정보

+
+ +
+ + + +
+ +
+
+ + +
+
+ + +
+
+ + {{-- 부양가족 정보 --}} +
+

부양가족 정보

+
+ +
+ + + +
+ + {{-- 첨부파일 안내 --}} +
+

첨부파일

+
+
+

사원 등록 후 상세/수정 페이지에서 파일을 업로드할 수 있습니다.

+
+ {{-- 버튼 --}}
주민등록번호 + +
+ {{-- 근무 정보 --}}

근무 정보

@@ -172,8 +182,112 @@ class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:rin class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
+ {{-- 급여이체정보 --}} +
+

급여이체정보

+
+ + @php $bankAccount = $employee->bank_account ?? []; @endphp + +
+ + + +
+ +
+
+ + +
+
+ + +
+
+ + {{-- 부양가족 정보 --}} +
+

부양가족 정보

+
+ +
+ + + +
+ {{-- 버튼 --}} -
+
취소 @@ -185,6 +299,57 @@ class="px-6 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-
+ + {{-- 첨부파일 --}} +
+
+

첨부파일

+
+ + {{-- 기존 파일 목록 --}} +
+ @forelse($files ?? [] as $file) +
+
+ + + + + {{ $file->original_name }} + + {{ number_format(($file->file_size ?? 0) / 1024, 0) }}KB +
+ +
+ @empty +

등록된 파일이 없습니다.

+ @endforelse +
+ + {{-- 파일 업로드 영역 --}} +
+ + + + +

파일을 드래그하거나 클릭하여 업로드

+

파일당 최대 20MB

+
+ + {{-- 업로드 진행 상태 --}} +
업로드 중...
+
{{-- 직급/직책 추가 모달 --}} @@ -193,6 +358,98 @@ class="px-6 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition- @push('scripts')