diff --git a/src/main/java/com/eactive/apim/portal/djb/apistatus/repository/ApiStatusIncidentQueryRepository.java b/src/main/java/com/eactive/apim/portal/djb/apistatus/repository/ApiStatusIncidentQueryRepository.java
index f3e900f..bc47090 100644
--- a/src/main/java/com/eactive/apim/portal/djb/apistatus/repository/ApiStatusIncidentQueryRepository.java
+++ b/src/main/java/com/eactive/apim/portal/djb/apistatus/repository/ApiStatusIncidentQueryRepository.java
@@ -15,31 +15,52 @@ import java.util.List;
import java.util.Optional;
/**
- * 개발자포탈 API Status 화면 전용 조회. 읽기 전용이며 공개 조건(공지 게시 + 초안 아님)을 항상 적용한다.
+ * 개발자포탈 API Status 화면 전용 조회. 읽기 전용이며 공개 조건을 항상 적용한다.
*
- *
PTL_NOTICE 와의 조인은 두 가지 역할을 한다.
- * (1) 관리자가 미게시(USE_YN='N')한 장애/점검을 숨긴다.
- * (2) 공지가 삭제된 고아 장애 행을 자연히 제외한다 (물리 FK 없음 - ADR-F10).
+ * 공개 조건({@link #VISIBLE})은 두 갈래다.
+ *
+ * - 공지가 붙은 이슈(장애·점검) - 그 공지가 게시(USE_YN='Y')되어 있어야 한다.
+ * 공지가 삭제된 고아 행도 이 조건에서 자연히 빠진다 (물리 FK 없음 - ADR-F10).
+ * - 공지가 없는 이슈(지연) - 자동 탐지 전용이라 검수할 공지가 없다.
+ * 초안(DRAFT_YN='Y')만 아니면 노출한다.
+ *
+ *
+ * 공지 조건을 EXISTS 로 쓰는 이유: 예전처럼 {@code FROM ... , PortalNotice n} 으로 조인하면
+ * NOTICE_ID 가 없는 지연 이슈가 행 자체에서 사라진다.
*/
public interface ApiStatusIncidentQueryRepository extends Repository {
- String VISIBLE = " i.noticeId = n.id AND n.useYn = 'Y' AND i.draftYn = 'N' ";
+ String VISIBLE = " i.draftYn = 'N'"
+ + " AND (i.noticeId IS NULL"
+ + " OR EXISTS (SELECT 1 FROM PortalNotice n"
+ + " WHERE n.id = i.noticeId AND n.useYn = 'Y')) ";
+
+ /** 종결 판정이 STATE 로 이뤄지는 종류 (장애·지연). JPQL 리터럴로 써야 해서 FQCN 을 쓴다 */
+ String KIND_INCIDENT = "com.eactive.apim.portal.djb.apistatus.incident.entity.IncidentKind.INCIDENT";
+ String KIND_DELAY = "com.eactive.apim.portal.djb.apistatus.incident.entity.IncidentKind.DELAY";
+ String KIND_MAINTENANCE = "com.eactive.apim.portal.djb.apistatus.incident.entity.IncidentKind.MAINTENANCE";
+
+ /** 종결 조건 - 장애·지연은 STATE 로, 점검은 종료 시각 경과로 판정한다 */
+ String CLOSED_CONDITION = " ((i.kind IN (" + KIND_INCIDENT + ", " + KIND_DELAY + ")"
+ + " AND i.state IN :closedStates)"
+ + " OR (i.kind = " + KIND_MAINTENANCE
+ + " AND i.endAt IS NOT NULL AND i.endAt < :now)) ";
/**
- * 진행 중 장애 (P3)
+ * 진행 중 이슈 (P3). 장애와 지연을 함께 본다.
*/
- @Query("SELECT i FROM DjbApistatusIncident i, PortalNotice n"
+ @Query("SELECT i FROM DjbApistatusIncident i"
+ " WHERE " + VISIBLE
- + " AND i.kind = :kind"
+ + " AND i.kind IN :kinds"
+ " AND i.state NOT IN :closedStates"
+ " ORDER BY i.startedAt DESC")
- List findVisibleOpenIncidents(@Param("kind") IncidentKind kind,
+ List findVisibleOpenIncidents(@Param("kinds") Collection kinds,
@Param("closedStates") Collection closedStates);
/**
* 예정/진행 중 점검 (P5). 종료 시각이 없거나 아직 지나지 않은 점검.
*/
- @Query("SELECT i FROM DjbApistatusIncident i, PortalNotice n"
+ @Query("SELECT i FROM DjbApistatusIncident i"
+ " WHERE " + VISIBLE
+ " AND i.kind = :kind"
+ " AND (i.endAt IS NULL OR i.endAt >= :now)"
@@ -48,21 +69,15 @@ public interface ApiStatusIncidentQueryRepository extends Repository findVisibleClosedIssues(@Param("closedStates") Collection closedStates,
@Param("now") LocalDateTime now,
Pageable pageable);
@@ -71,7 +86,7 @@ public interface ApiStatusIncidentQueryRepository extends Repository= :from)"
@@ -82,7 +97,7 @@ public interface ApiStatusIncidentQueryRepository extends Repository findVisibleById(@Param("incidentId") Long incidentId);
diff --git a/src/main/java/com/eactive/apim/portal/djb/apistatus/service/ApiCurrentStatusService.java b/src/main/java/com/eactive/apim/portal/djb/apistatus/service/ApiCurrentStatusService.java
index bc69dc2..6293cf0 100644
--- a/src/main/java/com/eactive/apim/portal/djb/apistatus/service/ApiCurrentStatusService.java
+++ b/src/main/java/com/eactive/apim/portal/djb/apistatus/service/ApiCurrentStatusService.java
@@ -149,10 +149,10 @@ public class ApiCurrentStatusService {
return names;
}
- /** 진행 중(미종결) 장애 중 API 별로 가장 심각한 한 건 */
+ /** 진행 중(미종결) 장애·지연 중 API 별로 가장 심각한 한 건 */
private Map mapOpenIncidents(Set apiIds) {
List openIncidents = incidentQueryRepository
- .findVisibleOpenIncidents(IncidentKind.INCIDENT, ApiStatusSupport.CLOSED_STATES);
+ .findVisibleOpenIncidents(IncidentKind.DEGRADING, ApiStatusSupport.CLOSED_STATES);
if (openIncidents.isEmpty()) {
return Collections.emptyMap();
}
@@ -204,12 +204,12 @@ public class ApiCurrentStatusService {
return byApi;
}
- /** 조회 기간 내 마지막 장애 발생 시각 */
+ /** 조회 기간 내 마지막 장애·지연 발생 시각 */
private Map collectLastIncidentAt(Set apiIds, LocalDateTime now, int windowDays) {
LocalDateTime windowStart = now.toLocalDate().minusDays(windowDays - 1L).atStartOfDay();
List incidents = incidentQueryRepository
.findVisibleOverlapping(windowStart, now.toLocalDate().plusDays(1).atStartOfDay()).stream()
- .filter(incident -> incident.getKind() == IncidentKind.INCIDENT)
+ .filter(incident -> incident.getKind() != null && incident.getKind().isDegrading())
.collect(Collectors.toList());
if (incidents.isEmpty()) {
return Collections.emptyMap();
@@ -233,14 +233,15 @@ public class ApiCurrentStatusService {
}
/**
- * 진행 중 장애 + 점검 여부로 현재 상태를 정한다.
+ * 진행 중 장애·지연 + 점검 여부로 현재 상태를 정한다.
*
- * 자동 탐지 지연(INTERFACE_ID {@code DELAY_START:}) 과 모니터링 단계는 장애가 아닌 지연으로 본다.
- * 이슈 이력 화면의 "지연/장애" 필터와 같은 기준이다.
+ * KIND 가 DELAY 면 지연이다. 장애(INCIDENT)라도 모니터링 단계면 이미 완화된 상태라
+ * 지연으로 표기한다 - 이슈 이력의 유형 필터(KIND 기준)와는 이 지점만 다르다.
*/
private String resolveStatus(DjbApistatusIncident open, boolean underMaintenance) {
if (open != null) {
- boolean degraded = open.getState() == IncidentState.MONITORING || ApiStatusSupport.isDelay(open);
+ boolean degraded = open.getKind() == IncidentKind.DELAY
+ || open.getState() == IncidentState.MONITORING;
return degraded ? ApiStatusSupport.STATUS_DEGRADED : ApiStatusSupport.STATUS_OUTAGE;
}
if (underMaintenance) {
@@ -250,14 +251,14 @@ public class ApiCurrentStatusService {
}
/**
- * 같은 API 에 열린 장애가 여럿일 때의 우선순위 (낮을수록 심각).
- * 지연으로 판정되는 건은 장애보다 뒤로 민다.
+ * 같은 API 에 열린 이슈가 여럿일 때의 우선순위 (낮을수록 심각).
+ * 지연은 장애보다 뒤로 민다.
*/
private int severity(DjbApistatusIncident incident) {
if (incident == null) {
return 99;
}
- int base = ApiStatusSupport.isDelay(incident) ? 10 : 0;
+ int base = incident.getKind() == IncidentKind.DELAY ? 10 : 0;
IncidentState state = incident.getState();
if (state == null) {
return base + 9;
diff --git a/src/main/java/com/eactive/apim/portal/djb/apistatus/service/ApiStatusAssembler.java b/src/main/java/com/eactive/apim/portal/djb/apistatus/service/ApiStatusAssembler.java
index 8ca2abc..896ae0d 100644
--- a/src/main/java/com/eactive/apim/portal/djb/apistatus/service/ApiStatusAssembler.java
+++ b/src/main/java/com/eactive/apim/portal/djb/apistatus/service/ApiStatusAssembler.java
@@ -125,7 +125,7 @@ public class ApiStatusAssembler {
}
/**
- * 지난 이슈 카드. 장애는 타임라인 전체를 붙이고 점검은 붙이지 않는다 (ADR-F15).
+ * 지난 이슈 카드. 장애·지연은 타임라인 전체를 붙이고 점검은 붙이지 않는다 (ADR-F15).
*/
public List toPastIssueCards(List incidents) {
if (incidents.isEmpty()) {
@@ -135,7 +135,7 @@ public class ApiStatusAssembler {
Map> apis = loadApis(ids);
List incidentKindIds = incidents.stream()
- .filter(incident -> incident.getKind() == IncidentKind.INCIDENT)
+ .filter(incident -> incident.getKind() != null && incident.getKind().isDegrading())
.map(DjbApistatusIncident::getIncidentId)
.collect(Collectors.toList());
Map> timelines = loadTimelines(incidentKindIds);
diff --git a/src/main/java/com/eactive/apim/portal/djb/apistatus/service/ApiStatusIssueHistoryService.java b/src/main/java/com/eactive/apim/portal/djb/apistatus/service/ApiStatusIssueHistoryService.java
index 4094ddd..3c5eb8e 100644
--- a/src/main/java/com/eactive/apim/portal/djb/apistatus/service/ApiStatusIssueHistoryService.java
+++ b/src/main/java/com/eactive/apim/portal/djb/apistatus/service/ApiStatusIssueHistoryService.java
@@ -30,9 +30,6 @@ import java.util.stream.Collectors;
@Transactional(readOnly = true)
public class ApiStatusIssueHistoryService {
- /** 지연은 IncidentKind 가 아니라 INCIDENT 의 하위 구분이므로 필터 값만 따로 둔다 */
- private static final String KIND_DELAY = "DELAY";
-
private final ApiStatusIncidentQueryRepository incidentQueryRepository;
private final ApiStatusAssembler assembler;
private final ApiStatusCatalogService catalogService;
@@ -85,19 +82,17 @@ public class ApiStatusIssueHistoryService {
* 인덱스바 1칸에 이슈 1건을 집계한다. 유형은 이슈 목록 필터와 같은 세 축(장애/지연/점검)으로 가른다.
*/
private void tally(IssueDateEntryDTO entry, DjbApistatusIncident incident) {
- String kind;
- if (incident.getKind() == IncidentKind.MAINTENANCE) {
- entry.setMntCount(entry.getMntCount() + 1);
- kind = IncidentKind.MAINTENANCE.name();
- } else if (ApiStatusSupport.isDelay(incident)) {
- entry.setDlyCount(entry.getDlyCount() + 1);
- kind = KIND_DELAY;
- } else {
- entry.setIncCount(entry.getIncCount() + 1);
- kind = IncidentKind.INCIDENT.name();
+ IncidentKind kind = incident.getKind();
+ if (kind == null) {
+ return;
}
- if (!entry.getKinds().contains(kind)) {
- entry.getKinds().add(kind);
+ switch (kind) {
+ case MAINTENANCE: entry.setMntCount(entry.getMntCount() + 1); break;
+ case DELAY: entry.setDlyCount(entry.getDlyCount() + 1); break;
+ default: entry.setIncCount(entry.getIncCount() + 1); break;
+ }
+ if (!entry.getKinds().contains(kind.name())) {
+ entry.getKinds().add(kind.name());
}
}
@@ -148,24 +143,14 @@ public class ApiStatusIssueHistoryService {
/**
* 유형 필터. 미지정/ALL/알 수 없는 값은 전체(null)로 본다.
- *
- * 지연(DELAY)은 별도 KIND 가 아니라 자동 탐지 시 INCIDENT 로 흡수된다(ADR-F12).
- * 자동 탐지 지연 건은 INTERFACE_ID 가 {@code DELAY_START:} 로 시작하므로 이를 기준으로 가른다.
- * "장애만" 은 지연을 제외한 INCIDENT 다.
+ * 필터 값은 {@link IncidentKind} 이름 그대로다 (INCIDENT / DELAY / MAINTENANCE).
*/
private java.util.function.Predicate kindFilter(String kind) {
if (StringUtils.isBlank(kind) || "ALL".equalsIgnoreCase(kind)) {
return null;
}
- if (KIND_DELAY.equalsIgnoreCase(kind)) {
- return ApiStatusSupport::isDelay;
- }
- if ("INCIDENT".equalsIgnoreCase(kind)) {
- return incident -> incident.getKind() == IncidentKind.INCIDENT
- && !ApiStatusSupport.isDelay(incident);
- }
try {
- IncidentKind selected = IncidentKind.valueOf(kind.toUpperCase());
+ IncidentKind selected = IncidentKind.valueOf(kind.trim().toUpperCase());
return incident -> incident.getKind() == selected;
} catch (IllegalArgumentException e) {
return null;
diff --git a/src/main/java/com/eactive/apim/portal/djb/apistatus/service/ApiStatusQueryService.java b/src/main/java/com/eactive/apim/portal/djb/apistatus/service/ApiStatusQueryService.java
index 7318d5a..7ac0314 100644
--- a/src/main/java/com/eactive/apim/portal/djb/apistatus/service/ApiStatusQueryService.java
+++ b/src/main/java/com/eactive/apim/portal/djb/apistatus/service/ApiStatusQueryService.java
@@ -26,11 +26,11 @@ public class ApiStatusQueryService {
private final ApiStatusIncidentQueryRepository incidentQueryRepository;
private final ApiStatusAssembler assembler;
- /** P3 - 진행 중 장애 */
+ /** P3 - 진행 중 장애·지연 */
public List getActiveIncidents() {
LocalDateTime now = ApiStatusSupport.now();
List incidents = incidentQueryRepository
- .findVisibleOpenIncidents(IncidentKind.INCIDENT, ApiStatusSupport.CLOSED_STATES);
+ .findVisibleOpenIncidents(IncidentKind.DEGRADING, ApiStatusSupport.CLOSED_STATES);
return assembler.toActiveIncidents(incidents, now);
}
diff --git a/src/main/java/com/eactive/apim/portal/djb/apistatus/service/ApiStatusSupport.java b/src/main/java/com/eactive/apim/portal/djb/apistatus/service/ApiStatusSupport.java
index a2f1bc1..bd9282d 100644
--- a/src/main/java/com/eactive/apim/portal/djb/apistatus/service/ApiStatusSupport.java
+++ b/src/main/java/com/eactive/apim/portal/djb/apistatus/service/ApiStatusSupport.java
@@ -31,13 +31,6 @@ public final class ApiStatusSupport {
public static final String STATUS_OUTAGE = "OUTAGE";
public static final String STATUS_MAINTENANCE = "MAINTENANCE";
- /**
- * 자동 탐지 지연 건의 INTERFACE_ID prefix (admin ApiStatusDetectionService 가 event 명으로 생성).
- *
- * 지연(DELAY)은 별도 KIND 가 아니라 INCIDENT 로 흡수되므로(ADR-F12) 이 prefix 로 가른다.
- */
- public static final String DELAY_INTERFACE_PREFIX = "DELAY_START:";
-
private ApiStatusSupport() {
}
@@ -45,12 +38,22 @@ public final class ApiStatusSupport {
return LocalDateTime.now(ZONE);
}
- /** 자동 탐지된 지연 건인지 여부 (장애와 구분) */
+ /** 지연 건인지 여부 (장애와 구분) */
public static boolean isDelay(DjbApistatusIncident incident) {
- return incident != null
- && incident.getKind() == IncidentKind.INCIDENT
- && incident.getInterfaceId() != null
- && incident.getInterfaceId().startsWith(DELAY_INTERFACE_PREFIX);
+ return incident != null && incident.getKind() == IncidentKind.DELAY;
+ }
+
+ /** 이슈 종류에 대응하는 현재 상태 코드 */
+ public static String statusOf(IncidentKind kind) {
+ if (kind == null) {
+ return STATUS_NORMAL;
+ }
+ switch (kind) {
+ case INCIDENT: return STATUS_OUTAGE;
+ case DELAY: return STATUS_DEGRADED;
+ case MAINTENANCE: return STATUS_MAINTENANCE;
+ default: return STATUS_NORMAL;
+ }
}
/**
diff --git a/src/main/java/com/eactive/apim/portal/djb/apistatus/service/ApiStatusUptimeService.java b/src/main/java/com/eactive/apim/portal/djb/apistatus/service/ApiStatusUptimeService.java
index 541242b..c51cf68 100644
--- a/src/main/java/com/eactive/apim/portal/djb/apistatus/service/ApiStatusUptimeService.java
+++ b/src/main/java/com/eactive/apim/portal/djb/apistatus/service/ApiStatusUptimeService.java
@@ -64,9 +64,10 @@ public class ApiStatusUptimeService {
LocalDateTime now = ApiStatusSupport.now();
LocalDateTime windowStart = now.toLocalDate().minusDays(windowDays - 1L).atStartOfDay();
+ // 장애와 지연 모두 서비스 저하이므로 가동률에서 차감한다. 점검은 계획된 작업이라 제외.
List incidents = incidentQueryRepository
.findVisibleOverlapping(windowStart, now.toLocalDate().plusDays(1).atStartOfDay()).stream()
- .filter(incident -> incident.getKind() == IncidentKind.INCIDENT)
+ .filter(incident -> incident.getKind() != null && incident.getKind().isDegrading())
.collect(Collectors.toList());
Map> intervalsByApi = new HashMap<>();
@@ -135,7 +136,7 @@ public class ApiStatusUptimeService {
maintenanceIntervals.add(interval);
} else {
incidentIntervals.add(interval);
- if (ApiStatusSupport.isDelay(incident)) {
+ if (incident.getKind() == IncidentKind.DELAY) {
delayIntervals.add(interval);
} else {
outageIntervals.add(interval);
diff --git a/src/main/resources/static/css/main.css b/src/main/resources/static/css/main.css
index 52dc713..55f9f7d 100644
--- a/src/main/resources/static/css/main.css
+++ b/src/main/resources/static/css/main.css
@@ -27221,6 +27221,9 @@ input[type=checkbox]:checked + .custom-checkbox {
.api-status .as-issue-card.is-incident {
border-left: 4px solid var(--as-err);
}
+.api-status .as-issue-card.is-degraded {
+ border-left: 4px solid var(--as-warn);
+}
.api-status .as-issue-card.is-maintenance {
border-left: 4px solid var(--as-info);
}
@@ -27243,6 +27246,9 @@ input[type=checkbox]:checked + .custom-checkbox {
.api-status .as-issue-card.is-incident .as-issue-title {
color: var(--as-err);
}
+.api-status .as-issue-card.is-degraded .as-issue-title {
+ color: var(--as-warn);
+}
.api-status .as-issue-card.is-maintenance .as-issue-title {
color: var(--as-info);
}
diff --git a/src/main/resources/static/js/djb/api-status-issues.js b/src/main/resources/static/js/djb/api-status-issues.js
index 2e16545..5b5d1dd 100644
--- a/src/main/resources/static/js/djb/api-status-issues.js
+++ b/src/main/resources/static/js/djb/api-status-issues.js
@@ -11,7 +11,8 @@
/** 오픈 API 목록에 노출되는 API (링크 가능 대상) */
const linkableApiIds = new Set();
- const KIND_LABEL = { INCIDENT: '장애', MAINTENANCE: '점검' };
+ const KIND_LABEL = { INCIDENT: '장애', DELAY: '지연', MAINTENANCE: '점검' };
+ const KIND_CLASS = { INCIDENT: 'is-incident', DELAY: 'is-degraded', MAINTENANCE: 'is-maintenance' };
const PAGE_SIZE = 10;
const API_LIST_LIMIT = 50; // 라이브서치 결과 표시 상한
@@ -191,7 +192,7 @@
}
function issueCardHtml(issue) {
- const kindClass = issue.kind === 'MAINTENANCE' ? 'is-maintenance' : 'is-incident';
+ const kindClass = KIND_CLASS[issue.kind] || 'is-incident';
let inner;
if (issue.kind === 'MAINTENANCE') {
diff --git a/src/main/resources/static/js/djb/api-status.js b/src/main/resources/static/js/djb/api-status.js
index ea8db63..33de027 100644
--- a/src/main/resources/static/js/djb/api-status.js
+++ b/src/main/resources/static/js/djb/api-status.js
@@ -12,7 +12,8 @@
/** 오픈 API 목록에 노출되는 API (링크 가능 대상) */
const linkableApiIds = new Set();
- const KIND_LABEL = { INCIDENT: '장애', MAINTENANCE: '점검' };
+ const KIND_LABEL = { INCIDENT: '장애', DELAY: '지연', MAINTENANCE: '점검' };
+ const KIND_CLASS = { INCIDENT: 'is-incident', DELAY: 'is-degraded', MAINTENANCE: 'is-maintenance' };
/** 진행 중 장애 카드 노출 상한. 초과분은 전체 이력 링크로 넘긴다. */
const ACTIVE_INCIDENT_LIMIT = 1;
@@ -262,8 +263,8 @@
// ---------------- ❺ 지난 이슈 사항 ----------------
function issueCardHtml(issue) {
- const kindClass = issue.kind === 'MAINTENANCE' ? 'is-maintenance' : 'is-incident';
- const kindBadge = issue.kind === 'MAINTENANCE' ? 'is-maintenance' : 'is-incident';
+ const kindClass = KIND_CLASS[issue.kind] || 'is-incident';
+ const kindBadge = kindClass;
let inner;
if (issue.kind === 'MAINTENANCE') {
diff --git a/src/main/resources/static/sass/pages/_api-status.scss b/src/main/resources/static/sass/pages/_api-status.scss
index abfb01d..603638a 100644
--- a/src/main/resources/static/sass/pages/_api-status.scss
+++ b/src/main/resources/static/sass/pages/_api-status.scss
@@ -495,6 +495,7 @@
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.05);
&.is-incident { border-left: 4px solid var(--as-err); }
+ &.is-degraded { border-left: 4px solid var(--as-warn); }
&.is-maintenance { border-left: 4px solid var(--as-info); }
.as-issue-meta-row {
@@ -516,6 +517,7 @@
}
&.is-incident .as-issue-title { color: var(--as-err); }
+ &.is-degraded .as-issue-title { color: var(--as-warn); }
&.is-maintenance .as-issue-title { color: var(--as-info); }
}