From 3e1d1ffc33e8729b28b82ffc3d6f162fff17c2fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B6=8C=ED=98=81=EC=84=B1?= Date: Sat, 7 Mar 2026 03:05:07 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20[=EB=AC=B8=EC=84=9C=EC=9D=B8=EC=87=84]?= =?UTF-8?q?=20=EC=8A=A4=EB=83=85=EC=83=B7=20=EC=B6=9C=EB=A0=A5=20+=20?= =?UTF-8?q?=EC=A0=88=EA=B3=A1=20=EC=A0=84=EC=9A=A9=20=EB=A0=8C=EB=8D=94?= =?UTF-8?q?=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - print.blade.php rendered_html 스냅샷 우선 출력 - bending-inspection-data, bending-worklog 파셜 추가 - documents/show.blade.php 개선 - DocumentTemplateSection 모델 보완 Co-Authored-By: Claude Opus 4.6 --- app/Http/Controllers/DocumentController.php | 20 +- app/Models/DocumentTemplateSection.php | 1 + .../bending-inspection-data.blade.php | 271 +++++++ .../partials/bending-worklog.blade.php | 671 ++++++++++++++++++ resources/views/documents/print.blade.php | 8 + resources/views/documents/show.blade.php | 83 ++- 6 files changed, 1030 insertions(+), 24 deletions(-) create mode 100644 resources/views/documents/partials/bending-inspection-data.blade.php create mode 100644 resources/views/documents/partials/bending-worklog.blade.php diff --git a/app/Http/Controllers/DocumentController.php b/app/Http/Controllers/DocumentController.php index d8d34d23..b2cd68d9 100644 --- a/app/Http/Controllers/DocumentController.php +++ b/app/Http/Controllers/DocumentController.php @@ -271,6 +271,22 @@ public function show(int $id): View // 기본정보 bf_ 자동 backfill $this->resolveAndBackfillBasicFields($document); + // 절곡 작업일지용: bending_info 추출 + $bendingInfo = null; + if ($workOrder) { + $woOptions = json_decode($workOrder->options ?? '{}', true); + $bendingInfo = $woOptions['bending_info'] ?? null; + } + + // 절곡 중간검사용: inspection_data 스냅샷 추출 (work_order_items.options.inspection_data) + $inspectionData = null; + foreach ($workOrderItems as $item) { + if (! empty($item->options['inspection_data'])) { + $inspectionData = $item->options['inspection_data']; + break; + } + } + return view('documents.show', [ 'document' => $document, 'workOrderItems' => $workOrderItems, @@ -279,6 +295,8 @@ public function show(int $id): View 'materialInputLots' => $materialInputLots, 'itemLotMap' => $itemLotMap ?? collect(), 'blockHtml' => $this->renderBlockHtml($document->template, $document, 'view'), + 'bendingInfo' => $bendingInfo, + 'inspectionData' => $inspectionData, ]); } @@ -295,7 +313,7 @@ private function resolveAndBackfillBasicFields(Document $document): void } // bf_ 레코드가 하나라도 있으면 이미 저장된 것 → skip - $existingBfCount = $document->data + $existingBfCount = ($document->data ?? collect()) ->filter(fn ($d) => str_starts_with($d->field_key, 'bf_')) ->count(); if ($existingBfCount > 0) { diff --git a/app/Models/DocumentTemplateSection.php b/app/Models/DocumentTemplateSection.php index f2ae8db5..45043263 100644 --- a/app/Models/DocumentTemplateSection.php +++ b/app/Models/DocumentTemplateSection.php @@ -14,6 +14,7 @@ class DocumentTemplateSection extends Model protected $fillable = [ 'template_id', 'title', + 'description', 'image_path', 'sort_order', ]; diff --git a/resources/views/documents/partials/bending-inspection-data.blade.php b/resources/views/documents/partials/bending-inspection-data.blade.php new file mode 100644 index 00000000..0a790b56 --- /dev/null +++ b/resources/views/documents/partials/bending-inspection-data.blade.php @@ -0,0 +1,271 @@ +{{-- + 절곡 중간검사 DATA 전용 렌더링 + React BendingInspectionContent.tsx와 동일한 구조 + + 데이터 소스: work_order_items.options.inspection_data (스냅샷) + - 제품 구조, 도면치수, 측정값이 모두 포함된 완전한 스냅샷 + - 정책 변경 시에도 저장 시점의 문서 그대로 유지 + + 필요 변수: + - $inspectionData: inspection_data 스냅샷 (array|null) + - $docData: document_data collection (fallback용) + - $document: Document model +--}} + +@php + // inspection_data 스냅샷에서 제품 목록 로드 + $products = $inspectionData['products'] ?? []; + $snapshotJudgment = $inspectionData['judgment'] ?? null; + $inadequateContent = $inspectionData['inadequateContent'] ?? ''; + + // 스냅샷이 없으면 INITIAL_PRODUCTS 하드코딩 fallback (레거시 호환) + if (empty($products)) { + $products = [ + ['id' => 'guide-rail-wall', 'category' => 'KWE01', 'productName' => '가이드레일', 'productType' => '벽면형', + 'lengthDesignValue' => '3000', 'widthDesignValue' => 'N/A', 'bendingStatus' => null, 'lengthMeasured' => '', 'widthMeasured' => '', + 'gapPoints' => [ + ['point' => '①', 'designValue' => '30', 'measured' => ''], + ['point' => '②', 'designValue' => '80', 'measured' => ''], + ['point' => '③', 'designValue' => '45', 'measured' => ''], + ['point' => '④', 'designValue' => '40', 'measured' => ''], + ['point' => '⑤', 'designValue' => '34', 'measured' => ''], + ]], + ['id' => 'guide-rail-side', 'category' => 'KWE01', 'productName' => '가이드레일', 'productType' => '측면형', + 'lengthDesignValue' => '3000', 'widthDesignValue' => 'N/A', 'bendingStatus' => null, 'lengthMeasured' => '', 'widthMeasured' => '', + 'gapPoints' => [ + ['point' => '①', 'designValue' => '28', 'measured' => ''], + ['point' => '②', 'designValue' => '75', 'measured' => ''], + ['point' => '③', 'designValue' => '42', 'measured' => ''], + ['point' => '④', 'designValue' => '38', 'measured' => ''], + ['point' => '⑤', 'designValue' => '32', 'measured' => ''], + ]], + ['id' => 'case', 'category' => 'KWE01', 'productName' => '케이스', 'productType' => '500X380', + 'lengthDesignValue' => '3000', 'widthDesignValue' => 'N/A', 'bendingStatus' => null, 'lengthMeasured' => '', 'widthMeasured' => '', + 'gapPoints' => [ + ['point' => '①', 'designValue' => '380', 'measured' => ''], + ['point' => '②', 'designValue' => '50', 'measured' => ''], + ['point' => '③', 'designValue' => '240', 'measured' => ''], + ['point' => '④', 'designValue' => '50', 'measured' => ''], + ]], + ['id' => 'bottom-finish', 'category' => 'KWE01', 'productName' => '하단마감재', 'productType' => '60X40', + 'lengthDesignValue' => '3000', 'widthDesignValue' => 'N/A', 'bendingStatus' => null, 'lengthMeasured' => '', 'widthMeasured' => '', + 'gapPoints' => [ + ['point' => '①', 'designValue' => '60', 'measured' => ''], + ['point' => '②', 'designValue' => '64', 'measured' => ''], + ]], + ['id' => 'bottom-l-bar', 'category' => 'KWE01', 'productName' => '하단L-BAR', 'productType' => '17X60', + 'lengthDesignValue' => '3000', 'widthDesignValue' => 'N/A', 'bendingStatus' => null, 'lengthMeasured' => '', 'widthMeasured' => '', + 'gapPoints' => [ + ['point' => '①', 'designValue' => '17', 'measured' => ''], + ]], + ['id' => 'smoke-w50', 'category' => 'KWE01', 'productName' => '연기차단재', 'productType' => "W50\n가이드레일용", + 'lengthDesignValue' => '3000', 'widthDesignValue' => '', 'bendingStatus' => null, 'lengthMeasured' => '', 'widthMeasured' => '', + 'gapPoints' => [ + ['point' => '①', 'designValue' => '50', 'measured' => ''], + ['point' => '②', 'designValue' => '12', 'measured' => ''], + ]], + ['id' => 'smoke-w80', 'category' => 'KWE01', 'productName' => '연기차단재', 'productType' => "W80\n케이스용", + 'lengthDesignValue' => '3000', 'widthDesignValue' => '', 'bendingStatus' => null, 'lengthMeasured' => '', 'widthMeasured' => '', + 'gapPoints' => [ + ['point' => '①', 'designValue' => '80', 'measured' => ''], + ['point' => '②', 'designValue' => '12', 'measured' => ''], + ]], + ]; + } + + // 제품 ID → 메타 정보 매핑 (스냅샷에 category/productName/productType가 없을 경우 보완) + $productMeta = [ + 'guide-rail-wall' => ['category' => 'KWE01', 'productName' => '가이드레일', 'productType' => '벽면형'], + 'guide_rail_wall' => ['category' => 'KWE01', 'productName' => '가이드레일', 'productType' => '벽면형'], + 'guide-rail-side' => ['category' => 'KWE01', 'productName' => '가이드레일', 'productType' => '측면형'], + 'guide_rail_side' => ['category' => 'KWE01', 'productName' => '가이드레일', 'productType' => '측면형'], + 'case' => ['category' => 'KWE01', 'productName' => '케이스', 'productType' => '500X380'], + 'bottom-finish' => ['category' => 'KWE01', 'productName' => '하단마감재', 'productType' => '60X40'], + 'bottom_finish' => ['category' => 'KWE01', 'productName' => '하단마감재', 'productType' => '60X40'], + 'bottom-l-bar' => ['category' => 'KWE01', 'productName' => '하단L-BAR', 'productType' => '17X60'], + 'bottom_l_bar' => ['category' => 'KWE01', 'productName' => '하단L-BAR', 'productType' => '17X60'], + 'smoke-w50' => ['category' => 'KWE01', 'productName' => '연기차단재', 'productType' => "W50\n가이드레일용"], + 'smoke_w50' => ['category' => 'KWE01', 'productName' => '연기차단재', 'productType' => "W50\n가이드레일용"], + 'smoke-w80' => ['category' => 'KWE01', 'productName' => '연기차단재', 'productType' => "W80\n케이스용"], + 'smoke_w80' => ['category' => 'KWE01', 'productName' => '연기차단재', 'productType' => "W80\n케이스용"], + ]; + + // 종합판정 결정 + $overallResult = $snapshotJudgment ?? ($docData->where('field_key', 'overall_result')->first()?->field_value ?? ''); + $isOverallPass = in_array(strtolower($overallResult), ['pass', 'ok', '적합', '합격']); +@endphp + +
+

