@extends('layouts.app') @section('title', '문서 상세') @section('content') @php $docData = $document->getRelation('data') ?? $document->data()->get(); @endphp

문서 상세

@if($document->canEdit()) 수정 @endif @if($document->isPending()) @endif 성적서 목록
{{-- 문서 정보 --}}
{{-- 메인 컨텐츠 --}}
{{-- 기본 정보 --}}

기본 정보

문서번호
{{ $document->document_no }}
템플릿
{{ $document->template->name ?? '-' }}
제목
{{ $document->title }}
상태
{{ $document->status_label }}
작성자
{{ $document->creator->name ?? '-' }}
작성일
{{ $document->created_at?->format('Y-m-d H:i') ?? '-' }}
@if($document->updated_at && $document->updated_at->ne($document->created_at))
수정자
{{ $document->updater->name ?? '-' }}
수정일
{{ $document->updated_at?->format('Y-m-d H:i') ?? '-' }}
@endif
{{-- HTML 스냅샷 우선 출력 (React에서 저장한 rendered_html) --}} @if($document->rendered_html)
{!! $document->rendered_html !!}
{{-- 블록 빌더 서식: 블록 렌더러로 조회 --}} @elseif($document->template?->isBlockBuilder() && !empty($document->template->schema))

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

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

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

@foreach($document->template->basicFields as $field) @php $fieldKey = 'bf_' . $field->id; $fieldData = $docData->where('field_key', $fieldKey)->first(); // 레거시 호환: bf_{label} 형식으로 저장된 데이터도 조회 if (!$fieldData) { $fieldData = $docData->where('field_key', 'bf_' . $field->label)->first(); } $value = $fieldData?->field_value ?? '-'; @endphp
{{ $field->label }}
{{ $value }}
@endforeach
@endif {{-- 절곡 작업일지 전용 렌더링 (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 연결 문서) --}}

작업 내역

