From ef556db9c6b8181486b539ee9a7919ef92b68dc2 Mon Sep 17 00:00:00 2001 From: chpark Date: Thu, 19 Feb 2026 10:41:52 +0900 Subject: [PATCH] =?UTF-8?q?=EA=B2=B0=EC=9E=AC=EC=9B=90=EB=B3=B5=EB=B3=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../view/contractMgmt/estimateList_new.jsp | 38 ++-- WebContent/init.jsp | 1 + WebContent/init_new.jsp | 1 + .../pms/api/AmaranthApprovalApiClient.java | 190 ++++++++++++++++++ .../pms/controller/ApprovalController.java | 43 ++++ src/com/pms/service/ApprovalService.java | 146 ++++++++++++++ 6 files changed, 393 insertions(+), 26 deletions(-) diff --git a/WebContent/WEB-INF/view/contractMgmt/estimateList_new.jsp b/WebContent/WEB-INF/view/contractMgmt/estimateList_new.jsp index bd4b997..6a18fbd 100644 --- a/WebContent/WEB-INF/view/contractMgmt/estimateList_new.jsp +++ b/WebContent/WEB-INF/view/contractMgmt/estimateList_new.jsp @@ -151,7 +151,7 @@ $(document).ready(function(){ document.form1.submit(); }); - //결재상신 (Amaranth10 연동) + //결재상신 $("#btnApproval").click(function(){ var selectedData = _tabulGrid.getSelectedData(); if(selectedData.length<1){ @@ -239,7 +239,7 @@ $(document).ready(function(){ } }); } else { - // 신규수주 또는 가격인하 → Amaranth10 결재 상신 팝업 + // 신규수주 또는 가격인하 → 결재필요 var reasonText = ""; if(reason == "신규수주") { reasonText = "신규수주입니다."; @@ -249,7 +249,7 @@ $(document).ready(function(){ Swal.fire({ title: '결재상신', - html: (reasonText ? reasonText + '
' : '') + '결재상신 하시겠습니까?

* Amaranth10 전자결재로 상신됩니다.', + html: (reasonText ? reasonText + '
' : '') + '결재상신 하시겠습니까?

