일반견적서 템플릿 변경, 견적서 pdf 변환하여 메일 첨부

This commit is contained in:
2025-10-29 17:59:19 +09:00
parent 399062a9a0
commit 50baa3d75e
7 changed files with 621 additions and 153 deletions

View File

@@ -42,6 +42,17 @@
<property name="suffix" value=".jsp"/>
<property name="order" value="1"/>
</bean>
<!-- MultipartResolver 설정 (파일 업로드) -->
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
<!-- 최대 업로드 크기: 50MB -->
<property name="maxUploadSize" value="52428800"/>
<!-- 메모리에서 처리할 최대 크기: 1MB -->
<property name="maxInMemorySize" value="1048576"/>
<!-- 기본 인코딩 -->
<property name="defaultEncoding" value="UTF-8"/>
</bean>
<bean id="jacksonMessageConverter" class="org.springframework.http.converter.json.MappingJacksonHttpMessageConverter"></bean>
<bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter">
<property name="messageConverters">

View File

@@ -668,21 +668,169 @@ function fn_sendEstimateMail(contractObjId){
return;
}
// 1단계: 견적서 템플릿 정보 조회
Swal.fire({
title: '메일 발송 중...',
title: '견적서 조회 중...',
text: '잠시만 기다려주세요.',
allowOutsideClick: false,
didOpen: () => {
onOpen: () => {
Swal.showLoading();
}
});
$.ajax({
url: "/contractMgmt/sendEstimateMail.do",
url: "/contractMgmt/getEstimateTemplateList.do",
type: "POST",
data: { objId: contractObjId },
dataType: "json",
success: function(data){
if(data.result === "success" && data.list && data.list.length > 0){
// 최종 차수 견적서 찾기
var latestEstimate = data.list[0]; // 이미 차수 내림차순으로 정렬되어 있음
var templateObjId = latestEstimate.OBJID || latestEstimate.objid;
var templateType = latestEstimate.TEMPLATE_TYPE || latestEstimate.template_type || latestEstimate.templateType;
// 2단계: 견적서 페이지를 새 창으로 열고 PDF 생성
fn_generatePdfAndSendMail(contractObjId, templateObjId, templateType);
} else {
Swal.close();
Swal.fire({
title: '오류',
text: '견적서를 찾을 수 없습니다.',
icon: 'error'
});
}
},
error: function(xhr, status, error){
Swal.close();
console.error("견적서 조회 오류:", xhr, status, error);
Swal.fire({
title: '오류',
text: '견적서 조회 중 오류가 발생했습니다.',
icon: 'error'
});
}
});
}
// PDF 생성 및 메일 발송
function fn_generatePdfAndSendMail(contractObjId, templateObjId, templateType){
Swal.fire({
title: 'PDF 생성 중...',
text: '견적서를 PDF로 변환하고 있습니다.',
allowOutsideClick: false,
onOpen: () => {
Swal.showLoading();
}
});
// 견적서 페이지 URL 생성
var url = "";
if(templateType === "1"){
url = "/contractMgmt/estimateTemplate1.do?templateObjId=" + templateObjId;
} else if(templateType === "2"){
url = "/contractMgmt/estimateTemplate2.do?templateObjId=" + templateObjId;
}
// 숨겨진 iframe으로 페이지 로드
var iframe = $('<iframe>', {
id: 'pdfGeneratorFrame',
src: url,
style: 'position:absolute;width:0;height:0;border:none;'
}).appendTo('body');
// iframe 로드 완료 대기
iframe.on('load', function(){
try {
var iframeWindow = this.contentWindow;
// iframe 내의 PDF 생성 함수 호출
if(typeof iframeWindow.fn_generateAndUploadPdf === 'function'){
iframeWindow.fn_generateAndUploadPdf(function(pdfBlob){
// iframe 제거
$('#pdfGeneratorFrame').remove();
if(pdfBlob){
// PDF Blob과 함께 메일 발송 요청
fn_sendMailWithPdf(contractObjId, pdfBlob);
} else {
Swal.close();
Swal.fire({
title: '오류',
text: 'PDF 생성에 실패했습니다.',
icon: 'error'
});
}
});
} else {
// iframe 제거
$('#pdfGeneratorFrame').remove();
Swal.close();
Swal.fire({
title: '오류',
text: '견적서 페이지 로드에 실패했습니다.',
icon: 'error'
});
}
} catch(e) {
console.error('PDF 생성 오류:', e);
$('#pdfGeneratorFrame').remove();
Swal.close();
Swal.fire({
title: '오류',
text: 'PDF 생성 중 오류가 발생했습니다.',
icon: 'error'
});
}
});
// 타임아웃 설정 (30초)
setTimeout(function(){
if($('#pdfGeneratorFrame').length > 0){
$('#pdfGeneratorFrame').remove();
Swal.close();
Swal.fire({
title: '타임아웃',
text: 'PDF 생성 시간이 초과되었습니다.',
icon: 'error'
});
}
}, 30000);
}
// PDF Blob과 함께 메일 발송 (FormData 사용)
function fn_sendMailWithPdf(contractObjId, pdfBlob){
console.log('===== 메일 발송 시작 =====');
console.log('contractObjId:', contractObjId);
console.log('PDF Blob 크기:', pdfBlob ? pdfBlob.size : 0, 'bytes');
console.log('========================');
Swal.fire({
title: '메일 발송 중...',
text: '잠시만 기다려주세요.',
allowOutsideClick: false,
onOpen: () => {
Swal.showLoading();
}
});
// FormData 생성
var formData = new FormData();
formData.append('objId', contractObjId);
formData.append('pdfFile', pdfBlob, 'estimate.pdf');
$.ajax({
url: "/contractMgmt/sendEstimateMail.do",
type: "POST",
data: formData,
processData: false, // FormData 사용 시 필수
contentType: false, // FormData 사용 시 필수
dataType: "json",
timeout: 60000, // 60초 타임아웃
success: function(data){
console.log('메일 발송 응답:', data);
Swal.close();
if(data.result === "success"){
Swal.fire({
@@ -706,9 +854,10 @@ function fn_sendEstimateMail(contractObjId){
error: function(xhr, status, error){
Swal.close();
console.error("메일 발송 오류:", xhr, status, error);
console.error("xhr.responseText:", xhr.responseText);
Swal.fire({
title: '오류',
text: '메일 발송 중 시스템 오류가 발생했습니다.',
text: '메일 발송 중 시스템 오류가 발생했습니다: ' + status,
icon: 'error',
confirmButtonText: '확인'
});

View File

@@ -165,9 +165,10 @@ body {
.items-table th,
.items-table td {
border: 1px solid #000;
padding: 6px 8px;
padding: 3px 5px;
text-align: center;
font-size: 9pt;
line-height: 1.3;
}
.items-table th {
@@ -265,11 +266,12 @@ textarea {
textarea {
resize: vertical;
min-height: 30px;
min-height: 25px;
padding: 2px;
}
.editable {
background-color: #fffef0;
background-color: transparent;
}
@media print {
@@ -462,8 +464,8 @@ function fn_calculateAmount(row) {
function fn_calculateTotal() {
var total = 0;
// 품목 행만 순회 (계 행, 원화환산 행, 비고 행 제외)
$("#itemsTableBody tr").not(".total-row, .total-krw-row, .remarks-row").each(function(){
// 품목 행만 순회 (계 행, 원화환산 행, 비고 행, 참조사항 행, 회사명 행 제외)
$("#itemsTableBody tr").not(".total-row, .total-krw-row, .remarks-row, .notes-row, .footer-row").each(function(){
var amount = $(this).find(".item-amount").val() || "0";
// 콤마와 통화 기호 제거 후 숫자로 변환
amount = amount.replace(/,/g, "").replace(/₩/g, "").replace(/\$/g, "").replace(/€/g, "").replace(/¥/g, "");
@@ -580,8 +582,8 @@ function fn_initItemDescSelect(itemId) {
// 행 추가 함수
function fn_addItemRow() {
// 계 행, 원화환산 행, 비고 행 제외하고 품목 행 개수 계산
var itemRows = $("#itemsTableBody tr").not(".total-row, .total-krw-row, .remarks-row");
// 계 행, 원화환산 행, 비고 행, 참조사항 행, 회사명 행 제외하고 품목 행 개수 계산
var itemRows = $("#itemsTableBody tr").not(".total-row, .total-krw-row, .remarks-row, .notes-row, .footer-row");
var nextNo = itemRows.length + 1;
// 새 행 생성
@@ -856,12 +858,6 @@ function fn_loadTemplateData(templateObjId){
$("#manager_contact").val(managerContact);
}
// 하단 비고 로드
$("#note1").val(note1);
$("#note2").val(note2);
$("#note3").val(note3);
$("#note4").val(note4);
// 테이블 내 비고는 나중에 설정 (textarea 생성 후)
var noteRemarks = template.NOTE_REMARKS || template.note_remarks || template.noteRemarks || "";
@@ -930,6 +926,24 @@ function fn_loadTemplateData(templateObjId){
itemsHtml += '</td>';
itemsHtml += '</tr>';
// 참조사항 행 추가
itemsHtml += '<tr class="notes-row">';
itemsHtml += '<td colspan="8" style="vertical-align: top; padding: 10px; text-align: left; border: 1px solid #000;">';
itemsHtml += '<div style="font-weight: bold; margin-bottom: 10px; text-align: left;">&lt;참조사항&gt;</div>';
itemsHtml += '<div class="editable" style="margin-bottom: 5px;"><input type="text" id="note1" value="1. 견적유효기간: 일" style="width: 100%; border: none; background: transparent; font-size: 10pt;"></div>';
itemsHtml += '<div class="editable" style="margin-bottom: 5px;"><input type="text" id="note2" value="2. 납품기간: 발주 후 1주 이내" style="width: 100%; border: none; background: transparent; font-size: 10pt;"></div>';
itemsHtml += '<div class="editable" style="margin-bottom: 5px;"><input type="text" id="note3" value="3. VAT 별도" style="width: 100%; border: none; background: transparent; font-size: 10pt;"></div>';
itemsHtml += '<div class="editable" style="margin-bottom: 5px;"><input type="text" id="note4" value="4. 결제 조건 : 기존 결제조건에 따름." style="width: 100%; border: none; background: transparent; font-size: 10pt;"></div>';
itemsHtml += '</td>';
itemsHtml += '</tr>';
// 하단 회사명 행 추가
itemsHtml += '<tr class="footer-row">';
itemsHtml += '<td colspan="8" style="text-align: right; padding: 15px; font-size: 10pt; font-weight: bold; border: none;">';
itemsHtml += '㈜알피에스';
itemsHtml += '</td>';
itemsHtml += '</tr>';
// HTML 삽입
$("#itemsTableBody").html(itemsHtml);
@@ -976,11 +990,17 @@ function fn_loadTemplateData(templateObjId){
fn_initItemDescSelect(itemId);
}
// 테이블 내 비고 값 설정 (textarea 생성 직후)
$("#note_remarks").val(noteRemarks);
// 합계 계산
fn_calculateTotal();
// 테이블 내 비고 값 설정 (textarea 생성 직후)
$("#note_remarks").val(noteRemarks);
// 참조사항 값 설정 (input 생성 직후)
$("#note1").val(note1 || "1. 견적유효기간: 일");
$("#note2").val(note2 || "2. 납품기간: 발주 후 1주 이내");
$("#note3").val(note3 || "3. VAT 별도");
$("#note4").val(note4 || "4. 결제 조건 : 기존 결제조건에 따름.");
// 합계 계산
fn_calculateTotal();
// 결재상태에 따라 버튼 제어
fn_controlButtons();
@@ -1025,8 +1045,8 @@ function fn_loadCustomerContact(customerObjId) {
function fn_save() {
var items = [];
// 계 행, 원화환산 행, 비고 행 제외하고 품목 행만 저장
$("#itemsTableBody tr").not(".total-row, .total-krw-row, .remarks-row").each(function(idx) {
// 계 행, 원화환산 행, 비고 행, 참조사항 행, 회사명 행 제외하고 품목 행만 저장
$("#itemsTableBody tr").not(".total-row, .total-krw-row, .remarks-row, .notes-row, .footer-row").each(function(idx) {
var row = $(this);
var quantity = row.find(".item-qty").val() || "";
var unitPrice = row.find(".item-price").val() || "";
@@ -1128,6 +1148,7 @@ function fn_save() {
<colgroup>
<col width="80px" />
<col width="*" />
<col width="50px" />
<col width="300px" />
</colgroup>
<tr>
@@ -1135,6 +1156,7 @@ function fn_save() {
<td class="editable">
<input type="text" id="executor" class="date_icon" value="" style="width: 150px;">
</td>
<td rowspan="4" style="border: none"></td>
<td rowspan="4" style="text-align: center; border: none; vertical-align: middle; padding: 0;">
<div style="width: 100%; text-align: center; margin-bottom: 5px;">
<img src="/images/company_stamp.png" alt="회사 도장" style="width: 100%; height: auto;"
@@ -1143,10 +1165,7 @@ function fn_save() {
<div class="company-stamp-text">㈊알피에스<br>RPS CO., LTD<br>대표이사이동준</div>
</div>
</div>
<div style="text-align: left; font-size: 9pt; line-height: 1.8; padding: 0 5px;">
담당자 : <input type="text" id="manager_name" value="" readonly style="width: 120px; border: none; border-bottom: 1px solid #ddd; font-size: 9pt; padding: 2px; background-color: #f5f5f5;"><br>
연락처 : <input type="text" id="manager_contact" value="" readonly style="width: 120px; border: none; border-bottom: 1px solid #ddd; font-size: 9pt; padding: 2px; background-color: #f5f5f5;">
</div>
</td>
</tr>
<tr>
@@ -1173,15 +1192,19 @@ function fn_save() {
</table>
<!-- 인사말 -->
<div style="margin-bottom: 10px; padding: 0px 5px; line-height: 1.6; font-size: 10pt;">
<div style="display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 10px; padding: 0px 5px;">
<!-- 왼쪽: 인사말 -->
<div style="line-height: 1.6; font-size: 10pt;">
견적을 요청해 주셔서 대단히 감사합니다.<br>
하기와 같이 견적서를 제출합니다.
</div>
<!-- 부가세 별도 표시 -->
<div style="text-align: right; margin-top: -30px; margin-bottom: 20px; padding-right: 10px; font-size: 10pt;">
부가세 별도
<!-- 오른쪽: 담당자 정보 및 부가세 별도 -->
<div style="text-align: right; font-size: 9pt; line-height: 1.8;">
담당자 : <input type="text" id="manager_name" value="" readonly style="width: 120px; border: none; border-bottom: 1px solid #ddd; font-size: 9pt; padding: 2px; background-color: #f5f5f5;"><br>
연락처 : <input type="text" id="manager_contact" value="" readonly style="width: 120px; border: none; border-bottom: 1px solid #ddd; font-size: 9pt; padding: 2px; background-color: #f5f5f5;"><br><br>
<span style="font-size: 10pt; margin-top: 5px; display: inline-block;">부가세 별도</span>
</div>
</div>
<!-- 품목 테이블 -->
<table class="items-table">
@@ -1245,31 +1268,224 @@ function fn_save() {
<textarea id="note_remarks" style="width: 100%; height: 70px; border: none; resize: none; font-family: inherit; font-size: 10pt; text-align: left;"></textarea>
</td>
</tr>
<!-- 참조사항 행 -->
<tr class="notes-row">
<td colspan="8" style="vertical-align: top; padding: 10px; text-align: left; border: 1px solid #000;">
<div style="font-weight: bold; margin-bottom: 10px; text-align: left;">&lt;참조사항&gt;</div>
<div class="editable" style="margin-bottom: 5px;"><input type="text" id="note1" value="1. 견적유효기간: 일" style="width: 100%; border: none; background: transparent; font-size: 10pt;"></div>
<div class="editable" style="margin-bottom: 5px;"><input type="text" id="note2" value="2. 납품기간: 발주 후 1주 이내" style="width: 100%; border: none; background: transparent; font-size: 10pt;"></div>
<div class="editable" style="margin-bottom: 5px;"><input type="text" id="note3" value="3. VAT 별도" style="width: 100%; border: none; background: transparent; font-size: 10pt;"></div>
<div class="editable" style="margin-bottom: 5px;"><input type="text" id="note4" value="4. 결제 조건 : 기존 결제조건에 따름." style="width: 100%; border: none; background: transparent; font-size: 10pt;"></div>
</td>
</tr>
<!-- 하단 회사명 행 -->
<tr class="footer-row">
<td colspan="8" style="text-align: right; padding: 15px; font-size: 10pt; font-weight: bold; border: none;">
㈜알피에스
</td>
</tr>
</tbody>
</table>
<!-- 참조사항 섹션 -->
<div class="notes-section">
<div class="notes-title">&lt;참조사항&gt;</div>
<div class="editable"><input type="text" id="note1" value="1. 견적유효기간: 일"></div>
<div class="editable"><input type="text" id="note2" value="2. 납품기간: 발주 후 1주 이내"></div>
<div class="editable"><input type="text" id="note3" value="3. VAT 별도"></div>
<div class="editable"><input type="text" id="note4" value="4. 결제 조건 : 기존 결제조건에 따름."></div>
</div>
<!-- 하단 회사명 -->
<div class="footer-company">
㈜알피에스
</div>
</div>
<!-- 버튼 영역 -->
<div class="btn-area no-print">
<button type="button" id="btnAddRow" class="estimate-btn">행 추가</button>
<button type="button" id="btnPrint" class="estimate-btn">인쇄</button>
<button type="button" id="btnDownloadPdf" class="estimate-btn">PDF 다운로드</button>
<button type="button" id="btnSave" class="estimate-btn">저장</button>
<button type="button" id="btnClose" class="estimate-btn">닫기</button>
</div>
<!-- html2canvas 및 jsPDF 라이브러리 -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/1.5.3/jspdf.min.js"></script>
<script>
// PDF 다운로드 버튼 클릭 이벤트
$("#btnDownloadPdf").click(function(){
fn_generatePdf();
});
// PDF 생성 함수
function fn_generatePdf() {
// 라이브러리 로드 확인
if(typeof html2canvas === 'undefined') {
Swal.fire({
title: '오류',
text: 'html2canvas 라이브러리가 로드되지 않았습니다.',
icon: 'error'
});
return;
}
if(typeof jsPDF === 'undefined') {
Swal.fire({
title: '오류',
text: 'jsPDF 라이브러리가 로드되지 않았습니다.',
icon: 'error'
});
return;
}
Swal.fire({
title: 'PDF 생성 중...',
text: '잠시만 기다려주세요.',
allowOutsideClick: false,
onOpen: () => {
Swal.showLoading();
}
});
// 버튼 영역 임시 숨김
$('.btn-area').hide();
// 견적서 컨테이너 캡처
html2canvas(document.querySelector('.estimate-container'), {
scale: 2, // 고해상도
useCORS: true,
logging: true, // 디버깅을 위해 true로 변경
backgroundColor: '#ffffff'
}).then(function(canvas) {
// 버튼 영역 다시 표시
$('.btn-area').show();
try {
// Canvas를 이미지로 변환
var imgData = canvas.toDataURL('image/png');
// PDF 생성 (A4 크기)
var pdf = new jsPDF('p', 'mm', 'a4');
var imgWidth = 210; // A4 width in mm
var pageHeight = 297; // A4 height in mm
var imgHeight = canvas.height * imgWidth / canvas.width;
var heightLeft = imgHeight;
var position = 0;
// 첫 페이지 추가
pdf.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight);
heightLeft -= pageHeight;
// 페이지가 넘어가면 추가 페이지 생성
while (heightLeft >= 0) {
position = heightLeft - imgHeight;
pdf.addPage();
pdf.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight);
heightLeft -= pageHeight;
}
// 파일명 생성
var estimateNo = $("#estimate_no").val() || "견적서";
var fileName = estimateNo + '.pdf';
// PDF 다운로드
pdf.save(fileName);
Swal.close();
Swal.fire({
title: 'PDF 생성 완료',
text: 'PDF 파일이 다운로드되었습니다.',
icon: 'success',
timer: 2000
});
} catch(pdfError) {
Swal.close();
console.error('PDF 생성 오류:', pdfError);
Swal.fire({
title: '오류',
text: 'PDF 생성 중 오류가 발생했습니다: ' + pdfError.message,
icon: 'error'
});
}
}).catch(function(error) {
$('.btn-area').show();
Swal.close();
console.error('Canvas 캡처 오류:', error);
Swal.fire({
title: '오류',
text: '페이지 캡처 중 오류가 발생했습니다: ' + error.message,
icon: 'error'
});
});
}
// PDF를 Blob으로 생성하여 서버로 전송하는 함수
function fn_generateAndUploadPdf(callback) {
console.log('fn_generateAndUploadPdf 호출됨');
// 라이브러리 로드 확인
if(typeof html2canvas === 'undefined' || typeof jsPDF === 'undefined') {
console.error('필요한 라이브러리가 로드되지 않았습니다.');
if(callback && typeof callback === 'function') {
callback(null);
}
return;
}
// 버튼 영역 임시 숨김
$('.btn-area').hide();
// 견적서 컨테이너 캡처
html2canvas(document.querySelector('.estimate-container'), {
scale: 2,
useCORS: true,
logging: true,
backgroundColor: '#ffffff'
}).then(function(canvas) {
console.log('Canvas 캡처 완료');
// 버튼 영역 다시 표시
$('.btn-area').show();
try {
// Canvas를 이미지로 변환
var imgData = canvas.toDataURL('image/png');
console.log('이미지 변환 완료');
// PDF 생성
var pdf = new jsPDF('p', 'mm', 'a4');
var imgWidth = 210;
var pageHeight = 297;
var imgHeight = canvas.height * imgWidth / canvas.width;
var heightLeft = imgHeight;
var position = 0;
pdf.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight);
heightLeft -= pageHeight;
while (heightLeft >= 0) {
position = heightLeft - imgHeight;
pdf.addPage();
pdf.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight);
heightLeft -= pageHeight;
}
console.log('PDF 생성 완료');
// PDF를 Blob으로 변환 (Base64 대신 바이너리)
var pdfBlob = pdf.output('blob');
console.log('PDF Blob 생성 완료, 크기:', pdfBlob.size, 'bytes');
// 콜백 함수 호출 (메일 발송 등에 사용)
if(callback && typeof callback === 'function') {
callback(pdfBlob);
}
} catch(pdfError) {
console.error('PDF 생성 중 오류:', pdfError);
if(callback && typeof callback === 'function') {
callback(null);
}
}
}).catch(function(error) {
$('.btn-area').show();
console.error('Canvas 캡처 오류:', error);
if(callback && typeof callback === 'function') {
callback(null);
}
});
}
</script>
</body>
</html>