Compare commits

..

19 Commits

Author SHA1 Message Date
86bd16ff77 영업정보 수정시 프로젝트 정보 업데이트 수정 2025-11-13 17:44:22 +09:00
dcef82cdca 영업번호 팝업창 수정 2025-11-13 17:30:14 +09:00
6a97bf72e1 수주 (프로젝트 생성) 후 품목 추가, 삭제, 변경 불가! 2025-11-13 16:36:01 +09:00
b761e511ff 공급업체->메이커, EO관련컬럼 주석 2025-11-13 13:31:06 +09:00
bf0e08d90b 화면 수정 2025-11-13 12:24:54 +09:00
333d05d19f E-bom 조회 레벨 검색 추가 2025-11-13 12:24:33 +09:00
031ce7615d ㅇㅇ 2025-11-13 09:56:55 +09:00
cf79338b55 fix: Use official Python image for backup container
- Change from dockerhub.wace.me/python:3.10-slim.linux to python:3.10-slim
- Fix Docker image not found error
2025-11-12 18:46:22 +09:00
ba026842f7 feat: Add database backup system
- Add Dockerfile.backup for backup container
- Add backup.py script with PostgreSQL backup functionality
- Add backup service to docker-compose.prod.yml
- Update env.production.example with backup configuration
- Add db/README.md with backup system documentation

Features:
- Automated daily backups (07:30, 18:00)
- Local and FTP remote backup support
- 7-day retention policy
- PostgreSQL 16 client for waceplm database
2025-11-12 18:19:54 +09:00
8cccd9db2c 화면 수정 2025-11-11 18:09:37 +09:00
da883f0007 수주등록 견적서 내용 기본입력되도록 수정 2025-11-11 16:47:29 +09:00
leeheejin
2bcc39ff65 Merge branch 'V2025111104' 2025-11-11 14:49:26 +09:00
leeheejin
0b291c4ea2 오류수정 2025-11-11 14:48:22 +09:00
0817253285 Merge pull request 'feature/estimate-template-improvements' (#59) from feature/estimate-template-improvements into main
Reviewed-on: #59
2025-11-11 05:14:16 +00:00
7ae57f9719 장비견적서 템플릿 수정, pdf 변환 메일 발송 2025-11-11 14:09:11 +09:00
a9836b599b 메일작성 변경 2025-11-11 14:09:11 +09:00
9aafeb3def 견적서 기본 템플릿 수정 2025-11-11 14:09:11 +09:00
c89266ec0f Merge pull request 'V2025111101' (#58) from V2025111101 into main
Reviewed-on: #58
2025-11-11 05:00:17 +00:00
leeheejin
03e59664ab auto commit 2025-11-11 13:52:56 +09:00
32 changed files with 4812 additions and 1009 deletions

View File

@@ -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
View 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"]

View File

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

View File

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

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

View File

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

View File

@@ -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;">&lt;비고&gt;</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;">&lt;참조사항&gt;</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

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

View File

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

View File

@@ -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 || '') + '" />';

View File

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

View File

@@ -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: '파일 삭제 중 오류가 발생했습니다.'
});
}
});
}
});
}

View File

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

View File

@@ -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}">
품목 등록

View File

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

View File

@@ -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" // 다중 선택 가능하도록 설정

View File

@@ -33,6 +33,18 @@
// 판매환종 초기값 설정 (견적환종과 동기화)
initializeSalesCurrency();
// 페이지 로드 시 금액 자동 계산 (수주 데이터가 있을 때)
setTimeout(function() {
var quantity = parseFloat($("#salesQuantity").val()) || 0;
var unitPrice = parseFloat($("#salesUnitPrice").val()) || 0;
// 수주 데이터가 있으면 자동 계산
if(quantity > 0 || unitPrice > 0) {
fn_calculateSupplyPrice();
console.log("페이지 로드 시 금액 자동 계산 완료");
}
}, 500);
// S/N 필드 클릭 이벤트
$("#serialNo").click(function() {
fn_openSnManagePopup();
@@ -681,7 +693,7 @@
<tr>
<td class="input_title"><label for="salesVat">판매부가세</label></td>
<td>
<input type="number" name="salesVat" id="salesVat" value="${saleInfo.SALES_VAT}" readonly style="background-color:#f5f5f5;" />
<input type="number" name="salesVat" id="salesVat" value="${saleInfo.SALES_VAT}" onchange="fn_calculateTotalAmount()" />
</td>
<td class="input_title"><label for="salesTotalAmount">판매총액</label></td>
<td>

111
db/README.md Normal file
View 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
View 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

View File

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

View File

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

View File

@@ -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;
}
}
/**
* 첨부파일 목록을 파일하나로 압축해서 반환

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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