diff --git a/resources/views/approvals/create.blade.php b/resources/views/approvals/create.blade.php
index bd96815e..c430b5cb 100644
--- a/resources/views/approvals/create.blade.php
+++ b/resources/views/approvals/create.blade.php
@@ -145,6 +145,11 @@ class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-
'tenantInfo' => $tenantInfo ?? [],
])
+ {{-- 공문서 전용 폼 --}}
+ @include('approvals.partials._official-letter-form', [
+ 'tenantInfo' => $tenantInfo ?? [],
+ ])
+
{{-- 견적서 전용 폼 --}}
@include('approvals.partials._quotation-form', [
'tenantInfo' => $tenantInfo ?? [],
@@ -284,6 +289,11 @@ class="p-1 text-gray-400 hover:text-gray-600 transition">
color: 'border-slate-200 bg-slate-50', titleColor: 'text-slate-800', textColor: 'text-slate-600',
text: '업무 추진에 필요한 사항을 기안하여 결재를 받는 기본 문서입니다. 프로젝트 계획, 업무 협조 요청, 내부 제안 등 정형화된 양식이 없는 일반적인 업무 보고·요청에 사용합니다.',
},
+ official_letter: {
+ title: '공문서', icon: '📨',
+ color: 'border-blue-200 bg-blue-50', titleColor: 'text-blue-800', textColor: 'text-blue-600',
+ text: '외부 기관이나 거래처에 발송하는 공식 문서입니다. 문서번호, 수신처, 제목, 본문, 붙임 서류를 기재하며, 승인 후 회사 직인이 날인된 공문서로 사용합니다.',
+ },
leave: {
title: '휴가신청', icon: '🏖️',
color: 'border-sky-200 bg-sky-50', titleColor: 'text-sky-800', textColor: 'text-sky-600',
@@ -373,7 +383,7 @@ class="p-1 text-gray-400 hover:text-gray-600 transition">
// 2단계 분류 정의 (코드 → 카테고리)
const formCategoryMap = {
- BUSINESS_DRAFT: '일반',
+ BUSINESS_DRAFT: '일반', official_letter: '일반',
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: '품의',
@@ -465,6 +475,7 @@ function updateFormDescription(formId) {
let isDelegationForm = false;
let isBoardMinutesForm = false;
let isQuotationForm = false;
+let isOfficialLetterForm = false;
// 양식코드별 표시할 유형 목록
const leaveTypesByFormCode = {
@@ -717,6 +728,7 @@ function switchFormMode(formId) {
const delegationContainer = document.getElementById('delegation-form-container');
const boardMinutesContainer = document.getElementById('board-minutes-form-container');
const quotationContainer = document.getElementById('quotation-form-container');
+ const officialLetterContainer = document.getElementById('official-letter-form-container');
const bodyArea = document.getElementById('body-area');
const expenseLoadBtn = document.getElementById('expense-load-btn');
@@ -734,6 +746,7 @@ function switchFormMode(formId) {
delegationContainer.style.display = 'none';
boardMinutesContainer.style.display = 'none';
quotationContainer.style.display = 'none';
+ officialLetterContainer.style.display = 'none';
expenseLoadBtn.style.display = 'none';
bodyArea.style.display = 'none';
isExpenseForm = false;
@@ -747,6 +760,7 @@ function switchFormMode(formId) {
isDelegationForm = false;
isBoardMinutesForm = false;
isQuotationForm = false;
+ isOfficialLetterForm = false;
if (code === 'expense') {
isExpenseForm = true;
@@ -818,6 +832,9 @@ function switchFormMode(formId) {
} else if (code === 'quotation') {
isQuotationForm = true;
quotationContainer.style.display = '';
+ } else if (code === 'official_letter') {
+ isOfficialLetterForm = true;
+ officialLetterContainer.style.display = '';
} else {
bodyArea.style.display = '';
}
@@ -828,7 +845,7 @@ function applyBodyTemplate(formId) {
switchFormMode(formId);
// 전용 폼이면 제목을 양식명으로 설정하고 body template 적용 건너뜀
- if (isExpenseForm || isPurchaseRequestForm || isLeaveForm || isCertForm || isCareerCertForm || isAppointmentCertForm || isResignationForm || isSealUsageForm || isDelegationForm || isBoardMinutesForm || isQuotationForm) {
+ if (isExpenseForm || isPurchaseRequestForm || isLeaveForm || isCertForm || isCareerCertForm || isAppointmentCertForm || isResignationForm || isSealUsageForm || isDelegationForm || isBoardMinutesForm || isQuotationForm || isOfficialLetterForm) {
const titleEl = document.getElementById('title');
const formSelect = document.getElementById('form_id');
titleEl.value = formSelect.options[formSelect.selectedIndex].text;
@@ -1140,6 +1157,30 @@ function applyBodyTemplate(formId) {
remarks: document.getElementById('qt-remarks').value.trim(),
};
formBody = null;
+ } else if (isOfficialLetterForm) {
+ const olRecipient = document.getElementById('ol-recipient').value.trim();
+ if (!olRecipient) { showToast('수신처를 입력해주세요.', 'warning'); return; }
+ const olSubject = document.getElementById('ol-subject').value.trim();
+ if (!olSubject) { showToast('제목을 입력해주세요.', 'warning'); return; }
+ const olBody = document.getElementById('ol-body').value.trim();
+ if (!olBody) { showToast('본문을 입력해주세요.', 'warning'); return; }
+
+ formContent = {
+ doc_number: document.getElementById('ol-doc-number').value.trim(),
+ doc_date: document.getElementById('ol-doc-date').value,
+ recipient: olRecipient,
+ reference: document.getElementById('ol-reference').value.trim(),
+ subject: olSubject,
+ body: olBody,
+ attachments_desc: document.getElementById('ol-attachments-desc').value.trim(),
+ company_name: document.getElementById('ol-company-name').value,
+ ceo_name: document.getElementById('ol-ceo-name').value,
+ company_address: document.getElementById('ol-company-address').value,
+ phone: document.getElementById('ol-phone-input')?.value || document.getElementById('ol-phone').value,
+ fax: document.getElementById('ol-fax-input')?.value || document.getElementById('ol-fax').value,
+ email: document.getElementById('ol-email-input')?.value || document.getElementById('ol-email').value,
+ };
+ formBody = null;
} else if (isDelegationForm) {
const dlAgentName = document.getElementById('dl-agent-name').value.trim();
if (!dlAgentName) {
@@ -2431,6 +2472,72 @@ function printQuotationPreview() {
win.print();
}
+// ─── 공문서 미리보기 ───
+function buildOfficialLetterPreviewHtml(data) {
+ const bodyHtml = (data.body || '').replace(/&/g,'&').replace(//g,'>').replace(/\n/g, '
');
+ const attachHtml = (data.attachments_desc || '').replace(/&/g,'&').replace(//g,'>').replace(/\n/g, '
');
+ return `
+
+
${data.company_name || ''}
+
+
+
문서번호 : ${data.doc_number || ''}
+
일자 : ${data.doc_date || ''}
+
수신 : ${data.recipient || ''}
+ ${data.reference ? `
참조 : ${data.reference}
` : ''}
+
제목 : ${data.subject || ''}
+
+ ${bodyHtml}
+ ${data.attachments_desc ? `붙임 :
${attachHtml}
` : ''}
+
+ ${data.company_name || ''}
+ 대표이사 ${data.ceo_name || ''}
+ [직인날인]
+
+
+ ${data.company_address ? `
${data.company_address}
` : ''}
+
${data.phone ? '전화 ' + data.phone : ''}${data.fax ? ' / 팩스 ' + data.fax : ''}${data.email ? ' / 이메일 ' + data.email : ''}
+
`;
+}
+
+function openOfficialLetterPreview() {
+ const data = {
+ doc_number: document.getElementById('ol-doc-number').value.trim(),
+ doc_date: document.getElementById('ol-doc-date').value,
+ recipient: document.getElementById('ol-recipient').value.trim(),
+ reference: document.getElementById('ol-reference').value.trim(),
+ subject: document.getElementById('ol-subject').value.trim(),
+ body: document.getElementById('ol-body').value.trim(),
+ attachments_desc: document.getElementById('ol-attachments-desc').value.trim(),
+ company_name: document.getElementById('ol-company-name').value,
+ ceo_name: document.getElementById('ol-ceo-name').value,
+ company_address: document.getElementById('ol-company-address').value,
+ phone: document.getElementById('ol-phone-input')?.value || '',
+ fax: document.getElementById('ol-fax-input')?.value || '',
+ email: document.getElementById('ol-email-input')?.value || '',
+ };
+ document.getElementById('official-letter-preview-content').innerHTML = buildOfficialLetterPreviewHtml(data);
+ document.getElementById('official-letter-preview-modal').style.display = '';
+ document.body.style.overflow = 'hidden';
+}
+
+function closeOfficialLetterPreview() {
+ document.getElementById('official-letter-preview-modal').style.display = 'none';
+ document.body.style.overflow = '';
+}
+
+function printOfficialLetterPreview() {
+ const content = document.getElementById('official-letter-preview-content').innerHTML;
+ const win = window.open('', '_blank');
+ win.document.write('공문서');
+ win.document.write('');
+ win.document.write('');
+ win.document.write(content);
+ win.document.write('');
+ win.document.close();
+ win.print();
+}
+
function printDelegationPreview() {
const content = document.getElementById('delegation-preview-content').innerHTML;
const win = window.open('', '_blank');
diff --git a/resources/views/approvals/edit.blade.php b/resources/views/approvals/edit.blade.php
index 496e0233..d83ec7fb 100644
--- a/resources/views/approvals/edit.blade.php
+++ b/resources/views/approvals/edit.blade.php
@@ -163,6 +163,11 @@ class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-
'tenantInfo' => $tenantInfo ?? [],
])
+ {{-- 공문서 전용 폼 --}}
+ @include('approvals.partials._official-letter-form', [
+ 'tenantInfo' => $tenantInfo ?? [],
+ ])
+
{{-- 견적서 전용 폼 --}}
@include('approvals.partials._quotation-form', [
'tenantInfo' => $tenantInfo ?? [],
@@ -308,6 +313,7 @@ class="px-6 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg text-sm fon
let isDelegationForm = false;
let isBoardMinutesForm = false;
let isQuotationForm = false;
+let isOfficialLetterForm = false;
const formDescriptions = {
BUSINESS_DRAFT: {
@@ -315,6 +321,11 @@ class="px-6 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg text-sm fon
color: 'border-slate-200 bg-slate-50', titleColor: 'text-slate-800', textColor: 'text-slate-600',
text: '업무 추진에 필요한 사항을 기안하여 결재를 받는 기본 문서입니다. 프로젝트 계획, 업무 협조 요청, 내부 제안 등 정형화된 양식이 없는 일반적인 업무 보고·요청에 사용합니다.',
},
+ official_letter: {
+ title: '공문서', icon: '📨',
+ color: 'border-blue-200 bg-blue-50', titleColor: 'text-blue-800', textColor: 'text-blue-600',
+ text: '외부 기관이나 거래처에 발송하는 공식 문서입니다. 문서번호, 수신처, 제목, 본문, 붙임 서류를 기재하며, 승인 후 회사 직인이 날인된 공문서로 사용합니다.',
+ },
leave: {
title: '휴가신청', icon: '🏖️',
color: 'border-sky-200 bg-sky-50', titleColor: 'text-sky-800', textColor: 'text-sky-600',
@@ -404,7 +415,7 @@ class="px-6 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg text-sm fon
// 2단계 분류 정의
const formCategoryMap = {
- BUSINESS_DRAFT: '일반',
+ BUSINESS_DRAFT: '일반', official_letter: '일반',
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: '품의',
@@ -649,6 +660,7 @@ function switchFormMode(formId) {
const delegationContainer = document.getElementById('delegation-form-container');
const boardMinutesContainer = document.getElementById('board-minutes-form-container');
const quotationContainer = document.getElementById('quotation-form-container');
+ const officialLetterContainer = document.getElementById('official-letter-form-container');
const bodyArea = document.getElementById('body-area');
expenseContainer.style.display = 'none';
@@ -658,6 +670,7 @@ function switchFormMode(formId) {
delegationContainer.style.display = 'none';
boardMinutesContainer.style.display = 'none';
quotationContainer.style.display = 'none';
+ officialLetterContainer.style.display = 'none';
bodyArea.style.display = 'none';
isExpenseForm = false;
isPurchaseRequestForm = false;
@@ -666,6 +679,7 @@ function switchFormMode(formId) {
isDelegationForm = false;
isBoardMinutesForm = false;
isQuotationForm = false;
+ isOfficialLetterForm = false;
if (code === 'expense') {
isExpenseForm = true;
@@ -694,6 +708,9 @@ function switchFormMode(formId) {
} else if (code === 'quotation') {
isQuotationForm = true;
quotationContainer.style.display = '';
+ } else if (code === 'official_letter') {
+ isOfficialLetterForm = true;
+ officialLetterContainer.style.display = '';
} else {
bodyArea.style.display = '';
}
@@ -704,7 +721,7 @@ function applyBodyTemplate(formId) {
switchFormMode(formId);
// 전용 폼이면 제목만 자동 설정하고 body template 적용 건너뜀
- if (isExpenseForm || isPurchaseRequestForm || isCertForm || isSealUsageForm || isDelegationForm || isBoardMinutesForm || isQuotationForm) {
+ if (isExpenseForm || isPurchaseRequestForm || isCertForm || isSealUsageForm || isDelegationForm || isBoardMinutesForm || isQuotationForm || isOfficialLetterForm) {
const titleEl = document.getElementById('title');
if (!titleEl.value.trim()) {
const formSelect = document.getElementById('form_id');
@@ -882,8 +899,25 @@ function applyBodyTemplate(formId) {
}
}
+ // 공문서 기존 데이터 복원
+ if (isOfficialLetterForm) {
+ const olContent = @json($approval->content ?? []);
+ if (olContent.recipient) {
+ document.getElementById('ol-doc-number').value = olContent.doc_number || '';
+ document.getElementById('ol-doc-date').value = olContent.doc_date || '';
+ document.getElementById('ol-recipient').value = olContent.recipient || '';
+ document.getElementById('ol-reference').value = olContent.reference || '';
+ document.getElementById('ol-subject').value = olContent.subject || '';
+ document.getElementById('ol-body').value = olContent.body || '';
+ document.getElementById('ol-attachments-desc').value = olContent.attachments_desc || '';
+ if (olContent.phone) { const el = document.getElementById('ol-phone-input'); if (el) el.value = olContent.phone; }
+ if (olContent.fax) { const el = document.getElementById('ol-fax-input'); if (el) el.value = olContent.fax; }
+ if (olContent.email) { const el = document.getElementById('ol-email-input'); if (el) el.value = olContent.email; }
+ }
+ }
+
// 전용 폼이 아닌 경우에만 Quill 편집기 자동 활성화
- if (!isExpenseForm && !isPurchaseRequestForm && !isCertForm && !isSealUsageForm && !isDelegationForm && !isBoardMinutesForm && !isQuotationForm) {
+ if (!isExpenseForm && !isPurchaseRequestForm && !isCertForm && !isSealUsageForm && !isDelegationForm && !isBoardMinutesForm && !isQuotationForm && !isOfficialLetterForm) {
const existingBody = document.getElementById('body').value;
if (/<[a-z][\s\S]*>/i.test(existingBody)) {
document.getElementById('useEditor').checked = true;
@@ -1044,6 +1078,28 @@ function applyBodyTemplate(formId) {
remarks: document.getElementById('qt-remarks').value.trim(),
};
formBody = null;
+ } else if (isOfficialLetterForm) {
+ const olRecipient = document.getElementById('ol-recipient').value.trim();
+ if (!olRecipient) { showToast('수신처를 입력해주세요.', 'warning'); return; }
+ const olSubject = document.getElementById('ol-subject').value.trim();
+ if (!olSubject) { showToast('제목을 입력해주세요.', 'warning'); return; }
+ const olBody = document.getElementById('ol-body').value.trim();
+ if (!olBody) { showToast('본문을 입력해주세요.', 'warning'); return; }
+
+ formContent = {
+ doc_number: document.getElementById('ol-doc-number').value.trim(),
+ doc_date: document.getElementById('ol-doc-date').value,
+ recipient: olRecipient, reference: document.getElementById('ol-reference').value.trim(),
+ subject: olSubject, body: olBody,
+ attachments_desc: document.getElementById('ol-attachments-desc').value.trim(),
+ company_name: document.getElementById('ol-company-name').value,
+ ceo_name: document.getElementById('ol-ceo-name').value,
+ company_address: document.getElementById('ol-company-address').value,
+ phone: document.getElementById('ol-phone-input')?.value || document.getElementById('ol-phone').value,
+ fax: document.getElementById('ol-fax-input')?.value || document.getElementById('ol-fax').value,
+ email: document.getElementById('ol-email-input')?.value || document.getElementById('ol-email').value,
+ };
+ formBody = null;
} else if (isDelegationForm) {
const dlAgentName = document.getElementById('dl-agent-name').value.trim();
if (!dlAgentName) {
@@ -1710,5 +1766,71 @@ function printQuotationPreview() {
win.document.close();
win.print();
}
+
+// ─── 공문서 미리보기 ───
+function buildOfficialLetterPreviewHtml(data) {
+ const bodyHtml = (data.body || '').replace(/&/g,'&').replace(//g,'>').replace(/\n/g, '
');
+ const attachHtml = (data.attachments_desc || '').replace(/&/g,'&').replace(//g,'>').replace(/\n/g, '
');
+ return `
+
+
${data.company_name || ''}
+
+
+
문서번호 : ${data.doc_number || ''}
+
일자 : ${data.doc_date || ''}
+
수신 : ${data.recipient || ''}
+ ${data.reference ? `
참조 : ${data.reference}
` : ''}
+
제목 : ${data.subject || ''}
+
+ ${bodyHtml}
+ ${data.attachments_desc ? `붙임 :
${attachHtml}
` : ''}
+
+ ${data.company_name || ''}
+ 대표이사 ${data.ceo_name || ''}
+ [직인날인]
+
+
+ ${data.company_address ? `
${data.company_address}
` : ''}
+
${data.phone ? '전화 ' + data.phone : ''}${data.fax ? ' / 팩스 ' + data.fax : ''}${data.email ? ' / 이메일 ' + data.email : ''}
+
`;
+}
+
+function openOfficialLetterPreview() {
+ const data = {
+ doc_number: document.getElementById('ol-doc-number').value.trim(),
+ doc_date: document.getElementById('ol-doc-date').value,
+ recipient: document.getElementById('ol-recipient').value.trim(),
+ reference: document.getElementById('ol-reference').value.trim(),
+ subject: document.getElementById('ol-subject').value.trim(),
+ body: document.getElementById('ol-body').value.trim(),
+ attachments_desc: document.getElementById('ol-attachments-desc').value.trim(),
+ company_name: document.getElementById('ol-company-name').value,
+ ceo_name: document.getElementById('ol-ceo-name').value,
+ company_address: document.getElementById('ol-company-address').value,
+ phone: document.getElementById('ol-phone-input')?.value || '',
+ fax: document.getElementById('ol-fax-input')?.value || '',
+ email: document.getElementById('ol-email-input')?.value || '',
+ };
+ document.getElementById('official-letter-preview-content').innerHTML = buildOfficialLetterPreviewHtml(data);
+ document.getElementById('official-letter-preview-modal').style.display = '';
+ document.body.style.overflow = 'hidden';
+}
+
+function closeOfficialLetterPreview() {
+ document.getElementById('official-letter-preview-modal').style.display = 'none';
+ document.body.style.overflow = '';
+}
+
+function printOfficialLetterPreview() {
+ const content = document.getElementById('official-letter-preview-content').innerHTML;
+ const win = window.open('', '_blank');
+ win.document.write('공문서');
+ win.document.write('');
+ win.document.write('');
+ win.document.write(content);
+ win.document.write('');
+ win.document.close();
+ win.print();
+}
@endpush
diff --git a/resources/views/approvals/partials/_official-letter-form.blade.php b/resources/views/approvals/partials/_official-letter-form.blade.php
new file mode 100644
index 00000000..c408bddc
--- /dev/null
+++ b/resources/views/approvals/partials/_official-letter-form.blade.php
@@ -0,0 +1,174 @@
+{{--
+ 공문서 전용 폼
+ Props:
+ $tenantInfo (array) - 테넌트(회사) 정보
+--}}
+@php
+ $tenantInfo = $tenantInfo ?? [];
+@endphp
+
+
+
+{{-- 공문서 미리보기 모달 --}}
+
+
+
+
+
+
공문서 미리보기
+
+
+
+
+
+
+
+
+
diff --git a/resources/views/approvals/partials/_official-letter-show.blade.php b/resources/views/approvals/partials/_official-letter-show.blade.php
new file mode 100644
index 00000000..448747cd
--- /dev/null
+++ b/resources/views/approvals/partials/_official-letter-show.blade.php
@@ -0,0 +1,179 @@
+{{--
+ 공문서 읽기전용 렌더링
+ Props:
+ $content (array) - approvals.content JSON
+--}}
+
+ {{-- 미리보기 버튼 --}}
+
+
+ {{-- 문서 정보 --}}
+
+
+
문서 정보
+
+
+
+
+
문서번호
+
{{ $content['doc_number'] ?? '-' }}
+
+
+
일자
+
{{ $content['doc_date'] ?? '-' }}
+
+
+
+
+
+ {{-- 수신/참조/제목 --}}
+
+
+
수신 정보
+
+
+
+
+
수신
+
{{ $content['recipient'] ?? '-' }}
+
+ @if(!empty($content['reference']))
+
+
참조
+
{{ $content['reference'] }}
+
+ @endif
+
+
+
제목
+
{{ $content['subject'] ?? '-' }}
+
+
+
+
+ {{-- 본문 --}}
+
+
+
본문
+
+
+
{{ $content['body'] ?? '' }}
+
+
+
+ {{-- 붙임 --}}
+ @if(!empty($content['attachments_desc']))
+
+
+
붙임
+
+
+
{{ $content['attachments_desc'] }}
+
+
+ @endif
+
+ {{-- 발신자 --}}
+
+
+
발신자
+
+
+
+
+
상호
+
{{ $content['company_name'] ?? '-' }}
+
+
+
대표이사
+
{{ $content['ceo_name'] ?? '-' }}
+
+
+
+
+
+
+{{-- 미리보기 모달 --}}
+
+
+
+
+
+
공문서 미리보기
+
+
+
+
+
+
+
+
+
+
+@push('scripts')
+
+@endpush
diff --git a/resources/views/approvals/show.blade.php b/resources/views/approvals/show.blade.php
index 0c709b8d..3615eea5 100644
--- a/resources/views/approvals/show.blade.php
+++ b/resources/views/approvals/show.blade.php
@@ -121,6 +121,8 @@ class="bg-gray-600 hover:bg-gray-700 text-white px-4 py-2 rounded-lg transition
@include('approvals.partials._board-minutes-show', ['content' => $approval->content])
@elseif(!empty($approval->content) && $approval->form?->code === 'quotation')
@include('approvals.partials._quotation-show', ['content' => $approval->content])
+ @elseif(!empty($approval->content) && $approval->form?->code === 'official_letter')
+ @include('approvals.partials._official-letter-show', ['content' => $approval->content])
@elseif($approval->body && preg_match('/<[a-z][\s\S]*>/i', $approval->body))
{!! strip_tags($approval->body, '