- IncidentTimelineUI VO 추가 - 장애 타임라인 화면 구현 지원
- PortalNoticeManService 장애 상태 타임라인 로직 추가 - 메인 CSS에 타임라인 스타일 추가
This commit is contained in:
@@ -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);
|
||||
|
||||
+11
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user