feat:성적서 인쇄뷰 + 문서 편집 버그 수정
- 성적서 인쇄뷰(print.blade.php) 추가: 동적 검사 테이블 렌더링
- DocumentController: print() 메서드, create/edit HTMX HX-Redirect 추가
- 기본필드 field_key: Str::slug→bf_{id} (한글 빈문자열 버그 수정)
- show.blade.php: 성적서 버튼 추가
- 양식 편집 UI 개선 + 복제 기능
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -37,8 +37,12 @@ public function index(Request $request): View|Response
|
||||
/**
|
||||
* 문서 생성 페이지
|
||||
*/
|
||||
public function create(Request $request): View
|
||||
public function create(Request $request): View|Response
|
||||
{
|
||||
if ($request->header('HX-Request')) {
|
||||
return response('', 200)->header('HX-Redirect', route('documents.create', $request->query()));
|
||||
}
|
||||
|
||||
$tenantId = session('selected_tenant_id');
|
||||
$templateId = $request->query('template_id');
|
||||
|
||||
@@ -65,8 +69,12 @@ public function create(Request $request): View
|
||||
/**
|
||||
* 문서 수정 페이지
|
||||
*/
|
||||
public function edit(int $id): View
|
||||
public function edit(int $id): View|Response
|
||||
{
|
||||
if (request()->header('HX-Request')) {
|
||||
return response('', 200)->header('HX-Redirect', route('documents.edit', $id));
|
||||
}
|
||||
|
||||
$tenantId = session('selected_tenant_id');
|
||||
|
||||
$document = Document::with([
|
||||
@@ -95,6 +103,28 @@ public function edit(int $id): View
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 문서 인쇄용 화면 (성적서 양식)
|
||||
*/
|
||||
public function print(int $id): View
|
||||
{
|
||||
$tenantId = session('selected_tenant_id');
|
||||
|
||||
$document = Document::with([
|
||||
'template.approvalLines',
|
||||
'template.basicFields',
|
||||
'template.sections.items',
|
||||
'template.columns',
|
||||
'approvals.user',
|
||||
'data',
|
||||
'creator',
|
||||
])->where('tenant_id', $tenantId)->findOrFail($id);
|
||||
|
||||
return view('documents.print', [
|
||||
'document' => $document,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 문서 상세 페이지 (읽기 전용)
|
||||
*/
|
||||
|
||||
@@ -98,6 +98,7 @@ private function prepareTemplateData(DocumentTemplate $template): array
|
||||
'name' => $template->name,
|
||||
'category' => $template->category,
|
||||
'title' => $template->title,
|
||||
'company_name' => $template->company_name,
|
||||
'footer_remark_label' => $template->footer_remark_label,
|
||||
'footer_judgement_label' => $template->footer_judgement_label,
|
||||
'footer_judgement_options' => $template->footer_judgement_options,
|
||||
@@ -110,6 +111,14 @@ private function prepareTemplateData(DocumentTemplate $template): array
|
||||
'role' => $l->role,
|
||||
];
|
||||
})->toArray(),
|
||||
'basic_fields' => $template->basicFields->map(function ($f) {
|
||||
return [
|
||||
'id' => $f->id,
|
||||
'label' => $f->label,
|
||||
'field_type' => $f->field_type,
|
||||
'default_value' => $f->default_value,
|
||||
];
|
||||
})->toArray(),
|
||||
'sections' => $template->sections->map(function ($s) {
|
||||
return [
|
||||
'id' => $s->id,
|
||||
|
||||
@@ -45,6 +45,10 @@ class="tab-btn px-6 py-4 text-sm font-medium border-b-2 border-blue-500 text-blu
|
||||
class="tab-btn px-6 py-4 text-sm font-medium border-b-2 border-transparent text-gray-500 hover:text-gray-700">
|
||||
결재라인
|
||||
</button>
|
||||
<button onclick="switchTab('basic-fields')" id="tab-basic-fields"
|
||||
class="tab-btn px-6 py-4 text-sm font-medium border-b-2 border-transparent text-gray-500 hover:text-gray-700">
|
||||
기본필드
|
||||
</button>
|
||||
<button onclick="switchTab('sections')" id="tab-sections"
|
||||
class="tab-btn px-6 py-4 text-sm font-medium border-b-2 border-transparent text-gray-500 hover:text-gray-700">
|
||||
검사 기준서
|
||||
@@ -82,12 +86,50 @@ class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none foc
|
||||
<input type="text" id="title" placeholder="예: 최종 검사 성적서"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
</div>
|
||||
<!-- 회사명 -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">회사명</label>
|
||||
<input type="text" id="company_name" placeholder="예: 케이디산업"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
</div>
|
||||
<!-- 활성 상태 -->
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="flex items-center gap-2 self-end pb-2">
|
||||
<input type="checkbox" id="is_active" checked class="w-4 h-4 text-blue-600 rounded focus:ring-blue-500">
|
||||
<label for="is_active" class="text-sm text-gray-700">활성화</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 구분선 -->
|
||||
<hr class="my-6 border-gray-200">
|
||||
|
||||
<!-- Footer 설정 -->
|
||||
<h3 class="text-sm font-semibold text-gray-700 mb-4">Footer 설정</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<!-- 비고 라벨 -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">비고 라벨</label>
|
||||
<input type="text" id="footer_remark_label" placeholder="예: 부적합 내용"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
</div>
|
||||
<!-- 종합판정 라벨 -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">종합판정 라벨</label>
|
||||
<input type="text" id="footer_judgement_label" placeholder="예: 종합판정"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
</div>
|
||||
<!-- 종합판정 옵션 -->
|
||||
<div class="md:col-span-2">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">종합판정 옵션</label>
|
||||
<div class="flex items-center gap-2">
|
||||
<div id="judgement-options-container" class="flex flex-wrap gap-2 flex-1">
|
||||
<!-- 동적으로 렌더링 -->
|
||||
</div>
|
||||
<button type="button" onclick="addJudgementOption()" class="bg-gray-100 hover:bg-gray-200 text-gray-600 px-3 py-2 rounded-lg text-sm flex-shrink-0">
|
||||
+ 옵션 추가
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 결재라인 탭 -->
|
||||
@@ -107,6 +149,26 @@ class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none foc
|
||||
<p id="approval-empty" class="text-gray-400 text-center py-8 hidden">결재라인이 없습니다. 추가 버튼을 클릭하세요.</p>
|
||||
</div>
|
||||
|
||||
<!-- 기본필드 탭 -->
|
||||
<div id="content-basic-fields" class="tab-content bg-white rounded-lg shadow-sm p-6 hidden">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<div>
|
||||
<h3 class="text-lg font-medium text-gray-800">기본필드 설정</h3>
|
||||
<p class="text-xs text-gray-500 mt-1">문서 상단에 표시되는 필드입니다 (예: 품명, LOT NO, 검사일자)</p>
|
||||
</div>
|
||||
<button type="button" onclick="addBasicField()" class="bg-blue-600 hover:bg-blue-700 text-white px-3 py-1.5 rounded-lg text-sm flex items-center gap-1">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
추가
|
||||
</button>
|
||||
</div>
|
||||
<div id="basic-fields-container" class="space-y-3">
|
||||
<!-- 동적으로 추가됨 -->
|
||||
</div>
|
||||
<p id="basic-fields-empty" class="text-gray-400 text-center py-8 hidden">기본필드가 없습니다. 추가 버튼을 클릭하세요.</p>
|
||||
</div>
|
||||
|
||||
<!-- 검사 기준서 탭 -->
|
||||
<div id="content-sections" class="tab-content bg-white rounded-lg shadow-sm p-6 hidden">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
@@ -171,11 +233,13 @@ class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none foc
|
||||
name: '',
|
||||
category: '',
|
||||
title: '',
|
||||
footer_remark_label: '비고',
|
||||
company_name: '',
|
||||
footer_remark_label: '부적합 내용',
|
||||
footer_judgement_label: '종합판정',
|
||||
footer_judgement_options: ['합격', '불합격', '조건부합격'],
|
||||
is_active: true,
|
||||
approval_lines: [],
|
||||
basic_fields: [],
|
||||
sections: [],
|
||||
columns: []
|
||||
};
|
||||
@@ -194,31 +258,38 @@ function generateId() {
|
||||
templateState.name = loadedData.name || '';
|
||||
templateState.category = loadedData.category || '';
|
||||
templateState.title = loadedData.title || '';
|
||||
templateState.company_name = loadedData.company_name || '';
|
||||
templateState.footer_remark_label = loadedData.footer_remark_label || '';
|
||||
templateState.footer_judgement_label = loadedData.footer_judgement_label || '';
|
||||
templateState.footer_judgement_options = loadedData.footer_judgement_options || [];
|
||||
templateState.is_active = loadedData.is_active || false;
|
||||
templateState.approval_lines = loadedData.approval_lines || [];
|
||||
templateState.basic_fields = loadedData.basic_fields || [];
|
||||
templateState.sections = loadedData.sections || [];
|
||||
templateState.columns = loadedData.columns || [];
|
||||
@endif
|
||||
|
||||
// UI 초기화
|
||||
initBasicFields();
|
||||
initBasicInfo();
|
||||
renderJudgementOptions();
|
||||
renderApprovalLines();
|
||||
renderBasicFields();
|
||||
renderSections();
|
||||
renderColumns();
|
||||
});
|
||||
|
||||
// ===== 기본정보 =====
|
||||
function initBasicFields() {
|
||||
function initBasicInfo() {
|
||||
document.getElementById('name').value = templateState.name || '';
|
||||
document.getElementById('category').value = templateState.category || '';
|
||||
document.getElementById('title').value = templateState.title || '';
|
||||
document.getElementById('company_name').value = templateState.company_name || '';
|
||||
document.getElementById('footer_remark_label').value = templateState.footer_remark_label || '';
|
||||
document.getElementById('footer_judgement_label').value = templateState.footer_judgement_label || '';
|
||||
document.getElementById('is_active').checked = templateState.is_active;
|
||||
|
||||
// 변경 이벤트 바인딩
|
||||
['name', 'category', 'title'].forEach(field => {
|
||||
['name', 'category', 'title', 'company_name', 'footer_remark_label', 'footer_judgement_label'].forEach(field => {
|
||||
document.getElementById(field).addEventListener('input', function() {
|
||||
templateState[field] = this.value;
|
||||
});
|
||||
@@ -228,6 +299,98 @@ function initBasicFields() {
|
||||
});
|
||||
}
|
||||
|
||||
// ===== 종합판정 옵션 =====
|
||||
function renderJudgementOptions() {
|
||||
const container = document.getElementById('judgement-options-container');
|
||||
container.innerHTML = templateState.footer_judgement_options.map((opt, idx) => `
|
||||
<div class="flex items-center gap-1 bg-gray-100 rounded-lg px-2 py-1">
|
||||
<input type="text" value="${escapeHtml(opt)}" placeholder="옵션"
|
||||
onchange="updateJudgementOption(${idx}, this.value)"
|
||||
class="w-24 px-2 py-1 border border-gray-200 rounded text-sm bg-white">
|
||||
<button onclick="removeJudgementOption(${idx})" class="text-red-400 hover:text-red-600">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function addJudgementOption() {
|
||||
templateState.footer_judgement_options.push('');
|
||||
renderJudgementOptions();
|
||||
// 마지막 input에 포커스
|
||||
const inputs = document.querySelectorAll('#judgement-options-container input[type="text"]');
|
||||
if (inputs.length) inputs[inputs.length - 1].focus();
|
||||
}
|
||||
|
||||
function updateJudgementOption(idx, value) {
|
||||
templateState.footer_judgement_options[idx] = value;
|
||||
}
|
||||
|
||||
function removeJudgementOption(idx) {
|
||||
templateState.footer_judgement_options.splice(idx, 1);
|
||||
renderJudgementOptions();
|
||||
}
|
||||
|
||||
// ===== 기본필드 (basic_fields) =====
|
||||
function addBasicField() {
|
||||
templateState.basic_fields.push({
|
||||
id: generateId(),
|
||||
label: '',
|
||||
field_type: 'text',
|
||||
default_value: ''
|
||||
});
|
||||
renderBasicFields();
|
||||
}
|
||||
|
||||
function removeBasicField(id) {
|
||||
templateState.basic_fields = templateState.basic_fields.filter(f => f.id !== id);
|
||||
renderBasicFields();
|
||||
}
|
||||
|
||||
function updateBasicField(id, field, value) {
|
||||
const bf = templateState.basic_fields.find(f => f.id === id);
|
||||
if (bf) bf[field] = value;
|
||||
}
|
||||
|
||||
function renderBasicFields() {
|
||||
const container = document.getElementById('basic-fields-container');
|
||||
const emptyMsg = document.getElementById('basic-fields-empty');
|
||||
|
||||
if (templateState.basic_fields.length === 0) {
|
||||
container.innerHTML = '';
|
||||
emptyMsg.classList.remove('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
emptyMsg.classList.add('hidden');
|
||||
container.innerHTML = templateState.basic_fields.map((field, idx) => `
|
||||
<div class="flex items-center gap-3 p-3 bg-gray-50 rounded-lg cursor-move" data-bf-id="${field.id}">
|
||||
<span class="text-gray-400 font-bold" title="드래그하여 순서 변경">⋮⋮</span>
|
||||
<span class="text-gray-400 font-mono text-sm w-6">${idx + 1}</span>
|
||||
<input type="text" value="${escapeHtml(field.label)}" placeholder="필드명 (예: 품명, LOT NO)"
|
||||
onchange="updateBasicField('${field.id}', 'label', this.value)"
|
||||
class="flex-1 px-3 py-2 border border-gray-300 rounded-lg text-sm">
|
||||
<select onchange="updateBasicField('${field.id}', 'field_type', this.value)"
|
||||
class="w-28 px-3 py-2 border border-gray-300 rounded-lg text-sm">
|
||||
<option value="text" ${field.field_type === 'text' ? 'selected' : ''}>텍스트</option>
|
||||
<option value="date" ${field.field_type === 'date' ? 'selected' : ''}>날짜</option>
|
||||
<option value="number" ${field.field_type === 'number' ? 'selected' : ''}>숫자</option>
|
||||
<option value="select" ${field.field_type === 'select' ? 'selected' : ''}>선택</option>
|
||||
</select>
|
||||
<input type="text" value="${escapeHtml(field.default_value || '')}" placeholder="기본값"
|
||||
onchange="updateBasicField('${field.id}', 'default_value', this.value)"
|
||||
class="w-32 px-3 py-2 border border-gray-300 rounded-lg text-sm">
|
||||
<button onclick="removeBasicField('${field.id}')" class="text-red-500 hover:text-red-700 p-1">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
// ===== 탭 전환 =====
|
||||
function switchTab(tabId) {
|
||||
// 모든 탭 버튼 비활성화
|
||||
@@ -496,34 +659,107 @@ function renderColumns() {
|
||||
|
||||
emptyMsg.classList.add('hidden');
|
||||
container.innerHTML = templateState.columns.map((col, idx) => `
|
||||
<div class="flex items-center gap-3 p-3 bg-gray-50 rounded-lg cursor-move" data-column-id="${col.id}">
|
||||
<span class="text-gray-400 font-bold" title="드래그하여 순서 변경">⋮⋮</span>
|
||||
<span class="text-gray-400 font-mono text-sm w-6">${idx + 1}</span>
|
||||
<input type="text" value="${escapeHtml(col.label)}" placeholder="컬럼명"
|
||||
onchange="updateColumn('${col.id}', 'label', this.value)"
|
||||
class="flex-1 px-3 py-2 border border-gray-300 rounded-lg text-sm">
|
||||
<input type="text" value="${escapeHtml(col.width)}" placeholder="너비"
|
||||
onchange="updateColumn('${col.id}', 'width', this.value)"
|
||||
class="w-24 px-3 py-2 border border-gray-300 rounded-lg text-sm">
|
||||
<select onchange="updateColumn('${col.id}', 'column_type', this.value)"
|
||||
class="w-32 px-3 py-2 border border-gray-300 rounded-lg text-sm">
|
||||
<option value="text" ${col.column_type === 'text' ? 'selected' : ''}>텍스트</option>
|
||||
<option value="check" ${col.column_type === 'check' ? 'selected' : ''}>체크</option>
|
||||
<option value="measurement" ${col.column_type === 'measurement' ? 'selected' : ''}>측정값</option>
|
||||
<option value="select" ${col.column_type === 'select' ? 'selected' : ''}>선택</option>
|
||||
</select>
|
||||
<input type="text" value="${escapeHtml(col.group_name || '')}" placeholder="그룹명"
|
||||
onchange="updateColumn('${col.id}', 'group_name', this.value)"
|
||||
class="w-28 px-3 py-2 border border-gray-300 rounded-lg text-sm">
|
||||
<button onclick="removeColumn('${col.id}')" class="text-red-500 hover:text-red-700 p-1">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||
</svg>
|
||||
</button>
|
||||
<div class="p-3 bg-gray-50 rounded-lg cursor-move" data-column-id="${col.id}">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="text-gray-400 font-bold" title="드래그하여 순서 변경">⋮⋮</span>
|
||||
<span class="text-gray-400 font-mono text-sm w-6">${idx + 1}</span>
|
||||
<input type="text" value="${escapeHtml(col.label)}" placeholder="컬럼명"
|
||||
onchange="updateColumn('${col.id}', 'label', this.value)"
|
||||
class="flex-1 px-3 py-2 border border-gray-300 rounded-lg text-sm">
|
||||
<input type="text" value="${escapeHtml(col.width)}" placeholder="너비"
|
||||
onchange="updateColumn('${col.id}', 'width', this.value)"
|
||||
class="w-24 px-3 py-2 border border-gray-300 rounded-lg text-sm">
|
||||
<select onchange="handleColumnTypeChange('${col.id}', this.value)"
|
||||
class="w-32 px-3 py-2 border border-gray-300 rounded-lg text-sm">
|
||||
<option value="text" ${col.column_type === 'text' ? 'selected' : ''}>텍스트</option>
|
||||
<option value="check" ${col.column_type === 'check' ? 'selected' : ''}>체크</option>
|
||||
<option value="measurement" ${col.column_type === 'measurement' ? 'selected' : ''}>측정값</option>
|
||||
<option value="select" ${col.column_type === 'select' ? 'selected' : ''}>선택</option>
|
||||
<option value="complex" ${col.column_type === 'complex' ? 'selected' : ''}>복합(하위컬럼)</option>
|
||||
</select>
|
||||
<input type="text" value="${escapeHtml(col.group_name || '')}" placeholder="그룹명"
|
||||
onchange="updateColumn('${col.id}', 'group_name', this.value)"
|
||||
class="w-28 px-3 py-2 border border-gray-300 rounded-lg text-sm">
|
||||
<button onclick="removeColumn('${col.id}')" class="text-red-500 hover:text-red-700 p-1">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
${col.column_type === 'complex' ? `
|
||||
<div class="ml-10 mt-2 p-2 bg-white rounded border border-gray-200">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<span class="text-xs font-medium text-gray-600">하위 라벨:</span>
|
||||
<button onclick="addSubLabel('${col.id}')" class="text-blue-600 hover:text-blue-800 text-xs">+ 추가</button>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
${(col.sub_labels || []).map((label, si) => `
|
||||
<div class="flex items-center gap-1 bg-blue-50 rounded px-2 py-1">
|
||||
<input type="text" value="${escapeHtml(label)}" placeholder="${si + 1}"
|
||||
onchange="updateSubLabel('${col.id}', ${si}, this.value)"
|
||||
class="w-16 px-1 py-0.5 border border-blue-200 rounded text-xs bg-white text-center">
|
||||
<button onclick="removeSubLabel('${col.id}', ${si})" class="text-red-400 hover:text-red-600">
|
||||
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
`).join('')}
|
||||
${(!col.sub_labels || col.sub_labels.length === 0) ? '<span class="text-xs text-gray-400">하위 라벨이 없습니다.</span>' : ''}
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
// ===== 컬럼 타입 변경 처리 =====
|
||||
function handleColumnTypeChange(id, value) {
|
||||
const column = templateState.columns.find(c => c.id === id);
|
||||
if (column) {
|
||||
column.column_type = value;
|
||||
if (value === 'complex' && !column.sub_labels) {
|
||||
column.sub_labels = ['1', '2', '3', '4', '5'];
|
||||
}
|
||||
if (value !== 'complex') {
|
||||
column.sub_labels = null;
|
||||
}
|
||||
renderColumns();
|
||||
}
|
||||
}
|
||||
|
||||
function addSubLabel(colId) {
|
||||
const column = templateState.columns.find(c => c.id === colId);
|
||||
if (column) {
|
||||
if (!column.sub_labels) column.sub_labels = [];
|
||||
column.sub_labels.push('');
|
||||
renderColumns();
|
||||
// 마지막 input에 포커스
|
||||
setTimeout(() => {
|
||||
const colEl = document.querySelector(`[data-column-id="${colId}"]`);
|
||||
if (colEl) {
|
||||
const inputs = colEl.querySelectorAll('.bg-blue-50 input');
|
||||
if (inputs.length) inputs[inputs.length - 1].focus();
|
||||
}
|
||||
}, 50);
|
||||
}
|
||||
}
|
||||
|
||||
function updateSubLabel(colId, idx, value) {
|
||||
const column = templateState.columns.find(c => c.id === colId);
|
||||
if (column && column.sub_labels) {
|
||||
column.sub_labels[idx] = value;
|
||||
}
|
||||
}
|
||||
|
||||
function removeSubLabel(colId, idx) {
|
||||
const column = templateState.columns.find(c => c.id === colId);
|
||||
if (column && column.sub_labels) {
|
||||
column.sub_labels.splice(idx, 1);
|
||||
renderColumns();
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 저장 =====
|
||||
function saveTemplate() {
|
||||
const name = document.getElementById('name').value.trim();
|
||||
@@ -538,8 +774,14 @@ function saveTemplate() {
|
||||
name: name,
|
||||
category: document.getElementById('category').value,
|
||||
title: document.getElementById('title').value,
|
||||
|
||||
company_name: document.getElementById('company_name').value,
|
||||
footer_remark_label: document.getElementById('footer_remark_label').value,
|
||||
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,
|
||||
approval_lines: templateState.approval_lines,
|
||||
basic_fields: templateState.basic_fields,
|
||||
sections: templateState.sections,
|
||||
columns: templateState.columns
|
||||
};
|
||||
@@ -588,121 +830,152 @@ function closePreviewModal() {
|
||||
}
|
||||
|
||||
function generatePreviewHtml() {
|
||||
const title = document.getElementById('title').value || '수입검사 성적서';
|
||||
const title = document.getElementById('title').value || '문서 양식';
|
||||
const companyName = document.getElementById('company_name').value || '회사명';
|
||||
const remarkLabel = document.getElementById('footer_remark_label').value || '비고';
|
||||
const judgementLabel = document.getElementById('footer_judgement_label').value || '종합판정';
|
||||
const judgementOptions = templateState.footer_judgement_options.filter(o => o.trim() !== '');
|
||||
|
||||
// 검사항목 행 생성
|
||||
const renderItems = () => {
|
||||
if (templateState.sections.length === 0 || templateState.sections[0].items.length === 0) {
|
||||
return `<tr><td colspan="10" class="text-center py-4 text-gray-400">검사항목이 없습니다.</td></tr>`;
|
||||
// 결재란 생성
|
||||
const renderApproval = () => {
|
||||
if (templateState.approval_lines.length === 0) {
|
||||
return '<span class="text-xs text-gray-400">결재라인 미설정</span>';
|
||||
}
|
||||
return templateState.sections[0].items.map((item, idx) => `
|
||||
<tr>
|
||||
<td class="border border-gray-400 px-2 py-1.5 text-center">${idx + 1}</td>
|
||||
<td class="border border-gray-400 px-2 py-1.5">${escapeHtml(item.item)}</td>
|
||||
<td class="border border-gray-400 px-2 py-1.5">${escapeHtml(item.standard)}</td>
|
||||
<td class="border border-gray-400 px-2 py-1.5 text-center">${escapeHtml(item.method)}</td>
|
||||
<td class="border border-gray-400 px-2 py-1.5 text-center">LOT</td>
|
||||
<td class="border border-gray-400 px-2 py-1.5 text-center"><input type="radio" name="j${idx}_1"> OK <input type="radio" name="j${idx}_1"> NG</td>
|
||||
<td class="border border-gray-400 px-2 py-1.5 text-center">-</td>
|
||||
<td class="border border-gray-400 px-2 py-1.5 text-center">-</td>
|
||||
<td class="border border-gray-400 px-2 py-1.5 text-center">-</td>
|
||||
<td class="border border-gray-400 px-2 py-1.5 text-center">-</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
return `<table class="border-collapse text-xs">
|
||||
<tr>${templateState.approval_lines.map(l => `<td class="border border-gray-400 px-3 py-1 bg-gray-100 text-center font-medium">${escapeHtml(l.name || '-')}</td>`).join('')}</tr>
|
||||
<tr>${templateState.approval_lines.map(l => `<td class="border border-gray-400 px-3 py-1 text-center"><div class="text-gray-400 text-xs">${escapeHtml(l.dept || '')}</div><div class="h-6"></div></td>`).join('')}</tr>
|
||||
</table>`;
|
||||
};
|
||||
|
||||
// 기본필드 테이블 생성
|
||||
const renderBasicInfo = () => {
|
||||
if (templateState.basic_fields.length === 0) {
|
||||
return '<p class="text-xs text-gray-400 mb-4">기본필드 미설정</p>';
|
||||
}
|
||||
const fields = templateState.basic_fields;
|
||||
const rows = [];
|
||||
for (let i = 0; i < fields.length; i += 2) {
|
||||
const f1 = fields[i];
|
||||
const f2 = fields[i + 1];
|
||||
rows.push(`<tr>
|
||||
<td class="border border-gray-400 px-2 py-1.5 bg-gray-100 font-medium" style="width:100px">${escapeHtml(f1.label || '-')}</td>
|
||||
<td class="border border-gray-400 px-2 py-1.5 text-gray-400">${escapeHtml(f1.default_value || '(입력)')}</td>
|
||||
${f2 ? `<td class="border border-gray-400 px-2 py-1.5 bg-gray-100 font-medium" style="width:100px">${escapeHtml(f2.label || '-')}</td>
|
||||
<td class="border border-gray-400 px-2 py-1.5 text-gray-400">${escapeHtml(f2.default_value || '(입력)')}</td>` : '<td class="border border-gray-400 px-2 py-1.5" colspan="2"></td>'}
|
||||
</tr>`);
|
||||
}
|
||||
return `<table class="w-full border-collapse text-xs mb-4">${rows.join('')}</table>`;
|
||||
};
|
||||
|
||||
// 검사 데이터 컬럼 헤더 생성
|
||||
const renderColumnHeaders = () => {
|
||||
if (templateState.columns.length === 0) return '';
|
||||
const hasComplex = templateState.columns.some(c => c.column_type === 'complex' && c.sub_labels && c.sub_labels.length > 0);
|
||||
let headerRow1 = '';
|
||||
let headerRow2 = '';
|
||||
templateState.columns.forEach(col => {
|
||||
if (col.column_type === 'complex' && col.sub_labels && col.sub_labels.length > 0) {
|
||||
headerRow1 += `<th class="border border-gray-400 px-2 py-1.5 bg-gray-100" colspan="${col.sub_labels.length}">${escapeHtml(col.label)}</th>`;
|
||||
col.sub_labels.forEach(sl => {
|
||||
headerRow2 += `<th class="border border-gray-400 px-1 py-1 bg-gray-100" style="width:40px">${escapeHtml(sl)}</th>`;
|
||||
});
|
||||
} else {
|
||||
headerRow1 += `<th class="border border-gray-400 px-2 py-1.5 bg-gray-100" ${hasComplex ? 'rowspan="2"' : ''} style="width:${col.width || '80px'}">${escapeHtml(col.label)}</th>`;
|
||||
}
|
||||
});
|
||||
if (hasComplex) {
|
||||
return `<tr>${headerRow1}</tr><tr>${headerRow2}</tr>`;
|
||||
}
|
||||
return `<tr>${headerRow1}</tr>`;
|
||||
};
|
||||
|
||||
// 검사항목 행 생성
|
||||
const renderInspectionRows = () => {
|
||||
const allItems = [];
|
||||
templateState.sections.forEach(section => {
|
||||
if (section.items) {
|
||||
section.items.forEach(item => allItems.push(item));
|
||||
}
|
||||
});
|
||||
if (allItems.length === 0) {
|
||||
const colSpan = templateState.columns.reduce((sum, c) => sum + (c.column_type === 'complex' && c.sub_labels ? c.sub_labels.length : 1), 0) || 1;
|
||||
return `<tr><td colspan="${colSpan}" class="text-center py-4 text-gray-400 border border-gray-400">검사항목이 없습니다.</td></tr>`;
|
||||
}
|
||||
return allItems.map((item, idx) => {
|
||||
const cells = templateState.columns.map(col => {
|
||||
if (col.column_type === 'complex' && col.sub_labels && col.sub_labels.length > 0) {
|
||||
return col.sub_labels.map(() => '<td class="border border-gray-400 px-1 py-1.5 text-center text-gray-300">-</td>').join('');
|
||||
}
|
||||
// 기본 컬럼: 검사항목 데이터 매칭 시도
|
||||
const label = (col.label || '').trim();
|
||||
let val = '-';
|
||||
if (label === 'NO' || label === 'no') val = idx + 1;
|
||||
else if (label.includes('검사항목') || label.includes('항목')) val = escapeHtml(item.item || '-');
|
||||
else if (label.includes('검사기준') || label.includes('기준')) val = escapeHtml(item.standard || '-');
|
||||
else if (label.includes('검사방')) val = escapeHtml(item.method || '-');
|
||||
else if (label.includes('주기')) val = escapeHtml(item.frequency || '-');
|
||||
else if (label.includes('판정')) val = '<span class="text-gray-400">-</span>';
|
||||
return `<td class="border border-gray-400 px-2 py-1.5 text-center">${val}</td>`;
|
||||
}).join('');
|
||||
return `<tr>${cells}</tr>`;
|
||||
}).join('');
|
||||
};
|
||||
|
||||
// 실제 React 성적서 양식과 동일한 형태
|
||||
return `
|
||||
<div class="bg-white p-6" style="font-family: 'Malgun Gothic', sans-serif; font-size: 12px; max-width: 900px; margin: 0 auto;">
|
||||
<!-- 헤더: 로고 + 제목 + 결재란 -->
|
||||
<div class="flex justify-between items-start mb-4">
|
||||
<!-- 로고 -->
|
||||
<div class="text-center" style="width: 80px;">
|
||||
<div class="text-2xl font-bold">KD</div>
|
||||
<div class="text-xs">경동기업</div>
|
||||
<div class="text-xs">${escapeHtml(companyName)}</div>
|
||||
</div>
|
||||
<!-- 제목 -->
|
||||
<div class="flex-1 text-center">
|
||||
<h1 class="text-xl font-bold tracking-widest">${escapeHtml(title)}</h1>
|
||||
</div>
|
||||
<!-- 결재란 -->
|
||||
<div>
|
||||
<table class="border-collapse text-xs" style="width: 120px;">
|
||||
<tr>
|
||||
<td class="border border-gray-400 px-2 py-1 bg-gray-100">담당</td>
|
||||
<td class="border border-gray-400 px-2 py-1 bg-gray-100">부서장</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="border border-gray-400 px-2 py-1">결재</td>
|
||||
<td class="border border-gray-400 px-2 py-1">노원호</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div class="text-right text-xs mt-1">참고일자: 2026-01-29</div>
|
||||
</div>
|
||||
<div>${renderApproval()}</div>
|
||||
</div>
|
||||
|
||||
<!-- 기본 정보 테이블 -->
|
||||
<table class="w-full border-collapse text-xs mb-4">
|
||||
<tr>
|
||||
<td class="border border-gray-400 px-2 py-1.5 bg-gray-100 font-medium" style="width:80px">품 명</td>
|
||||
<td class="border border-gray-400 px-2 py-1.5" colspan="2">SUS304 스테인리스 판재</td>
|
||||
<td class="border border-gray-400 px-2 py-1.5 bg-gray-100 font-medium" style="width:100px">납품업체<br>(제조업체)</td>
|
||||
<td class="border border-gray-400 px-2 py-1.5">(주)대한철강</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="border border-gray-400 px-2 py-1.5 bg-gray-100 font-medium">규 격<br>(두께*너비<br>*길이)</td>
|
||||
<td class="border border-gray-400 px-2 py-1.5" colspan="2">1000×2000×3T</td>
|
||||
<td class="border border-gray-400 px-2 py-1.5 bg-gray-100 font-medium">로트번호</td>
|
||||
<td class="border border-gray-400 px-2 py-1.5">LOT-2026-001</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="border border-gray-400 px-2 py-1.5 bg-gray-100 font-medium">자재번호</td>
|
||||
<td class="border border-gray-400 px-2 py-1.5" colspan="2">PE02RB</td>
|
||||
<td class="border border-gray-400 px-2 py-1.5 bg-gray-100 font-medium">검사일자</td>
|
||||
<td class="border border-gray-400 px-2 py-1.5">01/29</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="border border-gray-400 px-2 py-1.5 bg-gray-100 font-medium">로트크기</td>
|
||||
<td class="border border-gray-400 px-2 py-1.5">200</td>
|
||||
<td class="border border-gray-400 px-2 py-1.5 text-center">매</td>
|
||||
<td class="border border-gray-400 px-2 py-1.5 bg-gray-100 font-medium">검사자</td>
|
||||
<td class="border border-gray-400 px-2 py-1.5">노원호 <input type="checkbox" checked class="ml-2"></td>
|
||||
</tr>
|
||||
</table>
|
||||
<!-- 기본필드 정보 -->
|
||||
${renderBasicInfo()}
|
||||
|
||||
<!-- 검사 항목 테이블 -->
|
||||
<table class="w-full border-collapse text-xs">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="border border-gray-400 px-2 py-1.5 bg-gray-100" style="width:30px" rowspan="2">NO</th>
|
||||
<th class="border border-gray-400 px-2 py-1.5 bg-gray-100" style="width:80px" rowspan="2">검사항목</th>
|
||||
<th class="border border-gray-400 px-2 py-1.5 bg-gray-100" rowspan="2">검사기준</th>
|
||||
<th class="border border-gray-400 px-2 py-1.5 bg-gray-100" style="width:60px" rowspan="2">검사방식</th>
|
||||
<th class="border border-gray-400 px-2 py-1.5 bg-gray-100" style="width:60px" rowspan="2">검사주기</th>
|
||||
<th class="border border-gray-400 px-2 py-1.5 bg-gray-100" colspan="3">측정값</th>
|
||||
<th class="border border-gray-400 px-2 py-1.5 bg-gray-100" style="width:50px" rowspan="2">판정<br>(적/부)</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="border border-gray-400 px-1 py-1 bg-gray-100" style="width:50px">n1</th>
|
||||
<th class="border border-gray-400 px-1 py-1 bg-gray-100" style="width:50px">n2</th>
|
||||
<th class="border border-gray-400 px-1 py-1 bg-gray-100" style="width:50px">n3</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${renderItems()}
|
||||
</tbody>
|
||||
</table>
|
||||
<!-- 검사 기준 이미지 -->
|
||||
${templateState.sections.filter(s => s.image_path).map(s => `
|
||||
<div class="mb-4 text-center">
|
||||
<p class="text-xs font-medium text-gray-600 mb-1">${escapeHtml(s.title || '검사 기준')}</p>
|
||||
<img src="/storage/${s.image_path}" alt="${escapeHtml(s.title)}" class="max-h-40 mx-auto border rounded">
|
||||
</div>
|
||||
`).join('')}
|
||||
|
||||
<!-- 종합판정 -->
|
||||
<div class="flex justify-end mt-4">
|
||||
<table class="border-collapse text-xs">
|
||||
<tr>
|
||||
<td class="border border-gray-400 px-4 py-2 bg-gray-100 font-medium">종합판정</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="border border-gray-400 px-4 py-3 text-center text-gray-400">미완료</td>
|
||||
</tr>
|
||||
<!-- 검사 데이터 테이블 -->
|
||||
${templateState.columns.length > 0 ? `
|
||||
<table class="w-full border-collapse text-xs">
|
||||
<thead>${renderColumnHeaders()}</thead>
|
||||
<tbody>${renderInspectionRows()}</tbody>
|
||||
</table>
|
||||
` : '<p class="text-xs text-gray-400 text-center py-4">테이블 컬럼이 설정되지 않았습니다.</p>'}
|
||||
|
||||
<!-- Footer: 비고 + 종합판정 -->
|
||||
<div class="mt-4 flex gap-4">
|
||||
<div class="flex-1">
|
||||
<table class="w-full border-collapse text-xs">
|
||||
<tr>
|
||||
<td class="border border-gray-400 px-3 py-2 bg-gray-100 font-medium" style="width:100px">${escapeHtml(remarkLabel)}</td>
|
||||
<td class="border border-gray-400 px-3 py-2 text-gray-400">(내용 입력)</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div>
|
||||
<table class="border-collapse text-xs">
|
||||
<tr>
|
||||
<td class="border border-gray-400 px-4 py-2 bg-gray-100 font-medium">${escapeHtml(judgementLabel)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="border border-gray-400 px-4 py-3 text-center">
|
||||
${judgementOptions.length > 0 ? judgementOptions.map(opt => `<span class="inline-block mx-1 px-2 py-0.5 border border-gray-300 rounded text-gray-500">${escapeHtml(opt)}</span>`).join('') : '<span class="text-gray-400">옵션 미설정</span>'}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -792,6 +1065,23 @@ function initSortable() {
|
||||
});
|
||||
}
|
||||
|
||||
// 기본필드 정렬
|
||||
const basicFieldsContainer = document.getElementById('basic-fields-container');
|
||||
if (basicFieldsContainer && typeof Sortable !== 'undefined') {
|
||||
new Sortable(basicFieldsContainer, {
|
||||
animation: 150,
|
||||
onEnd: function(evt) {
|
||||
const newOrder = [];
|
||||
basicFieldsContainer.querySelectorAll('[data-bf-id]').forEach((el) => {
|
||||
const id = el.dataset.bfId;
|
||||
const field = templateState.basic_fields.find(f => f.id === id);
|
||||
if (field) newOrder.push(field);
|
||||
});
|
||||
templateState.basic_fields = newOrder;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 컬럼 정렬
|
||||
const columnsContainer = document.getElementById('columns-container');
|
||||
if (columnsContainer && typeof Sortable !== 'undefined') {
|
||||
@@ -817,6 +1107,12 @@ function initSortable() {
|
||||
setTimeout(initSortable, 100);
|
||||
};
|
||||
|
||||
const originalRenderBasicFields = renderBasicFields;
|
||||
renderBasicFields = function() {
|
||||
originalRenderBasicFields();
|
||||
setTimeout(initSortable, 100);
|
||||
};
|
||||
|
||||
const originalRenderSections = renderSections;
|
||||
renderSections = function() {
|
||||
originalRenderSections();
|
||||
|
||||
@@ -78,7 +78,7 @@ class="w-full rounded-lg border-gray-300 text-sm focus:border-blue-500 focus:rin
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
@foreach($template->basicFields as $field)
|
||||
@php
|
||||
$fieldKey = \Illuminate\Support\Str::slug($field->label, '_');
|
||||
$fieldKey = 'bf_' . $field->id;
|
||||
$savedValue = $document?->data->where('field_key', $fieldKey)->first()?->field_value ?? $field->default_value ?? '';
|
||||
@endphp
|
||||
<div>
|
||||
@@ -160,10 +160,6 @@ class="px-2 py-2 text-center text-xs font-medium text-gray-600 uppercase border
|
||||
@foreach($section->items as $rowIndex => $item)
|
||||
<tr class="hover:bg-blue-50" data-row-index="{{ $rowIndex }}">
|
||||
@foreach($template->columns as $col)
|
||||
@php
|
||||
$colSlug = \Illuminate\Support\Str::slug($col->label, '_');
|
||||
@endphp
|
||||
|
||||
@if($col->column_type === 'complex' && $col->sub_labels)
|
||||
{{-- complex: 서브 라벨별 입력 필드 --}}
|
||||
@foreach($col->sub_labels as $subIndex => $subLabel)
|
||||
|
||||
250
resources/views/documents/print.blade.php
Normal file
250
resources/views/documents/print.blade.php
Normal file
@@ -0,0 +1,250 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', '문서 인쇄 - ' . $document->title)
|
||||
|
||||
@section('content')
|
||||
<div class="max-w-7xl mx-auto">
|
||||
{{-- 상단 버튼 --}}
|
||||
<div class="flex justify-between items-center mb-4 print:hidden">
|
||||
<div>
|
||||
<h1 class="text-xl font-bold text-gray-800">{{ $document->title }}</h1>
|
||||
<p class="text-sm text-gray-500">{{ $document->document_no }}</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<a href="{{ route('documents.show', $document->id) }}"
|
||||
class="bg-gray-200 hover:bg-gray-300 text-gray-700 px-4 py-2 rounded-lg transition text-sm">
|
||||
상세보기
|
||||
</a>
|
||||
<a href="{{ route('documents.index') }}"
|
||||
class="bg-gray-200 hover:bg-gray-300 text-gray-700 px-4 py-2 rounded-lg transition text-sm">
|
||||
목록
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- 성적서 본문 --}}
|
||||
<div class="bg-white border border-gray-300 p-8 print:p-4 print:border-0" id="printArea">
|
||||
|
||||
@php
|
||||
$template = $document->template;
|
||||
$hasComplexCol = $template->columns->contains(fn($c) => $c->column_type === 'complex' && $c->sub_labels);
|
||||
@endphp
|
||||
|
||||
{{-- 섹션별 검사 테이블 --}}
|
||||
@if($template->sections && $template->sections->count() > 0)
|
||||
@foreach($template->sections as $sectionIndex => $section)
|
||||
|
||||
{{-- 섹션 제목 --}}
|
||||
@if($template->sections->count() > 1)
|
||||
<div class="mb-2 {{ $sectionIndex > 0 ? 'mt-8' : '' }}">
|
||||
<h3 class="text-sm font-semibold text-gray-700">{{ $section->title }}</h3>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- 검사 기준 이미지 --}}
|
||||
@if($section->image_path)
|
||||
<div class="mb-4">
|
||||
<img src="{{ asset($section->image_path) }}" alt="{{ $section->title }}" class="max-w-md h-auto border">
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- 검사 데이터 테이블 --}}
|
||||
@if($section->items->count() > 0 && $template->columns->count() > 0)
|
||||
<table class="w-full border-collapse text-sm mb-2" style="border: 1px solid #333;">
|
||||
{{-- 테이블 헤더 --}}
|
||||
<thead>
|
||||
<tr>
|
||||
@foreach($template->columns as $col)
|
||||
@if($col->column_type === 'complex' && $col->sub_labels)
|
||||
<th colspan="{{ count($col->sub_labels) }}"
|
||||
class="doc-th"
|
||||
style="min-width: {{ $col->width ?: '80px' }}">
|
||||
{{ $col->label }}
|
||||
</th>
|
||||
@else
|
||||
<th rowspan="{{ $hasComplexCol ? 2 : 1 }}"
|
||||
class="doc-th"
|
||||
style="min-width: {{ $col->width ?: '60px' }}">
|
||||
{{ $col->label }}
|
||||
</th>
|
||||
@endif
|
||||
@endforeach
|
||||
</tr>
|
||||
{{-- 서브 라벨 행 (complex 컬럼) --}}
|
||||
@if($hasComplexCol)
|
||||
<tr>
|
||||
@foreach($template->columns as $col)
|
||||
@if($col->column_type === 'complex' && $col->sub_labels)
|
||||
@foreach($col->sub_labels as $subLabel)
|
||||
<th class="doc-th text-xs font-normal" style="min-width: 60px">
|
||||
{{ $subLabel }}
|
||||
</th>
|
||||
@endforeach
|
||||
@endif
|
||||
@endforeach
|
||||
</tr>
|
||||
@endif
|
||||
</thead>
|
||||
|
||||
{{-- 테이블 바디 --}}
|
||||
<tbody>
|
||||
@foreach($section->items as $rowIndex => $item)
|
||||
<tr>
|
||||
@foreach($template->columns as $col)
|
||||
@if($col->column_type === 'complex' && $col->sub_labels)
|
||||
{{-- complex: 서브 라벨별 측정값 --}}
|
||||
@foreach($col->sub_labels as $subIndex => $subLabel)
|
||||
@php
|
||||
$fieldKey = "s{$section->id}_r{$rowIndex}_c{$col->id}_sub{$subIndex}";
|
||||
$savedVal = $document->data->where('field_key', $fieldKey)->first()?->field_value ?? '';
|
||||
@endphp
|
||||
<td class="doc-td font-mono text-center">
|
||||
{{ $savedVal ?: '-' }}
|
||||
</td>
|
||||
@endforeach
|
||||
|
||||
@elseif($col->column_type === 'select')
|
||||
{{-- select: 판정 --}}
|
||||
@php
|
||||
$fieldKey = "s{$section->id}_r{$rowIndex}_c{$col->id}";
|
||||
$savedVal = $document->data->where('field_key', $fieldKey)->first()?->field_value ?? '';
|
||||
@endphp
|
||||
<td class="doc-td text-center">
|
||||
@if($savedVal)
|
||||
<span class="{{ in_array($savedVal, ['적합', '합격', 'OK']) ? 'text-blue-700' : 'text-red-600' }} font-medium">
|
||||
{{ $savedVal }}
|
||||
</span>
|
||||
@else
|
||||
-
|
||||
@endif
|
||||
</td>
|
||||
|
||||
@elseif($col->column_type === 'check')
|
||||
{{-- check: OK 체크 --}}
|
||||
@php
|
||||
$fieldKey = "s{$section->id}_r{$rowIndex}_c{$col->id}";
|
||||
$savedVal = $document->data->where('field_key', $fieldKey)->first()?->field_value ?? '';
|
||||
@endphp
|
||||
<td class="doc-td text-center">
|
||||
@if($savedVal === 'OK')
|
||||
<span class="text-blue-700 font-bold">OK</span>
|
||||
@else
|
||||
-
|
||||
@endif
|
||||
</td>
|
||||
|
||||
@elseif($col->column_type === 'measurement')
|
||||
{{-- measurement: 수치 --}}
|
||||
@php
|
||||
$fieldKey = "s{$section->id}_r{$rowIndex}_c{$col->id}";
|
||||
$savedVal = $document->data->where('field_key', $fieldKey)->first()?->field_value ?? '';
|
||||
@endphp
|
||||
<td class="doc-td font-mono text-center">
|
||||
{{ $savedVal ?: '-' }}
|
||||
</td>
|
||||
|
||||
@else
|
||||
{{-- text: 정적 데이터 또는 입력 텍스트 --}}
|
||||
@php
|
||||
$staticValue = match(true) {
|
||||
str_contains(strtolower($col->label), 'no') && strlen($col->label) <= 4 => $rowIndex + 1,
|
||||
in_array($col->label, ['검사항목', '항목']) => $item->item,
|
||||
in_array($col->label, ['검사기준', '기준']) => $item->standard,
|
||||
in_array($col->label, ['검사방식', '방식', '검사방법']) => $item->method,
|
||||
in_array($col->label, ['검사주기', '주기']) => $item->frequency,
|
||||
in_array($col->label, ['규격', '적용규격', '관련규정']) => $item->regulation,
|
||||
in_array($col->label, ['분류', '카테고리']) => $item->category,
|
||||
default => null,
|
||||
};
|
||||
@endphp
|
||||
@if($staticValue !== null)
|
||||
<td class="doc-td {{ is_numeric($staticValue) ? 'text-center' : '' }}">
|
||||
{{ $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="doc-td text-center">
|
||||
{{ $savedVal ?: '-' }}
|
||||
</td>
|
||||
@endif
|
||||
@endif
|
||||
@endforeach
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
@endif
|
||||
|
||||
@endforeach
|
||||
@endif
|
||||
|
||||
{{-- 종합판정 --}}
|
||||
@if($template->footer_judgement_label)
|
||||
<div class="mt-6 flex justify-end">
|
||||
<table class="border-collapse text-sm" style="border: 1px solid #333; min-width: 200px;">
|
||||
<tr>
|
||||
<td class="doc-th font-semibold text-center" style="width: 100px;">
|
||||
{{ $template->footer_judgement_label ?? '종합판정' }}
|
||||
</td>
|
||||
<td class="doc-td text-center" style="min-width: 100px;">
|
||||
@php
|
||||
$judgementVal = $document->data->where('field_key', 'footer_judgement')->first()?->field_value ?? '';
|
||||
@endphp
|
||||
@if($judgementVal)
|
||||
<span class="{{ in_array($judgementVal, ['적합', '합격']) ? 'text-blue-700' : 'text-red-600' }} font-semibold">
|
||||
{{ $judgementVal }}
|
||||
</span>
|
||||
@else
|
||||
<span class="text-gray-400">미완료</span>
|
||||
@endif
|
||||
</td>
|
||||
</tr>
|
||||
@php
|
||||
$remarkVal = $document->data->where('field_key', 'footer_remark')->first()?->field_value ?? '';
|
||||
@endphp
|
||||
@if($remarkVal)
|
||||
<tr>
|
||||
<td class="doc-th font-semibold text-center">
|
||||
{{ $template->footer_remark_label ?? '비고' }}
|
||||
</td>
|
||||
<td class="doc-td">{{ $remarkVal }}</td>
|
||||
</tr>
|
||||
@endif
|
||||
</table>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* 성적서 테이블 공통 스타일 */
|
||||
.doc-th {
|
||||
border: 1px solid #555;
|
||||
padding: 6px 8px;
|
||||
background-color: #f9fafb;
|
||||
font-weight: 500;
|
||||
font-size: 0.8125rem;
|
||||
text-align: center;
|
||||
color: #374151;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.doc-td {
|
||||
border: 1px solid #999;
|
||||
padding: 5px 8px;
|
||||
font-size: 0.8125rem;
|
||||
color: #1f2937;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/* 인쇄 스타일 */
|
||||
@media print {
|
||||
body { background: white; }
|
||||
.doc-th { background-color: #f3f4f6 !important; -webkit-print-color-adjust: exact; }
|
||||
.doc-td { border-color: #666 !important; }
|
||||
}
|
||||
</style>
|
||||
@endsection
|
||||
@@ -35,6 +35,13 @@ class="bg-red-600 hover:bg-red-700 text-white px-4 py-2 rounded-lg transition fl
|
||||
반려
|
||||
</button>
|
||||
@endif
|
||||
<a href="{{ route('documents.print', $document->id) }}"
|
||||
class="bg-indigo-600 hover:bg-indigo-700 text-white px-4 py-2 rounded-lg transition flex items-center gap-2">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 17h2a2 2 0 002-2v-4a2 2 0 00-2-2H5a2 2 0 00-2 2v4a2 2 0 002 2h2m2 4h6a2 2 0 002-2v-4a2 2 0 00-2-2H9a2 2 0 00-2 2v4a2 2 0 002 2zm8-12V5a2 2 0 00-2-2H9a2 2 0 00-2 2v4h10z"/>
|
||||
</svg>
|
||||
성적서
|
||||
</a>
|
||||
<a href="{{ route('documents.index') }}"
|
||||
class="bg-gray-200 hover:bg-gray-300 text-gray-700 px-4 py-2 rounded-lg transition">
|
||||
목록
|
||||
@@ -108,7 +115,7 @@ class="bg-gray-200 hover:bg-gray-300 text-gray-700 px-4 py-2 rounded-lg transiti
|
||||
<dl class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
@foreach($document->template->basicFields as $field)
|
||||
@php
|
||||
$fieldKey = \Illuminate\Support\Str::slug($field->label, '_');
|
||||
$fieldKey = 'bf_' . $field->id;
|
||||
$fieldData = $document->data->where('field_key', $fieldKey)->first();
|
||||
$value = $fieldData?->field_value ?? '-';
|
||||
@endphp
|
||||
|
||||
@@ -350,6 +350,7 @@
|
||||
Route::get('/', [DocumentController::class, 'index'])->name('index');
|
||||
Route::get('/create', [DocumentController::class, 'create'])->name('create');
|
||||
Route::get('/{id}', [DocumentController::class, 'show'])->whereNumber('id')->name('show');
|
||||
Route::get('/{id}/print', [DocumentController::class, 'print'])->whereNumber('id')->name('print');
|
||||
Route::get('/{id}/edit', [DocumentController::class, 'edit'])->whereNumber('id')->name('edit');
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user