748 lines
28 KiB
Plaintext
748 lines
28 KiB
Plaintext
<%@ page language="java" contentType="text/html; charset=utf-8"%>
|
|
<%@ page import="java.io.*"%>
|
|
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
|
|
<%@ taglib uri="http://www.springframework.org/tags" prefix="spring"%>
|
|
<%@ include file="/jsp/common/include/localemessage.jsp" %>
|
|
<%
|
|
response.setHeader("Pragma", "No-cache");
|
|
response.setHeader("Cache-Control", "no-cache");
|
|
response.setHeader("Expires", "0");
|
|
%>
|
|
<html>
|
|
<head>
|
|
<title></title>
|
|
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
|
<jsp:include page="/jsp/common/include/css.jsp"/>
|
|
<link rel="stylesheet" type="text/css" href="<c:url value="/css/editor-content.css"/>"/>
|
|
<jsp:include page="/jsp/common/include/script.jsp"/>
|
|
|
|
<script language="javascript" >
|
|
var url = '<c:url value="/onl/apim/portalnotice/portalNoticeMan.json" />';
|
|
var url_view = '<c:url value="/onl/apim/portalnotice/portalNoticeMan.view" />';
|
|
var apiPopupUrl = '<c:url value="/onl/apim/apigroup/apiGroupMan.view" />';
|
|
var file_download = '<c:url value="/file/download.do" />';
|
|
var isDetail = false;
|
|
var fileInfo = null;
|
|
|
|
// 코드 상수 — PortalNoticeUI 와 동기화
|
|
var NOTICE_TYPE_NORMAL = '1';
|
|
var NOTICE_TYPE_MAINTENANCE = '2';
|
|
var NOTICE_TYPE_INCIDENT = '3';
|
|
|
|
// 영향 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";
|
|
|
|
if (isDetail) {
|
|
$("#title").append(" 수정");
|
|
buttonControl(true);
|
|
} else {
|
|
$("#title").append(" 등록");
|
|
$("#btn_modify").html('<i class="material-icons">save</i> <%= localeMessage.getString("button.register") %>');
|
|
buttonControl(false);
|
|
}
|
|
|
|
$.ajax({
|
|
type : "POST",
|
|
url:url,
|
|
dataType:"json",
|
|
data:{cmd: 'LIST_INIT_COMBO'},
|
|
success:function(json){
|
|
combo = json;
|
|
new makeOptions("CODE","NAME").setObj($("select[name=noticeType]")).setNoValueInclude(false).setData(json.noticeTypeList).rendering();
|
|
toggleIncidentFields();
|
|
|
|
if (key) {
|
|
detail(key);
|
|
}
|
|
},
|
|
error:function(e){
|
|
alert(e.responseText);
|
|
}
|
|
});
|
|
}
|
|
|
|
function toggleIncidentFields() {
|
|
var t = $('#noticeType').val();
|
|
if (isIncidentType(t)) {
|
|
$('.incident-row').show();
|
|
// 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);
|
|
}
|
|
// 양식(템플릿)은 장애 공지에만 쓴다 - 점검은 문구 체계가 다르다
|
|
$('.template-row').toggle(t === NOTICE_TYPE_INCIDENT);
|
|
|
|
// 타임라인은 등록된 장애(수정 모드)에서만 다룬다
|
|
if (t === NOTICE_TYPE_INCIDENT && isDetail && currentIncidentId) {
|
|
$('#timelineSection').show();
|
|
} else {
|
|
$('#timelineSection').hide();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 장애 공지 양식(제목·본문)을 서버에서 받아 채운다.
|
|
* 문구는 PTL_PROPERTY 'djb.apistatus.draft.*' 이며 자동 탐지 초안과 같은 양식이다.
|
|
*
|
|
* @param force true 면 이미 입력한 제목·본문도 (확인 후) 덮어쓴다
|
|
*/
|
|
function applyTemplate(force) {
|
|
if (force) {
|
|
var hasInput = $.trim($('#noticeSubject').val()) !== '' || !$('#contents').summernote('isEmpty');
|
|
if (hasInput && !confirm('현재 입력한 제목·본문을 양식으로 덮어씁니다. 계속하시겠습니까?')) {
|
|
return;
|
|
}
|
|
}
|
|
$.ajax({
|
|
type: "POST", url: url, dataType: "json",
|
|
data: {cmd: 'DRAFT_TEMPLATE', apisJson: JSON.stringify(affectedApis)},
|
|
success: function (data) {
|
|
if (!data) return;
|
|
$('#noticeSubject').val(data.subject || '');
|
|
$('#contents').summernote('code', data.detail || '');
|
|
},
|
|
error: function (e) { alert(e.responseText); }
|
|
});
|
|
}
|
|
|
|
function decodeHTMLEntities(text) {
|
|
var textArea = document.createElement('textarea');
|
|
textArea.innerHTML = text;
|
|
return textArea.value;
|
|
}
|
|
|
|
function formatFileSize(bytes) {
|
|
if(bytes == 0) return '0 Bytes';
|
|
var k = 1024,
|
|
sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'],
|
|
i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
}
|
|
|
|
function displayAttachFile(file) {
|
|
var $attachFiles = $('#attachFiles');
|
|
$attachFiles.empty();
|
|
if (file) {
|
|
var iconPath = '${pageContext.request.contextPath}/images/icon_file.png';
|
|
var fileItem = $('<div>').addClass('file-item');
|
|
$('<img>').attr('src', iconPath).css({'marginLeft': '15px','marginRight': '5px' ,'height': '1.2em'}).appendTo(fileItem);
|
|
var fileNameSpan = $('<span>').addClass('fileName')
|
|
.text(file.originalFileName + '.' + file.fileExtension + ' (' + formatFileSize(file.size) + ')')
|
|
.css('cursor', 'pointer')
|
|
.on('click', function() { if (file.fileId) downloadFile(file.fileId); });
|
|
fileNameSpan.appendTo(fileItem);
|
|
$('<button>').text('X').addClass('delete-file').css('marginLeft', '15px')
|
|
.on('click', function() {
|
|
fileInfo = null;
|
|
displayAttachFile(null);
|
|
$('<input>').attr({type: 'hidden', name: 'fileDeleted', value: 'true'}).appendTo('#ajaxForm');
|
|
}).appendTo(fileItem);
|
|
fileItem.appendTo($attachFiles);
|
|
}
|
|
}
|
|
|
|
function downloadFile(fileId) {
|
|
if (fileId) {
|
|
window.location.href = file_download + '?fileId=' + fileId + '&fileSn=1';
|
|
}
|
|
}
|
|
|
|
function renderAffectedApis() {
|
|
var $tb = $('#affectedApiTbody');
|
|
$tb.empty();
|
|
if (!affectedApis.length) {
|
|
$tb.append('<tr><td colspan="3" style="text-align:center;color:#999;">선택된 영향 API가 없습니다.</td></tr>');
|
|
return;
|
|
}
|
|
$.each(affectedApis, function(idx, api) {
|
|
var $tr = $('<tr>');
|
|
$tr.append('<td style="text-align:center;width:40px;"><input type="checkbox" class="affected-api-check" data-idx="' + idx + '"></td>');
|
|
$tr.append('<td>' + (api.apiId || '') + '</td>');
|
|
$tr.append('<td>' + (api.apiName || '') + '</td>');
|
|
$tb.append($tr);
|
|
});
|
|
}
|
|
|
|
function openApiPopup() {
|
|
// showModal 헬퍼는 urlAddServiceType + &menuId=getMenuId() + &pop=true 를 자동 부여 →
|
|
// AuthorizeInterceptor 가 정상적으로 권한 인식. window.open 직접 호출 시 권한 부족 발생.
|
|
// 본 페이지는 summernote 본문 에디터 등 다른 iframe 이 함께 존재할 수 있어
|
|
// frames[0] 가정은 위험 → DOM 에서 .ui-dialog 내 iframe 중 src=apiGroupMan 인 것을 식별.
|
|
// 공지 영향 API 는 포탈에 실제 노출되는 것만 고르면 되므로 두 조건을 켠 채로 연다.
|
|
// (타 화면에서 여는 팝업은 파라미터가 없어 해제 상태 — 팝업 기본값은 건드리지 않는다)
|
|
var selectedIds = affectedApis.map(function(a) { return a.apiId; }).join(',');
|
|
var url2 = apiPopupUrl + '?cmd=POPUP&searchApiSpecYn=Y&searchPortalDisplayYn=Y'
|
|
+ '&selectedApiIds=' + encodeURIComponent(selectedIds);
|
|
var args = {};
|
|
showModal(url2, args, 1280, 860, function () {
|
|
var doc = args.self && args.self.document;
|
|
if (!doc) return;
|
|
var popupFrame = null;
|
|
var iframes = doc.querySelectorAll('iframe');
|
|
for (var i = 0; i < iframes.length; i++) {
|
|
var src = iframes[i].getAttribute('src') || '';
|
|
if (src.indexOf('apiGroupMan') >= 0) {
|
|
popupFrame = iframes[i].contentWindow;
|
|
break;
|
|
}
|
|
}
|
|
if (!popupFrame) return;
|
|
var rv = popupFrame.returnValue;
|
|
if (!rv) return;
|
|
var rows = Array.isArray(rv) ? rv : [rv];
|
|
$.each(rows, function(_, item) {
|
|
if (!item || !item.apiId) return;
|
|
var exists = affectedApis.some(function(a){ return a.apiId === item.apiId; });
|
|
if (!exists) {
|
|
affectedApis.push({apiId: item.apiId, apiName: item.apiDesc || item.apiName || ''});
|
|
}
|
|
});
|
|
renderAffectedApis();
|
|
});
|
|
}
|
|
|
|
function removeSelectedAffectedApis() {
|
|
var idxes = [];
|
|
$('.affected-api-check:checked').each(function() {
|
|
idxes.push(parseInt($(this).data('idx'), 10));
|
|
});
|
|
if (!idxes.length) {
|
|
alert('삭제할 항목을 선택해주세요.');
|
|
return;
|
|
}
|
|
// 역순 삭제
|
|
idxes.sort(function(a,b){return b-a;}).forEach(function(i){ affectedApis.splice(i, 1); });
|
|
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({
|
|
type: "POST", url: url, dataType: "json",
|
|
data: {cmd: 'DETAIL', id: key},
|
|
success: function (data) {
|
|
$("#id").val(key);
|
|
$('#noticeType').val(data.noticeType);
|
|
$("#noticeSubject").val(data.noticeSubject);
|
|
$("#useYn").prop("checked", data.useYn === "Y");
|
|
$("#fixYn").prop("checked", data.fixYn === "Y");
|
|
|
|
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 || '');
|
|
// 상태/이전 상태는 타임라인 게시로만 바뀐다 (직접 수정 불가)
|
|
$('#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 = {
|
|
originalFileName: data.fileInfo.fileDetails[0].originalFileName,
|
|
fileExtension: data.fileInfo.fileDetails[0].fileExtension,
|
|
fileId: data.fileInfo.fileDetails[0].fileId,
|
|
size: data.fileInfo.fileDetails[0].fileSize
|
|
};
|
|
displayAttachFile(fileInfo);
|
|
}
|
|
},
|
|
error: function (e) { alert(e.responseText); }
|
|
});
|
|
}
|
|
|
|
$(document).ready(function () {
|
|
var returnUrl = getReturnUrlForReturn();
|
|
|
|
init();
|
|
|
|
initSummernote('#contents', { placeholder: '여기에 내용을 입력하세요.', height: 300 });
|
|
|
|
$('#noticeType').on('change', toggleIncidentFields);
|
|
|
|
// 신규 등록에서 장애를 고르면 빈 본문에 한해 양식을 자동으로 채운다.
|
|
// (수정 모드나 이미 쓴 본문은 건드리지 않는다 - 덮어쓰려면 '양식 적용' 버튼)
|
|
$('#noticeType').on('change', function () {
|
|
if (!isDetail && $(this).val() === NOTICE_TYPE_INCIDENT && $('#contents').summernote('isEmpty')) {
|
|
applyTemplate(false);
|
|
}
|
|
});
|
|
$('#btn_applyTemplate').click(function () { applyTemplate(true); });
|
|
|
|
$('#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) {
|
|
fileInfo = {
|
|
originalFileName: file.name.split('.').slice(0, -1).join('.'),
|
|
fileExtension: file.name.split('.').pop(),
|
|
file: file, size: file.size
|
|
};
|
|
displayAttachFile(fileInfo);
|
|
}
|
|
this.value = '';
|
|
});
|
|
|
|
$('#addFile').click(function() { $('#fileInput').click(); });
|
|
|
|
$("#btn_modify").click(function () {
|
|
if (!checkRequired("ajaxForm")) return;
|
|
|
|
var noticeType = $('#noticeType').val();
|
|
if (isIncidentType(noticeType)) {
|
|
if (!$('#startedAt').val()) {
|
|
alert('시작 시각을 입력해 주십시오.');
|
|
return;
|
|
}
|
|
if (noticeType === NOTICE_TYPE_MAINTENANCE && !$('#endAt').val()) {
|
|
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;
|
|
}
|
|
}
|
|
|
|
if (confirm("<%= localeMessage.getString("common.checkSave")%>") != true) return;
|
|
|
|
var formData = new FormData($("#ajaxForm")[0]);
|
|
// Summernote 그림 다이얼로그의 내부 파일 input(name="files")이 #ajaxForm 안에 있어
|
|
// 선택한 이미지가 지워지지 않고 남아 있으면 여기서 함께 딸려 온다.
|
|
// 본 화면은 첨부파일 기능을 쓰지 않으므로 항상 제거한다.
|
|
formData.delete("files");
|
|
formData.set("useYn", $("#useYn").is(":checked") ? "Y" : "N");
|
|
formData.set("fixYn", $("#fixYn").is(":checked") ? "Y" : "N");
|
|
formData.set("noticeDetail", $('#contents').summernote('code'));
|
|
|
|
// 시작/종료 시각이 비어 있으면 전송하지 않음 (서버 파싱 오류 방지)
|
|
if (!$('#startedAt').val()) formData.delete('startedAt');
|
|
if (!$('#endAt').val()) formData.delete('endAt');
|
|
|
|
// 영향 API → affectedApis[i].apiId/apiName 으로 전송
|
|
$.each(affectedApis, function(i, api) {
|
|
formData.set('affectedApis[' + i + '].apiId', api.apiId);
|
|
formData.set('affectedApis[' + i + '].apiName', api.apiName || '');
|
|
});
|
|
|
|
formData.append("cmd", isDetail ? "UPDATE" : "INSERT");
|
|
|
|
if (fileInfo && fileInfo.file) {
|
|
formData.set("files", fileInfo.file);
|
|
formData.set("fileName", fileInfo.originalFileName + '.' + fileInfo.fileExtension);
|
|
}
|
|
|
|
$.ajax({
|
|
type: "POST", url: url, data: formData,
|
|
processData: false, contentType: false,
|
|
success: function () {
|
|
alert("<%= localeMessage.getString("common.saveMsg") %>");
|
|
goNav(returnUrl);
|
|
},
|
|
error: function (e) { alert(e.responseText); }
|
|
});
|
|
});
|
|
|
|
$("#btn_delete").click(function () {
|
|
if (confirm("<%= localeMessage.getString("common.confirmMsg")%>")) {
|
|
var postData = $('#ajaxForm').serializeArray();
|
|
postData.push({name: "cmd", value: "DELETE"});
|
|
$.ajax({
|
|
type: "POST", url: url, data: postData,
|
|
success: function () {
|
|
alert("<%= localeMessage.getString("common.deleteMsg") %>");
|
|
goNav(returnUrl);
|
|
},
|
|
error: function (e) { alert(e.responseText); }
|
|
});
|
|
}
|
|
});
|
|
|
|
$("#btn_previous").click(function () { goNav(returnUrl); });
|
|
});
|
|
</script>
|
|
</head>
|
|
<body>
|
|
<div class="right_box">
|
|
<div class="content_top">
|
|
<ul class="path">
|
|
<li><a href="#">${rmsMenuPath}</a></li>
|
|
</ul>
|
|
</div>
|
|
<div class="content_middle">
|
|
<div class="search_wrap">
|
|
<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_delete" level="W" status="DETAIL"><i class="material-icons">delete</i> <%= localeMessage.getString("button.delete") %></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" id="title">공지사항</div>
|
|
<form id="ajaxForm" enctype="multipart/form-data" accept-charset="UTF-8">
|
|
<input type="hidden" name="id" id="id">
|
|
<table class="table_row" cellspacing="0">
|
|
<colgroup>
|
|
<col style="width: 10%"/>
|
|
<col style="width: 40%"/>
|
|
<col style="width: 10%"/>
|
|
<col style="width: 40%"/>
|
|
</colgroup>
|
|
<tr>
|
|
<th>게시유형 <font color="red">*</font></th>
|
|
<td colspan="3">
|
|
<div class="select-style">
|
|
<select name="noticeType" id="noticeType"></select>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<th>제목 <font color="red">*</font></th>
|
|
<td colspan="3"><input type="text" id="noticeSubject" name="noticeSubject" style="width:100%" data-required data-warning="제목을 입력하여 주십시오."/></td>
|
|
</tr>
|
|
<tr>
|
|
<th>사용여부</th>
|
|
<td>
|
|
<input type="checkbox" name="useYn" id="useYn" value="Y" ${portalNoticeUI.useYn eq 'Y' ? 'checked' : ''}> 사용
|
|
</td>
|
|
<th>상단고정</th>
|
|
<td>
|
|
<input type="checkbox" name="fixYn" id="fixYn" value="Y" ${portalNoticeUI.fixYn eq 'Y' ? 'checked' : ''}> 고정
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<th>본문 <font color="red">*</font>
|
|
<div class="template-row" style="display:none;margin-top:5px;">
|
|
<button type="button" class="cssbtn smallBtn" id="btn_applyTemplate" level="W" status="DETAIL,NEW"
|
|
style="min-width:0;width:90%;" title="장애 공지 양식으로 제목과 본문을 채웁니다.">양식 적용</button>
|
|
</div>
|
|
</th>
|
|
<td colspan="3">
|
|
<textarea id="contents" name="noticeDetail" style="width:100%;height:300px" data-required data-warning="본문을 입력하여 주십시오."></textarea>
|
|
</td>
|
|
</tr>
|
|
|
|
<!-- ───── 장애/점검 전용 영역 ───── -->
|
|
<tr class="incident-row" style="display:none;">
|
|
<th>시작 <font color="red">*</font></th>
|
|
<td>
|
|
<input type="datetime-local" id="startedAt" name="startedAt" style="width:90%">
|
|
</td>
|
|
<th>종료</th>
|
|
<td>
|
|
<input type="datetime-local" id="endAt" name="endAt" style="width:90%" placeholder="해소시 자동 채움">
|
|
<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>
|
|
<tr class="incident-row" style="display:none;">
|
|
<th>영향 API 선택</th>
|
|
<td colspan="3">
|
|
<div style="margin-bottom:5px;">
|
|
<button type="button" class="cssbtn smallBtn" id="btn_removeAffectedApi"><i class="material-icons">delete</i> 삭제</button>
|
|
<button type="button" class="cssbtn smallBtn" id="btn_addAffectedApi"><i class="material-icons">add</i> 추가</button>
|
|
</div>
|
|
<table class="table_row" cellspacing="0" style="width:100%;">
|
|
<thead>
|
|
<tr>
|
|
<th style="width:40px;text-align:center;"><input type="checkbox" id="affectedApiAll" onclick="$('.affected-api-check').prop('checked', this.checked);"></th>
|
|
<th>API ID</th>
|
|
<th>API 명</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="affectedApiTbody">
|
|
<tr><td colspan="3" style="text-align:center;color:#999;">선택된 영향 API가 없습니다.</td></tr>
|
|
</tbody>
|
|
</table>
|
|
</td>
|
|
</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>
|
|
</html>
|