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)
+ {{-- 분류 --}}
+ |
+ {{ $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 ?: '-' }}
+ |
+ @endif
+
+ {{-- 간격 포인트 --}}
+ @if(!empty($gapPoints))
+ @php $gap = $gapPoints[$gIdx] ?? []; @endphp
+
+ {{ $gap['point'] ?? '' }}
+ |
+
+ {{ $gap['designValue'] ?? '' }}
+ |
+
+ {{ ($gap['measured'] ?? '') ?: '-' }}
+ |
+ @else
+ - |
+ - |
+ - |
+ @endif
+
+ @if($gIdx === 0)
+ {{-- 판정 --}}
+
+ @if($judgmentVal === '적')
+
+
+ 적
+
+ @elseif($judgmentVal === '부')
+
+
+ 부
+
+ @else
+ -
+ @endif
+ |
+ @endif
+
+ @endfor
+ @endforeach
+
+
+
+
+ {{-- 종합판정 --}}
+
+
종합판정:
+ @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 }}
+
+
+
+
+ | 세부품명 |
+ 재질 |
+ 길이 |
+ 수량 |
+ LOT NO |
+ 무게(kg) |
+
+
+
+ @foreach($grRows as $row)
+
+ | {{ $row['partName'] }} |
+ {{ $row['material'] }} |
+ {{ ($row['isBase'] ?? false) ? $baseSize : $fmt($row['length']) }} |
+ {{ $fmt($row['quantity']) }} |
+ {{ $lookupLotNo($lotNoMap, $row['lotPrefix'], $row['length'] ?? null) }} |
+ {{ $fmtWeight($row['weight']) }} |
+
+ @endforeach
+
+
+
+
+ @endif
+ @endforeach
+
+ @endif
+
+ {{-- ===== 2. 하단마감재 ===== --}}
+ @php
+ $bottomRows = $buildBottomBarRows($bi['bottomBar'] ?? [], $mapping, $bi['productCode']);
+ @endphp
+ @if(count($bottomRows) > 0)
+
+
2. 하단마감재
+
+
+
+
+ | 세부품명 |
+ 재질 |
+ 길이 |
+ 수량 |
+ LOT NO |
+ 무게(kg) |
+
+
+
+ @foreach($bottomRows as $row)
+
+ | {{ $row['partName'] }} |
+ {{ $row['material'] }} |
+ {{ $fmt($row['length']) }} |
+ {{ $fmt($row['quantity']) }} |
+ {{ $lookupLotNo($lotNoMap, $row['lotPrefix'], $row['length'] ?? null) }} |
+ {{ $fmtWeight($row['weight']) }} |
+
+ @endforeach
+
+
+
+
+ @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'] ?? '' }}
+
+
+
+
+ | 구성요소 |
+ 재질 |
+ 길이/치수 |
+ 수량 |
+ LOT NO |
+ 무게(kg) |
+
+
+
+ @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
+
+ | {{ $row['partName'] }} |
+ {{ $row['material'] }} |
+ {{ $row['dimension'] }} |
+ {{ $fmt($row['quantity']) }} |
+ {{ $boxLot }} |
+ {{ $fmtWeight($row['weight']) }} |
+
+ @endforeach
+
+
+
+
+ @endif
+ @endforeach
+
+ @endif
+
+ {{-- ===== 4. 연기차단재 ===== --}}
+ @php
+ $smokeRows = $buildSmokeBarrierRows($bi['smokeBarrier'] ?? []);
+ @endphp
+ @if(count($smokeRows) > 0)
+
+
4. 연기차단재
+
+
+
+
+ | 파트 |
+ 재질 |
+ 길이 |
+ 수량 |
+ LOT NO |
+ 무게(kg) |
+
+
+
+ @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
+
+ | {{ $row['partName'] }} |
+ {{ $row['material'] }} |
+ {{ $fmt($row['length']) }} |
+ {{ $fmt($row['quantity']) }} |
+ {{ $smokeLot }} |
+ {{ $fmtWeight($row['weight']) }} |
+
+ @endforeach
+
+
+
+
+ @endif
+ @else
+
+ 절곡 데이터가 없습니다. (bending_info 미등록)
+
+ @endif
+
+ {{-- ===== 생산량 합계 ===== --}}
+
+
+
+
+ | 생산량 합계 [kg] |
+ SUS |
+ EGI |
+ 합계 |
+
+
+
+
+ | 무게 |
+ {{ $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
+

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