Merge remote-tracking branch 'origin/master' into master

Resolve conflict in PortalNoticeManService.java import block:
union both sides — keep Arrays (HEAD) and LocalDateTime/ArrayList/Collections (origin/master),
all of which are used in the merged body.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Rinjae-gf63
2026-06-16 16:41:57 +09:00
145 changed files with 5797 additions and 2318 deletions
@@ -254,6 +254,13 @@ public interface MonitoringContext {
// 이중 로그인 허용여부
public static final String RMS_DUAL_LOGIN_ENABLED = "rms.DUAL_LOGIN_ENABLED";
// 자동 로그아웃 타임아웃 (단위: 분, 기본값: 10)
public static final String RMS_AUTO_LOGOUT_TIMEOUT = "rms.auto.logout.timeout";
// 웹훅 재전송 설정
public static final String API_WEBHOOK_RETRY_COUNT = "api.webhook.retry_count";
public static final String API_WEBHOOK_RETRY_TIME = "api.webhook.retry_time";
@@ -617,6 +617,8 @@ public class MainController implements InterceptorSkipController {
menuId = menuId == null ? "" : menuId;
model.addAttribute("mainPage", mainPage);
model.addAttribute("menuId", menuId);
model.addAttribute("autoLogoutTimeout",
monitoringContext.getIntProperty(MonitoringContext.RMS_AUTO_LOGOUT_TIMEOUT, 10));
if (logger.isDebugEnabled()) {
logger.debug("CURRENT SERVICE TYPE : " + service);
@@ -35,6 +35,7 @@ public class MonitoringPropertyManService extends BaseService {
public MonitoringPropertyGroupUI selectDetail(String prptyGroupName) {
MonitoringPropertyGroup propertyGroup = propertyGroupService.getById(prptyGroupName);
propertyGroup.getMonitoringProperties().sort(Comparator.comparing(p -> p.getId().getPrptyName()));
return propertyGroupMapper.toVo(propertyGroup);
}
@@ -1,13 +1,11 @@
package com.eactive.eai.rms.common.scheduler;
import com.eactive.eai.rms.common.base.BaseService;
import com.eactive.eai.rms.common.scheduler.ui.JobInfoMapper;
import com.eactive.eai.rms.common.scheduler.ui.JobInfoUI;
import com.eactive.eai.rms.common.scheduler.ui.ScheduleInfo;
import com.eactive.eai.rms.common.scheduler.ui.SchedulerUISearch;
import com.eactive.eai.rms.data.entity.man.monitoringCode.service.MonitoringCodeService;
import com.eactive.eai.rms.data.entity.man.scheduler.JobHistoryService;
import com.eactive.eai.rms.data.entity.man.scheduler.JobInfoService;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
import org.apache.commons.lang3.StringUtils;
import org.quartz.JobDetail;
import org.quartz.JobKey;
@@ -20,9 +18,14 @@ import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
import com.eactive.eai.rms.common.base.BaseService;
import com.eactive.eai.rms.common.scheduler.ui.JobInfoMapper;
import com.eactive.eai.rms.common.scheduler.ui.JobInfoUI;
import com.eactive.eai.rms.common.scheduler.ui.ScheduleInfo;
import com.eactive.eai.rms.common.scheduler.ui.SchedulerUISearch;
import com.eactive.eai.rms.data.entity.man.monitoringCode.service.MonitoringCodeService;
import com.eactive.eai.rms.data.entity.man.scheduler.JobHistoryService;
import com.eactive.eai.rms.data.entity.man.scheduler.JobInfoService;
@Service("schedulerService")
@Transactional(transactionManager = "transactionManagerForEMS")
@@ -122,10 +125,10 @@ public class SchedulerManService extends BaseService {
List<? extends Trigger> triggersOfJob = scheduler.getTriggersOfJob(new JobKey(jobName, Scheduler.DEFAULT_GROUP));
if(triggersOfJob.size() > 0) {
result.setStartTime(String.valueOf(triggersOfJob.get(0).getStartTime()));
result.setEndTime(String.valueOf(triggersOfJob.get(0).getEndTime()));
result.setPreviousFireTime(String.valueOf(triggersOfJob.get(0).getPreviousFireTime()));
result.setNextFireTime(String.valueOf(triggersOfJob.get(0).getNextFireTime()));
result.setStartTime(dateFormat(triggersOfJob.get(0).getStartTime()));
result.setEndTime(dateFormat(triggersOfJob.get(0).getEndTime()));
result.setPreviousFireTime(dateFormat(triggersOfJob.get(0).getPreviousFireTime()));
result.setNextFireTime(dateFormat(triggersOfJob.get(0).getNextFireTime()));
}
} catch (Exception e) {
@@ -150,5 +153,14 @@ public class SchedulerManService extends BaseService {
public void delete(String jobName) {
jobInfoService.deleteById(jobName);
}
private String dateFormat(Date date) {
if (date == null) {
return "";
} else {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
return sdf.format(date);
}
}
}
@@ -1,5 +1,6 @@
package com.eactive.eai.rms.common.util;
import java.math.BigDecimal;
import java.util.List;
import org.apache.log4j.Logger;
@@ -191,6 +192,20 @@ public class StringUtils
}
return sb.toString();
}
public static String toString(Object val) {
return val != null ? val.toString() : null;
}
public static Long toLong(Object val) {
return val != null ? ((Number) val).longValue() : null;
}
public static BigDecimal toDecimal(Object val) {
if (val == null) return null;
if (val instanceof BigDecimal) return (BigDecimal) val;
return new BigDecimal(val.toString());
}
//
// /**
// * - "" 나 Null 을 입력받아 SPACE 로 변환한다.
@@ -107,6 +107,17 @@ public class UserInfoService extends
}).collect(Collectors.toList());
}
public List<UserInfo> findAllByRoleId(String roleId) {
QUserInfo qUserInfo = QUserInfo.userInfo;
QUserRole qUserRole = QUserRole.userRole;
return getJPAQueryFactory()
.selectFrom(qUserInfo)
.join(qUserRole).on(qUserInfo.userid.eq(qUserRole.id.userId))
.where(qUserRole.id.roleId.eq(roleId))
.fetch();
}
public Page<UserInfo> findByUsername(Pageable pageable, String username) {
QUserInfo qUserInfo = QUserInfo.userInfo;
BooleanExpression predicate = qUserInfo.isNotNull();
@@ -0,0 +1,28 @@
package com.eactive.eai.rms.data.entity.onl.apim.portalinquiry;
import com.eactive.apim.portal.qna.entity.InquiryComment;
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;
@EMSDataSource
public interface PortalInquiryCommentRepository extends BaseRepository<InquiryComment, String> {
long countByInquiry_Id(String inquiryId);
long countByInquiry_IdAndDelYn(String inquiryId, String delYn);
List<InquiryComment> findByInquiry_IdAndDelYnOrderByCreatedDateAsc(String inquiryId, String delYn);
Optional<InquiryComment> findTopByInquiry_IdAndDelYnOrderByCreatedDateDesc(
String inquiryId, String delYn);
Optional<InquiryComment> findTopByInquiry_IdAndAdminYnAndDelYnOrderByCreatedDateDesc(
String inquiryId, String adminYn, String delYn);
boolean existsByInquiry_IdAndAdminYnAndDelYnAndCreatedDateAfter(
String inquiryId, String adminYn, String delYn, LocalDateTime after);
}
@@ -0,0 +1,31 @@
package com.eactive.eai.rms.data.entity.onl.apim.portalinquiry;
import com.eactive.apim.portal.qna.entity.InquiryComment;
import com.eactive.eai.rms.data.jpa.AbstractEMSDataSerivce;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
@Transactional(transactionManager = "transactionManagerForEMS")
public class PortalInquiryCommentService extends AbstractEMSDataSerivce<InquiryComment, String, PortalInquiryCommentRepository> {
// AbstractEMSDataSerivce의 repository 필드가 BaseRepository<T,I>로 고정되어
// 커스텀 메서드를 호출할 수 없으므로 별도 필드로 직접 주입
@Autowired
private PortalInquiryCommentRepository inquiryCommentRepository;
public long countByInquiryId(String inquiryId) {
return inquiryCommentRepository.countByInquiry_Id(inquiryId);
}
public long countActiveByInquiryId(String inquiryId) {
return inquiryCommentRepository.countByInquiry_IdAndDelYn(inquiryId, "N");
}
public List<InquiryComment> findByInquiryId(String inquiryId) {
return inquiryCommentRepository.findByInquiry_IdAndDelYnOrderByCreatedDateAsc(inquiryId, "N");
}
}
@@ -4,7 +4,10 @@ import com.eactive.apim.portal.qna.entity.Inquiry;
import com.eactive.eai.data.jpa.BaseRepository;
import com.eactive.eai.rms.data.EMSDataSource;
import java.util.List;
@EMSDataSource
public interface PortalInquiryRepository extends BaseRepository<Inquiry, String> {
List<Inquiry> findByInquiryStatus(String inquiryStatus);
}
@@ -1,20 +1,28 @@
package com.eactive.eai.rms.data.entity.onl.apim.portalinquiry;
import java.util.List;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.eactive.apim.portal.qna.entity.Inquiry;
import com.eactive.apim.portal.qna.entity.QInquiry;
import com.eactive.eai.rms.data.jpa.AbstractEMSDataSerivce;
import com.querydsl.core.BooleanBuilder;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
@Transactional(transactionManager = "transactionManagerForEMS")
public class PortalInquiryService extends AbstractEMSDataSerivce<Inquiry, String, PortalInquiryRepository> {
// AbstractEMSDataSerivce의 repository 필드가 BaseRepository<T,I>로 고정되어
// 커스텀 메서드를 호출할 수 없으므로 별도 필드로 직접 주입
@Autowired
private PortalInquiryRepository portalInquiryRepository;
public Page<Inquiry> findAll(Pageable pageable, PortalInquirySearch portalInquirySearch) {
QInquiry qInquiry = QInquiry.inquiry;
@@ -58,4 +66,8 @@ public class PortalInquiryService extends AbstractEMSDataSerivce<Inquiry, String
repository.deleteAll(inquiries);
}
}
public List<Inquiry> findByInquiryStatus(String inquiryStatus) {
return portalInquiryRepository.findByInquiryStatus(inquiryStatus);
}
}
@@ -1,4 +1,4 @@
package com.eactive.eai.rms.data.ext.djb.apistatus;
package com.eactive.eai.rms.data.entity.onl.djb.apistatus;
import java.time.LocalDateTime;
@@ -1,7 +1,8 @@
package com.eactive.eai.rms.data.ext.djb.apistatus;
package com.eactive.eai.rms.data.entity.onl.djb.apistatus;
public interface ApiStatusEvent {
String getEaisvcname();
String getEaisvcdesc();
String getPrevStatusCode();
String getEvent();
}
@@ -1,4 +1,4 @@
package com.eactive.eai.rms.data.ext.djb.inflow;
package com.eactive.eai.rms.data.entity.onl.djb.inflow;
public interface InflowTokenInsufficient {
String getEaisvcname();
@@ -1,4 +1,4 @@
package com.eactive.eai.rms.data.ext.djb.inflow;
package com.eactive.eai.rms.data.entity.onl.djb.inflow;
import java.io.Serializable;
@@ -1,4 +1,4 @@
package com.eactive.eai.rms.data.ext.djb.inflow;
package com.eactive.eai.rms.data.entity.onl.djb.inflow;
import java.io.Serializable;
import javax.persistence.Column;
@@ -0,0 +1,33 @@
package com.eactive.eai.rms.data.entity.onl.djb.webhook;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.Comment;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
@Entity
@Table(name = "PTL_WEBHOOK_REQ")
@Getter
@NoArgsConstructor
public class WebhookReq {
@Id
@Column(name = "ID", length = 36, nullable = false)
private String id;
@Column(name = "ORG_ID", length = 100)
@Comment("법인 ID")
private String orgId;
@Column(name = "TARGET_URL", length = 500, nullable = false)
@Comment("웹훅 수신 URL")
private String targetUrl;
@Column(name = "SECRET", length = 200)
@Comment("HMAC 서명용 Secret Key")
private String secret;
}
@@ -0,0 +1,19 @@
package com.eactive.eai.rms.data.entity.onl.djb.webhook;
import lombok.Getter;
import lombok.NoArgsConstructor;
import javax.persistence.EmbeddedId;
import javax.persistence.Entity;
import javax.persistence.Table;
import java.io.Serializable;
@Entity
@Table(name = "PTL_WEBHOOK_REQ_API")
@Getter
@NoArgsConstructor
public class WebhookReqApi implements Serializable {
@EmbeddedId
private WebhookReqApiId id;
}
@@ -0,0 +1,25 @@
package com.eactive.eai.rms.data.entity.onl.djb.webhook;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.Comment;
import javax.persistence.Column;
import javax.persistence.Embeddable;
import java.io.Serializable;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Embeddable
public class WebhookReqApiId implements Serializable {
@Column(name = "WEBHOOK_REQ_ID", nullable = false, length = 36)
@Comment("웹훅 요청 ID")
private String webhookReqId;
@Column(name = "API_ID", nullable = false, length = 100)
@Comment("API ID")
private String apiId;
}
@@ -0,0 +1,19 @@
package com.eactive.eai.rms.data.entity.onl.djb.webhook;
import lombok.Getter;
import lombok.NoArgsConstructor;
import javax.persistence.EmbeddedId;
import javax.persistence.Entity;
import javax.persistence.Table;
import java.io.Serializable;
@Entity
@Table(name = "PTL_WEBHOOK_REQ_EVENT")
@Getter
@NoArgsConstructor
public class WebhookReqEvent implements Serializable {
@EmbeddedId
private WebhookReqEventId id;
}
@@ -0,0 +1,25 @@
package com.eactive.eai.rms.data.entity.onl.djb.webhook;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.Comment;
import javax.persistence.Column;
import javax.persistence.Embeddable;
import java.io.Serializable;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Embeddable
public class WebhookReqEventId implements Serializable {
@Column(name = "WEBHOOK_REQ_ID", nullable = false, length = 36)
@Comment("웹훅 요청 ID")
private String webhookReqId;
@Column(name = "EVENT_TYPE", nullable = false, length = 50)
@Comment("이벤트 유형 (CONTROL_START, ERROR_START 등)")
private String eventType;
}
@@ -1,4 +1,4 @@
package com.eactive.eai.rms.data.ext.djb.webhook;
package com.eactive.eai.rms.data.entity.onl.djb.webhook;
import lombok.Getter;
import lombok.NoArgsConstructor;
@@ -42,6 +42,9 @@ public class WebhookSendLog {
@Column(name = "STATUS_CODE")
private Integer statusCode;
@Column(name = "ORG_ID")
private String orgId;
@Lob
@Column(name = "RESPONSE_BODY")
private String responseBody;
@@ -1,9 +1,6 @@
package com.eactive.eai.rms.data.entity.onl.inflow;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import javax.persistence.EntityManager;
@@ -18,6 +15,7 @@ import org.springframework.stereotype.Service;
import com.eactive.apim.portal.app.entity.QCredential;
import com.eactive.eai.data.entity.onl.adapter.QAdapterGroup;
import com.eactive.eai.data.entity.onl.authserver.QClientEntity;
import com.eactive.eai.data.entity.onl.inflow.InflowControl;
import com.eactive.eai.data.entity.onl.inflow.InflowControlId;
import com.eactive.eai.data.entity.onl.inflow.QInflowControl;
@@ -29,7 +27,6 @@ import com.eactive.eai.rms.onl.manage.inflow.history.InflowControlHistoryManUISe
import com.eactive.eai.rms.onl.manage.inflow.inflow.InflowControlManMapper;
import com.querydsl.core.BooleanBuilder;
import com.querydsl.core.Tuple;
import com.querydsl.core.types.OrderSpecifier;
import com.querydsl.jpa.impl.JPAQuery;
import com.querydsl.jpa.impl.JPAQueryFactory;
@@ -186,101 +183,67 @@ public class InflowControlService extends AbstractDataService<InflowControl, Inf
}
public InflowControlServiceDto findByIdForClient(String clientId) {
QCredential qCredential = QCredential.credential;
JPAQueryFactory jpaQueryFactory = new JPAQueryFactory(entityManager);
QInflowControl qInflowControl = QInflowControl.inflowControl;
QClientEntity qClientEntity = QClientEntity.clientEntity;
// EMSADM: Credential 조회
Tuple credTuple = new JPAQueryFactory(entityManagerForEMS)
.select(qCredential.clientid, qCredential.clientname)
.from(qCredential)
.where(qCredential.clientid.eq(clientId))
Tuple tuple = jpaQueryFactory
.select(qInflowControl, qClientEntity.clientid, qClientEntity.clientname)
.from(qClientEntity)
.leftJoin(qInflowControl)
.on(qInflowControl.id.type.eq(CLIENT_TYPE_INFLOW)
.and(qInflowControl.id.name.eq(qClientEntity.clientid)))
.where(qClientEntity.clientid.eq(clientId))
.fetchOne();
// AGWADM: InflowControl 조회
InflowControl inflowControl = new JPAQueryFactory(entityManager)
.selectFrom(qInflowControl)
.where(qInflowControl.id.type.eq(CLIENT_TYPE_INFLOW)
.and(qInflowControl.id.name.eq(clientId)))
.fetchOne();
InflowControlServiceDto dto;
if (inflowControl != null) {
dto = mapper.toDto(inflowControl);
} else {
dto = new InflowControlServiceDto();
dto.setName(clientId);
dto.setType(CLIENT_TYPE_INFLOW);
}
if (credTuple != null) {
dto.setDesc(credTuple.get(qCredential.clientname));
}
return dto;
return toDto(qClientEntity, qInflowControl, tuple);
}
public Page<InflowControlServiceDto> findAllForClient(Pageable pageable, String searchName) {
QCredential qCredential = QCredential.credential;
JPAQueryFactory jpaQueryFactory = new JPAQueryFactory(entityManager);
QInflowControl qInflowControl = QInflowControl.inflowControl;
QClientEntity qClientEntity = QClientEntity.clientEntity;
// EMSADM: Credential 목록 및 건수 조회
JPAQueryFactory emsFactory = new JPAQueryFactory(entityManagerForEMS);
BooleanBuilder credPredicate = new BooleanBuilder();
if (!StringUtils.isEmpty(searchName)) {
credPredicate.and(qCredential.clientname.contains(searchName));
}
long totalCount = emsFactory
.select(qCredential.clientid.count())
.from(qCredential)
.where(credPredicate)
.fetchOne();
List<Tuple> credList = emsFactory
.select(qCredential.clientid, qCredential.clientname)
.from(qCredential)
.where(credPredicate)
.orderBy(qCredential.clientname.asc())
List<Tuple> tupleList = jpaQueryFactory
.select(qInflowControl, qClientEntity.clientid, qClientEntity.clientname)
.from(qClientEntity)
.leftJoin(qInflowControl)
.on(qInflowControl.id.type.eq(CLIENT_TYPE_INFLOW)
.and(qInflowControl.id.name.eq(qClientEntity.clientid)))
.where(qClientEntity.clientname.contains(searchName))
.orderBy(qClientEntity.clientname.asc())
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetch();
if (credList.isEmpty()) {
return new PageImpl<>(Collections.emptyList(), pageable, totalCount);
}
// AGWADM: 해당 clientId의 InflowControl 일괄 조회
List<String> clientIds = credList.stream()
.map(t -> t.get(qCredential.clientid))
.collect(Collectors.toList());
Map<String, InflowControl> inflowMap = new JPAQueryFactory(entityManager)
.selectFrom(qInflowControl)
.where(qInflowControl.id.type.eq(CLIENT_TYPE_INFLOW)
.and(qInflowControl.id.name.in(clientIds)))
.fetch()
List<InflowControlServiceDto> dtoList = tupleList
.stream()
.collect(Collectors.toMap(ic -> ic.getId().getName(), ic -> ic));
// Java에서 병합
List<InflowControlServiceDto> dtoList = credList.stream()
.map(tuple -> {
String clientId = tuple.get(qCredential.clientid);
InflowControl inflowControl = inflowMap.get(clientId);
InflowControlServiceDto dto;
if (inflowControl != null) {
dto = mapper.toDto(inflowControl);
} else {
dto = new InflowControlServiceDto();
dto.setName(clientId);
dto.setType(CLIENT_TYPE_INFLOW);
}
dto.setDesc(tuple.get(qCredential.clientname));
return dto;
})
.map(tuple -> toDto(qClientEntity, qInflowControl, tuple))
.collect(Collectors.toList());
long totalCount = jpaQueryFactory
.select(qClientEntity.clientname.count())
.from(qClientEntity)
.where(qClientEntity.clientname.contains(searchName))
.fetchOne();
return new PageImpl<>(dtoList, pageable, totalCount);
}
private InflowControlServiceDto toDto(QClientEntity qClientEntity, QInflowControl qInflowControl,
Tuple tuple) {
InflowControlServiceDto dto = null;
if (tuple.get(qInflowControl) != null)
dto = mapper.toDto(tuple.get(qInflowControl));
else {
dto = new InflowControlServiceDto();
dto.setName(tuple.get(qClientEntity.clientid));
dto.setType(CLIENT_TYPE_INFLOW);
}
dto.setDesc(tuple.get(qClientEntity.clientname));
return dto;
}
public Page<Tuple> selectLogList(Pageable pageable, InflowControlHistoryManUISearch uiSearch) {
QInflowControlLog qlog = QInflowControlLog.inflowControlLog;
QInflowControlGroup qgroup = QInflowControlGroup.inflowControlGroup;
@@ -120,11 +120,11 @@ public class ApiStatsDayService
q.seq900BizErrCnt.sum().as("seq900BizErrCnt"),
q.minRespTime.min().castToNum(BigDecimal.class).as("minRespTime"),
q.maxRespTime.max().castToNum(BigDecimal.class).as("maxRespTime"),
// 가중 평균: SUM(avg * successCnt) / SUM(successCnt)
// 가중 평균: SUM(avg * totalCnt) / SUM(totalCnt)
Expressions.cases()
.when(q.successCnt.sum().gt(0))
.then(q.avgRespTime.multiply(q.successCnt).sum()
.divide(q.successCnt.sum()).castToNum(BigDecimal.class))
.when(q.totalCnt.sum().gt(0))
.then(q.avgRespTime.multiply(q.totalCnt).sum()
.divide(q.totalCnt.sum()).castToNum(BigDecimal.class))
.otherwise((BigDecimal) null)
.as("avgRespTime")))
.from(q)
@@ -211,11 +211,10 @@ public class ApiStatsHourService
q.seq900BizErrCnt.sum().as("seq900BizErrCnt"),
q.minRespTime.min().castToNum(BigDecimal.class).as("minRespTime"),
q.maxRespTime.max().castToNum(BigDecimal.class).as("maxRespTime"),
// 가중 평균: SUM(avg * successCnt) / SUM(successCnt)
Expressions.cases()
.when(q.successCnt.sum().gt(0))
.then(q.avgRespTime.multiply(q.successCnt).sum()
.divide(q.successCnt.sum()).castToNum(BigDecimal.class))
.when(q.totalCnt.sum().gt(0))
.then(q.avgRespTime.multiply(q.totalCnt).sum()
.divide(q.totalCnt.sum()).castToNum(BigDecimal.class))
.otherwise((BigDecimal) null)
.as("avgRespTime")))
.from(q)
@@ -293,9 +292,9 @@ public class ApiStatsHourService
q.minRespTime.min().castToNum(BigDecimal.class).as("minRespTime"),
q.maxRespTime.max().castToNum(BigDecimal.class).as("maxRespTime"),
Expressions.cases()
.when(q.successCnt.sum().gt(0))
.then(q.avgRespTime.multiply(q.successCnt).sum()
.divide(q.successCnt.sum()).castToNum(BigDecimal.class))
.when(q.totalCnt.sum().gt(0))
.then(q.avgRespTime.multiply(q.totalCnt).sum()
.divide(q.totalCnt.sum()).castToNum(BigDecimal.class))
.otherwise((BigDecimal) null)
.as("avgRespTime")))
.from(q)
@@ -214,11 +214,11 @@ public class ApiStatsMinuteService
q.seq900BizErrCnt.sum().as("seq900BizErrCnt"),
q.minRespTime.min().castToNum(BigDecimal.class).as("minRespTime"),
q.maxRespTime.max().castToNum(BigDecimal.class).as("maxRespTime"),
// 가중 평균: SUM(avg * successCnt) / SUM(successCnt)
// 가중 평균: SUM(avg * totalCnt) / SUM(totalCnt)
Expressions.cases()
.when(q.successCnt.sum().gt(0))
.then(q.avgRespTime.multiply(q.successCnt).sum()
.divide(q.successCnt.sum()).castToNum(BigDecimal.class))
.when(q.totalCnt.sum().gt(0))
.then(q.avgRespTime.multiply(q.totalCnt).sum()
.divide(q.totalCnt.sum()).castToNum(BigDecimal.class))
.otherwise((BigDecimal) null)
.as("avgRespTime")))
.from(q)
@@ -120,11 +120,11 @@ public class ApiStatsMonthService
q.seq900BizErrCnt.sum().as("seq900BizErrCnt"),
q.minRespTime.min().castToNum(BigDecimal.class).as("minRespTime"),
q.maxRespTime.max().castToNum(BigDecimal.class).as("maxRespTime"),
// 가중 평균: SUM(avg * successCnt) / SUM(successCnt)
// 가중 평균: SUM(avg * totalCnt) / SUM(totalCnt)
Expressions.cases()
.when(q.successCnt.sum().gt(0))
.then(q.avgRespTime.multiply(q.successCnt).sum()
.divide(q.successCnt.sum()).castToNum(BigDecimal.class))
.when(q.totalCnt.sum().gt(0))
.then(q.avgRespTime.multiply(q.totalCnt).sum()
.divide(q.totalCnt.sum()).castToNum(BigDecimal.class))
.otherwise((BigDecimal) null)
.as("avgRespTime")))
.from(q)
@@ -172,9 +172,9 @@ public class ApiStatsMonthService
q.minRespTime.min().castToNum(BigDecimal.class).as("minRespTime"),
q.maxRespTime.max().castToNum(BigDecimal.class).as("maxRespTime"),
Expressions.cases()
.when(q.successCnt.sum().gt(0))
.then(q.avgRespTime.multiply(q.successCnt).sum()
.divide(q.successCnt.sum()).castToNum(BigDecimal.class))
.when(q.totalCnt.sum().gt(0))
.then(q.avgRespTime.multiply(q.totalCnt).sum()
.divide(q.totalCnt.sum()).castToNum(BigDecimal.class))
.otherwise((BigDecimal) null)
.as("avgRespTime")))
.from(q)
@@ -117,11 +117,11 @@ public class ApiStatsYearService
q.seq900BizErrCnt.sum().as("seq900BizErrCnt"),
q.minRespTime.min().castToNum(BigDecimal.class).as("minRespTime"),
q.maxRespTime.max().castToNum(BigDecimal.class).as("maxRespTime"),
// 가중 평균: SUM(avg * successCnt) / SUM(successCnt)
// 가중 평균: SUM(avg * totalCnt) / SUM(totalCnt)
Expressions.cases()
.when(q.successCnt.sum().gt(0))
.then(q.avgRespTime.multiply(q.successCnt).sum()
.divide(q.successCnt.sum()).castToNum(BigDecimal.class))
.when(q.totalCnt.sum().gt(0))
.then(q.avgRespTime.multiply(q.totalCnt).sum()
.divide(q.totalCnt.sum()).castToNum(BigDecimal.class))
.otherwise((BigDecimal) null)
.as("avgRespTime")))
.from(q)
@@ -169,9 +169,9 @@ public class ApiStatsYearService
q.minRespTime.min().castToNum(BigDecimal.class).as("minRespTime"),
q.maxRespTime.max().castToNum(BigDecimal.class).as("maxRespTime"),
Expressions.cases()
.when(q.successCnt.sum().gt(0))
.then(q.avgRespTime.multiply(q.successCnt).sum()
.divide(q.successCnt.sum()).castToNum(BigDecimal.class))
.when(q.totalCnt.sum().gt(0))
.then(q.avgRespTime.multiply(q.totalCnt).sum()
.divide(q.totalCnt.sum()).castToNum(BigDecimal.class))
.otherwise((BigDecimal) null)
.as("avgRespTime")))
.from(q)
@@ -5,7 +5,9 @@ import java.util.List;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -42,7 +44,12 @@ public class ExtendedColumnDefinitionService
predicate = predicate.and(qExtendedColumnDefinition.columnDesc.containsIgnoreCase(searchColumnDesc));
}
return repository.findAll(predicate, pageable);
Pageable orderedPageable = PageRequest.of(
pageable.getPageNumber(),
pageable.getPageSize(),
Sort.by(Sort.Order.asc("orderSeq"), Sort.Order.asc("columnName")));
return repository.findAll(predicate, orderedPageable);
}
}
@@ -1,69 +0,0 @@
package com.eactive.eai.rms.data.ext.djb.apistatus;
import java.util.HashMap;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.eactive.eai.rms.ext.djb.event.ApiStatusChangedEvent;
@Service
@Transactional
public class ApiStatusService {
private static final Logger log = LoggerFactory.getLogger(ApiStatusService.class);
@Autowired
private ApiStatusRepository apiStatusRepository;
@Autowired
private ApplicationEventPublisher eventPublisher;
public void updateApiStatus(HashMap<String, String> param) {
List<ApiStatusEvent> list = apiStatusRepository.findApiStatusEvents(
Integer.parseInt(param.get("errorRate")),
Integer.parseInt(param.get("errorRangeMinute")),
Integer.parseInt(param.get("delayRangeMinute")),
Integer.parseInt(param.get("delayAvgRespTime"))
);
for (ApiStatusEvent event : list) {
String newStatusCode = resolveStatusCode(event.getEvent());
if (newStatusCode == null) continue;
ApiStatus apiStatus = apiStatusRepository.findById(event.getEaisvcname()).orElse(new ApiStatus());
apiStatus.setEaisvcname(event.getEaisvcname());
apiStatus.setStatusCode(newStatusCode);
apiStatusRepository.save(apiStatus); // PK 있으면 UPDATE, 없으면 INSERT
log.debug("API 상태 변경: {}-{} {} → {}", event.getEaisvcname(), event.getEaisvcdesc(), event.getEvent(), newStatusCode);
eventPublisher.publishEvent(ApiStatusChangedEvent.from(event)); // 트랜잭션 커밋 후 리스너 실행
log.debug("이벤트 전파");
}
}
private String resolveStatusCode(String event) {
switch (event) {
case "CONTROL_START": return "C"; //점검
case "CONTROL_END": return "N"; //정상
case "ERROR_START": return "E"; //장애
case "ERROR_END": return "N"; //정상
case "DELAY_START": return "D"; //지연
case "DELAY_END": return "N"; //정상
default:
log.warn("알 수 없는 이벤트: {}", event);
return null;
}
}
}
@@ -0,0 +1,41 @@
package com.eactive.eai.rms.ext.djb.apistatus;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@Transactional
public class ApiStatusDetectionService {
private static final Logger log = LoggerFactory.getLogger(ApiStatusDetectionService.class);
/**
* 이벤트 감지
* @param event "CONTROL_START" : 점검시작
"CONTROL_END" : 점검종료
"ERROR_START" : 장애시작
"ERROR_END" : 장애종료
"DELAY_START" : 지연시작
"DELAY_END" : 지연종료
* @param apiIds API IDs
*/
public void detect(String event, List<String> apiIds) {
log.debug("이벤트 감지: {} {}", event, apiIds);
}
// API FSM 내 아직 장애로 남아있는 API 목록
public void remainDownApiIds() {
}
// 정상 동작 - detect()에서 일괄 처리 (복구 = 점검종료, 장애종료, 지연종료)
// public void recovered(String event, String[] apiIds) {
//
// }
}
@@ -1,10 +1,12 @@
package com.eactive.eai.rms.data.ext.djb.apistatus;
package com.eactive.eai.rms.ext.djb.apistatus;
import java.util.List;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import com.eactive.eai.data.jpa.BaseRepository;
import com.eactive.eai.rms.data.entity.onl.djb.apistatus.ApiStatus;
import com.eactive.eai.rms.data.entity.onl.djb.apistatus.ApiStatusEvent;
public interface ApiStatusRepository extends BaseRepository<ApiStatus, String> {
@@ -22,9 +24,11 @@ public interface ApiStatusRepository extends BaseRepository<ApiStatus, String> {
@Query(nativeQuery = true, value =
" SELECT EAISVCNAME"
+ " , (SELECT EAISVCDESC FROM TSEAIHE01 WHERE A.EAISVCNAME = EAISVCNAME) AS EAISVCDESC"
+ " , PREV_STATUS_CODE "
+ " , EVENT"
+ " FROM ("
+ " SELECT EAISVCNAME"
+ " , STATUS_CODE AS PREV_STATUS_CODE "
+ " , CASE WHEN STATUS_CODE != 'C' AND CTRL_YN = 'Y' THEN 'CONTROL_START'"
+ " WHEN STATUS_CODE = 'C' AND CTRL_YN = 'N' THEN 'CONTROL_END'"
+ " WHEN STATUS_CODE IN ('N','D') AND ERR_YN = 'Y' THEN 'ERROR_START'"
@@ -34,14 +38,14 @@ public interface ApiStatusRepository extends BaseRepository<ApiStatus, String> {
+ " ELSE 'STAY' END AS EVENT"
+ " FROM ("
+ " SELECT A.EAISVCNAME"
+ " , NVL(B.STATUS_CODE, '1') AS STATUS_CODE"
+ " , NVL((SELECT 'Y' FROM TSEAITI01"
+ " WHERE (TO_CHAR(SYSDATE,'HH24MI') BETWEEN SUBSTR(EAICTRLDSTICCTNT,16,4) AND SUBSTR(EAICTRLDSTICCTNT,21,4)"
+ " OR SUBSTR(EAICTRLDSTICCTNT,16,9) = '0000|0000')"
+ " AND A.EAISVCNAME = EAICTRLNAME),'N') AS CTRL_YN"
+ " , NVL(B.STATUS_CODE, 'N') AS STATUS_CODE"
+ " , NVL(( SELECT MAX('Y') FROM EMSAPP.DJB_APISTATUS_INCIDENT_API X"
+ " WHERE EXISTS (SELECT 1 FROM EMSAPP.DJB_APISTATUS_INCIDENT Y WHERE X.INCIDENT_ID = Y.INCIDENT_ID "
+ " AND SYSTIMESTAMP BETWEEN STARTED_AT AND END_AT) "
+ " AND A.EAISVCNAME = X.API_ID),'N') AS CTRL_YN "
+ " , (SELECT CASE WHEN TOTAL = 0 THEN 'X'"
+ " WHEN (TOTAL - SUCCESS) * 100 / TOTAL > :errorRate THEN 'Y'"
+ " ELSE 'N' END"
+ " WHEN (TOTAL - SUCCESS) * 100 / TOTAL > :errorRate THEN 'Y'"
+ " ELSE 'N' END"
+ " FROM (SELECT SUM(SUCCESS_CNT + TIMEOUT_CNT + SYSTEM_ERR_CNT + BIZ_ERR_CNT) AS TOTAL"
+ " , SUM(SUCCESS_CNT) AS SUCCESS"
+ " FROM API_STATS_MINUTE"
@@ -0,0 +1,133 @@
package com.eactive.eai.rms.ext.djb.apistatus;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.eactive.apim.portal.template.entity.MessageCode;
import com.eactive.eai.rms.data.entity.man.role.Role;
import com.eactive.eai.rms.data.entity.onl.djb.apistatus.ApiStatus;
import com.eactive.eai.rms.data.entity.onl.djb.apistatus.ApiStatusEvent;
import com.eactive.eai.rms.ext.djb.ums.UmsManager;
@Service
@Transactional
public class ApiStatusService {
private static final Logger log = LoggerFactory.getLogger(ApiStatusService.class);
@Autowired
private ApiStatusRepository apiStatusRepository;
@Autowired
private UmsManager ums;
@Autowired
private ApiStatusDetectionService apiStatusDectionService;
public void updateApiStatus(HashMap<String, String> param) {
List<ApiStatusEvent> list = apiStatusRepository.findApiStatusEvents(
Integer.parseInt(param.get("errorRate")),
Integer.parseInt(param.get("errorRangeMinute")),
Integer.parseInt(param.get("delayRangeMinute")),
Integer.parseInt(param.get("delayAvgRespTime"))
);
HashMap<String, List<String>> eventMap = new HashMap<>();
for (ApiStatusEvent event : list) {
String newStatusCode = resolveStatusCode(event.getEvent());
if (newStatusCode == null) continue;
ApiStatus apiStatus = apiStatusRepository.findById(event.getEaisvcname()).orElse(new ApiStatus());
apiStatus.setEaisvcname(event.getEaisvcname());
apiStatus.setStatusCode(newStatusCode);
apiStatusRepository.save(apiStatus); // PK 있으면 UPDATE, 없으면 INSERT
log.debug("API 상태 변경: {}-{} {} → {}", event.getEaisvcname(), event.getEaisvcdesc(), event.getEvent(), newStatusCode);
//이벤트별로 api분리 저장
List<String> apis = (List<String>)eventMap.get(event.getEvent());
if (apis == null) {
apis = new ArrayList<String>();
apis.add(event.getEaisvcname());
eventMap.put(event.getEvent(), apis);
} else {
apis.add(event.getEaisvcname());
}
}
//이벤트별로 ums, webhook을 발송한다
for (Map.Entry<String, List<String>> entry : eventMap.entrySet()) {
String event = entry.getKey();
List<String> apiIds = entry.getValue();
String message = getSwingChatMessage(event, apiIds);
ums.sendMessenger("api-monitor", MessageCode.API_STATUS_CHANGED, message);
//제휴사 웹훅 발송
ums.sendWebhook(event, apiIds);
//개발자포탈 API상태모니터링 화면을 위한 처리
apiStatusDectionService.detect(event, apiIds);
}
}
private String resolveStatusCode(String event) {
switch (event) {
case "CONTROL_START": return "C"; //점검
case "CONTROL_END": return "N"; //정상
case "ERROR_START": return "E"; //장애
case "ERROR_END": return "N"; //정상
case "DELAY_START": return "D"; //지연
case "DELAY_END": return "N"; //정상
default:
log.warn("알 수 없는 이벤트: {}", event);
return null;
}
}
private String getSwingChatMessage(String event, List<String> apiIds) {
String message = "";
switch (event) {
case "CONTROL_START":
message = "API 서비스 점검이 시작되었습니다"; //점검
break;
case "CONTROL_END":
message = "API 서비스 점검이 완료되었습니다"; //정상
break;
case "ERROR_START":
message = "API 서비스 장애가 발생하였습니다"; //장애
break;
case "ERROR_END":
message = "API 서비스 장애가 복구되었습니다"; //정상
break;
case "DELAY_START":
message = "API 서비스 지연이 발생하였습니다"; //지연
break;
case "DELAY_END":
message = "API 서비스 지연이 복구되었습니다"; //정상
break;
default:
message = "";
}
return message + "\n- " + String.join(",", apiIds);
}
}
@@ -1,25 +0,0 @@
package com.eactive.eai.rms.ext.djb.async;
import java.util.concurrent.Executor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean("asyncExecutor")
public Executor asyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(2);
executor.setMaxPoolSize(5);
executor.setQueueCapacity(20);
executor.setThreadNamePrefix("async-");
executor.initialize();
return executor;
}
}
@@ -1,40 +0,0 @@
package com.eactive.eai.rms.ext.djb.async;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import org.springframework.transaction.event.TransactionPhase;
import org.springframework.transaction.event.TransactionalEventListener;
import com.eactive.eai.rms.ext.djb.event.ApiStatusChangedEvent;
import com.eactive.eai.rms.ext.djb.event.InflowTokenInsufficientEvent;
import com.eactive.eai.rms.ext.djb.swing.service.SwingSendManager;
import com.eactive.eai.rms.ext.djb.webhook.service.WebhookSendManager;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Component
@RequiredArgsConstructor
public class AsyncEventListener {
private final WebhookSendManager webhookSendManager;
private final SwingSendManager swingSendManager;
@Async("asyncExecutor")
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void onApiStatusChanged(ApiStatusChangedEvent event) {
log.debug("API 상태 변경 이벤트 수신: {} {}", event.getEaisvcname(), event.getEvent());
webhookSendManager.send(event.getEaisvcname(), event.getEvent());
swingSendManager.send(null, 0, 0);
}
@Async("asyncExecutor")
@EventListener // 트랜잭션 write 없이 read만 하므로 TransactionalEventListener 불필요
public void onInflowTokenFail(InflowTokenInsufficientEvent event) {
log.debug("유량제어 토큰획득 실패 이벤트 수신: {} {}건", event.getEaisvcname(), event.getCnt());
swingSendManager.send(event.getEaisvcname(), event.getCnt(), event.getRangeMinute());
}
}
@@ -1,18 +0,0 @@
package com.eactive.eai.rms.ext.djb.event;
import com.eactive.eai.rms.data.ext.djb.apistatus.ApiStatusEvent;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@Getter
@RequiredArgsConstructor
public class ApiStatusChangedEvent {
private final String eaisvcname;
private final String eaisvcdesc;
private final String event;
public static ApiStatusChangedEvent from(ApiStatusEvent source) {
return new ApiStatusChangedEvent(source.getEaisvcname(), source.getEaisvcdesc(), source.getEvent());
}
}
@@ -1,19 +0,0 @@
package com.eactive.eai.rms.ext.djb.event;
import com.eactive.eai.rms.data.ext.djb.inflow.InflowTokenInsufficient;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@Getter
@RequiredArgsConstructor
public class InflowTokenInsufficientEvent {
private final String eaisvcname;
private final String eaisvcdesc;
private final long cnt;
private final long rangeMinute;
public static InflowTokenInsufficientEvent from(InflowTokenInsufficient summary, long rangeMinute) {
return new InflowTokenInsufficientEvent(summary.getEaisvcname(), summary.getEaisvcdesc(), summary.getCnt(), rangeMinute);
}
}
@@ -1,9 +1,12 @@
package com.eactive.eai.rms.data.ext.djb.inflow;
package com.eactive.eai.rms.ext.djb.inflow;
import java.util.List;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import com.eactive.eai.data.jpa.BaseRepository;
import com.eactive.eai.rms.data.entity.onl.djb.inflow.InflowTokenInsufficient;
import com.eactive.eai.rms.data.entity.onl.djb.inflow.InflowTokenInsufficientLog;
import com.eactive.eai.rms.data.entity.onl.djb.inflow.InflowTokenInsufficientLogId;
public interface InflowTokenRepository extends BaseRepository<InflowTokenInsufficientLog, InflowTokenInsufficientLogId> {
@@ -1,15 +1,17 @@
package com.eactive.eai.rms.data.ext.djb.inflow;
package com.eactive.eai.rms.ext.djb.inflow;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.eactive.eai.rms.ext.djb.event.InflowTokenInsufficientEvent;
import com.eactive.apim.portal.template.entity.MessageCode;
import com.eactive.eai.rms.data.entity.man.role.Role;
import com.eactive.eai.rms.data.entity.onl.djb.inflow.InflowTokenInsufficient;
import com.eactive.eai.rms.ext.djb.ums.UmsManager;
@Service
@@ -22,16 +24,16 @@ public class InflowTokenService {
private InflowTokenRepository inflowTokenRepository;
@Autowired
private ApplicationEventPublisher eventPublisher;
private UmsManager ums;
public void checkRecentFails(long rangeMinute) {
List<InflowTokenInsufficient> rows = inflowTokenRepository.countTokenInsufficient(rangeMinute);
for (InflowTokenInsufficient info : rows) {
log.debug("유량제어 토큰 획득 실패: {}-{} 최근 {}분 동안 {}건", info.getEaisvcname(), info.getEaisvcdesc(), rangeMinute, info.getCnt());
//내부직원 알림 발송
eventPublisher.publishEvent(InflowTokenInsufficientEvent.from(info, rangeMinute));
String message = "유량제어 토큰 획득 실패하였습니다 \n" + info.getEaisvcname();
ums.sendMessenger("api-monitor", MessageCode.INFLOW_TOKEN_FAILED, message);
}
}
}
@@ -0,0 +1,143 @@
package com.eactive.eai.rms.ext.djb.job;
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.quartz.SchedulerException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
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.kjb.statistics.ApiStatsHourService;
import com.eactive.eai.rms.data.entity.onl.kjb.statistics.QApiStatsHour;
/**
* API 로그 테이블을 시간별 통계로 집계하는 배치 작업
* TSEAILGXX → API_STATS_HOUR
* 실행 주기: 매시 00:10 (당일 데이터 집계)
*/
@Component
public class ApiStatsHourlyAggregationJob implements Job {
private static final Logger log = LoggerFactory.getLogger(ApiStatsHourlyAggregationJob.class);
@PersistenceContext
private EntityManager entityManager;
private ApiStatsHourService apiStatsHourService;
@Autowired
public void setApiStatsHourService(ApiStatsHourService apiStatsHourService) {
this.apiStatsHourService = apiStatsHourService;
}
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
ApplicationContext appContext = null;
final ApiStatsHourlyAggregationJob selfJob;
try {
appContext = (ApplicationContext) context.getScheduler().getContext().get("applicationContext");
selfJob = appContext.getBean(ApiStatsHourlyAggregationJob.class);
} catch (SchedulerException e) {
log.error("applicationContext get module error", e);
return;
}
DataSourceType dataType = DataSourceTypeManager.getDataSourceType(DataSourceTypeManager.APIGW);
DataSourceContextHolder.setDataSourceType(dataType);
try {
LocalDate targetDate = LocalDate.now();
if (LocalTime.now().isBefore(LocalTime.of(1, 0))) {
targetDate = targetDate.minusDays(1);
}
selfJob.executeManual(targetDate);
} catch (Exception e) {
throw new JobExecutionException(e);
} finally {
DataSourceContextHolder.clearDataSourceType();
}
}
/**
* 수동 실행 메서드 (날짜 지정)
*/
@Transactional
public int executeManual(LocalDate targetDate) {
long startTime = System.currentTimeMillis();
log.info("=== 시간별 통계 집계 작업 시작 === 대상: {}", targetDate);
String searchDate = targetDate.format(DateTimeFormatter.ofPattern("yyyyMMdd"));
String logTableName = CommonUtil.getLogTable(searchDate, true);
// 1. 기존 데이터 삭제
QApiStatsHour q = QApiStatsHour.apiStatsHour;
long deletedCount = apiStatsHourService.getJPAQueryFactory()
.delete(q)
.where(q.statTime.goe(targetDate.atStartOfDay())
.and(q.statTime.lt(targetDate.plusDays(1).atStartOfDay())))
.execute();
log.info("기존 데이터 삭제: {} 건", deletedCount);
// 2. 집계 및 데이터 입력
/**
* EAISVCSERNO로 그룹핑하여 1행으로 집계를 한 다음 (한거래 안에서 adapter가 여러개 있을 경우 seq=100의 adapter를 기준)
* 총건수 : EAISVCSERNO로 그룹핑한 총 건수
* 성공건수 : seq = 400 and EAIERRCD = null 인 건수
* Timeout : EAIERRCD in (공통코드 CODEGROUP = 'ERRCODE_TIMEOUT') 인 건수
* 시스템오류 : (seq400 = null or EAIERRCD is not null) and 공통코드(ERRCODE_TIMEOUT)에 정의되지 않은 errorcode 인 건수
*/
String sql =
"INSERT INTO API_STATS_HOUR" +
" (STAT_TIME, API_NAME, GW_INSTANCE_ID, BIZ_DIV_CODE, CLIENT_ID" +
" , INBOUND_ADAPTER, OUTBOUND_ADAPTER" +
", TOTAL_CNT, SUCCESS_CNT, TIMEOUT_CNT, SYSTEM_ERR_CNT, BIZ_ERR_CNT" +
", SEQ900_TIMEOUT_CNT, SEQ900_SYSTEM_ERR_CNT, SEQ900_BIZ_ERR_CNT" +
", AVG_RESP_TIME, MIN_RESP_TIME, MAX_RESP_TIME, P50_RESP_TIME, P95_RESP_TIME)" +
" SELECT TO_TIMESTAMP(SUBSTR(DT,1,10) || '0000', 'YYYYMMDDHH24MISS')" +
" , API_NAME, GW_INSTANCE_ID, BIZ_DIV_CODE, NVL(A.CLIENT_ID, 'NONE')" +
" , INBOUND_ADAPTER, OUTBOUND_ADAPTER" +
" , COUNT(EAISVCSERNO)" +
" , SUM(CASE WHEN A.ERROR_CODE IS NULL AND E400 IS NOT NULL THEN 1 ELSE 0 END)" +
" , SUM(CASE WHEN B.CODE IS NOT NULL THEN 1 ELSE 0 END)" +
" , SUM(CASE WHEN (A.E400 IS NULL OR A.ERROR_CODE IS NOT NULL) AND B.CODE IS NULL THEN 1 ELSE 0 END)" +
" , 0, 0, 0, 0" +
" , TRUNC(AVG(RESP_TIME)), MIN(RESP_TIME), MAX(RESP_TIME), 0, 0" +
" FROM (" +
" SELECT EAISVCSERNO" +
" , MAX(EAISVCNAME) AS API_NAME, MAX(EAISEVRINSTNCNAME) AS GW_INSTANCE_ID" +
" , MAX(EAIBZWKDSTCD) AS BIZ_DIV_CODE, MAX(CLIENTID) AS CLIENT_ID" +
" , MAX(CASE WHEN LOGPRCSSSERNO = '100' THEN GSTATSYSADPTRBZWKGROUPNAME ELSE '' END) AS INBOUND_ADAPTER" +
" , MAX(CASE WHEN LOGPRCSSSERNO = '100' THEN PSVSYSADPTRBZWKGROUPNAME ELSE '' END) AS OUTBOUND_ADAPTER" +
" , MIN(MSGDPSTYMS) AS DT" +
" , MAX(MSGPRCSSYMS) - MIN(MSGDPSTYMS) AS RESP_TIME" +
" , MAX(CASE WHEN LOGPRCSSSERNO = '400' THEN LOGPRCSSSERNO ELSE '' END) AS E400" +
" , MAX(EAIERRCD) AS ERROR_CODE" +
" FROM " + logTableName +
" WHERE MSGDPSTYMS LIKE :searchDate || '%'" +
" GROUP BY EAISVCSERNO" +
" ) A LEFT OUTER JOIN TSEAICM20 B ON A.ERROR_CODE = B.CODE AND B.CODEGROUP = 'ERRCODE_TIMEOUT' AND B.USEYN = 'Y' " +
" GROUP BY SUBSTR(DT,1,10), API_NAME, GW_INSTANCE_ID, BIZ_DIV_CODE, NVL(A.CLIENT_ID, 'NONE')" +
" , INBOUND_ADAPTER, OUTBOUND_ADAPTER";
int savedCount = entityManager.createNativeQuery(sql)
.setParameter("searchDate", searchDate)
.executeUpdate();
long elapsedTime = System.currentTimeMillis() - startTime;
log.info("=== 집계 완료 === (처리 건수: {}, 소요 시간: {}ms)", savedCount, elapsedTime);
return savedCount;
}
}
@@ -17,7 +17,7 @@ import com.eactive.eai.rms.common.context.MonitoringContext;
import com.eactive.eai.rms.common.datasource.DataSourceContextHolder;
import com.eactive.eai.rms.common.datasource.DataSourceTypeManager;
import com.eactive.eai.rms.common.util.CommonUtil;
import com.eactive.eai.rms.data.ext.djb.apistatus.ApiStatusService;
import com.eactive.eai.rms.ext.djb.apistatus.ApiStatusService;
import com.eactive.eai.rms.onl.common.util.DateUtil;
/**
@@ -88,11 +88,11 @@ public class ApiStatusMonitorJob implements Job {
monitoringContext = (MonitoringContext) appContext.getBean("monitoringContext");
ApiStatusService apiStatusUpdateService = appContext.getBean(ApiStatusService.class);
DataSourceContextHolder.setDataSourceType(
DataSourceTypeManager.getDataSourceType(DataSourceTypeManager.APIGW));
try {
apiStatusUpdateService.updateApiStatus(param);
} catch (Exception e) {
log.error("ApiStatusUpdateJob execution failed", e);
throw new JobExecutionException(e);
@@ -18,7 +18,7 @@ import com.eactive.eai.rms.common.context.MonitoringContext;
import com.eactive.eai.rms.common.datasource.DataSourceContextHolder;
import com.eactive.eai.rms.common.datasource.DataSourceTypeManager;
import com.eactive.eai.rms.common.util.CommonUtil;
import com.eactive.eai.rms.data.ext.djb.inflow.InflowTokenService;
import com.eactive.eai.rms.ext.djb.inflow.InflowTokenService;
import com.eactive.eai.rms.onl.common.util.DateUtil;
/**
@@ -0,0 +1,73 @@
package com.eactive.eai.rms.ext.djb.job;
import com.eactive.eai.rms.common.datasource.DataSourceContextHolder;
import com.eactive.eai.rms.common.datasource.DataSourceTypeManager;
import com.eactive.eai.rms.common.util.CommonUtil;
import com.eactive.eai.rms.onl.apim.portalInquiry.PortalInquiryClosingService;
import com.eactive.eai.rms.onl.common.util.DateUtil;
import org.quartz.DisallowConcurrentExecution;
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
/**
* 매일 00:05 실행 - RESPONDED 상태 문의글 자동 종료 배치
*
* <p>종료 조건 (기본 7일):</p>
* <ul>
* <li>답변완료 후 댓글이 7일간 없는 경우</li>
* <li>관리자 댓글 등록 후 7일간 포탈사용자 댓글이 없는 경우</li>
* </ul>
*
* <p>Cron: 0 5 0 * * ?</p>
*
* <p>Job 파라미터 (JobDataMap):</p>
* <ul>
* <li>{@value #KEY_INQUIRY_COMMENT_CLOSING_DAY}: 종료 기준 일수 (기본값: 7)</li>
* </ul>
*/
@DisallowConcurrentExecution
public class PortalInquiryClosingJob implements Job {
private static final Logger log = LoggerFactory.getLogger(PortalInquiryClosingJob.class);
/** Job 파라미터 키: 댓글종료 기간(일) */
public static final String KEY_INQUIRY_COMMENT_CLOSING_DAY = "inquiry.comment.closing_day";
private static final int DEFAULT_CLOSING_DAYS = 7;
@Autowired
private PortalInquiryClosingService inquiryCommentClosingService;
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
log.info("*** START PortalInquiryCommentClosingService run({})", CommonUtil.getToday("yyyy-MM-dd HH:mm"));
int closingDays = DEFAULT_CLOSING_DAYS;
String closingDayParam = context.getMergedJobDataMap().getString(KEY_INQUIRY_COMMENT_CLOSING_DAY);
if (closingDayParam != null) {
try {
closingDays = Integer.parseInt(closingDayParam);
} catch (NumberFormatException e) {
log.warn("잘못된 {} 파라미터 값: '{}', 기본값 {}일 사용", KEY_INQUIRY_COMMENT_CLOSING_DAY, closingDayParam, DEFAULT_CLOSING_DAYS);
}
}
DataSourceContextHolder.setDataSourceType(
DataSourceTypeManager.getDataSourceType(DataSourceTypeManager.APIGW));
try {
int closedCount = inquiryCommentClosingService.closeExpiredInquiries(closingDays);
log.info("PortalInquiryCommentClosingService 완료: {}건 CLOSED 처리", closedCount);
} catch (Exception e) {
log.error("PortalInquiryCommentClosingService execution failed", e);
throw new JobExecutionException(e);
} finally {
DataSourceContextHolder.clearDataSourceType();
}
log.info("*** END PortalInquiryCommentClosingService run({})", DateUtil.getDateTime("yyyy-MM-dd HH:mm"));
}
}
@@ -1,29 +1,20 @@
package com.eactive.eai.rms.ext.djb.statistics;
import java.io.IOException;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.stream.Collectors;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import com.eactive.eai.rms.common.vo.GridResponse;
import com.eactive.eai.rms.data.entity.onl.kjb.statistics.ApiStatsDay;
import com.eactive.eai.rms.data.entity.onl.kjb.statistics.ApiStatsDayService;
import com.eactive.ext.kjb.statistics.mapping.ApiStatsUIMapper;
import com.eactive.ext.kjb.statistics.service.ApiStatsExcelExportService;
import com.eactive.ext.kjb.statistics.ui.ApiStatsChartUI;
import com.eactive.ext.kjb.statistics.ui.ApiStatsSearch;
import com.eactive.ext.kjb.statistics.ui.ApiStatsUI;
@@ -35,37 +26,29 @@ import lombok.extern.slf4j.Slf4j;
@RequiredArgsConstructor
public class ApiUseStatsController {
private final ApiStatsDayService service;
private final ApiStatsUIMapper mapper;
private final ApiStatsExcelExportService excelExportService;
private final ApiUseStatsService service;
private final ApiUseStatsExcelExportService excelExportService;
@GetMapping(value = "/onl/djb/statistics/apiUseStatsMan.view")
@GetMapping(value = "/onl/kjb/statistics/apiUseStatsMan.view")
public String view() {
return "/onl/djb/statistics/apiUseStatsMan";
return "/onl/kjb/statistics/apiUseStatsMan";
}
@PostMapping(value = "/onl/djb/statistics/apiUseStatsMan.json", params = "cmd=LIST")
@PostMapping(value = "/onl/kjb/statistics/apiUseStatsMan.json", params = "cmd=LIST")
public ResponseEntity<GridResponse<ApiStatsUI>> selectList(ApiStatsSearch search, Pageable pageable) {
Pageable sortedPageable = PageRequest.of(
pageable.getPageNumber(),
pageable.getPageSize(),
Sort.by(Sort.Order.desc("statTime")));
Page<ApiStatsDay> page = service.selectList(search, sortedPageable);
Page<ApiStatsUI> uiPage = page.map(mapper::toVo);
return ResponseEntity.ok(new GridResponse<>(uiPage));
Page<ApiStatsUI> page = service.selectList(search, pageable);
return ResponseEntity.ok(new GridResponse<>(page));
}
@PostMapping(value = "/onl/djb/statistics/apiStatsDayMan.json", params = "cmd=EXCEL_EXPORT")
@PostMapping(value = "/onl/kjb/statistics/apiUseStatsMan.json", params = "cmd=EXCEL_EXPORT")
public void exportToExcel(ApiStatsSearch search, HttpServletResponse response) throws IOException {
log.info("Excel export started - search: {}", search);
try {
List<ApiStatsDay> dataList = service.selectListForExcel(search);
log.info("Data retrieved - count: {}", dataList.size());
List<ApiStatsUI> uiList = service.selectList(search);
log.info("Data retrieved - count: {}", uiList.size());
if (dataList.isEmpty()) {
if (uiList.isEmpty()) {
log.warn("No data found for export");
response.setStatus(HttpServletResponse.SC_NO_CONTENT);
response.setContentType("application/json; charset=UTF-8");
@@ -74,11 +57,6 @@ public class ApiUseStatsController {
return;
}
List<ApiStatsUI> uiList = dataList.stream()
.map(mapper::toVo)
.collect(Collectors.toList());
log.info("Data converted to UI list - count: {}", uiList.size());
String fileName = generateFileName(search);
log.info("Generating Excel file: {}", fileName);
@@ -101,10 +79,7 @@ public class ApiUseStatsController {
}
private String generateFileName(ApiStatsSearch search) {
String timeRange = StringUtils.isNotBlank(search.getSearchStartDateTime())
? search.getSearchStartDateTime()
: LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
return "API사용현황_" + timeRange + ".xlsx";
return "API사용현황_" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss")) + ".xlsx";
}
}
@@ -0,0 +1,256 @@
package com.eactive.eai.rms.ext.djb.statistics;
import java.io.IOException;
import java.math.BigDecimal;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.List;
import javax.servlet.http.HttpServletResponse;
import org.apache.poi.ss.usermodel.BorderStyle;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellStyle;
import org.apache.poi.ss.usermodel.FillPatternType;
import org.apache.poi.ss.usermodel.Font;
import org.apache.poi.ss.usermodel.HorizontalAlignment;
import org.apache.poi.ss.usermodel.IndexedColors;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.springframework.stereotype.Service;
import com.eactive.ext.kjb.statistics.ui.ApiStatsUI;
import lombok.extern.slf4j.Slf4j;
/**
* API 통계 Excel Export 서비스
* Hour/Minute 통계 데이터를 Excel 파일로 생성
*/
@Slf4j
@Service
public class ApiUseStatsExcelExportService {
private static final String[] COLUMN_HEADERS = {
"구분", "총건수", "성공", "성공율(%)", "실패율(%)", "Timeout", "시스템오류",
"평균응답(ms)", "최소응답(ms)", "최대응답(ms)"
};
/**
* API 통계 데이터를 Excel 파일로 생성하여 HTTP 응답으로 전송
*
* @param dataList API 통계 데이터 리스트
* @param fileName 다운로드 파일명
* @param response HTTP 응답 객체
* @throws IOException 파일 생성 실패 시
*/
public void exportApiStats(List<ApiStatsUI> dataList, String fileName, HttpServletResponse response)
throws IOException {
log.info("Starting Excel export - rows: {}, filename: {}", dataList.size(), fileName);
try (Workbook workbook = new XSSFWorkbook()) {
Sheet sheet = workbook.createSheet("API통계");
log.debug("Excel sheet created");
// 스타일 생성
CellStyle headerStyle = createHeaderStyle(workbook);
CellStyle stringStyle = createStringStyle(workbook);
CellStyle numberStyle = createNumberStyle(workbook);
CellStyle decimalStyle = createDecimalStyle(workbook);
log.debug("Excel styles created");
// 헤더 행 생성
createHeaderRow(sheet, headerStyle);
log.debug("Header row created");
// 데이터 행 생성
int rowNum = 1;
for (ApiStatsUI data : dataList) {
createDataRow(sheet, rowNum++, data, stringStyle, numberStyle, decimalStyle);
}
log.info("Data rows created - count: {}", dataList.size());
// 컬럼 너비 자동 조정
autoSizeColumns(sheet);
log.debug("Column widths adjusted");
// HTTP 응답 설정
setHttpResponse(response, fileName, workbook);
log.info("Excel file sent to response");
} catch (Exception e) {
log.error("Error creating Excel file", e);
throw new IOException("Excel file creation failed: " + e.getMessage(), e);
}
}
/**
* 헤더 스타일 생성 (회색 배경, 볼드, 테두리)
*/
private CellStyle createHeaderStyle(Workbook workbook) {
CellStyle style = workbook.createCellStyle();
// 배경색
style.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex());
style.setFillPattern(FillPatternType.SOLID_FOREGROUND);
// 테두리
style.setBorderTop(BorderStyle.THIN);
style.setBorderBottom(BorderStyle.THIN);
style.setBorderLeft(BorderStyle.THIN);
style.setBorderRight(BorderStyle.THIN);
// 정렬
style.setAlignment(HorizontalAlignment.CENTER);
// 폰트 (볼드)
Font font = workbook.createFont();
font.setBold(true);
style.setFont(font);
return style;
}
/**
* 문자열 데이터 스타일 생성 (기본 테두리)
*/
private CellStyle createStringStyle(Workbook workbook) {
CellStyle style = workbook.createCellStyle();
style.setBorderTop(BorderStyle.THIN);
style.setBorderBottom(BorderStyle.THIN);
style.setBorderLeft(BorderStyle.THIN);
style.setBorderRight(BorderStyle.THIN);
return style;
}
/**
* 숫자 데이터 스타일 생성 (우측정렬 + 쉼표 구분)
*/
private CellStyle createNumberStyle(Workbook workbook) {
CellStyle style = workbook.createCellStyle();
style.setBorderTop(BorderStyle.THIN);
style.setBorderBottom(BorderStyle.THIN);
style.setBorderLeft(BorderStyle.THIN);
style.setBorderRight(BorderStyle.THIN);
style.setAlignment(HorizontalAlignment.RIGHT);
style.setDataFormat(workbook.createDataFormat().getFormat("#,##0"));
return style;
}
/**
* 소수점 데이터 스타일 생성 (우측정렬 + 소수점 2자리)
*/
private CellStyle createDecimalStyle(Workbook workbook) {
CellStyle style = workbook.createCellStyle();
style.setBorderTop(BorderStyle.THIN);
style.setBorderBottom(BorderStyle.THIN);
style.setBorderLeft(BorderStyle.THIN);
style.setBorderRight(BorderStyle.THIN);
style.setAlignment(HorizontalAlignment.RIGHT);
style.setDataFormat(workbook.createDataFormat().getFormat("0.00"));
return style;
}
/**
* 헤더 행 생성
*/
private void createHeaderRow(Sheet sheet, CellStyle headerStyle) {
Row headerRow = sheet.createRow(0);
for (int i = 0; i < COLUMN_HEADERS.length; i++) {
Cell cell = headerRow.createCell(i);
cell.setCellValue(COLUMN_HEADERS[i]);
cell.setCellStyle(headerStyle);
}
}
/**
* 데이터 행 생성
*/
private void createDataRow(Sheet sheet, int rowNum, ApiStatsUI data,
CellStyle stringStyle, CellStyle numberStyle, CellStyle decimalStyle) {
Row row = sheet.createRow(rowNum);
int colNum = 0;
createStringCell(row, colNum++, data.getOrgName(), stringStyle);
createLongCell(row, colNum++, data.getTotalCnt(), numberStyle);
createLongCell(row, colNum++, data.getSuccessCnt(), numberStyle);
createDecimalCell(row, colNum++, data.getSuccessRate(), decimalStyle);
createDecimalCell(row, colNum++, data.getFailRate(), decimalStyle);
createLongCell(row, colNum++, data.getTimeoutCnt(), numberStyle);
createLongCell(row, colNum++, data.getSystemErrCnt(), numberStyle);
createDecimalCell(row, colNum++, data.getAvgRespTime(), numberStyle);
createDecimalCell(row, colNum++, data.getMinRespTime(), numberStyle);
createDecimalCell(row, colNum++, data.getMaxRespTime(), numberStyle);
}
/**
* 문자열 셀 생성
*/
private void createStringCell(Row row, int colNum, String value, CellStyle style) {
Cell cell = row.createCell(colNum);
cell.setCellValue(value != null ? value : "");
cell.setCellStyle(style);
}
/**
* Long 타입 숫자 셀 생성
*/
private void createLongCell(Row row, int colNum, Long value, CellStyle style) {
Cell cell = row.createCell(colNum);
if (value != null) {
cell.setCellValue(value.doubleValue());
} else {
cell.setCellValue(0);
}
cell.setCellStyle(style);
}
/**
* BigDecimal 타입 소수점 셀 생성
*/
private void createDecimalCell(Row row, int colNum, BigDecimal value, CellStyle style) {
Cell cell = row.createCell(colNum);
if (value != null) {
cell.setCellValue(value.doubleValue());
} else {
cell.setCellValue(0.0);
}
cell.setCellStyle(style);
}
/**
* 컬럼 너비 자동 조정
*/
private void autoSizeColumns(Sheet sheet) {
for (int i = 0; i < COLUMN_HEADERS.length; i++) {
sheet.autoSizeColumn(i);
// 한글 문자 고려하여 약간 여유 공간 추가
sheet.setColumnWidth(i, sheet.getColumnWidth(i) + 512);
}
}
/**
* HTTP 응답 설정 및 파일 전송
*/
private void setHttpResponse(HttpServletResponse response, String fileName, Workbook workbook)
throws IOException {
// Content-Type 설정
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
// Content-Disposition 설정 (파일명 UTF-8 인코딩)
String encodedFileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8.toString())
.replaceAll("\\+", "%20");
response.setHeader("Content-Disposition",
"attachment; filename=\"" + encodedFileName + "\"; filename*=UTF-8''" + encodedFileName);
// Workbook을 응답 스트림으로 전송
workbook.write(response.getOutputStream());
response.getOutputStream().flush();
}
}
@@ -0,0 +1,12 @@
package com.eactive.eai.rms.ext.djb.statistics;
import com.eactive.eai.data.jpa.BaseRepository;
import com.eactive.eai.rms.data.entity.onl.kjb.statistics.ApiStatsDay;
import com.eactive.eai.rms.data.entity.onl.kjb.statistics.ApiStatsDayId;
/**
* API 사용현황 Repository
*/
interface ApiUseStatsRepository extends BaseRepository<ApiStatsDay, ApiStatsDayId> {
}
@@ -0,0 +1,209 @@
package com.eactive.eai.rms.ext.djb.statistics;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.stream.Collectors;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.Query;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.eactive.eai.data.jpa.AbstractDataService;
import com.eactive.eai.rms.common.util.StringUtils;
import com.eactive.eai.rms.data.entity.onl.kjb.statistics.ApiStatsDay;
import com.eactive.eai.rms.data.entity.onl.kjb.statistics.ApiStatsDayId;
import com.eactive.ext.kjb.statistics.ui.ApiStatsSearch;
import com.eactive.ext.kjb.statistics.ui.ApiStatsUI;
@Service
@Transactional
public class ApiUseStatsService
extends AbstractDataService<ApiStatsDay, ApiStatsDayId, ApiUseStatsRepository> {
private static final int MAX_DAYS = 31;
@PersistenceContext
private EntityManager entityManager;
@SuppressWarnings("unchecked")
public Page<ApiStatsUI> selectList(ApiStatsSearch search, Pageable pageable) {
Query dataQuery = getDataQuery(search);
dataQuery.setFirstResult((int) pageable.getOffset());
dataQuery.setMaxResults(pageable.getPageSize());
List<Object[]> rows = dataQuery.getResultList();
List<ApiStatsUI> results = rows.stream()
.map(this::toVO)
.collect(Collectors.toList());
long records = rows.size() > 0 ? StringUtils.toLong(rows.get(0)[0]) : 0;
return new PageImpl<>(results, pageable, records);
}
@SuppressWarnings("unchecked")
public List<ApiStatsUI> selectList(ApiStatsSearch search) {
Query dataQuery = getDataQuery(search);
List<Object[]> rows = dataQuery.getResultList();
return rows.stream()
.map(this::toVO)
.collect(Collectors.toList());
}
private Query getDataQuery(ApiStatsSearch search) {
StringBuilder where = buildNativeWhere(search);
String dataSql = "";
if ("ORG".equals(search.getSearchType())) {
dataSql = "SELECT COUNT(*) OVER () AS RECORDS "
+ ", B.ORGNAME"
+ ", SUM(A.TOTAL_CNT)"
+ ", SUM(A.SUCCESS_CNT)"
+ ", CASE WHEN SUM(A.TOTAL_CNT) > 0 THEN SUM(A.SUCCESS_CNT) * 100 / SUM(A.TOTAL_CNT) ELSE NULL END"
+ ", CASE WHEN SUM(A.TOTAL_CNT) > 0 THEN (SUM(A.TOTAL_CNT) - SUM(A.SUCCESS_CNT)) * 100 / SUM(A.TOTAL_CNT) ELSE NULL END"
+ ", SUM(A.TIMEOUT_CNT)"
+ ", SUM(A.SYSTEM_ERR_CNT)"
+ ", CASE WHEN SUM(A.TOTAL_CNT) > 0 THEN TRUNC(SUM(A.TOTAL_CNT * A.AVG_RESP_TIME) / SUM(A.TOTAL_CNT)) ELSE NULL END"
+ ", MIN(A.MIN_RESP_TIME)"
+ ", MAX(A.MAX_RESP_TIME)"
+ " FROM API_STATS_DAY A"
+ " LEFT OUTER JOIN TSEAIAU01 B ON A.CLIENT_ID = B.CLIENTID"
+ " LEFT OUTER JOIN TSEAIHE01 C ON A.API_NAME = C.EAISVCNAME "
+ where
+ " GROUP BY B.ORGNAME"
+ " ORDER BY ORGNAME";
} else if ("API".equals(search.getSearchType())) {
dataSql = "SELECT COUNT(*) OVER () AS RECORDS "
+ ", C.EAISVCDESC"
+ ", SUM(A.TOTAL_CNT)"
+ ", SUM(A.SUCCESS_CNT)"
+ ", CASE WHEN SUM(A.TOTAL_CNT) > 0 THEN SUM(A.SUCCESS_CNT) * 100 / SUM(A.TOTAL_CNT) ELSE NULL END"
+ ", CASE WHEN SUM(A.TOTAL_CNT) > 0 THEN (SUM(A.TOTAL_CNT) - SUM(A.SUCCESS_CNT)) * 100 / SUM(A.TOTAL_CNT) ELSE NULL END"
+ ", SUM(A.TIMEOUT_CNT)"
+ ", SUM(A.SYSTEM_ERR_CNT)"
+ ", CASE WHEN SUM(A.TOTAL_CNT) > 0 THEN TRUNC(SUM(A.TOTAL_CNT * A.AVG_RESP_TIME) / SUM(A.TOTAL_CNT)) ELSE NULL END"
+ ", MIN(A.MIN_RESP_TIME)"
+ ", MAX(A.MAX_RESP_TIME)"
+ " FROM API_STATS_DAY A "
+ " LEFT OUTER JOIN TSEAIAU01 B ON A.CLIENT_ID = B.CLIENTID"
+ " LEFT OUTER JOIN TSEAIHE01 C ON A.API_NAME = C.EAISVCNAME "
+ where
+ " GROUP BY C.EAISVCDESC"
+ " ORDER BY EAISVCDESC";
} else if ("DATE".equals(search.getSearchType())) {
dataSql = "SELECT COUNT(*) OVER () AS RECORDS "
+ ", TO_CHAR(STAT_TIME,'YYYY-MM-DD') AS STAT_TIME "
+ ", SUM(A.TOTAL_CNT)"
+ ", SUM(A.SUCCESS_CNT)"
+ ", CASE WHEN SUM(A.TOTAL_CNT) > 0 THEN SUM(A.SUCCESS_CNT) * 100 / SUM(A.TOTAL_CNT) ELSE NULL END"
+ ", CASE WHEN SUM(A.TOTAL_CNT) > 0 THEN (SUM(A.TOTAL_CNT) - SUM(A.SUCCESS_CNT)) * 100 / SUM(A.TOTAL_CNT) ELSE NULL END"
+ ", SUM(A.TIMEOUT_CNT)"
+ ", SUM(A.SYSTEM_ERR_CNT)"
+ ", CASE WHEN SUM(A.TOTAL_CNT) > 0 THEN TRUNC(SUM(A.TOTAL_CNT * A.AVG_RESP_TIME) / SUM(A.TOTAL_CNT)) ELSE NULL END"
+ ", MIN(A.MIN_RESP_TIME)"
+ ", MAX(A.MAX_RESP_TIME)"
+ " FROM API_STATS_DAY A "
+ " LEFT OUTER JOIN TSEAIAU01 B ON A.CLIENT_ID = B.CLIENTID"
+ " LEFT OUTER JOIN TSEAIHE01 C ON A.API_NAME = C.EAISVCNAME "
+ where
+ " GROUP BY TO_CHAR(STAT_TIME,'YYYY-MM-DD')"
+ " ORDER BY STAT_TIME";
}
Query dataQuery = entityManager.createNativeQuery(dataSql);
if (search.getParsedStartDate() != null) {
dataQuery.setParameter("searchStartDate", search.getParsedStartDate());
dataQuery.setParameter("searchEndDate", search.getParsedEndDate());
}
if (StringUtils.isNotBlank(search.getSearchOrgName())) {
dataQuery.setParameter("searchOrgName", "%" + search.getSearchOrgName().toUpperCase() + "%");
}
if (StringUtils.isNotBlank(search.getSearchApiName())) {
dataQuery.setParameter("searchApiName", "%" + search.getSearchApiName().toUpperCase() + "%");
}
return dataQuery;
}
/**
* Native Query 결과를 ApiStatsUI로 변환
*/
private ApiStatsUI toVO(Object[] row) {
ApiStatsUI vo = new ApiStatsUI();
vo.setOrgName(StringUtils.toString(row[1]));
vo.setTotalCnt(StringUtils.toLong(row[2]));
vo.setSuccessCnt(StringUtils.toLong(row[3]));
vo.setSuccessRate(StringUtils.toDecimal(row[4]).setScale(2, RoundingMode.HALF_UP));
vo.setFailRate(StringUtils.toDecimal(row[5]).setScale(2, RoundingMode.HALF_UP));
vo.setTimeoutCnt(StringUtils.toLong(row[6]));
vo.setSystemErrCnt(StringUtils.toLong(row[7]));
vo.setAvgRespTime(StringUtils.toDecimal(row[8]));
vo.setMinRespTime(StringUtils.toDecimal(row[9]));
vo.setMaxRespTime(StringUtils.toDecimal(row[10]));
return vo;
}
/**
* native SQL용 동적 WHERE 절 생성
*/
private StringBuilder buildNativeWhere(ApiStatsSearch search) {
StringBuilder where = new StringBuilder(" WHERE 1=1");
calculateDateRange(search);
if (search.getParsedStartDate() != null) {
where.append(" AND A.STAT_TIME >= :searchStartDate ");
where.append(" AND A.STAT_TIME <= :searchEndDate ");
}
if (StringUtils.isNotBlank(search.getSearchOrgName())) {
where.append(" AND UPPER(B.ORGNAME) LIKE :searchOrgName ");
}
if (StringUtils.isNotBlank(search.getSearchApiName())) {
where.append(" AND UPPER(C.EAISVCDESC) LIKE :searchApiName ");
}
return where;
}
/**
* 조회 기간 계산 (최대 31일 제한)
*/
private void calculateDateRange(ApiStatsSearch search) {
if (StringUtils.isBlank(search.getSearchStartDateTime())) {
return;
}
LocalDate startDate = LocalDate.parse(search.getSearchStartDateTime(),
DateTimeFormatter.ofPattern("yyyyMMdd"));
LocalDate endDate;
if (StringUtils.isNotBlank(search.getSearchEndDateTime())) {
endDate = LocalDate.parse(search.getSearchEndDateTime(),
DateTimeFormatter.ofPattern("yyyyMMdd"));
if (java.time.Period.between(startDate, endDate).getDays() > MAX_DAYS) {
endDate = startDate.plusDays(MAX_DAYS);
}
} else {
endDate = startDate.plusDays(MAX_DAYS);
}
search.setParsedStartDate(startDate);
search.setParsedEndDate(endDate);
}
}
@@ -1,17 +0,0 @@
package com.eactive.eai.rms.ext.djb.swing.service;
import org.springframework.stereotype.Service;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Service
@RequiredArgsConstructor
public class SwingSendManager {
public void send(String eaisvcname, long cnt, long rangeMinute) {
log.info("[Swing] 발송 준비: {} 최근 {}분 {}건", eaisvcname, rangeMinute, cnt);
// TODO: 알림 발송 로직
}
}
@@ -0,0 +1,89 @@
package com.eactive.eai.rms.ext.djb.ums;
import java.util.HashMap;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import com.eactive.apim.portal.template.entity.MessageCode;
import com.eactive.apim.portal.template.service.MessageHandlerService;
import com.eactive.apim.portal.template.service.MessageRecipient;
import com.eactive.apim.portal.user.entity.UserInfo;
import com.eactive.eai.rms.data.entity.man.user.UserInfoService;
import com.eactive.eai.rms.ext.djb.webhook.dto.WebhookSendRequest;
import com.eactive.eai.rms.ext.djb.webhook.service.WebhookService;
import lombok.RequiredArgsConstructor;
@Component
@RequiredArgsConstructor
public class UmsManager {
private static final Logger log = LoggerFactory.getLogger(UmsManager.class);
private final MessageHandlerService messageHandlerService;
private final UserInfoService userInfoService;
private final WebhookService webhookService;
/**
* role 역할을 가진 내부직원에게 메신저(Swing chat) 발송
* @param role
* @param messageCode
* @param message
* @return
*/
public void sendMessenger(String roleId, MessageCode messageCode, String message) {
List<UserInfo> users = userInfoService.findAllByRoleId(roleId);
if (users.isEmpty()) {
log.info("sendMessenger: no users found for role '{}'", roleId);
return;
}
for (UserInfo user : users) {
MessageRecipient recipient = MessageRecipient.builder()
.userId(user.getUserid())
.username(user.getUsername())
.build();
HashMap<String, Object> params = new HashMap<>();
params.put("message", message);
messageHandlerService.publishEvent(messageCode, recipient, params);
}
}
/**
* 내부직원에게 메신저 발송
* @param user
* @param messageCode
* @param message
*/
public void sendMessenger(UserInfo user, MessageCode messageCode, String message) {
MessageRecipient recipient = MessageRecipient.builder()
.userId(user.getUserid())
.username(user.getUsername())
.build();
HashMap<String, Object> params = new HashMap<>();
params.put("message", message);
messageHandlerService.publishEvent(messageCode, recipient, params);
}
/**
* event를 구독중인 제휴사에게 웹훅 발송 (비동기)
* @param event
* @param apiIds
*/
@Async("webhookExecutor")
public void sendWebhook(String event, List<String> apiIds) {
List<WebhookSendRequest> list = webhookService.findSendList(event, apiIds);
for (WebhookSendRequest req : list) {
webhookService.send(req);
}
}
}
@@ -0,0 +1,43 @@
package com.eactive.eai.rms.ext.djb.ums.event;
import java.util.HashMap;
import java.util.Map;
import org.springframework.stereotype.Component;
import com.eactive.apim.portal.template.entity.MessageCode;
import com.eactive.apim.portal.template.service.MessageEventHandler;
import com.eactive.apim.portal.template.service.MessageRecipient;
import com.eactive.apim.portal.template.service.MessageSendEvent;
@Component
public class ApiStatusChangedEventHandler implements MessageEventHandler {
@Override
public MessageCode getKey() {
return MessageCode.API_STATUS_CHANGED;
}
@Override
public boolean allowAdditionalRecipients() {
return true;
}
@Override
public String getDisplayName() {
return "API 상태 변화";
}
@Override
public String getDescription() {
return "<div>사용 가능 변수: </div><br/>"
+ "<div> - message 알림 메시지 </div><br/>";
}
@Override
public MessageSendEvent createEvent(Object source, MessageRecipient recipient, Map<String, Object> params) {
Map<String, String> eventParams = new HashMap<>();
eventParams.put("message", (String) params.get("message"));
return new MessageSendEvent(source, MessageCode.API_STATUS_CHANGED, recipient, eventParams);
}
}
@@ -0,0 +1,43 @@
package com.eactive.eai.rms.ext.djb.ums.event;
import java.util.HashMap;
import java.util.Map;
import org.springframework.stereotype.Component;
import com.eactive.apim.portal.template.entity.MessageCode;
import com.eactive.apim.portal.template.service.MessageEventHandler;
import com.eactive.apim.portal.template.service.MessageRecipient;
import com.eactive.apim.portal.template.service.MessageSendEvent;
@Component
public class InflowTokenFailedEventHandler implements MessageEventHandler {
@Override
public MessageCode getKey() {
return MessageCode.INFLOW_TOKEN_FAILED;
}
@Override
public boolean allowAdditionalRecipients() {
return true;
}
@Override
public String getDisplayName() {
return "유량제어 토큰 획득 실패";
}
@Override
public String getDescription() {
return "<div>사용 가능 변수: </div><br/>"
+ "<div> - message 알림 메시지 </div><br/>";
}
@Override
public MessageSendEvent createEvent(Object source, MessageRecipient recipient, Map<String, Object> params) {
Map<String, String> eventParams = new HashMap<>();
eventParams.put("message", (String) params.get("message"));
return new MessageSendEvent(source, MessageCode.INFLOW_TOKEN_FAILED, recipient, eventParams);
}
}
@@ -0,0 +1,344 @@
package com.eactive.eai.rms.ext.djb.ums.service;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.Charset;
import java.security.SecureRandom;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import org.springframework.stereotype.Service;
import com.eactive.apim.portal.portalproperty.service.PortalPropertyService;
import com.eactive.apim.portal.template.entity.MessageRequest;
import com.eactive.eai.common.util.SystemUtil;
import com.eactive.eai.rms.data.entity.man.monitoringProperty.service.MonitoringPropertyService;
import com.eactive.eai.rms.ext.djb.ums.vo.EmailDataVO;
import com.eactive.eai.rms.ext.djb.ums.vo.SmsDataVO;
import com.eactive.eai.rms.ext.djb.ums.vo.StdHeadVO;
import com.eactive.eai.rms.ext.djb.ums.vo.SwingCommonVO;
import com.eactive.eai.rms.ext.djb.ums.vo.SwingDataVO;
import com.eactive.eai.rms.ext.djb.ums.vo.SwingReceiverDataVO;
import com.google.gson.Gson;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Service
public class UmsService {
private static final Gson gson = new Gson();
private static final String SYS_DVCD = "OPA";
private final MonitoringPropertyService propertyService;
public UmsService(MonitoringPropertyService propertyService) {
this.propertyService = propertyService;
}
public boolean send(MessageRequest messageRequest) throws Exception {
if (messageRequest == null) {
throw new IllegalArgumentException("messageRequest is null");
}
this.httpConnection(messageRequest);
return true;
}
private void httpConnection(MessageRequest messageRequest) throws Exception {
HttpURLConnection conn = null;
BufferedReader br = null;
log.debug("<<<HTTP Connection>>>:" + messageRequest.toString());
try {
String host = propertyService.getPropertyValue("Monitoring", "djb.ums."+messageRequest.getMessageType().toLowerCase()+".url", "");
URL url = new URL(host);
int timeoutValue = 5 * 1000; // 타임아웃 설정을 위한 값 (단위: ms)
// Charset charset = Charset.forName("UTF-8");
Charset charset = Charset.forName("MS949");
StringBuffer sb = null;
String bodyData = this.makeBodyData(messageRequest);
String responseData = "";
String method = bodyData.isEmpty() ? "GET" : "POST";
conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod(method); // 전송방식
// conn.setRequestProperty("Content-Type", "application/json; charset=UTF-8");
conn.setRequestProperty("Content-Type", "application/json; charset=MS949");
conn.setConnectTimeout(timeoutValue); // 연결 타임아웃 설정(5초)
conn.setReadTimeout(timeoutValue); // 읽기 타임아웃 설정(5초)
conn.setDoOutput(true);
// POst 방식인 경우에만
if (method.equals("POST")) {
OutputStream os = conn.getOutputStream();
byte requestData[] = bodyData.getBytes(charset);
os.write(requestData);
os.close();
}
if (log.isDebugEnabled()) {
log.debug("getContentType();" + conn.getContentType()); // 응답 콘텐츠 유형 구하기
log.debug("getResponsecode();" + conn.getResponseCode()); // 응답 코드 구하기
log.debug("getResponseMessage():" + conn.getResponseMessage()); // 응답 메세지 구하기
}
// http 요청 후 응답 받은 데이타를 버퍼에 쌓는다
if (conn.getResponseCode() >= 200 && conn.getResponseCode() <= 300) {
br = new BufferedReader(new InputStreamReader(conn.getInputStream(), charset));
} else {
br = new BufferedReader(new InputStreamReader(conn.getErrorStream(), charset));
}
String inputLine;
sb = new StringBuffer();
while ((inputLine = br.readLine()) != null) {
sb.append(inputLine);
}
responseData = sb.toString();
} finally {
// http 요청 및 응답 완료 후 BufferedReader를 닫는다
if (br != null) {
br.close();
}
if (conn != null) {
conn.disconnect();
}
}
}
private String makeBodyData(MessageRequest messageRequest) {
// 리턴값
String rtnVal = "";
// guID 및 날짜 시간 정보
String jsonData[] = this.makeStdGuidInfo();
// 표준 헤더
StdHeadVO stdHeadVO = new StdHeadVO();
stdHeadVO.setNxgn_stnd_idfr("JERA");
stdHeadVO.setGuid(jsonData[2]);
stdHeadVO.setGuid_prgs_no("1");
stdHeadVO.setStnd_mesg_ver("R10");
stdHeadVO.setOrtr_guid(jsonData[2]);
stdHeadVO.setSect_ecrpt_yn("N");
stdHeadVO.setFrst_trnm_ipad(propertyService.getPropertyValue("Monitoring", "djb.ums.was_ip_address", ""));
stdHeadVO.setFrst_trnm_ipv4_addr(propertyService.getPropertyValue("Monitoring", "djb.ums.was_ip_address", ""));
stdHeadVO.setFrst_trnm_mac(propertyService.getPropertyValue("Monitoring", "djb.ums.was_mac_address", ""));
stdHeadVO.setFrst_mesg_dman_dt(jsonData[0]);
stdHeadVO.setFrst_mesg_dman_time(jsonData[1]);
stdHeadVO.setFrst_trnm_sys_dvcd(SYS_DVCD);
stdHeadVO.setTrnm_sys_dvcd(SYS_DVCD);
stdHeadVO.setDman_rspn_dvcd("S"); // 요청응답구분코드 S:요청 R:응답
stdHeadVO.setSys_env_dvcd(SystemUtil.getSysOperEvirnDstcd()); // D:개발 T:테스트 P:운영
stdHeadVO.setTx_tycd("O"); // O:온라인 B:배치
stdHeadVO.setSynz_dvcd("S"); // S:동기 A:비동기
stdHeadVO.setChnl_tycd(""); // 채널유형코드
stdHeadVO.setHmab_dvcd("1"); // 내외구분코드 1:내부 2:외부(타발)
stdHeadVO.setIf_id(messageRequest.getEaiInterfaceId()); // 인터페이스ID
stdHeadVO.setTx_id(messageRequest.getServiceId()); // 거래ID
if ("MESSENGER".equals(messageRequest.getMessageType()) ) {
// 스윙챗 알림 공용
SwingCommonVO swiComVO = new SwingCommonVO();
swiComVO.setClientld(propertyService.getPropertyValue("Monitoring", "djb.ums.messenger.client_id", ""));
swiComVO.setClientSecret(propertyService.getPropertyValue("Monitoring", "djb.ums.messenger.client_secret", ""));
swiComVO.setCompanyCode("CB");
swiComVO.setRequestUniqueKey(this.genrateRandomStringNumber26());
// 스윙챗 알림 데이터
SwingDataVO swiDataVO = new SwingDataVO();
swiDataVO.setNotiCode("CNO_FEP_HMP_0002");
swiDataVO.setNotiTitle("알림");
swiDataVO.setNotiContent(messageRequest.getMessage());
// 스윙챗 알림 데이터
List<SwingReceiverDataVO> swiReDataVOList = new ArrayList<SwingReceiverDataVO>();
SwingReceiverDataVO receiver = new SwingReceiverDataVO();
receiver.setCode(messageRequest.getMessengerId());
swiReDataVOList.add(receiver);
swiDataVO.setReceiverInfo(swiReDataVOList);
String header = gson.toJson(stdHeadVO);
String common = gson.toJson(swiComVO);
String data = gson.toJson(swiDataVO);
// 헤더 + 데이터
rtnVal = "{\"HEAD\":" + header + " ,\r\n" + "\"DATA\":\r\n" + " [" + "{\r\n"
+ " \"data_dvcd\":\"IO\", \r\n" + " \"BIZ_DATA\":\r\n" + " {\r\n"
+ " \"common\":\r\n" + common + ",\r\n" + " \"data\":" + data
+ " }\r\n" + " }\r\n" + " ]\r\n" + "}";
}
else if ("SMS".equals(messageRequest.getMessageType())
|| "KAKAO".equals(messageRequest.getMessageType())) {
// Sms 데이터
SmsDataVO dataVO = new SmsDataVO();
dataVO.setSnd_dman_dt(jsonData[0]);
dataVO.setSnd_dman_id("");
dataVO.setSnd_dvcd("EAI");
dataVO.setMsg_titl("");
dataVO.setMsg_ctnt(messageRequest.getMessage());
dataVO.setRecvr_no(messageRequest.getPhone());
dataVO.setSndn_no("15880079"); //발신번호
dataVO.setSnd_empno(""); //요청자
dataVO.setSnd_emp_dprm_cd(""); //요청부서코드
if ("SMS".equals(messageRequest.getMessageType())) {
dataVO.setLtrs_snd_tycd("1"); // 1:SMS 2:LMS/MMS
dataVO.setMsg_snd_tycd("0000"); // 알림톡 메세지 종류 0000:SMS, 1000:LMS, 1008:알림톡, ....
} else if ("KAKAO".equals(messageRequest.getMessageType())) {
dataVO.setLtrs_snd_tycd("2"); // 1:SMS 2:LMS/MMS
dataVO.setMsg_snd_tycd("1008"); // 알림톡 메세지 종류 0000:SMS, 1000:LMS, 1008:알림톡, ....
dataVO.setAlmtk_snd_prfl_key(""); // 플러스친구아이디
dataVO.setAlmtk_tmplt_cd(""); // 템플릿코드
dataVO.setAlmtk_err_ltrs_snd_yn("Y"); // 오류자문자발송여부
dataVO.setAlmtk_btn_trnm_tycd("2"); // 카카오버튼전송방식 1-format string 2-JSON 3-XML
}
dataVO.setSms_trnm_bzwk_dvcd(""); //업무구분코드
dataVO.setSms_bzwk_dcls_dvcd(""); //업무세분코드
dataVO.setSms_sys_dvcd("83"); //시스템코드
dataVO.setTx_dt(jsonData[0]); //거래일자
dataVO.setTx_tktm(jsonData[1].substring(0,6)); //거래시간
List<SmsDataVO> dataList = new ArrayList<SmsDataVO>();
dataList.add(dataVO);
String header = gson.toJson(stdHeadVO);
String data = gson.toJson(dataList);
// 헤더 + 데이터
rtnVal = "{\"HEAD\":" + header + " ,\r\n" + "\"DATA\":\r\n" + " [" + "{\r\n"
+ " \"data_dvcd\":\"IO\", \r\n" + " \"BIZ_DATA\":\r\n" + " {\r\n"
+ " \"ums_if_data_ncnt\":1, \r\n"
+ " \"ums_if_data\":" + data
+ " }\r\n" + " }\r\n" + " ]\r\n" + "}";
}
else if ("EMAIL".equals(messageRequest.getMessageType())) {
// 이메일 데이터
EmailDataVO dataVO = new EmailDataVO();
dataVO.setUms_evnt_id(""); // UMS이벤트ID(필수)
dataVO.setSnd_dman_dt(jsonData[0]);
dataVO.setSnd_dman_id("");
dataVO.setSnd_dman_sno("1");
dataVO.setEmail_titl(messageRequest.getSubject()); // 메일제목
dataVO.setCstno(""); // 고객번호(필수)
dataVO.setSendr_emaddr(""); // 발신자 이메일
dataVO.setSendr_nm(""); // 발신자 이름
dataVO.setSndbk_emaddr(""); // 리턴 이메일
dataVO.setRecvr_emaddr(messageRequest.getEmail()); // 수신자 이메일(필수)
dataVO.setRecvr_nm(messageRequest.getUsername()); // 수신자명(필수)
dataVO.setTmplt_mpng_info(messageRequest.getMessage()); // 탬플릿매핑정보
dataVO.setTmplt_mpng_rptt_info(""); // 탬플릿매핑반복정보
dataVO.setSnd_empno(""); // 발신자ID(필수)
dataVO.setSnd_emp_dprm_cd(""); // 발신자부서코드(필수)
dataVO.setBzwk_cd(""); // 업무구분코드
dataVO.setSub_bzwk_cd(""); // 업무세부코드
dataVO.setSys_dvcd(SYS_DVCD);
dataVO.setTx_dt(jsonData[0]); //거래일자
dataVO.setTx_tktm(jsonData[1].substring(0,6)); //거래시간
List<EmailDataVO> dataList = new ArrayList<EmailDataVO>();
dataList.add(dataVO);
String header = gson.toJson(stdHeadVO);
String data = gson.toJson(dataList);
// 헤더 + 데이터
rtnVal = "{\"HEAD\":" + header + " ,\r\n" + "\"DATA\":\r\n" + " [" + "{\r\n"
+ " \"data_dvcd\":\"IO\", \r\n" + " \"BIZ_DATA\":\r\n" + " {\r\n"
+ " \"ums_if_data_ncnt\":1, \r\n"
+ " \"ums_if_data\":" + data
+ " }\r\n" + " }\r\n" + " ]\r\n" + "}";
}
log.debug(rtnVal);
return rtnVal;
}
private String[] makeStdGuidInfo() {
// 리턴값
String rtnVal[] = new String[3];
// 현재시간
Date currentDate = new Date();
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmssSSS");
String formattedDate = sdf.format(currentDate);
rtnVal[0] = formattedDate.substring(0,8);
rtnVal[1] = formattedDate.substring(8);
// 랜던값
String random20 = this.genrateRandomStringNumber20();
rtnVal[2] = formattedDate + SYS_DVCD + random20;
return rtnVal;
}
private String genrateRandomStringNumber20() {
String rtnVal= "";
StringBuilder buffer= new StringBuilder(10);
// 랜덤값
String characters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
// 랜덤객체 생성
SecureRandom random = new SecureRandom();
// 반복문
for (int i= 0;i< 10;i++) {
// 랜덤수
int randomIndex = random.nextInt(characters.length());
// 생성된 랜덤값
char randomChar = characters.charAt(randomIndex);
// 랜덤값 저장
buffer.append(randomChar);
}
// 랜덤객체 재생성
random = new SecureRandom();
// 난수 10개 생성
int random10 = (int) (Math.pow(10, 9) + random.nextDouble() * Math.pow(10, 9));
// 문자 숫자 조합(10) + 숫자(10)
rtnVal = buffer.toString() + Integer.toString(random10);
//리턴
return rtnVal;
}
private String genrateRandomStringNumber26() {
StringBuilder buffer = new StringBuilder(26);
String characters = "abcdefghijklmnopqrstuvwxyzABCDEFGHJKLMNOPQRSTUVWXYZ0123456789";
SecureRandom random = new SecureRandom();
for (int i = 0; i < 26; i++) {
buffer.append(characters.charAt(random.nextInt(characters.length())));
}
return buffer.toString();
}
}
@@ -0,0 +1,67 @@
package com.eactive.eai.rms.ext.djb.ums.vo;
import lombok.Data;
@Data
public class EmailDataVO {
/** UMS이벤트ID */
private String ums_evnt_id;
/** 발송요청일자 */
private String snd_dman_dt;
/** UUID */
private String snd_dman_id;
/** 발송요청순번 */
private String snd_dman_sno;
/** 메일제목 */
private String email_titl;
/** 고객번호 */
private String cstno;
/** 발신자 이메일 */
private String sendr_emaddr;
/** 발신자 이름 */
private String sendr_nm;
/** 리턴 이메일 */
private String sndbk_emaddr;
/** 수신자 이메일 */
private String recvr_emaddr;
/** 수신자명 */
private String recvr_nm;
/** 탬플릿매핑정보 */
private String tmplt_mpng_info;
/** 탬플릿매핑반복정보 */
private String tmplt_mpng_rptt_info;
/** 발신자ID */
private String snd_empno;
/** 발신자부서코드 */
private String snd_emp_dprm_cd;
/** 업무구분코드 */
private String bzwk_cd;
/** 업무세부코드 */
private String sub_bzwk_cd;
/** 시스템코드 */
private String sys_dvcd;
/** 거래일자 */
private String tx_dt;
/** 거래시간 */
private String tx_tktm;
}
@@ -0,0 +1,67 @@
package com.eactive.eai.rms.ext.djb.ums.vo;
import lombok.Data;
@Data
public class SmsDataVO {
/** 발송요청일자 */
private String snd_dman_dt;
/** UUID */
private String snd_dman_id;
/** 발송구분 */
private String snd_dvcd;
/** 발송타입 */
private String ltrs_snd_tycd;
/** LMS 제목 */
private String msg_titl;
/** 메세지 */
private String msg_ctnt;
/** 수신자번호 */
private String recvr_no;
/** 발신번호 */
private String sndn_no;
/** 요청자(직원번호) */
private String snd_empno;
/** 요청부서코드 */
private String snd_emp_dprm_cd;
/** 알림톡 메시지 종류 */
private String msg_snd_tycd;
/** 플러스친구아이디 */
private String almtk_snd_prfl_key;
/** 템플릿코드 */
private String almtk_tmplt_cd;
/** 오류자문자발송여부 */
private String almtk_err_ltrs_snd_yn;
/** 카카오톡전송방식 */
private String almtk_btn_trnm_tycd;
/** 업무구분 코드 */
private String sms_trnm_bzwk_dvcd;
/** 업무세부코드 */
private String sms_bzwk_dcls_dvcd;
/** 시스템코드 */
private String sms_sys_dvcd;
/** 거래일자 */
private String tx_dt;
/** 거래시간 */
private String tx_tktm;
}
@@ -0,0 +1,61 @@
package com.eactive.eai.rms.ext.djb.ums.vo;
import lombok.Data;
@Data
public class StdHeadVO {
private String nxgn_stnd_idfr;
private String guid;
private String guid_prgs_no;
private String stnd_mesg_ver;
private String ortr_guid;
private String sect_ecrpt_yn;
private String frst_trnm_ipad;
private String frst_trnm_ipv4_addr;
private String frst_trnm_mac;
private String frst_mesg_dman_dt;
private String frst_mesg_dman_time;
private String frst_trnm_sys_dvcd;
private String trnm_sys_dvcd;
private String dman_rspn_dvcd;
private String sys_env_dvcd;
private String tx_tycd;
private String synz_dvcd;
private String tx_id;
private String osdch_dvcd;
private String chnl_tycd;
private String if_id;
private String hmab_dvcd;
private String tx_brcd;
private String tlrno;
private String insl_brcd;
private String blng_brcd;
private String mgmt_brcd;
}
@@ -0,0 +1,19 @@
package com.eactive.eai.rms.ext.djb.ums.vo;
import lombok.Data;
@Data
public class SwingCommonVO {
/** 클라이업트 D */
private String clientld;
/** 클라이언트 시크릿 */
private String clientSecret;
/** 회사코드 */
private String companyCode;
/** 요청고유키 */
private String requestUniqueKey;
}
@@ -0,0 +1,21 @@
package com.eactive.eai.rms.ext.djb.ums.vo;
import java.util.List;
import lombok.Data;
@Data
public class SwingDataVO {
/** 알림코드 */
private String notiCode;
/** 알림제목 */
private String notiTitle;
/** 알림내용 */
private String notiContent;
/** 수신자정보 */
private List<SwingReceiverDataVO> receiverInfo;
}
@@ -0,0 +1,22 @@
package com.eactive.eai.rms.ext.djb.ums.vo;
import lombok.Data;
@Data
public class SwingReceiverDataVO {
/** 알림코드 */
private String name;
/** 알림제목 */
private String type;
/** 알림내용 */
private String companyCode;
/** 수신자정보 */
private String code;
/** 상품명 */
private String prdNm;
}
@@ -0,0 +1,26 @@
package com.eactive.eai.rms.ext.djb.webhook.config;
import java.util.concurrent.Executor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
@Configuration
@EnableAsync
public class WebhookAsyncConfig {
@Bean(name = "webhookExecutor")
public Executor webhookExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(2);
executor.setMaxPoolSize(10);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("webhook-");
executor.setWaitForTasksToCompleteOnShutdown(true);
executor.setAwaitTerminationSeconds(30);
executor.initialize();
return executor;
}
}
@@ -11,7 +11,7 @@ import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.eactive.eai.rms.data.ext.djb.webhook.WebhookSendLog;
import com.eactive.eai.rms.data.entity.onl.djb.webhook.WebhookSendLog;
import com.eactive.eai.rms.ext.djb.webhook.dto.WebhookSendRequest;
import com.eactive.eai.rms.ext.djb.webhook.service.WebhookReceiveService;
import com.eactive.eai.rms.ext.djb.webhook.service.WebhookService;
@@ -38,16 +38,12 @@ public class WebhookSendController {
public ResponseEntity<Map<String, Object>> sendWebhook(
@RequestBody WebhookSendRequest request) {
WebhookSendLog result = webhookService.send(
request.getTargetUrl(),
request.getEventType(),
request.getData()
);
webhookService.send(request);
Map<String, Object> response = new HashMap<>();
response.put("logId", result.getId());
response.put("success", result.getSuccess());
response.put("statusCode", result.getStatusCode());
// response.put("logId", result.getId());
// response.put("success", result.getSuccess());
// response.put("statusCode", result.getStatusCode());
return ResponseEntity.ok(response);
}
@@ -6,8 +6,9 @@ import lombok.Setter;
@Getter
@Setter
public class WebhookSendRequest {
private String orgId;
private String targetUrl;
private String eventType;
private String secret;
private Object data;
}
@@ -0,0 +1,11 @@
package com.eactive.eai.rms.ext.djb.webhook.repository;
import com.eactive.eai.rms.data.EMSDataSource;
import com.eactive.eai.rms.data.entity.onl.djb.webhook.WebhookReq;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
@EMSDataSource
public interface WebhookReqRepository extends JpaRepository<WebhookReq, String> {
}
@@ -10,7 +10,7 @@ import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import com.eactive.eai.rms.data.EMSDataSource;
import com.eactive.eai.rms.data.ext.djb.webhook.WebhookSendLog;
import com.eactive.eai.rms.data.entity.onl.djb.webhook.WebhookSendLog;
@Repository
@EMSDataSource
@@ -1,17 +0,0 @@
package com.eactive.eai.rms.ext.djb.webhook.service;
import org.springframework.stereotype.Service;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Service
@RequiredArgsConstructor
public class WebhookSendManager {
public void send(String eaisvcname, String eventType) {
log.info("[Webhook] 발송 준비: {} {}", eaisvcname, eventType);
// TODO: PTL_WEBHOOK_REQ 조인 조회 후 WebhookService.send() 호출
}
}
@@ -2,12 +2,16 @@ package com.eactive.eai.rms.ext.djb.webhook.service;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
@@ -18,10 +22,17 @@ import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.HttpServerErrorException;
import org.springframework.web.client.RestTemplate;
import com.eactive.eai.rms.data.ext.djb.webhook.WebhookSendLog;
import com.eactive.eai.rms.common.context.MonitoringContext;
import com.eactive.eai.rms.data.entity.onl.djb.webhook.QWebhookReq;
import com.eactive.eai.rms.data.entity.onl.djb.webhook.QWebhookReqApi;
import com.eactive.eai.rms.data.entity.onl.djb.webhook.QWebhookReqEvent;
import com.eactive.eai.rms.data.entity.onl.djb.webhook.WebhookSendLog;
import com.eactive.eai.rms.ext.djb.webhook.dto.WebhookPayload;
import com.eactive.eai.rms.ext.djb.webhook.dto.WebhookSendRequest;
import com.eactive.eai.rms.ext.djb.webhook.repository.WebhookSendLogRepository;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.querydsl.core.Tuple;
import com.querydsl.jpa.impl.JPAQueryFactory;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -34,19 +45,63 @@ public class WebhookService {
private final WebhookSendLogRepository sendLogRepository;
private final ObjectMapper objectMapper;
private final RestTemplate restTemplate;
private final MonitoringContext monitoringContext;
private String secretKey = "HW8JtFpkPmQqsVmr0Rb81P4qDypaNIVbGf3ZNMMPfyerhOHshoKABLaMBo1HA4BldTxhw1NjYmVnoPu9IIV7cyRXjecL3b2UctyR7DnVaJausltZLLr8Qm4Bzs4wRmgf";
@PersistenceContext(unitName = "entityManagerFactoryForEMS")
private EntityManager entityManager;
private static final String HMAC_ALGORITHM = "HmacSHA256";
private static final int MAX_RETRY = 3;
private static final int DEFAULT_RETRY_COUNT = 3;
private static final int DEFAULT_RETRY_TIME = 10000;
@SuppressWarnings("unchecked")
public List<WebhookSendRequest> findSendList(String event, List<String> apiIds) {
QWebhookReq req = QWebhookReq.webhookReq;
QWebhookReqEvent evt = QWebhookReqEvent.webhookReqEvent;
QWebhookReqApi api = QWebhookReqApi.webhookReqApi;
List<Tuple> tuples = new JPAQueryFactory(entityManager)
.select(req.orgId, req.targetUrl, req.secret, api.id.apiId)
.from(req)
.join(evt).on(req.id.eq(evt.id.webhookReqId))
.join(api).on(req.id.eq(api.id.webhookReqId))
.where(evt.id.eventType.eq(event)
.and(api.id.apiId.in(apiIds)))
.fetch();
// orgId + targetUrl + secret 기준으로 그룹핑, apiId를 data(List)로 집계
Map<String, WebhookSendRequest> resultMap = new LinkedHashMap<>();
for (Tuple t : tuples) {
String key = t.get(req.orgId) + "|" + t.get(req.targetUrl);
resultMap.computeIfAbsent(key, k -> {
WebhookSendRequest wr = new WebhookSendRequest();
wr.setOrgId(t.get(req.orgId));
wr.setTargetUrl(t.get(req.targetUrl));
wr.setSecret(t.get(req.secret));
wr.setEventType(event);
wr.setData(new ArrayList<String>());
return wr;
});
((List<String>) resultMap.get(key).getData()).add(t.get(api.id.apiId));
}
return new ArrayList<>(resultMap.values());
}
/**
* 웹훅 발송 메인 메서드
*/
@Transactional
public WebhookSendLog send(String targetUrl, String eventType, Object data) {
public void send(WebhookSendRequest req) {
String targetUrl = req.getTargetUrl();
String secret = req.getSecret();
String eventType = req.getEventType();
Object data = req.getData();
WebhookSendLog sendLog = new WebhookSendLog();
sendLog.setOrgId(req.getOrgId());
sendLog.setTargetUrl(targetUrl);
sendLog.setEventType(eventType);
@@ -57,7 +112,7 @@ public class WebhookService {
sendLog.setPayload(payloadJson);
// 2. Secret Key로 HMAC-SHA256 서명 생성
String signature = generateSignature(payloadJson);
String signature = generateSignature(secret, payloadJson);
sendLog.setSignature(signature);
// 3. HTTP 요청 헤더 구성
@@ -67,18 +122,20 @@ public class WebhookService {
headers.set("X-Webhook-Event", eventType);
headers.set("X-Webhook-Timestamp", String.valueOf(webhookPayload.getTimestamp()));
// 4. 발송
// 4. 발송 (재시도 포함)
int retryCount = monitoringContext.getIntProperty(MonitoringContext.API_WEBHOOK_RETRY_COUNT, DEFAULT_RETRY_COUNT);
int retryTime = monitoringContext.getIntProperty(MonitoringContext.API_WEBHOOK_RETRY_TIME, DEFAULT_RETRY_TIME);
sendLog.setSentAt(LocalDateTime.now());
HttpEntity<String> request = new HttpEntity<>(payloadJson, headers);
ResponseEntity<String> response = restTemplate.postForEntity(targetUrl, request, String.class);
HttpEntity<String> httpRequest = new HttpEntity<>(payloadJson, headers);
ResponseEntity<String> response = sendWithRetry(targetUrl, httpRequest, sendLog, retryCount, retryTime);
// 5. 성공 로그
sendLog.setStatusCode(response.getStatusCode().value());
sendLog.setResponseBody(response.getBody());
sendLog.setSuccess(response.getStatusCode().is2xxSuccessful());
log.info("[Webhook] 발송 성공 - eventType: {}, url: {}, status: {}",
eventType, targetUrl, response.getStatusCode());
log.info("[Webhook] 발송 성공 - eventType: {}, url: {}, status: {}, retryCount: {}",
eventType, targetUrl, response.getStatusCode(), sendLog.getRetryCount());
} catch (HttpClientErrorException | HttpServerErrorException e) {
sendLog.setStatusCode(e.getStatusCode().value());
@@ -95,40 +152,46 @@ public class WebhookService {
eventType, targetUrl, e.getMessage());
}
return sendLogRepository.save(sendLog);
sendLogRepository.save(sendLog);
}
/**
* 실패 건 재시도
* 재시도 포함 HTTP 발송
* - 4xx: 클라이언트 오류이므로 즉시 throw (재시도 불필요)
* - 5xx / 네트워크 오류: 최대 RETRY_COUNT회까지 exponential backoff 재시도 (1s → 2s → 4s)
*/
@Transactional
public void retryFailedWebhooks() {
List<WebhookSendLog> failedLogs =
sendLogRepository.findFailedLogs(MAX_RETRY);
for (WebhookSendLog failedLog : failedLogs) {
private ResponseEntity<String> sendWithRetry(String targetUrl, HttpEntity<String> request, WebhookSendLog sendLog, int retryCount, int retryTime) throws Exception {
Exception lastException = null;
for (int attempt = 0; attempt <= retryCount; attempt++) {
try {
log.info("[Webhook] 재시도 - id: {}, retry: {}", failedLog.getId(), failedLog.getRetryCount());
failedLog.setRetryCount(failedLog.getRetryCount() + 1);
sendLogRepository.save(failedLog);
send(failedLog.getTargetUrl(), failedLog.getEventType(),
objectMapper.readValue(failedLog.getPayload(), Object.class));
if (attempt > 0) {
long delayMs = retryTime * (1L << (attempt - 1));
log.info("[Webhook] 재시도 {}/{} - url: {}, delay: {}ms", attempt, retryCount, targetUrl, delayMs);
Thread.sleep(delayMs);
sendLog.setRetryCount(attempt);
}
return restTemplate.postForEntity(targetUrl, request, String.class);
} catch (HttpClientErrorException e) {
throw e; // 4xx는 재시도 불필요
} catch (Exception e) {
log.error("[Webhook] 재시도 실패 - id: {}", failedLog.getId(), e);
lastException = e;
log.warn("[Webhook] 발송 실패 (attempt {}/{}) - url: {}, error: {}", attempt, retryCount, targetUrl, e.getMessage());
}
}
throw lastException;
}
/**
* HMAC-SHA256 서명 생성
* Java 17 미만은 HexFormat 미지원이므로 직접 변환
*/
private String generateSignature(String payload) throws Exception {
private String generateSignature(String secret, String payload) throws Exception {
Mac mac = Mac.getInstance(HMAC_ALGORITHM);
SecretKeySpec keySpec = new SecretKeySpec(
secretKey.getBytes(StandardCharsets.UTF_8), HMAC_ALGORITHM);
secret.getBytes(StandardCharsets.UTF_8), HMAC_ALGORITHM);
mac.init(keySpec);
byte[] hash = mac.doFinal(payload.getBytes(StandardCharsets.UTF_8));
return bytesToHex(hash);
@@ -14,6 +14,7 @@ public class PortalApprovalAuthorizer implements ApprovalAuthorizer {
return approval.getApprovers().stream()
.filter(this::isCurrentApprover)
.map(this::getApproverId)
.filter(java.util.Objects::nonNull)
.anyMatch(id -> id.equals(approverId));
}
@@ -22,6 +23,6 @@ public class PortalApprovalAuthorizer implements ApprovalAuthorizer {
}
private String getApproverId(Approver approver) {
return approver.getUser().getId();
return approver.getUser() != null ? approver.getUser().getId() : null;
}
}
@@ -13,10 +13,16 @@ import org.springframework.data.domain.Sort;
import org.springframework.data.web.SortDefault;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Controller
@RequiredArgsConstructor
public class PortalApprovalManController extends BaseAnnotationController {
@@ -24,8 +30,8 @@ public class PortalApprovalManController extends BaseAnnotationController {
private final PortalApprovalManService portalApprovalManService;
@GetMapping(value = "/onl/apim/approval/portalApprovalMan.view")
public void view() {
// view
public void view(Model model) {
model.addAttribute("hardDeleteEnabled", portalApprovalManService.isHardDeleteEnabled());
}
@GetMapping(value = "/onl/apim/approval/portalApprovalMan.view", params = "cmd=DETAIL")
@@ -76,4 +82,52 @@ public class PortalApprovalManController extends BaseAnnotationController {
portalApprovalManService.redeploy(id);
return ResponseEntity.ok().build();
}
@PostMapping(value = "/onl/apim/approval/portalApprovalMan.json", params = "cmd=HARD_DELETE_MULTIPLE")
public ResponseEntity<Map<String, Object>> hardDeleteMultiple(String ids) {
Map<String, Object> resultMap = new HashMap<>();
try {
if (StringUtils.isBlank(ids)) {
resultMap.put("status", "fail");
resultMap.put("message", "삭제할 승인 요청을 선택해주세요.");
return ResponseEntity.ok(resultMap);
}
String[] idArray = ids.split(",");
int successCount = 0;
List<String> failMessages = new ArrayList<>();
for (String id : idArray) {
if (StringUtils.isNotBlank(id.trim())) {
try {
portalApprovalManService.hardDelete(id.trim());
successCount++;
} catch (BizException e) {
failMessages.add(e.getMessage());
} catch (Exception e) {
failMessages.add(id.trim() + ": " + e.getMessage());
}
}
}
if (failMessages.isEmpty()) {
resultMap.put("status", "success");
resultMap.put("message", successCount + "건의 승인 요청이 완전삭제되었습니다.");
} else {
resultMap.put("status", "fail");
resultMap.put("message", "완전삭제 실패: " + String.join(", ", failMessages)
+ (successCount > 0 ? " (" + successCount + "건 성공)" : ""));
}
} catch (Exception e) {
resultMap.put("status", "fail");
resultMap.put("message", "완전삭제 중 오류가 발생했습니다: " + e.getMessage());
}
return ResponseEntity.ok(resultMap);
}
@PostMapping(value = "/onl/apim/approval/portalApprovalMan.json", params = "cmd=UPDATE")
public ResponseEntity<Void> update(String id, String expectEndDate) {
portalApprovalManService.update(id, expectEndDate);
return ResponseEntity.ok().build();
}
}
@@ -6,6 +6,7 @@ import com.eactive.apim.portal.apprequest.repository.AppRequestRepository;
import com.eactive.apim.portal.approval.entity.Approval;
import com.eactive.apim.portal.approval.entity.ApprovalType;
import com.eactive.apim.portal.approval.statemachine.*;
import com.eactive.apim.portal.portalproperty.service.PortalPropertyService;
import com.eactive.apim.portal.portaluser.entity.PortalUser;
import com.eactive.apim.portal.portaluser.repository.PortalUserRepository;
import com.eactive.eai.data.entity.onl.apim.apigroup.ApiGroup;
@@ -25,6 +26,7 @@ import com.eactive.eai.rms.onl.apim.portalorg.PortalOrgManService;
import com.eactive.eai.rms.onl.apim.portaluser.PortalUserManService;
import com.eactive.eai.rms.onl.apim.portaluser.PortalUserUI;
import com.eactive.eai.rms.common.acl.user.ui.UserUI;
import com.eactive.eai.rms.onl.common.exception.BizException;
import com.eactive.eai.rms.onl.transaction.apim.ApiInterfaceService;
import com.eactive.eai.rms.onl.transaction.apim.mapping.ApiSpecUIMapper;
import com.eactive.eai.rms.onl.transaction.apim.ui.ApiInterfaceUI;
@@ -36,6 +38,8 @@ import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import java.util.*;
@Service
@@ -43,6 +47,9 @@ import java.util.*;
@RequiredArgsConstructor
public class PortalApprovalManService extends BaseService {
private static final String PORTAL_PROPERTY_GROUP = "Portal";
private static final String HARD_DELETE_FLAG_KEY = "approval.hard-delete.enabled";
private final ApprovalService approvalService;
private final ApprovalStateMachine approvalStateMachine;
private final AppRequestRepository appRequestRepository;
@@ -59,6 +66,10 @@ public class PortalApprovalManService extends BaseService {
private final ApiGroupUIMapper apiGroupUIMapper;
private final PortalApprovalAuthorizer portalApprovalAuthorizer;
private final PortalOrgManService portalOrgManService;
private final PortalPropertyService portalPropertyService;
@PersistenceContext(unitName = "entityManagerFactoryForEMS")
private EntityManager entityManager;
public Page<PortalApprovalUI> selectList(Pageable pageable, PortalApprovalUISearch portalApprovalUISearch) {
Page<Approval> approval = approvalService.findAll(pageable, portalApprovalUISearch);
@@ -76,6 +87,16 @@ public class PortalApprovalManService extends BaseService {
* 마스킹된 상세 정보 조회
*/
public PortalApprovalUI selectDetail(String id) {
//2026.06.05 관리자가 승인요청을 조회시 심사중으로 상태 변경
//APP 유형이고 REQUESTED 상태인 경우 관리자 조회 시 PROCESSING으로 변경
approvalService.findById(id).ifPresent(approval -> {
if (approval.getApprovalType().equals(ApprovalType.APP)
&& approval.getApprovalStatus() instanceof RequestedState) {
approval.setApprovalStatus(new ProcessingState());
approvalService.save(approval);
}
});
PortalApprovalUI result = getDetailWithMaskOption(id, false);
if (result == null) {
throw new IllegalArgumentException("승인 정보를 찾을 수 없습니다. ID: " + id);
@@ -320,6 +341,13 @@ public class PortalApprovalManService extends BaseService {
sendEvent(id, ApprovalEvent.DENY, options);
}
public void update(String id, String expectEndDate) {
Approval approval = approvalService.findById(id)
.orElseThrow(() -> new IllegalArgumentException("승인 정보를 찾을 수 없습니다. ID: " + id));
approval.setExpectEndDate(expectEndDate);
approvalService.save(approval);
}
public void redeploy(String id) {
Map options = new HashMap<>();
Approval approval = approvalService.findById(id).orElseThrow(() -> new IllegalArgumentException("승인 정보를 찾을 수 없습니다. ID: " + id));
@@ -330,6 +358,28 @@ public class PortalApprovalManService extends BaseService {
}
public boolean isHardDeleteEnabled() {
Map<String, String> properties = portalPropertyService.getPortalPropertiesAsMap(PORTAL_PROPERTY_GROUP);
return Boolean.parseBoolean(properties.getOrDefault(HARD_DELETE_FLAG_KEY, "false"));
}
public void hardDelete(String id) {
if (!isHardDeleteEnabled()) {
throw new BizException("승인 요청 완전삭제 기능이 비활성화되어 있습니다. (PTL_PROPERTY: " + HARD_DELETE_FLAG_KEY + ")");
}
// Approval.approvers 가 EAGER + Approver.user 가 NotFound 처리 없음 → 사라진 UserInfo 로 인해
// findById/deleteById 가 hydration 단계에서 실패하는 row 가 존재. 네이티브 SQL 로 우회.
entityManager.createNativeQuery("DELETE FROM PTL_APPROVER WHERE APPROVAL_ID = :id")
.setParameter("id", id)
.executeUpdate();
int affected = entityManager.createNativeQuery("DELETE FROM PTL_APPROVAL WHERE ID = :id")
.setParameter("id", id)
.executeUpdate();
if (affected == 0) {
throw new BizException("승인 정보를 찾을 수 없습니다. ID: " + id);
}
}
public void sendEvent(String id, ApprovalEvent event, Map<String, Object> options) {
approvalService.findById(id).ifPresent(approval -> {
options.put("authorizer", portalApprovalAuthorizer);
@@ -38,6 +38,8 @@ public class PortalApprovalUI {
@DateTimeFormat(pattern = "yyyyMMddHHmmssSSS")
private String approvalDate;
private String expectEndDate;
public String getApprovalTypeDescription() {
return approvalType != null ? approvalType.getDescription() : "";
}
@@ -6,7 +6,6 @@ import com.eactive.apim.portal.apprequest.repository.AppRequestRepository;
import com.eactive.apim.portal.approval.entity.Approval;
import com.eactive.apim.portal.approval.service.ApprovalDeployException;
import com.eactive.apim.portal.approval.statemachine.listener.ApprovalListener;
import com.eactive.apim.portal.portalproperty.service.PortalPropertyService;
import com.eactive.apim.portal.template.entity.MessageCode;
import com.eactive.apim.portal.template.service.MessageRecipient;
import com.eactive.apim.portal.template.service.MessageSendService;
@@ -19,10 +18,8 @@ import com.eactive.eai.rms.onl.apim.approval.RandomStringGenerator;
import com.eactive.eai.rms.onl.apim.approval.credential.CredentialManService;
import com.eactive.eai.rms.onl.apim.approval.credential.CredentialUI;
import com.eactive.eai.rms.onl.apim.approval.credential.CredentialUIMapper;
import com.eactive.eai.rms.onl.apim.portaluser.PortalUserUI;
import com.eactive.eai.rms.onl.common.service.AgentUtilService;
import com.eactive.eai.rms.onl.manage.authserver.client.ClientManService;
import com.eactive.eai.rms.onl.manage.authserver.client.ClientUI;
import com.eactive.eai.rms.onl.transaction.apim.ui.ApiSpecInfoUI;
import java.util.Arrays;
import java.util.Collections;
@@ -35,22 +32,12 @@ import java.util.stream.Collectors;
import java.util.stream.Stream;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang.StringUtils;
import org.apache.http.client.HttpClient;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.impl.client.HttpClientBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.client.RestTemplate;
@Service
@RequiredArgsConstructor
@@ -116,9 +103,15 @@ public class PortalAppApprovalListener implements ApprovalListener {
}
private void sendApprovalResult(AppRequest appRequest) {
com.eactive.apim.portal.portaluser.entity.PortalUser requester =
appRequest.getApproval() != null ? appRequest.getApproval().getRequester() : null;
if (requester == null) {
logger.warn("승인 결과 통지 생략 - 요청자 정보 없음. appRequestId: {}", appRequest.getId());
return;
}
MessageRecipient recipient = new MessageRecipient();
recipient.setPhone(appRequest.getApproval().getRequester().getMobileNumber());
recipient.setUserId(appRequest.getApproval().getRequester().getEmailAddr());
recipient.setPhone(requester.getMobileNumber());
recipient.setUserId(requester.getEmailAddr());
Map<String, String> params = new HashMap<>();
params.put("apiKey", appRequest.getClientName());
messageSendService.sendMessage(MessageCode.APP_REGISTER_APPROVED, recipient, params);
@@ -165,8 +158,8 @@ public class PortalAppApprovalListener implements ApprovalListener {
credentialUI.setClientname(appRequest.getClientName());
credentialUI.setScope("api");
credentialUI.setGranttypes("client_credentials");
credentialUI.setOrgid(appRequest.getOrg().getId());
credentialUI.setOrgname(appRequest.getOrg().getOrgName());
credentialUI.setOrgid(appRequest.getOrg() != null ? appRequest.getOrg().getId() : null);
credentialUI.setOrgname(appRequest.getOrg() != null ? appRequest.getOrg().getOrgName() : null);
credentialUI.setAllowedips(appRequest.getIpWhitelist());
credentialUI.setRedirecturi(appRequest.getCallbackUrl());
credentialUI.setAppIconFileId(appRequest.getAppIconFileId());
@@ -0,0 +1,78 @@
package com.eactive.eai.rms.onl.apim.portalInquiry;
import com.eactive.apim.portal.qna.entity.Inquiry;
import com.eactive.apim.portal.qna.entity.InquiryComment;
import com.eactive.eai.rms.data.entity.onl.apim.portalinquiry.PortalInquiryCommentRepository;
import com.eactive.eai.rms.data.entity.onl.apim.portalinquiry.PortalInquiryCommentService;
import com.eactive.eai.rms.data.entity.onl.apim.portalinquiry.PortalInquiryService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
/**
* RESPONDED 상태 문의글 중 아래 조건에 해당하는 경우 CLOSED로 변경한다.
* - 조건 A: 활성 댓글이 없고 답변완료일(responseDate)로부터 N일 경과
* - 조건 B: 마지막 관리자 댓글(adminYn=Y) 이후 N일간 포탈사용자(adminYn=N) 댓글 없음
*/
@Service
@Transactional(transactionManager = "transactionManagerForEMS")
@Slf4j
@RequiredArgsConstructor
public class PortalInquiryClosingService {
private static final String RESPONDED = "RESPONDED";
private static final String CLOSED = "CLOSED";
private final PortalInquiryService portalInquiryService;
private final PortalInquiryCommentService portalInquiryCommentService;
private final PortalInquiryCommentRepository inquiryCommentRepository;
public int closeExpiredInquiries(int closingDays) {
LocalDateTime threshold = LocalDateTime.now().minusDays(closingDays);
List<Inquiry> respondedInquiries = portalInquiryService.findByInquiryStatus(RESPONDED);
int closedCount = 0;
for (Inquiry inquiry : respondedInquiries) {
if (shouldClose(inquiry, threshold)) {
inquiry.setInquiryStatus(CLOSED);
portalInquiryService.save(inquiry);
closedCount++;
log.info("문의글 CLOSED 처리: id={}", inquiry.getId());
}
}
log.info("InquiryCommentClosingService: 총 {}건 CLOSED 처리 (기준={}일)", closedCount, closingDays);
return closedCount;
}
private boolean shouldClose(Inquiry inquiry, LocalDateTime threshold) {
long activeCommentCount = portalInquiryCommentService.countActiveByInquiryId(inquiry.getId());
// 댓글 없음 → 답변완료일 기준 N일 경과 여부만 확인
if (activeCommentCount == 0) {
LocalDateTime responseDate = inquiry.getResponseDate();
return responseDate != null && !responseDate.isAfter(threshold);
}
// 가장 최근 활성 댓글 확인
Optional<InquiryComment> lastComment = inquiryCommentRepository
.findTopByInquiry_IdAndDelYnOrderByCreatedDateDesc(inquiry.getId(), "N");
if (!lastComment.isPresent()) {
return false;
}
// 포탈사용자(adminYn != 'Y')가 마지막 댓글 작성자 → CLOSED 불가
if (!"Y".equals(lastComment.get().getAdminYn())) {
return false;
}
// 관리자가 마지막 댓글 작성자 → N일 경과 여부 확인
LocalDateTime lastAdminDate = lastComment.get().getCreatedDate();
return lastAdminDate != null && !lastAdminDate.isAfter(threshold);
}
}
@@ -0,0 +1,40 @@
package com.eactive.eai.rms.onl.apim.portalInquiry;
import com.eactive.apim.portal.file.entity.FileInfo;
import com.eactive.eai.data.converter.LocalDateTimeFormatters;
import com.eactive.eai.rms.onl.apim.portaluser.PortalUserUI;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.web.multipart.MultipartFile;
import java.time.LocalDateTime;
import java.util.List;
@Data
public class PortalInquiryCommentUI {
private String id;
private String inquiryId;
private String commentDetail;
private String delYn;
private String createdBy;
private String writerName;
private String lastModifiedBy;
@JsonFormat(pattern = LocalDateTimeFormatters.FORMAT_YYYYMMDDHHMMSS_14)
@DateTimeFormat(pattern = "yyyyMMddHHmmss")
private LocalDateTime createdDate;
@JsonFormat(pattern = LocalDateTimeFormatters.FORMAT_YYYYMMDDHHMMSS_14)
@DateTimeFormat(pattern = "yyyyMMddHHmmss")
private LocalDateTime lastModifiedDate;
}
@@ -0,0 +1,17 @@
package com.eactive.eai.rms.onl.apim.portalInquiry;
import com.eactive.apim.portal.qna.entity.InquiryComment;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.ReportingPolicy;
import java.util.List;
@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE)
public interface PortalInquiryCommentUIMapper {
@Mapping(target = "inquiryId", source = "inquiry.id")
PortalInquiryCommentUI toVo(InquiryComment entity);
List<PortalInquiryCommentUI> toVoList(List<InquiryComment> entities);
}
@@ -73,5 +73,23 @@ public class PortalInquiryManController extends BaseAnnotationController {
portalInquiryManService.deleteBulk(ids);
return ResponseEntity.ok().build();
}
@PostMapping(value = "/onl/apim/portalinquiry/portalInquiryMan.json", params = "cmd=LIST_COMMENT")
public ResponseEntity<List<PortalInquiryCommentUI>> selectCommentList(String id) {
return ResponseEntity.ok(portalInquiryManService.selectCommentList(id));
}
@PostMapping(value = "/onl/apim/portalinquiry/portalInquiryMan.json", params = "cmd=INSERT_COMMENT")
public ResponseEntity<String> insertComment(HttpServletRequest request, PortalInquiryCommentUI portalInquiryCommentUI) {
String userid = SessionManager.getUserId(request);
portalInquiryManService.insertComment(portalInquiryCommentUI, userid);
return ResponseEntity.ok().build();
}
@PostMapping(value = "/onl/apim/portalinquiry/portalInquiryMan.json", params = "cmd=DELETE_COMMENT")
public ResponseEntity<Void> deleteComment(String id) {
portalInquiryManService.deleteComment(id);
return ResponseEntity.ok().build();
}
}
@@ -4,10 +4,14 @@ import com.eactive.apim.portal.file.entity.FileInfo;
import com.eactive.apim.portal.file.service.FileService;
import com.eactive.apim.portal.portaluser.entity.PortalUser;
import com.eactive.apim.portal.portaluser.entity.PortalUserEnums;
import com.eactive.apim.portal.portaluser.repository.PortalUserRepository;
import com.eactive.apim.portal.qna.entity.Inquiry;
import com.eactive.apim.portal.qna.entity.InquiryComment;
import com.eactive.apim.portal.user.entity.UserInfo;
import com.eactive.eai.rms.common.base.BaseService;
import com.eactive.eai.rms.common.exception.IntegrationException;
import com.eactive.eai.rms.data.entity.man.user.UserInfoService;
import com.eactive.eai.rms.data.entity.onl.apim.portalinquiry.PortalInquiryCommentService;
import com.eactive.eai.rms.data.entity.onl.apim.portalinquiry.PortalInquirySearch;
import com.eactive.eai.rms.data.entity.onl.apim.portalinquiry.PortalInquiryService;
import com.eactive.eai.rms.onl.apim.masking.MaskingUtils;
@@ -22,27 +26,38 @@ import lombok.extern.slf4j.Slf4j;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
@Service
@Transactional(transactionManager = "transactionManagerForEMS")
@Slf4j
public class PortalInquiryManService extends BaseService {
private final PortalInquiryService portalInquiryService;
private final PortalInquiryCommentService portalInquiryCommentService;
private final PortalInquiryUIMapper portalInquiryUIMapper;
private final PortalInquiryCommentUIMapper portalInquiryCommentUIMapper;
private final UserInfoService userInfoService;
private final PortalUserRepository portalUserRepository;
private final FileService fileService;
public static final String PENDING = "PENDING";
public static final String RESPONDED = "RESPONDED";
public static final String CLOSED = "CLOSED";
public PortalInquiryManService(PortalInquiryService portalInquiryService,
PortalInquiryCommentService portalInquiryCommentService,
PortalInquiryUIMapper portalInquiryUIMapper,
PortalInquiryCommentUIMapper portalInquiryCommentUIMapper,
UserInfoService userInfoService,
PortalUserRepository portalUserRepository,
FileService fileService){
this.portalInquiryService = portalInquiryService;
this.portalInquiryCommentService = portalInquiryCommentService;
this.portalInquiryUIMapper = portalInquiryUIMapper;
this.portalInquiryCommentUIMapper = portalInquiryCommentUIMapper;
this.userInfoService = userInfoService;
this.portalUserRepository = portalUserRepository;
this.fileService = fileService;
}
@@ -55,6 +70,7 @@ public class PortalInquiryManService extends BaseService {
if (ui.getInquirer() != null) {
maskPortalInquiryUI(ui);
}
ui.setCommentCount((int) portalInquiryCommentService.countActiveByInquiryId(inquiry.getId()));
return setInquiryAttachmentStatus(ui);
});
}
@@ -141,6 +157,7 @@ public class PortalInquiryManService extends BaseService {
UserInfo userinfo = userInfoService.getById(userId);
Inquiry inquiry = portalInquiryService.getById(portalInquiryUI.getId());
validateNotClosed(inquiry);
// 답변 대기중 상태일 경우만 답변완료로 상태 변경
if(inquiry.getInquiryStatus().equals(PENDING)) {
inquiry.setInquiryStatus(RESPONDED);
@@ -161,5 +178,53 @@ public class PortalInquiryManService extends BaseService {
List<String> idList = Arrays.asList(ids.split(","));
portalInquiryService.deleteByIds(idList);
}
public List<PortalInquiryCommentUI> selectCommentList(String inquiryId) {
List<InquiryComment> comments = portalInquiryCommentService.findByInquiryId(inquiryId);
List<PortalInquiryCommentUI> result = portalInquiryCommentUIMapper.toVoList(comments);
result.forEach(ui -> ui.setWriterName(resolveWriterName(ui.getCreatedBy())));
return result;
}
private String resolveWriterName(String userId) {
if (StringUtils.isBlank(userId)) return "";
// 직원(내부 사용자) 여부 먼저 확인 → 이름 그대로 반환
Optional<UserInfo> staff = userInfoService.findById(userId);
if (staff.isPresent()) {
return staff.get().getUsername();
}
// 포털 회원이면 이름 마스킹 처리 (김수민 → 김수*)
Optional<PortalUser> portalUser = portalUserRepository.findById(userId);
if (portalUser.isPresent()) {
return MaskingUtils.maskName(portalUser.get().getUserName());
}
return userId;
}
public void insertComment(PortalInquiryCommentUI commentUI, String userId) {
Inquiry inquiry = portalInquiryService.getById(commentUI.getInquiryId());
validateNotClosed(inquiry);
InquiryComment comment = new InquiryComment();
comment.setInquiry(inquiry);
comment.setCommentDetail(commentUI.getCommentDetail());
comment.setDelYn("N");
comment.setAdminYn("Y");
portalInquiryCommentService.save(comment);
}
public void deleteComment(String commentId) {
InquiryComment comment = portalInquiryCommentService.getById(commentId);
validateNotClosed(comment.getInquiry());
comment.setDelYn("Y");
portalInquiryCommentService.save(comment);
}
private void validateNotClosed(Inquiry inquiry) {
if (CLOSED.equals(inquiry.getInquiryStatus())) {
throw new IntegrationException("답변종료되어 수정할 수 없습니다.");
}
}
}
@@ -55,4 +55,6 @@ public class PortalInquiryUI {
private LocalDateTime lastModifiedDate;
private String orgName;
private int commentCount;
}
@@ -0,0 +1,11 @@
package com.eactive.eai.rms.onl.apim.portalnotice;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
public class IncidentAffectedApiUI {
private String apiId;
private String apiName;
}
@@ -1,5 +1,13 @@
package com.eactive.eai.rms.onl.apim.portalnotice;
import com.eactive.apim.portal.djb.apistatus.incident.entity.DjbApistatusIncident;
import com.eactive.apim.portal.djb.apistatus.incident.entity.DjbApistatusIncidentApi;
import com.eactive.apim.portal.djb.apistatus.incident.entity.DjbApistatusIncidentApiId;
import com.eactive.apim.portal.djb.apistatus.incident.entity.IncidentKind;
import com.eactive.apim.portal.djb.apistatus.incident.entity.IncidentState;
import com.eactive.apim.portal.djb.apistatus.incident.repository.DjbApistatusIncidentApiRepository;
import com.eactive.apim.portal.djb.apistatus.incident.repository.DjbApistatusIncidentRepository;
import com.eactive.apim.portal.djb.apistatus.incident.repository.DjbApistatusIncidentTimelineRepository;
import com.eactive.apim.portal.file.entity.FileDetail;
import com.eactive.apim.portal.file.entity.FileInfo;
import com.eactive.apim.portal.file.service.FileService;
@@ -23,9 +31,13 @@ import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
@Service
@Transactional(transactionManager = "transactionManagerForEMS")
@@ -34,6 +46,9 @@ public class PortalNoticeManService extends BaseService {
private final PortalNoticeService portalNoticeService;
private final PortalNoticeUIMapper portalNoticeUIMapper;
private final FileService fileService;
private final DjbApistatusIncidentRepository incidentRepository;
private final DjbApistatusIncidentApiRepository incidentApiRepository;
private final DjbApistatusIncidentTimelineRepository incidentTimelineRepository;
private String decodeString(String value) {
if (ContainerUtil.get() == ContainerUtil.TOMCAT) {
@@ -50,10 +65,27 @@ public class PortalNoticeManService extends BaseService {
@Autowired
public PortalNoticeManService(PortalNoticeService portalNoticeService,
PortalNoticeUIMapper portalNoticeUIMapper,
FileService fileService){
FileService fileService,
DjbApistatusIncidentRepository incidentRepository,
DjbApistatusIncidentApiRepository incidentApiRepository,
DjbApistatusIncidentTimelineRepository incidentTimelineRepository){
this.portalNoticeService = portalNoticeService;
this.portalNoticeUIMapper = portalNoticeUIMapper;
this.fileService = fileService;
this.incidentRepository = incidentRepository;
this.incidentApiRepository = incidentApiRepository;
this.incidentTimelineRepository = incidentTimelineRepository;
}
private static boolean isIncidentKind(String noticeType) {
return PortalNoticeUI.NOTICE_TYPE_INCIDENT.equals(noticeType)
|| PortalNoticeUI.NOTICE_TYPE_MAINTENANCE.equals(noticeType);
}
private static IncidentKind toKind(String noticeType) {
if (PortalNoticeUI.NOTICE_TYPE_INCIDENT.equals(noticeType)) return IncidentKind.INCIDENT;
if (PortalNoticeUI.NOTICE_TYPE_MAINTENANCE.equals(noticeType)) return IncidentKind.MAINTENANCE;
return null;
}
@@ -91,9 +123,39 @@ public class PortalNoticeManService extends BaseService {
PortalNoticeUI ui = portalNoticeUIMapper.toVo(portalNotice);
setFileInfo(portalNotice, ui);
populateIncident(portalNotice, ui);
return ui;
}
private void populateIncident(PortalNotice portalNotice, PortalNoticeUI ui) {
if (!isIncidentKind(portalNotice.getNoticeType())) {
ui.setAffectedApis(Collections.emptyList());
return;
}
Optional<DjbApistatusIncident> incidentOpt = incidentRepository.findByNoticeId(portalNotice.getId());
if (!incidentOpt.isPresent()) {
ui.setAffectedApis(Collections.emptyList());
return;
}
DjbApistatusIncident incident = incidentOpt.get();
ui.setIncidentId(incident.getIncidentId());
ui.setSummary(incident.getSummary());
ui.setStartedAt(incident.getStartedAt());
ui.setEndAt(incident.getEndAt());
ui.setState(incident.getState() == null ? null : incident.getState().name());
ui.setPreviousState(incident.getPreviousState() == null ? null : incident.getPreviousState().name());
List<IncidentAffectedApiUI> apis = incidentApiRepository
.findByIncidentIdOrderByApiId(incident.getIncidentId()).stream()
.map(api -> {
IncidentAffectedApiUI vo = new IncidentAffectedApiUI();
vo.setApiId(api.getApiId());
vo.setApiName(api.getApiName());
return vo;
}).collect(Collectors.toList());
ui.setAffectedApis(apis);
}
public void insert(PortalNoticeUI portalNoticeUI) throws IOException {
//portalNoticeUI.setNoticeSubject(decodeString(portalNoticeUI.getNoticeSubject()));
//portalNoticeUI.setNoticeDetail(decodeString(StringEscapeUtils.unescapeHtml(portalNoticeUI.getNoticeDetail())));
@@ -108,6 +170,61 @@ public class PortalNoticeManService extends BaseService {
}
portalNoticeService.save(portalNotice);
if (isIncidentKind(portalNoticeUI.getNoticeType())) {
persistIncident(portalNoticeUI, portalNotice, true);
}
}
private void persistIncident(PortalNoticeUI portalNoticeUI, PortalNotice portalNotice, boolean isCreate) {
IncidentKind kind = toKind(portalNoticeUI.getNoticeType());
DjbApistatusIncident incident = incidentRepository.findByNoticeId(portalNotice.getId())
.orElseGet(DjbApistatusIncident::new);
incident.setKind(kind);
incident.setTitle(portalNotice.getNoticeSubject());
incident.setSummary(portalNoticeUI.getSummary());
incident.setStartedAt(portalNoticeUI.getStartedAt() != null
? portalNoticeUI.getStartedAt() : LocalDateTime.now());
incident.setEndAt(portalNoticeUI.getEndAt());
incident.setDetectedBy("MANUAL");
incident.setNoticeId(portalNotice.getId());
incident.setDraftYn("N");
incident.setFixYn(StringUtils.defaultIfBlank(portalNotice.getFixYn(), "N"));
if (kind == IncidentKind.INCIDENT) {
IncidentState newState = StringUtils.isNotBlank(portalNoticeUI.getState())
? IncidentState.valueOf(portalNoticeUI.getState())
: IncidentState.INVESTIGATING;
if (!isCreate && incident.getState() != newState) {
incident.setPreviousState(incident.getState());
}
incident.setState(newState);
} else {
incident.setState(null);
incident.setPreviousState(null);
}
DjbApistatusIncident saved = incidentRepository.save(incident);
// 영향 API 동기화 — 단순 전체 삭제 후 재삽입
incidentApiRepository.deleteByIncidentId(saved.getIncidentId());
List<IncidentAffectedApiUI> affected = portalNoticeUI.getAffectedApis();
if (affected != null && !affected.isEmpty()) {
List<DjbApistatusIncidentApi> rows = new ArrayList<>();
for (IncidentAffectedApiUI src : affected) {
if (StringUtils.isBlank(src.getApiId())) continue;
DjbApistatusIncidentApi api = new DjbApistatusIncidentApi();
api.setIncidentId(saved.getIncidentId());
api.setApiId(src.getApiId());
api.setApiName(src.getApiName());
rows.add(api);
}
if (!rows.isEmpty()) {
incidentApiRepository.saveAll(rows);
}
}
}
private void handleFileUpload(PortalNoticeUI portalNoticeUI, PortalNotice portalNotice) throws IOException {
@@ -131,6 +248,7 @@ public class PortalNoticeManService extends BaseService {
portalNoticeUI.setNoticeDetail(StringEscapeUtils.unescapeHtml(portalNoticeUI.getNoticeDetail()));
PortalNotice portalNotice = portalNoticeService.getById(portalNoticeUI.getId());
String previousNoticeType = portalNotice.getNoticeType();
if (portalNoticeUI.getFiles() != null && !portalNoticeUI.getFiles().isEmpty()) {
handleFileUpload(portalNoticeUI, portalNotice);
@@ -141,11 +259,27 @@ public class PortalNoticeManService extends BaseService {
portalNoticeUIMapper.updateToEntity(portalNoticeUI, portalNotice);
portalNoticeService.save(portalNotice);
if (isIncidentKind(portalNotice.getNoticeType())) {
persistIncident(portalNoticeUI, portalNotice, false);
} else if (isIncidentKind(previousNoticeType)) {
// 일반 공지로 유형 변경 → 기존 incident 정리
deleteIncidentIfExists(portalNotice.getId());
}
}
private void deleteIncidentIfExists(String noticeId) {
incidentRepository.findByNoticeId(noticeId).ifPresent(incident -> {
incidentApiRepository.deleteByIncidentId(incident.getIncidentId());
incidentTimelineRepository.deleteByIncidentId(incident.getIncidentId());
incidentRepository.delete(incident);
});
}
public void delete(String id) {
PortalNotice portalNotice = portalNoticeService.getById(id);
Optional.ofNullable(portalNotice.getFileId()).ifPresent(fileService::deleteFile);
deleteIncidentIfExists(id);
portalNoticeService.deleteById(id);
}
@@ -8,11 +8,16 @@ import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.web.multipart.MultipartFile;
import java.time.LocalDateTime;
import java.util.List;
@Data
public class PortalNoticeUI {
public static final String NOTICE_TYPE_NORMAL = "1";
public static final String NOTICE_TYPE_MAINTENANCE = "2";
public static final String NOTICE_TYPE_INCIDENT = "3";
private String id;
private String noticeSubject;
@@ -53,4 +58,24 @@ public class PortalNoticeUI {
private boolean hasAttachment; // 첨부파일 여부
private boolean fileDeleted; // 파일 삭제 여부
// ───── 장애/점검(djb_apistatus_incident) 연동 필드 ─────
private Long incidentId;
private String summary;
// HTML <input type="datetime-local"> 는 'T' 구분자 형식 (예: 2026-06-16T10:29).
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm")
@DateTimeFormat(pattern = "yyyy-MM-dd'T'HH:mm")
private LocalDateTime startedAt;
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm")
@DateTimeFormat(pattern = "yyyy-MM-dd'T'HH:mm")
private LocalDateTime endAt;
private String state; // INCIDENT 한정 (INVESTIGATING/IDENTIFIED/MONITORING/RESOLVED/CANCELED)
private String previousState; // 직전 상태
private List<IncidentAffectedApiUI> affectedApis;
}
@@ -3,7 +3,9 @@ package com.eactive.eai.rms.onl.apim.portalnotice;
import com.eactive.apim.portal.portalNotice.entity.PortalNotice;
import org.mapstruct.*;
@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE)
@Mapper(componentModel = "spring",
unmappedTargetPolicy = ReportingPolicy.IGNORE,
unmappedSourcePolicy = ReportingPolicy.IGNORE)
public interface PortalNoticeUIMapper {
PortalNoticeUI toVo(PortalNotice entity);
@@ -27,6 +27,8 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
@@ -48,6 +50,9 @@ public class PortalOrgManService extends BaseService {
private final PortalUserCorpManagerUIMapper portalUserCorpManagerUIMapper;
private final FileService fileService;
@PersistenceContext(unitName = "entityManagerFactoryForEMS")
private EntityManager entityManager;
public Page<PortalOrgUI> selectList(Pageable pageable, PortalOrgUISearch portalOrgUISearch) {
Page<PortalOrg> portalOrg = portalOrgService.findAll(pageable, portalOrgUISearch);
return portalOrg.map(entity -> convertToUIWithMaskOption(entity, true));
@@ -146,19 +151,37 @@ public class PortalOrgManService extends BaseService {
throw new BizException("삭제(탈퇴) 상태인 법인만 완전삭제할 수 있습니다.");
}
List<String> dependencies = new ArrayList<>();
// 사용자(PortalUser)는 별도 화면에서 정리해야 하므로 cascade 하지 않고 차단
long userCount = portalUserService.countByOrgId(id);
if (userCount > 0) dependencies.add("사용자 " + userCount + "");
long appRequestCount = portalOrgService.countAppRequestByOrgId(id);
if (appRequestCount > 0) dependencies.add("앱 신청 " + appRequestCount + "");
if (!dependencies.isEmpty()) {
throw new BizException("의존 데이터가 존재하여 완전삭제할 수 없습니다: " + String.join(", ", dependencies));
if (userCount > 0) {
throw new BizException("의존 데이터가 존재하여 완전삭제할 수 없습니다: 사용자 " + userCount + "");
}
// 앱 신청(AppRequest) + 연결된 승인(Approval/Approver) 은 cascade 삭제
cascadeDeleteAppRequestsByOrg(id);
portalOrgService.deleteById(id);
}
private void cascadeDeleteAppRequestsByOrg(String orgId) {
// 1) 해당 org 의 app_request 가 참조하는 approval 의 approver 제거
entityManager.createNativeQuery(
"DELETE FROM PTL_APPROVER WHERE APPROVAL_ID IN " +
"(SELECT APPROVAL_ID FROM PTL_APP_REQUEST WHERE ORG_ID = :orgId AND APPROVAL_ID IS NOT NULL)"
).setParameter("orgId", orgId).executeUpdate();
// 2) approval 제거
entityManager.createNativeQuery(
"DELETE FROM PTL_APPROVAL WHERE ID IN " +
"(SELECT APPROVAL_ID FROM PTL_APP_REQUEST WHERE ORG_ID = :orgId AND APPROVAL_ID IS NOT NULL)"
).setParameter("orgId", orgId).executeUpdate();
// 3) app_request 제거
entityManager.createNativeQuery(
"DELETE FROM PTL_APP_REQUEST WHERE ORG_ID = :orgId"
).setParameter("orgId", orgId).executeUpdate();
}
public Map<String, Object> selectDetailWithUsers(String id) {
return getDetailWithUsers(id, true);
}
@@ -1,19 +1,5 @@
package com.eactive.eai.rms.onl.common.service.ums;
import com.eactive.apim.portal.template.entity.MessageRequest;
import com.eactive.apim.portal.template.repository.MessageRequestRepository;
import com.eactive.ext.kjb.ums.KjbEmailSendModule;
import com.eactive.ext.kjb.ums.KjbUmsService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionTemplate;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import java.time.LocalDateTime;
import java.util.List;
import java.util.concurrent.LinkedBlockingQueue;
@@ -21,6 +7,23 @@ import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import javax.annotation.PostConstruct;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionTemplate;
import com.eactive.apim.portal.template.entity.MessageRequest;
import com.eactive.apim.portal.template.repository.MessageRequestRepository;
import com.eactive.eai.rms.common.datasource.DataSourceContextHolder;
import com.eactive.eai.rms.common.datasource.DataSourceTypeManager;
import com.eactive.eai.rms.ext.djb.ums.service.UmsService;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@@ -40,13 +43,17 @@ public class UmsDispatchService {
private EntityManager entityManager;
private final MessageRequestRepository messageRequestRepository;
private final UmsService umsService;
@Autowired
public UmsDispatchService(
@Qualifier("transactionManagerForEMS") PlatformTransactionManager transactionManager,
MessageRequestRepository messageRequestRepository
MessageRequestRepository messageRequestRepository,
UmsService umsService
) {
this.messageRequestRepository = messageRequestRepository;
this.umsService = umsService;
this.executorService = new ThreadPoolExecutor(
CORE_POOL_SIZE,
@@ -67,15 +74,8 @@ public class UmsDispatchService {
// KjbEmailSendModule.getInstance().setRepository(messageRequestRepository);
log.info("UmsDispatchService initialized - KjbUmsService and KjbEmailSendModule ready via singleton");
}
// private KjbUmsService getUmsService() {
// return KjbUmsService.getInstance();
// }
//
// private KjbEmailSendModule getEmailModule() {
// return KjbEmailSendModule.getInstance();
// }
//
@SuppressWarnings("unchecked")
@Transactional(transactionManager = "transactionManagerForEMS")
public void processMessages() {
@@ -112,7 +112,7 @@ public class UmsDispatchService {
log.info("Starting to submit {} messages for processing", messagesToProcess.size());
for (MessageRequest message : messagesToProcess) {
//submitMessageTask(message);
submitMessageTask(message);
}
} else {
log.info("No pending messages found");
@@ -123,66 +123,42 @@ public class UmsDispatchService {
}
// private void submitMessageTask(MessageRequest message) {
// int queueSize = executorService.getQueue().size();
// log.debug("Current queue size before submission: {}", queueSize);
//
// executorService.submit(() -> {
// try {
// transactionTemplate.execute(status -> {
// try {
// // 광주은행에서는 사용하지 않는 메소드
//// processMessage(message);
//
// if (KjbUmsService.isAllowChannel(message)) {
// getUmsService().send(message);
// }
//
// if (KjbEmailSendModule.isAllowChannel(message)) {
// getEmailModule().sendEmail(message);
// }
//
// return null;
// } catch (Exception e) {
// log.error("Failed to process message: {} - Error: {}",
// message.getId(), e.getMessage(), e);
// updateMessageStatus(message, "FAILED");
// status.setRollbackOnly();
// return null;
// }
// });
// } catch (Exception e) {
// log.error("Transaction execution failed for message: {} - Error: {}",
// message.getId(), e.getMessage(), e);
// }
// });
// }
private void submitMessageTask(MessageRequest message) {
int queueSize = executorService.getQueue().size();
log.debug("Current queue size before submission: {}", queueSize);
/**
* kbank 방식으로 광주은행에서는 사용하지 않음
* @param message
* @throws IOException
*/
// @Deprecated
// public void processMessage(MessageRequest message) throws IOException {
// try {
// String standardMessage = messageFactory.createMessage(message);
// log.debug("Processing message ID: {} - Type: {}", message.getId(), message.getMessageType());
//
// umsSender.sendMessage(standardMessage);
// updateMessageStatus(message, "SENT");
// log.debug("Successfully processed and sent message ID: {}", message.getId());
// } catch (Exception e) {
// log.error("Message processing failed for ID: {} - Error: {}",
// message.getId(), e.getMessage(), e);
// updateMessageStatus(message, "FAILED");
// throw e;
// }
// }
executorService.submit(() -> {
try {
transactionTemplate.execute(status -> {
try {
umsService.send(message);
updateMessageStatus(message, "SENT");
return null;
} catch (Exception e) {
// checked exception을 RuntimeException으로 감싸 전파 → TransactionTemplate이 rollback 후 re-throw
throw new RuntimeException(e);
}
});
} catch (Exception e) {
log.error("Failed to process message: {} - Error: {}", message.getId(), e.getMessage(), e);
try {
transactionTemplate.execute(s -> {
updateMessageStatus(message, "FAILED");
return null;
});
} catch (Exception ex) {
log.error("Failed to update FAIL status for message: {}", message.getId(), ex);
}
}
});
}
public void updateMessageStatus(MessageRequest message, String status) {
message.setRequestStatus(status);
message.setSentDate(LocalDateTime.now());
entityManager.merge(message);
entityManager.flush();
}
}
@@ -1,79 +0,0 @@
package com.eactive.eai.rms.onl.common.util;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.commons.lang3.StringUtils;
import org.apache.log4j.Logger;
import com.eactive.eai.rms.onl.common.service.OnlAgentUtilServiceImpl;
import io.kubernetes.client.openapi.ApiClient;
import io.kubernetes.client.openapi.Configuration;
import io.kubernetes.client.openapi.apis.CoreV1Api;
import io.kubernetes.client.openapi.models.V1EndpointAddress;
import io.kubernetes.client.openapi.models.V1EndpointSubset;
import io.kubernetes.client.openapi.models.V1Endpoints;
import io.kubernetes.client.util.ClientBuilder;
public class K8sUtil {
private static final Logger logger = Logger.getLogger(K8sUtil.class);
public static List<Map<String, String>> getServerInfoByK8sUrl(String namespace, String serviceName, int port, String extUrl) throws Exception {
List<Map<String, String>> resultList = getServerInfoByK8sApi(namespace,serviceName);
for (Map<String,String> map : resultList) {
String EAISEVRINSTNCNAME = map.get("EAISEVRINSTNCNAME");
String EAISEVRIP = map.get("EAISEVRIP");
Map<String, String> serverInfo = new HashMap<>();
serverInfo.put(OnlAgentUtilServiceImpl.FIELD_URL, new StringBuilder().append("http://" + EAISEVRIP + ":"+String.valueOf(port) + extUrl).toString());
serverInfo.put(OnlAgentUtilServiceImpl.FIELD_INST_NAME, EAISEVRINSTNCNAME);
}
return resultList;
}
public static List<Map<String, String>> getServerInfoByK8sApi(String namespace, String serviceName) throws Exception {
logger.info("K8sAgentUtilService getServerInfoByK8sApi start.");
List<Map<String, String>> resultList = new ArrayList<>();
try {
ApiClient client = ClientBuilder.cluster().build();
Configuration.setDefaultApiClient(client);
if (StringUtils.isBlank(namespace)) {
new RuntimeException("Can't find kubernetes_namespace in Engine Property");
}
if (StringUtils.isBlank(serviceName)) {
new RuntimeException("Can't find kubernetes_servicename in Engine Property");
}
logger.info("getServerInfoByK8sApi Codes [ namespace: " + namespace + ", serviceName:" + serviceName + " ]");
V1Endpoints endpoints = new CoreV1Api().readNamespacedEndpoints(serviceName, namespace, null);
for (V1EndpointSubset subset : endpoints.getSubsets()) {
for (V1EndpointAddress address : subset.getAddresses()) {
Map<String, String> serverInfo = new HashMap<>();
serverInfo.put("EAISEVRINSTNCNAME", address.getTargetRef().getName());
serverInfo.put("EAISEVRIP", address.getIp());
resultList.add(serverInfo);
}
}
logger.info("getServerInfoByK8sApi resultList [ " + resultList.toString() + " ]");
} catch (Exception e) {
logger.error(e);
throw new RuntimeException("K8S Mode로 AgentUtilService를 기동중 실패하였습니다.", e);
}
return resultList;
}
}
@@ -85,4 +85,12 @@ public class InflowAdapterControlManController extends OnlBaseAnnotationControll
return new ModelAndView("jsonView", resultMap);
}
@RequestMapping(value = "/onl/admin/inflow/inflowAdapterControlMan.json", params = "cmd=LIST_BUCKET_STATUS")
public ModelAndView getBucketStatus(HttpServletRequest request, HttpServletResponse response, String adapterId) {
List<Map<String, Object>> statusList = service.getAdapterBucketStatus(adapterId);
Map<String, Object> resultMap = new HashMap<>();
resultMap.put("servers", statusList);
return new ModelAndView("jsonView", resultMap);
}
}
@@ -83,4 +83,12 @@ public class InflowClientControlManController extends OnlBaseAnnotationControlle
return new ModelAndView("jsonView", resultMap);
}
@RequestMapping(value = "/onl/admin/inflow/inflowClientControlMan.json", params = "cmd=LIST_BUCKET_STATUS")
public ModelAndView getBucketStatus(HttpServletRequest request, HttpServletResponse response, String clientId) {
List<Map<String, Object>> statusList = service.getClientBucketStatus(clientId);
Map<String, Object> resultMap = new HashMap<>();
resultMap.put("servers", statusList);
return new ModelAndView("jsonView", resultMap);
}
}
@@ -1,12 +1,21 @@
package com.eactive.eai.rms.onl.manage.inflow.inflow;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import com.eactive.eai.data.entity.onl.inflow.InflowControl;
import com.eactive.eai.data.entity.onl.inflow.InflowControlId;
@@ -14,10 +23,13 @@ import com.eactive.eai.rms.common.base.BaseService;
import com.eactive.eai.rms.common.combo.ComboService;
import com.eactive.eai.rms.data.entity.onl.inflow.InflowControlService;
import com.eactive.eai.rms.data.entity.onl.inflow.InflowControlServiceDto;
import com.eactive.eai.rms.onl.common.service.EaiServerInfoService;
@Service
public class InflowControlManService extends BaseService {
private static final Logger logger = LoggerFactory.getLogger(InflowControlManService.class);
private static final String ADAPTER_TYPE_INFLOW = "01";
private static final String INTERFACE_TYPE_INFLOW = "02";
private static final String CLIENT_TYPE_INFLOW = "03";
@@ -32,6 +44,11 @@ public class InflowControlManService extends BaseService {
@Autowired
private InflowControlManMapper mapper;
@Autowired
private EaiServerInfoService eaiServerInfoService;
private RestTemplate restTemplate = new RestTemplate();
// 어댑터 유량제어
public Page<InflowControlManUI> selectAdapterList(Pageable pageable, String searchName) {
Page<InflowControlServiceDto> dtoList = service.findAllForAdapter(pageable, searchName);
@@ -123,6 +140,64 @@ public class InflowControlManService extends BaseService {
InflowControlId id = toId(CLIENT_TYPE_INFLOW, name);
service.findById(id).ifPresent(service::delete);
}
@SuppressWarnings("unchecked")
public List<Map<String, Object>> getClientBucketStatus(String clientId) {
return getBucketStatus("client", clientId, "클라이언트가 로드되지 않음", "클라이언트 버킷 상태 조회 실패");
}
@SuppressWarnings("unchecked")
public List<Map<String, Object>> getAdapterBucketStatus(String adapterId) {
return getBucketStatus("adapter", adapterId, "어댑터가 로드되지 않음", "어댑터 버킷 상태 조회 실패");
}
@SuppressWarnings("unchecked")
public List<Map<String, Object>> getInterfaceBucketStatus(String interfaceId) {
return getBucketStatus("interface", interfaceId, "인터페이스가 로드되지 않음", "인터페이스 버킷 상태 조회 실패");
}
@SuppressWarnings("unchecked")
private List<Map<String, Object>> getBucketStatus(String target, String targetId, String noDataMessage, String logPrefix) {
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/%s/%s/bucket-status",
serverIp, serverPort, target, targetId);
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", noDataMessage);
}
} catch (Exception e) {
logger.warn("{} - server: {}, error: {}", logPrefix, serverName, e.getMessage());
serverStatus.put("status", "offline");
serverStatus.put("message", "서버 연결 실패");
}
result.add(serverStatus);
}
return result;
}
private InflowControlId toId(InflowControlManUI ui) {
@@ -83,4 +83,12 @@ public class InflowInterfaceControlManController extends OnlBaseAnnotationContro
return new ModelAndView("jsonView", resultMap);
}
@RequestMapping(value = "/onl/admin/inflow/inflowInterfaceControlMan.json", params = "cmd=LIST_BUCKET_STATUS")
public ModelAndView getBucketStatus(HttpServletRequest request, HttpServletResponse response, String interfaceId) {
List<Map<String, Object>> statusList = service.getInterfaceBucketStatus(interfaceId);
Map<String, Object> resultMap = new HashMap<>();
resultMap.put("servers", statusList);
return new ModelAndView("jsonView", resultMap);
}
}
@@ -565,7 +565,7 @@ public class ApiInterfaceService extends OnlBaseService {
restOption.setMethod(outboundHttpMethod);
restOption.setExtraPath(outboundRestPath);
String pathType = outboundRestPath.matches(".*\\{[_a-zA-Z]+\\}.*") ? "variableUrlRequest" : "simpleRequest";
String pathType = outboundRestPath.matches(".*\\{.+\\}.*") ? "variableUrlRequest" : "simpleRequest";
restOption.setType(pathType);
if (StringUtils.isNotBlank(contentType)) {
@@ -400,7 +400,7 @@ public class SimpleInterfaceService extends OnlBaseService {
jsonObject.put("method", outboundHttpMethod);
jsonObject.put("extraPath", outboundRestPath);
String pathType = outboundRestPath.matches(".*\\{[a-zA-Z]+\\}.*") ? "variableUrlRequest" : "simpleRequest";
String pathType = outboundRestPath.matches(".*\\{.+\\}.*") ? "variableUrlRequest" : "simpleRequest";
jsonObject.put("type", pathType);
restOption = jsonObject.toString();
return restOption;
@@ -15,6 +15,7 @@ import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import com.eactive.eai.rms.ext.djb.job.ApiStatsHourlyAggregationJob;
import com.eactive.ext.kjb.statistics.job.DailyToMonthlyAggregationJob;
import com.eactive.ext.kjb.statistics.job.HourlyToDailyAggregationJob;
import com.eactive.ext.kjb.statistics.job.MonthlyToYearlyAggregationJob;
@@ -33,12 +34,62 @@ public class ApiStatsAggregationController {
private final HourlyToDailyAggregationJob hourlyToDailyAggregationJob;
private final DailyToMonthlyAggregationJob dailyToMonthlyAggregationJob;
private final MonthlyToYearlyAggregationJob monthlyToYearlyAggregationJob;
private final ApiStatsHourlyAggregationJob apiStatsHourlyAggregationJob;
@GetMapping(value = "/onl/kjb/statistics/apiStatsAggregationMan.view")
public String view() {
return "/onl/kjb/statistics/apiStatsAggregationMan";
}
/**
* 거래로그→시간별 통계 집계 수동 실행
* @param targetDate 집계 대상 날짜 (yyyyMMdd 형식, 예: 20250120)
* @return 처리 결과
*/
@PostMapping(value = "/onl/kjb/statistics/apiStatsAggregationMan.json", params = "cmd=AGGREGATION_HOUR")
public ResponseEntity<Map<String, Object>> executeAggregationHourly(
@RequestParam(required = false) String targetDate) {
Map<String, Object> result = new HashMap<>();
try {
// 날짜 입력 검증
if (targetDate == null || targetDate.trim().isEmpty()) {
log.error("targetDate 미입력");
result.put("success", false);
result.put("message", "대상 날짜를 입력하세요. (예: 20250120)");
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(result);
}
LocalDate date;
try {
date = LocalDate.parse(targetDate, DateTimeFormatter.ofPattern("yyyyMMdd"));
} catch (DateTimeParseException e) {
log.error("잘못된 날짜 형식: {}", targetDate);
result.put("success", false);
result.put("message", "잘못된 날짜 형식입니다. yyyyMMdd 형식으로 입력하세요. (예: 20250120)");
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(result);
}
log.info("거래로그→시간별 통계 집계 수동 실행 시작. targetDate: {}", date);
int count = apiStatsHourlyAggregationJob.executeManual(date);
result.put("success", true);
result.put("message", "거래로그→시간별 통계 집계가 완료되었습니다.");
result.put("targetDate", date.format(DateTimeFormatter.ofPattern("yyyyMMdd")));
result.put("processedCount", count);
return ResponseEntity.ok(result);
} catch (Exception e) {
log.error("거래로그→시간별 통계 집계 실행 중 오류 발생", e);
result.put("success", false);
result.put("message", "집계 실행 중 오류가 발생했습니다: " + e.getMessage());
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(result);
}
}
/**
* 시간별→일별 통계 집계 수동 실행
* @param targetDate 집계 대상 날짜 (yyyyMMdd 형식, 예: 20250120)
@@ -138,9 +138,9 @@ public class DailyToMonthlyAggregationJob implements Job {
qd.seq900SystemErrCnt.sum().as("seq900SystemErrCnt"),
qd.seq900BizErrCnt.sum().as("seq900BizErrCnt"),
Expressions.cases()
.when(qd.successCnt.sum().gt(0))
.then(qd.avgRespTime.multiply(qd.successCnt).sum()
.divide(qd.successCnt.sum()).castToNum(BigDecimal.class))
.when(qd.totalCnt.sum().gt(0))
.then(qd.avgRespTime.multiply(qd.totalCnt).sum()
.divide(qd.totalCnt.sum()).castToNum(BigDecimal.class))
.otherwise((BigDecimal) null)
.as("avgRespTime"),
qd.minRespTime.min().castToNum(BigDecimal.class).as("minRespTime"),
@@ -133,9 +133,9 @@ public class HourlyToDailyAggregationJob implements Job {
qh.seq900SystemErrCnt.sum().as("seq900SystemErrCnt"),
qh.seq900BizErrCnt.sum().as("seq900BizErrCnt"),
Expressions.cases()
.when(qh.successCnt.sum().gt(0))
.then(qh.avgRespTime.multiply(qh.successCnt).sum()
.divide(qh.successCnt.sum()).castToNum(BigDecimal.class))
.when(qh.totalCnt.sum().gt(0))
.then(qh.avgRespTime.multiply(qh.totalCnt).sum()
.divide(qh.totalCnt.sum()).castToNum(BigDecimal.class))
.otherwise((BigDecimal) null)
.as("avgRespTime"),
qh.minRespTime.min().castToNum(BigDecimal.class).as("minRespTime"),
@@ -136,9 +136,9 @@ public class MonthlyToYearlyAggregationJob implements Job {
qm.seq900SystemErrCnt.sum().as("seq900SystemErrCnt"),
qm.seq900BizErrCnt.sum().as("seq900BizErrCnt"),
Expressions.cases()
.when(qm.successCnt.sum().gt(0))
.then(qm.avgRespTime.multiply(qm.successCnt).sum()
.divide(qm.successCnt.sum()).castToNum(BigDecimal.class))
.when(qm.totalCnt.sum().gt(0))
.then(qm.avgRespTime.multiply(qm.totalCnt).sum()
.divide(qm.totalCnt.sum()).castToNum(BigDecimal.class))
.otherwise((BigDecimal) null)
.as("avgRespTime"),
qm.minRespTime.min().castToNum(BigDecimal.class).as("minRespTime"),
@@ -36,9 +36,8 @@ public class ApiStatsExcelExportService {
private static final String[] COLUMN_HEADERS = {
"통계시간", "API명", "인스턴스", "업무구분", "클라이언트ID",
"Inbound Adapter", "Outbound Adapter",
"총건수", "성공건수", "Timeout건수", "시스템오류건수", "업무오류건수",
"Seq900 Timeout", "Seq900 시스템오류", "Seq900 업무오류",
"평균응답시간", "최소응답시간", "최대응답시간", "P50응답시간", "P95응답시간"
"총건수", "성공건수", "Timeout건수", "시스템오류건수",
"평균응답시간", "최소응답시간", "최대응답시간"
};
/**
@@ -193,17 +192,13 @@ public class ApiStatsExcelExportService {
createLongCell(row, colNum++, data.getSuccessCnt(), numberStyle);
createLongCell(row, colNum++, data.getTimeoutCnt(), numberStyle);
createLongCell(row, colNum++, data.getSystemErrCnt(), numberStyle);
createLongCell(row, colNum++, data.getBizErrCnt(), numberStyle);
createLongCell(row, colNum++, data.getSeq900TimeoutCnt(), numberStyle);
createLongCell(row, colNum++, data.getSeq900SystemErrCnt(), numberStyle);
createLongCell(row, colNum++, data.getSeq900BizErrCnt(), numberStyle);
// 소수점 컬럼 (15-19)
createDecimalCell(row, colNum++, data.getAvgRespTime(), decimalStyle);
createDecimalCell(row, colNum++, data.getMinRespTime(), decimalStyle);
createDecimalCell(row, colNum++, data.getMaxRespTime(), decimalStyle);
createDecimalCell(row, colNum++, data.getP50RespTime(), decimalStyle);
createDecimalCell(row, colNum++, data.getP95RespTime(), decimalStyle);
}
/**
@@ -18,6 +18,8 @@ public class ApiStatsSearch {
private String searchClientId;
private String searchInboundAdapter;
private String searchOutboundAdapter;
private String searchOrgName;
private String searchType;
/** 파싱된 시작시간 */
private LocalDateTime parsedStartDateTime;
@@ -14,6 +14,7 @@ public class ApiStatsUI {
private String gwInstanceId;
private String bizDivCode;
private String clientId;
private String orgName;
private String inboundAdapter;
private String outboundAdapter;
@@ -22,15 +23,13 @@ public class ApiStatsUI {
private Long successCnt;
private Long timeoutCnt;
private Long systemErrCnt;
private Long bizErrCnt;
private Long seq900TimeoutCnt;
private Long seq900SystemErrCnt;
private Long seq900BizErrCnt;
// 응답시간 메트릭
private BigDecimal avgRespTime;
private BigDecimal minRespTime;
private BigDecimal maxRespTime;
private BigDecimal p50RespTime;
private BigDecimal p95RespTime;
//성공율,실패율
private BigDecimal successRate;
private BigDecimal failRate;
}