diff --git a/src/com/pms/api/BomErpApiClient.java b/src/com/pms/api/BomErpApiClient.java new file mode 100644 index 0000000..5e2a1fb --- /dev/null +++ b/src/com/pms/api/BomErpApiClient.java @@ -0,0 +1,267 @@ +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; + +/** + * BOM 구조 ERP 등록/조회/삭제 API 클라이언트 + * M-BOM의 모품목-자품목 관계를 아마란스 ERP로 전송합니다. + */ +public class BomErpApiClient { + + private static final String API_URL_INSERT = "~/apiproxy/api20A00I01001"; + private static final String API_URL_SEARCH = "~/apiproxy/api20A00S01001"; + private static final String API_URL_DELETE = "~/apiproxy/api20A00D01001"; + 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"; + + /** + * BOM 등록 - 모품목-자품목 관계 1건 등록 + */ + public String insertBom(String baseUrl, String coCd, String itemparentCd, + int bomSq, String itemchildCd, double justQt, + double lossRt, String bomOdrFg, String outFg, + String useYn, int sortSq) throws Exception { + validateRequired(coCd, "회사코드(coCd)"); + validateRequired(itemparentCd, "모품번코드(itemparentCd)"); + validateRequired(itemchildCd, "자품번코드(itemchildCd)"); + + StringBuilder json = new StringBuilder(); + json.append("{"); + json.append("\"coCd\":\"").append(escapeJson(coCd)).append("\""); + json.append(",\"itemparentCd\":\"").append(escapeJson(itemparentCd)).append("\""); + json.append(",\"bomSq\":").append(bomSq); + json.append(",\"itemchildCd\":\"").append(escapeJson(itemchildCd)).append("\""); + json.append(",\"justQt\":").append(justQt); + if (lossRt > 0) { + json.append(",\"lossRt\":").append(lossRt); + } + if (bomOdrFg != null && !bomOdrFg.trim().isEmpty()) { + json.append(",\"bomOdrFg\":\"").append(escapeJson(bomOdrFg)).append("\""); + } + if (outFg != null && !outFg.trim().isEmpty()) { + json.append(",\"outFg\":\"").append(escapeJson(outFg)).append("\""); + } + json.append(",\"useYn\":\"").append(escapeJson(useYn != null && !useYn.isEmpty() ? useYn : "1")).append("\""); + json.append(",\"sortSq\":").append(sortSq); + json.append("}"); + + return callApi(baseUrl, API_URL_INSERT, json.toString()); + } + + /** + * BOM 조회 - 모품목 기준 자품목 목록 조회 + */ + public String searchBom(String baseUrl, String coCd, String itemparentCd) throws Exception { + validateRequired(coCd, "회사코드(coCd)"); + validateRequired(itemparentCd, "모품번코드(itemparentCd)"); + + StringBuilder json = new StringBuilder(); + json.append("{"); + json.append("\"coCd\":\"").append(escapeJson(coCd)).append("\""); + json.append(",\"itemparentCd\":\"").append(escapeJson(itemparentCd)).append("\""); + json.append("}"); + + return callApi(baseUrl, API_URL_SEARCH, json.toString()); + } + + /** + * BOM 삭제 - 모품목-자품목 관계 1건 삭제 + */ + public String deleteBom(String baseUrl, String coCd, String itemparentCd, int bomSq) throws Exception { + validateRequired(coCd, "회사코드(coCd)"); + validateRequired(itemparentCd, "모품번코드(itemparentCd)"); + + StringBuilder json = new StringBuilder(); + json.append("{"); + json.append("\"coCd\":\"").append(escapeJson(coCd)).append("\""); + json.append(",\"itemparentCd\":\"").append(escapeJson(itemparentCd)).append("\""); + json.append(",\"bomSq\":").append(bomSq); + json.append("}"); + + return callApi(baseUrl, API_URL_DELETE, json.toString()); + } + + /** + * API 공통 호출 로직 + */ + private String callApi(String baseUrl, String apiUrlTemplate, String requestBody) throws Exception { + 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 = apiUrlTemplate.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(); + + 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(); + } + } + + if (responseCode >= 200 && responseCode < 300) { + return response.toString(); + } else { + throw new Exception("API 호출 실패: HTTP " + responseCode + " - " + response.toString()); + } + } finally { + connection.disconnect(); + } + } + + private void validateRequired(String value, String fieldName) { + if (value == null || value.trim().isEmpty()) { + throw new IllegalArgumentException(fieldName + "는 필수입니다."); + } + } + + 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 { + 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); + } +} diff --git a/src/com/pms/controller/AdminController.java b/src/com/pms/controller/AdminController.java index cfdc30e..e7c3c2c 100644 --- a/src/com/pms/controller/AdminController.java +++ b/src/com/pms/controller/AdminController.java @@ -5526,4 +5526,35 @@ public String clientImportFileProc(HttpServletRequest request, HttpSession sessi return resultMap; } + + /** + * M-BOM → ERP BOM 수동 동기화 + */ + @RequestMapping(value="/admin/syncMbomBomToErp.do", method=RequestMethod.POST) + @ResponseBody + public Map syncMbomBomToErp(HttpServletRequest request, @RequestParam Map paramMap) { + Map resultMap = new HashMap(); + + try { + String mbomHeaderObjid = CommonUtils.checkNull(paramMap.get("mbomHeaderObjid")); + if (mbomHeaderObjid.isEmpty()) { + resultMap.put("success", false); + resultMap.put("message", "mbomHeaderObjid는 필수입니다."); + return resultMap; + } + + System.out.println("===================================="); + System.out.println("M-BOM ERP BOM 수동 동기화 요청: " + mbomHeaderObjid); + System.out.println("===================================="); + + resultMap = batchService.syncMbomBomToErp(mbomHeaderObjid); + + } catch (Exception e) { + e.printStackTrace(); + resultMap.put("success", false); + resultMap.put("message", "BOM 동기화 중 오류: " + e.getMessage()); + } + + return resultMap; + } } diff --git a/src/com/pms/service/BatchService.java b/src/com/pms/service/BatchService.java index 3a18996..c6f4826 100644 --- a/src/com/pms/service/BatchService.java +++ b/src/com/pms/service/BatchService.java @@ -26,6 +26,7 @@ import com.pms.api.PartErpApiClient; import com.pms.api.PartErpDeleteApiClient; import com.pms.api.PartErpUpdateApiClient; import com.pms.api.AmaranthUserApiClient; +import com.pms.api.BomErpApiClient; import com.pms.common.Message; import com.pms.common.SqlMapConfig; import com.pms.common.bean.PersonBean; @@ -2015,6 +2016,189 @@ public class BatchService extends BaseService { return result; } + + /** + * M-BOM → ERP BOM 동기화 (전체 삭제 후 재등록) + * @param mbomHeaderObjid MBOM_HEADER.OBJID + * @return 동기화 결과 + */ + public Map syncMbomBomToErp(String mbomHeaderObjid) { + Map result = new HashMap(); + SqlSession sqlSession = SqlMapConfig.getInstance().getSqlSession(true); + + try { + String baseUrl = "https://erp.rps-korea.com"; + String coCd = "1000"; + + // M-BOM 헤더 정보 조회 + Map headerParam = new HashMap(); + headerParam.put("mbomHeaderObjid", mbomHeaderObjid); + Map mbomHeader = sqlSession.selectOne("productionplanning.getMbomHeaderByObjid", headerParam); + + if (mbomHeader == null) { + result.put("success", false); + result.put("message", "M-BOM 헤더를 찾을 수 없습니다: " + mbomHeaderObjid); + return result; + } + + String mbomNo = CommonUtils.checkNull(mbomHeader.get("MBOM_NO")); + String partNo = CommonUtils.checkNull(mbomHeader.get("PART_NO")); + System.out.println("====== M-BOM ERP BOM 동기화 시작 ======"); + System.out.println("MBOM_NO: " + mbomNo + ", PART_NO: " + partNo); + + BomErpApiClient bomApiClient = new BomErpApiClient(); + + // 1단계: DISTINCT 모품목 목록 조회 + List parentItems = sqlSession.selectList( + "productionplanning.selectDistinctParentItemsForErp", headerParam); + + System.out.println("모품목 수: " + (parentItems != null ? parentItems.size() : 0)); + + int deleteCount = 0; + int insertCount = 0; + int errorCount = 0; + + // 2단계: 각 모품목별 기존 ERP BOM 삭제 + if (parentItems != null) { + for (String parentItemCd : parentItems) { + if (parentItemCd == null || parentItemCd.trim().isEmpty()) continue; + + try { + String searchResponse = bomApiClient.searchBom(baseUrl, coCd, parentItemCd); + System.out.println("ERP BOM 조회 [" + parentItemCd + "]: " + searchResponse); + + // resultData에서 bomSq 목록 추출하여 삭제 + List bomSqList = parseBomSqFromResponse(searchResponse); + for (int bomSq : bomSqList) { + try { + String deleteResponse = bomApiClient.deleteBom(baseUrl, coCd, parentItemCd, bomSq); + System.out.println("ERP BOM 삭제 [" + parentItemCd + ", bomSq:" + bomSq + "]: " + deleteResponse); + deleteCount++; + } catch (Exception de) { + System.err.println("ERP BOM 삭제 실패 [" + parentItemCd + ", bomSq:" + bomSq + "]: " + de.getMessage()); + errorCount++; + } + } + } catch (Exception se) { + System.err.println("ERP BOM 조회 실패 [" + parentItemCd + "]: " + se.getMessage()); + } + } + } + + // 3단계: PLM M-BOM 구조를 ERP에 재등록 + List> bomRelations = sqlSession.selectList( + "productionplanning.selectMbomBomRelationsForErp", headerParam); + + System.out.println("등록 대상 BOM 관계 수: " + (bomRelations != null ? bomRelations.size() : 0)); + + if (bomRelations != null) { + for (Map rel : bomRelations) { + String itemParentCd = CommonUtils.checkNull(rel.get("item_parent_cd")); + String itemChildCd = CommonUtils.checkNull(rel.get("item_child_cd")); + String justQtStr = CommonUtils.checkNull(rel.get("just_qt")); + String bomSqStr = CommonUtils.checkNull(rel.get("bom_sq")); + String supplyType = CommonUtils.checkNull(rel.get("supply_type")); + + if (itemParentCd.isEmpty() || itemChildCd.isEmpty()) { + System.err.println("모품목 또는 자품목 코드 없음 - 건너뜀"); + continue; + } + + double justQt = 1; + try { justQt = Double.parseDouble(justQtStr); } catch (Exception e) {} + + int bomSq = 1; + try { bomSq = (int) Double.parseDouble(bomSqStr); } catch (Exception e) {} + + // SUPPLY_TYPE → bomOdrFg 변환 (자급→0:자재, 사급→1:사급) + String bomOdrFg = "0"; + if ("사급".equals(supplyType)) { + bomOdrFg = "1"; + } + + try { + String insertResponse = bomApiClient.insertBom( + baseUrl, coCd, itemParentCd, bomSq, itemChildCd, + justQt, 0, bomOdrFg, "0", "1", bomSq); + + System.out.println("ERP BOM 등록 [" + itemParentCd + " → " + itemChildCd + + ", bomSq:" + bomSq + ", qty:" + justQt + "]: " + insertResponse); + + if (insertResponse != null && insertResponse.contains("\"resultCode\":0")) { + insertCount++; + } else { + errorCount++; + System.err.println("ERP BOM 등록 실패 응답: " + insertResponse); + } + } catch (Exception ie) { + errorCount++; + System.err.println("ERP BOM 등록 실패 [" + itemParentCd + " → " + itemChildCd + "]: " + ie.getMessage()); + } + } + } + + System.out.println("====== M-BOM ERP BOM 동기화 완료 ======"); + System.out.println("삭제: " + deleteCount + "건, 등록: " + insertCount + "건, 오류: " + errorCount + "건"); + + result.put("success", errorCount == 0); + result.put("message", "BOM 동기화 완료 (삭제:" + deleteCount + ", 등록:" + insertCount + ", 오류:" + errorCount + ")"); + result.put("deleteCount", deleteCount); + result.put("insertCount", insertCount); + result.put("errorCount", errorCount); + + } catch (Exception e) { + e.printStackTrace(); + result.put("success", false); + result.put("message", "BOM 동기화 중 오류: " + e.getMessage()); + } finally { + sqlSession.close(); + } + + return result; + } + + /** + * ERP BOM 조회 응답에서 bomSq 목록 추출 + */ + private List parseBomSqFromResponse(String jsonResponse) { + List bomSqList = new ArrayList(); + if (jsonResponse == null || jsonResponse.isEmpty()) return bomSqList; + + try { + // "bomSq": 패턴으로 값 추출 + int searchIdx = 0; + while (true) { + int idx = jsonResponse.indexOf("\"bomSq\"", searchIdx); + if (idx < 0) break; + + int colonIdx = jsonResponse.indexOf(":", idx); + if (colonIdx < 0) break; + + // 숫자 시작점 찾기 + int numStart = colonIdx + 1; + while (numStart < jsonResponse.length() && !Character.isDigit(jsonResponse.charAt(numStart)) + && jsonResponse.charAt(numStart) != '-') { + numStart++; + } + + // 숫자 끝점 찾기 + int numEnd = numStart; + while (numEnd < jsonResponse.length() + && (Character.isDigit(jsonResponse.charAt(numEnd)) || jsonResponse.charAt(numEnd) == '.')) { + numEnd++; + } + + if (numStart < numEnd) { + String numStr = jsonResponse.substring(numStart, numEnd); + bomSqList.add((int) Double.parseDouble(numStr)); + } + searchIdx = numEnd; + } + } catch (Exception e) { + System.err.println("bomSq 파싱 오류: " + e.getMessage()); + } + + return bomSqList; + } } - - + diff --git a/src/com/pms/service/ProductionPlanningService.java b/src/com/pms/service/ProductionPlanningService.java index 8012583..5df3eb3 100644 --- a/src/com/pms/service/ProductionPlanningService.java +++ b/src/com/pms/service/ProductionPlanningService.java @@ -1589,25 +1589,26 @@ public class ProductionPlanningService { } } - // DB 커밋 성공 후 ERP BOM 동기화 (ERP 실패해도 DB는 유지) - if(result && mbomHeaderObjidForErp != null && !mbomHeaderObjidForErp.isEmpty()) { - try { - System.out.println("===================================="); - System.out.println("M-BOM 저장 후 ERP BOM 동기화 시작"); - System.out.println("MBOM_HEADER_OBJID: " + mbomHeaderObjidForErp); - System.out.println("===================================="); - - Map erpResult = batchService.syncMbomBomToErp(mbomHeaderObjidForErp); - if(erpResult != null && Boolean.TRUE.equals(erpResult.get("success"))) { - System.out.println("ERP BOM 동기화 성공: " + erpResult.get("message")); - } else { - System.err.println("ERP BOM 동기화 실패: " + (erpResult != null ? erpResult.get("message") : "결과 없음")); - } - } catch(Exception erpEx) { - System.err.println("ERP BOM 동기화 오류: " + erpEx.getMessage()); - erpEx.printStackTrace(); - } - } + // [임시 주석] DB 커밋 성공 후 ERP BOM 동기화 (검증 전이라 자동 호출 비활성화) + // 수동 동기화는 /admin/syncMbomBomToErp.do 로 가능. 검증 완료 후 주석 해제. + // if(result && mbomHeaderObjidForErp != null && !mbomHeaderObjidForErp.isEmpty()) { + // try { + // System.out.println("===================================="); + // System.out.println("M-BOM 저장 후 ERP BOM 동기화 시작"); + // System.out.println("MBOM_HEADER_OBJID: " + mbomHeaderObjidForErp); + // System.out.println("===================================="); + // + // Map erpResult = batchService.syncMbomBomToErp(mbomHeaderObjidForErp); + // if(erpResult != null && Boolean.TRUE.equals(erpResult.get("success"))) { + // System.out.println("ERP BOM 동기화 성공: " + erpResult.get("message")); + // } else { + // System.err.println("ERP BOM 동기화 실패: " + (erpResult != null ? erpResult.get("message") : "결과 없음")); + // } + // } catch(Exception erpEx) { + // System.err.println("ERP BOM 동기화 오류: " + erpEx.getMessage()); + // erpEx.printStackTrace(); + // } + // } return result; }