■ 중간검사 DATA

+ +
+ + + {{-- 1단 헤더 --}} + + + + + + + + + + + {{-- 2단 헤더 (서브라벨) --}} + + + + + + + + + + + + @foreach($products as $pIdx => $product) + @php + $gapPoints = $product['gapPoints'] ?? []; + $gapCount = count($gapPoints); + if ($gapCount === 0) $gapCount = 1; + + $productId = $product['id'] ?? ''; + $meta = $productMeta[$productId] ?? []; + + $category = $product['category'] ?? ($meta['category'] ?? ''); + $productName = $product['productName'] ?? ($meta['productName'] ?? $productId); + $productType = $product['productType'] ?? ($meta['productType'] ?? ''); + + // 스냅샷 필드 (React 저장 형식: lengthDesignValue, lengthMeasured 등) + $lengthDesign = $product['lengthDesignValue'] ?? ($product['lengthDesign'] ?? ''); + $lengthMeasured = $product['lengthMeasured'] ?? ''; + $widthDesign = $product['widthDesignValue'] ?? ($product['widthDesign'] ?? ''); + $widthMeasured = $product['widthMeasured'] ?? ''; + + // 절곡상태 + $statusVal = $product['bendingStatus'] ?? null; + + // 판정: 양호→적, 불량→부 + $judgmentVal = null; + if ($statusVal === '양호' || strtolower($statusVal ?? '') === 'pass' || strtolower($statusVal ?? '') === 'ok') { + $judgmentVal = '적'; + } elseif ($statusVal === '불량' || strtolower($statusVal ?? '') === 'fail' || strtolower($statusVal ?? '') === 'ng') { + $judgmentVal = '부'; + } + @endphp + + @for($gIdx = 0; $gIdx < max($gapCount, 1); $gIdx++) + + @if($gIdx === 0) + {{-- 분류 --}} + + {{-- 제품명 --}} + + {{-- 타입 --}} + + {{-- 겉모양/절곡상태 --}} + + {{-- 길이 도면치수 --}} + + {{-- 길이 측정값 --}} + + {{-- 너비 도면치수 --}} + + {{-- 너비 측정값 --}} + + @endif + + {{-- 간격 포인트 --}} + @if(!empty($gapPoints)) + @php $gap = $gapPoints[$gIdx] ?? []; @endphp + + + + @else + + + + @endif + + @if($gIdx === 0) + {{-- 판정 --}} + + @endif + + @endfor + @endforeach + +
분류제품명타입겉모양/
절곡상태
길이너비간격판정
도면치수측정값도면치수측정값포인트도면치수측정값
+ {{ $category }} + + {{ $productName }} + + {{ $productType }} + + @if($statusVal === '양호' || strtolower($statusVal ?? '') === 'pass' || strtolower($statusVal ?? '') === 'ok') + + + + + + @elseif($statusVal === '불량' || strtolower($statusVal ?? '') === 'fail' || strtolower($statusVal ?? '') === 'ng') + + + + + + @else + - + @endif + + {{ $lengthDesign ?: '-' }} + + {{ $lengthMeasured ?: '-' }} + + {{ $widthDesign ?: '-' }} + + {{ $widthMeasured ?: '-' }} + + {{ $gap['point'] ?? '' }} + + {{ $gap['designValue'] ?? '' }} + + {{ ($gap['measured'] ?? '') ?: '-' }} + --- + @if($judgmentVal === '적') + + + 적 + + @elseif($judgmentVal === '부') + + + 부 + + @else + - + @endif +
+
+ + {{-- 종합판정 --}} +
+ 종합판정: + @if($overallResult) + @if($isOverallPass) + + + 적합 + + @else + + + 부적합 + + @endif + @else + 미판정 + @endif +
+
\ No newline at end of file diff --git a/resources/views/documents/partials/bending-worklog.blade.php b/resources/views/documents/partials/bending-worklog.blade.php new file mode 100644 index 00000000..0c85a96c --- /dev/null +++ b/resources/views/documents/partials/bending-worklog.blade.php @@ -0,0 +1,671 @@ +{{-- + 절곡 작업일지 전용 렌더링 + React BendingWorkLogContent.tsx + bending/ sub-components와 동일 구조 + + 필요 변수: + - $document: Document model + - $workOrder: work_orders row (stdClass) + - $salesOrder: orders row (stdClass, nullable) + - $workOrderItems: work_order_items collection + - $bendingInfo: array (work_order.options.bending_info) + - $itemLotMap: collection (work_order_item_id → lot_no) +--}} + +@php + // ============================================================ + // 상수 (React utils.ts와 동일) + // ============================================================ + $SUS_DENSITY = 7.93; + $EGI_DENSITY = 7.85; + $WALL_PART_WIDTH = 412; + $SIDE_PART_WIDTH = 462; + $WALL_BASE_HEIGHT = 80; + $SIDE_BASE_HEIGHT = 130; + $BASE_WIDTH = 135; + $BOTTOM_BAR_WIDTH = 184; + $EXTRA_FINISH_WIDTH = 238; + $SMOKE_BARRIER_WIDTH = 26; + $BOX_FINISH_MATERIAL = 'EGI 1.55T'; + $BOX_COVER_LENGTH = 1219; + + $apiBaseUrl = rtrim(config('app.api_url', 'http://api.sam.kr'), '/'); + + // ============================================================ + // 유틸 함수 (React utils.ts 포팅) + // ============================================================ + + $calcWeight = function(string $material, float $width, float $height) use ($SUS_DENSITY, $EGI_DENSITY): array { + preg_match('/(\d+(\.\d+)?)/', $material, $m); + $thickness = $m ? (float)$m[1] : 0; + $isSUS = stripos($material, 'SUS') !== false; + $density = $isSUS ? $SUS_DENSITY : $EGI_DENSITY; + $volume_cm3 = ($thickness * $width * $height) / 1000; + $weight_kg = ($volume_cm3 * $density) / 1000; + return ['weight' => round($weight_kg, 2), 'type' => $isSUS ? 'SUS' : 'EGI']; + }; + + $parseBaseDimension = function(?string $baseDimension, float $fallbackHeight) use ($BASE_WIDTH): array { + if ($baseDimension) { + $parts = array_map('intval', explode('*', $baseDimension)); + if (count($parts) === 2 && $parts[0] > 0 && $parts[1] > 0) { + return ['width' => $parts[0], 'height' => $parts[1]]; + } + } + return ['width' => $BASE_WIDTH, 'height' => $fallbackHeight]; + }; + + // getMaterialMapping (React utils.ts 포팅) + $getMaterialMapping = function(string $productCode, string $finishMaterial): array { + if (in_array($productCode, ['KQTS01', 'KSS01', 'KSS02'])) { + return [ + 'guideRailFinish' => 'SUS 1.2T', 'bodyMaterial' => 'EGI 1.55T', + 'guideRailExtraFinish' => '', 'bottomBarFinish' => 'SUS 1.5T', 'bottomBarExtraFinish' => '없음', + ]; + } + if ($productCode === 'KTE01') { + $isSUS = $finishMaterial === 'SUS마감'; + return [ + 'guideRailFinish' => 'EGI 1.55T', 'bodyMaterial' => 'EGI 1.55T', + 'guideRailExtraFinish' => $isSUS ? 'SUS 1.2T' : '', + 'bottomBarFinish' => 'EGI 1.55T', 'bottomBarExtraFinish' => $isSUS ? 'SUS 1.2T' : '없음', + ]; + } + $isSUS = str_contains($finishMaterial ?? '', 'SUS'); + return [ + 'guideRailFinish' => 'EGI 1.55T', 'bodyMaterial' => 'EGI 1.55T', + 'guideRailExtraFinish' => $isSUS ? 'SUS 1.2T' : '', + 'bottomBarFinish' => 'EGI 1.55T', 'bottomBarExtraFinish' => $isSUS ? 'SUS 1.2T' : '없음', + ]; + }; + + // 가이드레일 행 생성 + $buildGuideRailRows = function(array $lengthData, string $baseDimension, array $mapping, string $productCode, string $type) use ($calcWeight, $parseBaseDimension, $WALL_PART_WIDTH, $SIDE_PART_WIDTH, $WALL_BASE_HEIGHT, $SIDE_BASE_HEIGHT): array { + $rows = []; + $isWall = $type === 'wall'; + $partWidth = $isWall ? $WALL_PART_WIDTH : $SIDE_PART_WIDTH; + $codePrefix = preg_replace('/\d+$/', '', $productCode) ?: 'KSS'; + $isSteel = $codePrefix === 'KTE'; + $isSUS = in_array($codePrefix, ['KSS', 'KQTS', 'KTE']); + $finishPrefix = $isWall ? ($isSUS ? 'RS' : 'RE') : ($isSUS ? 'SS' : 'SE'); + $bodyPrefix = $isWall ? ($isSteel ? 'RT' : 'RM') : ($isSteel ? 'ST' : 'SM'); + + foreach ($lengthData as $ld) { + if (($ld['quantity'] ?? 0) <= 0) continue; + $len = $ld['length']; $qty = $ld['quantity']; + + $fw = $calcWeight($mapping['guideRailFinish'], $partWidth, $len); + $rows[] = ['partName' => '①②마감재', 'material' => $mapping['guideRailFinish'], 'length' => $len, 'quantity' => $qty, 'weight' => round($fw['weight'] * $qty, 2), 'lotPrefix' => $finishPrefix]; + + $bw = $calcWeight($mapping['bodyMaterial'], $partWidth, $len); + $rows[] = ['partName' => '③본체', 'material' => $mapping['bodyMaterial'], 'length' => $len, 'quantity' => $qty, 'weight' => round($bw['weight'] * $qty, 2), 'lotPrefix' => $bodyPrefix]; + $rows[] = ['partName' => '④C형', 'material' => $mapping['bodyMaterial'], 'length' => $len, 'quantity' => $qty, 'weight' => round($bw['weight'] * $qty, 2), 'lotPrefix' => $isWall ? 'RC' : 'SC']; + $rows[] = ['partName' => '⑤D형', 'material' => $mapping['bodyMaterial'], 'length' => $len, 'quantity' => $qty, 'weight' => round($bw['weight'] * $qty, 2), 'lotPrefix' => $isWall ? 'RD' : 'SD']; + + if ($isWall && $mapping['guideRailExtraFinish']) { + $ew = $calcWeight($mapping['guideRailExtraFinish'], $partWidth, $len); + $rows[] = ['partName' => '⑥별도마감', 'material' => $mapping['guideRailExtraFinish'], 'length' => $len, 'quantity' => $qty, 'weight' => round($ew['weight'] * $qty, 2), 'lotPrefix' => 'YY']; + } + } + + $totalQty = array_sum(array_column($lengthData, 'quantity')); + if ($totalQty > 0) { + $baseDim = $parseBaseDimension($baseDimension, $isWall ? $WALL_BASE_HEIGHT : $SIDE_BASE_HEIGHT); + $baseW = $calcWeight('EGI 1.55T', $baseDim['width'], $baseDim['height']); + $rows[] = ['partName' => '하부BASE', 'material' => 'EGI 1.55T', 'length' => 0, 'quantity' => $totalQty, 'weight' => round($baseW['weight'] * $totalQty, 2), 'lotPrefix' => 'XX', 'isBase' => true]; + } + return $rows; + }; + + // 하단마감재 행 생성 + $buildBottomBarRows = function(array $bottomBar, array $mapping, string $productCode) use ($calcWeight, $BOTTOM_BAR_WIDTH, $EXTRA_FINISH_WIDTH): array { + $rows = []; + $codePrefix = preg_replace('/\d+$/', '', $productCode) ?: 'KSS'; + $isSteel = $codePrefix === 'KTE'; + $lotPrefix = $isSteel ? 'TS' : (str_contains($mapping['bottomBarFinish'], 'SUS') ? 'BS' : 'BE'); + + foreach ([3000, 4000] as $len) { + $qtyKey = "length{$len}Qty"; + $qty = $bottomBar[$qtyKey] ?? 0; + if ($qty <= 0) continue; + $w = $calcWeight($mapping['bottomBarFinish'], $BOTTOM_BAR_WIDTH, $len); + $rows[] = ['partName' => '①하단마감재', 'material' => $mapping['bottomBarFinish'], 'length' => $len, 'quantity' => $qty, 'weight' => round($w['weight'] * $qty, 2), 'lotPrefix' => $lotPrefix]; + } + + if ($mapping['bottomBarExtraFinish'] !== '없음' && $mapping['bottomBarExtraFinish']) { + foreach ([3000, 4000] as $len) { + $qtyKey = "length{$len}Qty"; + $qty = $bottomBar[$qtyKey] ?? 0; + if ($qty <= 0) continue; + $w = $calcWeight($mapping['bottomBarExtraFinish'], $EXTRA_FINISH_WIDTH, $len); + $rows[] = ['partName' => '④별도마감재', 'material' => $mapping['bottomBarExtraFinish'], 'length' => $len, 'quantity' => $qty, 'weight' => round($w['weight'] * $qty, 2), 'lotPrefix' => 'YY']; + } + } + return $rows; + }; + + // 셔터박스 행 생성 + $buildShutterBoxRows = function(array $box) use ($calcWeight, $BOX_FINISH_MATERIAL, $BOX_COVER_LENGTH): array { + $rows = []; + $sizeParts = array_map('intval', explode('*', $box['size'] ?? '500*380')); + $boxWidth = $sizeParts[0] ?: 500; + $boxHeight = $sizeParts[1] ?: 380; + $isStandard = $box['size'] === '500*380'; + $direction = $box['direction'] ?? '양면'; + + if ($isStandard) { + $parts = [ + ['name' => '①전면부', 'prefix' => 'CF', 'dim' => $boxHeight + 122], + ['name' => '②린텔부', 'prefix' => 'CL', 'dim' => $boxWidth - 330], + ['name' => '③⑤점검구', 'prefix' => 'CP', 'dim' => $boxWidth - 200], + ['name' => '④후면코너부', 'prefix' => 'CB', 'dim' => 170], + ]; + } elseif ($direction === '양면') { + $parts = [ + ['name' => '①전면부', 'prefix' => 'XX', 'dim' => $boxHeight + 122], + ['name' => '②린텔부', 'prefix' => 'CL', 'dim' => $boxWidth - 330], + ['name' => '③점검구', 'prefix' => 'XX', 'dim' => $boxWidth - 200], + ['name' => '④후면코너부', 'prefix' => 'CB', 'dim' => 170], + ['name' => '⑤점검구', 'prefix' => 'XX', 'dim' => $boxHeight - 100], + ]; + } elseif ($direction === '밑면') { + $parts = [ + ['name' => '①전면부', 'prefix' => 'XX', 'dim' => $boxHeight + 122], + ['name' => '②린텔부', 'prefix' => 'CL', 'dim' => $boxWidth - 330], + ['name' => '③점검구', 'prefix' => 'XX', 'dim' => $boxWidth - 200], + ['name' => '④후면부', 'prefix' => 'CB', 'dim' => $boxHeight + 85 * 2], + ]; + } elseif ($direction === '후면') { + $parts = [ + ['name' => '①전면부', 'prefix' => 'XX', 'dim' => $boxHeight + 122], + ['name' => '②린텔부', 'prefix' => 'CL', 'dim' => $boxWidth + 85 * 2], + ['name' => '③점검구', 'prefix' => 'XX', 'dim' => $boxHeight - 200], + ['name' => '④후면코너부', 'prefix' => 'CB', 'dim' => $boxHeight + 85 * 2], + ]; + } else { + $parts = []; + } + + foreach ($parts as $p) { + foreach ($box['lengthData'] ?? [] as $ld) { + if (($ld['quantity'] ?? 0) <= 0) continue; + $w = $calcWeight($BOX_FINISH_MATERIAL, $p['dim'], $ld['length']); + $rows[] = ['partName' => $p['name'], 'material' => $BOX_FINISH_MATERIAL, 'dimension' => (string)$ld['length'], 'quantity' => $ld['quantity'], 'weight' => round($w['weight'] * $ld['quantity'], 2), 'lotPrefix' => $p['prefix']]; + } + } + + $coverQty = $box['coverQty'] ?? 0; + if ($coverQty > 0) { + $coverWidth = $boxWidth - 111; + $w = $calcWeight($BOX_FINISH_MATERIAL, $coverWidth, $BOX_COVER_LENGTH); + $coverName = $isStandard ? '⑤상부덮개' : ($direction === '양면' ? '⑥상부덮개' : '⑤상부덮개'); + $rows[] = ['partName' => $coverName, 'material' => $BOX_FINISH_MATERIAL, 'dimension' => "1219 * {$coverWidth}", 'quantity' => $coverQty, 'weight' => round($w['weight'] * $coverQty, 2), 'lotPrefix' => 'XX']; + } + + $finCoverQty = $box['finCoverQty'] ?? 0; + if ($finCoverQty > 0) { + $w = $calcWeight($BOX_FINISH_MATERIAL, $boxWidth, $boxHeight); + $finName = $isStandard ? '⑥측면부(마구리)' : ($direction === '양면' ? '⑦측면부(마구리)' : '⑥측면부(마구리)'); + $rows[] = ['partName' => $finName, 'material' => $BOX_FINISH_MATERIAL, 'dimension' => ($boxWidth+5).' * '.($boxHeight+5), 'quantity' => $finCoverQty, 'weight' => round($w['weight'] * $finCoverQty, 2), 'lotPrefix' => 'XX']; + } + return $rows; + }; + + // lengthToCode (React utils.ts getSLengthCode 포팅) + $lengthToCode = function(int $length, string $category = ''): ?string { + if ($category === '연기차단재50') return match($length) { 3000 => '53', 4000 => '54', default => null }; + if ($category === '연기차단재80') return match($length) { 3000 => '83', 4000 => '84', default => null }; + $map = [1219 => '12', 2438 => '24', 3000 => '30', 3500 => '35', 4000 => '40', 4150 => '41', 4200 => '42', 4300 => '43']; + return $map[$length] ?? null; + }; + + // 연기차단재 행 생성 + $buildSmokeBarrierRows = function(array $smokeBarrier) use ($calcWeight, $SMOKE_BARRIER_WIDTH, $lengthToCode): array { + $rows = []; + foreach ($smokeBarrier['w50'] ?? [] as $ld) { + if (($ld['quantity'] ?? 0) <= 0) continue; + $w = $calcWeight('EGI 0.8T', $SMOKE_BARRIER_WIDTH, $ld['length']); + $code = $lengthToCode($ld['length'], '연기차단재50'); + $rows[] = ['partName' => '레일용 [W50]', 'material' => 'EGI 0.8T', 'length' => $ld['length'], 'quantity' => $ld['quantity'], 'weight' => round($w['weight'] * $ld['quantity'], 2), 'lotCode' => $code ? "GI-{$code}" : 'GI']; + } + $w80Qty = $smokeBarrier['w80Qty'] ?? 0; + if ($w80Qty > 0) { + $w = $calcWeight('EGI 0.8T', $SMOKE_BARRIER_WIDTH, 3000); + $code = $lengthToCode(3000, '연기차단재80'); + $rows[] = ['partName' => '케이스용 [W80]', 'material' => 'EGI 0.8T', 'length' => 3000, 'quantity' => $w80Qty, 'weight' => round($w['weight'] * $w80Qty, 2), 'lotCode' => $code ? "GI-{$code}" : 'GI']; + } + return $rows; + }; + + // 생산량 합계 계산 + $calculateProductionSummary = function(array $bi, array $mapping) use ($calcWeight, $parseBaseDimension, $buildShutterBoxRows, $buildSmokeBarrierRows, $WALL_PART_WIDTH, $SIDE_PART_WIDTH, $BOTTOM_BAR_WIDTH, $EXTRA_FINISH_WIDTH, $WALL_BASE_HEIGHT, $SIDE_BASE_HEIGHT): array { + $susTotal = 0; $egiTotal = 0; + $addWeight = function(string $mat, float $w, float $h, int $qty) use (&$susTotal, &$egiTotal, $calcWeight) { + if ($qty <= 0) return; + $r = $calcWeight($mat, $w, $h); + $total = $r['weight'] * $qty; + if ($r['type'] === 'SUS') $susTotal += $total; else $egiTotal += $total; + }; + // 가이드레일 벽면형 + if (!empty($bi['guideRail']['wall'])) { + foreach ($bi['guideRail']['wall']['lengthData'] ?? [] as $ld) { + if (($ld['quantity'] ?? 0) <= 0) continue; + $addWeight($mapping['guideRailFinish'], $WALL_PART_WIDTH, $ld['length'], $ld['quantity']); + $addWeight($mapping['bodyMaterial'], $WALL_PART_WIDTH, $ld['length'], $ld['quantity'] * 3); + if ($mapping['guideRailExtraFinish']) $addWeight($mapping['guideRailExtraFinish'], $WALL_PART_WIDTH, $ld['length'], $ld['quantity']); + } + $totalWallQty = array_sum(array_column($bi['guideRail']['wall']['lengthData'] ?? [], 'quantity')); + $wallBase = $parseBaseDimension($bi['guideRail']['wall']['baseDimension'] ?? null, $WALL_BASE_HEIGHT); + $addWeight('EGI 1.55T', $wallBase['width'], $wallBase['height'], $totalWallQty); + } + // 가이드레일 측면형 + if (!empty($bi['guideRail']['side'])) { + foreach ($bi['guideRail']['side']['lengthData'] ?? [] as $ld) { + if (($ld['quantity'] ?? 0) <= 0) continue; + $addWeight($mapping['guideRailFinish'], $SIDE_PART_WIDTH, $ld['length'], $ld['quantity']); + $addWeight($mapping['bodyMaterial'], $SIDE_PART_WIDTH, $ld['length'], $ld['quantity'] * 3); + } + $totalSideQty = array_sum(array_column($bi['guideRail']['side']['lengthData'] ?? [], 'quantity')); + $sideBase = $parseBaseDimension($bi['guideRail']['side']['baseDimension'] ?? null, $SIDE_BASE_HEIGHT); + $addWeight('EGI 1.55T', $sideBase['width'], $sideBase['height'], $totalSideQty); + } + // 하단마감재 + foreach ([3000, 4000] as $len) { + $qty = $bi['bottomBar']["length{$len}Qty"] ?? 0; + if ($qty > 0) { + $addWeight($mapping['bottomBarFinish'], $BOTTOM_BAR_WIDTH, $len, $qty); + if ($mapping['bottomBarExtraFinish'] !== '없음' && $mapping['bottomBarExtraFinish']) { + $addWeight($mapping['bottomBarExtraFinish'], $EXTRA_FINISH_WIDTH, $len, $qty); + } + } + } + // 셔터박스 + foreach ($bi['shutterBox'] ?? [] as $box) { + foreach ($buildShutterBoxRows($box) as $row) { + if ($row['weight'] > 0) $egiTotal += $row['weight']; + } + } + // 연기차단재 + foreach ($buildSmokeBarrierRows($bi['smokeBarrier'] ?? []) as $row) { + if ($row['weight'] > 0) $egiTotal += $row['weight']; + } + return ['susTotal' => round($susTotal, 2), 'egiTotal' => round($egiTotal, 2), 'grandTotal' => round($susTotal + $egiTotal, 2)]; + }; + + // 포맷 헬퍼 + $fmt = fn($v) => ($v !== null && $v > 0) ? number_format($v) : '-'; + $fmtWeight = fn($v) => $v > 0 ? number_format($v, 2) : '-'; + + // lookupLotNo (React utils.ts 포팅 - lotNoMap에서 LOT NO 조회) + $lookupLotNo = function(array $lotNoMap, string $prefix, ?int $length = null) use ($lengthToCode): string { + if (empty($lotNoMap)) return '-'; + // 1. 정확한 매칭 (prefix + lengthCode) + if ($length) { + $code = $lengthToCode($length); + if ($code && isset($lotNoMap["BD-{$prefix}-{$code}"])) return $lotNoMap["BD-{$prefix}-{$code}"]; + } + // 2. Fallback: prefix만으로 매칭 + $prefixKey = "BD-{$prefix}-"; + foreach ($lotNoMap as $k => $v) { + if (str_starts_with($k, $prefixKey)) return $v; + } + return '-'; + }; + + // ============================================================ + // 데이터 준비 + // ============================================================ + $bi = $bendingInfo ?? []; + $hasBendingData = !empty($bi['productCode']); + $mapping = $hasBendingData ? $getMaterialMapping($bi['productCode'], $bi['finishMaterial'] ?? '') : null; + $summary = ($hasBendingData && $mapping) ? $calculateProductionSummary($bi, $mapping) : ['susTotal' => 0, 'egiTotal' => 0, 'grandTotal' => 0]; + + // 날짜 포맷 + $today = now()->format('Y-m-d'); + $fullDate = now()->format('Y년 n월 j일'); + + // 기본 정보 + $documentNo = $workOrder->work_order_no ?? '-'; + $primaryAssignee = '-'; + // work_order_assignees에서 primary 찾기 + if ($workOrder) { + $assignee = \Illuminate\Support\Facades\DB::table('work_order_assignees') + ->join('users', 'users.id', '=', 'work_order_assignees.user_id') + ->where('work_order_assignees.work_order_id', $workOrder->id) + ->where('work_order_assignees.is_primary', true) + ->select('users.name') + ->first(); + $primaryAssignee = $assignee->name ?? '-'; + } + $dueDate = $salesOrder->delivery_date ?? $workOrder->scheduled_date ?? null; + $formattedDueDate = $dueDate ? \Carbon\Carbon::parse($dueDate)->format('Y-m-d') : '-'; + + // 담당자(수주 작성자) 조회 + $salesWriterName = '-'; + if ($salesOrder && $salesOrder->writer_id) { + $salesWriter = \Illuminate\Support\Facades\DB::table('users')->where('id', $salesOrder->writer_id)->first(); + $salesWriterName = $salesWriter->name ?? '-'; + } + + // LOT NO (work_order에 lot_no 컬럼 없음 - work_order_no 활용) + $lotNo = $workOrder->work_order_no ?? '-'; + + // lotNoMap 빌드: item.code (BD-{prefix}-{lengthCode}) → lot_no 매핑 + $lotNoMap = []; + if ($workOrder) { + $lotRows = \Illuminate\Support\Facades\DB::table('work_order_material_inputs as mi') + ->join('stock_lots as sl', 'sl.id', '=', 'mi.stock_lot_id') + ->join('items as i', 'i.id', '=', 'mi.item_id') + ->where('mi.work_order_id', $workOrder->id) + ->select('i.code', 'sl.lot_no') + ->distinct() + ->get(); + foreach ($lotRows as $lr) { + if ($lr->code && !isset($lotNoMap[$lr->code])) { + $lotNoMap[$lr->code] = $lr->lot_no; + } + } + } +@endphp + +
+ {{-- ===== 헤더 ===== --}} +
+
+

