2025-07-17 10:05:47 +09:00
|
|
|
<?php
|
|
|
|
|
|
feat: [quote] 견적 API Phase 2-3 완료 (Service + Controller Layer)
Phase 2 - Service Layer:
- QuoteService: 견적 CRUD + 상태관리 (확정/전환)
- QuoteNumberService: 견적번호 채번 (KD-{PREFIX}-YYMMDD-SEQ)
- FormulaEvaluatorService: 수식 평가 엔진 (SUM, IF, ROUND 등)
- QuoteCalculationService: 자동산출 (스크린/철재 제품)
- QuoteDocumentService: PDF 생성 및 이메일/카카오 발송
Phase 3 - Controller Layer:
- QuoteController: 16개 엔드포인트
- FormRequest 7개: Index, Store, Update, BulkDelete, Calculate, SendEmail, SendKakao
- QuoteApi.php: Swagger 문서 (12개 스키마, 16개 엔드포인트)
- routes/api.php: 16개 라우트 등록
i18n 키 추가:
- error.php: quote_not_found, formula_* 등
- message.php: quote.* 성공 메시지
2025-12-04 22:03:40 +09:00
|
|
|
use App\Http\Controllers\Api\Admin\GlobalMenuController;
|
2025-11-06 17:24:42 +09:00
|
|
|
use App\Http\Controllers\Api\V1\AdminController;
|
|
|
|
|
use App\Http\Controllers\Api\V1\ApiController;
|
2025-12-09 21:51:46 +09:00
|
|
|
use App\Http\Controllers\Api\V1\AttendanceController;
|
2025-11-30 21:06:22 +09:00
|
|
|
use App\Http\Controllers\Api\V1\BoardController;
|
2025-11-06 17:24:42 +09:00
|
|
|
use App\Http\Controllers\Api\V1\CategoryController;
|
|
|
|
|
use App\Http\Controllers\Api\V1\CategoryFieldController;
|
2025-08-25 17:46:34 +09:00
|
|
|
use App\Http\Controllers\Api\V1\CategoryLogController;
|
2025-11-06 17:24:42 +09:00
|
|
|
use App\Http\Controllers\Api\V1\CategoryTemplateController;
|
|
|
|
|
use App\Http\Controllers\Api\V1\ClassificationController;
|
|
|
|
|
use App\Http\Controllers\Api\V1\ClientController;
|
|
|
|
|
use App\Http\Controllers\Api\V1\ClientGroupController;
|
2025-07-18 11:37:07 +09:00
|
|
|
use App\Http\Controllers\Api\V1\CommonController;
|
2025-11-06 17:24:42 +09:00
|
|
|
use App\Http\Controllers\Api\V1\DepartmentController;
|
|
|
|
|
use App\Http\Controllers\Api\V1\Design\AuditLogController as DesignAuditLogController;
|
|
|
|
|
use App\Http\Controllers\Api\V1\Design\BomCalculationController;
|
|
|
|
|
use App\Http\Controllers\Api\V1\Design\BomTemplateController as DesignBomTemplateController;
|
|
|
|
|
use App\Http\Controllers\Api\V1\Design\DesignModelController;
|
|
|
|
|
use App\Http\Controllers\Api\V1\Design\ModelVersionController as DesignModelVersionController;
|
2025-12-09 21:51:46 +09:00
|
|
|
use App\Http\Controllers\Api\V1\EmployeeController;
|
2025-11-06 17:24:42 +09:00
|
|
|
use App\Http\Controllers\Api\V1\EstimateController;
|
feat: 파일 저장 시스템 DB 마이그레이션
- enhance_files_table: 이중 파일명 시스템 (display_name/stored_name), 폴더 관리, 문서 연결 지원
- create_folders_table: 동적 폴더 관리 시스템 (tenant별 커스터마이징 가능)
- 5개 stub 마이그레이션 생성 (file_share_links, file_deletion_logs, storage_usage_history, add_storage_columns_to_tenants)
- FolderSeeder stub 생성
- CURRENT_WORKS.md에 Phase 1 진행상황 문서화
fix: 파일 공유 및 삭제 기능 버그 수정
- ShareLinkRequest: PATH 파라미터 {id}를 file_id로 자동 병합
- routes/api.php: 공유 링크 다운로드를 auth.apikey 그룹 밖으로 이동 (인증 불필요)
- FileShareLink: File, Tenant 클래스 import 추가
- File 모델: softDeleteFile()에서 SoftDeletes의 delete() 메서드 사용
- FileStorageService: getTrash(), restoreFile(), permanentDelete()에서 onlyTrashed() 사용
- File 모델: Tenant 네임스페이스 수정 (App\Models\Tenants\Tenant)
refactor: Swagger 문서 정리 - File 태그를 Files로 통합
- FileApi.php의 모든 태그를 Files로 변경
- 구 파일 시스템 라우트 삭제 (prefix 'file')
- 구 FileController.php 삭제
- 신규 파일 저장소 시스템으로 완전 통합
fix: 모든 legacy 파일 컬럼 nullable 일괄 처리
- 5개 legacy 컬럼을 한 번에 nullable로 변경
* original_name, file_name, file_name_old (string)
* fileable_id, fileable_type (polymorphic)
- foreach 루프로 반복 작업 자동화
- 신규/기존 시스템 간 완전한 하위 호환성 확보
fix: legacy 파일 컬럼 nullable 처리 완료
- file_name, file_name_old 컬럼도 nullable로 변경
- 기존 시스템과 신규 시스템 간 완전한 하위 호환성 확보
- Legacy: original_name, file_name, file_name_old (nullable)
- New: display_name, stored_name (required)
fix: original_name 컬럼 nullable 처리
- original_name을 nullable로 변경하여 하위 호환성 유지
- 새 시스템에서는 display_name 사용, 기존 시스템은 original_name 사용 가능
fix: 파일 업로드 DB 컬럼 누락 및 메시지 구조 개선
- files 테이블에 감사 컬럼 추가 (created_by, updated_by, uploaded_by)
- ApiResponse::handle() 메시지 로직 개선 (접미사 제거)
- 다국어 지원을 위한 완성된 문장 구조 유지
- FileUploadRequest 파일 검증 규칙 수정
fix: 파일 저장소 버그 수정 및 신규 테넌트 폴더 자동 생성
- FolderSeeder 네임스페이스 수정 (App\Models\Tenant → App\Models\Tenants\Tenant)
- FileStorageController use 문 구문 오류 수정 (/ → \)
- TenantObserver에 신규 테넌트 기본 폴더 자동 생성 로직 추가
- 5개 기본 폴더 (생산관리, 품질관리, 회계, 인사, 일반)
- 에러 처리 및 로깅
- 회원가입 시 자동 실행
2025-11-10 19:08:56 +09:00
|
|
|
use App\Http\Controllers\Api\V1\FileStorageController;
|
|
|
|
|
use App\Http\Controllers\Api\V1\FolderController;
|
2025-11-20 17:16:03 +09:00
|
|
|
use App\Http\Controllers\Api\V1\ItemMaster\CustomTabController;
|
2025-11-26 14:09:31 +09:00
|
|
|
use App\Http\Controllers\Api\V1\ItemMaster\EntityRelationshipController;
|
2025-11-20 17:07:40 +09:00
|
|
|
use App\Http\Controllers\Api\V1\ItemMaster\ItemBomItemController;
|
2025-11-20 16:55:57 +09:00
|
|
|
use App\Http\Controllers\Api\V1\ItemMaster\ItemFieldController;
|
|
|
|
|
use App\Http\Controllers\Api\V1\ItemMaster\ItemMasterController;
|
|
|
|
|
use App\Http\Controllers\Api\V1\ItemMaster\ItemPageController;
|
|
|
|
|
use App\Http\Controllers\Api\V1\ItemMaster\ItemSectionController;
|
2025-11-20 17:07:40 +09:00
|
|
|
use App\Http\Controllers\Api\V1\ItemMaster\SectionTemplateController;
|
2025-11-20 17:16:03 +09:00
|
|
|
use App\Http\Controllers\Api\V1\ItemMaster\UnitOptionController;
|
2025-12-09 20:27:44 +09:00
|
|
|
use App\Http\Controllers\Api\V1\ItemsBomController;
|
|
|
|
|
use App\Http\Controllers\Api\V1\ItemsController;
|
|
|
|
|
use App\Http\Controllers\Api\V1\ItemsFileController;
|
2025-08-01 17:25:31 +09:00
|
|
|
use App\Http\Controllers\Api\V1\MaterialController;
|
2025-08-16 03:25:06 +09:00
|
|
|
use App\Http\Controllers\Api\V1\MenuController;
|
2025-11-06 17:24:42 +09:00
|
|
|
use App\Http\Controllers\Api\V1\ModelSetController;
|
|
|
|
|
use App\Http\Controllers\Api\V1\PermissionController;
|
2025-11-30 21:06:22 +09:00
|
|
|
use App\Http\Controllers\Api\V1\PostController;
|
2025-11-06 17:24:42 +09:00
|
|
|
use App\Http\Controllers\Api\V1\PricingController;
|
|
|
|
|
use App\Http\Controllers\Api\V1\ProductBomItemController;
|
|
|
|
|
use App\Http\Controllers\Api\V1\ProductController;
|
feat: [quote] 견적 API Phase 2-3 완료 (Service + Controller Layer)
Phase 2 - Service Layer:
- QuoteService: 견적 CRUD + 상태관리 (확정/전환)
- QuoteNumberService: 견적번호 채번 (KD-{PREFIX}-YYMMDD-SEQ)
- FormulaEvaluatorService: 수식 평가 엔진 (SUM, IF, ROUND 등)
- QuoteCalculationService: 자동산출 (스크린/철재 제품)
- QuoteDocumentService: PDF 생성 및 이메일/카카오 발송
Phase 3 - Controller Layer:
- QuoteController: 16개 엔드포인트
- FormRequest 7개: Index, Store, Update, BulkDelete, Calculate, SendEmail, SendKakao
- QuoteApi.php: Swagger 문서 (12개 스키마, 16개 엔드포인트)
- routes/api.php: 16개 라우트 등록
i18n 키 추가:
- error.php: quote_not_found, formula_* 등
- message.php: quote.* 성공 메시지
2025-12-04 22:03:40 +09:00
|
|
|
use App\Http\Controllers\Api\V1\QuoteController;
|
feat: 파일 저장 시스템 DB 마이그레이션
- enhance_files_table: 이중 파일명 시스템 (display_name/stored_name), 폴더 관리, 문서 연결 지원
- create_folders_table: 동적 폴더 관리 시스템 (tenant별 커스터마이징 가능)
- 5개 stub 마이그레이션 생성 (file_share_links, file_deletion_logs, storage_usage_history, add_storage_columns_to_tenants)
- FolderSeeder stub 생성
- CURRENT_WORKS.md에 Phase 1 진행상황 문서화
fix: 파일 공유 및 삭제 기능 버그 수정
- ShareLinkRequest: PATH 파라미터 {id}를 file_id로 자동 병합
- routes/api.php: 공유 링크 다운로드를 auth.apikey 그룹 밖으로 이동 (인증 불필요)
- FileShareLink: File, Tenant 클래스 import 추가
- File 모델: softDeleteFile()에서 SoftDeletes의 delete() 메서드 사용
- FileStorageService: getTrash(), restoreFile(), permanentDelete()에서 onlyTrashed() 사용
- File 모델: Tenant 네임스페이스 수정 (App\Models\Tenants\Tenant)
refactor: Swagger 문서 정리 - File 태그를 Files로 통합
- FileApi.php의 모든 태그를 Files로 변경
- 구 파일 시스템 라우트 삭제 (prefix 'file')
- 구 FileController.php 삭제
- 신규 파일 저장소 시스템으로 완전 통합
fix: 모든 legacy 파일 컬럼 nullable 일괄 처리
- 5개 legacy 컬럼을 한 번에 nullable로 변경
* original_name, file_name, file_name_old (string)
* fileable_id, fileable_type (polymorphic)
- foreach 루프로 반복 작업 자동화
- 신규/기존 시스템 간 완전한 하위 호환성 확보
fix: legacy 파일 컬럼 nullable 처리 완료
- file_name, file_name_old 컬럼도 nullable로 변경
- 기존 시스템과 신규 시스템 간 완전한 하위 호환성 확보
- Legacy: original_name, file_name, file_name_old (nullable)
- New: display_name, stored_name (required)
fix: original_name 컬럼 nullable 처리
- original_name을 nullable로 변경하여 하위 호환성 유지
- 새 시스템에서는 display_name 사용, 기존 시스템은 original_name 사용 가능
fix: 파일 업로드 DB 컬럼 누락 및 메시지 구조 개선
- files 테이블에 감사 컬럼 추가 (created_by, updated_by, uploaded_by)
- ApiResponse::handle() 메시지 로직 개선 (접미사 제거)
- 다국어 지원을 위한 완성된 문장 구조 유지
- FileUploadRequest 파일 검증 규칙 수정
fix: 파일 저장소 버그 수정 및 신규 테넌트 폴더 자동 생성
- FolderSeeder 네임스페이스 수정 (App\Models\Tenant → App\Models\Tenants\Tenant)
- FileStorageController use 문 구문 오류 수정 (/ → \)
- TenantObserver에 신규 테넌트 기본 폴더 자동 생성 로직 추가
- 5개 기본 폴더 (생산관리, 품질관리, 회계, 인사, 일반)
- 에러 처리 및 로깅
- 회원가입 시 자동 실행
2025-11-10 19:08:56 +09:00
|
|
|
use App\Http\Controllers\Api\V1\RefreshController;
|
2025-11-06 17:24:42 +09:00
|
|
|
use App\Http\Controllers\Api\V1\RegisterController;
|
2025-08-16 03:25:06 +09:00
|
|
|
use App\Http\Controllers\Api\V1\RoleController;
|
|
|
|
|
use App\Http\Controllers\Api\V1\RolePermissionController;
|
2025-11-06 17:24:42 +09:00
|
|
|
use App\Http\Controllers\Api\V1\TenantController;
|
2025-11-14 14:24:35 +09:00
|
|
|
// 설계 전용 (디자인 네임스페이스)
|
feat: [quote] 견적 API Phase 2-3 완료 (Service + Controller Layer)
Phase 2 - Service Layer:
- QuoteService: 견적 CRUD + 상태관리 (확정/전환)
- QuoteNumberService: 견적번호 채번 (KD-{PREFIX}-YYMMDD-SEQ)
- FormulaEvaluatorService: 수식 평가 엔진 (SUM, IF, ROUND 등)
- QuoteCalculationService: 자동산출 (스크린/철재 제품)
- QuoteDocumentService: PDF 생성 및 이메일/카카오 발송
Phase 3 - Controller Layer:
- QuoteController: 16개 엔드포인트
- FormRequest 7개: Index, Store, Update, BulkDelete, Calculate, SendEmail, SendKakao
- QuoteApi.php: Swagger 문서 (12개 스키마, 16개 엔드포인트)
- routes/api.php: 16개 라우트 등록
i18n 키 추가:
- error.php: quote_not_found, formula_* 등
- message.php: quote.* 성공 메시지
2025-12-04 22:03:40 +09:00
|
|
|
use App\Http\Controllers\Api\V1\TenantFieldSettingController;
|
2025-08-18 19:03:46 +09:00
|
|
|
use App\Http\Controllers\Api\V1\TenantOptionGroupController;
|
|
|
|
|
use App\Http\Controllers\Api\V1\TenantOptionValueController;
|
2025-11-14 14:24:35 +09:00
|
|
|
use App\Http\Controllers\Api\V1\TenantStatFieldController;
|
2025-08-18 19:03:46 +09:00
|
|
|
use App\Http\Controllers\Api\V1\TenantUserProfileController;
|
2025-09-24 17:41:26 +09:00
|
|
|
// 모델셋 관리 (견적 시스템)
|
feat: [quote] 견적 API Phase 2-3 완료 (Service + Controller Layer)
Phase 2 - Service Layer:
- QuoteService: 견적 CRUD + 상태관리 (확정/전환)
- QuoteNumberService: 견적번호 채번 (KD-{PREFIX}-YYMMDD-SEQ)
- FormulaEvaluatorService: 수식 평가 엔진 (SUM, IF, ROUND 등)
- QuoteCalculationService: 자동산출 (스크린/철재 제품)
- QuoteDocumentService: PDF 생성 및 이메일/카카오 발송
Phase 3 - Controller Layer:
- QuoteController: 16개 엔드포인트
- FormRequest 7개: Index, Store, Update, BulkDelete, Calculate, SendEmail, SendKakao
- QuoteApi.php: Swagger 문서 (12개 스키마, 16개 엔드포인트)
- routes/api.php: 16개 라우트 등록
i18n 키 추가:
- error.php: quote_not_found, formula_* 등
- message.php: quote.* 성공 메시지
2025-12-04 22:03:40 +09:00
|
|
|
use App\Http\Controllers\Api\V1\UserController;
|
2025-11-06 17:24:42 +09:00
|
|
|
use App\Http\Controllers\Api\V1\UserRoleController;
|
|
|
|
|
use Illuminate\Support\Facades\Route;
|
2025-09-24 17:41:26 +09:00
|
|
|
|
2025-07-18 11:37:07 +09:00
|
|
|
// V1 초기 개발
|
|
|
|
|
Route::prefix('v1')->group(function () {
|
2025-07-17 10:05:47 +09:00
|
|
|
|
2025-11-24 16:00:30 +09:00
|
|
|
// API KEY 인증 (글로벌 미들웨어로 이미 적용됨)
|
|
|
|
|
Route::get('/debug-apikey', [ApiController::class, 'debugApikey']);
|
2025-08-04 08:38:47 +09:00
|
|
|
|
2025-11-24 16:00:30 +09:00
|
|
|
// SAM API (글로벌 미들웨어로 이미 적용됨)
|
|
|
|
|
Route::group([], function () {
|
2025-07-17 10:05:47 +09:00
|
|
|
|
2025-11-06 17:24:42 +09:00
|
|
|
// Auth API
|
2025-08-18 16:37:02 +09:00
|
|
|
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');
|
2025-11-10 11:17:32 +09:00
|
|
|
Route::post('refresh', [RefreshController::class, 'refresh'])->name('v1.token.refresh');
|
2025-11-06 17:24:42 +09:00
|
|
|
Route::post('register', [RegisterController::class, 'register'])->name('v1.register');
|
2025-07-23 18:06:33 +09:00
|
|
|
|
2025-08-15 16:32:11 +09:00
|
|
|
// 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'); // 테넌트 사용자 비밀번호 초기화
|
2025-12-02 22:11:08 +09:00
|
|
|
|
|
|
|
|
// 글로벌 메뉴 관리 (시스템 관리자용)
|
|
|
|
|
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'); // 테넌트에 동기화
|
|
|
|
|
});
|
2025-08-15 16:32:11 +09:00
|
|
|
});
|
|
|
|
|
|
2025-07-18 11:37:07 +09:00
|
|
|
// Member API
|
2025-08-14 00:55:08 +09:00
|
|
|
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'); // 내 정보 조회
|
2025-08-18 16:37:02 +09:00
|
|
|
Route::put('me', [UserController::class, 'meUpdate'])->name('v1.users.me.update'); // 내 정보 수정
|
2025-08-14 00:55:08 +09:00
|
|
|
Route::put('me/password', [UserController::class, 'changePassword'])->name('v1.users.me.password'); // 비밀번호 변겅
|
|
|
|
|
|
2025-08-18 16:37:02 +09:00
|
|
|
Route::get('me/tenants', [UserController::class, 'tenants'])->name('v1.users.me.tenants.index'); // 내 테넌트 목록
|
2025-08-13 18:34:28 +09:00
|
|
|
Route::patch('me/tenants/switch', [UserController::class, 'switchTenant'])->name('v1.users.me.tenants.switch'); // 활성 테넌트 전환
|
2025-08-14 00:55:08 +09:00
|
|
|
});
|
2025-07-17 10:05:47 +09:00
|
|
|
|
2025-08-14 17:20:28 +09:00
|
|
|
// 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'); // 테넌트 복구
|
|
|
|
|
});
|
|
|
|
|
|
2025-11-14 14:09:53 +09:00
|
|
|
// 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'); // 일괄 저장
|
|
|
|
|
});
|
|
|
|
|
|
2025-08-16 03:25:06 +09:00
|
|
|
// Menu API
|
|
|
|
|
Route::middleware(['perm.map', 'permission'])->prefix('menus')->group(function () {
|
2025-08-22 18:08:57 +09:00
|
|
|
Route::get('/', [MenuController::class, 'index'])->name('v1.menus.index');
|
|
|
|
|
Route::post('/', [MenuController::class, 'store'])->name('v1.menus.store');
|
2025-12-02 22:11:08 +09:00
|
|
|
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');
|
2025-08-22 18:08:57 +09:00
|
|
|
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');
|
2025-12-02 22:11:08 +09:00
|
|
|
Route::post('/{id}/restore', [MenuController::class, 'restore'])->name('v1.menus.restore');
|
2025-08-16 03:25:06 +09:00
|
|
|
});
|
2025-07-17 10:05:47 +09:00
|
|
|
|
2025-08-16 03:25:06 +09:00
|
|
|
// Role API
|
|
|
|
|
Route::prefix('roles')->group(function () {
|
2025-08-22 18:08:57 +09:00
|
|
|
Route::get('/', [RoleController::class, 'index'])->name('v1.roles.index'); // view
|
|
|
|
|
Route::post('/', [RoleController::class, 'store'])->name('v1.roles.store'); // create
|
|
|
|
|
Route::get('/{id}', [RoleController::class, 'show'])->name('v1.roles.show'); // view
|
|
|
|
|
Route::patch('/{id}', [RoleController::class, 'update'])->name('v1.roles.update'); // update
|
2025-11-06 17:24:42 +09:00
|
|
|
Route::delete('/{id}', [RoleController::class, 'destroy'])->name('v1.roles.destroy'); // delete
|
2025-08-16 03:25:06 +09:00
|
|
|
});
|
2025-08-14 00:55:08 +09:00
|
|
|
|
2025-08-16 03:25:06 +09:00
|
|
|
// Role Permission API
|
|
|
|
|
Route::prefix('roles/{id}/permissions')->group(function () {
|
2025-08-22 18:08:57 +09:00
|
|
|
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
|
2025-08-16 03:25:06 +09:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// User Role API
|
|
|
|
|
Route::prefix('users/{id}/roles')->group(function () {
|
2025-08-22 18:08:57 +09:00
|
|
|
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
|
2025-08-16 03:25:06 +09:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Department API
|
|
|
|
|
Route::prefix('departments')->group(function () {
|
2025-09-24 20:35:17 +09:00
|
|
|
Route::get('', [DepartmentController::class, 'index'])->name('v1.departments.index'); // 목록
|
|
|
|
|
Route::post('', [DepartmentController::class, 'store'])->name('v1.departments.store'); // 생성
|
2025-12-09 20:27:44 +09:00
|
|
|
Route::get('/tree', [DepartmentController::class, 'tree'])->name('v1.departments.tree'); // 트리
|
2025-09-24 20:35:17 +09:00
|
|
|
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)
|
2025-08-16 03:25:06 +09:00
|
|
|
|
|
|
|
|
// 부서-사용자
|
2025-09-24 20:35:17 +09:00
|
|
|
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'); // 주부서 설정/해제
|
2025-08-16 03:25:06 +09:00
|
|
|
|
|
|
|
|
// 부서-권한
|
2025-09-24 20:35:17 +09:00
|
|
|
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'); // 권한 제거(해당 메뉴 범위까지)
|
2025-08-16 03:25:06 +09:00
|
|
|
});
|
2025-08-14 00:55:08 +09:00
|
|
|
|
2025-12-09 20:27:44 +09:00
|
|
|
// Employee API (사원 관리)
|
|
|
|
|
Route::prefix('employees')->group(function () {
|
|
|
|
|
Route::get('', [EmployeeController::class, 'index'])->name('v1.employees.index');
|
|
|
|
|
Route::post('', [EmployeeController::class, 'store'])->name('v1.employees.store');
|
|
|
|
|
Route::get('/stats', [EmployeeController::class, 'stats'])->name('v1.employees.stats');
|
|
|
|
|
Route::get('/{id}', [EmployeeController::class, 'show'])->name('v1.employees.show');
|
|
|
|
|
Route::patch('/{id}', [EmployeeController::class, 'update'])->name('v1.employees.update');
|
|
|
|
|
Route::delete('/{id}', [EmployeeController::class, 'destroy'])->name('v1.employees.destroy');
|
|
|
|
|
Route::post('/bulk-delete', [EmployeeController::class, 'bulkDelete'])->name('v1.employees.bulkDelete');
|
|
|
|
|
Route::post('/{id}/create-account', [EmployeeController::class, 'createAccount'])->name('v1.employees.createAccount');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Attendance API (근태 관리)
|
|
|
|
|
Route::prefix('attendances')->group(function () {
|
|
|
|
|
Route::get('', [AttendanceController::class, 'index'])->name('v1.attendances.index');
|
|
|
|
|
Route::post('', [AttendanceController::class, 'store'])->name('v1.attendances.store');
|
|
|
|
|
Route::get('/monthly-stats', [AttendanceController::class, 'monthlyStats'])->name('v1.attendances.monthlyStats');
|
|
|
|
|
Route::post('/check-in', [AttendanceController::class, 'checkIn'])->name('v1.attendances.checkIn');
|
|
|
|
|
Route::post('/check-out', [AttendanceController::class, 'checkOut'])->name('v1.attendances.checkOut');
|
|
|
|
|
Route::get('/{id}', [AttendanceController::class, 'show'])->name('v1.attendances.show');
|
|
|
|
|
Route::patch('/{id}', [AttendanceController::class, 'update'])->name('v1.attendances.update');
|
|
|
|
|
Route::delete('/{id}', [AttendanceController::class, 'destroy'])->name('v1.attendances.destroy');
|
|
|
|
|
Route::post('/bulk-delete', [AttendanceController::class, 'bulkDelete'])->name('v1.attendances.bulkDelete');
|
|
|
|
|
});
|
|
|
|
|
|
2025-08-21 20:48:39 +09:00
|
|
|
// Permission API
|
|
|
|
|
Route::prefix('permissions')->group(function () {
|
2025-09-24 20:35:17 +09:00
|
|
|
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'); // 부서별 권한 메트릭스
|
2025-08-21 20:48:39 +09:00
|
|
|
});
|
|
|
|
|
|
2025-09-24 20:35:17 +09:00
|
|
|
// Settings & Configuration (설정 및 환경설정 통합 관리)
|
|
|
|
|
Route::prefix('settings')->group(function () {
|
|
|
|
|
|
|
|
|
|
// 테넌트 필드 설정 (기존 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'); // 공통 코드 삭제
|
2025-08-18 19:03:46 +09:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 회원 프로필(테넌트 기준)
|
|
|
|
|
Route::prefix('profiles')->group(function () {
|
2025-08-22 18:08:57 +09:00
|
|
|
Route::get('', [TenantUserProfileController::class, 'index'])->name('v1.profiles.index'); // 프로필 목록(테넌트 기준)
|
|
|
|
|
Route::get('/{userId}', [TenantUserProfileController::class, 'show'])->name('v1.profiles.show'); // 특정 사용자 프로필 조회
|
|
|
|
|
Route::patch('/{userId}', [TenantUserProfileController::class, 'update'])->name('v1.profiles.update'); // 특정 사용자 프로필 수정(관리자)
|
|
|
|
|
Route::get('/me', [TenantUserProfileController::class, 'me'])->name('v1.profiles.me'); // 내 프로필 조회
|
|
|
|
|
Route::patch('/me', [TenantUserProfileController::class, 'updateMe'])->name('v1.profiles.me.update'); // 내 프로필 수정
|
|
|
|
|
});
|
|
|
|
|
|
2025-09-24 20:35:17 +09:00
|
|
|
// Category API (통합)
|
2025-08-22 18:08:57 +09:00
|
|
|
Route::prefix('categories')->group(function () {
|
|
|
|
|
|
2025-09-24 20:35:17 +09:00
|
|
|
// === 기본 Category CRUD ===
|
2025-08-22 18:08:57 +09:00
|
|
|
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)
|
2025-08-18 19:03:46 +09:00
|
|
|
|
2025-09-24 20:35:17 +09:00
|
|
|
// === 확장 기능 ===
|
|
|
|
|
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 ===
|
2025-08-25 17:46:34 +09:00
|
|
|
// 목록/생성 (카테고리 기준)
|
2025-11-06 17:24:42 +09:00
|
|
|
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');
|
2025-08-25 17:46:34 +09:00
|
|
|
// 단건
|
2025-11-06 17:24:42 +09:00
|
|
|
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');
|
2025-08-25 17:46:34 +09:00
|
|
|
// 일괄 정렬/업서트
|
2025-11-06 17:24:42 +09:00
|
|
|
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,...}]
|
2025-08-25 17:46:34 +09:00
|
|
|
|
2025-09-24 20:35:17 +09:00
|
|
|
// === Category Templates ===
|
2025-08-25 17:46:34 +09:00
|
|
|
// 버전 목록/생성 (카테고리 기준)
|
2025-11-06 17:24:42 +09:00
|
|
|
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'); // 새 버전 등록
|
2025-08-25 17:46:34 +09:00
|
|
|
// 단건
|
2025-11-06 17:24:42 +09:00
|
|
|
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');
|
2025-08-25 17:46:34 +09:00
|
|
|
// 운영 편의
|
2025-11-06 17:24:42 +09:00
|
|
|
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'); // 렌더용 스냅샷
|
2025-08-25 17:46:34 +09:00
|
|
|
// (선택) 버전 간 diff
|
2025-11-06 17:24:42 +09:00
|
|
|
Route::get('/{id}/templates/diff', [CategoryTemplateController::class, 'diff'])->name('v1.categories.templates.diff'); // ?a=ver&b=ver
|
2025-08-25 17:46:34 +09:00
|
|
|
|
2025-09-24 20:35:17 +09:00
|
|
|
// === Category Logs ===
|
2025-11-06 17:24:42 +09:00
|
|
|
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');
|
2025-08-25 17:46:34 +09:00
|
|
|
// (선택) 특정 변경 시점으로 카테고리 복구(템플릿/필드와 별개)
|
|
|
|
|
// Route::post('{id}/logs/{log}/restore', [CategoryLogController::class, 'restore'])->name('v1.categories.logs.restore');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Classifications API
|
2025-08-22 19:30:05 +09:00
|
|
|
Route::prefix('classifications')->group(function () {
|
2025-11-06 17:24:42 +09:00
|
|
|
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'); // 삭제
|
2025-08-22 19:30:05 +09:00
|
|
|
});
|
|
|
|
|
|
2025-10-13 21:52:34 +09:00
|
|
|
// Clients (거래처 관리)
|
|
|
|
|
Route::prefix('clients')->group(function () {
|
2025-11-06 17:24:42 +09:00
|
|
|
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'); // 활성/비활성
|
2025-10-13 21:52:34 +09:00
|
|
|
});
|
|
|
|
|
|
2025-10-13 22:06:42 +09:00
|
|
|
// Client Groups (고객 그룹 관리)
|
|
|
|
|
Route::prefix('client-groups')->group(function () {
|
2025-11-06 17:24:42 +09:00
|
|
|
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'); // 활성/비활성
|
2025-10-13 22:06:42 +09:00
|
|
|
});
|
|
|
|
|
|
feat: [quote] 견적 API Phase 2-3 완료 (Service + Controller Layer)
Phase 2 - Service Layer:
- QuoteService: 견적 CRUD + 상태관리 (확정/전환)
- QuoteNumberService: 견적번호 채번 (KD-{PREFIX}-YYMMDD-SEQ)
- FormulaEvaluatorService: 수식 평가 엔진 (SUM, IF, ROUND 등)
- QuoteCalculationService: 자동산출 (스크린/철재 제품)
- QuoteDocumentService: PDF 생성 및 이메일/카카오 발송
Phase 3 - Controller Layer:
- QuoteController: 16개 엔드포인트
- FormRequest 7개: Index, Store, Update, BulkDelete, Calculate, SendEmail, SendKakao
- QuoteApi.php: Swagger 문서 (12개 스키마, 16개 엔드포인트)
- routes/api.php: 16개 라우트 등록
i18n 키 추가:
- error.php: quote_not_found, formula_* 등
- message.php: quote.* 성공 메시지
2025-12-04 22:03:40 +09:00
|
|
|
// 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('/{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'); // 발송 이력
|
|
|
|
|
});
|
|
|
|
|
|
2025-12-08 19:03:50 +09:00
|
|
|
// Pricing (단가 관리)
|
2025-10-13 22:06:42 +09:00
|
|
|
Route::prefix('pricing')->group(function () {
|
2025-12-08 19:03:50 +09:00
|
|
|
Route::get('', [PricingController::class, 'index'])->name('v1.pricing.index'); // 목록
|
|
|
|
|
Route::get('/cost', [PricingController::class, 'cost'])->name('v1.pricing.cost'); // 원가 조회
|
|
|
|
|
Route::post('/by-items', [PricingController::class, 'byItems'])->name('v1.pricing.by-items'); // 품목별 단가 현황
|
|
|
|
|
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'); // 변경이력
|
2025-10-13 22:06:42 +09:00
|
|
|
});
|
|
|
|
|
|
2025-09-24 20:35:17 +09:00
|
|
|
// Products & Materials (제품/자재 통합 관리)
|
2025-11-06 17:24:42 +09:00
|
|
|
Route::prefix('products')->group(function () {
|
2025-09-05 17:59:34 +09:00
|
|
|
|
2025-09-24 20:35:17 +09:00
|
|
|
// 제품 카테고리 (기존 product/category에서 이동)
|
2025-11-06 17:24:42 +09:00
|
|
|
Route::get('/categories', [ProductController::class, 'getCategory'])->name('v1.products.categories'); // 제품 카테고리
|
2025-09-24 20:35:17 +09:00
|
|
|
|
2025-10-13 21:52:34 +09:00
|
|
|
// 자재 관리 (기존 독립 materials에서 이동) - ProductController 기본 라우팅보다 앞에 위치
|
2025-11-06 17:24:42 +09:00
|
|
|
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'); // 자재 수정
|
2025-10-13 21:52:34 +09:00
|
|
|
Route::delete('/materials/{id}', [MaterialController::class, 'destroy'])->name('v1.products.materials.destroy'); // 자재 삭제
|
|
|
|
|
|
2025-09-05 17:59:34 +09:00
|
|
|
// (선택) 드롭다운/모달용 간편 검색 & 활성 토글
|
2025-11-06 17:24:42 +09:00
|
|
|
Route::get('/search', [ProductController::class, 'search'])->name('v1.products.search');
|
|
|
|
|
Route::post('/{id}/toggle', [ProductController::class, 'toggle'])->name('v1.products.toggle');
|
2025-09-05 17:59:34 +09:00
|
|
|
|
2025-11-06 17:24:42 +09:00
|
|
|
Route::get('', [ProductController::class, 'index'])->name('v1.products.index'); // 목록/검색(q, category_id, product_type, active, page/size)
|
|
|
|
|
Route::post('', [ProductController::class, 'store'])->name('v1.products.store'); // 생성
|
|
|
|
|
Route::get('/{id}', [ProductController::class, 'show'])->name('v1.products.show'); // 단건
|
|
|
|
|
Route::patch('/{id}', [ProductController::class, 'update'])->name('v1.products.update'); // 수정
|
|
|
|
|
Route::delete('/{id}', [ProductController::class, 'destroy'])->name('v1.products.destroy'); // 삭제(soft)
|
2025-08-25 17:46:34 +09:00
|
|
|
|
2025-08-29 16:22:05 +09:00
|
|
|
// 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'); // 해당 제품에서 사용 중
|
2025-08-25 17:46:34 +09:00
|
|
|
});
|
|
|
|
|
|
2025-11-11 11:30:17 +09:00
|
|
|
// Items (통합 품목 조회 - materials + products UNION)
|
|
|
|
|
Route::prefix('items')->group(function () {
|
2025-12-09 20:27:44 +09:00
|
|
|
Route::get('', [ItemsController::class, 'index'])->name('v1.items.index'); // 통합 목록
|
|
|
|
|
Route::post('', [ItemsController::class, 'store'])->name('v1.items.store'); // 품목 생성
|
|
|
|
|
Route::get('/code/{code}', [ItemsController::class, 'showByCode'])->name('v1.items.show_by_code'); // code 기반 조회
|
|
|
|
|
Route::get('/{id}', [ItemsController::class, 'show'])->name('v1.items.show'); // 단건 (item_type 파라미터 필수)
|
|
|
|
|
Route::put('/{id}', [ItemsController::class, 'update'])->name('v1.items.update'); // 품목 수정
|
|
|
|
|
Route::delete('/batch', [ItemsController::class, 'batchDestroy'])->name('v1.items.batch_destroy'); // 품목 일괄 삭제
|
|
|
|
|
Route::delete('/{id}', [ItemsController::class, 'destroy'])->name('v1.items.destroy'); // 품목 삭제
|
2025-11-11 11:30:17 +09:00
|
|
|
});
|
|
|
|
|
|
2025-11-30 21:06:22 +09:00
|
|
|
// Items BOM (ID-based BOM API)
|
|
|
|
|
Route::prefix('items/{id}/bom')->group(function () {
|
2025-12-09 20:27:44 +09:00
|
|
|
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'); // 카테고리 목록
|
2025-11-17 11:45:16 +09:00
|
|
|
});
|
|
|
|
|
|
2025-12-12 17:38:22 +09:00
|
|
|
// Items Files (group_id 기반 파일 관리, 동적 field_key 지원)
|
2025-11-30 21:06:22 +09:00
|
|
|
Route::prefix('items/{id}/files')->group(function () {
|
2025-12-12 17:38:22 +09:00
|
|
|
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)
|
feat: 품목 파일 업로드 API 구현 (절곡도, 시방서, 인정서)
- Products 테이블에 9개 파일 관련 필드 추가
- bending_diagram, bending_details (JSON)
- specification_file, specification_file_name
- certification_file, certification_file_name
- certification_number, certification_start_date, certification_end_date
- ItemsFileController 구현 (Code-based API)
- POST /items/{code}/files - 파일 업로드
- DELETE /items/{code}/files/{type} - 파일 삭제
- 파일 타입: bending_diagram, specification, certification
- ItemsFileUploadRequest 검증
- 파일 타입별 MIME 검증 (이미지/문서)
- 파일 크기 제한 (10MB/20MB)
- 인증 정보 및 절곡 상세 정보 검증
- Swagger 문서 작성 (ItemsFileApi.php)
- 업로드/삭제 API 스펙
- 스키마: ItemFileUploadResponse, ItemFileDeleteResponse
2025-11-17 13:40:07 +09:00
|
|
|
});
|
|
|
|
|
|
2025-08-25 17:46:34 +09:00
|
|
|
// BOM (product_components: ref_type=PRODUCT|MATERIAL)
|
|
|
|
|
Route::prefix('products/{id}/bom')->group(function () {
|
2025-11-06 17:24:42 +09:00
|
|
|
Route::post('/', [ProductBomItemController::class, 'replace'])->name('v1.products.bom.replace');
|
2025-08-29 16:22:05 +09:00
|
|
|
|
2025-11-06 17:24:42 +09:00
|
|
|
Route::get('/items', [ProductBomItemController::class, 'index'])->name('v1.products.bom.items.index'); // 조회(제품+자재 병합)
|
|
|
|
|
Route::post('/items/bulk', [ProductBomItemController::class, 'bulkUpsert'])->name('v1.products.bom.items.bulk'); // 대량 업서트
|
|
|
|
|
Route::patch('/items/{item}', [ProductBomItemController::class, 'update'])->name('v1.products.bom.items.update'); // 단건 수정
|
|
|
|
|
Route::delete('/items/{item}', [ProductBomItemController::class, 'destroy'])->name('v1.products.bom.items.destroy'); // 단건 삭제
|
|
|
|
|
Route::post('/items/reorder', [ProductBomItemController::class, 'reorder'])->name('v1.products.bom.items.reorder'); // 정렬 변경
|
2025-08-25 17:46:34 +09:00
|
|
|
|
|
|
|
|
// (선택) 합계/검증
|
2025-11-06 17:24:42 +09:00
|
|
|
Route::get('/summary', [ProductBomItemController::class, 'summary'])->name('v1.products.bom.summary');
|
|
|
|
|
Route::get('/validate', [ProductBomItemController::class, 'validateBom'])->name('v1.products.bom.validate');
|
2025-08-29 16:22:05 +09:00
|
|
|
|
2025-09-11 13:34:20 +09:00
|
|
|
Route::get('/tree', [ProductBomItemController::class, 'tree'])->name('v1.products.bom.tree');
|
2025-08-25 17:46:34 +09:00
|
|
|
});
|
|
|
|
|
|
2025-09-05 17:59:34 +09:00
|
|
|
// 설계 전용 (Design) - 운영과 분리된 네임스페이스/경로
|
|
|
|
|
Route::prefix('design')->group(function () {
|
2025-11-06 17:24:42 +09:00
|
|
|
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');
|
2025-09-05 17:59:34 +09:00
|
|
|
Route::delete('/models/{id}', [DesignModelController::class, 'destroy'])->name('v1.design.models.destroy');
|
|
|
|
|
|
2025-11-06 17:24:42 +09:00
|
|
|
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');
|
2025-09-05 17:59:34 +09:00
|
|
|
|
2025-11-06 17:24:42 +09:00
|
|
|
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');
|
2025-09-11 14:39:55 +09:00
|
|
|
|
|
|
|
|
// 감사 로그 조회
|
2025-11-06 17:24:42 +09:00
|
|
|
Route::get('/audit-logs', [DesignAuditLogController::class, 'index'])->name('v1.design.audit-logs.index');
|
2025-09-22 22:09:42 +09:00
|
|
|
|
|
|
|
|
// BOM 계산 시스템
|
2025-11-06 17:24:42 +09:00
|
|
|
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');
|
2025-09-05 17:59:34 +09:00
|
|
|
});
|
|
|
|
|
|
2025-09-24 17:41:26 +09:00
|
|
|
// 모델셋 관리 API (견적 시스템)
|
|
|
|
|
Route::prefix('model-sets')->group(function () {
|
2025-11-06 17:24:42 +09:00
|
|
|
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'); // 모델셋 수정
|
2025-09-24 17:41:26 +09:00
|
|
|
Route::delete('/{id}', [ModelSetController::class, 'destroy'])->name('v1.model-sets.destroy'); // 모델셋 삭제
|
2025-11-06 17:24:42 +09:00
|
|
|
Route::post('/{id}/clone', [ModelSetController::class, 'clone'])->name('v1.model-sets.clone'); // 모델셋 복제
|
2025-09-24 17:41:26 +09:00
|
|
|
|
|
|
|
|
// 모델셋 세부 기능
|
2025-11-06 17:24:42 +09:00
|
|
|
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 계산
|
2025-09-24 17:41:26 +09:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 견적 관리 API
|
|
|
|
|
Route::prefix('estimates')->group(function () {
|
2025-11-06 17:24:42 +09:00
|
|
|
Route::get('/', [EstimateController::class, 'index'])->name('v1.estimates.index'); // 견적 목록
|
|
|
|
|
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'); // 견적 수정
|
2025-09-24 17:41:26 +09:00
|
|
|
Route::delete('/{id}', [EstimateController::class, 'destroy'])->name('v1.estimates.destroy'); // 견적 삭제
|
2025-11-06 17:24:42 +09:00
|
|
|
Route::post('/{id}/clone', [EstimateController::class, 'clone'])->name('v1.estimates.clone'); // 견적 복제
|
|
|
|
|
Route::put('/{id}/status', [EstimateController::class, 'changeStatus'])->name('v1.estimates.status'); // 견적 상태 변경
|
2025-09-24 17:41:26 +09:00
|
|
|
|
|
|
|
|
// 견적 폼 및 계산 기능
|
2025-11-06 17:24:42 +09:00
|
|
|
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'); // 견적 계산 미리보기
|
2025-09-24 17:41:26 +09:00
|
|
|
});
|
|
|
|
|
|
feat: 파일 저장 시스템 DB 마이그레이션
- enhance_files_table: 이중 파일명 시스템 (display_name/stored_name), 폴더 관리, 문서 연결 지원
- create_folders_table: 동적 폴더 관리 시스템 (tenant별 커스터마이징 가능)
- 5개 stub 마이그레이션 생성 (file_share_links, file_deletion_logs, storage_usage_history, add_storage_columns_to_tenants)
- FolderSeeder stub 생성
- CURRENT_WORKS.md에 Phase 1 진행상황 문서화
fix: 파일 공유 및 삭제 기능 버그 수정
- ShareLinkRequest: PATH 파라미터 {id}를 file_id로 자동 병합
- routes/api.php: 공유 링크 다운로드를 auth.apikey 그룹 밖으로 이동 (인증 불필요)
- FileShareLink: File, Tenant 클래스 import 추가
- File 모델: softDeleteFile()에서 SoftDeletes의 delete() 메서드 사용
- FileStorageService: getTrash(), restoreFile(), permanentDelete()에서 onlyTrashed() 사용
- File 모델: Tenant 네임스페이스 수정 (App\Models\Tenants\Tenant)
refactor: Swagger 문서 정리 - File 태그를 Files로 통합
- FileApi.php의 모든 태그를 Files로 변경
- 구 파일 시스템 라우트 삭제 (prefix 'file')
- 구 FileController.php 삭제
- 신규 파일 저장소 시스템으로 완전 통합
fix: 모든 legacy 파일 컬럼 nullable 일괄 처리
- 5개 legacy 컬럼을 한 번에 nullable로 변경
* original_name, file_name, file_name_old (string)
* fileable_id, fileable_type (polymorphic)
- foreach 루프로 반복 작업 자동화
- 신규/기존 시스템 간 완전한 하위 호환성 확보
fix: legacy 파일 컬럼 nullable 처리 완료
- file_name, file_name_old 컬럼도 nullable로 변경
- 기존 시스템과 신규 시스템 간 완전한 하위 호환성 확보
- Legacy: original_name, file_name, file_name_old (nullable)
- New: display_name, stored_name (required)
fix: original_name 컬럼 nullable 처리
- original_name을 nullable로 변경하여 하위 호환성 유지
- 새 시스템에서는 display_name 사용, 기존 시스템은 original_name 사용 가능
fix: 파일 업로드 DB 컬럼 누락 및 메시지 구조 개선
- files 테이블에 감사 컬럼 추가 (created_by, updated_by, uploaded_by)
- ApiResponse::handle() 메시지 로직 개선 (접미사 제거)
- 다국어 지원을 위한 완성된 문장 구조 유지
- FileUploadRequest 파일 검증 규칙 수정
fix: 파일 저장소 버그 수정 및 신규 테넌트 폴더 자동 생성
- FolderSeeder 네임스페이스 수정 (App\Models\Tenant → App\Models\Tenants\Tenant)
- FileStorageController use 문 구문 오류 수정 (/ → \)
- TenantObserver에 신규 테넌트 기본 폴더 자동 생성 로직 추가
- 5개 기본 폴더 (생산관리, 품질관리, 회계, 인사, 일반)
- 에러 처리 및 로깅
- 회원가입 시 자동 실행
2025-11-10 19:08:56 +09:00
|
|
|
// 파일 저장소 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'); // 폴더 순서 변경
|
|
|
|
|
});
|
|
|
|
|
|
2025-11-20 16:55:57 +09:00
|
|
|
// 품목기준관리 (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');
|
|
|
|
|
|
2025-11-26 14:09:31 +09:00
|
|
|
// 독립 섹션 관리
|
|
|
|
|
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');
|
|
|
|
|
|
|
|
|
|
// 섹션 관리 (페이지 연결)
|
2025-11-20 16:55:57 +09:00
|
|
|
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');
|
|
|
|
|
|
2025-11-26 14:09:31 +09:00
|
|
|
// 독립 필드 관리
|
|
|
|
|
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');
|
|
|
|
|
|
|
|
|
|
// 필드 관리 (섹션 연결)
|
2025-11-20 16:55:57 +09:00
|
|
|
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');
|
2025-11-20 17:07:40 +09:00
|
|
|
|
2025-11-26 14:09:31 +09:00
|
|
|
// 독립 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 항목 관리 (섹션 연결)
|
2025-11-20 17:07:40 +09:00
|
|
|
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');
|
|
|
|
|
|
2025-11-20 17:16:03 +09:00
|
|
|
// 커스텀 탭
|
|
|
|
|
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');
|
2025-11-20 20:28:33 +09:00
|
|
|
Route::put('/custom-tabs/reorder', [CustomTabController::class, 'reorder'])->name('v1.item-master.custom-tabs.reorder');
|
2025-11-20 17:16:03 +09:00
|
|
|
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');
|
2025-11-26 14:09:31 +09:00
|
|
|
|
|
|
|
|
// 엔티티 관계 관리 (독립 엔티티 + 링크 테이블)
|
|
|
|
|
// 페이지-섹션 연결
|
|
|
|
|
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');
|
2025-11-20 16:55:57 +09:00
|
|
|
});
|
|
|
|
|
|
2025-11-30 21:06:22 +09:00
|
|
|
// 게시판 관리 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('/{code}', [BoardController::class, 'show'])->name('v1.boards.show'); // 게시판 상세 (코드 기반)
|
|
|
|
|
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'); // 댓글 삭제
|
|
|
|
|
});
|
|
|
|
|
|
2025-08-14 00:55:08 +09:00
|
|
|
});
|
feat: 파일 저장 시스템 DB 마이그레이션
- enhance_files_table: 이중 파일명 시스템 (display_name/stored_name), 폴더 관리, 문서 연결 지원
- create_folders_table: 동적 폴더 관리 시스템 (tenant별 커스터마이징 가능)
- 5개 stub 마이그레이션 생성 (file_share_links, file_deletion_logs, storage_usage_history, add_storage_columns_to_tenants)
- FolderSeeder stub 생성
- CURRENT_WORKS.md에 Phase 1 진행상황 문서화
fix: 파일 공유 및 삭제 기능 버그 수정
- ShareLinkRequest: PATH 파라미터 {id}를 file_id로 자동 병합
- routes/api.php: 공유 링크 다운로드를 auth.apikey 그룹 밖으로 이동 (인증 불필요)
- FileShareLink: File, Tenant 클래스 import 추가
- File 모델: softDeleteFile()에서 SoftDeletes의 delete() 메서드 사용
- FileStorageService: getTrash(), restoreFile(), permanentDelete()에서 onlyTrashed() 사용
- File 모델: Tenant 네임스페이스 수정 (App\Models\Tenants\Tenant)
refactor: Swagger 문서 정리 - File 태그를 Files로 통합
- FileApi.php의 모든 태그를 Files로 변경
- 구 파일 시스템 라우트 삭제 (prefix 'file')
- 구 FileController.php 삭제
- 신규 파일 저장소 시스템으로 완전 통합
fix: 모든 legacy 파일 컬럼 nullable 일괄 처리
- 5개 legacy 컬럼을 한 번에 nullable로 변경
* original_name, file_name, file_name_old (string)
* fileable_id, fileable_type (polymorphic)
- foreach 루프로 반복 작업 자동화
- 신규/기존 시스템 간 완전한 하위 호환성 확보
fix: legacy 파일 컬럼 nullable 처리 완료
- file_name, file_name_old 컬럼도 nullable로 변경
- 기존 시스템과 신규 시스템 간 완전한 하위 호환성 확보
- Legacy: original_name, file_name, file_name_old (nullable)
- New: display_name, stored_name (required)
fix: original_name 컬럼 nullable 처리
- original_name을 nullable로 변경하여 하위 호환성 유지
- 새 시스템에서는 display_name 사용, 기존 시스템은 original_name 사용 가능
fix: 파일 업로드 DB 컬럼 누락 및 메시지 구조 개선
- files 테이블에 감사 컬럼 추가 (created_by, updated_by, uploaded_by)
- ApiResponse::handle() 메시지 로직 개선 (접미사 제거)
- 다국어 지원을 위한 완성된 문장 구조 유지
- FileUploadRequest 파일 검증 규칙 수정
fix: 파일 저장소 버그 수정 및 신규 테넌트 폴더 자동 생성
- FolderSeeder 네임스페이스 수정 (App\Models\Tenant → App\Models\Tenants\Tenant)
- FileStorageController use 문 구문 오류 수정 (/ → \)
- TenantObserver에 신규 테넌트 기본 폴더 자동 생성 로직 추가
- 5개 기본 폴더 (생산관리, 품질관리, 회계, 인사, 일반)
- 에러 처리 및 로깅
- 회원가입 시 자동 실행
2025-11-10 19:08:56 +09:00
|
|
|
|
|
|
|
|
// 공유 링크 다운로드 (인증 불필요 - auth.apikey 그룹 밖)
|
|
|
|
|
Route::get('/files/share/{token}', [FileStorageController::class, 'downloadShared'])->name('v1.files.share.download');
|
2025-08-16 03:25:06 +09:00
|
|
|
});
|