Files
sam-manage/resources/views/approvals/edit.blade.php
김보곤 5f1a211722 feat: [approval] 견적서 양식 추가
- 견적서 전용 폼/조회 파셜 추가
- create/edit/show 페이지에 견적서 통합
- Alpine.js 동적 품목 테이블 (자동 세액 계산)
- 공급자 정보 테넌트에서 자동 로드
- 미리보기/인쇄 기능
2026-03-06 23:22:21 +09:00

1715 lines
88 KiB
PHP

@extends('layouts.app')
@section('title', '기안 수정')
@section('content')
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4 mb-6">
<div>
<h1 class="text-2xl font-bold text-gray-800">기안 수정</h1>
<p class="text-sm text-gray-500 mt-1">{{ $approval->document_number }}</p>
</div>
<div class="flex gap-2">
<a href="{{ route('approvals.show', $approval->id) }}" class="text-gray-600 hover:text-gray-800 text-sm px-3 py-2 border rounded-lg">
상세보기
</a>
<a href="{{ route('approvals.drafts') }}" class="text-gray-600 hover:text-gray-800 text-sm px-3 py-2 border rounded-lg">
기안함
</a>
</div>
</div>
@if($approval->status === 'rejected')
<div class="bg-red-50 border-l-4 border-red-400 p-4 mb-6 rounded" style="max-width: 960px; margin-left: auto; margin-right: auto;">
<div class="flex items-center">
<span class="text-red-700 font-medium">반려됨</span>
</div>
@php
$rejectedStep = $approval->steps->firstWhere('status', 'rejected');
@endphp
@if($rejectedStep)
<p class="text-sm text-red-600 mt-1">
{{ $rejectedStep->approver_name ?? '' }} ({{ $rejectedStep->acted_at?->format('Y-m-d H:i') }}):
{{ $rejectedStep->comment }}
</p>
@endif
</div>
@endif
{{-- 반려 이력 (재상신 문서인 경우) --}}
@if(!empty($approval->rejection_history))
<div class="bg-orange-50 border border-orange-200 p-4 mb-6 rounded-lg" style="max-width: 960px; margin-left: auto; margin-right: auto;">
<h4 class="text-sm font-semibold text-orange-800 mb-2 flex items-center gap-2">
이전 반려 이력
<span class="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-orange-200 text-orange-700">{{ count($approval->rejection_history) }}</span>
</h4>
<div class="space-y-2">
@foreach($approval->rejection_history as $history)
<div class="text-sm border-l-2 border-orange-300 pl-3 py-1">
<div class="flex items-center gap-2">
<span class="text-xs font-medium text-orange-600">{{ $history['round'] ?? '-' }}</span>
<span class="font-medium text-gray-800">{{ $history['approver_name'] ?? '' }}</span>
<span class="text-gray-400 text-xs">{{ $history['rejected_at'] ?? '' }}</span>
</div>
<p class="text-gray-600 mt-0.5">{{ $history['comment'] ?? '' }}</p>
</div>
@endforeach
</div>
</div>
@endif
<div class="mx-auto" style="max-width: 960px;">
<div class="bg-white rounded-lg shadow-sm p-6">
<h2 class="text-lg font-semibold text-gray-800 mb-4">문서 내용</h2>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">양식 <span class="text-red-500">*</span></label>
<div class="flex gap-3" style="align-items: flex-start;">
<div style="width: 30%; min-width: 180px;" class="shrink-0">
<select id="form_category" class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 font-medium">
</select>
<select id="form_id" class="mt-2 w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500">
@foreach($forms as $form)
<option value="{{ $form->id }}" {{ $approval->form_id == $form->id ? 'selected' : '' }}>{{ $form->name }}</option>
@endforeach
</select>
</div>
<div id="form-description-card" style="flex: 1; display: none;">
<div class="rounded-lg border p-3 text-sm transition-all" id="form-desc-inner">
<div class="flex items-start gap-2">
<div id="form-desc-icon" class="shrink-0 mt-0.5"></div>
<div>
<div id="form-desc-title" class="font-semibold text-sm mb-1"></div>
<div id="form-desc-text" class="text-xs leading-relaxed"></div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">제목 <span class="text-red-500">*</span></label>
<input type="text" id="title" maxlength="200" value="{{ $approval->title }}"
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
{{-- 결재선 --}}
<div class="mb-4">
<div class="flex items-center justify-between mb-1">
<label class="block text-sm font-medium text-gray-700">결재선</label>
<div class="flex items-center gap-2">
<select id="quick-line-select" onchange="applyQuickLine(this.value)"
class="px-2 py-1.5 border border-gray-300 rounded-lg text-xs focus:outline-none focus:ring-2 focus:ring-blue-500"
style="min-width: 160px;">
<option value="">결재선 선택</option>
@foreach($lines as $line)
<option value="{{ $line->id }}" {{ ($approval->line_id ?? '') == $line->id ? 'selected' : '' }}>
{{ $line->name }} ({{ count($line->steps ?? []) }}단계)
</option>
@endforeach
</select>
<button type="button" onclick="openApprovalLineModal()"
class="px-3 py-1.5 bg-blue-50 text-blue-600 hover:bg-blue-100 rounded-lg text-xs font-medium transition whitespace-nowrap">
세부 설정
</button>
</div>
</div>
<div id="approval-line-summary" class="p-3 bg-gray-50 rounded-lg border border-gray-200 flex items-center">
<span class="text-sm text-gray-400">결재선이 설정되지 않았습니다.</span>
</div>
</div>
<div class="mb-4">
<label class="inline-flex items-center gap-2 cursor-pointer">
<input type="checkbox" id="is_urgent" {{ $approval->is_urgent ? 'checked' : '' }}
class="rounded border-gray-300 text-red-600 focus:ring-red-500">
<span class="text-sm text-gray-700">긴급</span>
</label>
</div>
{{-- 본문 (일반 양식) --}}
<div id="body-area" class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">
본문
<label class="inline-flex items-center gap-1 ml-3 cursor-pointer">
<input type="checkbox" id="useEditor" onchange="toggleEditor()"
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500">
<span class="text-xs text-gray-500 font-normal">편집기</span>
</label>
</label>
<textarea id="body" rows="12"
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
style="min-height: 300px;">{{ $approval->body }}</textarea>
<div id="quill-container" style="display: none; min-height: 300px;"></div>
</div>
{{-- 재직증명서 전용 --}}
@include('approvals.partials._certificate-form', [
'employees' => $employees ?? collect(),
])
{{-- 사용인감계 전용 --}}
@include('approvals.partials._seal-usage-form', [
'tenantInfo' => $tenantInfo ?? [],
])
{{-- 위임장 전용 --}}
@include('approvals.partials._delegation-form', [
'tenantInfo' => $tenantInfo ?? [],
])
{{-- 이사회의사록 전용 --}}
@include('approvals.partials._board-minutes-form', [
'tenantInfo' => $tenantInfo ?? [],
])
{{-- 견적서 전용 --}}
@include('approvals.partials._quotation-form', [
'tenantInfo' => $tenantInfo ?? [],
])
{{-- 지출결의서 전용 --}}
@php
$existingFiles = [];
if (!empty($approval->attachments)) {
$fileIds = collect($approval->attachments)->pluck('id')->filter();
if ($fileIds->isNotEmpty()) {
$existingFiles = \App\Models\Boards\File::whereIn('id', $fileIds)
->get()
->map(fn($f) => [
'id' => $f->id,
'name' => $f->original_name,
'size' => $f->file_size,
'mime_type' => $f->mime_type,
])->toArray();
}
}
@endphp
@include('approvals.partials._expense-form', [
'initialData' => $approval->content ?? [],
'initialFiles' => $existingFiles,
'cards' => $cards ?? collect(),
'accounts' => $accounts ?? collect(),
])
{{-- 품의서 전용 --}}
@include('approvals.partials._purchase-request-form', [
'initialData' => $approval->content ?? [],
'initialFiles' => $existingFiles,
])
{{-- 액션 버튼 --}}
<div class="border-t pt-4 mt-4 flex gap-2 justify-end">
<button onclick="updateApproval('save')"
class="bg-gray-600 hover:bg-gray-700 text-white px-6 py-2 rounded-lg transition text-sm font-medium">
저장
</button>
<button onclick="updateApproval('submit')"
class="bg-blue-600 hover:bg-blue-700 text-white px-6 py-2 rounded-lg transition text-sm font-medium">
{{ $approval->status === 'rejected' ? '재상신' : '상신' }}
</button>
@if($approval->isDeletable())
<button onclick="deleteApproval()"
class="bg-red-100 hover:bg-red-200 text-red-700 px-6 py-2 rounded-lg transition text-sm font-medium">
삭제
</button>
@endif
</div>
</div>
</div>
{{-- 결재선 모달 --}}
<div id="approval-line-modal" style="display: none;" class="fixed inset-0 z-50">
<div class="absolute inset-0 bg-black/50" onclick="closeApprovalLineModal()"></div>
<div class="relative flex items-center justify-center min-h-full p-4">
<div class="bg-white rounded-xl shadow-2xl w-full overflow-hidden relative" style="max-width: 720px;">
<button type="button" onclick="closeApprovalLineModal()"
class="absolute top-3 right-3 z-10 p-1 text-gray-400 hover:text-gray-600 transition bg-white rounded-full shadow-sm">
<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="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
<div class="overflow-y-auto" style="max-height: 70vh;">
@php
$initialSteps = $approval->steps->map(fn($s) => [
'user_id' => $s->approver_id,
'user_name' => $s->approver_name ?? ($s->approver?->name ?? ''),
'department' => $s->approver_department ?? '',
'position' => $s->approver_position ?? '',
'step_type' => $s->step_type,
])->toArray();
@endphp
@include('approvals.partials._approval-line-editor', [
'lines' => $lines,
'initialSteps' => $initialSteps,
'selectedLineId' => $approval->line_id ?? '',
])
</div>
<div class="px-5 py-3 border-t border-gray-200 flex justify-end">
<button type="button" onclick="closeApprovalLineModal()"
class="px-6 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg text-sm font-medium transition">
확인
</button>
</div>
</div>
</div>
</div>
@endsection
@push('styles')
<link href="https://cdn.jsdelivr.net/npm/quill@2.0.3/dist/quill.snow.css" rel="stylesheet">
<style>
#quill-container .ql-editor { min-height: 260px; font-size: 0.875rem; }
#quill-container .ql-toolbar { border-radius: 0.5rem 0.5rem 0 0; border-color: #d1d5db; }
#quill-container .ql-container { border-radius: 0 0 0.5rem 0.5rem; border-color: #d1d5db; }
#summary-sortable .step-card {
cursor: grab;
position: relative;
margin-right: 24px;
user-select: none;
transition: transform 0.15s ease, box-shadow 0.15s ease;
}
#summary-sortable .step-card:last-child { margin-right: 0; }
#summary-sortable .step-card:not(:last-child)::after {
content: '→';
position: absolute;
right: -18px;
top: 50%;
transform: translateY(-50%);
color: #d1d5db;
font-size: 14px;
pointer-events: none;
}
#summary-sortable .step-card:hover {
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
transform: translateY(-1px);
}
#summary-sortable .step-card:active { cursor: grabbing; }
#summary-sortable .step-card.sortable-ghost { opacity: 0.3; }
#summary-sortable .step-card.sortable-chosen {
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
transform: translateY(-2px);
}
</style>
@endpush
@push('scripts')
<script src="https://cdn.jsdelivr.net/npm/quill@2.0.3/dist/quill.js"></script>
<script>
let quillInstance = null;
var summarySortableInstance = null;
const formBodyTemplates = @json($forms->pluck('body_template', 'id'));
const formCodes = @json($forms->pluck('code', 'id'));
const linesData = @json($lines);
let isExpenseForm = false;
let isPurchaseRequestForm = false;
let isCertForm = false;
let isSealUsageForm = false;
let isDelegationForm = false;
let isBoardMinutesForm = false;
let isQuotationForm = false;
const formDescriptions = {
BUSINESS_DRAFT: {
title: '업무기안서', icon: '📄',
color: 'border-slate-200 bg-slate-50', titleColor: 'text-slate-800', textColor: 'text-slate-600',
text: '업무 추진에 필요한 사항을 기안하여 결재를 받는 기본 문서입니다. 프로젝트 계획, 업무 협조 요청, 내부 제안 등 정형화된 양식이 없는 일반적인 업무 보고·요청에 사용합니다.',
},
leave: {
title: '휴가신청', icon: '🏖️',
color: 'border-sky-200 bg-sky-50', titleColor: 'text-sky-800', textColor: 'text-sky-600',
text: '연차, 반차, 병가, 경조사 등 휴가를 사전에 신청하는 문서입니다. 휴가 유형, 기간, 사유를 기재하며, 승인 후 근태에 자동 반영됩니다.',
},
attendance_request: {
title: '근태신청', icon: '🕐',
color: 'border-indigo-200 bg-indigo-50', titleColor: 'text-indigo-800', textColor: 'text-indigo-600',
text: '외근, 출장, 조퇴, 지각 등 정상 근무 외 근태 변경 사항을 신청하는 문서입니다. 근태 유형과 해당 일시를 기재하여 승인을 받습니다.',
},
reason_report: {
title: '사유서', icon: '✏️',
color: 'border-rose-200 bg-rose-50', titleColor: 'text-rose-800', textColor: 'text-rose-600',
text: '지각, 결근, 조퇴 등 근태 이상 사항에 대한 사유를 소명하는 문서입니다. 사유 발생일과 상세 사유를 기재하여 사후 보고합니다.',
},
expense: {
title: '지출결의서', icon: '💰',
color: 'border-amber-200 bg-amber-50', titleColor: 'text-amber-800', textColor: 'text-amber-700',
text: '이미 발생한 지출에 대해 사후 보고하는 문서입니다. 법인카드 사용 내역, 계좌이체 등 실제 지출이 완료된 건에 대해 증빙자료와 함께 결재를 요청합니다.',
},
employment_cert: {
title: '재직증명서', icon: '🏢',
color: 'border-cyan-200 bg-cyan-50', titleColor: 'text-cyan-800', textColor: 'text-cyan-600',
text: '현재 재직 중임을 증명하는 공식 문서입니다. 은행 제출, 관공서 제출, 비자 신청 등의 용도로 발급하며, 승인 후 PDF로 출력할 수 있습니다.',
},
career_cert: {
title: '경력증명서', icon: '📊',
color: 'border-violet-200 bg-violet-50', titleColor: 'text-violet-800', textColor: 'text-violet-600',
text: '재직 또는 퇴직 사원의 경력 사항을 증명하는 문서입니다. 근무 기간, 부서, 직위 등을 포함하며, 승인 후 PDF로 출력할 수 있습니다.',
},
appointment_cert: {
title: '위촉증명서', icon: '🤝',
color: 'border-emerald-200 bg-emerald-50', titleColor: 'text-emerald-800', textColor: 'text-emerald-600',
text: '위촉직(프리랜서, 자문위원 등)의 위촉 사실을 증명하는 문서입니다. 위촉 기간, 담당 업무 등을 포함하며, 승인 후 PDF로 출력할 수 있습니다.',
},
resignation: {
title: '사직서', icon: '📮',
color: 'border-gray-300 bg-gray-50', titleColor: 'text-gray-800', textColor: 'text-gray-600',
text: '퇴직 의사를 공식적으로 표명하는 문서입니다. 사직 사유와 희망 퇴직일을 기재하며, 결재 완료 후 퇴직 절차가 진행됩니다.',
},
seal_usage: {
title: '사용인감계', icon: '🔏',
color: 'border-rose-200 bg-rose-50', titleColor: 'text-rose-800', textColor: 'text-rose-600',
text: '법인인감, 사용인감 등 회사 인감의 사용을 신청하는 문서입니다. 인감 종류, 용도, 제출처를 기재하며, 승인 후 인감을 사용할 수 있습니다.',
},
delegation: {
title: '위임장', icon: '📜',
color: 'border-amber-200 bg-amber-50', titleColor: 'text-amber-800', textColor: 'text-amber-600',
text: '법인의 업무를 대리인에게 위임하는 문서입니다. 위임인(회사), 수임인(대리인), 위임사항, 위임기간을 기재하며, 승인 후 위임장으로 사용할 수 있습니다.',
},
board_minutes: {
title: '이사회의사록', icon: '📋',
color: 'border-slate-300 bg-slate-50', titleColor: 'text-slate-800', textColor: 'text-slate-600',
text: '이사회 개최 내용을 기록하는 공식 문서입니다. 일시, 장소, 출석현황, 의안, 의사경과, 기명날인 등을 기재하며, 법적 효력을 갖는 회의록입니다.',
},
quotation: {
title: '견적서', icon: '💰',
color: 'border-emerald-200 bg-emerald-50', titleColor: 'text-emerald-800', textColor: 'text-emerald-600',
text: '고객에게 제출할 견적서를 작성하는 문서입니다. 품목, 수량, 단가, 공급가액, 부가세를 기재하며, 승인 후 공식 견적서로 사용할 수 있습니다.',
},
pr_expense: {
title: '지출품의서', icon: '📋',
color: 'border-orange-200 bg-orange-50', titleColor: 'text-orange-800', textColor: 'text-orange-700',
text: '지출이 발생하기 전 사전 승인을 받는 문서입니다. 예산 범위 내에서 지출 항목과 금액을 기재하여 사전에 승락을 받습니다.',
},
pr_contract: {
title: '계약체결품의서', icon: '📝',
color: 'border-purple-200 bg-purple-50', titleColor: 'text-purple-800', textColor: 'text-purple-700',
text: '외부 업체와의 계약 체결 전 승인을 받는 문서입니다. 계약 상대방, 계약 내용, 기간, 금액, 주요 조건 등을 명시하여 계약 진행에 대한 사전 승락을 받습니다.',
},
pr_purchase: {
title: '구매품의서', icon: '🛒',
color: 'border-blue-200 bg-blue-50', titleColor: 'text-blue-800', textColor: 'text-blue-700',
text: '물품 구매 전 사전 승인을 받는 문서입니다. 구매할 품목, 수량, 단가, 납품업체 등을 기재하여 구매 진행에 대한 사전 승락을 받습니다.',
},
pr_trip: {
title: '출장품의서', icon: '✈️',
color: 'border-green-200 bg-green-50', titleColor: 'text-green-800', textColor: 'text-green-700',
text: '출장 전 계획 승인을 받는 문서입니다. 출장지, 기간, 업무 내용, 예상 경비(교통비·숙박비·식비 등)를 기재하여 출장 진행에 대한 사전 승락을 받습니다.',
},
pr_settlement: {
title: '비용정산품의서', icon: '🧾',
color: 'border-teal-200 bg-teal-50', titleColor: 'text-teal-800', textColor: 'text-teal-700',
text: '업무 수행 중 발생한 비용의 정산 승인을 받는 문서입니다. 사용일자별 항목과 금액을 기재하고, 법인카드 사용 또는 개인 선지출 여부를 명시합니다.',
},
};
// 2단계 분류 정의
const formCategoryMap = {
BUSINESS_DRAFT: '일반',
leave: '인사/근태', attendance_request: '인사/근태', resignation: '인사/근태', reason_report: '인사/근태', delegation: '인사/근태', board_minutes: '인사/근태',
employment_cert: '증명서', career_cert: '증명서', appointment_cert: '증명서', seal_usage: '증명서',
pr_expense: '품의', pr_contract: '품의', pr_purchase: '품의', pr_trip: '품의', pr_settlement: '품의',
expense: '재무', quotation: '재무',
};
const categoryIcons = {
'일반': '📄', '인사/근태': '👤', '증명서': '📜', '품의': '📋', '재무': '💰',
};
const categoryOrder = ['일반', '인사/근태', '증명서', '품의', '재무'];
const formNames = @json($forms->pluck('name', 'id'));
function buildCategoryOptions() {
const catSelect = document.getElementById('form_category');
const formSelect = document.getElementById('form_id');
const usedCategories = new Set();
Array.from(formSelect.options).forEach(opt => {
const code = formCodes[opt.value];
const cat = formCategoryMap[code];
if (cat) usedCategories.add(cat);
});
catSelect.innerHTML = '';
categoryOrder.forEach(cat => {
if (!usedCategories.has(cat)) return;
const opt = document.createElement('option');
opt.value = cat;
opt.textContent = (categoryIcons[cat] || '') + ' ' + cat;
catSelect.appendChild(opt);
});
}
function filterFormsByCategory(category) {
const formSelect = document.getElementById('form_id');
const currentVal = formSelect.value;
let firstMatch = null;
let hasCurrentInCategory = false;
Array.from(formSelect.options).forEach(opt => {
const code = formCodes[opt.value];
const cat = formCategoryMap[code] || '일반';
const show = cat === category;
opt.style.display = show ? '' : 'none';
opt.disabled = !show;
if (show && !firstMatch) firstMatch = opt.value;
if (show && opt.value === currentVal) hasCurrentInCategory = true;
});
if (!hasCurrentInCategory && firstMatch) {
formSelect.value = firstMatch;
}
formSelect.dispatchEvent(new Event('change'));
}
function selectCategoryByFormId(formId) {
const code = formCodes[formId];
const cat = formCategoryMap[code];
if (cat) {
document.getElementById('form_category').value = cat;
filterFormsByCategory(cat);
}
}
function updateFormDescription(formId) {
const code = formCodes[formId];
const desc = formDescriptions[code];
const card = document.getElementById('form-description-card');
if (!desc) { card.style.display = 'none'; return; }
const inner = document.getElementById('form-desc-inner');
inner.className = 'rounded-lg border p-3 text-sm transition-all ' + desc.color;
document.getElementById('form-desc-icon').innerHTML = '<span style="font-size: 1.25rem;">' + desc.icon + '</span>';
document.getElementById('form-desc-title').className = 'font-semibold text-sm mb-1 ' + desc.titleColor;
document.getElementById('form-desc-title').textContent = desc.title;
document.getElementById('form-desc-text').className = 'text-xs leading-relaxed ' + desc.textColor;
document.getElementById('form-desc-text').textContent = desc.text;
card.style.display = '';
}
function escapeHtml(str) {
if (!str) return '';
const div = document.createElement('div');
div.appendChild(document.createTextNode(str));
return div.innerHTML;
}
function toggleEditor() {
const useEditor = document.getElementById('useEditor').checked;
const textarea = document.getElementById('body');
const container = document.getElementById('quill-container');
if (useEditor) {
container.style.display = '';
textarea.style.display = 'none';
if (!quillInstance) {
quillInstance = new Quill('#quill-container', {
theme: 'snow',
modules: {
toolbar: [
[{ 'header': [1, 2, 3, false] }],
['bold', 'italic', 'underline', 'strike'],
[{ 'color': [] }, { 'background': [] }],
[{ 'list': 'ordered' }, { 'list': 'bullet' }],
[{ 'align': [] }],
['blockquote', 'code-block'],
['link'],
['clean'],
],
},
placeholder: '기안 내용을 입력하세요...',
});
}
const text = textarea.value.trim();
if (text) {
if (/<[a-z][\s\S]*>/i.test(text)) {
quillInstance.root.innerHTML = text;
} else {
quillInstance.setText(text);
}
}
} else {
container.style.display = 'none';
textarea.style.display = '';
if (quillInstance) {
const html = quillInstance.root.innerHTML;
textarea.value = (html === '<p><br></p>') ? '' : html;
}
}
}
function getBodyContent() {
const useEditor = document.getElementById('useEditor')?.checked;
if (useEditor && quillInstance) {
const html = quillInstance.root.innerHTML;
return (html === '<p><br></p>') ? '' : html;
}
return document.getElementById('body').value;
}
function openApprovalLineModal() {
document.getElementById('approval-line-modal').style.display = '';
document.body.style.overflow = 'hidden';
}
function closeApprovalLineModal() {
document.getElementById('approval-line-modal').style.display = 'none';
document.body.style.overflow = '';
updateApprovalLineSummary();
// 모달 내부 결재선 선택과 외부 드롭다운 동기화
const editorEl = document.getElementById('approval-line-editor');
if (editorEl && editorEl._x_dataStack) {
document.getElementById('quick-line-select').value = editorEl._x_dataStack[0].selectedLineId || '';
}
}
function updateApprovalLineSummary() {
const editorEl = document.getElementById('approval-line-editor');
if (!editorEl || !editorEl._x_dataStack) return;
const steps = editorEl._x_dataStack[0].steps;
const summaryEl = document.getElementById('approval-line-summary');
if (summarySortableInstance) { summarySortableInstance.destroy(); summarySortableInstance = null; }
if (!steps || steps.length === 0) {
summaryEl.className = 'p-3 bg-gray-50 rounded-lg border border-gray-200 flex items-center';
summaryEl.innerHTML = '<span class="text-sm text-gray-400">결재선이 설정되지 않았습니다.</span>';
return;
}
const typeCounters = { approval: 0, agreement: 0, reference: 0 };
const typeLabels = { approval: '결재', agreement: '합의', reference: '참조' };
const typeBg = { approval: 'bg-blue-50 border-blue-100', agreement: 'bg-green-50 border-green-100', reference: 'bg-gray-100 border-gray-200' };
const typeLabelColor = { approval: 'text-blue-600', agreement: 'text-green-600', reference: 'text-gray-500' };
const cards = [];
steps.forEach(function(s, i) {
typeCounters[s.step_type] = (typeCounters[s.step_type] || 0) + 1;
var count = typeCounters[s.step_type];
var label = typeLabels[s.step_type] || s.step_type;
var bg = typeBg[s.step_type] || typeBg.reference;
var labelColor = typeLabelColor[s.step_type] || typeLabelColor.reference;
var stepLabel = s.step_type === 'reference' ? label : count + '차 ' + label;
var position = s.position || '';
cards.push(
'<div class="step-card text-center px-3 py-2 rounded-lg border ' + bg + '" data-index="' + i + '" style="min-width: 72px;">' +
'<div class="text-xs font-medium ' + labelColor + '">' + escapeHtml(stepLabel) + '</div>' +
(position ? '<div class="text-xs text-gray-400 mt-0.5">' + escapeHtml(position) + '</div>' : '') +
'<div class="text-sm font-semibold text-gray-800 mt-0.5">' + escapeHtml(s.user_name) + '</div>' +
'</div>'
);
});
summaryEl.className = 'p-3 bg-gray-50 rounded-lg border border-gray-200';
summaryEl.innerHTML = '<div id="summary-sortable" class="flex flex-wrap items-center">' + cards.join('') + '</div>' +
(steps.length > 1 ? '<div class="text-xs text-gray-400 mt-2">드래그하여 순서를 변경할 수 있습니다</div>' : '');
initSummarySortable();
}
function initSummarySortable() {
if (summarySortableInstance) { summarySortableInstance.destroy(); summarySortableInstance = null; }
var el = document.getElementById('summary-sortable');
if (!el || typeof Sortable === 'undefined') return;
summarySortableInstance = Sortable.create(el, {
animation: 150,
ghostClass: 'sortable-ghost',
chosenClass: 'sortable-chosen',
onEnd: function(evt) {
if (evt.oldIndex === evt.newIndex) return;
var editorEl = document.getElementById('approval-line-editor');
if (!editorEl || !editorEl._x_dataStack) return;
var steps = editorEl._x_dataStack[0].steps;
var item = steps.splice(evt.oldIndex, 1)[0];
steps.splice(evt.newIndex, 0, item);
updateApprovalLineSummary();
}
});
}
function applyQuickLine(lineId) {
const editorEl = document.getElementById('approval-line-editor');
if (!editorEl || !editorEl._x_dataStack) return;
const alpineData = editorEl._x_dataStack[0];
if (!lineId) {
alpineData.selectedLineId = '';
alpineData.steps = [];
} else {
alpineData.selectedLineId = lineId;
alpineData.loadLine();
}
setTimeout(updateApprovalLineSummary, 100);
}
function switchFormMode(formId) {
const code = formCodes[formId];
const expenseContainer = document.getElementById('expense-form-container');
const purchaseRequestContainer = document.getElementById('purchase-request-form-container');
const certContainer = document.getElementById('cert-form-container');
const sealUsageContainer = document.getElementById('seal-usage-form-container');
const delegationContainer = document.getElementById('delegation-form-container');
const boardMinutesContainer = document.getElementById('board-minutes-form-container');
const quotationContainer = document.getElementById('quotation-form-container');
const bodyArea = document.getElementById('body-area');
expenseContainer.style.display = 'none';
purchaseRequestContainer.style.display = 'none';
certContainer.style.display = 'none';
sealUsageContainer.style.display = 'none';
delegationContainer.style.display = 'none';
boardMinutesContainer.style.display = 'none';
quotationContainer.style.display = 'none';
bodyArea.style.display = 'none';
isExpenseForm = false;
isPurchaseRequestForm = false;
isCertForm = false;
isSealUsageForm = false;
isDelegationForm = false;
isBoardMinutesForm = false;
isQuotationForm = false;
if (code === 'expense') {
isExpenseForm = true;
expenseContainer.style.display = '';
} else if (code && code.startsWith('pr_')) {
isPurchaseRequestForm = true;
purchaseRequestContainer.style.display = '';
setTimeout(() => {
const prData = purchaseRequestContainer._x_dataStack?.[0];
if (prData) prData.setPrType(code);
}, 50);
} else if (code === 'employment_cert') {
isCertForm = true;
certContainer.style.display = '';
const certUserId = document.getElementById('cert-user-id').value;
if (certUserId) loadCertInfo(certUserId);
} else if (code === 'seal_usage') {
isSealUsageForm = true;
sealUsageContainer.style.display = '';
} else if (code === 'delegation') {
isDelegationForm = true;
delegationContainer.style.display = '';
} else if (code === 'board_minutes') {
isBoardMinutesForm = true;
boardMinutesContainer.style.display = '';
} else if (code === 'quotation') {
isQuotationForm = true;
quotationContainer.style.display = '';
} else {
bodyArea.style.display = '';
}
}
function applyBodyTemplate(formId) {
// 먼저 폼 모드 전환
switchFormMode(formId);
// 전용 폼이면 제목만 자동 설정하고 body template 적용 건너뜀
if (isExpenseForm || isPurchaseRequestForm || isCertForm || isSealUsageForm || isDelegationForm || isBoardMinutesForm || isQuotationForm) {
const titleEl = document.getElementById('title');
if (!titleEl.value.trim()) {
const formSelect = document.getElementById('form_id');
titleEl.value = formSelect.options[formSelect.selectedIndex].text;
}
return;
}
const template = formBodyTemplates[formId];
if (!template) return;
// edit에서는 항상 confirm (기존 본문이 있을 가능성 높음)
const bodyContent = getBodyContent();
if (bodyContent.trim()) {
if (!confirm('현재 본문 내용을 양식 템플릿으로 교체하시겠습니까?')) return;
}
// 제목 자동 설정 (비어있을 때만)
const titleEl = document.getElementById('title');
if (!titleEl.value.trim()) {
const formSelect = document.getElementById('form_id');
titleEl.value = formSelect.options[formSelect.selectedIndex].text;
}
const useEditorEl = document.getElementById('useEditor');
if (!useEditorEl.checked) {
useEditorEl.checked = true;
toggleEditor();
}
if (quillInstance) {
quillInstance.root.innerHTML = template;
}
document.getElementById('body').value = template;
}
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
const modal = document.getElementById('approval-line-modal');
if (modal && modal.style.display !== 'none') {
closeApprovalLineModal();
}
}
});
// 기존 HTML body 자동 감지 → 편집기 자동 활성화 + 결재선 요약 초기화
document.addEventListener('DOMContentLoaded', function () {
// 2단계 분류 초기화
buildCategoryOptions();
const initialFormId = document.getElementById('form_id').value;
selectCategoryByFormId(initialFormId);
switchFormMode(initialFormId);
updateFormDescription(initialFormId);
// 분류 변경 → 양식 필터링
document.getElementById('form_category').addEventListener('change', function() {
filterFormsByCategory(this.value);
});
// 재직증명서 기존 데이터 복원
if (isCertForm) {
const certContent = @json($approval->content ?? []);
if (certContent.name) {
document.getElementById('cert-name').value = certContent.name || '';
document.getElementById('cert-resident').value = certContent.resident_number || '';
document.getElementById('cert-address').value = certContent.address || '';
document.getElementById('cert-company').value = certContent.company_name || '';
document.getElementById('cert-business-num').value = certContent.business_num || '';
document.getElementById('cert-department').value = certContent.department || '';
document.getElementById('cert-position').value = certContent.position || '';
document.getElementById('cert-hire-date').value = certContent.hire_date || '';
document.getElementById('cert-ceo-name').value = certContent.ceo_name || '';
document.getElementById('cert-company-address').value = certContent.company_address || '';
document.getElementById('cert-issue-date').value = certContent.issue_date || '{{ now()->format("Y-m-d") }}';
// 용도 복원
const purposeSelect = document.getElementById('cert-purpose-select');
const purpose = certContent.purpose || '';
const predefined = ['은행 제출용', '관공서 제출용', '비자 신청용', '대출 신청용'];
if (predefined.includes(purpose)) {
purposeSelect.value = purpose;
} else if (purpose) {
purposeSelect.value = '__custom__';
document.getElementById('cert-purpose-custom-wrap').style.display = '';
document.getElementById('cert-purpose-custom').value = purpose;
}
// 사원 셀렉트 복원
if (certContent.cert_user_id) {
document.getElementById('cert-user-id').value = certContent.cert_user_id;
}
}
}
// 사용인감계 기존 데이터 복원
if (isSealUsageForm) {
const suContent = @json($approval->content ?? []);
if (suContent.usage_date) {
document.getElementById('su-usage-date').value = suContent.usage_date || '';
document.getElementById('su-purpose').value = suContent.purpose || '';
document.getElementById('su-submit-to').value = suContent.submit_to || '';
document.getElementById('su-attachment-desc').value = suContent.attachment_desc || '';
}
}
// 위임장 기존 데이터 복원
if (isDelegationForm) {
const dlContent = @json($approval->content ?? []);
if (dlContent.agent_name) {
document.getElementById('dl-agent-name').value = dlContent.agent_name || '';
document.getElementById('dl-agent-birth').value = dlContent.agent_birth || '';
document.getElementById('dl-agent-address').value = dlContent.agent_address || '';
document.getElementById('dl-agent-phone').value = dlContent.agent_phone || '';
document.getElementById('dl-agent-department').value = dlContent.agent_department || '';
document.getElementById('dl-delegation-detail').value = dlContent.delegation_detail || '';
document.getElementById('dl-period-start').value = dlContent.period_start || '';
document.getElementById('dl-period-end').value = dlContent.period_end || '';
document.getElementById('dl-attachments-desc').value = dlContent.attachments_desc || '';
}
}
// 이사회의사록 기존 데이터 복원
if (isBoardMinutesForm) {
const bmContent = @json($approval->content ?? []);
if (bmContent.meeting_datetime) {
document.getElementById('bm-meeting-datetime').value = bmContent.meeting_datetime || '';
document.getElementById('bm-meeting-place').value = bmContent.meeting_place || '';
document.getElementById('bm-total-directors').value = bmContent.total_directors || 0;
document.getElementById('bm-present-directors').value = bmContent.present_directors || 0;
document.getElementById('bm-total-auditors').value = bmContent.total_auditors || 0;
document.getElementById('bm-present-auditors').value = bmContent.present_auditors || 0;
document.getElementById('bm-chairman-name').value = bmContent.chairman_name || '';
document.getElementById('bm-proceedings').value = bmContent.proceedings || '';
document.getElementById('bm-closing-time').value = bmContent.closing_time || '';
const bmAlpine = document.getElementById('board-minutes-form-container')._x_dataStack?.[0];
if (bmAlpine) {
if (bmContent.agendas && bmContent.agendas.length > 0) bmAlpine.agendas = bmContent.agendas;
if (bmContent.signers && bmContent.signers.length > 0) bmAlpine.signers = bmContent.signers;
}
}
}
// 견적서 기존 데이터 복원
if (isQuotationForm) {
const qtContent = @json($approval->content ?? []);
if (qtContent.client_name) {
document.getElementById('qt-client-name').value = qtContent.client_name || '';
document.getElementById('qt-quote-date').value = qtContent.quote_date || '';
document.getElementById('qt-remarks').value = qtContent.remarks || '';
if (qtContent.business_type) {
const btInput = document.getElementById('qt-business-type-input');
if (btInput) btInput.value = qtContent.business_type;
}
if (qtContent.business_item) {
const biInput = document.getElementById('qt-business-item-input');
if (biInput) biInput.value = qtContent.business_item;
}
if (qtContent.phone) {
const phInput = document.getElementById('qt-phone-input');
if (phInput) phInput.value = qtContent.phone;
}
if (qtContent.bank_account) {
const baInput = document.getElementById('qt-bank-input');
if (baInput) baInput.value = qtContent.bank_account;
}
const qtAlpine = document.getElementById('quotation-form-container')._x_dataStack?.[0];
if (qtAlpine && qtContent.items && qtContent.items.length > 0) {
qtAlpine.items = qtContent.items.map(i => ({
name: i.name || '', spec: i.spec || '', qty: i.qty || 1,
unit_price: i.unit_price || 0, tax: i.tax || 0, note: i.note || '',
}));
}
}
}
// 전용 폼이 아닌 경우에만 Quill 편집기 자동 활성화
if (!isExpenseForm && !isPurchaseRequestForm && !isCertForm && !isSealUsageForm && !isDelegationForm && !isBoardMinutesForm && !isQuotationForm) {
const existingBody = document.getElementById('body').value;
if (/<[a-z][\s\S]*>/i.test(existingBody)) {
document.getElementById('useEditor').checked = true;
toggleEditor();
}
}
// Alpine 초기화 후 기존 결재선 요약 표시
setTimeout(updateApprovalLineSummary, 200);
// 양식 변경 시 본문 템플릿 자동 채움 + 설명 카드 업데이트
document.getElementById('form_id').addEventListener('change', function() {
applyBodyTemplate(this.value);
updateFormDescription(this.value);
});
});
async function updateApproval(action) {
const title = document.getElementById('title').value.trim();
if (!title) {
showToast('제목을 입력해주세요.', 'warning');
return;
}
const editorEl = document.getElementById('approval-line-editor');
const steps = editorEl._x_dataStack[0].getStepsData();
if (action === 'submit' && steps.filter(s => s.step_type !== 'reference').length === 0) {
showToast('결재자를 1명 이상 추가해주세요.', 'warning');
return;
}
let formContent = {};
let formBody = getBodyContent();
let attachmentFileIds = [];
if (isExpenseForm) {
const expenseEl = document.getElementById('expense-form-container');
if (expenseEl && expenseEl._x_dataStack) {
formContent = expenseEl._x_dataStack[0].getFormData();
attachmentFileIds = expenseEl._x_dataStack[0].getFileIds();
}
formBody = null;
} else if (isPurchaseRequestForm) {
const prEl = document.getElementById('purchase-request-form-container');
if (prEl && prEl._x_dataStack) {
formContent = prEl._x_dataStack[0].getFormData();
attachmentFileIds = prEl._x_dataStack[0].getFileIds();
}
formBody = null;
} else if (isCertForm) {
const purpose = getCertPurpose();
if (!purpose) {
showToast('사용용도를 입력해주세요.', 'warning');
return;
}
formContent = {
cert_user_id: document.getElementById('cert-user-id').value,
name: document.getElementById('cert-name').value,
resident_number: document.getElementById('cert-resident').value,
address: document.getElementById('cert-address').value,
department: document.getElementById('cert-department').value,
position: document.getElementById('cert-position').value,
hire_date: document.getElementById('cert-hire-date').value,
company_name: document.getElementById('cert-company').value,
business_num: document.getElementById('cert-business-num').value,
ceo_name: document.getElementById('cert-ceo-name').value,
company_address: document.getElementById('cert-company-address').value,
purpose: purpose,
issue_date: document.getElementById('cert-issue-date').value,
};
formBody = null;
} else if (isSealUsageForm) {
const suPurpose = document.getElementById('su-purpose').value.trim();
if (!suPurpose) {
showToast('용도를 입력해주세요.', 'warning');
return;
}
const suSubmitTo = document.getElementById('su-submit-to').value.trim();
if (!suSubmitTo) {
showToast('제출처를 입력해주세요.', 'warning');
return;
}
formContent = {
usage_date: document.getElementById('su-usage-date').value,
purpose: suPurpose,
submit_to: suSubmitTo,
attachment_desc: document.getElementById('su-attachment-desc').value.trim(),
company_name: document.getElementById('su-company-name').value,
business_num: document.getElementById('su-business-num').value,
ceo_name: document.getElementById('su-ceo-name').value,
company_address: document.getElementById('su-company-address').value,
};
formBody = null;
} else if (isBoardMinutesForm) {
const bmDatetime = document.getElementById('bm-meeting-datetime').value;
if (!bmDatetime) { showToast('일시를 입력해주세요.', 'warning'); return; }
const bmPlace = document.getElementById('bm-meeting-place').value.trim();
if (!bmPlace) { showToast('장소를 입력해주세요.', 'warning'); return; }
const bmChairman = document.getElementById('bm-chairman-name').value.trim();
if (!bmChairman) { showToast('의장(대표이사) 성명을 입력해주세요.', 'warning'); return; }
const bmContainer = document.getElementById('board-minutes-form-container');
const bmAlpine = bmContainer._x_dataStack?.[0];
const bmAgendas = bmAlpine ? bmAlpine.agendas : [];
const bmSigners = bmAlpine ? bmAlpine.signers : [];
formContent = {
meeting_datetime: bmDatetime,
meeting_place: bmPlace,
total_directors: parseInt(document.getElementById('bm-total-directors').value) || 0,
present_directors: parseInt(document.getElementById('bm-present-directors').value) || 0,
total_auditors: parseInt(document.getElementById('bm-total-auditors').value) || 0,
present_auditors: parseInt(document.getElementById('bm-present-auditors').value) || 0,
chairman_name: bmChairman,
agendas: bmAgendas.filter(a => a.title.trim()).map(a => ({ no: a.no, title: a.title.trim(), result: a.result.trim() })),
proceedings: document.getElementById('bm-proceedings').value.trim(),
closing_time: document.getElementById('bm-closing-time').value,
signers: bmSigners.filter(s => s.name.trim()).map(s => ({ role: s.role.trim(), name: s.name.trim() })),
company_name: document.getElementById('bm-company-name').value,
business_num: document.getElementById('bm-business-num').value,
ceo_name: document.getElementById('bm-ceo-name').value,
company_address: document.getElementById('bm-company-address').value,
meeting_date: bmDatetime.split('T')[0],
};
formBody = null;
} else if (isQuotationForm) {
const qtClientName = document.getElementById('qt-client-name').value.trim();
if (!qtClientName) { showToast('수신(고객명)을 입력해주세요.', 'warning'); return; }
const qtQuoteDate = document.getElementById('qt-quote-date').value;
if (!qtQuoteDate) { showToast('견적일자를 입력해주세요.', 'warning'); return; }
const qtContainer = document.getElementById('quotation-form-container');
const qtAlpine = qtContainer._x_dataStack?.[0];
const qtItems = qtAlpine ? qtAlpine.items : [];
if (qtItems.length === 0 || !qtItems[0].name.trim()) { showToast('품목을 1건 이상 입력해주세요.', 'warning'); return; }
const mappedItems = qtItems.filter(i => i.name.trim()).map(i => ({
name: i.name.trim(), spec: (i.spec || '').trim(), qty: parseInt(i.qty) || 0,
unit_price: parseInt(i.unit_price) || 0, supply_amount: (parseInt(i.qty) || 0) * (parseInt(i.unit_price) || 0),
tax: qtAlpine.itemTax(i), note: (i.note || '').trim(),
}));
formContent = {
client_name: qtClientName, quote_date: qtQuoteDate,
company_name: document.getElementById('qt-company-name').value,
business_num: document.getElementById('qt-business-num').value,
ceo_name: document.getElementById('qt-ceo-name').value,
company_address: document.getElementById('qt-company-address').value,
business_type: document.getElementById('qt-business-type-input')?.value || document.getElementById('qt-business-type').value,
business_item: document.getElementById('qt-business-item-input')?.value || document.getElementById('qt-business-item').value,
phone: document.getElementById('qt-phone-input')?.value || document.getElementById('qt-phone').value,
bank_account: document.getElementById('qt-bank-input')?.value || document.getElementById('qt-bank-account').value,
items: mappedItems, total_supply: qtAlpine ? qtAlpine.totalSupply() : 0,
total_tax: qtAlpine ? qtAlpine.totalTax() : 0, total_amount: qtAlpine ? qtAlpine.totalAmount() : 0,
remarks: document.getElementById('qt-remarks').value.trim(),
};
formBody = null;
} else if (isDelegationForm) {
const dlAgentName = document.getElementById('dl-agent-name').value.trim();
if (!dlAgentName) {
showToast('수임인(대리인) 성명을 입력해주세요.', 'warning');
return;
}
formContent = {
company_name: document.getElementById('dl-company-name').value,
business_num: document.getElementById('dl-business-num').value,
ceo_name: document.getElementById('dl-ceo-name').value,
company_address: document.getElementById('dl-company-address').value,
agent_name: dlAgentName,
agent_birth: document.getElementById('dl-agent-birth').value,
agent_address: document.getElementById('dl-agent-address').value.trim(),
agent_phone: document.getElementById('dl-agent-phone').value.trim(),
agent_department: document.getElementById('dl-agent-department').value.trim(),
delegation_detail: document.getElementById('dl-delegation-detail').value.trim(),
period_start: document.getElementById('dl-period-start').value,
period_end: document.getElementById('dl-period-end').value,
attachments_desc: document.getElementById('dl-attachments-desc').value.trim(),
};
formBody = null;
}
const payload = {
form_id: document.getElementById('form_id').value,
title: title,
body: formBody,
content: formContent,
attachment_file_ids: attachmentFileIds,
is_urgent: document.getElementById('is_urgent').checked,
steps: steps,
};
try {
const response = await fetch('/api/admin/approvals/{{ $approval->id }}', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': '{{ csrf_token() }}',
'Accept': 'application/json',
},
body: JSON.stringify(payload),
});
const data = await response.json();
if (!data.success) {
showToast(data.message || '저장에 실패했습니다.', 'error');
return;
}
if (action === 'submit') {
const submitResponse = await fetch('/api/admin/approvals/{{ $approval->id }}/submit', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': '{{ csrf_token() }}',
'Accept': 'application/json',
},
});
const submitData = await submitResponse.json();
if (submitData.success) {
showToast('결재가 상신되었습니다.', 'success');
setTimeout(() => location.href = '/approval-mgmt/drafts', 500);
} else {
showToast(submitData.message || '상신에 실패했습니다.', 'error');
}
} else {
showToast('저장되었습니다.', 'success');
}
} catch (e) {
showToast('서버 오류가 발생했습니다.', 'error');
}
}
async function deleteApproval() {
if (!confirm('이 문서를 삭제하시겠습니까?')) return;
try {
const response = await fetch('/api/admin/approvals/{{ $approval->id }}', {
method: 'DELETE',
headers: {
'X-CSRF-TOKEN': '{{ csrf_token() }}',
'Accept': 'application/json',
},
});
const data = await response.json();
if (data.success) {
showToast('삭제되었습니다.', 'success');
setTimeout(() => location.href = '/approval-mgmt/drafts', 500);
} else {
showToast(data.message || '삭제에 실패했습니다.', 'error');
}
} catch (e) {
showToast('서버 오류가 발생했습니다.', 'error');
}
}
// =========================================================================
// 재직증명서 관련 함수
// =========================================================================
async function loadCertInfo(userId) {
if (!userId) return;
try {
const resp = await fetch(`/api/admin/approvals/cert-info/${userId}`, {
headers: { 'Accept': 'application/json', 'X-CSRF-TOKEN': '{{ csrf_token() }}' }
});
const data = await resp.json();
if (data.success) {
const d = data.data;
document.getElementById('cert-name').value = d.name || '';
document.getElementById('cert-resident').value = d.resident_number || '';
document.getElementById('cert-address').value = d.address || '';
document.getElementById('cert-company').value = d.company_name || '';
document.getElementById('cert-business-num').value = d.business_num || '';
document.getElementById('cert-department').value = d.department || '';
document.getElementById('cert-position').value = d.position || '';
document.getElementById('cert-hire-date').value = d.hire_date ? d.hire_date + ' ~' : '';
document.getElementById('cert-ceo-name').value = d.ceo_name || '';
document.getElementById('cert-company-address').value = d.company_address || '';
} else {
showToast(data.message || '사원 정보를 불러올 수 없습니다.', 'error');
}
} catch (e) {
showToast('사원 정보 조회 중 오류가 발생했습니다.', 'error');
}
}
function onCertPurposeChange() {
const sel = document.getElementById('cert-purpose-select');
const customWrap = document.getElementById('cert-purpose-custom-wrap');
if (sel.value === '__custom__') {
customWrap.style.display = '';
document.getElementById('cert-purpose-custom').focus();
} else {
customWrap.style.display = 'none';
}
}
function getCertPurpose() {
const sel = document.getElementById('cert-purpose-select');
if (sel.value === '__custom__') {
return document.getElementById('cert-purpose-custom').value.trim();
}
return sel.value;
}
function openCertPreview() {
const name = document.getElementById('cert-name').value || '-';
const resident = document.getElementById('cert-resident').value || '-';
const address = document.getElementById('cert-address').value || '-';
const company = document.getElementById('cert-company').value || '-';
const businessNum = document.getElementById('cert-business-num').value || '-';
const department = document.getElementById('cert-department').value || '-';
const position = document.getElementById('cert-position').value || '-';
const hireDate = document.getElementById('cert-hire-date').value || '-';
const purpose = getCertPurpose() || '-';
const issueDate = document.getElementById('cert-issue-date').value || '-';
const ceoName = document.getElementById('cert-ceo-name').value || '-';
const issueDateFormatted = issueDate !== '-' ? issueDate.replace(/-/g, function(m, i) { return i === 4 ? '년 ' : '월 '; }) + '일' : '-';
const el = document.getElementById('cert-preview-content');
el.innerHTML = buildCertPreviewHtml({ name, resident, address, company, businessNum, department, position, hireDate, purpose, issueDateFormatted, ceoName });
document.getElementById('cert-preview-modal').style.display = '';
document.body.style.overflow = 'hidden';
}
function closeCertPreview() {
document.getElementById('cert-preview-modal').style.display = 'none';
document.body.style.overflow = '';
}
function printCertPreview() {
const content = document.getElementById('cert-preview-content').innerHTML;
const win = window.open('', '_blank', 'width=800,height=1000');
win.document.write('<html><head><title>재직증명서</title>');
win.document.write('<style>body{font-family:"Pretendard","Malgun Gothic",sans-serif;padding:48px 56px;margin:0;} table{border-collapse:collapse;width:100%;} th,td{border:1px solid #333;padding:10px 14px;font-size:14px;} th{background:#f8f9fa;font-weight:600;text-align:left;white-space:nowrap;width:120px;} @media print{body{padding:40px 48px;}}</style>');
win.document.write('</head><body>');
win.document.write(content);
win.document.write('</body></html>');
win.document.close();
win.onload = function() { win.print(); };
}
function buildCertPreviewHtml(d) {
const e = (s) => { const div = document.createElement('div'); div.textContent = s; return div.innerHTML; };
const thStyle = 'border:1px solid #333; padding:10px 14px; background:#f8f9fa; font-weight:600; text-align:left; white-space:nowrap; width:120px; font-size:14px;';
const tdStyle = 'border:1px solid #333; padding:10px 14px; font-size:14px;';
return `
<h1 style="text-align:center; font-size:28px; font-weight:700; letter-spacing:12px; margin-bottom:40px;">재 직 증 명 서</h1>
<h3 style="font-size:15px; font-weight:600; margin:24px 0 10px; color:#333;">1. 인적사항</h3>
<table style="border-collapse:collapse; width:100%; margin-bottom:20px;">
<tr>
<th style="${thStyle}">성 명</th>
<td style="${tdStyle}">${e(d.name)}</td>
<th style="${thStyle}">주민등록번호</th>
<td style="${tdStyle}">${e(d.resident)}</td>
</tr>
<tr>
<th style="${thStyle}">주 소</th>
<td style="${tdStyle}" colspan="3">${e(d.address)}</td>
</tr>
</table>
<h3 style="font-size:15px; font-weight:600; margin:24px 0 10px; color:#333;">2. 재직사항</h3>
<table style="border-collapse:collapse; width:100%; margin-bottom:20px;">
<tr>
<th style="${thStyle}">회 사 명</th>
<td style="${tdStyle}" colspan="3">${e(d.company)}</td>
</tr>
<tr>
<th style="${thStyle}">사업자번호</th>
<td style="${tdStyle}" colspan="3">${e(d.businessNum)}</td>
</tr>
<tr>
<th style="${thStyle}">근무부서</th>
<td style="${tdStyle}">${e(d.department)}</td>
<th style="${thStyle}">직 급</th>
<td style="${tdStyle}">${e(d.position)}</td>
</tr>
<tr>
<th style="${thStyle}">재직기간</th>
<td style="${tdStyle}" colspan="3">${e(d.hireDate)}</td>
</tr>
</table>
<h3 style="font-size:15px; font-weight:600; margin:24px 0 10px; color:#333;">3. 발급정보</h3>
<table style="border-collapse:collapse; width:100%; margin-bottom:32px;">
<tr>
<th style="${thStyle}">사용용도</th>
<td style="${tdStyle}" colspan="3">${e(d.purpose)}</td>
</tr>
</table>
<p style="text-align:center; font-size:15px; line-height:2; margin:36px 0;">
위 사항을 증명합니다.
</p>
<p style="text-align:center; font-size:15px; font-weight:500; margin-bottom:48px;">
${e(d.issueDateFormatted)}
</p>
<div style="text-align:center; margin-top:32px;">
<p style="font-size:16px; font-weight:600; margin-bottom:8px;">${e(d.company)}</p>
<p style="font-size:14px; color:#555;">대표이사 &nbsp;&nbsp; ${e(d.ceoName)} &nbsp;&nbsp; (인)</p>
</div>
`;
}
// =========================================================================
// 사용인감계 관련 함수
// =========================================================================
function buildSealUsagePreviewHtml(data) {
const e = (s) => s ? String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;') : '-';
const dateObj = data.usage_date ? new Date(data.usage_date + 'T00:00:00') : new Date();
const y = dateObj.getFullYear();
const m = String(dateObj.getMonth() + 1).padStart(2, ' ');
const d = String(dateObj.getDate()).padStart(2, ' ');
return `
<div style="text-align:center; margin-bottom:40px;">
<h1 style="font-size:26px; font-weight:700; letter-spacing:14px; margin:0;">사 용 인 감 계</h1>
</div>
<!-- 인감 비교란 -->
<table style="width:360px; margin:0 auto 32px; border-collapse:collapse;">
<tr>
<td style="border:1px solid #333; padding:8px; text-align:center; font-weight:600; font-size:13px; width:180px; background:#f8f8f8;">법인인감</td>
<td style="border:1px solid #333; padding:8px; text-align:center; font-weight:600; font-size:13px; width:180px; background:#f8f8f8;">사용인감</td>
</tr>
<tr>
<td style="border:1px solid #333; height:140px; text-align:center; vertical-align:middle;">
<span style="color:#bbb; font-size:11px;">(인감 날인)</span>
</td>
<td style="border:1px solid #333; height:140px; text-align:center; vertical-align:middle;">
<span style="color:#bbb; font-size:11px;">(인감 날인)</span>
</td>
</tr>
</table>
<!-- 용도 / 제출처 -->
<div style="font-size:13px; line-height:2; margin-bottom:8px;">
<p style="margin:0;"><span style="font-weight:600;">용도:</span> ${e(data.purpose)}</p>
<p style="margin:0;"><span style="font-weight:600;">제출처:</span> ${e(data.submit_to)}</p>
</div>
<!-- 확약 문구 -->
<div style="margin:28px 0; padding:16px 20px; border:1px solid #ccc; border-radius:4px; background:#fafafa;">
<p style="font-size:12px; line-height:1.8; margin:0; color:#333;">
위 사용인감은 당사에서 사용하는 인감입니다. 당사는 위 인감사용으로 인한 모든 책임을 질 것을 확약하고 사용인감계를 제출합니다.
</p>
</div>
<!-- 첨부서류 -->
${data.attachment_desc ? `<p style="font-size:12px; margin:0 0 24px; color:#333;"><span style="font-weight:600;">첨부서류:</span> ${e(data.attachment_desc)}</p>` : ''}
<!-- 일자 -->
<p style="text-align:center; font-size:14px; margin:32px 0 28px; letter-spacing:2px;">${y}년 ${m}월 ${d}일</p>
<!-- 회사 정보 -->
<div style="margin-top:20px; font-size:12px; line-height:2;">
<p style="margin:0;"><span style="display:inline-block; width:110px; font-weight:600; letter-spacing:6px;">상 호</span>: ${e(data.company_name)}</p>
<p style="margin:0;"><span style="display:inline-block; width:110px; font-weight:600;">사업자등록번호</span>: ${e(data.business_num)}</p>
<p style="margin:0;"><span style="display:inline-block; width:110px; font-weight:600; letter-spacing:6px;">주 소</span>: ${e(data.company_address)}</p>
<p style="margin:0;"><span style="display:inline-block; width:110px; font-weight:600;">대표이사</span>: ${e(data.ceo_name)}</p>
</div>
`;
}
function openSealUsagePreview() {
const data = {
usage_date: document.getElementById('su-usage-date').value,
purpose: document.getElementById('su-purpose').value,
submit_to: document.getElementById('su-submit-to').value,
attachment_desc: document.getElementById('su-attachment-desc').value,
company_name: document.getElementById('su-company-name').value,
business_num: document.getElementById('su-business-num').value,
ceo_name: document.getElementById('su-ceo-name').value,
company_address: document.getElementById('su-company-address').value,
};
document.getElementById('seal-usage-preview-content').innerHTML = buildSealUsagePreviewHtml(data);
document.getElementById('seal-usage-preview-modal').style.display = '';
document.body.style.overflow = 'hidden';
}
function closeSealUsagePreview() {
document.getElementById('seal-usage-preview-modal').style.display = 'none';
document.body.style.overflow = '';
}
function printSealUsagePreview() {
const content = document.getElementById('seal-usage-preview-content').innerHTML;
const win = window.open('', '_blank');
win.document.write('<html><head><title>사용인감계</title>');
win.document.write('<style>body{font-family:"Pretendard","Malgun Gothic",sans-serif;padding:48px 56px;margin:0;}@media print{body{padding:40px 48px;}}</style>');
win.document.write('</head><body>');
win.document.write(content);
win.document.write('</body></html>');
win.document.close();
win.print();
}
// ── 이사회의사록 미리보기 ──
function buildBoardMinutesPreviewHtml(data) {
const dt = data.meeting_datetime || '';
let dateStr = '';
if (dt) {
const d = new Date(dt);
const year = d.getFullYear();
const month = d.getMonth() + 1;
const day = d.getDate();
const hour = d.getHours();
const min = String(d.getMinutes()).padStart(2, '0');
const ampm = hour < 12 ? '오전' : '오후';
const h12 = hour % 12 || 12;
dateStr = `${year}년 ${month}월 ${day}일 ${ampm} ${h12}시 ${min}분`;
}
const agendas = data.agendas || [];
let agendasHtml = agendas.map(a =>
`<div style="margin-bottom: 12px;"><div style="font-weight: 600; margin-bottom: 4px;">[제${a.no}호 의안: ${a.title}]</div>${a.result ? `<div style="padding-left: 16px;">* ${a.result}</div>` : ''}</div>`
).join('');
const signers = data.signers || [];
let signersHtml = signers.map(s =>
`<div style="display: flex; gap: 40px; margin-bottom: 8px; justify-content: center;"><span style="min-width: 120px; text-align: right;">${s.role}:</span><span style="min-width: 100px;">${s.name}</span><span>(인) &#12958;</span></div>`
).join('');
return `<div style="text-align: center; margin-bottom: 36px;"><h1 style="font-size: 26px; font-weight: 800; letter-spacing: 6px;">이 사 회 의 사 록</h1></div>
<div style="font-size: 13px; line-height: 2;"><p><strong>1. 일 시:</strong> ${dateStr}</p><p><strong>2. 장 소:</strong> ${data.meeting_place || ''}</p>
<p><strong>3. 출석이사 및 감사:</strong></p><div style="padding-left: 24px;"><p>* 이사 총수: ${data.total_directors || 0}명 (출석이사: ${data.present_directors || 0}명)</p><p>* 감사 총수: ${data.total_auditors || 0}명 (출석감사: ${data.present_auditors || 0}명)</p></div>
<p style="margin-top: 8px;"><strong>4. 의 안:</strong></p><div style="padding-left: 24px; margin-bottom: 8px;">${agendas.map(a => `<p>${a.title}</p>`).join('')}</div>
<p><strong>5. 의사 경과 및 결과:</strong></p><div style="padding-left: 24px; margin-bottom: 8px;">${data.proceedings ? `<p>${data.proceedings}</p>` : `<p>의장(대표이사 ${data.chairman_name || ''}) 위와 같이 법정 수에 달하는 이사가 출석하였으므로 이사회가 적법하게 성립되었음을 선언하고, 다음의 의안을 상정하여 승인을 구하다.</p>`}${agendasHtml}</div>
<p><strong>6. 폐 회:</strong></p><div style="padding-left: 24px;"><p>의장은 이상으로써 의안 전부의 심의를 종료하였으므로 폐회를 선언하다.</p>${data.closing_time ? `<p>(폐회 시각: ${data.closing_time})</p>` : ''}</div></div>
<div style="margin-top: 32px; font-size: 13px; line-height: 1.8; text-align: center;"><p>위 의사의 경과와 결과를 명확히 하기 위하여 이 의사록을 작성하고,</p><p>의장과 출석한 이사 및 감사가 아래와 같이 기명날인한다.</p><p style="margin-top: 20px; font-size: 14px;">${data.meeting_date || ''}</p><p style="margin-top: 8px; font-size: 15px; font-weight: 700;">${data.company_name || ''}</p></div>
<div style="margin-top: 32px; font-size: 13px;">${signersHtml}</div>`;
}
function openBoardMinutesPreview() {
const bmContainer = document.getElementById('board-minutes-form-container');
const bmAlpine = bmContainer._x_dataStack?.[0];
const data = {
meeting_datetime: document.getElementById('bm-meeting-datetime').value,
meeting_place: document.getElementById('bm-meeting-place').value,
total_directors: parseInt(document.getElementById('bm-total-directors').value) || 0,
present_directors: parseInt(document.getElementById('bm-present-directors').value) || 0,
total_auditors: parseInt(document.getElementById('bm-total-auditors').value) || 0,
present_auditors: parseInt(document.getElementById('bm-present-auditors').value) || 0,
chairman_name: document.getElementById('bm-chairman-name').value,
agendas: bmAlpine ? bmAlpine.agendas.filter(a => a.title.trim()) : [],
proceedings: document.getElementById('bm-proceedings').value,
closing_time: document.getElementById('bm-closing-time').value,
signers: bmAlpine ? bmAlpine.signers.filter(s => s.name.trim()) : [],
company_name: document.getElementById('bm-company-name').value,
meeting_date: (document.getElementById('bm-meeting-datetime').value || '').split('T')[0],
};
document.getElementById('board-minutes-preview-content').innerHTML = buildBoardMinutesPreviewHtml(data);
document.getElementById('board-minutes-preview-modal').style.display = '';
document.body.style.overflow = 'hidden';
}
function closeBoardMinutesPreview() {
document.getElementById('board-minutes-preview-modal').style.display = 'none';
document.body.style.overflow = '';
}
function printBoardMinutesPreview() {
const content = document.getElementById('board-minutes-preview-content').innerHTML;
const win = window.open('', '_blank');
win.document.write('<html><head><title>이사회의사록</title><style>body{font-family:"Pretendard","Malgun Gothic",sans-serif;padding:48px 56px;margin:0;}@media print{body{padding:40px 48px;}}</style></head><body>' + content + '</body></html>');
win.document.close();
win.print();
}
// ── 위임장 미리보기 ──
function buildDelegationPreviewHtml(data) {
return `
<div style="text-align: center; margin-bottom: 36px;">
<h1 style="font-size: 28px; font-weight: 800; letter-spacing: 8px;">위 임 장</h1>
</div>
<table style="width: 100%; border-collapse: collapse; font-size: 13px; margin-bottom: 24px;">
<tr>
<td style="border: 1px solid #333; background: #f5f5f5; padding: 8px 12px; font-weight: 600; width: 100px; text-align: center;" rowspan="4">위임인</td>
<td style="border: 1px solid #333; background: #f5f5f5; padding: 8px 12px; font-weight: 600; width: 100px; text-align: center;">법인명</td>
<td style="border: 1px solid #333; padding: 8px 12px;" colspan="3">${data.company_name || ''}</td>
</tr>
<tr>
<td style="border: 1px solid #333; background: #f5f5f5; padding: 8px 12px; font-weight: 600; text-align: center;">사업자등록번호</td>
<td style="border: 1px solid #333; padding: 8px 12px;" colspan="3">${data.business_num || ''}</td>
</tr>
<tr>
<td style="border: 1px solid #333; background: #f5f5f5; padding: 8px 12px; font-weight: 600; text-align: center;">주소</td>
<td style="border: 1px solid #333; padding: 8px 12px;" colspan="3">${data.company_address || ''}</td>
</tr>
<tr>
<td style="border: 1px solid #333; background: #f5f5f5; padding: 8px 12px; font-weight: 600; text-align: center;">대표자</td>
<td style="border: 1px solid #333; padding: 8px 12px;" colspan="3">${data.ceo_name || ''}</td>
</tr>
<tr>
<td style="border: 1px solid #333; background: #f5f5f5; padding: 8px 12px; font-weight: 600; text-align: center;" rowspan="5">수임인</td>
<td style="border: 1px solid #333; background: #f5f5f5; padding: 8px 12px; font-weight: 600; text-align: center;">성명</td>
<td style="border: 1px solid #333; padding: 8px 12px;" colspan="3">${data.agent_name || ''}</td>
</tr>
<tr>
<td style="border: 1px solid #333; background: #f5f5f5; padding: 8px 12px; font-weight: 600; text-align: center;">생년월일</td>
<td style="border: 1px solid #333; padding: 8px 12px;" colspan="3">${data.agent_birth || ''}</td>
</tr>
<tr>
<td style="border: 1px solid #333; background: #f5f5f5; padding: 8px 12px; font-weight: 600; text-align: center;">주소</td>
<td style="border: 1px solid #333; padding: 8px 12px;" colspan="3">${data.agent_address || ''}</td>
</tr>
<tr>
<td style="border: 1px solid #333; background: #f5f5f5; padding: 8px 12px; font-weight: 600; text-align: center;">연락처</td>
<td style="border: 1px solid #333; padding: 8px 12px;" colspan="3">${data.agent_phone || ''}</td>
</tr>
<tr>
<td style="border: 1px solid #333; background: #f5f5f5; padding: 8px 12px; font-weight: 600; text-align: center;">소속</td>
<td style="border: 1px solid #333; padding: 8px 12px;" colspan="3">${data.agent_department || ''}</td>
</tr>
</table>
<div style="margin-bottom: 20px;">
<div style="font-weight: 600; font-size: 14px; margin-bottom: 8px; border-bottom: 2px solid #333; padding-bottom: 4px;">위임사항</div>
<div style="font-size: 13px; line-height: 1.8; white-space: pre-wrap; padding: 8px 4px;">${data.delegation_detail || ''}</div>
</div>
<div style="margin-bottom: 20px;">
<div style="font-weight: 600; font-size: 14px; margin-bottom: 8px; border-bottom: 2px solid #333; padding-bottom: 4px;">위임기간</div>
<div style="font-size: 13px; padding: 8px 4px;">${data.period_start || ''} ~ ${data.period_end || ''}</div>
</div>
${data.attachments_desc ? `
<div style="margin-bottom: 20px;">
<div style="font-weight: 600; font-size: 14px; margin-bottom: 8px; border-bottom: 2px solid #333; padding-bottom: 4px;">첨부서류</div>
<div style="font-size: 13px; line-height: 1.8; white-space: pre-wrap; padding: 8px 4px;">${data.attachments_desc}</div>
</div>` : ''}
<div style="text-align: center; margin-top: 40px; font-size: 13px;">
<p>위와 같이 권한을 위임합니다.</p>
<p style="margin-top: 24px; font-size: 14px; font-weight: 600;">${data.company_name || ''}</p>
<p style="margin-top: 4px;">대표이사 ${data.ceo_name || ''} (인)</p>
</div>
`;
}
function openDelegationPreview() {
const data = {
company_name: document.getElementById('dl-company-name').value,
business_num: document.getElementById('dl-business-num').value,
ceo_name: document.getElementById('dl-ceo-name').value,
company_address: document.getElementById('dl-company-address').value,
agent_name: document.getElementById('dl-agent-name').value,
agent_birth: document.getElementById('dl-agent-birth').value,
agent_address: document.getElementById('dl-agent-address').value,
agent_phone: document.getElementById('dl-agent-phone').value,
agent_department: document.getElementById('dl-agent-department').value,
delegation_detail: document.getElementById('dl-delegation-detail').value,
period_start: document.getElementById('dl-period-start').value,
period_end: document.getElementById('dl-period-end').value,
attachments_desc: document.getElementById('dl-attachments-desc').value,
};
document.getElementById('delegation-preview-content').innerHTML = buildDelegationPreviewHtml(data);
document.getElementById('delegation-preview-modal').style.display = '';
document.body.style.overflow = 'hidden';
}
function closeDelegationPreview() {
document.getElementById('delegation-preview-modal').style.display = 'none';
document.body.style.overflow = '';
}
function printDelegationPreview() {
const content = document.getElementById('delegation-preview-content').innerHTML;
const win = window.open('', '_blank');
win.document.write('<html><head><title>위임장</title>');
win.document.write('<style>body{font-family:"Pretendard","Malgun Gothic",sans-serif;padding:48px 56px;margin:0;}@media print{body{padding:40px 48px;}}</style>');
win.document.write('</head><body>');
win.document.write(content);
win.document.write('</body></html>');
win.document.close();
win.print();
}
// ─── 견적서 미리보기 ───
function buildQuotationPreviewHtml(data) {
const fmt = n => (n || 0).toLocaleString('ko-KR');
let itemsHtml = '';
(data.items || []).forEach((item, idx) => {
itemsHtml += `<tr style="border-bottom:1px solid #ddd;">
<td style="padding:6px 8px;text-align:center;font-size:12px;">${idx + 1}</td>
<td style="padding:6px 8px;font-size:12px;">${item.name || ''}</td>
<td style="padding:6px 8px;font-size:12px;color:#555;">${item.spec || ''}</td>
<td style="padding:6px 8px;text-align:right;font-size:12px;">${item.qty || 0}</td>
<td style="padding:6px 8px;text-align:right;font-size:12px;">${fmt(item.unit_price)}</td>
<td style="padding:6px 8px;text-align:right;font-size:12px;font-weight:600;">${fmt(item.supply_amount)}</td>
<td style="padding:6px 8px;text-align:right;font-size:12px;color:#555;">${fmt(item.tax)}</td>
<td style="padding:6px 8px;font-size:12px;color:#555;">${item.note || ''}</td>
</tr>`;
});
return `
<div style="text-align:center;margin-bottom:32px;">
<h1 style="font-size:28px;font-weight:800;letter-spacing:8px;margin:0;">견 적 서</h1>
</div>
<div style="display:flex;justify-content:space-between;margin-bottom:24px;">
<div style="font-size:14px;">
<p style="margin:0 0 4px;"><strong>${data.client_name || ''}</strong> 귀하</p>
<p style="margin:0;font-size:12px;color:#555;">아래와 같이 견적합니다.</p>
</div>
<div style="font-size:12px;text-align:right;color:#555;">
<p style="margin:0;">견적일자: ${data.quote_date || ''}</p>
</div>
</div>
<div style="background:#EFF6FF;border:2px solid #3B82F6;border-radius:8px;padding:16px 24px;text-align:center;margin-bottom:24px;">
<p style="margin:0 0 4px;font-size:13px;color:#555;">견적금액</p>
<p style="margin:0;font-size:24px;font-weight:800;color:#1E40AF;">₩ ${fmt(data.total_amount)}</p>
<p style="margin:4px 0 0;font-size:11px;color:#666;">(공급가액 ${fmt(data.total_supply)} + 부가세 ${fmt(data.total_tax)})</p>
</div>
<table style="width:100%;border-collapse:collapse;margin-bottom:16px;font-size:12px;">
<caption style="text-align:left;font-weight:700;font-size:13px;margin-bottom:8px;">공급자</caption>
<tbody>
<tr style="border-bottom:1px solid #eee;">
<td style="padding:6px 8px;background:#F9FAFB;width:100px;font-weight:600;font-size:11px;">사업자등록번호</td>
<td style="padding:6px 8px;">${data.business_num || ''}</td>
<td style="padding:6px 8px;background:#F9FAFB;width:60px;font-weight:600;font-size:11px;">상호</td>
<td style="padding:6px 8px;">${data.company_name || ''}</td>
<td style="padding:6px 8px;background:#F9FAFB;width:60px;font-weight:600;font-size:11px;">대표자</td>
<td style="padding:6px 8px;">${data.ceo_name || ''}</td>
</tr>
<tr style="border-bottom:1px solid #eee;">
<td style="padding:6px 8px;background:#F9FAFB;font-weight:600;font-size:11px;">소재지</td>
<td colspan="5" style="padding:6px 8px;">${data.company_address || ''}</td>
</tr>
<tr style="border-bottom:1px solid #eee;">
<td style="padding:6px 8px;background:#F9FAFB;font-weight:600;font-size:11px;">업태</td>
<td style="padding:6px 8px;">${data.business_type || ''}</td>
<td style="padding:6px 8px;background:#F9FAFB;font-weight:600;font-size:11px;">업종</td>
<td colspan="3" style="padding:6px 8px;">${data.business_item || ''}</td>
</tr>
<tr>
<td style="padding:6px 8px;background:#F9FAFB;font-weight:600;font-size:11px;">연락처</td>
<td style="padding:6px 8px;">${data.phone || ''}</td>
<td style="padding:6px 8px;background:#F9FAFB;font-weight:600;font-size:11px;">계좌</td>
<td colspan="3" style="padding:6px 8px;">${data.bank_account || ''}</td>
</tr>
</tbody>
</table>
<table style="width:100%;border-collapse:collapse;margin-bottom:16px;">
<thead>
<tr style="background:#F9FAFB;border-bottom:2px solid #ccc;">
<th style="padding:8px;text-align:center;font-size:11px;width:32px;">No</th>
<th style="padding:8px;text-align:left;font-size:11px;">품명</th>
<th style="padding:8px;text-align:left;font-size:11px;">규격</th>
<th style="padding:8px;text-align:right;font-size:11px;">수량</th>
<th style="padding:8px;text-align:right;font-size:11px;">단가</th>
<th style="padding:8px;text-align:right;font-size:11px;">공급가액</th>
<th style="padding:8px;text-align:right;font-size:11px;">세액</th>
<th style="padding:8px;text-align:left;font-size:11px;">비고</th>
</tr>
</thead>
<tbody>${itemsHtml}</tbody>
<tfoot>
<tr style="border-top:2px solid #999;background:#F9FAFB;">
<td colspan="5" style="padding:8px;text-align:right;font-weight:700;font-size:12px;">합 계</td>
<td style="padding:8px;text-align:right;font-weight:700;font-size:12px;color:#1E40AF;">${fmt(data.total_supply)}</td>
<td style="padding:8px;text-align:right;font-weight:700;font-size:12px;color:#1E40AF;">${fmt(data.total_tax)}</td>
<td></td>
</tr>
</tfoot>
</table>
${data.remarks ? `<div style="margin-top:16px;padding:12px 16px;background:#FFFBEB;border:1px solid #FDE68A;border-radius:6px;font-size:12px;"><strong style="font-size:11px;color:#92400E;">특이사항</strong><p style="margin:4px 0 0;white-space:pre-wrap;">${data.remarks}</p></div>` : ''}
`;
}
function openQuotationPreview() {
const qtContainer = document.getElementById('quotation-form-container');
const qtAlpine = qtContainer._x_dataStack?.[0];
const qtItems = qtAlpine ? qtAlpine.items.filter(i => i.name.trim()).map(i => ({
name: i.name.trim(), spec: (i.spec||'').trim(), qty: parseInt(i.qty)||0, unit_price: parseInt(i.unit_price)||0,
supply_amount: (parseInt(i.qty)||0) * (parseInt(i.unit_price)||0), tax: qtAlpine.itemTax(i), note: (i.note||'').trim(),
})) : [];
const data = {
client_name: document.getElementById('qt-client-name').value.trim(),
quote_date: document.getElementById('qt-quote-date').value,
company_name: document.getElementById('qt-company-name').value,
business_num: document.getElementById('qt-business-num').value,
ceo_name: document.getElementById('qt-ceo-name').value,
company_address: document.getElementById('qt-company-address').value,
business_type: document.getElementById('qt-business-type-input')?.value || '',
business_item: document.getElementById('qt-business-item-input')?.value || '',
phone: document.getElementById('qt-phone-input')?.value || '',
bank_account: document.getElementById('qt-bank-input')?.value || '',
items: qtItems,
total_supply: qtAlpine ? qtAlpine.totalSupply() : 0,
total_tax: qtAlpine ? qtAlpine.totalTax() : 0,
total_amount: qtAlpine ? qtAlpine.totalAmount() : 0,
remarks: document.getElementById('qt-remarks').value.trim(),
};
document.getElementById('quotation-preview-content').innerHTML = buildQuotationPreviewHtml(data);
document.getElementById('quotation-preview-modal').style.display = '';
document.body.style.overflow = 'hidden';
}
function closeQuotationPreview() {
document.getElementById('quotation-preview-modal').style.display = 'none';
document.body.style.overflow = '';
}
function printQuotationPreview() {
const content = document.getElementById('quotation-preview-content').innerHTML;
const win = window.open('', '_blank');
win.document.write('<html><head><title>견적서</title>');
win.document.write('<style>body{font-family:"Pretendard","Malgun Gothic",sans-serif;padding:48px 56px;margin:0;}@media print{body{padding:40px 48px;}}</style>');
win.document.write('</head><body>');
win.document.write(content);
win.document.write('</body></html>');
win.document.close();
win.print();
}
</script>
@endpush