From e6c02292d22d7438d4aa96a6ce9c941d743c1585 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B6=8C=ED=98=81=EC=84=B1?= Date: Fri, 27 Feb 2026 15:59:02 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20[quote/quality]=20Phase=202B=20?= =?UTF-8?q?=EA=B2=AC=EC=A0=81=20product=5Fcode=20=EC=9E=90=EB=8F=99?= =?UTF-8?q?=EC=B6=94=EC=B6=9C=20+=20inspections=20work=5Forder=5Fid=20FK?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - QuoteService: extractProductCodeFromInputs() 추가, store/update에서 자동 추출 - BackfillQuoteProductCodeCommand: 기존 quotes 25건 product_code 보정 - inspections 테이블에 work_order_id FK 마이그레이션 (nullable, nullOnDelete) - Inspection↔WorkOrder 양방향 관계 추가 - InspectionService: store/show/index에 work_order_id 처리 + transformToFrontend - InspectionStoreRequest: work_order_id 검증 규칙 추가 --- .../BackfillQuoteProductCodeCommand.php | 54 +++++++++++++++++++ .../Inspection/InspectionStoreRequest.php | 1 + app/Models/Production/WorkOrder.php | 9 ++++ app/Models/Qualitys/Inspection.php | 11 ++++ app/Services/InspectionService.php | 13 +++-- app/Services/Quote/QuoteService.php | 20 ++++++- ...add_work_order_id_to_inspections_table.php | 34 ++++++++++++ 7 files changed, 135 insertions(+), 7 deletions(-) create mode 100644 app/Console/Commands/BackfillQuoteProductCodeCommand.php create mode 100644 database/migrations/2026_02_27_000001_add_work_order_id_to_inspections_table.php diff --git a/app/Console/Commands/BackfillQuoteProductCodeCommand.php b/app/Console/Commands/BackfillQuoteProductCodeCommand.php new file mode 100644 index 0000000..4550ca9 --- /dev/null +++ b/app/Console/Commands/BackfillQuoteProductCodeCommand.php @@ -0,0 +1,54 @@ +option('dry-run'); + + $quotes = Quote::whereNull('product_code') + ->whereNotNull('calculation_inputs') + ->get(); + + $this->info("대상: {$quotes->count()}건".($dryRun ? ' (dry-run)' : '')); + + $updated = 0; + $skipped = 0; + + foreach ($quotes as $quote) { + $inputs = $quote->calculation_inputs; + if (! is_array($inputs)) { + $inputs = json_decode($inputs, true); + } + + $productCode = $inputs['items'][0]['productCode'] ?? null; + + if (! $productCode) { + $skipped++; + $this->line(" SKIP #{$quote->id} ({$quote->quote_number}) — productCode 없음"); + + continue; + } + + if (! $dryRun) { + $quote->update(['product_code' => $productCode]); + } + + $updated++; + $this->line(' '.($dryRun ? 'WOULD ' : '')."UPDATE #{$quote->id} ({$quote->quote_number}) → {$productCode}"); + } + + $this->info("완료: 보정 {$updated}건, 스킵 {$skipped}건"); + + return self::SUCCESS; + } +} diff --git a/app/Http/Requests/Inspection/InspectionStoreRequest.php b/app/Http/Requests/Inspection/InspectionStoreRequest.php index 56e38c1..9d92a4f 100644 --- a/app/Http/Requests/Inspection/InspectionStoreRequest.php +++ b/app/Http/Requests/Inspection/InspectionStoreRequest.php @@ -22,6 +22,7 @@ public function rules(): array Inspection::TYPE_FQC, ])], 'lot_no' => ['required', 'string', 'max:50'], + 'work_order_id' => ['nullable', 'integer', 'exists:work_orders,id'], 'item_name' => ['nullable', 'string', 'max:200'], 'process_name' => ['nullable', 'string', 'max:100'], 'quantity' => ['nullable', 'numeric', 'min:0'], diff --git a/app/Models/Production/WorkOrder.php b/app/Models/Production/WorkOrder.php index 4715845..0aff877 100644 --- a/app/Models/Production/WorkOrder.php +++ b/app/Models/Production/WorkOrder.php @@ -6,6 +6,7 @@ use App\Models\Members\User; use App\Models\Orders\Order; use App\Models\Process; +use App\Models\Qualitys\Inspection; use App\Models\Tenants\Department; use App\Models\Tenants\Shipment; use App\Traits\Auditable; @@ -234,6 +235,14 @@ public function shipments(): HasMany return $this->hasMany(Shipment::class); } + /** + * 품질검사 (IQC/PQC/FQC) + */ + public function inspections(): HasMany + { + return $this->hasMany(Inspection::class); + } + /** * 생성자 */ diff --git a/app/Models/Qualitys/Inspection.php b/app/Models/Qualitys/Inspection.php index f6453b3..fd4b6a2 100644 --- a/app/Models/Qualitys/Inspection.php +++ b/app/Models/Qualitys/Inspection.php @@ -4,6 +4,7 @@ use App\Models\Items\Item; use App\Models\Members\User; +use App\Models\Production\WorkOrder; use App\Traits\Auditable; use App\Traits\BelongsToTenant; use Illuminate\Database\Eloquent\Casts\Attribute; @@ -23,6 +24,7 @@ * @property string|null $inspection_date 검사일 * @property int|null $item_id 품목 ID * @property string $lot_no LOT번호 + * @property int|null $work_order_id 작업지시 ID (PQC/FQC용) * @property int|null $inspector_id 검사자 ID * @property array|null $meta 메타정보 (process_name, quantity, unit 등) * @property array|null $items 검사항목 배열 @@ -47,6 +49,7 @@ class Inspection extends Model 'inspection_date', 'item_id', 'lot_no', + 'work_order_id', 'inspector_id', 'meta', 'items', @@ -92,6 +95,14 @@ class Inspection extends Model // ===== Relationships ===== + /** + * 작업지시 (PQC/FQC용) + */ + public function workOrder() + { + return $this->belongsTo(WorkOrder::class); + } + /** * 품목 */ diff --git a/app/Services/InspectionService.php b/app/Services/InspectionService.php index dcc5e38..2964758 100644 --- a/app/Services/InspectionService.php +++ b/app/Services/InspectionService.php @@ -33,7 +33,7 @@ public function index(array $params) $query = Inspection::query() ->where('tenant_id', $tenantId) - ->with(['inspector:id,name', 'item:id,item_name']); + ->with(['inspector:id,name', 'item:id,item_name', 'workOrder:id,work_order_no']); // 검색어 (검사번호, LOT번호) if ($q !== '') { @@ -126,7 +126,7 @@ public function show(int $id) $tenantId = $this->tenantId(); $inspection = Inspection::where('tenant_id', $tenantId) - ->with(['inspector:id,name', 'item:id,item_name']) + ->with(['inspector:id,name', 'item:id,item_name', 'workOrder:id,work_order_no']) ->find($id); if (! $inspection) { @@ -183,6 +183,7 @@ public function store(array $data) 'inspection_type' => $data['inspection_type'], 'request_date' => $data['request_date'] ?? now()->toDateString(), 'lot_no' => $data['lot_no'], + 'work_order_id' => $data['work_order_id'] ?? null, 'inspector_id' => $data['inspector_id'] ?? null, 'meta' => $meta, 'items' => $items, @@ -200,7 +201,7 @@ public function store(array $data) $inspection->toArray() ); - return $this->transformToFrontend($inspection->load(['inspector:id,name'])); + return $this->transformToFrontend($inspection->load(['inspector:id,name', 'workOrder:id,work_order_no'])); }); } @@ -277,7 +278,7 @@ public function update(int $id, array $data) $inspection->fresh()->toArray() ); - return $this->transformToFrontend($inspection->load(['inspector:id,name'])); + return $this->transformToFrontend($inspection->load(['inspector:id,name', 'workOrder:id,work_order_no'])); }); } @@ -360,7 +361,7 @@ public function complete(int $id, array $data) $inspection->fresh()->toArray() ); - return $this->transformToFrontend($inspection->load(['inspector:id,name'])); + return $this->transformToFrontend($inspection->load(['inspector:id,name', 'workOrder:id,work_order_no'])); }); } @@ -380,6 +381,8 @@ private function transformToFrontend(Inspection $inspection): array 'inspection_date' => $inspection->inspection_date?->format('Y-m-d'), 'item_name' => $inspection->item?->item_name ?? ($meta['item_name'] ?? null), 'lot_no' => $inspection->lot_no, + 'work_order_id' => $inspection->work_order_id, + 'work_order_no' => $inspection->workOrder?->work_order_no, 'process_name' => $meta['process_name'] ?? null, 'quantity' => $meta['quantity'] ?? null, 'unit' => $meta['unit'] ?? null, diff --git a/app/Services/Quote/QuoteService.php b/app/Services/Quote/QuoteService.php index 34e05e0..b42cc01 100644 --- a/app/Services/Quote/QuoteService.php +++ b/app/Services/Quote/QuoteService.php @@ -321,7 +321,7 @@ public function store(array $data): Quote // 제품 정보 'product_category' => $data['product_category'] ?? Quote::CATEGORY_SCREEN, 'product_id' => $data['product_id'] ?? null, - 'product_code' => $data['product_code'] ?? null, + 'product_code' => $data['product_code'] ?? $this->extractProductCodeFromInputs($data), 'product_name' => $data['product_name'] ?? null, // 규격 정보 'open_size_width' => $data['open_size_width'] ?? null, @@ -418,7 +418,7 @@ public function update(int $id, array $data): Quote // 제품 정보 'product_category' => $data['product_category'] ?? $quote->product_category, 'product_id' => $data['product_id'] ?? $quote->product_id, - 'product_code' => $data['product_code'] ?? $quote->product_code, + 'product_code' => $data['product_code'] ?? $this->extractProductCodeFromInputs($data) ?? $quote->product_code, 'product_name' => $data['product_name'] ?? $quote->product_name, // 규격 정보 'open_size_width' => $data['open_size_width'] ?? $quote->open_size_width, @@ -799,6 +799,22 @@ private function resolveLocationIndex(QuoteItem $quoteItem, array $productItems) return 0; } + /** + * calculation_inputs에서 첫 번째 개소의 productCode 추출 + * 다중 개소 시 첫 번째를 대표값으로 사용 + */ + private function extractProductCodeFromInputs(array $data): ?string + { + $inputs = $data['calculation_inputs'] ?? null; + if (! $inputs || ! is_array($inputs)) { + return null; + } + + $items = $inputs['items'] ?? []; + + return $items[0]['productCode'] ?? null; + } + /** * 수주번호 생성 * 형식: ORD-YYMMDD-NNN (예: ORD-260105-001) diff --git a/database/migrations/2026_02_27_000001_add_work_order_id_to_inspections_table.php b/database/migrations/2026_02_27_000001_add_work_order_id_to_inspections_table.php new file mode 100644 index 0000000..f97f351 --- /dev/null +++ b/database/migrations/2026_02_27_000001_add_work_order_id_to_inspections_table.php @@ -0,0 +1,34 @@ +unsignedBigInteger('work_order_id') + ->nullable() + ->after('lot_no') + ->comment('작업지시 ID (PQC/FQC용)'); + + $table->foreign('work_order_id') + ->references('id') + ->on('work_orders') + ->nullOnDelete(); + + $table->index('work_order_id'); + }); + } + + public function down(): void + { + Schema::table('inspections', function (Blueprint $table) { + $table->dropForeign(['work_order_id']); + $table->dropIndex(['work_order_id']); + $table->dropColumn('work_order_id'); + }); + } +};