feat: 문서 양식 관리 및 수입검사 양식 개선
- 문서 양식 API 컨트롤러 및 뷰 개선 - 수입검사 양식 시더 업데이트 - 문서 미리보기 뷰 개선 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -625,6 +625,7 @@ private function saveRelations(DocumentTemplate $template, array $data, bool $de
|
||||
'frequency_c' => $item['frequency_c'] ?? null,
|
||||
'frequency' => $item['frequency'] ?? '',
|
||||
'regulation' => $item['regulation'] ?? '',
|
||||
'field_values' => $item['field_values'] ?? null,
|
||||
'sort_order' => $iIndex,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -213,7 +213,18 @@ private function prepareTemplateData(DocumentTemplate $template): array
|
||||
'is_required' => $f->is_required,
|
||||
];
|
||||
})->toArray(),
|
||||
'template_links' => $template->links->map(function ($l) {
|
||||
'template_links' => $this->buildTemplateLinks($template),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* template_links 데이터 빌드 (linked_item_ids 레거시 호환)
|
||||
*/
|
||||
private function buildTemplateLinks(DocumentTemplate $template): array
|
||||
{
|
||||
// 신규 template_links가 있으면 그대로 사용
|
||||
if ($template->links->isNotEmpty()) {
|
||||
return $template->links->map(function ($l) {
|
||||
$values = $l->linkValues->map(function ($v) use ($l) {
|
||||
$displayText = $this->resolveDisplayText($l->source_table, $v->linkable_id, $l->display_fields);
|
||||
|
||||
@@ -235,8 +246,35 @@ private function prepareTemplateData(DocumentTemplate $template): array
|
||||
'is_required' => $l->is_required,
|
||||
'values' => $values,
|
||||
];
|
||||
})->toArray(),
|
||||
];
|
||||
})->toArray();
|
||||
}
|
||||
|
||||
// 레거시: linked_item_ids만 있고 template_links가 없는 경우 가상 엔트리 생성
|
||||
$linkedItemIds = $template->linked_item_ids;
|
||||
if (! empty($linkedItemIds) && is_array($linkedItemIds)) {
|
||||
$displayFields = ['title' => 'name', 'subtitle' => 'code'];
|
||||
$values = collect($linkedItemIds)->map(function ($itemId) use ($displayFields) {
|
||||
return [
|
||||
'id' => $itemId,
|
||||
'linkable_id' => $itemId,
|
||||
'display_text' => $this->resolveDisplayText('items', $itemId, $displayFields),
|
||||
];
|
||||
})->toArray();
|
||||
|
||||
return [[
|
||||
'id' => 'legacy',
|
||||
'link_key' => 'items',
|
||||
'label' => '연결 품목',
|
||||
'link_type' => 'multiple',
|
||||
'source_table' => 'items',
|
||||
'search_params' => null,
|
||||
'display_fields' => $displayFields,
|
||||
'is_required' => false,
|
||||
'values' => $values,
|
||||
]];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
class IncomingInspectionTemplateSeeder extends Seeder
|
||||
{
|
||||
private int $tenantId = 1;
|
||||
private int $tenantId = 287;
|
||||
|
||||
public function run(): void
|
||||
{
|
||||
@@ -97,6 +97,7 @@ private function getTemplateDefinitions(): array
|
||||
'method' => '공급업체 밀시트',
|
||||
'frequency' => '입고시',
|
||||
'regulation' => 'KS D 3528',
|
||||
'field_values' => ['reference_attribute' => 'thickness'],
|
||||
],
|
||||
[
|
||||
'category' => '도금',
|
||||
@@ -296,4 +297,4 @@ private function cleanupExisting(string $name): void
|
||||
DocumentTemplateApprovalLine::where('template_id', $existing->id)->delete();
|
||||
$existing->forceDelete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1136,6 +1136,15 @@ function removeSubLabel(colId, idx) {
|
||||
}
|
||||
}
|
||||
|
||||
// template_links의 items 값을 linked_item_ids로 동기화
|
||||
function syncLinkedItemIds() {
|
||||
const link = getMainLink();
|
||||
if (link && link.source_table === 'items' && link.values && link.values.length > 0) {
|
||||
return link.values.map(v => v.linkable_id || v.id).filter(Boolean);
|
||||
}
|
||||
return templateState.linked_item_ids;
|
||||
}
|
||||
|
||||
// ===== 저장 =====
|
||||
function saveTemplate() {
|
||||
const name = document.getElementById('name').value.trim();
|
||||
@@ -1156,7 +1165,7 @@ function saveTemplate() {
|
||||
footer_judgement_label: document.getElementById('footer_judgement_label').value,
|
||||
footer_judgement_options: templateState.footer_judgement_options.filter(o => o.trim() !== ''),
|
||||
is_active: document.getElementById('is_active').checked,
|
||||
linked_item_ids: templateState.linked_item_ids,
|
||||
linked_item_ids: syncLinkedItemIds(),
|
||||
linked_process_id: templateState.linked_process_id,
|
||||
approval_lines: templateState.approval_lines,
|
||||
basic_fields: templateState.basic_fields,
|
||||
@@ -1850,7 +1859,7 @@ function updateDynamicField(sectionId, itemId, fieldKey, value) {
|
||||
// 기존 컬럼에도 업데이트 (하위 호환)
|
||||
item[fieldKey] = value;
|
||||
// field_values에도 업데이트
|
||||
if (!item.field_values) item.field_values = {};
|
||||
if (!item.field_values || Array.isArray(item.field_values)) item.field_values = {};
|
||||
item.field_values[fieldKey] = value;
|
||||
}
|
||||
|
||||
@@ -1862,6 +1871,16 @@ function calcTableWidth() {
|
||||
return Math.max(width, 600);
|
||||
}
|
||||
|
||||
// 참조속성 업데이트
|
||||
function updateReferenceAttribute(sectionId, itemId, value) {
|
||||
const section = templateState.sections.find(s => s.id == sectionId);
|
||||
if (!section) return;
|
||||
const item = section.items.find(i => i.id == itemId);
|
||||
if (!item) return;
|
||||
if (!item.field_values || Array.isArray(item.field_values)) item.field_values = {};
|
||||
item.field_values.reference_attribute = value || null;
|
||||
}
|
||||
|
||||
function renderDynamicFieldInput(field, sectionId, item) {
|
||||
const val = getItemFieldValue(item, field.field_key);
|
||||
const fk = field.field_key;
|
||||
@@ -1907,12 +1926,23 @@ class="w-full px-2 py-1 border border-gray-200 rounded text-xs">`;
|
||||
return renderToleranceInput(sid, iid, val || item.tolerance);
|
||||
|
||||
case 'text_with_criteria': {
|
||||
// 상단: 검사기준 텍스트, 하단: 기준범위 min/max
|
||||
// 상단: 참조속성 + 검사기준 텍스트, 하단: 기준범위 min/max
|
||||
const c = getItemFieldValue(item, 'standard_criteria') || item.standard_criteria;
|
||||
const refAttr = (item.field_values && item.field_values.reference_attribute) || '';
|
||||
return `<div class="flex flex-col gap-1">
|
||||
<input type="text" value="${escapeHtml(val || '')}" placeholder="검사기준"
|
||||
onchange="updateDynamicField('${sid}', '${iid}', '${fk}', this.value)"
|
||||
class="w-full px-2 py-1 border border-gray-200 rounded text-xs">
|
||||
<div class="flex gap-1">
|
||||
<select onchange="updateReferenceAttribute('${sid}', '${iid}', this.value)"
|
||||
class="px-1 py-1 border border-gray-200 rounded text-xs flex-shrink-0" style="width:72px"
|
||||
title="참조속성">
|
||||
<option value="" ${!refAttr ? 'selected' : ''}>기준치수</option>
|
||||
<option value="thickness" ${refAttr === 'thickness' ? 'selected' : ''}>두께</option>
|
||||
<option value="width" ${refAttr === 'width' ? 'selected' : ''}>너비</option>
|
||||
<option value="length" ${refAttr === 'length' ? 'selected' : ''}>길이</option>
|
||||
</select>
|
||||
<input type="text" value="${escapeHtml(val || '')}" placeholder="검사기준"
|
||||
onchange="updateDynamicField('${sid}', '${iid}', '${fk}', this.value)"
|
||||
class="flex-1 min-w-0 px-2 py-1 border border-gray-200 rounded text-xs">
|
||||
</div>
|
||||
<div style="display:grid;grid-template-columns:1fr auto 8px 1fr auto;align-items:center;gap:2px">
|
||||
<input type="number" step="any" value="${c?.min ?? ''}" placeholder="min"
|
||||
onchange="updateStandardCriteria('${sid}', '${iid}', 'min', this.value)"
|
||||
|
||||
@@ -86,6 +86,8 @@ class="bg-white rounded-lg shadow-sm overflow-hidden">
|
||||
<script>
|
||||
// 폼 제출 시 HTMX 이벤트 트리거
|
||||
(function() {
|
||||
let searchDebounceTimer = null;
|
||||
|
||||
function initFilterForm() {
|
||||
const filterForm = document.getElementById('filterForm');
|
||||
if (filterForm && !filterForm._initialized) {
|
||||
@@ -97,6 +99,19 @@ function initFilterForm() {
|
||||
filterForm._initialized = true;
|
||||
}
|
||||
|
||||
// 검색 input 실시간 필터링 (debounce 300ms)
|
||||
const searchInput = filterForm?.querySelector('input[name="search"]');
|
||||
if (searchInput && !searchInput._initialized) {
|
||||
searchInput.addEventListener('input', function() {
|
||||
clearTimeout(searchDebounceTimer);
|
||||
searchDebounceTimer = setTimeout(function() {
|
||||
handleTrashedFilter();
|
||||
htmx.trigger('#template-table', 'filterSubmit');
|
||||
}, 300);
|
||||
});
|
||||
searchInput._initialized = true;
|
||||
}
|
||||
|
||||
// 휴지통 필터 처리
|
||||
const isActiveFilter = document.getElementById('isActiveFilter');
|
||||
if (isActiveFilter && !isActiveFilter._initialized) {
|
||||
|
||||
@@ -25,16 +25,18 @@ class="text-blue-600 hover:text-blue-800 font-medium">
|
||||
</td>
|
||||
<td class="px-4 py-3 whitespace-nowrap">
|
||||
@if($template->category)
|
||||
@php
|
||||
$categoryName = $categoryNames[$template->category] ?? $template->category;
|
||||
@endphp
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium
|
||||
@switch($template->category)
|
||||
@case('품질') bg-green-100 text-green-800 @break
|
||||
@case('생산') bg-blue-100 text-blue-800 @break
|
||||
@case('영업') bg-purple-100 text-purple-800 @break
|
||||
@case('구매') bg-orange-100 text-orange-800 @break
|
||||
@default bg-gray-100 text-gray-800
|
||||
@endswitch
|
||||
@if(str_contains($categoryName, '수입')) bg-blue-100 text-blue-800
|
||||
@elseif(str_contains($categoryName, '출하')) bg-green-100 text-green-800
|
||||
@elseif(str_contains($categoryName, '품질')) bg-purple-100 text-purple-800
|
||||
@elseif(str_contains($categoryName, '중간')) bg-orange-100 text-orange-800
|
||||
@else bg-gray-100 text-gray-800
|
||||
@endif
|
||||
">
|
||||
{{ $template->category }}
|
||||
{{ $categoryName }}
|
||||
</span>
|
||||
@else
|
||||
<span class="text-gray-400">-</span>
|
||||
|
||||
@@ -176,54 +176,77 @@ class="px-2 py-2 text-center text-xs font-medium text-gray-600 uppercase border
|
||||
@endif
|
||||
</thead>
|
||||
{{-- 테이블 바디 (읽기 전용) --}}
|
||||
{{-- React field_key 형식: {item_id}_n{n}, {item_id}_okng_n{n}, {item_id}_result --}}
|
||||
<tbody>
|
||||
@foreach($section->items as $rowIndex => $item)
|
||||
@php
|
||||
$isOkng = $item->measurement_type === 'checkbox';
|
||||
@endphp
|
||||
<tr class="hover:bg-gray-50">
|
||||
@foreach($document->template->columns as $col)
|
||||
@if($col->column_type === 'complex' && $col->sub_labels)
|
||||
{{-- complex: 서브 라벨별 데이터 --}}
|
||||
{{-- complex: 측정치 (n1, n2, n3...) --}}
|
||||
@foreach($col->sub_labels as $subIndex => $subLabel)
|
||||
@php
|
||||
$fieldKey = "s{$section->id}_r{$rowIndex}_c{$col->id}_sub{$subIndex}";
|
||||
$n = $subIndex + 1;
|
||||
$fieldKey = $isOkng
|
||||
? "{$item->id}_okng_n{$n}"
|
||||
: "{$item->id}_n{$n}";
|
||||
$savedVal = $document->data->where('field_key', $fieldKey)->first()?->field_value ?? '';
|
||||
@endphp
|
||||
<td class="px-2 py-2 border border-gray-300 text-center text-sm">{{ $savedVal ?: '-' }}</td>
|
||||
<td class="px-2 py-2 border border-gray-300 text-center text-sm {{ !$isOkng ? 'font-mono' : '' }}">
|
||||
@if($isOkng && $savedVal)
|
||||
<span class="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium
|
||||
{{ strtolower($savedVal) === 'ok' ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800' }}">
|
||||
{{ strtoupper($savedVal) }}
|
||||
</span>
|
||||
@else
|
||||
{{ $savedVal ?: '-' }}
|
||||
@endif
|
||||
</td>
|
||||
@endforeach
|
||||
@elseif($col->column_type === 'select')
|
||||
{{-- select: 판정 결과 --}}
|
||||
{{-- select: 항목별 판정 → {item_id}_result --}}
|
||||
@php
|
||||
$fieldKey = "s{$section->id}_r{$rowIndex}_c{$col->id}";
|
||||
$fieldKey = "{$item->id}_result";
|
||||
$savedVal = $document->data->where('field_key', $fieldKey)->first()?->field_value ?? '';
|
||||
@endphp
|
||||
<td class="px-2 py-2 border border-gray-300 text-center text-sm">
|
||||
@if($savedVal)
|
||||
@php
|
||||
$isPass = in_array(strtolower($savedVal), ['ok', '적합', '합격', 'pass']);
|
||||
@endphp
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium
|
||||
{{ $savedVal === '적합' || $savedVal === '합격' || $savedVal === 'OK' ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800' }}">
|
||||
{{ $savedVal }}
|
||||
{{ $isPass ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800' }}">
|
||||
{{ $isPass ? '적합' : '부적합' }}
|
||||
</span>
|
||||
@else
|
||||
-
|
||||
@endif
|
||||
</td>
|
||||
@elseif($col->column_type === 'check')
|
||||
{{-- check: 체크 결과 --}}
|
||||
{{-- check: 체크 결과 → {item_id}_result --}}
|
||||
@php
|
||||
$fieldKey = "s{$section->id}_r{$rowIndex}_c{$col->id}";
|
||||
$fieldKey = "{$item->id}_result";
|
||||
$savedVal = $document->data->where('field_key', $fieldKey)->first()?->field_value ?? '';
|
||||
@endphp
|
||||
<td class="px-2 py-2 border border-gray-300 text-center text-sm">
|
||||
@if($savedVal === 'OK')
|
||||
@if(in_array(strtolower($savedVal), ['ok', 'pass', '적합', '합격']))
|
||||
<svg class="w-5 h-5 text-green-500 mx-auto" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
@elseif($savedVal)
|
||||
<svg class="w-5 h-5 text-red-500 mx-auto" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
@else
|
||||
-
|
||||
@endif
|
||||
</td>
|
||||
@elseif($col->column_type === 'measurement')
|
||||
{{-- measurement: 수치 데이터 --}}
|
||||
{{-- measurement: 수치 데이터 → {item_id}_n1 --}}
|
||||
@php
|
||||
$fieldKey = "s{$section->id}_r{$rowIndex}_c{$col->id}";
|
||||
$fieldKey = "{$item->id}_n1";
|
||||
$savedVal = $document->data->where('field_key', $fieldKey)->first()?->field_value ?? '';
|
||||
@endphp
|
||||
<td class="px-2 py-2 border border-gray-300 text-center text-sm font-mono">{{ $savedVal ?: '-' }}</td>
|
||||
@@ -247,7 +270,7 @@ class="px-2 py-2 text-center text-xs font-medium text-gray-600 uppercase border
|
||||
</td>
|
||||
@else
|
||||
@php
|
||||
$fieldKey = "s{$section->id}_r{$rowIndex}_c{$col->id}";
|
||||
$fieldKey = "{$item->id}_n1";
|
||||
$savedVal = $document->data->where('field_key', $fieldKey)->first()?->field_value ?? '';
|
||||
@endphp
|
||||
<td class="px-2 py-2 border border-gray-300 text-center text-sm">{{ $savedVal ?: '-' }}</td>
|
||||
@@ -262,25 +285,29 @@ class="px-2 py-2 text-center text-xs font-medium text-gray-600 uppercase border
|
||||
@endif
|
||||
|
||||
{{-- 종합판정 / 비고 (마지막 섹션에만) --}}
|
||||
@if($loop->last && $document->template->footer_judgement_label)
|
||||
@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
|
||||
?? '';
|
||||
$judgementVal = $document->data->where('field_key', 'overall_result')->first()?->field_value
|
||||
?? $document->data->where('field_key', 'footer_judgement')->first()?->field_value
|
||||
?? '';
|
||||
$isPass = in_array(strtolower($judgementVal), ['pass', 'ok', '적합', '합격']);
|
||||
@endphp
|
||||
<div class="mt-6 grid grid-cols-1 md:grid-cols-2 gap-4 pt-4 border-t border-gray-200">
|
||||
<div>
|
||||
<dt class="text-sm font-medium text-gray-500">{{ $document->template->footer_remark_label ?? '비고' }}</dt>
|
||||
@php
|
||||
$remarkVal = $document->data->where('field_key', 'footer_remark')->first()?->field_value ?? '';
|
||||
@endphp
|
||||
<dd class="mt-1 text-sm text-gray-900 whitespace-pre-wrap">{{ $remarkVal ?: '-' }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt class="text-sm font-medium text-gray-500">{{ $document->template->footer_judgement_label ?? '종합판정' }}</dt>
|
||||
@php
|
||||
$judgementVal = $document->data->where('field_key', 'footer_judgement')->first()?->field_value ?? '';
|
||||
@endphp
|
||||
<dd class="mt-1">
|
||||
@if($judgementVal)
|
||||
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium
|
||||
{{ $judgementVal === '적합' || $judgementVal === '합격' ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800' }}">
|
||||
{{ $judgementVal }}
|
||||
{{ $isPass ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800' }}">
|
||||
{{ $isPass ? '적합' : '부적합' }}
|
||||
</span>
|
||||
@else
|
||||
<span class="text-sm text-gray-500">-</span>
|
||||
|
||||
Reference in New Issue
Block a user