inflow group 추가

This commit is contained in:
daekuk
2025-12-11 10:04:25 +09:00
parent 22fca42329
commit 27fac38c44
10 changed files with 1862 additions and 0 deletions
+205
View File
@@ -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 =
'<div id="commonAlertModal" class="common-alert-overlay">' +
'<div class="common-alert-modal ' + colorClass + '">' +
'<div class="common-alert-header">' +
'<span class="common-alert-icon">' + iconHtml + '</span>' +
'<span class="common-alert-title">' + title + '</span>' +
'</div>' +
'<div class="common-alert-body">' +
'<p class="common-alert-message">' + message + '</p>' +
'</div>' +
'<div class="common-alert-footer">' +
'<button type="button" class="common-alert-btn" id="commonAlertOkBtn">확인</button>' +
'</div>' +
'</div>' +
'</div>';
$('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 '<i class="material-icons">error</i>';
case 'warning': return '<i class="material-icons">warning</i>';
case 'success': return '<i class="material-icons">check_circle</i>';
default: return '<i class="material-icons">info</i>';
}
}
/**
* 스타일링된 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 =
'<div id="commonConfirmModal" class="common-alert-overlay">' +
'<div class="common-alert-modal ' + colorClass + '">' +
'<div class="common-alert-header">' +
'<span class="common-alert-icon">' + iconHtml + '</span>' +
'<span class="common-alert-title">' + title + '</span>' +
'</div>' +
'<div class="common-alert-body">' +
'<p class="common-alert-message">' + message + '</p>' +
'</div>' +
'<div class="common-alert-footer">' +
'<button type="button" class="common-confirm-cancel-btn" id="commonConfirmCancelBtn">' + cancelText + '</button>' +
'<button type="button" class="common-alert-btn" id="commonConfirmOkBtn">' + confirmText + '</button>' +
'</div>' +
'</div>' +
'</div>';
$('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);
}
</script>
<!-- 공용 Alert 모달 스타일 -->
<style>
.common-alert-overlay { display:none; position:fixed; top:0; left:0; width:100%; height:100%; background:rgba(0,0,0,0.5); z-index:99999; justify-content:center; align-items:center; }
.common-alert-overlay.show { display:flex; animation:alertFadeIn 0.2s ease; }
@keyframes alertFadeIn { from { opacity:0; } to { opacity:1; } }
.common-alert-modal { background:#fff; border-radius:8px; box-shadow:0 8px 32px rgba(0,0,0,0.3); width:380px; max-width:90%; overflow:hidden; animation:alertSlideIn 0.2s ease; }
@keyframes alertSlideIn { from { transform:translateY(-20px); opacity:0; } to { transform:translateY(0); opacity:1; } }
.common-alert-header { display:flex; align-items:center; gap:10px; padding:16px 20px; border-bottom:1px solid #eee; }
.common-alert-icon { display:flex; align-items:center; justify-content:center; width:32px; height:32px; border-radius:50%; }
.common-alert-icon i { font-size:20px; }
.common-alert-title { font-size:15px; font-weight:600; color:#333; }
.common-alert-body { padding:20px; }
.common-alert-message { margin:0; font-size:13px; line-height:1.6; color:#555; word-break:keep-all; }
.common-alert-footer { display:flex; justify-content:flex-end; padding:12px 20px; background:#fafafa; border-top:1px solid #eee; }
.common-alert-btn { padding:8px 24px; font-size:13px; font-weight:500; border:none; border-radius:4px; cursor:pointer; transition:all 0.2s; }
.common-confirm-cancel-btn { padding:8px 24px; font-size:13px; font-weight:500; border:1px solid #ddd; border-radius:4px; cursor:pointer; transition:all 0.2s; background:#fff; color:#666; margin-right:8px; }
.common-confirm-cancel-btn:hover { background:#f5f5f5; border-color:#ccc; }
/* Info 타입 (기본) */
.alert-info .common-alert-icon { background:rgba(0,128,128,0.1); }
.alert-info .common-alert-icon i { color:rgba(0,128,128,1); }
.alert-info .common-alert-btn { background:rgba(0,128,128,0.85); color:#fff; }
.alert-info .common-alert-btn:hover { background:rgba(0,128,128,1); }
/* Warning 타입 */
.alert-warning .common-alert-icon { background:rgba(255,152,0,0.1); }
.alert-warning .common-alert-icon i { color:#ff9800; }
.alert-warning .common-alert-btn { background:#ff9800; color:#fff; }
.alert-warning .common-alert-btn:hover { background:#f57c00; }
/* Error 타입 */
.alert-error .common-alert-icon { background:rgba(244,67,54,0.1); }
.alert-error .common-alert-icon i { color:#f44336; }
.alert-error .common-alert-btn { background:#f44336; color:#fff; }
.alert-error .common-alert-btn:hover { background:#d32f2f; }
/* Success 타입 */
.alert-success .common-alert-icon { background:rgba(76,175,80,0.1); }
.alert-success .common-alert-icon i { color:#4caf50; }
.alert-success .common-alert-btn { background:#4caf50; color:#fff; }
.alert-success .common-alert-btn:hover { background:#388e3c; }
</style>
@@ -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");
%>
<html>
<head>
<title></title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<jsp:include page="/jsp/common/include/css.jsp"/>
<jsp:include page="/jsp/common/include/script.jsp"/>
<style>
.badge-use { display:inline-block; padding:3px 10px; border-radius:12px; font-size:11px; font-weight:500; }
.badge-use.active { background:rgba(0,128,128,0.15); color:rgba(0,128,128,1); border:1px solid rgba(0,128,128,0.3); }
.badge-use.inactive { background:#f5f5f5; color:#999; border:1px solid #ddd; }
.threshold-display { font-size:12px; color:#666; }
.threshold-display .num { color:rgba(0,128,128,1); font-weight:600; }
.threshold-display .unit { color:#555; }
.threshold-display .code { background:#e8f4f4; color:rgba(0,128,128,0.9); padding:1px 5px; border-radius:3px; font-family:Consolas,monospace; font-size:10px; margin-left:4px; }
.datetime-display { font-size:11px; color:#666; }
.datetime-display .date { color:#333; }
.datetime-display .time { color:rgba(0,128,128,0.9); font-weight:500; }
</style>
<script language="javascript" >
var url ='<c:url value="/onl/admin/inflow/inflowGroupControlMan.json" />';
var url_view ='<c:url value="/onl/admin/inflow/inflowGroupControlMan.view" />';
function useYnBadgeFormatter(cellValue, options, rowObject) {
if (cellValue === '1') {
return '<span class="badge-use active">사용</span>';
} else {
return '<span class="badge-use inactive">미사용</span>';
}
}
function perSecondFormatter(cellValue, options, rowObject) {
if (!cellValue) return '<span style="color:#ccc;">-</span>';
return '<span class="threshold-display"><span class="num">' + Number(cellValue).toLocaleString() + '</span>건 / <span class="unit">초</span></span>';
}
function dateTimeFormatter(cellValue, options, rowObject) {
if (!cellValue) return '<span style="color:#ccc;">-</span>';
// 2025-12-10 16:34:16 형식 파싱
var parts = cellValue.split(' ');
if (parts.length < 2) return cellValue;
var datePart = parts[0].split('-');
var timePart = parts[1].substring(0, 5); // HH:mm만
if (datePart.length < 3) return cellValue;
var year = datePart[0];
var month = parseInt(datePart[1], 10);
var day = parseInt(datePart[2], 10);
var currentYear = new Date().getFullYear().toString();
var dateStr = (year === currentYear) ? month + '월 ' + day + '일' : year.substring(2) + '년 ' + month + '월 ' + day + '일';
return '<span class="datetime-display"><span class="date">' + dateStr + '</span> <span class="time">' + timePart + '</span></span>';
}
function thresholdFormatter(cellValue, options, rowObject) {
var threshold = rowObject.THRESHOLD;
var timeUnit = rowObject.THRESHOLDTIMEUNIT;
if (!threshold || !timeUnit) return '<span style="color:#ccc;">-</span>';
var unitLabels = {
'SEC': '초',
'MIN': '분',
'HOU': '시간',
'DAY': '일',
'MON': '월'
};
var label = unitLabels[timeUnit] || timeUnit;
return '<span class="threshold-display"><span class="num">' + Number(threshold).toLocaleString() + '</span>건 / <span class="unit">' + label + '</span><span class="code">' + timeUnit + '</span></span>';
}
$(document).ready(function() {
$('#grid').jqGrid({
datatype:"json",
mtype: 'POST',
url: url,
postData : { cmd : 'LIST', searchGroupName: $('input[name=searchGroupName]').val()},
colNames:['그룹 ID',
'그룹명',
'초당 임계치',
'추가 임계치',
'사용여부',
'수정일시'
],
colModel:[
{ name : 'GROUPID' , align:'left' , width:'100' , sortable:false, hidden:true },
{ name : 'GROUPNAME' , align:'left' , width:'250'},
{ name : 'THRESHOLDPERSECOND' , align:'center' , width:'100' , formatter:perSecondFormatter },
{ name : 'THRESHOLD' , align:'center' , width:'120' , formatter:thresholdFormatter },
{ name : 'USEYN' , align:'center' , width:'70' , formatter:useYnBadgeFormatter },
{ name : 'MODIFIEDAT' , align:'center' , width:'100' , formatter:dateTimeFormatter }
],
jsonReader: {
repeatitems:false
},
pager : $('#pager'),
page : '${param.page}',
rowNum : '${rmsDefaultRowNum}',
autoheight: true,
height: $("#container").height(),
autowidth: true,
viewrecords: true,
rowList : eval('[${rmsDefaultRowList}]'),
ondblClickRow: function(rowId) {
var rowData = $(this).getRowData(rowId);
var groupId = rowData['GROUPID'];
var url2 = url_view;
url2 += '?cmd=DETAIL';
url2 += '&page='+$(this).getGridParam("page");
url2 += '&returnUrl='+getReturnUrl();
url2 += '&menuId='+'${param.menuId}';
//검색값
url2 += '&searchGroupName='+$("input[name=searchGroupName]").val();
//key값
url2 += '&groupId='+groupId;
goNav(url2);
},
gridComplete:function (d){
var colModel = $(this).getGridParam("colModel");
for(var i = 0 ; i< colModel.length; i++){
$(this).setColProp(colModel[i].name, {sortable : false});
}
}
});
resizeJqGridWidth('grid','content_middle','1000');
$("#btn_search").click(function(){
$("#grid").setGridParam({ postData: { searchGroupName: $('input[name=searchGroupName]').val()}, page:1 }).trigger("reloadGrid");
});
$("#btn_new").click(function(){
var url2 = url_view;
url2 += '?cmd=DETAIL';
url2 += '&page='+$("#grid").getGridParam("page");
url2 += '&returnUrl='+getReturnUrl();
url2 += '&menuId='+'${param.menuId}';
//검색값
url2 += '&searchGroupName=';
goNav(url2);
});
$("input[name^=search]").keydown(function(key){
if (key.keyCode == 13){
$("#btn_search").click();
}
});
buttonControl();
});
</script>
</head>
<body>
<div class="right_box">
<div class="content_top">
<ul class="path">
<li><a href="#">${rmsMenuPath}</a></li>
</ul>
</div><!-- end content_top -->
<div class="content_middle" id="content_middle">
<div class="search_wrap">
<button type="button" class="cssbtn" id="btn_new" level="W"><i class="material-icons">add</i> <%= localeMessage.getString("button.new") %></button>
<button type="button" class="cssbtn" id="btn_search" level="R"><i class="material-icons">search</i> <%= localeMessage.getString("button.search") %></button>
</div>
<div class="title">유량제어 그룹 관리<span class="tooltip">유량제어 그룹 관리</span></div>
<table class="search_condition" cellspacing=0;>
<tbody>
<tr>
<th style="width:180px;">그룹명</th>
<td><input type="text" name="searchGroupName" value="${param.searchGroupName}"></td>
</tr>
</tbody>
</table>
<table id="grid" ></table>
<div id="pager"></div>
</div><!-- end content_middle -->
</div><!-- end right_box -->
</body>
</html>
@@ -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");
%>
<html>
<head>
<title></title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<jsp:include page="/jsp/common/include/css.jsp"/>
<jsp:include page="/jsp/common/include/script.jsp"/>
<style>
/* 폼 테이블 - 기존 table_row 스타일 기반 */
.form-table { width:100%; border:0; border-top:1px solid #a3a9b1; border-left:1px solid #a3a9b1; font-size:12px; color:#333; margin-bottom:15px; }
.form-table th { padding:8px 15px; border-right:1px solid #ebebec; border-bottom:1px solid #a3a9b1; text-align:left; background:rgb(255, 247, 231); font-weight:bold; white-space:nowrap; }
.form-table td { padding:8px 15px; border-right:1px solid #a3a9b1; border-bottom:1px solid #a3a9b1; }
.form-table input[type="text"], .form-table input[type="number"] { border:1px solid #ebebec; padding:4px 8px; }
.form-table input[type="number"] { width:100px; text-align:right; }
.form-table input:disabled { background:#ebebec; color:#999; }
.form-table .select-style { display:inline-block; overflow:hidden; border:1px solid #ebebec; background:#fff; }
.form-table .select-style select { border:none; box-shadow:none; background:transparent; padding:4px 8px; min-width:80px; }
/* iOS 스타일 토글 */
.toggle-wrap { display:inline-flex; align-items:center; gap:10px; }
.toggle-label { font-size:12px; color:#333; min-width:40px; }
.ios-toggle { position:relative; display:inline-block; width:44px; height:24px; }
.ios-toggle input { display:none; }
.ios-toggle-slider { position:absolute; top:0; left:0; right:0; bottom:0; background:#ccc; border-radius:24px; cursor:pointer; transition:all 0.3s ease; }
.ios-toggle-slider::before { content:''; position:absolute; top:2px; left:2px; width:20px; height:20px; background:#fff; border-radius:50%; box-shadow:0 1px 3px rgba(0,0,0,0.2); transition:all 0.3s ease; }
.ios-toggle input:checked + .ios-toggle-slider { background:rgba(0,128,128,0.85); }
.ios-toggle input:checked + .ios-toggle-slider::before { transform:translateX(20px); }
/* 임계치 입력 그룹 */
.threshold-group { display:inline-flex; align-items:center; gap:8px; }
.threshold-group .unit { font-size:11px; color:#666; min-width:60px; }
/* 스타일드 셀렉트 */
.styled-select { appearance:none; -webkit-appearance:none; -moz-appearance:none; padding:6px 30px 6px 12px; font-size:12px; border:1px solid #ddd; border-radius:4px; background:#fff url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23666' d='M6 8L1 3h10z'/%3E%3C/svg%3E") no-repeat right 10px center; cursor:pointer; min-width:120px; transition:all 0.2s; color:#333; }
.styled-select:hover { border-color:rgba(0,128,128,0.5); }
.styled-select:focus { outline:none; border-color:rgba(0,128,128,0.8); box-shadow:0 0 0 2px rgba(0,128,128,0.1); }
/* 힌트 아이콘 */
.hint-icon { display:inline-flex; align-items:center; cursor:pointer; color:#999; position:relative; margin-left:4px; vertical-align:middle; }
.hint-icon:hover { color:rgba(0,128,128,1); }
.hint-icon i { font-size:14px; }
.hint-icon .hint-bubble { display:none; position:absolute; bottom:calc(100% + 8px); left:0; background:#333; color:#fff; padding:10px 12px; border-radius:4px; font-size:11px; width:220px; line-height:1.5; z-index:1000; box-shadow:0 2px 8px rgba(0,0,0,0.2); white-space:normal; word-break:keep-all; }
.hint-icon .hint-bubble::after { content:''; position:absolute; top:100%; left:14px; border:6px solid transparent; border-top-color:#333; }
.hint-icon:hover .hint-bubble { display:block; }
/* 섹션 구분 라벨 */
.section-label { display:block; font-size:13px; color:#333; font-weight:bold; margin:20px 0 10px 0; padding-bottom:5px; border-bottom:2px solid rgba(0,128,128,0.3); }
/* 타이틀 래퍼 */
.title-wrap { display:flex; align-items:center; gap:12px; margin-bottom:10px; }
.title-wrap .title { margin-bottom:0; }
/* 수정일시 뱃지 */
.modified-badge { display:inline-flex; align-items:center; gap:4px; padding:4px 10px; background:linear-gradient(135deg, #f0f7f7 0%, #e8f4f4 100%); border:1px solid rgba(0,128,128,0.2); border-radius:20px; font-size:11px; color:#555; }
.modified-badge i { font-size:13px; color:rgba(0,128,128,0.7); }
/* 인터페이스 리스트 헤더 */
.interface-header { display:flex; justify-content:space-between; align-items:center; margin-bottom:8px; }
.interface-count { font-size:12px; color:#666; }
/* 인터페이스 리스트 테이블 */
.interface-list-wrap { border:1px solid #a3a9b1; border-radius:3px; max-height:250px; overflow-y:auto; margin-bottom:15px; }
.interface-list-table { width:100%; border-collapse:collapse; font-size:12px; }
.interface-list-table th { padding:8px 12px; background:rgb(255, 247, 231); border-bottom:1px solid #a3a9b1; text-align:left; font-weight:bold; position:sticky; top:0; }
.interface-list-table td { padding:8px 12px; border-bottom:1px solid #ebebec; }
.interface-list-table tr:last-child td { border-bottom:none; }
.interface-list-table tr:hover { background:#f9f9f9; }
.interface-list-table .col-id { width:200px; font-family:Consolas, monospace; color:rgb(65,125,200); }
.interface-list-table .col-action { width:80px; text-align:center; white-space:nowrap; }
/* 미니 토글 - teal 컬러 */
.mini-toggle { position:relative; display:inline-block; width:32px; height:18px; vertical-align:middle; margin-right:8px; }
.mini-toggle input { display:none; }
.mini-toggle-slider { position:absolute; top:0; left:0; right:0; bottom:0; background:#ccc; border-radius:18px; cursor:pointer; transition:all 0.2s; }
.mini-toggle-slider::before { content:''; position:absolute; top:2px; left:2px; width:14px; height:14px; background:#fff; border-radius:50%; transition:all 0.2s; }
.mini-toggle input:checked + .mini-toggle-slider { background:rgba(0,128,128,0.8); }
.mini-toggle input:checked + .mini-toggle-slider::before { transform:translateX(14px); }
/* 인라인 삭제 버튼 */
.btn-icon { background:none; border:none; cursor:pointer; color:#999; padding:2px; transition:color 0.2s; vertical-align:middle; }
.btn-icon:hover { color:#ff4d4f; }
.btn-icon i { font-size:16px; }
/* 빈 상태 메시지 */
.interface-empty { padding:30px 20px; text-align:center; color:#999; font-size:12px; }
.interface-empty i { vertical-align:middle; margin-right:4px; font-size:18px; }
/* 도움말 모달 */
.help-modal-overlay { display:none; position:fixed; top:0; left:0; width:100%; height:100%; background:rgba(0,0,0,0.4); z-index:9999; }
.help-modal { position:fixed; top:50%; left:50%; transform:translate(-50%,-50%); background:#fff; border-radius:5px; box-shadow:0 4px 20px rgba(0,0,0,0.3); width:700px; max-height:80vh; overflow-y:auto; }
.help-modal-header { display:flex; justify-content:space-between; align-items:center; padding:12px 20px; border-bottom:1px solid #ddd; background:rgb(255, 247, 231); }
.help-modal-header h3 { margin:0; font-size:14px; font-weight:bold; color:#333; }
.help-modal-close { background:none; border:none; font-size:20px; cursor:pointer; color:#666; }
.help-modal-close:hover { color:#333; }
.help-modal-body { padding:20px; font-size:12px; line-height:1.6; }
.help-modal-body h4 { margin:15px 0 8px 0; font-size:13px; color:#333; border-left:3px solid rgba(0,128,128,0.6); padding-left:10px; }
.help-modal-body h4:first-child { margin-top:0; }
.help-modal-body p { margin:6px 0; color:#555; }
.help-modal-body ul { margin:6px 0; padding-left:18px; }
.help-modal-body li { margin:4px 0; }
.help-modal-body .example-box { background:#f9f9f9; border:1px solid #ddd; border-radius:3px; padding:12px; margin:8px 0; }
.help-modal-body .example-title { font-weight:bold; color:#333; margin-bottom:6px; }
.help-modal-body table { width:100%; border-collapse:collapse; margin:8px 0; }
.help-modal-body table th, .help-modal-body table td { border:1px solid #ddd; padding:6px 10px; text-align:left; }
.help-modal-body table th { background:rgb(255, 247, 231); }
.help-modal-body .note { background:#fffde7; border:1px solid #fff176; border-radius:3px; padding:10px 12px; margin:12px 0; }
/* 인터페이스 선택 모달 */
.if-modal-overlay { display:none; position:fixed; top:0; left:0; width:100%; height:100%; background:rgba(0,0,0,0.4); z-index:9998; }
.if-modal { position:fixed; top:50%; left:50%; transform:translate(-50%,-50%); background:#fff; border-radius:5px; box-shadow:0 4px 20px rgba(0,0,0,0.3); width:850px; }
.if-modal-header { display:flex; justify-content:space-between; align-items:center; padding:12px 20px; border-bottom:1px solid #ddd; background:rgb(255, 247, 231); border-radius:5px 5px 0 0; }
.if-modal-header h3 { margin:0; font-size:14px; font-weight:bold; color:#333; }
.if-modal-close { background:none; border:none; font-size:20px; cursor:pointer; color:#666; }
.if-modal-close:hover { color:#333; }
.if-modal-body { padding:15px 20px; }
/* 검색 영역 */
.if-search-box { display:flex; gap:8px; margin-bottom:12px; }
.if-search-box input { flex:1; border:1px solid #ebebec; padding:6px 10px; font-size:12px; }
.if-search-box button { padding:6px 12px; font-size:12px; }
/* 테이블 */
.if-table-wrap { border:1px solid #a3a9b1; border-radius:3px; max-height:400px; overflow-y:auto; }
.if-table { width:100%; border-collapse:collapse; font-size:12px; }
.if-table th { padding:8px 12px; background:rgb(255, 247, 231); border-bottom:1px solid #a3a9b1; text-align:left; font-weight:bold; position:sticky; top:0; }
.if-table td { padding:8px 12px; border-bottom:1px solid #ebebec; cursor:pointer; }
.if-table tr:last-child td { border-bottom:none; }
.if-table tbody tr:hover { background:#e8f4f4; }
.if-table tbody tr.selected { background:rgba(0,128,128,0.15); }
.if-table .col-id { width:180px; font-family:Consolas, monospace; color:rgb(65,125,200); }
.if-table-empty { padding:40px 20px; text-align:center; color:#999; }
/* 페이지네이션 */
.if-pagination { display:flex; justify-content:space-between; align-items:center; margin-top:12px; font-size:12px; }
.if-page-info { color:#666; }
.if-page-nav { display:flex; align-items:center; gap:4px; }
.if-page-btn { min-width:28px; height:28px; padding:0 6px; border:1px solid #ddd; background:#fff; cursor:pointer; font-size:11px; border-radius:3px; }
.if-page-btn:hover:not(:disabled) { background:#f0f0f0; border-color:#bbb; }
.if-page-btn:disabled { color:#ccc; cursor:default; }
.if-page-btn.active { background:rgba(0,128,128,0.8); color:#fff; border-color:rgba(0,128,128,0.8); }
.if-page-num { display:flex; gap:2px; }
/* 하단 버튼 */
.if-modal-footer { display:flex; justify-content:flex-end; gap:8px; padding:12px 20px; border-top:1px solid #ddd; background:#fafafa; border-radius:0 0 5px 5px; }
</style>
<script language="javascript">
var url ='<c:url value="/onl/admin/inflow/inflowGroupControlMan.json" />';
var url_view ='<c:url value="/onl/admin/inflow/inflowGroupControlMan.view" />';
var isDetail = false;
var interfaceList = [];
function isValid() {
if ($('input[name=groupName]').val().trim() == "") {
alert("그룹명을 입력하여 주십시요.");
$('input[name=groupName]').focus();
return false;
}
return true;
}
function updateThresholdLabel() {
var timeUnit = $('#thresholdTimeUnit').val();
var $input = $('#thresholdInput');
var $unit = $('#thresholdUnit');
var unitLabels = {
'': '',
'SEC': 'req/sec',
'MIN': 'req/min',
'HOU': 'req/hour',
'DAY': 'req/day',
'MON': 'req/month'
};
$unit.text(unitLabels[timeUnit] || 'requests');
if (timeUnit === '') {
$input.prop('disabled', true).val('');
$unit.css('visibility', 'hidden');
} else {
$input.prop('disabled', false);
$unit.css('visibility', 'visible');
}
}
function init(url, key, callback) {
// useYn 초기값 설정 (기본: 사용)
$('#useYn').val('1');
updateThresholdLabel();
if (typeof callback === 'function') {
callback(url, key);
}
}
function getSelectedTimeUnitText() {
return $('#thresholdTimeUnit option:selected').text();
}
function detail(url, key) {
if (!isDetail) return;
$.ajax({
type: "POST",
url: url,
dataType: "json",
data: {cmd: 'DETAIL', groupId: key},
success: function(json) {
$("input[name=groupId]").val(json.GROUPID || json.groupId);
$("input[name=groupName]").val(json.GROUPNAME || json.groupName);
$("input[name=thresholdPerSecond]").val(json.THRESHOLDPERSECOND || json.thresholdPerSecond);
$("input[name=threshold]").val(json.THRESHOLD || json.threshold);
$("select[name=thresholdTimeUnit]").val(json.THRESHOLDTIMEUNIT || json.thresholdTimeUnit || '');
var useYnVal = json.USEYN || json.useYn || '1';
$('#useYn').val(useYnVal);
$('#useYnToggle').prop('checked', useYnVal === '1');
$('#useYnLabel').text(useYnVal === '1' ? '사용' : '미사용');
updateThresholdLabel();
// 수정일시 표시
var modifiedAt = json.MODIFIEDAT || json.modifiedAt;
if (modifiedAt) {
$('#modifiedAtText').text(modifiedAt);
$('#modifiedAtInfo').show();
}
interfaceList = [];
if (json.interfaceList && json.interfaceList.length > 0) {
$.each(json.interfaceList, function(i, item) {
interfaceList.push({
interfaceId: item.interfaceId || item.INTERFACEID,
interfaceDesc: item.interfaceDesc || item.INTERFACEDESC
});
});
}
renderInterfaceList();
},
error: function(e) {
alert(e.responseText);
}
});
}
function renderInterfaceList() {
var container = $('#interfaceListContainer');
container.empty();
$('#interfaceCountNum').text(interfaceList.length);
if (interfaceList.length === 0) {
container.html('<tr class="interface-empty-row" id="interfaceEmpty"><td colspan="3" class="interface-empty"><i class="material-icons">inbox</i> 매핑된 API가 없습니다</td></tr>');
return;
}
$.each(interfaceList, function(i, item) {
var useYn = item.useYn !== undefined ? item.useYn : '1';
var checked = useYn === '1' ? 'checked' : '';
var row = '<tr data-id="' + item.interfaceId + '">' +
'<td class="col-id">' + item.interfaceId + '</td>' +
'<td class="col-desc">' + (item.interfaceDesc || '-') + '</td>' +
'<td class="col-action">' +
'<label class="mini-toggle" title="사용여부">' +
'<input type="checkbox" class="interface-use-toggle" data-id="' + item.interfaceId + '" ' + checked + ' />' +
'<span class="mini-toggle-slider"></span>' +
'</label>' +
'<button type="button" class="btn-icon btn-delete-item" data-id="' + item.interfaceId + '" title="삭제">' +
'<i class="material-icons">close</i>' +
'</button>' +
'</td>' +
'</tr>';
container.append(row);
});
}
function getInterfaceListJson() {
var gridData = [];
$.each(interfaceList, function(i, item) {
gridData.push({interfaceId: item.interfaceId});
});
return JSON.stringify(gridData);
}
// 인터페이스 선택 모달 관련
var ifModalState = {
data: [],
page: 1,
pageSize: 10,
totalCount: 0,
selectedItem: null
};
function openIfModal() {
ifModalState.page = 1;
ifModalState.selectedItem = null;
$('#ifSearchInput').val('');
$('#ifModal').fadeIn(200);
loadIfList();
}
function closeIfModal() {
$('#ifModal').fadeOut(200);
}
function loadIfList() {
var searchName = $('#ifSearchInput').val();
$.ajax({
type: "POST",
url: url,
dataType: "json",
data: {
cmd: 'INTERFACE_LIST',
searchName: searchName,
page: ifModalState.page,
rows: ifModalState.pageSize
},
success: function(json) {
ifModalState.data = json.rows || [];
ifModalState.totalCount = json.records || 0;
renderIfTable();
renderIfPagination();
},
error: function(e) {
alert(e.responseText);
}
});
}
function renderIfTable() {
var tbody = $('#ifTableBody');
tbody.empty();
if (ifModalState.data.length === 0) {
tbody.html('<tr><td colspan="2" class="if-table-empty">검색 결과가 없습니다</td></tr>');
return;
}
$.each(ifModalState.data, function(i, item) {
var interfaceId = item.INTERFACEID || item.interfaceId;
var interfaceDesc = item.INTERFACEDESC || item.interfaceDesc || '-';
var row = $('<tr data-id="' + interfaceId + '" data-desc="' + interfaceDesc + '">' +
'<td class="col-id">' + interfaceId + '</td>' +
'<td>' + interfaceDesc + '</td>' +
'</tr>');
tbody.append(row);
});
// 행 클릭 이벤트
tbody.find('tr').on('click', function() {
tbody.find('tr').removeClass('selected');
$(this).addClass('selected');
ifModalState.selectedItem = {
interfaceId: $(this).data('id'),
interfaceDesc: $(this).data('desc')
};
});
// 더블클릭으로 바로 선택
tbody.find('tr').on('dblclick', function() {
ifModalState.selectedItem = {
interfaceId: $(this).data('id'),
interfaceDesc: $(this).data('desc')
};
selectInterface();
});
}
function renderIfPagination() {
var totalPages = Math.ceil(ifModalState.totalCount / ifModalState.pageSize);
var currentPage = ifModalState.page;
$('#ifTotalCount').text(ifModalState.totalCount);
// 이전/다음 버튼
$('#ifPrevBtn').prop('disabled', currentPage <= 1);
$('#ifNextBtn').prop('disabled', currentPage >= totalPages);
// 페이지 번호
var pageNumContainer = $('#ifPageNum');
pageNumContainer.empty();
if (totalPages <= 0) return;
var startPage = Math.max(1, currentPage - 2);
var endPage = Math.min(totalPages, startPage + 4);
if (endPage - startPage < 4) {
startPage = Math.max(1, endPage - 4);
}
for (var i = startPage; i <= endPage; i++) {
var activeClass = (i === currentPage) ? ' active' : '';
var btn = $('<button type="button" class="if-page-btn' + activeClass + '">' + i + '</button>');
btn.data('page', i);
pageNumContainer.append(btn);
}
// 페이지 번호 클릭
pageNumContainer.find('.if-page-btn').on('click', function() {
ifModalState.page = $(this).data('page');
ifModalState.selectedItem = null;
loadIfList();
});
}
function selectInterface() {
if (!ifModalState.selectedItem) {
alert('API를 선택하여 주십시오.');
return;
}
var newId = String(ifModalState.selectedItem.interfaceId);
// 현재 화면 내 중복 체크
var exists = interfaceList.some(function(item) {
return String(item.interfaceId) === newId;
});
if (exists) {
alert('이미 추가된 API입니다.');
return;
}
// 서버에 다른 그룹 등록 여부 체크
var groupId = $('input[name=groupId]').val();
$.ajax({
type: 'POST',
url: url,
data: { cmd: 'LIST_CHECK_DUPLICATE', interfaceId: newId, groupId: groupId },
dataType: 'json',
async: false,
success: function(result) {
if (result.duplicate) {
showAlert('<strong>' + newId + '</strong> API는<br/><strong>[' + result.groupName + ']</strong> 그룹에 이미 등록되어 있습니다.', {
type: 'warning',
title: '중복 등록 불가'
});
} else {
interfaceList.push(ifModalState.selectedItem);
renderInterfaceList();
closeIfModal();
}
},
error: function() {
showAlert('중복 체크 중 오류가 발생했습니다.', { type: 'error' });
}
});
}
$(document).ready(function() {
var returnUrl = getReturnUrlForReturn();
var key = "${param.groupId}";
if (key != "" && key != "null") {
isDetail = true;
}
init(url, key, detail);
$('#thresholdTimeUnit').on('change', function() {
updateThresholdLabel();
});
// 사용여부 토글 변경 시 hidden 값 및 라벨 업데이트
$('#useYnToggle').on('change', function() {
var isChecked = $(this).is(':checked');
$('#useYn').val(isChecked ? '1' : '0');
$('#useYnLabel').text(isChecked ? '사용' : '미사용');
});
// 인터페이스 삭제 버튼 (동적 요소이므로 위임)
$(document).on('click', '.btn-delete-item', function() {
var id = $(this).data('id');
interfaceList = interfaceList.filter(function(item) {
return item.interfaceId !== id;
});
renderInterfaceList();
});
// 인터페이스 사용여부 토글 (동적 요소이므로 위임)
$(document).on('change', '.interface-use-toggle', function() {
var id = $(this).data('id');
var useYn = $(this).is(':checked') ? '1' : '0';
$.each(interfaceList, function(i, item) {
if (item.interfaceId === id) {
item.useYn = useYn;
}
});
});
$("#btn_modify").click(function() {
if (!isValid()) return;
var confirmMsg = isDetail ? '변경사항을 저장하시겠습니까?' : '새 그룹을 등록하시겠습니까?';
showConfirm(confirmMsg, {
title: '저장 확인',
confirmText: '저장',
onConfirm: function() {
var postData = $('#ajaxForm').serializeArray();
if (isDetail) {
postData.push({name: "cmd", value: "UPDATE"});
} else {
postData.push({name: "cmd", value: "INSERT"});
}
postData.push({name: "interfaceListJson", value: getInterfaceListJson()});
$.ajax({
type: "POST",
url: url,
data: postData,
success: function(args) {
showAlert("<%= localeMessage.getString("common.saveMsg") %>", {
type: 'success',
title: '저장 완료',
onClose: function() {
goNav(returnUrl);
}
});
},
error: function(e) {
showAlert(e.responseText, { type: 'error' });
}
});
}
});
});
$("#btn_delete").click(function() {
var groupName = $('input[name=groupName]').val();
showConfirm('<strong>[' + groupName + ']</strong> 그룹을 삭제하시겠습니까?<br/><span style="color:#999;font-size:11px;">매핑된 API 정보도 함께 삭제됩니다.</span>', {
type: 'error',
title: '삭제 확인',
confirmText: '삭제',
onConfirm: function() {
$.ajax({
type: "POST",
url: url,
data: {cmd: 'DELETE', groupId: $('input[name=groupId]').val()},
success: function(args) {
showAlert("<%= localeMessage.getString("common.deleteMsg") %>", {
type: 'success',
title: '삭제 완료',
onClose: function() {
goNav(returnUrl);
}
});
},
error: function(e) {
showAlert(e.responseText, { type: 'error' });
}
});
}
});
});
$("#btn_previous").click(function() {
goNav(returnUrl);
});
// 인터페이스 추가 버튼 → 모달 열기
$("#btn_popup_interface").click(function() {
openIfModal();
});
// 인터페이스 선택 모달 이벤트
$(".if-modal-close, #ifCancelBtn").click(function() {
closeIfModal();
});
$("#ifModal").click(function(e) {
if (e.target === this) closeIfModal();
});
$("#ifSearchBtn").click(function() {
ifModalState.page = 1;
ifModalState.selectedItem = null;
loadIfList();
});
$("#ifSearchInput").keydown(function(e) {
if (e.keyCode === 13) {
e.preventDefault();
$("#ifSearchBtn").click();
}
});
$("#ifSelectBtn").click(function() {
selectInterface();
});
$("#ifPrevBtn").click(function() {
if (ifModalState.page > 1) {
ifModalState.page--;
ifModalState.selectedItem = null;
loadIfList();
}
});
$("#ifNextBtn").click(function() {
var totalPages = Math.ceil(ifModalState.totalCount / ifModalState.pageSize);
if (ifModalState.page < totalPages) {
ifModalState.page++;
ifModalState.selectedItem = null;
loadIfList();
}
});
buttonControl(isDetail);
titleControl(isDetail);
// 도움말 모달
$("#btn_help").click(function() {
$("#helpModal").fadeIn(200);
});
$(".help-modal-close, .help-modal-overlay").click(function(e) {
if (e.target === this) {
$("#helpModal").fadeOut(200);
}
});
$(document).keydown(function(e) {
if (e.keyCode === 27) {
$("#helpModal").fadeOut(200);
$("#ifModal").fadeOut(200);
}
});
});
</script>
</head>
<body>
<div class="right_box">
<div class="content_top">
<ul class="path">
<li><a href="#">${rmsMenuPath}</a></li>
</ul>
</div>
<div class="content_middle" id="content_middle">
<div class="search_wrap">
<button type="button" class="cssbtn" id="btn_help"><i class="material-icons">help_outline</i> 도움말</button>
<button type="button" class="cssbtn" id="btn_delete" level="W" status="DETAIL"><i class="material-icons">delete</i> <%= localeMessage.getString("button.delete") %></button>
<button type="button" class="cssbtn" id="btn_modify" level="W" status="DETAIL,NEW"><i class="material-icons">save</i> <%= localeMessage.getString("button.save") %></button>
<button type="button" class="cssbtn" id="btn_previous"><i class="material-icons">arrow_back</i> <%= localeMessage.getString("button.previous") %></button>
</div>
<div class="title-wrap">
<div class="title">유량제어 그룹 관리<span class="tooltip">유량제어 그룹 관리</span></div>
<span id="modifiedAtInfo" class="modified-badge" style="display:none;">
<i class="material-icons">update</i><span id="modifiedAtText">-</span>
</span>
</div>
<form id="ajaxForm">
<input type="hidden" name="groupId" />
<input type="hidden" name="useYn" id="useYn" value="1" />
<!-- 기본 정보 -->
<table class="form-table">
<colgroup>
<col style="width:120px" />
<col />
</colgroup>
<tbody>
<tr>
<th>그룹명 <span style="color:red">*</span></th>
<td><input type="text" name="groupName" style="width:300px" /></td>
</tr>
<tr>
<th>사용여부</th>
<td>
<div class="toggle-wrap">
<label class="ios-toggle">
<input type="checkbox" id="useYnToggle" checked />
<span class="ios-toggle-slider"></span>
</label>
<span class="toggle-label" id="useYnLabel">사용</span>
</div>
</td>
</tr>
</tbody>
</table>
<!-- 유량제어 설정 -->
<span class="section-label">유량제어 설정</span>
<table class="form-table">
<colgroup>
<col style="width:120px" />
<col />
</colgroup>
<tbody>
<tr>
<th>
초당 임계치
<span class="hint-icon"><i class="material-icons">help_outline</i>
<span class="hint-bubble">1초 단위의 순간 트래픽 제한입니다. 매 초마다 설정한 건수만큼 토큰이 리필되며, 초과 시 요청이 거부됩니다.</span>
</span>
</th>
<td>
<div class="threshold-group">
<input type="number" name="thresholdPerSecond" placeholder="0" />
<span class="unit">req/sec</span>
</div>
</td>
</tr>
<tr>
<th>
추가 임계치
<span class="hint-icon"><i class="material-icons">help_outline</i>
<span class="hint-bubble">장시간 누적 트래픽 제한입니다. 시간단위를 선택하면 해당 주기마다 토큰이 리필됩니다.</span>
</span>
</th>
<td>
<div class="threshold-group">
<select name="thresholdTimeUnit" id="thresholdTimeUnit" class="styled-select">
<option value="">미사용</option>
<option value="SEC">초 (SEC)</option>
<option value="MIN">분 (MIN)</option>
<option value="HOU">시간 (HOU)</option>
<option value="DAY">일 (DAY)</option>
<option value="MON">월 (MON)</option>
</select>
<input type="number" name="threshold" id="thresholdInput" placeholder="0" />
<span class="unit" id="thresholdUnit">requests</span>
</div>
</td>
</tr>
</tbody>
</table>
<!-- API 매핑 -->
<span class="section-label">API 매핑</span>
<div class="interface-header">
<span class="interface-count">매핑된 API <strong id="interfaceCountNum">0</strong>개</span>
<button type="button" class="cssbtn" id="btn_popup_interface"><i class="material-icons">add</i> 추가</button>
</div>
<div class="interface-list-wrap">
<table class="interface-list-table">
<colgroup>
<col class="col-id" />
<col class="col-desc" />
<col class="col-action" />
</colgroup>
<thead>
<tr>
<th>API ID</th>
<th>설명</th>
<th>관리</th>
</tr>
</thead>
<tbody id="interfaceListContainer">
<tr class="interface-empty-row" id="interfaceEmpty">
<td colspan="3" class="interface-empty"><i class="material-icons">inbox</i> 매핑된 API가 없습니다</td>
</tr>
</tbody>
</table>
</div>
</form>
</div>
</div>
<!-- 인터페이스 선택 모달 -->
<div class="if-modal-overlay" id="ifModal">
<div class="if-modal">
<div class="if-modal-header">
<h3>API 선택</h3>
<button type="button" class="if-modal-close">&times;</button>
</div>
<div class="if-modal-body">
<div class="if-search-box">
<input type="text" id="ifSearchInput" placeholder="API ID 또는 설명 검색" />
<button type="button" class="cssbtn" id="ifSearchBtn"><i class="material-icons">search</i> 검색</button>
</div>
<div class="if-table-wrap">
<table class="if-table">
<thead>
<tr>
<th class="col-id">API ID</th>
<th>설명</th>
</tr>
</thead>
<tbody id="ifTableBody">
<tr><td colspan="2" class="if-table-empty">검색 결과가 없습니다</td></tr>
</tbody>
</table>
</div>
<div class="if-pagination">
<span class="if-page-info">총 <strong id="ifTotalCount">0</strong>건</span>
<div class="if-page-nav">
<button type="button" class="if-page-btn" id="ifPrevBtn" disabled>&lt;</button>
<div class="if-page-num" id="ifPageNum"></div>
<button type="button" class="if-page-btn" id="ifNextBtn" disabled>&gt;</button>
</div>
</div>
</div>
<div class="if-modal-footer">
<button type="button" class="cssbtn" id="ifCancelBtn">취소</button>
<button type="button" class="cssbtn" id="ifSelectBtn"><i class="material-icons">check</i> 선택</button>
</div>
</div>
</div>
<!-- 도움말 모달 -->
<div class="help-modal-overlay" id="helpModal">
<div class="help-modal">
<div class="help-modal-header">
<h3>유량제어 임계치 설명</h3>
<button type="button" class="help-modal-close">&times;</button>
</div>
<div class="help-modal-body">
<h4>개요</h4>
<p>유량제어는 <strong>Token Bucket 알고리즘</strong> 기반으로 동작합니다.
설정된 임계치만큼 토큰이 주기적으로 리필되며, 요청 1건당 토큰 1개를 소비합니다.
토큰이 부족하면 요청이 거부됩니다. </p>
<h4>초당 임계치 (thresholdPerSecond)</h4>
<p>1초 단위의 순간적인 트래픽 급증(spike)을 방어합니다.</p>
<div class="example-box">
<div class="example-title">예시: 초당 임계치 = 100</div>
<ul>
<li>매 1초마다 토큰 100개 리필</li>
<li>1초 내 100건까지 처리 가능</li>
<li>101번째 요청부터 차단</li>
</ul>
</div>
<h4>추가 임계치 (threshold) + 시간단위</h4>
<p>장시간 누적 트래픽을 제어합니다. 시간단위에 따라 토큰 리필 주기가 결정됩니다.</p>
<table>
<tr><th>시간단위</th><th>설명</th><th>예시</th></tr>
<tr><td>SEC</td><td>초</td><td>1초당 N건</td></tr>
<tr><td>MIN</td><td>분</td><td>1분당 N건</td></tr>
<tr><td>HOU</td><td>시</td><td>1시간당 N건</td></tr>
<tr><td>DAY</td><td>일</td><td>1일당 N건</td></tr>
<tr><td>MON</td><td>월</td><td>1개월당 N건</td></tr>
</table>
<div class="example-box">
<div class="example-title">예시: 추가 임계치 = 10,000 / 시간단위 = HOU</div>
<ul>
<li>매 1시간마다 토큰 10,000개 리필</li>
<li>1시간 내 10,000건까지 처리 가능</li>
<li>10,001번째 요청부터 차단</li>
</ul>
</div>
<h4>두 임계치 조합 (권장)</h4>
<p>두 조건을 함께 설정하면 <strong>이중 보호</strong>가 적용됩니다.</p>
<div class="example-box">
<div class="example-title">예시: 초당 100건 + 시간당 50,000건</div>
<ul>
<li>순간 폭주 방지: 초당 100건 제한 → 1초 내 101번째 요청부터 차단</li>
<li>장기 누적 제한: 시간당 50,000건 제한 → 시간 내 50,001번째 요청부터 차단</li>
<li>두 조건 중 하나라도 초과 시 차단 (둘 다 충족해야 통과)</li>
</ul>
</div>
<div class="note">
<strong>참고:</strong> 그룹에 매핑된 인터페이스들은 개별 유량제어 대신 그룹 유량제어가 적용됩니다.
그룹 내 모든 인터페이스의 호출이 합산되어 임계치와 비교됩니다.
</div>
</div>
</div>
</div>
</body>
</html>
@@ -0,0 +1,25 @@
package com.eactive.eai.rms.data.entity.onl.inflow;
import java.util.List;
import java.util.Optional;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import com.eactive.eai.data.entity.onl.inflow.InflowControlGroupMapping;
import com.eactive.eai.data.jpa.BaseRepository;
public interface InflowControlGroupMappingRepository extends BaseRepository<InflowControlGroupMapping, String> {
List<InflowControlGroupMapping> findByGroupId(String groupId);
@Modifying
@Query("DELETE FROM InflowControlGroupMapping m WHERE m.groupId = :groupId")
void deleteByGroupId(@Param("groupId") String groupId);
/**
* 인터페이스 ID로 매핑 조회 (중복 체크용)
*/
Optional<InflowControlGroupMapping> findByInterfaceId(String interfaceId);
}
@@ -0,0 +1,8 @@
package com.eactive.eai.rms.data.entity.onl.inflow;
import com.eactive.eai.data.entity.onl.inflow.InflowControlGroup;
import com.eactive.eai.data.jpa.BaseRepository;
public interface InflowControlGroupRepository extends BaseRepository<InflowControlGroup, String> {
}
@@ -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<GridResponse<InflowGroupControlManUI>> selectList(HttpServletRequest request,
Pageable pageVo, String searchGroupName) {
Page<InflowGroupControlManUI> uiPage = service.selectGroupList(pageVo, searchGroupName);
return ResponseEntity.ok(new GridResponse<>(uiPage));
}
/**
* 그룹 상세 조회
*/
@RequestMapping(value = "/onl/admin/inflow/inflowGroupControlMan.json", params = "cmd=DETAIL")
public ResponseEntity<InflowGroupControlManUI> selectDetail(HttpServletRequest request,
HttpServletResponse response, String groupId) {
InflowGroupControlManUI ui = service.selectGroupDetail(groupId);
return ResponseEntity.ok(ui);
}
/**
* 그룹 저장 (INSERT)
*/
@RequestMapping(value = "/onl/admin/inflow/inflowGroupControlMan.json", params = "cmd=INSERT")
public String insert(HttpServletRequest request, HttpServletResponse response, InflowGroupControlManUI ui,
@RequestParam(value = "interfaceListJson", required = false) String interfaceListJson) throws Exception {
parseInterfaceList(ui, interfaceListJson);
String modifiedBy = getSessionUserId(request);
service.mergeGroup(ui, modifiedBy);
// Agent Command 전송
CommonCommand command = new CommonCommand("com.eactive.eai.agent.inflow.ReloadInflowGroupControlCommand",
ui.getGroupId());
agentUtilService.broadcast(command);
return null;
}
/**
* 그룹 저장 (UPDATE)
*/
@RequestMapping(value = "/onl/admin/inflow/inflowGroupControlMan.json", params = "cmd=UPDATE")
public String update(HttpServletRequest request, HttpServletResponse response, InflowGroupControlManUI ui,
@RequestParam(value = "interfaceListJson", required = false) String interfaceListJson) throws Exception {
parseInterfaceList(ui, interfaceListJson);
String modifiedBy = getSessionUserId(request);
service.mergeGroup(ui, modifiedBy);
// Agent Command 전송
CommonCommand command = new CommonCommand("com.eactive.eai.agent.inflow.ReloadInflowGroupControlCommand",
ui.getGroupId());
agentUtilService.broadcast(command);
return null;
}
/**
* 그룹 삭제
*/
@RequestMapping(value = "/onl/admin/inflow/inflowGroupControlMan.json", params = "cmd=DELETE")
public String delete(HttpServletRequest request, HttpServletResponse response, String groupId) throws Exception {
service.deleteGroup(groupId);
// Agent Command 전송
CommonCommand command = new CommonCommand("com.eactive.eai.agent.inflow.RemoveInflowGroupControlCommand",
groupId);
agentUtilService.broadcast(command);
return null;
}
/**
* 콤보박스 초기화 데이터
*/
@RequestMapping(value = "/onl/admin/inflow/inflowGroupControlMan.json", params = "cmd=LIST_INIT_COMBO")
public ModelAndView initCombo(HttpServletRequest request, HttpServletResponse response) {
List<ComboVo> useYnRows = comboService.getFromCode("USE_YN");
List<ComboVo> timeUnitRows = comboService.getFromCode("INFLOW_CTRL_TIME_UNIT");
Map<String, List<ComboVo>> resultMap = new HashMap<>();
resultMap.put("useYnRows", useYnRows);
resultMap.put("timeUnitRows", timeUnitRows);
return new ModelAndView("jsonView", resultMap);
}
/**
* 인터페이스 목록 조회 (팝업용)
*/
@RequestMapping(value = "/onl/admin/inflow/inflowGroupControlMan.json", params = "cmd=INTERFACE_LIST")
public ResponseEntity<GridResponse<InflowGroupMappingUI>> selectInterfaceList(HttpServletRequest request,
Pageable pageVo, String searchName) {
Page<InflowGroupMappingUI> uiPage = service.selectInterfaceListForPopup(pageVo, searchName);
return ResponseEntity.ok(new GridResponse<>(uiPage));
}
/**
* 인터페이스 중복 등록 체크
*/
@RequestMapping(value = "/onl/admin/inflow/inflowGroupControlMan.json", params = "cmd=LIST_CHECK_DUPLICATE")
public ModelAndView checkDuplicate(HttpServletRequest request, HttpServletResponse response,
@RequestParam("interfaceId") String interfaceId,
@RequestParam(value = "groupId", required = false) String groupId) {
String existingGroupName = service.checkInterfaceDuplicate(interfaceId, groupId);
Map<String, Object> resultMap = new HashMap<>();
resultMap.put("duplicate", existingGroupName != null);
resultMap.put("groupName", existingGroupName);
return new ModelAndView("jsonView", resultMap);
}
/**
* JSON 문자열을 인터페이스 목록으로 변환
*/
private void parseInterfaceList(InflowGroupControlManUI ui, String interfaceListJson) throws Exception {
if (interfaceListJson != null && !interfaceListJson.isEmpty()) {
List<InflowGroupMappingUI> interfaceList = objectMapper.readValue(interfaceListJson,
new TypeReference<List<InflowGroupMappingUI>>() {
});
ui.setInterfaceList(interfaceList);
}
}
/**
* 세션에서 사용자 ID 조회
*/
private String getSessionUserId(HttpServletRequest request) {
Object userId = request.getSession().getAttribute("userId");
return userId != null ? userId.toString() : "SYSTEM";
}
}
@@ -0,0 +1,29 @@
package com.eactive.eai.rms.onl.manage.inflow.group;
import org.mapstruct.BeanMapping;
import org.mapstruct.Mapper;
import org.mapstruct.MappingTarget;
import org.mapstruct.NullValuePropertyMappingStrategy;
import org.mapstruct.ReportingPolicy;
import com.eactive.eai.data.entity.onl.inflow.InflowControlGroup;
import com.eactive.eai.data.entity.onl.inflow.InflowControlGroupMapping;
@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE)
public interface InflowGroupControlManMapper {
InflowGroupControlManUI toVo(InflowControlGroup entity);
InflowControlGroup toEntity(InflowGroupControlManUI vo);
@BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
void updateToEntity(InflowGroupControlManUI vo, @MappingTarget InflowControlGroup entity);
// Mapping Entity <-> UI
InflowGroupMappingUI toMappingVo(InflowControlGroupMapping entity);
InflowControlGroupMapping toMappingEntity(InflowGroupMappingUI vo);
@BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
void updateToMappingEntity(InflowGroupMappingUI vo, @MappingTarget InflowControlGroupMapping entity);
}
@@ -0,0 +1,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<InflowGroupControlManUI> selectGroupList(Pageable pageable, String searchGroupName) {
JPAQueryFactory jpaQueryFactory = new JPAQueryFactory(entityManager);
QInflowControlGroup qGroup = QInflowControlGroup.inflowControlGroup;
BooleanBuilder boolBuilder = new BooleanBuilder();
if (!StringUtils.isEmpty(searchGroupName)) {
boolBuilder.and(qGroup.groupName.contains(searchGroupName));
}
List<InflowControlGroup> groupList = jpaQueryFactory
.selectFrom(qGroup)
.where(boolBuilder)
.orderBy(qGroup.groupId.asc())
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetch();
List<InflowGroupControlManUI> uiList = groupList.stream()
.map(mapper::toVo)
.collect(Collectors.toList());
long totalCount = jpaQueryFactory
.select(qGroup.groupId.count())
.from(qGroup)
.where(boolBuilder)
.fetchOne();
return new PageImpl<>(uiList, pageable, totalCount);
}
/**
* 그룹 상세 조회 (매핑된 인터페이스 포함)
*/
public InflowGroupControlManUI selectGroupDetail(String groupId) {
Optional<InflowControlGroup> groupOpt = groupRepository.findById(groupId);
if (!groupOpt.isPresent()) {
return null;
}
InflowGroupControlManUI ui = mapper.toVo(groupOpt.get());
// 매핑된 인터페이스 목록 조회 (인터페이스 설명 포함)
List<InflowGroupMappingUI> mappingList = selectMappingListWithDesc(groupId);
ui.setInterfaceList(mappingList);
return ui;
}
/**
* 그룹에 매핑된 인터페이스 목록 조회 (인터페이스 설명 포함)
*/
private List<InflowGroupMappingUI> selectMappingListWithDesc(String groupId) {
JPAQueryFactory jpaQueryFactory = new JPAQueryFactory(entityManager);
QInflowControlGroupMapping qMapping = QInflowControlGroupMapping.inflowControlGroupMapping;
QEAIMessageEntity qMessage = QEAIMessageEntity.eAIMessageEntity;
List<Tuple> tupleList = jpaQueryFactory
.select(qMapping, qMessage.eaisvcdesc)
.from(qMapping)
.leftJoin(qMessage).on(qMapping.interfaceId.eq(qMessage.eaisvcname))
.where(qMapping.groupId.eq(groupId))
.orderBy(qMapping.interfaceId.asc())
.fetch();
return tupleList.stream()
.map(tuple -> {
InflowGroupMappingUI mappingUI = mapper.toMappingVo(tuple.get(qMapping));
mappingUI.setInterfaceDesc(tuple.get(qMessage.eaisvcdesc));
return mappingUI;
})
.collect(Collectors.toList());
}
/**
* 그룹 저장 (INSERT/UPDATE)
*/
@Transactional
public void mergeGroup(InflowGroupControlManUI ui, String modifiedBy) {
String groupId = ui.getGroupId();
boolean isNew = StringUtils.isEmpty(groupId);
InflowControlGroup entity;
if (!isNew) {
Optional<InflowControlGroup> groupOpt = groupRepository.findById(groupId);
if (groupOpt.isPresent()) {
entity = groupOpt.get();
mapper.updateToEntity(ui, entity);
} else {
entity = mapper.toEntity(ui);
}
} else {
entity = mapper.toEntity(ui);
}
entity.setModifiedBy(modifiedBy);
entity.setModifiedAt(LocalDateTime.now());
InflowControlGroup savedEntity = groupRepository.saveAndFlush(entity);
// 저장된 엔티티의 groupId 사용 (신규 생성 시 시퀀스에서 생성된 ID)
String savedGroupId = savedEntity.getGroupId();
// 매핑 정보 저장
saveMappings(savedGroupId, ui.getInterfaceList(), modifiedBy);
}
/**
* 그룹-인터페이스 매핑 저장
*/
@Transactional
public void saveMappings(String groupId, List<InflowGroupMappingUI> interfaceList, String modifiedBy) {
// 기존 매핑 삭제
mappingRepository.deleteByGroupId(groupId);
// 새로운 매핑 저장
if (interfaceList != null && !interfaceList.isEmpty()) {
for (InflowGroupMappingUI mappingUI : interfaceList) {
InflowControlGroupMapping mapping = new InflowControlGroupMapping();
mapping.setInterfaceId(mappingUI.getInterfaceId());
mapping.setGroupId(groupId);
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<InflowControlGroupMapping> mappingOpt = mappingRepository.findByInterfaceId(interfaceId);
if (!mappingOpt.isPresent()) {
return null;
}
InflowControlGroupMapping mapping = mappingOpt.get();
String existingGroupId = mapping.getGroupId();
// 현재 그룹과 동일하면 중복 아님 (수정 시)
if (existingGroupId.equals(currentGroupId)) {
return null;
}
// 그룹명 조회
Optional<InflowControlGroup> groupOpt = groupRepository.findById(existingGroupId);
if (groupOpt.isPresent()) {
return groupOpt.get().getGroupName();
}
return existingGroupId; // 그룹명 없으면 ID 반환
}
/**
* 인터페이스 목록 조회 (팝업용)
*/
public Page<InflowGroupMappingUI> selectInterfaceListForPopup(Pageable pageable, String searchName) {
JPAQueryFactory jpaQueryFactory = new JPAQueryFactory(entityManager);
QEAIMessageEntity qMessage = QEAIMessageEntity.eAIMessageEntity;
BooleanBuilder boolBuilder = new BooleanBuilder();
if (!StringUtils.isEmpty(searchName)) {
boolBuilder.and(qMessage.eaisvcname.containsIgnoreCase(searchName)
.or(qMessage.eaisvcdesc.containsIgnoreCase(searchName)));
}
List<Tuple> tupleList = jpaQueryFactory
.select(qMessage.eaisvcname, qMessage.eaisvcdesc)
.from(qMessage)
.where(boolBuilder)
.orderBy(qMessage.eaisvcname.asc())
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetch();
List<InflowGroupMappingUI> uiList = tupleList.stream()
.map(tuple -> {
InflowGroupMappingUI ui = new InflowGroupMappingUI();
ui.setInterfaceId(tuple.get(qMessage.eaisvcname));
ui.setInterfaceDesc(tuple.get(qMessage.eaisvcdesc));
return ui;
})
.collect(Collectors.toList());
long totalCount = jpaQueryFactory
.select(qMessage.eaisvcname.count())
.from(qMessage)
.where(boolBuilder)
.fetchOne();
return new PageImpl<>(uiList, pageable, totalCount);
}
}
@@ -0,0 +1,60 @@
package com.eactive.eai.rms.onl.manage.inflow.group;
import java.time.LocalDateTime;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
@Data
public class InflowGroupControlManUI {
/**
* 그룹 ID
*/
@JsonProperty("GROUPID")
private String groupId;
/**
* 그룹명
*/
@JsonProperty("GROUPNAME")
private String groupName;
/**
* 임계치
*/
@JsonProperty("THRESHOLD")
private Integer threshold;
/**
* 초당 임계치
*/
@JsonProperty("THRESHOLDPERSECOND")
private Integer thresholdPerSecond;
/**
* 임계치 TimeUnit
*/
@JsonProperty("THRESHOLDTIMEUNIT")
private String thresholdTimeUnit;
/**
* 사용여부
*/
@JsonProperty("USEYN")
private String useYn;
/**
* 수정일시
*/
@JsonProperty("MODIFIEDAT")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime modifiedAt;
/**
* 매핑된 인터페이스 목록 (저장용)
*/
private List<InflowGroupMappingUI> interfaceList;
}
@@ -0,0 +1,37 @@
package com.eactive.eai.rms.onl.manage.inflow.group;
import com.fasterxml.jackson.annotation.JsonAlias;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
@Data
public class InflowGroupMappingUI {
/**
* 인터페이스 ID
*/
@JsonProperty("INTERFACEID")
@JsonAlias("interfaceId")
private String interfaceId;
/**
* 인터페이스 설명 (조회용)
*/
@JsonProperty("INTERFACEDESC")
@JsonAlias("interfaceDesc")
private String interfaceDesc;
/**
* 그룹 ID
*/
@JsonProperty("GROUPID")
@JsonAlias("groupId")
private String groupId;
/**
* 사용여부
*/
@JsonProperty("USEYN")
@JsonAlias("useYn")
private String useYn;
}