refactor: 어플리케이션 업무코드(DAP) 전면 전환에 따른 패키지 및 모듈명 대규모 리팩토링

This commit is contained in:
jade
2026-07-16 16:18:04 +09:00
parent 6159b0856a
commit a82bc1a196
285 changed files with 782 additions and 782 deletions

35
dap-common/build.gradle Normal file
View File

@@ -0,0 +1,35 @@
plugins {
id 'java-library'
}
dependencies {
api 'org.springframework.boot:spring-boot-starter-web'
api 'org.springframework.boot:spring-boot-starter-validation'
api 'org.springframework.boot:spring-boot-starter-data-redis'
api 'io.github.resilience4j:resilience4j-spring-boot3:2.2.0'
api 'io.github.resilience4j:resilience4j-core:2.2.0'
api 'io.github.resilience4j:resilience4j-circuitbreaker:2.2.0'
api 'io.github.resilience4j:resilience4j-ratelimiter'
api 'io.github.resilience4j:resilience4j-retry:2.2.0'
api 'org.springframework.boot:spring-boot-starter-aop:3.3.0'
api 'org.springframework.boot:spring-boot-starter-jdbc'
api 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.3'
api 'com.h2database:h2'
api 'p6spy:p6spy:3.9.1'
api 'io.lettuce:lettuce-core:6.6.0.RELEASE'
api "org.mapstruct:mapstruct:1.5.5.Final"
api 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.17.1'
api 'com.fasterxml.jackson.core:jackson-databind:2.17.1'
api 'com.networknt:json-schema-validator:1.4.0'
api 'org.springframework.kafka:spring-kafka:3.2.0'
api 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.5.0'
}
dependencies {
compileOnly 'org.projectlombok:lombok:1.18.32'
annotationProcessor 'org.projectlombok:lombok:1.18.32'
}

View File

@@ -0,0 +1,32 @@
package io.shinhanlife.dap.biz.mcp.adapter.dto;
import lombok.Builder;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
/**
* @package io.shinhanlife.dap.biz.mcp.adapter.dto
* @className ErrorDetail
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
@Getter
@Setter
@AllArgsConstructor
@Builder
@NoArgsConstructor
public class ErrorDetail {
private int code;
private String message;
}

View File

@@ -0,0 +1,27 @@
package io.shinhanlife.dap.biz.mcp.adapter.dto;
import lombok.Getter;
import lombok.Setter;
/**
* @package io.shinhanlife.dap.biz.mcp.adapter.dto
* @className JsonRpcRequest
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
@Getter
@Setter
public class JsonRpcRequest {
private String jsonrpc;
private String method;
private Params params;
private String id;
}

View File

@@ -0,0 +1,30 @@
package io.shinhanlife.dap.biz.mcp.adapter.dto;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
import lombok.Getter;
import lombok.Setter;
// 2. 응답 DTO
/**
* @package io.shinhanlife.dap.biz.mcp.adapter.dto
* @className JsonRpcResponse
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </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;
}

View File

@@ -0,0 +1,38 @@
package io.shinhanlife.dap.biz.mcp.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.biz.mcp.adapter.dto
* @className Params
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </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;
}

View File

@@ -0,0 +1,87 @@
package io.shinhanlife.dap.biz.mcp.gateway.dto;
import lombok.Getter;
import lombok.Setter;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Map;
/**
* Tool(Agent)의 명세 및 라우팅 정보를 담고 있는 메타데이터 클래스
* Redis 레지스트리에 저장되며, Planner와 Router 간의 통신 객체(Plan)로 사용됩니다.
*/
/**
* @package io.shinhanlife.dap.biz.mcp.gateway.dto
* @className ToolMetadata
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </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;
// 2-5. Redis 등록 여부 (UI 표출용)
@Builder.Default
private Boolean isRegistered = true;
// 2-6. HITL 승인 필요 여부
@Builder.Default
private Boolean requiresApproval = 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초당 허용 최대 요청 수
}

View File

@@ -0,0 +1,33 @@
package io.shinhanlife.dap.common.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.common.config
* @className CorsConfig
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </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("*") // 모든 헤더 허용
.allowCredentials(true) // 쿠키/인증 정보 허용
.maxAge(3600); // preflight 요청 캐시 시간 (초 단위)
}
}

View File

@@ -0,0 +1,33 @@
package io.shinhanlife.dap.common.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.common.config
* @className MybatisConfig
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
@Configuration
public class MybatisConfig {
@Bean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
sessionFactory.setDataSource(dataSource);
return sessionFactory.getObject();
}
}

View File

@@ -0,0 +1,54 @@
package io.shinhanlife.dap.common.config;
import com.p6spy.engine.logging.Category;
import com.p6spy.engine.spy.appender.MessageFormattingStrategy;
/**
* @package io.shinhanlife.dap.common.config
* @className P6SpySqlFormatter
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </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 "";
}
}

View File

@@ -0,0 +1,105 @@
package io.shinhanlife.dap.common.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.common.integration
* @className GlowIntegrationCall
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </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);
}
*/
}

View File

@@ -0,0 +1,84 @@
package io.shinhanlife.dap.common.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.common.integration.dto
* @className SampleGlowMessage
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </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;
}
}

View File

@@ -0,0 +1,41 @@
package io.shinhanlife.dap.common.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.common.integration.mci.config
* @className ShinhanIntegrationProperties
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
@Getter
@Setter
@Configuration
@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;
}
}

View File

@@ -0,0 +1,34 @@
package io.shinhanlife.dap.common.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.common.integration.mci.dto
* @className MciRequestWrapper
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MciRequestWrapper<T> {
private ShinhanCommonHeaderDto tgrmCmnnhddValu;
@JsonUnwrapped
private T body;
}

View File

