Files
sam-manage/resources/views/document-templates/edit.blade.php
권혁성 6d708cfdda feat: 수입검사 성적서 템플릿 시더 및 미리보기 구현
- InspectionTemplateSeeder: 검사항목 4개(겉모양, 두께, 폭, 길이) 생성
- 템플릿 미리보기를 React 성적서 양식과 동일한 형태로 구현
  - 헤더: 로고, 제목, 결재란
  - 기본정보 테이블 (목업 데이터)
  - 검사항목 테이블: NO, 검사항목, 검사기준, 검사방식, 검사주기, 측정값(n1,n2,n3), 판정
  - 종합판정 영역
- 문서 목록/상세/편집 뷰 개선

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-29 01:06:53 +09:00

848 lines
44 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

@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">
검사 성적서, 작업지시서 등의 문서 양식을 설정합니다.
</p>
</div>
<div class="flex items-center gap-2">
<button type="button" onclick="openPreviewModal()" class="bg-gray-600 hover:bg-gray-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="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
</svg>
미리보기
</button>
<a href="{{ route('document-templates.index') }}" class="bg-gray-200 hover:bg-gray-300 text-gray-700 px-4 py-2 rounded-lg transition">
목록
</a>
<button type="button" onclick="saveTemplate()" class="bg-blue-600 hover:bg-blue-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="M5 13l4 4L19 7" />
</svg>
저장
</button>
</div>
</div>
<!-- 네비게이션 -->
<div class="mb-6 border-b border-gray-200 bg-white rounded-t-lg">
<nav class="-mb-px flex">
<button onclick="switchTab('basic')" id="tab-basic"
class="tab-btn px-6 py-4 text-sm font-medium border-b-2 border-blue-500 text-blue-600">
기본정보
</button>
<button onclick="switchTab('approval')" id="tab-approval"
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">
검사 기준서
</button>
<button onclick="switchTab('columns')" id="tab-columns"
class="tab-btn px-6 py-4 text-sm font-medium border-b-2 border-transparent text-gray-500 hover:text-gray-700">
테이블 컬럼
</button>
</nav>
</div>
<!-- 기본정보 -->
<div id="content-basic" class="tab-content bg-white rounded-lg shadow-sm p-6">
<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">양식명 <span class="text-red-500">*</span></label>
<input type="text" id="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>
<label class="block text-sm font-medium text-gray-700 mb-1">분류</label>
<input type="text" id="category" list="category-list" placeholder="분류를 입력하거나 선택하세요"
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
<datalist id="category-list">
@foreach($categories as $category)
<option value="{{ $category }}">
@endforeach
</datalist>
</div>
<!-- 문서 제목 -->
<div class="md:col-span-2">
<label class="block text-sm font-medium text-gray-700 mb-1">문서 제목</label>
<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 class="flex items-center gap-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>
</div>
<!-- 결재라인 -->
<div id="content-approval" class="tab-content bg-white rounded-lg shadow-sm p-6 hidden">
<div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-medium text-gray-800">결재라인 설정</h3>
<button type="button" onclick="addApprovalLine()" 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="approval-lines" class="space-y-3">
<!-- 동적으로 추가됨 -->
</div>
<p id="approval-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">
<h3 class="text-lg font-medium text-gray-800">검사 기준서 섹션</h3>
<button type="button" onclick="addSection()" 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="sections-container" class="space-y-4">
<!-- 동적으로 추가됨 -->
</div>
<p id="sections-empty" class="text-gray-400 text-center py-8 hidden">검사 기준서 섹션이 없습니다. 섹션 추가 버튼을 클릭하세요.</p>
</div>
<!-- 테이블 컬럼 -->
<div id="content-columns" class="tab-content bg-white rounded-lg shadow-sm p-6 hidden">
<div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-medium text-gray-800">검사 데이터 테이블 컬럼</h3>
<button type="button" onclick="addColumn()" 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="columns-container" class="space-y-3">
<!-- 동적으로 추가됨 -->
</div>
<p id="columns-empty" class="text-gray-400 text-center py-8 hidden">테이블 컬럼이 없습니다. 컬럼 추가 버튼을 클릭하세요.</p>
</div>
</div>
<!-- 미리보기 모달 -->
<div id="preview-modal" class="fixed inset-0 z-50 hidden overflow-y-auto">
<div class="flex items-center justify-center min-h-screen p-4">
<div class="fixed inset-0 bg-black bg-opacity-50" onclick="closePreviewModal()"></div>
<div class="relative bg-white rounded-lg shadow-xl max-w-5xl w-full max-h-[90vh] overflow-hidden">
<div class="flex justify-between items-center p-4 border-b">
<h3 class="text-lg font-bold text-gray-800">문서 미리보기</h3>
<button onclick="closePreviewModal()" class="text-gray-400 hover:text-gray-600">
<svg class="w-6 h-6" 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>
<div id="preview-content" class="p-4 overflow-auto max-h-[calc(90vh-120px)]">
<!-- 미리보기 내용 -->
</div>
</div>
</div>
</div>
@endsection
@push('scripts')
<script>
// ===== 상태 관리 =====
const templateState = {
id: {{ $template->id ?? 'null' }},
name: '',
category: '',
title: '',
footer_remark_label: '비고',
footer_judgement_label: '종합판정',
footer_judgement_options: ['합격', '불합격', '조건부합격'],
is_active: true,
approval_lines: [],
sections: [],
columns: []
};
// 고유 ID 생성
let uniqueId = 0;
function generateId() {
return `temp_${++uniqueId}`;
}
// ===== 초기화 =====
document.addEventListener('DOMContentLoaded', function() {
@if($template && isset($templateData))
// 기존 데이터 로드 (컨트롤러에서 준비된 데이터 사용)
const loadedData = @json($templateData);
templateState.name = loadedData.name || '';
templateState.category = loadedData.category || '';
templateState.title = loadedData.title || '';
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.sections = loadedData.sections || [];
templateState.columns = loadedData.columns || [];
@endif
// UI 초기화
initBasicFields();
renderApprovalLines();
renderSections();
renderColumns();
});
// ===== 기본정보 =====
function initBasicFields() {
document.getElementById('name').value = templateState.name || '';
document.getElementById('category').value = templateState.category || '';
document.getElementById('title').value = templateState.title || '';
document.getElementById('is_active').checked = templateState.is_active;
// 변경 이벤트 바인딩
['name', 'category', 'title'].forEach(field => {
document.getElementById(field).addEventListener('input', function() {
templateState[field] = this.value;
});
});
document.getElementById('is_active').addEventListener('change', function() {
templateState.is_active = this.checked;
});
}
// ===== 탭 전환 =====
function switchTab(tabId) {
// 모든 탭 버튼 비활성화
document.querySelectorAll('.tab-btn').forEach(btn => {
btn.classList.remove('border-blue-500', 'text-blue-600');
btn.classList.add('border-transparent', 'text-gray-500');
});
// 선택된 탭 버튼 활성화
const activeBtn = document.getElementById(`tab-${tabId}`);
activeBtn.classList.add('border-blue-500', 'text-blue-600');
activeBtn.classList.remove('border-transparent', 'text-gray-500');
// 모든 컨텐츠 숨김
document.querySelectorAll('.tab-content').forEach(content => {
content.classList.add('hidden');
});
// 선택된 컨텐츠 표시
document.getElementById(`content-${tabId}`).classList.remove('hidden');
}
// ===== 결재라인 =====
function addApprovalLine() {
const line = { id: generateId(), name: '', dept: '', role: '' };
templateState.approval_lines.push(line);
renderApprovalLines();
}
function removeApprovalLine(id) {
templateState.approval_lines = templateState.approval_lines.filter(l => l.id !== id);
renderApprovalLines();
}
function updateApprovalLine(id, field, value) {
const line = templateState.approval_lines.find(l => l.id === id);
if (line) line[field] = value;
}
function renderApprovalLines() {
const container = document.getElementById('approval-lines');
const emptyMsg = document.getElementById('approval-empty');
if (templateState.approval_lines.length === 0) {
container.innerHTML = '';
emptyMsg.classList.remove('hidden');
return;
}
emptyMsg.classList.add('hidden');
container.innerHTML = templateState.approval_lines.map((line, idx) => `
<div class="flex items-center gap-3 p-3 bg-gray-50 rounded-lg cursor-move" data-id="${line.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(line.name)}" placeholder="단계명 (작성, 검토, 승인)"
onchange="updateApprovalLine('${line.id}', 'name', this.value)"
class="flex-1 px-3 py-2 border border-gray-300 rounded-lg text-sm">
<input type="text" value="${escapeHtml(line.dept)}" placeholder="부서"
onchange="updateApprovalLine('${line.id}', 'dept', this.value)"
class="w-32 px-3 py-2 border border-gray-300 rounded-lg text-sm">
<input type="text" value="${escapeHtml(line.role)}" placeholder="직책/담당자"
onchange="updateApprovalLine('${line.id}', 'role', this.value)"
class="w-32 px-3 py-2 border border-gray-300 rounded-lg text-sm">
<button onclick="removeApprovalLine('${line.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 addSection() {
const section = { id: generateId(), title: '', image_path: null, items: [] };
templateState.sections.push(section);
renderSections();
}
function removeSection(id) {
templateState.sections = templateState.sections.filter(s => s.id !== id);
renderSections();
}
function updateSection(id, field, value) {
const section = templateState.sections.find(s => s.id === id);
if (section) section[field] = value;
}
function addSectionItem(sectionId) {
const section = templateState.sections.find(s => s.id === sectionId);
if (section) {
section.items.push({
id: generateId(),
category: '',
item: '',
standard: '',
method: '',
frequency: '',
regulation: ''
});
renderSections();
}
}
function removeSectionItem(sectionId, itemId) {
const section = templateState.sections.find(s => s.id === sectionId);
if (section) {
section.items = section.items.filter(i => i.id !== itemId);
renderSections();
}
}
function updateSectionItem(sectionId, itemId, field, value) {
const section = templateState.sections.find(s => s.id === sectionId);
if (section) {
const item = section.items.find(i => i.id === itemId);
if (item) item[field] = value;
}
}
function renderSections() {
const container = document.getElementById('sections-container');
const emptyMsg = document.getElementById('sections-empty');
if (templateState.sections.length === 0) {
container.innerHTML = '';
emptyMsg.classList.remove('hidden');
return;
}
emptyMsg.classList.add('hidden');
container.innerHTML = templateState.sections.map((section, idx) => `
<div class="border border-gray-200 rounded-lg overflow-hidden" data-section-id="${section.id}">
<div class="bg-gray-50 p-4 flex items-center justify-between">
<div class="flex items-center gap-3 flex-1">
<span class="text-gray-400 font-bold cursor-move drag-handle" title="드래그하여 순서 변경">⋮⋮</span>
<input type="text" value="${escapeHtml(section.title)}" placeholder="섹션 제목 (예: 가이드레일)"
onchange="updateSection('${section.id}', 'title', this.value)"
class="flex-1 px-3 py-2 border border-gray-300 rounded-lg text-sm font-medium">
</div>
<div class="flex items-center gap-2">
<label class="text-gray-600 hover:text-blue-600 text-sm flex items-center gap-1 cursor-pointer">
<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="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
이미지
<input type="file" accept="image/*" class="hidden" onchange="uploadSectionImage('${section.id}', this)">
</label>
<button onclick="addSectionItem('${section.id}')" class="text-blue-600 hover:text-blue-800 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>
<button onclick="removeSection('${section.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>
</div>
${section.image_path ? `
<div class="px-4 py-2 bg-blue-50 flex items-center justify-between">
<div class="flex items-center gap-2">
<img src="/storage/${section.image_path}" alt="섹션 이미지" class="h-16 rounded border border-gray-200">
<span class="text-xs text-gray-500">이미지 첨부됨</span>
</div>
<button onclick="removeSectionImage('${section.id}')" class="text-red-500 hover:text-red-700 text-xs">
이미지 삭제
</button>
</div>
` : ''}
<div class="p-4">
${section.items.length > 0 ? `
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead class="bg-gray-100">
<tr>
<th class="px-2 py-2 text-left text-xs font-medium text-gray-500 w-24">구분</th>
<th class="px-2 py-2 text-left text-xs font-medium text-gray-500">검사항목</th>
<th class="px-2 py-2 text-left text-xs font-medium text-gray-500">검사기준</th>
<th class="px-2 py-2 text-left text-xs font-medium text-gray-500 w-24">검사방법</th>
<th class="px-2 py-2 text-left text-xs font-medium text-gray-500 w-24">검사주기</th>
<th class="px-2 py-2 text-left text-xs font-medium text-gray-500 w-28">관련규정</th>
<th class="px-2 py-2 w-10"></th>
</tr>
</thead>
<tbody>
${section.items.map(item => `
<tr class="border-t" data-item-id="${item.id}">
<td class="px-1 py-1">
<input type="text" value="${escapeHtml(item.category)}" placeholder="구분"
onchange="updateSectionItem('${section.id}', '${item.id}', 'category', this.value)"
class="w-full px-2 py-1 border border-gray-200 rounded text-xs">
</td>
<td class="px-1 py-1">
<input type="text" value="${escapeHtml(item.item)}" placeholder="검사항목"
onchange="updateSectionItem('${section.id}', '${item.id}', 'item', this.value)"
class="w-full px-2 py-1 border border-gray-200 rounded text-xs">
</td>
<td class="px-1 py-1">
<input type="text" value="${escapeHtml(item.standard)}" placeholder="검사기준"
onchange="updateSectionItem('${section.id}', '${item.id}', 'standard', this.value)"
class="w-full px-2 py-1 border border-gray-200 rounded text-xs">
</td>
<td class="px-1 py-1">
<input type="text" value="${escapeHtml(item.method)}" placeholder="방법"
onchange="updateSectionItem('${section.id}', '${item.id}', 'method', this.value)"
class="w-full px-2 py-1 border border-gray-200 rounded text-xs">
</td>
<td class="px-1 py-1">
<input type="text" value="${escapeHtml(item.frequency)}" placeholder="주기"
onchange="updateSectionItem('${section.id}', '${item.id}', 'frequency', this.value)"
class="w-full px-2 py-1 border border-gray-200 rounded text-xs">
</td>
<td class="px-1 py-1">
<input type="text" value="${escapeHtml(item.regulation)}" placeholder="규정"
onchange="updateSectionItem('${section.id}', '${item.id}', 'regulation', this.value)"
class="w-full px-2 py-1 border border-gray-200 rounded text-xs">
</td>
<td class="px-1 py-1">
<button onclick="removeSectionItem('${section.id}', '${item.id}')" 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>
</td>
</tr>
`).join('')}
</tbody>
</table>
</div>
` : `
<p class="text-gray-400 text-center py-4 text-sm">항목이 없습니다. 항목 추가 버튼을 클릭하세요.</p>
`}
</div>
</div>
`).join('');
}
// ===== 컬럼 관리 =====
function addColumn() {
const column = { id: generateId(), label: '', width: '100px', column_type: 'text', group_name: '', sub_labels: null };
templateState.columns.push(column);
renderColumns();
}
function removeColumn(id) {
templateState.columns = templateState.columns.filter(c => c.id !== id);
renderColumns();
}
function updateColumn(id, field, value) {
const column = templateState.columns.find(c => c.id === id);
if (column) column[field] = value;
}
function renderColumns() {
const container = document.getElementById('columns-container');
const emptyMsg = document.getElementById('columns-empty');
if (templateState.columns.length === 0) {
container.innerHTML = '';
emptyMsg.classList.remove('hidden');
return;
}
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>
`).join('');
}
// ===== 저장 =====
function saveTemplate() {
const name = document.getElementById('name').value.trim();
if (!name) {
showToast('양식명은 필수입니다.', 'warning');
switchTab('basic');
document.getElementById('name').focus();
return;
}
const data = {
name: name,
category: document.getElementById('category').value,
title: document.getElementById('title').value,
is_active: document.getElementById('is_active').checked,
approval_lines: templateState.approval_lines,
sections: templateState.sections,
columns: templateState.columns
};
const url = templateState.id
? `/api/admin/document-templates/${templateState.id}`
: '/api/admin/document-templates';
const method = templateState.id ? 'PUT' : 'POST';
fetch(url, {
method: method,
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': '{{ csrf_token() }}',
'Accept': 'application/json'
},
body: JSON.stringify(data)
})
.then(response => response.json())
.then(result => {
if (result.success) {
showToast(result.message || '저장되었습니다.', 'success');
if (!templateState.id && result.data?.id) {
// 새로 생성된 경우 편집 페이지로 이동
window.location.href = `/document-templates/${result.data.id}/edit`;
}
} else {
showToast(result.message || '저장에 실패했습니다.', 'error');
}
})
.catch(error => {
console.error('Save error:', error);
showToast('저장 중 오류가 발생했습니다.', 'error');
});
}
// ===== 미리보기 =====
function openPreviewModal() {
const content = document.getElementById('preview-content');
content.innerHTML = generatePreviewHtml();
document.getElementById('preview-modal').classList.remove('hidden');
}
function closePreviewModal() {
document.getElementById('preview-modal').classList.add('hidden');
}
function generatePreviewHtml() {
const title = document.getElementById('title').value || '수입검사 성적서';
// 검사항목 행 생성
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>`;
}
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('');
};
// 실제 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>
<!-- 제목 -->
<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>
<!-- 기본 정보 테이블 -->
<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>
<!-- 검사 항목 테이블 -->
<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>
<!-- 종합판정 -->
<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>
</table>
</div>
</div>
`;
}
// ===== 이미지 업로드 =====
function uploadSectionImage(sectionId, input) {
const file = input.files[0];
if (!file) return;
const formData = new FormData();
formData.append('image', file);
showToast('이미지 업로드 중...', 'info');
fetch('/api/admin/document-templates/upload-image', {
method: 'POST',
headers: {
'X-CSRF-TOKEN': '{{ csrf_token() }}',
'Accept': 'application/json'
},
body: formData
})
.then(response => response.json())
.then(result => {
if (result.success) {
const section = templateState.sections.find(s => s.id === sectionId);
if (section) {
section.image_path = result.path;
renderSections();
showToast('이미지가 업로드되었습니다.', 'success');
}
} else {
showToast(result.message || '이미지 업로드에 실패했습니다.', 'error');
}
})
.catch(error => {
console.error('Upload error:', error);
showToast('이미지 업로드 중 오류가 발생했습니다.', 'error');
});
input.value = '';
}
function removeSectionImage(sectionId) {
const section = templateState.sections.find(s => s.id === sectionId);
if (section) {
section.image_path = null;
renderSections();
}
}
// ===== SortableJS 초기화 =====
function initSortable() {
// 섹션 정렬
const sectionsContainer = document.getElementById('sections-container');
if (sectionsContainer && typeof Sortable !== 'undefined') {
new Sortable(sectionsContainer, {
animation: 150,
handle: '.drag-handle',
onEnd: function(evt) {
const newOrder = [];
sectionsContainer.querySelectorAll('[data-section-id]').forEach((el, idx) => {
const sectionId = el.dataset.sectionId;
const section = templateState.sections.find(s => s.id === sectionId);
if (section) newOrder.push(section);
});
templateState.sections = newOrder;
}
});
}
// 결재라인 정렬
const approvalContainer = document.getElementById('approval-lines');
if (approvalContainer && typeof Sortable !== 'undefined') {
new Sortable(approvalContainer, {
animation: 150,
onEnd: function(evt) {
const newOrder = [];
approvalContainer.querySelectorAll('[data-id]').forEach((el) => {
const id = el.dataset.id;
const line = templateState.approval_lines.find(l => l.id === id);
if (line) newOrder.push(line);
});
templateState.approval_lines = newOrder;
}
});
}
// 컬럼 정렬
const columnsContainer = document.getElementById('columns-container');
if (columnsContainer && typeof Sortable !== 'undefined') {
new Sortable(columnsContainer, {
animation: 150,
onEnd: function(evt) {
const newOrder = [];
columnsContainer.querySelectorAll('[data-column-id]').forEach((el) => {
const id = el.dataset.columnId;
const col = templateState.columns.find(c => c.id === id);
if (col) newOrder.push(col);
});
templateState.columns = newOrder;
}
});
}
}
// 렌더링 후 SortableJS 재초기화
const originalRenderApprovalLines = renderApprovalLines;
renderApprovalLines = function() {
originalRenderApprovalLines();
setTimeout(initSortable, 100);
};
const originalRenderSections = renderSections;
renderSections = function() {
originalRenderSections();
setTimeout(initSortable, 100);
};
const originalRenderColumns = renderColumns;
renderColumns = function() {
originalRenderColumns();
setTimeout(initSortable, 100);
};
// 초기 SortableJS 로드
document.addEventListener('DOMContentLoaded', function() {
setTimeout(initSortable, 500);
});
// ===== 유틸리티 =====
function escapeHtml(text) {
if (!text) return '';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
</script>
<!-- SortableJS CDN -->
<script src="https://cdn.jsdelivr.net/npm/sortablejs@1.15.0/Sortable.min.js"></script>
@endpush