feat: [hr] 사원등록 기능 확장
- 기본정보에 주민등록번호 필드 추가 - 급여이체정보 섹션 추가 (이체은행, 예금주, 계좌번호) - 부양가족 정보 섹션 추가 (동적 행 추가/삭제) - 첨부파일 업로드/다운로드/삭제 기능 추가 - 은행 목록 config/banks.php 설정 파일 생성 - show 페이지 주민등록번호 뒷자리 마스킹 처리
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 직급/직책 추가
|
||||
*/
|
||||
|
||||
@@ -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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
28
config/banks.php
Normal file
28
config/banks.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'KB' => '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' => '한국수출입은행',
|
||||
];
|
||||
@@ -125,6 +125,15 @@ class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:rin
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- 주민등록번호 --}}
|
||||
<div>
|
||||
<label for="resident_number" class="block text-sm font-medium text-gray-700 mb-1">주민등록번호</label>
|
||||
<input type="text" name="resident_number" id="resident_number"
|
||||
placeholder="000000-0000000"
|
||||
maxlength="14"
|
||||
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="border-b border-gray-200 pb-4 mb-4 mt-8">
|
||||
<h2 class="text-lg font-semibold text-gray-700">근무 정보</h2>
|
||||
@@ -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">
|
||||
</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="bank_account_bank_code" class="block text-sm font-medium text-gray-700 mb-1">이체은행</label>
|
||||
<select name="bank_account[bank_code]" id="bank_account_bank_code"
|
||||
onchange="this.form['bank_account[bank_name]'].value = this.options[this.selectedIndex].text !== '선택하세요' ? this.options[this.selectedIndex].text : ''"
|
||||
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(config('banks', []) as $code => $name)
|
||||
<option value="{{ $code }}">{{ $name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<input type="hidden" name="bank_account[bank_name]" value="">
|
||||
</div>
|
||||
|
||||
<div class="flex gap-4" style="flex-wrap: wrap;">
|
||||
<div style="flex: 1 1 200px;">
|
||||
<label for="bank_account_account_holder" class="block text-sm font-medium text-gray-700 mb-1">예금주</label>
|
||||
<input type="text" name="bank_account[account_holder]" id="bank_account_account_holder"
|
||||
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 style="flex: 1 1 200px;">
|
||||
<label for="bank_account_account_number" class="block text-sm font-medium text-gray-700 mb-1">계좌번호</label>
|
||||
<input type="text" name="bank_account[account_number]" id="bank_account_account_number"
|
||||
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>
|
||||
|
||||
{{-- 부양가족 정보 --}}
|
||||
<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 x-data="dependentsManager()">
|
||||
<template x-for="(dep, index) in dependents" :key="index">
|
||||
<div class="border border-gray-200 rounded-lg p-4 mb-3 relative">
|
||||
<button type="button" @click="removeDependent(index)"
|
||||
class="absolute top-2 right-2 text-red-400 hover:text-red-600 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="M6 18L18 6M6 6l12 12"/>
|
||||
</svg>
|
||||
</button>
|
||||
<div class="text-xs font-medium text-gray-500 mb-2" x-text="'부양가족 ' + (index + 1)"></div>
|
||||
<div class="flex gap-3 mb-2" style="flex-wrap: wrap;">
|
||||
<div style="flex: 1 1 120px;">
|
||||
<input type="text" :name="'dependents['+index+'][name]'" x-model="dep.name"
|
||||
placeholder="이름" class="w-full px-2 py-1.5 border border-gray-300 rounded text-sm focus:ring-1 focus:ring-blue-500">
|
||||
</div>
|
||||
<div style="flex: 0 0 100px;">
|
||||
<select :name="'dependents['+index+'][nationality]'" x-model="dep.nationality"
|
||||
class="w-full px-2 py-1.5 border border-gray-300 rounded text-sm focus:ring-1 focus:ring-blue-500">
|
||||
<option value="korean">내국인</option>
|
||||
<option value="foreigner">외국인</option>
|
||||
</select>
|
||||
</div>
|
||||
<div style="flex: 1 1 150px;">
|
||||
<input type="text" :name="'dependents['+index+'][resident_number]'" x-model="dep.resident_number"
|
||||
placeholder="주민등록번호" maxlength="14" class="w-full px-2 py-1.5 border border-gray-300 rounded text-sm focus:ring-1 focus:ring-blue-500">
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-3 items-center" style="flex-wrap: wrap;">
|
||||
<div style="flex: 0 0 100px;">
|
||||
<select :name="'dependents['+index+'][relationship]'" x-model="dep.relationship"
|
||||
class="w-full px-2 py-1.5 border border-gray-300 rounded text-sm focus:ring-1 focus:ring-blue-500">
|
||||
<option value="">관계</option>
|
||||
<option value="spouse">배우자</option>
|
||||
<option value="child">자녀</option>
|
||||
<option value="parent">부모</option>
|
||||
<option value="sibling">형제자매</option>
|
||||
<option value="grandparent">조부모</option>
|
||||
<option value="other">기타</option>
|
||||
</select>
|
||||
</div>
|
||||
<label class="inline-flex items-center gap-1 text-sm text-gray-600 cursor-pointer">
|
||||
<input type="hidden" :name="'dependents['+index+'][is_disabled]'" value="0">
|
||||
<input type="checkbox" :name="'dependents['+index+'][is_disabled]'" x-model="dep.is_disabled" value="1"
|
||||
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500">
|
||||
장애인
|
||||
</label>
|
||||
<label class="inline-flex items-center gap-1 text-sm text-gray-600 cursor-pointer">
|
||||
<input type="hidden" :name="'dependents['+index+'][is_dependent]'" value="0">
|
||||
<input type="checkbox" :name="'dependents['+index+'][is_dependent]'" x-model="dep.is_dependent" value="1"
|
||||
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500">
|
||||
피부양자적용
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<button type="button" @click="addDependent()"
|
||||
class="w-full py-2 border-2 border-dashed border-gray-300 rounded-lg text-sm text-gray-500 hover:border-blue-400 hover:text-blue-600 transition-colors">
|
||||
+ 부양가족 추가
|
||||
</button>
|
||||
</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 class="bg-gray-50 border border-gray-200 rounded-lg p-4">
|
||||
<p class="text-sm text-gray-500">사원 등록 후 상세/수정 페이지에서 파일을 업로드할 수 있습니다.</p>
|
||||
</div>
|
||||
|
||||
{{-- 버튼 --}}
|
||||
<div class="flex justify-end gap-3 pt-4 border-t">
|
||||
<a href="{{ route('hr.employees.index') }}"
|
||||
@@ -307,6 +424,21 @@ function userSearch() {
|
||||
};
|
||||
}
|
||||
|
||||
function dependentsManager() {
|
||||
return {
|
||||
dependents: [],
|
||||
addDependent() {
|
||||
this.dependents.push({
|
||||
name: '', nationality: 'korean', resident_number: '',
|
||||
relationship: '', is_disabled: false, is_dependent: false
|
||||
});
|
||||
},
|
||||
removeDependent(index) {
|
||||
this.dependents.splice(index, 1);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
document.body.addEventListener('htmx:afterRequest', function(event) {
|
||||
if (event.detail.elt.id !== 'employeeForm') return;
|
||||
|
||||
|
||||
@@ -66,6 +66,16 @@ class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:rin
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- 주민등록번호 --}}
|
||||
<div>
|
||||
<label for="resident_number" class="block text-sm font-medium text-gray-700 mb-1">주민등록번호</label>
|
||||
<input type="text" name="resident_number" id="resident_number"
|
||||
value="{{ $employee->resident_number }}"
|
||||
placeholder="000000-0000000"
|
||||
maxlength="14"
|
||||
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="border-b border-gray-200 pb-4 mb-4 mt-8">
|
||||
<h2 class="text-lg font-semibold text-gray-700">근무 정보</h2>
|
||||
@@ -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">
|
||||
</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>
|
||||
|
||||
@php $bankAccount = $employee->bank_account ?? []; @endphp
|
||||
|
||||
<div>
|
||||
<label for="bank_account_bank_code" class="block text-sm font-medium text-gray-700 mb-1">이체은행</label>
|
||||
<select name="bank_account[bank_code]" id="bank_account_bank_code"
|
||||
onchange="this.form['bank_account[bank_name]'].value = this.options[this.selectedIndex].text !== '선택하세요' ? this.options[this.selectedIndex].text : ''"
|
||||
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(config('banks', []) as $code => $name)
|
||||
<option value="{{ $code }}" {{ ($bankAccount['bank_code'] ?? '') === $code ? 'selected' : '' }}>{{ $name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<input type="hidden" name="bank_account[bank_name]" value="{{ $bankAccount['bank_name'] ?? '' }}">
|
||||
</div>
|
||||
|
||||
<div class="flex gap-4" style="flex-wrap: wrap;">
|
||||
<div style="flex: 1 1 200px;">
|
||||
<label for="bank_account_account_holder" class="block text-sm font-medium text-gray-700 mb-1">예금주</label>
|
||||
<input type="text" name="bank_account[account_holder]" id="bank_account_account_holder"
|
||||
value="{{ $bankAccount['account_holder'] ?? '' }}"
|
||||
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 style="flex: 1 1 200px;">
|
||||
<label for="bank_account_account_number" class="block text-sm font-medium text-gray-700 mb-1">계좌번호</label>
|
||||
<input type="text" name="bank_account[account_number]" id="bank_account_account_number"
|
||||
value="{{ $bankAccount['account_number'] ?? '' }}"
|
||||
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>
|
||||
|
||||
{{-- 부양가족 정보 --}}
|
||||
<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 x-data="dependentsManager()">
|
||||
<template x-for="(dep, index) in dependents" :key="index">
|
||||
<div class="border border-gray-200 rounded-lg p-4 mb-3 relative">
|
||||
<button type="button" @click="removeDependent(index)"
|
||||
class="absolute top-2 right-2 text-red-400 hover:text-red-600 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="M6 18L18 6M6 6l12 12"/>
|
||||
</svg>
|
||||
</button>
|
||||
<div class="text-xs font-medium text-gray-500 mb-2" x-text="'부양가족 ' + (index + 1)"></div>
|
||||
<div class="flex gap-3 mb-2" style="flex-wrap: wrap;">
|
||||
<div style="flex: 1 1 120px;">
|
||||
<input type="text" :name="'dependents['+index+'][name]'" x-model="dep.name"
|
||||
placeholder="이름" class="w-full px-2 py-1.5 border border-gray-300 rounded text-sm focus:ring-1 focus:ring-blue-500">
|
||||
</div>
|
||||
<div style="flex: 0 0 100px;">
|
||||
<select :name="'dependents['+index+'][nationality]'" x-model="dep.nationality"
|
||||
class="w-full px-2 py-1.5 border border-gray-300 rounded text-sm focus:ring-1 focus:ring-blue-500">
|
||||
<option value="korean">내국인</option>
|
||||
<option value="foreigner">외국인</option>
|
||||
</select>
|
||||
</div>
|
||||
<div style="flex: 1 1 150px;">
|
||||
<input type="text" :name="'dependents['+index+'][resident_number]'" x-model="dep.resident_number"
|
||||
placeholder="주민등록번호" maxlength="14" class="w-full px-2 py-1.5 border border-gray-300 rounded text-sm focus:ring-1 focus:ring-blue-500">
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-3 items-center" style="flex-wrap: wrap;">
|
||||
<div style="flex: 0 0 100px;">
|
||||
<select :name="'dependents['+index+'][relationship]'" x-model="dep.relationship"
|
||||
class="w-full px-2 py-1.5 border border-gray-300 rounded text-sm focus:ring-1 focus:ring-blue-500">
|
||||
<option value="">관계</option>
|
||||
<option value="spouse">배우자</option>
|
||||
<option value="child">자녀</option>
|
||||
<option value="parent">부모</option>
|
||||
<option value="sibling">형제자매</option>
|
||||
<option value="grandparent">조부모</option>
|
||||
<option value="other">기타</option>
|
||||
</select>
|
||||
</div>
|
||||
<label class="inline-flex items-center gap-1 text-sm text-gray-600 cursor-pointer">
|
||||
<input type="hidden" :name="'dependents['+index+'][is_disabled]'" value="0">
|
||||
<input type="checkbox" :name="'dependents['+index+'][is_disabled]'" x-model="dep.is_disabled" value="1"
|
||||
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500">
|
||||
장애인
|
||||
</label>
|
||||
<label class="inline-flex items-center gap-1 text-sm text-gray-600 cursor-pointer">
|
||||
<input type="hidden" :name="'dependents['+index+'][is_dependent]'" value="0">
|
||||
<input type="checkbox" :name="'dependents['+index+'][is_dependent]'" x-model="dep.is_dependent" value="1"
|
||||
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500">
|
||||
피부양자적용
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<button type="button" @click="addDependent()"
|
||||
class="w-full py-2 border-2 border-dashed border-gray-300 rounded-lg text-sm text-gray-500 hover:border-blue-400 hover:text-blue-600 transition-colors">
|
||||
+ 부양가족 추가
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{{-- 버튼 --}}
|
||||
<div class="flex justify-end gap-3 pt-4 border-t">
|
||||
<div class="flex justify-end gap-3 pt-4 border-t mt-6">
|
||||
<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">
|
||||
취소
|
||||
@@ -185,6 +299,57 @@ class="px-6 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{{-- 첨부파일 --}}
|
||||
<div class="bg-white rounded-lg shadow-sm p-6 mt-6" x-data="fileUploader()">
|
||||
<div class="border-b border-gray-200 pb-4 mb-4">
|
||||
<h2 class="text-lg font-semibold text-gray-700">첨부파일</h2>
|
||||
</div>
|
||||
|
||||
{{-- 기존 파일 목록 --}}
|
||||
<div id="file-list" class="space-y-2 mb-4">
|
||||
@forelse($files ?? [] as $file)
|
||||
<div class="flex items-center justify-between bg-gray-50 rounded-lg px-4 py-2" id="file-row-{{ $file->id }}">
|
||||
<div class="flex items-center gap-2 min-w-0">
|
||||
<svg class="w-4 h-4 text-gray-400 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.172 7l-6.586 6.586a2 2 0 102.828 2.828l6.414-6.586a4 4 0 00-5.656-5.656l-6.415 6.585a6 6 0 108.486 8.486L20.5 13"/>
|
||||
</svg>
|
||||
<a href="{{ route('api.admin.hr.employees.download-file', [$employee->id, $file->id]) }}"
|
||||
class="text-sm text-blue-600 hover:text-blue-800 truncate" title="{{ $file->original_name }}">
|
||||
{{ $file->original_name }}
|
||||
</a>
|
||||
<span class="text-xs text-gray-400 shrink-0">{{ number_format(($file->file_size ?? 0) / 1024, 0) }}KB</span>
|
||||
</div>
|
||||
<button type="button" @click="deleteFile({{ $file->id }})"
|
||||
class="text-red-400 hover:text-red-600 shrink-0 ml-2" title="삭제">
|
||||
<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="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
@empty
|
||||
<p class="text-sm text-gray-400" id="no-files-msg">등록된 파일이 없습니다.</p>
|
||||
@endforelse
|
||||
</div>
|
||||
|
||||
{{-- 파일 업로드 영역 --}}
|
||||
<div class="border-2 border-dashed border-gray-300 rounded-lg p-6 text-center cursor-pointer hover:border-blue-400 transition-colors"
|
||||
@dragover.prevent="dragover = true"
|
||||
@dragleave.prevent="dragover = false"
|
||||
@drop.prevent="handleDrop($event)"
|
||||
:class="dragover ? 'border-blue-400 bg-blue-50' : ''"
|
||||
@click="$refs.fileInput.click()">
|
||||
<input type="file" multiple x-ref="fileInput" @change="handleFiles($event)" class="hidden">
|
||||
<svg class="mx-auto w-8 h-8 text-gray-400 mb-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"/>
|
||||
</svg>
|
||||
<p class="text-sm text-gray-500">파일을 드래그하거나 클릭하여 업로드</p>
|
||||
<p class="text-xs text-gray-400 mt-1">파일당 최대 20MB</p>
|
||||
</div>
|
||||
|
||||
{{-- 업로드 진행 상태 --}}
|
||||
<div x-show="uploading" class="mt-3 text-sm text-blue-600">업로드 중...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- 직급/직책 추가 모달 --}}
|
||||
@@ -193,6 +358,98 @@ class="px-6 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
function dependentsManager() {
|
||||
return {
|
||||
dependents: @json($employee->dependents ?? []),
|
||||
addDependent() {
|
||||
this.dependents.push({
|
||||
name: '', nationality: 'korean', resident_number: '',
|
||||
relationship: '', is_disabled: false, is_dependent: false
|
||||
});
|
||||
},
|
||||
removeDependent(index) {
|
||||
this.dependents.splice(index, 1);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function fileUploader() {
|
||||
return {
|
||||
dragover: false,
|
||||
uploading: false,
|
||||
|
||||
async handleFiles(event) {
|
||||
await this.upload(event.target.files);
|
||||
event.target.value = '';
|
||||
},
|
||||
|
||||
async handleDrop(event) {
|
||||
this.dragover = false;
|
||||
await this.upload(event.dataTransfer.files);
|
||||
},
|
||||
|
||||
async upload(files) {
|
||||
if (!files.length) return;
|
||||
this.uploading = true;
|
||||
|
||||
const formData = new FormData();
|
||||
for (const file of files) {
|
||||
formData.append('files[]', file);
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch('{{ route("api.admin.hr.employees.upload-file", $employee->id) }}', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': '{{ csrf_token() }}',
|
||||
'Accept': 'application/json',
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
},
|
||||
body: formData
|
||||
});
|
||||
const json = await res.json();
|
||||
|
||||
if (json.success) {
|
||||
showToast(json.message, 'success');
|
||||
location.reload();
|
||||
} else {
|
||||
showToast(json.message || '업로드 실패', 'error');
|
||||
}
|
||||
} catch (e) {
|
||||
showToast('파일 업로드 중 오류가 발생했습니다.', 'error');
|
||||
} finally {
|
||||
this.uploading = false;
|
||||
}
|
||||
},
|
||||
|
||||
async deleteFile(fileId) {
|
||||
if (!confirm('이 파일을 삭제하시겠습니까?')) return;
|
||||
|
||||
try {
|
||||
const res = await fetch('{{ url("/api/admin/hr/employees") }}/{{ $employee->id }}/files/' + fileId, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': '{{ csrf_token() }}',
|
||||
'Accept': 'application/json',
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
}
|
||||
});
|
||||
const json = await res.json();
|
||||
|
||||
if (json.success) {
|
||||
showToast(json.message, 'success');
|
||||
const row = document.getElementById('file-row-' + fileId);
|
||||
if (row) row.remove();
|
||||
} else {
|
||||
showToast(json.message || '삭제 실패', 'error');
|
||||
}
|
||||
} catch (e) {
|
||||
showToast('파일 삭제 중 오류가 발생했습니다.', 'error');
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
document.body.addEventListener('htmx:afterRequest', function(event) {
|
||||
if (event.detail.elt.id !== 'employeeForm') return;
|
||||
|
||||
|
||||
@@ -85,6 +85,16 @@ class="inline-flex items-center gap-2 px-4 py-2 bg-blue-600 hover:bg-blue-700 te
|
||||
<div class="shrink-0 text-sm font-medium text-gray-500" style="width: 140px;">연락처</div>
|
||||
<div class="text-sm text-gray-900">{{ $employee->user?->phone ?? '-' }}</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">
|
||||
@if($employee->resident_number)
|
||||
{{ Str::mask($employee->resident_number, '*', 8) }}
|
||||
@else
|
||||
-
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- 근무 정보 --}}
|
||||
<div class="px-6 py-4 bg-gray-50">
|
||||
@@ -142,6 +152,105 @@ class="inline-flex items-center gap-2 px-4 py-2 bg-blue-600 hover:bg-blue-700 te
|
||||
<div class="shrink-0 text-sm font-medium text-gray-500" style="width: 140px;">수정일</div>
|
||||
<div class="text-sm text-gray-900">{{ $employee->updated_at?->format('Y-m-d H:i') ?? '-' }}</div>
|
||||
</div>
|
||||
|
||||
{{-- 급여이체정보 --}}
|
||||
<div class="px-6 py-4 bg-gray-50">
|
||||
<span class="text-sm font-semibold text-gray-600">급여이체정보</span>
|
||||
</div>
|
||||
@php $bankAccount = $employee->bank_account ?? []; @endphp
|
||||
<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">{{ $bankAccount['bank_name'] ?? '-' }}</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">{{ $bankAccount['account_holder'] ?? '-' }}</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">{{ $bankAccount['account_number'] ?? '-' }}</div>
|
||||
</div>
|
||||
|
||||
{{-- 부양가족 정보 --}}
|
||||
<div class="px-6 py-4 bg-gray-50">
|
||||
<span class="text-sm font-semibold text-gray-600">부양가족 정보</span>
|
||||
</div>
|
||||
@php $dependents = $employee->dependents ?? []; @endphp
|
||||
@if(count($dependents) > 0)
|
||||
<div class="px-6 py-3">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm">
|
||||
<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">내/외국인</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>
|
||||
<th class="text-left py-2 font-medium">피부양자</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-100">
|
||||
@foreach($dependents as $dep)
|
||||
<tr class="text-gray-900">
|
||||
<td class="py-2 pr-3">{{ $dep['name'] ?? '-' }}</td>
|
||||
<td class="py-2 pr-3">{{ ($dep['nationality'] ?? 'korean') === 'korean' ? '내국인' : '외국인' }}</td>
|
||||
<td class="py-2 pr-3">
|
||||
@if(!empty($dep['resident_number']))
|
||||
{{ Str::mask($dep['resident_number'], '*', 8) }}
|
||||
@else
|
||||
-
|
||||
@endif
|
||||
</td>
|
||||
<td class="py-2 pr-3">
|
||||
@switch($dep['relationship'] ?? '')
|
||||
@case('spouse') 배우자 @break
|
||||
@case('child') 자녀 @break
|
||||
@case('parent') 부모 @break
|
||||
@case('sibling') 형제자매 @break
|
||||
@case('grandparent') 조부모 @break
|
||||
@case('other') 기타 @break
|
||||
@default - @break
|
||||
@endswitch
|
||||
</td>
|
||||
<td class="py-2 pr-3">{{ !empty($dep['is_disabled']) ? 'Y' : 'N' }}</td>
|
||||
<td class="py-2">{{ !empty($dep['is_dependent']) ? 'Y' : 'N' }}</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<div class="px-6 py-3">
|
||||
<p class="text-sm text-gray-400">등록된 부양가족이 없습니다.</p>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- 첨부파일 --}}
|
||||
<div class="px-6 py-4 bg-gray-50">
|
||||
<span class="text-sm font-semibold text-gray-600">첨부파일</span>
|
||||
</div>
|
||||
@if(!empty($files) && count($files) > 0)
|
||||
<div class="px-6 py-3 space-y-2">
|
||||
@foreach($files as $file)
|
||||
<div class="flex items-center gap-2">
|
||||
<svg class="w-4 h-4 text-gray-400 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.172 7l-6.586 6.586a2 2 0 102.828 2.828l6.414-6.586a4 4 0 00-5.656-5.656l-6.415 6.585a6 6 0 108.486 8.486L20.5 13"/>
|
||||
</svg>
|
||||
<a href="{{ route('api.admin.hr.employees.download-file', [$employee->id, $file->id]) }}"
|
||||
class="text-sm text-blue-600 hover:text-blue-800">
|
||||
{{ $file->original_name }}
|
||||
</a>
|
||||
<span class="text-xs text-gray-400">{{ number_format(($file->file_size ?? 0) / 1024, 0) }}KB</span>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@else
|
||||
<div class="px-6 py-3">
|
||||
<p class="text-sm text-gray-400">등록된 파일이 없습니다.</p>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1048,6 +1048,11 @@
|
||||
Route::get('/{id}', [\App\Http\Controllers\Api\Admin\HR\EmployeeController::class, 'show'])->name('show');
|
||||
Route::put('/{id}', [\App\Http\Controllers\Api\Admin\HR\EmployeeController::class, 'update'])->name('update');
|
||||
Route::delete('/{id}', [\App\Http\Controllers\Api\Admin\HR\EmployeeController::class, 'destroy'])->name('destroy');
|
||||
|
||||
// 첨부파일
|
||||
Route::post('/{id}/files', [\App\Http\Controllers\Api\Admin\HR\EmployeeController::class, 'uploadFile'])->name('upload-file');
|
||||
Route::delete('/{id}/files/{fileId}', [\App\Http\Controllers\Api\Admin\HR\EmployeeController::class, 'deleteFile'])->name('delete-file');
|
||||
Route::get('/{id}/files/{fileId}/download', [\App\Http\Controllers\Api\Admin\HR\EmployeeController::class, 'downloadFile'])->name('download-file');
|
||||
});
|
||||
|
||||
// 직급/직책 관리 API
|
||||
|
||||
Reference in New Issue
Block a user