Skip to content

Commit 4acbd1d

Browse files
Merge pull request #47 from DevKor-github/develop
chore: Service Insight 모바일 호환성 개선
2 parents f76972e + 4fedfe6 commit 4acbd1d

8 files changed

Lines changed: 845 additions & 28 deletions

File tree

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

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
import apu.saerok_admin.web.view.ToastMessage;
77
import com.fasterxml.jackson.core.JsonProcessingException;
88
import com.fasterxml.jackson.databind.ObjectMapper;
9+
import com.fasterxml.jackson.databind.SerializationFeature;
10+
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
911
import java.util.ArrayList;
1012
import java.util.List;
1113
import org.slf4j.Logger;
@@ -27,7 +29,10 @@ public class ServiceInsightController {
2729

2830
public ServiceInsightController(ServiceInsightService serviceInsightService, ObjectMapper objectMapper) {
2931
this.serviceInsightService = serviceInsightService;
30-
this.objectMapper = objectMapper;
32+
// ObjectMapper 설정 강화
33+
this.objectMapper = objectMapper.copy()
34+
.registerModule(new JavaTimeModule())
35+
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
3136
}
3237

3338
@GetMapping("/service-insight")
@@ -43,6 +48,8 @@ public String serviceInsight(Model model) {
4348
ServiceInsightViewModel viewModel;
4449
try {
4550
viewModel = serviceInsightService.loadViewModel();
51+
log.info("Successfully loaded service insight view model with {} metrics",
52+
viewModel.metricOptions().size());
4653
} catch (RestClientResponseException exception) {
4754
log.warn(
4855
"Failed to load service insight stats. status={}, body={}",
@@ -59,7 +66,11 @@ public String serviceInsight(Model model) {
5966
}
6067

6168
model.addAttribute("serviceInsight", viewModel);
62-
model.addAttribute("chartDataJson", toJson(viewModel));
69+
String chartDataJson = toJson(viewModel);
70+
model.addAttribute("chartDataJson", chartDataJson);
71+
72+
log.debug("Chart data JSON length: {}", chartDataJson.length());
73+
6374
return "service-insight/index";
6475
}
6576

@@ -92,10 +103,12 @@ private void attachErrorToast(Model model) {
92103

93104
private String toJson(ServiceInsightViewModel viewModel) {
94105
try {
95-
return objectMapper.writeValueAsString(viewModel);
106+
String json = objectMapper.writeValueAsString(viewModel);
107+
log.debug("Serialized view model to JSON successfully");
108+
return json;
96109
} catch (JsonProcessingException exception) {
97-
log.warn("Failed to serialize service insight payload.", exception);
110+
log.error("Failed to serialize service insight payload.", exception);
98111
return "{\"metricOptions\":[],\"series\":[],\"componentLabels\":{}}";
99112
}
100113
}
101-
}
114+
}
Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,43 @@
11
package apu.saerok_admin.web.view;
22

33
import apu.saerok_admin.infra.stat.StatMetric.MetricUnit;
4+
import com.fasterxml.jackson.annotation.JsonProperty;
45
import java.time.LocalDate;
56
import java.util.List;
67
import java.util.Map;
78

89
public record ServiceInsightViewModel(
9-
List<MetricOption> metricOptions,
10-
List<Series> series,
11-
Map<String, Map<String, String>> componentLabels
10+
@JsonProperty("metricOptions") List<MetricOption> metricOptions,
11+
@JsonProperty("series") List<Series> series,
12+
@JsonProperty("componentLabels") Map<String, Map<String, String>> componentLabels
1213
) {
1314

1415
public record MetricOption(
15-
String metric,
16-
String label,
17-
String description,
18-
MetricUnit unit,
19-
boolean multiSeries,
20-
boolean defaultActive
16+
@JsonProperty("metric") String metric,
17+
@JsonProperty("label") String label,
18+
@JsonProperty("description") String description,
19+
@JsonProperty("unit") MetricUnit unit,
20+
@JsonProperty("multiSeries") boolean multiSeries,
21+
@JsonProperty("defaultActive") boolean defaultActive
2122
) {
2223
}
2324

2425
public record Series(
25-
String metric,
26-
List<Point> points,
27-
List<ComponentSeries> components
26+
@JsonProperty("metric") String metric,
27+
@JsonProperty("points") List<Point> points,
28+
@JsonProperty("components") List<ComponentSeries> components
2829
) {
2930
}
3031

3132
public record Point(
32-
LocalDate date,
33-
double value
33+
@JsonProperty("date") LocalDate date,
34+
@JsonProperty("value") double value
3435
) {
3536
}
3637

3738
public record ComponentSeries(
38-
String key,
39-
List<Point> points
39+
@JsonProperty("key") String key,
40+
@JsonProperty("points") List<Point> points
4041
) {
4142
}
42-
}
43+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/* ===== Service Insight: 모바일 전용 오버라이드 =====
2+
- 사이드바 숨김, DnD 힌트 제거, 차트 컴팩트, FAB(모바일만) 등
3+
*/
4+
5+
.service-insight__container { height: auto !important; overflow: visible !important; }
6+
.service-insight__metrics,
7+
.service-insight__chart { height: auto !important; min-height: 0 !important; overflow: visible !important; }
8+
9+
/* 사이드바는 모바일에서 숨김(바텀시트 이용) */
10+
.service-insight__metrics { display: none !important; }
11+
12+
/* DnD affordance 제거 */
13+
.service-insight .si-plot__datasets,
14+
.service-insight .si-dropzone { display: none !important; }
15+
16+
/* 차트 높이 컴팩트 */
17+
.si-plots .si-plot__surface { height: 280px !important; }
18+
19+
/* FAB: 모바일에서만 표출 */
20+
#si-fab {
21+
display: inline-flex !important;
22+
position: fixed; right: max(16px, env(safe-area-inset-right));
23+
bottom: calc(max(16px, env(safe-area-inset-bottom)) + 8px);
24+
width: 56px; height: 56px; z-index: 1055;
25+
align-items: center; justify-content: center;
26+
border-radius: 50%; padding: 0; line-height: 1;
27+
}
28+
29+
/* 바텀시트 */
30+
.offcanvas.offcanvas-bottom#siDataPicker {
31+
height: 75vh; border-top-left-radius: 1rem; border-top-right-radius: 1rem;
32+
box-shadow: var(--shadow-overlay);
33+
}
34+
.offcanvas.offcanvas-bottom#siDataPicker .offcanvas-header { padding: 0.75rem 1rem; }
35+
.offcanvas.offcanvas-bottom#siDataPicker .offcanvas-body { padding: 0.5rem 1rem 1rem; overflow: auto; }
36+
37+
/* 툴바 줄바꿈 */
38+
.si-plots__toolbar .si-actions { display: flex; gap: .5rem; flex-wrap: wrap; }
39+
40+
/* 안전 여백 */
41+
.service-insight { padding-bottom: max(72px, env(safe-area-inset-bottom)); }
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
/* ===== resources/static/css/service-insight.target.css ===== */
2+
/* ===== 전역(PC/모바일 공통) ===== */
3+
4+
/* FAB는 PC 기본 숨김, 모바일 전용 CSS에서만 표출 */
5+
#si-fab { display: none !important; }
6+
7+
/* "플롯 n 데이터 선택" – 인라인 타이틀 */
8+
.si-targettitle {
9+
display: inline-flex;
10+
align-items: center;
11+
gap: .55rem;
12+
font-size: 1rem;
13+
font-weight: 800;
14+
letter-spacing: -0.2px;
15+
}
16+
.si-targettitle__prefix,
17+
.si-targettitle__suffix { font-weight: 800; }
18+
19+
/* 숫자 Select: 클릭감 강화 */
20+
.si-targettitle__select {
21+
-webkit-appearance: none;
22+
appearance: none;
23+
cursor: pointer;
24+
min-width: 4.5rem;
25+
width: auto;
26+
border-radius: 999px;
27+
padding-inline: 1.25rem 2rem; /* 오른쪽 캐럿 공간 */
28+
padding-block: .35rem;
29+
line-height: 1.2;
30+
border: 1px solid var(--border-strong, #c4cbd3);
31+
background:
32+
linear-gradient(to bottom, rgba(0,0,0,0.02), rgba(0,0,0,0)) padding-box,
33+
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 16 16'%3E%3Cpath fill='%23666' d='M3.2 6l4.8 5L12.8 6z'/%3E%3C/svg%3E")
34+
no-repeat right .6rem center/12px 12px;
35+
box-shadow: 0 1px 0 rgba(0,0,0,.02);
36+
transition: box-shadow .15s ease, transform .04s ease, border-color .15s ease;
37+
font-weight: 700;
38+
}
39+
.si-targettitle__select:hover {
40+
border-color: var(--accent, #0d6efd);
41+
box-shadow: 0 0 0 .2rem rgba(13,110,253,.12);
42+
}
43+
.si-targettitle__select:active { transform: translateY(1px); }
44+
.si-targettitle__select:focus {
45+
outline: none;
46+
border-color: var(--accent, #0d6efd);
47+
box-shadow: 0 0 0 .24rem rgba(13,110,253,.16);
48+
}
49+
50+
/* 데이터 패널 레이아웃 */
51+
.si-data-panel { display: flex; flex-direction: column; gap: .75rem; }
52+
53+
/* ========== ✅ 체크 인디케이터: "오른쪽 단일"로 통일 ========== */
54+
/* (1) 좌측 체크를 만들던 예전 커스텀 규칙들 강제 무력화 */
55+
.si-data-panel .si-chip::before,
56+
.si-data-panel [data-si-chip]::before,
57+
.si-data-panel .si-chip--toggle::before { content: none !important; }
58+
59+
/* (2) 우측 ::after 하나만 사용 (PC/모바일 공통) */
60+
.si-data-panel .si-chip,
61+
.si-data-panel [data-si-chip],
62+
.si-data-panel .si-chip--toggle {
63+
position: relative;
64+
padding-right: 28px; /* 오른쪽 체크 공간 확보 */
65+
}
66+
67+
.si-data-panel .si-chip::after,
68+
.si-data-panel [data-si-chip]::after,
69+
.si-data-panel .si-chip--toggle::after {
70+
content: "";
71+
position: absolute;
72+
right: 8px;
73+
top: 50%;
74+
transform: translateY(-50%);
75+
width: 16px; height: 16px;
76+
border-radius: 999px;
77+
border: 1.5px solid var(--border-subtle, #cfd4d9);
78+
background: var(--surface-primary, #fff);
79+
pointer-events: none; /* 클릭 간섭 방지 */
80+
transition: background-color .12s ease, border-color .12s ease, box-shadow .12s ease;
81+
}
82+
83+
/* (3) 활성 조건: .is-active 또는 [data-checked="true"] 중 하나라도 true면 체크 표시 */
84+
.si-data-panel .si-chip.is-active::after,
85+
.si-data-panel [data-si-chip].is-active::after,
86+
.si-data-panel .si-chip--toggle.is-active::after,
87+
.si-data-panel .si-chip[data-checked="true"]::after,
88+
.si-data-panel [data-si-chip][data-checked="true"]::after,
89+
.si-data-panel .si-chip--toggle[data-checked="true"]::after {
90+
content: "✓";
91+
font-weight: 700;
92+
font-size: 12px;
93+
text-align: center;
94+
line-height: 16px;
95+
color: var(--accent-contrast, #fff);
96+
background: var(--accent, #0d6efd);
97+
border-color: var(--accent, #0d6efd);
98+
box-shadow: 0 0 0 2px rgba(13,110,253,.15);
99+
}
100+
101+
/* 키보드 포커스 접근성 */
102+
.si-chip.si-chip--toggle:focus-visible {
103+
outline: none;
104+
box-shadow: 0 0 0 .2rem rgba(59,130,246,.20);
105+
border-color: rgba(59,130,246,.35);
106+
}
107+
108+
/* 모바일 폰트 톤 조정 */
109+
@media (max-width: 991.98px) {
110+
.si-targettitle { font-size: 1.05rem; }
111+
}

0 commit comments

Comments
 (0)