diff --git a/app/Http/Requests/Quality/QualityDocumentStoreRequest.php b/app/Http/Requests/Quality/QualityDocumentStoreRequest.php index ae450f4..14e8cd8 100644 --- a/app/Http/Requests/Quality/QualityDocumentStoreRequest.php +++ b/app/Http/Requests/Quality/QualityDocumentStoreRequest.php @@ -31,6 +31,8 @@ public function rules(): array 'options.material_distributor' => ['nullable', 'array'], 'options.contractor' => ['nullable', 'array'], 'options.supervisor' => ['nullable', 'array'], + 'order_ids' => ['nullable', 'array'], + 'order_ids.*' => ['integer', 'exists:orders,id'], ]; } diff --git a/app/Http/Requests/Quality/QualityDocumentUpdateRequest.php b/app/Http/Requests/Quality/QualityDocumentUpdateRequest.php index 58c7a93..763d607 100644 --- a/app/Http/Requests/Quality/QualityDocumentUpdateRequest.php +++ b/app/Http/Requests/Quality/QualityDocumentUpdateRequest.php @@ -31,6 +31,14 @@ public function rules(): array 'options.material_distributor' => ['nullable', 'array'], 'options.contractor' => ['nullable', 'array'], 'options.supervisor' => ['nullable', 'array'], + 'order_ids' => ['nullable', 'array'], + 'order_ids.*' => ['integer', 'exists:orders,id'], + 'locations' => ['nullable', 'array'], + 'locations.*.id' => ['required', 'integer'], + 'locations.*.post_width' => ['nullable', 'integer'], + 'locations.*.post_height' => ['nullable', 'integer'], + 'locations.*.change_reason' => ['nullable', 'string', 'max:500'], + 'locations.*.inspection_data' => ['nullable', 'array'], ]; } } diff --git a/app/Models/Qualitys/QualityDocumentLocation.php b/app/Models/Qualitys/QualityDocumentLocation.php index 1f7cecd..311ed9d 100644 --- a/app/Models/Qualitys/QualityDocumentLocation.php +++ b/app/Models/Qualitys/QualityDocumentLocation.php @@ -21,10 +21,15 @@ class QualityDocumentLocation extends Model 'post_width', 'post_height', 'change_reason', + 'inspection_data', 'document_id', 'inspection_status', ]; + protected $casts = [ + 'inspection_data' => 'array', + ]; + public function qualityDocument() { return $this->belongsTo(QualityDocument::class); diff --git a/app/Services/QualityDocumentService.php b/app/Services/QualityDocumentService.php index bfcaeb2..1d2336a 100644 --- a/app/Services/QualityDocumentService.php +++ b/app/Services/QualityDocumentService.php @@ -4,6 +4,7 @@ use App\Models\Orders\Order; use App\Models\Orders\OrderItem; +use App\Models\Orders\OrderNode; use App\Models\Qualitys\PerformanceReport; use App\Models\Qualitys\QualityDocument; use App\Models\Qualitys\QualityDocumentLocation; @@ -178,6 +179,10 @@ public function store(array $data) $userId = $this->apiUserId(); return DB::transaction(function () use ($data, $tenantId, $userId) { + // order_ids는 별도 처리 후 $data에서 제거 + $orderIds = $data['order_ids'] ?? null; + unset($data['order_ids']); + $data['tenant_id'] = $tenantId; $data['quality_doc_number'] = QualityDocument::generateDocNumber($tenantId); $data['status'] = QualityDocument::STATUS_RECEIVED; @@ -185,6 +190,11 @@ public function store(array $data) $doc = QualityDocument::create($data); + // 수주 연결 + if (! empty($orderIds)) { + $this->syncOrders($doc, $orderIds, $tenantId); + } + $this->auditLogger->log( $tenantId, self::AUDIT_TARGET, @@ -194,7 +204,7 @@ public function store(array $data) $doc->toArray() ); - return $this->transformToFrontend($doc->load(['client', 'inspector:id,name', 'creator:id,name'])); + return $this->transformToFrontend($doc->load(['client', 'inspector:id,name', 'creator:id,name', 'documentOrders.order', 'locations.orderItem.node'])); }); } @@ -213,7 +223,12 @@ public function update(int $id, array $data) $beforeData = $doc->toArray(); - return DB::transaction(function () use ($doc, $data, $userId, $beforeData) { + return DB::transaction(function () use ($doc, $data, $userId, $beforeData, $tenantId) { + // order_ids, locations는 별도 처리 후 $data에서 제거 + $orderIds = $data['order_ids'] ?? null; + $locations = $data['locations'] ?? null; + unset($data['order_ids'], $data['locations']); + $data['updated_by'] = $userId; // options는 기존 값과 병합 @@ -224,6 +239,16 @@ public function update(int $id, array $data) $doc->update($data); + // 수주 동기화 (order_ids가 전달된 경우만) + if ($orderIds !== null) { + $this->syncOrders($doc, $orderIds, $tenantId); + } + + // 개소별 데이터 업데이트 (시공규격, 변경사유, 검사데이터) + if (! empty($locations)) { + $this->updateLocations($doc->id, $locations); + } + $this->auditLogger->log( $doc->tenant_id, self::AUDIT_TARGET, @@ -233,7 +258,7 @@ public function update(int $id, array $data) $doc->fresh()->toArray() ); - return $this->transformToFrontend($doc->load(['client', 'inspector:id,name', 'creator:id,name', 'documentOrders.order', 'locations'])); + return $this->transformToFrontend($doc->load(['client', 'inspector:id,name', 'creator:id,name', 'documentOrders.order', 'locations.orderItem.node'])); }); } @@ -372,6 +397,101 @@ public function availableOrders(array $params): array ->toArray(); } + /** + * 개소별 데이터 업데이트 + */ + private function updateLocations(int $docId, array $locations): void + { + foreach ($locations as $locData) { + $locId = $locData['id'] ?? null; + if (! $locId) { + continue; + } + + $location = QualityDocumentLocation::where('quality_document_id', $docId)->find($locId); + if (! $location) { + continue; + } + + $updateData = []; + if (array_key_exists('post_width', $locData)) { + $updateData['post_width'] = $locData['post_width']; + } + if (array_key_exists('post_height', $locData)) { + $updateData['post_height'] = $locData['post_height']; + } + if (array_key_exists('change_reason', $locData)) { + $updateData['change_reason'] = $locData['change_reason']; + } + if (array_key_exists('inspection_data', $locData)) { + $updateData['inspection_data'] = $locData['inspection_data']; + } + + if (! empty($updateData)) { + $location->update($updateData); + } + } + } + + /** + * 수주 동기화 (update 시 사용) + */ + private function syncOrders(QualityDocument $doc, array $orderIds, int $tenantId): void + { + $existingOrderIds = QualityDocumentOrder::where('quality_document_id', $doc->id) + ->pluck('order_id') + ->toArray(); + + $toAttach = array_diff($orderIds, $existingOrderIds); + $toDetach = array_diff($existingOrderIds, $orderIds); + + // 새로 연결 + foreach ($toAttach as $orderId) { + $order = Order::where('tenant_id', $tenantId)->find($orderId); + if (! $order) { + continue; + } + + $docOrder = QualityDocumentOrder::firstOrCreate([ + 'quality_document_id' => $doc->id, + 'order_id' => $orderId, + ]); + + // 개소(root OrderNode) 기준으로 location 생성 + $rootNodes = OrderNode::where('order_id', $orderId) + ->whereNull('parent_id') + ->orderBy('sort_order') + ->get(); + + foreach ($rootNodes as $node) { + // 각 개소의 대표 OrderItem (해당 노드 하위 첫 번째 품목) + $representativeItem = OrderItem::where('order_node_id', $node->id) + ->orderBy('sort_order') + ->first(); + + if ($representativeItem) { + QualityDocumentLocation::firstOrCreate([ + 'quality_document_id' => $doc->id, + 'quality_document_order_id' => $docOrder->id, + 'order_item_id' => $representativeItem->id, + ]); + } + } + } + + // 연결 해제 + foreach ($toDetach as $orderId) { + $docOrder = QualityDocumentOrder::where('quality_document_id', $doc->id) + ->where('order_id', $orderId) + ->first(); + + if ($docOrder) { + QualityDocumentLocation::where('quality_document_order_id', $docOrder->id)->delete(); + $docOrder->delete(); + } + } + } + /** * 수주 연결 */ @@ -397,14 +517,24 @@ public function attachOrders(int $docId, array $orderIds) 'order_id' => $orderId, ]); - // 수주 연결 시 개소(order_items)를 locations에 자동 생성 - $orderItems = OrderItem::where('order_id', $orderId)->get(); - foreach ($orderItems as $item) { - QualityDocumentLocation::firstOrCreate([ - 'quality_document_id' => $doc->id, - 'quality_document_order_id' => $docOrder->id, - 'order_item_id' => $item->id, - ]); + // 수주 연결 시 개소(root OrderNode)를 locations에 자동 생성 + $rootNodes = OrderNode::where('order_id', $orderId) + ->whereNull('parent_id') + ->orderBy('sort_order') + ->get(); + + foreach ($rootNodes as $node) { + $representativeItem = OrderItem::where('order_node_id', $node->id) + ->orderBy('sort_order') + ->first(); + + if ($representativeItem) { + QualityDocumentLocation::firstOrCreate([ + 'quality_document_id' => $doc->id, + 'quality_document_order_id' => $docOrder->id, + 'order_item_id' => $representativeItem->id, + ]); + } } } @@ -535,7 +665,7 @@ private function transformToFrontend(QualityDocument $doc, bool $detail = false) 'site_address_detail' => $options['site_address']['detail'] ?? '', ]; - // 개소 목록 + // 개소 목록 (각 location은 1개 root OrderNode = 1개 개소) $result['order_items'] = $doc->locations->map(function ($loc) { $orderItem = $loc->orderItem; $node = $orderItem?->node; @@ -544,9 +674,10 @@ private function transformToFrontend(QualityDocument $doc, bool $detail = false) return [ 'id' => (string) $loc->id, + 'order_id' => $order?->id, 'order_number' => $order?->order_no ?? '', 'site_name' => $order?->site_name ?? '', - 'delivery_date' => $order?->delivery_date ?? '', + 'delivery_date' => $order?->delivery_date ? $order->delivery_date->format('Y-m-d') : '', 'floor' => $orderItem?->floor_code ?? '', 'symbol' => $orderItem?->symbol_code ?? '', 'order_width' => $nodeOptions['width'] ?? 0, @@ -554,6 +685,7 @@ private function transformToFrontend(QualityDocument $doc, bool $detail = false) 'construction_width' => $loc->post_width ?? 0, 'construction_height' => $loc->post_height ?? 0, 'change_reason' => $loc->change_reason ?? '', + 'inspection_data' => $loc->inspection_data, ]; })->toArray(); } diff --git a/database/migrations/2026_03_06_094425_add_inspection_data_to_quality_document_locations.php b/database/migrations/2026_03_06_094425_add_inspection_data_to_quality_document_locations.php new file mode 100644 index 0000000..d3e754a --- /dev/null +++ b/database/migrations/2026_03_06_094425_add_inspection_data_to_quality_document_locations.php @@ -0,0 +1,22 @@ +json('inspection_data')->nullable()->after('change_reason')->comment('검사 데이터 JSON'); + }); + } + + public function down(): void + { + Schema::table('quality_document_locations', function (Blueprint $table) { + $table->dropColumn('inspection_data'); + }); + } +};