diff --git a/WebContent/WEB-INF/springapp-servlet.xml b/WebContent/WEB-INF/springapp-servlet.xml index bdcc5db..f3a55c6 100644 --- a/WebContent/WEB-INF/springapp-servlet.xml +++ b/WebContent/WEB-INF/springapp-servlet.xml @@ -140,4 +140,5 @@ + diff --git a/WebContent/css/editor-content.css b/WebContent/css/editor-content.css new file mode 100644 index 0000000..c144110 --- /dev/null +++ b/WebContent/css/editor-content.css @@ -0,0 +1,334 @@ +/** + * Editor Content Styles (editor-content.css) + * + * Summernote 에디터로 작성된 HTML 콘텐츠의 공통 스타일 + * 관리자포탈/개발자포탈 양쪽에서 동일하게 사용 + * + * 사용법: + * - 관리자포탈: Summernote .note-editable에 .editor-content 클래스 추가 + * - 개발자포탈: 콘텐츠 wrapper에 .editor-content 클래스 추가 + * + * @version 1.0.0 + * @date 2025-12-30 + */ + +/* ========================================================================== + CSS Reset + 기본 설정 + ========================================================================== */ + +.editor-content { + all: revert; + font-family: 'Noto Sans KR', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif !important; + font-size: 16px !important; + font-weight: 400 !important; + line-height: 1.8 !important; + color: #1A1A2E !important; + word-wrap: break-word; + text-align: left !important; +} + +/* ========================================================================== + 헤딩 (Headings) + ========================================================================== */ + +.editor-content h1 { + font-size: 20px !important; + font-weight: 700 !important; + margin: 32px 0 16px !important; + line-height: 1.4 !important; + color: #1A1A2E !important; +} + +.editor-content h1:first-child { + margin-top: 0 !important; +} + +.editor-content h2 { + font-size: 18px !important; + font-weight: 700 !important; + margin: 24px 0 8px !important; + line-height: 1.4 !important; + color: #1A1A2E !important; +} + +.editor-content h3 { + font-size: 16px !important; + font-weight: 600 !important; + margin: 16px 0 8px !important; + line-height: 1.4 !important; + color: #1A1A2E !important; +} + +.editor-content h4, +.editor-content h5, +.editor-content h6 { + font-size: 16px !important; + font-weight: 600 !important; + margin: 16px 0 8px !important; + line-height: 1.4 !important; + color: #1A1A2E !important; +} + +/* ========================================================================== + 단락 (Paragraphs) + ========================================================================== */ + +.editor-content p { + margin-bottom: 16px !important; + line-height: 1.8 !important; + font-size: 16px !important; +} + +.editor-content p:last-child { + margin-bottom: 0 !important; +} + +/* ========================================================================== + 리스트 (Lists) - 글로벌 reset 대응 + ========================================================================== */ + +.editor-content ul { + list-style-type: disc !important; + padding-left: 32px !important; + margin: 16px 0 !important; +} + +.editor-content ol { + list-style-type: decimal !important; + padding-left: 32px !important; + margin: 16px 0 !important; +} + +.editor-content li { + margin-bottom: 8px !important; + line-height: 1.8 !important; + font-size: 16px !important; +} + +.editor-content li:last-child { + margin-bottom: 0 !important; +} + +/* 중첩 리스트 */ +.editor-content ul ul { + list-style-type: circle !important; + margin: 8px 0 !important; +} + +.editor-content ul ul ul { + list-style-type: square !important; +} + +.editor-content ol ol { + list-style-type: lower-alpha !important; + margin: 8px 0 !important; +} + +.editor-content ol ol ol { + list-style-type: lower-roman !important; +} + +/* ========================================================================== + 테이블 (Tables) + ========================================================================== */ + +.editor-content table { + width: 100% !important; + border-collapse: collapse !important; + margin: 24px 0 !important; + border: 1px solid #E2E8F0 !important; + border-radius: 8px !important; + overflow: hidden !important; + font-size: 14px !important; +} + +.editor-content th, +.editor-content td { + border: 1px solid #E2E8F0 !important; + padding: 8px 16px !important; + text-align: left !important; + vertical-align: top !important; +} + +.editor-content th { + background: #EFF6FF !important; + font-weight: 600 !important; + color: #1A1A2E !important; +} + +.editor-content tr:hover { + background: #F8FAFC !important; +} + +/* ========================================================================== + 링크 (Links) + ========================================================================== */ + +.editor-content a { + color: #0049b4 !important; + text-decoration: underline !important; + transition: color 0.3s ease !important; +} + +.editor-content a:hover { + color: #003080 !important; +} + +/* ========================================================================== + 인용문 (Blockquote) + ========================================================================== */ + +.editor-content blockquote { + border-left: 4px solid #0049b4 !important; + padding: 16px 24px !important; + margin: 24px 0 !important; + background: #F8FAFC !important; + font-style: italic !important; + color: #64748B !important; + border-radius: 0 8px 8px 0 !important; +} + +.editor-content blockquote p { + margin-bottom: 0 !important; +} + +/* ========================================================================== + 이미지 (Images) + ========================================================================== */ + +.editor-content img { + max-width: 100% !important; + height: auto !important; + border-radius: 8px !important; + margin: 16px 0 !important; + display: block !important; +} + +/* ========================================================================== + 코드 (Code) + ========================================================================== */ + +.editor-content code { + background: #F8FAFC !important; + padding: 2px 6px !important; + border-radius: 4px !important; + font-family: 'Fira Code', 'Courier New', monospace !important; + font-size: 0.9em !important; + color: #0049b4 !important; +} + +.editor-content pre { + background: #F8FAFC !important; + border: 1px solid #E2E8F0 !important; + border-radius: 8px !important; + padding: 16px !important; + overflow-x: auto !important; + margin: 16px 0 !important; +} + +.editor-content pre code { + background: transparent !important; + padding: 0 !important; + color: #1A1A2E !important; +} + +/* ========================================================================== + 구분선 (Horizontal Rule) + ========================================================================== */ + +.editor-content hr { + border: none !important; + border-top: 1px solid #E2E8F0 !important; + margin: 32px 0 !important; +} + +/* ========================================================================== + 텍스트 강조 (Text Emphasis) + ========================================================================== */ + +.editor-content strong, +.editor-content b { + font-weight: 700 !important; +} + +.editor-content em, +.editor-content i { + font-style: italic !important; +} + +.editor-content u { + text-decoration: underline !important; +} + +.editor-content s, +.editor-content strike, +.editor-content del { + text-decoration: line-through !important; +} + +.editor-content mark { + background-color: #FEF3C7 !important; + padding: 0 2px !important; +} + +.editor-content sub { + vertical-align: sub !important; + font-size: 0.8em !important; +} + +.editor-content sup { + vertical-align: super !important; + font-size: 0.8em !important; +} + +/* ========================================================================== + 반응형 (Responsive) + ========================================================================== */ + +@media (max-width: 768px) { + .editor-content { + font-size: 14px !important; + } + + .editor-content h1 { + font-size: 18px !important; + } + + .editor-content h2 { + font-size: 16px !important; + } + + .editor-content h3, + .editor-content h4, + .editor-content h5, + .editor-content h6 { + font-size: 14px !important; + } + + .editor-content p, + .editor-content li { + font-size: 14px !important; + } + + .editor-content table { + font-size: 12px !important; + } + + .editor-content th, + .editor-content td { + padding: 4px 8px !important; + } + + .editor-content ul, + .editor-content ol { + padding-left: 24px !important; + } + + .editor-content blockquote { + padding: 8px 16px !important; + } + + .editor-content pre { + padding: 8px !important; + } +} diff --git a/WebContent/jsp/common/include/script.jsp b/WebContent/jsp/common/include/script.jsp index 873de95..d96ba34 100644 --- a/WebContent/jsp/common/include/script.jsp +++ b/WebContent/jsp/common/include/script.jsp @@ -60,8 +60,247 @@ + + + ", Pattern.CASE_INSENSITIVE), - Pattern.compile("src[\r\n]*=[\r\n]*\\\'(.*?)\\\'", - Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL), - Pattern.compile("src[\r\n]*=[\r\n]*\\\"(.*?)\\\"", - Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL), Pattern.compile("", Pattern.CASE_INSENSITIVE), Pattern.compile("", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL), @@ -46,6 +43,16 @@ class RequestWrapper extends HttpServletRequestWrapper { Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL) }; + // src 속성 화이트리스트 패턴 - data:image/만 허용 + private static final Pattern SRC_ATTR_PATTERN = Pattern.compile( + "(src\\s*=\\s*)([\"'])([^\"']*?)\\2", + Pattern.CASE_INSENSITIVE); + + // 허용되는 src 값 패턴 (화이트리스트) - data:image/만 허용 + private static final Pattern ALLOWED_SRC_PATTERN = Pattern.compile( + "^data:image/.*", + Pattern.CASE_INSENSITIVE); + public RequestWrapper(HttpServletRequest servletRequest) { super(servletRequest); @@ -123,6 +130,10 @@ class RequestWrapper extends HttpServletRequestWrapper { // script 문자열을 완전히 제거 value = value.replaceAll("(?i)\\bscript\\b", ""); + + // src 속성 화이트리스트 필터링 - 허용된 프로토콜만 통과 + value = filterSrcAttribute(value); + StringBuilder sb = new StringBuilder(); value = convertChars(value, sb); @@ -133,6 +144,37 @@ class RequestWrapper extends HttpServletRequestWrapper { return value; } + /** + * src 속성을 화이트리스트 방식으로 필터링 + * 허용: data:image/ (Base64 이미지) + * 비허용: 그 외 모든 프로토콜 + */ + private String filterSrcAttribute(String value) { + if (value == null || !value.toLowerCase().contains("src")) { + return value; + } + + Matcher matcher = SRC_ATTR_PATTERN.matcher(value); + StringBuffer result = new StringBuffer(); + + while (matcher.find()) { + String srcPrefix = matcher.group(1); // src= + String quote = matcher.group(2); // " 또는 ' + String srcValue = matcher.group(3); // src 값 + + // 빈 값이거나 화이트리스트에 매칭되면 유지 + if (srcValue.isEmpty() || ALLOWED_SRC_PATTERN.matcher(srcValue).matches()) { + matcher.appendReplacement(result, Matcher.quoteReplacement(srcPrefix + quote + srcValue + quote)); + } else { + // 허용되지 않는 프로토콜은 src="" 로 대체 + matcher.appendReplacement(result, Matcher.quoteReplacement(srcPrefix + quote + quote)); + } + } + matcher.appendTail(result); + + return result.toString(); + } + // '(', ')', '"' 문자는 getMethod()가 "get"일 때만 변환됨. // 이는 gridData에서 사용되는 큰따옴표(")와 변환 함수에서 사용하는 괄호('(', ')')의 처리를 위함. private String convertChars(String value, StringBuilder sb) { diff --git a/src/main/java/com/eactive/eai/rms/data/entity/onl/logger/ApiLogDTO.java b/src/main/java/com/eactive/eai/rms/data/entity/onl/logger/ApiLogDTO.java new file mode 100644 index 0000000..618a124 --- /dev/null +++ b/src/main/java/com/eactive/eai/rms/data/entity/onl/logger/ApiLogDTO.java @@ -0,0 +1,17 @@ +package com.eactive.eai.rms.data.entity.onl.logger; + +import java.util.List; + +import lombok.Data; + +@Data +public class ApiLogDTO { + + private String apiServiceNumber; + + private String txDate; + + private List logs; + + private Long totalProcessingTime; +} diff --git a/src/main/java/com/eactive/eai/rms/data/entity/onl/logger/ApiMessageLogDTO.java b/src/main/java/com/eactive/eai/rms/data/entity/onl/logger/ApiMessageLogDTO.java new file mode 100644 index 0000000..793da46 --- /dev/null +++ b/src/main/java/com/eactive/eai/rms/data/entity/onl/logger/ApiMessageLogDTO.java @@ -0,0 +1,36 @@ +package com.eactive.eai.rms.data.entity.onl.logger; + +import java.util.List; +import java.util.Map; + +import lombok.Data; + +@Data +public class ApiMessageLogDTO { + + private String apiServiceNumber; + + private int processNumber; + + private String txDate; + + private String url; + + private String queryString; + + private String clientId; + + private String method; + + private Integer HttpStatus; + + private String header; + + private String body; + + private String exdata; + + private Long processingTime; + + private String keymgtmsgctnt; +} diff --git a/src/main/java/com/eactive/eai/rms/data/entity/onl/logger/EAILogRepository.java b/src/main/java/com/eactive/eai/rms/data/entity/onl/logger/EAILogRepository.java index 2b10fb0..b89448c 100644 --- a/src/main/java/com/eactive/eai/rms/data/entity/onl/logger/EAILogRepository.java +++ b/src/main/java/com/eactive/eai/rms/data/entity/onl/logger/EAILogRepository.java @@ -6,6 +6,7 @@ import org.springframework.data.domain.Pageable; import com.eactive.eai.data.entity.onl.logger.EAILog; import com.eactive.eai.data.entity.onl.logger.EAILogId; +import java.util.List; import java.util.Map; public interface EAILogRepository { @@ -16,4 +17,6 @@ public interface EAILogRepository { Map countDetailedTransactions(Class clazz, EAILogSearch eaiLogSearch); + List getApiLog(Class class1, String apiServiceNumber); + } diff --git a/src/main/java/com/eactive/eai/rms/data/entity/onl/logger/EAILogRepositoryImpl.java b/src/main/java/com/eactive/eai/rms/data/entity/onl/logger/EAILogRepositoryImpl.java index fc78b9c..aad5b34 100644 --- a/src/main/java/com/eactive/eai/rms/data/entity/onl/logger/EAILogRepositoryImpl.java +++ b/src/main/java/com/eactive/eai/rms/data/entity/onl/logger/EAILogRepositoryImpl.java @@ -1,5 +1,19 @@ package com.eactive.eai.rms.data.entity.onl.logger; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; + +import org.apache.commons.lang3.StringUtils; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Repository; + import com.eactive.eai.data.entity.onl.logger.EAILog; import com.eactive.eai.data.entity.onl.logger.EAILogId; import com.eactive.eai.data.entity.onl.logger.QEAILog; @@ -11,18 +25,6 @@ import com.querydsl.core.types.dsl.BooleanExpression; import com.querydsl.core.types.dsl.EntityPathBase; import com.querydsl.jpa.impl.JPAQuery; import com.querydsl.jpa.impl.JPAQueryFactory; -import org.apache.commons.lang3.StringUtils; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageImpl; -import org.springframework.data.domain.Pageable; -import org.springframework.stereotype.Repository; - -import javax.persistence.EntityManager; -import javax.persistence.PersistenceContext; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; @Repository public class EAILogRepositoryImpl implements EAILogRepository { @@ -217,4 +219,14 @@ public class EAILogRepositoryImpl implements EAILogRepository { .fetchOne(); } + @Override + public List getApiLog(Class clazz, String apiServiceNumber) { + QEAILog qeaiLog = QEAILog.eAILog; + EntityPathBase entityPathBase = new EntityPathBase<>(clazz, qeaiLog.getMetadata().getName()); + + return new JPAQueryFactory(em).select(qeaiLog).from(entityPathBase) + .where(qeaiLog.id.eaisvcserno.trim().eq(apiServiceNumber)).orderBy(qeaiLog.id.logprcssserno.asc()) + .fetch(); + } + } diff --git a/src/main/java/com/eactive/eai/rms/data/entity/onl/logger/EAILogService.java b/src/main/java/com/eactive/eai/rms/data/entity/onl/logger/EAILogService.java index 5742260..215687b 100644 --- a/src/main/java/com/eactive/eai/rms/data/entity/onl/logger/EAILogService.java +++ b/src/main/java/com/eactive/eai/rms/data/entity/onl/logger/EAILogService.java @@ -19,4 +19,6 @@ public interface EAILogService { List selectLogList(HashMap paramMap); Map getTransactionCounts(EAILogSearch eaiLogSearch); + + ApiLogDTO getApiLog(String txDate, String apiServiceNumber); } diff --git a/src/main/java/com/eactive/eai/rms/data/entity/onl/logger/EAILogServiceImpl.java b/src/main/java/com/eactive/eai/rms/data/entity/onl/logger/EAILogServiceImpl.java index 30a8d95..1c86c8d 100644 --- a/src/main/java/com/eactive/eai/rms/data/entity/onl/logger/EAILogServiceImpl.java +++ b/src/main/java/com/eactive/eai/rms/data/entity/onl/logger/EAILogServiceImpl.java @@ -1,55 +1,132 @@ package com.eactive.eai.rms.data.entity.onl.logger; -import com.eactive.eai.data.RollingTable; -import com.eactive.eai.data.entity.onl.logger.EAILog; -import com.eactive.eai.data.entity.onl.logger.EAILogId; -import com.eactive.eai.rms.common.util.CommonUtil; -import com.eactive.eai.rms.data.entity.onl.apim.portalstatistics.PortalStatisticsVO; -import lombok.RequiredArgsConstructor; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import org.apache.commons.lang3.StringUtils; import org.springframework.context.ApplicationContext; 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.HashMap; -import java.util.List; -import java.util.Map; +import com.eactive.eai.data.RollingTable; +import com.eactive.eai.data.entity.onl.logger.EAILog; +import com.eactive.eai.data.entity.onl.logger.EAILogId; +import com.eactive.eai.data.entity.onl.logger.HttpAdapterExtraLog; +import com.eactive.eai.rms.common.util.CommonUtil; +import com.eactive.eai.rms.data.entity.onl.apim.portalstatistics.PortalStatisticsVO; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Slf4j @Service @Transactional @RequiredArgsConstructor public class EAILogServiceImpl implements EAILogService { - private final EAILogRepository eaiLogRepository; - private final ApplicationContext applicationContext; - private final EAILogDAOForEMS eaiLogDAOForEMS; + private final EAILogRepository eaiLogRepository; + private final ApplicationContext applicationContext; + private final EAILogDAOForEMS eaiLogDAOForEMS; + private final HttpAdapterExtraLogService httpAdapterExtraLogService; - @Override - public Page findAll(Pageable pageable, EAILogSearch eaiLogSearch) { - long time = CommonUtil.getCalendar(eaiLogSearch.getSearchStartTime()).getTimeInMillis(); - EAILog eaiLog = (EAILog) applicationContext.getBean(RollingTable.class, EAILog.class, time); - Page eaiLogDtos = eaiLogRepository.findAll(eaiLog.getClass(), pageable, eaiLogSearch); - return eaiLogDtos; - } + @Override + public Page findAll(Pageable pageable, EAILogSearch eaiLogSearch) { + long time = CommonUtil.getCalendar(eaiLogSearch.getSearchStartTime()).getTimeInMillis(); + EAILog eaiLog = (EAILog) applicationContext.getBean(RollingTable.class, EAILog.class, time); + Page eaiLogDtos = eaiLogRepository.findAll(eaiLog.getClass(), pageable, eaiLogSearch); + return eaiLogDtos; + } - public Map getTransactionCounts(EAILogSearch eaiLogSearch){ - long time = CommonUtil.getCalendar(eaiLogSearch.getSearchStartTime()).getTimeInMillis(); - EAILog eaiLog = (EAILog) applicationContext.getBean(RollingTable.class, EAILog.class, time); - Map detailedCounts = eaiLogRepository.countDetailedTransactions(eaiLog.getClass(), eaiLogSearch); - return detailedCounts; - } + public Map getTransactionCounts(EAILogSearch eaiLogSearch) { + long time = CommonUtil.getCalendar(eaiLogSearch.getSearchStartTime()).getTimeInMillis(); + EAILog eaiLog = (EAILog) applicationContext.getBean(RollingTable.class, EAILog.class, time); + Map detailedCounts = eaiLogRepository.countDetailedTransactions(eaiLog.getClass(), eaiLogSearch); + return detailedCounts; + } - @Override - public EAILogDto getById(EAILogId eaiLogId) { - long time = CommonUtil.getCalendar(eaiLogId.getMsgdpstyms()).getTimeInMillis(); - EAILog eaiLog = (EAILog) applicationContext.getBean(RollingTable.class, EAILog.class, time); - return eaiLogRepository.getById(eaiLog.getClass(), eaiLogId); - } + @Override + public EAILogDto getById(EAILogId eaiLogId) { + long time = CommonUtil.getCalendar(eaiLogId.getMsgdpstyms()).getTimeInMillis(); + EAILog eaiLog = (EAILog) applicationContext.getBean(RollingTable.class, EAILog.class, time); + return eaiLogRepository.getById(eaiLog.getClass(), eaiLogId); + } + public List selectLogList(HashMap paramMap) { + return eaiLogDAOForEMS.selectLogList(paramMap); + } - public List selectLogList(HashMap paramMap) { - return eaiLogDAOForEMS.selectLogList(paramMap); - } + @Override + public ApiLogDTO getApiLog(String txDate, String apiServiceNumber) { + + long time = CommonUtil.getCalendar(txDate).getTimeInMillis(); + EAILog eaiLogTargetObject = (EAILog) applicationContext.getBean(RollingTable.class, EAILog.class, time); + List eaiLogs = eaiLogRepository.getApiLog(eaiLogTargetObject.getClass(), apiServiceNumber); + eaiLogs.sort((l1, l2) -> l1.getId().getEaisvcserno().compareTo(l2.getId().getEaisvcserno())); + + ApiLogDTO apiLogDTO = new ApiLogDTO(); + apiLogDTO.setApiServiceNumber(apiServiceNumber); + eaiLogs.stream().findFirst().ifPresent(log -> apiLogDTO.setTxDate(log.getId().getMsgdpstyms())); + + List apiMessageLogDTOs = new LinkedList<>(); + + for (EAILog eaiLog : eaiLogs) { + ApiMessageLogDTO apiMessageLogDTO = new ApiMessageLogDTO(); + + apiMessageLogDTO.setApiServiceNumber(apiServiceNumber); + apiMessageLogDTO.setTxDate(eaiLog.getMsgprcssyms()); + apiMessageLogDTO.setProcessNumber(Integer.parseInt(eaiLog.getId().getLogprcssserno())); + apiMessageLogDTO.setQueryString(""); + apiMessageLogDTO.setClientId(eaiLog.getClientid()); + apiMessageLogDTO.setKeymgtmsgctnt(eaiLog.getKeymgtmsgctnt()); + + String body = StringUtils.substringAfter(eaiLog.getBzwkdatactnt(), '|'); + body = StringUtils.substringBeforeLast(body, "|"); + + apiMessageLogDTO.setBody(body); + apiMessageLogDTOs.add(apiMessageLogDTO); + } + + Map extraLogs = httpAdapterExtraLogService.findByGuid(apiServiceNumber); + for (ApiMessageLogDTO apiMessageLogDTO : apiMessageLogDTOs) { + HttpAdapterExtraLog httpAdapterExtraLog = extraLogs.get(apiMessageLogDTO.getProcessNumber()); + + if (httpAdapterExtraLog != null) { + apiMessageLogDTO.setHeader(httpAdapterExtraLog.getHeaderContent()); + apiMessageLogDTO.setMethod(httpAdapterExtraLog.getHttpMethod()); + apiMessageLogDTO.setHttpStatus(httpAdapterExtraLog.getHttpStatus()); + apiMessageLogDTO.setUrl(httpAdapterExtraLog.getUrl()); + } + } + + // 처리 시간 계산 + for (int i = 0; i < apiMessageLogDTOs.size(); i++) { + ApiMessageLogDTO dto = apiMessageLogDTOs.get(i); + EAILog eaiLog = eaiLogs.get(i); + + // msgprcssyms - msgdpstyms + long receivedTime = CommonUtil.getCalendar(eaiLog.getId().getMsgdpstyms()).getTimeInMillis(); + long processedTime = CommonUtil.getCalendar(eaiLog.getMsgprcssyms()).getTimeInMillis(); + long processingTime = processedTime - receivedTime; + dto.setProcessingTime(processingTime); + } + + // 전체 처리 시간 계산 (첫 수신 시각 ~ 마지막 처리 시각) + if (!eaiLogs.isEmpty()) { + long firstReceivedTime = CommonUtil.getCalendar(eaiLogs.get(0).getId().getMsgdpstyms()).getTimeInMillis(); + long lastProcessedTime = CommonUtil.getCalendar(eaiLogs.get(eaiLogs.size() - 1).getMsgprcssyms()).getTimeInMillis(); + long totalProcessingTime = lastProcessedTime - firstReceivedTime; + apiLogDTO.setTotalProcessingTime(totalProcessingTime); + } + + apiLogDTO.setLogs(apiMessageLogDTOs); + return apiLogDTO; + } } diff --git a/src/main/java/com/eactive/eai/rms/data/entity/onl/logger/HttpAdapterExtraLogService.java b/src/main/java/com/eactive/eai/rms/data/entity/onl/logger/HttpAdapterExtraLogService.java index 2fa2c86..6441b7c 100644 --- a/src/main/java/com/eactive/eai/rms/data/entity/onl/logger/HttpAdapterExtraLogService.java +++ b/src/main/java/com/eactive/eai/rms/data/entity/onl/logger/HttpAdapterExtraLogService.java @@ -1,16 +1,28 @@ package com.eactive.eai.rms.data.entity.onl.logger; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + import javax.transaction.Transactional; import org.springframework.stereotype.Service; import com.eactive.eai.data.entity.onl.logger.HttpAdapterExtraLog; import com.eactive.eai.data.entity.onl.logger.HttpAdapterExtraLogId; +import com.eactive.eai.data.entity.onl.logger.QHttpAdapterExtraLog; import com.eactive.eai.data.jpa.AbstractDataService; +import com.querydsl.core.types.dsl.BooleanExpression; @Service @Transactional public class HttpAdapterExtraLogService extends AbstractDataService { + public Map findByGuid(String guid) { + BooleanExpression predicate = QHttpAdapterExtraLog.httpAdapterExtraLog.id.guid.eq(guid); + return StreamSupport.stream(repository.findAll(predicate).spliterator(), false) + .collect(Collectors.toMap(log -> log.getId().getServiceProcessNumber(), log -> log)); + } + } diff --git a/src/main/java/com/eactive/eai/rms/onl/transaction/apim/ApiInterfaceService.java b/src/main/java/com/eactive/eai/rms/onl/transaction/apim/ApiInterfaceService.java index f46f749..c3c521a 100644 --- a/src/main/java/com/eactive/eai/rms/onl/transaction/apim/ApiInterfaceService.java +++ b/src/main/java/com/eactive/eai/rms/onl/transaction/apim/ApiInterfaceService.java @@ -553,7 +553,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(".*\\{[_a-zA-Z]+\\}.*") ? "variableUrlRequest" : "simpleRequest"; restOption.setType(pathType); if (StringUtils.isNotBlank(contentType)) { diff --git a/src/main/java/com/eactive/ext/kjb/web/ApiLogUI.java b/src/main/java/com/eactive/ext/kjb/web/ApiLogUI.java new file mode 100644 index 0000000..7572680 --- /dev/null +++ b/src/main/java/com/eactive/ext/kjb/web/ApiLogUI.java @@ -0,0 +1,17 @@ +package com.eactive.ext.kjb.web; + +import java.util.List; + +import lombok.Data; + +@Data +public class ApiLogUI { + + private String apiServiceNumber; + + private String txDate; + + private List logs; + + private Long totalProcessingTime; +} diff --git a/src/main/java/com/eactive/ext/kjb/web/ApiMessageLogUI.java b/src/main/java/com/eactive/ext/kjb/web/ApiMessageLogUI.java new file mode 100644 index 0000000..7e2989a --- /dev/null +++ b/src/main/java/com/eactive/ext/kjb/web/ApiMessageLogUI.java @@ -0,0 +1,35 @@ +package com.eactive.ext.kjb.web; + +import java.util.Map; + +import lombok.Data; + +@Data +public class ApiMessageLogUI { + + private String apiServiceNumber; + + private int processNumber; + + private String txDate; + + private String url; + + private String queryString; + + private String clientId; + + private String method; + + private Integer HttpStatus; + + private Map header; + + private Object body; + + private String exdata; + + private Long processingTime; + + private String guid; +} diff --git a/src/main/java/com/eactive/ext/kjb/web/ApiTransactionLogController.java b/src/main/java/com/eactive/ext/kjb/web/ApiTransactionLogController.java new file mode 100644 index 0000000..c18c317 --- /dev/null +++ b/src/main/java/com/eactive/ext/kjb/web/ApiTransactionLogController.java @@ -0,0 +1,142 @@ +package com.eactive.ext.kjb.web; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; + +import com.eactive.eai.rms.common.datasource.DataSourceContextHolder; +import com.eactive.eai.rms.common.datasource.DataSourceTypeManager; +import com.eactive.eai.rms.common.interceptor.InterceptorSkipController; +import com.eactive.eai.rms.data.entity.onl.logger.ApiLogDTO; +import com.eactive.eai.rms.data.entity.onl.logger.ApiMessageLogDTO; +import com.eactive.eai.rms.data.entity.onl.logger.EAILogService; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Controller +@RequiredArgsConstructor +public class ApiTransactionLogController implements InterceptorSkipController { + + private final EAILogService eaiLogService; + private final ObjectMapper objectMapper = new ObjectMapper(); + + @GetMapping("/api/transaction/log.do") + @PostMapping("/api/transaction/log.do") + public ResponseEntity getApiLog(String txDate, String apiServiceNumber) { + DataSourceContextHolder.setDataSourceType(DataSourceTypeManager.getDataSourceType(DataSourceTypeManager.APIGW)); + + // txDate가 없으면 오늘 날짜 설정 + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMdd"); + LocalDate searchDate; + if (StringUtils.hasText(txDate)) { + searchDate = LocalDate.parse(txDate, formatter); + } else { + searchDate = LocalDate.now(); + } + + // 최대 10일 전까지 조회 시도 + ApiLogDTO dto = null; + for (int i = 0; i < 10; i++) { + String searchDateStr = searchDate.format(formatter); + dto = eaiLogService.getApiLog(searchDateStr, apiServiceNumber); + + // 결과가 있으면 종료 + if (dto != null && dto.getLogs() != null && !dto.getLogs().isEmpty()) { + break; + } + + // 하루 전으로 이동 + searchDate = searchDate.minusDays(1); + } + + return ResponseEntity.ok(convertToUI(dto)); + } + + private ApiLogUI convertToUI(ApiLogDTO dto) { + ApiLogUI ui = new ApiLogUI(); + ui.setApiServiceNumber(dto.getApiServiceNumber()); + ui.setTxDate(dto.getTxDate()); + ui.setTotalProcessingTime(dto.getTotalProcessingTime()); + + if (dto.getLogs() != null) { + List uiLogs = dto.getLogs().stream().map(this::convertMessageToUI) + .collect(Collectors.toList()); + ui.setLogs(uiLogs); + } + + return ui; + } + + private ApiMessageLogUI convertMessageToUI(ApiMessageLogDTO dto) { + ApiMessageLogUI ui = new ApiMessageLogUI(); + ui.setApiServiceNumber(dto.getApiServiceNumber()); + ui.setProcessNumber(dto.getProcessNumber()); + ui.setTxDate(dto.getTxDate()); + ui.setUrl(dto.getUrl()); + ui.setQueryString(dto.getQueryString()); + ui.setClientId(dto.getClientId()); + ui.setMethod(dto.getMethod()); + ui.setHttpStatus(dto.getHttpStatus()); + ui.setExdata(dto.getExdata()); + ui.setProcessingTime(dto.getProcessingTime()); + ui.setGuid(dto.getKeymgtmsgctnt()); + + // header: JSON 문자열 → Map + if (dto.getHeader() != null) { + try { + List> headerList = objectMapper.readValue(dto.getHeader(), + new TypeReference>>() { + }); + + Map headerMap = new HashMap<>(); + for (Map headerItem : headerList) { + String name = headerItem.get("name"); + String value = headerItem.get("value"); + if (name != null) { + headerMap.put(name, value); + } + } + + // header에서 client*id 패턴 추출 (정규식, ignoreCase) + for (Map.Entry entry : headerMap.entrySet()) { + if (entry.getKey() != null && entry.getKey().matches("(?i)client.*id")) { + ui.setClientId(entry.getValue()); + break; + } + } + + ui.setHeader(headerMap); + } catch (Exception e) { + log.error("Failed to parse header JSON: {}", dto.getHeader(), e); + } + } + + // body: JSON 문자열 → Object + if (dto.getBody() != null) { + try { + Object bodyObject = objectMapper.readValue(dto.getBody(), Object.class); + ui.setBody(bodyObject); + } catch (Exception e) { + // JSON 파싱 실패 시 원본 문자열 사용 + ui.setBody(dto.getBody()); + } + } + + return ui; + } + +} diff --git a/src/main/java/com/eactive/ext/kjb/web/package-info.java b/src/main/java/com/eactive/ext/kjb/web/package-info.java new file mode 100644 index 0000000..03ec7ba --- /dev/null +++ b/src/main/java/com/eactive/ext/kjb/web/package-info.java @@ -0,0 +1 @@ +package com.eactive.ext.kjb.web; \ No newline at end of file