Files
sam-manage/resources/views/documents/edit.blade.php
권혁성 b14b991d1c feat:검사 기준서 동적 필드 + 자동 하이라이트 + 미리보기 개선
- 문서 작성 시 연결 품목 규격(두께/너비/길이) 기반 자동 하이라이트
- 미리보기에서 field_values 동적 필드 데이터 정상 표시
- DocumentTemplateController에서 field_values 직렬화 추가
- DocumentController에 linkedItemSpecs 조회 로직 추가
- Item 모델 attributes JSON cast 추가

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-05 09:26:13 +09:00

916 lines
65 KiB
PHP

@extends('layouts.app')
@section('title', $isCreate ? '새 문서 작성' : '문서 수정')
@section('content')
<div class="max-w-7xl mx-auto">
<!-- 헤더 -->
<div class="flex flex-col lg:flex-row lg:justify-between lg:items-center gap-4 mb-6">
<div>
<h1 class="text-2xl font-bold text-gray-800">{{ $isCreate ? '새 문서 작성' : '문서 수정' }}</h1>
<p class="text-sm text-gray-500 mt-1 hidden sm:block">
@if($document)
{{ $document->document_no }} - {{ $document->title }}
@else
템플릿을 선택하여 문서를 작성합니다.
@endif
</p>
</div>
<div class="flex items-center gap-2">
<a href="{{ route('documents.index') }}"
class="bg-gray-200 hover:bg-gray-300 text-gray-700 px-4 py-2 rounded-lg transition">
목록
</a>
</div>
</div>
{{-- 템플릿 선택 (생성 ) --}}
@if($isCreate && !$template)
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6 mb-6">
<h2 class="text-lg font-semibold text-gray-800 mb-4">템플릿 선택</h2>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
@forelse($templates as $tpl)
<a href="{{ route('documents.create', ['template_id' => $tpl->id]) }}"
class="block p-4 border border-gray-200 rounded-lg hover:border-blue-500 hover:bg-blue-50 transition-colors">
<h3 class="font-medium text-gray-900">{{ $tpl->name }}</h3>
<p class="text-sm text-gray-500 mt-1">{{ $tpl->category }}</p>
</a>
@empty
<p class="text-gray-500 col-span-3">사용 가능한 템플릿이 없습니다.</p>
@endforelse
</div>
</div>
@endif
{{-- 문서 --}}
@if($template)
<form id="documentForm" class="space-y-6">
@csrf
<input type="hidden" name="template_id" value="{{ $template->id }}">
{{-- 기본 정보 --}}
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<h2 class="text-lg font-semibold text-gray-800 mb-4">기본 정보</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">템플릿</label>
<input type="text" value="{{ $template->name }}" disabled
class="w-full rounded-lg border-gray-300 bg-gray-50 text-sm">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">
제목 <span class="text-red-500">*</span>
</label>
<input type="text" name="title" value="{{ $document->title ?? '' }}" required
class="w-full rounded-lg border-gray-300 text-sm focus:border-blue-500 focus:ring-blue-500"
placeholder="문서 제목을 입력하세요">
</div>
</div>
</div>
{{-- 기본 필드 --}}
@if($template->basicFields && $template->basicFields->count() > 0)
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<h2 class="text-lg font-semibold text-gray-800 mb-4">{{ $template->title ?? '문서 정보' }}</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
@foreach($template->basicFields as $field)
@php
$fieldKey = 'bf_' . $field->id;
$savedValue = $document?->data->where('field_key', $fieldKey)->first()?->field_value ?? $field->default_value ?? '';
@endphp
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">
{{ $field->label }}
</label>
@if($field->field_type === 'textarea')
<textarea name="data[{{ $fieldKey }}]"
class="w-full rounded-lg border-gray-300 text-sm focus:border-blue-500 focus:ring-blue-500"
rows="3">{{ $savedValue }}</textarea>
@elseif($field->field_type === 'date')
<input type="date" name="data[{{ $fieldKey }}]"
value="{{ $savedValue }}"
class="w-full rounded-lg border-gray-300 text-sm focus:border-blue-500 focus:ring-blue-500">
@elseif($field->field_type === 'number')
<input type="number" name="data[{{ $fieldKey }}]"
value="{{ $savedValue }}"
class="w-full rounded-lg border-gray-300 text-sm focus:border-blue-500 focus:ring-blue-500">
@elseif($field->field_type === 'item_search')
<div class="relative" x-data="itemSearch('{{ $fieldKey }}', '{{ $savedValue }}')" x-init="init()">
<input type="text" name="data[{{ $fieldKey }}]"
x-model="searchText"
@input.debounce.300ms="search()"
@focus="showResults = results.length > 0"
@click.away="showResults = false"
placeholder="품명 또는 코드로 검색"
autocomplete="off"
class="w-full rounded-lg border-gray-300 text-sm focus:border-blue-500 focus:ring-blue-500">
<div x-show="showResults" x-cloak
class="absolute z-50 w-full mt-1 bg-white border border-gray-200 rounded-lg shadow-lg max-h-60 overflow-y-auto">
<template x-for="item in results" :key="item.id">
<div @click="selectItem(item)"
class="px-3 py-2 cursor-pointer hover:bg-blue-50 flex justify-between items-center">
<span class="text-sm" x-text="item.name"></span>
<span class="text-xs text-gray-400" x-text="item.code"></span>
</div>
</template>
<div x-show="results.length === 0 && searchText.length > 0"
class="px-3 py-2 text-sm text-gray-400">검색 결과 없음</div>
</div>
</div>
@else
<input type="text" name="data[{{ $fieldKey }}]"
value="{{ $savedValue }}"
class="w-full rounded-lg border-gray-300 text-sm focus:border-blue-500 focus:ring-blue-500">
@endif
</div>
@endforeach
</div>
</div>
@endif
{{-- 섹션 (검사 데이터 테이블) --}}
@if($template->sections && $template->sections->count() > 0)
@foreach($template->sections as $sectionIndex => $section)
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<h2 class="text-lg font-semibold text-gray-800 mb-4">{{ $section->title }}</h2>
@if($section->image_path)
<img src="{{ asset('storage/' . $section->image_path) }}" alt="{{ $section->title }}" class="max-w-full h-auto mb-4 rounded">
@endif
{{-- 연결 품목 규격 정보 --}}
@if(!empty($linkedItemSpecs))
<div class="mb-3 flex flex-wrap gap-2 items-center text-xs">
<span class="text-gray-500">연결 품목 규격:</span>
@foreach($linkedItemSpecs as $spec)
<span class="inline-flex items-center gap-1 bg-yellow-50 border border-yellow-200 rounded px-2 py-0.5">
<span class="font-medium">{{ $spec['name'] }}</span>
@if($spec['thickness'])<span class="text-gray-500">t={{ $spec['thickness'] }}</span>@endif
@if($spec['width'])<span class="text-gray-500">w={{ $spec['width'] }}</span>@endif
@if($spec['length'])<span class="text-gray-500">l={{ $spec['length'] }}</span>@endif
</span>
@endforeach
<span class="text-gray-400 ml-1"> 해당 범위 행이 노란색으로 표시됩니다</span>
</div>
@endif
{{-- 검사 데이터 테이블 --}}
@if($section->items->count() > 0 && $template->columns->count() > 0)
@php
// 검사방식 코드→한글 매핑
$methodNames = [
'visual' => '육안검사',
'check' => '체크검사',
'mill_sheet' => '공급업체 밀시트',
'certified_agency' => '공인시험기관',
'substitute_cert' => '공급업체 성적서 대체',
'other' => '기타',
];
// 공차 포맷 함수
$formatTolerance = function($tol) {
if (!$tol || !is_array($tol) || !isset($tol['type'])) {
// 레거시 문자열 지원
return is_string($tol) && $tol !== '' ? $tol : '-';
}
switch ($tol['type']) {
case 'symmetric':
return isset($tol['value']) ? "±{$tol['value']}" : '-';
case 'asymmetric':
$p = $tol['plus'] ?? 0;
$m = $tol['minus'] ?? 0;
return ($p || $m) ? "+{$p} / -{$m}" : '-';
case 'range':
$min = $tol['min'] ?? '';
$max = $tol['max'] ?? '';
return ($min !== '' || $max !== '') ? "{$min} ~ {$max}" : '-';
case 'limit':
$opSymbol = ['lte' => '≤', 'lt' => '<', 'gte' => '≥', 'gt' => '>'];
$op = $opSymbol[$tol['op'] ?? 'lte'] ?? '≤';
return isset($tol['value']) ? "{$op}{$tol['value']}" : '-';
default:
return '-';
}
};
// standard_criteria 포맷 함수 (getFieldValue 사용)
$formatStandard = function($item) use ($formatTolerance) {
$c = $item->getFieldValue('standard_criteria');
if ($c && is_array($c) && (isset($c['min']) || isset($c['max']))) {
$opLabel = ['gte' => '이상', 'gt' => '초과', 'lte' => '이하', 'lt' => '미만'];
$parts = [];
if (isset($c['min'])) $parts[] = $c['min'] . ' ' . ($opLabel[$c['min_op'] ?? 'gte'] ?? '이상');
if (isset($c['max'])) $parts[] = $c['max'] . ' ' . ($opLabel[$c['max_op'] ?? 'lte'] ?? '이하');
return implode(' ~ ', $parts);
}
$std = $item->getFieldValue('standard') ?: '-';
$tolStr = $formatTolerance($item->getFieldValue('tolerance'));
if ($tolStr !== '-') $std .= ' (' . $tolStr . ')';
return $std;
};
// 검사주기 포맷 함수 (getFieldValue 사용)
$formatFrequency = function($item) {
$parts = [];
$freqN = $item->getFieldValue('frequency_n');
$freqC = $item->getFieldValue('frequency_c');
$freq = $item->getFieldValue('frequency');
if ($freqN) {
$nc = "n={$freqN}";
if ($freqC !== null) $nc .= ", c={$freqC}";
$parts[] = $nc;
}
if ($freq) $parts[] = $freq;
return $parts ? implode(' / ', $parts) : '-';
};
// 카테고리별 그룹핑 (getFieldValue 사용)
$groupedRows = [];
$allItems = $section->items->values();
$idx = 0;
while ($idx < $allItems->count()) {
$item = $allItems[$idx];
$cat = trim($item->getFieldValue('category') ?? '');
if ($cat) {
$grouped = [$item];
while ($idx + 1 < $allItems->count() && trim($allItems[$idx + 1]->getFieldValue('category') ?? '') === $cat) {
$idx++;
$grouped[] = $allItems[$idx];
}
$groupedRows[] = ['type' => 'group', 'category' => $cat, 'items' => $grouped];
} else {
$groupedRows[] = ['type' => 'single', 'item' => $item];
}
$idx++;
}
// 연결 품목 규격 정보 (자동 하이라이트용)
$itemSpecs = $linkedItemSpecs ?? [];
$matchRow = function($item) use ($itemSpecs) {
$c = $item->getFieldValue('standard_criteria');
if (!$c || !is_array($c) || (!isset($c['min']) && !isset($c['max']))) {
return false;
}
// 검사항목명에서 비교 대상 결정 (두께→thickness, 너비→width, 길이→length)
$itemName = mb_strtolower(trim($item->getFieldValue('item') ?? ''));
$specKey = 'thickness'; // 기본값
if (str_contains($itemName, '너비') || str_contains($itemName, 'width')) {
$specKey = 'width';
} elseif (str_contains($itemName, '길이') || str_contains($itemName, 'length')) {
$specKey = 'length';
}
foreach ($itemSpecs as $spec) {
$val = $spec[$specKey] ?? null;
if ($val === null) continue;
$min = isset($c['min']) ? (float)$c['min'] : null;
$max = isset($c['max']) ? (float)$c['max'] : null;
$minOp = $c['min_op'] ?? 'gte';
$maxOp = $c['max_op'] ?? 'lte';
$minOk = $min === null || ($minOp === 'gt' ? $val > $min : $val >= $min);
$maxOk = $max === null || ($maxOp === 'lt' ? $val < $max : $val <= $max);
if ($minOk && $maxOk) return true;
}
return false;
};
// 측정치 컬럼 정보
$hasComplex = $template->columns->contains(fn($c) => $c->column_type === 'complex' && $c->sub_labels);
$maxFreqN = $allItems->max(fn($i) => $i->getFieldValue('frequency_n')) ?: 0;
$complexCol = $template->columns->first(fn($c) => $c->column_type === 'complex' && $c->sub_labels);
$totalMeasCols = $complexCol ? max(count($complexCol->sub_labels), $maxFreqN) : 0;
@endphp
<div class="overflow-x-auto mt-4">
<table class="min-w-full border border-gray-300 text-sm" data-section-id="{{ $section->id }}">
{{-- colgroup: 컬럼 너비 제어 --}}
<colgroup>
@foreach($template->columns as $col)
@php $colLabel = trim($col->label); @endphp
@if($col->column_type === 'complex' && $col->sub_labels)
@for($ci = 0; $ci < $totalMeasCols; $ci++)
<col style="width:60px">
@endfor
@elseif(str_contains(strtolower($colLabel), 'no') && strlen($colLabel) <= 4)
<col style="width:40px">
@elseif(in_array($colLabel, ['검사항목', '항목']))
<col style="width:60px">
<col style="width:100px">
@elseif(in_array($colLabel, ['검사기준', '기준']))
<col style="width:120px">
<col style="width:60px">
@elseif(str_contains($colLabel, '판정'))
<col style="width:40px">
@else
<col style="width:80px">
@endif
@endforeach
</colgroup>
{{-- 테이블 헤더 --}}
<thead class="bg-gray-50">
<tr>
@foreach($template->columns as $col)
@if($col->column_type === 'complex' && $col->sub_labels)
<th colspan="{{ $totalMeasCols }}"
class="px-2 py-2 text-center text-xs font-medium text-gray-600 uppercase border border-gray-300">
{{ $col->label }}
</th>
@else
@php
$colLabel = trim($col->label);
$isItemOrStd = in_array($colLabel, ['검사항목', '항목']) || in_array($colLabel, ['검사기준', '기준']);
@endphp
<th {{ $isItemOrStd ? 'colspan=2' : '' }}
{{ $hasComplex ? 'rowspan=2' : '' }}
class="px-2 py-2 text-center text-xs font-medium text-gray-600 uppercase border border-gray-300">
{{ $col->label }}
</th>
@endif
@endforeach
</tr>
@if($hasComplex)
<tr>
@for($si = 1; $si <= $totalMeasCols; $si++)
<th class="px-2 py-1 text-center text-xs font-medium text-gray-500 border border-gray-300 bg-gray-50">
n{{ $si }}
</th>
@endfor
</tr>
@endif
</thead>
{{-- 테이블 바디 --}}
<tbody>
@php $rowNum = 0; $globalRowIndex = 0; @endphp
@foreach($groupedRows as $row)
@php $rowNum++; @endphp
@if($row['type'] === 'single')
{{-- 단일 항목 --}}
@php $item = $row['item']; $rowIndex = $globalRowIndex; $globalRowIndex++; $isMatch = $matchRow($item); @endphp
<tr class="{{ $isMatch ? 'bg-yellow-50 border-l-4 border-l-yellow-400' : 'hover:bg-blue-50' }}" data-row-index="{{ $rowIndex }}" @if($isMatch) title="연결 품목 규격 범위에 해당" @endif>
@foreach($template->columns as $col)
@if($col->column_type === 'complex' && $col->sub_labels)
{{-- 측정치: measurement_type에 따라 분기 (getFieldValue 사용) --}}
@php
$mType = $item->getFieldValue('measurement_type') ?? '';
$freqN = $item->getFieldValue('frequency_n') ?: $totalMeasCols;
$remainder = $totalMeasCols - $freqN;
@endphp
@if($mType === 'checkbox')
@for($nIdx = 1; $nIdx <= $freqN; $nIdx++)
@php
$fieldKey = "s{$section->id}_r{$rowIndex}_c{$col->id}_n{$nIdx}";
$savedOK = $document?->data->where('field_key', $fieldKey . '_ok')->first()?->field_value ?? '';
$savedNG = $document?->data->where('field_key', $fieldKey . '_ng')->first()?->field_value ?? '';
@endphp
<td class="px-1 py-1 border border-gray-300 text-center">
<label class="inline-flex items-center gap-0.5 text-xs">
<input type="checkbox" name="section_data[{{ $fieldKey }}_ok]" value="OK" {{ $savedOK === 'OK' ? 'checked' : '' }}
data-section-id="{{ $section->id }}" data-column-id="{{ $col->id }}" data-row-index="{{ $rowIndex }}"
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500 w-3 h-3">OK
</label><br>
<label class="inline-flex items-center gap-0.5 text-xs">
<input type="checkbox" name="section_data[{{ $fieldKey }}_ng]" value="NG" {{ $savedNG === 'NG' ? 'checked' : '' }}
data-section-id="{{ $section->id }}" data-column-id="{{ $col->id }}" data-row-index="{{ $rowIndex }}"
class="rounded border-gray-300 text-red-600 focus:ring-red-500 w-3 h-3">NG
</label>
</td>
@endfor
@if($remainder > 0)
<td class="border border-gray-300" colspan="{{ $remainder }}"></td>
@endif
@elseif($mType === 'numeric')
@for($nIdx = 1; $nIdx <= $freqN; $nIdx++)
@php
$fieldKey = "s{$section->id}_r{$rowIndex}_c{$col->id}_n{$nIdx}";
$savedVal = $document?->data->where('field_key', $fieldKey)->first()?->field_value ?? '';
@endphp
<td class="px-1 py-1 border border-gray-300 text-center">
<input type="number" step="any" name="section_data[{{ $fieldKey }}]" value="{{ $savedVal }}"
data-section-id="{{ $section->id }}" data-column-id="{{ $col->id }}" data-row-index="{{ $rowIndex }}"
class="w-full text-center text-sm border-0 focus:ring-1 focus:ring-blue-400 rounded py-1"
placeholder="n{{ $nIdx }}">
</td>
@endfor
@if($remainder > 0)
<td class="border border-gray-300" colspan="{{ $remainder }}"></td>
@endif
@elseif($mType === 'single_value')
@php
$fieldKey = "s{$section->id}_r{$rowIndex}_c{$col->id}";
$savedVal = $document?->data->where('field_key', $fieldKey)->first()?->field_value ?? '';
@endphp
<td class="px-1 py-1 border border-gray-300 text-center" colspan="{{ $totalMeasCols }}">
<input type="text" name="section_data[{{ $fieldKey }}]" value="{{ $savedVal }}"
data-section-id="{{ $section->id }}" data-column-id="{{ $col->id }}" data-row-index="{{ $rowIndex }}"
class="w-full text-center text-sm border-0 focus:ring-1 focus:ring-blue-400 rounded py-1"
placeholder="입력">
</td>
@elseif($mType === 'substitute')
@php
$fieldKey = "s{$section->id}_r{$rowIndex}_c{$col->id}";
$savedVal = $document?->data->where('field_key', $fieldKey)->first()?->field_value ?? '';
@endphp
<td class="px-1 py-1 border border-gray-300 text-center text-gray-400 text-xs" colspan="{{ $totalMeasCols }}">
<input type="text" name="section_data[{{ $fieldKey }}]" value="{{ $savedVal }}"
data-section-id="{{ $section->id }}" data-column-id="{{ $col->id }}" data-row-index="{{ $rowIndex }}"
class="w-full text-center text-sm border-0 focus:ring-1 focus:ring-blue-400 rounded py-1"
placeholder="(입력)">
</td>
@else
@for($nIdx = 1; $nIdx <= $totalMeasCols; $nIdx++)
@php
$fieldKey = "s{$section->id}_r{$rowIndex}_c{$col->id}_n{$nIdx}";
$savedVal = $document?->data->where('field_key', $fieldKey)->first()?->field_value ?? '';
@endphp
<td class="px-1 py-1 border border-gray-300 text-center">
<input type="text" name="section_data[{{ $fieldKey }}]" value="{{ $savedVal }}"
data-section-id="{{ $section->id }}" data-column-id="{{ $col->id }}" data-row-index="{{ $rowIndex }}"
class="w-full text-center text-sm border-0 focus:ring-1 focus:ring-blue-400 rounded py-1"
placeholder="n{{ $nIdx }}">
</td>
@endfor
@endif
@elseif($col->column_type === 'select')
@php
$fieldKey = "s{$section->id}_r{$rowIndex}_c{$col->id}";
$savedVal = $document?->data->where('field_key', $fieldKey)->first()?->field_value ?? '';
$options = $template->footer_judgement_options ?? ['적합', '부적합'];
@endphp
<td class="px-1 py-1 border border-gray-300 text-center">
<select name="section_data[{{ $fieldKey }}]"
data-section-id="{{ $section->id }}" data-column-id="{{ $col->id }}" data-row-index="{{ $rowIndex }}"
class="w-full text-center text-sm border-0 focus:ring-1 focus:ring-blue-400 rounded py-1">
<option value="">-</option>
@foreach($options as $opt)
<option value="{{ $opt }}" {{ $savedVal === $opt ? 'selected' : '' }}>{{ $opt }}</option>
@endforeach
</select>
</td>
@elseif($col->column_type === 'check')
@php
$fieldKey = "s{$section->id}_r{$rowIndex}_c{$col->id}";
$savedVal = $document?->data->where('field_key', $fieldKey)->first()?->field_value ?? '';
@endphp
<td class="px-1 py-1 border border-gray-300 text-center">
<input type="checkbox" name="section_data[{{ $fieldKey }}]" value="OK" {{ $savedVal === 'OK' ? 'checked' : '' }}
data-section-id="{{ $section->id }}" data-column-id="{{ $col->id }}" data-row-index="{{ $rowIndex }}"
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500">
</td>
@else
{{-- text: 정적 데이터 --}}
@php
$label = trim($col->label);
$isNoCol = str_contains(strtolower($label), 'no') && strlen($label) <= 4;
$isItemCol = in_array($label, ['검사항목', '항목']);
$isStdCol = in_array($label, ['검사기준', '기준']);
$isMethodCol = str_contains($label, '검사방') || in_array($label, ['방식', '검사방법']);
$isFreqCol = str_contains($label, '주기') || in_array($label, ['검사주기']);
$isJudgeCol = str_contains($label, '판정');
@endphp
@if($isNoCol)
<td class="px-2 py-2 border border-gray-300 text-sm text-center">{{ $rowNum }}</td>
@elseif($isItemCol)
<td class="px-2 py-2 border border-gray-300 text-sm text-center" colspan="2">{{ $item->getFieldValue('item') ?: '-' }}</td>
@elseif($isStdCol)
<td class="px-2 py-2 border border-gray-300 text-sm text-center" colspan="2">{{ $formatStandard($item) }}</td>
@elseif($isMethodCol)
@php $methodVal = $item->getFieldValue('method'); @endphp
<td class="px-2 py-2 border border-gray-300 text-sm text-center">{{ $methodNames[$methodVal] ?? ($methodVal ?: '-') }}</td>
@elseif($isFreqCol)
<td class="px-2 py-2 border border-gray-300 text-sm text-center">{{ $formatFrequency($item) }}</td>
@elseif($isJudgeCol)
@php
$fieldKey = "s{$section->id}_r{$rowIndex}_c{$col->id}";
$savedVal = $document?->data->where('field_key', $fieldKey)->first()?->field_value ?? '';
@endphp
<td class="px-1 py-1 border border-gray-300 text-center">
<input type="checkbox" name="section_data[{{ $fieldKey }}]" value="OK" {{ $savedVal === 'OK' ? 'checked' : '' }}
data-section-id="{{ $section->id }}" data-column-id="{{ $col->id }}" data-row-index="{{ $rowIndex }}"
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500">
</td>
@else
@php
$staticValue = match(true) {
in_array($label, ['규격', '적용규격', '관련규정']) => $item->getFieldValue('regulation'),
in_array($label, ['분류', '카테고리']) => $item->getFieldValue('category'),
default => null,
};
@endphp
@if($staticValue !== null)
<td class="px-2 py-2 border border-gray-300 text-sm">{{ $staticValue }}</td>
@else
@php
$fieldKey = "s{$section->id}_r{$rowIndex}_c{$col->id}";
$savedVal = $document?->data->where('field_key', $fieldKey)->first()?->field_value ?? '';
@endphp
<td class="px-1 py-1 border border-gray-300 text-center">
<input type="text" name="section_data[{{ $fieldKey }}]" value="{{ $savedVal }}"
data-section-id="{{ $section->id }}" data-column-id="{{ $col->id }}" data-row-index="{{ $rowIndex }}"
class="w-full text-center text-sm border-0 focus:ring-1 focus:ring-blue-400 rounded py-1">
</td>
@endif
@endif
@endif
@endforeach
</tr>
@else
{{-- 그룹 항목 --}}
@php $groupItems = $row['items']; $groupCount = count($groupItems); @endphp
@foreach($groupItems as $itemIdx => $item)
@php $rowIndex = $globalRowIndex; $globalRowIndex++; $isMatch = $matchRow($item); @endphp
<tr class="{{ $isMatch ? 'bg-yellow-50 border-l-4 border-l-yellow-400' : 'hover:bg-blue-50' }}" data-row-index="{{ $rowIndex }}" @if($isMatch) title="연결 품목 규격 범위에 해당" @endif>
@foreach($template->columns as $col)
@if($col->column_type === 'complex' && $col->sub_labels)
{{-- 측정치: 개별 렌더링 (getFieldValue 사용) --}}
@php
$mType = $item->getFieldValue('measurement_type') ?? '';
$freqN = $item->getFieldValue('frequency_n') ?: $totalMeasCols;
$remainder = $totalMeasCols - $freqN;
@endphp
@if($mType === 'checkbox')
@for($nIdx = 1; $nIdx <= $freqN; $nIdx++)
@php
$fieldKey = "s{$section->id}_r{$rowIndex}_c{$col->id}_n{$nIdx}";
$savedOK = $document?->data->where('field_key', $fieldKey . '_ok')->first()?->field_value ?? '';
$savedNG = $document?->data->where('field_key', $fieldKey . '_ng')->first()?->field_value ?? '';
@endphp
<td class="px-1 py-1 border border-gray-300 text-center">
<label class="inline-flex items-center gap-0.5 text-xs">
<input type="checkbox" name="section_data[{{ $fieldKey }}_ok]" value="OK" {{ $savedOK === 'OK' ? 'checked' : '' }}
data-section-id="{{ $section->id }}" data-column-id="{{ $col->id }}" data-row-index="{{ $rowIndex }}"
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500 w-3 h-3">OK
</label><br>
<label class="inline-flex items-center gap-0.5 text-xs">
<input type="checkbox" name="section_data[{{ $fieldKey }}_ng]" value="NG" {{ $savedNG === 'NG' ? 'checked' : '' }}
data-section-id="{{ $section->id }}" data-column-id="{{ $col->id }}" data-row-index="{{ $rowIndex }}"
class="rounded border-gray-300 text-red-600 focus:ring-red-500 w-3 h-3">NG
</label>
</td>
@endfor
@if($remainder > 0)
<td class="border border-gray-300" colspan="{{ $remainder }}"></td>
@endif
@elseif($mType === 'numeric')
@for($nIdx = 1; $nIdx <= $freqN; $nIdx++)
@php
$fieldKey = "s{$section->id}_r{$rowIndex}_c{$col->id}_n{$nIdx}";
$savedVal = $document?->data->where('field_key', $fieldKey)->first()?->field_value ?? '';
@endphp
<td class="px-1 py-1 border border-gray-300 text-center">
<input type="number" step="any" name="section_data[{{ $fieldKey }}]" value="{{ $savedVal }}"
data-section-id="{{ $section->id }}" data-column-id="{{ $col->id }}" data-row-index="{{ $rowIndex }}"
class="w-full text-center text-sm border-0 focus:ring-1 focus:ring-blue-400 rounded py-1"
placeholder="n{{ $nIdx }}">
</td>
@endfor
@if($remainder > 0)
<td class="border border-gray-300" colspan="{{ $remainder }}"></td>
@endif
@elseif($mType === 'single_value')
@php
$fieldKey = "s{$section->id}_r{$rowIndex}_c{$col->id}";
$savedVal = $document?->data->where('field_key', $fieldKey)->first()?->field_value ?? '';
@endphp
<td class="px-1 py-1 border border-gray-300 text-center" colspan="{{ $totalMeasCols }}">
<input type="text" name="section_data[{{ $fieldKey }}]" value="{{ $savedVal }}"
data-section-id="{{ $section->id }}" data-column-id="{{ $col->id }}" data-row-index="{{ $rowIndex }}"
class="w-full text-center text-sm border-0 focus:ring-1 focus:ring-blue-400 rounded py-1"
placeholder="입력">
</td>
@elseif($mType === 'substitute')
@php
$fieldKey = "s{$section->id}_r{$rowIndex}_c{$col->id}";
$savedVal = $document?->data->where('field_key', $fieldKey)->first()?->field_value ?? '';
@endphp
<td class="px-1 py-1 border border-gray-300 text-center text-gray-400 text-xs" colspan="{{ $totalMeasCols }}">
<input type="text" name="section_data[{{ $fieldKey }}]" value="{{ $savedVal }}"
data-section-id="{{ $section->id }}" data-column-id="{{ $col->id }}" data-row-index="{{ $rowIndex }}"
class="w-full text-center text-sm border-0 focus:ring-1 focus:ring-blue-400 rounded py-1"
placeholder="(입력)">
</td>
@else
@for($nIdx = 1; $nIdx <= $totalMeasCols; $nIdx++)
@php
$fieldKey = "s{$section->id}_r{$rowIndex}_c{$col->id}_n{$nIdx}";
$savedVal = $document?->data->where('field_key', $fieldKey)->first()?->field_value ?? '';
@endphp
<td class="px-1 py-1 border border-gray-300 text-center">
<input type="text" name="section_data[{{ $fieldKey }}]" value="{{ $savedVal }}"
data-section-id="{{ $section->id }}" data-column-id="{{ $col->id }}" data-row-index="{{ $rowIndex }}"
class="w-full text-center text-sm border-0 focus:ring-1 focus:ring-blue-400 rounded py-1"
placeholder="n{{ $nIdx }}">
</td>
@endfor
@endif
@elseif($col->column_type === 'select')
@php
$fieldKey = "s{$section->id}_r{$rowIndex}_c{$col->id}";
$savedVal = $document?->data->where('field_key', $fieldKey)->first()?->field_value ?? '';
$options = $template->footer_judgement_options ?? ['적합', '부적합'];
@endphp
<td class="px-1 py-1 border border-gray-300 text-center">
<select name="section_data[{{ $fieldKey }}]"
data-section-id="{{ $section->id }}" data-column-id="{{ $col->id }}" data-row-index="{{ $rowIndex }}"
class="w-full text-center text-sm border-0 focus:ring-1 focus:ring-blue-400 rounded py-1">
<option value="">-</option>
@foreach($options as $opt)
<option value="{{ $opt }}" {{ $savedVal === $opt ? 'selected' : '' }}>{{ $opt }}</option>
@endforeach
</select>
</td>
@elseif($col->column_type === 'check')
@php
$fieldKey = "s{$section->id}_r{$rowIndex}_c{$col->id}";
$savedVal = $document?->data->where('field_key', $fieldKey)->first()?->field_value ?? '';
@endphp
<td class="px-1 py-1 border border-gray-300 text-center">
<input type="checkbox" name="section_data[{{ $fieldKey }}]" value="OK" {{ $savedVal === 'OK' ? 'checked' : '' }}
data-section-id="{{ $section->id }}" data-column-id="{{ $col->id }}" data-row-index="{{ $rowIndex }}"
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500">
</td>
@else
{{-- text: 정적 데이터 (그룹) --}}
@php
$label = trim($col->label);
$isNoCol = str_contains(strtolower($label), 'no') && strlen($label) <= 4;
$isItemCol = in_array($label, ['검사항목', '항목']);
$isStdCol = in_array($label, ['검사기준', '기준']);
$isMethodCol = str_contains($label, '검사방') || in_array($label, ['방식', '검사방법']);
$isFreqCol = str_contains($label, '주기') || in_array($label, ['검사주기']);
$isJudgeCol = str_contains($label, '판정');
@endphp
@if($isNoCol)
@if($itemIdx === 0)
<td class="px-2 py-2 border border-gray-300 text-sm text-center" rowspan="{{ $groupCount }}">{{ $rowNum }}</td>
@endif
@elseif($isItemCol)
{{-- 구분(rowspan) + 항목명( ) --}}
@if($itemIdx === 0)
<td class="px-2 py-2 border border-gray-300 text-sm text-center font-medium" rowspan="{{ $groupCount }}">{{ $row['category'] }}</td>
@endif
<td class="px-2 py-2 border border-gray-300 text-sm text-center">{{ $item->getFieldValue('item') ?: '-' }}</td>
@elseif($isStdCol)
{{-- 기준 + 공차 ( 개별) --}}
<td class="px-2 py-2 border border-gray-300 text-sm text-center">{{ $formatStandard($item) }}</td>
<td class="px-2 py-2 border border-gray-300 text-sm text-center">{{ $formatTolerance($item->getFieldValue('tolerance')) }}</td>
@elseif($isMethodCol)
@php $methodVal = $item->getFieldValue('method'); @endphp
<td class="px-2 py-2 border border-gray-300 text-sm text-center">{{ $methodNames[$methodVal] ?? ($methodVal ?: '-') }}</td>
@elseif($isFreqCol)
<td class="px-2 py-2 border border-gray-300 text-sm text-center">{{ $formatFrequency($item) }}</td>
@elseif($isJudgeCol)
@php
$fieldKey = "s{$section->id}_r{$rowIndex}_c{$col->id}";
$savedVal = $document?->data->where('field_key', $fieldKey)->first()?->field_value ?? '';
@endphp
<td class="px-1 py-1 border border-gray-300 text-center">
<input type="checkbox" name="section_data[{{ $fieldKey }}]" value="OK" {{ $savedVal === 'OK' ? 'checked' : '' }}
data-section-id="{{ $section->id }}" data-column-id="{{ $col->id }}" data-row-index="{{ $rowIndex }}"
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500">
</td>
@else
@php
$staticValue = match(true) {
in_array($label, ['규격', '적용규격', '관련규정']) => $item->getFieldValue('regulation'),
in_array($label, ['분류', '카테고리']) => $item->getFieldValue('category'),
default => null,
};
@endphp
@if($staticValue !== null)
<td class="px-2 py-2 border border-gray-300 text-sm">{{ $staticValue }}</td>
@else
@php
$fieldKey = "s{$section->id}_r{$rowIndex}_c{$col->id}";
$savedVal = $document?->data->where('field_key', $fieldKey)->first()?->field_value ?? '';
@endphp
<td class="px-1 py-1 border border-gray-300 text-center">
<input type="text" name="section_data[{{ $fieldKey }}]" value="{{ $savedVal }}"
data-section-id="{{ $section->id }}" data-column-id="{{ $col->id }}" data-row-index="{{ $rowIndex }}"
class="w-full text-center text-sm border-0 focus:ring-1 focus:ring-blue-400 rounded py-1">
</td>
@endif
@endif
@endif
@endforeach
</tr>
@endforeach
@endif
@endforeach
</tbody>
</table>
</div>
@endif
{{-- 종합판정 / 비고 --}}
@if($loop->last && $template->footer_judgement_label)
<div class="mt-6 grid grid-cols-1 md:grid-cols-2 gap-4 pt-4 border-t border-gray-200">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">
{{ $template->footer_remark_label ?? '비고' }}
</label>
@php
$remarkKey = 'footer_remark';
$remarkVal = $document?->data->where('field_key', $remarkKey)->first()?->field_value ?? '';
@endphp
<textarea name="data[{{ $remarkKey }}]" rows="3"
class="w-full rounded-lg border-gray-300 text-sm focus:border-blue-500 focus:ring-blue-500">{{ $remarkVal }}</textarea>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">
{{ $template->footer_judgement_label ?? '종합판정' }}
</label>
@php
$judgementKey = 'footer_judgement';
$judgementVal = $document?->data->where('field_key', $judgementKey)->first()?->field_value ?? '';
$judgementOptions = $template->footer_judgement_options ?? ['적합', '부적합'];
@endphp
<select name="data[{{ $judgementKey }}]"
class="w-full rounded-lg border-gray-300 text-sm focus:border-blue-500 focus:ring-blue-500">
<option value="">선택하세요</option>
@foreach($judgementOptions as $opt)
<option value="{{ $opt }}" {{ $judgementVal === $opt ? 'selected' : '' }}>{{ $opt }}</option>
@endforeach
</select>
</div>
</div>
@endif
</div>
@endforeach
@endif
{{-- 버튼 --}}
<div class="flex justify-end gap-3">
<a href="{{ route('documents.index') }}"
class="px-6 py-2 bg-gray-100 text-gray-700 text-sm font-medium rounded-lg hover:bg-gray-200 transition-colors">
취소
</a>
<button type="submit"
class="px-6 py-2 bg-blue-600 text-white text-sm font-medium rounded-lg hover:bg-blue-700 transition-colors">
{{ $isCreate ? '저장' : '저장' }}
</button>
@if(!$isCreate && $document && $document->canEdit())
<button type="button" onclick="submitForApproval()"
class="px-6 py-2 bg-green-600 text-white text-sm font-medium rounded-lg hover:bg-green-700 transition-colors">
결재 제출
</button>
@endif
</div>
</form>
@endif
</div>
@endsection
@push('scripts')
<script>
function itemSearch(fieldKey, savedValue) {
return {
searchText: savedValue,
results: [],
showResults: false,
init() {},
async search() {
if (this.searchText.length < 1) {
this.results = [];
this.showResults = false;
return;
}
try {
const res = await fetch(`/api/admin/items/search?q=${encodeURIComponent(this.searchText)}`, {
headers: { 'Accept': 'application/json' }
});
const json = await res.json();
this.results = json.data || [];
this.showResults = this.results.length > 0;
} catch (e) {
this.results = [];
}
},
selectItem(item) {
this.searchText = item.name;
this.showResults = false;
}
};
}
document.addEventListener('DOMContentLoaded', function() {
const form = document.getElementById('documentForm');
if (!form) return;
form.addEventListener('submit', function(e) {
e.preventDefault();
const formData = new FormData(form);
const data = {
template_id: formData.get('template_id'),
title: formData.get('title'),
data: []
};
// 기본 필드 수집 (data[field_key])
for (const [key, value] of formData.entries()) {
if (key.startsWith('data[') && key.endsWith(']')) {
const fieldKey = key.slice(5, -1);
data.data.push({
field_key: fieldKey,
field_value: value
});
}
}
// 섹션 테이블 데이터 수집 (section_data[field_key])
for (const [key, value] of formData.entries()) {
if (key.startsWith('section_data[') && key.endsWith(']')) {
const fieldKey = key.slice(13, -1);
const el = form.querySelector(`[name="${key}"]`);
// 체크박스는 체크 안된 경우 skip
if (el && el.type === 'checkbox' && !el.checked) continue;
data.data.push({
field_key: fieldKey,
field_value: value,
section_id: el?.dataset?.sectionId ? parseInt(el.dataset.sectionId) : null,
column_id: el?.dataset?.columnId ? parseInt(el.dataset.columnId) : null,
row_index: el?.dataset?.rowIndex ? parseInt(el.dataset.rowIndex) : 0,
});
}
}
// 체크 안된 체크박스도 빈 값으로 전송
form.querySelectorAll('input[type="checkbox"][name^="section_data["]').forEach(cb => {
if (!cb.checked) {
const fieldKey = cb.name.slice(13, -1);
data.data.push({
field_key: fieldKey,
field_value: '',
section_id: cb.dataset?.sectionId ? parseInt(cb.dataset.sectionId) : null,
column_id: cb.dataset?.columnId ? parseInt(cb.dataset.columnId) : null,
row_index: cb.dataset?.rowIndex ? parseInt(cb.dataset.rowIndex) : 0,
});
}
});
const isCreate = {{ $isCreate ? 'true' : 'false' }};
const url = isCreate ? '/api/admin/documents' : '/api/admin/documents/{{ $document?->id }}';
const method = isCreate ? 'POST' : 'PATCH';
fetch(url, {
method: method,
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
},
body: JSON.stringify(data)
})
.then(response => response.json())
.then(result => {
if (result.success) {
showToast(isCreate ? '문서가 저장되었습니다.' : '문서가 수정되었습니다.', 'success');
if (isCreate && result.data?.id) {
window.location.href = '/documents/' + result.data.id + '/edit';
} else {
window.location.href = '/documents';
}
} else {
showToast(result.message || '오류가 발생했습니다.', 'error');
}
})
.catch(error => {
console.error('Error:', error);
showToast('오류가 발생했습니다.', 'error');
});
});
});
@if(!$isCreate && $document)
window.submitForApproval = function() {
if (!confirm('결재를 제출하시겠습니까? 제출 후에는 수정이 불가합니다.')) return;
fetch('/api/admin/documents/{{ $document->id }}/submit', {
method: 'POST',
headers: {
'Accept': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
}
})
.then(response => response.json())
.then(result => {
if (result.success) {
showToast(result.message, 'success');
window.location.href = '/documents/{{ $document->id }}';
} else {
showToast(result.message || '오류가 발생했습니다.', 'error');
}
})
.catch(error => {
console.error('Submit error:', error);
showToast('결재 제출 중 오류가 발생했습니다.', 'error');
});
};
@endif
</script>
@endpush