API Status 개선 및 조회 기간 설정 추가

This commit is contained in:
Rinjae
2026-07-31 16:22:43 +09:00
parent fd229fca43
commit 6cb98d36bb
7 changed files with 76 additions and 15 deletions
@@ -61,7 +61,7 @@ public class ApiStatusController {
@GetMapping @GetMapping
public ModelAndView index() { public ModelAndView index() {
ModelAndView mav = new ModelAndView("djb/apistatus/index"); ModelAndView mav = new ModelAndView("djb/apistatus/index");
mav.addObject("windowDays", ApiStatusSupport.DEFAULT_WINDOW_DAYS); mav.addObject("windowDays", catalogService.getWindowDays());
mav.addObject("authenticated", SecurityUtil.isAuthenticated()); mav.addObject("authenticated", SecurityUtil.isAuthenticated());
LocalDateTime lastFireAt = catalogService.getLastMonitorFireAt(); LocalDateTime lastFireAt = catalogService.getLastMonitorFireAt();
mav.addObject("lastFireAt", lastFireAt); mav.addObject("lastFireAt", lastFireAt);
@@ -83,11 +83,12 @@ public class ApiStatusController {
@RequestParam(value = "kind", required = false) String kind) { @RequestParam(value = "kind", required = false) String kind) {
LocalDate today = ApiStatusSupport.now().toLocalDate(); LocalDate today = ApiStatusSupport.now().toLocalDate();
int windowDays = catalogService.getWindowDays();
ModelAndView mav = new ModelAndView("djb/apistatus/issues"); ModelAndView mav = new ModelAndView("djb/apistatus/issues");
mav.addObject("windowDays", ApiStatusSupport.DEFAULT_WINDOW_DAYS); mav.addObject("windowDays", windowDays);
mav.addObject("today", today); mav.addObject("today", today);
mav.addObject("minDate", today.minusDays(ApiStatusSupport.DEFAULT_WINDOW_DAYS - 1L)); mav.addObject("minDate", today.minusDays(windowDays - 1L));
mav.addObject("selectedDate", date); mav.addObject("selectedDate", date);
mav.addObject("selectedApiId", apiId); mav.addObject("selectedApiId", apiId);
mav.addObject("selectedKind", kind); mav.addObject("selectedKind", kind);
@@ -105,8 +106,8 @@ public class ApiStatusController {
@GetMapping("/uptime.json") @GetMapping("/uptime.json")
@ResponseBody @ResponseBody
public List<DailyStatDTO> uptime( public List<DailyStatDTO> uptime(
@RequestParam(value = "days", defaultValue = "90") int days) { @RequestParam(value = "days", defaultValue = "0") int days) {
return uptimeService.getDailyStats(days); return uptimeService.getDailyStats(days > 0 ? days : catalogService.getWindowDays());
} }
/** P3 - 진행 중 장애 */ /** P3 - 진행 중 장애 */
@@ -154,10 +155,10 @@ public class ApiStatusController {
@GetMapping("/issues/dates.json") @GetMapping("/issues/dates.json")
@ResponseBody @ResponseBody
public List<IssueDateEntryDTO> issueDates( public List<IssueDateEntryDTO> issueDates(
@RequestParam(value = "days", defaultValue = "90") int days, @RequestParam(value = "days", defaultValue = "0") int days,
@RequestParam(value = "apiId", required = false) String apiId, @RequestParam(value = "apiId", required = false) String apiId,
@RequestParam(value = "kind", required = false) String kind) { @RequestParam(value = "kind", required = false) String kind) {
return issueHistoryService.getIssueDates(days, apiId, kind); return issueHistoryService.getIssueDates(days > 0 ? days : catalogService.getWindowDays(), apiId, kind);
} }
/** P10 - 이슈 목록 (날짜/API/유형 필터) */ /** P10 - 이슈 목록 (날짜/API/유형 필터) */
@@ -38,6 +38,12 @@ public class ApiStatusCatalogService {
/** 실서버(DAPM) QRTZ_JOB_DETAILS 실사값 (2026-07-31) */ /** 실서버(DAPM) QRTZ_JOB_DETAILS 실사값 (2026-07-31) */
private static final String DEFAULT_MONITOR_JOB_NAME = "ApiStatusMonitorJob"; private static final String DEFAULT_MONITOR_JOB_NAME = "ApiStatusMonitorJob";
/** 상태/이력 조회 기간(일). 가동률 바·이슈 인덱스·date picker 범위가 모두 이 값을 따른다 */
public static final String KEY_WINDOW_DAYS = "djb.apistatus.window-days";
private static final int MIN_WINDOW_DAYS = 1;
private static final int MAX_WINDOW_DAYS = 365;
private final ApiSearchFacade apiSearchFacade; private final ApiSearchFacade apiSearchFacade;
private final PortalPropertyService portalPropertyService; private final PortalPropertyService portalPropertyService;
private final GwApiStatusRepository gwApiStatusRepository; private final GwApiStatusRepository gwApiStatusRepository;
@@ -107,6 +113,25 @@ public class ApiStatusCatalogService {
} }
} }
/**
* 상태/이력 조회 기간(일). PTL_PROPERTY({@code Portal} / {@code djb.apistatus.window-days})로
* 조절하며 1~365 로 clamp 한다. 값이 이상하면 기본 90일.
*/
@Transactional
public int getWindowDays() {
try {
String value = portalPropertyService.getOrCreateProperty(
PROPERTY_GROUP, KEY_WINDOW_DAYS,
String.valueOf(ApiStatusSupport.DEFAULT_WINDOW_DAYS),
"API Status 조회 기간(일). 가동률 바·이슈 인덱스·날짜 선택 범위에 적용 (1~365)");
int days = Integer.parseInt(StringUtils.trimToEmpty(value));
return Math.max(MIN_WINDOW_DAYS, Math.min(MAX_WINDOW_DAYS, days));
} catch (Exception e) {
log.warn("조회 기간 프로퍼티 해석 실패 - 기본값 {}일 사용", ApiStatusSupport.DEFAULT_WINDOW_DAYS, e);
return ApiStatusSupport.DEFAULT_WINDOW_DAYS;
}
}
/** /**
* API 상태가 마지막으로 <b>변경</b>된 시각 (AGWAPP.API_STATUS 최근 upsert). * API 상태가 마지막으로 <b>변경</b>된 시각 (AGWAPP.API_STATUS 최근 upsert).
* 실행 시각(fire)과 달리 상태 변화가 있을 때만 갱신된다. * 실행 시각(fire)과 달리 상태 변화가 있을 때만 갱신된다.
@@ -32,6 +32,7 @@ public class ApiStatusIssueHistoryService {
private final ApiStatusIncidentQueryRepository incidentQueryRepository; private final ApiStatusIncidentQueryRepository incidentQueryRepository;
private final ApiStatusAssembler assembler; private final ApiStatusAssembler assembler;
private final ApiStatusCatalogService catalogService;
/** /**
* P9 - 90일 인덱스바용 일자별 이슈 집계. 이슈가 여러 날에 걸치면 걸친 날짜 모두에 집계한다. * P9 - 90일 인덱스바용 일자별 이슈 집계. 이슈가 여러 날에 걸치면 걸친 날짜 모두에 집계한다.
@@ -97,7 +98,7 @@ public class ApiStatusIssueHistoryService {
if (date == null) { if (date == null) {
LocalDate today = now.toLocalDate(); LocalDate today = now.toLocalDate();
from = today.minusDays(ApiStatusSupport.DEFAULT_WINDOW_DAYS - 1L).atStartOfDay(); from = today.minusDays(catalogService.getWindowDays() - 1L).atStartOfDay();
to = today.plusDays(1).atStartOfDay(); to = today.plusDays(1).atStartOfDay();
} else { } else {
from = date.atStartOfDay(); from = date.atStartOfDay();
@@ -43,6 +43,7 @@ public class MyApiStatusQueryService {
private final ApiStatusIncidentQueryRepository incidentQueryRepository; private final ApiStatusIncidentQueryRepository incidentQueryRepository;
private final DjbApistatusIncidentApiRepository incidentApiRepository; private final DjbApistatusIncidentApiRepository incidentApiRepository;
private final ApiStatusUptimeService uptimeService; private final ApiStatusUptimeService uptimeService;
private final ApiStatusCatalogService catalogService;
public List<MyApiStatusDTO> getMyApiStatuses() { public List<MyApiStatusDTO> getMyApiStatuses() {
PortalAuthenticatedUser user = SecurityUtil.getPortalAuthenticatedUser(); PortalAuthenticatedUser user = SecurityUtil.getPortalAuthenticatedUser();
@@ -58,9 +59,9 @@ public class MyApiStatusQueryService {
LocalDateTime now = ApiStatusSupport.now(); LocalDateTime now = ApiStatusSupport.now();
Map<String, DjbApistatusIncident> openByApi = mapOpenIncidents(myApis.keySet()); Map<String, DjbApistatusIncident> openByApi = mapOpenIncidents(myApis.keySet());
Set<String> underMaintenance = collectApisUnderMaintenance(myApis.keySet(), now); Set<String> underMaintenance = collectApisUnderMaintenance(myApis.keySet(), now);
Map<String, LocalDateTime> lastIncidentAt = collectLastIncidentAt(myApis.keySet(), now); int windowDays = catalogService.getWindowDays();
Map<String, Double> uptimes = uptimeService.getApiUptimeRatios( Map<String, LocalDateTime> lastIncidentAt = collectLastIncidentAt(myApis.keySet(), now, windowDays);
myApis.keySet(), ApiStatusSupport.DEFAULT_WINDOW_DAYS); Map<String, Double> uptimes = uptimeService.getApiUptimeRatios(myApis.keySet(), windowDays);
List<MyApiStatusDTO> result = new ArrayList<>(); List<MyApiStatusDTO> result = new ArrayList<>();
for (Map.Entry<String, String> entry : myApis.entrySet()) { for (Map.Entry<String, String> entry : myApis.entrySet()) {
@@ -148,9 +149,9 @@ public class MyApiStatusQueryService {
return result; return result;
} }
private Map<String, LocalDateTime> collectLastIncidentAt(Set<String> apiIds, LocalDateTime now) { private Map<String, LocalDateTime> collectLastIncidentAt(Set<String> apiIds, LocalDateTime now, int windowDays) {
LocalDateTime windowStart = now.toLocalDate() LocalDateTime windowStart = now.toLocalDate()
.minusDays(ApiStatusSupport.DEFAULT_WINDOW_DAYS - 1L).atStartOfDay(); .minusDays(windowDays - 1L).atStartOfDay();
List<DjbApistatusIncident> incidents = incidentQueryRepository List<DjbApistatusIncident> incidents = incidentQueryRepository
.findVisibleOverlapping(windowStart, now.toLocalDate().plusDays(1).atStartOfDay()).stream() .findVisibleOverlapping(windowStart, now.toLocalDate().plusDays(1).atStartOfDay()).stream()
.filter(incident -> incident.getKind() == IncidentKind.INCIDENT) .filter(incident -> incident.getKind() == IncidentKind.INCIDENT)
+10
View File
@@ -26628,6 +26628,16 @@ input[type=checkbox]:checked + .custom-checkbox {
color: #ffffff; color: #ffffff;
} }
.as-title-banner,
.as-title-banner h1,
.api-status,
.api-status h1,
.api-status h2,
.api-status h3,
.api-status h4 {
font-family: "Spoqa Han Sans Neo", "SpoqaHanSans", "Noto Sans KR", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
.as-title-banner { .as-title-banner {
margin: 44px 0 28px; margin: 44px 0 28px;
border-radius: 12px; border-radius: 12px;
File diff suppressed because one or more lines are too long
@@ -3,6 +3,19 @@
// 설계: architecture/01-api-status/02-screen-design // 설계: architecture/01-api-status/02-screen-design
// 타이틀 배너: Figma eapim-portal node 1:16 // 타이틀 배너: Figma eapim-portal node 1:16
// ============================================================ // ============================================================
@use '../abstracts/variables' as *;
// API Status 화면은 제목 포함 전부 본문 폰트(Spoqa Han Sans)를 쓴다.
// 전역 타이포가 h1~h6 에 $font-family-heading(OneShinhan 우선)을 지정하므로 페이지 범위에서 되돌린다.
.as-title-banner,
.as-title-banner h1,
.api-status,
.api-status h1,
.api-status h2,
.api-status h3,
.api-status h4 {
font-family: $font-family-primary;
}
// 타이틀 배너 — 레이아웃의 .container 안에 놓이므로 컨테이너 폭을 따른다 // 타이틀 배너 — 레이아웃의 .container 안에 놓이므로 컨테이너 폭을 따른다
.as-title-banner { .as-title-banner {
@@ -92,6 +105,9 @@
--as-pill: 999px; --as-pill: 999px;
display: grid; display: grid;
// minmax(0,1fr): grid 자식의 암묵적 min-width(auto)를 해제한다.
// 90일 막대(90개 × 최소폭)가 카드의 최소 폭을 밀어올려 좁은 화면에서 카드가 잘리는 것 방지
grid-template-columns: minmax(0, 1fr);
gap: 20px; gap: 20px;
padding: 0 0 70px; padding: 0 0 70px;
@@ -827,7 +843,14 @@
.as-filter-right { justify-content: space-between; } .as-filter-right { justify-content: space-between; }
} }
.as-index-bar { height: 36px; } // 90일 바: 셀을 균등 축소하지 않고 최소 터치 폭을 보장한 뒤 가로 스크롤
.as-index-bar {
height: 40px;
.as-index-cell { flex: 0 0 12px; }
}
.as-uptime .as-bar-chart .as-bar { flex: 0 0 12px; }
.as-issue-card { padding: 18px 16px; } .as-issue-card { padding: 18px 16px; }
.as-issue-title { font-size: 17px; } .as-issue-title { font-size: 17px; }