Compare commits
20 Commits
V202511110
...
feature/es
| Author | SHA1 | Date | |
|---|---|---|---|
| 86bd16ff77 | |||
| dcef82cdca | |||
| 6a97bf72e1 | |||
| b761e511ff | |||
| bf0e08d90b | |||
| 333d05d19f | |||
| 031ce7615d | |||
| cf79338b55 | |||
| ba026842f7 | |||
| 8cccd9db2c | |||
| da883f0007 | |||
|
|
2bcc39ff65 | ||
|
|
0b291c4ea2 | ||
| 0817253285 | |||
| 7ae57f9719 | |||
| a9836b599b | |||
| 9aafeb3def | |||
| c89266ec0f | |||
|
|
1c0e673068 | ||
|
|
ebc27216f4 |
@@ -19,4 +19,11 @@ DB_PASSWORD=waceplm0909!!
|
||||
DB_DRIVER_CLASS_NAME=org.postgresql.Driver
|
||||
DB_MAX_TOTAL=200
|
||||
DB_MAX_IDLE=50
|
||||
DB_MAX_WAIT_MILLIS=-1
|
||||
DB_MAX_WAIT_MILLIS=-1
|
||||
|
||||
# --- NAS Backup SMB Configuration ---
|
||||
FTP_HOST=effectsno1.synology.me
|
||||
FTP_USER=esgrin-mes-backup
|
||||
FTP_PASSWORD=UyD12#11YHnn
|
||||
FTP_PATH=esgrin-mes-backup
|
||||
FTP_PORT=2112
|
||||
54
Dockerfile.backup
Normal file
54
Dockerfile.backup
Normal file
@@ -0,0 +1,54 @@
|
||||
# Use an official Python runtime as a parent image
|
||||
FROM python:3.10-slim
|
||||
|
||||
# Set environment variables
|
||||
ENV PYTHONDONTWRITEBYTECODE=1
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
ENV TZ=Asia/Seoul
|
||||
|
||||
# Install system dependencies including PostgreSQL client 16
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends \
|
||||
gnupg \
|
||||
lsb-release \
|
||||
wget \
|
||||
ca-certificates \
|
||||
tzdata \
|
||||
gpg \
|
||||
lftp && \
|
||||
# Add PostgreSQL Apt Repository
|
||||
# Download the key
|
||||
wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | \
|
||||
# Dearmor the key and save to the keyring directory
|
||||
gpg --dearmor -o /usr/share/keyrings/postgresql-archive-keyring.gpg && \
|
||||
# Set correct permissions for the keyring file
|
||||
chmod 644 /usr/share/keyrings/postgresql-archive-keyring.gpg && \
|
||||
# Add the repository source, signed by the keyring
|
||||
sh -c 'echo "deb [signed-by=/usr/share/keyrings/postgresql-archive-keyring.gpg] https://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list' && \
|
||||
# Update again after adding repo
|
||||
apt-get update && \
|
||||
# Install specific client version
|
||||
apt-get install -y --no-install-recommends \
|
||||
postgresql-client-16 && \
|
||||
# Configure timezone
|
||||
ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone && \
|
||||
# Clean up (remove build dependencies)
|
||||
apt-get purge -y --auto-remove wget gnupg lsb-release ca-certificates gpg && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install Python dependencies
|
||||
# Using requirements.txt is generally better, but for a simple script:
|
||||
RUN pip install --no-cache-dir schedule pytz
|
||||
|
||||
# Set the working directory in the container
|
||||
WORKDIR /app
|
||||
|
||||
# Copy the Python script into the container
|
||||
COPY db/backup.py .
|
||||
|
||||
# Ensure .ssh directory exists (still good practice, though key might not be used)
|
||||
# RUN mkdir -p /root/.ssh && chmod 700 /root/.ssh # No longer needed for SSH keys
|
||||
|
||||
# Command to run the application
|
||||
CMD ["python", "backup.py"]
|
||||
@@ -2356,6 +2356,16 @@ SELECT
|
||||
,BUS_REG_NO
|
||||
,OFFICE_NO
|
||||
,EMAIL
|
||||
,MANAGER1_NAME
|
||||
,MANAGER1_EMAIL
|
||||
,MANAGER2_NAME
|
||||
,MANAGER2_EMAIL
|
||||
,MANAGER3_NAME
|
||||
,MANAGER3_EMAIL
|
||||
,MANAGER4_NAME
|
||||
,MANAGER4_EMAIL
|
||||
,MANAGER5_NAME
|
||||
,MANAGER5_EMAIL
|
||||
FROM SUPPLY_MNG
|
||||
WHERE OBJID = #{objid}::numeric
|
||||
</select>
|
||||
|
||||
@@ -63,11 +63,26 @@ $(document).ready(function(){
|
||||
|
||||
//영업활동 등록 팝업
|
||||
$(".btnRegist").click(function(){
|
||||
var selectedData = _tabulGrid.getSelectedData();
|
||||
var popup_width = 1400;
|
||||
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");
|
||||
var popup_height = 560;
|
||||
var url = "";
|
||||
|
||||
// 선택된 행이 없으면 신규 등록
|
||||
if(selectedData.length === 0){
|
||||
url = "/contractMgmt/estimateRegistFormPopup.do?actionType=regist";
|
||||
}
|
||||
// 한 줄 선택된 경우 상세 팝업 (영업번호 클릭과 동일)
|
||||
else if(selectedData.length === 1){
|
||||
var objid = fnc_checkNull(selectedData[0].OBJID);
|
||||
url = "/contractMgmt/estimateRegistFormPopup.do?objId=" + objid;
|
||||
}
|
||||
// 여러 줄 선택된 경우 경고
|
||||
else {
|
||||
Swal.fire("한 개의 행만 선택해주세요.");
|
||||
return false;
|
||||
}
|
||||
|
||||
fn_centerPopup(popup_width, popup_height, url);
|
||||
});
|
||||
|
||||
@@ -190,19 +205,8 @@ $(document).ready(function(){
|
||||
return false;
|
||||
}
|
||||
|
||||
// 메일 발송 확인
|
||||
Swal.fire({
|
||||
title: '견적서 메일 발송',
|
||||
text: "최종 차수의 견적서를 발송하시겠습니까?",
|
||||
icon: 'question',
|
||||
showCancelButton: true,
|
||||
confirmButtonText: '발송',
|
||||
cancelButtonText: '취소'
|
||||
}).then((result) => {
|
||||
if(result.isConfirmed){
|
||||
fn_sendEstimateMail(objId);
|
||||
}
|
||||
});
|
||||
// 메일 작성 팝업 열기
|
||||
fn_openMailFormPopup(objId);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -513,11 +517,11 @@ function fn_FileRegist(objId, docType, docTypeName){
|
||||
|
||||
fn_centerPopup(popup_width, popup_height, url);
|
||||
}
|
||||
//영업활동등록 상세
|
||||
//영업활동등록 상세 (뷰 전용 팝업)
|
||||
function fn_projectConceptDetail(objId){
|
||||
var popup_width = 1400;
|
||||
var popup_height = 560;
|
||||
var url = "/contractMgmt/estimateRegistFormPopup.do?objId="+objId;
|
||||
var popup_width = 900;
|
||||
var popup_height = 700;
|
||||
var url = "/contractMgmt/estimateViewPopup.do?objId="+objId;
|
||||
|
||||
fn_centerPopup(popup_width, popup_height, url);
|
||||
}
|
||||
@@ -683,309 +687,42 @@ function fn_showSerialNoPopup(serialNoString){
|
||||
});
|
||||
}
|
||||
|
||||
// 견적서 메일 발송
|
||||
function fn_sendEstimateMail(contractObjId){
|
||||
// 메일 작성 팝업 열기
|
||||
function fn_openMailFormPopup(contractObjId){
|
||||
if(!contractObjId || contractObjId === ''){
|
||||
Swal.fire("잘못된 요청입니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
// 1단계: 견적서 템플릿 정보 조회
|
||||
Swal.fire({
|
||||
title: '견적서 조회 중...',
|
||||
text: '잠시만 기다려주세요.',
|
||||
allowOutsideClick: false,
|
||||
onOpen: () => {
|
||||
Swal.showLoading();
|
||||
}
|
||||
});
|
||||
var popup_width = 950;
|
||||
var popup_height = 800;
|
||||
var url = "/contractMgmt/estimateMailFormPopup.do?contractObjId=" + contractObjId;
|
||||
|
||||
$.ajax({
|
||||
url: "/contractMgmt/getEstimateTemplateList.do",
|
||||
type: "POST",
|
||||
data: { objId: contractObjId },
|
||||
dataType: "json",
|
||||
success: function(data){
|
||||
if(data.result === "success" && data.list && data.list.length > 0){
|
||||
// 최종 차수 견적서 찾기
|
||||
var latestEstimate = data.list[0]; // 이미 차수 내림차순으로 정렬되어 있음
|
||||
var templateObjId = latestEstimate.OBJID || latestEstimate.objid;
|
||||
var templateType = latestEstimate.TEMPLATE_TYPE || latestEstimate.template_type || latestEstimate.templateType;
|
||||
|
||||
// 2단계: 견적서 페이지를 새 창으로 열고 PDF 생성
|
||||
fn_generatePdfAndSendMail(contractObjId, templateObjId, templateType);
|
||||
} else {
|
||||
Swal.close();
|
||||
Swal.fire({
|
||||
title: '오류',
|
||||
text: '견적서를 찾을 수 없습니다.',
|
||||
icon: 'error'
|
||||
});
|
||||
}
|
||||
},
|
||||
error: function(xhr, status, error){
|
||||
Swal.close();
|
||||
console.error("견적서 조회 오류:", xhr, status, error);
|
||||
Swal.fire({
|
||||
title: '오류',
|
||||
text: '견적서 조회 중 오류가 발생했습니다.',
|
||||
icon: 'error'
|
||||
});
|
||||
}
|
||||
});
|
||||
window.open(url, "estimateMailForm", "width="+popup_width+",height="+popup_height+",menubar=no,scrollbars=yes,resizable=yes");
|
||||
}
|
||||
|
||||
// PDF 생성 및 메일 발송
|
||||
/*
|
||||
* 아래 함수들은 자동 메일 발송 기능을 위한 것입니다.
|
||||
* 현재는 메일 작성 팝업을 사용하므로 주석 처리합니다.
|
||||
* 필요시 참고용으로 남겨둡니다.
|
||||
*/
|
||||
|
||||
/*
|
||||
// PDF 생성 및 메일 발송 (자동)
|
||||
function fn_generatePdfAndSendMail(contractObjId, templateObjId, templateType){
|
||||
Swal.fire({
|
||||
title: 'PDF 생성 중...',
|
||||
text: '견적서를 PDF로 변환하고 있습니다.',
|
||||
allowOutsideClick: false,
|
||||
onOpen: () => {
|
||||
Swal.showLoading();
|
||||
}
|
||||
});
|
||||
|
||||
// 견적서 페이지 URL 생성
|
||||
var url = "";
|
||||
if(templateType === "1"){
|
||||
url = "/contractMgmt/estimateTemplate1.do?templateObjId=" + templateObjId;
|
||||
} else if(templateType === "2"){
|
||||
url = "/contractMgmt/estimateTemplate2.do?templateObjId=" + templateObjId;
|
||||
}
|
||||
|
||||
// 숨겨진 iframe으로 페이지 로드
|
||||
var iframe = $('<iframe>', {
|
||||
id: 'pdfGeneratorFrame',
|
||||
src: url,
|
||||
style: 'position:absolute;width:0;height:0;border:none;'
|
||||
}).appendTo('body');
|
||||
|
||||
// iframe 로드 완료 대기
|
||||
iframe.on('load', function(){
|
||||
try {
|
||||
var iframeWindow = this.contentWindow;
|
||||
|
||||
// 데이터 로딩 완료를 기다림 (최대 10초)
|
||||
var checkDataLoaded = function(attempts) {
|
||||
if(attempts > 100) { // 10초 (100ms * 100)
|
||||
$('#pdfGeneratorFrame').remove();
|
||||
Swal.close();
|
||||
Swal.fire({
|
||||
title: '타임아웃',
|
||||
text: '견적서 데이터 로딩 시간이 초과되었습니다.',
|
||||
icon: 'error'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if(iframeWindow.dataLoaded === true) {
|
||||
console.log('데이터 로딩 완료 확인, PDF 생성 시작');
|
||||
|
||||
// iframe 내의 PDF 생성 함수 호출
|
||||
if(typeof iframeWindow.fn_generateAndUploadPdf === 'function'){
|
||||
iframeWindow.fn_generateAndUploadPdf(function(pdfBase64){
|
||||
// iframe 제거
|
||||
$('#pdfGeneratorFrame').remove();
|
||||
|
||||
if(pdfBase64){
|
||||
// PDF Base64와 함께 메일 발송 요청
|
||||
fn_sendMailWithPdf(contractObjId, pdfBase64);
|
||||
} else {
|
||||
Swal.close();
|
||||
Swal.fire({
|
||||
title: '오류',
|
||||
text: 'PDF 생성에 실패했습니다.',
|
||||
icon: 'error'
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// iframe 제거
|
||||
$('#pdfGeneratorFrame').remove();
|
||||
|
||||
Swal.close();
|
||||
Swal.fire({
|
||||
title: '오류',
|
||||
text: '견적서 페이지 로드에 실패했습니다.',
|
||||
icon: 'error'
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// 아직 로딩 중이면 100ms 후 다시 확인
|
||||
setTimeout(function() {
|
||||
checkDataLoaded(attempts + 1);
|
||||
}, 100);
|
||||
}
|
||||
};
|
||||
|
||||
// 데이터 로딩 체크 시작
|
||||
checkDataLoaded(0);
|
||||
} catch(e) {
|
||||
console.error('PDF 생성 오류:', e);
|
||||
$('#pdfGeneratorFrame').remove();
|
||||
|
||||
Swal.close();
|
||||
Swal.fire({
|
||||
title: '오류',
|
||||
text: 'PDF 생성 중 오류가 발생했습니다.',
|
||||
icon: 'error'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 타임아웃 설정 (30초)
|
||||
setTimeout(function(){
|
||||
if($('#pdfGeneratorFrame').length > 0){
|
||||
$('#pdfGeneratorFrame').remove();
|
||||
Swal.close();
|
||||
Swal.fire({
|
||||
title: '타임아웃',
|
||||
text: 'PDF 생성 시간이 초과되었습니다.',
|
||||
icon: 'error'
|
||||
});
|
||||
}
|
||||
}, 30000);
|
||||
// ... (생략)
|
||||
}
|
||||
|
||||
// PDF Base64와 함께 메일 발송 (청크 방식)
|
||||
function fn_sendMailWithPdf(contractObjId, pdfBase64){
|
||||
console.log('===== 메일 발송 시작 =====');
|
||||
console.log('contractObjId:', contractObjId);
|
||||
console.log('PDF Base64 길이:', pdfBase64 ? pdfBase64.length : 0);
|
||||
console.log('========================');
|
||||
|
||||
Swal.fire({
|
||||
title: '메일 발송 중...',
|
||||
text: 'PDF 업로드 중...',
|
||||
allowOutsideClick: false,
|
||||
onOpen: () => {
|
||||
Swal.showLoading();
|
||||
}
|
||||
});
|
||||
|
||||
// 청크 크기: 100KB (Base64) - POST 크기 제한 고려
|
||||
var chunkSize = 100 * 1024;
|
||||
var totalChunks = Math.ceil(pdfBase64.length / chunkSize);
|
||||
var uploadedChunks = 0;
|
||||
var sessionId = 'pdf_' + contractObjId + '_' + new Date().getTime();
|
||||
|
||||
console.log('PDF Base64 전체 길이:', pdfBase64.length);
|
||||
console.log('청크 크기:', chunkSize);
|
||||
console.log('총 청크 수:', totalChunks);
|
||||
|
||||
// 청크 업로드 함수
|
||||
function uploadChunk(chunkIndex) {
|
||||
var start = chunkIndex * chunkSize;
|
||||
var end = Math.min(start + chunkSize, pdfBase64.length);
|
||||
var chunk = pdfBase64.substring(start, end);
|
||||
|
||||
console.log('청크 ' + (chunkIndex + 1) + '/' + totalChunks + ' 업로드 중...');
|
||||
|
||||
$.ajax({
|
||||
url: "/contractMgmt/uploadPdfChunk.do",
|
||||
type: "POST",
|
||||
data: {
|
||||
sessionId: sessionId,
|
||||
chunkIndex: chunkIndex,
|
||||
totalChunks: totalChunks,
|
||||
chunk: chunk
|
||||
},
|
||||
dataType: "json",
|
||||
timeout: 30000,
|
||||
success: function(data){
|
||||
if(data.result === "success"){
|
||||
uploadedChunks++;
|
||||
|
||||
// 진행률 업데이트
|
||||
var progress = Math.round((uploadedChunks / totalChunks) * 100);
|
||||
Swal.update({
|
||||
text: 'PDF 업로드 중... ' + progress + '%'
|
||||
});
|
||||
|
||||
// 다음 청크 업로드
|
||||
if(chunkIndex + 1 < totalChunks){
|
||||
uploadChunk(chunkIndex + 1);
|
||||
} else {
|
||||
// 모든 청크 업로드 완료, 메일 발송 요청
|
||||
console.log('모든 청크 업로드 완료, 메일 발송 시작');
|
||||
sendMailWithUploadedPdf(contractObjId, sessionId);
|
||||
}
|
||||
} else {
|
||||
Swal.close();
|
||||
Swal.fire({
|
||||
title: '업로드 실패',
|
||||
text: 'PDF 업로드 중 오류가 발생했습니다.',
|
||||
icon: 'error',
|
||||
confirmButtonText: '확인'
|
||||
});
|
||||
}
|
||||
},
|
||||
error: function(xhr, status, error){
|
||||
Swal.close();
|
||||
console.error("청크 업로드 오류:", xhr, status, error);
|
||||
Swal.fire({
|
||||
title: '오류',
|
||||
text: 'PDF 업로드 중 시스템 오류가 발생했습니다.',
|
||||
icon: 'error',
|
||||
confirmButtonText: '확인'
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 첫 번째 청크부터 시작
|
||||
uploadChunk(0);
|
||||
// ... (생략)
|
||||
}
|
||||
|
||||
// 업로드된 PDF로 메일 발송
|
||||
function sendMailWithUploadedPdf(contractObjId, sessionId){
|
||||
Swal.update({
|
||||
text: '메일 발송 중...'
|
||||
});
|
||||
|
||||
$.ajax({
|
||||
url: "/contractMgmt/sendEstimateMail.do",
|
||||
type: "POST",
|
||||
data: {
|
||||
objId: contractObjId,
|
||||
pdfSessionId: sessionId
|
||||
},
|
||||
dataType: "json",
|
||||
timeout: 60000,
|
||||
success: function(data){
|
||||
console.log('메일 발송 응답:', data);
|
||||
Swal.close();
|
||||
if(data.result === "success"){
|
||||
Swal.fire({
|
||||
title: '발송 완료',
|
||||
text: '견적서가 성공적으로 발송되었습니다.',
|
||||
icon: 'success',
|
||||
confirmButtonText: '확인'
|
||||
}).then(() => {
|
||||
fn_search();
|
||||
});
|
||||
} 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){
|
||||
@@ -1181,26 +918,24 @@ function openProjectFormPopUp(objId){
|
||||
</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>
|
||||
<select name="search_partName" id="search_partName" class="select2-part" style="width: 100%;">
|
||||
<option value="">품명 선택</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<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>
|
||||
<select name="search_partName" id="search_partName" class="select2-part" style="width: 100%;">
|
||||
<option value="">품명 선택</option>
|
||||
</select>
|
||||
</td>
|
||||
|
||||
<td class="">
|
||||
<label for="" class="">S/N</label>
|
||||
</td>
|
||||
@@ -1209,7 +944,7 @@ function openProjectFormPopUp(objId){
|
||||
</td>
|
||||
|
||||
<td><label for="">결재상태</label></td>
|
||||
<td>
|
||||
<td>
|
||||
<select name="appr_status" id="appr_status" class="select2" autocomplete="off" style="width:130px">
|
||||
<option value="">선택</option>
|
||||
<%-- ${code_map.appr_status} --%>
|
||||
@@ -1228,17 +963,18 @@ function openProjectFormPopUp(objId){
|
||||
<input type="text" name="receipt_start_date" id="receipt_start_date" style="width:90px;" autocomplete="off" value="${param.receipt_start_date}" class="date_icon">~
|
||||
<input type="text" name="receipt_end_date" id="receipt_end_date" style="width:90px;" autocomplete="off" value="${param.receipt_end_date}" class="date_icon">
|
||||
</td>
|
||||
|
||||
<%--
|
||||
</tr>
|
||||
|
||||
<%--<tr>
|
||||
<td class="">
|
||||
<label>요청납기</label>
|
||||
</td>
|
||||
<td>
|
||||
<input type="text" name="due_start_date" id="due_start_date" style="width:90px;" autocomplete="off" value="${param.due_start_date}" class="date_icon">~
|
||||
<input type="text" name="due_end_date" id="due_end_date" style="width:90px;" autocomplete="off" value="${param.due_end_date}" class="date_icon">
|
||||
</td>
|
||||
--%>
|
||||
</tr>
|
||||
</td>
|
||||
</tr>--%>
|
||||
|
||||
</table>
|
||||
</div>
|
||||
|
||||
|
||||
657
WebContent/WEB-INF/view/contractMgmt/estimateMailFormPopup.jsp
Normal file
657
WebContent/WEB-INF/view/contractMgmt/estimateMailFormPopup.jsp
Normal file
@@ -0,0 +1,657 @@
|
||||
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
|
||||
<%@ page import="com.pms.common.utils.*"%>
|
||||
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt"%>
|
||||
<%@ page import="java.util.*" %>
|
||||
<%@include file= "/init.jsp" %>
|
||||
<%
|
||||
PersonBean person = (PersonBean)session.getAttribute(Constants.PERSON_BEAN);
|
||||
String connector = person.getUserId();
|
||||
String contractObjId = request.getParameter("contractObjId");
|
||||
%>
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
<title>견적서 메일 발송</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 10px;
|
||||
background-color: #f5f5f5;
|
||||
font-size: 13px;
|
||||
}
|
||||
.mail-form-container {
|
||||
background: white;
|
||||
padding: 15px 20px;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 1px 5px rgba(0,0,0,0.1);
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.form-title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 15px;
|
||||
color: #333;
|
||||
border-bottom: 2px solid #3085d6;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
.form-group {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.form-group label {
|
||||
display: block;
|
||||
font-weight: bold;
|
||||
margin-bottom: 5px;
|
||||
color: #555;
|
||||
font-size: 13px;
|
||||
}
|
||||
.form-group label.required:after {
|
||||
content: " *";
|
||||
color: red;
|
||||
}
|
||||
.form-group input[type="text"],
|
||||
.form-group input[type="email"],
|
||||
.form-group textarea {
|
||||
width: 100%;
|
||||
padding: 6px 8px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 3px;
|
||||
font-size: 13px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.form-group textarea {
|
||||
min-height: 120px;
|
||||
resize: vertical;
|
||||
}
|
||||
.manager-list {
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 3px;
|
||||
padding: 8px;
|
||||
background-color: #fafafa;
|
||||
max-height: 150px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.manager-item {
|
||||
padding: 5px;
|
||||
margin-bottom: 3px;
|
||||
background: white;
|
||||
border-radius: 3px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 13px;
|
||||
}
|
||||
.manager-item input[type="checkbox"] {
|
||||
margin-right: 8px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.manager-item label {
|
||||
cursor: pointer;
|
||||
margin: 0;
|
||||
flex: 1;
|
||||
font-size: 13px;
|
||||
}
|
||||
.no-managers {
|
||||
color: #999;
|
||||
text-align: center;
|
||||
padding: 12px;
|
||||
font-size: 13px;
|
||||
}
|
||||
.button-group {
|
||||
text-align: center;
|
||||
margin-top: 15px;
|
||||
padding-top: 15px;
|
||||
border-top: 1px solid #eee;
|
||||
}
|
||||
.btn {
|
||||
padding: 8px 20px;
|
||||
margin: 0 4px;
|
||||
border: none;
|
||||
border-radius: 3px;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
.btn-primary {
|
||||
background-color: #3085d6;
|
||||
color: white;
|
||||
}
|
||||
.btn-primary:hover {
|
||||
background-color: #2874c5;
|
||||
}
|
||||
.btn-secondary {
|
||||
background-color: #6c757d;
|
||||
color: white;
|
||||
}
|
||||
.btn-secondary:hover {
|
||||
background-color: #5a6268;
|
||||
}
|
||||
.info-text {
|
||||
font-size: 11px;
|
||||
color: #666;
|
||||
margin-top: 3px;
|
||||
}
|
||||
.pdf-status {
|
||||
padding: 8px 10px;
|
||||
background-color: #e7f3ff;
|
||||
border-left: 3px solid #3085d6;
|
||||
border-radius: 3px;
|
||||
margin-bottom: 12px;
|
||||
font-size: 13px;
|
||||
}
|
||||
.pdf-status i {
|
||||
color: #3085d6;
|
||||
margin-right: 5px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="mail-form-container">
|
||||
<div class="form-title">견적서 메일 발송</div>
|
||||
|
||||
<div class="pdf-status">
|
||||
<i class="fa fa-file-pdf-o"></i>
|
||||
<strong>PDF 첨부:</strong> 최종 차수 견적서가 자동으로 첨부됩니다.
|
||||
</div>
|
||||
|
||||
<form id="mailForm">
|
||||
<input type="hidden" id="contractObjId" name="contractObjId" value="<%=contractObjId%>"/>
|
||||
<input type="hidden" id="pdfSessionId" name="pdfSessionId" value=""/>
|
||||
|
||||
<!-- 고객사 담당자 선택 -->
|
||||
<div class="form-group">
|
||||
<label>고객사 담당자 선택</label>
|
||||
<div id="managerListContainer" class="manager-list">
|
||||
<div class="no-managers">담당자 정보를 불러오는 중...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 수신인 이메일 -->
|
||||
<div class="form-group">
|
||||
<label for="toEmails" class="required">수신인 (To)</label>
|
||||
<input type="text" id="toEmails" name="toEmails" placeholder="이메일 주소를 입력하세요 (여러 개는 쉼표로 구분)"/>
|
||||
<div class="info-text">예: email1@example.com, email2@example.com</div>
|
||||
</div>
|
||||
|
||||
<!-- 참조 이메일 -->
|
||||
<div class="form-group">
|
||||
<label for="ccEmails">참조 (CC)</label>
|
||||
<input type="text" id="ccEmails" name="ccEmails" placeholder="참조 이메일 주소 (선택사항)"/>
|
||||
<div class="info-text">작성자 이메일이 자동으로 참조에 추가됩니다.</div>
|
||||
</div>
|
||||
|
||||
<!-- 메일 제목 -->
|
||||
<div class="form-group">
|
||||
<label for="subject" class="required">제목</label>
|
||||
<input type="text" id="subject" name="subject" placeholder="메일 제목을 입력하세요"/>
|
||||
</div>
|
||||
|
||||
<!-- 메일 내용 -->
|
||||
<div class="form-group">
|
||||
<label for="contents" class="required">내용</label>
|
||||
<textarea id="contents" name="contents" placeholder="메일 내용을 입력하세요"></textarea>
|
||||
</div>
|
||||
|
||||
<!-- 버튼 -->
|
||||
<div class="button-group">
|
||||
<button type="button" class="btn btn-primary" onclick="fn_sendMail()">발송</button>
|
||||
<button type="button" class="btn btn-secondary" onclick="window.close()">취소</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
var contractInfo = null;
|
||||
var managerList = [];
|
||||
|
||||
$(document).ready(function(){
|
||||
// 계약 정보 및 담당자 목록 로드
|
||||
fn_loadContractInfo();
|
||||
});
|
||||
|
||||
// 계약 정보 로드
|
||||
function fn_loadContractInfo(){
|
||||
var contractObjId = $("#contractObjId").val();
|
||||
|
||||
$.ajax({
|
||||
url: "/contractMgmt/getContractInfoForMail.do",
|
||||
type: "POST",
|
||||
data: { objId: contractObjId },
|
||||
dataType: "json",
|
||||
success: function(data){
|
||||
if(data.result === "success" && data.contractInfo){
|
||||
contractInfo = data.contractInfo;
|
||||
|
||||
// 메일 제목 자동 생성
|
||||
var contractNo = fnc_checkNull(contractInfo.CONTRACT_NO);
|
||||
var customerName = fnc_checkNull(contractInfo.CUSTOMER_NAME);
|
||||
$("#subject").val("[" + customerName + "] " + contractNo + " 견적서");
|
||||
|
||||
// 메일 내용 템플릿 생성
|
||||
fn_generateMailTemplate();
|
||||
|
||||
// 고객사 담당자 목록 로드
|
||||
var customerObjId = fnc_checkNull(contractInfo.CUSTOMER_OBJID);
|
||||
if(customerObjId !== ""){
|
||||
fn_loadCustomerManagers(customerObjId);
|
||||
} else {
|
||||
$("#managerListContainer").html('<div class="no-managers">고객사 정보가 없습니다.</div>');
|
||||
}
|
||||
} else {
|
||||
Swal.fire({
|
||||
title: '오류',
|
||||
text: '계약 정보를 불러올 수 없습니다.',
|
||||
icon: 'error'
|
||||
}).then(() => {
|
||||
window.close();
|
||||
});
|
||||
}
|
||||
},
|
||||
error: function(){
|
||||
Swal.fire({
|
||||
title: '오류',
|
||||
text: '계약 정보 조회 중 오류가 발생했습니다.',
|
||||
icon: 'error'
|
||||
}).then(() => {
|
||||
window.close();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 고객사 담당자 목록 로드
|
||||
function fn_loadCustomerManagers(customerObjId){
|
||||
$.ajax({
|
||||
url: "/contractMgmt/getCustomerManagerList.do",
|
||||
type: "POST",
|
||||
data: { customerObjId: customerObjId },
|
||||
dataType: "json",
|
||||
success: function(data){
|
||||
if(data.result === "success" && data.managers && data.managers.length > 0){
|
||||
managerList = data.managers;
|
||||
fn_renderManagerList();
|
||||
} else {
|
||||
$("#managerListContainer").html('<div class="no-managers">등록된 담당자가 없습니다. 수신인을 직접 입력해주세요.</div>');
|
||||
}
|
||||
},
|
||||
error: function(){
|
||||
$("#managerListContainer").html('<div class="no-managers">담당자 정보를 불러올 수 없습니다. 수신인을 직접 입력해주세요.</div>');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 담당자 목록 렌더링
|
||||
function fn_renderManagerList(){
|
||||
var html = '';
|
||||
|
||||
for(var i = 0; i < managerList.length; i++){
|
||||
var manager = managerList[i];
|
||||
var name = fnc_checkNull(manager.name);
|
||||
var email = fnc_checkNull(manager.email);
|
||||
|
||||
if(name !== ""){
|
||||
html += '<div class="manager-item">';
|
||||
html += '<input type="checkbox" id="manager_' + i + '" data-email="' + email + '" onchange="fn_updateRecipients()">';
|
||||
html += '<label for="manager_' + i + '">' + name;
|
||||
if(email !== ""){
|
||||
html += ' (' + email + ')';
|
||||
}
|
||||
html += '</label>';
|
||||
html += '</div>';
|
||||
}
|
||||
}
|
||||
|
||||
if(html === ''){
|
||||
html = '<div class="no-managers">등록된 담당자가 없습니다. 수신인을 직접 입력해주세요.</div>';
|
||||
}
|
||||
|
||||
$("#managerListContainer").html(html);
|
||||
}
|
||||
|
||||
// 담당자 선택 시 수신인 필드 업데이트
|
||||
function fn_updateRecipients(){
|
||||
var selectedEmails = [];
|
||||
|
||||
$("input[type='checkbox'][id^='manager_']:checked").each(function(){
|
||||
var email = $(this).attr("data-email");
|
||||
if(email && email !== ""){
|
||||
selectedEmails.push(email);
|
||||
}
|
||||
});
|
||||
|
||||
$("#toEmails").val(selectedEmails.join(", "));
|
||||
}
|
||||
|
||||
// 메일 내용 템플릿 생성
|
||||
function fn_generateMailTemplate(){
|
||||
var customerName = fnc_checkNull(contractInfo.CUSTOMER_NAME);
|
||||
var contractNo = fnc_checkNull(contractInfo.CONTRACT_NO);
|
||||
|
||||
var template = "안녕하세요.\n\n";
|
||||
template += customerName + " 귀하께서 요청하신 견적서를 첨부파일로 송부드립니다.\n\n";
|
||||
template += "영업번호: " + contractNo + "\n\n";
|
||||
template += "첨부된 견적서를 검토하신 후 문의사항이 있으시면 연락 주시기 바랍니다.\n\n";
|
||||
template += "감사합니다.\n";
|
||||
|
||||
$("#contents").val(template);
|
||||
}
|
||||
|
||||
// 메일 발송
|
||||
function fn_sendMail(){
|
||||
// 입력값 검증
|
||||
var toEmails = $("#toEmails").val().trim();
|
||||
var subject = $("#subject").val().trim();
|
||||
var contents = $("#contents").val().trim();
|
||||
|
||||
if(toEmails === ""){
|
||||
Swal.fire("수신인을 입력해주세요.");
|
||||
$("#toEmails").focus();
|
||||
return;
|
||||
}
|
||||
|
||||
// 이메일 형식 검증
|
||||
var emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
var emails = toEmails.split(",").map(function(e){ return e.trim(); });
|
||||
for(var i = 0; i < emails.length; i++){
|
||||
if(!emailPattern.test(emails[i])){
|
||||
Swal.fire("올바른 이메일 형식이 아닙니다: " + emails[i]);
|
||||
$("#toEmails").focus();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if(subject === ""){
|
||||
Swal.fire("제목을 입력해주세요.");
|
||||
$("#subject").focus();
|
||||
return;
|
||||
}
|
||||
|
||||
if(contents === ""){
|
||||
Swal.fire("내용을 입력해주세요.");
|
||||
$("#contents").focus();
|
||||
return;
|
||||
}
|
||||
|
||||
// 발송 확인
|
||||
Swal.fire({
|
||||
title: '메일 발송',
|
||||
text: "견적서를 발송하시겠습니까?",
|
||||
icon: 'question',
|
||||
showCancelButton: true,
|
||||
confirmButtonText: '발송',
|
||||
cancelButtonText: '취소'
|
||||
}).then((result) => {
|
||||
if(result.isConfirmed){
|
||||
// PDF 생성 및 발송 시작
|
||||
fn_generatePdfAndSend();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// PDF 생성 및 발송
|
||||
function fn_generatePdfAndSend(){
|
||||
var contractObjId = $("#contractObjId").val();
|
||||
|
||||
Swal.fire({
|
||||
title: 'PDF 생성 중...',
|
||||
text: '견적서를 PDF로 변환하고 있습니다.',
|
||||
allowOutsideClick: false,
|
||||
onOpen: () => {
|
||||
Swal.showLoading();
|
||||
}
|
||||
});
|
||||
|
||||
// 1. 최종 차수 견적서 정보 조회
|
||||
$.ajax({
|
||||
url: "/contractMgmt/getEstimateTemplateList.do",
|
||||
type: "POST",
|
||||
data: { objId: contractObjId },
|
||||
dataType: "json",
|
||||
success: function(data){
|
||||
if(data.result === "success" && data.list && data.list.length > 0){
|
||||
var latestEstimate = data.list[0];
|
||||
var templateObjId = latestEstimate.OBJID || latestEstimate.objid;
|
||||
var templateType = latestEstimate.TEMPLATE_TYPE || latestEstimate.template_type || latestEstimate.templateType;
|
||||
|
||||
// 2. PDF 생성
|
||||
fn_generatePdf(contractObjId, templateObjId, templateType);
|
||||
} else {
|
||||
Swal.close();
|
||||
Swal.fire({
|
||||
title: '오류',
|
||||
text: '견적서를 찾을 수 없습니다.',
|
||||
icon: 'error'
|
||||
});
|
||||
}
|
||||
},
|
||||
error: function(){
|
||||
Swal.close();
|
||||
Swal.fire({
|
||||
title: '오류',
|
||||
text: '견적서 조회 중 오류가 발생했습니다.',
|
||||
icon: 'error'
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// PDF 생성
|
||||
function fn_generatePdf(contractObjId, templateObjId, templateType){
|
||||
var url = "";
|
||||
if(templateType === "1"){
|
||||
url = "/contractMgmt/estimateTemplate1.do?templateObjId=" + templateObjId;
|
||||
} else if(templateType === "2"){
|
||||
url = "/contractMgmt/estimateTemplate2.do?templateObjId=" + templateObjId;
|
||||
}
|
||||
|
||||
// 숨겨진 iframe으로 페이지 로드
|
||||
var iframe = $('<iframe>', {
|
||||
id: 'pdfGeneratorFrame',
|
||||
src: url,
|
||||
style: 'position:absolute;width:0;height:0;border:none;'
|
||||
}).appendTo('body');
|
||||
|
||||
// iframe 로드 완료 대기
|
||||
iframe.on('load', function(){
|
||||
try {
|
||||
var iframeWindow = this.contentWindow;
|
||||
|
||||
// 데이터 로딩 완료 대기 (최대 120초 = 1200번 시도)
|
||||
var checkDataLoaded = function(attempts) {
|
||||
if(attempts > 1200) {
|
||||
$('#pdfGeneratorFrame').remove();
|
||||
Swal.close();
|
||||
Swal.fire({
|
||||
title: '타임아웃',
|
||||
text: '견적서 데이터 로딩 시간이 초과되었습니다. (120초)',
|
||||
icon: 'error'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if(iframeWindow.dataLoaded === true) {
|
||||
// PDF 생성 함수 호출
|
||||
if(typeof iframeWindow.fn_generateAndUploadPdf === 'function'){
|
||||
iframeWindow.fn_generateAndUploadPdf(function(pdfBase64){
|
||||
$('#pdfGeneratorFrame').remove();
|
||||
|
||||
if(pdfBase64){
|
||||
// PDF 업로드 후 메일 발송
|
||||
fn_uploadPdfAndSendMail(contractObjId, pdfBase64);
|
||||
} else {
|
||||
Swal.close();
|
||||
Swal.fire({
|
||||
title: '오류',
|
||||
text: 'PDF 생성에 실패했습니다.',
|
||||
icon: 'error'
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
$('#pdfGeneratorFrame').remove();
|
||||
Swal.close();
|
||||
Swal.fire({
|
||||
title: '오류',
|
||||
text: '견적서 페이지 로드에 실패했습니다.',
|
||||
icon: 'error'
|
||||
});
|
||||
}
|
||||
} else {
|
||||
setTimeout(function() {
|
||||
checkDataLoaded(attempts + 1);
|
||||
}, 100);
|
||||
}
|
||||
};
|
||||
|
||||
checkDataLoaded(0);
|
||||
} catch(e) {
|
||||
console.error('PDF 생성 오류:', e);
|
||||
$('#pdfGeneratorFrame').remove();
|
||||
Swal.close();
|
||||
Swal.fire({
|
||||
title: '오류',
|
||||
text: 'PDF 생성 중 오류가 발생했습니다.',
|
||||
icon: 'error'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 타임아웃 설정 (180초 = 3분 - 장비 견적서는 데이터가 많아서 시간이 더 필요)
|
||||
setTimeout(function(){
|
||||
if($('#pdfGeneratorFrame').length > 0){
|
||||
$('#pdfGeneratorFrame').remove();
|
||||
Swal.close();
|
||||
Swal.fire({
|
||||
title: '타임아웃',
|
||||
text: 'PDF 생성 시간이 초과되었습니다. (180초)\n견적서 데이터가 많은 경우 시간이 오래 걸릴 수 있습니다.',
|
||||
icon: 'error'
|
||||
});
|
||||
}
|
||||
}, 180000);
|
||||
}
|
||||
|
||||
// PDF 업로드 및 메일 발송
|
||||
function fn_uploadPdfAndSendMail(contractObjId, pdfBase64){
|
||||
Swal.update({
|
||||
text: 'PDF 업로드 중...'
|
||||
});
|
||||
|
||||
// 청크 업로드 (기존 로직 활용)
|
||||
var chunkSize = 100 * 1024;
|
||||
var totalChunks = Math.ceil(pdfBase64.length / chunkSize);
|
||||
var uploadedChunks = 0;
|
||||
var sessionId = 'pdf_' + contractObjId + '_' + new Date().getTime();
|
||||
|
||||
function uploadChunk(chunkIndex) {
|
||||
var start = chunkIndex * chunkSize;
|
||||
var end = Math.min(start + chunkSize, pdfBase64.length);
|
||||
var chunk = pdfBase64.substring(start, end);
|
||||
|
||||
$.ajax({
|
||||
url: "/contractMgmt/uploadPdfChunk.do",
|
||||
type: "POST",
|
||||
data: {
|
||||
sessionId: sessionId,
|
||||
chunkIndex: chunkIndex,
|
||||
totalChunks: totalChunks,
|
||||
chunk: chunk
|
||||
},
|
||||
dataType: "json",
|
||||
timeout: 30000,
|
||||
success: function(data){
|
||||
if(data.result === "success"){
|
||||
uploadedChunks++;
|
||||
|
||||
var progress = Math.round((uploadedChunks / totalChunks) * 100);
|
||||
Swal.update({
|
||||
text: 'PDF 업로드 중... ' + progress + '%'
|
||||
});
|
||||
|
||||
if(chunkIndex + 1 < totalChunks){
|
||||
uploadChunk(chunkIndex + 1);
|
||||
} else {
|
||||
// 모든 청크 업로드 완료, 메일 발송
|
||||
$("#pdfSessionId").val(sessionId);
|
||||
fn_submitMailForm();
|
||||
}
|
||||
} else {
|
||||
Swal.close();
|
||||
Swal.fire({
|
||||
title: '업로드 실패',
|
||||
text: 'PDF 업로드 중 오류가 발생했습니다.',
|
||||
icon: 'error'
|
||||
});
|
||||
}
|
||||
},
|
||||
error: function(){
|
||||
Swal.close();
|
||||
Swal.fire({
|
||||
title: '오류',
|
||||
text: 'PDF 업로드 중 시스템 오류가 발생했습니다.',
|
||||
icon: 'error'
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
uploadChunk(0);
|
||||
}
|
||||
|
||||
// 메일 발송 요청
|
||||
function fn_submitMailForm(){
|
||||
Swal.update({
|
||||
text: '메일 발송 중...'
|
||||
});
|
||||
|
||||
var formData = {
|
||||
objId: $("#contractObjId").val(),
|
||||
pdfSessionId: $("#pdfSessionId").val(),
|
||||
toEmails: $("#toEmails").val(),
|
||||
ccEmails: $("#ccEmails").val(),
|
||||
subject: $("#subject").val(),
|
||||
contents: $("#contents").val()
|
||||
};
|
||||
|
||||
$.ajax({
|
||||
url: "/contractMgmt/sendEstimateMailCustom.do",
|
||||
type: "POST",
|
||||
data: formData,
|
||||
dataType: "json",
|
||||
timeout: 60000,
|
||||
success: function(data){
|
||||
Swal.close();
|
||||
if(data.result === "success"){
|
||||
Swal.fire({
|
||||
title: '발송 완료',
|
||||
text: '견적서가 성공적으로 발송되었습니다.',
|
||||
icon: 'success'
|
||||
}).then(() => {
|
||||
// 부모 창 새로고침
|
||||
if(window.opener && typeof window.opener.fn_search === 'function'){
|
||||
window.opener.fn_search();
|
||||
}
|
||||
window.close();
|
||||
});
|
||||
} else {
|
||||
Swal.fire({
|
||||
title: '발송 실패',
|
||||
text: data.message || '메일 발송 중 오류가 발생했습니다.',
|
||||
icon: 'error'
|
||||
});
|
||||
}
|
||||
},
|
||||
error: function(){
|
||||
Swal.close();
|
||||
Swal.fire({
|
||||
title: '오류',
|
||||
text: '메일 발송 중 시스템 오류가 발생했습니다.',
|
||||
icon: 'error'
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -47,6 +47,9 @@
|
||||
|
||||
// 품번 데이터는 AJAX로 검색하므로 초기 데이터 없음
|
||||
|
||||
// 프로젝트 존재 여부 체크 변수
|
||||
var hasProject = false;
|
||||
|
||||
$(function() {
|
||||
// 반납사유 옵션을 템플릿에서 읽어옴
|
||||
returnReasonOptions = $('#return_reason_template').html();
|
||||
@@ -107,6 +110,9 @@
|
||||
fn_addItemRow();
|
||||
});
|
||||
|
||||
// 프로젝트 존재 여부 먼저 확인 (동기 방식)
|
||||
fn_checkProjectExists();
|
||||
|
||||
// 기존 품목 데이터 로드
|
||||
fn_loadExistingItems();
|
||||
|
||||
@@ -533,6 +539,17 @@
|
||||
// 품번/품명 옵션 채우기 (저장된 품번/품명도 함께 전달)
|
||||
fn_fillPartOptions(itemId, savedPartObjId, savedPartNo, savedPartName);
|
||||
|
||||
// 프로젝트가 있으면 품번/품명 셀렉트박스 비활성화 및 삭제 버튼 비활성화
|
||||
if(hasProject) {
|
||||
$("#PART_NO_" + itemId).prop("disabled", true).css("background-color", "#f5f5f5");
|
||||
$("#PART_NAME_" + itemId).prop("disabled", true).css("background-color", "#f5f5f5");
|
||||
$("#" + itemId + " button[onclick*='fn_deleteItemRow']").prop("disabled", true).css({
|
||||
"background-color": "#ccc",
|
||||
"cursor": "not-allowed",
|
||||
"opacity": "0.6"
|
||||
});
|
||||
}
|
||||
|
||||
// datepicker 적용
|
||||
$("#" + itemId + " .date_icon").datepicker({
|
||||
changeMonth: true,
|
||||
@@ -1176,8 +1193,66 @@
|
||||
|
||||
// ========== 품목 관리 함수 ==========
|
||||
|
||||
// 프로젝트 존재 여부 확인
|
||||
function fn_checkProjectExists() {
|
||||
var contractObjId = "${info.OBJID}";
|
||||
console.log("=== fn_checkProjectExists 시작 ===");
|
||||
console.log("contractObjId:", contractObjId);
|
||||
|
||||
if(!contractObjId || contractObjId === '') {
|
||||
console.log("contractObjId 없음 - 체크 중단");
|
||||
return;
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: "/contractMgmt/checkProjectExists.do",
|
||||
type: "POST",
|
||||
data: { contractObjId: contractObjId },
|
||||
dataType: "json",
|
||||
async: false,
|
||||
success: function(data) {
|
||||
console.log("AJAX 응답 전체:", data);
|
||||
console.log("data.exists 값:", data.exists);
|
||||
console.log("data.exists 타입:", typeof data.exists);
|
||||
console.log("JSON.stringify(data):", JSON.stringify(data));
|
||||
|
||||
if(data && data.exists === true) {
|
||||
hasProject = true;
|
||||
console.log("✅ 프로젝트 존재 확인 - hasProject = true");
|
||||
console.log("프로젝트 정보:", data.projectInfo);
|
||||
|
||||
// 품목 추가 버튼 비활성화
|
||||
$("#btnAddItem").prop("disabled", true).css({
|
||||
"background-color": "#ccc",
|
||||
"cursor": "not-allowed",
|
||||
"opacity": "0.6"
|
||||
});
|
||||
} else {
|
||||
console.log("❌ 프로젝트 없음 - hasProject = false");
|
||||
}
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
console.error("프로젝트 존재 여부 확인 실패:", error);
|
||||
console.error("Status:", status);
|
||||
console.error("Response:", xhr.responseText);
|
||||
}
|
||||
});
|
||||
|
||||
console.log("=== fn_checkProjectExists 종료 - hasProject:", hasProject, "===");
|
||||
}
|
||||
|
||||
// 품목 행 추가
|
||||
function fn_addItemRow() {
|
||||
// 프로젝트가 있으면 품목 추가 불가
|
||||
if(hasProject) {
|
||||
Swal.fire({
|
||||
title: '품목 추가 불가',
|
||||
text: '프로젝트가 이미 생성되어 품목을 추가할 수 없습니다.\n수량, 납기일 등만 수정 가능합니다.',
|
||||
icon: 'warning'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
var itemId = 'item_' + itemCounter++;
|
||||
|
||||
// "품목 추가하세요" 메시지 행 제거
|
||||
@@ -1520,6 +1595,16 @@
|
||||
|
||||
// 품목 행 삭제
|
||||
function fn_deleteItemRow(itemId) {
|
||||
// 프로젝트가 있으면 품목 삭제 불가
|
||||
if(hasProject) {
|
||||
Swal.fire({
|
||||
title: '품목 삭제 불가',
|
||||
text: '프로젝트가 이미 생성되어 품목을 삭제할 수 없습니다.',
|
||||
icon: 'warning'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if(confirm("해당 품목을 삭제하시겠습니까?")) {
|
||||
$("#" + itemId).remove();
|
||||
|
||||
|
||||
@@ -264,6 +264,10 @@ textarea {
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
.item-amount {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
textarea {
|
||||
resize: vertical;
|
||||
min-height: 25px;
|
||||
@@ -323,11 +327,12 @@ $(function(){
|
||||
}
|
||||
});
|
||||
|
||||
// 초기 로드는 $(document).ready에서 처리하므로 주석 처리
|
||||
// templateObjId가 있으면 기존 데이터 로드
|
||||
var templateObjId = "<%=templateObjId%>";
|
||||
if(templateObjId && templateObjId !== ""){
|
||||
fn_loadTemplateData(templateObjId);
|
||||
}
|
||||
// var templateObjId = "<%=templateObjId%>";
|
||||
// if(templateObjId && templateObjId !== ""){
|
||||
// fn_loadTemplateData(templateObjId);
|
||||
// }
|
||||
|
||||
// 인쇄 버튼
|
||||
$("#btnPrint").click(function(){
|
||||
@@ -351,55 +356,62 @@ $(function(){
|
||||
fn_addItemRow();
|
||||
});
|
||||
|
||||
// 금액 자동 계산
|
||||
// 계 행 삭제 버튼
|
||||
$(document).on("click", ".btn-delete-total-row", function(){
|
||||
if(confirm("계 행을 삭제하시겠습니까?")) {
|
||||
$(".total-row").hide();
|
||||
}
|
||||
});
|
||||
|
||||
// 금액 자동 계산 (수량, 단가 변경 시)
|
||||
$(document).on("change keyup", ".item-qty, .item-price", function(){
|
||||
fn_calculateAmount($(this).closest("tr"));
|
||||
fn_calculateTotal(); // 합계 재계산
|
||||
});
|
||||
|
||||
// 금액 필드 직접 수정 시에도 합계 재계산
|
||||
$(document).on("change keyup", ".item-amount", function(){
|
||||
fn_calculateTotal(); // 합계 재계산
|
||||
});
|
||||
|
||||
// 콤마 자동 추가 (동적 요소 포함)
|
||||
$(document).on("blur", ".item-price, .item-amount", function(){
|
||||
var val = $(this).val().replace(/,/g, "").replace(/₩/g, "");
|
||||
// 단가 입력 완료 시 콤마 자동 추가
|
||||
$(document).on("blur", ".item-price", function(){
|
||||
var val = $(this).val().replace(/,/g, "");
|
||||
if(!isNaN(val) && val !== "") {
|
||||
$(this).val(addComma(val));
|
||||
}
|
||||
fn_calculateTotal(); // blur 시에도 합계 재계산
|
||||
});
|
||||
|
||||
// 단가 입력 시 실시간 콤마 처리
|
||||
// 단가 입력 시 숫자만 입력 가능하도록 제한 및 실시간 콤마 처리
|
||||
$(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;
|
||||
var originalVal = $(this).val();
|
||||
var commasBefore = (originalVal.substring(0, cursorPos).match(/,/g) || []).length;
|
||||
|
||||
if(!isNaN(val) && val !== "") {
|
||||
// 콤마 제거 후 숫자가 아닌 문자 제거
|
||||
var val = originalVal.replace(/,/g, "").replace(/[^0-9]/g, "");
|
||||
|
||||
// 값 설정 (빈 문자열 포함)
|
||||
if(val !== "") {
|
||||
$(this).val(addComma(val));
|
||||
// 커서 위치 조정
|
||||
var commasAfter = ($(this).val().substring(0, cursorPos).match(/,/g) || []).length;
|
||||
var newPos = cursorPos + (commasAfter - commasBefore);
|
||||
this.setSelectionRange(newPos, newPos);
|
||||
} else {
|
||||
$(this).val("");
|
||||
}
|
||||
});
|
||||
|
||||
// 수량 입력 시 숫자만 입력 가능하도록 제한
|
||||
$(document).on("input", ".item-qty", function(){
|
||||
var val = $(this).val().replace(/[^0-9]/g, "");
|
||||
$(this).val(val);
|
||||
});
|
||||
|
||||
// 데이터 로드
|
||||
if("<%=objId%>" !== "" && "<%=objId%>" !== "-1") {
|
||||
fn_loadData();
|
||||
} else {
|
||||
// 새 견적서 작성 시 기본 행의 셀렉트박스 초기화
|
||||
fn_initItemDescSelect('default_item_1');
|
||||
fn_initItemDescSelect('default_item_2');
|
||||
|
||||
// 초기 로드 시 합계 계산
|
||||
fn_calculateTotal();
|
||||
|
||||
// 새로 등록 시 명시적으로 작성중 상태 설정
|
||||
g_apprStatus = "작성중";
|
||||
fn_controlButtons();
|
||||
if("<%=templateObjId%>" !== "" && "<%=templateObjId%>" !== "-1") {
|
||||
// 저장된 견적서 수정: 견적서 데이터 로드
|
||||
fn_loadTemplateData("<%=templateObjId%>");
|
||||
} else if("<%=objId%>" !== "" && "<%=objId%>" !== "-1") {
|
||||
// 처음 견적서 작성: 영업정보의 품목 데이터 로드
|
||||
fn_loadContractItems("<%=objId%>");
|
||||
}
|
||||
});
|
||||
|
||||
@@ -427,6 +439,7 @@ function fn_controlButtons() {
|
||||
|
||||
// 삭제 버튼 숨김
|
||||
$(".btn-delete-row").hide();
|
||||
$(".btn-delete-total-row").hide(); // 계 삭제 버튼도 숨김
|
||||
} else {
|
||||
console.log("결재완료 아님 - 입력 필드 활성화");
|
||||
// 결재완료가 아닌 경우 버튼 표시
|
||||
@@ -446,6 +459,7 @@ function fn_controlButtons() {
|
||||
|
||||
// 삭제 버튼 표시
|
||||
$(".btn-delete-row").show();
|
||||
$(".btn-delete-total-row").show(); // 계 삭제 버튼도 표시
|
||||
}
|
||||
}
|
||||
|
||||
@@ -456,7 +470,8 @@ function fn_calculateAmount(row) {
|
||||
|
||||
var amount = parseInt(qty) * parseInt(price);
|
||||
if(!isNaN(amount)) {
|
||||
row.find(".item-amount").val(addComma(amount));
|
||||
var currencySymbol = getCurrencySymbol();
|
||||
row.find(".item-amount").val(currencySymbol + addComma(amount));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -467,8 +482,8 @@ function fn_calculateTotal() {
|
||||
// 품목 행만 순회 (계 행, 원화환산 행, 비고 행, 참조사항 행, 회사명 행 제외)
|
||||
$("#itemsTableBody tr").not(".total-row, .total-krw-row, .remarks-row, .notes-row, .footer-row").each(function(){
|
||||
var amount = $(this).find(".item-amount").val() || "0";
|
||||
// 콤마와 통화 기호 제거 후 숫자로 변환
|
||||
amount = amount.replace(/,/g, "").replace(/₩/g, "").replace(/\$/g, "").replace(/€/g, "").replace(/¥/g, "");
|
||||
// 모든 비숫자 문자 제거 (통화 기호, 콤마 등)
|
||||
amount = amount.replace(/[^0-9]/g, "");
|
||||
var numAmount = parseInt(amount) || 0;
|
||||
total += numAmount;
|
||||
});
|
||||
@@ -508,7 +523,7 @@ function getCurrencySymbol() {
|
||||
}
|
||||
}
|
||||
|
||||
// 품명 셀렉트박스 초기화 함수
|
||||
/* 품명 셀렉트박스 초기화 함수 - 주석처리 (텍스트 readonly로 변경)
|
||||
function fn_initItemDescSelect(itemId) {
|
||||
$("#" + itemId + " .item-desc-select").select2({
|
||||
placeholder: "품명 입력하여 검색...",
|
||||
@@ -579,6 +594,7 @@ function fn_initItemDescSelect(itemId) {
|
||||
$("#" + itemId + " .item-part-objid").val(''); // part_objid 초기화
|
||||
});
|
||||
}
|
||||
*/
|
||||
|
||||
// 행 추가 함수
|
||||
function fn_addItemRow() {
|
||||
@@ -591,8 +607,7 @@ function fn_addItemRow() {
|
||||
var newRow = '<tr id="' + itemId + '">' +
|
||||
'<td>' + nextNo + '</td>' +
|
||||
'<td class="text-left editable">' +
|
||||
'<select class="item-desc-select" style="width:100%;"></select>' +
|
||||
'<input type="hidden" class="item-desc" value="">' +
|
||||
'<input type="text" class="item-desc" value="" readonly style="background-color: #f5f5f5;">' +
|
||||
'<input type="hidden" class="item-part-objid" value="">' +
|
||||
'</td>' +
|
||||
'<td class="text-left editable"><textarea class="item-spec"></textarea></td>' +
|
||||
@@ -606,13 +621,166 @@ function fn_addItemRow() {
|
||||
// 계 행 바로 위에 추가
|
||||
$(".total-row").before(newRow);
|
||||
|
||||
// 품명 셀렉트박스 초기화
|
||||
fn_initItemDescSelect(itemId);
|
||||
// 품명 셀렉트박스 초기화 - 주석처리 (텍스트 readonly로 변경)
|
||||
// fn_initItemDescSelect(itemId);
|
||||
|
||||
// 합계 재계산
|
||||
fn_calculateTotal();
|
||||
}
|
||||
|
||||
// 영업정보의 품목 데이터 로드
|
||||
function fn_loadContractItems(contractObjId) {
|
||||
$.ajax({
|
||||
url: "/contractMgmt/getContractItemList.do",
|
||||
type: "POST",
|
||||
data: {
|
||||
contractObjId: contractObjId
|
||||
},
|
||||
dataType: "json",
|
||||
success: function(data) {
|
||||
console.log("품목 데이터:", data); // 디버깅용
|
||||
|
||||
if(data && data.result === "success" && data.items && data.items.length > 0) {
|
||||
// 환율 정보 설정
|
||||
if(data.exchangeRate) {
|
||||
g_exchangeRate = parseFloat(data.exchangeRate);
|
||||
}
|
||||
if(data.currencyName) {
|
||||
g_currencyName = data.currencyName;
|
||||
}
|
||||
console.log("환율 정보:", g_exchangeRate, g_currencyName);
|
||||
|
||||
// 고객사 정보 설정
|
||||
if(data.customerObjId && data.customerObjId !== "") {
|
||||
// 데이터 로드 중 플래그 설정
|
||||
window.isLoadingData = true;
|
||||
|
||||
$("#recipient").val(data.customerObjId).trigger('change');
|
||||
|
||||
// 담당자 목록 로드
|
||||
fn_loadCustomerContact(data.customerObjId);
|
||||
|
||||
// 플래그 해제
|
||||
setTimeout(function() {
|
||||
window.isLoadingData = false;
|
||||
}, 100);
|
||||
}
|
||||
|
||||
// 기존 테이블 초기화
|
||||
$("#itemsTableBody").empty();
|
||||
|
||||
// 품목 HTML 생성
|
||||
var itemsHtml = "";
|
||||
for(var i = 0; i < data.items.length; i++) {
|
||||
var item = data.items[i];
|
||||
console.log("품목 " + (i+1) + ":", item); // 각 품목 디버깅
|
||||
|
||||
var itemId = 'contract_item_' + i;
|
||||
var partNo = item.PART_NO || item.part_no || '';
|
||||
var partName = item.PART_NAME || item.part_name || '';
|
||||
var quantity = item.QUANTITY || item.quantity || '';
|
||||
var partObjId = item.PART_OBJID || item.part_objid || '';
|
||||
|
||||
console.log("추출된 값 - partName:", partName, ", partObjId:", partObjId, ", quantity:", quantity);
|
||||
|
||||
itemsHtml += '<tr id="' + itemId + '">';
|
||||
itemsHtml += '<td>' + (i + 1) + '</td>';
|
||||
itemsHtml += '<td class="text-left editable">';
|
||||
itemsHtml += '<input type="text" class="item-desc" value="' + partName + '" readonly style="background-color: #f5f5f5;">';
|
||||
itemsHtml += '<input type="hidden" class="item-part-objid" value="' + partObjId + '">';
|
||||
itemsHtml += '</td>';
|
||||
itemsHtml += '<td class="text-left editable"><textarea class="item-spec"></textarea></td>';
|
||||
itemsHtml += '<td class="editable"><input type="text" class="item-qty" value="' + quantity + '"></td>';
|
||||
itemsHtml += '<td class="editable"><input type="text" class="item-unit" value="EA"></td>';
|
||||
itemsHtml += '<td class="text-right editable"><input type="text" class="item-price" value=""></td>';
|
||||
itemsHtml += '<td class="text-right editable"><input type="text" class="item-amount" value="" readonly></td>';
|
||||
itemsHtml += '<td class="editable"><input type="text" class="item-note" value=""></td>';
|
||||
itemsHtml += '</tr>';
|
||||
}
|
||||
|
||||
// 계 행 추가
|
||||
itemsHtml += '<tr class="total-row">';
|
||||
itemsHtml += '<td colspan="6" style="text-align: center; font-weight: bold; background-color: #f0f0f0;">계</td>';
|
||||
itemsHtml += '<td class="text-right" style="font-weight: bold; background-color: #f0f0f0;"><span id="totalAmount">0</span></td>';
|
||||
itemsHtml += '<td style="background-color: #f0f0f0; text-align: center;"><button type="button" class="btn-delete-total-row" style="padding: 2px 8px; font-size: 9pt; cursor: pointer;">삭제</button></td>';
|
||||
itemsHtml += '</tr>';
|
||||
|
||||
// 원화환산 공급가액 행 추가 (숨김)
|
||||
itemsHtml += '<tr class="total-krw-row" style="display: none;">';
|
||||
itemsHtml += '<td colspan="6" style="text-align: center; font-weight: bold; background-color: #e8f4f8;">원화환산 공급가액 (KRW)</td>';
|
||||
itemsHtml += '<td class="text-right" style="font-weight: bold; background-color: #e8f4f8;"><span id="totalAmountKRW">0</span></td>';
|
||||
itemsHtml += '<td style="background-color: #e8f4f8;"></td>';
|
||||
itemsHtml += '</tr>';
|
||||
|
||||
// 비고 행 추가
|
||||
itemsHtml += '<tr class="remarks-row">';
|
||||
itemsHtml += '<td colspan="8" style="height: 100px; vertical-align: top; padding: 10px; text-align: left;">';
|
||||
itemsHtml += '<div style="font-weight: bold; margin-bottom: 10px; text-align: left;"><비고></div>';
|
||||
itemsHtml += '<textarea id="note_remarks" style="width: 100%; height: 70px; border: none; resize: none; font-family: inherit; font-size: 10pt; text-align: left;"></textarea>';
|
||||
itemsHtml += '</td>';
|
||||
itemsHtml += '</tr>';
|
||||
|
||||
// 참조사항 행 추가
|
||||
itemsHtml += '<tr class="notes-row">';
|
||||
itemsHtml += '<td colspan="8" style="vertical-align: top; padding: 10px; text-align: left; border: 1px solid #000;">';
|
||||
itemsHtml += '<div style="font-weight: bold; margin-bottom: 10px; text-align: left;"><참조사항></div>';
|
||||
itemsHtml += '<div class="editable" style="margin-bottom: 5px;"><input type="text" id="note1" value="1. 견적유효기간: 일" style="width: 100%; border: none; background: transparent; font-size: 10pt;"></div>';
|
||||
itemsHtml += '<div class="editable" style="margin-bottom: 5px;"><input type="text" id="note2" value="2. 납품기간: 발주 후 1주 이내" style="width: 100%; border: none; background: transparent; font-size: 10pt;"></div>';
|
||||
itemsHtml += '<div class="editable" style="margin-bottom: 5px;"><input type="text" id="note3" value="3. VAT 별도" style="width: 100%; border: none; background: transparent; font-size: 10pt;"></div>';
|
||||
itemsHtml += '<div class="editable" style="margin-bottom: 5px;"><input type="text" id="note4" value="4. 결제 조건 : 기존 결제조건에 따름." style="width: 100%; border: none; background: transparent; font-size: 10pt;"></div>';
|
||||
itemsHtml += '</td>';
|
||||
itemsHtml += '</tr>';
|
||||
|
||||
// 하단 회사명 행 추가
|
||||
itemsHtml += '<tr class="footer-row">';
|
||||
itemsHtml += '<td colspan="8" style="text-align: right; padding: 15px; font-size: 10pt; font-weight: bold; border: none;">';
|
||||
itemsHtml += '㈜알피에스';
|
||||
itemsHtml += '</td>';
|
||||
itemsHtml += '</tr>';
|
||||
|
||||
// HTML 삽입
|
||||
$("#itemsTableBody").html(itemsHtml);
|
||||
|
||||
/* 셀렉트박스 초기화 - 주석처리 (텍스트 readonly로 변경)
|
||||
for(var i = 0; i < data.items.length; i++) {
|
||||
var itemId = 'contract_item_' + i;
|
||||
fn_initItemDescSelect(itemId);
|
||||
}
|
||||
*/
|
||||
|
||||
// 합계 계산
|
||||
fn_calculateTotal();
|
||||
|
||||
// 결재상태에 따라 버튼 제어
|
||||
g_apprStatus = "작성중";
|
||||
fn_controlButtons();
|
||||
} else {
|
||||
// 품목이 없으면 기본 행 표시
|
||||
/* 셀렉트박스 초기화 - 주석처리 (텍스트 readonly로 변경)
|
||||
fn_initItemDescSelect('default_item_1');
|
||||
fn_initItemDescSelect('default_item_2');
|
||||
*/
|
||||
fn_calculateTotal();
|
||||
g_apprStatus = "작성중";
|
||||
fn_controlButtons();
|
||||
}
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
console.error("품목 데이터 로드 오류:", xhr, status, error);
|
||||
Swal.fire("품목 데이터를 불러오는데 실패했습니다.");
|
||||
|
||||
// 오류 시 기본 행 표시
|
||||
/* 셀렉트박스 초기화 - 주석처리 (텍스트 readonly로 변경)
|
||||
fn_initItemDescSelect('default_item_1');
|
||||
fn_initItemDescSelect('default_item_2');
|
||||
*/
|
||||
fn_calculateTotal();
|
||||
g_apprStatus = "작성중";
|
||||
fn_controlButtons();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 데이터 로드
|
||||
function fn_loadData() {
|
||||
$.ajax({
|
||||
@@ -692,12 +860,12 @@ function fn_loadData() {
|
||||
itemsHtml += '</tr>';
|
||||
}
|
||||
|
||||
// 계 행 추가
|
||||
itemsHtml += '<tr class="total-row">';
|
||||
itemsHtml += '<td colspan="6" style="text-align: center; font-weight: bold; background-color: #f0f0f0;">계</td>';
|
||||
itemsHtml += '<td class="text-right" style="font-weight: bold; background-color: #f0f0f0;"><span id="totalAmount">0</span></td>';
|
||||
itemsHtml += '<td style="background-color: #f0f0f0;"></td>';
|
||||
itemsHtml += '</tr>';
|
||||
// 계 행 추가
|
||||
itemsHtml += '<tr class="total-row">';
|
||||
itemsHtml += '<td colspan="6" style="text-align: center; font-weight: bold; background-color: #f0f0f0;">계</td>';
|
||||
itemsHtml += '<td class="text-right" style="font-weight: bold; background-color: #f0f0f0;"><span id="totalAmount">0</span></td>';
|
||||
itemsHtml += '<td style="background-color: #f0f0f0; text-align: center;"><button type="button" class="btn-delete-total-row" style="padding: 2px 8px; font-size: 9pt; cursor: pointer;">삭제</button></td>';
|
||||
itemsHtml += '</tr>';
|
||||
|
||||
// 원화환산 공급가액 행 추가 (숨김)
|
||||
itemsHtml += '<tr class="total-krw-row" style="display: none;">';
|
||||
@@ -767,9 +935,11 @@ function fn_loadData() {
|
||||
}
|
||||
*/ // 품목 데이터 로드 주석처리 끝
|
||||
|
||||
// 새로 작성 시 기본 행의 셀렉트박스 초기화
|
||||
// 새로 작성 시 기본 행의 셀렉트박스 초기화 - 주석처리 (텍스트 readonly로 변경)
|
||||
/*
|
||||
fn_initItemDescSelect('default_item_1');
|
||||
fn_initItemDescSelect('default_item_2');
|
||||
*/
|
||||
|
||||
// 초기 로드 시 합계 계산
|
||||
fn_calculateTotal();
|
||||
@@ -843,6 +1013,9 @@ function fn_loadTemplateData(templateObjId){
|
||||
if(recipient && recipient !== "") {
|
||||
// OBJID로 셀렉트박스 선택
|
||||
$("#recipient").val(recipient).trigger('change');
|
||||
|
||||
// 담당자 목록 로드 (저장된 수신인도 함께 전달)
|
||||
fn_loadCustomerContact(recipient, contactPerson);
|
||||
}
|
||||
|
||||
// 플래그 해제
|
||||
@@ -883,20 +1056,17 @@ function fn_loadTemplateData(templateObjId){
|
||||
var note = item.NOTE || item.note || '';
|
||||
var partObjId = item.PART_OBJID || item.part_objid || '';
|
||||
|
||||
// 단가와 금액에 콤마 추가
|
||||
// 통화 기호 가져오기
|
||||
var currencySymbol = getCurrencySymbol();
|
||||
|
||||
// 단가와 금액에 콤마 및 통화 기호 추가
|
||||
var unitPriceFormatted = unitPrice ? addComma(unitPrice) : '';
|
||||
var amountFormatted = amount ? addComma(amount) : '';
|
||||
var amountFormatted = amount ? (currencySymbol + addComma(amount)) : '';
|
||||
|
||||
itemsHtml += '<tr id="' + itemId + '">';
|
||||
itemsHtml += '<td>' + (i + 1) + '</td>';
|
||||
itemsHtml += '<td class="text-left editable">';
|
||||
itemsHtml += '<select class="item-desc-select" style="width:100%;">';
|
||||
// PART_OBJID가 없으면 기존 텍스트를 옵션으로 표시
|
||||
if(description) {
|
||||
itemsHtml += '<option value="" selected>' + description + '</option>';
|
||||
}
|
||||
itemsHtml += '</select>';
|
||||
itemsHtml += '<input type="hidden" class="item-desc" value="' + description + '">';
|
||||
itemsHtml += '<input type="text" class="item-desc" value="' + description + '" readonly style="background-color: #f5f5f5;">';
|
||||
itemsHtml += '<input type="hidden" class="item-part-objid" value="' + partObjId + '">';
|
||||
itemsHtml += '</td>';
|
||||
itemsHtml += '<td class="text-left editable"><textarea class="item-spec">' + specification + '</textarea></td>';
|
||||
@@ -908,12 +1078,12 @@ function fn_loadTemplateData(templateObjId){
|
||||
itemsHtml += '</tr>';
|
||||
}
|
||||
|
||||
// 계 행 추가
|
||||
itemsHtml += '<tr class="total-row">';
|
||||
itemsHtml += '<td colspan="6" style="text-align: center; font-weight: bold; background-color: #f0f0f0;">계</td>';
|
||||
itemsHtml += '<td class="text-right" style="font-weight: bold; background-color: #f0f0f0;"><span id="totalAmount">0</span></td>';
|
||||
itemsHtml += '<td style="background-color: #f0f0f0;"></td>';
|
||||
itemsHtml += '</tr>';
|
||||
// 계 행 추가
|
||||
itemsHtml += '<tr class="total-row">';
|
||||
itemsHtml += '<td colspan="6" style="text-align: center; font-weight: bold; background-color: #f0f0f0;">계</td>';
|
||||
itemsHtml += '<td class="text-right" style="font-weight: bold; background-color: #f0f0f0;"><span id="totalAmount">0</span></td>';
|
||||
itemsHtml += '<td style="background-color: #f0f0f0; text-align: center;"><button type="button" class="btn-delete-total-row" style="padding: 2px 8px; font-size: 9pt; cursor: pointer;">삭제</button></td>';
|
||||
itemsHtml += '</tr>';
|
||||
|
||||
// 원화환산 공급가액 행 추가 (숨김)
|
||||
itemsHtml += '<tr class="total-krw-row" style="display: none;">';
|
||||
@@ -951,7 +1121,7 @@ function fn_loadTemplateData(templateObjId){
|
||||
// HTML 삽입
|
||||
$("#itemsTableBody").html(itemsHtml);
|
||||
|
||||
// 셀렉트박스 초기화 및 데이터 설정
|
||||
/* 셀렉트박스 초기화 및 데이터 설정 - 주석처리 (텍스트 readonly로 변경)
|
||||
for(var i = 0; i < data.items.length; i++) {
|
||||
var item = data.items[i];
|
||||
var itemId = 'template_item_' + i;
|
||||
@@ -993,6 +1163,7 @@ function fn_loadTemplateData(templateObjId){
|
||||
// 셀렉트박스 초기화 (옵션 추가 후)
|
||||
fn_initItemDescSelect(itemId);
|
||||
}
|
||||
*/
|
||||
|
||||
// 테이블 내 비고 값 설정 (textarea 생성 직후)
|
||||
$("#note_remarks").val(noteRemarks);
|
||||
@@ -1006,6 +1177,14 @@ function fn_loadTemplateData(templateObjId){
|
||||
// 합계 계산
|
||||
fn_calculateTotal();
|
||||
|
||||
// 계 행 표시 여부 복원
|
||||
var showTotalRow = template.SHOW_TOTAL_ROW || template.show_total_row || "Y";
|
||||
if(showTotalRow === "N") {
|
||||
$(".total-row").hide();
|
||||
} else {
|
||||
$(".total-row").show();
|
||||
}
|
||||
|
||||
// 결재상태에 따라 버튼 제어
|
||||
fn_controlButtons();
|
||||
|
||||
@@ -1027,27 +1206,42 @@ function fn_loadTemplateData(templateObjId){
|
||||
}
|
||||
|
||||
// 저장
|
||||
// 고객사 담당자 정보 로드
|
||||
function fn_loadCustomerContact(customerObjId) {
|
||||
// 고객사 담당자 목록 로드
|
||||
function fn_loadCustomerContact(customerObjId, selectedContact) {
|
||||
if(!customerObjId || customerObjId === "") {
|
||||
$("#contact_person").val("");
|
||||
$("#contact_person").empty().append('<option value="">담당자 선택</option>');
|
||||
return;
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: "/contractMgmt/getCustomerContactInfo.do",
|
||||
url: "/contractMgmt/getCustomerManagerList.do",
|
||||
type: "POST",
|
||||
data: { customerObjId: customerObjId },
|
||||
dataType: "json",
|
||||
success: function(data) {
|
||||
if(data && data.contactPerson) {
|
||||
$("#contact_person").val(data.contactPerson + " 귀하");
|
||||
$("#contact_person").empty();
|
||||
$("#contact_person").append('<option value="">담당자 선택</option>');
|
||||
|
||||
if(data && data.managers && data.managers.length > 0) {
|
||||
for(var i = 0; i < data.managers.length; i++) {
|
||||
var manager = data.managers[i];
|
||||
if(manager.name && manager.name !== "") {
|
||||
$("#contact_person").append('<option value="' + manager.name + ' 귀하">' + manager.name + ' 귀하</option>');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$("#contact_person").val("구매 담당자님 귀하");
|
||||
$("#contact_person").append('<option value="구매 담당자님 귀하">구매 담당자님 귀하</option>');
|
||||
}
|
||||
|
||||
// 저장된 수신인이 있으면 선택
|
||||
if(selectedContact && selectedContact !== "") {
|
||||
$("#contact_person").val(selectedContact);
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
$("#contact_person").val("구매 담당자님 귀하");
|
||||
$("#contact_person").empty();
|
||||
$("#contact_person").append('<option value="">담당자 선택</option>');
|
||||
$("#contact_person").append('<option value="구매 담당자님 귀하">구매 담당자님 귀하</option>');
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1066,10 +1260,10 @@ function fn_save() {
|
||||
part_objid: row.find(".item-part-objid").val() || "", // part_objid 추가
|
||||
description: row.find(".item-desc").val() || "",
|
||||
specification: row.find(".item-spec").val() || "",
|
||||
quantity: quantity.replace(/,/g, ""), // 콤마 제거
|
||||
quantity: quantity.replace(/[^0-9]/g, ""), // 숫자만 추출
|
||||
unit: row.find(".item-unit").val() || "",
|
||||
unit_price: unitPrice.replace(/,/g, ""), // 콤마 제거
|
||||
amount: amount.replace(/,/g, "").replace(/₩/g, ""), // 콤마와 ₩ 제거
|
||||
unit_price: unitPrice.replace(/[^0-9]/g, ""), // 숫자만 추출
|
||||
amount: amount.replace(/[^0-9]/g, ""), // 숫자만 추출
|
||||
note: row.find(".item-note").val() || ""
|
||||
});
|
||||
});
|
||||
@@ -1090,6 +1284,9 @@ function fn_save() {
|
||||
// 디버깅: 품목 데이터 확인
|
||||
console.log("저장할 품목 데이터:", items);
|
||||
|
||||
// 계 행 표시 여부 확인
|
||||
var showTotalRow = $(".total-row").is(":visible") ? "Y" : "N";
|
||||
|
||||
var formData = {
|
||||
objId: contractObjId,
|
||||
template_type: "1",
|
||||
@@ -1107,6 +1304,7 @@ function fn_save() {
|
||||
note2: $("#note2").val(),
|
||||
note3: $("#note3").val(),
|
||||
note4: $("#note4").val(),
|
||||
show_total_row: showTotalRow, // 계 행 표시 여부
|
||||
items: JSON.stringify(items)
|
||||
};
|
||||
|
||||
@@ -1189,7 +1387,9 @@ function fn_save() {
|
||||
<tr>
|
||||
<td class="label">수신인</td>
|
||||
<td class="editable">
|
||||
<input type="text" id="contact_person" value="구매 담당자님 귀하" readonly style="background-color: #f5f5f5;">
|
||||
<select id="contact_person" style="width: 100%; border: none; font-size: 9pt; padding: 2px;">
|
||||
<option value="">담당자 선택</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@@ -1233,8 +1433,7 @@ function fn_save() {
|
||||
<tr id="default_item_1">
|
||||
<td>1</td>
|
||||
<td class="text-left editable">
|
||||
<select class="item-desc-select" style="width:100%;"></select>
|
||||
<input type="hidden" class="item-desc" value="">
|
||||
<input type="text" class="item-desc" value="" readonly style="background-color: #f5f5f5;">
|
||||
<input type="hidden" class="item-part-objid" value="">
|
||||
</td>
|
||||
<td class="text-left editable"><textarea class="item-spec"></textarea></td>
|
||||
@@ -1247,8 +1446,7 @@ function fn_save() {
|
||||
<tr id="default_item_2">
|
||||
<td>2</td>
|
||||
<td class="text-left editable">
|
||||
<select class="item-desc-select" style="width:100%;"></select>
|
||||
<input type="hidden" class="item-desc" value="">
|
||||
<input type="text" class="item-desc" value="" readonly style="background-color: #f5f5f5;">
|
||||
<input type="hidden" class="item-part-objid" value="">
|
||||
</td>
|
||||
<td class="text-left editable"><textarea class="item-spec"></textarea></td>
|
||||
@@ -1258,12 +1456,12 @@ function fn_save() {
|
||||
<td class="text-right editable"><input type="text" class="item-amount" value="" readonly></td>
|
||||
<td class="editable"><input type="text" class="item-note" value=""></td>
|
||||
</tr>
|
||||
<!-- 계 행 -->
|
||||
<tr class="total-row">
|
||||
<td colspan="6" style="text-align: center; font-weight: bold; background-color: #f0f0f0;">계</td>
|
||||
<td class="text-right" style="font-weight: bold; background-color: #f0f0f0;"><span id="totalAmount">0</span></td>
|
||||
<td style="background-color: #f0f0f0;"></td>
|
||||
</tr>
|
||||
<!-- 계 행 -->
|
||||
<tr class="total-row">
|
||||
<td colspan="6" style="text-align: center; font-weight: bold; background-color: #f0f0f0;">계</td>
|
||||
<td class="text-right" style="font-weight: bold; background-color: #f0f0f0;"><span id="totalAmount">0</span></td>
|
||||
<td style="background-color: #f0f0f0; text-align: center;"><button type="button" class="btn-delete-total-row" style="padding: 2px 8px; font-size: 9pt; cursor: pointer;">삭제</button></td>
|
||||
</tr>
|
||||
<!-- 원화환산 공급가액 행 (숨김) -->
|
||||
<tr class="total-krw-row" style="display: none;">
|
||||
<td colspan="6" style="text-align: center; font-weight: bold; background-color: #e8f4f8;">원화환산 공급가액 (KRW)</td>
|
||||
@@ -1299,7 +1497,7 @@ function fn_save() {
|
||||
|
||||
<!-- 버튼 영역 -->
|
||||
<div class="btn-area no-print">
|
||||
<button type="button" id="btnAddRow" class="estimate-btn">행 추가</button>
|
||||
<!-- <button type="button" id="btnAddRow" class="estimate-btn">행 추가</button> -->
|
||||
<button type="button" id="btnPrint" class="estimate-btn">인쇄</button>
|
||||
<button type="button" id="btnDownloadPdf" class="estimate-btn">PDF 다운로드</button>
|
||||
<button type="button" id="btnSave" class="estimate-btn">저장</button>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
201
WebContent/WEB-INF/view/contractMgmt/estimateViewPopup.jsp
Normal file
201
WebContent/WEB-INF/view/contractMgmt/estimateViewPopup.jsp
Normal file
@@ -0,0 +1,201 @@
|
||||
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
|
||||
<%@ page import="com.pms.common.utils.*"%>
|
||||
<%@ page import="java.util.*"%>
|
||||
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt"%>
|
||||
<%@include file="/init_new.jsp"%>
|
||||
<%
|
||||
PersonBean person = (PersonBean) session.getAttribute(Constants.PERSON_BEAN);
|
||||
String userId = CommonUtils.checkNull(person.getUserId());
|
||||
%>
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
<title><%=Constants.SYSTEM_NAME%></title>
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Malgun Gothic', sans-serif;
|
||||
font-size: 12px;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.popup-container {
|
||||
background-color: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
padding: 10px;
|
||||
background-color: #f8f9fa;
|
||||
border-left: 4px solid #007bff;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.info-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.info-table th {
|
||||
background-color: #f8f9fa;
|
||||
padding: 10px;
|
||||
text-align: center;
|
||||
border: 1px solid #dee2e6;
|
||||
font-weight: bold;
|
||||
width: 15%;
|
||||
}
|
||||
|
||||
.info-table td {
|
||||
padding: 10px;
|
||||
border: 1px solid #dee2e6;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.item-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.item-table thead th {
|
||||
background-color: #f8f9fa;
|
||||
padding: 10px;
|
||||
text-align: center;
|
||||
border: 1px solid #dee2e6;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.item-table tbody td {
|
||||
padding: 8px;
|
||||
border: 1px solid #dee2e6;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.item-table tbody td.text-left {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.btn-area {
|
||||
text-align: center;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.plm_btns {
|
||||
padding: 8px 20px;
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
margin: 0 5px;
|
||||
}
|
||||
|
||||
.plm_btns:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
</style>
|
||||
<script type="text/javascript">
|
||||
$(function() {
|
||||
$("#btnClose").click(function() {
|
||||
self.close();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="popup-container">
|
||||
<!-- 영업정보 섹션 -->
|
||||
<div class="section-title">영업정보</div>
|
||||
<table class="info-table">
|
||||
<colgroup>
|
||||
<col width="15%">
|
||||
<col width="35%">
|
||||
<col width="15%">
|
||||
<col width="35%">
|
||||
</colgroup>
|
||||
<tr>
|
||||
<th>주문유형</th>
|
||||
<td>${info.CATEGORY_NAME}</td>
|
||||
<th>제품구분</th>
|
||||
<td>${info.PRODUCT_NAME}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>유/무상</th>
|
||||
<td>${info.PAID_TYPE_NAME}</td>
|
||||
<th>접수일</th>
|
||||
<td>${info.RECEIPT_DATE}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>국내/해외</th>
|
||||
<td>${info.AREA_NAME}</td>
|
||||
<th>고객사</th>
|
||||
<td>${info.CUSTOMER_NAME}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- 품목정보 섹션 -->
|
||||
<div class="section-title">품목정보</div>
|
||||
<table class="item-table">
|
||||
<colgroup>
|
||||
<col width="5%">
|
||||
<col width="15%">
|
||||
<col width="25%">
|
||||
<col width="15%">
|
||||
<col width="10%">
|
||||
<col width="15%">
|
||||
<col width="15%">
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>No</th>
|
||||
<th>품번</th>
|
||||
<th>품명</th>
|
||||
<th>S/N</th>
|
||||
<th>요청납기</th>
|
||||
<th>고객요청사항</th>
|
||||
<th>반납사유</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<c:choose>
|
||||
<c:when test="${empty itemList}">
|
||||
<tr>
|
||||
<td colspan="7" style="text-align:center; padding:30px; color:#999;">
|
||||
등록된 품목이 없습니다.
|
||||
</td>
|
||||
</tr>
|
||||
</c:when>
|
||||
<c:otherwise>
|
||||
<c:forEach items="${itemList}" var="item" varStatus="status">
|
||||
<tr>
|
||||
<td>${status.count}</td>
|
||||
<td>${item.part_no}</td>
|
||||
<td class="text-left">${item.part_name}</td>
|
||||
<td>${item.serial_nos}</td>
|
||||
<td>${item.due_date}</td>
|
||||
<td class="text-left">${item.customer_request}</td>
|
||||
<td>${item.return_reason}</td>
|
||||
</tr>
|
||||
</c:forEach>
|
||||
</c:otherwise>
|
||||
</c:choose>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- 버튼 영역 -->
|
||||
<div class="btn-area">
|
||||
<input type="button" value="닫기" class="plm_btns" id="btnClose">
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -317,11 +317,11 @@ function fn_FileRegist(objId, docType, docTypeName){
|
||||
fn_centerPopup(popup_width, popup_height, url);
|
||||
}
|
||||
|
||||
//영업활동등록 상세
|
||||
//영업활동등록 상세 (뷰 전용 팝업)
|
||||
function fn_projectConceptDetail(objId){
|
||||
var popup_width = 1400;
|
||||
var popup_height = 560;
|
||||
var url = "/contractMgmt/estimateRegistFormPopup.do?objId="+objId;
|
||||
var popup_width = 900;
|
||||
var popup_height = 700;
|
||||
var url = "/contractMgmt/estimateViewPopup.do?objId="+objId;
|
||||
|
||||
fn_centerPopup(popup_width, popup_height, url);
|
||||
}
|
||||
|
||||
@@ -219,10 +219,15 @@
|
||||
html += '<td><input type="text" class="item-part-no" value="' + (item.PART_NO || '') + '" readonly style="background:#f5f5f5;" /></td>';
|
||||
html += '<td><input type="text" class="item-part-name" value="' + (item.PART_NAME || '') + '" readonly style="background:#f5f5f5;" /></td>';
|
||||
html += '<td><input type="text" class="item-serial-no" value="' + serialNoDisplay + '" readonly style="background:#f5f5f5;" title="' + serialNo + '" /></td>';
|
||||
// ORDER_QUANTITY가 없으면 QUANTITY 사용 (견적서에서 가져온 값)
|
||||
html += '<td><input type="text" class="item-quantity" value="' + (item.ORDER_QUANTITY || item.QUANTITY || '') + '" numberOnly required /></td>';
|
||||
// ORDER_UNIT_PRICE 수정 가능
|
||||
html += '<td><input type="text" class="item-unit-price" value="' + (item.ORDER_UNIT_PRICE || '') + '" numberOnly required /></td>';
|
||||
// ORDER_SUPPLY_PRICE 자동 계산
|
||||
html += '<td><input type="text" class="item-supply-price" value="' + (item.ORDER_SUPPLY_PRICE || '') + '" numberOnly readonly style="background:#f5f5f5;" /></td>';
|
||||
html += '<td><input type="text" class="item-vat" value="' + (item.ORDER_VAT || '') + '" numberOnly /></td>'; // readonly 제거
|
||||
// ORDER_VAT 수정 가능
|
||||
html += '<td><input type="text" class="item-vat" value="' + (item.ORDER_VAT || '') + '" numberOnly /></td>';
|
||||
// ORDER_TOTAL_AMOUNT 자동 계산
|
||||
html += '<td><input type="text" class="item-total-amount" value="' + (item.ORDER_TOTAL_AMOUNT || '') + '" numberOnly readonly style="background:#f5f5f5;" /></td>';
|
||||
html += '<td style="text-align:center;">-</td>'; // 삭제 불가
|
||||
html += '<input type="hidden" class="item-objid" value="' + (item.OBJID || '') + '" />';
|
||||
|
||||
@@ -12,7 +12,11 @@
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
<title><%=Constants.SYSTEM_NAME%></title>
|
||||
|
||||
<style type="text/css">
|
||||
#plmSearchZon {
|
||||
width: auto !important;
|
||||
}
|
||||
</style>
|
||||
<!-- //JSTL 변수선언 -->
|
||||
<c:set var="totalCount" value="${empty TOTAL_COUNT?0:TOTAL_COUNT}" />
|
||||
<c:set var="maxPage" value="${empty MAX_PAGE_SIZE?1:MAX_PAGE_SIZE}" />
|
||||
@@ -146,7 +150,7 @@ function fn_excel() {
|
||||
}
|
||||
function openOEMPopUp(objid){
|
||||
var popup_width = 900;
|
||||
var popup_height = 700;
|
||||
var popup_height = 750;
|
||||
var target = "openOEMPopUp";
|
||||
|
||||
fn_centerPopup(popup_width, popup_height, url, target);
|
||||
|
||||
@@ -257,21 +257,48 @@
|
||||
|
||||
// 사업자등록증 파일 삭제
|
||||
function deleteBusRegFile(fileObjId){
|
||||
if(!confirm("파일을 삭제하시겠습니까?")){
|
||||
return;
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: "/common/deleteFile.do",
|
||||
type: "POST",
|
||||
data: {objId: fileObjId},
|
||||
dataType: "json",
|
||||
success: function(data){
|
||||
Swal.fire("삭제되었습니다.");
|
||||
loadBusRegFile();
|
||||
},
|
||||
error: function(){
|
||||
Swal.fire("삭제 중 오류가 발생했습니다.");
|
||||
Swal.fire({
|
||||
title: '파일을 삭제하시겠습니까?',
|
||||
text: '삭제된 파일은 복구할 수 없습니다.',
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: '#d33',
|
||||
cancelButtonColor: '#3085d6',
|
||||
confirmButtonText: '삭제',
|
||||
cancelButtonText: '취소'
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
$.ajax({
|
||||
url: "/common/deleteFile.do",
|
||||
type: "POST",
|
||||
data: {objId: fileObjId},
|
||||
dataType: "json",
|
||||
success: function(data){
|
||||
if(data.success){
|
||||
Swal.fire({
|
||||
icon: 'success',
|
||||
title: '삭제되었습니다.',
|
||||
showConfirmButton: false,
|
||||
timer: 1500
|
||||
});
|
||||
loadBusRegFile();
|
||||
}else{
|
||||
Swal.fire({
|
||||
icon: 'error',
|
||||
title: '삭제 실패',
|
||||
text: data.message || '파일 삭제 중 오류가 발생했습니다.'
|
||||
});
|
||||
}
|
||||
},
|
||||
error: function(xhr, status, error){
|
||||
console.error("삭제 오류:", error);
|
||||
Swal.fire({
|
||||
icon: 'error',
|
||||
title: '삭제 실패',
|
||||
text: '파일 삭제 중 오류가 발생했습니다.'
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<%@ page import="com.pms.common.utils.*"%>
|
||||
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt"%>
|
||||
<%@ page import="java.util.*" %>
|
||||
<%@include file= "/init.jsp" %>
|
||||
<%@include file= "/init_new.jsp" %>
|
||||
<%
|
||||
PersonBean person = (PersonBean)session.getAttribute(Constants.PERSON_BEAN);
|
||||
String userId = CommonUtils.checkNull(person.getUserId());
|
||||
@@ -412,7 +412,7 @@ function fn_edit(){
|
||||
<input type="hidden" name="CONTRACT_OBJID" id="CONTRACT_OBJID" value="${resultMap.CONTRACT_OBJID}">
|
||||
<section>
|
||||
<div class="plm_menu_name" style="display:flex;">
|
||||
<h2>
|
||||
<h2 style="width: 100%;">
|
||||
<span>품목 상세</span>
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<%@ page import="com.pms.common.utils.*"%>
|
||||
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt"%>
|
||||
<%@ page import="java.util.*" %>
|
||||
<%@include file= "/init.jsp" %>
|
||||
<%@include file= "/init_new.jsp" %>
|
||||
<%
|
||||
PersonBean person = (PersonBean)session.getAttribute(Constants.PERSON_BEAN);
|
||||
String userId = CommonUtils.checkNull(person.getUserId());
|
||||
@@ -286,7 +286,7 @@ function fn_overlapPartMng(){
|
||||
<input type="hidden" name="CONTRACT_OBJID" id="CONTRACT_OBJID" value="${resultMap.CONTRACT_OBJID}">
|
||||
<section>
|
||||
<div class="plm_menu_name" style="display:flex;">
|
||||
<h2>
|
||||
<h2 style="width: 100%;">
|
||||
<span>
|
||||
<c:if test="${ 'changeDesign' ne param.ACTION_TYPE}">
|
||||
품목 등록
|
||||
|
||||
@@ -26,6 +26,48 @@ $(document).ready(function(){
|
||||
debug: false // 디버깅 모드 비활성화
|
||||
});
|
||||
|
||||
// Select2 선택 시 텍스트 저장
|
||||
$('#search_partNo').on('select2:select', function (e) {
|
||||
var data = e.params.data;
|
||||
$('#search_partNo_text').val(data.text);
|
||||
});
|
||||
|
||||
$('#search_partName').on('select2:select', function (e) {
|
||||
var data = e.params.data;
|
||||
$('#search_partName_text').val(data.text);
|
||||
});
|
||||
|
||||
// 이전 검색 조건 복원
|
||||
setTimeout(function(){
|
||||
var prevPartNoText = '${param.search_partNo_text}';
|
||||
var prevPartNameText = '${param.search_partName_text}';
|
||||
var prevPartNo = '${param.search_partNo}';
|
||||
var prevPartName = '${param.search_partName}';
|
||||
var prevPartObjId = '${param.search_partObjId}';
|
||||
|
||||
console.log("=== 검색 조건 복원 ===");
|
||||
console.log("prevPartNoText:", prevPartNoText);
|
||||
console.log("prevPartNameText:", prevPartNameText);
|
||||
console.log("prevPartNo:", prevPartNo);
|
||||
console.log("prevPartName:", prevPartName);
|
||||
console.log("prevPartObjId:", prevPartObjId);
|
||||
|
||||
// 품번과 품명은 같은 OBJID를 사용하므로, 둘 중 하나만 복원
|
||||
if(prevPartNoText && prevPartNo){
|
||||
// 품번이 선택되어 있었던 경우
|
||||
console.log("품번 복원:", prevPartNoText, prevPartNo);
|
||||
var newOption = new Option(prevPartNoText, prevPartNo, true, true);
|
||||
$('#search_partNo').append(newOption).trigger('change');
|
||||
$('#search_partObjId').val(prevPartNo);
|
||||
} else if(prevPartNameText && prevPartName){
|
||||
// 품명이 선택되어 있었던 경우
|
||||
console.log("품명 복원:", prevPartNameText, prevPartName);
|
||||
var newOption = new Option(prevPartNameText, prevPartName, true, true);
|
||||
$('#search_partName').append(newOption).trigger('change');
|
||||
$('#search_partObjId').val(prevPartName);
|
||||
}
|
||||
}, 500);
|
||||
|
||||
$("#mainGrid").jqGrid({
|
||||
height : 630,
|
||||
colNames : headerNames,
|
||||
@@ -82,12 +124,30 @@ $(document).ready(function(){
|
||||
|
||||
//정전개 조회
|
||||
$("#btnSearchAscending").click(function(){
|
||||
// 품번 또는 품명 필수 체크
|
||||
var partNo = $.trim($("#search_partNo").val());
|
||||
var partName = $.trim($("#search_partName").val());
|
||||
|
||||
if(!partNo && !partName){
|
||||
Swal.fire('품번 또는 품명을 선택해 주세요');
|
||||
return;
|
||||
}
|
||||
|
||||
$("#searchType").val("ascending");
|
||||
fn_search();
|
||||
});
|
||||
|
||||
//역전개 조회
|
||||
$("#btnSearchDescending").click(function(){
|
||||
// 품번 또는 품명 필수 체크
|
||||
var partNo = $.trim($("#search_partNo").val());
|
||||
var partName = $.trim($("#search_partName").val());
|
||||
|
||||
if(!partNo && !partName){
|
||||
Swal.fire('품번 또는 품명을 선택해 주세요');
|
||||
return;
|
||||
}
|
||||
|
||||
$("#searchType").val("descending");
|
||||
fn_search();
|
||||
});
|
||||
@@ -100,18 +160,43 @@ $(document).ready(function(){
|
||||
fn_excelExport($("#mainGrid"),"BOM_REPORT_정전개");
|
||||
});
|
||||
|
||||
var search_level = '${param.search_level}';
|
||||
$(".dataTr").each(function(i){
|
||||
var lev = $(this).attr("data-LEVEL");
|
||||
//if(lev == 1){ //1level만 활성화
|
||||
if(!fnc_isEmpty(search_level)){ //검색조건 LEVEL까지만 활성화
|
||||
if(Number(lev) <= Number(search_level)){ //검색조건 LEVEL까지만 활성화
|
||||
$(this).show();
|
||||
// 초기 로딩 시 레벨에 따라 표시 (검색 결과 기준)
|
||||
setTimeout(function(){
|
||||
var searchLevel = '${param.search_level}'; // 검색 조건의 레벨
|
||||
|
||||
// 최소 레벨 찾기
|
||||
var minLevel = 999;
|
||||
$(".dataTr").each(function(){
|
||||
var lev = parseInt($(this).attr("data-LEVEL"));
|
||||
if(lev < minLevel){
|
||||
minLevel = lev;
|
||||
}
|
||||
}else{
|
||||
$(this).show(); //전체 펼치기
|
||||
});
|
||||
|
||||
// 표시할 최대 레벨 결정
|
||||
var maxDisplayLevel = minLevel; // 기본값: 최소 레벨만
|
||||
if(searchLevel && searchLevel != ''){
|
||||
maxDisplayLevel = parseInt(searchLevel);
|
||||
}
|
||||
});
|
||||
|
||||
console.log("최소 레벨:", minLevel, "표시할 최대 레벨:", maxDisplayLevel);
|
||||
|
||||
// 지정된 레벨까지 표시
|
||||
$(".dataTr").each(function(){
|
||||
var lev = parseInt($(this).attr("data-LEVEL"));
|
||||
if(lev <= maxDisplayLevel){
|
||||
$(this).show();
|
||||
|
||||
// 표시된 항목 중 하위가 있고 아직 펼쳐지지 않은 항목은 Minus 아이콘으로 변경
|
||||
if(lev < maxDisplayLevel){
|
||||
var $img = $(this).find(".btnToggle");
|
||||
if($img.length > 0 && $img.attr("src").indexOf("Plus") > -1){
|
||||
$img.attr("src", "/images/btnMinus.png");
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}, 100); // DOM 로딩 대기
|
||||
/*
|
||||
//클릭시 하위정보를 토글한다.
|
||||
$(".dataTr").click(function(){
|
||||
@@ -707,6 +792,8 @@ function fn_excelExport(pGridObj,pFileName){
|
||||
<input type="hidden" name="search" id="search" value="Y">
|
||||
<input type="hidden" name="actionType" id="actionType" value="" />
|
||||
<input type="hidden" name="searchType" id="searchType" value="ascending" />
|
||||
<input type="hidden" name="search_partNo_text" id="search_partNo_text" value="${param.search_partNo_text}" />
|
||||
<input type="hidden" name="search_partName_text" id="search_partName_text" value="${param.search_partName_text}" />
|
||||
<div class="min_part_enroll">
|
||||
<div class="content-box">
|
||||
<div class=""> <!-- content-box-s -->
|
||||
@@ -766,16 +853,29 @@ function fn_excelExport(pGridObj,pFileName){
|
||||
<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=""/>
|
||||
<input type="hidden" name="search_partObjId" id="search_partObjId" value="${param.search_partObjId}"/>
|
||||
</td>
|
||||
<td class="align_r">
|
||||
<label for="" class="">품명</label>
|
||||
</td>
|
||||
<td colspan="3">
|
||||
<td>
|
||||
<select name="search_partName" id="search_partName" class="select2-part" style="width: 100%;">
|
||||
<option value="">품명 선택</option>
|
||||
</select>
|
||||
</td>
|
||||
<td class="align_r">
|
||||
<label for="" class="">표시 레벨</label>
|
||||
</td>
|
||||
<td>
|
||||
<select name="search_level" id="search_level" class="select2" style="width: 100%;">
|
||||
<option value="">선택</option>
|
||||
<option value="1" ${param.search_level eq '1'?'selected':''}>1레벨</option>
|
||||
<option value="2" ${param.search_level eq '2'?'selected':''}>2레벨</option>
|
||||
<option value="3" ${param.search_level eq '3'?'selected':''}>3레벨</option>
|
||||
<option value="4" ${param.search_level eq '4'?'selected':''}>4레벨</option>
|
||||
<option value="5" ${param.search_level eq '5'?'selected':''}>5레벨</option>
|
||||
</select>
|
||||
</td>
|
||||
|
||||
<!-- <td class="align_r">
|
||||
<label for="" class="">품번</label>
|
||||
@@ -847,10 +947,10 @@ function fn_excelExport(pGridObj,pFileName){
|
||||
<col width="90px" /> <!-- 표면처리 -->
|
||||
<col width="90px" /> <!-- 공급업체 -->
|
||||
<col width="80px" /> <!-- PART 타입 -->
|
||||
<col width="60px" /> <!-- REVISION -->
|
||||
<col width="70px" /> <!-- EO No -->
|
||||
<col width="70px" /> <!-- EO Date -->
|
||||
<col width="200px" /> <!-- REMARK -->
|
||||
<!-- <col width="60px" /> REVISION
|
||||
<col width="70px" /> EO No
|
||||
<col width="70px" /> EO Date -->
|
||||
<col width="200px" /> <!-- REMARK -->
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr class="plm_thead">
|
||||
@@ -877,11 +977,11 @@ function fn_excelExport(pGridObj,pFileName){
|
||||
<td>열처리경도</td>
|
||||
<td>열처리방법</td>
|
||||
<td>표면처리</td>
|
||||
<td>공급업체</td>
|
||||
<td>메이커</td>
|
||||
<td>범주 이름</td>
|
||||
<td>Revision</td>
|
||||
<!-- <td>Revision</td>
|
||||
<td>EO No</td>
|
||||
<td>EO Date</td>
|
||||
<td>EO Date</td> -->
|
||||
<td>비고</td>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -915,9 +1015,9 @@ function fn_excelExport(pGridObj,pFileName){
|
||||
<col width="90px" /> <!-- 표면처리 -->
|
||||
<col width="90px" /> <!-- 공급업체 -->
|
||||
<col width="80px" /> <!-- PART 타입 -->
|
||||
<col width="60px" /> <!-- REVISION -->
|
||||
<col width="70px" /> <!-- EO No -->
|
||||
<col width="70px" /> <!-- EO Date -->
|
||||
<!-- <col width="60px" /> REVISION
|
||||
<col width="70px" /> EO No
|
||||
<col width="70px" /> EO Date -->
|
||||
<col width="200px" /> <!-- REMARK -->
|
||||
</colgroup>
|
||||
<c:choose>
|
||||
@@ -931,22 +1031,17 @@ function fn_excelExport(pGridObj,pFileName){
|
||||
<tr class="dataTr" data-LEVEL="${item.LEVEL}" data-REAL_LEVEL="${item.LEV}" data-BOM_REPORT_OBJID="${item.BOM_REPORT_OBJID}" data-TOP_OBJID="${item.ROOT_OBJID}"
|
||||
data-SUB_TOP_OBJID="${item.SUB_ROOT_OBJID}" data-OBJID="${item.PART_OBJID}${status.index}" style="display:none;">
|
||||
<td>
|
||||
<c:if test="${item.LEAF eq '0'}">
|
||||
<c:choose>
|
||||
<c:when test="${empty param.search_level}">
|
||||
<img src="/images/btnMinus.png" width="13px" height="13px" class="btnToggle" style="${item.LEVEL eq '1' and item.LEAF eq '0'?'cursor:pointer;':''}" data-BOM_REPORT_OBJID="${item.BOM_REPORT_OBJID}"
|
||||
data-OBJID="${item.PART_OBJID}${status.index}" data-REAL_LEVEL="${item.LEV}" data-TOP_OBJID="${item.ROOT_OBJID}" data-SUB_TOP_OBJID="${item.SUB_ROOT_OBJID}">
|
||||
<c:choose>
|
||||
<c:when test="${item.LEAF eq '0'}">
|
||||
<!-- 하위 항목이 있음: Plus 아이콘 표시 -->
|
||||
<img src="/images/btnPlus.png" width="13px" height="13px" class="btnToggle" style="cursor:pointer;" data-BOM_REPORT_OBJID="${item.BOM_REPORT_OBJID}"
|
||||
data-OBJID="${item.PART_OBJID}${status.index}" data-REAL_LEVEL="${item.LEV}" data-TOP_OBJID="${item.ROOT_OBJID}" data-SUB_TOP_OBJID="${item.SUB_ROOT_OBJID}">
|
||||
</c:when>
|
||||
<c:when test="${param.search_level > item.LEV}">
|
||||
<img src="/images/btnMinus.png" width="13px" height="13px" class="btnToggle" style="${item.LEVEL eq '1' and item.LEAF eq '0'?'cursor:pointer;':''}" data-BOM_REPORT_OBJID="${item.BOM_REPORT_OBJID}"
|
||||
data-OBJID="${item.PART_OBJID}${status.index}" data-REAL_LEVEL="${item.LEV}" data-TOP_OBJID="${item.ROOT_OBJID}" data-SUB_TOP_OBJID="${item.SUB_ROOT_OBJID}">
|
||||
</c:when>
|
||||
<c:when test="${param.search_level <= item.LEV}">
|
||||
<img src="/images/btnPlus.png" width="13px" height="13px" class="btnToggle" style="${item.LEVEL eq '1' and item.LEAF eq '0'?'cursor:pointer;':''}" data-BOM_REPORT_OBJID="${item.BOM_REPORT_OBJID}"
|
||||
data-OBJID="${item.PART_OBJID}${status.index}" data-REAL_LEVEL="${item.LEV}" data-TOP_OBJID="${item.ROOT_OBJID}" data-SUB_TOP_OBJID="${item.SUB_ROOT_OBJID}">
|
||||
</c:when>
|
||||
</c:choose>
|
||||
</c:if>
|
||||
<c:otherwise>
|
||||
<!-- 하위 항목이 없음: - 표시 -->
|
||||
<span style="display:inline-block; width:13px; text-align:center; color:#999;">-</span>
|
||||
</c:otherwise>
|
||||
</c:choose>
|
||||
</td>
|
||||
<c:forEach var="i" begin="1" end="${item.MAX_LEVEL}">
|
||||
<c:if test="${item.LEVEL eq i}">
|
||||
@@ -971,9 +1066,9 @@ function fn_excelExport(pGridObj,pFileName){
|
||||
<td title="${item.SURFACE_TREATMENT}" class="align_l" style="text-align: left; padding-left: 5px;">${item.SURFACE_TREATMENT}</td><!-- 표면처리 -->
|
||||
<td title="${item.MAKER}" class="align_l" style="text-align: left; padding-left: 5px;">${item.MAKER}</td><!-- 공급업체 -->
|
||||
<td title="${item.PART_TYPE_TITLE}" class="align_c">${item.PART_TYPE_TITLE}</td><!-- PART_TYPE -->
|
||||
<td title="${item.REVISION}" class="align_c" style="text-align: left; padding-left: 5px;">${item.REVISION}</td><!-- REVISION -->
|
||||
<td title="${item.EO_NO}" class="align_c" style="text-align: left; padding-left: 5px;">${item.EO_NO}</td><!-- EO_NO -->
|
||||
<td title="${item.EO_DATE}" class="align_c" style="text-align: left; padding-left: 5px;">${item.EO_DATE}</td><!-- EO_DATE -->
|
||||
<!-- <td title="${item.REVISION}" class="align_c" style="text-align: left; padding-left: 5px;">${item.REVISION}</td> REVISION
|
||||
<td title="${item.EO_NO}" class="align_c" style="text-align: left; padding-left: 5px;">${item.EO_NO}</td> EO_NO
|
||||
<td title="${item.EO_DATE}" class="align_c" style="text-align: left; padding-left: 5px;">${item.EO_DATE}</td> EO_DATE -->
|
||||
<td title="${item.REMARK}" class="align_l">${item.REMARK}</td><!-- REMARK -->
|
||||
</tr>
|
||||
</c:forEach>
|
||||
|
||||
@@ -12,7 +12,11 @@
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
<title><%=Constants.SYSTEM_NAME%></title>
|
||||
|
||||
<style type="text/css">
|
||||
#plmSearchZon {
|
||||
width: auto !important;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
// Excel 버튼 이벤트 등록 함수
|
||||
function fn_bindExcelButton() {
|
||||
@@ -187,7 +191,8 @@ var columns = [
|
||||
// 요청된 열 순서: 프로젝트번호, 주문유형, 매출 마감, 발주일, 발주번호, 고객사, 제품구분, 품명, 수량, 단가, 공급가액, 부가세, 총액, 원화총액, 출고일, 국내/해외, 환종, 환율, S/N, 품번
|
||||
{headerHozAlign : 'center', hozAlign : 'center', width : '120', title : '프로젝트번호', field : 'PROJECT_NO', frozen : true},
|
||||
{headerHozAlign : 'center', hozAlign : 'center', width : '100', title : '주문유형', field : 'ORDER_TYPE'},
|
||||
{headerHozAlign : 'center', hozAlign : 'center', width : '120', title : '매출마감', field : 'SALES_STATUS'},
|
||||
//{headerHozAlign : 'center', hozAlign : 'center', width : '120', title : '매출마감', field : 'SALES_STATUS'},
|
||||
{headerHozAlign : 'center', hozAlign : 'center', width : '120', title : '매출마감', field : 'SALES_DEADLINE_DATE'},
|
||||
{headerHozAlign : 'center', hozAlign : 'center', width : '100', title : '발주일', field : 'ORDER_DATE'},
|
||||
{headerHozAlign : 'center', hozAlign : 'center', width : '120', title : '발주번호', field : 'PO_NO'},
|
||||
{headerHozAlign : 'center', hozAlign : 'left', width : '150', title : '고객사', field : 'CUSTOMER'},
|
||||
@@ -253,6 +258,7 @@ function fn_search(){
|
||||
// 그리드 초기화
|
||||
_tabulGrid = new Tabulator("#mainGrid", {
|
||||
layout: _tabul_layout_fitColumns,
|
||||
height: "650px", // 그리드 고정 높이 설정
|
||||
columns: columns,
|
||||
data: response.RESULTLIST || [],
|
||||
selectable: "highlight" // 다중 선택 가능하도록 설정
|
||||
|
||||
@@ -139,6 +139,18 @@
|
||||
<script type="text/javascript">
|
||||
// 새로운 테이블 구조에 맞게 컬럼 정의 수정
|
||||
var columns = [
|
||||
{
|
||||
formatter: "rowSelection",
|
||||
titleFormatter: "rowSelection",
|
||||
headerHozAlign: 'center',
|
||||
hozAlign: 'center',
|
||||
width: 50,
|
||||
frozen: true,
|
||||
headerSort: false,
|
||||
cellClick: function(e, cell){
|
||||
e.stopPropagation();
|
||||
}
|
||||
},
|
||||
{headerHozAlign : 'center', hozAlign : 'center', width : '120', title : '프로젝트번호', field : 'PROJECT_NO', frozen : true,
|
||||
formatter: fnc_createGridAnchorTag,
|
||||
cellClick: function(e, cell){
|
||||
|
||||
111
db/README.md
Normal file
111
db/README.md
Normal file
@@ -0,0 +1,111 @@
|
||||
# 데이터베이스 백업 시스템
|
||||
|
||||
## 개요
|
||||
PostgreSQL 데이터베이스를 자동으로 백업하는 시스템입니다.
|
||||
- 하루 2회 자동 백업 (오전 7:30, 오후 6:00)
|
||||
- 로컬 백업 및 FTP 원격 백업 지원
|
||||
- 7일 이상 된 백업 자동 삭제
|
||||
|
||||
## 파일 구성
|
||||
- `backup.py`: 백업 스크립트
|
||||
- `00-create-roles.sh`: 데이터베이스 역할 생성 스크립트
|
||||
- `dbexport.pgsql`: 데이터베이스 덤프 파일
|
||||
|
||||
## 설정 방법
|
||||
|
||||
### 1. 환경 변수 설정
|
||||
`.env.production` 파일에 다음 설정을 추가:
|
||||
|
||||
```bash
|
||||
# 필수 설정
|
||||
POSTGRES_HOST=wace-plm-db
|
||||
POSTGRES_DOCKER_PORT=5432
|
||||
POSTGRES_USER=postgres
|
||||
POSTGRES_PASSWORD=your_password
|
||||
POSTGRES_DB=waceplm
|
||||
LOCAL_BACKUP_PATH=/backups/local
|
||||
BACKUP_RETENTION_DAYS=7
|
||||
BACKUP_TIME_AM=07:30
|
||||
BACKUP_TIME_PM=18:00
|
||||
|
||||
# 선택 설정 (FTP 원격 백업)
|
||||
FTP_HOST=ftp.example.com
|
||||
FTP_USER=backup_user
|
||||
FTP_PASSWORD=ftp_password
|
||||
FTP_PATH=/backups
|
||||
FTP_PORT=2122
|
||||
```
|
||||
|
||||
### 2. Docker Compose 실행
|
||||
```bash
|
||||
# 백업 서비스 포함하여 실행
|
||||
docker-compose -f docker-compose.prod.yml up -d
|
||||
|
||||
# 백업 서비스만 재시작
|
||||
docker-compose -f docker-compose.prod.yml restart wace-plm-backup
|
||||
```
|
||||
|
||||
### 3. 백업 확인
|
||||
```bash
|
||||
# 백업 로그 확인
|
||||
docker logs -f wace-plm-backup
|
||||
|
||||
# 백업 파일 확인
|
||||
ls -lh /home/wace-plm/backups/
|
||||
```
|
||||
|
||||
## 수동 백업 실행
|
||||
|
||||
### 즉시 백업 실행
|
||||
```bash
|
||||
docker exec wace-plm-backup python -c "from backup import backup_databases; backup_databases()"
|
||||
```
|
||||
|
||||
### 백업 파일 복원
|
||||
```bash
|
||||
# 백업 파일 확인
|
||||
ls /home/wace-plm/backups/
|
||||
|
||||
# 복원 실행
|
||||
docker exec -i wace-plm-db psql -U postgres -d waceplm < /home/wace-plm/backups/waceplm_backup_YYYYMMDD_HHMMSS.sql
|
||||
```
|
||||
|
||||
## 백업 스케줄
|
||||
- **오전 백업**: 매일 07:30
|
||||
- **오후 백업**: 매일 18:00
|
||||
- **자동 삭제**: 7일 이상 된 백업 파일
|
||||
|
||||
## 트러블슈팅
|
||||
|
||||
### 백업이 실행되지 않는 경우
|
||||
```bash
|
||||
# 컨테이너 상태 확인
|
||||
docker ps | grep backup
|
||||
|
||||
# 로그 확인
|
||||
docker logs wace-plm-backup
|
||||
|
||||
# 환경 변수 확인
|
||||
docker exec wace-plm-backup env | grep POSTGRES
|
||||
```
|
||||
|
||||
### 디스크 공간 부족
|
||||
```bash
|
||||
# 백업 파일 크기 확인
|
||||
du -sh /home/wace-plm/backups/
|
||||
|
||||
# 오래된 백업 수동 삭제
|
||||
find /home/wace-plm/backups/ -name "*.sql" -mtime +7 -delete
|
||||
```
|
||||
|
||||
### FTP 업로드 실패
|
||||
- FTP 서버 접속 정보 확인
|
||||
- 방화벽 설정 확인
|
||||
- FTP 경로 권한 확인
|
||||
|
||||
## 주의사항
|
||||
1. 백업 파일은 민감한 정보를 포함하므로 접근 권한 관리 필요
|
||||
2. 충분한 디스크 공간 확보 필요 (데이터베이스 크기의 최소 10배 권장)
|
||||
3. FTP 비밀번호는 반드시 환경 변수로 관리
|
||||
4. 정기적으로 복원 테스트 수행 권장
|
||||
|
||||
463
db/backup.py
Normal file
463
db/backup.py
Normal file
@@ -0,0 +1,463 @@
|
||||
import os
|
||||
import schedule
|
||||
import time
|
||||
import subprocess
|
||||
from datetime import datetime, timedelta
|
||||
import logging
|
||||
import sys
|
||||
import ftplib # Use the built-in FTP library
|
||||
from io import BytesIO # Needed for reading file content for FTP upload
|
||||
|
||||
# --- Configuration (from environment variables) ---
|
||||
POSTGRES_HOST = os.getenv('POSTGRES_HOST', 'wace-plm-db')
|
||||
POSTGRES_PORT = os.getenv('POSTGRES_DOCKER_PORT', '5432')
|
||||
POSTGRES_USER = os.getenv('POSTGRES_USER')
|
||||
POSTGRES_PASSWORD = os.getenv('POSTGRES_PASSWORD')
|
||||
POSTGRES_DB = os.getenv('POSTGRES_DB')
|
||||
LOCAL_BACKUP_PATH = os.getenv('LOCAL_BACKUP_PATH', '/backups/local')
|
||||
|
||||
# FTP Configuration
|
||||
FTP_HOST = os.getenv('FTP_HOST')
|
||||
FTP_USER = os.getenv('FTP_USER')
|
||||
FTP_PASSWORD = os.getenv('FTP_PASSWORD')
|
||||
FTP_PATH = os.getenv('FTP_PATH', '/') # Default to root FTP directory if not specified
|
||||
FTP_PORT = int(os.getenv('FTP_PORT', 2122)) # Default FTP port is 21
|
||||
|
||||
BACKUP_RETENTION_DAYS = int(os.getenv('BACKUP_RETENTION_DAYS', 7))
|
||||
BACKUP_TIME_AM = os.getenv('BACKUP_TIME_AM', "07:30")
|
||||
BACKUP_TIME_PM = os.getenv('BACKUP_TIME_PM', "18:00")
|
||||
# --- End Configuration ---
|
||||
|
||||
# --- Logging Setup ---
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(levelname)s - %(message)s',
|
||||
stream=sys.stdout # Log to stdout to be captured by Docker logs
|
||||
)
|
||||
# --- End Logging Setup ---
|
||||
|
||||
def check_env_vars():
|
||||
"""Check if required environment variables are set."""
|
||||
required_vars = ['POSTGRES_USER', 'POSTGRES_PASSWORD', 'POSTGRES_DB']
|
||||
|
||||
# Add FTP vars only if host/user/password/path are provided
|
||||
if FTP_HOST and FTP_USER and FTP_PASSWORD and FTP_PATH:
|
||||
required_vars.extend(['FTP_HOST', 'FTP_USER', 'FTP_PASSWORD', 'FTP_PATH'])
|
||||
logging.info("FTP configuration found in environment variables.")
|
||||
else:
|
||||
logging.warning("FTP configuration not fully provided (HOST, USER, PASSWORD, PATH). Remote backups will be skipped.")
|
||||
|
||||
# Check database vars
|
||||
missing_vars = [var for var in required_vars if not os.getenv(var)]
|
||||
if missing_vars:
|
||||
logging.error(f"Missing required environment variables: {', '.join(missing_vars)}")
|
||||
sys.exit(1)
|
||||
|
||||
logging.info("All required environment variables checked.")
|
||||
|
||||
def create_backup_dirs():
|
||||
"""Create local backup directory if it doesn't exist."""
|
||||
try:
|
||||
os.makedirs(LOCAL_BACKUP_PATH, exist_ok=True)
|
||||
logging.info(f"Ensured local backup directory exists: {LOCAL_BACKUP_PATH}")
|
||||
except OSError as e:
|
||||
logging.error(f"Error creating local backup directory: {e}")
|
||||
sys.exit(1) # Stop if local backup isn't possible
|
||||
|
||||
# Note: We will try to create the remote FTP directory if it doesn't exist during upload/cleanup
|
||||
|
||||
def ensure_ftp_dir(ftp, path):
|
||||
"""Ensures the specified directory exists on the FTP server."""
|
||||
parts = path.strip('/').split('/')
|
||||
current_path = ''
|
||||
for part in parts:
|
||||
if not part: continue
|
||||
current_path += '/' + part
|
||||
try:
|
||||
ftp.cwd(current_path)
|
||||
except ftplib.error_perm as e:
|
||||
if str(e).startswith('550'): # 550: Directory not found or permission denied
|
||||
try:
|
||||
ftp.mkd(current_path)
|
||||
logging.info(f"Created remote FTP directory: {current_path}")
|
||||
ftp.cwd(current_path) # Go into the newly created dir
|
||||
except ftplib.error_perm as mkd_e:
|
||||
logging.error(f"Failed to create or access FTP directory {current_path}: {mkd_e}")
|
||||
raise # Re-raise the exception to signal failure
|
||||
else:
|
||||
logging.error(f"FTP error accessing {current_path}: {e}")
|
||||
raise
|
||||
# Ensure we are in the final target directory
|
||||
ftp.cwd(path)
|
||||
logging.info(f"Ensured remote FTP directory exists and CWD set to: {path}")
|
||||
|
||||
def perform_database_backup(db_config, backup_prefix):
|
||||
"""Performs PostgreSQL database backup for a specific database configuration."""
|
||||
timestamp = datetime.now().strftime('%Y-%m-%d_%H%M%S')
|
||||
backup_filename = f"{backup_prefix}_{timestamp}.sql" # SQL 파일로 변경 (.gz 제거)
|
||||
local_filepath = os.path.join(LOCAL_BACKUP_PATH, backup_filename)
|
||||
|
||||
logging.info(f"Starting backup for database '{db_config['db_name']}' with prefix '{backup_prefix}'...")
|
||||
logging.info(f"Local target: {local_filepath}")
|
||||
|
||||
# 1. Create local backup using pg_dump with plain SQL format (no compression)
|
||||
pg_dump_command = [
|
||||
'pg_dump',
|
||||
f'--host={db_config["host"]}',
|
||||
f'--port={db_config["port"]}',
|
||||
f'--username={db_config["user"]}',
|
||||
f'--dbname={db_config["db_name"]}',
|
||||
'--format=plain', # 일반 SQL 텍스트 형식 사용
|
||||
'--no-owner', # 소유자 정보 제외 (복원 시 유연성 향상)
|
||||
'--no-privileges', # 권한 정보 제외 (복원 시 유연성 향상)
|
||||
f'--file={local_filepath}'
|
||||
]
|
||||
env = os.environ.copy()
|
||||
env['PGPASSWORD'] = db_config['password']
|
||||
|
||||
try:
|
||||
process = subprocess.run(
|
||||
pg_dump_command,
|
||||
env=env,
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
logging.info(f"Successfully created local backup: {local_filepath}")
|
||||
logging.debug(f"pg_dump stdout: {process.stdout}")
|
||||
logging.debug(f"pg_dump stderr: {process.stderr}")
|
||||
|
||||
# 2. Upload to Remote via FTP if configured
|
||||
if FTP_HOST and FTP_USER and FTP_PASSWORD and FTP_PATH:
|
||||
remote_target_path = FTP_PATH.rstrip("/") + "/" + backup_filename
|
||||
# Ensure log path starts with / for clarity
|
||||
log_full_path = f"/{remote_target_path.lstrip('/')}"
|
||||
logging.info(f"Attempting to upload backup via FTP to: ftp://{FTP_HOST}:{FTP_PORT}{log_full_path}")
|
||||
ftp = None # Initialize ftp to None
|
||||
try:
|
||||
ftp = ftplib.FTP()
|
||||
ftp.connect(FTP_HOST, FTP_PORT, timeout=60) # Increased timeout to 60 seconds
|
||||
ftp.login(FTP_USER, FTP_PASSWORD)
|
||||
ftp.set_pasv(True) # Use passive mode, usually necessary
|
||||
|
||||
# --- Simplified directory change (mimicking lftp) ---
|
||||
try:
|
||||
if FTP_PATH: # Only change directory if FTP_PATH is set
|
||||
logging.info(f"Changing remote directory to: {FTP_PATH}")
|
||||
ftp.cwd(FTP_PATH)
|
||||
logging.info(f"Successfully changed remote directory to: {ftp.pwd()}") # Log current dir
|
||||
else:
|
||||
logging.info("FTP_PATH is not set, uploading to user's home directory.")
|
||||
|
||||
# Upload the file to the current directory
|
||||
logging.info(f"Attempting to upload {backup_filename} to current remote directory.")
|
||||
with open(local_filepath, 'rb') as local_file:
|
||||
ftp.storbinary(f'STOR {backup_filename}', local_file)
|
||||
logging.info(f"Successfully uploaded backup via FTP to remote path: {FTP_PATH}/{backup_filename}") # Adjust log message slightly
|
||||
|
||||
except ftplib.error_perm as ftp_cwd_err:
|
||||
logging.error(f"Failed to change FTP directory to '{FTP_PATH}': {ftp_cwd_err}")
|
||||
except ftplib.all_errors as ftp_err:
|
||||
logging.error(f"FTP operation failed during/after CWD or during STOR: {ftp_err}")
|
||||
# --- End Simplified directory change ---
|
||||
|
||||
except ftplib.all_errors as ftp_err:
|
||||
logging.error(f"FTP connection/login failed: {ftp_err}") # Adjusted error scope
|
||||
# Potentially retry or raise an error to indicate failure
|
||||
except FileNotFoundError:
|
||||
logging.error(f"Local backup file not found for FTP upload: {local_filepath}")
|
||||
except Exception as ftp_e:
|
||||
logging.error(f"An unexpected error occurred during FTP upload: {ftp_e}")
|
||||
finally:
|
||||
if ftp:
|
||||
try:
|
||||
ftp.quit()
|
||||
except ftplib.all_errors:
|
||||
logging.debug("FTP quit command failed, closing connection.")
|
||||
ftp.close() # Force close if quit fails
|
||||
|
||||
else:
|
||||
logging.warning("FTP configuration not provided. Skipping remote upload.")
|
||||
|
||||
except subprocess.CalledProcessError as e:
|
||||
logging.error(f"pg_dump failed with exit code {e.returncode} for database '{db_config['db_name']}'")
|
||||
logging.error(f"pg_dump stderr: {e.stderr}")
|
||||
if os.path.exists(local_filepath):
|
||||
try:
|
||||
os.remove(local_filepath)
|
||||
logging.info(f"Removed incomplete local backup file: {local_filepath}")
|
||||
except OSError as remove_err:
|
||||
logging.error(f"Error removing incomplete local backup file {local_filepath}: {remove_err}")
|
||||
except Exception as e:
|
||||
logging.error(f"An unexpected error occurred during backup for '{db_config['db_name']}': {e}")
|
||||
|
||||
def perform_backup():
|
||||
"""Performs the PostgreSQL database backup."""
|
||||
logging.info("=== Starting backup process ===")
|
||||
|
||||
# Database configuration
|
||||
db_config = {
|
||||
'host': POSTGRES_HOST,
|
||||
'port': POSTGRES_PORT,
|
||||
'user': POSTGRES_USER,
|
||||
'password': POSTGRES_PASSWORD,
|
||||
'db_name': POSTGRES_DB
|
||||
}
|
||||
|
||||
# Perform backup
|
||||
try:
|
||||
perform_database_backup(db_config, POSTGRES_DB)
|
||||
logging.info(f"Completed backup for database: {POSTGRES_DB}")
|
||||
except Exception as e:
|
||||
logging.error(f"Failed to backup database: {e}")
|
||||
|
||||
logging.info("=== Backup process completed ===")
|
||||
|
||||
# Legacy function kept for compatibility (now calls the new generic function)
|
||||
def perform_backup_legacy():
|
||||
"""Legacy backup function - kept for backward compatibility."""
|
||||
return perform_backup()
|
||||
|
||||
def cleanup_local_backups(backup_dir):
|
||||
"""Removes local backups older than BACKUP_RETENTION_DAYS."""
|
||||
if not os.path.isdir(backup_dir):
|
||||
logging.warning(f"Local cleanup skipped: Directory not found or inaccessible: {backup_dir}")
|
||||
return
|
||||
|
||||
logging.info(f"Starting cleanup of old local backups in: {backup_dir}")
|
||||
cutoff_date = datetime.now() - timedelta(days=BACKUP_RETENTION_DAYS)
|
||||
files_deleted = 0
|
||||
files_checked = 0
|
||||
|
||||
try:
|
||||
for filename in os.listdir(backup_dir):
|
||||
# Match the filename pattern
|
||||
is_db_backup = filename.startswith(f"{POSTGRES_DB}_") and filename.endswith(".sql")
|
||||
|
||||
if is_db_backup:
|
||||
files_checked += 1
|
||||
filepath = os.path.join(backup_dir, filename)
|
||||
try:
|
||||
# Use file modification time for age check
|
||||
file_mod_time_ts = os.path.getmtime(filepath)
|
||||
file_mod_time = datetime.fromtimestamp(file_mod_time_ts)
|
||||
|
||||
if file_mod_time < cutoff_date:
|
||||
os.remove(filepath)
|
||||
logging.info(f"Deleted old local backup: {filepath} (modified: {file_mod_time})")
|
||||
files_deleted += 1
|
||||
except OSError as e:
|
||||
logging.error(f"Error processing or deleting local file {filepath}: {e}")
|
||||
except ValueError: # Should not happen with getmtime
|
||||
logging.warning(f"Could not get modification time for local file: {filename}. Skipping.")
|
||||
|
||||
logging.info(f"Local cleanup finished for {backup_dir}. Checked: {files_checked}, Deleted: {files_deleted}.")
|
||||
except OSError as e:
|
||||
logging.error(f"Error listing directory {backup_dir} during local cleanup: {e}")
|
||||
|
||||
def parse_mlsd_time(timestr):
|
||||
"""Parses the timestamp from MLSD command output (YYYYMMDDHHMMSS)."""
|
||||
try:
|
||||
return datetime.strptime(timestr, '%Y%m%d%H%M%S')
|
||||
except ValueError:
|
||||
logging.warning(f"Could not parse MLSD time string: {timestr}")
|
||||
return None
|
||||
|
||||
def cleanup_remote_backups():
|
||||
"""Removes remote backups older than BACKUP_RETENTION_DAYS using FTP."""
|
||||
if not (FTP_HOST and FTP_USER and FTP_PASSWORD and FTP_PATH):
|
||||
logging.warning("FTP configuration not provided. Skipping remote cleanup.")
|
||||
return
|
||||
|
||||
remote_dir = FTP_PATH.rstrip("/")
|
||||
# Correct the logging message to avoid double slash if FTP_PATH starts with /
|
||||
log_path = f"/{remote_dir.lstrip('/')}" if remote_dir else "/"
|
||||
logging.info(f"Starting cleanup of old remote backups in: ftp://{FTP_HOST}:{FTP_PORT}{log_path}")
|
||||
cutoff_date = datetime.now() - timedelta(days=BACKUP_RETENTION_DAYS)
|
||||
files_deleted = 0
|
||||
files_checked = 0
|
||||
ftp = None
|
||||
|
||||
try:
|
||||
ftp = ftplib.FTP()
|
||||
ftp.connect(FTP_HOST, FTP_PORT, timeout=60) # Increased timeout to 60 seconds
|
||||
ftp.login(FTP_USER, FTP_PASSWORD)
|
||||
ftp.set_pasv(True)
|
||||
|
||||
# --- Simplified directory change (similar to upload) ---
|
||||
try:
|
||||
if remote_dir: # Only change directory if remote_dir (derived from FTP_PATH) is set
|
||||
logging.info(f"Changing remote directory for cleanup to: {remote_dir}")
|
||||
ftp.cwd(remote_dir)
|
||||
logging.info(f"Successfully changed remote directory for cleanup to: {ftp.pwd()}") # Log current dir
|
||||
else:
|
||||
logging.info("FTP_PATH is not set, performing cleanup in user's home directory.")
|
||||
|
||||
# --- Proceed with listing and deletion in the CURRENT directory ---
|
||||
# Use MLSD for modern servers, fallback needed if not supported
|
||||
try:
|
||||
lines = []
|
||||
ftp.retrlines('MLSD', lines.append)
|
||||
logging.debug(f"MLSD output for current directory ({ftp.pwd()}):\n" + "\n".join(lines))
|
||||
|
||||
for line in lines:
|
||||
parts = line.split(';')
|
||||
facts = {}
|
||||
for part in parts:
|
||||
if '=' in part:
|
||||
key, value = part.split('=', 1)
|
||||
facts[key.strip().lower()] = value.strip()
|
||||
|
||||
filename = facts.get('') # Filename is the part without key=value
|
||||
filetype = facts.get('type')
|
||||
modify_time_str = facts.get('modify')
|
||||
|
||||
# Process files matching database pattern
|
||||
is_db_backup = filename and filename.startswith(f"{POSTGRES_DB}_") and filename.endswith(".sql")
|
||||
|
||||
if filetype == 'file' and is_db_backup:
|
||||
files_checked += 1
|
||||
if modify_time_str:
|
||||
file_mod_time = parse_mlsd_time(modify_time_str)
|
||||
if file_mod_time and file_mod_time < cutoff_date:
|
||||
try:
|
||||
ftp.delete(filename)
|
||||
logging.info(f"Deleted old remote backup: {filename} (modified: {file_mod_time})")
|
||||
files_deleted += 1
|
||||
except ftplib.error_perm as del_err:
|
||||
logging.error(f"Failed to delete remote file {filename}: {del_err}")
|
||||
elif not file_mod_time:
|
||||
logging.warning(f"Skipping remote file due to unparseable time: {filename}")
|
||||
else:
|
||||
logging.warning(f"Could not get modification time for remote file: {filename}. Skipping deletion check.")
|
||||
|
||||
logging.info(f"Remote cleanup finished using MLSD for {remote_dir}. Checked: {files_checked}, Deleted: {files_deleted}.")
|
||||
|
||||
except ftplib.error_perm as mlsd_err:
|
||||
logging.warning(f"MLSD command failed (server might not support it): {mlsd_err}. Falling back to LIST/MDTM (less reliable).")
|
||||
# Fallback to LIST and MDTM (less efficient and parsing can be fragile)
|
||||
files_deleted = 0 # Reset counter for fallback
|
||||
files_checked = 0
|
||||
try:
|
||||
filenames = ftp.nlst()
|
||||
logging.debug(f"NLST output for {remote_dir}: {filenames}")
|
||||
for filename in filenames:
|
||||
# Check for database backup pattern
|
||||
is_db_backup = filename.startswith(f"{POSTGRES_DB}_") and filename.endswith(".sql")
|
||||
|
||||
if is_db_backup:
|
||||
files_checked += 1
|
||||
try:
|
||||
# Attempt to get modification time
|
||||
mdtm_str = ftp.voidcmd(f"MDTM {filename}")
|
||||
# Response format is usually "213 YYYYMMDDHHMMSS"
|
||||
if mdtm_str.startswith("213 "):
|
||||
file_mod_time = parse_mlsd_time(mdtm_str[4:].strip())
|
||||
if file_mod_time and file_mod_time < cutoff_date:
|
||||
try:
|
||||
ftp.delete(filename)
|
||||
logging.info(f"Deleted old remote backup (fallback): {filename} (modified: {file_mod_time})")
|
||||
files_deleted += 1
|
||||
except ftplib.error_perm as del_err_fb:
|
||||
logging.error(f"Failed to delete remote file {filename} (fallback): {del_err_fb}")
|
||||
elif not file_mod_time:
|
||||
logging.warning(f"Skipping remote file (fallback) due to unparseable time: {filename}")
|
||||
else:
|
||||
logging.warning(f"Could not get MDTM for remote file {filename}: {mdtm_str}. Skipping deletion check.")
|
||||
except ftplib.error_perm as mdtm_err:
|
||||
logging.warning(f"MDTM command failed for {filename}: {mdtm_err}. Skipping deletion check.")
|
||||
except Exception as fb_err:
|
||||
logging.warning(f"Error processing file {filename} in fallback: {fb_err}. Skipping.")
|
||||
logging.info(f"Remote cleanup finished using LIST/MDTM fallback for {remote_dir}. Checked: {files_checked}, Deleted: {files_deleted}.")
|
||||
except ftplib.error_perm as list_err:
|
||||
logging.error(f"Failed to list files using NLST in fallback: {list_err}")
|
||||
except Exception as fallback_list_err:
|
||||
logging.error(f"An unexpected error occurred during FTP fallback cleanup: {fallback_list_err}")
|
||||
|
||||
except ftplib.error_perm as ftp_cwd_err:
|
||||
logging.error(f"Failed to change FTP directory for cleanup to '{remote_dir}': {ftp_cwd_err}")
|
||||
# If we can't change directory, we can't clean it.
|
||||
return # Exit cleanup function
|
||||
except ftplib.all_errors as ftp_err:
|
||||
logging.error(f"FTP connection/login failed during cleanup: {ftp_err}")
|
||||
return # Exit cleanup function
|
||||
# --- End Simplified directory change ---
|
||||
|
||||
except ftplib.all_errors as ftp_err:
|
||||
logging.error(f"FTP cleanup failed: {ftp_err}")
|
||||
except Exception as ftp_clean_e:
|
||||
logging.error(f"An unexpected error occurred during FTP cleanup: {ftp_clean_e}")
|
||||
finally:
|
||||
if ftp:
|
||||
try:
|
||||
ftp.quit()
|
||||
except ftplib.all_errors:
|
||||
logging.debug("FTP quit command failed during cleanup, closing connection.")
|
||||
ftp.close()
|
||||
|
||||
|
||||
def run_cleanup():
|
||||
"""Runs cleanup for both local and remote directories."""
|
||||
logging.info("Running scheduled cleanup job.")
|
||||
cleanup_local_backups(LOCAL_BACKUP_PATH)
|
||||
cleanup_remote_backups()
|
||||
|
||||
def run_backup_job():
|
||||
"""Runs the backup job."""
|
||||
logging.info("Running scheduled backup job.")
|
||||
perform_backup()
|
||||
# Cleanup is handled by a separate schedule
|
||||
|
||||
if __name__ == "__main__":
|
||||
check_env_vars()
|
||||
create_backup_dirs()
|
||||
|
||||
logging.info("Backup script starting.")
|
||||
logging.info(f"Scheduling backups for {BACKUP_TIME_AM} and {BACKUP_TIME_PM} KST (Asia/Seoul).")
|
||||
logging.info(f"Backup retention: {BACKUP_RETENTION_DAYS} days.")
|
||||
logging.info(f"Local backup path: {LOCAL_BACKUP_PATH}")
|
||||
|
||||
# Log database configuration
|
||||
logging.info(f"Database: {POSTGRES_DB} at {POSTGRES_HOST}:{POSTGRES_PORT}")
|
||||
|
||||
if FTP_HOST and FTP_USER and FTP_PASSWORD and FTP_PATH:
|
||||
# Ensure log path starts with / for clarity
|
||||
log_ftp_path = f"/{FTP_PATH.lstrip('/')}"
|
||||
logging.info(f"FTP Target: ftp://{FTP_USER}:****@{FTP_HOST}:{FTP_PORT}{log_ftp_path}")
|
||||
else:
|
||||
logging.info("FTP Target: Not configured.")
|
||||
|
||||
# --- Initial Run ---
|
||||
logging.info("--- Performing initial backup and cleanup run on startup ---")
|
||||
try:
|
||||
run_backup_job() # Perform the backup job first
|
||||
run_cleanup() # Then run cleanup
|
||||
logging.info("--- Initial run complete. Proceeding to scheduled runs. ---")
|
||||
except Exception as initial_run_error:
|
||||
logging.error(f"Error during initial backup/cleanup run: {initial_run_error}")
|
||||
# Log the error and continue to the scheduler.
|
||||
# --- End Initial Run ---
|
||||
|
||||
# --- Scheduling ---
|
||||
schedule.every().day.at(BACKUP_TIME_AM, "Asia/Seoul").do(run_backup_job)
|
||||
schedule.every().day.at(BACKUP_TIME_PM, "Asia/Seoul").do(run_backup_job)
|
||||
|
||||
# Schedule cleanup (e.g., once daily, shortly after the first backup)
|
||||
# Ensure the time parsing and addition handles potential day rollovers if needed,
|
||||
# but adding 15 minutes should be safe.
|
||||
try:
|
||||
cleanup_dt = datetime.strptime(BACKUP_TIME_AM, "%H:%M") + timedelta(minutes=15)
|
||||
cleanup_time = cleanup_dt.strftime("%H:%M")
|
||||
logging.info(f"Scheduling daily cleanup job for {cleanup_time} KST (Asia/Seoul).")
|
||||
schedule.every().day.at(cleanup_time, "Asia/Seoul").do(run_cleanup)
|
||||
except ValueError:
|
||||
logging.error(f"Invalid BACKUP_TIME_AM format: {BACKUP_TIME_AM}. Cannot schedule cleanup accurately.")
|
||||
# Fallback: Schedule cleanup at a fixed time like 08:00
|
||||
logging.warning("Scheduling cleanup for 08:00 KST as fallback.")
|
||||
schedule.every().day.at("08:00", "Asia/Seoul").do(run_cleanup)
|
||||
# --- End Scheduling ---
|
||||
|
||||
logging.info("Scheduler started. Waiting for scheduled jobs...")
|
||||
while True:
|
||||
schedule.run_pending()
|
||||
time.sleep(60) # Check every 60 seconds
|
||||
@@ -46,6 +46,20 @@ services:
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
wace-plm-backup:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.backup
|
||||
container_name: wace-plm-backup
|
||||
restart: always
|
||||
env_file:
|
||||
- .env.production
|
||||
depends_on:
|
||||
wace-plm-db:
|
||||
condition: service_healthy
|
||||
volumes:
|
||||
- /home/wace-plm/backups:/backups/local
|
||||
|
||||
networks:
|
||||
default:
|
||||
external:
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
# PLM ILSHIN 운영환경 설정
|
||||
# WACE PLM 운영환경 설정
|
||||
|
||||
# 애플리케이션 환경
|
||||
NODE_ENV=production
|
||||
|
||||
# 데이터베이스 설정
|
||||
DB_URL=jdbc:postgresql://localhost:5432/ilshin
|
||||
DB_URL=jdbc:postgresql://wace-plm-db:5432/waceplm
|
||||
DB_USERNAME=postgres
|
||||
DB_PASSWORD=your_production_password
|
||||
|
||||
# PostgreSQL 환경 변수
|
||||
POSTGRES_DB=ilshin
|
||||
POSTGRES_DB=waceplm
|
||||
POSTGRES_USER=postgres
|
||||
POSTGRES_PASSWORD=your_production_password
|
||||
|
||||
@@ -26,5 +26,19 @@ DEBUG=false
|
||||
SSL_ENABLED=true
|
||||
|
||||
# 도메인 설정
|
||||
DOMAIN=ilshin.esgrin.com
|
||||
ALT_DOMAIN=autoclave.co.kr
|
||||
DOMAIN=waceplm.esgrin.com
|
||||
|
||||
# 백업 설정
|
||||
POSTGRES_HOST=wace-plm-db
|
||||
POSTGRES_DOCKER_PORT=5432
|
||||
LOCAL_BACKUP_PATH=/backups/local
|
||||
BACKUP_RETENTION_DAYS=7
|
||||
BACKUP_TIME_AM=07:30
|
||||
BACKUP_TIME_PM=18:00
|
||||
|
||||
# FTP 백업 설정 (NAS Backup SMB Configuration)
|
||||
FTP_HOST=effectsno1.synology.me
|
||||
FTP_USER=esgrin-mes-backup
|
||||
FTP_PASSWORD=UyD12#11YHnn
|
||||
FTP_PATH=esgrin-mes-backup
|
||||
FTP_PORT=2112
|
||||
@@ -783,6 +783,21 @@ public class MailUtil {
|
||||
, ArrayList<HashMap> attachFileList
|
||||
, String mailType
|
||||
) {
|
||||
// 기본적으로 실제 제목을 mail_log에도 사용
|
||||
return sendMailWithAttachFileUTF8(fromUserId, fromEmail, toUserIdList, toEmailList, ccEmailList, bccEmailList,
|
||||
important, subject, contents, attachFileList, mailType, subject);
|
||||
}
|
||||
|
||||
/**
|
||||
* 메일 발송 (UTF-8, 파일 첨부 가능) - mail_log용 제목 별도 지정 가능
|
||||
*/
|
||||
public static boolean sendMailWithAttachFileUTF8(String fromUserId, String fromEmail
|
||||
, ArrayList<String> toUserIdList, ArrayList<String> toEmailList, ArrayList<String> ccEmailList, ArrayList<String> bccEmailList
|
||||
, String important, String subject, String contents
|
||||
, ArrayList<HashMap> attachFileList
|
||||
, String mailType
|
||||
, String subjectForLog // mail_log에 저장될 제목 (OBJID 포함)
|
||||
) {
|
||||
|
||||
if(Constants.Mail.sendMailSwitch == false){
|
||||
System.out.println("MailUtil.sendMailWithAttachFileUTF8 ::: Constants.Mail.sendMailSwitch is FALSE");
|
||||
@@ -798,6 +813,11 @@ public class MailUtil {
|
||||
contents = CommonUtils.checkNull(contents, "empty contents");
|
||||
mailType = CommonUtils.checkNull(mailType, "empty mailType");
|
||||
|
||||
// mail_log용 제목이 없으면 실제 제목 사용
|
||||
if(subjectForLog == null || "".equals(subjectForLog.trim())) {
|
||||
subjectForLog = subject;
|
||||
}
|
||||
|
||||
System.out.println("MailUtil.sendMailWithAttachFileUTF8()..");
|
||||
System.out.println("MailUtil.sendMailWithAttachFileUTF8(fromUserId ):"+fromUserId);
|
||||
System.out.println("MailUtil.sendMailWithAttachFileUTF8(fromEmail ):"+fromEmail);
|
||||
@@ -896,18 +916,65 @@ public class MailUtil {
|
||||
message.setContent(multipart);
|
||||
message.setSentDate(new Date());
|
||||
|
||||
// 메일 발송
|
||||
Transport.send(message);
|
||||
System.out.println("메일 발송 성공 (UTF-8)");
|
||||
//◆◆◆ 3. db log & send mail ◆◆◆
|
||||
SqlSession sqlSession = null;
|
||||
boolean mailSendSuccess = false;
|
||||
|
||||
return true;
|
||||
Map paramMap = new HashMap();
|
||||
try{
|
||||
if(Constants.Mail.dbLogWrite){
|
||||
sqlSession = SqlMapConfig.getInstance().getSqlSession();
|
||||
|
||||
String objId = CommonUtils.createObjId();
|
||||
paramMap.put("objId" , objId);
|
||||
paramMap.put("systemName" , Constants.SYSTEM_NAME);
|
||||
paramMap.put("sendUserId" , fromUserId);
|
||||
paramMap.put("fromAddr" , fromEmail);
|
||||
paramMap.put("receptionUserId", toUserIdList.toString());
|
||||
paramMap.put("receiverTo" , toEmailList.toString());
|
||||
paramMap.put("title" , subjectForLog); // mail_log용 제목 (OBJID 포함)
|
||||
paramMap.put("contents" , contents);
|
||||
paramMap.put("mailType" , mailType);
|
||||
|
||||
sqlSession.insert("mail.insertMailLog", paramMap);
|
||||
System.out.println("메일 발송 로그 기록 (UTF-8) ==============================================");
|
||||
System.out.println("paramMap >>> "+paramMap);
|
||||
}
|
||||
|
||||
//◆◆◆ send mail ◆◆◆
|
||||
Transport.send(message);
|
||||
mailSendSuccess = true;
|
||||
System.out.println("메일 발송 성공 (UTF-8)");
|
||||
|
||||
} catch (Exception e) {
|
||||
System.out.println("메일 발송 실패 (UTF-8): " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
if(Constants.Mail.dbLogWrite){
|
||||
System.out.println("메일 발송후 paramMap >> "+paramMap);
|
||||
sqlSession.update("mail.updateMailSendedSuccess", paramMap);
|
||||
}
|
||||
|
||||
}catch(Exception sqle){
|
||||
System.out.println("메일 발송 실패 (UTF-8): " + sqle.getMessage());
|
||||
sqle.printStackTrace();
|
||||
|
||||
if(Constants.Mail.dbLogWrite){
|
||||
try{
|
||||
sqlSession.update("mail.updateMailSendedFail", paramMap);
|
||||
}catch(Exception e2){
|
||||
e2.printStackTrace();
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}finally{
|
||||
if(sqlSession != null) sqlSession.close();
|
||||
}
|
||||
|
||||
return mailSendSuccess;
|
||||
|
||||
} catch (Exception e) {
|
||||
System.out.println("메일 발송 실패 (UTF-8): " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 첨부파일 목록을 파일하나로 압축해서 반환
|
||||
|
||||
@@ -168,6 +168,28 @@ public class CommonController {
|
||||
return "/ajax/ajaxResult";
|
||||
}
|
||||
|
||||
/**
|
||||
* 파일을 삭제한다. (논리적삭제, status : Deleted) - JSON 응답
|
||||
* @param request
|
||||
* @param paramMap
|
||||
* @return
|
||||
*/
|
||||
@RequestMapping("/common/deleteFile.do")
|
||||
@ResponseBody
|
||||
public Map<String, Object> deleteFile(HttpServletRequest request, @RequestParam Map<String, Object> paramMap){
|
||||
Map<String, Object> result = new HashMap<String, Object>();
|
||||
try {
|
||||
String msg = commonService.deleteFileInfoLogical(paramMap);
|
||||
result.put("success", true);
|
||||
result.put("message", msg);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
result.put("success", false);
|
||||
result.put("message", "파일 삭제 중 오류가 발생했습니다.");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 파일 다운로드
|
||||
* @param request
|
||||
|
||||
@@ -3205,4 +3205,32 @@ SELECT option_objid::VARCHAR AS CODE
|
||||
WHERE USER_ID = #{userId}
|
||||
AND MASTER_OBJID::varchar = #{masterObjid}
|
||||
</select>
|
||||
|
||||
<!-- 고객사 정보 조회 -->
|
||||
<select id="getSupplyInfo" parameterType="map" resultType="map">
|
||||
SELECT
|
||||
OBJID,
|
||||
SUPPLY_CODE,
|
||||
SUPPLY_NAME,
|
||||
REG_NO,
|
||||
SUPPLY_ADDRESS,
|
||||
SUPPLY_BUSNAME,
|
||||
SUPPLY_STOCKNAME,
|
||||
SUPPLY_TEL_NO,
|
||||
SUPPLY_FAX_NO,
|
||||
CHARGE_USER_NAME,
|
||||
PAYMENT_METHOD,
|
||||
MANAGER1_NAME,
|
||||
MANAGER1_EMAIL,
|
||||
MANAGER2_NAME,
|
||||
MANAGER2_EMAIL,
|
||||
MANAGER3_NAME,
|
||||
MANAGER3_EMAIL,
|
||||
MANAGER4_NAME,
|
||||
MANAGER4_EMAIL,
|
||||
MANAGER5_NAME,
|
||||
MANAGER5_EMAIL
|
||||
FROM SUPPLY_MNG
|
||||
WHERE OBJID = #{objId}::numeric
|
||||
</select>
|
||||
</mapper>
|
||||
@@ -6038,7 +6038,7 @@ SELECT T1.LEV, T1.BOM_REPORT_OBJID, T1.ROOT_PART_NO, T1.PATH, T1.LEAF, T2.*
|
||||
BT.ITEM_QTY,
|
||||
BT.QTY AS QTY_TEMP,
|
||||
BT.SEQ,
|
||||
BT.LEAF,
|
||||
-- BT.LEAF, -- 재귀 CTE의 LEAF는 항상 0이므로 제거 (아래에서 재계산)
|
||||
-- PART 정보
|
||||
PM.OBJID AS PART_OBJID,
|
||||
PM.PART_NO,
|
||||
@@ -6424,12 +6424,21 @@ SELECT T1.LEV, T1.BOM_REPORT_OBJID, T1.ROOT_PART_NO, T1.PATH, T1.LEAF, T2.*
|
||||
)
|
||||
SELECT
|
||||
BT.BOM_REPORT_OBJID,
|
||||
BT.LEV,
|
||||
BT.LEV AS LEVEL,
|
||||
-- 역전개는 레벨을 거꾸로 표시 (최상위가 1레벨)
|
||||
(ML.MAX_LEVEL - BT.LEV + 1) AS LEV,
|
||||
(ML.MAX_LEVEL - BT.LEV + 1) AS LEVEL,
|
||||
BT.QTY,
|
||||
BT.ITEM_QTY,
|
||||
BT.QTY AS P_QTY,
|
||||
BT.SEQ,
|
||||
-- LEAF 계산 (역전개: 하위 항목이 있는지 체크 - 정전개와 동일)
|
||||
(
|
||||
SELECT CASE WHEN COUNT(*) > 0 THEN 0 ELSE 1 END
|
||||
FROM BOM_PART_QTY BPQ2
|
||||
WHERE BPQ2.PARENT_OBJID = BT.CHILD_OBJID
|
||||
AND BPQ2.BOM_REPORT_OBJID = BT.BOM_REPORT_OBJID
|
||||
AND COALESCE(BPQ2.STATUS, '') NOT IN ('deleting', 'deleted')
|
||||
) AS LEAF,
|
||||
-- PART 정보
|
||||
PM.OBJID AS PART_OBJID,
|
||||
PM.PART_NO,
|
||||
@@ -6472,7 +6481,7 @@ SELECT T1.LEV, T1.BOM_REPORT_OBJID, T1.ROOT_PART_NO, T1.PATH, T1.LEAF, T2.*
|
||||
|
||||
ORDER BY
|
||||
PBR.REGDATE DESC,
|
||||
BT.LEV,
|
||||
(ML.MAX_LEVEL - BT.LEV + 1), -- 역전개: 레벨 역순
|
||||
BT.PATH
|
||||
</select>
|
||||
|
||||
|
||||
@@ -7627,29 +7627,65 @@ SELECT
|
||||
<!-- //영업정보 수정시 프로젝트 정보 업데이트 -->
|
||||
<update id="ModifyProjectByContract" parameterType="map">
|
||||
UPDATE PROJECT_MGMT
|
||||
SET
|
||||
DUE_DATE = #{due_date}
|
||||
,CUSTOMER_PROJECT_NAME = #{customer_project_name}
|
||||
,LOCATION = #{location}
|
||||
,SETUP = #{setup}
|
||||
,FACILITY = #{facility}
|
||||
,FACILITY_TYPE = #{facility_type}
|
||||
,FACILITY_DEPTH = #{facility_depth}
|
||||
,CONTRACT_DATE = #{contract_date}
|
||||
,PO_NO = #{po_no}
|
||||
,PM_USER_ID = #{pm_user_id}
|
||||
,CONTRACT_CURRENCY = #{contract_currency}
|
||||
,CONTRACT_PRICE_CURRENCY = #{contract_price_currency}
|
||||
,CONTRACT_PRICE = #{contract_price}
|
||||
,PROJECT_NAME = #{project_name}
|
||||
,CONTRACT_DEL_DATE = #{contract_del_date}
|
||||
,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}
|
||||
<set>
|
||||
<if test="due_date != null and due_date != ''">
|
||||
DUE_DATE = #{due_date},
|
||||
</if>
|
||||
<if test="customer_project_name != null and customer_project_name != ''">
|
||||
CUSTOMER_PROJECT_NAME = #{customer_project_name},
|
||||
</if>
|
||||
<if test="location != null and location != ''">
|
||||
LOCATION = #{location},
|
||||
</if>
|
||||
<if test="setup != null and setup != ''">
|
||||
SETUP = #{setup},
|
||||
</if>
|
||||
<if test="facility != null and facility != ''">
|
||||
FACILITY = #{facility},
|
||||
</if>
|
||||
<if test="facility_type != null and facility_type != ''">
|
||||
FACILITY_TYPE = #{facility_type},
|
||||
</if>
|
||||
<if test="facility_depth != null and facility_depth != ''">
|
||||
FACILITY_DEPTH = #{facility_depth},
|
||||
</if>
|
||||
<if test="contract_date != null and contract_date != ''">
|
||||
CONTRACT_DATE = #{contract_date},
|
||||
</if>
|
||||
<if test="po_no != null and po_no != ''">
|
||||
PO_NO = #{po_no},
|
||||
</if>
|
||||
<if test="pm_user_id != null and pm_user_id != ''">
|
||||
PM_USER_ID = #{pm_user_id},
|
||||
</if>
|
||||
<if test="contract_currency != null and contract_currency != ''">
|
||||
CONTRACT_CURRENCY = #{contract_currency},
|
||||
</if>
|
||||
<if test="contract_price_currency != null and contract_price_currency != ''">
|
||||
CONTRACT_PRICE_CURRENCY = #{contract_price_currency},
|
||||
</if>
|
||||
<if test="contract_price != null and contract_price != ''">
|
||||
CONTRACT_PRICE = #{contract_price},
|
||||
</if>
|
||||
<if test="project_name != null and project_name != ''">
|
||||
PROJECT_NAME = #{project_name},
|
||||
</if>
|
||||
<if test="contract_del_date != null and contract_del_date != ''">
|
||||
CONTRACT_DEL_DATE = #{contract_del_date},
|
||||
</if>
|
||||
<if test="req_del_date != null and req_del_date != ''">
|
||||
REQ_DEL_DATE = #{req_del_date},
|
||||
</if>
|
||||
<if test="contract_company != null and contract_company != ''">
|
||||
CONTRACT_COMPANY = #{contract_company},
|
||||
</if>
|
||||
<if test="manufacture_plant != null and manufacture_plant != ''">
|
||||
MANUFACTURE_PLANT = #{manufacture_plant},
|
||||
</if>
|
||||
<if test="quantity != null and quantity != ''">
|
||||
QUANTITY = #{quantity}
|
||||
</if>
|
||||
</set>
|
||||
WHERE CONTRACT_OBJID = #{objId}
|
||||
AND PART_OBJID = #{part_objid}
|
||||
</update>
|
||||
|
||||
@@ -1813,6 +1813,40 @@ public class ContractMgmtController {
|
||||
return "/contractMgmt/estimateRegistFormPopup";
|
||||
}
|
||||
|
||||
/**
|
||||
* 견적요청 조회 전용 팝업 (View Only)
|
||||
* @param request
|
||||
* @param paramMap
|
||||
* @return
|
||||
*/
|
||||
@SuppressWarnings({ "rawtypes", "unchecked" })
|
||||
@RequestMapping("/contractMgmt/estimateViewPopup.do")
|
||||
public String estimateViewPopup(HttpSession session, HttpServletRequest request, @RequestParam Map<String, Object> paramMap){
|
||||
String objId = CommonUtils.checkNull(paramMap.get("objId"));
|
||||
|
||||
try{
|
||||
Map info = null;
|
||||
List<Map<String, Object>> itemList = new ArrayList<Map<String, Object>>();
|
||||
|
||||
if(paramMap.get("objId")!=null){
|
||||
paramMap.put("objId",objId);
|
||||
info = CommonUtils.keyChangeUpperMap(contractMgmtService.getContractMgmtInfo(paramMap));
|
||||
|
||||
// 품목 목록 조회
|
||||
itemList = contractMgmtService.getContractItemList(objId);
|
||||
}
|
||||
|
||||
request.setAttribute("info", info);
|
||||
request.setAttribute("objId", objId);
|
||||
request.setAttribute("itemList", itemList);
|
||||
|
||||
}catch(Exception e){
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
return "/contractMgmt/estimateViewPopup";
|
||||
}
|
||||
|
||||
/**
|
||||
* 품번 검색 (AJAX용)
|
||||
*/
|
||||
@@ -1950,22 +1984,56 @@ public class ContractMgmtController {
|
||||
try{
|
||||
Map estimate = null;
|
||||
List<Map> items = new ArrayList<Map>();
|
||||
Map<String, String> code_map = new HashMap<String, String>();
|
||||
|
||||
// templateObjId가 있으면 기존 견적서 조회 (견적현황에서 클릭한 경우)
|
||||
if(!"".equals(templateObjId) && !"-1".equals(templateObjId)){
|
||||
paramMap.put("templateObjId", templateObjId);
|
||||
estimate = contractMgmtService.getEstimateTemplateByObjId(paramMap);
|
||||
items = contractMgmtService.getEstimateTemplateItemsByTemplateObjId(paramMap);
|
||||
// Map 키를 대문자로 변환
|
||||
if(estimate != null) {
|
||||
estimate = CommonUtils.toUpperCaseMapKey(estimate);
|
||||
}
|
||||
if(items != null && items.size() > 0) {
|
||||
items = CommonUtils.keyChangeUpperList((ArrayList)items);
|
||||
}
|
||||
}
|
||||
// objId만 있으면 CONTRACT 정보로 견적서 신규 작성
|
||||
else if(!"".equals(objId) && !"-1".equals(objId)){
|
||||
// 기존 견적서 데이터 조회
|
||||
// 계약 정보 조회
|
||||
estimate = contractMgmtService.getEstimateTemplateInfo(paramMap);
|
||||
items = contractMgmtService.getEstimateTemplateItems(paramMap);
|
||||
// 계약 품목 조회 (CONTRACT_ITEM에서)
|
||||
paramMap.put("contractObjId", objId);
|
||||
items = contractMgmtService.getContractItems(paramMap);
|
||||
// Map 키를 대문자로 변환
|
||||
if(estimate != null) {
|
||||
estimate = CommonUtils.toUpperCaseMapKey(estimate);
|
||||
}
|
||||
if(items != null && items.size() > 0) {
|
||||
items = CommonUtils.keyChangeUpperList((ArrayList)items);
|
||||
}
|
||||
}
|
||||
|
||||
request.setAttribute("estimate", estimate);
|
||||
request.setAttribute("items", items);
|
||||
// 고객사 코드맵 추가
|
||||
String customer_cd = "";
|
||||
if(estimate != null) {
|
||||
// templateObjId가 있으면 RECIPIENT 사용 (저장된 견적서)
|
||||
if(!"".equals(templateObjId) && !"-1".equals(templateObjId)) {
|
||||
if(estimate.get("RECIPIENT") != null) {
|
||||
customer_cd = CommonUtils.nullToEmpty((String)estimate.get("RECIPIENT"));
|
||||
}
|
||||
}
|
||||
// 신규 작성이면 CUSTOMER_OBJID 사용
|
||||
else if(estimate.get("CUSTOMER_OBJID") != null) {
|
||||
customer_cd = CommonUtils.nullToEmpty((String)estimate.get("CUSTOMER_OBJID"));
|
||||
}
|
||||
}
|
||||
code_map.put("customer_cd", commonService.bizMakeOptionList("", customer_cd, "common.getsupplyselect"));
|
||||
|
||||
request.setAttribute("estimate", estimate);
|
||||
request.setAttribute("items", items);
|
||||
request.setAttribute("code_map", code_map);
|
||||
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
@@ -2040,7 +2108,7 @@ public class ContractMgmtController {
|
||||
}
|
||||
|
||||
/**
|
||||
* 견적서 저장 (AJAX)
|
||||
* 일반 견적서 저장 (AJAX) - Template 1
|
||||
* @param request
|
||||
* @param paramMap
|
||||
* @return
|
||||
@@ -2055,7 +2123,7 @@ public class ContractMgmtController {
|
||||
paramMap.put("userId", person.getUserId());
|
||||
|
||||
// 합계 정보 로그 (디버깅용)
|
||||
System.out.println("견적서 저장 - 합계: " + paramMap.get("total_amount") + ", 원화환산: " + paramMap.get("total_amount_krw"));
|
||||
System.out.println("일반 견적서 저장 - 합계: " + paramMap.get("total_amount") + ", 원화환산: " + paramMap.get("total_amount_krw"));
|
||||
|
||||
contractMgmtService.saveEstimateTemplate(request, paramMap);
|
||||
|
||||
@@ -2070,6 +2138,36 @@ public class ContractMgmtController {
|
||||
return resultMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* 장비 견적서 저장 (AJAX) - Template 2
|
||||
* @param request
|
||||
* @param paramMap
|
||||
* @return
|
||||
*/
|
||||
@ResponseBody
|
||||
@RequestMapping(value="/contractMgmt/saveEstimate2.do", method=RequestMethod.POST)
|
||||
public Map saveEstimate2(HttpServletRequest request, @RequestParam Map<String, Object> paramMap){
|
||||
Map resultMap = new HashMap();
|
||||
|
||||
try {
|
||||
PersonBean person = (PersonBean)request.getSession().getAttribute(Constants.PERSON_BEAN);
|
||||
paramMap.put("userId", person.getUserId());
|
||||
|
||||
System.out.println("장비 견적서 저장");
|
||||
|
||||
contractMgmtService.saveEstimateTemplate2(request, paramMap);
|
||||
|
||||
resultMap.put("result", "success");
|
||||
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
resultMap.put("result", "error");
|
||||
resultMap.put("message", e.getMessage());
|
||||
}
|
||||
|
||||
return resultMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* PDF 청크 업로드 (AJAX)
|
||||
* @param request
|
||||
@@ -2125,6 +2223,115 @@ public class ContractMgmtController {
|
||||
|
||||
return resultMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* 견적서 메일 작성 팝업
|
||||
* @param request
|
||||
* @param paramMap - contractObjId
|
||||
* @return
|
||||
*/
|
||||
@RequestMapping("/contractMgmt/estimateMailFormPopup.do")
|
||||
public String estimateMailFormPopup(HttpServletRequest request, @RequestParam Map paramMap){
|
||||
return "/contractMgmt/estimateMailFormPopup";
|
||||
}
|
||||
|
||||
/**
|
||||
* 계약 정보 조회 (메일 발송용) (AJAX)
|
||||
* @param request
|
||||
* @param paramMap - objId (CONTRACT_OBJID)
|
||||
* @return
|
||||
*/
|
||||
@ResponseBody
|
||||
@RequestMapping(value="/contractMgmt/getContractInfoForMail.do", method=RequestMethod.POST)
|
||||
public Map getContractInfoForMail(HttpServletRequest request, @RequestParam Map<String, Object> paramMap){
|
||||
Map resultMap = new HashMap();
|
||||
|
||||
try {
|
||||
String objId = CommonUtils.checkNull(paramMap.get("objId"));
|
||||
|
||||
if("".equals(objId)){
|
||||
resultMap.put("result", "error");
|
||||
resultMap.put("message", "계약 OBJID가 없습니다.");
|
||||
return resultMap;
|
||||
}
|
||||
|
||||
// 계약 정보 조회
|
||||
Map contractInfo = contractMgmtService.getContractInfoForMail(paramMap);
|
||||
|
||||
if(contractInfo != null && !contractInfo.isEmpty()){
|
||||
// Map 키를 대문자로 변환
|
||||
contractInfo = CommonUtils.toUpperCaseMapKey(contractInfo);
|
||||
|
||||
resultMap.put("result", "success");
|
||||
resultMap.put("contractInfo", contractInfo);
|
||||
} else {
|
||||
resultMap.put("result", "error");
|
||||
resultMap.put("message", "계약 정보를 찾을 수 없습니다.");
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
resultMap.put("result", "error");
|
||||
resultMap.put("message", "계약 정보 조회 중 오류가 발생했습니다: " + e.getMessage());
|
||||
}
|
||||
|
||||
return resultMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* 견적서 메일 발송 (커스텀) (AJAX)
|
||||
* @param request
|
||||
* @param paramMap - objId, pdfSessionId, toEmails, ccEmails, subject, contents
|
||||
* @return
|
||||
*/
|
||||
@ResponseBody
|
||||
@RequestMapping(value="/contractMgmt/sendEstimateMailCustom.do", method=RequestMethod.POST)
|
||||
public Map sendEstimateMailCustom(HttpServletRequest request,
|
||||
@RequestParam Map<String, Object> paramMap){
|
||||
Map resultMap = new HashMap();
|
||||
|
||||
try {
|
||||
String objId = CommonUtils.checkNull(paramMap.get("objId"));
|
||||
String toEmails = CommonUtils.checkNull(paramMap.get("toEmails"));
|
||||
String subject = CommonUtils.checkNull(paramMap.get("subject"));
|
||||
String contents = CommonUtils.checkNull(paramMap.get("contents"));
|
||||
|
||||
// 필수 파라미터 검증
|
||||
if("".equals(objId)){
|
||||
resultMap.put("result", "error");
|
||||
resultMap.put("message", "계약 OBJID가 없습니다.");
|
||||
return resultMap;
|
||||
}
|
||||
|
||||
if("".equals(toEmails)){
|
||||
resultMap.put("result", "error");
|
||||
resultMap.put("message", "수신인이 없습니다.");
|
||||
return resultMap;
|
||||
}
|
||||
|
||||
if("".equals(subject)){
|
||||
resultMap.put("result", "error");
|
||||
resultMap.put("message", "제목이 없습니다.");
|
||||
return resultMap;
|
||||
}
|
||||
|
||||
if("".equals(contents)){
|
||||
resultMap.put("result", "error");
|
||||
resultMap.put("message", "내용이 없습니다.");
|
||||
return resultMap;
|
||||
}
|
||||
|
||||
// 메일 발송 서비스 호출
|
||||
resultMap = contractMgmtService.sendEstimateMailCustom(request, paramMap);
|
||||
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
resultMap.put("result", "error");
|
||||
resultMap.put("message", "메일 발송 중 오류가 발생했습니다: " + e.getMessage());
|
||||
}
|
||||
|
||||
return resultMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* 주문서관리 리스트
|
||||
@@ -2267,17 +2474,9 @@ public class ContractMgmtController {
|
||||
Map resultMap = new HashMap();
|
||||
|
||||
try {
|
||||
// 결재완료 상태인 경우 최종 견적서 템플릿에서 품목 조회
|
||||
String useEstimateTemplate = CommonUtils.checkNull(paramMap.get("useEstimateTemplate"));
|
||||
List<Map> items = null;
|
||||
|
||||
if("Y".equals(useEstimateTemplate)) {
|
||||
// 최종 견적서 템플릿의 품목 조회
|
||||
items = contractMgmtService.getEstimateTemplateItemsForOrder(paramMap);
|
||||
} else {
|
||||
// 기존 방식: CONTRACT_ITEM에서 조회
|
||||
items = contractMgmtService.getContractItems(paramMap);
|
||||
}
|
||||
// getContractItems()에서 ORDER_* 정보가 없으면 자동으로 견적서에서 가져옴
|
||||
// 일반 견적서와 장비 견적서 모두 처리 가능
|
||||
List<Map> items = contractMgmtService.getContractItems(paramMap);
|
||||
|
||||
resultMap.put("result", "success");
|
||||
resultMap.put("items", items);
|
||||
@@ -2297,4 +2496,152 @@ public class ContractMgmtController {
|
||||
request.setAttribute("docTypeName", CommonUtils.checkNull(paramMap.get("docTypeName")));
|
||||
return "/contractMgmt/FileRegistPopup";
|
||||
}
|
||||
|
||||
/**
|
||||
* 영업정보의 품목 목록 조회 (견적서 작성 시 사용)
|
||||
*/
|
||||
@RequestMapping(value="/contractMgmt/getContractItemList.do", method=RequestMethod.POST)
|
||||
@ResponseBody
|
||||
public Map<String, Object> getContractItemList(@RequestParam Map<String, Object> paramMap) {
|
||||
Map<String, Object> resultMap = new HashMap<String, Object>();
|
||||
|
||||
try {
|
||||
String contractObjId = CommonUtils.checkNull((String)paramMap.get("contractObjId"));
|
||||
|
||||
if(CommonUtils.isEmpty(contractObjId)) {
|
||||
resultMap.put("result", "error");
|
||||
resultMap.put("message", "영업정보 OBJID가 없습니다.");
|
||||
return resultMap;
|
||||
}
|
||||
|
||||
// 영업정보 조회 (고객사 정보 포함)
|
||||
Map<String, Object> contractInfo = contractMgmtService.getContractInfo(contractObjId);
|
||||
|
||||
// 품목 목록 조회
|
||||
List<Map<String, Object>> items = contractMgmtService.getContractItemList(contractObjId);
|
||||
|
||||
// 대문자로 키 변환
|
||||
List<Map<String, Object>> upperItems = new ArrayList<Map<String, Object>>();
|
||||
for(Map<String, Object> item : items) {
|
||||
upperItems.add(CommonUtils.toUpperCaseMapKey(item));
|
||||
}
|
||||
|
||||
resultMap.put("result", "success");
|
||||
resultMap.put("items", upperItems);
|
||||
|
||||
// 고객사 정보 및 환율 정보 추가
|
||||
if(contractInfo != null) {
|
||||
Map<String, Object> upperContractInfo = CommonUtils.toUpperCaseMapKey(contractInfo);
|
||||
resultMap.put("customerObjId", upperContractInfo.get("CUSTOMER_OBJID"));
|
||||
resultMap.put("exchangeRate", upperContractInfo.get("EXCHANGE_RATE"));
|
||||
resultMap.put("currencyName", upperContractInfo.get("CONTRACT_CURRENCY_NAME"));
|
||||
}
|
||||
|
||||
} catch(Exception e) {
|
||||
e.printStackTrace();
|
||||
resultMap.put("result", "error");
|
||||
resultMap.put("message", e.getMessage());
|
||||
}
|
||||
|
||||
return resultMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* 고객사 담당자 목록 조회
|
||||
*/
|
||||
@RequestMapping(value="/contractMgmt/getCustomerManagerList.do", method=RequestMethod.POST)
|
||||
@ResponseBody
|
||||
public Map<String, Object> getCustomerManagerList(@RequestParam Map<String, Object> paramMap) {
|
||||
Map<String, Object> resultMap = new HashMap<String, Object>();
|
||||
|
||||
try {
|
||||
String customerObjId = CommonUtils.checkNull((String)paramMap.get("customerObjId"));
|
||||
|
||||
if(CommonUtils.isEmpty(customerObjId)) {
|
||||
resultMap.put("result", "error");
|
||||
resultMap.put("message", "고객사 OBJID가 없습니다.");
|
||||
return resultMap;
|
||||
}
|
||||
|
||||
// 고객사 정보 조회
|
||||
Map<String, Object> customerInfo = contractMgmtService.getCustomerInfo(customerObjId);
|
||||
|
||||
System.out.println("=== 고객사 정보 조회 결과 ===");
|
||||
System.out.println("customerInfo: " + customerInfo);
|
||||
|
||||
if(customerInfo != null) {
|
||||
List<Map<String, Object>> managers = new ArrayList<Map<String, Object>>();
|
||||
|
||||
// manager1 ~ manager5 추출 (소문자 키로 접근)
|
||||
for(int i = 1; i <= 5; i++) {
|
||||
String managerName = CommonUtils.checkNull((String)customerInfo.get("manager" + i + "_name"));
|
||||
String managerEmail = CommonUtils.checkNull((String)customerInfo.get("manager" + i + "_email"));
|
||||
|
||||
System.out.println("manager" + i + "_name: " + managerName);
|
||||
System.out.println("manager" + i + "_email: " + managerEmail);
|
||||
|
||||
if(!CommonUtils.isEmpty(managerName)) {
|
||||
Map<String, Object> manager = new HashMap<String, Object>();
|
||||
manager.put("name", managerName);
|
||||
manager.put("email", managerEmail);
|
||||
managers.add(manager);
|
||||
}
|
||||
}
|
||||
|
||||
System.out.println("최종 managers 개수: " + managers.size());
|
||||
|
||||
resultMap.put("result", "success");
|
||||
resultMap.put("managers", managers);
|
||||
} else {
|
||||
resultMap.put("result", "success");
|
||||
resultMap.put("managers", new ArrayList<Map<String, Object>>());
|
||||
}
|
||||
|
||||
} catch(Exception e) {
|
||||
e.printStackTrace();
|
||||
resultMap.put("result", "error");
|
||||
resultMap.put("message", e.getMessage());
|
||||
}
|
||||
|
||||
return resultMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* 프로젝트 존재 여부 확인 (AJAX)
|
||||
* @param request
|
||||
* @param paramMap - contractObjId
|
||||
* @return
|
||||
*/
|
||||
@ResponseBody
|
||||
@RequestMapping(value="/contractMgmt/checkProjectExists.do", method=RequestMethod.POST)
|
||||
public Map checkProjectExists(HttpServletRequest request, @RequestParam Map<String, Object> paramMap){
|
||||
Map resultMap = new HashMap();
|
||||
|
||||
try {
|
||||
String contractObjId = CommonUtils.checkNull(paramMap.get("contractObjId"));
|
||||
|
||||
if(StringUtils.isBlank(contractObjId)) {
|
||||
System.out.println("contractObjId가 비어있음 - exists: false");
|
||||
resultMap.put("exists", false);
|
||||
return resultMap;
|
||||
}
|
||||
|
||||
paramMap.put("objId", contractObjId);
|
||||
Map projectInfo = contractMgmtService.checkProjectExists(paramMap);
|
||||
|
||||
if(projectInfo != null) {
|
||||
resultMap.put("exists", true);
|
||||
resultMap.put("projectInfo", projectInfo);
|
||||
} else {
|
||||
resultMap.put("exists", false);
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
resultMap.put("exists", false);
|
||||
resultMap.put("error", e.getMessage());
|
||||
}
|
||||
|
||||
return resultMap;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,124 +0,0 @@
|
||||
/*
|
||||
* ContractMgmtController
|
||||
*
|
||||
* 1.0
|
||||
*
|
||||
* 2021.10.01
|
||||
*
|
||||
* Copyright ions
|
||||
*/
|
||||
package com.pms.salesmgmt.controller;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpSession;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
|
||||
import com.pms.common.utils.CommonUtils;
|
||||
import com.pms.salesmgmt.service.AccountService;
|
||||
import com.pms.salesmgmt.service.ContractMgmtService;
|
||||
import com.pms.salesmgmt.service.DeliveryService;
|
||||
import com.pms.salesmgmt.service.SalesMgmtCommonService;
|
||||
|
||||
/**
|
||||
* <pre>
|
||||
* 계약관리 Controller
|
||||
* </pre>
|
||||
* @since 2021.10.01
|
||||
* @author kim
|
||||
* @version 1.0
|
||||
*
|
||||
* <pre>
|
||||
* << 개정 이력 >>
|
||||
*
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------------- --------------------- --------------------------------------------------------
|
||||
* 2021.10.01 김효일 최초작성
|
||||
*
|
||||
* </pre>
|
||||
*
|
||||
*/
|
||||
@Controller
|
||||
public class SampleController {
|
||||
|
||||
/** 계약관리 Service */
|
||||
@Autowired
|
||||
private DeliveryService deliveryService;
|
||||
|
||||
|
||||
/** 결제예정 Service */
|
||||
@Autowired
|
||||
private AccountService accountService;
|
||||
|
||||
/**
|
||||
* <pre>
|
||||
* 납기예정 목록 조회
|
||||
* </pre>
|
||||
* @param request
|
||||
* @param paramMap - 계약관리 검색 정보
|
||||
* @return String
|
||||
*
|
||||
* <pre>
|
||||
* << 개정 이력 >>
|
||||
*
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------------- --------------------- ----------------------------------------------------------
|
||||
* 2021.10.01 김효일 최초작성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@RequestMapping(value = " /contractMgmt/tabSample1.do", method = RequestMethod.GET)
|
||||
public String tabSample1(HttpServletRequest request
|
||||
, @RequestParam Map<String, Object> paramMap) {
|
||||
try {
|
||||
List<Map<String,Object>> list = deliveryService.getDeliveryAllByOrderNo(request, paramMap);
|
||||
System.out.println("paramMap>>>>>>>>>>>>> " + paramMap);
|
||||
request.setAttribute("LIST", list);
|
||||
} catch(Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
return "/salesmgmt/sample/tabContractMgmtListSample1";
|
||||
}
|
||||
|
||||
/**
|
||||
* <pre>
|
||||
* 결제예정 목록 조회
|
||||
* </pre>
|
||||
* @param request
|
||||
* @param paramMap - 계약관리 검색 정보
|
||||
* @return String
|
||||
*
|
||||
* <pre>
|
||||
* << 개정 이력 >>
|
||||
*
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------------- --------------------- ----------------------------------------------------------
|
||||
* 2021.10.01 김효일 최초작성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@RequestMapping(value = " /contractMgmt/tabSample2.do", method = RequestMethod.GET)
|
||||
public String tabSample2(HttpServletRequest request
|
||||
, @RequestParam Map<String, Object> paramMap) {
|
||||
try {
|
||||
List<Map<String,Object>> list = accountService.getAccountAllByOrderNo(request, paramMap);
|
||||
|
||||
request.setAttribute("LIST", list);
|
||||
} catch(Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
return "/salesmgmt/sample/tabContractMgmtListSample2";
|
||||
}
|
||||
}
|
||||
@@ -1411,8 +1411,11 @@
|
||||
SELECT
|
||||
A.OBJID
|
||||
,A.CATEGORY_CD
|
||||
,CODE_NAME(A.CATEGORY_CD) AS CATEGORY_NAME
|
||||
,A.CUSTOMER_OBJID
|
||||
,(SELECT SUPPLY_NAME FROM SUPPLY_MNG AS O WHERE O.OBJID = A.CUSTOMER_OBJID::NUMERIC) AS CUSTOMER_NAME
|
||||
,A.PRODUCT
|
||||
,CODE_NAME(A.PRODUCT) AS PRODUCT_NAME
|
||||
,A.CUSTOMER_PROJECT_NAME
|
||||
,A.STATUS_CD
|
||||
,A.DUE_DATE
|
||||
@@ -1455,12 +1458,14 @@
|
||||
,A.EST_COMP_DATE
|
||||
,A.EST_RESULT_CD
|
||||
,A.AREA_CD
|
||||
,CODE_NAME(A.AREA_CD) AS AREA_NAME
|
||||
,A.TARGET_PROJECT_NO
|
||||
,A.TARGET_PROJECT_NO_DIRECT
|
||||
,A.CUSTOMER_PRODUCTION_NO
|
||||
,A.MECHANICAL_TYPE
|
||||
,A.OVERHAUL_ORDER
|
||||
,A.PAID_TYPE
|
||||
,(case when A.PAID_TYPE = 'paid' then '유상' when A.PAID_TYPE = 'free' then '무상' else A.PAID_TYPE end) AS PAID_TYPE_NAME
|
||||
,A.RECEIPT_DATE
|
||||
,A.PART_NO
|
||||
,A.PART_NAME
|
||||
@@ -2356,6 +2361,16 @@ SELECT
|
||||
,BUS_REG_NO
|
||||
,OFFICE_NO
|
||||
,EMAIL
|
||||
,MANAGER1_NAME
|
||||
,MANAGER1_EMAIL
|
||||
,MANAGER2_NAME
|
||||
,MANAGER2_EMAIL
|
||||
,MANAGER3_NAME
|
||||
,MANAGER3_EMAIL
|
||||
,MANAGER4_NAME
|
||||
,MANAGER4_EMAIL
|
||||
,MANAGER5_NAME
|
||||
,MANAGER5_EMAIL
|
||||
FROM SUPPLY_MNG
|
||||
WHERE OBJID = #{objid}::numeric
|
||||
</select>
|
||||
@@ -3501,7 +3516,9 @@ ORDER BY ASM.SUPPLY_NAME
|
||||
|
||||
<select id="getProjectListBycontractObjid" parameterType="map" resultType="map">
|
||||
SELECT
|
||||
PROJECT_NAME
|
||||
OBJID,
|
||||
PROJECT_NAME,
|
||||
PROJECT_NO
|
||||
FROM
|
||||
PROJECT_MGMT
|
||||
WHERE CONTRACT_OBJID = #{objId}
|
||||
@@ -3860,7 +3877,14 @@ ORDER BY ASM.SUPPLY_NAME
|
||||
FROM
|
||||
CONTRACT_MGMT AS T
|
||||
WHERE
|
||||
OBJID::VARCHAR = #{objId}
|
||||
<choose>
|
||||
<when test="templateObjId != null and templateObjId != '' and templateObjId != '-1' and (objId == null or objId == '' or objId == '-1')">
|
||||
OBJID::VARCHAR = (SELECT CONTRACT_OBJID FROM ESTIMATE_TEMPLATE WHERE OBJID = #{templateObjId})
|
||||
</when>
|
||||
<otherwise>
|
||||
OBJID::VARCHAR = #{objId}
|
||||
</otherwise>
|
||||
</choose>
|
||||
</select>
|
||||
|
||||
<!-- 견적서 템플릿 목록 조회 (CONTRACT_OBJID 기준) -->
|
||||
@@ -3925,6 +3949,12 @@ ORDER BY ASM.SUPPLY_NAME
|
||||
ET.TOTAL_AMOUNT_KRW,
|
||||
ET.MANAGER_NAME,
|
||||
ET.MANAGER_CONTACT,
|
||||
ET.PART_NAME,
|
||||
ET.PART_OBJID,
|
||||
ET.NOTES_CONTENT,
|
||||
ET.VALIDITY_PERIOD,
|
||||
ET.CATEGORIES_JSON,
|
||||
ET.GROUP1_SUBTOTAL,
|
||||
ET.WRITER,
|
||||
ET.REGDATE,
|
||||
ET.CHG_USER_ID,
|
||||
@@ -3950,12 +3980,19 @@ ORDER BY ASM.SUPPLY_NAME
|
||||
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
|
||||
<choose>
|
||||
<when test="templateObjId != null and templateObjId != '' and templateObjId != '-1'">
|
||||
ET.OBJID = #{templateObjId}
|
||||
</when>
|
||||
<otherwise>
|
||||
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
|
||||
</otherwise>
|
||||
</choose>
|
||||
</select>
|
||||
|
||||
<!-- 견적서 템플릿 데이터 조회 (OBJID 기준) -->
|
||||
@@ -3981,6 +4018,13 @@ ORDER BY ASM.SUPPLY_NAME
|
||||
ET.TOTAL_AMOUNT_KRW,
|
||||
ET.MANAGER_NAME,
|
||||
ET.MANAGER_CONTACT,
|
||||
ET.SHOW_TOTAL_ROW,
|
||||
ET.PART_NAME,
|
||||
ET.PART_OBJID,
|
||||
ET.NOTES_CONTENT,
|
||||
ET.VALIDITY_PERIOD,
|
||||
ET.CATEGORIES_JSON,
|
||||
ET.GROUP1_SUBTOTAL,
|
||||
ET.WRITER,
|
||||
TO_CHAR(ET.REGDATE, 'YYYY-MM-DD HH24:MI') AS REGDATE,
|
||||
ET.CHG_USER_ID,
|
||||
@@ -4059,6 +4103,30 @@ ORDER BY ASM.SUPPLY_NAME
|
||||
ORDER BY SEQ
|
||||
</select>
|
||||
|
||||
<!-- 견적서 템플릿 품목 조회 (PART_OBJID로) -->
|
||||
<select id="getEstimateTemplateItemByPartObjId" parameterType="map" resultType="map">
|
||||
SELECT
|
||||
OBJID,
|
||||
TEMPLATE_OBJID,
|
||||
SEQ,
|
||||
CATEGORY,
|
||||
PART_OBJID,
|
||||
DESCRIPTION,
|
||||
SPECIFICATION,
|
||||
QUANTITY,
|
||||
UNIT,
|
||||
UNIT_PRICE,
|
||||
AMOUNT,
|
||||
NOTE,
|
||||
REMARK
|
||||
FROM
|
||||
ESTIMATE_TEMPLATE_ITEM
|
||||
WHERE
|
||||
TEMPLATE_OBJID = #{templateObjId}
|
||||
AND PART_OBJID = #{partObjId}
|
||||
LIMIT 1
|
||||
</select>
|
||||
|
||||
<!-- 견적서 템플릿 저장 -->
|
||||
<insert id="insertEstimateTemplate" parameterType="map">
|
||||
INSERT INTO ESTIMATE_TEMPLATE (
|
||||
@@ -4082,6 +4150,7 @@ ORDER BY ASM.SUPPLY_NAME
|
||||
TOTAL_AMOUNT_KRW,
|
||||
MANAGER_NAME,
|
||||
MANAGER_CONTACT,
|
||||
SHOW_TOTAL_ROW,
|
||||
WRITER,
|
||||
REGDATE,
|
||||
CHG_USER_ID,
|
||||
@@ -4107,6 +4176,7 @@ ORDER BY ASM.SUPPLY_NAME
|
||||
#{total_amount_krw},
|
||||
#{manager_name},
|
||||
#{manager_contact},
|
||||
#{show_total_row},
|
||||
#{writer},
|
||||
NOW(),
|
||||
#{chg_user_id},
|
||||
@@ -4135,6 +4205,7 @@ ORDER BY ASM.SUPPLY_NAME
|
||||
TOTAL_AMOUNT_KRW = #{total_amount_krw},
|
||||
MANAGER_NAME = #{manager_name},
|
||||
MANAGER_CONTACT = #{manager_contact},
|
||||
SHOW_TOTAL_ROW = #{show_total_row},
|
||||
CHG_USER_ID = #{chg_user_id},
|
||||
CHGDATE = NOW()
|
||||
WHERE
|
||||
@@ -4192,6 +4263,67 @@ WHERE
|
||||
OBJID = #{template_objid}
|
||||
</update>
|
||||
|
||||
<!-- 장비 견적서 템플릿 신규 저장 (Template 2) -->
|
||||
<insert id="insertEstimateTemplate2" parameterType="map">
|
||||
INSERT INTO ESTIMATE_TEMPLATE (
|
||||
OBJID,
|
||||
CONTRACT_OBJID,
|
||||
TEMPLATE_TYPE,
|
||||
EXECUTOR_DATE,
|
||||
RECIPIENT,
|
||||
PART_NAME,
|
||||
PART_OBJID,
|
||||
NOTES_CONTENT,
|
||||
VALIDITY_PERIOD,
|
||||
CATEGORIES_JSON,
|
||||
GROUP1_SUBTOTAL,
|
||||
TOTAL_AMOUNT,
|
||||
TOTAL_AMOUNT_KRW,
|
||||
WRITER,
|
||||
REGDATE,
|
||||
CHG_USER_ID,
|
||||
CHGDATE
|
||||
) VALUES (
|
||||
#{template_objid},
|
||||
#{contract_objid},
|
||||
'2',
|
||||
#{executor_date},
|
||||
#{recipient},
|
||||
#{part_name},
|
||||
#{part_objid},
|
||||
#{notes_content},
|
||||
#{validity_period},
|
||||
#{categories_json},
|
||||
#{group1_subtotal},
|
||||
#{total_amount},
|
||||
#{total_amount_krw},
|
||||
#{writer},
|
||||
NOW(),
|
||||
#{chg_user_id},
|
||||
NOW()
|
||||
)
|
||||
</insert>
|
||||
|
||||
<!-- 장비 견적서 템플릿 수정 (Template 2) -->
|
||||
<update id="updateEstimateTemplate2" parameterType="map">
|
||||
UPDATE ESTIMATE_TEMPLATE
|
||||
SET
|
||||
EXECUTOR_DATE = #{executor_date},
|
||||
RECIPIENT = #{recipient},
|
||||
PART_NAME = #{part_name},
|
||||
PART_OBJID = #{part_objid},
|
||||
NOTES_CONTENT = #{notes_content},
|
||||
VALIDITY_PERIOD = #{validity_period},
|
||||
CATEGORIES_JSON = #{categories_json},
|
||||
GROUP1_SUBTOTAL = #{group1_subtotal},
|
||||
TOTAL_AMOUNT = #{total_amount},
|
||||
TOTAL_AMOUNT_KRW = #{total_amount_krw},
|
||||
CHG_USER_ID = #{chg_user_id},
|
||||
CHGDATE = NOW()
|
||||
WHERE
|
||||
OBJID = #{template_objid}
|
||||
</update>
|
||||
|
||||
<!-- 최종 차수 견적서 조회 (메일 발송용) -->
|
||||
<select id="getLatestEstimateTemplate" parameterType="map" resultType="map">
|
||||
SELECT
|
||||
@@ -4355,8 +4487,9 @@ WHERE
|
||||
SELECT
|
||||
OBJID,
|
||||
CONTRACT_NO,
|
||||
CUSTOMER_OBJID,
|
||||
CONTRACT_CURRENCY,
|
||||
(SELECT CODE_NAME FROM TB_CODE WHERE CODE_ID = CONTRACT_CURRENCY) AS CONTRACT_CURRENCY_NAME,
|
||||
(SELECT CODE_NAME FROM COMM_CODE WHERE CODE_ID = CONTRACT_CURRENCY) AS CONTRACT_CURRENCY_NAME,
|
||||
EXCHANGE_RATE,
|
||||
QUANTITY,
|
||||
PART_NO,
|
||||
@@ -4582,8 +4715,8 @@ WHERE
|
||||
CI.CONTRACT_OBJID,
|
||||
CI.SEQ,
|
||||
CI.PART_OBJID,
|
||||
CI.PART_NO,
|
||||
CI.PART_NAME,
|
||||
COALESCE(NULLIF(CI.PART_NO, ''), PM.PART_NO) AS PART_NO,
|
||||
COALESCE(NULLIF(CI.PART_NAME, ''), PM.PART_NAME) AS PART_NAME,
|
||||
CI.QUANTITY,
|
||||
CI.DUE_DATE,
|
||||
CI.CUSTOMER_REQUEST,
|
||||
@@ -4597,6 +4730,8 @@ WHERE
|
||||
LEFT JOIN CONTRACT_ITEM_SERIAL CIS
|
||||
ON CI.OBJID = CIS.ITEM_OBJID
|
||||
AND CIS.STATUS = 'ACTIVE'
|
||||
LEFT JOIN PART_MNG PM
|
||||
ON CI.PART_OBJID = PM.OBJID
|
||||
WHERE
|
||||
CI.CONTRACT_OBJID = #{contractObjId}
|
||||
AND CI.STATUS = 'ACTIVE'
|
||||
@@ -4606,7 +4741,9 @@ WHERE
|
||||
CI.SEQ,
|
||||
CI.PART_OBJID,
|
||||
CI.PART_NO,
|
||||
CI.PART_NAME,
|
||||
CI.PART_NAME,
|
||||
PM.PART_NO,
|
||||
PM.PART_NAME,
|
||||
CI.QUANTITY,
|
||||
CI.DUE_DATE,
|
||||
CI.CUSTOMER_REQUEST,
|
||||
|
||||
@@ -819,6 +819,7 @@
|
||||
T.OBJID,
|
||||
T.PROJECT_NO,
|
||||
T.CONTRACT_OBJID,
|
||||
T.SALES_DEADLINE_DATE,
|
||||
CODE_NAME(T.CATEGORY_CD) AS ORDER_TYPE,
|
||||
CODE_NAME(T.PRODUCT) AS PRODUCT_TYPE,
|
||||
CODE_NAME(T.AREA_CD) AS NATION,
|
||||
|
||||
@@ -28,6 +28,8 @@ import org.json.simple.parser.JSONParser;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
|
||||
import com.pms.common.Message;
|
||||
import com.pms.common.SqlMapConfig;
|
||||
import com.pms.common.bean.PersonBean;
|
||||
@@ -510,6 +512,35 @@ public class ContractMgmtService {
|
||||
paramMap.put("writer", person.getUserId());
|
||||
int cnt = sqlSession.update("contractMgmt.saveContractMgmtInfo", paramMap);
|
||||
|
||||
// 프로젝트가 존재하는 경우 프로젝트 정보도 업데이트 (수량 제외)
|
||||
String contract_objid = CommonUtils.checkNull(paramMap.get("objId"));
|
||||
if(!"".equals(contract_objid)) {
|
||||
// CONTRACT_OBJID로 프로젝트 존재 여부 확인
|
||||
resultList = sqlSession.selectOne("contractMgmt.getProjectListBycontractObjid", paramMap);
|
||||
|
||||
if(resultList != null) {
|
||||
System.out.println("=== 견적요청 수정 시 프로젝트 업데이트 (수량 제외) ===");
|
||||
System.out.println("CONTRACT_OBJID: " + contract_objid);
|
||||
|
||||
// 품목별로 프로젝트 업데이트 (수량은 제외)
|
||||
List<Map> contractItems = getContractItems(paramMap);
|
||||
|
||||
if(contractItems != null && !contractItems.isEmpty()) {
|
||||
for(Map item : contractItems) {
|
||||
Map<String, Object> updateParam = new HashMap<String, Object>();
|
||||
updateParam.putAll(paramMap);
|
||||
updateParam.put("part_objid", item.get("PART_OBJID"));
|
||||
updateParam.put("due_date", item.get("DUE_DATE"));
|
||||
// quantity는 paramMap에서 제거하여 업데이트되지 않도록 함
|
||||
updateParam.remove("quantity");
|
||||
|
||||
System.out.println("프로젝트 업데이트 - PART_OBJID: " + item.get("PART_OBJID") + ", 납기일: " + updateParam.get("due_date"));
|
||||
sqlSession.update("project.ModifyProjectByContract", updateParam);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resultMap.put("result", true);
|
||||
resultMap.put("msg", Message.SAVE_SUCCESS);
|
||||
sqlSession.commit();
|
||||
@@ -1249,17 +1280,39 @@ public class ContractMgmtService {
|
||||
sqlSession = SqlMapConfig.getInstance().getSqlSession(false);
|
||||
|
||||
String objId = CommonUtils.checkNull(paramMap.get("objId"));
|
||||
String templateObjId = CommonUtils.checkNull(paramMap.get("templateObjId"));
|
||||
String templateType = CommonUtils.checkNull(paramMap.get("template_type"));
|
||||
|
||||
if(!"".equals(objId) && !"-1".equals(objId)){
|
||||
System.out.println("=== getEstimateTemplateInfo 파라미터 ===");
|
||||
System.out.println("objId: " + objId);
|
||||
System.out.println("templateObjId: " + templateObjId);
|
||||
System.out.println("templateType: " + templateType);
|
||||
|
||||
// templateObjId가 있으면 직접 조회
|
||||
if(!"".equals(templateObjId) && !"-1".equals(templateObjId)){
|
||||
paramMap.put("templateObjId", templateObjId);
|
||||
}
|
||||
|
||||
// objId 또는 templateObjId 중 하나라도 있으면 조회
|
||||
if((!"".equals(objId) && !"-1".equals(objId)) || (!"".equals(templateObjId) && !"-1".equals(templateObjId))){
|
||||
// 견적서 기본 정보 조회 (CONTRACT_MGMT 테이블에서)
|
||||
resultMap = (Map) sqlSession.selectOne("contractMgmt.getEstimateTemplateInfo", paramMap);
|
||||
Map baseInfo = (Map) sqlSession.selectOne("contractMgmt.getEstimateTemplateInfo", paramMap);
|
||||
System.out.println("baseInfo: " + baseInfo);
|
||||
if(baseInfo != null){
|
||||
resultMap = CommonUtils.toUpperCaseMapKey(baseInfo);
|
||||
System.out.println("baseInfo (변환 후): " + resultMap);
|
||||
}
|
||||
|
||||
// 견적서 템플릿 정보 조회 (ESTIMATE_TEMPLATE 테이블에서, 있는 경우)
|
||||
Map templateInfo = (Map) sqlSession.selectOne("contractMgmt.getEstimateTemplateData", paramMap);
|
||||
System.out.println("templateInfo: " + templateInfo);
|
||||
if(templateInfo != null && !templateInfo.isEmpty()){
|
||||
templateInfo = CommonUtils.toUpperCaseMapKey(templateInfo);
|
||||
System.out.println("templateInfo (변환 후): " + templateInfo);
|
||||
resultMap.putAll(templateInfo);
|
||||
}
|
||||
|
||||
System.out.println("최종 resultMap 크기: " + resultMap.size());
|
||||
}
|
||||
|
||||
}catch(Exception e){
|
||||
@@ -1284,9 +1337,54 @@ public class ContractMgmtService {
|
||||
sqlSession = SqlMapConfig.getInstance().getSqlSession(false);
|
||||
|
||||
String objId = CommonUtils.checkNull(paramMap.get("objId"));
|
||||
String templateObjId = CommonUtils.checkNull(paramMap.get("templateObjId"));
|
||||
String templateType = CommonUtils.checkNull(paramMap.get("template_type"));
|
||||
|
||||
if(!"".equals(objId) && !"-1".equals(objId)){
|
||||
resultList = sqlSession.selectList("contractMgmt.getEstimateTemplateItems", paramMap);
|
||||
System.out.println("=== getEstimateTemplateItems 파라미터 ===");
|
||||
System.out.println("objId: " + objId);
|
||||
System.out.println("templateObjId: " + templateObjId);
|
||||
System.out.println("templateType: " + templateType);
|
||||
|
||||
// templateObjId가 있으면 우선 사용, 없으면 objId 사용
|
||||
String targetId = !"".equals(templateObjId) && !"-1".equals(templateObjId) ? templateObjId : objId;
|
||||
|
||||
if(!"".equals(targetId) && !"-1".equals(targetId)){
|
||||
// 장비 견적서(Template 2)인 경우 categories_json에서 데이터 가져오기
|
||||
if("2".equals(templateType)){
|
||||
// templateObjId가 있으면 직접 조회, 없으면 contract_objid로 조회
|
||||
if(!"".equals(templateObjId) && !"-1".equals(templateObjId)){
|
||||
paramMap.put("templateObjId", templateObjId);
|
||||
}
|
||||
|
||||
// ESTIMATE_TEMPLATE에서 categories_json 조회
|
||||
Map templateData = (Map) sqlSession.selectOne("contractMgmt.getEstimateTemplateData", paramMap);
|
||||
System.out.println("=== getEstimateTemplateItems 디버깅 ===");
|
||||
System.out.println("templateData (변환 전): " + templateData);
|
||||
|
||||
if(templateData != null){
|
||||
// 대문자로 변환
|
||||
templateData = CommonUtils.toUpperCaseMapKey(templateData);
|
||||
System.out.println("templateData (변환 후): " + templateData);
|
||||
|
||||
// 대소문자 모두 체크 (안전장치)
|
||||
String categoriesJson = CommonUtils.checkNull(templateData.get("CATEGORIES_JSON"));
|
||||
if("".equals(categoriesJson)) categoriesJson = CommonUtils.checkNull(templateData.get("categories_json"));
|
||||
|
||||
System.out.println("categoriesJson: " + categoriesJson);
|
||||
|
||||
if(categoriesJson != null && !categoriesJson.isEmpty()){
|
||||
// JSON 파싱 (Gson 사용)
|
||||
com.google.gson.Gson gson = new com.google.gson.Gson();
|
||||
java.lang.reflect.Type listType = new com.google.gson.reflect.TypeToken<List<Map>>(){}.getType();
|
||||
resultList = gson.fromJson(categoriesJson, listType);
|
||||
System.out.println("파싱된 resultList 크기: " + resultList.size());
|
||||
}
|
||||
}
|
||||
}
|
||||
// 일반 견적서(Template 1)인 경우 기존 방식대로 ESTIMATE_TEMPLATE_ITEM에서 조회
|
||||
else {
|
||||
resultList = sqlSession.selectList("contractMgmt.getEstimateTemplateItems", paramMap);
|
||||
}
|
||||
}
|
||||
|
||||
}catch(Exception e){
|
||||
@@ -1413,20 +1511,20 @@ public class ContractMgmtService {
|
||||
PersonBean person = (PersonBean)request.getSession().getAttribute(Constants.PERSON_BEAN);
|
||||
String userId = person.getUserId();
|
||||
|
||||
String objId = CommonUtils.checkNull(paramMap.get("objId")); // CONTRACT_OBJID
|
||||
String templateObjId = CommonUtils.checkNull(paramMap.get("templateObjId")); // 기존 템플릿 수정 시
|
||||
String templateType = CommonUtils.checkNull(paramMap.get("template_type"));
|
||||
String itemsJson = CommonUtils.checkNull(paramMap.get("items"));
|
||||
String categoriesJson = CommonUtils.checkNull(paramMap.get("categories"));
|
||||
|
||||
// 합계 정보 (일반 견적서용)
|
||||
String totalAmount = CommonUtils.checkNull(paramMap.get("total_amount"));
|
||||
String totalAmountKrw = CommonUtils.checkNull(paramMap.get("total_amount_krw"));
|
||||
|
||||
paramMap.put("writer", userId);
|
||||
paramMap.put("chg_user_id", userId);
|
||||
paramMap.put("total_amount", totalAmount);
|
||||
paramMap.put("total_amount_krw", totalAmountKrw);
|
||||
String objId = CommonUtils.checkNull(paramMap.get("objId")); // CONTRACT_OBJID
|
||||
String templateObjId = CommonUtils.checkNull(paramMap.get("templateObjId")); // 기존 템플릿 수정 시
|
||||
String templateType = CommonUtils.checkNull(paramMap.get("template_type"));
|
||||
String itemsJson = CommonUtils.checkNull(paramMap.get("items"));
|
||||
String categoriesJson = CommonUtils.checkNull(paramMap.get("categories"));
|
||||
|
||||
// 합계 정보 (일반 견적서용)
|
||||
String totalAmount = CommonUtils.checkNull(paramMap.get("total_amount"));
|
||||
String totalAmountKrw = CommonUtils.checkNull(paramMap.get("total_amount_krw"));
|
||||
|
||||
paramMap.put("writer", userId);
|
||||
paramMap.put("chg_user_id", userId);
|
||||
paramMap.put("total_amount", totalAmount);
|
||||
paramMap.put("total_amount_krw", totalAmountKrw);
|
||||
|
||||
// 기존 템플릿 수정인지 신규 작성인지 확인
|
||||
// 중요: templateObjId가 명시적으로 있을 때만 수정, 없으면 항상 신규 작성
|
||||
@@ -1487,6 +1585,189 @@ public class ContractMgmtService {
|
||||
return resultMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* 장비 견적서 저장 (Template 2)
|
||||
* @param request
|
||||
* @param paramMap
|
||||
* @return
|
||||
*/
|
||||
public Map saveEstimateTemplate2(HttpServletRequest request, Map paramMap) throws Exception {
|
||||
Map resultMap = new HashMap();
|
||||
SqlSession sqlSession = null;
|
||||
|
||||
try{
|
||||
sqlSession = SqlMapConfig.getInstance().getSqlSession(false);
|
||||
|
||||
String userId = CommonUtils.checkNull(paramMap.get("userId"));
|
||||
String objId = CommonUtils.checkNull(paramMap.get("objId"));
|
||||
String templateObjId = CommonUtils.checkNull(paramMap.get("templateObjId"));
|
||||
String categoriesJson = CommonUtils.checkNull(paramMap.get("categories"));
|
||||
|
||||
paramMap.put("writer", userId);
|
||||
paramMap.put("chg_user_id", userId);
|
||||
paramMap.put("template_type", "2"); // 장비 견적서
|
||||
|
||||
// 기존 템플릿 수정인지 신규 작성인지 확인
|
||||
boolean isUpdate = false;
|
||||
|
||||
if(!"".equals(templateObjId) && !"-1".equals(templateObjId)){
|
||||
Map existingTemplate = (Map) sqlSession.selectOne("contractMgmt.getEstimateTemplateByObjId", paramMap);
|
||||
if(existingTemplate != null && !existingTemplate.isEmpty()){
|
||||
isUpdate = true;
|
||||
paramMap.put("template_objid", templateObjId);
|
||||
|
||||
// objId가 없으면 기존 템플릿에서 가져오기
|
||||
if("".equals(objId) || "-1".equals(objId)){
|
||||
existingTemplate = CommonUtils.toUpperCaseMapKey(existingTemplate);
|
||||
objId = CommonUtils.checkNull(existingTemplate.get("CONTRACT_OBJID"));
|
||||
paramMap.put("contract_objid", objId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(isUpdate){
|
||||
// 기존 견적서 수정
|
||||
sqlSession.update("contractMgmt.updateEstimateTemplate2", paramMap);
|
||||
} else {
|
||||
// 신규 견적서 작성
|
||||
templateObjId = CommonUtils.createObjId();
|
||||
paramMap.put("template_objid", templateObjId);
|
||||
paramMap.put("contract_objid", objId);
|
||||
sqlSession.insert("contractMgmt.insertEstimateTemplate2", paramMap);
|
||||
}
|
||||
|
||||
// 카테고리 정보 저장 (categories_json 필드에 저장)
|
||||
if(!"".equals(categoriesJson)){
|
||||
paramMap.put("categories_json", categoriesJson);
|
||||
sqlSession.update("contractMgmt.updateEstimateTemplateCategories", paramMap);
|
||||
}
|
||||
|
||||
// 장비 견적서도 ESTIMATE_TEMPLATE_ITEM에 저장 (일관성을 위해)
|
||||
// 기존 품목 삭제
|
||||
sqlSession.delete("contractMgmt.deleteEstimateTemplateItems", paramMap);
|
||||
|
||||
// 품목 정보 생성 및 저장
|
||||
String partName = CommonUtils.checkNull(paramMap.get("part_name"));
|
||||
String partObjId = CommonUtils.checkNull(paramMap.get("part_objid"));
|
||||
Object totalAmount = paramMap.get("total_amount");
|
||||
|
||||
if(!"".equals(partName) && totalAmount != null) {
|
||||
try {
|
||||
long amount = Long.parseLong(totalAmount.toString().replaceAll("[^0-9]", ""));
|
||||
|
||||
// 수량은 categories JSON에서 CNC Machine의 quantity를 가져옴
|
||||
int quantity = 1; // 기본값
|
||||
|
||||
System.out.println("=== 장비 견적서 수량 추출 시작 ===");
|
||||
System.out.println("categoriesJson: " + categoriesJson);
|
||||
|
||||
if(!"".equals(categoriesJson)) {
|
||||
try {
|
||||
// JSON 파싱
|
||||
org.json.simple.parser.JSONParser parser = new org.json.simple.parser.JSONParser();
|
||||
org.json.simple.JSONArray categoriesArray = (org.json.simple.JSONArray) parser.parse(categoriesJson);
|
||||
|
||||
System.out.println("categories 배열 크기: " + categoriesArray.size());
|
||||
|
||||
// CNC Machine 카테고리 찾기
|
||||
for(int i = 0; i < categoriesArray.size(); i++) {
|
||||
org.json.simple.JSONObject category = (org.json.simple.JSONObject) categoriesArray.get(i);
|
||||
String categoryId = (String) category.get("category");
|
||||
|
||||
System.out.println("카테고리 " + i + ": " + categoryId);
|
||||
|
||||
if("cnc_machine".equals(categoryId)) {
|
||||
System.out.println("CNC Machine 카테고리 찾음!");
|
||||
|
||||
// cnc_machine의 quantity는 items 배열 안에 있음
|
||||
org.json.simple.JSONArray items = (org.json.simple.JSONArray) category.get("items");
|
||||
System.out.println("items 배열: " + items);
|
||||
|
||||
if(items != null && items.size() > 0) {
|
||||
org.json.simple.JSONObject firstItem = (org.json.simple.JSONObject) items.get(0);
|
||||
Object qtyObj = firstItem.get("quantity");
|
||||
|
||||
System.out.println("quantity 객체: " + qtyObj);
|
||||
System.out.println("quantity 타입: " + (qtyObj != null ? qtyObj.getClass().getName() : "null"));
|
||||
|
||||
if(qtyObj != null && !"".equals(qtyObj.toString().trim())) {
|
||||
// quantity는 텍스트 형식일 수 있으므로 첫 줄만 추출
|
||||
String qtyStr = qtyObj.toString().split("\n")[0].trim();
|
||||
System.out.println("추출된 수량 문자열: '" + qtyStr + "'");
|
||||
|
||||
if(!qtyStr.isEmpty()) {
|
||||
String numOnly = qtyStr.replaceAll("[^0-9]", "");
|
||||
System.out.println("숫자만 추출: '" + numOnly + "'");
|
||||
|
||||
if(!numOnly.isEmpty()) {
|
||||
quantity = Integer.parseInt(numOnly);
|
||||
System.out.println("최종 수량: " + quantity);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
System.out.println("quantity가 null이거나 비어있음");
|
||||
}
|
||||
} else {
|
||||
System.out.println("items 배열이 비어있음");
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
System.out.println("최종 결정된 수량: " + quantity);
|
||||
} catch(Exception e) {
|
||||
System.out.println("수량 추출 실패, 기본값 1 사용");
|
||||
e.printStackTrace();
|
||||
}
|
||||
} else {
|
||||
System.out.println("categoriesJson이 비어있음");
|
||||
}
|
||||
|
||||
// 단가 계산 (총액 / 수량)
|
||||
long unitPrice = quantity > 0 ? amount / quantity : amount;
|
||||
|
||||
// JSON 형식으로 품목 데이터 생성
|
||||
String itemJson = "[{" +
|
||||
"\"seq\": 1," +
|
||||
"\"category\": \"\"," +
|
||||
"\"part_objid\": \"" + partObjId + "\"," +
|
||||
"\"description\": \"" + partName + "\"," +
|
||||
"\"specification\": \"\"," +
|
||||
"\"quantity\": " + quantity + "," +
|
||||
"\"unit\": \"EA\"," +
|
||||
"\"unit_price\": " + unitPrice + "," +
|
||||
"\"amount\": " + amount + "," +
|
||||
"\"note\": \"\"," +
|
||||
"\"remark\": \"\"" +
|
||||
"}]";
|
||||
|
||||
paramMap.put("items_json", itemJson);
|
||||
sqlSession.insert("contractMgmt.insertEstimateTemplateItems", paramMap);
|
||||
|
||||
System.out.println("장비 견적서 품목 저장 완료 - 품명: " + partName + ", 수량: " + quantity + ", 단가: " + unitPrice);
|
||||
} catch(Exception e) {
|
||||
System.out.println("장비 견적서 품목 저장 실패: " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
// 품목 저장 실패해도 견적서는 저장되도록 예외를 던지지 않음
|
||||
}
|
||||
}
|
||||
|
||||
sqlSession.commit();
|
||||
|
||||
resultMap.put("result", "success");
|
||||
resultMap.put("msg", Message.SAVE_SUCCESS);
|
||||
|
||||
}catch(Exception e){
|
||||
if(sqlSession != null) sqlSession.rollback();
|
||||
e.printStackTrace();
|
||||
throw e; // 예외를 컨트롤러로 전달
|
||||
}finally{
|
||||
if(sqlSession != null) sqlSession.close();
|
||||
}
|
||||
|
||||
return resultMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* 견적서 메일 발송
|
||||
* @param request
|
||||
@@ -1530,26 +1811,61 @@ public class ContractMgmtService {
|
||||
|
||||
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());
|
||||
// 3. 견적서 품목 조회
|
||||
List<Map> estimateItems = new ArrayList<Map>();
|
||||
|
||||
if("2".equals(templateType)) {
|
||||
// 장비 견적서 (Template 2): CATEGORIES_JSON 파싱
|
||||
String categoriesJson = CommonUtils.checkNull(estimateTemplate.get("categories_json"));
|
||||
if("".equals(categoriesJson)) categoriesJson = CommonUtils.checkNull(estimateTemplate.get("CATEGORIES_JSON"));
|
||||
|
||||
if(!"".equals(categoriesJson)) {
|
||||
try {
|
||||
Gson gson = new Gson();
|
||||
List<Map> categories = gson.fromJson(categoriesJson, List.class);
|
||||
|
||||
// CNC Machine 카테고리의 items 추출
|
||||
for(Map category : categories) {
|
||||
String categoryName = CommonUtils.checkNull(category.get("category"));
|
||||
if("cnc_machine".equals(categoryName)) {
|
||||
List<Map> items = (List<Map>) category.get("items");
|
||||
if(items != null && !items.isEmpty()) {
|
||||
estimateItems.addAll(items);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch(Exception e) {
|
||||
System.out.println("CATEGORIES_JSON 파싱 오류: " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
System.out.println("================================");
|
||||
} else {
|
||||
// 일반 견적서 (Template 1): ESTIMATE_TEMPLATE_ITEM 테이블 조회
|
||||
Map itemParam = new HashMap();
|
||||
itemParam.put("templateObjId", templateObjId);
|
||||
estimateItems = sqlSession.selectList("contractMgmt.getEstimateTemplateItemsByTemplateObjId", itemParam);
|
||||
}
|
||||
|
||||
System.out.println("===== Estimate Items Debug =====");
|
||||
System.out.println("Template Type: " + templateType);
|
||||
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 포함)
|
||||
// 4. 메일 제목 생성
|
||||
String contractNo = CommonUtils.checkNull(contractInfo.get("contract_no"));
|
||||
String customerName = CommonUtils.checkNull(contractInfo.get("customer_name"));
|
||||
String subject = "[" + customerName + "] " + contractNo + " 견적서 [OBJID:" + objId + "]";
|
||||
String subject = "[" + customerName + "] " + contractNo + " 견적서";
|
||||
|
||||
// mail_log 조회용 제목 (OBJID 포함)
|
||||
String subjectForLog = subject + " [OBJID:" + objId + "]";
|
||||
|
||||
// 5. 메일 내용 생성 (간단한 텍스트 버전)
|
||||
String contents = makeEstimateMailContents(contractInfo, estimateTemplate, estimateItems);
|
||||
@@ -1600,7 +1916,8 @@ public class ContractMgmtService {
|
||||
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("Subject (실제 메일): " + subject);
|
||||
System.out.println("Subject (mail_log): " + subjectForLog);
|
||||
System.out.println("첨부파일 개수: " + attachFileList.size());
|
||||
System.out.println("========================");
|
||||
|
||||
@@ -1615,10 +1932,11 @@ public class ContractMgmtService {
|
||||
ccEmailList,
|
||||
new ArrayList<String>(), // bccEmailList (빈 리스트)
|
||||
null, // important
|
||||
subject,
|
||||
subject, // 실제 메일 제목 (OBJID 없음)
|
||||
contents,
|
||||
attachFileList.size() > 0 ? attachFileList : null, // PDF 첨부
|
||||
"CONTRACT_ESTIMATE"
|
||||
"CONTRACT_ESTIMATE",
|
||||
subjectForLog // mail_log용 제목 (OBJID 포함)
|
||||
);
|
||||
|
||||
System.out.println("메일 발송 결과: " + mailSent);
|
||||
@@ -1986,6 +2304,32 @@ private String encodeImageToBase64(String imagePath) {
|
||||
return resultMap != null ? resultMap : new HashMap<String, Object>();
|
||||
}
|
||||
|
||||
/**
|
||||
* 고객사 정보 조회
|
||||
*/
|
||||
public Map<String, Object> getCustomerInfo(String customerObjId) {
|
||||
Map<String, Object> resultMap = new HashMap<String, Object>();
|
||||
SqlSession sqlSession = null;
|
||||
|
||||
try {
|
||||
sqlSession = SqlMapConfig.getInstance().getSqlSession();
|
||||
|
||||
Map<String, Object> paramMap = new HashMap<String, Object>();
|
||||
paramMap.put("objId", customerObjId);
|
||||
|
||||
resultMap = (Map<String, Object>) sqlSession.selectOne("common.getSupplyInfo", paramMap);
|
||||
|
||||
} catch(Exception e) {
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
if(sqlSession != null) {
|
||||
sqlSession.close();
|
||||
}
|
||||
}
|
||||
|
||||
return resultMap != null ? resultMap : new HashMap<String, Object>();
|
||||
}
|
||||
|
||||
/**
|
||||
* 수주정보 조회 (영업정보와 동일)
|
||||
* @param objId
|
||||
@@ -2029,9 +2373,76 @@ private String encodeImageToBase64(String imagePath) {
|
||||
sqlSession = SqlMapConfig.getInstance().getSqlSession();
|
||||
items = sqlSession.selectList("contractMgmt.getContractItems", paramMap);
|
||||
|
||||
System.out.println("=== getContractItems 디버깅 시작 ===");
|
||||
System.out.println("조회된 품목 수: " + (items != null ? items.size() : 0));
|
||||
|
||||
// ORDER_* 정보가 없으면 견적서에서 가져오기
|
||||
if(items != null && !items.isEmpty()) {
|
||||
for(Map item : items) {
|
||||
// ORDER_QUANTITY가 없거나 0이면 견적서에서 가져오기
|
||||
Object orderQty = item.get("order_quantity");
|
||||
System.out.println("품목 PART_NO: " + item.get("part_no") + ", ORDER_QUANTITY: " + orderQty);
|
||||
|
||||
if(orderQty == null || "".equals(orderQty.toString().trim()) || "0".equals(orderQty.toString().trim())) {
|
||||
System.out.println("ORDER_QUANTITY가 비어있음 - 견적서에서 조회 시작");
|
||||
|
||||
// 최종 견적서 조회
|
||||
Map estimateParam = new HashMap();
|
||||
estimateParam.put("objId", paramMap.get("contractObjId"));
|
||||
Map latestEstimate = sqlSession.selectOne("contractMgmt.getLatestEstimateTemplate", estimateParam);
|
||||
|
||||
System.out.println("최종 견적서 조회 결과: " + (latestEstimate != null ? "있음" : "없음"));
|
||||
|
||||
if(latestEstimate != null) {
|
||||
// 일반 견적서와 장비 견적서 모두 ESTIMATE_TEMPLATE_ITEM에서 조회
|
||||
Map itemParam = new HashMap();
|
||||
itemParam.put("templateObjId", latestEstimate.get("OBJID"));
|
||||
itemParam.put("partObjId", item.get("part_objid"));
|
||||
|
||||
System.out.println("견적서 품목 조회 - templateObjId: " + latestEstimate.get("OBJID") + ", partObjId: " + item.get("part_objid"));
|
||||
|
||||
Map estimateItem = sqlSession.selectOne("contractMgmt.getEstimateTemplateItemByPartObjId", itemParam);
|
||||
|
||||
System.out.println("견적서 품목 조회 결과: " + (estimateItem != null ? "있음" : "없음"));
|
||||
|
||||
if(estimateItem != null) {
|
||||
System.out.println("견적서 품목 - quantity: " + estimateItem.get("quantity") + ", unit_price: " + estimateItem.get("unit_price"));
|
||||
|
||||
// 견적서의 수량, 단가 정보를 ORDER_* 필드에 매핑
|
||||
item.put("order_quantity", estimateItem.get("quantity"));
|
||||
item.put("order_unit_price", estimateItem.get("unit_price"));
|
||||
|
||||
// 공급가액 계산
|
||||
Object quantity = estimateItem.get("quantity");
|
||||
Object unitPrice = estimateItem.get("unit_price");
|
||||
long supplyPrice = 0;
|
||||
|
||||
if(quantity != null && unitPrice != null) {
|
||||
try {
|
||||
long qty = Long.parseLong(quantity.toString().replaceAll("[^0-9]", ""));
|
||||
long price = Long.parseLong(unitPrice.toString().replaceAll("[^0-9]", ""));
|
||||
supplyPrice = qty * price;
|
||||
} catch(Exception e) {
|
||||
System.out.println("금액 계산 실패: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
item.put("order_supply_price", supplyPrice);
|
||||
item.put("order_vat", Math.round(supplyPrice * 0.1));
|
||||
item.put("order_total_amount", supplyPrice + Math.round(supplyPrice * 0.1));
|
||||
|
||||
System.out.println("계산 완료 - 공급가액: " + supplyPrice + ", 부가세: " + Math.round(supplyPrice * 0.1));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 대문자 변환
|
||||
items = CommonUtils.keyChangeUpperList(items);
|
||||
|
||||
System.out.println("=== getContractItems 디버깅 종료 ===");
|
||||
|
||||
}catch(Exception e){
|
||||
e.printStackTrace();
|
||||
}finally{
|
||||
@@ -2044,6 +2455,11 @@ private String encodeImageToBase64(String imagePath) {
|
||||
/**
|
||||
* 견적서 템플릿 품목 조회 (수주등록용)
|
||||
* 최종 견적서의 품목 정보를 수주등록 형식으로 변환하여 반환
|
||||
*
|
||||
* 참고: 이 메서드는 사용되지 않음. getContractItems()를 사용하세요.
|
||||
* 일반 견적서와 장비 견적서 모두 ESTIMATE_TEMPLATE_ITEM에 저장되므로
|
||||
* 동일한 방식으로 처리 가능
|
||||
*
|
||||
* @param paramMap - contractObjId
|
||||
* @return
|
||||
*/
|
||||
@@ -2058,15 +2474,17 @@ private String encodeImageToBase64(String imagePath) {
|
||||
// 1. 최종 견적서 템플릿 조회
|
||||
Map templateParam = new HashMap();
|
||||
templateParam.put("objId", paramMap.get("contractObjId"));
|
||||
Map template = sqlSession.selectOne("contractMgmt.getEstimateTemplateData", templateParam);
|
||||
Map template = sqlSession.selectOne("contractMgmt.getLatestEstimateTemplate", templateParam);
|
||||
|
||||
if(template != null) {
|
||||
// 2. 견적서 템플릿의 품목 조회
|
||||
template = CommonUtils.toUpperCaseMapKey(template);
|
||||
|
||||
// 일반 견적서와 장비 견적서 모두 ESTIMATE_TEMPLATE_ITEM에서 조회
|
||||
Map itemParam = new HashMap();
|
||||
itemParam.put("templateObjId", template.get("objid"));
|
||||
itemParam.put("templateObjId", template.get("OBJID"));
|
||||
List<Map> templateItems = sqlSession.selectList("contractMgmt.getEstimateTemplateItemsByTemplateObjId", itemParam);
|
||||
|
||||
// 3. 수주등록 형식으로 변환
|
||||
// 수주등록 형식으로 변환
|
||||
for(Map templateItem : templateItems) {
|
||||
Map orderItem = new HashMap();
|
||||
|
||||
@@ -2202,39 +2620,36 @@ private String encodeImageToBase64(String imagePath) {
|
||||
//paramMap.put("contract_price", contract_price/project_cnt + "");
|
||||
|
||||
if("0000964".equals(result_cd) || "0000968".equals(result_cd)){
|
||||
// 품목별로 프로젝트 생성
|
||||
// CONTRACT_OBJID로 프로젝트 존재 여부 확인 (한 번만 체크)
|
||||
resultList = sqlSession.selectOne("contractMgmt.getProjectListBycontractObjid", paramMap);
|
||||
boolean hasProject = (resultList != null);
|
||||
|
||||
// 품목별로 프로젝트 생성 또는 업데이트
|
||||
List<Map> contractItems = getContractItems(paramMap);
|
||||
|
||||
if(contractItems != null && !contractItems.isEmpty()) {
|
||||
System.out.println("품목 개수: " + contractItems.size() + "개 - 품목별 프로젝트 생성 시작");
|
||||
System.out.println("품목 개수: " + contractItems.size() + "개 - 프로젝트 " + (hasProject ? "업데이트" : "생성") + " 시작");
|
||||
|
||||
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"));
|
||||
|
||||
if("0000170".equals(category_cd) || "0000171".equals(category_cd)){
|
||||
projectParam.put("overhaul_project_no", target_project_no);
|
||||
}
|
||||
|
||||
System.out.println("프로젝트 생성 - PART_OBJID: " + item.get("PART_OBJID") + ", 품번: " + item.get("PART_NO") + ", 품명: " + item.get("PART_NAME"));
|
||||
for(Map item : contractItems) {
|
||||
if(!hasProject) {
|
||||
// 프로젝트가 없으면 모든 품목에 대해 생성
|
||||
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"));
|
||||
|
||||
if("0000170".equals(category_cd) || "0000171".equals(category_cd)){
|
||||
projectParam.put("overhaul_project_no", target_project_no);
|
||||
}
|
||||
|
||||
System.out.println("프로젝트 생성 - PART_OBJID: " + item.get("PART_OBJID") + ", 품번: " + item.get("PART_NO") + ", 품명: " + item.get("PART_NAME"));
|
||||
|
||||
// 프로젝트 등록
|
||||
cnt = sqlSession.update("project.createProject", projectParam);
|
||||
@@ -2259,25 +2674,22 @@ private String encodeImageToBase64(String imagePath) {
|
||||
}
|
||||
}
|
||||
}
|
||||
} 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 {
|
||||
// 프로젝트가 있으면 모든 품목 업데이트 (수량, 금액 등만)
|
||||
Map<String, Object> updateParam = new HashMap<String, Object>();
|
||||
updateParam.putAll(paramMap);
|
||||
updateParam.put("part_objid", item.get("PART_OBJID"));
|
||||
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") + ", 수량: " + updateParam.get("quantity"));
|
||||
sqlSession.update("project.ModifyProjectByContract", updateParam);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
System.out.println("품목이 없습니다 - 기존 방식으로 프로젝트 생성");
|
||||
// 품목이 없는 경우 기존 방식대로 처리
|
||||
resultList = sqlSession.selectOne("contractMgmt.getProjectListBycontractObjid", paramMap);
|
||||
if(null==resultList){
|
||||
if(!hasProject){
|
||||
paramMap.put("OBJID", CommonUtils.createObjId());
|
||||
paramMap.put("is_temp", '1');
|
||||
if("0000170".equals(category_cd) || "0000171".equals(category_cd)){
|
||||
@@ -2668,4 +3080,213 @@ private String encodeImageToBase64(String imagePath) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 계약 정보 조회 (메일 발송용)
|
||||
* @param paramMap - objId (CONTRACT_OBJID)
|
||||
* @return
|
||||
*/
|
||||
public Map getContractInfoForMail(Map paramMap) {
|
||||
SqlSession sqlSession = null;
|
||||
Map contractInfo = null;
|
||||
|
||||
try {
|
||||
sqlSession = SqlMapConfig.getInstance().getSqlSession();
|
||||
contractInfo = (Map) sqlSession.selectOne("contractMgmt.getContractInfoForMail", paramMap);
|
||||
} catch(Exception e) {
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
if(sqlSession != null) sqlSession.close();
|
||||
}
|
||||
|
||||
return contractInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* 견적서 메일 발송 (커스텀)
|
||||
* @param request
|
||||
* @param paramMap - objId, pdfSessionId, toEmails, ccEmails, subject, contents
|
||||
* @return
|
||||
*/
|
||||
public Map sendEstimateMailCustom(HttpServletRequest request, Map paramMap) {
|
||||
Map resultMap = new HashMap();
|
||||
SqlSession sqlSession = null;
|
||||
|
||||
try {
|
||||
sqlSession = SqlMapConfig.getInstance().getSqlSession(false);
|
||||
|
||||
String objId = CommonUtils.checkNull(paramMap.get("objId"));
|
||||
String toEmailsStr = CommonUtils.checkNull(paramMap.get("toEmails"));
|
||||
String ccEmailsStr = CommonUtils.checkNull(paramMap.get("ccEmails"));
|
||||
String subject = CommonUtils.checkNull(paramMap.get("subject"));
|
||||
String contents = CommonUtils.checkNull(paramMap.get("contents"));
|
||||
String pdfSessionId = CommonUtils.checkNull(paramMap.get("pdfSessionId"));
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
// 3. 수신인 이메일 리스트 생성
|
||||
ArrayList<String> toEmailList = new ArrayList<String>();
|
||||
if(!"".equals(toEmailsStr)) {
|
||||
String[] toEmails = toEmailsStr.split(",");
|
||||
for(String email : toEmails) {
|
||||
String trimmedEmail = email.trim();
|
||||
if(!"".equals(trimmedEmail)) {
|
||||
toEmailList.add(trimmedEmail);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(toEmailList.isEmpty()) {
|
||||
resultMap.put("result", "error");
|
||||
resultMap.put("message", "수신인 이메일이 없습니다.");
|
||||
return resultMap;
|
||||
}
|
||||
|
||||
// 4. 참조 이메일 리스트 생성
|
||||
ArrayList<String> ccEmailList = new ArrayList<String>();
|
||||
|
||||
// 작성자 이메일 자동 추가
|
||||
String writerEmail = CommonUtils.checkNull(contractInfo.get("writer_email"));
|
||||
if("".equals(writerEmail)) writerEmail = CommonUtils.checkNull(contractInfo.get("WRITER_EMAIL"));
|
||||
if(!"".equals(writerEmail)) {
|
||||
ccEmailList.add(writerEmail);
|
||||
}
|
||||
|
||||
// 사용자가 입력한 참조 이메일 추가
|
||||
if(!"".equals(ccEmailsStr)) {
|
||||
String[] ccEmails = ccEmailsStr.split(",");
|
||||
for(String email : ccEmails) {
|
||||
String trimmedEmail = email.trim();
|
||||
if(!"".equals(trimmedEmail) && !ccEmailList.contains(trimmedEmail)) {
|
||||
ccEmailList.add(trimmedEmail);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 5. PDF 파일 처리
|
||||
ArrayList<HashMap> attachFileList = new ArrayList<HashMap>();
|
||||
if(!"".equals(pdfSessionId)) {
|
||||
File pdfFile = getPdfFromSession(pdfSessionId, estimateTemplate);
|
||||
if(pdfFile != null && pdfFile.exists()) {
|
||||
HashMap<String, String> fileMap = new HashMap<String, String>();
|
||||
fileMap.put(Constants.Db.COL_FILE_REAL_NAME, pdfFile.getName());
|
||||
fileMap.put(Constants.Db.COL_FILE_SAVED_NAME, pdfFile.getName());
|
||||
fileMap.put(Constants.Db.COL_FILE_PATH, pdfFile.getParent());
|
||||
attachFileList.add(fileMap);
|
||||
|
||||
System.out.println("PDF 파일 첨부 완료: " + pdfFile.getAbsolutePath());
|
||||
} else {
|
||||
System.out.println("PDF 파일을 찾을 수 없습니다: " + pdfSessionId);
|
||||
}
|
||||
}
|
||||
|
||||
// 6. 메일 발송
|
||||
PersonBean person = (PersonBean)request.getSession().getAttribute(Constants.PERSON_BEAN);
|
||||
String fromUserId = person.getUserId();
|
||||
|
||||
// mail_log 조회용 제목 (OBJID 포함)
|
||||
String subjectForLog = subject + " [OBJID:" + objId + "]";
|
||||
|
||||
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("Subject (mail_log): " + subjectForLog);
|
||||
System.out.println("첨부파일 개수: " + attachFileList.size());
|
||||
System.out.println("================================");
|
||||
|
||||
// HTML 형식으로 내용 변환 (줄바꿈 처리)
|
||||
String htmlContents = contents.replace("\n", "<br/>");
|
||||
|
||||
boolean mailSent = false;
|
||||
try {
|
||||
mailSent = MailUtil.sendMailWithAttachFileUTF8(
|
||||
fromUserId,
|
||||
null, // fromEmail (자동으로 SMTP 설정 사용)
|
||||
new ArrayList<String>(), // toUserIdList (빈 리스트)
|
||||
toEmailList,
|
||||
ccEmailList,
|
||||
new ArrayList<String>(), // bccEmailList (빈 리스트)
|
||||
null, // important
|
||||
subject, // 실제 메일 제목 (OBJID 없음)
|
||||
htmlContents,
|
||||
attachFileList.size() > 0 ? attachFileList : null,
|
||||
"CONTRACT_ESTIMATE",
|
||||
subjectForLog // mail_log용 제목 (OBJID 포함)
|
||||
);
|
||||
|
||||
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 paramMap - objId (contractObjId)
|
||||
* @return 프로젝트 정보 (없으면 null)
|
||||
*/
|
||||
@SuppressWarnings("rawtypes")
|
||||
public Map checkProjectExists(Map paramMap) {
|
||||
SqlSession sqlSession = null;
|
||||
Map projectInfo = null;
|
||||
|
||||
try {
|
||||
// autoCommit = true로 세션 생성
|
||||
sqlSession = SqlMapConfig.getInstance().getSqlSession(true);
|
||||
|
||||
projectInfo = sqlSession.selectOne("contractMgmt.getProjectListBycontractObjid", paramMap);
|
||||
|
||||
if(projectInfo != null) {
|
||||
projectInfo = CommonUtils.keyChangeUpperMap(projectInfo);
|
||||
}
|
||||
|
||||
} catch(Exception e) {
|
||||
System.out.println("❌ Service 오류: " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
if(sqlSession != null) sqlSession.close();
|
||||
}
|
||||
|
||||
System.out.println("Service 최종 반환값: " + projectInfo);
|
||||
return projectInfo;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user