From 6733a431bb5e531aab77090d46384af595b0edb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B6=8C=ED=98=81=EC=84=B1?= Date: Tue, 10 Feb 2026 16:55:29 +0900 Subject: [PATCH] =?UTF-8?q?feat:=EC=A4=91=EA=B0=84=EA=B2=80=EC=82=AC=20API?= =?UTF-8?q?=20=EB=8B=A4=EC=A4=91=EB=8B=A8=EA=B3=84/resolve/upsert=20?= =?UTF-8?q?=EC=A7=80=EC=9B=90=20(Phase=205.1.3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - getInspectionTemplate: 전체 검사 단계 templates[] 반환 (기존 첫번째만→다중) - resolveInspectionDocument 신규: step_id 기반 기존 문서 조회 또는 템플릿 반환 - createInspectionDocument 개선: step_id 파라미터, 기존 DRAFT/REJECTED 문서 update 지원 - GET inspection-resolve 라우트 추가 Co-Authored-By: Claude Opus 4.6 --- .../Api/V1/WorkOrderController.php | 12 +- app/Services/WorkOrderService.php | 159 +++++++++++++++--- routes/api/v1/production.php | 3 +- 3 files changed, 144 insertions(+), 30 deletions(-) diff --git a/app/Http/Controllers/Api/V1/WorkOrderController.php b/app/Http/Controllers/Api/V1/WorkOrderController.php index e9e386a..640f29c 100644 --- a/app/Http/Controllers/Api/V1/WorkOrderController.php +++ b/app/Http/Controllers/Api/V1/WorkOrderController.php @@ -230,7 +230,17 @@ public function inspectionTemplate(int $id) } /** - * 검사 완료 시 검사 문서(Document) 생성 + * 검사 문서 resolve (기존 문서 조회 또는 생성 정보 반환) + */ + public function resolveInspectionDocument(Request $request, int $id) + { + return ApiResponse::handle(function () use ($request, $id) { + return $this->service->resolveInspectionDocument($id, $request->all()); + }, __('message.fetched')); + } + + /** + * 검사 완료 시 검사 문서(Document) 생성/수정 */ public function createInspectionDocument(Request $request, int $id) { diff --git a/app/Services/WorkOrderService.php b/app/Services/WorkOrderService.php index 3d5ba14..aa8896d 100644 --- a/app/Services/WorkOrderService.php +++ b/app/Services/WorkOrderService.php @@ -2,6 +2,7 @@ namespace App\Services; +use App\Models\Documents\Document; use App\Models\Documents\DocumentTemplate; use App\Models\Orders\Order; use App\Models\Production\WorkOrder; @@ -1630,6 +1631,7 @@ public function getInspectionData(int $workOrderId, array $params = []): array * 작업지시의 검사용 문서 템플릿 조회 * * work_order → process → steps(needs_inspection=true) → documentTemplate 로드 + * 모든 검사 단계의 템플릿을 반환 (다중 검사 단계 지원) */ public function getInspectionTemplate(int $workOrderId): array { @@ -1662,38 +1664,112 @@ public function getInspectionTemplate(int $workOrderId): array return [ 'work_order_id' => $workOrderId, 'has_template' => false, + 'templates' => [], 'template' => null, 'work_order_info' => $this->buildWorkOrderInfo($workOrder), ]; } - // 첫 번째 검사 단계의 템플릿 사용 (향후 다중 검사 단계 지원 가능) - $inspectionStep = $inspectionSteps->first(); - $template = $inspectionStep->documentTemplate; - - if (! $template) { - return [ - 'work_order_id' => $workOrderId, - 'has_template' => false, - 'template' => null, - 'work_order_info' => $this->buildWorkOrderInfo($workOrder), - ]; - } - - // DocumentService의 formatTemplateForReact와 동일한 포맷 $documentService = app(DocumentService::class); - $formattedTemplate = $documentService->formatTemplateForReact($template); + $templates = []; + + foreach ($inspectionSteps as $step) { + if (! $step->documentTemplate) { + continue; + } + + $templates[] = [ + 'step_id' => $step->id, + 'step_name' => $step->step_name, + 'step_code' => $step->step_code, + 'sort_order' => $step->sort_order, + 'template' => $documentService->formatTemplateForReact($step->documentTemplate), + ]; + } return [ 'work_order_id' => $workOrderId, - 'has_template' => true, + 'has_template' => ! empty($templates), + 'templates' => $templates, + // 하위호환: 첫 번째 템플릿 + 'template' => ! empty($templates) ? $templates[0]['template'] : null, + 'work_order_info' => $this->buildWorkOrderInfo($workOrder), + ]; + } + + /** + * 작업지시 검사 문서 resolve (기존 문서 조회 또는 생성 정보 반환) + * + * step_id 기반으로 해당 검사 단계의 템플릿과 기존 문서를 조회. + * 기존 DRAFT/REJECTED 문서가 있으면 반환, 없으면 template만 반환. + */ + public function resolveInspectionDocument(int $workOrderId, array $params = []): array + { + $tenantId = $this->tenantId(); + + $workOrder = WorkOrder::where('tenant_id', $tenantId) + ->with([ + 'process.steps' => fn ($q) => $q->where('is_active', true) + ->where('needs_inspection', true) + ->whereNotNull('document_template_id') + ->orderBy('sort_order'), + 'process.steps.documentTemplate' => fn ($q) => $q->with([ + 'approvalLines', + 'basicFields', + 'sections.items', + 'columns', + 'sectionFields', + ]), + 'salesOrder:id,order_no,client_name,site_name', + 'items:id,work_order_id,item_name,specification,quantity,unit,sort_order', + ]) + ->find($workOrderId); + + if (! $workOrder) { + throw new NotFoundHttpException(__('error.not_found')); + } + + // step_id가 지정되면 해당 단계 사용, 아니면 첫 번째 검사 단계 + $inspectionSteps = $workOrder->process?->steps ?? collect(); + $stepId = $params['step_id'] ?? null; + + $inspectionStep = $stepId + ? $inspectionSteps->firstWhere('id', (int) $stepId) + : $inspectionSteps->first(); + + if (! $inspectionStep || ! $inspectionStep->document_template_id) { + throw new BadRequestHttpException(__('error.work_order.no_inspection_template')); + } + + $documentService = app(DocumentService::class); + $formattedTemplate = $documentService->formatTemplateForReact($inspectionStep->documentTemplate); + + // 기존 문서 조회 (work_order + template, 수정 가능한 상태) + $existingDocument = Document::query() + ->where('tenant_id', $tenantId) + ->where('template_id', $inspectionStep->document_template_id) + ->where('linkable_type', 'work_order') + ->where('linkable_id', $workOrderId) + ->whereIn('status', [Document::STATUS_DRAFT, Document::STATUS_REJECTED]) + ->with(['data', 'attachments.file', 'approvals.user:id,name']) + ->latest() + ->first(); + + return [ + 'work_order_id' => $workOrderId, + 'step_id' => $inspectionStep->id, + 'step_name' => $inspectionStep->step_name, 'template' => $formattedTemplate, + 'existing_document' => $existingDocument, 'work_order_info' => $this->buildWorkOrderInfo($workOrder), ]; } /** * 검사 완료 시 Document + DocumentData 생성 + * + * step_id 지정 시 해당 단계의 템플릿 사용, 미지정 시 첫 번째 검사 단계 사용. + * 기존 DRAFT/REJECTED 문서가 있으면 update, 없으면 create. */ public function createInspectionDocument(int $workOrderId, array $inspectionData): array { @@ -1713,31 +1789,57 @@ public function createInspectionDocument(int $workOrderId, array $inspectionData throw new NotFoundHttpException(__('error.not_found')); } - $inspectionStep = $workOrder->process?->steps?->first(); + // step_id가 지정되면 해당 단계 사용, 아니면 첫 번째 + $inspectionSteps = $workOrder->process?->steps ?? collect(); + $stepId = $inspectionData['step_id'] ?? null; + + $inspectionStep = $stepId + ? $inspectionSteps->firstWhere('id', (int) $stepId) + : $inspectionSteps->first(); + if (! $inspectionStep || ! $inspectionStep->document_template_id) { throw new BadRequestHttpException(__('error.work_order.no_inspection_template')); } $documentService = app(DocumentService::class); - // DocumentService::create() 재사용 - $documentData = [ - 'template_id' => $inspectionStep->document_template_id, - 'title' => $inspectionData['title'] ?? "중간검사성적서 - {$workOrder->work_order_no}", - 'linkable_type' => 'work_order', - 'linkable_id' => $workOrderId, - 'data' => $inspectionData['data'] ?? [], - 'approvers' => $inspectionData['approvers'] ?? [], - ]; + // 기존 DRAFT/REJECTED 문서가 있으면 update + $existingDocument = Document::query() + ->where('tenant_id', $tenantId) + ->where('template_id', $inspectionStep->document_template_id) + ->where('linkable_type', 'work_order') + ->where('linkable_id', $workOrderId) + ->whereIn('status', [Document::STATUS_DRAFT, Document::STATUS_REJECTED]) + ->latest() + ->first(); - $document = $documentService->create($documentData); + if ($existingDocument) { + $document = $documentService->update($existingDocument->id, [ + 'title' => $inspectionData['title'] ?? $existingDocument->title, + 'data' => $inspectionData['data'] ?? [], + ]); + + $action = 'inspection_document_updated'; + } else { + $documentData = [ + 'template_id' => $inspectionStep->document_template_id, + 'title' => $inspectionData['title'] ?? "중간검사성적서 - {$workOrder->work_order_no}", + 'linkable_type' => 'work_order', + 'linkable_id' => $workOrderId, + 'data' => $inspectionData['data'] ?? [], + 'approvers' => $inspectionData['approvers'] ?? [], + ]; + + $document = $documentService->create($documentData); + $action = 'inspection_document_created'; + } // 감사 로그 $this->auditLogger->log( $tenantId, self::AUDIT_TARGET, $workOrderId, - 'inspection_document_created', + $action, null, ['document_id' => $document->id, 'document_no' => $document->document_no] ); @@ -1746,6 +1848,7 @@ public function createInspectionDocument(int $workOrderId, array $inspectionData 'document_id' => $document->id, 'document_no' => $document->document_no, 'status' => $document->status, + 'is_new' => $action === 'inspection_document_created', ]; } diff --git a/routes/api/v1/production.php b/routes/api/v1/production.php index b970abc..526cef3 100644 --- a/routes/api/v1/production.php +++ b/routes/api/v1/production.php @@ -77,7 +77,8 @@ 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'); // 검사 성적서 조회 Route::get('/{id}/inspection-template', [WorkOrderController::class, 'inspectionTemplate'])->whereNumber('id')->name('v1.work-orders.inspection-template'); // 검사 문서 템플릿 조회 - Route::post('/{id}/inspection-document', [WorkOrderController::class, 'createInspectionDocument'])->whereNumber('id')->name('v1.work-orders.inspection-document'); // 검사 문서 생성 + Route::get('/{id}/inspection-resolve', [WorkOrderController::class, 'resolveInspectionDocument'])->whereNumber('id')->name('v1.work-orders.inspection-resolve'); // 검사 문서 resolve (기존 문서/템플릿) + Route::post('/{id}/inspection-document', [WorkOrderController::class, 'createInspectionDocument'])->whereNumber('id')->name('v1.work-orders.inspection-document'); // 검사 문서 생성/수정 }); // Work Result API (작업실적 관리)