feat: 견적관리 메일 발송 기능 추가 및 UI 개선
- 견적서 메일 발송 API 추가 (ContractMgmtController, ContractMgmtService) - 견적관리 리스트에 메일 발송 상태 및 발송일시 컬럼 추가 - 메일 내용을 견적서 형식과 동일하게 변경 (품목 테이블 포함) - 메일 제목에 영업번호 및 OBJID 포함하여 발송 이력 추적 가능 - 견적서 템플릿: 단가/금액 콤마 표시 기능 추가 - 견적서 템플릿: 비고 컬럼 너비 확대 - S/N 모달창 텍스트 색상 개선 (가독성 향상) - 견적서 수정 시 특정 템플릿만 업데이트되도록 SQL 쿼리 수정
This commit is contained in:
@@ -76,7 +76,7 @@ $(function(){
|
||||
window.open(url, "", "width=1000,height=880");
|
||||
}else if(targetType == "CONTRACT_ESTIMATE"){
|
||||
url = "/contractMgmt/estimateRegistFormPopup.do?objId="+targetObjId+"&actionType=view";
|
||||
window.open(url, "", "width=650,height=400");
|
||||
window.open(url, "", "width=1000,height=560");
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -48,8 +48,8 @@ $(document).ready(function(){
|
||||
|
||||
//영업활동 등록 팝업
|
||||
$(".btnRegist").click(function(){
|
||||
var popup_width = 650;
|
||||
var popup_height = 400;
|
||||
var popup_width = 1000;
|
||||
var popup_height = 560;
|
||||
var params = "?actionType=regist"
|
||||
var url = "/contractMgmt/estimateRegistFormPopup.do"+params;
|
||||
//window.open("/ordermgmt/ordermgmtUpdateFormPopup.do"+params, "", "width=650, height=750","menubars=no, scrollbars=yes, resizable=yes");
|
||||
@@ -148,6 +148,48 @@ $(document).ready(function(){
|
||||
}
|
||||
});
|
||||
|
||||
// 메일발송
|
||||
$("#btnMail").click(function(){
|
||||
var selectedData = _tabulGrid.getSelectedData();
|
||||
if(selectedData.length < 1){
|
||||
Swal.fire("메일발송할 행을 선택해주십시오.");
|
||||
return false;
|
||||
} else if(selectedData.length > 1){
|
||||
Swal.fire("한번에 한개의 견적서만 발송 가능합니다.");
|
||||
return false;
|
||||
} else {
|
||||
var apprStatus = fnc_checkNull(selectedData[0].APPR_STATUS);
|
||||
var objId = fnc_checkNull(selectedData[0].OBJID);
|
||||
var estStatus = fnc_checkNull(selectedData[0].EST_STATUS);
|
||||
|
||||
// 결재완료 상태 확인
|
||||
if(apprStatus !== "결재완료"){
|
||||
Swal.fire("결재완료된 견적서만 발송 가능합니다.");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 견적서 존재 여부 확인
|
||||
if(estStatus === "0" || estStatus === 0){
|
||||
Swal.fire("작성된 견적서가 없습니다.");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 메일 발송 확인
|
||||
Swal.fire({
|
||||
title: '견적서 메일 발송',
|
||||
text: "최종 차수의 견적서를 PDF로 발송하시겠습니까?",
|
||||
icon: 'question',
|
||||
showCancelButton: true,
|
||||
confirmButtonText: '발송',
|
||||
cancelButtonText: '취소'
|
||||
}).then((result) => {
|
||||
if(result.isConfirmed){
|
||||
fn_sendEstimateMail(objId);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
fn_search();
|
||||
});
|
||||
|
||||
@@ -198,13 +240,35 @@ var columns = [
|
||||
fn_showEstimateList(objid);
|
||||
}
|
||||
},
|
||||
{headerHozAlign : 'center', hozAlign : 'center', width : '80', title : '결재상태', field : 'APPR_STATUS',
|
||||
formatter:fnc_createGridAnchorTag,
|
||||
cellClick:function(e, cell){
|
||||
var objid = fnc_checkNull(cell.getData().OBJID);
|
||||
fn_projectConceptDetail(objid);
|
||||
}
|
||||
},
|
||||
{headerHozAlign : 'center', hozAlign : 'center', width : '80', title : '결재상태', field : 'APPR_STATUS',
|
||||
formatter:fnc_createGridAnchorTag,
|
||||
cellClick:function(e, cell){
|
||||
var APPROVAL_OBJID = fnc_checkNull(cell.getData().APPROVAL_OBJID);
|
||||
var ROUTE_OBJID = fnc_checkNull(cell.getData().ROUTE_OBJID);
|
||||
approval_form(APPROVAL_OBJID,ROUTE_OBJID);
|
||||
}
|
||||
},
|
||||
{headerHozAlign : 'center', hozAlign : 'center', width : '100', title : '메일발송', field : 'MAIL_SEND_STATUS',
|
||||
formatter: function(cell, formatterParams, onRendered){
|
||||
var status = fnc_checkNull(cell.getValue());
|
||||
var sendDate = fnc_checkNull(cell.getData().MAIL_SEND_DATE);
|
||||
|
||||
if(status === 'Y'){
|
||||
return '<span style="color:green;">발송완료</span>';
|
||||
} else if(status === 'N'){
|
||||
return '<span style="color:red;">발송실패</span>';
|
||||
} else {
|
||||
return '<span style="color:#999;">미발송</span>';
|
||||
}
|
||||
// if(status === 'Y'){
|
||||
// return '<span style="color:green;">발송완료</span><br/><span style="font-size:11px;">' + sendDate + '</span>';
|
||||
// } else if(status === 'N'){
|
||||
// return '<span style="color:red;">발송실패</span><br/><span style="font-size:11px;">' + sendDate + '</span>';
|
||||
// } else {
|
||||
// return '<span style="color:#999;">미발송</span>';
|
||||
// }
|
||||
}
|
||||
},
|
||||
{headerHozAlign : 'center', hozAlign : 'center', width : '100', title : '견적단가', field : 'EST_PRICE' },
|
||||
{headerHozAlign : 'center', hozAlign : 'center', width : '100', title : '견적공급가액', field : 'EST_SUPPLY_PRICE' },
|
||||
{headerHozAlign : 'center', hozAlign : 'center', width : '80', title : '환종', field : 'CONTRACT_CURRENCY_NAME' },
|
||||
@@ -358,8 +422,8 @@ function fn_FileRegist(objId, docType, docTypeName){
|
||||
}
|
||||
//영업활동등록 상세
|
||||
function fn_projectConceptDetail(objId){
|
||||
var popup_width = 650;
|
||||
var popup_height = 400;
|
||||
var popup_width = 1000;
|
||||
var popup_height = 560;
|
||||
var url = "/contractMgmt/estimateRegistFormPopup.do?objId="+objId;
|
||||
|
||||
fn_centerPopup(popup_width, popup_height, url);
|
||||
@@ -382,7 +446,7 @@ function fn_showEstimateList(contractObjId){
|
||||
data: { objId: contractObjId },
|
||||
dataType: "json",
|
||||
success: function(data){
|
||||
console.log("견적서 목록 응답:", data); // 디버깅용
|
||||
//console.log("견적서 목록 응답:", data); // 디버깅용
|
||||
|
||||
if(data.result === "success" && data.list && data.list.length > 0){
|
||||
var html = '<div style="max-height: 400px; overflow-y: auto;">';
|
||||
@@ -396,7 +460,7 @@ function fn_showEstimateList(contractObjId){
|
||||
html += '</tr></thead><tbody>';
|
||||
|
||||
data.list.forEach(function(item){
|
||||
console.log("견적서 항목:", item); // 디버깅용
|
||||
//console.log("견적서 항목:", item); // 디버깅용
|
||||
|
||||
// 대문자/소문자 모두 지원하도록 안전하게 처리
|
||||
var objid = item.OBJID || item.objid || '';
|
||||
@@ -511,6 +575,58 @@ function fn_showSerialNoPopup(serialNoString){
|
||||
});
|
||||
}
|
||||
|
||||
// 견적서 메일 발송
|
||||
function fn_sendEstimateMail(contractObjId){
|
||||
if(!contractObjId || contractObjId === ''){
|
||||
Swal.fire("잘못된 요청입니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
Swal.fire({
|
||||
title: '메일 발송 중...',
|
||||
text: '잠시만 기다려주세요.',
|
||||
allowOutsideClick: false,
|
||||
didOpen: () => {
|
||||
Swal.showLoading();
|
||||
}
|
||||
});
|
||||
|
||||
$.ajax({
|
||||
url: "/contractMgmt/sendEstimateMail.do",
|
||||
type: "POST",
|
||||
data: { objId: contractObjId },
|
||||
dataType: "json",
|
||||
success: function(data){
|
||||
Swal.close();
|
||||
if(data.result === "success"){
|
||||
Swal.fire({
|
||||
title: '발송 완료',
|
||||
text: '견적서가 성공적으로 발송되었습니다.',
|
||||
icon: 'success',
|
||||
confirmButtonText: '확인'
|
||||
});
|
||||
} else {
|
||||
Swal.fire({
|
||||
title: '발송 실패',
|
||||
text: data.message || '견적서 발송 중 오류가 발생했습니다.',
|
||||
icon: 'error',
|
||||
confirmButtonText: '확인'
|
||||
});
|
||||
}
|
||||
},
|
||||
error: function(xhr, status, error){
|
||||
Swal.close();
|
||||
console.error("메일 발송 오류:", xhr, status, error);
|
||||
Swal.fire({
|
||||
title: '오류',
|
||||
text: '메일 발송 중 시스템 오류가 발생했습니다.',
|
||||
icon: 'error',
|
||||
confirmButtonText: '확인'
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
//코드값을 받아와서 동적으로 selectbox 생성
|
||||
function optionJobGroup(code){
|
||||
var val=code;
|
||||
|
||||
@@ -17,6 +17,25 @@
|
||||
.fileListscrollTbody td {
|
||||
font-size: 11px !important;
|
||||
}
|
||||
|
||||
/* S/N 입력 필드 placeholder 색상 */
|
||||
#serial_no::placeholder {
|
||||
color: #999 !important;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
#serial_no::-webkit-input-placeholder {
|
||||
color: #999 !important;
|
||||
}
|
||||
|
||||
#serial_no::-moz-placeholder {
|
||||
color: #999 !important;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
#serial_no:-ms-input-placeholder {
|
||||
color: #999 !important;
|
||||
}
|
||||
</style>
|
||||
<script type="text/javascript">
|
||||
$(function() {
|
||||
@@ -613,11 +632,11 @@
|
||||
}
|
||||
|
||||
// 팝업 HTML 생성
|
||||
var popupHtml = '<div style="padding:10px;">';
|
||||
popupHtml += ' <h3 style="margin:0 0 15px 0; text-align:center;">S/N 관리</h3>';
|
||||
popupHtml += ' <div id="snListContainer" style="margin-bottom:15px; max-height:300px; overflow-y:auto;"></div>';
|
||||
var popupHtml = '<div style="padding:10px; color:#333;">';
|
||||
popupHtml += ' <h3 style="margin:0 0 15px 0; text-align:center; color:#333;">S/N 관리</h3>';
|
||||
popupHtml += ' <div id="snListContainer" style="margin-bottom:15px; max-height:300px; overflow-y:auto; color:#333;"></div>';
|
||||
popupHtml += ' <div style="margin-bottom:15px; display:flex; gap:5px;">';
|
||||
popupHtml += ' <input type="text" id="newSnInput" placeholder="S/N 입력" style="flex:1; padding:8px; border:1px solid #ddd; border-radius:4px;">';
|
||||
popupHtml += ' <input type="text" id="newSnInput" placeholder="S/N 입력" style="flex:1; padding:8px; border:1px solid #ddd; border-radius:4px; color:#333;">';
|
||||
popupHtml += ' <button type="button" onclick="fn_addSn()" class="plm_btns">추가</button>';
|
||||
popupHtml += ' </div>';
|
||||
popupHtml += ' <div style="text-align:center; margin-top:20px; display:flex; gap:10px; justify-content:center;">';
|
||||
@@ -656,12 +675,12 @@
|
||||
console.log("fn_renderSnList 실행, snList 길이:", snList.length);
|
||||
console.log("snList 내용:", snList);
|
||||
|
||||
var html = '<table style="width:100%; margin-bottom:10px; border-collapse:collapse; border:1px solid #ddd;">';
|
||||
var html = '<table style="width:100%; margin-bottom:10px; border-collapse:collapse; border:1px solid #ddd; color:#333;">';
|
||||
html += '<colgroup><col width="15%"><col width="65%"><col width="20%"></colgroup>';
|
||||
html += '<thead><tr style="background:#f5f5f5;">';
|
||||
html += '<th style="padding:10px; border:1px solid #ddd; text-align:center;">번호</th>';
|
||||
html += '<th style="padding:10px; border:1px solid #ddd; text-align:center;">S/N</th>';
|
||||
html += '<th style="padding:10px; border:1px solid #ddd; text-align:center;">삭제</th>';
|
||||
html += '<thead><tr style="background:#f5f5f5; color:#333;">';
|
||||
html += '<th style="padding:10px; border:1px solid #ddd; text-align:center; color:#333;">번호</th>';
|
||||
html += '<th style="padding:10px; border:1px solid #ddd; text-align:center; color:#333;">S/N</th>';
|
||||
html += '<th style="padding:10px; border:1px solid #ddd; text-align:center; color:#333;">삭제</th>';
|
||||
html += '</tr></thead>';
|
||||
html += '<tbody>';
|
||||
|
||||
@@ -670,8 +689,8 @@
|
||||
} else {
|
||||
for(var i = 0; i < snList.length; i++) {
|
||||
html += '<tr>';
|
||||
html += '<td style="text-align:center; padding:8px; border:1px solid #ddd;">' + (i+1) + '</td>';
|
||||
html += '<td style="padding:8px; border:1px solid #ddd;">' + snList[i].value + '</td>';
|
||||
html += '<td style="text-align:center; padding:8px; border:1px solid #ddd; color:#333;">' + (i+1) + '</td>';
|
||||
html += '<td style="padding:8px; border:1px solid #ddd; color:#333;">' + snList[i].value + '</td>';
|
||||
html += '<td style="text-align:center; padding:8px; border:1px solid #ddd;">';
|
||||
html += '<button type="button" onclick="fn_deleteSn(' + snList[i].id + ')" class="plm_btns" style="padding:5px 10px; font-size:12px;">삭제</button>';
|
||||
html += '</td>';
|
||||
@@ -732,14 +751,14 @@
|
||||
Swal.fire({
|
||||
title: '연속번호 생성',
|
||||
html:
|
||||
'<div style="text-align:left; padding:5px;">' +
|
||||
'<div style="text-align:left; padding:5px; color:#333;">' +
|
||||
'<div style="margin-bottom:10px;">' +
|
||||
'<label style="display:block; margin-bottom:3px; font-size:13px;">시작번호 <span style="color:red;">*</span></label>' +
|
||||
'<input type="text" id="seqStartNo" placeholder="예: ITEM-001" style="width:100%; padding:6px; border:1px solid #ddd; border-radius:3px; font-size:13px;">' +
|
||||
'<label style="display:block; margin-bottom:3px; font-size:13px; color:#333;">시작번호 <span style="color:red;">*</span></label>' +
|
||||
'<input type="text" id="seqStartNo" placeholder="예: ITEM-001" style="width:100%; padding:6px; border:1px solid #ddd; border-radius:3px; font-size:13px; color:#333;">' +
|
||||
'</div>' +
|
||||
'<div style="margin-bottom:10px;">' +
|
||||
'<label style="display:block; margin-bottom:3px; font-size:13px;">생성개수 <span style="color:red;">*</span></label>' +
|
||||
'<input type="number" id="seqCount" placeholder="예: 10" min="1" max="100" style="width:100%; padding:6px; border:1px solid #ddd; border-radius:3px; font-size:13px;">' +
|
||||
'<label style="display:block; margin-bottom:3px; font-size:13px; color:#333;">생성개수 <span style="color:red;">*</span></label>' +
|
||||
'<input type="number" id="seqCount" placeholder="예: 10" min="1" max="100" style="width:100%; padding:6px; border:1px solid #ddd; border-radius:3px; font-size:13px; color:#333;">' +
|
||||
'</div>' +
|
||||
'<div style="background:#f8f9fa; padding:8px; border-radius:3px; color:#666; font-size:11px; line-height:1.5;">' +
|
||||
'예: ITEM-001, 개수 3 → ITEM-001, ITEM-002, ITEM-003<br>' +
|
||||
@@ -982,7 +1001,7 @@
|
||||
<tr>
|
||||
<td class="input_title"><label for="">S/N</label></td>
|
||||
<td colspan="">
|
||||
<input type="text" name="serial_no" id="serial_no" placeholder="클릭하여 S/N 추가" reqTitle="S/N" value="${info.SERIAL_NO}" readonly style="cursor:pointer; background-color:#f8f9fa;" />
|
||||
<input type="text" name="serial_no" id="serial_no" placeholder="클릭하여 S/N 추가" reqTitle="S/N" value="${info.SERIAL_NO}" readonly style="cursor:pointer; background-color:#f8f9fa; color:#333 !important;" />
|
||||
</td>
|
||||
<td class="input_title"><label for="">수량 <span style="color:red;">*</span></label></td>
|
||||
<td>
|
||||
|
||||
@@ -137,14 +137,14 @@ body {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.items-table .col-no { width: 8%; }
|
||||
.items-table .col-desc { width: 22%; }
|
||||
.items-table .col-spec { width: 25%; }
|
||||
.items-table .col-qty { width: 8%; }
|
||||
.items-table .col-unit { width: 10%; }
|
||||
.items-table .col-price { width: 12%; }
|
||||
.items-table .col-amount { width: 12%; }
|
||||
.items-table .col-note { width: 13%; }
|
||||
.items-table .col-no { width: 6%; }
|
||||
.items-table .col-desc { width: 20%; }
|
||||
.items-table .col-spec { width: 22%; }
|
||||
.items-table .col-qty { width: 7%; }
|
||||
.items-table .col-unit { width: 8%; }
|
||||
.items-table .col-price { width: 11%; }
|
||||
.items-table .col-amount { width: 11%; }
|
||||
.items-table .col-note { width: 15%; }
|
||||
|
||||
.items-table .text-left {
|
||||
text-align: left;
|
||||
@@ -258,14 +258,29 @@ $(function(){
|
||||
fn_calculateAmount($(this).closest("tr"));
|
||||
});
|
||||
|
||||
// 콤마 자동 추가
|
||||
$(".item-price, .item-amount").on("blur", function(){
|
||||
var val = $(this).val().replace(/,/g, "");
|
||||
// 콤마 자동 추가 (동적 요소 포함)
|
||||
$(document).on("blur", ".item-price, .item-amount", function(){
|
||||
var val = $(this).val().replace(/,/g, "").replace(/₩/g, "");
|
||||
if(!isNaN(val) && val !== "") {
|
||||
$(this).val(addComma(val));
|
||||
}
|
||||
});
|
||||
|
||||
// 단가 입력 시 실시간 콤마 처리
|
||||
$(document).on("input", ".item-price", function(){
|
||||
var val = $(this).val().replace(/,/g, "");
|
||||
var cursorPos = this.selectionStart;
|
||||
var commasBefore = ($(this).val().substring(0, cursorPos).match(/,/g) || []).length;
|
||||
|
||||
if(!isNaN(val) && val !== "") {
|
||||
$(this).val(addComma(val));
|
||||
// 커서 위치 조정
|
||||
var commasAfter = ($(this).val().substring(0, cursorPos).match(/,/g) || []).length;
|
||||
var newPos = cursorPos + (commasAfter - commasBefore);
|
||||
this.setSelectionRange(newPos, newPos);
|
||||
}
|
||||
});
|
||||
|
||||
// 데이터 로드
|
||||
if("<%=objId%>" !== "" && "<%=objId%>" !== "-1") {
|
||||
fn_loadData();
|
||||
@@ -308,16 +323,8 @@ function fn_addItemRow() {
|
||||
'<td class="editable"><input type="text" class="item-note" value=""></td>' +
|
||||
'</tr>';
|
||||
|
||||
// 비고 행 바로 위에 추가
|
||||
// 비고 행 바로 위에 추가 (이벤트는 이미 document에 바인딩되어 있으므로 별도 바인딩 불필요)
|
||||
$("#itemsTableBody tr:last").before(newRow);
|
||||
|
||||
// 콤마 자동 추가 이벤트 바인딩
|
||||
$("#itemsTableBody").find(".item-price, .item-amount").last().on("blur", function(){
|
||||
var val = $(this).val().replace(/,/g, "");
|
||||
if(!isNaN(val) && val !== "") {
|
||||
$(this).val(addComma(val));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 데이터 로드
|
||||
@@ -424,14 +431,18 @@ function fn_loadTemplateData(templateObjId){
|
||||
var amount = item.AMOUNT || item.amount || '';
|
||||
var note = item.NOTE || item.note || '';
|
||||
|
||||
// 단가와 금액에 콤마 추가
|
||||
var unitPriceFormatted = unitPrice ? addComma(unitPrice) : '';
|
||||
var amountFormatted = amount ? addComma(amount) : '₩0';
|
||||
|
||||
var row = $("<tr>");
|
||||
row.append('<td>' + (idx + 1) + '</td>');
|
||||
row.append('<td class="text-left editable"><input type="text" class="item-desc" value="' + description + '"></td>');
|
||||
row.append('<td class="text-left editable"><textarea class="item-spec">' + specification + '</textarea></td>');
|
||||
row.append('<td class="editable"><input type="text" class="item-qty" value="' + quantity + '"></td>');
|
||||
row.append('<td class="editable"><input type="text" class="item-unit" value="' + unit + '"></td>');
|
||||
row.append('<td class="text-right editable"><input type="text" class="item-price" value="' + unitPrice + '"></td>');
|
||||
row.append('<td class="text-right editable"><input type="text" class="item-amount" value="' + amount + '" readonly></td>');
|
||||
row.append('<td class="text-right editable"><input type="text" class="item-price" value="' + unitPriceFormatted + '"></td>');
|
||||
row.append('<td class="text-right editable"><input type="text" class="item-amount" value="' + amountFormatted + '" readonly></td>');
|
||||
row.append('<td class="editable"><input type="text" class="item-note" value="' + note + '"></td>');
|
||||
$("#itemsTableBody").append(row);
|
||||
});
|
||||
|
||||
@@ -1933,4 +1933,36 @@ public class ContractMgmtController {
|
||||
|
||||
return resultMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* 견적서 메일 발송 (AJAX)
|
||||
* @param request
|
||||
* @param paramMap - objId (CONTRACT_OBJID)
|
||||
* @return
|
||||
*/
|
||||
@ResponseBody
|
||||
@RequestMapping(value="/contractMgmt/sendEstimateMail.do", method=RequestMethod.POST)
|
||||
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;
|
||||
}
|
||||
|
||||
// 메일 발송 서비스 호출
|
||||
resultMap = contractMgmtService.sendEstimateMail(request, paramMap);
|
||||
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
resultMap.put("result", "error");
|
||||
resultMap.put("message", "메일 발송 중 오류가 발생했습니다: " + e.getMessage());
|
||||
}
|
||||
|
||||
return resultMap;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -501,9 +501,25 @@
|
||||
,EST_PRICE
|
||||
,EST_SUPPLY_PRICE
|
||||
,(SELECT COUNT(1) FROM ESTIMATE_TEMPLATE WHERE CONTRACT_OBJID = T.OBJID) AS EST_STATUS
|
||||
,(
|
||||
SELECT IS_SEND
|
||||
FROM MAIL_LOG
|
||||
WHERE MAIL_TYPE = 'CONTRACT_ESTIMATE'
|
||||
AND TITLE LIKE '%[OBJID:' || T.OBJID || ']%'
|
||||
ORDER BY LOG_TIME DESC
|
||||
LIMIT 1
|
||||
) AS MAIL_SEND_STATUS
|
||||
,(
|
||||
SELECT TO_CHAR(LOG_TIME, 'YYYY-MM-DD HH24:MI')
|
||||
FROM MAIL_LOG
|
||||
WHERE MAIL_TYPE = 'CONTRACT_ESTIMATE'
|
||||
AND TITLE LIKE '%[OBJID:' || T.OBJID || ']%'
|
||||
ORDER BY LOG_TIME DESC
|
||||
LIMIT 1
|
||||
) AS MAIL_SEND_DATE
|
||||
,A.APPR_STATUS
|
||||
,A.APPROVAL_OBJID
|
||||
,A.ROUTE_OBJID
|
||||
,A.ROUTE_OBJID
|
||||
FROM
|
||||
CONTRACT_MGMT AS T
|
||||
LEFT OUTER JOIN
|
||||
@@ -3907,8 +3923,7 @@ ORDER BY ASM.SUPPLY_NAME
|
||||
CHG_USER_ID = #{chg_user_id},
|
||||
CHGDATE = NOW()
|
||||
WHERE
|
||||
CONTRACT_OBJID = #{objId}
|
||||
AND TEMPLATE_TYPE = #{template_type}
|
||||
OBJID = #{template_objid}
|
||||
</update>
|
||||
|
||||
<!-- 견적서 템플릿 품목 삭제 -->
|
||||
@@ -3963,8 +3978,61 @@ ORDER BY ASM.SUPPLY_NAME
|
||||
CHG_USER_ID = #{chg_user_id},
|
||||
CHGDATE = NOW()
|
||||
WHERE
|
||||
CONTRACT_OBJID = #{objId}
|
||||
AND TEMPLATE_TYPE = #{template_type}
|
||||
OBJID = #{template_objid}
|
||||
</update>
|
||||
|
||||
<!-- 최종 차수 견적서 조회 (메일 발송용) -->
|
||||
<select id="getLatestEstimateTemplate" parameterType="map" resultType="map">
|
||||
SELECT
|
||||
OBJID AS "OBJID",
|
||||
CONTRACT_OBJID AS "CONTRACT_OBJID",
|
||||
TEMPLATE_TYPE AS "TEMPLATE_TYPE",
|
||||
EXECUTOR AS "EXECUTOR",
|
||||
RECIPIENT AS "RECIPIENT",
|
||||
ESTIMATE_NO AS "ESTIMATE_NO",
|
||||
CONTACT_PERSON AS "CONTACT_PERSON",
|
||||
GREETING_TEXT AS "GREETING_TEXT",
|
||||
MODEL_NAME AS "MODEL_NAME",
|
||||
MODEL_CODE AS "MODEL_CODE",
|
||||
EXECUTOR_DATE AS "EXECUTOR_DATE",
|
||||
NOTE1 AS "NOTE1",
|
||||
NOTE2 AS "NOTE2",
|
||||
NOTE3 AS "NOTE3",
|
||||
NOTE4 AS "NOTE4",
|
||||
WRITER AS "WRITER",
|
||||
TO_CHAR(REGDATE, 'YYYY-MM-DD HH24:MI') AS "REGDATE",
|
||||
CHG_USER_ID AS "CHG_USER_ID",
|
||||
TO_CHAR(CHGDATE, 'YYYY-MM-DD HH24:MI') AS "CHGDATE",
|
||||
CATEGORIES_JSON AS "CATEGORIES_JSON"
|
||||
FROM
|
||||
ESTIMATE_TEMPLATE
|
||||
WHERE
|
||||
CONTRACT_OBJID = #{objId}
|
||||
ORDER BY
|
||||
TEMPLATE_TYPE,
|
||||
REGDATE DESC
|
||||
LIMIT 1
|
||||
</select>
|
||||
|
||||
<!-- 계약 정보 조회 (메일 발송용) -->
|
||||
<select id="getContractInfoForMail" parameterType="map" resultType="map">
|
||||
SELECT
|
||||
T.OBJID,
|
||||
T.CUSTOMER_OBJID,
|
||||
(SELECT SUPPLY_NAME FROM SUPPLY_MNG AS O WHERE O.OBJID = T.CUSTOMER_OBJID::NUMERIC) AS CUSTOMER_NAME,
|
||||
(SELECT EMAIL FROM SUPPLY_MNG AS O WHERE O.OBJID = T.CUSTOMER_OBJID::NUMERIC) AS CUSTOMER_EMAIL,
|
||||
T.CUSTOMER_PROJECT_NAME,
|
||||
T.CONTRACT_NO,
|
||||
T.PRODUCT,
|
||||
CODE_NAME(T.PRODUCT) AS PRODUCT_NAME,
|
||||
T.WRITER,
|
||||
(SELECT USER_NAME FROM USER_INFO AS O WHERE O.USER_ID = T.WRITER) AS WRITER_NAME,
|
||||
(SELECT EMAIL FROM USER_INFO AS O WHERE O.USER_ID = T.WRITER) AS WRITER_EMAIL,
|
||||
TO_CHAR(T.REGDATE, 'YYYY-MM-DD') AS REGDATE
|
||||
FROM
|
||||
CONTRACT_MGMT AS T
|
||||
WHERE
|
||||
OBJID::VARCHAR = #{objId}
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
@@ -28,6 +28,7 @@ import com.pms.common.SqlMapConfig;
|
||||
import com.pms.common.bean.PersonBean;
|
||||
import com.pms.common.utils.CommonUtils;
|
||||
import com.pms.common.utils.Constants;
|
||||
import com.pms.common.utils.MailUtil;
|
||||
import com.pms.service.CommonService;
|
||||
|
||||
/**
|
||||
@@ -1453,4 +1454,282 @@ public class ContractMgmtService {
|
||||
|
||||
return resultMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* 견적서 메일 발송
|
||||
* @param request
|
||||
* @param paramMap - objId (CONTRACT_OBJID)
|
||||
* @return
|
||||
*/
|
||||
public Map sendEstimateMail(HttpServletRequest request, Map paramMap){
|
||||
Map resultMap = new HashMap();
|
||||
SqlSession sqlSession = null;
|
||||
|
||||
try{
|
||||
sqlSession = SqlMapConfig.getInstance().getSqlSession(false);
|
||||
|
||||
String objId = CommonUtils.checkNull(paramMap.get("objId"));
|
||||
|
||||
// 1. 계약 정보 조회
|
||||
Map contractInfo = (Map) sqlSession.selectOne("contractMgmt.getContractInfoForMail", paramMap);
|
||||
if(contractInfo == null || contractInfo.isEmpty()){
|
||||
resultMap.put("result", "error");
|
||||
resultMap.put("message", "계약 정보를 찾을 수 없습니다.");
|
||||
return resultMap;
|
||||
}
|
||||
|
||||
// 2. 최종 차수 견적서 조회
|
||||
Map estimateTemplate = (Map) sqlSession.selectOne("contractMgmt.getLatestEstimateTemplate", paramMap);
|
||||
if(estimateTemplate == null || estimateTemplate.isEmpty()){
|
||||
resultMap.put("result", "error");
|
||||
resultMap.put("message", "견적서를 찾을 수 없습니다.");
|
||||
return resultMap;
|
||||
}
|
||||
|
||||
// 디버깅: estimateTemplate 출력
|
||||
System.out.println("===== Estimate Template Debug =====");
|
||||
for(Object key : estimateTemplate.keySet()){
|
||||
System.out.println(key + " = " + estimateTemplate.get(key));
|
||||
}
|
||||
System.out.println("===================================");
|
||||
|
||||
String templateObjId = CommonUtils.checkNull(estimateTemplate.get("objid"));
|
||||
if("".equals(templateObjId)) templateObjId = CommonUtils.checkNull(estimateTemplate.get("OBJID"));
|
||||
|
||||
String templateType = CommonUtils.checkNull(estimateTemplate.get("template_type"));
|
||||
if("".equals(templateType)) templateType = CommonUtils.checkNull(estimateTemplate.get("TEMPLATE_TYPE"));
|
||||
|
||||
// 3. 견적서 품목 조회
|
||||
Map itemParam = new HashMap();
|
||||
itemParam.put("templateObjId", templateObjId);
|
||||
List<Map> estimateItems = sqlSession.selectList("contractMgmt.getEstimateTemplateItemsByTemplateObjId", itemParam);
|
||||
|
||||
System.out.println("===== Estimate Items Debug =====");
|
||||
System.out.println("Items count: " + (estimateItems != null ? estimateItems.size() : 0));
|
||||
if(estimateItems != null && !estimateItems.isEmpty()){
|
||||
for(int i = 0; i < Math.min(3, estimateItems.size()); i++){
|
||||
Map item = estimateItems.get(i);
|
||||
System.out.println("Item " + (i+1) + " keys: " + item.keySet());
|
||||
}
|
||||
}
|
||||
System.out.println("================================");
|
||||
|
||||
// 4. 메일 제목 생성 (CONTRACT_OBJID 포함)
|
||||
String contractNo = CommonUtils.checkNull(contractInfo.get("contract_no"));
|
||||
String customerName = CommonUtils.checkNull(contractInfo.get("customer_name"));
|
||||
String subject = "[" + customerName + "] " + contractNo + " 견적서 [OBJID:" + objId + "]";
|
||||
|
||||
// 5. 메일 내용 생성
|
||||
String contents = makeEstimateMailContents(contractInfo, estimateTemplate, estimateItems);
|
||||
|
||||
// 6. 수신자 정보 설정
|
||||
ArrayList<String> toEmailList = new ArrayList<String>();
|
||||
String customerEmail = CommonUtils.checkNull(contractInfo.get("customer_email"));
|
||||
if(!"".equals(customerEmail)){
|
||||
toEmailList.add(customerEmail);
|
||||
} else {
|
||||
resultMap.put("result", "error");
|
||||
resultMap.put("message", "고객사 이메일 정보가 없습니다.");
|
||||
return resultMap;
|
||||
}
|
||||
|
||||
// 7. 참조: 작성자 이메일
|
||||
ArrayList<String> ccEmailList = new ArrayList<String>();
|
||||
String writerEmail = CommonUtils.checkNull(contractInfo.get("writer_email"));
|
||||
if(!"".equals(writerEmail)){
|
||||
ccEmailList.add(writerEmail);
|
||||
}
|
||||
|
||||
// 8. 메일 발송
|
||||
PersonBean person = (PersonBean)request.getSession().getAttribute(Constants.PERSON_BEAN);
|
||||
String fromUserId = person.getUserId();
|
||||
|
||||
System.out.println("===== 메일 발송 시도 =====");
|
||||
System.out.println("From UserId: " + fromUserId);
|
||||
System.out.println("To Email: " + toEmailList);
|
||||
System.out.println("CC Email: " + ccEmailList);
|
||||
System.out.println("Subject: " + subject);
|
||||
System.out.println("========================");
|
||||
|
||||
boolean mailSent = false;
|
||||
try {
|
||||
mailSent = MailUtil.sendMailWithAttachFile(
|
||||
fromUserId,
|
||||
null, // fromEmail (자동으로 SMTP 설정 사용)
|
||||
new ArrayList<String>(), // toUserIdList (빈 리스트)
|
||||
toEmailList,
|
||||
ccEmailList,
|
||||
new ArrayList<String>(), // bccEmailList (빈 리스트)
|
||||
null, // important
|
||||
subject,
|
||||
contents,
|
||||
null, // attachFileList (TODO: PDF 첨부 구현)
|
||||
"CONTRACT_ESTIMATE"
|
||||
);
|
||||
|
||||
System.out.println("메일 발송 결과: " + mailSent);
|
||||
|
||||
} catch(Exception mailEx) {
|
||||
System.out.println("메일 발송 중 예외 발생: " + mailEx.getMessage());
|
||||
mailEx.printStackTrace();
|
||||
resultMap.put("result", "error");
|
||||
resultMap.put("message", "메일 발송 중 예외가 발생했습니다: " + mailEx.getMessage());
|
||||
return resultMap;
|
||||
}
|
||||
|
||||
if(mailSent){
|
||||
resultMap.put("result", "success");
|
||||
resultMap.put("message", "견적서가 성공적으로 발송되었습니다.");
|
||||
} else {
|
||||
resultMap.put("result", "error");
|
||||
resultMap.put("message", "메일 발송에 실패했습니다.");
|
||||
}
|
||||
|
||||
}catch(Exception e){
|
||||
resultMap.put("result", "error");
|
||||
resultMap.put("message", "메일 발송 중 오류가 발생했습니다: " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
}finally{
|
||||
if(sqlSession != null) sqlSession.close();
|
||||
}
|
||||
|
||||
return resultMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* 견적서 메일 내용 생성
|
||||
* @param contractInfo
|
||||
* @param estimateTemplate
|
||||
* @param estimateItems
|
||||
* @return
|
||||
*/
|
||||
private String makeEstimateMailContents(Map contractInfo, Map estimateTemplate, List<Map> estimateItems){
|
||||
StringBuilder contents = new StringBuilder();
|
||||
|
||||
contents.append("<html>");
|
||||
contents.append("<head>");
|
||||
contents.append("<meta charset='UTF-8'>");
|
||||
contents.append("<style>");
|
||||
contents.append("body { font-family: 'Malgun Gothic', '맑은 고딕', Arial, sans-serif; margin: 20px; background-color: #f5f5f5; }");
|
||||
contents.append(".estimate-container { max-width: 800px; 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(".info-table { width: 100%; border-collapse: collapse; margin-bottom: 30px; }");
|
||||
contents.append(".info-table td { padding: 8px; border: 1px solid #000; font-size: 10pt; }");
|
||||
contents.append(".info-table .label { background-color: #f0f0f0; font-weight: bold; width: 100px; text-align: center; }");
|
||||
contents.append(".items-table { width: 100%; border-collapse: collapse; margin: 20px 0; }");
|
||||
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; }");
|
||||
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>");
|
||||
contents.append("<div class='estimate-container'>");
|
||||
|
||||
// 제목
|
||||
contents.append("<div class='title'>견 적 서</div>");
|
||||
|
||||
// estimateTemplate 키는 대문자/소문자 모두 체크
|
||||
String recipient = CommonUtils.checkNull(estimateTemplate.get("recipient"));
|
||||
if("".equals(recipient)) recipient = CommonUtils.checkNull(estimateTemplate.get("RECIPIENT"));
|
||||
|
||||
String contactPerson = CommonUtils.checkNull(estimateTemplate.get("contact_person"));
|
||||
if("".equals(contactPerson)) contactPerson = CommonUtils.checkNull(estimateTemplate.get("CONTACT_PERSON"));
|
||||
|
||||
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_date"));
|
||||
if("".equals(executorDate)) executorDate = CommonUtils.checkNull(estimateTemplate.get("EXECUTOR_DATE"));
|
||||
|
||||
// 기본 정보 테이블
|
||||
contents.append("<table class='info-table'>");
|
||||
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("<tr><td class='label'>발행일</td><td>" + executorDate + "</td></tr>");
|
||||
contents.append("</table>");
|
||||
|
||||
// 견적 품목
|
||||
if(estimateItems != null && !estimateItems.isEmpty()){
|
||||
contents.append("<table class='items-table'>");
|
||||
contents.append("<thead>");
|
||||
contents.append("<tr>");
|
||||
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>");
|
||||
|
||||
long totalAmount = 0;
|
||||
for(int i = 0; i < estimateItems.size(); i++){
|
||||
Map item = estimateItems.get(i);
|
||||
String amount = CommonUtils.checkNull(item.get("amount"));
|
||||
if(!"".equals(amount)){
|
||||
try {
|
||||
totalAmount += Long.parseLong(amount.replace(",", ""));
|
||||
} catch(Exception e){}
|
||||
}
|
||||
|
||||
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>");
|
||||
}
|
||||
|
||||
// 합계
|
||||
contents.append("<tr style='background-color:#f0f0f0; font-weight:bold;'>");
|
||||
contents.append("<td colspan='6' class='text-right'>합계</td>");
|
||||
contents.append("<td class='text-right'>₩" + String.format("%,d", totalAmount) + "</td>");
|
||||
contents.append("<td></td>");
|
||||
contents.append("</tr>");
|
||||
contents.append("</tbody>");
|
||||
contents.append("</table>");
|
||||
}
|
||||
|
||||
// 비고 (대소문자 모두 체크)
|
||||
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>");
|
||||
if(!"".equals(note1)) contents.append("<div>1. " + note1 + "</div>");
|
||||
if(!"".equals(note2)) contents.append("<div>2. " + note2 + "</div>");
|
||||
if(!"".equals(note3)) contents.append("<div>3. " + note3 + "</div>");
|
||||
if(!"".equals(note4)) contents.append("<div>4. " + note4 + "</div>");
|
||||
contents.append("</div>");
|
||||
}
|
||||
|
||||
contents.append("</div>"); // estimate-container 닫기
|
||||
contents.append("</body>");
|
||||
contents.append("</html>");
|
||||
|
||||
return contents.toString();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user