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

17
dap-gateway/Dockerfile Normal file
View File

@@ -0,0 +1,17 @@
FROM eclipse-temurin:21-jdk-alpine AS builder
WORKDIR /app
COPY gradlew .
COPY gradle gradle
COPY build.gradle settings.gradle ./
COPY dap-common dap-common
COPY dap-gateway dap-gateway
RUN chmod +x gradlew
RUN ./gradlew :dap-gateway:build -x test
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
RUN apk add --no-cache tzdata
ENV TZ=Asia/Seoul
COPY --from=builder /app/dap-gateway/build/libs/*-SNAPSHOT.jar app.jar
EXPOSE 8081
ENTRYPOINT ["java", "-jar", "app.jar"]

38
dap-gateway/build.gradle Normal file
View File

@@ -0,0 +1,38 @@
plugins {
id 'org.springframework.boot'
}
dependencies {
implementation project(':dap-common')
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
// Spring AI MCP Server
implementation 'org.springframework.ai:spring-ai-starter-mcp-server-webmvc'
implementation 'org.springframework.ai:spring-ai-starter-model-openai'
// MyBatis & DB
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.3'
runtimeOnly 'com.h2database:h2'
implementation 'p6spy:p6spy:3.9.1'
// MapStruct
implementation "org.mapstruct:mapstruct:1.5.5.Final"
annotationProcessor "org.projectlombok:lombok-mapstruct-binding:0.2.0"
annotationProcessor "org.mapstruct:mapstruct-processor:1.5.5.Final"
// implementation 'com.networknt:json-schema-validator:1.4.0' // Spring AI 내장 버전과 충돌 방지를 위해 주석 처리
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.5.0'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
tasks.named('test') {
useJUnitPlatform()
}
dependencies {
compileOnly 'org.projectlombok:lombok:1.18.32'
annotationProcessor 'org.projectlombok:lombok:1.18.32'
}

View File

@@ -0,0 +1,15 @@
package io.shinhanlife.dap.biz.mcp.gateway;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
import org.springframework.cache.annotation.EnableCaching;
@SpringBootApplication(scanBasePackages = {"io.shinhanlife.dap.biz.mcp.gateway", "io.shinhanlife.dap.common.mcp", "io.shinhanlife.dap.common.config"})
@ConfigurationPropertiesScan(basePackages = {"io.shinhanlife.dap.biz.mcp.gateway", "io.shinhanlife.dap.common.mcp", "io.shinhanlife.dap.common.config"})
@EnableCaching
public class AxHubGatewayApplication {
public static void main(String[] args) {
SpringApplication.run(AxHubGatewayApplication.class, args);
}
}

View File

@@ -0,0 +1,53 @@
package io.shinhanlife.dap.biz.mcp.gateway.aop;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
import org.springframework.util.StopWatch;
/**
* @package io.shinhanlife.dap.biz.mcp.gateway.aop
* @className GatewayLoggingAspect
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
@Slf4j
@Aspect
@Component
public class GatewayLoggingAspect {
// gateway의 controller 패키지 하위의 모든 클래스/메서드 실행 시 작동
@Around("execution(* io.shinhanlife.dap.biz.mcp.gateway.presentation..*(..))")
public Object logGatewayExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
String targetMethod = joinPoint.getSignature().toShortString();
StopWatch stopWatch = new StopWatch();
log.info(" [Gateway 송신 시작] Target: {}", targetMethod);
stopWatch.start();
try {
// 실제 대상 메서드(외부 통신) 실행
Object result = joinPoint.proceed();
return result;
} finally {
stopWatch.stop();
long timeMillis = stopWatch.getTotalTimeMillis();
if (timeMillis > 3000) { // 3초 이상 걸리면 WARN 로그로 슬로우 통신 경고
log.warn("⏱ [Gateway 슬로우 응답] Target: {} | 소요시간: {}ms", targetMethod, timeMillis);
} else {
log.info("⏹ [Gateway 송신 완료] Target: {} | 소요시간: {}ms", targetMethod, timeMillis);
}
}
}
}

View File

@@ -0,0 +1,48 @@
package io.shinhanlife.dap.biz.mcp.gateway.audit;
import io.shinhanlife.dap.biz.mcp.gateway.config.McpGatewayProperties;
import io.shinhanlife.dap.biz.mcp.gateway.guardrail.SensitiveDataMasker;
import io.shinhanlife.dap.biz.mcp.gateway.security.McpRequestContext;
import com.fasterxml.jackson.databind.JsonNode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
@Service
/**
* MCP Tool 호출 이력을 감사 로그로 남기는 서비스입니다.
*
* 금융권 환경에서 누가, 어떤 Agent로, 어떤 Tool을 호출했는지 추적하기 위한 로그를 담당합니다.
*/
public class AuditLogService {
private static final Logger audit = LoggerFactory.getLogger("MCP_AUDIT");
private final McpGatewayProperties properties;
private final SensitiveDataMasker masker;
public AuditLogService(McpGatewayProperties properties, SensitiveDataMasker masker) {
this.properties = properties;
this.masker = masker;
}
/**
* Tool 실행 시작 시점에 요청자와 마스킹된 인자를 기록합니다.
*/
public void toolStarted(McpRequestContext context, String toolName, JsonNode arguments) {
if (!properties.auditEnabled()) {
return;
}
audit.info("event=tool_started requestId={} agentId={} userId={} clientAddress={} tool={} arguments={}",
context.requestId(), context.agentId(), context.userId(), context.clientAddress(), toolName, masker.mask(arguments));
}
/**
* Tool 실행 종료 시점에 성공 여부, 처리 시간, 오류 유형을 기록합니다.
*/
public void toolFinished(McpRequestContext context, String toolName, long elapsedMillis, boolean success, String errorCode) {
if (!properties.auditEnabled()) {
return;
}
audit.info("event=tool_finished requestId={} agentId={} userId={} clientAddress={} tool={} elapsedMillis={} success={} errorCode={}",
context.requestId(), context.agentId(), context.userId(), context.clientAddress(), toolName, elapsedMillis, success, errorCode);
}
}

View File

@@ -0,0 +1,26 @@
package io.shinhanlife.dap.biz.mcp.gateway.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Component
@ConfigurationProperties(prefix = "mcp.agent-response")
public class AgentResponseBudgetProperties {
private Integer maxPreviewItems;
private Integer maxFieldsPerItem;
private Long maxItemBytes;
private Long maxTotalBytes;
private Boolean includeSummary;
public void setMaxPreviewItems(Integer maxPreviewItems) { this.maxPreviewItems = maxPreviewItems; }
public void setMaxFieldsPerItem(Integer maxFieldsPerItem) { this.maxFieldsPerItem = maxFieldsPerItem; }
public void setMaxItemBytes(Long maxItemBytes) { this.maxItemBytes = maxItemBytes; }
public void setMaxTotalBytes(Long maxTotalBytes) { this.maxTotalBytes = maxTotalBytes; }
public void setIncludeSummary(Boolean includeSummary) { this.includeSummary = includeSummary; }
public int maxPreviewItems() { return maxPreviewItems == null || maxPreviewItems < 1 ? 20 : maxPreviewItems; }
public int maxFieldsPerItem() { return maxFieldsPerItem == null || maxFieldsPerItem < 1 ? 10 : maxFieldsPerItem; }
public long maxItemBytes() { return maxItemBytes == null || maxItemBytes < 1 ? 4_096 : maxItemBytes; }
public long maxTotalBytes() { return maxTotalBytes == null || maxTotalBytes < 1 ? 65_536 : maxTotalBytes; }
public boolean includeSummary() { return includeSummary == null || includeSummary; }
}

View File

@@ -0,0 +1,30 @@
package io.shinhanlife.dap.biz.mcp.gateway.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
import java.util.Map;
/**
* @package io.shinhanlife.dap.biz.mcp.gateway.config
* @className GatewayFallbackProperties
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
@Data
@Configuration
@ConfigurationProperties(prefix = "mcp.gateway.fallback")
public class GatewayFallbackProperties {
private Map<String, String> routes = new HashMap<>();
private String defaultUrl;
}

View File

@@ -0,0 +1,130 @@
package io.shinhanlife.dap.biz.mcp.gateway.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
@Component
@ConfigurationProperties(prefix = "mcp")
/**
* application.properties의 mcp.* 설정을 Java 코드에서 사용하기 위한 설정 클래스입니다.
*
* API Key, 허용 Tool, 감사 로그, Redis Trace, Retry, Circuit Breaker,
* EIMS/Tool 서버 주소 같은 MCP Gateway 운영 설정을 한 곳에서 관리합니다.
*/
public class McpGatewayProperties {
private String apiKey;
private List<String> allowedTools;
private Boolean auditEnabled;
private Boolean agentClaimsRequired;
private Boolean trustedClaimsRequired;
private Boolean writeApprovalRequired;
private Boolean redisTraceEnabled;
private Long redisTraceTtlSeconds;
private Long toolTimeoutMillis;
private Integer retryMaxAttempts;
private Long retryInitialBackoffMillis;
private Double retryBackoffMultiplier;
private Long retryMaxBackoffMillis;
private Integer circuitBreakerFailureThreshold;
private Long circuitBreakerOpenMillis;
private String eimsBaseUrl;
private Long eimsTimeoutMillis;
private String toolServerBaseUrl;
private String toolServerApiKey;
private Long toolHeartbeatTimeoutSeconds;
private Integer defaultPageSize;
private Integer maxPageSize;
private Integer maxPagesPerCall;
private Long maxStreamBytes;
private Long maxDurationSeconds;
private Long maxResponseBytesFromTool;
private Long largeResponseMaxItemBytes;
private Integer largeResponseMaxPreviewItems;
private Integer toolExecutorCorePoolSize;
private Integer toolExecutorMaxPoolSize;
private Integer toolExecutorQueueCapacity;
private Long toolExecutorKeepAliveSeconds;
public void setApiKey(String apiKey) { this.apiKey = apiKey; }
public void setAllowedTools(List<String> allowedTools) { this.allowedTools = allowedTools; }
public void setAuditEnabled(Boolean auditEnabled) { this.auditEnabled = auditEnabled; }
public void setAgentClaimsRequired(Boolean agentClaimsRequired) { this.agentClaimsRequired = agentClaimsRequired; }
public void setTrustedClaimsRequired(Boolean trustedClaimsRequired) { this.trustedClaimsRequired = trustedClaimsRequired; }
public void setWriteApprovalRequired(Boolean writeApprovalRequired) { this.writeApprovalRequired = writeApprovalRequired; }
public void setRedisTraceEnabled(Boolean redisTraceEnabled) { this.redisTraceEnabled = redisTraceEnabled; }
public void setRedisTraceTtlSeconds(Long redisTraceTtlSeconds) { this.redisTraceTtlSeconds = redisTraceTtlSeconds; }
public void setToolTimeoutMillis(Long toolTimeoutMillis) { this.toolTimeoutMillis = toolTimeoutMillis; }
public void setRetryMaxAttempts(Integer retryMaxAttempts) { this.retryMaxAttempts = retryMaxAttempts; }
public void setRetryInitialBackoffMillis(Long retryInitialBackoffMillis) { this.retryInitialBackoffMillis = retryInitialBackoffMillis; }
public void setRetryBackoffMultiplier(Double retryBackoffMultiplier) { this.retryBackoffMultiplier = retryBackoffMultiplier; }
public void setRetryMaxBackoffMillis(Long retryMaxBackoffMillis) { this.retryMaxBackoffMillis = retryMaxBackoffMillis; }
public void setCircuitBreakerFailureThreshold(Integer circuitBreakerFailureThreshold) { this.circuitBreakerFailureThreshold = circuitBreakerFailureThreshold; }
public void setCircuitBreakerOpenMillis(Long circuitBreakerOpenMillis) { this.circuitBreakerOpenMillis = circuitBreakerOpenMillis; }
public void setEimsBaseUrl(String eimsBaseUrl) { this.eimsBaseUrl = eimsBaseUrl; }
public void setEimsTimeoutMillis(Long eimsTimeoutMillis) { this.eimsTimeoutMillis = eimsTimeoutMillis; }
public void setToolServerBaseUrl(String toolServerBaseUrl) { this.toolServerBaseUrl = toolServerBaseUrl; }
public void setToolServerApiKey(String toolServerApiKey) { this.toolServerApiKey = toolServerApiKey; }
public void setToolHeartbeatTimeoutSeconds(Long toolHeartbeatTimeoutSeconds) { this.toolHeartbeatTimeoutSeconds = toolHeartbeatTimeoutSeconds; }
public void setDefaultPageSize(Integer defaultPageSize) { this.defaultPageSize = defaultPageSize; }
public void setMaxPageSize(Integer maxPageSize) { this.maxPageSize = maxPageSize; }
public void setMaxPagesPerCall(Integer maxPagesPerCall) { this.maxPagesPerCall = maxPagesPerCall; }
public void setMaxStreamBytes(Long maxStreamBytes) { this.maxStreamBytes = maxStreamBytes; }
public void setMaxDurationSeconds(Long maxDurationSeconds) { this.maxDurationSeconds = maxDurationSeconds; }
public void setMaxResponseBytesFromTool(Long maxResponseBytesFromTool) { this.maxResponseBytesFromTool = maxResponseBytesFromTool; }
public void setLargeResponseMaxItemBytes(Long largeResponseMaxItemBytes) { this.largeResponseMaxItemBytes = largeResponseMaxItemBytes; }
public void setLargeResponseMaxPreviewItems(Integer largeResponseMaxPreviewItems) { this.largeResponseMaxPreviewItems = largeResponseMaxPreviewItems; }
public void setToolExecutorCorePoolSize(Integer toolExecutorCorePoolSize) { this.toolExecutorCorePoolSize = toolExecutorCorePoolSize; }
public void setToolExecutorMaxPoolSize(Integer toolExecutorMaxPoolSize) { this.toolExecutorMaxPoolSize = toolExecutorMaxPoolSize; }
public void setToolExecutorQueueCapacity(Integer toolExecutorQueueCapacity) { this.toolExecutorQueueCapacity = toolExecutorQueueCapacity; }
public void setToolExecutorKeepAliveSeconds(Long toolExecutorKeepAliveSeconds) { this.toolExecutorKeepAliveSeconds = toolExecutorKeepAliveSeconds; }
public String apiKey() { return apiKey; }
public boolean apiKeyEnabled() { return apiKey != null && !apiKey.isBlank(); }
public boolean auditEnabled() { return auditEnabled == null || auditEnabled; }
public boolean agentClaimsRequired() { return agentClaimsRequired == null || agentClaimsRequired; }
public boolean trustedClaimsRequired() { return trustedClaimsRequired == null || trustedClaimsRequired; }
public boolean writeApprovalRequired() { return writeApprovalRequired == null || writeApprovalRequired; }
public boolean redisTraceEnabled() { return redisTraceEnabled == null || redisTraceEnabled; }
public long redisTraceTtlSeconds() { return redisTraceTtlSeconds == null || redisTraceTtlSeconds < 1 ? 3_600 : redisTraceTtlSeconds; }
public long toolTimeoutMillis() { return toolTimeoutMillis == null || toolTimeoutMillis < 1 ? 5_000 : toolTimeoutMillis; }
public int retryMaxAttempts() { return retryMaxAttempts == null || retryMaxAttempts < 1 ? 3 : retryMaxAttempts; }
public long retryInitialBackoffMillis() { return retryInitialBackoffMillis == null || retryInitialBackoffMillis < 1 ? 1_000 : retryInitialBackoffMillis; }
public double retryBackoffMultiplier() { return retryBackoffMultiplier == null || retryBackoffMultiplier < 1.0 ? 2.0 : retryBackoffMultiplier; }
public long retryMaxBackoffMillis() { return retryMaxBackoffMillis == null || retryMaxBackoffMillis < 1 ? 5_000 : retryMaxBackoffMillis; }
public int circuitBreakerFailureThreshold() { return circuitBreakerFailureThreshold == null || circuitBreakerFailureThreshold < 1 ? 5 : circuitBreakerFailureThreshold; }
public long circuitBreakerOpenMillis() { return circuitBreakerOpenMillis == null || circuitBreakerOpenMillis < 1 ? 30_000 : circuitBreakerOpenMillis; }
public String eimsBaseUrl() { return eimsBaseUrl == null ? "" : eimsBaseUrl.trim(); }
public long eimsTimeoutMillis() { return eimsTimeoutMillis == null || eimsTimeoutMillis < 1 ? 3_000 : eimsTimeoutMillis; }
public String toolServerBaseUrl() { return toolServerBaseUrl == null || toolServerBaseUrl.isBlank() ? "http://localhost:9090" : toolServerBaseUrl.trim(); }
public String toolServerApiKey() { return toolServerApiKey == null ? "" : toolServerApiKey.trim(); }
public long toolHeartbeatTimeoutSeconds() { return toolHeartbeatTimeoutSeconds == null || toolHeartbeatTimeoutSeconds < 1 ? 30 : toolHeartbeatTimeoutSeconds; }
public int defaultPageSize() { return defaultPageSize == null || defaultPageSize < 1 ? 100 : defaultPageSize; }
public int maxPageSize() { return maxPageSize == null || maxPageSize < 1 ? 500 : maxPageSize; }
public int maxPagesPerCall() { return maxPagesPerCall == null || maxPagesPerCall < 1 ? 3 : maxPagesPerCall; }
public long maxStreamBytes() { return maxStreamBytes == null || maxStreamBytes < 1 ? 1_048_576 : maxStreamBytes; }
public long maxDurationSeconds() { return maxDurationSeconds == null || maxDurationSeconds < 1 ? 10 : maxDurationSeconds; }
public long maxResponseBytesFromTool() { return maxResponseBytesFromTool == null || maxResponseBytesFromTool < 1 ? 5_242_880 : maxResponseBytesFromTool; }
public long largeResponseMaxItemBytes() { return largeResponseMaxItemBytes == null || largeResponseMaxItemBytes < 1 ? 16_384 : largeResponseMaxItemBytes; }
public int largeResponseMaxPreviewItems() { return largeResponseMaxPreviewItems == null || largeResponseMaxPreviewItems < 1 ? 100 : largeResponseMaxPreviewItems; }
public int toolExecutorCorePoolSize() { return toolExecutorCorePoolSize == null || toolExecutorCorePoolSize < 1 ? 20 : toolExecutorCorePoolSize; }
public int toolExecutorMaxPoolSize() {
int core = toolExecutorCorePoolSize();
return toolExecutorMaxPoolSize == null || toolExecutorMaxPoolSize < core ? Math.max(core, 100) : toolExecutorMaxPoolSize;
}
public int toolExecutorQueueCapacity() { return toolExecutorQueueCapacity == null || toolExecutorQueueCapacity < 1 ? 200 : toolExecutorQueueCapacity; }
public long toolExecutorKeepAliveSeconds() { return toolExecutorKeepAliveSeconds == null || toolExecutorKeepAliveSeconds < 1 ? 60 : toolExecutorKeepAliveSeconds; }
public Set<String> allowedToolSet() {
if (allowedTools == null) {
return Set.of();
}
return allowedTools.stream()
.filter(tool -> tool != null && !tool.isBlank())
.map(String::trim)
.collect(Collectors.toUnmodifiableSet());
}
}

View File

@@ -0,0 +1,46 @@
package io.shinhanlife.dap.biz.mcp.gateway.config;
import io.shinhanlife.dap.biz.mcp.gateway.dto.ToolMetadata;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
* @package io.shinhanlife.dap.biz.mcp.gateway.config
* @className RedisConfig
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, ToolMetadata> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, ToolMetadata> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
// 1. Key는 무조건 String
template.setKeySerializer(new StringRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
// 2. Value는 Generic 직렬화기 사용 (생성자 인자 없음)
Jackson2JsonRedisSerializer<ToolMetadata> serializer = new Jackson2JsonRedisSerializer<>(ToolMetadata.class);
template.setValueSerializer(serializer);
template.setHashValueSerializer(serializer);
template.afterPropertiesSet();
return template;
}
}

View File

@@ -0,0 +1,20 @@
package io.shinhanlife.dap.biz.mcp.gateway.dto;
/**
* @package io.shinhanlife.dap.biz.mcp.gateway.dto
* @className OperationType
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
public enum OperationType {
READ,
WRITE
}

View File

@@ -0,0 +1,116 @@
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;
import java.util.Set;
import java.util.List;
import java.util.HashSet;
/**
* 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초당 허용 최대 요청 수
// 7. Gateway 코어 제어용 설정 필드 추가 (재시도, 타임아웃, 오퍼레이션 타입)
@Builder.Default
private OperationType operationType = OperationType.READ;
@Builder.Default
private Boolean retryEnabled = true;
@Builder.Default
private Integer circuitBreakerFailureThreshold = 0;
@Builder.Default
private Long circuitBreakerOpenMillis = 0L;
@Builder.Default
private Long timeoutMillis = 0L;
// --- Guardrail 호환성을 위한 메서드 추가 ---
public Set<String> allowedArguments() {
if (parametersSchema == null || !parametersSchema.containsKey("properties")) return Set.of();
return ((Map<String, Object>) parametersSchema.get("properties")).keySet();
}
public Set<String> requiredArguments() {
if (parametersSchema == null || !parametersSchema.containsKey("required")) return Set.of();
return new HashSet<>((List<String>) parametersSchema.get("required"));
}
}

View File

@@ -0,0 +1,49 @@
package io.shinhanlife.dap.biz.mcp.gateway.guardrail;
import io.shinhanlife.dap.biz.mcp.gateway.resilience.FailureType;
import io.shinhanlife.dap.biz.mcp.gateway.resilience.ToolExecutionException;
import io.shinhanlife.dap.biz.mcp.gateway.dto.ToolMetadata;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.springframework.stereotype.Service;
import java.util.Iterator;
@Service
/**
* Tool 호출 인자의 규격을 검증하는 Guardrail 서비스입니다.
*
* 필수값 누락, 허용되지 않은 인자 전달 같은 문제를 Tool 서버 호출 전에 차단합니다.
*/
public class GuardrailService {
/**
* ToolMetadata에 정의된 required/allowed arguments 기준으로 요청 인자를 검증합니다.
*/
public void validate(ToolMetadata metadata, ObjectNode arguments) {
if (metadata.getParametersSchema() == null) {
// 스키마 정보가 없으면(Fallback 툴 등) 검증을 생략합니다.
return;
}
Iterator<String> fieldNames = arguments.fieldNames();
while (fieldNames.hasNext()) {
String fieldName = fieldNames.next();
if (!metadata.allowedArguments().contains(fieldName)) {
throw new ToolExecutionException(FailureType.CLIENT_ERROR, "Unknown argument for " + metadata.getName() + ": " + fieldName);
}
}
for (String requiredArgument : metadata.requiredArguments()) {
JsonNode value = arguments.get(requiredArgument);
if (isMissing(value)) {
throw new ToolExecutionException(FailureType.CLIENT_ERROR, "Missing required argument for " + metadata.getName() + ": " + requiredArgument);
}
}
}
private boolean isMissing(JsonNode value) {
if (value == null || value.isNull() || value.isMissingNode()) {
return true;
}
return value.isTextual() && value.asText("").isBlank();
}
}

View File

@@ -0,0 +1,77 @@
package io.shinhanlife.dap.biz.mcp.gateway.guardrail;
import org.springframework.stereotype.Component;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
/**
* Audit Log, Redis Trace, Agent 응답 preview에 남으면 안 되는 민감정보를 마스킹합니다.
*/
@Component
public class SensitiveDataMasker {
private static final Set<String> SENSITIVE_KEYS = Set.of(
"password", "passwd", "pwd", "token", "accessToken", "refreshToken", "secret",
"ssn", "rrn", "residentNumber", "cardNumber", "accountNumber", "accountNo",
"phone", "mobile", "email", "idempotencyKey");
private static final Pattern EMAIL = Pattern.compile("([a-zA-Z0-9._%+-]{2})[a-zA-Z0-9._%+-]*(@[a-zA-Z0-9.-]+)");
private static final Pattern CARD_OR_ACCOUNT = Pattern.compile("\\b(\\d{4})\\d{4,12}(\\d{2,4})\\b");
private final ObjectMapper json;
public SensitiveDataMasker(ObjectMapper json) {
this.json = json;
}
/**
* JsonNode 전체를 재귀적으로 순회하며 민감 key와 민감 패턴을 마스킹합니다.
*/
public JsonNode mask(JsonNode input) {
if (input == null || input.isMissingNode() || input.isNull()) {
return json.createObjectNode();
}
if (input.isArray()) {
ArrayNode masked = json.createArrayNode();
for (JsonNode item : input) {
masked.add(mask(item));
}
return masked;
}
if (input.isObject()) {
ObjectNode masked = json.createObjectNode();
Iterator<Map.Entry<String, JsonNode>> fields = input.fields();
while (fields.hasNext()) {
Map.Entry<String, JsonNode> entry = fields.next();
String key = entry.getKey();
JsonNode value = entry.getValue();
if (isSensitiveKey(key)) {
masked.put(key, "***");
} else {
masked.set(key, mask(value));
}
}
return masked;
}
if (input.isTextual()) {
return json.valueToTree(maskText(input.asText()));
}
return input;
}
private boolean isSensitiveKey(String key) {
return key != null && SENSITIVE_KEYS.stream().anyMatch(sensitive -> sensitive.equalsIgnoreCase(key));
}
private String maskText(String value) {
if (value == null || value.isBlank()) {
return value;
}
String masked = EMAIL.matcher(value).replaceAll("$1***$2");
return CARD_OR_ACCOUNT.matcher(masked).replaceAll("$1********$2");
}
}

View File

@@ -0,0 +1,70 @@
package io.shinhanlife.dap.biz.mcp.gateway.guardrail;
import io.shinhanlife.dap.biz.mcp.gateway.resilience.FailureType;
import io.shinhanlife.dap.biz.mcp.gateway.resilience.ToolExecutionException;
import io.shinhanlife.dap.biz.mcp.gateway.dto.ToolMetadata;
import org.springframework.stereotype.Service;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
/**
* Tool 서버 응답을 Agent에게 전달하기 전에 환각 방어 규칙을 적용하는 서비스입니다.
*/
@Service
public class ToolResponseGuardrailService {
private final ObjectMapper json;
public ToolResponseGuardrailService(ObjectMapper json) {
this.json = json;
}
/**
* Tool 응답 data를 검증한 뒤 표준 MCP 응답 문자열로 감쌉니다.
*/
public String validateAndWrap(ToolMetadata metadata, String requestId, JsonNode data) {
validateData(metadata.getName(), data);
ObjectNode response;
if (data.isObject() && data.has("success") && data.has("data")) {
response = ((ObjectNode) data).deepCopy();
response.set("answerPolicy", answerPolicy());
} else {
response = json.createObjectNode();
response.put("success", true);
response.put("toolName", metadata.getName());
response.put("requestId", requestId);
response.put("source", "registered-tool-server");
response.set("data", data);
response.set("answerPolicy", answerPolicy());
}
try {
return json.writerWithDefaultPrettyPrinter().writeValueAsString(response);
} catch (Exception error) {
throw new ToolExecutionException(FailureType.INTERNAL_ERROR, "검증된 Tool 응답 직렬화에 실패했습니다.", error);
}
}
/**
* Tool 응답 data를 Agent에게 전달하기 전에 응답 규격으로 검증합니다.
*/
public void validateData(String toolName, JsonNode data) {
if (data == null || data.isMissingNode() || data.isNull()) {
throw new ToolExecutionException(FailureType.HALLUCINATION_GUARDRAIL, "Tool 응답 data가 비어 있습니다. tool=" + toolName);
}
// 향후 ToolMetadata에 responseAllowedFields 등이 추가되면 스키마 검증 로직 추가
}
/**
* Agent가 Tool 결과 밖의 내용을 추측하지 않도록 응답 정책을 함께 내려줍니다.
*/
private ObjectNode answerPolicy() {
ObjectNode policy = json.createObjectNode();
policy.put("allowOnlyToolData", true);
policy.put("noGuessing", true);
policy.put("onMissingData", "answer_unknown_or_request_more_information");
policy.put("instruction", "Tool이 반환한 data 필드만 사용하세요. data에 없는 값은 추측해서 만들지 마세요.");
return policy;
}
}

View File

@@ -0,0 +1,61 @@
package io.shinhanlife.dap.biz.mcp.gateway.messaging;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.stereotype.Service;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.Map;
import java.util.UUID;
/**
* @package io.shinhanlife.dap.biz.mcp.gateway.messaging
* @className KafkaProducerService
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class KafkaProducerService {
// Spring Kafka 제공 템플릿
private final KafkaTemplate<String, String> kafkaTemplate;
private final ObjectMapper jsonMapper;
/**
* 트래픽 초과 등의 이유로 즉시 처리가 불가능한 요청을 Kafka 대기열로 보냅니다.
* @param topic 발행할 Kafka 토픽명
* @param payload 원본 요청 페이로드
* @return 발급된 고유 티켓 ID
*/
public String queueRequest(String topic, Map<String, Object> payload) {
String ticketId = "TICKET-" + UUID.randomUUID().toString().substring(0, 8).toUpperCase();
// 페이로드에 티켓 ID 추가 (Consumer가 알 수 있도록)
payload.put("ticketId", ticketId);
payload.put("queuedAt", System.currentTimeMillis());
try {
String jsonPayload = jsonMapper.writeValueAsString(payload);
// 실제 Kafka 서버로 전송
kafkaTemplate.send(topic, ticketId, jsonPayload);
log.info(" [Kafka Queue] 요청 대기열 등록 완료 - Topic: {}, Ticket ID: {}", topic, ticketId);
} catch (Exception e) {
// 로컬 테스트 시 실제 Kafka 브로커가 없어서 나는 예외를 방어합니다.
// 실제 상용 환경에서는 Dead Letter Queue 등을 태우거나 예외 처리 정책에 따릅니다.
log.error(" [Kafka Queue] Kafka 서버 통신 실패 (Broker Down). Mock 모드로 티켓은 발급합니다. Error: {}", e.getMessage());
}
return ticketId;
}
}

View File

@@ -0,0 +1,166 @@
package io.shinhanlife.dap.biz.mcp.gateway.presentation;
import io.shinhanlife.dap.biz.mcp.gateway.dto.ToolMetadata;
import io.shinhanlife.dap.biz.mcp.gateway.registry.RedisRegistryService;
import io.shinhanlife.dap.biz.mcp.gateway.service.ExecuteService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.tool.ToolCallback;
import org.springframework.ai.tool.definition.ToolDefinition;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import com.fasterxml.jackson.databind.ObjectMapper;
import reactor.core.publisher.Flux;
import java.util.*;
@Slf4j
@RestController
@RequestMapping("/api/chat")
@RequiredArgsConstructor
public class ChatController {
private final ExecuteService executeService;
private final ObjectMapper objectMapper;
private final RedisRegistryService registryService;
private final ChatClient.Builder chatClientBuilder;
@PostMapping(produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter chatStream(@RequestBody Map<String, String> request,
@RequestHeader(value = "X-Agent-Id", required = false) String agentId,
@RequestHeader(value = "X-Tenant-Id", required = false, defaultValue = "system") String tenantId) {
String effectiveTenantId = (agentId != null && !agentId.trim().isEmpty()) ? agentId : tenantId;
String message = request.getOrDefault("message", "").trim();
log.info("[Real AI Chat] 사용자의 메시지 수신: {}", message);
SseEmitter emitter = new SseEmitter(120000L); // 120 seconds timeout
try {
List<ToolCallback> callbacks = new ArrayList<>();
for (ToolMetadata meta : registryService.getAllTools()) {
if (Boolean.TRUE.equals(meta.getVisible())) {
callbacks.add(new DynamicMcpToolCallback(meta, executeService, objectMapper, effectiveTenantId));
}
}
ChatClient chatClient = chatClientBuilder
.defaultSystem("You are AX HUB Assistant, a highly capable enterprise AI agent. You must use the provided tools to answer user questions when necessary. Always answer politely in Korean.")
.build();
Flux<String> responseStream = chatClient.prompt()
.user(message)
.tools((Object[]) callbacks.toArray(new ToolCallback[0])) // Spring AI 2.0 uses tools()
.stream()
.content();
responseStream.subscribe(
chunk -> {
try {
if (chunk != null) {
emitter.send(chunk);
}
} catch (Exception e) {
emitter.completeWithError(e);
}
},
error -> {
log.error("[Real AI Chat] 스트리밍 중 에러 발생", error);
try {
String errorMsg = error.getMessage();
if (errorMsg != null && (errorMsg.contains("timeout") || errorMsg.contains("OpenAIIoException"))) {
emitter.send("\n\n⚠ **요청 시간이 초과되었습니다.** (현재 여러 개의 도구를 분석하느라 모델의 응답이 지연되었습니다. 해당하는 도구가 없거나 너무 복잡한 요청일 수 있습니다.)");
} else if (errorMsg != null && errorMsg.contains("503")) {
emitter.send("\n\n⚠ **AI 모델 서버 혼잡 (503)**: 현재 AI 모델을 제공하는 서버에 일시적으로 접속자가 많아 지연이 발생하고 있습니다. 잠시 후 다시 시도해 주세요.");
} else if (errorMsg != null && errorMsg.contains("429")) {
emitter.send("\n\n⚠ **API 사용량 초과 (429)**: 현재 사용 중인 Gemini API(무료 티어)의 일일 또는 분당 요청 한도를 초과했습니다. 잠시 후 다시 시도하시거나 API 플랜을 확인해 주세요.");
} else {
emitter.send("\n[에러 발생: " + errorMsg + "]");
}
} catch (Exception ignore) {}
emitter.complete(); // 클라이언트 측에서 네트워크 에러로 처리하지 않도록 정상 종료
},
() -> {
log.info("[Real AI Chat] 스트리밍 완료");
emitter.complete();
}
);
} catch (Exception e) {
log.error("[Real AI Chat] 초기화 중 에러 발생", e);
try {
emitter.send("초기화 중 에러가 발생했습니다: " + e.getMessage());
} catch (Exception ignore) {}
emitter.completeWithError(e);
}
return emitter;
}
private static class DynamicMcpToolCallback implements ToolCallback {
private final ToolMetadata metadata;
private final ExecuteService executeService;
private final ObjectMapper objectMapper;
private final ToolDefinition toolDefinition;
private final String tenantId;
public DynamicMcpToolCallback(ToolMetadata metadata, ExecuteService executeService, ObjectMapper objectMapper, String tenantId) {
this.metadata = metadata;
this.executeService = executeService;
this.objectMapper = objectMapper;
this.tenantId = tenantId;
String schema = "{\"type\":\"object\",\"properties\":{}}";
try {
if (metadata.getParametersSchema() != null) {
schema = objectMapper.writeValueAsString(metadata.getParametersSchema());
}
} catch (Exception e) {
log.warn("Schema parsing error for tool: {}", metadata.getName());
}
this.toolDefinition = ToolDefinition.builder()
.name(metadata.getName().replaceAll("[^a-zA-Z0-9_-]", "_"))
.description(metadata.getDescription() != null ? metadata.getDescription() : "No description provided")
.inputSchema(schema)
.build();
}
@Override
public ToolDefinition getToolDefinition() {
return this.toolDefinition;
}
@Override
public String call(String toolInput) {
log.info("[Function Calling] LLM이 '{}' 툴을 호출했습니다. 입력값: {}", metadata.getName(), toolInput);
try {
Map<String, Object> payload = new HashMap<>();
payload.put("jsonrpc", "2.0");
payload.put("method", "tools/call");
payload.put("id", UUID.randomUUID().toString());
Map<String, Object> params = new HashMap<>();
params.put("name", metadata.getName());
if (toolInput != null && !toolInput.trim().isEmpty()) {
Map<String, Object> arguments = objectMapper.readValue(toolInput, Map.class);
params.put("arguments", arguments);
} else {
params.put("arguments", new HashMap<>());
}
payload.put("params", params);
Object result = executeService.execute(payload, this.tenantId);
String jsonResult = objectMapper.writeValueAsString(result);
log.info("[Function Calling] '{}' 툴 실행 완료. 결과: {}", metadata.getName(), jsonResult);
return jsonResult;
} catch (Exception e) {
log.error("[Function Calling] '{}' 툴 실행 중 에러", metadata.getName(), e);
return "{\"error\": \"" + e.getMessage() + "\"}";
}
}
}
}

View File

@@ -0,0 +1,69 @@
package io.shinhanlife.dap.biz.mcp.gateway.presentation;
import io.shinhanlife.dap.biz.mcp.gateway.service.KillSwitchService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
/**
* @package io.shinhanlife.dap.biz.mcp.gateway.presentation
* @className KillSwitchController
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
@RestController
@RequestMapping("/mcp/api/v1/admin/kill-switch")
@RequiredArgsConstructor
public class KillSwitchController {
private final KillSwitchService killSwitchService;
/**
* 관리자가 비상 차단(Kill Switch) 상태를 토글합니다.
* @param type "agent", "tool", "route" 중 하나
* @param target 차단할 대상의 ID 또는 이름
* @param state true(차단), false(차단 해제)
*/
@PostMapping("/{type}/{target}")
public ResponseEntity<?> toggleKillSwitch(
@PathVariable String type,
@PathVariable String target,
@RequestParam boolean state) {
killSwitchService.toggleKillSwitch(type, target, state);
return ResponseEntity.ok(Map.of(
"message", "Kill Switch 상태 변경 완료",
"type", type,
"target", target,
"state", state
));
}
/**
* 관리자가 비상 차단(Kill Switch) 키를 Redis에서 완전히 삭제합니다.
*/
@DeleteMapping("/{type}/{target}")
public ResponseEntity<?> removeKillSwitch(
@PathVariable String type,
@PathVariable String target) {
killSwitchService.removeKillSwitch(type, target);
return ResponseEntity.ok(Map.of(
"message", "Kill Switch 키 삭제 완료 (차단 해제)",
"type", type,
"target", target
));
}
}

View File

@@ -0,0 +1,238 @@
package io.shinhanlife.dap.biz.mcp.gateway.presentation;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.shinhanlife.dap.biz.mcp.adapter.dto.JsonRpcRequest;
import io.shinhanlife.dap.biz.mcp.adapter.dto.JsonRpcResponse;
import io.shinhanlife.dap.biz.mcp.adapter.dto.Params;
import io.shinhanlife.dap.biz.mcp.gateway.config.GatewayFallbackProperties;
import io.shinhanlife.dap.biz.mcp.gateway.dto.ToolMetadata;
import io.shinhanlife.dap.biz.mcp.gateway.registry.RedisRegistryService;
import io.shinhanlife.dap.biz.mcp.gateway.service.ExecuteService;
import io.shinhanlife.dap.common.mcp.security.SecurityProperties;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestClient;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
@Slf4j
@RestController
@RequestMapping("/mcp/api/v1")
@Tag(name = "MCP Router API", description = "AI Agent의 요청을 받아 Adapter 시스템으로 라우팅하는 게이트웨이 API")
public class McpRouterController {
private final RedisRegistryService redisRegistryService;
private final ExecuteService executeService;
private final SecurityProperties securityProperties;
private final ObjectMapper objectMapper;
private final GatewayFallbackProperties gatewayFallbackProperties;
private final RestClient restClient;
public McpRouterController(RedisRegistryService redisRegistryService,
ExecuteService executeService,
SecurityProperties securityProperties,
ObjectMapper objectMapper,
GatewayFallbackProperties gatewayFallbackProperties) {
this.redisRegistryService = redisRegistryService;
this.executeService = executeService;
this.securityProperties = securityProperties;
this.objectMapper = objectMapper;
this.gatewayFallbackProperties = gatewayFallbackProperties;
// [수정됨] 1초 타임아웃을 강제하여 죽은 서버 대기로 인한 지연 방지
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
factory.setConnectTimeout(1000); // 연결 시도 타임아웃 1초
factory.setReadTimeout(1000); // 응답 대기 타임아웃 1초
this.restClient = RestClient.builder().requestFactory(factory).build();
}
@PostMapping("/tools/call")
public ResponseEntity<?> callTool(@RequestBody Map<String, Object> payload,
@RequestHeader(value = "X-Agent-Id", required = false) String agentId,
@RequestHeader(value = "X-Tenant-Id", required = false, defaultValue = "system") String tenantId) {
String effectiveTenantId = (agentId != null && !agentId.trim().isEmpty()) ? agentId : tenantId;
try {
log.info("[Admin UI -> MCP Gateway] 동적 툴 실행 요청 수신: {}", objectMapper.writeValueAsString(payload));
} catch (Exception ex) {
log.info("[Admin UI -> MCP Gateway] 동적 툴 실행 요청 수신: {}", payload);
}
try {
Object result = executeService.execute(payload, effectiveTenantId);
io.shinhanlife.dap.biz.mcp.adapter.dto.JsonRpcResponse response = new io.shinhanlife.dap.biz.mcp.adapter.dto.JsonRpcResponse();
response.setJsonrpc("2.0");
response.setId(payload.containsKey("id") ? String.valueOf(payload.get("id")) : UUID.randomUUID().toString());
response.setResult(result);
try {
log.info("[MCP Gateway -> Admin UI] 동적 툴 실행 결과 반환: {}", objectMapper.writeValueAsString(response));
} catch (Exception ex) {
log.info("[MCP Gateway -> Admin UI] 동적 툴 실행 결과 반환: {}", response);
}
return ResponseEntity.ok(response);
} catch (SecurityException se) {
log.warn(" [보안 차단] 권한 오류: {}", se.getMessage());
return ResponseEntity.status(403).body(Map.of("error", se.getMessage()));
} catch (Exception e) {
log.error(" 파이프라인 실행 중 오류:", e);
return ResponseEntity.internalServerError().body(Map.of("error", e.getMessage()));
}
}
@GetMapping("/tools/list")
public ResponseEntity<JsonRpcResponse> listTools(
@RequestParam(value = "categoryKey", required = false) String categoryKey) {
List<ToolMetadata> activeTools = redisRegistryService.getAllTools()
.stream()
.filter(ToolMetadata::getVisible)
.collect(Collectors.toList());
Set<String> knownTools = activeTools.stream()
.map(ToolMetadata::getUid)
.collect(Collectors.toSet());
Set<String> fallbackUrls = new HashSet<>(gatewayFallbackProperties.getRoutes().values());
if (gatewayFallbackProperties.getDefaultUrl() != null) {
fallbackUrls.add(gatewayFallbackProperties.getDefaultUrl());
}
for (String url : fallbackUrls) {
try {
List<ToolMetadata> localTools = restClient.get()
.uri(url + "/mcp/api/v1/tools/local")
.retrieve()
.body(new ParameterizedTypeReference<List<ToolMetadata>>() {});
if (localTools != null) {
for (ToolMetadata t : localTools) {
if (!Boolean.TRUE.equals(t.getIsRegistered()) && Boolean.TRUE.equals(t.getVisible()) && !knownTools.contains(t.getUid())) {
activeTools.add(t);
knownTools.add(t.getUid());
}
}
}
} catch (Exception e) {
log.warn("Failed to fetch local tools from fallback URL: {}", url);
}
}
if (categoryKey != null && !categoryKey.trim().isEmpty()) {
activeTools = activeTools.stream()
.filter(t -> categoryKey.equals(t.getCategoryKey()))
.collect(Collectors.toList());
}
JsonRpcResponse response = new JsonRpcResponse();
response.setId(UUID.randomUUID().toString());
response.setResult(Map.of("tools", activeTools));
return ResponseEntity.ok(response);
}
@GetMapping(value = "/tools/docs/markdown", produces = "text/markdown;charset=UTF-8")
public ResponseEntity<String> generateToolsMarkdown() {
List<ToolMetadata> tools = redisRegistryService.getAllTools()
.stream()
.filter(ToolMetadata::getVisible)
.collect(Collectors.toList());
StringBuilder md = new StringBuilder();
md.append("# \uD83E\uDD16 Shinhan AI Tool Catalog\n\n");
md.append("**총 등록된 툴:** ").append(tools.size()).append("\n");
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
md.append("**마지막 업데이트:** ").append(LocalDateTime.now().format(formatter)).append("\n\n");
md.append("---\n\n");
// Group by Domain Group
Map<String, List<ToolMetadata>> groupedTools = new HashMap<>();
for (ToolMetadata tool : tools) {
String group = tool.getCategoryKey() != null ? tool.getCategoryKey() : "기타 (Others)";
groupedTools.computeIfAbsent(group, k -> new ArrayList<>()).add(tool);
}
for (Map.Entry<String, List<ToolMetadata>> entry : groupedTools.entrySet()) {
md.append("## \uD83D\uDCC1 도메인: ").append(entry.getKey()).append("\n\n");
int index = 1;
for (ToolMetadata tool : entry.getValue()) {
md.append("### ").append(index++).append(". ").append(tool.getUid()).append("\n");
if (tool.getDescription() != null) {
md.append("- **설명**: ").append(tool.getDescription()).append("\n");
}
md.append("- **연동 방식**: `").append(tool.getIntegrationType() != null ? tool.getIntegrationType() : "DIRECT").append("`\n\n");
// Parameters Table
if (tool.getParametersSchema() != null && tool.getParametersSchema().containsKey("properties")) {
md.append("#### \u2699\uFE0F 파라미터 (Parameters)\n");
md.append("| 파라미터명 | 타입 | 필수 여부 | 설명 |\n");
md.append("|---|---|---|---|\n");
Map<String, Object> properties = (Map<String, Object>) tool.getParametersSchema().get("properties");
List<String> required = (List<String>) tool.getParametersSchema().get("required");
for (Map.Entry<String, Object> prop : properties.entrySet()) {
String name = prop.getKey();
Map<String, Object> details = (Map<String, Object>) prop.getValue();
String type = details.containsKey("type") ? String.valueOf(details.get("type")) : "string";
String desc = details.containsKey("description") ? String.valueOf(details.get("description")) : "";
String req = (required != null && required.contains(name)) ? "Y" : "N";
md.append("| `").append(name).append("` | `").append(type).append("` | ").append(req).append(" | ").append(desc).append(" |\n");
}
md.append("\n");
}
// Action Prompts
if (tool.getActionPrompts() != null && !tool.getActionPrompts().isEmpty()) {
md.append("#### \uD83D\uDCAC 프롬프트 예시 (Action Prompts)\n");
for (Map.Entry<String, String> prompt : tool.getActionPrompts().entrySet()) {
md.append("- \"").append(prompt.getValue()).append("\"\n");
}
md.append("\n");
}
md.append("---\n\n");
}
}
return ResponseEntity.ok(md.toString());
}
@PostMapping("/registry/register")
public ResponseEntity<String> registerTool(@RequestBody ToolMetadata meta) {
redisRegistryService.saveTool(meta);
return ResponseEntity.ok("Registered");
}
@PostMapping("/registry/deregister")
public ResponseEntity<String> deregisterTool(@RequestBody String uid) {
redisRegistryService.removeTool(uid);
return ResponseEntity.ok("Deregistered");
}
@PostMapping("/registry/heartbeat")
public ResponseEntity<String> heartbeat(@RequestBody String uid) {
boolean success = redisRegistryService.refreshHeartbeat(uid);
if (success) {
return ResponseEntity.ok("Heartbeat updated");
} else {
return ResponseEntity.status(404).body("Tool not found");
}
}
}

View File

@@ -0,0 +1,90 @@
package io.shinhanlife.dap.biz.mcp.gateway.presentation;
import io.shinhanlife.dap.common.util.PodScaffolder;
import io.shinhanlife.dap.common.util.ToolScaffolder;
import io.shinhanlife.dap.common.util.ToolSourceUpdater;
import java.io.File;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/v1/scaffold")
public class ScaffoldingController {
@PostMapping("/pod")
public String scaffoldPod(@RequestBody Map<String, String> req) {
try {
String moduleName = req.getOrDefault("moduleName", "axhub-tool-other");
if (!moduleName.startsWith("axhub-tool-")) moduleName = "axhub-tool-" + moduleName;
String port = req.getOrDefault("port", "8085");
String shortName = moduleName.replace("axhub-tool-", "").replace("-", "");
String author = req.get("author");
if (author == null || author.trim().isEmpty()) author = System.getProperty("user.name");
String date = req.get("date");
if (date == null || date.trim().isEmpty()) date = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy.MM.dd"));
return PodScaffolder.scaffoldPod(moduleName, port, shortName, author, date);
} catch (Exception e) {
return "오류 발생: " + e.getMessage();
}
}
@PostMapping("/tool")
public String scaffoldTool(@RequestBody Map<String, String> req) {
try {
String baseName = req.get("baseName");
String interfaceId = req.get("interfaceId");
String description = req.get("description");
String group = req.getOrDefault("categoryKey", req.getOrDefault("group", "COMMON"));
String routingType = req.getOrDefault("routingType", "HTTP");
String moduleName = req.getOrDefault("moduleName", "axhub-tool-other");
String author = req.get("author");
if (author == null || author.trim().isEmpty()) author = System.getProperty("user.name");
String date = req.get("date");
if (date == null || date.trim().isEmpty()) date = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy.MM.dd"));
boolean register = Boolean.parseBoolean(req.getOrDefault("register", "true"));
return ToolScaffolder.scaffold(baseName, interfaceId, description, group, routingType, moduleName, author, date, register);
} catch (Exception e) {
return "오류 발생: " + e.getMessage();
}
}
@PostMapping("/tool/update")
public String updateTool(@RequestBody Map<String, String> req) {
try {
String toolName = req.get("toolName");
String domainGroup = req.getOrDefault("categoryKey", req.get("domainGroup"));
String description = req.get("description");
boolean register = Boolean.parseBoolean(req.getOrDefault("register", "true"));
Boolean requiresApproval = req.containsKey("requiresApproval") ? Boolean.parseBoolean(req.get("requiresApproval")) : null;
ToolSourceUpdater.updateToolSource(toolName, domainGroup, description, register, requiresApproval);
return "성공";
} catch (Exception e) {
return "오류 발생: " + e.getMessage();
}
}
@GetMapping("/modules")
public List<String> listModules() {
try {
String sourceDir = System.getenv("AXHUB_SOURCE_DIR");
if (sourceDir == null) sourceDir = System.getProperty("user.dir");
File dir = new File(sourceDir);
File[] files = dir.listFiles(f -> f.isDirectory() && f.getName().startsWith("axhub-tool-") && !f.getName().equals("axhub-tool-core"));
if (files == null) return List.of("axhub-tool-other");
return Arrays.stream(files).map(File::getName).sorted().collect(Collectors.toList());
} catch (Exception e) {
return List.of("axhub-tool-other");
}
}
}

View File

@@ -0,0 +1,40 @@
package io.shinhanlife.dap.biz.mcp.gateway.redis;
import org.springframework.stereotype.Service;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.io.IOException;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
@Service
public class McpMonitorEventService {
private final List<SseEmitter> emitters = new CopyOnWriteArrayList<>();
/**
* MCP Monitor browser screen subscribes to this emitter.
*/
public SseEmitter subscribe() {
SseEmitter emitter = new SseEmitter(0L);
emitters.add(emitter);
emitter.onCompletion(() -> emitters.remove(emitter));
emitter.onTimeout(() -> emitters.remove(emitter));
emitter.onError(error -> emitters.remove(emitter));
return emitter;
}
/**
* Sends one monitor event only when an Agent tool request changes state inside MCP.
*/
public void publish(String payload) {
for (SseEmitter emitter : emitters) {
try {
emitter.send(SseEmitter.event()
.name("mcp-tool-trace")
.data(payload));
} catch (IOException | IllegalStateException error) {
emitters.remove(emitter);
}
}
}
}

View File

@@ -0,0 +1,253 @@
package io.shinhanlife.dap.biz.mcp.gateway.redis;
import io.shinhanlife.dap.biz.mcp.gateway.config.McpGatewayProperties;
import io.shinhanlife.dap.biz.mcp.gateway.guardrail.SensitiveDataMasker;
import io.shinhanlife.dap.biz.mcp.gateway.security.McpRequestContext;
import io.shinhanlife.dap.biz.mcp.gateway.dto.ToolMetadata;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.time.Duration;
import java.time.Instant;
import java.util.HexFormat;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Service
public class RedisToolTraceService {
private static final Logger log = LoggerFactory.getLogger(RedisToolTraceService.class);
private static final String KEY_PREFIX = "mcp:request:";
private static final String RECENT_KEY = "mcp:request:recent";
private final McpGatewayProperties properties;
private final ObjectProvider<StringRedisTemplate> redisProvider;
private final ObjectMapper json;
private final McpMonitorEventService monitorEvents;
private final SensitiveDataMasker masker;
private final Map<String, AttemptState> attemptStates = new ConcurrentHashMap<>();
public RedisToolTraceService(McpGatewayProperties properties,
ObjectProvider<StringRedisTemplate> redisProvider,
ObjectMapper json,
McpMonitorEventService monitorEvents,
SensitiveDataMasker masker) {
this.properties = properties;
this.redisProvider = redisProvider;
this.json = json;
this.monitorEvents = monitorEvents;
this.masker = masker;
}
/**
* Agent request has entered the MCP tool gateway.
*/
public void started(McpRequestContext context, ToolMetadata metadata, ObjectNode arguments) {
save(context, metadata, arguments, "STARTED", 0, "", 0, 0, 0,
"Agent request entered MCP", "");
}
/**
* MCP is attempting to call the target Tool server.
*/
public void attemptStarted(McpRequestContext context, ToolMetadata metadata, ObjectNode arguments,
int attempt, int maxAttempts) {
save(context, metadata, arguments, "ATTEMPTING", 0, "", attempt, maxAttempts, 0,
"Calling tool server", "");
}
/**
* MCP will retry the Tool call after backoff.
*/
public void retryWaiting(McpRequestContext context, ToolMetadata metadata, ObjectNode arguments,
int failedAttempt, int maxAttempts, long backoffMillis, String failureType) {
save(context, metadata, arguments, "RETRY_WAITING", 0, failureType, failedAttempt, maxAttempts, backoffMillis,
"Retry will be attempted after backoff", "");
}
/**
* Tool execution has finished.
*/
public void finished(McpRequestContext context, ToolMetadata metadata, ObjectNode arguments,
long elapsedMillis, boolean success, String failureType, String responseText) {
save(context, metadata, arguments, success ? "SUCCESS" : "FAILED", elapsedMillis, failureType, 0, 0, 0,
success ? "Tool call completed" : "Tool call failed", responseText);
}
/**
* Recent live trace entries. One current entry is kept per request id.
*/
public List<String> recent() {
if (!properties.redisTraceEnabled()) {
return List.of("Redis trace is disabled. Set MCP_REDIS_TRACE_ENABLED=true.");
}
StringRedisTemplate redis = redisProvider.getIfAvailable();
if (redis == null) {
return List.of("RedisTemplate is not available.");
}
try {
List<String> requestIds = redis.opsForList().range(RECENT_KEY, 0, 49);
if (requestIds == null || requestIds.isEmpty()) {
return List.of();
}
return requestIds.stream()
.map(requestId -> redis.opsForValue().get(KEY_PREFIX + requestId))
.filter(value -> value != null && !value.isBlank())
.toList();
} catch (Exception error) {
log.warn("Redis trace read failed. message={}", error.getMessage());
return List.of("Redis trace read failed: " + error.getMessage());
}
}
/**
* Historical accumulation is intentionally disabled.
*
* The MCP monitor is used to observe the current Agent request flow only,
* so this method returns the same live entries as recent().
*/
public List<String> history() {
return recent();
}
private void save(McpRequestContext context, ToolMetadata metadata, ObjectNode arguments, String status,
long elapsedMillis, String failureType, int attempt, int maxAttempts,
long backoffMillis, String message, String responseText) {
String stateKey = stateKey(context, metadata);
if (attempt > 0 && maxAttempts > 0) {
attemptStates.put(stateKey, new AttemptState(attempt, maxAttempts));
}
String payload;
try {
payload = tracePayload(context, metadata, arguments, status, elapsedMillis, failureType, attempt,
maxAttempts, backoffMillis, message, responseText);
monitorEvents.publish(payload);
if ("SUCCESS".equals(status) || "FAILED".equals(status)) {
attemptStates.remove(stateKey);
}
} catch (Exception error) {
log.warn("MCP monitor event publish failed. requestId={} tool={} message={}",
context.requestId(), metadata.getName(), error.getMessage());
return;
}
if (!properties.redisTraceEnabled()) {
return;
}
StringRedisTemplate redis = redisProvider.getIfAvailable();
if (redis == null) {
return;
}
try {
String requestId = context.requestId();
Duration ttl = Duration.ofSeconds(properties.redisTraceTtlSeconds());
redis.opsForValue().set(KEY_PREFIX + requestId, payload, ttl);
redis.opsForList().remove(RECENT_KEY, 0, requestId);
redis.opsForList().leftPush(RECENT_KEY, requestId);
redis.opsForList().trim(RECENT_KEY, 0, 49);
redis.expire(RECENT_KEY, ttl);
} catch (Exception error) {
log.warn("Redis trace save failed. requestId={} tool={} message={}",
context.requestId(), metadata.getName(), error.getMessage());
}
}
String tracePayload(McpRequestContext context, ToolMetadata metadata, ObjectNode arguments, String status,
long elapsedMillis, String failureType, int attempt, int maxAttempts,
long backoffMillis, String message, String responseText) throws Exception {
AttemptState state = attemptStates.get(stateKey(context, metadata));
int displayAttempt = attempt > 0 ? attempt : state == null ? 0 : state.attempt();
int displayMaxAttempts = maxAttempts > 0 ? maxAttempts : state == null ? 0 : state.maxAttempts();
int retryCount = Math.max(0, displayAttempt - 1);
Map<String, Object> trace = new LinkedHashMap<>();
trace.put("requestId", context.requestId());
trace.put("traceGroupId", context.traceGroupId());
trace.put("agentId", context.agentId());
trace.put("userId", context.userId());
trace.put("clientAddress", context.clientAddress());
trace.put("toolName", metadata.getName());
trace.put("operationType", metadata.getOperationType() != null ? metadata.getOperationType().name() : "READ");
trace.put("status", status);
trace.put("message", message == null ? "" : message);
trace.put("failureType", failureType == null ? "" : failureType);
trace.put("attempt", displayAttempt);
trace.put("maxAttempts", displayMaxAttempts);
trace.put("retryCount", retryCount);
trace.put("backoffMillis", backoffMillis);
trace.put("elapsedMillis", elapsedMillis);
trace.put("idempotencyKeyHash", hashText(arguments.path("idempotencyKey").asText("")));
List<String> argNames = new ArrayList<>();
arguments.fieldNames().forEachRemaining(argNames::add);
trace.put("argumentNames", argNames);
trace.put("arguments", masker.mask(arguments).toString());
trace.put("responseSummary", responseSummary(responseText));
trace.put("timestamp", Instant.now().toString());
return json.writeValueAsString(trace);
}
/**
* Redis Trace에는 Tool 응답 원문을 저장하지 않고 size/hash/상태/카운트만 저장합니다.
*/
Map<String, Object> responseSummary(String responseText) {
Map<String, Object> summary = new LinkedHashMap<>();
if (responseText == null || responseText.isBlank()) {
summary.put("bytes", 0);
summary.put("sha256", "");
return summary;
}
byte[] bytes = responseText.getBytes(StandardCharsets.UTF_8);
summary.put("bytes", bytes.length);
summary.put("sha256", sha256(bytes));
try {
JsonNode root = json.readTree(responseText);
summary.put("success", root.path("success").asBoolean(false));
summary.put("status", root.path("status").asText(""));
summary.put("toolName", root.path("toolName").asText(""));
summary.put("pageSize", root.path("pageSize").asInt(0));
summary.put("pageCount", root.path("pageCount").asInt(0));
summary.put("returnedCount", root.path("returnedCount").asInt(0));
summary.put("totalCount", root.path("totalCount").asLong(0));
summary.put("hasMore", root.path("hasMore").asBoolean(false));
summary.put("nextCursor", root.path("nextCursor").asText(""));
summary.put("message", root.path("message").asText(""));
summary.put("resultRef", root.path("resultRef").asText(""));
} catch (Exception ignored) {
summary.put("parseable", false);
}
return summary;
}
private String sha256(byte[] bytes) {
try {
return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(bytes));
} catch (Exception error) {
return "";
}
}
private String hashText(String value) {
if (value == null || value.isBlank()) {
return "";
}
return sha256(value.getBytes(StandardCharsets.UTF_8));
}
private String stateKey(McpRequestContext context, ToolMetadata metadata) {
return context.requestId() + ":" + metadata.getName();
}
private record AttemptState(int attempt, int maxAttempts) {
}
}

View File

@@ -0,0 +1,110 @@
package io.shinhanlife.dap.biz.mcp.gateway.registry;
import io.shinhanlife.dap.biz.mcp.gateway.dto.ToolMetadata;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.time.Duration;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.Objects;
/**
* @package io.shinhanlife.dap.biz.mcp.gateway.registry
* @className RedisRegistryService
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class RedisRegistryService {
private final RedisTemplate<String, ToolMetadata> redisTemplate;
private static final String KEY_PREFIX = "mcp:tool:";
private static final Duration DEFAULT_TTL = Duration.ofSeconds(45); // 실무 하트비트 주기 반영
/**
* 툴 등록 및 갱신 (TTL 기반으로 60초 뒤 자동 만료)
*/
public void saveTool(ToolMetadata meta) {
String key = KEY_PREFIX + meta.getUid();
meta.setLastHeartbeat(System.currentTimeMillis());
redisTemplate.opsForValue().set(key, meta, DEFAULT_TTL);
log.info(" [RedisRegistry] 툴 등록 완료: {}", meta.getUid());
}
/**
* 하트비트 갱신 (TTL 초기화)
*/
public boolean refreshHeartbeat(String uid) {
String key = KEY_PREFIX + uid;
Boolean exists = redisTemplate.expire(key, DEFAULT_TTL);
if (Boolean.TRUE.equals(exists)) {
log.debug(" [RedisRegistry] 하트비트 갱신: {}", uid);
return true;
} else {
log.warn(" [RedisRegistry] 존재하지 않는 툴에 대한 하트비트 요청: {}", uid);
return false;
}
}
public List<String> getAvailablePods(String uid) {
ToolMetadata tool = getTool(uid);
if (tool != null && tool.getPodUrl() != null) {
return List.of(tool.getPodUrl());
}
return List.of();
}
/**
* 실행 시 툴 정보 조회 (Tool Execution 시 참조)
*/
public ToolMetadata getTool(String uid) {
return redisTemplate.opsForValue().get(KEY_PREFIX + uid);
}
/**
* 툴 이름으로 정보 조회
*/
public ToolMetadata getToolByName(String name) {
List<ToolMetadata> allTools = getAllTools();
for (ToolMetadata tool : allTools) {
if (name.equals(tool.getName())) {
return tool;
}
}
return null;
}
/**
* 등록된 모든 활성 툴 목록 조회
*/
public List<ToolMetadata> getAllTools() {
Set<String> keys = redisTemplate.keys(KEY_PREFIX + "*");
if (keys == null || keys.isEmpty()) {
return List.of();
}
List<ToolMetadata> tools = redisTemplate.opsForValue().multiGet(keys);
return tools != null ? tools.stream().filter(Objects::nonNull).collect(Collectors.toList()) : List.of();
}
/**
* 툴 명시적 제거 (Deregister)
*/
public void removeTool(String uid) {
redisTemplate.delete(KEY_PREFIX + uid);
log.info(" [RedisRegistry] 툴 삭제 완료: {}", uid);
}
}

View File

@@ -0,0 +1,134 @@
package io.shinhanlife.dap.biz.mcp.gateway.resilience;
import java.time.Clock;
import java.time.Instant;
/**
* Tool별 장애 확산을 막기 위한 Circuit Breaker입니다.
*
* 현재 OPEN 조건은 사용자가 요청한 기준에 맞춰
* TIMEOUT 3회 이상 또는 SERVER_ERROR(5xx) 3회 이상입니다.
*/
public class CircuitBreaker {
private static final int TIMEOUT_OPEN_THRESHOLD = 3;
private static final int SERVER_ERROR_OPEN_THRESHOLD = 3;
private final String name;
private final long openDurationMillis;
private final Clock clock;
private CircuitBreakerState state = CircuitBreakerState.CLOSED;
private int timeoutFailures;
private int serverErrorFailures;
private FailureType lastFailureType;
private String openedReason = "";
private Instant openedAt;
public CircuitBreaker(String name, int failureThreshold, long openDurationMillis, Clock clock) {
this.name = name;
this.openDurationMillis = Math.max(1, openDurationMillis);
this.clock = clock;
}
/**
* 실제 Tool 호출 전에 현재 회로가 호출 가능한 상태인지 확인합니다.
*/
public synchronized void beforeCall() {
if (state != CircuitBreakerState.OPEN) {
return;
}
long openedMillis = openedAt == null ? 0 : clock.millis() - openedAt.toEpochMilli();
if (openedMillis >= openDurationMillis) {
state = CircuitBreakerState.HALF_OPEN;
return;
}
throw new ToolExecutionException(FailureType.CIRCUIT_OPEN, "Circuit breaker is open: " + name);
}
/**
* 호출 성공 시 실패 카운터를 초기화하고 CLOSED 상태로 복구합니다.
*/
public synchronized void recordSuccess() {
timeoutFailures = 0;
serverErrorFailures = 0;
lastFailureType = null;
openedReason = "";
openedAt = null;
state = CircuitBreakerState.CLOSED;
}
/**
* 장애 유형별 실패 횟수를 집계하고 조건에 맞으면 OPEN 상태로 전환합니다.
*/
public synchronized void recordFailure(FailureType failureType) {
if (failureType == FailureType.TIMEOUT) {
timeoutFailures++;
lastFailureType = failureType;
} else if (failureType == FailureType.SERVER_ERROR) {
serverErrorFailures++;
lastFailureType = failureType;
} else if (state == CircuitBreakerState.HALF_OPEN && isCircuitBreakerFailure(failureType)) {
lastFailureType = failureType;
open("HALF_OPEN_TEST_FAILED");
return;
} else {
return;
}
if (state == CircuitBreakerState.HALF_OPEN) {
open(failureType.name() + "_IN_HALF_OPEN");
return;
}
if (timeoutFailures >= TIMEOUT_OPEN_THRESHOLD) {
open("TIMEOUT_3_OR_MORE");
}
if (serverErrorFailures >= SERVER_ERROR_OPEN_THRESHOLD) {
open("SERVER_ERROR_5XX_3_OR_MORE");
}
}
public synchronized CircuitBreakerState state() {
return state;
}
/**
* MCP Monitor 화면에 표시할 현재 상태 스냅샷입니다.
*/
public synchronized CircuitBreakerSnapshot snapshot() {
long remainingOpenMillis = 0;
if (state == CircuitBreakerState.OPEN && openedAt != null) {
long elapsed = Math.max(0, clock.millis() - openedAt.toEpochMilli());
remainingOpenMillis = Math.max(0, openDurationMillis - elapsed);
}
return new CircuitBreakerSnapshot(
name,
state.name(),
timeoutFailures,
serverErrorFailures,
TIMEOUT_OPEN_THRESHOLD,
SERVER_ERROR_OPEN_THRESHOLD,
lastFailureType == null ? "" : lastFailureType.name(),
openedReason,
openedAt == null ? "" : openedAt.toString(),
remainingOpenMillis);
}
/**
* 테스트 후 화면에서 회로를 수동 초기화할 때 사용합니다.
*/
public synchronized void reset() {
recordSuccess();
}
private void open(String reason) {
state = CircuitBreakerState.OPEN;
openedAt = Instant.now(clock);
openedReason = reason;
}
private boolean isCircuitBreakerFailure(FailureType failureType) {
return failureType == FailureType.TIMEOUT
|| failureType == FailureType.NETWORK_ERROR
|| failureType == FailureType.SERVER_ERROR
|| failureType == FailureType.INTERNAL_ERROR;
}
}

View File

@@ -0,0 +1,39 @@
package io.shinhanlife.dap.biz.mcp.gateway.resilience;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import java.util.Map;
/**
* MCP Monitor 화면에서 Circuit Breaker 상태를 확인하고 초기화하기 위한 API입니다.
*/
@RestController
@RequestMapping("/internal/circuit-breakers")
public class CircuitBreakerMonitorController {
private final CircuitBreakerService circuitBreakers;
public CircuitBreakerMonitorController(CircuitBreakerService circuitBreakers) {
this.circuitBreakers = circuitBreakers;
}
/**
* Tool별 Circuit Breaker 상태 목록을 반환합니다.
*/
@GetMapping
public List<CircuitBreakerSnapshot> snapshots() {
return circuitBreakers.snapshots();
}
/**
* 모든 Circuit Breaker를 CLOSED 상태로 초기화합니다.
*/
@PostMapping("/reset")
public Map<String, Object> reset() {
circuitBreakers.resetAll();
return Map.of("reset", true, "items", circuitBreakers.snapshots());
}
}

View File

@@ -0,0 +1,71 @@
package io.shinhanlife.dap.biz.mcp.gateway.resilience;
import io.shinhanlife.dap.biz.mcp.gateway.config.McpGatewayProperties;
import org.springframework.stereotype.Service;
import java.time.Clock;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Service
/**
* Tool 이름별 Circuit Breaker 인스턴스를 관리하는 서비스입니다.
*
* registry에 등록된 Tool별로 장애 상태를 독립적으로 관리합니다.
*/
public class CircuitBreakerService {
private final McpGatewayProperties properties;
private final Map<String, CircuitBreaker> breakers = new ConcurrentHashMap<>();
public CircuitBreakerService(McpGatewayProperties properties) {
this.properties = properties;
}
/**
* 이름에 해당하는 Circuit Breaker를 반환하고, 없으면 새로 생성합니다.
*/
public CircuitBreaker breaker(String name) {
return breaker(name, 0, 0);
}
/**
* Tool별 정책이 있으면 해당 threshold/open 시간을 우선 사용해 Circuit Breaker를 생성합니다.
*/
public CircuitBreaker breaker(String name, int failureThreshold, long openMillis) {
int effectiveThreshold = failureThreshold > 0 ? failureThreshold : properties.circuitBreakerFailureThreshold();
long effectiveOpenMillis = openMillis > 0 ? openMillis : properties.circuitBreakerOpenMillis();
return breakers.computeIfAbsent(name, key -> new CircuitBreaker(
key,
effectiveThreshold,
effectiveOpenMillis,
Clock.systemUTC()));
}
/**
* Tool registry가 갱신될 때 Circuit Breaker 정책도 새 값으로 갱신합니다.
*/
public void refresh(String name, int failureThreshold, long openMillis) {
int effectiveThreshold = failureThreshold > 0 ? failureThreshold : properties.circuitBreakerFailureThreshold();
long effectiveOpenMillis = openMillis > 0 ? openMillis : properties.circuitBreakerOpenMillis();
breakers.put(name, new CircuitBreaker(name, effectiveThreshold, effectiveOpenMillis, Clock.systemUTC()));
}
/**
* MCP Monitor 화면에 표시할 전체 Circuit Breaker 상태를 반환합니다.
*/
public List<CircuitBreakerSnapshot> snapshots() {
return breakers.values().stream()
.map(CircuitBreaker::snapshot)
.sorted(Comparator.comparing(CircuitBreakerSnapshot::name))
.toList();
}
/**
* 테스트 후 모든 Circuit Breaker 상태를 CLOSED로 초기화합니다.
*/
public void resetAll() {
breakers.values().forEach(CircuitBreaker::reset);
}
}

View File

@@ -0,0 +1,18 @@
package io.shinhanlife.dap.biz.mcp.gateway.resilience;
/**
* MCP Monitor 화면에 내려주는 Circuit Breaker 상태 정보입니다.
*/
public record CircuitBreakerSnapshot(
String name,
String state,
int timeoutFailures,
int serverErrorFailures,
int timeoutOpenThreshold,
int serverErrorOpenThreshold,
String lastFailureType,
String openedReason,
String openedAt,
long remainingOpenMillis
) {
}

View File

@@ -0,0 +1,12 @@
package io.shinhanlife.dap.biz.mcp.gateway.resilience;
/**
* Circuit Breaker의 현재 상태입니다.
*
* CLOSED는 정상 호출 가능, OPEN은 호출 차단, HALF_OPEN은 복구 확인 상태를 의미합니다.
*/
public enum CircuitBreakerState {
CLOSED,
OPEN,
HALF_OPEN
}

View File

@@ -0,0 +1,20 @@
package io.shinhanlife.dap.biz.mcp.gateway.resilience;
/**
* Tool/EIMS/Tool 서버 호출 실패를 업무적으로 구분하기 위한 장애 유형입니다.
*
* Retry, Circuit Breaker, 감사 로그에서 같은 기준으로 오류를 판단합니다.
*/
public enum FailureType {
TIMEOUT,
CLIENT_ERROR,
SERVER_ERROR,
NETWORK_ERROR,
AUTHORIZATION_ERROR,
BUSINESS_ERROR,
HALLUCINATION_GUARDRAIL,
TOOL_DELETED,
CIRCUIT_OPEN,
TOO_LARGE_RESULT,
INTERNAL_ERROR
}

View File

@@ -0,0 +1,29 @@
package io.shinhanlife.dap.biz.mcp.gateway.resilience;
/**
* Tool 호출 재시도 정책입니다.
*
* 최대 재시도 횟수, 최초 대기 시간, Backoff 배율, 최대 대기 시간을 담습니다.
*/
public record RetryPolicy(int maxAttempts, long initialBackoffMillis, double backoffMultiplier, long maxBackoffMillis) {
/**
* WRITE 요청처럼 재시도하면 위험한 경우 사용하는 비활성 정책입니다.
*/
public static RetryPolicy disabled() {
return new RetryPolicy(1, 0, 1.0, 0);
}
/**
* 실패한 시도 횟수에 따라 다음 재시도 전 대기 시간을 계산합니다.
*/
public long backoffMillis(int failedAttempt) {
if (failedAttempt < 1 || initialBackoffMillis < 1) {
return 0;
}
double backoff = initialBackoffMillis * Math.pow(backoffMultiplier, failedAttempt - 1);
if (maxBackoffMillis > 0) {
backoff = Math.min(backoff, maxBackoffMillis);
}
return Math.max(0, Math.round(backoff));
}
}

View File

@@ -0,0 +1,28 @@
package io.shinhanlife.dap.biz.mcp.gateway.resilience;
/**
* Tool 실행 중 발생한 오류를 FailureType과 함께 전달하는 공통 예외입니다.
*
* 상위 GatewayToolExecutor가 이 예외를 기준으로 감사 로그, Redis Trace, 재시도 여부를 판단합니다.
*/
public class ToolExecutionException extends RuntimeException {
private final FailureType failureType;
public ToolExecutionException(String message) {
this(FailureType.BUSINESS_ERROR, message);
}
public ToolExecutionException(FailureType failureType, String message) {
super(message);
this.failureType = failureType;
}
public ToolExecutionException(FailureType failureType, String message, Throwable cause) {
super(message, cause);
this.failureType = failureType;
}
public FailureType failureType() {
return failureType;
}
}

View File

@@ -0,0 +1,21 @@
package io.shinhanlife.dap.biz.mcp.gateway.security;
import java.util.Set;
/**
* Agent가 MCP 서버로 보낸 헤더 정보를 표준화한 요청 컨텍스트입니다.
*
* 이후 권한 검증, 감사 로그, Redis Trace, Tool 서버 호출 시 공통으로 사용됩니다.
*/
public record McpRequestContext(
String requestId,
String traceGroupId,
String clientAddress,
String agentId,
String userId,
Set<String> roles,
Set<String> allowedTools,
boolean claimsTrusted,
boolean writeApproved
) {
}

View File

@@ -0,0 +1,56 @@
package io.shinhanlife.dap.biz.mcp.gateway.security;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import java.util.Arrays;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
@Component
/**
* 현재 HTTP 요청에서 Agent/User/권한 관련 헤더를 읽어 McpRequestContext로 변환합니다.
*
* MCP Tool 메소드 자체는 HTTP 객체를 직접 받지 않으므로, 이 클래스가 요청 정보를 꺼내주는 역할을 합니다.
*/
public class McpRequestContextResolver {
/**
* 현재 요청의 헤더를 읽어 권한/추적용 컨텍스트를 생성합니다.
*/
public McpRequestContext current() {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes == null) {
String requestId = UUID.randomUUID().toString();
return new McpRequestContext(requestId, requestId, "internal", "internal", "internal", Set.of(), Set.of(), true, true);
}
HttpServletRequest request = attributes.getRequest();
String requestId = firstNonBlank(request.getHeader("X-Request-Id"), UUID.randomUUID().toString());
return new McpRequestContext(
requestId,
firstNonBlank(request.getHeader("X-MCP-Trace-Group-Id"), requestId),
request.getRemoteAddr(),
firstNonBlank(request.getHeader("X-MCP-Agent-Id"), "anonymous"),
firstNonBlank(request.getHeader("X-MCP-User-Id"), "anonymous"),
csvHeader(request.getHeader("X-MCP-Roles")),
csvHeader(request.getHeader("X-MCP-Allowed-Tools")),
Boolean.parseBoolean(firstNonBlank(request.getHeader("X-MCP-Claims-Trusted"), "false")),
Boolean.parseBoolean(firstNonBlank(request.getHeader("X-MCP-Write-Approved"), "false")));
}
private String firstNonBlank(String value, String fallback) {
return value == null || value.isBlank() ? fallback : value.trim();
}
private Set<String> csvHeader(String value) {
if (value == null || value.isBlank()) {
return Set.of();
}
return Arrays.stream(value.split(","))
.map(String::trim)
.filter(item -> !item.isBlank())
.collect(Collectors.toUnmodifiableSet());
}
}

View File

@@ -0,0 +1,55 @@
package io.shinhanlife.dap.biz.mcp.gateway.security;
import io.shinhanlife.dap.biz.mcp.gateway.config.McpGatewayProperties;
import io.shinhanlife.dap.biz.mcp.gateway.resilience.FailureType;
import io.shinhanlife.dap.biz.mcp.gateway.resilience.ToolExecutionException;
import io.shinhanlife.dap.biz.mcp.gateway.dto.OperationType;
import io.shinhanlife.dap.biz.mcp.gateway.dto.ToolMetadata;
import org.springframework.stereotype.Service;
import java.util.Set;
@Service
/**
* Agent가 특정 Tool을 호출할 수 있는지 판단하는 권한 검증 서비스입니다.
*
* 서버 전체 허용 Tool, Agent가 보낸 allowed-tools 헤더, WRITE 승인 여부를 함께 확인합니다.
*/
public class ToolAuthorizationService {
private final McpGatewayProperties properties;
public ToolAuthorizationService(McpGatewayProperties properties) {
this.properties = properties;
}
/**
* Tool 실행 직전에 호출 가능 여부를 검사하고, 거부 시 ToolExecutionException을 발생시킵니다.
*/
public void authorize(McpRequestContext context, ToolMetadata metadata) {
if (properties.agentClaimsRequired() && context.allowedTools().isEmpty()) {
throw new ToolExecutionException(FailureType.AUTHORIZATION_ERROR, "Missing agent authorization claims");
}
if (properties.trustedClaimsRequired() && !context.claimsTrusted()) {
throw new ToolExecutionException(FailureType.AUTHORIZATION_ERROR, "Agent authorization claims are not trusted");
}
Set<String> serverAllowedTools = properties.allowedToolSet();
if (!isAllowed(serverAllowedTools, metadata.getName())) {
throw new ToolExecutionException(FailureType.AUTHORIZATION_ERROR, "Tool is not allowed by server policy: " + metadata.getName());
}
if (!isAllowed(context.allowedTools(), metadata.getName())) {
throw new ToolExecutionException(FailureType.AUTHORIZATION_ERROR, "Tool is not allowed by agent claims: " + metadata.getName());
}
if (properties.writeApprovalRequired()
&& (metadata.getOperationType() == OperationType.WRITE || (metadata.getRequiresApproval() != null && metadata.getRequiresApproval()))
&& !context.writeApproved()) {
throw new ToolExecutionException(FailureType.AUTHORIZATION_ERROR, "Write tool requires approval: " + metadata.getName());
}
}
/**
* 빈 목록은 제한 없음, "*"는 모든 Tool 허용을 의미합니다.
*/
private boolean isAllowed(Set<String> allowedTools, String toolName) {
return allowedTools.isEmpty() || allowedTools.contains("*") || allowedTools.contains(toolName);
}
}

View File

@@ -0,0 +1,340 @@
package io.shinhanlife.dap.biz.mcp.gateway.service;
import io.shinhanlife.dap.biz.mcp.gateway.dto.ToolMetadata;
import io.shinhanlife.dap.biz.mcp.gateway.resilience.FailureType;
import io.shinhanlife.dap.biz.mcp.gateway.resilience.RetryPolicy;
import io.shinhanlife.dap.biz.mcp.gateway.resilience.ToolExecutionException;
import io.shinhanlife.dap.biz.mcp.gateway.dto.OperationType;
import io.shinhanlife.dap.biz.mcp.gateway.config.McpGatewayProperties;
import io.shinhanlife.dap.biz.mcp.gateway.guardrail.SensitiveDataMasker;
import io.shinhanlife.dap.biz.mcp.gateway.guardrail.GuardrailService;
import io.shinhanlife.dap.biz.mcp.gateway.security.McpRequestContext;
import io.shinhanlife.dap.biz.mcp.gateway.security.McpRequestContextResolver;
import io.shinhanlife.dap.biz.mcp.gateway.audit.AuditLogService;
import io.shinhanlife.dap.biz.mcp.gateway.resilience.CircuitBreaker;
import io.shinhanlife.dap.biz.mcp.gateway.resilience.CircuitBreakerService;
import io.shinhanlife.dap.biz.mcp.gateway.security.ToolAuthorizationService;
import io.shinhanlife.dap.biz.mcp.gateway.redis.RedisToolTraceService;
import jakarta.annotation.PreDestroy;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestClient;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import io.shinhanlife.dap.biz.mcp.gateway.tool.large.LargeToolResponseService;
import io.shinhanlife.dap.biz.mcp.gateway.tool.large.PaginationRequestValidator;
import io.shinhanlife.dap.biz.mcp.gateway.tool.result.ToolExecutionResultFormatter;
import io.shinhanlife.dap.biz.mcp.gateway.tool.result.ToolExecutionResult;
import io.shinhanlife.dap.biz.mcp.gateway.guardrail.ToolResponseGuardrailService;
import io.shinhanlife.dap.biz.mcp.gateway.transport.ToolInvoker;
@Slf4j
@Service
public class ExecuteService {
private final ToolPlanner planner;
private final KillSwitchService killSwitchService;
private final ObjectMapper objectMapper;
private final SensitiveDataMasker dataMasker;
private final GuardrailService guardrailService;
private final McpRequestContextResolver contextResolver;
private final AuditLogService auditLogService;
private final CircuitBreakerService circuitBreakerService;
private final ToolAuthorizationService authorizationService;
private final RedisToolTraceService redisTrace;
private final McpGatewayProperties properties;
private final LargeToolResponseService largeResponses;
private final PaginationRequestValidator paginationValidator;
private final ToolExecutionResultFormatter resultFormatter;
private final ToolResponseGuardrailService responseGuardrail;
private final ToolInvoker toolInvoker;
private final ExecutorService executor;
public ExecuteService(ToolPlanner planner,
KillSwitchService killSwitchService,
ObjectMapper objectMapper,
SensitiveDataMasker dataMasker,
GuardrailService guardrailService,
McpRequestContextResolver contextResolver,
AuditLogService auditLogService,
CircuitBreakerService circuitBreakerService,
ToolAuthorizationService authorizationService,
RedisToolTraceService redisTrace,
McpGatewayProperties properties,
LargeToolResponseService largeResponses,
PaginationRequestValidator paginationValidator,
ToolExecutionResultFormatter resultFormatter,
ToolResponseGuardrailService responseGuardrail,
ToolInvoker toolInvoker) {
this.planner = planner;
this.killSwitchService = killSwitchService;
this.objectMapper = objectMapper;
this.dataMasker = dataMasker;
this.guardrailService = guardrailService;
this.contextResolver = contextResolver;
this.auditLogService = auditLogService;
this.circuitBreakerService = circuitBreakerService;
this.authorizationService = authorizationService;
this.redisTrace = redisTrace;
this.properties = properties;
this.largeResponses = largeResponses;
this.paginationValidator = paginationValidator;
this.resultFormatter = resultFormatter;
this.responseGuardrail = responseGuardrail;
this.toolInvoker = toolInvoker;
this.executor = new ThreadPoolExecutor(
properties.toolExecutorCorePoolSize(),
properties.toolExecutorMaxPoolSize(),
properties.toolExecutorKeepAliveSeconds(),
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(properties.toolExecutorQueueCapacity()),
new ThreadPoolExecutor.AbortPolicy()
);
}
public Object execute(Map<String, Object> payload, String tenantId) {
log.info(" [ExecuteService] 전체 실행 흐름 제어 시작");
killSwitchService.checkAgent(tenantId);
Map<String, Object> params = payload.containsKey("params") ? (Map<String, Object>) payload.get("params") : null;
String toolName = params != null ? (String) params.get("name") : (String) payload.get("toolName");
killSwitchService.checkTool(toolName);
var planObj = planner.createPlan(payload, tenantId);
ToolMetadata metadata = (ToolMetadata) planObj;
McpRequestContext context = contextResolver.current();
ObjectNode argumentsNode = objectMapper.createObjectNode();
if (params != null && params.containsKey("arguments")) {
argumentsNode = objectMapper.valueToTree(params.get("arguments"));
}
long startedAt = System.nanoTime();
auditLogService.toolStarted(context, toolName, argumentsNode);
redisTrace.started(context, metadata, argumentsNode);
try {
authorizationService.authorize(context, metadata);
if (params != null && params.containsKey("arguments")) {
guardrailService.validate(metadata, argumentsNode);
}
if (metadata.getIntegrationType() != null) {
killSwitchService.checkRoute(metadata.getIntegrationType());
}
Object result = executeWithResilience(context, metadata, argumentsNode, payload);
if (result instanceof com.fasterxml.jackson.databind.JsonNode) {
// Apply Output Guardrail
String wrappedResponse = responseGuardrail.validateAndWrap(metadata, context.requestId(), (com.fasterxml.jackson.databind.JsonNode) result);
// Format the result (Restore legacy raw format as Map/List for backward compatibility)
result = objectMapper.readValue(wrappedResponse, Object.class);
}
long elapsedMillis = elapsedMillis(startedAt);
String responseText = "";
try { responseText = objectMapper.writeValueAsString(result); } catch (Exception ignore) {}
auditLogService.toolFinished(context, metadata.getName(), elapsedMillis, true, "");
redisTrace.finished(context, metadata, argumentsNode, elapsedMillis, true, "", responseText);
return result;
} catch (ToolExecutionException error) {
long elapsedMillis = elapsedMillis(startedAt);
auditLogService.toolFinished(context, toolName, elapsedMillis, false, error.failureType().name());
redisTrace.finished(context, metadata, argumentsNode, elapsedMillis, false, error.failureType().name(), "");
throw error;
} catch (Exception error) {
long elapsedMillis = elapsedMillis(startedAt);
auditLogService.toolFinished(context, toolName, elapsedMillis, false, FailureType.INTERNAL_ERROR.name());
redisTrace.finished(context, metadata, argumentsNode, elapsedMillis, false, FailureType.INTERNAL_ERROR.name(), "");
throw new ToolExecutionException(FailureType.INTERNAL_ERROR, "Tool execution failed: " + toolName, error);
}
}
private Object executeWithResilience(McpRequestContext context, ToolMetadata metadata, ObjectNode arguments, Map<String, Object> payload) {
RetryPolicy retryPolicy = retryPolicy(metadata, arguments);
int failureThreshold = (metadata.getCircuitBreakerFailureThreshold() != null && metadata.getCircuitBreakerFailureThreshold() > 0)
? metadata.getCircuitBreakerFailureThreshold()
: properties.circuitBreakerFailureThreshold();
long openMillis = (metadata.getCircuitBreakerOpenMillis() != null && metadata.getCircuitBreakerOpenMillis() > 0)
? metadata.getCircuitBreakerOpenMillis()
: properties.circuitBreakerOpenMillis();
CircuitBreaker circuitBreaker = circuitBreakerService.breaker("tool:" + metadata.getName(),
failureThreshold, openMillis);
ToolExecutionException lastError = null;
for (int attempt = 1; attempt <= retryPolicy.maxAttempts(); attempt++) {
try {
redisTrace.attemptStarted(context, metadata, arguments, attempt, retryPolicy.maxAttempts());
circuitBreaker.beforeCall();
Object result = executeOnce(context, metadata, arguments, payload);
circuitBreaker.recordSuccess();
return result;
} catch (ToolExecutionException error) {
lastError = error;
circuitBreaker.recordFailure(error.failureType());
if (!shouldRetry(metadata, arguments, error.failureType(), attempt, retryPolicy)) {
throw error;
}
long backoffMillis = retryPolicy.backoffMillis(attempt);
redisTrace.retryWaiting(context, metadata, arguments, attempt, retryPolicy.maxAttempts(), backoffMillis,
error.failureType().name());
sleepBeforeRetry(metadata.getName(), attempt, backoffMillis, error.failureType());
}
}
throw lastError == null
? new ToolExecutionException(FailureType.INTERNAL_ERROR, "Tool execution failed: " + metadata.getName())
: lastError;
}
private Object executeOnce(McpRequestContext context, ToolMetadata metadata, ObjectNode arguments, Map<String, Object> payload) {
CompletableFuture<Object> future;
try {
future = CompletableFuture.supplyAsync(() -> {
try {
String targetUrl = "http://localhost:8084"; // Fallback
if (metadata.getPodUrl() != null && !metadata.getPodUrl().isEmpty()) {
targetUrl = metadata.getPodUrl();
}
String executeApiUrl = targetUrl + "/mcp/api/v1/tools/call";
ObjectNode pageArguments = paginationValidator.normalize(arguments);
LargeToolResponseService.Collector collector = largeResponses.newCollector(metadata.getName(), context.requestId());
while (true) {
Map<String, Object> pagePayload = new java.util.HashMap<>(payload);
if (pagePayload.containsKey("params")) {
Map<String, Object> params = new java.util.HashMap<>((Map<String, Object>) pagePayload.get("params"));
params.put("arguments", objectMapper.convertValue(pageArguments, Map.class));
pagePayload.put("params", params);
} else {
pagePayload.put("arguments", objectMapper.convertValue(pageArguments, Map.class));
}
try {
log.info(" [ExecuteService] 요청 페이로드(마스킹 적용): {}", objectMapper.writeValueAsString(dataMasker.mask(objectMapper.valueToTree(pagePayload))));
} catch (Exception ignore) {}
JsonNode data = null;
try {
data = toolInvoker.invoke(pagePayload, executeApiUrl);
} catch (Exception e) {
if (executeApiUrl.contains("http://tool-")) {
String fallbackUrl = executeApiUrl.replaceAll("http://tool-[a-zA-Z0-9-]+", "http://localhost");
log.warn(" [ExecuteService] 호스트를 찾을 수 없어 localhost로 재시도합니다: {}", fallbackUrl);
try {
data = toolInvoker.invoke(pagePayload, fallbackUrl);
} catch (Exception ex) {
throw new ToolExecutionException(FailureType.SERVER_ERROR, "Tool Pod 호출 실패 (localhost 재시도 포함): " + ex.getMessage());
}
} else {
throw new ToolExecutionException(FailureType.SERVER_ERROR, "Tool Pod 호출 실패: " + e.getMessage());
}
}
collector.accept(data);
if (!collector.shouldFetchNextPage()) {
return collector.finish();
}
pageArguments.put("cursor", collector.nextCursor());
pageArguments.put("pageSize", collector.pageSize());
}
} catch (ToolExecutionException error) {
throw error;
} catch (Exception error) {
throw new ToolExecutionException(
FailureType.INTERNAL_ERROR,
"Tool execution failed: " + metadata.getName(),
error
);
}
}, executor);
} catch (RuntimeException error) {
throw new ToolExecutionException(
FailureType.SERVER_ERROR,
"MCP Tool 실행 큐가 가득 찼습니다: " + metadata.getName(),
error
);
}
try {
return future.get(timeoutMillis(metadata), TimeUnit.MILLISECONDS);
} catch (TimeoutException error) {
future.cancel(true);
throw new ToolExecutionException(FailureType.TIMEOUT, "Tool execution timed out: " + metadata.getName(), error);
} catch (ExecutionException error) {
if (error.getCause() instanceof ToolExecutionException toolError) {
throw toolError;
}
throw new ToolExecutionException(FailureType.INTERNAL_ERROR, "Tool execution failed: " + metadata.getName(), error);
} catch (InterruptedException error) {
Thread.currentThread().interrupt();
throw new ToolExecutionException(FailureType.INTERNAL_ERROR, "Tool 실행이 중단되었습니다: " + metadata.getName(), error);
}
}
private RetryPolicy retryPolicy(ToolMetadata metadata, ObjectNode arguments) {
if (!retryAllowedByOperation(metadata, arguments)) {
return RetryPolicy.disabled();
}
return new RetryPolicy(properties.retryMaxAttempts(), properties.retryInitialBackoffMillis(),
properties.retryBackoffMultiplier(), properties.retryMaxBackoffMillis());
}
private boolean retryAllowedByOperation(ToolMetadata metadata, ObjectNode arguments) {
if (metadata.getOperationType() == OperationType.READ) {
return metadata.getRetryEnabled() != null ? metadata.getRetryEnabled() : true;
}
return arguments.hasNonNull("idempotencyKey") && !arguments.path("idempotencyKey").asText("").isBlank();
}
private boolean shouldRetry(ToolMetadata metadata, ObjectNode arguments, FailureType failureType, int attempt, RetryPolicy retryPolicy) {
if (attempt >= retryPolicy.maxAttempts() || !retryAllowedByOperation(metadata, arguments)) {
return false;
}
return failureType == FailureType.TIMEOUT || failureType == FailureType.NETWORK_ERROR || failureType == FailureType.SERVER_ERROR;
}
private void sleepBeforeRetry(String toolName, int attempt, long backoffMillis, FailureType failureType) {
if (backoffMillis <= 0) {
return;
}
log.warn("Retrying tool call. tool={} failedAttempt={} failureType={} backoffMillis={}", toolName, attempt, failureType, backoffMillis);
try {
Thread.sleep(backoffMillis);
} catch (InterruptedException error) {
Thread.currentThread().interrupt();
throw new ToolExecutionException(FailureType.INTERNAL_ERROR, "Tool 재시도가 중단되었습니다: " + toolName, error);
}
}
private long timeoutMillis(ToolMetadata metadata) {
return (metadata.getTimeoutMillis() != null && metadata.getTimeoutMillis() > 0) ? metadata.getTimeoutMillis() : properties.toolTimeoutMillis();
}
private long elapsedMillis(long startedAt) {
return (System.nanoTime() - startedAt) / 1_000_000;
}
@PreDestroy
public void shutdown() {
executor.shutdownNow();
}
}

View File

@@ -0,0 +1,71 @@
package io.shinhanlife.dap.biz.mcp.gateway.service;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
/**
* @package io.shinhanlife.dap.biz.mcp.gateway.service
* @className KillSwitchService
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class KillSwitchService {
// 기본 제공되는 StringRedisTemplate 사용
private final StringRedisTemplate stringRedisTemplate;
public void checkAgent(String tenantId) {
String key = "mcp:kill:agent:" + tenantId;
if ("true".equalsIgnoreCase(stringRedisTemplate.opsForValue().get(key))) {
log.warn(" [KillSwitch] 차단된 에이전트 접근 시도: {}", tenantId);
throw new SecurityException(" 비상 차단: 해당 테넌트(" + tenantId + ")의 접근이 관리자에 의해 차단되었습니다.");
}
}
public void checkTool(String toolName) {
if (toolName == null || toolName.isBlank()) return;
String key = "mcp:kill:tool:" + toolName;
if ("true".equalsIgnoreCase(stringRedisTemplate.opsForValue().get(key))) {
log.warn(" [KillSwitch] 차단된 툴 실행 시도: {}", toolName);
throw new SecurityException(" 비상 차단: 해당 툴(" + toolName + ")의 실행이 관리자에 의해 차단되었습니다.");
}
}
public void checkRoute(String integrationType) {
if (integrationType == null || integrationType.isBlank()) return;
String key = "mcp:kill:route:" + integrationType;
if ("true".equalsIgnoreCase(stringRedisTemplate.opsForValue().get(key))) {
log.warn(" [KillSwitch] 차단된 라우트 통신 시도: {}", integrationType);
throw new SecurityException(" 비상 차단: 해당 레거시 라우트(" + integrationType + ")로의 통신이 관리자에 의해 차단되었습니다.");
}
}
// 관리자 API용 토글 메서드
public void toggleKillSwitch(String type, String target, boolean state) {
String key = "mcp:kill:" + type + ":" + target;
stringRedisTemplate.opsForValue().set(key, String.valueOf(state));
log.info(" [KillSwitch] 상태 변경: {} -> {} (차단 상태: {})", type, target, state);
}
// 관리자 API용 삭제 메서드 (Redis 키 자체를 완전 삭제)
public void removeKillSwitch(String type, String target) {
String key = "mcp:kill:" + type + ":" + target;
stringRedisTemplate.delete(key);
log.info(" [KillSwitch] 차단 키 완전 삭제: {} -> {}", type, target);
}
}

View File

@@ -0,0 +1,105 @@
package io.shinhanlife.dap.biz.mcp.gateway.service;
import io.shinhanlife.dap.common.mcp.security.SecurityProperties;
import io.shinhanlife.dap.biz.mcp.gateway.registry.RedisRegistryService;
import io.shinhanlife.dap.biz.mcp.gateway.dto.ToolMetadata;
import io.shinhanlife.dap.biz.mcp.gateway.config.GatewayFallbackProperties;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Map;
/**
* @package io.shinhanlife.dap.biz.mcp.gateway.service
* @className ToolPlanner
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class ToolPlanner {
private final RedisRegistryService redisRegistryService;
private final SecurityProperties securityProperties;
private final GatewayFallbackProperties fallbackProperties;
/**
* 요청(payload)을 분석하여 실행해야 할 Tool의 계획을 생성합니다.
*/
public Object createPlan(Map<String, Object> payload, String tenantId) {
log.info(" [Planner] 요청 분석 및 실행 계획 수립 시작");
// 1. 요청에서 호출하려는 툴 이름 추출 (JSON-RPC params.name)
Map<String, Object> params = (Map<String, Object>) payload.get("params");
String toolName = params != null ? (String) params.get("name") : null;
if (toolName == null || toolName.isEmpty()) {
throw new IllegalArgumentException("요청에 toolName이 포함되어 있지 않습니다.");
}
// 2. RedisRegistry에서 해당 툴의 메타데이터 조회
// (실제로는 이 메타데이터가 실행 계획의 핵심이 됩니다)
var toolMetadata = redisRegistryService.getToolByName(toolName);
if (toolMetadata == null) {
log.warn(" [Planner] 등록되지 않은 툴 요청: {}. Fallback 라우팅 규칙을 확인합니다.", toolName);
String fallbackPodUrl = fallbackProperties.getDefaultUrl();
if (fallbackProperties.getRoutes() != null) {
for (Map.Entry<String, String> entry : fallbackProperties.getRoutes().entrySet()) {
if (toolName.contains(entry.getKey())) {
fallbackPodUrl = entry.getValue();
break;
}
}
}
if (fallbackPodUrl == null || fallbackPodUrl.isEmpty()) {
log.error(" [Planner] Fallback 라우팅 대상이 아닙니다. 툴: {}", toolName);
throw new RuntimeException("해당 툴(" + toolName + ")이 레지스트리에 존재하지 않습니다.");
}
log.info(" [Planner] Fallback 라우팅 매칭됨: {} -> {}", toolName, fallbackPodUrl);
toolMetadata = ToolMetadata.builder()
.uid(toolName)
.name(toolName)
.integrationType("DIRECT")
.podUrl(fallbackPodUrl)
.build();
}
// 2-1. [신규] 도메인 그룹핑 기반 권한 검증
if (tenantId != null && toolMetadata.getCategoryKey() != null) {
String normalizedTenantId = tenantId.toLowerCase();
List<String> allowedDomains = securityProperties.getTenantDomains().get(normalizedTenantId);
// 만약 대소문자 변환 후에도 없으면 원래 값으로 한 번 더 시도 (하위 호환성)
if (allowedDomains == null) {
allowedDomains = securityProperties.getTenantDomains().get(tenantId);
}
if (allowedDomains == null ||
(!allowedDomains.contains("ALL") && !allowedDomains.contains(toolMetadata.getCategoryKey()))) {
log.warn(" [Planner] 권한 거부 - Tenant: {}, Request Domain: {}", tenantId, toolMetadata.getCategoryKey());
throw new SecurityException("해당 도메인(" + toolMetadata.getCategoryKey() + ")의 툴을 실행할 권한이 없습니다.");
}
}
log.info(" [Planner] 툴 '{}'에 대한 실행 계획 수립 완료", toolName);
// 3. 수립된 계획(툴 메타데이터) 반환
return toolMetadata;
}
}

View File

@@ -0,0 +1,156 @@
package io.shinhanlife.dap.biz.mcp.gateway.sync;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.modelcontextprotocol.json.TypeRef;
import io.modelcontextprotocol.spec.McpSchema;
import io.modelcontextprotocol.spec.McpServerSession;
import io.modelcontextprotocol.spec.McpServerTransport;
import io.modelcontextprotocol.spec.McpServerTransportProvider;
import org.springframework.http.MediaType;
import org.springframework.web.servlet.function.RouterFunction;
import org.springframework.web.servlet.function.RouterFunctions;
import org.springframework.web.servlet.function.ServerResponse;
import reactor.core.publisher.Mono;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import lombok.extern.slf4j.Slf4j;
import static org.springframework.web.servlet.function.RequestPredicates.GET;
import static org.springframework.web.servlet.function.RequestPredicates.POST;
import static org.springframework.web.servlet.function.RequestPredicates.accept;
@Slf4j
public class CustomWebMvcSseServerTransportProvider implements McpServerTransportProvider {
private McpServerSession.Factory sessionFactory;
private final String sseEndpoint;
private final String messageEndpoint;
private final Map<String, McpServerSession> sessions = new ConcurrentHashMap<>();
private final ObjectMapper objectMapper;
public CustomWebMvcSseServerTransportProvider(String sseEndpoint, String messageEndpoint, ObjectMapper objectMapper) {
this.sseEndpoint = sseEndpoint;
this.messageEndpoint = messageEndpoint;
this.objectMapper = objectMapper != null ? objectMapper : new ObjectMapper();
}
@Override
public void setSessionFactory(McpServerSession.Factory sessionFactory) {
this.sessionFactory = sessionFactory;
}
@Override
public Mono<Void> closeGracefully() {
return Mono.fromRunnable(() -> {
sessions.values().forEach(session -> {
try {
session.closeGracefully().subscribe();
} catch (Exception ignored) {}
});
sessions.clear();
});
}
@Override
public Mono<Void> notifyClients(String method, Object params) {
return Mono.when(sessions.values().stream()
.map(session -> session.sendNotification(method, params))
.toList());
}
public org.springframework.web.servlet.mvc.method.annotation.SseEmitter handleSse() {
if (sessionFactory == null) {
throw new IllegalStateException("SessionFactory not configured");
}
org.springframework.web.servlet.mvc.method.annotation.SseEmitter emitter = new org.springframework.web.servlet.mvc.method.annotation.SseEmitter(-1L);
String sessionId = UUID.randomUUID().toString();
CustomMcpSessionTransport sessionTransport = new CustomMcpSessionTransport(emitter, sessionId);
McpServerSession session = sessionFactory.create(sessionTransport);
sessions.put(sessionId, session);
emitter.onCompletion(() -> sessions.remove(sessionId));
emitter.onTimeout(() -> sessions.remove(sessionId));
new Thread(() -> {
try {
Thread.sleep(100);
emitter.send(org.springframework.web.servlet.mvc.method.annotation.SseEmitter.event().name("endpoint").data(messageEndpoint + "?sessionId=" + sessionId));
} catch (Exception e) {
emitter.completeWithError(e);
}
}).start();
return emitter;
}
public org.springframework.http.ResponseEntity<String> handleMessage(String sessionId, String body) {
log.info("Received POST message for sessionId: " + sessionId + ", body: " + body);
if (sessionId == null || !sessions.containsKey(sessionId)) {
return org.springframework.http.ResponseEntity.badRequest().body("Missing or invalid sessionId");
}
McpServerSession session = sessions.get(sessionId);
try {
java.util.Map<String, Object> map = objectMapper.readValue(body, new com.fasterxml.jackson.core.type.TypeReference<java.util.Map<String, Object>>() {});
io.modelcontextprotocol.spec.McpSchema.JSONRPCMessage message;
if (map.containsKey("id")) {
if (map.containsKey("method")) {
message = objectMapper.convertValue(map, io.modelcontextprotocol.spec.McpSchema.JSONRPCRequest.class);
} else {
message = objectMapper.convertValue(map, io.modelcontextprotocol.spec.McpSchema.JSONRPCResponse.class);
}
} else {
message = objectMapper.convertValue(map, io.modelcontextprotocol.spec.McpSchema.JSONRPCNotification.class);
}
log.info("Converted message type: " + message.getClass().getName());
session.handle(message).subscribe();
log.info("Message sent to session handler");
return org.springframework.http.ResponseEntity.ok().build();
} catch (Exception e) {
log.error("Failed to handle message", e);
return org.springframework.http.ResponseEntity.status(500).body(e.getMessage());
}
}
private class CustomMcpSessionTransport implements McpServerTransport {
private final org.springframework.web.servlet.mvc.method.annotation.SseEmitter emitter;
private final String sessionId;
public CustomMcpSessionTransport(org.springframework.web.servlet.mvc.method.annotation.SseEmitter emitter, String sessionId) {
this.emitter = emitter;
this.sessionId = sessionId;
}
@Override
public Mono<Void> sendMessage(McpSchema.JSONRPCMessage message) {
return Mono.fromRunnable(() -> {
log.info("Sending message to SSE stream: " + message.getClass().getName());
try {
String json = objectMapper.writeValueAsString(message);
log.info("Serialized message: " + json);
emitter.send(org.springframework.web.servlet.mvc.method.annotation.SseEmitter.event().name("message").data(json));
log.info("Message successfully sent to SSE emitter");
} catch (Exception e) {
log.error("Error sending message to SSE emitter", e);
emitter.completeWithError(e);
}
});
}
@Override
public Mono<Void> closeGracefully() {
return Mono.fromRunnable(emitter::complete);
}
@Override
public <T> T unmarshalFrom(Object object, TypeRef<T> typeRef) {
return objectMapper.convertValue(object, objectMapper.constructType(typeRef.getType()));
}
}
}

View File

@@ -0,0 +1,42 @@
package io.shinhanlife.dap.biz.mcp.gateway.sync;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
@RestController
public class DynamicMcpController {
private final DynamicMcpServerManager manager;
public DynamicMcpController(DynamicMcpServerManager manager) {
this.manager = manager;
}
@GetMapping("/mcp/sse/{category}")
public SseEmitter handleSse(@PathVariable("category") String category) {
CustomWebMvcSseServerTransportProvider transport = manager.getTransport(category);
if (transport == null) {
throw new IllegalArgumentException("Unknown category: " + category);
}
return transport.handleSse();
}
@PostMapping("/mcp/message/{category}")
public ResponseEntity<String> handleMessage(
@PathVariable("category") String category,
@RequestParam("sessionId") String sessionId,
@RequestBody String body) {
CustomWebMvcSseServerTransportProvider transport = manager.getTransport(category);
if (transport == null) {
return ResponseEntity.badRequest().body("Unknown category: " + category);
}
return transport.handleMessage(sessionId, body);
}
}

View File

@@ -0,0 +1,102 @@
package io.shinhanlife.dap.biz.mcp.gateway.sync;
import io.modelcontextprotocol.server.McpServer;
import io.modelcontextprotocol.server.McpSyncServer;
import io.shinhanlife.dap.biz.mcp.gateway.dto.ToolMetadata;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
@Component
public class DynamicMcpServerManager {
private static final Logger log = LoggerFactory.getLogger(DynamicMcpServerManager.class);
private final Map<String, McpSyncServer> categoryServers = new ConcurrentHashMap<>();
private final Map<String, CustomWebMvcSseServerTransportProvider> categoryTransports = new ConcurrentHashMap<>();
private final Map<String, Set<String>> managedToolNamesPerCategory = new ConcurrentHashMap<>();
private final RegistryMcpToolSpecificationFactory specificationFactory;
private final ObjectMapper objectMapper;
public DynamicMcpServerManager(RegistryMcpToolSpecificationFactory specificationFactory, ObjectMapper objectMapper) {
this.specificationFactory = specificationFactory;
this.objectMapper = objectMapper;
// Pre-initialize basic categories so their endpoints are always open
getOrCreateServer("common");
getOrCreateServer("hr");
getOrCreateServer("payment");
getOrCreateServer("sms");
getOrCreateServer("email");
getOrCreateServer("other");
}
private McpSyncServer getOrCreateServer(String categoryKey) {
String safeCategory = (categoryKey == null || categoryKey.trim().isEmpty()) ? "common" : categoryKey.toLowerCase();
return categoryServers.computeIfAbsent(safeCategory, key -> {
log.info("Creating dynamic MCP Server for category: {}", key);
String ssePath = "/mcp/sse/" + key;
String msgPath = "/mcp/message/" + key;
CustomWebMvcSseServerTransportProvider transport = new CustomWebMvcSseServerTransportProvider(ssePath, msgPath, objectMapper);
McpSyncServer newServer = McpServer.sync(transport)
.serverInfo("AXHUB-Gateway-" + key, "1.0.0")
.capabilities(io.modelcontextprotocol.spec.McpSchema.ServerCapabilities.builder().tools(true).build())
.build();
categoryTransports.put(key, transport);
managedToolNamesPerCategory.put(key, ConcurrentHashMap.newKeySet());
return newServer;
});
}
public void synchronizeCategory(String categoryKey, List<ToolMetadata> tools) {
String safeCategory = (categoryKey == null || categoryKey.trim().isEmpty()) ? "common" : categoryKey.toLowerCase();
McpSyncServer server = getOrCreateServer(safeCategory);
Set<String> managedToolNames = managedToolNamesPerCategory.get(safeCategory);
Set<String> activeNames = tools.stream()
.map(ToolMetadata::getName)
.collect(Collectors.toSet());
Set<String> removedNames = managedToolNames.stream()
.filter(name -> !activeNames.contains(name))
.collect(Collectors.toSet());
for (String removedName : removedNames) {
server.removeTool(removedName);
managedToolNames.remove(removedName);
log.info("[{}] Removed tool: {}", safeCategory, removedName);
}
for (ToolMetadata entry : tools) {
if (!managedToolNames.contains(entry.getName())) {
server.addTool(specificationFactory.create(entry));
managedToolNames.add(entry.getName());
log.info("[{}] Added tool: {}", safeCategory, entry.getName());
} else {
server.removeTool(entry.getName());
server.addTool(specificationFactory.create(entry));
}
}
}
public Set<String> getKnownCategories() {
return Collections.unmodifiableSet(categoryServers.keySet());
}
public CustomWebMvcSseServerTransportProvider getTransport(String categoryKey) {
String safeCategory = (categoryKey == null || categoryKey.trim().isEmpty()) ? "common" : categoryKey.toLowerCase();
return categoryTransports.get(safeCategory);
}
}

View File

@@ -0,0 +1,134 @@
package io.shinhanlife.dap.biz.mcp.gateway.sync;
import io.shinhanlife.dap.biz.mcp.gateway.dto.ToolMetadata;
import io.shinhanlife.dap.biz.mcp.gateway.service.ExecuteService;
import io.modelcontextprotocol.server.McpServerFeatures;
import io.modelcontextprotocol.spec.McpSchema;
import io.shinhanlife.dap.biz.mcp.gateway.tool.result.ToolExecutionResult;
import org.springframework.stereotype.Component;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
/**
* Registry에 등록된 실제 Tool 메타데이터를 MCP 표준 Tool specification으로 변환합니다.
*/
@Component
public class RegistryMcpToolSpecificationFactory {
private final ExecuteService executeService;
private final ObjectMapper objectMapper;
public RegistryMcpToolSpecificationFactory(ExecuteService executeService, ObjectMapper objectMapper) {
this.executeService = executeService;
this.objectMapper = objectMapper;
}
/**
* Registry Entry 하나를 MCP SDK의 stateless sync Tool specification으로 변환합니다.
*/
public McpServerFeatures.SyncToolSpecification create(ToolMetadata entry) {
McpSchema.Tool tool = McpSchema.Tool.builder()
.name(entry.getName())
.description(description(entry))
.inputSchema(inputSchema(entry))
.build();
return McpServerFeatures.SyncToolSpecification.builder()
.tool(tool)
.callHandler((context, request) -> execute(entry, request))
.build();
}
private McpSchema.CallToolResult execute(ToolMetadata entry, McpSchema.CallToolRequest request) {
try {
// Build legacy JSON-RPC payload format expected by ExecuteService
Map<String, Object> payload = new HashMap<>();
payload.put("jsonrpc", "2.0");
payload.put("method", "tools/call");
payload.put("id", UUID.randomUUID().toString());
Map<String, Object> params = new HashMap<>();
params.put("name", entry.getName());
params.put("arguments", request.arguments());
payload.put("params", params);
// Execute through ExecuteService
Object rawResult = executeService.execute(payload, "system");
// Convert JSON-RPC response back to McpSchema.CallToolResult
return toCallToolResult(rawResult);
} catch (Exception error) {
return McpSchema.CallToolResult.builder()
.addTextContent("Tool 실행 중 내부 오류가 발생했습니다: " + error.getMessage())
.isError(true)
.build();
}
}
private McpSchema.CallToolResult toCallToolResult(Object rawResult) {
if (rawResult instanceof ToolExecutionResult result) {
McpSchema.CallToolResult.Builder builder = McpSchema.CallToolResult.builder()
.isError(result.isError())
.meta(result.metadata());
if (result.content() == null || result.content().isEmpty()) {
builder.addTextContent("");
} else {
for (ToolExecutionResult.ContentItem item : result.content()) {
if ("text".equals(item.type())) {
builder.addTextContent(item.text());
}
}
}
if (result.structuredContent() != null) {
builder.structuredContent(result.structuredContent());
}
return builder.build();
}
// Fallback for old map format or unexpected types
try {
Map<String, Object> resultMap = objectMapper.convertValue(rawResult, new TypeReference<Map<String, Object>>() {});
if (resultMap.containsKey("resultType") || resultMap.containsKey("structuredContent")) {
ToolExecutionResult result = objectMapper.convertValue(rawResult, ToolExecutionResult.class);
return toCallToolResult(result);
}
Object innerResult = resultMap.containsKey("result") ? resultMap.get("result") : resultMap;
McpSchema.CallToolResult.Builder builder = McpSchema.CallToolResult.builder();
builder.isError(resultMap.containsKey("error"));
builder.addTextContent(objectMapper.writeValueAsString(innerResult));
return builder.build();
} catch (Exception e) {
return McpSchema.CallToolResult.builder()
.addTextContent(String.valueOf(rawResult))
.build();
}
}
private Map<String, Object> inputSchema(ToolMetadata entry) {
if (entry.getParametersSchema() != null) {
return entry.getParametersSchema();
}
Map<String, Object> schema = new LinkedHashMap<>();
schema.put("type", "object");
schema.put("properties", new HashMap<>());
schema.put("additionalProperties", false);
return schema;
}
private String description(ToolMetadata entry) {
return entry.getDescription() == null || entry.getDescription().isBlank()
? entry.getName() + " Tool"
: entry.getDescription();
}
}

View File

@@ -0,0 +1,114 @@
package io.shinhanlife.dap.biz.mcp.gateway.sync;
import io.shinhanlife.dap.biz.mcp.gateway.dto.ToolMetadata;
import io.shinhanlife.dap.biz.mcp.gateway.registry.RedisRegistryService;
import io.modelcontextprotocol.server.McpSyncServer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors;
/**
* @package io.shinhanlife.dap.biz.mcp.gateway.sync
* @className RegistryMcpToolSynchronizer
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
@Service
public class RegistryMcpToolSynchronizer {
private static final Logger log = LoggerFactory.getLogger(RegistryMcpToolSynchronizer.class);
private final ObjectProvider<McpSyncServer> mcpServerProvider;
private final DynamicMcpServerManager dynamicMcpServerManager;
private final RedisRegistryService redisRegistryService;
private final RegistryMcpToolSpecificationFactory specificationFactory;
private final Set<String> managedToolNames = new LinkedHashSet<>();
private final ReentrantLock lock = new ReentrantLock();
public RegistryMcpToolSynchronizer(ObjectProvider<McpSyncServer> mcpServerProvider,
DynamicMcpServerManager dynamicMcpServerManager,
RedisRegistryService redisRegistryService,
RegistryMcpToolSpecificationFactory specificationFactory) {
this.mcpServerProvider = mcpServerProvider;
this.dynamicMcpServerManager = dynamicMcpServerManager;
this.redisRegistryService = redisRegistryService;
this.specificationFactory = specificationFactory;
}
@EventListener(ApplicationReadyEvent.class)
public void synchronizeOnStartup() {
synchronize();
}
@Scheduled(fixedDelayString = "${mcp.tool-registry-sync-interval-millis:5000}")
public void synchronizeScheduled() {
synchronize();
}
public void synchronize() {
lock.lock();
try {
List<ToolMetadata> activeEntries = redisRegistryService.getAllTools()
.stream().filter(ToolMetadata::getVisible).collect(Collectors.toList());
// 1. Dynamic MCP Server 동기화 (카테고리별)
Map<String, List<ToolMetadata>> toolsByCategory = activeEntries.stream()
.collect(Collectors.groupingBy(
t -> (t.getCategoryKey() == null || t.getCategoryKey().trim().isEmpty()) ? "common" : t.getCategoryKey()
));
Set<String> allKnownCategories = new LinkedHashSet<>(dynamicMcpServerManager.getKnownCategories());
allKnownCategories.addAll(toolsByCategory.keySet());
for (String category : allKnownCategories) {
dynamicMcpServerManager.synchronizeCategory(category, toolsByCategory.getOrDefault(category, List.of()));
}
// 2. Global MCP Server 동기화 (기존 하위 호환)
McpSyncServer server = mcpServerProvider.getIfAvailable();
if (server != null) {
Set<String> activeNames = activeEntries.stream()
.map(ToolMetadata::getName)
.collect(Collectors.toCollection(LinkedHashSet::new));
Set<String> removedNames = new LinkedHashSet<>(managedToolNames);
removedNames.removeAll(activeNames);
for (String removedName : removedNames) {
server.removeTool(removedName);
managedToolNames.remove(removedName);
log.info("Removed MCP registry tool (Global). tool={}", removedName);
}
for (ToolMetadata entry : activeEntries) {
if (managedToolNames.contains(entry.getName())) {
server.removeTool(entry.getName());
}
server.addTool(specificationFactory.create(entry));
managedToolNames.add(entry.getName());
}
}
} catch (Exception error) {
log.warn("MCP registry tool synchronization failed. message={}", error.getMessage());
} finally {
lock.unlock();
}
}
}

View File

@@ -0,0 +1,229 @@
package io.shinhanlife.dap.biz.mcp.gateway.tool.large;
import io.shinhanlife.dap.biz.mcp.gateway.config.AgentResponseBudgetProperties;
import io.shinhanlife.dap.biz.mcp.gateway.guardrail.SensitiveDataMasker;
import org.springframework.stereotype.Service;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
@Service
public class AgentResponseBudgetService {
private final AgentResponseBudgetProperties properties;
private final ObjectMapper json;
private final SensitiveDataMasker masker;
public AgentResponseBudgetService(AgentResponseBudgetProperties properties, ObjectMapper json, SensitiveDataMasker masker) {
this.properties = properties;
this.json = json;
this.masker = masker;
}
public ObjectNode apply(ObjectNode response) {
ArrayNode originalItems = response.path("previewItems").isArray()
? (ArrayNode) response.path("previewItems")
: json.createArrayNode();
int originalCount = originalItems.size();
BudgetStats stats = new BudgetStats();
ArrayNode budgetItems = json.createArrayNode();
for (JsonNode item : originalItems) {
if (budgetItems.size() >= properties.maxPreviewItems()) {
stats.omittedItems++;
stats.limited = true;
continue;
}
JsonNode budgetItem = budgetItem(item, stats);
if (jsonBytes(budgetItem) > properties.maxItemBytes()) {
budgetItem = fallbackTruncatedItem(budgetItem);
stats.truncatedItems++;
stats.limited = true;
}
budgetItems.add(budgetItem);
}
ObjectNode budgeted = copyWithout(response, "previewItems", "budget", "summary");
budgeted.set("previewItems", budgetItems);
if (properties.includeSummary()) {
budgeted.put("summary", summary(budgeted, originalCount, budgetItems.size()));
}
while (jsonBytes(budgeted) > properties.maxTotalBytes() && budgetItems.size() > 0) {
budgetItems.remove(budgetItems.size() - 1);
stats.omittedItems++;
stats.limited = true;
budgeted.set("previewItems", budgetItems);
if (properties.includeSummary()) {
budgeted.put("summary", summary(budgeted, originalCount, budgetItems.size()));
}
}
ObjectNode budget = json.createObjectNode();
updateBudget(budget, stats, originalCount, budgetItems.size());
budgeted.put("previewCount", budgetItems.size());
budgeted.set("budget", budget);
while (jsonBytes(budgeted) > properties.maxTotalBytes() && budgetItems.size() > 0) {
budgetItems.remove(budgetItems.size() - 1);
stats.omittedItems++;
stats.limited = true;
budgeted.set("previewItems", budgetItems);
budgeted.put("previewCount", budgetItems.size());
if (properties.includeSummary()) {
budgeted.put("summary", summary(budgeted, originalCount, budgetItems.size()));
}
updateBudget(budget, stats, originalCount, budgetItems.size());
budgeted.set("budget", budget);
}
return budgeted;
}
private void updateBudget(ObjectNode budget, BudgetStats stats, int originalCount, int previewCount) {
budget.put("limited", stats.limited || stats.omittedItems > 0 || stats.truncatedItems > 0);
budget.put("maxPreviewItems", properties.maxPreviewItems());
budget.put("maxFieldsPerItem", properties.maxFieldsPerItem());
budget.put("maxItemBytes", properties.maxItemBytes());
budget.put("maxTotalBytes", properties.maxTotalBytes());
budget.put("originalPreviewCount", originalCount);
budget.put("previewCount", previewCount);
budget.put("omittedItems", stats.omittedItems);
budget.put("truncatedItems", stats.truncatedItems);
}
private JsonNode budgetItem(JsonNode item, BudgetStats stats) {
JsonNode masked = masker.mask(item);
if (!masked.isObject()) {
return truncateByBytes(masked, stats);
}
ObjectNode limited = json.createObjectNode();
int copied = 0;
int originalFields = 0;
java.util.Iterator<java.util.Map.Entry<String, JsonNode>> fields = masked.fields();
while (fields.hasNext()) {
java.util.Map.Entry<String, JsonNode> entry = fields.next();
originalFields++;
if (copied >= properties.maxFieldsPerItem()) {
continue;
}
limited.set(entry.getKey(), entry.getValue());
copied++;
}
if (originalFields > copied) {
limited.put("truncated", true);
limited.put("omittedFieldCount", originalFields - copied);
stats.truncatedItems++;
stats.limited = true;
}
return truncateByBytes(limited, stats);
}
private JsonNode truncateByBytes(JsonNode item, BudgetStats stats) {
if (jsonBytes(item) <= properties.maxItemBytes()) {
return item;
}
if (!item.isObject()) {
stats.truncatedItems++;
stats.limited = true;
ObjectNode truncated = json.createObjectNode();
truncated.put("truncated", true);
truncated.put("originalBytes", jsonBytes(item));
truncated.put("maxItemBytes", properties.maxItemBytes());
return truncated;
}
ObjectNode source = (ObjectNode) item;
ObjectNode limited = json.createObjectNode();
java.util.Iterator<java.util.Map.Entry<String, JsonNode>> fields = source.fields();
while (fields.hasNext()) {
java.util.Map.Entry<String, JsonNode> entry = fields.next();
JsonNode value = entry.getValue();
if (value.isTextual()) {
limited.put(entry.getKey(), truncateText(value.asText()));
} else {
limited.set(entry.getKey(), value);
}
}
limited.put("truncated", true);
limited.put("originalBytes", jsonBytes(item));
limited.put("maxItemBytes", properties.maxItemBytes());
stats.truncatedItems++;
stats.limited = true;
if (jsonBytes(limited) <= properties.maxItemBytes()) {
return limited;
}
return fallbackTruncatedItem(item);
}
private ObjectNode fallbackTruncatedItem(JsonNode item) {
ObjectNode fallback = json.createObjectNode();
fallback.put("truncated", true);
fallback.put("originalBytes", jsonBytes(item));
fallback.put("maxItemBytes", properties.maxItemBytes());
if (item.isObject()) {
ArrayNode fieldNames = json.createArrayNode();
java.util.Iterator<String> fieldNamesIter = item.fieldNames();
while (fieldNamesIter.hasNext()) {
fieldNames.add(fieldNamesIter.next());
}
fallback.set("fieldNames", fieldNames);
}
return fallback;
}
private String truncateText(String value) {
if (value == null) {
return "";
}
int maxChars = Math.min(value.length(), 256);
String truncated = value.substring(0, maxChars);
while (truncated.length() > 16 && truncated.getBytes(java.nio.charset.StandardCharsets.UTF_8).length > properties.maxItemBytes() / 2) {
truncated = truncated.substring(0, truncated.length() / 2);
}
return truncated + "...";
}
private ObjectNode copyWithout(ObjectNode source, String... excludedFields) {
ObjectNode copy = json.createObjectNode();
java.util.Iterator<java.util.Map.Entry<String, JsonNode>> fields = source.fields();
while (fields.hasNext()) {
java.util.Map.Entry<String, JsonNode> entry = fields.next();
if (!excluded(entry.getKey(), excludedFields)) {
copy.set(entry.getKey(), entry.getValue());
}
}
return copy;
}
private boolean excluded(String field, String[] excludedFields) {
for (String excluded : excludedFields) {
if (excluded.equals(field)) {
return true;
}
}
return false;
}
private String summary(ObjectNode response, int originalCount, int previewCount) {
long totalCount = response.path("totalCount").asLong(originalCount);
return "Returned " + previewCount + " preview item(s) out of " + totalCount + " total result(s).";
}
private long jsonBytes(JsonNode node) {
try {
return json.writeValueAsBytes(node).length;
} catch (Exception error) {
return Long.MAX_VALUE;
}
}
private static final class BudgetStats {
private boolean limited;
private int omittedItems;
private int truncatedItems;
}
}

View File

@@ -0,0 +1,347 @@
package io.shinhanlife.dap.biz.mcp.gateway.tool.large;
import io.shinhanlife.dap.biz.mcp.gateway.config.McpGatewayProperties;
import io.shinhanlife.dap.biz.mcp.gateway.guardrail.SensitiveDataMasker;
import io.shinhanlife.dap.biz.mcp.gateway.resilience.FailureType;
import io.shinhanlife.dap.biz.mcp.gateway.resilience.ToolExecutionException;
import org.springframework.stereotype.Service;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import java.time.Duration;
import java.time.Instant;
@Service
public class LargeToolResponseService {
private final McpGatewayProperties properties;
private final ObjectMapper json;
private final SensitiveDataMasker masker;
private final AgentResponseBudgetService agentBudget;
public LargeToolResponseService(McpGatewayProperties properties,
ObjectMapper json,
SensitiveDataMasker masker,
AgentResponseBudgetService agentBudget) {
this.properties = properties;
this.json = json;
this.masker = masker;
this.agentBudget = agentBudget;
}
public Collector newCollector(String toolName, String requestId) {
return new Collector(toolName, requestId);
}
public ObjectNode tooLargeResult(String toolName, String requestId, long receivedBytes, String nextCursor) {
ObjectNode response = basePartialResponse(toolName, requestId);
response.put("failureType", FailureType.TOO_LARGE_RESULT.name());
response.put("errorCode", "TOOL_RESULT_TOO_LARGE");
response.put("message", "Tool 응답이 허용 크기를 초과했습니다. 원문 body는 저장하지 않았고 cursor/resultRef 기반 재조회가 필요합니다.");
response.put("receivedBytes", receivedBytes);
response.put("maxResponseBytesFromTool", properties.maxResponseBytesFromTool());
response.put("hasMore", true);
response.put("nextCursor", safe(nextCursor));
response.put("resultRef", resultRef(toolName, requestId, nextCursor));
response.put("truncated", true);
response.put("omittedCount", 0);
response.set("previewItems", json.createArrayNode());
response.set("streamEvents", streamEvents(toolName, 0, 0, true, nextCursor));
attachPageInfoAndStructuredContent(response);
return agentBudget.apply(response);
}
private ObjectNode basePartialResponse(String toolName, String requestId) {
ObjectNode response = json.createObjectNode();
response.put("success", true);
response.put("toolName", toolName);
response.put("requestId", requestId);
response.put("status", "PARTIAL_RESULT");
response.put("pageSize", pageSize());
response.put("pageCount", 0);
response.put("returnedCount", 0);
response.put("totalCount", 0);
response.put("hasMore", false);
response.put("nextCursor", "");
response.put("truncated", false);
response.put("omittedCount", 0);
response.put("maxStreamBytes", properties.maxStreamBytes());
response.put("maxItemBytes", properties.largeResponseMaxItemBytes());
return response;
}
private ArrayNode streamEvents(String toolName, int pageCount, int returnedCount, boolean hasMore, String nextCursor) {
ArrayNode events = json.createArrayNode();
ObjectNode start = json.createObjectNode();
start.put("type", "start");
start.put("tool", toolName);
events.add(start);
ObjectNode summary = json.createObjectNode();
summary.put("type", "summary");
summary.put("pageCount", pageCount);
summary.put("returnedCount", returnedCount);
summary.put("message", returnedCount + "건의 preview를 생성했습니다.");
events.add(summary);
ObjectNode done = json.createObjectNode();
done.put("type", "done");
done.put("hasMore", hasMore);
done.put("nextCursor", safe(nextCursor));
events.add(done);
return events;
}
private String resultRef(String toolName, String requestId, String nextCursor) {
if (nextCursor == null || nextCursor.isBlank()) {
return "tool-result:" + toolName + ":" + requestId;
}
return "tool-result:" + toolName + ":" + requestId + ":cursor:" + nextCursor;
}
private int pageSize() {
return Math.min(properties.defaultPageSize(), properties.maxPageSize());
}
private String safe(String value) {
return value == null ? "" : value;
}
public class Collector {
private final String toolName;
private final String requestId;
private final Instant startedAt = Instant.now();
private final ArrayNode previewItems = json.createArrayNode();
private final ArrayNode streamEvents = json.createArrayNode();
private int pageCount;
private int returnedCount;
private long totalCount;
private boolean paginated;
private boolean hasMore;
private String nextCursor = "";
private String stopReason = "";
private JsonNode normalData;
private boolean truncated;
private int omittedCount;
private Collector(String toolName, String requestId) {
this.toolName = toolName;
this.requestId = requestId;
ObjectNode start = json.createObjectNode();
start.put("type", "start");
start.put("tool", toolName);
streamEvents.add(start);
}
public void accept(JsonNode data) {
if (data == null || data.isMissingNode() || data.isNull()) {
throw new ToolExecutionException(FailureType.BUSINESS_ERROR, "Tool response data is empty. tool=" + toolName);
}
Page page = pageFrom(data);
if (!page.paginated() && pageCount == 0 && count(page.items()) <= pageSize()) {
JsonNode masked = masker.mask(data);
normalData = masked;
pageCount = 1;
returnedCount = count(page.items());
totalCount = returnedCount;
return;
}
paginated = true;
pageCount++;
totalCount = page.totalCount() >= 0 ? page.totalCount() : totalCount;
hasMore = page.hasMore();
nextCursor = page.nextCursor();
int pageItemCount = 0;
for (JsonNode item : page.items()) {
pageItemCount++;
JsonNode preview = previewItem(item);
if (previewItems.size() < maxPreviewItems() && canAddPreviewItem(preview)) {
previewItems.add(preview);
returnedCount++;
} else {
omittedCount++;
truncated = true;
}
}
ObjectNode summary = json.createObjectNode();
summary.put("type", "summary");
summary.put("page", pageCount);
summary.put("count", pageItemCount);
summary.put("returnedCount", returnedCount);
summary.put("omittedCount", omittedCount);
summary.put("nextCursor", nextCursor());
summary.put("message", returnedCount + " items previewed");
addStreamEvent(summary);
if (pageCount >= properties.maxPagesPerCall() && hasMore) {
stopReason = "MAX_PAGES_PER_CALL";
truncated = true;
}
if (Duration.between(startedAt, Instant.now()).toSeconds() >= properties.maxDurationSeconds() && hasMore) {
stopReason = "MAX_DURATION_SECONDS";
truncated = true;
}
}
public boolean shouldFetchNextPage() {
return paginated
&& hasMore
&& nextCursor != null
&& !nextCursor.isBlank()
&& pageCount < properties.maxPagesPerCall()
&& Duration.between(startedAt, Instant.now()).toSeconds() < properties.maxDurationSeconds()
&& currentResponseBytes() < properties.maxStreamBytes();
}
public String nextCursor() {
return safe(nextCursor);
}
public int pageSize() {
return LargeToolResponseService.this.pageSize();
}
public ObjectNode finish() {
if (normalData != null) {
ObjectNode wrap = json.createObjectNode();
wrap.put("success", true);
wrap.put("toolName", toolName);
wrap.put("requestId", requestId);
wrap.set("data", normalData);
return wrap;
}
ObjectNode done = json.createObjectNode();
done.put("type", "done");
done.put("totalCount", totalCount);
done.put("hasMore", hasMore);
done.put("nextCursor", nextCursor());
addStreamEvent(done);
ObjectNode response = basePartialResponse(toolName, requestId);
response.put("pageCount", pageCount);
response.put("returnedCount", returnedCount);
response.put("totalCount", totalCount);
response.put("hasMore", hasMore);
response.put("nextCursor", nextCursor());
response.put("resultRef", resultRef(toolName, requestId, nextCursor()));
response.put("truncated", truncated);
response.put("omittedCount", omittedCount);
response.put("message", stopReason.isBlank()
? "대량 결과라 일부 preview만 반환했습니다."
: "대량 결과라 " + stopReason + " 조건에서 중단하고 일부 preview만 반환했습니다.");
response.set("previewItems", previewItems);
response.set("streamEvents", streamEvents);
attachPageInfoAndStructuredContent(response);
return agentBudget.apply(response);
}
private Page pageFrom(JsonNode data) {
if (data.isObject() && data.path("items").isArray()) {
return new Page(true, data.path("items"), data.path("nextCursor").asText(""),
data.path("hasMore").asBoolean(false), data.path("totalCount").asLong(-1));
}
if (data.isArray()) {
return new Page(false, data, "", false, count(data));
}
ArrayNode one = json.createArrayNode();
one.add(data);
return new Page(false, one, "", false, 1);
}
private JsonNode previewItem(JsonNode item) {
JsonNode masked = masker.mask(item);
long itemBytes = jsonBytes(masked);
if (itemBytes <= properties.largeResponseMaxItemBytes()) {
return masked;
}
truncated = true;
ObjectNode preview = json.createObjectNode();
preview.put("truncated", true);
preview.put("originalBytes", itemBytes);
preview.put("maxItemBytes", properties.largeResponseMaxItemBytes());
if (masked.isObject()) {
ArrayNode fieldNames = json.createArrayNode();
java.util.Iterator<String> fieldNamesIter = masked.fieldNames();
while (fieldNamesIter.hasNext()) {
fieldNames.add(fieldNamesIter.next());
}
preview.set("fieldNames", fieldNames);
}
return preview;
}
private boolean canAddPreviewItem(JsonNode item) {
return jsonBytes(item) <= properties.largeResponseMaxItemBytes()
&& currentResponseBytes() + jsonBytes(item) <= properties.maxStreamBytes();
}
private void addStreamEvent(ObjectNode event) {
if (currentResponseBytes() + jsonBytes(event) <= properties.maxStreamBytes()) {
streamEvents.add(event);
return;
}
truncated = true;
}
private int maxPreviewItems() {
return Math.min(pageSize() * properties.maxPagesPerCall(), properties.largeResponseMaxPreviewItems());
}
private long currentResponseBytes() {
ObjectNode estimate = json.createObjectNode();
estimate.set("previewItems", previewItems);
estimate.set("streamEvents", streamEvents);
return jsonBytes(estimate);
}
private long jsonBytes(JsonNode node) {
try {
return json.writeValueAsBytes(node).length;
} catch (Exception error) {
return Long.MAX_VALUE;
}
}
private int count(JsonNode node) {
int count = 0;
java.util.Iterator<JsonNode> iter = node.elements();
while(iter.hasNext()) {
iter.next();
count++;
}
return count;
}
}
private record Page(boolean paginated, JsonNode items, String nextCursor, boolean hasMore, long totalCount) {}
private void attachPageInfoAndStructuredContent(ObjectNode response) {
ObjectNode pageInfo = json.createObjectNode();
pageInfo.put("pageSize", response.path("pageSize").asInt(pageSize()));
pageInfo.put("pageCount", response.path("pageCount").asInt(0));
pageInfo.put("returnedCount", response.path("returnedCount").asInt(0));
pageInfo.put("totalCount", response.path("totalCount").asLong(0));
pageInfo.put("hasMore", response.path("hasMore").asBoolean(false));
pageInfo.put("nextCursor", response.path("nextCursor").asText(""));
response.set("pageInfo", pageInfo);
ObjectNode summary = json.createObjectNode();
summary.put("status", response.path("status").asText("PARTIAL_RESULT"));
summary.put("toolName", response.path("toolName").asText(""));
summary.put("message", response.path("message").asText(""));
summary.put("truncated", response.path("truncated").asBoolean(false));
summary.put("omittedCount", response.path("omittedCount").asInt(0));
summary.put("resultRef", response.path("resultRef").asText(""));
ObjectNode structuredContent = json.createObjectNode();
structuredContent.set("summary", summary);
structuredContent.set("pageInfo", pageInfo.deepCopy());
structuredContent.set("previewItems", response.path("previewItems").deepCopy());
response.set("structuredContent", structuredContent);
}
}

View File

@@ -0,0 +1,101 @@
package io.shinhanlife.dap.biz.mcp.gateway.tool.large;
import io.shinhanlife.dap.biz.mcp.gateway.config.McpGatewayProperties;
import io.shinhanlife.dap.biz.mcp.gateway.resilience.FailureType;
import io.shinhanlife.dap.biz.mcp.gateway.resilience.ToolExecutionException;
import org.springframework.stereotype.Component;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
/**
* Tool 서버로 전달할 pagination 요청값을 MCP 정책 기준으로 검증하고 보정합니다.
*/
@Component
public class PaginationRequestValidator {
private static final int MAX_CURSOR_LENGTH = 2048;
private final McpGatewayProperties properties;
private final ObjectMapper json;
public PaginationRequestValidator(McpGatewayProperties properties, ObjectMapper json) {
this.properties = properties;
this.json = json;
}
/**
* pageSize/cursor를 검증한 뒤 Tool 서버에 넘길 안전한 arguments 복사본을 만듭니다.
*/
public ObjectNode normalize(ObjectNode arguments) {
try {
ObjectNode normalized = arguments == null
? json.createObjectNode()
: (ObjectNode) json.readTree(json.writeValueAsString(arguments));
normalizePageSize(normalized);
validateCursor(normalized);
return normalized;
} catch (ToolExecutionException error) {
throw error;
} catch (Exception error) {
throw standardError("INVALID_PAGINATION_REQUEST",
"Pagination 요청값을 검증하는 중 오류가 발생했습니다.",
detail("cause", error.getMessage()));
}
}
private void normalizePageSize(ObjectNode normalized) {
int pageSize = normalized.has("pageSize")
? normalized.path("pageSize").asInt(-1)
: properties.defaultPageSize();
if (pageSize < 1) {
throw standardError("INVALID_PAGINATION_REQUEST",
"pageSize는 1 이상이어야 합니다.",
detail("requestedPageSize", pageSize));
}
if (pageSize > properties.maxPageSize()) {
ObjectNode detail = detail("requestedPageSize", pageSize);
detail.put("maxPageSize", properties.maxPageSize());
detail.put("suggestedPageSize", properties.defaultPageSize());
throw standardError("PAGE_SIZE_EXCEEDED",
"요청한 pageSize가 MCP 최대 허용값을 초과했습니다.",
detail);
}
normalized.put("pageSize", pageSize);
}
private void validateCursor(ObjectNode normalized) {
if (!normalized.has("cursor")) {
return;
}
String cursor = normalized.path("cursor").asText("");
if (cursor.length() > MAX_CURSOR_LENGTH) {
ObjectNode detail = detail("cursorLength", cursor.length());
detail.put("maxCursorLength", MAX_CURSOR_LENGTH);
throw standardError("INVALID_CURSOR",
"cursor 값이 너무 깁니다.",
detail);
}
}
private ToolExecutionException standardError(String code, String message, ObjectNode detail) {
ObjectNode error = json.createObjectNode();
error.put("isError", true);
error.put("errorCode", code);
error.put("message", message);
error.set("details", detail);
try {
return new ToolExecutionException(FailureType.CLIENT_ERROR, json.writeValueAsString(error));
} catch (Exception serializeError) {
return new ToolExecutionException(FailureType.CLIENT_ERROR, code + ": " + message);
}
}
private ObjectNode detail(String name, Object value) {
ObjectNode detail = json.createObjectNode();
if (value instanceof Number number) {
detail.put(name, number.longValue());
} else {
detail.put(name, value == null ? "" : String.valueOf(value));
}
return detail;
}
}

View File

@@ -0,0 +1,35 @@
package io.shinhanlife.dap.biz.mcp.gateway.tool.result;
import com.fasterxml.jackson.databind.JsonNode;
import java.util.List;
import java.util.Map;
/**
* MCP Tool 실행 결과를 text 요약과 structuredContent로 분리해서 표현합니다.
*/
public record ToolExecutionResult(
ResultType resultType,
List<ContentItem> content,
JsonNode structuredContent,
List<ResourceLink> resourceLinks,
Map<String, Object> metadata,
boolean isError,
boolean truncated,
long originalSizeBytes
) {
public record ContentItem(String type, String text) {
}
public record ResourceLink(String uri, String name, String mimeType, long sizeBytes) {
}
public enum ResultType {
JSON_OBJECT,
JSON_ARRAY,
PLAIN_TEXT,
BINARY_FILE,
EMPTY,
ERROR
}
}

View File

@@ -0,0 +1,153 @@
package io.shinhanlife.dap.biz.mcp.gateway.tool.result;
import io.shinhanlife.dap.biz.mcp.gateway.guardrail.SensitiveDataMasker;
import org.springframework.stereotype.Service;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;
/**
* Tool 서버 원시 응답을 MCP의 content 요약과 structuredContent로 분리합니다.
*/
@Service
public class ToolExecutionResultFormatter {
private static final int TEXT_PREVIEW_LIMIT = 2_000;
private final ObjectMapper json;
private final SensitiveDataMasker masker;
public ToolExecutionResultFormatter(ObjectMapper json, SensitiveDataMasker masker) {
this.json = json;
this.masker = masker;
}
/**
* JSON 문자열을 content.text에 그대로 넣지 않고 structuredContent로 분리합니다.
*/
public ToolExecutionResult fromRawResponse(String toolName, String rawResponse) {
long sizeBytes = rawResponse == null ? 0 : rawResponse.getBytes(StandardCharsets.UTF_8).length;
if (rawResponse == null || rawResponse.isBlank()) {
return result(ToolExecutionResult.ResultType.EMPTY, "Tool 응답이 비어 있습니다.",
json.createObjectNode(), Map.of("toolName", toolName), false, false, sizeBytes);
}
try {
JsonNode parsed = json.readTree(rawResponse);
return fromJson(toolName, parsed, sizeBytes);
} catch (Exception parseError) {
ObjectNode metadata = json.createObjectNode();
metadata.put("toolName", toolName);
metadata.put("parseError", "JSON_PARSE_FAILED");
metadata.put("preview", preview(rawResponse));
return new ToolExecutionResult(
ToolExecutionResult.ResultType.PLAIN_TEXT,
List.of(new ToolExecutionResult.ContentItem("text", "Tool 응답이 JSON 형식이 아니어서 제한된 preview만 제공합니다.")),
null,
List.of(),
Map.of("toolName", toolName, "parseError", "JSON_PARSE_FAILED", "preview", preview(rawResponse)),
false,
rawResponse.length() > TEXT_PREVIEW_LIMIT,
sizeBytes);
}
}
public ToolExecutionResult fromJson(String toolName, JsonNode parsed, long sizeBytes) {
JsonNode masked = masker.mask(parsed);
if (masked.isObject()) {
ObjectNode object = (ObjectNode) masked;
if (object.path("isError").asBoolean(false) || object.has("error") || object.has("failureType")) {
return result(ToolExecutionResult.ResultType.ERROR, errorSummary(toolName, object), object,
metadata(toolName, sizeBytes), true, object.path("truncated").asBoolean(false), sizeBytes);
}
JsonNode structured = object.has("structuredContent") ? object.path("structuredContent") : structuredFromObject(object);
return result(ToolExecutionResult.ResultType.JSON_OBJECT, summary(toolName, object, structured), structured,
metadata(toolName, sizeBytes), false, object.path("truncated").asBoolean(false), sizeBytes);
}
if (masked.isArray()) {
ObjectNode structured = json.createObjectNode();
structured.set("items", masked);
structured.put("count", count(masked));
return result(ToolExecutionResult.ResultType.JSON_ARRAY,
toolName + " 결과 " + count(masked) + "건을 조회했습니다.",
structured,
metadata(toolName, sizeBytes),
false,
false,
sizeBytes);
}
if (masked.isTextual()) {
String text = masked.asText("");
return result(ToolExecutionResult.ResultType.PLAIN_TEXT, preview(text), null,
metadata(toolName, sizeBytes), false, text.length() > TEXT_PREVIEW_LIMIT, sizeBytes);
}
ObjectNode structured = json.createObjectNode();
structured.set("value", masked);
return result(ToolExecutionResult.ResultType.JSON_OBJECT, toolName + " 결과를 조회했습니다.",
structured, metadata(toolName, sizeBytes), false, false, sizeBytes);
}
private JsonNode structuredFromObject(ObjectNode object) {
if (object.has("data")) {
return object.path("data");
}
return object;
}
private String summary(String toolName, ObjectNode object, JsonNode structured) {
String message = object.path("message").asText("");
if (!message.isBlank()) {
return message;
}
if (structured != null && structured.isObject()) {
long totalCount = structured.path("totalCount").asLong(-1);
if (totalCount >= 0) {
return toolName + " 대량 결과 preview 조회가 완료되었습니다. totalCount=" + totalCount;
}
}
if (structured != null && structured.isArray()) {
return toolName + " 결과 " + count(structured) + "건을 조회했습니다.";
}
return toolName + " 조회가 완료되었습니다.";
}
private String errorSummary(String toolName, ObjectNode object) {
String message = object.path("message").asText(object.path("error").asText(""));
return message.isBlank() ? toolName + " 호출 중 오류가 발생했습니다." : message;
}
private ToolExecutionResult result(ToolExecutionResult.ResultType type, String text, JsonNode structuredContent,
Map<String, Object> metadata, boolean error, boolean truncated, long sizeBytes) {
return new ToolExecutionResult(
type,
List.of(new ToolExecutionResult.ContentItem("text", text)),
structuredContent,
List.of(),
metadata,
error,
truncated,
sizeBytes);
}
private Map<String, Object> metadata(String toolName, long sizeBytes) {
return Map.of("toolName", toolName, "originalSizeBytes", sizeBytes);
}
private int count(JsonNode node) {
int count = 0;
for (JsonNode ignored : node) {
count++;
}
return count;
}
private String preview(String value) {
if (value == null) {
return "";
}
return value.length() <= TEXT_PREVIEW_LIMIT ? value : value.substring(0, TEXT_PREVIEW_LIMIT);
}
}

View File

@@ -0,0 +1,51 @@
package io.shinhanlife.dap.biz.mcp.gateway.transport;
import io.shinhanlife.dap.biz.mcp.gateway.resilience.FailureType;
import io.shinhanlife.dap.biz.mcp.gateway.resilience.ToolExecutionException;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestClient;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import java.util.Map;
import java.util.UUID;
@Slf4j
@Component
public class HttpToolInvoker implements ToolInvoker {
private final RestClient restClient;
private final ObjectMapper objectMapper;
public HttpToolInvoker(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
this.restClient = RestClient.create();
}
@Override
public JsonNode invoke(Map<String, Object> payload, String targetUrl) {
try {
Object httpResult = restClient.post()
.uri(targetUrl)
.contentType(MediaType.APPLICATION_JSON)
// TODO: Use actual tenant's key
.header("X-Trace-Id", UUID.randomUUID().toString())
.body(payload)
.retrieve()
.body(Object.class);
return extractData(objectMapper.valueToTree(httpResult));
} catch (Exception e) {
throw new ToolExecutionException(FailureType.SERVER_ERROR, "Tool Pod HTTP 호출 실패: " + e.getMessage(), e);
}
}
private JsonNode extractData(JsonNode root) {
if (!root.path("success").asBoolean(true)) {
throw new ToolExecutionException(FailureType.BUSINESS_ERROR, "Tool 서버 업무 오류: " + root.path("error").asText());
}
return root.has("data") ? root.get("data") : root;
}
}

View File

@@ -0,0 +1,11 @@
package io.shinhanlife.dap.biz.mcp.gateway.transport;
import com.fasterxml.jackson.databind.JsonNode;
import java.util.Map;
/**
* Tool 서버 호출 transport의 최소 공통 인터페이스입니다.
*/
public interface ToolInvoker {
JsonNode invoke(Map<String, Object> payload, String targetUrl);
}

View File

@@ -0,0 +1,37 @@
package io.shinhanlife.dap.biz.so.atm.converter;
import io.shinhanlife.dap.biz.so.atm.dto.AthrOutDto;
import io.shinhanlife.dap.biz.so.atm.dto.AthrSaveInDto;
import io.shinhanlife.dap.biz.so.atm.dto.AthrSearchInDto;
import io.shinhanlife.dap.biz.so.atm.dto.RoleListInDto;
import io.shinhanlife.dap.biz.so.atm.presentation.io.AthrSaveRequest;
import io.shinhanlife.dap.biz.so.atm.presentation.io.AthrSearchRequest;
import io.shinhanlife.dap.biz.so.atm.presentation.io.AthrSearchResponse;
import io.shinhanlife.dap.biz.so.atm.presentation.io.RoleListRequest;
import org.mapstruct.Mapper;
/**
* @package io.shinhanlife.dap.biz.so.atm.converter
* @className AccessMgmtConverter
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
@Mapper(componentModel = "spring")
public abstract class AccessMgmtConverter {
public abstract RoleListInDto toInDto(RoleListRequest request);
public abstract AthrSearchInDto toInDto(AthrSearchRequest request);
public abstract AthrSaveInDto toInDto(AthrSaveRequest request);
public abstract AthrSearchResponse toResponse(AthrOutDto outDto);
}

View File

@@ -0,0 +1,48 @@
package io.shinhanlife.dap.biz.so.atm.domain.repository;
import io.shinhanlife.dap.biz.so.atm.dto.AthrItemOutDto;
import io.shinhanlife.dap.biz.so.atm.dto.AthrSearchInDto;
import io.shinhanlife.dap.biz.so.atm.dto.RoleKnwlAthrInDto;
import io.shinhanlife.dap.biz.so.atm.dto.RoleListInDto;
import io.shinhanlife.dap.biz.so.atm.dto.RoleListOutDto;
import io.shinhanlife.dap.biz.so.atm.dto.RoleToolAthrInDto;
import io.shinhanlife.glow.GlowMybatisMapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* @package io.shinhanlife.dap.biz.so.atm.domain.repository
* @className AccessMgmtMapper
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
@GlowMybatisMapper
public interface AccessMgmtMapper {
List<RoleListOutDto> selectRoles(RoleListInDto inDto);
List<AthrItemOutDto> selectAllTools();
List<AthrItemOutDto> selectAllKnwls();
List<String> selectGrantedToolIds(AthrSearchInDto inDto);
List<String> selectGrantedKnwlIds(AthrSearchInDto inDto);
void deleteToolAthr(@Param("systId") String systId, @Param("roleNo") String roleNo);
void insertToolAthr(RoleToolAthrInDto inDto);
void deleteKnwlAthr(@Param("systId") String systId, @Param("roleNo") String roleNo);
void insertKnwlAthr(RoleKnwlAthrInDto inDto);
}

View File

@@ -0,0 +1,32 @@
package io.shinhanlife.dap.biz.so.atm.domain.service;
import io.shinhanlife.dap.biz.so.atm.dto.AthrOutDto;
import io.shinhanlife.dap.biz.so.atm.dto.AthrSaveInDto;
import io.shinhanlife.dap.biz.so.atm.dto.AthrSearchInDto;
import io.shinhanlife.dap.biz.so.atm.dto.RoleListInDto;
import io.shinhanlife.dap.biz.so.atm.dto.RoleListOutDto;
import java.util.List;
/**
* @package io.shinhanlife.dap.biz.so.atm.domain.service
* @className AccessMgmtService
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
public interface AccessMgmtService {
List<RoleListOutDto> getRoles(RoleListInDto inDto);
AthrOutDto getAthr(AthrSearchInDto inDto);
void saveAthr(AthrSaveInDto inDto);
}

View File

@@ -0,0 +1,142 @@
package io.shinhanlife.dap.biz.so.atm.domain.service.impl;
import io.shinhanlife.dap.biz.so.atm.domain.repository.AccessMgmtMapper;
import io.shinhanlife.dap.biz.so.atm.domain.service.AccessMgmtService;
import io.shinhanlife.dap.biz.so.atm.dto.AthrItemOutDto;
import io.shinhanlife.dap.biz.so.atm.dto.AthrOutDto;
import io.shinhanlife.dap.biz.so.atm.dto.AthrSaveInDto;
import io.shinhanlife.dap.biz.so.atm.dto.AthrSearchInDto;
import io.shinhanlife.dap.biz.so.atm.dto.RoleKnwlAthrInDto;
import io.shinhanlife.dap.biz.so.atm.dto.RoleListInDto;
import io.shinhanlife.dap.biz.so.atm.dto.RoleListOutDto;
import io.shinhanlife.dap.biz.so.atm.dto.RoleToolAthrInDto;
import io.shinhanlife.dap.common.util.SessionUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Date;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
/**
* @package io.shinhanlife.dap.biz.so.atm.domain.service.impl
* @className AccessMgmtServiceImpl
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
@Service
@RequiredArgsConstructor
public class AccessMgmtServiceImpl implements AccessMgmtService {
private final AccessMgmtMapper accessMgmtMapper;
private static final String SYSTEM_CD = "AXH";
private static final String SYSTEM_PRG = "SOATM0100";
@Override
public List<RoleListOutDto> getRoles(RoleListInDto inDto) {
return accessMgmtMapper.selectRoles(inDto);
}
@Override
public AthrOutDto getAthr(AthrSearchInDto inDto) {
Set<String> grantedToolIds = accessMgmtMapper.selectGrantedToolIds(inDto)
.stream().collect(Collectors.toSet());
Set<String> grantedKnwlIds = accessMgmtMapper.selectGrantedKnwlIds(inDto)
.stream().collect(Collectors.toSet());
List<AthrItemOutDto> tools = accessMgmtMapper.selectAllTools()
.stream()
.peek(item -> item.setGranted(grantedToolIds.contains(item.getResourceId())))
.collect(Collectors.toList());
List<AthrItemOutDto> knwls = accessMgmtMapper.selectAllKnwls()
.stream()
.peek(item -> item.setGranted(grantedKnwlIds.contains(item.getResourceId())))
.collect(Collectors.toList());
return new AthrOutDto(tools, knwls);
}
@Override
@Transactional
public void saveAthr(AthrSaveInDto inDto) {
Date now = new Date();
String systId = inDto.getSystId();
String roleNo = inDto.getRoleNo();
accessMgmtMapper.deleteToolAthr(systId, roleNo);
if (inDto.getGrantedToolIds() != null) {
for (String toolId : inDto.getGrantedToolIds()) {
accessMgmtMapper.insertToolAthr(buildToolAthrInDto(systId, roleNo, toolId, now));
}
}
accessMgmtMapper.deleteKnwlAthr(systId, roleNo);
if (inDto.getGrantedKnwlIds() != null) {
for (String knwlId : inDto.getGrantedKnwlIds()) {
accessMgmtMapper.insertKnwlAthr(buildKnwlAthrInDto(systId, roleNo, knwlId, now));
}
}
}
private RoleToolAthrInDto buildToolAthrInDto(String systId, String roleNo, String toolId, Date now) {
RoleToolAthrInDto dto = new RoleToolAthrInDto();
dto.setRoleToolAthrId("RTA-" + shortUuid());
dto.setSystId(systId);
dto.setRoleNo(roleNo);
dto.setToolId(toolId);
dto.setPuseYn("Y");
fillAudit(dto, now);
return dto;
}
private RoleKnwlAthrInDto buildKnwlAthrInDto(String systId, String roleNo, String knwlId, Date now) {
RoleKnwlAthrInDto dto = new RoleKnwlAthrInDto();
dto.setRoleKnwlAthrId("RKA-" + shortUuid());
dto.setSystId(systId);
dto.setRoleNo(roleNo);
dto.setKnwlId(knwlId);
dto.setPuseYn("Y");
fillAudit(dto, now);
return dto;
}
private void fillAudit(RoleToolAthrInDto dto, Date now) {
String prafNo = SessionUtil.getPrafNo();
String ognzNo = SessionUtil.getOgnzNo();
dto.setSystRgiDt(now); dto.setSystRgiPrafNo(prafNo);
dto.setSystRgiOgnzNo(ognzNo); dto.setSystRgiSystCd(SYSTEM_CD);
dto.setSystRgiPrgrId(SYSTEM_PRG);
dto.setSystChgDt(now); dto.setSystChgPrafNo(prafNo);
dto.setSystChgOgnzNo(ognzNo); dto.setSystChgSystCd(SYSTEM_CD);
dto.setSystChgPrgrId(SYSTEM_PRG);
}
private void fillAudit(RoleKnwlAthrInDto dto, Date now) {
String prafNo = SessionUtil.getPrafNo();
String ognzNo = SessionUtil.getOgnzNo();
dto.setSystRgiDt(now); dto.setSystRgiPrafNo(prafNo);
dto.setSystRgiOgnzNo(ognzNo); dto.setSystRgiSystCd(SYSTEM_CD);
dto.setSystRgiPrgrId(SYSTEM_PRG);
dto.setSystChgDt(now); dto.setSystChgPrafNo(prafNo);
dto.setSystChgOgnzNo(ognzNo); dto.setSystChgSystCd(SYSTEM_CD);
dto.setSystChgPrgrId(SYSTEM_PRG);
}
private String shortUuid() {
return UUID.randomUUID().toString().replace("-", "").substring(0, 12).toUpperCase();
}
}

View File

@@ -0,0 +1,35 @@
package io.shinhanlife.dap.biz.so.atm.dto;
import lombok.Builder;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
/**
* @package io.shinhanlife.dap.biz.so.atm.dto
* @className AthrItemOutDto
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class AthrItemOutDto {
private String resourceId;
private String resourceNm;
private String resourceDs;
private String typeCode;
private boolean granted;
}

View File

@@ -0,0 +1,34 @@
package io.shinhanlife.dap.biz.so.atm.dto;
import lombok.Setter;
import lombok.Builder;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.List;
/**
* @package io.shinhanlife.dap.biz.so.atm.dto
* @className AthrOutDto
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
@Getter
@AllArgsConstructor
@Setter
@Builder
@NoArgsConstructor
public class AthrOutDto {
private List<AthrItemOutDto> tools;
private List<AthrItemOutDto> knwls;
}

View File

@@ -0,0 +1,36 @@
package io.shinhanlife.dap.biz.so.atm.dto;
import lombok.Builder;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import java.util.List;
/**
* @package io.shinhanlife.dap.biz.so.atm.dto
* @className AthrSaveInDto
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class AthrSaveInDto {
private String systId;
private String roleNo;
private List<String> grantedToolIds;
private List<String> grantedKnwlIds;
}

View File

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

View File

@@ -0,0 +1,48 @@
package io.shinhanlife.dap.biz.so.atm.dto;
import lombok.Builder;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import java.util.Date;
/**
* @package io.shinhanlife.dap.biz.so.atm.dto
* @className RoleKnwlAthrInDto
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class RoleKnwlAthrInDto {
private String roleKnwlAthrId;
private String systId;
private String roleNo;
private String knwlId;
private String puseYn;
private Date systRgiDt;
private String systRgiPrafNo;
private String systRgiOgnzNo;
private String systRgiSystCd;
private String systRgiPrgrId;
private Date systChgDt;
private String systChgPrafNo;
private String systChgOgnzNo;
private String systChgSystCd;
private String systChgPrgrId;
}

View File

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

View File

@@ -0,0 +1,35 @@
package io.shinhanlife.dap.biz.so.atm.dto;
import lombok.Builder;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
/**
* @package io.shinhanlife.dap.biz.so.atm.dto
* @className RoleListOutDto
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class RoleListOutDto {
private String systId;
private String roleNo;
private String roleNm;
private String roleDs;
private String puseYn;
}

View File

@@ -0,0 +1,48 @@
package io.shinhanlife.dap.biz.so.atm.dto;
import lombok.Builder;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import java.util.Date;
/**
* @package io.shinhanlife.dap.biz.so.atm.dto
* @className RoleToolAthrInDto
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class RoleToolAthrInDto {
private String roleToolAthrId;
private String systId;
private String roleNo;
private String toolId;
private String puseYn;
private Date systRgiDt;
private String systRgiPrafNo;
private String systRgiOgnzNo;
private String systRgiSystCd;
private String systRgiPrgrId;
private Date systChgDt;
private String systChgPrafNo;
private String systChgOgnzNo;
private String systChgSystCd;
private String systChgPrgrId;
}

View File

@@ -0,0 +1,59 @@
package io.shinhanlife.dap.biz.so.atm.presentation;
import io.shinhanlife.dap.biz.so.atm.dto.RoleListOutDto;
import io.shinhanlife.dap.biz.so.atm.presentation.io.AthrSaveRequest;
import io.shinhanlife.dap.biz.so.atm.presentation.io.AthrSearchRequest;
import io.shinhanlife.dap.biz.so.atm.presentation.io.AthrSearchResponse;
import io.shinhanlife.dap.biz.so.atm.presentation.io.RoleListRequest;
import io.shinhanlife.dap.biz.so.atm.usecase.AccessMgmtUseCase;
import io.shinhanlife.glow.BaseResponse;
import io.shinhanlife.glow.ResponseUtil;
import lombok.RequiredArgsConstructor;
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.List;
/**
* @package io.shinhanlife.dap.biz.so.atm.presentation
* @className SOATM0100Controller
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
@RestController
@RequestMapping("/so/atm")
@RequiredArgsConstructor
public class SOATM0100Controller {
private final AccessMgmtUseCase accessMgmtUseCase;
@PostMapping("/SOATM0100R")
public ResponseEntity<BaseResponse<List<RoleListOutDto>>> getRoles(
@RequestBody RoleListRequest request) {
return ResponseUtil.ok(accessMgmtUseCase.getRoles(request));
}
@PostMapping("/SOATM0101R")
public ResponseEntity<BaseResponse<AthrSearchResponse>> getAthr(
@RequestBody AthrSearchRequest request) {
return ResponseUtil.ok(accessMgmtUseCase.getAthr(request));
}
@PostMapping("/SOATM0100U")
public ResponseEntity<BaseResponse<Void>> saveAthr(
@RequestBody AthrSaveRequest request) {
accessMgmtUseCase.saveAthr(request);
return ResponseUtil.ok();
}
}

View File

@@ -0,0 +1,29 @@
package io.shinhanlife.dap.biz.so.atm.presentation.io;
import lombok.Getter;
import lombok.Setter;
import java.util.List;
/**
* @package io.shinhanlife.dap.biz.so.atm.presentation.io
* @className AthrSaveRequest
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
@Getter
@Setter
public class AthrSaveRequest {
private String systId;
private String roleNo;
private List<String> grantedToolIds;
private List<String> grantedKnwlIds;
}

View File

@@ -0,0 +1,25 @@
package io.shinhanlife.dap.biz.so.atm.presentation.io;
import lombok.Getter;
import lombok.Setter;
/**
* @package io.shinhanlife.dap.biz.so.atm.presentation.io
* @className AthrSearchRequest
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
@Getter
@Setter
public class AthrSearchRequest {
private String systId;
private String roleNo;
}

View File

@@ -0,0 +1,28 @@
package io.shinhanlife.dap.biz.so.atm.presentation.io;
import io.shinhanlife.dap.biz.so.atm.dto.AthrItemOutDto;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.List;
/**
* @package io.shinhanlife.dap.biz.so.atm.presentation.io
* @className AthrSearchResponse
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
@Getter
@AllArgsConstructor
public class AthrSearchResponse {
private List<AthrItemOutDto> tools;
private List<AthrItemOutDto> knwls;
}

View File

@@ -0,0 +1,26 @@
package io.shinhanlife.dap.biz.so.atm.presentation.io;
import lombok.Getter;
import lombok.Setter;
/**
* @package io.shinhanlife.dap.biz.so.atm.presentation.io
* @className RoleListRequest
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
@Getter
@Setter
public class RoleListRequest {
private String systId;
private String puseYn;
private String keyword;
}

View File

@@ -0,0 +1,32 @@
package io.shinhanlife.dap.biz.so.atm.usecase;
import io.shinhanlife.dap.biz.so.atm.dto.RoleListOutDto;
import io.shinhanlife.dap.biz.so.atm.presentation.io.AthrSaveRequest;
import io.shinhanlife.dap.biz.so.atm.presentation.io.AthrSearchRequest;
import io.shinhanlife.dap.biz.so.atm.presentation.io.AthrSearchResponse;
import io.shinhanlife.dap.biz.so.atm.presentation.io.RoleListRequest;
import java.util.List;
/**
* @package io.shinhanlife.dap.biz.so.atm.usecase
* @className AccessMgmtUseCase
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
public interface AccessMgmtUseCase {
List<RoleListOutDto> getRoles(RoleListRequest request);
AthrSearchResponse getAthr(AthrSearchRequest request);
void saveAthr(AthrSaveRequest request);
}

View File

@@ -0,0 +1,51 @@
package io.shinhanlife.dap.biz.so.atm.usecase.impl;
import io.shinhanlife.dap.biz.so.atm.converter.AccessMgmtConverter;
import io.shinhanlife.dap.biz.so.atm.domain.service.AccessMgmtService;
import io.shinhanlife.dap.biz.so.atm.dto.RoleListOutDto;
import io.shinhanlife.dap.biz.so.atm.presentation.io.AthrSaveRequest;
import io.shinhanlife.dap.biz.so.atm.presentation.io.AthrSearchRequest;
import io.shinhanlife.dap.biz.so.atm.presentation.io.AthrSearchResponse;
import io.shinhanlife.dap.biz.so.atm.presentation.io.RoleListRequest;
import io.shinhanlife.dap.biz.so.atm.usecase.AccessMgmtUseCase;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* @package io.shinhanlife.dap.biz.so.atm.usecase.impl
* @className AccessMgmtUseCaseImpl
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
@Service
@RequiredArgsConstructor
public class AccessMgmtUseCaseImpl implements AccessMgmtUseCase {
private final AccessMgmtService accessMgmtService;
private final AccessMgmtConverter converter;
@Override
public List<RoleListOutDto> getRoles(RoleListRequest request) {
return accessMgmtService.getRoles(converter.toInDto(request));
}
@Override
public AthrSearchResponse getAthr(AthrSearchRequest request) {
return converter.toResponse(accessMgmtService.getAthr(converter.toInDto(request)));
}
@Override
public void saveAthr(AthrSaveRequest request) {
accessMgmtService.saveAthr(converter.toInDto(request));
}
}

View File

@@ -0,0 +1,37 @@
package io.shinhanlife.dap.sample.converter;
import io.shinhanlife.dap.sample.domain.model.AppliSystNtfyPatiModel;
import io.shinhanlife.dap.sample.dto.AppliSystNtfyRgiInDTO;
import io.shinhanlife.dap.sample.dto.StrnTermListInDTO;
import io.shinhanlife.dap.sample.dto.StrnTermListOutDTO;
import io.shinhanlife.dap.sample.presentation.io.AppliSystNtfyPatiRequest;
import io.shinhanlife.dap.sample.presentation.io.StrnTermRequest;
import io.shinhanlife.dap.sample.presentation.io.StrnTermResponse;
import org.mapstruct.Mapper;
/**
* @package io.shinhanlife.dap.sample.converter
* @className SampleConverter
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
@Mapper(componentModel = "spring")
public abstract class SampleConverter {
public abstract StrnTermListInDTO convertRequestToDto(StrnTermRequest req);
public abstract StrnTermResponse convertDtoToResponse(StrnTermListOutDTO outDto);
public abstract AppliSystNtfyPatiModel convertDtoToModel(AppliSystNtfyRgiInDTO inDto);
public abstract AppliSystNtfyRgiInDTO convertAppliRequestToDto(AppliSystNtfyPatiRequest request);
}

View File

@@ -0,0 +1,35 @@
package io.shinhanlife.dap.sample.domain.model;
import io.shinhanlife.glow.db.dto.AuditInfo;
import lombok.*;
/**
* @package io.shinhanlife.dap.sample.domain.model
* @className AppliSystNtfyPatiModel
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class AppliSystNtfyPatiModel extends AuditInfo {
private String appliSystNtfyPatiId;
private String appliSystNtfyKdCd;
private String appliDutjCd;
private String appliDutjPrjcCd;
private String ntfyMsgCt;
private String ntfyOccDt;
private String ntfyCfmDt;
private String ntfyTrgtPrafNo;
private String shmsPmlYn;
private String ntfyCfmYn;
}

View File

@@ -0,0 +1,34 @@
package io.shinhanlife.dap.sample.domain.repository;
import io.shinhanlife.dap.sample.domain.model.AppliSystNtfyPatiModel;
import io.shinhanlife.dap.sample.dto.StrnTermListInDTO;
import io.shinhanlife.dap.sample.dto.StrnTermListOutDTO;
import io.shinhanlife.glow.GlowIndexPaging;
import io.shinhanlife.glow.GlowMybatisMapper;
import io.shinhanlife.glow.PageInfo;
import java.util.List;
/**
* @package io.shinhanlife.dap.sample.domain.repository
* @className GlowSampleRepository
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
@GlowMybatisMapper
public interface GlowSampleRepository {
@GlowIndexPaging
List<StrnTermListOutDTO.StrnTerm> selectStrnTerm(StrnTermListInDTO inDto, PageInfo pageInfo);
int insertAppliSystNtfyPati(AppliSystNtfyPatiModel appliSystNtfyPatiModel);
}

View File

@@ -0,0 +1,25 @@
package io.shinhanlife.dap.sample.domain.service;
import io.shinhanlife.dap.sample.dto.AppliSystNtfyRgiInDTO;
import io.shinhanlife.dap.sample.dto.StrnTermListInDTO;
import io.shinhanlife.dap.sample.dto.StrnTermListOutDTO;
import io.shinhanlife.glow.PageInfo;
/**
* @package io.shinhanlife.dap.sample.domain.service
* @className GlowSampleService
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
public interface GlowSampleService {
StrnTermListOutDTO getStrnTerms(StrnTermListInDTO inDto, PageInfo pageInfo);
int insertAppliSystNtfyPati(AppliSystNtfyRgiInDTO inDto);
}

View File

@@ -0,0 +1,55 @@
package io.shinhanlife.dap.sample.domain.service.impl;
import io.shinhanlife.dap.sample.converter.SampleConverter;
import io.shinhanlife.dap.sample.domain.model.AppliSystNtfyPatiModel;
import io.shinhanlife.dap.sample.domain.repository.GlowSampleRepository;
import io.shinhanlife.dap.sample.domain.service.GlowSampleService;
import io.shinhanlife.dap.sample.dto.AppliSystNtfyRgiInDTO;
import io.shinhanlife.dap.sample.dto.StrnTermListInDTO;
import io.shinhanlife.dap.sample.dto.StrnTermListOutDTO;
import io.shinhanlife.glow.GlowLogger;
import io.shinhanlife.glow.GlowLogTarget;
import io.shinhanlife.glow.PageInfo;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* @package io.shinhanlife.dap.sample.domain.service.impl
* @className GlowSampleServiceImpl
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
@Service
@RequiredArgsConstructor
public class GlowSampleServiceImpl implements GlowSampleService {
@GlowLogTarget(GlowLogTarget.Target.FILE)
private final GlowLogger log;
private final GlowSampleRepository glowSampleRepository;
private final SampleConverter sampleConverter;
@Override
public StrnTermListOutDTO getStrnTerms(StrnTermListInDTO inDto, PageInfo pageInfo) {
List<StrnTermListOutDTO.StrnTerm> out = glowSampleRepository.selectStrnTerm(inDto, pageInfo);
return StrnTermListOutDTO.builder()
.strnTerms(out)
.pageInfo(pageInfo)
.build();
}
@Override
public int insertAppliSystNtfyPati(AppliSystNtfyRgiInDTO inDto) {
AppliSystNtfyPatiModel appliSystNtfyPatiModel = sampleConverter.convertDtoToModel(inDto);
return glowSampleRepository.insertAppliSystNtfyPati(appliSystNtfyPatiModel);
}
}

View File

@@ -0,0 +1,41 @@
package io.shinhanlife.dap.sample.dto;
import lombok.Getter;
import lombok.Setter;
import lombok.Builder;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import lombok.*;
/**
* @package io.shinhanlife.dap.sample.dto
* @className AppliSystNtfyRgiInDTO
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class AppliSystNtfyRgiInDTO {
private String appliSystNtfyPatiId;
private String appliSystNtfyKdCd;
private String appliDutjCd;
private String appliDutjPrjcCd;
private String ntfyMsgCt;
private String ntfyOccDt;
private String ntfyCfmDt;
private String ntfyTrgtPrafNo;
private String shmsPmlYn;
private String ntfyCfmYn;
}

View File

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

View File

@@ -0,0 +1,46 @@
package io.shinhanlife.dap.sample.dto;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import io.shinhanlife.glow.PageInfo;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
import java.util.List;
/**
* @package io.shinhanlife.dap.sample.dto
* @className StrnTermListOutDTO
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
@Builder
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class StrnTermListOutDTO {
private List<StrnTerm> strnTerms;
private PageInfo pageInfo;
@Builder
@Getter
@Setter
public static class StrnTerm{
private String strnTermHanNm;
private String strnTermEngcAbrNm;
private String strnTermEngNm;
private String strnTermScrnTermNm;
}
}

View File

@@ -0,0 +1,52 @@
package io.shinhanlife.dap.sample.presentation;
import io.shinhanlife.dap.sample.presentation.io.AppliSystNtfyPatiRequest;
import io.shinhanlife.dap.sample.presentation.io.AppliSystNtfyPatiResponse;
import io.shinhanlife.dap.sample.presentation.io.StrnTermRequest;
import io.shinhanlife.dap.sample.presentation.io.StrnTermResponse;
import io.shinhanlife.dap.sample.usecase.GlowSampleUseCase;
import io.shinhanlife.glow.*;
import lombok.RequiredArgsConstructor;
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.RestController;
/**
* @package io.shinhanlife.dap.sample.presentation
* @className GlowSampleController
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
@RestController
@RequiredArgsConstructor
public class GlowSampleController {
@GlowLogTarget({GlowLogTarget.Target.CONSOLE, GlowLogTarget.Target.FILE})
private final GlowLogger log;
private final GlowSampleUseCase glowSampleUseCase;
@GlowControllerId(value = "selectStrnTerm")
@PostMapping("/selectStrnTerm")
public ResponseEntity<BaseResponse<StrnTermResponse>> getStrnTerms(@RequestBody StrnTermRequest request) {
return ResponseUtil.ok(glowSampleUseCase.getStrnTerms(request));
}
@GlowControllerId(value = "insertAp")
@PostMapping("/insertAppliSystNtfyPati")
public ResponseEntity<BaseResponse<AppliSystNtfyPatiResponse>> insertAppliSystNtfyPati(@RequestBody AppliSystNtfyPatiRequest request) {
return ResponseUtil.ok(glowSampleUseCase.insertAppliSystNtfyPati(request));
}
}

View File

@@ -0,0 +1,34 @@
package io.shinhanlife.dap.sample.presentation.io;
import lombok.*;
/**
* @package io.shinhanlife.dap.sample.presentation.io
* @className AppliSystNtfyPatiRequest
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
@Getter
@Builder
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
public class AppliSystNtfyPatiRequest {
private String appliSystNtfyPatiId;
private String appliSystNtfyKdCd;
private String appliDutjCd;
private String appliDutjPrjcCd;
private String ntfyMsgCt;
private String ntfyOccDt;
private String ntfyCfmDt;
private String ntfyTrgtPrafNo;
private String shmsPmlYn;
private String ntfyCfmYn;
}

View File

@@ -0,0 +1,26 @@
package io.shinhanlife.dap.sample.presentation.io;
import lombok.*;
/**
* @package io.shinhanlife.dap.sample.presentation.io
* @className AppliSystNtfyPatiResponse
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
@Getter
@Builder
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
public class AppliSystNtfyPatiResponse {
private String successYn;
private String msg;
}

View File

@@ -0,0 +1,28 @@
package io.shinhanlife.dap.sample.presentation.io;
import io.shinhanlife.glow.PageInfo;
import lombok.*;
/**
* @package io.shinhanlife.dap.sample.presentation.io
* @className StrnTermRequest
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
@Getter
@Builder
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
public class StrnTermRequest {
private PageInfo pageInfo;
private String strnTermHanNm;
}

View File

@@ -0,0 +1,41 @@
package io.shinhanlife.dap.sample.presentation.io;
import io.shinhanlife.glow.PageInfo;
import lombok.*;
import java.util.List;
/**
* @package io.shinhanlife.dap.sample.presentation.io
* @className StrnTermResponse
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
@Getter
@Builder
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
public class StrnTermResponse {
private List<StrnTerm> strnTerms;
private PageInfo pageInfo;
@Builder
@Getter
@NoArgsConstructor
@AllArgsConstructor
public static class StrnTerm{
private String strnTermHanNm;
private String strnTermEngcAbrNm;
private String strnTermEngNm;
private String strnTermScrnTermNm;
}
}

View File

@@ -0,0 +1,25 @@
package io.shinhanlife.dap.sample.usecase;
import io.shinhanlife.dap.sample.presentation.io.AppliSystNtfyPatiRequest;
import io.shinhanlife.dap.sample.presentation.io.AppliSystNtfyPatiResponse;
import io.shinhanlife.dap.sample.presentation.io.StrnTermRequest;
import io.shinhanlife.dap.sample.presentation.io.StrnTermResponse;
/**
* @package io.shinhanlife.dap.sample.usecase
* @className GlowSampleUseCase
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
public interface GlowSampleUseCase {
StrnTermResponse getStrnTerms(StrnTermRequest req);
AppliSystNtfyPatiResponse insertAppliSystNtfyPati(AppliSystNtfyPatiRequest req);
}

View File

@@ -0,0 +1,57 @@
package io.shinhanlife.dap.sample.usecase.impl;
import io.shinhanlife.dap.sample.converter.SampleConverter;
import io.shinhanlife.dap.sample.domain.service.GlowSampleService;
import io.shinhanlife.dap.sample.dto.StrnTermListOutDTO;
import io.shinhanlife.dap.sample.presentation.io.AppliSystNtfyPatiRequest;
import io.shinhanlife.dap.sample.presentation.io.AppliSystNtfyPatiResponse;
import io.shinhanlife.dap.sample.presentation.io.StrnTermRequest;
import io.shinhanlife.dap.sample.presentation.io.StrnTermResponse;
import io.shinhanlife.dap.sample.usecase.GlowSampleUseCase;
import io.shinhanlife.glow.GlowAppServiceId;
import io.shinhanlife.glow.GlowLogTarget;
import io.shinhanlife.glow.GlowLogger;
import io.shinhanlife.glow.GlowServiceGroupId;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
/**
* @package io.shinhanlife.dap.sample.usecase.impl
* @className GlowSampleUseCaseImpl
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
@Service
@RequiredArgsConstructor
@GlowServiceGroupId(value = "GlowSample", description="GlowSampleUseCase")
public class GlowSampleUseCaseImpl implements GlowSampleUseCase {
@GlowLogTarget(GlowLogTarget.Target.FILE)
private final GlowLogger log;
private final GlowSampleService glowSampleService;
private final SampleConverter converter;
@GlowAppServiceId(value = "SAMPLE-DB-0001", description = "표준용어조회(DB)")
public StrnTermResponse getStrnTerms(StrnTermRequest req) {
StrnTermListOutDTO outDto = glowSampleService.getStrnTerms(converter.convertRequestToDto(req), req.getPageInfo());
return converter.convertDtoToResponse(outDto);
}
@GlowAppServiceId(value = "SAMPLE-DB-0002", description = "어플리케이션시스템알림내역등록")
public AppliSystNtfyPatiResponse insertAppliSystNtfyPati(AppliSystNtfyPatiRequest req) {
int rtn = glowSampleService.insertAppliSystNtfyPati(converter.convertAppliRequestToDto(req));
return AppliSystNtfyPatiResponse.builder()
.successYn(rtn > 0 ? "Y" : "N")
.msg(rtn > 0 ? "성공적으로 처리 되었습니다." : "처리중 오류발생!!")
.build();
}
}

View File

@@ -0,0 +1,160 @@
-- ============================================================
-- AX HUB 역할-Tool/지식 권한 관리 DDL
-- 대상 스키마 : 프로젝트 AP 스키마 (S_EIAM_TIS 는 읽기전용 참조)
-- 작성일 : 2026-06-25
-- ============================================================
-- ① Tool 마스터
CREATE TABLE AX_TOOL_MST (
TOOL_ID VARCHAR(20) NOT NULL, -- Tool ID (PK)
TOOL_NM VARCHAR(100) NOT NULL, -- Tool명
TOOL_DS VARCHAR(500), -- Tool설명
TOOL_TYPE_CD VARCHAR(20), -- Tool유형코드 (MCP / FUNC / EXT)
PUSE_YN VARCHAR(1) DEFAULT 'Y',-- 사용여부
SYST_RGI_DT DATE,
SYST_RGI_PRAF_NO VARCHAR(8),
SYST_RGI_OGNZ_NO VARCHAR(7),
SYST_RGI_SYST_CD VARCHAR(3),
SYST_RGI_PRGR_ID VARCHAR(100),
SYST_CHG_DT DATE,
SYST_CHG_PRAF_NO VARCHAR(8),
SYST_CHG_OGNZ_NO VARCHAR(7),
SYST_CHG_SYST_CD VARCHAR(3),
SYST_CHG_PRGR_ID VARCHAR(100),
CONSTRAINT PK_AX_TOOL_MST PRIMARY KEY (TOOL_ID)
);
COMMENT ON TABLE AX_TOOL_MST IS 'AX HUB Tool 마스터';
COMMENT ON COLUMN AX_TOOL_MST.TOOL_ID IS 'Tool ID (PK)';
COMMENT ON COLUMN AX_TOOL_MST.TOOL_NM IS 'Tool명';
COMMENT ON COLUMN AX_TOOL_MST.TOOL_DS IS 'Tool설명';
COMMENT ON COLUMN AX_TOOL_MST.TOOL_TYPE_CD IS 'Tool유형코드 (MCP/FUNC/EXT)';
COMMENT ON COLUMN AX_TOOL_MST.PUSE_YN IS '사용여부 (Y/N)';
-- ② 지식(Knowledge) 마스터
CREATE TABLE AX_KNWL_MST (
KNWL_ID VARCHAR(20) NOT NULL, -- 지식 ID (PK)
KNWL_NM VARCHAR(100) NOT NULL, -- 지식명
KNWL_DS VARCHAR(500), -- 지식설명
KNWL_TYPE_CD VARCHAR(20), -- 지식유형코드 (PRODUCT/MANUAL/LEGAL 등)
PUSE_YN VARCHAR(1) DEFAULT 'Y',
SYST_RGI_DT DATE,
SYST_RGI_PRAF_NO VARCHAR(8),
SYST_RGI_OGNZ_NO VARCHAR(7),
SYST_RGI_SYST_CD VARCHAR(3),
SYST_RGI_PRGR_ID VARCHAR(100),
SYST_CHG_DT DATE,
SYST_CHG_PRAF_NO VARCHAR(8),
SYST_CHG_OGNZ_NO VARCHAR(7),
SYST_CHG_SYST_CD VARCHAR(3),
SYST_CHG_PRGR_ID VARCHAR(100),
CONSTRAINT PK_AX_KNWL_MST PRIMARY KEY (KNWL_ID)
);
COMMENT ON TABLE AX_KNWL_MST IS 'AX HUB 지식 마스터';
COMMENT ON COLUMN AX_KNWL_MST.KNWL_ID IS '지식 ID (PK)';
COMMENT ON COLUMN AX_KNWL_MST.KNWL_NM IS '지식명';
COMMENT ON COLUMN AX_KNWL_MST.KNWL_DS IS '지식설명';
COMMENT ON COLUMN AX_KNWL_MST.KNWL_TYPE_CD IS '지식유형코드 (PRODUCT/MANUAL/LEGAL/REPORT)';
COMMENT ON COLUMN AX_KNWL_MST.PUSE_YN IS '사용여부 (Y/N)';
-- ③ 역할-Tool 권한 매핑
-- SYST_ID + ROLE_NO → S_EIAM_TIS.AA_ROLE 참조 (FK 미설정: 스키마 분리 환경)
CREATE TABLE AX_ROLE_TOOL_ATHR (
ROLE_TOOL_ATHR_ID VARCHAR(20) NOT NULL, -- 역할Tool권한ID (PK)
SYST_ID VARCHAR(10) NOT NULL, -- 시스템ID (EIAM 연동 키)
ROLE_NO VARCHAR(50) NOT NULL, -- 역할번호 (EIAM AA_ROLE.ROLE_NO)
TOOL_ID VARCHAR(20) NOT NULL, -- Tool ID
PUSE_YN VARCHAR(1) DEFAULT 'Y',
SYST_RGI_DT DATE,
SYST_RGI_PRAF_NO VARCHAR(8),
SYST_RGI_OGNZ_NO VARCHAR(7),
SYST_RGI_SYST_CD VARCHAR(3),
SYST_RGI_PRGR_ID VARCHAR(100),
SYST_CHG_DT DATE,
SYST_CHG_PRAF_NO VARCHAR(8),
SYST_CHG_OGNZ_NO VARCHAR(7),
SYST_CHG_SYST_CD VARCHAR(3),
SYST_CHG_PRGR_ID VARCHAR(100),
CONSTRAINT PK_AX_ROLE_TOOL_ATHR PRIMARY KEY (ROLE_TOOL_ATHR_ID)
);
-- (SYST_ID, ROLE_NO, TOOL_ID) 중복 방지
CREATE UNIQUE INDEX UK_AX_ROLE_TOOL_ATHR
ON AX_ROLE_TOOL_ATHR (SYST_ID, ROLE_NO, TOOL_ID);
COMMENT ON TABLE AX_ROLE_TOOL_ATHR IS '역할-Tool 권한 매핑';
COMMENT ON COLUMN AX_ROLE_TOOL_ATHR.ROLE_TOOL_ATHR_ID IS '역할Tool권한ID (PK)';
COMMENT ON COLUMN AX_ROLE_TOOL_ATHR.SYST_ID IS '시스템ID';
COMMENT ON COLUMN AX_ROLE_TOOL_ATHR.ROLE_NO IS '역할번호 (EIAM)';
COMMENT ON COLUMN AX_ROLE_TOOL_ATHR.TOOL_ID IS 'Tool ID';
COMMENT ON COLUMN AX_ROLE_TOOL_ATHR.PUSE_YN IS '사용여부';
-- ④ 역할-지식 권한 매핑
CREATE TABLE AX_ROLE_KNWL_ATHR (
ROLE_KNWL_ATHR_ID VARCHAR(20) NOT NULL, -- 역할지식권한ID (PK)
SYST_ID VARCHAR(10) NOT NULL,
ROLE_NO VARCHAR(50) NOT NULL,
KNWL_ID VARCHAR(20) NOT NULL,
PUSE_YN VARCHAR(1) DEFAULT 'Y',
SYST_RGI_DT DATE,
SYST_RGI_PRAF_NO VARCHAR(8),
SYST_RGI_OGNZ_NO VARCHAR(7),
SYST_RGI_SYST_CD VARCHAR(3),
SYST_RGI_PRGR_ID VARCHAR(100),
SYST_CHG_DT DATE,
SYST_CHG_PRAF_NO VARCHAR(8),
SYST_CHG_OGNZ_NO VARCHAR(7),
SYST_CHG_SYST_CD VARCHAR(3),
SYST_CHG_PRGR_ID VARCHAR(100),
CONSTRAINT PK_AX_ROLE_KNWL_ATHR PRIMARY KEY (ROLE_KNWL_ATHR_ID)
);
CREATE UNIQUE INDEX UK_AX_ROLE_KNWL_ATHR
ON AX_ROLE_KNWL_ATHR (SYST_ID, ROLE_NO, KNWL_ID);
COMMENT ON TABLE AX_ROLE_KNWL_ATHR IS '역할-지식 권한 매핑';
COMMENT ON COLUMN AX_ROLE_KNWL_ATHR.ROLE_KNWL_ATHR_ID IS '역할지식권한ID (PK)';
COMMENT ON COLUMN AX_ROLE_KNWL_ATHR.SYST_ID IS '시스템ID';
COMMENT ON COLUMN AX_ROLE_KNWL_ATHR.ROLE_NO IS '역할번호 (EIAM)';
COMMENT ON COLUMN AX_ROLE_KNWL_ATHR.KNWL_ID IS '지식ID';
COMMENT ON COLUMN AX_ROLE_KNWL_ATHR.PUSE_YN IS '사용여부';
-- ============================================================
-- 샘플 데이터 (개발용)
-- ============================================================
INSERT INTO AX_TOOL_MST
(TOOL_ID, TOOL_NM, TOOL_DS, TOOL_TYPE_CD, PUSE_YN,
SYST_RGI_DT, SYST_RGI_PRAF_NO, SYST_RGI_OGNZ_NO, SYST_RGI_SYST_CD, SYST_RGI_PRGR_ID,
SYST_CHG_DT, SYST_CHG_PRAF_NO, SYST_CHG_OGNZ_NO, SYST_CHG_SYST_CD, SYST_CHG_PRGR_ID)
VALUES
('TOOL_A', '지식 검색', '지식베이스 내 문서 의미 기반 검색', 'MCP', 'Y',
CURRENT_DATE, 'SYSTEM', 'SYSTEM', 'AXH', 'SOATM0100',
CURRENT_DATE, 'SYSTEM', 'SYSTEM', 'AXH', 'SOATM0100'),
('TOOL_B', 'DB 조회', '사내 데이터베이스 직접 조회 (GLOW)', 'MCP', 'Y',
CURRENT_DATE, 'SYSTEM', 'SYSTEM', 'AXH', 'SOATM0100',
CURRENT_DATE, 'SYSTEM', 'SYSTEM', 'AXH', 'SOATM0100'),
('TOOL_C', '문서 생성', 'AI 기반 문서 초안 자동 생성', 'FUNC', 'Y',
CURRENT_DATE, 'SYSTEM', 'SYSTEM', 'AXH', 'SOATM0100',
CURRENT_DATE, 'SYSTEM', 'SYSTEM', 'AXH', 'SOATM0100');
INSERT INTO AX_KNWL_MST
(KNWL_ID, KNWL_NM, KNWL_DS, KNWL_TYPE_CD, PUSE_YN,
SYST_RGI_DT, SYST_RGI_PRAF_NO, SYST_RGI_OGNZ_NO, SYST_RGI_SYST_CD, SYST_RGI_PRGR_ID,
SYST_CHG_DT, SYST_CHG_PRAF_NO, SYST_CHG_OGNZ_NO, SYST_CHG_SYST_CD, SYST_CHG_PRGR_ID)
VALUES
('KNWL_Q', '상품기초서류', '보험 상품 기초서류 약 8만건', 'PRODUCT', 'Y',
CURRENT_DATE, 'SYSTEM', 'SYSTEM', 'AXH', 'SOATM0100',
CURRENT_DATE, 'SYSTEM', 'SYSTEM', 'AXH', 'SOATM0100'),
('KNWL_W', '업무매뉴얼', '내부 업무 매뉴얼 및 프로세스 가이드', 'MANUAL', 'Y',
CURRENT_DATE, 'SYSTEM', 'SYSTEM', 'AXH', 'SOATM0100',
CURRENT_DATE, 'SYSTEM', 'SYSTEM', 'AXH', 'SOATM0100'),
('KNWL_E', '법률/규정', '보험업법 및 금융당국 규정', 'LEGAL', 'Y',
CURRENT_DATE, 'SYSTEM', 'SYSTEM', 'AXH', 'SOATM0100',
CURRENT_DATE, 'SYSTEM', 'SYSTEM', 'AXH', 'SOATM0100');
COMMIT;

View File

@@ -0,0 +1,19 @@
# Dev 환경 전용 설정 (개발 서버 DB 연결 정보 등)
# spring.datasource.url=jdbc:oracle:thin:@//dev-db-host:1521/XEPDB1
# spring.datasource.driverClassName=oracle.jdbc.OracleDriver
# spring.datasource.username=dev_user
# spring.datasource.password=dev_password
# Render 클라우드 환경 전용 Fallback 라우팅
mcp.gateway.fallback.routes.sms=https://dap-tool-sms.onrender.com
mcp.gateway.fallback.routes.email=https://dap-tool-email.onrender.com
mcp.gateway.fallback.default-url=https://dap-tool-other.onrender.com
mcp.gateway.fallback.routes.payment=https://dap-tool-payment.onrender.com
mcp.gateway.fallback.routes.hr=https://dap-tool-hr.onrender.com
# --- 신한라이프 EAI/MCI 연계 IP 정보 (개발 환경) ---
shinhan.integration.envrTypeCd=D
shinhan.integration.eai.url=http://10.176.32.181
shinhan.integration.internalMci.url=http://10.176.32.173
shinhan.integration.bancaMci.url=http://10.176.32.117
shinhan.integration.externalMci.url=http://10.176.32.176

View File

@@ -0,0 +1,27 @@
# 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
mcp.adapter.url=http://localhost:${server.port}/rpc/v1/execute
mcp.gateway.url=http://localhost:${server.port}/mcp/api/v1
mcp.tool.host=localhost
# Disable Kafka
spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.kafka.KafkaAutoConfiguration
# Suppress Kafka Connection Logs
logging.level.org.apache.kafka=ERROR

View File

@@ -0,0 +1,12 @@
# Prod 환경 전용 설정 (운영 서버 DB 연결 정보 등)
# spring.datasource.url=jdbc:oracle:thin:@//prod-db-host:1521/PROD
# spring.datasource.driverClassName=oracle.jdbc.OracleDriver
# spring.datasource.username=prod_user
# spring.datasource.password=prod_password
# --- 신한라이프 EAI/MCI 연계 IP 정보 (운영 환경) ---
shinhan.integration.envrTypeCd=R
shinhan.integration.eai.url=http://10.172.32.181
shinhan.integration.internalMci.url=http://10.172.32.173
shinhan.integration.bancaMci.url=http://10.172.32.117
shinhan.integration.externalMci.url=http://10.172.32.177

View File

@@ -0,0 +1,51 @@
spring.application.name=dap-admin
# Gateway Fallback Routing (For unregistered tools)
mcp.gateway.fallback.routes.sms=http://tool-sms:8082
mcp.gateway.fallback.routes.email=http://tool-email:8083
mcp.gateway.fallback.default-url=http://tool-other:8084
mcp.gateway.fallback.routes.payment=http://tool-payment:8085
mcp.gateway.fallback.routes.hr=http://tool-hr:8086
server.port=8081
server.http2.enabled=true
# 기본적으로 local 환경을 실행
spring.profiles.active=local
# MyBatis 공통 설정
mybatis.mapper-locations=classpath:mapper/**/*.xml
mybatis.type-aliases-package=
mybatis.configuration.map-underscore-to-camel-case=true
spring.sql.init.mode=always
logging.level.com.corundumstudio.socketio=DEBUG
# 우아한 종료 (Graceful Shutdown) 설정
server.shutdown=graceful
spring.lifecycle.timeout-per-shutdown-phase=20s
# Suppress Kafka Connection Logs
logging.level.org.apache.kafka=ERROR
# Spring AI MCP Server Settings
# spring.ai.mcp.server.protocol=STATELESS
mcp.agent-claims-required=false
mcp.trusted-claims-required=false
mcp.write-approval-required=false
mcp.default-page-size=100
mcp.max-page-size=500
mcp.max-pages-per-call=3
mcp.max-stream-bytes=1048576
mcp.max-response-bytes-from-tool=5242880
mcp.tool-registry-sync-interval-millis=5000
# API 보안 키 설정
mcp.security.tenant-domains.TESTER-DEV=ALL
# Spring AI OpenAI / Gemini API Config
spring.ai.openai.api-key=AQ.Ab8RN6KFZggsQf8iooY1v_3h3vp2TIjiYB54dV4Yay3vVKEMtg
spring.ai.openai.base-url=https://generativelanguage.googleapis.com/v1beta/openai/
spring.ai.openai.chat.options.model=gemini-flash-latest
spring.ai.openai.chat.options.temperature=0.3

View File

@@ -0,0 +1,258 @@
SET SCHEMA S_TIS;
-- INSERT 문 재정렬: 부모부터 삽입 (루트 -> 자식 순)
-- 루트 메뉴 먼저 (11: 샘플 화면, 10: AX HUB)
INSERT INTO ZT_MENU (MENU_ID, PARENT_MENU_ID, NAME, PATH, TITLE, ICON, SHOW_BADGE, SHOW_TEXT_BADGE, IS_HIDE, IS_HIDE_TAB, LINK, IS_IFRAME, KEEP_ALIVE, ORDER_SEQ, IS_ACTIVE, SYST_RGI_DT, SYST_RGI_PRAF_NO, SYST_RGI_OGNZ_NO, SYST_RGI_SYST_CD, SYST_RGI_PRGR_ID, SYST_CHG_DT, SYST_CHG_PRAF_NO, SYST_CHG_OGNZ_NO, SYST_CHG_SYST_CD, SYST_CHG_PRGR_ID) VALUES (11, NULL, 'Sample', '/sample', '샘플 화면', '&#xe8d2;', 'N', NULL, 'N', 'N', NULL, 'N', 'N', 0, 'Y', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
INSERT INTO ZT_MENU (MENU_ID, PARENT_MENU_ID, NAME, PATH, TITLE, ICON, SHOW_BADGE, SHOW_TEXT_BADGE, IS_HIDE, IS_HIDE_TAB, LINK, IS_IFRAME, KEEP_ALIVE, ORDER_SEQ, IS_ACTIVE, SYST_RGI_DT, SYST_RGI_PRAF_NO, SYST_RGI_OGNZ_NO, SYST_RGI_SYST_CD, SYST_RGI_PRGR_ID, SYST_CHG_DT, SYST_CHG_PRAF_NO, SYST_CHG_OGNZ_NO, SYST_CHG_SYST_CD, SYST_CHG_PRGR_ID) VALUES (10, NULL, 'AxHub', '/axhub', 'AX HUB', '&#xe7b9;', 'N', NULL, 'N', 'N', NULL, 'N', 'N', 1, 'Y', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
-- 샘플 화면(11번) 자식 메뉴
INSERT INTO ZT_MENU (MENU_ID, PARENT_MENU_ID, NAME, PATH, TITLE, ICON, SHOW_BADGE, SHOW_TEXT_BADGE, IS_HIDE, IS_HIDE_TAB, LINK, IS_IFRAME, KEEP_ALIVE, ORDER_SEQ, IS_ACTIVE, SYST_RGI_DT, SYST_RGI_PRAF_NO, SYST_RGI_OGNZ_NO, SYST_RGI_SYST_CD, SYST_RGI_PRGR_ID, SYST_CHG_DT, SYST_CHG_PRAF_NO, SYST_CHG_OGNZ_NO, SYST_CHG_SYST_CD, SYST_CHG_PRGR_ID) VALUES (1, 11, 'Dashboard', '/dashboard', '대시보드', '&#xe721;', 'N', NULL, 'N', 'N', NULL, 'N', 'N', 0, 'Y', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
INSERT INTO ZT_MENU (MENU_ID, PARENT_MENU_ID, NAME, PATH, TITLE, ICON, SHOW_BADGE, SHOW_TEXT_BADGE, IS_HIDE, IS_HIDE_TAB, LINK, IS_IFRAME, KEEP_ALIVE, ORDER_SEQ, IS_ACTIVE, SYST_RGI_DT, SYST_RGI_PRAF_NO, SYST_RGI_OGNZ_NO, SYST_RGI_SYST_CD, SYST_RGI_PRGR_ID, SYST_CHG_DT, SYST_CHG_PRAF_NO, SYST_CHG_OGNZ_NO, SYST_CHG_SYST_CD, SYST_CHG_PRGR_ID) VALUES (5, 11, 'Widgets', '/widgets', '컴포넌트 모음', '&#xe81a;', 'N', NULL, 'N', 'N', NULL, 'N', 'N', 1, 'Y', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
INSERT INTO ZT_MENU (MENU_ID, PARENT_MENU_ID, NAME, PATH, TITLE, ICON, SHOW_BADGE, SHOW_TEXT_BADGE, IS_HIDE, IS_HIDE_TAB, LINK, IS_IFRAME, KEEP_ALIVE, ORDER_SEQ, IS_ACTIVE, SYST_RGI_DT, SYST_RGI_PRAF_NO, SYST_RGI_OGNZ_NO, SYST_RGI_SYST_CD, SYST_RGI_PRGR_ID, SYST_CHG_DT, SYST_CHG_PRAF_NO, SYST_CHG_OGNZ_NO, SYST_CHG_SYST_CD, SYST_CHG_PRGR_ID) VALUES (126, 11, 'Template', '/template', '템플릿 센터', '&#xe860;', 'N', NULL, 'N', 'N', NULL, 'N', 'N', 2, 'Y', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
INSERT INTO ZT_MENU (MENU_ID, PARENT_MENU_ID, NAME, PATH, TITLE, ICON, SHOW_BADGE, SHOW_TEXT_BADGE, IS_HIDE, IS_HIDE_TAB, LINK, IS_IFRAME, KEEP_ALIVE, ORDER_SEQ, IS_ACTIVE, SYST_RGI_DT, SYST_RGI_PRAF_NO, SYST_RGI_OGNZ_NO, SYST_RGI_SYST_CD, SYST_RGI_PRGR_ID, SYST_CHG_DT, SYST_CHG_PRAF_NO, SYST_CHG_OGNZ_NO, SYST_CHG_SYST_CD, SYST_CHG_PRGR_ID) VALUES (4, 11, 'Article', '/article', '기사 관리', '&#xe7ae;', 'N', NULL, 'N', 'N', NULL, 'N', 'N', 3, 'Y', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
INSERT INTO ZT_MENU (MENU_ID, PARENT_MENU_ID, NAME, PATH, TITLE, ICON, SHOW_BADGE, SHOW_TEXT_BADGE, IS_HIDE, IS_HIDE_TAB, LINK, IS_IFRAME, KEEP_ALIVE, ORDER_SEQ, IS_ACTIVE, SYST_RGI_DT, SYST_RGI_PRAF_NO, SYST_RGI_OGNZ_NO, SYST_RGI_SYST_CD, SYST_RGI_PRGR_ID, SYST_CHG_DT, SYST_CHG_PRAF_NO, SYST_CHG_OGNZ_NO, SYST_CHG_SYST_CD, SYST_CHG_PRGR_ID) VALUES (2, 11, 'User', '/user', '사용자 관리', '&#xe86e;', 'N', NULL, 'N', 'N', NULL, 'N', 'N', 4, 'Y', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
INSERT INTO ZT_MENU (MENU_ID, PARENT_MENU_ID, NAME, PATH, TITLE, ICON, SHOW_BADGE, SHOW_TEXT_BADGE, IS_HIDE, IS_HIDE_TAB, LINK, IS_IFRAME, KEEP_ALIVE, ORDER_SEQ, IS_ACTIVE, SYST_RGI_DT, SYST_RGI_PRAF_NO, SYST_RGI_OGNZ_NO, SYST_RGI_SYST_CD, SYST_RGI_PRGR_ID, SYST_CHG_DT, SYST_CHG_PRAF_NO, SYST_CHG_OGNZ_NO, SYST_CHG_SYST_CD, SYST_CHG_PRGR_ID) VALUES (3, 11, 'Menu', '/menu', '메뉴 관리', '&#xe8a4;', 'N', NULL, 'N', 'N', NULL, 'N', 'N', 5, 'Y', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
INSERT INTO ZT_MENU (MENU_ID, PARENT_MENU_ID, NAME, PATH, TITLE, ICON, SHOW_BADGE, SHOW_TEXT_BADGE, IS_HIDE, IS_HIDE_TAB, LINK, IS_IFRAME, KEEP_ALIVE, ORDER_SEQ, IS_ACTIVE, SYST_RGI_DT, SYST_RGI_PRAF_NO, SYST_RGI_OGNZ_NO, SYST_RGI_SYST_CD, SYST_RGI_PRGR_ID, SYST_CHG_DT, SYST_CHG_PRAF_NO, SYST_CHG_OGNZ_NO, SYST_CHG_SYST_CD, SYST_CHG_PRGR_ID) VALUES (18, 11, 'Result', '/result', '결과 페이지', '&#xe715;', 'N', NULL, 'N', 'N', NULL, 'N', 'N', 6, 'Y', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
INSERT INTO ZT_MENU (MENU_ID, PARENT_MENU_ID, NAME, PATH, TITLE, ICON, SHOW_BADGE, SHOW_TEXT_BADGE, IS_HIDE, IS_HIDE_TAB, LINK, IS_IFRAME, KEEP_ALIVE, ORDER_SEQ, IS_ACTIVE, SYST_RGI_DT, SYST_RGI_PRAF_NO, SYST_RGI_OGNZ_NO, SYST_RGI_SYST_CD, SYST_RGI_PRGR_ID, SYST_CHG_DT, SYST_CHG_PRAF_NO, SYST_CHG_OGNZ_NO, SYST_CHG_SYST_CD, SYST_CHG_PRGR_ID) VALUES (8, 11, 'Exception', '/exception', '예외 페이지', '&#xe820;', 'N', NULL, 'N', 'N', NULL, 'N', 'N', 7, 'Y', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
INSERT INTO ZT_MENU (MENU_ID, PARENT_MENU_ID, NAME, PATH, TITLE, ICON, SHOW_BADGE, SHOW_TEXT_BADGE, IS_HIDE, IS_HIDE_TAB, LINK, IS_IFRAME, KEEP_ALIVE, ORDER_SEQ, IS_ACTIVE, SYST_RGI_DT, SYST_RGI_PRAF_NO, SYST_RGI_OGNZ_NO, SYST_RGI_SYST_CD, SYST_RGI_PRGR_ID, SYST_CHG_DT, SYST_CHG_PRAF_NO, SYST_CHG_OGNZ_NO, SYST_CHG_SYST_CD, SYST_CHG_PRGR_ID) VALUES (9, 11, 'System', '/system', '시스템 설정', '&#xe7b9;', 'N', NULL, 'N', 'N', NULL, 'N', 'N', 8, 'Y', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
-- 1번 자식
INSERT INTO ZT_MENU (MENU_ID, PARENT_MENU_ID, NAME, PATH, TITLE, ICON, SHOW_BADGE, SHOW_TEXT_BADGE, IS_HIDE, IS_HIDE_TAB, LINK, IS_IFRAME, KEEP_ALIVE, ORDER_SEQ, IS_ACTIVE, SYST_RGI_DT, SYST_RGI_PRAF_NO, SYST_RGI_OGNZ_NO, SYST_RGI_SYST_CD, SYST_RGI_PRGR_ID, SYST_CHG_DT, SYST_CHG_PRAF_NO, SYST_CHG_OGNZ_NO, SYST_CHG_SYST_CD, SYST_CHG_PRGR_ID) VALUES (101, 1, NULL, '/dashboard/console', '콘솔', NULL, 'N', NULL, 'N', 'N', NULL, 'N', 'Y', 0, 'Y', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
INSERT INTO ZT_MENU (MENU_ID, PARENT_MENU_ID, NAME, PATH, TITLE, ICON, SHOW_BADGE, SHOW_TEXT_BADGE, IS_HIDE, IS_HIDE_TAB, LINK, IS_IFRAME, KEEP_ALIVE, ORDER_SEQ, IS_ACTIVE, SYST_RGI_DT, SYST_RGI_PRAF_NO, SYST_RGI_OGNZ_NO, SYST_RGI_SYST_CD, SYST_RGI_PRGR_ID, SYST_CHG_DT, SYST_CHG_PRAF_NO, SYST_CHG_OGNZ_NO, SYST_CHG_SYST_CD, SYST_CHG_PRGR_ID) VALUES (102, 1, NULL, '/dashboard/analysis', '분석 페이지', NULL, 'N', NULL, 'N', 'N', NULL, 'N', 'Y', 1, 'Y', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
INSERT INTO ZT_MENU (MENU_ID, PARENT_MENU_ID, NAME, PATH, TITLE, ICON, SHOW_BADGE, SHOW_TEXT_BADGE, IS_HIDE, IS_HIDE_TAB, LINK, IS_IFRAME, KEEP_ALIVE, ORDER_SEQ, IS_ACTIVE, SYST_RGI_DT, SYST_RGI_PRAF_NO, SYST_RGI_OGNZ_NO, SYST_RGI_SYST_CD, SYST_RGI_PRGR_ID, SYST_CHG_DT, SYST_CHG_PRAF_NO, SYST_CHG_OGNZ_NO, SYST_CHG_SYST_CD, SYST_CHG_PRGR_ID) VALUES (103, 1, NULL, '/dashboard/kanban', '칸반보드', NULL, 'N', NULL, 'N', 'N', NULL, 'N', 'Y', 2, 'Y', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
-- 5번 자식
INSERT INTO ZT_MENU (MENU_ID, PARENT_MENU_ID, NAME, PATH, TITLE, ICON, SHOW_BADGE, SHOW_TEXT_BADGE, IS_HIDE, IS_HIDE_TAB, LINK, IS_IFRAME, KEEP_ALIVE, ORDER_SEQ, IS_ACTIVE, SYST_RGI_DT, SYST_RGI_PRAF_NO, SYST_RGI_OGNZ_NO, SYST_RGI_SYST_CD, SYST_RGI_PRGR_ID, SYST_CHG_DT, SYST_CHG_PRAF_NO, SYST_CHG_OGNZ_NO, SYST_CHG_SYST_CD, SYST_CHG_PRGR_ID) VALUES (503, 5, NULL, '/widgets/icon-list', '아이콘 목록', NULL, 'N', NULL, 'N', 'N', NULL, 'N', 'Y', 0, 'Y', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
INSERT INTO ZT_MENU (MENU_ID, PARENT_MENU_ID, NAME, PATH, TITLE, ICON, SHOW_BADGE, SHOW_TEXT_BADGE, IS_HIDE, IS_HIDE_TAB, LINK, IS_IFRAME, KEEP_ALIVE, ORDER_SEQ, IS_ACTIVE, SYST_RGI_DT, SYST_RGI_PRAF_NO, SYST_RGI_OGNZ_NO, SYST_RGI_SYST_CD, SYST_RGI_PRGR_ID, SYST_CHG_DT, SYST_CHG_PRAF_NO, SYST_CHG_OGNZ_NO, SYST_CHG_SYST_CD, SYST_CHG_PRGR_ID) VALUES (504, 5, NULL, '/widgets/icon-selector', '아이콘 선택기', NULL, 'N', NULL, 'N', 'N', NULL, 'N', 'Y', 1, 'Y', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
INSERT INTO ZT_MENU (MENU_ID, PARENT_MENU_ID, NAME, PATH, TITLE, ICON, SHOW_BADGE, SHOW_TEXT_BADGE, IS_HIDE, IS_HIDE_TAB, LINK, IS_IFRAME, KEEP_ALIVE, ORDER_SEQ, IS_ACTIVE, SYST_RGI_DT, SYST_RGI_PRAF_NO, SYST_RGI_OGNZ_NO, SYST_RGI_SYST_CD, SYST_RGI_PRGR_ID, SYST_CHG_DT, SYST_CHG_PRAF_NO, SYST_CHG_OGNZ_NO, SYST_CHG_SYST_CD, SYST_CHG_PRGR_ID) VALUES (506, 5, NULL, '/widgets/excel', 'Excel 가져오기 내보내기', NULL, 'N', NULL, 'N', 'N', NULL, 'N', 'Y', 2, 'Y', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
INSERT INTO ZT_MENU (MENU_ID, PARENT_MENU_ID, NAME, PATH, TITLE, ICON, SHOW_BADGE, SHOW_TEXT_BADGE, IS_HIDE, IS_HIDE_TAB, LINK, IS_IFRAME, KEEP_ALIVE, ORDER_SEQ, IS_ACTIVE, SYST_RGI_DT, SYST_RGI_PRAF_NO, SYST_RGI_OGNZ_NO, SYST_RGI_SYST_CD, SYST_RGI_PRGR_ID, SYST_CHG_DT, SYST_CHG_PRAF_NO, SYST_CHG_OGNZ_NO, SYST_CHG_SYST_CD, SYST_CHG_PRGR_ID) VALUES (508, 5, NULL, '/widgets/count-to', '숫자 롤링', NULL, 'N', NULL, 'N', 'N', NULL, 'N', 'N', 3, 'Y', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
INSERT INTO ZT_MENU (MENU_ID, PARENT_MENU_ID, NAME, PATH, TITLE, ICON, SHOW_BADGE, SHOW_TEXT_BADGE, IS_HIDE, IS_HIDE_TAB, LINK, IS_IFRAME, KEEP_ALIVE, ORDER_SEQ, IS_ACTIVE, SYST_RGI_DT, SYST_RGI_PRAF_NO, SYST_RGI_OGNZ_NO, SYST_RGI_SYST_CD, SYST_RGI_PRGR_ID, SYST_CHG_DT, SYST_CHG_PRAF_NO, SYST_CHG_OGNZ_NO, SYST_CHG_SYST_CD, SYST_CHG_PRGR_ID) VALUES (509, 5, NULL, '/widgets/toastUI', 'ToastUI 에디터', NULL, 'N', NULL, 'N', 'N', NULL, 'N', 'Y', 4, 'Y', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
INSERT INTO ZT_MENU (MENU_ID, PARENT_MENU_ID, NAME, PATH, TITLE, ICON, SHOW_BADGE, SHOW_TEXT_BADGE, IS_HIDE, IS_HIDE_TAB, LINK, IS_IFRAME, KEEP_ALIVE, ORDER_SEQ, IS_ACTIVE, SYST_RGI_DT, SYST_RGI_PRAF_NO, SYST_RGI_OGNZ_NO, SYST_RGI_SYST_CD, SYST_RGI_PRGR_ID, SYST_CHG_DT, SYST_CHG_PRAF_NO, SYST_CHG_OGNZ_NO, SYST_CHG_SYST_CD, SYST_CHG_PRGR_ID) VALUES (511, 5, NULL, '/widgets/context-menu', '오른쪽 클릭 메뉴', NULL, 'N', NULL, 'N', 'N', NULL, 'N', 'Y', 5, 'Y', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
INSERT INTO ZT_MENU (MENU_ID, PARENT_MENU_ID, NAME, PATH, TITLE, ICON, SHOW_BADGE, SHOW_TEXT_BADGE, IS_HIDE, IS_HIDE_TAB, LINK, IS_IFRAME, KEEP_ALIVE, ORDER_SEQ, IS_ACTIVE, SYST_RGI_DT, SYST_RGI_PRAF_NO, SYST_RGI_OGNZ_NO, SYST_RGI_SYST_CD, SYST_RGI_PRGR_ID, SYST_CHG_DT, SYST_CHG_PRAF_NO, SYST_CHG_OGNZ_NO, SYST_CHG_SYST_CD, SYST_CHG_PRGR_ID) VALUES (513, 5, NULL, '/widgets/drag', '드래그', NULL, 'N', NULL, 'N', 'N', NULL, 'N', 'Y', 6, 'Y', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
-- 126번 자식
INSERT INTO ZT_MENU (MENU_ID, PARENT_MENU_ID, NAME, PATH, TITLE, ICON, SHOW_BADGE, SHOW_TEXT_BADGE, IS_HIDE, IS_HIDE_TAB, LINK, IS_IFRAME, KEEP_ALIVE, ORDER_SEQ, IS_ACTIVE, SYST_RGI_DT, SYST_RGI_PRAF_NO, SYST_RGI_OGNZ_NO, SYST_RGI_SYST_CD, SYST_RGI_PRGR_ID, SYST_CHG_DT, SYST_CHG_PRAF_NO, SYST_CHG_OGNZ_NO, SYST_CHG_SYST_CD, SYST_CHG_PRGR_ID) VALUES (12601, 126, NULL, '/template/chat', '채팅', NULL, 'N', NULL, 'N', 'N', NULL, 'N', 'Y', 0, 'Y', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
INSERT INTO ZT_MENU (MENU_ID, PARENT_MENU_ID, NAME, PATH, TITLE, ICON, SHOW_BADGE, SHOW_TEXT_BADGE, IS_HIDE, IS_HIDE_TAB, LINK, IS_IFRAME, KEEP_ALIVE, ORDER_SEQ, IS_ACTIVE, SYST_RGI_DT, SYST_RGI_PRAF_NO, SYST_RGI_OGNZ_NO, SYST_RGI_SYST_CD, SYST_RGI_PRGR_ID, SYST_CHG_DT, SYST_CHG_PRAF_NO, SYST_CHG_OGNZ_NO, SYST_CHG_SYST_CD, SYST_CHG_PRGR_ID) VALUES (12602, 126, NULL, '/template/cards', '카드', NULL, 'N', NULL, 'N', 'N', NULL, 'N', 'N', 1, 'Y', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
INSERT INTO ZT_MENU (MENU_ID, PARENT_MENU_ID, NAME, PATH, TITLE, ICON, SHOW_BADGE, SHOW_TEXT_BADGE, IS_HIDE, IS_HIDE_TAB, LINK, IS_IFRAME, KEEP_ALIVE, ORDER_SEQ, IS_ACTIVE, SYST_RGI_DT, SYST_RGI_PRAF_NO, SYST_RGI_OGNZ_NO, SYST_RGI_SYST_CD, SYST_RGI_PRGR_ID, SYST_CHG_DT, SYST_CHG_PRAF_NO, SYST_CHG_OGNZ_NO, SYST_CHG_SYST_CD, SYST_CHG_PRGR_ID) VALUES (12603, 126, NULL, '/template/banners', '배너', NULL, 'N', NULL, 'N', 'N', NULL, 'N', 'N', 2, 'Y', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
INSERT INTO ZT_MENU (MENU_ID, PARENT_MENU_ID, NAME, PATH, TITLE, ICON, SHOW_BADGE, SHOW_TEXT_BADGE, IS_HIDE, IS_HIDE_TAB, LINK, IS_IFRAME, KEEP_ALIVE, ORDER_SEQ, IS_ACTIVE, SYST_RGI_DT, SYST_RGI_PRAF_NO, SYST_RGI_OGNZ_NO, SYST_RGI_SYST_CD, SYST_RGI_PRGR_ID, SYST_CHG_DT, SYST_CHG_PRAF_NO, SYST_CHG_OGNZ_NO, SYST_CHG_SYST_CD, SYST_CHG_PRGR_ID) VALUES (12604, 126, NULL, '/template/charts', '차트', NULL, 'N', NULL, 'N', 'N', NULL, 'N', 'N', 3, 'Y', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
INSERT INTO ZT_MENU (MENU_ID, PARENT_MENU_ID, NAME, PATH, TITLE, ICON, SHOW_BADGE, SHOW_TEXT_BADGE, IS_HIDE, IS_HIDE_TAB, LINK, IS_IFRAME, KEEP_ALIVE, ORDER_SEQ, IS_ACTIVE, SYST_RGI_DT, SYST_RGI_PRAF_NO, SYST_RGI_OGNZ_NO, SYST_RGI_SYST_CD, SYST_RGI_PRGR_ID, SYST_CHG_DT, SYST_CHG_PRAF_NO, SYST_CHG_OGNZ_NO, SYST_CHG_SYST_CD, SYST_CHG_PRGR_ID) VALUES (12605, 126, NULL, '/template/calendar', '캘린더', NULL, 'N', NULL, 'N', 'N', NULL, 'N', 'Y', 4, 'Y', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
-- 4번 자식
INSERT INTO ZT_MENU (MENU_ID, PARENT_MENU_ID, NAME, PATH, TITLE, ICON, SHOW_BADGE, SHOW_TEXT_BADGE, IS_HIDE, IS_HIDE_TAB, LINK, IS_IFRAME, KEEP_ALIVE, ORDER_SEQ, IS_ACTIVE, SYST_RGI_DT, SYST_RGI_PRAF_NO, SYST_RGI_OGNZ_NO, SYST_RGI_SYST_CD, SYST_RGI_PRGR_ID, SYST_CHG_DT, SYST_CHG_PRAF_NO, SYST_CHG_OGNZ_NO, SYST_CHG_SYST_CD, SYST_CHG_PRGR_ID) VALUES (202, 4, NULL, '/article/article-list', '기사 목록', NULL, 'N', NULL, 'N', 'N', NULL, 'N', 'Y', 0, 'Y', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
INSERT INTO ZT_MENU (MENU_ID, PARENT_MENU_ID, NAME, PATH, TITLE, ICON, SHOW_BADGE, SHOW_TEXT_BADGE, IS_HIDE, IS_HIDE_TAB, LINK, IS_IFRAME, KEEP_ALIVE, ORDER_SEQ, IS_ACTIVE, SYST_RGI_DT, SYST_RGI_PRAF_NO, SYST_RGI_OGNZ_NO, SYST_RGI_SYST_CD, SYST_RGI_PRGR_ID, SYST_CHG_DT, SYST_CHG_PRAF_NO, SYST_CHG_OGNZ_NO, SYST_CHG_SYST_CD, SYST_CHG_PRGR_ID) VALUES (204, 4, NULL, '/article/detail', '기사 상세', NULL, 'N', NULL, 'Y', 'N', NULL, 'N', 'Y', 1, 'Y', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
INSERT INTO ZT_MENU (MENU_ID, PARENT_MENU_ID, NAME, PATH, TITLE, ICON, SHOW_BADGE, SHOW_TEXT_BADGE, IS_HIDE, IS_HIDE_TAB, LINK, IS_IFRAME, KEEP_ALIVE, ORDER_SEQ, IS_ACTIVE, SYST_RGI_DT, SYST_RGI_PRAF_NO, SYST_RGI_OGNZ_NO, SYST_RGI_SYST_CD, SYST_RGI_PRGR_ID, SYST_CHG_DT, SYST_CHG_PRAF_NO, SYST_CHG_OGNZ_NO, SYST_CHG_SYST_CD, SYST_CHG_PRGR_ID) VALUES (205, 4, NULL, '/article/comment', '댓글 관리', NULL, 'N', NULL, 'N', 'N', NULL, 'N', 'Y', 2, 'Y', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
INSERT INTO ZT_MENU (MENU_ID, PARENT_MENU_ID, NAME, PATH, TITLE, ICON, SHOW_BADGE, SHOW_TEXT_BADGE, IS_HIDE, IS_HIDE_TAB, LINK, IS_IFRAME, KEEP_ALIVE, ORDER_SEQ, IS_ACTIVE, SYST_RGI_DT, SYST_RGI_PRAF_NO, SYST_RGI_OGNZ_NO, SYST_RGI_SYST_CD, SYST_RGI_PRGR_ID, SYST_CHG_DT, SYST_CHG_PRAF_NO, SYST_CHG_OGNZ_NO, SYST_CHG_SYST_CD, SYST_CHG_PRGR_ID) VALUES (201, 4, NULL, '/article/article-publish', '기사 게시', NULL, 'N', NULL, 'N', 'N', NULL, 'N', 'Y', 3, 'Y', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
-- 2번 자식
INSERT INTO ZT_MENU (MENU_ID, PARENT_MENU_ID, NAME, PATH, TITLE, ICON, SHOW_BADGE, SHOW_TEXT_BADGE, IS_HIDE, IS_HIDE_TAB, LINK, IS_IFRAME, KEEP_ALIVE, ORDER_SEQ, IS_ACTIVE, SYST_RGI_DT, SYST_RGI_PRAF_NO, SYST_RGI_OGNZ_NO, SYST_RGI_SYST_CD, SYST_RGI_PRGR_ID, SYST_CHG_DT, SYST_CHG_PRAF_NO, SYST_CHG_OGNZ_NO, SYST_CHG_SYST_CD, SYST_CHG_PRGR_ID) VALUES (301, 2, NULL, '/user/account', '계정 관리', NULL, 'N', NULL, 'N', 'N', NULL, 'N', 'Y', 0, 'Y', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
INSERT INTO ZT_MENU (MENU_ID, PARENT_MENU_ID, NAME, PATH, TITLE, ICON, SHOW_BADGE, SHOW_TEXT_BADGE, IS_HIDE, IS_HIDE_TAB, LINK, IS_IFRAME, KEEP_ALIVE, ORDER_SEQ, IS_ACTIVE, SYST_RGI_DT, SYST_RGI_PRAF_NO, SYST_RGI_OGNZ_NO, SYST_RGI_SYST_CD, SYST_RGI_PRGR_ID, SYST_CHG_DT, SYST_CHG_PRAF_NO, SYST_CHG_OGNZ_NO, SYST_CHG_SYST_CD, SYST_CHG_PRGR_ID) VALUES (302, 2, NULL, '/user/department', '부서 관리', NULL, 'N', NULL, 'N', 'N', NULL, 'N', 'Y', 1, 'Y', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
INSERT INTO ZT_MENU (MENU_ID, PARENT_MENU_ID, NAME, PATH, TITLE, ICON, SHOW_BADGE, SHOW_TEXT_BADGE, IS_HIDE, IS_HIDE_TAB, LINK, IS_IFRAME, KEEP_ALIVE, ORDER_SEQ, IS_ACTIVE, SYST_RGI_DT, SYST_RGI_PRAF_NO, SYST_RGI_OGNZ_NO, SYST_RGI_SYST_CD, SYST_RGI_PRGR_ID, SYST_CHG_DT, SYST_CHG_PRAF_NO, SYST_CHG_OGNZ_NO, SYST_CHG_SYST_CD, SYST_CHG_PRGR_ID) VALUES (303, 2, NULL, '/user/role', '역할 권한', NULL, 'N', NULL, 'N', 'N', NULL, 'N', 'Y', 2, 'Y', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
INSERT INTO ZT_MENU (MENU_ID, PARENT_MENU_ID, NAME, PATH, TITLE, ICON, SHOW_BADGE, SHOW_TEXT_BADGE, IS_HIDE, IS_HIDE_TAB, LINK, IS_IFRAME, KEEP_ALIVE, ORDER_SEQ, IS_ACTIVE, SYST_RGI_DT, SYST_RGI_PRAF_NO, SYST_RGI_OGNZ_NO, SYST_RGI_SYST_CD, SYST_RGI_PRGR_ID, SYST_CHG_DT, SYST_CHG_PRAF_NO, SYST_CHG_OGNZ_NO, SYST_CHG_SYST_CD, SYST_CHG_PRGR_ID) VALUES (304, 2, NULL, '/user/user', '마이페이지', NULL, 'N', NULL, 'Y', 'Y', NULL, 'N', 'Y', 3, 'Y', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
-- 3번 자식 (중첩 메뉴 부모 ID 402로 수정)
INSERT INTO ZT_MENU (MENU_ID, PARENT_MENU_ID, NAME, PATH, TITLE, ICON, SHOW_BADGE, SHOW_TEXT_BADGE, IS_HIDE, IS_HIDE_TAB, LINK, IS_IFRAME, KEEP_ALIVE, ORDER_SEQ, IS_ACTIVE, SYST_RGI_DT, SYST_RGI_PRAF_NO, SYST_RGI_OGNZ_NO, SYST_RGI_SYST_CD, SYST_RGI_PRGR_ID, SYST_CHG_DT, SYST_CHG_PRAF_NO, SYST_CHG_OGNZ_NO, SYST_CHG_SYST_CD, SYST_CHG_PRGR_ID) VALUES (401, 3, NULL, '/menu/menu', '메뉴 권한', '&#xe8a4;', 'N', NULL, 'N', 'N', NULL, 'N', 'Y', 0, 'Y', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
INSERT INTO ZT_MENU (MENU_ID, PARENT_MENU_ID, NAME, PATH, TITLE, ICON, SHOW_BADGE, SHOW_TEXT_BADGE, IS_HIDE, IS_HIDE_TAB, LINK, IS_IFRAME, KEEP_ALIVE, ORDER_SEQ, IS_ACTIVE, SYST_RGI_DT, SYST_RGI_PRAF_NO, SYST_RGI_OGNZ_NO, SYST_RGI_SYST_CD, SYST_RGI_PRGR_ID, SYST_CHG_DT, SYST_CHG_PRAF_NO, SYST_CHG_OGNZ_NO, SYST_CHG_SYST_CD, SYST_CHG_PRGR_ID) VALUES (411, 3, NULL, '/menu/permission', '권한 제어', '&#xe831;', 'N', 'new', 'N', 'N', NULL, 'N', 'Y', 1, 'Y', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
INSERT INTO ZT_MENU (MENU_ID, PARENT_MENU_ID, NAME, PATH, TITLE, ICON, SHOW_BADGE, SHOW_TEXT_BADGE, IS_HIDE, IS_HIDE_TAB, LINK, IS_IFRAME, KEEP_ALIVE, ORDER_SEQ, IS_ACTIVE, SYST_RGI_DT, SYST_RGI_PRAF_NO, SYST_RGI_OGNZ_NO, SYST_RGI_SYST_CD, SYST_RGI_PRGR_ID, SYST_CHG_DT, SYST_CHG_PRAF_NO, SYST_CHG_OGNZ_NO, SYST_CHG_SYST_CD, SYST_CHG_PRGR_ID) VALUES (402, 3, NULL, '/menu/nested', '중첩 메뉴', '&#xe676;', 'N', NULL, 'N', 'N', NULL, 'N', 'Y', 2, 'Y', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
-- 402번 자식 (중첩 메뉴)
INSERT INTO ZT_MENU (MENU_ID, PARENT_MENU_ID, NAME, PATH, TITLE, ICON, SHOW_BADGE, SHOW_TEXT_BADGE, IS_HIDE, IS_HIDE_TAB, LINK, IS_IFRAME, KEEP_ALIVE, ORDER_SEQ, IS_ACTIVE, SYST_RGI_DT, SYST_RGI_PRAF_NO, SYST_RGI_OGNZ_NO, SYST_RGI_SYST_CD, SYST_RGI_PRGR_ID, SYST_CHG_DT, SYST_CHG_PRAF_NO, SYST_CHG_OGNZ_NO, SYST_CHG_SYST_CD, SYST_CHG_PRGR_ID) VALUES (40201, 402, NULL, '/menu/nested/menu1', '메뉴1', '&#xe676;', 'N', NULL, 'N', 'N', NULL, 'N', 'Y', 0, 'Y', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
INSERT INTO ZT_MENU (MENU_ID, PARENT_MENU_ID, NAME, PATH, TITLE, ICON, SHOW_BADGE, SHOW_TEXT_BADGE, IS_HIDE, IS_HIDE_TAB, LINK, IS_IFRAME, KEEP_ALIVE, ORDER_SEQ, IS_ACTIVE, SYST_RGI_DT, SYST_RGI_PRAF_NO, SYST_RGI_OGNZ_NO, SYST_RGI_SYST_CD, SYST_RGI_PRGR_ID, SYST_CHG_DT, SYST_CHG_PRAF_NO, SYST_CHG_OGNZ_NO, SYST_CHG_SYST_CD, SYST_CHG_PRGR_ID) VALUES (40202, 402, NULL, '/menu/nested/menu2', '메뉴2', '&#xe676;', 'N', NULL, 'N', 'N', NULL, 'N', 'Y', 1, 'Y', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
INSERT INTO ZT_MENU (MENU_ID, PARENT_MENU_ID, NAME, PATH, TITLE, ICON, SHOW_BADGE, SHOW_TEXT_BADGE, IS_HIDE, IS_HIDE_TAB, LINK, IS_IFRAME, KEEP_ALIVE, ORDER_SEQ, IS_ACTIVE, SYST_RGI_DT, SYST_RGI_PRAF_NO, SYST_RGI_OGNZ_NO, SYST_RGI_SYST_CD, SYST_RGI_PRGR_ID, SYST_CHG_DT, SYST_CHG_PRAF_NO, SYST_CHG_OGNZ_NO, SYST_CHG_SYST_CD, SYST_CHG_PRGR_ID) VALUES (40203, 402, NULL, '/menu/nested/menu3', '메뉴3', '&#xe676;', 'N', NULL, 'N', 'N', NULL, 'N', 'Y', 2, 'Y', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
-- 40202 자식
INSERT INTO ZT_MENU (MENU_ID, PARENT_MENU_ID, NAME, PATH, TITLE, ICON, SHOW_BADGE, SHOW_TEXT_BADGE, IS_HIDE, IS_HIDE_TAB, LINK, IS_IFRAME, KEEP_ALIVE, ORDER_SEQ, IS_ACTIVE, SYST_RGI_DT, SYST_RGI_PRAF_NO, SYST_RGI_OGNZ_NO, SYST_RGI_SYST_CD, SYST_RGI_PRGR_ID, SYST_CHG_DT, SYST_CHG_PRAF_NO, SYST_CHG_OGNZ_NO, SYST_CHG_SYST_CD, SYST_CHG_PRGR_ID) VALUES (4020201, 40202, NULL, '/menu/nested/menu2/menu2-1', '메뉴2-1', '&#xe676;', 'N', NULL, 'N', 'N', NULL, 'N', 'Y', 0, 'Y', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
-- 40203 자식
INSERT INTO ZT_MENU (MENU_ID, PARENT_MENU_ID, NAME, PATH, TITLE, ICON, SHOW_BADGE, SHOW_TEXT_BADGE, IS_HIDE, IS_HIDE_TAB, LINK, IS_IFRAME, KEEP_ALIVE, ORDER_SEQ, IS_ACTIVE, SYST_RGI_DT, SYST_RGI_PRAF_NO, SYST_RGI_OGNZ_NO, SYST_RGI_SYST_CD, SYST_RGI_PRGR_ID, SYST_CHG_DT, SYST_CHG_PRAF_NO, SYST_CHG_OGNZ_NO, SYST_CHG_SYST_CD, SYST_CHG_PRGR_ID) VALUES (4020301, 40203, NULL, '/menu/nested/menu3/menu3-1', '메뉴3-1', '&#xe676;', 'N', NULL, 'N', 'N', NULL, 'N', 'Y', 0, 'Y', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
INSERT INTO ZT_MENU (MENU_ID, PARENT_MENU_ID, NAME, PATH, TITLE, ICON, SHOW_BADGE, SHOW_TEXT_BADGE, IS_HIDE, IS_HIDE_TAB, LINK, IS_IFRAME, KEEP_ALIVE, ORDER_SEQ, IS_ACTIVE, SYST_RGI_DT, SYST_RGI_PRAF_NO, SYST_RGI_OGNZ_NO, SYST_RGI_SYST_CD, SYST_RGI_PRGR_ID, SYST_CHG_DT, SYST_CHG_PRAF_NO, SYST_CHG_OGNZ_NO, SYST_CHG_SYST_CD, SYST_CHG_PRGR_ID) VALUES (4020302, 40203, NULL, '/menu/nested/menu3/menu3-2', '메뉴3-2', '&#xe676;', 'N', NULL, 'N', 'N', NULL, 'N', 'Y', 1, 'Y', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
-- 4020302 자식
INSERT INTO ZT_MENU (MENU_ID, PARENT_MENU_ID, NAME, PATH, TITLE, ICON, SHOW_BADGE, SHOW_TEXT_BADGE, IS_HIDE, IS_HIDE_TAB, LINK, IS_IFRAME, KEEP_ALIVE, ORDER_SEQ, IS_ACTIVE, SYST_RGI_DT, SYST_RGI_PRAF_NO, SYST_RGI_OGNZ_NO, SYST_RGI_SYST_CD, SYST_RGI_PRGR_ID, SYST_CHG_DT, SYST_CHG_PRAF_NO, SYST_CHG_OGNZ_NO, SYST_CHG_SYST_CD, SYST_CHG_PRGR_ID) VALUES (402030201, 4020302, NULL, '/menu/nested/menu3/menu3-2/menu3-2-1', '메뉴3-2-1', '&#xe676;', 'N', NULL, 'N', 'N', NULL, 'N', 'Y', 0, 'Y', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
-- 18번 자식
INSERT INTO ZT_MENU (MENU_ID, PARENT_MENU_ID, NAME, PATH, TITLE, ICON, SHOW_BADGE, SHOW_TEXT_BADGE, IS_HIDE, IS_HIDE_TAB, LINK, IS_IFRAME, KEEP_ALIVE, ORDER_SEQ, IS_ACTIVE, SYST_RGI_DT, SYST_RGI_PRAF_NO, SYST_RGI_OGNZ_NO, SYST_RGI_SYST_CD, SYST_RGI_PRGR_ID, SYST_CHG_DT, SYST_CHG_PRAF_NO, SYST_CHG_OGNZ_NO, SYST_CHG_SYST_CD, SYST_CHG_PRGR_ID) VALUES (405, 18, NULL, '/result/success', '성공 페이지', NULL, 'N', NULL, 'N', 'N', NULL, 'N', 'Y', 0, 'Y', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
INSERT INTO ZT_MENU (MENU_ID, PARENT_MENU_ID, NAME, PATH, TITLE, ICON, SHOW_BADGE, SHOW_TEXT_BADGE, IS_HIDE, IS_HIDE_TAB, LINK, IS_IFRAME, KEEP_ALIVE, ORDER_SEQ, IS_ACTIVE, SYST_RGI_DT, SYST_RGI_PRAF_NO, SYST_RGI_OGNZ_NO, SYST_RGI_SYST_CD, SYST_RGI_PRGR_ID, SYST_CHG_DT, SYST_CHG_PRAF_NO, SYST_CHG_OGNZ_NO, SYST_CHG_SYST_CD, SYST_CHG_PRGR_ID) VALUES (404, 18, NULL, '/result/fail', '실패 페이지', NULL, 'N', NULL, 'N', 'N', NULL, 'N', 'Y', 1, 'Y', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
-- 8번 자식
INSERT INTO ZT_MENU (MENU_ID, PARENT_MENU_ID, NAME, PATH, TITLE, ICON, SHOW_BADGE, SHOW_TEXT_BADGE, IS_HIDE, IS_HIDE_TAB, LINK, IS_IFRAME, KEEP_ALIVE, ORDER_SEQ, IS_ACTIVE, SYST_RGI_DT, SYST_RGI_PRAF_NO, SYST_RGI_OGNZ_NO, SYST_RGI_SYST_CD, SYST_RGI_PRGR_ID, SYST_CHG_DT, SYST_CHG_PRAF_NO, SYST_CHG_OGNZ_NO, SYST_CHG_SYST_CD, SYST_CHG_PRGR_ID) VALUES (801, 8, NULL, '/exception/403', '403', NULL, 'N', NULL, 'N', 'N', NULL, 'N', 'Y', 0, 'Y', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
INSERT INTO ZT_MENU (MENU_ID, PARENT_MENU_ID, NAME, PATH, TITLE, ICON, SHOW_BADGE, SHOW_TEXT_BADGE, IS_HIDE, IS_HIDE_TAB, LINK, IS_IFRAME, KEEP_ALIVE, ORDER_SEQ, IS_ACTIVE, SYST_RGI_DT, SYST_RGI_PRAF_NO, SYST_RGI_OGNZ_NO, SYST_RGI_SYST_CD, SYST_RGI_PRGR_ID, SYST_CHG_DT, SYST_CHG_PRAF_NO, SYST_CHG_OGNZ_NO, SYST_CHG_SYST_CD, SYST_CHG_PRGR_ID) VALUES (802, 8, NULL, '/exception/404', '404', NULL, 'N', NULL, 'N', 'N', NULL, 'N', 'Y', 1, 'Y', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
INSERT INTO ZT_MENU (MENU_ID, PARENT_MENU_ID, NAME, PATH, TITLE, ICON, SHOW_BADGE, SHOW_TEXT_BADGE, IS_HIDE, IS_HIDE_TAB, LINK, IS_IFRAME, KEEP_ALIVE, ORDER_SEQ, IS_ACTIVE, SYST_RGI_DT, SYST_RGI_PRAF_NO, SYST_RGI_OGNZ_NO, SYST_RGI_SYST_CD, SYST_RGI_PRGR_ID, SYST_CHG_DT, SYST_CHG_PRAF_NO, SYST_CHG_OGNZ_NO, SYST_CHG_SYST_CD, SYST_CHG_PRGR_ID) VALUES (803, 8, NULL, '/exception/500', '500', NULL, 'N', NULL, 'N', 'N', NULL, 'N', 'Y', 2, 'Y', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
-- 9번 자식
INSERT INTO ZT_MENU (MENU_ID, PARENT_MENU_ID, NAME, PATH, TITLE, ICON, SHOW_BADGE, SHOW_TEXT_BADGE, IS_HIDE, IS_HIDE_TAB, LINK, IS_IFRAME, KEEP_ALIVE, ORDER_SEQ, IS_ACTIVE, SYST_RGI_DT, SYST_RGI_PRAF_NO, SYST_RGI_OGNZ_NO, SYST_RGI_SYST_CD, SYST_RGI_PRGR_ID, SYST_CHG_DT, SYST_CHG_PRAF_NO, SYST_CHG_OGNZ_NO, SYST_CHG_SYST_CD, SYST_CHG_PRGR_ID) VALUES (901, 9, NULL, '/system/setting', '시스템 설정', NULL, 'N', NULL, 'N', 'N', NULL, 'N', 'Y', 0, 'Y', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
INSERT INTO ZT_MENU (MENU_ID, PARENT_MENU_ID, NAME, PATH, TITLE, ICON, SHOW_BADGE, SHOW_TEXT_BADGE, IS_HIDE, IS_HIDE_TAB, LINK, IS_IFRAME, KEEP_ALIVE, ORDER_SEQ, IS_ACTIVE, SYST_RGI_DT, SYST_RGI_PRAF_NO, SYST_RGI_OGNZ_NO, SYST_RGI_SYST_CD, SYST_RGI_PRGR_ID, SYST_CHG_DT, SYST_CHG_PRAF_NO, SYST_CHG_OGNZ_NO, SYST_CHG_SYST_CD, SYST_CHG_PRGR_ID) VALUES (902, 9, NULL, '/system/api', 'API 관리', NULL, 'N', NULL, 'N', 'N', NULL, 'N', 'Y', 1, 'Y', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
INSERT INTO ZT_MENU (MENU_ID, PARENT_MENU_ID, NAME, PATH, TITLE, ICON, SHOW_BADGE, SHOW_TEXT_BADGE, IS_HIDE, IS_HIDE_TAB, LINK, IS_IFRAME, KEEP_ALIVE, ORDER_SEQ, IS_ACTIVE, SYST_RGI_DT, SYST_RGI_PRAF_NO, SYST_RGI_OGNZ_NO, SYST_RGI_SYST_CD, SYST_RGI_PRGR_ID, SYST_CHG_DT, SYST_CHG_PRAF_NO, SYST_CHG_OGNZ_NO, SYST_CHG_SYST_CD, SYST_CHG_PRGR_ID) VALUES (903, 9, NULL, '/system/log', '시스템 로그', NULL, 'N', NULL, 'N', 'N', NULL, 'N', 'Y', 2, 'Y', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
-- 10번 자식
INSERT INTO ZT_MENU (MENU_ID, PARENT_MENU_ID, NAME, PATH, TITLE, ICON, SHOW_BADGE, SHOW_TEXT_BADGE, IS_HIDE, IS_HIDE_TAB, LINK, IS_IFRAME, KEEP_ALIVE, ORDER_SEQ, IS_ACTIVE, SYST_RGI_DT, SYST_RGI_PRAF_NO, SYST_RGI_OGNZ_NO, SYST_RGI_SYST_CD, SYST_RGI_PRGR_ID, SYST_CHG_DT, SYST_CHG_PRAF_NO, SYST_CHG_OGNZ_NO, SYST_CHG_SYST_CD, SYST_CHG_PRGR_ID) VALUES (1001, 10, NULL, '/axhub/auth', '권한설정', NULL, 'N', NULL, 'N', 'N', NULL, 'N', 'Y', 0, 'Y', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
INSERT INTO ZT_MENU (MENU_ID, PARENT_MENU_ID, NAME, PATH, TITLE, ICON, SHOW_BADGE, SHOW_TEXT_BADGE, IS_HIDE, IS_HIDE_TAB, LINK, IS_IFRAME, KEEP_ALIVE, ORDER_SEQ, IS_ACTIVE, SYST_RGI_DT, SYST_RGI_PRAF_NO, SYST_RGI_OGNZ_NO, SYST_RGI_SYST_CD, SYST_RGI_PRGR_ID, SYST_CHG_DT, SYST_CHG_PRAF_NO, SYST_CHG_OGNZ_NO, SYST_CHG_SYST_CD, SYST_CHG_PRGR_ID) VALUES (1002, 10, 'MenuMgmt', '/axhub/menu-mgmt', '메뉴 관리', NULL, 'N', NULL, 'N', 'N', NULL, 'N', 'Y', 1, 'Y', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
-- ============================================================
-- 샘플 데이터 (개발용)
-- ============================================================
INSERT INTO AX_TOOL_MST
(TOOL_ID, TOOL_NM, TOOL_DS, TOOL_TYPE_CD, PUSE_YN,
SYST_RGI_DT, SYST_RGI_PRAF_NO, SYST_RGI_OGNZ_NO, SYST_RGI_SYST_CD, SYST_RGI_PRGR_ID,
SYST_CHG_DT, SYST_CHG_PRAF_NO, SYST_CHG_OGNZ_NO, SYST_CHG_SYST_CD, SYST_CHG_PRGR_ID)
VALUES
('TOOL_A', '지식 검색', '지식베이스 내 문서 의미 기반 검색', 'MCP', 'Y',
CURRENT_DATE, 'SYSTEM', 'SYSTEM', 'AXH', 'SOATM0100',
CURRENT_DATE, 'SYSTEM', 'SYSTEM', 'AXH', 'SOATM0100'),
('TOOL_B', 'DB 조회', '사내 데이터베이스 직접 조회 (GLOW)', 'MCP', 'Y',
CURRENT_DATE, 'SYSTEM', 'SYSTEM', 'AXH', 'SOATM0100',
CURRENT_DATE, 'SYSTEM', 'SYSTEM', 'AXH', 'SOATM0100'),
('TOOL_C', '문서 생성', 'AI 기반 문서 초안 자동 생성', 'FUNC', 'Y',
CURRENT_DATE, 'SYSTEM', 'SYSTEM', 'AXH', 'SOATM0100',
CURRENT_DATE, 'SYSTEM', 'SYSTEM', 'AXH', 'SOATM0100');
INSERT INTO AX_KNWL_MST
(KNWL_ID, KNWL_NM, KNWL_DS, KNWL_TYPE_CD, PUSE_YN,
SYST_RGI_DT, SYST_RGI_PRAF_NO, SYST_RGI_OGNZ_NO, SYST_RGI_SYST_CD, SYST_RGI_PRGR_ID,
SYST_CHG_DT, SYST_CHG_PRAF_NO, SYST_CHG_OGNZ_NO, SYST_CHG_SYST_CD, SYST_CHG_PRGR_ID)
VALUES
('KNWL_Q', '상품기초서류', '보험 상품 기초서류 약 8만건', 'PRODUCT', 'Y',
CURRENT_DATE, 'SYSTEM', 'SYSTEM', 'AXH', 'SOATM0100',
CURRENT_DATE, 'SYSTEM', 'SYSTEM', 'AXH', 'SOATM0100'),
('KNWL_W', '업무매뉴얼', '내부 업무 매뉴얼 및 프로세스 가이드', 'MANUAL', 'Y',
CURRENT_DATE, 'SYSTEM', 'SYSTEM', 'AXH', 'SOATM0100',
CURRENT_DATE, 'SYSTEM', 'SYSTEM', 'AXH', 'SOATM0100'),
('KNWL_E', '법률/규정', '보험업법 및 금융당국 규정', 'LEGAL', 'Y',
CURRENT_DATE, 'SYSTEM', 'SYSTEM', 'AXH', 'SOATM0100',
CURRENT_DATE, 'SYSTEM', 'SYSTEM', 'AXH', 'SOATM0100');
COMMIT;
-- 역할 데이터
INSERT INTO S_EIAM_TIS.AA_ROLE (SYST_ID,ROLE_NO,ROLE_NM,ROLE_DS,ASST_OWNR_ROLE_TYPE_CD,MASK_ECPT_MD_CD,INDV_INFO_ATHR_YN,MAIN_CST_IFIN_YN,PUSE_YN,SYST_RGI_DT,SYST_RGI_PRAF_NO,SYST_RGI_OGNZ_NO,SYST_RGI_SYST_CD,SYST_RGI_PRGR_ID,SYST_CHG_DT,SYST_CHG_PRAF_NO,SYST_CHG_OGNZ_NO,SYST_CHG_SYST_CD,SYST_CHG_PRGR_ID) VALUES ('0000000205','110','AI아키텍트','AI 시스템 아키텍처 설계 및 에이전트 구조 검토 담당자','ZZZ','N','N','N','Y',TIMESTAMP '2022-04-19 12:19:18','99999999','9999999','SAA','EIAM',TIMESTAMP '2022-04-19 12:30:08','99999999','9999999','SAA','EIAM' ) ;
INSERT INTO S_EIAM_TIS.AA_ROLE (SYST_ID,ROLE_NO,ROLE_NM,ROLE_DS,ASST_OWNR_ROLE_TYPE_CD,MASK_ECPT_MD_CD,INDV_INFO_ATHR_YN,MAIN_CST_IFIN_YN,PUSE_YN,SYST_RGI_DT,SYST_RGI_PRAF_NO,SYST_RGI_OGNZ_NO,SYST_RGI_SYST_CD,SYST_RGI_PRGR_ID,SYST_CHG_DT,SYST_CHG_PRAF_NO,SYST_CHG_OGNZ_NO,SYST_CHG_SYST_CD,SYST_CHG_PRGR_ID) VALUES ('0000000205','106','데이터분석사용자','AI 분석 대시보드 및 에이전트 통계 조회 가능 사용자','ZZZ','N','N','N','Y',TIMESTAMP '2022-04-19 12:19:19','99999999','9999999','SAA','EIAM',TIMESTAMP '2022-04-19 12:30:08','99999999','9999999','SAA','EIAM' ) ;
INSERT INTO S_EIAM_TIS.AA_ROLE (SYST_ID,ROLE_NO,ROLE_NM,ROLE_DS,ASST_OWNR_ROLE_TYPE_CD,MASK_ECPT_MD_CD,INDV_INFO_ATHR_YN,MAIN_CST_IFIN_YN,PUSE_YN,SYST_RGI_DT,SYST_RGI_PRAF_NO,SYST_RGI_OGNZ_NO,SYST_RGI_SYST_CD,SYST_RGI_PRGR_ID,SYST_CHG_DT,SYST_CHG_PRAF_NO,SYST_CHG_OGNZ_NO,SYST_CHG_SYST_CD,SYST_CHG_PRGR_ID) VALUES ('0000000205','29','일반사용자','AI Hub 기본 이용 권한이 부여된 전사 일반 직원','ZZZ','N','N','N','Y',TIMESTAMP '2022-04-19 12:19:19','99999999','9999999','SAA','EIAM',TIMESTAMP '2022-04-19 12:30:08','99999999','9999999','SAA','EIAM' ) ;
INSERT INTO S_EIAM_TIS.AA_ROLE (SYST_ID,ROLE_NO,ROLE_NM,ROLE_DS,ASST_OWNR_ROLE_TYPE_CD,MASK_ECPT_MD_CD,INDV_INFO_ATHR_YN,MAIN_CST_IFIN_YN,PUSE_YN,SYST_RGI_DT,SYST_RGI_PRAF_NO,SYST_RGI_OGNZ_NO,SYST_RGI_SYST_CD,SYST_RGI_PRGR_ID,SYST_CHG_DT,SYST_CHG_PRAF_NO,SYST_CHG_OGNZ_NO,SYST_CHG_SYST_CD,SYST_CHG_PRGR_ID) VALUES ('0000000205','126','AI개발책임자','AI 서비스 개발을 총괄하는 책임자로 에이전트 빌드 파이프라인 최종 승인 권한 보유','ZZZ','N','N','N','Y',TIMESTAMP '2022-04-19 12:19:19','99999999','9999999','SAA','EIAM',TIMESTAMP '2022-04-19 12:30:08','99999999','9999999','SAA','EIAM' ) ;
INSERT INTO S_EIAM_TIS.AA_ROLE (SYST_ID,ROLE_NO,ROLE_NM,ROLE_DS,ASST_OWNR_ROLE_TYPE_CD,MASK_ECPT_MD_CD,INDV_INFO_ATHR_YN,MAIN_CST_IFIN_YN,PUSE_YN,SYST_RGI_DT,SYST_RGI_PRAF_NO,SYST_RGI_OGNZ_NO,SYST_RGI_SYST_CD,SYST_RGI_PRGR_ID,SYST_CHG_DT,SYST_CHG_PRAF_NO,SYST_CHG_OGNZ_NO,SYST_CHG_SYST_CD,SYST_CHG_PRGR_ID) VALUES ('0000000205','133','AI운영책임자','AI 서비스 운영 및 에이전트 인프라를 총괄하는 책임자','ZZZ','N','N','N','Y',TIMESTAMP '2022-04-19 12:19:19','99999999','9999999','SAA','EIAM',TIMESTAMP '2022-04-19 12:30:08','99999999','9999999','SAA','EIAM' ) ;
INSERT INTO S_EIAM_TIS.AA_ROLE (SYST_ID,ROLE_NO,ROLE_NM,ROLE_DS,ASST_OWNR_ROLE_TYPE_CD,MASK_ECPT_MD_CD,INDV_INFO_ATHR_YN,MAIN_CST_IFIN_YN,PUSE_YN,SYST_RGI_DT,SYST_RGI_PRAF_NO,SYST_RGI_OGNZ_NO,SYST_RGI_SYST_CD,SYST_RGI_PRGR_ID,SYST_CHG_DT,SYST_CHG_PRAF_NO,SYST_CHG_OGNZ_NO,SYST_CHG_SYST_CD,SYST_CHG_PRGR_ID) VALUES ('0000000205','122','AI위험관리자','AI 모델 리스크 평가 및 컴플라이언스 모니터링 담당자','ZZZ','N','N','N','Y',TIMESTAMP '2022-04-19 12:19:19','99999999','9999999','SAA','EIAM',TIMESTAMP '2022-04-19 12:30:08','99999999','9999999','SAA','EIAM' ) ;
INSERT INTO S_EIAM_TIS.AA_ROLE (SYST_ID,ROLE_NO,ROLE_NM,ROLE_DS,ASST_OWNR_ROLE_TYPE_CD,MASK_ECPT_MD_CD,INDV_INFO_ATHR_YN,MAIN_CST_IFIN_YN,PUSE_YN,SYST_RGI_DT,SYST_RGI_PRAF_NO,SYST_RGI_OGNZ_NO,SYST_RGI_SYST_CD,SYST_RGI_PRGR_ID,SYST_CHG_DT,SYST_CHG_PRAF_NO,SYST_CHG_OGNZ_NO,SYST_CHG_SYST_CD,SYST_CHG_PRGR_ID) VALUES ('0000000205','125','AI감사인','AI 에이전트 활동 이력 및 데이터 사용 내역 감사 담당자','ZZZ','N','N','N','Y',TIMESTAMP '2022-04-19 12:19:19','99999999','9999999','SAA','EIAM',TIMESTAMP '2022-04-19 12:30:08','99999999','9999999','SAA','EIAM' ) ;
INSERT INTO S_EIAM_TIS.AA_ROLE (SYST_ID,ROLE_NO,ROLE_NM,ROLE_DS,ASST_OWNR_ROLE_TYPE_CD,MASK_ECPT_MD_CD,INDV_INFO_ATHR_YN,MAIN_CST_IFIN_YN,PUSE_YN,SYST_RGI_DT,SYST_RGI_PRAF_NO,SYST_RGI_OGNZ_NO,SYST_RGI_SYST_CD,SYST_RGI_PRGR_ID,SYST_CHG_DT,SYST_CHG_PRAF_NO,SYST_CHG_OGNZ_NO,SYST_CHG_SYST_CD,SYST_CHG_PRGR_ID) VALUES ('0000000205','69','AI배포검토관','AI 모델 및 에이전트 서비스 배포 전 변경 검토 회의 주관자','ZZZ','N','N','N','Y',TIMESTAMP '2022-04-19 12:19:19','99999999','9999999','SAA','EIAM',TIMESTAMP '2022-04-19 12:30:08','99999999','9999999','SAA','EIAM' ) ;
INSERT INTO S_EIAM_TIS.AA_ROLE (SYST_ID,ROLE_NO,ROLE_NM,ROLE_DS,ASST_OWNR_ROLE_TYPE_CD,MASK_ECPT_MD_CD,INDV_INFO_ATHR_YN,MAIN_CST_IFIN_YN,PUSE_YN,SYST_RGI_DT,SYST_RGI_PRAF_NO,SYST_RGI_OGNZ_NO,SYST_RGI_SYST_CD,SYST_RGI_PRGR_ID,SYST_CHG_DT,SYST_CHG_PRAF_NO,SYST_CHG_OGNZ_NO,SYST_CHG_SYST_CD,SYST_CHG_PRGR_ID) VALUES ('0000000205','4','AI개발자','AI 에이전트 및 서비스 개발에 참여하는 내부 및 협력사 직원','ZZZ','N','N','N','Y',TIMESTAMP '2022-04-19 12:19:19','99999999','9999999','SAA','EIAM',TIMESTAMP '2022-04-19 12:30:08','99999999','9999999','SAA','EIAM' ) ;
INSERT INTO S_EIAM_TIS.AA_ROLE (SYST_ID,ROLE_NO,ROLE_NM,ROLE_DS,ASST_OWNR_ROLE_TYPE_CD,MASK_ECPT_MD_CD,INDV_INFO_ATHR_YN,MAIN_CST_IFIN_YN,PUSE_YN,SYST_RGI_DT,SYST_RGI_PRAF_NO,SYST_RGI_OGNZ_NO,SYST_RGI_SYST_CD,SYST_RGI_PRGR_ID,SYST_CHG_DT,SYST_CHG_PRAF_NO,SYST_CHG_OGNZ_NO,SYST_CHG_SYST_CD,SYST_CHG_PRGR_ID) VALUES ('0000000205','18','시스템관리자','AI Hub 전체 시스템 설정 및 에이전트 플랫폼 관리 담당자','ZZZ','N','N','N','Y',TIMESTAMP '2022-04-19 12:19:19','99999999','9999999','SAA','EIAM',TIMESTAMP '2022-04-19 12:30:08','99999999','9999999','SAA','EIAM' ) ;
INSERT INTO S_EIAM_TIS.AA_ROLE (SYST_ID,ROLE_NO,ROLE_NM,ROLE_DS,ASST_OWNR_ROLE_TYPE_CD,MASK_ECPT_MD_CD,INDV_INFO_ATHR_YN,MAIN_CST_IFIN_YN,PUSE_YN,SYST_RGI_DT,SYST_RGI_PRAF_NO,SYST_RGI_OGNZ_NO,SYST_RGI_SYST_CD,SYST_RGI_PRGR_ID,SYST_CHG_DT,SYST_CHG_PRAF_NO,SYST_CHG_OGNZ_NO,SYST_CHG_SYST_CD,SYST_CHG_PRGR_ID) VALUES ('0000000205','123','AI서비스요청자','AI 기능 개발 및 에이전트 도입을 요청하는 전사 직원','ZZZ','N','N','N','Y',TIMESTAMP '2022-04-19 12:19:19','99999999','9999999','SAA','EIAM',TIMESTAMP '2022-04-19 12:30:08','99999999','9999999','SAA','EIAM' ) ;
INSERT INTO S_EIAM_TIS.AA_ROLE (SYST_ID,ROLE_NO,ROLE_NM,ROLE_DS,ASST_OWNR_ROLE_TYPE_CD,MASK_ECPT_MD_CD,INDV_INFO_ATHR_YN,MAIN_CST_IFIN_YN,PUSE_YN,SYST_RGI_DT,SYST_RGI_PRAF_NO,SYST_RGI_OGNZ_NO,SYST_RGI_SYST_CD,SYST_RGI_PRGR_ID,SYST_CHG_DT,SYST_CHG_PRAF_NO,SYST_CHG_OGNZ_NO,SYST_CHG_SYST_CD,SYST_CHG_PRGR_ID) VALUES ('0000000205','132','AI검증자','AI 모델 및 에이전트 기능 검증에 참여하는 전사 직원','ZZZ','N','N','N','Y',TIMESTAMP '2022-04-19 12:19:19','99999999','9999999','SAA','EIAM',TIMESTAMP '2022-04-19 12:30:08','99999999','9999999','SAA','EIAM' ) ;
INSERT INTO S_EIAM_TIS.AA_ROLE (SYST_ID,ROLE_NO,ROLE_NM,ROLE_DS,ASST_OWNR_ROLE_TYPE_CD,MASK_ECPT_MD_CD,INDV_INFO_ATHR_YN,MAIN_CST_IFIN_YN,PUSE_YN,SYST_RGI_DT,SYST_RGI_PRAF_NO,SYST_RGI_OGNZ_NO,SYST_RGI_SYST_CD,SYST_RGI_PRGR_ID,SYST_CHG_DT,SYST_CHG_PRAF_NO,SYST_CHG_OGNZ_NO,SYST_CHG_SYST_CD,SYST_CHG_PRGR_ID) VALUES ('0000000205','127','현업AI위험관리자','현업 부서 내 AI 서비스 도입 리스크 관리를 수행하는 직원','ZZZ','N','N','N','Y',TIMESTAMP '2022-04-19 12:19:19','99999999','9999999','SAA','EIAM',TIMESTAMP '2022-04-19 12:30:08','99999999','9999999','SAA','EIAM' ) ;
INSERT INTO S_EIAM_TIS.AA_ROLE (SYST_ID,ROLE_NO,ROLE_NM,ROLE_DS,ASST_OWNR_ROLE_TYPE_CD,MASK_ECPT_MD_CD,INDV_INFO_ATHR_YN,MAIN_CST_IFIN_YN,PUSE_YN,SYST_RGI_DT,SYST_RGI_PRAF_NO,SYST_RGI_OGNZ_NO,SYST_RGI_SYST_CD,SYST_RGI_PRGR_ID,SYST_CHG_DT,SYST_CHG_PRAF_NO,SYST_CHG_OGNZ_NO,SYST_CHG_SYST_CD,SYST_CHG_PRGR_ID) VALUES ('0000000205','128','현업AI책임자','현업 부서 내 AI 서비스 개발 및 도입을 책임지는 직원','ZZZ','N','N','N','Y',TIMESTAMP '2022-04-19 12:19:19','99999999','9999999','SAA','EIAM',TIMESTAMP '2022-04-19 12:30:08','99999999','9999999','SAA','EIAM') ;
INSERT INTO S_EIAM_TIS.AA_ROLE (SYST_ID,ROLE_NO,ROLE_NM,ROLE_DS,ASST_OWNR_ROLE_TYPE_CD,MASK_ECPT_MD_CD,INDV_INFO_ATHR_YN,MAIN_CST_IFIN_YN,PUSE_YN,SYST_RGI_DT,SYST_RGI_PRAF_NO,SYST_RGI_OGNZ_NO,SYST_RGI_SYST_CD,SYST_RGI_PRGR_ID,SYST_CHG_DT,SYST_CHG_PRAF_NO,SYST_CHG_OGNZ_NO,SYST_CHG_SYST_CD,SYST_CHG_PRGR_ID) VALUES ('0000000205','142','AI개발부서장','AI 개발 조직의 부서장으로 에이전트 개발 방향성 결정 권한 보유','ZZZ','N','N','N','Y',TIMESTAMP '2022-04-19 12:19:19','99999999','9999999','SAA','EIAM',TIMESTAMP '2022-04-19 12:30:08','99999999','9999999','SAA','EIAM' ) ;
INSERT INTO S_EIAM_TIS.AA_ROLE (SYST_ID,ROLE_NO,ROLE_NM,ROLE_DS,ASST_OWNR_ROLE_TYPE_CD,MASK_ECPT_MD_CD,INDV_INFO_ATHR_YN,MAIN_CST_IFIN_YN,PUSE_YN,SYST_RGI_DT,SYST_RGI_PRAF_NO,SYST_RGI_OGNZ_NO,SYST_RGI_SYST_CD,SYST_RGI_PRGR_ID,SYST_CHG_DT,SYST_CHG_PRAF_NO,SYST_CHG_OGNZ_NO,SYST_CHG_SYST_CD,SYST_CHG_PRGR_ID) VALUES ('0000000205','143','AI운영부서장','AI 운영 조직의 부서장으로 에이전트 서비스 안정성 총괄','ZZZ','N','N','N','Y',TIMESTAMP '2022-04-19 12:19:19','99999999','9999999','SAA','EIAM',TIMESTAMP '2022-04-19 12:30:08','99999999','9999999','SAA','EIAM' ) ;
INSERT INTO S_EIAM_TIS.AA_ROLE (SYST_ID,ROLE_NO,ROLE_NM,ROLE_DS,ASST_OWNR_ROLE_TYPE_CD,MASK_ECPT_MD_CD,INDV_INFO_ATHR_YN,MAIN_CST_IFIN_YN,PUSE_YN,SYST_RGI_DT,SYST_RGI_PRAF_NO,SYST_RGI_OGNZ_NO,SYST_RGI_SYST_CD,SYST_RGI_PRGR_ID,SYST_CHG_DT,SYST_CHG_PRAF_NO,SYST_CHG_OGNZ_NO,SYST_CHG_SYST_CD,SYST_CHG_PRGR_ID) VALUES ('0000000205','225','지식감사인','AI 지식베이스 무결성 및 에이전트 활동 이력 독립 감사 담당자','ZZZ','N','N','N','Y',TIMESTAMP '2022-04-19 12:19:19','99999999','9999999','SAA','EIAM',TIMESTAMP '2022-05-06 15:45:08','61003745','0997100','SAA','EIAM' ) ;
INSERT INTO S_EIAM_TIS.AA_ROLE (SYST_ID,ROLE_NO,ROLE_NM,ROLE_DS,ASST_OWNR_ROLE_TYPE_CD,MASK_ECPT_MD_CD,INDV_INFO_ATHR_YN,MAIN_CST_IFIN_YN,PUSE_YN,SYST_RGI_DT,SYST_RGI_PRAF_NO,SYST_RGI_OGNZ_NO,SYST_RGI_SYST_CD,SYST_RGI_PRGR_ID,SYST_CHG_DT,SYST_CHG_PRAF_NO,SYST_CHG_OGNZ_NO,SYST_CHG_SYST_CD,SYST_CHG_PRGR_ID) VALUES ('0000000205','144','AI품질관리자','AI 모델 성능 및 응답 품질 제3자 검증 담당자','ZZZ','N','N','N','Y',TIMESTAMP '2022-04-19 12:19:19','99999999','9999999','SAA','EIAM',TIMESTAMP '2022-05-03 13:30:07','61003745','0997100','SAA','EIAM' ) ;
-- 사용자 그룹
INSERT INTO S_EIAM_TIS.AA_USER_GROU (SYST_ID,USER_GROU_ID,OGNZ_NO,USER_GROU_NM,USER_GROU_DS,PUSE_YN,SYST_RGI_DT,SYST_RGI_PRAF_NO,SYST_RGI_OGNZ_NO,SYST_RGI_SYST_CD,SYST_RGI_PRGR_ID,SYST_CHG_DT,SYST_CHG_PRAF_NO,SYST_CHG_OGNZ_NO,SYST_CHG_SYST_CD,SYST_CHG_PRGR_ID) VALUES ('0000000205','UG_0011296', NULL ,'어플리케이션아키텍트','어플리케이션아키텍트','Y',TIMESTAMP '2022-04-19 12:19:18','99999999','9999999','SAA','EIAM',TIMESTAMP '2022-04-19 12:30:17','99999999','9999999','SAA','EIAM' ) ;
INSERT INTO S_EIAM_TIS.AA_USER_GROU (SYST_ID,USER_GROU_ID,OGNZ_NO,USER_GROU_NM,USER_GROU_DS,PUSE_YN,SYST_RGI_DT,SYST_RGI_PRAF_NO,SYST_RGI_OGNZ_NO,SYST_RGI_SYST_CD,SYST_RGI_PRGR_ID,SYST_CHG_DT,SYST_CHG_PRAF_NO,SYST_CHG_OGNZ_NO,SYST_CHG_SYST_CD,SYST_CHG_PRGR_ID) VALUES ('0000000205','UG_0011297', NULL ,'통계사용자','통계화면을 볼 수 있는 사용자','Y',TIMESTAMP '2022-04-19 12:19:18','99999999','9999999','SAA','EIAM',TIMESTAMP '2022-04-19 12:30:17','99999999','9999999','SAA','EIAM' ) ;
INSERT INTO S_EIAM_TIS.AA_USER_GROU (SYST_ID,USER_GROU_ID,OGNZ_NO,USER_GROU_NM,USER_GROU_DS,PUSE_YN,SYST_RGI_DT,SYST_RGI_PRAF_NO,SYST_RGI_OGNZ_NO,SYST_RGI_SYST_CD,SYST_RGI_PRGR_ID,SYST_CHG_DT,SYST_CHG_PRAF_NO,SYST_CHG_OGNZ_NO,SYST_CHG_SYST_CD,SYST_CHG_PRGR_ID) VALUES ('0000000205','UG_0011298', NULL ,'IT내부직원','ICT본부내 신한라이프 정직원 자동부여','Y',TIMESTAMP '2022-04-19 12:19:18','99999999','9999999','SAA','EIAM',TIMESTAMP '2022-04-19 12:30:17','99999999','9999999','SAA','EIAM' ) ;
INSERT INTO S_EIAM_TIS.AA_USER_GROU (SYST_ID,USER_GROU_ID,OGNZ_NO,USER_GROU_NM,USER_GROU_DS,PUSE_YN,SYST_RGI_DT,SYST_RGI_PRAF_NO,SYST_RGI_OGNZ_NO,SYST_RGI_SYST_CD,SYST_RGI_PRGR_ID,SYST_CHG_DT,SYST_CHG_PRAF_NO,SYST_CHG_OGNZ_NO,SYST_CHG_SYST_CD,SYST_CHG_PRGR_ID) VALUES ('0000000205','UG_0011299', NULL ,'IT개발책임자','(ICT본부만 해당)IT개발부서장 및 IT개발부서장 권한이양받은 파트장 (근거 있어야 함)','Y',TIMESTAMP '2022-04-19 12:19:18','99999999','9999999','SAA','EIAM',TIMESTAMP '2022-04-19 12:30:17','99999999','9999999','SAA','EIAM' ) ;
INSERT INTO S_EIAM_TIS.AA_USER_GROU (SYST_ID,USER_GROU_ID,OGNZ_NO,USER_GROU_NM,USER_GROU_DS,PUSE_YN,SYST_RGI_DT,SYST_RGI_PRAF_NO,SYST_RGI_OGNZ_NO,SYST_RGI_SYST_CD,SYST_RGI_PRGR_ID,SYST_CHG_DT,SYST_CHG_PRAF_NO,SYST_CHG_OGNZ_NO,SYST_CHG_SYST_CD,SYST_CHG_PRGR_ID) VALUES ('0000000205','UG_0011300', NULL ,'IT운영책임자','(ICT본부만 해당)IT운영부서장 및 IT운영부서장 권한이양받은 파트장 (근거 있어야 함)','Y',TIMESTAMP '2022-04-19 12:19:18','99999999','9999999','SAA','EIAM',TIMESTAMP '2022-04-19 12:30:17','99999999','9999999','SAA','EIAM' ) ;
INSERT INTO S_EIAM_TIS.AA_USER_GROU (SYST_ID,USER_GROU_ID,OGNZ_NO,USER_GROU_NM,USER_GROU_DS,PUSE_YN,SYST_RGI_DT,SYST_RGI_PRAF_NO,SYST_RGI_OGNZ_NO,SYST_RGI_SYST_CD,SYST_RGI_PRGR_ID,SYST_CHG_DT,SYST_CHG_PRAF_NO,SYST_CHG_OGNZ_NO,SYST_CHG_SYST_CD,SYST_CHG_PRGR_ID) VALUES ('0000000205','UG_0011301', NULL ,'RM','ICT본부내 IT RM그룹','Y',TIMESTAMP '2022-04-19 12:19:18','99999999','9999999','SAA','EIAM',TIMESTAMP '2022-04-19 12:30:17','99999999','9999999','SAA','EIAM' ) ;
INSERT INTO S_EIAM_TIS.AA_USER_GROU (SYST_ID,USER_GROU_ID,OGNZ_NO,USER_GROU_NM,USER_GROU_DS,PUSE_YN,SYST_RGI_DT,SYST_RGI_PRAF_NO,SYST_RGI_OGNZ_NO,SYST_RGI_SYST_CD,SYST_RGI_PRGR_ID,SYST_CHG_DT,SYST_CHG_PRAF_NO,SYST_CHG_OGNZ_NO,SYST_CHG_SYST_CD,SYST_CHG_PRGR_ID) VALUES ('0000000205','UG_0011302', NULL ,'IT감사인','ICT본부내 IT감사인','Y',TIMESTAMP '2022-04-19 12:19:18','99999999','9999999','SAA','EIAM',TIMESTAMP '2022-04-19 12:30:17','99999999','9999999','SAA','EIAM' ) ;
INSERT INTO S_EIAM_TIS.AA_USER_GROU (SYST_ID,USER_GROU_ID,OGNZ_NO,USER_GROU_NM,USER_GROU_DS,PUSE_YN,SYST_RGI_DT,SYST_RGI_PRAF_NO,SYST_RGI_OGNZ_NO,SYST_RGI_SYST_CD,SYST_RGI_PRGR_ID,SYST_CHG_DT,SYST_CHG_PRAF_NO,SYST_CHG_OGNZ_NO,SYST_CHG_SYST_CD,SYST_CHG_PRGR_ID) VALUES ('0000000205','UG_0011303', NULL ,'이행검토주관자','ICT본부내 변경관리회의 주관자','Y',TIMESTAMP '2022-04-19 12:19:18','99999999','9999999','SAA','EIAM',TIMESTAMP '2022-04-19 12:30:17','99999999','9999999','SAA','EIAM' ) ;
INSERT INTO S_EIAM_TIS.AA_USER_GROU (SYST_ID,USER_GROU_ID,OGNZ_NO,USER_GROU_NM,USER_GROU_DS,PUSE_YN,SYST_RGI_DT,SYST_RGI_PRAF_NO,SYST_RGI_OGNZ_NO,SYST_RGI_SYST_CD,SYST_RGI_PRGR_ID,SYST_CHG_DT,SYST_CHG_PRAF_NO,SYST_CHG_OGNZ_NO,SYST_CHG_SYST_CD,SYST_CHG_PRGR_ID) VALUES ('0000000205','UG_0011304', NULL ,'IT개발자','ICT본부내 신한라이프 정직원-도급직원 자동부여','Y',TIMESTAMP '2022-04-19 12:19:18','99999999','9999999','SAA','EIAM',TIMESTAMP '2022-04-19 12:30:17','99999999','9999999','SAA','EIAM' ) ;
INSERT INTO S_EIAM_TIS.AA_USER_GROU (SYST_ID,USER_GROU_ID,OGNZ_NO,USER_GROU_NM,USER_GROU_DS,PUSE_YN,SYST_RGI_DT,SYST_RGI_PRAF_NO,SYST_RGI_OGNZ_NO,SYST_RGI_SYST_CD,SYST_RGI_PRGR_ID,SYST_CHG_DT,SYST_CHG_PRAF_NO,SYST_CHG_OGNZ_NO,SYST_CHG_SYST_CD,SYST_CHG_PRGR_ID) VALUES ('0000000205','UG_0011305', NULL ,'관리자','IT개발관리시스템 설정 및 시스템 관리','Y',TIMESTAMP '2022-04-19 12:19:18','99999999','9999999','SAA','EIAM',TIMESTAMP '2022-04-19 12:30:17','99999999','9999999','SAA','EIAM' ) ;
INSERT INTO S_EIAM_TIS.AA_USER_GROU (SYST_ID,USER_GROU_ID,OGNZ_NO,USER_GROU_NM,USER_GROU_DS,PUSE_YN,SYST_RGI_DT,SYST_RGI_PRAF_NO,SYST_RGI_OGNZ_NO,SYST_RGI_SYST_CD,SYST_RGI_PRGR_ID,SYST_CHG_DT,SYST_CHG_PRAF_NO,SYST_CHG_OGNZ_NO,SYST_CHG_SYST_CD,SYST_CHG_PRGR_ID) VALUES ('0000000205','UG_0011306', NULL ,'IT개발의뢰자','신한라이프 전사 정직원 자동부여','Y',TIMESTAMP '2022-04-19 12:19:18','99999999','9999999','SAA','EIAM',TIMESTAMP '2022-04-19 12:30:17','99999999','9999999','SAA','EIAM' ) ;
INSERT INTO S_EIAM_TIS.AA_USER_GROU (SYST_ID,USER_GROU_ID,OGNZ_NO,USER_GROU_NM,USER_GROU_DS,PUSE_YN,SYST_RGI_DT,SYST_RGI_PRAF_NO,SYST_RGI_OGNZ_NO,SYST_RGI_SYST_CD,SYST_RGI_PRGR_ID,SYST_CHG_DT,SYST_CHG_PRAF_NO,SYST_CHG_OGNZ_NO,SYST_CHG_SYST_CD,SYST_CHG_PRGR_ID) VALUES ('0000000205','UG_0011307', NULL ,'UAT테스터','신한라이프 전사 정직원 자동부여','Y',TIMESTAMP '2022-04-19 12:19:18','99999999','9999999','SAA','EIAM',TIMESTAMP '2022-04-19 12:30:17','99999999','9999999','SAA','EIAM' ) ;
INSERT INTO S_EIAM_TIS.AA_USER_GROU (SYST_ID,USER_GROU_ID,OGNZ_NO,USER_GROU_NM,USER_GROU_DS,PUSE_YN,SYST_RGI_DT,SYST_RGI_PRAF_NO,SYST_RGI_OGNZ_NO,SYST_RGI_SYST_CD,SYST_RGI_PRGR_ID,SYST_CHG_DT,SYST_CHG_PRAF_NO,SYST_CHG_OGNZ_NO,SYST_CHG_SYST_CD,SYST_CHG_PRGR_ID) VALUES ('0000000205','UG_0011308', NULL ,'현업_RM','(ICT본부외 해당)IT개발업무를 수행하는 현업부서내 RM역할 수행직원','Y',TIMESTAMP '2022-04-19 12:19:18','99999999','9999999','SAA','EIAM',TIMESTAMP '2022-04-19 12:30:17','99999999','9999999','SAA','EIAM' ) ;
INSERT INTO S_EIAM_TIS.AA_USER_GROU (SYST_ID,USER_GROU_ID,OGNZ_NO,USER_GROU_NM,USER_GROU_DS,PUSE_YN,SYST_RGI_DT,SYST_RGI_PRAF_NO,SYST_RGI_OGNZ_NO,SYST_RGI_SYST_CD,SYST_RGI_PRGR_ID,SYST_CHG_DT,SYST_CHG_PRAF_NO,SYST_CHG_OGNZ_NO,SYST_CHG_SYST_CD,SYST_CHG_PRGR_ID) VALUES ('0000000205','UG_0011309', NULL ,'현업_개발책임자','(ICT본부외 해당)IT개발업무를 수행하는 현업부서내 IT개발부서장역할을 수행하는 직원','Y',TIMESTAMP '2022-04-19 12:19:18','99999999','9999999','SAA','EIAM',TIMESTAMP '2022-04-19 12:30:17','99999999','9999999','SAA','EIAM') ;
INSERT INTO S_EIAM_TIS.AA_USER_GROU (SYST_ID,USER_GROU_ID,OGNZ_NO,USER_GROU_NM,USER_GROU_DS,PUSE_YN,SYST_RGI_DT,SYST_RGI_PRAF_NO,SYST_RGI_OGNZ_NO,SYST_RGI_SYST_CD,SYST_RGI_PRGR_ID,SYST_CHG_DT,SYST_CHG_PRAF_NO,SYST_CHG_OGNZ_NO,SYST_CHG_SYST_CD,SYST_CHG_PRGR_ID) VALUES ('0000000205','UG_0011310', NULL ,'IT개발부서장','ICT본부내 개발 부서장 자동부여','Y',TIMESTAMP '2022-04-19 12:19:18','99999999','9999999','SAA','EIAM',TIMESTAMP '2022-04-19 12:30:17','99999999','9999999','SAA','EIAM' ) ;
INSERT INTO S_EIAM_TIS.AA_USER_GROU (SYST_ID,USER_GROU_ID,OGNZ_NO,USER_GROU_NM,USER_GROU_DS,PUSE_YN,SYST_RGI_DT,SYST_RGI_PRAF_NO,SYST_RGI_OGNZ_NO,SYST_RGI_SYST_CD,SYST_RGI_PRGR_ID,SYST_CHG_DT,SYST_CHG_PRAF_NO,SYST_CHG_OGNZ_NO,SYST_CHG_SYST_CD,SYST_CHG_PRGR_ID) VALUES ('0000000205','UG_0011311', NULL ,'IT운영부서장','ICT본부내 운영 부서장 자동부여','Y',TIMESTAMP '2022-04-19 12:19:18','99999999','9999999','SAA','EIAM',TIMESTAMP '2022-04-19 12:30:17','99999999','9999999','SAA','EIAM' ) ;
INSERT INTO S_EIAM_TIS.AA_USER_GROU (SYST_ID,USER_GROU_ID,OGNZ_NO,USER_GROU_NM,USER_GROU_DS,PUSE_YN,SYST_RGI_DT,SYST_RGI_PRAF_NO,SYST_RGI_OGNZ_NO,SYST_RGI_SYST_CD,SYST_RGI_PRGR_ID,SYST_CHG_DT,SYST_CHG_PRAF_NO,SYST_CHG_OGNZ_NO,SYST_CHG_SYST_CD,SYST_CHG_PRGR_ID) VALUES ('0000000205','UG_0011312', NULL ,'감사인','감사팀내 IT감사인','N',TIMESTAMP '2022-04-19 12:19:18','99999999','9999999','SAA','EIAM',TIMESTAMP '2022-05-10 16:30:32','61003745','0997100','SAA','EIAM' ) ;
INSERT INTO S_EIAM_TIS.AA_USER_GROU (SYST_ID,USER_GROU_ID,OGNZ_NO,USER_GROU_NM,USER_GROU_DS,PUSE_YN,SYST_RGI_DT,SYST_RGI_PRAF_NO,SYST_RGI_OGNZ_NO,SYST_RGI_SYST_CD,SYST_RGI_PRGR_ID,SYST_CHG_DT,SYST_CHG_PRAF_NO,SYST_CHG_OGNZ_NO,SYST_CHG_SYST_CD,SYST_CHG_PRGR_ID) VALUES ('0000000205','UG_0011313', NULL ,'품질관리자','제3자점검자','Y',TIMESTAMP '2022-04-19 12:19:18','99999999','9999999','SAA','EIAM',TIMESTAMP '2022-04-19 12:30:17','99999999','9999999','SAA','EIAM' ) ;
-- 사용자 그룹-역할 관계
INSERT INTO S_EIAM_TIS.AA_USER_GROU_role_rtns (USER_GROU_ROLE_RTNS_ID,SYST_ID,USER_GROU_ID,ROLE_NO,PUSE_YN,SYST_RGI_DT,SYST_RGI_PRAF_NO,SYST_RGI_OGNZ_NO,SYST_RGI_SYST_CD,SYST_RGI_PRGR_ID,SYST_CHG_DT,SYST_CHG_PRAF_NO,SYST_CHG_OGNZ_NO,SYST_CHG_SYST_CD,SYST_CHG_PRGR_ID) VALUES ('0000030452','0000000205','UG_0011312','225','N',TIMESTAMP '2022-05-06 09:35:00','61003745','0997100','SAA','EIAM',TIMESTAMP '2022-05-10 16:15:25','99999999','9999999','SAA','EIAM') ;
INSERT INTO S_EIAM_TIS.AA_USER_GROU_role_rtns (USER_GROU_ROLE_RTNS_ID,SYST_ID,USER_GROU_ID,ROLE_NO,PUSE_YN,SYST_RGI_DT,SYST_RGI_PRAF_NO,SYST_RGI_OGNZ_NO,SYST_RGI_SYST_CD,SYST_RGI_PRGR_ID,SYST_CHG_DT,SYST_CHG_PRAF_NO,SYST_CHG_OGNZ_NO,SYST_CHG_SYST_CD,SYST_CHG_PRGR_ID) VALUES ('0000030428','0000000205','UG_0011296','110','Y',TIMESTAMP '2022-05-02 16:58:00','61003745','0997100','SAA','EIAM',TIMESTAMP '2022-05-02 17:00:23','99999999','9999999','SAA','EIAM') ;
INSERT INTO S_EIAM_TIS.AA_USER_GROU_role_rtns (USER_GROU_ROLE_RTNS_ID,SYST_ID,USER_GROU_ID,ROLE_NO,PUSE_YN,SYST_RGI_DT,SYST_RGI_PRAF_NO,SYST_RGI_OGNZ_NO,SYST_RGI_SYST_CD,SYST_RGI_PRGR_ID,SYST_CHG_DT,SYST_CHG_PRAF_NO,SYST_CHG_OGNZ_NO,SYST_CHG_SYST_CD,SYST_CHG_PRGR_ID) VALUES ('0000030455','0000000205','UG_0011312','225','N',TIMESTAMP '2022-05-06 15:55:03','61003745','0997100','SAA','EIAM',TIMESTAMP '2022-05-10 16:15:25','99999999','9999999','SAA','EIAM') ;
INSERT INTO S_EIAM_TIS.AA_USER_GROU_role_rtns (USER_GROU_ROLE_RTNS_ID,SYST_ID,USER_GROU_ID,ROLE_NO,PUSE_YN,SYST_RGI_DT,SYST_RGI_PRAF_NO,SYST_RGI_OGNZ_NO,SYST_RGI_SYST_CD,SYST_RGI_PRGR_ID,SYST_CHG_DT,SYST_CHG_PRAF_NO,SYST_CHG_OGNZ_NO,SYST_CHG_SYST_CD,SYST_CHG_PRGR_ID) VALUES ('0000030433','0000000205','UG_0011312','225','N',TIMESTAMP '2022-05-03 15:01:00','61003745','0997100','SAA','EIAM',TIMESTAMP '2022-05-10 16:15:25','99999999','9999999','SAA','EIAM') ;
INSERT INTO S_EIAM_TIS.AA_USER_GROU_role_rtns (USER_GROU_ROLE_RTNS_ID,SYST_ID,USER_GROU_ID,ROLE_NO,PUSE_YN,SYST_RGI_DT,SYST_RGI_PRAF_NO,SYST_RGI_OGNZ_NO,SYST_RGI_SYST_CD,SYST_RGI_PRGR_ID,SYST_CHG_DT,SYST_CHG_PRAF_NO,SYST_CHG_OGNZ_NO,SYST_CHG_SYST_CD,SYST_CHG_PRGR_ID) VALUES ('0000029430','0000000205','UG_0011298','29','Y',TIMESTAMP '2022-04-19 12:19:19','99999999','9999999','SAA','EIAM',TIMESTAMP '2022-04-19 12:30:24','99999999','9999999','SAA','EIAM' ) ;
INSERT INTO S_EIAM_TIS.AA_USER_GROU_role_rtns (USER_GROU_ROLE_RTNS_ID,SYST_ID,USER_GROU_ID,ROLE_NO,PUSE_YN,SYST_RGI_DT,SYST_RGI_PRAF_NO,SYST_RGI_OGNZ_NO,SYST_RGI_SYST_CD,SYST_RGI_PRGR_ID,SYST_CHG_DT,SYST_CHG_PRAF_NO,SYST_CHG_OGNZ_NO,SYST_CHG_SYST_CD,SYST_CHG_PRGR_ID) VALUES ('0000029431','0000000205','UG_0011299','126','Y',TIMESTAMP '2022-04-19 12:19:19','99999999','9999999','SAA','EIAM',TIMESTAMP '2022-04-19 12:30:24','99999999','9999999','SAA','EIAM') ;
INSERT INTO S_EIAM_TIS.AA_USER_GROU_role_rtns (USER_GROU_ROLE_RTNS_ID,SYST_ID,USER_GROU_ID,ROLE_NO,PUSE_YN,SYST_RGI_DT,SYST_RGI_PRAF_NO,SYST_RGI_OGNZ_NO,SYST_RGI_SYST_CD,SYST_RGI_PRGR_ID,SYST_CHG_DT,SYST_CHG_PRAF_NO,SYST_CHG_OGNZ_NO,SYST_CHG_SYST_CD,SYST_CHG_PRGR_ID) VALUES ('0000029432','0000000205','UG_0011300','133','Y',TIMESTAMP '2022-04-19 12:19:19','99999999','9999999','SAA','EIAM',TIMESTAMP '2022-04-19 12:30:24','99999999','9999999','SAA','EIAM') ;
INSERT INTO S_EIAM_TIS.AA_USER_GROU_role_rtns (USER_GROU_ROLE_RTNS_ID,SYST_ID,USER_GROU_ID,ROLE_NO,PUSE_YN,SYST_RGI_DT,SYST_RGI_PRAF_NO,SYST_RGI_OGNZ_NO,SYST_RGI_SYST_CD,SYST_RGI_PRGR_ID,SYST_CHG_DT,SYST_CHG_PRAF_NO,SYST_CHG_OGNZ_NO,SYST_CHG_SYST_CD,SYST_CHG_PRGR_ID) VALUES ('0000029433','0000000205','UG_0011301','122','Y',TIMESTAMP '2022-04-19 12:19:19','99999999','9999999','SAA','EIAM',TIMESTAMP '2022-04-19 12:30:24','99999999','9999999','SAA','EIAM') ;
INSERT INTO S_EIAM_TIS.AA_USER_GROU_role_rtns (USER_GROU_ROLE_RTNS_ID,SYST_ID,USER_GROU_ID,ROLE_NO,PUSE_YN,SYST_RGI_DT,SYST_RGI_PRAF_NO,SYST_RGI_OGNZ_NO,SYST_RGI_SYST_CD,SYST_RGI_PRGR_ID,SYST_CHG_DT,SYST_CHG_PRAF_NO,SYST_CHG_OGNZ_NO,SYST_CHG_SYST_CD,SYST_CHG_PRGR_ID) VALUES ('0000029434','0000000205','UG_0011302','125','Y',TIMESTAMP '2022-04-19 12:19:19','99999999','9999999','SAA','EIAM',TIMESTAMP '2022-04-19 12:30:24','99999999','9999999','SAA','EIAM') ;
INSERT INTO S_EIAM_TIS.AA_USER_GROU_role_rtns (USER_GROU_ROLE_RTNS_ID,SYST_ID,USER_GROU_ID,ROLE_NO,PUSE_YN,SYST_RGI_DT,SYST_RGI_PRAF_NO,SYST_RGI_OGNZ_NO,SYST_RGI_SYST_CD,SYST_RGI_PRGR_ID,SYST_CHG_DT,SYST_CHG_PRAF_NO,SYST_CHG_OGNZ_NO,SYST_CHG_SYST_CD,SYST_CHG_PRGR_ID) VALUES ('0000029435','0000000205','UG_0011303','69','Y',TIMESTAMP '2022-04-19 12:19:19','99999999','9999999','SAA','EIAM',TIMESTAMP '2022-04-19 12:30:24','99999999','9999999','SAA','EIAM' ) ;
INSERT INTO S_EIAM_TIS.AA_USER_GROU_role_rtns (USER_GROU_ROLE_RTNS_ID,SYST_ID,USER_GROU_ID,ROLE_NO,PUSE_YN,SYST_RGI_DT,SYST_RGI_PRAF_NO,SYST_RGI_OGNZ_NO,SYST_RGI_SYST_CD,SYST_RGI_PRGR_ID,SYST_CHG_DT,SYST_CHG_PRAF_NO,SYST_CHG_OGNZ_NO,SYST_CHG_SYST_CD,SYST_CHG_PRGR_ID) VALUES ('0000029436','0000000205','UG_0011304','4','Y',TIMESTAMP '2022-04-19 12:19:19','99999999','9999999','SAA','EIAM',TIMESTAMP '2022-04-19 12:30:24','99999999','9999999','SAA','EIAM' ) ;
INSERT INTO S_EIAM_TIS.AA_USER_GROU_role_rtns (USER_GROU_ROLE_RTNS_ID,SYST_ID,USER_GROU_ID,ROLE_NO,PUSE_YN,SYST_RGI_DT,SYST_RGI_PRAF_NO,SYST_RGI_OGNZ_NO,SYST_RGI_SYST_CD,SYST_RGI_PRGR_ID,SYST_CHG_DT,SYST_CHG_PRAF_NO,SYST_CHG_OGNZ_NO,SYST_CHG_SYST_CD,SYST_CHG_PRGR_ID) VALUES ('0000029437','0000000205','UG_0011305','18','Y',TIMESTAMP '2022-04-19 12:19:19','99999999','9999999','SAA','EIAM',TIMESTAMP '2022-04-19 12:30:24','99999999','9999999','SAA','EIAM' ) ;
INSERT INTO S_EIAM_TIS.AA_USER_GROU_role_rtns (USER_GROU_ROLE_RTNS_ID,SYST_ID,USER_GROU_ID,ROLE_NO,PUSE_YN,SYST_RGI_DT,SYST_RGI_PRAF_NO,SYST_RGI_OGNZ_NO,SYST_RGI_SYST_CD,SYST_RGI_PRGR_ID,SYST_CHG_DT,SYST_CHG_PRAF_NO,SYST_CHG_OGNZ_NO,SYST_CHG_SYST_CD,SYST_CHG_PRGR_ID) VALUES ('0000029438','0000000205','UG_0011306','123','Y',TIMESTAMP '2022-04-19 12:19:19','99999999','9999999','SAA','EIAM',TIMESTAMP '2022-04-19 12:30:24','99999999','9999999','SAA','EIAM') ;
INSERT INTO S_EIAM_TIS.AA_USER_GROU_role_rtns (USER_GROU_ROLE_RTNS_ID,SYST_ID,USER_GROU_ID,ROLE_NO,PUSE_YN,SYST_RGI_DT,SYST_RGI_PRAF_NO,SYST_RGI_OGNZ_NO,SYST_RGI_SYST_CD,SYST_RGI_PRGR_ID,SYST_CHG_DT,SYST_CHG_PRAF_NO,SYST_CHG_OGNZ_NO,SYST_CHG_SYST_CD,SYST_CHG_PRGR_ID) VALUES ('0000029439','0000000205','UG_0011307','132','Y',TIMESTAMP '2022-04-19 12:19:19','99999999','9999999','SAA','EIAM',TIMESTAMP '2022-04-19 12:30:24','99999999','9999999','SAA','EIAM') ;
INSERT INTO S_EIAM_TIS.AA_USER_GROU_role_rtns (USER_GROU_ROLE_RTNS_ID,SYST_ID,USER_GROU_ID,ROLE_NO,PUSE_YN,SYST_RGI_DT,SYST_RGI_PRAF_NO,SYST_RGI_OGNZ_NO,SYST_RGI_SYST_CD,SYST_RGI_PRGR_ID,SYST_CHG_DT,SYST_CHG_PRAF_NO,SYST_CHG_OGNZ_NO,SYST_CHG_SYST_CD,SYST_CHG_PRGR_ID) VALUES ('0000029440','0000000205','UG_0011308','127','Y',TIMESTAMP '2022-04-19 12:19:19','99999999','9999999','SAA','EIAM',TIMESTAMP '2022-04-19 12:30:24','99999999','9999999','SAA','EIAM') ;
INSERT INTO S_EIAM_TIS.AA_USER_GROU_role_rtns (USER_GROU_ROLE_RTNS_ID,SYST_ID,USER_GROU_ID,ROLE_NO,PUSE_YN,SYST_RGI_DT,SYST_RGI_PRAF_NO,SYST_RGI_OGNZ_NO,SYST_RGI_SYST_CD,SYST_RGI_PRGR_ID,SYST_CHG_DT,SYST_CHG_PRAF_NO,SYST_CHG_OGNZ_NO,SYST_CHG_SYST_CD,SYST_CHG_PRGR_ID) VALUES ('0000029441','0000000205','UG_0011309','128','Y',TIMESTAMP '2022-04-19 12:19:19','99999999','9999999','SAA','EIAM',TIMESTAMP '2022-04-19 12:30:24','99999999','9999999','SAA','EIAM') ;
INSERT INTO S_EIAM_TIS.AA_USER_GROU_role_rtns (USER_GROU_ROLE_RTNS_ID,SYST_ID,USER_GROU_ID,ROLE_NO,PUSE_YN,SYST_RGI_DT,SYST_RGI_PRAF_NO,SYST_RGI_OGNZ_NO,SYST_RGI_SYST_CD,SYST_RGI_PRGR_ID,SYST_CHG_DT,SYST_CHG_PRAF_NO,SYST_CHG_OGNZ_NO,SYST_CHG_SYST_CD,SYST_CHG_PRGR_ID) VALUES ('0000029442','0000000205','UG_0011310','142','Y',TIMESTAMP '2022-04-19 12:19:19','99999999','9999999','SAA','EIAM',TIMESTAMP '2022-04-19 12:30:24','99999999','9999999','SAA','EIAM') ;
INSERT INTO S_EIAM_TIS.AA_USER_GROU_role_rtns (USER_GROU_ROLE_RTNS_ID,SYST_ID,USER_GROU_ID,ROLE_NO,PUSE_YN,SYST_RGI_DT,SYST_RGI_PRAF_NO,SYST_RGI_OGNZ_NO,SYST_RGI_SYST_CD,SYST_RGI_PRGR_ID,SYST_CHG_DT,SYST_CHG_PRAF_NO,SYST_CHG_OGNZ_NO,SYST_CHG_SYST_CD,SYST_CHG_PRGR_ID) VALUES ('0000029443','0000000205','UG_0011311','143','Y',TIMESTAMP '2022-04-19 12:19:19','99999999','9999999','SAA','EIAM',TIMESTAMP '2022-04-19 12:30:24','99999999','9999999','SAA','EIAM') ;
INSERT INTO S_EIAM_TIS.AA_USER_GROU_role_rtns (USER_GROU_ROLE_RTNS_ID,SYST_ID,USER_GROU_ID,ROLE_NO,PUSE_YN,SYST_RGI_DT,SYST_RGI_PRAF_NO,SYST_RGI_OGNZ_NO,SYST_RGI_SYST_CD,SYST_RGI_PRGR_ID,SYST_CHG_DT,SYST_CHG_PRAF_NO,SYST_CHG_OGNZ_NO,SYST_CHG_SYST_CD,SYST_CHG_PRGR_ID) VALUES ('0000029444','0000000205','UG_0011312','225','N',TIMESTAMP '2022-04-19 12:19:19','99999999','9999999','SAA','EIAM',TIMESTAMP '2022-05-10 16:15:25','99999999','9999999','SAA','EIAM') ;
INSERT INTO S_EIAM_TIS.AA_USER_GROU_role_rtns (USER_GROU_ROLE_RTNS_ID,SYST_ID,USER_GROU_ID,ROLE_NO,PUSE_YN,SYST_RGI_DT,SYST_RGI_PRAF_NO,SYST_RGI_OGNZ_NO,SYST_RGI_SYST_CD,SYST_RGI_PRGR_ID,SYST_CHG_DT,SYST_CHG_PRAF_NO,SYST_CHG_OGNZ_NO,SYST_CHG_SYST_CD,SYST_CHG_PRGR_ID) VALUES ('0000029445','0000000205','UG_0011313','144','Y',TIMESTAMP '2022-04-19 12:19:19','99999999','9999999','SAA','EIAM',TIMESTAMP '2022-04-19 12:30:24','99999999','9999999','SAA','EIAM') ;
INSERT INTO S_EIAM_TIS.AA_USER_GROU_role_rtns (USER_GROU_ROLE_RTNS_ID,SYST_ID,USER_GROU_ID,ROLE_NO,PUSE_YN,SYST_RGI_DT,SYST_RGI_PRAF_NO,SYST_RGI_OGNZ_NO,SYST_RGI_SYST_CD,SYST_RGI_PRGR_ID,SYST_CHG_DT,SYST_CHG_PRAF_NO,SYST_CHG_OGNZ_NO,SYST_CHG_SYST_CD,SYST_CHG_PRGR_ID) VALUES ('0000030432','0000000205','UG_0011312','225','N',TIMESTAMP '2022-05-03 14:30:00','61003745','0997100','SAA','EIAM',TIMESTAMP '2022-05-10 16:15:25','99999999','9999999','SAA','EIAM') ;
INSERT INTO S_EIAM_TIS.AA_USER_GROU_role_rtns (USER_GROU_ROLE_RTNS_ID,SYST_ID,USER_GROU_ID,ROLE_NO,PUSE_YN,SYST_RGI_DT,SYST_RGI_PRAF_NO,SYST_RGI_OGNZ_NO,SYST_RGI_SYST_CD,SYST_RGI_PRGR_ID,SYST_CHG_DT,SYST_CHG_PRAF_NO,SYST_CHG_OGNZ_NO,SYST_CHG_SYST_CD,SYST_CHG_PRGR_ID) VALUES ('0000030434','0000000205','UG_0011312','225','N',TIMESTAMP '2022-05-03 15:18:00','61003745','0997100','SAA','EIAM',TIMESTAMP '2022-05-10 16:15:25','99999999','9999999','SAA','EIAM') ;
INSERT INTO S_EIAM_TIS.AA_USER_GROU_role_rtns (USER_GROU_ROLE_RTNS_ID,SYST_ID,USER_GROU_ID,ROLE_NO,PUSE_YN,SYST_RGI_DT,SYST_RGI_PRAF_NO,SYST_RGI_OGNZ_NO,SYST_RGI_SYST_CD,SYST_RGI_PRGR_ID,SYST_CHG_DT,SYST_CHG_PRAF_NO,SYST_CHG_OGNZ_NO,SYST_CHG_SYST_CD,SYST_CHG_PRGR_ID) VALUES ('0000029428','0000000205','UG_0011296','110','N',TIMESTAMP '2022-04-19 12:19:19','99999999','9999999','SAA','EIAM',TIMESTAMP '2022-05-02 16:15:25','99999999','9999999','SAA','EIAM') ;
INSERT INTO S_EIAM_TIS.AA_USER_GROU_role_rtns (USER_GROU_ROLE_RTNS_ID,SYST_ID,USER_GROU_ID,ROLE_NO,PUSE_YN,SYST_RGI_DT,SYST_RGI_PRAF_NO,SYST_RGI_OGNZ_NO,SYST_RGI_SYST_CD,SYST_RGI_PRGR_ID,SYST_CHG_DT,SYST_CHG_PRAF_NO,SYST_CHG_OGNZ_NO,SYST_CHG_SYST_CD,SYST_CHG_PRGR_ID) VALUES ('0000029429','0000000205','UG_0011297','106','Y',TIMESTAMP '2022-04-19 12:19:19','99999999','9999999','SAA','EIAM',TIMESTAMP '2022-04-19 12:30:24','99999999','9999999','SAA','EIAM') ;
-- 사용자-그룹관계
INSERT INTO S_EIAM_TIS.AA_USAC_user_grou_rtns (USAC_USER_GROU_RTNS_ID,SYST_ID,PRAF_NO,USER_GROU_ID,VLDT_YMD,EXPR_YMD,PUSE_YN,SYST_RGI_DT,SYST_RGI_PRAF_NO,SYST_RGI_OGNZ_NO,SYST_RGI_SYST_CD,SYST_RGI_PRGR_ID,SYST_CHG_DT,SYST_CHG_PRAF_NO,SYST_CHG_OGNZ_NO,SYST_CHG_SYST_CD,SYST_CHG_PRGR_ID) VALUES ('0000390266','0000000205','09861303','UG_0011307',TIMESTAMP '2022-04-19 00:00:00',TIMESTAMP '9999-12-31 00:00:00','Y',TIMESTAMP '2022-04-19 12:21:14','99999999','9999999','SAA','EIAM',TIMESTAMP '2022-04-19 12:21:14','99999999','9999999','SAA','EIAM') ;
INSERT INTO S_EIAM_TIS.AA_USAC_user_grou_rtns (USAC_USER_GROU_RTNS_ID,SYST_ID,PRAF_NO,USER_GROU_ID,VLDT_YMD,EXPR_YMD,PUSE_YN,SYST_RGI_DT,SYST_RGI_PRAF_NO,SYST_RGI_OGNZ_NO,SYST_RGI_SYST_CD,SYST_RGI_PRGR_ID,SYST_CHG_DT,SYST_CHG_PRAF_NO,SYST_CHG_OGNZ_NO,SYST_CHG_SYST_CD,SYST_CHG_PRGR_ID) VALUES ('0000389612','0000000205','61005896','UG_0011306',TIMESTAMP '2022-04-19 00:00:00',TIMESTAMP '9999-12-31 00:00:00','Y',TIMESTAMP '2022-04-19 12:21:14','99999999','9999999','SAA','EIAM',TIMESTAMP '2022-04-19 12:21:14','99999999','9999999','SAA','EIAM') ;
INSERT INTO S_EIAM_TIS.AA_USAC_user_grou_rtns (USAC_USER_GROU_RTNS_ID,SYST_ID,PRAF_NO,USER_GROU_ID,VLDT_YMD,EXPR_YMD,PUSE_YN,SYST_RGI_DT,SYST_RGI_PRAF_NO,SYST_RGI_OGNZ_NO,SYST_RGI_SYST_CD,SYST_RGI_PRGR_ID,SYST_CHG_DT,SYST_CHG_PRAF_NO,SYST_CHG_OGNZ_NO,SYST_CHG_SYST_CD,SYST_CHG_PRGR_ID) VALUES ('0000390990','0000000205','09861100','UG_0011306',TIMESTAMP '2022-04-19 00:00:00',TIMESTAMP '9999-12-31 00:00:00','Y',TIMESTAMP '2022-04-19 12:21:14','99999999','9999999','SAA','EIAM',TIMESTAMP '2022-04-19 12:21:14','99999999','9999999','SAA','EIAM') ;
INSERT INTO S_EIAM_TIS.AA_USAC_user_grou_rtns (USAC_USER_GROU_RTNS_ID,SYST_ID,PRAF_NO,USER_GROU_ID,VLDT_YMD,EXPR_YMD,PUSE_YN,SYST_RGI_DT,SYST_RGI_PRAF_NO,SYST_RGI_OGNZ_NO,SYST_RGI_SYST_CD,SYST_RGI_PRGR_ID,SYST_CHG_DT,SYST_CHG_PRAF_NO,SYST_CHG_OGNZ_NO,SYST_CHG_SYST_CD,SYST_CHG_PRGR_ID) VALUES ('0000390096','0000000205','09861195','UG_0011307',TIMESTAMP '2022-04-19 00:00:00',TIMESTAMP '9999-12-31 00:00:00','Y',TIMESTAMP '2022-04-19 12:21:14','99999999','9999999','SAA','EIAM',TIMESTAMP '2022-04-19 12:21:14','99999999','9999999','SAA','EIAM') ;
INSERT INTO S_EIAM_TIS.AA_USAC_user_grou_rtns (USAC_USER_GROU_RTNS_ID,SYST_ID,PRAF_NO,USER_GROU_ID,VLDT_YMD,EXPR_YMD,PUSE_YN,SYST_RGI_DT,SYST_RGI_PRAF_NO,SYST_RGI_OGNZ_NO,SYST_RGI_SYST_CD,SYST_RGI_PRGR_ID,SYST_CHG_DT,SYST_CHG_PRAF_NO,SYST_CHG_OGNZ_NO,SYST_CHG_SYST_CD,SYST_CHG_PRGR_ID) VALUES ('0000390103','0000000205','61005899','UG_0011306',TIMESTAMP '2022-04-19 00:00:00',TIMESTAMP '9999-12-31 00:00:00','Y',TIMESTAMP '2022-04-19 12:21:14','99999999','9999999','SAA','EIAM',TIMESTAMP '2022-04-19 12:21:14','99999999','9999999','SAA','EIAM') ;
INSERT INTO S_EIAM_TIS.AA_USAC_user_grou_rtns (USAC_USER_GROU_RTNS_ID,SYST_ID,PRAF_NO,USER_GROU_ID,VLDT_YMD,EXPR_YMD,PUSE_YN,SYST_RGI_DT,SYST_RGI_PRAF_NO,SYST_RGI_OGNZ_NO,SYST_RGI_SYST_CD,SYST_RGI_PRGR_ID,SYST_CHG_DT,SYST_CHG_PRAF_NO,SYST_CHG_OGNZ_NO,SYST_CHG_SYST_CD,SYST_CHG_PRGR_ID) VALUES ('0000390377','0000000205','09861194','UG_0011307',TIMESTAMP '2022-04-19 00:00:00',TIMESTAMP '9999-12-31 00:00:00','Y',TIMESTAMP '2022-04-19 12:21:14','99999999','9999999','SAA','EIAM',TIMESTAMP '2022-04-19 12:21:14','99999999','9999999','SAA','EIAM') ;
INSERT INTO S_EIAM_TIS.AA_USAC_user_grou_rtns (USAC_USER_GROU_RTNS_ID,SYST_ID,PRAF_NO,USER_GROU_ID,VLDT_YMD,EXPR_YMD,PUSE_YN,SYST_RGI_DT,SYST_RGI_PRAF_NO,SYST_RGI_OGNZ_NO,SYST_RGI_SYST_CD,SYST_RGI_PRGR_ID,SYST_CHG_DT,SYST_CHG_PRAF_NO,SYST_CHG_OGNZ_NO,SYST_CHG_SYST_CD,SYST_CHG_PRGR_ID) VALUES ('0000390201','0000000205','09861302','UG_0011306',TIMESTAMP '2022-04-19 00:00:00',TIMESTAMP '9999-12-31 00:00:00','Y',TIMESTAMP '2022-04-19 12:21:14','99999999','9999999','SAA','EIAM',TIMESTAMP '2022-04-19 12:21:14','99999999','9999999','SAA','EIAM') ;
INSERT INTO S_EIAM_TIS.AA_USAC_user_grou_rtns (USAC_USER_GROU_RTNS_ID,SYST_ID,PRAF_NO,USER_GROU_ID,VLDT_YMD,EXPR_YMD,PUSE_YN,SYST_RGI_DT,SYST_RGI_PRAF_NO,SYST_RGI_OGNZ_NO,SYST_RGI_SYST_CD,SYST_RGI_PRGR_ID,SYST_CHG_DT,SYST_CHG_PRAF_NO,SYST_CHG_OGNZ_NO,SYST_CHG_SYST_CD,SYST_CHG_PRGR_ID) VALUES ('0000390396','0000000205','61005872','UG_0011306',TIMESTAMP '2022-04-19 00:00:00',TIMESTAMP '9999-12-31 00:00:00','Y',TIMESTAMP '2022-04-19 12:21:14','99999999','9999999','SAA','EIAM',TIMESTAMP '2022-04-19 12:21:14','99999999','9999999','SAA','EIAM') ;
INSERT INTO S_EIAM_TIS.AA_USAC_user_grou_rtns (USAC_USER_GROU_RTNS_ID,SYST_ID,PRAF_NO,USER_GROU_ID,VLDT_YMD,EXPR_YMD,PUSE_YN,SYST_RGI_DT,SYST_RGI_PRAF_NO,SYST_RGI_OGNZ_NO,SYST_RGI_SYST_CD,SYST_RGI_PRGR_ID,SYST_CHG_DT,SYST_CHG_PRAF_NO,SYST_CHG_OGNZ_NO,SYST_CHG_SYST_CD,SYST_CHG_PRGR_ID) VALUES ('0000390209','0000000205','09861196','UG_0011307',TIMESTAMP '2022-04-19 00:00:00',TIMESTAMP '9999-12-31 00:00:00','Y',TIMESTAMP '2022-04-19 12:21:14','99999999','9999999','SAA','EIAM',TIMESTAMP '2022-04-19 12:21:14','99999999','9999999','SAA','EIAM') ;
INSERT INTO S_EIAM_TIS.AA_USAC_user_grou_rtns (USAC_USER_GROU_RTNS_ID,SYST_ID,PRAF_NO,USER_GROU_ID,VLDT_YMD,EXPR_YMD,PUSE_YN,SYST_RGI_DT,SYST_RGI_PRAF_NO,SYST_RGI_OGNZ_NO,SYST_RGI_SYST_CD,SYST_RGI_PRGR_ID,SYST_CHG_DT,SYST_CHG_PRAF_NO,SYST_CHG_OGNZ_NO,SYST_CHG_SYST_CD,SYST_CHG_PRGR_ID) VALUES ('0000390236','0000000205','61005905','UG_0011306',TIMESTAMP '2022-04-19 00:00:00',TIMESTAMP '9999-12-31 00:00:00','Y',TIMESTAMP '2022-04-19 12:21:14','99999999','9999999','SAA','EIAM',TIMESTAMP '2022-04-19 12:21:14','99999999','9999999','SAA','EIAM') ;
INSERT INTO S_EIAM_TIS.AA_USAC_user_grou_rtns (USAC_USER_GROU_RTNS_ID,SYST_ID,PRAF_NO,USER_GROU_ID,VLDT_YMD,EXPR_YMD,PUSE_YN,SYST_RGI_DT,SYST_RGI_PRAF_NO,SYST_RGI_OGNZ_NO,SYST_RGI_SYST_CD,SYST_RGI_PRGR_ID,SYST_CHG_DT,SYST_CHG_PRAF_NO,SYST_CHG_OGNZ_NO,SYST_CHG_SYST_CD,SYST_CHG_PRGR_ID) VALUES ('0000391388','0000000205','09861195','UG_0011306',TIMESTAMP '2022-04-19 00:00:00',TIMESTAMP '9999-12-31 00:00:00','Y',TIMESTAMP '2022-04-19 12:21:14','99999999','9999999','SAA','EIAM',TIMESTAMP '2022-04-19 12:21:14','99999999','9999999','SAA','EIAM') ;
INSERT INTO S_EIAM_TIS.AA_USAC_user_grou_rtns (USAC_USER_GROU_RTNS_ID,SYST_ID,PRAF_NO,USER_GROU_ID,VLDT_YMD,EXPR_YMD,PUSE_YN,SYST_RGI_DT,SYST_RGI_PRAF_NO,SYST_RGI_OGNZ_NO,SYST_RGI_SYST_CD,SYST_RGI_PRGR_ID,SYST_CHG_DT,SYST_CHG_PRAF_NO,SYST_CHG_OGNZ_NO,SYST_CHG_SYST_CD,SYST_CHG_PRGR_ID) VALUES ('0000389987','0000000205','61005898','UG_0011306',TIMESTAMP '2022-04-19 00:00:00',TIMESTAMP '9999-12-31 00:00:00','Y',TIMESTAMP '2022-04-19 12:21:14','99999999','9999999','SAA','EIAM',TIMESTAMP '2022-04-19 12:21:14','99999999','9999999','SAA','EIAM') ;
INSERT INTO S_EIAM_TIS.AA_USAC_user_grou_rtns (USAC_USER_GROU_RTNS_ID,SYST_ID,PRAF_NO,USER_GROU_ID,VLDT_YMD,EXPR_YMD,PUSE_YN,SYST_RGI_DT,SYST_RGI_PRAF_NO,SYST_RGI_OGNZ_NO,SYST_RGI_SYST_CD,SYST_RGI_PRGR_ID,SYST_CHG_DT,SYST_CHG_PRAF_NO,SYST_CHG_OGNZ_NO,SYST_CHG_SYST_CD,SYST_CHG_PRGR_ID) VALUES ('0000389376','0000000205','61005886','UG_0011307',TIMESTAMP '2022-04-19 00:00:00',TIMESTAMP '9999-12-31 00:00:00','Y',TIMESTAMP '2022-04-19 12:21:14','99999999','9999999','SAA','EIAM',TIMESTAMP '2022-04-19 12:21:14','99999999','9999999','SAA','EIAM') ;
INSERT INTO S_EIAM_TIS.AA_USAC_user_grou_rtns (USAC_USER_GROU_RTNS_ID,SYST_ID,PRAF_NO,USER_GROU_ID,VLDT_YMD,EXPR_YMD,PUSE_YN,SYST_RGI_DT,SYST_RGI_PRAF_NO,SYST_RGI_OGNZ_NO,SYST_RGI_SYST_CD,SYST_RGI_PRGR_ID,SYST_CHG_DT,SYST_CHG_PRAF_NO,SYST_CHG_OGNZ_NO,SYST_CHG_SYST_CD,SYST_CHG_PRGR_ID) VALUES ('0000391283','0000000205','09861303','UG_0011306',TIMESTAMP '2022-04-19 00:00:00',TIMESTAMP '9999-12-31 00:00:00','Y',TIMESTAMP '2022-04-19 12:21:14','99999999','9999999','SAA','EIAM',TIMESTAMP '2022-04-19 12:21:14','99999999','9999999','SAA','EIAM') ;
INSERT INTO S_EIAM_TIS.AA_USAC_user_grou_rtns (USAC_USER_GROU_RTNS_ID,SYST_ID,PRAF_NO,USER_GROU_ID,VLDT_YMD,EXPR_YMD,PUSE_YN,SYST_RGI_DT,SYST_RGI_PRAF_NO,SYST_RGI_OGNZ_NO,SYST_RGI_SYST_CD,SYST_RGI_PRGR_ID,SYST_CHG_DT,SYST_CHG_PRAF_NO,SYST_CHG_OGNZ_NO,SYST_CHG_SYST_CD,SYST_CHG_PRGR_ID) VALUES ('0000389378','0000000205','61005873','UG_0011307',TIMESTAMP '2022-04-19 00:00:00',TIMESTAMP '9999-12-31 00:00:00','Y',TIMESTAMP '2022-04-19 12:21:14','99999999','9999999','SAA','EIAM',TIMESTAMP '2022-04-19 12:21:14','99999999','9999999','SAA','EIAM') ;
INSERT INTO S_EIAM_TIS.AA_USAC_user_grou_rtns (USAC_USER_GROU_RTNS_ID,SYST_ID,PRAF_NO,USER_GROU_ID,VLDT_YMD,EXPR_YMD,PUSE_YN,SYST_RGI_DT,SYST_RGI_PRAF_NO,SYST_RGI_OGNZ_NO,SYST_RGI_SYST_CD,SYST_RGI_PRGR_ID,SYST_CHG_DT,SYST_CHG_PRAF_NO,SYST_CHG_OGNZ_NO,SYST_CHG_SYST_CD,SYST_CHG_PRGR_ID) VALUES ('0000389388','0000000205','09861194','UG_0011306',TIMESTAMP '2022-04-19 00:00:00',TIMESTAMP '9999-12-31 00:00:00','Y',TIMESTAMP '2022-04-19 12:21:14','99999999','9999999','SAA','EIAM',TIMESTAMP '2022-04-19 12:21:14','99999999','9999999','SAA','EIAM') ;
INSERT INTO S_EIAM_TIS.AA_USAC_user_grou_rtns (USAC_USER_GROU_RTNS_ID,SYST_ID,PRAF_NO,USER_GROU_ID,VLDT_YMD,EXPR_YMD,PUSE_YN,SYST_RGI_DT,SYST_RGI_PRAF_NO,SYST_RGI_OGNZ_NO,SYST_RGI_SYST_CD,SYST_RGI_PRGR_ID,SYST_CHG_DT,SYST_CHG_PRAF_NO,SYST_CHG_OGNZ_NO,SYST_CHG_SYST_CD,SYST_CHG_PRGR_ID) VALUES ('0000389410','0000000205','09861096','UG_0011306',TIMESTAMP '2022-04-19 00:00:00',TIMESTAMP '9999-12-31 00:00:00','Y',TIMESTAMP '2022-04-19 12:21:14','99999999','9999999','SAA','EIAM',TIMESTAMP '2022-04-19 12:21:14','99999999','9999999','SAA','EIAM') ;
INSERT INTO S_EIAM_TIS.AA_USAC_user_grou_rtns (USAC_USER_GROU_RTNS_ID,SYST_ID,PRAF_NO,USER_GROU_ID,VLDT_YMD,EXPR_YMD,PUSE_YN,SYST_RGI_DT,SYST_RGI_PRAF_NO,SYST_RGI_OGNZ_NO,SYST_RGI_SYST_CD,SYST_RGI_PRGR_ID,SYST_CHG_DT,SYST_CHG_PRAF_NO,SYST_CHG_OGNZ_NO,SYST_CHG_SYST_CD,SYST_CHG_PRGR_ID) VALUES ('0000391162','0000000205','09861196','UG_0011306',TIMESTAMP '2022-04-19 00:00:00',TIMESTAMP '9999-12-31 00:00:00','Y',TIMESTAMP '2022-04-19 12:21:14','99999999','9999999','SAA','EIAM',TIMESTAMP '2022-04-19 12:21:14','99999999','9999999','SAA','EIAM') ;
INSERT INTO S_EIAM_TIS.AA_USAC_user_grou_rtns (USAC_USER_GROU_RTNS_ID,SYST_ID,PRAF_NO,USER_GROU_ID,VLDT_YMD,EXPR_YMD,PUSE_YN,SYST_RGI_DT,SYST_RGI_PRAF_NO,SYST_RGI_OGNZ_NO,SYST_RGI_SYST_CD,SYST_RGI_PRGR_ID,SYST_CHG_DT,SYST_CHG_PRAF_NO,SYST_CHG_OGNZ_NO,SYST_CHG_SYST_CD,SYST_CHG_PRGR_ID) VALUES ('0000390709','0000000205','61005895','UG_0011306',TIMESTAMP '2022-04-19 00:00:00',TIMESTAMP '9999-12-31 00:00:00','Y',TIMESTAMP '2022-04-19 12:21:14','99999999','9999999','SAA','EIAM',TIMESTAMP '2022-04-19 12:21:14','99999999','9999999','SAA','EIAM') ;
INSERT INTO S_EIAM_TIS.AA_USAC_user_grou_rtns (USAC_USER_GROU_RTNS_ID,SYST_ID,PRAF_NO,USER_GROU_ID,VLDT_YMD,EXPR_YMD,PUSE_YN,SYST_RGI_DT,SYST_RGI_PRAF_NO,SYST_RGI_OGNZ_NO,SYST_RGI_SYST_CD,SYST_RGI_PRGR_ID,SYST_CHG_DT,SYST_CHG_PRAF_NO,SYST_CHG_OGNZ_NO,SYST_CHG_SYST_CD,SYST_CHG_PRGR_ID) VALUES ('0000391138','0000000205','09861197','UG_0011307',TIMESTAMP '2022-04-19 00:00:00',TIMESTAMP '9999-12-31 00:00:00','Y',TIMESTAMP '2022-04-19 12:21:14','99999999','9999999','SAA','EIAM',TIMESTAMP '2022-04-19 12:21:14','99999999','9999999','SAA','EIAM') ;
INSERT INTO S_EIAM_TIS.AA_USAC_user_grou_rtns (USAC_USER_GROU_RTNS_ID,SYST_ID,PRAF_NO,USER_GROU_ID,VLDT_YMD,EXPR_YMD,PUSE_YN,SYST_RGI_DT,SYST_RGI_PRAF_NO,SYST_RGI_OGNZ_NO,SYST_RGI_SYST_CD,SYST_RGI_PRGR_ID,SYST_CHG_DT,SYST_CHG_PRAF_NO,SYST_CHG_OGNZ_NO,SYST_CHG_SYST_CD,SYST_CHG_PRGR_ID) VALUES ('0000391142','0000000205','09861102','UG_0011306',TIMESTAMP '2022-04-19 00:00:00',TIMESTAMP '9999-12-31 00:00:00','Y',TIMESTAMP '2022-04-19 12:21:14','99999999','9999999','SAA','EIAM',TIMESTAMP '2022-04-19 12:21:14','99999999','9999999','SAA','EIAM') ;
INSERT INTO S_EIAM_TIS.AA_USAC_user_grou_rtns (USAC_USER_GROU_RTNS_ID,SYST_ID,PRAF_NO,USER_GROU_ID,VLDT_YMD,EXPR_YMD,PUSE_YN,SYST_RGI_DT,SYST_RGI_PRAF_NO,SYST_RGI_OGNZ_NO,SYST_RGI_SYST_CD,SYST_RGI_PRGR_ID,SYST_CHG_DT,SYST_CHG_PRAF_NO,SYST_CHG_OGNZ_NO,SYST_CHG_SYST_CD,SYST_CHG_PRGR_ID) VALUES ('0000389116','0000000205','09861101','UG_0011306',TIMESTAMP '2022-04-19 00:00:00',TIMESTAMP '9999-12-31 00:00:00','Y',TIMESTAMP '2022-04-19 12:21:14','99999999','9999999','SAA','EIAM',TIMESTAMP '2022-04-19 12:21:14','99999999','9999999','SAA','EIAM') ;
INSERT INTO S_EIAM_TIS.AA_USAC_user_grou_rtns (USAC_USER_GROU_RTNS_ID,SYST_ID,PRAF_NO,USER_GROU_ID,VLDT_YMD,EXPR_YMD,PUSE_YN,SYST_RGI_DT,SYST_RGI_PRAF_NO,SYST_RGI_OGNZ_NO,SYST_RGI_SYST_CD,SYST_RGI_PRGR_ID,SYST_CHG_DT,SYST_CHG_PRAF_NO,SYST_CHG_OGNZ_NO,SYST_CHG_SYST_CD,SYST_CHG_PRGR_ID) VALUES ('0000389667','0000000205','09861100','UG_0011307',TIMESTAMP '2022-04-19 00:00:00',TIMESTAMP '9999-12-31 00:00:00','Y',TIMESTAMP '2022-04-19 12:21:14','99999999','9999999','SAA','EIAM',TIMESTAMP '2022-04-19 12:21:14','99999999','9999999','SAA','EIAM') ;
INSERT INTO S_EIAM_TIS.AA_USAC_user_grou_rtns (USAC_USER_GROU_RTNS_ID,SYST_ID,PRAF_NO,USER_GROU_ID,VLDT_YMD,EXPR_YMD,PUSE_YN,SYST_RGI_DT,SYST_RGI_PRAF_NO,SYST_RGI_OGNZ_NO,SYST_RGI_SYST_CD,SYST_RGI_PRGR_ID,SYST_CHG_DT,SYST_CHG_PRAF_NO,SYST_CHG_OGNZ_NO,SYST_CHG_SYST_CD,SYST_CHG_PRGR_ID) VALUES ('0000389185','0000000205','61005895','UG_0011307',TIMESTAMP '2022-04-19 00:00:00',TIMESTAMP '9999-12-31 00:00:00','Y',TIMESTAMP '2022-04-19 12:21:14','99999999','9999999','SAA','EIAM',TIMESTAMP '2022-04-19 12:21:14','99999999','9999999','SAA','EIAM') ;
INSERT INTO S_EIAM_TIS.AA_USAC_user_grou_rtns (USAC_USER_GROU_RTNS_ID,SYST_ID,PRAF_NO,USER_GROU_ID,VLDT_YMD,EXPR_YMD,PUSE_YN,SYST_RGI_DT,SYST_RGI_PRAF_NO,SYST_RGI_OGNZ_NO,SYST_RGI_SYST_CD,SYST_RGI_PRGR_ID,SYST_CHG_DT,SYST_CHG_PRAF_NO,SYST_CHG_OGNZ_NO,SYST_CHG_SYST_CD,SYST_CHG_PRGR_ID) VALUES ('0000391098','0000000205','09861097','UG_0011307',TIMESTAMP '2022-04-19 00:00:00',TIMESTAMP '9999-12-31 00:00:00','Y',TIMESTAMP '2022-04-19 12:21:14','99999999','9999999','SAA','EIAM',TIMESTAMP '2022-04-19 12:21:14','99999999','9999999','SAA','EIAM') ;
INSERT INTO S_EIAM_TIS.AA_USAC_user_grou_rtns (USAC_USER_GROU_RTNS_ID,SYST_ID,PRAF_NO,USER_GROU_ID,VLDT_YMD,EXPR_YMD,PUSE_YN,SYST_RGI_DT,SYST_RGI_PRAF_NO,SYST_RGI_OGNZ_NO,SYST_RGI_SYST_CD,SYST_RGI_PRGR_ID,SYST_CHG_DT,SYST_CHG_PRAF_NO,SYST_CHG_OGNZ_NO,SYST_CHG_SYST_CD,SYST_CHG_PRGR_ID) VALUES ('0000388336','0000000205','09861097','UG_0011306',TIMESTAMP '2022-04-19 00:00:00',TIMESTAMP '9999-12-31 00:00:00','Y',TIMESTAMP '2022-04-19 12:21:14','99999999','9999999','SAA','EIAM',TIMESTAMP '2022-04-19 12:21:14','99999999','9999999','SAA','EIAM') ;
INSERT INTO S_EIAM_TIS.AA_USAC_user_grou_rtns (USAC_USER_GROU_RTNS_ID,SYST_ID,PRAF_NO,USER_GROU_ID,VLDT_YMD,EXPR_YMD,PUSE_YN,SYST_RGI_DT,SYST_RGI_PRAF_NO,SYST_RGI_OGNZ_NO,SYST_RGI_SYST_CD,SYST_RGI_PRGR_ID,SYST_CHG_DT,SYST_CHG_PRAF_NO,SYST_CHG_OGNZ_NO,SYST_CHG_SYST_CD,SYST_CHG_PRGR_ID) VALUES ('0000388550','0000000205','61005896','UG_0011307',TIMESTAMP '2022-04-19 00:00:00',TIMESTAMP '9999-12-31 00:00:00','Y',TIMESTAMP '2022-04-19 12:21:14','99999999','9999999','SAA','EIAM',TIMESTAMP '2022-04-19 12:21:14','99999999','9999999','SAA','EIAM') ;
INSERT INTO S_EIAM_TIS.AA_USAC_user_grou_rtns (USAC_USER_GROU_RTNS_ID,SYST_ID,PRAF_NO,USER_GROU_ID,VLDT_YMD,EXPR_YMD,PUSE_YN,SYST_RGI_DT,SYST_RGI_PRAF_NO,SYST_RGI_OGNZ_NO,SYST_RGI_SYST_CD,SYST_RGI_PRGR_ID,SYST_CHG_DT,SYST_CHG_PRAF_NO,SYST_CHG_OGNZ_NO,SYST_CHG_SYST_CD,SYST_CHG_PRGR_ID) VALUES ('0000388825','0000000205','61005872','UG_0011307',TIMESTAMP '2022-04-19 00:00:00',TIMESTAMP '9999-12-31 00:00:00','Y',TIMESTAMP '2022-04-19 12:21:14','99999999','9999999','SAA','EIAM',TIMESTAMP '2022-04-19 12:21:14','99999999','9999999','SAA','EIAM') ;
INSERT INTO S_EIAM_TIS.AA_USAC_user_grou_rtns (USAC_USER_GROU_RTNS_ID,SYST_ID,PRAF_NO,USER_GROU_ID,VLDT_YMD,EXPR_YMD,PUSE_YN,SYST_RGI_DT,SYST_RGI_PRAF_NO,SYST_RGI_OGNZ_NO,SYST_RGI_SYST_CD,SYST_RGI_PRGR_ID,SYST_CHG_DT,SYST_CHG_PRAF_NO,SYST_CHG_OGNZ_NO,SYST_CHG_SYST_CD,SYST_CHG_PRGR_ID) VALUES ('0000388839','0000000205','61005886','UG_0011306',TIMESTAMP '2022-04-19 00:00:00',TIMESTAMP '9999-12-31 00:00:00','Y',TIMESTAMP '2022-04-19 12:21:14','99999999','9999999','SAA','EIAM',TIMESTAMP '2022-04-19 12:21:14','99999999','9999999','SAA','EIAM') ;
INSERT INTO S_EIAM_TIS.AA_USAC_user_grou_rtns (USAC_USER_GROU_RTNS_ID,SYST_ID,PRAF_NO,USER_GROU_ID,VLDT_YMD,EXPR_YMD,PUSE_YN,SYST_RGI_DT,SYST_RGI_PRAF_NO,SYST_RGI_OGNZ_NO,SYST_RGI_SYST_CD,SYST_RGI_PRGR_ID,SYST_CHG_DT,SYST_CHG_PRAF_NO,SYST_CHG_OGNZ_NO,SYST_CHG_SYST_CD,SYST_CHG_PRGR_ID) VALUES ('0000388149','0000000205','61005873','UG_0011306',TIMESTAMP '2022-04-19 00:00:00',TIMESTAMP '9999-12-31 00:00:00','Y',TIMESTAMP '2022-04-19 12:21:14','99999999','9999999','SAA','EIAM',TIMESTAMP '2022-04-19 12:21:14','99999999','9999999','SAA','EIAM') ;
INSERT INTO S_EIAM_TIS.AA_USAC_user_grou_rtns (USAC_USER_GROU_RTNS_ID,SYST_ID,PRAF_NO,USER_GROU_ID,VLDT_YMD,EXPR_YMD,PUSE_YN,SYST_RGI_DT,SYST_RGI_PRAF_NO,SYST_RGI_OGNZ_NO,SYST_RGI_SYST_CD,SYST_RGI_PRGR_ID,SYST_CHG_DT,SYST_CHG_PRAF_NO,SYST_CHG_OGNZ_NO,SYST_CHG_SYST_CD,SYST_CHG_PRGR_ID) VALUES ('0000388081','0000000205','09861096','UG_0011307',TIMESTAMP '2022-04-19 00:00:00',TIMESTAMP '9999-12-31 00:00:00','Y',TIMESTAMP '2022-04-19 12:21:14','99999999','9999999','SAA','EIAM',TIMESTAMP '2022-04-19 12:21:14','99999999','9999999','SAA','EIAM') ;
INSERT INTO S_EIAM_TIS.AA_USAC_user_grou_rtns (USAC_USER_GROU_RTNS_ID,SYST_ID,PRAF_NO,USER_GROU_ID,VLDT_YMD,EXPR_YMD,PUSE_YN,SYST_RGI_DT,SYST_RGI_PRAF_NO,SYST_RGI_OGNZ_NO,SYST_RGI_SYST_CD,SYST_RGI_PRGR_ID,SYST_CHG_DT,SYST_CHG_PRAF_NO,SYST_CHG_OGNZ_NO,SYST_CHG_SYST_CD,SYST_CHG_PRGR_ID) VALUES ('0000388097','0000000205','09861102','UG_0011307',TIMESTAMP '2022-04-19 00:00:00',TIMESTAMP '9999-12-31 00:00:00','Y',TIMESTAMP '2022-04-19 12:21:14','99999999','9999999','SAA','EIAM',TIMESTAMP '2022-04-19 12:21:14','99999999','9999999','SAA','EIAM') ;
INSERT INTO S_EIAM_TIS.AA_USAC_user_grou_rtns (USAC_USER_GROU_RTNS_ID,SYST_ID,PRAF_NO,USER_GROU_ID,VLDT_YMD,EXPR_YMD,PUSE_YN,SYST_RGI_DT,SYST_RGI_PRAF_NO,SYST_RGI_OGNZ_NO,SYST_RGI_SYST_CD,SYST_RGI_PRGR_ID,SYST_CHG_DT,SYST_CHG_PRAF_NO,SYST_CHG_OGNZ_NO,SYST_CHG_SYST_CD,SYST_CHG_PRGR_ID) VALUES ('0000388116','0000000205','61005905','UG_0011307',TIMESTAMP '2022-04-19 00:00:00',TIMESTAMP '9999-12-31 00:00:00','Y',TIMESTAMP '2022-04-19 12:21:14','99999999','9999999','SAA','EIAM',TIMESTAMP '2022-04-19 12:21:14','99999999','9999999','SAA','EIAM') ;
INSERT INTO S_EIAM_TIS.AA_USAC_user_grou_rtns (USAC_USER_GROU_RTNS_ID,SYST_ID,PRAF_NO,USER_GROU_ID,VLDT_YMD,EXPR_YMD,PUSE_YN,SYST_RGI_DT,SYST_RGI_PRAF_NO,SYST_RGI_OGNZ_NO,SYST_RGI_SYST_CD,SYST_RGI_PRGR_ID,SYST_CHG_DT,SYST_CHG_PRAF_NO,SYST_CHG_OGNZ_NO,SYST_CHG_SYST_CD,SYST_CHG_PRGR_ID) VALUES ('0000388229','0000000205','61005899','UG_0011307',TIMESTAMP '2022-04-19 00:00:00',TIMESTAMP '9999-12-31 00:00:00','Y',TIMESTAMP '2022-04-19 12:21:14','99999999','9999999','SAA','EIAM',TIMESTAMP '2022-04-19 12:21:14','99999999','9999999','SAA','EIAM') ;
INSERT INTO S_EIAM_TIS.AA_USAC_user_grou_rtns (USAC_USER_GROU_RTNS_ID,SYST_ID,PRAF_NO,USER_GROU_ID,VLDT_YMD,EXPR_YMD,PUSE_YN,SYST_RGI_DT,SYST_RGI_PRAF_NO,SYST_RGI_OGNZ_NO,SYST_RGI_SYST_CD,SYST_RGI_PRGR_ID,SYST_CHG_DT,SYST_CHG_PRAF_NO,SYST_CHG_OGNZ_NO,SYST_CHG_SYST_CD,SYST_CHG_PRGR_ID) VALUES ('0000389054','0000000205','09861197','UG_0011306',TIMESTAMP '2022-04-19 00:00:00',TIMESTAMP '9999-12-31 00:00:00','Y',TIMESTAMP '2022-04-19 12:21:14','99999999','9999999','SAA','EIAM',TIMESTAMP '2022-04-19 12:21:14','99999999','9999999','SAA','EIAM') ;
INSERT INTO S_EIAM_TIS.AA_USAC_user_grou_rtns (USAC_USER_GROU_RTNS_ID,SYST_ID,PRAF_NO,USER_GROU_ID,VLDT_YMD,EXPR_YMD,PUSE_YN,SYST_RGI_DT,SYST_RGI_PRAF_NO,SYST_RGI_OGNZ_NO,SYST_RGI_SYST_CD,SYST_RGI_PRGR_ID,SYST_CHG_DT,SYST_CHG_PRAF_NO,SYST_CHG_OGNZ_NO,SYST_CHG_SYST_CD,SYST_CHG_PRGR_ID) VALUES ('0000389005','0000000205','09861302','UG_0011307',TIMESTAMP '2022-04-19 00:00:00',TIMESTAMP '9999-12-31 00:00:00','Y',TIMESTAMP '2022-04-19 12:21:14','99999999','9999999','SAA','EIAM',TIMESTAMP '2022-04-19 12:21:14','99999999','9999999','SAA','EIAM') ;
INSERT INTO S_EIAM_TIS.AA_USAC_user_grou_rtns (USAC_USER_GROU_RTNS_ID,SYST_ID,PRAF_NO,USER_GROU_ID,VLDT_YMD,EXPR_YMD,PUSE_YN,SYST_RGI_DT,SYST_RGI_PRAF_NO,SYST_RGI_OGNZ_NO,SYST_RGI_SYST_CD,SYST_RGI_PRGR_ID,SYST_CHG_DT,SYST_CHG_PRAF_NO,SYST_CHG_OGNZ_NO,SYST_CHG_SYST_CD,SYST_CHG_PRGR_ID) VALUES ('0000390044','0000000205','09861101','UG_0011307',TIMESTAMP '2022-04-19 00:00:00',TIMESTAMP '9999-12-31 00:00:00','Y',TIMESTAMP '2022-04-19 12:21:14','99999999','9999999','SAA','EIAM',TIMESTAMP '2022-04-19 12:21:14','99999999','9999999','SAA','EIAM') ;
INSERT INTO S_EIAM_TIS.AA_USAC_user_grou_rtns (USAC_USER_GROU_RTNS_ID,SYST_ID,PRAF_NO,USER_GROU_ID,VLDT_YMD,EXPR_YMD,PUSE_YN,SYST_RGI_DT,SYST_RGI_PRAF_NO,SYST_RGI_OGNZ_NO,SYST_RGI_SYST_CD,SYST_RGI_PRGR_ID,SYST_CHG_DT,SYST_CHG_PRAF_NO,SYST_CHG_OGNZ_NO,SYST_CHG_SYST_CD,SYST_CHG_PRGR_ID) VALUES ('0000389007','0000000205','61005898','UG_0011307',TIMESTAMP '2022-04-19 00:00:00',TIMESTAMP '9999-12-31 00:00:00','Y',TIMESTAMP '2022-04-19 12:21:14','99999999','9999999','SAA','EIAM',TIMESTAMP '2022-04-19 12:21:14','99999999','9999999','SAA','EIAM') ;
INSERT INTO S_EIAM_TIS.AA_USAC (PRAF_NO,PRAF_NM,OGNZ_NO,ADDRE,PRAF_OFDU_CD,PRAF_OFDU_NM,PRAF_OFLE_CD,PRAF_OFLE_NM,PRAF_DUTY_CD,PRAF_DUTY_NM,PUSE_YN,SYST_RGI_DT,SYST_RGI_PRAF_NO,SYST_RGI_OGNZ_NO,SYST_RGI_SYST_CD,SYST_RGI_PRGR_ID,SYST_CHG_DT,SYST_CHG_PRAF_NO,SYST_CHG_OGNZ_NO,SYST_CHG_SYST_CD,SYST_CHG_PRGR_ID) VALUES ('09861101','김정우','0993129','ZnNnIt2Vpx0VuIFtTTC4X-fRMEUZVxibJyigyjw58Ms=','N08550701','영업지원시스템기획-운영','80SR','Sr','810135','챕터원','Y',TIMESTAMP '2021-04-21 05:04:04','99999999','9999999','SAA','0000000002',TIMESTAMP '2025-07-01 12:34:37','99999999','9999999','SAA','EIAM') ;
INSERT INTO S_EIAM_TIS.AA_USAC (PRAF_NO,PRAF_NM,OGNZ_NO,ADDRE,PRAF_OFDU_CD,PRAF_OFDU_NM,PRAF_OFLE_CD,PRAF_OFLE_NM,PRAF_DUTY_CD,PRAF_DUTY_NM,PUSE_YN,SYST_RGI_DT,SYST_RGI_PRAF_NO,SYST_RGI_OGNZ_NO,SYST_RGI_SYST_CD,SYST_RGI_PRGR_ID,SYST_CHG_DT,SYST_CHG_PRAF_NO,SYST_CHG_OGNZ_NO,SYST_CHG_SYST_CD,SYST_CHG_PRGR_ID) VALUES ('09861102','한준영','0999200','hIX9bJ9IISfan3Gv8RU3huipr7otRghkLqlNgyw8ZVc=','N08580303','내부통제','80SR','Sr','810030','팀원','Y',TIMESTAMP '2021-04-21 05:04:08','99999999','9999999','SAA','0000000002',TIMESTAMP '2023-04-01 12:31:16','99999999','9999999','SAA','EIAM' ) ;
INSERT INTO S_EIAM_TIS.AA_USAC (PRAF_NO,PRAF_NM,OGNZ_NO,ADDRE,PRAF_OFDU_CD,PRAF_OFDU_NM,PRAF_OFLE_CD,PRAF_OFLE_NM,PRAF_DUTY_CD,PRAF_DUTY_NM,PUSE_YN,SYST_RGI_DT,SYST_RGI_PRAF_NO,SYST_RGI_OGNZ_NO,SYST_RGI_SYST_CD,SYST_RGI_PRGR_ID,SYST_CHG_DT,SYST_CHG_PRAF_NO,SYST_CHG_OGNZ_NO,SYST_CHG_SYST_CD,SYST_CHG_PRGR_ID) VALUES ('09861197','최원진','0995400','QhUqiS4UN6Byc+Z9n7xs8AzRTe4FeArLtMyNHtGGi10=','N08540202','계약심사','80SR','Sr','810030','팀원','Y',TIMESTAMP '2021-04-21 05:04:59','99999999','9999999','SAA','0000000002',TIMESTAMP '2023-04-01 12:30:18','99999999','9999999','SAA','EIAM' ) ;
INSERT INTO S_EIAM_TIS.AA_USAC (PRAF_NO,PRAF_NM,OGNZ_NO,ADDRE,PRAF_OFDU_CD,PRAF_OFDU_NM,PRAF_OFLE_CD,PRAF_OFLE_NM,PRAF_DUTY_CD,PRAF_DUTY_NM,PUSE_YN,SYST_RGI_DT,SYST_RGI_PRAF_NO,SYST_RGI_OGNZ_NO,SYST_RGI_SYST_CD,SYST_RGI_PRGR_ID,SYST_CHG_DT,SYST_CHG_PRAF_NO,SYST_CHG_OGNZ_NO,SYST_CHG_SYST_CD,SYST_CHG_PRGR_ID) VALUES ('09861096','이석진','0995146','WfydcrzCqws+QIilkz13iXWxHeXTgZ+knGEvxuzUFjE=','N08620101','고객서비스 업무지원','80SR','Sr','810114','CS지원','Y',TIMESTAMP '2021-04-21 05:04:59','99999999','9999999','SAA','0000000002',TIMESTAMP '2025-05-01 12:34:43','99999999','9999999','SAA','EIAM' ) ;
INSERT INTO S_EIAM_TIS.AA_USAC (PRAF_NO,PRAF_NM,OGNZ_NO,ADDRE,PRAF_OFDU_CD,PRAF_OFDU_NM,PRAF_OFLE_CD,PRAF_OFLE_NM,PRAF_DUTY_CD,PRAF_DUTY_NM,PUSE_YN,SYST_RGI_DT,SYST_RGI_PRAF_NO,SYST_RGI_OGNZ_NO,SYST_RGI_SYST_CD,SYST_RGI_PRGR_ID,SYST_CHG_DT,SYST_CHG_PRAF_NO,SYST_CHG_OGNZ_NO,SYST_CHG_SYST_CD,SYST_CHG_PRGR_ID) VALUES ('09861097','김남중','0993203','wd12hSaDEPA5UbaPxrPaCZqHO-w23cd7WLhkzHz01+s=','N08550803','제휴마케팅기획관리','80SR','Sr','810030','팀원','Y',TIMESTAMP '2021-04-21 05:04:10','99999999','9999999','SAA','0000000002',TIMESTAMP '2024-10-10 15:41:26','99999999','9999999','SAA','EIAM' ) ;
INSERT INTO S_EIAM_TIS.AA_USAC (PRAF_NO,PRAF_NM,OGNZ_NO,ADDRE,PRAF_OFDU_CD,PRAF_OFDU_NM,PRAF_OFLE_CD,PRAF_OFLE_NM,PRAF_DUTY_CD,PRAF_DUTY_NM,PUSE_YN,SYST_RGI_DT,SYST_RGI_PRAF_NO,SYST_RGI_OGNZ_NO,SYST_RGI_SYST_CD,SYST_RGI_PRGR_ID,SYST_CHG_DT,SYST_CHG_PRAF_NO,SYST_CHG_OGNZ_NO,SYST_CHG_SYST_CD,SYST_CHG_PRGR_ID) VALUES ('09861194','윤승로','0993134','XuVpEqWforwy4JgbLOOO7Qc3OTMM2uXFucCXgXAKPec=','N08550209','FC교육운영','80SR','Sr','810030','팀원','Y',TIMESTAMP '2021-04-21 05:04:59','99999999','9999999','SAA','0000000002',TIMESTAMP '2024-11-01 12:41:52','99999999','9999999','SAA','EIAM' ) ;
INSERT INTO S_EIAM_TIS.AA_USAC (PRAF_NO,PRAF_NM,OGNZ_NO,ADDRE,PRAF_OFDU_CD,PRAF_OFDU_NM,PRAF_OFLE_CD,PRAF_OFLE_NM,PRAF_DUTY_CD,PRAF_DUTY_NM,PUSE_YN,SYST_RGI_DT,SYST_RGI_PRAF_NO,SYST_RGI_OGNZ_NO,SYST_RGI_SYST_CD,SYST_RGI_PRGR_ID,SYST_CHG_DT,SYST_CHG_PRAF_NO,SYST_CHG_OGNZ_NO,SYST_CHG_SYST_CD,SYST_CHG_PRGR_ID) VALUES ('09861303','박범진','0999200','MTa1Jvmkzhwk+-5cnsORvnXAwLCyWYs5tiPa9xgVrPQ=','N08550301','GA채널기획관리','80MST','Mst','810122','파트원','Y',TIMESTAMP '2021-04-21 05:04:01','99999999','9999999','SAA','0000000002',TIMESTAMP '2025-01-01 12:31:03','99999999','9999999','SAA','EIAM' ) ;
INSERT INTO S_EIAM_TIS.AA_USAC (PRAF_NO,PRAF_NM,OGNZ_NO,ADDRE,PRAF_OFDU_CD,PRAF_OFDU_NM,PRAF_OFLE_CD,PRAF_OFLE_NM,PRAF_DUTY_CD,PRAF_DUTY_NM,PUSE_YN,SYST_RGI_DT,SYST_RGI_PRAF_NO,SYST_RGI_OGNZ_NO,SYST_RGI_SYST_CD,SYST_RGI_PRGR_ID,SYST_CHG_DT,SYST_CHG_PRAF_NO,SYST_CHG_OGNZ_NO,SYST_CHG_SYST_CD,SYST_CHG_PRGR_ID) VALUES ('09861302','문준호','0995400','XafkrGHaWLTxackt7T2NsjCAUXBF3uyjMNAXVyUSvC4=','N08560301','소비자보호기획','80SR','Sr','810122','파트원','Y',TIMESTAMP '2021-04-21 05:04:08','99999999','9999999','SAA','0000000002',TIMESTAMP '2025-02-01 12:30:54','99999999','9999999','SAA','EIAM' ) ;
INSERT INTO S_EIAM_TIS.AA_USAC (PRAF_NO,PRAF_NM,OGNZ_NO,ADDRE,PRAF_OFDU_CD,PRAF_OFDU_NM,PRAF_OFLE_CD,PRAF_OFLE_NM,PRAF_DUTY_CD,PRAF_DUTY_NM,PUSE_YN,SYST_RGI_DT,SYST_RGI_PRAF_NO,SYST_RGI_OGNZ_NO,SYST_RGI_SYST_CD,SYST_RGI_PRGR_ID,SYST_CHG_DT,SYST_CHG_PRAF_NO,SYST_CHG_OGNZ_NO,SYST_CHG_SYST_CD,SYST_CHG_PRGR_ID) VALUES ('09861100','윤희준','0993203','X-4VwtIV1YCBR6+0UqZBVHqk9GpqHy3AMDJYKihWQhk=','N08560101','계약관리','80SR','Sr','810122','파트원','Y',TIMESTAMP '2021-04-21 05:04:04','99999999','9999999','SAA','0000000002',TIMESTAMP '2025-02-01 12:30:55','99999999','9999999','SAA','EIAM' ) ;
INSERT INTO S_EIAM_TIS.AA_USAC (PRAF_NO,PRAF_NM,OGNZ_NO,ADDRE,PRAF_OFDU_CD,PRAF_OFDU_NM,PRAF_OFLE_CD,PRAF_OFLE_NM,PRAF_DUTY_CD,PRAF_DUTY_NM,PUSE_YN,SYST_RGI_DT,SYST_RGI_PRAF_NO,SYST_RGI_OGNZ_NO,SYST_RGI_SYST_CD,SYST_RGI_PRGR_ID,SYST_CHG_DT,SYST_CHG_PRAF_NO,SYST_CHG_OGNZ_NO,SYST_CHG_SYST_CD,SYST_CHG_PRGR_ID) VALUES ('09861195','박준영','0993203','tyg5izJmKkXYqIa6YfKp0Y42yIUjrhY+-6hgYZ8Vtmo=','N08510105','부서총괄','80MGR','Mgr','810015','팀장','Y',TIMESTAMP '2021-04-21 05:04:02','99999999','9999999','SAA','0000000002',TIMESTAMP '2025-01-01 12:30:55','99999999','9999999','SAA','EIAM' ) ;
INSERT INTO S_EIAM_TIS.AA_USAC (PRAF_NO,PRAF_NM,OGNZ_NO,ADDRE,PRAF_OFDU_CD,PRAF_OFDU_NM,PRAF_OFLE_CD,PRAF_OFLE_NM,PRAF_DUTY_CD,PRAF_DUTY_NM,PUSE_YN,SYST_RGI_DT,SYST_RGI_PRAF_NO,SYST_RGI_OGNZ_NO,SYST_RGI_SYST_CD,SYST_RGI_PRGR_ID,SYST_CHG_DT,SYST_CHG_PRAF_NO,SYST_CHG_OGNZ_NO,SYST_CHG_SYST_CD,SYST_CHG_PRGR_ID) VALUES ('09861196','정재용','0993129','wPIIdQ3PwXlj5fxgtjWzkZs4yagmbAC9kDU5dNq6sKw=','N08520101','고객DB운영','80SR','Sr','810135','챕터원','Y',TIMESTAMP '2021-04-21 05:04:04','99999999','9999999','SAA','0000000002',TIMESTAMP '2024-10-10 15:41:11','99999999','9999999','SAA','EIAM' ) ;
INSERT INTO S_EIAM_TIS.AA_USAC (PRAF_NO,PRAF_NM,OGNZ_NO,ADDRE,PRAF_OFDU_CD,PRAF_OFDU_NM,PRAF_OFLE_CD,PRAF_OFLE_NM,PRAF_DUTY_CD,PRAF_DUTY_NM,PUSE_YN,SYST_RGI_DT,SYST_RGI_PRAF_NO,SYST_RGI_OGNZ_NO,SYST_RGI_SYST_CD,SYST_RGI_PRGR_ID,SYST_CHG_DT,SYST_CHG_PRAF_NO,SYST_CHG_OGNZ_NO,SYST_CHG_SYST_CD,SYST_CHG_PRGR_ID) VALUES ('61005872','동영예','0993129','bgHjYQyZochoxQXjI2stZf4dmHANf7r514SxeGnHJL0=','N08550203','FC채널지원','80SR','Sr','810030','팀원','Y',TIMESTAMP '2021-04-21 05:04:04','99999999','9999999','SAA','0000000002',TIMESTAMP '2025-08-01 12:39:02','99999999','9999999','SAA','EIAM' ) ;
INSERT INTO S_EIAM_TIS.AA_USAC (PRAF_NO,PRAF_NM,OGNZ_NO,ADDRE,PRAF_OFDU_CD,PRAF_OFDU_NM,PRAF_OFLE_CD,PRAF_OFLE_NM,PRAF_DUTY_CD,PRAF_DUTY_NM,PUSE_YN,SYST_RGI_DT,SYST_RGI_PRAF_NO,SYST_RGI_OGNZ_NO,SYST_RGI_SYST_CD,SYST_RGI_PRGR_ID,SYST_CHG_DT,SYST_CHG_PRAF_NO,SYST_CHG_OGNZ_NO,SYST_CHG_SYST_CD,SYST_CHG_PRGR_ID) VALUES ('61005873','동한름','0993129','Vip4QCNT4HYfB9Pa86g7XsEw+LBu02H5BlbDo-W7eQw=','N08580101','내부감사','80SR','Sr','810030','팀원','Y',TIMESTAMP '2021-04-21 05:04:05','99999999','9999999','SAA','0000000002',TIMESTAMP '2024-10-10 15:41:14','99999999','9999999','SAA','EIAM' ) ;
INSERT INTO S_EIAM_TIS.AA_USAC (PRAF_NO,PRAF_NM,OGNZ_NO,ADDRE,PRAF_OFDU_CD,PRAF_OFDU_NM,PRAF_OFLE_CD,PRAF_OFLE_NM,PRAF_DUTY_CD,PRAF_DUTY_NM,PUSE_YN,SYST_RGI_DT,SYST_RGI_PRAF_NO,SYST_RGI_OGNZ_NO,SYST_RGI_SYST_CD,SYST_RGI_PRGR_ID,SYST_CHG_DT,SYST_CHG_PRAF_NO,SYST_CHG_OGNZ_NO,SYST_CHG_SYST_CD,SYST_CHG_PRGR_ID) VALUES ('61005886','빈길들','0993134','yPGgtuE14m+po6oyB9CfxEIilfrEzPxfi-xN0LXG4t8=','N08560302','고객민원관리','80SR','Sr','810122','파트원','Y',TIMESTAMP '2021-04-21 05:04:04','99999999','9999999','SAA','0000000002',TIMESTAMP '2025-02-01 12:31:08','99999999','9999999','SAA','EIAM' ) ;
INSERT INTO S_EIAM_TIS.AA_USAC (PRAF_NO,PRAF_NM,OGNZ_NO,ADDRE,PRAF_OFDU_CD,PRAF_OFDU_NM,PRAF_OFLE_CD,PRAF_OFLE_NM,PRAF_DUTY_CD,PRAF_DUTY_NM,PUSE_YN,SYST_RGI_DT,SYST_RGI_PRAF_NO,SYST_RGI_OGNZ_NO,SYST_RGI_SYST_CD,SYST_RGI_PRGR_ID,SYST_CHG_DT,SYST_CHG_PRAF_NO,SYST_CHG_OGNZ_NO,SYST_CHG_SYST_CD,SYST_CHG_PRGR_ID) VALUES ('61005895','오해나','0993134','YNscG5QLZ1B8KIrBUIeGlK8otbz94A1RJdhh8mRNlGA=','N08900804','육아휴직','80SR','Sr','810095','기타','Y',TIMESTAMP '2021-04-21 05:04:03','99999999','9999999','SAA','0000000002',TIMESTAMP '2025-09-01 12:34:52','99999999','9999999','SAA','EIAM' ) ;
INSERT INTO S_EIAM_TIS.AA_USAC (PRAF_NO,PRAF_NM,OGNZ_NO,ADDRE,PRAF_OFDU_CD,PRAF_OFDU_NM,PRAF_OFLE_CD,PRAF_OFLE_NM,PRAF_DUTY_CD,PRAF_DUTY_NM,PUSE_YN,SYST_RGI_DT,SYST_RGI_PRAF_NO,SYST_RGI_OGNZ_NO,SYST_RGI_SYST_CD,SYST_RGI_PRGR_ID,SYST_CHG_DT,SYST_CHG_PRAF_NO,SYST_CHG_OGNZ_NO,SYST_CHG_SYST_CD,SYST_CHG_PRGR_ID) VALUES ('61005896','동복수','0993134','BMRwD1vs0bmNdZBBs5VBMq1V0OUTH7AeQdKHwUQvQqw=','N08540302','보험금심사기획-지원','80MGR','Mgr','810030','팀원','Y',TIMESTAMP '2021-04-21 05:04:05','99999999','9999999','SAA','0000000002',TIMESTAMP '2025-06-01 12:31:09','99999999','9999999','SAA','EIAM' ) ;
INSERT INTO S_EIAM_TIS.AA_USAC (PRAF_NO,PRAF_NM,OGNZ_NO,ADDRE,PRAF_OFDU_CD,PRAF_OFDU_NM,PRAF_OFLE_CD,PRAF_OFLE_NM,PRAF_DUTY_CD,PRAF_DUTY_NM,PUSE_YN,SYST_RGI_DT,SYST_RGI_PRAF_NO,SYST_RGI_OGNZ_NO,SYST_RGI_SYST_CD,SYST_RGI_PRGR_ID,SYST_CHG_DT,SYST_CHG_PRAF_NO,SYST_CHG_OGNZ_NO,SYST_CHG_SYST_CD,SYST_CHG_PRGR_ID) VALUES ('61005898','동권문','0995400','jyBPF4WMlcE8On8jG9XiWYlV92t5f1MkGKRe2N+xZ+8=','N08540202','계약심사','80SR','Sr','810030','팀원','Y',TIMESTAMP '2021-04-21 05:04:05','99999999','9999999','SAA','0000000002',TIMESTAMP '2025-01-01 12:31:51','99999999','9999999','SAA','EIAM' ) ;
INSERT INTO S_EIAM_TIS.AA_USAC (PRAF_NO,PRAF_NM,OGNZ_NO,ADDRE,PRAF_OFDU_CD,PRAF_OFDU_NM,PRAF_OFLE_CD,PRAF_OFLE_NM,PRAF_DUTY_CD,PRAF_DUTY_NM,PUSE_YN,SYST_RGI_DT,SYST_RGI_PRAF_NO,SYST_RGI_OGNZ_NO,SYST_RGI_SYST_CD,SYST_RGI_PRGR_ID,SYST_CHG_DT,SYST_CHG_PRAF_NO,SYST_CHG_OGNZ_NO,SYST_CHG_SYST_CD,SYST_CHG_PRGR_ID) VALUES ('61005899','심복','0995400','pH9Da+rtRLXLu9oKplGsqvcrF5rL5N8ZVfInlRMa-Vs=','N08550302','GA채널지원','80SR','Sr','810030','팀원','Y',TIMESTAMP '2021-04-21 05:04:02','99999999','9999999','SAA','0000000002',TIMESTAMP '2025-01-01 12:31:11','99999999','9999999','SAA','EIAM' ) ;
INSERT INTO S_EIAM_TIS.AA_USAC (PRAF_NO,PRAF_NM,OGNZ_NO,ADDRE,PRAF_OFDU_CD,PRAF_OFDU_NM,PRAF_OFLE_CD,PRAF_OFLE_NM,PRAF_DUTY_CD,PRAF_DUTY_NM,PUSE_YN,SYST_RGI_DT,SYST_RGI_PRAF_NO,SYST_RGI_OGNZ_NO,SYST_RGI_SYST_CD,SYST_RGI_PRGR_ID,SYST_CHG_DT,SYST_CHG_PRAF_NO,SYST_CHG_OGNZ_NO,SYST_CHG_SYST_CD,SYST_CHG_PRGR_ID) VALUES ('61005905','모중제','0995400','+GR4ai78TrPdOiSDkJ+SbjCiwWQfO9QbSdKD7Kydpis=','N08560507','고객서비스지원','80MGR','Mgr','810122','파트원','Y',TIMESTAMP '2021-04-21 05:04:09','99999999','9999999','SAA','0000000002',TIMESTAMP '2024-10-10 15:46:12','99999999','9999999','SAA','EIAM' ) ;
INSERT INTO S_EIAM_TIS.AA_OGNZ (OGNZ_NM, OGNZ_ABR_NM, OGNZ_NO, SPPO_GONZ_NO, VLDT_YMD, PUSE_YN, SYST_RGI_DT, SYST_RGI_PRAF_NO, SYST_RGI_OGNZ_NO, SYST_RGI_SYST_CD, SYST_RGI_PRGR_ID, SYST_CHG_DT, SYST_CHG_PRAF_NO, SYST_CHG_OGNZ_NO, SYST_CHG_SYST_CD, SYST_CHG_PRGR_ID) VALUES ('총무팀', '총무팀', '0993129', 'shinhanlife@shinhanlife.com', '20260101', 'Y', TIMESTAMP '2023-01-01 12:00:00', 'A1234567', '7654321', 'SYS', 'PROG001', TIMESTAMP '2023-01-01 12:00:00', 'A1234567', '7654321', 'SYS', 'PROG001');
INSERT INTO S_EIAM_TIS.AA_OGNZ (OGNZ_NM, OGNZ_ABR_NM, OGNZ_NO, SPPO_GONZ_NO, VLDT_YMD, PUSE_YN, SYST_RGI_DT, SYST_RGI_PRAF_NO, SYST_RGI_OGNZ_NO, SYST_RGI_SYST_CD, SYST_RGI_PRGR_ID, SYST_CHG_DT, SYST_CHG_PRAF_NO, SYST_CHG_OGNZ_NO, SYST_CHG_SYST_CD, SYST_CHG_PRGR_ID) VALUES ('재무팀', '재무팀', '0999200', 'shinhanlife@shinhanlife.com', '20260101', 'Y', TIMESTAMP '2023-01-01 12:00:00', 'A1234567', '7654321', 'SYS', 'PROG001', TIMESTAMP '2023-01-01 12:00:00', 'A1234567', '7654321', 'SYS', 'PROG001');
INSERT INTO S_EIAM_TIS.AA_OGNZ (OGNZ_NM, OGNZ_ABR_NM, OGNZ_NO, SPPO_GONZ_NO, VLDT_YMD, PUSE_YN, SYST_RGI_DT, SYST_RGI_PRAF_NO, SYST_RGI_OGNZ_NO, SYST_RGI_SYST_CD, SYST_RGI_PRGR_ID, SYST_CHG_DT, SYST_CHG_PRAF_NO, SYST_CHG_OGNZ_NO, SYST_CHG_SYST_CD, SYST_CHG_PRGR_ID) VALUES ('개발팀', '개발팀', '0995400', 'shinhanlife@shinhanlife.com', '20260101', 'Y', TIMESTAMP '2023-01-01 12:00:00', 'A1234567', '7654321', 'SYS', 'PROG001', TIMESTAMP '2023-01-01 12:00:00', 'A1234567', '7654321', 'SYS', 'PROG001');
INSERT INTO S_EIAM_TIS.AA_OGNZ (OGNZ_NM, OGNZ_ABR_NM, OGNZ_NO, SPPO_GONZ_NO, VLDT_YMD, PUSE_YN, SYST_RGI_DT, SYST_RGI_PRAF_NO, SYST_RGI_OGNZ_NO, SYST_RGI_SYST_CD, SYST_RGI_PRGR_ID, SYST_CHG_DT, SYST_CHG_PRAF_NO, SYST_CHG_OGNZ_NO, SYST_CHG_SYST_CD, SYST_CHG_PRGR_ID) VALUES ('구매팀', '구매팀', '0995146', 'shinhanlife@shinhanlife.com', '20260101', 'Y', TIMESTAMP '2023-01-01 12:00:00', 'A1234567', '7654321', 'SYS', 'PROG001', TIMESTAMP '2023-01-01 12:00:00', 'A1234567', '7654321', 'SYS', 'PROG001');
INSERT INTO S_EIAM_TIS.AA_OGNZ (OGNZ_NM, OGNZ_ABR_NM, OGNZ_NO, SPPO_GONZ_NO, VLDT_YMD, PUSE_YN, SYST_RGI_DT, SYST_RGI_PRAF_NO, SYST_RGI_OGNZ_NO, SYST_RGI_SYST_CD, SYST_RGI_PRGR_ID, SYST_CHG_DT, SYST_CHG_PRAF_NO, SYST_CHG_OGNZ_NO, SYST_CHG_SYST_CD, SYST_CHG_PRGR_ID) VALUES ('AI사업부', 'AI사업부', '0993203', 'shinhanlife@shinhanlife.com', '20260101', 'Y', TIMESTAMP '2023-01-01 12:00:00', 'A1234567', '7654321', 'SYS', 'PROG001', TIMESTAMP '2023-01-01 12:00:00', 'A1234567', '7654321', 'SYS', 'PROG001');
INSERT INTO S_EIAM_TIS.AA_OGNZ (OGNZ_NM, OGNZ_ABR_NM, OGNZ_NO, SPPO_GONZ_NO, VLDT_YMD, PUSE_YN, SYST_RGI_DT, SYST_RGI_PRAF_NO, SYST_RGI_OGNZ_NO, SYST_RGI_SYST_CD, SYST_RGI_PRGR_ID, SYST_CHG_DT, SYST_CHG_PRAF_NO, SYST_CHG_OGNZ_NO, SYST_CHG_SYST_CD, SYST_CHG_PRGR_ID) VALUES ('보안팀', '보안팀', '0993134', 'shinhanlife@shinhanlife.com', '20260101', 'Y', TIMESTAMP '2023-01-01 12:00:00', 'A1234567', '7654321', 'SYS', 'PROG001', TIMESTAMP '2023-01-01 12:00:00', 'A1234567', '7654321', 'SYS', 'PROG001');

View File

@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- 1. 로그 패턴 설정 (MDC traceId 포함) -->
<property name="LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%X{traceId}] %-5level %logger{36} - %msg%n" />
<!-- 2. 콘솔(Console) 출력 설정 (로컬 개발용) -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${LOG_PATTERN}</pattern>
</encoder>
</appender>
<!-- 3. 파일(File) 출력 설정 (서버 운영용) -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logs/axhub-gateway.log</file> <!-- 현재 로그가 쌓이는 파일 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 매일 자정에 날짜를 붙여서 지난 로그를 분리 저장 -->
<fileNamePattern>logs/axhub-gateway-%d{yyyy-MM-dd}.log</fileNamePattern>
<!-- 최대 30일치 로그만 보관하고 오래된 것은 자동 삭제 -->
<maxHistory>30</maxHistory>
</rollingPolicy>
<encoder>
<pattern>${LOG_PATTERN}</pattern>
</encoder>
</appender>
<!-- 4. 기본 로깅 레벨 설정 -->
<root level="INFO">
<appender-ref ref="CONSOLE" />
<appender-ref ref="FILE" />
</root>
<!-- 5. 우리 프로젝트 패키지는 디버그 레벨까지 상세히 보기 -->
<logger name="io.shinhanlife" level="DEBUG" />
</configuration>

View File

@@ -0,0 +1,193 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="io.shinhanlife.axhub.biz.so.atm.domain.repository.AccessMgmtMapper">
<resultMap id="roleResultMap" type="io.shinhanlife.axhub.biz.so.atm.dto.RoleListOutDto">
<result column="SYST_ID" property="systId"/>
<result column="ROLE_NO" property="roleNo"/>
<result column="ROLE_NM" property="roleNm"/>
<result column="ROLE_DS" property="roleDs"/>
<result column="PUSE_YN" property="puseYn"/>
</resultMap>
<resultMap id="athrItemResultMap" type="io.shinhanlife.axhub.biz.so.atm.dto.AthrItemOutDto">
<result column="RESOURCE_ID" property="resourceId"/>
<result column="RESOURCE_NM" property="resourceNm"/>
<result column="RESOURCE_DS" property="resourceDs"/>
<result column="TYPE_CODE" property="typeCode"/>
</resultMap>
<!-- 역할 목록 조회 -->
<select id="selectRoles" parameterType="io.shinhanlife.axhub.biz.so.atm.dto.RoleListInDto"
resultMap="roleResultMap">
SELECT
SYST_ID,
ROLE_NO,
ROLE_NM,
ROLE_DS,
PUSE_YN
FROM
S_EIAM_TIS.AA_ROLE
<where>
<if test="systId != null and systId != ''">
AND SYST_ID = #{systId}
</if>
<if test="puseYn != null and puseYn != ''">
AND PUSE_YN = #{puseYn}
</if>
<if test="keyword != null and keyword != ''">
AND ROLE_NM LIKE '%' || #{keyword} || '%'
</if>
</where>
ORDER BY ROLE_NM ASC
</select>
<!-- Tool 마스터 전체 조회 -->
<select id="selectAllTools" resultMap="athrItemResultMap">
SELECT
TOOL_ID AS RESOURCE_ID,
TOOL_NM AS RESOURCE_NM,
TOOL_DS AS RESOURCE_DS,
TOOL_TYPE_CD AS TYPE_CODE
FROM
AX_TOOL_MST
WHERE
PUSE_YN = 'Y'
ORDER BY
TOOL_NM ASC
</select>
<!-- 지식 마스터 전체 조회 -->
<select id="selectAllKnwls" resultMap="athrItemResultMap">
SELECT
KNWL_ID AS RESOURCE_ID,
KNWL_NM AS RESOURCE_NM,
KNWL_DS AS RESOURCE_DS,
KNWL_TYPE_CD AS TYPE_CODE
FROM
AX_KNWL_MST
WHERE
PUSE_YN = 'Y'
ORDER BY
KNWL_NM ASC
</select>
<!-- 역할에 부여된 Tool ID 목록 -->
<select id="selectGrantedToolIds" parameterType="io.shinhanlife.axhub.biz.so.atm.dto.AthrSearchInDto"
resultType="string">
SELECT
TOOL_ID
FROM
AX_ROLE_TOOL_ATHR
WHERE
SYST_ID = #{systId}
AND ROLE_NO = #{roleNo}
AND PUSE_YN = 'Y'
</select>
<!-- 역할에 부여된 지식 ID 목록 -->
<select id="selectGrantedKnwlIds" parameterType="io.shinhanlife.axhub.biz.so.atm.dto.AthrSearchInDto"
resultType="string">
SELECT
KNWL_ID
FROM
AX_ROLE_KNWL_ATHR
WHERE
SYST_ID = #{systId}
AND ROLE_NO = #{roleNo}
AND PUSE_YN = 'Y'
</select>
<!-- Tool 권한 전체 삭제 -->
<delete id="deleteToolAthr">
DELETE FROM AX_ROLE_TOOL_ATHR
WHERE
SYST_ID = #{systId}
AND ROLE_NO = #{roleNo}
</delete>
<!-- Tool 권한 단건 삽입 -->
<insert id="insertToolAthr" parameterType="io.shinhanlife.axhub.biz.so.atm.dto.RoleToolAthrInDto">
INSERT INTO AX_ROLE_TOOL_ATHR (
ROLE_TOOL_ATHR_ID,
SYST_ID,
ROLE_NO,
TOOL_ID,
PUSE_YN,
SYST_RGI_DT,
SYST_RGI_PRAF_NO,
SYST_RGI_OGNZ_NO,
SYST_RGI_SYST_CD,
SYST_RGI_PRGR_ID,
SYST_CHG_DT,
SYST_CHG_PRAF_NO,
SYST_CHG_OGNZ_NO,
SYST_CHG_SYST_CD,
SYST_CHG_PRGR_ID
) VALUES (
#{roleToolAthrId},
#{systId},
#{roleNo},
#{toolId},
#{puseYn},
#{systRgiDt},
#{systRgiPrafNo},
#{systRgiOgnzNo},
#{systRgiSystCd},
#{systRgiPrgrId},
#{systChgDt},
#{systChgPrafNo},
#{systChgOgnzNo},
#{systChgSystCd},
#{systChgPrgrId}
)
</insert>
<!-- 지식 권한 전체 삭제 -->
<delete id="deleteKnwlAthr">
DELETE FROM AX_ROLE_KNWL_ATHR
WHERE
SYST_ID = #{systId}
AND ROLE_NO = #{roleNo}
</delete>
<!-- 지식 권한 단건 삽입 -->
<insert id="insertKnwlAthr" parameterType="io.shinhanlife.axhub.biz.so.atm.dto.RoleKnwlAthrInDto">
INSERT INTO AX_ROLE_KNWL_ATHR (
ROLE_KNWL_ATHR_ID,
SYST_ID,
ROLE_NO,
KNWL_ID,
PUSE_YN,
SYST_RGI_DT,
SYST_RGI_PRAF_NO,
SYST_RGI_OGNZ_NO,
SYST_RGI_SYST_CD,
SYST_RGI_PRGR_ID,
SYST_CHG_DT,
SYST_CHG_PRAF_NO,
SYST_CHG_OGNZ_NO,
SYST_CHG_SYST_CD,
SYST_CHG_PRGR_ID
) VALUES (
#{roleKnwlAthrId},
#{systId},
#{roleNo},
#{knwlId},
#{puseYn},
#{systRgiDt},
#{systRgiPrafNo},
#{systRgiOgnzNo},
#{systRgiSystCd},
#{systRgiPrgrId},
#{systChgDt},
#{systChgPrafNo},
#{systChgOgnzNo},
#{systChgSystCd},
#{systChgPrgrId}
)
</insert>
</mapper>

View File

@@ -0,0 +1,46 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="io.shinhanlife.axhub.common.session.domain.repository.ZtUsacRepository">
<select id="selectZtUsac" resultType="io.shinhanlife.axhub.common.session.dto.ZtUsacOutDto">
SELECT
AU.PRAF_NO, -- 인사번호
AU.PRAF_NM, -- 인사명
AU.OGNZ_NO, -- 조직번호
OG.OGNZ_NM, -- 조직명
AU.ADDRE, -- 이메일주소
AU.PRAF_OFDU_CD, -- 인사직무코드
AU.PRAF_OFDU_NM, -- 인사직무명
AU.PRAF_OFLE_CD, -- 인사직급코드
AU.PRAF_OFLE_NM, -- 인사직급명
AU.PRAF_DUTY_CD, -- 인사직책코드
AU.PRAF_DUTY_NM, -- 인사직책명
AU.MNGR_PRAF_NO, -- 관리자인사번호
AU.PUSE_YN, -- 사용여부
LISTAGG(URR.ROLE_NO, ',') WITHIN GROUP (ORDER BY URR.ROLE_NO) AS ROLE_NO_STR_LIST,
LISTAGG(UR.ROLE_NM, ',') WITHIN GROUP (ORDER BY URR.ROLE_NO) AS ROLE_NM_STR_LIST,
LISTAGG(AM.TGTR_PRAF_NO, ',') WITHIN GROUP (ORDER BY AM.TGTR_PRAF_NO) AS TGTR_PRAF_NO_STR_LIST,
LISTAGG(AM.TGTR_OGNZ_NO, ',') WITHIN GROUP (ORDER BY AM.TGTR_PRAF_NO) AS TGTR_OGNZ_NO_STR_LIST
FROM
S_EIAM_TIS.AA_USAC AU
INNER JOIN
S_EIAM_TIS.AA_USAC_USER_GROU_RTNS UGR ON AU.PRAF_NO = UGR.PRAF_NO
INNER JOIN
S_EIAM_TIS.AA_USER_GROU UG ON UGR.USER_GROU_ID = UG.USER_GROU_ID
INNER JOIN
S_EIAM_TIS.AA_USER_GROU_ROLE_RTNS URR ON UG.USER_GROU_ID = URR.USER_GROU_ID
INNER JOIN
S_EIAM_TIS.AA_ROLE UR ON URR.ROLE_NO = UR.ROLE_NO
LEFT OUTER JOIN
S_EIAM_TIS.AA_MNDT AM ON AM.PRAF_NO = AU.PRAF_NO
INNER JOIN
S_EIAM_TIS.AA_OGNZ OG ON OG.OGNZ_NO = AU.OGNZ_NO
WHERE
AU.PRAF_NO = #{prafNo}
GROUP BY
AU.PRAF_NO, AU.PRAF_NM, AU.OGNZ_NO, AU.ADDRE, AU.PRAF_OFDU_CD, AU.PRAF_OFDU_NM, AU.PRAF_OFLE_CD, AU.PRAF_OFLE_NM, AU.PRAF_DUTY_CD, AU.PRAF_DUTY_NM, AU.MNGR_PRAF_NO, AU.PUSE_YN;
</select>
</mapper>

View File

@@ -0,0 +1,99 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="io.shinhanlife.axhub.biz.sm.mmg.domain.repository.MenuRepository">
<select id="selectMenu" resultType="io.shinhanlife.axhub.biz.sm.mmg.dto.MenuOutDto">
SELECT MENU_ID
, PARENT_MENU_ID
, NAME
, PATH
, TITLE
, ICON
, IS_HIDE
, IS_HIDE_TAB
, LINK
, IS_IFRAME
, KEEP_ALIVE
, ORDER_SEQ
, IS_ACTIVE
FROM S_TIS.ZT_MENU
WHERE 1=1
<if test="isActive != null and isActive != ''">
AND IS_ACTIVE = #{isActive}
</if>
<if test="path != null and path != ''">
AND PATH LIKE CONCAT('%', #{path}, '%')
</if>
<if test="title != null and title != ''">
AND TITLE LIKE CONCAT('%', #{title}, '%')
</if>
ORDER BY ORDER_SEQ ASC
</select>
<select id="selectMenuById" resultType="io.shinhanlife.axhub.biz.sm.mmg.dto.MenuOutDto">
SELECT MENU_ID
, PARENT_MENU_ID
, NAME
, PATH
, TITLE
, ICON
, IS_HIDE
, IS_HIDE_TAB
, LINK
, IS_IFRAME
, KEEP_ALIVE
, ORDER_SEQ
, IS_ACTIVE
FROM S_TIS.ZT_MENU
WHERE MENU_ID = #{menuId}
</select>
<insert id="insertMenu"
parameterType="io.shinhanlife.axhub.biz.sm.mmg.dto.MenuSaveInDto"
useGeneratedKeys="true"
keyProperty="menuId">
INSERT INTO S_TIS.ZT_MENU (
PARENT_MENU_ID, NAME, PATH, TITLE, ICON,
IS_HIDE, IS_HIDE_TAB, LINK, IS_IFRAME, KEEP_ALIVE,
ORDER_SEQ, IS_ACTIVE,
SYST_RGI_DT, SYST_RGI_PRAF_NO, SYST_RGI_OGNZ_NO, SYST_RGI_SYST_CD, SYST_RGI_PRGR_ID,
SYST_CHG_DT, SYST_CHG_PRAF_NO, SYST_CHG_OGNZ_NO, SYST_CHG_SYST_CD, SYST_CHG_PRGR_ID
) VALUES (
#{parentMenuId}, #{name}, #{path}, #{title}, #{icon},
#{isHide}, #{isHideTab}, #{link}, #{isIframe}, #{keepAlive},
#{orderSeq}, #{isActive},
#{systRgiDt}, #{systRgiPrafNo}, #{systRgiOgnzNo}, #{systRgiSystCd}, #{systRgiPrgrId},
#{systChgDt}, #{systChgPrafNo}, #{systChgOgnzNo}, #{systChgSystCd}, #{systChgPrgrId}
)
</insert>
<update id="updateMenu" parameterType="io.shinhanlife.axhub.biz.sm.mmg.dto.MenuSaveInDto">
UPDATE S_TIS.ZT_MENU
SET
PARENT_MENU_ID = #{parentMenuId},
NAME = #{name},
PATH = #{path},
TITLE = #{title},
ICON = #{icon},
IS_HIDE = #{isHide},
IS_HIDE_TAB = #{isHideTab},
LINK = #{link},
IS_IFRAME = #{isIframe},
KEEP_ALIVE = #{keepAlive},
ORDER_SEQ = #{orderSeq},
IS_ACTIVE = #{isActive},
SYST_CHG_DT = #{systChgDt},
SYST_CHG_PRAF_NO = #{systChgPrafNo},
SYST_CHG_OGNZ_NO = #{systChgOgnzNo},
SYST_CHG_SYST_CD = #{systChgSystCd},
SYST_CHG_PRGR_ID = #{systChgPrgrId}
WHERE MENU_ID = #{menuId}
</update>
<delete id="deleteMenu">
DELETE FROM S_TIS.ZT_MENU
WHERE MENU_ID = #{menuId}
</delete>
</mapper>

View File

@@ -0,0 +1,99 @@
{
"BILL_001": {
"status": "SUCCESS",
"message": "청구 심사 상태 조회가 완료되었습니다.",
"data": {
"billingStatus": "PROCESSING",
"expectedCompletionDate": "2026-07-05"
}
},
"BILL_002": {
"status": "SUCCESS",
"message": "청구 처리가 완료되었습니다.",
"data": {
"processId": "PRC-998811",
"result": "APPROVED"
}
},
"BOND_001": {
"status": "SUCCESS",
"message": "디지털 증권 발행 가능 한도 조회가 완료되었습니다.",
"data": {
"availableLimit": 500000000,
"currency": "KRW"
}
},
"BOND_002": {
"status": "SUCCESS",
"message": "디지털 증권 발행이 성공적으로 완료되었습니다.",
"data": {
"bondId": "BND-2026-07-04-1234",
"issuedAmount": 10000000
}
},
"HR_VAC_01": {
"status": "SUCCESS",
"message": "연차 휴가 등록이 완료되었습니다.",
"data": {
"vacationId": "VAC-8877",
"status": "APPROVED"
}
},
"HR_VAC_02": {
"status": "SUCCESS",
"message": "잔여 연차 일수 조회가 완료되었습니다.",
"data": {
"totalDays": 15,
"usedDays": 3,
"remainingDays": 12
}
},
"COM_SMS_01": {
"status": "SUCCESS",
"message": "SMS 발송이 성공적으로 완료되었습니다.",
"data": {
"messageId": "SMS-11223344",
"sentTime": "2026-07-04 10:00:00"
}
},
"COM_EML_01": {
"status": "SUCCESS",
"message": "이메일 발송이 성공적으로 완료되었습니다.",
"data": {
"emailId": "EML-998877",
"sentTime": "2026-07-04 10:00:00"
}
},
"CNTR_001": {
"status": "SUCCESS",
"message": "계약 상태 조회가 완료되었습니다.",
"data": {
"contractStatus": "ACTIVE",
"startDate": "2024-01-01"
}
},
"CNTR_002": {
"status": "SUCCESS",
"message": "계약 상세 내역 조회가 완료되었습니다.",
"data": {
"productName": "신한 라이프 스마트 연금보험",
"monthlyPremium": 500000
}
},
"CRM_001": {
"status": "SUCCESS",
"message": "고객 등급 조회가 완료되었습니다.",
"data": {
"customerGrade": "VIP",
"loyaltyPoints": 15000
}
},
"CRM_002": {
"status": "SUCCESS",
"message": "고객 상세 정보 조회가 완료되었습니다.",
"data": {
"address": "서울특별시 중구 세종대로 9",
"phoneNumber": "010-XXXX-XXXX"
}
}
}

View File

@@ -0,0 +1,73 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="io.shinhanlife.axhub.sample.domain.repository.GlowSampleRepository">
<select id="selectStrnTerm" parameterType="string" resultType="io.shinhanlife.axhub.sample.dto.StrnTermListOutDTO$StrnTerm">
SELECT /*
--------------------------------------------------------------------------
-- 프로그램 설명 : 표준영문약어명조회
-- 프로그램명 :
--------------------------------------------------------------------------
*/
'한글명' AS strnTermHanNm,
'영문약어' AS strnTermEngcAbrNm,
'영문명' AS strnTermEngNm,
'화면표기명' AS strnTermScrnTermNm
FROM DUAL
WHERE '한글명' LIKE '%' || NVL(#{strnTermHanNm}, '') || '%'
</select>
<insert id="insertAppliSystNtfyPati" parameterType="io.shinhanlife.axhub.sample.domain.model.AppliSystNtfyPatiModel">
INSERT /*
--------------------------------------------------------------------------
-- 업무파트명 : 프레임워크
-- 프로그램 한글명 또는 화면명 : 어플리케이션시스템알림등록
-- 프로그램명 : AppliSystNtfyPati
-------------------------------------------------------------------------- */
INTO S_TFG.GL_APPLI_SYST_NTFY_PATI
( APPLI_SYST_NTFY_PATI_ID
,APPLI_SYST_NTFY_KD_CD
,APPLI_DUTJ_CD
,APPLI_DUTJ_PRJC_CD
,NTFY_MSG_CT
,NTFY_OCC_DT
,NTFY_CFM_DT
,NTFY_TRGT_PRAF_NO
,SHMS_PML_YN
,NTFY_CFM_YN
,SYST_RGI_DT
,SYST_RGI_PRAF_NO
,SYST_RGI_OGNZ_NO
,SYST_RGI_SYST_CD
,SYST_RGI_PRGR_ID
,SYST_CHG_DT
,SYST_CHG_PRAF_NO
,SYST_CHG_OGNZ_NO
,SYST_CHG_SYST_CD
,SYST_CHG_PRGR_ID
) VALUES (
#{appliSystNtfyPatiId}
,NVL(#{appliSystNtfyKdCd},'ZZ')
,NVL(#{appliDutjCd},'ZZ')
,NVL(#{appliDutjPrjcCd},'ZZ')
,#{ntfyMsgCt}
,TO_DATE(#{ntfyOccDt}, 'YYYY-MM-DD HH24:MI:SS')
,TO_DATE(#{ntfyCfmDt}, 'YYYY-MM-DD HH24:MI:SS')
,#{ntfyTrgtPrafNo}
,#{shmsPmlYn}
,#{ntfyCfmYn}
,SYSDATE
,#{systRgiPrafNo}
,#{systRgiOgnzNo}
,#{systRgiSystCd}
,#{systRgiPrgrId}
,SYSDATE
,#{systChgPrafNo}
,#{systChgOgnzNo}
,#{systChgSystCd}
,#{systChgPrgrId}
)
</insert>
</mapper>

View File

@@ -0,0 +1,618 @@
CREATE SCHEMA IF NOT EXISTS S_TIS;
CREATE SCHEMA IF NOT EXISTS S_EIAM_TIS;
SET SCHEMA S_TIS;
-- ZT_통합코드
CREATE TABLE ZT_UNFC_CD (
UNFC_CD_ID VARCHAR(50) NOT NULL,
UNFC_CD_NM VARCHAR(100),
UNFC_CD_HAN_NM VARCHAR(100),
UNFC_CD_ENG_NM VARCHAR(100),
UNFC_CD_DS VARCHAR(4000),
META_SYST_CD VARCHAR(3) NOT NULL,
META_DUTJ_TYPE_CD VARCHAR(5) NOT NULL,
PUSE_YN VARCHAR(1) NOT NULL,
SPPO_UNFC_CD_ID VARCHAR(50),
DATA_LOAD_DT DATE,
SYST_RGI_DT DATE NOT NULL,
SYST_RGI_PRAF_NO VARCHAR(8) NOT NULL,
SYST_RGI_OGNZ_NO VARCHAR(7) NOT NULL,
SYST_RGI_SYST_CD VARCHAR(3) NOT NULL,
SYST_RGI_PRGR_ID VARCHAR(100) NOT NULL,
SYST_CHG_DT DATE NOT NULL,
SYST_CHG_PRAF_NO VARCHAR(8) NOT NULL,
SYST_CHG_OGNZ_NO VARCHAR(7) NOT NULL,
SYST_CHG_SYST_CD VARCHAR(3) NOT NULL,
SYST_CHG_PRGR_ID VARCHAR(100) NOT NULL,
PRIMARY KEY (UNFC_CD_ID)
);
COMMENT ON TABLE ZT_UNFC_CD IS '통합코드';
COMMENT ON COLUMN ZT_UNFC_CD.UNFC_CD_ID IS '통합코드ID (PK)';
COMMENT ON COLUMN ZT_UNFC_CD.UNFC_CD_NM IS '통합코드명';
COMMENT ON COLUMN ZT_UNFC_CD.UNFC_CD_HAN_NM IS '통합코드한글명';
COMMENT ON COLUMN ZT_UNFC_CD.UNFC_CD_ENG_NM IS '통합코드영문명';
COMMENT ON COLUMN ZT_UNFC_CD.UNFC_CD_DS IS '통합코드설명';
COMMENT ON COLUMN ZT_UNFC_CD.META_SYST_CD IS '메타시스템코드';
COMMENT ON COLUMN ZT_UNFC_CD.META_DUTJ_TYPE_CD IS '메타업무유형코드';
COMMENT ON COLUMN ZT_UNFC_CD.PUSE_YN IS '사용여부 (Y/N)';
COMMENT ON COLUMN ZT_UNFC_CD.SPPO_UNFC_CD_ID IS '상위통합코드ID';
COMMENT ON COLUMN ZT_UNFC_CD.DATA_LOAD_DT IS '데이터적재일시';
COMMENT ON COLUMN ZT_UNFC_CD.SYST_RGI_DT IS '시스템등록일시';
COMMENT ON COLUMN ZT_UNFC_CD.SYST_RGI_PRAF_NO IS '시스템등록인사번호';
COMMENT ON COLUMN ZT_UNFC_CD.SYST_RGI_OGNZ_NO IS '시스템등록조직번호';
COMMENT ON COLUMN ZT_UNFC_CD.SYST_RGI_SYST_CD IS '시스템등록시스템코드';
COMMENT ON COLUMN ZT_UNFC_CD.SYST_RGI_PRGR_ID IS '시스템등록프로그램ID';
COMMENT ON COLUMN ZT_UNFC_CD.SYST_CHG_DT IS '시스템변경일시';
COMMENT ON COLUMN ZT_UNFC_CD.SYST_CHG_PRAF_NO IS '시스템변경인사번호';
COMMENT ON COLUMN ZT_UNFC_CD.SYST_CHG_OGNZ_NO IS '시스템변경조직번호';
COMMENT ON COLUMN ZT_UNFC_CD.SYST_CHG_SYST_CD IS '시스템변경시스템코드';
COMMENT ON COLUMN ZT_UNFC_CD.SYST_CHG_PRGR_ID IS '시스템변경프로그램ID';
-- ZT_통합코드 상세
CREATE TABLE S_TIS.ZT_UNFC_CD_DET
(
UNFC_CD_ID VARCHAR(50) NOT NULL,
CD_VLDT_VALU VARCHAR(50) NOT NULL,
CD_VLDT_VALU_INQR_SEQ NUMBER(5) NULL,
CD_VLDT_VALU_NM VARCHAR(400) NULL,
CD_VLDT_VALU_HNGL_ABR_NM VARCHAR(200) NULL,
CD_VLDT_VALU_ENGC_ABR_NM VARCHAR(200) NULL,
CD_VLDT_VALU_HAN_NM VARCHAR(400) NULL,
CD_VLDT_VALU_ENG_NM VARCHAR(400) NULL,
CD_VLDT_VALU_DS VARCHAR(4000) NULL,
CD_ASRT_TYPE_BIT_CD VARCHAR(20) NOT NULL,
SPPO_CD_VLDT_VALU VARCHAR(50) NULL,
SPPO_UNFC_CD_ID VARCHAR(50) NULL,
VLDT_STRT_YMD DATE NULL,
VLDT_END_YMD DATE NULL,
USER_DEF_VALU_N01 VARCHAR(100) NULL,
USER_DEF_VALU_N02 VARCHAR(100) NULL,
SYST_RGI_DT DATE NOT NULL,
SYST_RGI_PRAF_NO VARCHAR(8) NOT NULL,
SYST_RGI_OGNZ_NO VARCHAR(7) NOT NULL,
SYST_RGI_SYST_CD VARCHAR(3) NOT NULL,
SYST_RGI_PRGR_ID VARCHAR(100) NOT NULL,
SYST_CHG_DT DATE NOT NULL,
SYST_CHG_PRAF_NO VARCHAR(8) NOT NULL,
SYST_CHG_OGNZ_NO VARCHAR(7) NOT NULL,
SYST_CHG_SYST_CD VARCHAR(3) NOT NULL,
SYST_CHG_PRGR_ID VARCHAR(100) NOT NULL,
DATA_LOAD_DT DATE NULL
);
COMMENT ON TABLE S_TIS.ZT_UNFC_CD_DET IS '통합코드 상세';
COMMENT ON COLUMN S_TIS.ZT_UNFC_CD_DET.UNFC_CD_ID IS '통합코드ID (PK, 부모)';
COMMENT ON COLUMN S_TIS.ZT_UNFC_CD_DET.CD_VLDT_VALU IS '코드유효값 (PK)';
COMMENT ON COLUMN S_TIS.ZT_UNFC_CD_DET.CD_VLDT_VALU_INQR_SEQ IS '코드유효값조회순서';
COMMENT ON COLUMN S_TIS.ZT_UNFC_CD_DET.CD_VLDT_VALU_NM IS '코드유효값명';
COMMENT ON COLUMN S_TIS.ZT_UNFC_CD_DET.CD_VLDT_VALU_HNGL_ABR_NM IS '코드유효값한글약어명';
COMMENT ON COLUMN S_TIS.ZT_UNFC_CD_DET.CD_VLDT_VALU_ENGC_ABR_NM IS '코드유효값영문약어명';
COMMENT ON COLUMN S_TIS.ZT_UNFC_CD_DET.CD_VLDT_VALU_HAN_NM IS '코드유효값한글명';
COMMENT ON COLUMN S_TIS.ZT_UNFC_CD_DET.CD_VLDT_VALU_ENG_NM IS '코드유효값영문명';
COMMENT ON COLUMN S_TIS.ZT_UNFC_CD_DET.CD_VLDT_VALU_DS IS '코드유효값설명';
COMMENT ON COLUMN S_TIS.ZT_UNFC_CD_DET.CD_ASRT_TYPE_BIT_CD IS '코드분류유형비트코드';
COMMENT ON COLUMN S_TIS.ZT_UNFC_CD_DET.SPPO_CD_VLDT_VALU IS '상위코드유효값';
COMMENT ON COLUMN S_TIS.ZT_UNFC_CD_DET.SPPO_UNFC_CD_ID IS '상위통합코드ID';
COMMENT ON COLUMN S_TIS.ZT_UNFC_CD_DET.VLDT_STRT_YMD IS '유효시작일자';
COMMENT ON COLUMN S_TIS.ZT_UNFC_CD_DET.VLDT_END_YMD IS '유효종료일자';
COMMENT ON COLUMN S_TIS.ZT_UNFC_CD_DET.USER_DEF_VALU_N01 IS '사용자정의값1';
COMMENT ON COLUMN S_TIS.ZT_UNFC_CD_DET.USER_DEF_VALU_N02 IS '사용자정의값2';
COMMENT ON COLUMN S_TIS.ZT_UNFC_CD_DET.SYST_RGI_DT IS '시스템등록일시';
COMMENT ON COLUMN S_TIS.ZT_UNFC_CD_DET.SYST_RGI_PRAF_NO IS '시스템등록인사번호';
COMMENT ON COLUMN S_TIS.ZT_UNFC_CD_DET.SYST_RGI_OGNZ_NO IS '시스템등록조직번호';
COMMENT ON COLUMN S_TIS.ZT_UNFC_CD_DET.SYST_RGI_SYST_CD IS '시스템등록시스템코드';
COMMENT ON COLUMN S_TIS.ZT_UNFC_CD_DET.SYST_RGI_PRGR_ID IS '시스템등록프로그램ID';
COMMENT ON COLUMN S_TIS.ZT_UNFC_CD_DET.SYST_CHG_DT IS '시스템변경일시';
COMMENT ON COLUMN S_TIS.ZT_UNFC_CD_DET.SYST_CHG_PRAF_NO IS '시스템변경인사번호';
COMMENT ON COLUMN S_TIS.ZT_UNFC_CD_DET.SYST_CHG_OGNZ_NO IS '시스템변경조직번호';
COMMENT ON COLUMN S_TIS.ZT_UNFC_CD_DET.SYST_CHG_SYST_CD IS '시스템변경시스템코드';
COMMENT ON COLUMN S_TIS.ZT_UNFC_CD_DET.SYST_CHG_PRGR_ID IS '시스템변경프로그램ID';
COMMENT ON COLUMN S_TIS.ZT_UNFC_CD_DET.DATA_LOAD_DT IS '데이터적재일시';
-------------------------------------------------------------------------------------------
-- ZT_MENU 테이블 생성 (메뉴 기본 정보 저장)
CREATE TABLE ZT_MENU (
MENU_ID INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, -- 자동 증가 ID (PK)
PARENT_MENU_ID INTEGER, -- 부모 메뉴 ID (FK, NULL 가능 for 루트 메뉴)
NAME VARCHAR(100), -- 컴포넌트 이름 (옵셔널)
PATH VARCHAR(255) NOT NULL, -- 라우트 경로 (필수)
TITLE VARCHAR(100) NOT NULL, -- 메뉴 이름 (meta.title, 필수)
ICON VARCHAR(50), -- 메뉴 아이콘 (옵셔널)
SHOW_BADGE VARCHAR(1) DEFAULT 'N' CHECK (SHOW_BADGE IN ('Y', 'N')), -- 배지 표시 여부 (Y/N, 옵셔널)
SHOW_TEXT_BADGE VARCHAR(50), -- 텍스트 배지 표시 (옵셔널)
IS_HIDE VARCHAR(1) DEFAULT 'N' CHECK (IS_HIDE IN ('Y', 'N')), -- 메뉴 숨김 여부 (Y/N, 옵셔널)
IS_HIDE_TAB VARCHAR(1) DEFAULT 'N' CHECK (IS_HIDE_TAB IN ('Y', 'N')),-- 탭 숨김 여부 (Y/N, 옵셔널)
LINK VARCHAR(255), -- 링크 (옵셔널)
IS_IFRAME VARCHAR(1) DEFAULT 'N' CHECK (IS_IFRAME IN ('Y', 'N')), -- iframe 여부 (Y/N, 옵셔널)
KEEP_ALIVE VARCHAR(1) DEFAULT 'N' CHECK (KEEP_ALIVE IN ('Y', 'N')), -- 캐시 여부 (Y/N, 옵셔널)
ORDER_SEQ INTEGER DEFAULT 0 NOT NULL, -- 메뉴 표시 순서 (정렬용, 필수)
IS_ACTIVE VARCHAR(1) DEFAULT 'Y' CHECK (IS_ACTIVE IN ('Y', 'N')), -- 활성 여부 (Y/N, 기본 Y)
SYST_RGI_DT DATE, -- 시스템등록일시
SYST_RGI_PRAF_NO VARCHAR(50), -- 시스템등록인사번호
SYST_RGI_OGNZ_NO VARCHAR(50), -- 시스템등록조직번호
SYST_RGI_SYST_CD VARCHAR(50), -- 시스템등록시스템코드
SYST_RGI_PRGR_ID VARCHAR(50), -- 시스템등록프로그램ID
SYST_CHG_DT DATE, -- 시스템변경일시
SYST_CHG_PRAF_NO VARCHAR(50), -- 시스템변경인사번호
SYST_CHG_OGNZ_NO VARCHAR(50), -- 시스템변경조직번호
SYST_CHG_SYST_CD VARCHAR(50), -- 시스템변경시스템코드
SYST_CHG_PRGR_ID VARCHAR(50) -- 시스템변경프로그램ID
);
-- 제약 조건 추가
ALTER TABLE ZT_MENU ADD CONSTRAINT FK_ZT_MENU_PARENT FOREIGN KEY (PARENT_MENU_ID) REFERENCES ZT_MENU (MENU_ID) ON DELETE CASCADE; -- 부모 삭제 시 자식도 삭제 (계층 삭제 지원)
-- 인덱스 생성 (조회 최적화)
CREATE INDEX IDX_ZT_MENU_PARENT ON ZT_MENU (PARENT_MENU_ID);
CREATE INDEX IDX_ZT_MENU_PATH ON ZT_MENU (PATH);
-- 컬럼 코멘트 추가
COMMENT ON COLUMN ZT_MENU.MENU_ID IS '메뉴 ID (PK)';
COMMENT ON COLUMN ZT_MENU.PARENT_MENU_ID IS '부모 메뉴 ID (FK)';
COMMENT ON COLUMN ZT_MENU.NAME IS '컴포넌트 이름';
COMMENT ON COLUMN ZT_MENU.PATH IS '라우트 경로';
COMMENT ON COLUMN ZT_MENU.TITLE IS '메뉴 이름';
COMMENT ON COLUMN ZT_MENU.ICON IS '메뉴 아이콘';
COMMENT ON COLUMN ZT_MENU.SHOW_BADGE IS '배지 표시 여부 (Y/N)';
COMMENT ON COLUMN ZT_MENU.SHOW_TEXT_BADGE IS '텍스트 배지 표시';
COMMENT ON COLUMN ZT_MENU.IS_HIDE IS '메뉴 숨김 여부 (Y/N)';
COMMENT ON COLUMN ZT_MENU.IS_HIDE_TAB IS '탭 숨김 여부 (Y/N)';
COMMENT ON COLUMN ZT_MENU.LINK IS '링크';
COMMENT ON COLUMN ZT_MENU.IS_IFRAME IS 'iframe 여부 (Y/N)';
COMMENT ON COLUMN ZT_MENU.KEEP_ALIVE IS '캐시 여부 (Y/N)';
COMMENT ON COLUMN ZT_MENU.ORDER_SEQ IS '메뉴 표시 순서';
COMMENT ON COLUMN ZT_MENU.IS_ACTIVE IS '활성 여부 (Y/N)';
COMMENT ON COLUMN ZT_MENU.SYST_RGI_DT IS '시스템등록일시';
COMMENT ON COLUMN ZT_MENU.SYST_RGI_PRAF_NO IS '시스템등록인사번호';
COMMENT ON COLUMN ZT_MENU.SYST_RGI_OGNZ_NO IS '시스템등록조직번호';
COMMENT ON COLUMN ZT_MENU.SYST_RGI_SYST_CD IS '시스템등록시스템코드';
COMMENT ON COLUMN ZT_MENU.SYST_RGI_PRGR_ID IS '시스템등록프로그램ID';
COMMENT ON COLUMN ZT_MENU.SYST_CHG_DT IS '시스템변경일시';
COMMENT ON COLUMN ZT_MENU.SYST_CHG_PRAF_NO IS '시스템변경인사번호';
COMMENT ON COLUMN ZT_MENU.SYST_CHG_OGNZ_NO IS '시스템변경조직번호';
COMMENT ON COLUMN ZT_MENU.SYST_CHG_SYST_CD IS '시스템변경시스템코드';
COMMENT ON COLUMN ZT_MENU.SYST_CHG_PRGR_ID IS '시스템변경프로그램ID';
-- ============================================================
-- AX HUB 역할-Tool/지식 권한 관리 DDL
-- 대상 스키마 : 프로젝트 AP 스키마 (S_EIAM_TIS 는 읽기전용 참조)
-- 작성일 : 2026-06-25
-- ============================================================
-- ① Tool 마스터
CREATE TABLE AX_TOOL_MST (
TOOL_ID VARCHAR(20) NOT NULL, -- Tool ID (PK)
TOOL_NM VARCHAR(100) NOT NULL, -- Tool명
TOOL_DS VARCHAR(500), -- Tool설명
TOOL_TYPE_CD VARCHAR(20), -- Tool유형코드 (MCP / FUNC / EXT)
PUSE_YN VARCHAR(1) DEFAULT 'Y',-- 사용여부
SYST_RGI_DT DATE,
SYST_RGI_PRAF_NO VARCHAR(8),
SYST_RGI_OGNZ_NO VARCHAR(7),
SYST_RGI_SYST_CD VARCHAR(3),
SYST_RGI_PRGR_ID VARCHAR(100),
SYST_CHG_DT DATE,
SYST_CHG_PRAF_NO VARCHAR(8),
SYST_CHG_OGNZ_NO VARCHAR(7),
SYST_CHG_SYST_CD VARCHAR(3),
SYST_CHG_PRGR_ID VARCHAR(100),
CONSTRAINT PK_AX_TOOL_MST PRIMARY KEY (TOOL_ID)
);
COMMENT ON TABLE AX_TOOL_MST IS 'AX HUB Tool 마스터';
COMMENT ON COLUMN AX_TOOL_MST.TOOL_ID IS 'Tool ID (PK)';
COMMENT ON COLUMN AX_TOOL_MST.TOOL_NM IS 'Tool명';
COMMENT ON COLUMN AX_TOOL_MST.TOOL_DS IS 'Tool설명';
COMMENT ON COLUMN AX_TOOL_MST.TOOL_TYPE_CD IS 'Tool유형코드 (MCP/FUNC/EXT)';
COMMENT ON COLUMN AX_TOOL_MST.PUSE_YN IS '사용여부 (Y/N)';
-- ② 지식(Knowledge) 마스터
CREATE TABLE AX_KNWL_MST (
KNWL_ID VARCHAR(20) NOT NULL, -- 지식 ID (PK)
KNWL_NM VARCHAR(100) NOT NULL, -- 지식명
KNWL_DS VARCHAR(500), -- 지식설명
KNWL_TYPE_CD VARCHAR(20), -- 지식유형코드 (PRODUCT/MANUAL/LEGAL 등)
PUSE_YN VARCHAR(1) DEFAULT 'Y',
SYST_RGI_DT DATE,
SYST_RGI_PRAF_NO VARCHAR(8),
SYST_RGI_OGNZ_NO VARCHAR(7),
SYST_RGI_SYST_CD VARCHAR(3),
SYST_RGI_PRGR_ID VARCHAR(100),
SYST_CHG_DT DATE,
SYST_CHG_PRAF_NO VARCHAR(8),
SYST_CHG_OGNZ_NO VARCHAR(7),
SYST_CHG_SYST_CD VARCHAR(3),
SYST_CHG_PRGR_ID VARCHAR(100),
CONSTRAINT PK_AX_KNWL_MST PRIMARY KEY (KNWL_ID)
);
COMMENT ON TABLE AX_KNWL_MST IS 'AX HUB 지식 마스터';
COMMENT ON COLUMN AX_KNWL_MST.KNWL_ID IS '지식 ID (PK)';
COMMENT ON COLUMN AX_KNWL_MST.KNWL_NM IS '지식명';
COMMENT ON COLUMN AX_KNWL_MST.KNWL_DS IS '지식설명';
COMMENT ON COLUMN AX_KNWL_MST.KNWL_TYPE_CD IS '지식유형코드 (PRODUCT/MANUAL/LEGAL/REPORT)';
COMMENT ON COLUMN AX_KNWL_MST.PUSE_YN IS '사용여부 (Y/N)';
-- ③ 역할-Tool 권한 매핑
-- SYST_ID + ROLE_NO → S_EIAM_TIS.AA_ROLE 참조 (FK 미설정: 스키마 분리 환경)
CREATE TABLE AX_ROLE_TOOL_ATHR (
ROLE_TOOL_ATHR_ID VARCHAR(20) NOT NULL, -- 역할Tool권한ID (PK)
SYST_ID VARCHAR(10) NOT NULL, -- 시스템ID (EIAM 연동 키)
ROLE_NO VARCHAR(50) NOT NULL, -- 역할번호 (EIAM AA_ROLE.ROLE_NO)
TOOL_ID VARCHAR(20) NOT NULL, -- Tool ID
PUSE_YN VARCHAR(1) DEFAULT 'Y',
SYST_RGI_DT DATE,
SYST_RGI_PRAF_NO VARCHAR(8),
SYST_RGI_OGNZ_NO VARCHAR(7),
SYST_RGI_SYST_CD VARCHAR(3),
SYST_RGI_PRGR_ID VARCHAR(100),
SYST_CHG_DT DATE,
SYST_CHG_PRAF_NO VARCHAR(8),
SYST_CHG_OGNZ_NO VARCHAR(7),
SYST_CHG_SYST_CD VARCHAR(3),
SYST_CHG_PRGR_ID VARCHAR(100),
CONSTRAINT PK_AX_ROLE_TOOL_ATHR PRIMARY KEY (ROLE_TOOL_ATHR_ID)
);
-- (SYST_ID, ROLE_NO, TOOL_ID) 중복 방지
CREATE UNIQUE INDEX UK_AX_ROLE_TOOL_ATHR
ON AX_ROLE_TOOL_ATHR (SYST_ID, ROLE_NO, TOOL_ID);
COMMENT ON TABLE AX_ROLE_TOOL_ATHR IS '역할-Tool 권한 매핑';
COMMENT ON COLUMN AX_ROLE_TOOL_ATHR.ROLE_TOOL_ATHR_ID IS '역할Tool권한ID (PK)';
COMMENT ON COLUMN AX_ROLE_TOOL_ATHR.SYST_ID IS '시스템ID';
COMMENT ON COLUMN AX_ROLE_TOOL_ATHR.ROLE_NO IS '역할번호 (EIAM)';
COMMENT ON COLUMN AX_ROLE_TOOL_ATHR.TOOL_ID IS 'Tool ID';
COMMENT ON COLUMN AX_ROLE_TOOL_ATHR.PUSE_YN IS '사용여부';
-- ④ 역할-지식 권한 매핑
CREATE TABLE AX_ROLE_KNWL_ATHR (
ROLE_KNWL_ATHR_ID VARCHAR(20) NOT NULL, -- 역할지식권한ID (PK)
SYST_ID VARCHAR(10) NOT NULL,
ROLE_NO VARCHAR(50) NOT NULL,
KNWL_ID VARCHAR(20) NOT NULL,
PUSE_YN VARCHAR(1) DEFAULT 'Y',
SYST_RGI_DT DATE,
SYST_RGI_PRAF_NO VARCHAR(8),
SYST_RGI_OGNZ_NO VARCHAR(7),
SYST_RGI_SYST_CD VARCHAR(3),
SYST_RGI_PRGR_ID VARCHAR(100),
SYST_CHG_DT DATE,
SYST_CHG_PRAF_NO VARCHAR(8),
SYST_CHG_OGNZ_NO VARCHAR(7),
SYST_CHG_SYST_CD VARCHAR(3),
SYST_CHG_PRGR_ID VARCHAR(100),
CONSTRAINT PK_AX_ROLE_KNWL_ATHR PRIMARY KEY (ROLE_KNWL_ATHR_ID)
);
CREATE UNIQUE INDEX UK_AX_ROLE_KNWL_ATHR
ON AX_ROLE_KNWL_ATHR (SYST_ID, ROLE_NO, KNWL_ID);
COMMENT ON TABLE AX_ROLE_KNWL_ATHR IS '역할-지식 권한 매핑';
COMMENT ON COLUMN AX_ROLE_KNWL_ATHR.ROLE_KNWL_ATHR_ID IS '역할지식권한ID (PK)';
COMMENT ON COLUMN AX_ROLE_KNWL_ATHR.SYST_ID IS '시스템ID';
COMMENT ON COLUMN AX_ROLE_KNWL_ATHR.ROLE_NO IS '역할번호 (EIAM)';
COMMENT ON COLUMN AX_ROLE_KNWL_ATHR.KNWL_ID IS '지식ID';
COMMENT ON COLUMN AX_ROLE_KNWL_ATHR.PUSE_YN IS '사용여부';
-- AA_USAC 테이블 생성 (유저 마스터)
CREATE TABLE S_EIAM_TIS.AA_USAC (
PRAF_NO VARCHAR(8) PRIMARY KEY, -- 인사번호 (PK)
PRAF_NM VARCHAR(200), -- 인사명
OGNZ_NO VARCHAR(7) NOT NULL, -- 조직번호 (NOT NULL)
ADDRE VARCHAR(250), -- 이메일주소
PRAF_OFDU_CD VARCHAR(10), -- 인사직무코드
PRAF_OFDU_NM VARCHAR(100), -- 인사직무명
PRAF_OFLE_CD VARCHAR(10), -- 인사직급코드
PRAF_OFLE_NM VARCHAR(100), -- 인사직급명
PRAF_DUTY_CD VARCHAR(10), -- 인사직책코드
PRAF_DUTY_NM VARCHAR(100), -- 인사직책명
MNGR_PRAF_NO VARCHAR(8), -- 관리자인사번호
PUSE_YN VARCHAR(1), -- 사용여부
SYST_RGI_DT DATE, -- 시스템등록일시
SYST_RGI_PRAF_NO VARCHAR(8), -- 시스템등록인사번호
SYST_RGI_OGNZ_NO VARCHAR(7), -- 시스템등록조직번호
SYST_RGI_SYST_CD VARCHAR(3), -- 시스템등록시스템코드
SYST_RGI_PRGR_ID VARCHAR(100), -- 시스템등록프로그램ID
SYST_CHG_DT DATE, -- 시스템변경일시
SYST_CHG_PRAF_NO VARCHAR(8), -- 시스템변경인사번호
SYST_CHG_OGNZ_NO VARCHAR(7), -- 시스템변경조직번호
SYST_CHG_SYST_CD VARCHAR(3), -- 시스템변경시스템코드
SYST_CHG_PRGR_ID VARCHAR(100) -- 시스템변경프로그램ID
);
-- 인덱스 생성
CREATE INDEX IDX_AA_USAC_OGNZ_NO ON S_EIAM_TIS.AA_USAC (OGNZ_NO);
-- 컬럼 코멘트 추가
COMMENT ON COLUMN S_EIAM_TIS.AA_USAC.PRAF_NO IS '인사번호 (PK)';
COMMENT ON COLUMN S_EIAM_TIS.AA_USAC.PRAF_NM IS '인사명';
COMMENT ON COLUMN S_EIAM_TIS.AA_USAC.OGNZ_NO IS '조직번호 (NOT NULL)';
COMMENT ON COLUMN S_EIAM_TIS.AA_USAC.ADDRE IS '이메일주소';
COMMENT ON COLUMN S_EIAM_TIS.AA_USAC.PRAF_OFDU_CD IS '인사직무코드';
COMMENT ON COLUMN S_EIAM_TIS.AA_USAC.PRAF_OFDU_NM IS '인사직무명';
COMMENT ON COLUMN S_EIAM_TIS.AA_USAC.PRAF_OFLE_CD IS '인사직급코드';
COMMENT ON COLUMN S_EIAM_TIS.AA_USAC.PRAF_OFLE_NM IS '인사직급명';
COMMENT ON COLUMN S_EIAM_TIS.AA_USAC.PRAF_DUTY_CD IS '인사직책코드';
COMMENT ON COLUMN S_EIAM_TIS.AA_USAC.PRAF_DUTY_NM IS '인사직책명';
COMMENT ON COLUMN S_EIAM_TIS.AA_USAC.MNGR_PRAF_NO IS '관리자인사번호';
COMMENT ON COLUMN S_EIAM_TIS.AA_USAC.PUSE_YN IS '사용여부';
COMMENT ON COLUMN S_EIAM_TIS.AA_USAC.SYST_RGI_DT IS '시스템등록일시';
COMMENT ON COLUMN S_EIAM_TIS.AA_USAC.SYST_RGI_PRAF_NO IS '시스템등록인사번호';
COMMENT ON COLUMN S_EIAM_TIS.AA_USAC.SYST_RGI_OGNZ_NO IS '시스템등록조직번호';
COMMENT ON COLUMN S_EIAM_TIS.AA_USAC.SYST_RGI_SYST_CD IS '시스템등록시스템코드';
COMMENT ON COLUMN S_EIAM_TIS.AA_USAC.SYST_RGI_PRGR_ID IS '시스템등록프로그램ID';
COMMENT ON COLUMN S_EIAM_TIS.AA_USAC.SYST_CHG_DT IS '시스템변경일시';
COMMENT ON COLUMN S_EIAM_TIS.AA_USAC.SYST_CHG_PRAF_NO IS '시스템변경인사번호';
COMMENT ON COLUMN S_EIAM_TIS.AA_USAC.SYST_CHG_OGNZ_NO IS '시스템변경조직번호';
COMMENT ON COLUMN S_EIAM_TIS.AA_USAC.SYST_CHG_SYST_CD IS '시스템변경시스템코드';
COMMENT ON COLUMN S_EIAM_TIS.AA_USAC.SYST_CHG_PRGR_ID IS '시스템변경프로그램ID';
-- 위임
CREATE TABLE S_EIAM_TIS.AA_MNDT
(
MNDT_ID VARCHAR(10) NOT NULL,
SYST_ID VARCHAR(10) NOT NULL,
PRAF_NO VARCHAR(8) NOT NULL,
OGNZ_NO VARCHAR(7),
TGTR_PRAF_NO VARCHAR(8) NOT NULL,
TGTR_OGNZ_NO VARCHAR(7),
ROLE_NO VARCHAR(50) NOT NULL,
MNDT_STRT_DT DATE,
MNDT_END_DT DATE,
MNDT_DS VARCHAR(1000),
PUSE_YN VARCHAR(1),
SYST_RGI_DT DATE,
SYST_RGI_PRAF_NO VARCHAR(8),
SYST_RGI_OGNZ_NO VARCHAR(7),
SYST_RGI_SYST_CD VARCHAR(3),
SYST_RGI_PRGR_ID VARCHAR(100),
SYST_CHG_DT DATE,
SYST_CHG_PRAF_NO VARCHAR(8),
SYST_CHG_OGNZ_NO VARCHAR(7),
SYST_CHG_SYST_CD VARCHAR(3),
SYST_CHG_PRGR_ID VARCHAR(100)
);
ALTER TABLE S_EIAM_TIS.AA_MNDT ADD CONSTRAINT PK_AA_MNDT PRIMARY KEY (MNDT_ID);
COMMENT ON COLUMN S_EIAM_TIS.AA_MNDT.MNDT_ID IS '위임ID';
COMMENT ON COLUMN S_EIAM_TIS.AA_MNDT.SYST_ID IS '시스템ID';
COMMENT ON COLUMN S_EIAM_TIS.AA_MNDT.PRAF_NO IS '인사번호';
COMMENT ON COLUMN S_EIAM_TIS.AA_MNDT.OGNZ_NO IS '조직번호';
COMMENT ON COLUMN S_EIAM_TIS.AA_MNDT.TGTR_PRAF_NO IS '대상자인사번호';
COMMENT ON COLUMN S_EIAM_TIS.AA_MNDT.TGTR_OGNZ_NO IS '대상자조직번호';
COMMENT ON COLUMN S_EIAM_TIS.AA_MNDT.ROLE_NO IS '역할번호';
COMMENT ON COLUMN S_EIAM_TIS.AA_MNDT.MNDT_STRT_DT IS '위임시작일시';
COMMENT ON COLUMN S_EIAM_TIS.AA_MNDT.MNDT_END_DT IS '위임종료일시';
COMMENT ON COLUMN S_EIAM_TIS.AA_MNDT.MNDT_DS IS '위임설명';
COMMENT ON COLUMN S_EIAM_TIS.AA_MNDT.PUSE_YN IS '사용여부';
COMMENT ON COLUMN S_EIAM_TIS.AA_MNDT.SYST_RGI_DT IS '시스템등록일시';
COMMENT ON COLUMN S_EIAM_TIS.AA_MNDT.SYST_RGI_PRAF_NO IS '시스템등록인사번호';
COMMENT ON COLUMN S_EIAM_TIS.AA_MNDT.SYST_RGI_OGNZ_NO IS '시스템등록조직번호';
COMMENT ON COLUMN S_EIAM_TIS.AA_MNDT.SYST_RGI_SYST_CD IS '시스템등록시스템코드';
COMMENT ON COLUMN S_EIAM_TIS.AA_MNDT.SYST_RGI_PRGR_ID IS '시스템등록프로그램ID';
COMMENT ON COLUMN S_EIAM_TIS.AA_MNDT.SYST_CHG_DT IS '시스템변경일시';
COMMENT ON COLUMN S_EIAM_TIS.AA_MNDT.SYST_CHG_PRAF_NO IS '시스템변경인사번호';
COMMENT ON COLUMN S_EIAM_TIS.AA_MNDT.SYST_CHG_OGNZ_NO IS '시스템변경조직번호';
COMMENT ON COLUMN S_EIAM_TIS.AA_MNDT.SYST_CHG_SYST_CD IS '시스템변경시스템코드';
COMMENT ON COLUMN S_EIAM_TIS.AA_MNDT.SYST_CHG_PRGR_ID IS '시스템변경프로그램ID';
-- 역할
CREATE TABLE S_EIAM_TIS.AA_ROLE
(
SYST_ID VARCHAR(10) NOT NULL,
ROLE_NO VARCHAR(50) NOT NULL,
ROLE_NM VARCHAR(100),
ROLE_DS VARCHAR(1000),
ASST_OWNR_ROLE_TYPE_CD VARCHAR(3),
MASK_ECPT_MD_CD VARCHAR(1),
INDV_INFO_ATHR_YN VARCHAR(1),
MAIN_CST_IFIN_YN VARCHAR(1),
PUSE_YN VARCHAR(1),
SYST_RGI_DT DATE,
SYST_RGI_PRAF_NO VARCHAR(8),
SYST_RGI_OGNZ_NO VARCHAR(7),
SYST_RGI_SYST_CD VARCHAR(3),
SYST_RGI_PRGR_ID VARCHAR(100),
SYST_CHG_DT DATE,
SYST_CHG_PRAF_NO VARCHAR(8),
SYST_CHG_OGNZ_NO VARCHAR(7),
SYST_CHG_SYST_CD VARCHAR(3),
SYST_CHG_PRGR_ID VARCHAR(100)
);
CREATE UNIQUE INDEX S_EIAM_TIS.PK_AA_ROLE
ON S_EIAM_TIS.AA_ROLE (SYST_ID,ROLE_NO);
ALTER TABLE S_EIAM_TIS.AA_ROLE ADD CONSTRAINT PK_AA_ROLE PRIMARY KEY (SYST_ID,ROLE_NO);
COMMENT ON COLUMN S_EIAM_TIS.AA_ROLE.SYST_ID IS '시스템ID';
COMMENT ON COLUMN S_EIAM_TIS.AA_ROLE.ROLE_NO IS '역할번호';
COMMENT ON COLUMN S_EIAM_TIS.AA_ROLE.ROLE_NM IS '역할명';
COMMENT ON COLUMN S_EIAM_TIS.AA_ROLE.ROLE_DS IS '역할설명';
COMMENT ON COLUMN S_EIAM_TIS.AA_ROLE.ASST_OWNR_ROLE_TYPE_CD IS '자산소유자역할유형코드';
COMMENT ON COLUMN S_EIAM_TIS.AA_ROLE.MASK_ECPT_MD_CD IS '마스킹제외방법코드';
COMMENT ON COLUMN S_EIAM_TIS.AA_ROLE.INDV_INFO_ATHR_YN IS '개인정보권한여부';
COMMENT ON COLUMN S_EIAM_TIS.AA_ROLE.MAIN_CST_IFIN_YN IS '주요고객정보조회여부';
COMMENT ON COLUMN S_EIAM_TIS.AA_ROLE.PUSE_YN IS '사용여부';
COMMENT ON COLUMN S_EIAM_TIS.AA_ROLE.SYST_RGI_DT IS '시스템등록일시';
COMMENT ON COLUMN S_EIAM_TIS.AA_ROLE.SYST_RGI_PRAF_NO IS '시스템등록인사번호';
COMMENT ON COLUMN S_EIAM_TIS.AA_ROLE.SYST_RGI_OGNZ_NO IS '시스템등록조직번호';
COMMENT ON COLUMN S_EIAM_TIS.AA_ROLE.SYST_RGI_SYST_CD IS '시스템등록시스템코드';
COMMENT ON COLUMN S_EIAM_TIS.AA_ROLE.SYST_RGI_PRGR_ID IS '시스템등록프로그램ID';
COMMENT ON COLUMN S_EIAM_TIS.AA_ROLE.SYST_CHG_DT IS '시스템변경일시';
COMMENT ON COLUMN S_EIAM_TIS.AA_ROLE.SYST_CHG_PRAF_NO IS '시스템변경인사번호';
COMMENT ON COLUMN S_EIAM_TIS.AA_ROLE.SYST_CHG_OGNZ_NO IS '시스템변경조직번호';
COMMENT ON COLUMN S_EIAM_TIS.AA_ROLE.SYST_CHG_SYST_CD IS '시스템변경시스템코드';
COMMENT ON COLUMN S_EIAM_TIS.AA_ROLE.SYST_CHG_PRGR_ID IS '시스템변경프로그램ID';
-- 사용자그룹
CREATE TABLE S_EIAM_TIS.AA_USER_GROU
(
SYST_ID VARCHAR(10) NOT NULL,
USER_GROU_ID VARCHAR(10) NOT NULL,
OGNZ_NO VARCHAR(7),
USER_GROU_NM VARCHAR(100),
USER_GROU_DS VARCHAR(1000),
PUSE_YN VARCHAR(1),
SYST_RGI_DT DATE,
SYST_RGI_PRAF_NO VARCHAR(8),
SYST_RGI_OGNZ_NO VARCHAR(7),
SYST_RGI_SYST_CD VARCHAR(3),
SYST_RGI_PRGR_ID VARCHAR(100),
SYST_CHG_DT DATE,
SYST_CHG_PRAF_NO VARCHAR(8),
SYST_CHG_OGNZ_NO VARCHAR(7),
SYST_CHG_SYST_CD VARCHAR(3),
SYST_CHG_PRGR_ID VARCHAR(100)
);
CREATE UNIQUE INDEX S_EIAM_TIS.PK_AA_USER_GROU
ON S_EIAM_TIS.AA_USER_GROU (SYST_ID,USER_GROU_ID);
ALTER TABLE S_EIAM_TIS.AA_USER_GROU
ADD CONSTRAINT PK_AA_USER_GROU PRIMARY KEY (SYST_ID,USER_GROU_ID);
COMMENT ON COLUMN S_EIAM_TIS.AA_USER_GROU.SYST_ID IS '시스템ID';
COMMENT ON COLUMN S_EIAM_TIS.AA_USER_GROU.USER_GROU_ID IS '사용자그룹ID';
COMMENT ON COLUMN S_EIAM_TIS.AA_USER_GROU.OGNZ_NO IS '조직번호';
COMMENT ON COLUMN S_EIAM_TIS.AA_USER_GROU.USER_GROU_NM IS '사용자그룹명';
COMMENT ON COLUMN S_EIAM_TIS.AA_USER_GROU.USER_GROU_DS IS '사용자그룹설명';
COMMENT ON COLUMN S_EIAM_TIS.AA_USER_GROU.PUSE_YN IS '사용여부';
COMMENT ON COLUMN S_EIAM_TIS.AA_USER_GROU.SYST_RGI_DT IS '시스템등록일시';
COMMENT ON COLUMN S_EIAM_TIS.AA_USER_GROU.SYST_RGI_PRAF_NO IS '시스템등록인사번호';
COMMENT ON COLUMN S_EIAM_TIS.AA_USER_GROU.SYST_RGI_OGNZ_NO IS '시스템등록조직번호';
COMMENT ON COLUMN S_EIAM_TIS.AA_USER_GROU.SYST_RGI_SYST_CD IS '시스템등록시스템코드';
COMMENT ON COLUMN S_EIAM_TIS.AA_USER_GROU.SYST_RGI_PRGR_ID IS '시스템등록프로그램ID';
COMMENT ON COLUMN S_EIAM_TIS.AA_USER_GROU.SYST_CHG_DT IS '시스템변경일시';
COMMENT ON COLUMN S_EIAM_TIS.AA_USER_GROU.SYST_CHG_PRAF_NO IS '시스템변경인사번호';
COMMENT ON COLUMN S_EIAM_TIS.AA_USER_GROU.SYST_CHG_OGNZ_NO IS '시스템변경조직번호';
COMMENT ON COLUMN S_EIAM_TIS.AA_USER_GROU.SYST_CHG_SYST_CD IS '시스템변경시스템코드';
COMMENT ON COLUMN S_EIAM_TIS.AA_USER_GROU.SYST_CHG_PRGR_ID IS '시스템변경프로그램ID';
-- 사용자/그룹관계
CREATE TABLE S_EIAM_TIS.AA_USAC_USER_GROU_RTNS
(
USAC_USER_GROU_RTNS_ID VARCHAR(10) NOT NULL,
SYST_ID VARCHAR(10) NOT NULL,
PRAF_NO VARCHAR(8) NOT NULL,
USER_GROU_ID VARCHAR(10) NOT NULL,
VLDT_YMD DATE,
EXPR_YMD DATE,
PUSE_YN VARCHAR(1),
SYST_RGI_DT DATE,
SYST_RGI_PRAF_NO VARCHAR(8),
SYST_RGI_OGNZ_NO VARCHAR(7),
SYST_RGI_SYST_CD VARCHAR(3),
SYST_RGI_PRGR_ID VARCHAR(100),
SYST_CHG_DT DATE,
SYST_CHG_PRAF_NO VARCHAR(8),
SYST_CHG_OGNZ_NO VARCHAR(7),
SYST_CHG_SYST_CD VARCHAR(3),
SYST_CHG_PRGR_ID VARCHAR(100)
);
CREATE UNIQUE INDEX S_EIAM_TIS.PK_AA_USAC_USER_GROU_RTNS
ON S_EIAM_TIS.AA_USAC_USER_GROU_RTNS (USAC_USER_GROU_RTNS_ID);
ALTER TABLE S_EIAM_TIS.AA_USAC_USER_GROU_RTNS
ADD CONSTRAINT PK_AA_USAC_USER_GROU_RTNS PRIMARY KEY (USAC_USER_GROU_RTNS_ID);
COMMENT ON COLUMN S_EIAM_TIS.AA_USAC_USER_GROU_RTNS.USAC_USER_GROU_RTNS_ID IS '사용자계정사용자그룹관계ID';
COMMENT ON COLUMN S_EIAM_TIS.AA_USAC_USER_GROU_RTNS.SYST_ID IS '시스템ID';
COMMENT ON COLUMN S_EIAM_TIS.AA_USAC_USER_GROU_RTNS.PRAF_NO IS '인사번호';
COMMENT ON COLUMN S_EIAM_TIS.AA_USAC_USER_GROU_RTNS.USER_GROU_ID IS '사용자그룹ID';
COMMENT ON COLUMN S_EIAM_TIS.AA_USAC_USER_GROU_RTNS.VLDT_YMD IS '유효일자';
COMMENT ON COLUMN S_EIAM_TIS.AA_USAC_USER_GROU_RTNS.EXPR_YMD IS '만료일자';
COMMENT ON COLUMN S_EIAM_TIS.AA_USAC_USER_GROU_RTNS.PUSE_YN IS '사용여부';
COMMENT ON COLUMN S_EIAM_TIS.AA_USAC_USER_GROU_RTNS.SYST_RGI_DT IS '시스템등록일시';
COMMENT ON COLUMN S_EIAM_TIS.AA_USAC_USER_GROU_RTNS.SYST_RGI_PRAF_NO IS '시스템등록인사번호';
COMMENT ON COLUMN S_EIAM_TIS.AA_USAC_USER_GROU_RTNS.SYST_RGI_OGNZ_NO IS '시스템등록조직번호';
COMMENT ON COLUMN S_EIAM_TIS.AA_USAC_USER_GROU_RTNS.SYST_RGI_SYST_CD IS '시스템등록시스템코드';
COMMENT ON COLUMN S_EIAM_TIS.AA_USAC_USER_GROU_RTNS.SYST_RGI_PRGR_ID IS '시스템등록프로그램ID';
COMMENT ON COLUMN S_EIAM_TIS.AA_USAC_USER_GROU_RTNS.SYST_CHG_DT IS '시스템변경일시';
COMMENT ON COLUMN S_EIAM_TIS.AA_USAC_USER_GROU_RTNS.SYST_CHG_PRAF_NO IS '시스템변경인사번호';
COMMENT ON COLUMN S_EIAM_TIS.AA_USAC_USER_GROU_RTNS.SYST_CHG_OGNZ_NO IS '시스템변경조직번호';
COMMENT ON COLUMN S_EIAM_TIS.AA_USAC_USER_GROU_RTNS.SYST_CHG_SYST_CD IS '시스템변경시스템코드';
COMMENT ON COLUMN S_EIAM_TIS.AA_USAC_USER_GROU_RTNS.SYST_CHG_PRGR_ID IS '시스템변경프로그램ID';
-- 사용자그룹/역할 관계
CREATE TABLE S_EIAM_TIS.AA_USER_GROU_ROLE_RTNS
(
USER_GROU_ROLE_RTNS_ID VARCHAR(10) NOT NULL,
SYST_ID VARCHAR(10) NOT NULL,
USER_GROU_ID VARCHAR(10) NOT NULL,
ROLE_NO VARCHAR(50) NOT NULL,
PUSE_YN VARCHAR(1),
SYST_RGI_DT DATE,
SYST_RGI_PRAF_NO VARCHAR(8),
SYST_RGI_OGNZ_NO VARCHAR(7),
SYST_RGI_SYST_CD VARCHAR(3),
SYST_RGI_PRGR_ID VARCHAR(100),
SYST_CHG_DT DATE,
SYST_CHG_PRAF_NO VARCHAR(8),
SYST_CHG_OGNZ_NO VARCHAR(7),
SYST_CHG_SYST_CD VARCHAR(3),
SYST_CHG_PRGR_ID VARCHAR(100)
);
CREATE UNIQUE INDEX S_EIAM_TIS.PK_AA_USER_GROU_ROLE_RTNS
ON S_EIAM_TIS.AA_USER_GROU_ROLE_RTNS (USER_GROU_ROLE_RTNS_ID);
ALTER TABLE S_EIAM_TIS.AA_USER_GROU_ROLE_RTNS
ADD CONSTRAINT PK_AA_USER_GROU_ROLE_RTNS PRIMARY KEY (USER_GROU_ROLE_RTNS_ID);
COMMENT ON COLUMN S_EIAM_TIS.AA_USER_GROU_ROLE_RTNS.USER_GROU_ROLE_RTNS_ID IS '사용자그룹역할관계ID';
COMMENT ON COLUMN S_EIAM_TIS.AA_USER_GROU_ROLE_RTNS.SYST_ID IS '시스템ID';
COMMENT ON COLUMN S_EIAM_TIS.AA_USER_GROU_ROLE_RTNS.USER_GROU_ID IS '사용자그룹ID';
COMMENT ON COLUMN S_EIAM_TIS.AA_USER_GROU_ROLE_RTNS.ROLE_NO IS '역할번호';
COMMENT ON COLUMN S_EIAM_TIS.AA_USER_GROU_ROLE_RTNS.PUSE_YN IS '사용여부';
COMMENT ON COLUMN S_EIAM_TIS.AA_USER_GROU_ROLE_RTNS.SYST_RGI_DT IS '시스템등록일시';
COMMENT ON COLUMN S_EIAM_TIS.AA_USER_GROU_ROLE_RTNS.SYST_RGI_PRAF_NO IS '시스템등록인사번호';
COMMENT ON COLUMN S_EIAM_TIS.AA_USER_GROU_ROLE_RTNS.SYST_RGI_OGNZ_NO IS '시스템등록조직번호';
COMMENT ON COLUMN S_EIAM_TIS.AA_USER_GROU_ROLE_RTNS.SYST_RGI_SYST_CD IS '시스템등록시스템코드';
COMMENT ON COLUMN S_EIAM_TIS.AA_USER_GROU_ROLE_RTNS.SYST_RGI_PRGR_ID IS '시스템등록프로그램ID';
COMMENT ON COLUMN S_EIAM_TIS.AA_USER_GROU_ROLE_RTNS.SYST_CHG_DT IS '시스템변경일시';
COMMENT ON COLUMN S_EIAM_TIS.AA_USER_GROU_ROLE_RTNS.SYST_CHG_PRAF_NO IS '시스템변경인사번호';
COMMENT ON COLUMN S_EIAM_TIS.AA_USER_GROU_ROLE_RTNS.SYST_CHG_OGNZ_NO IS '시스템변경조직번호';
COMMENT ON COLUMN S_EIAM_TIS.AA_USER_GROU_ROLE_RTNS.SYST_CHG_SYST_CD IS '시스템변경시스템코드';
COMMENT ON COLUMN S_EIAM_TIS.AA_USER_GROU_ROLE_RTNS.SYST_CHG_PRGR_ID IS '시스템변경프로그램ID';
-- AA_OGNZ 테이블 생성 (조직)
CREATE TABLE S_EIAM_TIS.AA_OGNZ (
OGNZ_NM VARCHAR2(8) PRIMARY KEY, -- 인사번호 (PK)
OGNZ_ABR_NM VARCHAR2(200), -- 인사명
OGNZ_NO VARCHAR2(7) NOT NULL, -- 조직번호 (NOT NULL)
SPPO_GONZ_NO VARCHAR2(250), -- 이메일주소
VLDT_YMD VARCHAR2(10), -- 인사직무코드
PUSE_YN VARCHAR2(100), -- 인사직무명
SYST_RGI_DT DATE, -- 시스템등록일시
SYST_RGI_PRAF_NO VARCHAR2(8), -- 시스템등록인사번호
SYST_RGI_OGNZ_NO VARCHAR2(7), -- 시스템등록조직번호
SYST_RGI_SYST_CD VARCHAR2(3), -- 시스템등록시스템코드
SYST_RGI_PRGR_ID VARCHAR2(100), -- 시스템등록프로그램ID
SYST_CHG_DT DATE, -- 시스템변경일시
SYST_CHG_PRAF_NO VARCHAR2(8), -- 시스템변경인사번호
SYST_CHG_OGNZ_NO VARCHAR2(7), -- 시스템변경조직번호
SYST_CHG_SYST_CD VARCHAR2(3), -- 시스템변경시스템코드
SYST_CHG_PRGR_ID VARCHAR2(100) -- 시스템변경프로그램ID
);

View File

@@ -0,0 +1,8 @@
# SLF4J ??? ?? (logback?? ???)
appender=com.p6spy.engine.spy.appender.Slf4JLogger
# ??? ??? ??? ?? (???? ?? ??)
logMessageFormat=io.shinhanlife.dap.common.config.P6SpySqlFormatter
# ?? ?? 0ms?? (connection ?? ??) ???
filter.executionThreshold=1

View File

@@ -0,0 +1,805 @@
<!DOCTYPE html>
<html lang="ko" class="dark" data-bs-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AX HUB Developer Portal</title>
<!-- Geist Fonts -->
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource/geist-sans@5.0.1/400.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource/geist-sans@5.0.1/500.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource/geist-sans@5.0.1/600.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource/geist-mono@5.0.1/400.css">
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
:root {
--bg-color: #09090b;
--surface-color: #18181b;
--border-color: #3f3f46; /* zinc-700 */
--text-primary: #f4f4f5; /* zinc-100 */
--text-secondary: #a1a1aa; /* zinc-400 */
--accent-color: #3b82f6;
--accent-hover: #2563eb;
--link-color: #60a5fa; /* blue-400 */
--radius-md: 6px;
--radius-sm: 4px;
}
body {
font-family: 'Geist Sans', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background-color: var(--bg-color);
color: var(--text-primary);
min-height: 100vh;
padding: 0;
-webkit-font-smoothing: antialiased;
}
.container {
max-width: 1000px; /* Tighter layout */
width: 95%;
margin: 0 auto;
}
/* Editorial / Tech Header */
.page-header {
text-align: left;
margin-bottom: 2rem;
padding-bottom: 2rem;
border-bottom: 1px solid var(--border-color);
}
.page-header h1 {
font-weight: 600;
font-size: 2rem;
letter-spacing: -0.04em;
color: var(--text-primary);
margin-bottom: 0.25rem;
}
.page-header p {
color: var(--text-secondary);
font-size: 0.95rem;
margin: 0;
}
/* Clean Panel */
.panel {
background: var(--surface-color);
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
overflow: hidden;
}
/* Minimalist Tabs */
.nav-tabs {
border-bottom: 1px solid var(--border-color);
background: #0f0f11;
padding: 0 1.5rem;
gap: 1.5rem;
}
.nav-tabs .nav-item {
margin-bottom: -1px;
}
.nav-tabs .nav-link {
border: none;
color: var(--text-secondary);
font-weight: 500;
font-size: 0.875rem;
padding: 1rem 0;
border-bottom: 2px solid transparent;
transition: color 0.15s ease;
background: transparent !important;
}
.nav-tabs .nav-link:hover {
color: var(--text-primary);
border-color: transparent;
}
.nav-tabs .nav-link.active {
color: var(--text-primary);
border-bottom: 2px solid var(--text-primary);
font-weight: 600;
}
.tab-content {
padding: 2rem 1.5rem;
background: var(--surface-color);
}
/* Dense Forms */
.form-label {
font-weight: 500;
color: var(--text-primary);
font-size: 0.875rem;
margin-bottom: 0.35rem;
}
.form-control, .form-select {
border-radius: var(--radius-sm);
border: 1px solid var(--border-color);
padding: 0.5rem 0.75rem;
font-size: 0.875rem;
background-color: var(--surface-color);
color: var(--text-primary);
box-shadow: none;
}
.form-control:focus, .form-select:focus {
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.25);
background-color: var(--surface-color);
color: var(--text-primary);
}
.form-control:focus, .form-select:focus {
border-color: var(--text-secondary);
box-shadow: 0 0 0 1px var(--text-secondary);
outline: none;
}
.form-control::placeholder {
color: #a1a1aa; /* zinc-400 */
}
.input-hint {
font-size: 0.75rem;
color: var(--text-secondary);
margin-top: 0.35rem;
}
/* Utilitarian Button */
.btn-action {
background: var(--accent-color);
color: #ffffff;
border: none;
border-radius: var(--radius-sm);
padding: 0.5rem 1rem;
font-weight: 500;
font-size: 0.875rem;
transition: background-color 0.1s ease, transform 0.1s ease;
}
.btn-action:hover {
background: var(--accent-hover);
color: #ffffff;
}
.btn-action:active {
transform: scale(0.98); /* Tactile feedback */
}
.btn-secondary-action {
background: var(--surface-color);
color: var(--text-primary);
border: 1px solid var(--border-color);
border-radius: var(--radius-sm);
padding: 0.3rem 0.75rem;
font-size: 0.75rem;
font-weight: 500;
}
.btn-secondary-action:hover {
background: #f4f4f5;
}
/* Result Box - Terminal Style */
#resultBox {
display: none;
margin-top: 1.5rem;
border-radius: var(--radius-md);
padding: 1.25rem;
white-space: pre-wrap;
font-family: 'Geist Mono', Consolas, monospace;
font-size: 0.8rem;
line-height: 1.6;
background-color: #09090b; /* zinc-950 */
color: #e4e4e7; /* zinc-200 */
border: 1px solid #27272a;
}
#resultBox.success {
border-top: 3px solid #10b981;
}
#resultBox.error {
border-top: 3px solid #ef4444;
}
/* Dense Table */
.table {
font-size: 0.875rem;
margin-bottom: 0;
}
.table th {
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--text-secondary);
font-weight: 500;
border-bottom: 1px solid var(--border-color);
padding-bottom: 0.75rem;
background: transparent;
}
.table td {
vertical-align: middle;
padding: 1rem 0.5rem;
border-bottom: 1px solid var(--border-color);
color: var(--text-primary);
}
.table tr:last-child td {
border-bottom: none;
}
.badge-mono {
font-family: 'Geist Mono', monospace;
font-size: 0.7rem;
font-weight: 500;
padding: 0.25em 0.5em;
border-radius: 4px;
background: #f4f4f5;
color: var(--text-secondary);
border: 1px solid var(--border-color);
}
a.tool-link {
color: var(--text-primary);
text-decoration: none;
font-weight: 500;
}
a.tool-link:hover {
text-decoration: underline;
color: var(--link-color);
}
/* Modal styling overrides - DARK THEME */
.modal-content {
border-radius: var(--radius-md);
border: 1px solid var(--border-color);
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.8);
background-color: #18181b;
color: var(--text-primary);
}
.modal-header {
border-bottom: 1px solid var(--border-color);
padding: 1.25rem 1.5rem;
background-color: #18181b;
}
.modal-title {
font-size: 1.1rem;
font-weight: 600;
color: var(--text-primary);
}
.modal-body {
background-color: #18181b;
color: var(--text-primary);
}
.modal-body .form-label {
color: #d4d4d8 !important;
}
.modal-footer {
border-top: 1px solid var(--border-color);
padding: 1rem 1.5rem;
background-color: #18181b;
}
.modal-backdrop.show {
opacity: 0.7;
}
.btn-close {
filter: invert(1);
}
/* Badge overrides for dark theme */
.badge-mono {
font-family: 'Geist Mono', monospace;
font-size: 0.7rem;
font-weight: 500;
padding: 0.25em 0.5em;
border-radius: 4px;
background: #27272a;
color: #a1a1aa;
border: 1px solid #3f3f46;
}
/* Bootstrap secondary button override */
.btn-secondary-action:hover {
background: #27272a;
color: var(--text-primary);
}
/* Table hover for dark */
.table > tbody > tr:hover > td {
background-color: rgba(255,255,255,0.03);
}
/* Tool name in table - ensure visibility */
.table td {
color: #e4e4e7 !important;
}
.table td a.tool-link {
color: #e4e4e7 !important;
}
.table td a.tool-link:hover {
color: #60a5fa !important;
}
/* Category badge in table */
.badge {
font-size: 0.7rem;
font-weight: 500;
}
/* Alert/Notice dark theme */
.alert-warning {
background-color: rgba(245, 158, 11, 0.1);
border-color: rgba(245, 158, 11, 0.3);
color: #fbbf24;
}
/* Select option dark theme */
select option, select optgroup {
background-color: #18181b;
color: #f4f4f5;
}
.container {
max-width: 72rem !important;
padding-left: 1.5rem !important;
padding-right: 1.5rem !important;
width: 100% !important;
}
</style>
</head>
<body style="overflow-y: scroll;">
<header style="border-bottom: 1px solid #27272a; background: rgba(9,9,11,0.85); backdrop-filter: blur(16px);" class="sticky top-0 z-50">
<div class="max-w-6xl mx-auto px-6 h-14 flex items-center justify-between">
<div class="flex items-center space-x-5">
<a href="/index.html" class="flex items-center group">
<div class="w-2 h-2 rounded-full mr-2" style="background:#3b82f6; box-shadow: 0 0 8px rgba(59,130,246,0.8);"></div>
<span class="font-semibold tracking-tight text-sm" style="color:#f4f4f5;">AXHUB Gateway</span>
</a>
<div class="h-4 w-px" style="background:#27272a;"></div>
<nav class="flex space-x-5 text-[13px] font-medium">
<a href="/admin/scaffold.html" style="color:#ffffff;" class="font-semibold">Scaffold</a>
<a href="/catalog.html" style="color:#a1a1aa;" class="hover:text-white transition-colors">Catalog</a>
<a href="/playground.html" style="color:#a1a1aa;" class="hover:text-white transition-colors">Playground</a>
</nav>
</div>
<div class="flex items-center">
<span class="text-[10px] uppercase tracking-widest px-2 py-1 rounded font-bold" style="background:rgba(59,130,246,0.1); color:#60a5fa; border:1px solid rgba(59,130,246,0.2);">v0.0.1</span>
</div>
</div>
</header>
<div class="container">
<div class="page-header">
<h1>Developer Portal</h1>
<p>AX HUB No-Code Scaffolding Wizard</p>
</div>
<div class="panel">
<ul class="nav nav-tabs" id="myTab" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="pod-tab" data-bs-toggle="tab" data-bs-target="#pod" type="button" role="tab">Pod Module</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="tool-tab" data-bs-toggle="tab" data-bs-target="#tool" type="button" role="tab">Tool Function</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="list-tab" data-bs-toggle="tab" data-bs-target="#list" type="button" role="tab" onclick="loadToolList()">Registry</button>
</li>
</ul>
<div class="card-body p-0">
<div class="tab-content" id="myTabContent">
<!-- Pod Creation Form -->
<div class="tab-pane fade show active" id="pod" role="tabpanel">
<form id="podForm">
<div class="mb-4">
<label class="form-label">Module Name</label>
<input type="text" class="form-control" name="moduleName" placeholder="e.g. hr (will be prefixed with axhub-tool-)" required>
</div>
<div class="mb-4">
<label class="form-label">Service Port</label>
<input type="number" class="form-control" name="port" placeholder="e.g. 8086" required>
<div class="input-hint">Unique port for Gateway routing and local development.</div>
</div>
<div class="row mb-4">
<div class="col-md-6">
<label class="form-label">Author</label>
<input type="text" class="form-control" name="author" placeholder="Defaults to OS user">
</div>
<div class="col-md-6 mt-3 mt-md-0">
<label class="form-label">Date</label>
<input type="text" class="form-control" name="date" placeholder="Defaults to today">
</div>
</div>
<div class="d-flex justify-content-end mt-4">
<button type="submit" class="btn-action">Generate Pod</button>
</div>
</form>
</div>
<!-- Tool Creation Form -->
<div class="tab-pane fade" id="tool" role="tabpanel">
<form id="toolForm">
<div class="row mb-3">
<div class="col-md-6">
<label class="form-label">Base Name (PascalCase)</label>
<input type="text" class="form-control" name="baseName" placeholder="e.g. ExchangeRate" required>
<div class="input-hint">Used for class generation (ExchangeRateService).</div>
</div>
<div class="col-md-6 mt-3 mt-md-0">
<label class="form-label">Legacy Interface ID</label>
<input type="text" class="form-control" name="interfaceId" placeholder="e.g. EXCH_001" required>
</div>
</div>
<div class="mb-3">
<label class="form-label">Description (LLM Prompt)</label>
<input type="text" class="form-control" name="description" placeholder="e.g. Fetch current exchange rates for customers." required>
<div class="input-hint">Critical for AI agent intent matching. Be descriptive.</div>
</div>
<div class="row mb-3">
<div class="col-md-6">
<label class="form-label">Domain Category</label>
<input type="text" class="form-control" name="categoryKey" pattern="^[a-z0-9][a-z0-9_-]*$" list="groupList" placeholder="e.g. common, hr, payment" oninput="this.value = this.value.toLowerCase()" required>
<datalist id="groupList">
<option value="common">common</option>
<option value="customer">customer</option>
<option value="contract">contract</option>
<option value="claim">claim</option>
</datalist>
</div>
<div class="col-md-6 mt-3 mt-md-0">
<label class="form-label">Protocol</label>
<select class="form-select" name="routingType">
<option value="HTTP">HTTP (REST)</option>
<option value="TCP">TCP (Socket)</option>
<option value="MCI">MCI (Legacy)</option>
<option value="EAI">EAI (Internal)</option>
</select>
</div>
</div>
<div class="row mb-4">
<div class="col-md-6">
<label class="form-label">Target Module</label>
<select class="form-select" name="moduleName" id="targetModuleSelect" required>
<option value="axhub-tool-other">axhub-tool-other</option>
</select>
<div class="input-hint">Destination Pod directory.</div>
</div>
<div class="col-md-6 mt-3 mt-md-0">
<label class="form-label">Gateway Registration</label>
<select class="form-select" name="register">
<option value="true">True (Public)</option>
<option value="false">False (Private)</option>
</select>
<div class="input-hint">Controls dynamic routing exposure.</div>
</div>
</div>
<div class="row mb-4">
<div class="col-md-6">
<label class="form-label">Author</label>
<input type="text" class="form-control" name="author" placeholder="Defaults to OS user">
</div>
<div class="col-md-6 mt-3 mt-md-0">
<label class="form-label">Date</label>
<input type="text" class="form-control" name="date" placeholder="Defaults to today">
</div>
</div>
<div class="d-flex justify-content-end mt-4">
<button type="submit" class="btn-action">Generate Tool</button>
</div>
</form>
</div>
<!-- Tool List Form -->
<div class="tab-pane fade" id="list" role="tabpanel">
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h5 class="mb-1" style="font-size: 1rem; font-weight: 600;">Active Tools</h5>
<div class="input-hint mt-0">Tools dynamically registered in the Gateway.</div>
</div>
<div class="d-flex gap-2">
<select id="groupFilterSelect" class="form-select" style="width: auto; min-width: 140px; padding: 0.25rem 0.5rem; font-size: 0.8rem; height: 30px;" onchange="filterTools()">
<option value="ALL">All Categories</option>
</select>
<input type="text" id="toolFilter" class="form-control" style="padding: 0.25rem 0.5rem; font-size: 0.8rem; height: 30px;" placeholder="Search..." onkeyup="filterTools()">
<button class="btn-secondary-action text-nowrap" style="height: 30px;" onclick="loadToolList()">Refresh</button>
</div>
</div>
<div class="table-responsive">
<table class="table align-middle" id="toolTable">
<thead>
<tr>
<th style="width: 10%;">Category</th>
<th style="width: 20%;">Tool Name</th>
<th style="width: 40%;">Description</th>
<th style="width: 15%;">Pod</th>
<th style="width: 15%;">Interface</th>
</tr>
</thead>
<tbody id="toolListBody">
<tr>
<td colspan="5" class="text-center py-5 text-muted">Loading data...</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<div id="resultBox"></div>
</div>
<!-- Edit Tool Modal -->
<div class="modal fade" id="editToolModal" tabindex="-1" aria-labelledby="editToolModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="editToolModalLabel">Edit Tool Definition</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form id="editToolForm">
<div class="mb-3">
<label class="form-label">Tool Name</label>
<input type="text" class="form-control" id="editToolName" name="toolName" readonly style="background-color: #f4f4f5; color: #a1a1aa;">
</div>
<div class="mb-3">
<label class="form-label">Category</label>
<input type="text" class="form-control" id="editCategoryKey" name="categoryKey" pattern="^[a-z0-9][a-z0-9_-]*$" oninput="this.value = this.value.toLowerCase()">
</div>
<div class="mb-3">
<label class="form-label">Description</label>
<textarea class="form-control" id="editDescription" name="description" rows="3"></textarea>
</div>
<div class="mb-3">
<label class="form-label">Registry Exposure</label>
<select class="form-select" id="editRegister" name="register">
<option value="true">True (Public)</option>
<option value="false">False (Private)</option>
</select>
</div>
<div style="font-size: 0.8rem; color: #d97706; background: #fffbeb; padding: 0.75rem; border: 1px solid #fde68a; border-radius: var(--radius-sm); margin-top: 1rem;">
<strong>Notice:</strong> Applying edits modifies the physical Java source code. A rebuild of the backend container is required for changes to take effect.
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn-secondary-action" data-bs-dismiss="modal" style="padding: 0.5rem 1rem; font-size: 0.875rem;">Cancel</button>
<button type="button" class="btn-action" onclick="submitToolUpdate()">Apply Changes</button>
</div>
</div>
</div>
</div>
<!-- Bootstrap JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
function handleFormSubmit(formId, apiUrl) {
document.getElementById(formId).addEventListener('submit', function(e) {
e.preventDefault();
const formData = new FormData(this);
const data = Object.fromEntries(formData.entries());
const btn = this.querySelector('button[type="submit"]');
const originalText = btn.innerHTML;
btn.innerHTML = 'Processing...';
btn.disabled = true;
btn.style.opacity = '0.7';
fetch(apiUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
})
.then(response => response.text())
.then(result => {
const resultBox = document.getElementById('resultBox');
resultBox.style.display = 'block';
if (result.includes('[오류]') || result.includes('오류 발생:')) {
resultBox.className = 'error';
resultBox.textContent = result;
} else {
resultBox.className = 'success';
resultBox.textContent = result + "\n\n> Note: Please sync Gradle in your IDE to reflect source code generation.";
}
})
.catch(error => {
const resultBox = document.getElementById('resultBox');
resultBox.style.display = 'block';
resultBox.className = 'error';
resultBox.textContent = "Network error: " + error;
})
.finally(() => {
btn.innerHTML = originalText;
btn.disabled = false;
btn.style.opacity = '1';
});
});
}
document.addEventListener('DOMContentLoaded', function() {
fetch('/api/v1/scaffold/modules')
.then(res => res.json())
.then(modules => {
const select = document.getElementById('targetModuleSelect');
if (modules && modules.length > 0) {
select.innerHTML = modules.map(m => `<option value="${m}" ${m === 'axhub-tool-other' ? 'selected' : ''}>${m}</option>`).join('');
}
})
.catch(err => console.error('Failed to load modules:', err));
});
handleFormSubmit('podForm', '/api/v1/scaffold/pod');
handleFormSubmit('toolForm', '/api/v1/scaffold/tool');
function loadToolList() {
const tbody = document.getElementById('toolListBody');
tbody.innerHTML = '<tr><td colspan="5" class="text-center py-5 text-muted">Loading data...</td></tr>';
fetch('/mcp/api/v1/tools/list', {
method: 'GET',
})
.then(res => res.json())
.then(data => {
let tools = data.result.tools || [];
if (tools.length === 0) {
tbody.innerHTML = '<tr><td colspan="5" class="text-center py-5 text-muted">No tools registered.</td></tr>';
return;
}
tools.sort((a, b) => {
const gA = a.categoryKey || '';
const gB = b.categoryKey || '';
return gA.localeCompare(gB);
});
const groupSet = new Set();
tools.forEach(t => groupSet.add(t.categoryKey || 'unknown'));
const groupSelect = document.getElementById('groupFilterSelect');
const currentSelectedGroup = groupSelect.value;
groupSelect.innerHTML = '<option value="ALL">All Categories</option>';
Array.from(groupSet).sort().forEach(g => {
const opt = document.createElement('option');
opt.value = g;
opt.textContent = g;
groupSelect.appendChild(opt);
});
if (groupSet.has(currentSelectedGroup)) {
groupSelect.value = currentSelectedGroup;
}
tbody.innerHTML = tools.map(t => {
const isReg = t.registered !== false;
const badgeHtml = isReg ? '' : ' <span class="badge-mono" style="color:#d97706;border-color:#fde68a;background:#fffbeb;">Private</span>';
const escDesc = (t.description || '').replace(/'/g, "\\'").replace(/"/g, '&quot;');
return `
<tr class="tool-row">
<td><span class="badge-mono">${t.categoryKey || 'unknown'}</span></td>
<td>
<a href="javascript:void(0)" onclick="openEditModal('${t.name}', '${t.categoryKey}', '${escDesc}', ${isReg})" class="tool-link">
${t.name}
</a>
${badgeHtml}
</td>
<td class="text-secondary" style="font-size:0.8rem;">${t.description}</td>
<td><span class="badge-mono">${t.podUrl}</span></td>
<td><span class="badge-mono">${t.integrationType}</span> <span class="text-secondary ms-1" style="font-size:0.75rem;">${t.mciServiceId || '-'}</span></td>
</tr>
`}).join('');
filterTools();
})
.catch(err => {
tbody.innerHTML = `<tr><td colspan="5" class="text-center py-5 text-danger">Error loading data: ${err.message}</td></tr>`;
});
}
function filterTools() {
const input = document.getElementById('toolFilter').value.toLowerCase();
const selectedGroup = document.getElementById('groupFilterSelect').value;
const rows = document.querySelectorAll('.tool-row');
rows.forEach(row => {
const name = row.cells[1].textContent.toLowerCase();
const group = row.cells[0].textContent;
const matchText = name.includes(input) || group.toLowerCase().includes(input);
const matchGroup = (selectedGroup === 'ALL' || group === selectedGroup);
if (matchText && matchGroup) {
row.style.display = '';
} else {
row.style.display = 'none';
}
});
}
let editModalInstance = null;
function openEditModal(toolName, categoryKey, description, isReg) {
document.getElementById('editToolName').value = toolName;
document.getElementById('editCategoryKey').value = categoryKey !== 'null' ? categoryKey : '';
document.getElementById('editDescription').value = description;
document.getElementById('editRegister').value = isReg ? 'true' : 'false';
if (!editModalInstance) {
editModalInstance = new bootstrap.Modal(document.getElementById('editToolModal'));
}
editModalInstance.show();
}
function submitToolUpdate() {
const toolName = document.getElementById('editToolName').value;
const categoryKey = document.getElementById('editCategoryKey').value;
const description = document.getElementById('editDescription').value;
const register = document.getElementById('editRegister').value;
const btn = document.querySelector('#editToolModal .btn-action');
const originalText = btn.innerHTML;
btn.innerHTML = 'Processing...';
btn.disabled = true;
btn.style.opacity = '0.7';
fetch('/api/v1/scaffold/tool/update', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
toolName: toolName,
categoryKey: categoryKey,
description: description,
register: register
})
})
.then(response => response.text())
.then(result => {
if (result.includes('[오류]') || result.includes('오류 발생:')) {
alert('Error updating source:\n' + result);
} else {
alert('Source code updated successfully.\nPlease rebuild the Pod container to reflect changes.');
editModalInstance.hide();
loadToolList();
}
})
.catch(err => {
alert('Network error: ' + err);
})
.finally(() => {
btn.innerHTML = originalText;
btn.disabled = false;
btn.style.opacity = '1';
});
}
</script>
</body>
</html>

View File

@@ -0,0 +1,233 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI Tool Catalog</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://fonts.googleapis.com/css2?family=Geist:wght@300;400;500;600;700&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<style>
* { font-family: 'Geist', sans-serif; }
body { background-color: #09090b; color: #f4f4f5; }
.geist-mono { font-family: 'Geist Mono', monospace; }
.btn-primary {
background: linear-gradient(135deg, #3b82f6, #6366f1);
color: #ffffff;
font-weight: 600;
padding: 10px 24px;
border-radius: 8px;
font-size: 14px;
border: none;
cursor: pointer;
transition: all 0.2s;
box-shadow: 0 0 20px rgba(59, 130, 246, 0.3);
}
.btn-primary:hover { box-shadow: 0 0 30px rgba(99, 102, 241, 0.5); transform: translateY(-1px); }
.tool-card {
background: #18181b;
border: 1px solid #3f3f46;
border-radius: 12px;
padding: 20px 24px;
transition: border-color 0.2s;
}
.tool-card:hover { border-color: #52525b; }
.param-table {
width: 100%;
font-size: 13px;
border-collapse: collapse;
}
.param-table th {
text-align: left;
padding: 8px 12px;
font-weight: 600;
color: #a1a1aa;
border-bottom: 1px solid #3f3f46;
font-size: 12px;
}
.param-table td {
padding: 8px 12px;
border-bottom: 1px solid #27272a;
color: #d4d4d8;
}
.param-table tr:last-child td { border-bottom: none; }
</style>
</head>
<body class="min-h-screen" style="overflow-y: scroll;">
<header style="border-bottom: 1px solid #27272a; background: rgba(9,9,11,0.85); backdrop-filter: blur(16px);" class="sticky top-0 z-50">
<div class="max-w-6xl mx-auto px-6 h-14 flex items-center justify-between">
<div class="flex items-center space-x-5">
<a href="/index.html" class="flex items-center group">
<div class="w-2 h-2 rounded-full mr-2" style="background:#3b82f6; box-shadow: 0 0 8px rgba(59,130,246,0.8);"></div>
<span class="font-semibold tracking-tight text-sm" style="color:#f4f4f5;">AXHUB Gateway</span>
</a>
<div class="h-4 w-px" style="background:#27272a;"></div>
<nav class="flex space-x-5 text-[13px] font-medium">
<a href="/admin/scaffold.html" style="color:#a1a1aa;" class="hover:text-white transition-colors">Scaffold</a>
<a href="/catalog.html" style="color:#ffffff;" class="font-semibold">Catalog</a>
<a href="/playground.html" style="color:#a1a1aa;" class="hover:text-white transition-colors">Playground</a>
</nav>
</div>
<div class="flex items-center">
<span class="text-[10px] uppercase tracking-widest px-2 py-1 rounded font-bold" style="background:rgba(59,130,246,0.1); color:#60a5fa; border:1px solid rgba(59,130,246,0.2);">v0.0.1</span>
</div>
</div>
</header>
<main class="max-w-6xl mx-auto px-6 py-10">
<div class="flex flex-col md:flex-row justify-between items-start md:items-end mb-8">
<div class="max-w-2xl">
<h1 class="text-3xl font-bold tracking-tight mb-2" style="color:#f4f4f5;">Registry Catalog</h1>
<p class="text-sm" style="color:#a1a1aa;">Currently integrated AI function tools across all modules. View parameters, descriptions, and integration types.</p>
</div>
<div class="mt-4 md:mt-0">
<button id="copyMdBtn" class="btn-primary flex items-center">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3"></path></svg>
<span id="copyBtnText">Copy Markdown Docs</span>
</button>
</div>
</div>
<div id="catalogContainer" class="space-y-12 pb-16">
<div class="flex justify-center py-20 text-sm geist-mono" style="color:#71717a;">
<div class="w-4 h-4 border-2 border-t-blue-500 rounded-full animate-spin mr-3" style="border-color:#3f3f46; border-top-color:#3b82f6;"></div>
Loading registry...
</div>
</div>
</main>
<script>
async function fetchTools() {
try {
const response = await fetch('/mcp/api/v1/tools/list');
const data = await response.json();
const tools = data.result?.tools || [];
renderCatalog(tools);
} catch (err) {
document.getElementById('catalogContainer').innerHTML = `
<div style="padding:16px; border:1px solid #7f1d1d; background:rgba(127,29,29,0.15); color:#fca5a5; font-size:14px; border-radius:8px;">
Failed to fetch tools from registry. Error: ${err.message}
</div>
`;
}
}
function renderCatalog(tools) {
const container = document.getElementById('catalogContainer');
container.innerHTML = '';
if (tools.length === 0) {
container.innerHTML = '<div style="padding:32px; text-align:center; color:#71717a; border:1px dashed #3f3f46; border-radius:8px;">No tools registered.</div>';
return;
}
const grouped = {};
tools.forEach(tool => {
const group = tool.categoryKey || 'Others';
if (!grouped[group]) grouped[group] = [];
grouped[group].push(tool);
});
Object.keys(grouped).sort().forEach(group => {
const section = document.createElement('section');
const header = document.createElement('div');
header.style.cssText = 'margin-bottom:16px; padding-bottom:8px; border-bottom:1px solid #27272a; display:flex; align-items:baseline; justify-content:space-between;';
header.innerHTML = `
<h2 style="font-size:20px; font-weight:600; color:#f4f4f5; text-transform:capitalize;">${group}</h2>
<span class="geist-mono" style="font-size:12px; color:#71717a;">${grouped[group].length} items</span>
`;
section.appendChild(header);
const grid = document.createElement('div');
grid.style.cssText = 'display:grid; grid-template-columns:1fr; gap:12px;';
grouped[group].forEach(tool => grid.appendChild(createToolCard(tool)));
section.appendChild(grid);
container.appendChild(section);
});
}
function createToolCard(tool) {
const card = document.createElement('div');
card.className = 'tool-card';
const isDirect = tool.integrationType === 'DIRECT';
const badgeBg = isDirect ? 'rgba(59,130,246,0.15)' : 'rgba(249,115,22,0.15)';
const badgeColor = isDirect ? '#60a5fa' : '#fb923c';
const badgeBorder = isDirect ? 'rgba(59,130,246,0.3)' : 'rgba(249,115,22,0.3)';
let paramHtml = '';
if (tool.parametersSchema && tool.parametersSchema.properties) {
const props = tool.parametersSchema.properties;
const required = tool.parametersSchema.required || [];
if (Object.keys(props).length > 0) {
paramHtml = `
<div style="margin-top:16px; padding-top:16px; border-top:1px solid #27272a;">
<div style="font-size:10px; font-weight:700; color:#71717a; margin-bottom:8px; letter-spacing:0.1em; text-transform:uppercase;">Parameters</div>
<table class="param-table">
<thead><tr><th style="width:25%;">Name</th><th style="width:15%;">Type</th><th>Description</th></tr></thead>
<tbody>
${Object.entries(props).map(([k,v]) => `
<tr>
<td class="geist-mono" style="color:#93c5fd;">${k}${required.includes(k) ? '<span style="color:#f87171; margin-left:4px;">*</span>' : ''}</td>
<td class="geist-mono" style="color:#a1a1aa;">${v.type || 'string'}</td>
<td style="color:#d4d4d8;">${v.description || '-'}</td>
</tr>
`).join('')}
</tbody>
</table>
</div>
`;
}
}
let promptHtml = '';
if (tool.actionPrompts && Object.keys(tool.actionPrompts).length > 0) {
promptHtml = `
<div style="margin-top:16px; padding-top:16px; border-top:1px solid #27272a;">
<div style="font-size:10px; font-weight:700; color:#71717a; margin-bottom:8px; letter-spacing:0.1em; text-transform:uppercase;">Action Prompts</div>
<ul style="list-style:disc; padding-left:20px; font-size:14px; color:#a1a1aa; space-y:2;">
${Object.values(tool.actionPrompts).map(p => `<li style="margin-bottom:4px;">${p}</li>`).join('')}
</ul>
</div>
`;
}
card.innerHTML = `
<div style="display:flex; align-items:flex-start; justify-content:space-between;">
<div style="flex:1;">
<div style="display:flex; align-items:center; margin-bottom:4px;">
<h3 style="font-size:16px; font-weight:600; color:#f4f4f5;">${tool.displayName || tool.name}</h3>
<span style="margin:0 8px; color:#3f3f46;">/</span>
<span class="geist-mono" style="font-size:12px; color:#71717a;">${tool.name}</span>
</div>
<p style="font-size:14px; color:#a1a1aa;">${tool.description || 'No description provided.'}</p>
</div>
<div style="margin-left:16px; padding:4px 10px; border-radius:6px; font-size:11px; font-weight:600; background:${badgeBg}; color:${badgeColor}; border:1px solid ${badgeBorder};" class="geist-mono">
${tool.integrationType || 'DIRECT'}
</div>
</div>
${paramHtml}
${promptHtml}
`;
return card;
}
document.getElementById('copyMdBtn').addEventListener('click', async (e) => {
const btn = e.currentTarget;
const textSpan = document.getElementById('copyBtnText');
const originalText = textSpan.textContent;
try {
const res = await fetch('/mcp/api/v1/tools/docs/markdown');
if(!res.ok) throw new Error('Failed to fetch markdown');
const mdText = await res.text();
await navigator.clipboard.writeText(mdText);
textSpan.textContent = 'Copied!';
setTimeout(() => { textSpan.textContent = originalText; }, 2000);
} catch (err) {
alert('Failed to copy markdown.');
}
});
document.addEventListener('DOMContentLoaded', fetchTools);
</script>
</body>
</html>

View File

@@ -0,0 +1,235 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AX HUB - ChatClient</title>
<script src="https://unpkg.com/@tailwindcss/browser@4"></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
<style>
body { font-family: 'Inter', sans-serif; background-color: #0f1115; color: #e2e8f0; }
.scrollbar-hide::-webkit-scrollbar { display: none; }
.scrollbar-hide { -ms-overflow-style: none; scrollbar-width: none; }
.message-enter { animation: fadeInUp 0.3s ease-out forwards; }
@keyframes fadeInUp {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.typing-dot { animation: typing 1.4s infinite ease-in-out; fill: #94a3b8; }
.typing-dot:nth-child(1) { animation-delay: 0s; }
.typing-dot:nth-child(2) { animation-delay: 0.2s; }
.typing-dot:nth-child(3) { animation-delay: 0.4s; }
@keyframes typing {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-3px); }
}
</style>
</head>
<body class="h-screen flex flex-col items-center justify-center p-4">
<!-- Chat Container -->
<div class="w-full max-w-3xl h-[85vh] flex flex-col bg-[#16181d] rounded-2xl shadow-2xl overflow-hidden border border-white/5 relative">
<!-- Header -->
<div class="px-6 py-4 border-b border-white/5 flex items-center justify-between bg-[#16181d] z-10">
<div class="flex items-center gap-3">
<div class="w-8 h-8 rounded-full bg-emerald-500/20 flex items-center justify-center">
<svg class="w-4 h-4 text-emerald-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"></path></svg>
</div>
<div>
<h1 class="text-sm font-semibold tracking-wide text-white">AX HUB Assistant</h1>
<p class="text-[11px] text-slate-500">Powered by Mock Engine & MCP Tools</p>
</div>
</div>
<div class="flex items-center gap-2">
<span class="relative flex h-2.5 w-2.5">
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75"></span>
<span class="relative inline-flex rounded-full h-2.5 w-2.5 bg-emerald-500"></span>
</span>
<span class="text-xs text-slate-400">Online</span>
</div>
</div>
<!-- Chat Area -->
<div id="chat-box" class="flex-1 overflow-y-auto p-6 flex flex-col gap-6 scrollbar-hide scroll-smooth">
<!-- System Greeting -->
<div class="flex gap-4 message-enter">
<div class="w-8 h-8 rounded-full bg-emerald-500/10 flex-shrink-0 flex items-center justify-center mt-1 border border-emerald-500/20">
<svg class="w-4 h-4 text-emerald-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z"></path></svg>
</div>
<div class="flex flex-col gap-1 max-w-[80%]">
<span class="text-xs text-slate-500 ml-1 font-medium">Assistant</span>
<div class="bg-[#1e2128] px-5 py-3.5 rounded-2xl rounded-tl-sm text-sm text-slate-200 leading-relaxed border border-white/5 shadow-sm">
안녕하세요! 저는 AX HUB Assistant입니다. <br>
채팅을 통해 MCP(도구)들을 직접 호출해보세요. <br><br>
💡 <strong>예시:</strong> "오늘 서울 날씨 어때?"
</div>
</div>
</div>
</div>
<!-- Input Area -->
<div class="p-4 bg-[#16181d] border-t border-white/5 relative">
<div class="relative flex items-center w-full max-w-full mx-auto group">
<input type="text" id="chat-input" placeholder="메시지를 입력하세요... (예: 서울 날씨 어때?)"
class="w-full bg-[#1e2128] text-slate-200 text-sm px-5 py-4 rounded-xl border border-white/5 focus:outline-none focus:border-emerald-500/50 focus:ring-1 focus:ring-emerald-500/50 transition-all placeholder-slate-600 pr-12 shadow-inner"
onkeypress="handleKeyPress(event)">
<button onclick="sendMessage()" class="absolute right-2 p-2 rounded-lg bg-emerald-500 hover:bg-emerald-400 text-white transition-colors flex items-center justify-center shadow-lg shadow-emerald-500/20">
<svg class="w-4 h-4 translate-x-[1px]" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8"></path></svg>
</button>
</div>
</div>
</div>
<!-- 로딩 인디케이터 템플릿 -->
<template id="loading-template">
<div class="flex gap-4 message-enter" id="loading-indicator">
<div class="w-8 h-8 rounded-full bg-emerald-500/10 flex-shrink-0 flex items-center justify-center mt-1 border border-emerald-500/20">
<svg class="w-4 h-4 text-emerald-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z"></path></svg>
</div>
<div class="flex flex-col gap-1 max-w-[80%]">
<span class="text-xs text-slate-500 ml-1 font-medium">Assistant</span>
<div class="bg-[#1e2128] px-5 py-4 rounded-2xl rounded-tl-sm border border-white/5 flex items-center gap-1 shadow-sm h-[48px]">
<svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<circle class="typing-dot" cx="4" cy="12" r="3"/>
<circle class="typing-dot" cx="12" cy="12" r="3"/>
<circle class="typing-dot" cx="20" cy="12" r="3"/>
</svg>
</div>
</div>
</div>
</template>
<script>
const chatBox = document.getElementById('chat-box');
const chatInput = document.getElementById('chat-input');
const loadingTemplate = document.getElementById('loading-template');
let isLoading = false;
function handleKeyPress(e) {
if (e.key === 'Enter') {
sendMessage();
}
}
function appendUserMessage(text) {
const div = document.createElement('div');
div.className = 'flex gap-4 justify-end message-enter';
div.innerHTML = `
<div class="flex flex-col gap-1 max-w-[80%] items-end">
<span class="text-xs text-slate-500 mr-1 font-medium">You</span>
<div class="bg-emerald-500/10 px-5 py-3.5 rounded-2xl rounded-tr-sm text-sm text-emerald-50 leading-relaxed border border-emerald-500/20 shadow-sm whitespace-pre-wrap">${escapeHtml(text)}</div>
</div>
`;
chatBox.appendChild(div);
scrollToBottom();
}
function appendBotMessageContainer() {
const div = document.createElement('div');
div.className = 'flex gap-4 message-enter';
div.innerHTML = `
<div class="w-8 h-8 rounded-full bg-emerald-500/10 flex-shrink-0 flex items-center justify-center mt-1 border border-emerald-500/20">
<svg class="w-4 h-4 text-emerald-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z"></path></svg>
</div>
<div class="flex flex-col gap-1 max-w-[80%]">
<span class="text-xs text-slate-500 ml-1 font-medium">Assistant</span>
<div class="bg-[#1e2128] px-5 py-3.5 rounded-2xl rounded-tl-sm text-sm text-slate-200 leading-relaxed border border-white/5 shadow-sm whitespace-pre-wrap bot-text"></div>
</div>
`;
chatBox.appendChild(div);
scrollToBottom();
return div.querySelector('.bot-text');
}
function showLoading() {
const clone = loadingTemplate.content.cloneNode(true);
chatBox.appendChild(clone);
scrollToBottom();
}
function hideLoading() {
const loadingIndicator = document.getElementById('loading-indicator');
if (loadingIndicator) {
loadingIndicator.remove();
}
}
function scrollToBottom() {
chatBox.scrollTo({ top: chatBox.scrollHeight, behavior: 'smooth' });
}
function escapeHtml(unsafe) {
return unsafe
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
async function sendMessage() {
const text = chatInput.value.trim();
if (!text || isLoading) return;
// 1. 유저 메시지 추가
appendUserMessage(text);
chatInput.value = '';
isLoading = true;
// 2. 로딩 애니메이션
showLoading();
try {
// 3. 백엔드 통신 (SSE Stream)
const response = await fetch('/api/chat', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Agent-Id': 'TESTER-DEV' // 권한 통과를 위한 테스트 Agent ID
},
body: JSON.stringify({ message: text })
});
if (!response.ok) throw new Error('Network response was not ok');
// 4. 로딩 숨기고 봇 응답 컨테이너 생성
hideLoading();
const textContainer = appendBotMessageContainer();
// 5. 스트림 읽기
const reader = response.body.getReader();
const decoder = new TextDecoder("utf-8");
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value, { stream: true });
const lines = chunk.split('\n');
for (const line of lines) {
if (line.startsWith('data:')) {
const data = line.substring(5);
textContainer.innerHTML += escapeHtml(data);
scrollToBottom();
}
}
}
} catch (error) {
console.error(error);
hideLoading();
const textContainer = appendBotMessageContainer();
textContainer.innerHTML = "서버와의 통신에 실패했습니다. Gateway가 켜져있는지 확인해주세요.";
} finally {
isLoading = false;
}
}
</script>
</body>
</html>

View File

@@ -0,0 +1,169 @@
<!DOCTYPE html>
<html lang="ko" class="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AXHUB Gateway</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://fonts.googleapis.com/css2?family=Geist:wght@300;400;500;600;700&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<script>
tailwind.config = {
darkMode: 'class',
theme: {
extend: {
fontFamily: {
sans: ['Geist', 'sans-serif'],
mono: ['Geist Mono', 'monospace']
},
colors: {
background: '#09090b', /* zinc-950 */
panel: '#18181b', /* zinc-900 */
foreground: '#f4f4f5', /* zinc-100 */
muted: '#a1a1aa', /* zinc-400 */
border: 'rgba(255,255,255,0.1)',
primary: '#3b82f6', /* blue-500 */
primaryHover: '#2563eb', /* blue-600 */
}
}
}
}
</script>
<style>
body {
background-color: theme('colors.background');
color: theme('colors.foreground');
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* Subtle radial background for depth */
body::before {
content: "";
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: radial-gradient(circle at 50% -20%, rgba(59, 130, 246, 0.15), transparent 70%);
pointer-events: none;
z-index: -1;
}
.geist-mono { font-family: 'Geist Mono', monospace; }
.card-panel {
background: rgba(24, 24, 27, 0.7); /* zinc-900 with transparency */
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
border: 1px solid theme('colors.border');
border-radius: 12px;
box-shadow: 0 4px 6px -1px rgba(0,0,0,0.5), inset 0 1px 0 rgba(255,255,255,0.05);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.card-panel:hover {
transform: translateY(-4px);
box-shadow: 0 12px 20px -8px rgba(0,0,0,0.6), 0 0 15px rgba(59, 130, 246, 0.15), inset 0 1px 0 rgba(255,255,255,0.1);
border-color: rgba(255,255,255,0.2);
background: rgba(39, 39, 42, 0.8); /* zinc-800 */
}
.premium-text-gradient {
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-image: linear-gradient(135deg, #ffffff 0%, #a1a1aa 100%);
}
.icon-box {
background: rgba(255,255,255,0.05);
border: 1px solid rgba(255,255,255,0.1);
box-shadow: inset 0 1px 0 rgba(255,255,255,0.1);
transition: all 0.3s ease;
}
.card-panel:hover .icon-box {
background: rgba(59, 130, 246, 0.15);
border-color: rgba(59, 130, 246, 0.4);
color: #60a5fa; /* blue-400 */
box-shadow: 0 0 15px rgba(59, 130, 246, 0.3), inset 0 1px 0 rgba(255,255,255,0.2);
}
</style>
</head>
<body class="min-h-screen flex flex-col relative">
<!-- Header Navigation -->
<header class="border-b border-border sticky top-0 bg-background/80 backdrop-blur-xl z-50">
<div class="max-w-6xl mx-auto px-6 h-14 flex items-center justify-between">
<div class="flex items-center space-x-5">
<div class="flex items-center">
<div class="w-2 h-2 rounded-full bg-blue-500 shadow-[0_0_8px_rgba(59,130,246,0.8)] mr-2"></div>
<span class="font-semibold tracking-tight text-sm text-zinc-100">AXHUB Gateway</span>
</div>
<div class="h-4 w-px bg-white/10"></div>
<nav class="flex space-x-5 text-[13px] font-medium">
<a href="/admin/scaffold.html" class="text-zinc-400 hover:text-white transition-colors">Scaffold</a>
<a href="/catalog.html" class="text-zinc-400 hover:text-white transition-colors">Catalog</a>
<a href="/playground.html" class="text-zinc-400 hover:text-white transition-colors">Playground</a>
</nav>
</div>
<div class="flex items-center">
<span class="text-[10px] uppercase tracking-widest bg-blue-500/10 text-blue-400 px-2 py-1 rounded font-bold border border-blue-500/20">v0.0.1</span>
</div>
</div>
</header>
<main class="flex-grow max-w-6xl mx-auto px-6 py-20 w-full flex flex-col items-center justify-center relative z-10">
<div class="text-center mb-20 max-w-2xl">
<div class="inline-flex items-center justify-center px-3 py-1 mb-6 text-xs font-medium rounded-full bg-white/5 border border-white/10 shadow-sm backdrop-blur-md">
<span class="flex w-2 h-2 rounded-full bg-blue-400 shadow-[0_0_5px_rgba(96,165,250,0.8)] mr-2 animate-pulse"></span>
<span class="text-zinc-300">System Operational</span>
</div>
<h1 class="text-5xl font-bold tracking-tight mb-6 premium-text-gradient leading-tight">AI Plugin Gateway</h1>
<p class="text-[15px] text-zinc-400 leading-relaxed max-w-lg mx-auto">
The central hub for managing, discovering, and testing AI tool integrations. High-performance tooling for the modern enterprise.
</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 w-full max-w-5xl">
<!-- Scaffold Card -->
<a href="/admin/scaffold.html" class="card-panel p-8 group block">
<div class="w-12 h-12 icon-box rounded-xl flex items-center justify-center mb-6 text-zinc-300">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4"></path></svg>
</div>
<h2 class="text-lg font-semibold text-zinc-100 mb-3">Scaffold Tool</h2>
<p class="text-[13px] text-zinc-400 leading-relaxed">
Generate boilerplate code for new AI tool integrations automatically. Select a target module and build your plugin structure in seconds.
</p>
<div class="mt-6 flex items-center text-[12px] font-semibold text-zinc-300 group-hover:text-blue-400 group-hover:translate-x-1 transition-all">
Launch Scaffold <svg class="w-3 h-3 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 5l7 7m0 0l-7 7m7-7H3"></path></svg>
</div>
</a>
<!-- Catalog Card -->
<a href="/catalog.html" class="card-panel p-8 group block">
<div class="w-12 h-12 icon-box rounded-xl flex items-center justify-center mb-6 text-zinc-300">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M4 6h16M4 10h16M4 14h16M4 18h16"></path></svg>
</div>
<h2 class="text-lg font-semibold text-zinc-100 mb-3">Tool Catalog</h2>
<p class="text-[13px] text-zinc-400 leading-relaxed">
Browse all active tool integrations. View JSON schemas, parameters, and action prompts registered across the entire AXHUB ecosystem.
</p>
<div class="mt-6 flex items-center text-[12px] font-semibold text-zinc-300 group-hover:text-blue-400 group-hover:translate-x-1 transition-all">
View Catalog <svg class="w-3 h-3 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 5l7 7m0 0l-7 7m7-7H3"></path></svg>
</div>
</a>
<!-- Playground Card -->
<a href="/playground.html" class="card-panel p-8 group block">
<div class="w-12 h-12 icon-box rounded-xl flex items-center justify-center mb-6 text-zinc-300">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
</div>
<h2 class="text-lg font-semibold text-zinc-100 mb-3">Playground</h2>
<p class="text-[13px] text-zinc-400 leading-relaxed">
Test and execute tools directly from the browser. Verify RPC calls, test inputs, and inspect raw JSON responses before using them in code.
</p>
<div class="mt-6 flex items-center text-[12px] font-semibold text-zinc-300 group-hover:text-blue-400 group-hover:translate-x-1 transition-all">
Enter Playground <svg class="w-3 h-3 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 5l7 7m0 0l-7 7m7-7H3"></path></svg>
</div>
</a>
</div>
</main>
</body>
</html>

Some files were not shown because too many files have changed in this diff Show More