Files
sam-api/routes/api/v1/hr.php
김보곤 3fd412f89d feat: [approval] 결재관리 시스템 MNG 스타일로 전면 개선
- 보류/보류해제 기능 추가 (hold, releaseHold)
- 전결 기능 추가 (preDecide - 이후 결재 건너뛰고 최종 승인)
- 복사 재기안 기능 추가 (copyForRedraft)
- 반려 후 재상신 로직 (rejection_history 저장, resubmit_count 증가)
- 결재자 스냅샷 저장 (approver_name, department, position)
- 완료함 목록/현황 API 추가 (completed, completedSummary)
- 뱃지 카운트 API 추가 (badgeCounts)
- 완료함 일괄 읽음 처리 (markCompletedAsRead)
- 위임 관리 CRUD API 추가 (delegations)
- Leave 연동 (승인/반려/회수/삭제 시 휴가 상태 동기화)
- ApprovalDelegation 모델 신규 생성
- STATUS_ON_HOLD 상수 추가 (Approval, ApprovalStep)
- isEditable/isSubmittable 반려 상태 허용으로 확장
- isCancellable 보류 상태 포함
- 회수 시 첫 번째 결재자 처리 여부 검증 추가
- i18n 에러/메시지 키 추가
2026-03-11 16:57:54 +09:00

243 lines
18 KiB
PHP

