diff --git a/WebContent/WEB-INF/view/admin/acct/acctCodeList.jsp b/WebContent/WEB-INF/view/admin/acct/acctCodeList.jsp new file mode 100644 index 0000000..dca1541 --- /dev/null +++ b/WebContent/WEB-INF/view/admin/acct/acctCodeList.jsp @@ -0,0 +1,238 @@ +<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> +<%@ page import="com.pms.common.utils.*"%> +<%@ page import="java.util.*" %> +<%@include file= "/init.jsp" %> +<% +ArrayList list = (ArrayList)request.getAttribute("LIST"); +%> + + + + +<%=Constants.SYSTEM_NAME%> + + + + + + + + + + + + +
+ +
+
+

계정과목 관리

+
+
+ + + + + + + + + + + +
+ +
+
+
+ + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
No계정코드계정과목명차대구분세목여부연동항목그룹코드그룹명증빙구분관련계정관련계정명
${info.RNUM}${info.ACCT_CD}${info.ACCT_NM}${info.DRCR_FG_NM}${info.CH_FG_NM}${info.SUB_DISP_NM}${info.GROUP_CD}${info.GROUP_NM}${info.ATTR_FG_NM}${info.RACCT_CD}${info.RACCT_NM}
조회된 정보가 없습니다.
+
+
+
+ + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
prev   prev   ${nPage}   ${status.index}   nextnext
+

총 ${totalCount}건

+
+
+
+
+
+
+ + diff --git a/WebContent/WEB-INF/view/approval/amaranthApprovalSubmit.jsp b/WebContent/WEB-INF/view/approval/amaranthApprovalSubmit.jsp new file mode 100644 index 0000000..e49b951 --- /dev/null +++ b/WebContent/WEB-INF/view/approval/amaranthApprovalSubmit.jsp @@ -0,0 +1,656 @@ +<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> +<%@ page import="com.pms.common.utils.*"%> +<%@ page import="java.util.*" %> +<%@include file= "/init.jsp" %> + + + + +<%=Constants.SYSTEM_NAME%> - 결재 상신 + + + +<% + // URL 파라미터 + String targetType = CommonUtils.checkNull(request.getParameter("targetType")); + String targetObjId = CommonUtils.checkNull(request.getParameter("targetObjId")); + String approvalTitle = CommonUtils.checkNull(request.getParameter("approvalTitle")); + + // 양식코드/연동코드/기록물철 (추후 관리화면에서 설정 가능하도록) + String tiKeyCode = CommonUtils.checkNull(request.getParameter("tiKeyCode"), "1576"); + String approKey = CommonUtils.checkNull(request.getParameter("approKey"), ""); + String aiKeyCode = CommonUtils.checkNull(request.getParameter("aiKeyCode"), "102433"); +%> + +
+ +
문서 정보
+
+ + + + + +
문서 제목
+
+ + +
사원 검색
+
+
+ + +
+
+ + + + + + + + + + + + + +
선택사원명부서직급직책
사원명을 입력하고 검색하세요
+
+
+ + +
결재라인
+
+
+ + + + + +
+
+ + + + + + + + + + + + + + + +
선택순번결재유형사원명부서직급직책
+
+
+ + +
+ + +
+
+ + +
+
결재 상신 중...
+
+ + + + diff --git a/WebContent/WEB-INF/view/contractMgmt/estimateList_new.jsp b/WebContent/WEB-INF/view/contractMgmt/estimateList_new.jsp index 6a18fbd..bd4b997 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 + '
' : '') + '결재상신 하시겠습니까?

* 결재완료 후 메일발송이 가능합니다.', + html: (reasonText ? reasonText + '
' : '') + '결재상신 하시겠습니까?

