Merge remote-tracking branch 'origin/master'

# Conflicts:
#	src/main/java/com/eactive/eai/rms/onl/apim/approval/PortalApprovalManController.java
This commit is contained in:
Rinjae
2026-06-12 16:08:41 +09:00
66 changed files with 2817 additions and 761 deletions
@@ -16,173 +16,486 @@
<jsp:include page="/jsp/common/include/css.jsp"/>
<jsp:include page="/jsp/common/include/script.jsp"/>
<script language="javascript" >
<!-- 페이지 커스텀 스타일 (fragment) -->
<jsp:include page="fragments/inflowGroupDetailStyle.jsp"/>
<script language="javascript">
var url ='<c:url value="/onl/admin/inflow/inflowAdapterControlMan.json" />';
var url_view ='<c:url value="/onl/admin/inflow/inflowAdapterControlMan.view" />';
var isDetail = false;
function init(url,key,callback){
$.ajax({
type : "POST",
url:url,
dataType:"json",
data:{cmd: 'LIST_INIT_COMBO'},
success:function(json){
function updateThresholdLabel() {
var timeUnit = $('select[name=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) {
$.ajax({
type: "POST",
url: url,
dataType: "json",
data: {cmd: 'LIST_INIT_COMBO'},
success: function(json) {
new makeOptions("CODE","NAME").setObj($("select[name=useYn]")).setData(json.useYnRows).setFormat(codeName3OptionFormat).rendering();
new makeOptions("CODE","NAME").setObj($("select[name=thresholdTimeUnit]")).setNoValueInclude(true).setData(json.timeUnitRows).setFormat(codeName3OptionFormat).rendering();
if(typeof callback === 'function') {
callback(url,key);
updateThresholdLabel();
if (typeof callback === 'function') {
callback(url, key);
}
},
error:function(e){
error: function(e) {
alert(e.responseText);
}
});
}
function detail(url,key){
if (!isDetail)return;
$.ajax({
type : "POST",
url:url,
dataType:"json",
data:{cmd: 'DETAIL', name : key},
success:function(json){
function detail(url, key) {
if (!isDetail) return;
$.ajax({
type: "POST",
url: url,
dataType: "json",
data: {cmd: 'DETAIL', name: key},
success: function(json) {
var data = json;
$("input[name=name]").attr('readonly',true);
$("input[name=desc]").attr('readonly',true);
$("#ajaxForm input[type!=radio],#ajaxForm select,#ajaxForm textarea").each(function(){
$("input[name=name]").attr('readonly', true);
$("input[name=desc]").attr('readonly', true);
$("#ajaxForm input[type!=radio],#ajaxForm select,#ajaxForm textarea").each(function() {
var name = $(this).attr("name");
var tag = $(this).prop("tagName").toLowerCase();
$(tag+"[name="+name+"]").val(data[name.toUpperCase()]);
});
updateThresholdLabel();
var modifiedAt = data.MODIFIEDAT || data.modifiedAt;
if (modifiedAt) {
$('#modifiedAtText').text(modifiedAt);
$('#modifiedAtInfo').show();
}
},
error:function(e){
error: function(e) {
alert(e.responseText);
}
});
}
$(document).ready(function() {
var returnUrl = getReturnUrlForReturn();
var key ="${param.name}";
if (key != "" && key !="null"){
isDetail = true;
}
init(url,key,detail);
$("#btn_modify").click(function(){
var postData = $('#ajaxForm').serializeArray();
if (isDetail){
postData.push({ name: "cmd" , value:"UPDATE"});
}else{
postData.push({ name: "cmd" , value:"INSERT"});
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() {
$('#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', adapterId: "${param.name}" },
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>');
}
$.ajax({
type : "POST",
url:url,
data:postData,
success:function(args){
alert("<%= localeMessage.getString("common.saveMsg") %>");
goNav(returnUrl);//LIST로 이동
},
error:function(e){
alert(e.responseText);
});
}
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, ',');
}
$(document).ready(function() {
var returnUrl = getReturnUrlForReturn();
var key = "${param.name}";
if (key != "" && key != "null") {
isDetail = true;
}
init(url, key, detail);
$("select[name=thresholdTimeUnit]").on('change', function() {
updateThresholdLabel();
});
$("#btn_modify").click(function() {
$(this).blur();
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"});
}
var isInsert = !isDetail;
$.ajax({
type: "POST",
url: url,
data: postData,
success: function(args) {
showAlert("<%= localeMessage.getString("common.saveMsg") %>", {
type: 'success',
title: '저장 완료',
onClose: isInsert ? function() { goNav(returnUrl); } : undefined
});
},
error: function(e) {
showAlert(e.responseText, {type: 'error'});
}
});
}
});
});
$("#btn_delete").click(function(){
if ( confirm( '<%=localeMessage.getString("common.confirmMsg")%>' ) != true ) return;
var postData = $('#ajaxForm').serializeArray();
postData.push({ name: "cmd" , value:"DELETE"});
$.ajax({
type : "POST",
url:url,
data:postData,
success:function(args){
alert("<%= localeMessage.getString("common.deleteMsg") %>");
goNav(returnUrl);//LIST로 이동
},
error:function(e){
alert(e.responseText);
$("#btn_delete").click(function() {
$(this).blur();
var adapterName = $('input[name=desc]').val() || $('input[name=name]').val();
showConfirm('<strong>[' + adapterName + ']</strong> 어댑터를 삭제하시겠습니까?', {
type: 'error',
title: '삭제 확인',
confirmText: '삭제',
onConfirm: function() {
var postData = $('#ajaxForm').serializeArray();
postData.push({name: "cmd", value: "DELETE"});
$.ajax({
type: "POST",
url: url,
data: postData,
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);//LIST로 이동
});
$("#btn_previous").click(function() {
goNav(returnUrl);
});
buttonControl(isDetail);
titleControl(isDetail);
$('.detail-tab').click(function() {
switchTab($(this).data('tab'));
});
if (isDetail) {
$('#tabStatus').show();
}
$('#btn_refresh_bucket').click(function() {
loadBucketStatus();
});
});
</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">
<div class="search_wrap">
<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.modify") %></button>
<button type="button" class="cssbtn" id="btn_previous" level="R" status="DETAIL,NEW"><i class="material-icons">arrow_back</i> <%= localeMessage.getString("button.previous") %></button>
<%-- <img src="<c:url value="/img/btn_delete.png"/>" alt="" id="btn_delete" level="W" status="DETAIL"/> --%>
<%-- <img src="<c:url value="/img/btn_modify.png"/>" alt="" id="btn_modify" level="W" status="DETAIL,NEW" /> --%>
<%-- <img src="<c:url value="/img/btn_previous.png"/>" alt="" id="btn_previous" level="R" status="DETAIL,NEW"/> --%>
</div>
<div class="title"><%= localeMessage.getString("infAdpConMan.title") %><span class="tooltip" ><%= localeMessage.getString("infAdpConMan.title") %></span></div>
<table id="grid" ></table>
<div id="pager"></div>
<!-- detail -->
<form id="ajaxForm">
<table class="table_row" cellspacing="0">
<tr>
<th style="width:20%;"><%= localeMessage.getString("infAdpConMan.adaNm") %></th>
<td><input type="text" name="name" /></td>
</tr>
<tr>
<th style="width:20%;"><%= localeMessage.getString("infAdpConMan.adaDes") %></th>
<td><input type="text" name="desc" /></td>
</tr>
<tr>
<th style="width:20%;"><%= localeMessage.getString("infAdpConMan.thrPerSecond") %></th>
<td><input type="text" name="thresholdPerSecond" /></td>
</tr>
<tr>
<th style="width:20%;"><%= localeMessage.getString("infAdpConMan.thr") %></th>
<td><input type="text" name="threshold" /></td>
</tr>
<tr>
<th style="width:20%;"><%= localeMessage.getString("infAdpConMan.thrTimeUnit") %></th>
<td>
<div class="select-style">
<select name="thresholdTimeUnit" />
</div>
</td>
</tr>
<tr>
<th style="width:20%;"><%= localeMessage.getString("infAdpConMan.useYn") %></th>
<td>
<div class="select-style">
<select name="useYn" />
</div>
</td>
</tr>
</table>
</form>
</div><!-- end content_middle -->
</div><!-- end right_box -->
</body>
</html>
<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_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" level="R" status="DETAIL,NEW"><i class="material-icons">arrow_back</i> <%= localeMessage.getString("button.previous") %></button>
</div>
<div class="title-wrap">
<div class="title"><%= localeMessage.getString("infAdpConMan.title") %><span class="tooltip"><%= localeMessage.getString("infAdpConMan.title") %></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">
<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">
<span class="section-label">기본 정보</span>
<table class="form-table" cellspacing="0">
<colgroup>
<col style="width:150px" />
<col />
</colgroup>
<tbody>
<tr>
<th><%= localeMessage.getString("infAdpConMan.adaNm") %></th>
<td><input type="text" name="name" style="width:300px; background-color: #ebebec;" /></td>
</tr>
<tr>
<th><%= localeMessage.getString("infAdpConMan.adaDes") %></th>
<td><input type="text" name="desc" style="width:300px; background-color: #ebebec;" /></td>
</tr>
<tr>
<th><%= localeMessage.getString("infAdpConMan.useYn") %></th>
<td>
<select name="useYn" class="styled-select"></select>
</td>
</tr>
</tbody>
</table>
<span class="section-label">유량제어 설정</span>
<table class="form-table" cellspacing="0">
<colgroup>
<col style="width:150px" />
<col />
</colgroup>
<tbody>
<tr>
<th>
<%= localeMessage.getString("infAdpConMan.thrPerSecond") %>
<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>
<%= localeMessage.getString("infAdpConMan.thr") %>
<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" class="styled-select"></select>
<input type="number" name="threshold" id="thresholdInput" placeholder="0" />
<span class="unit" id="thresholdUnit">requests</span>
</div>
</td>
</tr>
</tbody>
</table>
</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><!-- end content_middle -->
</div><!-- end right_box -->
</body>
</html>
@@ -16,170 +16,501 @@
<jsp:include page="/jsp/common/include/css.jsp"/>
<jsp:include page="/jsp/common/include/script.jsp"/>
<script language="javascript" >
<!-- 페이지 커스텀 스타일 (fragment) -->
<jsp:include page="fragments/inflowGroupDetailStyle.jsp"/>
<script language="javascript">
var url ='<c:url value="/onl/admin/inflow/inflowClientControlMan.json" />';
var url_view ='<c:url value="/onl/admin/inflow/inflowClientControlMan.view" />';
var isDetail = false;
function init(url,key,callback){
$.ajax({
type : "POST",
url:url,
dataType:"json",
data:{cmd: 'LIST_INIT_COMBO'},
success:function(json){
function updateThresholdLabel() {
var timeUnit = $('select[name=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) {
$.ajax({
type: "POST",
url: url,
dataType: "json",
data: {cmd: 'LIST_INIT_COMBO'},
success: function(json) {
new makeOptions("CODE","NAME").setObj($("select[name=useYn]")).setData(json.useYnRows).setFormat(codeName3OptionFormat).rendering();
new makeOptions("CODE","NAME").setObj($("select[name=thresholdTimeUnit]")).setNoValueInclude(true).setData(json.timeUnitRows).setFormat(codeName3OptionFormat).rendering();
if(typeof callback === 'function') {
callback(url,key);
updateThresholdLabel();
if (typeof callback === 'function') {
callback(url, key);
}
},
error:function(e){
error: function(e) {
alert(e.responseText);
}
});
}
function detail(url,key){
if (!isDetail)return;
$.ajax({
type : "POST",
url:url,
dataType:"json",
data:{cmd: 'DETAIL', name : key},
success:function(json){
function detail(url, key) {
if (!isDetail) return;
$.ajax({
type: "POST",
url: url,
dataType: "json",
data: {cmd: 'DETAIL', name: key},
success: function(json) {
var data = json;
$("input[name=name]").attr('readonly',true);
$("input[name=desc]").attr('readonly',true);
$("#ajaxForm input[type!=radio],#ajaxForm select,#ajaxForm textarea").each(function(){
$("input[name=name]").attr('readonly', true);
$("input[name=desc]").attr('readonly', true);
$("#ajaxForm input[type!=radio],#ajaxForm select,#ajaxForm textarea").each(function() {
var name = $(this).attr("name");
var tag = $(this).prop("tagName").toLowerCase();
$(tag+"[name="+name+"]").val(data[name.toUpperCase()]);
});
updateThresholdLabel();
var modifiedAt = data.MODIFIEDAT || data.modifiedAt;
if (modifiedAt) {
$('#modifiedAtText').text(modifiedAt);
$('#modifiedAtInfo').show();
}
},
error:function(e){
error: function(e) {
alert(e.responseText);
}
});
}
$(document).ready(function() {
var returnUrl = getReturnUrlForReturn();
var key ="${param.name}";
if (key != "" && key !="null"){
isDetail = true;
}
init(url,key,detail);
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() {
$("#btn_modify").click(function(){
var postData = $('#ajaxForm').serializeArray();
if (isDetail){
postData.push({ name: "cmd" , value:"UPDATE"});
}else{
postData.push({ name: "cmd" , value:"INSERT"});
$('#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', clientId: "${param.name}" },
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>');
}
$.ajax({
type : "POST",
url:url,
data:postData,
success:function(args){
alert("<%= localeMessage.getString("common.saveMsg") %>");
goNav(returnUrl);//LIST로 이동
},
error:function(e){
alert(e.responseText);
});
}
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, ',');
}
$(document).ready(function() {
var returnUrl = getReturnUrlForReturn();
var key = "${param.name}";
if (key != "" && key != "null") {
isDetail = true;
}
init(url, key, detail);
$("select[name=thresholdTimeUnit]").on('change', function() {
updateThresholdLabel();
});
$("#btn_modify").click(function() {
$(this).blur();
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"});
}
var isInsert = !isDetail;
$.ajax({
type: "POST",
url: url,
data: postData,
success: function(args) {
showAlert("<%= localeMessage.getString("common.saveMsg") %>", {
type: 'success',
title: '저장 완료',
onClose: isInsert ? function() { goNav(returnUrl); } : undefined
});
},
error: function(e) {
showAlert(e.responseText, {type: 'error'});
}
});
}
});
});
$("#btn_delete").click(function(){
if ( confirm( '<%=localeMessage.getString("common.confirmMsg")%>' ) != true ) return;
var postData = $('#ajaxForm').serializeArray();
postData.push({ name: "cmd" , value:"DELETE"});
$.ajax({
type : "POST",
url:url,
data:postData,
success:function(args){
alert("<%= localeMessage.getString("common.deleteMsg") %>");
goNav(returnUrl);//LIST로 이동
},
error:function(e){
alert(e.responseText);
$("#btn_delete").click(function() {
$(this).blur();
var clientName = $('input[name=desc]').val() || $('input[name=name]').val();
showConfirm('<strong>[' + clientName + ']</strong> 클라이언트를 삭제하시겠습니까?', {
type: 'error',
title: '삭제 확인',
confirmText: '삭제',
onConfirm: function() {
var postData = $('#ajaxForm').serializeArray();
postData.push({name: "cmd", value: "DELETE"});
$.ajax({
type: "POST",
url: url,
data: postData,
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);//LIST로 이동
});
$("#btn_previous").click(function() {
goNav(returnUrl);
});
buttonControl(isDetail);
titleControl(isDetail);
$('.detail-tab').click(function() {
switchTab($(this).data('tab'));
});
if (isDetail) {
$('#tabStatus').show();
}
$('#btn_refresh_bucket').click(function() {
loadBucketStatus();
});
$(document).keydown(function(e) {
if (e.keyCode === 27) {
// ESC 키 처리 (필요시 모달 닫기 등)
}
});
});
</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">
<div class="search_wrap">
<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.modify") %></button>
<button type="button" class="cssbtn" id="btn_previous" level="R" status="DETAIL,NEW"><i class="material-icons">arrow_back</i> <%= localeMessage.getString("button.previous") %></button>
</div>
<div class="title">클라이언트 유량제어 <span class="tooltip" >클라이언트 유량제어 </span></div>
<table id="grid" ></table>
<div id="pager"></div>
<!-- detail -->
<form id="ajaxForm">
<table class="table_row" cellspacing="0">
<tr>
<th style="width:20%;">클라이언트 ID</th>
<td><input type="text" name="name" /></td>
</tr>
<tr>
<th style="width:20%;">클라이언트명</th>
<td><input type="text" name="desc" /></td>
</tr>
<tr>
<th style="width:20%;"><%= localeMessage.getString("infAdpConMan.thrPerSecond") %></th>
<td><input type="text" name="thresholdPerSecond" /></td>
</tr>
<tr>
<th style="width:20%;"><%= localeMessage.getString("infAdpConMan.thr") %></th>
<td><input type="text" name="threshold" /></td>
</tr>
<tr>
<th style="width:20%;"><%= localeMessage.getString("infAdpConMan.thrTimeUnit") %></th>
<td>
<div class="select-style">
<select name="thresholdTimeUnit" />
</div>
</td>
</tr>
<tr>
<th style="width:20%;"><%= localeMessage.getString("infAdpConMan.useYn") %></th>
<td>
<div class="select-style">
<select name="useYn" />
</div>
</td>
</tr>
</table>
</form>
</div><!-- end content_middle -->
</div><!-- end right_box -->
</body>
</html>
<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_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" level="R" status="DETAIL,NEW"><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">
<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">
<span class="section-label">기본 정보</span>
<table class="form-table" cellspacing="0">
<colgroup>
<col style="width:150px" />
<col />
</colgroup>
<tbody>
<tr>
<th>클라이언트 ID</th>
<td><input type="text" name="name" style="width:300px; background-color: #ebebec;" /></td>
</tr>
<tr>
<th>클라이언트명</th>
<td><input type="text" name="desc" style="width:300px; background-color: #ebebec;" /></td>
</tr>
<tr>
<th><%= localeMessage.getString("infAdpConMan.useYn") %></th>
<td>
<select name="useYn" class="styled-select"></select>
</td>
</tr>
</tbody>
</table>
<span class="section-label">유량제어 설정</span>
<table class="form-table" cellspacing="0">
<colgroup>
<col style="width:150px" />
<col />
</colgroup>
<tbody>
<tr>
<th>
<%= localeMessage.getString("infAdpConMan.thrPerSecond") %>
<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>
<%= localeMessage.getString("infAdpConMan.thr") %>
<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" class="styled-select"></select>
<input type="number" name="threshold" id="thresholdInput" placeholder="0" />
<span class="unit" id="thresholdUnit">requests</span>
</div>
</td>
</tr>
</tbody>
</table>
</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><!-- end content_middle -->
</div><!-- end right_box -->
</body>
</html>
@@ -16,173 +16,486 @@
<jsp:include page="/jsp/common/include/css.jsp"/>
<jsp:include page="/jsp/common/include/script.jsp"/>
<script language="javascript" >
<!-- 페이지 커스텀 스타일 (fragment) -->
<jsp:include page="fragments/inflowGroupDetailStyle.jsp"/>
<script language="javascript">
var url ='<c:url value="/onl/admin/inflow/inflowInterfaceControlMan.json" />';
var url_view ='<c:url value="/onl/admin/inflow/inflowInterfaceControlMan.view" />';
var isDetail = false;
function init(url,key,callback){
$.ajax({
type : "POST",
url:url,
dataType:"json",
data:{cmd: 'LIST_INIT_COMBO'},
success:function(json){
function updateThresholdLabel() {
var timeUnit = $('select[name=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) {
$.ajax({
type: "POST",
url: url,
dataType: "json",
data: {cmd: 'LIST_INIT_COMBO'},
success: function(json) {
new makeOptions("CODE","NAME").setObj($("select[name=useYn]")).setData(json.useYnRows).setFormat(codeName3OptionFormat).rendering();
new makeOptions("CODE","NAME").setObj($("select[name=thresholdTimeUnit]")).setNoValueInclude(true).setData(json.timeUnitRows).setFormat(codeName3OptionFormat).rendering();
if(typeof callback === 'function') {
callback(url,key);
updateThresholdLabel();
if (typeof callback === 'function') {
callback(url, key);
}
},
error:function(e){
error: function(e) {
alert(e.responseText);
}
});
}
function detail(url,key){
if (!isDetail)return;
$.ajax({
type : "POST",
url:url,
dataType:"json",
data:{cmd: 'DETAIL', name : key},
success:function(json){
function detail(url, key) {
if (!isDetail) return;
$.ajax({
type: "POST",
url: url,
dataType: "json",
data: {cmd: 'DETAIL', name: key},
success: function(json) {
var data = json;
$("input[name=name]").attr('readonly',true);
$("input[name=desc]").attr('readonly',true);
$("#ajaxForm input[type!=radio],#ajaxForm select,#ajaxForm textarea").each(function(){
$("input[name=name]").attr('readonly', true);
$("input[name=desc]").attr('readonly', true);
$("#ajaxForm input[type!=radio],#ajaxForm select,#ajaxForm textarea").each(function() {
var name = $(this).attr("name");
var tag = $(this).prop("tagName").toLowerCase();
$(tag+"[name="+name+"]").val(data[name.toUpperCase()]);
});
updateThresholdLabel();
var modifiedAt = data.MODIFIEDAT || data.modifiedAt;
if (modifiedAt) {
$('#modifiedAtText').text(modifiedAt);
$('#modifiedAtInfo').show();
}
},
error:function(e){
error: function(e) {
alert(e.responseText);
}
});
}
$(document).ready(function() {
var returnUrl = getReturnUrlForReturn();
var key ="${param.name}";
if (key != "" && key !="null"){
isDetail = true;
}
init(url,key,detail);
$("#btn_modify").click(function(){
var postData = $('#ajaxForm').serializeArray();
if (isDetail){
postData.push({ name: "cmd" , value:"UPDATE"});
}else{
postData.push({ name: "cmd" , value:"INSERT"});
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() {
$('#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', interfaceId: "${param.name}" },
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>');
}
$.ajax({
type : "POST",
url:url,
data:postData,
success:function(args){
alert("<%= localeMessage.getString("common.saveMsg") %>");
goNav(returnUrl);//LIST로 이동
},
error:function(e){
alert(e.responseText);
});
}
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, ',');
}
$(document).ready(function() {
var returnUrl = getReturnUrlForReturn();
var key = "${param.name}";
if (key != "" && key != "null") {
isDetail = true;
}
init(url, key, detail);
$("select[name=thresholdTimeUnit]").on('change', function() {
updateThresholdLabel();
});
$("#btn_modify").click(function() {
$(this).blur();
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"});
}
var isInsert = !isDetail;
$.ajax({
type: "POST",
url: url,
data: postData,
success: function(args) {
showAlert("<%= localeMessage.getString("common.saveMsg") %>", {
type: 'success',
title: '저장 완료',
onClose: isInsert ? function() { goNav(returnUrl); } : undefined
});
},
error: function(e) {
showAlert(e.responseText, {type: 'error'});
}
});
}
});
});
$("#btn_delete").click(function(){
if ( confirm( '<%=localeMessage.getString("common.confirmMsg")%>' ) != true ) return;
var postData = $('#ajaxForm').serializeArray();
postData.push({ name: "cmd" , value:"DELETE"});
$.ajax({
type : "POST",
url:url,
data:postData,
success:function(args){
alert("<%= localeMessage.getString("common.deleteMsg") %>");
goNav(returnUrl);//LIST로 이동
},
error:function(e){
alert(e.responseText);
$("#btn_delete").click(function() {
$(this).blur();
var ifName = $('input[name=desc]').val() || $('input[name=name]').val();
showConfirm('<strong>[' + ifName + ']</strong> 인터페이스를 삭제하시겠습니까?', {
type: 'error',
title: '삭제 확인',
confirmText: '삭제',
onConfirm: function() {
var postData = $('#ajaxForm').serializeArray();
postData.push({name: "cmd", value: "DELETE"});
$.ajax({
type: "POST",
url: url,
data: postData,
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);//LIST로 이동
});
$("#btn_previous").click(function() {
goNav(returnUrl);
});
buttonControl(isDetail);
titleControl(isDetail);
$('.detail-tab').click(function() {
switchTab($(this).data('tab'));
});
if (isDetail) {
$('#tabStatus').show();
}
$('#btn_refresh_bucket').click(function() {
loadBucketStatus();
});
});
</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">
<div class="search_wrap">
<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.modify") %></button>
<button type="button" class="cssbtn" id="btn_previous" level="R" status="DETAIL,NEW"><i class="material-icons">arrow_back</i> <%= localeMessage.getString("button.previous") %></button>
<%-- <img src="<c:url value="/img/btn_delete.png"/>" alt="" id="btn_delete" level="W" status="DETAIL"/> --%>
<%-- <img src="<c:url value="/img/btn_modify.png"/>" alt="" id="btn_modify" level="W" status="DETAIL,NEW" /> --%>
<%-- <img src="<c:url value="/img/btn_previous.png"/>" alt="" id="btn_previous" level="R" status="DETAIL,NEW"/> --%>
</div>
<div class="title"><%= localeMessage.getString("infIfConMan.title") %><span class="tooltip" ><%= localeMessage.getString("infIfConMan.title") %></span></div>
<table id="grid" ></table>
<div id="pager"></div>
<!-- detail -->
<form id="ajaxForm">
<table class="table_row" cellspacing="0">
<tr>
<th style="width:20%;"><%= localeMessage.getString("infIfConMan.ifNm") %></th>
<td><input type="text" name="name" /></td>
</tr>
<tr>
<th style="width:20%;"><%= localeMessage.getString("infIfConMan.ifDesc") %></th>
<td><input type="text" name="desc" /></td>
</tr>
<tr>
<th style="width:20%;"><%= localeMessage.getString("infAdpConMan.thrPerSecond") %></th>
<td><input type="text" name="thresholdPerSecond" /></td>
</tr>
<tr>
<th style="width:20%;"><%= localeMessage.getString("infAdpConMan.thr") %></th>
<td><input type="text" name="threshold" /></td>
</tr>
<tr>
<th style="width:20%;"><%= localeMessage.getString("infAdpConMan.thrTimeUnit") %></th>
<td>
<div class="select-style">
<select name="thresholdTimeUnit" />
</div>
</td>
</tr>
<tr>
<th style="width:20%;"><%= localeMessage.getString("infAdpConMan.useYn") %></th>
<td>
<div class="select-style">
<select name="useYn" />
</div>
</td>
</tr>
</table>
</form>
</div><!-- end content_middle -->
</div><!-- end right_box -->
</body>
</html>
<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_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" level="R" status="DETAIL,NEW"><i class="material-icons">arrow_back</i> <%= localeMessage.getString("button.previous") %></button>
</div>
<div class="title-wrap">
<div class="title"><%= localeMessage.getString("infIfConMan.title") %><span class="tooltip"><%= localeMessage.getString("infIfConMan.title") %></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">
<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">
<span class="section-label">기본 정보</span>
<table class="form-table" cellspacing="0">
<colgroup>
<col style="width:150px" />
<col />
</colgroup>
<tbody>
<tr>
<th><%= localeMessage.getString("infIfConMan.ifNm") %></th>
<td><input type="text" name="name" style="width:300px;background-color: #ebebec;" /></td>
</tr>
<tr>
<th><%= localeMessage.getString("infIfConMan.ifDesc") %></th>
<td><input type="text" name="desc" style="width:300px;background-color: #ebebec;" /></td>
</tr>
<tr>
<th><%= localeMessage.getString("infAdpConMan.useYn") %></th>
<td>
<select name="useYn" class="styled-select"></select>
</td>
</tr>
</tbody>
</table>
<span class="section-label">유량제어 설정</span>
<table class="form-table" cellspacing="0">
<colgroup>
<col style="width:150px" />
<col />
</colgroup>
<tbody>
<tr>
<th>
<%= localeMessage.getString("infAdpConMan.thrPerSecond") %>
<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>
<%= localeMessage.getString("infAdpConMan.thr") %>
<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" class="styled-select"></select>
<input type="number" name="threshold" id="thresholdInput" placeholder="0" />
<span class="unit" id="thresholdUnit">requests</span>
</div>
</td>
</tr>
</tbody>
</table>
</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><!-- end content_middle -->
</div><!-- end right_box -->
</body>
</html>
@@ -38,12 +38,22 @@
var loginUser = '<%= loginVo.getUserId() %>';
var url = '<c:url value="/onl/apim/approval/portalApprovalMan.json" />';
var url_view = '<c:url value="/onl/apim/approval/portalApprovalMan.view" />';
function dateFormat(cellvalue) {
if ( cellvalue == null || cellvalue.length < 8 )
return '';
return cellvalue.substr(0 ,4) + '-' +
cellvalue.substr(4, 2) + '-' +
cellvalue.substr(6, 2) ;
}
$(document).ready(function () {
var key = "${param.id}";
if (key) {
detail(key);
}
$("#expectEndDate").datepicker().inputmask("yyyy-mm-dd", {'autoUnmask': true});
$("#btn_app_approve").click(function () {
if (confirm("승인하시겠습니까?")) {
@@ -112,6 +122,28 @@
});
}
});
$("#btn_modify").click(function () {
if (confirm("수정하시겠습니까?")) {
$.ajax({
type: "POST",
url: url,
data: {cmd: "UPDATE", id: key, expectEndDate: $("#expectEndDate").val().replaceAll('-','')},
success: function (args) {
alert("수정되었습니다.");
if (window.dialogArguments && window.dialogArguments.self) {
window.dialogArguments.self.location.reload();
} else if (window.opener) {
window.opener.location.reload();
}
window.close();
},
error: function (e) {
alert(e.responseText);
}
});
}
});
$("#btn_close").click(function () {
window.close();
@@ -126,12 +158,25 @@
data: {cmd: 'DETAIL', id: key},
success: function (json) {
var data = json;
if (data.approvalStatus.code == 'END') {
$('#btn_modify').hide();
$('#expectEndDate').hide();
$('#spExpectEndDate').show();
$('#spExpectEndDate').text(dateFormat(data.expectEndDate));
} else {
$('#btn_modify').show();
$('#expectEndDate').show();
$('#spExpectEndDate').hide();
}
$("#approvalType").text(data.approvalType === 'USER' ? "법인사용자" : "API 사용");
$("#approvalStatus").text(data.approvalStatus.description);
$("#requesterName").text(data.requester.userName);
$("#approvalSubject").text(data.approvalSubject);
$("#approvalDetail").html(data.approvalDetail);
$("#createdDate").text(data.createdDate).inputmask("9999-99-99 99:99:99.999", {'autoUnmask': true});
$("#expectEndDate").val(data.expectEndDate);
if (data.approvers.some(approver =>
approver.approverStatus === 'CURRENT' &&
@@ -144,7 +189,7 @@
document.getElementById("btn_app_reject").style.display = "none";
}
if (data.approvalStatus.description === '배포실패') {
if (data.approvalStatus.code === 'FAILED') {
document.getElementById("btn_app_redeploy").style.display = "";
}
@@ -242,9 +287,14 @@
</tr>
<tr>
<th>상세 내용</th>
<td colspan="3">
<td>
<div id="approvalDetail" style="white-space: pre-wrap;"></div>
</td>
<th>예상완료일</th>
<td>
<input type="text" id="expectEndDate" style="width: 120px; text-align:center"/>
<span id="spExpectEndDate"></span>
</td>
</tr>
</table>
@@ -282,6 +332,9 @@
</table>
</div>
<div class="popup_button">
<button type="button" class="cssbtn" id="btn_modify" level="W"><i
class="material-icons">save</i>수정
</button>
<button type="button" class="cssbtn" style="display: none;" id="btn_app_approve" level="W"><i
class="material-icons">check</i>승인
</button>
@@ -22,6 +22,14 @@
'USER': '법인사용자',
'APP': 'API 사용'
};
function dateFormat(cellvalue, options, rowObject) {
if ( cellvalue == null || cellvalue.length < 8 )
return '';
return cellvalue.substr(0 ,4) + '-' +
cellvalue.substr(4, 2) + '-' +
cellvalue.substr(6, 2) ;
}
function deleteSelectedApprovals() {
var selectedIds = $("#grid").jqGrid('getGridParam', 'selarrrow');
@@ -74,7 +82,7 @@
searchApprovalStatus: $('select[name=searchApprovalStatus]').val(),
searchApprovalSubject: $('input[name=searchApprovalSubject]').val()
},
colNames: ['No.', 'id', '제목', '승인 유형', '승인 유형', '승인 상태','요청자', '생성일'],
colNames: ['No.', 'id', '제목', '승인 유형', '승인 유형', '승인 상태','요청자', '생성일', '예상완료일'],
colModel: [
{ name: 'rowNum', align: 'center', width: '30', sortable: false},
{ name: 'id', align: 'center', key: true, hidden: true },
@@ -83,7 +91,8 @@
{ name: 'approvalType', align: 'center', width: 80, hidden: true},
{ name: 'approvalStatus.description', align: 'center', width: 80 },
{ name: 'requester.userName', align: 'center', width: 100 },
{ name: 'createdDate', align: 'center', width: 120, formatter: 'date', formatoptions: { srcformat: 'ISO8601Long', newformat: 'Y-m-d H:i:s' } }
{ name: 'createdDate', align: 'center', width: 120, formatter: 'date', formatoptions: { srcformat: 'ISO8601Long', newformat: 'Y-m-d H:i:s' } },
{ name: 'expectEndDate', align: 'center', width: 100, formatter: dateFormat},
],
jsonReader: {
repeatitems: false
@@ -189,8 +198,8 @@
<div class="select-style">
<select name="searchApprovalStatus">
<option value="">전체</option>
<option value="CREATED">생성됨</option>
<option value="REQUESTED">요청됨</option>
<option value="PROCESSING">진행중</option>
<option value="DENIED">반려됨</option>
<option value="END">승인됨</option>
</select>
@@ -20,15 +20,22 @@
var select_orgId = "";
function formatInquiryStatus(cellvalue, options, rowObject) {
return cellvalue === 'PENDING'
? '<span style="color: red;">문의중</span>'
: '<span>답변완료</span>';
if (cellvalue == 'PENDING') {
return '<span style="color: red;">문의중</span>';
} else if (cellvalue == 'RESPONDED') {
return '<span >답변완료</span>'
} else if (cellvalue == 'CLOSED') {
return '<span >답변종료</span>'
} else {
return cellvalue;
}
}
function formatFile(cellvalue, options, rowObject) {
var iconPath = '${pageContext.request.contextPath}/images/icon_file.png';
var icon = rowObject.hasAttachment ? '<img src="' + iconPath + '" alt="File Attached" style="vertical-align: middle; margin-left: 5px; width: 16px; height: 16px;">' : '';
return cellvalue + icon;
var commentCount = rowObject.commentCount > 0 ? ' <span style="color: #337ab7;">[' + rowObject.commentCount + ']</span>' : '';
return cellvalue + icon + commentCount;
}
function deleteSelectedRows() {
@@ -36,6 +36,13 @@
padding-bottom: 10px;
border-bottom: 2px solid #e0e0e0; /* 제목 아래 줄 긋기 */
}
.comment-section {
margin-bottom: 30px;
border: 1px solid #e0e0e0; /* 동일한 박스 스타일 */
padding: 20px;
border-radius: 5px;
}
.info-row {
margin-bottom: 10px;
@@ -57,6 +64,15 @@
width: 100%;
min-height: 200px;
}
#commentDetail {
min-height: 100px;
border: 1px solid #00000032;
border-radius: 4px;
padding: 10px;
flex: 1;
font-family: '맑은고딕'
}
.required-mark {
color: red;
@@ -68,6 +84,7 @@
var url = '<c:url value="/onl/apim/portalinquiry/portalInquiryMan.json" />';
var url_view = '<c:url value="/onl/apim/portalinquiry/portalInquiryMan.view" />';
var file_download = '<c:url value="/file/download.do" />';
var inquiryStatus = 'PENDING';
function decodeHTMLEntities(text) {
var textArea = document.createElement('textarea');
@@ -82,10 +99,21 @@
dataType: "json",
data: {cmd: 'DETAIL', id: key},
success: function (data) {
$("#id").val(key);
$("#inquirySubject").text(data.inquirySubject);
$("#inquiryDetail").html(data.inquiryDetail);
$("#inquiryStatus").text(data.inquiryStatus === 'PENDING' ? '문의중' : '답변완료');
inquiryStatus = data.inquiryStatus;
if (data.inquiryStatus == 'PENDING') {
$("#inquiryStatus").text('문의중');
} else if (data.inquiryStatus == 'RESPONDED') {
$("#inquiryStatus").text('답변완료');
} else if (data.inquiryStatus == 'CLOSED') {
$("#inquiryStatus").text('답변종료');
}
$("#inquirer").text(data.inquirer.userName);
$("#createdDate").text(timeStampFormat(data.createdDate));
$("#responder").text(data.responderName || '');
@@ -94,6 +122,8 @@
var decodedContent = decodeHTMLEntities(data.responseDetail);
$("#responseDetail").summernote('code', decodedContent || '');
// 댓글 표시
listComment(key);
// 첨부 파일 표시
displayAttachFiles(data.fileInfo);
@@ -104,6 +134,59 @@
}
});
}
function listComment(inquiryId) {
$.ajax({
type: "POST",
url: url,
dataType: "json",
data: {cmd: 'LIST_COMMENT', id: inquiryId},
success: function (data) {
var $commentList = $('#commentList');
$commentList.empty();
if (!data || data.length === 0) return;
$.each(data, function (i, comment) {
var date = comment.createdDate ? timeStampFormat(comment.createdDate) : '';
var row = $('<div>').addClass('info-row');
$('<div>').addClass('info-label').css('color', '#555').text(comment.writerName || '').appendTo(row);
var content = $('<div>').addClass('info-content');
$('<div>').css('white-space', 'pre-wrap').text(comment.commentDetail).appendTo(content);
var meta = $('<div>').css({'font-size': '0.9em', 'color': '#999', 'margin-top': '2px'});
$('<span>').text(date).appendTo(meta);
$('<button>').attr('type', 'button').addClass('cssbtn smallBtn2').css({'margin-left': '8px', 'font-size': '0.85em', 'padding': '1px 6px'})
.text('delete').on('click', (function(id, inquiryId) {
return function() { deleteComment(id, inquiryId); };
})(comment.id, comment.inquiryId))
.appendTo(meta);
meta.appendTo(content);
content.appendTo(row);
row.appendTo($commentList);
});
},
error: function (e) {
alert(e.responseText);
}
});
}
function deleteComment(commentId, inquiryId) {
if (inquiryStatus == 'CLOSED') {
alert('답변이 종료되어 삭제할 수 없습니다');
return;
}
if (!confirm("댓글을 삭제하시겠습니까?")) return;
$.ajax({
type: "POST",
url: url,
data: {cmd: 'DELETE_COMMENT', id: commentId},
success: function () {
listComment(inquiryId);
},
error: function (e) {
alert(e.responseText);
}
});
}
$(document).ready(function () {
var returnUrl = getReturnUrlForReturn();
@@ -114,12 +197,17 @@
placeholder: '여기에 답변을 입력하세요.',
height: 200
});
if (key) {
detail(key);
}
$("#btn_modify").click(function () {
if (inquiryStatus == 'CLOSED') {
alert('답변이 종료되어 수정할 수 없습니다');
return;
}
if (!checkRequired("ajaxForm")) return;
if (!confirm("<%= localeMessage.getString("common.checkSave")%>")) return;
@@ -139,6 +227,32 @@
}
});
});
$("#btn_comment").click(function () {
if (inquiryStatus == 'CLOSED') {
alert('답변이 종료되어 댓글을 등록할 수 없습니다');
return;
}
if (!confirm("<%= localeMessage.getString("common.checkSave")%>")) return;
$.ajax({
type: "POST",
url: url,
data: {
cmd: "INSERT_COMMENT",
inquiryId: $('#id').val(),
commentDetail: $('#commentDetail').val(),
},
success: function (json) {
alert("<%= localeMessage.getString("common.saveMsg") %>");
$('#commentDetail').val('');
listComment(key);
},
error: function (e) {
alert(e.responseText);
}
});
});
$("#btn_previous").click(function () {
goNav(returnUrl);
@@ -206,6 +320,10 @@
<!-- 답변 작성 부분 -->
<div class="response-section">
<div class="info-row">
<div class="info-label">상태</div>
<div id="inquiryStatus" class="info-content"></div>
</div>
<div class="info-row">
<div class="info-label">답변자</div>
<div id="responder" class="info-content"></div>
@@ -221,6 +339,19 @@
</div>
</div>
</div>
<!-- 댓글 작성 부분 -->
<div class="comment-section">
<div id="commentList"></div>
<div class="info-row">
<div class="info-label">${sessionScope.login.userName}</div>
<div style="flex:1; display: flex; gap: 8px; align-items: stretch;">
<textarea id="commentDetail" name="commentDetail" data-required data-warning="댓글 내용을 입력하여 주십시오."></textarea>
<button type="button" class="cssbtn" id="btn_comment" level="W" style="width:100px; height: 100px; text-align:center; ">댓글등록</button>
</div>
</div>
</div>
<input type="hidden" name="id" id="id">
</form>
</div>