From 84cce6742ec2b397ecbb15ebda93ed5e0731911f Mon Sep 17 00:00:00 2001 From: kent Date: Fri, 26 Dec 2025 15:18:08 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20G-2=20=EC=9E=91=EC=97=85=EC=8B=A4?= =?UTF-8?q?=EC=A0=81=20=EA=B4=80=EB=A6=AC=20API=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - WorkResult 모델 생성 (Production 네임스페이스) - WorkResultService 서비스 구현 (CRUD + 통계 + 토글) - WorkResultController 컨트롤러 생성 (8개 엔드포인트) - FormRequest 검증 클래스 (Store/Update) - Swagger 문서 작성 (WorkResultApi.php) - 라우트 추가 (/api/v1/work-results) - i18n 메시지 추가 (work_result 키) API Endpoints: - GET /work-results - 목록 조회 (페이징, 필터링) - GET /work-results/stats - 통계 조회 - GET /work-results/{id} - 상세 조회 - POST /work-results - 등록 - PUT /work-results/{id} - 수정 - DELETE /work-results/{id} - 삭제 - PATCH /work-results/{id}/inspection - 검사 상태 토글 - PATCH /work-results/{id}/packaging - 포장 상태 토글 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../Api/V1/WorkResultController.php | 97 ++++++ .../WorkResult/WorkResultStoreRequest.php | 46 +++ .../WorkResult/WorkResultUpdateRequest.php | 43 +++ app/Models/Production/WorkResult.php | 242 ++++++++++++++ app/Services/WorkResultService.php | 279 ++++++++++++++++ app/Swagger/v1/WorkResultApi.php | 313 ++++++++++++++++++ ...12_26_150334_create_work_results_table.php | 57 ++++ lang/ko/message.php | 37 +++ routes/api.php | 40 +++ 9 files changed, 1154 insertions(+) create mode 100644 app/Http/Controllers/Api/V1/WorkResultController.php create mode 100644 app/Http/Requests/WorkResult/WorkResultStoreRequest.php create mode 100644 app/Http/Requests/WorkResult/WorkResultUpdateRequest.php create mode 100644 app/Models/Production/WorkResult.php create mode 100644 app/Services/WorkResultService.php create mode 100644 app/Swagger/v1/WorkResultApi.php create mode 100644 database/migrations/2025_12_26_150334_create_work_results_table.php diff --git a/app/Http/Controllers/Api/V1/WorkResultController.php b/app/Http/Controllers/Api/V1/WorkResultController.php new file mode 100644 index 0000000..93a6c4c --- /dev/null +++ b/app/Http/Controllers/Api/V1/WorkResultController.php @@ -0,0 +1,97 @@ +service->index($request->all()); + }, __('message.work_result.fetched')); + } + + /** + * 통계 조회 + */ + public function stats(Request $request) + { + return ApiResponse::handle(function () use ($request) { + return $this->service->stats($request->all()); + }, __('message.work_result.fetched')); + } + + /** + * 단건 조회 + */ + public function show(int $id) + { + return ApiResponse::handle(function () use ($id) { + return $this->service->show($id); + }, __('message.work_result.fetched')); + } + + /** + * 생성 + */ + public function store(WorkResultStoreRequest $request) + { + return ApiResponse::handle(function () use ($request) { + return $this->service->store($request->validated()); + }, __('message.work_result.created')); + } + + /** + * 수정 + */ + public function update(WorkResultUpdateRequest $request, int $id) + { + return ApiResponse::handle(function () use ($request, $id) { + return $this->service->update($id, $request->validated()); + }, __('message.work_result.updated')); + } + + /** + * 삭제 + */ + public function destroy(int $id) + { + return ApiResponse::handle(function () use ($id) { + $this->service->destroy($id); + + return 'success'; + }, __('message.work_result.deleted')); + } + + /** + * 검사 상태 토글 + */ + public function toggleInspection(int $id) + { + return ApiResponse::handle(function () use ($id) { + return $this->service->toggleInspection($id); + }, __('message.work_result.inspection_toggled')); + } + + /** + * 포장 상태 토글 + */ + public function togglePackaging(int $id) + { + return ApiResponse::handle(function () use ($id) { + return $this->service->togglePackaging($id); + }, __('message.work_result.packaging_toggled')); + } +} diff --git a/app/Http/Requests/WorkResult/WorkResultStoreRequest.php b/app/Http/Requests/WorkResult/WorkResultStoreRequest.php new file mode 100644 index 0000000..4234325 --- /dev/null +++ b/app/Http/Requests/WorkResult/WorkResultStoreRequest.php @@ -0,0 +1,46 @@ + 'required|integer|exists:work_orders,id', + 'work_order_item_id' => 'nullable|integer|exists:work_order_items,id', + 'lot_no' => 'required|string|max:50', + 'work_date' => 'required|date', + 'process_type' => 'nullable|in:screen,slat,bending', + 'product_name' => 'required|string|max:200', + 'specification' => 'nullable|string|max:100', + 'production_qty' => 'required|integer|min:0', + 'good_qty' => 'nullable|integer|min:0', + 'defect_qty' => 'required|integer|min:0', + 'is_inspected' => 'nullable|boolean', + 'is_packaged' => 'nullable|boolean', + 'worker_id' => 'nullable|integer|exists:users,id', + 'memo' => 'nullable|string|max:2000', + ]; + } + + public function messages(): array + { + return [ + 'work_order_id.required' => __('validation.required', ['attribute' => '작업지시']), + 'work_order_id.exists' => __('validation.exists', ['attribute' => '작업지시']), + 'lot_no.required' => __('validation.required', ['attribute' => '로트번호']), + 'work_date.required' => __('validation.required', ['attribute' => '작업일']), + 'product_name.required' => __('validation.required', ['attribute' => '품목명']), + 'production_qty.required' => __('validation.required', ['attribute' => '생산수량']), + 'defect_qty.required' => __('validation.required', ['attribute' => '불량수량']), + ]; + } +} diff --git a/app/Http/Requests/WorkResult/WorkResultUpdateRequest.php b/app/Http/Requests/WorkResult/WorkResultUpdateRequest.php new file mode 100644 index 0000000..951fe1a --- /dev/null +++ b/app/Http/Requests/WorkResult/WorkResultUpdateRequest.php @@ -0,0 +1,43 @@ + 'sometimes|integer|exists:work_orders,id', + 'work_order_item_id' => 'nullable|integer|exists:work_order_items,id', + 'lot_no' => 'sometimes|string|max:50', + 'work_date' => 'sometimes|date', + 'process_type' => 'sometimes|in:screen,slat,bending', + 'product_name' => 'sometimes|string|max:200', + 'specification' => 'nullable|string|max:100', + 'production_qty' => 'sometimes|integer|min:0', + 'good_qty' => 'nullable|integer|min:0', + 'defect_qty' => 'sometimes|integer|min:0', + 'is_inspected' => 'nullable|boolean', + 'is_packaged' => 'nullable|boolean', + 'worker_id' => 'nullable|integer|exists:users,id', + 'memo' => 'nullable|string|max:2000', + ]; + } + + public function messages(): array + { + return [ + 'work_order_id.exists' => __('validation.exists', ['attribute' => '작업지시']), + 'lot_no.max' => __('validation.max.string', ['attribute' => '로트번호', 'max' => 50]), + 'work_date.date' => __('validation.date', ['attribute' => '작업일']), + 'product_name.max' => __('validation.max.string', ['attribute' => '품목명', 'max' => 200]), + ]; + } +} diff --git a/app/Models/Production/WorkResult.php b/app/Models/Production/WorkResult.php new file mode 100644 index 0000000..42d1483 --- /dev/null +++ b/app/Models/Production/WorkResult.php @@ -0,0 +1,242 @@ + 'date', + 'production_qty' => 'integer', + 'good_qty' => 'integer', + 'defect_qty' => 'integer', + 'defect_rate' => 'decimal:2', + 'is_inspected' => 'boolean', + 'is_packaged' => 'boolean', + ]; + + protected $hidden = [ + 'deleted_at', + ]; + + // ────────────────────────────────────────────────────────────── + // 상수 + // ────────────────────────────────────────────────────────────── + + /** + * 공정 유형 (WorkOrder와 동일) + */ + public const PROCESS_SCREEN = 'screen'; + + public const PROCESS_SLAT = 'slat'; + + public const PROCESS_BENDING = 'bending'; + + public const PROCESS_TYPES = [ + self::PROCESS_SCREEN, + self::PROCESS_SLAT, + self::PROCESS_BENDING, + ]; + + // ────────────────────────────────────────────────────────────── + // 관계 + // ────────────────────────────────────────────────────────────── + + /** + * 작업지시 + */ + public function workOrder(): BelongsTo + { + return $this->belongsTo(WorkOrder::class); + } + + /** + * 작업지시 품목 + */ + public function workOrderItem(): BelongsTo + { + return $this->belongsTo(WorkOrderItem::class); + } + + /** + * 작업자 + */ + public function worker(): BelongsTo + { + return $this->belongsTo(User::class, 'worker_id'); + } + + /** + * 생성자 + */ + public function creator(): BelongsTo + { + return $this->belongsTo(User::class, 'created_by'); + } + + /** + * 수정자 + */ + public function updater(): BelongsTo + { + return $this->belongsTo(User::class, 'updated_by'); + } + + // ────────────────────────────────────────────────────────────── + // 스코프 + // ────────────────────────────────────────────────────────────── + + /** + * 공정유형별 필터 + */ + public function scopeProcessType($query, string $type) + { + return $query->where('process_type', $type); + } + + /** + * 작업일 범위 필터 + */ + public function scopeWorkDateBetween($query, $from, $to) + { + return $query->whereBetween('work_date', [$from, $to]); + } + + /** + * 작업지시별 필터 + */ + public function scopeWorkOrder($query, int $workOrderId) + { + return $query->where('work_order_id', $workOrderId); + } + + /** + * 작업자별 필터 + */ + public function scopeWorker($query, int $workerId) + { + return $query->where('worker_id', $workerId); + } + + /** + * 검사 완료 필터 + */ + public function scopeInspected($query) + { + return $query->where('is_inspected', true); + } + + /** + * 포장 완료 필터 + */ + public function scopePackaged($query) + { + return $query->where('is_packaged', true); + } + + /** + * 불량 있음 + */ + public function scopeHasDefects($query) + { + return $query->where('defect_qty', '>', 0); + } + + // ────────────────────────────────────────────────────────────── + // 헬퍼 메서드 + // ────────────────────────────────────────────────────────────── + + /** + * 불량률 계산 + */ + public function calculateDefectRate(): float + { + if ($this->production_qty <= 0) { + return 0; + } + + return round(($this->defect_qty / $this->production_qty) * 100, 2); + } + + /** + * 불량률 업데이트 + */ + public function updateDefectRate(): void + { + $this->defect_rate = $this->calculateDefectRate(); + } + + /** + * 양품수량 자동 계산 및 설정 + */ + public function calculateGoodQty(): int + { + return max(0, $this->production_qty - $this->defect_qty); + } + + /** + * 불량이 있는지 확인 + */ + public function hasDefects(): bool + { + return $this->defect_qty > 0; + } + + /** + * 검사 및 포장 모두 완료인지 확인 + */ + public function isFullyProcessed(): bool + { + return $this->is_inspected && $this->is_packaged; + } + + // ────────────────────────────────────────────────────────────── + // 부팅 + // ────────────────────────────────────────────────────────────── + + protected static function boot() + { + parent::boot(); + + // 저장 전 불량률 자동 계산 + static::saving(function (WorkResult $model) { + $model->updateDefectRate(); + }); + } +} diff --git a/app/Services/WorkResultService.php b/app/Services/WorkResultService.php new file mode 100644 index 0000000..7ba34fc --- /dev/null +++ b/app/Services/WorkResultService.php @@ -0,0 +1,279 @@ +tenantId(); + + $page = (int) ($params['page'] ?? 1); + $size = (int) ($params['size'] ?? 20); + $q = trim((string) ($params['q'] ?? '')); + $processType = $params['process_type'] ?? null; + $workOrderId = $params['work_order_id'] ?? null; + $workerId = $params['worker_id'] ?? null; + $workDateFrom = $params['work_date_from'] ?? null; + $workDateTo = $params['work_date_to'] ?? null; + $isInspected = isset($params['is_inspected']) ? filter_var($params['is_inspected'], FILTER_VALIDATE_BOOLEAN) : null; + $isPackaged = isset($params['is_packaged']) ? filter_var($params['is_packaged'], FILTER_VALIDATE_BOOLEAN) : null; + + $query = WorkResult::query() + ->where('tenant_id', $tenantId) + ->with([ + 'workOrder:id,work_order_no', + 'worker:id,name', + ]); + + // 검색어 + if ($q !== '') { + $query->where(function ($qq) use ($q) { + $qq->where('lot_no', 'like', "%{$q}%") + ->orWhere('product_name', 'like', "%{$q}%") + ->orWhereHas('workOrder', fn ($wo) => $wo->where('work_order_no', 'like', "%{$q}%")); + }); + } + + // 공정유형 필터 + if ($processType !== null) { + $query->where('process_type', $processType); + } + + // 작업지시 필터 + if ($workOrderId !== null) { + $query->where('work_order_id', $workOrderId); + } + + // 작업자 필터 + if ($workerId !== null) { + $query->where('worker_id', $workerId); + } + + // 작업일 범위 + if ($workDateFrom !== null) { + $query->where('work_date', '>=', $workDateFrom); + } + if ($workDateTo !== null) { + $query->where('work_date', '<=', $workDateTo); + } + + // 검사 완료 필터 + if ($isInspected !== null) { + $query->where('is_inspected', $isInspected); + } + + // 포장 완료 필터 + if ($isPackaged !== null) { + $query->where('is_packaged', $isPackaged); + } + + $query->orderByDesc('work_date')->orderByDesc('created_at'); + + return $query->paginate($size, ['*'], 'page', $page); + } + + /** + * 통계 조회 + */ + public function stats(array $params = []): array + { + $tenantId = $this->tenantId(); + + $workDateFrom = $params['work_date_from'] ?? null; + $workDateTo = $params['work_date_to'] ?? null; + $processType = $params['process_type'] ?? null; + + $query = WorkResult::where('tenant_id', $tenantId); + + // 작업일 범위 + if ($workDateFrom !== null) { + $query->where('work_date', '>=', $workDateFrom); + } + if ($workDateTo !== null) { + $query->where('work_date', '<=', $workDateTo); + } + + // 공정유형 필터 + if ($processType !== null) { + $query->where('process_type', $processType); + } + + $totals = $query->select([ + DB::raw('SUM(production_qty) as total_production'), + DB::raw('SUM(good_qty) as total_good'), + DB::raw('SUM(defect_qty) as total_defect'), + ])->first(); + + $totalProduction = (int) ($totals->total_production ?? 0); + $totalGood = (int) ($totals->total_good ?? 0); + $totalDefect = (int) ($totals->total_defect ?? 0); + + $defectRate = $totalProduction > 0 + ? round(($totalDefect / $totalProduction) * 100, 1) + : 0; + + return [ + 'total_production' => $totalProduction, + 'total_good' => $totalGood, + 'total_defect' => $totalDefect, + 'defect_rate' => $defectRate, + ]; + } + + /** + * 단건 조회 + */ + public function show(int $id) + { + $tenantId = $this->tenantId(); + + $workResult = WorkResult::where('tenant_id', $tenantId) + ->with([ + 'workOrder:id,work_order_no,project_name,status', + 'workOrderItem:id,item_name,specification,quantity', + 'worker:id,name', + ]) + ->find($id); + + if (! $workResult) { + throw new NotFoundHttpException(__('error.not_found')); + } + + return $workResult; + } + + /** + * 생성 + */ + public function store(array $data) + { + $tenantId = $this->tenantId(); + $userId = $this->apiUserId(); + + return DB::transaction(function () use ($data, $tenantId, $userId) { + $data['tenant_id'] = $tenantId; + $data['created_by'] = $userId; + $data['updated_by'] = $userId; + + // 양품수량 자동 계산 (입력 안 된 경우) + if (! isset($data['good_qty'])) { + $data['good_qty'] = max(0, ($data['production_qty'] ?? 0) - ($data['defect_qty'] ?? 0)); + } + + // 작업지시 정보로 자동 채움 + if (! empty($data['work_order_id'])) { + $workOrder = WorkOrder::find($data['work_order_id']); + if ($workOrder) { + $data['process_type'] = $data['process_type'] ?? $workOrder->process_type; + } + } + + return WorkResult::create($data); + }); + } + + /** + * 수정 + */ + public function update(int $id, array $data) + { + $tenantId = $this->tenantId(); + $userId = $this->apiUserId(); + + $workResult = WorkResult::where('tenant_id', $tenantId)->find($id); + + if (! $workResult) { + throw new NotFoundHttpException(__('error.not_found')); + } + + return DB::transaction(function () use ($workResult, $data, $userId) { + $data['updated_by'] = $userId; + + // 양품수량 재계산 (생산수량 또는 불량수량 변경 시) + if (isset($data['production_qty']) || isset($data['defect_qty'])) { + $productionQty = $data['production_qty'] ?? $workResult->production_qty; + $defectQty = $data['defect_qty'] ?? $workResult->defect_qty; + + if (! isset($data['good_qty'])) { + $data['good_qty'] = max(0, $productionQty - $defectQty); + } + } + + $workResult->update($data); + + return $workResult->fresh([ + 'workOrder:id,work_order_no', + 'worker:id,name', + ]); + }); + } + + /** + * 삭제 + */ + public function destroy(int $id): void + { + $tenantId = $this->tenantId(); + + $workResult = WorkResult::where('tenant_id', $tenantId)->find($id); + + if (! $workResult) { + throw new NotFoundHttpException(__('error.not_found')); + } + + $workResult->delete(); + } + + /** + * 검사 상태 토글 + */ + public function toggleInspection(int $id) + { + $tenantId = $this->tenantId(); + $userId = $this->apiUserId(); + + $workResult = WorkResult::where('tenant_id', $tenantId)->find($id); + + if (! $workResult) { + throw new NotFoundHttpException(__('error.not_found')); + } + + $workResult->update([ + 'is_inspected' => ! $workResult->is_inspected, + 'updated_by' => $userId, + ]); + + return $workResult->fresh(); + } + + /** + * 포장 상태 토글 + */ + public function togglePackaging(int $id) + { + $tenantId = $this->tenantId(); + $userId = $this->apiUserId(); + + $workResult = WorkResult::where('tenant_id', $tenantId)->find($id); + + if (! $workResult) { + throw new NotFoundHttpException(__('error.not_found')); + } + + $workResult->update([ + 'is_packaged' => ! $workResult->is_packaged, + 'updated_by' => $userId, + ]); + + return $workResult->fresh(); + } +} diff --git a/app/Swagger/v1/WorkResultApi.php b/app/Swagger/v1/WorkResultApi.php new file mode 100644 index 0000000..1d194dd --- /dev/null +++ b/app/Swagger/v1/WorkResultApi.php @@ -0,0 +1,313 @@ +id(); + $table->foreignId('tenant_id')->constrained()->comment('테넌트 ID'); + $table->foreignId('work_order_id')->constrained()->comment('작업지시 ID'); + $table->foreignId('work_order_item_id')->nullable()->constrained()->comment('작업지시 품목 ID'); + + $table->string('lot_no', 50)->comment('로트번호'); + $table->date('work_date')->comment('작업일'); + $table->enum('process_type', ['screen', 'slat', 'bending'])->comment('공정구분'); + $table->string('product_name', 200)->comment('품목명'); + $table->string('specification', 100)->nullable()->comment('규격'); + + $table->integer('production_qty')->default(0)->comment('생산수량'); + $table->integer('good_qty')->default(0)->comment('양품수량'); + $table->integer('defect_qty')->default(0)->comment('불량수량'); + $table->decimal('defect_rate', 5, 2)->default(0)->comment('불량률 (%)'); + + $table->boolean('is_inspected')->default(false)->comment('검사 완료 여부'); + $table->boolean('is_packaged')->default(false)->comment('포장 완료 여부'); + + $table->foreignId('worker_id')->nullable()->constrained('users')->comment('작업자 ID'); + $table->text('memo')->nullable()->comment('비고'); + + $table->foreignId('created_by')->nullable()->constrained('users')->comment('생성자'); + $table->foreignId('updated_by')->nullable()->constrained('users')->comment('수정자'); + $table->timestamps(); + $table->softDeletes(); + + // 인덱스 + $table->index(['tenant_id', 'work_date']); + $table->index(['tenant_id', 'process_type']); + $table->index(['tenant_id', 'work_order_id']); + $table->index('lot_no'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('work_results'); + } +}; diff --git a/lang/ko/message.php b/lang/ko/message.php index c0c4e0b..32ed5db 100644 --- a/lang/ko/message.php +++ b/lang/ko/message.php @@ -122,6 +122,20 @@ 'deleted' => '자재가 삭제되었습니다.', ], + // 입고 관리 + 'receiving' => [ + 'fetched' => '입고를 조회했습니다.', + 'created' => '입고가 등록되었습니다.', + 'updated' => '입고가 수정되었습니다.', + 'deleted' => '입고가 삭제되었습니다.', + 'processed' => '입고처리가 완료되었습니다.', + ], + + // 재고 관리 + 'stock' => [ + 'fetched' => '재고를 조회했습니다.', + ], + // 단가 관리 'pricing' => [ 'created' => '단가가 등록되었습니다.', @@ -391,4 +405,27 @@ 'cancelled' => '휴가가 취소되었습니다.', 'granted' => '휴가가 부여되었습니다.', ], + + // 작업지시 관리 + 'work_order' => [ + 'fetched' => '작업지시를 조회했습니다.', + 'created' => '작업지시가 등록되었습니다.', + 'updated' => '작업지시가 수정되었습니다.', + 'deleted' => '작업지시가 삭제되었습니다.', + 'status_updated' => '작업지시 상태가 변경되었습니다.', + 'assigned' => '작업지시가 배정되었습니다.', + 'bending_toggled' => '벤딩 항목이 변경되었습니다.', + 'issue_added' => '이슈가 등록되었습니다.', + 'issue_resolved' => '이슈가 해결되었습니다.', + ], + + // 작업실적 관리 + 'work_result' => [ + 'fetched' => '작업실적을 조회했습니다.', + 'created' => '작업실적이 등록되었습니다.', + 'updated' => '작업실적이 수정되었습니다.', + 'deleted' => '작업실적이 삭제되었습니다.', + 'inspection_toggled' => '검사 상태가 변경되었습니다.', + 'packaging_toggled' => '포장 상태가 변경되었습니다.', + ], ]; diff --git a/routes/api.php b/routes/api.php index 742fe04..b4fbf94 100644 --- a/routes/api.php +++ b/routes/api.php @@ -38,6 +38,9 @@ use App\Http\Controllers\Api\V1\VendorLedgerController; use App\Http\Controllers\Api\V1\BankTransactionController; use App\Http\Controllers\Api\V1\CardTransactionController; +use App\Http\Controllers\Api\V1\ReceivablesController; +use App\Http\Controllers\Api\V1\DailyReportController; +use App\Http\Controllers\Api\V1\ComprehensiveAnalysisController; use App\Http\Controllers\Api\V1\EstimateController; use App\Http\Controllers\Api\V1\FileStorageController; use App\Http\Controllers\Api\V1\FolderController; @@ -56,6 +59,7 @@ use App\Http\Controllers\Api\V1\ItemsController; use App\Http\Controllers\Api\V1\ItemsFileController; use App\Http\Controllers\Api\V1\LeaveController; +use App\Http\Controllers\Api\V1\LeavePolicyController; use App\Http\Controllers\Api\V1\LoanController; use App\Http\Controllers\Api\V1\MenuController; use App\Http\Controllers\Api\V1\ModelSetController; @@ -96,6 +100,7 @@ use App\Http\Controllers\Api\V1\UserRoleController; use App\Http\Controllers\Api\V1\WithdrawalController; use App\Http\Controllers\Api\V1\WorkOrderController; +use App\Http\Controllers\Api\V1\WorkResultController; use App\Http\Controllers\Api\V1\WorkSettingController; use Illuminate\Support\Facades\Route; @@ -328,6 +333,10 @@ Route::post('/{id}/cancel', [LeaveController::class, 'cancel'])->name('v1.leaves.cancel'); }); + // Leave Policy API (휴가 정책) + Route::get('/leave-policy', [LeavePolicyController::class, 'show'])->name('v1.leave-policy.show'); + Route::put('/leave-policy', [LeavePolicyController::class, 'update'])->name('v1.leave-policy.update'); + // Approval Form API (결재 양식) Route::prefix('approval-forms')->group(function () { Route::get('', [ApprovalFormController::class, 'index'])->name('v1.approval-forms.index'); @@ -499,6 +508,22 @@ Route::get('/accounts', [BankTransactionController::class, 'accounts'])->name('v1.bank-transactions.accounts'); }); + // Receivables API (채권 현황) + Route::prefix('receivables')->group(function () { + Route::get('', [ReceivablesController::class, 'index'])->name('v1.receivables.index'); + Route::get('/summary', [ReceivablesController::class, 'summary'])->name('v1.receivables.summary'); + }); + + // Daily Report API (일일 보고서) + Route::prefix('daily-report')->group(function () { + Route::get('/note-receivables', [DailyReportController::class, 'noteReceivables'])->name('v1.daily-report.note-receivables'); + Route::get('/daily-accounts', [DailyReportController::class, 'dailyAccounts'])->name('v1.daily-report.daily-accounts'); + Route::get('/summary', [DailyReportController::class, 'summary'])->name('v1.daily-report.summary'); + }); + + // Comprehensive Analysis API (종합 분석 보고서) + Route::get('/comprehensive-analysis', [ComprehensiveAnalysisController::class, 'index'])->name('v1.comprehensive-analysis.index'); + // Plan API (요금제 관리) Route::prefix('plans')->group(function () { Route::get('', [PlanController::class, 'index'])->name('v1.plans.index'); @@ -1008,6 +1033,21 @@ Route::patch('/{id}/issues/{issueId}/resolve', [WorkOrderController::class, 'resolveIssue'])->whereNumber('id')->name('v1.work-orders.issues.resolve'); // 이슈 해결 }); + // 작업실적 관리 API (Production) + Route::prefix('work-results')->group(function () { + // 기본 CRUD + Route::get('', [WorkResultController::class, 'index'])->name('v1.work-results.index'); // 목록 + Route::get('/stats', [WorkResultController::class, 'stats'])->name('v1.work-results.stats'); // 통계 + Route::post('', [WorkResultController::class, 'store'])->name('v1.work-results.store'); // 생성 + Route::get('/{id}', [WorkResultController::class, 'show'])->whereNumber('id')->name('v1.work-results.show'); // 상세 + Route::put('/{id}', [WorkResultController::class, 'update'])->whereNumber('id')->name('v1.work-results.update'); // 수정 + Route::delete('/{id}', [WorkResultController::class, 'destroy'])->whereNumber('id')->name('v1.work-results.destroy'); // 삭제 + + // 상태 토글 + Route::patch('/{id}/inspection', [WorkResultController::class, 'toggleInspection'])->whereNumber('id')->name('v1.work-results.inspection'); // 검사 상태 토글 + Route::patch('/{id}/packaging', [WorkResultController::class, 'togglePackaging'])->whereNumber('id')->name('v1.work-results.packaging'); // 포장 상태 토글 + }); + // 파일 저장소 API Route::prefix('files')->group(function () { Route::post('/upload', [FileStorageController::class, 'upload'])->name('v1.files.upload'); // 파일 업로드 (임시)