작업일지 (절곡)

+

+ 문서번호: {{ $documentNo }} | 작성일자: {{ $fullDate }} +

+
+ {{-- 결재란 --}} + + + + + + + + + + + + + + + +
작성검토승인
{{ $primaryAssignee }}
+
+ + {{-- ===== 신청업체 / 신청내용 ===== --}} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
신청업체신청내용
수주일{{ $salesOrder?->received_at ? \Carbon\Carbon::parse($salesOrder->received_at)->format('Y-m-d') : '-' }}현장명{{ $salesOrder->site_name ?? $workOrder->project_name ?? '-' }}
수주처{{ $salesOrder->client_name ?? '-' }}작업일자{{ $today }}
담당자{{ $salesWriterName }}제품 LOT NO{{ $lotNo }}
연락처{{ $salesOrder->client_contact ?? '-' }}생산담당자{{ $primaryAssignee }}
출고예정일{{ $formattedDueDate }}
+ + {{-- ===== 제품 정보 ===== --}} + + + + + + + + + + + + + + + + + +
제품명재질마감유형
{{ $hasBendingData ? $bi['productCode'] : '-' }}{{ $mapping['bodyMaterial'] ?? '-' }}{{ $hasBendingData ? $bi['finishMaterial'] : '-' }}{{ $hasBendingData ? ($bi['common']['type'] ?? '-') : '-' }}
+ + @if($hasBendingData && $mapping) + {{-- ===== 1. 가이드레일 ===== --}} + @php + $wallRows = !empty($bi['guideRail']['wall']) ? $buildGuideRailRows($bi['guideRail']['wall']['lengthData'] ?? [], $bi['guideRail']['wall']['baseDimension'] ?? '135*80', $mapping, $bi['productCode'], 'wall') : []; + $sideRows = !empty($bi['guideRail']['side']) ? $buildGuideRailRows($bi['guideRail']['side']['lengthData'] ?? [], $bi['guideRail']['side']['baseDimension'] ?? '135*130', $mapping, $bi['productCode'], 'side') : []; + @endphp + @if(count($wallRows) > 0 || count($sideRows) > 0) +
+
1. 가이드레일
+ + @foreach(['wall' => $wallRows, 'side' => $sideRows] as $grType => $grRows) + @if(count($grRows) > 0) + @php + $grTitle = $grType === 'wall' ? '1.1 벽면형' : '1.2 측면형'; + $grData = $bi['guideRail'][$grType] ?? []; + $baseSize = $grData['baseDimension'] ?? $grData['baseSize'] ?? '-'; + @endphp +
+
{{ $grTitle }}
+
+ + + + + + + + + + + + + @foreach($grRows as $row) + + + + + + + + + @endforeach + +
세부품명재질길이수량LOT NO무게(kg)
{{ $row['partName'] }}{{ $row['material'] }}{{ ($row['isBase'] ?? false) ? $baseSize : $fmt($row['length']) }}{{ $fmt($row['quantity']) }}{{ $lookupLotNo($lotNoMap, $row['lotPrefix'], $row['length'] ?? null) }}{{ $fmtWeight($row['weight']) }}
+
+
+ @endif + @endforeach +
+ @endif + + {{-- ===== 2. 하단마감재 ===== --}} + @php + $bottomRows = $buildBottomBarRows($bi['bottomBar'] ?? [], $mapping, $bi['productCode']); + @endphp + @if(count($bottomRows) > 0) +
+
2. 하단마감재
+
+ + + + + + + + + + + + + @foreach($bottomRows as $row) + + + + + + + + + @endforeach + +
세부품명재질길이수량LOT NO무게(kg)
{{ $row['partName'] }}{{ $row['material'] }}{{ $fmt($row['length']) }}{{ $fmt($row['quantity']) }}{{ $lookupLotNo($lotNoMap, $row['lotPrefix'], $row['length'] ?? null) }}{{ $fmtWeight($row['weight']) }}
+
+
+ @endif + + {{-- ===== 3. 셔터박스 ===== --}} + @if(!empty($bi['shutterBox'])) +
+
3. 셔터박스
+ @foreach($bi['shutterBox'] as $boxIdx => $box) + @php $boxRows = $buildShutterBoxRows($box); @endphp + @if(count($boxRows) > 0) +
+
3.{{ $boxIdx + 1 }} 셔터박스 [{{ $box['size'] ?? '-' }}] {{ $box['direction'] ?? '' }}
+
+ + + + + + + + + + + + + @foreach($boxRows as $row) + @php + // 셔터박스 LOT: dimension에서 숫자 추출하여 길이코드로 변환 + $boxDimNum = is_numeric($row['dimension']) ? (int)$row['dimension'] : null; + $boxLot = $boxDimNum ? $lookupLotNo($lotNoMap, $row['lotPrefix'], $boxDimNum) : $lookupLotNo($lotNoMap, $row['lotPrefix']); + @endphp + + + + + + + + + @endforeach + +
구성요소재질길이/치수수량LOT NO무게(kg)
{{ $row['partName'] }}{{ $row['material'] }}{{ $row['dimension'] }}{{ $fmt($row['quantity']) }}{{ $boxLot }}{{ $fmtWeight($row['weight']) }}
+
+
+ @endif + @endforeach +
+ @endif + + {{-- ===== 4. 연기차단재 ===== --}} + @php + $smokeRows = $buildSmokeBarrierRows($bi['smokeBarrier'] ?? []); + @endphp + @if(count($smokeRows) > 0) +
+
4. 연기차단재
+
+ + + + + + + + + + + + + @foreach($smokeRows as $row) + @php + // 연기차단재 LOT: lotCode (GI-53, GI-83 등) 사용 + $smokeLotCode = $row['lotCode'] ?? 'GI'; + $smokeLot = isset($lotNoMap["BD-{$smokeLotCode}"]) ? $lotNoMap["BD-{$smokeLotCode}"] : $lookupLotNo($lotNoMap, explode('-', $smokeLotCode)[0], $row['length']); + @endphp + + + + + + + + + @endforeach + +
파트재질길이수량LOT NO무게(kg)
{{ $row['partName'] }}{{ $row['material'] }}{{ $fmt($row['length']) }}{{ $fmt($row['quantity']) }}{{ $smokeLot }}{{ $fmtWeight($row['weight']) }}
+
+
+ @endif + @else +
+ 절곡 데이터가 없습니다. (bending_info 미등록) +
+ @endif + + {{-- ===== 생산량 합계 ===== --}} +
+ + + + + + + + + + + + + + + + + +
생산량 합계 [kg]SUSEGI합계
무게{{ $summary['susTotal'] > 0 ? number_format($summary['susTotal'], 2) . ' kg' : '-' }}{{ $summary['egiTotal'] > 0 ? number_format($summary['egiTotal'], 2) . ' kg' : '-' }}{{ $summary['grandTotal'] > 0 ? number_format($summary['grandTotal'], 2) . ' kg' : '-' }}
+
+ + {{-- ===== 비고 ===== --}} + + + + + + + +
비고 + {{ $workOrder->memo ?? '' }} +
+
\ No newline at end of file diff --git a/resources/views/documents/print.blade.php b/resources/views/documents/print.blade.php index 1d1ff7c1..a61de1ab 100644 --- a/resources/views/documents/print.blade.php +++ b/resources/views/documents/print.blade.php @@ -25,6 +25,13 @@ class="bg-gray-200 hover:bg-gray-300 text-gray-700 px-4 py-2 rounded-lg transiti {{-- 성적서 본문 --}}
+ {{-- HTML 스냅샷 우선 출력 (React에서 저장한 rendered_html) --}} + @if($document->rendered_html) +
+ {!! $document->rendered_html !!} +
+ @else + {{-- 레거시: 템플릿 기반 동적 렌더링 --}} @php $template = $document->template; $hasComplexCol = $template->columns->contains(fn($c) => $c->column_type === 'complex' && $c->sub_labels); @@ -364,6 +371,7 @@ class="doc-th"
@endif + @endif {{-- 스냅샷 vs 레거시 분기 끝 --}} diff --git a/resources/views/documents/show.blade.php b/resources/views/documents/show.blade.php index fff0ed0b..ea480e4a 100644 --- a/resources/views/documents/show.blade.php +++ b/resources/views/documents/show.blade.php @@ -3,6 +3,7 @@ @section('title', '문서 상세') @section('content') + @php $docData = $document->getRelation('data') ?? $document->data()->get(); @endphp
@@ -107,16 +108,24 @@ class="bg-gray-200 hover:bg-gray-300 text-gray-700 px-4 py-2 rounded-lg transiti
+ {{-- HTML 스냅샷 우선 출력 (React에서 저장한 rendered_html) --}} + @if($document->rendered_html) +
+
+ {!! $document->rendered_html !!} +
+
+ {{-- 블록 빌더 서식: 블록 렌더러로 조회 --}} - @if($document->template?->isBlockBuilder() && !empty($document->template->schema)) + @elseif($document->template?->isBlockBuilder() && !empty($document->template->schema))

