diff --git a/app/Http/Middleware/ApiVersionMiddleware.php b/app/Http/Middleware/ApiVersionMiddleware.php new file mode 100644 index 0000000..3dad8bf --- /dev/null +++ b/app/Http/Middleware/ApiVersionMiddleware.php @@ -0,0 +1,171 @@ + 쿼리 파라미터 > 기본값) + $requestedVersion = $this->getRequestedVersion($request); + + // 2. 실제 사용할 버전 결정 (fallback 적용) + $actualVersion = $this->resolveVersion($request, $requestedVersion); + + // 3. 요청에 버전 정보 저장 (컨트롤러에서 사용 가능) + $request->attributes->set('api_version', $actualVersion); + $request->attributes->set('api_version_requested', $requestedVersion); + $request->attributes->set('api_version_fallback', $actualVersion !== $requestedVersion); + + // 4. 요청 처리 + $response = $next($request); + + // 5. 응답 헤더에 버전 정보 추가 + $response->headers->set('X-API-Version', $actualVersion); + if ($actualVersion !== $requestedVersion) { + $response->headers->set('X-API-Version-Fallback', 'true'); + $response->headers->set('X-API-Version-Requested', $requestedVersion); + } + + return $response; + } + + /** + * 요청에서 버전 정보 추출 + */ + protected function getRequestedVersion(Request $request): string + { + // 1. Accept-Version 헤더 (권장) + $version = $request->header('Accept-Version'); + if ($version && $this->isValidVersion($version)) { + return $version; + } + + // 2. X-API-Version 헤더 (대안) + $version = $request->header('X-API-Version'); + if ($version && $this->isValidVersion($version)) { + return $version; + } + + // 3. 쿼리 파라미터 (테스트용) + $version = $request->query('api_version'); + if ($version && $this->isValidVersion($version)) { + return $version; + } + + return $this->defaultVersion; + } + + /** + * 유효한 버전인지 확인 + */ + protected function isValidVersion(string $version): bool + { + return in_array($version, $this->supportedVersions, true); + } + + /** + * 실제 사용할 버전 결정 (fallback 로직) + */ + protected function resolveVersion(Request $request, string $requestedVersion): string + { + // 요청된 버전부터 하위 버전까지 순차 확인 + $startIndex = array_search($requestedVersion, $this->supportedVersions, true); + + if ($startIndex === false) { + return $this->defaultVersion; + } + + // 요청된 버전부터 하위 버전까지 체크 + for ($i = $startIndex; $i < count($this->supportedVersions); $i++) { + $version = $this->supportedVersions[$i]; + + if ($this->versionRouteExists($request, $version)) { + return $version; + } + } + + // 모든 버전에서 라우트를 찾지 못하면 기본값 반환 + return $this->defaultVersion; + } + + /** + * 해당 버전의 라우트가 존재하는지 확인 + */ + protected function versionRouteExists(Request $request, string $version): bool + { + $path = $request->path(); + + // URL에서 버전 부분 교체 + // /api/v1/users → /api/v2/users + $versionedPath = preg_replace('/^api\/v\d+/', "api/{$version}", $path); + + // 해당 경로의 라우트가 존재하는지 확인 + $routes = Route::getRoutes(); + + foreach ($routes as $route) { + $routeUri = $route->uri(); + + // 정확히 일치하거나 파라미터 패턴 매칭 + if ($this->matchesRoute($versionedPath, $routeUri, $request->method())) { + return true; + } + } + + return false; + } + + /** + * 경로가 라우트와 일치하는지 확인 + */ + protected function matchesRoute(string $path, string $routeUri, string $method): bool + { + // 라우트 URI의 파라미터를 정규식으로 변환 + // {id} → [^/]+ + $pattern = preg_replace('/\{[^}]+\}/', '[^/]+', $routeUri); + $pattern = '#^'.$pattern.'$#'; + + return (bool) preg_match($pattern, $path); + } + + /** + * 지원 버전 목록 반환 (외부에서 사용) + */ + public function getSupportedVersions(): array + { + return $this->supportedVersions; + } + + /** + * 기본 버전 반환 + */ + public function getDefaultVersion(): string + { + return $this->defaultVersion; + } +} diff --git a/bootstrap/app.php b/bootstrap/app.php index 5319a29..9091600 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -3,6 +3,7 @@ use App\Exceptions\Handler; use App\Http\Middleware\ApiKeyMiddleware; use App\Http\Middleware\ApiRateLimiter; +use App\Http\Middleware\ApiVersionMiddleware; use App\Http\Middleware\CheckPermission; use App\Http\Middleware\CheckSwaggerAuth; use App\Http\Middleware\CorsMiddleware; @@ -23,14 +24,16 @@ ->withMiddleware(function (Middleware $middleware) { // 글로벌 미들웨어 (모든 요청에 적용, 순서 중요) $middleware->append(CorsMiddleware::class); - $middleware->append(ApiRateLimiter::class); // 1. Rate Limiting 먼저 체크 - $middleware->append(ApiKeyMiddleware::class); // 2. API Key 검증 + $middleware->append(ApiRateLimiter::class); // 1. Rate Limiting 먼저 체크 + $middleware->append(ApiKeyMiddleware::class); // 2. API Key 검증 + $middleware->append(ApiVersionMiddleware::class); // 3. API 버전 해석 및 폴백 // API 미들웨어 그룹에 로깅 추가 $middleware->appendToGroup('api', LogApiRequest::class); $middleware->alias([ - 'auth.apikey' => ApiKeyMiddleware::class, // 인증: apikey + basic auth (alias 유지) + 'auth.apikey' => ApiKeyMiddleware::class, // 인증: apikey + basic auth (alias 유지) + 'api.version' => ApiVersionMiddleware::class, // API 버전 해석 및 폴백 'swagger.auth' => CheckSwaggerAuth::class, 'perm.map' => PermMapper::class, // 전처리: 라우트명 → perm 키 생성/주입 'permission' => CheckPermission::class, // 검사: perm 키로 접근 허용/차단 diff --git a/routes/api.php b/routes/api.php index 2b2cfd7..269f423 100644 --- a/routes/api.php +++ b/routes/api.php @@ -1,1479 +1,61 @@ group(function () { - - // 내부 서버간 통신 (API Key, Bearer 인증 제외 - HMAC 인증 사용) - Route::prefix('internal')->group(function () { - Route::post('/exchange-token', [InternalController::class, 'exchangeToken'])->name('v1.internal.exchange-token'); - }); - - // API KEY 인증 (글로벌 미들웨어로 이미 적용됨) - Route::get('/debug-apikey', [ApiController::class, 'debugApikey']); - - // SAM API (글로벌 미들웨어로 이미 적용됨) - Route::group([], function () { - - // Auth API - Route::post('login', [ApiController::class, 'login'])->name('v1.users.login'); - Route::middleware('auth:sanctum')->post('logout', [ApiController::class, 'logout'])->name('v1.users.logout'); - Route::post('signup', [ApiController::class, 'signup'])->name('v1.users.signup'); - Route::post('token-login', [ApiController::class, 'tokenLogin'])->name('v1.auth.token-login'); // MNG → DEV 자동 로그인 - Route::post('refresh', [RefreshController::class, 'refresh'])->name('v1.token.refresh'); - Route::post('register', [RegisterController::class, 'register'])->name('v1.register'); - - // Tenant Admin API - Route::prefix('admin')->group(function () { - // 목록/생성 - Route::get('users', [AdminController::class, 'index'])->name('v1.admin.users.index'); // 테넌트 사용자 목록 조회 - Route::post('users', [AdminController::class, 'store'])->name('v1.admin.users.store'); // 테넌트 사용자 생성 - - // 단건 - Route::get('users/{id}', [AdminController::class, 'show'])->name('v1.admin.users.show'); // 테넌트 사용자 단건 조회 - Route::put('users/{id}', [AdminController::class, 'update'])->name('v1.admin.users.update'); // 테넌트 사용자 수정 - - // 소프트 삭제 복구 - Route::delete('users/{id}', [AdminController::class, 'destroy'])->name('v1.admin.users.destroy'); // 테넌트 사용자 삭제(연결 삭제) - Route::post('users/{id}/restore', [AdminController::class, 'restore'])->name('v1.admin.users.restore'); // 테넌트 사용자 삭제 복구 - - // 상태 토글 - Route::patch('users/{id}/status', [AdminController::class, 'toggle'])->name('v1.admin.users.status.toggle'); // 테넌트 사용자 활성/비활성 - - // 역할 부여/해제 - Route::post('users/{id}/roles', [AdminController::class, 'attach'])->name('v1.admin.users.roles.attach'); // 테넌트 사용자 역할 부여 - Route::delete('users/{id}/roles/{role}', [AdminController::class, 'detach'])->name('v1.admin.users.roles.detach'); // 테넌트 사용자 역할 해제 - - // 비밀번호 초기화 - Route::post('users/{id}/reset-password', [AdminController::class, 'reset'])->name('v1.admin.users.password.reset'); // 테넌트 사용자 비밀번호 초기화 - - // 글로벌 메뉴 관리 (시스템 관리자용) - Route::prefix('global-menus')->group(function () { - Route::get('/', [GlobalMenuController::class, 'index'])->name('v1.admin.global-menus.index'); // 글로벌 메뉴 목록 - Route::post('/', [GlobalMenuController::class, 'store'])->name('v1.admin.global-menus.store'); // 글로벌 메뉴 생성 - Route::get('/tree', [GlobalMenuController::class, 'tree'])->name('v1.admin.global-menus.tree'); // 글로벌 메뉴 트리 - Route::get('/stats', [GlobalMenuController::class, 'stats'])->name('v1.admin.global-menus.stats'); // 글로벌 메뉴 통계 - Route::post('/reorder', [GlobalMenuController::class, 'reorder'])->name('v1.admin.global-menus.reorder'); // 글로벌 메뉴 순서 변경 - Route::get('/{id}', [GlobalMenuController::class, 'show'])->name('v1.admin.global-menus.show'); // 글로벌 메뉴 단건 조회 - Route::put('/{id}', [GlobalMenuController::class, 'update'])->name('v1.admin.global-menus.update'); // 글로벌 메뉴 수정 - Route::delete('/{id}', [GlobalMenuController::class, 'destroy'])->name('v1.admin.global-menus.destroy'); // 글로벌 메뉴 삭제 - Route::post('/{id}/sync-to-tenants', [GlobalMenuController::class, 'syncToTenants'])->name('v1.admin.global-menus.sync-to-tenants'); // 테넌트에 동기화 - }); - }); - - // Member API - Route::prefix('users')->group(function () { - Route::get('index', [UserController::class, 'index'])->name('v1.users.index'); // 회원 목록 조회 - Route::get('show/{user_no}', [UserController::class, 'show'])->name('v1.users.show'); // 회원 상세 조회 - - Route::get('me', [UserController::class, 'me'])->name('v1.users.users.me'); // 내 정보 조회 - Route::put('me', [UserController::class, 'meUpdate'])->name('v1.users.me.update'); // 내 정보 수정 - Route::put('me/password', [UserController::class, 'changePassword'])->name('v1.users.me.password'); // 비밀번호 변겅 - - Route::get('me/tenants', [UserController::class, 'tenants'])->name('v1.users.me.tenants.index'); // 내 테넌트 목록 - Route::patch('me/tenants/switch', [UserController::class, 'switchTenant'])->name('v1.users.me.tenants.switch'); // 활성 테넌트 전환 - - // 사용자 초대 API - Route::get('invitations', [UserInvitationController::class, 'index'])->name('v1.users.invitations.index'); // 초대 목록 - Route::post('invite', [UserInvitationController::class, 'invite'])->name('v1.users.invite'); // 초대 발송 - Route::post('invitations/{token}/accept', [UserInvitationController::class, 'accept'])->name('v1.users.invitations.accept'); // 초대 수락 - Route::delete('invitations/{id}', [UserInvitationController::class, 'cancel'])->whereNumber('id')->name('v1.users.invitations.cancel'); // 초대 취소 - Route::post('invitations/{id}/resend', [UserInvitationController::class, 'resend'])->whereNumber('id')->name('v1.users.invitations.resend'); // 초대 재발송 - - // 알림 설정 API (auth:sanctum 필수) - Route::middleware('auth:sanctum')->group(function () { - Route::get('me/notification-settings', [NotificationSettingController::class, 'index'])->name('v1.users.me.notification-settings.index'); // 알림 설정 조회 - Route::put('me/notification-settings', [NotificationSettingController::class, 'update'])->name('v1.users.me.notification-settings.update'); // 알림 설정 수정 - Route::put('me/notification-settings/bulk', [NotificationSettingController::class, 'bulkUpdate'])->name('v1.users.me.notification-settings.bulk'); // 알림 일괄 설정 - }); - }); - - // Account API (계정 관리 - 탈퇴, 사용중지, 약관동의) - Route::prefix('account')->middleware('auth:sanctum')->group(function () { - Route::post('withdraw', [AccountController::class, 'withdraw'])->name('v1.account.withdraw'); // 회원 탈퇴 - Route::post('suspend', [AccountController::class, 'suspend'])->name('v1.account.suspend'); // 사용 중지 (테넌트) - Route::get('agreements', [AccountController::class, 'getAgreements'])->name('v1.account.agreements.index'); // 약관 동의 조회 - Route::put('agreements', [AccountController::class, 'updateAgreements'])->name('v1.account.agreements.update'); // 약관 동의 수정 - }); - - // Tenant API - Route::prefix('tenants')->group(function () { - Route::get('list', [TenantController::class, 'index'])->name('v1.tenant.index'); // 테넌트 목록 조회 - Route::get('/', [TenantController::class, 'show'])->name('v1.tenant.show'); // 테넌트 정보 조회 - Route::put('/', [TenantController::class, 'update'])->name('v1.tenant.update'); // 테넌트 정보 수정 - Route::post('/', [TenantController::class, 'store'])->name('v1.tenant.store'); // 테넌트 등록 - Route::delete('/', [TenantController::class, 'destroy'])->name('v1.tenant.destroy'); // 테넌트 삭제(탈퇴) - Route::put('/restore/{tenant_id}', [TenantController::class, 'restore'])->name('v1.tenant.restore'); // 테넌트 복구 - Route::post('/logo', [TenantController::class, 'uploadLogo'])->name('v1.tenant.upload-logo'); // 로고 업로드 - }); - - // Tenant Statistics Field API - Route::prefix('tenant-stat-fields')->group(function () { - Route::get('/', [TenantStatFieldController::class, 'index'])->name('v1.tenant-stat-fields.index'); // 목록 조회 - Route::post('/', [TenantStatFieldController::class, 'store'])->name('v1.tenant-stat-fields.store'); // 생성 - Route::get('/{id}', [TenantStatFieldController::class, 'show'])->name('v1.tenant-stat-fields.show'); // 단건 조회 - Route::patch('/{id}', [TenantStatFieldController::class, 'update'])->name('v1.tenant-stat-fields.update'); // 수정 - Route::delete('/{id}', [TenantStatFieldController::class, 'destroy'])->name('v1.tenant-stat-fields.destroy'); // 삭제 - Route::post('/reorder', [TenantStatFieldController::class, 'reorder'])->name('v1.tenant-stat-fields.reorder'); // 정렬 변경 - Route::put('/bulk-upsert', [TenantStatFieldController::class, 'bulkUpsert'])->name('v1.tenant-stat-fields.bulk-upsert'); // 일괄 저장 - }); - - // Tenant Settings API (테넌트별 설정) - Route::prefix('tenant-settings')->group(function () { - Route::get('/', [TenantSettingController::class, 'index'])->name('v1.tenant-settings.index'); // 전체 설정 조회 - Route::post('/', [TenantSettingController::class, 'store'])->name('v1.tenant-settings.store'); // 설정 저장 - Route::put('/bulk', [TenantSettingController::class, 'bulkUpdate'])->name('v1.tenant-settings.bulk'); // 일괄 저장 - Route::post('/initialize', [TenantSettingController::class, 'initialize'])->name('v1.tenant-settings.initialize'); // 기본 설정 초기화 - Route::get('/{group}/{key}', [TenantSettingController::class, 'show'])->name('v1.tenant-settings.show'); // 단일 설정 조회 - Route::delete('/{group}/{key}', [TenantSettingController::class, 'destroy'])->name('v1.tenant-settings.destroy'); // 설정 삭제 - }); - - // Menu API - Route::middleware(['perm.map', 'permission'])->prefix('menus')->group(function () { - Route::get('/', [MenuController::class, 'index'])->name('v1.menus.index'); - Route::post('/', [MenuController::class, 'store'])->name('v1.menus.store'); - Route::post('/reorder', [MenuController::class, 'reorder'])->name('v1.menus.reorder'); - - // 동기화 관련 라우트 (/{id} 전에 위치해야 함) - Route::get('/trashed', [MenuController::class, 'trashed'])->name('v1.menus.trashed'); - Route::get('/available-global', [MenuController::class, 'availableGlobal'])->name('v1.menus.available-global'); - Route::get('/sync-status', [MenuController::class, 'syncStatus'])->name('v1.menus.sync-status'); - Route::post('/sync', [MenuController::class, 'sync'])->name('v1.menus.sync'); - Route::post('/sync-new', [MenuController::class, 'syncNew'])->name('v1.menus.sync-new'); - Route::post('/sync-updates', [MenuController::class, 'syncUpdates'])->name('v1.menus.sync-updates'); - - // 단일 메뉴 관련 라우트 - Route::get('/{id}', [MenuController::class, 'show'])->name('v1.menus.show'); - Route::patch('/{id}', [MenuController::class, 'update'])->name('v1.menus.update'); - Route::delete('/{id}', [MenuController::class, 'destroy'])->name('v1.menus.destroy'); - Route::post('/{id}/toggle', [MenuController::class, 'toggle'])->name('v1.menus.toggle'); - Route::post('/{id}/restore', [MenuController::class, 'restore'])->name('v1.menus.restore'); - }); - - // Role API - Route::prefix('roles')->group(function () { - Route::get('/', [RoleController::class, 'index'])->name('v1.roles.index'); // view - Route::post('/', [RoleController::class, 'store'])->name('v1.roles.store'); // create - Route::get('/stats', [RoleController::class, 'stats'])->name('v1.roles.stats'); // stats - Route::get('/active', [RoleController::class, 'active'])->name('v1.roles.active'); // active list - Route::get('/{id}', [RoleController::class, 'show'])->name('v1.roles.show'); // view - Route::patch('/{id}', [RoleController::class, 'update'])->name('v1.roles.update'); // update - Route::delete('/{id}', [RoleController::class, 'destroy'])->name('v1.roles.destroy'); // delete - }); - - // Role Permission API - 공통 - Route::get('/role-permissions/menus', [RolePermissionController::class, 'menus'])->name('v1.roles.perms.menus'); // 메뉴 트리 - - // Role Permission API - 역할별 - Route::prefix('roles/{id}/permissions')->group(function () { - Route::get('/', [RolePermissionController::class, 'index'])->name('v1.roles.perms.index'); // list - Route::post('/', [RolePermissionController::class, 'grant'])->name('v1.roles.perms.grant'); // grant - Route::delete('/', [RolePermissionController::class, 'revoke'])->name('v1.roles.perms.revoke'); // revoke - Route::put('/sync', [RolePermissionController::class, 'sync'])->name('v1.roles.perms.sync'); // sync - // 권한 매트릭스 API - Route::get('/matrix', [RolePermissionController::class, 'matrix'])->name('v1.roles.perms.matrix'); // 권한 매트릭스 조회 - Route::post('/toggle', [RolePermissionController::class, 'toggle'])->name('v1.roles.perms.toggle'); // 개별 권한 토글 - Route::post('/allow-all', [RolePermissionController::class, 'allowAll'])->name('v1.roles.perms.allowAll'); // 전체 허용 - Route::post('/deny-all', [RolePermissionController::class, 'denyAll'])->name('v1.roles.perms.denyAll'); // 전체 거부 - Route::post('/reset', [RolePermissionController::class, 'reset'])->name('v1.roles.perms.reset'); // 기본값 초기화 - }); - - // User Role API - Route::prefix('users/{id}/roles')->group(function () { - Route::get('/', [UserRoleController::class, 'index'])->name('v1.users.roles.index'); // list - Route::post('/', [UserRoleController::class, 'grant'])->name('v1.users.roles.grant'); // grant - Route::delete('/', [UserRoleController::class, 'revoke'])->name('v1.users.roles.revoke'); // revoke - Route::put('/sync', [UserRoleController::class, 'sync'])->name('v1.users.roles.sync'); // sync - }); - - // 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'); - // 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}/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'); - }); - }); - - // Card API (카드 관리) - Route::prefix('cards')->group(function () { - Route::get('', [CardController::class, 'index'])->name('v1.cards.index'); - Route::post('', [CardController::class, 'store'])->name('v1.cards.store'); - Route::get('/active', [CardController::class, 'active'])->name('v1.cards.active'); - Route::get('/{id}', [CardController::class, 'show'])->whereNumber('id')->name('v1.cards.show'); - Route::put('/{id}', [CardController::class, 'update'])->whereNumber('id')->name('v1.cards.update'); - Route::delete('/{id}', [CardController::class, 'destroy'])->whereNumber('id')->name('v1.cards.destroy'); - Route::patch('/{id}/toggle', [CardController::class, 'toggle'])->whereNumber('id')->name('v1.cards.toggle'); - }); - - // BankAccount API (계좌 관리) - Route::prefix('bank-accounts')->group(function () { - Route::get('', [BankAccountController::class, 'index'])->name('v1.bank-accounts.index'); - Route::post('', [BankAccountController::class, 'store'])->name('v1.bank-accounts.store'); - Route::get('/active', [BankAccountController::class, 'active'])->name('v1.bank-accounts.active'); - Route::get('/{id}', [BankAccountController::class, 'show'])->whereNumber('id')->name('v1.bank-accounts.show'); - Route::put('/{id}', [BankAccountController::class, 'update'])->whereNumber('id')->name('v1.bank-accounts.update'); - Route::delete('/{id}', [BankAccountController::class, 'destroy'])->whereNumber('id')->name('v1.bank-accounts.destroy'); - Route::patch('/{id}/toggle', [BankAccountController::class, 'toggle'])->whereNumber('id')->name('v1.bank-accounts.toggle'); - Route::patch('/{id}/set-primary', [BankAccountController::class, 'setPrimary'])->whereNumber('id')->name('v1.bank-accounts.set-primary'); - }); - - // Deposit API (입금 관리) - Route::prefix('deposits')->group(function () { - Route::get('', [DepositController::class, 'index'])->name('v1.deposits.index'); - Route::post('', [DepositController::class, 'store'])->name('v1.deposits.store'); - Route::get('/summary', [DepositController::class, 'summary'])->name('v1.deposits.summary'); - Route::post('/bulk-update-account-code', [DepositController::class, 'bulkUpdateAccountCode'])->name('v1.deposits.bulk-update-account-code'); - Route::get('/{id}', [DepositController::class, 'show'])->whereNumber('id')->name('v1.deposits.show'); - Route::put('/{id}', [DepositController::class, 'update'])->whereNumber('id')->name('v1.deposits.update'); - Route::delete('/{id}', [DepositController::class, 'destroy'])->whereNumber('id')->name('v1.deposits.destroy'); - }); - - // Withdrawal API (출금 관리) - Route::prefix('withdrawals')->group(function () { - Route::get('', [WithdrawalController::class, 'index'])->name('v1.withdrawals.index'); - Route::post('', [WithdrawalController::class, 'store'])->name('v1.withdrawals.store'); - Route::get('/summary', [WithdrawalController::class, 'summary'])->name('v1.withdrawals.summary'); - Route::post('/bulk-update-account-code', [WithdrawalController::class, 'bulkUpdateAccountCode'])->name('v1.withdrawals.bulk-update-account-code'); - Route::get('/{id}', [WithdrawalController::class, 'show'])->whereNumber('id')->name('v1.withdrawals.show'); - Route::put('/{id}', [WithdrawalController::class, 'update'])->whereNumber('id')->name('v1.withdrawals.update'); - Route::delete('/{id}', [WithdrawalController::class, 'destroy'])->whereNumber('id')->name('v1.withdrawals.destroy'); - }); - - // Payroll API (급여 관리) - Route::prefix('payrolls')->group(function () { - Route::get('', [PayrollController::class, 'index'])->name('v1.payrolls.index'); - Route::post('', [PayrollController::class, 'store'])->name('v1.payrolls.store'); - Route::get('/summary', [PayrollController::class, 'summary'])->name('v1.payrolls.summary'); - Route::post('/calculate', [PayrollController::class, 'calculate'])->name('v1.payrolls.calculate'); - Route::post('/bulk-confirm', [PayrollController::class, 'bulkConfirm'])->name('v1.payrolls.bulk-confirm'); - Route::get('/{id}', [PayrollController::class, 'show'])->whereNumber('id')->name('v1.payrolls.show'); - Route::put('/{id}', [PayrollController::class, 'update'])->whereNumber('id')->name('v1.payrolls.update'); - Route::delete('/{id}', [PayrollController::class, 'destroy'])->whereNumber('id')->name('v1.payrolls.destroy'); - Route::post('/{id}/confirm', [PayrollController::class, 'confirm'])->whereNumber('id')->name('v1.payrolls.confirm'); - Route::post('/{id}/pay', [PayrollController::class, 'pay'])->whereNumber('id')->name('v1.payrolls.pay'); - Route::get('/{id}/payslip', [PayrollController::class, 'payslip'])->whereNumber('id')->name('v1.payrolls.payslip'); - }); - - // Salary API (급여 관리 - React 연동) - Route::prefix('salaries')->group(function () { - Route::get('', [SalaryController::class, 'index'])->name('v1.salaries.index'); - Route::post('', [SalaryController::class, 'store'])->name('v1.salaries.store'); - Route::get('/statistics', [SalaryController::class, 'statistics'])->name('v1.salaries.statistics'); - Route::get('/export', [SalaryController::class, 'export'])->name('v1.salaries.export'); - Route::post('/bulk-update-status', [SalaryController::class, 'bulkUpdateStatus'])->name('v1.salaries.bulk-update-status'); - Route::get('/{id}', [SalaryController::class, 'show'])->whereNumber('id')->name('v1.salaries.show'); - Route::put('/{id}', [SalaryController::class, 'update'])->whereNumber('id')->name('v1.salaries.update'); - Route::delete('/{id}', [SalaryController::class, 'destroy'])->whereNumber('id')->name('v1.salaries.destroy'); - Route::patch('/{id}/status', [SalaryController::class, 'updateStatus'])->whereNumber('id')->name('v1.salaries.update-status'); - }); - - // Expected Expense API (미지급비용 관리) - Route::prefix('expected-expenses')->group(function () { - Route::get('', [ExpectedExpenseController::class, 'index'])->name('v1.expected-expenses.index'); - Route::post('', [ExpectedExpenseController::class, 'store'])->name('v1.expected-expenses.store'); - Route::get('/summary', [ExpectedExpenseController::class, 'summary'])->name('v1.expected-expenses.summary'); - Route::get('/dashboard-detail', [ExpectedExpenseController::class, 'dashboardDetail'])->name('v1.expected-expenses.dashboard-detail'); - Route::delete('', [ExpectedExpenseController::class, 'destroyMany'])->name('v1.expected-expenses.destroy-many'); - Route::put('/update-payment-date', [ExpectedExpenseController::class, 'updateExpectedPaymentDate'])->name('v1.expected-expenses.update-payment-date'); - Route::get('/{id}', [ExpectedExpenseController::class, 'show'])->whereNumber('id')->name('v1.expected-expenses.show'); - Route::put('/{id}', [ExpectedExpenseController::class, 'update'])->whereNumber('id')->name('v1.expected-expenses.update'); - Route::delete('/{id}', [ExpectedExpenseController::class, 'destroy'])->whereNumber('id')->name('v1.expected-expenses.destroy'); - }); - - // Loan API (가지급금 관리) - Route::prefix('loans')->group(function () { - Route::get('', [LoanController::class, 'index'])->name('v1.loans.index'); - Route::post('', [LoanController::class, 'store'])->name('v1.loans.store'); - Route::get('/summary', [LoanController::class, 'summary'])->name('v1.loans.summary'); - Route::get('/dashboard', [LoanController::class, 'dashboard'])->name('v1.loans.dashboard'); - Route::get('/tax-simulation', [LoanController::class, 'taxSimulation'])->name('v1.loans.tax-simulation'); - Route::post('/calculate-interest', [LoanController::class, 'calculateInterest'])->name('v1.loans.calculate-interest'); - Route::get('/interest-report/{year}', [LoanController::class, 'interestReport'])->whereNumber('year')->name('v1.loans.interest-report'); - Route::get('/{id}', [LoanController::class, 'show'])->whereNumber('id')->name('v1.loans.show'); - Route::put('/{id}', [LoanController::class, 'update'])->whereNumber('id')->name('v1.loans.update'); - Route::delete('/{id}', [LoanController::class, 'destroy'])->whereNumber('id')->name('v1.loans.destroy'); - Route::post('/{id}/settle', [LoanController::class, 'settle'])->whereNumber('id')->name('v1.loans.settle'); - }); - - // Vendor Ledger API (거래처원장) - Route::prefix('vendor-ledger')->group(function () { - Route::get('', [VendorLedgerController::class, 'index'])->name('v1.vendor-ledger.index'); - Route::get('/summary', [VendorLedgerController::class, 'summary'])->name('v1.vendor-ledger.summary'); - Route::get('/{clientId}', [VendorLedgerController::class, 'show'])->whereNumber('clientId')->name('v1.vendor-ledger.show'); - }); - - // Card Transaction API (카드 거래) - Route::prefix('card-transactions')->group(function () { - Route::get('', [CardTransactionController::class, 'index'])->name('v1.card-transactions.index'); - Route::get('/summary', [CardTransactionController::class, 'summary'])->name('v1.card-transactions.summary'); - Route::get('/dashboard', [CardTransactionController::class, 'dashboard'])->name('v1.card-transactions.dashboard'); - Route::post('', [CardTransactionController::class, 'store'])->name('v1.card-transactions.store'); - Route::put('/bulk-update-account', [CardTransactionController::class, 'bulkUpdateAccountCode'])->name('v1.card-transactions.bulk-update-account'); - Route::get('/{id}', [CardTransactionController::class, 'show'])->whereNumber('id')->name('v1.card-transactions.show'); - Route::put('/{id}', [CardTransactionController::class, 'update'])->whereNumber('id')->name('v1.card-transactions.update'); - Route::delete('/{id}', [CardTransactionController::class, 'destroy'])->whereNumber('id')->name('v1.card-transactions.destroy'); - }); - - // Bank Transaction API (은행 거래 조회) - Route::prefix('bank-transactions')->group(function () { - Route::get('', [BankTransactionController::class, 'index'])->name('v1.bank-transactions.index'); - Route::get('/summary', [BankTransactionController::class, 'summary'])->name('v1.bank-transactions.summary'); - Route::get('/accounts', [BankTransactionController::class, 'accounts'])->name('v1.bank-transactions.accounts'); - }); - - // Receivables API (채권 현황) - Route::prefix('receivables')->group(function () { - Route::get('', [ReceivablesController::class, 'index'])->name('v1.receivables.index'); - Route::get('/summary', [ReceivablesController::class, 'summary'])->name('v1.receivables.summary'); - Route::put('/overdue-status', [ReceivablesController::class, 'updateOverdueStatus'])->name('v1.receivables.update-overdue-status'); - Route::put('/memos', [ReceivablesController::class, 'updateMemos'])->name('v1.receivables.update-memos'); - }); - - // Daily Report API (일일 보고서) - Route::prefix('daily-report')->group(function () { - Route::get('/note-receivables', [DailyReportController::class, 'noteReceivables'])->name('v1.daily-report.note-receivables'); - Route::get('/daily-accounts', [DailyReportController::class, 'dailyAccounts'])->name('v1.daily-report.daily-accounts'); - Route::get('/summary', [DailyReportController::class, 'summary'])->name('v1.daily-report.summary'); - }); - - // Comprehensive Analysis API (종합 분석 보고서) - Route::get('/comprehensive-analysis', [ComprehensiveAnalysisController::class, 'index'])->name('v1.comprehensive-analysis.index'); - - // Status Board API (CEO 대시보드 현황판) - Route::get('/status-board/summary', [StatusBoardController::class, 'summary'])->name('v1.status-board.summary'); - - // Today Issue API (CEO 대시보드 오늘의 이슈 리스트) - Route::get('/today-issues/summary', [TodayIssueController::class, 'summary'])->name('v1.today-issues.summary'); - Route::get('/today-issues/unread', [TodayIssueController::class, 'unread'])->name('v1.today-issues.unread'); - Route::get('/today-issues/unread/count', [TodayIssueController::class, 'unreadCount'])->name('v1.today-issues.unread.count'); - Route::post('/today-issues/{id}/read', [TodayIssueController::class, 'markAsRead'])->whereNumber('id')->name('v1.today-issues.read'); - Route::post('/today-issues/read-all', [TodayIssueController::class, 'markAllAsRead'])->name('v1.today-issues.read-all'); - - // Calendar API (CEO 대시보드 캘린더) - Route::get('/calendar/schedules', [CalendarController::class, 'summary'])->name('v1.calendar.schedules'); - - // Vat API (CEO 대시보드 부가세 현황) - Route::get('/vat/summary', [VatController::class, 'summary'])->name('v1.vat.summary'); - - // Entertainment API (CEO 대시보드 접대비 현황) - Route::get('/entertainment/summary', [EntertainmentController::class, 'summary'])->name('v1.entertainment.summary'); - - // Welfare API (CEO 대시보드 복리후생비 현황) - Route::get('/welfare/summary', [WelfareController::class, 'summary'])->name('v1.welfare.summary'); - Route::get('/welfare/detail', [WelfareController::class, 'detail'])->name('v1.welfare.detail'); - - // Plan API (요금제 관리) - Route::prefix('plans')->group(function () { - Route::get('', [PlanController::class, 'index'])->name('v1.plans.index'); - Route::post('', [PlanController::class, 'store'])->name('v1.plans.store'); - Route::get('/active', [PlanController::class, 'active'])->name('v1.plans.active'); - Route::get('/{id}', [PlanController::class, 'show'])->whereNumber('id')->name('v1.plans.show'); - Route::put('/{id}', [PlanController::class, 'update'])->whereNumber('id')->name('v1.plans.update'); - Route::delete('/{id}', [PlanController::class, 'destroy'])->whereNumber('id')->name('v1.plans.destroy'); - Route::patch('/{id}/toggle', [PlanController::class, 'toggle'])->whereNumber('id')->name('v1.plans.toggle'); - }); - - // Subscription API (구독 관리) - Route::prefix('subscriptions')->group(function () { - Route::get('', [SubscriptionController::class, 'index'])->name('v1.subscriptions.index'); - Route::post('', [SubscriptionController::class, 'store'])->name('v1.subscriptions.store'); - Route::get('/current', [SubscriptionController::class, 'current'])->name('v1.subscriptions.current'); - Route::get('/usage', [SubscriptionController::class, 'usage'])->name('v1.subscriptions.usage'); - Route::post('/export', [SubscriptionController::class, 'export'])->name('v1.subscriptions.export'); - Route::get('/export/{id}', [SubscriptionController::class, 'exportStatus'])->whereNumber('id')->name('v1.subscriptions.export.status'); - Route::get('/{id}', [SubscriptionController::class, 'show'])->whereNumber('id')->name('v1.subscriptions.show'); - Route::post('/{id}/cancel', [SubscriptionController::class, 'cancel'])->whereNumber('id')->name('v1.subscriptions.cancel'); - Route::post('/{id}/renew', [SubscriptionController::class, 'renew'])->whereNumber('id')->name('v1.subscriptions.renew'); - Route::post('/{id}/suspend', [SubscriptionController::class, 'suspend'])->whereNumber('id')->name('v1.subscriptions.suspend'); - Route::post('/{id}/resume', [SubscriptionController::class, 'resume'])->whereNumber('id')->name('v1.subscriptions.resume'); - }); - - // Payment API (결제 관리) - Route::prefix('payments')->group(function () { - Route::get('', [PaymentController::class, 'index'])->name('v1.payments.index'); - Route::post('', [PaymentController::class, 'store'])->name('v1.payments.store'); - Route::get('/summary', [PaymentController::class, 'summary'])->name('v1.payments.summary'); - Route::get('/{id}', [PaymentController::class, 'show'])->whereNumber('id')->name('v1.payments.show'); - Route::post('/{id}/complete', [PaymentController::class, 'complete'])->whereNumber('id')->name('v1.payments.complete'); - Route::post('/{id}/cancel', [PaymentController::class, 'cancel'])->whereNumber('id')->name('v1.payments.cancel'); - Route::post('/{id}/refund', [PaymentController::class, 'refund'])->whereNumber('id')->name('v1.payments.refund'); - Route::get('/{id}/statement', [PaymentController::class, 'statement'])->whereNumber('id')->name('v1.payments.statement'); - }); - - // Company API (회사 추가 관리) - Route::prefix('companies')->group(function () { - Route::post('/check', [CompanyController::class, 'check'])->name('v1.companies.check'); // 사업자등록번호 검증 - Route::post('/request', [CompanyController::class, 'request'])->name('v1.companies.request'); // 회사 추가 신청 - Route::get('/requests', [CompanyController::class, 'requests'])->name('v1.companies.requests.index'); // 신청 목록 (관리자) - Route::get('/requests/{id}', [CompanyController::class, 'showRequest'])->whereNumber('id')->name('v1.companies.requests.show'); // 신청 상세 - Route::post('/requests/{id}/approve', [CompanyController::class, 'approve'])->whereNumber('id')->name('v1.companies.requests.approve'); // 승인 - Route::post('/requests/{id}/reject', [CompanyController::class, 'reject'])->whereNumber('id')->name('v1.companies.requests.reject'); // 반려 - Route::get('/my-requests', [CompanyController::class, 'myRequests'])->name('v1.companies.my-requests'); // 내 신청 목록 - }); - - // Sale API (매출 관리) - Route::prefix('sales')->group(function () { - Route::get('', [SaleController::class, 'index'])->name('v1.sales.index'); - Route::post('', [SaleController::class, 'store'])->name('v1.sales.store'); - Route::get('/summary', [SaleController::class, 'summary'])->name('v1.sales.summary'); - Route::get('/{id}', [SaleController::class, 'show'])->whereNumber('id')->name('v1.sales.show'); - Route::put('/{id}', [SaleController::class, 'update'])->whereNumber('id')->name('v1.sales.update'); - Route::delete('/{id}', [SaleController::class, 'destroy'])->whereNumber('id')->name('v1.sales.destroy'); - Route::post('/{id}/confirm', [SaleController::class, 'confirm'])->whereNumber('id')->name('v1.sales.confirm'); - Route::put('/bulk-update-account', [SaleController::class, 'bulkUpdateAccountCode'])->name('v1.sales.bulk-update-account'); - // 거래명세서 API - Route::get('/{id}/statement', [SaleController::class, 'getStatement'])->whereNumber('id')->name('v1.sales.statement.show'); - Route::post('/{id}/statement/issue', [SaleController::class, 'issueStatement'])->whereNumber('id')->name('v1.sales.statement.issue'); - Route::post('/{id}/statement/send', [SaleController::class, 'sendStatement'])->whereNumber('id')->name('v1.sales.statement.send'); - Route::post('/bulk-issue-statement', [SaleController::class, 'bulkIssueStatement'])->name('v1.sales.bulk-issue-statement'); - }); - - // Purchase API (매입 관리) - Route::prefix('purchases')->group(function () { - Route::get('', [PurchaseController::class, 'index'])->name('v1.purchases.index'); - Route::post('', [PurchaseController::class, 'store'])->name('v1.purchases.store'); - Route::get('/summary', [PurchaseController::class, 'summary'])->name('v1.purchases.summary'); - Route::get('/dashboard-detail', [PurchaseController::class, 'dashboardDetail'])->name('v1.purchases.dashboard-detail'); - Route::post('/bulk-update-type', [PurchaseController::class, 'bulkUpdatePurchaseType'])->name('v1.purchases.bulk-update-type'); - Route::post('/bulk-update-tax-received', [PurchaseController::class, 'bulkUpdateTaxReceived'])->name('v1.purchases.bulk-update-tax-received'); - Route::get('/{id}', [PurchaseController::class, 'show'])->whereNumber('id')->name('v1.purchases.show'); - Route::put('/{id}', [PurchaseController::class, 'update'])->whereNumber('id')->name('v1.purchases.update'); - Route::delete('/{id}', [PurchaseController::class, 'destroy'])->whereNumber('id')->name('v1.purchases.destroy'); - Route::post('/{id}/confirm', [PurchaseController::class, 'confirm'])->whereNumber('id')->name('v1.purchases.confirm'); - }); - - // Receiving API (입고 관리) - Route::prefix('receivings')->group(function () { - Route::get('', [ReceivingController::class, 'index'])->name('v1.receivings.index'); - Route::post('', [ReceivingController::class, 'store'])->name('v1.receivings.store'); - Route::get('/stats', [ReceivingController::class, 'stats'])->name('v1.receivings.stats'); - Route::get('/{id}', [ReceivingController::class, 'show'])->whereNumber('id')->name('v1.receivings.show'); - Route::put('/{id}', [ReceivingController::class, 'update'])->whereNumber('id')->name('v1.receivings.update'); - Route::delete('/{id}', [ReceivingController::class, 'destroy'])->whereNumber('id')->name('v1.receivings.destroy'); - Route::post('/{id}/process', [ReceivingController::class, 'process'])->whereNumber('id')->name('v1.receivings.process'); - }); - - // Stock API (재고 현황) - Route::prefix('stocks')->group(function () { - Route::get('', [StockController::class, 'index'])->name('v1.stocks.index'); - Route::get('/stats', [StockController::class, 'stats'])->name('v1.stocks.stats'); - Route::get('/stats-by-type', [StockController::class, 'statsByItemType'])->name('v1.stocks.stats-by-type'); - Route::get('/{id}', [StockController::class, 'show'])->whereNumber('id')->name('v1.stocks.show'); - }); - - // Shipment API (출하 관리) - Route::prefix('shipments')->group(function () { - Route::get('', [ShipmentController::class, 'index'])->name('v1.shipments.index'); - Route::get('/stats', [ShipmentController::class, 'stats'])->name('v1.shipments.stats'); - Route::get('/stats-by-status', [ShipmentController::class, 'statsByStatus'])->name('v1.shipments.stats-by-status'); - Route::get('/options/lots', [ShipmentController::class, 'lotOptions'])->name('v1.shipments.options.lots'); - Route::get('/options/logistics', [ShipmentController::class, 'logisticsOptions'])->name('v1.shipments.options.logistics'); - Route::get('/options/vehicle-tonnage', [ShipmentController::class, 'vehicleTonnageOptions'])->name('v1.shipments.options.vehicle-tonnage'); - Route::post('', [ShipmentController::class, 'store'])->name('v1.shipments.store'); - Route::get('/{id}', [ShipmentController::class, 'show'])->whereNumber('id')->name('v1.shipments.show'); - Route::put('/{id}', [ShipmentController::class, 'update'])->whereNumber('id')->name('v1.shipments.update'); - Route::patch('/{id}/status', [ShipmentController::class, 'updateStatus'])->whereNumber('id')->name('v1.shipments.status'); - Route::delete('/{id}', [ShipmentController::class, 'destroy'])->whereNumber('id')->name('v1.shipments.destroy'); - }); - - // Barobill Setting API (바로빌 설정) - Route::prefix('barobill-settings')->group(function () { - Route::get('', [BarobillSettingController::class, 'show'])->name('v1.barobill-settings.show'); - Route::put('', [BarobillSettingController::class, 'save'])->name('v1.barobill-settings.save'); - Route::post('/test-connection', [BarobillSettingController::class, 'testConnection'])->name('v1.barobill-settings.test-connection'); - }); - - // Tax Invoice API (세금계산서) - Route::prefix('tax-invoices')->group(function () { - Route::get('', [TaxInvoiceController::class, 'index'])->name('v1.tax-invoices.index'); - Route::post('', [TaxInvoiceController::class, 'store'])->name('v1.tax-invoices.store'); - Route::get('/summary', [TaxInvoiceController::class, 'summary'])->name('v1.tax-invoices.summary'); - Route::get('/{id}', [TaxInvoiceController::class, 'show'])->whereNumber('id')->name('v1.tax-invoices.show'); - Route::put('/{id}', [TaxInvoiceController::class, 'update'])->whereNumber('id')->name('v1.tax-invoices.update'); - Route::delete('/{id}', [TaxInvoiceController::class, 'destroy'])->whereNumber('id')->name('v1.tax-invoices.destroy'); - Route::post('/{id}/issue', [TaxInvoiceController::class, 'issue'])->whereNumber('id')->name('v1.tax-invoices.issue'); - Route::post('/{id}/cancel', [TaxInvoiceController::class, 'cancel'])->whereNumber('id')->name('v1.tax-invoices.cancel'); - Route::get('/{id}/check-status', [TaxInvoiceController::class, 'checkStatus'])->whereNumber('id')->name('v1.tax-invoices.check-status'); - Route::post('/bulk-issue', [TaxInvoiceController::class, 'bulkIssue'])->name('v1.tax-invoices.bulk-issue'); - }); - - // Bad Debt API (악성채권 추심관리) - Route::prefix('bad-debts')->group(function () { - Route::get('', [BadDebtController::class, 'index'])->name('v1.bad-debts.index'); - Route::post('', [BadDebtController::class, 'store'])->name('v1.bad-debts.store'); - Route::get('/summary', [BadDebtController::class, 'summary'])->name('v1.bad-debts.summary'); - Route::get('/{id}', [BadDebtController::class, 'show'])->whereNumber('id')->name('v1.bad-debts.show'); - Route::put('/{id}', [BadDebtController::class, 'update'])->whereNumber('id')->name('v1.bad-debts.update'); - Route::delete('/{id}', [BadDebtController::class, 'destroy'])->whereNumber('id')->name('v1.bad-debts.destroy'); - Route::patch('/{id}/toggle', [BadDebtController::class, 'toggle'])->whereNumber('id')->name('v1.bad-debts.toggle'); - // 서류 - Route::post('/{id}/documents', [BadDebtController::class, 'addDocument'])->whereNumber('id')->name('v1.bad-debts.documents.store'); - Route::delete('/{id}/documents/{documentId}', [BadDebtController::class, 'removeDocument'])->whereNumber(['id', 'documentId'])->name('v1.bad-debts.documents.destroy'); - // 메모 - Route::post('/{id}/memos', [BadDebtController::class, 'addMemo'])->whereNumber('id')->name('v1.bad-debts.memos.store'); - Route::delete('/{id}/memos/{memoId}', [BadDebtController::class, 'removeMemo'])->whereNumber(['id', 'memoId'])->name('v1.bad-debts.memos.destroy'); - }); - - // Bill API (어음관리) - Route::prefix('bills')->group(function () { - Route::get('', [BillController::class, 'index'])->name('v1.bills.index'); - Route::post('', [BillController::class, 'store'])->name('v1.bills.store'); - Route::get('/summary', [BillController::class, 'summary'])->name('v1.bills.summary'); - Route::get('/dashboard-detail', [BillController::class, 'dashboardDetail'])->name('v1.bills.dashboard-detail'); - Route::get('/{id}', [BillController::class, 'show'])->whereNumber('id')->name('v1.bills.show'); - Route::put('/{id}', [BillController::class, 'update'])->whereNumber('id')->name('v1.bills.update'); - Route::delete('/{id}', [BillController::class, 'destroy'])->whereNumber('id')->name('v1.bills.destroy'); - Route::patch('/{id}/status', [BillController::class, 'updateStatus'])->whereNumber('id')->name('v1.bills.update-status'); - }); - - // Popup API (팝업관리) - Route::prefix('popups')->group(function () { - Route::get('', [PopupController::class, 'index'])->name('v1.popups.index'); - Route::post('', [PopupController::class, 'store'])->name('v1.popups.store'); - Route::get('/active', [PopupController::class, 'active'])->name('v1.popups.active'); - Route::get('/{id}', [PopupController::class, 'show'])->whereNumber('id')->name('v1.popups.show'); - Route::put('/{id}', [PopupController::class, 'update'])->whereNumber('id')->name('v1.popups.update'); - Route::delete('/{id}', [PopupController::class, 'destroy'])->whereNumber('id')->name('v1.popups.destroy'); - }); - - // Report API (보고서) - Route::prefix('reports')->group(function () { - Route::get('/daily', [ReportController::class, 'daily'])->name('v1.reports.daily'); - Route::get('/daily/export', [ReportController::class, 'dailyExport'])->name('v1.reports.daily.export'); - Route::get('/expense-estimate', [ReportController::class, 'expenseEstimate'])->name('v1.reports.expense-estimate'); - Route::get('/expense-estimate/export', [ReportController::class, 'expenseEstimateExport'])->name('v1.reports.expense-estimate.export'); - - // AI Report API (AI 리포트) - Route::get('/ai', [AiReportController::class, 'index'])->name('v1.reports.ai.index'); - Route::post('/ai/generate', [AiReportController::class, 'generate'])->name('v1.reports.ai.generate'); - Route::get('/ai/{id}', [AiReportController::class, 'show'])->whereNumber('id')->name('v1.reports.ai.show'); - Route::delete('/ai/{id}', [AiReportController::class, 'destroy'])->whereNumber('id')->name('v1.reports.ai.destroy'); - }); - - // Dashboard API (대시보드) - Route::prefix('dashboard')->group(function () { - Route::get('/summary', [DashboardController::class, 'summary'])->name('v1.dashboard.summary'); - Route::get('/charts', [DashboardController::class, 'charts'])->name('v1.dashboard.charts'); - Route::get('/approvals', [DashboardController::class, 'approvals'])->name('v1.dashboard.approvals'); - }); - - // Permission API - Route::prefix('permissions')->group(function () { - Route::get('departments/{dept_id}/menu-matrix', [PermissionController::class, 'deptMenuMatrix'])->name('v1.permissions.deptMenuMatrix'); // 부서별 권한 메트릭스 - Route::get('roles/{role_id}/menu-matrix', [PermissionController::class, 'roleMenuMatrix'])->name('v1.permissions.roleMenuMatrix'); // 부서별 권한 메트릭스 - Route::get('users/{user_id}/menu-matrix', [PermissionController::class, 'userMenuMatrix'])->name('v1.permissions.userMenuMatrix'); // 부서별 권한 메트릭스 - }); - - // Settings & Configuration (설정 및 환경설정 통합 관리) - Route::prefix('settings')->group(function () { - - // 근무 설정 - Route::get('/work', [WorkSettingController::class, 'showWorkSetting'])->name('v1.settings.work.show'); - Route::put('/work', [WorkSettingController::class, 'updateWorkSetting'])->name('v1.settings.work.update'); - - // 출퇴근 설정 - Route::get('/attendance', [WorkSettingController::class, 'showAttendanceSetting'])->name('v1.settings.attendance.show'); - Route::put('/attendance', [WorkSettingController::class, 'updateAttendanceSetting'])->name('v1.settings.attendance.update'); - - // 급여 설정 - Route::get('/payroll', [PayrollController::class, 'getSettings'])->name('v1.settings.payroll.show'); - Route::put('/payroll', [PayrollController::class, 'updateSettings'])->name('v1.settings.payroll.update'); - - // 테넌트 필드 설정 (기존 fields에서 이동) - Route::get('/fields', [TenantFieldSettingController::class, 'index'])->name('v1.settings.fields.index'); // 필드 설정 목록(전역+테넌트 병합 효과값) - Route::put('/fields/bulk', [TenantFieldSettingController::class, 'bulkUpsert'])->name('v1.settings.fields.bulk'); // 필드 설정 대량 저장(트랜잭션 처리) - Route::patch('/fields/{key}', [TenantFieldSettingController::class, 'updateOne'])->name('v1.settings.fields.update'); // 필드 설정 단건 수정/업데이트 - - // 옵션 그룹/값 (기존 opt-groups에서 이동) - Route::get('/options', [TenantOptionGroupController::class, 'index'])->name('v1.settings.options.index'); // 옵션 그룹 목록 - Route::post('/options', [TenantOptionGroupController::class, 'store'])->name('v1.settings.options.store'); // 옵션 그룹 생성 - Route::get('/options/{id}', [TenantOptionGroupController::class, 'show'])->name('v1.settings.options.show'); // 옵션 그룹 단건 조회 - Route::patch('/options/{id}', [TenantOptionGroupController::class, 'update'])->name('v1.settings.options.update'); // 옵션 그룹 수정 - Route::delete('/options/{id}', [TenantOptionGroupController::class, 'destroy'])->name('v1.settings.options.destroy'); // 옵션 그룹 삭제 - Route::get('/options/{gid}/values', [TenantOptionValueController::class, 'index'])->name('v1.settings.options.values.index'); // 옵션 값 목록 - Route::post('/options/{gid}/values', [TenantOptionValueController::class, 'store'])->name('v1.settings.options.values.store'); // 옵션 값 생성 - Route::get('/options/{gid}/values/{id}', [TenantOptionValueController::class, 'show'])->name('v1.settings.options.values.show'); // 옵션 값 단건 조회 - Route::patch('/options/{gid}/values/{id}', [TenantOptionValueController::class, 'update'])->name('v1.settings.options.values.update'); // 옵션 값 수정 - Route::delete('/options/{gid}/values/{id}', [TenantOptionValueController::class, 'destroy'])->name('v1.settings.options.values.destroy'); // 옵션 값 삭제 - Route::patch('/options/{gid}/values/reorder', [TenantOptionValueController::class, 'reorder'])->name('v1.settings.options.values.reorder'); // 옵션 값 정렬순서 재배치 - - // 공통 코드 관리 (기존 common에서 이동) - Route::get('/common/code', [CommonController::class, 'getComeCode'])->name('v1.settings.common.code'); // 공통코드 조회 (기존 v1.common.code에서 이동) - Route::get('/common', [CommonController::class, 'list'])->name('v1.settings.common.list'); // 공통 코드 목록 - Route::get('/common/{group}', [CommonController::class, 'index'])->name('v1.settings.common.index'); // 특정 그룹 코드 목록 - Route::post('/common', [CommonController::class, 'store'])->name('v1.settings.common.store'); // 공통 코드 생성 - Route::patch('/common/{id}', [CommonController::class, 'update'])->name('v1.settings.common.update'); // 공통 코드 수정 - Route::delete('/common/{id}', [CommonController::class, 'destroy'])->name('v1.settings.common.destroy'); // 공통 코드 삭제 - - // 알림 설정 (그룹 기반, React 호환) - auth:sanctum 필수 - Route::middleware('auth:sanctum')->group(function () { - Route::get('/notifications', [NotificationSettingController::class, 'indexGrouped'])->name('v1.settings.notifications.index'); // 알림 설정 조회 (그룹 기반) - Route::put('/notifications', [NotificationSettingController::class, 'updateGrouped'])->name('v1.settings.notifications.update'); // 알림 설정 수정 (그룹 기반) - }); - }); - - // Push Notification API (FCM 푸시 알림) - auth:sanctum 필수 (tenantId, apiUserId 필요) - Route::prefix('push')->middleware('auth:sanctum')->group(function () { - Route::post('/register-token', [PushNotificationController::class, 'registerToken'])->name('v1.push.register-token'); // FCM 토큰 등록 - Route::post('/unregister-token', [PushNotificationController::class, 'unregisterToken'])->name('v1.push.unregister-token'); // FCM 토큰 해제 - Route::get('/tokens', [PushNotificationController::class, 'getTokens'])->name('v1.push.tokens'); // 등록된 토큰 목록 - Route::get('/settings', [PushNotificationController::class, 'getSettings'])->name('v1.push.settings'); // 알림 설정 조회 - Route::put('/settings', [PushNotificationController::class, 'updateSettings'])->name('v1.push.settings.update'); // 알림 설정 수정 - Route::get('/notification-types', [PushNotificationController::class, 'getNotificationTypes'])->name('v1.push.notification-types'); // 알림 유형 목록 - }); - - // Admin FCM API (MNG 관리자용 FCM 발송) - API Key 인증만 사용 - Route::prefix('admin/fcm')->group(function () { - Route::post('/send', [FcmController::class, 'send'])->name('v1.admin.fcm.send'); // FCM 발송 - Route::get('/preview-count', [FcmController::class, 'previewCount'])->name('v1.admin.fcm.preview'); // 대상 토큰 수 미리보기 - Route::get('/tokens', [FcmController::class, 'tokens'])->name('v1.admin.fcm.tokens'); // 토큰 목록 - Route::get('/tokens/stats', [FcmController::class, 'tokenStats'])->name('v1.admin.fcm.tokens.stats'); // 토큰 통계 - Route::patch('/tokens/{id}/toggle', [FcmController::class, 'toggleToken'])->name('v1.admin.fcm.tokens.toggle'); // 토큰 상태 토글 - Route::delete('/tokens/{id}', [FcmController::class, 'deleteToken'])->name('v1.admin.fcm.tokens.delete'); // 토큰 삭제 - Route::get('/history', [FcmController::class, 'history'])->name('v1.admin.fcm.history'); // 발송 이력 - }); - - // 회원 프로필(테넌트 기준) - Route::prefix('profiles')->group(function () { - Route::get('', [TenantUserProfileController::class, 'index'])->name('v1.profiles.index'); // 프로필 목록(테넌트 기준) - // /me 라우트는 /{userId} 와일드카드보다 먼저 정의해야 함 - // auth:sanctum 미들웨어로 Bearer 토큰 인증 필요 - Route::middleware('auth:sanctum')->group(function () { - Route::get('/me', [TenantUserProfileController::class, 'me'])->name('v1.profiles.me'); // 내 프로필 조회 - Route::patch('/me', [TenantUserProfileController::class, 'updateMe'])->name('v1.profiles.me.update'); // 내 프로필 수정 - }); - Route::get('/{userId}', [TenantUserProfileController::class, 'show'])->name('v1.profiles.show'); // 특정 사용자 프로필 조회 - Route::patch('/{userId}', [TenantUserProfileController::class, 'update'])->name('v1.profiles.update'); // 특정 사용자 프로필 수정(관리자) - }); - - // Category API (통합) - Route::prefix('categories')->group(function () { - - // === 확장 기능 (와일드카드 라우트보다 먼저 정의) === - Route::get('/tree', [CategoryController::class, 'tree'])->name('v1.categories.tree'); // 트리 - Route::post('/reorder', [CategoryController::class, 'reorder'])->name('v1.categories.reorder'); // 정렬 일괄 - - // === 기본 Category CRUD === - Route::get('', [CategoryController::class, 'index'])->name('v1.categories.index'); // 목록(페이징) - Route::post('', [CategoryController::class, 'store'])->name('v1.categories.store'); // 생성 - Route::get('/{id}', [CategoryController::class, 'show'])->name('v1.categories.show'); // 단건 - Route::patch('/{id}', [CategoryController::class, 'update'])->name('v1.categories.update'); // 수정 - Route::delete('/{id}', [CategoryController::class, 'destroy'])->name('v1.categories.destroy'); // 삭제(soft) - Route::post('/{id}/toggle', [CategoryController::class, 'toggle'])->name('v1.categories.toggle'); // 활성 토글 - Route::patch('/{id}/move', [CategoryController::class, 'move'])->name('v1.categories.move'); // 부모/순서 이동 - - // === Category Fields === - // 목록/생성 (카테고리 기준) - Route::get('/{id}/fields', [CategoryFieldController::class, 'index'])->name('v1.categories.fields.index'); // ?page&size&sort&order - Route::post('/{id}/fields', [CategoryFieldController::class, 'store'])->name('v1.categories.fields.store'); - // 단건 - Route::get('/fields/{field}', [CategoryFieldController::class, 'show'])->name('v1.categories.fields.show'); - Route::patch('/fields/{field}', [CategoryFieldController::class, 'update'])->name('v1.categories.fields.update'); - Route::delete('/fields/{field}', [CategoryFieldController::class, 'destroy'])->name('v1.categories.fields.destroy'); - // 일괄 정렬/업서트 - Route::post('/{id}/fields/reorder', [CategoryFieldController::class, 'reorder'])->name('v1.categories.fields.reorder'); // [{id,sort_order}] - Route::put('/{id}/fields/bulk-upsert', [CategoryFieldController::class, 'bulkUpsert'])->name('v1.categories.fields.bulk'); // [{id?,field_key,...}] - - // === Category Templates === - // 버전 목록/생성 (카테고리 기준) - Route::get('/{id}/templates', [CategoryTemplateController::class, 'index'])->name('v1.categories.templates.index'); // ?page&size - Route::post('/{id}/templates', [CategoryTemplateController::class, 'store'])->name('v1.categories.templates.store'); // 새 버전 등록 - // 단건 - Route::get('/templates/{tpl}', [CategoryTemplateController::class, 'show'])->name('v1.categories.templates.show'); - Route::patch('/templates/{tpl}', [CategoryTemplateController::class, 'update'])->name('v1.categories.templates.update'); // remarks 등 메타 수정 - Route::delete('/templates/{tpl}', [CategoryTemplateController::class, 'destroy'])->name('v1.categories.templates.destroy'); - // 운영 편의 - Route::post('/{id}/templates/{tpl}/apply', [CategoryTemplateController::class, 'apply'])->name('v1.categories.templates.apply'); // 해당 버전 활성화 - Route::get('/{id}/templates/{tpl}/preview', [CategoryTemplateController::class, 'preview'])->name('v1.categories.templates.preview'); // 렌더용 스냅샷 - // (선택) 버전 간 diff - Route::get('/{id}/templates/diff', [CategoryTemplateController::class, 'diff'])->name('v1.categories.templates.diff'); // ?a=ver&b=ver - - // === Category Logs === - Route::get('/{id}/logs', [CategoryLogController::class, 'index'])->name('v1.categories.logs.index'); // ?action=&from=&to=&page&size - Route::get('/logs/{log}', [CategoryLogController::class, 'show'])->name('v1.categories.logs.show'); - // (선택) 특정 변경 시점으로 카테고리 복구(템플릿/필드와 별개) - // Route::post('{id}/logs/{log}/restore', [CategoryLogController::class, 'restore'])->name('v1.categories.logs.restore'); - }); - - // Classifications API - Route::prefix('classifications')->group(function () { - Route::get('', [ClassificationController::class, 'index'])->name('v1.classifications.index'); // 목록 - Route::post('', [ClassificationController::class, 'store'])->name('v1.classifications.store'); // 생성 - Route::get('/{id}', [ClassificationController::class, 'show'])->whereNumber('id')->name('v1.classifications.show'); // 단건 - Route::patch('/{id}', [ClassificationController::class, 'update'])->whereNumber('id')->name('v1.classifications.update'); // 수정 - Route::delete('/{id}', [ClassificationController::class, 'destroy'])->whereNumber('id')->name('v1.classifications.destroy'); // 삭제 - }); - - // Clients (거래처 관리) - Route::prefix('clients')->group(function () { - Route::get('/stats', [ClientController::class, 'stats'])->name('v1.clients.stats'); // 통계 - Route::delete('/bulk', [ClientController::class, 'bulkDestroy'])->name('v1.clients.bulk-destroy'); // 일괄 삭제 - Route::get('', [ClientController::class, 'index'])->name('v1.clients.index'); // 목록 - Route::post('', [ClientController::class, 'store'])->name('v1.clients.store'); // 생성 - Route::get('/{id}', [ClientController::class, 'show'])->whereNumber('id')->name('v1.clients.show'); // 단건 - Route::put('/{id}', [ClientController::class, 'update'])->whereNumber('id')->name('v1.clients.update'); // 수정 - Route::delete('/{id}', [ClientController::class, 'destroy'])->whereNumber('id')->name('v1.clients.destroy'); // 삭제 - Route::patch('/{id}/toggle', [ClientController::class, 'toggle'])->whereNumber('id')->name('v1.clients.toggle'); // 활성/비활성 - }); - - // Client Groups (고객 그룹 관리) - Route::prefix('client-groups')->group(function () { - Route::get('', [ClientGroupController::class, 'index'])->name('v1.client-groups.index'); // 목록 - Route::post('', [ClientGroupController::class, 'store'])->name('v1.client-groups.store'); // 생성 - Route::get('/{id}', [ClientGroupController::class, 'show'])->whereNumber('id')->name('v1.client-groups.show'); // 단건 - Route::put('/{id}', [ClientGroupController::class, 'update'])->whereNumber('id')->name('v1.client-groups.update'); // 수정 - Route::delete('/{id}', [ClientGroupController::class, 'destroy'])->whereNumber('id')->name('v1.client-groups.destroy'); // 삭제 - Route::patch('/{id}/toggle', [ClientGroupController::class, 'toggle'])->whereNumber('id')->name('v1.client-groups.toggle'); // 활성/비활성 - }); - - // Quotes (견적 관리) - Route::prefix('quotes')->group(function () { - // 기본 CRUD - Route::get('', [QuoteController::class, 'index'])->name('v1.quotes.index'); // 목록 - Route::post('', [QuoteController::class, 'store'])->name('v1.quotes.store'); // 생성 - Route::get('/{id}', [QuoteController::class, 'show'])->whereNumber('id')->name('v1.quotes.show'); // 단건 - Route::put('/{id}', [QuoteController::class, 'update'])->whereNumber('id')->name('v1.quotes.update'); // 수정 - Route::delete('/{id}', [QuoteController::class, 'destroy'])->whereNumber('id')->name('v1.quotes.destroy'); // 삭제 - - // 일괄 삭제 - Route::delete('/bulk', [QuoteController::class, 'bulkDestroy'])->name('v1.quotes.bulk-destroy'); // 일괄 삭제 - - // 상태 관리 - Route::post('/{id}/finalize', [QuoteController::class, 'finalize'])->whereNumber('id')->name('v1.quotes.finalize'); // 확정 - Route::post('/{id}/cancel-finalize', [QuoteController::class, 'cancelFinalize'])->whereNumber('id')->name('v1.quotes.cancel-finalize'); // 확정 취소 - Route::post('/{id}/convert', [QuoteController::class, 'convertToOrder'])->whereNumber('id')->name('v1.quotes.convert'); // 수주 전환 - - // 견적번호 미리보기 - Route::get('/number/preview', [QuoteController::class, 'previewNumber'])->name('v1.quotes.number-preview'); // 번호 미리보기 - - // 자동산출 - Route::get('/calculation/schema', [QuoteController::class, 'calculationSchema'])->name('v1.quotes.calculation-schema'); // 입력 스키마 - Route::post('/calculate', [QuoteController::class, 'calculate'])->name('v1.quotes.calculate'); // 자동산출 실행 - Route::post('/calculate/bom', [QuoteController::class, 'calculateBom'])->name('v1.quotes.calculate-bom'); // BOM 기반 자동산출 - Route::post('/calculate/bom/bulk', [QuoteController::class, 'calculateBomBulk'])->name('v1.quotes.calculate-bom-bulk'); // 다건 BOM 자동산출 - - // 문서 관리 - Route::post('/{id}/pdf', [QuoteController::class, 'generatePdf'])->whereNumber('id')->name('v1.quotes.pdf'); // PDF 생성 - Route::post('/{id}/send/email', [QuoteController::class, 'sendEmail'])->whereNumber('id')->name('v1.quotes.send-email'); // 이메일 발송 - Route::post('/{id}/send/kakao', [QuoteController::class, 'sendKakao'])->whereNumber('id')->name('v1.quotes.send-kakao'); // 카카오톡 발송 - Route::get('/{id}/send/history', [QuoteController::class, 'sendHistory'])->whereNumber('id')->name('v1.quotes.send-history'); // 발송 이력 - - // 입찰 전환 - Route::post('/{id}/convert-to-bidding', [QuoteController::class, 'convertToBidding'])->whereNumber('id')->name('v1.quotes.convert-to-bidding'); // 입찰 전환 - }); - - // Biddings (입찰관리) - Route::prefix('biddings')->group(function () { - Route::get('', [BiddingController::class, 'index'])->name('v1.biddings.index'); // 목록 - Route::post('', [BiddingController::class, 'store'])->name('v1.biddings.store'); // 생성 - Route::get('/stats', [BiddingController::class, 'stats'])->name('v1.biddings.stats'); // 통계 - Route::delete('/bulk', [BiddingController::class, 'bulkDestroy'])->name('v1.biddings.bulk-destroy'); // 일괄 삭제 - Route::get('/{id}', [BiddingController::class, 'show'])->whereNumber('id')->name('v1.biddings.show'); // 단건 - Route::put('/{id}', [BiddingController::class, 'update'])->whereNumber('id')->name('v1.biddings.update'); // 수정 - Route::delete('/{id}', [BiddingController::class, 'destroy'])->whereNumber('id')->name('v1.biddings.destroy'); // 삭제 - Route::patch('/{id}/status', [BiddingController::class, 'updateStatus'])->whereNumber('id')->name('v1.biddings.status'); // 상태 변경 - }); - - // Pricing (단가 관리) - Route::prefix('pricing')->group(function () { - Route::get('', [PricingController::class, 'index'])->name('v1.pricing.index'); // 목록 - Route::get('/stats', [PricingController::class, 'stats'])->name('v1.pricing.stats'); // 통계 - Route::get('/cost', [PricingController::class, 'cost'])->name('v1.pricing.cost'); // 원가 조회 - Route::post('/by-items', [PricingController::class, 'byItems'])->name('v1.pricing.by-items'); // 품목별 단가 현황 - Route::delete('/bulk', [PricingController::class, 'bulkDestroy'])->name('v1.pricing.bulk-destroy'); // 일괄 삭제 - Route::post('', [PricingController::class, 'store'])->name('v1.pricing.store'); // 등록 - Route::get('/{id}', [PricingController::class, 'show'])->whereNumber('id')->name('v1.pricing.show'); // 상세 - Route::put('/{id}', [PricingController::class, 'update'])->whereNumber('id')->name('v1.pricing.update'); // 수정 - Route::delete('/{id}', [PricingController::class, 'destroy'])->whereNumber('id')->name('v1.pricing.destroy'); // 삭제 - Route::post('/{id}/finalize', [PricingController::class, 'finalize'])->whereNumber('id')->name('v1.pricing.finalize'); // 확정 - Route::get('/{id}/revisions', [PricingController::class, 'revisions'])->whereNumber('id')->name('v1.pricing.revisions'); // 변경이력 - }); - - // Labor (노임관리) - Route::prefix('labor')->group(function () { - Route::get('', [LaborController::class, 'index'])->name('v1.labor.index'); // 목록 - Route::get('/stats', [LaborController::class, 'stats'])->name('v1.labor.stats'); // 통계 - Route::delete('/bulk', [LaborController::class, 'bulkDestroy'])->name('v1.labor.bulk-destroy'); // 일괄 삭제 - Route::post('', [LaborController::class, 'store'])->name('v1.labor.store'); // 등록 - Route::get('/{id}', [LaborController::class, 'show'])->whereNumber('id')->name('v1.labor.show'); // 상세 - Route::put('/{id}', [LaborController::class, 'update'])->whereNumber('id')->name('v1.labor.update'); // 수정 - Route::delete('/{id}', [LaborController::class, 'destroy'])->whereNumber('id')->name('v1.labor.destroy'); // 삭제 - }); - - // REMOVED: Products & Materials 라우트 삭제됨 (products/materials 테이블 삭제) - // 모든 품목 관리는 /items 엔드포인트 사용 - - // Items (통합 품목 관리 - items 테이블) - Route::prefix('items')->group(function () { - Route::get('', [ItemsController::class, 'index'])->name('v1.items.index'); // 통합 목록 - Route::get('/stats', [ItemsController::class, 'stats'])->name('v1.items.stats'); // 통계 - 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 - 전체 BOM 목록 (item_id 없이) - // 주의: /items/{id}/bom 보다 먼저 정의해야 함 ('bom'이 {id}로 인식되지 않도록) - Route::get('items/bom', [ItemsBomController::class, 'listAll'])->name('v1.items.bom.list-all'); - - // Items BOM (ID-based BOM API) - Route::prefix('items/{id}/bom')->group(function () { - 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 (group_id 기반 파일 관리, 동적 field_key 지원) - Route::prefix('items/{id}/files')->group(function () { - Route::get('', [ItemsFileController::class, 'index'])->name('v1.items.files.index'); // 파일 조회 (field_key별 그룹핑) - Route::post('', [ItemsFileController::class, 'upload'])->name('v1.items.files.upload'); // 파일 업로드 (field_key 동적) - Route::delete('/{fileId}', [ItemsFileController::class, 'delete'])->name('v1.items.files.delete'); // 파일 삭제 (file_id) - }); - - // REMOVED: products/{id}/bom 라우트 삭제됨 (product_components 테이블 삭제) - // BOM 관리는 /items/{id}/bom 엔드포인트 사용 - - // 설계 전용 (Design) - 운영과 분리된 네임스페이스/경로 - Route::prefix('design')->group(function () { - Route::get('/models', [DesignModelController::class, 'index'])->name('v1.design.models.index'); - Route::post('/models', [DesignModelController::class, 'store'])->name('v1.design.models.store'); - Route::get('/models/{id}', [DesignModelController::class, 'show'])->name('v1.design.models.show'); - Route::put('/models/{id}', [DesignModelController::class, 'update'])->name('v1.design.models.update'); - Route::delete('/models/{id}', [DesignModelController::class, 'destroy'])->name('v1.design.models.destroy'); - - Route::get('/models/{modelId}/versions', [DesignModelVersionController::class, 'index'])->name('v1.design.models.versions.index'); - Route::post('/models/{modelId}/versions', [DesignModelVersionController::class, 'createDraft'])->name('v1.design.models.versions.store'); - Route::post('/versions/{versionId}/release', [DesignModelVersionController::class, 'release'])->name('v1.design.versions.release'); - - Route::get('/versions/{versionId}/bom-templates', [DesignBomTemplateController::class, 'listByVersion'])->name('v1.design.bom.templates.index'); - Route::post('/versions/{versionId}/bom-templates', [DesignBomTemplateController::class, 'upsertTemplate'])->name('v1.design.bom.templates.store'); - Route::get('/bom-templates/{templateId}', [DesignBomTemplateController::class, 'show'])->name('v1.design.bom.templates.show'); - Route::put('/bom-templates/{templateId}/items', [DesignBomTemplateController::class, 'replaceItems'])->name('v1.design.bom.templates.items.replace'); - Route::get('/bom-templates/{templateId}/diff', [DesignBomTemplateController::class, 'diff'])->name('v1.design.bom.templates.diff'); - Route::post('/bom-templates/{templateId}/clone', [DesignBomTemplateController::class, 'cloneTemplate'])->name('v1.design.bom.templates.clone'); - - // 감사 로그 조회 - Route::get('/audit-logs', [DesignAuditLogController::class, 'index'])->name('v1.design.audit-logs.index'); - - // BOM 계산 시스템 - Route::get('/models/{modelId}/estimate-parameters', [BomCalculationController::class, 'getEstimateParameters'])->name('v1.design.models.estimate-parameters'); - Route::post('/bom-templates/{bomTemplateId}/calculate-bom', [BomCalculationController::class, 'calculateBom'])->name('v1.design.bom-templates.calculate-bom'); - Route::get('/companies/{companyName}/formulas', [BomCalculationController::class, 'getCompanyFormulas'])->name('v1.design.companies.formulas'); - Route::post('/companies/{companyName}/formulas/{formulaType}', [BomCalculationController::class, 'saveCompanyFormula'])->name('v1.design.companies.formulas.save'); - Route::post('/formulas/test', [BomCalculationController::class, 'testFormula'])->name('v1.design.formulas.test'); - }); - - // 모델셋 관리 API (견적 시스템) - Route::prefix('model-sets')->group(function () { - Route::get('/', [ModelSetController::class, 'index'])->name('v1.model-sets.index'); // 모델셋 목록 - Route::post('/', [ModelSetController::class, 'store'])->name('v1.model-sets.store'); // 모델셋 생성 - Route::get('/{id}', [ModelSetController::class, 'show'])->name('v1.model-sets.show'); // 모델셋 상세 - Route::put('/{id}', [ModelSetController::class, 'update'])->name('v1.model-sets.update'); // 모델셋 수정 - Route::delete('/{id}', [ModelSetController::class, 'destroy'])->name('v1.model-sets.destroy'); // 모델셋 삭제 - Route::post('/{id}/clone', [ModelSetController::class, 'clone'])->name('v1.model-sets.clone'); // 모델셋 복제 - - // 모델셋 세부 기능 - Route::get('/{id}/fields', [ModelSetController::class, 'getCategoryFields'])->name('v1.model-sets.fields'); // 카테고리 필드 조회 - Route::get('/{id}/bom-templates', [ModelSetController::class, 'getBomTemplates'])->name('v1.model-sets.bom-templates'); // BOM 템플릿 조회 - Route::get('/{id}/estimate-parameters', [ModelSetController::class, 'getEstimateParameters'])->name('v1.model-sets.estimate-parameters'); // 견적 파라미터 - Route::post('/{id}/calculate-bom', [ModelSetController::class, 'calculateBom'])->name('v1.model-sets.calculate-bom'); // BOM 계산 - }); - - // 견적 관리 API - Route::prefix('estimates')->group(function () { - Route::get('/', [EstimateController::class, 'index'])->name('v1.estimates.index'); // 견적 목록 - Route::get('/stats', [EstimateController::class, 'stats'])->name('v1.estimates.stats'); // 견적 통계 - Route::post('/', [EstimateController::class, 'store'])->name('v1.estimates.store'); // 견적 생성 - Route::get('/{id}', [EstimateController::class, 'show'])->name('v1.estimates.show'); // 견적 상세 - Route::put('/{id}', [EstimateController::class, 'update'])->name('v1.estimates.update'); // 견적 수정 - Route::delete('/{id}', [EstimateController::class, 'destroy'])->name('v1.estimates.destroy'); // 견적 삭제 - Route::post('/{id}/clone', [EstimateController::class, 'clone'])->name('v1.estimates.clone'); // 견적 복제 - Route::put('/{id}/status', [EstimateController::class, 'changeStatus'])->name('v1.estimates.status'); // 견적 상태 변경 - - // 견적 폼 및 계산 기능 - Route::get('/form-schema/{model_set_id}', [EstimateController::class, 'getFormSchema'])->name('v1.estimates.form-schema'); // 견적 폼 스키마 - Route::post('/preview/{model_set_id}', [EstimateController::class, 'previewCalculation'])->name('v1.estimates.preview'); // 견적 계산 미리보기 - }); - - // 공정 관리 API (Process Management) - Route::prefix('processes')->group(function () { - Route::get('', [ProcessController::class, 'index'])->name('v1.processes.index'); - Route::get('/options', [ProcessController::class, 'options'])->name('v1.processes.options'); - Route::get('/stats', [ProcessController::class, 'stats'])->name('v1.processes.stats'); - Route::post('', [ProcessController::class, 'store'])->name('v1.processes.store'); - Route::delete('', [ProcessController::class, 'destroyMany'])->name('v1.processes.destroy-many'); - Route::get('/{id}', [ProcessController::class, 'show'])->whereNumber('id')->name('v1.processes.show'); - Route::put('/{id}', [ProcessController::class, 'update'])->whereNumber('id')->name('v1.processes.update'); - Route::delete('/{id}', [ProcessController::class, 'destroy'])->whereNumber('id')->name('v1.processes.destroy'); - Route::patch('/{id}/toggle', [ProcessController::class, 'toggleActive'])->whereNumber('id')->name('v1.processes.toggle'); - }); - - // 수주관리 API (Sales) - Route::prefix('orders')->group(function () { - // 기본 CRUD - Route::get('', [OrderController::class, 'index'])->name('v1.orders.index'); // 목록 - Route::get('/stats', [OrderController::class, 'stats'])->name('v1.orders.stats'); // 통계 - Route::post('', [OrderController::class, 'store'])->name('v1.orders.store'); // 생성 - Route::get('/{id}', [OrderController::class, 'show'])->whereNumber('id')->name('v1.orders.show'); // 상세 - Route::put('/{id}', [OrderController::class, 'update'])->whereNumber('id')->name('v1.orders.update'); // 수정 - Route::delete('/{id}', [OrderController::class, 'destroy'])->whereNumber('id')->name('v1.orders.destroy'); // 삭제 - - // 상태 관리 - Route::patch('/{id}/status', [OrderController::class, 'updateStatus'])->whereNumber('id')->name('v1.orders.status'); // 상태 변경 - - // 견적에서 수주 생성 - Route::post('/from-quote/{quoteId}', [OrderController::class, 'createFromQuote'])->whereNumber('quoteId')->name('v1.orders.from-quote'); - - // 생산지시 생성 - Route::post('/{id}/production-order', [OrderController::class, 'createProductionOrder'])->whereNumber('id')->name('v1.orders.production-order'); - - // 수주확정 되돌리기 - Route::post('/{id}/revert-confirmation', [OrderController::class, 'revertOrderConfirmation'])->whereNumber('id')->name('v1.orders.revert-confirmation'); - - // 생산지시 되돌리기 - Route::post('/{id}/revert-production', [OrderController::class, 'revertProductionOrder'])->whereNumber('id')->name('v1.orders.revert-production'); - }); - - // 작업지시 관리 API (Production) - Route::prefix('work-orders')->group(function () { - // 기본 CRUD - Route::get('', [WorkOrderController::class, 'index'])->name('v1.work-orders.index'); // 목록 - Route::get('/stats', [WorkOrderController::class, 'stats'])->name('v1.work-orders.stats'); // 통계 - Route::post('', [WorkOrderController::class, 'store'])->name('v1.work-orders.store'); // 생성 - Route::get('/{id}', [WorkOrderController::class, 'show'])->whereNumber('id')->name('v1.work-orders.show'); // 상세 - Route::put('/{id}', [WorkOrderController::class, 'update'])->whereNumber('id')->name('v1.work-orders.update'); // 수정 - Route::delete('/{id}', [WorkOrderController::class, 'destroy'])->whereNumber('id')->name('v1.work-orders.destroy'); // 삭제 - - // 상태 및 담당자 관리 - Route::patch('/{id}/status', [WorkOrderController::class, 'updateStatus'])->whereNumber('id')->name('v1.work-orders.status'); // 상태 변경 - Route::patch('/{id}/assign', [WorkOrderController::class, 'assign'])->whereNumber('id')->name('v1.work-orders.assign'); // 담당자 배정 - - // 벤딩 공정 상세 토글 - Route::patch('/{id}/bending/toggle', [WorkOrderController::class, 'toggleBendingField'])->whereNumber('id')->name('v1.work-orders.bending-toggle'); - - // 이슈 관리 - Route::post('/{id}/issues', [WorkOrderController::class, 'addIssue'])->whereNumber('id')->name('v1.work-orders.issues.store'); // 이슈 등록 - Route::patch('/{id}/issues/{issueId}/resolve', [WorkOrderController::class, 'resolveIssue'])->whereNumber('id')->name('v1.work-orders.issues.resolve'); // 이슈 해결 - - // 품목 상태 변경 - Route::patch('/{id}/items/{itemId}/status', [WorkOrderController::class, 'updateItemStatus'])->whereNumber('id')->whereNumber('itemId')->name('v1.work-orders.items.status'); - - // 자재 관리 - Route::get('/{id}/materials', [WorkOrderController::class, 'materials'])->whereNumber('id')->name('v1.work-orders.materials'); // 자재 목록 조회 - Route::post('/{id}/material-inputs', [WorkOrderController::class, 'registerMaterialInput'])->whereNumber('id')->name('v1.work-orders.material-inputs'); // 자재 투입 등록 - }); - - // 작업실적 관리 API (Production) - Route::prefix('work-results')->group(function () { - // 기본 CRUD - Route::get('', [WorkResultController::class, 'index'])->name('v1.work-results.index'); // 목록 - Route::get('/stats', [WorkResultController::class, 'stats'])->name('v1.work-results.stats'); // 통계 - Route::post('', [WorkResultController::class, 'store'])->name('v1.work-results.store'); // 생성 - Route::get('/{id}', [WorkResultController::class, 'show'])->whereNumber('id')->name('v1.work-results.show'); // 상세 - Route::put('/{id}', [WorkResultController::class, 'update'])->whereNumber('id')->name('v1.work-results.update'); // 수정 - Route::delete('/{id}', [WorkResultController::class, 'destroy'])->whereNumber('id')->name('v1.work-results.destroy'); // 삭제 - - // 상태 토글 - Route::patch('/{id}/inspection', [WorkResultController::class, 'toggleInspection'])->whereNumber('id')->name('v1.work-results.inspection'); // 검사 상태 토글 - Route::patch('/{id}/packaging', [WorkResultController::class, 'togglePackaging'])->whereNumber('id')->name('v1.work-results.packaging'); // 포장 상태 토글 - }); - - // 검사 관리 API (Quality) - Route::prefix('inspections')->group(function () { - Route::get('', [InspectionController::class, 'index'])->name('v1.inspections.index'); // 목록 - Route::get('/stats', [InspectionController::class, 'stats'])->name('v1.inspections.stats'); // 통계 - Route::post('', [InspectionController::class, 'store'])->name('v1.inspections.store'); // 생성 - Route::get('/{id}', [InspectionController::class, 'show'])->whereNumber('id')->name('v1.inspections.show'); // 상세 - Route::put('/{id}', [InspectionController::class, 'update'])->whereNumber('id')->name('v1.inspections.update'); // 수정 - Route::delete('/{id}', [InspectionController::class, 'destroy'])->whereNumber('id')->name('v1.inspections.destroy'); // 삭제 - Route::patch('/{id}/complete', [InspectionController::class, 'complete'])->whereNumber('id')->name('v1.inspections.complete'); // 완료 처리 - }); - - // 파일 저장소 API - Route::prefix('files')->group(function () { - Route::post('/upload', [FileStorageController::class, 'upload'])->name('v1.files.upload'); // 파일 업로드 (임시) - Route::post('/move', [FileStorageController::class, 'move'])->name('v1.files.move'); // 파일 이동 (temp → folder) - Route::get('/', [FileStorageController::class, 'index'])->name('v1.files.index'); // 파일 목록 - Route::get('/trash', [FileStorageController::class, 'trash'])->name('v1.files.trash'); // 휴지통 목록 - Route::get('/{id}', [FileStorageController::class, 'show'])->name('v1.files.show'); // 파일 상세 - Route::get('/{id}/download', [FileStorageController::class, 'download'])->name('v1.files.download'); // 파일 다운로드 - Route::delete('/{id}', [FileStorageController::class, 'destroy'])->name('v1.files.destroy'); // 파일 삭제 (soft) - Route::post('/{id}/restore', [FileStorageController::class, 'restore'])->name('v1.files.restore'); // 파일 복구 - Route::delete('/{id}/permanent', [FileStorageController::class, 'permanentDelete'])->name('v1.files.permanent'); // 파일 영구 삭제 - Route::post('/{id}/share', [FileStorageController::class, 'createShareLink'])->name('v1.files.share'); // 공유 링크 생성 - }); - - // 저장소 사용량 - Route::get('/storage/usage', [FileStorageController::class, 'storageUsage'])->name('v1.storage.usage'); - - // 폴더 관리 API - Route::prefix('folders')->group(function () { - Route::get('/', [FolderController::class, 'index'])->name('v1.folders.index'); // 폴더 목록 - Route::post('/', [FolderController::class, 'store'])->name('v1.folders.store'); // 폴더 생성 - Route::get('/{id}', [FolderController::class, 'show'])->name('v1.folders.show'); // 폴더 상세 - Route::put('/{id}', [FolderController::class, 'update'])->name('v1.folders.update'); // 폴더 수정 - Route::delete('/{id}', [FolderController::class, 'destroy'])->name('v1.folders.destroy'); // 폴더 삭제/비활성화 - Route::post('/reorder', [FolderController::class, 'reorder'])->name('v1.folders.reorder'); // 폴더 순서 변경 - }); - - // 품목기준관리 (ItemMaster) API - Route::prefix('item-master')->group(function () { - // 초기화 - Route::get('/init', [ItemMasterController::class, 'init'])->name('v1.item-master.init'); - - // 페이지 관리 - Route::get('/pages', [ItemPageController::class, 'index'])->name('v1.item-master.pages.index'); - Route::post('/pages', [ItemPageController::class, 'store'])->name('v1.item-master.pages.store'); - Route::put('/pages/{id}', [ItemPageController::class, 'update'])->name('v1.item-master.pages.update'); - Route::delete('/pages/{id}', [ItemPageController::class, 'destroy'])->name('v1.item-master.pages.destroy'); - - // 독립 섹션 관리 - Route::get('/sections', [ItemSectionController::class, 'index'])->name('v1.item-master.sections.index'); - Route::post('/sections', [ItemSectionController::class, 'storeIndependent'])->name('v1.item-master.sections.store-independent'); - Route::post('/sections/{id}/clone', [ItemSectionController::class, 'clone'])->name('v1.item-master.sections.clone'); - Route::get('/sections/{id}/usage', [ItemSectionController::class, 'getUsage'])->name('v1.item-master.sections.usage'); - - // 섹션 관리 (페이지 연결) - Route::post('/pages/{pageId}/sections', [ItemSectionController::class, 'store'])->name('v1.item-master.sections.store'); - Route::put('/sections/{id}', [ItemSectionController::class, 'update'])->name('v1.item-master.sections.update'); - Route::delete('/sections/{id}', [ItemSectionController::class, 'destroy'])->name('v1.item-master.sections.destroy'); - Route::put('/pages/{pageId}/sections/reorder', [ItemSectionController::class, 'reorder'])->name('v1.item-master.sections.reorder'); - - // 독립 필드 관리 - Route::get('/fields', [ItemFieldController::class, 'index'])->name('v1.item-master.fields.index'); - Route::post('/fields', [ItemFieldController::class, 'storeIndependent'])->name('v1.item-master.fields.store-independent'); - Route::post('/fields/{id}/clone', [ItemFieldController::class, 'clone'])->name('v1.item-master.fields.clone'); - Route::get('/fields/{id}/usage', [ItemFieldController::class, 'getUsage'])->name('v1.item-master.fields.usage'); - - // 필드 관리 (섹션 연결) - Route::post('/sections/{sectionId}/fields', [ItemFieldController::class, 'store'])->name('v1.item-master.fields.store'); - Route::put('/fields/{id}', [ItemFieldController::class, 'update'])->name('v1.item-master.fields.update'); - Route::delete('/fields/{id}', [ItemFieldController::class, 'destroy'])->name('v1.item-master.fields.destroy'); - Route::put('/sections/{sectionId}/fields/reorder', [ItemFieldController::class, 'reorder'])->name('v1.item-master.fields.reorder'); - - // 독립 BOM 항목 관리 - Route::get('/bom-items', [ItemBomItemController::class, 'index'])->name('v1.item-master.bom-items.index'); - Route::post('/bom-items', [ItemBomItemController::class, 'storeIndependent'])->name('v1.item-master.bom-items.store-independent'); - - // BOM 항목 관리 (섹션 연결) - Route::post('/sections/{sectionId}/bom-items', [ItemBomItemController::class, 'store'])->name('v1.item-master.bom-items.store'); - Route::put('/bom-items/{id}', [ItemBomItemController::class, 'update'])->name('v1.item-master.bom-items.update'); - Route::delete('/bom-items/{id}', [ItemBomItemController::class, 'destroy'])->name('v1.item-master.bom-items.destroy'); - - // 섹션 템플릿 - Route::get('/section-templates', [SectionTemplateController::class, 'index'])->name('v1.item-master.section-templates.index'); - Route::post('/section-templates', [SectionTemplateController::class, 'store'])->name('v1.item-master.section-templates.store'); - Route::put('/section-templates/{id}', [SectionTemplateController::class, 'update'])->name('v1.item-master.section-templates.update'); - Route::delete('/section-templates/{id}', [SectionTemplateController::class, 'destroy'])->name('v1.item-master.section-templates.destroy'); - - // 커스텀 탭 - Route::get('/custom-tabs', [CustomTabController::class, 'index'])->name('v1.item-master.custom-tabs.index'); - Route::post('/custom-tabs', [CustomTabController::class, 'store'])->name('v1.item-master.custom-tabs.store'); - Route::put('/custom-tabs/reorder', [CustomTabController::class, 'reorder'])->name('v1.item-master.custom-tabs.reorder'); - Route::put('/custom-tabs/{id}', [CustomTabController::class, 'update'])->name('v1.item-master.custom-tabs.update'); - Route::delete('/custom-tabs/{id}', [CustomTabController::class, 'destroy'])->name('v1.item-master.custom-tabs.destroy'); - - // 단위 옵션 - Route::get('/unit-options', [UnitOptionController::class, 'index'])->name('v1.item-master.unit-options.index'); - Route::post('/unit-options', [UnitOptionController::class, 'store'])->name('v1.item-master.unit-options.store'); - Route::delete('/unit-options/{id}', [UnitOptionController::class, 'destroy'])->name('v1.item-master.unit-options.destroy'); - - // 엔티티 관계 관리 (독립 엔티티 + 링크 테이블) - // 페이지-섹션 연결 - Route::post('/pages/{pageId}/link-section', [EntityRelationshipController::class, 'linkSectionToPage'])->name('v1.item-master.pages.link-section'); - Route::delete('/pages/{pageId}/unlink-section/{sectionId}', [EntityRelationshipController::class, 'unlinkSectionFromPage'])->name('v1.item-master.pages.unlink-section'); - - // 페이지-필드 직접 연결 - Route::post('/pages/{pageId}/link-field', [EntityRelationshipController::class, 'linkFieldToPage'])->name('v1.item-master.pages.link-field'); - Route::delete('/pages/{pageId}/unlink-field/{fieldId}', [EntityRelationshipController::class, 'unlinkFieldFromPage'])->name('v1.item-master.pages.unlink-field'); - - // 페이지 관계 조회 - Route::get('/pages/{pageId}/relationships', [EntityRelationshipController::class, 'getPageRelationships'])->name('v1.item-master.pages.relationships'); - Route::get('/pages/{pageId}/structure', [EntityRelationshipController::class, 'getPageStructure'])->name('v1.item-master.pages.structure'); - - // 섹션-필드 연결 - Route::post('/sections/{sectionId}/link-field', [EntityRelationshipController::class, 'linkFieldToSection'])->name('v1.item-master.sections.link-field'); - Route::delete('/sections/{sectionId}/unlink-field/{fieldId}', [EntityRelationshipController::class, 'unlinkFieldFromSection'])->name('v1.item-master.sections.unlink-field'); - - // 섹션-BOM 연결 - Route::post('/sections/{sectionId}/link-bom', [EntityRelationshipController::class, 'linkBomToSection'])->name('v1.item-master.sections.link-bom'); - Route::delete('/sections/{sectionId}/unlink-bom/{bomId}', [EntityRelationshipController::class, 'unlinkBomFromSection'])->name('v1.item-master.sections.unlink-bom'); - - // 섹션 관계 조회 - Route::get('/sections/{sectionId}/relationships', [EntityRelationshipController::class, 'getSectionRelationships'])->name('v1.item-master.sections.relationships'); - - // 관계 순서 변경 - Route::post('/relationships/reorder', [EntityRelationshipController::class, 'reorderRelationships'])->name('v1.item-master.relationships.reorder'); - }); - - // 시스템 게시판 API (is_system=true, tenant_id=null) - Route::prefix('system-boards')->group(function () { - // 시스템 게시판 목록/상세 - Route::get('/', [SystemBoardController::class, 'index'])->name('v1.system-boards.index'); - Route::get('/{code}', [SystemBoardController::class, 'show'])->name('v1.system-boards.show'); - Route::get('/{code}/fields', [SystemBoardController::class, 'fields'])->name('v1.system-boards.fields'); - - // 시스템 게시글 API - Route::get('/{code}/posts', [SystemPostController::class, 'index'])->name('v1.system-boards.posts.index'); - Route::post('/{code}/posts', [SystemPostController::class, 'store'])->name('v1.system-boards.posts.store'); - Route::get('/{code}/posts/{id}', [SystemPostController::class, 'show'])->name('v1.system-boards.posts.show'); - Route::put('/{code}/posts/{id}', [SystemPostController::class, 'update'])->name('v1.system-boards.posts.update'); - Route::delete('/{code}/posts/{id}', [SystemPostController::class, 'destroy'])->name('v1.system-boards.posts.destroy'); - - // 시스템 댓글 API - Route::get('/{code}/posts/{postId}/comments', [SystemPostController::class, 'comments'])->name('v1.system-boards.posts.comments.index'); - Route::post('/{code}/posts/{postId}/comments', [SystemPostController::class, 'storeComment'])->name('v1.system-boards.posts.comments.store'); - Route::put('/{code}/posts/{postId}/comments/{commentId}', [SystemPostController::class, 'updateComment'])->name('v1.system-boards.posts.comments.update'); - Route::delete('/{code}/posts/{postId}/comments/{commentId}', [SystemPostController::class, 'destroyComment'])->name('v1.system-boards.posts.comments.destroy'); - }); - - // 게시판 관리 API (테넌트용) - Route::prefix('boards')->group(function () { - // 게시판 목록/상세 - Route::get('/', [BoardController::class, 'index'])->name('v1.boards.index'); // 접근 가능한 게시판 목록 - Route::get('/tenant', [BoardController::class, 'tenantBoards'])->name('v1.boards.tenant'); // 테넌트 게시판만 - Route::post('/', [BoardController::class, 'store'])->name('v1.boards.store'); // 테넌트 게시판 생성 - Route::get('/{id}', [BoardController::class, 'show'])->whereNumber('id')->name('v1.boards.show'); // 게시판 상세 (ID 기반) - Route::put('/{id}', [BoardController::class, 'update'])->whereNumber('id')->name('v1.boards.update'); // 테넌트 게시판 수정 - Route::delete('/{id}', [BoardController::class, 'destroy'])->whereNumber('id')->name('v1.boards.destroy'); // 테넌트 게시판 삭제 - Route::get('/{code}/fields', [BoardController::class, 'fields'])->name('v1.boards.fields'); // 게시판 필드 목록 - - // 게시글 API - Route::get('/{code}/posts', [PostController::class, 'index'])->name('v1.boards.posts.index'); // 게시글 목록 - Route::post('/{code}/posts', [PostController::class, 'store'])->name('v1.boards.posts.store'); // 게시글 작성 - Route::get('/{code}/posts/{id}', [PostController::class, 'show'])->name('v1.boards.posts.show'); // 게시글 상세 - Route::put('/{code}/posts/{id}', [PostController::class, 'update'])->name('v1.boards.posts.update'); // 게시글 수정 - Route::delete('/{code}/posts/{id}', [PostController::class, 'destroy'])->name('v1.boards.posts.destroy'); // 게시글 삭제 - - // 댓글 API - Route::get('/{code}/posts/{postId}/comments', [PostController::class, 'comments'])->name('v1.boards.posts.comments.index'); // 댓글 목록 - Route::post('/{code}/posts/{postId}/comments', [PostController::class, 'storeComment'])->name('v1.boards.posts.comments.store'); // 댓글 작성 - Route::put('/{code}/posts/{postId}/comments/{commentId}', [PostController::class, 'updateComment'])->name('v1.boards.posts.comments.update'); // 댓글 수정 - Route::delete('/{code}/posts/{postId}/comments/{commentId}', [PostController::class, 'destroyComment'])->name('v1.boards.posts.comments.destroy'); // 댓글 삭제 - - // 게시판 상세 (코드 기반) - 가장 마지막에 배치 (catch-all) - Route::get('/{code}', [BoardController::class, 'showByCode'])->name('v1.boards.show_by_code'); - }); - - // 게시글 API (사용자 중심) - Route::prefix('posts')->group(function () { - Route::get('/my', [PostController::class, 'myPosts'])->name('v1.posts.my'); // 나의 게시글 목록 - }); - - }); + // 도메인별 라우트 파일 로드 + require __DIR__.'/api/v1/auth.php'; + require __DIR__.'/api/v1/admin.php'; + require __DIR__.'/api/v1/users.php'; + require __DIR__.'/api/v1/tenants.php'; + require __DIR__.'/api/v1/hr.php'; + require __DIR__.'/api/v1/finance.php'; + require __DIR__.'/api/v1/sales.php'; + require __DIR__.'/api/v1/inventory.php'; + require __DIR__.'/api/v1/production.php'; + require __DIR__.'/api/v1/design.php'; + require __DIR__.'/api/v1/files.php'; + require __DIR__.'/api/v1/boards.php'; + require __DIR__.'/api/v1/common.php'; // 공유 링크 다운로드 (인증 불필요 - auth.apikey 그룹 밖) Route::get('/files/share/{token}', [FileStorageController::class, 'downloadShared'])->name('v1.files.share.download'); }); + +/* +|-------------------------------------------------------------------------- +| V2 API Routes (확장 버전 - 필요시 추가) +|-------------------------------------------------------------------------- +| +| V2 라우트를 추가할 때: +| 1. routes/api/v2/ 디렉토리에 도메인별 파일 생성 +| 2. 아래 주석을 해제하고 필요한 파일만 require +| 3. V2에 없는 라우트는 자동으로 V1으로 폴백 +| +*/ +// Route::prefix('v2')->group(function () { +// // V2 전용 라우트만 정의 (V1에 없거나 변경된 API) +// require __DIR__ . '/api/v2/auth.php'; // 예: 새로운 인증 방식 +// require __DIR__ . '/api/v2/users.php'; // 예: 확장된 사용자 API +// +// // V1과 동일한 라우트는 정의하지 않음 - 미들웨어가 자동 폴백 처리 +// }); diff --git a/routes/api/v1/admin.php b/routes/api/v1/admin.php new file mode 100644 index 0000000..3ca3fd5 --- /dev/null +++ b/routes/api/v1/admin.php @@ -0,0 +1,63 @@ +group(function () { + // 목록/생성 + Route::get('users', [AdminController::class, 'index'])->name('v1.admin.users.index'); // 테넌트 사용자 목록 조회 + Route::post('users', [AdminController::class, 'store'])->name('v1.admin.users.store'); // 테넌트 사용자 생성 + + // 단건 + Route::get('users/{id}', [AdminController::class, 'show'])->name('v1.admin.users.show'); // 테넌트 사용자 단건 조회 + Route::put('users/{id}', [AdminController::class, 'update'])->name('v1.admin.users.update'); // 테넌트 사용자 수정 + + // 소프트 삭제 복구 + Route::delete('users/{id}', [AdminController::class, 'destroy'])->name('v1.admin.users.destroy'); // 테넌트 사용자 삭제(연결 삭제) + Route::post('users/{id}/restore', [AdminController::class, 'restore'])->name('v1.admin.users.restore'); // 테넌트 사용자 삭제 복구 + + // 상태 토글 + Route::patch('users/{id}/status', [AdminController::class, 'toggle'])->name('v1.admin.users.status.toggle'); // 테넌트 사용자 활성/비활성 + + // 역할 부여/해제 + Route::post('users/{id}/roles', [AdminController::class, 'attach'])->name('v1.admin.users.roles.attach'); // 테넌트 사용자 역할 부여 + Route::delete('users/{id}/roles/{role}', [AdminController::class, 'detach'])->name('v1.admin.users.roles.detach'); // 테넌트 사용자 역할 해제 + + // 비밀번호 초기화 + Route::post('users/{id}/reset-password', [AdminController::class, 'reset'])->name('v1.admin.users.password.reset'); // 테넌트 사용자 비밀번호 초기화 + + // 글로벌 메뉴 관리 (시스템 관리자용) + Route::prefix('global-menus')->group(function () { + Route::get('/', [GlobalMenuController::class, 'index'])->name('v1.admin.global-menus.index'); // 글로벌 메뉴 목록 + Route::post('/', [GlobalMenuController::class, 'store'])->name('v1.admin.global-menus.store'); // 글로벌 메뉴 생성 + Route::get('/tree', [GlobalMenuController::class, 'tree'])->name('v1.admin.global-menus.tree'); // 글로벌 메뉴 트리 + Route::get('/stats', [GlobalMenuController::class, 'stats'])->name('v1.admin.global-menus.stats'); // 글로벌 메뉴 통계 + Route::post('/reorder', [GlobalMenuController::class, 'reorder'])->name('v1.admin.global-menus.reorder'); // 글로벌 메뉴 순서 변경 + Route::get('/{id}', [GlobalMenuController::class, 'show'])->name('v1.admin.global-menus.show'); // 글로벌 메뉴 단건 조회 + Route::put('/{id}', [GlobalMenuController::class, 'update'])->name('v1.admin.global-menus.update'); // 글로벌 메뉴 수정 + Route::delete('/{id}', [GlobalMenuController::class, 'destroy'])->name('v1.admin.global-menus.destroy'); // 글로벌 메뉴 삭제 + Route::post('/{id}/sync-to-tenants', [GlobalMenuController::class, 'syncToTenants'])->name('v1.admin.global-menus.sync-to-tenants'); // 테넌트에 동기화 + }); + + // Admin FCM API (MNG 관리자용 FCM 발송) - API Key 인증만 사용 + Route::prefix('fcm')->group(function () { + Route::post('/send', [FcmController::class, 'send'])->name('v1.admin.fcm.send'); // FCM 발송 + Route::get('/preview-count', [FcmController::class, 'previewCount'])->name('v1.admin.fcm.preview'); // 대상 토큰 수 미리보기 + Route::get('/tokens', [FcmController::class, 'tokens'])->name('v1.admin.fcm.tokens'); // 토큰 목록 + Route::get('/tokens/stats', [FcmController::class, 'tokenStats'])->name('v1.admin.fcm.tokens.stats'); // 토큰 통계 + Route::patch('/tokens/{id}/toggle', [FcmController::class, 'toggleToken'])->name('v1.admin.fcm.tokens.toggle'); // 토큰 상태 토글 + Route::delete('/tokens/{id}', [FcmController::class, 'deleteToken'])->name('v1.admin.fcm.tokens.delete'); // 토큰 삭제 + Route::get('/history', [FcmController::class, 'history'])->name('v1.admin.fcm.history'); // 발송 이력 + }); +}); diff --git a/routes/api/v1/auth.php b/routes/api/v1/auth.php new file mode 100644 index 0000000..5eb252e --- /dev/null +++ b/routes/api/v1/auth.php @@ -0,0 +1,32 @@ +group(function () { + Route::post('/exchange-token', [InternalController::class, 'exchangeToken'])->name('v1.internal.exchange-token'); +}); + +// API KEY 인증 (글로벌 미들웨어로 이미 적용됨) +Route::get('/debug-apikey', [ApiController::class, 'debugApikey']); + +// Auth API +Route::post('login', [ApiController::class, 'login'])->name('v1.users.login'); +Route::middleware('auth:sanctum')->post('logout', [ApiController::class, 'logout'])->name('v1.users.logout'); +Route::post('signup', [ApiController::class, 'signup'])->name('v1.users.signup'); +Route::post('token-login', [ApiController::class, 'tokenLogin'])->name('v1.auth.token-login'); // MNG → DEV 자동 로그인 +Route::post('refresh', [RefreshController::class, 'refresh'])->name('v1.token.refresh'); +Route::post('register', [RegisterController::class, 'register'])->name('v1.register'); diff --git a/routes/api/v1/boards.php b/routes/api/v1/boards.php new file mode 100644 index 0000000..e97a3c2 --- /dev/null +++ b/routes/api/v1/boards.php @@ -0,0 +1,170 @@ +group(function () { + // 시스템 게시판 목록/상세 + Route::get('/', [SystemBoardController::class, 'index'])->name('v1.system-boards.index'); + Route::get('/{code}', [SystemBoardController::class, 'show'])->name('v1.system-boards.show'); + Route::get('/{code}/fields', [SystemBoardController::class, 'fields'])->name('v1.system-boards.fields'); + + // 시스템 게시글 API + Route::get('/{code}/posts', [SystemPostController::class, 'index'])->name('v1.system-boards.posts.index'); + Route::post('/{code}/posts', [SystemPostController::class, 'store'])->name('v1.system-boards.posts.store'); + Route::get('/{code}/posts/{id}', [SystemPostController::class, 'show'])->name('v1.system-boards.posts.show'); + Route::put('/{code}/posts/{id}', [SystemPostController::class, 'update'])->name('v1.system-boards.posts.update'); + Route::delete('/{code}/posts/{id}', [SystemPostController::class, 'destroy'])->name('v1.system-boards.posts.destroy'); + + // 시스템 댓글 API + Route::get('/{code}/posts/{postId}/comments', [SystemPostController::class, 'comments'])->name('v1.system-boards.posts.comments.index'); + Route::post('/{code}/posts/{postId}/comments', [SystemPostController::class, 'storeComment'])->name('v1.system-boards.posts.comments.store'); + Route::put('/{code}/posts/{postId}/comments/{commentId}', [SystemPostController::class, 'updateComment'])->name('v1.system-boards.posts.comments.update'); + Route::delete('/{code}/posts/{postId}/comments/{commentId}', [SystemPostController::class, 'destroyComment'])->name('v1.system-boards.posts.comments.destroy'); +}); + +// 게시판 관리 API (테넌트용) +Route::prefix('boards')->group(function () { + // 게시판 목록/상세 + Route::get('/', [BoardController::class, 'index'])->name('v1.boards.index'); // 접근 가능한 게시판 목록 + Route::get('/tenant', [BoardController::class, 'tenantBoards'])->name('v1.boards.tenant'); // 테넌트 게시판만 + Route::post('/', [BoardController::class, 'store'])->name('v1.boards.store'); // 테넌트 게시판 생성 + Route::get('/{id}', [BoardController::class, 'show'])->whereNumber('id')->name('v1.boards.show'); // 게시판 상세 (ID 기반) + Route::put('/{id}', [BoardController::class, 'update'])->whereNumber('id')->name('v1.boards.update'); // 테넌트 게시판 수정 + Route::delete('/{id}', [BoardController::class, 'destroy'])->whereNumber('id')->name('v1.boards.destroy'); // 테넌트 게시판 삭제 + Route::get('/{code}/fields', [BoardController::class, 'fields'])->name('v1.boards.fields'); // 게시판 필드 목록 + + // 게시글 API + Route::get('/{code}/posts', [PostController::class, 'index'])->name('v1.boards.posts.index'); // 게시글 목록 + Route::post('/{code}/posts', [PostController::class, 'store'])->name('v1.boards.posts.store'); // 게시글 작성 + Route::get('/{code}/posts/{id}', [PostController::class, 'show'])->name('v1.boards.posts.show'); // 게시글 상세 + Route::put('/{code}/posts/{id}', [PostController::class, 'update'])->name('v1.boards.posts.update'); // 게시글 수정 + Route::delete('/{code}/posts/{id}', [PostController::class, 'destroy'])->name('v1.boards.posts.destroy'); // 게시글 삭제 + + // 댓글 API + Route::get('/{code}/posts/{postId}/comments', [PostController::class, 'comments'])->name('v1.boards.posts.comments.index'); // 댓글 목록 + Route::post('/{code}/posts/{postId}/comments', [PostController::class, 'storeComment'])->name('v1.boards.posts.comments.store'); // 댓글 작성 + Route::put('/{code}/posts/{postId}/comments/{commentId}', [PostController::class, 'updateComment'])->name('v1.boards.posts.comments.update'); // 댓글 수정 + Route::delete('/{code}/posts/{postId}/comments/{commentId}', [PostController::class, 'destroyComment'])->name('v1.boards.posts.comments.destroy'); // 댓글 삭제 + + // 게시판 상세 (코드 기반) - 가장 마지막에 배치 (catch-all) + Route::get('/{code}', [BoardController::class, 'showByCode'])->name('v1.boards.show_by_code'); +}); + +// 게시글 API (사용자 중심) +Route::prefix('posts')->group(function () { + Route::get('/my', [PostController::class, 'myPosts'])->name('v1.posts.my'); // 나의 게시글 목록 +}); + +// 품목기준관리 (ItemMaster) API +Route::prefix('item-master')->group(function () { + // 초기화 + Route::get('/init', [ItemMasterController::class, 'init'])->name('v1.item-master.init'); + + // 페이지 관리 + Route::get('/pages', [ItemPageController::class, 'index'])->name('v1.item-master.pages.index'); + Route::post('/pages', [ItemPageController::class, 'store'])->name('v1.item-master.pages.store'); + Route::put('/pages/{id}', [ItemPageController::class, 'update'])->name('v1.item-master.pages.update'); + Route::delete('/pages/{id}', [ItemPageController::class, 'destroy'])->name('v1.item-master.pages.destroy'); + + // 독립 섹션 관리 + Route::get('/sections', [ItemSectionController::class, 'index'])->name('v1.item-master.sections.index'); + Route::post('/sections', [ItemSectionController::class, 'storeIndependent'])->name('v1.item-master.sections.store-independent'); + Route::post('/sections/{id}/clone', [ItemSectionController::class, 'clone'])->name('v1.item-master.sections.clone'); + Route::get('/sections/{id}/usage', [ItemSectionController::class, 'getUsage'])->name('v1.item-master.sections.usage'); + + // 섹션 관리 (페이지 연결) + Route::post('/pages/{pageId}/sections', [ItemSectionController::class, 'store'])->name('v1.item-master.sections.store'); + Route::put('/sections/{id}', [ItemSectionController::class, 'update'])->name('v1.item-master.sections.update'); + Route::delete('/sections/{id}', [ItemSectionController::class, 'destroy'])->name('v1.item-master.sections.destroy'); + Route::put('/pages/{pageId}/sections/reorder', [ItemSectionController::class, 'reorder'])->name('v1.item-master.sections.reorder'); + + // 독립 필드 관리 + Route::get('/fields', [ItemFieldController::class, 'index'])->name('v1.item-master.fields.index'); + Route::post('/fields', [ItemFieldController::class, 'storeIndependent'])->name('v1.item-master.fields.store-independent'); + Route::post('/fields/{id}/clone', [ItemFieldController::class, 'clone'])->name('v1.item-master.fields.clone'); + Route::get('/fields/{id}/usage', [ItemFieldController::class, 'getUsage'])->name('v1.item-master.fields.usage'); + + // 필드 관리 (섹션 연결) + Route::post('/sections/{sectionId}/fields', [ItemFieldController::class, 'store'])->name('v1.item-master.fields.store'); + Route::put('/fields/{id}', [ItemFieldController::class, 'update'])->name('v1.item-master.fields.update'); + Route::delete('/fields/{id}', [ItemFieldController::class, 'destroy'])->name('v1.item-master.fields.destroy'); + Route::put('/sections/{sectionId}/fields/reorder', [ItemFieldController::class, 'reorder'])->name('v1.item-master.fields.reorder'); + + // 독립 BOM 항목 관리 + Route::get('/bom-items', [ItemBomItemController::class, 'index'])->name('v1.item-master.bom-items.index'); + Route::post('/bom-items', [ItemBomItemController::class, 'storeIndependent'])->name('v1.item-master.bom-items.store-independent'); + + // BOM 항목 관리 (섹션 연결) + Route::post('/sections/{sectionId}/bom-items', [ItemBomItemController::class, 'store'])->name('v1.item-master.bom-items.store'); + Route::put('/bom-items/{id}', [ItemBomItemController::class, 'update'])->name('v1.item-master.bom-items.update'); + Route::delete('/bom-items/{id}', [ItemBomItemController::class, 'destroy'])->name('v1.item-master.bom-items.destroy'); + + // 섹션 템플릿 + Route::get('/section-templates', [SectionTemplateController::class, 'index'])->name('v1.item-master.section-templates.index'); + Route::post('/section-templates', [SectionTemplateController::class, 'store'])->name('v1.item-master.section-templates.store'); + Route::put('/section-templates/{id}', [SectionTemplateController::class, 'update'])->name('v1.item-master.section-templates.update'); + Route::delete('/section-templates/{id}', [SectionTemplateController::class, 'destroy'])->name('v1.item-master.section-templates.destroy'); + + // 커스텀 탭 + Route::get('/custom-tabs', [CustomTabController::class, 'index'])->name('v1.item-master.custom-tabs.index'); + Route::post('/custom-tabs', [CustomTabController::class, 'store'])->name('v1.item-master.custom-tabs.store'); + Route::put('/custom-tabs/reorder', [CustomTabController::class, 'reorder'])->name('v1.item-master.custom-tabs.reorder'); + Route::put('/custom-tabs/{id}', [CustomTabController::class, 'update'])->name('v1.item-master.custom-tabs.update'); + Route::delete('/custom-tabs/{id}', [CustomTabController::class, 'destroy'])->name('v1.item-master.custom-tabs.destroy'); + + // 단위 옵션 + Route::get('/unit-options', [UnitOptionController::class, 'index'])->name('v1.item-master.unit-options.index'); + Route::post('/unit-options', [UnitOptionController::class, 'store'])->name('v1.item-master.unit-options.store'); + Route::delete('/unit-options/{id}', [UnitOptionController::class, 'destroy'])->name('v1.item-master.unit-options.destroy'); + + // 엔티티 관계 관리 (독립 엔티티 + 링크 테이블) + // 페이지-섹션 연결 + Route::post('/pages/{pageId}/link-section', [EntityRelationshipController::class, 'linkSectionToPage'])->name('v1.item-master.pages.link-section'); + Route::delete('/pages/{pageId}/unlink-section/{sectionId}', [EntityRelationshipController::class, 'unlinkSectionFromPage'])->name('v1.item-master.pages.unlink-section'); + + // 페이지-필드 직접 연결 + Route::post('/pages/{pageId}/link-field', [EntityRelationshipController::class, 'linkFieldToPage'])->name('v1.item-master.pages.link-field'); + Route::delete('/pages/{pageId}/unlink-field/{fieldId}', [EntityRelationshipController::class, 'unlinkFieldFromPage'])->name('v1.item-master.pages.unlink-field'); + + // 페이지 관계 조회 + Route::get('/pages/{pageId}/relationships', [EntityRelationshipController::class, 'getPageRelationships'])->name('v1.item-master.pages.relationships'); + Route::get('/pages/{pageId}/structure', [EntityRelationshipController::class, 'getPageStructure'])->name('v1.item-master.pages.structure'); + + // 섹션-필드 연결 + Route::post('/sections/{sectionId}/link-field', [EntityRelationshipController::class, 'linkFieldToSection'])->name('v1.item-master.sections.link-field'); + Route::delete('/sections/{sectionId}/unlink-field/{fieldId}', [EntityRelationshipController::class, 'unlinkFieldFromSection'])->name('v1.item-master.sections.unlink-field'); + + // 섹션-BOM 연결 + Route::post('/sections/{sectionId}/link-bom', [EntityRelationshipController::class, 'linkBomToSection'])->name('v1.item-master.sections.link-bom'); + Route::delete('/sections/{sectionId}/unlink-bom/{bomId}', [EntityRelationshipController::class, 'unlinkBomFromSection'])->name('v1.item-master.sections.unlink-bom'); + + // 섹션 관계 조회 + Route::get('/sections/{sectionId}/relationships', [EntityRelationshipController::class, 'getSectionRelationships'])->name('v1.item-master.sections.relationships'); + + // 관계 순서 변경 + Route::post('/relationships/reorder', [EntityRelationshipController::class, 'reorderRelationships'])->name('v1.item-master.relationships.reorder'); +}); diff --git a/routes/api/v1/common.php b/routes/api/v1/common.php new file mode 100644 index 0000000..ede81c7 --- /dev/null +++ b/routes/api/v1/common.php @@ -0,0 +1,228 @@ +prefix('menus')->group(function () { + Route::get('/', [MenuController::class, 'index'])->name('v1.menus.index'); + Route::post('/', [MenuController::class, 'store'])->name('v1.menus.store'); + Route::post('/reorder', [MenuController::class, 'reorder'])->name('v1.menus.reorder'); + + // 동기화 관련 라우트 (/{id} 전에 위치해야 함) + Route::get('/trashed', [MenuController::class, 'trashed'])->name('v1.menus.trashed'); + Route::get('/available-global', [MenuController::class, 'availableGlobal'])->name('v1.menus.available-global'); + Route::get('/sync-status', [MenuController::class, 'syncStatus'])->name('v1.menus.sync-status'); + Route::post('/sync', [MenuController::class, 'sync'])->name('v1.menus.sync'); + Route::post('/sync-new', [MenuController::class, 'syncNew'])->name('v1.menus.sync-new'); + Route::post('/sync-updates', [MenuController::class, 'syncUpdates'])->name('v1.menus.sync-updates'); + + // 단일 메뉴 관련 라우트 + Route::get('/{id}', [MenuController::class, 'show'])->name('v1.menus.show'); + Route::patch('/{id}', [MenuController::class, 'update'])->name('v1.menus.update'); + Route::delete('/{id}', [MenuController::class, 'destroy'])->name('v1.menus.destroy'); + Route::post('/{id}/toggle', [MenuController::class, 'toggle'])->name('v1.menus.toggle'); + Route::post('/{id}/restore', [MenuController::class, 'restore'])->name('v1.menus.restore'); +}); + +// Role API +Route::prefix('roles')->group(function () { + Route::get('/', [RoleController::class, 'index'])->name('v1.roles.index'); + Route::post('/', [RoleController::class, 'store'])->name('v1.roles.store'); + Route::get('/stats', [RoleController::class, 'stats'])->name('v1.roles.stats'); + Route::get('/active', [RoleController::class, 'active'])->name('v1.roles.active'); + Route::get('/{id}', [RoleController::class, 'show'])->name('v1.roles.show'); + Route::patch('/{id}', [RoleController::class, 'update'])->name('v1.roles.update'); + Route::delete('/{id}', [RoleController::class, 'destroy'])->name('v1.roles.destroy'); +}); + +// Role Permission API - 공통 +Route::get('/role-permissions/menus', [RolePermissionController::class, 'menus'])->name('v1.roles.perms.menus'); + +// Role Permission API - 역할별 +Route::prefix('roles/{id}/permissions')->group(function () { + Route::get('/', [RolePermissionController::class, 'index'])->name('v1.roles.perms.index'); + Route::post('/', [RolePermissionController::class, 'grant'])->name('v1.roles.perms.grant'); + Route::delete('/', [RolePermissionController::class, 'revoke'])->name('v1.roles.perms.revoke'); + Route::put('/sync', [RolePermissionController::class, 'sync'])->name('v1.roles.perms.sync'); + // 권한 매트릭스 API + Route::get('/matrix', [RolePermissionController::class, 'matrix'])->name('v1.roles.perms.matrix'); + Route::post('/toggle', [RolePermissionController::class, 'toggle'])->name('v1.roles.perms.toggle'); + Route::post('/allow-all', [RolePermissionController::class, 'allowAll'])->name('v1.roles.perms.allowAll'); + Route::post('/deny-all', [RolePermissionController::class, 'denyAll'])->name('v1.roles.perms.denyAll'); + Route::post('/reset', [RolePermissionController::class, 'reset'])->name('v1.roles.perms.reset'); +}); + +// Permission API +Route::prefix('permissions')->group(function () { + Route::get('departments/{dept_id}/menu-matrix', [PermissionController::class, 'deptMenuMatrix'])->name('v1.permissions.deptMenuMatrix'); + Route::get('roles/{role_id}/menu-matrix', [PermissionController::class, 'roleMenuMatrix'])->name('v1.permissions.roleMenuMatrix'); + Route::get('users/{user_id}/menu-matrix', [PermissionController::class, 'userMenuMatrix'])->name('v1.permissions.userMenuMatrix'); +}); + +// Settings & Configuration (설정 및 환경설정 통합 관리) +Route::prefix('settings')->group(function () { + // 근무 설정 + Route::get('/work', [WorkSettingController::class, 'showWorkSetting'])->name('v1.settings.work.show'); + Route::put('/work', [WorkSettingController::class, 'updateWorkSetting'])->name('v1.settings.work.update'); + + // 출퇴근 설정 + Route::get('/attendance', [WorkSettingController::class, 'showAttendanceSetting'])->name('v1.settings.attendance.show'); + Route::put('/attendance', [WorkSettingController::class, 'updateAttendanceSetting'])->name('v1.settings.attendance.update'); + + // 급여 설정 + Route::get('/payroll', [PayrollController::class, 'getSettings'])->name('v1.settings.payroll.show'); + Route::put('/payroll', [PayrollController::class, 'updateSettings'])->name('v1.settings.payroll.update'); + + // 테넌트 필드 설정 + Route::get('/fields', [TenantFieldSettingController::class, 'index'])->name('v1.settings.fields.index'); + Route::put('/fields/bulk', [TenantFieldSettingController::class, 'bulkUpsert'])->name('v1.settings.fields.bulk'); + Route::patch('/fields/{key}', [TenantFieldSettingController::class, 'updateOne'])->name('v1.settings.fields.update'); + + // 옵션 그룹/값 + Route::get('/options', [TenantOptionGroupController::class, 'index'])->name('v1.settings.options.index'); + Route::post('/options', [TenantOptionGroupController::class, 'store'])->name('v1.settings.options.store'); + Route::get('/options/{id}', [TenantOptionGroupController::class, 'show'])->name('v1.settings.options.show'); + Route::patch('/options/{id}', [TenantOptionGroupController::class, 'update'])->name('v1.settings.options.update'); + Route::delete('/options/{id}', [TenantOptionGroupController::class, 'destroy'])->name('v1.settings.options.destroy'); + Route::get('/options/{gid}/values', [TenantOptionValueController::class, 'index'])->name('v1.settings.options.values.index'); + Route::post('/options/{gid}/values', [TenantOptionValueController::class, 'store'])->name('v1.settings.options.values.store'); + Route::get('/options/{gid}/values/{id}', [TenantOptionValueController::class, 'show'])->name('v1.settings.options.values.show'); + Route::patch('/options/{gid}/values/{id}', [TenantOptionValueController::class, 'update'])->name('v1.settings.options.values.update'); + Route::delete('/options/{gid}/values/{id}', [TenantOptionValueController::class, 'destroy'])->name('v1.settings.options.values.destroy'); + Route::patch('/options/{gid}/values/reorder', [TenantOptionValueController::class, 'reorder'])->name('v1.settings.options.values.reorder'); + + // 공통 코드 관리 + Route::get('/common/code', [CommonController::class, 'getComeCode'])->name('v1.settings.common.code'); + Route::get('/common', [CommonController::class, 'list'])->name('v1.settings.common.list'); + Route::get('/common/{group}', [CommonController::class, 'index'])->name('v1.settings.common.index'); + Route::post('/common', [CommonController::class, 'store'])->name('v1.settings.common.store'); + Route::patch('/common/{id}', [CommonController::class, 'update'])->name('v1.settings.common.update'); + Route::delete('/common/{id}', [CommonController::class, 'destroy'])->name('v1.settings.common.destroy'); + + // 알림 설정 (그룹 기반) - auth:sanctum 필수 + Route::middleware('auth:sanctum')->group(function () { + Route::get('/notifications', [NotificationSettingController::class, 'indexGrouped'])->name('v1.settings.notifications.index'); + Route::put('/notifications', [NotificationSettingController::class, 'updateGrouped'])->name('v1.settings.notifications.update'); + }); +}); + +// Push Notification API (FCM) - auth:sanctum 필수 +Route::prefix('push')->middleware('auth:sanctum')->group(function () { + Route::post('/register-token', [PushNotificationController::class, 'registerToken'])->name('v1.push.register-token'); + Route::post('/unregister-token', [PushNotificationController::class, 'unregisterToken'])->name('v1.push.unregister-token'); + Route::get('/tokens', [PushNotificationController::class, 'getTokens'])->name('v1.push.tokens'); + Route::get('/settings', [PushNotificationController::class, 'getSettings'])->name('v1.push.settings'); + Route::put('/settings', [PushNotificationController::class, 'updateSettings'])->name('v1.push.settings.update'); + Route::get('/notification-types', [PushNotificationController::class, 'getNotificationTypes'])->name('v1.push.notification-types'); +}); + +// Category API (통합) +Route::prefix('categories')->group(function () { + // 확장 기능 (와일드카드 라우트보다 먼저 정의) + Route::get('/tree', [CategoryController::class, 'tree'])->name('v1.categories.tree'); + Route::post('/reorder', [CategoryController::class, 'reorder'])->name('v1.categories.reorder'); + + // 기본 Category CRUD + Route::get('', [CategoryController::class, 'index'])->name('v1.categories.index'); + Route::post('', [CategoryController::class, 'store'])->name('v1.categories.store'); + Route::get('/{id}', [CategoryController::class, 'show'])->name('v1.categories.show'); + Route::patch('/{id}', [CategoryController::class, 'update'])->name('v1.categories.update'); + Route::delete('/{id}', [CategoryController::class, 'destroy'])->name('v1.categories.destroy'); + Route::post('/{id}/toggle', [CategoryController::class, 'toggle'])->name('v1.categories.toggle'); + Route::patch('/{id}/move', [CategoryController::class, 'move'])->name('v1.categories.move'); + + // Category Fields + Route::get('/{id}/fields', [CategoryFieldController::class, 'index'])->name('v1.categories.fields.index'); + Route::post('/{id}/fields', [CategoryFieldController::class, 'store'])->name('v1.categories.fields.store'); + Route::get('/fields/{field}', [CategoryFieldController::class, 'show'])->name('v1.categories.fields.show'); + Route::patch('/fields/{field}', [CategoryFieldController::class, 'update'])->name('v1.categories.fields.update'); + Route::delete('/fields/{field}', [CategoryFieldController::class, 'destroy'])->name('v1.categories.fields.destroy'); + Route::post('/{id}/fields/reorder', [CategoryFieldController::class, 'reorder'])->name('v1.categories.fields.reorder'); + Route::put('/{id}/fields/bulk-upsert', [CategoryFieldController::class, 'bulkUpsert'])->name('v1.categories.fields.bulk'); + + // Category Templates + Route::get('/{id}/templates', [CategoryTemplateController::class, 'index'])->name('v1.categories.templates.index'); + Route::post('/{id}/templates', [CategoryTemplateController::class, 'store'])->name('v1.categories.templates.store'); + Route::get('/templates/{tpl}', [CategoryTemplateController::class, 'show'])->name('v1.categories.templates.show'); + Route::patch('/templates/{tpl}', [CategoryTemplateController::class, 'update'])->name('v1.categories.templates.update'); + Route::delete('/templates/{tpl}', [CategoryTemplateController::class, 'destroy'])->name('v1.categories.templates.destroy'); + Route::post('/{id}/templates/{tpl}/apply', [CategoryTemplateController::class, 'apply'])->name('v1.categories.templates.apply'); + Route::get('/{id}/templates/{tpl}/preview', [CategoryTemplateController::class, 'preview'])->name('v1.categories.templates.preview'); + Route::get('/{id}/templates/diff', [CategoryTemplateController::class, 'diff'])->name('v1.categories.templates.diff'); + + // Category Logs + Route::get('/{id}/logs', [CategoryLogController::class, 'index'])->name('v1.categories.logs.index'); + Route::get('/logs/{log}', [CategoryLogController::class, 'show'])->name('v1.categories.logs.show'); +}); + +// Classifications API +Route::prefix('classifications')->group(function () { + Route::get('', [ClassificationController::class, 'index'])->name('v1.classifications.index'); + Route::post('', [ClassificationController::class, 'store'])->name('v1.classifications.store'); + Route::get('/{id}', [ClassificationController::class, 'show'])->whereNumber('id')->name('v1.classifications.show'); + Route::patch('/{id}', [ClassificationController::class, 'update'])->whereNumber('id')->name('v1.classifications.update'); + Route::delete('/{id}', [ClassificationController::class, 'destroy'])->whereNumber('id')->name('v1.classifications.destroy'); +}); + +// Popup API (팝업관리) +Route::prefix('popups')->group(function () { + Route::get('', [PopupController::class, 'index'])->name('v1.popups.index'); + Route::post('', [PopupController::class, 'store'])->name('v1.popups.store'); + Route::get('/active', [PopupController::class, 'active'])->name('v1.popups.active'); + Route::get('/{id}', [PopupController::class, 'show'])->whereNumber('id')->name('v1.popups.show'); + Route::put('/{id}', [PopupController::class, 'update'])->whereNumber('id')->name('v1.popups.update'); + Route::delete('/{id}', [PopupController::class, 'destroy'])->whereNumber('id')->name('v1.popups.destroy'); +}); + +// Report API (보고서) +Route::prefix('reports')->group(function () { + Route::get('/daily', [ReportController::class, 'daily'])->name('v1.reports.daily'); + Route::get('/daily/export', [ReportController::class, 'dailyExport'])->name('v1.reports.daily.export'); + Route::get('/expense-estimate', [ReportController::class, 'expenseEstimate'])->name('v1.reports.expense-estimate'); + Route::get('/expense-estimate/export', [ReportController::class, 'expenseEstimateExport'])->name('v1.reports.expense-estimate.export'); + + // AI Report API + Route::get('/ai', [AiReportController::class, 'index'])->name('v1.reports.ai.index'); + Route::post('/ai/generate', [AiReportController::class, 'generate'])->name('v1.reports.ai.generate'); + Route::get('/ai/{id}', [AiReportController::class, 'show'])->whereNumber('id')->name('v1.reports.ai.show'); + Route::delete('/ai/{id}', [AiReportController::class, 'destroy'])->whereNumber('id')->name('v1.reports.ai.destroy'); +}); + +// Dashboard API (대시보드) +Route::prefix('dashboard')->group(function () { + Route::get('/summary', [DashboardController::class, 'summary'])->name('v1.dashboard.summary'); + Route::get('/charts', [DashboardController::class, 'charts'])->name('v1.dashboard.charts'); + Route::get('/approvals', [DashboardController::class, 'approvals'])->name('v1.dashboard.approvals'); +}); diff --git a/routes/api/v1/design.php b/routes/api/v1/design.php new file mode 100644 index 0000000..8fbb9fe --- /dev/null +++ b/routes/api/v1/design.php @@ -0,0 +1,84 @@ +group(function () { + // Design Model API (설계 모델) + Route::prefix('models')->group(function () { + Route::get('', [DesignModelController::class, 'index'])->name('v1.design.models.index'); + Route::post('', [DesignModelController::class, 'store'])->name('v1.design.models.store'); + Route::get('/archived', [DesignModelController::class, 'archived'])->name('v1.design.models.archived'); + Route::get('/{id}', [DesignModelController::class, 'show'])->whereNumber('id')->name('v1.design.models.show'); + Route::put('/{id}', [DesignModelController::class, 'update'])->whereNumber('id')->name('v1.design.models.update'); + Route::delete('/{id}', [DesignModelController::class, 'destroy'])->whereNumber('id')->name('v1.design.models.destroy'); + Route::post('/{id}/archive', [DesignModelController::class, 'archive'])->whereNumber('id')->name('v1.design.models.archive'); + Route::post('/{id}/restore', [DesignModelController::class, 'restore'])->whereNumber('id')->name('v1.design.models.restore'); + + // Model Version API (모델 버전) + Route::get('/{modelId}/versions', [DesignModelVersionController::class, 'index'])->whereNumber('modelId')->name('v1.design.models.versions.index'); + Route::post('/{modelId}/versions', [DesignModelVersionController::class, 'store'])->whereNumber('modelId')->name('v1.design.models.versions.store'); + Route::get('/{modelId}/versions/{id}', [DesignModelVersionController::class, 'show'])->whereNumber(['modelId', 'id'])->name('v1.design.models.versions.show'); + Route::put('/{modelId}/versions/{id}', [DesignModelVersionController::class, 'update'])->whereNumber(['modelId', 'id'])->name('v1.design.models.versions.update'); + Route::delete('/{modelId}/versions/{id}', [DesignModelVersionController::class, 'destroy'])->whereNumber(['modelId', 'id'])->name('v1.design.models.versions.destroy'); + Route::post('/{modelId}/versions/{id}/release', [DesignModelVersionController::class, 'release'])->whereNumber(['modelId', 'id'])->name('v1.design.models.versions.release'); + Route::post('/{modelId}/versions/{id}/clone', [DesignModelVersionController::class, 'clone'])->whereNumber(['modelId', 'id'])->name('v1.design.models.versions.clone'); + Route::get('/{modelId}/versions/{id}/diff', [DesignModelVersionController::class, 'diff'])->whereNumber(['modelId', 'id'])->name('v1.design.models.versions.diff'); + }); + + // BOM Template API (BOM 템플릿) + Route::prefix('bom-templates')->group(function () { + Route::get('', [DesignBomTemplateController::class, 'index'])->name('v1.design.bom-templates.index'); + Route::post('', [DesignBomTemplateController::class, 'store'])->name('v1.design.bom-templates.store'); + Route::get('/{id}', [DesignBomTemplateController::class, 'show'])->whereNumber('id')->name('v1.design.bom-templates.show'); + Route::put('/{id}', [DesignBomTemplateController::class, 'update'])->whereNumber('id')->name('v1.design.bom-templates.update'); + Route::delete('/{id}', [DesignBomTemplateController::class, 'destroy'])->whereNumber('id')->name('v1.design.bom-templates.destroy'); + Route::put('/{id}/items/bulk-upsert', [DesignBomTemplateController::class, 'bulkUpsertItems'])->whereNumber('id')->name('v1.design.bom-templates.items.bulk-upsert'); + Route::post('/{id}/items/reorder', [DesignBomTemplateController::class, 'reorderItems'])->whereNumber('id')->name('v1.design.bom-templates.items.reorder'); + Route::get('/{id}/summary', [DesignBomTemplateController::class, 'summary'])->whereNumber('id')->name('v1.design.bom-templates.summary'); + Route::get('/{id}/validate', [DesignBomTemplateController::class, 'validate'])->whereNumber('id')->name('v1.design.bom-templates.validate'); + }); + + // BOM Calculation API (BOM 계산) + Route::prefix('bom-calculation')->group(function () { + Route::post('/calculate', [BomCalculationController::class, 'calculate'])->name('v1.design.bom-calculation.calculate'); + Route::post('/preview', [BomCalculationController::class, 'preview'])->name('v1.design.bom-calculation.preview'); + Route::get('/form-schema/{versionId}', [BomCalculationController::class, 'getFormSchema'])->whereNumber('versionId')->name('v1.design.bom-calculation.form-schema'); + }); + + // Audit Log API (감사 로그) + Route::prefix('audit-logs')->group(function () { + Route::get('', [DesignAuditLogController::class, 'index'])->name('v1.design.audit-logs.index'); + Route::get('/{id}', [DesignAuditLogController::class, 'show'])->whereNumber('id')->name('v1.design.audit-logs.show'); + }); +}); + +// Model Set API (모델셋 관리) +Route::prefix('model-sets')->group(function () { + Route::get('', [ModelSetController::class, 'index'])->name('v1.model-sets.index'); + Route::post('', [ModelSetController::class, 'store'])->name('v1.model-sets.store'); + Route::get('/active', [ModelSetController::class, 'active'])->name('v1.model-sets.active'); + Route::get('/{id}', [ModelSetController::class, 'show'])->whereNumber('id')->name('v1.model-sets.show'); + Route::put('/{id}', [ModelSetController::class, 'update'])->whereNumber('id')->name('v1.model-sets.update'); + Route::delete('/{id}', [ModelSetController::class, 'destroy'])->whereNumber('id')->name('v1.model-sets.destroy'); + Route::patch('/{id}/toggle', [ModelSetController::class, 'toggle'])->whereNumber('id')->name('v1.model-sets.toggle'); + Route::post('/{id}/clone', [ModelSetController::class, 'clone'])->whereNumber('id')->name('v1.model-sets.clone'); + Route::put('/{id}/items', [ModelSetController::class, 'updateItems'])->whereNumber('id')->name('v1.model-sets.items'); +}); diff --git a/routes/api/v1/files.php b/routes/api/v1/files.php new file mode 100644 index 0000000..1f0ca39 --- /dev/null +++ b/routes/api/v1/files.php @@ -0,0 +1,44 @@ +group(function () { + Route::post('/upload', [FileStorageController::class, 'upload'])->name('v1.files.upload'); // 파일 업로드 (임시) + Route::post('/move', [FileStorageController::class, 'move'])->name('v1.files.move'); // 파일 이동 (temp → folder) + Route::get('/', [FileStorageController::class, 'index'])->name('v1.files.index'); // 파일 목록 + Route::get('/trash', [FileStorageController::class, 'trash'])->name('v1.files.trash'); // 휴지통 목록 + Route::get('/{id}', [FileStorageController::class, 'show'])->name('v1.files.show'); // 파일 상세 + Route::get('/{id}/download', [FileStorageController::class, 'download'])->name('v1.files.download'); // 파일 다운로드 + Route::delete('/{id}', [FileStorageController::class, 'destroy'])->name('v1.files.destroy'); // 파일 삭제 (soft) + Route::post('/{id}/restore', [FileStorageController::class, 'restore'])->name('v1.files.restore'); // 파일 복구 + Route::delete('/{id}/permanent', [FileStorageController::class, 'permanentDelete'])->name('v1.files.permanent'); // 파일 영구 삭제 + Route::post('/{id}/share', [FileStorageController::class, 'createShareLink'])->name('v1.files.share'); // 공유 링크 생성 +}); + +// 저장소 사용량 +Route::get('/storage/usage', [FileStorageController::class, 'storageUsage'])->name('v1.storage.usage'); + +// 폴더 관리 API +Route::prefix('folders')->group(function () { + Route::get('/', [FolderController::class, 'index'])->name('v1.folders.index'); // 폴더 목록 + Route::post('/', [FolderController::class, 'store'])->name('v1.folders.store'); // 폴더 생성 + Route::get('/{id}', [FolderController::class, 'show'])->name('v1.folders.show'); // 폴더 상세 + Route::put('/{id}', [FolderController::class, 'update'])->name('v1.folders.update'); // 폴더 수정 + Route::delete('/{id}', [FolderController::class, 'destroy'])->name('v1.folders.destroy'); // 폴더 삭제/비활성화 + Route::post('/reorder', [FolderController::class, 'reorder'])->name('v1.folders.reorder'); // 폴더 순서 변경 +}); + +// 공유 링크 다운로드 (인증 불필요 - 메인 api.php에서 v1 그룹 밖으로 분리) +// Route::get('/files/share/{token}', [FileStorageController::class, 'downloadShared'])->name('v1.files.share.download'); diff --git a/routes/api/v1/finance.php b/routes/api/v1/finance.php new file mode 100644 index 0000000..2d03aca --- /dev/null +++ b/routes/api/v1/finance.php @@ -0,0 +1,298 @@ +group(function () { + Route::get('', [CardController::class, 'index'])->name('v1.cards.index'); + Route::post('', [CardController::class, 'store'])->name('v1.cards.store'); + Route::get('/active', [CardController::class, 'active'])->name('v1.cards.active'); + Route::get('/{id}', [CardController::class, 'show'])->whereNumber('id')->name('v1.cards.show'); + Route::put('/{id}', [CardController::class, 'update'])->whereNumber('id')->name('v1.cards.update'); + Route::delete('/{id}', [CardController::class, 'destroy'])->whereNumber('id')->name('v1.cards.destroy'); + Route::patch('/{id}/toggle', [CardController::class, 'toggle'])->whereNumber('id')->name('v1.cards.toggle'); +}); + +// BankAccount API (계좌 관리) +Route::prefix('bank-accounts')->group(function () { + Route::get('', [BankAccountController::class, 'index'])->name('v1.bank-accounts.index'); + Route::post('', [BankAccountController::class, 'store'])->name('v1.bank-accounts.store'); + Route::get('/active', [BankAccountController::class, 'active'])->name('v1.bank-accounts.active'); + Route::get('/{id}', [BankAccountController::class, 'show'])->whereNumber('id')->name('v1.bank-accounts.show'); + Route::put('/{id}', [BankAccountController::class, 'update'])->whereNumber('id')->name('v1.bank-accounts.update'); + Route::delete('/{id}', [BankAccountController::class, 'destroy'])->whereNumber('id')->name('v1.bank-accounts.destroy'); + Route::patch('/{id}/toggle', [BankAccountController::class, 'toggle'])->whereNumber('id')->name('v1.bank-accounts.toggle'); + Route::patch('/{id}/set-primary', [BankAccountController::class, 'setPrimary'])->whereNumber('id')->name('v1.bank-accounts.set-primary'); +}); + +// Deposit API (입금 관리) +Route::prefix('deposits')->group(function () { + Route::get('', [DepositController::class, 'index'])->name('v1.deposits.index'); + Route::post('', [DepositController::class, 'store'])->name('v1.deposits.store'); + Route::get('/summary', [DepositController::class, 'summary'])->name('v1.deposits.summary'); + Route::post('/bulk-update-account-code', [DepositController::class, 'bulkUpdateAccountCode'])->name('v1.deposits.bulk-update-account-code'); + Route::get('/{id}', [DepositController::class, 'show'])->whereNumber('id')->name('v1.deposits.show'); + Route::put('/{id}', [DepositController::class, 'update'])->whereNumber('id')->name('v1.deposits.update'); + Route::delete('/{id}', [DepositController::class, 'destroy'])->whereNumber('id')->name('v1.deposits.destroy'); +}); + +// Withdrawal API (출금 관리) +Route::prefix('withdrawals')->group(function () { + Route::get('', [WithdrawalController::class, 'index'])->name('v1.withdrawals.index'); + Route::post('', [WithdrawalController::class, 'store'])->name('v1.withdrawals.store'); + Route::get('/summary', [WithdrawalController::class, 'summary'])->name('v1.withdrawals.summary'); + Route::post('/bulk-update-account-code', [WithdrawalController::class, 'bulkUpdateAccountCode'])->name('v1.withdrawals.bulk-update-account-code'); + Route::get('/{id}', [WithdrawalController::class, 'show'])->whereNumber('id')->name('v1.withdrawals.show'); + Route::put('/{id}', [WithdrawalController::class, 'update'])->whereNumber('id')->name('v1.withdrawals.update'); + Route::delete('/{id}', [WithdrawalController::class, 'destroy'])->whereNumber('id')->name('v1.withdrawals.destroy'); +}); + +// Payroll API (급여 관리) +Route::prefix('payrolls')->group(function () { + Route::get('', [PayrollController::class, 'index'])->name('v1.payrolls.index'); + Route::post('', [PayrollController::class, 'store'])->name('v1.payrolls.store'); + Route::get('/summary', [PayrollController::class, 'summary'])->name('v1.payrolls.summary'); + Route::post('/calculate', [PayrollController::class, 'calculate'])->name('v1.payrolls.calculate'); + Route::post('/bulk-confirm', [PayrollController::class, 'bulkConfirm'])->name('v1.payrolls.bulk-confirm'); + Route::get('/{id}', [PayrollController::class, 'show'])->whereNumber('id')->name('v1.payrolls.show'); + Route::put('/{id}', [PayrollController::class, 'update'])->whereNumber('id')->name('v1.payrolls.update'); + Route::delete('/{id}', [PayrollController::class, 'destroy'])->whereNumber('id')->name('v1.payrolls.destroy'); + Route::post('/{id}/confirm', [PayrollController::class, 'confirm'])->whereNumber('id')->name('v1.payrolls.confirm'); + Route::post('/{id}/pay', [PayrollController::class, 'pay'])->whereNumber('id')->name('v1.payrolls.pay'); + Route::get('/{id}/payslip', [PayrollController::class, 'payslip'])->whereNumber('id')->name('v1.payrolls.payslip'); +}); + +// Salary API (급여 관리 - React 연동) +Route::prefix('salaries')->group(function () { + Route::get('', [SalaryController::class, 'index'])->name('v1.salaries.index'); + Route::post('', [SalaryController::class, 'store'])->name('v1.salaries.store'); + Route::get('/statistics', [SalaryController::class, 'statistics'])->name('v1.salaries.statistics'); + Route::get('/export', [SalaryController::class, 'export'])->name('v1.salaries.export'); + Route::post('/bulk-update-status', [SalaryController::class, 'bulkUpdateStatus'])->name('v1.salaries.bulk-update-status'); + Route::get('/{id}', [SalaryController::class, 'show'])->whereNumber('id')->name('v1.salaries.show'); + Route::put('/{id}', [SalaryController::class, 'update'])->whereNumber('id')->name('v1.salaries.update'); + Route::delete('/{id}', [SalaryController::class, 'destroy'])->whereNumber('id')->name('v1.salaries.destroy'); + Route::patch('/{id}/status', [SalaryController::class, 'updateStatus'])->whereNumber('id')->name('v1.salaries.update-status'); +}); + +// Expected Expense API (미지급비용 관리) +Route::prefix('expected-expenses')->group(function () { + Route::get('', [ExpectedExpenseController::class, 'index'])->name('v1.expected-expenses.index'); + Route::post('', [ExpectedExpenseController::class, 'store'])->name('v1.expected-expenses.store'); + Route::get('/summary', [ExpectedExpenseController::class, 'summary'])->name('v1.expected-expenses.summary'); + Route::get('/dashboard-detail', [ExpectedExpenseController::class, 'dashboardDetail'])->name('v1.expected-expenses.dashboard-detail'); + Route::delete('', [ExpectedExpenseController::class, 'destroyMany'])->name('v1.expected-expenses.destroy-many'); + Route::put('/update-payment-date', [ExpectedExpenseController::class, 'updateExpectedPaymentDate'])->name('v1.expected-expenses.update-payment-date'); + Route::get('/{id}', [ExpectedExpenseController::class, 'show'])->whereNumber('id')->name('v1.expected-expenses.show'); + Route::put('/{id}', [ExpectedExpenseController::class, 'update'])->whereNumber('id')->name('v1.expected-expenses.update'); + Route::delete('/{id}', [ExpectedExpenseController::class, 'destroy'])->whereNumber('id')->name('v1.expected-expenses.destroy'); +}); + +// Loan API (가지급금 관리) +Route::prefix('loans')->group(function () { + Route::get('', [LoanController::class, 'index'])->name('v1.loans.index'); + Route::post('', [LoanController::class, 'store'])->name('v1.loans.store'); + Route::get('/summary', [LoanController::class, 'summary'])->name('v1.loans.summary'); + Route::get('/dashboard', [LoanController::class, 'dashboard'])->name('v1.loans.dashboard'); + Route::get('/tax-simulation', [LoanController::class, 'taxSimulation'])->name('v1.loans.tax-simulation'); + Route::post('/calculate-interest', [LoanController::class, 'calculateInterest'])->name('v1.loans.calculate-interest'); + Route::get('/interest-report/{year}', [LoanController::class, 'interestReport'])->whereNumber('year')->name('v1.loans.interest-report'); + Route::get('/{id}', [LoanController::class, 'show'])->whereNumber('id')->name('v1.loans.show'); + Route::put('/{id}', [LoanController::class, 'update'])->whereNumber('id')->name('v1.loans.update'); + Route::delete('/{id}', [LoanController::class, 'destroy'])->whereNumber('id')->name('v1.loans.destroy'); + Route::post('/{id}/settle', [LoanController::class, 'settle'])->whereNumber('id')->name('v1.loans.settle'); +}); + +// Vendor Ledger API (거래처원장) +Route::prefix('vendor-ledger')->group(function () { + Route::get('', [VendorLedgerController::class, 'index'])->name('v1.vendor-ledger.index'); + Route::get('/summary', [VendorLedgerController::class, 'summary'])->name('v1.vendor-ledger.summary'); + Route::get('/{clientId}', [VendorLedgerController::class, 'show'])->whereNumber('clientId')->name('v1.vendor-ledger.show'); +}); + +// Card Transaction API (카드 거래) +Route::prefix('card-transactions')->group(function () { + Route::get('', [CardTransactionController::class, 'index'])->name('v1.card-transactions.index'); + Route::get('/summary', [CardTransactionController::class, 'summary'])->name('v1.card-transactions.summary'); + Route::get('/dashboard', [CardTransactionController::class, 'dashboard'])->name('v1.card-transactions.dashboard'); + Route::post('', [CardTransactionController::class, 'store'])->name('v1.card-transactions.store'); + Route::put('/bulk-update-account', [CardTransactionController::class, 'bulkUpdateAccountCode'])->name('v1.card-transactions.bulk-update-account'); + Route::get('/{id}', [CardTransactionController::class, 'show'])->whereNumber('id')->name('v1.card-transactions.show'); + Route::put('/{id}', [CardTransactionController::class, 'update'])->whereNumber('id')->name('v1.card-transactions.update'); + Route::delete('/{id}', [CardTransactionController::class, 'destroy'])->whereNumber('id')->name('v1.card-transactions.destroy'); +}); + +// Bank Transaction API (은행 거래 조회) +Route::prefix('bank-transactions')->group(function () { + Route::get('', [BankTransactionController::class, 'index'])->name('v1.bank-transactions.index'); + Route::get('/summary', [BankTransactionController::class, 'summary'])->name('v1.bank-transactions.summary'); + Route::get('/accounts', [BankTransactionController::class, 'accounts'])->name('v1.bank-transactions.accounts'); +}); + +// Receivables API (채권 현황) +Route::prefix('receivables')->group(function () { + Route::get('', [ReceivablesController::class, 'index'])->name('v1.receivables.index'); + Route::get('/summary', [ReceivablesController::class, 'summary'])->name('v1.receivables.summary'); + Route::put('/overdue-status', [ReceivablesController::class, 'updateOverdueStatus'])->name('v1.receivables.update-overdue-status'); + Route::put('/memos', [ReceivablesController::class, 'updateMemos'])->name('v1.receivables.update-memos'); +}); + +// Daily Report API (일일 보고서) +Route::prefix('daily-report')->group(function () { + Route::get('/note-receivables', [DailyReportController::class, 'noteReceivables'])->name('v1.daily-report.note-receivables'); + Route::get('/daily-accounts', [DailyReportController::class, 'dailyAccounts'])->name('v1.daily-report.daily-accounts'); + Route::get('/summary', [DailyReportController::class, 'summary'])->name('v1.daily-report.summary'); +}); + +// Comprehensive Analysis API (종합 분석 보고서) +Route::get('/comprehensive-analysis', [ComprehensiveAnalysisController::class, 'index'])->name('v1.comprehensive-analysis.index'); + +// Status Board API (CEO 대시보드 현황판) +Route::get('/status-board/summary', [StatusBoardController::class, 'summary'])->name('v1.status-board.summary'); + +// Today Issue API (CEO 대시보드 오늘의 이슈 리스트) +Route::get('/today-issues/summary', [TodayIssueController::class, 'summary'])->name('v1.today-issues.summary'); +Route::get('/today-issues/unread', [TodayIssueController::class, 'unread'])->name('v1.today-issues.unread'); +Route::get('/today-issues/unread/count', [TodayIssueController::class, 'unreadCount'])->name('v1.today-issues.unread.count'); +Route::post('/today-issues/{id}/read', [TodayIssueController::class, 'markAsRead'])->whereNumber('id')->name('v1.today-issues.read'); +Route::post('/today-issues/read-all', [TodayIssueController::class, 'markAllAsRead'])->name('v1.today-issues.read-all'); + +// Calendar API (CEO 대시보드 캘린더) +Route::get('/calendar/schedules', [CalendarController::class, 'summary'])->name('v1.calendar.schedules'); + +// Vat API (CEO 대시보드 부가세 현황) +Route::get('/vat/summary', [VatController::class, 'summary'])->name('v1.vat.summary'); + +// Entertainment API (CEO 대시보드 접대비 현황) +Route::get('/entertainment/summary', [EntertainmentController::class, 'summary'])->name('v1.entertainment.summary'); + +// Welfare API (CEO 대시보드 복리후생비 현황) +Route::get('/welfare/summary', [WelfareController::class, 'summary'])->name('v1.welfare.summary'); +Route::get('/welfare/detail', [WelfareController::class, 'detail'])->name('v1.welfare.detail'); + +// Plan API (요금제 관리) +Route::prefix('plans')->group(function () { + Route::get('', [PlanController::class, 'index'])->name('v1.plans.index'); + Route::post('', [PlanController::class, 'store'])->name('v1.plans.store'); + Route::get('/active', [PlanController::class, 'active'])->name('v1.plans.active'); + Route::get('/{id}', [PlanController::class, 'show'])->whereNumber('id')->name('v1.plans.show'); + Route::put('/{id}', [PlanController::class, 'update'])->whereNumber('id')->name('v1.plans.update'); + Route::delete('/{id}', [PlanController::class, 'destroy'])->whereNumber('id')->name('v1.plans.destroy'); + Route::patch('/{id}/toggle', [PlanController::class, 'toggle'])->whereNumber('id')->name('v1.plans.toggle'); +}); + +// Subscription API (구독 관리) +Route::prefix('subscriptions')->group(function () { + Route::get('', [SubscriptionController::class, 'index'])->name('v1.subscriptions.index'); + Route::post('', [SubscriptionController::class, 'store'])->name('v1.subscriptions.store'); + Route::get('/current', [SubscriptionController::class, 'current'])->name('v1.subscriptions.current'); + Route::get('/usage', [SubscriptionController::class, 'usage'])->name('v1.subscriptions.usage'); + Route::post('/export', [SubscriptionController::class, 'export'])->name('v1.subscriptions.export'); + Route::get('/export/{id}', [SubscriptionController::class, 'exportStatus'])->whereNumber('id')->name('v1.subscriptions.export.status'); + Route::get('/{id}', [SubscriptionController::class, 'show'])->whereNumber('id')->name('v1.subscriptions.show'); + Route::post('/{id}/cancel', [SubscriptionController::class, 'cancel'])->whereNumber('id')->name('v1.subscriptions.cancel'); + Route::post('/{id}/renew', [SubscriptionController::class, 'renew'])->whereNumber('id')->name('v1.subscriptions.renew'); + Route::post('/{id}/suspend', [SubscriptionController::class, 'suspend'])->whereNumber('id')->name('v1.subscriptions.suspend'); + Route::post('/{id}/resume', [SubscriptionController::class, 'resume'])->whereNumber('id')->name('v1.subscriptions.resume'); +}); + +// Payment API (결제 관리) +Route::prefix('payments')->group(function () { + Route::get('', [PaymentController::class, 'index'])->name('v1.payments.index'); + Route::post('', [PaymentController::class, 'store'])->name('v1.payments.store'); + Route::get('/summary', [PaymentController::class, 'summary'])->name('v1.payments.summary'); + Route::get('/{id}', [PaymentController::class, 'show'])->whereNumber('id')->name('v1.payments.show'); + Route::post('/{id}/complete', [PaymentController::class, 'complete'])->whereNumber('id')->name('v1.payments.complete'); + Route::post('/{id}/cancel', [PaymentController::class, 'cancel'])->whereNumber('id')->name('v1.payments.cancel'); + Route::post('/{id}/refund', [PaymentController::class, 'refund'])->whereNumber('id')->name('v1.payments.refund'); + Route::get('/{id}/statement', [PaymentController::class, 'statement'])->whereNumber('id')->name('v1.payments.statement'); +}); + +// Barobill Setting API (바로빌 설정) +Route::prefix('barobill-settings')->group(function () { + Route::get('', [BarobillSettingController::class, 'show'])->name('v1.barobill-settings.show'); + Route::put('', [BarobillSettingController::class, 'save'])->name('v1.barobill-settings.save'); + Route::post('/test-connection', [BarobillSettingController::class, 'testConnection'])->name('v1.barobill-settings.test-connection'); +}); + +// Tax Invoice API (세금계산서) +Route::prefix('tax-invoices')->group(function () { + Route::get('', [TaxInvoiceController::class, 'index'])->name('v1.tax-invoices.index'); + Route::post('', [TaxInvoiceController::class, 'store'])->name('v1.tax-invoices.store'); + Route::get('/summary', [TaxInvoiceController::class, 'summary'])->name('v1.tax-invoices.summary'); + Route::get('/{id}', [TaxInvoiceController::class, 'show'])->whereNumber('id')->name('v1.tax-invoices.show'); + Route::put('/{id}', [TaxInvoiceController::class, 'update'])->whereNumber('id')->name('v1.tax-invoices.update'); + Route::delete('/{id}', [TaxInvoiceController::class, 'destroy'])->whereNumber('id')->name('v1.tax-invoices.destroy'); + Route::post('/{id}/issue', [TaxInvoiceController::class, 'issue'])->whereNumber('id')->name('v1.tax-invoices.issue'); + Route::post('/{id}/cancel', [TaxInvoiceController::class, 'cancel'])->whereNumber('id')->name('v1.tax-invoices.cancel'); + Route::get('/{id}/check-status', [TaxInvoiceController::class, 'checkStatus'])->whereNumber('id')->name('v1.tax-invoices.check-status'); + Route::post('/bulk-issue', [TaxInvoiceController::class, 'bulkIssue'])->name('v1.tax-invoices.bulk-issue'); +}); + +// Bad Debt API (악성채권 추심관리) +Route::prefix('bad-debts')->group(function () { + Route::get('', [BadDebtController::class, 'index'])->name('v1.bad-debts.index'); + Route::post('', [BadDebtController::class, 'store'])->name('v1.bad-debts.store'); + Route::get('/summary', [BadDebtController::class, 'summary'])->name('v1.bad-debts.summary'); + Route::get('/{id}', [BadDebtController::class, 'show'])->whereNumber('id')->name('v1.bad-debts.show'); + Route::put('/{id}', [BadDebtController::class, 'update'])->whereNumber('id')->name('v1.bad-debts.update'); + Route::delete('/{id}', [BadDebtController::class, 'destroy'])->whereNumber('id')->name('v1.bad-debts.destroy'); + Route::patch('/{id}/toggle', [BadDebtController::class, 'toggle'])->whereNumber('id')->name('v1.bad-debts.toggle'); + // 서류 + Route::post('/{id}/documents', [BadDebtController::class, 'addDocument'])->whereNumber('id')->name('v1.bad-debts.documents.store'); + Route::delete('/{id}/documents/{documentId}', [BadDebtController::class, 'removeDocument'])->whereNumber(['id', 'documentId'])->name('v1.bad-debts.documents.destroy'); + // 메모 + Route::post('/{id}/memos', [BadDebtController::class, 'addMemo'])->whereNumber('id')->name('v1.bad-debts.memos.store'); + Route::delete('/{id}/memos/{memoId}', [BadDebtController::class, 'removeMemo'])->whereNumber(['id', 'memoId'])->name('v1.bad-debts.memos.destroy'); +}); + +// Bill API (어음관리) +Route::prefix('bills')->group(function () { + Route::get('', [BillController::class, 'index'])->name('v1.bills.index'); + Route::post('', [BillController::class, 'store'])->name('v1.bills.store'); + Route::get('/summary', [BillController::class, 'summary'])->name('v1.bills.summary'); + Route::get('/dashboard-detail', [BillController::class, 'dashboardDetail'])->name('v1.bills.dashboard-detail'); + Route::get('/{id}', [BillController::class, 'show'])->whereNumber('id')->name('v1.bills.show'); + Route::put('/{id}', [BillController::class, 'update'])->whereNumber('id')->name('v1.bills.update'); + Route::delete('/{id}', [BillController::class, 'destroy'])->whereNumber('id')->name('v1.bills.destroy'); + Route::patch('/{id}/status', [BillController::class, 'updateStatus'])->whereNumber('id')->name('v1.bills.update-status'); +}); diff --git a/routes/api/v1/hr.php b/routes/api/v1/hr.php new file mode 100644 index 0000000..4bcf9cf --- /dev/null +++ b/routes/api/v1/hr.php @@ -0,0 +1,215 @@ +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'); + // 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}/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'); + }); +}); diff --git a/routes/api/v1/inventory.php b/routes/api/v1/inventory.php new file mode 100644 index 0000000..4dbde58 --- /dev/null +++ b/routes/api/v1/inventory.php @@ -0,0 +1,117 @@ +group(function () { + Route::get('', [ItemsController::class, 'index'])->name('v1.items.index'); + Route::post('', [ItemsController::class, 'store'])->name('v1.items.store'); + Route::get('/options', [ItemsController::class, 'options'])->name('v1.items.options'); + Route::get('/stats', [ItemsController::class, 'stats'])->name('v1.items.stats'); + Route::get('/stats-by-type', [ItemsController::class, 'statsByItemType'])->name('v1.items.stats-by-type'); + Route::delete('/bulk', [ItemsController::class, 'bulkDestroy'])->name('v1.items.bulk-destroy'); + Route::get('/{id}', [ItemsController::class, 'show'])->whereNumber('id')->name('v1.items.show'); + Route::put('/{id}', [ItemsController::class, 'update'])->whereNumber('id')->name('v1.items.update'); + Route::delete('/{id}', [ItemsController::class, 'destroy'])->whereNumber('id')->name('v1.items.destroy'); + Route::patch('/{id}/toggle', [ItemsController::class, 'toggle'])->whereNumber('id')->name('v1.items.toggle'); +}); + +// Items BOM API (품목 BOM) +Route::prefix('items/{id}/bom')->group(function () { + Route::get('', [ItemsBomController::class, 'index'])->whereNumber('id')->name('v1.items.bom.index'); // BOM 목록 + Route::post('', [ItemsBomController::class, 'store'])->whereNumber('id')->name('v1.items.bom.store'); // BOM 항목 추가 + Route::put('/bulk-upsert', [ItemsBomController::class, 'bulkUpsert'])->whereNumber('id')->name('v1.items.bom.bulk-upsert'); // BOM 일괄 저장 + Route::post('/reorder', [ItemsBomController::class, 'reorder'])->whereNumber('id')->name('v1.items.bom.reorder'); // BOM 순서 변경 + Route::get('/summary', [ItemsBomController::class, 'summary'])->whereNumber('id')->name('v1.items.bom.summary'); // BOM 요약 + Route::get('/validate', [ItemsBomController::class, 'validate'])->whereNumber('id')->name('v1.items.bom.validate'); // BOM 검증 + Route::get('/{bomId}', [ItemsBomController::class, 'show'])->whereNumber(['id', 'bomId'])->name('v1.items.bom.show'); // BOM 항목 상세 + Route::put('/{bomId}', [ItemsBomController::class, 'update'])->whereNumber(['id', 'bomId'])->name('v1.items.bom.update'); // BOM 항목 수정 + Route::delete('/{bomId}', [ItemsBomController::class, 'destroy'])->whereNumber(['id', 'bomId'])->name('v1.items.bom.destroy'); // BOM 항목 삭제 +}); + +// Items File API (품목 파일) +Route::prefix('items/{id}/files')->group(function () { + Route::get('', [ItemsFileController::class, 'index'])->whereNumber('id')->name('v1.items.files.index'); // 파일 목록 + Route::post('', [ItemsFileController::class, 'store'])->whereNumber('id')->name('v1.items.files.store'); // 파일 추가 + Route::get('/{fileId}', [ItemsFileController::class, 'show'])->whereNumber(['id', 'fileId'])->name('v1.items.files.show'); // 파일 상세 + Route::delete('/{fileId}', [ItemsFileController::class, 'destroy'])->whereNumber(['id', 'fileId'])->name('v1.items.files.destroy'); // 파일 삭제 +}); + +// Labor API (노무비 관리) +Route::prefix('labor')->group(function () { + Route::get('', [LaborController::class, 'index'])->name('v1.labor.index'); + Route::post('', [LaborController::class, 'store'])->name('v1.labor.store'); + Route::get('/active', [LaborController::class, 'active'])->name('v1.labor.active'); + Route::get('/summary', [LaborController::class, 'summary'])->name('v1.labor.summary'); + Route::post('/bulk-upsert', [LaborController::class, 'bulkUpsert'])->name('v1.labor.bulk-upsert'); + Route::get('/{id}', [LaborController::class, 'show'])->whereNumber('id')->name('v1.labor.show'); + Route::put('/{id}', [LaborController::class, 'update'])->whereNumber('id')->name('v1.labor.update'); + Route::delete('/{id}', [LaborController::class, 'destroy'])->whereNumber('id')->name('v1.labor.destroy'); +}); + +// Purchase API (매입 관리) +Route::prefix('purchases')->group(function () { + Route::get('', [PurchaseController::class, 'index'])->name('v1.purchases.index'); + Route::post('', [PurchaseController::class, 'store'])->name('v1.purchases.store'); + Route::get('/summary', [PurchaseController::class, 'summary'])->name('v1.purchases.summary'); + Route::get('/dashboard-detail', [PurchaseController::class, 'dashboardDetail'])->name('v1.purchases.dashboard-detail'); + Route::post('/bulk-update-type', [PurchaseController::class, 'bulkUpdatePurchaseType'])->name('v1.purchases.bulk-update-type'); + Route::post('/bulk-update-tax-received', [PurchaseController::class, 'bulkUpdateTaxReceived'])->name('v1.purchases.bulk-update-tax-received'); + Route::get('/{id}', [PurchaseController::class, 'show'])->whereNumber('id')->name('v1.purchases.show'); + Route::put('/{id}', [PurchaseController::class, 'update'])->whereNumber('id')->name('v1.purchases.update'); + Route::delete('/{id}', [PurchaseController::class, 'destroy'])->whereNumber('id')->name('v1.purchases.destroy'); + Route::post('/{id}/confirm', [PurchaseController::class, 'confirm'])->whereNumber('id')->name('v1.purchases.confirm'); +}); + +// Receiving API (입고 관리) +Route::prefix('receivings')->group(function () { + Route::get('', [ReceivingController::class, 'index'])->name('v1.receivings.index'); + Route::post('', [ReceivingController::class, 'store'])->name('v1.receivings.store'); + Route::get('/stats', [ReceivingController::class, 'stats'])->name('v1.receivings.stats'); + Route::get('/{id}', [ReceivingController::class, 'show'])->whereNumber('id')->name('v1.receivings.show'); + Route::put('/{id}', [ReceivingController::class, 'update'])->whereNumber('id')->name('v1.receivings.update'); + Route::delete('/{id}', [ReceivingController::class, 'destroy'])->whereNumber('id')->name('v1.receivings.destroy'); + Route::post('/{id}/process', [ReceivingController::class, 'process'])->whereNumber('id')->name('v1.receivings.process'); +}); + +// Stock API (재고 현황) +Route::prefix('stocks')->group(function () { + Route::get('', [StockController::class, 'index'])->name('v1.stocks.index'); + Route::get('/stats', [StockController::class, 'stats'])->name('v1.stocks.stats'); + Route::get('/stats-by-type', [StockController::class, 'statsByItemType'])->name('v1.stocks.stats-by-type'); + Route::get('/{id}', [StockController::class, 'show'])->whereNumber('id')->name('v1.stocks.show'); +}); + +// Shipment API (출하 관리) +Route::prefix('shipments')->group(function () { + Route::get('', [ShipmentController::class, 'index'])->name('v1.shipments.index'); + Route::get('/stats', [ShipmentController::class, 'stats'])->name('v1.shipments.stats'); + Route::get('/stats-by-status', [ShipmentController::class, 'statsByStatus'])->name('v1.shipments.stats-by-status'); + Route::get('/options/lots', [ShipmentController::class, 'lotOptions'])->name('v1.shipments.options.lots'); + Route::get('/options/logistics', [ShipmentController::class, 'logisticsOptions'])->name('v1.shipments.options.logistics'); + Route::get('/options/vehicle-tonnage', [ShipmentController::class, 'vehicleTonnageOptions'])->name('v1.shipments.options.vehicle-tonnage'); + Route::post('', [ShipmentController::class, 'store'])->name('v1.shipments.store'); + Route::get('/{id}', [ShipmentController::class, 'show'])->whereNumber('id')->name('v1.shipments.show'); + Route::put('/{id}', [ShipmentController::class, 'update'])->whereNumber('id')->name('v1.shipments.update'); + Route::patch('/{id}/status', [ShipmentController::class, 'updateStatus'])->whereNumber('id')->name('v1.shipments.status'); + Route::delete('/{id}', [ShipmentController::class, 'destroy'])->whereNumber('id')->name('v1.shipments.destroy'); +}); diff --git a/routes/api/v1/production.php b/routes/api/v1/production.php new file mode 100644 index 0000000..92aea15 --- /dev/null +++ b/routes/api/v1/production.php @@ -0,0 +1,84 @@ +group(function () { + Route::get('', [ProcessController::class, 'index'])->name('v1.processes.index'); + Route::get('/options', [ProcessController::class, 'options'])->name('v1.processes.options'); + Route::get('/stats', [ProcessController::class, 'stats'])->name('v1.processes.stats'); + Route::post('', [ProcessController::class, 'store'])->name('v1.processes.store'); + Route::delete('', [ProcessController::class, 'destroyMany'])->name('v1.processes.destroy-many'); + Route::get('/{id}', [ProcessController::class, 'show'])->whereNumber('id')->name('v1.processes.show'); + Route::put('/{id}', [ProcessController::class, 'update'])->whereNumber('id')->name('v1.processes.update'); + Route::delete('/{id}', [ProcessController::class, 'destroy'])->whereNumber('id')->name('v1.processes.destroy'); + Route::patch('/{id}/toggle', [ProcessController::class, 'toggleActive'])->whereNumber('id')->name('v1.processes.toggle'); +}); + +// Work Order API (작업지시 관리) +Route::prefix('work-orders')->group(function () { + // 기본 CRUD + Route::get('', [WorkOrderController::class, 'index'])->name('v1.work-orders.index'); // 목록 + Route::get('/stats', [WorkOrderController::class, 'stats'])->name('v1.work-orders.stats'); // 통계 + Route::post('', [WorkOrderController::class, 'store'])->name('v1.work-orders.store'); // 생성 + Route::get('/{id}', [WorkOrderController::class, 'show'])->whereNumber('id')->name('v1.work-orders.show'); // 상세 + Route::put('/{id}', [WorkOrderController::class, 'update'])->whereNumber('id')->name('v1.work-orders.update'); // 수정 + Route::delete('/{id}', [WorkOrderController::class, 'destroy'])->whereNumber('id')->name('v1.work-orders.destroy'); // 삭제 + + // 상태 및 담당자 관리 + Route::patch('/{id}/status', [WorkOrderController::class, 'updateStatus'])->whereNumber('id')->name('v1.work-orders.status'); // 상태 변경 + Route::patch('/{id}/assign', [WorkOrderController::class, 'assign'])->whereNumber('id')->name('v1.work-orders.assign'); // 담당자 배정 + + // 벤딩 공정 상세 토글 + Route::patch('/{id}/bending/toggle', [WorkOrderController::class, 'toggleBendingField'])->whereNumber('id')->name('v1.work-orders.bending-toggle'); + + // 이슈 관리 + Route::post('/{id}/issues', [WorkOrderController::class, 'addIssue'])->whereNumber('id')->name('v1.work-orders.issues.store'); // 이슈 등록 + Route::patch('/{id}/issues/{issueId}/resolve', [WorkOrderController::class, 'resolveIssue'])->whereNumber('id')->name('v1.work-orders.issues.resolve'); // 이슈 해결 + + // 품목 상태 변경 + Route::patch('/{id}/items/{itemId}/status', [WorkOrderController::class, 'updateItemStatus'])->whereNumber('id')->whereNumber('itemId')->name('v1.work-orders.items.status'); + + // 자재 관리 + Route::get('/{id}/materials', [WorkOrderController::class, 'materials'])->whereNumber('id')->name('v1.work-orders.materials'); // 자재 목록 조회 + Route::post('/{id}/material-inputs', [WorkOrderController::class, 'registerMaterialInput'])->whereNumber('id')->name('v1.work-orders.material-inputs'); // 자재 투입 등록 +}); + +// Work Result API (작업실적 관리) +Route::prefix('work-results')->group(function () { + // 기본 CRUD + Route::get('', [WorkResultController::class, 'index'])->name('v1.work-results.index'); // 목록 + Route::get('/stats', [WorkResultController::class, 'stats'])->name('v1.work-results.stats'); // 통계 + Route::post('', [WorkResultController::class, 'store'])->name('v1.work-results.store'); // 생성 + Route::get('/{id}', [WorkResultController::class, 'show'])->whereNumber('id')->name('v1.work-results.show'); // 상세 + Route::put('/{id}', [WorkResultController::class, 'update'])->whereNumber('id')->name('v1.work-results.update'); // 수정 + Route::delete('/{id}', [WorkResultController::class, 'destroy'])->whereNumber('id')->name('v1.work-results.destroy'); // 삭제 + + // 상태 토글 + Route::patch('/{id}/inspection', [WorkResultController::class, 'toggleInspection'])->whereNumber('id')->name('v1.work-results.inspection'); // 검사 상태 토글 + Route::patch('/{id}/packaging', [WorkResultController::class, 'togglePackaging'])->whereNumber('id')->name('v1.work-results.packaging'); // 포장 상태 토글 +}); + +// Inspection API (검사 관리) +Route::prefix('inspections')->group(function () { + Route::get('', [InspectionController::class, 'index'])->name('v1.inspections.index'); // 목록 + Route::get('/stats', [InspectionController::class, 'stats'])->name('v1.inspections.stats'); // 통계 + Route::post('', [InspectionController::class, 'store'])->name('v1.inspections.store'); // 생성 + Route::get('/{id}', [InspectionController::class, 'show'])->whereNumber('id')->name('v1.inspections.show'); // 상세 + Route::put('/{id}', [InspectionController::class, 'update'])->whereNumber('id')->name('v1.inspections.update'); // 수정 + Route::delete('/{id}', [InspectionController::class, 'destroy'])->whereNumber('id')->name('v1.inspections.destroy'); // 삭제 + Route::patch('/{id}/complete', [InspectionController::class, 'complete'])->whereNumber('id')->name('v1.inspections.complete'); // 완료 처리 +}); diff --git a/routes/api/v1/sales.php b/routes/api/v1/sales.php new file mode 100644 index 0000000..4c2e37c --- /dev/null +++ b/routes/api/v1/sales.php @@ -0,0 +1,161 @@ +group(function () { + Route::get('', [ClientController::class, 'index'])->name('v1.clients.index'); + Route::post('', [ClientController::class, 'store'])->name('v1.clients.store'); + Route::get('/active', [ClientController::class, 'active'])->name('v1.clients.active'); + Route::get('/stats', [ClientController::class, 'stats'])->name('v1.clients.stats'); + Route::get('/{id}', [ClientController::class, 'show'])->whereNumber('id')->name('v1.clients.show'); + Route::put('/{id}', [ClientController::class, 'update'])->whereNumber('id')->name('v1.clients.update'); + Route::delete('/{id}', [ClientController::class, 'destroy'])->whereNumber('id')->name('v1.clients.destroy'); + Route::patch('/{id}/toggle', [ClientController::class, 'toggle'])->whereNumber('id')->name('v1.clients.toggle'); +}); + +// Client Group API (거래처 그룹 관리) +Route::prefix('client-groups')->group(function () { + Route::get('', [ClientGroupController::class, 'index'])->name('v1.client-groups.index'); + Route::post('', [ClientGroupController::class, 'store'])->name('v1.client-groups.store'); + Route::get('/{id}', [ClientGroupController::class, 'show'])->whereNumber('id')->name('v1.client-groups.show'); + Route::put('/{id}', [ClientGroupController::class, 'update'])->whereNumber('id')->name('v1.client-groups.update'); + Route::delete('/{id}', [ClientGroupController::class, 'destroy'])->whereNumber('id')->name('v1.client-groups.destroy'); +}); + +// Quote API (견적 관리) +Route::prefix('quotes')->group(function () { + Route::get('', [QuoteController::class, 'index'])->name('v1.quotes.index'); + Route::get('/stats', [QuoteController::class, 'stats'])->name('v1.quotes.stats'); + Route::get('/stage-counts', [QuoteController::class, 'stageCounts'])->name('v1.quotes.stage-counts'); + Route::post('', [QuoteController::class, 'store'])->name('v1.quotes.store'); + Route::delete('/bulk', [QuoteController::class, 'bulkDestroy'])->name('v1.quotes.bulk-destroy'); + Route::get('/{id}', [QuoteController::class, 'show'])->whereNumber('id')->name('v1.quotes.show'); + Route::put('/{id}', [QuoteController::class, 'update'])->whereNumber('id')->name('v1.quotes.update'); + Route::delete('/{id}', [QuoteController::class, 'destroy'])->whereNumber('id')->name('v1.quotes.destroy'); + Route::post('/{id}/clone', [QuoteController::class, 'clone'])->whereNumber('id')->name('v1.quotes.clone'); + Route::put('/{id}/stage', [QuoteController::class, 'updateStage'])->whereNumber('id')->name('v1.quotes.stage'); + Route::put('/{id}/items', [QuoteController::class, 'updateItems'])->whereNumber('id')->name('v1.quotes.items'); + Route::get('/{id}/histories', [QuoteController::class, 'histories'])->whereNumber('id')->name('v1.quotes.histories'); + Route::post('/{id}/histories', [QuoteController::class, 'addHistory'])->whereNumber('id')->name('v1.quotes.histories.store'); + Route::put('/{id}/histories/{historyId}', [QuoteController::class, 'updateHistory'])->whereNumber('id')->whereNumber('historyId')->name('v1.quotes.histories.update'); + Route::delete('/{id}/histories/{historyId}', [QuoteController::class, 'deleteHistory'])->whereNumber('id')->whereNumber('historyId')->name('v1.quotes.histories.destroy'); + // 견적서 관련 API + Route::get('/{id}/document', [QuoteController::class, 'getDocument'])->whereNumber('id')->name('v1.quotes.document.show'); + Route::post('/{id}/document/issue', [QuoteController::class, 'issueDocument'])->whereNumber('id')->name('v1.quotes.document.issue'); + Route::post('/{id}/document/send', [QuoteController::class, 'sendDocument'])->whereNumber('id')->name('v1.quotes.document.send'); + Route::post('/bulk-issue-document', [QuoteController::class, 'bulkIssueDocument'])->name('v1.quotes.bulk-issue-document'); +}); + +// Bidding API (입찰 관리) +Route::prefix('biddings')->group(function () { + Route::get('', [BiddingController::class, 'index'])->name('v1.biddings.index'); + Route::post('', [BiddingController::class, 'store'])->name('v1.biddings.store'); + Route::get('/stats', [BiddingController::class, 'stats'])->name('v1.biddings.stats'); + Route::delete('/bulk', [BiddingController::class, 'bulkDestroy'])->name('v1.biddings.bulk-destroy'); + Route::get('/{id}', [BiddingController::class, 'show'])->whereNumber('id')->name('v1.biddings.show'); + Route::put('/{id}', [BiddingController::class, 'update'])->whereNumber('id')->name('v1.biddings.update'); + Route::delete('/{id}', [BiddingController::class, 'destroy'])->whereNumber('id')->name('v1.biddings.destroy'); +}); + +// Pricing API (단가 관리) +Route::prefix('pricing')->group(function () { + Route::get('', [PricingController::class, 'index'])->name('v1.pricing.index'); + Route::post('', [PricingController::class, 'store'])->name('v1.pricing.store'); + Route::get('/types', [PricingController::class, 'types'])->name('v1.pricing.types'); + Route::get('/summary', [PricingController::class, 'summary'])->name('v1.pricing.summary'); + Route::get('/history/{itemId}', [PricingController::class, 'history'])->whereNumber('itemId')->name('v1.pricing.history'); + Route::post('/bulk-upsert', [PricingController::class, 'bulkUpsert'])->name('v1.pricing.bulk-upsert'); + Route::get('/{id}', [PricingController::class, 'show'])->whereNumber('id')->name('v1.pricing.show'); + Route::put('/{id}', [PricingController::class, 'update'])->whereNumber('id')->name('v1.pricing.update'); + Route::delete('/{id}', [PricingController::class, 'destroy'])->whereNumber('id')->name('v1.pricing.destroy'); +}); + +// Estimate API (견적/설계) +Route::prefix('estimates')->group(function () { + Route::get('', [EstimateController::class, 'index'])->name('v1.estimates.index'); // 견적 목록 + Route::get('/stats', [EstimateController::class, 'stats'])->name('v1.estimates.stats'); // 견적 통계 + Route::post('', [EstimateController::class, 'store'])->name('v1.estimates.store'); // 견적 생성 + Route::get('/{id}', [EstimateController::class, 'show'])->whereNumber('id')->name('v1.estimates.show'); // 견적 상세 + Route::put('/{id}', [EstimateController::class, 'update'])->whereNumber('id')->name('v1.estimates.update'); // 견적 수정 + Route::delete('/{id}', [EstimateController::class, 'destroy'])->whereNumber('id')->name('v1.estimates.destroy'); // 견적 삭제 + Route::post('/{id}/clone', [EstimateController::class, 'clone'])->name('v1.estimates.clone'); // 견적 복제 + Route::put('/{id}/status', [EstimateController::class, 'changeStatus'])->name('v1.estimates.status'); // 견적 상태 변경 + + // 견적 폼 및 계산 기능 + Route::get('/form-schema/{model_set_id}', [EstimateController::class, 'getFormSchema'])->name('v1.estimates.form-schema'); // 견적 폼 스키마 + Route::post('/preview/{model_set_id}', [EstimateController::class, 'previewCalculation'])->name('v1.estimates.preview'); // 견적 계산 미리보기 +}); + +// Order API (수주관리) +Route::prefix('orders')->group(function () { + // 기본 CRUD + Route::get('', [OrderController::class, 'index'])->name('v1.orders.index'); // 목록 + Route::get('/stats', [OrderController::class, 'stats'])->name('v1.orders.stats'); // 통계 + Route::post('', [OrderController::class, 'store'])->name('v1.orders.store'); // 생성 + Route::get('/{id}', [OrderController::class, 'show'])->whereNumber('id')->name('v1.orders.show'); // 상세 + Route::put('/{id}', [OrderController::class, 'update'])->whereNumber('id')->name('v1.orders.update'); // 수정 + Route::delete('/{id}', [OrderController::class, 'destroy'])->whereNumber('id')->name('v1.orders.destroy'); // 삭제 + + // 상태 관리 + Route::patch('/{id}/status', [OrderController::class, 'updateStatus'])->whereNumber('id')->name('v1.orders.status'); // 상태 변경 + + // 견적에서 수주 생성 + Route::post('/from-quote/{quoteId}', [OrderController::class, 'createFromQuote'])->whereNumber('quoteId')->name('v1.orders.from-quote'); + + // 생산지시 생성 + Route::post('/{id}/production-order', [OrderController::class, 'createProductionOrder'])->whereNumber('id')->name('v1.orders.production-order'); + + // 수주확정 되돌리기 + Route::post('/{id}/revert-confirmation', [OrderController::class, 'revertOrderConfirmation'])->whereNumber('id')->name('v1.orders.revert-confirmation'); + + // 생산지시 되돌리기 + Route::post('/{id}/revert-production', [OrderController::class, 'revertProductionOrder'])->whereNumber('id')->name('v1.orders.revert-production'); +}); + +// Sale API (매출 관리) +Route::prefix('sales')->group(function () { + Route::get('', [SaleController::class, 'index'])->name('v1.sales.index'); + Route::post('', [SaleController::class, 'store'])->name('v1.sales.store'); + Route::get('/summary', [SaleController::class, 'summary'])->name('v1.sales.summary'); + Route::get('/{id}', [SaleController::class, 'show'])->whereNumber('id')->name('v1.sales.show'); + Route::put('/{id}', [SaleController::class, 'update'])->whereNumber('id')->name('v1.sales.update'); + Route::delete('/{id}', [SaleController::class, 'destroy'])->whereNumber('id')->name('v1.sales.destroy'); + Route::post('/{id}/confirm', [SaleController::class, 'confirm'])->whereNumber('id')->name('v1.sales.confirm'); + Route::put('/bulk-update-account', [SaleController::class, 'bulkUpdateAccountCode'])->name('v1.sales.bulk-update-account'); + // 거래명세서 API + Route::get('/{id}/statement', [SaleController::class, 'getStatement'])->whereNumber('id')->name('v1.sales.statement.show'); + Route::post('/{id}/statement/issue', [SaleController::class, 'issueStatement'])->whereNumber('id')->name('v1.sales.statement.issue'); + Route::post('/{id}/statement/send', [SaleController::class, 'sendStatement'])->whereNumber('id')->name('v1.sales.statement.send'); + Route::post('/bulk-issue-statement', [SaleController::class, 'bulkIssueStatement'])->name('v1.sales.bulk-issue-statement'); +}); + +// Company API (회사 추가 관리) +Route::prefix('companies')->group(function () { + Route::post('/check', [CompanyController::class, 'check'])->name('v1.companies.check'); // 사업자등록번호 검증 + Route::post('/request', [CompanyController::class, 'request'])->name('v1.companies.request'); // 회사 추가 신청 + Route::get('/requests', [CompanyController::class, 'requests'])->name('v1.companies.requests.index'); // 신청 목록 (관리자) + Route::get('/requests/{id}', [CompanyController::class, 'showRequest'])->whereNumber('id')->name('v1.companies.requests.show'); // 신청 상세 + Route::post('/requests/{id}/approve', [CompanyController::class, 'approve'])->whereNumber('id')->name('v1.companies.requests.approve'); // 승인 + Route::post('/requests/{id}/reject', [CompanyController::class, 'reject'])->whereNumber('id')->name('v1.companies.requests.reject'); // 반려 + Route::get('/my-requests', [CompanyController::class, 'myRequests'])->name('v1.companies.my-requests'); // 내 신청 목록 +}); diff --git a/routes/api/v1/tenants.php b/routes/api/v1/tenants.php new file mode 100644 index 0000000..39dcb40 --- /dev/null +++ b/routes/api/v1/tenants.php @@ -0,0 +1,46 @@ +group(function () { + Route::get('list', [TenantController::class, 'index'])->name('v1.tenant.index'); // 테넌트 목록 조회 + Route::get('/', [TenantController::class, 'show'])->name('v1.tenant.show'); // 테넌트 정보 조회 + Route::put('/', [TenantController::class, 'update'])->name('v1.tenant.update'); // 테넌트 정보 수정 + Route::post('/', [TenantController::class, 'store'])->name('v1.tenant.store'); // 테넌트 등록 + Route::delete('/', [TenantController::class, 'destroy'])->name('v1.tenant.destroy'); // 테넌트 삭제(탈퇴) + Route::put('/restore/{tenant_id}', [TenantController::class, 'restore'])->name('v1.tenant.restore'); // 테넌트 복구 + Route::post('/logo', [TenantController::class, 'uploadLogo'])->name('v1.tenant.upload-logo'); // 로고 업로드 +}); + +// Tenant Statistics Field API +Route::prefix('tenant-stat-fields')->group(function () { + Route::get('/', [TenantStatFieldController::class, 'index'])->name('v1.tenant-stat-fields.index'); // 목록 조회 + Route::post('/', [TenantStatFieldController::class, 'store'])->name('v1.tenant-stat-fields.store'); // 생성 + Route::get('/{id}', [TenantStatFieldController::class, 'show'])->name('v1.tenant-stat-fields.show'); // 단건 조회 + Route::patch('/{id}', [TenantStatFieldController::class, 'update'])->name('v1.tenant-stat-fields.update'); // 수정 + Route::delete('/{id}', [TenantStatFieldController::class, 'destroy'])->name('v1.tenant-stat-fields.destroy'); // 삭제 + Route::post('/reorder', [TenantStatFieldController::class, 'reorder'])->name('v1.tenant-stat-fields.reorder'); // 정렬 변경 + Route::put('/bulk-upsert', [TenantStatFieldController::class, 'bulkUpsert'])->name('v1.tenant-stat-fields.bulk-upsert'); // 일괄 저장 +}); + +// Tenant Settings API (테넌트별 설정) +Route::prefix('tenant-settings')->group(function () { + Route::get('/', [TenantSettingController::class, 'index'])->name('v1.tenant-settings.index'); // 전체 설정 조회 + Route::post('/', [TenantSettingController::class, 'store'])->name('v1.tenant-settings.store'); // 설정 저장 + Route::put('/bulk', [TenantSettingController::class, 'bulkUpdate'])->name('v1.tenant-settings.bulk'); // 일괄 저장 + Route::post('/initialize', [TenantSettingController::class, 'initialize'])->name('v1.tenant-settings.initialize'); // 기본 설정 초기화 + Route::get('/{group}/{key}', [TenantSettingController::class, 'show'])->name('v1.tenant-settings.show'); // 단일 설정 조회 + Route::delete('/{group}/{key}', [TenantSettingController::class, 'destroy'])->name('v1.tenant-settings.destroy'); // 설정 삭제 +}); diff --git a/routes/api/v1/users.php b/routes/api/v1/users.php new file mode 100644 index 0000000..ccabd31 --- /dev/null +++ b/routes/api/v1/users.php @@ -0,0 +1,75 @@ +group(function () { + Route::get('index', [UserController::class, 'index'])->name('v1.users.index'); // 회원 목록 조회 + Route::get('show/{user_no}', [UserController::class, 'show'])->name('v1.users.show'); // 회원 상세 조회 + + Route::get('me', [UserController::class, 'me'])->name('v1.users.users.me'); // 내 정보 조회 + Route::put('me', [UserController::class, 'meUpdate'])->name('v1.users.me.update'); // 내 정보 수정 + Route::put('me/password', [UserController::class, 'changePassword'])->name('v1.users.me.password'); // 비밀번호 변겅 + + Route::get('me/tenants', [UserController::class, 'tenants'])->name('v1.users.me.tenants.index'); // 내 테넌트 목록 + Route::patch('me/tenants/switch', [UserController::class, 'switchTenant'])->name('v1.users.me.tenants.switch'); // 활성 테넌트 전환 + + // 사용자 초대 API + Route::get('invitations', [UserInvitationController::class, 'index'])->name('v1.users.invitations.index'); // 초대 목록 + Route::post('invite', [UserInvitationController::class, 'invite'])->name('v1.users.invite'); // 초대 발송 + Route::post('invitations/{token}/accept', [UserInvitationController::class, 'accept'])->name('v1.users.invitations.accept'); // 초대 수락 + Route::delete('invitations/{id}', [UserInvitationController::class, 'cancel'])->whereNumber('id')->name('v1.users.invitations.cancel'); // 초대 취소 + Route::post('invitations/{id}/resend', [UserInvitationController::class, 'resend'])->whereNumber('id')->name('v1.users.invitations.resend'); // 초대 재발송 + + // 알림 설정 API (auth:sanctum 필수) + Route::middleware('auth:sanctum')->group(function () { + Route::get('me/notification-settings', [NotificationSettingController::class, 'index'])->name('v1.users.me.notification-settings.index'); // 알림 설정 조회 + Route::put('me/notification-settings', [NotificationSettingController::class, 'update'])->name('v1.users.me.notification-settings.update'); // 알림 설정 수정 + Route::put('me/notification-settings/bulk', [NotificationSettingController::class, 'bulkUpdate'])->name('v1.users.me.notification-settings.bulk'); // 알림 일괄 설정 + }); +}); + +// User Role API +Route::prefix('users/{id}/roles')->group(function () { + Route::get('/', [UserRoleController::class, 'index'])->name('v1.users.roles.index'); // list + Route::post('/', [UserRoleController::class, 'grant'])->name('v1.users.roles.grant'); // grant + Route::delete('/', [UserRoleController::class, 'revoke'])->name('v1.users.roles.revoke'); // revoke + Route::put('/sync', [UserRoleController::class, 'sync'])->name('v1.users.roles.sync'); // sync +}); + +// Account API (계정 관리 - 탈퇴, 사용중지, 약관동의) +Route::prefix('account')->middleware('auth:sanctum')->group(function () { + Route::post('withdraw', [AccountController::class, 'withdraw'])->name('v1.account.withdraw'); // 회원 탈퇴 + Route::post('suspend', [AccountController::class, 'suspend'])->name('v1.account.suspend'); // 사용 중지 (테넌트) + Route::get('agreements', [AccountController::class, 'getAgreements'])->name('v1.account.agreements.index'); // 약관 동의 조회 + Route::put('agreements', [AccountController::class, 'updateAgreements'])->name('v1.account.agreements.update'); // 약관 동의 수정 +}); + +// 회원 프로필(테넌트 기준) +Route::prefix('profiles')->group(function () { + Route::get('', [TenantUserProfileController::class, 'index'])->name('v1.profiles.index'); // 프로필 목록(테넌트 기준) + // /me 라우트는 /{userId} 와일드카드보다 먼저 정의해야 함 + // auth:sanctum 미들웨어로 Bearer 토큰 인증 필요 + Route::middleware('auth:sanctum')->group(function () { + Route::get('/me', [TenantUserProfileController::class, 'me'])->name('v1.profiles.me'); // 내 프로필 조회 + Route::patch('/me', [TenantUserProfileController::class, 'updateMe'])->name('v1.profiles.me.update'); // 내 프로필 수정 + }); + Route::get('/{userId}', [TenantUserProfileController::class, 'show'])->name('v1.profiles.show'); // 특정 사용자 프로필 조회 + Route::patch('/{userId}', [TenantUserProfileController::class, 'update'])->name('v1.profiles.update'); // 특정 사용자 프로필 수정(관리자) +});