feat: 유량제어 그룹 버킷 분리 및 현황 화면 개선
- CustomGroupBucket 클래스 신규 생성: 초당/추가 임계치를 별도 LocalBucket으로 분리하여 개별 토큰 조회 가능 - InflowControlManager 수정: groupBucketList 타입을 CustomGroupBucket으로 변경, makeGroupBucket() 메서드 추가 - InflowGroupBucketService 단순화: 복잡한 리플렉션 로직 제거, CustomGroupBucket 직접 메서드 사용 - 현황 탭에 전체 인스턴스 합산 요약 추가 (활성 인스턴스 수, 초당/추가 임계치 합산) - 추가 임계치 시간단위에서 '초(SEC)' 옵션 제거 - 저장 완료 후 리스트 화면 이동 제거
This commit is contained in:
@@ -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('<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();
|
||||
}
|
||||
|
||||
$('#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를 선택하여 주십시오.');
|
||||
@@ -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() {
|
||||
</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" />
|
||||
@@ -713,7 +957,6 @@ $(document).ready(function() {
|
||||
<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>
|
||||
@@ -755,6 +998,45 @@ $(document).ready(function() {
|
||||
</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>
|
||||
</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>
|
||||
|
||||
Reference in New Issue
Block a user