fix(WEB): 수주 완전삭제(force) 시 생산지시완료 상태 처리 및 skip 응답 반영

- bulkDestroy force=true일 때 상태 체크 bypass, 연관 작업지시 데이터 모두 삭제
- forceDeleteWorkOrders() 헬퍼: 자재투입 재고복구, 문서, 부속데이터 정리 후 hard delete

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-21 09:49:00 +09:00
parent f8858cf1b7
commit ba49313ffa

View File

@@ -428,31 +428,34 @@ public function bulkDestroy(array $ids, bool $force = false): array
$deletedCount = 0;
$skippedIds = [];
return DB::transaction(function () use ($orders, $force, $userId, &$deletedCount, &$skippedIds) {
return DB::transaction(function () use ($orders, $force, $userId, $tenantId, &$deletedCount, &$skippedIds) {
foreach ($orders as $order) {
// 상태 검증: DRAFT/CONFIRMED/CANCELLED만 삭제 가능
if (! in_array($order->status_code, [
Order::STATUS_DRAFT,
Order::STATUS_CONFIRMED,
Order::STATUS_CANCELLED,
])) {
$skippedIds[] = $order->id;
if ($force) {
// force=true (개발환경 완전삭제): 모든 상태 허용, 연관 데이터 모두 삭제
$this->forceDeleteWorkOrders($order, $tenantId);
} else {
// 일반 삭제: 상태/작업지시/출하 검증
if (! in_array($order->status_code, [
Order::STATUS_DRAFT,
Order::STATUS_CONFIRMED,
Order::STATUS_CANCELLED,
])) {
$skippedIds[] = $order->id;
continue;
}
continue;
}
// 작업지시 존재 시 skip
if ($order->workOrders()->exists()) {
$skippedIds[] = $order->id;
if ($order->workOrders()->exists()) {
$skippedIds[] = $order->id;
continue;
}
continue;
}
// 출하 존재 시 skip
if ($order->shipments()->exists()) {
$skippedIds[] = $order->id;
if ($order->shipments()->exists()) {
$skippedIds[] = $order->id;
continue;
continue;
}
}
// 견적 연결 해제
@@ -500,6 +503,72 @@ public function bulkDestroy(array $ids, bool $force = false): array
});
}
/**
* 작업지시 및 연관 데이터 강제 삭제 (개발환경 완전삭제용)
*/
private function forceDeleteWorkOrders(Order $order, int $tenantId): void
{
$workOrderIds = WorkOrder::where('tenant_id', $tenantId)
->where('sales_order_id', $order->id)
->pluck('id')
->toArray();
if (empty($workOrderIds)) {
return;
}
// 1. 자재 투입 재고 복구 + 삭제
$materialInputs = WorkOrderMaterialInput::whereIn('work_order_id', $workOrderIds)->get();
if ($materialInputs->isNotEmpty()) {
$stockService = app(StockService::class);
foreach ($materialInputs as $input) {
try {
$stockService->increaseToLot(
stockLotId: $input->stock_lot_id,
qty: (float) $input->qty,
reason: 'work_order_input_cancel',
referenceId: $input->work_order_id
);
} catch (\Exception $e) {
Log::warning('완전삭제: 재고 복원 실패', [
'input_id' => $input->id,
'stock_lot_id' => $input->stock_lot_id,
'error' => $e->getMessage(),
]);
}
}
WorkOrderMaterialInput::whereIn('work_order_id', $workOrderIds)->delete();
}
// 2. 문서 삭제
$documentIds = Document::where('linkable_type', 'work_order')
->whereIn('linkable_id', $workOrderIds)
->pluck('id')
->toArray();
if (! empty($documentIds)) {
DocumentData::whereIn('document_id', $documentIds)->delete();
DocumentApproval::whereIn('document_id', $documentIds)->delete();
Document::whereIn('id', $documentIds)->forceDelete();
}
// 3. 출하 참조 해제
DB::table('shipments')
->whereIn('work_order_id', $workOrderIds)
->update(['work_order_id' => null]);
// 4. 부속 데이터 삭제
DB::table('work_order_step_progress')->whereIn('work_order_id', $workOrderIds)->delete();
DB::table('work_order_assignees')->whereIn('work_order_id', $workOrderIds)->delete();
DB::table('work_order_bending_details')->whereIn('work_order_id', $workOrderIds)->delete();
DB::table('work_order_issues')->whereIn('work_order_id', $workOrderIds)->delete();
DB::table('work_results')->whereIn('work_order_id', $workOrderIds)->delete();
// 5. 작업지시 품목 → 작업지시 삭제
DB::table('work_order_items')->whereIn('work_order_id', $workOrderIds)->delete();
WorkOrder::whereIn('id', $workOrderIds)->forceDelete();
}
/**
* 상태 변경
*/
@@ -783,6 +852,17 @@ public function createFromQuote(int $quoteId, array $data = [])
? intdiv($quote->items->count(), $locationCount)
: 0;
// DEBUG: 분배 로직 디버깅 (임시)
\Log::info('[createFromQuote] Distribution params', [
'quoteId' => $quote->id,
'itemCount' => $quote->items->count(),
'locationCount' => $locationCount,
'hasFormulaSource' => $hasFormulaSource,
'itemsPerLocation' => $itemsPerLocation,
'collectionKeys_first5' => $quote->items->keys()->take(5)->all(),
'nodeMapKeys' => array_keys($nodeMap),
]);
foreach ($quote->items as $index => $quoteItem) {
$floorCode = null;
$symbolCode = null;
@@ -799,6 +879,11 @@ public function createFromQuote(int $quoteId, array $data = [])
$locIdx = min(intdiv($index, $itemsPerLocation), $locationCount - 1);
}
// DEBUG: 처음 3개와 전환점(17-19) 로깅 (임시)
if ($index < 3 || ($index >= 17 && $index <= 19)) {
\Log::info("[createFromQuote] item idx={$index} locIdx={$locIdx} fs='{$formulaSource}'");
}
// calculation_inputs에서 floor/code 가져오기
if (isset($productItems[$locIdx])) {
$floorCode = $productItems[$locIdx]['floor'] ?? null;