{{ $document->template->title ?? $document->template->name }}

{!! $blockHtml !!}
@else - {{-- 기본 필드 데이터 (레거시 서식) --}} - @if($document->template?->basicFields && $document->template->basicFields->count() > 0) + {{-- 기본 필드 데이터 (절곡 작업일지는 전용 partial에서 렌더링하므로 스킵) --}} + @if($document->template?->basicFields && $document->template->basicFields->count() > 0 && !(str_contains($document->template->name ?? '', '절곡') && str_contains($document->template->name ?? '', '작업일지')))

{{ $document->template->title ?? '문서 정보' }}

@@ -124,10 +133,10 @@ class="bg-gray-200 hover:bg-gray-300 text-gray-700 px-4 py-2 rounded-lg transiti @foreach($document->template->basicFields as $field) @php $fieldKey = 'bf_' . $field->id; - $fieldData = $document->data->where('field_key', $fieldKey)->first(); + $fieldData = $docData->where('field_key', $fieldKey)->first(); // 레거시 호환: bf_{label} 형식으로 저장된 데이터도 조회 if (!$fieldData) { - $fieldData = $document->data->where('field_key', 'bf_' . $field->label)->first(); + $fieldData = $docData->where('field_key', 'bf_' . $field->label)->first(); } $value = $fieldData?->field_value ?? '-'; @endphp @@ -140,8 +149,11 @@ class="bg-gray-200 hover:bg-gray-300 text-gray-700 px-4 py-2 rounded-lg transiti
@endif - {{-- 작업일지 전용: 작업내역 + 자재LOT + 통계 (섹션 없는 work_order 연결 문서) --}} - @if($document->linkable_type === 'work_order' && $workOrderItems->isNotEmpty() && (!$document->template?->sections || $document->template->sections->count() === 0)) + {{-- 절곡 작업일지 전용 렌더링 (React BendingWorkLogContent와 동일 구조) --}} + @if($document->linkable_type === 'work_order' && $document->template && str_contains($document->template->name ?? '', '절곡') && str_contains($document->template->name ?? '', '작업일지')) + @include('documents.partials.bending-worklog') + @elseif($document->linkable_type === 'work_order' && $workOrderItems->isNotEmpty() && (!$document->template?->sections || $document->template->sections->count() === 0)) + {{-- 일반 작업일지: 작업내역 + 자재LOT + 통계 (섹션 없는 work_order 연결 문서) --}}