@@ -0,0 +1,32 @@
package io.shinhanlife.dap.common.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.common.integration.mci.dto
* @className MciResponseWrapper
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MciResponseWrapper<T> {
private ShinhanCommonHeaderDto tgrmCmnnhddValu;
@JsonUnwrapped
private T body;
}

View File

@@ -0,0 +1,40 @@
package io.shinhanlife.dap.common.integration.mci.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @package io.shinhanlife.dap.common.integration.mci.dto
* @className ShinhanCommonHeaderDto
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ShinhanCommonHeaderDto {
private String envrTypeCd; // 환경유형코드 (D, T, R)
private String glbId; // 전사공통키 (37 Byte)
private String pgrsSriaNo; // 진행일련번호
private String trgmVrsnInfoValu; // 전문버전정보값
private String tgrmEncrYn; // 전문암호화여부
private String gpcpCd; // 글로벌법인코드
private String appliDutjCd; // 어플리케이션업무코드
private String rcvSvcId; // 수신서비스ID
private String reqRspnScCd; // 요청응답구분코드 (S:요청, R:응답)
private String inqrTraTypeCd; // 조회거래유형코드
private String reqTgrmTnsmDtptDt; // 요청전문전송일시
private String itrIfId; // 인터페이스ID
}

View File

@@ -0,0 +1,32 @@
package io.shinhanlife.dap.common.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.common.mcp.config
* @className CacheConfig
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
@Configuration
@EnableCaching
public class CacheConfig {
// 스프링이 캐시를 관리할 기본 저장소를 빈(Bean)으로 등록합니다.
@Bean
public CacheManager cacheManager() {
return new ConcurrentMapCacheManager("eimsData"); // 아까 설정한 캐시 이름 등록
}
}

View File

@@ -0,0 +1,41 @@
package io.shinhanlife.dap.common.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.common.mcp.config
* @className JacksonConfig
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </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();
}
}

View File

@@ -0,0 +1,58 @@
package io.shinhanlife.dap.common.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.common.mcp.config
* @className KafkaLocalConfig
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </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());
}
}

View File

@@ -0,0 +1,47 @@
package io.shinhanlife.dap.common.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.common.mcp.config
* @className SwaggerConfig
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </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를 입력해주세요. ")));
}
}

View File

@@ -0,0 +1,53 @@
package io.shinhanlife.dap.common.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.common.mcp.security.ApiKeyInterceptor;
/**
* @package io.shinhanlife.dap.common.mcp.config
* @className WebConfig
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </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("*")
.allowCredentials(true);
}
}

View File

@@ -0,0 +1,45 @@
package io.shinhanlife.dap.common.mcp.exception;
import io.shinhanlife.dap.biz.mcp.adapter.dto.ErrorDetail;
import io.shinhanlife.dap.biz.mcp.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);
}
}

View File

@@ -0,0 +1,56 @@
package io.shinhanlife.dap.common.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.common.mcp.filter
* @className MdcLoggingFilter
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </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();
}
}
}

View File

@@ -0,0 +1,80 @@
package io.shinhanlife.dap.common.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.common.mcp.security
* @className ApiKeyInterceptor
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </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");
}
}

View File

@@ -0,0 +1,40 @@
package io.shinhanlife.dap.common.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.common.mcp.security
* @className SecurityProperties
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </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<>();
}

View File

@@ -0,0 +1,29 @@
package io.shinhanlife.dap.common.session.converter;
import io.shinhanlife.dap.common.session.dto.SessionDto;
import io.shinhanlife.dap.common.session.dto.ZtUsacOutDto;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
/**
* @package io.shinhanlife.dap.common.session.converter
* @className ZtUsacConverter
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
@Mapper(componentModel = "spring")
public abstract class ZtUsacConverter {
@Mapping(target = "loginDtm", ignore = true)
@Mapping(target = "isManager", ignore = true)
public abstract SessionDto toSessionDto(ZtUsacOutDto dto);
}

View File

@@ -0,0 +1,51 @@
package io.shinhanlife.dap.common.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.common.session.domain.model
* @className ZtUsacModel
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </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;
}

View File

@@ -0,0 +1,33 @@
package io.shinhanlife.dap.common.session.domain.repository;
import io.shinhanlife.glow.GlowMybatisMapper;
import io.shinhanlife.dap.common.session.dto.ZtUsacInDto;
import io.shinhanlife.dap.common.session.dto.ZtUsacOutDto;
/**
* @package io.shinhanlife.dap.common.session.domain.repository
* @className ZtUsacRepository
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
@GlowMybatisMapper
public interface ZtUsacRepository {
/**
* 사용자 조회 (단건)
*
* @param dto 사번
* @return 인사정보
*/
ZtUsacOutDto selectZtUsac(ZtUsacInDto dto);
}

View File

@@ -0,0 +1,29 @@
package io.shinhanlife.dap.common.session.domain.service;
import io.shinhanlife.dap.common.session.dto.ZtUsacInDto;
import io.shinhanlife.dap.common.session.dto.ZtUsacOutDto;
/**
* @package io.shinhanlife.dap.common.session.domain.service
* @className ZtUsacService
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
public interface ZtUsacService {
/**
* 사용자 조회 (단건)
*
* @param dto 사번
* @return 인사정보
*/
ZtUsacOutDto selectZtUsac(ZtUsacInDto dto);
}

View File

@@ -0,0 +1,42 @@
package io.shinhanlife.dap.common.session.domain.service.impl;
import io.shinhanlife.dap.common.session.domain.repository.ZtUsacRepository;
import io.shinhanlife.dap.common.session.domain.service.ZtUsacService;
import io.shinhanlife.dap.common.session.dto.ZtUsacInDto;
import io.shinhanlife.dap.common.session.dto.ZtUsacOutDto;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
/**
* @package io.shinhanlife.dap.common.session.domain.service.impl
* @className ZtUsacServiceImpl
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
@Service
@RequiredArgsConstructor
public class ZtUsacServiceImpl implements ZtUsacService {
private final ZtUsacRepository ztUsacRepository;
/**
* 사용자 조회 (단건)
*
* @param dto 사번
* @return 인사정보
*/
@Override
public ZtUsacOutDto selectZtUsac(ZtUsacInDto dto) {
ZtUsacOutDto result = ztUsacRepository.selectZtUsac(dto);
result.initLists();
return result;
}
}

