Skip to content

Commit a8dfd50

Browse files
Merge pull request #58 from DevKor-github/develop
운영 배포
2 parents 7e255df + 3e82d60 commit a8dfd50

10 files changed

Lines changed: 641 additions & 29 deletions

File tree

src/main/java/apu/saerok_admin/infra/stat/AdminStatClient.java

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
import apu.saerok_admin.infra.SaerokApiProps;
44
import apu.saerok_admin.infra.stat.dto.StatSeriesResponse;
55
import java.net.URI;
6+
import java.time.LocalDate;
7+
import java.time.format.DateTimeFormatter;
68
import java.util.Collection;
79
import java.util.Objects;
810
import org.springframework.stereotype.Component;
@@ -23,13 +25,15 @@ public AdminStatClient(RestClient saerokRestClient, SaerokApiProps saerokApiProp
2325
this.missingPrefixSegments = saerokApiProps.missingPrefixSegments().toArray(new String[0]);
2426
}
2527

26-
public StatSeriesResponse fetchSeries(Collection<StatMetric> metrics) {
28+
private static final DateTimeFormatter ISO_DATE = DateTimeFormatter.ISO_DATE;
29+
30+
public StatSeriesResponse fetchSeries(Collection<StatMetric> metrics, LocalDate startDate, LocalDate endDate) {
2731
if (metrics == null || metrics.isEmpty()) {
2832
throw new IllegalArgumentException("metrics must not be empty");
2933
}
3034

3135
StatSeriesResponse response = saerokRestClient.get()
32-
.uri(uriBuilder -> buildSeriesUri(uriBuilder, metrics))
36+
.uri(uriBuilder -> buildSeriesUri(uriBuilder, metrics, startDate, endDate))
3337
.retrieve()
3438
.body(StatSeriesResponse.class);
3539

@@ -40,7 +44,7 @@ public StatSeriesResponse fetchSeries(Collection<StatMetric> metrics) {
4044
return response;
4145
}
4246

43-
private URI buildSeriesUri(UriBuilder builder, Collection<StatMetric> metrics) {
47+
private URI buildSeriesUri(UriBuilder builder, Collection<StatMetric> metrics, LocalDate startDate, LocalDate endDate) {
4448
if (missingPrefixSegments.length > 0) {
4549
builder.pathSegment(missingPrefixSegments);
4650
}
@@ -52,6 +56,16 @@ private URI buildSeriesUri(UriBuilder builder, Collection<StatMetric> metrics) {
5256
.map(Enum::name)
5357
.forEach(metric -> builder.queryParam("metric", metric));
5458

59+
if (startDate != null || endDate != null) {
60+
builder.queryParam("period", buildPeriodQuery(startDate, endDate));
61+
}
62+
5563
return builder.build();
5664
}
65+
66+
private String buildPeriodQuery(LocalDate startDate, LocalDate endDate) {
67+
String startToken = startDate != null ? ISO_DATE.format(startDate) : "";
68+
String endToken = endDate != null ? ISO_DATE.format(endDate) : "";
69+
return startToken + "," + endToken;
70+
}
5771
}

src/main/java/apu/saerok_admin/web/ServiceInsightController.java

Lines changed: 136 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
package apu.saerok_admin.web;
22

3+
import apu.saerok_admin.web.serviceinsight.ServiceInsightAjaxResponse;
4+
import apu.saerok_admin.web.serviceinsight.ServiceInsightQuery;
5+
import apu.saerok_admin.web.serviceinsight.ServiceInsightRangePreset;
36
import apu.saerok_admin.web.serviceinsight.ServiceInsightService;
47
import apu.saerok_admin.web.view.Breadcrumb;
58
import apu.saerok_admin.web.view.ServiceInsightViewModel;
@@ -8,13 +11,19 @@
811
import com.fasterxml.jackson.databind.ObjectMapper;
912
import com.fasterxml.jackson.databind.SerializationFeature;
1013
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
14+
import java.time.LocalDate;
15+
import java.time.ZoneId;
1116
import java.util.ArrayList;
1217
import java.util.List;
1318
import org.slf4j.Logger;
1419
import org.slf4j.LoggerFactory;
20+
import org.springframework.format.annotation.DateTimeFormat;
21+
import org.springframework.http.MediaType;
22+
import org.springframework.http.ResponseEntity;
1523
import org.springframework.stereotype.Controller;
1624
import org.springframework.ui.Model;
1725
import org.springframework.web.bind.annotation.GetMapping;
26+
import org.springframework.web.bind.annotation.RequestParam;
1827
import org.springframework.web.client.RestClientException;
1928
import org.springframework.web.client.RestClientResponseException;
2029

@@ -23,6 +32,8 @@ public class ServiceInsightController {
2332

2433
private static final Logger log = LoggerFactory.getLogger(ServiceInsightController.class);
2534
private static final String ERROR_TOAST_ID = "toastServiceInsightError";
35+
private static final ZoneId DEFAULT_ZONE = ZoneId.of("Asia/Seoul");
36+
private static final ServiceInsightRangePreset DEFAULT_PRESET = ServiceInsightRangePreset.LAST_14_DAYS;
2637

2738
private final ServiceInsightService serviceInsightService;
2839
private final ObjectMapper objectMapper;
@@ -35,8 +46,15 @@ public ServiceInsightController(ServiceInsightService serviceInsightService, Obj
3546
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
3647
}
3748

38-
@GetMapping("/service-insight")
39-
public String serviceInsight(Model model) {
49+
@GetMapping(value = "/service-insight", produces = MediaType.TEXT_HTML_VALUE)
50+
public String serviceInsight(
51+
Model model,
52+
@RequestParam(value = "range", required = false) String rangeParam,
53+
@RequestParam(value = "startDate", required = false)
54+
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate,
55+
@RequestParam(value = "endDate", required = false)
56+
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate
57+
) {
4058
model.addAttribute("pageTitle", "서비스 인사이트");
4159
model.addAttribute("activeMenu", "serviceInsight");
4260
model.addAttribute("breadcrumbs", List.of(
@@ -45,41 +63,133 @@ public String serviceInsight(Model model) {
4563
));
4664
ensureToastMessages(model);
4765

48-
ServiceInsightViewModel viewModel;
49-
try {
50-
viewModel = serviceInsightService.loadViewModel();
51-
log.info("Successfully loaded service insight view model with {} metrics",
52-
viewModel.metricOptions().size());
53-
} catch (RestClientResponseException exception) {
54-
log.warn(
55-
"Failed to load service insight stats. status={}, body={}",
56-
exception.getStatusCode(),
57-
exception.getResponseBodyAsString(),
58-
exception
59-
);
60-
viewModel = serviceInsightService.defaultViewModel();
61-
attachErrorToast(model);
62-
} catch (RestClientException | IllegalStateException exception) {
63-
log.warn("Failed to load service insight stats.", exception);
64-
viewModel = serviceInsightService.defaultViewModel();
66+
PageData pageData = loadPageData(rangeParam, startDate, endDate);
67+
ServiceInsightViewModel viewModel = pageData.viewModel();
68+
RangeSelection rangeSelection = pageData.rangeSelection();
69+
if (pageData.hadError()) {
6570
attachErrorToast(model);
6671
}
6772

6873
model.addAttribute("serviceInsight", viewModel);
6974
String chartDataJson = toJson(viewModel);
7075
model.addAttribute("chartDataJson", chartDataJson);
76+
model.addAttribute("rangeQuickOptions", ServiceInsightRangePreset.quickSelections());
77+
model.addAttribute("selectedRange", rangeSelection.preset().paramValue());
78+
model.addAttribute("customRangeActive", rangeSelection.preset() == ServiceInsightRangePreset.CUSTOM);
79+
model.addAttribute("selectedStartDate", rangeSelection.query().startDate());
80+
model.addAttribute("selectedEndDate", rangeSelection.query().endDate());
7181

7282
log.debug("Chart data JSON length: {}", chartDataJson.length());
7383

7484
return "service-insight/index";
7585
}
7686

87+
@GetMapping(value = "/service-insight", produces = MediaType.APPLICATION_JSON_VALUE)
88+
public ResponseEntity<ServiceInsightAjaxResponse> serviceInsightData(
89+
@RequestParam(value = "range", required = false) String rangeParam,
90+
@RequestParam(value = "startDate", required = false)
91+
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate,
92+
@RequestParam(value = "endDate", required = false)
93+
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate
94+
) {
95+
PageData pageData = loadPageData(rangeParam, startDate, endDate);
96+
RangeSelection rangeSelection = pageData.rangeSelection();
97+
98+
ServiceInsightAjaxResponse response = new ServiceInsightAjaxResponse(
99+
pageData.viewModel(),
100+
rangeSelection.preset().paramValue(),
101+
rangeSelection.preset() == ServiceInsightRangePreset.CUSTOM,
102+
rangeSelection.query().startDate(),
103+
rangeSelection.query().endDate(),
104+
pageData.hadError()
105+
);
106+
107+
return ResponseEntity.ok(response);
108+
}
109+
77110
private void ensureToastMessages(Model model) {
78111
if (!model.containsAttribute("toastMessages")) {
79112
model.addAttribute("toastMessages", List.of());
80113
}
81114
}
82115

116+
private PageData loadPageData(String rangeParam, LocalDate startDate, LocalDate endDate) {
117+
RangeSelection rangeSelection = resolveRange(rangeParam, startDate, endDate);
118+
119+
try {
120+
ServiceInsightViewModel viewModel = serviceInsightService.loadViewModel(rangeSelection.query());
121+
log.info("Successfully loaded service insight view model with {} metrics (range: {} - {}, preset: {})",
122+
viewModel.metricOptions().size(),
123+
rangeSelection.query().startDate(),
124+
rangeSelection.query().endDate(),
125+
rangeSelection.preset().name());
126+
return new PageData(rangeSelection, viewModel, false);
127+
} catch (RestClientResponseException exception) {
128+
log.warn(
129+
"Failed to load service insight stats. status={}, body={}",
130+
exception.getStatusCode(),
131+
exception.getResponseBodyAsString(),
132+
exception
133+
);
134+
} catch (RestClientException | IllegalStateException exception) {
135+
log.warn("Failed to load service insight stats.", exception);
136+
}
137+
138+
ServiceInsightViewModel fallback = serviceInsightService.defaultViewModel();
139+
return new PageData(rangeSelection, fallback, true);
140+
}
141+
142+
private RangeSelection resolveRange(String rangeParam, LocalDate startDate, LocalDate endDate) {
143+
LocalDate today = LocalDate.now(DEFAULT_ZONE);
144+
145+
ServiceInsightRangePreset requestedPreset = ServiceInsightRangePreset.fromParameter(rangeParam)
146+
.orElse(DEFAULT_PRESET);
147+
148+
if (startDate != null || endDate != null) {
149+
requestedPreset = ServiceInsightRangePreset.CUSTOM;
150+
}
151+
152+
if (requestedPreset == ServiceInsightRangePreset.ALL) {
153+
return new RangeSelection(ServiceInsightRangePreset.ALL, ServiceInsightQuery.all());
154+
}
155+
156+
if (requestedPreset == ServiceInsightRangePreset.CUSTOM) {
157+
if (startDate == null || endDate == null) {
158+
log.debug("Incomplete custom range supplied (start={}, end={}), falling back to default preset {}",
159+
startDate,
160+
endDate,
161+
DEFAULT_PRESET.name());
162+
return buildPresetSelection(DEFAULT_PRESET, today);
163+
}
164+
165+
LocalDate effectiveStart = startDate;
166+
LocalDate effectiveEnd = endDate;
167+
168+
if (effectiveEnd.isBefore(effectiveStart)) {
169+
effectiveStart = endDate;
170+
effectiveEnd = startDate;
171+
}
172+
173+
if (effectiveEnd.isAfter(today)) {
174+
effectiveEnd = today;
175+
}
176+
177+
if (effectiveStart.isAfter(effectiveEnd)) {
178+
effectiveStart = effectiveEnd;
179+
}
180+
181+
return new RangeSelection(ServiceInsightRangePreset.CUSTOM, new ServiceInsightQuery(effectiveStart, effectiveEnd));
182+
}
183+
184+
return buildPresetSelection(requestedPreset, today);
185+
}
186+
187+
private RangeSelection buildPresetSelection(ServiceInsightRangePreset preset, LocalDate today) {
188+
return preset.toWindow(today)
189+
.map(window -> new RangeSelection(preset, new ServiceInsightQuery(window.startDate(), window.endDate())))
190+
.orElseGet(() -> new RangeSelection(ServiceInsightRangePreset.ALL, ServiceInsightQuery.all()));
191+
}
192+
83193
private void attachErrorToast(Model model) {
84194
ToastMessage errorToast = new ToastMessage(
85195
ERROR_TOAST_ID,
@@ -111,4 +221,10 @@ private String toJson(ServiceInsightViewModel viewModel) {
111221
return "{\"metricOptions\":[],\"series\":[],\"componentLabels\":{}}";
112222
}
113223
}
114-
}
224+
225+
private record RangeSelection(ServiceInsightRangePreset preset, ServiceInsightQuery query) {
226+
}
227+
228+
private record PageData(RangeSelection rangeSelection, ServiceInsightViewModel viewModel, boolean hadError) {
229+
}
230+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package apu.saerok_admin.web.serviceinsight;
2+
3+
import apu.saerok_admin.web.view.ServiceInsightViewModel;
4+
import com.fasterxml.jackson.annotation.JsonProperty;
5+
import java.time.LocalDate;
6+
7+
public record ServiceInsightAjaxResponse(
8+
@JsonProperty("viewModel") ServiceInsightViewModel viewModel,
9+
@JsonProperty("selectedRange") String selectedRange,
10+
@JsonProperty("customRangeActive") boolean customRangeActive,
11+
@JsonProperty("startDate") LocalDate startDate,
12+
@JsonProperty("endDate") LocalDate endDate,
13+
@JsonProperty("error") boolean error
14+
) {
15+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package apu.saerok_admin.web.serviceinsight;
2+
3+
import java.time.LocalDate;
4+
5+
public record ServiceInsightQuery(LocalDate startDate, LocalDate endDate) {
6+
7+
public static ServiceInsightQuery all() {
8+
return new ServiceInsightQuery(null, null);
9+
}
10+
11+
public boolean hasRange() {
12+
return startDate != null && endDate != null;
13+
}
14+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package apu.saerok_admin.web.serviceinsight;
2+
3+
import java.time.LocalDate;
4+
import java.util.Arrays;
5+
import java.util.List;
6+
import java.util.Locale;
7+
import java.util.Optional;
8+
9+
public enum ServiceInsightRangePreset {
10+
11+
LAST_7_DAYS("recent-7", "최근 1주", 7),
12+
LAST_14_DAYS("recent-14", "최근 2주", 14),
13+
LAST_30_DAYS("recent-30", "최근 1달", 30),
14+
ALL("all", "전체", null),
15+
CUSTOM("custom", "사용자 지정", null);
16+
17+
private final String paramValue;
18+
private final String displayLabel;
19+
private final Integer days;
20+
21+
ServiceInsightRangePreset(String paramValue, String displayLabel, Integer days) {
22+
this.paramValue = paramValue;
23+
this.displayLabel = displayLabel;
24+
this.days = days;
25+
}
26+
27+
public String paramValue() {
28+
return paramValue;
29+
}
30+
31+
public String displayLabel() {
32+
return displayLabel;
33+
}
34+
35+
public boolean isCustom() {
36+
return this == CUSTOM;
37+
}
38+
39+
public boolean isAll() {
40+
return this == ALL;
41+
}
42+
43+
public Optional<RangeWindow> toWindow(LocalDate today) {
44+
if (days == null) {
45+
return Optional.empty();
46+
}
47+
if (today == null) {
48+
return Optional.empty();
49+
}
50+
if (days <= 0) {
51+
return Optional.empty();
52+
}
53+
LocalDate endDate = today;
54+
LocalDate startDate = today.minusDays(days - 1L);
55+
return Optional.of(new RangeWindow(startDate, endDate));
56+
}
57+
58+
public static Optional<ServiceInsightRangePreset> fromParameter(String parameter) {
59+
if (parameter == null || parameter.isBlank()) {
60+
return Optional.empty();
61+
}
62+
String normalized = parameter.trim().toLowerCase(Locale.ROOT);
63+
return Arrays.stream(values())
64+
.filter(preset -> preset.paramValue.equalsIgnoreCase(normalized) || preset.name().equalsIgnoreCase(normalized))
65+
.findFirst();
66+
}
67+
68+
public static List<ServiceInsightRangePreset> quickSelections() {
69+
return List.of(LAST_7_DAYS, LAST_14_DAYS, LAST_30_DAYS, ALL);
70+
}
71+
72+
public record RangeWindow(LocalDate startDate, LocalDate endDate) {
73+
}
74+
}

0 commit comments

Comments
 (0)