From 10a8918bb654f50baa02508b7db83f5e8129b5ce Mon Sep 17 00:00:00 2001 From: daekuk Date: Thu, 11 Dec 2025 11:04:00 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=9C=A0=EB=9F=89=EC=A0=9C=EC=96=B4=20?= =?UTF-8?q?=EA=B7=B8=EB=A3=B9=20=EB=B2=84=ED=82=B7=20=EB=B6=84=EB=A6=AC=20?= =?UTF-8?q?=EB=B0=8F=20=ED=98=84=ED=99=A9=20=ED=99=94=EB=A9=B4=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CustomGroupBucket 클래스 신규 생성: 초당/추가 임계치를 별도 LocalBucket으로 분리하여 개별 토큰 조회 가능 - InflowControlManager 수정: groupBucketList 타입을 CustomGroupBucket으로 변경, makeGroupBucket() 메서드 추가 - InflowGroupBucketService 단순화: 복잡한 리플렉션 로직 제거, CustomGroupBucket 직접 메서드 사용 - 현황 탭에 전체 인스턴스 합산 요약 추가 (활성 인스턴스 수, 초당/추가 임계치 합산) - 추가 임계치 시간단위에서 '초(SEC)' 옵션 제거 - 저장 완료 후 리스트 화면 이동 제거 --- .../inflow/inflowGroupControlManDetail.jsp | 292 +++++++++++++++++- .../InflowGroupControlManController.java | 14 + .../group/InflowGroupControlManService.java | 61 ++++ 3 files changed, 362 insertions(+), 5 deletions(-) diff --git a/WebContent/jsp/onl/admin/inflow/inflowGroupControlManDetail.jsp b/WebContent/jsp/onl/admin/inflow/inflowGroupControlManDetail.jsp index 5c8d399..d0e7dab 100644 --- a/WebContent/jsp/onl/admin/inflow/inflowGroupControlManDetail.jsp +++ b/WebContent/jsp/onl/admin/inflow/inflowGroupControlManDetail.jsp @@ -96,6 +96,57 @@ .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; } +/* 탭 스타일 */ +.detail-tabs { display:flex; gap:0; border-bottom:2px solid rgba(0,128,128,0.3); margin-bottom:20px; } +.detail-tab { padding:10px 24px; font-size:13px; font-weight:bold; color:#666; cursor:pointer; border:none; background:transparent; border-bottom:2px solid transparent; margin-bottom:-2px; transition:all 0.2s; } +.detail-tab:hover { color:rgba(0,128,128,0.8); background:rgba(0,128,128,0.05); } +.detail-tab.active { color:rgba(0,128,128,1); border-bottom-color:rgba(0,128,128,0.8); background:rgba(0,128,128,0.05); } +.detail-tab i { vertical-align:middle; margin-right:4px; font-size:18px; } +.tab-content { display:none; } +.tab-content.active { display:block; } + +/* 버킷 현황 스타일 */ +.bucket-status-wrap { margin-top:10px; } +.bucket-refresh-bar { display:flex; justify-content:space-between; align-items:center; margin-bottom:15px; } +.bucket-refresh-bar .last-refresh { font-size:11px; color:#999; } +.bucket-server-list { display:flex; flex-wrap:wrap; gap:15px; } +.bucket-server-card { flex:0 0 calc(33.333% - 10px); min-width:280px; border:1px solid #ddd; border-radius:5px; overflow:hidden; } +.bucket-server-header { display:flex; justify-content:space-between; align-items:center; padding:10px 15px; background:#f9f9f9; border-bottom:1px solid #eee; } +.bucket-server-name { font-weight:bold; font-size:13px; } +.bucket-server-ip { font-size:11px; color:#888; font-family:Consolas, monospace; } +.bucket-server-status { font-size:11px; padding:3px 8px; border-radius:10px; } +.bucket-server-status.online { background:#e8f5e9; color:#2e7d32; } +.bucket-server-status.offline { background:#ffebee; color:#c62828; } +.bucket-server-status.no_data { background:#fff3e0; color:#ef6c00; } +.bucket-server-body { padding:15px; } +.bucket-info-grid { display:grid; grid-template-columns:repeat(auto-fit, minmax(280px, 1fr)); gap:15px; } +.bucket-card { background:#fafafa; border:1px solid #eee; border-radius:4px; padding:12px 15px; } +.bucket-card-header { display:flex; justify-content:space-between; align-items:center; margin-bottom:10px; } +.bucket-type { font-size:12px; font-weight:bold; color:#333; } +.bucket-type-badge { font-size:10px; padding:2px 6px; border-radius:3px; background:#e3f2fd; color:#1565c0; } +.bucket-progress-wrap { margin-bottom:8px; } +.bucket-progress-bar { height:8px; background:#e0e0e0; border-radius:4px; overflow:hidden; } +.bucket-progress-fill { height:100%; border-radius:4px; transition:width 0.3s ease; } +.bucket-progress-fill.low { background:linear-gradient(90deg, #4caf50, #66bb6a); } +.bucket-progress-fill.medium { background:linear-gradient(90deg, #ff9800, #ffa726); } +.bucket-progress-fill.high { background:linear-gradient(90deg, #f44336, #ef5350); } +.bucket-stats { display:flex; justify-content:space-between; font-size:11px; color:#666; } +.bucket-stats-value { font-family:Consolas, monospace; } +.bucket-empty-msg { padding:30px; text-align:center; color:#999; font-size:12px; } +.bucket-empty-msg i { font-size:36px; display:block; margin-bottom:10px; color:#ddd; } +.bucket-error-msg { color:#c62828; font-size:12px; } + +/* 전체 합산 요약 */ +.bucket-summary { background:linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%); border:1px solid #dee2e6; border-radius:5px; padding:15px 20px; margin-bottom:20px; } +.bucket-summary-title { font-size:13px; font-weight:bold; color:#333; margin-bottom:12px; display:flex; align-items:center; gap:6px; } +.bucket-summary-title i { font-size:18px; color:rgba(0,128,128,0.8); } +.bucket-summary-grid { display:flex; gap:30px; flex-wrap:wrap; } +.bucket-summary-item { display:flex; flex-direction:column; } +.bucket-summary-label { font-size:11px; color:#666; margin-bottom:4px; } +.bucket-summary-value { font-size:18px; font-weight:bold; color:#333; font-family:Consolas, monospace; } +.bucket-summary-value .unit { font-size:12px; font-weight:normal; color:#888; margin-left:4px; } +.bucket-summary-sub { font-size:11px; color:#888; margin-top:2px; } + /* 도움말 모달 */ .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; } @@ -412,6 +463,174 @@ function renderIfPagination() { }); } +// 탭 전환 기능 +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('
hourglass_empty조회 중...
'); + + $.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('
error_outline조회 실패: ' + e.statusText + '
'); + } + }); +} + +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('
dns등록된 서버가 없습니다
'); + $('#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 + ''); + + var configPerSecond = parseInt($('input[name=thresholdPerSecond]').val()) || 0; + var configThreshold = parseInt($('input[name=threshold]').val()) || 0; + + if (configPerSecond > 0) { + $('#summaryPerSecond').html(formatNumber(perSecondCapacity) + 'req/sec'); + $('#summaryPerSecondCalc').text(formatNumber(configPerSecond) + ' × ' + onlineCount + ' 인스턴스'); + $('#summaryPerSecondWrap').show(); + } else { + $('#summaryPerSecondWrap').hide(); + } + + if (configThreshold > 0) { + var timeUnitLabel = getTimeUnitText(thresholdTimeUnit); + $('#summaryThreshold').html(formatNumber(thresholdCapacity) + 'req/' + timeUnitLabel + ''); + $('#summaryThresholdCalc').text(formatNumber(configThreshold) + ' × ' + onlineCount + ' 인스턴스'); + $('#summaryThresholdWrap').show(); + } else { + $('#summaryThresholdWrap').hide(); + } + + $('#bucketSummary').show(); + } else { + $('#bucketSummary').hide(); + } + + $.each(servers, function(i, server) { + var statusClass = server.status || 'offline'; + var statusText = { online: '정상', offline: '오프라인', no_data: '데이터 없음' }; + + var card = '
' + + '
' + + '
' + + '' + server.serverName + ' ' + + '' + server.serverIp + ':' + server.serverPort + '' + + '
' + + '' + (statusText[statusClass] || statusClass) + '' + + '
' + + '
' + renderBucketBody(server) + '
' + + '
'; + container.append(card); + }); +} + +function renderBucketBody(server) { + if (server.status === 'offline') { + return '
' + (server.message || '서버 연결 실패') + '
'; + } + if (server.status === 'no_data') { + return '
info_outline ' + (server.message || '그룹이 로드되지 않음') + '
'; + } + + var data = server.data; + if (!data || !data.buckets || data.buckets.length === 0) { + return '
버킷 정보 없음
'; + } + + var html = '
'; + $.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 += '
' + + '
' + + '' + typeLabel + '' + + '' + timeUnitText + '' + + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + + '사용량: ' + usagePercent.toFixed(1) + '%' + + '잔여: ' + formatNumber(bucket.availableTokens) + ' / ' + formatNumber(bucket.capacity) + '' + + '
' + + '
'; + }); + html += '
'; + 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를 선택하여 주십시오.'); @@ -520,10 +739,7 @@ $(document).ready(function() { success: function(args) { showAlert("<%= localeMessage.getString("common.saveMsg") %>", { type: 'success', - title: '저장 완료', - onClose: function() { - goNav(returnUrl); - } + title: '저장 완료' }); }, error: function(e) { @@ -611,6 +827,22 @@ $(document).ready(function() { 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() { $("#helpModal").fadeIn(200); @@ -650,6 +882,18 @@ $(document).ready(function() { + +
+ + +
+ + +
@@ -713,7 +957,6 @@ $(document).ready(function() {