tags = definitionOptions.tags();
+ if (tags == null || tags.isEmpty()) {
+ tags = List.of(group.toLowerCase(Locale.ROOT), isMutation ? "처리" : "조회");
+ }
+ sb.append(" tags = {")
+ .append(tags.stream().map(t -> "\"" + javaText(t) + "\"").collect(Collectors.joining(", ")))
+ .append("},\n");
+
+ sb.append(" ownerOrg = \"").append(javaText(definitionOptions.ownerOrg() != null && !definitionOptions.ownerOrg().isBlank() ? definitionOptions.ownerOrg() : "MCP_TOOL")).append("\"\n");
+ sb.append(" )");
+ String toolHintLine = sb.toString();
+
+ String serviceInterfaceContent = """
+ package %s.usecase;
+
+ import org.springaicommunity.mcp.annotation.McpTool;
+ import io.shinhanlife.dat.lib.annotation.GrowToolHint;
+ import %s.dto.%sRequest;
+ import %s.dto.%sResponse;
+
+ /**
+ * @package %s.usecase
+ * @className %sUseCase
+ * @description AX HUB 시스템 처리 클래스
+ * @author %s
+ * @create %s
+ *
+ * ---------- 개정이력 ----------
+ * 수정일 수정자 수정내용
+ * ---------- -------- ---------------------------
+ * %s %s 최초생성
+ *
+ *
+ */
+ public interface %sUseCase {
+
+ @McpTool(name = "%s", title = "%s", description = "%s")
+ %s
+ %sResponse execute(%sRequest req);
+ }
+ """.formatted(
+ bizPackage,
+ bizPackage, baseName,
+ bizPackage, baseName,
+ bizPackage, baseName, author, createDate, createDate, author,
+ baseName,
+ toolName, title, description,
+ toolHintLine,
+ baseName, baseName
+ );
+
+ String methodName = Character.toLowerCase(baseName.charAt(0)) + baseName.substring(1);
+ serviceInterfaceContent = serviceInterfaceContent.replace("execute(", methodName + "(");
+ writeUtf8(usecaseDir.resolve(baseName + "UseCase.java"), serviceInterfaceContent);
+
+ String serviceImplContent;
+
+ if (isMci && pagingMode != PagingMode.NONE) {
+ String pagingSuffix = pagingMode == PagingMode.SCROLL ? "ScrollPaging" : "PageNumberPaging";
+ String pagingInterface = baseName + pagingSuffix;
+ if (pagingMode == PagingMode.PAGE_NUMBER) {
+ serviceImplContent = """
+ package %s.usecase.impl;
+
+ import %s.dto.%sRequest;
+ import %s.dto.%sResponse;
+ import %s.usecase.%sUseCase;
+ import %s.paging.%s;
+ import io.shinhanlife.dat.lib.paging.MciPage;
+ import io.shinhanlife.dat.lib.paging.PgNumPagingInfo;
+ import io.shinhanlife.dat.lib.paging.MciPagingUtils;
+ import io.shinhanlife.glow.BizException;
+ 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
+ *
+ * ---------- 개정이력 ----------
+ * 수정일 수정자 수정내용
+ * ---------- -------- ---------------------------
+ * %s %s 최초생성
+ *
+ *
+ */
+ @Slf4j
+ @Service
+ @RequiredArgsConstructor
+ public class %sUseCaseImpl implements %sUseCase {
+
+ private final %s paging;
+
+ @Override
+ public %sResponse execute(%sRequest req) {
+ log.info("[MCI Tool] {} 요청 수신 (페이징 모드: {}).", "%s", "%s");
+ try {
+ PgNumPagingInfo pagingInfo = null;
+ PgNumPagingInfo pagingInfoLast = null;
+ %sResponse finalResponse = null;
+ int loopCount = 0;
+ final int MAX_PAGING_COUNT = 10;
+ boolean hasMore = false;
+
+ while (loopCount < MAX_PAGING_COUNT) {
+ loopCount++;
+ MciPage<%sResponse, PgNumPagingInfo> page = paging.fetch(req, pagingInfo);
+ pagingInfoLast = page.pagingInfo();
+ if (finalResponse == null) {
+ finalResponse = page.data();
+ } else if (page.data() != null) {
+ MciPagingUtils.mergeResponseData(finalResponse, page.data());
+ }
+ if (!page.hasNext()) {
+ hasMore = false;
+ break;
+ }
+ hasMore = true;
+ pagingInfo = page.pagingInfo();
+ }
+
+ if (finalResponse == null) {
+ finalResponse = new %sResponse();
+ }
+ finalResponse.setResultCode("SUCCESS");
+ finalResponse.setHasMore(hasMore);
+ if (pagingInfoLast != null) {
+ int lastPageNo = Math.max(1, pagingInfoLast.getPageNo() - 1);
+ finalResponse.setPageNo(lastPageNo);
+ finalResponse.setTotalPageCount(pagingInfoLast.getTotaPageCn());
+ finalResponse.setTotalCount(pagingInfoLast.getTotaPageDataCc());
+ } else {
+ finalResponse.setPageNo(1);
+ finalResponse.setTotalPageCount(1);
+ }
+ if (hasMore) {
+ finalResponse.setResultMessage("MCI 기본 최대 조회 건수(10회)까지 조회되었습니다. 추가 데이터가 더 존재합니다 (현재 " + finalResponse.getPageNo() + "페이지 / 총 " + finalResponse.getTotalPageCount() + "페이지, hasMore=true).");
+ } else {
+ finalResponse.setResultMessage("MCI 페이징 조회가 완료되었습니다 (현재 " + finalResponse.getPageNo() + "페이지 / 총 " + finalResponse.getTotalPageCount() + "페이지).");
+ }
+ return finalResponse;
+ } catch (BizException e) {
+ log.error("[MCI Tool] 연동 중 오류 발생: {}", e.getMessage(), e);
+ %sResponse response = new %sResponse();
+ response.setResultCode("ERROR");
+ response.setResultMessage(e.getMessage() != null ? e.getMessage() : "Unknown error");
+ response.setHasMore(false);
+ response.setPageNo(1);
+ response.setTotalPageCount(0);
+ response.setTotalCount(0);
+ return response;
+ } catch (Exception e) {
+ log.error("[MCI Tool] 연동 중 오류 발생: {}", e.getMessage(), e);
+ %sResponse response = new %sResponse();
+ response.setResultCode("ERROR");
+ response.setResultMessage(e.getMessage() != null ? e.getMessage() : "Unknown error");
+ response.setHasMore(false);
+ response.setPageNo(1);
+ response.setTotalPageCount(0);
+ response.setTotalCount(0);
+ return response;
+ }
+ }
+ }
+ """.formatted(
+ bizPackage,
+ bizPackage, baseName,
+ bizPackage, baseName,
+ bizPackage, baseName,
+ bizPackage, pagingInterface,
+ bizPackage,
+ baseName,
+ author,
+ createDate,
+ createDate, author,
+ baseName, baseName,
+ pagingInterface,
+ baseName, baseName,
+ toolName, pagingMode,
+ baseName,
+ baseName,
+ baseName,
+ baseName, baseName,
+ baseName, baseName
+ );
+ } else {
+ serviceImplContent = """
+ package %s.usecase.impl;
+
+ import %s.dto.%sRequest;
+ import %s.dto.%sResponse;
+ import %s.usecase.%sUseCase;
+ import %s.paging.%s;
+ import io.shinhanlife.dat.lib.paging.MciPage;
+ import io.shinhanlife.dat.lib.paging.ScrollPagingInfo;
+ import io.shinhanlife.dat.lib.paging.MciPagingUtils;
+ import io.shinhanlife.glow.BizException;
+ 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
+ *
+ * ---------- 개정이력 ----------
+ * 수정일 수정자 수정내용
+ * ---------- -------- ---------------------------
+ * %s %s 최초생성
+ *
+ *
+ */
+ @Slf4j
+ @Service
+ @RequiredArgsConstructor
+ public class %sUseCaseImpl implements %sUseCase {
+
+ private final %s paging;
+
+ @Override
+ public %sResponse execute(%sRequest req) {
+ log.info("[MCI Tool] {} 요청 수신 (페이징 모드: {}).", "%s", "%s");
+ try {
+ ScrollPagingInfo pagingInfo = null;
+ %sResponse finalResponse = null;
+ int loopCount = 0;
+ final int MAX_PAGING_COUNT = 10;
+ boolean hasMore = false;
+
+ while (loopCount < MAX_PAGING_COUNT) {
+ loopCount++;
+ MciPage<%sResponse, ScrollPagingInfo> page = paging.fetch(req, pagingInfo);
+ if (finalResponse == null) {
+ finalResponse = page.data();
+ } else if (page.data() != null) {
+ MciPagingUtils.mergeResponseData(finalResponse, page.data());
+ }
+ if (!page.hasNext()) {
+ hasMore = false;
+ break;
+ }
+ hasMore = true;
+ pagingInfo = page.pagingInfo();
+ }
+
+ if (finalResponse == null) {
+ finalResponse = new %sResponse();
+ }
+ finalResponse.setResultCode("SUCCESS");
+ finalResponse.setHasMore(hasMore);
+ if (hasMore) {
+ finalResponse.setResultMessage("MCI 기본 최대 조회 건수(10회)까지 조회되었습니다. 추가 데이터가 더 존재합니다 (총 " + loopCount + "회 호출, hasMore=true).");
+ } else {
+ finalResponse.setResultMessage("MCI 스크롤 페이징 조회가 완료되었습니다 (총 " + loopCount + "회 호출).");
+ }
+ return finalResponse;
+ } catch (BizException e) {
+ log.error("[MCI Tool] 연동 중 오류 발생: {}", e.getMessage(), e);
+ %sResponse response = new %sResponse();
+ response.setResultCode("ERROR");
+ response.setResultMessage(e.getMessage() != null ? e.getMessage() : "Unknown error");
+ response.setHasMore(false);
+ return response;
+ } catch (Exception e) {
+ log.error("[MCI Tool] 연동 중 오류 발생: {}", e.getMessage(), e);
+ %sResponse response = new %sResponse();
+ response.setResultCode("ERROR");
+ response.setResultMessage(e.getMessage() != null ? e.getMessage() : "Unknown error");
+ response.setHasMore(false);
+ return response;
+ }
+ }
+ }
+ """.formatted(
+ bizPackage,
+ bizPackage, baseName,
+ bizPackage, baseName,
+ bizPackage, baseName,
+ bizPackage, pagingInterface,
+ bizPackage,
+ baseName,
+ author,
+ createDate,
+ createDate, author,
+ baseName, baseName,
+ pagingInterface,
+ baseName, baseName,
+ toolName, pagingMode,
+ baseName,
+ baseName,
+ baseName,
+ baseName, baseName,
+ baseName, baseName
+ );
+ }
+ } else if (isMci) {
+ serviceImplContent = """
+ package %s.usecase.impl;
+
+ import %s.dto.%sRequest;
+ import %s.dto.%sResponse;
+ import %s.usecase.%sUseCase;
+ import io.shinhanlife.dat.lib.integration.mci.component.AxhubMciComponent;
+ import io.shinhanlife.glow.communication.dto.Transfer;
+ import io.shinhanlife.glow.BizException;
+ import org.springframework.stereotype.Service;
+ import lombok.RequiredArgsConstructor;
+ import lombok.extern.slf4j.Slf4j;
+ import %s.%s;
+ import %s.%s.io.%s_I;
+ import %s.%s.io.%s_O;
+ %s
+
+ /**
+ * @package %s.usecase.impl
+ * @className %sUseCaseImpl
+ * @description AX HUB 시스템 처리 클래스
+ * @author %s
+ * @create %s
+ *
+ * ---------- 개정이력 ----------
+ * 수정일 수정자 수정내용
+ * ---------- -------- ---------------------------
+ * %s %s 최초생성
+ *
+ *
+ */
+ @Slf4j
+ @Service
+ @RequiredArgsConstructor
+ public class %sUseCaseImpl implements %sUseCase {
+
+ %s
+ private final %s converter;
+
+ @Override
+ public %sResponse execute(%sRequest req) {
+ log.info("[MCI Tool] {} 요청 수신.", "%s");
+ try {
+ // MapStruct를 이용한 자동 매핑 (AI DTO -> MCI DTO)
+ %s_I mciReq = converter.toLegacyRequest(req);
+ Transfer<%s_O> resTransfer = null;
+
+ if (mciReq != null) {
+ resTransfer = mci.callTo(
+ "%s",
+ "%s",
+ mciReq,
+ %s_O.class
+ );
+ }
+
+ %sResponse response = new %sResponse();
+ if (resTransfer != null && resTransfer.getBody() != null) {
+ response = converter.toResponse(resTransfer.getBody());
+ }
+
+ response.setResultCode("SUCCESS");
+ response.setResultMessage(resTransfer != null && resTransfer.getBody() != null
+ ? "MCI call completed."
+ : "MCI call completed without a response body.");
+ return response;
+ } catch (BizException e) {
+ log.error("[MCI Tool] 연동 중 오류 발생: {}", e.getMessage(), e);
+ %sResponse response = new %sResponse();
+ response.setResultCode("ERROR");
+ response.setResultMessage(e.getMessage() != null ? e.getMessage() : "Unknown error");
+ return response;
+ } catch (Exception e) {
+ log.error("[MCI Tool] 연동 중 오류 발생: {}", e.getMessage(), e);
+ %sResponse response = new %sResponse();
+ response.setResultCode("ERROR");
+ response.setResultMessage(e.getMessage() != null ? e.getMessage() : "Unknown error");
+ return response;
+ }
+ }
+ }
+ """.formatted(
+ bizPackage,
+ bizPackage, baseName,
+ bizPackage, baseName,
+ bizPackage, baseName,
+ converterPackage, converterClassName,
+ BASE_PACKAGE, mciGroupPath.replace("/", "."), ioPrefix,
+ BASE_PACKAGE, mciGroupPath.replace("/", "."), ioPrefix,
+ (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;"),
+ converterClassName,
+ baseName,
+ baseName,
+ toolName,
+ ioPrefix,
+ ioPrefix,
+ interfaceId,
+ ((clientSystemCode != null && !clientSystemCode.isBlank()) ? clientSystemCode.toUpperCase() : ""),
+ ioPrefix,
+ baseName,
+ baseName,
+ baseName,
+ baseName,
+ baseName,
+ baseName
+ );
+ } else {
+ serviceImplContent = isHttp
+ ? httpUseCaseImplContent(bizPackage, baseName, httpGroupPath.replace("/", "."), httpApiClass, author, createDate)
+ : """
+ package %s.usecase.impl;
+
+ import %s.dto.%sRequest;
+ import %s.dto.%sResponse;
+ import %s.usecase.%sUseCase;
+ 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
+ *
+ * ---------- 개정이력 ----------
+ * 수정일 수정자 수정내용
+ * ---------- -------- ---------------------------
+ * %s %s 최초생성
+ *
+ *
+ */
+ @Slf4j
+ @Service
+ @RequiredArgsConstructor
+ public class %sUseCaseImpl implements %sUseCase {
+
+ private final %sConverter converter;
+
+ @Override
+ public %sResponse execute(%sRequest req) {
+ // %sLegacyRequest legacyRequest = converter.toLegacyRequest(req);
+ Object legacyResponse = null;
+ if (legacyResponse instanceof %sResponse response) {
+ return response;
+ }
+ %sResponse response = new %sResponse();
+ response.setResultCode("SUCCESS");
+ response.setResultMessage("Legacy call completed.");
+ return response;
+ }
+ }
+ """.formatted(
+ bizPackage,
+ bizPackage, baseName,
+ bizPackage, baseName,
+ bizPackage, baseName,
+ bizPackage, baseName,
+ bizPackage, baseName,
+ author,
+ createDate,
+ createDate, author,
+ baseName, baseName,
+ baseName,
+ baseName,
+ baseName,
+ baseName,
+ baseName,
+ baseName,
+ baseName,
+ routingType, interfaceId
+ );
+ }
+
+ serviceImplContent = serviceImplContent.replace("execute(", methodName + "(");
+ writeUtf8(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
+ *
+ * ---------- 개정이력 ----------
+ * 수정일 수정자 수정내용
+ * ---------- -------- ---------------------------
+ * %s %s 최초생성
+ *
+ *
+ */
+ @Data
+ public class %s_I {
+ // TODO: Add request fields here
+ /**
+ * EAI 시스템이 요구하는 수신자 번호 파라미터명
+ */
+ private String phone;
+
+ /**
+ * EAI 시스템이 요구하는 메시지 내용 파라미터명
+ */
+ private String content;
+ }
+ """.formatted(BASE_PACKAGE, mciGroupPath.replace("/", "."), BASE_PACKAGE, mciGroupPath.replace("/", "."), ioPrefix, author, createDate, createDate, author, ioPrefix);
+ mciReqContent = mciIoContent(mciGroupPath.replace("/", "."), ioPrefix + "_I", inputFields, author, createDate, pagingMode);
+ writeUtf8(mciIoDir.resolve(ioPrefix + "_I.java"), mciReqContent);
+ writeStructuredFieldTypes(mciIoDir, BASE_PACKAGE + "." + mciGroupPath.replace("/", ".") + ".io", ioPrefix + "_I", inputFields);
+
+ String mciResContent = """
+ package %s.%s.io;
+
+ import lombok.Data;
+
+ /**
+ * @package %s.%s.io
+ * @className %s_O
+ * @description AX HUB 시스템 처리 클래스
+ * @author %s
+ * @create %s
+ *
+ * ---------- 개정이력 ----------
+ * 수정일 수정자 수정내용
+ * ---------- -------- ---------------------------
+ * %s %s 최초생성
+ *
+ *
+ */
+ @Data
+ public class %s_O {
+ // TODO: Add response fields here
+ }
+ """.formatted(BASE_PACKAGE, mciGroupPath.replace("/", "."), BASE_PACKAGE, mciGroupPath.replace("/", "."), ioPrefix, author, createDate, createDate, author, ioPrefix);
+ mciResContent = mciIoContent(mciGroupPath.replace("/", "."), ioPrefix + "_O", outputFields, author, createDate, pagingMode);
+ writeUtf8(mciIoDir.resolve(ioPrefix + "_O.java"), mciResContent);
+ writeStructuredFieldTypes(mciIoDir, BASE_PACKAGE + "." + mciGroupPath.replace("/", ".") + ".io", ioPrefix + "_O", outputFields);
+
+ 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
+ *
+ * ---------- 개정이력 ----------
+ * 수정일 수정자 수정내용
+ * ---------- -------- ---------------------------
+ * %s %s 최초생성
+ *
+ *
+ */
+ @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("/", "."), ioPrefix,
+ BASE_PACKAGE, mciGroupPath.replace("/", "."), ioPrefix,
+ bizPackage, baseName, author, createDate, createDate, author,
+ baseName, ioPrefix, baseName,
+ baseName, ioPrefix,
+ baseName, ioPrefix
+ );
+ converterContent = mciConverterContent(converterPackage, bizPackage, baseName, mciGroupPath.replace("/", "."), ioPrefix, converterClassName);
+ writeUtf8(converterDir.resolve(converterClassName + ".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(converterClassName + ".java")).append("\n");
+
+ if (!clientPrefixCap.isEmpty()) {
+ String receiveServiceId = (clientSystemCode != null && !clientSystemCode.isBlank()) ? clientSystemCode.toUpperCase() : "";
+ String mciClientContent = """
+ package %s.%s;
+
+ import org.springframework.stereotype.Component;
+ import lombok.RequiredArgsConstructor;
+ import io.shinhanlife.dat.lib.integration.mci.component.AxhubMciComponent;
+ import io.shinhanlife.glow.communication.dto.Transfer;
+ import %s.%s.io.%s_I;
+ import %s.%s.io.%s_O;
+
+ /**
+ * @package %s.%s
+ * @className Mci%sClient
+ * @description AX HUB 시스템 처리 클래스
+ * @author %s
+ * @create %s
+ *
+ * ---------- 개정이력 ----------
+ * 수정일 수정자 수정내용
+ * ---------- -------- ---------------------------
+ * %s %s 최초생성
+ *
+ *
+ */
+ @Component
+ @RequiredArgsConstructor
+ public class Mci%sClient {
+
+ private static final String INTERFACE_ID = "%s";
+ private static final String RECEIVE_SERVICE_ID = "%s";
+
+ private final AxhubMciComponent mciComponent;
+
+ public Transfer callTo(String interfaceId, String receiveServiceId, Object mciReq, Class resType) {
+ return mciComponent.callTo(interfaceId, receiveServiceId, mciReq, resType);
+ }
+
+ public %s_O call%s(%s_I request) {
+ Transfer<%s_O> transfer = mciComponent.callTo(
+ INTERFACE_ID, RECEIVE_SERVICE_ID, request, %s_O.class);
+ return transfer == null ? null : transfer.getBody();
+ }
+ }
+ """.formatted(
+ BASE_PACKAGE, mciGroupPath.replace("/", "."),
+ BASE_PACKAGE, mciGroupPath.replace("/", "."), ioPrefix,
+ BASE_PACKAGE, mciGroupPath.replace("/", "."), ioPrefix,
+ BASE_PACKAGE, mciGroupPath.replace("/", "."), clientPrefixCap, author, createDate, createDate, author,
+ clientPrefixCap,
+ interfaceId, receiveServiceId,
+ ioPrefix, toPascalCase(ioPrefix), ioPrefix,
+ ioPrefix, ioPrefix
+ );
+ writeUtf8(mciClientDir.resolve("Mci" + clientPrefixCap + "Client.java"), mciClientContent);
+ log.append("[MCI Client] ").append(mciClientDir.resolve("Mci" + clientPrefixCap + "Client.java")).append("\n");
+ }
+
+ } else if (isHttp) {
+ String httpPackage = BASE_PACKAGE + "." + httpGroupPath.replace("/", ".");
+ String httpRequestClass = baseName + "HttpRequest";
+ String httpResponseClass = baseName + "HttpResponse";
+ String httpClientClass = httpApiClass + "Client";
+
+ writeUtf8(httpIoDir.resolve(httpRequestClass + ".java"),
+ dtoContent(httpPackage + ".io", httpRequestClass, inputFields, author, createDate, true));
+ writeUtf8(httpIoDir.resolve(httpResponseClass + ".java"),
+ dtoContent(httpPackage + ".io", httpResponseClass, outputFields, author, createDate, false));
+ writeStructuredFieldTypes(httpIoDir, httpPackage + ".io", httpRequestClass, inputFields);
+ writeStructuredFieldTypes(httpIoDir, httpPackage + ".io", httpResponseClass, outputFields);
+ writeUtf8(httpClientDir.resolve(httpClientClass + ".java"),
+ httpClientContent(httpPackage, httpClientClass, httpApiName));
+ writeUtf8(converterDir.resolve(baseName + "Converter.java"),
+ httpConverterContent(bizPackage, baseName, httpPackage));
+
+ log.append("\n=========================================\n");
+ log.append(" Scaffolding Complete! (Routing: HTTP)\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("[HTTP Request IO] ").append(httpIoDir.resolve(httpRequestClass + ".java")).append("\n");
+ log.append("[HTTP Response IO] ").append(httpIoDir.resolve(httpResponseClass + ".java")).append("\n");
+ log.append("[HTTP Client] ").append(httpClientDir.resolve(httpClientClass + ".java")).append("\n");
+ log.append("[HTTP Converter] ").append(converterDir.resolve(baseName + "Converter.java")).append("\n");
+ } else {
+ String legacyReqContent = """
+ package %s.legacy;
+
+ import lombok.Data;
+
+ /**
+ * @package %s.legacy
+ * @className %sLegacyRequest
+ * @description AX HUB 시스템 처리 클래스
+ * @author %s
+ * @create %s
+ *
+ * ---------- 개정이력 ----------
+ * 수정일 수정자 수정내용
+ * ---------- -------- ---------------------------
+ * %s %s 최초생성
+ *
+ *
+ */
+ @Data
+ public class %sLegacyRequest {
+ /**
+ * EAI 시스템이 요구하는 수신자 번호 파라미터명
+ */
+ private String phone;
+
+ /**
+ * EAI 시스템이 요구하는 메시지 내용 파라미터명
+ */
+ private String content;
+ }
+ """.formatted(bizPackage, bizPackage, baseName, author, createDate, createDate, author, baseName);
+ legacyReqContent = dtoContent(bizPackage + ".legacy", baseName + "LegacyRequest", inputFields, author, createDate, true);
+ writeUtf8(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
+ *
+ * ---------- 개정이력 ----------
+ * 수정일 수정자 수정내용
+ * ---------- -------- ---------------------------
+ * %s %s 최초생성
+ *
+ *
+ */
+ @Data
+ public class %sLegacyResponse {
+ // TODO: Add legacy response fields here
+ }
+ """.formatted(bizPackage, bizPackage, baseName, author, createDate, createDate, author, baseName);
+ legacyResContent = dtoContent(bizPackage + ".legacy", baseName + "LegacyResponse", outputFields, author, createDate, true);
+ writeUtf8(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
+ *
+ * ---------- 개정이력 ----------
+ * 수정일 수정자 수정내용
+ * ---------- -------- ---------------------------
+ * %s %s 최초생성
+ *
+ *
+ */
+ @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
+ );
+ converterContent = legacyConverterContent(bizPackage, baseName);
+ writeUtf8(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");
+ }
+ // Schema Resource 파일 생성(useSchemaResource=true일 때)
+ if (useSchemaResource) {
+ Files.createDirectories(schemaDir);
+ String inputSchema = """
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "TODO_FIELD": {
+ "type": "string",
+ "description": "TODO: 파라미터 설명을 입력하세요."
+ }
+ },
+ "required": []
+ }
+ """;
+ String outputSchema = """
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "status": {
+ "type": "string",
+ "description": "처리 결과 상태 (SUCCESS / FAILURE)",
+ "enum": ["SUCCESS", "FAILURE"]
+ },
+ "message": {
+ "type": "string",
+ "description": "처리 결과 메시지"
+ }
+ },
+ "required": ["status"]
+ }
+ """;
+ writeUtf8(schemaDir.resolve(inputSchemaFileName), inputSchema);
+ writeUtf8(schemaDir.resolve(outputSchemaFileName), outputSchema);
+ log.append("[Input Schema] ").append(schemaDir.resolve(inputSchemaFileName)).append("\n");
+ log.append("[Output Schema] ").append(schemaDir.resolve(outputSchemaFileName)).append("\n");
+ }
+
+ // Response JSON mock files are intentionally not generated. Runtime response contracts are represented by DTOs.
+ if (isHttp) {
+ Path moduleRoot = rootDir.resolve(moduleName).toAbsolutePath().normalize();
+ Path projectRoot = moduleRoot.getParent();
+ Path glowConfig = ensureLocalHttpApiConfiguration(projectRoot, httpApiName, toolName);
+ log.append("[HTTP Config] ").append(glowConfig).append("\n");
+ }
+
+ Path generatedTestDir = rootDir.resolve(Paths.get(moduleName, "src/test/java/io/shinhanlife/dat/mcc/biz", group.toLowerCase(), "usecase"));
+ Files.createDirectories(generatedTestDir);
+ Path generatedTestPath = generatedTestDir.resolve(baseName + "UseCaseTest.java");
+ writeUtf8(generatedTestPath, useCaseTestContent(bizPackage, baseName));
+ log.append("[Unit Test] ").append(generatedTestPath).append("\\n");
+ log.append("[Test Command] .\\gradlew.bat :").append(moduleName.substring(moduleName.lastIndexOf(java.io.File.separator) + 1)).append(":test --tests \"*").append(baseName).append("UseCaseTest\"\\n");
+ // Tool Definition YML is no longer generated. We use @GrowToolHint instead.
+ // log.append("[V17 Tool Definition] ...\n");
+ log.append("\n Tip: HTTP Tool은 WireMock 실행 후 생성된 mapping URL로 호출을 확인하세요.\n");
+
+ return log.toString();
+ }
+
+ private static String toolDefinitionContent(String toolName, String title, String description,
+ String categoryKey, String interfaceId,
+ List inputFields, boolean mutation) {
+ String safeDescription = description == null || description.isBlank()
+ ? title + " 기능을 수행한다." : description;
+ StringBuilder properties = new StringBuilder();
+ StringBuilder required = new StringBuilder();
+ Set generatedNames = new LinkedHashSet<>();
+ for (FieldDefinition field : inputFields == null ? List.of() : inputFields) {
+ if (field == null || field.name() == null || field.name().isBlank()
+ || !generatedNames.add(field.name().trim())) {
+ continue;
+ }
+ appendSchemaProperty(properties, field);
+ if (field.required()) {
+ required.append(" - ").append(field.name()).append("\n");
+ }
+ }
+ if (properties.isEmpty()) {
+ properties.append(" {}\n");
+ }
+ String requiredBlock = required.isEmpty() ? "" : " required:\n" + required;
+ String legacyLine = interfaceId == null || interfaceId.isBlank()
+ ? "" : "legacy_interface_id: " + yamlText(interfaceId) + "\n";
+ return """
+ name: %s
+ display_name: %s
+ version: 1.0.0
+ category_key: %s
+ description:
+ function: %s
+ when_to_use: 사용자가 이 업무 기능의 실행 또는 조회를 명확히 요청한 경우 사용한다.
+ when_not_to_use: 입력값이 확인되지 않았거나 다른 업무 기능이 더 적합한 경우에는 사용하지 않는다.
+ io_limits: 정의된 입력 항목만 허용하며 응답 DTO에 정의된 업무 결과만 반환한다.
+ display_description: %s
+ example_queries:
+ - %s 처리해줘
+ - %s 정보를 확인해줘
+ - %s 업무 결과를 알려줘
+ read_only: %s
+ destructive: %s
+ idempotent: %s
+ parameters_schema:
+ type: object
+ properties:
+ %s%s additionalProperties: false
+ tags: [%s]
+ %srequired_env_keys: []
+ owner_org: MCP_TOOL
+ """.formatted(toolName, yamlText(title), categoryKey.toLowerCase(Locale.ROOT),
+ yamlText(safeDescription), yamlText(title), yamlText(title), yamlText(title), yamlText(title),
+ !mutation, mutation, !mutation, properties, requiredBlock,
+ categoryKey.toLowerCase(Locale.ROOT), legacyLine);
+ }
+
+ private static String toolDefinitionContentV17(String toolName, String title, String description,
+ String categoryKey, String interfaceId,
+ List inputFields, boolean mutation,
+ ToolDefinitionOptions options) {
+ String function = option(options == null ? null : options.functionDescription(),
+ option(description, title + " 기능을 수행한다."));
+ String whenToUse = option(options == null ? null : options.whenToUse(),
+ "사용자가 해당 업무 기능의 실행 또는 조회를 명확히 요청한 경우 사용한다.");
+ String whenNotToUse = option(options == null ? null : options.whenNotToUse(),
+ "필수 입력값이 확인되지 않았거나 다른 업무 기능이 더 적합한 경우에는 사용하지 않는다.");
+ String ioLimits = option(options == null ? null : options.ioLimits(),
+ "정의된 입력 항목만 허용하며 응답 DTO에 정의된 업무 결과만 반환한다.");
+ String displayDescription = option(options == null ? null : options.displayDescription(), title);
+ List examples = normalizedList(options == null ? null : options.exampleQueries(), List.of(
+ title + " 처리해줘", title + " 정보를 확인해줘", title + " 업무 결과를 알려줘"));
+ List tags = normalizedList(options == null ? null : options.tags(),
+ List.of(categoryKey.toLowerCase(Locale.ROOT)));
+ String ownerOrg = option(options == null ? null : options.ownerOrg(), "MCP_TOOL");
+
+ StringBuilder properties = new StringBuilder();
+ StringBuilder required = new StringBuilder();
+ Set generatedNames = new LinkedHashSet<>();
+ for (FieldDefinition field : inputFields == null ? List.of() : inputFields) {
+ if (field == null || field.name() == null || field.name().isBlank()
+ || !generatedNames.add(field.name().trim())) {
+ continue;
+ }
+ appendSchemaProperty(properties, field);
+ if (field.required()) {
+ required.append(" - ").append(field.name().trim()).append("\n");
+ }
+ }
+ if (properties.isEmpty()) {
+ properties.append(" {}\n");
+ }
+ String requiredBlock = required.isEmpty() ? "" : " required:\n" + required;
+ String legacyLine = interfaceId == null || interfaceId.isBlank()
+ ? "" : "legacy_interface_id: " + yamlText(interfaceId) + "\n";
+ String exampleBlock = examples.stream().map(value -> " - " + yamlText(value))
+ .collect(java.util.stream.Collectors.joining("\n"));
+ String tagBlock = tags.stream().map(ToolScaffolder::yamlText)
+ .collect(java.util.stream.Collectors.joining(", "));
+
+ return """
+ name: %s
+ display_name: %s
+ version: 1.0.0
+ category_key: %s
+ description:
+ function: %s
+ when_to_use: %s
+ when_not_to_use: %s
+ io_limits: %s
+ display_description: %s
+ example_queries:
+ %s
+ read_only: %s
+ destructive: %s
+ idempotent: %s
+ parameters_schema:
+ type: object
+ properties:
+ %s%s additionalProperties: false
+ tags: [%s]
+ %srequired_env_keys: []
+ owner_org: %s
+ """.formatted(toolName, yamlText(title), categoryKey.toLowerCase(Locale.ROOT),
+ yamlText(function), yamlText(whenToUse), yamlText(whenNotToUse), yamlText(ioLimits),
+ yamlText(displayDescription), exampleBlock, !mutation, mutation, !mutation,
+ properties, requiredBlock, tagBlock, legacyLine, yamlText(ownerOrg));
+ }
+
+ private static String option(String value, String fallback) {
+ return value == null || value.isBlank() ? fallback : value.trim();
+ }
+
+ private static List normalizedList(List values, List fallback) {
+ if (values == null) {
+ return fallback;
+ }
+ List normalized = values.stream()
+ .filter(java.util.Objects::nonNull)
+ .map(String::trim)
+ .filter(value -> !value.isBlank())
+ .distinct()
+ .toList();
+ return normalized.isEmpty() ? fallback : normalized;
+ }
+
+ private static boolean isMutationTool(String baseName) {
+ String value = baseName.toLowerCase(Locale.ROOT);
+ return value.matches(".*(create|add|update|delete|remove|send|process|approve|reject|register|issue).*");
+ }
+
+ private static String jsonSchemaType(String javaType) {
+ return switch (javaType == null ? "String" : javaType) {
+ case "Integer", "Long" -> "integer";
+ case "Double", "BigDecimal" -> "number";
+ case "Boolean" -> "boolean";
+ case "List" -> "array";
+ default -> "string";
+ };
+ }
+
+ private static void appendSchemaProperty(StringBuilder properties, FieldDefinition field) {
+ properties.append(" ").append(field.name().trim()).append(":\n")
+ .append(" type: ").append(jsonSchemaType(field.type())).append("\n")
+ .append(" description: ").append(yamlText(richDescription(field))).append("\n");
+ if ("Enum".equals(field.type()) && field.enumValues() != null && !field.enumValues().isEmpty()) {
+ properties.append(" enum: [").append(field.enumValues().stream()
+ .filter(value -> value != null && !value.isBlank()).map(String::trim)
+ .collect(java.util.stream.Collectors.joining(", "))).append("]\n");
+ }
+ if ("List".equals(field.type())) {
+ properties.append(" items:\n")
+ .append(" type: ").append("Object".equals(field.itemType()) ? "object" : jsonSchemaType(field.itemType())).append("\n");
+ if ("Object".equals(field.itemType()) && field.itemFields() != null && !field.itemFields().isEmpty()) {
+ properties.append(" properties:\n");
+ for (FieldDefinition itemField : field.itemFields()) {
+ properties.append(" ").append(itemField.name()).append(":\n")
+ .append(" type: ").append(jsonSchemaType(itemField.type())).append("\n")
+ .append(" description: ").append(yamlText(richDescription(itemField))).append("\n");
+ }
+ }
+ }
+ }
+
+ private static String yamlText(String value) {
+ String safe = value == null ? "" : value.replace("\\", "\\\\").replace("\"", "\\\"")
+ .replace("\r", " ").replace("\n", " ");
+ return "\"" + safe + "\"";
+ }
+
+ private static String toKebabCase(String pascalCase) {
+ if (pascalCase == null || pascalCase.isEmpty()) return pascalCase;
+ return pascalCase
+ .replaceAll("([a-z0-9])([A-Z])", "$1-$2")
+ .toLowerCase(Locale.ROOT);
+ }
+
+ private static Path findGlowLocalConfigPath(Path startPath) {
+ String sourceDir = System.getProperty("AXHUB_SOURCE_DIR");
+ if (sourceDir == null) {
+ sourceDir = System.getenv("AXHUB_SOURCE_DIR");
+ }
+
+ List baseDirs = new ArrayList<>();
+ if (startPath != null) {
+ baseDirs.add(startPath);
+ if (startPath.getParent() != null) {
+ baseDirs.add(startPath.getParent());
+ }
+ }
+ if (sourceDir != null && !sourceDir.isBlank()) {
+ baseDirs.add(Paths.get(sourceDir.trim()));
+ }
+ baseDirs.add(Paths.get("."));
+
+ String[] subPaths = {
+ "dat-was-lib/src/main/resources/glow",
+ "src/main/resources/glow"
+ };
+ String[] fileNames = {
+ "application-glow-local.yml",
+ "application-glow-local.yaml",
+ "application-glow-local.xml"
+ };
+
+ // 1. 이미 존재하는 파일 우선 탐색 (dat-was-lib 우선)
+ for (Path base : baseDirs) {
+ for (String sub : subPaths) {
+ for (String name : fileNames) {
+ Path candidate = base.resolve(sub).resolve(name).normalize();
+ if (Files.exists(candidate)) {
+ return candidate;
+ }
+ }
+ }
+ }
+
+ // 2. dat-was-lib 디렉토리가 존재하는 디렉토리 찾기
+ for (Path base : baseDirs) {
+ Path libDir = base.resolve("dat-was-lib");
+ if (Files.isDirectory(libDir)) {
+ return libDir.resolve("src/main/resources/glow/application-glow-local.yml").normalize();
+ }
+ }
+
+ // 3. startPath 형제(sibling)로 dat-was-lib 찾기
+ if (startPath != null) {
+ Path libSibling = startPath.resolveSibling("dat-was-lib");
+ if (Files.isDirectory(libSibling)) {
+ return libSibling.resolve("src/main/resources/glow/application-glow-local.yml").normalize();
+ }
+ }
+
+ // 4. Fallback
+ if (sourceDir != null && !sourceDir.isBlank()) {
+ return Paths.get(sourceDir.trim()).resolve("dat-was-lib/src/main/resources/glow/application-glow-local.yml").normalize();
+ }
+ return Paths.get("dat-was-lib/src/main/resources/glow/application-glow-local.yml").normalize();
+ }
+
+ private static Path ensureLocalHttpApiConfiguration(Path projectRoot, String httpApiName, String toolName) throws IOException {
+ Path localConfigPath = findGlowLocalConfigPath(projectRoot);
+ Files.createDirectories(localConfigPath.getParent());
+ String existing = Files.exists(localConfigPath) ? Files.readString(localConfigPath, StandardCharsets.UTF_8) : "";
+ if (java.util.regex.Pattern.compile("(?m)^\\s*-\\s+name:\\s*"
+ + java.util.regex.Pattern.quote(httpApiName) + "\\s*$").matcher(existing).find()) {
+ return localConfigPath;
+ }
+ String environmentKey = toPackageSegment(httpApiName).toUpperCase(Locale.ROOT).replace('-', '_');
+ String apiEntry = """
+ - name: %s
+ domain: ${AXHUB_%s_HTTP_DOMAIN:http://localhost:${server.port}}
+ url: ${AXHUB_%s_HTTP_URL:/api/mock/http/%s}
+ method: POST
+ content-type: application/json;charset=UTF-8
+ biz-pod: false
+ """.formatted(httpApiName, environmentKey, environmentKey, toolName).stripTrailing() + "\n";
+ boolean isCrlf = existing.contains("\r\n");
+ String formattedApiEntry = isCrlf ? apiEntry.replace("\n", "\r\n") : apiEntry;
+
+ if (existing.isBlank()) {
+ existing = """
+ spring:
+ config:
+ activate:
+ on-profile: local
+
+ glow:
+ communication:
+ http:
+ api-list:
+ """ + apiEntry;
+ if (isCrlf) {
+ existing = existing.replace("\n", "\r\n");
+ }
+ } else if (existing.contains("\r\n mci:")) {
+ existing = existing.replace("\r\n mci:", "\r\n" + formattedApiEntry + " mci:");
+ } else if (existing.contains("\n mci:")) {
+ existing = existing.replace("\n mci:", "\n" + formattedApiEntry + " mci:");
+ } else if (existing.contains("\r\naxhub:")) {
+ existing = existing.replace("\r\naxhub:", "\r\n" + formattedApiEntry + "axhub:");
+ } else if (existing.contains("\naxhub:")) {
+ existing = existing.replace("\naxhub:", "\n" + formattedApiEntry + "axhub:");
+ } else if (existing.contains("api-list:")) {
+ existing += formattedApiEntry;
+ } else {
+ throw new IllegalStateException("application-glow-local.yml must define glow.communication.http.api-list");
+ }
+ if (!existing.contains("axhub:\n mock:\n http:\n enabled: true")
+ && !existing.contains("axhub:\r\n mock:\r\n http:\r\n enabled: true")) {
+ String mockConfig = """
+
+ axhub:
+ mock:
+ http:
+ enabled: true
+ """;
+ existing += isCrlf ? mockConfig.replace("\n", "\r\n") : mockConfig;
+ }
+ writeUtf8(localConfigPath, existing);
+ return localConfigPath;
+ }
+
+ private static void writePagingComponents(Path pagingDir, String bizPackage,
+ String baseName, PagingMode pagingMode,
+ String ioPackage, String ioPrefix,
+ String clientClassName, String converterPkg,
+ String converterName, String interfaceId,
+ String receiveServiceId) throws IOException {
+ Files.createDirectories(pagingDir);
+ Path implDir = pagingDir.resolve("impl");
+ Files.createDirectories(implDir);
+
+ String pagingInfoType = pagingMode == PagingMode.SCROLL ? "ScrollPagingInfo" : "PgNumPagingInfo";
+ String pagingSuffix = pagingMode == PagingMode.SCROLL ? "ScrollPaging" : "PageNumberPaging";
+ String interfaceName = baseName + pagingSuffix;
+ String implName = interfaceName + "Impl";
+ String clientImport = clientClassName.equals("AxhubMciComponent")
+ ? "import io.shinhanlife.dat.lib.integration.mci.component.AxhubMciComponent;"
+ : "import " + ioPackage + "." + clientClassName + ";";
+
+ String interfaceSource = """
+ package %s.paging;
+
+ import %s.dto.%sRequest;
+ import %s.dto.%sResponse;
+ import io.shinhanlife.dat.lib.paging.MciPage;
+ import io.shinhanlife.dat.lib.paging.%s;
+
+ /**
+ * @package %s.paging
+ * @className %s
+ * @description AX HUB 시스템 처리 클래스
+ * @author 0986406
+ * @create 2026.09.01
+ *
+ * ---------- 개정이력 ----------
+ * 수정일 수정자 수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01 0986406 최초생성
+ *
+ *
+ */
+ public interface %s {
+
+ MciPage<%sResponse, %s> fetch(%sRequest request, %s pagingInfo);
+ }
+ """.formatted(bizPackage, bizPackage, baseName, bizPackage, baseName, pagingInfoType,
+ bizPackage, interfaceName, interfaceName, baseName, pagingInfoType, baseName, pagingInfoType);
+ writeUtf8(pagingDir.resolve(interfaceName + ".java"), interfaceSource);
+
+ String implSource;
+ if (pagingMode == PagingMode.SCROLL) {
+ implSource = """
+ package %s.paging.impl;
+
+ import %s.dto.%sRequest;
+ import %s.dto.%sResponse;
+ import %s.paging.%s;
+ import %s.%s;
+ %s
+ import %s.io.%s_I;
+ import %s.io.%s_O;
+ import io.shinhanlife.dat.lib.paging.MciPage;
+ import io.shinhanlife.dat.lib.paging.ScrollPagingInfo;
+ import io.shinhanlife.glow.db.dto.ScrPageInfo;
+ import io.shinhanlife.glow.communication.dto.Transfer;
+ import java.lang.reflect.Method;
+ import java.util.List;
+ import lombok.RequiredArgsConstructor;
+ import lombok.extern.slf4j.Slf4j;
+ import org.springframework.stereotype.Component;
+
+ /**
+ * @package %s.paging.impl
+ * @className %s
+ * @description AX HUB 시스템 처리 클래스
+ * @author 0986406
+ * @create 2026.09.01
+ *
+ * ---------- 개정이력 ----------
+ * 수정일 수정자 수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01 0986406 최초생성
+ *
+ *
+ */
+ @Slf4j
+ @Component
+ @RequiredArgsConstructor
+ public class %s implements %s {
+
+ private final %s mci;
+ private final %s converter;
+
+ @Override
+ public MciPage<%sResponse, ScrollPagingInfo> fetch(%sRequest request, ScrollPagingInfo pagingInfo) {
+ if (request.getScrPageInfo() == null) {
+ request.setScrPageInfo(new ScrPageInfo());
+ }
+ if (request.getScrPageInfo().getPageDataCc() <= 0) {
+ request.getScrPageInfo().setPageDataCc(20);
+ }
+ if (pagingInfo != null) {
+ if (pagingInfo.getScrlmhdNm() != null) request.getScrPageInfo().setScrlmhdNm(pagingInfo.getScrlmhdNm());
+ if (pagingInfo.getScrlItva() != null) request.getScrPageInfo().setScrlItva(pagingInfo.getScrlItva());
+ if (pagingInfo.getScrSortValu() != null) request.getScrPageInfo().setScrSortValu(pagingInfo.getScrSortValu());
+ if (pagingInfo.getPageDataCc() > 0) request.getScrPageInfo().setPageDataCc(pagingInfo.getPageDataCc());
+ }
+
+ %s_I mciReq = converter.toRequest(request);
+ if (mciReq != null && request.getScrPageInfo() != null) {
+ try {
+ Method setMethod = mciReq.getClass().getMethod("setScrPageInfo", List.class);
+ setMethod.invoke(mciReq, List.of(request.getScrPageInfo()));
+ } catch (NoSuchMethodException e) {
+ try {
+ Method setMethod = mciReq.getClass().getMethod("setScrPageInfo", ScrPageInfo.class);
+ setMethod.invoke(mciReq, request.getScrPageInfo());
+ } catch (Exception ignored) {}
+ } catch (Exception ignored) {}
+ }
+
+ Transfer<%s_O> resTransfer = mci.callTo("%s", "%s", mciReq, %s_O.class);
+ %s_O mciRes = resTransfer != null ? resTransfer.getBody() : null;
+
+ %sResponse response = converter.toResponse(mciRes);
+
+ ScrPageInfo resPageInfo = null;
+ if (mciRes != null) {
+ try {
+ Method getMethod = mciRes.getClass().getMethod("getScrPageInfo");
+ Object val = getMethod.invoke(mciRes);
+ if (val instanceof List> list && !list.isEmpty()) {
+ if (list.get(0) instanceof ScrPageInfo pi) resPageInfo = pi;
+ } else if (val instanceof ScrPageInfo pi) {
+ resPageInfo = pi;
+ }
+ } catch (Exception ignored) {}
+ }
+
+ boolean hasNext = resPageInfo != null && "Y".equalsIgnoreCase(resPageInfo.getNextDataExtYn());
+ String nextSortValu = resPageInfo != null ? resPageInfo.getScrSortValu() : null;
+ String nextItva = resPageInfo != null ? resPageInfo.getScrlItva() : null;
+
+ if (response != null) {
+ response.setHasMore(hasNext);
+ }
+
+ ScrollPagingInfo nextPaging = new ScrollPagingInfo(
+ request.getScrPageInfo().getScrlmhdNm(),
+ nextItva,
+ nextSortValu,
+ hasNext,
+ request.getScrPageInfo().getPageDataCc()
+ );
+
+ return new MciPage<>(response, nextPaging);
+ }
+ }
+ """.formatted(
+ bizPackage,
+ bizPackage, baseName,
+ bizPackage, baseName,
+ bizPackage, interfaceName,
+ converterPkg, converterName,
+ clientImport,
+ ioPackage, ioPrefix,
+ ioPackage, ioPrefix,
+ bizPackage, implName,
+ implName, interfaceName,
+ clientClassName, converterName,
+ baseName, baseName,
+ ioPrefix,
+ ioPrefix, interfaceId, receiveServiceId, ioPrefix,
+ ioPrefix,
+ baseName
+ );
+ } else {
+ implSource = """
+ package %s.paging.impl;
+
+ import %s.dto.%sRequest;
+ import %s.dto.%sResponse;
+ import %s.paging.%s;
+ import %s.%s;
+ %s
+ import %s.io.%s_I;
+ import %s.io.%s_O;
+ import io.shinhanlife.dat.lib.paging.MciPage;
+ import io.shinhanlife.dat.lib.paging.PgNumPagingInfo;
+ import io.shinhanlife.glow.db.dto.PageInfo;
+ import io.shinhanlife.glow.communication.dto.Transfer;
+ import java.lang.reflect.Method;
+ import java.util.List;
+ import lombok.RequiredArgsConstructor;
+ import lombok.extern.slf4j.Slf4j;
+ import org.springframework.stereotype.Component;
+
+ /**
+ * @package %s.paging.impl
+ * @className %s
+ * @description AX HUB 시스템 처리 클래스
+ * @author 0986406
+ * @create 2026.09.01
+ *
+ * ---------- 개정이력 ----------
+ * 수정일 수정자 수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01 0986406 최초생성
+ *
+ *
+ */
+ @Slf4j
+ @Component
+ @RequiredArgsConstructor
+ public class %s implements %s {
+
+ private final %s mci;
+ private final %s converter;
+
+ @Override
+ public MciPage<%sResponse, PgNumPagingInfo> fetch(%sRequest request, PgNumPagingInfo pagingInfo) {
+ if (request.getPageInfo() == null) {
+ request.setPageInfo(new PageInfo());
+ }
+ if (request.getPageInfo().getPageNo() <= 0) {
+ request.getPageInfo().setPageNo(1);
+ }
+ if (request.getPageInfo().getPageDataCc() <= 0) {
+ request.getPageInfo().setPageDataCc(20);
+ }
+ if (pagingInfo != null) {
+ if (pagingInfo.getPageNo() > 0) request.getPageInfo().setPageNo(pagingInfo.getPageNo());
+ if (pagingInfo.getPageDataCc() > 0) request.getPageInfo().setPageDataCc(pagingInfo.getPageDataCc());
+ }
+
+ %s_I mciReq = converter.toRequest(request);
+ if (mciReq != null) {
+ try {
+ Method setMethod = mciReq.getClass().getMethod("setPageInfo", List.class);
+ setMethod.invoke(mciReq, List.of(request.getPageInfo()));
+ } catch (NoSuchMethodException e) {
+ try {
+ Method setMethod = mciReq.getClass().getMethod("setPageInfo", PageInfo.class);
+ setMethod.invoke(mciReq, request.getPageInfo());
+ } catch (Exception ignored) {}
+ } catch (Exception ignored) {}
+ }
+
+ Transfer<%s_O> resTransfer = mci.callTo("%s", "%s", mciReq, %s_O.class);
+ %s_O mciRes = resTransfer != null ? resTransfer.getBody() : null;
+
+ %sResponse response = converter.toResponse(mciRes);
+
+ PageInfo resPageInfo = null;
+ if (mciRes != null) {
+ try {
+ Method getMethod = mciRes.getClass().getMethod("getPageInfo");
+ Object val = getMethod.invoke(mciRes);
+ if (val instanceof List> list && !list.isEmpty()) {
+ if (list.get(0) instanceof PageInfo pi) resPageInfo = pi;
+ } else if (val instanceof PageInfo pi) {
+ resPageInfo = pi;
+ }
+ } catch (Exception ignored) {}
+ }
+
+ int currentPageNo = resPageInfo != null && resPageInfo.getPageNo() > 0 ? resPageInfo.getPageNo() : (pagingInfo != null ? pagingInfo.getPageNo() : 1);
+ int totalPageCn = resPageInfo != null ? resPageInfo.getTotaPageCn() : 0;
+ int totalDataCc = resPageInfo != null ? resPageInfo.getTotaPageDataCc() : 0;
+ int pageDataCc = resPageInfo != null && resPageInfo.getPageDataCc() > 0 ? resPageInfo.getPageDataCc() : (pagingInfo != null ? pagingInfo.getPageDataCc() : 20);
+
+ boolean hasNext = false;
+ if (totalPageCn > 0) {
+ hasNext = currentPageNo < totalPageCn;
+ } else if (totalDataCc > 0 && pageDataCc > 0) {
+ hasNext = (long) currentPageNo * pageDataCc < totalDataCc;
+ }
+
+ if (response != null) {
+ response.setPageNo(currentPageNo);
+ response.setTotalPageCount(totalPageCn);
+ response.setTotalCount(totalDataCc);
+ response.setHasMore(hasNext);
+ }
+
+ PgNumPagingInfo nextPaging = new PgNumPagingInfo(
+ currentPageNo + 1,
+ pageDataCc,
+ totalPageCn,
+ totalDataCc,
+ hasNext
+ );
+
+ return new MciPage<>(response, nextPaging);
+ }
+ }
+ """.formatted(
+ bizPackage,
+ bizPackage, baseName,
+ bizPackage, baseName,
+ bizPackage, interfaceName,
+ converterPkg, converterName,
+ clientImport,
+ ioPackage, ioPrefix,
+ ioPackage, ioPrefix,
+ bizPackage, implName,
+ implName, interfaceName,
+ clientClassName, converterName,
+ baseName, baseName,
+ ioPrefix,
+ ioPrefix, interfaceId, receiveServiceId, ioPrefix,
+ ioPrefix,
+ baseName
+ );
+ }
+ writeUtf8(implDir.resolve(implName + ".java"), implSource);
+ }
+
+ private static void writeUtf8(Path path, String content) throws IOException {
+ Files.writeString(path, content, StandardCharsets.UTF_8);
+ }
+
+ private static String dtoContent(String packageName, String className, List fields,
+ String author, String createDate, boolean request) {
+ return dtoContent(packageName, className, fields, author, createDate, request, PagingMode.NONE);
+ }
+
+ private static String dtoContent(String packageName, String className, List fields,
+ String author, String createDate, boolean request, PagingMode pagingMode) {
+ Set excludedFields;
+ if (request) {
+ if (pagingMode == PagingMode.PAGE_NUMBER) {
+ excludedFields = Set.of("pageInfo");
+ } else if (pagingMode == PagingMode.SCROLL) {
+ excludedFields = Set.of("scrPageInfo");
+ } else {
+ excludedFields = Set.of();
+ }
+ } else {
+ if (pagingMode == PagingMode.PAGE_NUMBER) {
+ excludedFields = Set.of("resultCode", "resultMessage", "pageNo", "totalPageCount", "totalCount", "hasMore");
+ } else if (pagingMode == PagingMode.SCROLL) {
+ excludedFields = Set.of("resultCode", "resultMessage", "hasMore");
+ } else {
+ excludedFields = Set.of("resultCode", "resultMessage");
+ }
+ }
+ String body = fieldLines(fields, excludedFields, className);
+ if (!request) {
+ String pagingResponseFields = "";
+ if (pagingMode == PagingMode.PAGE_NUMBER) {
+ pagingResponseFields = """
+ @Schema(description = "현재 페이지 번호")
+ private Integer pageNo;
+
+ @Schema(description = "총 페이지 수")
+ private Integer totalPageCount;
+
+ @Schema(description = "총 데이터 건수")
+ private Integer totalCount;
+
+ @Schema(description = "추가 데이터 존재 여부 (기본 최대 조회 건수 초과 시 true)")
+ private Boolean hasMore;
+
+ """;
+ } else if (pagingMode == PagingMode.SCROLL) {
+ pagingResponseFields = """
+ @Schema(description = "추가 데이터 존재 여부 (기본 최대 조회 건수 초과 시 true)")
+ private Boolean hasMore;
+
+ """;
+ }
+ body = " private String resultCode;\n\n private String resultMessage;\n\n" + pagingResponseFields + body;
+ }
+ String listImport = hasListField(fields) ? "import java.util.List;\n" : "";
+ String patternImport = hasPatternField(fields) ? "import jakarta.validation.constraints.Pattern;\n" : "";
+ String pagingImport = "";
+ String pagingField = "";
+ if (request && pagingMode != null && pagingMode != PagingMode.NONE) {
+ if (pagingMode == PagingMode.PAGE_NUMBER) {
+ pagingImport = "import io.shinhanlife.glow.db.dto.PageInfo;\n";
+ pagingField = " @Schema(description = \"페이지 정보\")\n private PageInfo pageInfo;\n\n";
+ } else if (pagingMode == PagingMode.SCROLL) {
+ pagingImport = "import io.shinhanlife.glow.db.dto.ScrPageInfo;\n";
+ pagingField = " @Schema(description = \"스크롤 페이지 정보\")\n private ScrPageInfo scrPageInfo;\n\n";
+ }
+ }
+ return """
+ package %s;
+
+ import com.fasterxml.jackson.annotation.JsonInclude;
+ import io.swagger.v3.oas.annotations.media.Schema;
+ import lombok.Data;
+ %s%s%s
+ @Data
+ @JsonInclude(JsonInclude.Include.NON_NULL)
+ public class %s {
+ %s%s%s}
+ """.formatted(packageName, listImport, patternImport, pagingImport, className, pagingField, body, innerObjectListClasses(fields));
+ }
+
+ private static String mciIoContent(String packageSuffix, String className, List fields,
+ String author, String createDate) {
+ return mciIoContent(packageSuffix, className, fields, author, createDate, PagingMode.NONE);
+ }
+
+ private static String mciIoContent(String packageSuffix, String className, List fields,
+ String author, String createDate, PagingMode pagingMode) {
+ String listImport = hasListField(fields) ? "import java.util.List;\n" : "";
+ String patternImport = hasPatternField(fields) ? "import jakarta.validation.constraints.Pattern;\n" : "";
+ String pagingImport = "";
+ String pagingField = "";
+ Set excluded = Set.of();
+ if ((className.endsWith("_I") || className.endsWith("_O")) && pagingMode != null && pagingMode != PagingMode.NONE) {
+ boolean hasPageInfo = fields != null && fields.stream().anyMatch(f -> f != null && "pageInfo".equalsIgnoreCase(f.name()));
+ boolean hasScrPageInfo = fields != null && fields.stream().anyMatch(f -> f != null && "scrPageInfo".equalsIgnoreCase(f.name()));
+ if (pagingMode == PagingMode.PAGE_NUMBER) {
+ excluded = Set.of("pageInfo");
+ if (!hasPageInfo) {
+ pagingImport = "import io.shinhanlife.glow.GlowTrgmField;\nimport io.shinhanlife.glow.db.dto.PageInfo;\n";
+ if (!listImport.contains("List")) {
+ pagingImport += "import java.util.List;\n";
+ }
+ pagingField = " @Schema(description = \"페이지 정보\")\n @GlowTrgmField(order = 1, description = \"페이지 정보\", type = \"gm\")\n private List pageInfo;\n\n";
+ }
+ } else if (pagingMode == PagingMode.SCROLL) {
+ excluded = Set.of("scrPageInfo");
+ if (!hasScrPageInfo) {
+ pagingImport = "import io.shinhanlife.glow.GlowTrgmField;\nimport io.shinhanlife.glow.db.dto.ScrPageInfo;\n";
+ pagingField = " @Schema(description = \"스크롤 페이지 정보\")\n @GlowTrgmField(order = 1, length = 306, description = \"스크롤 페이지 정보\")\n private ScrPageInfo scrPageInfo;\n\n";
+ }
+ }
+ }
+ return """
+ package %s.%s.io;
+
+ import io.swagger.v3.oas.annotations.media.Schema;
+ import lombok.Data;
+ %s%s%s
+ @Data
+ public class %s {
+ %s%s%s}
+ """.formatted(BASE_PACKAGE, packageSuffix, listImport, patternImport, pagingImport, className, pagingField, fieldLines(fields, excluded, className),
+ innerObjectListClasses(fields));
+ }
+
+ private static boolean hasListField(List fields) {
+ return fields != null && fields.stream().anyMatch(field -> field != null && "List".equals(field.type()));
+ }
+
+ private static boolean hasPatternField(List fields) {
+ if (fields == null) return false;
+ return fields.stream().anyMatch(field -> {
+ if (field == null) return false;
+ if (field.pattern() != null && !field.pattern().isBlank()) return true;
+ if ("List".equals(field.type()) && "Object".equals(field.itemType())) {
+ return hasPatternField(field.itemFields());
+ }
+ return false;
+ });
+ }
+
+ private static void writeStructuredFieldTypes(Path directory, String packageName, String ownerClass,
+ List fields) throws IOException {
+ for (FieldDefinition field : fields == null ? List.of() : fields) {
+ if (field == null || field.name() == null || field.name().isBlank()) {
+ continue;
+ }
+ if ("Enum".equals(field.type())) {
+ String enumName = toPascalCase(field.name());
+ List values = field.enumValues() == null ? List.of() : field.enumValues().stream()
+ .filter(value -> value != null && !value.isBlank()).map(String::trim).distinct().toList();
+ if (values.isEmpty()) {
+ throw new IllegalArgumentException("Enum field needs at least one allowed value: " + field.name());
+ }
+ String constants = values.stream().map(value -> " " + enumConstant(value) + "(\"" + javaText(value) + "\")")
+ .collect(java.util.stream.Collectors.joining(",\n"));
+ writeUtf8(directory.resolve(enumName + ".java"), """
+ package %s;
+
+ import com.fasterxml.jackson.annotation.JsonCreator;
+ import com.fasterxml.jackson.annotation.JsonValue;
+
+ public enum %s {
+ %s;
+
+ private final String value;
+
+ %s(String value) {
+ this.value = value;
+ }
+
+ @JsonValue
+ public String getValue() {
+ return value;
+ }
+
+ @JsonCreator
+ public static %s fromValue(String value) {
+ for (%s candidate : values()) {
+ if (candidate.value.equals(value)) return candidate;
+ }
+ throw new IllegalArgumentException("Unsupported value: " + value);
+ }
+ }
+ """.formatted(packageName, enumName, constants, enumName, enumName, enumName));
+ }
+ if ("List".equals(field.type()) && "Object".equals(field.itemType())
+ && (field.itemFields() == null || field.itemFields().isEmpty())) {
+ throw new IllegalArgumentException("Object List field needs item fields: " + field.name());
+ }
+ }
+ }
+
+ private static String enumConstant(String value) {
+ String constant = value.toUpperCase(Locale.ROOT).replaceAll("[^A-Z0-9]+", "_").replaceAll("^_+|_+$", "");
+ return constant.isBlank() ? "VALUE" : (Character.isDigit(constant.charAt(0)) ? "VALUE_" + constant : constant);
+ }
+
+ private static String listItemClassName(String ownerClass, FieldDefinition field) {
+ return toPascalCase(field.name()) + "Item";
+ }
+
+ private static String innerObjectListClasses(List fields) {
+ StringBuilder source = new StringBuilder();
+ Set generated = new LinkedHashSet<>();
+ for (FieldDefinition field : fields == null ? List.of() : fields) {
+ if (field == null || !"List".equals(field.type()) || !"Object".equals(field.itemType())
+ || field.name() == null || field.name().isBlank()) {
+ continue;
+ }
+ String itemName = listItemClassName("", field);
+ if (!generated.add(itemName)) continue;
+ List itemFields = field.itemFields() == null ? List.of() : field.itemFields();
+ if (itemFields.isEmpty()) {
+ throw new IllegalArgumentException("Object List field needs item fields: " + field.name());
+ }
+ source.append("\n @Data\n public static class ").append(itemName).append(" {\n")
+ .append(fieldLines(itemFields, Set.of(), itemName))
+ .append(innerObjectListClasses(itemFields))
+ .append(" }\n");
+ }
+ return source.toString();
+ }
+
+ private static String httpUseCaseImplContent(String bizPackage, String baseName, String httpPackage,
+ String httpApiClass, String author, String createDate) {
+ String httpRequestClass = baseName + "HttpRequest";
+ String httpResponseClass = baseName + "HttpResponse";
+ String httpClientClass = httpApiClass + "Client";
+ String clientVariable = Character.toLowerCase(httpClientClass.charAt(0)) + httpClientClass.substring(1);
+ String methodName = Character.toLowerCase(baseName.charAt(0)) + baseName.substring(1);
+ String result = """
+ package %s.usecase.impl;
+
+ import %s.converter.%sConverter;
+ import %s.dto.%sRequest;
+ import %s.dto.%sResponse;
+ import %s.%s.%s;
+ import %s.%s.io.%s;
+ import %s.%s.io.%s;
+ import %s.usecase.%sUseCase;
+ import lombok.RequiredArgsConstructor;
+ import org.springframework.stereotype.Service;
+
+ @Service
+ @RequiredArgsConstructor
+ public class %sUseCaseImpl implements %sUseCase {
+
+ private final %sConverter converter;
+ private final %s %s;
+
+ @Override
+ public %sResponse execute(%sRequest req) {
+ %s httpRequest = converter.toHttpRequest(req);
+ %s httpResponse = %s.call(httpRequest, %s.class);
+
+ %sResponse response = converter.toResponse(httpResponse);
+ response.setResultCode("SUCCESS");
+ response.setResultMessage("HTTP API call completed.");
+ return response;
+ }
+ }
+ """.formatted(
+ bizPackage,
+ bizPackage, baseName,
+ bizPackage, baseName,
+ bizPackage, baseName,
+ BASE_PACKAGE, httpPackage, httpClientClass,
+ BASE_PACKAGE, httpPackage, httpRequestClass,
+ BASE_PACKAGE, httpPackage, httpResponseClass,
+ bizPackage, baseName,
+ baseName, baseName,
+ baseName, httpClientClass, clientVariable,
+ baseName, baseName,
+ httpRequestClass, httpResponseClass, clientVariable, httpResponseClass,
+ baseName);
+ return result.replace("execute(", methodName + "(");
+ }
+
+ private static String httpClientContent(String httpPackage, String clientClass, String apiName) {
+ return """
+ package %s;
+
+ import io.shinhanlife.dat.lib.integration.http.component.AxhubHttpComponent;
+ import lombok.RequiredArgsConstructor;
+ import org.springframework.stereotype.Component;
+
+ @Component
+ @RequiredArgsConstructor
+ public class %s {
+ private static final String API_NAME = "%s";
+
+ private final AxhubHttpComponent http;
+
+ public O call(I request, Class responseType) {
+ return http.call(API_NAME, request, responseType);
+ }
+ }
+ """.formatted(httpPackage, clientClass, apiName);
+ }
+
+ private static String httpConverterContent(String bizPackage, String baseName, String httpPackage) {
+ return """
+ package %s.converter;
+
+ import %s.dto.%sRequest;
+ import %s.dto.%sResponse;
+ import %s.io.%sHttpRequest;
+ import %s.io.%sHttpResponse;
+ import org.mapstruct.Mapper;
+ import org.mapstruct.Mapping;
+ import org.mapstruct.ReportingPolicy;
+
+ @Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE)
+ public interface %sConverter {
+ // Field names differ? Add mappings like this before the method.
+ // @Mapping(source = "sourceField", target = "targetField")
+ %sHttpRequest toHttpRequest(%sRequest request);
+ %sResponse toResponse(%sHttpResponse httpResponse);
+ }
+ """.formatted(bizPackage,
+ bizPackage, baseName,
+ bizPackage, baseName,
+ httpPackage, baseName,
+ httpPackage, baseName,
+ baseName, baseName, baseName, baseName, baseName);
+ }
+
+ private static String toPackageSegment(String value) {
+ String normalized = value == null ? "" : value.toLowerCase(Locale.ROOT)
+ .replaceAll("[^a-z0-9]+", "_")
+ .replaceAll("^_+|_+$", "");
+ return normalized.isBlank() ? "http_api" : normalized;
+ }
+ private static String mciConverterContent(String converterPackage, String bizPackage, String baseName,
+ String mciPackage, String interfaceId, String converterClassName) {
+ return """
+ package %s;
+
+ 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.ReportingPolicy;
+
+ @Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE)
+ public interface %s {
+ // Field names differ? Add mappings like this before the method.
+ // @Mapping(source = "sourceField", target = "targetField")
+ @Mapping(target = "pageInfo", ignore = true)
+ @Mapping(target = "scrPageInfo", ignore = true)
+ %s_I toLegacyRequest(%sRequest request);
+ default %s_I toRequest(%sRequest request) {
+ return toLegacyRequest(request);
+ }
+ %sRequest toRequest(%s_I mciRequest);
+ %sResponse toResponse(%s_O mciRes);
+ }
+ """.formatted(converterPackage, bizPackage, baseName, bizPackage, baseName,
+ BASE_PACKAGE, mciPackage, interfaceId, BASE_PACKAGE, mciPackage, interfaceId,
+ converterClassName, interfaceId, baseName, interfaceId, baseName, baseName, interfaceId, baseName, interfaceId);
+ }
+
+ private static String mciTargetSystemPackage(String clientSystemCode) {
+ if (clientSystemCode == null || clientSystemCode.length() != 9) {
+ return null;
+ }
+ String prefix = clientSystemCode.substring(1, 5).toLowerCase(Locale.ROOT);
+ return prefix.substring(0, 3) + "." + prefix.substring(3);
+ }
+
+ private static String legacyConverterContent(String bizPackage, String baseName) {
+ return """
+ 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.ReportingPolicy;
+
+ @Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE)
+ public interface %sConverter {
+ // Field names differ? Add mappings like this before the method.
+ // @Mapping(source = "sourceField", target = "targetField")
+ %sLegacyRequest toLegacyRequest(%sRequest request);
+ %sRequest toRequest(%sLegacyRequest legacyRequest);
+ %sResponse toResponse(%sLegacyResponse legacyResponse);
+ }
+ """.formatted(
+ bizPackage, bizPackage, baseName, bizPackage, baseName,
+ bizPackage, baseName, bizPackage, baseName,
+ baseName, baseName, baseName, baseName, baseName, baseName, baseName);
+ }
+ private static String fieldLines(List fields) {
+ return fieldLines(fields, Set.of(), "");
+ }
+
+ private static String fieldLines(List fields, Set excludedNames) {
+ return fieldLines(fields, excludedNames, "");
+ }
+
+ private static String fieldLines(List fields, Set excludedNames, String ownerClass) {
+ StringBuilder source = new StringBuilder();
+ Set generatedNames = new LinkedHashSet<>();
+ for (FieldDefinition field : fields == null ? List.of() : fields) {
+ if (field == null || field.name() == null || field.name().isBlank()) {
+ continue;
+ }
+ String fieldName = field.name().trim();
+ if (excludedNames.contains(fieldName) || !generatedNames.add(fieldName)) {
+ continue;
+ }
+ String type = javaFieldType(field, ownerClass);
+ String description = javaText(richDescription(field));
+ String example = "";
+ if (field.examples() != null && !field.examples().isEmpty()) {
+ example = javaText(field.examples().get(0));
+ }
+ source.append(" @Schema(description = \"").append(description).append("\"");
+ if (!example.isEmpty()) {
+ source.append(", example = \"").append(example).append("\"");
+ }
+ if (field.required()) {
+ source.append(", requiredMode = Schema.RequiredMode.REQUIRED");
+ }
+ source.append(")\n");
+ if (field.pattern() != null && !field.pattern().isBlank()) {
+ source.append(" @Pattern(regexp = \"").append(javaText(field.pattern())).append("\")\n");
+ }
+ source.append(" private ").append(type).append(' ').append(fieldName).append(";\n\n");
+ }
+ return source.toString();
+ }
+
+ private static String richDescription(FieldDefinition field) {
+ String desc = field.description() == null ? "" : field.description().trim();
+ if (field.pattern() != null && !field.pattern().isBlank()) {
+ desc += " (형식: " + field.pattern() + ")";
+ }
+ return desc.trim();
+ }
+
+ private static String javaFieldType(FieldDefinition field, String ownerClass) {
+ return switch (field.type() == null ? "String" : field.type()) {
+ case "Enum" -> toPascalCase(field.name());
+ case "List" -> "List<" + listItemJavaType(field, ownerClass) + ">";
+ default -> supportedType(field.type());
+ };
+ }
+
+ private static String listItemJavaType(FieldDefinition field, String ownerClass) {
+ String itemType = field.itemType() == null ? "" : field.itemType();
+ if ("Object".equals(itemType)) {
+ return listItemClassName(ownerClass, field);
+ }
+ return supportedType(itemType);
+ }
+ private static String supportedType(String type) {
+ if (type == null || type.isBlank()) return "String";
+ return type;
+ }
+
+
+ static String wireMockMappingContent(String interfaceId, String bodyFileName) {
+ return """
+ {
+ "request" : {
+ "method" : "POST",
+ "urlPath" : "/%s"
+ },
+ "response" : {
+ "status" : 200,
+ "headers" : {
+ "Content-Type" : "application/json;charset=UTF-8"
+ },
+ "bodyFileName" : "%s"
+ }
+ }
+ """.formatted(jsonEscape(interfaceId), jsonEscape(bodyFileName));
+ }
+ private static String mockResponseContent(List outputFields) {
+ StringBuilder json = new StringBuilder("{\n");
+ List fields = outputFields == null ? List.of() : outputFields;
+ boolean first = true;
+ for (FieldDefinition field : fields) {
+ if (field.name() == null || field.name().isBlank()) {
+ continue;
+ }
+ if (!first) {
+ json.append(",\n");
+ }
+ json.append(" \"").append(jsonEscape(field.name())).append("\" : ")
+ .append(mockValue(field));
+ first = false;
+ }
+ return json.append("\n}\n").toString();
+ }
+
+ private static String mockValue(FieldDefinition field) {
+ if ("List".equals(field.type())) {
+ if ("Object".equals(field.itemType())) {
+ StringBuilder object = new StringBuilder("{");
+ boolean first = true;
+ for (FieldDefinition itemField : field.itemFields() == null ? List.of() : field.itemFields()) {
+ if (!first) object.append(", ");
+ object.append("\"").append(jsonEscape(itemField.name())).append("\" : ").append(mockValue(itemField));
+ first = false;
+ }
+ return "[" + object + "]";
+ }
+ FieldDefinition item = new FieldDefinition("item", field.itemType(), "", field.examples(), field.pattern(), false);
+ return "[" + mockValue(item) + "]";
+ }
+
+ String exampleStr = (field.examples() != null && !field.examples().isEmpty()) ? field.examples().get(0) : null;
+ if (exampleStr == null || exampleStr.isBlank()) {
+ return "null";
+ }
+ if ("Enum".equals(field.type())) {
+ return "\"" + jsonEscape(exampleStr) + "\"";
+ }
+ return switch (supportedType(field.type())) {
+ case "Integer", "Long", "Double", "BigDecimal" -> exampleStr;
+ case "Boolean" -> Boolean.parseBoolean(exampleStr) ? "true" : "false";
+ default -> "\"" + jsonEscape(exampleStr) + "\"";
+ };
+ }
+
+ private static String jsonEscape(String value) {
+ return value.replace("\\", "\\\\").replace("\"", "\\\"");
+ }
+
+ private static String useCaseTestContent(String bizPackage, String baseName) {
+ return """
+ package %s.usecase;
+
+ import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+ import %s.dto.%sRequest;
+ import %s.dto.%sResponse;
+ import org.junit.jupiter.api.Test;
+
+ class %sUseCaseTest {
+
+ @Test
+ void createsToolRequestAndResponseDtos() {
+ assertNotNull(new %sRequest());
+ assertNotNull(new %sResponse());
+ }
+ }
+ """.formatted(bizPackage, bizPackage, baseName, bizPackage, baseName,
+ baseName, baseName, baseName);
+ }
+ private static String toToolName(String moduleName, String group, String baseName) {
+ 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".formatted(
+ group.toLowerCase(Locale.ROOT),
+ service,
+ action);
+ }
+
+ private static String abbreviatedMciSourceBaseName(String baseName) {
+ String[] words = baseName.split("(?<=[a-z])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])");
+ if (words.length < 3) {
+ return baseName;
+ }
+ return abbreviatedWord(words[0], 4) + abbreviatedWord(words[1], 6);
+ }
+
+ private static String abbreviatedWord(String word, int maximumLength) {
+ int length = Math.min(word.length(), maximumLength);
+ return toPascalCase(word.substring(0, length).toLowerCase(Locale.ROOT));
+ }
+
+ 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();
+ }
+}
diff --git a/dat-was-lib/src/main/resources/codes.csv b/dat-was-lib/src/main/resources/codes.csv
new file mode 100644
index 000000000..f268bc248
--- /dev/null
+++ b/dat-was-lib/src/main/resources/codes.csv
@@ -0,0 +1,17 @@
+그룹,코드,명칭
+COMPANY,005930,삼성전자
+COMPANY,035420,NAVER
+COMPANY,035720,카카오
+COMPANY,000660,SK하이닉스
+COMPANY,055550,신한지주
+COMPANY,086790,하나금융지주
+COMPANY,105560,KB금융
+COMPANY,316140,우리금융지주
+COMPANY,000810,삼성화재
+COMPANY,088350,한화생명
+COMPANY,005830,DB손해보험
+COMPANY,000030,우리은행
+STATUS,01,정상
+STATUS,02,비정상
+STATUS,03,해지
+STATUS,04,실효
\ No newline at end of file
diff --git a/dat-was-lib/src/test/java/io/shinhanlife/dat/lib/util/CommonCodeResolverTest.java b/dat-was-lib/src/test/java/io/shinhanlife/dat/lib/util/CommonCodeResolverTest.java
new file mode 100644
index 000000000..f16cbd45e
--- /dev/null
+++ b/dat-was-lib/src/test/java/io/shinhanlife/dat/lib/util/CommonCodeResolverTest.java
@@ -0,0 +1,62 @@
+package io.shinhanlife.dat.lib.util;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import org.junit.jupiter.api.Test;
+
+/**
+ * @package io.shinhanlife.dat.lib.util
+ * @className CommonCodeResolverTest
+ * @description AX HUB 시스템 처리 클래스
+ * @author 0986406
+ * @create 2026.09.01
+ *
+ * ---------- 개정이력 ----------
+ * 수정일 수정자 수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01 0986406 최초생성
+ *
+ *
+ */
+class CommonCodeResolverTest {
+
+ @Test
+ void resolvesCompanyCodeFromExactName() {
+ assertEquals("005930", CommonCodeResolver.resolve("COMPANY", "삼성전자"));
+ assertEquals("035420", CommonCodeResolver.resolve("COMPANY", "NAVER"));
+ assertEquals("035720", CommonCodeResolver.resolve("COMPANY", "카카오"));
+ assertEquals("055550", CommonCodeResolver.resolve("COMPANY", "신한지주"));
+ }
+
+ @Test
+ void resolvesCompanyCodeWhenAlreadyCode() {
+ assertEquals("005930", CommonCodeResolver.resolve("COMPANY", "005930"));
+ assertEquals("035420", CommonCodeResolver.resolve("COMPANY", "035420"));
+ }
+
+ @Test
+ void resolvesCaseInsensitiveAndSpaceIgnored() {
+ assertEquals("035420", CommonCodeResolver.resolve("COMPANY", "naver"));
+ assertEquals("005930", CommonCodeResolver.resolve("COMPANY", "삼성 전자"));
+ }
+
+ @Test
+ void resolvesStatusGroupCode() {
+ assertEquals("01", CommonCodeResolver.resolve("STATUS", "정상"));
+ assertEquals("02", CommonCodeResolver.resolve("STATUS", "비정상"));
+ assertEquals("03", CommonCodeResolver.resolve("STATUS", "해지"));
+ assertEquals("01", CommonCodeResolver.resolve("STATUS", "01"));
+ }
+
+ @Test
+ void getsNameFromCode() {
+ assertEquals("삼성전자", CommonCodeResolver.getName("COMPANY", "005930"));
+ assertEquals("정상", CommonCodeResolver.getName("STATUS", "01"));
+ }
+
+ @Test
+ void returnsOriginalWhenNotFound() {
+ assertEquals("알수없는회사", CommonCodeResolver.resolve("COMPANY", "알수없는회사"));
+ assertEquals("99", CommonCodeResolver.resolve("STATUS", "99"));
+ }
+}
\ No newline at end of file
diff --git a/dat-was-pro/src/main/java/io/shinhanlife/dat/mcc/biz/pro/converter/RecruitmentFeeDisclosureDataConverter.java b/dat-was-pro/src/main/java/io/shinhanlife/dat/mcc/biz/pro/converter/RecruitmentFeeDisclosureDataConverter.java
new file mode 100644
index 000000000..c5b0c1f8b
--- /dev/null
+++ b/dat-was-pro/src/main/java/io/shinhanlife/dat/mcc/biz/pro/converter/RecruitmentFeeDisclosureDataConverter.java
@@ -0,0 +1,38 @@
+package io.shinhanlife.dat.mcc.biz.pro.converter;
+
+import io.shinhanlife.dat.lib.util.CommonCodeResolver;
+import io.shinhanlife.dat.mcc.biz.pro.dto.RecruitmentFeeDisclosureDataRequest;
+import io.shinhanlife.dat.mcc.biz.pro.dto.RecruitmentFeeDisclosureDataResponse;
+import io.shinhanlife.dat.mcc.infra.itrf.mci.nbt.i.io.ONBTI2370_I;
+import io.shinhanlife.dat.mcc.infra.itrf.mci.nbt.i.io.ONBTI2370_O;
+import org.mapstruct.Mapper;
+import org.mapstruct.Mapping;
+import org.mapstruct.ReportingPolicy;
+
+/**
+ * @package io.shinhanlife.dat.mcc.biz.pro.converter
+ * @className RecruitmentFeeDisclosureDataConverter
+ * @description AX HUB 시스템 처리 클래스
+ * @author 0986406
+ * @create 2026.09.01
+ *
+ * ---------- 개정이력 ----------
+ * 수정일 수정자 수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01 0986406 최초생성
+ *
+ *
+ */
+@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE)
+public interface RecruitmentFeeDisclosureDataConverter {
+
+ @Mapping(target = "companyCode", expression = "java(toCompanyCode(request.getCompanyCode()))")
+ ONBTI2370_I toLegacyRequest(RecruitmentFeeDisclosureDataRequest request);
+
+ default String toCompanyCode(String company) {
+ return CommonCodeResolver.resolve("COMPANY", company);
+ }
+
+ RecruitmentFeeDisclosureDataRequest toRequest(ONBTI2370_I mciRequest);
+ RecruitmentFeeDisclosureDataResponse toResponse(ONBTI2370_O mciRes);
+}
diff --git a/dat-was-pro/src/main/java/io/shinhanlife/dat/mcc/biz/pro/dto/RecruitmentFeeDisclosureDataRequest.java b/dat-was-pro/src/main/java/io/shinhanlife/dat/mcc/biz/pro/dto/RecruitmentFeeDisclosureDataRequest.java
new file mode 100644
index 000000000..fd8c633ac
--- /dev/null
+++ b/dat-was-pro/src/main/java/io/shinhanlife/dat/mcc/biz/pro/dto/RecruitmentFeeDisclosureDataRequest.java
@@ -0,0 +1,45 @@
+package io.shinhanlife.dat.mcc.biz.pro.dto;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import jakarta.validation.constraints.Pattern;
+
+
+/**
+ * @package io.shinhanlife.dat.mcc.biz.pro.dto
+ * @className RecruitmentFeeDisclosureDataRequest
+ * @description AX HUB 시스템 처리 클래스
+ * @author 0986406
+ * @create 2026.09.01
+ *
+ * ---------- 개정이력 ----------
+ * 수정일 수정자 수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01 0986406 최초생성
+ *
+ *
+ */
+@Data
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class RecruitmentFeeDisclosureDataRequest {
+ @Schema(description = "조회할 회사 코드 또는 회사명 (예시: 005930, 삼성전자, NAVER, 카카오)", example = "삼성전자", requiredMode = Schema.RequiredMode.REQUIRED)
+ private String companyCode;
+
+ @Schema(description = "조회할 연도 (형식: ^[0-9]{4}$) (예시: 2023, 2024)", example = "2023", requiredMode = Schema.RequiredMode.REQUIRED)
+ @Pattern(regexp = "^[0-9]{4}$")
+ private String year;
+
+ @Schema(description = "조회할 분기 (1-4) (형식: ^[1-4]$) (예시: 1, 2, 3, 4)", example = "1")
+ @Pattern(regexp = "^[1-4]$")
+ private String quarter;
+
+ @Schema(description = "조회 시작일 (YYYY-MM-DD) (형식: ^[0-9]{4}-[0-9]{2}-[0-9]{2}$) (예시: 2024-01-01, 2024-04-01)", example = "2024-01-01")
+ @Pattern(regexp = "^[0-9]{4}-[0-9]{2}-[0-9]{2}$")
+ private String startDate;
+
+ @Schema(description = "조회 종료일 (YYYY-MM-DD) (형식: ^[0-9]{4}-[0-9]{2}-[0-9]{2}$) (예시: 2024-03-31, 2024-06-30)", example = "2024-03-31")
+ @Pattern(regexp = "^[0-9]{4}-[0-9]{2}-[0-9]{2}$")
+ private String endDate;
+
+}
diff --git a/dat-was-pro/src/test/java/io/shinhanlife/dat/mcc/biz/pro/converter/RecruitmentFeeDisclosureDataConverterTest.java b/dat-was-pro/src/test/java/io/shinhanlife/dat/mcc/biz/pro/converter/RecruitmentFeeDisclosureDataConverterTest.java
new file mode 100644
index 000000000..09d94c701
--- /dev/null
+++ b/dat-was-pro/src/test/java/io/shinhanlife/dat/mcc/biz/pro/converter/RecruitmentFeeDisclosureDataConverterTest.java
@@ -0,0 +1,67 @@
+package io.shinhanlife.dat.mcc.biz.pro.converter;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+import io.shinhanlife.dat.mcc.biz.pro.dto.RecruitmentFeeDisclosureDataRequest;
+import io.shinhanlife.dat.mcc.infra.itrf.mci.nbt.i.io.ONBTI2370_I;
+import org.junit.jupiter.api.Test;
+import org.mapstruct.factory.Mappers;
+
+/**
+ * @package io.shinhanlife.dat.mcc.biz.pro.converter
+ * @className RecruitmentFeeDisclosureDataConverterTest
+ * @description AX HUB 시스템 처리 클래스
+ * @author 0986406
+ * @create 2026.09.01
+ *
+ * ---------- 개정이력 ----------
+ * 수정일 수정자 수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01 0986406 최초생성
+ *
+ *
+ */
+class RecruitmentFeeDisclosureDataConverterTest {
+
+ private final RecruitmentFeeDisclosureDataConverter converter =
+ Mappers.getMapper(RecruitmentFeeDisclosureDataConverter.class);
+
+ @Test
+ void mapsCompanyNameToLegacyCompanyCode() {
+ RecruitmentFeeDisclosureDataRequest request = new RecruitmentFeeDisclosureDataRequest();
+ request.setCompanyCode("삼성전자");
+ request.setYear("2024");
+ request.setQuarter("1");
+
+ ONBTI2370_I legacyReq = converter.toLegacyRequest(request);
+
+ assertNotNull(legacyReq);
+ assertEquals("005930", legacyReq.getCompanyCode());
+ assertEquals("2024", legacyReq.getYear());
+ assertEquals("1", legacyReq.getQuarter());
+ }
+
+ @Test
+ void preservesCompanyCodeWhenAlreadyNumericCode() {
+ RecruitmentFeeDisclosureDataRequest request = new RecruitmentFeeDisclosureDataRequest();
+ request.setCompanyCode("035420");
+ request.setYear("2024");
+
+ ONBTI2370_I legacyReq = converter.toLegacyRequest(request);
+
+ assertNotNull(legacyReq);
+ assertEquals("035420", legacyReq.getCompanyCode());
+ }
+
+ @Test
+ void mapsNaverAndKakao() {
+ RecruitmentFeeDisclosureDataRequest requestNaver = new RecruitmentFeeDisclosureDataRequest();
+ requestNaver.setCompanyCode("NAVER");
+ assertEquals("035420", converter.toLegacyRequest(requestNaver).getCompanyCode());
+
+ RecruitmentFeeDisclosureDataRequest requestKakao = new RecruitmentFeeDisclosureDataRequest();
+ requestKakao.setCompanyCode("카카오");
+ assertEquals("035720", converter.toLegacyRequest(requestKakao).getCompanyCode());
+ }
+}
\ No newline at end of file
diff --git a/dat-was-sal/Dockerfile b/dat-was-sal/Dockerfile
new file mode 100644
index 000000000..5b9b12ab2
--- /dev/null
+++ b/dat-was-sal/Dockerfile
@@ -0,0 +1,7 @@
+FROM eclipse-temurin:21-jre-alpine
+WORKDIR /app
+RUN apk add --no-cache tzdata
+ENV TZ=Asia/Seoul
+COPY dat-was-sal/build/libs/*-SNAPSHOT.jar app.jar
+EXPOSE 8083
+ENTRYPOINT ["java", "-jar", "app.jar"]
diff --git a/dat-was-sal/build.gradle b/dat-was-sal/build.gradle
new file mode 100644
index 000000000..e08421661
--- /dev/null
+++ b/dat-was-sal/build.gradle
@@ -0,0 +1,9 @@
+plugins {
+ // Spring Boot 3.5.11 version is managed by the root build.gradle.
+ id 'org.springframework.boot'
+}
+
+dependencies {
+ // Shared MCP, Glow integration, validation, logging, Lombok and MapStruct configuration.
+ implementation project(':dat-was-lib')
+}
diff --git a/dat-was-sal/src/main/java/io/shinhanlife/dat/mcc/sal/DatWasSalApplication.java b/dat-was-sal/src/main/java/io/shinhanlife/dat/mcc/sal/DatWasSalApplication.java
new file mode 100644
index 000000000..8730bf839
--- /dev/null
+++ b/dat-was-sal/src/main/java/io/shinhanlife/dat/mcc/sal/DatWasSalApplication.java
@@ -0,0 +1,29 @@
+package io.shinhanlife.dat.mcc.sal;
+
+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.dat.mcc.sal
+ * @className DatWasSalApplication
+ * @description AX HUB 시스템 처리 클래스
+ * @author jade
+ * @create 2026.09.08
+ *
+ * ---------- 개정이력 ----------
+ * 수정일 수정자 수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.08 jade 최초생성
+ *
+ *
+ */
+@SpringBootApplication(scanBasePackages = {"io.shinhanlife.dat.mcc", "io.shinhanlife.dat.lib"})
+@ConfigurationPropertiesScan(basePackages = {"io.shinhanlife.dat.mcc", "io.shinhanlife.dat.lib"})
+@EnableCaching
+public class DatWasSalApplication {
+ public static void main(String[] args) {
+ SpringApplication.run(DatWasSalApplication.class, args);
+ }
+}
diff --git a/dat-was-sal/src/main/resources/application-dev.yml b/dat-was-sal/src/main/resources/application-dev.yml
new file mode 100644
index 000000000..7fd673f5b
--- /dev/null
+++ b/dat-was-sal/src/main/resources/application-dev.yml
@@ -0,0 +1,11 @@
+server:
+ port: ${PORT:8083}
+
+spring:
+ config:
+ activate:
+ on-profile: dev
+ import:
+ - classpath:application-core-dev.yml
+ - classpath:glow/application-glow.yml
+ - classpath:glow/application-glow-dev.yml
diff --git a/dat-was-sal/src/main/resources/application-local.yml b/dat-was-sal/src/main/resources/application-local.yml
new file mode 100644
index 000000000..d985a21ec
--- /dev/null
+++ b/dat-was-sal/src/main/resources/application-local.yml
@@ -0,0 +1,7 @@
+spring:
+ config:
+ activate:
+ on-profile: local
+ import:
+ - classpath:glow/application-glow.yml
+ - classpath:glow/application-glow-local.yml
diff --git a/dat-was-sal/src/main/resources/application-prod.yml b/dat-was-sal/src/main/resources/application-prod.yml
new file mode 100644
index 000000000..31306c3fe
--- /dev/null
+++ b/dat-was-sal/src/main/resources/application-prod.yml
@@ -0,0 +1,11 @@
+server:
+ port: ${PORT:8083}
+
+spring:
+ config:
+ activate:
+ on-profile: prod
+ import:
+ - classpath:application-core-prod.yml
+ - classpath:glow/application-glow.yml
+ - classpath:glow/application-glow-prod.yml
diff --git a/dat-was-sal/src/main/resources/application-test.yml b/dat-was-sal/src/main/resources/application-test.yml
new file mode 100644
index 000000000..1ce9d5ad4
--- /dev/null
+++ b/dat-was-sal/src/main/resources/application-test.yml
@@ -0,0 +1,11 @@
+server:
+ port: ${PORT:8083}
+
+spring:
+ config:
+ activate:
+ on-profile: test
+ import:
+ - classpath:application-core-test.yml
+ - classpath:glow/application-glow.yml
+ - classpath:glow/application-glow-test.yml
diff --git a/dat-was-sal/src/main/resources/application.yml b/dat-was-sal/src/main/resources/application.yml
new file mode 100644
index 000000000..d0cedef0a
--- /dev/null
+++ b/dat-was-sal/src/main/resources/application.yml
@@ -0,0 +1,21 @@
+server:
+ port: 8083
+spring:
+ application:
+ name: dat-was-sal
+ config:
+ import: optional:classpath:tool-service-manifest.yml
+ profiles:
+ active: local
+logging:
+ level:
+ org.apache.kafka: ERROR
+mcp:
+ namespace: ""
+ manifest:
+ bundle-id: dat-was-sal
+ name-prefix: ""
+ security:
+ api-key: ${TOOL_SERVER_API_KEY:tool-server-key}
+ tenant-domains:
+ TESTER-DEV: ALL
diff --git a/dat-was-sal/src/main/resources/logback-spring.xml b/dat-was-sal/src/main/resources/logback-spring.xml
new file mode 100644
index 000000000..46a102842
--- /dev/null
+++ b/dat-was-sal/src/main/resources/logback-spring.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+ ${LOG_PATTERN}
+
+
+
+ logs/dat-was-sal.log
+
+ logs/dat-was-sal-%d{yyyy-MM-dd}.log
+ 30
+
+
+ ${LOG_PATTERN}
+
+
+
+
+
+
+
+
diff --git a/dat-was-sal/src/main/resources/tool-service-manifest.yml b/dat-was-sal/src/main/resources/tool-service-manifest.yml
new file mode 100644
index 000000000..4002085ab
--- /dev/null
+++ b/dat-was-sal/src/main/resources/tool-service-manifest.yml
@@ -0,0 +1,22 @@
+---
+mcp:
+ manifest:
+ routing-functions:
+ - name: "route_to_dat-was-sal"
+ server-id: "dat-was-sal"
+ category-key: "sal"
+ product-boundary: "영업 업무 서버를 위한 제품 범위 정의입니다."
+ business-domain: "영업 조직 및 설계사 관리 도메인입니다."
+ business-outcome: "영업 요청의 효율적인 처리와 설계사 업무 지원을 보장합니다."
+ primary-entities: "영업 조직, 설계사, 고객"
+ capabilities: "요청 라우팅, 업무 할당, 상태 조회"
+ select-if: "요청의 주요 엔티티가 영업 조직 또는 설계사일 경우 선택합니다."
+ reject-if: "요청이 다른 도메인에 속하거나 유효하지 않은 엔티티를 포함할 경우 거부합니다."
+ confusable-servers:
+ - "dat-was-cus"
+ - "dat-was-ehr"
+ - "dat-was-pro"
+ - "dat-was-sys"
+ decision-policy: "가장 가까운 영업 서버를 선택하고, 부하를 고려하여 결정합니다."
+ description_serialization: "이 도구는 영업 요청을 처리하며, 영업 조직과 설계사 업무를 효율적으로 라우팅하고, 부\
+ 하를 고려한 결정 정책을 적용하여 적절한 서버로 분배합니다."