작업 내역

@@ -328,7 +340,7 @@ class="px-2 py-2 text-center text-xs font-medium text-gray-600 border border-gra {{-- 작업 통계 (document_data에서 stats_ 조회) --}} @php - $statsData = $document->data->filter(fn($d) => str_starts_with($d->field_key, 'stats_')); + $statsData = $docData->filter(fn($d) => str_starts_with($d->field_key, 'stats_')); $statsMap = $statsData->pluck('field_value', 'field_key')->toArray(); @endphp @if(!empty($statsMap)) @@ -392,7 +404,7 @@ class="px-2 py-2 text-center text-xs font-medium text-gray-600 border border-gra {{-- 비고 (document_data에서 remarks 조회) --}} @php - $remarksData = $document->data->where('field_key', 'remarks')->first(); + $remarksData = $docData->where('field_key', 'remarks')->first(); @endphp @if($remarksData && $remarksData->field_value)
@@ -402,6 +414,29 @@ class="px-2 py-2 text-center text-xs font-medium text-gray-600 border border-gra @endif @endif + {{-- 절곡 중간검사 DATA 전용 렌더링 (React BendingInspectionContent와 동일 구조) --}} + @if($document->template && str_contains($document->template->name ?? '', '절곡') && str_contains($document->template->name ?? '', '검사')) + @php + $docData = $document->getRelation('data') ?? $document->data()->get(); + @endphp + {{-- 검사기준서 섹션 (이미지만 렌더링) --}} + @foreach(($document->template->sections ?? collect()) as $section) + @if($section->title !== '중간검사 DATA' && $section->image_path) +
+

