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%>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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