Merge branch 'jenkins_with_weblogic' of ssh://git@192.168.240.178:18081/eapim/eapim-admin.git into jenkins_with_weblogic

This commit is contained in:
daekuk
2025-12-11 17:31:53 +09:00
37 changed files with 3658 additions and 63 deletions
@@ -2,6 +2,7 @@ package com.eactive.eai.rms.data.entity.man.monitoringProperty.service;
import java.util.Optional;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -14,13 +15,14 @@ import com.querydsl.core.types.dsl.BooleanExpression;
@Service
@Transactional
@Slf4j
public class MonitoringPropertyService
extends AbstractDataService<MonitoringProperty, MonitoringPropertyId, MonitoringPropertyRepository> {
public String findPrpty2ValById(String prptyGroupName, String key) {
MonitoringPropertyId id = new MonitoringPropertyId();
id.setPrptyGroupName(prptyGroupName);
id.setPrptyGroupName(key);
id.setPrptyName(key);
Optional<MonitoringProperty> property = repository.findById(id);
return property.map(MonitoringProperty::getPrpty2Val).orElse(null);
@@ -41,12 +43,16 @@ public class MonitoringPropertyService
id.setPrptyName(prptyName);
Optional<MonitoringProperty> property = repository.findById(id);
if (property.isPresent() && !property.get().getPrpty2Val().isEmpty()) {
return property.get().getPrpty2Val();
} else {
return defaultValue;
if (property.isPresent()) {
String value = property.get().getPrpty2Val();
if (value != null && !value.isEmpty()) {
log.debug("getPropertyValue - {} : {}", prptyName, value);
return value;
}
}
log.debug("getPropertyValue - {} : {} (defaultValue)", prptyName, defaultValue);
return defaultValue;
}
public Iterable<MonitoringProperty> findAllByIdPrptyName(String prptyName) {
@@ -0,0 +1,113 @@
package com.eactive.eai.rms.data.entity.onl.apim.obp;
import com.eactive.eai.rms.common.base.SqlMapClientTemplateDao;
import org.springframework.stereotype.Repository;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
/**
* ObpGwMetric DAO - iBATIS를 사용한 로그 테이블 집계
*/
@Repository
public class ObpGwMetricDAO extends SqlMapClientTemplateDao {
/**
* EAIBZWKDSTCD 코드 목록 조회
*/
@SuppressWarnings("unchecked")
public List<String> selectAllBzwkCodes(String schemaId) {
HashMap<String, Object> paramMap = new HashMap<>();
paramMap.put("schemaId", schemaId);
return this.template.queryForList("ObpGwMetric.selectAllBzwkCodes", paramMap);
}
/**
* EAIBZWKDSTCD별 시간대 로그 집계
*/
@SuppressWarnings("unchecked")
public List<ObpGwMetricVO> selectHourlyMetricsByBzwkCode(HashMap<String, Object> paramMap) {
return this.template.queryForList("ObpGwMetric.selectHourlyMetricsByBzwkCode", paramMap);
}
/**
* API 이름 조회 (TSEAIHE01)
*/
@SuppressWarnings("unchecked")
public Map<String, String> selectApiNames(String schemaId, Set<String> apiIds) {
if (apiIds == null || apiIds.isEmpty()) {
return new HashMap<>();
}
HashMap<String, Object> paramMap = new HashMap<>();
paramMap.put("schemaId", schemaId);
paramMap.put("apiIds", new ArrayList<>(apiIds)); // Set → List 변환 (iBATIS iterate 호환)
List<Map<String, String>> resultList = this.template.queryForList("ObpGwMetric.selectApiNames", paramMap);
Map<String, String> result = new HashMap<>();
for (Map<String, String> m : resultList) {
String apiId = m.get("API_ID");
String apiName = m.get("API_NAME");
if (apiId != null && apiName != null) {
result.put(apiId, apiName);
}
}
return result;
}
/**
* URI 조회 (TSEAIHS04)
*/
@SuppressWarnings("unchecked")
public Map<String, String> selectUris(String schemaId, Set<String> apiIds) {
if (apiIds == null || apiIds.isEmpty()) {
return new HashMap<>();
}
HashMap<String, Object> paramMap = new HashMap<>();
paramMap.put("schemaId", schemaId);
paramMap.put("apiIds", new ArrayList<>(apiIds)); // Set → List 변환 (iBATIS iterate 호환)
List<Map<String, String>> resultList = this.template.queryForList("ObpGwMetric.selectUris", paramMap);
Map<String, String> result = new HashMap<>();
for (Map<String, String> m : resultList) {
String apiId = m.get("API_ID");
String uri = m.get("URI");
if (apiId != null && uri != null) {
result.put(apiId, uri);
}
}
return result;
}
/**
* AppId 조회 (ptl_app_request) - org_id로 조회
*/
@SuppressWarnings("unchecked")
public Map<String, String> selectAppIdsByOrgIds(Set<String> orgIds) {
if (orgIds == null || orgIds.isEmpty()) {
return new HashMap<>();
}
HashMap<String, Object> paramMap = new HashMap<>();
paramMap.put("orgIds", new ArrayList<>(orgIds)); // Set → List 변환 (iBATIS iterate 호환)
List<Map<String, String>> resultList = this.template.queryForList("ObpGwMetric.selectAppIdsByOrgIds", paramMap);
Map<String, String> result = new HashMap<>();
for (Map<String, String> m : resultList) {
String orgId = m.get("ORG_ID");
String appId = m.get("APP_ID");
if (orgId != null && appId != null) {
result.put(orgId, appId);
}
}
return result;
}
}
@@ -0,0 +1,39 @@
package com.eactive.eai.rms.data.entity.onl.apim.obp;
import com.eactive.apim.portal.obp.entity.ObpGwMetric;
import com.eactive.eai.data.jpa.BaseRepository;
import com.eactive.eai.rms.data.EMSDataSource;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
/**
* ObpGwMetric Repository (eapim-admin 위치)
* API Gateway 시간별 거래량 지표 조회/저장을 위한 Spring Data JPA Repository
*/
@EMSDataSource
public interface ObpGwMetricRepository extends BaseRepository<ObpGwMetric, String> {
/**
* UPSERT 판단을 위한 기존 데이터 조회
* 동일한 timeslice + apiId + clientId + hostname + orgId 조합이 존재하는지 확인
*/
Optional<ObpGwMetric> findByTimesliceAndApiIdAndClientIdAndHostnameAndOrgId(
LocalDateTime timeslice,
String apiId,
String clientId,
String hostname,
String orgId
);
/**
* 특정 시간대의 모든 GwMetric 데이터 조회
*/
List<ObpGwMetric> findByTimeslice(LocalDateTime timeslice);
/**
* 특정 시간 범위의 GwMetric 데이터 조회
*/
List<ObpGwMetric> findByTimesliceBetween(LocalDateTime startTime, LocalDateTime endTime);
}
@@ -0,0 +1,59 @@
package com.eactive.eai.rms.data.entity.onl.apim.obp;
import com.eactive.apim.portal.obp.entity.ObpGwMetric;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
/**
* ObpGwMetric Service - 엔티티 CRUD 서비스
*/
@Service
@Transactional(transactionManager = "transactionManagerForEMS")
public class ObpGwMetricService {
private final ObpGwMetricRepository repository;
public ObpGwMetricService(ObpGwMetricRepository repository) {
this.repository = repository;
}
/**
* UPSERT 판단을 위한 기존 데이터 조회
*/
public Optional<ObpGwMetric> findByUniqueKey(LocalDateTime timeslice, String apiId, String clientId, String hostname, String orgId) {
return repository.findByTimesliceAndApiIdAndClientIdAndHostnameAndOrgId(
timeslice, apiId, clientId, hostname, orgId);
}
/**
* 특정 시간대의 모든 GwMetric 데이터 조회
*/
public List<ObpGwMetric> findByTimeslice(LocalDateTime timeslice) {
return repository.findByTimeslice(timeslice);
}
/**
* 특정 시간 범위의 GwMetric 데이터 조회
*/
public List<ObpGwMetric> findByTimesliceBetween(LocalDateTime startTime, LocalDateTime endTime) {
return repository.findByTimesliceBetween(startTime, endTime);
}
/**
* 엔티티 저장 (INSERT 또는 UPDATE)
*/
public ObpGwMetric save(ObpGwMetric entity) {
return repository.save(entity);
}
/**
* 엔티티 목록 저장
*/
public List<ObpGwMetric> saveAll(List<ObpGwMetric> entities) {
return repository.saveAll(entities);
}
}
@@ -0,0 +1,68 @@
package com.eactive.eai.rms.data.entity.onl.apim.obp;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
/**
* ObpGwMetric VO - 로그 집계 결과 매핑용
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ObpGwMetricVO {
private static final DateTimeFormatter TIMESLICE_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMddHH0000000");
/** API ID (EAISVCNAME) */
private String apiId;
/** 클라이언트 ID (CLIENTID) */
private String clientId;
/** 호스트 이름 (EAISEVRINSTNCNAME) */
private String hostname;
/** 기관 ID (ORGID) */
private String orgId;
/** 타임슬라이스 문자열 (yyyyMMddHH0000000) */
private String timeslice;
/** 시도 횟수 (LOGPRCSSSERNO='100' 건수) */
private Long attemptedCount;
/** 완료 횟수 (LOGPRCSSSERNO='400' AND 성공 조건 건수) */
private Long completedCount;
/**
* timeslice 문자열을 LocalDateTime으로 변환
*/
public LocalDateTime getTimesliceAsLocalDateTime() {
if (timeslice == null || timeslice.isEmpty()) {
return null;
}
// "yyyyMMddHH0000000" 형식에서 앞 10자리(yyyyMMddHH)만 사용
String dateTimeStr = timeslice.substring(0, 10) + "0000";
return LocalDateTime.parse(dateTimeStr, DateTimeFormatter.ofPattern("yyyyMMddHHmmss"));
}
/**
* 집계 키 생성 (apiId + clientId + hostname + orgId)
*/
public String getAggregationKey() {
return String.join("|",
nullSafe(apiId),
nullSafe(clientId),
nullSafe(hostname),
nullSafe(orgId)
);
}
private String nullSafe(String value) {
return value != null ? value : "";
}
}
@@ -0,0 +1,25 @@
package com.eactive.eai.rms.data.entity.onl.inflow;
import java.util.List;
import java.util.Optional;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import com.eactive.eai.data.entity.onl.inflow.InflowControlGroupMapping;
import com.eactive.eai.data.jpa.BaseRepository;
public interface InflowControlGroupMappingRepository extends BaseRepository<InflowControlGroupMapping, String> {
List<InflowControlGroupMapping> findByGroupId(String groupId);
@Modifying
@Query("DELETE FROM InflowControlGroupMapping m WHERE m.groupId = :groupId")
void deleteByGroupId(@Param("groupId") String groupId);
/**
* 인터페이스 ID로 매핑 조회 (중복 체크용)
*/
Optional<InflowControlGroupMapping> findByInterfaceId(String interfaceId);
}
@@ -0,0 +1,8 @@
package com.eactive.eai.rms.data.entity.onl.inflow;
import com.eactive.eai.data.entity.onl.inflow.InflowControlGroup;
import com.eactive.eai.data.jpa.BaseRepository;
public interface InflowControlGroupRepository extends BaseRepository<InflowControlGroup, String> {
}
@@ -28,7 +28,7 @@ public abstract class CredentialUIMapper implements GenericMapper<CredentialUI,
@InheritInverseConfiguration
@BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
@Mapping(target = "apiList", expression = "java(apiSpecUIMapper.mapList(credentialUI.getApiList()))")
@Mapping(target = "apiList", source = "apiList")
public abstract void updateToEntity(CredentialUI credentialUI, @MappingTarget Credential credential);
@Mapping(source = "clientid", target = "clientId")
@@ -0,0 +1,97 @@
package com.eactive.eai.rms.onl.apim.obp;
import com.eactive.eai.rms.common.interceptor.InterceptorSkipController;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
/**
* ObpGwMetric REST Controller
* API Gateway 시간별 거래량 지표 조회 API
*
* <p>세션 체크를 우회하기 위해 InterceptorSkipController 인터페이스 구현</p>
*/
@Controller
public class ObpGwMetricController implements InterceptorSkipController {
private static final Logger log = LoggerFactory.getLogger(ObpGwMetricController.class);
private static final DateTimeFormatter PATH_FORMATTER =
DateTimeFormatter.ofPattern("yyyy-MM-dd_HH:mm:ss");
private final ObpGwMetricManService obpGwMetricManService;
public ObpGwMetricController(ObpGwMetricManService obpGwMetricManService) {
this.obpGwMetricManService = obpGwMetricManService;
}
/**
* 특정 시간대의 GwMetric 데이터 조회
*
* @param datetime 조회 시간대 (형식: yyyy-MM-dd_HH:mm:ss)
* @return JSON Array
*/
@RequestMapping(
value = "/kjb/gw-metrics/{datetime}",
produces = "application/json;charset=utf-8"
)
@ResponseBody
public String getGwMetrics(@PathVariable("datetime") String datetime) {
log.debug("GwMetric 조회 요청: datetime={}", datetime);
long startTime = System.currentTimeMillis();
try {
// Path 파라미터 파싱
LocalDateTime targetTime = LocalDateTime.parse(datetime, PATH_FORMATTER);
LocalDateTime timeslice = targetTime.withMinute(0).withSecond(0).withNano(0);
// 조회
List<ObpGwMetricDTO> metrics = obpGwMetricManService.findByTimeslice(timeslice);
// JSON 변환 (GSON)
Gson gson = new GsonBuilder()
.serializeNulls()
.create();
String json = gson.toJson(metrics);
// 응답 크기 계산 (UTF-8 기준)
int jsonBytes = json.getBytes(StandardCharsets.UTF_8).length;
String jsonSize = formatByteSize(jsonBytes);
long elapsed = System.currentTimeMillis() - startTime;
log.info("GwMetric 조회 완료: datetime={}, count={}, size={}, elapsed={}ms",
datetime, metrics.size(), jsonSize, elapsed);
return json;
} catch (Exception e) {
log.error("GwMetric 조회 실패: datetime={}", datetime, e);
throw e;
}
}
/**
* 바이트 크기를 읽기 쉬운 형식으로 변환
* 예: 1024 -> "1.0 KB", 1048576 -> "1.0 MB"
*/
private String formatByteSize(long bytes) {
if (bytes < 1024) {
return bytes + " B";
} else if (bytes < 1024 * 1024) {
return String.format("%.1f KB", bytes / 1024.0);
} else {
return String.format("%.1f MB", bytes / (1024.0 * 1024.0));
}
}
}
@@ -0,0 +1,98 @@
package com.eactive.eai.rms.onl.apim.obp;
import com.eactive.apim.portal.obp.entity.ObpGwMetric;
import com.google.gson.annotations.SerializedName;
import lombok.Data;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
/**
* ObpGwMetric JSON 응답 DTO
* Entity 주석에 명시된 JSON Key 사용
*/
@Data
public class ObpGwMetricDTO {
private static final DateTimeFormatter ISO8601_FORMATTER =
DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'+0000'");
@SerializedName("appKey")
private String clientId;
@SerializedName("clusterHostname")
private String hostname;
@SerializedName("timeslice")
private String timeslice;
@SerializedName("apiName")
private String apiName;
@SerializedName("apiRoutingUri")
private String uri;
@SerializedName("attemptedCount")
private Long attemptedCount;
@SerializedName("completedCount")
private Long completedCount;
// EAPIM ID
@SerializedName("apiIdByEapim")
private String apiId;
@SerializedName("appIdByEapim")
private String appId;
@SerializedName("orgIdByEapim")
private String orgId;
// AS-IS 호환용 필드
@SerializedName("appId")
private String appIdByCaPortal;
@SerializedName("portalOrgId")
private String orgIdByCaPortal;
@SerializedName("portalApiId")
private String apiIdByCaPortal;
@SerializedName("partnerCode")
private String partnerCode;
@SerializedName("apiId")
private String apiIdByCa;
/**
* Entity → DTO 변환
*/
public static ObpGwMetricDTO from(ObpGwMetric entity) {
ObpGwMetricDTO dto = new ObpGwMetricDTO();
dto.setClientId(entity.getClientId());
dto.setHostname(entity.getHostname());
dto.setTimeslice(formatTimeslice(entity.getTimeslice()));
dto.setApiName(entity.getApiName());
dto.setUri(entity.getUri());
dto.setAttemptedCount(entity.getAttemptedCount());
dto.setCompletedCount(entity.getCompletedCount());
dto.setApiId(entity.getApiId());
dto.setAppId(entity.getAppId());
dto.setOrgId(entity.getOrgId());
dto.setAppIdByCaPortal(entity.getAppIdByCaPortal());
dto.setOrgIdByCaPortal(entity.getOrgIdByCaPortal());
dto.setApiIdByCaPortal(entity.getApiIdByCaPortal());
dto.setPartnerCode(entity.getPartnerCode());
dto.setApiIdByCa(entity.getApiIdByCa());
return dto;
}
/**
* timeslice를 ISO8601 형식 + UTC 시간대로 변환
* 예: 2025-12-02T01:00:00+0000
*/
private static String formatTimeslice(LocalDateTime timeslice) {
if (timeslice == null) return null;
return timeslice.format(ISO8601_FORMATTER);
}
}
@@ -0,0 +1,276 @@
package com.eactive.eai.rms.onl.apim.obp;
import com.eactive.apim.portal.obp.entity.ObpGwMetric;
import com.eactive.eai.rms.common.base.BaseService;
import com.eactive.eai.rms.common.datasource.DataSourceContextHolder;
import com.eactive.eai.rms.common.datasource.DataSourceType;
import com.eactive.eai.rms.common.datasource.DataSourceTypeManager;
import com.eactive.eai.rms.common.util.CommonUtil;
import com.eactive.eai.rms.data.entity.onl.apim.obp.ObpGwMetricDAO;
import com.eactive.eai.rms.data.entity.onl.apim.obp.ObpGwMetricService;
import com.eactive.eai.rms.data.entity.onl.apim.obp.ObpGwMetricVO;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.stream.Collectors;
/**
* ObpGwMetric Man Service - 비즈니스 로직
* API Gateway 로그 데이터를 집계하여 ObpGwMetric 엔티티로 저장
*/
@Service
@Transactional(transactionManager = "transactionManagerForEMS")
public class ObpGwMetricManService extends BaseService {
private static final Logger log = LoggerFactory.getLogger(ObpGwMetricManService.class);
/**
* 기본 집계 범위 시간 (단위: 시간)
* - 1: 직전 1시간만 집계
* - 5: 직전 5시간까지 각각 1시간 단위로 집계
*
* <p>Job 파라미터로 오버라이드 가능 (aggregation.hour.range)</p>
*/
public static final int DEFAULT_AGGREGATION_HOUR_RANGE = 1;
private static final DateTimeFormatter TIMESLICE_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMddHH0000000");
private static final DateTimeFormatter TIMESTAMP_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS");
private static final DateTimeFormatter YYYYMMDD_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd");
private final ObpGwMetricDAO obpGwMetricDAO;
private final ObpGwMetricService obpGwMetricService;
public ObpGwMetricManService(ObpGwMetricDAO obpGwMetricDAO, ObpGwMetricService obpGwMetricService) {
this.obpGwMetricDAO = obpGwMetricDAO;
this.obpGwMetricService = obpGwMetricService;
}
/**
* 다중 시간대 집계 실행 (Job에서 호출) - 기본 집계 범위 사용
*/
public void executeHourlyAggregation() {
executeHourlyAggregation(DEFAULT_AGGREGATION_HOUR_RANGE);
}
/**
* 다중 시간대 집계 실행 (Job에서 호출)
*
* @param aggregationHourRange 집계 범위 시간 (단위: 시간)
* 예: 1=직전 1시간, 3=직전 3시간 각각 집계
*/
public void executeHourlyAggregation(int aggregationHourRange) {
long totalStartTime = System.currentTimeMillis();
LocalDateTime now = LocalDateTime.now();
LocalDateTime baseHour = now.truncatedTo(ChronoUnit.HOURS); // 현재 시간의 정각
log.info("========== ObpGwMetric 집계 시작 ==========");
log.info("실행 시간: {}, aggregationHourRange: {}", now, aggregationHourRange);
for (int i = 1; i <= aggregationHourRange; i++) {
LocalDateTime targetHour = baseHour.minusHours(i);
log.info("집계 대상 시간대: {}", targetHour);
createHourlyData(targetHour);
}
long totalElapsed = System.currentTimeMillis() - totalStartTime;
log.info("========== ObpGwMetric 집계 완료 ==========");
log.info("전체 집계 소요시간: {}ms ({}초)", totalElapsed, String.format("%.2f", totalElapsed / 1000.0));
}
/**
* 1시간 단위 ObpGwMetric 데이터 생성 (UPSERT)
*/
public void createHourlyData(LocalDateTime targetHour) {
long jobStartTime = System.currentTimeMillis();
log.info("ObpGwMetric 집계 시작: targetHour={}", targetHour);
try {
String timeslice = targetHour.format(TIMESLICE_FORMATTER);
String yyyymmdd = targetHour.format(YYYYMMDD_FORMATTER);
String tableName = CommonUtil.getLogTable(yyyymmdd, true);
String startTime = targetHour.format(TIMESTAMP_FORMATTER);
String endTime = targetHour.plusHours(1).minusNanos(1).format(TIMESTAMP_FORMATTER);
// ═══════════════════════════════════════════════════════════════════
// 1단계: GW DB에서 EAIBZWKDSTCD 코드 목록 조회
// ═══════════════════════════════════════════════════════════════════
long step1Start = System.currentTimeMillis();
DataSourceType apigwDataSourceType = DataSourceTypeManager.getDataSourceType(DataSourceTypeManager.APIGW);
DataSourceContextHolder.setDataSourceType(apigwDataSourceType);
List<String> bzwkCodes = obpGwMetricDAO.selectAllBzwkCodes(apigwDataSourceType.getSchema());
log.info("[1단계] EAIBZWKDSTCD 코드 조회 완료: count={}, elapsed={}ms",
bzwkCodes.size(), System.currentTimeMillis() - step1Start);
// ═══════════════════════════════════════════════════════════════════
// 2단계: 코드별 개별 쿼리 실행 후 결과 병합
// ═══════════════════════════════════════════════════════════════════
long step2Start = System.currentTimeMillis();
Map<String, ObpGwMetricVO> aggregatedMap = new HashMap<>();
int totalLogCount = 0;
for (String bzwkCode : bzwkCodes) {
long codeStart = System.currentTimeMillis();
HashMap<String, Object> paramMap = new HashMap<>();
paramMap.put("schemaId", apigwDataSourceType.getSchema());
paramMap.put("tableName", tableName);
paramMap.put("bzwkCode", bzwkCode);
paramMap.put("startTime", startTime);
paramMap.put("endTime", endTime);
paramMap.put("timeslice", timeslice);
List<ObpGwMetricVO> partialResult = obpGwMetricDAO.selectHourlyMetricsByBzwkCode(paramMap);
for (ObpGwMetricVO vo : partialResult) {
String key = vo.getAggregationKey();
aggregatedMap.merge(key, vo, this::mergeMetricVO);
}
if (!partialResult.isEmpty()) {
log.debug("[2단계] bzwkCode={}, 집계건수={}, elapsed={}ms",
bzwkCode, partialResult.size(), System.currentTimeMillis() - codeStart);
}
totalLogCount += partialResult.size();
}
List<ObpGwMetricVO> aggregatedList = new ArrayList<>(aggregatedMap.values());
log.info("[2단계] 로그 집계 완료: 원본건수={}, 병합후건수={}, elapsed={}ms",
totalLogCount, aggregatedList.size(), System.currentTimeMillis() - step2Start);
if (aggregatedList.isEmpty()) {
log.warn("집계 대상 데이터 없음: targetHour={}", targetHour);
return;
}
// ═══════════════════════════════════════════════════════════════════
// 3단계: 부가 정보 조회 (apiName, uri) - GW DB
// ═══════════════════════════════════════════════════════════════════
long step3Start = System.currentTimeMillis();
Set<String> apiIds = aggregatedList.stream()
.map(ObpGwMetricVO::getApiId)
.filter(Objects::nonNull)
.collect(Collectors.toSet());
Map<String, String> apiNameMap = obpGwMetricDAO.selectApiNames(apigwDataSourceType.getSchema(), apiIds);
Map<String, String> uriMap = obpGwMetricDAO.selectUris(apigwDataSourceType.getSchema(), apiIds);
log.info("[3단계] 부가정보 조회 완료: apiCount={}, elapsed={}ms",
apiIds.size(), System.currentTimeMillis() - step3Start);
// ═══════════════════════════════════════════════════════════════════
// 4단계: 지표 DB로 전환 후 appId 조회 및 UPSERT
// ═══════════════════════════════════════════════════════════════════
long step4Start = System.currentTimeMillis();
DataSourceContextHolder.setDataSourceType(
DataSourceTypeManager.getDataSourceType(DataSourceTypeManager.MONITORING));
Set<String> orgIds = aggregatedList.stream()
.map(ObpGwMetricVO::getOrgId)
.filter(Objects::nonNull)
.collect(Collectors.toSet());
Map<String, String> appIdMap = obpGwMetricDAO.selectAppIdsByOrgIds(orgIds);
int insertCount = 0;
int updateCount = 0;
for (ObpGwMetricVO vo : aggregatedList) {
LocalDateTime timesliceDateTime = vo.getTimesliceAsLocalDateTime();
Optional<ObpGwMetric> existing = obpGwMetricService.findByUniqueKey(
timesliceDateTime,
vo.getApiId(),
vo.getClientId(),
vo.getHostname(),
vo.getOrgId()
);
if (existing.isPresent()) {
// UPDATE: 기존 데이터 갱신
ObpGwMetric entity = existing.get();
entity.setAttemptedCount(vo.getAttemptedCount());
entity.setCompletedCount(vo.getCompletedCount());
entity.setApiName(apiNameMap.get(vo.getApiId()));
entity.setUri(uriMap.get(vo.getApiId()));
entity.setAppId(appIdMap.get(vo.getOrgId()));
entity.setUpdateCount(entity.getUpdateCount() + 1); // 보정 횟수 증가
entity.setUpdatedAt(LocalDateTime.now());
entity.setUpdatedBy("GwMetricHourJob");
obpGwMetricService.save(entity);
updateCount++;
} else {
// INSERT: 신규 데이터 생성
ObpGwMetric entity = convertToEntity(vo, apiNameMap, uriMap, appIdMap);
obpGwMetricService.save(entity);
insertCount++;
}
}
log.info("[4단계] UPSERT 완료: INSERT={}, UPDATE={}, elapsed={}ms",
insertCount, updateCount, System.currentTimeMillis() - step4Start);
long totalElapsed = System.currentTimeMillis() - jobStartTime;
log.info("ObpGwMetric 집계 완료: targetHour={}, 총 소요시간={}ms ({}초)",
targetHour, totalElapsed, totalElapsed / 1000.0);
} catch (Exception e) {
log.error("ObpGwMetric 집계 실패: targetHour={}", targetHour, e);
throw e;
}
}
/**
* ObpGwMetricVO 병합 (count 누적)
*/
private ObpGwMetricVO mergeMetricVO(ObpGwMetricVO existing, ObpGwMetricVO incoming) {
existing.setAttemptedCount(
(existing.getAttemptedCount() != null ? existing.getAttemptedCount() : 0L) +
(incoming.getAttemptedCount() != null ? incoming.getAttemptedCount() : 0L)
);
existing.setCompletedCount(
(existing.getCompletedCount() != null ? existing.getCompletedCount() : 0L) +
(incoming.getCompletedCount() != null ? incoming.getCompletedCount() : 0L)
);
return existing;
}
/**
* VO → Entity 변환
*/
private ObpGwMetric convertToEntity(ObpGwMetricVO vo, Map<String, String> apiNameMap,
Map<String, String> uriMap, Map<String, String> appIdMap) {
ObpGwMetric entity = new ObpGwMetric();
entity.setClientId(vo.getClientId());
entity.setHostname(vo.getHostname());
entity.setTimeslice(vo.getTimesliceAsLocalDateTime());
entity.setApiId(vo.getApiId());
entity.setOrgId(vo.getOrgId());
entity.setAttemptedCount(vo.getAttemptedCount());
entity.setCompletedCount(vo.getCompletedCount());
entity.setApiName(apiNameMap.get(vo.getApiId()));
entity.setUri(uriMap.get(vo.getApiId()));
entity.setAppId(appIdMap.get(vo.getOrgId()));
entity.setUpdateCount(0);
entity.setCreatedAt(LocalDateTime.now());
entity.setCreatedBy("GwMetricHourJob");
return entity;
}
/**
* 특정 시간대의 GwMetric 데이터 조회 (REST API용)
*/
public List<ObpGwMetricDTO> findByTimeslice(LocalDateTime timeslice) {
DataSourceContextHolder.setDataSourceType(
DataSourceTypeManager.getDataSourceType(DataSourceTypeManager.MONITORING));
List<ObpGwMetric> entities = obpGwMetricService.findByTimeslice(timeslice);
return entities.stream()
.map(ObpGwMetricDTO::from)
.collect(Collectors.toList());
}
}
@@ -16,7 +16,7 @@ import com.eactive.eai.rms.onl.audit.adapter.ui.AuditUI;
@Mapper(config = BaseMapperConfig.class, uses = {})
public interface AuditUIMapper {
@Mapping(target = "revTstmp", expression = "java(mapLongToString(entity.getRevTstmp()))")
@Mapping(target = "revTstmp", source = "revTstmp")
@Mapping(target = "revNo", source = "rev")
@Mapping(target = "revUserId", source = "userId")
void map(CustomRevisionEntity entity, @MappingTarget AuditUI auditUI);
@@ -0,0 +1,147 @@
package com.eactive.eai.rms.onl.common.service;
import com.eactive.eai.rms.common.context.MonitoringContext;
import com.eactive.eai.rms.common.util.CommonUtil;
import com.eactive.eai.rms.onl.apim.obp.ObpGwMetricManService;
import com.eactive.eai.rms.onl.common.util.DateUtil;
import org.quartz.Job;
import org.quartz.JobDataMap;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.quartz.SchedulerException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationContext;
/**
* ObpGwMetric Hour Job - Quartz Job
* API Gateway 로그 데이터를 1시간 단위로 집계하여 ObpGwMetric 테이블에 저장
*
* <p>Cron Schedule 권장:</p>
* <ul>
* <li>기본: 0 5 * * * ? (매시 05분 1회 실행)</li>
* <li>보정 (15분): 0 5,20,35,50 * * * ? (15분 간격 4회 실행)</li>
* <li>보정 (10분): 0 5,15,25,35,45,55 * * * ? (10분 간격 6회 실행)</li>
* </ul>
*
* <p>Job 파라미터 (JobDataMap):</p>
* <ul>
* <li><b>aggregation.hour.range</b>: 집계 범위 시간 (단위: 시간, 기본값: 1)
* <ul>
* <li>1: 직전 1시간만 집계</li>
* <li>3: 직전 3시간 각각 집계</li>
* <li>5: 직전 5시간 각각 집계</li>
* </ul>
* </li>
* </ul>
*
* <p>예시 (Quartz 스케줄 등록 시):</p>
* <pre>
* JobDataMap jobDataMap = new JobDataMap();
* jobDataMap.put("aggregation.hour.range", "3");
* </pre>
*/
public class ObpGwMetricHourJob implements Job {
private static final Logger log = LoggerFactory.getLogger(ObpGwMetricHourJob.class);
/** Job 파라미터 키: 집계 범위 시간 */
public static final String PARAM_AGGREGATION_HOUR_RANGE = "aggregation.hour.range";
/** 기본 집계 범위 시간 (시간 단위) */
public static final int DEFAULT_AGGREGATION_HOUR_RANGE = 1;
private transient MonitoringContext monitoringContext;
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
log.info("*** START ObpGwMetricHourJob run({})", CommonUtil.getToday("yyyy-MM-dd HH:mm"));
// Job 파라미터 로깅
JobDataMap jobDataMap = context.getJobDetail().getJobDataMap();
logJobParameters(jobDataMap);
ApplicationContext appContext;
try {
appContext = (ApplicationContext) context.getScheduler().getContext().get("applicationContext");
} catch (SchedulerException e) {
log.error("applicationContext get module error", e);
return;
}
monitoringContext = (MonitoringContext) appContext.getBean("monitoringContext");
ObpGwMetricManService obpGwMetricManService = appContext.getBean(ObpGwMetricManService.class);
// 집계 범위 시간 파라미터 파싱
int aggregationHourRange = parseAggregationHourRange(jobDataMap);
try {
obpGwMetricManService.executeHourlyAggregation(aggregationHourRange);
} catch (Exception e) {
log.error("ObpGwMetricHourJob execution failed", e);
throw new JobExecutionException(e);
}
log.info("*** END ObpGwMetricHourJob run({})", DateUtil.getDateTime("yyyy-MM-dd HH:mm"));
}
/**
* Job 파라미터 로깅
*/
private void logJobParameters(JobDataMap jobDataMap) {
if (jobDataMap == null || jobDataMap.isEmpty()) {
log.debug("Job 파라미터 없음 (기본값 사용)");
return;
}
StringBuilder sb = new StringBuilder("Job 파라미터: ");
for (String key : jobDataMap.getKeys()) {
sb.append(key).append("=").append(jobDataMap.getString(key)).append(", ");
}
log.info(sb.toString());
}
/**
* 집계 범위 시간 파라미터 파싱
*
* @param jobDataMap Job 파라미터 맵
* @return 집계 범위 시간 (파싱 실패 시 기본값 반환)
*/
private int parseAggregationHourRange(JobDataMap jobDataMap) {
if (jobDataMap == null) {
return DEFAULT_AGGREGATION_HOUR_RANGE;
}
try {
String value = jobDataMap.getString(PARAM_AGGREGATION_HOUR_RANGE);
if (value != null && !value.trim().isEmpty()) {
int parsedValue = Integer.parseInt(value.trim());
if (parsedValue < 1) {
log.warn("aggregation.hour.range 값이 1 미만입니다. 기본값({}) 사용: input={}",
DEFAULT_AGGREGATION_HOUR_RANGE, parsedValue);
return DEFAULT_AGGREGATION_HOUR_RANGE;
}
// 개발 모드(D)가 아닌 경우에만 24시간 제한 적용
if (parsedValue > 24 && !isDevMode()) {
log.warn("aggregation.hour.range 값이 24 초과입니다. 24로 제한: input={}", parsedValue);
return 24;
}
return parsedValue;
}
} catch (NumberFormatException e) {
log.warn("aggregation.hour.range 파싱 실패, 기본값({}) 사용: {}",
DEFAULT_AGGREGATION_HOUR_RANGE, e.getMessage());
}
return DEFAULT_AGGREGATION_HOUR_RANGE;
}
/**
* 개발 모드 여부 확인
* @return eai.systemmode=D 이면 true
*/
private boolean isDevMode() {
String systemMode = System.getProperty("eai.systemmode", "");
return "D".equalsIgnoreCase(systemMode);
}
}
@@ -9,6 +9,7 @@ import java.util.Iterator;
import java.util.List;
import java.util.Map;
import com.eactive.eai.rms.onl.common.util.FileSystemUtil;
import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.apache.poi.hssf.usermodel.HSSFCell;
@@ -75,7 +76,7 @@ public class TransactionReportHourJob implements Job {
}
String trTime = monitoringContext.getStringProperty(MonitoringContext.RMS_TRANSACTION_LOG_TIME, "");
if( trTime.indexOf("EVERY_HOUR") > -1 ) {
if (trTime.contains("EVERY_HOUR")) {
// go on
} else if (!StringUtils.contains(trTime, hour)) {
return;
@@ -93,9 +94,11 @@ public class TransactionReportHourJob implements Job {
+ CommonUtil.getToday("yyyy-MM-dd HH:mm") + ")");
}
String pathDir = monitoringContext.getStringProperty( MonitoringContext.RMS_TRANSACTION_LOG_PATH,"/fslog/eai/")
+ monitoringContext.getStringProperty( Keys.SERVER_KEY)
+ "/trlog";
String pathDir = FileSystemUtil.pathNormalize(
monitoringContext.getStringProperty( MonitoringContext.RMS_TRANSACTION_LOG_PATH,"/fslog/eai/")
, monitoringContext.getStringProperty( Keys.SERVER_KEY)
, "trlog"
);
///////////////////////////////////////////////
// 로컬테스트용
@@ -17,13 +17,14 @@ import org.springframework.transaction.support.TransactionTemplate;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import java.io.IOException;
import java.time.LocalDateTime;
import java.util.List;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import javax.annotation.PostConstruct;
@Slf4j
@Service
@@ -46,6 +47,7 @@ public class UmsDispatchService {
private KjbUmsService kjbUmsService;
private KjbEmailSendModule kjbEmailSendModule;
private final KjbPropertyInjector kjbPropertyInjector;
private KjbProperty kjbProperty;
@Autowired
@@ -55,11 +57,7 @@ public class UmsDispatchService {
MessageRequestRepository messageRequestRepository
) {
this.kjbPropertyInjector = new KjbPropertyInjector(monitoringPropertyService, PROP_GORUP_ID);
this.messageRequestRepository = messageRequestRepository;
// this.kjbEmailService = kjbEmailService;
// this.kjbSafedbWrapper = kjbSafedbWrapper;
// this.monitoringPropertyService = monitoringPropertyService;
this.executorService = new ThreadPoolExecutor(
CORE_POOL_SIZE,
@@ -72,32 +70,51 @@ public class UmsDispatchService {
this.transactionTemplate = new TransactionTemplate(transactionManager);
log.debug("UmsDispatchService initialized with thread pool - core: {}, max: {}, queue: {}",
CORE_POOL_SIZE, MAX_POOL_SIZE, QUEUE_CAPACITY);
}
KjbProperty prop = new KjbProperty();
// prop.setEaiBatchUrl(monitoringPropertyService.getPrpty2ValById(PROP_GORUP_ID, "kjb.ums.eai_batch.url"));
// prop.setUmsEaiBatchSendDir(monitoringPropertyService.getPrpty2ValById(PROP_GORUP_ID, "kjb.ums.eai_batch.send_dir"));
// prop.setUmsEaiBatchBackupDir(monitoringPropertyService.getPrpty2ValById(PROP_GORUP_ID, "kjb.ums.eai_batch.backup_dir"));
// prop.setUmsEaiBatchInterfaceId(monitoringPropertyService.getPrpty2ValById(PROP_GORUP_ID, "kjb.ums.eai_batch.interface_id"));
// prop.setUmsHostUrl(monitoringPropertyService.getPrpty2ValById(PROP_GORUP_ID, "kjb.ums.host_url"));
// prop.setUmsApiKey(monitoringPropertyService.getPrpty2ValById(PROP_GORUP_ID, "kjb.ums.api_key"));
prop = kjbPropertyInjector.inject(prop);
@PostConstruct
public void init() {
this.kjbProperty = kjbPropertyInjector.inject(new KjbProperty());
try {
this.kjbUmsService = new KjbUmsService(prop);
this.kjbEmailSendModule = KjbEmailSendModule.getInstance(prop, messageRequestRepository);
this.kjbUmsService = new KjbUmsService(kjbProperty);
this.kjbEmailSendModule = KjbEmailSendModule.getInstance(kjbProperty, messageRequestRepository);
log.info("KjbUmsService and KjbEmailSendModule initialized successfully");
} catch (Exception e) {
log.error("Failed to initialize KjbUmsService or KjbEmailSendModule: {}", e.getMessage(), e);
}
}
/**
* URL 설정값이 비어있을 경우 DB에서 재로딩 후 서비스 재초기화
*/
private void reloadPropertyIfNeeded() {
if (!kjbPropertyInjector.isUrlConfigured(kjbProperty)) {
log.info("KjbProperty URL 값이 비어있어 재로딩 시도");
KjbProperty reloaded = kjbPropertyInjector.reloadIfUrlEmpty(kjbProperty);
if (reloaded != kjbProperty && kjbPropertyInjector.isUrlConfigured(reloaded)) {
this.kjbProperty = reloaded;
try {
this.kjbUmsService = new KjbUmsService(kjbProperty);
this.kjbEmailSendModule = KjbEmailSendModule.getInstance(kjbProperty, messageRequestRepository);
log.info("KjbProperty 재로딩 후 서비스 재초기화 완료");
} catch (Exception e) {
log.error("KjbProperty 재로딩 후 서비스 재초기화 실패: {}", e.getMessage(), e);
}
}
}
}
@SuppressWarnings("unchecked")
@Transactional(transactionManager = "transactionManagerForEMS")
public void processMessages() {
log.info("Starting to process pending messages");
// 상태가 PENDING인 메시지들을 조회하고 바로 PROCESSING으로 변경
// URL 설정값이 비어있을 경우 DB에서 재로딩 시도
reloadPropertyIfNeeded();
// 상태가 PENDING인 메시지들을 조회하고 바로 PROCESSING으로 변경
List<MessageRequest> pendingMessages = entityManager.createQuery(
"SELECT m FROM MessageRequest m WHERE m.requestStatus = :status " +
"ORDER BY m.requestDate ASC")
@@ -0,0 +1,73 @@
package com.eactive.eai.rms.onl.common.util;
import java.io.File;
import java.util.StringJoiner;
public class FileSystemUtil {
/**
* Normalizes a path by joining segments and applying the correct file separator for the current operating system.
* It handles redundant separators between segments.
*
* <p>Examples for Linux/macOS:</p>
* <ul>
* <li>{@code pathNormalize("/asd1/asd2", "asd3")} returns {@code "/asd1/asd2/asd3"}</li>
* <li>{@code pathNormalize("/asd1/asd2/", "/asd3")} returns {@code "/asd1/asd2/asd3"}</li>
* </ul>
*
* <p>Examples for Windows:</p>
* <ul>
* <li>{@code pathNormalize("C:/Users/Test", "Documents")} returns {@code "C:\\Users\\Test\\Documents"}</li>
* <li>{@code pathNormalize("C:/Users/Test/", "/Documents")} returns {@code "C:\\Users\\Test\\Documents"}</li>
* </ul>
*
* @param args A variable number of path segments to join.
* @return A normalized path string for the current operating system.
*/
public static String pathNormalize(String... args) {
if (args == null || args.length == 0) {
return "";
}
StringJoiner joiner = new StringJoiner(File.separator);
for (String arg : args) {
if (arg != null && !arg.isEmpty()) {
String segment = arg;
// Remove leading separators from the segment
while (segment.startsWith("/") || segment.startsWith("\\")) {
segment = segment.substring(1);
}
// Remove trailing separators from the segment
while (segment.endsWith("/") || segment.endsWith("\\")) {
segment = segment.substring(0, segment.length() - 1);
}
if (!segment.isEmpty()) {
joiner.add(segment);
}
}
}
String joinedPath = joiner.toString();
// Handle the root path for the first argument
String firstArg = args[0];
if (firstArg != null && !firstArg.isEmpty()) {
if (firstArg.startsWith("/")) {
joinedPath = File.separator + joinedPath;
} else if (firstArg.matches("^[a-zA-Z]:\\\\") || firstArg.matches("^[a-zA-Z]:/.*")) { // Handle Windows drive letters, e.g., "C:\" or "C:/"
String drive = firstArg.substring(0, 2);
return drive + File.separator + joinedPath.substring(joinedPath.indexOf(File.separator) + 1);
}
}
// Final normalization for Windows if the path starts with a separator (e.g. from a UNC path)
if (System.getProperty("os.name").toLowerCase().contains("win")) {
if (args[0] != null && args[0].startsWith("//") || args[0].startsWith("\\\\")) {
return "\\\\" + joinedPath;
}
return joinedPath.replace("/", "\\");
}
return joinedPath.replace("\\", "/");
}
}
@@ -0,0 +1,199 @@
package com.eactive.eai.rms.onl.manage.inflow.group;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.ModelAndView;
import com.eactive.eai.agent.command.CommonCommand;
import com.eactive.eai.rms.common.base.OnlBaseAnnotationController;
import com.eactive.eai.rms.common.combo.ComboService;
import com.eactive.eai.rms.common.combo.ComboVo;
import com.eactive.eai.rms.common.vo.GridResponse;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
@Controller
public class InflowGroupControlManController extends OnlBaseAnnotationController {
@Autowired
private InflowGroupControlManService service;
@Autowired
private ComboService comboService;
private ObjectMapper objectMapper = new ObjectMapper();
// ========== View Mappings ==========
@RequestMapping(value = "/onl/admin/inflow/inflowGroupControlMan.view")
public String viewList() {
return "/onl/admin/inflow/inflowGroupControlMan";
}
@RequestMapping(value = "/onl/admin/inflow/inflowGroupControlMan.view", params = "cmd=DETAIL")
public String viewDetail() {
return "/onl/admin/inflow/inflowGroupControlManDetail";
}
@RequestMapping(value = "/onl/admin/inflow/inflowGroupControlMan.view", params = "cmd=INTERFACE_POPUP")
public String viewInterfacePopup() {
return "/onl/admin/inflow/inflowGroupInterfacePopup";
}
// ========== JSON API Mappings ==========
/**
* 그룹 목록 조회
*/
@RequestMapping(value = "/onl/admin/inflow/inflowGroupControlMan.json", params = "cmd=LIST")
public ResponseEntity<GridResponse<InflowGroupControlManUI>> selectList(HttpServletRequest request,
Pageable pageVo, String searchGroupName) {
Page<InflowGroupControlManUI> uiPage = service.selectGroupList(pageVo, searchGroupName);
return ResponseEntity.ok(new GridResponse<>(uiPage));
}
/**
* 그룹 상세 조회
*/
@RequestMapping(value = "/onl/admin/inflow/inflowGroupControlMan.json", params = "cmd=DETAIL")
public ResponseEntity<InflowGroupControlManUI> selectDetail(HttpServletRequest request,
HttpServletResponse response, String groupId) {
InflowGroupControlManUI ui = service.selectGroupDetail(groupId);
return ResponseEntity.ok(ui);
}
/**
* 그룹 저장 (INSERT)
*/
@RequestMapping(value = "/onl/admin/inflow/inflowGroupControlMan.json", params = "cmd=INSERT")
public String insert(HttpServletRequest request, HttpServletResponse response, InflowGroupControlManUI ui,
@RequestParam(value = "interfaceListJson", required = false) String interfaceListJson) throws Exception {
parseInterfaceList(ui, interfaceListJson);
String modifiedBy = getSessionUserId(request);
service.mergeGroup(ui, modifiedBy);
// Agent Command 전송
CommonCommand command = new CommonCommand("com.eactive.eai.agent.inflow.ReloadInflowGroupControlCommand",
ui.getGroupId());
agentUtilService.broadcast(command);
return null;
}
/**
* 그룹 저장 (UPDATE)
*/
@RequestMapping(value = "/onl/admin/inflow/inflowGroupControlMan.json", params = "cmd=UPDATE")
public String update(HttpServletRequest request, HttpServletResponse response, InflowGroupControlManUI ui,
@RequestParam(value = "interfaceListJson", required = false) String interfaceListJson) throws Exception {
parseInterfaceList(ui, interfaceListJson);
String modifiedBy = getSessionUserId(request);
service.mergeGroup(ui, modifiedBy);
// Agent Command 전송
CommonCommand command = new CommonCommand("com.eactive.eai.agent.inflow.ReloadInflowGroupControlCommand",
ui.getGroupId());
agentUtilService.broadcast(command);
return null;
}
/**
* 그룹 삭제
*/
@RequestMapping(value = "/onl/admin/inflow/inflowGroupControlMan.json", params = "cmd=DELETE")
public String delete(HttpServletRequest request, HttpServletResponse response, String groupId) throws Exception {
service.deleteGroup(groupId);
// Agent Command 전송
CommonCommand command = new CommonCommand("com.eactive.eai.agent.inflow.RemoveInflowGroupControlCommand",
groupId);
agentUtilService.broadcast(command);
return null;
}
/**
* 콤보박스 초기화 데이터
*/
@RequestMapping(value = "/onl/admin/inflow/inflowGroupControlMan.json", params = "cmd=LIST_INIT_COMBO")
public ModelAndView initCombo(HttpServletRequest request, HttpServletResponse response) {
List<ComboVo> useYnRows = comboService.getFromCode("USE_YN");
List<ComboVo> timeUnitRows = comboService.getFromCode("INFLOW_CTRL_TIME_UNIT");
Map<String, List<ComboVo>> resultMap = new HashMap<>();
resultMap.put("useYnRows", useYnRows);
resultMap.put("timeUnitRows", timeUnitRows);
return new ModelAndView("jsonView", resultMap);
}
/**
* 인터페이스 목록 조회 (팝업용)
*/
@RequestMapping(value = "/onl/admin/inflow/inflowGroupControlMan.json", params = "cmd=INTERFACE_LIST")
public ResponseEntity<GridResponse<InflowGroupMappingUI>> selectInterfaceList(HttpServletRequest request,
Pageable pageVo, String searchName) {
Page<InflowGroupMappingUI> uiPage = service.selectInterfaceListForPopup(pageVo, searchName);
return ResponseEntity.ok(new GridResponse<>(uiPage));
}
/**
* 인터페이스 중복 등록 체크
*/
@RequestMapping(value = "/onl/admin/inflow/inflowGroupControlMan.json", params = "cmd=LIST_CHECK_DUPLICATE")
public ModelAndView checkDuplicate(HttpServletRequest request, HttpServletResponse response,
@RequestParam("interfaceId") String interfaceId,
@RequestParam(value = "groupId", required = false) String groupId) {
String existingGroupName = service.checkInterfaceDuplicate(interfaceId, groupId);
Map<String, Object> resultMap = new HashMap<>();
resultMap.put("duplicate", existingGroupName != null);
resultMap.put("groupName", existingGroupName);
return new ModelAndView("jsonView", resultMap);
}
/**
* 그룹 버킷 상태 조회 (GW 서버별 현황)
*/
@RequestMapping(value = "/onl/admin/inflow/inflowGroupControlMan.json", params = "cmd=LIST_BUCKET_STATUS")
public ModelAndView getBucketStatus(HttpServletRequest request, HttpServletResponse response,
@RequestParam("groupId") String groupId) {
List<Map<String, Object>> statusList = service.getGroupBucketStatus(groupId);
Map<String, Object> resultMap = new HashMap<>();
resultMap.put("servers", statusList);
return new ModelAndView("jsonView", resultMap);
}
/**
* JSON 문자열을 인터페이스 목록으로 변환
*/
private void parseInterfaceList(InflowGroupControlManUI ui, String interfaceListJson) throws Exception {
if (interfaceListJson != null && !interfaceListJson.isEmpty()) {
List<InflowGroupMappingUI> interfaceList = objectMapper.readValue(interfaceListJson,
new TypeReference<List<InflowGroupMappingUI>>() {
});
ui.setInterfaceList(interfaceList);
}
}
/**
* 세션에서 사용자 ID 조회
*/
private String getSessionUserId(HttpServletRequest request) {
Object userId = request.getSession().getAttribute("userId");
return userId != null ? userId.toString() : "SYSTEM";
}
}
@@ -0,0 +1,29 @@
package com.eactive.eai.rms.onl.manage.inflow.group;
import org.mapstruct.BeanMapping;
import org.mapstruct.Mapper;
import org.mapstruct.MappingTarget;
import org.mapstruct.NullValuePropertyMappingStrategy;
import org.mapstruct.ReportingPolicy;
import com.eactive.eai.data.entity.onl.inflow.InflowControlGroup;
import com.eactive.eai.data.entity.onl.inflow.InflowControlGroupMapping;
@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE)
public interface InflowGroupControlManMapper {
InflowGroupControlManUI toVo(InflowControlGroup entity);
InflowControlGroup toEntity(InflowGroupControlManUI vo);
@BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
void updateToEntity(InflowGroupControlManUI vo, @MappingTarget InflowControlGroup entity);
// Mapping Entity <-> UI
InflowGroupMappingUI toMappingVo(InflowControlGroupMapping entity);
InflowControlGroupMapping toMappingEntity(InflowGroupMappingUI vo);
@BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
void updateToMappingEntity(InflowGroupMappingUI vo, @MappingTarget InflowControlGroupMapping entity);
}
@@ -0,0 +1,316 @@
package com.eactive.eai.rms.onl.manage.inflow.group;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.client.RestTemplate;
import com.eactive.eai.data.entity.onl.inflow.InflowControlGroup;
import com.eactive.eai.data.entity.onl.inflow.InflowControlGroupMapping;
import com.eactive.eai.data.entity.onl.inflow.QInflowControlGroup;
import com.eactive.eai.data.entity.onl.inflow.QInflowControlGroupMapping;
import com.eactive.eai.data.entity.onl.message.QEAIMessageEntity;
import com.eactive.eai.rms.common.base.BaseService;
import com.eactive.eai.rms.data.entity.onl.inflow.InflowControlGroupMappingRepository;
import com.eactive.eai.rms.data.entity.onl.inflow.InflowControlGroupRepository;
import com.eactive.eai.rms.onl.common.service.EaiServerInfoService;
import com.querydsl.core.BooleanBuilder;
import com.querydsl.core.Tuple;
import com.querydsl.jpa.impl.JPAQueryFactory;
@Service
public class InflowGroupControlManService extends BaseService {
private static final Logger logger = LoggerFactory.getLogger(InflowGroupControlManService.class);
@PersistenceContext
EntityManager entityManager;
@Autowired
private InflowControlGroupRepository groupRepository;
@Autowired
private InflowControlGroupMappingRepository mappingRepository;
@Autowired
private InflowGroupControlManMapper mapper;
@Autowired
private EaiServerInfoService eaiServerInfoService;
private RestTemplate restTemplate = new RestTemplate();
/**
* 그룹 목록 조회
*/
public Page<InflowGroupControlManUI> selectGroupList(Pageable pageable, String searchGroupName) {
JPAQueryFactory jpaQueryFactory = new JPAQueryFactory(entityManager);
QInflowControlGroup qGroup = QInflowControlGroup.inflowControlGroup;
BooleanBuilder boolBuilder = new BooleanBuilder();
if (!StringUtils.isEmpty(searchGroupName)) {
boolBuilder.and(qGroup.groupName.contains(searchGroupName));
}
List<InflowControlGroup> groupList = jpaQueryFactory
.selectFrom(qGroup)
.where(boolBuilder)
.orderBy(qGroup.groupId.asc())
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetch();
List<InflowGroupControlManUI> uiList = groupList.stream()
.map(mapper::toVo)
.collect(Collectors.toList());
long totalCount = jpaQueryFactory
.select(qGroup.groupId.count())
.from(qGroup)
.where(boolBuilder)
.fetchOne();
return new PageImpl<>(uiList, pageable, totalCount);
}
/**
* 그룹 상세 조회 (매핑된 인터페이스 포함)
*/
public InflowGroupControlManUI selectGroupDetail(String groupId) {
Optional<InflowControlGroup> groupOpt = groupRepository.findById(groupId);
if (!groupOpt.isPresent()) {
return null;
}
InflowGroupControlManUI ui = mapper.toVo(groupOpt.get());
// 매핑된 인터페이스 목록 조회 (인터페이스 설명 포함)
List<InflowGroupMappingUI> mappingList = selectMappingListWithDesc(groupId);
ui.setInterfaceList(mappingList);
return ui;
}
/**
* 그룹에 매핑된 인터페이스 목록 조회 (인터페이스 설명 포함)
*/
private List<InflowGroupMappingUI> selectMappingListWithDesc(String groupId) {
JPAQueryFactory jpaQueryFactory = new JPAQueryFactory(entityManager);
QInflowControlGroupMapping qMapping = QInflowControlGroupMapping.inflowControlGroupMapping;
QEAIMessageEntity qMessage = QEAIMessageEntity.eAIMessageEntity;
List<Tuple> tupleList = jpaQueryFactory
.select(qMapping, qMessage.eaisvcdesc)
.from(qMapping)
.leftJoin(qMessage).on(qMapping.interfaceId.eq(qMessage.eaisvcname))
.where(qMapping.groupId.eq(groupId))
.orderBy(qMapping.interfaceId.asc())
.fetch();
return tupleList.stream()
.map(tuple -> {
InflowGroupMappingUI mappingUI = mapper.toMappingVo(tuple.get(qMapping));
mappingUI.setInterfaceDesc(tuple.get(qMessage.eaisvcdesc));
return mappingUI;
})
.collect(Collectors.toList());
}
/**
* 그룹 저장 (INSERT/UPDATE)
*/
@Transactional
public void mergeGroup(InflowGroupControlManUI ui, String modifiedBy) {
String groupId = ui.getGroupId();
boolean isNew = StringUtils.isEmpty(groupId);
InflowControlGroup entity;
if (!isNew) {
Optional<InflowControlGroup> groupOpt = groupRepository.findById(groupId);
if (groupOpt.isPresent()) {
entity = groupOpt.get();
mapper.updateToEntity(ui, entity);
} else {
entity = mapper.toEntity(ui);
}
} else {
entity = mapper.toEntity(ui);
}
entity.setModifiedBy(modifiedBy);
entity.setModifiedAt(LocalDateTime.now());
InflowControlGroup savedEntity = groupRepository.saveAndFlush(entity);
// 저장된 엔티티의 groupId 사용 (신규 생성 시 시퀀스에서 생성된 ID)
String savedGroupId = savedEntity.getGroupId();
// 매핑 정보 저장
saveMappings(savedGroupId, ui.getInterfaceList(), modifiedBy);
}
/**
* 그룹-인터페이스 매핑 저장
*/
@Transactional
public void saveMappings(String groupId, List<InflowGroupMappingUI> interfaceList, String modifiedBy) {
// 기존 매핑 삭제
mappingRepository.deleteByGroupId(groupId);
// 새로운 매핑 저장
if (interfaceList != null && !interfaceList.isEmpty()) {
for (InflowGroupMappingUI mappingUI : interfaceList) {
InflowControlGroupMapping mapping = new InflowControlGroupMapping();
mapping.setInterfaceId(mappingUI.getInterfaceId());
mapping.setGroupId(groupId);
// 화면에서 전달된 useYn 사용, 없으면 기본값 "1"
String useYn = StringUtils.isNotEmpty(mappingUI.getUseYn()) ? mappingUI.getUseYn() : "1";
mapping.setUseYn(useYn);
mapping.setModifiedBy(modifiedBy);
mapping.setModifiedAt(LocalDateTime.now());
mappingRepository.save(mapping);
}
}
}
/**
* 그룹 삭제
*/
@Transactional
public void deleteGroup(String groupId) {
// 매핑 먼저 삭제
mappingRepository.deleteByGroupId(groupId);
// 그룹 삭제
groupRepository.deleteById(groupId);
}
/**
* 인터페이스 중복 등록 체크 (다른 그룹에 등록 여부)
* @param interfaceId 체크할 인터페이스 ID
* @param currentGroupId 현재 그룹 ID (수정 시 자기 그룹 제외, 신규 시 null)
* @return 등록된 그룹명 (미등록 시 null)
*/
public String checkInterfaceDuplicate(String interfaceId, String currentGroupId) {
Optional<InflowControlGroupMapping> mappingOpt = mappingRepository.findByInterfaceId(interfaceId);
if (!mappingOpt.isPresent()) {
return null;
}
InflowControlGroupMapping mapping = mappingOpt.get();
String existingGroupId = mapping.getGroupId();
// 현재 그룹과 동일하면 중복 아님 (수정 시)
if (existingGroupId.equals(currentGroupId)) {
return null;
}
// 그룹명 조회
Optional<InflowControlGroup> groupOpt = groupRepository.findById(existingGroupId);
if (groupOpt.isPresent()) {
return groupOpt.get().getGroupName();
}
return existingGroupId; // 그룹명 없으면 ID 반환
}
/**
* 인터페이스 목록 조회 (팝업용)
*/
public Page<InflowGroupMappingUI> selectInterfaceListForPopup(Pageable pageable, String searchName) {
JPAQueryFactory jpaQueryFactory = new JPAQueryFactory(entityManager);
QEAIMessageEntity qMessage = QEAIMessageEntity.eAIMessageEntity;
BooleanBuilder boolBuilder = new BooleanBuilder();
if (!StringUtils.isEmpty(searchName)) {
boolBuilder.and(qMessage.eaisvcname.containsIgnoreCase(searchName)
.or(qMessage.eaisvcdesc.containsIgnoreCase(searchName)));
}
List<Tuple> tupleList = jpaQueryFactory
.select(qMessage.eaisvcname, qMessage.eaisvcdesc)
.from(qMessage)
.where(boolBuilder)
.orderBy(qMessage.eaisvcname.asc())
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetch();
List<InflowGroupMappingUI> uiList = tupleList.stream()
.map(tuple -> {
InflowGroupMappingUI ui = new InflowGroupMappingUI();
ui.setInterfaceId(tuple.get(qMessage.eaisvcname));
ui.setInterfaceDesc(tuple.get(qMessage.eaisvcdesc));
return ui;
})
.collect(Collectors.toList());
long totalCount = jpaQueryFactory
.select(qMessage.eaisvcname.count())
.from(qMessage)
.where(boolBuilder)
.fetchOne();
return new PageImpl<>(uiList, pageable, totalCount);
}
/**
* 그룹 버킷 상태 조회 (모든 GW 서버에서 수집)
*/
@SuppressWarnings("unchecked")
public List<Map<String, Object>> getGroupBucketStatus(String groupId) {
List<Map<String, Object>> result = new ArrayList<>();
List<Map<String, String>> serverList = eaiServerInfoService.getEaiServerIpList();
for (Map<String, String> server : serverList) {
String serverName = server.get("EAISVRINSTNM");
String serverIp = server.get("EAISVRIP");
String serverPort = server.get("WEBSERVERPORT");
if (StringUtils.isEmpty(serverPort)) {
serverPort = server.get("EAISVRLSNPORT");
}
Map<String, Object> serverStatus = new HashMap<>();
serverStatus.put("serverName", serverName);
serverStatus.put("serverIp", serverIp);
serverStatus.put("serverPort", serverPort);
try {
String url = String.format("http://%s:%s/manage/inflow/group/%s/bucket-status",
serverIp, serverPort, groupId);
ResponseEntity<Map> response = restTemplate.getForEntity(url, Map.class);
if (response.getBody() != null && Boolean.TRUE.equals(response.getBody().get("success"))) {
serverStatus.put("status", "online");
serverStatus.put("data", response.getBody().get("data"));
} else {
serverStatus.put("status", "no_data");
serverStatus.put("message", "그룹이 로드되지 않음");
}
} catch (Exception e) {
logger.warn("버킷 상태 조회 실패 - server: {}, error: {}", serverName, e.getMessage());
serverStatus.put("status", "offline");
serverStatus.put("message", "서버 연결 실패");
}
result.add(serverStatus);
}
return result;
}
}
@@ -0,0 +1,60 @@
package com.eactive.eai.rms.onl.manage.inflow.group;
import java.time.LocalDateTime;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
@Data
public class InflowGroupControlManUI {
/**
* 그룹 ID
*/
@JsonProperty("GROUPID")
private String groupId;
/**
* 그룹명
*/
@JsonProperty("GROUPNAME")
private String groupName;
/**
* 임계치
*/
@JsonProperty("THRESHOLD")
private Integer threshold;
/**
* 초당 임계치
*/
@JsonProperty("THRESHOLDPERSECOND")
private Integer thresholdPerSecond;
/**
* 임계치 TimeUnit
*/
@JsonProperty("THRESHOLDTIMEUNIT")
private String thresholdTimeUnit;
/**
* 사용여부
*/
@JsonProperty("USEYN")
private String useYn;
/**
* 수정일시
*/
@JsonProperty("MODIFIEDAT")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime modifiedAt;
/**
* 매핑된 인터페이스 목록 (저장용)
*/
private List<InflowGroupMappingUI> interfaceList;
}
@@ -0,0 +1,37 @@
package com.eactive.eai.rms.onl.manage.inflow.group;
import com.fasterxml.jackson.annotation.JsonAlias;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
@Data
public class InflowGroupMappingUI {
/**
* 인터페이스 ID
*/
@JsonProperty("INTERFACEID")
@JsonAlias("interfaceId")
private String interfaceId;
/**
* 인터페이스 설명 (조회용)
*/
@JsonProperty("INTERFACEDESC")
@JsonAlias("interfaceDesc")
private String interfaceDesc;
/**
* 그룹 ID
*/
@JsonProperty("GROUPID")
@JsonAlias("groupId")
private String groupId;
/**
* 사용여부
*/
@JsonProperty("USEYN")
@JsonAlias("useYn")
private String useYn;
}
@@ -19,6 +19,48 @@ public class KjbPropertyInjector {
this.groupId = groupId;
}
/**
* KjbProperty의 URL 값들이 설정되어 있는지 확인
* @param property 검사할 KjbProperty 객체
* @return URL 값들이 모두 설정되어 있으면 true
*/
public boolean isUrlConfigured(KjbProperty property) {
if (property == null) return false;
// 주요 URL 값들 중 하나라도 설정되어 있으면 true
return StringUtils.isNotEmpty(property.getEaiBatchUrl())
|| StringUtils.isNotEmpty(property.getUmsHostUrl())
|| StringUtils.isNotEmpty(property.getObpUrl());
}
/**
* URL 값이 비어있을 경우 DB에서 재로딩 시도
* @param property 기존 KjbProperty 객체
* @return 재로딩된 KjbProperty 객체 (재로딩 실패 시 기존 객체 반환)
*/
public KjbProperty reloadIfUrlEmpty(KjbProperty property) {
if (property == null) {
return inject(new KjbProperty());
}
if (!isUrlConfigured(property)) {
log.info("KjbProperty URL 값이 비어있어 DB에서 재로딩 시도");
try {
KjbProperty reloaded = inject(new KjbProperty());
if (isUrlConfigured(reloaded)) {
log.info("KjbProperty 재로딩 성공");
return reloaded;
} else {
log.warn("KjbProperty 재로딩 후에도 URL 값이 비어있음");
}
} catch (Exception e) {
log.error("KjbProperty 재로딩 실패: {}", e.getMessage(), e);
}
}
return property;
}
public <T> T inject(T property) {
Class<?> clazz = property.getClass();
@@ -30,6 +72,11 @@ public class KjbPropertyInjector {
String rawValue = monitoringPropertyService.getPropertyValue(groupId, key, anno.defaultValue());
Object convertedValue = convertValue(field.getType(), rawValue);
// null이면 필드의 기본값 유지 (primitive 타입에 null 할당 방지)
if (convertedValue == null) {
continue;
}
field.setAccessible(true);
try {
field.set(property, convertedValue);
@@ -46,16 +93,26 @@ public class KjbPropertyInjector {
private static Object convertValue(Class<?> type, String rawValue) {
if (rawValue == null) return null;
if (StringUtils.isEmpty(rawValue)) return "";
// 빈 문자열인 경우 null 반환 (필드의 기본값 유지)
if (StringUtils.isEmpty(rawValue)) {
return null;
}
if (type == String.class) return rawValue;
if (type == Integer.class || type == int.class) return Integer.valueOf(rawValue);
if (type == Long.class || type == long.class) return Long.valueOf(rawValue);
if (type == Double.class || type == double.class) return Double.valueOf(rawValue);
if (type == Boolean.class || type == boolean.class) return Boolean.valueOf(rawValue);
if (type == Float.class || type == float.class) return Float.valueOf(rawValue);
if (type == Short.class || type == short.class) return Short.valueOf(rawValue);
if (type == Byte.class || type == byte.class) return Byte.valueOf(rawValue);
try {
if (type == Integer.class || type == int.class) return Integer.valueOf(rawValue);
if (type == Long.class || type == long.class) return Long.valueOf(rawValue);
if (type == Double.class || type == double.class) return Double.valueOf(rawValue);
if (type == Boolean.class || type == boolean.class) return Boolean.valueOf(rawValue);
if (type == Float.class || type == float.class) return Float.valueOf(rawValue);
if (type == Short.class || type == short.class) return Short.valueOf(rawValue);
if (type == Byte.class || type == byte.class) return Byte.valueOf(rawValue);
} catch (NumberFormatException e) {
log.warn("Failed to convert value '{}' to type {}, returning null", rawValue, type.getSimpleName());
return null;
}
return rawValue;
}
@@ -0,0 +1,90 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE sqlMap PUBLIC "-//ibatis.apache.org//DTD SQL Map 2.0//EN" "http://ibatis.apache.org/dtd/sql-map-2.dtd">
<sqlMap namespace="ObpGwMetric">
<!-- EAIBZWKDSTCD 코드 목록 조회 (TSEAICM01) -->
<statement id="selectAllBzwkCodes" parameterClass="java.util.HashMap" resultClass="java.lang.String">
SELECT EAIBZWKDSTCD
FROM $schemaId$.TSEAICM01
ORDER BY EAIBZWKDSTCD
</statement>
<!-- 로그 집계 결과 매핑 -->
<resultMap id="obpGwMetricResult" class="com.eactive.eai.rms.data.entity.onl.apim.obp.ObpGwMetricVO">
<result property="apiId" column="API_ID"/>
<result property="clientId" column="CLIENT_ID"/>
<result property="hostname" column="HOSTNAME"/>
<result property="orgId" column="ORG_ID"/>
<result property="timeslice" column="TIMESLICE"/>
<result property="attemptedCount" column="ATTEMPTED_COUNT"/>
<result property="completedCount" column="COMPLETED_COUNT"/>
</resultMap>
<!--
EAIBZWKDSTCD별 시간대 로그 집계 (Index Range Scan 활용)
에러 판단 기준:
- 성공 조건: RSPNSERRCDNAME IS NULL 또는 RSPNSERRCDNAME LIKE 'ESB%'
- 에러 조건: RE%, RF%, REC%, RIB%, 기타 모든 에러 코드
성능 최적화:
- PK 인덱스 (EAIBZWKDSTCD, MSGDPSTYMS, EAISVCSERNO, LOGPRCSSSERNO) 활용
- EAIBZWKDSTCD를 고정하여 Index Range Scan 보장
-->
<statement id="selectHourlyMetricsByBzwkCode" parameterClass="java.util.HashMap" resultMap="obpGwMetricResult">
SELECT
EAISVCNAME AS API_ID,
CLIENTID AS CLIENT_ID,
EAISEVRINSTNCNAME AS HOSTNAME,
ORGID AS ORG_ID,
'$timeslice$' AS TIMESLICE,
SUM(CASE WHEN LOGPRCSSSERNO = '100' THEN 1 ELSE 0 END) AS ATTEMPTED_COUNT,
SUM(CASE WHEN LOGPRCSSSERNO = '400'
AND (RSPNSERRCDNAME IS NULL OR RSPNSERRCDNAME LIKE 'ESB%')
THEN 1 ELSE 0 END) AS COMPLETED_COUNT
FROM $schemaId$.$tableName$
WHERE EAIBZWKDSTCD = #bzwkCode#
AND MSGDPSTYMS BETWEEN #startTime# AND #endTime#
GROUP BY EAISVCNAME, CLIENTID, EAISEVRINSTNCNAME, ORGID
HAVING SUM(CASE WHEN LOGPRCSSSERNO = '100' THEN 1 ELSE 0 END) > 0
OR SUM(CASE WHEN LOGPRCSSSERNO = '400' THEN 1 ELSE 0 END) > 0
</statement>
<!-- API 이름 조회 (TSEAIHE01) -->
<statement id="selectApiNames" parameterClass="java.util.HashMap" resultClass="java.util.HashMap">
SELECT
EAISVCNAME AS API_ID,
EAISVCDESC AS API_NAME
FROM $schemaId$.TSEAIHE01
WHERE EAISVCNAME IN
<iterate property="apiIds" open="(" close=")" conjunction=",">
#apiIds[]#
</iterate>
</statement>
<!-- URI 조회 (TSEAIHS04) - APIFULLPATH 사용 -->
<statement id="selectUris" parameterClass="java.util.HashMap" resultClass="java.util.HashMap">
SELECT
EAISVCNAME AS API_ID,
APIFULLPATH AS URI
FROM $schemaId$.TSEAIHS04
WHERE EAISVCNAME IN
<iterate property="apiIds" open="(" close=")" conjunction=",">
#apiIds[]#
</iterate>
</statement>
<!-- AppId 조회 (ptl_app_request) - org_id로 조회 -->
<statement id="selectAppIdsByOrgIds" parameterClass="java.util.HashMap" resultClass="java.util.HashMap">
SELECT
ORG_ID,
ID AS APP_ID
FROM PTL_APP_REQUEST
WHERE ORG_ID IN
<iterate property="orgIds" open="(" close=")" conjunction=",">
#orgIds[]#
</iterate>
</statement>
</sqlMap>
@@ -0,0 +1,50 @@
package com.eactive.eai.rms.onl.common.util;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledOnOs;
import org.junit.jupiter.api.condition.OS;
import java.io.File;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class FileSystemUtilTest {
@Test
@EnabledOnOs(OS.LINUX)
void testPathNormalize_Linux() {
assertEquals("/asd1/asd2/asd3", FileSystemUtil.pathNormalize("/asd1/asd2", "asd3"));
assertEquals("/asd1/asd2/asd3", FileSystemUtil.pathNormalize("/asd1/asd2/", "/asd3"));
assertEquals("/asd1/asd2/asd3", FileSystemUtil.pathNormalize("/asd1/asd2", "/asd3"));
assertEquals("/asd1/asd2/asd3", FileSystemUtil.pathNormalize("/asd1/asd2/", "asd3/"));
assertEquals("/a/b/c", FileSystemUtil.pathNormalize("/a/", "/b/", "/c/"));
assertEquals("a/b/c", FileSystemUtil.pathNormalize("a", "b", "c"));
assertEquals("/a", FileSystemUtil.pathNormalize("/a"));
}
@Test
@EnabledOnOs(OS.WINDOWS)
void testPathNormalize_Windows() {
assertEquals("\\asd1\\asd2\\asd3", FileSystemUtil.pathNormalize("/asd1/asd2", "asd3"));
assertEquals("\\asd1\\asd2\\asd3", FileSystemUtil.pathNormalize("/asd1/asd2/", "/asd3"));
assertEquals("\\asd1\\asd2\\asd3", FileSystemUtil.pathNormalize("/asd1/asd2", "/asd3"));
assertEquals("\\asd1\\asd2\\asd3", FileSystemUtil.pathNormalize("/asd1/asd2/", "asd3/"));
assertEquals("C:\\Users\\Test", FileSystemUtil.pathNormalize("C:/Users", "Test"));
assertEquals("C:\\Users\\Test", FileSystemUtil.pathNormalize("C:\\Users", "Test"));
assertEquals("\\a\\b\\c", FileSystemUtil.pathNormalize("/a/", "/b/", "/c/"));
assertEquals("a\\b\\c", FileSystemUtil.pathNormalize("a", "b", "c"));
assertEquals("\\a", FileSystemUtil.pathNormalize("/a"));
}
@Test
void testPathNormalize_General() {
String expected = "a" + File.separator + "b" + File.separator + "c";
assertEquals(expected, FileSystemUtil.pathNormalize("a", "b", "c"));
assertEquals("", FileSystemUtil.pathNormalize(""));
assertEquals("", FileSystemUtil.pathNormalize(null));
assertEquals("", FileSystemUtil.pathNormalize());
assertEquals("a", FileSystemUtil.pathNormalize("a"));
assertEquals("a", FileSystemUtil.pathNormalize("a/"));
assertEquals("a", FileSystemUtil.pathNormalize("/a"));
}
}