From ee6794be1a62e3ae027a06c658f4f84e20b6878c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B6=8C=ED=98=81=EC=84=B1?= Date: Mon, 9 Feb 2026 09:00:57 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20[=EC=83=9D=EC=82=B0=EA=B4=80=EB=A6=AC]?= =?UTF-8?q?=20=EC=A4=91=EA=B0=84=EA=B2=80=EC=82=AC=20=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=84=B0=20=EC=A0=80=EC=9E=A5/=EC=A1=B0=ED=9A=8C=20API=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - POST /work-orders/{id}/items/{itemId}/inspection: 품목별 검사 데이터 저장 - GET /work-orders/{id}/inspection-data: 전체 품목 검사 데이터 조회 - GET /work-orders/{id}/inspection-report: 검사 성적서용 데이터 조회 - WorkOrderItem 모델에 getInspectionData/setInspectionData 헬퍼 추가 - StoreItemInspectionRequest FormRequest 생성 - work_order_items.options['inspection_data']에 검사 결과 저장 Co-Authored-By: Claude Opus 4.6 --- .../Api/V1/WorkOrderController.php | 31 ++++ .../WorkOrder/StoreItemInspectionRequest.php | 51 ++++++ app/Models/Production/WorkOrderItem.php | 18 +++ app/Services/WorkOrderService.php | 147 ++++++++++++++++++ lang/ko/message.php | 1 + routes/api/v1/production.php | 5 + 6 files changed, 253 insertions(+) create mode 100644 app/Http/Requests/WorkOrder/StoreItemInspectionRequest.php diff --git a/app/Http/Controllers/Api/V1/WorkOrderController.php b/app/Http/Controllers/Api/V1/WorkOrderController.php index 68b9489..11fa921 100644 --- a/app/Http/Controllers/Api/V1/WorkOrderController.php +++ b/app/Http/Controllers/Api/V1/WorkOrderController.php @@ -4,6 +4,7 @@ use App\Helpers\ApiResponse; use App\Http\Controllers\Controller; +use App\Http\Requests\WorkOrder\StoreItemInspectionRequest; use App\Http\Requests\WorkOrder\WorkOrderAssignRequest; use App\Http\Requests\WorkOrder\WorkOrderIssueRequest; use App\Http\Requests\WorkOrder\WorkOrderStatusRequest; @@ -187,4 +188,34 @@ public function materialInputHistory(int $id) return $this->service->getMaterialInputHistory($id); }, __('message.work_order.fetched')); } + + /** + * 품목별 중간검사 데이터 저장 + */ + public function storeItemInspection(StoreItemInspectionRequest $request, int $workOrderId, int $itemId) + { + return ApiResponse::handle(function () use ($request, $workOrderId, $itemId) { + return $this->service->storeItemInspection($workOrderId, $itemId, $request->validated()); + }, __('message.work_order.inspection_saved')); + } + + /** + * 작업지시 전체 품목 검사 데이터 조회 + */ + public function inspectionData(Request $request, int $id) + { + return ApiResponse::handle(function () use ($request, $id) { + return $this->service->getInspectionData($id, $request->all()); + }, __('message.work_order.fetched')); + } + + /** + * 작업지시 검사 성적서용 데이터 조회 + */ + public function inspectionReport(int $id) + { + return ApiResponse::handle(function () use ($id) { + return $this->service->getInspectionReport($id); + }, __('message.work_order.fetched')); + } } diff --git a/app/Http/Requests/WorkOrder/StoreItemInspectionRequest.php b/app/Http/Requests/WorkOrder/StoreItemInspectionRequest.php new file mode 100644 index 0000000..ef74550 --- /dev/null +++ b/app/Http/Requests/WorkOrder/StoreItemInspectionRequest.php @@ -0,0 +1,51 @@ + ['required', 'string', Rule::in([ + 'screen', 'slat', 'slat_jointbar', 'bending', 'bending_wip', + ])], + 'inspection_data' => 'required|array', + 'inspection_data.productName' => 'nullable|string|max:200', + 'inspection_data.specification' => 'nullable|string|max:200', + 'inspection_data.bendingStatus' => ['nullable', Rule::in(['good', 'bad'])], + 'inspection_data.processingStatus' => ['nullable', Rule::in(['good', 'bad'])], + 'inspection_data.sewingStatus' => ['nullable', Rule::in(['good', 'bad'])], + 'inspection_data.assemblyStatus' => ['nullable', Rule::in(['good', 'bad'])], + 'inspection_data.length' => 'nullable|numeric', + 'inspection_data.width' => 'nullable|numeric', + 'inspection_data.height1' => 'nullable|numeric', + 'inspection_data.height2' => 'nullable|numeric', + 'inspection_data.length3' => 'nullable|numeric', + 'inspection_data.gap4' => 'nullable|numeric', + 'inspection_data.gapStatus' => ['nullable', Rule::in(['ok', 'ng'])], + 'inspection_data.gapPoints' => 'nullable|array', + 'inspection_data.gapPoints.*.left' => 'nullable|numeric', + 'inspection_data.gapPoints.*.right' => 'nullable|numeric', + 'inspection_data.judgment' => ['nullable', Rule::in(['pass', 'fail'])], + 'inspection_data.nonConformingContent' => 'nullable|string|max:1000', + ]; + } + + public function messages(): array + { + return [ + 'process_type.required' => __('validation.required', ['attribute' => '공정 유형']), + 'process_type.in' => __('validation.in', ['attribute' => '공정 유형']), + 'inspection_data.required' => __('validation.required', ['attribute' => '검사 데이터']), + ]; + } +} diff --git a/app/Models/Production/WorkOrderItem.php b/app/Models/Production/WorkOrderItem.php index 39bf08e..ccf3143 100644 --- a/app/Models/Production/WorkOrderItem.php +++ b/app/Models/Production/WorkOrderItem.php @@ -113,6 +113,24 @@ public function scopeHasResult($query) // 헬퍼 메서드 // ────────────────────────────────────────────────────────────── + /** + * 중간검사 데이터 가져오기 + */ + public function getInspectionData(): ?array + { + return $this->options['inspection_data'] ?? null; + } + + /** + * 중간검사 데이터 설정 + */ + public function setInspectionData(array $data): void + { + $options = $this->options ?? []; + $options['inspection_data'] = $data; + $this->options = $options; + } + /** * 작업 결과 데이터 가져오기 */ diff --git a/app/Services/WorkOrderService.php b/app/Services/WorkOrderService.php index 6ab3be8..62d44aa 100644 --- a/app/Services/WorkOrderService.php +++ b/app/Services/WorkOrderService.php @@ -1519,4 +1519,151 @@ public function getMaterialInputHistory(int $workOrderId): array ]; })->toArray(); } + + // ────────────────────────────────────────────────────────────── + // 중간검사 관련 + // ────────────────────────────────────────────────────────────── + + /** + * 품목별 중간검사 데이터 저장 + */ + public function storeItemInspection(int $workOrderId, int $itemId, array $data): array + { + $tenantId = $this->tenantId(); + $userId = $this->apiUserId(); + + $workOrder = WorkOrder::where('tenant_id', $tenantId)->find($workOrderId); + if (! $workOrder) { + throw new NotFoundHttpException(__('error.not_found')); + } + + $item = $workOrder->items()->find($itemId); + if (! $item) { + throw new NotFoundHttpException(__('error.not_found')); + } + + $beforeData = $item->getInspectionData(); + + $inspectionData = $data['inspection_data']; + $inspectionData['process_type'] = $data['process_type']; + $inspectionData['inspected_at'] = now()->toDateTimeString(); + $inspectionData['inspected_by'] = $userId; + + $item->setInspectionData($inspectionData); + $item->save(); + + // 감사 로그 + $this->auditLogger->log( + $tenantId, + self::AUDIT_TARGET, + $workOrderId, + 'item_inspection_saved', + ['item_id' => $itemId, 'inspection_data' => $beforeData], + ['item_id' => $itemId, 'inspection_data' => $inspectionData] + ); + + return [ + 'item_id' => $item->id, + 'inspection_data' => $item->getInspectionData(), + ]; + } + + /** + * 작업지시의 전체 품목 검사 데이터 조회 + */ + public function getInspectionData(int $workOrderId, array $params = []): array + { + $tenantId = $this->tenantId(); + + $workOrder = WorkOrder::where('tenant_id', $tenantId)->find($workOrderId); + if (! $workOrder) { + throw new NotFoundHttpException(__('error.not_found')); + } + + $query = $workOrder->items()->ordered(); + + // 공정 유형 필터 + if (! empty($params['process_type'])) { + $query->where('options->inspection_data->process_type', $params['process_type']); + } + + $items = $query->get(); + + $inspectionMap = []; + foreach ($items as $item) { + $inspectionData = $item->getInspectionData(); + if ($inspectionData) { + $inspectionMap[$item->id] = [ + 'item_id' => $item->id, + 'item_name' => $item->item_name, + 'specification' => $item->specification, + 'quantity' => $item->quantity, + 'sort_order' => $item->sort_order, + 'options' => $item->options, + 'inspection_data' => $inspectionData, + ]; + } + } + + return [ + 'work_order_id' => $workOrderId, + 'items' => array_values($inspectionMap), + 'total' => count($inspectionMap), + ]; + } + + /** + * 작업지시 검사 성적서용 데이터 조회 (전체 품목 + 검사 데이터 + 주문 정보) + */ + public function getInspectionReport(int $workOrderId): array + { + $tenantId = $this->tenantId(); + + $workOrder = WorkOrder::where('tenant_id', $tenantId) + ->with(['order', 'items' => function ($q) { + $q->ordered(); + }]) + ->find($workOrderId); + + if (! $workOrder) { + throw new NotFoundHttpException(__('error.not_found')); + } + + $items = $workOrder->items->map(function ($item) { + return [ + 'id' => $item->id, + 'item_name' => $item->item_name, + 'specification' => $item->specification, + 'quantity' => $item->quantity, + 'sort_order' => $item->sort_order, + 'status' => $item->status, + 'options' => $item->options, + 'inspection_data' => $item->getInspectionData(), + ]; + }); + + return [ + 'work_order' => [ + 'id' => $workOrder->id, + 'order_no' => $workOrder->order_no, + 'status' => $workOrder->status, + 'planned_date' => $workOrder->planned_date, + 'due_date' => $workOrder->due_date, + ], + 'order' => $workOrder->order ? [ + 'id' => $workOrder->order->id, + 'order_no' => $workOrder->order->order_no, + 'client_name' => $workOrder->order->client_name ?? null, + 'site_name' => $workOrder->order->site_name ?? null, + 'order_date' => $workOrder->order->order_date ?? null, + ] : null, + 'items' => $items, + 'summary' => [ + 'total_items' => $items->count(), + 'inspected_items' => $items->filter(fn ($i) => $i['inspection_data'] !== null)->count(), + 'passed_items' => $items->filter(fn ($i) => ($i['inspection_data']['judgment'] ?? null) === 'pass')->count(), + 'failed_items' => $items->filter(fn ($i) => ($i['inspection_data']['judgment'] ?? null) === 'fail')->count(), + ], + ]; + } } diff --git a/lang/ko/message.php b/lang/ko/message.php index 281b02a..b5e025f 100644 --- a/lang/ko/message.php +++ b/lang/ko/message.php @@ -438,6 +438,7 @@ 'item_status_updated' => '품목 상태가 변경되었습니다.', 'materials_fetched' => '자재 목록을 조회했습니다.', 'material_input_registered' => '자재 투입이 등록되었습니다.', + 'inspection_saved' => '검사 데이터가 저장되었습니다.', ], // 검사 관리 diff --git a/routes/api/v1/production.php b/routes/api/v1/production.php index 9bcc74e..2f9e961 100644 --- a/routes/api/v1/production.php +++ b/routes/api/v1/production.php @@ -71,6 +71,11 @@ // 공정 단계 진행 관리 Route::get('/{id}/step-progress', [WorkOrderController::class, 'stepProgress'])->whereNumber('id')->name('v1.work-orders.step-progress'); // 단계 진행 조회 Route::patch('/{id}/step-progress/{progressId}/toggle', [WorkOrderController::class, 'toggleStepProgress'])->whereNumber('id')->whereNumber('progressId')->name('v1.work-orders.step-progress.toggle'); // 단계 토글 + + // 중간검사 관리 + Route::post('/{id}/items/{itemId}/inspection', [WorkOrderController::class, 'storeItemInspection'])->whereNumber('id')->whereNumber('itemId')->name('v1.work-orders.items.inspection'); // 품목 검사 저장 + Route::get('/{id}/inspection-data', [WorkOrderController::class, 'inspectionData'])->whereNumber('id')->name('v1.work-orders.inspection-data'); // 검사 데이터 조회 + Route::get('/{id}/inspection-report', [WorkOrderController::class, 'inspectionReport'])->whereNumber('id')->name('v1.work-orders.inspection-report'); // 검사 성적서 조회 }); // Work Result API (작업실적 관리)