@dssp/dcsp 1.0.2 → 1.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/dist-client/components/kpi-2d-lookup-chart.d.ts +50 -0
  2. package/dist-client/components/kpi-2d-lookup-chart.js +263 -0
  3. package/dist-client/components/kpi-2d-lookup-chart.js.map +1 -0
  4. package/dist-client/components/kpi-boxplot-chart.d.ts +31 -0
  5. package/dist-client/components/kpi-boxplot-chart.js +306 -0
  6. package/dist-client/components/kpi-boxplot-chart.js.map +1 -0
  7. package/dist-client/components/kpi-lookup-chart.d.ts +58 -0
  8. package/dist-client/components/kpi-lookup-chart.js +464 -0
  9. package/dist-client/components/kpi-lookup-chart.js.map +1 -0
  10. package/dist-client/components/kpi-mini-trend-chart.d.ts +14 -0
  11. package/dist-client/components/kpi-mini-trend-chart.js +180 -0
  12. package/dist-client/components/kpi-mini-trend-chart.js.map +1 -0
  13. package/dist-client/components/kpi-radar-chart.d.ts +17 -0
  14. package/dist-client/components/kpi-radar-chart.js +259 -0
  15. package/dist-client/components/kpi-radar-chart.js.map +1 -0
  16. package/dist-client/components/kpi-single-boxplot-chart.d.ts +36 -0
  17. package/dist-client/components/kpi-single-boxplot-chart.js +417 -0
  18. package/dist-client/components/kpi-single-boxplot-chart.js.map +1 -0
  19. package/dist-client/components/kpi-step-lookup-chart.d.ts +33 -0
  20. package/dist-client/components/kpi-step-lookup-chart.js +181 -0
  21. package/dist-client/components/kpi-step-lookup-chart.js.map +1 -0
  22. package/dist-client/components/kpi-trend-chart.d.ts +25 -0
  23. package/dist-client/components/kpi-trend-chart.js +241 -0
  24. package/dist-client/components/kpi-trend-chart.js.map +1 -0
  25. package/dist-client/google-map/common-google-map.d.ts +35 -0
  26. package/dist-client/google-map/common-google-map.js +349 -0
  27. package/dist-client/google-map/common-google-map.js.map +1 -0
  28. package/dist-client/google-map/google-map-loader.d.ts +6 -0
  29. package/dist-client/google-map/google-map-loader.js +23 -0
  30. package/dist-client/google-map/google-map-loader.js.map +1 -0
  31. package/dist-client/pages/kpi-admin/dssp-kpi-overview.d.ts +47 -0
  32. package/dist-client/pages/kpi-admin/dssp-kpi-overview.js +393 -0
  33. package/dist-client/pages/kpi-admin/dssp-kpi-overview.js.map +1 -0
  34. package/dist-client/pages/kpi-admin/kpi-system-guide.d.ts +18 -0
  35. package/dist-client/pages/kpi-admin/kpi-system-guide.js +696 -0
  36. package/dist-client/pages/kpi-admin/kpi-system-guide.js.map +1 -0
  37. package/dist-client/pages/kpi-dashboard/components/kpi-left-panel.d.ts +46 -0
  38. package/dist-client/pages/kpi-dashboard/components/kpi-left-panel.js +945 -0
  39. package/dist-client/pages/kpi-dashboard/components/kpi-left-panel.js.map +1 -0
  40. package/dist-client/pages/kpi-dashboard/components/kpi-map-panel.d.ts +34 -0
  41. package/dist-client/pages/kpi-dashboard/components/kpi-map-panel.js +238 -0
  42. package/dist-client/pages/kpi-dashboard/components/kpi-map-panel.js.map +1 -0
  43. package/dist-client/pages/kpi-dashboard/components/kpi-region-popup.d.ts +37 -0
  44. package/dist-client/pages/kpi-dashboard/components/kpi-region-popup.js +689 -0
  45. package/dist-client/pages/kpi-dashboard/components/kpi-region-popup.js.map +1 -0
  46. package/dist-client/pages/kpi-dashboard/kpi-dashboard-map.d.ts +57 -0
  47. package/dist-client/pages/kpi-dashboard/kpi-dashboard-map.js +866 -0
  48. package/dist-client/pages/kpi-dashboard/kpi-dashboard-map.js.map +1 -0
  49. package/dist-client/pages/project-complete-tabs/pc-tab1-plan.d.ts +69 -0
  50. package/dist-client/pages/project-complete-tabs/pc-tab1-plan.js +790 -0
  51. package/dist-client/pages/project-complete-tabs/pc-tab1-plan.js.map +1 -0
  52. package/dist-client/pages/project-complete-tabs/pc-tab2-rating.d.ts +22 -0
  53. package/dist-client/pages/project-complete-tabs/pc-tab2-rating.js +344 -0
  54. package/dist-client/pages/project-complete-tabs/pc-tab2-rating.js.map +1 -0
  55. package/dist-client/pages/project-complete-tabs/pc-tab3-upload.d.ts +21 -0
  56. package/dist-client/pages/project-complete-tabs/pc-tab3-upload.js +335 -0
  57. package/dist-client/pages/project-complete-tabs/pc-tab3-upload.js.map +1 -0
  58. package/dist-client/pages/project-complete-tabs/pc-tab4-monthly.d.ts +51 -0
  59. package/dist-client/pages/project-complete-tabs/pc-tab4-monthly.js +693 -0
  60. package/dist-client/pages/project-complete-tabs/pc-tab4-monthly.js.map +1 -0
  61. package/dist-client/pages/sv-project-complete.d.ts +31 -0
  62. package/dist-client/pages/sv-project-complete.js +364 -0
  63. package/dist-client/pages/sv-project-complete.js.map +1 -0
  64. package/dist-client/pages/sv-project-detail.d.ts +73 -0
  65. package/dist-client/pages/sv-project-detail.js +1387 -0
  66. package/dist-client/pages/sv-project-detail.js.map +1 -0
  67. package/dist-client/route.d.ts +1 -1
  68. package/dist-client/route.js +15 -0
  69. package/dist-client/route.js.map +1 -1
  70. package/dist-client/shared/complete-api.d.ts +45 -0
  71. package/dist-client/shared/complete-api.js +274 -0
  72. package/dist-client/shared/complete-api.js.map +1 -0
  73. package/dist-client/shared/func.d.ts +2 -0
  74. package/dist-client/shared/func.js +22 -0
  75. package/dist-client/shared/func.js.map +1 -0
  76. package/dist-client/shared/integration-fetch.d.ts +35 -0
  77. package/dist-client/shared/integration-fetch.js +53 -0
  78. package/dist-client/shared/integration-fetch.js.map +1 -0
  79. package/dist-client/shared/kpi-project-sync.d.ts +16 -0
  80. package/dist-client/shared/kpi-project-sync.js +129 -0
  81. package/dist-client/shared/kpi-project-sync.js.map +1 -0
  82. package/dist-client/tsconfig.tsbuildinfo +1 -1
  83. package/dist-client/viewparts/menu-tools.d.ts +5 -0
  84. package/dist-client/viewparts/menu-tools.js +149 -16
  85. package/dist-client/viewparts/menu-tools.js.map +1 -1
  86. package/dist-server/index.d.ts +1 -0
  87. package/dist-server/index.js +4 -0
  88. package/dist-server/index.js.map +1 -1
  89. package/dist-server/service/index.d.ts +4 -0
  90. package/dist-server/service/index.js +19 -0
  91. package/dist-server/service/index.js.map +1 -0
  92. package/dist-server/service/kpi-metric-value/index.d.ts +4 -0
  93. package/dist-server/service/kpi-metric-value/index.js +8 -0
  94. package/dist-server/service/kpi-metric-value/index.js.map +1 -0
  95. package/dist-server/service/kpi-metric-value/kpi-metric-value-mutation.d.ts +198 -0
  96. package/dist-server/service/kpi-metric-value/kpi-metric-value-mutation.js +1067 -0
  97. package/dist-server/service/kpi-metric-value/kpi-metric-value-mutation.js.map +1 -0
  98. package/dist-server/service/kpi-metric-value/kpi-metric-value-query.d.ts +15 -0
  99. package/dist-server/service/kpi-metric-value/kpi-metric-value-query.js +72 -0
  100. package/dist-server/service/kpi-metric-value/kpi-metric-value-query.js.map +1 -0
  101. package/dist-server/service/kpi-stat/index.d.ts +4 -0
  102. package/dist-server/service/kpi-stat/index.js +8 -0
  103. package/dist-server/service/kpi-stat/index.js.map +1 -0
  104. package/dist-server/service/kpi-stat/kpi-stat-query.d.ts +13 -0
  105. package/dist-server/service/kpi-stat/kpi-stat-query.js +751 -0
  106. package/dist-server/service/kpi-stat/kpi-stat-query.js.map +1 -0
  107. package/dist-server/service/kpi-stat/kpi-stat-types.d.ts +32 -0
  108. package/dist-server/service/kpi-stat/kpi-stat-types.js +126 -0
  109. package/dist-server/service/kpi-stat/kpi-stat-types.js.map +1 -0
  110. package/dist-server/service/kpi-value/index.d.ts +3 -0
  111. package/dist-server/service/kpi-value/index.js +7 -0
  112. package/dist-server/service/kpi-value/index.js.map +1 -0
  113. package/dist-server/service/kpi-value/kpi-value-query.d.ts +8 -0
  114. package/dist-server/service/kpi-value/kpi-value-query.js +90 -0
  115. package/dist-server/service/kpi-value/kpi-value-query.js.map +1 -0
  116. package/dist-server/tsconfig.tsbuildinfo +1 -1
  117. package/package.json +8 -7
  118. package/schema.graphql +1274 -22
  119. package/things-factory.config.js +7 -1
