From 1d7ef66d193cc8df46f52cf6a20bbcd313124ad7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B6=8C=ED=98=81=EC=84=B1?= Date: Wed, 11 Feb 2026 09:51:35 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=9E=91=EC=97=85=EC=9D=BC=EC=A7=80/?= =?UTF-8?q?=EC=A4=91=EA=B0=84=EA=B2=80=EC=82=AC=20=EC=84=A4=EC=A0=95?= =?UTF-8?q?=EC=9D=84=20ProcessStep=20=E2=86=92=20Process=20=EB=A0=88?= =?UTF-8?q?=EB=B2=A8=EB=A1=9C=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Process 모델에 document_template_id, needs_work_log, work_log_template_id 추가 - ProcessStep에서 해당 필드 제거 - WorkOrderService의 검사 관련 3개 메서드(getInspectionTemplate, resolveInspectionDocument, createInspectionDocument) 공정 레벨 참조로 변경 - ProcessService eager loading에 documentTemplate, workLogTemplateRelation 추가 - FormRequest 검증 규칙 이동 (ProcessStep → Process) Co-Authored-By: Claude Opus 4.6 --- .../V1/Process/StoreProcessRequest.php | 6 + .../V1/Process/UpdateProcessRequest.php | 6 + .../ProcessStep/StoreProcessStepRequest.php | 2 - .../ProcessStep/UpdateProcessStepRequest.php | 2 - app/Models/Process.php | 21 ++++ app/Models/ProcessStep.php | 10 -- app/Services/ProcessService.php | 10 +- app/Services/ProcessStepService.php | 4 +- app/Services/WorkOrderService.php | 109 ++++++++---------- ...1_add_work_log_fields_to_process_steps.php | 47 ++++++++ 10 files changed, 133 insertions(+), 84 deletions(-) create mode 100644 database/migrations/2026_02_10_100001_add_work_log_fields_to_process_steps.php diff --git a/app/Http/Requests/V1/Process/StoreProcessRequest.php b/app/Http/Requests/V1/Process/StoreProcessRequest.php index 1f2d4d3..d2794a8 100644 --- a/app/Http/Requests/V1/Process/StoreProcessRequest.php +++ b/app/Http/Requests/V1/Process/StoreProcessRequest.php @@ -19,6 +19,9 @@ public function rules(): array 'process_type' => ['required', 'string', 'in:생산,검사,포장,조립'], 'department' => ['nullable', 'string', 'max:100'], 'work_log_template' => ['nullable', 'string', 'max:100'], + 'document_template_id' => ['nullable', 'integer', 'exists:document_templates,id'], + 'needs_work_log' => ['nullable', 'boolean'], + 'work_log_template_id' => ['nullable', 'integer', 'exists:document_templates,id'], 'required_workers' => ['nullable', 'integer', 'min:1'], 'equipment_info' => ['nullable', 'string', 'max:255'], 'work_steps' => ['nullable'], @@ -48,6 +51,9 @@ public function attributes(): array 'process_type' => '공정구분', 'department' => '담당부서', 'work_log_template' => '작업일지 양식', + 'document_template_id' => '중간검사 양식', + 'needs_work_log' => '작업일지 여부', + 'work_log_template_id' => '작업일지 양식 ID', 'required_workers' => '필요인원', 'equipment_info' => '설비정보', 'work_steps' => '작업단계', diff --git a/app/Http/Requests/V1/Process/UpdateProcessRequest.php b/app/Http/Requests/V1/Process/UpdateProcessRequest.php index 9d103d1..fd96339 100644 --- a/app/Http/Requests/V1/Process/UpdateProcessRequest.php +++ b/app/Http/Requests/V1/Process/UpdateProcessRequest.php @@ -19,6 +19,9 @@ public function rules(): array 'process_type' => ['sometimes', 'required', 'string', 'in:생산,검사,포장,조립'], 'department' => ['nullable', 'string', 'max:100'], 'work_log_template' => ['nullable', 'string', 'max:100'], + 'document_template_id' => ['nullable', 'integer', 'exists:document_templates,id'], + 'needs_work_log' => ['nullable', 'boolean'], + 'work_log_template_id' => ['nullable', 'integer', 'exists:document_templates,id'], 'required_workers' => ['nullable', 'integer', 'min:1'], 'equipment_info' => ['nullable', 'string', 'max:255'], 'work_steps' => ['nullable'], @@ -48,6 +51,9 @@ public function attributes(): array 'process_type' => '공정구분', 'department' => '담당부서', 'work_log_template' => '작업일지 양식', + 'document_template_id' => '중간검사 양식', + 'needs_work_log' => '작업일지 여부', + 'work_log_template_id' => '작업일지 양식 ID', 'required_workers' => '필요인원', 'equipment_info' => '설비정보', 'work_steps' => '작업단계', diff --git a/app/Http/Requests/V1/ProcessStep/StoreProcessStepRequest.php b/app/Http/Requests/V1/ProcessStep/StoreProcessStepRequest.php index f7a261d..543bf95 100644 --- a/app/Http/Requests/V1/ProcessStep/StoreProcessStepRequest.php +++ b/app/Http/Requests/V1/ProcessStep/StoreProcessStepRequest.php @@ -18,7 +18,6 @@ public function rules(): array 'is_required' => ['nullable', 'boolean'], 'needs_approval' => ['nullable', 'boolean'], 'needs_inspection' => ['nullable', 'boolean'], - 'document_template_id' => ['nullable', 'integer', 'exists:document_templates,id'], 'is_active' => ['nullable', 'boolean'], 'connection_type' => ['nullable', 'string', 'max:20'], 'connection_target' => ['nullable', 'string', 'max:255'], @@ -33,7 +32,6 @@ public function attributes(): array 'is_required' => '필수여부', 'needs_approval' => '승인필요여부', 'needs_inspection' => '검사필요여부', - 'document_template_id' => '문서양식', 'is_active' => '사용여부', 'connection_type' => '연결유형', 'connection_target' => '연결대상', diff --git a/app/Http/Requests/V1/ProcessStep/UpdateProcessStepRequest.php b/app/Http/Requests/V1/ProcessStep/UpdateProcessStepRequest.php index 4103802..0fc3ca7 100644 --- a/app/Http/Requests/V1/ProcessStep/UpdateProcessStepRequest.php +++ b/app/Http/Requests/V1/ProcessStep/UpdateProcessStepRequest.php @@ -18,7 +18,6 @@ public function rules(): array 'is_required' => ['nullable', 'boolean'], 'needs_approval' => ['nullable', 'boolean'], 'needs_inspection' => ['nullable', 'boolean'], - 'document_template_id' => ['nullable', 'integer', 'exists:document_templates,id'], 'is_active' => ['nullable', 'boolean'], 'connection_type' => ['nullable', 'string', 'max:20'], 'connection_target' => ['nullable', 'string', 'max:255'], @@ -33,7 +32,6 @@ public function attributes(): array 'is_required' => '필수여부', 'needs_approval' => '승인필요여부', 'needs_inspection' => '검사필요여부', - 'document_template_id' => '문서양식', 'is_active' => '사용여부', 'connection_type' => '연결유형', 'connection_target' => '연결대상', diff --git a/app/Models/Process.php b/app/Models/Process.php index 0c222b4..23df3f0 100644 --- a/app/Models/Process.php +++ b/app/Models/Process.php @@ -7,6 +7,7 @@ use App\Traits\ModelTrait; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\SoftDeletes; @@ -26,6 +27,9 @@ class Process extends Model 'process_type', 'department', 'work_log_template', + 'document_template_id', + 'needs_work_log', + 'work_log_template_id', 'required_workers', 'equipment_info', 'work_steps', @@ -39,9 +43,26 @@ class Process extends Model protected $casts = [ 'work_steps' => 'array', 'is_active' => 'boolean', + 'needs_work_log' => 'boolean', 'required_workers' => 'integer', ]; + /** + * 중간검사 양식 + */ + public function documentTemplate(): BelongsTo + { + return $this->belongsTo(Documents\DocumentTemplate::class); + } + + /** + * 작업일지 양식 (관계명: work_log_template 컬럼과 충돌 방지) + */ + public function workLogTemplateRelation(): BelongsTo + { + return $this->belongsTo(Documents\DocumentTemplate::class, 'work_log_template_id'); + } + /** * 공정 자동 분류 규칙 (패턴 규칙) */ diff --git a/app/Models/ProcessStep.php b/app/Models/ProcessStep.php index bfc6137..953fda9 100644 --- a/app/Models/ProcessStep.php +++ b/app/Models/ProcessStep.php @@ -2,7 +2,6 @@ namespace App\Models; -use App\Models\Documents\DocumentTemplate; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; @@ -18,7 +17,6 @@ class ProcessStep extends Model 'is_required', 'needs_approval', 'needs_inspection', - 'document_template_id', 'is_active', 'sort_order', 'connection_type', @@ -41,12 +39,4 @@ public function process(): BelongsTo { return $this->belongsTo(Process::class); } - - /** - * 문서 양식 (검사 시 사용할 템플릿) - */ - public function documentTemplate(): BelongsTo - { - return $this->belongsTo(DocumentTemplate::class); - } } diff --git a/app/Services/ProcessService.php b/app/Services/ProcessService.php index 06e3688..7eb6499 100644 --- a/app/Services/ProcessService.php +++ b/app/Services/ProcessService.php @@ -25,7 +25,7 @@ public function index(array $params) $query = Process::query() ->where('tenant_id', $tenantId) - ->with(['classificationRules', 'processItems.item:id,code,name', 'steps']); + ->with(['classificationRules', 'processItems.item:id,code,name', 'steps', 'documentTemplate:id,name,category', 'workLogTemplateRelation:id,name,category']); // 검색어 if ($q !== '') { @@ -62,7 +62,7 @@ public function show(int $id) $tenantId = $this->tenantId(); $process = Process::where('tenant_id', $tenantId) - ->with(['classificationRules', 'processItems.item:id,code,name', 'steps']) + ->with(['classificationRules', 'processItems.item:id,code,name', 'steps', 'documentTemplate:id,name,category', 'workLogTemplateRelation:id,name,category']) ->find($id); if (! $process) { @@ -104,7 +104,7 @@ public function store(array $data) // 개별 품목 연결 $this->syncProcessItems($process, $itemIds); - return $process->load(['classificationRules', 'processItems.item:id,code,name', 'steps']); + return $process->load(['classificationRules', 'processItems.item:id,code,name', 'steps', 'documentTemplate:id,name,category', 'workLogTemplateRelation:id,name,category']); }); } @@ -145,7 +145,7 @@ public function update(int $id, array $data) $this->syncProcessItems($process, $itemIds); } - return $process->fresh(['classificationRules', 'processItems.item:id,code,name', 'steps']); + return $process->fresh(['classificationRules', 'processItems.item:id,code,name', 'steps', 'documentTemplate:id,name,category', 'workLogTemplateRelation:id,name,category']); }); } @@ -201,7 +201,7 @@ public function toggleActive(int $id) 'updated_by' => $userId, ]); - return $process->fresh(['classificationRules', 'processItems.item:id,code,name', 'steps']); + return $process->fresh(['classificationRules', 'processItems.item:id,code,name', 'steps', 'documentTemplate:id,name,category', 'workLogTemplateRelation:id,name,category']); } /** diff --git a/app/Services/ProcessStepService.php b/app/Services/ProcessStepService.php index 8402954..b0148b4 100644 --- a/app/Services/ProcessStepService.php +++ b/app/Services/ProcessStepService.php @@ -17,7 +17,7 @@ public function index(int $processId) $process = $this->findProcess($processId); return $process->steps() - ->with('documentTemplate:id,name,category') + ->select('*') ->orderBy('sort_order') ->get(); } @@ -30,7 +30,7 @@ public function show(int $processId, int $stepId) $this->findProcess($processId); $step = ProcessStep::where('process_id', $processId) - ->with('documentTemplate:id,name,category') + ->select('*') ->find($stepId); if (! $step) { throw new NotFoundHttpException(__('error.not_found')); diff --git a/app/Services/WorkOrderService.php b/app/Services/WorkOrderService.php index aa8896d..d8e2c92 100644 --- a/app/Services/WorkOrderService.php +++ b/app/Services/WorkOrderService.php @@ -55,7 +55,7 @@ public function index(array $params) 'salesOrder.client:id,name', 'process:id,process_name,process_code,department', 'items:id,work_order_id,item_id,item_name,specification,quantity,unit,status,options,sort_order,source_order_item_id', - 'items.sourceOrderItem:id,order_node_id', + 'items.sourceOrderItem:id,order_node_id,floor_code,symbol_code', 'items.sourceOrderItem.node:id,name,code', ]); @@ -210,7 +210,7 @@ public function show(int $id) 'salesOrder.writer:id,name', 'process:id,process_name,process_code,work_steps,department', 'process.steps' => fn ($q) => $q->where('is_active', true)->orderBy('sort_order'), - 'items.sourceOrderItem:id,order_node_id', + 'items.sourceOrderItem:id,order_node_id,floor_code,symbol_code', 'items.sourceOrderItem.node:id,name,code', 'bendingDetail', 'issues' => fn ($q) => $q->orderByDesc('created_at'), @@ -257,9 +257,22 @@ public function store(array $data) // 품목 저장: 직접 전달된 품목이 없고 수주 ID가 있으면 수주에서 복사 if (empty($items) && $salesOrderId) { - $salesOrder = \App\Models\Orders\Order::with('items')->find($salesOrderId); + $salesOrder = \App\Models\Orders\Order::with('items.node')->find($salesOrderId); if ($salesOrder && $salesOrder->items->isNotEmpty()) { foreach ($salesOrder->items as $index => $orderItem) { + // 수주 품목 + 노드에서 options 조합 + $nodeOptions = $orderItem->node?->options ?? []; + $options = array_filter([ + 'floor' => $orderItem->floor_code, + 'code' => $orderItem->symbol_code, + 'width' => $nodeOptions['width'] ?? $nodeOptions['open_width'] ?? null, + 'height' => $nodeOptions['height'] ?? $nodeOptions['open_height'] ?? null, + 'cutting_info' => $nodeOptions['cutting_info'] ?? null, + 'slat_info' => $nodeOptions['slat_info'] ?? null, + 'bending_info' => $nodeOptions['bending_info'] ?? null, + 'wip_info' => $nodeOptions['wip_info'] ?? null, + ], fn ($v) => $v !== null); + $workOrder->items()->create([ 'tenant_id' => $tenantId, 'source_order_item_id' => $orderItem->id, // 원본 수주 품목 추적용 @@ -269,6 +282,7 @@ public function store(array $data) 'quantity' => $orderItem->quantity, 'unit' => $orderItem->unit, 'sort_order' => $index, + 'options' => ! empty($options) ? $options : null, ]); } } @@ -1639,11 +1653,7 @@ public function getInspectionTemplate(int $workOrderId): array $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([ + 'process.documentTemplate' => fn ($q) => $q->with([ 'approvalLines', 'basicFields', 'sections.items', @@ -1659,8 +1669,10 @@ public function getInspectionTemplate(int $workOrderId): array throw new NotFoundHttpException(__('error.not_found')); } - $inspectionSteps = $workOrder->process?->steps ?? collect(); - if ($inspectionSteps->isEmpty()) { + $process = $workOrder->process; + $docTemplate = $process?->documentTemplate; + + if (! $docTemplate) { return [ 'work_order_id' => $workOrderId, 'has_template' => false, @@ -1671,28 +1683,17 @@ public function getInspectionTemplate(int $workOrderId): array } $documentService = app(DocumentService::class); - $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), - ]; - } + $formattedTemplate = $documentService->formatTemplateForReact($docTemplate); return [ 'work_order_id' => $workOrderId, - 'has_template' => ! empty($templates), - 'templates' => $templates, - // 하위호환: 첫 번째 템플릿 - 'template' => ! empty($templates) ? $templates[0]['template'] : null, + 'has_template' => true, + 'templates' => [[ + 'template_id' => $docTemplate->id, + 'template_name' => $docTemplate->name, + 'template' => $formattedTemplate, + ]], + 'template' => $formattedTemplate, 'work_order_info' => $this->buildWorkOrderInfo($workOrder), ]; } @@ -1709,11 +1710,7 @@ public function resolveInspectionDocument(int $workOrderId, array $params = []): $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([ + 'process.documentTemplate' => fn ($q) => $q->with([ 'approvalLines', 'basicFields', 'sections.items', @@ -1729,25 +1726,21 @@ public function resolveInspectionDocument(int $workOrderId, array $params = []): throw new NotFoundHttpException(__('error.not_found')); } - // step_id가 지정되면 해당 단계 사용, 아니면 첫 번째 검사 단계 - $inspectionSteps = $workOrder->process?->steps ?? collect(); - $stepId = $params['step_id'] ?? null; + $process = $workOrder->process; + $templateId = $process?->document_template_id; + $docTemplate = $process?->documentTemplate; - $inspectionStep = $stepId - ? $inspectionSteps->firstWhere('id', (int) $stepId) - : $inspectionSteps->first(); - - if (! $inspectionStep || ! $inspectionStep->document_template_id) { + if (! $templateId || ! $docTemplate) { throw new BadRequestHttpException(__('error.work_order.no_inspection_template')); } $documentService = app(DocumentService::class); - $formattedTemplate = $documentService->formatTemplateForReact($inspectionStep->documentTemplate); + $formattedTemplate = $documentService->formatTemplateForReact($docTemplate); // 기존 문서 조회 (work_order + template, 수정 가능한 상태) $existingDocument = Document::query() ->where('tenant_id', $tenantId) - ->where('template_id', $inspectionStep->document_template_id) + ->where('template_id', $templateId) ->where('linkable_type', 'work_order') ->where('linkable_id', $workOrderId) ->whereIn('status', [Document::STATUS_DRAFT, Document::STATUS_REJECTED]) @@ -1757,8 +1750,7 @@ public function resolveInspectionDocument(int $workOrderId, array $params = []): return [ 'work_order_id' => $workOrderId, - 'step_id' => $inspectionStep->id, - 'step_name' => $inspectionStep->step_name, + 'template_id' => $templateId, 'template' => $formattedTemplate, 'existing_document' => $existingDocument, 'work_order_info' => $this->buildWorkOrderInfo($workOrder), @@ -1768,7 +1760,7 @@ public function resolveInspectionDocument(int $workOrderId, array $params = []): /** * 검사 완료 시 Document + DocumentData 생성 * - * step_id 지정 시 해당 단계의 템플릿 사용, 미지정 시 첫 번째 검사 단계 사용. + * 공정(Process) 레벨의 document_template_id를 사용. * 기존 DRAFT/REJECTED 문서가 있으면 update, 없으면 create. */ public function createInspectionDocument(int $workOrderId, array $inspectionData): array @@ -1777,27 +1769,18 @@ public function createInspectionDocument(int $workOrderId, array $inspectionData $userId = $this->apiUserId(); $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'), - ]) + ->with(['process']) ->find($workOrderId); if (! $workOrder) { throw new NotFoundHttpException(__('error.not_found')); } - // step_id가 지정되면 해당 단계 사용, 아니면 첫 번째 - $inspectionSteps = $workOrder->process?->steps ?? collect(); - $stepId = $inspectionData['step_id'] ?? null; + // 공정 레벨의 중간검사 양식 사용 + $process = $workOrder->process; + $templateId = $process?->document_template_id; - $inspectionStep = $stepId - ? $inspectionSteps->firstWhere('id', (int) $stepId) - : $inspectionSteps->first(); - - if (! $inspectionStep || ! $inspectionStep->document_template_id) { + if (! $templateId) { throw new BadRequestHttpException(__('error.work_order.no_inspection_template')); } @@ -1806,7 +1789,7 @@ public function createInspectionDocument(int $workOrderId, array $inspectionData // 기존 DRAFT/REJECTED 문서가 있으면 update $existingDocument = Document::query() ->where('tenant_id', $tenantId) - ->where('template_id', $inspectionStep->document_template_id) + ->where('template_id', $templateId) ->where('linkable_type', 'work_order') ->where('linkable_id', $workOrderId) ->whereIn('status', [Document::STATUS_DRAFT, Document::STATUS_REJECTED]) @@ -1822,7 +1805,7 @@ public function createInspectionDocument(int $workOrderId, array $inspectionData $action = 'inspection_document_updated'; } else { $documentData = [ - 'template_id' => $inspectionStep->document_template_id, + 'template_id' => $templateId, 'title' => $inspectionData['title'] ?? "중간검사성적서 - {$workOrder->work_order_no}", 'linkable_type' => 'work_order', 'linkable_id' => $workOrderId, diff --git a/database/migrations/2026_02_10_100001_add_work_log_fields_to_process_steps.php b/database/migrations/2026_02_10_100001_add_work_log_fields_to_process_steps.php new file mode 100644 index 0000000..db8e719 --- /dev/null +++ b/database/migrations/2026_02_10_100001_add_work_log_fields_to_process_steps.php @@ -0,0 +1,47 @@ +foreignId('document_template_id')->nullable()->after('work_log_template') + ->constrained('document_templates')->nullOnDelete()->comment('중간검사 양식 ID'); + $table->boolean('needs_work_log')->default(false)->after('document_template_id')->comment('작업일지 여부'); + $table->foreignId('work_log_template_id')->nullable()->after('needs_work_log') + ->constrained('document_templates')->nullOnDelete()->comment('작업일지 양식 ID'); + }); + + // 2. ProcessStep에서 document_template_id 제거 + Schema::table('process_steps', function (Blueprint $table) { + $table->dropForeign(['document_template_id']); + $table->dropColumn('document_template_id'); + }); + } + + public function down(): void + { + // ProcessStep에 document_template_id 복원 + Schema::table('process_steps', function (Blueprint $table) { + $table->foreignId('document_template_id')->nullable()->after('needs_inspection') + ->constrained('document_templates')->nullOnDelete(); + }); + + // Process에서 필드 제거 + Schema::table('processes', function (Blueprint $table) { + $table->dropForeign(['work_log_template_id']); + $table->dropForeign(['document_template_id']); + $table->dropColumn(['work_log_template_id', 'needs_work_log', 'document_template_id']); + }); + } +};