- PortalNoticeManService 장애 상태 타임라인 로직 추가 - 메인 CSS에 타임라인 스타일 추가
This commit is contained in:
@@ -18,17 +18,43 @@
|
||||
var url = '<c:url value="/onl/apim/portalnotice/portalNoticeMan.json" />';
|
||||
var url_view = '<c:url value="/onl/apim/portalnotice/portalNoticeMan.view" />';
|
||||
var combo;
|
||||
|
||||
|
||||
// 코드 상수 — PortalNoticeUI 와 동기화
|
||||
var NOTICE_TYPE_INCIDENT = '3';
|
||||
|
||||
// IncidentState 라벨 — 상세 화면과 동일하게 유지
|
||||
var STATE_LABELS = {
|
||||
'INVESTIGATING': '조사중',
|
||||
'IDENTIFIED': '원인 파악',
|
||||
'MONITORING': '모니터링',
|
||||
'RESOLVED': '해소',
|
||||
'CANCELED': '취소'
|
||||
};
|
||||
|
||||
function formatIncidentState(cellvalue, options, rowObject) {
|
||||
if (!cellvalue) return '';
|
||||
var label = STATE_LABELS[cellvalue] || cellvalue;
|
||||
var closed = (cellvalue === 'RESOLVED' || cellvalue === 'CANCELED');
|
||||
return closed
|
||||
? '<span title="' + cellvalue + '">' + label + '</span>'
|
||||
: '<span style="color: red;" title="' + cellvalue + '">' + label + '</span>';
|
||||
}
|
||||
|
||||
function formatNoticeType(cellvalue, options, rowObject) {
|
||||
if (!combo || !combo.noticeTypeList) {
|
||||
return cellvalue;
|
||||
}
|
||||
for (var i = 0; i < combo.noticeTypeList.length; i++) {
|
||||
if (combo.noticeTypeList[i].CODE == cellvalue) {
|
||||
return combo.noticeTypeList[i].NAME;
|
||||
var name = cellvalue;
|
||||
if (combo && combo.noticeTypeList) {
|
||||
for (var i = 0; i < combo.noticeTypeList.length; i++) {
|
||||
if (combo.noticeTypeList[i].CODE == cellvalue) {
|
||||
name = combo.noticeTypeList[i].NAME;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return cellvalue;
|
||||
// 장애인데 종료 시각이 없으면 아직 진행 중이다
|
||||
if (cellvalue == NOTICE_TYPE_INCIDENT && rowObject && !rowObject.endAt) {
|
||||
return '<span style="color: red;">' + name + '-진행중</span>';
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
function formatuseYn(cellvalue, options, rowObject) {
|
||||
@@ -115,14 +141,15 @@
|
||||
searchNoticeSubject: $('input[name=searchNoticeSubject]').val(),
|
||||
searchNoticeDetail: $('input[name=searchNoticeDetail]').val()
|
||||
},
|
||||
colNames:['id', 'No.', '게시유형', '제목', '고정여부', '사용', '마지막 수정일', '등록일', '등록자', '조회수'],
|
||||
colNames:['id', 'No.', '게시유형', '제목', '고정여부', '사용', '장애상태', '마지막 수정일', '등록일', '등록자', '조회수'],
|
||||
colModel:[
|
||||
{ name : 'id' , align:'center', key:true, hidden:true},
|
||||
{ name : 'rowNum' , align:'center', width:60 },
|
||||
{ name : 'noticeType' , align:'center', width:60, formatter: formatNoticeType },
|
||||
{ name : 'noticeType' , align:'center', width:100, formatter: formatNoticeType },
|
||||
{ name : 'noticeSubject' , align:'left' , width:300, formatter: formatFile },
|
||||
{ name : 'fixYn' , align:'center', width:60, formatter: formatFixYn },
|
||||
{ name : 'useYn' , align:'center', width:60, formatter: formatuseYn },
|
||||
{ name : 'state' , align:'center', width:90, sortable:false, formatter: formatIncidentState },
|
||||
{ name : 'lastModifiedDate', align:'center', width:120 , formatter: timeStampFormat},
|
||||
{ name : 'createdDate' , align:'center', width:120, formatter: timeStampFormat },
|
||||
{ name : 'inquirerName' , align:'center', width:100 },
|
||||
|
||||
@@ -32,10 +32,35 @@
|
||||
// 영향 API 목록 (UI 상태)
|
||||
var affectedApis = [];
|
||||
|
||||
// 장애 타임라인 (UI 상태)
|
||||
var currentIncidentId = null;
|
||||
var currentIncidentState = null;
|
||||
|
||||
// 종결 상태 — 재전이 불가 (재발은 새 장애로 등록)
|
||||
var TERMINAL_STATES = ['RESOLVED', 'CANCELED'];
|
||||
|
||||
// IncidentState 라벨 — 상태 콤보 문구와 동일하게 유지
|
||||
var STATE_LABELS = {
|
||||
'INVESTIGATING': '조사중',
|
||||
'IDENTIFIED': '원인 파악',
|
||||
'MONITORING': '모니터링',
|
||||
'RESOLVED': '해소',
|
||||
'CANCELED': '취소'
|
||||
};
|
||||
|
||||
function isIncidentType(t) {
|
||||
return t === NOTICE_TYPE_INCIDENT || t === NOTICE_TYPE_MAINTENANCE;
|
||||
}
|
||||
|
||||
function stateLabel(code) {
|
||||
if (!code) return '';
|
||||
return (STATE_LABELS[code] || code) + ' (' + code + ')';
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
return $('<div>').text(text == null ? '' : text).html();
|
||||
}
|
||||
|
||||
function init() {
|
||||
var key = "${param.id}";
|
||||
isDetail = key != "" && key != "null";
|
||||
@@ -76,12 +101,28 @@
|
||||
// INCIDENT 만 상태 영역 표시
|
||||
if (t === NOTICE_TYPE_INCIDENT) {
|
||||
$('.incident-state-row').show();
|
||||
$('.incident-endat-hint').show();
|
||||
} else {
|
||||
$('.incident-state-row').hide();
|
||||
$('.incident-endat-hint').hide();
|
||||
}
|
||||
// 장애는 종결(해소·취소) 후에만 종료 시각을 손볼 수 있다. 진행 중에는 상태를 따른다.
|
||||
var closed = $.inArray(currentIncidentState, TERMINAL_STATES) >= 0;
|
||||
$('#endAt').prop('readonly', t === NOTICE_TYPE_INCIDENT && !closed);
|
||||
$('.incident-endat-hint').text(closed
|
||||
? '※ 종결된 장애입니다. 실제 복구 시각으로 수정할 수 있습니다.'
|
||||
: '※ 타임라인에서 해소·취소로 전이하면 자동 입력됩니다.');
|
||||
} else {
|
||||
$('.incident-row').hide();
|
||||
$('.incident-state-row').hide();
|
||||
$('.incident-endat-hint').hide();
|
||||
$('#endAt').prop('readonly', false);
|
||||
}
|
||||
// 타임라인은 등록된 장애(수정 모드)에서만 다룬다
|
||||
if (t === NOTICE_TYPE_INCIDENT && isDetail && currentIncidentId) {
|
||||
$('#timelineSection').show();
|
||||
} else {
|
||||
$('#timelineSection').hide();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,6 +233,150 @@
|
||||
renderAffectedApis();
|
||||
}
|
||||
|
||||
// ───────────────────── 장애 처리 타임라인 ─────────────────────
|
||||
|
||||
/** 현재 상태 기준으로 전이 가능한 선택지만 남긴다. 실제 차단은 서버가 한다. */
|
||||
function applyStateChangeGuard() {
|
||||
var terminal = $.inArray(currentIncidentState, TERMINAL_STATES) >= 0;
|
||||
|
||||
$('#timelineStateChange').prop('disabled', terminal);
|
||||
if (terminal) {
|
||||
$('#timelineStateChange').prop('checked', false);
|
||||
$('#timelineNextState').prop('disabled', true).val('');
|
||||
}
|
||||
$('#timelineClosedHint').toggle(terminal);
|
||||
$('#timelineResolveHint').toggle(!terminal);
|
||||
|
||||
// 현재와 동일한 상태로는 전이할 수 없다
|
||||
$('#timelineNextState option').each(function() {
|
||||
var v = $(this).val();
|
||||
$(this).prop('disabled', v !== '' && v === currentIncidentState);
|
||||
});
|
||||
if ($('#timelineNextState').val() === currentIncidentState) {
|
||||
$('#timelineNextState').val('');
|
||||
}
|
||||
}
|
||||
|
||||
function loadTimeline() {
|
||||
if (!currentIncidentId) {
|
||||
renderTimeline([]);
|
||||
return;
|
||||
}
|
||||
$.ajax({
|
||||
type: "POST", url: url, dataType: "json",
|
||||
data: {cmd: 'TIMELINE_LIST', incidentId: currentIncidentId},
|
||||
success: function (rows) { renderTimeline(rows || []); },
|
||||
error: function (e) { alert(e.responseText); }
|
||||
});
|
||||
}
|
||||
|
||||
function renderTimeline(rows) {
|
||||
var $tb = $('#timelineTbody');
|
||||
$tb.empty();
|
||||
if (!rows.length) {
|
||||
$tb.append('<tr><td colspan="6" style="text-align:center;color:#999;">등록된 타임라인이 없습니다.</td></tr>');
|
||||
return;
|
||||
}
|
||||
$.each(rows, function(_, row) {
|
||||
var author = row.authorType === 'SYSTEM' ? 'System' : 'Admin';
|
||||
var visible = row.visibleYn === 'Y';
|
||||
var $tr = $('<tr>');
|
||||
$tr.append('<td style="text-align:center;white-space:nowrap;">' + escapeHtml(row.eventAt) + '</td>');
|
||||
$tr.append('<td style="text-align:center;">' + author + '</td>');
|
||||
$tr.append('<td style="text-align:center;white-space:nowrap;">' + (row.stateAfter ? escapeHtml(stateLabel(row.stateAfter)) : '-') + '</td>');
|
||||
$tr.append('<td>' + escapeHtml(row.body).replace(/\n/g, '<br>') + '</td>');
|
||||
|
||||
// 현재 상태(라벨) + 눌렀을 때 바뀔 상태(버튼) 를 나눠 표기 — 버튼 하나면 의미가 모호하다
|
||||
var $visibleTd = $('<td style="text-align:center;white-space:nowrap;">');
|
||||
$('<span>')
|
||||
.text(visible ? '공개중' : '비공개')
|
||||
.css({color: visible ? '#1B8C4A' : '#999', 'margin-right': '5px'})
|
||||
.appendTo($visibleTd);
|
||||
$('<button type="button" class="cssbtn" level="W" status="DETAIL" style="min-width:0;padding:3px 8px;">')
|
||||
.text(visible ? '비공개로' : '공개로')
|
||||
.click(function() { toggleTimelineVisible(row.timelineId, visible ? 'N' : 'Y'); })
|
||||
.appendTo($visibleTd);
|
||||
$tr.append($visibleTd);
|
||||
|
||||
var $deleteTd = $('<td style="text-align:center;">');
|
||||
$('<button type="button" class="cssbtn smallBtn" level="W" status="DETAIL" style="min-width:0;width:90%;">')
|
||||
.text('삭제')
|
||||
.click(function() { deleteTimeline(row.timelineId); })
|
||||
.appendTo($deleteTd);
|
||||
$tr.append($deleteTd);
|
||||
|
||||
$tb.append($tr);
|
||||
});
|
||||
buttonControl(true);
|
||||
}
|
||||
|
||||
function addTimeline() {
|
||||
if (!currentIncidentId) {
|
||||
alert('장애 정보를 먼저 저장하여 주십시오.');
|
||||
return;
|
||||
}
|
||||
var body = $('#timelineBody').val();
|
||||
if (!body || !$.trim(body)) {
|
||||
alert('타임라인 메세지를 입력하여 주십시오.');
|
||||
$('#timelineBody').focus();
|
||||
return;
|
||||
}
|
||||
var stateChange = $('#timelineStateChange').is(':checked');
|
||||
var nextState = $('#timelineNextState').val();
|
||||
if (stateChange && !nextState) {
|
||||
alert('변경할 상태를 선택하여 주십시오.');
|
||||
return;
|
||||
}
|
||||
$.ajax({
|
||||
type: "POST", url: url, dataType: "json",
|
||||
data: {cmd: 'TIMELINE_ADD', incidentId: currentIncidentId, body: body,
|
||||
stateChange: stateChange, nextState: nextState},
|
||||
success: function (data) {
|
||||
$('#timelineBody').val('');
|
||||
$('#timelineStateChange').prop('checked', false).trigger('change');
|
||||
// 상태 전이 시 서버가 상태/이전 상태/종료 시각을 함께 갱신한다
|
||||
$('#stateText').text(data.state ? stateLabel(data.state) : '');
|
||||
$('#previousState').text(data.previousState ? stateLabel(data.previousState) : '없음');
|
||||
$('#endAt').val(data.endAt || '');
|
||||
currentIncidentState = data.state || null;
|
||||
applyStateChangeGuard();
|
||||
// 종결 전이 직후부터 종료 시각을 손볼 수 있어야 한다
|
||||
toggleIncidentFields();
|
||||
loadTimeline();
|
||||
},
|
||||
error: function (e) { alert(e.responseText); }
|
||||
});
|
||||
}
|
||||
|
||||
function toggleTimelineVisible(timelineId, visibleYn) {
|
||||
$.ajax({
|
||||
type: "POST", url: url,
|
||||
data: {cmd: 'TIMELINE_VISIBLE', timelineId: timelineId, visibleYn: visibleYn},
|
||||
success: loadTimeline,
|
||||
error: function (e) { alert(e.responseText); }
|
||||
});
|
||||
}
|
||||
|
||||
function deleteTimeline(timelineId) {
|
||||
if (confirm("<%= localeMessage.getString("common.checkDelete")%>") != true) return;
|
||||
$.ajax({
|
||||
type: "POST", url: url,
|
||||
data: {cmd: 'TIMELINE_DELETE', timelineId: timelineId},
|
||||
success: loadTimeline,
|
||||
error: function (e) { alert(e.responseText); }
|
||||
});
|
||||
}
|
||||
|
||||
/** 장애/점검 공지는 최초 게시 후 게시유형을 바꿀 수 없다. */
|
||||
function lockNoticeType(noticeType) {
|
||||
if (!isIncidentType(noticeType)) return;
|
||||
$('#noticeType').prop('disabled', true);
|
||||
if (!$('#noticeTypeHidden').length) {
|
||||
$('#ajaxForm').append('<input type="hidden" id="noticeTypeHidden" name="noticeType">');
|
||||
}
|
||||
$('#noticeTypeHidden').val(noticeType);
|
||||
}
|
||||
|
||||
function detail(key) {
|
||||
if (!isDetail) return;
|
||||
$.ajax({
|
||||
@@ -207,17 +392,28 @@
|
||||
var decodedContent = decodeHTMLEntities(data.noticeDetail);
|
||||
$('#contents').summernote('code', decodedContent);
|
||||
|
||||
// 게시유형은 최초 저장 후 변경 불가 (disabled 는 전송되지 않아 hidden 으로 값을 유지)
|
||||
lockNoticeType(data.noticeType);
|
||||
|
||||
// 장애/점검 필드
|
||||
$('#summary').val(data.summary || '');
|
||||
$('#startedAt').val(data.startedAt || '');
|
||||
$('#endAt').val(data.endAt || '');
|
||||
$('#state').val(data.state || 'INVESTIGATING');
|
||||
$('#previousState').text(data.previousState || '없음');
|
||||
// 상태/이전 상태는 타임라인 게시로만 바뀐다 (직접 수정 불가)
|
||||
$('#stateText').text(data.state ? stateLabel(data.state) : '');
|
||||
$('#previousState').text(data.previousState ? stateLabel(data.previousState) : '없음');
|
||||
affectedApis = (data.affectedApis || []).map(function(a){
|
||||
return {apiId: a.apiId, apiName: a.apiName || ''};
|
||||
});
|
||||
renderAffectedApis();
|
||||
|
||||
currentIncidentId = data.incidentId || null;
|
||||
currentIncidentState = data.state || null;
|
||||
applyStateChangeGuard();
|
||||
toggleIncidentFields();
|
||||
if (data.noticeType === NOTICE_TYPE_INCIDENT && currentIncidentId) {
|
||||
loadTimeline();
|
||||
}
|
||||
|
||||
if (data.fileInfo) {
|
||||
fileInfo = {
|
||||
@@ -245,6 +441,13 @@
|
||||
$('#btn_addAffectedApi').click(openApiPopup);
|
||||
$('#btn_removeAffectedApi').click(removeSelectedAffectedApis);
|
||||
|
||||
$('#timelineStateChange').on('change', function() {
|
||||
var checked = $(this).is(':checked');
|
||||
$('#timelineNextState').prop('disabled', !checked);
|
||||
if (!checked) $('#timelineNextState').val('');
|
||||
});
|
||||
$('#btn_addTimeline').click(addTimeline);
|
||||
|
||||
$('#fileInput').change(function(e) {
|
||||
var file = e.target.files[0];
|
||||
if (file) {
|
||||
@@ -273,6 +476,11 @@
|
||||
alert('점검 종료 시각을 입력해 주십시오.');
|
||||
return;
|
||||
}
|
||||
// datetime-local 값은 'YYYY-MM-DDTHH:mm' 이라 문자열 비교로 선후가 가려진다
|
||||
if ($('#endAt').val() && $('#endAt').val() < $('#startedAt').val()) {
|
||||
alert('종료 시각은 시작 시각보다 빠를 수 없습니다.');
|
||||
return;
|
||||
}
|
||||
if (affectedApis.length === 0) {
|
||||
alert('영향 API를 1건 이상 선택해 주십시오.');
|
||||
return;
|
||||
@@ -394,21 +602,15 @@
|
||||
<th>종료</th>
|
||||
<td>
|
||||
<input type="datetime-local" id="endAt" name="endAt" style="width:90%" placeholder="해소시 자동 채움">
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="incident-row incident-state-row" style="display:none;">
|
||||
<th>상태</th>
|
||||
<td>
|
||||
<div class="select-style">
|
||||
<select id="state" name="state">
|
||||
<option value="INVESTIGATING">INVESTIGATING - 조사중</option>
|
||||
<option value="IDENTIFIED">IDENTIFIED - 원인 파악</option>
|
||||
<option value="MONITORING">MONITORING - 모니터링</option>
|
||||
<option value="RESOLVED">RESOLVED - 해소</option>
|
||||
<option value="CANCELED">CANCELED - 취소</option>
|
||||
</select>
|
||||
<div class="incident-endat-hint" style="display:none;color:#888;font-size:12px;margin-top:3px;">
|
||||
※ 타임라인에서 해소·취소로 전이하면 자동 입력됩니다.
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- 상태/이전 상태는 타임라인 게시로만 전이한다 (직접 수정 불가) -->
|
||||
<tr class="incident-row incident-state-row" style="display:none;">
|
||||
<th>상태</th>
|
||||
<td><span id="stateText"></span></td>
|
||||
<th>이전 상태</th>
|
||||
<td><span id="previousState">없음</span></td>
|
||||
</tr>
|
||||
@@ -435,6 +637,61 @@
|
||||
</tr>
|
||||
</table>
|
||||
</form>
|
||||
|
||||
<!-- 타임라인은 [수정] 저장과 별개로 게시 즉시 반영되므로 폼 밖에 둔다 -->
|
||||
<div id="timelineSection" style="display:none;margin-top:20px;">
|
||||
<div class="title">타임라인</div>
|
||||
<table class="table_row" cellspacing="0" style="width:100%;">
|
||||
<colgroup>
|
||||
<col style="width: 130px"/>
|
||||
<col style="width: 80px"/>
|
||||
<col style="width: 140px"/>
|
||||
<col/>
|
||||
<col style="width: 160px"/>
|
||||
<col style="width: 80px"/>
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>시간</th>
|
||||
<th>작성자</th>
|
||||
<th>상태</th>
|
||||
<th>메세지</th>
|
||||
<th>개발자포탈 공개</th>
|
||||
<th>삭제</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="timelineTbody">
|
||||
<tr><td colspan="6" style="text-align:center;color:#999;">등록된 타임라인이 없습니다.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div style="margin-top:8px;display:flex;align-items:center;gap:8px;">
|
||||
<label>
|
||||
<input type="checkbox" id="timelineStateChange"> 상태 변경
|
||||
</label>
|
||||
<div class="select-style" style="width:220px;">
|
||||
<select id="timelineNextState" disabled>
|
||||
<option value="">(현재 상태 유지)</option>
|
||||
<option value="INVESTIGATING">INVESTIGATING - 조사중</option>
|
||||
<option value="IDENTIFIED">IDENTIFIED - 원인 파악</option>
|
||||
<option value="MONITORING">MONITORING - 모니터링</option>
|
||||
<option value="RESOLVED">RESOLVED - 해소</option>
|
||||
<option value="CANCELED">CANCELED - 취소</option>
|
||||
</select>
|
||||
</div>
|
||||
<span id="timelineResolveHint" style="color:#888;font-size:12px;">※ 해소(RESOLVED)·취소(CANCELED)로 전이하면 종료 시각이 자동 입력됩니다.</span>
|
||||
<span id="timelineClosedHint" style="display:none;color:#C0392B;font-size:12px;">
|
||||
※ 종결된 장애입니다. 상태 변경 불가 — 재발 시 새 장애로 등록하십시오.
|
||||
</span>
|
||||
</div>
|
||||
<div style="margin-top:5px;display:flex;gap:5px;">
|
||||
<textarea id="timelineBody" style="flex:1;height:60px;"
|
||||
placeholder="타임라인 메세지 입력 예 : 모니터링 5분간 정상 응답 확인 후 완료 상태 전이 예정"></textarea>
|
||||
<button type="button" class="cssbtn" id="btn_addTimeline" level="W" status="DETAIL">
|
||||
<i class="material-icons">send</i> 게시
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
Reference in New Issue
Block a user