Merge pull request 'V20260210' (#209) from V20260210 into main

Reviewed-on: #209
This commit was merged in pull request #209.
This commit is contained in:
2026-04-27 09:36:16 +00:00
4 changed files with 504 additions and 21 deletions

View File

@@ -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);
}
}

View File

@@ -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<String, Object> syncMbomBomToErp(HttpServletRequest request, @RequestParam Map<String, Object> paramMap) {
Map<String, Object> resultMap = new HashMap<String, Object>();
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;
}
}

View File

@@ -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<String, Object> syncMbomBomToErp(String mbomHeaderObjid) {
Map<String, Object> result = new HashMap<String, Object>();
SqlSession sqlSession = SqlMapConfig.getInstance().getSqlSession(true);
try {
String baseUrl = "https://erp.rps-korea.com";
String coCd = "1000";
// M-BOM 헤더 정보 조회
Map<String, Object> headerParam = new HashMap<String, Object>();
headerParam.put("mbomHeaderObjid", mbomHeaderObjid);
Map<String, Object> 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<String> 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<Integer> 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<Map<String, Object>> bomRelations = sqlSession.selectList(
"productionplanning.selectMbomBomRelationsForErp", headerParam);
System.out.println("등록 대상 BOM 관계 수: " + (bomRelations != null ? bomRelations.size() : 0));
if (bomRelations != null) {
for (Map<String, Object> 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<Integer> parseBomSqFromResponse(String jsonResponse) {
List<Integer> bomSqList = new ArrayList<Integer>();
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;
}
}

View File

@@ -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<String, Object> 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<String, Object> 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;
}