From d43cb4bc9b95227d5ee2f27f7927431d461c2788 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=B3=B4=EA=B3=A4?= Date: Fri, 6 Mar 2026 23:38:55 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20[approval]=20=EA=B3=B5=EB=AC=B8?= =?UTF-8?q?=EC=84=9C=20=EC=96=91=EC=8B=9D=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 공문서 전용 폼/조회 파셜 추가 - create/edit/show 페이지에 공문서 통합 - 문서번호, 수신, 참조, 제목, 본문, 붙임 입력 - 발신자 정보 테넌트에서 자동 로드 - 미리보기/인쇄 기능 (공문서 형식) --- resources/views/approvals/create.blade.php | 111 ++++++++++- resources/views/approvals/edit.blade.php | 128 ++++++++++++- .../partials/_official-letter-form.blade.php | 174 +++++++++++++++++ .../partials/_official-letter-show.blade.php | 179 ++++++++++++++++++ resources/views/approvals/show.blade.php | 2 + 5 files changed, 589 insertions(+), 5 deletions(-) create mode 100644 resources/views/approvals/partials/_official-letter-form.blade.php create mode 100644 resources/views/approvals/partials/_official-letter-show.blade.php 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, '