* Amaranth10 전자결재로 상신됩니다.', icon: 'question', showCancelButton: true, confirmButtonColor: '#3085d6', @@ -258,21 +258,19 @@ $(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"); + var docTitle = encodeURIComponent(fnc_checkNull(selectedData[0].CONTRACT_NO)); + fn_openAmaranthApprovalSubmit(estObjId, docTitle); } }); } } else { - // API 오류 시 기존 방식으로 진행 - fn_showApprovalConfirmSimple(estObjId, selectedData[0].CONTRACT_NO); + // API 오류 시 Amaranth 결재 팝업으로 진행 + fn_openAmaranthApprovalSubmit(estObjId, encodeURIComponent(fnc_checkNull(selectedData[0].CONTRACT_NO))); } }, error: function() { - // AJAX 오류 시 기존 방식으로 진행 - fn_showApprovalConfirmSimple(estObjId, selectedData[0].CONTRACT_NO); + // AJAX 오류 시 Amaranth 결재 팝업으로 진행 + fn_openAmaranthApprovalSubmit(estObjId, encodeURIComponent(fnc_checkNull(selectedData[0].CONTRACT_NO))); } }); } @@ -825,7 +823,22 @@ 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: '결재상신', @@ -844,6 +857,7 @@ function fn_showApprovalConfirmSimple(estObjId, contractNo) { } }); } +*/ // 메일 작성 팝업 열기 function fn_openMailFormPopup(contractObjId){ diff --git a/src/com/pms/api/AccountCodeApiClient.java b/src/com/pms/api/AccountCodeApiClient.java new file mode 100644 index 0000000..76d9c0d --- /dev/null +++ b/src/com/pms/api/AccountCodeApiClient.java @@ -0,0 +1,248 @@ +package com.pms.api; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; +import java.security.cert.X509Certificate; +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import org.apache.commons.codec.binary.Base64; +import java.util.Random; + +/** + * 계정과목 조회 API 클라이언트 + * ERP 계정과목 목록을 조회합니다. + */ +public class AccountCodeApiClient { + + // 계정과목등록조회 API + private static final String API_URL = "~/apiproxy/api11A02"; + private static final String CALLER_NAME = "API_gcmsAmaranth40578"; + private static final String ACCESS_TOKEN = "MN5KzKBWRAa92BPxDlRLl3GcsxeZXc"; + private static final String HASH_KEY = "22519103205540290721741689643674301018832465"; + private static final String GROUP_SEQ = "gcmsAmaranth40578"; + + /** + * 계정과목 목록을 조회합니다. + * + * @param baseUrl API 서버의 기본 URL (예: https://erp.rps-korea.com) + * @param coCd 회사코드 (4자리, 필수) + * @return API 응답 결과 (JSON 문자열) + * @throws Exception API 호출 중 발생하는 예외 + */ + public String getAccountCodeList(String baseUrl, String coCd) throws Exception { + if (coCd == null || coCd.trim().isEmpty()) { + throw new IllegalArgumentException("회사코드(coCd)는 필수입니다."); + } + + // JDK 1.7에서 TLS 1.2 활성화 + System.setProperty("https.protocols", "TLSv1.2"); + + // SSL 인증서 검증 우회 (개발 환경용) + 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; + } + }); + + // API URL 구성 + String urlPath = API_URL.replace("~", ""); + 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(); + + // 연결 타임아웃 설정 (30초) + connection.setConnectTimeout(30000); + connection.setReadTimeout(30000); + connection.setInstanceFollowRedirects(false); + + try { + // HTTP 메서드 및 헤더 설정 + 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 " + ACCESS_TOKEN); + + String transactionId = generateTransactionId(); + connection.setRequestProperty("transaction-id", transactionId); + + String timestamp = String.valueOf(System.currentTimeMillis() / 1000); + connection.setRequestProperty("timestamp", timestamp); + + connection.setRequestProperty("groupSeq", GROUP_SEQ); + + String wehagoSign = generateWehagoSign(ACCESS_TOKEN, transactionId, timestamp, urlPath); + connection.setRequestProperty("wehago-sign", wehagoSign); + + // 요청 본문 작성 + String requestBody = buildRequestBody(coCd); + + // 요청 전송 + 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(); + + // 리다이렉트 처리 + if (responseCode == 301 || responseCode == 302 || responseCode == 303 || + responseCode == 307 || responseCode == 308) { + String location = connection.getHeaderField("Location"); + connection.disconnect(); + + if (location != null) { + URL redirectUrl = new URL(location); + connection = (HttpURLConnection) redirectUrl.openConnection(); + connection.setConnectTimeout(30000); + connection.setReadTimeout(30000); + connection.setInstanceFollowRedirects(false); + 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 " + ACCESS_TOKEN); + connection.setRequestProperty("transaction-id", transactionId); + connection.setRequestProperty("timestamp", timestamp); + connection.setRequestProperty("groupSeq", GROUP_SEQ); + connection.setRequestProperty("wehago-sign", wehagoSign); + + connection.setDoOutput(true); + connection.setDoInput(true); + + OutputStreamWriter redirectWriter = new OutputStreamWriter( + connection.getOutputStream(), StandardCharsets.UTF_8); + redirectWriter.write(requestBody); + redirectWriter.flush(); + redirectWriter.close(); + + 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("계정과목 API 호출 실패: HTTP " + responseCode + " (에러 응답 본문 없음)"); + } + } + + String line; + while ((line = reader.readLine()) != null) { + response.append(line); + } + } finally { + if (reader != null) { + reader.close(); + } + } + + if (responseCode >= 200 && responseCode < 300) { + return response.toString(); + } else { + throw new Exception("계정과목 API 호출 실패: HTTP " + responseCode + " - " + response.toString()); + } + + } finally { + connection.disconnect(); + } + } + + /** + * 요청 본문 JSON 생성 (API 문서 기준 flat JSON) + */ + private String buildRequestBody(String coCd) { + StringBuilder json = new StringBuilder(); + json.append("{"); + json.append("\"coCd\":\"").append(escapeJson(coCd)).append("\""); + json.append("}"); + return json.toString(); + } + + /** + * JSON 문자열 이스케이프 처리 + */ + private String escapeJson(String value) { + if (value == null) return ""; + return value.replace("\\", "\\\\") + .replace("\"", "\\\"") + .replace("\n", "\\n") + .replace("\r", "\\r") + .replace("\t", "\\t"); + } + + /** + * 32자리 랜덤 transaction-id 생성 + */ + private String generateTransactionId() { + String chars = "0123456789abcdef"; + Random random = new Random(); + StringBuilder sb = new StringBuilder(32); + for (int i = 0; i < 32; i++) { + sb.append(chars.charAt(random.nextInt(chars.length()))); + } + return sb.toString(); + } + + /** + * Wehago-sign 생성 (HmacSHA256) + */ + private String generateWehagoSign(String accessToken, String transactionId, + String timestamp, String urlPath) throws Exception { + try { + String value = accessToken + transactionId + timestamp + urlPath; + + SecretKeySpec keySpec = new SecretKeySpec( + HASH_KEY.getBytes(StandardCharsets.UTF_8), "HmacSHA256"); + Mac mac = Mac.getInstance("HmacSHA256"); + mac.init(keySpec); + byte[] encrypted = mac.doFinal(value.getBytes(StandardCharsets.UTF_8)); + + return Base64.encodeBase64String(encrypted); + + } catch (Exception e) { + System.err.println("Wehago-sign 생성 오류: " + e.getMessage()); + e.printStackTrace(); + throw e; + } + } +} diff --git a/src/com/pms/api/AmaranthApprovalApiClient.java b/src/com/pms/api/AmaranthApprovalApiClient.java index 9cc4338..eb090c4 100644 --- a/src/com/pms/api/AmaranthApprovalApiClient.java +++ b/src/com/pms/api/AmaranthApprovalApiClient.java @@ -39,7 +39,7 @@ public class AmaranthApprovalApiClient { * @param loginId 로그인 ID * @return authToken, hashKey를 포함한 Map */ - public Map getAuthToken(String baseUrl, String loginId) throws Exception { + public Map getAuthToken(String baseUrl, String empSeq) throws Exception { // JDK 1.7에서 TLS 1.2 활성화 System.setProperty("https.protocols", "TLSv1.2"); @@ -93,11 +93,12 @@ public class AmaranthApprovalApiClient { String wehagoSign = generateWehagoSign(ACCESS_TOKEN, transactionId, timestamp, urlPath); connection.setRequestProperty("wehago-sign", wehagoSign); - // loginId 암호화 - String loginIdEnc = encryptLoginId(loginId); + // empSeq 암호화 (문서에 따라 loginIdEnc 또는 empSeqEnc 사용 가능) + String empSeqEnc = encryptValue(empSeq); + System.out.println("[인증] empSeq: " + empSeq + " → empSeqEnc: " + empSeqEnc); - // 요청 본문 작성 - String requestBody = buildAuthRequestBody(loginIdEnc); + // 요청 본문 작성 (empSeqEnc 사용) + String requestBody = buildAuthRequestBody(empSeqEnc); connection.setDoOutput(true); connection.setDoInput(true); @@ -683,27 +684,171 @@ public class AmaranthApprovalApiClient { } /** - * loginId 암호화 (AES128 CBC PKCS5Padding) + * 전자결재(비영리) 문서 상신 - 사용자 인증 방식 + * 1단계: getAuthToken(서버인증)으로 사용자 인증 토큰 발급 + * 2단계: 발급받은 authToken + hashKey로 상신 API 호출 + * @param baseUrl API 서버 기본 URL + * @param loginId 기안자 loginId (인증 토큰 발급용) + * @param requestBodyJson 상신 API 전체 요청 Body (JSON 문자열) + * @return API 응답 결과 (JSON 문자열) */ - private String encryptLoginId(String loginId) throws Exception { + public String submitApprovalDoc(String baseUrl, String empSeq, String requestBodyJson) throws Exception { + + System.out.println("=== Amaranth 결재 문서 상신 시작 ==="); + System.out.println("[1단계] 사용자 인증 토큰 발급 - empSeq: " + empSeq); + + // 1단계: 인증 토큰 발급 (empSeqEnc 사용) + Map authResult = getAuthToken(baseUrl, empSeq); + + System.out.println("[1단계] 인증 토큰 발급 결과: " + authResult); + + if (!"true".equals(authResult.get("success"))) { + String errMsg = authResult.get("resultMsg"); + if (errMsg == null || errMsg.isEmpty()) errMsg = authResult.get("error"); + System.err.println("[1단계] 인증 토큰 발급 실패: " + errMsg); + return "{\"resultCode\":-1,\"resultMsg\":\"인증 토큰 발급 실패: " + escapeJson(errMsg) + "\"}"; + } + + String authToken = authResult.get("authToken"); + String userHashKey = authResult.get("hashKey"); + + System.out.println("[1단계] 인증 토큰 발급 성공 - authToken 길이: " + (authToken != null ? authToken.length() : "null")); + + // 2단계: 문서 상신 API 호출 (사용자 인증) + System.setProperty("https.protocols", "TLSv1.2"); + + 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; } + }); + + String urlPath = "/apiproxy/authUser/api99u03A03"; + 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"); + + // 사용자 인증 헤더 (발급받은 authToken 사용) + 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); + + System.out.println("[2단계] 문서 상신 API 호출"); + System.out.println("URL: " + fullUrl); + System.out.println("Request Body: " + requestBodyJson); + + connection.setDoOutput(true); + connection.setDoInput(true); + + OutputStreamWriter writer = new OutputStreamWriter( + connection.getOutputStream(), StandardCharsets.UTF_8); + writer.write(requestBodyJson); + 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("결재 문서 상신 실패: 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(); + } + } + + /** + * AES128 CBC PKCS5Padding 암호화 (loginId 또는 empSeq) + * 문서: 현재날짜(YYYYMMDDHHmmss)▦값 → AES 암호화 → Base64 + * Key = API상품연동설정 메뉴에서 확인 가능 (정확히 16바이트) + */ + private String encryptValue(String value) throws Exception { // 현재 날짜시간 (YYYYMMDDHHmmss) String currentDateTime = new java.text.SimpleDateFormat("yyyyMMddHHmmss").format(new java.util.Date()); - // 암호화할 평문: 현재날짜시간▦loginId - String plainText = currentDateTime + "▦" + loginId; + // 암호화할 평문: 현재날짜시간▦값 (반드시 특수문자 ▦ 사용) + String plainText = currentDateTime + "\u25A6" + value; - // AES 키와 IV는 정확히 16바이트여야 함 - byte[] keyBytes = AES_KEY.getBytes(StandardCharsets.UTF_8); - byte[] key16Bytes = new byte[16]; - System.arraycopy(keyBytes, 0, key16Bytes, 0, Math.min(keyBytes.length, 16)); + System.out.println("[AES] plainText: " + plainText); + System.out.println("[AES] AES_KEY: " + AES_KEY + " (길이: " + AES_KEY.length() + ")"); - SecretKeySpec secretKey = new SecretKeySpec(key16Bytes, "AES"); + // 문서 샘플코드: Key.getBytes()를 직접 사용 (Key, IV 동일) + byte[] keyData = AES_KEY.getBytes("UTF-8"); + // AES128은 16바이트 키 필요 - 키가 16바이트가 아니면 조정 + if(keyData.length != 16){ + System.out.println("[AES] 키 길이가 16바이트가 아님: " + keyData.length + "바이트 → 16바이트로 조정"); + byte[] key16 = new byte[16]; + System.arraycopy(keyData, 0, key16, 0, Math.min(keyData.length, 16)); + keyData = key16; + } + + SecretKeySpec secureKey = new SecretKeySpec(keyData, "AES"); Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); - cipher.init(Cipher.ENCRYPT_MODE, secretKey, new IvParameterSpec(key16Bytes)); + cipher.init(Cipher.ENCRYPT_MODE, secureKey, new IvParameterSpec(keyData)); - byte[] encrypted = cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8)); - return Base64.encodeBase64String(encrypted); + byte[] encrypted = cipher.doFinal(plainText.getBytes("UTF-8")); + String result = new String(Base64.encodeBase64(encrypted)); + + System.out.println("[AES] 암호화 결과: " + result); + + return result; } /** @@ -752,15 +897,15 @@ public class AmaranthApprovalApiClient { } /** - * 인증 토큰 발급 요청 Body 생성 + * 인증 토큰 발급 요청 Body 생성 (empSeqEnc 사용) */ - private String buildAuthRequestBody(String loginIdEnc) { + private String buildAuthRequestBody(String empSeqEnc) { StringBuilder json = new StringBuilder(); json.append("{"); json.append("\"header\":{},"); json.append("\"body\":{"); json.append("\"groupSeq\":\"").append(escapeJson(GROUP_SEQ)).append("\","); - json.append("\"loginIdEnc\":\"").append(escapeJson(loginIdEnc)).append("\""); + json.append("\"empSeqEnc\":\"").append(escapeJson(empSeqEnc)).append("\""); json.append("}"); json.append("}"); return json.toString(); diff --git a/src/com/pms/controller/AdminController.java b/src/com/pms/controller/AdminController.java index 2266eea..cfdc30e 100644 --- a/src/com/pms/controller/AdminController.java +++ b/src/com/pms/controller/AdminController.java @@ -5337,6 +5337,46 @@ public String clientImportFileProc(HttpServletRequest request, HttpSession sessi return resultMap; } + /** + * 계정과목 목록 화면 + * @param request + * @param paramMap + * @return + */ + @RequestMapping("/admin/acctCodeList.do") + public String acctCodeList(HttpServletRequest request, @RequestParam Map paramMap){ + List list = adminService.getAcctCodeList(request, paramMap); + request.setAttribute("LIST", CommonUtils.toUpperCaseMapKey(list)); + return "/admin/acct/acctCodeList"; + } + + /** + * ERP 계정과목 정보 동기화 수동 실행 + * @param request + * @param paramMap + * @return + */ + @RequestMapping("/admin/syncAccountCodeDataManual.do") + @ResponseBody + public Map syncAccountCodeDataManual(HttpServletRequest request, @RequestParam Map paramMap) { + Map resultMap = new HashMap(); + + try { + System.out.println("===================================="); + System.out.println("관리자 수동 ERP 계정과목 동기화 실행 요청"); + System.out.println("===================================="); + + resultMap = batchService.syncAccountCodeDataManual(); + + } catch (Exception e) { + e.printStackTrace(); + resultMap.put("success", false); + resultMap.put("message", "계정과목 동기화 실행 중 오류가 발생했습니다: " + e.getMessage()); + } + + return resultMap; + } + /** * 전체 PART 정보 ERP 전송 * @param request diff --git a/src/com/pms/controller/ApprovalController.java b/src/com/pms/controller/ApprovalController.java index 3a4170c..7438235 100644 --- a/src/com/pms/controller/ApprovalController.java +++ b/src/com/pms/controller/ApprovalController.java @@ -86,6 +86,44 @@ public class ApprovalController { return "/ajax/ajaxResult"; } + /** + * Amaranth10 결재 상신 팝업 페이지 + * 결재라인 설정 및 문서 상신 화면 + * @param request + * @param paramMap targetType, targetObjId, approvalTitle 등 + * @return 상신 팝업 JSP + */ + @RequestMapping("/approval/amaranthApprovalSubmit.do") + public String amaranthApprovalSubmit(HttpServletRequest request, @RequestParam Map paramMap)throws Exception{ + return "/approval/amaranthApprovalSubmit"; + } + + /** + * Amaranth10 결재 문서 상신 처리 (AJAX) + * @param request + * @param paramMap 상신 데이터 + * @return JSON 결과 + */ + @RequestMapping("/approval/submitAmaranthApproval.do") + public String submitAmaranthApproval(HttpServletRequest request, @RequestParam Map paramMap)throws Exception{ + String jsonResult = approvalService.submitAmaranthApprovalDoc(request, paramMap); + request.setAttribute("RESULT", jsonResult); + return "/ajax/ajaxResult"; + } + + /** + * Amaranth10 사원 검색 (결재라인 사용자 검색용 AJAX) + * @param request + * @param paramMap + * @return JSON 결과 + */ + @RequestMapping("/approval/searchAmaranthEmployees.do") + public String searchAmaranthEmployees(HttpServletRequest request, @RequestParam Map paramMap)throws Exception{ + String jsonResult = approvalService.searchAmaranthEmployees(request, paramMap); + request.setAttribute("RESULT", jsonResult); + return "/ajax/ajaxResult"; + } + /** * 결재 상신 Form * @param request diff --git a/src/com/pms/mapper/admin.xml b/src/com/pms/mapper/admin.xml index b72f70f..e40b60a 100644 --- a/src/com/pms/mapper/admin.xml +++ b/src/com/pms/mapper/admin.xml @@ -9528,4 +9528,71 @@ SELECT WHERE OBJID = #{objid} + + + + + + \ No newline at end of file diff --git a/src/com/pms/mapper/batch.xml b/src/com/pms/mapper/batch.xml index d296de1..0dd50f5 100644 --- a/src/com/pms/mapper/batch.xml +++ b/src/com/pms/mapper/batch.xml @@ -244,4 +244,36 @@ status = #{status} + + + INSERT INTO erp_acct_code ( + co_cd, acct_cd, acct_nm, drcr_fg, sub_disp, sub_disp_nm, + ch_fg, ch_fg_nm, acct_nmk, group_cd, group_nm, + bud_fg, bud_fg_nm, attr_fg, attr_fg_nm, ctrl_cds, + racct_cd, racct_nm + ) VALUES ( + #{co_cd}, #{acct_cd}, #{acct_nm}, #{drcr_fg}, #{sub_disp}, #{sub_disp_nm}, + #{ch_fg}, #{ch_fg_nm}, #{acct_nmk}, #{group_cd}, #{group_nm}, + #{bud_fg}, #{bud_fg_nm}, #{attr_fg}, #{attr_fg_nm}, #{ctrl_cds}, + #{racct_cd}, #{racct_nm} + ) ON CONFLICT (co_cd, acct_cd) DO + UPDATE SET + acct_nm = #{acct_nm}, + drcr_fg = #{drcr_fg}, + sub_disp = #{sub_disp}, + sub_disp_nm = #{sub_disp_nm}, + ch_fg = #{ch_fg}, + ch_fg_nm = #{ch_fg_nm}, + acct_nmk = #{acct_nmk}, + group_cd = #{group_cd}, + group_nm = #{group_nm}, + bud_fg = #{bud_fg}, + bud_fg_nm = #{bud_fg_nm}, + attr_fg = #{attr_fg}, + attr_fg_nm = #{attr_fg_nm}, + ctrl_cds = #{ctrl_cds}, + racct_cd = #{racct_cd}, + racct_nm = #{racct_nm} + + diff --git a/src/com/pms/service/AdminService.java b/src/com/pms/service/AdminService.java index fddd8ef..3054768 100644 --- a/src/com/pms/service/AdminService.java +++ b/src/com/pms/service/AdminService.java @@ -4156,6 +4156,43 @@ public class AdminService extends BaseService { return resultList; } + /** + * 계정과목 목록 조회 + * @param request + * @param paramMap + * @return + */ + public List getAcctCodeList(HttpServletRequest request, Map paramMap){ + List resultList = new ArrayList(); + SqlSession sqlSession = null; + + try{ + sqlSession = SqlMapConfig.getInstance().getSqlSession(); + + String page = CommonUtils.checkNull(request.getParameter("page")); + String countPerPage = CommonUtils.checkNull(request.getParameter("countPerPage"), ""+Constants.ADMIN_COUNT_PER_PAGE+""); + + paramMap.put("COUNT_PER_PAGE", Integer.parseInt(countPerPage)); + + Map pageMap = new HashMap(); + pageMap = (HashMap)sqlSession.selectOne("admin.getAcctCodeListCnt", paramMap); + pageMap.putAll(paramMap); + pageMap = (HashMap)CommonUtils.setPagingInfo(request, pageMap); + + paramMap.put("PAGE_END", CommonUtils.checkNull(pageMap.get("PAGE_END"))); + paramMap.put("PAGE_START", CommonUtils.checkNull(pageMap.get("PAGE_START"))); + + resultList = sqlSession.selectList("admin.getAcctCodeList", paramMap); + + }catch(Exception e){ + e.printStackTrace(); + }finally{ + if(sqlSession != null) sqlSession.close(); + } + + return resultList; + } + /** * 기타부서 상세조회 * @param request diff --git a/src/com/pms/service/ApprovalService.java b/src/com/pms/service/ApprovalService.java index 9295d23..1084e77 100644 --- a/src/com/pms/service/ApprovalService.java +++ b/src/com/pms/service/ApprovalService.java @@ -1647,6 +1647,127 @@ public class ApprovalService { * @param paramMap * @return */ + /** + * Amaranth10 전자결재 문서 상신 (사용자 인증 방식) + * 1단계: getAuthToken(서버인증)으로 사용자 토큰 발급 + * 2단계: 발급받은 토큰으로 상신 API 호출 + * @param request HttpServletRequest + * @param paramMap 상신 데이터 (title, appLineListJson 등) + * @return 결과 JSON 문자열 + */ + public String submitAmaranthApprovalDoc(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()); + String compSeq = "1000"; + + if(empSeq == null || empSeq.isEmpty()){ + return "{\"resultCode\":-1,\"resultMsg\":\"empSeq가 비어있습니다. 관리자에게 문의하세요.\"}"; + } + + // 파라미터 추출 + String title = CommonUtils.checkNull(paramMap.get("title")); + String appLineListJson = CommonUtils.checkNull(paramMap.get("appLineListJson")); + String deptSeq = CommonUtils.checkNull(paramMap.get("deptSeq")); + + // 양식코드, 연동코드, 기록물철 (Amaranth10 설정값) + String tiKeyCode = CommonUtils.checkNull(paramMap.get("tiKeyCode"), "1576"); + String approKey = CommonUtils.checkNull(paramMap.get("approKey"), ""); + String aiKeyCode = CommonUtils.checkNull(paramMap.get("aiKeyCode"), "102433"); + + System.out.println("=== Amaranth 결재 문서 상신 (사용자 인증 - empSeqEnc) ==="); + System.out.println("empSeq: " + empSeq); + System.out.println("title: " + title); + System.out.println("deptSeq: " + deptSeq); + System.out.println("appLineList: " + appLineListJson); + + // 요청 Body 구성 + StringBuilder body = new StringBuilder(); + body.append("{"); + body.append("\"header\":{"); + body.append("\"groupSeq\":\"").append(escapeJsonValue(GROUP_SEQ)).append("\","); + body.append("\"empSeq\":\"").append(escapeJsonValue(empSeq)).append("\""); + body.append("},"); + body.append("\"body\":{"); + body.append("\"empSeq\":\"").append(escapeJsonValue(empSeq)).append("\","); + body.append("\"groupSeq\":\"").append(escapeJsonValue(GROUP_SEQ)).append("\","); + body.append("\"langCode\":\"kr\","); + body.append("\"saveType\":\"002\","); + body.append("\"tiKeyCode\":\"").append(escapeJsonValue(tiKeyCode)).append("\","); + body.append("\"approKey\":\"").append(escapeJsonValue(approKey)).append("\","); + // 결재라인 (JSON 문자열로 전달 - API 스펙: jsonstring 타입) + body.append("\"appLineList\":\"").append(appLineListJson.replace("\"", "\\\"")).append("\","); + // 회사정보 + body.append("\"companyInfo\":{"); + body.append("\"groupSeq\":\"").append(escapeJsonValue(GROUP_SEQ)).append("\","); + body.append("\"compSeq\":\"").append(escapeJsonValue(compSeq)).append("\","); + body.append("\"deptSeq\":\"").append(escapeJsonValue(deptSeq)).append("\","); + body.append("\"empSeq\":\"").append(escapeJsonValue(empSeq)).append("\""); + body.append("},"); + // 문서기본정보 + body.append("\"docParamList\":[{"); + body.append("\"diTitle\":\"").append(escapeJsonValue(title)).append("\","); + body.append("\"aiKeyCode\":\"").append(escapeJsonValue(aiKeyCode)).append("\","); + body.append("\"diDocGrade\":\"000\","); + body.append("\"diDocType\":\"000\","); + body.append("\"diPublic\":\"001\","); + body.append("\"diGrade\":\"000\","); + body.append("\"diSecretGrade\":\"\","); + body.append("\"diTreatMent\":\"001\","); + body.append("\"fileAttachInfo\":[]"); + body.append("}]"); + body.append("}"); + body.append("}"); + + String requestBody = body.toString(); + System.out.println("Request Body: " + requestBody); + + // API 호출 (사용자 인증 - empSeq로 토큰 발급 후 상신) + com.pms.api.AmaranthApprovalApiClient apiClient = new com.pms.api.AmaranthApprovalApiClient(); + String baseUrl = "https://erp.rps-korea.com"; + + String apiResponse = apiClient.submitApprovalDoc(baseUrl, empSeq, requestBody); + + System.out.println("상신 API 응답: " + apiResponse); + + return apiResponse; + + } catch(Exception e){ + System.err.println("Amaranth 결재 문서 상신 오류: " + e.getMessage()); + e.printStackTrace(); + return "{\"resultCode\":-1,\"resultMsg\":\"" + escapeJsonValue(e.getMessage()) + "\"}"; + } + } + + /** + * Amaranth10 사원 목록 조회 (결재라인 사용자 검색용) + * @param request HttpServletRequest + * @return 사원 목록 JSON 문자열 + */ + public String searchAmaranthEmployees(HttpServletRequest request, Map paramMap){ + try { + com.pms.api.AmaranthUserApiClient apiClient = new com.pms.api.AmaranthUserApiClient(); + String baseUrl = "https://erp.rps-korea.com"; + + String apiResponse = apiClient.getAllUserInfo(baseUrl); + + return apiResponse; + + } catch(Exception e){ + System.err.println("Amaranth 사원 검색 오류: " + e.getMessage()); + e.printStackTrace(); + return "{\"resultCode\":-1,\"resultMsg\":\"" + escapeJsonValue(e.getMessage()) + "\"}"; + } + } + + // Amaranth10 그룹 시퀀스 (상신 Body 구성용) + private static final String GROUP_SEQ = "gcmsAmaranth40578"; + public Map checkApprovalComplete(Map paramMap){ Map resultMap = new HashMap(); SqlSession sqlSession = null; diff --git a/src/com/pms/service/BatchService.java b/src/com/pms/service/BatchService.java index 82b9dc0..12280b0 100644 --- a/src/com/pms/service/BatchService.java +++ b/src/com/pms/service/BatchService.java @@ -17,6 +17,7 @@ import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; import org.springframework.web.bind.annotation.RequestParam; +import com.pms.api.AccountCodeApiClient; import com.pms.api.CustomerApiClient; import com.pms.api.DepartmentApiClient; import com.pms.api.EmployeeApiClient; @@ -589,6 +590,41 @@ public class BatchService extends BaseService { return result; } + /** + * 계정과목 정보만 동기화 (수동 실행) + * @return 성공 여부 Map + */ + public Map syncAccountCodeDataManual() { + Map result = new HashMap(); + SqlSession sqlSession = SqlMapConfig.getInstance().getSqlSession(false); + + System.out.println("===================================="); + System.out.println("ERP 계정과목 정보 동기화 시작 (수동)"); + System.out.println("===================================="); + + try { + String baseUrl = "https://erp.rps-korea.com"; + String coCd = "1000"; + + syncAccountCodeData(sqlSession, baseUrl, coCd); + + sqlSession.commit(); + result.put("success", true); + result.put("message", "계정과목 정보 동기화가 완료되었습니다."); + System.out.println("ERP 계정과목 정보 동기화 완료"); + } catch (Exception e) { + sqlSession.rollback(); + result.put("success", false); + result.put("message", "계정과목 정보 동기화 중 오류가 발생했습니다: " + e.getMessage()); + System.err.println("계정과목 정보 동기화 오류: " + e.getMessage()); + e.printStackTrace(); + } finally { + sqlSession.close(); + } + + return result; + } + /** * ERP 데이터 동기화 실제 실행 로직 */ @@ -612,6 +648,9 @@ public class BatchService extends BaseService { // 4. 창고 정보 동기화 syncWarehouseData(sqlSession, baseUrl, coCd); + // 5. 계정과목 동기화 + syncAccountCodeData(sqlSession, baseUrl, coCd); + sqlSession.commit(); System.out.println("ERP 데이터 동기화 배치 완료"); @@ -810,6 +849,180 @@ public class BatchService extends BaseService { } } + /** + * 계정과목 정보 동기화 + */ + private void syncAccountCodeData(SqlSession sqlSession, String baseUrl, String coCd) { + try { + System.out.println("계정과목 정보 동기화 시작..."); + + AccountCodeApiClient acctClient = new AccountCodeApiClient(); + String jsonResponse = acctClient.getAccountCodeList(baseUrl, coCd); + + // JSON 파싱 및 DB 저장 + List> acctList = parseAccountCodeJson(jsonResponse, coCd); + + int processCount = 0; + + // 처음 10건 로그 출력 + System.out.println("===================================="); + System.out.println("계정과목 데이터 샘플 (최대 10건)"); + System.out.println("===================================="); + + for (Map acct : acctList) { + // 처음 10건만 로그 출력 + if (processCount < 10) { + System.out.println("[계정과목 " + (processCount + 1) + "]"); + System.out.println(" - CO_CD: " + acct.get("co_cd")); + System.out.println(" - ACCT_CD: " + acct.get("acct_cd")); + System.out.println(" - ACCT_NM: " + acct.get("acct_nm")); + System.out.println(" - DRCR_FG: " + acct.get("drcr_fg")); + System.out.println(" - CH_FG: " + acct.get("ch_fg") + " (" + acct.get("ch_fg_nm") + ")"); + System.out.println(" - GROUP_CD: " + acct.get("group_cd") + " (" + acct.get("group_nm") + ")"); + System.out.println("---"); + } + + // UPSERT 실행 + sqlSession.insert("batch.upsertAccountCode", acct); + processCount++; + } + + System.out.println("===================================="); + System.out.println("계정과목 정보 동기화 완료 - 처리: " + processCount + "건"); + + } catch (Exception e) { + System.err.println("계정과목 정보 동기화 실패: " + e.getMessage()); + throw new RuntimeException(e); + } + } + + /** + * 계정과목 JSON 파싱 + */ + private List> parseAccountCodeJson(String jsonResponse, String coCd) { + List> acctList = new ArrayList>(); + + try { + // resultData 배열 찾기 + int dataStart = jsonResponse.indexOf("\"resultData\":["); + if (dataStart == -1) { + // resultData가 없으면 전체 응답을 로그로 출력 (디버깅용) + System.out.println("계정과목 API 응답에 resultData 없음. 응답 앞 500자: " + + jsonResponse.substring(0, Math.min(500, jsonResponse.length()))); + return acctList; + } + + String dataSection = jsonResponse.substring(dataStart + 14); + int bracketCount = 0; + int startIdx = -1; + + for (int i = 0; i < dataSection.length(); i++) { + char c = dataSection.charAt(i); + + if (c == '{') { + if (bracketCount == 0) { + startIdx = i; + } + bracketCount++; + } else if (c == '}') { + bracketCount--; + if (bracketCount == 0 && startIdx != -1) { + String acctJson = dataSection.substring(startIdx, i + 1); + Map acct = parseAccountCodeObject(acctJson, coCd); + if (acct != null && acct.get("acct_cd") != null && !acct.get("acct_cd").toString().isEmpty()) { + acctList.add(acct); + } + startIdx = -1; + } + } + } + } catch (Exception e) { + System.err.println("계정과목 JSON 파싱 오류: " + e.getMessage()); + } + + return acctList; + } + + /** + * 계정과목 객체 파싱 (API 응답 camelCase → DB snake_case 매핑) + */ + private Map parseAccountCodeObject(String json, String coCd) { + Map acct = new HashMap(); + + acct.put("co_cd", coCd); // 회사코드 (파라미터에서) + acct.put("acct_cd", extractJsonValue(json, "acctCd")); // 계정코드 + acct.put("acct_nm", extractJsonValue(json, "acctNm")); // 계정과목명 + acct.put("drcr_fg", extractJsonValue(json, "drcrFg")); // 차대구분 + acct.put("sub_disp", extractJsonValue(json, "subDisp")); // 연동항목 + acct.put("sub_disp_nm", extractJsonValue(json, "subDispNm")); // 연동항목명 + acct.put("ch_fg", extractJsonValue(json, "chFg")); // 세목여부 + acct.put("ch_fg_nm", extractJsonValue(json, "chFgNm")); // 세목여부명칭 + acct.put("acct_nmk", extractJsonValue(json, "acctNmk")); // 계정과목명(보조언어) + acct.put("group_cd", extractJsonValue(json, "groupCd")); // 그룹코드 + acct.put("group_nm", extractJsonValue(json, "groupNm")); // 그룹명 + acct.put("bud_fg", extractJsonValue(json, "budFg")); // 예산통제 + acct.put("bud_fg_nm", extractJsonValue(json, "budFgNm")); // 예산통제명 + acct.put("attr_fg", extractJsonValue(json, "attrFg")); // 증빙구분 + acct.put("attr_fg_nm", extractJsonValue(json, "attrFgNm")); // 입력필수명칭 + acct.put("racct_cd", extractJsonValue(json, "racctCd")); // 관련계정코드 + acct.put("racct_nm", extractJsonValue(json, "racctNm")); // 관련계정명 + + // ctrl_cds는 JSON 배열/객체가 올 수 있어 별도 처리 + String ctrlCds = extractJsonRawValue(json, "ctrlCds"); + acct.put("ctrl_cds", ctrlCds != null ? ctrlCds : ""); + + return acct; + } + + /** + * JSON 필드에서 배열/객체 등 raw 값을 추출 (ctrl_cds 같은 복합 데이터용) + */ + private String extractJsonRawValue(String json, String fieldName) { + String searchKey = "\"" + fieldName + "\":"; + int startIdx = json.indexOf(searchKey); + if (startIdx == -1) return ""; + + startIdx += searchKey.length(); + + // 공백 제거 + while (startIdx < json.length() && Character.isWhitespace(json.charAt(startIdx))) { + startIdx++; + } + + if (startIdx >= json.length()) return ""; + + char firstChar = json.charAt(startIdx); + + // 문자열인 경우 → 일반 extractJsonValue 위임 + if (firstChar == '"') { + return extractJsonValue(json, fieldName); + } + + // 배열 또는 객체인 경우 → 중괄호/대괄호 매칭으로 추출 + if (firstChar == '[' || firstChar == '{') { + char openChar = firstChar; + char closeChar = (openChar == '[') ? ']' : '}'; + int depth = 0; + int endIdx = startIdx; + + for (int i = startIdx; i < json.length(); i++) { + char c = json.charAt(i); + if (c == openChar) depth++; + else if (c == closeChar) { + depth--; + if (depth == 0) { + endIdx = i + 1; + break; + } + } + } + return json.substring(startIdx, endIdx); + } + + // null 또는 숫자인 경우 + return extractJsonValue(json, fieldName); + } + /** * 거래처 JSON 파싱 */