From 27fac38c4414b6f145a3341b72df94112ab8a7e8 Mon Sep 17 00:00:00 2001 From: daekuk Date: Thu, 11 Dec 2025 10:04:25 +0900 Subject: [PATCH] =?UTF-8?q?inflow=20group=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- WebContent/jsp/common/include/script.jsp | 205 +++++ .../admin/inflow/inflowGroupControlMan.jsp | 195 ++++ .../inflow/inflowGroupControlManDetail.jsp | 865 ++++++++++++++++++ .../InflowControlGroupMappingRepository.java | 25 + .../inflow/InflowControlGroupRepository.java | 8 + .../InflowGroupControlManController.java | 185 ++++ .../group/InflowGroupControlManMapper.java | 29 + .../group/InflowGroupControlManService.java | 253 +++++ .../inflow/group/InflowGroupControlManUI.java | 60 ++ .../inflow/group/InflowGroupMappingUI.java | 37 + 10 files changed, 1862 insertions(+) create mode 100644 WebContent/jsp/onl/admin/inflow/inflowGroupControlMan.jsp create mode 100644 WebContent/jsp/onl/admin/inflow/inflowGroupControlManDetail.jsp create mode 100644 src/main/java/com/eactive/eai/rms/data/entity/onl/inflow/InflowControlGroupMappingRepository.java create mode 100644 src/main/java/com/eactive/eai/rms/data/entity/onl/inflow/InflowControlGroupRepository.java create mode 100644 src/main/java/com/eactive/eai/rms/onl/manage/inflow/group/InflowGroupControlManController.java create mode 100644 src/main/java/com/eactive/eai/rms/onl/manage/inflow/group/InflowGroupControlManMapper.java create mode 100644 src/main/java/com/eactive/eai/rms/onl/manage/inflow/group/InflowGroupControlManService.java create mode 100644 src/main/java/com/eactive/eai/rms/onl/manage/inflow/group/InflowGroupControlManUI.java create mode 100644 src/main/java/com/eactive/eai/rms/onl/manage/inflow/group/InflowGroupMappingUI.java diff --git a/WebContent/jsp/common/include/script.jsp b/WebContent/jsp/common/include/script.jsp index 80ed3ec..873de95 100644 --- a/WebContent/jsp/common/include/script.jsp +++ b/WebContent/jsp/common/include/script.jsp @@ -362,4 +362,209 @@ console.error('Download failed:', error); } } + + /** + * 스타일링된 Alert 모달 + * @param {string} message - 표시할 메시지 + * @param {object} options - 옵션 (type: 'info'|'warning'|'error'|'success', title: string, onClose: function) + */ + function showAlert(message, options) { + options = options || {}; + var type = options.type || 'info'; + var title = options.title || getAlertTitle(type); + var onClose = options.onClose || function(){}; + + // 기존 모달 제거 + $('#commonAlertModal').remove(); + + var iconHtml = getAlertIcon(type); + var colorClass = 'alert-' + type; + + var modalHtml = + '
' + + '
' + + '
' + + '' + iconHtml + '' + + '' + title + '' + + '
' + + '
' + + '

' + message + '

' + + '
' + + '' + + '
' + + '
'; + + $('body').append(modalHtml); + + // 애니메이션 효과 + setTimeout(function() { + $('#commonAlertModal').addClass('show'); + }, 10); + + // 닫기 이벤트 + $('#commonAlertOkBtn').on('click', function() { + closeCommonAlert(onClose); + }); + + // ESC 키로 닫기 + $(document).on('keydown.commonAlert', function(e) { + if (e.keyCode === 27) { + closeCommonAlert(onClose); + } + }); + + // Enter 키로 확인 + $(document).on('keydown.commonAlertEnter', function(e) { + if (e.keyCode === 13) { + closeCommonAlert(onClose); + } + }); + } + + function closeCommonAlert(callback) { + $('#commonAlertModal').removeClass('show'); + setTimeout(function() { + $('#commonAlertModal').remove(); + $(document).off('keydown.commonAlert'); + $(document).off('keydown.commonAlertEnter'); + if (typeof callback === 'function') { + callback(); + } + }, 200); + } + + function getAlertTitle(type) { + switch(type) { + case 'error': return '오류'; + case 'warning': return '경고'; + case 'success': return '완료'; + default: return '알림'; + } + } + + function getAlertIcon(type) { + switch(type) { + case 'error': return 'error'; + case 'warning': return 'warning'; + case 'success': return 'check_circle'; + default: return 'info'; + } + } + + /** + * 스타일링된 Confirm 모달 + * @param {string} message - 표시할 메시지 + * @param {object} options - 옵션 (type, title, confirmText, cancelText, onConfirm, onCancel) + */ + function showConfirm(message, options) { + options = options || {}; + var type = options.type || 'info'; + var title = options.title || '확인'; + var confirmText = options.confirmText || '확인'; + var cancelText = options.cancelText || '취소'; + var onConfirm = options.onConfirm || function(){}; + var onCancel = options.onCancel || function(){}; + + // 기존 모달 제거 + $('#commonConfirmModal').remove(); + + var iconHtml = getAlertIcon(type); + var colorClass = 'alert-' + type; + + var modalHtml = + '
' + + '
' + + '
' + + '' + iconHtml + '' + + '' + title + '' + + '
' + + '
' + + '

' + message + '

' + + '
' + + '' + + '
' + + '
'; + + $('body').append(modalHtml); + + setTimeout(function() { + $('#commonConfirmModal').addClass('show'); + }, 10); + + // 확인 버튼 + $('#commonConfirmOkBtn').on('click', function() { + closeCommonConfirm(onConfirm); + }); + + // 취소 버튼 + $('#commonConfirmCancelBtn').on('click', function() { + closeCommonConfirm(onCancel); + }); + + // ESC 키로 취소 + $(document).on('keydown.commonConfirm', function(e) { + if (e.keyCode === 27) { + closeCommonConfirm(onCancel); + } + }); + } + + function closeCommonConfirm(callback) { + $('#commonConfirmModal').removeClass('show'); + setTimeout(function() { + $('#commonConfirmModal').remove(); + $(document).off('keydown.commonConfirm'); + if (typeof callback === 'function') { + callback(); + } + }, 200); + } + + + diff --git a/WebContent/jsp/onl/admin/inflow/inflowGroupControlMan.jsp b/WebContent/jsp/onl/admin/inflow/inflowGroupControlMan.jsp new file mode 100644 index 0000000..02a845e --- /dev/null +++ b/WebContent/jsp/onl/admin/inflow/inflowGroupControlMan.jsp @@ -0,0 +1,195 @@ +<%@ 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"); + +%> + + + + + + + + + + +
+
+ +
+
+
+ + +
+
유량제어 그룹 관리유량제어 그룹 관리
+ + + + + + + + +
그룹명
+ +
+
+ +
+
+ + diff --git a/WebContent/jsp/onl/admin/inflow/inflowGroupControlManDetail.jsp b/WebContent/jsp/onl/admin/inflow/inflowGroupControlManDetail.jsp new file mode 100644 index 0000000..5c8d399 --- /dev/null +++ b/WebContent/jsp/onl/admin/inflow/inflowGroupControlManDetail.jsp @@ -0,0 +1,865 @@ +<%@ 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"); +%> + + + + + + + + + + + + + +
+
+ +
+
+
+ + + + +
+
+
유량제어 그룹 관리유량제어 그룹 관리
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
그룹명 *
사용여부 +
+ + 사용 +
+
+ + + + + + + + + + + + + + + + + + +
+ 초당 임계치 + help_outline + 1초 단위의 순간 트래픽 제한입니다. 매 초마다 설정한 건수만큼 토큰이 리필되며, 초과 시 요청이 거부됩니다. + + +
+ + req/sec +
+
+ 추가 임계치 + help_outline + 장시간 누적 트래픽 제한입니다. 시간단위를 선택하면 해당 주기마다 토큰이 리필됩니다. + + +
+ + + requests +
+
+ + + +
+ 매핑된 API 0 + +
+
+ + + + + + + + + + + + + + + + + + +
API ID설명관리
inbox 매핑된 API가 없습니다
+
+
+ +
+
+ + +
+
+
+

API 선택

+ +
+
+ +
+ + + + + + + + + + +
API ID설명
검색 결과가 없습니다
+
+
+ 0 +
+ +
+ +
+
+
+ +
+
+ + +
+
+
+

유량제어 임계치 설명

+ +
+
+

개요

+

유량제어는 Token Bucket 알고리즘 기반으로 동작합니다. + 설정된 임계치만큼 토큰이 주기적으로 리필되며, 요청 1건당 토큰 1개를 소비합니다. + 토큰이 부족하면 요청이 거부됩니다.

+ +

초당 임계치 (thresholdPerSecond)

+

1초 단위의 순간적인 트래픽 급증(spike)을 방어합니다.

+
+
예시: 초당 임계치 = 100
+
    +
  • 매 1초마다 토큰 100개 리필
  • +
  • 1초 내 100건까지 처리 가능
  • +
  • 101번째 요청부터 차단
  • +
+
+ +

추가 임계치 (threshold) + 시간단위

+

장시간 누적 트래픽을 제어합니다. 시간단위에 따라 토큰 리필 주기가 결정됩니다.

+ + + + + + + +
시간단위설명예시
SEC1초당 N건
MIN1분당 N건
HOU1시간당 N건
DAY1일당 N건
MON1개월당 N건
+
+
예시: 추가 임계치 = 10,000 / 시간단위 = HOU
+
    +
  • 매 1시간마다 토큰 10,000개 리필
  • +
  • 1시간 내 10,000건까지 처리 가능
  • +
  • 10,001번째 요청부터 차단
  • +
+
+ +

두 임계치 조합 (권장)

+

두 조건을 함께 설정하면 이중 보호가 적용됩니다.

+
+
예시: 초당 100건 + 시간당 50,000건
+
    +
  • 순간 폭주 방지: 초당 100건 제한 → 1초 내 101번째 요청부터 차단
  • +
  • 장기 누적 제한: 시간당 50,000건 제한 → 시간 내 50,001번째 요청부터 차단
  • +
  • 두 조건 중 하나라도 초과 시 차단 (둘 다 충족해야 통과)
  • +
+
+ +
+ 참고: 그룹에 매핑된 인터페이스들은 개별 유량제어 대신 그룹 유량제어가 적용됩니다. + 그룹 내 모든 인터페이스의 호출이 합산되어 임계치와 비교됩니다. +
+
+
+
+ + diff --git a/src/main/java/com/eactive/eai/rms/data/entity/onl/inflow/InflowControlGroupMappingRepository.java b/src/main/java/com/eactive/eai/rms/data/entity/onl/inflow/InflowControlGroupMappingRepository.java new file mode 100644 index 0000000..618a1e2 --- /dev/null +++ b/src/main/java/com/eactive/eai/rms/data/entity/onl/inflow/InflowControlGroupMappingRepository.java @@ -0,0 +1,25 @@ +package com.eactive.eai.rms.data.entity.onl.inflow; + +import java.util.List; +import java.util.Optional; + +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import com.eactive.eai.data.entity.onl.inflow.InflowControlGroupMapping; +import com.eactive.eai.data.jpa.BaseRepository; + +public interface InflowControlGroupMappingRepository extends BaseRepository { + + List findByGroupId(String groupId); + + @Modifying + @Query("DELETE FROM InflowControlGroupMapping m WHERE m.groupId = :groupId") + void deleteByGroupId(@Param("groupId") String groupId); + + /** + * 인터페이스 ID로 매핑 조회 (중복 체크용) + */ + Optional findByInterfaceId(String interfaceId); +} diff --git a/src/main/java/com/eactive/eai/rms/data/entity/onl/inflow/InflowControlGroupRepository.java b/src/main/java/com/eactive/eai/rms/data/entity/onl/inflow/InflowControlGroupRepository.java new file mode 100644 index 0000000..d267d5c --- /dev/null +++ b/src/main/java/com/eactive/eai/rms/data/entity/onl/inflow/InflowControlGroupRepository.java @@ -0,0 +1,8 @@ +package com.eactive.eai.rms.data.entity.onl.inflow; + +import com.eactive.eai.data.entity.onl.inflow.InflowControlGroup; +import com.eactive.eai.data.jpa.BaseRepository; + +public interface InflowControlGroupRepository extends BaseRepository { + +} diff --git a/src/main/java/com/eactive/eai/rms/onl/manage/inflow/group/InflowGroupControlManController.java b/src/main/java/com/eactive/eai/rms/onl/manage/inflow/group/InflowGroupControlManController.java new file mode 100644 index 0000000..a14f5ea --- /dev/null +++ b/src/main/java/com/eactive/eai/rms/onl/manage/inflow/group/InflowGroupControlManController.java @@ -0,0 +1,185 @@ +package com.eactive.eai.rms.onl.manage.inflow.group; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.servlet.ModelAndView; + +import com.eactive.eai.agent.command.CommonCommand; +import com.eactive.eai.rms.common.base.OnlBaseAnnotationController; +import com.eactive.eai.rms.common.combo.ComboService; +import com.eactive.eai.rms.common.combo.ComboVo; +import com.eactive.eai.rms.common.vo.GridResponse; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; + +@Controller +public class InflowGroupControlManController extends OnlBaseAnnotationController { + + @Autowired + private InflowGroupControlManService service; + + @Autowired + private ComboService comboService; + + private ObjectMapper objectMapper = new ObjectMapper(); + + // ========== View Mappings ========== + + @RequestMapping(value = "/onl/admin/inflow/inflowGroupControlMan.view") + public String viewList() { + return "/onl/admin/inflow/inflowGroupControlMan"; + } + + @RequestMapping(value = "/onl/admin/inflow/inflowGroupControlMan.view", params = "cmd=DETAIL") + public String viewDetail() { + return "/onl/admin/inflow/inflowGroupControlManDetail"; + } + + @RequestMapping(value = "/onl/admin/inflow/inflowGroupControlMan.view", params = "cmd=INTERFACE_POPUP") + public String viewInterfacePopup() { + return "/onl/admin/inflow/inflowGroupInterfacePopup"; + } + + // ========== JSON API Mappings ========== + + /** + * 그룹 목록 조회 + */ + @RequestMapping(value = "/onl/admin/inflow/inflowGroupControlMan.json", params = "cmd=LIST") + public ResponseEntity> selectList(HttpServletRequest request, + Pageable pageVo, String searchGroupName) { + Page uiPage = service.selectGroupList(pageVo, searchGroupName); + return ResponseEntity.ok(new GridResponse<>(uiPage)); + } + + /** + * 그룹 상세 조회 + */ + @RequestMapping(value = "/onl/admin/inflow/inflowGroupControlMan.json", params = "cmd=DETAIL") + public ResponseEntity selectDetail(HttpServletRequest request, + HttpServletResponse response, String groupId) { + InflowGroupControlManUI ui = service.selectGroupDetail(groupId); + return ResponseEntity.ok(ui); + } + + /** + * 그룹 저장 (INSERT) + */ + @RequestMapping(value = "/onl/admin/inflow/inflowGroupControlMan.json", params = "cmd=INSERT") + public String insert(HttpServletRequest request, HttpServletResponse response, InflowGroupControlManUI ui, + @RequestParam(value = "interfaceListJson", required = false) String interfaceListJson) throws Exception { + parseInterfaceList(ui, interfaceListJson); + String modifiedBy = getSessionUserId(request); + service.mergeGroup(ui, modifiedBy); + + // Agent Command 전송 + CommonCommand command = new CommonCommand("com.eactive.eai.agent.inflow.ReloadInflowGroupControlCommand", + ui.getGroupId()); + agentUtilService.broadcast(command); + return null; + } + + /** + * 그룹 저장 (UPDATE) + */ + @RequestMapping(value = "/onl/admin/inflow/inflowGroupControlMan.json", params = "cmd=UPDATE") + public String update(HttpServletRequest request, HttpServletResponse response, InflowGroupControlManUI ui, + @RequestParam(value = "interfaceListJson", required = false) String interfaceListJson) throws Exception { + parseInterfaceList(ui, interfaceListJson); + String modifiedBy = getSessionUserId(request); + service.mergeGroup(ui, modifiedBy); + + // Agent Command 전송 + CommonCommand command = new CommonCommand("com.eactive.eai.agent.inflow.ReloadInflowGroupControlCommand", + ui.getGroupId()); + agentUtilService.broadcast(command); + return null; + } + + /** + * 그룹 삭제 + */ + @RequestMapping(value = "/onl/admin/inflow/inflowGroupControlMan.json", params = "cmd=DELETE") + public String delete(HttpServletRequest request, HttpServletResponse response, String groupId) throws Exception { + service.deleteGroup(groupId); + + // Agent Command 전송 + CommonCommand command = new CommonCommand("com.eactive.eai.agent.inflow.RemoveInflowGroupControlCommand", + groupId); + agentUtilService.broadcast(command); + return null; + } + + /** + * 콤보박스 초기화 데이터 + */ + @RequestMapping(value = "/onl/admin/inflow/inflowGroupControlMan.json", params = "cmd=LIST_INIT_COMBO") + public ModelAndView initCombo(HttpServletRequest request, HttpServletResponse response) { + List useYnRows = comboService.getFromCode("USE_YN"); + List timeUnitRows = comboService.getFromCode("INFLOW_CTRL_TIME_UNIT"); + + Map> resultMap = new HashMap<>(); + resultMap.put("useYnRows", useYnRows); + resultMap.put("timeUnitRows", timeUnitRows); + + return new ModelAndView("jsonView", resultMap); + } + + /** + * 인터페이스 목록 조회 (팝업용) + */ + @RequestMapping(value = "/onl/admin/inflow/inflowGroupControlMan.json", params = "cmd=INTERFACE_LIST") + public ResponseEntity> selectInterfaceList(HttpServletRequest request, + Pageable pageVo, String searchName) { + Page uiPage = service.selectInterfaceListForPopup(pageVo, searchName); + return ResponseEntity.ok(new GridResponse<>(uiPage)); + } + + /** + * 인터페이스 중복 등록 체크 + */ + @RequestMapping(value = "/onl/admin/inflow/inflowGroupControlMan.json", params = "cmd=LIST_CHECK_DUPLICATE") + public ModelAndView checkDuplicate(HttpServletRequest request, HttpServletResponse response, + @RequestParam("interfaceId") String interfaceId, + @RequestParam(value = "groupId", required = false) String groupId) { + String existingGroupName = service.checkInterfaceDuplicate(interfaceId, groupId); + + Map resultMap = new HashMap<>(); + resultMap.put("duplicate", existingGroupName != null); + resultMap.put("groupName", existingGroupName); + + return new ModelAndView("jsonView", resultMap); + } + + /** + * JSON 문자열을 인터페이스 목록으로 변환 + */ + private void parseInterfaceList(InflowGroupControlManUI ui, String interfaceListJson) throws Exception { + if (interfaceListJson != null && !interfaceListJson.isEmpty()) { + List interfaceList = objectMapper.readValue(interfaceListJson, + new TypeReference>() { + }); + ui.setInterfaceList(interfaceList); + } + } + + /** + * 세션에서 사용자 ID 조회 + */ + private String getSessionUserId(HttpServletRequest request) { + Object userId = request.getSession().getAttribute("userId"); + return userId != null ? userId.toString() : "SYSTEM"; + } +} diff --git a/src/main/java/com/eactive/eai/rms/onl/manage/inflow/group/InflowGroupControlManMapper.java b/src/main/java/com/eactive/eai/rms/onl/manage/inflow/group/InflowGroupControlManMapper.java new file mode 100644 index 0000000..83cf138 --- /dev/null +++ b/src/main/java/com/eactive/eai/rms/onl/manage/inflow/group/InflowGroupControlManMapper.java @@ -0,0 +1,29 @@ +package com.eactive.eai.rms.onl.manage.inflow.group; + +import org.mapstruct.BeanMapping; +import org.mapstruct.Mapper; +import org.mapstruct.MappingTarget; +import org.mapstruct.NullValuePropertyMappingStrategy; +import org.mapstruct.ReportingPolicy; + +import com.eactive.eai.data.entity.onl.inflow.InflowControlGroup; +import com.eactive.eai.data.entity.onl.inflow.InflowControlGroupMapping; + +@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE) +public interface InflowGroupControlManMapper { + + InflowGroupControlManUI toVo(InflowControlGroup entity); + + InflowControlGroup toEntity(InflowGroupControlManUI vo); + + @BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE) + void updateToEntity(InflowGroupControlManUI vo, @MappingTarget InflowControlGroup entity); + + // Mapping Entity <-> UI + InflowGroupMappingUI toMappingVo(InflowControlGroupMapping entity); + + InflowControlGroupMapping toMappingEntity(InflowGroupMappingUI vo); + + @BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE) + void updateToMappingEntity(InflowGroupMappingUI vo, @MappingTarget InflowControlGroupMapping entity); +} diff --git a/src/main/java/com/eactive/eai/rms/onl/manage/inflow/group/InflowGroupControlManService.java b/src/main/java/com/eactive/eai/rms/onl/manage/inflow/group/InflowGroupControlManService.java new file mode 100644 index 0000000..625ede1 --- /dev/null +++ b/src/main/java/com/eactive/eai/rms/onl/manage/inflow/group/InflowGroupControlManService.java @@ -0,0 +1,253 @@ +package com.eactive.eai.rms.onl.manage.inflow.group; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; + +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.eactive.eai.data.entity.onl.inflow.InflowControlGroup; +import com.eactive.eai.data.entity.onl.inflow.InflowControlGroupMapping; +import com.eactive.eai.data.entity.onl.inflow.QInflowControlGroup; +import com.eactive.eai.data.entity.onl.inflow.QInflowControlGroupMapping; +import com.eactive.eai.data.entity.onl.message.QEAIMessageEntity; +import com.eactive.eai.rms.common.base.BaseService; +import com.eactive.eai.rms.data.entity.onl.inflow.InflowControlGroupMappingRepository; +import com.eactive.eai.rms.data.entity.onl.inflow.InflowControlGroupRepository; +import com.querydsl.core.BooleanBuilder; +import com.querydsl.core.Tuple; +import com.querydsl.jpa.impl.JPAQueryFactory; + +@Service +public class InflowGroupControlManService extends BaseService { + + @PersistenceContext + EntityManager entityManager; + + @Autowired + private InflowControlGroupRepository groupRepository; + + @Autowired + private InflowControlGroupMappingRepository mappingRepository; + + @Autowired + private InflowGroupControlManMapper mapper; + + /** + * 그룹 목록 조회 + */ + public Page selectGroupList(Pageable pageable, String searchGroupName) { + JPAQueryFactory jpaQueryFactory = new JPAQueryFactory(entityManager); + QInflowControlGroup qGroup = QInflowControlGroup.inflowControlGroup; + + BooleanBuilder boolBuilder = new BooleanBuilder(); + if (!StringUtils.isEmpty(searchGroupName)) { + boolBuilder.and(qGroup.groupName.contains(searchGroupName)); + } + + List groupList = jpaQueryFactory + .selectFrom(qGroup) + .where(boolBuilder) + .orderBy(qGroup.groupId.asc()) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + + List uiList = groupList.stream() + .map(mapper::toVo) + .collect(Collectors.toList()); + + long totalCount = jpaQueryFactory + .select(qGroup.groupId.count()) + .from(qGroup) + .where(boolBuilder) + .fetchOne(); + + return new PageImpl<>(uiList, pageable, totalCount); + } + + /** + * 그룹 상세 조회 (매핑된 인터페이스 포함) + */ + public InflowGroupControlManUI selectGroupDetail(String groupId) { + Optional groupOpt = groupRepository.findById(groupId); + if (!groupOpt.isPresent()) { + return null; + } + + InflowGroupControlManUI ui = mapper.toVo(groupOpt.get()); + + // 매핑된 인터페이스 목록 조회 (인터페이스 설명 포함) + List mappingList = selectMappingListWithDesc(groupId); + ui.setInterfaceList(mappingList); + + return ui; + } + + /** + * 그룹에 매핑된 인터페이스 목록 조회 (인터페이스 설명 포함) + */ + private List selectMappingListWithDesc(String groupId) { + JPAQueryFactory jpaQueryFactory = new JPAQueryFactory(entityManager); + QInflowControlGroupMapping qMapping = QInflowControlGroupMapping.inflowControlGroupMapping; + QEAIMessageEntity qMessage = QEAIMessageEntity.eAIMessageEntity; + + List tupleList = jpaQueryFactory + .select(qMapping, qMessage.eaisvcdesc) + .from(qMapping) + .leftJoin(qMessage).on(qMapping.interfaceId.eq(qMessage.eaisvcname)) + .where(qMapping.groupId.eq(groupId)) + .orderBy(qMapping.interfaceId.asc()) + .fetch(); + + return tupleList.stream() + .map(tuple -> { + InflowGroupMappingUI mappingUI = mapper.toMappingVo(tuple.get(qMapping)); + mappingUI.setInterfaceDesc(tuple.get(qMessage.eaisvcdesc)); + return mappingUI; + }) + .collect(Collectors.toList()); + } + + /** + * 그룹 저장 (INSERT/UPDATE) + */ + @Transactional + public void mergeGroup(InflowGroupControlManUI ui, String modifiedBy) { + String groupId = ui.getGroupId(); + boolean isNew = StringUtils.isEmpty(groupId); + + InflowControlGroup entity; + if (!isNew) { + Optional groupOpt = groupRepository.findById(groupId); + if (groupOpt.isPresent()) { + entity = groupOpt.get(); + mapper.updateToEntity(ui, entity); + } else { + entity = mapper.toEntity(ui); + } + } else { + entity = mapper.toEntity(ui); + } + entity.setModifiedBy(modifiedBy); + entity.setModifiedAt(LocalDateTime.now()); + InflowControlGroup savedEntity = groupRepository.saveAndFlush(entity); + + // 저장된 엔티티의 groupId 사용 (신규 생성 시 시퀀스에서 생성된 ID) + String savedGroupId = savedEntity.getGroupId(); + + // 매핑 정보 저장 + saveMappings(savedGroupId, ui.getInterfaceList(), modifiedBy); + } + + /** + * 그룹-인터페이스 매핑 저장 + */ + @Transactional + public void saveMappings(String groupId, List interfaceList, String modifiedBy) { + // 기존 매핑 삭제 + mappingRepository.deleteByGroupId(groupId); + + // 새로운 매핑 저장 + if (interfaceList != null && !interfaceList.isEmpty()) { + for (InflowGroupMappingUI mappingUI : interfaceList) { + InflowControlGroupMapping mapping = new InflowControlGroupMapping(); + mapping.setInterfaceId(mappingUI.getInterfaceId()); + mapping.setGroupId(groupId); + mapping.setUseYn("1"); + mapping.setModifiedBy(modifiedBy); + mapping.setModifiedAt(LocalDateTime.now()); + mappingRepository.save(mapping); + } + } + } + + /** + * 그룹 삭제 + */ + @Transactional + public void deleteGroup(String groupId) { + // 매핑 먼저 삭제 + mappingRepository.deleteByGroupId(groupId); + // 그룹 삭제 + groupRepository.deleteById(groupId); + } + + /** + * 인터페이스 중복 등록 체크 (다른 그룹에 등록 여부) + * @param interfaceId 체크할 인터페이스 ID + * @param currentGroupId 현재 그룹 ID (수정 시 자기 그룹 제외, 신규 시 null) + * @return 등록된 그룹명 (미등록 시 null) + */ + public String checkInterfaceDuplicate(String interfaceId, String currentGroupId) { + Optional mappingOpt = mappingRepository.findByInterfaceId(interfaceId); + if (!mappingOpt.isPresent()) { + return null; + } + + InflowControlGroupMapping mapping = mappingOpt.get(); + String existingGroupId = mapping.getGroupId(); + + // 현재 그룹과 동일하면 중복 아님 (수정 시) + if (existingGroupId.equals(currentGroupId)) { + return null; + } + + // 그룹명 조회 + Optional groupOpt = groupRepository.findById(existingGroupId); + if (groupOpt.isPresent()) { + return groupOpt.get().getGroupName(); + } + return existingGroupId; // 그룹명 없으면 ID 반환 + } + + /** + * 인터페이스 목록 조회 (팝업용) + */ + public Page selectInterfaceListForPopup(Pageable pageable, String searchName) { + JPAQueryFactory jpaQueryFactory = new JPAQueryFactory(entityManager); + QEAIMessageEntity qMessage = QEAIMessageEntity.eAIMessageEntity; + + BooleanBuilder boolBuilder = new BooleanBuilder(); + if (!StringUtils.isEmpty(searchName)) { + boolBuilder.and(qMessage.eaisvcname.containsIgnoreCase(searchName) + .or(qMessage.eaisvcdesc.containsIgnoreCase(searchName))); + } + + List tupleList = jpaQueryFactory + .select(qMessage.eaisvcname, qMessage.eaisvcdesc) + .from(qMessage) + .where(boolBuilder) + .orderBy(qMessage.eaisvcname.asc()) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + + List uiList = tupleList.stream() + .map(tuple -> { + InflowGroupMappingUI ui = new InflowGroupMappingUI(); + ui.setInterfaceId(tuple.get(qMessage.eaisvcname)); + ui.setInterfaceDesc(tuple.get(qMessage.eaisvcdesc)); + return ui; + }) + .collect(Collectors.toList()); + + long totalCount = jpaQueryFactory + .select(qMessage.eaisvcname.count()) + .from(qMessage) + .where(boolBuilder) + .fetchOne(); + + return new PageImpl<>(uiList, pageable, totalCount); + } +} diff --git a/src/main/java/com/eactive/eai/rms/onl/manage/inflow/group/InflowGroupControlManUI.java b/src/main/java/com/eactive/eai/rms/onl/manage/inflow/group/InflowGroupControlManUI.java new file mode 100644 index 0000000..2dac454 --- /dev/null +++ b/src/main/java/com/eactive/eai/rms/onl/manage/inflow/group/InflowGroupControlManUI.java @@ -0,0 +1,60 @@ +package com.eactive.eai.rms.onl.manage.inflow.group; + +import java.time.LocalDateTime; +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonProperty; + +import lombok.Data; + +@Data +public class InflowGroupControlManUI { + /** + * 그룹 ID + */ + @JsonProperty("GROUPID") + private String groupId; + + /** + * 그룹명 + */ + @JsonProperty("GROUPNAME") + private String groupName; + + /** + * 임계치 + */ + @JsonProperty("THRESHOLD") + private Integer threshold; + + /** + * 초당 임계치 + */ + @JsonProperty("THRESHOLDPERSECOND") + private Integer thresholdPerSecond; + + /** + * 임계치 TimeUnit + */ + @JsonProperty("THRESHOLDTIMEUNIT") + private String thresholdTimeUnit; + + /** + * 사용여부 + */ + @JsonProperty("USEYN") + private String useYn; + + /** + * 수정일시 + */ + @JsonProperty("MODIFIEDAT") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime modifiedAt; + + /** + * 매핑된 인터페이스 목록 (저장용) + */ + private List interfaceList; +} diff --git a/src/main/java/com/eactive/eai/rms/onl/manage/inflow/group/InflowGroupMappingUI.java b/src/main/java/com/eactive/eai/rms/onl/manage/inflow/group/InflowGroupMappingUI.java new file mode 100644 index 0000000..1fdc8a4 --- /dev/null +++ b/src/main/java/com/eactive/eai/rms/onl/manage/inflow/group/InflowGroupMappingUI.java @@ -0,0 +1,37 @@ +package com.eactive.eai.rms.onl.manage.inflow.group; + +import com.fasterxml.jackson.annotation.JsonAlias; +import com.fasterxml.jackson.annotation.JsonProperty; + +import lombok.Data; + +@Data +public class InflowGroupMappingUI { + /** + * 인터페이스 ID + */ + @JsonProperty("INTERFACEID") + @JsonAlias("interfaceId") + private String interfaceId; + + /** + * 인터페이스 설명 (조회용) + */ + @JsonProperty("INTERFACEDESC") + @JsonAlias("interfaceDesc") + private String interfaceDesc; + + /** + * 그룹 ID + */ + @JsonProperty("GROUPID") + @JsonAlias("groupId") + private String groupId; + + /** + * 사용여부 + */ + @JsonProperty("USEYN") + @JsonAlias("useYn") + private String useYn; +}