- IncidentTimelineUI VO 추가 - 장애 타임라인 화면 구현 지원
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled

- PortalNoticeManService 장애 상태 타임라인 로직 추가
- 메인 CSS에 타임라인 스타일 추가
This commit is contained in:
Rinjae
2026-08-04 13:10:31 +09:00
parent 07fbe1ba54
commit b45a7a162a
6 changed files with 231 additions and 1 deletions
@@ -1,5 +1,6 @@
package com.eactive.apim.portal.apps.community.notice.dto;
import com.eactive.apim.portal.djb.apistatus.dto.TimelineEntryDTO;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@@ -53,6 +54,12 @@ public class PortalNoticeDTO {
private String state;
private String previousState;
private List<IncidentAffectedApiDTO> affectedApis = Collections.emptyList();
/** 장애 처리 타임라인. 최신순(내림차순), 공개(visibleYn='Y') 항목만 담는다. */
private List<TimelineEntryDTO> timeline = Collections.emptyList();
public boolean hasTimeline() {
return timeline != null && !timeline.isEmpty();
}
public boolean isIncidentType() {
return NOTICE_TYPE_INCIDENT.equals(noticeType);
@@ -5,6 +5,7 @@ import com.eactive.apim.portal.apps.community.notice.dto.PortalNoticeDTO;
import com.eactive.apim.portal.apps.community.notice.dto.PortalNoticeSearch;
import com.eactive.apim.portal.apps.community.notice.mapper.PortalNoticeMapper;
import com.eactive.apim.portal.djb.apistatus.incident.entity.DjbApistatusIncident;
import com.eactive.apim.portal.djb.apistatus.service.ApiStatusAssembler;
import com.eactive.apim.portal.djb.apistatus.incident.repository.DjbApistatusIncidentApiRepository;
import com.eactive.apim.portal.djb.apistatus.incident.repository.DjbApistatusIncidentRepository;
import com.eactive.apim.portal.portalNotice.entity.PortalNotice;
@@ -29,6 +30,7 @@ public class PortalNoticeFacadeImpl implements PortalNoticeFacade {
private final PortalNoticeMapper portalNoticeMapper;
private final DjbApistatusIncidentRepository incidentRepository;
private final DjbApistatusIncidentApiRepository incidentApiRepository;
private final ApiStatusAssembler apiStatusAssembler;
@Override
public List<PortalNoticeDTO> getLatestNotices() {
@@ -67,11 +69,13 @@ public class PortalNoticeFacadeImpl implements PortalNoticeFacade {
private void populateIncident(PortalNoticeDTO dto) {
if (!dto.isIncidentOrMaintenance()) {
dto.setAffectedApis(Collections.emptyList());
dto.setTimeline(Collections.emptyList());
return;
}
Optional<DjbApistatusIncident> incidentOpt = incidentRepository.findByNoticeId(dto.getId());
if (!incidentOpt.isPresent()) {
dto.setAffectedApis(Collections.emptyList());
dto.setTimeline(Collections.emptyList());
return;
}
DjbApistatusIncident incident = incidentOpt.get();
@@ -86,5 +90,12 @@ public class PortalNoticeFacadeImpl implements PortalNoticeFacade {
.map(api -> new IncidentAffectedApiDTO(api.getApiId(), api.getApiName()))
.collect(Collectors.toList());
dto.setAffectedApis(apis);
// 장애·지연만 타임라인을 붙인다 (점검은 타임라인을 쌓지 않음 — ADR-F15)
boolean degrading = incident.getKind() != null && incident.getKind().isDegrading();
dto.setTimeline(degrading
? apiStatusAssembler.loadTimelines(Collections.singletonList(incident.getIncidentId()))
.getOrDefault(incident.getIncidentId(), Collections.emptyList())
: Collections.emptyList());
}
}
+90
View File
@@ -19748,6 +19748,94 @@ input[type=checkbox]:checked + .custom-checkbox {
margin-bottom: 8px;
}
.notice-detail-timeline {
padding-bottom: 40px;
border-bottom: 1px solid #eee;
margin-bottom: 48px;
}
.notice-detail-timeline .notice-timeline-title {
margin: 0 0 16px;
font-size: 18px;
font-weight: 700;
color: #000;
}
.notice-detail-timeline .timeline-table {
width: 100%;
border-collapse: collapse;
table-layout: fixed;
border-bottom: 1px solid #e3e8f0;
}
.notice-detail-timeline .timeline-table thead th {
background: #f9f9f9;
height: 43px;
font-weight: 500;
font-size: 14px;
text-align: center;
color: #000;
border-bottom: 1px solid #e3e8f0;
}
.notice-detail-timeline .timeline-table tbody tr {
border-bottom: 1px solid #e3e8f0;
}
.notice-detail-timeline .timeline-table td {
padding: 10px 16px;
font-size: 14px;
color: #000;
vertical-align: top;
}
.notice-detail-timeline .timeline-table .timeline-ts {
color: #555;
white-space: nowrap;
font-variant-numeric: tabular-nums;
}
.notice-detail-timeline .timeline-table .timeline-body {
line-height: 1.6;
white-space: pre-line;
word-break: break-all;
}
@media (max-width: 1024px) {
.notice-detail-timeline .timeline-table {
table-layout: auto;
}
.notice-detail-timeline .timeline-table colgroup {
display: none;
}
.notice-detail-timeline .timeline-table thead th,
.notice-detail-timeline .timeline-table td {
padding: 8px 10px;
font-size: 13px;
}
.notice-detail-timeline .timeline-table .timeline-ts {
white-space: normal;
}
}
.notice-detail-timeline .timeline-state {
display: inline-block;
padding: 2px 10px;
border-radius: 999px;
font-size: 12px;
font-weight: 500;
white-space: nowrap;
background: #eef1f5;
color: #4b5563;
}
.notice-detail-timeline .timeline-state.state-RESOLVED {
background: #e7f6ec;
color: #1b8c4a;
}
.notice-detail-timeline .timeline-state.state-MONITORING, .notice-detail-timeline .timeline-state.state-INVESTIGATING {
background: #fef3c7;
color: #c77800;
}
.notice-detail-timeline .timeline-state.state-IDENTIFIED {
background: #fdeee2;
color: #c95a0f;
}
.notice-detail-timeline .timeline-state.state-CANCELED {
background: #eef1f5;
color: #6b7280;
}
.notice-detail-actions {
display: flex;
justify-content: center;
@@ -28620,3 +28708,5 @@ input[type=checkbox]:checked + .custom-checkbox {
white-space: nowrap;
border: 0;
}
/*# sourceMappingURL=main.css.map */
File diff suppressed because one or more lines are too long
@@ -443,6 +443,96 @@
}
}
// 장애 처리 타임라인 — 상단 장애정보 표와 같은 규격의 간결한 표
.notice-detail-timeline {
padding-bottom: 40px;
border-bottom: 1px solid #eee;
margin-bottom: $spacing-3xl;
.notice-timeline-title {
margin: 0 0 16px;
font-size: 18px;
font-weight: 700;
color: #000;
}
.timeline-table {
width: 100%;
border-collapse: collapse;
table-layout: fixed;
border-bottom: 1px solid #e3e8f0;
thead th {
background: #f9f9f9;
height: 43px;
font-weight: 500;
font-size: 14px;
text-align: center;
color: #000;
border-bottom: 1px solid #e3e8f0;
}
tbody tr {
border-bottom: 1px solid #e3e8f0;
}
td {
padding: 10px 16px;
font-size: 14px;
color: #000;
vertical-align: top;
}
.timeline-ts {
color: #555;
white-space: nowrap;
font-variant-numeric: tabular-nums;
}
.timeline-body {
line-height: 1.6;
white-space: pre-line;
word-break: break-all;
}
@media (max-width: $breakpoint-md) {
table-layout: auto;
colgroup {
display: none;
}
thead th,
td {
padding: 8px 10px;
font-size: 13px;
}
.timeline-ts {
white-space: normal;
}
}
}
// 상태 배지 — API Status 타임라인 dot 색 규격과 동일
.timeline-state {
display: inline-block;
padding: 2px 10px;
border-radius: 999px;
font-size: 12px;
font-weight: 500;
white-space: nowrap;
background: #eef1f5;
color: #4b5563;
&.state-RESOLVED { background: #e7f6ec; color: #1b8c4a; }
&.state-MONITORING,
&.state-INVESTIGATING { background: #fef3c7; color: #c77800; }
&.state-IDENTIFIED { background: #fdeee2; color: #c95a0f; }
&.state-CANCELED { background: #eef1f5; color: #6b7280; }
}
}
// Notice Detail Actions
.notice-detail-actions {
display: flex;
@@ -141,6 +141,38 @@
</div>
</div>
<!-- 장애 처리 타임라인 (공개 항목이 있을 때만) -->
<div class="notice-detail-timeline" th:if="${portalNotice.hasTimeline()}">
<h3 class="notice-timeline-title">처리 경과</h3>
<table class="detail-table timeline-table">
<colgroup>
<col style="width: 160px;">
<col style="width: 110px;">
<col>
</colgroup>
<thead>
<tr>
<th>일시</th>
<th>상태</th>
<th>내용</th>
</tr>
</thead>
<tbody>
<tr th:each="entry : ${portalNotice.timeline}">
<td class="timeline-ts"
th:text="${entry.eventAt != null ? #temporals.format(entry.eventAt, 'yyyy-MM-dd HH:mm') : '-'}">
2026-08-04 11:44</td>
<td>
<span class="timeline-state"
th:classappend="${'state-' + (entry.stateAfter != null ? entry.stateAfter : 'NONE')}"
th:text="${entry.labelKo != null ? entry.labelKo : '진행 상황'}">원인 확인</span>
</td>
<td class="timeline-body" th:text="${entry.body}">진행 내용</td>
</tr>
</tbody>
</table>
</div>
<!-- Action Buttons -->
<div class="notice-detail-actions">
<button type="button" class="btn-action-primary btn-notice-list"