refactor: dap-tool-* 모듈을 dap-was-* 이름으로 전면 개편 및 컨테이너명/파이프라인 연동
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 1m57s
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 1m57s
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
package io.shinhanlife.dap.lib.adapter.aop;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
import org.aspectj.lang.annotation.Around;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.lib.adapter.aop
|
||||
* @className EimsMonitoringAspect
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@Slf4j
|
||||
@Aspect
|
||||
@Component
|
||||
public class EimsMonitoringAspect {
|
||||
|
||||
// 포인트컷: io.shinhanlife.dap.lib.adapter.sender 패키지 내의 EimsSender를 구현한 모든 클래스의 메서드를 타겟으로 지정합니다.
|
||||
@Around("execution(* io.shinhanlife.dap.lib.adapter.sender.*EimsSender.*(..))")
|
||||
public Object monitorEimsCommunication(ProceedingJoinPoint joinPoint) throws Throwable {
|
||||
|
||||
// 1. 호출되는 클래스와 메서드 이름 추출
|
||||
String className = joinPoint.getTarget().getClass().getSimpleName();
|
||||
String methodName = joinPoint.getSignature().getName();
|
||||
Object[] args = joinPoint.getArgs(); // 파라미터
|
||||
|
||||
long startTime = System.currentTimeMillis();
|
||||
|
||||
// 2. [요청 로깅] EIMS망으로 요청이 나가기 직전
|
||||
log.info(" [EIMS 요청] {} - {}() | Params: {}", className, methodName, Arrays.toString(args));
|
||||
|
||||
try {
|
||||
// 실제 레거시 통신 로직 실행 (이 코드가 없으면 통신이 진행되지 않습니다)
|
||||
Object result = joinPoint.proceed();
|
||||
|
||||
// 3. [응답 로깅] 정상적으로 통신이 완료된 후
|
||||
long executionTime = System.currentTimeMillis() - startTime;
|
||||
log.info("◀ [EIMS 응답] {} - {}() | 소요시간: {}ms | Result: {}", className, methodName, executionTime, result);
|
||||
|
||||
return result;
|
||||
|
||||
} catch (Exception e) {
|
||||
// 4. [에러 로깅] 레거시 통신 중 장애(타임아웃 등) 발생 시
|
||||
long executionTime = System.currentTimeMillis() - startTime;
|
||||
log.error(" [EIMS 에러] {} - {}() | 소요시간: {}ms | Error: {}", className, methodName, executionTime, e.getMessage());
|
||||
|
||||
// 에러를 삼키지 않고 다시 던져서 기존 예외 처리 로직(서킷 브레이커 등)이 작동하게 합니다.
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package io.shinhanlife.dap.lib.adapter.connector;
|
||||
|
||||
import io.shinhanlife.dap.lib.adapter.support.DynamicPayloadBuilder;
|
||||
import io.shinhanlife.dap.lib.adapter.support.DynamicSchemaValidator;
|
||||
import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
|
||||
import io.github.resilience4j.ratelimiter.annotation.RateLimiter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.client.RestClient;
|
||||
import java.util.Map;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.lib.adapter.connector
|
||||
* @className ExternalApiConnector
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class ExternalApiConnector {
|
||||
|
||||
private final RestClient restClient;
|
||||
private final DynamicSchemaValidator schemaValidator;
|
||||
private final DynamicPayloadBuilder payloadBuilder;
|
||||
|
||||
public ExternalApiConnector(DynamicSchemaValidator schemaValidator, DynamicPayloadBuilder payloadBuilder) {
|
||||
this.restClient = RestClient.create();
|
||||
this.schemaValidator = schemaValidator;
|
||||
this.payloadBuilder = payloadBuilder;
|
||||
}
|
||||
|
||||
@RateLimiter(name = "externalApi", fallbackMethod = "fallbackForExternalApi")
|
||||
@CircuitBreaker(name = "externalApi", fallbackMethod = "fallbackForExternalApi")
|
||||
public String callExternalApi(String apiName, String endpoint, Map<String, Object> data, List<Map<String, Object>> spec, boolean isFixedLength) throws Exception {
|
||||
|
||||
// 1. 요청 데이터 검증 (errorLog 전달하여 구체적 에러 포착)
|
||||
StringBuilder errorLog = new StringBuilder();
|
||||
if (!schemaValidator.validate(spec, data, errorLog)) {
|
||||
String errorMsg = "API 요청 데이터 스키마 불일치 [" + apiName + "]: " + errorLog.toString();
|
||||
log.error(" {}", errorMsg);
|
||||
throw new IllegalArgumentException(errorMsg);
|
||||
}
|
||||
|
||||
// 2. 페이로드 빌드
|
||||
Object payload = isFixedLength ? payloadBuilder.buildFixedLengthString(spec, data) : data;
|
||||
String contentType = isFixedLength ? "application/x-www-form-urlencoded;charset=EUC-KR" : "application/json";
|
||||
|
||||
log.info(" 외부 API 호출 [{}] 시작 (FixedLength: {})", apiName, isFixedLength);
|
||||
|
||||
return restClient.post()
|
||||
.uri(endpoint)
|
||||
.contentType(MediaType.parseMediaType(contentType))
|
||||
.body(payload)
|
||||
.retrieve()
|
||||
.body(String.class);
|
||||
}
|
||||
|
||||
public String fallbackForExternalApi(String apiName, String endpoint, Map<String, Object> data, List<Map<String, Object>> spec, boolean isFixedLength, Throwable t) {
|
||||
log.error(" [외부 API 장애] {} 호출 실패: {}", apiName, t.getMessage());
|
||||
return String.format("{\"status\":\"EXTERNAL_API_ERROR\", \"message\":\"외부 서비스 연동 중 오류 발생: %s\"}", t.getMessage());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package io.shinhanlife.dap.lib.adapter.connector;
|
||||
|
||||
import io.shinhanlife.dap.lib.adapter.support.DynamicPayloadBuilder;
|
||||
import io.shinhanlife.dap.lib.adapter.support.DynamicSchemaValidator;
|
||||
import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
|
||||
import io.github.resilience4j.ratelimiter.annotation.RateLimiter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.client.RestClient;
|
||||
import java.util.Map;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.lib.adapter.connector
|
||||
* @className InternalSystemConnector
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class InternalSystemConnector {
|
||||
|
||||
private final RestClient restClient;
|
||||
private final DynamicSchemaValidator schemaValidator;
|
||||
private final DynamicPayloadBuilder payloadBuilder;
|
||||
|
||||
// 생성자를 통한 의존성 주입 (Spring이 알아서 Validator와 Builder를 넣어줍니다)
|
||||
public InternalSystemConnector(DynamicSchemaValidator schemaValidator, DynamicPayloadBuilder payloadBuilder) {
|
||||
this.restClient = RestClient.create();
|
||||
this.schemaValidator = schemaValidator;
|
||||
this.payloadBuilder = payloadBuilder;
|
||||
}
|
||||
|
||||
@RateLimiter(name = "internalSystem", fallbackMethod = "fallbackForInternal")
|
||||
@CircuitBreaker(name = "internalSystem", fallbackMethod = "fallbackForInternal")
|
||||
public String callInternalSystem(String targetName, String endpoint, Map<String, Object> data, List<Map<String, Object>> spec, boolean isFixedLength) throws Exception {
|
||||
|
||||
// 1. 요청 데이터 검증 (errorLog 전달하여 구체적 에러 포착)
|
||||
StringBuilder errorLog = new StringBuilder();
|
||||
if (!schemaValidator.validate(spec, data, errorLog)) {
|
||||
String errorMsg = "대내외 시스템 연계 데이터 스키마 불일치 [" + targetName + "]: " + errorLog.toString();
|
||||
log.error(" {}", errorMsg);
|
||||
throw new IllegalArgumentException(errorMsg);
|
||||
}
|
||||
|
||||
// 2. 페이로드 빌드 (고정장 vs JSON)
|
||||
Object payload = isFixedLength ? payloadBuilder.buildFixedLengthString(spec, data) : data;
|
||||
String contentType = isFixedLength ? "application/x-www-form-urlencoded;charset=EUC-KR" : "application/json";
|
||||
|
||||
log.info(" 대내외 시스템 호출 [{}] 시작 (FixedLength: {})", targetName, isFixedLength);
|
||||
|
||||
return restClient.post()
|
||||
.uri(endpoint)
|
||||
.contentType(MediaType.parseMediaType(contentType))
|
||||
.body(payload)
|
||||
.retrieve()
|
||||
.body(String.class);
|
||||
}
|
||||
|
||||
// 통신 장애(CircuitBreaker) 또는 허용량 초과(RateLimiter) 시 처리 로직
|
||||
public String fallbackForInternal(String targetName, String endpoint, Map<String, Object> data, List<Map<String, Object>> spec, boolean isFixedLength, Throwable t) {
|
||||
log.error(" [대내외 시스템 장애/지연] {} 연계 실패: {}", targetName, t.getMessage());
|
||||
return String.format("{\"status\":\"INTERNAL_SYSTEM_ERROR\", \"message\":\"대내외 연계 시스템 호출 중 오류가 발생했거나 요청이 지연되었습니다. 사유: %s\"}", t.getMessage());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package io.shinhanlife.dap.lib.adapter.connector;
|
||||
|
||||
import io.shinhanlife.dap.lib.adapter.support.ResultStandardizer;
|
||||
import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
|
||||
import io.github.resilience4j.ratelimiter.annotation.RateLimiter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.lib.adapter.connector
|
||||
* @className LegacyDbConnector
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class LegacyDbConnector {
|
||||
|
||||
private final NamedParameterJdbcTemplate jdbcTemplate; // 동적 파라미터 바인딩을 위한 템플릿
|
||||
private final ResultStandardizer resultStandardizer;
|
||||
|
||||
@RateLimiter(name = "legacyDb", fallbackMethod = "fallbackForDb")
|
||||
@CircuitBreaker(name = "legacyDb", fallbackMethod = "fallbackForDb")
|
||||
public List<Map<String, Object>> executeDynamicQuery(String queryId, String sql, Map<String, Object> params) {
|
||||
log.info(" Legacy DB 조회 시작 [QueryID: {}]", queryId);
|
||||
|
||||
// 1. SQL 쿼리 실행 (오라클 등)
|
||||
List<Map<String, Object>> rawResults = jdbcTemplate.queryForList(sql, params);
|
||||
|
||||
// 2. 스키마 매핑 및 결과 정형화 (대문자 -> 카멜케이스 변환)
|
||||
List<Map<String, Object>> standardResults = rawResults.stream()
|
||||
.map(resultStandardizer::standardize)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
log.info(" Legacy DB 조회 완료 ({}건 반환)", standardResults.size());
|
||||
return standardResults;
|
||||
}
|
||||
|
||||
public List<Map<String, Object>> fallbackForDb(String queryId, String sql, Map<String, Object> params, Throwable t) {
|
||||
log.error(" [Legacy DB 장애/지연] 쿼리 실행 실패 [{}]: {}", queryId, t.getMessage());
|
||||
throw new RuntimeException("레거시 DB 연동 중 오류가 발생했습니다. (QueryID: " + queryId + ")");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
package io.shinhanlife.dap.lib.adapter.connector;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import io.shinhanlife.dap.lib.adapter.sender.EimsSender;
|
||||
import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
|
||||
import io.github.resilience4j.ratelimiter.RequestNotPermitted;
|
||||
import io.github.resilience4j.ratelimiter.annotation.RateLimiter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.cache.annotation.Cacheable;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import io.shinhanlife.dap.lib.adapter.util.LegacyDataTransformer;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.lib.adapter.connector
|
||||
* @className LegacyEimsConnector
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class LegacyEimsConnector {
|
||||
|
||||
// 4가지 방식의 Sender를 모두 주입받습니다. (변수명이 아주 중요합니다!)
|
||||
private final EimsSender httpEimsSender; // 1~20 (EIMS API)
|
||||
private final EimsSender tcpEimsSender; // 21~30 (EIMS Socket)
|
||||
private final EimsSender jspFormEimsSender; // 31~40 (JSP Form)
|
||||
private final EimsSender jspJsonEimsSender; // 41~50 (JSP JSON)
|
||||
|
||||
private final EimsSender mciEimsSender; // 실시간 연계 (기존 HTTP/TCP 대체, 동기식 API)
|
||||
private final EimsSender mciStringEimsSender; // 실시간 연계 (String 전문 버전)
|
||||
private final EimsSender eaiEimsSender; // 비동기/대용량 연계 (배치 통신 등)
|
||||
|
||||
private final ObjectMapper jsonMapper;
|
||||
|
||||
|
||||
// 방어막 적용: 예외가 발생하거나 차단기가 열리면 fallbackMethod를 즉시 실행합니다!
|
||||
// 서킷 브레이커와 Rate Limiter를 동시에 적용 (둘 중 하나라도 걸리면 fallbackForEims 실행)
|
||||
@RateLimiter(name = "eims", fallbackMethod = "fallbackForEims")
|
||||
@CircuitBreaker(name = "eims", fallbackMethod = "fallbackForEims")
|
||||
// 2번 업그레이드 적용: 파라미터 기반의 스마트 캐싱 (고객의 요청 데이터가 다르면 캐시도 다르게 적용)
|
||||
@Cacheable(value = "eimsData", key = "#routingType + '-' + #interfaceId + '-' + (#data != null ? #data.hashCode() : 0)")
|
||||
public String executeByTool(String routingType, String interfaceId, Map<String, Object> data, List<Map<String, Object>> spec) throws Exception {
|
||||
|
||||
// 1. 데이터 조립 (방어 로직 포함)
|
||||
String payload = buildPayload(data, spec);
|
||||
|
||||
log.info("\n [Adapter -> Legacy] EIMS 통신 요청 - RoutingType: {}, Interface: {}, Payload: {}", routingType, interfaceId, payload);
|
||||
|
||||
// 2. 4단계 라우팅 분기 처리 (Switch Expression 활용)
|
||||
String upperRoutingType = routingType != null ? routingType.toUpperCase() : "";
|
||||
|
||||
String legacyResponse = switch (upperRoutingType) {
|
||||
case "HTTP" -> {
|
||||
log.info("[라우팅] EIMS API (HTTP) 통신으로 전달");
|
||||
yield httpEimsSender.send(interfaceId, payload);
|
||||
}
|
||||
case "TCP" -> {
|
||||
log.info("[라우팅] EIMS 소켓 (TCP) 통신으로 전달");
|
||||
yield tcpEimsSender.send(interfaceId, payload);
|
||||
}
|
||||
case "JSP_FORM" -> {
|
||||
log.info("[라우팅] JSP Form 통신으로 전달");
|
||||
yield jspFormEimsSender.send(interfaceId, payload);
|
||||
}
|
||||
case "JSP_JSON" -> {
|
||||
log.info("[라우팅] JSP JSON 통신으로 전달");
|
||||
yield jspJsonEimsSender.send(interfaceId, payload);
|
||||
}
|
||||
|
||||
case "MCI" -> {
|
||||
if (isStringMci(spec)) {
|
||||
log.info("[라우팅] 스펙 자동 판별 (String 포맷 감지) -> MCI 연계 어댑터(String)를 통해 EIMS 전달");
|
||||
yield mciStringEimsSender.send(interfaceId, payload);
|
||||
} else {
|
||||
log.info("[라우팅] 실시간 AI 요청 -> MCI 연계 어댑터를 통해 EIMS 전달");
|
||||
yield mciEimsSender.send(interfaceId, payload);
|
||||
}
|
||||
}
|
||||
case "EAI" -> {
|
||||
log.info("[라우팅] 비동기/대용량 요청 -> EAI 연계 어댑터를 통해 EIMS 전달");
|
||||
yield eaiEimsSender.send(interfaceId, payload);
|
||||
}
|
||||
default -> {
|
||||
log.error("[라우팅] 알 수 없는 라우팅 타입: {}", routingType);
|
||||
throw new IllegalArgumentException("지원하지 않는 라우팅 타입입니다: " + routingType);
|
||||
}
|
||||
};
|
||||
|
||||
log.info("\n [Legacy -> Adapter] EIMS 통신 응답 수신: {}", legacyResponse);
|
||||
return legacyResponse;
|
||||
}
|
||||
|
||||
|
||||
// 비상용 응답 메서드 (차단기가 열려있거나, 타임아웃/에러가 났을 때 실행됨)
|
||||
// 주의: 파라미터는 원본 메서드와 100% 똑같이 맞추고, 마지막에 Throwable을 받아야 합니다.
|
||||
public String fallbackForEims(String routingType, String interfaceId, Map<String, Object> data, List<Map<String, Object>> spec, Throwable t) {
|
||||
|
||||
// 1. Rate Limiter에 의해 차단된 경우 (트래픽 폭주)
|
||||
if (t instanceof RequestNotPermitted) {
|
||||
log.warn(" [Rate Limiter 발동] 트래픽 폭주로 요청 차단! interfaceId: {}", interfaceId);
|
||||
return String.format(
|
||||
"{\"status\":\"TOO_MANY_REQUESTS\", \"message\":\"순간적인 요청 폭주로 인해 일시적으로 제한되었습니다. 잠시 후 시도해 주세요.\", \"interfaceId\":\"%s\"}",
|
||||
interfaceId
|
||||
);
|
||||
}
|
||||
|
||||
// 2. Circuit Breaker에 의해 차단된 경우 (레거시 시스템 장애/지연)
|
||||
log.error(" [서킷 브레이커 발동] 레거시 통신 차단! 원인: {}", t.getMessage());
|
||||
return String.format(
|
||||
"{\"status\":\"CIRCUIT_OPEN\", \"message\":\"신한라이프 내부 시스템 장애로 인해 일시적으로 차단되었습니다. 복구 후 재시도 부탁드립니다.\", \"interfaceId\":\"%s\"}",
|
||||
interfaceId
|
||||
);
|
||||
}
|
||||
|
||||
private String buildPayload(Map<String, Object> data, List<Map<String, Object>> spec) {
|
||||
// 3번 항목 적용: 원본 데이터를 스펙에 맞게 엄격히 정제(변환, 형변환, 잘라내기, 기본값 등)
|
||||
Map<String, Object> transformedData = LegacyDataTransformer.transform(data, spec);
|
||||
|
||||
if (transformedData == null || transformedData.isEmpty()) return "{}";
|
||||
|
||||
try {
|
||||
return jsonMapper.writeValueAsString(transformedData);
|
||||
} catch (Exception e) {
|
||||
log.error(" JSON 변환 에러: {}", e.getMessage());
|
||||
return "{}";
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isStringMci(List<Map<String, Object>> spec) {
|
||||
if (spec == null || spec.isEmpty()) return false;
|
||||
// 스펙 내에 maxLength 등 고정길이 전문 관련 속성이 하나라도 존재하거나 명시적으로 STRING 힌트가 있으면 String 전문으로 간주
|
||||
return spec.stream().anyMatch(field ->
|
||||
field.containsKey("maxLength") ||
|
||||
field.containsKey("byteSize") ||
|
||||
"STRING".equalsIgnoreCase(String.valueOf(field.get("mciFormat")))
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package io.shinhanlife.dap.lib.adapter.connector;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.lib.adapter.connector
|
||||
* @className ThirdPartySecurityConnector
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class ThirdPartySecurityConnector {
|
||||
|
||||
private final ObjectMapper jsonMapper;
|
||||
|
||||
/**
|
||||
* 3rd Party 보안 모듈(DRM, BM 등)과 연동하기 위한 전용 메서드입니다.
|
||||
*/
|
||||
public String executeSecurityModule(String interfaceId, Map<String, Object> data) throws Exception {
|
||||
log.info(" [3rd Party Security] 보안 모듈 연동 시작 - Interface: {}", interfaceId);
|
||||
|
||||
// 보안 모듈 통신을 위한 특수 페이로드 조립 (예시)
|
||||
// 실제로는 RestClient나 WebClient를 통해 보안 VM의 전용 엔드포인트로 호출합니다.
|
||||
|
||||
String resultJson;
|
||||
|
||||
if (interfaceId.startsWith("DRM_")) {
|
||||
log.info(" [DRM 처리] 내부 문서 암/복호화 모듈과 통신 중...");
|
||||
resultJson = jsonMapper.writeValueAsString(Map.of(
|
||||
"status", "SUCCESS",
|
||||
"module", "DRM",
|
||||
"message", "문서 보안 처리가 완료되었습니다.",
|
||||
"interfaceId", interfaceId,
|
||||
"data", data != null ? data : Map.of()
|
||||
));
|
||||
} else if (interfaceId.startsWith("BM_")) {
|
||||
log.info(" [BM 처리] 바이오 인증 모듈과 통신 중...");
|
||||
resultJson = jsonMapper.writeValueAsString(Map.of(
|
||||
"status", "SUCCESS",
|
||||
"module", "Bio-Metric",
|
||||
"message", "바이오 인증이 완료되었습니다.",
|
||||
"interfaceId", interfaceId,
|
||||
"data", data != null ? data : Map.of()
|
||||
));
|
||||
} else {
|
||||
log.warn(" 알 수 없는 보안 모듈 연동 요청: {}", interfaceId);
|
||||
resultJson = jsonMapper.writeValueAsString(Map.of(
|
||||
"status", "UNKNOWN_MODULE",
|
||||
"message", "알 수 없는 보안 모듈 인터페이스입니다."
|
||||
));
|
||||
}
|
||||
|
||||
log.info(" [3rd Party Security] 보안 모듈 처리 완료");
|
||||
return resultJson;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package io.shinhanlife.dap.lib.adapter.dto;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.lib.adapter.dto
|
||||
* @className ErrorDetail
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
public class ErrorDetail {
|
||||
private int code;
|
||||
private String message;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package io.shinhanlife.dap.lib.adapter.dto;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.lib.adapter.dto
|
||||
* @className JsonRpcRequest
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
public class JsonRpcRequest {
|
||||
private String jsonrpc;
|
||||
private String method;
|
||||
private Params params;
|
||||
private String id;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package io.shinhanlife.dap.lib.adapter.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
// 2. 응답 DTO
|
||||
/**
|
||||
* @package io.shinhanlife.dap.lib.adapter.dto
|
||||
* @className JsonRpcResponse
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@JsonPropertyOrder({"jsonrpc", "result", "error", "id"})
|
||||
public class JsonRpcResponse {
|
||||
public String jsonrpc = "2.0";
|
||||
public Object result;
|
||||
public Object error;
|
||||
public String id;
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package io.shinhanlife.dap.lib.adapter.dto;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.AllArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.lib.adapter.dto
|
||||
* @className Params
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class Params {
|
||||
private String routingType;
|
||||
private String name;
|
||||
private String interfaceId;
|
||||
private Map<String, Object> data;
|
||||
private List<Map<String, Object>> spec;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package io.shinhanlife.dap.lib.adapter.exception;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.lib.adapter.exception
|
||||
* @className MciCommunicationException
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
public class MciCommunicationException extends RuntimeException {
|
||||
|
||||
public MciCommunicationException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public MciCommunicationException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
package io.shinhanlife.dap.lib.adapter.sender;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
|
||||
import io.shinhanlife.dap.lib.adapter.support.TicketManager;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.kafka.core.KafkaTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StopWatch;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.lib.adapter.sender
|
||||
* @className EaiEimsSender
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@Slf4j
|
||||
@Service("eaiEimsSender")
|
||||
@RequiredArgsConstructor
|
||||
public class EaiEimsSender implements EimsSender {
|
||||
|
||||
private final ObjectMapper jsonMapper;
|
||||
private final XmlMapper xmlMapper;
|
||||
|
||||
// 실전 코드: 스프링이 제공하는 카프카 템플릿 주입
|
||||
private final KafkaTemplate<String, String> kafkaTemplate;
|
||||
|
||||
// 비동기 티켓 매니저
|
||||
private final TicketManager ticketManager;
|
||||
|
||||
@Override
|
||||
public String send(String interfaceId, String jsonPayload) throws Exception {
|
||||
StopWatch stopWatch = new StopWatch(); stopWatch.start();
|
||||
|
||||
try {
|
||||
JsonNode jsonNode = jsonMapper.readTree(jsonPayload);
|
||||
String xmlData = xmlMapper.writeValueAsString(jsonNode);
|
||||
String esbStandardXml = wrapWithEaiHeader(interfaceId, xmlData);
|
||||
|
||||
log.info(" [EAI 어댑터] Kafka 토픽(eai-topic)으로 전송 시도...");
|
||||
|
||||
try {
|
||||
// 실전 코드 적용: Kafka로 메시지 발행
|
||||
kafkaTemplate.send("eai-topic", esbStandardXml);
|
||||
log.info(" [EAI 어댑터] Kafka 전송 완료!");
|
||||
} catch (Exception e) {
|
||||
// 로컬 환경에는 카프카가 없으므로 에러가 날 수 있습니다. 테스트를 위해 로깅만 하고 넘깁니다.
|
||||
log.warn(" 로컬 환경이거나 Ka<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n" +
|
||||
"<EaiMessage>\n" +
|
||||
" <Header>\n" +
|
||||
" <ChannelId>MCP_GATEWAY_ASYNC</ChannelId>\n" +
|
||||
" <InterfaceId>EAI_BATCH_JOB</InterfaceId>\n" +
|
||||
" <Timestamp>1782705901850</Timestamp>\n" +
|
||||
" <TransferType>ASYNC</TransferType>\n" +
|
||||
" </Header>\n" +
|
||||
" <Body>\n" +
|
||||
" <ObjectNode>\n" +
|
||||
" <batchId>BATCH_20260629_001</batchId>\n" +
|
||||
" <targetSystem>GLOBAL_MINIMUM_TAX_SYS</targetSystem>\n" +
|
||||
" <recordCount>50000</recordCount>\n" +
|
||||
" </ObjectNode>\n" +
|
||||
" </Body>\n" +
|
||||
"</EaiMessage>fka 서버에 연결할 수 없습니다. (메시지 출력으로 대체합니다) \n전송하려던 메시지: {}", esbStandardXml);
|
||||
}
|
||||
|
||||
// 비동기 폴링을 위한 티켓 발급
|
||||
String ticketId = ticketManager.issueTicket(interfaceId, jsonPayload);
|
||||
|
||||
return String.format(
|
||||
"{\"status\":\"PROCESSING\", \"interfaceId\":\"%s\", \"ticketId\":\"%s\", \"message\":\"비동기 작업이 접수되었습니다. 상태 조회 API를 통해 결과를 확인하세요.\"}",
|
||||
interfaceId, ticketId);
|
||||
|
||||
} finally {
|
||||
stopWatch.stop();
|
||||
log.info(" [SLA 모니터링 - EAI] 소요시간: {} ms", stopWatch.getTotalTimeMillis());
|
||||
}
|
||||
}
|
||||
|
||||
private String wrapWithEaiHeader(String interfaceId, String xmlData) {
|
||||
// 비동기 EAI는 추적을 위해 TransferType이나 Batch ID 같은 속성이 추가로 들어가는 경우가 많습니다.
|
||||
return String.format(
|
||||
"<EaiMessage>" +
|
||||
"<Header>" +
|
||||
"<ChannelId>MCP_GATEWAY_ASYNC</ChannelId>" +
|
||||
"<InterfaceId>%s</InterfaceId>" +
|
||||
"<Timestamp>%d</Timestamp>" +
|
||||
"<TransferType>ASYNC</TransferType>" +
|
||||
"</Header>" +
|
||||
"<Body>%s</Body>" +
|
||||
"</EaiMessage>",
|
||||
interfaceId, System.currentTimeMillis(), xmlData
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package io.shinhanlife.dap.lib.adapter.sender;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.lib.adapter.sender
|
||||
* @className EimsSender
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
public interface EimsSender {
|
||||
// 프로토콜에 상관없이 이 메서드 하나로 통일합니다.
|
||||
String send(String interfaceId, String payload) throws Exception;
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package io.shinhanlife.dap.lib.adapter.sender;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.client.RestClient;
|
||||
import org.springframework.http.client.SimpleClientHttpRequestFactory;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.slf4j.MDC;
|
||||
|
||||
import io.shinhanlife.dap.lib.config.GlowCommunicationProperties;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.lib.adapter.sender
|
||||
* @className HttpEimsSender
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class HttpEimsSender implements EimsSender {
|
||||
|
||||
private final RestClient restClient;
|
||||
private final String eimsUrl;
|
||||
private final GlowCommunicationProperties glowProps;
|
||||
|
||||
public HttpEimsSender(GlowCommunicationProperties glowProps) {
|
||||
this.glowProps = glowProps;
|
||||
// yml의 대내 MCI host, port, uri를 조합하여 EIMS 호출 주소 생성
|
||||
this.eimsUrl = glowProps.getMci().getHost() + ":" + glowProps.getMci().getPort() + glowProps.getMci().getUri();
|
||||
|
||||
/*
|
||||
*********************************************** 중요 **************************************************
|
||||
this.restClient = RestClient.create();
|
||||
보통 금융권(신한라이프 등 은행/보험사)의 내부 레거시 시스템이나 MCI(Message Channel Integration) 솔루션은 HTTP/2를 기본으로 지원하지 않는 경우가 훨씬 많습니다.
|
||||
*********************************************** 중요 **************************************************
|
||||
*/
|
||||
|
||||
// HTTP/2 통신 시 Stream Cancelled(RST_STREAM) 에러 방지를 위해 HTTP/1.1 전용 Factory 사용
|
||||
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
|
||||
factory.setConnectTimeout(3000);
|
||||
factory.setReadTimeout(5000);
|
||||
|
||||
this.restClient = RestClient.builder().requestFactory(factory).build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String send(String interfaceId, String payload) {
|
||||
log.info(" [HTTP 모드] EIMS API 호출 중... URL: {}", eimsUrl);
|
||||
|
||||
// EIMS가 요구하는 JSON 포맷으로 래핑해서 전송 (EIMS 규격에 따라 수정 가능)
|
||||
Map<String, String> requestBody = Map.of(
|
||||
"interfaceId", interfaceId,
|
||||
"data", payload
|
||||
);
|
||||
|
||||
// 4번 항목 적용: MDC에 저장된 traceId를 추출하여 HTTP Header(X-Trace-Id)로 전파
|
||||
String traceId = MDC.get("traceId");
|
||||
if (traceId == null) traceId = "SYSTEM-GENERATED-" + UUID.randomUUID().toString();
|
||||
|
||||
return restClient.post()
|
||||
.uri(eimsUrl)
|
||||
.header("X-Trace-Id", traceId)
|
||||
.header("X-Shinhan-Global-ID", traceId)
|
||||
.body(requestBody)
|
||||
.retrieve()
|
||||
.body(String.class); // 응답 결과를 String으로 받음
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package io.shinhanlife.dap.lib.adapter.sender;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.client.RestClient;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.lib.adapter.sender
|
||||
* @className JspFormEimsSender
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class JspFormEimsSender implements EimsSender {
|
||||
|
||||
private final RestClient restClient;
|
||||
private final String jspUrl;
|
||||
|
||||
public JspFormEimsSender(@Value("${eims.jsp.form.url}") String jspUrl) {
|
||||
this.jspUrl = jspUrl;
|
||||
this.restClient = RestClient.builder().build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String send(String interfaceId, String payload) {
|
||||
log.info(" [JSP Form 모드] 레거시 폼 데이터 전송 중... URL: {}", jspUrl);
|
||||
|
||||
// 1. Form Data 조립 (HTML <form> 태그 전송과 동일한 효과)
|
||||
MultiValueMap<String, String> formData = new LinkedMultiValueMap<>();
|
||||
formData.add("interfaceId", interfaceId);
|
||||
formData.add("data", payload);
|
||||
|
||||
// 2. HTTP 전송 (Content-Type: application/x-www-form-urlencoded)
|
||||
String rawResponse = restClient.post()
|
||||
.uri(jspUrl)
|
||||
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
|
||||
.body(formData)
|
||||
.retrieve()
|
||||
.body(String.class);
|
||||
|
||||
// 3. JSP 특유의 앞뒤 공백 및 엔터 제거
|
||||
return rawResponse != null ? rawResponse.trim() : "";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package io.shinhanlife.dap.lib.adapter.sender;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.client.RestClient;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.lib.adapter.sender
|
||||
* @className JspJsonEimsSender
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class JspJsonEimsSender implements EimsSender {
|
||||
|
||||
private final RestClient restClient;
|
||||
private final String jspUrl;
|
||||
|
||||
// 여기서 eims.jsp.json.url 딱 하나만 깔끔하게 받아옵니다!
|
||||
public JspJsonEimsSender(@Value("${eims.jsp.json.url}") String jspUrl) {
|
||||
this.jspUrl = jspUrl;
|
||||
this.restClient = RestClient.builder().build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String send(String interfaceId, String payload) {
|
||||
log.info(" [JSP JSON 모드] JSON 페이로드 전송 중... URL: {}", jspUrl);
|
||||
|
||||
// 1. JSON 객체로 조립 (스프링이 알아서 JSON String으로 변환해 줌)
|
||||
Map<String, String> jsonBody = Map.of(
|
||||
"interfaceId", interfaceId,
|
||||
"data", payload
|
||||
);
|
||||
|
||||
// 2. HTTP 전송 (Content-Type: application/json)
|
||||
String rawResponse = restClient.post()
|
||||
.uri(jspUrl)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.body(jsonBody)
|
||||
.retrieve()
|
||||
.body(String.class);
|
||||
|
||||
// 3. JSP 응답 정제
|
||||
return rawResponse != null ? rawResponse.trim() : "";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package io.shinhanlife.dap.lib.adapter.sender;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.client.SimpleClientHttpRequestFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StopWatch;
|
||||
import org.springframework.web.client.RestClient;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.lib.adapter.sender
|
||||
* @className MciEimsSender
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@Slf4j
|
||||
@Service("mciEimsSender")
|
||||
public class MciEimsSender implements EimsSender {
|
||||
|
||||
private final ObjectMapper jsonMapper;
|
||||
private final XmlMapper xmlMapper;
|
||||
private final RestClient restClient; // Spring Boot 3.2+ 최신 HTTP 클라이언트
|
||||
private final String mciUrl;
|
||||
|
||||
public MciEimsSender(ObjectMapper jsonMapper, XmlMapper xmlMapper, @Value("${eims.mci.url}") String mciUrl) {
|
||||
this.jsonMapper = jsonMapper;
|
||||
this.xmlMapper = xmlMapper;
|
||||
this.mciUrl = mciUrl;
|
||||
this.restClient = RestClient.create(); // 클라이언트 초기화
|
||||
|
||||
/*
|
||||
*********************************************** 중요 **************************************************
|
||||
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
|
||||
factory.setConnectTimeout(3000);
|
||||
factory.setReadTimeout(5000);
|
||||
this.restClient = RestClient.builder().requestFactory(factory).build();
|
||||
보통 금융권(신한라이프 등 은행/보험사)의 내부 레거시 시스템이나 MCI(Message Channel Integration) 솔루션은 HTTP/2를 기본으로 지원하지 않는 경우가 훨씬 많습니다.
|
||||
*********************************************** 중요 **************************************************
|
||||
*/
|
||||
}
|
||||
|
||||
@Override
|
||||
public String send(String interfaceId, String jsonPayload) throws Exception {
|
||||
StopWatch stopWatch = new StopWatch(); stopWatch.start();
|
||||
|
||||
try {
|
||||
JsonNode jsonNode = jsonMapper.readTree(jsonPayload);
|
||||
String xmlData = xmlMapper.writer().withRootName("Body").writeValueAsString(jsonNode);
|
||||
String esbStandardXml = wrapWithEsbHeader(interfaceId, xmlData);
|
||||
|
||||
log.info(" [ESB 어댑터] 전송 준비 완료 - RestClient 호출 시작");
|
||||
|
||||
String responseXml = restClient.post()
|
||||
.uri(mciUrl)
|
||||
.contentType(MediaType.APPLICATION_XML)
|
||||
.body(esbStandardXml)
|
||||
.retrieve()
|
||||
.body(String.class);
|
||||
|
||||
log.info(" [ESB 어댑터] 응답 수신 완료: {}", responseXml);
|
||||
|
||||
JsonNode responseNode = xmlMapper.readTree(responseXml);
|
||||
return jsonMapper.writeValueAsString(responseNode);
|
||||
|
||||
} finally {
|
||||
stopWatch.stop();
|
||||
log.info(" [SLA 모니터링 - MCI] 소요시간: {} ms", stopWatch.getTotalTimeMillis());
|
||||
}
|
||||
}
|
||||
|
||||
private String wrapWithEsbHeader(String interfaceId, String xmlData) {
|
||||
return String.format("<EsbMessage><Header><InterfaceId>%s</InterfaceId></Header><Body>%s</Body></EsbMessage>", interfaceId, xmlData);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package io.shinhanlife.dap.lib.adapter.sender;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StopWatch;
|
||||
import org.springframework.web.client.RestClient;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.lib.adapter.sender
|
||||
* @className MciStringEimsSender
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@Slf4j
|
||||
@Service("mciStringEimsSender")
|
||||
public class MciStringEimsSender implements EimsSender {
|
||||
|
||||
private final RestClient restClient;
|
||||
private final String mciUrl;
|
||||
|
||||
public MciStringEimsSender(@Value("${eims.mcistring.url}") String mciUrl) {
|
||||
this.mciUrl = mciUrl;
|
||||
this.restClient = RestClient.create();
|
||||
/*
|
||||
*********************************************** 중요 **************************************************
|
||||
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
|
||||
factory.setConnectTimeout(3000);
|
||||
factory.setReadTimeout(5000);
|
||||
this.restClient = RestClient.builder().requestFactory(factory).build();
|
||||
보통 금융권(신한라이프 등 은행/보험사)의 내부 레거시 시스템이나 MCI(Message Channel Integration) 솔루션은 HTTP/2를 기본으로 지원하지 않는 경우가 훨씬 많습니다.
|
||||
*********************************************** 중요 **************************************************
|
||||
*/
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public String send(String interfaceId, String payload) throws Exception {
|
||||
StopWatch stopWatch = new StopWatch(); stopWatch.start();
|
||||
|
||||
try {
|
||||
log.info(" [ESB 어댑터(String)] 전송 준비 완료 - RestClient 호출 시작 (Interface: {})", interfaceId);
|
||||
|
||||
String response = restClient.post()
|
||||
.uri(mciUrl)
|
||||
.contentType(MediaType.TEXT_PLAIN)
|
||||
.body(payload != null ? payload : "")
|
||||
.retrieve()
|
||||
.body(String.class);
|
||||
|
||||
log.info(" [ESB 어댑터(String)] 응답 수신 완료: {}", response);
|
||||
return response != null ? response : "";
|
||||
|
||||
} finally {
|
||||
stopWatch.stop();
|
||||
log.info(" [SLA 모니터링 - MCI(String)] 소요시간: {} ms", stopWatch.getTotalTimeMillis());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
package io.shinhanlife.dap.lib.adapter.sender;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import io.shinhanlife.dap.lib.integration.mci.config.ShinhanIntegrationProperties;
|
||||
import io.shinhanlife.dap.lib.integration.mci.dto.MciRequestWrapper;
|
||||
import io.shinhanlife.dap.lib.integration.mci.dto.ShinhanCommonHeaderDto;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StopWatch;
|
||||
import org.springframework.web.client.RestClient;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.lib.adapter.sender
|
||||
* @className ShinhanMciSender
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@EnableConfigurationProperties(ShinhanIntegrationProperties.class)
|
||||
public class ShinhanMciSender {
|
||||
|
||||
private final ObjectMapper jsonMapper;
|
||||
private final RestClient restClient;
|
||||
private final ShinhanIntegrationProperties properties;
|
||||
private final AtomicInteger sequenceGenerator = new AtomicInteger(1);
|
||||
|
||||
public ShinhanMciSender(ObjectMapper jsonMapper, ShinhanIntegrationProperties properties) {
|
||||
this.jsonMapper = jsonMapper;
|
||||
this.properties = properties;
|
||||
this.restClient = RestClient.create();
|
||||
}
|
||||
|
||||
public <T> String send(String targetUrl, MciRequestWrapper<T> requestWrapper) throws Exception {
|
||||
StopWatch stopWatch = new StopWatch();
|
||||
stopWatch.start();
|
||||
|
||||
try {
|
||||
ShinhanCommonHeaderDto header = requestWrapper.getTgrmCmnnhddValu();
|
||||
if (header == null) {
|
||||
header = new ShinhanCommonHeaderDto();
|
||||
requestWrapper.setTgrmCmnnhddValu(header);
|
||||
}
|
||||
|
||||
// 필수 헤더 자동 세팅 로직
|
||||
header.setEnvrTypeCd(properties.getEnvrTypeCd());
|
||||
header.setReqRspnScCd("S"); // S: 요청
|
||||
header.setAppliDutjCd("DAP"); // 어플리케이션업무코드
|
||||
|
||||
if (header.getGlbId() == null || header.getGlbId().isEmpty()) {
|
||||
header.setGlbId(generateGlbId());
|
||||
}
|
||||
|
||||
int currentSeq = sequenceGenerator.getAndIncrement();
|
||||
header.setPgrsSriaNo(String.format("%03d", currentSeq)); // 3자리
|
||||
|
||||
if (header.getReqTgrmTnsmDtptDt() == null) {
|
||||
header.setReqTgrmTnsmDtptDt(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS")));
|
||||
}
|
||||
|
||||
String jsonPayload = jsonMapper.writeValueAsString(requestWrapper);
|
||||
|
||||
log.info(" [신한 통합 MCI 어댑터] 전송 준비 완료 - URL: {}", targetUrl);
|
||||
log.debug(" [신한 통합 MCI 어댑터] 요청 Payload: {}", jsonPayload);
|
||||
|
||||
String responseJson = restClient.post()
|
||||
.uri(targetUrl)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.body(jsonPayload)
|
||||
.retrieve()
|
||||
.body(String.class);
|
||||
|
||||
log.info(" [신한 통합 MCI 어댑터] 응답 수신 완료");
|
||||
log.debug(" [신한 통합 MCI 어댑터] 응답 Payload: {}", responseJson);
|
||||
|
||||
return responseJson;
|
||||
|
||||
} finally {
|
||||
stopWatch.stop();
|
||||
log.info(" [SLA 모니터링 - MCI 연계] 소요시간: {} ms", stopWatch.getTotalTimeMillis());
|
||||
}
|
||||
}
|
||||
|
||||
private String generateGlbId() {
|
||||
// 전사공통키 (37 Byte)
|
||||
// 전문생성상세일시(17) + 전문생성시스템명(9) + 어플리케이션업무코드(3) + 전문세션번호(8)
|
||||
String timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS")); // 17 byte
|
||||
String systemName = String.format("%-9s", "AXHUB"); // 9 byte, left-aligned padded with spaces
|
||||
String appCode = "DAP"; // 3 byte
|
||||
String sessionNo = UUID.randomUUID().toString().substring(0, 8).toUpperCase(); // 8 byte
|
||||
return timestamp + systemName + appCode + sessionNo;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package io.shinhanlife.dap.lib.adapter.sender;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.Socket;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.lib.adapter.sender
|
||||
* @className TcpEimsSender
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class TcpEimsSender implements EimsSender {
|
||||
|
||||
@Value("${eims.tcp.host}")
|
||||
private String host;
|
||||
|
||||
@Value("${eims.tcp.port}")
|
||||
private int port;
|
||||
|
||||
@Value("${eims.tcp.timeout}")
|
||||
private int timeout;
|
||||
|
||||
@Override
|
||||
public String send(String interfaceId, String payload) throws Exception {
|
||||
log.info(" [TCP 소켓 모드] EIMS 접속 중... {}:{}", host, port);
|
||||
|
||||
// TCP 소켓 자원을 사용 후 안전하게 닫아주는 try-with-resources 구문
|
||||
try (Socket socket = new Socket()) {
|
||||
// 1. 타임아웃 및 연결 설정
|
||||
socket.connect(new InetSocketAddress(host, port), timeout);
|
||||
socket.setSoTimeout(timeout); // 읽기 타임아웃
|
||||
|
||||
OutputStream os = socket.getOutputStream();
|
||||
InputStream is = socket.getInputStream();
|
||||
|
||||
// 2. 데이터 송신 (EUC-KR 인코딩 필수)
|
||||
// 보통 금융권 TCP 통신은 맨 앞에 전체 길이나 인터페이스 ID를 헤더로 붙입니다.
|
||||
String sendData = interfaceId + payload;
|
||||
os.write(sendData.getBytes("EUC-KR"));
|
||||
os.flush();
|
||||
|
||||
// 3. 데이터 수신
|
||||
byte[] buffer = new byte[4096];
|
||||
int readByte = is.read(buffer);
|
||||
|
||||
if (readByte == -1) {
|
||||
throw new RuntimeException("EIMS 서버가 응답 없이 연결을 종료했습니다.");
|
||||
}
|
||||
|
||||
// 받은 바이트를 다시 한글(EUC-KR) 문자열로 복원
|
||||
return new String(buffer, 0, readByte, "EUC-KR");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package io.shinhanlife.dap.lib.adapter.support;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.lib.adapter.support
|
||||
* @className DynamicPayloadBuilder
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@Service
|
||||
public class DynamicPayloadBuilder {
|
||||
|
||||
public String buildFixedLengthString(List<Map<String, Object>> specList, Map<String, Object> data) throws Exception {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
for (Map<String, Object> spec : specList) {
|
||||
String name = (String) spec.get("name");
|
||||
// EIMS가 고정장을 요구할 경우를 대비한 len 파라미터 체크 (기본값 0 방어)
|
||||
int len = spec.get("len") != null ? (Integer) spec.get("len") : 0;
|
||||
String type = (String) spec.get("type");
|
||||
String rawValue = String.valueOf(data.getOrDefault(name, ""));
|
||||
|
||||
// len 값이 없으면 변환 없이 바로 이어붙임 (JSON 통신용)
|
||||
if (len == 0) {
|
||||
sb.append(rawValue);
|
||||
continue;
|
||||
}
|
||||
|
||||
// len 값이 있으면 고정장 통신 규칙 적용
|
||||
byte[] rawBytes = rawValue.getBytes("EUC-KR");
|
||||
if (rawBytes.length > len) {
|
||||
throw new IllegalArgumentException(name + " 길이가 " + len + " 바이트를 초과합니다.");
|
||||
}
|
||||
|
||||
int padLength = len - rawBytes.length;
|
||||
StringBuilder paddedValue = new StringBuilder(rawValue);
|
||||
|
||||
if ("NUMBER".equalsIgnoreCase(type)) {
|
||||
for (int i = 0; i < padLength; i++) paddedValue.insert(0, "0");
|
||||
} else {
|
||||
for (int i = 0; i < padLength; i++) paddedValue.append(" ");
|
||||
}
|
||||
sb.append(paddedValue.toString());
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package io.shinhanlife.dap.lib.adapter.support;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.lib.adapter.support
|
||||
* @className DynamicSchemaValidator
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@Service
|
||||
public class DynamicSchemaValidator {
|
||||
|
||||
public boolean validate(List<Map<String, Object>> specList, Map<String, Object> data, StringBuilder errorLog) {
|
||||
for (Map<String, Object> spec : specList) {
|
||||
String name = (String) spec.get("name");
|
||||
String type = (String) spec.get("type");
|
||||
boolean isRequired = spec.get("required") != null && (Boolean) spec.get("required");
|
||||
Object value = data.get(name);
|
||||
|
||||
// 1. 필수값 체크
|
||||
if (isRequired && (value == null || String.valueOf(value).trim().isEmpty())) {
|
||||
errorLog.append(String.format("[%s] 필드는 필수 입력 항목입니다. ", name));
|
||||
return false;
|
||||
}
|
||||
|
||||
// 2. 타입 체크 (값이 있을 때만)
|
||||
if (value != null && !String.valueOf(value).trim().isEmpty()) {
|
||||
if ("NUMBER".equalsIgnoreCase(type) && !String.valueOf(value).matches("-?\\d+(\\.\\d+)?")) {
|
||||
errorLog.append(String.format("[%s] 필드는 숫자여야 합니다. ", name));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package io.shinhanlife.dap.lib.adapter.support;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import io.shinhanlife.dap.lib.adapter.exception.MciCommunicationException;
|
||||
import io.shinhanlife.dap.lib.adapter.sender.EimsSender;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.lib.adapter.support
|
||||
* @className MciTemplate
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class MciTemplate {
|
||||
|
||||
private final EimsSender httpEimsSender;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
/**
|
||||
* MCI 인터페이스를 호출하고 결과를 지정된 타입으로 반환합니다.
|
||||
*
|
||||
* @param interfaceId 호출할 MCI 인터페이스 ID
|
||||
* @param requestDto 요청 데이터 객체
|
||||
* @param responseType 응답을 매핑할 클래스 타입
|
||||
* @param <T> 요청 객체 타입
|
||||
* @param <R> 응답 객체 타입
|
||||
* @return 매핑된 응답 객체
|
||||
* @throws MciCommunicationException 통신 또는 파싱 실패 시 예외 발생
|
||||
*/
|
||||
public <T, R> R call(String interfaceId, T requestDto, Class<R> responseType) {
|
||||
log.info(" [MciTemplate] 시작 - Interface ID: {}", interfaceId);
|
||||
|
||||
try {
|
||||
String payload = objectMapper.writeValueAsString(requestDto);
|
||||
log.debug(" [MciTemplate] 전송 페이로드: {}", payload);
|
||||
|
||||
String responseJson = httpEimsSender.send(interfaceId, payload);
|
||||
log.debug(" [MciTemplate] 수신 응답 JSON: {}", responseJson);
|
||||
|
||||
R response = objectMapper.readValue(responseJson, responseType);
|
||||
log.info(" [MciTemplate] 완료 - Interface ID: {}", interfaceId);
|
||||
|
||||
return response;
|
||||
|
||||
} catch (JsonProcessingException e) {
|
||||
log.error(" [MciTemplate] JSON 변환 중 오류 발생", e);
|
||||
throw new MciCommunicationException("MCI 통신 중 JSON 파싱 오류", e);
|
||||
} catch (Exception e) {
|
||||
log.error(" [MciTemplate] 통신 중 오류 발생", e);
|
||||
throw new MciCommunicationException("MCI 통신 실패", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package io.shinhanlife.dap.lib.adapter.support;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.lib.adapter.support
|
||||
* @className ResultStandardizer
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@Component
|
||||
public class ResultStandardizer {
|
||||
|
||||
// 오라클 조회 결과(Map)의 키 값을 카멜 케이스로 변환 (스키마 매핑)
|
||||
public Map<String, Object> standardize(Map<String, Object> rawData) {
|
||||
Map<String, Object> standardMap = new HashMap<>();
|
||||
|
||||
rawData.forEach((key, value) -> {
|
||||
String camelKey = convertToCamelCase(key);
|
||||
standardMap.put(camelKey, value);
|
||||
});
|
||||
|
||||
return standardMap;
|
||||
}
|
||||
|
||||
private String convertToCamelCase(String snakeCase) {
|
||||
if (snakeCase == null || snakeCase.isEmpty()) return snakeCase;
|
||||
|
||||
StringBuilder result = new StringBuilder();
|
||||
boolean nextIsUpper = false;
|
||||
|
||||
for (char c : snakeCase.toLowerCase().toCharArray()) {
|
||||
if (c == '_') {
|
||||
nextIsUpper = true;
|
||||
} else {
|
||||
result.append(nextIsUpper ? Character.toUpperCase(c) : c);
|
||||
nextIsUpper = false;
|
||||
}
|
||||
}
|
||||
return result.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package io.shinhanlife.dap.lib.adapter.support;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.lib.adapter.support
|
||||
* @className TicketManager
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class TicketManager {
|
||||
|
||||
// 로컬 시뮬레이션을 위한 인메모리 저장소 (운영 환경에서는 Redis 등을 사용)
|
||||
private final Map<String, Ticket> ticketStore = new ConcurrentHashMap<>();
|
||||
|
||||
// 가짜 EAI 작업(10초 대기)을 실행할 백그라운드 쓰레드 풀
|
||||
private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(5);
|
||||
|
||||
/**
|
||||
* 새로운 비동기 작업을 접수하고 티켓을 발급합니다.
|
||||
*/
|
||||
public String issueTicket(String interfaceId, String payload) {
|
||||
String ticketId = "TICKET-" + UUID.randomUUID().toString();
|
||||
|
||||
Ticket ticket = new Ticket();
|
||||
ticket.setTicketId(ticketId);
|
||||
ticket.setStatus("PROCESSING");
|
||||
ticket.setMessage("EAI 시스템에서 데이터 처리 중입니다...");
|
||||
|
||||
ticketStore.put(ticketId, ticket);
|
||||
|
||||
log.info(" [TicketManager] 비동기 작업 티켓 발급 완료: {}", ticketId);
|
||||
|
||||
// 10초 뒤에 자동으로 작업을 완료 상태로 변경하는 백그라운드 시뮬레이터 실행
|
||||
simulateEaiProcessing(ticketId, interfaceId);
|
||||
|
||||
return ticketId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 티켓의 현재 상태를 조회합니다.
|
||||
*/
|
||||
public Ticket getTicketStatus(String ticketId) {
|
||||
return ticketStore.getOrDefault(ticketId, new Ticket("NOT_FOUND", "해당 티켓을 찾을 수 없습니다."));
|
||||
}
|
||||
|
||||
/**
|
||||
* 10초 뒤에 상태를 COMPLETED로 변경하여 진짜 EAI 배치가 끝난 것처럼 흉내냅니다.
|
||||
*/
|
||||
private void simulateEaiProcessing(String ticketId, String interfaceId) {
|
||||
scheduler.schedule(() -> {
|
||||
Ticket ticket = ticketStore.get(ticketId);
|
||||
if (ticket != null) {
|
||||
ticket.setStatus("COMPLETED");
|
||||
ticket.setMessage("EAI 배치가 정상적으로 완료되었습니다.");
|
||||
// 가짜 최종 결과 데이터 주입
|
||||
ticket.setResultData("{\"resultCode\":\"0000\", \"interfaceId\":\"" + interfaceId + "\", \"processedRecords\":50000}");
|
||||
ticketStore.put(ticketId, ticket);
|
||||
log.info(" [TicketManager] EAI 비동기 작업 시뮬레이션 완료! (Ticket: {})", ticketId);
|
||||
}
|
||||
}, 10, TimeUnit.SECONDS); // 10초 지연
|
||||
}
|
||||
|
||||
// 티켓 상태를 담을 내부 DTO 클래스
|
||||
@lombok.Data
|
||||
public static class Ticket {
|
||||
private String ticketId;
|
||||
private String status; // PROCESSING, COMPLETED, FAILED, NOT_FOUND
|
||||
private String message;
|
||||
private String resultData; // 최종 완료 시 담길 데이터
|
||||
|
||||
public Ticket() {}
|
||||
public Ticket(String status, String message) {
|
||||
this.status = status;
|
||||
this.message = message;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package io.shinhanlife.dap.lib.adapter.support;
|
||||
|
||||
import io.shinhanlife.dap.lib.adapter.connector.LegacyEimsConnector;
|
||||
import io.shinhanlife.dap.lib.adapter.dto.ErrorDetail;
|
||||
import io.shinhanlife.dap.lib.adapter.dto.JsonRpcRequest;
|
||||
import io.shinhanlife.dap.lib.adapter.dto.JsonRpcResponse;
|
||||
import io.shinhanlife.dap.lib.adapter.dto.Params;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.lib.adapter.support
|
||||
* @className ToolExecutionService
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class ToolExecutionService {
|
||||
|
||||
// 방어막(서킷/캐시)이 적용된 커넥터 주입
|
||||
private final LegacyEimsConnector legacyEimsConnector;
|
||||
|
||||
/**
|
||||
* AI Agent가 호출한 툴을 실제 레거시 커넥터로 전달합니다.
|
||||
*/
|
||||
public JsonRpcResponse executeTool(JsonRpcRequest request) {
|
||||
Params params = request.getParams();
|
||||
|
||||
// 1. 필수 파라미터 검증
|
||||
if (params == null || params.getName() == null) {
|
||||
return createErrorResponse(request.getId(), -32602, "Invalid params: 'name' is required");
|
||||
}
|
||||
|
||||
try {
|
||||
// 2. Connector의 executeByTool 메서드 호출
|
||||
// [참고] connector에 선언된 파라미터 구조에 맞게 매핑합니다.
|
||||
String result = legacyEimsConnector.executeByTool(
|
||||
params.getRoutingType(),
|
||||
params.getInterfaceId(),
|
||||
params.getData(),
|
||||
params.getSpec()
|
||||
);
|
||||
|
||||
// 3. 성공 응답 생성 (result가 JSON 문자열일 경우, 실제 DTO로 변환하여 반환하면 더 좋습니다)
|
||||
return createSuccessResponse(request.getId(), result);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error(" [ToolExecution] 커넥터 호출 실패: RoutingType={}, Error={}", params.getRoutingType(), e.getMessage());
|
||||
return createErrorResponse(request.getId(), -32000, "Connector error: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* AI Agent를 위한 툴 목록 스키마 반환
|
||||
*/
|
||||
public JsonRpcResponse getToolList(String requestId) {
|
||||
// 기존에 정의한 Tool 스키마 리스트 반환 로직...
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
// ... (Tool 명세 내용)
|
||||
return createSuccessResponse(requestId, result);
|
||||
}
|
||||
|
||||
private JsonRpcResponse createSuccessResponse(String id, Object result) {
|
||||
JsonRpcResponse response = new JsonRpcResponse();
|
||||
response.setId(id);
|
||||
response.setResult(result);
|
||||
return response;
|
||||
}
|
||||
|
||||
private JsonRpcResponse createErrorResponse(String id, int code, String message) {
|
||||
JsonRpcResponse response = new JsonRpcResponse();
|
||||
response.setId(id);
|
||||
response.setError(new ErrorDetail(code, message));
|
||||
return response;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package io.shinhanlife.dap.lib.adapter.test;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.lib.adapter.test
|
||||
* @className MockEimsHttpServer
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api")
|
||||
public class MockEimsHttpServer {
|
||||
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
public MockEimsHttpServer(ObjectMapper objectMapper) {
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
@PostMapping("/gateway")
|
||||
public Map<String, Object> mockEimsReceiver(
|
||||
@RequestHeader(value = "X-Trace-Id", required = false) String traceId,
|
||||
@RequestBody Map<String, Object> request) {
|
||||
|
||||
String interfaceId = (String) request.get("interfaceId");
|
||||
String data = (String) request.get("data");
|
||||
|
||||
log.info(" [가짜 EIMS 서버] HTTP 요청 수신 완료!");
|
||||
log.info(" 수신된 Trace-ID (거래고유번호): {}", traceId != null ? traceId : "없음");
|
||||
log.info(" 인터페이스ID: {}, 데이터: [{}]", interfaceId, data);
|
||||
|
||||
// No-Code Mock 응답 생성 (JSON 설정 파일 기반)
|
||||
try {
|
||||
ClassPathResource resource = new ClassPathResource("mock-responses.json");
|
||||
Map<String, Object> mockDataMap = objectMapper.readValue(resource.getInputStream(), Map.class);
|
||||
|
||||
if (mockDataMap.containsKey(interfaceId)) {
|
||||
return (Map<String, Object>) mockDataMap.get(interfaceId);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("JSON 파싱 에러 또는 mock 파일 읽기 실패 (기본 응답 반환)", e);
|
||||
}
|
||||
|
||||
// 기본 응답
|
||||
return Map.of(
|
||||
"status", "404",
|
||||
"message", "MOCK 데이터가 정의되지 않았습니다.",
|
||||
"receivedLength", data.length()
|
||||
);
|
||||
}
|
||||
|
||||
@PostMapping("/mock/esb/api")
|
||||
public String mockEsbReceiver(@RequestBody String xmlPayload) {
|
||||
log.info(" [가짜 ESB 서버] MCI/ESB 요청 수신 완료!");
|
||||
log.info(" 수신된 XML 전문: {}", xmlPayload);
|
||||
|
||||
// MciEimsSender가 기대하는 JSON 변환용 XML 포맷 응답
|
||||
return "<Response><status>SUCCESS</status><message>MOCK_MCI_EIMS_RECEIVE_SUCCESS</message><data><info>정상 처리되었습니다.</info></data></Response>";
|
||||
}
|
||||
|
||||
@PostMapping("/mock/esb/string")
|
||||
public String mockEsbStringReceiver(@RequestBody(required = false) String payload) {
|
||||
log.info(" [가짜 ESB 서버] MCI String 요청 수신 완료!");
|
||||
log.info(" 수신된 String 전문: {}", payload);
|
||||
|
||||
// MciSampleStringResponse 에 맞게 고정 길이 응답 생성
|
||||
// name (10), age (3), joinDate (8), statusCode (2)
|
||||
// targetList (30) -> MciSampleTargetDto (itemCode 5, itemValue 5) x 3
|
||||
return "홍길동 03020260901OKA0001B0001A0002B0002A0003B0003";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
package io.shinhanlife.dap.lib.adapter.test;
|
||||
|
||||
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
||||
import org.springframework.context.event.EventListener;
|
||||
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.annotation.PreDestroy;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Profile;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.ServerSocket;
|
||||
import java.net.Socket;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import java.util.Map;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
// 대신 "local" 환경(application-local.properties)에서만 가짜 소켓 서버가 켜지도록 보장합니다.
|
||||
/**
|
||||
* @package io.shinhanlife.dap.lib.adapter.test
|
||||
* @className MockEimsTcpServer
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@Profile("local")
|
||||
public class MockEimsTcpServer {
|
||||
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
public MockEimsTcpServer(ObjectMapper objectMapper) {
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
@Value("${eims.tcp.port:8090}")
|
||||
private int port;
|
||||
|
||||
@Value("${server.port:8081}")
|
||||
private int serverPort;
|
||||
|
||||
private ServerSocket serverSocket;
|
||||
private boolean running = true;
|
||||
|
||||
@EventListener(ApplicationReadyEvent.class)
|
||||
public void startTcpServer() {
|
||||
// MSA 환경에서는 각 툴 Pod 내부에서 독립적인 가짜 EIMS 서버가 실행되도록 허용
|
||||
log.info(" [가짜 EIMS 서버] 로컬 테스트 환경용 EIMS TCP 서버 가동을 준비합니다.");
|
||||
|
||||
// Java 21 가상 스레드를 사용하여 메인 서버 가동에 방해 없이 백그라운드에서 실행
|
||||
Thread.ofVirtual().start(() -> {
|
||||
try {
|
||||
serverSocket = new ServerSocket(port);
|
||||
log.info(" [가짜 EIMS 서버] 로컬 TCP 소켓 서버 가동 완료 (Port: {})", port);
|
||||
|
||||
while (running) {
|
||||
Socket clientSocket = serverSocket.accept();
|
||||
|
||||
// 연결된 요청을 별도 스레드로 처리
|
||||
Thread.ofVirtual().start(() -> handleClient(clientSocket));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
if (running) log.error(" 가짜 TCP 서버 에러: {}", e.getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void handleClient(Socket socket) {
|
||||
try (socket) {
|
||||
InputStream is = socket.getInputStream();
|
||||
OutputStream os = socket.getOutputStream();
|
||||
|
||||
byte[] buffer = new byte[4096];
|
||||
int readByte = is.read(buffer);
|
||||
|
||||
if (readByte != -1) {
|
||||
String receivedData = new String(buffer, 0, readByte, "EUC-KR");
|
||||
log.info(" [가짜 EIMS 서버] TCP 데이터 수신 완료!");
|
||||
log.info(" 수신된 전문: [{}]", receivedData);
|
||||
|
||||
String responseData = "{\"status\":\"404\",\"message\":\"MOCK 데이터가 정의되지 않았습니다.\"}";
|
||||
try {
|
||||
int braceIndex = receivedData.indexOf('{');
|
||||
String interfaceId = "";
|
||||
if (braceIndex > 0) {
|
||||
interfaceId = receivedData.substring(0, braceIndex).trim();
|
||||
} else if (braceIndex == -1) {
|
||||
interfaceId = receivedData.trim();
|
||||
}
|
||||
|
||||
ClassPathResource resource = new ClassPathResource("mock-responses.json");
|
||||
Map<String, Object> mockDataMap = objectMapper.readValue(resource.getInputStream(), Map.class);
|
||||
|
||||
if (mockDataMap.containsKey(interfaceId)) {
|
||||
responseData = objectMapper.writeValueAsString(mockDataMap.get(interfaceId));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("TCP JSON 파싱 에러 또는 파일 읽기 실패", e);
|
||||
}
|
||||
|
||||
// 응답 데이터 송신
|
||||
os.write(responseData.getBytes("EUC-KR"));
|
||||
os.flush();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error(" 가짜 TCP 클라이언트 처리 에러: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
public void stopTcpServer() {
|
||||
this.running = false;
|
||||
try {
|
||||
if (serverSocket != null) serverSocket.close();
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package io.shinhanlife.dap.lib.adapter.test;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.lib.adapter.test
|
||||
* @className MockJspServer
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/mock")
|
||||
public class MockJspServer {
|
||||
|
||||
// ==========================================
|
||||
// 1. Form Data 방식 테스트 수신부 (jsp-form)
|
||||
// ==========================================
|
||||
@PostMapping(value = "/jsp-form", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
|
||||
public String mockJspFormReceiver(
|
||||
@RequestParam("interfaceId") String interfaceId,
|
||||
@RequestParam("data") String data) {
|
||||
|
||||
log.info(" [가짜 JSP 서버] 폼 데이터(Form) 수신 완료!");
|
||||
log.info(" 파라미터 파싱 확인 - ID: {}, DATA: [{}]", interfaceId, data);
|
||||
|
||||
// 실제 JSP 서버처럼 앞뒤에 의미 없는 줄바꿈(엔터)과 공백을 잔뜩 넣어서 리턴합니다.
|
||||
return "\n\n SUCCESS_FROM_MOCK_JSP_FORM \n\n";
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// 2. JSON 방식 테스트 수신부 (jsp-json)
|
||||
// ==========================================
|
||||
@PostMapping(value = "/jsp-json", consumes = MediaType.APPLICATION_JSON_VALUE)
|
||||
public String mockJspJsonReceiver(@RequestBody Map<String, String> request) {
|
||||
|
||||
String interfaceId = request.get("interfaceId");
|
||||
String data = request.get("data");
|
||||
|
||||
log.info(" [가짜 JSP 서버] 제이슨(JSON) 수신 완료!");
|
||||
log.info(" JSON 파싱 확인 - ID: {}, DATA: [{}]", interfaceId, data);
|
||||
|
||||
// 여기도 마찬가지로 쓰레기 여백을 넣어줍니다.
|
||||
return "\n\n SUCCESS_FROM_MOCK_JSP_JSON \n\n";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package io.shinhanlife.dap.lib.adapter.util;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.lib.adapter.util
|
||||
* @className LegacyDataTransformer
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@Slf4j
|
||||
public class LegacyDataTransformer {
|
||||
|
||||
/**
|
||||
* AI 모델이 보낸 자유로운 형태의 data를 레거시 시스템 규격(spec)에 맞게 강제 변환합니다.
|
||||
*
|
||||
* @param data AI가 보낸 파라미터 맵
|
||||
* @param spec 레거시 시스템이 요구하는 파라미터 스펙 (name, type, maxLength, defaultValue, required 등)
|
||||
* @return 엄격하게 정제된 파라미터 맵
|
||||
*/
|
||||
public static Map<String, Object> transform(Map<String, Object> data, List<Map<String, Object>> spec) {
|
||||
if (spec == null || spec.isEmpty()) {
|
||||
return data != null ? data : new HashMap<>();
|
||||
}
|
||||
|
||||
Map<String, Object> transformedData = new HashMap<>();
|
||||
|
||||
for (Map<String, Object> fieldSpec : spec) {
|
||||
String fieldName = (String) fieldSpec.get("name");
|
||||
if (fieldName == null) continue;
|
||||
|
||||
Object rawValue = data != null ? data.get(fieldName) : null;
|
||||
Object finalValue = rawValue;
|
||||
|
||||
// 1. 기본값(Default Value) 주입
|
||||
if (finalValue == null && fieldSpec.containsKey("defaultValue")) {
|
||||
finalValue = fieldSpec.get("defaultValue");
|
||||
log.debug(" [DataTransformer] '{}' 필드 누락 -> 기본값 '{}' 주입", fieldName, finalValue);
|
||||
}
|
||||
|
||||
// 2. 강제 형변환 (Type Coercion)
|
||||
String type = (String) fieldSpec.getOrDefault("type", "string");
|
||||
if (finalValue != null) {
|
||||
if ("string".equalsIgnoreCase(type) && !(finalValue instanceof String)) {
|
||||
finalValue = String.valueOf(finalValue);
|
||||
log.debug(" [DataTransformer] '{}' 필드 강제 String 형변환", fieldName);
|
||||
} else if ("number".equalsIgnoreCase(type) && finalValue instanceof String) {
|
||||
try {
|
||||
finalValue = Long.parseLong((String) finalValue);
|
||||
log.debug(" [DataTransformer] '{}' 필드 강제 Number 형변환", fieldName);
|
||||
} catch (NumberFormatException e) {
|
||||
log.warn(" [DataTransformer] '{}' 필드 Number 형변환 실패. 기존 값 유지", fieldName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 길이 제한 (Truncation / MaxLength)
|
||||
if (finalValue instanceof String && fieldSpec.containsKey("maxLength")) {
|
||||
int maxLength = (Integer) fieldSpec.get("maxLength");
|
||||
String strVal = (String) finalValue;
|
||||
if (strVal.length() > maxLength) {
|
||||
finalValue = strVal.substring(0, maxLength);
|
||||
log.warn(" [DataTransformer] '{}' 필드 길이 초과! {}자로 강제 자름 (Truncated)", fieldName, maxLength);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 필수값(Required) 누락 체크 (에러를 던지지 않고 빈 문자열 강제 주입하여 레거시 팅김 방지)
|
||||
boolean isRequired = (Boolean) fieldSpec.getOrDefault("required", false);
|
||||
if (isRequired && finalValue == null) {
|
||||
log.error(" [DataTransformer] 필수 필드 '{}' 누락! 강제 공백 주입하여 시스템 장애 방지", fieldName);
|
||||
finalValue = "string".equalsIgnoreCase(type) ? "" : 0;
|
||||
}
|
||||
|
||||
if (finalValue != null) {
|
||||
transformedData.put(fieldName, finalValue);
|
||||
}
|
||||
}
|
||||
|
||||
return transformedData;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package io.shinhanlife.dap.lib.adapter.util;
|
||||
|
||||
import ch.qos.logback.classic.pattern.MessageConverter;
|
||||
import ch.qos.logback.classic.spi.ILoggingEvent;
|
||||
|
||||
/**
|
||||
* Logback 커스텀 컨버터
|
||||
* 모든 로그 메시지(%msg)가 파일이나 콘솔에 찍히기 직전에 이 클래스를 거쳐가게 됩니다.
|
||||
* 여기서 PiiMaskingUtils.mask()를 호출하여 PII(주민번호, 계좌번호 등)를 안전하게 별표(*) 처리합니다.
|
||||
*/
|
||||
/**
|
||||
* @package io.shinhanlife.dap.lib.adapter.util
|
||||
* @className PiiMaskingLogbackConverter
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
public class PiiMaskingLogbackConverter extends MessageConverter {
|
||||
|
||||
@Override
|
||||
public String convert(ILoggingEvent event) {
|
||||
// 원본 로그 메시지를 가져옵니다.
|
||||
String originalMessage = super.convert(event);
|
||||
|
||||
// 정규식을 이용하여 개인정보가 포함되어 있으면 마스킹 처리하여 반환합니다.
|
||||
return PiiMaskingUtils.mask(originalMessage);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package io.shinhanlife.dap.lib.adapter.util;
|
||||
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.lib.adapter.util
|
||||
* @className PiiMaskingUtils
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
public class PiiMaskingUtils {
|
||||
|
||||
// 1. 주민등록번호 패턴 (ex: 900101-1234567 또는 9001011234567)
|
||||
private static final Pattern RRN_PATTERN = Pattern.compile("(\\d{6})[-]?([1-4]\\d{6})");
|
||||
|
||||
// 2. 휴대전화번호 패턴 (ex: 010-1234-5678)
|
||||
private static final Pattern PHONE_PATTERN = Pattern.compile("(01[016789])[-]?(\\d{3,4})[-]?(\\d{4})");
|
||||
|
||||
// 3. 신한라이프 계좌/증권번호 패턴 (단순 예시용 계좌번호 11~14자리)
|
||||
private static final Pattern ACCOUNT_PATTERN = Pattern.compile("(\\d{3})-?(\\d{3})-?(\\d{5,8})");
|
||||
|
||||
public static String mask(String input) {
|
||||
if (input == null || input.isEmpty()) {
|
||||
return input;
|
||||
}
|
||||
|
||||
String masked = input;
|
||||
|
||||
// [1] 주민번호 뒷자리 마스킹 (첫자리 성별 식별자는 남기고 마스킹: 900101-1******)
|
||||
Matcher rrnMatcher = RRN_PATTERN.matcher(masked);
|
||||
StringBuffer rrnBuffer = new StringBuffer();
|
||||
while (rrnMatcher.find()) {
|
||||
String firstPart = rrnMatcher.group(1);
|
||||
String secondPart = rrnMatcher.group(2);
|
||||
rrnMatcher.appendReplacement(rrnBuffer, firstPart + "-" + secondPart.charAt(0) + "******");
|
||||
}
|
||||
rrnMatcher.appendTail(rrnBuffer);
|
||||
masked = rrnBuffer.toString();
|
||||
|
||||
// [2] 전화번호 중간자리 마스킹 (010-****-5678)
|
||||
Matcher phoneMatcher = PHONE_PATTERN.matcher(masked);
|
||||
StringBuffer phoneBuffer = new StringBuffer();
|
||||
while (phoneMatcher.find()) {
|
||||
String p1 = phoneMatcher.group(1);
|
||||
String p2 = phoneMatcher.group(2);
|
||||
String p3 = phoneMatcher.group(3);
|
||||
String maskedP2 = p2.replaceAll(".", "*");
|
||||
phoneMatcher.appendReplacement(phoneBuffer, p1 + "-" + maskedP2 + "-" + p3);
|
||||
}
|
||||
phoneMatcher.appendTail(phoneBuffer);
|
||||
masked = phoneBuffer.toString();
|
||||
|
||||
// [3] 계좌번호 뒷자리 마스킹 (110-123-********)
|
||||
Matcher accMatcher = ACCOUNT_PATTERN.matcher(masked);
|
||||
StringBuffer accBuffer = new StringBuffer();
|
||||
while (accMatcher.find()) {
|
||||
String a1 = accMatcher.group(1);
|
||||
String a2 = accMatcher.group(2);
|
||||
String a3 = accMatcher.group(3);
|
||||
String maskedA3 = a3.replaceAll(".", "*");
|
||||
accMatcher.appendReplacement(accBuffer, a1 + "-" + a2 + "-" + maskedA3);
|
||||
}
|
||||
accMatcher.appendTail(accBuffer);
|
||||
masked = accBuffer.toString();
|
||||
|
||||
return masked;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package io.shinhanlife.dap.lib.annotation;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* MCP 스키마 생성 시 anyOf (해당 필드들 중 최소 1개 이상 필수) 제약을 부여합니다.
|
||||
*/
|
||||
@Target({ElementType.TYPE})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface McpAnyOf {
|
||||
/**
|
||||
* anyOf 제약에 포함될 필드명 목록
|
||||
* 예: @McpAnyOf({"claimNo", "contractNo"})
|
||||
*/
|
||||
String[] value();
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package io.shinhanlife.dap.lib.annotation;
|
||||
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.lib.annotation
|
||||
* @className McpFunction
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
import java.lang.annotation.*;
|
||||
|
||||
@Target(ElementType.METHOD)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
public @interface McpFunction {
|
||||
String displayName(); // 사람이 읽는 라벨 (예: "고객 조회 툴")
|
||||
String name(); // MCP 서브툴 명칭 (예: "customer_search")
|
||||
String description();
|
||||
String prompt() default "";
|
||||
String mappingId() default "";
|
||||
|
||||
|
||||
/**
|
||||
* Tool 입력 JSON Schema를 인라인으로 지정한다. 지정하지 않으면 요청 DTO에서 자동 생성한다.
|
||||
*/
|
||||
String inputSchema() default "{}";
|
||||
|
||||
/**
|
||||
* 복합 조건(anyOf 등)이 필요한 Tool의 입력 JSON Schema 클래스패스 경로다.
|
||||
* inputSchemaResource가 지정되면 inputSchema 및 DTO 자동 생성보다 우선한다.
|
||||
*/
|
||||
// 추가: Redis 자동 등록 및 Heartbeat 대상 여부 제어
|
||||
String inputSchemaResource() default "";
|
||||
|
||||
/**
|
||||
* Tool response JSON Schema. When unset, output validation is skipped.
|
||||
*/
|
||||
String outputSchema() default "{}";
|
||||
|
||||
/**
|
||||
* Classpath resource for a complex Tool response JSON Schema.
|
||||
* This value has priority over outputSchema.
|
||||
*/
|
||||
String outputSchemaResource() default "";
|
||||
boolean register() default false;
|
||||
|
||||
// 추가: 툴 목록 노출 여부 제어 (false 시 라우팅은 되나 목록에서 숨김)
|
||||
boolean visible() default true;
|
||||
|
||||
// 추가: HITL 승인 체계 지원 (실행 전 사용자 승인 필요 여부)
|
||||
boolean requiresApproval() default false;
|
||||
|
||||
boolean readOnlyHint() default false;
|
||||
boolean destructiveHint() default false;
|
||||
boolean idempotentHint() default false;
|
||||
boolean openWorldHint() default false;
|
||||
|
||||
/** Version exposed as _meta.version in the Tool Manifest. */
|
||||
String version() default "1.0.0";
|
||||
|
||||
/** Maximum execution time exposed as _meta.timeoutMillis in the Tool Manifest. */
|
||||
long timeoutMillis() default 300000L;
|
||||
|
||||
/** Whether the Tool is available for MCP exposure. */
|
||||
boolean enabled() default true;
|
||||
|
||||
// 추가: 툴 별 기본 Timeout 설정 (기본 300초 = 300000ms)
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package io.shinhanlife.dap.lib.annotation;
|
||||
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* Marks a Tool response DTO for automatic output JSON Schema generation.
|
||||
* Field constraints are declared with {@link McpValidation}.
|
||||
*/
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
public @interface McpOutputSchema {
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package io.shinhanlife.dap.lib.annotation;
|
||||
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.lib.annotation
|
||||
* @className McpParameter
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
import java.lang.annotation.*;
|
||||
|
||||
@Target(ElementType.FIELD)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
public @interface McpParameter {
|
||||
String description();
|
||||
boolean required() default false;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package io.shinhanlife.dap.lib.annotation;
|
||||
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.lib.annotation
|
||||
* @className McpTool
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
import org.springframework.core.annotation.AliasFor;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
@Component
|
||||
public @interface McpTool {
|
||||
@AliasFor(annotation = Component.class)
|
||||
String value() default "";
|
||||
|
||||
String categoryKey() default "common";
|
||||
String routingType() default "HTTP";
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package io.shinhanlife.dap.lib.annotation;
|
||||
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.lib.annotation
|
||||
* @className McpValidation
|
||||
* @description Declares JSON Schema validation constraints for MCP tool input fields
|
||||
* @author 0986406
|
||||
* @create 2026.07.27
|
||||
* <pre>
|
||||
* ---------- revision history ----------
|
||||
* date author description
|
||||
* ---------- --------- ---------------------------
|
||||
* 2026.07.27 0986406 initial creation
|
||||
* </pre>
|
||||
*/
|
||||
@Target(ElementType.FIELD)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
public @interface McpValidation {
|
||||
boolean required() default false;
|
||||
String pattern() default "";
|
||||
long minimum() default Long.MIN_VALUE;
|
||||
long maximum() default Long.MAX_VALUE;
|
||||
int minLength() default -1;
|
||||
int maxLength() default -1;
|
||||
String[] allowedValues() default {};
|
||||
String format() default "";
|
||||
boolean nullable() default false; String defaultValue() default "";
|
||||
String[] examples() default {};
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package io.shinhanlife.dap.lib.aop;
|
||||
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.lib.aop
|
||||
* @className ToolSlaMonitoringAspect
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
import io.shinhanlife.dap.lib.annotation.McpFunction;
|
||||
import io.shinhanlife.dap.lib.config.McpProperties;
|
||||
import java.lang.reflect.Method;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
import org.aspectj.lang.annotation.Around;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
import org.aspectj.lang.reflect.MethodSignature;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.StopWatch;
|
||||
|
||||
@Slf4j
|
||||
@Aspect
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class ToolSlaMonitoringAspect {
|
||||
|
||||
private final McpProperties mcpProperties;
|
||||
|
||||
// @McpFunction 어노테이션이 붙은 모든 비즈니스 툴 메서드 실행을 가로챕니다.
|
||||
@Around("@annotation(McpFunction)")
|
||||
public Object monitorToolSla(ProceedingJoinPoint joinPoint) throws Throwable {
|
||||
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
|
||||
Method method = signature.getMethod();
|
||||
McpFunction functionAnnotation = method.getAnnotation(McpFunction.class);
|
||||
|
||||
// 네임스페이스 자동 주입 로직을 반영하여 최종 툴 이름을 산출합니다.
|
||||
String baseName = functionAnnotation.name();
|
||||
String finalName = mcpProperties.getNamespace() != null && !mcpProperties.getNamespace().isEmpty()
|
||||
? mcpProperties.getNamespace() + "_" + baseName
|
||||
: baseName;
|
||||
|
||||
StopWatch stopWatch = new StopWatch();
|
||||
stopWatch.start();
|
||||
|
||||
try {
|
||||
// 실제 비즈니스 로직(툴) 실행
|
||||
Object result = joinPoint.proceed();
|
||||
|
||||
stopWatch.stop();
|
||||
long timeMillis = stopWatch.getTotalTimeMillis();
|
||||
|
||||
// SLA 기준을 초과하면 (예: 2초 이상) 경고 로깅 처리 가능
|
||||
if (timeMillis > 2000) {
|
||||
log.warn(" [SLA 경고] Tool: {} | 소요시간: {}ms | 상태: SLOW_RESPONSE", finalName, timeMillis);
|
||||
} else {
|
||||
log.info(" [SLA 추적] Tool: {} | 소요시간: {}ms | 상태: SUCCESS", finalName, timeMillis);
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
} catch (Throwable e) {
|
||||
if (stopWatch.isRunning()) {
|
||||
stopWatch.stop();
|
||||
}
|
||||
long timeMillis = stopWatch.getTotalTimeMillis();
|
||||
|
||||
// 에러 발생 시 명확하게 실패 로그 기록
|
||||
log.error(" [SLA 장애] Tool: {} | 소요시간: {}ms | 상태: FAILED | 사유: {}", finalName, timeMillis, e.getMessage());
|
||||
|
||||
// 원래 흐름대로 예외를 던져서 게이트웨이나 상위 로직이 에러를 처리하게 함
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package io.shinhanlife.dap.lib.config;
|
||||
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.config.annotation.CorsRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.lib.config
|
||||
* @className CorsConfig
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@Configuration
|
||||
public class CorsConfig implements WebMvcConfigurer {
|
||||
|
||||
@Override
|
||||
public void addCorsMappings(CorsRegistry registry) {
|
||||
registry.addMapping("/**") // 모든 엔드포인트에 대해 CORS 허용
|
||||
.allowedOriginPatterns("*") // 외부 Agent Builder 등 모든 오리진 허용
|
||||
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS", "HEAD", "PATCH") // 허용할 HTTP 메서드
|
||||
.allowedHeaders("*") // 모든 헤더 허용
|
||||
.exposedHeaders("Mcp-Session-Id") // MCP-HTTP 세션 아이디 노출 허용
|
||||
.allowCredentials(true) // 쿠키/인증 정보 허용
|
||||
.maxAge(3600); // preflight 요청 캐시 시간 (초 단위)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package io.shinhanlife.dap.lib.config;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* [Glow Framework 통신 환경 설정 클래스]
|
||||
* application-glow-local.yml 의 'glow.communication' 하위 설정값들을
|
||||
* 자바 객체(Bean)로 매핑하여 제공합니다.
|
||||
*/
|
||||
/**
|
||||
* @package io.shinhanlife.dap.lib.config
|
||||
* @className GlowCommunicationProperties
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@Component
|
||||
@Getter
|
||||
@Setter
|
||||
@ConfigurationProperties(prefix = "glow.communication")
|
||||
public class GlowCommunicationProperties {
|
||||
|
||||
private Common common = new Common();
|
||||
private Http http = new Http();
|
||||
private Mci mci = new Mci();
|
||||
private ExtMci extmci = new ExtMci();
|
||||
private Eai eai = new Eai();
|
||||
private Websocket websocket = new Websocket();
|
||||
|
||||
@Getter @Setter
|
||||
public static class Common {
|
||||
private String envType; // 대내표준 헤더의 환경 타입정보 (D, T, P)
|
||||
}
|
||||
|
||||
@Getter @Setter
|
||||
public static class Http {
|
||||
private int connectionTimeout; // 연결 타임아웃 시간 (초 단위)
|
||||
private int readTimeout; // 읽기 타임아웃 시간 (초 단위)
|
||||
}
|
||||
|
||||
@Getter @Setter
|
||||
public static class Mci {
|
||||
private String host;
|
||||
private int port;
|
||||
private String uri;
|
||||
private String receiveUri;
|
||||
private int connectionTimeout;
|
||||
private int readTimeout;
|
||||
private String encoding;
|
||||
}
|
||||
|
||||
@Getter @Setter
|
||||
public static class ExtMci {
|
||||
private String host;
|
||||
private int port;
|
||||
private String uri;
|
||||
private String jsonUri;
|
||||
private String receiveUri;
|
||||
private int connectionTimeout;
|
||||
private int readTimeout;
|
||||
private String encoding;
|
||||
}
|
||||
|
||||
@Getter @Setter
|
||||
public static class Eai {
|
||||
private String host;
|
||||
private int port;
|
||||
}
|
||||
|
||||
@Getter @Setter
|
||||
public static class Websocket {
|
||||
private String endpoint;
|
||||
private String allowedOrigins;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package io.shinhanlife.dap.lib.config;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.lib.config
|
||||
* @className McpProperties
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@Data
|
||||
@Configuration
|
||||
@ConfigurationProperties(prefix = "mcp")
|
||||
public class McpProperties {
|
||||
|
||||
private String namespace;
|
||||
private Manifest manifest = new Manifest();
|
||||
|
||||
@Data
|
||||
public static class Manifest {
|
||||
private String bundleId;
|
||||
private String namePrefix;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package io.shinhanlife.dap.lib.config;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
|
||||
import org.apache.ibatis.session.SqlSessionFactory;
|
||||
import org.mybatis.spring.SqlSessionFactoryBean;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.lib.config
|
||||
* @className MybatisConfig
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@Configuration
|
||||
public class MybatisConfig {
|
||||
|
||||
@Bean
|
||||
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
|
||||
SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
|
||||
sessionFactory.setDataSource(dataSource);
|
||||
return sessionFactory.getObject();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package io.shinhanlife.dap.lib.config;
|
||||
|
||||
import com.p6spy.engine.logging.Category;
|
||||
import com.p6spy.engine.spy.appender.MessageFormattingStrategy;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.lib.config
|
||||
* @className P6SpySqlFormatter
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
public class P6SpySqlFormatter implements MessageFormattingStrategy {
|
||||
|
||||
@Override
|
||||
public String formatMessage(int connectionId, String now, long elapsed,
|
||||
String category, String prepared, String sql, String url) {
|
||||
|
||||
if (sql == null || sql.isBlank()) return "";
|
||||
if (Category.STATEMENT.getName().equals(category)) {
|
||||
|
||||
String prettySQL = sql
|
||||
.replaceAll("(?i)\\bSELECT\\b", "\nSELECT")
|
||||
.replaceAll("(?i)\\bFROM\\b", "\n FROM")
|
||||
.replaceAll("(?i)\\bWHERE\\b", "\n WHERE")
|
||||
.replaceAll("(?i)\\bAND\\b", "\n AND")
|
||||
.replaceAll("(?i)\\bOR\\b", "\n OR")
|
||||
.replaceAll("(?i)\\bINNER JOIN\\b", "\n INNER JOIN")
|
||||
.replaceAll("(?i)\\bLEFT JOIN\\b", "\n LEFT JOIN")
|
||||
.replaceAll("(?i)\\bORDER BY\\b", "\n ORDER BY")
|
||||
.replaceAll("(?i)\\bGROUP BY\\b", "\n GROUP BY")
|
||||
.replaceAll("(?i)\\bINSERT INTO\\b", "\nINSERT INTO")
|
||||
.replaceAll("(?i)\\bVALUES\\b", "\n VALUES")
|
||||
.replaceAll("(?i)\\bUPDATE\\b", "\nUPDATE")
|
||||
.replaceAll("(?i)\\bSET\\b", "\n SET")
|
||||
.replaceAll("(?i)\\bDELETE FROM\\b", "\nDELETE FROM");
|
||||
|
||||
return String.format("""
|
||||
\n┌─────────────────────────────────────────
|
||||
│ SQL [%dms]
|
||||
│%s
|
||||
└─────────────────────────────────────────
|
||||
""", elapsed, prettySQL.indent(2).stripTrailing());
|
||||
}
|
||||
return "";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package io.shinhanlife.dap.lib.config;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import io.shinhanlife.dap.lib.util.ToolSchemaResolver;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* Common MCP Tool Schema Bean configuration.
|
||||
*/
|
||||
@Configuration
|
||||
public class ToolSchemaConfiguration {
|
||||
|
||||
@Bean
|
||||
public ToolSchemaResolver toolSchemaResolver(ObjectMapper objectMapper) {
|
||||
return new ToolSchemaResolver(objectMapper);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package io.shinhanlife.dap.lib.dto;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.mcg.dto
|
||||
* @className OperationType
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
public enum OperationType {
|
||||
READ,
|
||||
WRITE
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
package io.shinhanlife.dap.lib.dto;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.List;
|
||||
import java.util.HashSet;
|
||||
/**
|
||||
* Tool(Agent)의 명세 및 라우팅 정보를 담고 있는 메타데이터 클래스
|
||||
* Redis 레지스트리에 저장되며, Planner와 Router 간의 통신 객체(Plan)로 사용됩니다.
|
||||
*/
|
||||
/**
|
||||
* @package io.shinhanlife.dap.mcg.dto
|
||||
* @className ToolMetadata
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Getter
|
||||
@Setter
|
||||
public class ToolMetadata {
|
||||
|
||||
// 1. Tool 기본 정보
|
||||
private String uid; // UUID 형식의 고유 식별자
|
||||
private String semver; // 버전 (예: 1.0.0)
|
||||
private String displayName; // 사람이 읽는 라벨 (1-128자)
|
||||
private String name; // MCP 서브툴 명칭 (64자 이하, 예: CustomerSearchTool)
|
||||
private String description; // 툴의 목적 및 설명 (LLM 프롬프트에 활용 가능)
|
||||
|
||||
// 2. 파라미터 스키마 (JSON Schema 형태의 Map)
|
||||
private Map<String, Object> parametersSchema;
|
||||
|
||||
// 2-0. 프론트엔드 UI용 함수별 프롬프트 매핑 (추가됨)
|
||||
private Map<String, String> actionPrompts;
|
||||
|
||||
// 2-1. 도메인 부서 그룹명 (category_key, 슬러그 형식)
|
||||
private String categoryKey;
|
||||
|
||||
// 2-2. 툴 처리 엔드포인트 URI 경로 (예: /api/tool/customer-info)
|
||||
private String endpoint;
|
||||
|
||||
// 2-3. Pod 실행 URL (독립적인 Microservice 라우팅용, 예: http://localhost:8082)
|
||||
private String podUrl;
|
||||
|
||||
// 2-4. 가시성 여부
|
||||
@Builder.Default
|
||||
private Boolean visible = true;
|
||||
|
||||
// 활성화 여부
|
||||
@Builder.Default
|
||||
private Boolean enabled = true;
|
||||
|
||||
// 2-5. Redis 등록 여부 (UI 표출용)
|
||||
@Builder.Default
|
||||
private Boolean isRegistered = true;
|
||||
|
||||
// 2-6. HITL 승인 필요 여부
|
||||
@Builder.Default
|
||||
private Boolean requiresApproval = false;
|
||||
|
||||
@Builder.Default
|
||||
private Boolean readOnlyHint = false;
|
||||
|
||||
@Builder.Default
|
||||
private Boolean destructiveHint = false;
|
||||
|
||||
@Builder.Default
|
||||
private Boolean idempotentHint = false;
|
||||
|
||||
@Builder.Default
|
||||
private Boolean openWorldHint = false;
|
||||
|
||||
|
||||
|
||||
// 3. 연동 아키텍처 구분 (DIRECT / MCI_EAI)
|
||||
private String integrationType; // 연동 타입: "DIRECT" 또는 "MCI_EAI"
|
||||
|
||||
// 4. 레거시(MCI/EAI) 연동 시 필수 정보 (integrationType이 "MCI_EAI"일 때 사용)
|
||||
private String mciServiceId; // MCI/EAI 호출을 위한 서비스 ID (예: CRM_001, LICO_992)
|
||||
|
||||
// 5. 인프라 상태 정보 (DIRECT 연동 시 사용)
|
||||
private Long lastHeartbeat; // Redis TTL 갱신용 마지막 하트비트 타임스탬프
|
||||
|
||||
// 6. 동적 서킷 브레이커 & 속도 제어 설정 (Registry 기반)
|
||||
private Integer failureRateThreshold; // 서킷 브레이커 동작 기준 실패율 (%)
|
||||
private Integer slidingWindowSize; // 서킷 브레이커 에러율 계산 표본 요청 수
|
||||
private Integer rateLimitForPeriod; // 속도 제어: 1초당 허용 최대 요청 수
|
||||
|
||||
// 7. Gateway 코어 제어용 설정 필드 추가 (재시도, 타임아웃, 오퍼레이션 타입)
|
||||
@Builder.Default
|
||||
private OperationType operationType = OperationType.READ;
|
||||
|
||||
@Builder.Default
|
||||
private Boolean retryEnabled = true;
|
||||
|
||||
@Builder.Default
|
||||
private Integer circuitBreakerFailureThreshold = 0;
|
||||
|
||||
@Builder.Default
|
||||
private Long circuitBreakerOpenMillis = 0L;
|
||||
|
||||
@Builder.Default
|
||||
private Long timeoutMillis = 0L;
|
||||
|
||||
// --- Guardrail 호환성을 위한 메서드 추가 ---
|
||||
public Set<String> allowedArguments() {
|
||||
if (parametersSchema == null || !parametersSchema.containsKey("properties")) return Set.of();
|
||||
return ((Map<String, Object>) parametersSchema.get("properties")).keySet();
|
||||
}
|
||||
|
||||
public Set<String> requiredArguments() {
|
||||
if (parametersSchema == null || !parametersSchema.containsKey("required")) return Set.of();
|
||||
return new HashSet<>((List<String>) parametersSchema.get("required"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
package io.shinhanlife.dap.lib.integration;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
// TODO: 실제 Glow Framework 의존성이 추가되면 아래 주석들을 풀고 사용하세요!
|
||||
// import io.shinhanlife.glow.communication.dto.CommonHeader;
|
||||
// import io.shinhanlife.glow.communication.dto.Transfer;
|
||||
// import io.shinhanlife.glow.communication.module.eai.component.GlowEaiComponent;
|
||||
// import io.shinhanlife.glow.communication.module.mci.component.GlowExtMciComponent;
|
||||
// import io.shinhanlife.glow.communication.module.mci.component.GlowMciComponent;
|
||||
// import io.shinhanlife.glow.communication.util.CommonHeaderFactory;
|
||||
|
||||
/**
|
||||
* [MCI / EAI 공통 연동 래퍼(Wrapper) 템플릿]
|
||||
* 신한라이프 Glow Framework 개발표준정의서를 바탕으로 대내/대외망/EAI 통신을
|
||||
* MCP 툴에서 손쉽게 호출할 수 있도록 일원화한 컴포넌트입니다.
|
||||
*/
|
||||
/**
|
||||
* @package io.shinhanlife.dap.lib.integration
|
||||
* @className GlowIntegrationCall
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class GlowIntegrationCall {
|
||||
|
||||
// 1. 실제 의존성이 주입될 프레임워크 컴포넌트들 (임시 주석 처리)
|
||||
/*
|
||||
private final GlowMciComponent mci;
|
||||
private final GlowEaiComponent eai;
|
||||
private final GlowExtMciComponent extMci;
|
||||
*/
|
||||
|
||||
/**
|
||||
* 1. 대외 MCI 호출 (타행, 금융결제원 등 외부 기관)
|
||||
* 가이드 2.2.2에 명시된 필수 파라미터(기관코드, 종별코드, 업무코드, 거래코드)를 모두 포함합니다.
|
||||
*/
|
||||
/*
|
||||
public <S, R> Transfer<R> callExtMci(String itrfId, String frbuCd, String cmouDutjCd, String cmouCssfCd, String cmouTraCd, S body, Class<R> resBody) {
|
||||
|
||||
// 1. 공통 헤더 생성
|
||||
CommonHeader header = CommonHeaderFactory.createRequestHeader(itrfId);
|
||||
|
||||
// 2. 대외 전용 필수 코드 세팅 로직 (프레임워크 내부 스펙에 맞게 가공)
|
||||
// (예: 헤더에 해당 속성들을 주입하거나 Transfer 객체에 싣는 과정 추가)
|
||||
|
||||
// 3. Transfer 객체 빌드
|
||||
Transfer<S> req = Transfer.<S>builder()
|
||||
.header(header)
|
||||
.body(body)
|
||||
.build();
|
||||
|
||||
// 4. 대외 MCI 컴포넌트를 통해 최종 전송
|
||||
return extMci.call(req, resBody);
|
||||
}
|
||||
*/
|
||||
|
||||
/**
|
||||
* 2. EAI 호출 (대내망 중계기)
|
||||
* 가이드 2.3에 명시된 대로 수신서비스 ID 없이 인터페이스 ID(itrfId)만 필수로 받습니다.
|
||||
*/
|
||||
/*
|
||||
public <S, R> Transfer<R> callEai(String itrfId, S body, Class<R> resBody) {
|
||||
|
||||
// 1. EAI는 인터페이스 ID만으로 심플하게 헤더 생성
|
||||
CommonHeader header = CommonHeaderFactory.createRequestHeader(itrfId);
|
||||
|
||||
// 2. Transfer 객체 빌드
|
||||
Transfer<S> req = Transfer.<S>builder()
|
||||
.header(header)
|
||||
.body(body)
|
||||
.build();
|
||||
|
||||
// 3. EAI 컴포넌트를 통해 최종 전송
|
||||
return eai.call(req, resBody);
|
||||
}
|
||||
*/
|
||||
|
||||
/**
|
||||
* 3. 대내 MCI 호출 (사내 시스템 간 통신)
|
||||
*/
|
||||
/*
|
||||
public <S, R> Transfer<R> callMci(String itrfId, S body, Class<R> resBody) {
|
||||
|
||||
CommonHeader header = CommonHeaderFactory.createRequestHeader(itrfId);
|
||||
|
||||
Transfer<S> req = Transfer.<S>builder()
|
||||
.header(header)
|
||||
.body(body)
|
||||
.build();
|
||||
|
||||
return mci.call(req, resBody);
|
||||
}
|
||||
*/
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package io.shinhanlife.dap.lib.integration.dto;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import java.util.List;
|
||||
|
||||
// TODO: 실제 Glow Framework 의존성이 추가되면 아래 주석을 풀고 사용하세요!
|
||||
// import io.shinhanlife.glow.communication.annotation.GlowMciFieldInfo;
|
||||
|
||||
/**
|
||||
* [대외 MCI 연동용 DTO 표준 템플릿]
|
||||
* Glow Framework 개발표준정의서(2.2.1 IO 작성) 규칙을 100% 준수한 샘플입니다.
|
||||
* 새로운 대외 통신 전문을 만들 때 이 파일을 복사해서 필드명과 길이만 수정하여 사용하세요.
|
||||
*/
|
||||
/**
|
||||
* @package io.shinhanlife.dap.lib.integration.dto
|
||||
* @className SampleGlowMessage
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@Getter
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor(access = AccessLevel.PUBLIC) // [규칙 1] Reflection을 위한 기본 생성자 필수 (public 유지)
|
||||
public class SampleGlowMessage {
|
||||
|
||||
// [규칙 2] @GlowMciFieldInfo 선언 필수 (order: 순서)
|
||||
// @GlowMciFieldInfo(order = 1)
|
||||
private MessageHeader header;
|
||||
|
||||
// [규칙 2] @GlowMciFieldInfo 선언 필수 (order: 순서)
|
||||
// @GlowMciFieldInfo(order = 2)
|
||||
private List<MessageBody> msgDtdvValu; // 다건(List) 본문 데이터
|
||||
|
||||
|
||||
@Getter
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor(access = AccessLevel.PUBLIC)
|
||||
public static class MessageHeader {
|
||||
|
||||
// [규칙 2] 단건 필드의 경우 length 필수 입력 (EIMS 길이와 일치해야 함)
|
||||
// @GlowMciFieldInfo(order = 1, length = 1)
|
||||
private String msgTnsmTypeCd;
|
||||
|
||||
// @GlowMciFieldInfo(order = 2, length = 8)
|
||||
private int msdvLen;
|
||||
|
||||
// [규칙 3] 다건(List) 건수 필드의 경우, target 속성에 대상 변수명("msgDtdvValu") 필수 기입!
|
||||
// @GlowMciFieldInfo(order = 3, length = 2, target = "msgDtdvValu")
|
||||
private int msgRpttCc;
|
||||
}
|
||||
|
||||
|
||||
@Getter
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor(access = AccessLevel.PUBLIC)
|
||||
public static class MessageBody {
|
||||
|
||||
// @GlowMciFieldInfo(order = 1, length = 8)
|
||||
private String msgCd;
|
||||
|
||||
// @GlowMciFieldInfo(order = 2, length = 1)
|
||||
private String msgPrnAttrCd;
|
||||
|
||||
// @GlowMciFieldInfo(order = 3, length = 200)
|
||||
private String msgCt;
|
||||
|
||||
// @GlowMciFieldInfo(order = 4, length = 200)
|
||||
private String anxMsgCt;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package io.shinhanlife.dap.lib.integration.eai.component;
|
||||
|
||||
import io.shinhanlife.dap.lib.config.GlowCommunicationProperties;
|
||||
import io.shinhanlife.glow.communication.dto.CommonHeader;
|
||||
import io.shinhanlife.glow.communication.dto.Transfer;
|
||||
import io.shinhanlife.glow.communication.module.eai.component.GlowEaiComponent;
|
||||
import io.shinhanlife.glow.communication.util.CommonHeaderFactory;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 신한라이프 내부 Glow 표준 EAI 컴포넌트 어댑터 (AXHUB)
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.lib.integration.eai.component
|
||||
* @className AxhubEaiComponent
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
public class AxhubEaiComponent {
|
||||
|
||||
@SuppressWarnings("rawtypes")
|
||||
private final GlowEaiComponent eai;
|
||||
private final GlowCommunicationProperties communicationProperties;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private <O> Transfer<O> syncEai(Transfer<Object> request) {
|
||||
// LOG 저장 (AXHUB 방식 로깅)
|
||||
CommonHeader reqHeader = (CommonHeader) request.getHeader();
|
||||
log.info("[AxhubEaiComponent] {} EAI 호출시작 (수신서비스: {})", reqHeader.getItrfId(), reqHeader.getRcvSvcId());
|
||||
|
||||
Transfer<O> response = (Transfer<O>) eai.sync(request);
|
||||
|
||||
log.info("[AxhubEaiComponent] {} EAI 호출종료 (수신서비스: {})", reqHeader.getItrfId(), reqHeader.getRcvSvcId());
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* EAI 호출
|
||||
*/
|
||||
public <O, I> Transfer<O> call(String itrfId, String rcvSvcId, I inputDto) throws Exception {
|
||||
CommonHeader header = CommonHeaderFactory.createRequestHeader(itrfId, rcvSvcId);
|
||||
|
||||
Transfer<Object> request = Transfer.builder()
|
||||
.header(header)
|
||||
.body(inputDto)
|
||||
.build();
|
||||
|
||||
return syncEai(request);
|
||||
}
|
||||
|
||||
/**
|
||||
* EAI 호출 (응답 타입 명시)
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public <O, I> Transfer<O> call(String itrfId, String rcvSvcId, I inputDto, Class<O> resBodyClass) throws Exception {
|
||||
CommonHeader header = CommonHeaderFactory.createRequestHeader(itrfId, rcvSvcId);
|
||||
|
||||
Transfer<Object> request = Transfer.builder()
|
||||
.header(header)
|
||||
.body(inputDto)
|
||||
.resBodyClass((Class<Object>) (Class<?>) resBodyClass)
|
||||
.build();
|
||||
|
||||
return syncEai(request);
|
||||
}
|
||||
|
||||
/**
|
||||
* EAI 호출 (rcvSvcId 없는 경우)
|
||||
*/
|
||||
public <O, I> Transfer<O> call(String itrfId, I inputDTO, Class<O> resBodyClass) throws Exception {
|
||||
String className = inputDTO.getClass().getSimpleName();
|
||||
String rcvSvcId = className.replace("_I", "");
|
||||
return call(itrfId, rcvSvcId, inputDTO, resBodyClass);
|
||||
}
|
||||
|
||||
/**
|
||||
* EAI 호출 (Response body class와 rcvSvcId 없는 경우)
|
||||
*/
|
||||
public <O, I> Transfer<O> call(String itrfId, I inputDTO) throws Exception {
|
||||
String className = inputDTO.getClass().getSimpleName();
|
||||
String rcvSvcId = className.replace("_I", "");
|
||||
return call(itrfId, rcvSvcId, inputDTO);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
package io.shinhanlife.dap.lib.integration.mci.component;
|
||||
|
||||
import io.shinhanlife.dap.lib.session.dto.SessionDto;
|
||||
import io.shinhanlife.dap.lib.util.SessionUtil;
|
||||
import io.shinhanlife.dap.lib.config.GlowCommunicationProperties;
|
||||
import io.shinhanlife.glow.communication.dto.CommonHeader;
|
||||
import io.shinhanlife.glow.communication.dto.HeaderDefaults;
|
||||
import io.shinhanlife.glow.communication.dto.Transfer;
|
||||
import io.shinhanlife.glow.communication.module.mci.component.GlowMciComponent;
|
||||
import io.shinhanlife.glow.communication.util.CommonHeaderFactory;
|
||||
import io.shinhanlife.dap.lib.integration.mci.enums.IndvCtinRoleTyp;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 신한라이프 내부 Glow 표준 컴포넌트 어댑터 (AXHUB)
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.lib.integration.mci.component
|
||||
* @className AxhubMciComponent
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
public class AxhubMciComponent {
|
||||
|
||||
@SuppressWarnings("rawtypes")
|
||||
private final GlowMciComponent mci;
|
||||
private final GlowCommunicationProperties communicationProperties;
|
||||
|
||||
/** 전문생성채널유형코드 : 1 (채널계) */
|
||||
private final static String TGRM_CREA_CHNN_TYPE_CD_1 = "1";
|
||||
private static final String SUCO_UNBL_CODE = "NNB00147"; // 청약불가
|
||||
|
||||
/**
|
||||
* 전문 Common Header 생성
|
||||
* @param itrfName 인터페이스Id
|
||||
* @param rcvSvcId 수신서비스Id
|
||||
* @return Map<HeaderDefaults, String>
|
||||
*/
|
||||
private Map<HeaderDefaults, String> createCommonHeaderMap(String itrfName, String rcvSvcId) {
|
||||
SessionDto sessionDto = SessionUtil.getSession();
|
||||
Map<HeaderDefaults, String> commonHeaderMap = new HashMap<>();
|
||||
|
||||
commonHeaderMap.put(HeaderDefaults.ITRF_ID, itrfName);
|
||||
commonHeaderMap.put(HeaderDefaults.RCV_SVC_ID, rcvSvcId);
|
||||
|
||||
if (sessionDto != null) {
|
||||
commonHeaderMap.put(HeaderDefaults.STR_YMD, sessionDto.getStrYmd());
|
||||
commonHeaderMap.put(HeaderDefaults.ACNT_OGNZ_NO, sessionDto.getBrafNo());
|
||||
commonHeaderMap.put(HeaderDefaults.PSMR_ASRT_CD, sessionDto.getPsmrAsrtCd());
|
||||
commonHeaderMap.put(HeaderDefaults.SBSN_RULP_ASRT_CD, sessionDto.getSbsnRulpAsrtCd());
|
||||
commonHeaderMap.put(HeaderDefaults.BSDU_CD, sessionDto.getBsduCd());
|
||||
commonHeaderMap.put(HeaderDefaults.BSQU_CD, sessionDto.getBsquCd());
|
||||
commonHeaderMap.put(HeaderDefaults.OGNZ_ASRT_CD, sessionDto.getOgnzAsrtCd());
|
||||
commonHeaderMap.put(HeaderDefaults.OGNZ_LEVE_CD, sessionDto.getOgnzLeveCd());
|
||||
commonHeaderMap.put(HeaderDefaults.SCRN_ID, sessionDto.getPrgrId());
|
||||
}
|
||||
|
||||
commonHeaderMap.put(HeaderDefaults.INDV_CTIN_ROLE_CD, IndvCtinRoleTyp.CD_Z99.getCode());
|
||||
commonHeaderMap.put(HeaderDefaults.TGRM_CREA_CHNN_TYPE_CD, TGRM_CREA_CHNN_TYPE_CD_1);
|
||||
|
||||
if (communicationProperties != null && communicationProperties.getCommon() != null) {
|
||||
commonHeaderMap.put(HeaderDefaults.ENVR_TYPE_CD, communicationProperties.getCommon().getEnvType());
|
||||
}
|
||||
|
||||
return commonHeaderMap;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private <O> Transfer<O> syncMci(Transfer<Object> request) {
|
||||
// LOG 저장 (AXHUB 방식 로깅)
|
||||
CommonHeader reqHeader = (CommonHeader) request.getHeader();
|
||||
log.info("[AxhubMciComponent] {} MCI 호출시작 (수신서비스: {})", reqHeader.getItrfId(), reqHeader.getRcvSvcId());
|
||||
Transfer<O> response = (Transfer<O>) mci.sync(request);
|
||||
log.info("[AxhubMciComponent] {} MCI 호출종료 (수신서비스: {})", reqHeader.getItrfId(), reqHeader.getRcvSvcId());
|
||||
|
||||
if (response != null && response.getHeader() != null) {
|
||||
CommonHeader resHeader = (CommonHeader) response.getHeader();
|
||||
String tgrmDalRsltCd = resHeader.getTgrmDalRsltCd();
|
||||
// TODO 추가 메시지 처리 및 오류 코드 제어 로직
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
public <O, I> Transfer<O> callTo(String itrfName, String rcvSvcId, I inputDto) throws Exception {
|
||||
Map<HeaderDefaults, String> commonHeaderMap = createCommonHeaderMap(itrfName, rcvSvcId);
|
||||
CommonHeader header = CommonHeaderFactory.createRequestHeader(commonHeaderMap);
|
||||
|
||||
Transfer<Object> request = Transfer.builder()
|
||||
.header(header)
|
||||
.body(inputDto)
|
||||
.build();
|
||||
|
||||
return syncMci(request);
|
||||
}
|
||||
|
||||
/**
|
||||
* 대내 mci 호출
|
||||
* @param itrfName 인터페이스Id
|
||||
* @param rcvSvcId 수신서비스Id
|
||||
* @param inputDto inputDto
|
||||
* @param resBodyClass resBodyClass
|
||||
* @return Transfer
|
||||
* @param <O> resBodyClass 제너릭
|
||||
* @param <I> inputDto 제너릭
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public <O, I> Transfer<O> callTo(String itrfName, String rcvSvcId, I inputDto, Class<O> resBodyClass) throws Exception {
|
||||
Map<HeaderDefaults, String> commonHeaderMap = createCommonHeaderMap(itrfName, rcvSvcId);
|
||||
CommonHeader header = CommonHeaderFactory.createRequestHeader(commonHeaderMap);
|
||||
|
||||
Transfer<Object> request = Transfer.builder()
|
||||
.header(header)
|
||||
.body(inputDto)
|
||||
.resBodyClass((Class<Object>) (Class<?>) resBodyClass)
|
||||
.build();
|
||||
|
||||
return syncMci(request);
|
||||
}
|
||||
|
||||
/**
|
||||
* 대내 mci 호출 (rcvSvcId 없는 경우)
|
||||
* @param itrfName 인터페이스Id
|
||||
* @param inputDTO 수신서비스Id (클래스명 대체)
|
||||
* @param resBodyClass resBodyClass
|
||||
* @return Transfer
|
||||
* @param <O> resBodyClass 제너릭
|
||||
* @param <I> inputDto 제너릭
|
||||
* @throws Exception Exception
|
||||
*/
|
||||
public <O, I> Transfer<O> callTo(String itrfName, I inputDTO, Class<O> resBodyClass) throws Exception {
|
||||
String className = inputDTO.getClass().getSimpleName();
|
||||
String rcvSvcId = className.replace("_I", "");
|
||||
return callTo(itrfName, rcvSvcId, inputDTO, resBodyClass);
|
||||
}
|
||||
|
||||
/**
|
||||
* 대내 mci 호출 (Response body class와 rcvSvcId 없는 경우)
|
||||
* @param itrfName 인터페이스Id
|
||||
* @param inputDTO 수신서비스Id (클래스명 대체)
|
||||
* @return Transfer
|
||||
* @param <O> resBodyClass 제너릭
|
||||
* @param <I> inputDto 제너릭
|
||||
* @throws Exception Exception
|
||||
*/
|
||||
public <O, I> Transfer<O> callTo(String itrfName, I inputDTO) throws Exception {
|
||||
String className = inputDTO.getClass().getSimpleName();
|
||||
String rcvSvcId = className.replace("_I", "");
|
||||
return callTo(itrfName, rcvSvcId, inputDTO);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package io.shinhanlife.dap.lib.integration.mci.config;
|
||||
|
||||
import io.shinhanlife.glow.communication.module.mci.component.GlowMciComponent;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* TODO: 실제 Glow Framework 의존성이 추가되어 io.shinhanlife.glow 패키지가
|
||||
* ComponentScan에 잡히게 되면 이 설정 클래스는 삭제하세요.
|
||||
*/
|
||||
@Configuration
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.lib.integration.mci.config
|
||||
* @className GlowMockConfig
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
public class GlowMockConfig {
|
||||
|
||||
@Bean
|
||||
@SuppressWarnings("rawtypes")
|
||||
public GlowMciComponent glowMciComponent() {
|
||||
return new GlowMciComponent();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@SuppressWarnings("rawtypes")
|
||||
public io.shinhanlife.glow.communication.module.eai.component.GlowEaiComponent glowEaiComponent() {
|
||||
return new io.shinhanlife.glow.communication.module.eai.component.GlowEaiComponent();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package io.shinhanlife.dap.lib.integration.mci.config;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.lib.integration.mci.config
|
||||
* @className ShinhanIntegrationProperties
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@ConfigurationProperties(prefix = "shinhan.integration")
|
||||
public class ShinhanIntegrationProperties {
|
||||
|
||||
/**
|
||||
* 환경유형코드: 운영(R), 테스트(T), 개발(D)
|
||||
*/
|
||||
private String envrTypeCd = "D";
|
||||
|
||||
private ServerInfo eai = new ServerInfo();
|
||||
private ServerInfo internalMci = new ServerInfo();
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
public static class ServerInfo {
|
||||
private String url;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package io.shinhanlife.dap.lib.integration.mci.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonUnwrapped;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.lib.integration.mci.dto
|
||||
* @className MciRequestWrapper
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class MciRequestWrapper<T> {
|
||||
private ShinhanCommonHeaderDto tgrmCmnnhddValu;
|
||||
|
||||
@JsonUnwrapped
|
||||
private T body;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package io.shinhanlife.dap.lib.integration.mci.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonUnwrapped;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.lib.integration.mci.dto
|
||||
* @className MciResponseWrapper
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class MciResponseWrapper<T> {
|
||||
private ShinhanCommonHeaderDto tgrmCmnnhddValu;
|
||||
|
||||
@JsonUnwrapped
|
||||
private T body;
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package io.shinhanlife.dap.lib.integration.mci.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.lib.integration.mci.dto
|
||||
* @className OlCommonHeaderDto
|
||||
* @description AX HUB 시스템 처리 클래스 - OL(구 오렌지라이프) 공통 헤더
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class OlCommonHeaderDto {
|
||||
private String custNm; // 고객명
|
||||
private String custRrn; // 고객 주민등록번호
|
||||
private String custNo; // 고객번호
|
||||
private String rcevNo; // 접수번호
|
||||
private String pono; // 증권번호
|
||||
private String scrNm; // 화면명
|
||||
private String scrId; // 화면ID
|
||||
private String lginDttm; // 사용자가 로그인한 접속일시
|
||||
private String lginIpAddr; // 사용자가 접속한 IP 주소
|
||||
private String userNm; // 사용자 이름(한글)
|
||||
private String userEngNm; // 사용자 영문이름
|
||||
private String userId; // 사용자 ID(AD ID)
|
||||
private String userNo; // 사용자번호
|
||||
private String deptCd; // 사용자조직 코드
|
||||
private String salsDvCd; // 영업본부코드
|
||||
private String salsBoCd; // 영업지점코드
|
||||
private String uppDeptCd; // 상위조직코드
|
||||
private String prcsrUserId; // 처리자 ID(AD ID)
|
||||
private String prcsrUserNo; // 처리지번호
|
||||
private String prcsrDeptCd; // 처리지조직 코드
|
||||
private String prcsrDvCd; // 처리지 영업본부코드
|
||||
private String prcsrBoCd; // 처리지 영업지점코드
|
||||
private String prcsrUppDeptCd; // 부서코드
|
||||
private String sysCd; // 요청이 들어온 시스템을 표시
|
||||
private String reqtSvcNm; // 요청하는 서비스 모듈명
|
||||
private String reqtMthdNm; // 요청하는 메소드명
|
||||
private String reqtVoNm; // 요청메소드에 전달할 값을 담는 VO명
|
||||
private String scrButnFuncClssCd; // 화면에서 버튼 별 이벤트 구분을 위한 구분코드
|
||||
private String scrGriCnt; // 화면 그리드 개수
|
||||
private List<OlPageDto> pageList; // 페이징 리스트 (L2 반복)
|
||||
private String reqtDttm; // 요청일시
|
||||
private String crdtInfoIcluFlg; // 신용정보포함여부(Y,N)
|
||||
private String crdtInfoDataChgTypCd; // 업무내역별 식별코드 부여
|
||||
private String crdtInfoIdfInEngAbbrNm; // 신용정보식별영문약어명
|
||||
private String crdtInfoIdfnSysCd; // 신용정보식별시스템코드
|
||||
private String scrButnNm; // 화면버튼명
|
||||
private String msgCnt; // 메시지 개수
|
||||
private List<OlMsgDto> msgList; // 메시지 리스트 (L2 반복)
|
||||
private String respDttm; // 응답일시
|
||||
private String svcRunNm; // 거래별로 유일한 ServiceExecutionID
|
||||
private String stdate; // 기준일자
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class OlPageDto {
|
||||
private String pageSrno; // 페이지 인덱스값 (L3)
|
||||
private String pageInqCnt; // 한페이지에 조회될 건수 (L3)
|
||||
private String nxtButnNm; // 다음버튼ID (L3)
|
||||
private String nxtButnEnbFlg; // 다음버튼 활성여부 (L3)
|
||||
}
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class OlMsgDto {
|
||||
private String msgNo; // 서버 측에서 세팅한 정상/에러 메시지코드 (L3)
|
||||
private String msgTypCd; // 메시지유형코드 (L3)
|
||||
private String msgNm; // 메시지코드의 내용 (L3)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package io.shinhanlife.dap.lib.integration.mci.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.lib.integration.mci.dto
|
||||
* @className ShinhanCommonHeaderDto
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class ShinhanCommonHeaderDto {
|
||||
|
||||
private String tgrmLencn; // 전문길이
|
||||
private String glbId; // 글로벌ID (전사공통키)
|
||||
private String pgrsSriaNo; // 진행일련번호
|
||||
private String tgrmVrsnInfoValu; // 전문버전정보값
|
||||
private String tgrmEncrYn; // 전문암호화여부
|
||||
private String gpcpCd; // 그룹사코드
|
||||
private String appliDutjCd; // 어플리케이션업무코드
|
||||
private String appliDtptDutjCd; // 어플리케이션상세업무코드
|
||||
private String frbuCd; // 대외기관코드
|
||||
private String cmouDutjCd; // 대외업무코드
|
||||
private String cmouCssfCd; // 대외종별코드
|
||||
private String cmouTraCd; // 대외거래코드
|
||||
private String rcvSvcId; // 수신서비스ID
|
||||
private String rsltRcvSvcId; // 결과수신서비스ID
|
||||
private String tgrmCreaChnnTypeCd; // 전문생성채널유형코드
|
||||
|
||||
private String lnggDvsnCd; // 언어구분코드
|
||||
private String simulTraYn; // 시뮬레이션거래여부
|
||||
private String itrIfId; // 인터페이스ID
|
||||
private String reqRspnScCd; // 요청응답구분코드
|
||||
private String tnsmTypeCd; // 전송유형코드
|
||||
private String envrTypeCd; // 환경유형코드
|
||||
private String inqrTraTypeCd; // 조회거래유형코드
|
||||
private String reqTgrmTnsmDtptDt; // 요청전문전송상세일시
|
||||
private String strYmd; // 기준일자
|
||||
private String scrnId; // 화면ID
|
||||
private String scrnBtnId; // 화면버튼ID
|
||||
|
||||
private String userIpAddr; // 사용자IP주소
|
||||
private String drtmCd; // 부서코드
|
||||
private String userId; // 사용자ID
|
||||
private String indvCtinRoleCd; // 개인신용정보역할코드
|
||||
private String acntOgnzNo; // 경리조직번호
|
||||
private String rspnTgrmTnsmDtptDt; // 응답전문전송상세일시
|
||||
private String tgrmDalRsltCd; // 전문처리결과코드
|
||||
private String ognzAsrtCd; // 조직분류코드
|
||||
private String ognzLeveCd; // 조직레벨코드
|
||||
private String psmrAsrtCd; // 인사조직분류코드
|
||||
private String sbsnRulpAsrtCd; // 영업규정분류코드
|
||||
private String bsduCd; // 영업지국코드
|
||||
private String bsquCd; // 영업자격코드
|
||||
private String linkPrafDutyCd; // 연계인사직책코드
|
||||
private String indvInfoLogWritYn; // 개인정보로그작성여부
|
||||
private String prepImhdNm; // 예비항목명
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package io.shinhanlife.dap.lib.integration.mci.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.lib.integration.mci.dto
|
||||
* @className ShinhanMessageDto
|
||||
* @description AX HUB 시스템 처리 클래스 - MCI 전문 메시지부
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class ShinhanMessageDto {
|
||||
|
||||
private MsgHddvValu msgHddvValu; // 메시지헤더부값
|
||||
private MsgDtdvValu msgDtdvValu; // 메시지데이터부값
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class MsgHddvValu {
|
||||
private String msgTnsmTypeCd; // 메시지전송유형코드
|
||||
private Integer msdvLencn; // 메시지부길이
|
||||
private Integer msgRpttCc; // 메시지반복건수
|
||||
}
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class MsgDtdvValu {
|
||||
private String msgCd; // 메시지코드
|
||||
private String msgPrnAttrCd; // 메시지출력속성코드
|
||||
private String msgCt; // 메시지내용
|
||||
private String anxMsgCt; // 부가메시지내용
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package io.shinhanlife.dap.lib.integration.mci.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonUnwrapped;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.lib.integration.mci.dto
|
||||
* @className ShinhanTelegramWrapper
|
||||
* @description AX HUB 시스템 처리 클래스 - MCI 전문 전체 래퍼 (공통헤더부 + 메시지부 + 데이터부)
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class ShinhanTelegramWrapper<T> {
|
||||
|
||||
// 1. 공통 헤더부
|
||||
private ShinhanCommonHeaderDto tgrmCmnnhddValu;
|
||||
|
||||
// 2. 메시지부
|
||||
private ShinhanMessageDto tgrmMsdvValu;
|
||||
|
||||
// 3. 데이터부 (비즈니스마다 다름, JsonUnwrapped로 평탄화하거나 객체 자체로 유지 가능. 여기서는 객체 유지)
|
||||
private T tgrmDtdvValu;
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package io.shinhanlife.dap.lib.integration.mci.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.lib.integration.mci.dto
|
||||
* @className SlCommonHeaderDto
|
||||
* @description AX HUB 시스템 처리 클래스 - SL(신한라이프) 표준 헤더
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class SlCommonHeaderDto {
|
||||
private String length; // 전문길이
|
||||
private SlGlobalId globalId; // 글로벌ID
|
||||
private String headerVer; // 전문헤더버전
|
||||
private String encodeFlag; // 전문암호화여부
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class SlGlobalId {
|
||||
private String writeDate; // 전문작성일 (8)
|
||||
private String sysCd; // 생성시스템명 (8)
|
||||
private String typeCd; // 구분코드 (2)
|
||||
private String detailCd; // 세부업무코드 (4)
|
||||
private String seqNo; // 채번번호 (8)
|
||||
private String step; // 진행상황 (2)
|
||||
}
|
||||
|
||||
private String groupCoCd; // 그룹사코드
|
||||
private String instCd; // 기관코드
|
||||
private String applCd; // 업무코드
|
||||
private String kindCd; // 종별코드
|
||||
private String txCd; // 거래코드
|
||||
private String pfmAppName; // 어플리케이션 명
|
||||
private String pfmSvcName; // 서비스 명
|
||||
private String pfmFnName; // 오퍼레이션 명
|
||||
private String systemCd; // 생성시스템구분
|
||||
private String trFlag; // 요청응답구분
|
||||
private String syncFlag; // 동기구분
|
||||
private String envrFlag; // 환경구분
|
||||
private String crudFlag; // 조회거래구분
|
||||
private String sendTime; // 전문전송일시
|
||||
private String screenId; // 화면ID
|
||||
private String clntIp; // Client IP
|
||||
private String orgCd; // 부서(지점)코드
|
||||
private String userId; // 사용자 사번(아이디)
|
||||
private String indvCrdtInfo; // 개인신용정보역할코드
|
||||
private String acntOgnzNo; // 경리조직번호
|
||||
private String ttiFlag; // TimeOut사용
|
||||
private String ttiStartTm; // 최초시작시간
|
||||
private String ttiKeepTm; // 유지시간초수
|
||||
private String outMsgTm; // 응답전문작성일시
|
||||
private String resType; // 처리결과
|
||||
private String resCode; // 응답코드
|
||||
private String resBascMsg; // 응답기본내역
|
||||
private String msgType; // 메시지 유형
|
||||
private String rcvSvcCd; // 수신 서비스 Code
|
||||
private String rsltRcvSvcCd; // 결과수신 서비스 Code
|
||||
private String realSvcCd; // Real 서비스 Code
|
||||
private String ognzAsrtCd; // 조직분류코드
|
||||
private String ognzLeveCd; // 조직레벨구분코드
|
||||
private String psmrAsrtCd; // 인사조직분류코드
|
||||
private String sbsnRulpAsrtCd; // 영업규정분류코드
|
||||
private String bsduCd; // 영업지국코드
|
||||
private String bsquCd; // 영업자격코드
|
||||
private String linkPrafDutyCd; // 직책코드
|
||||
private String temp; // 예비 필드
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package io.shinhanlife.dap.lib.integration.mci.enums;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.lib.integration.mci.enums
|
||||
* @className IndvCtinRoleTyp
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
public enum IndvCtinRoleTyp {
|
||||
CD_Z99("Z99");
|
||||
|
||||
private final String code;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package io.shinhanlife.dap.lib.manifest;
|
||||
|
||||
/** Behaviour hints exposed by the Tool Service manifest. */
|
||||
public record ToolManifestAnnotations(
|
||||
String title,
|
||||
boolean readOnlyHint,
|
||||
boolean destructiveHint,
|
||||
boolean idempotentHint,
|
||||
boolean openWorldHint) {
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package io.shinhanlife.dap.lib.manifest;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import java.util.Map;
|
||||
|
||||
/** One MCP Tool declaration published by a Tool Service. */
|
||||
public record ToolManifestItem(
|
||||
String name,
|
||||
String endpoint,
|
||||
String title,
|
||||
String description,
|
||||
Map<String, Object> inputSchema,
|
||||
ToolManifestAnnotations annotations,
|
||||
@JsonProperty("_meta") ToolManifestMeta meta) {
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package io.shinhanlife.dap.lib.manifest;
|
||||
|
||||
/** Operational metadata exposed by the Tool Service manifest. */
|
||||
public record ToolManifestMeta(String version, long timeoutMillis, boolean enabled) {
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package io.shinhanlife.dap.lib.manifest;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/** Top-level response for GET /tool-manifest. */
|
||||
public record ToolManifestResponse(String bundleId, String revision, List<ToolManifestItem> tools) {
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
package io.shinhanlife.dap.lib.manifest;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import io.shinhanlife.dap.lib.config.McpProperties;
|
||||
import io.shinhanlife.dap.lib.dto.ToolMetadata;
|
||||
import io.shinhanlife.dap.lib.usecase.ToolRegistryHeartbeatSender;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.function.Supplier;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/** Builds the Tool Service owned manifest consumed by the MCP server. */
|
||||
@Service
|
||||
public class ToolManifestService {
|
||||
|
||||
private static final long DEFAULT_TIMEOUT_MILLIS = 300000L;
|
||||
private static final AtomicLong LAST_ISSUED_REVISION = new AtomicLong();
|
||||
private final Supplier<List<ToolMetadata>> toolSupplier;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final McpProperties properties;
|
||||
private String lastFingerprint;
|
||||
private String lastRevision;
|
||||
|
||||
@Autowired
|
||||
public ToolManifestService(ToolRegistryHeartbeatSender heartbeatSender, ObjectMapper objectMapper,
|
||||
McpProperties properties) {
|
||||
this(heartbeatSender::getAllScannedTools, objectMapper, properties);
|
||||
}
|
||||
|
||||
ToolManifestService(Supplier<List<ToolMetadata>> toolSupplier, ObjectMapper objectMapper,
|
||||
McpProperties properties) {
|
||||
this.toolSupplier = toolSupplier;
|
||||
this.objectMapper = objectMapper;
|
||||
this.properties = properties;
|
||||
}
|
||||
|
||||
public ToolManifestResponse currentManifest() {
|
||||
String bundleId = properties.getManifest() == null ? null : properties.getManifest().getBundleId();
|
||||
if (bundleId == null || bundleId.isBlank()) {
|
||||
throw new IllegalStateException("mcp.manifest.bundle-id must be configured");
|
||||
}
|
||||
|
||||
List<ToolManifestItem> tools = toolSupplier.get().stream()
|
||||
.map(this::toManifestItem)
|
||||
.sorted(Comparator.comparing(ToolManifestItem::name))
|
||||
.toList();
|
||||
validate(tools);
|
||||
return new ToolManifestResponse(bundleId, revision(bundleId, tools), tools);
|
||||
}
|
||||
|
||||
private ToolManifestItem toManifestItem(ToolMetadata tool) {
|
||||
String title = tool.getDisplayName() == null || tool.getDisplayName().isBlank()
|
||||
? tool.getName() : tool.getDisplayName();
|
||||
Map<String, Object> schema = tool.getParametersSchema() == null ? emptySchema() : tool.getParametersSchema();
|
||||
return new ToolManifestItem(
|
||||
tool.getName(), endpoint(tool), title, tool.getDescription(), schema,
|
||||
new ToolManifestAnnotations(title, isTrue(tool.getReadOnlyHint()), isTrue(tool.getDestructiveHint()),
|
||||
isTrue(tool.getIdempotentHint()), isTrue(tool.getOpenWorldHint())),
|
||||
new ToolManifestMeta(defaultString(tool.getSemver(), "1.0.0"),
|
||||
tool.getTimeoutMillis() == null ? DEFAULT_TIMEOUT_MILLIS : tool.getTimeoutMillis(),
|
||||
tool.getEnabled() == null || tool.getEnabled()));
|
||||
}
|
||||
|
||||
private void validate(List<ToolManifestItem> tools) {
|
||||
String namePrefix = properties.getManifest() == null ? null : properties.getManifest().getNamePrefix();
|
||||
Set<String> names = new LinkedHashSet<>();
|
||||
for (ToolManifestItem tool : tools) {
|
||||
if (tool.name() == null || tool.name().isBlank()) {
|
||||
throw new IllegalStateException("Tool manifest contains a blank tool name");
|
||||
}
|
||||
if (!names.add(tool.name())) {
|
||||
throw new IllegalStateException("Tool manifest contains duplicate tool name: " + tool.name());
|
||||
}
|
||||
if (namePrefix != null && !namePrefix.isBlank() && !tool.name().startsWith(namePrefix)) {
|
||||
throw new IllegalStateException("Tool name does not match mcp.manifest.name-prefix: " + tool.name());
|
||||
}
|
||||
if (!"object".equals(tool.inputSchema().get("type"))) {
|
||||
throw new IllegalStateException("Tool inputSchema root type must be object: " + tool.name());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized String revision(String bundleId, List<ToolManifestItem> tools) {
|
||||
String fingerprint = fingerprint(bundleId, tools);
|
||||
if (!fingerprint.equals(lastFingerprint)) {
|
||||
long localMinimum = lastRevision == null ? Long.MIN_VALUE : Long.parseLong(lastRevision) + 1;
|
||||
long nextTimestamp = LAST_ISSUED_REVISION.updateAndGet(previous ->
|
||||
Math.max(Math.max(System.currentTimeMillis(), localMinimum), previous + 1));
|
||||
lastFingerprint = fingerprint;
|
||||
lastRevision = Long.toString(nextTimestamp);
|
||||
}
|
||||
return lastRevision;
|
||||
}
|
||||
|
||||
private String fingerprint(String bundleId, List<ToolManifestItem> tools) {
|
||||
try {
|
||||
return objectMapper.writeValueAsString(Map.of("bundleId", bundleId, "tools", tools));
|
||||
} catch (Exception exception) {
|
||||
throw new IllegalStateException("Failed to build Tool manifest revision source", exception);
|
||||
}
|
||||
}
|
||||
|
||||
private String endpoint(ToolMetadata tool) {
|
||||
if (tool.getEndpoint() != null && !tool.getEndpoint().isBlank()) {
|
||||
return tool.getEndpoint();
|
||||
}
|
||||
if (tool.getPodUrl() == null || tool.getPodUrl().isBlank()) {
|
||||
return "/mcp/" + tool.getName();
|
||||
}
|
||||
return tool.getPodUrl().replaceAll("/+$", "") + "/mcp/" + tool.getName();
|
||||
}
|
||||
private Map<String, Object> emptySchema() {
|
||||
return Map.of("type", "object", "properties", Map.of(), "additionalProperties", false);
|
||||
}
|
||||
|
||||
private boolean isTrue(Boolean value) {
|
||||
return Boolean.TRUE.equals(value);
|
||||
}
|
||||
|
||||
private String defaultString(String value, String fallback) {
|
||||
return value == null || value.isBlank() ? fallback : value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package io.shinhanlife.dap.lib.mcp.config;
|
||||
|
||||
import org.springframework.cache.CacheManager;
|
||||
import org.springframework.cache.annotation.EnableCaching;
|
||||
import org.springframework.cache.concurrent.ConcurrentMapCacheManager;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.lib.mcp.config
|
||||
* @className CacheConfig
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@Configuration
|
||||
@EnableCaching
|
||||
public class CacheConfig {
|
||||
|
||||
// 스프링이 캐시를 관리할 기본 저장소를 빈(Bean)으로 등록합니다.
|
||||
@Bean
|
||||
public CacheManager cacheManager() {
|
||||
return new ConcurrentMapCacheManager("eimsData"); // 아까 설정한 캐시 이름 등록
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package io.shinhanlife.dap.lib.mcp.config;
|
||||
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.lib.mcp.config
|
||||
* @className JacksonConfig
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@Configuration
|
||||
public class JacksonConfig {
|
||||
|
||||
// 1. JSON 변환기(ObjectMapper)를 스프링 Bean으로 등록
|
||||
@Bean
|
||||
@Primary
|
||||
public ObjectMapper jsonMapper() {
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
||||
return mapper;
|
||||
}
|
||||
|
||||
// 2. XML 변환기(XmlMapper)를 스프링 Bean으로 등록
|
||||
@Bean
|
||||
public XmlMapper xmlMapper() {
|
||||
return new XmlMapper();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package io.shinhanlife.dap.lib.mcp.config;
|
||||
|
||||
import org.apache.kafka.clients.producer.ProducerConfig;
|
||||
import org.apache.kafka.common.serialization.StringSerializer;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.kafka.core.DefaultKafkaProducerFactory;
|
||||
import org.springframework.kafka.core.KafkaTemplate;
|
||||
import org.springframework.kafka.core.ProducerFactory;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.lib.mcp.config
|
||||
* @className KafkaLocalConfig
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@Configuration
|
||||
public class KafkaLocalConfig {
|
||||
|
||||
@Value("${spring.kafka.bootstrap-servers:localhost:9092}")
|
||||
private String bootstrapServers;
|
||||
|
||||
// 1. 카프카 전송 공장(Factory) 세팅
|
||||
@Bean
|
||||
public ProducerFactory<String, String> producerFactory() {
|
||||
Map<String, Object> configProps = new HashMap<>();
|
||||
// 가짜 로컬 주소 혹은 환경변수 세팅
|
||||
configProps.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
|
||||
// 데이터를 카프카로 보낼 때 문자열(String) 형태로 변환하겠다는 규칙
|
||||
configProps.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
|
||||
configProps.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
|
||||
|
||||
// 3초 만에 빠른 실패 처리 (로컬 무한 대기 방지)
|
||||
configProps.put(ProducerConfig.MAX_BLOCK_MS_CONFIG, 3000);
|
||||
// 재접속 주기를 10초로 설정 (콘솔 로그 도배 방지)
|
||||
configProps.put(ProducerConfig.RECONNECT_BACKOFF_MAX_MS_CONFIG, 10000);
|
||||
|
||||
return new DefaultKafkaProducerFactory<>(configProps);
|
||||
}
|
||||
|
||||
// 2. EaiEimsSender가 애타게 찾던 KafkaTemplate을 스프링 Bean으로 등록!
|
||||
@Bean
|
||||
public KafkaTemplate<String, String> kafkaTemplate() {
|
||||
return new KafkaTemplate<>(producerFactory());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package io.shinhanlife.dap.lib.mcp.config;
|
||||
|
||||
import io.swagger.v3.oas.models.Components;
|
||||
import io.swagger.v3.oas.models.OpenAPI;
|
||||
import io.swagger.v3.oas.models.info.Info;
|
||||
import io.swagger.v3.oas.models.security.SecurityRequirement;
|
||||
import io.swagger.v3.oas.models.security.SecurityScheme;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.lib.mcp.config
|
||||
* @className SwaggerConfig
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@Configuration
|
||||
public class SwaggerConfig {
|
||||
|
||||
@Bean
|
||||
public OpenAPI customOpenAPI() {
|
||||
return new OpenAPI()
|
||||
.info(new Info()
|
||||
.title("Shinhan MCP Gateway API 명세서")
|
||||
.version("v1.0")
|
||||
.description("AI Agent와 신한라이프 내부망(EIMS/EAI)을 연결하는 Adapter Gateway API 문서입니다."))
|
||||
.addServersItem(new io.swagger.v3.oas.models.servers.Server().url("http://localhost:8080").description("Adapter Pod (8080)"))
|
||||
.addServersItem(new io.swagger.v3.oas.models.servers.Server().url("http://localhost:8081").description("Gateway Pod (8081)"))
|
||||
// 전역적으로 X-API-KEY 보안 설정을 Swagger UI에 추가합니다.
|
||||
.addSecurityItem(new SecurityRequirement().addList("X-API-KEY"))
|
||||
.components(new Components()
|
||||
.addSecuritySchemes("X-API-KEY",
|
||||
new SecurityScheme()
|
||||
.name("X-API-KEY")
|
||||
.type(SecurityScheme.Type.APIKEY)
|
||||
.in(SecurityScheme.In.HEADER)
|
||||
.description("헤더에 API Key를 입력해주세요. ")));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package io.shinhanlife.dap.lib.mcp.config;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
import org.springframework.web.servlet.config.annotation.CorsRegistry;
|
||||
|
||||
import io.shinhanlife.dap.lib.mcp.security.ApiKeyInterceptor;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.lib.mcp.config
|
||||
* @className WebConfig
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@Configuration
|
||||
@RequiredArgsConstructor
|
||||
public class WebConfig implements WebMvcConfigurer {
|
||||
|
||||
// 1. 우리가 만든 인터셉터를 주입받습니다.
|
||||
private final ApiKeyInterceptor apiKeyInterceptor;
|
||||
|
||||
@Override
|
||||
public void addInterceptors(InterceptorRegistry registry) {
|
||||
// 2. 인터셉터 등록 및 검사할 URL 패턴 지정
|
||||
registry.addInterceptor(apiKeyInterceptor)
|
||||
.addPathPatterns("/rpc/**", "/mcp/api/v1/**") // /rpc/, /mcp/api/v1/ 로 시작하는 모든 API는 API Key 검사 수행!
|
||||
.excludePathPatterns(
|
||||
"/test/**", "/health", "/error", "/mcp/api/v1/admin/**",
|
||||
"/swagger-ui/**", "/v3/api-docs/**", "/swagger-resources/**", "/webjars/**", // Swagger UI 경로는 인증 제외
|
||||
"/mcp/api/v1/tools/docs/markdown", "/favicon.ico", "/mcp/api/v1/tools/list"
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addCorsMappings(CorsRegistry registry) {
|
||||
// Swagger UI(8080)에서 Gateway(8081)로 API 호출 시 발생하는 CORS 에러 해결
|
||||
registry.addMapping("/**")
|
||||
.allowedOriginPatterns("*")
|
||||
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
|
||||
.allowedHeaders("*")
|
||||
.exposedHeaders("Mcp-Session-Id")
|
||||
.allowCredentials(true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package io.shinhanlife.dap.lib.mcp.exception;
|
||||
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.lib.mcp.exception
|
||||
* @className GlobalExceptionHandler
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
import io.shinhanlife.dap.lib.adapter.dto.ErrorDetail;
|
||||
import io.shinhanlife.dap.lib.adapter.dto.JsonRpcResponse;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
import org.springframework.web.servlet.resource.NoResourceFoundException;
|
||||
|
||||
@Slf4j
|
||||
@RestControllerAdvice // 이 어노테이션이 전역 적용의 핵심입니다!
|
||||
public class GlobalExceptionHandler {
|
||||
|
||||
@ExceptionHandler(NoResourceFoundException.class)
|
||||
public ResponseEntity<Void> handleNoResourceFound(NoResourceFoundException e) {
|
||||
log.warn(" [Gateway Not Found] 요청하신 리소스를 찾을 수 없습니다: {}", e.getResourcePath());
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
@ExceptionHandler(IllegalArgumentException.class)
|
||||
public ResponseEntity<JsonRpcResponse> handleIllegalArgument(IllegalArgumentException e) {
|
||||
log.warn(" [Gateway Bad Request] 잘못된 요청: {}", e.getMessage());
|
||||
return buildErrorResponse(-32602, "Invalid params: " + e.getMessage());
|
||||
}
|
||||
|
||||
@ExceptionHandler(RuntimeException.class)
|
||||
public ResponseEntity<JsonRpcResponse> handleRuntime(RuntimeException e) {
|
||||
log.error(" [Gateway Internal Error] 시스템 장애: {}", e.getMessage(), e);
|
||||
return buildErrorResponse(-32603, "Internal error: " + e.getMessage());
|
||||
}
|
||||
|
||||
@ExceptionHandler(Exception.class)
|
||||
public ResponseEntity<JsonRpcResponse> handleAllException(Exception e) {
|
||||
log.error(" [Gateway Fatal Error] 치명적 오류 발생", e);
|
||||
return buildErrorResponse(-32000, "Server error: 시스템 관리자에게 문의하세요.");
|
||||
}
|
||||
|
||||
private ResponseEntity<JsonRpcResponse> buildErrorResponse(int code, String message) {
|
||||
JsonRpcResponse response = new JsonRpcResponse();
|
||||
response.setError(new ErrorDetail(code, message));
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package io.shinhanlife.dap.lib.mcp.filter;
|
||||
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.slf4j.MDC;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.lib.mcp.filter
|
||||
* @className MdcLoggingFilter
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@Component
|
||||
public class MdcLoggingFilter extends OncePerRequestFilter {
|
||||
|
||||
private static final String TRACE_ID_HEADER = "X-Trace-Id";
|
||||
private static final String MDC_KEY = "traceId";
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
|
||||
throws ServletException, IOException {
|
||||
|
||||
// 클라이언트가 보낸 Trace ID가 있으면 쓰고, 없으면 새로 생성
|
||||
String traceId = request.getHeader(TRACE_ID_HEADER);
|
||||
if (traceId == null || traceId.isEmpty()) {
|
||||
// 간결하게 8자리 UUID만 사용
|
||||
traceId = UUID.randomUUID().toString().substring(0, 8);
|
||||
}
|
||||
|
||||
// 로깅 컨텍스트에 고유 ID 저장
|
||||
MDC.put(MDC_KEY, traceId);
|
||||
|
||||
try {
|
||||
// 이 요청이 처리되는 동안 찍히는 모든 log.info, log.error에 traceId가 자동으로 붙습니다.
|
||||
filterChain.doFilter(request, response);
|
||||
} finally {
|
||||
// 메모리 누수 방지를 위해 요청이 끝나면 반드시 비워줍니다.
|
||||
MDC.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package io.shinhanlife.dap.lib.mcp;
|
||||
|
||||
/** Holds optional MCP headers for the lifetime of one HTTP request thread. */
|
||||
public final class McpRequestHeaderContext {
|
||||
private static final ThreadLocal<McpRequestHeaders> CURRENT_HEADERS = new ThreadLocal<>();
|
||||
|
||||
private McpRequestHeaderContext() {
|
||||
}
|
||||
|
||||
public static McpRequestHeaders current() {
|
||||
return CURRENT_HEADERS.get();
|
||||
}
|
||||
|
||||
static void set(McpRequestHeaders headers) {
|
||||
CURRENT_HEADERS.set(headers);
|
||||
}
|
||||
|
||||
static void clear() {
|
||||
CURRENT_HEADERS.remove();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package io.shinhanlife.dap.lib.mcp;
|
||||
|
||||
import java.io.IOException;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
/** Captures optional correlation and employee headers for an MCP HTTP call. */
|
||||
@Component
|
||||
public class McpRequestHeaderFilter extends OncePerRequestFilter {
|
||||
|
||||
@Override
|
||||
protected boolean shouldNotFilter(HttpServletRequest request) {
|
||||
return !request.getRequestURI().endsWith("/mcp");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
|
||||
FilterChain filterChain) throws ServletException, IOException {
|
||||
McpRequestHeaderContext.set(new McpRequestHeaders(
|
||||
request.getHeader("X-Request-Id"),
|
||||
request.getHeader("trace-id"),
|
||||
request.getHeader("request-id"),
|
||||
request.getHeader("employee-id")));
|
||||
try {
|
||||
filterChain.doFilter(request, response);
|
||||
} finally {
|
||||
McpRequestHeaderContext.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package io.shinhanlife.dap.lib.mcp;
|
||||
|
||||
/** Optional request headers propagated from an MCP HTTP request to a Tool invocation. */
|
||||
public record McpRequestHeaders(
|
||||
String headerRequestId,
|
||||
String traceId,
|
||||
String requestId,
|
||||
String encryptedEmployeeId) {
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package io.shinhanlife.dap.lib.mcp;
|
||||
|
||||
import io.modelcontextprotocol.server.transport.HttpServletStreamableServerTransportProvider;
|
||||
import org.springframework.boot.web.servlet.ServletRegistrationBean;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
|
||||
/** Exposes every Tool Pod through the MCP Streamable HTTP transport. */
|
||||
@Configuration
|
||||
public class ToolMcpServerConfiguration {
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
public HttpServletStreamableServerTransportProvider toolMcpTransportProvider() {
|
||||
return HttpServletStreamableServerTransportProvider.builder()
|
||||
.mcpEndpoint("/mcp")
|
||||
.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ServletRegistrationBean<HttpServletStreamableServerTransportProvider> toolMcpServlet(
|
||||
HttpServletStreamableServerTransportProvider transportProvider) {
|
||||
return new ServletRegistrationBean<>(transportProvider, "/mcp");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package io.shinhanlife.dap.lib.mcp;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import io.modelcontextprotocol.server.McpServerFeatures;
|
||||
import io.modelcontextprotocol.server.McpSyncServer;
|
||||
import io.modelcontextprotocol.spec.McpSchema;
|
||||
import io.shinhanlife.dap.lib.dto.ToolMetadata;
|
||||
import io.shinhanlife.dap.mcc.presentation.BusinessToolController;
|
||||
import io.shinhanlife.dap.lib.usecase.ToolRegistryHeartbeatSender;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/** Registers the Tool Pod's existing annotated tools with its MCP SDK server. */
|
||||
@Component
|
||||
public class ToolPodMcpToolSynchronizer {
|
||||
private final McpSyncServer mcpServer;
|
||||
private final ToolRegistryHeartbeatSender heartbeatSender;
|
||||
private final BusinessToolController businessToolController;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
public ToolPodMcpToolSynchronizer(McpSyncServer mcpServer, ToolRegistryHeartbeatSender heartbeatSender,
|
||||
BusinessToolController businessToolController, ObjectMapper objectMapper) {
|
||||
this.mcpServer = mcpServer;
|
||||
this.heartbeatSender = heartbeatSender;
|
||||
this.businessToolController = businessToolController;
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
@EventListener(ApplicationReadyEvent.class)
|
||||
public void registerLocalTools() {
|
||||
heartbeatSender.getAllScannedTools().stream()
|
||||
.filter(tool -> Boolean.TRUE.equals(tool.getVisible()))
|
||||
.forEach(tool -> mcpServer.addTool(specification(tool)));
|
||||
}
|
||||
|
||||
private McpServerFeatures.SyncToolSpecification specification(ToolMetadata tool) {
|
||||
McpSchema.Tool mcpTool = McpSchema.Tool.builder()
|
||||
.name(tool.getName())
|
||||
.description(tool.getDescription() == null || tool.getDescription().isBlank() ? tool.getName() + " Tool" : tool.getDescription())
|
||||
.inputSchema(tool.getParametersSchema() == null ? emptySchema() : tool.getParametersSchema())
|
||||
.annotations(McpSchema.ToolAnnotations.builder()
|
||||
.readOnlyHint(Boolean.TRUE.equals(tool.getReadOnlyHint()))
|
||||
.destructiveHint(Boolean.TRUE.equals(tool.getDestructiveHint()))
|
||||
.idempotentHint(Boolean.TRUE.equals(tool.getIdempotentHint()))
|
||||
.openWorldHint(Boolean.TRUE.equals(tool.getOpenWorldHint())).build())
|
||||
.build();
|
||||
return McpServerFeatures.SyncToolSpecification.builder().tool(mcpTool)
|
||||
.callHandler((context, request) -> invoke(tool.getName(), McpRequestHeaderContext.current(), request.arguments())).build();
|
||||
}
|
||||
|
||||
private McpSchema.CallToolResult invoke(String toolName, McpRequestHeaders requestHeaders,
|
||||
Map<String, Object> arguments) {
|
||||
ResponseEntity<?> response = businessToolController.executeDynamicTool(
|
||||
toolName,
|
||||
requestHeaders == null ? null : requestHeaders.headerRequestId(),
|
||||
requestHeaders == null ? null : requestHeaders.traceId(),
|
||||
requestHeaders == null ? null : requestHeaders.requestId(),
|
||||
requestHeaders == null ? null : requestHeaders.encryptedEmployeeId(),
|
||||
arguments);
|
||||
boolean failed = !response.getStatusCode().is2xxSuccessful();
|
||||
Object body = response.getBody();
|
||||
try {
|
||||
return McpSchema.CallToolResult.builder().addTextContent(objectMapper.writeValueAsString(body))
|
||||
.structuredContent(body).isError(failed).build();
|
||||
} catch (Exception error) {
|
||||
return McpSchema.CallToolResult.builder().addTextContent(String.valueOf(body)).isError(failed).build();
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String, Object> emptySchema() {
|
||||
Map<String, Object> schema = new LinkedHashMap<>();
|
||||
schema.put("type", "object");
|
||||
schema.put("properties", Map.of());
|
||||
schema.put("additionalProperties", false);
|
||||
return schema;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package io.shinhanlife.dap.lib.mcp.security;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.slf4j.MDC;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.servlet.HandlerInterceptor;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.lib.mcp.security
|
||||
* @className ApiKeyInterceptor
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class ApiKeyInterceptor implements HandlerInterceptor {
|
||||
|
||||
// 1. 다중 테넌트 API Key 목록이 담긴 프로퍼티 객체를 주입받습니다.
|
||||
private final SecurityProperties securityProperties;
|
||||
|
||||
@Override
|
||||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
|
||||
|
||||
if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
String apiKey = request.getHeader("X-API-KEY");
|
||||
Map<String, String> validApiKeys = securityProperties.getApiKeys();
|
||||
|
||||
// 2. 만약 프로퍼티에 API Key가 하나도 설정되어 있지 않다면 (개발/로컬 환경 등) 인증 없이 통과시킵니다.
|
||||
if (validApiKeys == null || validApiKeys.isEmpty()) {
|
||||
MDC.put("tenantId", "anonymous");
|
||||
request.setAttribute("tenantId", "anonymous");
|
||||
log.debug(" [보안 패스] 등록된 API Key 없음 - 익명 사용자(anonymous)로 통과");
|
||||
return true;
|
||||
}
|
||||
|
||||
// 3. 헤더로 들어온 API Key가 우리가 발급해준 목록(Map)에 존재하는지 확인합니다.
|
||||
if (apiKey == null || !validApiKeys.containsKey(apiKey)) {
|
||||
log.warn(" [보안 차단] 유효하지 않은 API Key 접근 시도 - IP: {}", request.getRemoteAddr());
|
||||
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid API Key");
|
||||
return false; // 컨트롤러로 넘어가지 않음
|
||||
}
|
||||
|
||||
// 4. 유효하다면 해당 키에 맵핑된 Tenant ID(식별자)를 가져옵니다. (ex. mcp-client-1)
|
||||
String tenantId = validApiKeys.get(apiKey);
|
||||
|
||||
// 4. 추출한 Tenant ID를 현재 스레드의 로깅 컨텍스트(MDC)에 저장합니다.
|
||||
// 이렇게 하면 이 요청이 끝날 때까지 찍히는 모든 로그에 어떤 테넌트가 호출했는지 자동으로 기록됩니다.
|
||||
MDC.put("tenantId", tenantId);
|
||||
|
||||
// 5. 필요시 컨트롤러 로직에서 사용할 수 있도록 Request 속성에도 담아줍니다.
|
||||
request.setAttribute("tenantId", tenantId);
|
||||
|
||||
log.debug(" [보안 통과] API Key 인증 성공 - 접속 테넌트: {}", tenantId);
|
||||
|
||||
return true; // 인증 통과! 컨트롤러로 진행
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
|
||||
// 6. 메모리 누수를 방지하기 위해 요청 처리가 완전히 끝나면 MDC에서 테넌트 정보를 지워줍니다.
|
||||
MDC.remove("tenantId");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package io.shinhanlife.dap.lib.mcp.security;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* [다중 테넌트 설정 매핑 클래스]
|
||||
* application-local.properties 파일에 정의된 mcp.security.api-keys.* 설정들을
|
||||
* Map 자료구조로 자동 바인딩(주입) 받기 위한 설정 클래스입니다.
|
||||
*/
|
||||
/**
|
||||
* @package io.shinhanlife.dap.lib.mcp.security
|
||||
* @className SecurityProperties
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@Data
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = "mcp.security")
|
||||
public class SecurityProperties {
|
||||
// API Key를 Key로, Tenant ID를 Value로 가지는 맵
|
||||
private Map<String, String> apiKeys = new HashMap<>();
|
||||
|
||||
// Tenant ID를 Key로, 허용된 도메인 그룹 목록을 Value로 가지는 맵 (ex. mcp-client-1 -> [CUSTOMER, COMMON])
|
||||
// 만약 "ALL" 이 포함되어 있다면 모든 도메인에 접근 허용
|
||||
private Map<String, List<String>> tenantDomains = new HashMap<>();
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package io.shinhanlife.dap.lib.session.converter;
|
||||
|
||||
import io.shinhanlife.dap.lib.session.dto.SessionDto;
|
||||
import io.shinhanlife.dap.lib.session.dto.ZtUsacOutDto;
|
||||
import org.mapstruct.Mapper;
|
||||
import org.mapstruct.Mapping;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.lib.session.converter
|
||||
* @className ZtUsacConverter
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@Mapper(componentModel = "spring")
|
||||
public abstract class ZtUsacConverter {
|
||||
|
||||
@Mapping(target = "loginDtm", ignore = true)
|
||||
@Mapping(target = "isManager", ignore = true)
|
||||
public abstract SessionDto toSessionDto(ZtUsacOutDto dto);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package io.shinhanlife.dap.lib.session.domain.model;
|
||||
|
||||
import io.shinhanlife.glow.db.dto.AuditInfo;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.lib.session.domain.model
|
||||
* @className ZtUsacModel
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
||||
@AllArgsConstructor
|
||||
public class ZtUsacModel extends AuditInfo {
|
||||
|
||||
/* 인사번호 */
|
||||
private String prafNo;
|
||||
/* 인사명 */
|
||||
private String prafNm;
|
||||
/* 조직번호 */
|
||||
private String ognzNo;
|
||||
/* 이메일주소 */
|
||||
private String addre;
|
||||
/* 인사직무코드 */
|
||||
private String prafOfduCd;
|
||||
/* 인사직무명 */
|
||||
private String prafOfduNm;
|
||||
/* 인사직급코드 */
|
||||
private String prafOfleCd;
|
||||
/* 인사직급명 */
|
||||
private String prafOfleNm;
|
||||
/* 인사직책코드 */
|
||||
private String prafDutyCd;
|
||||
/* 인사직책명 */
|
||||
private String prafDutyNm;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package io.shinhanlife.dap.lib.session.domain.repository;
|
||||
|
||||
import io.shinhanlife.glow.GlowMybatisMapper;
|
||||
import io.shinhanlife.dap.lib.session.dto.ZtUsacInDto;
|
||||
import io.shinhanlife.dap.lib.session.dto.ZtUsacOutDto;
|
||||
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.lib.session.domain.repository
|
||||
* @className ZtUsacRepository
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@GlowMybatisMapper
|
||||
public interface ZtUsacRepository {
|
||||
|
||||
/**
|
||||
* 사용자 조회 (단건)
|
||||
*
|
||||
* @param dto 사번
|
||||
* @return 인사정보
|
||||
*/
|
||||
ZtUsacOutDto selectZtUsac(ZtUsacInDto dto);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package io.shinhanlife.dap.lib.session.domain.usecase;
|
||||
|
||||
import io.shinhanlife.dap.lib.session.dto.ZtUsacInDto;
|
||||
import io.shinhanlife.dap.lib.session.dto.ZtUsacOutDto;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.lib.session.domain.service
|
||||
* @className ZtUsacUseCase
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
public interface ZtUsacUseCase {
|
||||
|
||||
/**
|
||||
* 사용자 조회 (단건)
|
||||
*
|
||||
* @param dto 사번
|
||||
* @return 인사정보
|
||||
*/
|
||||
ZtUsacOutDto selectZtUsac(ZtUsacInDto dto);
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package io.shinhanlife.dap.lib.session.domain.usecase.impl;
|
||||
|
||||
import io.shinhanlife.dap.lib.session.domain.repository.ZtUsacRepository;
|
||||
import io.shinhanlife.dap.lib.session.domain.usecase.ZtUsacUseCase;
|
||||
import io.shinhanlife.dap.lib.session.dto.ZtUsacInDto;
|
||||
import io.shinhanlife.dap.lib.session.dto.ZtUsacOutDto;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.lib.session.domain.usecase.impl
|
||||
* @className ZtUsacUseCaseImpl
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class ZtUsacUseCaseImpl implements ZtUsacUseCase {
|
||||
|
||||
private final ZtUsacRepository ztUsacRepository;
|
||||
|
||||
/**
|
||||
* 사용자 조회 (단건)
|
||||
*
|
||||
* @param dto 사번
|
||||
* @return 인사정보
|
||||
*/
|
||||
@Override
|
||||
public ZtUsacOutDto selectZtUsac(ZtUsacInDto dto) {
|
||||
ZtUsacOutDto result = ztUsacRepository.selectZtUsac(dto);
|
||||
result.initLists();
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package io.shinhanlife.dap.lib.session.dto;
|
||||
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.List;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.lib.session.dto
|
||||
* @className SessionDto
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class SessionDto {
|
||||
|
||||
/* 인사번호 */
|
||||
private String prafNo;
|
||||
/* 인사명 */
|
||||
private String prafNm;
|
||||
/* 조직번호 */
|
||||
private String ognzNo;
|
||||
/* 조직번호 */
|
||||
private String ognzNm;
|
||||
/* 이메일주소 */
|
||||
private String addre;
|
||||
/* 인사직무코드 */
|
||||
private String prafOfduCd;
|
||||
/* 인사직무명 */
|
||||
private String prafOfduNm;
|
||||
/* 인사직급코드 */
|
||||
private String prafOfleCd;
|
||||
/* 인사직급명 */
|
||||
private String prafOfleNm;
|
||||
/* 인사직책코드 */
|
||||
private String prafDutyCd;
|
||||
/* 인사직책명 */
|
||||
private String prafDutyNm;
|
||||
|
||||
private List<String> roleNoList;
|
||||
private List<String> roleNmList;
|
||||
private List<String> tgtrPrafNoList;
|
||||
private List<String> tgtrOgnzNoList;
|
||||
|
||||
// 추가된 LICO 연동 공통 헤더 필수 필드들
|
||||
private String strYmd;
|
||||
private String brafNo;
|
||||
private String psmrAsrtCd;
|
||||
private String sbsnRulpAsrtCd;
|
||||
private String bsduCd;
|
||||
private String bsquCd;
|
||||
private String ognzAsrtCd;
|
||||
private String ognzLeveCd;
|
||||
private String prgrId;
|
||||
|
||||
|
||||
// 유틸성
|
||||
private String loginDtm; // 로그인일시
|
||||
private String isManager; // 관리자여부
|
||||
|
||||
public void setLoginDtm() {
|
||||
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS");
|
||||
this.loginDtm = LocalDateTime.now().format(formatter);
|
||||
}
|
||||
|
||||
public void setIsManager(String isManager) {
|
||||
// TODO 역할 필터링 후 관리자 여부 체크
|
||||
this.isManager = "Y";
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package io.shinhanlife.dap.lib.session.dto;
|
||||
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.lib.session.dto
|
||||
* @className ZtUsacInDto
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class ZtUsacInDto {
|
||||
/* 인사번호 */
|
||||
private String prafNo;
|
||||
|
||||
/* 사용여부 */
|
||||
@Builder.Default
|
||||
private String puseYn = "Y";
|
||||
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package io.shinhanlife.dap.lib.session.dto;
|
||||
|
||||
import lombok.Setter;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.lib.session.dto
|
||||
* @className ZtUsacOutDto
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
||||
@AllArgsConstructor
|
||||
@Setter
|
||||
public class ZtUsacOutDto {
|
||||
|
||||
/* 인사번호 */
|
||||
private String prafNo;
|
||||
/* 인사명 */
|
||||
private String prafNm;
|
||||
/* 조직번호 */
|
||||
private String ognzNo;
|
||||
/* 조직명 */
|
||||
private String ognzNm;
|
||||
/* 이메일주소 */
|
||||
// @GlowSecureField(type = DataSecureType.DECRYPT_DB, direction = SafeDBType.COMM) TODO 방화벽 뚫리면 확인
|
||||
private String addre;
|
||||
/* 인사직무코드 */
|
||||
private String prafOfduCd;
|
||||
/* 인사직무명 */
|
||||
private String prafOfduNm;
|
||||
/* 인사직급코드 */
|
||||
private String prafOfleCd;
|
||||
/* 인사직급명 */
|
||||
private String prafOfleNm;
|
||||
/* 인사직책코드 */
|
||||
private String prafDutyCd;
|
||||
/* 인사직책명 */
|
||||
private String prafDutyNm;
|
||||
/* 사용여부 */
|
||||
private String puseYn;
|
||||
|
||||
private String roleNoStrList;
|
||||
private String roleNmStrList;
|
||||
private String tgtrPrafNoStrList;
|
||||
private String tgtrOgnzNoStrList;
|
||||
|
||||
private List<String> roleNoList;
|
||||
private List<String> roleNmList;
|
||||
private List<String> tgtrPrafNoList;
|
||||
private List<String> tgtrOgnzNoList;
|
||||
|
||||
public void initLists() {
|
||||
this.roleNoList = convertStrToList(roleNoStrList);
|
||||
this.roleNmList = convertStrToList(roleNmStrList);
|
||||
this.tgtrPrafNoList = convertStrToList(tgtrPrafNoStrList);
|
||||
this.tgtrOgnzNoList = convertStrToList(tgtrOgnzNoStrList);
|
||||
}
|
||||
|
||||
private List<String> convertStrToList(String str) {
|
||||
if (str == null || str.isEmpty()) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
return Arrays.asList(str.split(","));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package io.shinhanlife.dap.lib.session.presentation;
|
||||
|
||||
import io.micrometer.common.util.StringUtils;
|
||||
import io.shinhanlife.glow.BaseResponse;
|
||||
import io.shinhanlife.glow.BizException;
|
||||
import io.shinhanlife.glow.GlowControllerId;
|
||||
import io.shinhanlife.glow.ResponseUtil;
|
||||
import io.shinhanlife.dap.lib.session.converter.ZtUsacConverter;
|
||||
import io.shinhanlife.dap.lib.session.domain.usecase.ZtUsacUseCase;
|
||||
import io.shinhanlife.dap.lib.session.dto.SessionDto;
|
||||
import io.shinhanlife.dap.lib.session.dto.ZtUsacInDto;
|
||||
import io.shinhanlife.dap.lib.session.dto.ZtUsacOutDto;
|
||||
import io.shinhanlife.dap.lib.session.presentation.io.SsoResponse;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import jakarta.servlet.http.HttpSession;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.ibatis.javassist.NotFoundException;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.lib.session.presentation
|
||||
* @className SsoRestController
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
@RequestMapping("/sso")
|
||||
public class SsoRestController {
|
||||
|
||||
private static final String NLS_LOGIN_URL = "";
|
||||
private final ZtUsacUseCase ztUsacService;
|
||||
private final ZtUsacConverter ztUsacConverter;
|
||||
|
||||
/**
|
||||
* sso 연동 전 임시 로그인
|
||||
*
|
||||
* @param request
|
||||
* @param response
|
||||
* @param session
|
||||
* @param <T>
|
||||
* @return
|
||||
*/
|
||||
@GlowControllerId("tempLogin")
|
||||
@PostMapping("/tempLogin")
|
||||
public <T> ResponseEntity<BaseResponse<SsoResponse>> tempLogin(HttpServletRequest request, HttpServletResponse response,
|
||||
HttpSession session, @RequestBody SessionDto requestDto) {
|
||||
try {
|
||||
if (StringUtils.isEmpty(requestDto.getPrafNo())) {
|
||||
throw new NotFoundException("SSO >> not found sso id");
|
||||
}
|
||||
|
||||
// DB 유저 가져오기
|
||||
ZtUsacOutDto ztUsacOutDto = ztUsacService.selectZtUsac(ZtUsacInDto.builder().prafNo(requestDto.getPrafNo()).puseYn("Y")
|
||||
.build());
|
||||
if (Objects.isNull(ztUsacOutDto) || StringUtils.isEmpty(ztUsacOutDto.getPrafNo())) {
|
||||
throw new BizException("SSO >> not found UserInfo >> retCode:");
|
||||
}
|
||||
|
||||
SessionDto sessionDto = ztUsacConverter.toSessionDto(ztUsacOutDto);
|
||||
sessionDto.setLoginDtm(); // 로그인 시점 세팅
|
||||
session.setAttribute("userInfo", sessionDto);
|
||||
return ResponseUtil.ok(SsoResponse.builder().retCode("0").userInfo(sessionDto).build());
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("로그인 실패", e);
|
||||
session.invalidate();
|
||||
}
|
||||
|
||||
return ResponseUtil.ok(SsoResponse.builder().redirectUrl(NLS_LOGIN_URL).build());
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package io.shinhanlife.dap.lib.session.presentation.io;
|
||||
|
||||
import io.shinhanlife.dap.lib.session.dto.SessionDto;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.lib.session.presentation.io
|
||||
* @className SsoResponse
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class SsoResponse {
|
||||
|
||||
private String retCode;
|
||||
private SessionDto userInfo;
|
||||
private String redirectUrl;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package io.shinhanlife.dap.lib.usecase;
|
||||
|
||||
import io.shinhanlife.dap.lib.adapter.connector.LegacyEimsConnector;
|
||||
import io.shinhanlife.dap.lib.adapter.util.PiiMaskingUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.List;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.mcc.service
|
||||
* @className AbstractMcpToolUseCase
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@Slf4j
|
||||
public abstract class AbstractMcpToolUseCase {
|
||||
|
||||
@Autowired
|
||||
protected LegacyEimsConnector legacyEimsConnector;
|
||||
|
||||
@Autowired
|
||||
protected ObjectMapper objectMapper;
|
||||
|
||||
protected Map<String, Object> executeLegacy(String routingType, String interfaceId, Object inputData) {
|
||||
return executeLegacy(routingType, interfaceId, inputData, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 레거시 시스템을 호출하고 공통 처리(PII 마스킹 등)를 수행합니다. (스펙 지정 가능)
|
||||
*/
|
||||
protected Map<String, Object> executeLegacy(String routingType, String interfaceId, Object inputData, List<Map<String, Object>> spec) {
|
||||
Map<String, Object> inputMap;
|
||||
if (inputData == null) {
|
||||
inputMap = new HashMap<>();
|
||||
} else if (inputData instanceof Map) {
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> map = (Map<String, Object>) inputData;
|
||||
inputMap = map;
|
||||
} else {
|
||||
inputMap = objectMapper.convertValue(inputData, new TypeReference<Map<String, Object>>() {});
|
||||
}
|
||||
|
||||
try {
|
||||
log.info("\n=======================================================");
|
||||
log.info(" [Tool -> Legacy] 레거시 시스템 통신 시작");
|
||||
log.info(" - Routing Type: {}", routingType);
|
||||
log.info(" - Interface ID: {}", interfaceId);
|
||||
log.info(" - 호출 파라미터 (Request): \n{}", objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(inputMap));
|
||||
log.info("=======================================================\n");
|
||||
|
||||
// 1. Adapter 공통 모듈 직접 호출
|
||||
String executionResult = legacyEimsConnector.executeByTool(routingType, interfaceId, inputMap, spec);
|
||||
|
||||
log.info("\n=======================================================");
|
||||
log.info(" [Legacy -> Tool] 레거시 시스템 통신 완료");
|
||||
log.info(" - 응답 파라미터 (Response, 마스킹 전): \n{}", executionResult);
|
||||
log.info("=======================================================\n");
|
||||
|
||||
// 2. PII 마스킹 처리
|
||||
String maskedResult = PiiMaskingUtils.mask(executionResult);
|
||||
|
||||
if (executionResult != null && (executionResult.contains("\"status\":\"CIRCUIT_OPEN\"") || executionResult.contains("\"status\":\"TOO_MANY_REQUESTS\""))) {
|
||||
try {
|
||||
return objectMapper.readValue(executionResult, new TypeReference<Map<String, Object>>() {});
|
||||
} catch (Exception e) {
|
||||
log.error("Fallback JSON 파싱 에러: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("status", "SUCCESS");
|
||||
result.put("legacy_response", maskedResult);
|
||||
return result;
|
||||
} catch (Exception e) {
|
||||
Map<String, Object> error = new HashMap<>();
|
||||
error.put("status", "ERROR");
|
||||
error.put("message", e.getMessage());
|
||||
return error;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
package io.shinhanlife.dap.lib.usecase;
|
||||
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.mcc.service
|
||||
* @className ToolRegistryHeartbeatSender
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import io.shinhanlife.dap.lib.annotation.McpFunction;
|
||||
import io.shinhanlife.dap.lib.annotation.McpTool;
|
||||
import io.shinhanlife.dap.lib.config.McpProperties;
|
||||
import io.shinhanlife.dap.lib.dto.ToolMetadata;
|
||||
import io.shinhanlife.dap.lib.util.ToolSchemaResolver;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.aop.support.AopUtils;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.annotation.AnnotationUtils;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.web.client.RestClient;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
@Configuration
|
||||
@EnableScheduling
|
||||
@RequiredArgsConstructor
|
||||
public class ToolRegistryHeartbeatSender {
|
||||
|
||||
private final ApplicationContext applicationContext;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final McpProperties mcpProperties;
|
||||
private final RestClient restClient = RestClient.create();
|
||||
private final ToolSchemaResolver toolSchemaResolver;
|
||||
|
||||
@Value("${axhub.gateway.url:http://localhost:8081}")
|
||||
private String gatewayUrl;
|
||||
|
||||
@Value("${axhub.tool.url:http://localhost:8080}")
|
||||
private String podUrl;
|
||||
|
||||
private List<ToolMetadata> registeredTools = new ArrayList<>();
|
||||
|
||||
@Getter
|
||||
private List<ToolMetadata> allScannedTools = new ArrayList<>();
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
log.info(" [HeartbeatSender] 초기화 시작. Gateway URL: {}, Pod URL: {}", gatewayUrl, podUrl);
|
||||
scanAndBuildMetadata();
|
||||
}
|
||||
|
||||
private void scanAndBuildMetadata() {
|
||||
Map<String, Object> allBeans = applicationContext.getBeansOfType(Object.class);
|
||||
for (Object bean : allBeans.values()) {
|
||||
Class<?> targetClass = AopUtils.getTargetClass(bean);
|
||||
|
||||
// 클래스 또는 프록시(인터페이스)에서 @McpTool 스캔
|
||||
McpTool toolAnnotation = AnnotationUtils.findAnnotation(targetClass, McpTool.class);
|
||||
if (toolAnnotation == null) {
|
||||
toolAnnotation = AnnotationUtils.findAnnotation(bean.getClass(), McpTool.class);
|
||||
}
|
||||
|
||||
for (Method method : targetClass.getDeclaredMethods()) {
|
||||
// 메서드, 수퍼클래스, 인터페이스를 모두 뒤져서 @McpFunction 스캔
|
||||
McpFunction functionAnnotation = AnnotationUtils.findAnnotation(method, McpFunction.class);
|
||||
|
||||
if (functionAnnotation != null && toolAnnotation != null) {
|
||||
String baseName = functionAnnotation.displayName();
|
||||
String rawSubToolName = functionAnnotation.name();
|
||||
String subToolName = mcpProperties.getNamespace() != null && !mcpProperties.getNamespace().isEmpty()
|
||||
? mcpProperties.getNamespace() + "_" + rawSubToolName
|
||||
: rawSubToolName;
|
||||
|
||||
boolean isRegister = functionAnnotation.register();
|
||||
if (!isRegister) {
|
||||
log.info(" [HeartbeatSender] '{}' 툴은 어노테이션 설정에 의해 외부 등록(Redis) 대상에서 제외되었습니다. (최종 이름: {})", baseName, subToolName);
|
||||
}
|
||||
|
||||
ToolMetadata meta = new ToolMetadata();
|
||||
meta.setUid(UUID.nameUUIDFromBytes(subToolName.getBytes()).toString());
|
||||
meta.setDisplayName(baseName);
|
||||
meta.setName(subToolName);
|
||||
meta.setSemver(functionAnnotation.version());
|
||||
meta.setTimeoutMillis(functionAnnotation.timeoutMillis());
|
||||
meta.setEnabled(functionAnnotation.enabled());
|
||||
meta.setDescription(functionAnnotation.description());
|
||||
meta.setCategoryKey(toolAnnotation.categoryKey());
|
||||
meta.setIntegrationType(toolAnnotation.routingType());
|
||||
meta.setMciServiceId(functionAnnotation.mappingId());
|
||||
meta.setPodUrl(podUrl);
|
||||
meta.setEndpoint(podUrl.replaceAll("/+$", "") + "/mcp/" + subToolName);
|
||||
|
||||
boolean isVisible = functionAnnotation.visible();
|
||||
meta.setVisible(isVisible);
|
||||
meta.setIsRegistered(isRegister);
|
||||
meta.setRequiresApproval(functionAnnotation.requiresApproval());
|
||||
meta.setReadOnlyHint(functionAnnotation.readOnlyHint());
|
||||
meta.setDestructiveHint(functionAnnotation.destructiveHint());
|
||||
meta.setIdempotentHint(functionAnnotation.idempotentHint());
|
||||
meta.setOpenWorldHint(functionAnnotation.openWorldHint());
|
||||
|
||||
Map<String, String> prompts = new HashMap<>();
|
||||
String promptText = functionAnnotation.prompt();
|
||||
prompts.put(subToolName, promptText);
|
||||
meta.setActionPrompts(prompts);
|
||||
|
||||
if (method.getParameterCount() > 0) {
|
||||
try {
|
||||
Class<?> paramType = method.getParameterTypes()[0];
|
||||
Map<String, Object> finalSchema = toolSchemaResolver.resolve(functionAnnotation, paramType);
|
||||
meta.setParametersSchema(finalSchema);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to generate schema for {}", subToolName, e);
|
||||
}
|
||||
}
|
||||
|
||||
if (isRegister) {
|
||||
registeredTools.add(meta);
|
||||
}
|
||||
allScannedTools.add(meta);
|
||||
log.info(" [HeartbeatSender] 도구 메타데이터 생성: {} (isRegistered: {})", meta.getUid(), isRegister);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Scheduled(fixedRate = 30000)
|
||||
public void sendHeartbeats() {
|
||||
if (registeredTools.isEmpty()) return;
|
||||
|
||||
for (ToolMetadata tool : registeredTools) {
|
||||
try {
|
||||
ResponseEntity<String> response = restClient.post()
|
||||
.uri(gatewayUrl + "/mcp/api/v1/registry/heartbeat")
|
||||
.header("Content-Type", "application/json")
|
||||
|
||||
.body(tool.getUid())
|
||||
.retrieve()
|
||||
.toEntity(String.class);
|
||||
|
||||
if (response.getStatusCode().is2xxSuccessful()) {
|
||||
log.info(" [HeartbeatSender] 하트비트 전송 성공: {}", tool.getUid());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn(" [HeartbeatSender] 하트비트 전송 실패 ({}): {}. 재등록을 시도합니다.", tool.getUid(), e.getMessage());
|
||||
registerTool(tool);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void registerTool(ToolMetadata tool) {
|
||||
try {
|
||||
restClient.post()
|
||||
.uri(gatewayUrl + "/mcp/api/v1/registry/register")
|
||||
.header("Content-Type", "application/json")
|
||||
|
||||
.body(tool)
|
||||
.retrieve()
|
||||
.toBodilessEntity();
|
||||
log.info(" [HeartbeatSender] 툴 재등록 성공: {}", tool.getUid());
|
||||
} catch (Exception ex) {
|
||||
log.error(" [HeartbeatSender] 툴 등록 실패: {}", ex.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
package io.shinhanlife.dap.lib.util;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.annotation.JsonPropertyDescription;
|
||||
import io.shinhanlife.dap.lib.annotation.McpParameter;
|
||||
import io.shinhanlife.dap.lib.annotation.McpValidation;
|
||||
import io.shinhanlife.dap.lib.annotation.McpAnyOf;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.ParameterizedType;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.lib.util
|
||||
* @className JsonSchemaGenerator
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
public class JsonSchemaGenerator {
|
||||
|
||||
/**
|
||||
* Java DTO 클래스를 분석하여 MCP 규격의 완전한 JSON Schema를 생성합니다.
|
||||
*/
|
||||
public static Map<String, Object> generateSchema(Class<?> clazz) {
|
||||
return generateSchema(clazz, new HashSet<>());
|
||||
}
|
||||
|
||||
private static Map<String, Object> generateSchema(Class<?> clazz, Set<Class<?>> visiting) {
|
||||
Map<String, Object> schema = new HashMap<>();
|
||||
schema.put("type", "object");
|
||||
schema.put("additionalProperties", false);
|
||||
if (!visiting.add(clazz)) {
|
||||
return schema;
|
||||
}
|
||||
|
||||
Map<String, Object> properties = new HashMap<>();
|
||||
List<String> requiredList = new ArrayList<>();
|
||||
|
||||
for (Field field : clazz.getDeclaredFields()) {
|
||||
Map<String, Object> fieldSchema = createFieldSchema(field, visiting);
|
||||
|
||||
// 1. 타입 매핑
|
||||
|
||||
// 2. 어노테이션 기반 설명 추출
|
||||
McpParameter paramAnnotation = field.getAnnotation(McpParameter.class);
|
||||
JsonPropertyDescription descAnnotation = field.getAnnotation(JsonPropertyDescription.class);
|
||||
if (paramAnnotation != null && !paramAnnotation.description().isEmpty()) {
|
||||
fieldSchema.put("description", paramAnnotation.description());
|
||||
} else if (descAnnotation != null && !descAnnotation.value().isEmpty()) {
|
||||
fieldSchema.put("description", descAnnotation.value());
|
||||
} else {
|
||||
fieldSchema.put("description", field.getName()); // 기본값
|
||||
}
|
||||
|
||||
// 3. 필수 여부 판단
|
||||
JsonProperty jsonProp = field.getAnnotation(JsonProperty.class);
|
||||
if ((jsonProp != null && jsonProp.required()) || (paramAnnotation != null && paramAnnotation.required())) {
|
||||
requiredList.add(field.getName());
|
||||
}
|
||||
|
||||
McpValidation validation = field.getAnnotation(McpValidation.class);
|
||||
if (validation != null && validation.required() && !requiredList.contains(field.getName())) {
|
||||
requiredList.add(field.getName());
|
||||
}
|
||||
if (validation != null && !validation.pattern().isEmpty()) {
|
||||
fieldSchema.put("pattern", validation.pattern());
|
||||
}
|
||||
if (validation != null && validation.minimum() != Long.MIN_VALUE) {
|
||||
fieldSchema.put("minimum", validation.minimum());
|
||||
}
|
||||
if (validation != null && validation.maximum() != Long.MAX_VALUE) {
|
||||
fieldSchema.put("maximum", validation.maximum());
|
||||
}
|
||||
if (validation != null && validation.minLength() >= 0) {
|
||||
fieldSchema.put("minLength", validation.minLength());
|
||||
}
|
||||
if (validation != null && validation.maxLength() >= 0) {
|
||||
fieldSchema.put("maxLength", validation.maxLength());
|
||||
}
|
||||
if (validation != null && validation.allowedValues().length > 0) {
|
||||
fieldSchema.put("enum", List.of(validation.allowedValues()));
|
||||
}
|
||||
if (validation != null && !validation.format().isEmpty()) {
|
||||
fieldSchema.put("format", validation.format());
|
||||
}
|
||||
if (validation != null && !validation.defaultValue().isEmpty()) {
|
||||
fieldSchema.put("default", coerceDefaultValue(validation.defaultValue(), field.getType()));
|
||||
}
|
||||
if (validation != null && validation.examples().length > 0) {
|
||||
fieldSchema.put("examples", List.of(validation.examples()));
|
||||
}
|
||||
if (validation != null && validation.nullable()) {
|
||||
Map<String, Object> nonNullSchema = new HashMap<>(fieldSchema);
|
||||
fieldSchema = new HashMap<>();
|
||||
fieldSchema.put("anyOf", List.of(
|
||||
nonNullSchema,
|
||||
Map.of("type", "null")
|
||||
));
|
||||
}
|
||||
|
||||
properties.put(field.getName(), fieldSchema);
|
||||
}
|
||||
|
||||
schema.put("properties", properties);
|
||||
if (!requiredList.isEmpty()) {
|
||||
schema.put("required", requiredList);
|
||||
}
|
||||
|
||||
McpAnyOf anyOfAnnotation = clazz.getAnnotation(McpAnyOf.class);
|
||||
if (anyOfAnnotation != null && anyOfAnnotation.value().length > 0) {
|
||||
List<Map<String, Object>> anyOfList = new ArrayList<>();
|
||||
for (String fieldName : anyOfAnnotation.value()) {
|
||||
anyOfList.add(Map.of("required", List.of(fieldName)));
|
||||
|
||||
}
|
||||
schema.put("anyOf", anyOfList);
|
||||
}
|
||||
|
||||
visiting.remove(clazz);
|
||||
return schema;
|
||||
}
|
||||
|
||||
|
||||
private static Object coerceDefaultValue(String value, Class<?> fieldType) {
|
||||
try {
|
||||
if (fieldType == Integer.class || fieldType == int.class
|
||||
|| fieldType == Long.class || fieldType == long.class
|
||||
|| fieldType == Short.class || fieldType == short.class
|
||||
|| fieldType == Byte.class || fieldType == byte.class) {
|
||||
return Long.valueOf(value);
|
||||
}
|
||||
if (fieldType == Double.class || fieldType == double.class
|
||||
|| fieldType == Float.class || fieldType == float.class) {
|
||||
return Double.valueOf(value);
|
||||
}
|
||||
if (fieldType == Boolean.class || fieldType == boolean.class) {
|
||||
return Boolean.valueOf(value);
|
||||
}
|
||||
return value;
|
||||
} catch (NumberFormatException e) {
|
||||
throw new IllegalArgumentException("Invalid MCP default value: " + value, e);
|
||||
}
|
||||
}
|
||||
private static Map<String, Object> createFieldSchema(Field field, Set<Class<?>> visiting) {
|
||||
Class<?> fieldType = field.getType();
|
||||
if (isSimpleType(fieldType)) {
|
||||
return new HashMap<>(Map.of("type", mapJavaTypeToJsonType(fieldType)));
|
||||
}
|
||||
if (List.class.isAssignableFrom(fieldType)) {
|
||||
Map<String, Object> fieldSchema = new HashMap<>();
|
||||
fieldSchema.put("type", "array");
|
||||
fieldSchema.put("items", generateItemsSchema(field, visiting));
|
||||
return fieldSchema;
|
||||
}
|
||||
return generateSchema(fieldType, visiting);
|
||||
}
|
||||
|
||||
private static Map<String, Object> generateItemsSchema(Field field, Set<Class<?>> visiting) {
|
||||
Type genericType = field.getGenericType();
|
||||
if (genericType instanceof ParameterizedType parameterizedType) {
|
||||
Type itemType = parameterizedType.getActualTypeArguments()[0];
|
||||
if (itemType instanceof Class<?> itemClass) {
|
||||
if (isSimpleType(itemClass)) {
|
||||
return new HashMap<>(Map.of("type", mapJavaTypeToJsonType(itemClass)));
|
||||
}
|
||||
return generateSchema(itemClass, visiting);
|
||||
}
|
||||
}
|
||||
return new HashMap<>(Map.of("type", "object"));
|
||||
}
|
||||
|
||||
private static boolean isSimpleType(Class<?> clazz) {
|
||||
return clazz == String.class
|
||||
|| clazz == Integer.class || clazz == int.class
|
||||
|| clazz == Long.class || clazz == long.class
|
||||
|| clazz == Double.class || clazz == double.class
|
||||
|| clazz == Float.class || clazz == float.class
|
||||
|| clazz == Boolean.class || clazz == boolean.class;
|
||||
}
|
||||
|
||||
private static String mapJavaTypeToJsonType(Class<?> clazz) {
|
||||
if (clazz == String.class) return "string";
|
||||
if (clazz == Integer.class || clazz == int.class) return "integer";
|
||||
if (clazz == Long.class || clazz == long.class) return "integer";
|
||||
if (clazz == Double.class || clazz == double.class) return "number";
|
||||
if (clazz == Float.class || clazz == float.class) return "number";
|
||||
if (clazz == Boolean.class || clazz == boolean.class) return "boolean";
|
||||
if (List.class.isAssignableFrom(clazz)) return "array";
|
||||
return "object";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,309 @@
|
||||
package io.shinhanlife.dap.lib.util;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Scanner;
|
||||
|
||||
public class PodScaffolder {
|
||||
|
||||
public static void main(String[] args) throws IOException {
|
||||
Scanner scanner = new Scanner(System.in);
|
||||
|
||||
System.out.println("=========================================");
|
||||
System.out.println(" MCP Tool Pod Scaffolder (Java CLI) ");
|
||||
System.out.println("=========================================\n");
|
||||
|
||||
String rawModuleName = getOrAsk(args, 0, scanner, "1. 생성할 모듈(Pod) 이름 (예: payment 또는 dap-tool-payment): ");
|
||||
String moduleName = rawModuleName.startsWith("dap-tool-") ? rawModuleName : "dap-tool-" + rawModuleName;
|
||||
String portStr = getOrAsk(args, 1, scanner, "2. 사용할 포트 번호 (예: 8085): ");
|
||||
String shortName = moduleName.replace("dap-tool-", "").replace("-", "");
|
||||
|
||||
String defaultAuthor = System.getProperty("user.name");
|
||||
String defaultDate = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy.MM.dd"));
|
||||
|
||||
String author = getOrAsk(args, 2, scanner, "3. 작성자 (엔터 입력 시 '" + defaultAuthor + "'): ");
|
||||
if (author.trim().isEmpty()) author = defaultAuthor;
|
||||
String createDate = getOrAsk(args, 3, scanner, "4. 작성일 (엔터 입력 시 '" + defaultDate + "'): ");
|
||||
if (createDate.trim().isEmpty()) createDate = defaultDate;
|
||||
|
||||
String result = scaffoldPod(moduleName, portStr, shortName, author, createDate);
|
||||
System.out.println(result);
|
||||
}
|
||||
|
||||
private static String getOrAsk(String[] args, int index, Scanner scanner, String prompt) {
|
||||
if (args.length > index) {
|
||||
return args[index];
|
||||
}
|
||||
System.out.print(prompt);
|
||||
return scanner.nextLine().trim();
|
||||
}
|
||||
|
||||
public static String scaffoldPod(String moduleName, String portStr, String shortName, String author, String createDate) throws IOException {
|
||||
String envSourceDir = System.getenv("AXHUB_SOURCE_DIR");
|
||||
Path rootDir = envSourceDir != null ? Paths.get(envSourceDir) : Paths.get(".");
|
||||
|
||||
Path modulePath = rootDir.resolve(Paths.get(moduleName));
|
||||
if (Files.exists(modulePath)) {
|
||||
return "[오류] 이미 존재하는 모듈입니다: " + moduleName;
|
||||
}
|
||||
|
||||
StringBuilder log = new StringBuilder();
|
||||
log.append("[1/6] 모듈 디렉터리 생성 중...\n");
|
||||
Files.createDirectories(modulePath);
|
||||
|
||||
log.append("[2/6] build.gradle 생성 중...\n");
|
||||
String buildGradle = """
|
||||
plugins {
|
||||
id 'org.springframework.boot'
|
||||
}
|
||||
dependencies {
|
||||
implementation project(':dap-was-lib')
|
||||
}
|
||||
dependencies {
|
||||
compileOnly 'org.projectlombok:lombok:1.18.32'
|
||||
annotationProcessor 'org.projectlombok:lombok:1.18.32'
|
||||
}
|
||||
""";
|
||||
Files.writeString(modulePath.resolve("build.gradle"), buildGradle);
|
||||
|
||||
log.append("[3/6] Dockerfile 생성 중...\n");
|
||||
String dockerfile = """
|
||||
FROM eclipse-temurin:21-jdk-alpine
|
||||
WORKDIR /app
|
||||
COPY build/libs/%s-0.0.1-SNAPSHOT.jar app.jar
|
||||
ENTRYPOINT ["java", "-jar", "app.jar"]
|
||||
""".formatted(moduleName);
|
||||
Files.writeString(modulePath.resolve("Dockerfile"), dockerfile);
|
||||
|
||||
log.append("[4/6] Application 클래스 및 설정 파일 생성 중...\n");
|
||||
Path srcPath = modulePath.resolve("src/main/java/io/shinhanlife/dap/mcc/" + shortName);
|
||||
Files.createDirectories(srcPath);
|
||||
|
||||
String appClass = """
|
||||
package io.shinhanlife.dap.mcc.%s;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
|
||||
import org.springframework.cache.annotation.EnableCaching;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.mcc.%s
|
||||
* @className DapTool%sApplication
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author %s
|
||||
* @create %s
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* %s %s 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@SpringBootApplication(scanBasePackages = {"io.shinhanlife.dap.mcc", "io.shinhanlife.dap.lib.adapter", "io.shinhanlife.dap.lib.mcp", "io.shinhanlife.dap.lib.config", "io.shinhanlife.dap.lib.integration"})
|
||||
@ConfigurationPropertiesScan(basePackages = {"io.shinhanlife.dap.mcc", "io.shinhanlife.dap.lib.adapter", "io.shinhanlife.dap.lib.mcp", "io.shinhanlife.dap.lib.config", "io.shinhanlife.dap.lib.integration"})
|
||||
@EnableCaching
|
||||
public class DapTool%sApplication {
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(DapTool%sApplication.class, args);
|
||||
}
|
||||
}
|
||||
""".formatted(shortName, shortName, capitalize(shortName), author, createDate, createDate, author, capitalize(shortName), capitalize(shortName));
|
||||
Files.writeString(srcPath.resolve("DapTool" + capitalize(shortName) + "Application.java"), appClass);
|
||||
|
||||
Path resPath = modulePath.resolve("src/main/resources");
|
||||
Files.createDirectories(resPath);
|
||||
String applicationYml = """
|
||||
server:
|
||||
port: %s
|
||||
spring:
|
||||
application:
|
||||
name: %s
|
||||
profiles:
|
||||
active: local
|
||||
logging:
|
||||
level:
|
||||
org.apache.kafka: ERROR
|
||||
mcp:
|
||||
namespace: ""
|
||||
security:
|
||||
tenant-domains:
|
||||
TESTER-DEV: ALL
|
||||
""".formatted(portStr, moduleName);
|
||||
Files.writeString(resPath.resolve("application.yml"), applicationYml);
|
||||
|
||||
String applicationLocalYml = """
|
||||
# Local 환경 전용 설정 (H2 메모리 DB 등)
|
||||
spring:
|
||||
datasource:
|
||||
url: jdbc:p6spy:h2:mem:testdb;DB_CLOSE_DELAY=-1;
|
||||
driverClassName: com.p6spy.engine.spy.P6SpyDriver
|
||||
username: sa
|
||||
password: password
|
||||
h2:
|
||||
console:
|
||||
enabled: true
|
||||
|
||||
eims:
|
||||
http:
|
||||
url: http://localhost:${server.port}/api/gateway
|
||||
tcp:
|
||||
host: 127.0.0.1
|
||||
port: 8090
|
||||
timeout: 5000
|
||||
jsp:
|
||||
form:
|
||||
url: http://localhost:${server.port}/mock/jsp-form
|
||||
json:
|
||||
url: http://localhost:${server.port}/mock/jsp-json
|
||||
mci:
|
||||
url: http://localhost:${server.port}/api/mock/esb/api
|
||||
mcistring:
|
||||
url: http://localhost:${server.port}/api/mock/esb/string
|
||||
|
||||
mcp:
|
||||
security:
|
||||
tenant-domains:
|
||||
mcp-client-1: CUSTOMER,COMMON
|
||||
mcp-client-2: ALL
|
||||
|
||||
axhub:
|
||||
gateway:
|
||||
url: http://localhost:8081
|
||||
tool:
|
||||
url: ${AXHUB_TOOL_URL:http://localhost:${server.port}}
|
||||
""";
|
||||
Files.writeString(resPath.resolve("application-local.yml"), applicationLocalYml);
|
||||
|
||||
String applicationDevYml = """
|
||||
# OCI 클라우드 환경 전용 설정
|
||||
server:
|
||||
port: ${PORT:%s}
|
||||
|
||||
axhub:
|
||||
gateway:
|
||||
url: https://axhubmcp.devjun.net
|
||||
tool:
|
||||
url: http://144.24.70.100:%s
|
||||
|
||||
eims:
|
||||
http:
|
||||
url: http://localhost:${server.port}/api/gateway
|
||||
tcp:
|
||||
host: 127.0.0.1
|
||||
port: 8090
|
||||
timeout: 5000
|
||||
jsp:
|
||||
form:
|
||||
url: http://localhost:${server.port}/mock/jsp-form
|
||||
json:
|
||||
url: http://localhost:${server.port}/mock/jsp-json
|
||||
mci:
|
||||
url: http://localhost:${server.port}/api/mock/esb/api
|
||||
mcistring:
|
||||
url: http://localhost:${server.port}/api/mock/esb/string
|
||||
|
||||
shinhan:
|
||||
integration:
|
||||
envrTypeCd: D
|
||||
eai:
|
||||
url: http://10.176.32.181
|
||||
internalMci:
|
||||
url: http://10.176.32.173
|
||||
bancaMci:
|
||||
url: http://10.176.32.117
|
||||
externalMci:
|
||||
url: http://10.176.32.176
|
||||
""".formatted(portStr, portStr);
|
||||
Files.writeString(resPath.resolve("application-dev.yml"), applicationDevYml);
|
||||
|
||||
String logbackXml = """
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<configuration>
|
||||
<property name="LOG_PATTERN" value="%%d{yyyy-MM-dd HH:mm:ss.SSS} [%%thread] [%%X{traceId}] %%-5level %%logger{36} - %%msg%%n" />
|
||||
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder>
|
||||
<pattern>${LOG_PATTERN}</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<file>logs/%s.log</file>
|
||||
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
||||
<fileNamePattern>logs/%s-%%d{yyyy-MM-dd}.log</fileNamePattern>
|
||||
<maxHistory>30</maxHistory>
|
||||
</rollingPolicy>
|
||||
<encoder>
|
||||
<pattern>${LOG_PATTERN}</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
<root level="INFO">
|
||||
<appender-ref ref="CONSOLE" />
|
||||
<appender-ref ref="FILE" />
|
||||
</root>
|
||||
<logger name="io.shinhanlife" level="DEBUG" />
|
||||
</configuration>
|
||||
""".formatted(moduleName, moduleName);
|
||||
Files.writeString(resPath.resolve("logback-spring.xml"), logbackXml);
|
||||
|
||||
log.append("[5/6] settings.gradle 에 모듈 등록 중...\n");
|
||||
Path settingsPath = rootDir.resolve(Paths.get("settings.gradle"));
|
||||
if (Files.exists(settingsPath)) {
|
||||
String settings = Files.readString(settingsPath);
|
||||
if (!settings.contains("include '" + moduleName + "'")) {
|
||||
Files.writeString(settingsPath, System.lineSeparator() + "include '" + moduleName + "'" + System.lineSeparator(), StandardOpenOption.APPEND);
|
||||
}
|
||||
}
|
||||
|
||||
log.append("[6/6] docker-compose.yml 에 서비스 추가 중...\n");
|
||||
Path dockerComposePath = rootDir.resolve(Paths.get("docker-compose.yml"));
|
||||
if (Files.exists(dockerComposePath)) {
|
||||
String compose = Files.readString(dockerComposePath);
|
||||
String serviceName = moduleName.replace("dap-", ""); // e.g. tool-payment
|
||||
if (!compose.contains(" " + serviceName + ":")) {
|
||||
String newService = """
|
||||
%s:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: %s/Dockerfile
|
||||
ports:
|
||||
- "%s:%s"
|
||||
depends_on:
|
||||
- redis
|
||||
environment:
|
||||
- TZ=Asia/Seoul
|
||||
- SPRING_REDIS_HOST=redis
|
||||
- SPRING_REDIS_PORT=6379
|
||||
- SPRING_DATA_REDIS_PORT=6379
|
||||
- AXHUB_GATEWAY_URL=http://gateway:8081
|
||||
- AXHUB_TOOL_URL=http://%s:%s
|
||||
- GLOW_COMMUNICATION_MCI_HOST=http://mci-mock
|
||||
- GLOW_COMMUNICATION_MCI_PORT=8080
|
||||
- GLOW_COMMUNICATION_EXTMCI_HOST=http://mci-mock
|
||||
- GLOW_COMMUNICATION_EXTMCI_PORT=8080
|
||||
- GLOW_COMMUNICATION_EAI_HOST=http://mci-mock
|
||||
- GLOW_COMMUNICATION_EAI_PORT=8080
|
||||
""".formatted(serviceName, moduleName, portStr, portStr, serviceName, portStr);
|
||||
Files.writeString(dockerComposePath, System.lineSeparator() + newService, StandardOpenOption.APPEND);
|
||||
}
|
||||
}
|
||||
|
||||
log.append("\n=========================================\n");
|
||||
log.append(" Pod Scaffolding Complete! \n");
|
||||
log.append("=========================================\n");
|
||||
log.append("1. [새로운 모듈] ").append(moduleName).append(" 폴더가 생성되었습니다.\n");
|
||||
log.append("2. [ToolScaffolder]를 사용해 이 모듈 안에 툴을 추가하세요.\n");
|
||||
log.append("3. 실행 전 Gradle 동기화(Sync)를 한 번 진행해 주세요.\n");
|
||||
return log.toString();
|
||||
}
|
||||
|
||||
private static String capitalize(String str) {
|
||||
if (str == null || str.isEmpty()) return str;
|
||||
return str.substring(0, 1).toUpperCase() + str.substring(1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package io.shinhanlife.dap.lib.util;
|
||||
|
||||
import io.shinhanlife.dap.lib.session.dto.SessionDto;
|
||||
import jakarta.servlet.http.HttpSession;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.lib.util
|
||||
* @className SessionUtil
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
public class SessionUtil {
|
||||
|
||||
private static final String SESSION_KEY = "userInfo";
|
||||
|
||||
private SessionUtil() {}
|
||||
|
||||
public static SessionDto getSession() {
|
||||
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||||
if (attributes == null) return null;
|
||||
HttpSession session = attributes.getRequest().getSession(false);
|
||||
if (session == null) return null;
|
||||
return (SessionDto) session.getAttribute(SESSION_KEY);
|
||||
}
|
||||
|
||||
public static String getPrafNo() {
|
||||
SessionDto session = getSession();
|
||||
return session != null ? session.getPrafNo() : null;
|
||||
}
|
||||
|
||||
public static String getOgnzNo() {
|
||||
SessionDto session = getSession();
|
||||
return session != null ? session.getOgnzNo() : null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,714 @@
|
||||
package io.shinhanlife.dap.lib.util;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Locale;
|
||||
import java.util.Scanner;
|
||||
|
||||
/**
|
||||
* MCP Tool 코드를 자동 생성(Scaffolding)하는 유틸리티 클래스
|
||||
*
|
||||
* [실행 방법]
|
||||
* 방법 1. IDE(IntelliJ 등)에서 직접 실행 (대화형 모드 추천 ⭐)
|
||||
* - 이 클래스(ToolScaffolder.java)를 열고 main 메서드를 직접 실행(Run)합니다.
|
||||
* - 콘솔 창에 뜨는 질문에 차례대로 값을 입력하기만 하면 파일이 생성됩니다.
|
||||
*
|
||||
* 방법 2. 커맨드라인(터미널)에서 실행 (명령어 기반)
|
||||
* - 컴파일: javac -encoding UTF-8 dap-was-lib/src/main/java/io/shinhanlife/dap/mcc/util/ToolScaffolder.java
|
||||
* - 실행: java -cp dap-was-lib/src/main/java io.shinhanlife.dap.lib.util.ToolScaffolder [이름] [ID] "[설명]" "[그룹]" "[통신방식]" "[모듈명]"
|
||||
*/
|
||||
/**
|
||||
* @package io.shinhanlife.dap.lib.util
|
||||
* @className ToolScaffolder
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
public class ToolScaffolder {
|
||||
|
||||
private static final String BASE_PACKAGE = "io.shinhanlife.dap.mcc";
|
||||
private static final String BASE_PACKAGE_PATH = "src/main/java/io/shinhanlife/dap/mcc";
|
||||
|
||||
public static void main(String[] args) throws IOException {
|
||||
Scanner scanner = new Scanner(System.in);
|
||||
|
||||
System.out.println("=========================================");
|
||||
System.out.println(" MCP Tool Scaffolder (Java CLI) ");
|
||||
System.out.println("=========================================\n");
|
||||
|
||||
String baseName = getOrAsk(args, 0, scanner, "1. 생성할 Tool의 기본 이름 (예: ExchangeRate) [영문 PascalCase]: ");
|
||||
String interfaceId = getOrAsk(args, 1, scanner, "2. 레거시 API 인터페이스 ID (예: EXCH_001): ");
|
||||
String description = getOrAsk(args, 2, scanner, "3. Tool 기능 설명 (예: 환율 조회): ");
|
||||
String group = getOrAsk(args, 3, scanner, "4. Tool 소속 그룹 (예: SAMPLE, NOTIFICATION, CLAIM, POLICY, HR, CONTRACT, CUSTOMER 등): ");
|
||||
if (group.isEmpty()) group = "COMMON";
|
||||
String routingType = getOrAsk(args, 4, scanner, "5. 통신 프로토콜 (예: HTTP, TCP, MCI, EAI): ");
|
||||
if (routingType.trim().isEmpty()) {
|
||||
routingType = "HTTP";
|
||||
}
|
||||
String moduleName = getOrAsk(args, 5, scanner, "6. 코드를 생성할 모듈 (기본: dap-was-oth): ");
|
||||
if (moduleName.trim().isEmpty()) {
|
||||
moduleName = "dap-was-oth";
|
||||
}
|
||||
|
||||
String defaultAuthor = System.getProperty("user.name");
|
||||
String defaultDate = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy.MM.dd"));
|
||||
|
||||
String author = getOrAsk(args, 6, scanner, "7. 작성자 (엔터 입력 시 '" + defaultAuthor + "'): ");
|
||||
if (author.trim().isEmpty()) author = defaultAuthor;
|
||||
String createDate = getOrAsk(args, 7, scanner, "8. 작성일 (엔터 입력 시 '" + defaultDate + "'): ");
|
||||
if (createDate.trim().isEmpty()) createDate = defaultDate;
|
||||
|
||||
String result = scaffold(baseName, interfaceId, description, group, routingType, moduleName, author, createDate, true, null);
|
||||
System.out.println(result);
|
||||
}
|
||||
|
||||
private static String getOrAsk(String[] args, int index, Scanner scanner, String prompt) {
|
||||
if (args.length > index) {
|
||||
return args[index];
|
||||
}
|
||||
System.out.print(prompt);
|
||||
return scanner.nextLine().trim();
|
||||
}
|
||||
|
||||
public static String scaffold(String baseName, String interfaceId, String description, String group, String routingType, String moduleName, String author, String createDate, boolean register, String clientSystemCode) throws IOException {
|
||||
baseName = toPascalCase(baseName);
|
||||
String envSourceDir = System.getenv("AXHUB_SOURCE_DIR");
|
||||
Path rootDir = envSourceDir != null ? Paths.get(envSourceDir) : Paths.get(".");
|
||||
|
||||
Path usecaseDir = rootDir.resolve(Paths.get(moduleName, BASE_PACKAGE_PATH, "biz", group.toLowerCase(), "usecase"));
|
||||
Path usecaseImplDir = usecaseDir.resolve("impl");
|
||||
Path dtoDir = rootDir.resolve(Paths.get(moduleName, BASE_PACKAGE_PATH, "biz", group.toLowerCase(), "dto"));
|
||||
|
||||
Path legacyDtoDir = rootDir.resolve(Paths.get(moduleName, BASE_PACKAGE_PATH, "biz", group.toLowerCase(), "legacy"));
|
||||
Path converterDir = rootDir.resolve(Paths.get(moduleName, BASE_PACKAGE_PATH, "biz", group.toLowerCase(), "converter"));
|
||||
|
||||
String bizPackage = BASE_PACKAGE + ".biz." + group.toLowerCase();
|
||||
boolean isMci = "MCI".equalsIgnoreCase(routingType);
|
||||
String mciGroupPath = "infra/itrf/mci/" + group.toLowerCase();
|
||||
String clientPkgSuffix = "";
|
||||
String clientPrefixCap = "";
|
||||
Path mciClientDir = null;
|
||||
|
||||
if (isMci && clientSystemCode != null && clientSystemCode.length() == 4) {
|
||||
String clientPrefix = clientSystemCode.toLowerCase();
|
||||
clientPkgSuffix = clientPrefix.substring(0, 3) + "." + clientPrefix.substring(3, 4);
|
||||
clientPrefixCap = toPascalCase(clientSystemCode);
|
||||
mciGroupPath = "infra/itrf/mci/" + clientPrefix.substring(0, 3) + "/" + clientPrefix.substring(3, 4);
|
||||
mciClientDir = rootDir.resolve(Paths.get(moduleName, BASE_PACKAGE_PATH, mciGroupPath));
|
||||
}
|
||||
|
||||
Path mciIoDir = rootDir.resolve(Paths.get(moduleName, BASE_PACKAGE_PATH, mciGroupPath, "io"));
|
||||
|
||||
Files.createDirectories(usecaseDir);
|
||||
Files.createDirectories(usecaseImplDir);
|
||||
Files.createDirectories(dtoDir);
|
||||
if (isMci) {
|
||||
Files.createDirectories(mciIoDir);
|
||||
if (mciClientDir != null) {
|
||||
Files.createDirectories(mciClientDir);
|
||||
}
|
||||
} else {
|
||||
Files.createDirectories(legacyDtoDir);
|
||||
}
|
||||
Files.createDirectories(converterDir);
|
||||
|
||||
StringBuilder log = new StringBuilder();
|
||||
|
||||
// Generate Request DTO
|
||||
String reqContent = """
|
||||
package %s.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import io.shinhanlife.dap.lib.annotation.McpParameter;
|
||||
import io.shinhanlife.dap.lib.annotation.McpValidation;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* @package %s.dto
|
||||
* @className %sRequest
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author %s
|
||||
* @create %s
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* %s %s 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@Data
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public class %sRequest {
|
||||
@McpParameter(description = "수신자 전화번호", required = true)
|
||||
@McpValidation(pattern = "^01(?:0|1|[6-9])-(?:\\\\d{3}|\\\\d{4})-\\\\d{4}$", examples = {"010-1234-5678"})
|
||||
private String phoneNumber;
|
||||
|
||||
@McpParameter(description = "전송할 메시지 내용", required = true)
|
||||
@McpValidation(defaultValue = "안녕하세요.")
|
||||
private String message;
|
||||
}
|
||||
""".formatted(bizPackage, bizPackage, baseName, author, createDate, createDate, author, baseName);
|
||||
Files.writeString(dtoDir.resolve(baseName + "Request.java"), reqContent);
|
||||
|
||||
// Generate Response DTO
|
||||
String resContent = """
|
||||
package %s.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import io.shinhanlife.dap.lib.annotation.McpOutputSchema;
|
||||
import io.shinhanlife.dap.lib.annotation.McpValidation;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* @package %s.dto
|
||||
* @className %sResponse
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author %s
|
||||
* @create %s
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* %s %s 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@Data
|
||||
@McpOutputSchema
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public class %sResponse {
|
||||
@McpValidation(required = true)
|
||||
private String resultCode;
|
||||
|
||||
@McpValidation(maxLength = 200, nullable = true)
|
||||
private String resultMessage;
|
||||
|
||||
// TODO: Add response fields here. Do not include PII in the Tool response.
|
||||
}
|
||||
""".formatted(bizPackage, bizPackage, baseName, author, createDate, createDate, author, baseName);
|
||||
Files.writeString(dtoDir.resolve(baseName + "Response.java"), resContent);
|
||||
|
||||
String toolName = toToolName(moduleName, group, baseName);
|
||||
|
||||
String serviceInterfaceContent = """
|
||||
package %s.usecase;
|
||||
|
||||
import io.shinhanlife.dap.lib.annotation.McpFunction;
|
||||
import io.shinhanlife.dap.lib.annotation.McpTool;
|
||||
import %s.dto.%sRequest;
|
||||
|
||||
@McpTool(
|
||||
routingType = "%s",
|
||||
categoryKey = "%s"
|
||||
)
|
||||
public interface %sUseCase {
|
||||
@McpFunction(
|
||||
displayName = "%s 툴",
|
||||
name = "%s",
|
||||
description = "%s",
|
||||
prompt = "%s",
|
||||
mappingId = "%s",
|
||||
register = %s,
|
||||
requiresApproval = false,
|
||||
openWorldHint = true,
|
||||
version = "1.0.0",
|
||||
timeoutMillis = 300000L,
|
||||
enabled = true
|
||||
)
|
||||
Object execute(%sRequest req);
|
||||
}
|
||||
""".formatted(
|
||||
bizPackage,
|
||||
bizPackage, baseName,
|
||||
routingType, group.toLowerCase(),
|
||||
baseName,
|
||||
baseName, toolName, description, description + " ?줘.", interfaceId, register,
|
||||
baseName
|
||||
);
|
||||
|
||||
Files.writeString(usecaseDir.resolve(baseName + "UseCase.java"), serviceInterfaceContent);
|
||||
|
||||
String serviceImplContent;
|
||||
|
||||
if (isMci) {
|
||||
serviceImplContent = """
|
||||
package %s.usecase.impl;
|
||||
|
||||
import %s.dto.%sRequest;
|
||||
import %s.dto.%sResponse;
|
||||
import %s.usecase.%sUseCase;
|
||||
import io.shinhanlife.dap.lib.integration.mci.component.AxhubMciComponent;
|
||||
import io.shinhanlife.glow.communication.dto.Transfer;
|
||||
import org.springframework.stereotype.Service;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import java.util.Map;
|
||||
import %s.converter.%sConverter;
|
||||
import %s.%s.io.%s_I;
|
||||
%s
|
||||
|
||||
/**
|
||||
* @package %s.usecase.impl
|
||||
* @className %sUseCaseImpl
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author %s
|
||||
* @create %s
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* %s %s 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class %sUseCaseImpl implements %sUseCase {
|
||||
|
||||
%s
|
||||
private final %sConverter converter;
|
||||
|
||||
@Override
|
||||
public Object execute(%sRequest req) {
|
||||
log.info("[MCI Tool] {} 요청 수신.", "%s");
|
||||
try {
|
||||
// MapStruct를 이용한 자동 매핑 (AI DTO -> MCI DTO)
|
||||
%s_I mciReq = converter.toLegacyRequest(req);
|
||||
|
||||
Transfer<Object> resTransfer = mci.callTo(
|
||||
"%s",
|
||||
null,
|
||||
mciReq,
|
||||
Object.class
|
||||
);
|
||||
return resTransfer.getBody() != null ? resTransfer.getBody() : Map.of("status", "SUCCESS");
|
||||
} catch (Exception e) {
|
||||
log.error("[MCI Tool] 연동 중 오류 발생: {}", e.getMessage(), e);
|
||||
return Map.of("status", "ERROR", "message", e.getMessage() != null ? e.getMessage() : "Unknown error");
|
||||
}
|
||||
}
|
||||
}
|
||||
""".formatted(
|
||||
bizPackage,
|
||||
bizPackage, baseName,
|
||||
bizPackage, baseName,
|
||||
bizPackage, baseName,
|
||||
bizPackage, baseName,
|
||||
BASE_PACKAGE, mciGroupPath.replace("/", "."), interfaceId,
|
||||
(clientPrefixCap.isEmpty() ? "" : "import " + BASE_PACKAGE + "." + mciGroupPath.replace("/", ".") + ".Mci" + clientPrefixCap + "Client;\n"),
|
||||
bizPackage,
|
||||
baseName,
|
||||
author,
|
||||
createDate,
|
||||
createDate, author,
|
||||
baseName,
|
||||
baseName,
|
||||
(clientPrefixCap.isEmpty() ? "private final AxhubMciComponent mci;" : "private final Mci" + clientPrefixCap + "Client mci;"),
|
||||
baseName,
|
||||
baseName,
|
||||
toolName,
|
||||
interfaceId,
|
||||
interfaceId
|
||||
);
|
||||
} else {
|
||||
serviceImplContent = """
|
||||
package %s.usecase.impl;
|
||||
|
||||
import %s.dto.%sRequest;
|
||||
import %s.dto.%sResponse;
|
||||
import %s.usecase.%sUseCase;
|
||||
import io.shinhanlife.dap.lib.usecase.AbstractMcpToolUseCase;
|
||||
import %s.converter.%sConverter;
|
||||
import org.springframework.stereotype.Service;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* @package %s.usecase.impl
|
||||
* @className %sUseCaseImpl
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author %s
|
||||
* @create %s
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* %s %s 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class %sUseCaseImpl extends AbstractMcpToolUseCase implements %sUseCase {
|
||||
|
||||
private final %sConverter converter;
|
||||
|
||||
@Override
|
||||
public Object execute(%sRequest req) {
|
||||
// %sLegacyRequest legacyRequest = converter.toLegacyRequest(req);
|
||||
return executeLegacy("%s", "%s", req); // Or pass legacyRequest
|
||||
}
|
||||
}
|
||||
""".formatted(
|
||||
bizPackage,
|
||||
bizPackage, baseName,
|
||||
bizPackage, baseName,
|
||||
bizPackage, baseName,
|
||||
bizPackage, baseName,
|
||||
bizPackage, baseName,
|
||||
author,
|
||||
createDate,
|
||||
createDate, author,
|
||||
baseName, baseName,
|
||||
baseName,
|
||||
baseName,
|
||||
baseName,
|
||||
routingType, interfaceId
|
||||
);
|
||||
}
|
||||
|
||||
Files.writeString(usecaseImplDir.resolve(baseName + "UseCaseImpl.java"), serviceImplContent);
|
||||
|
||||
if (isMci) {
|
||||
String mciReqContent = """
|
||||
package %s.%s.io;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* @package %s.%s.io
|
||||
* @className %s_I
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author %s
|
||||
* @create %s
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* %s %s 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@Data
|
||||
public class %s_I {
|
||||
/**
|
||||
* EAI 시스템이 요구하는 수신자 번호 파라미터명
|
||||
*/
|
||||
private String phone;
|
||||
|
||||
/**
|
||||
* EAI 시스템이 요구하는 메시지 내용 파라미터명
|
||||
*/
|
||||
private String content;
|
||||
}
|
||||
""".formatted(BASE_PACKAGE, mciGroupPath.replace("/", "."), BASE_PACKAGE, mciGroupPath.replace("/", "."), interfaceId, author, createDate, createDate, author, interfaceId);
|
||||
Files.writeString(mciIoDir.resolve(interfaceId + "_I.java"), mciReqContent);
|
||||
|
||||
String mciResContent = """
|
||||
package %s.%s.io;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* @package %s.%s.io
|
||||
* @className %s_O
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author %s
|
||||
* @create %s
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* %s %s 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@Data
|
||||
public class %s_O {
|
||||
// TODO: Add response fields here
|
||||
}
|
||||
""".formatted(BASE_PACKAGE, mciGroupPath.replace("/", "."), BASE_PACKAGE, mciGroupPath.replace("/", "."), interfaceId, author, createDate, createDate, author, interfaceId);
|
||||
Files.writeString(mciIoDir.resolve(interfaceId + "_O.java"), mciResContent);
|
||||
|
||||
String converterContent = """
|
||||
package %s.converter;
|
||||
|
||||
import %s.dto.%sRequest;
|
||||
import %s.dto.%sResponse;
|
||||
import %s.%s.io.%s_I;
|
||||
import %s.%s.io.%s_O;
|
||||
import org.mapstruct.Mapper;
|
||||
import org.mapstruct.Mapping;
|
||||
import org.mapstruct.factory.Mappers;
|
||||
|
||||
/**
|
||||
* @package %s.converter
|
||||
* @className %sConverter
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author %s
|
||||
* @create %s
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* %s %s 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@Mapper(componentModel = "spring")
|
||||
public interface %sConverter {
|
||||
|
||||
@Mapping(source = "phoneNumber", target = "phone")
|
||||
@Mapping(source = "message", target = "content")
|
||||
%s_I toLegacyRequest(%sRequest req);
|
||||
|
||||
@Mapping(source = "phone", target = "phoneNumber")
|
||||
@Mapping(source = "content", target = "message")
|
||||
%sRequest toRequest(%s_I mciReq);
|
||||
|
||||
// %sResponse toResponse(%s_O mciRes);
|
||||
}
|
||||
""".formatted(
|
||||
bizPackage,
|
||||
bizPackage, baseName,
|
||||
bizPackage, baseName,
|
||||
BASE_PACKAGE, mciGroupPath.replace("/", "."), interfaceId,
|
||||
BASE_PACKAGE, mciGroupPath.replace("/", "."), interfaceId,
|
||||
bizPackage, baseName, author, createDate, createDate, author,
|
||||
baseName, interfaceId, baseName,
|
||||
baseName, interfaceId,
|
||||
baseName, interfaceId
|
||||
);
|
||||
Files.writeString(converterDir.resolve(baseName + "Converter.java"), converterContent);
|
||||
|
||||
log.append("\n=========================================\n");
|
||||
log.append(" Scaffolding Complete! (Routing: " + routingType + ")\n");
|
||||
log.append("=========================================\n");
|
||||
log.append("[Usecase Interface] ").append(usecaseDir.resolve(baseName + "UseCase.java")).append("\n");
|
||||
log.append("[Usecase Impl] ").append(usecaseImplDir.resolve(baseName + "UseCaseImpl.java")).append("\n");
|
||||
log.append("[Request DTO] ").append(dtoDir.resolve(baseName + "Request.java")).append("\n");
|
||||
log.append("[Response DTO] ").append(dtoDir.resolve(baseName + "Response.java")).append("\n");
|
||||
log.append("[MCI Request IO] ").append(mciIoDir.resolve(interfaceId + "_I.java")).append("\n");
|
||||
log.append("[MCI Response IO] ").append(mciIoDir.resolve(interfaceId + "_O.java")).append("\n");
|
||||
log.append("[MCI Converter] ").append(converterDir.resolve(baseName + "Converter.java")).append("\n");
|
||||
|
||||
if (!clientPrefixCap.isEmpty()) {
|
||||
String mciClientContent = """
|
||||
package %s.%s;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import io.shinhanlife.dap.lib.integration.mci.component.AxhubMciComponent;
|
||||
import io.shinhanlife.glow.communication.dto.Transfer;
|
||||
|
||||
/**
|
||||
* @package %s.%s
|
||||
* @className Mci%sClient
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author %s
|
||||
* @create %s
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* %s %s 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class Mci%sClient {
|
||||
private final AxhubMciComponent mci;
|
||||
|
||||
public Transfer<Object> callTo(String interfaceId, String dummy, Object mciReq, Class<Object> resType) throws Exception {
|
||||
return mci.callTo(interfaceId, dummy, mciReq, resType);
|
||||
}
|
||||
}
|
||||
""".formatted(
|
||||
BASE_PACKAGE, mciGroupPath.replace("/", "."),
|
||||
BASE_PACKAGE, mciGroupPath.replace("/", "."), clientPrefixCap, author, createDate, createDate, author, clientPrefixCap
|
||||
);
|
||||
Files.writeString(mciClientDir.resolve("Mci" + clientPrefixCap + "Client.java"), mciClientContent);
|
||||
log.append("[MCI Client] ").append(mciClientDir.resolve("Mci" + clientPrefixCap + "Client.java")).append("\n");
|
||||
}
|
||||
|
||||
} else {
|
||||
String legacyReqContent = """
|
||||
package %s.legacy;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* @package %s.legacy
|
||||
* @className %sLegacyRequest
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author %s
|
||||
* @create %s
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* %s %s 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@Data
|
||||
public class %sLegacyRequest {
|
||||
/**
|
||||
* EAI 시스템이 요구하는 수신자 번호 파라미터명
|
||||
*/
|
||||
private String phone;
|
||||
|
||||
/**
|
||||
* EAI 시스템이 요구하는 메시지 내용 파라미터명
|
||||
*/
|
||||
private String content;
|
||||
}
|
||||
""".formatted(bizPackage, bizPackage, baseName, author, createDate, createDate, author, baseName);
|
||||
Files.writeString(legacyDtoDir.resolve(baseName + "LegacyRequest.java"), legacyReqContent);
|
||||
|
||||
String legacyResContent = """
|
||||
package %s.legacy;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* @package %s.legacy
|
||||
* @className %sLegacyResponse
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author %s
|
||||
* @create %s
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* %s %s 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@Data
|
||||
public class %sLegacyResponse {
|
||||
// TODO: Add legacy response fields here
|
||||
}
|
||||
""".formatted(bizPackage, bizPackage, baseName, author, createDate, createDate, author, baseName);
|
||||
Files.writeString(legacyDtoDir.resolve(baseName + "LegacyResponse.java"), legacyResContent);
|
||||
|
||||
String converterContent = """
|
||||
package %s.converter;
|
||||
|
||||
import %s.dto.%sRequest;
|
||||
import %s.dto.%sResponse;
|
||||
import %s.legacy.%sLegacyRequest;
|
||||
import %s.legacy.%sLegacyResponse;
|
||||
import org.mapstruct.Mapper;
|
||||
import org.mapstruct.Mapping;
|
||||
import org.mapstruct.factory.Mappers;
|
||||
|
||||
/**
|
||||
* @package %s.converter
|
||||
* @className %sConverter
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author %s
|
||||
* @create %s
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* %s %s 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@Mapper(componentModel = "spring")
|
||||
public interface %sConverter {
|
||||
|
||||
@Mapping(source = "phoneNumber", target = "phone")
|
||||
@Mapping(source = "message", target = "content")
|
||||
%sLegacyRequest toLegacyRequest(%sRequest req);
|
||||
|
||||
@Mapping(source = "phone", target = "phoneNumber")
|
||||
@Mapping(source = "content", target = "message")
|
||||
%sRequest toRequest(%sLegacyRequest legacyRequest);
|
||||
|
||||
// %sResponse toResponse(%sLegacyResponse legacyResponse);
|
||||
}
|
||||
""".formatted(
|
||||
bizPackage,
|
||||
bizPackage, baseName,
|
||||
bizPackage, baseName,
|
||||
bizPackage, baseName,
|
||||
bizPackage, baseName,
|
||||
bizPackage, baseName, author, createDate, createDate, author,
|
||||
baseName, baseName, baseName, baseName, baseName, baseName, baseName
|
||||
);
|
||||
Files.writeString(converterDir.resolve(baseName + "Converter.java"), converterContent);
|
||||
|
||||
log.append("\n=========================================\n");
|
||||
log.append(" Scaffolding Complete! (Routing: " + routingType + ")\n");
|
||||
log.append("=========================================\n");
|
||||
log.append("[Usecase Interface] ").append(usecaseDir.resolve(baseName + "UseCase.java")).append("\n");
|
||||
log.append("[Usecase Impl] ").append(usecaseImplDir.resolve(baseName + "UseCaseImpl.java")).append("\n");
|
||||
log.append("[Request DTO] ").append(dtoDir.resolve(baseName + "Request.java")).append("\n");
|
||||
log.append("[Response DTO] ").append(dtoDir.resolve(baseName + "Response.java")).append("\n");
|
||||
log.append("[Legacy Request DTO] ").append(legacyDtoDir.resolve(baseName + "LegacyRequest.java")).append("\n");
|
||||
log.append("[Legacy Response DTO] ").append(legacyDtoDir.resolve(baseName + "LegacyResponse.java")).append("\n");
|
||||
log.append("[Legacy Converter] ").append(converterDir.resolve(baseName + "Converter.java")).append("\n");
|
||||
}
|
||||
log.append("\n Tip: ").append(interfaceId).append(" 목업 데이터를 mock-responses.json에 추가하세요.\n");
|
||||
|
||||
return log.toString();
|
||||
}
|
||||
|
||||
private static String toToolName(String moduleName, String group, String baseName) {
|
||||
String pod = moduleName.startsWith("dap-tool-")
|
||||
? moduleName.substring("dap-tool-".length())
|
||||
: "oth";
|
||||
String normalizedName = baseName.replaceAll("([a-z0-9])([A-Z])", "$1 $2")
|
||||
.toLowerCase(Locale.ROOT)
|
||||
.replaceAll("[^a-z0-9]+", " ")
|
||||
.trim();
|
||||
String[] words = normalizedName.split("\\s+");
|
||||
String service = words[0];
|
||||
String action = words.length == 1 ? "execute" : words[words.length - 1];
|
||||
return "%s.%s.%s.%s".formatted(
|
||||
pod.toLowerCase(Locale.ROOT),
|
||||
group.toLowerCase(Locale.ROOT),
|
||||
service,
|
||||
action);
|
||||
}
|
||||
|
||||
private static String toPascalCase(String str) {
|
||||
if (str == null || str.isEmpty()) {
|
||||
return str;
|
||||
}
|
||||
StringBuilder result = new StringBuilder();
|
||||
boolean capitalizeNext = true;
|
||||
for (char c : str.toCharArray()) {
|
||||
if (c == '_' || c == '-' || c == ' ') {
|
||||
capitalizeNext = true;
|
||||
} else if (capitalizeNext) {
|
||||
result.append(Character.toUpperCase(c));
|
||||
capitalizeNext = false;
|
||||
} else {
|
||||
result.append(c);
|
||||
}
|
||||
}
|
||||
if (result.length() > 0) {
|
||||
result.setCharAt(0, Character.toUpperCase(result.charAt(0)));
|
||||
}
|
||||
return result.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package io.shinhanlife.dap.lib.util;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import io.shinhanlife.dap.lib.annotation.McpFunction;
|
||||
import io.shinhanlife.dap.lib.annotation.McpOutputSchema;import java.io.InputStream;
|
||||
import java.util.Map;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
|
||||
/** Resolves an MCP Tool input schema from resource, inline value, or DTO metadata. */
|
||||
public class ToolSchemaResolver {
|
||||
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
public ToolSchemaResolver(ObjectMapper objectMapper) {
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
public Map<String, Object> resolve(McpFunction function, Class<?> requestType) {
|
||||
if (function != null && !function.inputSchemaResource().isBlank()) {
|
||||
return loadResource(function.inputSchemaResource());
|
||||
}
|
||||
if (function != null && !function.inputSchema().isBlank()
|
||||
&& !"{}".equals(function.inputSchema().trim())) {
|
||||
return parse(function.inputSchema(), "McpFunction.inputSchema");
|
||||
}
|
||||
return JsonSchemaGenerator.generateSchema(requestType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves an explicitly declared response schema.
|
||||
* Response schemas are opt-in so existing tools keep their current response behavior.
|
||||
*/
|
||||
public Map<String, Object> resolveOutput(McpFunction function, Class<?> responseType) {
|
||||
if (function != null && !function.outputSchemaResource().isBlank()) {
|
||||
return loadResource(function.outputSchemaResource());
|
||||
}
|
||||
if (function != null && !function.outputSchema().isBlank()
|
||||
&& !"{}".equals(function.outputSchema().trim())) {
|
||||
return parse(function.outputSchema(), "McpFunction.outputSchema");
|
||||
}
|
||||
if (responseType != null && responseType.isAnnotationPresent(McpOutputSchema.class)) {
|
||||
return JsonSchemaGenerator.generateSchema(responseType);
|
||||
}
|
||||
return Map.of();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retained for callers that use only explicit output schemas.
|
||||
*/
|
||||
public Map<String, Object> resolveOutput(McpFunction function) {
|
||||
return resolveOutput(function, null);
|
||||
}
|
||||
|
||||
private Map<String, Object> loadResource(String location) {
|
||||
String path = location.startsWith("classpath:")
|
||||
? location.substring("classpath:".length())
|
||||
: location;
|
||||
ClassPathResource resource = new ClassPathResource(path);
|
||||
if (!resource.exists()) {
|
||||
throw new IllegalStateException("MCP schema resource not found: " + location);
|
||||
}
|
||||
|
||||
try (InputStream inputStream = resource.getInputStream()) {
|
||||
return objectMapper.readValue(inputStream, new TypeReference<>() { });
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException("Failed to load MCP schema resource: " + location, e);
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String, Object> parse(String schema, String source) {
|
||||
try {
|
||||
return objectMapper.readValue(schema, new TypeReference<>() { });
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException("Failed to parse MCP input schema from " + source, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
package io.shinhanlife.dap.lib.util;
|
||||
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.lib.util
|
||||
* @className ToolSourceUpdater
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public class ToolSourceUpdater {
|
||||
|
||||
public static void updateToolSource(String toolName, String domainGroup, String description, boolean register, Boolean requiresApproval) throws Exception {
|
||||
// 1. Find all *UseCase.java files in dap-tool-* directories
|
||||
String envSourceDir = System.getenv("AXHUB_SOURCE_DIR");
|
||||
Path rootDir = envSourceDir != null ? Paths.get(envSourceDir) : Paths.get(".");
|
||||
|
||||
List<Path> javaFiles;
|
||||
try (Stream<Path> paths = Files.walk(rootDir)) {
|
||||
javaFiles = paths
|
||||
.filter(Files::isRegularFile)
|
||||
.filter(p -> p.toString().endsWith("UseCase.java"))
|
||||
.filter(p -> p.toString().contains("dap-tool-") || p.toString().contains("axhub-tool-"))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
Path targetFile = null;
|
||||
String content = null;
|
||||
|
||||
// 2. Find the specific file for the tool
|
||||
String functionName = toolName;
|
||||
if (toolName.contains("_")) {
|
||||
functionName = toolName.substring(toolName.indexOf("_") + 1);
|
||||
}
|
||||
|
||||
Pattern namePattern = Pattern.compile("@McpFunction\\s*\\([^)]*name\\s*=\\s*\"" + Pattern.quote(toolName) + "\"", Pattern.DOTALL);
|
||||
Pattern namePattern2 = Pattern.compile("@McpFunction\\s*\\([^)]*name\\s*=\\s*\"" + Pattern.quote(functionName) + "\"", Pattern.DOTALL);
|
||||
|
||||
for (Path path : javaFiles) {
|
||||
String text = Files.readString(path);
|
||||
if (namePattern.matcher(text).find()) {
|
||||
targetFile = path;
|
||||
content = text;
|
||||
break;
|
||||
} else if (namePattern2.matcher(text).find()) {
|
||||
targetFile = path;
|
||||
content = text;
|
||||
toolName = functionName; // Use baseName for subsequent replacements
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (targetFile == null) {
|
||||
throw new Exception("소스 코드를 찾을 수 없습니다: " + toolName);
|
||||
}
|
||||
|
||||
// 3. Update @McpTool group
|
||||
if (domainGroup != null && !domainGroup.trim().isEmpty()) {
|
||||
Pattern groupPattern = Pattern.compile("(@McpTool\\s*\\([^)]*group\\s*=\\s*\")([^\"]+)(\")", Pattern.DOTALL);
|
||||
Matcher groupMatcher = groupPattern.matcher(content);
|
||||
if (groupMatcher.find()) {
|
||||
content = groupMatcher.replaceFirst("$1" + domainGroup + "$3");
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Update @McpFunction description
|
||||
if (description != null) {
|
||||
Pattern funcPattern = Pattern.compile("(@McpFunction\\s*\\([^)]*name\\s*=\\s*\"" + Pattern.quote(toolName) + "\"[^)]*description\\s*=\\s*\")([^\"]+)(\")", Pattern.DOTALL);
|
||||
Matcher funcMatcher = funcPattern.matcher(content);
|
||||
if (funcMatcher.find()) {
|
||||
content = funcMatcher.replaceFirst("$1" + description.replace("\\", "\\\\").replace("$", "\\\\$") + "$3");
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Update register flag
|
||||
Pattern regPattern = Pattern.compile("(@McpFunction\\s*\\([^)]*name\\s*=\\s*\"" + Pattern.quote(toolName) + "\"[^)]*register\\s*=\\s*)(true|false)([^a-zA-Z0-9])", Pattern.DOTALL);
|
||||
Matcher regMatcher = regPattern.matcher(content);
|
||||
if (regMatcher.find()) {
|
||||
content = regMatcher.replaceFirst("$1" + register + "$3");
|
||||
} else {
|
||||
Pattern addRegPattern = Pattern.compile("(@McpFunction\\s*\\([^)]*name\\s*=\\s*\"" + Pattern.quote(toolName) + "\")", Pattern.DOTALL);
|
||||
Matcher addRegMatcher = addRegPattern.matcher(content);
|
||||
if (addRegMatcher.find()) {
|
||||
content = addRegMatcher.replaceFirst("$1, register = " + register);
|
||||
}
|
||||
}
|
||||
|
||||
// 5.5 Update requiresApproval flag
|
||||
if (requiresApproval != null) {
|
||||
Pattern appPattern = Pattern.compile("(@McpFunction\\s*\\([^)]*name\\s*=\\s*\"" + Pattern.quote(toolName) + "\"[^)]*requiresApproval\\s*=\\s*)(true|false)([^a-zA-Z0-9])", Pattern.DOTALL);
|
||||
Matcher appMatcher = appPattern.matcher(content);
|
||||
if (appMatcher.find()) {
|
||||
content = appMatcher.replaceFirst("$1" + requiresApproval + "$3");
|
||||
} else {
|
||||
Pattern addAppPattern = Pattern.compile("(@McpFunction\\s*\\([^)]*name\\s*=\\s*\"" + Pattern.quote(toolName) + "\")", Pattern.DOTALL);
|
||||
Matcher addAppMatcher = addAppPattern.matcher(content);
|
||||
if (addAppMatcher.find()) {
|
||||
content = addAppMatcher.replaceFirst("$1, requiresApproval = " + requiresApproval);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 6. Write back to file
|
||||
Files.writeString(targetFile, content);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package io.shinhanlife.dap.lib.validation;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
||||
/** Gradle entry point for validating unique MCP Tool names before packaging. */
|
||||
public final class McpToolNameValidationRunner {
|
||||
|
||||
private McpToolNameValidationRunner() {
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
if (args.length != 1) {
|
||||
throw new IllegalArgumentException("Usage: McpToolNameValidationRunner <project-root>");
|
||||
}
|
||||
validate(Path.of(args[0]));
|
||||
}
|
||||
|
||||
static void validate(Path projectRoot) {
|
||||
McpToolNameValidator.assertUnique(projectRoot);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
package io.shinhanlife.dap.lib.validation;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.lib.validation
|
||||
* @className McpToolNameValidator
|
||||
* @description Validates unique MCP function names across tool modules
|
||||
* @author 0986406
|
||||
* @create 2026.07.27
|
||||
* <pre>
|
||||
* ---------- revision history ----------
|
||||
* date author description
|
||||
* ---------- --------- ---------------------------
|
||||
* 2026.07.27 0986406 initial creation
|
||||
* </pre>
|
||||
*/
|
||||
public final class McpToolNameValidator {
|
||||
|
||||
private static final Pattern TOOL_NAME_PATTERN = Pattern.compile("\\bname\\s*=\\s*\\\"([^\\\"]+)\\\"");
|
||||
private static final Pattern TOOL_NAME_CONVENTION = Pattern.compile("^[a-z][a-z0-9-]*\\.[a-z][a-z0-9-]*\\.[a-z][a-z0-9-]*\\.[a-z][a-z0-9-]*$");
|
||||
|
||||
private McpToolNameValidator() {
|
||||
}
|
||||
|
||||
public static void assertUnique(Path projectRoot) {
|
||||
Map<String, List<ToolDeclaration>> declarationsByName = new LinkedHashMap<>();
|
||||
|
||||
try (var modules = Files.list(projectRoot)) {
|
||||
modules.filter(Files::isDirectory)
|
||||
.filter(path -> path.getFileName().toString().startsWith("dap-tool-"))
|
||||
.filter(path -> !path.getFileName().toString().equals("dap-was-lib"))
|
||||
.sorted()
|
||||
.forEach(module -> collectDeclarations(module, declarationsByName));
|
||||
} catch (IOException exception) {
|
||||
throw new UncheckedIOException("Failed to scan MCP tool modules", exception);
|
||||
}
|
||||
|
||||
List<Map.Entry<String, List<ToolDeclaration>>> invalidNames = declarationsByName.entrySet().stream()
|
||||
.filter(entry -> !TOOL_NAME_CONVENTION.matcher(entry.getKey()).matches())
|
||||
.sorted(Map.Entry.comparingByKey())
|
||||
.toList();
|
||||
|
||||
if (!invalidNames.isEmpty()) {
|
||||
throw new IllegalStateException(buildInvalidNameMessage(invalidNames));
|
||||
}
|
||||
List<Map.Entry<String, List<ToolDeclaration>>> duplicates = declarationsByName.entrySet().stream()
|
||||
.filter(entry -> entry.getValue().size() > 1)
|
||||
.sorted(Map.Entry.comparingByKey())
|
||||
.toList();
|
||||
|
||||
if (!duplicates.isEmpty()) {
|
||||
throw new IllegalStateException(buildDuplicateMessage(duplicates));
|
||||
}
|
||||
}
|
||||
|
||||
private static void collectDeclarations(Path module, Map<String, List<ToolDeclaration>> declarationsByName) {
|
||||
Path sourceDirectory = module.resolve("src/main/java");
|
||||
if (!Files.isDirectory(sourceDirectory)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try (var sources = Files.walk(sourceDirectory)) {
|
||||
sources.filter(path -> path.toString().endsWith(".java"))
|
||||
.sorted()
|
||||
.forEach(source -> collectDeclarations(module.getFileName().toString(), source, declarationsByName));
|
||||
} catch (IOException exception) {
|
||||
throw new UncheckedIOException("Failed to scan module " + module.getFileName(), exception);
|
||||
}
|
||||
}
|
||||
|
||||
private static void collectDeclarations(String moduleName, Path source,
|
||||
Map<String, List<ToolDeclaration>> declarationsByName) {
|
||||
String content;
|
||||
try {
|
||||
content = Files.readString(source);
|
||||
} catch (IOException exception) {
|
||||
throw new UncheckedIOException("Failed to read " + source, exception);
|
||||
}
|
||||
|
||||
int annotationOffset = content.indexOf("@McpFunction");
|
||||
while (annotationOffset >= 0) {
|
||||
int openingParenthesis = content.indexOf('(', annotationOffset);
|
||||
int closingParenthesis = findAnnotationEnd(content, openingParenthesis);
|
||||
if (openingParenthesis < 0 || closingParenthesis < 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
Matcher matcher = TOOL_NAME_PATTERN.matcher(content.substring(openingParenthesis + 1, closingParenthesis));
|
||||
if (matcher.find()) {
|
||||
String toolName = matcher.group(1);
|
||||
int line = 1 + (int) content.substring(0, annotationOffset).chars().filter(character -> character == '\n').count();
|
||||
declarationsByName.computeIfAbsent(toolName, ignored -> new ArrayList<>())
|
||||
.add(new ToolDeclaration(moduleName, source, line));
|
||||
}
|
||||
annotationOffset = content.indexOf("@McpFunction", closingParenthesis + 1);
|
||||
}
|
||||
}
|
||||
|
||||
private static int findAnnotationEnd(String content, int openingParenthesis) {
|
||||
if (openingParenthesis < 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
int depth = 0;
|
||||
boolean inString = false;
|
||||
boolean escaped = false;
|
||||
for (int index = openingParenthesis; index < content.length(); index++) {
|
||||
char character = content.charAt(index);
|
||||
if (inString) {
|
||||
if (escaped) {
|
||||
escaped = false;
|
||||
} else if (character == '\\') {
|
||||
escaped = true;
|
||||
} else if (character == '\"') {
|
||||
inString = false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (character == '\"') {
|
||||
inString = true;
|
||||
} else if (character == '(') {
|
||||
depth++;
|
||||
} else if (character == ')' && --depth == 0) {
|
||||
return index;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
private static String buildInvalidNameMessage(List<Map.Entry<String, List<ToolDeclaration>>> invalidNames) {
|
||||
StringBuilder message = new StringBuilder("Invalid MCP tool name(s): expected pod.domain.service.action using lowercase letters, digits, or hyphens.");
|
||||
for (Map.Entry<String, List<ToolDeclaration>> invalidName : invalidNames) {
|
||||
message.append("\n\n").append(invalidName.getKey());
|
||||
invalidName.getValue().stream()
|
||||
.sorted(Comparator.comparing(ToolDeclaration::moduleName).thenComparing(declaration -> declaration.source().toString()))
|
||||
.forEach(declaration -> message.append("\n- ")
|
||||
.append(declaration.moduleName())
|
||||
.append(": ")
|
||||
.append(declaration.source())
|
||||
.append(':').append(declaration.line()));
|
||||
}
|
||||
return message.toString();
|
||||
}
|
||||
|
||||
private static String buildDuplicateMessage(List<Map.Entry<String, List<ToolDeclaration>>> duplicates) {
|
||||
StringBuilder message = new StringBuilder("Duplicate MCP tool name(s):");
|
||||
for (Map.Entry<String, List<ToolDeclaration>> duplicate : duplicates) {
|
||||
message.append("\n\n").append(duplicate.getKey());
|
||||
duplicate.getValue().stream()
|
||||
.sorted(Comparator.comparing(ToolDeclaration::moduleName).thenComparing(declaration -> declaration.source().toString()))
|
||||
.forEach(declaration -> message.append("\n- ")
|
||||
.append(declaration.moduleName())
|
||||
.append(": ")
|
||||
.append(declaration.source())
|
||||
.append(':').append(declaration.line()));
|
||||
}
|
||||
return message.toString();
|
||||
}
|
||||
|
||||
private record ToolDeclaration(String moduleName, Path source, int line) {
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user