Merge branch 'master' of https://git.eactive.synology.me:8090/eapim/djbank/eapim-admin.git
eapim-admin CI / build (push) Has been cancelled
eapim-admin CI / build (push) Has been cancelled
This commit is contained in:
@@ -56,11 +56,16 @@
|
||||
$("#expectEndDate").datepicker().inputmask("yyyy-mm-dd", {'autoUnmask': true});
|
||||
|
||||
$("#btn_app_approve").click(function () {
|
||||
if (confirm("승인하시겠습니까?")) {
|
||||
var isDeleteReq = (currentRequestType === 'DELETE');
|
||||
var gwAction = (isDeleteReq && $('#chkGwDelete').is(':checked')) ? 'DELETE' : 'BLOCK';
|
||||
var confirmMsg = isDeleteReq
|
||||
? "해지 승인 시 포털 인증키(Credential)는 삭제되며,\n게이트웨이 클라이언트는 [" + (gwAction === 'DELETE' ? "완전 삭제" : "차단(비활성화)") + "] 처리됩니다.\n승인하시겠습니까?"
|
||||
: "승인하시겠습니까?";
|
||||
if (confirm(confirmMsg)) {
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: url,
|
||||
data: {cmd: "APPROVE", id: key},
|
||||
data: {cmd: "APPROVE", id: key, gwAction: gwAction},
|
||||
success: function (args) {
|
||||
<%--alert("<%= localeMessage.getString("approval.approveSuccess") %>");--%>
|
||||
alert("승인되었습니다.");
|
||||
@@ -176,6 +181,8 @@
|
||||
});
|
||||
});
|
||||
|
||||
var currentRequestType = null; // 현재 신청 유형 (DELETE 승인 시 GW 처리방식 선택 노출용)
|
||||
|
||||
function detail(key) {
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
@@ -216,6 +223,21 @@
|
||||
document.getElementById("btn_app_reject").style.display = "none";
|
||||
}
|
||||
|
||||
// API 이용해지(DELETE) 건: 승인 가능 상태의 현재 승인자에게만 GW 처리방식 선택 노출
|
||||
currentRequestType = data.requestType;
|
||||
var inProgress = data.approvalStatus.code === 'REQUESTED' || data.approvalStatus.code === 'PROCESSING';
|
||||
if (data.requestType === 'DELETE' && isCurrentApprover && inProgress) {
|
||||
$("#gwActionSection").show();
|
||||
} else {
|
||||
$("#gwActionSection").hide();
|
||||
}
|
||||
// 처리 완료된 해지 건은 어떤 방식으로 실행됐는지 표시
|
||||
if (data.requestType === 'DELETE' && data.gwAction) {
|
||||
$("#gwActionResult").text("GW 처리방식: " + (data.gwAction === 'DELETE' ? "완전 삭제" : "차단(비활성화)")).show();
|
||||
} else {
|
||||
$("#gwActionResult").hide();
|
||||
}
|
||||
|
||||
// 삭제된 승인자(USERID 없음)로 진행 불가한 건: 요청됨/진행중 상태면 승인자가 아니어도 강제 반려 허용
|
||||
var hasDeletedApprover = data.approvers.some(approver =>
|
||||
!approver.user || !approver.user.USERID
|
||||
@@ -370,6 +392,13 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div id="gwActionSection" style="display:none; margin: 12px 0 0 4px; color: #c0392b;">
|
||||
<label style="cursor:pointer;">
|
||||
<input type="checkbox" id="chkGwDelete"/>
|
||||
GW 인증정보 완전 삭제 (미체크 시 차단 처리 — 인증정보 보존, 복구 가능)
|
||||
</label>
|
||||
</div>
|
||||
<div id="gwActionResult" style="display:none; margin: 12px 0 0 4px; font-weight: bold;"></div>
|
||||
<div class="popup_button">
|
||||
<button type="button" class="cssbtn" id="btn_modify" level="W"><i
|
||||
class="material-icons">save</i>수정
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.eactive.eai.rms.data.entity.onl.djb.apistatus;
|
||||
|
||||
/**
|
||||
* EAI 서비스명 조회 결과 (TSEAIHE01)
|
||||
*/
|
||||
public interface ApiNameRow {
|
||||
String getEaisvcname();
|
||||
String getEaisvcdesc();
|
||||
}
|
||||
+375
-7
@@ -1,18 +1,90 @@
|
||||
package com.eactive.eai.rms.ext.djb.apistatus;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.dao.DataIntegrityViolationException;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import com.eactive.apim.portal.common.entity.Auditable;
|
||||
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.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;
|
||||
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.eai.rms.data.entity.onl.apim.portalnotice.PortalNoticeService;
|
||||
import com.eactive.eai.rms.data.entity.onl.djb.apistatus.ApiNameRow;
|
||||
|
||||
/**
|
||||
* API 상태 탐지 결과를 개발자포탈 API Status 화면용 장애 데이터로 반영한다.
|
||||
*
|
||||
* <p>탐지 자체는 {@link ApiStatusService} 가 수행하며, 본 서비스는 그 결과를
|
||||
* {@code DJB_APISTATUS_INCIDENT} / {@code _API} / {@code _TIMELINE} + {@code PTL_NOTICE} 초안으로 기록한다.</p>
|
||||
*
|
||||
* <p>점검(CONTROL_*) 은 관리자가 공지사항으로 등록한 점검 일정이 원천이다
|
||||
* ({@code ApiStatusRepository.findApiStatusEvents} 가 INCIDENT 기간을 읽어 CTRL_YN 을 판정).
|
||||
* 따라서 CONTROL 이벤트를 다시 기록하면 순환이 되므로 기록하지 않는다.</p>
|
||||
*/
|
||||
@Service
|
||||
@Transactional
|
||||
@Transactional(transactionManager = "transactionManagerForEMS", rollbackFor = Exception.class)
|
||||
public class ApiStatusDetectionService {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(ApiStatusDetectionService.class);
|
||||
|
||||
public static final String EVENT_CONTROL_START = "CONTROL_START";
|
||||
public static final String EVENT_CONTROL_END = "CONTROL_END";
|
||||
public static final String EVENT_ERROR_START = "ERROR_START";
|
||||
public static final String EVENT_ERROR_END = "ERROR_END";
|
||||
public static final String EVENT_DELAY_START = "DELAY_START";
|
||||
public static final String EVENT_DELAY_END = "DELAY_END";
|
||||
|
||||
/** INTERFACE_ID 멱등 윈도우 (분) */
|
||||
private static final int IDEMPOTENT_WINDOW_MINUTES = 5;
|
||||
|
||||
/** 종결 상태 - 신규 탐지 병합 대상에서 제외 */
|
||||
private static final List<IncidentState> CLOSED_STATES =
|
||||
Collections.unmodifiableList(Arrays.asList(IncidentState.RESOLVED, IncidentState.CANCELED));
|
||||
|
||||
private static final String AUTHOR_SYSTEM = "SYSTEM";
|
||||
private static final String DETECTED_BY_AUTO = "AUTO";
|
||||
private static final String NOTICE_TYPE_INCIDENT = "3";
|
||||
private static final int NOTICE_SUBJECT_MAX_LENGTH = 255;
|
||||
|
||||
@Autowired
|
||||
private DjbApistatusIncidentRepository incidentRepository;
|
||||
|
||||
@Autowired
|
||||
private DjbApistatusIncidentApiRepository incidentApiRepository;
|
||||
|
||||
@Autowired
|
||||
private DjbApistatusIncidentTimelineRepository incidentTimelineRepository;
|
||||
|
||||
@Autowired
|
||||
private PortalNoticeService portalNoticeService;
|
||||
|
||||
@Autowired
|
||||
private ApiStatusRepository apiStatusRepository;
|
||||
|
||||
/**
|
||||
* 이벤트 감지
|
||||
* @param event "CONTROL_START" : 점검시작
|
||||
@@ -25,17 +97,313 @@ public class ApiStatusDetectionService {
|
||||
*/
|
||||
public void detect(String event, List<String> apiIds) {
|
||||
log.debug("이벤트 감지: {} {}", event, apiIds);
|
||||
|
||||
if (StringUtils.isBlank(event) || apiIds == null || apiIds.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<String> targets = apiIds.stream()
|
||||
.filter(StringUtils::isNotBlank)
|
||||
.distinct()
|
||||
.sorted()
|
||||
.collect(Collectors.toList());
|
||||
if (targets.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (event) {
|
||||
case EVENT_ERROR_START:
|
||||
openIncident(event, targets, "에러율 임계 초과", "장애");
|
||||
break;
|
||||
case EVENT_DELAY_START:
|
||||
openIncident(event, targets, "응답시간 임계 초과", "응답지연");
|
||||
break;
|
||||
case EVENT_ERROR_END:
|
||||
case EVENT_DELAY_END:
|
||||
markRecovered(event, targets);
|
||||
break;
|
||||
case EVENT_CONTROL_START:
|
||||
case EVENT_CONTROL_END:
|
||||
// 점검은 관리자 등록 공지가 원천 - 재기록하지 않음
|
||||
log.debug("점검 이벤트는 장애 데이터로 기록하지 않음: {} {}", event, targets);
|
||||
break;
|
||||
default:
|
||||
log.warn("알 수 없는 이벤트: {}", event);
|
||||
}
|
||||
}
|
||||
|
||||
// API FSM 내 아직 장애로 남아있는 API 목록
|
||||
public void remainDownApiIds() {
|
||||
/**
|
||||
* 미종결 장애로 남아있는 API 목록 (탐지 Job 폴링용)
|
||||
*/
|
||||
@Transactional(transactionManager = "transactionManagerForEMS", readOnly = true)
|
||||
public List<String> remainDownApiIds() {
|
||||
List<DjbApistatusIncident> openIncidents =
|
||||
incidentRepository.findByKindAndStateNotInOrderByStartedAtDesc(IncidentKind.INCIDENT, CLOSED_STATES);
|
||||
if (openIncidents.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
List<Long> incidentIds = openIncidents.stream()
|
||||
.map(DjbApistatusIncident::getIncidentId)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return incidentApiRepository.findByIncidentIdInOrderByIncidentIdAscApiIdAsc(incidentIds).stream()
|
||||
.filter(api -> api.getRecoveredAt() == null)
|
||||
.map(DjbApistatusIncidentApi::getApiId)
|
||||
.distinct()
|
||||
.sorted()
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
// ────────────── 장애 발생 ──────────────
|
||||
|
||||
// 정상 동작 - detect()에서 일괄 처리 (복구 = 점검종료, 장애종료, 지연종료)
|
||||
// public void recovered(String event, String[] apiIds) {
|
||||
//
|
||||
// }
|
||||
private void openIncident(String event, List<String> apiIds, String summary, String titleKeyword) {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
String interfaceId = makeInterfaceId(event, apiIds, now);
|
||||
|
||||
// 1) 동일 윈도우 재탐지 - 타임라인만 남기고 종료
|
||||
Optional<DjbApistatusIncident> duplicated = incidentRepository.findByInterfaceId(interfaceId);
|
||||
if (duplicated.isPresent()) {
|
||||
appendTimeline(duplicated.get().getIncidentId(), null,
|
||||
"재탐지 신호 수신 (" + event + ")", now);
|
||||
return;
|
||||
}
|
||||
|
||||
// 2) 이미 열려있는 자동 등록 장애가 해당 API 를 포함하면 영향 API 만 병합
|
||||
// (관리자가 직접 작성한 장애 공지는 건드리지 않는다)
|
||||
List<DjbApistatusIncident> openIncidents = incidentRepository
|
||||
.findOpenByApiIds(IncidentKind.INCIDENT, DETECTED_BY_AUTO, CLOSED_STATES, apiIds);
|
||||
if (!openIncidents.isEmpty()) {
|
||||
mergeIntoOpenIncident(openIncidents.get(0), event, apiIds, now);
|
||||
return;
|
||||
}
|
||||
|
||||
// 3) 신규 장애 + 공지 초안 생성
|
||||
Map<String, String> apiNames = resolveApiNames(apiIds);
|
||||
String title = buildTitle(titleKeyword, apiIds, apiNames);
|
||||
|
||||
PortalNotice notice = createDraftNotice(title, summary, apiIds, apiNames, now);
|
||||
|
||||
DjbApistatusIncident incident = new DjbApistatusIncident();
|
||||
incident.setKind(IncidentKind.INCIDENT);
|
||||
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.setFixYn("N");
|
||||
stampAudit(incident, now);
|
||||
|
||||
DjbApistatusIncident saved;
|
||||
try {
|
||||
saved = incidentRepository.saveAndFlush(incident);
|
||||
} catch (DataIntegrityViolationException e) {
|
||||
// INTERFACE_ID UNIQUE 충돌 - 동시 탐지. 공지 초안 정리 후 타임라인만 남긴다
|
||||
log.info("장애 중복 감지 (interfaceId={}) - 기존 장애에 타임라인 추가", interfaceId, e);
|
||||
portalNoticeService.deleteById(notice.getId());
|
||||
incidentRepository.findByInterfaceId(interfaceId).ifPresent(existing ->
|
||||
appendTimeline(existing.getIncidentId(), null,
|
||||
"재탐지 신호 수신 (" + event + ")", now));
|
||||
return;
|
||||
}
|
||||
|
||||
saveIncidentApis(saved.getIncidentId(), apiIds, apiNames, now);
|
||||
appendTimeline(saved.getIncidentId(), IncidentState.INVESTIGATING,
|
||||
summary + " 자동 감지 (" + event + ")\n영향 API: " + String.join(", ", apiIds), now);
|
||||
|
||||
log.info("자동 장애 등록: incidentId={}, noticeId={}, apis={}",
|
||||
saved.getIncidentId(), notice.getId(), apiIds);
|
||||
}
|
||||
|
||||
private void mergeIntoOpenIncident(DjbApistatusIncident incident, String event,
|
||||
List<String> apiIds, LocalDateTime now) {
|
||||
Long incidentId = incident.getIncidentId();
|
||||
|
||||
Set<String> mapped = incidentApiRepository.findByIncidentIdOrderByApiId(incidentId).stream()
|
||||
.map(DjbApistatusIncidentApi::getApiId)
|
||||
.collect(Collectors.toCollection(HashSet::new));
|
||||
|
||||
List<String> added = apiIds.stream()
|
||||
.filter(apiId -> !mapped.contains(apiId))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
if (added.isEmpty()) {
|
||||
appendTimeline(incidentId, null, "재탐지 신호 수신 (" + event + ")", now);
|
||||
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);
|
||||
}
|
||||
|
||||
// ────────────── 복구 ──────────────
|
||||
|
||||
private void markRecovered(String event, List<String> apiIds) {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
|
||||
List<DjbApistatusIncidentApi> candidates =
|
||||
incidentApiRepository.findByApiIdInAndRecoveredAtIsNull(apiIds);
|
||||
if (candidates.isEmpty()) {
|
||||
log.debug("복구 대상 장애 없음: {} {}", event, apiIds);
|
||||
return;
|
||||
}
|
||||
|
||||
Map<Long, DjbApistatusIncident> openIncidents = incidentRepository
|
||||
.findByKindAndStateNotInOrderByStartedAtDesc(IncidentKind.INCIDENT, CLOSED_STATES).stream()
|
||||
.collect(Collectors.toMap(DjbApistatusIncident::getIncidentId, incident -> incident));
|
||||
|
||||
Map<Long, List<String>> recoveredByIncident = new HashMap<>();
|
||||
for (DjbApistatusIncidentApi api : candidates) {
|
||||
if (!openIncidents.containsKey(api.getIncidentId())) {
|
||||
continue;
|
||||
}
|
||||
api.setRecoveredAt(now);
|
||||
api.setLastModifiedBy(AUTHOR_SYSTEM);
|
||||
api.setLastModifiedDate(now);
|
||||
incidentApiRepository.save(api);
|
||||
|
||||
recoveredByIncident.computeIfAbsent(api.getIncidentId(), key -> new ArrayList<>())
|
||||
.add(api.getApiId());
|
||||
}
|
||||
|
||||
if (recoveredByIncident.isEmpty()) {
|
||||
log.debug("복구 대상 장애 없음 (미종결 장애 미포함): {} {}", event, apiIds);
|
||||
return;
|
||||
}
|
||||
|
||||
for (Map.Entry<Long, List<String>> entry : recoveredByIncident.entrySet()) {
|
||||
Long incidentId = entry.getKey();
|
||||
List<String> recoveredApis = entry.getValue();
|
||||
DjbApistatusIncident incident = openIncidents.get(incidentId);
|
||||
|
||||
boolean allRecovered = incidentApiRepository.countByIncidentIdAndRecoveredAtIsNull(incidentId) == 0;
|
||||
IncidentState nextState = allRecovered ? IncidentState.RESOLVED : IncidentState.MONITORING;
|
||||
|
||||
if (incident.getState() != nextState) {
|
||||
incident.setPreviousState(incident.getState());
|
||||
incident.setState(nextState);
|
||||
}
|
||||
if (allRecovered) {
|
||||
incident.setEndAt(now);
|
||||
}
|
||||
incident.setLastModifiedBy(AUTHOR_SYSTEM);
|
||||
incident.setLastModifiedDate(now);
|
||||
incidentRepository.save(incident);
|
||||
|
||||
String body = (allRecovered ? "전체 복구 확인 (" : "일부 복구 확인 (") + event + ")\n"
|
||||
+ String.join(", ", recoveredApis);
|
||||
appendTimeline(incidentId, nextState, body, now);
|
||||
|
||||
log.info("자동 복구 처리: incidentId={}, state={}, apis={}", incidentId, nextState, recoveredApis);
|
||||
}
|
||||
}
|
||||
|
||||
// ────────────── 내부 헬퍼 ──────────────
|
||||
|
||||
private String makeInterfaceId(String event, List<String> sortedApiIds, LocalDateTime now) {
|
||||
long windowSeconds = IDEMPOTENT_WINDOW_MINUTES * 60L;
|
||||
long flooredEpoch = now.atZone(ZoneId.systemDefault()).toEpochSecond()
|
||||
/ windowSeconds * windowSeconds;
|
||||
String joined = String.join(",", sortedApiIds);
|
||||
String interfaceId = event + ":" + joined + ":" + flooredEpoch;
|
||||
|
||||
// INTERFACE_ID 는 VARCHAR2(200) - 초과 시 API 목록을 해시로 축약
|
||||
if (interfaceId.length() > 200) {
|
||||
interfaceId = event + ":#" + Integer.toHexString(joined.hashCode())
|
||||
+ ":" + sortedApiIds.size() + ":" + flooredEpoch;
|
||||
}
|
||||
return interfaceId;
|
||||
}
|
||||
|
||||
private Map<String, String> resolveApiNames(List<String> apiIds) {
|
||||
Map<String, String> names = new HashMap<>();
|
||||
try {
|
||||
for (ApiNameRow row : apiStatusRepository.findApiNames(apiIds)) {
|
||||
names.put(row.getEaisvcname(), row.getEaisvcdesc());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("API 명 조회 실패 - API ID 를 그대로 사용", e);
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
private String apiLabel(String apiId, Map<String, String> apiNames) {
|
||||
return StringUtils.defaultIfBlank(apiNames.get(apiId), apiId);
|
||||
}
|
||||
|
||||
private String buildTitle(String titleKeyword, List<String> apiIds, Map<String, String> apiNames) {
|
||||
String first = apiLabel(apiIds.get(0), apiNames);
|
||||
if (apiIds.size() > 1) {
|
||||
return "[자동감지] " + first + " 외 " + (apiIds.size() - 1) + "종 API " + titleKeyword;
|
||||
}
|
||||
return "[자동감지] " + first + " API " + titleKeyword;
|
||||
}
|
||||
|
||||
private PortalNotice createDraftNotice(String title, String summary, List<String> apiIds,
|
||||
Map<String, String> apiNames, LocalDateTime now) {
|
||||
StringBuilder detail = new StringBuilder();
|
||||
detail.append("<p>").append(summary).append(" 로 자동 감지된 장애입니다. 관리자 검수 후 정식 게시됩니다.</p>");
|
||||
detail.append("<p>영향 API</p><ul>");
|
||||
for (String apiId : apiIds) {
|
||||
detail.append("<li>").append(apiLabel(apiId, apiNames)).append(" (").append(apiId).append(")</li>");
|
||||
}
|
||||
detail.append("</ul>");
|
||||
|
||||
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.setFixYn("N");
|
||||
notice.setReadCount(0L);
|
||||
notice.setInquirerName(AUTHOR_SYSTEM);
|
||||
notice.setCreatedBy(AUTHOR_SYSTEM);
|
||||
notice.setCreatedDate(now);
|
||||
|
||||
portalNoticeService.save(notice);
|
||||
return notice;
|
||||
}
|
||||
|
||||
private void saveIncidentApis(Long incidentId, Collection<String> apiIds,
|
||||
Map<String, String> apiNames, LocalDateTime now) {
|
||||
List<DjbApistatusIncidentApi> rows = new ArrayList<>();
|
||||
for (String apiId : apiIds) {
|
||||
DjbApistatusIncidentApi api = new DjbApistatusIncidentApi();
|
||||
api.setIncidentId(incidentId);
|
||||
api.setApiId(apiId);
|
||||
api.setApiName(apiNames.get(apiId));
|
||||
stampAudit(api, now);
|
||||
rows.add(api);
|
||||
}
|
||||
if (!rows.isEmpty()) {
|
||||
incidentApiRepository.saveAll(rows);
|
||||
}
|
||||
}
|
||||
|
||||
private void appendTimeline(Long incidentId, IncidentState stateAfter, String body, LocalDateTime now) {
|
||||
DjbApistatusIncidentTimeline timeline = new DjbApistatusIncidentTimeline();
|
||||
timeline.setIncidentId(incidentId);
|
||||
timeline.setEventAt(now);
|
||||
timeline.setStateAfter(stateAfter);
|
||||
timeline.setBody(StringUtils.abbreviate(body, 4000));
|
||||
timeline.setAuthorType(AUTHOR_SYSTEM);
|
||||
timeline.setVisibleYn("Y");
|
||||
stampAudit(timeline, now);
|
||||
incidentTimelineRepository.save(timeline);
|
||||
}
|
||||
|
||||
/**
|
||||
* 스케줄러 스레드는 로그인 세션이 없어 AuditorAware 가 비어있다. CREATED_BY 는 NOT NULL 이므로 직접 채운다.
|
||||
*/
|
||||
private void stampAudit(Auditable entity, LocalDateTime now) {
|
||||
entity.setCreatedBy(AUTHOR_SYSTEM);
|
||||
entity.setCreatedDate(now);
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,10 @@ import java.util.List;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
import com.eactive.eai.data.jpa.BaseRepository;
|
||||
import com.eactive.eai.rms.data.entity.onl.djb.apistatus.ApiNameRow;
|
||||
import com.eactive.eai.rms.data.entity.onl.djb.apistatus.ApiStatus;
|
||||
import com.eactive.eai.rms.data.entity.onl.djb.apistatus.ApiStatusEvent;
|
||||
|
||||
@@ -73,4 +76,11 @@ public interface ApiStatusRepository extends BaseRepository<ApiStatus, String> {
|
||||
@Param("delayRangeMinute") int delayRangeMinute,
|
||||
@Param("delayAvgRespTime") int delayAvgRespTime
|
||||
);
|
||||
|
||||
/**
|
||||
* EAI 서비스명 조회. 장애/점검 영향 API 의 API_NAME 캐시용.
|
||||
*/
|
||||
@Query(nativeQuery = true, value =
|
||||
" SELECT EAISVCNAME, EAISVCDESC FROM TSEAIHE01 WHERE EAISVCNAME IN (:apiIds)")
|
||||
List<ApiNameRow> findApiNames(@Param("apiIds") Collection<String> apiIds);
|
||||
}
|
||||
+2
-2
@@ -66,8 +66,8 @@ public class PortalApprovalManController extends BaseAnnotationController {
|
||||
}
|
||||
|
||||
@PostMapping(value = "/onl/apim/approval/portalApprovalMan.json", params = "cmd=APPROVE")
|
||||
public ResponseEntity<Void> approve(String id) {
|
||||
portalApprovalManService.approve(id, SessionManager.getLoginVo().getUserId());
|
||||
public ResponseEntity<Void> approve(String id, @RequestParam(required = false) String gwAction) {
|
||||
portalApprovalManService.approve(id, SessionManager.getLoginVo().getUserId(), gwAction);
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@ package com.eactive.eai.rms.onl.apim.approval;
|
||||
|
||||
import com.eactive.apim.portal.apispec.service.ApiSpecInfoService;
|
||||
import com.eactive.apim.portal.apprequest.entity.AppRequest;
|
||||
import com.eactive.apim.portal.apprequest.entity.AppRequestType;
|
||||
import com.eactive.apim.portal.apprequest.entity.GwAction;
|
||||
import com.eactive.apim.portal.apprequest.repository.AppRequestRepository;
|
||||
import com.eactive.apim.portal.approval.entity.Approval;
|
||||
import com.eactive.apim.portal.approval.entity.ApprovalType;
|
||||
@@ -56,6 +58,7 @@ public class PortalApprovalManService extends BaseService {
|
||||
private static final String HARD_DELETE_FLAG_KEY = "approval.hard-delete.enabled";
|
||||
|
||||
private final ApprovalService approvalService;
|
||||
private final PortalApprovalUIMapper portalApprovalUIMapper;
|
||||
private final ApprovalStateMachine approvalStateMachine;
|
||||
private final AppRequestRepository appRequestRepository;
|
||||
private final PortalAppApprovalListener portalAppApprovalListener;
|
||||
@@ -77,12 +80,18 @@ public class PortalApprovalManService extends BaseService {
|
||||
private EntityManager entityManager;
|
||||
|
||||
public Page<PortalApprovalUI> selectList(Pageable pageable, PortalApprovalUISearch portalApprovalUISearch) {
|
||||
// 목록 그리드는 Approval 자체 필드(제목/유형/상태/요청자/생성일/예상완료일)만 사용하므로
|
||||
// 행별 상세 재조회(getDetailWithMaskOption: AppRequest + API 목록 + AGW 크로스 조회) 없이 경량 매핑한다.
|
||||
Page<Approval> approval = approvalService.findAll(pageable, portalApprovalUISearch);
|
||||
return approval.map(entity -> {
|
||||
try {
|
||||
return getDetailWithMaskOption(entity.getId(), false);
|
||||
PortalApprovalUI ui = portalApprovalUIMapper.toVo(entity);
|
||||
if (ui.getRequester() == null) {
|
||||
ui.setRequester(createDeletedUserPlaceholder());
|
||||
}
|
||||
return ui;
|
||||
} catch (Exception e) {
|
||||
logger.error("승인 상세 조회 실패 - ID: {}, 오류: {}", entity.getId(), e.getMessage());
|
||||
logger.error("승인 목록 매핑 실패 - ID: {}, 오류: {}", entity.getId(), e.getMessage());
|
||||
return null;
|
||||
}
|
||||
});
|
||||
@@ -204,6 +213,7 @@ public class PortalApprovalManService extends BaseService {
|
||||
if (optRequest.isPresent()) {
|
||||
AppRequest appRequest = optRequest.get();
|
||||
appApprovalUI.setRequestType(appRequest.getType());
|
||||
appApprovalUI.setGwAction(appRequest.getGwAction());
|
||||
|
||||
|
||||
if (appRequest.getApiGroupList() == null || appRequest.getApiGroupList().isEmpty()) {
|
||||
@@ -321,7 +331,7 @@ public class PortalApprovalManService extends BaseService {
|
||||
return placeholder;
|
||||
}
|
||||
|
||||
public void approve(String id, String approverId) {
|
||||
public void approve(String id, String approverId, String gwAction) {
|
||||
Map options = new HashMap<>();
|
||||
|
||||
Approval approval = approvalService.findById(id).orElseThrow(() -> new IllegalArgumentException("승인 정보를 찾을 수 없습니다. ID: " + id));
|
||||
@@ -329,6 +339,18 @@ public class PortalApprovalManService extends BaseService {
|
||||
throw new IllegalArgumentException("아직 승인 차례가 아닙니다.");
|
||||
}
|
||||
|
||||
// API 이용해지(DELETE) 승인이면 GW 처리방식(차단/완전삭제)을 신청서에 영속화한다.
|
||||
// 같은 EMS 트랜잭션이므로 최종 승인 시 listener 가 이 값을 읽고, REDEPLOY 재처리에서도 유지된다.
|
||||
// 미지정(null)은 안전 기본값인 차단(BLOCK)으로 처리.
|
||||
if (approval.getApprovalType().equals(ApprovalType.APP)) {
|
||||
appRequestRepository.findById(approval.getTargetId()).ifPresent(request -> {
|
||||
if (AppRequestType.DELETE.equals(request.getType())) {
|
||||
request.setGwAction("DELETE".equalsIgnoreCase(gwAction) ? GwAction.DELETE : GwAction.BLOCK);
|
||||
appRequestRepository.save(request);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
options.put(ApprovalConstants.APPROVER, approverId);
|
||||
sendEvent(id, ApprovalEvent.APPROVE, options);
|
||||
}
|
||||
|
||||
+36
-4
@@ -2,6 +2,7 @@ package com.eactive.eai.rms.onl.apim.approval.app;
|
||||
|
||||
import com.eactive.apim.portal.apprequest.entity.AppRequest;
|
||||
import com.eactive.apim.portal.apprequest.entity.AppRequestType;
|
||||
import com.eactive.apim.portal.apprequest.entity.GwAction;
|
||||
import com.eactive.apim.portal.apprequest.repository.AppRequestRepository;
|
||||
import com.eactive.apim.portal.approval.entity.Approval;
|
||||
import com.eactive.apim.portal.approval.service.ApprovalDeployException;
|
||||
@@ -72,7 +73,7 @@ public class PortalAppApprovalListener implements ApprovalListener {
|
||||
|
||||
// 위 메서드가 반환되면 트랜잭션이 커밋되어 Portal DB에 영구 저장됨
|
||||
// 이제 APIGW 동기화 및 GW 리로드 수행
|
||||
syncTargetServer(type, credentialUI);
|
||||
syncTargetServer(appRequest, credentialUI);
|
||||
sendApprovalResult(appRequest);
|
||||
}
|
||||
}
|
||||
@@ -94,7 +95,12 @@ public class PortalAppApprovalListener implements ApprovalListener {
|
||||
} else if (type.equals(AppRequestType.MODIFY)) {
|
||||
credentialUI = modifyClientUI(appRequest);
|
||||
} else if (type.equals(AppRequestType.DELETE)) {
|
||||
credentialManService.delete(appRequest.getClientId());
|
||||
// REDEPLOY 재처리 시 credential 이 이미 삭제되어 있을 수 있으므로 부재는 정상 통과시킨다.
|
||||
try {
|
||||
credentialManService.delete(appRequest.getClientId());
|
||||
} catch (org.springframework.dao.EmptyResultDataAccessException e) {
|
||||
logger.warn("포털 credential 이미 삭제됨 (재처리 통과) - clientId: {}", appRequest.getClientId());
|
||||
}
|
||||
credentialUI = new CredentialUI();
|
||||
credentialUI.setClientid(appRequest.getClientId());
|
||||
}
|
||||
@@ -119,7 +125,9 @@ public class PortalAppApprovalListener implements ApprovalListener {
|
||||
messageSendService.sendMessage(MessageCode.APP_APPROVE, recipient, params);
|
||||
}
|
||||
|
||||
private void syncTargetServer(AppRequestType action, CredentialUI credentialUI) throws ApprovalDeployException {
|
||||
private void syncTargetServer(AppRequest appRequest, CredentialUI credentialUI) throws ApprovalDeployException {
|
||||
|
||||
AppRequestType action = appRequest.getType();
|
||||
|
||||
DataSourceType type = DataSourceTypeManager.getDataSourceType("APIGW");
|
||||
DataSourceContextHolder.setDataSourceType(type);
|
||||
@@ -129,7 +137,7 @@ public class PortalAppApprovalListener implements ApprovalListener {
|
||||
} else if (action.equals(AppRequestType.MODIFY)) {
|
||||
clientManService.update(credentialUIMapper.mapClient(credentialUI));
|
||||
} else if (action.equals(AppRequestType.DELETE)) {
|
||||
clientManService.delete(credentialUI.getClientid());
|
||||
applyGwActionForDelete(appRequest.getGwAction(), credentialUI.getClientid());
|
||||
}
|
||||
|
||||
CommonCommand.builder()
|
||||
@@ -138,6 +146,30 @@ public class PortalAppApprovalListener implements ApprovalListener {
|
||||
.build().broadcast(agentUtilService);
|
||||
}
|
||||
|
||||
/**
|
||||
* API 이용해지 승인 시 GW 클라이언트(TSEAIAU01) 처리.
|
||||
* 승인 시점에 관리자가 선택해 PTL_APP_REQUEST.GW_ACTION 에 영속화된 값을 따른다.
|
||||
* - DELETE: 행 완전 삭제 / - BLOCK 또는 null(미지정): 차단(appstatus='0')
|
||||
* GW 에 행이 이미 없으면(선삭제·재처리) 목적이 달성된 상태이므로 통과시키고,
|
||||
* 그 외 오류는 ApprovalDeployException 으로 던져 FAILED(재처리 가능) 상태를 만든다.
|
||||
*/
|
||||
private void applyGwActionForDelete(GwAction gwAction, String clientId) throws ApprovalDeployException {
|
||||
try {
|
||||
if (GwAction.DELETE.equals(gwAction)) {
|
||||
clientManService.delete(clientId);
|
||||
logger.info("GW 클라이언트 완전 삭제 - clientId: {}", clientId);
|
||||
} else {
|
||||
clientManService.blockClient(clientId);
|
||||
logger.info("GW 클라이언트 차단(appstatus=0) - clientId: {}", clientId);
|
||||
}
|
||||
} catch (org.springframework.dao.EmptyResultDataAccessException | javax.persistence.EntityNotFoundException e) {
|
||||
logger.warn("GW 클라이언트가 이미 존재하지 않아 처리 생략 (재처리 통과) - clientId: {}", clientId);
|
||||
} catch (RuntimeException e) {
|
||||
logger.error("GW 클라이언트 해지 처리 실패 - clientId: {}, gwAction: {}", clientId, gwAction, e);
|
||||
throw new ApprovalDeployException("GW 클라이언트 해지 처리에 실패했습니다: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private CredentialUI modifyClientUI(AppRequest appRequest) {
|
||||
CredentialUI existingCredential = credentialManService.selectDetail(appRequest.getClientId());
|
||||
existingCredential.getApiList().clear();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.eactive.eai.rms.onl.apim.approval.app;
|
||||
|
||||
import com.eactive.apim.portal.apprequest.entity.AppRequestType;
|
||||
import com.eactive.apim.portal.apprequest.entity.GwAction;
|
||||
import com.eactive.eai.rms.onl.apim.apigroup.ui.ApiGroupUI;
|
||||
import com.eactive.eai.rms.onl.apim.approval.PortalApprovalUI;
|
||||
import java.util.ArrayList;
|
||||
@@ -26,4 +27,7 @@ public class PortalAppApprovalUI extends PortalApprovalUI {
|
||||
private List<String> prevApiGroupNameList = new ArrayList<>();
|
||||
|
||||
private AppRequestType requestType;
|
||||
|
||||
//DELETE 승인 시 선택된 GW 처리방식 (승인 완료 건 상세 표시용)
|
||||
private GwAction gwAction;
|
||||
}
|
||||
|
||||
+2
-2
@@ -82,9 +82,9 @@ public class MessageRequestManService extends BaseService {
|
||||
return t == null ? "" : t.format(f);
|
||||
}
|
||||
|
||||
/** 상태 검색 콤보 (PENDING/SENT/FAILED). */
|
||||
/** 상태 검색 콤보 (PENDING/SENT/FAILED/SKIPPED). SKIPPED 는 행번 아닌 ID 로 발송을 건너뛴 건. */
|
||||
public List<ComboVo> statusCombo() {
|
||||
return Arrays.stream(new String[]{"PENDING", "SENT", "FAILED"})
|
||||
return Arrays.stream(new String[]{"PENDING", "SENT", "FAILED", "SKIPPED"})
|
||||
.map(s -> {
|
||||
ComboVo v = new ComboVo();
|
||||
v.setCode(s);
|
||||
|
||||
Reference in New Issue
Block a user