{{ $section->title }}

+ @php + $sectionImgUrl = preg_match('/^\d+\//', $section->image_path) + ? rtrim(config('app.api_url', 'http://api.sam.kr'), '/') . '/storage/tenants/' . $section->image_path + : asset('storage/' . $section->image_path); + @endphp + {{ $section->title }} +
+ @endif + @endforeach + {{-- 중간검사 DATA 테이블 (rowSpan + 간격 포인트) --}} + @include('documents.partials.bending-inspection-data', ['inspectionData' => $inspectionData ?? null]) + @else + {{-- 섹션 데이터 (테이블) --}} @if($document->template?->sections && $document->template->sections->count() > 0) @foreach($document->template->sections as $section) @@ -421,9 +456,9 @@ class="px-2 py-2 text-center text-xs font-medium text-gray-600 border border-gra @if($section->items->count() > 0 && $document->template->columns->count() > 0) @php // 데이터 조회 헬퍼: 정규화 형식 우선, 레거시 fallback - $getData = function($sectionId, $colId, $rowIdx, $fieldKey) use ($document) { + $getData = function($sectionId, $colId, $rowIdx, $fieldKey) use ($docData) { // 1차: 정규화 형식 (section_id + column_id + row_index + field_key) - $record = $document->data + $record = $docData ->where('section_id', $sectionId) ->where('column_id', $colId) ->where('row_index', $rowIdx) @@ -433,9 +468,9 @@ class="px-2 py-2 text-center text-xs font-medium text-gray-600 border border-gra }; // 레거시 데이터 조회 헬퍼 (수입검사 호환: {itemId}_n{n}, {itemId}_result 등) - $getLegacyData = function($itemId, $fieldKey) use ($document) { - return $document->data->where('field_key', "{$itemId}_{$fieldKey}")->first()?->field_value - ?? $document->data->where('field_key', $fieldKey)->first()?->field_value + $getLegacyData = function($itemId, $fieldKey) use ($docData) { + return $docData->where('field_key', "{$itemId}_{$fieldKey}")->first()?->field_value + ?? $docData->where('field_key', $fieldKey)->first()?->field_value ?? ''; }; @@ -526,7 +561,7 @@ class="px-2 py-2 text-center text-xs font-medium text-gray-600 uppercase border // fallback: document_data의 max row_index + 1 $rowCount = $workOrderItems->isNotEmpty() ? $workOrderItems->count() - : max(1, ($document->data->where('section_id', $section->id)->max('row_index') ?? 0) + 1); + : max(1, ($docData->where('section_id', $section->id)->max('row_index') ?? 0) + 1); @endphp @for($rowIndex = 0; $rowIndex < $rowCount; $rowIndex++) @@ -628,7 +663,7 @@ class="px-2 py-2 text-center text-xs font-medium text-gray-600 uppercase border @php $savedVal = $getData($section->id, $col->id, $rowIndex, 'value'); if (!$savedVal) { - $rowJudgment = $document->data->where('field_key', 'row_judgment')->where('row_index', $rowIndex)->first()?->field_value ?? ''; + $rowJudgment = $docData->where('field_key', 'row_judgment')->where('row_index', $rowIndex)->first()?->field_value ?? ''; if (in_array(strtolower($rowJudgment), ['pass', 'ok', '적합'])) $savedVal = 'OK'; elseif (in_array(strtolower($rowJudgment), ['fail', 'ng', '부적합'])) $savedVal = 'NG'; } @@ -692,7 +727,7 @@ class="px-2 py-2 text-center text-xs font-medium text-gray-600 uppercase border @php $savedVal = $getData($section->id, $col->id, $rowIndex, 'value'); if (!$savedVal) { - $rowJudgment = $document->data->where('field_key', 'row_judgment')->where('row_index', $rowIndex)->first()?->field_value ?? ''; + $rowJudgment = $docData->where('field_key', 'row_judgment')->where('row_index', $rowIndex)->first()?->field_value ?? ''; if (in_array(strtolower($rowJudgment), ['pass', 'ok', '적합'])) $savedVal = 'OK'; elseif (in_array(strtolower($rowJudgment), ['fail', 'ng', '부적합'])) $savedVal = 'NG'; } @@ -734,15 +769,15 @@ class="px-2 py-2 text-center text-xs font-medium text-gray-600 uppercase border @if($loop->last) @php // React 형식: overall_result, remark (+ 레거시 호환: footer_judgement, footer_remark) - $remarkVal = $document->data->where('field_key', 'remark')->first()?->field_value - ?? $document->data->where('field_key', 'footer_remark')->first()?->field_value + $remarkVal = $docData->where('field_key', 'remark')->first()?->field_value + ?? $docData->where('field_key', 'footer_remark')->first()?->field_value ?? ''; - $judgementVal = $document->data->where('field_key', 'overall_result')->first()?->field_value - ?? $document->data->where('field_key', 'footer_judgement')->first()?->field_value + $judgementVal = $docData->where('field_key', 'overall_result')->first()?->field_value + ?? $docData->where('field_key', 'footer_judgement')->first()?->field_value ?? ''; // fallback: overall_result 없으면 row_judgment에서 계산 (전체 행 수 대비 판정) if (!$judgementVal) { - $rowJudgments = $document->data->where('field_key', 'row_judgment')->pluck('field_value'); + $rowJudgments = $docData->where('field_key', 'row_judgment')->pluck('field_value'); if ($rowJudgments->isNotEmpty() && $rowJudgments->count() >= $rowCount) { $hasFail = $rowJudgments->contains(fn($v) => in_array(strtolower($v), ['fail', '부', '부적합', '불합격'])); $allPass = $rowJudgments->every(fn($v) => in_array(strtolower($v), ['pass', '적', '적합', '합격'])); @@ -775,7 +810,9 @@ class="px-2 py-2 text-center text-xs font-medium text-gray-600 uppercase border @endforeach @endif - @endif {{-- end: 블록 빌더 vs 레거시 분기 --}} + @endif {{-- end: 스냅샷 vs 블록빌더 vs 레거시 분기 --}} + + @endif {{-- end: 절곡 검사 vs 일반 섹션 분기 --}} {{-- 첨부파일 --}} @if($document->attachments && $document->attachments->count() > 0)