From 5fb6160d066702927d9aecc8deabbb32fcec462e Mon Sep 17 00:00:00 2001 From: hjjeong Date: Tue, 24 Feb 2026 11:44:10 +0900 Subject: [PATCH] =?UTF-8?q?=EB=A7=A4=EC=B6=9C=EB=A7=88=EA=B0=90=20?= =?UTF-8?q?=EC=95=84=EB=A7=88=EB=9E=80=EC=8A=A4=20api=20=EC=97=B0=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/com/pms/api/SalesSlipApiClient.java | 500 ++++++++++++++++++ .../salesmgmt/mapper/salesNcollectMgmt.xml | 98 +++- .../service/SalesNcollectMgmtService.java | 366 +++++++++++-- 3 files changed, 914 insertions(+), 50 deletions(-) create mode 100644 src/com/pms/api/SalesSlipApiClient.java diff --git a/src/com/pms/api/SalesSlipApiClient.java b/src/com/pms/api/SalesSlipApiClient.java new file mode 100644 index 0000000..3e25515 --- /dev/null +++ b/src/com/pms/api/SalesSlipApiClient.java @@ -0,0 +1,500 @@ +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; +import java.util.List; +import java.util.Map; + +/** + * 아마란스10 자동전표데이터등록 API 클라이언트 + * 매출마감 시 회계전표를 ERP에 자동 등록한다. + * API: /apiproxy/api11A10 + */ +public class SalesSlipApiClient { + + private static final String API_URL = "~/apiproxy/api11A10"; + 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"; + + // 회사코드 / 회계단위 + public static final String CO_CD = "1000"; + public static final String DIV_CD = "1000"; + + // 김하얀 사원코드 (전표 작성자) + public static final String INSERT_ID = "2024010"; + + // 영업팀 부서코드 + public static final String SALES_DEPT_CD = "004"; + + // 계정과목 코드 (기본값, DB erp_acct_code에서 조회하여 덮어쓸 수 있음) + public static final String DEFAULT_ACCT_ACCOUNTS_RECEIVABLE = "1080000"; // 외상매출금 + public static final String DEFAULT_ACCT_VAT_COLLECTED = "2550000"; // 부가세예수금 + public static final String DEFAULT_ACCT_PRODUCT_SALES = "4040000"; // 제품매출 + + // 실제 사용할 계정코드 (Service에서 DB 조회 후 설정) + private String acctAccountsReceivable = DEFAULT_ACCT_ACCOUNTS_RECEIVABLE; + private String acctVatCollected = DEFAULT_ACCT_VAT_COLLECTED; + private String acctProductSales = DEFAULT_ACCT_PRODUCT_SALES; + + /** + * DB에서 조회한 계정과목 코드를 설정한다. + * erp_acct_code 테이블에서 api11A02로 동기화된 값을 사용. + */ + public void setAccountCodes(String accountsReceivable, String vatCollected, String productSales) { + if (accountsReceivable != null && !accountsReceivable.isEmpty()) { + this.acctAccountsReceivable = accountsReceivable; + } + if (vatCollected != null && !vatCollected.isEmpty()) { + this.acctVatCollected = vatCollected; + } + if (productSales != null && !productSales.isEmpty()) { + this.acctProductSales = productSales; + } + System.out.println("[SalesSlipApi] 계정과목 설정 - 외상매출금: " + this.acctAccountsReceivable + + ", 부가세예수금: " + this.acctVatCollected + + ", 제품매출: " + this.acctProductSales); + } + + // 증빙코드 + public static final String ATTR_TAX_INVOICE = "1"; // 세금계산서 + public static final String ATTR_ETC = "9"; // 기타 + + // 세무구분 + public static final String TAX_FG_DOMESTIC = "11"; // 과세-세금계산서 + public static final String TAX_FG_EXPORT = "12"; // 영세-수출 + + // 전표유형 + public static final String DOCU_TY_SALES = "3"; // 매출 + + // 차대구분 + public static final String DRCR_DEBIT = "3"; // 차변 + public static final String DRCR_CREDIT = "4"; // 대변 + + /** + * 자동전표 데이터를 아마란스에 등록한다. + * + * @param baseUrl API 서버 기본 URL (https://erp.rps-korea.com) + * @param requestBody 전표 데이터 JSON 문자열 + * @return API 응답 JSON 문자열 + * @throws Exception API 호출 실패 + */ + public String registerSalesSlip(String baseUrl, String requestBody) throws Exception { + if (requestBody == null || requestBody.trim().isEmpty()) { + throw new IllegalArgumentException("전표 데이터(requestBody)는 필수입니다."); + } + + 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 = API_URL.replace("~", ""); + String cleanBaseUrl = baseUrl.endsWith("/") ? baseUrl.substring(0, baseUrl.length() - 1) : baseUrl; + String fullUrl = cleanBaseUrl + urlPath; + + System.out.println("[SalesSlipApi] 요청 URL: " + fullUrl); + System.out.println("[SalesSlipApi] 요청 Body: " + requestBody); + + 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 " + 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); + + 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(); + } + } + + String responseStr = response.toString(); + System.out.println("[SalesSlipApi] 응답 코드: " + responseCode); + System.out.println("[SalesSlipApi] 응답 Body: " + responseStr); + + if (responseCode >= 200 && responseCode < 300) { + return responseStr; + } else { + throw new Exception("전표등록 API 호출 실패: HTTP " + responseCode + " - " + responseStr); + } + + } finally { + connection.disconnect(); + } + } + + /** + * 국내 매출전표 JSON 생성 (세금계산서) + * + * @param menuDt 작성일자 (yyyyMMdd, 세금계산서발행일) + * @param menuSq 작성번호 (동일 일자 내 고유) + * @param slipTitle 전표제목 (거래처명_품명) + * @param trCd 거래처코드 (client_mng.client_cd) + * @param trNm 거래처명 + * @param totalAmount 총액 (공급가액 + 부가세) + * @param vatAmount 부가세 + * @param supplyAmount 공급가액 + * @param itemSummary 적요 (품명) + * @param taxFg 세무구분코드 (convertTaxType으로 변환한 값) + * @return 전표 JSON 문자열 + */ + public String buildDomesticSlipJson(String menuDt, int menuSq, String slipTitle, + String trCd, String trNm, + long totalAmount, long vatAmount, long supplyAmount, + String itemSummary, String taxFg) { + StringBuilder json = new StringBuilder(); + json.append("{"); + json.append("\"coCd\":\"").append(CO_CD).append("\""); + json.append(",\"groupSeq\":\"").append(GROUP_SEQ).append("\""); + json.append(",\"data\":["); + + // Line 1: 차변 외상매출금 (총액) + json.append("{"); + json.append("\"inDivCd\":\"").append(DIV_CD).append("\""); + json.append(",\"menuDt\":\"").append(escapeJson(menuDt)).append("\""); + json.append(",\"menuSq\":").append(menuSq); + json.append(",\"menuLnSq\":1"); + json.append(",\"isuDoc\":\"").append(escapeJson(slipTitle)).append("\""); + json.append(",\"docuTy\":\"").append(DOCU_TY_SALES).append("\""); + json.append(",\"drcrFg\":\"").append(DRCR_DEBIT).append("\""); + json.append(",\"acctCd\":\"").append(acctAccountsReceivable).append("\""); + json.append(",\"trCd\":\"").append(escapeJson(trCd)).append("\""); + json.append(",\"trNm\":\"").append(escapeJson(trNm)).append("\""); + json.append(",\"acctAm\":").append(totalAmount); + json.append(",\"attrCd\":\"").append(ATTR_TAX_INVOICE).append("\""); + json.append(",\"rmkDc\":\"").append(escapeJson(itemSummary)).append("\""); + json.append(",\"ctDept\":\"").append(SALES_DEPT_CD).append("\""); + json.append(",\"insertId\":\"").append(INSERT_ID).append("\""); + json.append(",\"exFg\":\"1\""); + json.append("}"); + + // Line 2: 대변 부가세예수금 (부가세) + json.append(",{"); + json.append("\"inDivCd\":\"").append(DIV_CD).append("\""); + json.append(",\"menuDt\":\"").append(escapeJson(menuDt)).append("\""); + json.append(",\"menuSq\":").append(menuSq); + json.append(",\"menuLnSq\":2"); + json.append(",\"isuDoc\":\"").append(escapeJson(slipTitle)).append("\""); + json.append(",\"docuTy\":\"").append(DOCU_TY_SALES).append("\""); + json.append(",\"drcrFg\":\"").append(DRCR_CREDIT).append("\""); + json.append(",\"acctCd\":\"").append(acctVatCollected).append("\""); + json.append(",\"trCd\":\"").append(escapeJson(trCd)).append("\""); + json.append(",\"trNm\":\"").append(escapeJson(trNm)).append("\""); + json.append(",\"acctAm\":").append(vatAmount); + json.append(",\"attrCd\":\"").append(ATTR_TAX_INVOICE).append("\""); + json.append(",\"rmkDc\":\"").append(escapeJson(itemSummary)).append("\""); + json.append(",\"taxFg\":\"").append(escapeJson(taxFg)).append("\""); + json.append(",\"jeonjaYn\":\"1\""); + json.append(",\"supAm\":").append(supplyAmount); + json.append(",\"ctDept\":\"").append(SALES_DEPT_CD).append("\""); + json.append(",\"insertId\":\"").append(INSERT_ID).append("\""); + json.append(",\"exFg\":\"1\""); + json.append("}"); + + // Line 3: 대변 제품매출 (공급가액) + json.append(",{"); + json.append("\"inDivCd\":\"").append(DIV_CD).append("\""); + json.append(",\"menuDt\":\"").append(escapeJson(menuDt)).append("\""); + json.append(",\"menuSq\":").append(menuSq); + json.append(",\"menuLnSq\":3"); + json.append(",\"isuDoc\":\"").append(escapeJson(slipTitle)).append("\""); + json.append(",\"docuTy\":\"").append(DOCU_TY_SALES).append("\""); + json.append(",\"drcrFg\":\"").append(DRCR_CREDIT).append("\""); + json.append(",\"acctCd\":\"").append(acctProductSales).append("\""); + json.append(",\"trCd\":\"").append(escapeJson(trCd)).append("\""); + json.append(",\"trNm\":\"").append(escapeJson(trNm)).append("\""); + json.append(",\"acctAm\":").append(supplyAmount); + json.append(",\"attrCd\":\"").append(ATTR_TAX_INVOICE).append("\""); + json.append(",\"rmkDc\":\"").append(escapeJson(itemSummary)).append("\""); + json.append(",\"ctDept\":\"").append(SALES_DEPT_CD).append("\""); + json.append(",\"insertId\":\"").append(INSERT_ID).append("\""); + json.append(",\"exFg\":\"1\""); + json.append("}"); + + json.append("]}"); + return json.toString(); + } + + /** + * 해외 매출전표 JSON 생성 (수출) + * + * @param menuDt 작성일자 (yyyyMMdd, 선적일자) + * @param menuSq 작성번호 + * @param slipTitle 전표제목 (거래처명_품명_외화총액_환율) + * @param trCd 거래처코드 + * @param trNm 거래처명 + * @param krwTotalAmount 원화총액 + * @param exchCd 환종코드 (USD, JPY 등) + * @param exchangeRate 환율 + * @param foreignAmount 외화금액 + * @param exportDeclNo 수출신고필증 신고번호 + * @param loadingDate 선적일자 (yyyyMMdd) + * @param itemSummary 적요 + * @return 전표 JSON 문자열 + */ + public String buildOverseasSlipJson(String menuDt, int menuSq, String slipTitle, + String trCd, String trNm, + long krwTotalAmount, + String exchCd, double exchangeRate, double foreignAmount, + String exportDeclNo, String loadingDate, + String itemSummary, String taxFg) { + StringBuilder json = new StringBuilder(); + json.append("{"); + json.append("\"coCd\":\"").append(CO_CD).append("\""); + json.append(",\"groupSeq\":\"").append(GROUP_SEQ).append("\""); + json.append(",\"data\":["); + + // Line 1: 차변 외상매출금 (원화총액) + json.append("{"); + json.append("\"inDivCd\":\"").append(DIV_CD).append("\""); + json.append(",\"menuDt\":\"").append(escapeJson(menuDt)).append("\""); + json.append(",\"menuSq\":").append(menuSq); + json.append(",\"menuLnSq\":1"); + json.append(",\"isuDoc\":\"").append(escapeJson(slipTitle)).append("\""); + json.append(",\"docuTy\":\"").append(DOCU_TY_SALES).append("\""); + json.append(",\"drcrFg\":\"").append(DRCR_DEBIT).append("\""); + json.append(",\"acctCd\":\"").append(acctAccountsReceivable).append("\""); + json.append(",\"trCd\":\"").append(escapeJson(trCd)).append("\""); + json.append(",\"trNm\":\"").append(escapeJson(trNm)).append("\""); + json.append(",\"acctAm\":").append(krwTotalAmount); + json.append(",\"attrCd\":\"").append(ATTR_ETC).append("\""); + json.append(",\"rmkDc\":\"").append(escapeJson(itemSummary)).append("\""); + json.append(",\"ctDept\":\"").append(SALES_DEPT_CD).append("\""); + json.append(",\"insertId\":\"").append(INSERT_ID).append("\""); + json.append(",\"exFg\":\"1\""); + json.append("}"); + + // Line 2: 대변 제품매출 (원화총액) — 해외는 제품매출이 2번 + json.append(",{"); + json.append("\"inDivCd\":\"").append(DIV_CD).append("\""); + json.append(",\"menuDt\":\"").append(escapeJson(menuDt)).append("\""); + json.append(",\"menuSq\":").append(menuSq); + json.append(",\"menuLnSq\":2"); + json.append(",\"isuDoc\":\"").append(escapeJson(slipTitle)).append("\""); + json.append(",\"docuTy\":\"").append(DOCU_TY_SALES).append("\""); + json.append(",\"drcrFg\":\"").append(DRCR_CREDIT).append("\""); + json.append(",\"acctCd\":\"").append(acctProductSales).append("\""); + json.append(",\"trCd\":\"").append(escapeJson(trCd)).append("\""); + json.append(",\"trNm\":\"").append(escapeJson(trNm)).append("\""); + json.append(",\"acctAm\":").append(krwTotalAmount); + json.append(",\"attrCd\":\"").append(ATTR_ETC).append("\""); + json.append(",\"rmkDc\":\"").append(escapeJson(itemSummary)).append("\""); + json.append(",\"ctDept\":\"").append(SALES_DEPT_CD).append("\""); + json.append(",\"insertId\":\"").append(INSERT_ID).append("\""); + json.append(",\"exFg\":\"1\""); + json.append("}"); + + // Line 3: 대변 부가세예수금 (0원) + 수출 부가세 정보 + json.append(",{"); + json.append("\"inDivCd\":\"").append(DIV_CD).append("\""); + json.append(",\"menuDt\":\"").append(escapeJson(menuDt)).append("\""); + json.append(",\"menuSq\":").append(menuSq); + json.append(",\"menuLnSq\":3"); + json.append(",\"isuDoc\":\"").append(escapeJson(slipTitle)).append("\""); + json.append(",\"docuTy\":\"").append(DOCU_TY_SALES).append("\""); + json.append(",\"drcrFg\":\"").append(DRCR_CREDIT).append("\""); + json.append(",\"acctCd\":\"").append(acctVatCollected).append("\""); + json.append(",\"trCd\":\"").append(escapeJson(trCd)).append("\""); + json.append(",\"trNm\":\"").append(escapeJson(trNm)).append("\""); + json.append(",\"acctAm\":0"); + json.append(",\"attrCd\":\"").append(ATTR_ETC).append("\""); + json.append(",\"rmkDc\":\"").append(escapeJson(itemSummary)).append("\""); + // 부가세 수출 관련 필드 + json.append(",\"taxFg\":\"").append(escapeJson(taxFg)).append("\""); + json.append(",\"issDt\":\"").append(escapeJson(loadingDate)).append("\""); + json.append(",\"dummy1\":\"").append(escapeJson(exchCd)).append("\""); + json.append(",\"billAm\":").append(exchangeRate); + json.append(",\"cashAm\":").append(foreignAmount); + json.append(",\"supAm\":").append(krwTotalAmount); + json.append(",\"ctNb\":\"").append(escapeJson(exportDeclNo)).append("\""); + json.append(",\"ctDept\":\"").append(SALES_DEPT_CD).append("\""); + json.append(",\"insertId\":\"").append(INSERT_ID).append("\""); + json.append(",\"exFg\":\"1\""); + json.append("}"); + + json.append("]}"); + return json.toString(); + } + + /** + * PLM 환종 code_id → 아마란스 ISO 환종코드 변환 + */ + /** + * PLM 과세구분 code_id → 아마란스 세무구분(taxFg) 변환 + * 부가세 라인에만 세무구분이 들어감 + */ + public static String convertTaxType(String plmTaxTypeCodeId) { + if (plmTaxTypeCodeId == null) return ""; + switch (plmTaxTypeCodeId) { + case "0900216": return "11"; // 과세매출 → 과세-세금계산서 + case "0900217": return "12"; // 수출 → 영세-수출 + case "0900218": return "51"; // 과세매입 → 과세-세금계산서(매입) + case "0900219": return "52"; // 영세매입 → 영세-수출(매입) + case "0900220": return "54"; // 수입 + default: return ""; + } + } + + public static String convertCurrencyCode(String plmCurrencyCodeId) { + if (plmCurrencyCodeId == null) return "KRW"; + switch (plmCurrencyCodeId) { + case "0001566": return "KRW"; + case "0001534": return "USD"; + case "0001537": return "JPY"; + case "0001536": return "CNY"; + case "0001535": return "EUR"; + default: return "KRW"; + } + } + + private String escapeJson(String value) { + if (value == null) return ""; + return value.replace("\\", "\\\\") + .replace("\"", "\\\"") + .replace("\n", "\\n") + .replace("\r", "\\r") + .replace("\t", "\\t"); + } + + 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(); + } + + 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("[SalesSlipApi] Wehago-sign 생성 오류: " + e.getMessage()); + e.printStackTrace(); + throw e; + } + } +} diff --git a/src/com/pms/salesmgmt/mapper/salesNcollectMgmt.xml b/src/com/pms/salesmgmt/mapper/salesNcollectMgmt.xml index 5c83660..1fb1c24 100644 --- a/src/com/pms/salesmgmt/mapper/salesNcollectMgmt.xml +++ b/src/com/pms/salesmgmt/mapper/salesNcollectMgmt.xml @@ -2142,6 +2142,102 @@ ORDER BY T.REGDATE DESC, T.PROJECT_NO DESC LOADING_DATE = #{loadingDate} WHERE OBJID::VARCHAR = #{OBJID} - + + + + + + + + + + + + /* salesNcollectMgmt.updateSlipInfo - 전표 연동 정보 저장 */ + UPDATE PROJECT_MGMT + SET + SALES_STATUS = '완료', + SALES_DEADLINE_DATE = #{deadlineDate}, + SALES_SLIP_DATE = #{slipDate}, + SALES_SLIP_MENU_SQ = #{slipMenuSq} + WHERE OBJID::VARCHAR = #{OBJID} + + diff --git a/src/com/pms/salesmgmt/service/SalesNcollectMgmtService.java b/src/com/pms/salesmgmt/service/SalesNcollectMgmtService.java index 895f547..7d5966e 100644 --- a/src/com/pms/salesmgmt/service/SalesNcollectMgmtService.java +++ b/src/com/pms/salesmgmt/service/SalesNcollectMgmtService.java @@ -29,6 +29,7 @@ import com.pms.common.SqlMapConfig; import com.pms.common.bean.PersonBean; import com.pms.common.utils.CommonUtils; import com.pms.common.utils.Constants; +import com.pms.api.SalesSlipApiClient; /** *
@@ -983,74 +984,341 @@ public Map saveSaleRegistration(HttpServletRequest request, Map<
 	 * @param paramMap
 	 * @return
 	 */
-	public Map salesDeadlineConfirm(HttpServletRequest request,Map paramMap){
+	public Map salesDeadlineConfirm(HttpServletRequest request, Map paramMap) {
 		Map resultMap = new HashMap();
-		SqlSession sqlSession = SqlMapConfig.getInstance().getSqlSession(false);
-		try{
-			System.out.println("===== 매출마감 처리 시작 =====");
-			System.out.println("paramMap: " + paramMap);
-			
+		SqlSession sqlSession = null;
+		try {
+			System.out.println("===== 매출마감 + 아마란스 전표연동 시작 =====");
+
 			String objIdListStr = CommonUtils.checkNull(paramMap.get("objIdList"));
 			String deadlineDate = CommonUtils.checkNull(paramMap.get("deadlineDate"));
-			
-			System.out.println("objIdListStr: " + objIdListStr);
-			System.out.println("deadlineDate: " + deadlineDate);
-			
-			if(objIdListStr == null || objIdListStr.isEmpty()){
+
+			if (objIdListStr == null || objIdListStr.isEmpty()) {
 				resultMap.put("result", false);
 				resultMap.put("msg", "선택된 항목이 없습니다.");
-				System.out.println("에러: 선택된 항목 없음");
 				return resultMap;
 			}
-			
-			if(deadlineDate == null || deadlineDate.isEmpty()){
+			if (deadlineDate == null || deadlineDate.isEmpty()) {
 				resultMap.put("result", false);
 				resultMap.put("msg", "매출마감일을 입력해주세요.");
-				System.out.println("에러: 매출마감일 없음");
 				return resultMap;
 			}
-			
+
 			String[] targetObjIdList = objIdListStr.split(",");
-			System.out.println("targetObjIdList 길이: " + targetObjIdList.length);
-			
-			if(null != targetObjIdList && 0 < targetObjIdList.length){
-				HttpSession session = request.getSession();
-				sqlSession = SqlMapConfig.getInstance().getSqlSession();
-				PersonBean person = (PersonBean)session.getAttribute(Constants.PERSON_BEAN);
-				
-				String userId = person.getUserId();
-				System.out.println("userId: " + userId);
-				
-				for(int i=0; i> slipDataList = new ArrayList>();
+			for (int i = 0; i < targetObjIdList.length; i++) {
+				String objId = CommonUtils.checkNull(targetObjIdList[i]);
+				if (objId.isEmpty()) continue;
+
+				HashMap queryParam = new HashMap();
+				queryParam.put("OBJID", objId);
+				Map slipData = sqlSession.selectOne("salesNcollectMgmt.getSlipDataForDeadline", queryParam);
+
+				if (slipData == null) {
+					resultMap.put("result", false);
+					resultMap.put("msg", "프로젝트 정보를 찾을 수 없습니다. (OBJID: " + objId + ")");
+					return resultMap;
+				}
+
+				// 이미 마감 완료된 건 체크
+				String salesStatus = CommonUtils.checkNull(slipData.get("SALES_STATUS"));
+				if ("완료".equals(salesStatus)) {
+					resultMap.put("result", false);
+					resultMap.put("msg", "이미 매출마감 완료된 건이 포함되어 있습니다. (" + slipData.get("PROJECT_NO") + ")");
+					return resultMap;
+				}
+
+				// 마감정보 등록 여부 체크
+				String taxType = CommonUtils.checkNull(slipData.get("TAX_TYPE"));
+				if (taxType.isEmpty()) {
+					resultMap.put("result", false);
+					resultMap.put("msg", "마감정보를 먼저 등록해주세요. (" + slipData.get("PROJECT_NO") + ")");
+					return resultMap;
+				}
+
+				// ERP 거래처코드 체크
+				String erpClientCd = CommonUtils.checkNull(slipData.get("ERP_CLIENT_CD"));
+				if (erpClientCd.isEmpty()) {
+					resultMap.put("result", false);
+					resultMap.put("msg", "ERP 연동 거래처가 아닙니다. 거래처 정보를 확인해주세요. (" + slipData.get("CUSTOMER_NAME") + ")");
+					return resultMap;
+				}
+
+				slipDataList.add(slipData);
+			}
+
+			if (slipDataList.isEmpty()) {
+				resultMap.put("result", false);
+				resultMap.put("msg", "처리할 데이터가 없습니다.");
+				return resultMap;
+			}
+
+			// 2) 동일 거래처별 그룹핑 + 금액 합산
+			Map>> groupByCustomer = new HashMap>>();
+			for (Map data : slipDataList) {
+				String customerKey = CommonUtils.checkNull(data.get("CUSTOMER_OBJID"));
+				if (!groupByCustomer.containsKey(customerKey)) {
+					groupByCustomer.put(customerKey, new ArrayList>());
+				}
+				groupByCustomer.get(customerKey).add(data);
+			}
+
+			// 3) DB에서 계정과목 코드 조회 (erp_acct_code → api11A02로 동기화된 값)
+			SalesSlipApiClient slipApiClient = new SalesSlipApiClient();
+			String erpBaseUrl = "https://erp.rps-korea.com";
+
+			HashMap acctParam = new HashMap();
+			acctParam.put("coCd", SalesSlipApiClient.CO_CD);
+
+			acctParam.put("acctNm", "외상매출금");
+			Map arAcct = sqlSession.selectOne("salesNcollectMgmt.getErpAccountCode", acctParam);
+			acctParam.put("acctNm", "부가세예수금");
+			Map vatAcct = sqlSession.selectOne("salesNcollectMgmt.getErpAccountCode", acctParam);
+			acctParam.put("acctNm", "제품매출");
+			Map salesAcct = sqlSession.selectOne("salesNcollectMgmt.getErpAccountCode", acctParam);
+
+			slipApiClient.setAccountCodes(
+				arAcct != null ? CommonUtils.checkNull(arAcct.get("ACCT_CD")) : null,
+				vatAcct != null ? CommonUtils.checkNull(vatAcct.get("ACCT_CD")) : null,
+				salesAcct != null ? CommonUtils.checkNull(salesAcct.get("ACCT_CD")) : null
+			);
+
+			int successCount = 0;
+
+			for (Map.Entry>> entry : groupByCustomer.entrySet()) {
+				List> customerItems = entry.getValue();
+				Map firstItem = customerItems.get(0);
+
+				String erpClientCd = CommonUtils.checkNull(firstItem.get("ERP_CLIENT_CD"));
+				String customerName = CommonUtils.checkNull(firstItem.get("CUSTOMER_NAME"));
+				String areaCd = CommonUtils.checkNull(firstItem.get("AREA_CD"));
+				String taxType = CommonUtils.checkNull(firstItem.get("TAX_TYPE"));
+				// 국내/해외는 프로젝트의 AREA_CD로 판단, 세무구분(taxFg)은 과세구분(TAX_TYPE)으로 전달
+				boolean isDomestic = "0001220".equals(areaCd);
+
+				// 금액 합산
+				long totalSupplyPrice = 0;
+				long totalVat = 0;
+				long totalAmount = 0;
+				double totalForeignAmount = 0;
+				StringBuilder itemNames = new StringBuilder();
+
+				for (Map item : customerItems) {
+					totalSupplyPrice += toLong(item.get("SALES_SUPPLY_PRICE"));
+					totalVat += toLong(item.get("SALES_VAT"));
+					totalAmount += toLong(item.get("SALES_TOTAL_AMOUNT"));
+					totalForeignAmount += toDouble(item.get("FOREIGN_AMOUNT"));
+
+					if (itemNames.length() > 0) itemNames.append(", ");
+					itemNames.append(CommonUtils.checkNull(item.get("PART_NAME")));
+				}
+
+				// 적요: 다건이면 "품명 외 N건"
+				String itemSummary;
+				if (customerItems.size() == 1) {
+					itemSummary = CommonUtils.checkNull(firstItem.get("PART_NAME"));
+				} else {
+					itemSummary = CommonUtils.checkNull(firstItem.get("PART_NAME")) + " 외 " + (customerItems.size() - 1) + "건";
+				}
+
+				// 전표 JSON 생성
+				String slipJson;
+				String slipDate;
+				String taxFg = SalesSlipApiClient.convertTaxType(taxType);
+
+				if (isDomestic) {
+					// 국내: 세금계산서발행일 기준
+					String taxInvoiceDate = CommonUtils.checkNull(firstItem.get("TAX_INVOICE_DATE"));
+					slipDate = taxInvoiceDate.replace("-", "");
+					if (slipDate.isEmpty()) {
+						resultMap.put("result", false);
+						resultMap.put("msg", "세금계산서 발행일이 없습니다. (" + customerName + ")");
+						return resultMap;
+					}
+
+					String slipTitle = customerName + "_" + itemSummary;
+
+					// 작성번호는 타임스탬프 기반으로 유니크하게 생성
+					int menuSq = generateMenuSq();
+
+					slipJson = slipApiClient.buildDomesticSlipJson(
+						slipDate, menuSq, slipTitle,
+						erpClientCd, customerName,
+						totalAmount, totalVat, totalSupplyPrice,
+						itemSummary, taxFg
+					);
+
+					// API 호출
+					String apiResponse = slipApiClient.registerSalesSlip(erpBaseUrl, slipJson);
+					System.out.println("[매출마감] 국내 전표 API 응답: " + apiResponse);
+
+					// 응답 검증
+					validateApiResponse(apiResponse, customerName);
+
+					// DB 업데이트: 각 항목에 전표 정보 저장
+					for (Map item : customerItems) {
+						HashMap updateParam = new HashMap();
+						updateParam.put("OBJID", CommonUtils.checkNull(item.get("OBJID")));
+						updateParam.put("deadlineDate", deadlineDate);
+						updateParam.put("slipDate", slipDate);
+						updateParam.put("slipMenuSq", menuSq);
+						sqlSession.update("salesNcollectMgmt.updateSlipInfo", updateParam);
+					}
+
+				} else {
+					// 해외: 선적일자 기준
+					String loadingDate = CommonUtils.checkNull(firstItem.get("LOADING_DATE"));
+					slipDate = loadingDate.replace("-", "");
+					if (slipDate.isEmpty()) {
+						resultMap.put("result", false);
+						resultMap.put("msg", "선적일자가 없습니다. (" + customerName + ")");
+						return resultMap;
+					}
+
+					String salesCurrency = CommonUtils.checkNull(firstItem.get("SALES_CURRENCY"));
+					double exchangeRate = toDouble(firstItem.get("SALES_EXCHANGE_RATE"));
+					String exportDeclNo = CommonUtils.checkNull(firstItem.get("EXPORT_DECL_NO"));
+					String exchCd = SalesSlipApiClient.convertCurrencyCode(salesCurrency);
+
+					String slipTitle = customerName + "_" + itemSummary 
+						+ "_" + String.format("%.2f", totalForeignAmount)
+						+ "_@" + String.format("%.2f", exchangeRate);
+
+					int menuSq = generateMenuSq();
+
+					slipJson = slipApiClient.buildOverseasSlipJson(
+						slipDate, menuSq, slipTitle,
+						erpClientCd, customerName,
+						totalAmount,
+						exchCd, exchangeRate, totalForeignAmount,
+						exportDeclNo, slipDate,
+						customerName + "_" + itemSummary + "_" + String.format("%.2f", totalForeignAmount) + "_@" + String.format("%.2f", exchangeRate),
+						taxFg
+					);
+
+					String apiResponse = slipApiClient.registerSalesSlip(erpBaseUrl, slipJson);
+					System.out.println("[매출마감] 해외 전표 API 응답: " + apiResponse);
+
+					validateApiResponse(apiResponse, customerName);
+
+					for (Map item : customerItems) {
+						HashMap updateParam = new HashMap();
+						updateParam.put("OBJID", CommonUtils.checkNull(item.get("OBJID")));
+						updateParam.put("deadlineDate", deadlineDate);
+						updateParam.put("slipDate", slipDate);
+						updateParam.put("slipMenuSq", menuSq);
+						sqlSession.update("salesNcollectMgmt.updateSlipInfo", updateParam);
+					}
+				}
+
+				successCount += customerItems.size();
+			}
+
+			sqlSession.commit();
+			resultMap.put("result", true);
+			resultMap.put("msg", successCount + "건의 매출마감이 완료되었습니다. (전표 " + groupByCustomer.size() + "건 등록)");
+			System.out.println("===== 매출마감 + 아마란스 전표연동 완료 =====");
+
+		} catch (Exception e) {
 			resultMap.put("result", false);
-			resultMap.put("msg", "매출마감 처리 중 오류가 발생했습니다.");
-			sqlSession.rollback();
+			String errorMsg = e.getMessage();
+			if (errorMsg != null && errorMsg.startsWith("[전표오류]")) {
+				resultMap.put("msg", errorMsg);
+			} else {
+				resultMap.put("msg", "매출마감 처리 중 오류가 발생했습니다: " + errorMsg);
+			}
+			if (sqlSession != null) sqlSession.rollback();
 			System.out.println("===== 매출마감 처리 중 예외 발생 =====");
 			e.printStackTrace();
-		}finally{
-			sqlSession.close();
+		} finally {
+			if (sqlSession != null) sqlSession.close();
 		}
 		return resultMap;
 	}
+
+	/**
+	 * 아마란스 API 응답 검증
+	 * resultCode가 0이 아니면 예외 발생
+	 */
+	private void validateApiResponse(String apiResponse, String customerName) throws Exception {
+		if (apiResponse == null || apiResponse.isEmpty()) {
+			throw new Exception("[전표오류] 아마란스 API 응답이 없습니다. (" + customerName + ")");
+		}
+		// resultCode 추출 (간이 JSON 파싱)
+		String resultCode = extractJsonField(apiResponse, "resultCode");
+		if (resultCode != null && !"0".equals(resultCode.trim())) {
+			String resultMsg = extractJsonField(apiResponse, "resultMsg");
+			String errorDetail = extractJsonField(apiResponse, "errorMsg");
+			StringBuilder errMsg = new StringBuilder("[전표오류] 아마란스 전표 등록 실패 (");
+			errMsg.append(customerName).append("): ");
+			if (resultMsg != null) errMsg.append(resultMsg);
+			if (errorDetail != null) errMsg.append(" - ").append(errorDetail);
+			throw new Exception(errMsg.toString());
+		}
+	}
+
+	/**
+	 * JSON 문자열에서 특정 필드 값 추출 (간이 파서)
+	 */
+	private String extractJsonField(String json, String fieldName) {
+		String searchKey = "\"" + fieldName + "\"";
+		int keyIdx = json.indexOf(searchKey);
+		if (keyIdx < 0) return null;
+		int colonIdx = json.indexOf(":", keyIdx + searchKey.length());
+		if (colonIdx < 0) return null;
+		int valueStart = colonIdx + 1;
+		// 공백 스킵
+		while (valueStart < json.length() && json.charAt(valueStart) == ' ') valueStart++;
+		if (valueStart >= json.length()) return null;
+		char firstChar = json.charAt(valueStart);
+		if (firstChar == '"') {
+			int valueEnd = json.indexOf("\"", valueStart + 1);
+			if (valueEnd < 0) return null;
+			return json.substring(valueStart + 1, valueEnd);
+		} else {
+			int valueEnd = valueStart;
+			while (valueEnd < json.length() && json.charAt(valueEnd) != ',' 
+				   && json.charAt(valueEnd) != '}' && json.charAt(valueEnd) != ']') {
+				valueEnd++;
+			}
+			return json.substring(valueStart, valueEnd).trim();
+		}
+	}
+
+	/**
+	 * 전표 작성번호 생성 (타임스탬프 기반, 밀리초 하위 5자리)
+	 */
+	private int generateMenuSq() {
+		long timestamp = System.currentTimeMillis();
+		return (int) (timestamp % 100000);
+	}
+
+	private long toLong(Object value) {
+		if (value == null) return 0;
+		try {
+			return new BigDecimal(value.toString()).longValue();
+		} catch (Exception e) {
+			return 0;
+		}
+	}
+
+	private double toDouble(Object value) {
+		if (value == null) return 0.0;
+		try {
+			return new BigDecimal(value.toString()).doubleValue();
+		} catch (Exception e) {
+			return 0.0;
+		}
+	}
 	
 	/**
 	 * 모든 분할 출하의 총 판매 수량 조회