Revert: PDF 변환 기능 되돌림 (4a9577c)

This commit is contained in:
2025-10-30 18:11:45 +09:00
parent 4d56d649e1
commit d1a3ae950d
7 changed files with 152 additions and 620 deletions

View File

@@ -42,17 +42,6 @@
<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,169 +668,21 @@ function fn_sendEstimateMail(contractObjId){
return;
}
// 1단계: 견적서 템플릿 정보 조회
Swal.fire({
title: '견적서 조회 중...',
text: '잠시만 기다려주세요.',
allowOutsideClick: false,
onOpen: () => {
Swal.showLoading();
}
});
$.ajax({
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: () => {
didOpen: () => {
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 사용 시 필수
data: { objId: contractObjId },
dataType: "json",
timeout: 60000, // 60초 타임아웃
success: function(data){
console.log('메일 발송 응답:', data);
Swal.close();
if(data.result === "success"){
Swal.fire({
@@ -854,10 +706,9 @@ function fn_sendMailWithPdf(contractObjId, pdfBlob){
error: function(xhr, status, error){
Swal.close();
console.error("메일 발송 오류:", xhr, status, error);
console.error("xhr.responseText:", xhr.responseText);
Swal.fire({
title: '오류',
text: '메일 발송 중 시스템 오류가 발생했습니다: ' + status,
text: '메일 발송 중 시스템 오류가 발생했습니다.',
icon: 'error',
confirmButtonText: '확인'
});

View File

@@ -165,10 +165,9 @@ body {
.items-table th,
.items-table td {
border: 1px solid #000;
padding: 3px 5px;
padding: 6px 8px;
text-align: center;
font-size: 9pt;
line-height: 1.3;
}
.items-table th {
@@ -266,12 +265,11 @@ textarea {
textarea {
resize: vertical;
min-height: 25px;
padding: 2px;
min-height: 30px;
}
.editable {
background-color: transparent;
background-color: #fffef0;
}
@media print {
@@ -464,8 +462,8 @@ function fn_calculateAmount(row) {
function fn_calculateTotal() {
var total = 0;
// 품목 행만 순회 (계 행, 원화환산 행, 비고 행, 참조사항 행, 회사명 행 제외)
$("#itemsTableBody tr").not(".total-row, .total-krw-row, .remarks-row, .notes-row, .footer-row").each(function(){
// 품목 행만 순회 (계 행, 원화환산 행, 비고 행 제외)
$("#itemsTableBody tr").not(".total-row, .total-krw-row, .remarks-row").each(function(){
var amount = $(this).find(".item-amount").val() || "0";
// 콤마와 통화 기호 제거 후 숫자로 변환
amount = amount.replace(/,/g, "").replace(/₩/g, "").replace(/\$/g, "").replace(/€/g, "").replace(/¥/g, "");
@@ -582,8 +580,8 @@ function fn_initItemDescSelect(itemId) {
// 행 추가 함수
function fn_addItemRow() {
// 계 행, 원화환산 행, 비고 행, 참조사항 행, 회사명 행 제외하고 품목 행 개수 계산
var itemRows = $("#itemsTableBody tr").not(".total-row, .total-krw-row, .remarks-row, .notes-row, .footer-row");
// 계 행, 원화환산 행, 비고 행 제외하고 품목 행 개수 계산
var itemRows = $("#itemsTableBody tr").not(".total-row, .total-krw-row, .remarks-row");
var nextNo = itemRows.length + 1;
// 새 행 생성
@@ -858,6 +856,12 @@ 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 || "";
@@ -926,24 +930,6 @@ 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);
@@ -990,17 +976,11 @@ function fn_loadTemplateData(templateObjId){
fn_initItemDescSelect(itemId);
}
// 테이블 내 비고 값 설정 (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();
// 테이블 내 비고 값 설정 (textarea 생성 직후)
$("#note_remarks").val(noteRemarks);
// 합계 계산
fn_calculateTotal();
// 결재상태에 따라 버튼 제어
fn_controlButtons();
@@ -1045,8 +1025,8 @@ function fn_loadCustomerContact(customerObjId) {
function fn_save() {
var items = [];
// 계 행, 원화환산 행, 비고 행, 참조사항 행, 회사명 행 제외하고 품목 행만 저장
$("#itemsTableBody tr").not(".total-row, .total-krw-row, .remarks-row, .notes-row, .footer-row").each(function(idx) {
// 계 행, 원화환산 행, 비고 행 제외하고 품목 행만 저장
$("#itemsTableBody tr").not(".total-row, .total-krw-row, .remarks-row").each(function(idx) {
var row = $(this);
var quantity = row.find(".item-qty").val() || "";
var unitPrice = row.find(".item-price").val() || "";
@@ -1148,7 +1128,6 @@ function fn_save() {
<colgroup>
<col width="80px" />
<col width="*" />
<col width="50px" />
<col width="300px" />
</colgroup>
<tr>
@@ -1156,7 +1135,6 @@ 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;"
@@ -1165,7 +1143,10 @@ 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>
@@ -1192,19 +1173,15 @@ function fn_save() {
</table>
<!-- 인사말 -->
<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;">
<div style="margin-bottom: 10px; padding: 0px 5px; line-height: 1.6; font-size: 10pt;">
견적을 요청해 주셔서 대단히 감사합니다.<br>
하기와 같이 견적서를 제출합니다.
</div>
<!-- 오른쪽: 담당자 정보 및 부가세 별도 -->
<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 style="text-align: right; margin-top: -30px; margin-bottom: 20px; padding-right: 10px; font-size: 10pt;">
부가세 별도
</div>
</div>
<!-- 품목 테이블 -->
<table class="items-table">
@@ -1268,224 +1245,31 @@ 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>

View File

@@ -672,14 +672,11 @@ public class MailUtil {
//◆첨부파일
if(attachFileList != null){
for(HashMap hmFile : attachFileList){
String filepath = CommonUtils.checkNull(hmFile.get(Constants.Db.COL_FILE_PATH)) + File.separator + CommonUtils.checkNull(hmFile.get(Constants.Db.COL_FILE_SAVED_NAME));
String filepath = CommonUtils.checkNull(hmFile.get(Constants.Db.COL_FILE_PATH)) +"\\"+ CommonUtils.checkNull(hmFile.get(Constants.Db.COL_FILE_SAVED_NAME));
File attach_file = new File(filepath);
//String attach_fileName = attach_file.getName();
String attach_fileName = CommonUtils.checkNull(hmFile.get(Constants.Db.COL_FILE_REAL_NAME));
//System.out.println("첨부파일 처리: " + filepath);
//System.out.println("파일 존재 여부: " + attach_file.exists());
//첨부파일용 MimeBodyPart 객체생성
MimeBodyPart mbp_attachFile = new MimeBodyPart();
mbp_attachFile.setFileName(MimeUtility.encodeText(attach_fileName, "euc-kr", "B"));

View File

@@ -2071,34 +2071,25 @@ public class ContractMgmtController {
}
/**
* 견적서 메일 발송 (AJAX) - PDF 파일 업로드 방식
* 견적서 메일 발송 (AJAX)
* @param request
* @param paramMap - objId (CONTRACT_OBJID)
* @param pdfFile - PDF 파일 (MultipartFile)
* @return
*/
@ResponseBody
@RequestMapping(value="/contractMgmt/sendEstimateMail.do", method=RequestMethod.POST)
public Map sendEstimateMail(HttpServletRequest request,
@RequestParam Map<String, Object> paramMap,
@RequestParam(value="pdfFile", required=false) org.springframework.web.multipart.MultipartFile pdfFile){
public Map sendEstimateMail(HttpServletRequest request, @RequestParam Map<String, Object> paramMap){
Map resultMap = new HashMap();
try {
String objId = CommonUtils.checkNull(paramMap.get("objId"));
if("".equals(objId) || "-1".equals(objId)){
resultMap.put("result", "error");
resultMap.put("message", "잘못된 요청입니다.");
return resultMap;
}
// PDF 파일을 paramMap에 추가
if(pdfFile != null && !pdfFile.isEmpty()) {
paramMap.put("pdfFile", pdfFile);
}
// 메일 발송 서비스 호출
resultMap = contractMgmtService.sendEstimateMail(request, paramMap);

View File

@@ -1551,9 +1551,9 @@ public class ContractMgmtService {
String customerName = CommonUtils.checkNull(contractInfo.get("customer_name"));
String subject = "[" + customerName + "] " + contractNo + " 견적서 [OBJID:" + objId + "]";
// 5. 메일 내용 생성 (간단한 텍스트 버전)
// 5. 메일 내용 생성
String contents = makeEstimateMailContents(contractInfo, estimateTemplate, estimateItems);
// 6. 수신자 정보 설정
ArrayList<String> toEmailList = new ArrayList<String>();
String customerEmail = CommonUtils.checkNull(contractInfo.get("customer_email"));
@@ -1571,65 +1571,6 @@ public class ContractMgmtService {
if(!"".equals(writerEmail)){
ccEmailList.add(writerEmail);
}
// 7-1. PDF 파일 처리 (MultipartFile로 전송된 경우)
ArrayList<HashMap> attachFileList = new ArrayList<HashMap>();
org.springframework.web.multipart.MultipartFile pdfFile =
(org.springframework.web.multipart.MultipartFile) paramMap.get("pdfFile");
System.out.println("===== PDF 파일 수신 확인 =====");
System.out.println("pdfFile 존재 여부: " + (pdfFile != null));
if(pdfFile != null) {
System.out.println("pdfFile 크기: " + pdfFile.getSize() + " bytes");
System.out.println("pdfFile 이름: " + pdfFile.getOriginalFilename());
}
System.out.println("==============================");
if(pdfFile != null && !pdfFile.isEmpty()) {
// MultipartFile을 임시 파일로 저장
try {
System.out.println("PDF 파일 저장 시작...");
// 견적번호를 파일명으로 사용
String estimateNo = CommonUtils.checkNull(estimateTemplate.get("estimate_no"));
if("".equals(estimateNo)) estimateNo = CommonUtils.checkNull(estimateTemplate.get("ESTIMATE_NO"));
if("".equals(estimateNo)) estimateNo = "견적서";
System.out.println("견적번호: " + estimateNo);
// 임시 디렉토리에 파일 저장
String tempDir = System.getProperty("java.io.tmpdir");
java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("yyyyMMddHHmmss");
String timestamp = sdf.format(new java.util.Date());
String fileName = estimateNo + "_" + timestamp + ".pdf";
String filePath = tempDir + File.separator + fileName;
File tempFile = new File(filePath);
pdfFile.transferTo(tempFile);
// JVM 종료 시 자동 삭제
tempFile.deleteOnExit();
System.out.println("PDF 파일 저장 성공: " + tempFile.getAbsolutePath());
// File 객체를 HashMap으로 변환 (MailUtil이 요구하는 형식)
HashMap<String, String> fileMap = new HashMap<String, String>();
fileMap.put(Constants.Db.COL_FILE_REAL_NAME, tempFile.getName());
fileMap.put(Constants.Db.COL_FILE_SAVED_NAME, tempFile.getName());
fileMap.put(Constants.Db.COL_FILE_PATH, tempFile.getParent());
attachFileList.add(fileMap);
System.out.println("===== PDF 파일 처리 완료 =====");
System.out.println("PDF 파일 경로: " + tempFile.getAbsolutePath());
System.out.println("PDF 파일 크기: " + tempFile.length() + " bytes");
System.out.println("=============================");
} catch(Exception pdfEx) {
System.out.println("PDF 처리 중 오류 발생: " + pdfEx.getMessage());
pdfEx.printStackTrace();
// PDF 처리 실패해도 메일은 발송 (첨부 없이)
}
}
// 8. 메일 발송
PersonBean person = (PersonBean)request.getSession().getAttribute(Constants.PERSON_BEAN);
@@ -1640,7 +1581,6 @@ public class ContractMgmtService {
System.out.println("To Email: " + toEmailList);
System.out.println("CC Email: " + ccEmailList);
System.out.println("Subject: " + subject);
System.out.println("첨부파일 개수: " + attachFileList.size());
System.out.println("========================");
boolean mailSent = false;
@@ -1655,7 +1595,7 @@ public class ContractMgmtService {
null, // important
subject,
contents,
attachFileList.size() > 0 ? attachFileList : null, // PDF 첨부
null, // attachFileList (TODO: PDF 첨부 구현)
"CONTRACT_ESTIMATE"
);
@@ -1703,27 +1643,25 @@ private String makeEstimateMailContents(Map contractInfo, Map estimateTemplate,
contents.append("<meta charset='UTF-8'>");
contents.append("<style>");
contents.append("body { font-family: 'Malgun Gothic', '맑은 고딕', Arial, sans-serif; margin: 0; padding: 20px; background-color: #f5f5f5; }");
contents.append(".estimate-container { width: 210mm; background: white; margin: 0 auto; padding: 20mm; box-shadow: 0 0 10px rgba(0,0,0,0.1); box-sizing: border-box; }");
contents.append(".title { text-align: center; font-size: 28pt; font-weight: bold; letter-spacing: 20px; margin-bottom: 40px; padding: 10px 0; }");
contents.append(".estimate-container { max-width: 1200px; background: white; margin: 0 auto; padding: 40px; box-shadow: 0 0 10px rgba(0,0,0,0.1); }");
contents.append(".title { text-align: center; font-size: 24pt; font-weight: bold; letter-spacing: 10px; margin-bottom: 40px; border-bottom: 3px solid #333; padding-bottom: 20px; }");
contents.append(".header-section { width: 100%; margin-bottom: 30px; overflow: hidden; }");
contents.append(".header-left { float: left; width: 48%; }");
contents.append(".header-right { float: right; width: 48%; text-align: right; }");
contents.append(".info-table { width: 100%; border-collapse: collapse; margin-bottom: 20px; }");
contents.append(".info-table td { padding: 5px 8px; border: 1px solid #000; font-size: 9pt; }");
contents.append(".info-table .label { background-color: #f0f0f0; font-weight: bold; width: 80px; text-align: center; }");
contents.append(".company-info { display: inline-block; text-align: right; }");
contents.append(".company-stamp { width: 120px; height: 120px; border: 2px solid #e74c3c; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; margin-bottom: 10px; position: relative; }");
contents.append(".company-stamp-text { writing-mode: vertical-rl; font-size: 16pt; font-weight: bold; color: #e74c3c; letter-spacing: 3px; }");
contents.append(".company-details { font-size: 9pt; line-height: 1.6; margin-top: 10px; }");
contents.append(".greeting-section { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 10px; padding: 0px 5px; }");
contents.append(".greeting-left { line-height: 1.6; font-size: 10pt; }");
contents.append(".greeting-right { text-align: right; font-size: 9pt; line-height: 1.8; }");
contents.append(".items-table { width: 100%; border-collapse: collapse; margin-bottom: 30px; }");
contents.append(".items-table th, .items-table td { border: 1px solid #000; padding: 3px 5px; text-align: center; font-size: 9pt; line-height: 1.3; }");
contents.append(".items-table th { background-color: #f0f0f0; font-weight: bold; }");
contents.append(".text-left { text-align: left; }");
contents.append(".header-right { float: right; width: 48%; }");
contents.append(".info-table { width: 100%; border-collapse: collapse; }");
contents.append(".info-table td { padding: 10px; border: 1px solid #000; font-size: 10pt; vertical-align: middle; }");
contents.append(".info-table .label { background-color: #f0f0f0; font-weight: bold; width: 120px; text-align: center; }");
contents.append(".company-info { border: 2px solid #000; padding: 20px; text-align: center; min-height: 200px; box-sizing: border-box; }");
contents.append(".company-stamp-img { max-width: 100%; height: auto; margin: 0 auto 15px; display: block; }");
contents.append(".manager-info { margin-top: 15px; padding-top: 15px; border-top: 1px solid #ddd; text-align: left; font-size: 9pt; line-height: 1.8; }");
contents.append(".items-table { width: 100%; border-collapse: collapse; margin: 20px 0; clear: both; }");
contents.append(".items-table th, .items-table td { border: 1px solid #000; padding: 8px; font-size: 9pt; }");
contents.append(".items-table th { background-color: #e0e0e0; font-weight: bold; text-align: center; }");
contents.append(".text-right { text-align: right; }");
contents.append(".text-center { text-align: center; }");
contents.append(".note-section { margin-top: 30px; padding: 20px; background-color: #f9f9f9; border: 1px solid #ddd; clear: both; }");
contents.append(".note-section h4 { margin-top: 0; font-size: 11pt; }");
contents.append(".note-section div { margin: 10px 0; font-size: 9pt; line-height: 1.6; }");
contents.append("</style>");
contents.append("</head>");
contents.append("<body>");
@@ -1733,11 +1671,7 @@ private String makeEstimateMailContents(Map contractInfo, Map estimateTemplate,
contents.append("<div class='title'>견 적 서</div>");
// estimateTemplate 키는 대문자/소문자 모두 체크
// 수신처는 contractInfo에서 고객사명 가져오기
String recipient = CommonUtils.checkNull(contractInfo.get("customer_name"));
if("".equals(recipient)) recipient = CommonUtils.checkNull(contractInfo.get("CUSTOMER_NAME"));
// 없으면 estimateTemplate에서 가져오기
if("".equals(recipient)) recipient = CommonUtils.checkNull(estimateTemplate.get("recipient"));
String recipient = CommonUtils.checkNull(estimateTemplate.get("recipient"));
if("".equals(recipient)) recipient = CommonUtils.checkNull(estimateTemplate.get("RECIPIENT"));
String contactPerson = CommonUtils.checkNull(estimateTemplate.get("contact_person"));
@@ -1746,9 +1680,7 @@ private String makeEstimateMailContents(Map contractInfo, Map estimateTemplate,
String estimateNo = CommonUtils.checkNull(estimateTemplate.get("estimate_no"));
if("".equals(estimateNo)) estimateNo = CommonUtils.checkNull(estimateTemplate.get("ESTIMATE_NO"));
String executorDate = CommonUtils.checkNull(estimateTemplate.get("executor"));
if("".equals(executorDate)) executorDate = CommonUtils.checkNull(estimateTemplate.get("EXECUTOR"));
if("".equals(executorDate)) executorDate = CommonUtils.checkNull(estimateTemplate.get("executor_date"));
String executorDate = CommonUtils.checkNull(estimateTemplate.get("executor_date"));
if("".equals(executorDate)) executorDate = CommonUtils.checkNull(estimateTemplate.get("EXECUTOR_DATE"));
// 담당자 정보
@@ -1765,10 +1697,10 @@ private String makeEstimateMailContents(Map contractInfo, Map estimateTemplate,
// 왼쪽: 기본 정보 테이블
contents.append("<div class='header-left'>");
contents.append("<table class='info-table'>");
contents.append("<tr><td class='label'>시행일자</td><td>" + executorDate + "</td></tr>");
contents.append("<tr><td class='label'>수신처</td><td>" + recipient + "</td></tr>");
contents.append("<tr><td class='label'>수신인</td><td>" + contactPerson + "</td></tr>");
contents.append("<tr><td class='label'>견적번호</td><td>" + estimateNo + "</td></tr>");
contents.append("<tr><td class='label'>영업번호</td><td>" + CommonUtils.checkNull(contractInfo.get("contract_no")) + "</td></tr>");
contents.append("</table>");
contents.append("</div>");
@@ -1793,54 +1725,49 @@ private String makeEstimateMailContents(Map contractInfo, Map estimateTemplate,
String stampBase64 = !"".equals(companyStampPath) ? encodeImageToBase64(companyStampPath) : "";
if(!"".equals(stampBase64)) {
// 이미지가 있으면 Base64로 인코딩된 이미지 표시
contents.append("<img src='" + stampBase64 + "' alt='회사 도장' style='max-width: 150px; height: auto; margin: 0 auto 15px; display: block;'>");
// 이미지가 있으면 Base64로 인코딩된 이미지 표시
contents.append("<img src='" + stampBase64 + "' alt='회사 도장' class='company-stamp-img'>");
} else {
// 이미지가 없으면 텍스트로 표시
contents.append("<div class='company-stamp'>");
contents.append("<div class='company-stamp-text'>(주)알피에스<br>대표이사<br>이 종 현</div>");
contents.append("<div style='width:120px; height:120px; margin:0 auto 15px; border:2px solid #cc0000; border-radius:50%; display:inline-flex; align-items:center; justify-content:center; background:linear-gradient(135deg, #fff 0%, #f5f5f5 100%);'>");
contents.append("<div style='text-align:center; font-size:11pt; font-weight:bold; color:#cc0000; line-height:1.3;'>");
contents.append("(주)알피에스<br>대표이사<br>이 종 현");
contents.append("</div>");
contents.append("</div>");
contents.append("<div style='font-size:10pt; margin-bottom:5px;'>RPS CO., LTD</div>");
contents.append("<div style='font-size:9pt; color:#666; margin-bottom:10px;'>대 표 이 사 이 종 현</div>");
contents.append("<div style='font-size:9pt; line-height:1.8;'>");
contents.append("<div>대전광역시 유성구 국제과학10로 8</div>");
contents.append("<div>TEL:(042)602-3300 FAX:(042)672-3399</div>");
contents.append("</div>");
}
// 회사 주소 및 연락처
contents.append("<div class='company-details'>");
contents.append("대전광역시 유성구 국제과학10로 8<br>");
contents.append("TEL:(042)602-3300 FAX:(042)672-3399");
// 담당자 정보는 항상 표시
contents.append("<div class='manager-info'>");
contents.append("<div><strong>담당자 :</strong> " + managerName + "</div>");
if(!"".equals(managerContact)) {
contents.append("<div><strong>연락처 :</strong> " + managerContact + "</div>");
}
contents.append("</div>");
contents.append("</div>"); // company-info 닫기
contents.append("</div>"); // header-right 닫기
contents.append("</div>"); // header-section 닫기
// 인사말 및 담당자 정보
contents.append("<div class='greeting-section'>");
contents.append("<div class='greeting-left'>");
contents.append("견적을 요청해 주셔서 대단히 감사합니다.<br>");
contents.append("하기와 같이 견적서를 제출합니다.");
contents.append("</div>");
contents.append("<div class='greeting-right'>");
contents.append("담당자 : " + managerName + "<br>");
if(!"".equals(managerContact)) {
contents.append("연락처 : " + managerContact + "<br>");
}
contents.append("<span style='font-size: 10pt; margin-top: 5px; display: inline-block;'>부가세 별도</span>");
contents.append("</div>");
contents.append("</div>");
// 견적 품목
if(estimateItems != null && !estimateItems.isEmpty()){
contents.append("<table class='items-table'>");
contents.append("<thead>");
contents.append("<tr>");
contents.append("<th style='width:6%;'>번호<br>NO.</th>");
contents.append("<th style='width:20%;'>품 명<br>DESCRIPTION</th>");
contents.append("<th style='width:22%;'>규 격<br>SPECIFICATION</th>");
contents.append("<th style='width:7%;'>수량<br>Q'TY</th>");
contents.append("<th style='width:8%;'>단위<br>UNIT</th>");
contents.append("<th style='width:11%;'>단 가<br>UNIT<br>PRICE</th>");
contents.append("<th style='width:11%;'>금 액<br>AMOUNT</th>");
contents.append("<th style='width:15%;'>비고</th>");
contents.append("<th style='width:5%;'>No.</th>");
contents.append("<th style='width:20%;'>품</th>");
contents.append("<th style='width:22%;'>규</th>");
contents.append("<th style='width:7%;'>수량</th>");
contents.append("<th style='width:8%;'>단위</th>");
contents.append("<th style='width:11%;'>단</th>");
contents.append("<th style='width:11%;'>금</th>");
contents.append("<th style='width:16%;'>비고</th>");
contents.append("</tr>");
contents.append("</thead>");
contents.append("<tbody>");
@@ -1855,16 +1782,16 @@ private String makeEstimateMailContents(Map contractInfo, Map estimateTemplate,
} catch(Exception e){}
}
contents.append("<tr>");
contents.append("<td class='text-center'>" + (i + 1) + "</td>");
contents.append("<td class='text-left'>" + CommonUtils.checkNull(item.get("description")) + "</td>");
contents.append("<td class='text-left'>" + CommonUtils.checkNull(item.get("specification")) + "</td>");
contents.append("<td class='text-center'>" + CommonUtils.checkNull(item.get("quantity")) + "</td>");
contents.append("<td class='text-center'>" + CommonUtils.checkNull(item.get("unit")) + "</td>");
contents.append("<td class='text-right'>" + CommonUtils.checkNull(item.get("unit_price")) + "</td>");
contents.append("<td class='text-right'>" + CommonUtils.checkNull(item.get("amount")) + "</td>");
contents.append("<td class='text-left'>" + CommonUtils.checkNull(item.get("note")) + "</td>");
contents.append("</tr>");
contents.append("<tr>");
contents.append("<td class='text-center'>" + (i + 1) + "</td>");
contents.append("<td>" + CommonUtils.checkNull(item.get("description")) + "</td>");
contents.append("<td>" + CommonUtils.checkNull(item.get("specification")) + "</td>");
contents.append("<td class='text-center'>" + CommonUtils.checkNull(item.get("quantity")) + "</td>");
contents.append("<td class='text-center'>" + CommonUtils.checkNull(item.get("unit")) + "</td>");
contents.append("<td class='text-right'>" + CommonUtils.checkNull(item.get("unit_price")) + "</td>");
contents.append("<td class='text-right'>" + CommonUtils.checkNull(item.get("amount")) + "</td>");
contents.append("<td>" + CommonUtils.checkNull(item.get("note")) + "</td>");
contents.append("</tr>");
}
// 계 (총 합계) - 통화 기호 포함
@@ -1899,60 +1826,58 @@ private String makeEstimateMailContents(Map contractInfo, Map estimateTemplate,
// 원화환산 공급가액 - 제거 (화면에서도 숨김 처리)
// 테이블 내 비고 (항상 표시)
// 테이블 내 비고
String noteRemarks = CommonUtils.checkNull(estimateTemplate.get("note_remarks"));
if("".equals(noteRemarks)) noteRemarks = CommonUtils.checkNull(estimateTemplate.get("NOTE_REMARKS"));
System.out.println("===== 비고 내용 확인 =====");
System.out.println("noteRemarks: [" + noteRemarks + "]");
System.out.println("========================");
contents.append("<tr>");
contents.append("<td colspan='8' style='height:100px; vertical-align:top; padding:10px; text-align:left;'>");
contents.append("<div style='font-weight:bold; margin-bottom:10px;'>&lt;비고&gt;</div>");
contents.append("<div style='white-space:pre-wrap;'>" + noteRemarks + "</div>");
contents.append("</td>");
contents.append("</tr>");
// 참조사항 (NOTE1~4)
String note1 = CommonUtils.checkNull(estimateTemplate.get("note1"));
if("".equals(note1)) note1 = CommonUtils.checkNull(estimateTemplate.get("NOTE1"));
if("".equals(note1)) note1 = "1. 견적유효기간: 일";
String note2 = CommonUtils.checkNull(estimateTemplate.get("note2"));
if("".equals(note2)) note2 = CommonUtils.checkNull(estimateTemplate.get("NOTE2"));
if("".equals(note2)) note2 = "2. 납품기간: 발주 후 1주 이내";
String note3 = CommonUtils.checkNull(estimateTemplate.get("note3"));
if("".equals(note3)) note3 = CommonUtils.checkNull(estimateTemplate.get("NOTE3"));
if("".equals(note3)) note3 = "3. VAT 별도";
String note4 = CommonUtils.checkNull(estimateTemplate.get("note4"));
if("".equals(note4)) note4 = CommonUtils.checkNull(estimateTemplate.get("NOTE4"));
if("".equals(note4)) note4 = "4. 결제 조건 : 기존 결제조건에 따름.";
// 참조사항 행 추가
contents.append("<tr>");
contents.append("<td colspan='8' style='vertical-align: top; padding: 10px; text-align: left;'>");
contents.append("<div style='font-weight: bold; margin-bottom: 10px;'>&lt;참조사항&gt;</div>");
contents.append("<div style='margin-bottom: 5px;'>" + note1 + "</div>");
contents.append("<div style='margin-bottom: 5px;'>" + note2 + "</div>");
contents.append("<div style='margin-bottom: 5px;'>" + note3 + "</div>");
contents.append("<div style='margin-bottom: 5px;'>" + note4 + "</div>");
contents.append("</td>");
contents.append("</tr>");
// 하단 회사명 행 추가
contents.append("<tr>");
contents.append("<td colspan='8' style='text-align: right; padding: 15px; font-size: 10pt; font-weight: bold; border: none;'>");
contents.append("㈜알피에스");
contents.append("</td>");
contents.append("</tr>");
if(!"".equals(noteRemarks)){
contents.append("<tr>");
contents.append("<td colspan='8' style='height:100px; vertical-align:top; padding:10px; text-align:left;'>");
contents.append("<div style='font-weight:bold; margin-bottom:10px;'>&lt;비고&gt;</div>");
contents.append("<div style='white-space:pre-wrap;'>" + noteRemarks + "</div>");
contents.append("</td>");
contents.append("</tr>");
}
contents.append("</tbody>");
contents.append("</table>");
}
// 참조사항 (NOTE1~4)
String note1 = CommonUtils.checkNull(estimateTemplate.get("note1"));
if("".equals(note1)) note1 = CommonUtils.checkNull(estimateTemplate.get("NOTE1"));
String note2 = CommonUtils.checkNull(estimateTemplate.get("note2"));
if("".equals(note2)) note2 = CommonUtils.checkNull(estimateTemplate.get("NOTE2"));
String note3 = CommonUtils.checkNull(estimateTemplate.get("note3"));
if("".equals(note3)) note3 = CommonUtils.checkNull(estimateTemplate.get("NOTE3"));
String note4 = CommonUtils.checkNull(estimateTemplate.get("note4"));
if("".equals(note4)) note4 = CommonUtils.checkNull(estimateTemplate.get("NOTE4"));
if(!"".equals(note1) || !"".equals(note2) || !"".equals(note3) || !"".equals(note4)){
contents.append("<div class='note-section'>");
contents.append("<h4>※ 참조사항</h4>");
int noteNum = 1;
if(!"".equals(note1)) {
contents.append("<div>" + noteNum + ". " + note1 + "</div>");
noteNum++;
}
if(!"".equals(note2)) {
contents.append("<div>" + noteNum + ". " + note2 + "</div>");
noteNum++;
}
if(!"".equals(note3)) {
contents.append("<div>" + noteNum + ". " + note3 + "</div>");
noteNum++;
}
if(!"".equals(note4)) {
contents.append("<div>" + noteNum + ". " + note4 + "</div>");
}
contents.append("</div>");
}
contents.append("</div>"); // estimate-container 닫기
contents.append("</body>");
contents.append("</html>");
@@ -2445,7 +2370,6 @@ private String encodeImageToBase64(String imagePath) {
return partList;
}
/**
* 품목 목록 조회
* @param contractObjId 견적 OBJID

View File

@@ -15,15 +15,11 @@
<Service name="Catalina">
<!-- URIEncoding을 UTF-8로 설정하여 한글 파라미터 처리 -->
<!-- maxPostSize: POST 요청 최대 크기 (PDF Base64 전송을 위해 50MB로 설정) -->
<!-- maxHttpHeaderSize: HTTP 헤더 최대 크기 -->
<Connector port="8080"
protocol="HTTP/1.1"
connectionTimeout="60000"
connectionTimeout="20000"
redirectPort="8443"
URIEncoding="UTF-8"
maxPostSize="52428800"
maxHttpHeaderSize="65536" />
URIEncoding="UTF-8" />
<Engine name="Catalina" defaultHost="localhost">
<Realm className="org.apache.catalina.realm.LockOutRealm">