{{-- 작업지시 기본정보 --}} @if($workOrder)
작업지시번호
{{ $workOrder->work_order_no ?? '-' }}
상태
@php $statusLabels = ['pending' => '대기', 'in_progress' => '진행중', 'completed' => '완료', 'shipped' => '출고']; $statusColors = ['pending' => 'bg-gray-100 text-gray-700', 'in_progress' => 'bg-blue-100 text-blue-700', 'completed' => 'bg-green-100 text-green-700', 'shipped' => 'bg-purple-100 text-purple-700']; @endphp {{ $statusLabels[$workOrder->status] ?? $workOrder->status }}
@if($salesOrder)
발주처
{{ $salesOrder->client_name ?? '-' }}
현장명
{{ $salesOrder->site_name ?? $workOrder->project_name ?? '-' }}
@endif
@endif {{-- 품목 테이블: 템플릿 컬럼 기반 렌더링 --}} @php $templateColumns = $document->template->columns ?? collect(); $hasComplexCol = $templateColumns->contains(fn($c) => $c->column_type === 'complex' && $c->sub_labels); // 개소별 투입자재 LOT 매핑 (work_order_item_id → lot_no) $itemLotMapData = $itemLotMap ?? collect(); // 재단 알고리즘 (React ScreenWorkLogContent calculateCutSize 동일) $fabricConfigs = [ '실리카' => ['width' => 1220, 'threshold' => 940, 'ranges' => ['900' => [841,940], '800' => [641,840], '600' => [441,640], '400' => [341,440], '300' => [1,340]]], '와이어' => ['width' => 1100, 'threshold' => 860, 'ranges' => ['900' => [761,860], '800' => [561,760], '600' => [361,560], '400' => [261,360], '300' => [1,260]]], '화이바' => ['width' => 1100, 'threshold' => 860, 'ranges' => ['900' => [761,860], '800' => [561,760], '600' => [361,560], '400' => [261,360], '300' => [1,260]]], ]; $detectFabricType = function($name) { if (str_contains($name, '와이어')) return '와이어'; if (str_contains($name, '화이바')) return '화이바'; return '실리카'; }; $calculateCutSize = function($fabricType, $height) use ($fabricConfigs) { if (!$height || $height <= 0) return null; $cfg = $fabricConfigs[$fabricType] ?? $fabricConfigs['실리카']; $w = $cfg['width']; $makeVertical = $height + 140 + floor($height / $w) * 40; $firstCut = floor($makeVertical / $w); $remaining = $makeVertical - ($firstCut * $w); if ($remaining > $cfg['threshold']) { $firstCut++; $remaining = $makeVertical - ($firstCut * $w); } $sizes = []; foreach ($cfg['ranges'] as $key => [$min, $max]) { $sizes[$key] = ($remaining >= $min && $remaining <= $max) ? 1 : 0; } return ['firstCut' => $firstCut, 'remaining' => $remaining, 'sizes' => $sizes, 'baseWidth' => $w]; }; // 각 품목별 재단 계산 $itemCuts = $workOrderItems->map(function($item) use ($detectFabricType, $calculateCutSize) { $opts = $item->options ?? []; $ft = $detectFabricType($item->item_name ?? ''); return $calculateCutSize($ft, $opts['height'] ?? 0); }); // 컬럼 label → 셀 값 매핑 함수 $getCellValue = function($col, $item, $index) use ($itemLotMapData, $itemCuts) { $label = trim($col->label); $opts = $item->options ?? []; $cut = $itemCuts[$index] ?? null; $lowerLabel = mb_strtolower($label); if (str_contains($lowerLabel, 'no') && !str_contains($label, 'LOT')) return $index + 1; if (str_contains($label, 'LOT')) return $itemLotMapData[$item->id] ?? '-'; if (str_contains($label, '제품명')) return $item->item_name ?? '-'; if (str_contains($label, '부호')) return $opts['code'] ?? '-'; if (str_contains($label, '나머지높이')) return $cut && $cut['remaining'] > 0 ? $cut['remaining'] : ''; return '-'; }; // complex 컬럼 sub_label별 값 매핑 $getSubCellValue = function($col, $subLabel, $subIndex, $item, $index) use ($itemCuts) { $label = trim($col->label); $opts = $item->options ?? []; $cut = $itemCuts[$index] ?? null; // 제작사이즈(mm): 가로, 세로 if (str_contains($label, '사이즈') || str_contains($label, '제작')) { if (str_contains($subLabel, '가로')) return isset($opts['width']) ? number_format($opts['width']) : '-'; if (str_contains($subLabel, '세로')) return isset($opts['height']) ? number_format($opts['height']) : '-'; } // 규격(매수): 기준폭, 900, 800, 600, 400, 300 if (str_contains($label, '규격') || str_contains($label, '매수')) { if (str_contains($subLabel, '기준') || str_contains($subLabel, '폭')) { return $cut && $cut['firstCut'] > 0 ? $cut['firstCut'] : ''; } // 나머지 사이즈 키 (900, 800, 600, 400, 300) $sizeKey = trim($subLabel); if ($cut && isset($cut['sizes'][$sizeKey])) { return $cut['sizes'][$sizeKey] > 0 ? $cut['sizes'][$sizeKey] : ''; } } return ''; }; @endphp @if($templateColumns->isNotEmpty())
{{-- 헤더 1행 --}} @foreach($templateColumns as $col) @if($col->column_type === 'complex' && $col->sub_labels) @else @endif @endforeach {{-- 헤더 2행 (sub_labels) --}} @if($hasComplexCol) @foreach($templateColumns as $col) @if($col->column_type === 'complex' && $col->sub_labels) @foreach($col->sub_labels as $subLabel) @endforeach @endif @endforeach @endif @foreach($workOrderItems as $index => $item) @foreach($templateColumns as $col) @if($col->column_type === 'complex' && $col->sub_labels) @foreach($col->sub_labels as $subIndex => $subLabel) @php $subVal = $getSubCellValue($col, $subLabel, $subIndex, $item, $index); @endphp @endforeach @else @php $cellVal = $getCellValue($col, $item, $index); @endphp @endif @endforeach @endforeach
{{ $col->label }} {{ $col->label }}
{{ $subLabel }}
{{ $subVal !== '' ? $subVal : '' }} {{ $cellVal }}
@endif {{-- 작업 통계 (document_data에서 stats_ 조회) --}} @php $statsData = $docData->filter(fn($d) => str_starts_with($d->field_key, 'stats_')); $statsMap = $statsData->pluck('field_value', 'field_key')->toArray(); @endphp @if(!empty($statsMap))
@php $statItems = [ 'stats_order_qty' => ['label' => '총 수량', 'color' => 'bg-blue-50 text-blue-700'], 'stats_completed_qty' => ['label' => '완료', 'color' => 'bg-green-50 text-green-700'], 'stats_in_progress_qty' => ['label' => '진행중', 'color' => 'bg-yellow-50 text-yellow-700'], 'stats_waiting_qty' => ['label' => '대기', 'color' => 'bg-gray-50 text-gray-700'], 'stats_progress' => ['label' => '진행률', 'color' => 'bg-indigo-50 text-indigo-700'], ]; @endphp @foreach($statItems as $key => $stat) @if(isset($statsMap[$key]))
{{ $stat['label'] }}
{{ $statsMap[$key] }}{{ $key === 'stats_progress' ? '%' : '' }}
@endif @endforeach
@endif
{{-- 투입 자재 LOT --}} @if($materialInputLots->isNotEmpty())

