From b3f797b7268cb21b9ba56bfdecc49746cb4d88e7 Mon Sep 17 00:00:00 2001 From: eastargh Date: Wed, 26 Aug 2026 10:58:32 +0900 Subject: [PATCH] =?UTF-8?q?=EC=9B=B9=ED=9B=85=20=EC=8B=A0=EC=B2=AD?= =?UTF-8?q?=EB=82=B4=EC=97=AD=20=EC=A1=B0=ED=9A=8C=ED=99=94=EB=A9=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../jsp/onl/apim/webhook/webhookReqMan.jsp | 149 +++++++++++++ .../onl/apim/webhook/webhookReqManDetail.jsp | 154 +++++++++++++ .../entity/onl/djb/webhook/WebhookReq.java | 4 + .../controller/WebhookReqManController.java | 67 ++++++ .../djb/webhook/dto/WebhookReqDetailUI.java | 34 +++ .../rms/ext/djb/webhook/dto/WebhookReqUI.java | 21 ++ .../djb/webhook/dto/WebhookReqUISearch.java | 16 ++ .../repository/WebhookReqRepository.java | 4 +- .../webhook/service/WebhookReqManService.java | 202 ++++++++++++++++++ 9 files changed, 649 insertions(+), 2 deletions(-) create mode 100644 WebContent/jsp/onl/apim/webhook/webhookReqMan.jsp create mode 100644 WebContent/jsp/onl/apim/webhook/webhookReqManDetail.jsp create mode 100644 src/main/java/com/eactive/eai/rms/ext/djb/webhook/controller/WebhookReqManController.java create mode 100644 src/main/java/com/eactive/eai/rms/ext/djb/webhook/dto/WebhookReqDetailUI.java create mode 100644 src/main/java/com/eactive/eai/rms/ext/djb/webhook/dto/WebhookReqUI.java create mode 100644 src/main/java/com/eactive/eai/rms/ext/djb/webhook/dto/WebhookReqUISearch.java create mode 100644 src/main/java/com/eactive/eai/rms/ext/djb/webhook/service/WebhookReqManService.java diff --git a/WebContent/jsp/onl/apim/webhook/webhookReqMan.jsp b/WebContent/jsp/onl/apim/webhook/webhookReqMan.jsp new file mode 100644 index 0000000..a587d68 --- /dev/null +++ b/WebContent/jsp/onl/apim/webhook/webhookReqMan.jsp @@ -0,0 +1,149 @@ +<%@ page language="java" contentType="text/html; charset=utf-8" %> +<%@ page import="java.io.*" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> +<%@ taglib uri="http://www.springframework.org/tags" prefix="spring" %> +<%@ include file="/jsp/common/include/localemessage.jsp" %> +<% + response.setHeader("Pragma", "No-cache"); + response.setHeader("Cache-Control", "no-cache"); + response.setHeader("Expires", "0"); +%> + + + + + + + + + + +
+ +
+
+ +
+
웹훅 신청내역등록된 웹훅 신청 내역을 조회합니다.
+
+ + + + + + + + + + + + + + + + + +
기관명리버스 프록시 경로
Target URL
+
+
+
+
+
+ + diff --git a/WebContent/jsp/onl/apim/webhook/webhookReqManDetail.jsp b/WebContent/jsp/onl/apim/webhook/webhookReqManDetail.jsp new file mode 100644 index 0000000..118539f --- /dev/null +++ b/WebContent/jsp/onl/apim/webhook/webhookReqManDetail.jsp @@ -0,0 +1,154 @@ +<%@ page language="java" contentType="text/html; charset=utf-8" %> +<%@ page import="java.io.*" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> +<%@ taglib uri="http://www.springframework.org/tags" prefix="spring" %> +<%@ include file="/jsp/common/include/localemessage.jsp" %> +<% + response.setHeader("Pragma", "No-cache"); + response.setHeader("Cache-Control", "no-cache"); + response.setHeader("Expires", "0"); +%> + + + + + + + + + + +
+ +
+
+ +
+
웹훅 신청내역 상세
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
신청 ID
기관명신청일시
리버스 프록시 경로Target URL
Secret
User Secret
신청 이벤트
+ +
API 목록
+ + + + +
API ID
+
+
+ + diff --git a/src/main/java/com/eactive/eai/rms/data/entity/onl/djb/webhook/WebhookReq.java b/src/main/java/com/eactive/eai/rms/data/entity/onl/djb/webhook/WebhookReq.java index 344dbb3..2cc60cc 100644 --- a/src/main/java/com/eactive/eai/rms/data/entity/onl/djb/webhook/WebhookReq.java +++ b/src/main/java/com/eactive/eai/rms/data/entity/onl/djb/webhook/WebhookReq.java @@ -34,4 +34,8 @@ public class WebhookReq { @Column(name = "USER_SECRET", length = 200) @Comment("사용자 Secret Key") private String userSecret; + + @Column(name = "CREATED_DATE", length = 14) + @Comment("신청일시 (yyyyMMddHHmmss)") + private String createdDate; } diff --git a/src/main/java/com/eactive/eai/rms/ext/djb/webhook/controller/WebhookReqManController.java b/src/main/java/com/eactive/eai/rms/ext/djb/webhook/controller/WebhookReqManController.java new file mode 100644 index 0000000..4445390 --- /dev/null +++ b/src/main/java/com/eactive/eai/rms/ext/djb/webhook/controller/WebhookReqManController.java @@ -0,0 +1,67 @@ +package com.eactive.eai.rms.ext.djb.webhook.controller; + +import java.util.HashMap; +import java.util.Map; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +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.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; + +import com.eactive.eai.rms.common.base.BaseAnnotationController; +import com.eactive.eai.rms.common.combo.ComboService; +import com.eactive.eai.rms.common.vo.GridResponse; +import com.eactive.eai.rms.ext.djb.webhook.dto.WebhookReqDetailUI; +import com.eactive.eai.rms.ext.djb.webhook.dto.WebhookReqUI; +import com.eactive.eai.rms.ext.djb.webhook.dto.WebhookReqUISearch; +import com.eactive.eai.rms.ext.djb.webhook.service.WebhookReqManService; + +import lombok.RequiredArgsConstructor; + +/** + * 웹훅 신청내역(PTL_WEBHOOK_REQ) 조회 화면 컨트롤러. + * + *

조회 전용 (등록/수정/삭제 없음)

+ */ +@Controller +@RequiredArgsConstructor +public class WebhookReqManController extends BaseAnnotationController { + + private final WebhookReqManService webhookReqManService; + private final ComboService comboService; + + @GetMapping(value = "/onl/apim/webhook/webhookReqMan.view") + public void view() { + // 목록 view + } + + @GetMapping(value = "/onl/apim/webhook/webhookReqMan.view", params = "cmd=DETAIL") + public String detailView() { + return "/onl/apim/webhook/webhookReqManDetail"; + } + + @PostMapping(value = "/onl/apim/webhook/webhookReqMan.json", params = "cmd=LIST") + public ResponseEntity> selectList( + @SortDefault(sort = "createdDate", direction = Sort.Direction.DESC) Pageable pageable, + WebhookReqUISearch search) { + Page page = webhookReqManService.selectList(pageable, search); + return ResponseEntity.ok(new GridResponse<>(page)); + } + + @PostMapping(value = "/onl/apim/webhook/webhookReqMan.json", params = "cmd=LIST_INIT_COMBO") + public ResponseEntity> initCombo() { + Map resultMap = new HashMap<>(); + resultMap.put("eventTypeList", comboService.getMonitoringCodeSortedBySeq( + WebhookReqManService.CODE_GROUP_EVENT_TYPE)); + return ResponseEntity.ok(resultMap); + } + + @PostMapping(value = "/onl/apim/webhook/webhookReqMan.json", params = "cmd=DETAIL") + public ResponseEntity selectDetail(String id) { + return ResponseEntity.ok(webhookReqManService.selectDetail(id)); + } +} diff --git a/src/main/java/com/eactive/eai/rms/ext/djb/webhook/dto/WebhookReqDetailUI.java b/src/main/java/com/eactive/eai/rms/ext/djb/webhook/dto/WebhookReqDetailUI.java new file mode 100644 index 0000000..07c6b3d --- /dev/null +++ b/src/main/java/com/eactive/eai/rms/ext/djb/webhook/dto/WebhookReqDetailUI.java @@ -0,0 +1,34 @@ +package com.eactive.eai.rms.ext.djb.webhook.dto; + +import java.util.List; + +import lombok.Data; + +/** + * 웹훅 신청내역(PTL_WEBHOOK_REQ) 상세 화면 응답 DTO. + * + *

신청 상세 정보에 더해, 연관된 API 목록(PTL_WEBHOOK_REQ_API)과 + * 신청 이벤트 목록(PTL_WEBHOOK_REQ_EVENT)을 함께 담는다.

+ */ +@Data +public class WebhookReqDetailUI { + + private String id; + + private String orgId; + private String orgName; + private String reverseProxyPath; + + private String targetUrl; + private String secret; + private String userSecret; + + /* 신청일시 — yyyy-MM-dd HH:mm:ss */ + private String createdDate; + + /** PTL_WEBHOOK_REQ_API 목록 (API ID) */ + private List apiIds; + + /** PTL_WEBHOOK_REQ_EVENT 목록 (이벤트유형 코드). 코드명은 화면에서 콤보로 변환한다. */ + private List eventTypes; +} diff --git a/src/main/java/com/eactive/eai/rms/ext/djb/webhook/dto/WebhookReqUI.java b/src/main/java/com/eactive/eai/rms/ext/djb/webhook/dto/WebhookReqUI.java new file mode 100644 index 0000000..3b54ad8 --- /dev/null +++ b/src/main/java/com/eactive/eai/rms/ext/djb/webhook/dto/WebhookReqUI.java @@ -0,0 +1,21 @@ +package com.eactive.eai.rms.ext.djb.webhook.dto; + +import lombok.Data; + +/** + * 웹훅 신청내역(PTL_WEBHOOK_REQ) 목록 화면 응답 DTO. + */ +@Data +public class WebhookReqUI { + + private String id; + + private String orgId; + private String orgName; + private String reverseProxyPath; + + private String targetUrl; + + /* 신청일시 — yyyy-MM-dd HH:mm:ss */ + private String createdDate; +} diff --git a/src/main/java/com/eactive/eai/rms/ext/djb/webhook/dto/WebhookReqUISearch.java b/src/main/java/com/eactive/eai/rms/ext/djb/webhook/dto/WebhookReqUISearch.java new file mode 100644 index 0000000..87d559b --- /dev/null +++ b/src/main/java/com/eactive/eai/rms/ext/djb/webhook/dto/WebhookReqUISearch.java @@ -0,0 +1,16 @@ +package com.eactive.eai.rms.ext.djb.webhook.dto; + +import lombok.Data; + +@Data +public class WebhookReqUISearch { + + /* 기관명 (ptl_org.org_name, LIKE 검색) */ + private String searchOrgName; + + /* 리버스 프록시 경로 (ptl_org.reverse_proxy_path, LIKE 검색) */ + private String searchReverseProxyPath; + + /* 수신 URL (LIKE 검색) */ + private String searchTargetUrl; +} diff --git a/src/main/java/com/eactive/eai/rms/ext/djb/webhook/repository/WebhookReqRepository.java b/src/main/java/com/eactive/eai/rms/ext/djb/webhook/repository/WebhookReqRepository.java index 7abc722..d0d71d9 100644 --- a/src/main/java/com/eactive/eai/rms/ext/djb/webhook/repository/WebhookReqRepository.java +++ b/src/main/java/com/eactive/eai/rms/ext/djb/webhook/repository/WebhookReqRepository.java @@ -1,11 +1,11 @@ package com.eactive.eai.rms.ext.djb.webhook.repository; +import com.eactive.eai.data.jpa.BaseRepository; 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 { +public interface WebhookReqRepository extends BaseRepository { } diff --git a/src/main/java/com/eactive/eai/rms/ext/djb/webhook/service/WebhookReqManService.java b/src/main/java/com/eactive/eai/rms/ext/djb/webhook/service/WebhookReqManService.java new file mode 100644 index 0000000..6601426 --- /dev/null +++ b/src/main/java/com/eactive/eai/rms/ext/djb/webhook/service/WebhookReqManService.java @@ -0,0 +1,202 @@ +package com.eactive.eai.rms.ext.djb.webhook.service; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; + +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.portalorg.entity.QPortalOrg; +import com.eactive.eai.rms.common.base.BaseService; +import com.eactive.eai.rms.common.util.StringUtils; +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.WebhookReq; +import com.eactive.eai.rms.ext.djb.webhook.dto.WebhookReqDetailUI; +import com.eactive.eai.rms.ext.djb.webhook.dto.WebhookReqUI; +import com.eactive.eai.rms.ext.djb.webhook.dto.WebhookReqUISearch; +import com.eactive.eai.rms.ext.djb.webhook.repository.WebhookReqRepository; +import com.eactive.eai.rms.onl.common.exception.BizException; +import com.querydsl.core.BooleanBuilder; +import com.querydsl.jpa.impl.JPAQueryFactory; + +import lombok.RequiredArgsConstructor; + +/** + * 웹훅 신청내역(PTL_WEBHOOK_REQ) 조회 화면 서비스. + * + *

조회 전용. orgId 는 엔티티 연관관계가 아닌 단순 컬럼이므로, + * 기관명은 QueryDSL 로 ptl_org 를 별도 조회하여 매핑한다.

+ */ +@Service +@RequiredArgsConstructor +@Transactional(transactionManager = "transactionManagerForEMS") +public class WebhookReqManService extends BaseService { + + /** 모니터링 공통코드(tseairm28) 상의 웹훅 이벤트유형 코드 그룹. */ + public static final String CODE_GROUP_EVENT_TYPE = "EVENT_TYPE"; + + private static final DateTimeFormatter FMT_DATETIME_OUT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + private static final DateTimeFormatter FMT_DATETIME_IN = DateTimeFormatter.ofPattern("yyyyMMddHHmmss"); + + private final WebhookReqRepository webhookReqRepository; + + @PersistenceContext(unitName = "entityManagerFactoryForEMS") + private EntityManager entityManager; + + public Page selectList(Pageable pageable, WebhookReqUISearch search) { + QWebhookReq req = QWebhookReq.webhookReq; + BooleanBuilder predicate = new BooleanBuilder(); + + if (StringUtils.isNotBlank(search.getSearchOrgName())) { + List orgIds = findOrgIdsByName(search.getSearchOrgName()); + if (orgIds.isEmpty()) { + // 일치하는 기관이 없으면 쿼리를 실행할 필요 없이 빈 결과를 바로 반환한다. + return Page.empty(pageable); + } + predicate.and(req.orgId.in(orgIds)); + } + if (StringUtils.isNotBlank(search.getSearchReverseProxyPath())) { + List orgIds = findOrgIdsByReverseProxyPath(search.getSearchReverseProxyPath()); + if (orgIds.isEmpty()) { + return Page.empty(pageable); + } + predicate.and(req.orgId.in(orgIds)); + } + if (StringUtils.isNotBlank(search.getSearchTargetUrl())) { + predicate.and(req.targetUrl.containsIgnoreCase(search.getSearchTargetUrl())); + } + + Page page = webhookReqRepository.findAll(predicate, pageable); + + Map orgInfoMap = findOrgInfo(page.getContent()); + return page.map(e -> convertToListUI(e, orgInfoMap.get(e.getOrgId()))); + } + + public WebhookReqDetailUI selectDetail(String id) { + WebhookReq entity = webhookReqRepository.findById(id) + .orElseThrow(() -> new BizException("존재하지 않는 웹훅 신청내역입니다.")); + + Map orgInfoMap = findOrgInfo(Collections.singletonList(entity)); + OrgInfo orgInfo = orgInfoMap.get(entity.getOrgId()); + + WebhookReqDetailUI ui = new WebhookReqDetailUI(); + ui.setId(entity.getId()); + ui.setOrgId(entity.getOrgId()); + ui.setOrgName(orgInfo != null ? orgInfo.orgName : null); + ui.setReverseProxyPath(orgInfo != null ? orgInfo.reverseProxyPath : null); + ui.setTargetUrl(entity.getTargetUrl()); + ui.setSecret(entity.getSecret()); + ui.setUserSecret(entity.getUserSecret()); + ui.setCreatedDate(fmtCreatedDate(entity.getCreatedDate())); + ui.setApiIds(findApiIds(id)); + ui.setEventTypes(findEventTypes(id)); + return ui; + } + + /** 기관명 LIKE 검색으로 매칭되는 orgId 목록. */ + private List findOrgIdsByName(String orgName) { + QPortalOrg org = QPortalOrg.portalOrg; + return new JPAQueryFactory(entityManager) + .select(org.id) + .from(org) + .where(org.orgName.containsIgnoreCase(orgName)) + .fetch(); + } + + /** 리버스 프록시 경로 LIKE 검색으로 매칭되는 orgId 목록. */ + private List findOrgIdsByReverseProxyPath(String reverseProxyPath) { + QPortalOrg org = QPortalOrg.portalOrg; + return new JPAQueryFactory(entityManager) + .select(org.id) + .from(org) + .where(org.reverseProxyPath.containsIgnoreCase(reverseProxyPath)) + .fetch(); + } + + /** 목록/상세에 필요한 orgId → (기관명, 리버스 프록시 경로) 매핑 (일괄 조회로 N+1 방지). */ + private Map findOrgInfo(List reqs) { + Set orgIds = reqs.stream() + .map(WebhookReq::getOrgId) + .filter(StringUtils::isNotBlank) + .collect(Collectors.toSet()); + if (orgIds.isEmpty()) { + return Collections.emptyMap(); + } + + QPortalOrg org = QPortalOrg.portalOrg; + return new JPAQueryFactory(entityManager) + .select(org.id, org.orgName, org.reverseProxyPath) + .from(org) + .where(org.id.in(orgIds)) + .fetch() + .stream() + .collect(Collectors.toMap(t -> t.get(org.id), + t -> new OrgInfo(t.get(org.orgName), t.get(org.reverseProxyPath)))); + } + + /** 신청 건에 등록된 API ID 목록 (PTL_WEBHOOK_REQ_API). */ + private List findApiIds(String webhookReqId) { + QWebhookReqApi api = QWebhookReqApi.webhookReqApi; + return new JPAQueryFactory(entityManager) + .select(api.id.apiId) + .from(api) + .where(api.id.webhookReqId.eq(webhookReqId)) + .fetch(); + } + + /** 신청 건에 등록된 이벤트유형 코드 목록 (PTL_WEBHOOK_REQ_EVENT). */ + private List findEventTypes(String webhookReqId) { + QWebhookReqEvent evt = QWebhookReqEvent.webhookReqEvent; + return new JPAQueryFactory(entityManager) + .select(evt.id.eventType) + .from(evt) + .where(evt.id.webhookReqId.eq(webhookReqId)) + .fetch(); + } + + private WebhookReqUI convertToListUI(WebhookReq e, OrgInfo orgInfo) { + WebhookReqUI ui = new WebhookReqUI(); + ui.setId(e.getId()); + ui.setOrgId(e.getOrgId()); + ui.setOrgName(orgInfo != null ? orgInfo.orgName : null); + ui.setReverseProxyPath(orgInfo != null ? orgInfo.reverseProxyPath : null); + ui.setTargetUrl(e.getTargetUrl()); + ui.setCreatedDate(fmtCreatedDate(e.getCreatedDate())); + return ui; + } + + /** ptl_org 조회 결과(기관명 + 리버스 프록시 경로) 보관용. */ + private static final class OrgInfo { + private final String orgName; + private final String reverseProxyPath; + + private OrgInfo(String orgName, String reverseProxyPath) { + this.orgName = orgName; + this.reverseProxyPath = reverseProxyPath; + } + } + + /** CREATED_DATE(yyyyMMddHHmmss 문자열) → 화면 표시용(yyyy-MM-dd HH:mm:ss). */ + private static String fmtCreatedDate(String raw) { + if (StringUtils.isBlank(raw)) { + return ""; + } + try { + return LocalDateTime.parse(raw, FMT_DATETIME_IN).format(FMT_DATETIME_OUT); + } catch (Exception e) { + return raw; + } + } +}