feat: 품번/품명 Select2 AJAX 검색 기능 추가

- common.js에 initPartSelect2Ajax 함수 추가 (기존 select2 영향 없음)
- estimateList_new.jsp: 품번/품명 AJAX 검색 적용
- orderMgmtList.jsp: 품번/품명 AJAX 검색 적용
- 디버깅 로그 추가로 문제 해결 용이
This commit is contained in:
2025-10-20 15:55:15 +09:00
parent 889231ae27
commit ca25e0de56
10 changed files with 934 additions and 257 deletions

View File

@@ -24,8 +24,23 @@ $(document).ready(function(){
//날짜
_fnc_datepick();
// jQuery 및 select2 로드 확인
console.log('jQuery loaded:', typeof $ !== 'undefined');
console.log('jQuery.fn exists:', typeof $.fn !== 'undefined');
console.log('select2 loaded:', typeof $.fn !== 'undefined' && typeof $.fn.select2 !== 'undefined');
$('.select2').select2();
// select2가 로드되었을 때만 초기화
if(typeof $.fn !== 'undefined' && typeof $.fn.select2 === 'function') {
$('.select2').select2();
// 품번/품명 Select2 AJAX 초기화 (common.js의 새 함수 사용)
initPartSelect2Ajax("#search_partNo", "#search_partName", "#search_partObjId", {
debug: true // 디버깅 모드 활성화
});
} else {
console.error('select2 라이브러리가 로드되지 않았습니다.');
console.error('$.fn:', $.fn);
}
$("#btnSearch").click(function(){
$("#page").val("1");
@@ -178,7 +193,7 @@ $(document).ready(function(){
// 메일 발송 확인
Swal.fire({
title: '견적서 메일 발송',
text: "최종 차수의 견적서를 PDF로 발송하시겠습니까?",
text: "최종 차수의 견적서를 발송하시겠습니까?",
icon: 'question',
showCancelButton: true,
confirmButtonText: '발송',
@@ -381,6 +396,20 @@ var columns = [
//var grid;
function fn_search(){
// Select2 값을 실제 input/select에 명시적으로 설정
var partObjId = $("#search_partObjId").val();
// 디버깅: 검색 조건 확인
console.log("품번:", $("#search_partNo").val());
console.log("품명:", $("#search_partName").val());
console.log("품목 OBJID:", partObjId);
// 품목 OBJID가 있으면 hidden 필드에 확실히 설정
if(partObjId && partObjId !== '') {
// 폼 데이터에 명시적으로 추가
console.log("품목 검색 조건 설정됨:", partObjId);
}
_tabulGrid = fnc_tabul_search(_tabul_layout_fitColumns, _tabulGrid, "/contractMgmt/contractGridList.do", columns, true);
}
@@ -495,6 +524,7 @@ function fn_showEstimateList(contractObjId){
html += '<th style="border: 1px solid #ddd; padding: 8px;">견적번호</th>';
html += '<th style="border: 1px solid #ddd; padding: 8px;">작성일</th>';
html += '<th style="border: 1px solid #ddd; padding: 8px;">작성자</th>';
html += '<th style="border: 1px solid #ddd; padding: 8px;">결재상태</th>';
html += '</tr></thead><tbody>';
data.list.forEach(function(item){
@@ -508,6 +538,19 @@ function fn_showEstimateList(contractObjId){
var estimateNo = item.ESTIMATE_NO || item.estimate_no || item.estimateNo || '-';
var regdate = item.REGDATE || item.regdate || '';
var writer = item.WRITER || item.writer || '';
var apprStatus = item.APPR_STATUS || item.appr_status || item.apprStatus || '-';
// 결재상태별 색상 지정
var statusColor = '#333';
if(apprStatus === '결재완료') {
statusColor = 'green';
} else if(apprStatus === '결재중') {
statusColor = 'blue';
} else if(apprStatus === '반려') {
statusColor = 'red';
} else if(apprStatus === '작성중') {
statusColor = '#999';
}
html += '<tr style="cursor: pointer;" onclick="fn_openEstimateByObjId(\'' + objid + '\', \'' + templateType + '\')">';
html += '<td style="border: 1px solid #ddd; padding: 8px; text-align: center;">' + revision + '차</td>';
@@ -515,6 +558,7 @@ function fn_showEstimateList(contractObjId){
html += '<td style="border: 1px solid #ddd; padding: 8px; text-align: center;">' + estimateNo + '</td>';
html += '<td style="border: 1px solid #ddd; padding: 8px; text-align: center;">' + regdate + '</td>';
html += '<td style="border: 1px solid #ddd; padding: 8px; text-align: center;">' + writer + '</td>';
html += '<td style="border: 1px solid #ddd; padding: 8px; text-align: center; color: ' + statusColor + '; font-weight: bold;">' + apprStatus + '</td>';
html += '</tr>';
});
@@ -855,18 +899,23 @@ function openProjectFormPopUp(objId){
</select>
</td>
<td class="align_r">
<label for="" class="">품번</label>
</td>
<td>
<input type="text" name="search_partNo" id="search_partNo" value="${param.search_partNo}"/>
</td>
<td class="align_r">
<label for="" class="">품명</label>
</td>
<td colspan="3">
<input type="text" name="search_partName" id="search_partName" value="${param.search_partName}"/>
</td>
<td class="align_r">
<label for="" class="">품번</label>
</td>
<td>
<select name="search_partNo" id="search_partNo" class="select2-part" style="width: 100%;">
<option value="">품번 선택</option>
</select>
<input type="hidden" name="search_partObjId" id="search_partObjId" value=""/>
</td>
<td class="align_r">
<label for="" class="">품명</label>
</td>
<td colspan="3">
<select name="search_partName" id="search_partName" class="select2-part" style="width: 100%;">
<option value="">품명 선택</option>
</select>
</td>
</tr>
<tr>

View File

@@ -224,6 +224,7 @@ var g_contractObjId = "<%=objId%>";
var g_templateObjId = "<%=templateObjId%>";
var g_exchangeRate = 1; // 환율 (기본값 1)
var g_currencyName = "KRW"; // 통화명 (기본값 원화)
var g_apprStatus = ""; // 결재상태
$(function(){
@@ -293,6 +294,33 @@ $(function(){
}
});
// 결재상태에 따라 버튼 표시 제어
function fn_controlButtons() {
if(g_apprStatus === "결재완료") {
// 결재완료된 경우 행추가, 저장 버튼 숨김
$("#btnAddItem").hide();
$("#btnSave").hide();
// 모든 입력 필드를 읽기 전용으로 변경
$("input, textarea").attr("readonly", true);
$("input, textarea").css("background-color", "#f5f5f5");
// 삭제 버튼 숨김
$(".btn-delete-row").hide();
} else {
// 결재완료가 아닌 경우 버튼 표시
$("#btnAddItem").show();
$("#btnSave").show();
// 입력 필드 활성화
$("input, textarea").attr("readonly", false);
$("input, textarea").css("background-color", "");
// 삭제 버튼 표시
$(".btn-delete-row").show();
}
}
// 금액 계산
function fn_calculateAmount(row) {
var qty = row.find(".item-qty").val().replace(/,/g, "") || "0";
@@ -376,12 +404,17 @@ function fn_loadData() {
g_exchangeRate = parseFloat(data.estimate.EXCHANGE_RATE || "1");
g_currencyName = data.estimate.CONTRACT_CURRENCY_NAME || "KRW";
// 데이터 바인딩
$("#executor").val(data.estimate.EXECUTOR || "");
$("#recipient").val(data.estimate.RECIPIENT || "");
$("#estimate_no").val(data.estimate.ESTIMATE_NO || "");
$("#contact_person").val(data.estimate.CONTACT_PERSON || "");
$("#greeting_text").val(data.estimate.GREETING_TEXT || "견적을 요청해 주셔서 대단히 감사합니다.\n하기와 같이 견적서를 제출합니다.");
// 결재상태 저장
g_apprStatus = data.estimate.APPR_STATUS || "작성중";
// 데이터 바인딩
$("#executor").val(data.estimate.EXECUTOR || "");
$("#recipient").val(data.estimate.RECIPIENT || "");
$("#estimate_no").val(data.estimate.ESTIMATE_NO || "");
$("#contact_person").val(data.estimate.CONTACT_PERSON || "");
$("#greeting_text").val(data.estimate.GREETING_TEXT || "견적을 요청해 주셔서 대단히 감사합니다.\n하기와 같이 견적서를 제출합니다.");
$("#manager_name").val(data.estimate.MANAGER_NAME || "영업부");
$("#manager_contact").val(data.estimate.MANAGER_CONTACT || "");
// 품목 데이터 로드
if(data.items && data.items.length > 0) {
@@ -423,18 +456,24 @@ function fn_loadData() {
itemsHtml += '</td>';
itemsHtml += '</tr>';
$("#itemsTableBody").html(itemsHtml);
// 합계 계산
fn_calculateTotal();
}
$("#itemsTableBody").html(itemsHtml);
// 비고 로드
$("#note1").val(data.estimate.NOTE1 || "1. 견적유효기간: 일");
$("#note2").val(data.estimate.NOTE2 || "2. 납품기간: 발주 후 1주 이내");
$("#note3").val(data.estimate.NOTE3 || "3. VAT 별도");
$("#note4").val(data.estimate.NOTE4 || "4. 결제 조건 : 기존 결제조건에 따름.");
// 비고 로드 (테이블 생성 직후)
$("#note_remarks").val(data.estimate.NOTE_REMARKS || "");
// 합계 계산
fn_calculateTotal();
}
// 하단 비고 로드
$("#note1").val(data.estimate.NOTE1 || "1. 견적유효기간: 일");
$("#note2").val(data.estimate.NOTE2 || "2. 납품기간: 발주 후 1주 이내");
$("#note3").val(data.estimate.NOTE3 || "3. VAT 별도");
$("#note4").val(data.estimate.NOTE4 || "4. 결제 조건 : 기존 결제조건에 따름.");
// 결재상태에 따라 버튼 제어
fn_controlButtons();
}
},
error: function() {
Swal.fire("데이터를 불러오는데 실패했습니다.");
@@ -461,33 +500,45 @@ function fn_loadTemplateData(templateObjId){
g_contractObjId = contractObjId;
}
// 환율 정보 저장
g_exchangeRate = parseFloat(template.EXCHANGE_RATE || template.exchange_rate || template.exchangeRate || "1");
g_currencyName = template.CONTRACT_CURRENCY_NAME || template.contract_currency_name || template.contractCurrencyName || "KRW";
// 대문자/소문자 모두 지원
// 환율 정보 저장
g_exchangeRate = parseFloat(template.EXCHANGE_RATE || template.exchange_rate || template.exchangeRate || "1");
g_currencyName = template.CONTRACT_CURRENCY_NAME || template.contract_currency_name || template.contractCurrencyName || "KRW";
// 결재상태 저장
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 || "";
// 기본 정보 채우기
$("#executor").val(executor);
$("#recipient").val(recipient);
$("#estimate_no").val(estimateNo);
$("#contact_person").val(contactPerson);
$("#greeting_text").val(greetingText);
$("#note1").val(note1);
$("#note2").val(note2);
$("#note3").val(note3);
$("#note4").val(note4);
// 품목 데이터 채우기
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);
$("#recipient").val(recipient);
$("#estimate_no").val(estimateNo);
$("#contact_person").val(contactPerson);
$("#greeting_text").val(greetingText);
$("#manager_name").val(managerName);
$("#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 || "";
// 품목 데이터 채우기
if(data.items && data.items.length > 0){
$("#itemsTableBody").empty();
data.items.forEach(function(item, idx){
@@ -530,26 +581,32 @@ function fn_loadTemplateData(templateObjId){
$("#itemsTableBody").append(totalKRWRow);
// 비고 행 추가
var remarksRow = $("<tr class='remarks-row'>");
remarksRow.append('<td colspan="8" style="height: 100px; vertical-align: top; padding: 10px; text-align: left;">' +
'<div style="font-weight: bold; margin-bottom: 10px; text-align: left;">&lt;비고&gt;</div>' +
'<textarea id="note_remarks" style="width: 100%; height: 70px; border: none; resize: none; font-family: inherit; font-size: 10pt; text-align: left;"></textarea>' +
'</td>');
$("#itemsTableBody").append(remarksRow);
// 합계 계산
fn_calculateTotal();
}
} else {
console.error("데이터 로드 실패:", data);
Swal.fire("데이터를 불러오는데 실패했습니다.");
var remarksRow = $("<tr class='remarks-row'>");
remarksRow.append('<td colspan="8" style="height: 100px; vertical-align: top; padding: 10px; text-align: left;">' +
'<div style="font-weight: bold; margin-bottom: 10px; text-align: left;">&lt;비고&gt;</div>' +
'<textarea id="note_remarks" style="width: 100%; height: 70px; border: none; resize: none; font-family: inherit; font-size: 10pt; text-align: left;"></textarea>' +
'</td>');
$("#itemsTableBody").append(remarksRow);
// 테이블 내 비고 값 설정 (textarea 생성 직후)
$("#note_remarks").val(noteRemarks);
// 합계 계산
fn_calculateTotal();
// 결재상태에 따라 버튼 제어
fn_controlButtons();
}
},
error: function(xhr, status, error){
console.error("AJAX 오류:", xhr, status, error);
} else {
console.error("데이터 로드 실패:", data);
Swal.fire("데이터를 불러오는데 실패했습니다.");
}
});
},
error: function(xhr, status, error){
console.error("AJAX 오류:", xhr, status, error);
Swal.fire("데이터를 불러오는데 실패했습니다.");
}
});
}
// 저장
@@ -596,6 +653,9 @@ function fn_save() {
greeting_text: $("#greeting_text").val(),
total_amount: totalAmount, // 합계
total_amount_krw: totalAmountKRW, // 원화환산 공급가액
manager_name: $("#manager_name").val(), // 담당자
manager_contact: $("#manager_contact").val(), // 연락처
note_remarks: $("#note_remarks").val(), // 테이블 내 비고
note1: $("#note1").val(),
note2: $("#note2").val(),
note3: $("#note3").val(),
@@ -665,10 +725,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;">
담당자 : 영업부<br>
연락처 :
</div>
<div style="text-align: left; font-size: 9pt; line-height: 1.8; padding: 0 5px;">
담당자 : <input type="text" id="manager_name" value="영업부" style="width: 80px; border: none; border-bottom: 1px solid #ddd; font-size: 9pt; padding: 2px;"><br>
연락처 : <input type="text" id="manager_contact" value="" style="width: 120px; border: none; border-bottom: 1px solid #ddd; font-size: 9pt; padding: 2px;">
</div>
</td>
</tr>
<tr>

View File

@@ -27,6 +27,11 @@ $(document).ready(function(){
$('.select2').select2();
// 품번/품명 Select2 AJAX 초기화 (common.js의 새 함수 사용)
initPartSelect2Ajax("#search_partNo", "#search_partName", "#search_partObjId", {
debug: false // 디버깅 모드 비활성화
});
$("#btnSearch").click(function(){
$("#page").val("1");
fn_search();
@@ -599,18 +604,23 @@ function openProjectFormPopUp(objId){
</select>
</td>
<td class="align_r">
<label for="" class="">품번</label>
</td>
<td>
<input type="text" name="search_partNo" id="search_partNo" value="${param.search_partNo}"/>
</td>
<td class="align_r">
<label for="" class="">품명</label>
</td>
<td colspan="3">
<input type="text" name="search_partName" id="search_partName" value="${param.search_partName}"/>
</td>
<td class="align_r">
<label for="" class="">품번</label>
</td>
<td>
<select name="search_partNo" id="search_partNo" class="select2-part" style="width: 100%;">
<option value="">품번 선택</option>
</select>
<input type="hidden" name="search_partObjId" id="search_partObjId" value=""/>
</td>
<td class="align_r">
<label for="" class="">품명</label>
</td>
<td colspan="3">
<select name="search_partName" id="search_partName" class="select2-part" style="width: 100%;">
<option value="">품명 선택</option>
</select>
</td>
</tr>
<tr>

View File

@@ -3255,4 +3255,182 @@ function fnc_tabulCallbackFnc(objid, docType, columnField, fileCnt){
});
}
//tabulator용 Function 종료
//tabulator용 Function 종료
/**
* 품번/품명 Select2 AJAX 검색 함수 (견적 목록 전용)
* @param {string} partNoSelectId - 품번 셀렉트 박스 ID (예: "#search_partNo")
* @param {string} partNameSelectId - 품명 셀렉트 박스 ID (예: "#search_partName")
* @param {string} partObjIdInputId - 품목 OBJID hidden 필드 ID (예: "#search_partObjId")
* @param {object} options - 추가 옵션 (placeholder, minimumInputLength 등)
*/
function initPartSelect2Ajax(partNoSelectId, partNameSelectId, partObjIdInputId, options) {
options = options || {};
var partNoPlaceholder = options.partNoPlaceholder || "품번 입력하여 검색...";
var partNamePlaceholder = options.partNamePlaceholder || "품명 입력하여 검색...";
var minimumInputLength = options.minimumInputLength || 1;
var searchUrl = options.searchUrl || '/contractMgmt/searchPartList.do';
var debug = options.debug || false;
// 품번 Select2 AJAX 설정
$(partNoSelectId).select2({
placeholder: partNoPlaceholder,
allowClear: true,
width: '100%',
minimumInputLength: minimumInputLength,
language: {
inputTooShort: function() {
return "최소 " + minimumInputLength + "글자 이상 입력하세요";
},
searching: function() {
return "검색 중...";
},
noResults: function() {
return "검색 결과가 없습니다";
}
},
ajax: {
url: searchUrl,
dataType: 'json',
type: 'POST',
contentType: 'application/x-www-form-urlencoded; charset=UTF-8',
delay: 250,
data: function(params) {
return {
searchTerm: params.term
};
},
processResults: function(data) {
var results = $.map(data, function(item) {
var objId = item.OBJID || item.objid || item.objId;
var partNo = item.PART_NO || item.part_no || item.partNo;
var partName = item.PART_NAME || item.part_name || item.partName;
return {
id: partNo,
text: partNo,
objId: objId,
partName: partName,
partNo: partNo
};
});
return {
results: results
};
},
cache: true
}
});
// 품명 Select2 AJAX 설정
$(partNameSelectId).select2({
placeholder: partNamePlaceholder,
allowClear: true,
width: '100%',
minimumInputLength: minimumInputLength,
language: {
inputTooShort: function() {
return "최소 " + minimumInputLength + "글자 이상 입력하세요";
},
searching: function() {
return "검색 중...";
},
noResults: function() {
return "검색 결과가 없습니다";
}
},
ajax: {
url: searchUrl,
dataType: 'json',
type: 'POST',
contentType: 'application/x-www-form-urlencoded; charset=UTF-8',
delay: 250,
data: function(params) {
return {
searchTerm: params.term
};
},
processResults: function(data) {
var results = $.map(data, function(item) {
var objId = item.OBJID || item.objid || item.objId;
var partNo = item.PART_NO || item.part_no || item.partNo;
var partName = item.PART_NAME || item.part_name || item.partName;
return {
id: partName,
text: partName,
objId: objId,
partName: partName,
partNo: partNo
};
});
return {
results: results
};
},
cache: true
}
});
// 품번 변경 이벤트 핸들러
var partNoChangeHandler = function() {
var selectedData = $(this).select2('data')[0];
if(debug) console.log("품번 변경됨:", selectedData);
if(selectedData && selectedData.objId) {
if(debug) console.log("품목 OBJID 설정:", selectedData.objId);
$(partObjIdInputId).val(selectedData.objId);
// 품명 셀렉트박스도 동기화
var $partNameSelect = $(partNameSelectId);
$partNameSelect.off('change');
if($partNameSelect.find("option[value='" + selectedData.partName + "']").length === 0) {
var newOption = new Option(selectedData.partName, selectedData.partName, true, true);
$partNameSelect.append(newOption);
} else {
$partNameSelect.val(selectedData.partName);
}
$partNameSelect.trigger('change.select2');
setTimeout(function() {
$partNameSelect.on('change', partNameChangeHandler);
}, 100);
} else {
$(partObjIdInputId).val("");
}
};
// 품명 변경 이벤트 핸들러
var partNameChangeHandler = function() {
var selectedData = $(this).select2('data')[0];
if(debug) console.log("품명 변경됨:", selectedData);
if(selectedData && selectedData.objId) {
if(debug) console.log("품목 OBJID 설정:", selectedData.objId);
$(partObjIdInputId).val(selectedData.objId);
// 품번 셀렉트박스도 동기화
var $partNoSelect = $(partNoSelectId);
$partNoSelect.off('change');
if($partNoSelect.find("option[value='" + selectedData.partNo + "']").length === 0) {
var newOption = new Option(selectedData.partNo, selectedData.partNo, true, true);
$partNoSelect.append(newOption);
} else {
$partNoSelect.val(selectedData.partNo);
}
$partNoSelect.trigger('change.select2');
setTimeout(function() {
$partNoSelect.on('change', partNoChangeHandler);
}, 100);
} else {
$(partObjIdInputId).val("");
}
};
// 이벤트 연결
$(partNoSelectId).on('change', partNoChangeHandler);
$(partNameSelectId).on('change', partNameChangeHandler);
}

View File

@@ -0,0 +1,16 @@
-- ESTIMATE_TEMPLATE 테이블에 담당자 정보 및 테이블 내 비고 컬럼 추가
ALTER TABLE ESTIMATE_TEMPLATE
ADD COLUMN IF NOT EXISTS MANAGER_NAME VARCHAR(100);
ALTER TABLE ESTIMATE_TEMPLATE
ADD COLUMN IF NOT EXISTS MANAGER_CONTACT VARCHAR(100);
ALTER TABLE ESTIMATE_TEMPLATE
ADD COLUMN IF NOT EXISTS NOTE_REMARKS TEXT;
-- 컬럼 설명 추가
COMMENT ON COLUMN ESTIMATE_TEMPLATE.MANAGER_NAME IS '담당자 이름';
COMMENT ON COLUMN ESTIMATE_TEMPLATE.MANAGER_CONTACT IS '담당자 연락처';
COMMENT ON COLUMN ESTIMATE_TEMPLATE.NOTE_REMARKS IS '테이블 내 비고 (품목 하단)';

View File

@@ -0,0 +1,27 @@
-- PROJECT_MGMT 테이블에 품목 관련 컬럼 추가
-- 품목별 프로젝트 생성을 위한 컬럼들
ALTER TABLE PROJECT_MGMT
ADD COLUMN IF NOT EXISTS PART_OBJID VARCHAR(50);
ALTER TABLE PROJECT_MGMT
ADD COLUMN IF NOT EXISTS PART_NO VARCHAR(100);
ALTER TABLE PROJECT_MGMT
ADD COLUMN IF NOT EXISTS PART_NAME VARCHAR(200);
ALTER TABLE PROJECT_MGMT
ADD COLUMN IF NOT EXISTS QUANTITY VARCHAR(50);
-- 컬럼 설명 추가
COMMENT ON COLUMN PROJECT_MGMT.PART_OBJID IS '품목 OBJID (품목별 프로젝트 생성 시 사용)';
COMMENT ON COLUMN PROJECT_MGMT.PART_NO IS '품번';
COMMENT ON COLUMN PROJECT_MGMT.PART_NAME IS '품명';
COMMENT ON COLUMN PROJECT_MGMT.QUANTITY IS '수주수량';
-- 기존 인덱스 확인 후 추가 (성능 향상)
CREATE INDEX IF NOT EXISTS idx_project_mgmt_contract_part
ON PROJECT_MGMT(CONTRACT_OBJID, PART_OBJID);
COMMENT ON INDEX idx_project_mgmt_contract_part IS '계약-품목별 프로젝트 조회 성능 향상';

View File

@@ -691,6 +691,7 @@ public class MailUtil {
//◆◆◆ 3. db log & send mail ◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆
SqlSession sqlSession = null;
boolean mailSendSuccess = false; // 메일 발송 성공 여부
Map paramMap = new HashMap();
try{
@@ -715,6 +716,7 @@ public class MailUtil {
//◆◆◆ send mail ◆◆◆
Transport.send(message);
mailSendSuccess = true; // 메일 발송 성공
if(Constants.Mail.dbLogWrite){
System.out.println("메일 발송후 paramMap >> "+paramMap);
@@ -722,6 +724,7 @@ public class MailUtil {
}
}catch(Exception sqle){
mailSendSuccess = false; // 메일 발송 실패
if(Constants.Mail.dbLogWrite){
paramMap.put("errorLog", sqle.getMessage());
System.out.println("메일 발송 오류 paramMap >> "+paramMap);
@@ -735,7 +738,7 @@ public class MailUtil {
}
}
return true;
return mailSendSuccess; // 실제 발송 성공 여부 반환
}catch(Exception e) {
e.printStackTrace();
return false;

View File

@@ -7362,16 +7362,20 @@ SELECT
,EST_USER_ID
,EST_COMP_DATE
,EST_RESULT_CD
,AREA_CD
,MECHANICAL_TYPE
,OVERHAUL_ORDER
,IS_TEMP
)
,AREA_CD
,MECHANICAL_TYPE
,OVERHAUL_ORDER
,IS_TEMP
,PART_OBJID
,PART_NO
,PART_NAME
,QUANTITY
)
(
SELECT
#{OBJID}
,#{objId}
,#{objId}
,CATEGORY_CD
,CUSTOMER_OBJID
,PRODUCT
@@ -7390,29 +7394,71 @@ SELECT
,CHG_USER_ID
,PLAN_DATE
,COMPLETE_DATE
,RESULT_CD
<choose>
<when test="overhaul_project_no != null and overhaul_project_no !='' ">
,#{overhaul_project_no}<!-- || '_' || #{overhaul_order} -->
</when>
<otherwise>
,MECHANICAL_TYPE || '-' ||
<!--
(SELECT (SUBSTRING(PROJECT_NO,POSITION('-' IN PROJECT_NO)+1))::integer+1 FROM PROJECT_MGMT WHERE PROJECT_NO NOT LIKE '%\_%' ESCAPE '\' ORDER BY SUBSTRING(PROJECT_NO,POSITION('-' IN PROJECT_NO)+1) DESC LIMIT 1)
-->
(SELECT CASE
WHEN PROJECT_NO ~ '[-\s][0-9]+$'
THEN REGEXP_REPLACE(PROJECT_NO, '.*[-\s]([0-9]+)$', '\1')::integer+1
ELSE NULL
END AS extracted_number
FROM PROJECT_MGMT
WHERE PROJECT_NO NOT LIKE '%\_%' ESCAPE '\'
ORDER BY extracted_number DESC NULLS LAST
LIMIT 1)
</otherwise>
</choose>
<!-- ,#{project_no} -->
<!-- ,(SELECT TITLE FROM PMS_WBS_TEMPLATE PWT WHERE PWT.OBJID = #{mechanical_type}) || '-' || (SELECT (SUBSTRING(PROJECT_NO,POSITION('-' IN PROJECT_NO)+1))::integer+1 FROM PROJECT_MGMT ORDER BY SUBSTRING(PROJECT_NO,POSITION('-' IN PROJECT_NO)+1) DESC LIMIT 1) -->
,RESULT_CD
<!-- 기존 PROJECT_NO 자동생성 로직 (주석처리)
<choose>
<when test="overhaul_project_no != null and overhaul_project_no !='' ">
,#{overhaul_project_no}
</when>
<otherwise>
,MECHANICAL_TYPE || '-' ||
(SELECT CASE
WHEN PROJECT_NO ~ '[-\s][0-9]+$'
THEN REGEXP_REPLACE(PROJECT_NO, '.*[-\s]([0-9]+)$', '\1')::integer+1
ELSE NULL
END AS extracted_number
FROM PROJECT_MGMT
WHERE PROJECT_NO NOT LIKE '%\_%' ESCAPE '\'
ORDER BY extracted_number DESC NULLS LAST
LIMIT 1)
</otherwise>
</choose>
-->
<!-- 신규 PROJECT_NO 생성 로직: 주문유형-제품구분-날짜-순번 형식 (예: R-AS-250302-001) -->
,(
SELECT
-- 주문유형 코드 (CATEGORY_CD를 영문 약어로 매핑)
CASE CODE_NAME(CATEGORY_CD)
WHEN '오버홀' THEN 'O'
WHEN '개조' THEN 'M'
WHEN '개발' THEN 'D'
WHEN '견적' THEN 'Q'
WHEN '수리' THEN 'R'
WHEN '판매' THEN 'S'
ELSE 'T'
END || '-' ||
-- 제품구분 코드 (PRODUCT의 CODE_NAME에서 슬래시 제거)
REPLACE(CODE_NAME(PRODUCT), '/', '') || '-' ||
-- 날짜 (YYMMDD)
TO_CHAR(CURRENT_DATE, 'YYMMDD') || '-' ||
-- 순번 (001, 002, ...)
LPAD(
COALESCE(
(
SELECT MAX(SUBSTRING(PROJECT_NO FROM '\d{3}$')::INTEGER) + 1
FROM PROJECT_MGMT
WHERE PROJECT_NO LIKE
CASE CODE_NAME(CATEGORY_CD)
WHEN '오버홀' THEN 'O'
WHEN '개조' THEN 'M'
WHEN '개발' THEN 'D'
WHEN '견적' THEN 'Q'
WHEN '수리' THEN 'R'
WHEN '판매' THEN 'S'
ELSE 'T'
END || '-' ||
REPLACE(CODE_NAME(PRODUCT), '/', '') || '-' ||
TO_CHAR(CURRENT_DATE, 'YYMMDD') || '-%'
),
1
)::TEXT,
3,
'0'
)
FROM CONTRACT_MGMT
WHERE OBJID = #{objId}
)
,PM_USER_ID
,#{contract_price}
,#{contract_price_currency}
@@ -7437,12 +7483,16 @@ SELECT
,EST_USER_ID
,EST_COMP_DATE
,EST_RESULT_CD
,AREA_CD
,MECHANICAL_TYPE
,#{overhaul_order}
,#{is_temp}
FROM CONTRACT_MGMT
WHERE OBJID=#{objId}
,AREA_CD
,MECHANICAL_TYPE
,#{overhaul_order}
,#{is_temp}
,#{part_objid}
,#{part_no}
,#{part_name}
,#{quantity}
FROM CONTRACT_MGMT
WHERE OBJID=#{objId}
)
</insert>
@@ -7549,7 +7599,12 @@ SELECT
,REQ_DEL_DATE = #{req_del_date}
,CONTRACT_COMPANY = #{contract_company}
,MANUFACTURE_PLANT = #{manufacture_plant}
,PART_OBJID = #{part_objid}
,PART_NO = #{part_no}
,PART_NAME = #{part_name}
,QUANTITY = #{quantity}
WHERE CONTRACT_OBJID = #{objId}
AND PART_OBJID = #{part_objid}
</update>
<delete id="deleteProjectMngInfo" parameterType="map">

View File

@@ -463,8 +463,9 @@
,TO_CHAR(REGDATE,'YYYY-MM-DD') AS REG_DATE
,WRITER
,(SELECT USER_NAME FROM USER_INFO AS O WHERE O.USER_ID = T.WRITER ) AS WRITER_NAME
,(SELECT COUNT(1) FROM ATTACH_FILE_INFO WHERE TARGET_OBJID = T.OBJID AND DOC_TYPE='contractMgmt01' AND UPPER(STATUS) = 'ACTIVE') AS CU01_CNT
,(SELECT COUNT(1) FROM ATTACH_FILE_INFO WHERE TARGET_OBJID = T.OBJID AND DOC_TYPE='contractMgmt02' AND UPPER(STATUS) = 'ACTIVE') AS CU02_CNT
,(SELECT COUNT(1) FROM ATTACH_FILE_INFO WHERE TARGET_OBJID = T.OBJID AND DOC_TYPE='ORDER_DOC' AND UPPER(STATUS) = 'ACTIVE') AS CU01_CNT
<!-- ,(SELECT COUNT(1) FROM ATTACH_FILE_INFO WHERE TARGET_OBJID = T.OBJID AND DOC_TYPE='contractMgmt01' AND UPPER(STATUS) = 'ACTIVE') AS CU01_CNT
,(SELECT COUNT(1) FROM ATTACH_FILE_INFO WHERE TARGET_OBJID = T.OBJID AND DOC_TYPE='contractMgmt02' AND UPPER(STATUS) = 'ACTIVE') AS CU02_CNT -->
,(CASE WHEN (RESULT_CD is null or RESULT_CD ='') and (SPEC_RESULT_CD is null or RESULT_CD ='') and (EST_RESULT_CD is null or RESULT_CD ='') then '0'
ELSE 1
END
@@ -477,6 +478,7 @@
,CODE_NAME(CONTRACT_COMPANY) AS CONTRACT_COMPANY_NAME
,CONTRACT_DATE
,PO_NO
,ORDER_DATE
,MANUFACTURE_PLANT
,CODE_NAME(MANUFACTURE_PLANT) AS MANUFACTURE_PLANT_NAME
,CONTRACT_RESULT
@@ -753,12 +755,15 @@
AND PAID_TYPE = #{paid_type}
</if>
<if test="search_partNo != null and search_partNo != ''">
AND UPPER(PART_NO) LIKE UPPER('%${search_partNo}%')
</if>
<if test="search_partName != null and search_partName != ''">
AND UPPER(PART_NAME) LIKE UPPER('%${search_partName}%')
<!-- 품번/품명 검색: PART_OBJID로 정확하게 검색 -->
<if test="search_partObjId != null and search_partObjId != ''">
AND EXISTS (
SELECT 1
FROM CONTRACT_ITEM CI
WHERE CI.CONTRACT_OBJID = T.OBJID
AND CI.STATUS = 'ACTIVE'
AND CI.PART_OBJID = #{search_partObjId}
)
</if>
<if test="search_serialNo != null and search_serialNo != ''">
@@ -779,9 +784,9 @@
AND TO_DATE(DUE_DATE,'YYYY-MM-DD') <![CDATA[ <= ]]> TO_DATE(#{due_end_date}, 'YYYY-MM-DD')
</if>
ORDER BY REGDATE DESC
</select>
<select id="contractList_bak" parameterType="map" resultType="map">
</select>
<select id="contractList_bak" parameterType="map" resultType="map">
SELECT *
FROM (
SELECT CONTRACT_MGMT.*,ROW_NUMBER() OVER (ORDER BY CONTRACT_NO DESC) AS RNUM
@@ -3095,7 +3100,8 @@ SELECT
,#{writer}
FROM PMS_WBS_TASK_STANDARD AS T LEFT JOIN PMS_WBS_TEMPLATE AS T1
ON T.PARENT_OBJID = T1.OBJID
WHERE T1.TITLE=#{mechanical_type}
WHERE T1.OBJID = '1662715267'
<!-- WHERE T1.TITLE=#{mechanical_type} -->
<!-- WHERE T1.OBJID=#{mechanical_type} -->
<!-- WHERE T1.PRODUCT_OBJID=#{product} -->
)
@@ -3465,6 +3471,20 @@ ORDER BY ASM.SUPPLY_NAME
limit 1
</select>
<!-- 품목별 프로젝트 조회 (PART_OBJID 기준) -->
<select id="getProjectListByContractAndPartObjid" parameterType="map" resultType="map">
SELECT
PROJECT_NAME,
PART_OBJID,
PART_NO,
PART_NAME
FROM
PROJECT_MGMT
WHERE CONTRACT_OBJID = #{contractObjId}
AND PART_OBJID = #{part_objid}
LIMIT 1
</select>
<select id="overlapOrder" parameterType="map" resultType="map">
SELECT
T.*
@@ -3809,25 +3829,40 @@ ORDER BY ASM.SUPPLY_NAME
<!-- 견적서 템플릿 목록 조회 (CONTRACT_OBJID 기준) -->
<select id="getEstimateTemplateList" parameterType="map" resultType="map">
SELECT
OBJID AS "OBJID",
CONTRACT_OBJID AS "CONTRACT_OBJID",
TEMPLATE_TYPE AS "TEMPLATE_TYPE",
ET.OBJID AS "OBJID",
ET.CONTRACT_OBJID AS "CONTRACT_OBJID",
ET.TEMPLATE_TYPE AS "TEMPLATE_TYPE",
CASE
WHEN TEMPLATE_TYPE = '1' THEN '일반 견적서'
WHEN TEMPLATE_TYPE = '2' THEN '장비 견적서'
ELSE TEMPLATE_TYPE
WHEN ET.TEMPLATE_TYPE = '1' THEN '일반 견적서'
WHEN ET.TEMPLATE_TYPE = '2' THEN '장비 견적서'
ELSE ET.TEMPLATE_TYPE
END AS "TEMPLATE_TYPE_NAME",
ESTIMATE_NO AS "ESTIMATE_NO",
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",
ROW_NUMBER() OVER (PARTITION BY TEMPLATE_TYPE ORDER BY REGDATE) AS "REVISION"
ET.ESTIMATE_NO AS "ESTIMATE_NO",
ET.WRITER AS "WRITER",
TO_CHAR(ET.REGDATE, 'YYYY-MM-DD HH24:MI') AS "REGDATE",
ET.CHG_USER_ID AS "CHG_USER_ID",
TO_CHAR(ET.CHGDATE, 'YYYY-MM-DD HH24:MI') AS "CHGDATE",
ROW_NUMBER() OVER (PARTITION BY ET.TEMPLATE_TYPE ORDER BY ET.REGDATE) AS "REVISION",
COALESCE(
(SELECT CASE
WHEN A.STATUS = 'complete' THEN '결재완료'
WHEN A.STATUS = 'cancel' THEN '취소'
WHEN A.STATUS = 'reject' THEN '반려'
WHEN A.STATUS = 'inProcess' THEN '결재중'
ELSE '작성중'
END
FROM APPROVAL A
WHERE A.TARGET_OBJID::VARCHAR = ET.OBJID
AND A.TARGET_TYPE = 'CONTRACT_ESTIMATE'
ORDER BY A.REGDATE DESC
LIMIT 1),
'작성중'
) AS "APPR_STATUS"
FROM
ESTIMATE_TEMPLATE
ESTIMATE_TEMPLATE ET
WHERE
CONTRACT_OBJID = #{objId}
ORDER BY TEMPLATE_TYPE, REGDATE DESC
ET.CONTRACT_OBJID = #{objId}
ORDER BY ET.TEMPLATE_TYPE, ET.REGDATE DESC
</select>
<!-- 견적서 템플릿 데이터 조회 (ESTIMATE_TEMPLATE 테이블) -->
@@ -3848,24 +3883,42 @@ ORDER BY ASM.SUPPLY_NAME
ET.NOTE2,
ET.NOTE3,
ET.NOTE4,
ET.NOTE_REMARKS,
ET.TOTAL_AMOUNT,
ET.TOTAL_AMOUNT_KRW,
ET.MANAGER_NAME,
ET.MANAGER_CONTACT,
ET.WRITER,
ET.REGDATE,
ET.CHG_USER_ID,
ET.CHGDATE,
CM.EXCHANGE_RATE,
CODE_NAME(CM.CONTRACT_CURRENCY) AS CONTRACT_CURRENCY_NAME
CODE_NAME(CM.CONTRACT_CURRENCY) AS CONTRACT_CURRENCY_NAME,
COALESCE(
(SELECT CASE
WHEN A.STATUS = 'complete' THEN '결재완료'
WHEN A.STATUS = 'cancel' THEN '취소'
WHEN A.STATUS = 'reject' THEN '반려'
WHEN A.STATUS = 'inProcess' THEN '결재중'
ELSE '작성중'
END
FROM APPROVAL A
WHERE A.TARGET_OBJID::VARCHAR = ET.OBJID
AND A.TARGET_TYPE = 'CONTRACT_ESTIMATE'
ORDER BY A.REGDATE DESC
LIMIT 1),
'작성중'
) AS APPR_STATUS
FROM
ESTIMATE_TEMPLATE ET
LEFT JOIN CONTRACT_MGMT CM ON ET.CONTRACT_OBJID = CM.OBJID
WHERE
ET.CONTRACT_OBJID = #{objId}
<if test="template_type != null and template_type != ''">
AND ET.TEMPLATE_TYPE = #{template_type}
</if>
ORDER BY ET.REGDATE DESC
LIMIT 1
<if test="template_type != null and template_type != ''">
AND ET.TEMPLATE_TYPE = #{template_type}
</if>
ORDER BY ET.REGDATE DESC
LIMIT 1
</select>
<!-- 견적서 템플릿 데이터 조회 (OBJID 기준) -->
@@ -3886,14 +3939,32 @@ ORDER BY ASM.SUPPLY_NAME
ET.NOTE2,
ET.NOTE3,
ET.NOTE4,
ET.NOTE_REMARKS,
ET.TOTAL_AMOUNT,
ET.TOTAL_AMOUNT_KRW,
ET.MANAGER_NAME,
ET.MANAGER_CONTACT,
ET.WRITER,
TO_CHAR(ET.REGDATE, 'YYYY-MM-DD HH24:MI') AS REGDATE,
ET.CHG_USER_ID,
TO_CHAR(ET.CHGDATE, 'YYYY-MM-DD HH24:MI') AS CHGDATE,
CM.EXCHANGE_RATE,
CODE_NAME(CM.CONTRACT_CURRENCY) AS CONTRACT_CURRENCY_NAME
CODE_NAME(CM.CONTRACT_CURRENCY) AS CONTRACT_CURRENCY_NAME,
COALESCE(
(SELECT CASE
WHEN A.STATUS = 'complete' THEN '결재완료'
WHEN A.STATUS = 'cancel' THEN '취소'
WHEN A.STATUS = 'reject' THEN '반려'
WHEN A.STATUS = 'inProcess' THEN '결재중'
ELSE '작성중'
END
FROM APPROVAL A
WHERE A.TARGET_OBJID::VARCHAR = ET.OBJID
AND A.TARGET_TYPE = 'CONTRACT_ESTIMATE'
ORDER BY A.REGDATE DESC
LIMIT 1),
'작성중'
) AS APPR_STATUS
FROM
ESTIMATE_TEMPLATE ET
LEFT JOIN CONTRACT_MGMT CM ON ET.CONTRACT_OBJID = CM.OBJID
@@ -3963,39 +4034,45 @@ ORDER BY ASM.SUPPLY_NAME
MODEL_NAME,
MODEL_CODE,
EXECUTOR_DATE,
NOTE1,
NOTE2,
NOTE3,
NOTE4,
TOTAL_AMOUNT,
TOTAL_AMOUNT_KRW,
WRITER,
REGDATE,
CHG_USER_ID,
CHGDATE
) VALUES (
#{template_objid},
#{objId},
#{template_type},
#{executor},
#{recipient},
#{estimate_no},
#{contact_person},
#{greeting_text},
#{model_name},
#{model_code},
#{executor_date},
#{note1},
#{note2},
#{note3},
#{note4},
#{total_amount},
#{total_amount_krw},
#{writer},
NOW(),
#{chg_user_id},
NOW()
)
NOTE1,
NOTE2,
NOTE3,
NOTE4,
NOTE_REMARKS,
TOTAL_AMOUNT,
TOTAL_AMOUNT_KRW,
MANAGER_NAME,
MANAGER_CONTACT,
WRITER,
REGDATE,
CHG_USER_ID,
CHGDATE
) VALUES (
#{template_objid},
#{objId},
#{template_type},
#{executor},
#{recipient},
#{estimate_no},
#{contact_person},
#{greeting_text},
#{model_name},
#{model_code},
#{executor_date},
#{note1},
#{note2},
#{note3},
#{note4},
#{note_remarks},
#{total_amount},
#{total_amount_krw},
#{manager_name},
#{manager_contact},
#{writer},
NOW(),
#{chg_user_id},
NOW()
)
</insert>
<!-- 견적서 템플릿 수정 -->
@@ -4010,16 +4087,19 @@ ORDER BY ASM.SUPPLY_NAME
MODEL_NAME = #{model_name},
MODEL_CODE = #{model_code},
EXECUTOR_DATE = #{executor_date},
NOTE1 = #{note1},
NOTE2 = #{note2},
NOTE3 = #{note3},
NOTE4 = #{note4},
TOTAL_AMOUNT = #{total_amount},
TOTAL_AMOUNT_KRW = #{total_amount_krw},
CHG_USER_ID = #{chg_user_id},
CHGDATE = NOW()
WHERE
OBJID = #{template_objid}
NOTE1 = #{note1},
NOTE2 = #{note2},
NOTE3 = #{note3},
NOTE4 = #{note4},
NOTE_REMARKS = #{note_remarks},
TOTAL_AMOUNT = #{total_amount},
TOTAL_AMOUNT_KRW = #{total_amount_krw},
MANAGER_NAME = #{manager_name},
MANAGER_CONTACT = #{manager_contact},
CHG_USER_ID = #{chg_user_id},
CHGDATE = NOW()
WHERE
OBJID = #{template_objid}
</update>
<!-- 견적서 템플릿 품목 삭제 -->
@@ -4095,8 +4175,11 @@ ORDER BY ASM.SUPPLY_NAME
ET.NOTE2 AS "NOTE2",
ET.NOTE3 AS "NOTE3",
ET.NOTE4 AS "NOTE4",
ET.NOTE_REMARKS AS "NOTE_REMARKS",
ET.TOTAL_AMOUNT AS "TOTAL_AMOUNT",
ET.TOTAL_AMOUNT_KRW AS "TOTAL_AMOUNT_KRW",
ET.MANAGER_NAME AS "MANAGER_NAME",
ET.MANAGER_CONTACT AS "MANAGER_CONTACT",
ET.WRITER AS "WRITER",
TO_CHAR(ET.REGDATE, 'YYYY-MM-DD HH24:MI') AS "REGDATE",
ET.CHG_USER_ID AS "CHG_USER_ID",
@@ -4104,11 +4187,11 @@ ORDER BY ASM.SUPPLY_NAME
ET.CATEGORIES_JSON AS "CATEGORIES_JSON",
CM.EXCHANGE_RATE AS "EXCHANGE_RATE",
CODE_NAME(CM.CONTRACT_CURRENCY) AS "CONTRACT_CURRENCY_NAME"
FROM
ESTIMATE_TEMPLATE ET
LEFT JOIN CONTRACT_MGMT CM ON ET.CONTRACT_OBJID = CM.OBJID
WHERE
ET.CONTRACT_OBJID = #{objId}
FROM
ESTIMATE_TEMPLATE ET
LEFT JOIN CONTRACT_MGMT CM ON ET.CONTRACT_OBJID = CM.OBJID
WHERE
ET.CONTRACT_OBJID = #{objId}
ORDER BY
ET.TEMPLATE_TYPE,
ET.REGDATE DESC

View File

@@ -1621,18 +1621,24 @@ private String makeEstimateMailContents(Map contractInfo, Map estimateTemplate,
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("body { font-family: 'Malgun Gothic', '맑은 고딕', Arial, sans-serif; margin: 0; padding: 20px; background-color: #f5f5f5; }");
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(".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(".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%; }");
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; }");
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>");
@@ -1656,14 +1662,77 @@ private String makeEstimateMailContents(Map contractInfo, Map estimateTemplate,
String executorDate = CommonUtils.checkNull(estimateTemplate.get("executor_date"));
if("".equals(executorDate)) executorDate = CommonUtils.checkNull(estimateTemplate.get("EXECUTOR_DATE"));
// 기본 정보 테이블
// 담당자 정보
String managerName = CommonUtils.checkNull(estimateTemplate.get("manager_name"));
if("".equals(managerName)) managerName = CommonUtils.checkNull(estimateTemplate.get("MANAGER_NAME"));
if("".equals(managerName)) managerName = "영업부";
String managerContact = CommonUtils.checkNull(estimateTemplate.get("manager_contact"));
if("".equals(managerContact)) managerContact = CommonUtils.checkNull(estimateTemplate.get("MANAGER_CONTACT"));
// 헤더 섹션 (왼쪽: 기본정보, 오른쪽: 회사정보)
contents.append("<div class='header-section'>");
// 왼쪽: 기본 정보 테이블
contents.append("<div class='header-left'>");
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>");
contents.append("</div>");
// 오른쪽: 회사 정보
contents.append("<div class='header-right'>");
contents.append("<div class='company-info'>");
// 회사 도장 이미지 경로
String projectRoot = System.getProperty("user.dir");
String companyStampPath = projectRoot + "/WebContent/images/company_stamp.png";
// Docker 환경인 경우 다른 경로 시도
File stampFile = new File(companyStampPath);
if(!stampFile.exists()) {
companyStampPath = "/usr/local/tomcat/webapps/ROOT/images/company_stamp.png";
stampFile = new File(companyStampPath);
if(!stampFile.exists()) {
companyStampPath = "";
}
}
String stampBase64 = !"".equals(companyStampPath) ? encodeImageToBase64(companyStampPath) : "";
if(!"".equals(stampBase64)) {
// 이미지가 있으면 Base64로 인코딩된 이미지만 표시
contents.append("<img src='" + stampBase64 + "' alt='회사 도장' class='company-stamp-img'>");
} else {
// 이미지가 없으면 텍스트로 표시
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='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 닫기
// 견적 품목
if(estimateItems != null && !estimateItems.isEmpty()){
@@ -1704,17 +1773,51 @@ private String makeEstimateMailContents(Map contractInfo, Map estimateTemplate,
contents.append("</tr>");
}
// 합계
// 계 (총 합계)
String totalAmountStr = CommonUtils.checkNull(estimateTemplate.get("total_amount"));
if("".equals(totalAmountStr)) totalAmountStr = CommonUtils.checkNull(estimateTemplate.get("TOTAL_AMOUNT"));
// DB에 저장된 합계가 없으면 계산한 값 사용
if("".equals(totalAmountStr)){
totalAmountStr = String.format("%,d", totalAmount);
}
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 colspan='6' class='text-center'>계</td>");
contents.append("<td class='text-right'>" + totalAmountStr + "</td>");
contents.append("<td></td>");
contents.append("</tr>");
// 원화환산 공급가액
String totalAmountKrw = CommonUtils.checkNull(estimateTemplate.get("total_amount_krw"));
if("".equals(totalAmountKrw)) totalAmountKrw = CommonUtils.checkNull(estimateTemplate.get("TOTAL_AMOUNT_KRW"));
if(!"".equals(totalAmountKrw)){
contents.append("<tr style='background-color:#e8f4f8; font-weight:bold;'>");
contents.append("<td colspan='6' class='text-center'>원화환산 공급가액 (KRW)</td>");
contents.append("<td class='text-right'>" + totalAmountKrw + "</td>");
contents.append("<td></td>");
contents.append("</tr>");
}
// 테이블 내 비고
String noteRemarks = CommonUtils.checkNull(estimateTemplate.get("note_remarks"));
if("".equals(noteRemarks)) noteRemarks = CommonUtils.checkNull(estimateTemplate.get("NOTE_REMARKS"));
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"));
@@ -1729,11 +1832,23 @@ private String makeEstimateMailContents(Map contractInfo, Map estimateTemplate,
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("<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>");
}
@@ -1744,6 +1859,42 @@ private String makeEstimateMailContents(Map contractInfo, Map estimateTemplate,
return contents.toString();
}
/**
* 이미지 파일을 Base64로 인코딩
* @param imagePath 이미지 파일 경로
* @return Base64 인코딩된 문자열 (data:image/png;base64,...)
*/
private String encodeImageToBase64(String imagePath) {
try {
File imageFile = new File(imagePath);
if(!imageFile.exists()) {
return "";
}
java.io.FileInputStream fis = new java.io.FileInputStream(imageFile);
byte[] imageBytes = new byte[(int) imageFile.length()];
fis.read(imageBytes);
fis.close();
// Apache Commons Codec 사용 (Java 7 호환)
String base64 = org.apache.commons.codec.binary.Base64.encodeBase64String(imageBytes);
// 파일 확장자로 MIME 타입 결정
String mimeType = "image/png";
if(imagePath.toLowerCase().endsWith(".jpg") || imagePath.toLowerCase().endsWith(".jpeg")) {
mimeType = "image/jpeg";
} else if(imagePath.toLowerCase().endsWith(".gif")) {
mimeType = "image/gif";
}
return "data:" + mimeType + ";base64," + base64;
} catch(Exception e) {
System.out.println("이미지 Base64 인코딩 실패: " + e.getMessage());
e.printStackTrace();
return "";
}
}
/**
* 영업정보 조회 (수주등록용)
* @param contractObjId
@@ -1907,51 +2058,96 @@ private String makeEstimateMailContents(Map contractInfo, Map estimateTemplate,
//paramMap.put("contract_price_currency", contract_price_currency/project_cnt + "");
//paramMap.put("contract_price", contract_price/project_cnt + "");
if("0000964".equals(result_cd) || "0000968".equals(result_cd)){
resultList = sqlSession.selectOne("contractMgmt.getProjectListBycontractObjid", paramMap);
System.out.println("resultList:::"+resultList);
//resultList = sqlSession.selectOne("contractMgmt.getProjectCnt", paramMap);
if(null==resultList){
// for (int i=0; i<project_cnt; i++){
if("0000964".equals(result_cd) || "0000968".equals(result_cd)){
// 품목별로 프로젝트 생성
List<Map> contractItems = getContractItems(paramMap);
if(contractItems != null && !contractItems.isEmpty()) {
System.out.println("품목 개수: " + contractItems.size() + "개 - 품목별 프로젝트 생성 시작");
for(Map item : contractItems) {
// 품목별 프로젝트 존재 여부 확인
Map<String, Object> projectCheckParam = new HashMap<String, Object>();
projectCheckParam.put("contractObjId", contract_objid);
projectCheckParam.put("part_objid", item.get("PART_OBJID"));
resultList = sqlSession.selectOne("contractMgmt.getProjectListByContractAndPartObjid", projectCheckParam);
if(null == resultList) {
// 새 프로젝트 생성
Map<String, Object> projectParam = new HashMap<String, Object>();
projectParam.putAll(paramMap); // 기본 정보 복사
// 품목별 정보 설정
projectParam.put("OBJID", CommonUtils.createObjId());
projectParam.put("is_temp", '1');
projectParam.put("part_objid", item.get("PART_OBJID"));
projectParam.put("part_no", item.get("PART_NO"));
projectParam.put("part_name", item.get("PART_NAME"));
projectParam.put("quantity", item.get("ORDER_QUANTITY") != null ? item.get("ORDER_QUANTITY") : item.get("QUANTITY"));
projectParam.put("due_date", item.get("DUE_DATE"));
paramMap.put("OBJID", CommonUtils.createObjId());
paramMap.put("is_temp", '1');
//paramMap.put("facility_qty", '1');
if("0000170".equals(category_cd) || "0000171".equals(category_cd)){
paramMap.put("overhaul_project_no", target_project_no);
//paramMap.put("overhaul_order", overhaul_order+i);
}else{
projectParam.put("overhaul_project_no", target_project_no);
}
//프로젝트 등록
cnt = sqlSession.update("project.createProject", paramMap);
//프로젝트 TASK 등록
cnt = sqlSession.insert("contractMgmt.insertProjectTask", paramMap);
//프로젝트 SETUP_TASK 등록
cnt = sqlSession.insert("contractMgmt.insertProjectSetupTask", paramMap);
//project_no - unit 폴더 생성
//paramMap.put("OBJID", paramMap.get("OBJID"));
Map<String,Object> projectInfo = (Map)sqlSession.selectOne("project.getProjectMngInfo", paramMap);
paramMap.put("contract_objid", paramMap.get("contractObjId"));
paramMap.put("customer_product", paramMap.get("mechanical_type"));
List<Map<String,Object>> taskUnitList = (ArrayList)sqlSession.selectList("project.getWbsTaskListByProject", paramMap);
if(CommonUtils.isNotEmpty(taskUnitList) && !taskUnitList.isEmpty()){
String projectNo = (String)projectInfo.get("project_no");
String filepath = Constants.FILE_STORAGE+"\\PART_DATA\\";
for (Map<String, Object> map : taskUnitList) {
File file = new File(filepath+File.separator+projectNo+File.separator+CommonUtils.checkNull((String)map.get("unit_no"))+"-"+CommonUtils.checkNull((String)map.get("task_name")));
if(!file.exists()){
file.mkdirs();
System.out.println("프로젝트 생성 - PART_OBJID: " + item.get("PART_OBJID") + ", 품번: " + item.get("PART_NO") + ", 품명: " + item.get("PART_NAME"));
// 프로젝트 등록
cnt = sqlSession.update("project.createProject", projectParam);
// 프로젝트 TASK 등록
cnt = sqlSession.insert("contractMgmt.insertProjectTask", projectParam);
// 프로젝트 SETUP_TASK 등록
cnt = sqlSession.insert("contractMgmt.insertProjectSetupTask", projectParam);
// project_no - unit 폴더 생성
Map<String,Object> projectInfo = (Map)sqlSession.selectOne("project.getProjectMngInfo", projectParam);
projectParam.put("contract_objid", contract_objid);
projectParam.put("customer_product", projectParam.get("mechanical_type"));
List<Map<String,Object>> taskUnitList = (ArrayList)sqlSession.selectList("project.getWbsTaskListByProject", projectParam);
if(CommonUtils.isNotEmpty(taskUnitList) && !taskUnitList.isEmpty()){
String projectNo = (String)projectInfo.get("project_no");
String filepath = Constants.FILE_STORAGE+"\\PART_DATA\\";
for (Map<String, Object> map : taskUnitList) {
File file = new File(filepath+File.separator+projectNo+File.separator+CommonUtils.checkNull((String)map.get("unit_no"))+"-"+CommonUtils.checkNull((String)map.get("task_name")));
if(!file.exists()){
file.mkdirs();
}
}
}
}
// }
} else {
// 기존 프로젝트 업데이트
Map<String, Object> updateParam = new HashMap<String, Object>();
updateParam.putAll(paramMap);
updateParam.put("part_objid", item.get("PART_OBJID"));
updateParam.put("part_no", item.get("PART_NO"));
updateParam.put("part_name", item.get("PART_NAME"));
updateParam.put("quantity", item.get("ORDER_QUANTITY") != null ? item.get("ORDER_QUANTITY") : item.get("QUANTITY"));
updateParam.put("due_date", item.get("DUE_DATE"));
System.out.println("프로젝트 업데이트 - PART_OBJID: " + item.get("PART_OBJID") + ", 품번: " + item.get("PART_NO"));
sqlSession.update("project.ModifyProjectByContract", updateParam);
}
}
} else {
System.out.println("품목이 없습니다 - 기존 방식으로 프로젝트 생성");
// 품목이 없는 경우 기존 방식대로 처리
resultList = sqlSession.selectOne("contractMgmt.getProjectListBycontractObjid", paramMap);
if(null==resultList){
paramMap.put("OBJID", CommonUtils.createObjId());
paramMap.put("is_temp", '1');
if("0000170".equals(category_cd) || "0000171".equals(category_cd)){
paramMap.put("overhaul_project_no", target_project_no);
}
cnt = sqlSession.update("project.createProject", paramMap);
cnt = sqlSession.insert("contractMgmt.insertProjectTask", paramMap);
cnt = sqlSession.insert("contractMgmt.insertProjectSetupTask", paramMap);
}else{
sqlSession.update("project.ModifyProjectByContract", paramMap);
}
}
}
// if(cnt > 0){
//계약완료 일시 메일
// if("0000964".equals(CommonUtils.checkNull(paramMap.get("contract_result")))){