View File

@@ -0,0 +1,79 @@
package io.shinhanlife.dap.common.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.common.session.dto
* @className SessionDto
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </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;
// 유틸성
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";
}
}

View File

@@ -0,0 +1,37 @@
package io.shinhanlife.dap.common.session.dto;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
/**
* @package io.shinhanlife.dap.common.session.dto
* @className ZtUsacInDto
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class ZtUsacInDto {
/* 인사번호 */
private String prafNo;
/* 사용여부 */
@Builder.Default
private String puseYn = "Y";
}

View File

@@ -0,0 +1,85 @@
package io.shinhanlife.dap.common.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.common.session.dto
* @className ZtUsacOutDto
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </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(","));
}
}

View File

@@ -0,0 +1,91 @@
package io.shinhanlife.dap.common.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.common.session.converter.ZtUsacConverter;
import io.shinhanlife.dap.common.session.domain.service.ZtUsacService;
import io.shinhanlife.dap.common.session.dto.SessionDto;
import io.shinhanlife.dap.common.session.dto.ZtUsacInDto;
import io.shinhanlife.dap.common.session.dto.ZtUsacOutDto;
import io.shinhanlife.dap.common.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.common.session.presentation
* @className SsoRestController
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
@RestController
@RequiredArgsConstructor
@Slf4j
@RequestMapping("/sso")
public class SsoRestController {
private static final String NLS_LOGIN_URL = "";
private final ZtUsacService 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());
}
}

View File

@@ -0,0 +1,35 @@
package io.shinhanlife.dap.common.session.presentation.io;
import io.shinhanlife.dap.common.session.dto.SessionDto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
/**
* @package io.shinhanlife.dap.common.session.presentation.io
* @className SsoResponse
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class SsoResponse {
private String retCode;
private SessionDto userInfo;
private String redirectUrl;
}

View File

@@ -0,0 +1,275 @@
package io.shinhanlife.dap.common.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 또는 axhub-tool-payment): ");
String moduleName = rawModuleName.startsWith("axhub-tool-") ? rawModuleName : "axhub-tool-" + rawModuleName;
String portStr = getOrAsk(args, 1, scanner, "2. 사용할 포트 번호 (예: 8085): ");
String shortName = moduleName.replace("axhub-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(':axhub-tool-core')
}
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/axhub/biz/mcp/tool/" + shortName);
Files.createDirectories(srcPath);
String appClass = """
package io.shinhanlife.dap.biz.mcp.tool.%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.biz.mcp.tool.%s
* @className %sToolApplication
* @description AX HUB 시스템 처리 클래스
* @author %s
* @create %s
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* %s %s 최초생성
*
* </pre>
*/
@SpringBootApplication(scanBasePackages = {"io.shinhanlife.dap.biz.mcp.tool", "io.shinhanlife.dap.biz.mcp.adapter", "io.shinhanlife.dap.common.mcp", "io.shinhanlife.dap.common.config"})
@ConfigurationPropertiesScan(basePackages = {"io.shinhanlife.dap.biz.mcp.tool", "io.shinhanlife.dap.biz.mcp.adapter", "io.shinhanlife.dap.common.mcp", "io.shinhanlife.dap.common.config"})
@EnableCaching
public class %sToolApplication {
public static void main(String[] args) {
SpringApplication.run(%sToolApplication.class, args);
}
}
""".formatted(shortName, shortName, capitalize(shortName), author, createDate, createDate, author, capitalize(shortName), capitalize(shortName));
Files.writeString(srcPath.resolve(capitalize(shortName) + "ToolApplication.java"), appClass);
Path resPath = modulePath.resolve("src/main/resources");
Files.createDirectories(resPath);
String applicationYml = """
spring:
config:
import: "classpath:config/application-glow-local.yml"
""";
Files.writeString(resPath.resolve("application-local.yml"), applicationYml);
String applicationProperties = """
server.port=%s
spring.application.name=%s
spring.profiles.active=local
# Suppress Kafka Connection Logs
logging.level.org.apache.kafka=ERROR
# Auto Prefix Namespace
mcp.namespace=%s
# API 보안 키 설정
mcp.security.tenant-domains.TESTER-DEV=ALL
""".formatted(portStr, moduleName, shortName);
Files.writeString(resPath.resolve("application.properties"), applicationProperties);
String applicationLocalProperties = """
# Local 환경 전용 설정 (H2 메모리 DB 등)
spring.datasource.url=jdbc:p6spy:h2:mem:testdb;DB_CLOSE_DELAY=-1;
spring.datasource.driverClassName=com.p6spy.engine.spy.P6SpyDriver
spring.datasource.username=sa
spring.datasource.password=password
spring.h2.console.enabled=true
# EIMS 동적 라우팅 접속 정보
eims.http.url=http://localhost:${server.port}/api/gateway
eims.tcp.host=127.0.0.1
eims.tcp.port=8090
eims.tcp.timeout=5000
eims.jsp.form.url=http://localhost:${server.port}/mock/jsp-form
eims.jsp.json.url=http://localhost:${server.port}/mock/jsp-json
eims.mci.url=http://localhost:${server.port}/api/mock/esb/api
eims.mcistring.url=http://localhost:${server.port}/api/mock/esb/string
# Gateway/Tool URLs
axhub.gateway.url=http://localhost:8081
axhub.tool.url=http://localhost:${server.port}
# Disable Kafka
spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.kafka.KafkaAutoConfiguration
# Suppress Kafka Connection Logs
logging.level.org.apache.kafka=ERROR
""";
Files.writeString(resPath.resolve("application-local.properties"), applicationLocalProperties);
String applicationDevProperties = """
# Render 클라우드 환경 전용 설정
axhub.gateway.url=https://axhub-gateway.onrender.com
axhub.tool.url=https://%s.onrender.com
# EIMS 동적 라우팅 접속 정보 (Mock)
eims.http.url=http://localhost:${server.port}/api/gateway
eims.tcp.host=127.0.0.1
eims.tcp.port=8090
eims.tcp.timeout=5000
eims.jsp.form.url=http://localhost:${server.port}/mock/jsp-form
eims.jsp.json.url=http://localhost:${server.port}/mock/jsp-json
eims.mci.url=http://localhost:${server.port}/api/mock/esb/api
eims.mcistring.url=http://localhost:${server.port}/api/mock/esb/string
""".formatted(moduleName);
Files.writeString(resPath.resolve("application-dev.properties"), applicationDevProperties);
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("axhub-", ""); // e.g. tool-payment
if (!compose.contains(" " + serviceName + ":")) {
String newService = """
%s:
build:
context: ./%s
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);
}
}

