feat: 근태관리/직원관리 API 구현

- AttendanceController, AttendanceService 추가
- EmployeeController, EmployeeService 추가
- Attendance 모델 및 마이그레이션 추가
- TenantUserProfile에 employee_status 컬럼 추가
- DepartmentService 트리 조회 기능 개선
- Swagger 문서 추가 (AttendanceApi, EmployeeApi)
- API 라우트 등록
This commit is contained in:
2025-12-09 20:27:44 +09:00
parent 33010f1916
commit f1f4c52c31
24 changed files with 2844 additions and 27 deletions

View File

@@ -13,6 +13,8 @@
use App\Http\Controllers\Api\V1\ClientGroupController;
use App\Http\Controllers\Api\V1\CommonController;
use App\Http\Controllers\Api\V1\DepartmentController;
use App\Http\Controllers\Api\V1\EmployeeController;
use App\Http\Controllers\Api\V1\AttendanceController;
use App\Http\Controllers\Api\V1\Design\AuditLogController as DesignAuditLogController;
use App\Http\Controllers\Api\V1\Design\BomCalculationController;
use App\Http\Controllers\Api\V1\Design\BomTemplateController as DesignBomTemplateController;
@@ -30,6 +32,9 @@
use App\Http\Controllers\Api\V1\ItemMaster\ItemSectionController;
use App\Http\Controllers\Api\V1\ItemMaster\SectionTemplateController;
use App\Http\Controllers\Api\V1\ItemMaster\UnitOptionController;
use App\Http\Controllers\Api\V1\ItemsBomController;
use App\Http\Controllers\Api\V1\ItemsController;
use App\Http\Controllers\Api\V1\ItemsFileController;
use App\Http\Controllers\Api\V1\MaterialController;
use App\Http\Controllers\Api\V1\MenuController;
use App\Http\Controllers\Api\V1\ModelSetController;
@@ -194,6 +199,7 @@
Route::prefix('departments')->group(function () {
Route::get('', [DepartmentController::class, 'index'])->name('v1.departments.index'); // 목록
Route::post('', [DepartmentController::class, 'store'])->name('v1.departments.store'); // 생성
Route::get('/tree', [DepartmentController::class, 'tree'])->name('v1.departments.tree'); // 트리
Route::get('/{id}', [DepartmentController::class, 'show'])->name('v1.departments.show'); // 단건
Route::patch('/{id}', [DepartmentController::class, 'update'])->name('v1.departments.update'); // 수정
Route::delete('/{id}', [DepartmentController::class, 'destroy'])->name('v1.departments.destroy'); // 삭제(soft)
@@ -210,6 +216,31 @@
Route::delete('/{id}/permissions/{permission}', [DepartmentController::class, 'revokePermissions'])->name('v1.departments.permissions.revoke'); // 권한 제거(해당 메뉴 범위까지)
});
// Employee API (사원 관리)
Route::prefix('employees')->group(function () {
Route::get('', [EmployeeController::class, 'index'])->name('v1.employees.index');
Route::post('', [EmployeeController::class, 'store'])->name('v1.employees.store');
Route::get('/stats', [EmployeeController::class, 'stats'])->name('v1.employees.stats');
Route::get('/{id}', [EmployeeController::class, 'show'])->name('v1.employees.show');
Route::patch('/{id}', [EmployeeController::class, 'update'])->name('v1.employees.update');
Route::delete('/{id}', [EmployeeController::class, 'destroy'])->name('v1.employees.destroy');
Route::post('/bulk-delete', [EmployeeController::class, 'bulkDelete'])->name('v1.employees.bulkDelete');
Route::post('/{id}/create-account', [EmployeeController::class, 'createAccount'])->name('v1.employees.createAccount');
});
// Attendance API (근태 관리)
Route::prefix('attendances')->group(function () {
Route::get('', [AttendanceController::class, 'index'])->name('v1.attendances.index');
Route::post('', [AttendanceController::class, 'store'])->name('v1.attendances.store');
Route::get('/monthly-stats', [AttendanceController::class, 'monthlyStats'])->name('v1.attendances.monthlyStats');
Route::post('/check-in', [AttendanceController::class, 'checkIn'])->name('v1.attendances.checkIn');
Route::post('/check-out', [AttendanceController::class, 'checkOut'])->name('v1.attendances.checkOut');
Route::get('/{id}', [AttendanceController::class, 'show'])->name('v1.attendances.show');
Route::patch('/{id}', [AttendanceController::class, 'update'])->name('v1.attendances.update');
Route::delete('/{id}', [AttendanceController::class, 'destroy'])->name('v1.attendances.destroy');
Route::post('/bulk-delete', [AttendanceController::class, 'bulkDelete'])->name('v1.attendances.bulkDelete');
});
// Permission API
Route::prefix('permissions')->group(function () {
Route::get('departments/{dept_id}/menu-matrix', [PermissionController::class, 'deptMenuMatrix'])->name('v1.permissions.deptMenuMatrix'); // 부서별 권한 메트릭스
@@ -408,33 +439,33 @@
// Items (통합 품목 조회 - materials + products UNION)
Route::prefix('items')->group(function () {
Route::get('', [\App\Http\Controllers\Api\V1\ItemsController::class, 'index'])->name('v1.items.index'); // 통합 목록
Route::post('', [\App\Http\Controllers\Api\V1\ItemsController::class, 'store'])->name('v1.items.store'); // 품목 생성
Route::get('/code/{code}', [\App\Http\Controllers\Api\V1\ItemsController::class, 'showByCode'])->name('v1.items.show_by_code'); // code 기반 조회
Route::get('/{id}', [\App\Http\Controllers\Api\V1\ItemsController::class, 'show'])->name('v1.items.show'); // 단건 (item_type 파라미터 필수)
Route::put('/{id}', [\App\Http\Controllers\Api\V1\ItemsController::class, 'update'])->name('v1.items.update'); // 품목 수정
Route::delete('/batch', [\App\Http\Controllers\Api\V1\ItemsController::class, 'batchDestroy'])->name('v1.items.batch_destroy'); // 품목 일괄 삭제
Route::delete('/{id}', [\App\Http\Controllers\Api\V1\ItemsController::class, 'destroy'])->name('v1.items.destroy'); // 품목 삭제
Route::get('', [ItemsController::class, 'index'])->name('v1.items.index'); // 통합 목록
Route::post('', [ItemsController::class, 'store'])->name('v1.items.store'); // 품목 생성
Route::get('/code/{code}', [ItemsController::class, 'showByCode'])->name('v1.items.show_by_code'); // code 기반 조회
Route::get('/{id}', [ItemsController::class, 'show'])->name('v1.items.show'); // 단건 (item_type 파라미터 필수)
Route::put('/{id}', [ItemsController::class, 'update'])->name('v1.items.update'); // 품목 수정
Route::delete('/batch', [ItemsController::class, 'batchDestroy'])->name('v1.items.batch_destroy'); // 품목 일괄 삭제
Route::delete('/{id}', [ItemsController::class, 'destroy'])->name('v1.items.destroy'); // 품목 삭제
});
// Items BOM (ID-based BOM API)
Route::prefix('items/{id}/bom')->group(function () {
Route::get('', [\App\Http\Controllers\Api\V1\ItemsBomController::class, 'index'])->name('v1.items.bom.index'); // BOM 목록 (flat)
Route::get('/tree', [\App\Http\Controllers\Api\V1\ItemsBomController::class, 'tree'])->name('v1.items.bom.tree'); // BOM 트리 (계층)
Route::post('', [\App\Http\Controllers\Api\V1\ItemsBomController::class, 'store'])->name('v1.items.bom.store'); // BOM 추가 (bulk)
Route::put('/{lineId}', [\App\Http\Controllers\Api\V1\ItemsBomController::class, 'update'])->name('v1.items.bom.update'); // BOM 수정
Route::delete('/{lineId}', [\App\Http\Controllers\Api\V1\ItemsBomController::class, 'destroy'])->name('v1.items.bom.destroy'); // BOM 삭제
Route::get('/summary', [\App\Http\Controllers\Api\V1\ItemsBomController::class, 'summary'])->name('v1.items.bom.summary'); // BOM 요약
Route::get('/validate', [\App\Http\Controllers\Api\V1\ItemsBomController::class, 'validate'])->name('v1.items.bom.validate'); // BOM 검증
Route::post('/replace', [\App\Http\Controllers\Api\V1\ItemsBomController::class, 'replace'])->name('v1.items.bom.replace'); // BOM 전체 교체
Route::post('/reorder', [\App\Http\Controllers\Api\V1\ItemsBomController::class, 'reorder'])->name('v1.items.bom.reorder'); // BOM 정렬
Route::get('/categories', [\App\Http\Controllers\Api\V1\ItemsBomController::class, 'listCategories'])->name('v1.items.bom.categories'); // 카테고리 목록
Route::get('', [ItemsBomController::class, 'index'])->name('v1.items.bom.index'); // BOM 목록 (flat)
Route::get('/tree', [ItemsBomController::class, 'tree'])->name('v1.items.bom.tree'); // BOM 트리 (계층)
Route::post('', [ItemsBomController::class, 'store'])->name('v1.items.bom.store'); // BOM 추가 (bulk)
Route::put('/{lineId}', [ItemsBomController::class, 'update'])->name('v1.items.bom.update'); // BOM 수정
Route::delete('/{lineId}', [ItemsBomController::class, 'destroy'])->name('v1.items.bom.destroy'); // BOM 삭제
Route::get('/summary', [ItemsBomController::class, 'summary'])->name('v1.items.bom.summary'); // BOM 요약
Route::get('/validate', [ItemsBomController::class, 'validate'])->name('v1.items.bom.validate'); // BOM 검증
Route::post('/replace', [ItemsBomController::class, 'replace'])->name('v1.items.bom.replace'); // BOM 전체 교체
Route::post('/reorder', [ItemsBomController::class, 'reorder'])->name('v1.items.bom.reorder'); // BOM 정렬
Route::get('/categories', [ItemsBomController::class, 'listCategories'])->name('v1.items.bom.categories'); // 카테고리 목록
});
// Items Files (ID-based File Upload API)
Route::prefix('items/{id}/files')->group(function () {
Route::post('', [\App\Http\Controllers\Api\V1\ItemsFileController::class, 'upload'])->name('v1.items.files.upload'); // 파일 업로드
Route::delete('/{type}', [\App\Http\Controllers\Api\V1\ItemsFileController::class, 'delete'])->name('v1.items.files.delete'); // 파일 삭제 (type: bending_diagram|specification|certification)
Route::post('', [ItemsFileController::class, 'upload'])->name('v1.items.files.upload'); // 파일 업로드
Route::delete('/{type}', [ItemsFileController::class, 'delete'])->name('v1.items.files.delete'); // 파일 삭제 (type: bending_diagram|specification|certification)
});
// BOM (product_components: ref_type=PRODUCT|MATERIAL)