투입 자재 LOT

@foreach($materialInputLots as $index => $lot) @endforeach
No. LOT 번호 자재코드 자재명 투입 수량 투입 횟수 최초 투입일
{{ $index + 1 }} {{ $lot->lot_no }} {{ $lot->item_code }} {{ $lot->item_name }} {{ number_format($lot->total_qty, 1) }} {{ $lot->input_count }}회 {{ substr($lot->first_input_at, 0, 16) }}
@endif {{-- 비고 (document_data에서 remarks 조회) --}} @php $remarksData = $docData->where('field_key', 'remarks')->first(); @endphp @if($remarksData && $remarksData->field_value)

비고

{{ $remarksData->field_value }}

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

{{ $section->title }}

@if($section->image_path) @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 {{-- 검사 데이터 테이블 (읽기 전용) --}} {{-- 정규화 형식: section_id + column_id + row_index + field_key 기반 조회 --}} @if($section->items->count() > 0 && $document->template->columns->count() > 0) @php // 데이터 조회 헬퍼: 정규화 형식 우선, 레거시 fallback $getData = function($sectionId, $colId, $rowIdx, $fieldKey) use ($docData) { // 1차: 정규화 형식 (section_id + column_id + row_index + field_key) $record = $docData ->where('section_id', $sectionId) ->where('column_id', $colId) ->where('row_index', $rowIdx) ->where('field_key', $fieldKey) ->first(); return $record?->field_value ?? ''; }; // 레거시 데이터 조회 헬퍼 (수입검사 호환: {itemId}_n{n}, {itemId}_result 등) $getLegacyData = function($itemId, $fieldKey) use ($docData) { return $docData->where('field_key', "{$itemId}_{$fieldKey}")->first()?->field_value ?? $docData->where('field_key', $fieldKey)->first()?->field_value ?? ''; }; // 컬럼 → 섹션 아이템 매핑 (React normalizeLabel 로직 동기화) $normalizeLabel = fn($label) => preg_replace('/[①②③④⑤⑥⑦⑧⑨⑩\s]/u', '', trim($label)); $allItems = $section->items; $columnItemMap = []; foreach ($document->template->columns as $col) { $colKey = $normalizeLabel($col->label); foreach ($allItems as $sItem) { $itemKey = $normalizeLabel($sItem->item ?? $sItem->category ?? ''); if ($itemKey === $colKey) { $columnItemMap[$col->id] = $sItem; break; } } } // 기준치 해석: reference_attribute → work_order_item 치수 $resolveStandard = function($colId, $rowIndex) use ($columnItemMap, &$workOrderItems) { $sItem = $columnItemMap[$colId] ?? null; if (!$sItem) return ''; $woItem = $workOrderItems[$rowIndex] ?? null; // 1. reference_attribute → work_order_item 치수 if ($woItem) { $fv = $sItem->field_values; $refAttr = is_array($fv) ? ($fv['reference_attribute'] ?? null) : null; if ($refAttr) { $dimKey = $refAttr === 'length' ? 'width' : $refAttr; $dimVal = $woItem->options[$dimKey] ?? null; if ($dimVal) return (string) $dimVal; } } // 2. standard_criteria $sc = $sItem->standard_criteria; if ($sc) { if (is_array($sc)) { if (isset($sc['nominal'])) return (string) $sc['nominal']; if (isset($sc['min'], $sc['max'])) return $sc['min'] . ' ~ ' . $sc['max']; if (isset($sc['max'])) return '≤ ' . $sc['max']; if (isset($sc['min'])) return '≥ ' . $sc['min']; } return (string) $sc; } // 3. standard 텍스트 return $sItem->standard ?? ''; }; @endphp
{{-- 테이블 헤더 --}} @foreach($document->template->columns as $col) @if($col->column_type === 'complex' && $col->sub_labels) @else @endif @endforeach {{-- 서브 라벨 행 --}} @if($document->template->columns->contains(fn($c) => $c->column_type === 'complex' && $c->sub_labels)) @foreach($document->template->columns as $col) @if($col->column_type === 'complex' && $col->sub_labels) @foreach($col->sub_labels as $subLabel) @endforeach @endif @endforeach @endif @php // 행 수 결정: workOrderItems 기반 (React effectiveWorkItems와 동일) // fallback: document_data의 max row_index + 1 $rowCount = $workOrderItems->isNotEmpty() ? $workOrderItems->count() : max(1, ($docData->where('section_id', $section->id)->max('row_index') ?? 0) + 1); @endphp @for($rowIndex = 0; $rowIndex < $rowCount; $rowIndex++) @foreach($document->template->columns as $col) @if($col->column_type === 'complex' && $col->sub_labels) {{-- complex: sub_label 유형별 분리 (React inputIdx 로직 동기화) --}} @php $inputIdx = 0; $mappedItem = $columnItemMap[$col->id] ?? null; $isOkng = $mappedItem?->measurement_type === 'checkbox'; @endphp @foreach($col->sub_labels as $subIndex => $subLabel) @php $sl = strtolower($subLabel); $isStandardSub = str_contains($sl, '도면') || str_contains($sl, '기준'); $isOkNgSub = str_contains($sl, 'ok') || str_contains($sl, 'ng'); @endphp @if($isStandardSub) {{-- 기준치: document_data → work_order_item 치수 → item standard --}} @php $standardVal = $getData($section->id, $col->id, $rowIndex, 'standard'); if (!$standardVal) $standardVal = $resolveStandard($col->id, $rowIndex); @endphp @elseif($isOkNgSub) {{-- OK·NG sub_label --}} @php $n = $inputIdx + 1; $okVal = $getData($section->id, $col->id, $rowIndex, "n{$n}_ok"); $ngVal = $getData($section->id, $col->id, $rowIndex, "n{$n}_ng"); $savedVal = $okVal === 'OK' ? 'OK' : ($ngVal === 'NG' ? 'NG' : ''); if (!$savedVal) { $valFallback = $getData($section->id, $col->id, $rowIndex, 'value'); if (in_array(strtolower($valFallback), ['ok', 'pass', '적합', '합격'])) $savedVal = 'OK'; elseif (in_array(strtolower($valFallback), ['ng', 'fail', '부적합'])) $savedVal = 'NG'; } $inputIdx++; @endphp @else {{-- 측정값 sub_label --}} @php $n = $inputIdx + 1; if ($isOkng) { $okVal = $getData($section->id, $col->id, $rowIndex, "n{$n}_ok"); $ngVal = $getData($section->id, $col->id, $rowIndex, "n{$n}_ng"); $savedVal = $okVal === 'OK' ? 'OK' : ($ngVal === 'NG' ? 'NG' : ''); if (!$savedVal) { $valFallback = $getData($section->id, $col->id, $rowIndex, 'value'); if (in_array(strtolower($valFallback), ['ok', 'pass', '적합', '합격'])) $savedVal = 'OK'; elseif (in_array(strtolower($valFallback), ['ng', 'fail', '부적합'])) $savedVal = 'NG'; } } else { $savedVal = $getData($section->id, $col->id, $rowIndex, "n{$n}"); if (!$savedVal) $savedVal = $getData($section->id, $col->id, $rowIndex, 'value'); } $inputIdx++; @endphp @endif @endforeach @elseif($col->column_type === 'select') {{-- select: 판정 --}} @php $savedVal = $getData($section->id, $col->id, $rowIndex, 'value'); if (!$savedVal) { $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'; } @endphp @elseif($col->column_type === 'check') {{-- check: 체크 결과 --}} @php $savedVal = $getData($section->id, $col->id, $rowIndex, 'value'); @endphp @elseif($col->column_type === 'measurement') {{-- measurement: 수치 데이터 --}} @php $savedVal = $getData($section->id, $col->id, $rowIndex, 'n1'); if (!$savedVal) $savedVal = $getData($section->id, $col->id, $rowIndex, 'value'); @endphp @else {{-- text: 일련번호 / 판정 / 기타 --}} @php $label = trim($col->label); $isNoCol = (str_contains(strtolower($label), 'no') && strlen($label) <= 4) || in_array($label, ['일렬번호', '일련번호', '번호', '순번']); $isJudgeCol = str_contains($label, '판정'); @endphp @if($isNoCol) @elseif($isJudgeCol) @php $savedVal = $getData($section->id, $col->id, $rowIndex, 'value'); if (!$savedVal) { $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'; } @endphp @else @php $savedVal = $getData($section->id, $col->id, $rowIndex, 'value'); if (!$savedVal) $savedVal = $getData($section->id, $col->id, $rowIndex, 'n1'); @endphp @endif @endif @endforeach @endfor
{{ $col->label }} {{ $col->label }}
{{ $subLabel }}
{{ $standardVal ?: '-' }} @if($savedVal) @if(strtolower($savedVal) === 'ok') OK @else NG @endif @else - @endif @if($isOkng && $savedVal) @if(strtolower($savedVal) === 'ok') OK @else NG @endif @else {{ $savedVal ?: '-' }} @endif @if($savedVal) @php $isPass = in_array(strtolower($savedVal), ['ok', '적합', '합격', 'pass', '적']); @endphp @if($isPass) @else @endif {{ $isPass ? '적합' : '부적합' }} @else - @endif @if(in_array(strtolower($savedVal), ['ok', 'pass', '적합', '합격'])) @elseif($savedVal) @else - @endif {{ $savedVal ?: '-' }}{{ $rowIndex + 1 }} @if(in_array(strtolower($savedVal ?? ''), ['ok', 'pass', '적합', '합격'])) @elseif($savedVal) @else - @endif {{ $savedVal ?: '-' }}
@endif {{-- 종합판정 / 비고 (마지막 섹션에만) --}} @if($loop->last) @php // React 형식: overall_result, remark (+ 레거시 호환: footer_judgement, footer_remark) $remarkVal = $docData->where('field_key', 'remark')->first()?->field_value ?? $docData->where('field_key', 'footer_remark')->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 = $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', '적', '적합', '합격'])); $judgementVal = $hasFail ? '불합격' : ($allPass ? '합격' : ''); } } $isPass = in_array(strtolower($judgementVal), ['pass', 'ok', '적합', '합격']); @endphp
{{ $document->template->footer_remark_label ?? '비고' }}
{{ $remarkVal ?: '-' }}
{{ $document->template->footer_judgement_label ?? '종합판정' }}
@if($judgementVal) {{ $isPass ? '적합' : '부적합' }} @else - @endif
@endif
@endforeach @endif @endif {{-- end: 스냅샷 vs 블록빌더 vs 레거시 분기 --}} @endif {{-- end: 절곡 검사 vs 일반 섹션 분기 --}} {{-- 첨부파일 --}} @if($document->attachments && $document->attachments->count() > 0)