View File

@@ -0,0 +1,45 @@
package io.shinhanlife.dap.common.util;
import io.shinhanlife.dap.common.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.common.util
* @className SessionUtil
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </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;
}
}

View File

@@ -0,0 +1,372 @@
package io.shinhanlife.dap.common.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.Scanner;
/**
* MCP Tool 코드를 자동 생성(Scaffolding)하는 유틸리티 클래스
*
* [실행 방법]
* 방법 1. IDE(IntelliJ 등)에서 직접 실행 (대화형 모드 추천 ⭐)
* - 이 클래스(ToolScaffolder.java)를 열고 main 메서드를 직접 실행(Run)합니다.
* - 콘솔 창에 뜨는 질문에 차례대로 값을 입력하기만 하면 파일이 생성됩니다.
*
* 방법 2. 커맨드라인(터미널)에서 실행 (명령어 기반)
* - 컴파일: javac -encoding UTF-8 axhub-tool-core/src/main/java/io/shinhanlife/axhub/biz/mcp/tool/util/ToolScaffolder.java
* - 실행: java -cp axhub-tool-core/src/main/java io.shinhanlife.dap.biz.mcp.tool.util.ToolScaffolder [이름] [ID] "[설명]" "[그룹]" "[통신방식]" "[모듈명]"
*/
/**
* @package io.shinhanlife.dap.biz.mcp.tool.util
* @className ToolScaffolder
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
public class ToolScaffolder {
private static final String BASE_PACKAGE = "io.shinhanlife.dap.biz.mcp.tool";
private static final String BASE_PACKAGE_PATH = "src/main/java/io/shinhanlife/axhub/biz/mcp/tool";
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 소속 그룹 (예: 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. 코드를 생성할 모듈 (기본: axhub-tool-other): ");
if (moduleName.trim().isEmpty()) {
moduleName = "axhub-tool-other";
}
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);
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) throws IOException {
baseName = toPascalCase(baseName);
String envSourceDir = System.getenv("AXHUB_SOURCE_DIR");
Path rootDir = envSourceDir != null ? Paths.get(envSourceDir) : Paths.get(".");
Path serviceDir = rootDir.resolve(Paths.get(moduleName, BASE_PACKAGE_PATH, "service"));
Path dtoDir = rootDir.resolve(Paths.get(moduleName, BASE_PACKAGE_PATH, "dto"));
String shortName = moduleName.replace("axhub-tool-", "").replace("-", "");
Path legacyDtoDir = rootDir.resolve(Paths.get(moduleName, BASE_PACKAGE_PATH, shortName, "dto"));
Path converterDir = rootDir.resolve(Paths.get(moduleName, BASE_PACKAGE_PATH, shortName, "converter"));
Files.createDirectories(serviceDir);
Files.createDirectories(dtoDir);
Files.createDirectories(legacyDtoDir);
Files.createDirectories(converterDir);
StringBuilder log = new StringBuilder();
// Generate Req DTO
String reqContent = """
package %s.dto;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;
/**
* @package %s.dto
* @className %sReq
* @description AX HUB 시스템 처리 클래스
* @author %s
* @create %s
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* %s %s 최초생성
*
* </pre>
*/
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class %sReq {
// TODO: Add request fields here
}
""".formatted(BASE_PACKAGE, BASE_PACKAGE, baseName, author, createDate, createDate, author, baseName);
Files.writeString(dtoDir.resolve(baseName + "Req.java"), reqContent);
// Generate Res DTO
String resContent = """
package %s.dto;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;
/**
* @package %s.dto
* @className %sRes
* @description AX HUB 시스템 처리 클래스
* @author %s
* @create %s
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* %s %s 최초생성
*
* </pre>
*/
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class %sRes {
private String status;
private String message;
// TODO: Add response fields here
}
""".formatted(BASE_PACKAGE, BASE_PACKAGE, baseName, author, createDate, createDate, author, baseName);
Files.writeString(dtoDir.resolve(baseName + "Res.java"), resContent);
String toolName = baseName.isEmpty() ? baseName : Character.toLowerCase(baseName.charAt(0)) + baseName.substring(1);
// Generate Service
String serviceContent = """
package %s.service;
import %s.annotation.McpFunction;
import %s.annotation.McpTool;
import %s.dto.%sReq;
import %s.dto.%sRes;
import %s.%s.converter.%sLegacyConverter;
import org.springframework.stereotype.Service;
import org.mapstruct.factory.Mappers;
/**
* @package %s.service
* @className %sService
* @description AX HUB 시스템 처리 클래스
* @author %s
* @create %s
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* %s %s 최초생성
*
* </pre>
*/
@Service
@McpTool(
routingType = "%s",
categoryKey = "%s"
)
public class %sService extends AbstractMcpToolService {
private final %sLegacyConverter converter = Mappers.getMapper(%sLegacyConverter.class);
@McpFunction(
displayName = "%s 툴",
name = "%s",
description = "%s",
prompt = "%s",
mappingId = "%s",
register = %s,
requiresApproval = false
)
public Object execute(%sReq req) {
// %sLegacyReq legacyReq = converter.toLegacyReq(req);
return executeLegacy("%s", "%s", req); // Or pass legacyReq
}
}
""".formatted(
BASE_PACKAGE,
BASE_PACKAGE,
BASE_PACKAGE,
BASE_PACKAGE, baseName,
BASE_PACKAGE, baseName,
BASE_PACKAGE, shortName, baseName,
BASE_PACKAGE,
baseName,
author,
createDate,
createDate, author,
routingType, group.toLowerCase(),
baseName,
baseName, baseName,
baseName, toolName, description, description + " 해줘.", interfaceId, register,
baseName,
baseName,
routingType, interfaceId
);
Files.writeString(serviceDir.resolve(baseName + "Service.java"), serviceContent);
// Generate Legacy Req DTO
String legacyReqContent = """
package %s.%s.dto;
import lombok.Data;
/**
* @package %s.%s.dto
* @className %sLegacyReq
* @description AX HUB 시스템 처리 클래스
* @author %s
* @create %s
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* %s %s 최초생성
*
* </pre>
*/
@Data
public class %sLegacyReq {
// TODO: Add legacy request fields here
}
""".formatted(BASE_PACKAGE, shortName, BASE_PACKAGE, shortName, baseName, author, createDate, createDate, author, baseName);
Files.writeString(legacyDtoDir.resolve(baseName + "LegacyReq.java"), legacyReqContent);
// Generate Legacy Res DTO
String legacyResContent = """
package %s.%s.dto;
import lombok.Data;
/**
* @package %s.%s.dto
* @className %sLegacyRes
* @description AX HUB 시스템 처리 클래스
* @author %s
* @create %s
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* %s %s 최초생성
*
* </pre>
*/
@Data
public class %sLegacyRes {
// TODO: Add legacy response fields here
}
""".formatted(BASE_PACKAGE, shortName, BASE_PACKAGE, shortName, baseName, author, createDate, createDate, author, baseName);
Files.writeString(legacyDtoDir.resolve(baseName + "LegacyRes.java"), legacyResContent);
// Generate Legacy Converter
String converterContent = """
package %s.%s.converter;
import %s.dto.%sReq;
import %s.dto.%sRes;
import %s.%s.dto.%sLegacyReq;
import %s.%s.dto.%sLegacyRes;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers;
/**
* @package %s.%s.converter
* @className %sLegacyConverter
* @description AX HUB 시스템 처리 클래스
* @author %s
* @create %s
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* %s %s 최초생성
*
* </pre>
*/
@Mapper(componentModel = "spring")
public interface %sLegacyConverter {
// @Mapping(source = "sourceField", target = "targetField")
%sLegacyReq toLegacyReq(%sReq req);
%sRes toRes(%sLegacyRes legacyRes);
}
""".formatted(
BASE_PACKAGE, shortName,
BASE_PACKAGE, baseName,
BASE_PACKAGE, baseName,
BASE_PACKAGE, shortName, baseName,
BASE_PACKAGE, shortName, baseName,
BASE_PACKAGE, shortName, baseName, author, createDate, createDate, author,
baseName, baseName, baseName, baseName, baseName, baseName
);
Files.writeString(converterDir.resolve(baseName + "LegacyConverter.java"), converterContent);
log.append("\n=========================================\n");
log.append(" Scaffolding Complete!\n");
log.append("=========================================\n");
log.append("[Service] ").append(serviceDir.resolve(baseName + "Service.java")).append("\n");
log.append("[Req DTO] ").append(dtoDir.resolve(baseName + "Req.java")).append("\n");
log.append("[Res DTO] ").append(dtoDir.resolve(baseName + "Res.java")).append("\n");
log.append("[Legacy Req DTO] ").append(legacyDtoDir.resolve(baseName + "LegacyReq.java")).append("\n");
log.append("[Legacy Res DTO] ").append(legacyDtoDir.resolve(baseName + "LegacyRes.java")).append("\n");
log.append("[Legacy Converter] ").append(converterDir.resolve(baseName + "LegacyConverter.java")).append("\n");
log.append("\n Tip: ").append(interfaceId).append(" 목업 데이터를 mock-responses.json에 추가하세요.\n");
return log.toString();
}
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();
}
}

View File

@@ -0,0 +1,108 @@
package io.shinhanlife.dap.common.util;
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 *Service.java files in axhub-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("Service.java"))
.filter(p -> 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);
}
}

View File

@@ -0,0 +1,43 @@
package io.shinhanlife.glow;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
//@Schema(description = "응답 에러 객체. 성공 케이스일 경우 null. 실제 에러가 발생할 경우에만 예외명, 예외 메시지 필드 세팅 예정.")
/**
* @package io.shinhanlife.glow
* @className BaseException
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
public class BaseException {
// @Schema(description = "Error 코드 Meta 참조 운영. (예) 20001, 50001 등", shinhanlife = "20001")
String code;
// @Schema(description = "메시지")
String message;
// @Schema(description = "예외명.", shinhanlife = "NullPointerException")
@Builder.Default
String exceptionName = "";
// @Schema(description = "예외상세", shinhanlife = "StackTrace")
@Builder.Default
String exceptionDetail = "";
}

View File

@@ -0,0 +1,39 @@
package io.shinhanlife.glow;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.ToString;
/**
* @package io.shinhanlife.glow
* @className BaseResponse
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
@ToString
@Getter
@Builder
@AllArgsConstructor
public class BaseResponse<T> {
// @Schema(description = "응답 코드 HTTP STATUS 오류 - 실제 Header 는 200 으로 내려감", shinhanlife = "200")
private int code;
// @Schema(description = "응답 메시지. 응답 코드와 매핑된 메시지.", shinhanlife = "데이터 생성 성공")
private String message;
// @Schema(description = "응답 본문 데이터. 실제 비즈니스 데이터.")
private T data;
private BaseException error;
}

View File

@@ -0,0 +1,20 @@
package io.shinhanlife.glow;
/**
* @package io.shinhanlife.glow
* @className BizException
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
public class BizException extends RuntimeException {
public BizException(String s) {
}
}

View File

@@ -0,0 +1,13 @@
package io.shinhanlife.glow;
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface GlowAppServiceId {
String value();
String description() default "";
}

View File

@@ -0,0 +1,5 @@
package io.shinhanlife.glow;
public @interface GlowControllerId {
String value();
}

View File

@@ -0,0 +1,9 @@
package io.shinhanlife.glow;
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface GlowIndexPaging {
}

View File

@@ -0,0 +1,29 @@
package io.shinhanlife.glow;
import java.lang.annotation.*;
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface GlowLogTarget {
Target[] value() default {};
/**
* @package io.shinhanlife.glow
* @className Target
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
enum Target {
FILE, CONSOLE
}
}

View File

@@ -0,0 +1,33 @@
package io.shinhanlife.glow;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
/**
* @package io.shinhanlife.glow
* @className GlowLogger
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
@Component
@Scope("prototype")
public class GlowLogger {
private Logger log = LoggerFactory.getLogger(GlowLogger.class);
public void debug(String message, Object... args) { log.debug(message, args); }
public void info(String message, Object... args) { log.info(message, args); }
public void warn(String message, Object... args) { log.warn(message, args); }
public void error(String message, Object... args) { log.error(message, args); }
public void error(String message, Throwable t) { log.error(message, t); }
}

View File

@@ -0,0 +1,29 @@
package io.shinhanlife.glow;
import java.lang.annotation.*;
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface GlowMciFieldInfo {
/**
* 필드 순서
*/
int order();
/**
* 필드 길이 (List인 경우 전체 길이로 활용될 수 있음)
*/
int length();
/**
* 필드 설명
*/
String description() default "";
/**
* List 매핑 시 대상 DTO 클래스
*/
Class<?> target() default void.class;
}

View File

@@ -0,0 +1,18 @@
package io.shinhanlife.glow;
import org.apache.ibatis.annotations.Mapper;
import org.springframework.stereotype.Component;
import java.lang.annotation.*;
/**
* MyBatis Mapper 인터페이스를 나타내는 어노테이션
* MyBatis Mapper와 동일한 기능을 제공합니다.
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Mapper
@Component
public @interface GlowMybatisMapper {
}

View File

@@ -0,0 +1,7 @@
package io.shinhanlife.glow;
public @interface GlowServiceGroupId {
String value();
String description();
}

View File

@@ -0,0 +1,25 @@
package io.shinhanlife.glow;
import java.lang.annotation.*;
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface GlowTrgmField {
/**
* 필드 순서
*/
int order();
/**
* 필드 길이
*/
int length();
/**
* 필드 설명
*/
String description() default "";
}

View File

@@ -0,0 +1,82 @@
package io.shinhanlife.glow;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import org.apache.ibatis.session.RowBounds;
import java.io.Serial;
import java.io.Serializable;
/**
* @package io.shinhanlife.glow
* @className PageInfo
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
@Getter
@Setter
@JsonIgnoreProperties(ignoreUnknown = true)
@EqualsAndHashCode(callSuper = true)
public class PageInfo extends RowBounds implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 페이지번호 ( 입력값 )
*/
@GlowTrgmField(order = 1, length = 5, description = "페이지번호")
private int pageNo;
/**
* 페이지 데이터 건수 ( 열 건수, 입력값 )
*/
@GlowTrgmField(order = 2, length = 5, description = "페이지데이터건수")
private int pageDataCc;
/**
* 총페이지 수 ( 리턴값 )
*/
@GlowTrgmField(order = 3, length = 10, description = "총페이지수")
private int totaPageCn;
/**
* 총 페이지 데이터 건수 ( 리턴값 )
*/
@GlowTrgmField(order = 4, length = 10, description = "총페이지데이터건수")
private int totaPageDataCc;
public PageInfo(int pageNo, int pageDataCc) {
super(((pageNo <= 0 ? 1 : pageNo) - 1) * pageDataCc, pageDataCc);
this.pageNo = pageNo;
this.pageDataCc = pageDataCc;
}
public PageInfo() {
}
@JsonIgnore
@Override
public int getOffset() {
return super.getOffset();
}
@JsonIgnore
@Override
public int getLimit() {
return super.getLimit();
}
}

View File

@@ -0,0 +1,45 @@
package io.shinhanlife.glow;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
/**
* @package io.shinhanlife.glow
* @className ResponseCode
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
@Getter
@RequiredArgsConstructor
public enum ResponseCode {
/* ===================== 성공 ===================== */
SUCCESS(HttpStatus.OK, "정상 처리되었습니다."),
CREATED(HttpStatus.CREATED, "데이터 생성 성공"),
/* ===================== 클라이언트 오류 (4xx) ===================== */
BAD_REQUEST(HttpStatus.BAD_REQUEST, "잘못된 요청입니다."),
INVALID_PARAMETER(HttpStatus.BAD_REQUEST, "유효하지 않은 파라미터입니다."),
UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "인증이 필요합니다."),
FORBIDDEN(HttpStatus.FORBIDDEN, "접근 권한이 없습니다."),
NOT_FOUND(HttpStatus.NOT_FOUND, "요청한 리소스를 찾을 수 없습니다."),
METHOD_NOT_ALLOWED(HttpStatus.METHOD_NOT_ALLOWED, "허용되지 않은 HTTP 메서드입니다."),
CONFLICT(HttpStatus.CONFLICT, "이미 존재하는 데이터입니다."),
/* ===================== 서버 오류 (5xx) ===================== */
INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "서버 내부 오류가 발생했습니다."),
SERVICE_UNAVAILABLE(HttpStatus.SERVICE_UNAVAILABLE, "서비스를 사용할 수 없습니다.");
private final HttpStatus status;
private final String message;
}

View File

@@ -0,0 +1,125 @@
package io.shinhanlife.glow;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
/**
* @package io.shinhanlife.glow
* @className ResponseUtil
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
public final class ResponseUtil {
private ResponseUtil() {
}
public static <T> ResponseEntity<BaseResponse<T>> ok() {
return ResponseEntity.ok(
BaseResponse.<T>builder()
.code(ResponseCode.SUCCESS.getStatus().value())
.message(ResponseCode.SUCCESS.getMessage())
.build()
);
}
public static <T> ResponseEntity<BaseResponse<T>> ok(T data) {
return ResponseEntity.ok(
BaseResponse.<T>builder()
.code(ResponseCode.SUCCESS.getStatus().value())
.message(ResponseCode.SUCCESS.getMessage())
.data(data)
.build()
);
}
public static <T> ResponseEntity<BaseResponse<T>> ok(T data, ResponseCode responseCode) {
return ResponseEntity.ok(
BaseResponse.<T>builder()
.code(responseCode.getStatus().value())
.message(responseCode.getMessage())
.data(data)
.build()
);
}
public static <T> ResponseEntity<BaseResponse<T>> error(
HttpStatus status, String code, String message
) {
return ResponseEntity.status(status)
.body(
BaseResponse.<T>builder()
.code(status.value())
.error(
BaseException.builder()
.code(code)
.message(message)
.build()
)
.build()
);
}
public static <T> ResponseEntity<BaseResponse<T>> error(
HttpStatus status, String code, String message, String exceptionName, String stackTrace
) {
return ResponseEntity.status(status)
.body(
BaseResponse.<T>builder()
.code(status.value())
.error(
BaseException.builder()
.code(code)
.message(message)
.exceptionName(exceptionName)
.exceptionDetail(stackTrace)
.build()
)
.build()
);
}
public static <T> ResponseEntity<BaseResponse<T>> okError(
HttpStatus status, String code, String message
) {
return ResponseEntity.ok(
BaseResponse.<T>builder()
.code(status.value())
.error(
BaseException.builder()
.code(code)
.message(message)
.build()
)
.build()
);
}
public static <T> ResponseEntity<BaseResponse<T>> okError(
HttpStatus status, String code, String message, String exceptionName, String stackTrace
) {
return ResponseEntity.ok(
BaseResponse.<T>builder()
.code(status.value())
.error(
BaseException.builder()
.code(code)
.message(message)
.exceptionName(exceptionName)
.exceptionDetail(stackTrace)
.build()
)
.build()
);
}
}

View File

@@ -0,0 +1,40 @@
package io.shinhanlife.glow.db.dto;
import lombok.Getter;
import lombok.Setter;
import lombok.Builder;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import java.util.Date;
/**
* @package io.shinhanlife.glow.db.dto
* @className AuditInfo
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class AuditInfo {
private Date systRgiDt; // 시스템등록일시
private String systRgiPrafNo; // 시스템등록인사번호
private String systRgiOgnzNo; // 시스템등록조직번호
private String systRgiSystCd; // 시스템등록시스템코드
private String systRgiPrgrId; // 시스템등록프로그램ID
private Date systChgDt; // 시스템변경일시
private String systChgPrafNo; // 시스템변경인사번호
private String systChgOgnzNo; // 시스템변경조직번호
private String systChgSystCd; // 시스템변경시스템코드
private String systChgPrgrId; // 시스템변경프로그램ID
}

View File

@@ -0,0 +1,105 @@
package io.shinhanlife.glow.util;
import io.shinhanlife.glow.GlowMciFieldInfo;
import lombok.extern.slf4j.Slf4j;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
/**
* @package io.shinhanlife.glow.util
* @className GlowMciParser
* @description MCI 고정 길이 전문 파싱 유틸리티 (GlowMciFieldInfo 기반)
* @author 김형식
* @create 2026.09.01
*/
@Slf4j
public class GlowMciParser {
public static <T> T parse(String mciString, Class<T> clazz) {
if (mciString == null || mciString.isEmpty()) {
return null;
}
try {
T instance = clazz.getDeclaredConstructor().newInstance();
List<Field> fields = new ArrayList<>();
for (Field field : clazz.getDeclaredFields()) {
if (field.isAnnotationPresent(GlowMciFieldInfo.class)) {
fields.add(field);
}
}
fields.sort(Comparator.comparingInt(f -> f.getAnnotation(GlowMciFieldInfo.class).order()));
int currentIndex = 0;
for (Field field : fields) {
GlowMciFieldInfo annotation = field.getAnnotation(GlowMciFieldInfo.class);
int length = annotation.length();
if (currentIndex >= mciString.length()) {
break;
}
int endIndex = Math.min(currentIndex + length, mciString.length());
String value = mciString.substring(currentIndex, endIndex);
field.setAccessible(true);
setFieldValue(instance, field, value, annotation);
currentIndex += length;
}
return instance;
} catch (Exception e) {
log.error("GlowMciFieldInfo 파싱 중 오류 발생: {}", e.getMessage(), e);
throw new RuntimeException("MCI 전문 파싱 오류", e);
}
}
private static void setFieldValue(Object instance, Field field, String value, GlowMciFieldInfo annotation) throws IllegalAccessException {
Class<?> fieldType = field.getType();
String trimmedValue = value.trim();
if (fieldType == String.class) {
field.set(instance, trimmedValue);
} else if (fieldType == int.class || fieldType == Integer.class) {
field.set(instance, trimmedValue.isEmpty() ? 0 : Integer.parseInt(trimmedValue));
} else if (fieldType == long.class || fieldType == Long.class) {
field.set(instance, trimmedValue.isEmpty() ? 0L : Long.parseLong(trimmedValue));
} else if (fieldType == boolean.class || fieldType == Boolean.class) {
field.set(instance, Boolean.parseBoolean(trimmedValue));
} else if (fieldType == double.class || fieldType == Double.class) {
field.set(instance, trimmedValue.isEmpty() ? 0.0 : Double.parseDouble(trimmedValue));
} else if (List.class.isAssignableFrom(fieldType)) {
Class<?> targetClass = annotation.target();
if (targetClass != void.class) {
List<Object> list = new ArrayList<>();
int itemLength = calculateTotalLength(targetClass);
if (itemLength > 0) {
for (int i = 0; i < value.length(); i += itemLength) {
int end = Math.min(i + itemLength, value.length());
String itemStr = value.substring(i, end);
if (itemStr.trim().isEmpty()) continue;
list.add(parse(itemStr, targetClass));
}
}
field.set(instance, list);
}
} else {
field.set(instance, trimmedValue);
}
}
private static int calculateTotalLength(Class<?> clazz) {
int total = 0;
for (Field field : clazz.getDeclaredFields()) {
if (field.isAnnotationPresent(GlowMciFieldInfo.class)) {
total += field.getAnnotation(GlowMciFieldInfo.class).length();
}
}
return total;
}
}

View File

@@ -0,0 +1,92 @@
package io.shinhanlife.glow.util;
import io.shinhanlife.glow.GlowTrgmField;
import lombok.extern.slf4j.Slf4j;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
/**
* @package io.shinhanlife.glow.util
* @className GlowTrgmParser
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
@Slf4j
public class GlowTrgmParser {
/**
* 고정 길이 문자열 전문을 @GlowTrgmField 어노테이션 정보에 따라 DTO 객체로 파싱합니다.
* @param trgmString 원본 전문 문자열
* @param clazz 매핑할 DTO 클래스
* @return 파싱된 DTO 인스턴스
*/
public static <T> T parse(String trgmString, Class<T> clazz) {
if (trgmString == null || trgmString.isEmpty()) {
return null;
}
try {
T instance = clazz.getDeclaredConstructor().newInstance();
List<Field> fields = new ArrayList<>();
for (Field field : clazz.getDeclaredFields()) {
if (field.isAnnotationPresent(GlowTrgmField.class)) {
fields.add(field);
}
}
fields.sort(Comparator.comparingInt(f -> f.getAnnotation(GlowTrgmField.class).order()));
int currentIndex = 0;
for (Field field : fields) {
GlowTrgmField annotation = field.getAnnotation(GlowTrgmField.class);
int length = annotation.length();
if (currentIndex >= trgmString.length()) {
break;
}
int endIndex = Math.min(currentIndex + length, trgmString.length());
String value = trgmString.substring(currentIndex, endIndex).trim();
field.setAccessible(true);
setFieldValue(instance, field, value);
currentIndex += length;
}
return instance;
} catch (Exception e) {
log.error("GlowTrgmField 파싱 중 오류 발생: {}", e.getMessage(), e);
throw new RuntimeException("전문 파싱 오류", e);
}
}
private static void setFieldValue(Object instance, Field field, String value) throws IllegalAccessException {
Class<?> fieldType = field.getType();
if (fieldType == String.class) {
field.set(instance, value);
} else if (fieldType == int.class || fieldType == Integer.class) {
field.set(instance, value.isEmpty() ? 0 : Integer.parseInt(value));
} else if (fieldType == long.class || fieldType == Long.class) {
field.set(instance, value.isEmpty() ? 0L : Long.parseLong(value));
} else if (fieldType == boolean.class || fieldType == Boolean.class) {
field.set(instance, Boolean.parseBoolean(value));
} else if (fieldType == double.class || fieldType == Double.class) {
field.set(instance, value.isEmpty() ? 0.0 : Double.parseDouble(value));
} else {
field.set(instance, value);
}
}
}

View File

@@ -0,0 +1,50 @@
package io.shinhanlife.glow.util;
import io.shinhanlife.glow.GlowMciFieldInfo;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
public class GlowMciParserTest {
public static class DummyMciDto {
@GlowMciFieldInfo(order = 1, length = 10)
private String customerId;
@GlowMciFieldInfo(order = 2, length = 15)
private String name;
@GlowMciFieldInfo(order = 3, length = 3)
private int age;
public String getCustomerId() { return customerId; }
public String getName() { return name; }
public int getAge() { return age; }
}
@Test
@DisplayName("고정 길이 MCI 문자열을 DTO로 파싱하는 테스트")
public void testParseFixedLengthString() {
// given: 고정 길이 텍스트 (총 28자리)
// ID(10) + Name(15) + Age(3)
String rawMciString = "CUST000001KIM SHINHAN 035";
// when
DummyMciDto result = GlowMciParser.parse(rawMciString, DummyMciDto.class);
// then
System.out.println("==================================================");
System.out.println(" [원본 MCI 전문] : [" + rawMciString + "]");
System.out.println(" [파싱된 ID (10자리)] : [" + result.getCustomerId() + "]");
System.out.println(" [파싱된 Name (15자리)] : [" + result.getName() + "]");
System.out.println(" [파싱된 Age (3자리)] : [" + result.getAge() + "]");
System.out.println("==================================================");
assertNotNull(result);
assertEquals("CUST000001", result.getCustomerId());
assertEquals("KIM SHINHAN", result.getName());
assertEquals(35, result.getAge());
}
}