<?php
/**
* 인사 관리 API 라우트 (v1)
*
* - 부서/직급 관리
* - 직원 관리
* - 근태 관리
* - 휴가 관리
* - 전자결재
* - 현장 관리
* - 시공 관리
*/
use App\Http\Controllers\Api\V1\ApprovalController;
use App\Http\Controllers\Api\V1\ApprovalFormController;
use App\Http\Controllers\Api\V1\ApprovalLineController;
use App\Http\Controllers\Api\V1\AttendanceController;
use App\Http\Controllers\Api\V1\CalendarScheduleController;
use App\Http\Controllers\Api\V1\Construction\ContractController;
use App\Http\Controllers\Api\V1\Construction\HandoverReportController;
use App\Http\Controllers\Api\V1\Construction\StructureReviewController;
use App\Http\Controllers\Api\V1\DepartmentController;
use App\Http\Controllers\Api\V1\EmployeeController;
use App\Http\Controllers\Api\V1\LeaveController;
use App\Http\Controllers\Api\V1\LeavePolicyController;
use App\Http\Controllers\Api\V1\PositionController;
use App\Http\Controllers\Api\V1\SiteBriefingController;
use App\Http\Controllers\Api\V1\SiteController;
use Illuminate\Support\Facades\Route;
// Department API
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)
// 부서-사용자
Route::get('/{id}/users', [DepartmentController::class, 'listUsers'])->name('v1.departments.users.index'); // 부서 사용자 목록
Route::post('/{id}/users', [DepartmentController::class, 'attachUser'])->name('v1.departments.users.attach'); // 사용자 배정(주/부서)
Route::delete('/{id}/users/{user}', [DepartmentController::class, 'detachUser'])->name('v1.departments.users.detach'); // 사용자 제거
Route::patch('/{id}/users/{user}/primary', [DepartmentController::class, 'setPrimary'])->name('v1.departments.users.primary'); // 주부서 설정/해제
// 부서-권한
Route::get('/{id}/permissions', [DepartmentController::class, 'listPermissions'])->name('v1.departments.permissions.index'); // 권한 목록
Route::post('/{id}/permissions', [DepartmentController::class, 'upsertPermissions'])->name('v1.departments.permissions.upsert'); // 권한 부여/차단(메뉴별 가능)
Route::delete('/{id}/permissions/{permission}', [DepartmentController::class, 'revokePermissions'])->name('v1.departments.permissions.revoke'); // 권한 제거(해당 메뉴 범위까지)
});
// Position API (직급/직책 통합 관리)
Route::prefix('positions')->group(function () {
Route::get('', [PositionController::class, 'index'])->name('v1.positions.index');
Route::post('', [PositionController::class, 'store'])->name('v1.positions.store');
Route::put('/reorder', [PositionController::class, 'reorder'])->name('v1.positions.reorder');
Route::get('/{id}', [PositionController::class, 'show'])->name('v1.positions.show');
Route::put('/{id}', [PositionController::class, 'update'])->name('v1.positions.update');
Route::delete('/{id}', [PositionController::class, 'destroy'])->name('v1.positions.destroy');
});
// 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');
Route::post('/{id}/revoke-account', [EmployeeController::class, 'revokeAccount'])->name('v1.employees.revokeAccount');
});
// 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::get('/export', [AttendanceController::class, 'export'])->name('v1.attendances.export');
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');
});
// Leave API (휴가 관리)
Route::prefix('leaves')->group(function () {
Route::get('', [LeaveController::class, 'index'])->name('v1.leaves.index');
Route::post('', [LeaveController::class, 'store'])->name('v1.leaves.store');
Route::get('/balances', [LeaveController::class, 'balances'])->name('v1.leaves.balances');
Route::get('/balance', [LeaveController::class, 'balance'])->name('v1.leaves.balance');
Route::get('/balance/{userId}', [LeaveController::class, 'userBalance'])->name('v1.leaves.userBalance');
Route::put('/balance', [LeaveController::class, 'setBalance'])->name('v1.leaves.setBalance');
Route::get('/grants', [LeaveController::class, 'grants'])->name('v1.leaves.grants');
Route::post('/grants', [LeaveController::class, 'storeGrant'])->name('v1.leaves.grants.store');
Route::delete('/grants/{id}', [LeaveController::class, 'destroyGrant'])->name('v1.leaves.grants.destroy');
Route::get('/{id}', [LeaveController::class, 'show'])->name('v1.leaves.show');
Route::patch('/{id}', [LeaveController::class, 'update'])->name('v1.leaves.update');
Route::delete('/{id}', [LeaveController::class, 'destroy'])->name('v1.leaves.destroy');
Route::post('/{id}/approve', [LeaveController::class, 'approve'])->name('v1.leaves.approve');
Route::post('/{id}/reject', [LeaveController::class, 'reject'])->name('v1.leaves.reject');
Route::post('/{id}/cancel', [LeaveController::class, 'cancel'])->name('v1.leaves.cancel');
});
// Leave Policy API (휴가 정책)
Route::get('/leave-policy', [LeavePolicyController::class, 'show'])->name('v1.leave-policy.show');
Route::put('/leave-policy', [LeavePolicyController::class, 'update'])->name('v1.leave-policy.update');
// Approval Form API (결재 양식)
Route::prefix('approval-forms')->group(function () {
Route::get('', [ApprovalFormController::class, 'index'])->name('v1.approval-forms.index');
Route::post('', [ApprovalFormController::class, 'store'])->name('v1.approval-forms.store');
Route::get('/active', [ApprovalFormController::class, 'active'])->name('v1.approval-forms.active');
Route::get('/{id}', [ApprovalFormController::class, 'show'])->whereNumber('id')->name('v1.approval-forms.show');
Route::patch('/{id}', [ApprovalFormController::class, 'update'])->whereNumber('id')->name('v1.approval-forms.update');
Route::delete('/{id}', [ApprovalFormController::class, 'destroy'])->whereNumber('id')->name('v1.approval-forms.destroy');
});
// Approval Line API (결재선)
Route::prefix('approval-lines')->group(function () {
Route::get('', [ApprovalLineController::class, 'index'])->name('v1.approval-lines.index');
Route::post('', [ApprovalLineController::class, 'store'])->name('v1.approval-lines.store');
Route::get('/{id}', [ApprovalLineController::class, 'show'])->whereNumber('id')->name('v1.approval-lines.show');
Route::patch('/{id}', [ApprovalLineController::class, 'update'])->whereNumber('id')->name('v1.approval-lines.update');
Route::delete('/{id}', [ApprovalLineController::class, 'destroy'])->whereNumber('id')->name('v1.approval-lines.destroy');
});
// Approval API (전자결재)
Route::prefix('approvals')->group(function () {
// 기안함
Route::get('/drafts', [ApprovalController::class, 'drafts'])->name('v1.approvals.drafts');
Route::get('/drafts/summary', [ApprovalController::class, 'draftsSummary'])->name('v1.approvals.drafts.summary');
// 결재함
Route::get('/inbox', [ApprovalController::class, 'inbox'])->name('v1.approvals.inbox');
Route::get('/inbox/summary', [ApprovalController::class, 'inboxSummary'])->name('v1.approvals.inbox.summary');
// 참조함
Route::get('/reference', [ApprovalController::class, 'reference'])->name('v1.approvals.reference');
// 완료함
Route::get('/completed', [ApprovalController::class, 'completed'])->name('v1.approvals.completed');
Route::get('/completed/summary', [ApprovalController::class, 'completedSummary'])->name('v1.approvals.completed.summary');
Route::post('/completed/mark-read', [ApprovalController::class, 'markCompletedAsRead'])->name('v1.approvals.completed.mark-read');
// 뱃지 카운트
Route::get('/badge-counts', [ApprovalController::class, 'badgeCounts'])->name('v1.approvals.badge-counts');
// 위임 관리
Route::get('/delegations', [ApprovalController::class, 'delegationIndex'])->name('v1.approvals.delegations.index');
Route::post('/delegations', [ApprovalController::class, 'delegationStore'])->name('v1.approvals.delegations.store');
Route::patch('/delegations/{id}', [ApprovalController::class, 'delegationUpdate'])->whereNumber('id')->name('v1.approvals.delegations.update');
Route::delete('/delegations/{id}', [ApprovalController::class, 'delegationDestroy'])->whereNumber('id')->name('v1.approvals.delegations.destroy');
// CRUD
Route::post('', [ApprovalController::class, 'store'])->name('v1.approvals.store');
Route::get('/{id}', [ApprovalController::class, 'show'])->whereNumber('id')->name('v1.approvals.show');
Route::patch('/{id}', [ApprovalController::class, 'update'])->whereNumber('id')->name('v1.approvals.update');
Route::delete('/{id}', [ApprovalController::class, 'destroy'])->whereNumber('id')->name('v1.approvals.destroy');
// 액션
Route::post('/{id}/submit', [ApprovalController::class, 'submit'])->whereNumber('id')->name('v1.approvals.submit');
Route::post('/{id}/approve', [ApprovalController::class, 'approve'])->whereNumber('id')->name('v1.approvals.approve');
Route::post('/{id}/reject', [ApprovalController::class, 'reject'])->whereNumber('id')->name('v1.approvals.reject');
Route::post('/{id}/cancel', [ApprovalController::class, 'cancel'])->whereNumber('id')->name('v1.approvals.cancel');
Route::post('/{id}/hold', [ApprovalController::class, 'hold'])->whereNumber('id')->name('v1.approvals.hold');
Route::post('/{id}/release-hold', [ApprovalController::class, 'releaseHold'])->whereNumber('id')->name('v1.approvals.release-hold');
Route::post('/{id}/pre-decide', [ApprovalController::class, 'preDecide'])->whereNumber('id')->name('v1.approvals.pre-decide');
Route::post('/{id}/copy', [ApprovalController::class, 'copyForRedraft'])->whereNumber('id')->name('v1.approvals.copy');
// 참조 열람
Route::post('/{id}/read', [ApprovalController::class, 'markRead'])->whereNumber('id')->name('v1.approvals.read');
Route::post('/{id}/unread', [ApprovalController::class, 'markUnread'])->whereNumber('id')->name('v1.approvals.unread');
});
// Site API (현장 관리)
Route::prefix('sites')->group(function () {
Route::get('', [SiteController::class, 'index'])->name('v1.sites.index');
Route::post('', [SiteController::class, 'store'])->name('v1.sites.store');
Route::get('/stats', [SiteController::class, 'stats'])->name('v1.sites.stats');
Route::get('/active', [SiteController::class, 'active'])->name('v1.sites.active');
Route::delete('/bulk', [SiteController::class, 'bulkDestroy'])->name('v1.sites.bulk-destroy');
Route::get('/{id}', [SiteController::class, 'show'])->whereNumber('id')->name('v1.sites.show');
Route::put('/{id}', [SiteController::class, 'update'])->whereNumber('id')->name('v1.sites.update');
Route::delete('/{id}', [SiteController::class, 'destroy'])->whereNumber('id')->name('v1.sites.destroy');
});
// Site Briefing API (현장설명회 관리)
Route::prefix('site-briefings')->group(function () {
Route::get('', [SiteBriefingController::class, 'index'])->name('v1.site-briefings.index');
Route::post('', [SiteBriefingController::class, 'store'])->name('v1.site-briefings.store');
Route::get('/stats', [SiteBriefingController::class, 'stats'])->name('v1.site-briefings.stats');
Route::delete('/bulk', [SiteBriefingController::class, 'bulkDestroy'])->name('v1.site-briefings.bulk-destroy');
Route::get('/{id}', [SiteBriefingController::class, 'show'])->whereNumber('id')->name('v1.site-briefings.show');
Route::put('/{id}', [SiteBriefingController::class, 'update'])->whereNumber('id')->name('v1.site-briefings.update');
Route::delete('/{id}', [SiteBriefingController::class, 'destroy'])->whereNumber('id')->name('v1.site-briefings.destroy');
});
// Construction API (시공관리)
Route::prefix('construction')->group(function () {
// Contract API (계약관리)
Route::prefix('contracts')->group(function () {
Route::get('', [ContractController::class, 'index'])->name('v1.construction.contracts.index');
Route::post('', [ContractController::class, 'store'])->name('v1.construction.contracts.store');
Route::get('/stats', [ContractController::class, 'stats'])->name('v1.construction.contracts.stats');
Route::get('/stage-counts', [ContractController::class, 'stageCounts'])->name('v1.construction.contracts.stage-counts');
Route::delete('/bulk', [ContractController::class, 'bulkDestroy'])->name('v1.construction.contracts.bulk-destroy');
Route::post('/from-bidding/{biddingId}', [ContractController::class, 'storeFromBidding'])->whereNumber('biddingId')->name('v1.construction.contracts.store-from-bidding');
Route::get('/{id}', [ContractController::class, 'show'])->whereNumber('id')->name('v1.construction.contracts.show');
Route::put('/{id}', [ContractController::class, 'update'])->whereNumber('id')->name('v1.construction.contracts.update');
Route::delete('/{id}', [ContractController::class, 'destroy'])->whereNumber('id')->name('v1.construction.contracts.destroy');
});
// HandoverReport API (인수인계보고서관리)
Route::prefix('handover-reports')->group(function () {
Route::get('', [HandoverReportController::class, 'index'])->name('v1.construction.handover-reports.index');
Route::post('', [HandoverReportController::class, 'store'])->name('v1.construction.handover-reports.store');
Route::get('/stats', [HandoverReportController::class, 'stats'])->name('v1.construction.handover-reports.stats');
Route::delete('/bulk', [HandoverReportController::class, 'bulkDestroy'])->name('v1.construction.handover-reports.bulk-destroy');
Route::get('/{id}', [HandoverReportController::class, 'show'])->whereNumber('id')->name('v1.construction.handover-reports.show');
Route::put('/{id}', [HandoverReportController::class, 'update'])->whereNumber('id')->name('v1.construction.handover-reports.update');
Route::delete('/{id}', [HandoverReportController::class, 'destroy'])->whereNumber('id')->name('v1.construction.handover-reports.destroy');
});
// StructureReview API (구조검토관리)
Route::prefix('structure-reviews')->group(function () {
Route::get('', [StructureReviewController::class, 'index'])->name('v1.construction.structure-reviews.index');
Route::post('', [StructureReviewController::class, 'store'])->name('v1.construction.structure-reviews.store');
Route::get('/stats', [StructureReviewController::class, 'stats'])->name('v1.construction.structure-reviews.stats');
Route::delete('/bulk', [StructureReviewController::class, 'bulkDestroy'])->name('v1.construction.structure-reviews.bulk-destroy');
Route::get('/{id}', [StructureReviewController::class, 'show'])->whereNumber('id')->name('v1.construction.structure-reviews.show');
Route::put('/{id}', [StructureReviewController::class, 'update'])->whereNumber('id')->name('v1.construction.structure-reviews.update');
Route::delete('/{id}', [StructureReviewController::class, 'destroy'])->whereNumber('id')->name('v1.construction.structure-reviews.destroy');
});
});
// Calendar Schedule API (달력 일정 관리)
Route::prefix('calendar-schedules')->group(function () {
Route::get('', [CalendarScheduleController::class, 'index'])->name('v1.calendar-schedules.index');
Route::get('/stats', [CalendarScheduleController::class, 'stats'])->name('v1.calendar-schedules.stats');
Route::post('', [CalendarScheduleController::class, 'store'])->name('v1.calendar-schedules.store');
Route::post('/bulk', [CalendarScheduleController::class, 'bulkStore'])->name('v1.calendar-schedules.bulk');
Route::get('/{id}', [CalendarScheduleController::class, 'show'])->whereNumber('id')->name('v1.calendar-schedules.show');
Route::put('/{id}', [CalendarScheduleController::class, 'update'])->whereNumber('id')->name('v1.calendar-schedules.update');
Route::delete('/{id}', [CalendarScheduleController::class, 'destroy'])->whereNumber('id')->name('v1.calendar-schedules.destroy');
});