diff --git a/WebContent/WEB-INF/dispatcher-servlet.xml b/WebContent/WEB-INF/dispatcher-servlet.xml index 0769b0a..e5406f5 100644 --- a/WebContent/WEB-INF/dispatcher-servlet.xml +++ b/WebContent/WEB-INF/dispatcher-servlet.xml @@ -43,16 +43,6 @@ - - - - - - - - - - diff --git a/WebContent/WEB-INF/view/contractMgmt/estimateList_new.jsp b/WebContent/WEB-INF/view/contractMgmt/estimateList_new.jsp index bf768b7..5d0bfa1 100644 --- a/WebContent/WEB-INF/view/contractMgmt/estimateList_new.jsp +++ b/WebContent/WEB-INF/view/contractMgmt/estimateList_new.jsp @@ -744,35 +744,61 @@ function fn_generatePdfAndSendMail(contractObjId, templateObjId, templateType){ try { var iframeWindow = this.contentWindow; - // iframe 내의 PDF 생성 함수 호출 - if(typeof iframeWindow.fn_generateAndUploadPdf === 'function'){ - iframeWindow.fn_generateAndUploadPdf(function(pdfBlob){ - // iframe 제거 + // 데이터 로딩 완료를 기다림 (최대 10초) + var checkDataLoaded = function(attempts) { + if(attempts > 100) { // 10초 (100ms * 100) $('#pdfGeneratorFrame').remove(); + Swal.close(); + Swal.fire({ + title: '타임아웃', + text: '견적서 데이터 로딩 시간이 초과되었습니다.', + icon: 'error' + }); + return; + } + + if(iframeWindow.dataLoaded === true) { + console.log('데이터 로딩 완료 확인, PDF 생성 시작'); - if(pdfBlob){ - // PDF Blob과 함께 메일 발송 요청 - fn_sendMailWithPdf(contractObjId, pdfBlob); + // iframe 내의 PDF 생성 함수 호출 + if(typeof iframeWindow.fn_generateAndUploadPdf === 'function'){ + iframeWindow.fn_generateAndUploadPdf(function(pdfBase64){ + // iframe 제거 + $('#pdfGeneratorFrame').remove(); + + if(pdfBase64){ + // PDF Base64와 함께 메일 발송 요청 + fn_sendMailWithPdf(contractObjId, pdfBase64); + } else { + Swal.close(); + Swal.fire({ + title: '오류', + text: 'PDF 생성에 실패했습니다.', + icon: 'error' + }); + } + }); } else { + // iframe 제거 + $('#pdfGeneratorFrame').remove(); + Swal.close(); Swal.fire({ title: '오류', - text: 'PDF 생성에 실패했습니다.', + text: '견적서 페이지 로드에 실패했습니다.', icon: 'error' }); } - }); - } else { - // iframe 제거 - $('#pdfGeneratorFrame').remove(); - - Swal.close(); - Swal.fire({ - title: '오류', - text: '견적서 페이지 로드에 실패했습니다.', - icon: 'error' - }); - } + } else { + // 아직 로딩 중이면 100ms 후 다시 확인 + setTimeout(function() { + checkDataLoaded(attempts + 1); + }, 100); + } + }; + + // 데이터 로딩 체크 시작 + checkDataLoaded(0); } catch(e) { console.error('PDF 생성 오류:', e); $('#pdfGeneratorFrame').remove(); @@ -800,35 +826,111 @@ function fn_generatePdfAndSendMail(contractObjId, templateObjId, templateType){ }, 30000); } -// PDF Blob과 함께 메일 발송 (FormData 사용) -function fn_sendMailWithPdf(contractObjId, pdfBlob){ +// PDF Base64와 함께 메일 발송 (청크 방식) +function fn_sendMailWithPdf(contractObjId, pdfBase64){ console.log('===== 메일 발송 시작 ====='); console.log('contractObjId:', contractObjId); - console.log('PDF Blob 크기:', pdfBlob ? pdfBlob.size : 0, 'bytes'); + console.log('PDF Base64 길이:', pdfBase64 ? pdfBase64.length : 0); console.log('========================'); Swal.fire({ title: '메일 발송 중...', - text: '잠시만 기다려주세요.', + text: 'PDF 업로드 중...', allowOutsideClick: false, onOpen: () => { Swal.showLoading(); } }); - // FormData 생성 - var formData = new FormData(); - formData.append('objId', contractObjId); - formData.append('pdfFile', pdfBlob, 'estimate.pdf'); + // 청크 크기: 100KB (Base64) - POST 크기 제한 고려 + var chunkSize = 100 * 1024; + var totalChunks = Math.ceil(pdfBase64.length / chunkSize); + var uploadedChunks = 0; + var sessionId = 'pdf_' + contractObjId + '_' + new Date().getTime(); + + console.log('PDF Base64 전체 길이:', pdfBase64.length); + console.log('청크 크기:', chunkSize); + console.log('총 청크 수:', totalChunks); + + // 청크 업로드 함수 + function uploadChunk(chunkIndex) { + var start = chunkIndex * chunkSize; + var end = Math.min(start + chunkSize, pdfBase64.length); + var chunk = pdfBase64.substring(start, end); + + console.log('청크 ' + (chunkIndex + 1) + '/' + totalChunks + ' 업로드 중...'); + + $.ajax({ + url: "/contractMgmt/uploadPdfChunk.do", + type: "POST", + data: { + sessionId: sessionId, + chunkIndex: chunkIndex, + totalChunks: totalChunks, + chunk: chunk + }, + dataType: "json", + timeout: 30000, + success: function(data){ + if(data.result === "success"){ + uploadedChunks++; + + // 진행률 업데이트 + var progress = Math.round((uploadedChunks / totalChunks) * 100); + Swal.update({ + text: 'PDF 업로드 중... ' + progress + '%' + }); + + // 다음 청크 업로드 + if(chunkIndex + 1 < totalChunks){ + uploadChunk(chunkIndex + 1); + } else { + // 모든 청크 업로드 완료, 메일 발송 요청 + console.log('모든 청크 업로드 완료, 메일 발송 시작'); + sendMailWithUploadedPdf(contractObjId, sessionId); + } + } else { + Swal.close(); + Swal.fire({ + title: '업로드 실패', + text: 'PDF 업로드 중 오류가 발생했습니다.', + icon: 'error', + confirmButtonText: '확인' + }); + } + }, + error: function(xhr, status, error){ + Swal.close(); + console.error("청크 업로드 오류:", xhr, status, error); + Swal.fire({ + title: '오류', + text: 'PDF 업로드 중 시스템 오류가 발생했습니다.', + icon: 'error', + confirmButtonText: '확인' + }); + } + }); + } + + // 첫 번째 청크부터 시작 + uploadChunk(0); +} + +// 업로드된 PDF로 메일 발송 +function sendMailWithUploadedPdf(contractObjId, sessionId){ + Swal.update({ + text: '메일 발송 중...' + }); $.ajax({ url: "/contractMgmt/sendEstimateMail.do", type: "POST", - data: formData, - processData: false, // FormData 사용 시 필수 - contentType: false, // FormData 사용 시 필수 + data: { + objId: contractObjId, + pdfSessionId: sessionId + }, dataType: "json", - timeout: 60000, // 60초 타임아웃 + timeout: 60000, success: function(data){ console.log('메일 발송 응답:', data); Swal.close(); @@ -839,7 +941,6 @@ function fn_sendMailWithPdf(contractObjId, pdfBlob){ icon: 'success', confirmButtonText: '확인' }).then(() => { - // 메일 발송 상태 업데이트를 위해 목록 새로고침 fn_search(); }); } else { @@ -854,10 +955,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: '확인' }); diff --git a/WebContent/WEB-INF/view/contractMgmt/estimateTemplate1.jsp b/WebContent/WEB-INF/view/contractMgmt/estimateTemplate1.jsp index 66c3bd7..fe38b37 100644 --- a/WebContent/WEB-INF/view/contractMgmt/estimateTemplate1.jsp +++ b/WebContent/WEB-INF/view/contractMgmt/estimateTemplate1.jsp @@ -819,32 +819,36 @@ function fn_loadTemplateData(templateObjId){ // 결재상태 저장 g_apprStatus = template.appr_status || template.APPR_STATUS || template.apprStatus || "작성중"; - // 대문자/소문자 모두 지원 - var executor = template.EXECUTOR || template.executor || ""; - var recipient = template.RECIPIENT || template.recipient || ""; - var estimateNo = template.ESTIMATE_NO || template.estimate_no || template.estimateNo || ""; - var contactPerson = template.CONTACT_PERSON || template.contact_person || template.contactPerson || ""; - var greetingText = template.GREETING_TEXT || template.greeting_text || template.greetingText || ""; - var note1 = template.NOTE1 || template.note1 || ""; - var note2 = template.NOTE2 || template.note2 || ""; - var note3 = template.NOTE3 || template.note3 || ""; - var note4 = template.NOTE4 || template.note4 || ""; - var managerName = template.MANAGER_NAME || template.manager_name || template.managerName || "영업부"; - var managerContact = template.MANAGER_CONTACT || template.manager_contact || template.managerContact || ""; - - // 기본 정보 채우기 - $("#executor").val(executor); + // 대문자/소문자 모두 지원 + var executor = template.EXECUTOR || template.executor || ""; + var recipient = template.RECIPIENT || template.recipient || ""; + var recipientName = template.RECIPIENT_NAME || template.recipient_name || template.recipientName || ""; + var estimateNo = template.ESTIMATE_NO || template.estimate_no || template.estimateNo || ""; + var contactPerson = template.CONTACT_PERSON || template.contact_person || template.contactPerson || ""; + var greetingText = template.GREETING_TEXT || template.greeting_text || template.greetingText || ""; + var note1 = template.NOTE1 || template.note1 || ""; + var note2 = template.NOTE2 || template.note2 || ""; + var note3 = template.NOTE3 || template.note3 || ""; + var note4 = template.NOTE4 || template.note4 || ""; + var managerName = template.MANAGER_NAME || template.manager_name || template.managerName || "영업부"; + var managerContact = template.MANAGER_CONTACT || template.manager_contact || template.managerContact || ""; - // 데이터 로드 중 플래그 설정 (수신인 자동 로드 방지) - window.isLoadingData = true; - - // 수신처 설정 + // 기본 정보 채우기 + $("#executor").val(executor); + + // 데이터 로드 중 플래그 설정 (수신인 자동 로드 방지) + window.isLoadingData = true; + + // 수신처 설정 + if(recipient && recipient !== "") { + // OBJID로 셀렉트박스 선택 $("#recipient").val(recipient).trigger('change'); - - // 플래그 해제 - setTimeout(function() { - window.isLoadingData = false; - }, 100); + } + + // 플래그 해제 + setTimeout(function() { + window.isLoadingData = false; + }, 100); $("#estimate_no").val(estimateNo); $("#contact_person").val(contactPerson); @@ -1004,9 +1008,14 @@ function fn_loadTemplateData(templateObjId){ // 결재상태에 따라 버튼 제어 fn_controlButtons(); + + // 데이터 로딩 완료 플래그 설정 + window.dataLoaded = true; + console.log("견적서 데이터 로딩 완료"); } } else { console.error("데이터 로드 실패:", data); + window.dataLoaded = false; Swal.fire("데이터를 불러오는데 실패했습니다."); } }, @@ -1340,19 +1349,43 @@ function fn_generatePdf() { // 버튼 영역 임시 숨김 $('.btn-area').hide(); + // PDF 생성을 위해 스타일 조정 + var container = $('.estimate-container'); + var originalBg = container.css('background'); + var originalShadow = container.css('box-shadow'); + var originalPadding = container.css('padding'); + + // 깔끔한 PDF를 위해 배경, 그림자, 패딩 제거 + container.css({ + 'background': 'white', + 'box-shadow': 'none', + 'padding': '10mm' + }); + + // body 배경색도 흰색으로 + var originalBodyBg = $('body').css('background-color'); + $('body').css('background-color', 'white'); + // 견적서 컨테이너 캡처 html2canvas(document.querySelector('.estimate-container'), { - scale: 2, // 고해상도 + scale: 2, // 적절한 해상도 (파일 크기 최적화) useCORS: true, - logging: true, // 디버깅을 위해 true로 변경 + logging: false, backgroundColor: '#ffffff' }).then(function(canvas) { + // 스타일 복원 + container.css({ + 'background': originalBg, + 'box-shadow': originalShadow, + 'padding': originalPadding + }); + $('body').css('background-color', originalBodyBg); // 버튼 영역 다시 표시 $('.btn-area').show(); try { - // Canvas를 이미지로 변환 - var imgData = canvas.toDataURL('image/png'); + // Canvas를 JPEG 이미지로 변환 (PNG보다 파일 크기 작음) + var imgData = canvas.toDataURL('image/jpeg', 0.85); // 85% 품질 // PDF 생성 (A4 크기) var pdf = new jsPDF('p', 'mm', 'a4'); @@ -1362,15 +1395,15 @@ function fn_generatePdf() { var heightLeft = imgHeight; var position = 0; - // 첫 페이지 추가 - pdf.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight); + // 첫 페이지 추가 (JPEG 압축) + pdf.addImage(imgData, 'JPEG', 0, position, imgWidth, imgHeight, undefined, 'FAST'); heightLeft -= pageHeight; // 페이지가 넘어가면 추가 페이지 생성 while (heightLeft >= 0) { position = heightLeft - imgHeight; pdf.addPage(); - pdf.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight); + pdf.addImage(imgData, 'JPEG', 0, position, imgWidth, imgHeight, undefined, 'FAST'); heightLeft -= pageHeight; } @@ -1409,7 +1442,7 @@ function fn_generatePdf() { }); } -// PDF를 Blob으로 생성하여 서버로 전송하는 함수 +// PDF를 Base64로 생성하여 서버로 전송하는 함수 function fn_generateAndUploadPdf(callback) { console.log('fn_generateAndUploadPdf 호출됨'); @@ -1425,21 +1458,46 @@ function fn_generateAndUploadPdf(callback) { // 버튼 영역 임시 숨김 $('.btn-area').hide(); + // PDF 생성을 위해 스타일 조정 + var container = $('.estimate-container'); + var originalBg = container.css('background'); + var originalShadow = container.css('box-shadow'); + var originalPadding = container.css('padding'); + + // 깔끔한 PDF를 위해 배경, 그림자, 패딩 제거 + container.css({ + 'background': 'white', + 'box-shadow': 'none', + 'padding': '10mm' + }); + + // body 배경색도 흰색으로 + var originalBodyBg = $('body').css('background-color'); + $('body').css('background-color', 'white'); + // 견적서 컨테이너 캡처 html2canvas(document.querySelector('.estimate-container'), { - scale: 2, + scale: 2, // 적절한 해상도 (파일 크기 최적화) useCORS: true, - logging: true, + logging: false, backgroundColor: '#ffffff' }).then(function(canvas) { console.log('Canvas 캡처 완료'); + // 스타일 복원 + container.css({ + 'background': originalBg, + 'box-shadow': originalShadow, + 'padding': originalPadding + }); + $('body').css('background-color', originalBodyBg); + // 버튼 영역 다시 표시 $('.btn-area').show(); try { - // Canvas를 이미지로 변환 - var imgData = canvas.toDataURL('image/png'); + // Canvas를 JPEG 이미지로 변환 (PNG보다 파일 크기 작음) + var imgData = canvas.toDataURL('image/jpeg', 0.85); // 85% 품질 console.log('이미지 변환 완료'); // PDF 생성 @@ -1450,25 +1508,26 @@ function fn_generateAndUploadPdf(callback) { var heightLeft = imgHeight; var position = 0; - pdf.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight); + // JPEG 이미지 추가 (압축됨) + pdf.addImage(imgData, 'JPEG', 0, position, imgWidth, imgHeight, undefined, 'FAST'); heightLeft -= pageHeight; while (heightLeft >= 0) { position = heightLeft - imgHeight; pdf.addPage(); - pdf.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight); + pdf.addImage(imgData, 'JPEG', 0, position, imgWidth, imgHeight, undefined, 'FAST'); heightLeft -= pageHeight; } console.log('PDF 생성 완료'); - // PDF를 Blob으로 변환 (Base64 대신 바이너리) - var pdfBlob = pdf.output('blob'); - console.log('PDF Blob 생성 완료, 크기:', pdfBlob.size, 'bytes'); + // PDF를 Base64로 변환 + var pdfBase64 = pdf.output('dataurlstring').split(',')[1]; + console.log('PDF Base64 생성 완료, 길이:', pdfBase64.length); // 콜백 함수 호출 (메일 발송 등에 사용) if(callback && typeof callback === 'function') { - callback(pdfBlob); + callback(pdfBase64); } } catch(pdfError) { console.error('PDF 생성 중 오류:', pdfError); diff --git a/src/com/pms/common/utils/MailUtil.java b/src/com/pms/common/utils/MailUtil.java index 2207f14..021e215 100644 --- a/src/com/pms/common/utils/MailUtil.java +++ b/src/com/pms/common/utils/MailUtil.java @@ -773,6 +773,142 @@ public class MailUtil { return sendMailNew("", toUserEmail, subject, contents, null); } + /** + * UTF-8 인코딩으로 메일 발송 (견적서 등 한글 내용이 많은 경우) + * 기존 sendMailWithAttachFile과 동일하지만 UTF-8 인코딩 사용 + */ + public static boolean sendMailWithAttachFileUTF8(String fromUserId, String fromEmail + , ArrayList toUserIdList, ArrayList toEmailList, ArrayList ccEmailList, ArrayList bccEmailList + , String important, String subject, String contents + , ArrayList attachFileList + , String mailType + ) { + + if(Constants.Mail.sendMailSwitch == false){ + System.out.println("MailUtil.sendMailWithAttachFileUTF8 ::: Constants.Mail.sendMailSwitch is FALSE"); + return false; + } + + try { + /*변수 초기화*/ + fromUserId = CommonUtils.checkNull(fromUserId, "admin"); + fromEmail = Constants.Mail.SMTP_USER; + important = CommonUtils.checkNull(important); + subject = CommonUtils.checkNull(subject , "empty subject" ); + contents = CommonUtils.checkNull(contents, "empty contents"); + mailType = CommonUtils.checkNull(mailType, "empty mailType"); + + System.out.println("MailUtil.sendMailWithAttachFileUTF8().."); + System.out.println("MailUtil.sendMailWithAttachFileUTF8(fromUserId ):"+fromUserId); + System.out.println("MailUtil.sendMailWithAttachFileUTF8(fromEmail ):"+fromEmail); + System.out.println("MailUtil.sendMailWithAttachFileUTF8(toEmailList ):"+toEmailList); + System.out.println("MailUtil.sendMailWithAttachFileUTF8(ccEmailList ):"+ccEmailList); + System.out.println("MailUtil.sendMailWithAttachFileUTF8(subject ):"+subject); + + if(toEmailList == null){ return false; } + + //◆◆◆ 1. mail session ◆◆◆ + Properties prop = new Properties(); + prop.put("mail.smtp.host", Constants.Mail.SMTP_HOST); + prop.put("mail.smtp.port", Constants.Mail.SMTP_PORT); + prop.put("mail.smtp.auth" , "true"); + prop.put("mail.smtp.starttls.enable" , "true"); + prop.put("mail.smtps.checkserveridentity", "true"); + prop.put("mail.smtps.ssl.trust" , "*"); + prop.put("mail.debug" , "true"); + prop.put("mail.smtp.socketFactory.class" , "javax.net.ssl.SSLSocketFactory"); + prop.put("mail.smtp.ssl.enable" , "true"); + prop.put("mail.smtp.socketFactory.port" , Constants.Mail.SMTP_PORT); + prop.put("mail.smtp.ssl.trust" , "smtp.gmail.com"); + prop.put("mail.transport.protocol", "smtp"); + prop.put("mail.smtp.ssl.protocols", "TLSv1.2"); + + Session mailSession = Session.getDefaultInstance(prop, new javax.mail.Authenticator() { + protected PasswordAuthentication getPasswordAuthentication() { + return new PasswordAuthentication(Constants.Mail.SMTP_USER, Constants.Mail.SMTP_USER_PW); + } + }); + mailSession.setDebug(true); + mailSession.setDebugOut(new PrintStream(new ByteArrayOutputStream())); + + //◆◆◆ 2. message ◆◆◆ + MimeMessage message = new MimeMessage(mailSession); + message.setFrom(new InternetAddress( fromEmail )); + + // 수신자 + if(toEmailList != null){ + for (String _mail : toEmailList) { + if(CommonUtils.checkNull(_mail).equals("")){ + continue; + } + message.addRecipient(Message.RecipientType.TO, new InternetAddress(_mail)); + } + } + // 참조 + if(ccEmailList != null){ + for (String _mail : ccEmailList) { + if(CommonUtils.checkNull(_mail).equals("")){ + continue; + } + message.addRecipient(Message.RecipientType.CC, new InternetAddress(_mail)); + } + } + // 숨은참조 + if(bccEmailList != null){ + for (String _mail : bccEmailList) { + if(CommonUtils.checkNull(_mail).equals("")){ + continue; + } + message.addRecipient(Message.RecipientType.BCC, new InternetAddress(_mail)); + } + } + // 중요도 + if("".equals(important)){ + message.setHeader("Importance", "Normal"); + }else{ + message.setHeader("Importance", important); + } + // 제목 (UTF-8) + message.setSubject(subject, "UTF-8"); + + Multipart multipart = new MimeMultipart(); + // 내용 (UTF-8) + MimeBodyPart mbp_content = new MimeBodyPart(); + mbp_content.setContent(contents, "text/html;charset=UTF-8"); + mbp_content.setHeader("Content-Transfer-Encoding", "base64"); + multipart.addBodyPart(mbp_content); + + // 첨부파일 + 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)); + File attach_file = new File(filepath); + String attach_fileName = CommonUtils.checkNull(hmFile.get(Constants.Db.COL_FILE_REAL_NAME)); + + MimeBodyPart mbp_attachFile = new MimeBodyPart(); + mbp_attachFile.setFileName(MimeUtility.encodeText(attach_fileName, "UTF-8", "B")); + mbp_attachFile.setDataHandler(new DataHandler(new FileDataSource(attach_file))); + mbp_attachFile.setHeader("Content-Type", Files.probeContentType(Paths.get(attach_file.getCanonicalPath()))); + mbp_attachFile.setDescription(attach_fileName.split("\\.")[0], "UTF-8"); + multipart.addBodyPart(mbp_attachFile); + } + } + message.setContent(multipart); + message.setSentDate(new Date()); + + // 메일 발송 + Transport.send(message); + System.out.println("메일 발송 성공 (UTF-8)"); + + return true; + + } catch (Exception e) { + System.out.println("메일 발송 실패 (UTF-8): " + e.getMessage()); + e.printStackTrace(); + return false; + } + } + /** * 첨부파일 목록을 파일하나로 압축해서 반환 * @param zipFileName 압축파일명 diff --git a/src/com/pms/salesmgmt/controller/ContractMgmtController.java b/src/com/pms/salesmgmt/controller/ContractMgmtController.java index 13f13e9..0026744 100644 --- a/src/com/pms/salesmgmt/controller/ContractMgmtController.java +++ b/src/com/pms/salesmgmt/controller/ContractMgmtController.java @@ -2071,34 +2071,49 @@ public class ContractMgmtController { } /** - * 견적서 메일 발송 (AJAX) - PDF 파일 업로드 방식 + * PDF 청크 업로드 (AJAX) * @param request - * @param paramMap - objId (CONTRACT_OBJID) - * @param pdfFile - PDF 파일 (MultipartFile) + * @param paramMap - sessionId, chunkIndex, totalChunks, chunk + * @return + */ + @ResponseBody + @RequestMapping(value="/contractMgmt/uploadPdfChunk.do", method=RequestMethod.POST) + public Map uploadPdfChunk(HttpServletRequest request, + @RequestParam Map paramMap){ + Map resultMap = new HashMap(); + + try { + resultMap = contractMgmtService.uploadPdfChunk(request, paramMap); + } catch (Exception e) { + e.printStackTrace(); + resultMap.put("result", "error"); + resultMap.put("message", "청크 업로드 중 오류가 발생했습니다: " + e.getMessage()); + } + + return resultMap; + } + + /** + * 견적서 메일 발송 (AJAX) - PDF 세션 ID 방식 + * @param request + * @param paramMap - objId (CONTRACT_OBJID), pdfSessionId (업로드된 PDF 세션 ID) * @return */ @ResponseBody @RequestMapping(value="/contractMgmt/sendEstimateMail.do", method=RequestMethod.POST) public Map sendEstimateMail(HttpServletRequest request, - @RequestParam Map paramMap, - @RequestParam(value="pdfFile", required=false) org.springframework.web.multipart.MultipartFile pdfFile){ + @RequestParam Map 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); diff --git a/src/com/pms/salesmgmt/service/ContractMgmtService.java b/src/com/pms/salesmgmt/service/ContractMgmtService.java index 4a5ba99..99c6c34 100644 --- a/src/com/pms/salesmgmt/service/ContractMgmtService.java +++ b/src/com/pms/salesmgmt/service/ContractMgmtService.java @@ -1572,62 +1572,23 @@ public class ContractMgmtService { ccEmailList.add(writerEmail); } - // 7-1. PDF 파일 처리 (MultipartFile로 전송된 경우) + // 7-1. PDF 파일 처리 (세션 ID로 업로드된 경우) ArrayList attachFileList = new ArrayList(); - org.springframework.web.multipart.MultipartFile pdfFile = - (org.springframework.web.multipart.MultipartFile) paramMap.get("pdfFile"); + String pdfSessionId = CommonUtils.checkNull(paramMap.get("pdfSessionId")); - 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이 요구하는 형식) + if(!"".equals(pdfSessionId)) { + // 세션 ID로 저장된 PDF 파일 가져오기 + File pdfFile = getPdfFromSession(pdfSessionId, estimateTemplate); + if(pdfFile != null && pdfFile.exists()) { HashMap fileMap = new HashMap(); - 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()); + fileMap.put(Constants.Db.COL_FILE_REAL_NAME, pdfFile.getName()); + fileMap.put(Constants.Db.COL_FILE_SAVED_NAME, pdfFile.getName()); + fileMap.put(Constants.Db.COL_FILE_PATH, pdfFile.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 처리 실패해도 메일은 발송 (첨부 없이) + System.out.println("PDF 파일 첨부 완료: " + pdfFile.getAbsolutePath()); + } else { + System.out.println("PDF 파일을 찾을 수 없습니다: " + pdfSessionId); } } @@ -1645,7 +1606,8 @@ public class ContractMgmtService { boolean mailSent = false; try { - mailSent = MailUtil.sendMailWithAttachFile( + // UTF-8 인코딩 메서드 사용 (견적서는 한글 내용이 많음) + mailSent = MailUtil.sendMailWithAttachFileUTF8( fromUserId, null, // fromEmail (자동으로 SMTP 설정 사용) new ArrayList(), // toUserIdList (빈 리스트) @@ -2487,4 +2449,143 @@ private String encodeImageToBase64(String imagePath) { return itemList; } + + /** + * PDF 청크 업로드 처리 + * @param request + * @param paramMap - sessionId, chunkIndex, totalChunks, chunk + * @return + */ + public Map uploadPdfChunk(HttpServletRequest request, Map paramMap) { + Map resultMap = new HashMap(); + + try { + System.out.println("===== uploadPdfChunk 호출 ====="); + System.out.println("전달된 파라미터: " + paramMap); + System.out.println("=============================="); + + String sessionId = CommonUtils.checkNull(paramMap.get("sessionId")); + String chunkIndexStr = CommonUtils.checkNull(paramMap.get("chunkIndex")); + String totalChunksStr = CommonUtils.checkNull(paramMap.get("totalChunks")); + String chunk = CommonUtils.checkNull(paramMap.get("chunk")); + + if("".equals(sessionId) || "".equals(chunkIndexStr) || "".equals(totalChunksStr) || "".equals(chunk)) { + System.out.println("필수 파라미터 누락"); + resultMap.put("result", "error"); + resultMap.put("message", "필수 파라미터가 누락되었습니다."); + return resultMap; + } + + int chunkIndex = Integer.parseInt(chunkIndexStr); + int totalChunks = Integer.parseInt(totalChunksStr); + + System.out.println("청크 업로드: " + sessionId + " [" + (chunkIndex + 1) + "/" + totalChunks + "]"); + System.out.println("청크 크기: " + chunk.length() + " bytes"); + + // 임시 디렉토리에 청크 저장 + String tempDir = System.getProperty("java.io.tmpdir"); + String chunkDir = tempDir + File.separator + "pdf_chunks" + File.separator + sessionId; + File chunkDirFile = new File(chunkDir); + if(!chunkDirFile.exists()) { + chunkDirFile.mkdirs(); + } + + // 청크 파일 저장 + String chunkFileName = "chunk_" + chunkIndex + ".txt"; + File chunkFile = new File(chunkDir + File.separator + chunkFileName); + java.io.FileWriter fw = new java.io.FileWriter(chunkFile); + fw.write(chunk); + fw.close(); + + // 마지막 청크인 경우 모든 청크를 합쳐서 PDF 파일 생성 + if(chunkIndex == totalChunks - 1) { + System.out.println("모든 청크 수신 완료, PDF 파일 생성 시작"); + + // 모든 청크 읽어서 합치기 + StringBuilder fullBase64 = new StringBuilder(); + for(int i = 0; i < totalChunks; i++) { + File cf = new File(chunkDir + File.separator + "chunk_" + i + ".txt"); + java.io.BufferedReader br = new java.io.BufferedReader(new java.io.FileReader(cf)); + String line; + while((line = br.readLine()) != null) { + fullBase64.append(line); + } + br.close(); + } + + // Base64 디코딩하여 PDF 파일 생성 (Apache Commons Codec 사용) + byte[] pdfBytes = org.apache.commons.codec.binary.Base64.decodeBase64(fullBase64.toString()); + + String pdfFileName = sessionId + ".pdf"; + File pdfFile = new File(tempDir + File.separator + pdfFileName); + java.io.FileOutputStream fos = new java.io.FileOutputStream(pdfFile); + fos.write(pdfBytes); + fos.close(); + + pdfFile.deleteOnExit(); + + System.out.println("PDF 파일 생성 완료: " + pdfFile.getAbsolutePath()); + + // 청크 파일들 삭제 + for(int i = 0; i < totalChunks; i++) { + File cf = new File(chunkDir + File.separator + "chunk_" + i + ".txt"); + cf.delete(); + } + chunkDirFile.delete(); + } + + resultMap.put("result", "success"); + + } catch(Exception e) { + System.out.println("청크 업로드 중 오류: " + e.getMessage()); + e.printStackTrace(); + resultMap.put("result", "error"); + resultMap.put("message", e.getMessage()); + } + + return resultMap; + } + + /** + * 세션 ID로 저장된 PDF 파일 가져오기 + * @param sessionId + * @param estimateTemplate + * @return + */ + private File getPdfFromSession(String sessionId, Map estimateTemplate) { + try { + String tempDir = System.getProperty("java.io.tmpdir"); + File pdfFile = new File(tempDir + File.separator + sessionId + ".pdf"); + + if(pdfFile.exists()) { + // 견적번호로 파일명 변경 + String estimateNo = CommonUtils.checkNull(estimateTemplate.get("estimate_no")); + if("".equals(estimateNo)) estimateNo = CommonUtils.checkNull(estimateTemplate.get("ESTIMATE_NO")); + if("".equals(estimateNo)) estimateNo = "견적서"; + + java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("yyyyMMddHHmmss"); + String timestamp = sdf.format(new java.util.Date()); + String newFileName = estimateNo + "_" + timestamp + ".pdf"; + File renamedFile = new File(tempDir + File.separator + newFileName); + + // 파일 복사 + java.nio.file.Files.copy(pdfFile.toPath(), renamedFile.toPath(), + java.nio.file.StandardCopyOption.REPLACE_EXISTING); + + renamedFile.deleteOnExit(); + + // 원본 파일 삭제 + pdfFile.delete(); + + return renamedFile; + } + + return null; + + } catch(Exception e) { + System.out.println("PDF 파일 가져오기 중 오류: " + e.getMessage()); + e.printStackTrace(); + return null; + } + } } diff --git a/tomcat-conf/server.xml b/tomcat-conf/server.xml index c069afd..c7d1748 100644 --- a/tomcat-conf/server.xml +++ b/tomcat-conf/server.xml @@ -15,14 +15,14 @@ - +