영업관리_견적관리 결재상신 아마란스 연동(추가 테스트 필요) #149

Merged
hjjeong merged 1 commits from V20260210 into main 2026-02-25 03:39:04 +00:00
6 changed files with 536 additions and 104 deletions

View File

@@ -151,33 +151,37 @@ $(document).ready(function(){
document.form1.submit();
});
//결재상신
// 결재상신 (Amaranth10 전자결재 SSO 방식)
$("#btnApproval").click(function(){
var selectedData = _tabulGrid.getSelectedData();
if(selectedData.length<1){
if(selectedData.length < 1){
Swal.fire("결재상신할 행을 선택해주십시오.");
return false;
}else if(selectedData.length>1){
} else if(selectedData.length > 1){
Swal.fire("한번에 한개의 결재만 가능합니다.");
return false;
}else{
var targetStatus = fnc_checkNull(selectedData[0].APPR_STATUS);
var status = fnc_checkNull(selectedData[0].STATUS);
} else {
var amaranthStatus = fnc_checkNull(selectedData[0].AMARANTH_STATUS);
var estObjId = fnc_checkNull(selectedData[0].EST_OBJID);
var contractObjId = fnc_checkNull(selectedData[0].OBJID);
var customerObjId = fnc_checkNull(selectedData[0].CUSTOMER_OBJID);
// 견적서 작성 여부 확인
if(estObjId == ""){
Swal.fire("견적서를 먼저 작성해주세요.");
return false;
}
if(targetStatus == "결재완료" || targetStatus == "결재중" || targetStatus == "결재불필요" || status == "cancel"){
Swal.fire("작성중/결재반려인 상태만 결재상신 가능합니다.");
if(amaranthStatus == "inProcess"){
Swal.fire("결재 진행중인 건은 상신할 수 없습니다.");
return false;
}else{
} else if(amaranthStatus == "complete"){
Swal.fire("결재 완료된 건은 상신할 수 없습니다.");
return false;
} else if(amaranthStatus == "notRequired"){
Swal.fire("결재불필요로 처리된 건입니다.");
return false;
}
// 결재 필요 여부 체크 (재오더/신규수주/가격인하)
$.ajax({
url: "/contractMgmt/checkApprovalRequired.do",
@@ -191,9 +195,8 @@ $(document).ready(function(){
if(response.result == "success") {
var approvalRequired = response.approvalRequired;
var reason = response.reason;
if(approvalRequired == "N") {
// 재오더 + 가격동일/인상 → 결재불필요
Swal.fire({
title: '결재 불필요',
html: '재오더(가격동일/인상)로 결재가 필요하지 않습니다.<br>결재불필요로 처리하시겠습니까?<br><br><small>* 결재불필요 처리 후 메일발송이 가능합니다.</small>',
@@ -205,13 +208,10 @@ $(document).ready(function(){
cancelButtonText: '취소'
}).then((result) => {
if(result.isConfirmed) {
// 결재불필요 처리
$.ajax({
url: "/contractMgmt/setApprovalNotRequired.do",
type: "POST",
data: {
estObjId: estObjId
},
data: { estObjId: estObjId },
dataType: "json",
success: function(res) {
if(res.result == "success") {
@@ -223,7 +223,7 @@ $(document).ready(function(){
confirmButtonText: "메일발송",
cancelButtonText: "나중에"
}).then((mailResult) => {
fn_search(); // 목록 새로고침
fn_search();
if(mailResult.isConfirmed) {
fn_openMailFormPopup(contractObjId);
}
@@ -239,14 +239,10 @@ $(document).ready(function(){
}
});
} else {
// 신규수주 또는 가격인하 → 결재필요
var reasonText = "";
if(reason == "신규수주") {
reasonText = "신규수주입니다.";
} else if(reason == "가격인하") {
reasonText = "가격인하 건입니다.";
}
if(reason == "신규수주") reasonText = "신규수주입니다.";
else if(reason == "가격인하") reasonText = "가격인하 건입니다.";
Swal.fire({
title: '결재상신',
html: (reasonText ? reasonText + '<br>' : '') + '결재상신 하시겠습니까?<br><br><small>* 결재완료 후 메일발송이 가능합니다.</small>',
@@ -258,24 +254,18 @@ $(document).ready(function(){
cancelButtonText: '취소'
}).then((result) => {
if(result.isConfirmed) {
var objId = estObjId;
var title = encodeURIComponent(fnc_checkNull(selectedData[0].CONTRACT_NO));
var approvalUrl = "/approval/registApproval.do?targetType=CONTRACT_ESTIMATE&targetObjId="+objId+"&approvalTitle="+title;
window.open(approvalUrl, "registApproval", "width=700,height=700");
fn_openAmaranthApproval(estObjId, fnc_checkNull(selectedData[0].CONTRACT_NO));
}
});
}
} else {
// API 오류 시 기존 방식으로 진행
fn_showApprovalConfirmSimple(estObjId, selectedData[0].CONTRACT_NO);
}
},
error: function() {
// AJAX 오류 시 기존 방식으로 진행
fn_showApprovalConfirmSimple(estObjId, selectedData[0].CONTRACT_NO);
}
});
}
}
});
@@ -427,15 +417,19 @@ var columns = [
fn_showEstimateList(objid);
}
},
// 12. 결재상태
{headerHozAlign : 'center', hozAlign : 'center', minWidth: 50, widthGrow: 0.7, title : '결재상태', field : 'APPR_STATUS',
formatter:fnc_createGridAnchorTag,
cellClick:function(e, cell){
var APPROVAL_OBJID = fnc_checkNull(cell.getData().APPROVAL_OBJID);
var ROUTE_OBJID = fnc_checkNull(cell.getData().ROUTE_OBJID);
approval_form(APPROVAL_OBJID,ROUTE_OBJID);
}
},
// 12. 아마란스 결재상태 (hidden)
{title:'AMARANTH_STATUS', field:'AMARANTH_STATUS', visible: false},
// 13. 결재상태 (아마란스 전자결재)
{headerHozAlign : 'center', hozAlign : 'center', minWidth: 50, widthGrow: 0.7, title : '결재상태', field : 'APPR_STATUS',
formatter:function(cell){
var val = fnc_checkNull(cell.getValue());
if(val == '결재중') return "<span style='color:#e67e22; font-weight:bold;'>" + val + "</span>";
if(val == '결재완료') return "<span style='color:#27ae60; font-weight:bold;'>" + val + "</span>";
if(val == '반려') return "<span style='color:#e74c3c; font-weight:bold;'>" + val + "</span>";
if(val == '결재불필요') return "<span style='color:#3498db; font-weight:bold;'>" + val + "</span>";
return val;
}
},
// 13. 메일발송
{headerHozAlign : 'center', hozAlign : 'center', minWidth: 50, widthGrow: 0.7, title : '메일발송', field : 'MAIL_SEND_STATUS',
formatter: function(cell, formatterParams, onRendered){
@@ -634,12 +628,6 @@ function fn_delete(){
}
}
function approval_form(APPROVAL_OBJID,ROUTE_OBJID){
url = "/approval/approvalDetail.do?approvalObjId="+APPROVAL_OBJID+"&routeObjId="+ROUTE_OBJID;
fn_centerPopup(650,400,url,'approvalDetailPopup')
}
function fn_FileRegist(objId, docType, docTypeName){
var popup_width = 800;
@@ -825,7 +813,7 @@ function fn_showSerialNoPopup(serialNoString){
});
}
// 결재상신 확인 다이얼로그 (단순 버전)
// 결재상신 확인 다이얼로그 (단순 버전 - 아마란스 전자결재)
function fn_showApprovalConfirmSimple(estObjId, contractNo) {
Swal.fire({
title: '결재상신',
@@ -838,9 +826,39 @@ function fn_showApprovalConfirmSimple(estObjId, contractNo) {
cancelButtonText: '취소'
}).then((result) => {
if(result.isConfirmed) {
var title = encodeURIComponent(fnc_checkNull(contractNo));
var approvalUrl = "/approval/registApproval.do?targetType=CONTRACT_ESTIMATE&targetObjId="+estObjId+"&approvalTitle="+title;
window.open(approvalUrl, "registApproval", "width=700,height=700");
fn_openAmaranthApproval(estObjId, contractNo);
}
});
}
// Amaranth10 전자결재 SSO 팝업 열기
function fn_openAmaranthApproval(estObjId, contractNo) {
var title = "견적서 결재" + (contractNo ? " - " + contractNo : "");
$.ajax({
url: "/approval/getAmaranthSsoUrl.do",
type: "POST",
data: {
"targetType": "CONTRACT_ESTIMATE",
"targetObjId": estObjId,
"approvalTitle": title,
"outProcessCode": "${AMARANTH_OUT_PROCESS_CODE}",
"formId": "",
"compSeq": "1000",
"deptSeq": ""
},
dataType: "json",
success: function(data) {
if(data.resultCode == 0 && data.resultData && data.resultData.fullUrl) {
var fullUrl = data.resultData.fullUrl;
window.open(fullUrl, "amaranthApproval", "width=1200,height=900,scrollbars=yes,resizable=yes");
} else {
Swal.fire("결재 연동 오류: " + (data.resultMsg || "알 수 없는 오류"));
}
},
error: function(xhr, status, error) {
console.error("Amaranth SSO URL 요청 오류:", error);
Swal.fire("결재 시스템 연동 중 오류가 발생했습니다.");
}
});
}

View File

@@ -603,7 +603,7 @@
SELECT
CONTRACT_OBJID
FROM ESTIMATE_TEMPLATE
WHERE OBJID = #{estObjId}::NUMERIC
WHERE OBJID::VARCHAR = #{estObjId}
</select>
<!-- =====================================================
@@ -671,6 +671,23 @@
WHERE APPRO_KEY = #{approKey}
</update>
<!-- 결재불필요 처리: AMARANTH_APPROVAL에 notRequired 상태로 등록 -->
<insert id="insertAmaranthApprovalNotRequired" parameterType="map">
INSERT INTO AMARANTH_APPROVAL (
APPRO_KEY, TARGET_TYPE, TARGET_OBJID, APPROVAL_TITLE, WRITER, STATUS, REGDATE
) VALUES (
#{approKey}, #{targetType}, #{targetObjId}, '결재불필요', #{writer}, 'notRequired', NOW()
)
</insert>
<!-- 결재불필요 처리: 기존 AMARANTH_APPROVAL 레코드 상태 변경 -->
<update id="updateAmaranthApprovalNotRequired" parameterType="map">
UPDATE AMARANTH_APPROVAL SET
STATUS = 'notRequired',
UPDATE_DATE = NOW()
WHERE TARGET_OBJID = #{targetObjId} AND TARGET_TYPE = #{targetType}
</update>
<!-- 품의서(SALES_REQUEST_MASTER) 결재 상태 변경 -->
<update id="changeProposalApprovalStatus" parameterType="map">
UPDATE SALES_REQUEST_MASTER SET
@@ -678,4 +695,57 @@
WHERE OBJID::VARCHAR = #{targetObjId}::VARCHAR
</update>
<!-- 견적서 정보 조회 (결재 본문용) -->
<select id="getEstimateInfoForApproval" parameterType="map" resultType="map">
SELECT
ET.OBJID,
ET.CONTRACT_OBJID,
ET.ESTIMATE_NO,
ET.TEMPLATE_TYPE,
ET.EXECUTOR,
ET.RECIPIENT,
ET.CONTACT_PERSON,
ET.MODEL_NAME,
ET.TOTAL_AMOUNT,
ET.TOTAL_AMOUNT_KRW,
ET.MANAGER_NAME,
ET.MANAGER_CONTACT,
ET.PART_NAME,
ET.WRITER,
ET.NOTE1,
ET.NOTE2,
ET.NOTE3,
ET.NOTE4,
ET.NOTE_REMARKS,
ET.VALIDITY_PERIOD,
TO_CHAR(ET.REGDATE, 'YYYY-MM-DD') AS REGDATE,
CM.CONTRACT_NO,
(SELECT CLIENT_NM FROM CLIENT_MNG AS C WHERE 'C_' || C.OBJID::VARCHAR = CM.CUSTOMER_OBJID) AS CUSTOMER_NAME,
CM.AREA_CD,
CODE_NAME(CM.CONTRACT_CURRENCY) AS CURRENCY_NAME,
CM.EXCHANGE_RATE,
(SELECT DEPT_NAME || ' ' || USER_NAME FROM USER_INFO WHERE USER_ID = ET.WRITER) AS WRITER_NAME
FROM ESTIMATE_TEMPLATE ET
LEFT JOIN CONTRACT_MGMT CM ON ET.CONTRACT_OBJID = CM.OBJID
WHERE ET.OBJID::VARCHAR = #{targetObjId}
</select>
<!-- 견적서 품목 리스트 조회 (결재 본문용) -->
<select id="getEstimateItemsForApproval" parameterType="map" resultType="map">
SELECT
SEQ,
CATEGORY,
DESCRIPTION,
SPECIFICATION,
QUANTITY,
UNIT,
UNIT_PRICE,
AMOUNT,
NOTE,
REMARK
FROM ESTIMATE_TEMPLATE_ITEM
WHERE TEMPLATE_OBJID::VARCHAR = #{targetObjId}
ORDER BY SEQ
</select>
</mapper>

View File

@@ -1659,7 +1659,7 @@ public class ContractMgmtController {
request.setAttribute("code_map",code_map);
request.setAttribute("actionType",actionType);
//request.setAttribute("LIST", list);
request.setAttribute("AMARANTH_OUT_PROCESS_CODE", com.pms.common.utils.Constants.AMARANTH_OUT_PROCESS_CODE);
}catch(Exception e){
e.printStackTrace();

View File

@@ -423,7 +423,7 @@
<sql id="contractBase">
(
SELECT
OBJID
T.OBJID
,CATEGORY_CD
,CODE_NAME(CATEGORY_CD) AS CATEGORY_NAME
,CUSTOMER_OBJID
@@ -494,9 +494,9 @@
,CONTRACT_PRICE_CURRENCY
,CONTRACT_CURRENCY
,CODE_NAME(CONTRACT_CURRENCY) AS CONTRACT_CURRENCY_NAME
,REGDATE
,TO_CHAR(REGDATE,'YYYY-MM-DD') AS REG_DATE
,WRITER
,T.REGDATE
,TO_CHAR(T.REGDATE,'YYYY-MM-DD') AS REG_DATE
,T.WRITER
,(SELECT USER_NAME FROM USER_INFO AS O WHERE O.USER_ID = T.WRITER ) AS WRITER_NAME
,(SELECT COUNT(1) FROM ATTACH_FILE_INFO WHERE TARGET_OBJID = T.OBJID AND DOC_TYPE IN ('FTC_ORDER', 'ORDER') AND UPPER(STATUS) = 'ACTIVE') AS CU01_CNT
<!-- ,(SELECT COUNT(1) FROM ATTACH_FILE_INFO WHERE TARGET_OBJID = T.OBJID AND DOC_TYPE='contractMgmt01' AND UPPER(STATUS) = 'ACTIVE') AS CU01_CNT
@@ -555,10 +555,18 @@
ORDER BY LOG_TIME DESC
LIMIT 1
) AS MAIL_SEND_DATE
,A.APPR_STATUS
,A.APPROVAL_OBJID
,A.ROUTE_OBJID
,(SELECT objid FROM ESTIMATE_TEMPLATE WHERE CONTRACT_OBJID = T.OBJID order by regdate desc limit 1) AS EST_OBJID
,CASE
WHEN AMR.STATUS = 'complete' THEN '결재완료'
WHEN AMR.STATUS = 'inProcess' THEN '결재중'
WHEN AMR.STATUS = 'reject' THEN '반려'
WHEN AMR.STATUS = 'create' THEN '작성중'
WHEN AMR.STATUS = 'notRequired' THEN '결재불필요'
ELSE ''
END AS APPR_STATUS
,COALESCE(AMR.STATUS, '') AS AMARANTH_STATUS
,A.APPROVAL_OBJID
,A.ROUTE_OBJID
,(SELECT objid FROM ESTIMATE_TEMPLATE WHERE CONTRACT_OBJID = T.OBJID order by regdate desc limit 1) AS EST_OBJID
-- 최근 차수 견적서 합계 정보
,(SELECT TOTAL_AMOUNT FROM ESTIMATE_TEMPLATE WHERE CONTRACT_OBJID = T.OBJID ORDER BY REGDATE DESC LIMIT 1) AS EST_TOTAL_AMOUNT
,(SELECT TOTAL_AMOUNT_KRW FROM ESTIMATE_TEMPLATE WHERE CONTRACT_OBJID = T.OBJID ORDER BY REGDATE DESC LIMIT 1) AS EST_TOTAL_AMOUNT_KRW
@@ -731,10 +739,13 @@
WHERE
A.OBJID = B.APPROVAL_OBJID
AND TARGET_TYPE IN ('CONTRACT_ESTIMATE')
) A
ON (SELECT objid FROM ESTIMATE_TEMPLATE WHERE CONTRACT_OBJID = T.OBJID order by regdate desc limit 1)::numeric = A.TARGET_OBJID
)
</sql>
) A
ON (SELECT objid FROM ESTIMATE_TEMPLATE WHERE CONTRACT_OBJID = T.OBJID order by regdate desc limit 1)::numeric = A.TARGET_OBJID
LEFT OUTER JOIN AMARANTH_APPROVAL AMR
ON (SELECT objid FROM ESTIMATE_TEMPLATE WHERE CONTRACT_OBJID = T.OBJID order by regdate desc limit 1)::VARCHAR = AMR.TARGET_OBJID
AND AMR.TARGET_TYPE = 'CONTRACT_ESTIMATE'
)
</sql>
<!-- //계약관리 리스트 -->
<select id="contractList" parameterType="map" resultType="map">
@@ -5659,15 +5670,15 @@ WHERE
WITH current_items AS (
SELECT
ETI.PART_OBJID,
COALESCE(PM.PART_NO, ETI.PART_NO) AS PART_NO,
CAST(COALESCE(NULLIF(ETI.SUPPLY_UNIT_PRICE, ''), '0') AS NUMERIC) AS CURRENT_PRICE
PM.PART_NO,
CAST(COALESCE(NULLIF(ETI.UNIT_PRICE, ''), '0') AS NUMERIC) AS CURRENT_PRICE
FROM ESTIMATE_TEMPLATE ET
INNER JOIN ESTIMATE_TEMPLATE_ITEM ETI ON ET.OBJID = ETI.TEMPLATE_OBJID
LEFT JOIN PART_MNG PM ON ETI.PART_OBJID = PM.OBJID
WHERE ET.CONTRACT_OBJID = #{contractObjId}::NUMERIC
WHERE ET.CONTRACT_OBJID::VARCHAR = #{contractObjId}
AND ET.OBJID = (
SELECT OBJID FROM ESTIMATE_TEMPLATE
WHERE CONTRACT_OBJID = #{contractObjId}::NUMERIC
WHERE CONTRACT_OBJID::VARCHAR = #{contractObjId}
ORDER BY REGDATE DESC LIMIT 1
)
),
@@ -5675,16 +5686,16 @@ WHERE
SELECT
CI.PART_OBJID,
COALESCE(PM.PART_NO, CI.PART_NO) AS PART_NO,
MAX(CAST(COALESCE(NULLIF(ETI.SUPPLY_UNIT_PRICE, ''), '0') AS NUMERIC)) AS PREV_MAX_PRICE
MAX(CAST(COALESCE(NULLIF(ETI.UNIT_PRICE, ''), '0') AS NUMERIC)) AS PREV_MAX_PRICE
FROM CONTRACT_MGMT CM
INNER JOIN CONTRACT_ITEM CI ON CM.OBJID = CI.CONTRACT_OBJID AND CI.STATUS = 'ACTIVE'
LEFT JOIN PART_MNG PM ON CI.PART_OBJID = PM.OBJID
LEFT JOIN ESTIMATE_TEMPLATE ET ON ET.CONTRACT_OBJID = CM.OBJID
LEFT JOIN ESTIMATE_TEMPLATE_ITEM ETI ON ET.OBJID = ETI.TEMPLATE_OBJID
AND (ETI.PART_OBJID = CI.PART_OBJID OR ETI.PART_NO = CI.PART_NO)
AND ETI.PART_OBJID = CI.PART_OBJID
WHERE CM.CUSTOMER_OBJID = #{customerObjId}
AND CM.OBJID != #{contractObjId}::NUMERIC
AND CM.STATUS != 'cancel'
AND CM.OBJID::VARCHAR != #{contractObjId}
AND CM.STATUS_CD != 'cancel'
AND CM.CONTRACT_RESULT = '0000964'
GROUP BY CI.PART_OBJID, COALESCE(PM.PART_NO, CI.PART_NO)
)

View File

@@ -3766,43 +3766,35 @@ private String encodeImageToBase64(String imagePath) {
/**
* 결재불필요 처리 (재오더 + 가격동일/인상인 경우)
* APPROVAL 테이블에 결재불필요 상태로 레코드 생성
* AMARANTH_APPROVAL 테이블에 notRequired 상태로 등록/갱신
* @param paramMap - estObjId, userId
*/
@SuppressWarnings({ "rawtypes", "unchecked" })
public void setApprovalNotRequired(Map paramMap) throws Exception {
SqlSession sqlSession = null;
try {
sqlSession = SqlMapConfig.getInstance().getSqlSession(false);
String estObjId = CommonUtils.checkNull(paramMap.get("estObjId"));
String userId = CommonUtils.checkNull(paramMap.get("userId"));
// APPROVAL 테이블에 결재불필요 레코드 생성
Map<String, Object> approvalParam = new HashMap<String, Object>();
approvalParam.put("OBJID", CommonUtils.createObjId());
approvalParam.put("TARGET_TYPE", "CONTRACT_ESTIMATE");
approvalParam.put("TARGET_OBJID", estObjId);
approvalParam.put("APPROVAL_TITLE", "결재불필요");
approvalParam.put("WRITER", userId);
sqlSession.insert("contractMgmt.insertApprovalNotRequired", approvalParam);
// ROUTE 테이블에 결재불필요 상태 레코드 생성
Map<String, Object> routeParam = new HashMap<String, Object>();
routeParam.put("OBJID", CommonUtils.createObjId());
routeParam.put("APPROVAL_OBJID", approvalParam.get("OBJID"));
routeParam.put("TARGET_OBJID", estObjId);
routeParam.put("TARGET_TYPE", "CONTRACT_ESTIMATE");
routeParam.put("STATUS", "notRequired"); // 결재불필요 상태
routeParam.put("ROUTE_SEQ", "1");
routeParam.put("WRITER", userId);
sqlSession.insert("contractMgmt.insertRouteNotRequired", routeParam);
Map<String, Object> queryParam = new HashMap<String, Object>();
queryParam.put("targetObjId", estObjId);
queryParam.put("targetType", "CONTRACT_ESTIMATE");
Map existing = sqlSession.selectOne("approval.selectAmaranthApprovalByTarget", queryParam);
if(existing != null) {
sqlSession.update("approval.updateAmaranthApprovalNotRequired", queryParam);
} else {
queryParam.put("approKey", "NOT_REQ_" + estObjId + "_" + System.currentTimeMillis());
queryParam.put("writer", userId);
sqlSession.insert("approval.insertAmaranthApprovalNotRequired", queryParam);
}
sqlSession.commit();
} catch(Exception e) {
if(sqlSession != null) sqlSession.rollback();
throw e;

View File

@@ -1837,10 +1837,14 @@ public class ApprovalService {
// API 클라이언트 생성
com.pms.api.AmaranthApprovalApiClient apiClient = new com.pms.api.AmaranthApprovalApiClient();
// 첨부파일 처리: ATTACH_FILE_INFO에서 품의서 파일 조회 → 원챔버 업로드
// 첨부파일 처리: targetType별 분기하여 원챔버 업로드
String fileListEncoded = null;
try {
fileListEncoded = uploadProposalFilesToOneChamber(apiClient, empSeq, targetObjId);
if("CONTRACT_ESTIMATE".equals(targetType)) {
fileListEncoded = uploadEstimateFilesToOneChamber(apiClient, empSeq, targetObjId);
} else {
fileListEncoded = uploadProposalFilesToOneChamber(apiClient, empSeq, targetObjId);
}
} catch(Exception fileEx) {
System.err.println("[첨부파일] 원챔버 업로드 중 오류 (결재는 계속 진행): " + fileEx.getMessage());
fileEx.printStackTrace();
@@ -2022,6 +2026,324 @@ public class ApprovalService {
}
}
/**
* 견적서 첨부파일을 원챔버에 업로드하고 fileList (URL 인코딩) 반환
* @return URL 인코딩된 fileList JSON 문자열 (파일 없으면 null)
*/
private String uploadEstimateFilesToOneChamber(
com.pms.api.AmaranthApprovalApiClient apiClient,
String empSeq, String targetObjId) throws Exception {
if(targetObjId == null || targetObjId.isEmpty()) return null;
SqlSession sqlSession = SqlMapConfig.getInstance().getSqlSession(true);
try {
Map<String, String> authResult = apiClient.getAuthToken(AMARANTH_BASE_URL, empSeq);
if(!"true".equals(authResult.get("success"))){
throw new Exception("파일 업로드용 인증 토큰 발급 실패: " + authResult.get("resultMsg"));
}
String authToken = authResult.get("authToken");
String userHashKey = authResult.get("hashKey");
StringBuilder listItems = new StringBuilder();
int uploadCount = 0;
// 1. 견적서 본문을 HTML 파일로 생성하여 업로드
try {
Map<String, Object> estParam = new HashMap<String, Object>();
estParam.put("targetObjId", targetObjId);
Map<String, Object> estimateInfo = sqlSession.selectOne("approval.getEstimateInfoForApproval", estParam);
if(estimateInfo != null){
estimateInfo = CommonUtils.toUpperCaseMapKey(estimateInfo);
String estimateNo = CommonUtils.checkNull(estimateInfo.get("ESTIMATE_NO"));
Map<String, Object> itemParam = new HashMap<String, Object>();
itemParam.put("targetObjId", targetObjId);
List<Map> itemList = sqlSession.selectList("approval.getEstimateItemsForApproval", itemParam);
String fullHtml = buildEstimateFormFileHtml(estimateInfo, itemList);
String tempFileName = "견적서_" + estimateNo + ".html";
java.io.File tempFile = java.io.File.createTempFile("estimate_", ".html");
java.io.OutputStreamWriter writer = new java.io.OutputStreamWriter(
new java.io.FileOutputStream(tempFile), "UTF-8");
writer.write(fullHtml);
writer.close();
System.out.println("[첨부파일] 견적서 HTML 생성: " + tempFileName + " (" + tempFile.length() + " bytes)");
String uploadResponse = apiClient.uploadFileToOneChamber(
AMARANTH_BASE_URL, authToken, userHashKey, empSeq, tempFile, tempFileName
);
String listItem = apiClient.extractListItemFromUploadResponse(uploadResponse);
if(listItem != null){
listItems.append(listItem);
uploadCount++;
System.out.println("[첨부파일] 견적서 HTML 업로드 성공");
}
tempFile.delete();
}
} catch(Exception htmlEx){
System.err.println("[첨부파일] 견적서 HTML 생성/업로드 오류: " + htmlEx.getMessage());
}
// 2. ATTACH_FILE_INFO에서 기존 첨부파일도 업로드
Map<String, Object> fileParam = new HashMap<String, Object>();
fileParam.put("targetObjId", targetObjId);
List<Map<String, Object>> fileList = sqlSession.selectList("common.getFileList", fileParam);
if(fileList != null && !fileList.isEmpty()){
System.out.println("[첨부파일] 견적서(" + targetObjId + ") 기존 첨부파일 " + fileList.size() + "건 발견");
for(Map<String, Object> fileInfo : fileList){
String savedFileName = CommonUtils.checkNull(fileInfo.get("saved_file_name"));
String realFileName = CommonUtils.checkNull(fileInfo.get("real_file_name"));
String filePath = CommonUtils.checkNull(fileInfo.get("file_path"));
if(savedFileName.isEmpty()) continue;
String fullPath = filePath + "/" + savedFileName;
java.io.File physicalFile = new java.io.File(fullPath);
if(!physicalFile.exists()){
System.err.println("[첨부파일] 파일 미존재: " + fullPath);
continue;
}
String originalName = realFileName.isEmpty() ? savedFileName : realFileName;
String uploadResponse = apiClient.uploadFileToOneChamber(
AMARANTH_BASE_URL, authToken, userHashKey, empSeq, physicalFile, originalName
);
String listItem = apiClient.extractListItemFromUploadResponse(uploadResponse);
if(listItem != null){
if(uploadCount > 0) listItems.append(",");
listItems.append(listItem);
uploadCount++;
System.out.println("[첨부파일] 업로드 성공: " + originalName);
}
}
}
if(uploadCount == 0) return null;
String fileListJson = "[" + listItems.toString() + "]";
String fileListEncoded = java.net.URLEncoder.encode(fileListJson, "UTF-8");
System.out.println("[첨부파일] 견적서 총 " + uploadCount + "건 업로드 완료");
return fileListEncoded;
} finally {
if(sqlSession != null) sqlSession.close();
}
}
/**
* 견적서 데이터를 HTML 파일로 구성 (결재 첨부파일용)
*/
private String buildEstimateFormFileHtml(Map estimateInfo, List<Map> itemList){
String estimateNo = CommonUtils.checkNull(estimateInfo.get("ESTIMATE_NO"));
String contractNo = CommonUtils.checkNull(estimateInfo.get("CONTRACT_NO"));
String customerName = CommonUtils.checkNull(estimateInfo.get("CUSTOMER_NAME"));
String writerName = CommonUtils.checkNull(estimateInfo.get("WRITER_NAME"));
String regdate = CommonUtils.checkNull(estimateInfo.get("REGDATE"));
String totalAmount = CommonUtils.checkNull(estimateInfo.get("TOTAL_AMOUNT"));
String totalAmountKrw = CommonUtils.checkNull(estimateInfo.get("TOTAL_AMOUNT_KRW"));
String currencyName = CommonUtils.checkNull(estimateInfo.get("CURRENCY_NAME"));
String exchangeRate = CommonUtils.checkNull(estimateInfo.get("EXCHANGE_RATE"));
String recipient = CommonUtils.checkNull(estimateInfo.get("RECIPIENT"));
String contactPerson = CommonUtils.checkNull(estimateInfo.get("CONTACT_PERSON"));
String modelName = CommonUtils.checkNull(estimateInfo.get("MODEL_NAME"));
String validityPeriod = CommonUtils.checkNull(estimateInfo.get("VALIDITY_PERIOD"));
String noteRemarks = CommonUtils.checkNull(estimateInfo.get("NOTE_REMARKS"));
StringBuilder html = new StringBuilder();
html.append("<!DOCTYPE html><html><head><meta charset='UTF-8'>");
html.append("<title>견적서 - " + estimateNo + "</title>");
html.append("<style>body{font-family:'맑은 고딕',sans-serif;font-size:10pt;margin:20px;}");
html.append("table{border-collapse:collapse;width:100%;}");
html.append("th,td{border:1px solid #333;padding:6px 8px;font-size:9pt;}");
html.append("th{background-color:#4472C4;color:#fff;text-align:center;}");
html.append(".header{font-size:14pt;font-weight:bold;text-align:center;margin-bottom:15px;}");
html.append(".info-table td{border:1px solid #999;} .info-table th{background-color:#D9E2F3;color:#333;}");
html.append("</style></head><body>");
html.append("<div class='header'>견 적 서</div>");
// 기본 정보 테이블
html.append("<table class='info-table' style='margin-bottom:15px;'>");
html.append("<tr><th style='width:15%'>견적번호</th><td style='width:35%'>" + estimateNo + "</td>");
html.append("<th style='width:15%'>영업번호</th><td style='width:35%'>" + contractNo + "</td></tr>");
html.append("<tr><th>고객사</th><td>" + customerName + "</td>");
html.append("<th>작성일</th><td>" + regdate + "</td></tr>");
html.append("<tr><th>수신</th><td>" + recipient + "</td>");
html.append("<th>담당자</th><td>" + contactPerson + "</td></tr>");
html.append("<tr><th>작성자</th><td>" + writerName + "</td>");
html.append("<th>모델명</th><td>" + modelName + "</td></tr>");
if(!currencyName.isEmpty()){
html.append("<tr><th>통화</th><td>" + currencyName + "</td>");
html.append("<th>환율</th><td>" + exchangeRate + "</td></tr>");
}
html.append("<tr><th>합계금액</th><td colspan='3'>");
if(!totalAmountKrw.isEmpty() && !"0".equals(totalAmountKrw)){
html.append(totalAmountKrw + " (KRW)");
if(!totalAmount.isEmpty()) html.append(" / " + totalAmount + " (" + currencyName + ")");
} else {
html.append(totalAmount);
}
html.append("</td></tr>");
if(!validityPeriod.isEmpty()){
html.append("<tr><th>유효기간</th><td colspan='3'>" + validityPeriod + "</td></tr>");
}
html.append("</table>");
// 품목 리스트
if(itemList != null && !itemList.isEmpty()){
html.append("<table>");
html.append("<thead><tr>");
html.append("<th>No</th><th>품명</th><th>규격</th><th>수량</th><th>단위</th><th>단가</th><th>금액</th><th>비고</th>");
html.append("</tr></thead><tbody>");
int no = 1;
for(Map item : itemList){
item = CommonUtils.toUpperCaseMapKey(item);
html.append("<tr>");
html.append("<td style='text-align:center;'>" + no++ + "</td>");
html.append("<td>" + CommonUtils.checkNull(item.get("DESCRIPTION")) + "</td>");
html.append("<td>" + CommonUtils.checkNull(item.get("SPECIFICATION")) + "</td>");
html.append("<td style='text-align:right;'>" + CommonUtils.checkNull(item.get("QUANTITY")) + "</td>");
html.append("<td style='text-align:center;'>" + CommonUtils.checkNull(item.get("UNIT")) + "</td>");
html.append("<td style='text-align:right;'>" + CommonUtils.checkNull(item.get("UNIT_PRICE")) + "</td>");
html.append("<td style='text-align:right;'>" + CommonUtils.checkNull(item.get("AMOUNT")) + "</td>");
html.append("<td>" + CommonUtils.checkNull(item.get("REMARK")) + "</td>");
html.append("</tr>");
}
html.append("</tbody></table>");
}
// 비고
if(!noteRemarks.isEmpty()){
html.append("<br/><div><strong>비고:</strong><br/>" + noteRemarks.replace("\n", "<br/>") + "</div>");
}
html.append("</body></html>");
return html.toString();
}
/**
* 견적서 데이터를 HTML 본문으로 구성 (아마란스 결재 화면 표시용)
*/
private String buildEstimateContentsHtml(Map estimateInfo, List<Map> itemList){
StringBuilder html = new StringBuilder();
String estimateNo = CommonUtils.checkNull(estimateInfo.get("ESTIMATE_NO"));
String contractNo = CommonUtils.checkNull(estimateInfo.get("CONTRACT_NO"));
String customerName = CommonUtils.checkNull(estimateInfo.get("CUSTOMER_NAME"));
String writerName = CommonUtils.checkNull(estimateInfo.get("WRITER_NAME"));
String regdate = CommonUtils.checkNull(estimateInfo.get("REGDATE"));
String totalAmount = CommonUtils.checkNull(estimateInfo.get("TOTAL_AMOUNT"));
String totalAmountKrw = CommonUtils.checkNull(estimateInfo.get("TOTAL_AMOUNT_KRW"));
String currencyName = CommonUtils.checkNull(estimateInfo.get("CURRENCY_NAME"));
String exchangeRate = CommonUtils.checkNull(estimateInfo.get("EXCHANGE_RATE"));
String recipient = CommonUtils.checkNull(estimateInfo.get("RECIPIENT"));
String contactPerson = CommonUtils.checkNull(estimateInfo.get("CONTACT_PERSON"));
String modelName = CommonUtils.checkNull(estimateInfo.get("MODEL_NAME"));
String validityPeriod = CommonUtils.checkNull(estimateInfo.get("VALIDITY_PERIOD"));
String noteRemarks = CommonUtils.checkNull(estimateInfo.get("NOTE_REMARKS"));
html.append("<div style='font-family:맑은 고딕,sans-serif;font-size:10pt;'>");
html.append("<h3 style='text-align:center;margin-bottom:10px;'>견 적 서</h3>");
html.append("<table cellspacing='0' cellpadding='4' style='border-collapse:collapse;width:100%;margin-bottom:10px;'>");
html.append("<tr><td style='border:1px solid #333;background-color:#D9E2F3;width:15%;font-weight:bold;font-size:9pt;'>견적번호</td>");
html.append("<td style='border:1px solid #333;width:35%;font-size:9pt;'>" + estimateNo + "</td>");
html.append("<td style='border:1px solid #333;background-color:#D9E2F3;width:15%;font-weight:bold;font-size:9pt;'>영업번호</td>");
html.append("<td style='border:1px solid #333;width:35%;font-size:9pt;'>" + contractNo + "</td></tr>");
html.append("<tr><td style='border:1px solid #333;background-color:#D9E2F3;font-weight:bold;font-size:9pt;'>고객사</td>");
html.append("<td style='border:1px solid #333;font-size:9pt;'>" + customerName + "</td>");
html.append("<td style='border:1px solid #333;background-color:#D9E2F3;font-weight:bold;font-size:9pt;'>작성일</td>");
html.append("<td style='border:1px solid #333;font-size:9pt;'>" + regdate + "</td></tr>");
html.append("<tr><td style='border:1px solid #333;background-color:#D9E2F3;font-weight:bold;font-size:9pt;'>수신</td>");
html.append("<td style='border:1px solid #333;font-size:9pt;'>" + recipient + "</td>");
html.append("<td style='border:1px solid #333;background-color:#D9E2F3;font-weight:bold;font-size:9pt;'>담당자</td>");
html.append("<td style='border:1px solid #333;font-size:9pt;'>" + contactPerson + "</td></tr>");
html.append("<tr><td style='border:1px solid #333;background-color:#D9E2F3;font-weight:bold;font-size:9pt;'>작성자</td>");
html.append("<td style='border:1px solid #333;font-size:9pt;'>" + writerName + "</td>");
html.append("<td style='border:1px solid #333;background-color:#D9E2F3;font-weight:bold;font-size:9pt;'>모델명</td>");
html.append("<td style='border:1px solid #333;font-size:9pt;'>" + modelName + "</td></tr>");
if(!currencyName.isEmpty()){
html.append("<tr><td style='border:1px solid #333;background-color:#D9E2F3;font-weight:bold;font-size:9pt;'>통화</td>");
html.append("<td style='border:1px solid #333;font-size:9pt;'>" + currencyName + "</td>");
html.append("<td style='border:1px solid #333;background-color:#D9E2F3;font-weight:bold;font-size:9pt;'>환율</td>");
html.append("<td style='border:1px solid #333;font-size:9pt;'>" + exchangeRate + "</td></tr>");
}
html.append("<tr><td style='border:1px solid #333;background-color:#D9E2F3;font-weight:bold;font-size:9pt;'>합계금액</td>");
html.append("<td style='border:1px solid #333;font-size:9pt;' colspan='3'>");
if(!totalAmountKrw.isEmpty() && !"0".equals(totalAmountKrw)){
html.append(totalAmountKrw + " (KRW)");
if(!totalAmount.isEmpty()) html.append(" / " + totalAmount + " (" + currencyName + ")");
} else {
html.append(totalAmount);
}
html.append("</td></tr>");
if(!validityPeriod.isEmpty()){
html.append("<tr><td style='border:1px solid #333;background-color:#D9E2F3;font-weight:bold;font-size:9pt;'>유효기간</td>");
html.append("<td style='border:1px solid #333;font-size:9pt;' colspan='3'>" + validityPeriod + "</td></tr>");
}
html.append("</table>");
// 품목 리스트
if(itemList != null && !itemList.isEmpty()){
html.append("<table cellspacing='0' cellpadding='4' style='border-collapse:collapse;width:100%;'>");
html.append("<thead><tr>");
html.append("<th style='border:1px solid #333;background-color:#4472C4;color:#fff;text-align:center;font-size:9pt;'>No</th>");
html.append("<th style='border:1px solid #333;background-color:#4472C4;color:#fff;text-align:center;font-size:9pt;'>품명</th>");
html.append("<th style='border:1px solid #333;background-color:#4472C4;color:#fff;text-align:center;font-size:9pt;'>규격</th>");
html.append("<th style='border:1px solid #333;background-color:#4472C4;color:#fff;text-align:center;font-size:9pt;'>수량</th>");
html.append("<th style='border:1px solid #333;background-color:#4472C4;color:#fff;text-align:center;font-size:9pt;'>단위</th>");
html.append("<th style='border:1px solid #333;background-color:#4472C4;color:#fff;text-align:center;font-size:9pt;'>단가</th>");
html.append("<th style='border:1px solid #333;background-color:#4472C4;color:#fff;text-align:center;font-size:9pt;'>금액</th>");
html.append("<th style='border:1px solid #333;background-color:#4472C4;color:#fff;text-align:center;font-size:9pt;'>비고</th>");
html.append("</tr></thead><tbody>");
int no = 1;
for(Map item : itemList){
item = CommonUtils.toUpperCaseMapKey(item);
html.append("<tr>");
html.append("<td style='border:1px solid #333;text-align:center;font-size:9pt;'>" + no++ + "</td>");
html.append("<td style='border:1px solid #333;font-size:9pt;'>" + CommonUtils.checkNull(item.get("DESCRIPTION")) + "</td>");
html.append("<td style='border:1px solid #333;font-size:9pt;'>" + CommonUtils.checkNull(item.get("SPECIFICATION")) + "</td>");
html.append("<td style='border:1px solid #333;text-align:right;font-size:9pt;'>" + CommonUtils.checkNull(item.get("QUANTITY")) + "</td>");
html.append("<td style='border:1px solid #333;text-align:center;font-size:9pt;'>" + CommonUtils.checkNull(item.get("UNIT")) + "</td>");
html.append("<td style='border:1px solid #333;text-align:right;font-size:9pt;'>" + CommonUtils.checkNull(item.get("UNIT_PRICE")) + "</td>");
html.append("<td style='border:1px solid #333;text-align:right;font-size:9pt;'>" + CommonUtils.checkNull(item.get("AMOUNT")) + "</td>");
html.append("<td style='border:1px solid #333;font-size:9pt;'>" + CommonUtils.checkNull(item.get("REMARK")) + "</td>");
html.append("</tr>");
}
html.append("</tbody></table>");
}
if(!noteRemarks.isEmpty()){
html.append("<br/><div><strong>비고:</strong><br/>" + noteRemarks.replace("\n", "<br/>") + "</div>");
}
html.append("</div>");
return html.toString();
}
/**
* Amaranth10 전자결재 콜백 처리
* 결재 이벤트(상신/진행/종결/반려/삭제 등) 발생 시 Amaranth10에서 호출
@@ -2091,6 +2413,9 @@ public class ApprovalService {
} else if("SALES_REQUEST".equals(targetType)){
sqlSession.update("approval.salesRequestApprovalStatus", statusParam);
System.out.println("구매요청 상태 변경 - targetObjId: " + targetObjId + "" + proposalStatus);
} else if("CONTRACT_ESTIMATE".equals(targetType)){
// 견적서: AMARANTH_APPROVAL 테이블만으로 상태 관리 (품의서와 동일 패턴)
System.out.println("견적서 결재 상태 변경 - targetObjId: " + targetObjId + "" + internalStatus);
}
}
}
@@ -2195,6 +2520,22 @@ public class ApprovalService {
title = "품의서 결재 - " + proposalNo;
}
}
} else if("CONTRACT_ESTIMATE".equals(targetType) && !targetObjId.isEmpty()){
Map<String, Object> estParam = new HashMap();
estParam.put("targetObjId", targetObjId);
Map<String, Object> estimateInfo = sqlSession.selectOne("approval.getEstimateInfoForApproval", estParam);
if(estimateInfo != null){
estimateInfo = CommonUtils.toUpperCaseMapKey(estimateInfo);
List<Map> itemList = sqlSession.selectList("approval.getEstimateItemsForApproval", estParam);
contentsHtml = buildEstimateContentsHtml(estimateInfo, itemList);
String estimateNo = CommonUtils.checkNull(estimateInfo.get("ESTIMATE_NO"));
if(!estimateNo.isEmpty()){
title = "견적서 결재 - " + estimateNo;
}
}
}
}