첨부파일

    @foreach($document->attachments as $attachment)
  • {{ $attachment->file->original_name ?? '파일명 없음' }}

    {{ $attachment->type_label }} · {{ $attachment->file ? number_format($attachment->file->size / 1024, 1) . ' KB' : '-' }}

    @if($attachment->file) 다운로드 @endif
  • @endforeach
@endif
{{-- 사이드바 --}}
{{-- 결재 현황 --}}

결재 현황

@if($document->approvals && $document->approvals->count() > 0)
    @foreach($document->approvals as $approval)
  1. @if($approval->status === 'APPROVED') @elseif($approval->status === 'REJECTED') @else {{ $approval->step }} @endif

    {{ $approval->role }} ({{ $approval->status_label }})

    {{ $approval->user->name ?? '미지정' }}

    @if($approval->acted_at)

    {{ $approval->acted_at->format('Y-m-d H:i') }}

    @endif @if($approval->comment)

    {{ $approval->comment }}

    @endif
  2. @endforeach
@else

결재선이 설정되지 않았습니다.

@endif
{{-- 문서 이력 --}}

문서 이력

문서 이력 기능은 추후 구현 예정입니다.

{{-- 반려 사유 모달 --}} @if($document->isPending()) @endif @endsection @push('scripts') @endpush