feat: 견적관리 메일 발송 기능 추가 및 UI 개선

- 견적서 메일 발송 API 추가 (ContractMgmtController, ContractMgmtService)
- 견적관리 리스트에 메일 발송 상태 및 발송일시 컬럼 추가
- 메일 내용을 견적서 형식과 동일하게 변경 (품목 테이블 포함)
- 메일 제목에 영업번호 및 OBJID 포함하여 발송 이력 추적 가능
- 견적서 템플릿: 단가/금액 콤마 표시 기능 추가
- 견적서 템플릿: 비고 컬럼 너비 확대
- S/N 모달창 텍스트 색상 개선 (가독성 향상)
- 견적서 수정 시 특정 템플릿만 업데이트되도록 SQL 쿼리 수정
This commit is contained in:
2025-10-16 13:24:08 +09:00
parent 1b84bee342
commit 546e8e8e02
7 changed files with 583 additions and 58 deletions

View File

@@ -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");
}
});

View File

@@ -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;

View File

@@ -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>

View File

@@ -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);
});