Files
eapim-admin/WebContent/jsp/onl/admin/inflow/inflowGroupControlManDetail.jsp
T
daekuk 3696cbb6c5 API 명 -> API ID
API 설명 -> API 명

- API 변경된 이름 통일
2026-02-23 17:59:32 +09:00

933 lines
29 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<%@ 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"/>
<!-- 페이지 커스텀 스타일 (fragment) -->
<jsp:include page="fragments/inflowGroupDetailStyle.jsp"/>
<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) {
var groupId = json.GROUPID || json.groupId;
$("input[name=groupId]").val(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();
// 그룹 ID 표시 (수정 화면에서만)
if (groupId) {
$('#groupIdDisplay').text(groupId);
$('#groupIdRow').show();
}
// 수정일시 표시
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,
useYn: item.useYn || item.USEYN || '1'
});
});
}
renderInterfaceList();
},
error: function(e) {
alert(e.responseText);
}
});
}
function renderInterfaceList() {
var container = $('#interfaceListContainer');
container.empty();
var activeCount = interfaceList.filter(function(item) {
return item.useYn === '1' || item.useYn === undefined;
}).length;
$('#interfaceCountNum').text(interfaceList.length);
if (interfaceList.length > 0 && activeCount === interfaceList.length) {
$('#interfaceActiveLabel').html('<strong>전체 활성</strong>');
} else {
$('#interfaceActiveLabel').html('활성 <strong id="interfaceActiveNum">' + activeCount + '</strong>개');
}
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,
useYn: item.useYn !== undefined ? item.useYn : '1'
});
});
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, function() {
$('#ifSearchInput').focus();
});
$(document.activeElement).blur();
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 switchTab(tabName) {
$('.detail-tab').removeClass('active');
$('.detail-tab[data-tab="' + tabName + '"]').addClass('active');
$('.tab-content').removeClass('active');
$('#tab' + tabName.charAt(0).toUpperCase() + tabName.slice(1) + 'Content').addClass('active');
if (tabName === 'status' && isDetail) {
loadBucketStatus();
}
}
// 버킷 현황 조회
function loadBucketStatus() {
var groupId = $('input[name=groupId]').val();
if (!groupId) return;
$('#bucketServerList').html('<div class="bucket-empty-msg"><i class="material-icons">hourglass_empty</i>조회 중...</div>');
$.ajax({
type: 'POST',
url: url,
data: { cmd: 'LIST_BUCKET_STATUS', groupId: groupId },
dataType: 'json',
success: function(result) {
renderBucketStatus(result.servers || []);
$('#lastRefreshTime').text(formatDateTime(new Date()));
},
error: function(e) {
$('#bucketServerList').html('<div class="bucket-empty-msg bucket-error-msg"><i class="material-icons">error_outline</i>조회 실패: ' + e.statusText + '</div>');
}
});
}
function formatDateTime(date) {
var h = date.getHours();
var m = date.getMinutes();
var s = date.getSeconds();
return (h < 10 ? '0' + h : h) + ':' + (m < 10 ? '0' + m : m) + ':' + (s < 10 ? '0' + s : s);
}
function renderBucketStatus(servers) {
var container = $('#bucketServerList');
container.empty();
if (servers.length === 0) {
container.html('<div class="bucket-empty-msg"><i class="material-icons">dns</i>등록된 서버가 없습니다</div>');
$('#bucketSummary').hide();
return;
}
// 전체 합산 계산
var onlineCount = 0;
var perSecondCapacity = 0;
var thresholdCapacity = 0;
var thresholdTimeUnit = '';
$.each(servers, function(i, server) {
if (server.status === 'online' && server.data && server.data.buckets) {
onlineCount++;
$.each(server.data.buckets, function(j, bucket) {
if (bucket.type === 'perSecond') {
perSecondCapacity += bucket.capacity || 0;
} else if (bucket.type === 'threshold') {
thresholdCapacity += bucket.capacity || 0;
if (!thresholdTimeUnit) thresholdTimeUnit = bucket.timeUnit;
}
});
}
});
// 요약 영역 표시
if (onlineCount > 0) {
$('#summaryInstanceCount').html(onlineCount + '<span class="unit">개</span>');
var configPerSecond = parseInt($('input[name=thresholdPerSecond]').val()) || 0;
var configThreshold = parseInt($('input[name=threshold]').val()) || 0;
if (configPerSecond > 0) {
$('#summaryPerSecond').html(formatNumber(perSecondCapacity) + '<span class="unit">req/sec</span>');
$('#summaryPerSecondCalc').text(formatNumber(configPerSecond) + ' × ' + onlineCount + ' 인스턴스');
$('#summaryPerSecondWrap').show();
} else {
$('#summaryPerSecondWrap').hide();
}
if (configThreshold > 0) {
var timeUnitLabel = getTimeUnitText(thresholdTimeUnit);
$('#summaryThreshold').html(formatNumber(thresholdCapacity) + '<span class="unit">req/' + timeUnitLabel + '</span>');
$('#summaryThresholdCalc').text(formatNumber(configThreshold) + ' × ' + onlineCount + ' 인스턴스');
$('#summaryThresholdWrap').show();
} else {
$('#summaryThresholdWrap').hide();
}
// 활성 API 정보 표시
var activeApis = interfaceList.filter(function(item) {
return item.useYn === '1' || item.useYn === undefined;
});
$('#summaryActiveApi').html(activeApis.length + '<span class="unit">개</span>');
if (activeApis.length > 0) {
var apiIds = activeApis.map(function(item) { return item.interfaceId; });
var displayText = apiIds.length <= 3 ? apiIds.join(', ') : apiIds.slice(0, 3).join(', ') + ' 외 ' + (apiIds.length - 3) + '개';
$('#summaryActiveApiList').text(displayText);
} else {
$('#summaryActiveApiList').text('');
}
$('#bucketSummary').show();
} else {
$('#bucketSummary').hide();
}
$.each(servers, function(i, server) {
var statusClass = server.status || 'offline';
var statusText = { online: '정상', offline: '오프라인', no_data: '데이터 없음' };
var card = '<div class="bucket-server-card">' +
'<div class="bucket-server-header">' +
'<div>' +
'<span class="bucket-server-name">' + server.serverName + '</span> ' +
'<span class="bucket-server-ip">' + server.serverIp + ':' + server.serverPort + '</span>' +
'</div>' +
'<span class="bucket-server-status ' + statusClass + '">' + (statusText[statusClass] || statusClass) + '</span>' +
'</div>' +
'<div class="bucket-server-body">' + renderBucketBody(server) + '</div>' +
'</div>';
container.append(card);
});
}
function renderBucketBody(server) {
if (server.status === 'offline') {
return '<div class="bucket-error-msg">' + (server.message || '서버 연결 실패') + '</div>';
}
if (server.status === 'no_data') {
return '<div class="bucket-empty-msg" style="padding:10px;"><i class="material-icons" style="font-size:18px;">info_outline</i> ' + (server.message || '그룹이 로드되지 않음') + '</div>';
}
var data = server.data;
if (!data || !data.buckets || data.buckets.length === 0) {
return '<div class="bucket-empty-msg" style="padding:10px;">버킷 정보 없음</div>';
}
var html = '<div class="bucket-info-grid">';
$.each(data.buckets, function(j, bucket) {
var typeLabel = bucket.type === 'perSecond' ? '초당 임계치' : '추가 임계치';
var timeUnitText = getTimeUnitText(bucket.timeUnit);
var usagePercent = bucket.usagePercent || 0;
var progressClass = usagePercent < 50 ? 'low' : (usagePercent < 80 ? 'medium' : 'high');
html += '<div class="bucket-card">' +
'<div class="bucket-card-header">' +
'<span class="bucket-type">' + typeLabel + '</span>' +
'<span class="bucket-type-badge">' + timeUnitText + '</span>' +
'</div>' +
'<div class="bucket-progress-wrap">' +
'<div class="bucket-progress-bar">' +
'<div class="bucket-progress-fill ' + progressClass + '" style="width:' + usagePercent + '%"></div>' +
'</div>' +
'</div>' +
'<div class="bucket-stats">' +
'<span>사용량: <span class="bucket-stats-value">' + usagePercent.toFixed(1) + '%</span></span>' +
'<span>잔여: <span class="bucket-stats-value">' + formatNumber(bucket.availableTokens) + '</span> / ' + formatNumber(bucket.capacity) + '</span>' +
'</div>' +
'</div>';
});
html += '</div>';
return html;
}
function getTimeUnitText(timeUnit) {
var units = { SEC: '초', MIN: '분', HOU: '시간', DAY: '일', MON: '월' };
return units[timeUnit] || timeUnit || '-';
}
function formatNumber(num) {
if (num === undefined || num === null) return '-';
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
}
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;
}
});
// 활성 건수 업데이트
var activeCount = interfaceList.filter(function(item) {
return item.useYn === '1' || item.useYn === undefined;
}).length;
if (interfaceList.length > 0 && activeCount === interfaceList.length) {
$('#interfaceActiveLabel').html('<strong>전체 활성</strong>');
} else {
$('#interfaceActiveLabel').html('활성 <strong id="interfaceActiveNum">' + activeCount + '</strong>개');
}
});
$("#btn_modify").click(function() {
$(this).blur();
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()});
var isInsert = !isDetail;
$.ajax({
type: "POST",
url: url,
data: postData,
success: function(args) {
if (isInsert) {
// 신규 등록 시 목록으로 이동
showAlert("<%= localeMessage.getString("common.saveMsg") %>", {
type: 'success',
title: '저장 완료',
onClose: function() {
goNav(returnUrl);
}
});
} else {
// 수정 시 현재 화면 유지
showAlert("<%= localeMessage.getString("common.saveMsg") %>", {
type: 'success',
title: '저장 완료'
});
}
},
error: function(e) {
showAlert(e.responseText, { type: 'error' });
}
});
}
});
});
$("#btn_delete").click(function() {
$(this).blur();
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);
// 탭 전환 이벤트
$('.detail-tab').click(function() {
var tabName = $(this).data('tab');
switchTab(tabName);
});
// 현황 탭 표시 (상세 조회 시에만)
if (isDetail) {
$('#tabStatus').show();
}
// 버킷 새로고침 버튼
$('#btn_refresh_bucket').click(function() {
loadBucketStatus();
});
// 도움말 모달
$("#btn_help").click(function() {
$(document.activeElement).blur();
$("#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>
<!-- 탭 네비게이션 -->
<div class="detail-tabs" id="detailTabs">
<button type="button" class="detail-tab active" data-tab="config">
<i class="material-icons">settings</i> 설정
</button>
<button type="button" class="detail-tab" data-tab="status" id="tabStatus" style="display:none;">
<i class="material-icons">analytics</i> 현황
</button>
</div>
<!-- 설정 탭 -->
<div class="tab-content active" id="tabConfigContent">
<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 id="groupIdRow" style="display:none;">
<th>그룹 ID</th>
<td><span id="groupIdDisplay" style="font-family:Consolas, monospace; font-size:12px; color:#666;"></span></td>
</tr>
<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="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 id="interfaceActiveLabel">활성 <strong id="interfaceActiveNum">0</strong>개</span>)</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><%= localeMessage.getString("eaiMessage.eaiSvcName")%></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><!-- /tabConfigContent -->
<!-- 현황 탭 -->
<div class="tab-content" id="tabStatusContent">
<div class="bucket-status-wrap">
<div class="bucket-refresh-bar">
<span class="last-refresh">마지막 갱신: <span id="lastRefreshTime">-</span></span>
<button type="button" class="cssbtn" id="btn_refresh_bucket"><i class="material-icons">refresh</i> 새로고침</button>
</div>
<!-- 전체 합산 요약 -->
<div class="bucket-summary" id="bucketSummary" style="display:none;">
<div class="bucket-summary-title"><i class="material-icons">functions</i> 전체 인스턴스 합산</div>
<div class="bucket-summary-grid">
<div class="bucket-summary-item">
<span class="bucket-summary-label">활성 인스턴스</span>
<span class="bucket-summary-value" id="summaryInstanceCount">0<span class="unit">개</span></span>
</div>
<div class="bucket-summary-item" id="summaryPerSecondWrap">
<span class="bucket-summary-label">초당 임계치 (전체)</span>
<span class="bucket-summary-value" id="summaryPerSecond">0<span class="unit">req/sec</span></span>
<span class="bucket-summary-sub" id="summaryPerSecondCalc"></span>
</div>
<div class="bucket-summary-item" id="summaryThresholdWrap">
<span class="bucket-summary-label">추가 임계치 (전체)</span>
<span class="bucket-summary-value" id="summaryThreshold">0<span class="unit">req</span></span>
<span class="bucket-summary-sub" id="summaryThresholdCalc"></span>
</div>
<div class="bucket-summary-item">
<span class="bucket-summary-label">적용 API</span>
<span class="bucket-summary-value" id="summaryActiveApi">0<span class="unit">개</span></span>
<span class="bucket-summary-sub" id="summaryActiveApiList"></span>
</div>
</div>
</div>
<div class="bucket-server-list" id="bucketServerList">
<div class="bucket-empty-msg">
<i class="material-icons">hourglass_empty</i>
버킷 현황을 조회하려면 새로고침 버튼을 클릭하세요
</div>
</div>
</div>
</div><!-- /tabStatusContent -->
</div>
</div>
<!-- 인터페이스 선택 모달 (fragment) -->
<jsp:include page="fragments/inflowGroupInterfaceModal.jsp"/>
<!-- 도움말 모달 (fragment) -->
<jsp:include page="fragments/inflowGroupHelpModal.jsp"/>
</body>
</html>