diff --git a/app/Http/Controllers/Api/V1/BomController.php b/app/Http/Controllers/Api/V1/BomController.php deleted file mode 100644 index c7bdbba..0000000 --- a/app/Http/Controllers/Api/V1/BomController.php +++ /dev/null @@ -1,51 +0,0 @@ -morphedByMany(Material::class, 'taggable'); } - /** - * BOM(Bill of Materials)와 연결 (N:M, 폴리모픽) - */ - public function boms(): MorphToMany - { - return $this->morphedByMany(Bom::class, 'taggable'); - } } diff --git a/app/Models/Products/Bom.php b/app/Models/Products/Bom.php deleted file mode 100644 index 75745f4..0000000 --- a/app/Models/Products/Bom.php +++ /dev/null @@ -1,42 +0,0 @@ -belongsTo(Product::class); - } - public function category() { - return $this->belongsTo(CommonCode::class, 'category_id'); - } - public function items() { - return $this->hasMany(BomItem::class); - } - public function image() { - return $this->belongsTo(File::class, 'image_file_id'); - } - - // 파일 목록 (N:M, 폴리모픽) - public function files() - { - return $this->morphMany(File::class, 'fileable'); - } - - // 태그 목록 (N:M, 폴리모픽) - public function tags() - { - return $this->morphToMany(Tag::class, 'taggable'); - } -} diff --git a/app/Models/Products/BomItem.php b/app/Models/Products/BomItem.php deleted file mode 100644 index 2da83ef..0000000 --- a/app/Models/Products/BomItem.php +++ /dev/null @@ -1,25 +0,0 @@ -belongsTo(Bom::class); - } - public function parent() { - return $this->belongsTo(self::class, 'parent_id'); - } - public function children() { - return $this->hasMany(self::class, 'parent_id'); - } -} diff --git a/app/Services/BomService.php b/app/Services/BomService.php deleted file mode 100644 index 1159e73..0000000 --- a/app/Services/BomService.php +++ /dev/null @@ -1,44 +0,0 @@ -get(); - } - - public static function setBom() - { - $query = DB::table('COM_CODE') - ->select(['CODE_TP_ID', 'CODE_ID', 'CODE_VAL', 'CODE_DESC', 'USE_YN']); - return $query->get(); - } - - public static function getBom(int $id) - { - $query = Bom::find($id); - return $query->get(); - } - - public static function updateBom(int $id) - { - $query = DB::table('COM_CODE') - ->select(['CODE_TP_ID', 'CODE_ID', 'CODE_VAL', 'CODE_DESC', 'USE_YN']); - return $query->get(); - } - - public static function destoryBom(int $id) - { - $query = DB::table('COM_CODE') - ->select(['CODE_TP_ID', 'CODE_ID', 'CODE_VAL', 'CODE_DESC', 'USE_YN']); - return $query->get(); - } - -} diff --git a/app/Services/ModelService.php b/app/Services/ModelService.php deleted file mode 100644 index 1c5fa01..0000000 --- a/app/Services/ModelService.php +++ /dev/null @@ -1,44 +0,0 @@ -get(); - } - - public static function setModel() - { - $query = DB::table('COM_CODE') - ->select(['CODE_TP_ID', 'CODE_ID', 'CODE_VAL', 'CODE_DESC', 'USE_YN']); - return $query->get(); - } - - public static function getModel(int $id) - { - $query = Bom::find($id); - return $query->get(); - } - - public static function updateModel(int $id) - { - $query = DB::table('COM_CODE') - ->select(['CODE_TP_ID', 'CODE_ID', 'CODE_VAL', 'CODE_DESC', 'USE_YN']); - return $query->get(); - } - - public static function destoryModel(int $id) - { - $query = DB::table('COM_CODE') - ->select(['CODE_TP_ID', 'CODE_ID', 'CODE_VAL', 'CODE_DESC', 'USE_YN']); - return $query->get(); - } - -} diff --git a/lang/en/error.php b/lang/en/error.php index 8450b5f..7463ffd 100644 --- a/lang/en/error.php +++ b/lang/en/error.php @@ -32,4 +32,43 @@ // Server errors 'server_error' => 'An internal server error occurred.', // 5xx + // Estimate related errors + 'estimate' => [ + 'cannot_delete_sent_or_approved' => 'Cannot delete estimates that have been sent or approved.', + 'invalid_status_transition' => 'Cannot change status from the current state.', + ], + + // BOM template related + 'bom_template' => [ + 'not_found' => 'No applicable BOM template found.', + ], + + // Model set related + 'modelset' => [ + 'has_dependencies' => 'Cannot delete due to associated products or subcategories.', + ], + + // Settings management related + 'settings' => [ + 'field_not_found' => 'Field setting not found.', + 'option_group_not_found' => 'Option group not found.', + 'common_code_duplicate' => 'Duplicate common code exists.', + 'invalid_field_type' => 'Invalid field type.', + ], + + // Materials management related + 'materials' => [ + 'not_found' => 'Material information not found.', + 'duplicate_code' => 'Duplicate material code.', + 'in_use_cannot_delete' => 'Cannot delete material that is currently in use.', + ], + + // File management related + 'file' => [ + 'not_found' => 'File not found.', + 'upload_failed' => 'File upload failed.', + 'invalid_file_type' => 'Invalid file type.', + 'file_too_large' => 'File size is too large.', + ], + ]; diff --git a/lang/en/message.php b/lang/en/message.php index b9fc735..08fb52d 100644 --- a/lang/en/message.php +++ b/lang/en/message.php @@ -38,6 +38,11 @@ 'fetched' => 'BOM items have been fetched.', 'bulk_upsert' => 'BOM items have been saved.', 'reordered' => 'BOM order has been updated.', + 'fetch' => 'BOM item fetch', + 'create' => 'BOM item created', + 'update' => 'BOM item updated', + 'delete' => 'BOM item deleted', + 'restore' => 'BOM item restored', ], 'category' => [ @@ -50,4 +55,41 @@ 'template_cloned' => 'BOM template has been cloned.', 'template_diff' => 'BOM template differences have been computed.', ], + + 'model_set' => [ + 'cloned' => 'Model set cloned successfully.', + 'calculated' => 'BOM calculation completed.', + ], + + 'estimate' => [ + 'cloned' => 'Estimate cloned successfully.', + 'status_changed' => 'Estimate status updated.', + ], + + // Calculation related + 'calculated' => 'Calculation completed', + + // Settings & Configuration Management + 'settings' => [ + 'fields_updated' => 'Field settings have been updated.', + 'fields_bulk_saved' => 'Field settings bulk save completed.', + 'options_saved' => 'Option group has been saved.', + 'options_reordered' => 'Option values reordered successfully.', + 'common_code_saved' => 'Common code has been saved.', + ], + + // Materials Management (Products & Materials integrated) + 'materials' => [ + 'created' => 'Material has been created.', + 'updated' => 'Material has been updated.', + 'deleted' => 'Material has been deleted.', + 'fetched' => 'Materials list retrieved successfully.', + ], + + // File Management + 'file' => [ + 'uploaded' => 'File has been uploaded.', + 'deleted' => 'File has been deleted.', + 'fetched' => 'File list retrieved successfully.', + ], ]; diff --git a/lang/ko/error.php b/lang/ko/error.php index 18532d3..24c3241 100644 --- a/lang/ko/error.php +++ b/lang/ko/error.php @@ -44,4 +44,27 @@ 'modelset' => [ 'has_dependencies' => '연관된 제품 또는 하위 카테고리가 있어 삭제할 수 없습니다.', ], + + // 설정 관리 관련 + 'settings' => [ + 'field_not_found' => '해당 필드 설정을 찾을 수 없습니다.', + 'option_group_not_found' => '해당 옵션 그룹을 찾을 수 없습니다.', + 'common_code_duplicate' => '중복된 공통 코드가 존재합니다.', + 'invalid_field_type' => '유효하지 않은 필드 타입입니다.', + ], + + // 자재 관리 관련 + 'materials' => [ + 'not_found' => '자재 정보를 찾을 수 없습니다.', + 'duplicate_code' => '중복된 자재 코드입니다.', + 'in_use_cannot_delete' => '사용 중인 자재는 삭제할 수 없습니다.', + ], + + // 파일 관리 관련 + 'file' => [ + 'not_found' => '파일을 찾을 수 없습니다.', + 'upload_failed' => '파일 업로드에 실패했습니다.', + 'invalid_file_type' => '허용되지 않는 파일 형식입니다.', + 'file_too_large' => '파일 크기가 너무 큽니다.', + ], ]; diff --git a/lang/ko/message.php b/lang/ko/message.php index 247e03e..858e71e 100644 --- a/lang/ko/message.php +++ b/lang/ko/message.php @@ -39,7 +39,7 @@ 'bulk_upsert' => 'BOM 항목이 저장되었습니다.', 'reordered' => 'BOM 정렬이 변경되었습니다.', 'fetch' => 'BOM 항목 조회', - 'creat' => 'BOM 항목 등록', + 'create' => 'BOM 항목 등록', 'update' => 'BOM 항목 수정', 'delete' => 'BOM 항목 삭제', 'restore' => 'BOM 항목 복구', @@ -68,4 +68,28 @@ // 계산 관련 'calculated' => '계산 완료', + + // 설정 관리 (Settings & Configuration 통합) + 'settings' => [ + 'fields_updated' => '필드 설정이 업데이트되었습니다.', + 'fields_bulk_saved' => '필드 설정 일괄 저장이 완료되었습니다.', + 'options_saved' => '옵션 그룹이 저장되었습니다.', + 'options_reordered' => '옵션 값 정렬이 변경되었습니다.', + 'common_code_saved' => '공통 코드가 저장되었습니다.', + ], + + // 자재 관리 (Products & Materials 통합) + 'materials' => [ + 'created' => '자재가 등록되었습니다.', + 'updated' => '자재가 수정되었습니다.', + 'deleted' => '자재가 삭제되었습니다.', + 'fetched' => '자재 목록을 조회했습니다.', + ], + + // 파일 관리 + 'file' => [ + 'uploaded' => '파일이 업로드되었습니다.', + 'deleted' => '파일이 삭제되었습니다.', + 'fetched' => '파일 목록을 조회했습니다.', + ], ]; diff --git a/routes/api.php b/routes/api.php index 75e2607..0832592 100644 --- a/routes/api.php +++ b/routes/api.php @@ -9,8 +9,6 @@ use App\Http\Controllers\Api\V1\FileController; use App\Http\Controllers\Api\V1\ProductController; use App\Http\Controllers\Api\V1\MaterialController; -use App\Http\Controllers\Api\V1\ModelController; -use App\Http\Controllers\Api\V1\BomController; use App\Http\Controllers\Api\V1\UserController; use App\Http\Controllers\Api\V1\TenantController; use App\Http\Controllers\Api\V1\AdminController; @@ -41,11 +39,6 @@ use App\Http\Controllers\Api\V1\ModelSetController; use App\Http\Controllers\Api\V1\EstimateController; -// error test -Route::get('/test-error', function () { - throw new \Exception('슬랙 전송 테스트 예외'); -}); - // V1 초기 개발 Route::prefix('v1')->group(function () { @@ -61,16 +54,6 @@ Route::post('signup', [ApiController::class, 'signup'])->name('v1.users.signup'); - // Common API - Route::prefix('common')->group(function () { - Route::get('code', [CommonController::class, 'getComeCode'])->name('v1.common.code'); // 공통코드 조회 - }); - - - // Product API - Route::prefix('product')->group(function () { - Route::get('category', [ProductController::class, 'getCategory'])->name('v1.product.category'); // 제품 카테고리 - }); // Tenant Admin API @@ -132,12 +115,6 @@ }); - // Material, Model, BOM API - Route::resource('materials', MaterialController::class)->except(['create', 'edit']); // 자재관리 - Route::resource('models', ModelController::class)->except(['v1.create', 'edit']); // 모델관리 - Route::resource('boms', BomController::class)->except(['create', 'edit']); // BOM관리 - - // Menu API Route::middleware(['perm.map', 'permission'])->prefix('menus')->group(function () { Route::get('/', [MenuController::class, 'index'])->name('v1.menus.index'); @@ -180,53 +157,61 @@ // Department API Route::prefix('departments')->group(function () { - Route::get('', [DepartmentController::class, 'index'])->name('departments.index'); // 목록 - Route::post('', [DepartmentController::class, 'store'])->name('departments.store'); // 생성 - Route::get('/{id}', [DepartmentController::class, 'show'])->name('departments.show'); // 단건 - Route::patch('/{id}', [DepartmentController::class, 'update'])->name('departments.update'); // 수정 - Route::delete('/{id}', [DepartmentController::class, 'destroy'])->name('departments.destroy'); // 삭제(soft) + Route::get('', [DepartmentController::class, 'index'])->name('v1.departments.index'); // 목록 + Route::post('', [DepartmentController::class, 'store'])->name('v1.departments.store'); // 생성 + 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('departments.users.index'); // 부서 사용자 목록 - Route::post('/{id}/users', [DepartmentController::class, 'attachUser'])->name('departments.users.attach'); // 사용자 배정(주/부서) - Route::delete('/{id}/users/{user}', [DepartmentController::class, 'detachUser'])->name('departments.users.detach'); // 사용자 제거 - Route::patch('/{id}/users/{user}/primary', [DepartmentController::class, 'setPrimary'])->name('departments.users.primary'); // 주부서 설정/해제 + 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('departments.permissions.index'); // 권한 목록 - Route::post('/{id}/permissions', [DepartmentController::class, 'upsertPermissions'])->name('departments.permissions.upsert'); // 권한 부여/차단(메뉴별 가능) - Route::delete('/{id}/permissions/{permission}', [DepartmentController::class, 'revokePermissions'])->name('departments.permissions.revoke'); // 권한 제거(해당 메뉴 범위까지) + 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'); // 권한 제거(해당 메뉴 범위까지) }); // Permission API Route::prefix('permissions')->group(function () { - Route::get('departments/{dept_id}/menu-matrix', [PermissionController::class, 'deptMenuMatrix'])->name('permissions.deptMenuMatrix'); // 부서별 권한 메트릭스 - Route::get('roles/{role_id}/menu-matrix', [PermissionController::class, 'roleMenuMatrix'])->name('permissions.roleMenuMatrix'); // 부서별 권한 메트릭스 - Route::get('users/{user_id}/menu-matrix', [PermissionController::class, 'userMenuMatrix'])->name('permissions.userMenuMatrix'); // 부서별 권한 메트릭스 + 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'); // 부서별 권한 메트릭스 }); - // 테넌트 필드 설정 - Route::prefix('fields')->group(function () { - Route::get('', [TenantFieldSettingController::class, 'index'])->name('v1.fields.index'); // 필드 설정 목록(전역+테넌트 병합 효과값) - Route::put('/bulk', [TenantFieldSettingController::class, 'bulkUpsert'])->name('v1.fields.bulk'); // 필드 설정 대량 저장(트랜잭션 처리) - Route::patch('/{key}', [TenantFieldSettingController::class, 'updateOne'])->name('v1.fields.update'); // 필드 설정 단건 수정/업데이트 - }); + // Settings & Configuration (설정 및 환경설정 통합 관리) + Route::prefix('settings')->group(function () { - // 옵션 그룹/값 - Route::prefix('opt-groups')->group(function () { - Route::get('', [TenantOptionGroupController::class, 'index'])->name('v1.opt-groups.index'); // 옵션 그룹 목록 - Route::post('', [TenantOptionGroupController::class, 'store'])->name('v1.opt-groups.store'); // 옵션 그룹 생성 - Route::get('/{id}', [TenantOptionGroupController::class, 'show'])->name('v1.opt-groups.show'); // 옵션 그룹 단건 조회 - Route::patch('/{id}', [TenantOptionGroupController::class, 'update'])->name('v1.opt-groups.update'); // 옵션 그룹 수정 - Route::delete('/{id}', [TenantOptionGroupController::class, 'destroy'])->name('v1.opt-groups.destroy'); // 옵션 그룹 삭제 - Route::get('/{gid}/values', [TenantOptionValueController::class, 'index'])->name('v1.opt-groups.values.index'); // 옵션 값 목록 - Route::post('/{gid}/values', [TenantOptionValueController::class, 'store'])->name('v1.opt-groups.values.store'); // 옵션 값 생성 - Route::get('/{gid}/values/{id}', [TenantOptionValueController::class, 'show'])->name('v1.opt-groups.values.show'); // 옵션 값 단건 조회 - Route::patch('/{gid}/values/{id}', [TenantOptionValueController::class, 'update'])->name('v1.opt-groups.values.update'); // 옵션 값 수정 - Route::delete('/{gid}/values/{id}', [TenantOptionValueController::class, 'destroy'])->name('v1.opt-groups.values.destroy'); // 옵션 값 삭제 - Route::patch('/{gid}/values/reorder', [TenantOptionValueController::class, 'reorder'])->name('v1.opt-groups.values.reorder'); // 옵션 값 정렬순서 재배치 + // 테넌트 필드 설정 (기존 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'); // 공통 코드 삭제 }); // 회원 프로필(테넌트 기준) @@ -238,61 +223,49 @@ Route::patch('/me', [TenantUserProfileController::class, 'updateMe'])->name('v1.profiles.me.update'); // 내 프로필 수정 }); - // Category API + // 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'); // 정렬 일괄 - Route::post('/{id}/toggle', [CategoryController::class, 'toggle'])->name('v1.categories.toggle'); // 활성 토글 - Route::patch('/{id}/move', [CategoryController::class, 'move'])->name('v1.categories.move'); // 부모/순서 이동 - - // 기본 + // === 기본 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) - }); - // Category Field 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'); // 정렬 일괄 + 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 Template API - Route::prefix('categories')->group(function () { + // === 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 Log API - Route::prefix('categories')->group(function () { + // === 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'); // (선택) 특정 변경 시점으로 카테고리 복구(템플릿/필드와 별개) @@ -308,9 +281,12 @@ Route::delete('/{id}', [ClassificationController::class, 'destroy'])->whereNumber('id')->name('v1.classifications.destroy'); // 삭제 }); - // Products (모델/부품/서브어셈블리) + // Products & Materials (제품/자재 통합 관리) Route::prefix('products')->group(function (){ + // 제품 카테고리 (기존 product/category에서 이동) + Route::get ('/categories', [ProductController::class, 'getCategory'])->name('v1.products.categories'); // 제품 카테고리 + // (선택) 드롭다운/모달용 간편 검색 & 활성 토글 Route::get ('/search', [ProductController::class, 'search'])->name('v1.products.search'); Route::post ('/{id}/toggle', [ProductController::class, 'toggle'])->name('v1.products.toggle'); @@ -324,6 +300,13 @@ // BOM 카테고리 Route::get('bom/categories', [ProductBomItemController::class, 'suggestCategories'])->name('v1.products.bom.categories.suggest'); // 전역(테넌트) 추천 Route::get('{id}/bom/categories', [ProductBomItemController::class, 'listCategories'])->name('v1.products.bom.categories'); // 해당 제품에서 사용 중 + + // 자재 관리 (기존 독립 materials에서 이동) + Route::get ('/materials', [MaterialController::class, 'index'])->name('v1.products.materials.index'); // 자재 목록 + Route::post ('/materials', [MaterialController::class, 'store'])->name('v1.products.materials.store'); // 자재 생성 + Route::get ('/materials/{id}', [MaterialController::class, 'show'])->name('v1.products.materials.show'); // 자재 단건 + Route::patch ('/materials/{id}', [MaterialController::class, 'update'])->name('v1.products.materials.update'); // 자재 수정 + Route::delete('/materials/{id}', [MaterialController::class, 'destroy'])->name('v1.products.materials.destroy'); // 자재 삭제 }); // BOM (product_components: ref_type=PRODUCT|MATERIAL)