- PortalNoticeManService 장애 상태 타임라인 로직 추가 - 메인 CSS에 타임라인 스타일 추가
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
package com.eactive.eai.rms.onl.apim.portalnotice;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 장애 처리 타임라인 (DJB_APISTATUS_INCIDENT_TIMELINE) 화면 표시용 VO.
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
public class IncidentTimelineUI {
|
||||
|
||||
private Long timelineId;
|
||||
|
||||
private Long incidentId;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm")
|
||||
private LocalDateTime eventAt;
|
||||
|
||||
/** 상태 변경이 있었던 항목만 값이 있다. (INVESTIGATING/IDENTIFIED/MONITORING/RESOLVED/CANCELED) */
|
||||
private String stateAfter;
|
||||
|
||||
private String body;
|
||||
|
||||
/** SYSTEM(자동 탐지) / OPERATOR(관리자 입력) */
|
||||
private String authorType;
|
||||
|
||||
/** 개발자포탈 공개 여부 */
|
||||
private String visibleYn;
|
||||
}
|
||||
+25
@@ -15,6 +15,7 @@ import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.io.IOException;
|
||||
@@ -86,4 +87,28 @@ public class PortalNoticeManController extends BaseAnnotationController {
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
@PostMapping(value = "/onl/apim/portalnotice/portalNoticeMan.json", params = "cmd=TIMELINE_LIST")
|
||||
public ResponseEntity<List<IncidentTimelineUI>> selectTimeline(Long incidentId) {
|
||||
return ResponseEntity.ok(portalNoticeManService.selectTimeline(incidentId));
|
||||
}
|
||||
|
||||
@PostMapping(value = "/onl/apim/portalnotice/portalNoticeMan.json", params = "cmd=TIMELINE_ADD")
|
||||
public ResponseEntity<PortalNoticeUI> appendTimeline(Long incidentId, String body,
|
||||
@RequestParam(defaultValue = "false") boolean stateChange,
|
||||
String nextState) {
|
||||
return ResponseEntity.ok(portalNoticeManService.appendTimeline(incidentId, body, stateChange, nextState));
|
||||
}
|
||||
|
||||
@PostMapping(value = "/onl/apim/portalnotice/portalNoticeMan.json", params = "cmd=TIMELINE_VISIBLE")
|
||||
public ResponseEntity<String> updateTimelineVisible(Long timelineId, String visibleYn) {
|
||||
portalNoticeManService.updateTimelineVisible(timelineId, visibleYn);
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
@PostMapping(value = "/onl/apim/portalnotice/portalNoticeMan.json", params = "cmd=TIMELINE_DELETE")
|
||||
public ResponseEntity<String> deleteTimeline(Long timelineId) {
|
||||
portalNoticeManService.deleteTimeline(timelineId);
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+176
-8
@@ -3,6 +3,7 @@ package com.eactive.eai.rms.onl.apim.portalnotice;
|
||||
import com.eactive.apim.portal.djb.apistatus.incident.entity.DjbApistatusIncident;
|
||||
import com.eactive.apim.portal.djb.apistatus.incident.entity.DjbApistatusIncidentApi;
|
||||
import com.eactive.apim.portal.djb.apistatus.incident.entity.DjbApistatusIncidentApiId;
|
||||
import com.eactive.apim.portal.djb.apistatus.incident.entity.DjbApistatusIncidentTimeline;
|
||||
import com.eactive.apim.portal.djb.apistatus.incident.entity.IncidentKind;
|
||||
import com.eactive.apim.portal.djb.apistatus.incident.entity.IncidentState;
|
||||
import com.eactive.apim.portal.djb.apistatus.incident.repository.DjbApistatusIncidentApiRepository;
|
||||
@@ -36,6 +37,7 @@ import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@@ -90,12 +92,40 @@ public class PortalNoticeManService extends BaseService {
|
||||
|
||||
|
||||
public Page<PortalNoticeUI> selectList(Pageable pageable, PortalNoticeUISearch portalNoticeUISearch) {
|
||||
return portalNoticeService.findAll(pageable, portalNoticeUISearch)
|
||||
Page<PortalNoticeUI> page = portalNoticeService.findAll(pageable, portalNoticeUISearch)
|
||||
.map(portalNotice -> {
|
||||
PortalNoticeUI ui = portalNoticeUIMapper.toVo(portalNotice);
|
||||
setFileInfo(portalNotice, ui);
|
||||
return ui;
|
||||
});
|
||||
populateIncidentSummary(page.getContent());
|
||||
return page;
|
||||
}
|
||||
|
||||
/**
|
||||
* 목록에서 장애 진행 여부(종료 시각 미입력)를 표기하기 위해 incident 정보를 한 번에 붙인다.
|
||||
*/
|
||||
private void populateIncidentSummary(List<PortalNoticeUI> rows) {
|
||||
List<String> noticeIds = rows.stream()
|
||||
.filter(ui -> isIncidentKind(ui.getNoticeType()))
|
||||
.map(PortalNoticeUI::getId)
|
||||
.filter(StringUtils::isNotBlank)
|
||||
.collect(Collectors.toList());
|
||||
if (noticeIds.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Map<String, DjbApistatusIncident> byNoticeId = incidentRepository.findByNoticeIdIn(noticeIds).stream()
|
||||
.collect(Collectors.toMap(DjbApistatusIncident::getNoticeId, incident -> incident, (a, b) -> a));
|
||||
|
||||
for (PortalNoticeUI ui : rows) {
|
||||
DjbApistatusIncident incident = byNoticeId.get(ui.getId());
|
||||
if (incident == null) continue;
|
||||
ui.setIncidentId(incident.getIncidentId());
|
||||
ui.setStartedAt(incident.getStartedAt());
|
||||
ui.setEndAt(incident.getEndAt());
|
||||
ui.setState(incident.getState() == null ? null : incident.getState().name());
|
||||
}
|
||||
}
|
||||
|
||||
private void setFileInfo(PortalNotice portalNotice, PortalNoticeUI ui) {
|
||||
@@ -172,11 +202,17 @@ public class PortalNoticeManService extends BaseService {
|
||||
portalNoticeService.save(portalNotice);
|
||||
|
||||
if (isIncidentKind(portalNoticeUI.getNoticeType())) {
|
||||
persistIncident(portalNoticeUI, portalNotice, true);
|
||||
DjbApistatusIncident incident = persistIncident(portalNoticeUI, portalNotice, true);
|
||||
if (incident.getKind() == IncidentKind.INCIDENT) {
|
||||
// 최초 발생 항목을 남겨야 개발자포탈 장애 카드가 자동 탐지 건과 같은 모양으로 보인다.
|
||||
String body = StringUtils.defaultIfBlank(incident.getSummary(), incident.getTitle());
|
||||
writeTimeline(incident.getIncidentId(), incident.getStartedAt(),
|
||||
IncidentState.INVESTIGATING, body);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void persistIncident(PortalNoticeUI portalNoticeUI, PortalNotice portalNotice, boolean isCreate) {
|
||||
private DjbApistatusIncident persistIncident(PortalNoticeUI portalNoticeUI, PortalNotice portalNotice, boolean isCreate) {
|
||||
IncidentKind kind = toKind(portalNoticeUI.getNoticeType());
|
||||
|
||||
DjbApistatusIncident incident = incidentRepository.findByNoticeId(portalNotice.getId())
|
||||
@@ -185,6 +221,8 @@ public class PortalNoticeManService extends BaseService {
|
||||
incident.setKind(kind);
|
||||
incident.setTitle(portalNotice.getNoticeSubject());
|
||||
incident.setSummary(portalNoticeUI.getSummary());
|
||||
IncidentState stateBefore = incident.getState();
|
||||
LocalDateTime endAtBefore = incident.getEndAt();
|
||||
incident.setStartedAt(portalNoticeUI.getStartedAt() != null
|
||||
? portalNoticeUI.getStartedAt() : LocalDateTime.now());
|
||||
incident.setEndAt(portalNoticeUI.getEndAt());
|
||||
@@ -194,18 +232,43 @@ public class PortalNoticeManService extends BaseService {
|
||||
incident.setFixYn(StringUtils.defaultIfBlank(portalNotice.getFixYn(), "N"));
|
||||
|
||||
if (kind == IncidentKind.INCIDENT) {
|
||||
IncidentState newState = StringUtils.isNotBlank(portalNoticeUI.getState())
|
||||
? IncidentState.valueOf(portalNoticeUI.getState())
|
||||
: IncidentState.INVESTIGATING;
|
||||
if (!isCreate && incident.getState() != newState) {
|
||||
incident.setPreviousState(incident.getState());
|
||||
// 상태는 타임라인 게시로만 전이한다(화면에서 select 제거). 폼에 값이 없으면 현재 상태를 보존해야
|
||||
// 저장할 때마다 INVESTIGATING 으로 되돌아가는 회귀가 생기지 않는다.
|
||||
IncidentState newState;
|
||||
if (StringUtils.isNotBlank(portalNoticeUI.getState())) {
|
||||
newState = IncidentState.valueOf(portalNoticeUI.getState());
|
||||
} else {
|
||||
newState = stateBefore != null ? stateBefore : IncidentState.INVESTIGATING;
|
||||
}
|
||||
// 종결된 장애는 되돌릴 수 없다 — 재발은 새 장애로 등록한다 (appendTimeline 과 같은 규칙)
|
||||
if (!isCreate && isTerminal(stateBefore) && newState != stateBefore) {
|
||||
throw new IllegalArgumentException("이미 종결된 장애입니다. 상태를 변경할 수 없습니다.");
|
||||
}
|
||||
if (!isCreate && stateBefore != newState) {
|
||||
incident.setPreviousState(stateBefore);
|
||||
}
|
||||
incident.setState(newState);
|
||||
|
||||
// 종료 시각은 상태를 따른다. 비종결인데 종료 시각이 남으면 개발자포탈에서
|
||||
// '종료 시각이 있는데 진행 중'인 모순 카드가 된다.
|
||||
// 종결된 건은 실제 복구 시각으로 손보정할 수 있어야 하므로 폼 입력을 그대로 받는다.
|
||||
if (isTerminal(newState)) {
|
||||
if (incident.getEndAt() == null) {
|
||||
incident.setEndAt(endAtBefore != null ? endAtBefore : LocalDateTime.now());
|
||||
}
|
||||
} else {
|
||||
incident.setEndAt(null);
|
||||
}
|
||||
} else {
|
||||
incident.setState(null);
|
||||
incident.setPreviousState(null);
|
||||
}
|
||||
|
||||
// 장애·점검 공통 — 종료가 시작보다 빠른 구간은 가동률 집계를 음수로 만든다
|
||||
if (incident.getEndAt() != null && incident.getEndAt().isBefore(incident.getStartedAt())) {
|
||||
throw new IllegalArgumentException("종료 시각은 시작 시각보다 빠를 수 없습니다.");
|
||||
}
|
||||
|
||||
DjbApistatusIncident saved = incidentRepository.save(incident);
|
||||
|
||||
// 영향 API 동기화 — 단순 전체 삭제 후 재삽입
|
||||
@@ -225,6 +288,111 @@ public class PortalNoticeManService extends BaseService {
|
||||
incidentApiRepository.saveAll(rows);
|
||||
}
|
||||
}
|
||||
|
||||
return saved;
|
||||
}
|
||||
|
||||
// ───────────────────── 장애 처리 타임라인 ─────────────────────
|
||||
|
||||
private static final String AUTHOR_OPERATOR = "OPERATOR";
|
||||
|
||||
/** 종결 상태 — 이후 어떤 상태로도 전이할 수 없다. */
|
||||
private static boolean isTerminal(IncidentState state) {
|
||||
return state == IncidentState.RESOLVED || state == IncidentState.CANCELED;
|
||||
}
|
||||
|
||||
public List<IncidentTimelineUI> selectTimeline(Long incidentId) {
|
||||
if (incidentId == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
return incidentTimelineRepository.findByIncidentIdOrderByEventAtAsc(incidentId).stream()
|
||||
.map(entity -> {
|
||||
IncidentTimelineUI ui = new IncidentTimelineUI();
|
||||
ui.setTimelineId(entity.getTimelineId());
|
||||
ui.setIncidentId(entity.getIncidentId());
|
||||
ui.setEventAt(entity.getEventAt());
|
||||
ui.setStateAfter(entity.getStateAfter() == null ? null : entity.getStateAfter().name());
|
||||
ui.setBody(entity.getBody());
|
||||
ui.setAuthorType(entity.getAuthorType());
|
||||
ui.setVisibleYn(entity.getVisibleYn());
|
||||
return ui;
|
||||
}).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* 타임라인 게시. 상태 변경 여부와 무관하게 항상 새 행을 추가한다(append-only).
|
||||
* 상태 변경이 있으면 장애의 STATE/PREVIOUS_STATE 를 같은 트랜잭션에서 동기화하고,
|
||||
* 종결 상태(해소/취소)로 가면 종료 시각을 자동으로 채운다.
|
||||
*
|
||||
* @return 갱신된 상태/이전 상태/종료 시각 (화면 갱신용 — 상세를 다시 읽지 않기 위함)
|
||||
*/
|
||||
public PortalNoticeUI appendTimeline(Long incidentId, String body, boolean stateChange, String nextState) {
|
||||
if (StringUtils.isBlank(body)) {
|
||||
throw new IllegalArgumentException("타임라인 메세지를 입력하여 주십시오.");
|
||||
}
|
||||
DjbApistatusIncident incident = incidentRepository.findById(incidentId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("장애 정보를 찾을 수 없습니다. incidentId=" + incidentId));
|
||||
if (incident.getKind() != IncidentKind.INCIDENT) {
|
||||
// 점검은 STATE/타임라인을 쓰지 않는다 (ADR-F15)
|
||||
throw new IllegalArgumentException("장애 유형만 타임라인을 등록할 수 있습니다.");
|
||||
}
|
||||
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
IncidentState after = null;
|
||||
|
||||
if (stateChange) {
|
||||
if (StringUtils.isBlank(nextState)) {
|
||||
throw new IllegalArgumentException("변경할 상태를 선택하여 주십시오.");
|
||||
}
|
||||
after = IncidentState.valueOf(nextState);
|
||||
// 종결된 장애는 재전이 불가 — 재발은 새 장애로 등록한다 (상태 전이 설계 가드)
|
||||
if (isTerminal(incident.getState())) {
|
||||
throw new IllegalArgumentException("이미 종결된 장애입니다. 상태를 변경할 수 없습니다.");
|
||||
}
|
||||
if (incident.getState() == after) {
|
||||
throw new IllegalArgumentException("현재와 동일한 상태로는 변경할 수 없습니다.");
|
||||
}
|
||||
incident.setPreviousState(incident.getState());
|
||||
incident.setState(after);
|
||||
if (after == IncidentState.RESOLVED || after == IncidentState.CANCELED) {
|
||||
incident.setEndAt(now);
|
||||
}
|
||||
incidentRepository.save(incident);
|
||||
}
|
||||
|
||||
writeTimeline(incidentId, now, after, body);
|
||||
|
||||
PortalNoticeUI result = new PortalNoticeUI();
|
||||
result.setIncidentId(incident.getIncidentId());
|
||||
result.setState(incident.getState() == null ? null : incident.getState().name());
|
||||
result.setPreviousState(incident.getPreviousState() == null ? null : incident.getPreviousState().name());
|
||||
result.setEndAt(incident.getEndAt());
|
||||
return result;
|
||||
}
|
||||
|
||||
public void updateTimelineVisible(Long timelineId, String visibleYn) {
|
||||
DjbApistatusIncidentTimeline timeline = incidentTimelineRepository.findById(timelineId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("타임라인 정보를 찾을 수 없습니다. timelineId=" + timelineId));
|
||||
timeline.setVisibleYn("Y".equals(visibleYn) ? "Y" : "N");
|
||||
incidentTimelineRepository.save(timeline);
|
||||
}
|
||||
|
||||
public void deleteTimeline(Long timelineId) {
|
||||
incidentTimelineRepository.deleteById(timelineId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 감사 컬럼(CREATED_BY/CREATED_DATE)은 EMSAuditorAware 가 채운다 — 직접 세팅하지 않는다.
|
||||
*/
|
||||
private void writeTimeline(Long incidentId, LocalDateTime eventAt, IncidentState stateAfter, String body) {
|
||||
DjbApistatusIncidentTimeline timeline = new DjbApistatusIncidentTimeline();
|
||||
timeline.setIncidentId(incidentId);
|
||||
timeline.setEventAt(eventAt != null ? eventAt : LocalDateTime.now());
|
||||
timeline.setStateAfter(stateAfter);
|
||||
timeline.setBody(StringUtils.abbreviate(body, 4000));
|
||||
timeline.setAuthorType(AUTHOR_OPERATOR);
|
||||
timeline.setVisibleYn("Y");
|
||||
incidentTimelineRepository.save(timeline);
|
||||
}
|
||||
|
||||
private void handleFileUpload(PortalNoticeUI portalNoticeUI, PortalNotice portalNotice) throws IOException {
|
||||
|
||||
Reference in New Issue
Block a user