feat: [품질관리] order_ids 영속성 + location 데이터 저장

- StoreRequest/UpdateRequest에 order_ids 검증 추가
- UpdateRequest에 locations 검증 추가 (시공규격, 변경사유, 검사데이터)
- QualityDocumentLocation에 inspection_data(JSON) fillable/cast 추가
- QualityDocumentService store()에 syncOrders 연동
- QualityDocumentService update()에 syncOrders + updateLocations 연동
- inspection_data 컬럼 추가 migration 신규

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-06 21:09:22 +09:00
parent c5d5b5d076
commit f2eede6e3a
5 changed files with 182 additions and 13 deletions

View File

@@ -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'],
];
}

View File

@@ -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'],
];
}
}

View File

@@ -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);

View File

@@ -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();
}

View File

@@ -0,0 +1,22 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('quality_document_locations', function (Blueprint $table) {
$table->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');
});
}
};