API Status 개선 및 조회 기간 설정 추가
This commit is contained in:
+8
-7
@@ -61,7 +61,7 @@ public class ApiStatusController {
|
||||
@GetMapping
|
||||
public ModelAndView 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());
|
||||
LocalDateTime lastFireAt = catalogService.getLastMonitorFireAt();
|
||||
mav.addObject("lastFireAt", lastFireAt);
|
||||
@@ -83,11 +83,12 @@ public class ApiStatusController {
|
||||
@RequestParam(value = "kind", required = false) String kind) {
|
||||
|
||||
LocalDate today = ApiStatusSupport.now().toLocalDate();
|
||||
int windowDays = catalogService.getWindowDays();
|
||||
|
||||
ModelAndView mav = new ModelAndView("djb/apistatus/issues");
|
||||
mav.addObject("windowDays", ApiStatusSupport.DEFAULT_WINDOW_DAYS);
|
||||
mav.addObject("windowDays", windowDays);
|
||||
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("selectedApiId", apiId);
|
||||
mav.addObject("selectedKind", kind);
|
||||
@@ -105,8 +106,8 @@ public class ApiStatusController {
|
||||
@GetMapping("/uptime.json")
|
||||
@ResponseBody
|
||||
public List<DailyStatDTO> uptime(
|
||||
@RequestParam(value = "days", defaultValue = "90") int days) {
|
||||
return uptimeService.getDailyStats(days);
|
||||
@RequestParam(value = "days", defaultValue = "0") int days) {
|
||||
return uptimeService.getDailyStats(days > 0 ? days : catalogService.getWindowDays());
|
||||
}
|
||||
|
||||
/** P3 - 진행 중 장애 */
|
||||
@@ -154,10 +155,10 @@ public class ApiStatusController {
|
||||
@GetMapping("/issues/dates.json")
|
||||
@ResponseBody
|
||||
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 = "kind", required = false) String kind) {
|
||||
return issueHistoryService.getIssueDates(days, apiId, kind);
|
||||
return issueHistoryService.getIssueDates(days > 0 ? days : catalogService.getWindowDays(), apiId, kind);
|
||||
}
|
||||
|
||||
/** P10 - 이슈 목록 (날짜/API/유형 필터) */
|
||||
|
||||
+25
@@ -38,6 +38,12 @@ public class ApiStatusCatalogService {
|
||||
/** 실서버(DAPM) QRTZ_JOB_DETAILS 실사값 (2026-07-31) */
|
||||
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 PortalPropertyService portalPropertyService;
|
||||
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).
|
||||
* 실행 시각(fire)과 달리 상태 변화가 있을 때만 갱신된다.
|
||||
|
||||
+2
-1
@@ -32,6 +32,7 @@ public class ApiStatusIssueHistoryService {
|
||||
|
||||
private final ApiStatusIncidentQueryRepository incidentQueryRepository;
|
||||
private final ApiStatusAssembler assembler;
|
||||
private final ApiStatusCatalogService catalogService;
|
||||
|
||||
/**
|
||||
* P9 - 90일 인덱스바용 일자별 이슈 집계. 이슈가 여러 날에 걸치면 걸친 날짜 모두에 집계한다.
|
||||
@@ -97,7 +98,7 @@ public class ApiStatusIssueHistoryService {
|
||||
|
||||
if (date == null) {
|
||||
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();
|
||||
} else {
|
||||
from = date.atStartOfDay();
|
||||
|
||||
+6
-5
@@ -43,6 +43,7 @@ public class MyApiStatusQueryService {
|
||||
private final ApiStatusIncidentQueryRepository incidentQueryRepository;
|
||||
private final DjbApistatusIncidentApiRepository incidentApiRepository;
|
||||
private final ApiStatusUptimeService uptimeService;
|
||||
private final ApiStatusCatalogService catalogService;
|
||||
|
||||
public List<MyApiStatusDTO> getMyApiStatuses() {
|
||||
PortalAuthenticatedUser user = SecurityUtil.getPortalAuthenticatedUser();
|
||||
@@ -58,9 +59,9 @@ public class MyApiStatusQueryService {
|
||||
LocalDateTime now = ApiStatusSupport.now();
|
||||
Map<String, DjbApistatusIncident> openByApi = mapOpenIncidents(myApis.keySet());
|
||||
Set<String> underMaintenance = collectApisUnderMaintenance(myApis.keySet(), now);
|
||||
Map<String, LocalDateTime> lastIncidentAt = collectLastIncidentAt(myApis.keySet(), now);
|
||||
Map<String, Double> uptimes = uptimeService.getApiUptimeRatios(
|
||||
myApis.keySet(), ApiStatusSupport.DEFAULT_WINDOW_DAYS);
|
||||
int windowDays = catalogService.getWindowDays();
|
||||
Map<String, LocalDateTime> lastIncidentAt = collectLastIncidentAt(myApis.keySet(), now, windowDays);
|
||||
Map<String, Double> uptimes = uptimeService.getApiUptimeRatios(myApis.keySet(), windowDays);
|
||||
|
||||
List<MyApiStatusDTO> result = new ArrayList<>();
|
||||
for (Map.Entry<String, String> entry : myApis.entrySet()) {
|
||||
@@ -148,9 +149,9 @@ public class MyApiStatusQueryService {
|
||||
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()
|
||||
.minusDays(ApiStatusSupport.DEFAULT_WINDOW_DAYS - 1L).atStartOfDay();
|
||||
.minusDays(windowDays - 1L).atStartOfDay();
|
||||
List<DjbApistatusIncident> incidents = incidentQueryRepository
|
||||
.findVisibleOverlapping(windowStart, now.toLocalDate().plusDays(1).atStartOfDay()).stream()
|
||||
.filter(incident -> incident.getKind() == IncidentKind.INCIDENT)
|
||||
|
||||
@@ -26628,6 +26628,16 @@ input[type=checkbox]:checked + .custom-checkbox {
|
||||
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 {
|
||||
margin: 44px 0 28px;
|
||||
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
|
||||
// 타이틀 배너: 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 안에 놓이므로 컨테이너 폭을 따른다
|
||||
.as-title-banner {
|
||||
@@ -92,6 +105,9 @@
|
||||
--as-pill: 999px;
|
||||
|
||||
display: grid;
|
||||
// minmax(0,1fr): grid 자식의 암묵적 min-width(auto)를 해제한다.
|
||||
// 90일 막대(90개 × 최소폭)가 카드의 최소 폭을 밀어올려 좁은 화면에서 카드가 잘리는 것 방지
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
gap: 20px;
|
||||
padding: 0 0 70px;
|
||||
|
||||
@@ -827,7 +843,14 @@
|
||||
.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-title { font-size: 17px; }
|
||||
|
||||
Reference in New Issue
Block a user