From cd8f826ba525c2963f2b0c986437c2b1ff1b6165 Mon Sep 17 00:00:00 2001
From: Rinjae
Date: Mon, 3 Aug 2026 17:03:35 +0900
Subject: [PATCH] =?UTF-8?q?=EC=9E=90=EB=8F=99=20=EC=9E=A5=EC=95=A0=20?=
=?UTF-8?q?=ED=83=90=EC=A7=80=20=EB=B0=8F=20=EA=B3=B5=EC=A7=80=20=EC=B2=98?=
=?UTF-8?q?=EB=A6=AC=20=EA=B0=9C=EC=84=A0=20-=20=EC=A0=90=EA=B2=80=20?=
=?UTF-8?q?=ED=8C=90=EC=A0=95=20=EB=A1=9C=EC=A7=81=EC=97=90=20KIND=3D'MAIN?=
=?UTF-8?q?TENANCE'=20=EC=A1=B0=EA=B1=B4=20=EC=B6=94=EA=B0=80=20-=20?=
=?UTF-8?q?=EC=9E=90=EB=8F=99=20=EB=93=B1=EB=A1=9D=EB=AA=A8=EB=93=9C=20OFF?=
=?UTF-8?q?/DRAFT/PUBLISH=20=EC=A7=80=EC=9B=90=20=EB=B0=8F=20=EA=B8=B0?=
=?UTF-8?q?=EB=B3=B8=EA=B0=92=20=EC=A0=81=EC=9A=A9=20-=20=EC=9E=A5?=
=?UTF-8?q?=EC=95=A0/=EC=A7=80=EC=97=B0=20=EB=B3=91=ED=95=A9=20=EB=B0=8F?=
=?UTF-8?q?=20=EC=9E=90=EB=8F=99=20=EA=B3=B5=EC=A7=80=20=ED=9B=84=20?=
=?UTF-8?q?=EA=B2=8C=EC=8B=9C=20=EC=83=81=ED=83=9C=20=EB=8F=99=EA=B8=B0?=
=?UTF-8?q?=ED=99=94?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../apistatus/ApiStatusDetectionService.java | 250 +++++++++++++++---
.../djb/apistatus/ApiStatusRepository.java | 3 +
2 files changed, 223 insertions(+), 30 deletions(-)
diff --git a/src/main/java/com/eactive/eai/rms/ext/djb/apistatus/ApiStatusDetectionService.java b/src/main/java/com/eactive/eai/rms/ext/djb/apistatus/ApiStatusDetectionService.java
index 636f412..ad25d18 100644
--- a/src/main/java/com/eactive/eai/rms/ext/djb/apistatus/ApiStatusDetectionService.java
+++ b/src/main/java/com/eactive/eai/rms/ext/djb/apistatus/ApiStatusDetectionService.java
@@ -32,6 +32,7 @@ import com.eactive.apim.portal.djb.apistatus.incident.repository.DjbApistatusInc
import com.eactive.apim.portal.djb.apistatus.incident.repository.DjbApistatusIncidentRepository;
import com.eactive.apim.portal.djb.apistatus.incident.repository.DjbApistatusIncidentTimelineRepository;
import com.eactive.apim.portal.portalNotice.entity.PortalNotice;
+import com.eactive.apim.portal.portalproperty.service.PortalPropertyService;
import com.eactive.eai.rms.data.entity.onl.apim.portalnotice.PortalNoticeService;
import com.eactive.eai.rms.data.entity.onl.djb.apistatus.ApiNameRow;
@@ -39,7 +40,13 @@ import com.eactive.eai.rms.data.entity.onl.djb.apistatus.ApiNameRow;
* API 상태 탐지 결과를 개발자포탈 API Status 화면용 장애 데이터로 반영한다.
*
* 탐지 자체는 {@link ApiStatusService} 가 수행하며, 본 서비스는 그 결과를
- * {@code DJB_APISTATUS_INCIDENT} / {@code _API} / {@code _TIMELINE} + {@code PTL_NOTICE} 초안으로 기록한다.
+ * {@code DJB_APISTATUS_INCIDENT} / {@code _API} / {@code _TIMELINE} + {@code PTL_NOTICE} 로 기록한다.
+ *
+ * 자동 등록 여부와 공지 게시 시점은 PTL_PROPERTY
+ * ({@code Portal} / {@code djb.apistatus.auto-incident-mode}) 로 정한다.
+ * {@code OFF}면 자동 등록을 하지 않고, {@code DRAFT}(기본)면 관리자가 공지사항 관리에서
+ * 검수·게시해야 개발자포탈에 노출되며, {@code PUBLISH}면 탐지 즉시 노출된다.
+ * {@code OFF} 여도 이미 열려있는 장애의 복구 처리(ERROR_END/DELAY_END)는 계속 동작한다.
*
* 점검(CONTROL_*) 은 관리자가 공지사항으로 등록한 점검 일정이 원천이다
* ({@code ApiStatusRepository.findApiStatusEvents} 가 INCIDENT 기간을 읽어 CTRL_YN 을 판정).
@@ -70,6 +77,24 @@ public class ApiStatusDetectionService {
private static final String NOTICE_TYPE_INCIDENT = "3";
private static final int NOTICE_SUBJECT_MAX_LENGTH = 255;
+ /** 자동 탐지 공지 처리 방식 프로퍼티 그룹 (개발자포탈과 동일 그룹을 쓴다) */
+ public static final String PROPERTY_GROUP = "Portal";
+
+ /** 자동 탐지 장애/지연의 공지 처리 방식 - OFF / DRAFT / PUBLISH */
+ public static final String KEY_AUTO_INCIDENT_MODE = "djb.apistatus.auto-incident-mode";
+
+ /** 자동 등록 안 함. 신규 등록·병합 모두 하지 않는다 (복구 처리는 계속 동작) */
+ public static final String MODE_OFF = "OFF";
+
+ /** 초안만 생성. 관리자가 공지사항 관리에서 검수·게시해야 개발자포탈에 노출된다 */
+ public static final String MODE_DRAFT = "DRAFT";
+
+ /** 초안 생성 후 곧바로 게시. 검수 없이 개발자포탈에 즉시 노출된다 */
+ public static final String MODE_PUBLISH = "PUBLISH";
+
+ /** 오탐이 그대로 고객에게 노출되지 않도록 기본은 검수(DRAFT) */
+ private static final String DEFAULT_AUTO_INCIDENT_MODE = MODE_DRAFT;
+
@Autowired
private DjbApistatusIncidentRepository incidentRepository;
@@ -85,6 +110,9 @@ public class ApiStatusDetectionService {
@Autowired
private ApiStatusRepository apiStatusRepository;
+ @Autowired
+ private PortalPropertyService portalPropertyService;
+
/**
* 이벤트 감지
* @param event "CONTROL_START" : 점검시작
@@ -113,10 +141,10 @@ public class ApiStatusDetectionService {
switch (event) {
case EVENT_ERROR_START:
- openIncident(event, targets, "에러율 임계 초과", "장애");
+ openIncident(event, IncidentKind.INCIDENT, targets, "에러율 임계 초과", "장애");
break;
case EVENT_DELAY_START:
- openIncident(event, targets, "응답시간 임계 초과", "응답지연");
+ openIncident(event, IncidentKind.DELAY, targets, "응답시간 임계 초과", "응답지연");
break;
case EVENT_ERROR_END:
case EVENT_DELAY_END:
@@ -133,12 +161,12 @@ public class ApiStatusDetectionService {
}
/**
- * 미종결 장애로 남아있는 API 목록 (탐지 Job 폴링용)
+ * 미종결 장애·지연으로 남아있는 API 목록 (탐지 Job 폴링용)
*/
@Transactional(transactionManager = "transactionManagerForEMS", readOnly = true)
public List remainDownApiIds() {
- List openIncidents =
- incidentRepository.findByKindAndStateNotInOrderByStartedAtDesc(IncidentKind.INCIDENT, CLOSED_STATES);
+ List openIncidents = incidentRepository
+ .findByKindInAndStateNotInOrderByStartedAtDesc(IncidentKind.DEGRADING, CLOSED_STATES);
if (openIncidents.isEmpty()) {
return Collections.emptyList();
}
@@ -157,7 +185,20 @@ public class ApiStatusDetectionService {
// ────────────── 장애 발생 ──────────────
- private void openIncident(String event, List apiIds, String summary, String titleKeyword) {
+ /**
+ * 신규 이슈 등록. 지연(DELAY)은 공지를 만들지 않고, 장애(INCIDENT)만 공지 초안/게시를 동반한다.
+ */
+ private void openIncident(String event, IncidentKind kind, List apiIds,
+ String summary, String titleKeyword) {
+ // 0) OFF 면 신규 등록·병합·타임라인 기록을 모두 건너뛴다.
+ // 이미 열려있는 이슈의 복구(markRecovered)는 계속 동작해야 하므로 여기서만 막는다.
+ String mode = resolveAutoIncidentMode();
+ if (MODE_OFF.equals(mode)) {
+ log.info("자동 이슈 등록 비활성(OFF) - 기록하지 않음: {} {}", event, apiIds);
+ return;
+ }
+ boolean autoPublish = MODE_PUBLISH.equals(mode);
+
LocalDateTime now = LocalDateTime.now();
String interfaceId = makeInterfaceId(event, apiIds, now);
@@ -169,31 +210,34 @@ public class ApiStatusDetectionService {
return;
}
- // 2) 이미 열려있는 자동 등록 장애가 해당 API 를 포함하면 영향 API 만 병합
+ // 2) 이미 열려있는 자동 등록 이슈가 해당 API 를 포함하면 영향 API 만 병합
// (관리자가 직접 작성한 장애 공지는 건드리지 않는다)
+ // 지연으로 열린 건에 장애가 얹히는 경우가 있어 지연·장애를 함께 찾는다.
List openIncidents = incidentRepository
- .findOpenByApiIds(IncidentKind.INCIDENT, DETECTED_BY_AUTO, CLOSED_STATES, apiIds);
+ .findOpenByApiIds(IncidentKind.DEGRADING, DETECTED_BY_AUTO, CLOSED_STATES, apiIds);
if (!openIncidents.isEmpty()) {
- mergeIntoOpenIncident(openIncidents.get(0), event, apiIds, now);
+ mergeIntoOpenIncident(openIncidents.get(0), event, kind, apiIds, now, autoPublish);
return;
}
- // 3) 신규 장애 + 공지 초안 생성
+ // 3) 신규 이슈 생성. 장애는 공지도 같이 만들고, PUBLISH 면 검수 없이 바로 게시한다.
Map apiNames = resolveApiNames(apiIds);
String title = buildTitle(titleKeyword, apiIds, apiNames);
- PortalNotice notice = createDraftNotice(title, summary, apiIds, apiNames, now);
+ PortalNotice notice = kind == IncidentKind.DELAY
+ ? null : createNotice(title, summary, apiIds, apiNames, now, autoPublish);
DjbApistatusIncident incident = new DjbApistatusIncident();
- incident.setKind(IncidentKind.INCIDENT);
+ incident.setKind(kind);
incident.setState(IncidentState.INVESTIGATING);
incident.setTitle(title);
incident.setSummary(summary);
incident.setStartedAt(now);
incident.setDetectedBy(DETECTED_BY_AUTO);
incident.setInterfaceId(interfaceId);
- incident.setNoticeId(notice.getId());
- incident.setDraftYn("Y");
+ incident.setNoticeId(notice == null ? null : notice.getId());
+ // 개발자포탈 노출 조건: 공지가 있으면 DRAFT_YN='N' + USE_YN='Y', 없으면(지연) DRAFT_YN='N' 만
+ incident.setDraftYn(autoPublish ? "N" : "Y");
incident.setFixYn("N");
stampAudit(incident, now);
@@ -201,9 +245,11 @@ public class ApiStatusDetectionService {
try {
saved = incidentRepository.saveAndFlush(incident);
} catch (DataIntegrityViolationException e) {
- // INTERFACE_ID UNIQUE 충돌 - 동시 탐지. 공지 초안 정리 후 타임라인만 남긴다
- log.info("장애 중복 감지 (interfaceId={}) - 기존 장애에 타임라인 추가", interfaceId, e);
- portalNoticeService.deleteById(notice.getId());
+ // INTERFACE_ID UNIQUE 충돌 - 동시 탐지. 만든 공지를 정리하고 타임라인만 남긴다
+ log.info("이슈 중복 감지 (interfaceId={}) - 기존 이슈에 타임라인 추가", interfaceId, e);
+ if (notice != null) {
+ portalNoticeService.deleteById(notice.getId());
+ }
incidentRepository.findByInterfaceId(interfaceId).ifPresent(existing ->
appendTimeline(existing.getIncidentId(), null,
"재탐지 신호 수신 (" + event + ")", now));
@@ -214,14 +260,62 @@ public class ApiStatusDetectionService {
appendTimeline(saved.getIncidentId(), IncidentState.INVESTIGATING,
summary + " 자동 감지 (" + event + ")\n영향 API: " + String.join(", ", apiIds), now);
- log.info("자동 장애 등록: incidentId={}, noticeId={}, apis={}",
- saved.getIncidentId(), notice.getId(), apiIds);
+ log.info("자동 이슈 등록: incidentId={}, kind={}, noticeId={}, mode={}, apis={}",
+ saved.getIncidentId(), kind, notice == null ? "-" : notice.getId(),
+ autoPublish ? MODE_PUBLISH : MODE_DRAFT, apiIds);
}
- private void mergeIntoOpenIncident(DjbApistatusIncident incident, String event,
- List apiIds, LocalDateTime now) {
+ /**
+ * 자동 탐지 장애/지연의 공지 처리 방식.
+ *
+ * PTL_PROPERTY({@code Portal} / {@code djb.apistatus.auto-incident-mode}) 를 읽는다.
+ * 최초 접근 시 기본값으로 row 가 자동 생성된다.
+ *
+ *
+ * - {@code OFF} - 자동 등록 안 함 (복구 처리는 계속)
+ * - {@code DRAFT}(기본) - 초안만 생성, 관리자 검수 후 게시
+ * - {@code PUBLISH} - 초안 생성 후 즉시 게시
+ *
+ *
+ * 조회 실패나 알 수 없는 값은 검수 쪽(DRAFT)으로 떨어뜨린다 - 오탐이 고객 화면에
+ * 그대로 나가는 것도, 진짜 장애를 아예 안 남기는 것도 기본 동작으로는 위험하기 때문이다.
+ */
+ private String resolveAutoIncidentMode() {
+ try {
+ String value = portalPropertyService.getOrCreateProperty(
+ PROPERTY_GROUP, KEY_AUTO_INCIDENT_MODE, DEFAULT_AUTO_INCIDENT_MODE,
+ "API 상태 자동 탐지 공지 처리 방식 - OFF: 자동 등록 안 함, "
+ + "DRAFT: 초안만 생성(관리자 검수 후 게시), PUBLISH: 초안 생성 후 즉시 게시");
+ String normalized = StringUtils.trimToEmpty(value);
+ if (MODE_OFF.equalsIgnoreCase(normalized)) {
+ return MODE_OFF;
+ }
+ if (MODE_PUBLISH.equalsIgnoreCase(normalized)) {
+ return MODE_PUBLISH;
+ }
+ if (!MODE_DRAFT.equalsIgnoreCase(normalized)) {
+ log.warn("알 수 없는 자동 탐지 공지 처리 방식 '{}' - 기본값 {} 적용", value, DEFAULT_AUTO_INCIDENT_MODE);
+ }
+ return MODE_DRAFT;
+ } catch (Exception e) {
+ log.warn("자동 탐지 공지 처리 방식 조회 실패 - 기본값 {} 적용", DEFAULT_AUTO_INCIDENT_MODE, e);
+ return DEFAULT_AUTO_INCIDENT_MODE;
+ }
+ }
+
+ private void mergeIntoOpenIncident(DjbApistatusIncident incident, String event, IncidentKind kind,
+ List apiIds, LocalDateTime now, boolean autoPublish) {
Long incidentId = incident.getIncidentId();
+ // 지연으로 열린 건에 장애가 얹히면 종류를 올린다 (강등은 하지 않는다)
+ escalateIfNeeded(incident, event, kind, now, autoPublish);
+
+ // DRAFT 로 만들어진 뒤 PUBLISH 로 설정이 바뀐 경우, 이 이슈가 닫힐 때까지 모든 탐지가
+ // 여기로 병합돼 아무것도 게시되지 않는다. 병합 시점에 게시 상태를 맞춰준다.
+ if (autoPublish) {
+ publishIfPending(incident);
+ }
+
Set mapped = incidentApiRepository.findByIncidentIdOrderByApiId(incidentId).stream()
.map(DjbApistatusIncidentApi::getApiId)
.collect(Collectors.toCollection(HashSet::new));
@@ -232,14 +326,81 @@ public class ApiStatusDetectionService {
if (added.isEmpty()) {
appendTimeline(incidentId, null, "재탐지 신호 수신 (" + event + ")", now);
- log.debug("기존 장애에 이미 포함된 API - 타임라인만 추가: incidentId={}, apis={}", incidentId, apiIds);
+ log.debug("기존 이슈에 이미 포함된 API - 타임라인만 추가: incidentId={}, apis={}", incidentId, apiIds);
return;
}
saveIncidentApis(incidentId, added, resolveApiNames(added), now);
appendTimeline(incidentId, null,
"영향 API 추가 감지 (" + event + ")\n" + String.join(", ", added), now);
- log.info("기존 장애에 영향 API 병합: incidentId={}, addedApis={}", incidentId, added);
+ log.info("기존 이슈에 영향 API 병합: incidentId={}, addedApis={}", incidentId, added);
+ }
+
+ /**
+ * 지연(DELAY)으로 열린 이슈에 장애(ERROR_START)가 감지되면 종류를 장애로 올린다.
+ *
+ * 지연은 공지가 없으므로 격상 시점에 공지를 새로 만들어 붙인다. 반대 방향(장애→지연)은
+ * 하지 않는다 - 한 번 장애로 인지된 구간을 나중에 완화해서 표기하면 이력이 왜곡된다.
+ */
+ private void escalateIfNeeded(DjbApistatusIncident incident, String event, IncidentKind kind,
+ LocalDateTime now, boolean autoPublish) {
+ if (kind != IncidentKind.INCIDENT || incident.getKind() != IncidentKind.DELAY) {
+ return;
+ }
+
+ List apiIds = incidentApiRepository.findByIncidentIdOrderByApiId(incident.getIncidentId()).stream()
+ .map(DjbApistatusIncidentApi::getApiId)
+ .collect(Collectors.toList());
+ Map apiNames = resolveApiNames(apiIds);
+ String title = buildTitle("장애", apiIds, apiNames);
+ String summary = "에러율 임계 초과";
+
+ incident.setKind(IncidentKind.INCIDENT);
+ incident.setTitle(title);
+ incident.setSummary(summary);
+ if (incident.getNoticeId() == null) {
+ // 이미 노출 중이던(DRAFT_YN='N') 지연이 격상되는데 공지가 미게시로 붙으면
+ // 보이던 이슈가 사라진다. 그 경우엔 공지도 게시 상태로 만든다.
+ boolean publishNotice = autoPublish || "N".equals(incident.getDraftYn());
+ PortalNotice notice = createNotice(title, summary, apiIds, apiNames, now, publishNotice);
+ incident.setNoticeId(notice.getId());
+ }
+ incident.setLastModifiedBy(AUTHOR_SYSTEM);
+ incident.setLastModifiedDate(now);
+ incidentRepository.save(incident);
+
+ appendTimeline(incident.getIncidentId(), null,
+ "지연에서 장애로 격상 (" + event + ")", now);
+ log.info("지연 → 장애 격상: incidentId={}, noticeId={}",
+ incident.getIncidentId(), incident.getNoticeId());
+ }
+
+ /**
+ * 아직 초안으로 남아있는 자동 탐지 이슈를 게시 상태로 올린다 (PUBLISH 모드에서만 호출).
+ *
+ * 대상은 자동 등록(DETECTED_BY='AUTO') 건뿐이다. 관리자가 공지사항 관리에서 손댄 건은
+ * DETECTED_BY 가 'MANUAL' 로 바뀌므로 여기 오지 않는다.
+ *
+ * 지연은 연결 공지가 없으므로 DRAFT_YN 만 바꾸면 노출된다.
+ */
+ private void publishIfPending(DjbApistatusIncident incident) {
+ if (!"Y".equals(incident.getDraftYn())) {
+ return;
+ }
+
+ incident.setDraftYn("N");
+ incidentRepository.save(incident);
+
+ if (StringUtils.isNotBlank(incident.getNoticeId())) {
+ portalNoticeService.findById(incident.getNoticeId())
+ .filter(notice -> !"Y".equals(notice.getUseYn()))
+ .ifPresent(notice -> {
+ notice.setUseYn("Y");
+ portalNoticeService.save(notice);
+ });
+ }
+ log.info("자동 탐지 이슈 뒤늦은 게시: incidentId={}, noticeId={}",
+ incident.getIncidentId(), incident.getNoticeId());
}
// ────────────── 복구 ──────────────
@@ -254,8 +415,10 @@ public class ApiStatusDetectionService {
return;
}
+ // 복구는 이벤트 종류를 구분하지 않는다. ERROR_END 하나로 그 API 의 지연 건까지 닫는다
+ // (상태머신상 E → N 으로 돌아가므로 DELAY_END 가 따로 오지 않는다).
Map openIncidents = incidentRepository
- .findByKindAndStateNotInOrderByStartedAtDesc(IncidentKind.INCIDENT, CLOSED_STATES).stream()
+ .findByKindInAndStateNotInOrderByStartedAtDesc(IncidentKind.DEGRADING, CLOSED_STATES).stream()
.collect(Collectors.toMap(DjbApistatusIncident::getIncidentId, incident -> incident));
Map> recoveredByIncident = new HashMap<>();
@@ -345,22 +508,49 @@ public class ApiStatusDetectionService {
return "[자동감지] " + first + " API " + titleKeyword;
}
- private PortalNotice createDraftNotice(String title, String summary, List apiIds,
- Map apiNames, LocalDateTime now) {
+ /** 관리자가 검수 시 채워 넣을 항목 - {제목, 작성 힌트} */
+ private static final String[][] NOTICE_FILL_IN_SECTIONS = {
+ {"발생 원인", "예) 백엔드 DB 커넥션 풀 고갈로 응답 지연 발생"},
+ {"영향 범위", "예) 조회 API 전 구간 응답 실패, 등록/수정은 정상"},
+ {"조치 내용", "예) 커넥션 풀 증설 및 장애 인스턴스 격리 완료"},
+ {"예상 복구 시간", "예) 2026-08-03 14:30 (복구 완료 시 실제 시각으로 갱신)"}
+ };
+
+ /**
+ * 자동 탐지 장애 공지 생성.
+ *
+ * 본문에는 관리자가 검수하며 채울 항목(발생 원인·영향 범위·조치 내용·예상 복구 시간)을
+ * 미리 넣어 둔다. 초안이면 작성 힌트를 함께 남기고, 이미 게시되는 경우엔 고객에게
+ * 그대로 노출되므로 "확인 중입니다" 로 채워 둔다.
+ *
+ * @param publish true 면 게시(USE_YN='Y'), false 면 초안(USE_YN='N')
+ */
+ private PortalNotice createNotice(String title, String summary, List apiIds,
+ Map apiNames, LocalDateTime now, boolean publish) {
StringBuilder detail = new StringBuilder();
- detail.append("").append(summary).append(" 로 자동 감지된 장애입니다. 관리자 검수 후 정식 게시됩니다.
");
- detail.append("영향 API
");
+ detail.append("").append(summary).append(" 로 자동 감지된 장애입니다.");
+ detail.append(publish ? " 상세 내용은 확인 후 갱신될 수 있습니다." : " 관리자 검수 후 정식 게시됩니다.");
+ detail.append("
");
+ detail.append("영향 API
");
for (String apiId : apiIds) {
detail.append("- ").append(apiLabel(apiId, apiNames)).append(" (").append(apiId).append(")
");
}
detail.append("
");
+ for (String[] section : NOTICE_FILL_IN_SECTIONS) {
+ detail.append("").append(section[0]).append("
");
+ // 게시 상태로 나가는 본문에 "작성 필요" 가 그대로 보이면 안 되므로 문구를 나눈다
+ detail.append(publish
+ ? "확인 중입니다.
"
+ : "[작성 필요] " + section[1] + "
");
+ }
+
PortalNotice notice = new PortalNotice();
notice.setId(null);
notice.setNoticeSubject(StringUtils.abbreviate(title, NOTICE_SUBJECT_MAX_LENGTH));
notice.setNoticeDetail(detail.toString());
notice.setNoticeType(NOTICE_TYPE_INCIDENT);
- notice.setUseYn("N");
+ notice.setUseYn(publish ? "Y" : "N");
notice.setFixYn("N");
notice.setReadCount(0L);
notice.setInquirerName(AUTHOR_SYSTEM);
diff --git a/src/main/java/com/eactive/eai/rms/ext/djb/apistatus/ApiStatusRepository.java b/src/main/java/com/eactive/eai/rms/ext/djb/apistatus/ApiStatusRepository.java
index b0ed6c4..ee8445d 100644
--- a/src/main/java/com/eactive/eai/rms/ext/djb/apistatus/ApiStatusRepository.java
+++ b/src/main/java/com/eactive/eai/rms/ext/djb/apistatus/ApiStatusRepository.java
@@ -42,8 +42,11 @@ public interface ApiStatusRepository extends BaseRepository {
+ " FROM ("
+ " SELECT A.EAISVCNAME"
+ " , NVL(B.STATUS_CODE, 'N') AS STATUS_CODE"
+ // 점검(KIND='MAINTENANCE')만 CTRL_YN 대상이다. KIND 를 안 걸면 관리자가 시작~종료 시각을
+ // 넣어 등록한 장애 공지까지 '점검중'으로 오판해 CONTROL_START 가 발생한다.
+ " , NVL(( SELECT MAX('Y') FROM EMSAPP.DJB_APISTATUS_INCIDENT_API X"
+ " WHERE EXISTS (SELECT 1 FROM EMSAPP.DJB_APISTATUS_INCIDENT Y WHERE X.INCIDENT_ID = Y.INCIDENT_ID "
+ + " AND Y.KIND = 'MAINTENANCE'"
+ " AND SYSTIMESTAMP BETWEEN STARTED_AT AND END_AT) "
+ " AND A.EAISVCNAME = X.API_ID),'N') AS CTRL_YN "
+ " , (SELECT CASE WHEN TOTAL = 0 THEN 'X'"