@@ -0,0 +1,689 @@
1
+ import { __decorate, __metadata } from "tslib";
2
+ import gql from 'graphql-tag';
3
+ import { LitElement, html, css, nothing } from 'lit';
4
+ import { customElement, property, state } from 'lit/decorators.js';
5
+ import { client } from '@operato/graphql';
6
+ import { ScrollbarStyles } from '@operato/styles';
7
+ import '../../../components/kpi-radar-chart.js';
8
+ import '../../../components/kpi-boxplot-chart.js';
9
+ import '../../../components/kpi-trend-chart.js';
10
+ let KpiRegionPopup = class KpiRegionPopup extends LitElement {
11
+ constructor() {
12
+ super(...arguments);
13
+ this.selectedRegion = null;
14
+ this.geoGroup = null; // 시군구 geo_group 코드 (있으면 시군구 쿼리 사용)
15
+ this.selectedCategory = '전체 KPI';
16
+ this.selectedChartType = 'boxplot';
17
+ this.selectedPeriod = '월';
18
+ this.startYearMonth = ''; // YYYY-MM 형식
19
+ this.endYearMonth = ''; // YYYY-MM 형식
20
+ this.sectorType = ''; // 유형 필터 PUBLIC | PRIVATE
21
+ this.buildingUsage = ''; // 용도 필터 RESIDENTIAL | NON_RESIDENTIAL
22
+ this.monthlyTrendData = []; // 부모에서 전달받은 월별 추이 데이터
23
+ this.totalKpiStats = []; // 전체 KPI 통계 (비교용)
24
+ this.chartData = [];
25
+ this.chartCategories = [];
26
+ this.trendData = [];
27
+ this.regionKpiStats = [];
28
+ /** 시군구 모드일 때만 사용하는 해당 시군구 월별 추이 (광역 추이 monthlyTrendData 와 별개) */
29
+ this.sigunguTrendData = [];
30
+ }
31
+ updated(changedProperties) {
32
+ const regionChanged = changedProperties.has('selectedRegion') || changedProperties.has('geoGroup');
33
+ const periodChanged = changedProperties.has('startYearMonth') || changedProperties.has('endYearMonth');
34
+ const filterChanged = changedProperties.has('sectorType') || changedProperties.has('buildingUsage');
35
+ if ((regionChanged || periodChanged || filterChanged) && this.selectedRegion) {
36
+ this.fetchRegionKpiStats();
37
+ }
38
+ // 추이 갱신: 시군구 모드면 시군구 전용 추이 조회, 아니면 부모가 준 광역(metro) 데이터로 생성
39
+ if (regionChanged ||
40
+ periodChanged ||
41
+ filterChanged ||
42
+ changedProperties.has('selectedCategory') ||
43
+ changedProperties.has('monthlyTrendData')) {
44
+ this._refreshTrend();
45
+ }
46
+ }
47
+ _refreshTrend() {
48
+ if (this.geoGroup) {
49
+ this.fetchSigunguTrend(); // 완료 후 내부에서 generateTrendData 호출
50
+ }
51
+ else {
52
+ this.generateTrendData();
53
+ }
54
+ }
55
+ /** 시군구(geo_group 코드) 단위 월별 추이 조회 → generateTrendData 재생성 */
56
+ async fetchSigunguTrend() {
57
+ var _a;
58
+ if (!this.geoGroup) {
59
+ this.sigunguTrendData = [];
60
+ this.generateTrendData();
61
+ return;
62
+ }
63
+ try {
64
+ const response = await client.query({
65
+ query: gql `
66
+ query GetSigunguTrend(
67
+ $geoGroup: String!
68
+ $startYearMonth: String
69
+ $endYearMonth: String
70
+ $sectorType: String
71
+ $buildingUsage: String
72
+ ) {
73
+ kpiZValueMonthlyTrendBySigungu(
74
+ geoGroup: $geoGroup
75
+ startYearMonth: $startYearMonth
76
+ endYearMonth: $endYearMonth
77
+ sectorType: $sectorType
78
+ buildingUsage: $buildingUsage
79
+ ) {
80
+ geoGroup
81
+ yearMonth
82
+ avgVal
83
+ projectCount
84
+ }
85
+ }
86
+ `,
87
+ variables: {
88
+ geoGroup: this.geoGroup,
89
+ startYearMonth: this.startYearMonth || null,
90
+ endYearMonth: this.endYearMonth || null,
91
+ sectorType: this.sectorType || null,
92
+ buildingUsage: this.buildingUsage || null
93
+ }
94
+ });
95
+ this.sigunguTrendData = ((_a = response.data) === null || _a === void 0 ? void 0 : _a.kpiZValueMonthlyTrendBySigungu) || [];
96
+ }
97
+ catch (e) {
98
+ console.error('시군구 추이 조회 실패:', e);
99
+ this.sigunguTrendData = [];
100
+ }
101
+ this.generateTrendData();
102
+ }
103
+ async fetchRegionKpiStats() {
104
+ if (!this.selectedRegion)
105
+ return;
106
+ try {
107
+ let regionKpiStats;
108
+ if (this.geoGroup) {
109
+ // 시군구 모드: geo_group 코드로 조회
110
+ const response = await client.query({
111
+ query: gql `
112
+ query GetSigunguKpiStats(
113
+ $geoGroup: String!
114
+ $startYearMonth: String
115
+ $endYearMonth: String
116
+ $sectorType: String
117
+ $buildingUsage: String
118
+ ) {
119
+ kpiYValueStatsByGeoGroup(
120
+ geoGroup: $geoGroup
121
+ startYearMonth: $startYearMonth
122
+ endYearMonth: $endYearMonth
123
+ sectorType: $sectorType
124
+ buildingUsage: $buildingUsage
125
+ ) {
126
+ kpiName
127
+ minVal
128
+ q1Val
129
+ medVal
130
+ q3Val
131
+ maxVal
132
+ avgVal
133
+ projectCount
134
+ }
135
+ }
136
+ `,
137
+ variables: {
138
+ geoGroup: this.geoGroup,
139
+ startYearMonth: this.startYearMonth,
140
+ endYearMonth: this.endYearMonth,
141
+ sectorType: this.sectorType || null,
142
+ buildingUsage: this.buildingUsage || null
143
+ }
144
+ });
145
+ regionKpiStats = response.data.kpiYValueStatsByGeoGroup || [];
146
+ }
147
+ else {
148
+ // 광역 모드: 광역시도 이름으로 조회
149
+ const response = await client.query({
150
+ query: gql `
151
+ query GetRegionKpiStats(
152
+ $metroArea: String!
153
+ $startYearMonth: String
154
+ $endYearMonth: String
155
+ $sectorType: String
156
+ $buildingUsage: String
157
+ ) {
158
+ kpiYValueStatsByMetroArea(
159
+ metroArea: $metroArea
160
+ startYearMonth: $startYearMonth
161
+ endYearMonth: $endYearMonth
162
+ sectorType: $sectorType
163
+ buildingUsage: $buildingUsage
164
+ ) {
165
+ kpiName
166
+ minVal
167
+ q1Val
168
+ medVal
169
+ q3Val
170
+ maxVal
171
+ avgVal
172
+ projectCount
173
+ }
174
+ }
175
+ `,
176
+ variables: {
177
+ metroArea: this.selectedRegion,
178
+ startYearMonth: this.startYearMonth,
179
+ endYearMonth: this.endYearMonth,
180
+ sectorType: this.sectorType || null,
181
+ buildingUsage: this.buildingUsage || null
182
+ }
183
+ });
184
+ regionKpiStats = response.data.kpiYValueStatsByMetroArea || [];
185
+ }
186
+ this.regionKpiStats = regionKpiStats;
187
+ this.generateChartData();
188
+ }
189
+ catch (error) {
190
+ console.error('Failed to fetch region KPI stats:', error);
191
+ this.regionKpiStats = [];
192
+ this.generateChartData();
193
+ }
194
+ }
195
+ generateChartData() {
196
+ // kpiName 표기가 'Y1. 일정성과' / '일정성과(Y1)' 등으로 환경마다 달라 전체 문자열 매칭은 깨짐.
197
+ // Y1~Y6 코드만 추출해 짧은 라벨로 매핑.
198
+ const yLabelByCode = {
199
+ Y1: '일정',
200
+ Y2: '비용',
201
+ Y3: '품질',
202
+ Y4: '안전',
203
+ Y5: '환경',
204
+ Y6: '생산성'
205
+ };
206
+ const toCategory = (kpiName) => { var _a, _b; return yLabelByCode[(_b = (_a = (kpiName || '').match(/Y[1-6]/)) === null || _a === void 0 ? void 0 : _a[0]) !== null && _b !== void 0 ? _b : ''] || kpiName; };
207
+ // 레이더 차트와 박스플롯은 항상 전체 Y-level KPI 표시 (selectedCategory 무시)
208
+ if (this.selectedChartType === 'radar') {
209
+ // 레이더 차트용 데이터 생성 — 지역 평균 단일 시리즈
210
+ // (전체평균 기준선은 지역 평균과 차별성이 적어 제거 → 단일 시리즈라 범례도 자동 미표시)
211
+ const data = [];
212
+ const categories = [];
213
+ // 카테고리 수집
214
+ this.regionKpiStats.forEach(stat => {
215
+ const category = toCategory(stat.kpiName);
216
+ if (!categories.includes(category)) {
217
+ categories.push(category);
218
+ }
219
+ });
220
+ // 지역 데이터 추가 (박스플롯과 동일 스케일·정밀도 — Math.round 제거해 수치 일치)
221
+ this.regionKpiStats.forEach(stat => {
222
+ const category = toCategory(stat.kpiName);
223
+ data.push({
224
+ org: this.selectedRegion || '지역',
225
+ category,
226
+ value: stat.avgVal * 100
227
+ });
228
+ });
229
+ this.chartCategories = categories.length > 0 ? categories : ['일정', '비용', '품질', '안전', '환경', '생산성'];
230
+ this.chartData = data;
231
+ }
232
+ else {
233
+ // 박스플롯용 데이터 생성
234
+ const data = [];
235
+ const categories = [];
236
+ this.regionKpiStats.forEach(stat => {
237
+ const category = toCategory(stat.kpiName);
238
+ if (!categories.includes(category)) {
239
+ categories.push(category);
240
+ }
241
+ data.push({
242
+ org: category,
243
+ min: stat.minVal * 100,
244
+ max: stat.maxVal * 100,
245
+ q1: stat.q1Val * 100,
246
+ q3: stat.q3Val * 100,
247
+ median: stat.medVal * 100,
248
+ mean: stat.avgVal * 100,
249
+ value: stat.avgVal * 100
250
+ });
251
+ });
252
+ this.chartCategories = categories.length > 0 ? categories : ['일정', '비용', '품질', '안전', '환경', '생산성'];
253
+ this.chartData = data;
254
+ }
255
+ }
256
+ generateTrendData() {
257
+ if (!this.selectedRegion)
258
+ return;
259
+ // 추이 데이터 소스: 시군구 모드면 시군구 전용 추이(sigunguTrendData), 아니면 광역(metro) 추이에서 해당 지역 필터.
260
+ const rows = this.geoGroup
261
+ ? this.sigunguTrendData
262
+ : this.monthlyTrendData.filter(d => d.geoGroup === this.selectedRegion);
263
+ // 월별 데이터를 YYYY-MM 기준 맵으로
264
+ const byMonth = new Map();
265
+ rows.forEach(d => byMonth.set(d.yearMonth, d));
266
+ // 선택 기간(start~end) 전체 월을 축으로 사용 — 데이터가 없는 월도 레코드(값 0)로 채워
267
+ // 전국 패널의 기간과 동일한 범위를 플로팅. (전체 기간 모드 등으로 start/end 가 비면 데이터 월만 사용)
268
+ const months = this.startYearMonth && this.endYearMonth
269
+ ? this.buildMonthRange(this.startYearMonth, this.endYearMonth)
270
+ : [...byMonth.keys()].sort();
271
+ let prev = null;
272
+ this.trendData = months.map(ym => {
273
+ const d = byMonth.get(ym);
274
+ const hasData = !!d;
275
+ // 데이터 없는 월은 0이 아니라 null — 0(실제 0점)과 구분하여 플로팅/표시에서 제외.
276
+ // 값은 반올림하지 않음(정수화하면 표의 소수점이 항상 .0 으로 나옴) — 표시 시 toFixed(1) 적용.
277
+ const value = hasData ? d.avgVal * 100 : null;
278
+ // 추이: 무데이터 'none', 직전 데이터 없으면(첫 점) 'flat', 그 외 실제 증감 비교
279
+ let trendDir;
280
+ if (!hasData) {
281
+ trendDir = 'none';
282
+ }
283
+ else if (prev === null) {
284
+ trendDir = 'flat';
285
+ }
286
+ else {
287
+ trendDir = value > prev ? 'up' : value < prev ? 'down' : 'flat';
288
+ }
289
+ if (hasData)
290
+ prev = value;
291
+ return {
292
+ date: ym, // YYYY-MM 형식
293
+ value,
294
+ color: value == null ? undefined : value > 60 ? '#4caf50' : value > 40 ? '#ff9800' : '#f44336',
295
+ trendDir
296
+ };
297
+ });
298
+ }
299
+ /** start~end(YYYY-MM) 사이 모든 월을 순서대로 반환. 잘못된 범위는 안전하게 종료. */
300
+ buildMonthRange(start, end) {
301
+ const [sy, sm] = start.split('-').map(Number);
302
+ const [ey, em] = end.split('-').map(Number);
303
+ if (!sy || !sm || !ey || !em)
304
+ return [];
305
+ const out = [];
306
+ let y = sy;
307
+ let m = sm;
308
+ for (let i = 0; i < 240; i++) {
309
+ out.push(`${y}-${String(m).padStart(2, '0')}`);
310
+ if (y === ey && m === em)
311
+ break;
312
+ if (y > ey || (y === ey && m > em))
313
+ break; // start > end 방어
314
+ m++;
315
+ if (m > 12) {
316
+ m = 1;
317
+ y++;
318
+ }
319
+ }
320
+ return out;
321
+ }
322
+ onClose() {
323
+ this.dispatchEvent(new CustomEvent('popup-close', {
324
+ bubbles: true,
325
+ composed: true
326
+ }));
327
+ }
328
+ onChartTypeChange(type) {
329
+ this.selectedChartType = type;
330
+ this.generateChartData();
331
+ }
332
+ render() {
333
+ if (!this.selectedRegion) {
334
+ return nothing;
335
+ }
336
+ return html `
337
+ <div class="popup-header">
338
+ <div class="popup-title">${this.selectedRegion} KPI</div>
339
+ <button class="popup-close" @click=${this.onClose}>×</button>
340
+ </div>
341
+ <div class="popup-content">
342
+ <!-- 종합 성과 -->
343
+ <div class="chart-section">
344
+ <div class="sub-title">종합 성과</div>
345
+ <div class="chart-toggle">
346
+ <button
347
+ class="toggle-button ${this.selectedChartType === 'boxplot' ? 'active' : ''}"
348
+ @click=${() => this.onChartTypeChange('boxplot')}
349
+ >
350
+ 박스플롯
351
+ </button>
352
+ <button
353
+ class="toggle-button ${this.selectedChartType === 'radar' ? 'active' : ''}"
354
+ @click=${() => this.onChartTypeChange('radar')}
355
+ >
356
+ 레이더차트
357
+ </button>
358
+ </div>
359
+ <div class="chart-container">
360
+ ${this.chartData.length === 0
361
+ ? html `<div
362
+ style="display:flex;align-items:center;justify-content:center;height:100%;color:#888;font-size:0.9rem;"
363
+ >
364
+ 선택한 기간에 데이터가 없습니다.
365
+ </div>`
366
+ : this.selectedChartType === 'boxplot'
367
+ ? html `
368
+ <sv-kpi-boxplot-chart
369
+ .data=${this.chartData}
370
+ .groups=${this.chartCategories}
371
+ .valueKey=${'value'}
372
+ .valueLabel=${this.selectedRegion || '지역'}
373
+ .showStatus=${false}
374
+ ></sv-kpi-boxplot-chart>
375
+ `
376
+ : html `
377
+ <sv-kpi-radar-chart
378
+ .data=${this.chartData}
379
+ .categories=${this.chartCategories}
380
+ .valueKey=${'value'}
381
+ ></sv-kpi-radar-chart>
382
+ `}
383
+ </div>
384
+ </div>
385
+
386
+ <!-- 기간별 성과 추이 -->
387
+ <div class="trend-section">
388
+ <div class="sub-title">기간별 성과 추이</div>
389
+ <div class="trend-chart-container" style="height: 200px; margin-bottom: 16px;">
390
+ <sv-kpi-trend-chart
391
+ .data=${this.trendData}
392
+ .lineColor=${'#2196f3'}
393
+ .strokeWidth=${2}
394
+ .showPoints=${false}
395
+ .pointRadius=${4}
396
+ ></sv-kpi-trend-chart>
397
+ </div>
398
+ <div class="trend-table">
399
+ <table style="width: 100%; border-collapse: collapse; font-size: 0.85rem;">
400
+ <thead style="position: sticky; top: 0; background: white; z-index: 1;">
401
+ <tr style="border-bottom: 2px solid #f1f3f4; background-color: #f8f9fa;">
402
+ <th style="padding: 8px; text-align: left; font-weight: 600;">날짜</th>
403
+ <th style="padding: 8px; text-align: right; font-weight: 600;">성과</th>
404
+ <th style="padding: 8px; text-align: center; font-weight: 600;">추이</th>
405
+ </tr>
406
+ </thead>
407
+ <tbody>
408
+ ${[...this.trendData].reverse().map((item) => html `
409
+ <tr style="border-bottom: 1px solid #f1f3f4;">
410
+ <td style="padding: 8px; font-size: 0.85rem;">${item.date}</td>
411
+ <td style="padding: 8px; text-align: right; font-size: 0.85rem; font-weight: 600;">
412
+ ${item.value != null ? item.value.toFixed(1) : '-'}
413
+ </td>
414
+ <td style="padding: 8px; text-align: center; font-size: 0.85rem;">
415
+ <span style="color: ${item.trendDir === 'up' ? '#e53935' : item.trendDir === 'down' ? '#2196f3' : '#999'};">
416
+ ${item.trendDir === 'up' ? '▲' : item.trendDir === 'down' ? '▼' : '–'}
417
+ </span>
418
+ </td>
419
+ </tr>
420
+ `)}
421
+ </tbody>
422
+ </table>
423
+ </div>
424
+ </div>
425
+ </div>
426
+ `;
427
+ }
428
+ };
429
+ KpiRegionPopup.styles = [
430
+ ScrollbarStyles,
431
+ css `
432
+ :host {
433
+ display: block;
434
+ position: absolute;
435
+ top: 0;
436
+ left: 402px;
437
+ width: 400px;
438
+ height: 100%;
439
+ background: #fff;
440
+ border-right: 1px solid #e0e0e0;
441
+ z-index: 1000;
442
+ overflow: hidden;
443
+ display: flex;
444
+ flex-direction: column;
445
+ box-shadow: 2px 0 8px rgba(0, 0, 0, 0.1);
446
+ }
447
+ .popup-content {
448
+ padding: 20px;
449
+ overflow-y: auto;
450
+ flex: 1;
451
+ }
452
+ .popup-header {
453
+ display: flex;
454
+ justify-content: space-between;
455
+ align-items: center;
456
+ padding: 20px;
457
+ border-bottom: 1px solid #e0e0e0;
458
+ background: #fff;
459
+ height: 70px;
460
+ box-sizing: border-box;
461
+ }
462
+ .popup-title {
463
+ font-size: 1.2rem;
464
+ font-weight: bold;
465
+ color: #333;
466
+ }
467
+ .popup-close {
468
+ width: 32px;
469
+ height: 32px;
470
+ border: none;
471
+ background: #fff;
472
+ border-radius: 50%;
473
+ cursor: pointer;
474
+ display: flex;
475
+ align-items: center;
476
+ justify-content: center;
477
+ font-size: 1.2rem;
478
+ color: #666;
479
+ transition: all 0.2s;
480
+ }
481
+ .popup-close:hover {
482
+ background: #e9ecef;
483
+ color: #333;
484
+ }
485
+ .sub-title {
486
+ font-size: 1rem;
487
+ font-weight: 600;
488
+ margin-bottom: 16px;
489
+ color: #495057;
490
+ }
491
+ .chart-section {
492
+ background: #f8f9fa;
493
+ border-radius: 8px;
494
+ padding: 16px;
495
+ margin-bottom: 20px;
496
+ }
497
+ .chart-toggle {
498
+ display: flex;
499
+ gap: 8px;
500
+ margin-bottom: 16px;
501
+ }
502
+ .toggle-button {
503
+ padding: 8px 16px;
504
+ border: 1px solid #ced4da;
505
+ background: #fff;
506
+ border-radius: 6px;
507
+ cursor: pointer;
508
+ font-size: 0.9rem;
509
+ transition: all 0.2s;
510
+ }
511
+ .toggle-button.active {
512
+ background: #667eea;
513
+ color: white;
514
+ border-color: #667eea;
515
+ }
516
+ .chart-container {
517
+ height: 300px;
518
+ display: flex;
519
+ align-items: center;
520
+ justify-content: center;
521
+ background: white;
522
+ border-radius: 6px;
523
+ border: 1px solid #e9ecef;
524
+ }
525
+ .period-selector {
526
+ display: flex;
527
+ gap: 8px;
528
+ margin-bottom: 16px;
529
+ }
530
+ .period-button {
531
+ padding: 6px 12px;
532
+ border: 1px solid #ced4da;
533
+ background: #fff;
534
+ border-radius: 4px;
535
+ cursor: pointer;
536
+ font-size: 0.85rem;
537
+ transition: all 0.2s;
538
+ }
539
+ .period-button.active {
540
+ background: #667eea;
541
+ color: white;
542
+ border-color: #667eea;
543
+ }
544
+ .date-range-selector {
545
+ display: flex;
546
+ align-items: center;
547
+ gap: 8px;
548
+ margin-bottom: 16px;
549
+ }
550
+ .date-select {
551
+ padding: 6px 12px;
552
+ border: 1px solid #ced4da;
553
+ border-radius: 4px;
554
+ background: white;
555
+ font-size: 0.9rem;
556
+ }
557
+ `
558
+ ];
559
+ // geo_group → 광역시도 매핑 (시군구 모드에서 추이 데이터 조회용)
560
+ KpiRegionPopup.GEO_TO_METRO = {
561
+ '01': '서울특별시',
562
+ '02': '서울특별시',
563
+ '03': '서울특별시',
564
+ '04': '서울특별시',
565
+ '05': '서울특별시',
566
+ '06': '서울특별시',
567
+ '07': '서울특별시',
568
+ '08': '서울특별시',
569
+ '10': '경기도',
570
+ '11': '경기도',
571
+ '12': '경기도',
572
+ '13': '경기도',
573
+ '14': '경기도',
574
+ '15': '경기도',
575
+ '16': '경기도',
576
+ '17': '경기도',
577
+ '18': '경기도',
578
+ '21': '인천광역시',
579
+ '22': '인천광역시',
580
+ '23': '인천광역시',
581
+ '24': '강원도',
582
+ '25': '강원도',
583
+ '26': '강원도',
584
+ '33': '강원도',
585
+ '27': '충청북도',
586
+ '28': '충청북도',
587
+ '29': '충청북도',
588
+ '30': '세종특별자치시',
589
+ '31': '충청남도',
590
+ '32': '충청남도',
591
+ '34': '대전광역시',
592
+ '35': '대전광역시',
593
+ '36': '경상북도',
594
+ '37': '경상북도',
595
+ '38': '경상북도',
596
+ '39': '경상북도',
597
+ '40': '경상북도',
598
+ '41': '대구광역시',
599
+ '42': '대구광역시',
600
+ '43': '대구광역시',
601
+ '44': '울산광역시',
602
+ '45': '울산광역시',
603
+ '46': '부산광역시',
604
+ '47': '부산광역시',
605
+ '48': '부산광역시',
606
+ '49': '부산광역시',
607
+ '50': '경상남도',
608
+ '51': '경상남도',
609
+ '52': '경상남도',
610
+ '53': '경상남도',
611
+ '54': '전라북도',
612
+ '55': '전라북도',
613
+ '56': '전라북도',
614
+ '57': '전라남도',
615
+ '58': '전라남도',
616
+ '59': '전라남도',
617
+ '61': '광주광역시',
618
+ '62': '광주광역시',
619
+ '63': '제주특별자치도'
620
+ };
621
+ __decorate([
622
+ property({ type: String }),
623
+ __metadata("design:type", Object)
624
+ ], KpiRegionPopup.prototype, "selectedRegion", void 0);
625
+ __decorate([
626
+ property({ type: String }),
627
+ __metadata("design:type", Object)
628
+ ], KpiRegionPopup.prototype, "geoGroup", void 0);
629
+ __decorate([
630
+ property({ type: String }),
631
+ __metadata("design:type", Object)
632
+ ], KpiRegionPopup.prototype, "selectedCategory", void 0);
633
+ __decorate([
634
+ property({ type: String }),
635
+ __metadata("design:type", Object)
636
+ ], KpiRegionPopup.prototype, "selectedChartType", void 0);
637
+ __decorate([
638
+ property({ type: String }),
639
+ __metadata("design:type", Object)
640
+ ], KpiRegionPopup.prototype, "selectedPeriod", void 0);
641
+ __decorate([
642
+ property({ type: String }),
643
+ __metadata("design:type", Object)
644
+ ], KpiRegionPopup.prototype, "startYearMonth", void 0);
645
+ __decorate([
646
+ property({ type: String }),
647
+ __metadata("design:type", Object)
648
+ ], KpiRegionPopup.prototype, "endYearMonth", void 0);
649
+ __decorate([
650
+ property({ type: String }),
651
+ __metadata("design:type", Object)
652
+ ], KpiRegionPopup.prototype, "sectorType", void 0);
653
+ __decorate([
654
+ property({ type: String }),
655
+ __metadata("design:type", Object)
656
+ ], KpiRegionPopup.prototype, "buildingUsage", void 0);
657
+ __decorate([
658
+ property({ type: Array }),
659
+ __metadata("design:type", Array)
660
+ ], KpiRegionPopup.prototype, "monthlyTrendData", void 0);
661
+ __decorate([
662
+ property({ type: Array }),
663
+ __metadata("design:type", Array)
664
+ ], KpiRegionPopup.prototype, "totalKpiStats", void 0);
665
+ __decorate([
666
+ state(),
667
+ __metadata("design:type", Array)
668
+ ], KpiRegionPopup.prototype, "chartData", void 0);
669
+ __decorate([
670
+ state(),
671
+ __metadata("design:type", Array)
672
+ ], KpiRegionPopup.prototype, "chartCategories", void 0);
673
+ __decorate([
674
+ state(),
675
+ __metadata("design:type", Array)
676
+ ], KpiRegionPopup.prototype, "trendData", void 0);
677
+ __decorate([
678
+ state(),
679
+ __metadata("design:type", Array)
680
+ ], KpiRegionPopup.prototype, "regionKpiStats", void 0);
681
+ __decorate([
682
+ state(),
683
+ __metadata("design:type", Array)
684
+ ], KpiRegionPopup.prototype, "sigunguTrendData", void 0);
685
+ KpiRegionPopup = __decorate([
686
+ customElement('kpi-region-popup')
687
+ ], KpiRegionPopup);
688
+ export { KpiRegionPopup };
689
+ //# sourceMappingURL=kpi-region-popup.js.map