* 결재완료 후 메일발송이 가능합니다.', icon: 'question', showCancelButton: true, confirmButtonColor: '#3085d6', @@ -258,19 +258,21 @@ $(document).ready(function(){ cancelButtonText: '취소' }).then((result) => { if(result.isConfirmed) { - var docTitle = encodeURIComponent(fnc_checkNull(selectedData[0].CONTRACT_NO)); - fn_openAmaranthApprovalSubmit(estObjId, docTitle); + 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"); } }); } } else { - // API 오류 시 Amaranth 결재 팝업으로 진행 - fn_openAmaranthApprovalSubmit(estObjId, encodeURIComponent(fnc_checkNull(selectedData[0].CONTRACT_NO))); + // API 오류 시 기존 방식으로 진행 + fn_showApprovalConfirmSimple(estObjId, selectedData[0].CONTRACT_NO); } }, error: function() { - // AJAX 오류 시 Amaranth 결재 팝업으로 진행 - fn_openAmaranthApprovalSubmit(estObjId, encodeURIComponent(fnc_checkNull(selectedData[0].CONTRACT_NO))); + // AJAX 오류 시 기존 방식으로 진행 + fn_showApprovalConfirmSimple(estObjId, selectedData[0].CONTRACT_NO); } }); } @@ -823,22 +825,7 @@ function fn_showSerialNoPopup(serialNoString){ }); } -/** - * Amaranth10 결재 상신 팝업 열기 - * @param estObjId 견적서 ObjId - * @param docTitle 문서 제목 (URL 인코딩 상태) - */ -function fn_openAmaranthApprovalSubmit(estObjId, docTitle) { - var approvalUrl = "/approval/amaranthApprovalSubmit.do" - + "?targetType=CONTRACT_ESTIMATE" - + "&targetObjId=" + estObjId - + "&approvalTitle=" + docTitle; - window.open(approvalUrl, "amaranthApprovalSubmit", "width=750,height=700,menubar=no,scrollbars=yes,resizable=yes"); -} - -/* -// [주석 처리] 기존 내부 결재상신 확인 다이얼로그 (단순 버전) -// Amaranth10 연동으로 대체됨. 필요시 주석 해제하여 사용 가능 +// 결재상신 확인 다이얼로그 (단순 버전) function fn_showApprovalConfirmSimple(estObjId, contractNo) { Swal.fire({ title: '결재상신', @@ -857,7 +844,6 @@ function fn_showApprovalConfirmSimple(estObjId, contractNo) { } }); } -*/ // 메일 작성 팝업 열기 function fn_openMailFormPopup(contractObjId){ diff --git a/WebContent/init.jsp b/WebContent/init.jsp index b31fdca..d0b89f0 100644 --- a/WebContent/init.jsp +++ b/WebContent/init.jsp @@ -39,6 +39,7 @@ pageContext.setAttribute("newLineChar", "\n"); + diff --git a/WebContent/init_new.jsp b/WebContent/init_new.jsp index c303e2c..50df74a 100644 --- a/WebContent/init_new.jsp +++ b/WebContent/init_new.jsp @@ -39,6 +39,7 @@ pageContext.setAttribute("newLineChar", "\n"); + diff --git a/src/com/pms/api/AmaranthApprovalApiClient.java b/src/com/pms/api/AmaranthApprovalApiClient.java index eb090c4..ad4cbf1 100644 --- a/src/com/pms/api/AmaranthApprovalApiClient.java +++ b/src/com/pms/api/AmaranthApprovalApiClient.java @@ -813,6 +813,196 @@ public class AmaranthApprovalApiClient { } } + /** + * Amaranth10 전자결재 SSO URL 생성 (결재작성 팝업용) + * 1단계: getAuthToken()으로 사용자 인증 토큰 발급 + * 2단계: api99u01A02 호출로 SSO URL(fullUrl) 획득 + * + * @param baseUrl Amaranth10 서버 URL + * @param empSeq 사용자 시퀀스 + * @param outProcessCode 외부시스템 결재연동코드 (결재연동설정에서 등록한 코드) + * @param formId 그룹웨어 양식코드 (outProcessCode 대신 사용 가능) + * @param approKey 외부시스템 연동키 ("UB_" + UUID) + * @param subjectStr 결재문서 제목 (선택) + * @param mod 작성/보기/삭제 구분 (W:작성, V:보기, D:삭제) + * @param compSeq 회사 시퀀스 (선택, 겸직별 결재 시 필요) + * @param deptSeq 부서 시퀀스 (선택, 겸직별 결재 시 필요) + * @return API 응답 JSON (resultData.fullUrl 포함) + */ + public String getSsoUrl(String baseUrl, String empSeq, String outProcessCode, + String formId, String approKey, String subjectStr, + String mod, String compSeq, String deptSeq) throws Exception { + + System.out.println("=== Amaranth SSO URL 생성 시작 ==="); + System.out.println("empSeq: " + empSeq + ", outProcessCode: " + outProcessCode + ", formId: " + formId); + + // 1단계: 인증 토큰 발급 + Map authResult = getAuthToken(baseUrl, empSeq); + if (!"true".equals(authResult.get("success"))) { + String errMsg = authResult.get("resultMsg"); + if (errMsg == null || errMsg.isEmpty()) errMsg = authResult.get("error"); + throw new Exception("인증 토큰 발급 실패: " + errMsg); + } + + String authToken = authResult.get("authToken"); + String userHashKey = authResult.get("hashKey"); + System.out.println("[1단계] 인증 토큰 발급 성공"); + + // 2단계: SSO API 호출 (api99u01A02) + System.setProperty("https.protocols", "TLSv1.2"); + setupSslTrustAll(); + + String urlPath = "/apiproxy/authUser/api99u01A02"; + String cleanBaseUrl = baseUrl.endsWith("/") ? baseUrl.substring(0, baseUrl.length() - 1) : baseUrl; + String fullUrl = cleanBaseUrl + urlPath; + + URL url = new URL(fullUrl); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setConnectTimeout(30000); + connection.setReadTimeout(30000); + connection.setInstanceFollowRedirects(false); + + try { + connection.setRequestMethod("POST"); + connection.setRequestProperty("Content-Type", "application/json; charset=UTF-8"); + connection.setRequestProperty("Accept", "application/json"); + connection.setRequestProperty("callerName", CALLER_NAME); + connection.setRequestProperty("Authorization", "Bearer " + authToken); + + String transactionId = generateTransactionId(); + connection.setRequestProperty("transaction-id", transactionId); + + String timestamp = String.valueOf(System.currentTimeMillis() / 1000); + connection.setRequestProperty("timestamp", timestamp); + connection.setRequestProperty("groupSeq", GROUP_SEQ); + + // 사용자 인증 wehago-sign (발급받은 authToken + hashKey 사용) + String wehagoSign = generateWehagoSign(authToken, transactionId, timestamp, urlPath, userHashKey); + connection.setRequestProperty("wehago-sign", wehagoSign); + + // empSeqEnc 재생성 (SSO body용) + String empSeqEnc = encryptValue(empSeq); + + // 요청 본문 구성 + String requestBody = buildSsoRequestBody(empSeqEnc, outProcessCode, formId, + approKey, subjectStr, mod, compSeq, deptSeq); + + System.out.println("[2단계] SSO API 호출 - URL: " + fullUrl); + System.out.println("[2단계] Request Body: " + requestBody); + + connection.setDoOutput(true); + connection.setDoInput(true); + + OutputStreamWriter writer = new OutputStreamWriter( + connection.getOutputStream(), StandardCharsets.UTF_8); + writer.write(requestBody); + writer.flush(); + writer.close(); + + int responseCode = connection.getResponseCode(); + BufferedReader reader = null; + StringBuilder response = new StringBuilder(); + + try { + if (responseCode >= 200 && responseCode < 300) { + reader = new BufferedReader(new InputStreamReader( + connection.getInputStream(), StandardCharsets.UTF_8)); + } else { + java.io.InputStream errorStream = connection.getErrorStream(); + if (errorStream != null) { + reader = new BufferedReader(new InputStreamReader( + errorStream, StandardCharsets.UTF_8)); + } else { + throw new Exception("SSO API 호출 실패: HTTP " + responseCode); + } + } + String line; + while ((line = reader.readLine()) != null) { + response.append(line); + } + } finally { + if (reader != null) reader.close(); + } + + System.out.println("[2단계] Response Code: " + responseCode); + System.out.println("[2단계] Response: " + response.toString()); + + return response.toString(); + + } finally { + connection.disconnect(); + } + } + + /** + * SSO API 요청 Body 생성 (api99u01A02) + */ + private String buildSsoRequestBody(String empSeqEnc, String outProcessCode, + String formId, String approKey, + String subjectStr, String mod, + String compSeq, String deptSeq) { + StringBuilder json = new StringBuilder(); + json.append("{"); + json.append("\"header\":{},"); + json.append("\"body\":{"); + json.append("\"groupSeq\":\"").append(escapeJson(GROUP_SEQ)).append("\","); + json.append("\"empSeqEnc\":\"").append(escapeJson(empSeqEnc)).append("\","); + json.append("\"type\":\"popup\","); + json.append("\"popupCode\":\"UBAP036\","); + + // 회사/부서 정보 (겸직별 결재 시 필요) + if (compSeq != null && !compSeq.isEmpty()) { + json.append("\"companyInfo\":{"); + json.append("\"compSeq\":\"").append(escapeJson(compSeq)).append("\""); + if (deptSeq != null && !deptSeq.isEmpty()) { + json.append(",\"deptSeq\":\"").append(escapeJson(deptSeq)).append("\""); + } + json.append("},"); + } + + // appParams (전자결재 관련 파라미터) + json.append("\"appParams\":{"); + json.append("\"approKey\":\"").append(escapeJson(approKey)).append("\","); + json.append("\"mod\":\"").append(escapeJson(mod)).append("\""); + + // outProcessCode 또는 formId (둘 중 하나 필수) + if (outProcessCode != null && !outProcessCode.isEmpty()) { + json.append(",\"outProcessCode\":\"").append(escapeJson(outProcessCode)).append("\""); + } + if (formId != null && !formId.isEmpty()) { + json.append(",\"formId\":\"").append(escapeJson(formId)).append("\""); + } + + // 제목 (선택) + if (subjectStr != null && !subjectStr.isEmpty()) { + json.append(",\"subjectStr\":\"").append(escapeJson(subjectStr)).append("\""); + } + + json.append("}"); // appParams 닫기 + json.append("}"); // body 닫기 + json.append("}"); // root 닫기 + return json.toString(); + } + + /** + * SSL 인증서 검증 우회 설정 (개발 환경용) + */ + private void setupSslTrustAll() throws Exception { + TrustManager[] trustAllCerts = new TrustManager[] { + new X509TrustManager() { + public X509Certificate[] getAcceptedIssuers() { return null; } + public void checkClientTrusted(X509Certificate[] certs, String authType) {} + public void checkServerTrusted(X509Certificate[] certs, String authType) {} + } + }; + SSLContext sc = SSLContext.getInstance("TLS"); + sc.init(null, trustAllCerts, new java.security.SecureRandom()); + HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory()); + HttpsURLConnection.setDefaultHostnameVerifier(new javax.net.ssl.HostnameVerifier() { + public boolean verify(String hostname, javax.net.ssl.SSLSession session) { return true; } + }); + } + /** * AES128 CBC PKCS5Padding 암호화 (loginId 또는 empSeq) * 문서: 현재날짜(YYYYMMDDHHmmss)▦값 → AES 암호화 → Base64 diff --git a/src/com/pms/controller/ApprovalController.java b/src/com/pms/controller/ApprovalController.java index 7438235..0780429 100644 --- a/src/com/pms/controller/ApprovalController.java +++ b/src/com/pms/controller/ApprovalController.java @@ -370,4 +370,47 @@ public class ApprovalController { return "/ajax/ajaxResult"; } + /** + * Amaranth10 전자결재 SSO URL 생성 (B방식 - 팝업 직접 오픈) + * 업무 리스트에서 결재상신 버튼 클릭 시 호출 + * @param request + * @param paramMap targetType, targetObjId, approvalTitle, outProcessCode, formId + * @return fullUrl이 포함된 JSON + */ + @RequestMapping("/approval/getAmaranthSsoUrl.do") + public String getAmaranthSsoUrl(HttpServletRequest request, @RequestParam Map paramMap)throws Exception{ + String jsonResult = approvalService.getAmaranthSsoUrl(request, paramMap); + request.setAttribute("RESULT", jsonResult); + return "/ajax/ajaxResult"; + } + + /** + * Amaranth10 전자결재 콜백 API + * 결재 이벤트(상신/진행/종결/반려/삭제 등) 발생 시 Amaranth10에서 호출 + * 결재연동설정의 상신~삭제 URL에 이 엔드포인트를 등록 + * @param request + * @param paramMap processId, approkey, docId, docSts, userId, formId, docTitle 등 + * @return SUCCESS 응답 JSON + */ + @RequestMapping("/approval/amaranthApprovalCallback.do") + public String amaranthApprovalCallback(HttpServletRequest request, @RequestParam Map paramMap)throws Exception{ + String jsonResult = approvalService.handleAmaranthApprovalCallback(paramMap); + request.setAttribute("RESULT", jsonResult); + return "/ajax/ajaxResult"; + } + + /** + * Amaranth10 전자결재 본문 조회 API (결재작성 시 호출) + * 결재연동설정의 결재작성 URL에 이 엔드포인트를 등록 + * @param request + * @param paramMap approkey, formId, docId, userId, empSeq 등 + * @return 제목/본문 JSON + */ + @RequestMapping("/approval/amaranthApprovalContents.do") + public String amaranthApprovalContents(HttpServletRequest request, @RequestParam Map paramMap)throws Exception{ + String jsonResult = approvalService.getAmaranthApprovalContents(paramMap); + request.setAttribute("RESULT", jsonResult); + return "/ajax/ajaxResult"; + } + } diff --git a/src/com/pms/service/ApprovalService.java b/src/com/pms/service/ApprovalService.java index 1084e77..62b88a5 100644 --- a/src/com/pms/service/ApprovalService.java +++ b/src/com/pms/service/ApprovalService.java @@ -1767,6 +1767,152 @@ public class ApprovalService { // Amaranth10 그룹 시퀀스 (상신 Body 구성용) private static final String GROUP_SEQ = "gcmsAmaranth40578"; + private static final String AMARANTH_BASE_URL = "https://erp.rps-korea.com"; + + /** + * Amaranth10 전자결재 SSO URL 생성 (B방식 - 팝업 직접 오픈) + * 1단계: 인증 토큰 발급 (api99u01A01) + * 2단계: SSO 호출 (api99u01A02) → fullUrl 획득 + * + * @param request HttpServletRequest (세션에서 사용자 정보 추출) + * @param paramMap targetType, targetObjId, approvalTitle, outProcessCode, formId 등 + * @return fullUrl이 포함된 JSON 문자열 + */ + public String getAmaranthSsoUrl(HttpServletRequest request, Map paramMap){ + try { + HttpSession session = request.getSession(); + PersonBean person = (PersonBean)session.getAttribute(Constants.PERSON_BEAN); + if(person == null){ + return "{\"resultCode\":-1,\"resultMsg\":\"세션이 만료되었습니다. 다시 로그인하세요.\"}"; + } + + String empSeq = CommonUtils.checkNull(person.getEmpseq()); + if(empSeq.isEmpty()){ + return "{\"resultCode\":-1,\"resultMsg\":\"empSeq 정보가 없습니다. 관리자에게 문의하세요.\"}"; + } + + // 파라미터 추출 + String targetType = CommonUtils.checkNull(paramMap.get("targetType")); + String targetObjId = CommonUtils.checkNull(paramMap.get("targetObjId")); + String approvalTitle = CommonUtils.checkNull(paramMap.get("approvalTitle")); + String outProcessCode = CommonUtils.checkNull(paramMap.get("outProcessCode")); + String formId = CommonUtils.checkNull(paramMap.get("formId")); + String compSeq = CommonUtils.checkNull(paramMap.get("compSeq"), "1000"); + String deptSeq = CommonUtils.checkNull(paramMap.get("deptSeq")); + + // approKey 생성: "UB_" + UUID (외부시스템과 결재문서 매핑 키) + String approKey = "UB_" + java.util.UUID.randomUUID().toString(); + + System.out.println("=== Amaranth SSO URL 생성 ==="); + System.out.println("empSeq: " + empSeq); + System.out.println("targetType: " + targetType + ", targetObjId: " + targetObjId); + System.out.println("outProcessCode: " + outProcessCode + ", formId: " + formId); + System.out.println("approKey: " + approKey); + + // API 호출 + com.pms.api.AmaranthApprovalApiClient apiClient = new com.pms.api.AmaranthApprovalApiClient(); + String apiResponse = apiClient.getSsoUrl( + AMARANTH_BASE_URL, empSeq, outProcessCode, formId, + approKey, approvalTitle, "W", compSeq, deptSeq + ); + + System.out.println("SSO API 응답: " + apiResponse); + + // 응답에서 fullUrl 추출하여 approKey와 함께 반환 + String fullUrl = extractJsonStringValue(apiResponse, "fullUrl"); + String resultCode = extractJsonStringValue(apiResponse, "resultCode"); + + if("0".equals(resultCode) && !fullUrl.isEmpty()){ + StringBuilder result = new StringBuilder(); + result.append("{\"resultCode\":0,\"resultMsg\":\"SUCCESS\",\"resultData\":{"); + result.append("\"fullUrl\":\"").append(escapeJsonValue(fullUrl)).append("\","); + result.append("\"approKey\":\"").append(escapeJsonValue(approKey)).append("\","); + result.append("\"targetType\":\"").append(escapeJsonValue(targetType)).append("\","); + result.append("\"targetObjId\":\"").append(escapeJsonValue(targetObjId)).append("\""); + result.append("}}"); + return result.toString(); + } else { + String resultMsg = extractJsonStringValue(apiResponse, "resultMsg"); + return "{\"resultCode\":-1,\"resultMsg\":\"SSO URL 생성 실패: " + escapeJsonValue(resultMsg) + "\"}"; + } + + } catch(Exception e){ + System.err.println("Amaranth SSO URL 생성 오류: " + e.getMessage()); + e.printStackTrace(); + return "{\"resultCode\":-1,\"resultMsg\":\"" + escapeJsonValue(e.getMessage()) + "\"}"; + } + } + + /** + * Amaranth10 전자결재 콜백 처리 + * 결재 이벤트(상신/진행/종결/반려/삭제 등) 발생 시 Amaranth10에서 호출 + * + * @param paramMap 콜백 파라미터 (processId, approkey, docId, docSts, userId, formId 등) + * @return 처리 결과 JSON + */ + public String handleAmaranthApprovalCallback(Map paramMap){ + String approkey = CommonUtils.checkNull(paramMap.get("approkey")); + String docId = CommonUtils.checkNull(paramMap.get("docId")); + String docSts = CommonUtils.checkNull(paramMap.get("docSts")); + String docTitle = CommonUtils.checkNull(paramMap.get("docTitle")); + String userId = CommonUtils.checkNull(paramMap.get("userId")); + String processId = CommonUtils.checkNull(paramMap.get("processId")); + + System.out.println("=== Amaranth 결재 콜백 수신 ==="); + System.out.println("approkey: " + approkey); + System.out.println("docId: " + docId); + System.out.println("docSts: " + docSts + " (" + getDocStsName(docSts) + ")"); + System.out.println("docTitle: " + docTitle); + System.out.println("userId: " + userId); + System.out.println("processId: " + processId); + System.out.println("전체 파라미터: " + paramMap); + + // TODO: approkey로 우리 시스템의 대상 문서를 찾아서 상태 업데이트 + // 예: approval 테이블에서 approkey로 조회 → targetObjId의 상태 변경 + + return "{\"resultCode\":\"SUCCESS\",\"resultMessage\":\"성공하였습니다.\"}"; + } + + /** + * Amaranth10 결재작성 시 본문 내용 조회 (Binding WebAPI / WebAPI) + * 결재 작성 화면이 열릴 때 Amaranth10에서 호출 + * + * @param paramMap approkey, formId, docId, userId, empSeq 등 + * @return 제목/본문 JSON + */ + public String getAmaranthApprovalContents(Map paramMap){ + String approkey = CommonUtils.checkNull(paramMap.get("approkey")); + String docId = CommonUtils.checkNull(paramMap.get("docId")); + + System.out.println("=== Amaranth 결재 본문 조회 ==="); + System.out.println("approkey: " + approkey); + System.out.println("docId: " + docId); + System.out.println("전체 파라미터: " + paramMap); + + // TODO: approkey로 대상 문서 데이터를 조회하여 본문 HTML 또는 Binding JSON 구성 + // Binding WebAPI 방식일 경우 ITEMS/TABLE 구조의 JSON 반환 + + StringBuilder result = new StringBuilder(); + result.append("{\"resultCode\":0,\"resultMessage\":\"SUCCESS\",\"resultData\":{"); + result.append("\"title\":\"결재 문서\","); + result.append("\"contents\":\"

본문 내용

\""); + result.append("}}"); + return result.toString(); + } + + /** + * 결재 상태코드 → 한글명 변환 + */ + private String getDocStsName(String docSts){ + if("10".equals(docSts)) return "임시보관"; + if("20".equals(docSts)) return "상신"; + if("30".equals(docSts)) return "진행"; + if("40".equals(docSts)) return "발신종결"; + if("90".equals(docSts)) return "종결"; + if("100".equals(docSts)) return "반려"; + if("110".equals(docSts)) return "보류"; + return "기타(" + docSts + ")"; + } public Map checkApprovalComplete(Map paramMap){ Map resultMap = new HashMap();