@things-factory/kpi 9.0.29 → 9.0.31

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 (73) hide show
  1. package/client/charts/kpi-mini-trend-chart.ts +125 -0
  2. package/client/charts/kpi-trend-chart.ts +163 -0
  3. package/client/google-map/common-google-map.ts +370 -0
  4. package/client/google-map/google-map-loader.ts +29 -0
  5. package/client/pages/kpi-dashboard/cards/kpi-level1-card.ts +248 -0
  6. package/client/pages/kpi-dashboard/cards/kpi-level2-comparison.ts +369 -0
  7. package/client/pages/kpi-dashboard/cards/kpi-level3-comparison.ts +443 -0
  8. package/client/pages/kpi-dashboard/components/kpi-chart-toggle.ts +72 -0
  9. package/client/pages/kpi-dashboard/components/kpi-left-panel.ts +399 -0
  10. package/client/pages/kpi-dashboard/components/kpi-map-panel.ts +302 -0
  11. package/client/pages/kpi-dashboard/components/kpi-region-popup.ts +355 -0
  12. package/client/pages/kpi-dashboard/kpi-dashboard-map.ts +243 -0
  13. package/client/pages/kpi-dashboard/kpi-dashboard.ts +416 -0
  14. package/client/route.ts +4 -0
  15. package/dist-client/charts/kpi-mini-trend-chart.d.ts +14 -0
  16. package/dist-client/charts/kpi-mini-trend-chart.js +148 -0
  17. package/dist-client/charts/kpi-mini-trend-chart.js.map +1 -0
  18. package/dist-client/charts/kpi-trend-chart.d.ts +25 -0
  19. package/dist-client/charts/kpi-trend-chart.js +186 -0
  20. package/dist-client/charts/kpi-trend-chart.js.map +1 -0
  21. package/dist-client/google-map/common-google-map.d.ts +34 -0
  22. package/dist-client/google-map/common-google-map.js +333 -0
  23. package/dist-client/google-map/common-google-map.js.map +1 -0
  24. package/dist-client/google-map/google-map-loader.d.ts +6 -0
  25. package/dist-client/google-map/google-map-loader.js +22 -0
  26. package/dist-client/google-map/google-map-loader.js.map +1 -0
  27. package/dist-client/pages/kpi-dashboard/cards/kpi-level1-card.d.ts +17 -0
  28. package/dist-client/pages/kpi-dashboard/cards/kpi-level1-card.js +279 -0
  29. package/dist-client/pages/kpi-dashboard/cards/kpi-level1-card.js.map +1 -0
  30. package/dist-client/pages/kpi-dashboard/cards/kpi-level2-comparison.d.ts +19 -0
  31. package/dist-client/pages/kpi-dashboard/cards/kpi-level2-comparison.js +385 -0
  32. package/dist-client/pages/kpi-dashboard/cards/kpi-level2-comparison.js.map +1 -0
  33. package/dist-client/pages/kpi-dashboard/cards/kpi-level3-comparison.d.ts +23 -0
  34. package/dist-client/pages/kpi-dashboard/cards/kpi-level3-comparison.js +465 -0
  35. package/dist-client/pages/kpi-dashboard/cards/kpi-level3-comparison.js.map +1 -0
  36. package/dist-client/pages/kpi-dashboard/components/kpi-chart-toggle.d.ts +8 -0
  37. package/dist-client/pages/kpi-dashboard/components/kpi-chart-toggle.js +78 -0
  38. package/dist-client/pages/kpi-dashboard/components/kpi-chart-toggle.js.map +1 -0
  39. package/dist-client/pages/kpi-dashboard/components/kpi-left-panel.d.ts +22 -0
  40. package/dist-client/pages/kpi-dashboard/components/kpi-left-panel.js +404 -0
  41. package/dist-client/pages/kpi-dashboard/components/kpi-left-panel.js.map +1 -0
  42. package/dist-client/pages/kpi-dashboard/components/kpi-map-panel.d.ts +28 -0
  43. package/dist-client/pages/kpi-dashboard/components/kpi-map-panel.js +298 -0
  44. package/dist-client/pages/kpi-dashboard/components/kpi-map-panel.js.map +1 -0
  45. package/dist-client/pages/kpi-dashboard/components/kpi-region-popup.d.ts +23 -0
  46. package/dist-client/pages/kpi-dashboard/components/kpi-region-popup.js +368 -0
  47. package/dist-client/pages/kpi-dashboard/components/kpi-region-popup.js.map +1 -0
  48. package/dist-client/pages/kpi-dashboard/kpi-dashboard-map.d.ts +29 -0
  49. package/dist-client/pages/kpi-dashboard/kpi-dashboard-map.js +271 -0
  50. package/dist-client/pages/kpi-dashboard/kpi-dashboard-map.js.map +1 -0
  51. package/dist-client/pages/kpi-dashboard/kpi-dashboard.d.ts +21 -0
  52. package/dist-client/pages/kpi-dashboard/kpi-dashboard.js +398 -0
  53. package/dist-client/pages/kpi-dashboard/kpi-dashboard.js.map +1 -1
  54. package/dist-client/route.d.ts +1 -1
  55. package/dist-client/route.js +3 -0
  56. package/dist-client/route.js.map +1 -1
  57. package/dist-client/tsconfig.tsbuildinfo +1 -1
  58. package/dist-server/index.d.ts +1 -0
  59. package/dist-server/index.js +1 -0
  60. package/dist-server/index.js.map +1 -1
  61. package/dist-server/migrations/index.d.ts +1 -0
  62. package/dist-server/migrations/index.js +12 -0
  63. package/dist-server/migrations/index.js.map +1 -0
  64. package/dist-server/tsconfig.tsbuildinfo +1 -1
  65. package/package.json +2 -2
  66. package/server/index.ts +1 -0
  67. package/server/migrations/index.ts +9 -0
  68. package/things-factory.config.js +2 -1
  69. package/translations/en.json +1 -0
  70. package/translations/ja.json +1 -0
  71. package/translations/ko.json +1 -0
  72. package/translations/ms.json +1 -0
  73. package/translations/zh.json +1 -0
@@ -0,0 +1,355 @@
1
+ import { LitElement, html, css, nothing } from 'lit'
2
+ import { customElement, property, state } from 'lit/decorators.js'
3
+
4
+ import '../../../charts/kpi-radar-chart.js'
5
+ import '../../../charts/kpi-boxplot-chart.js'
6
+ import '../../../charts/kpi-trend-chart.js'
7
+
8
+ @customElement('kpi-region-popup')
9
+ export class KpiRegionPopup extends LitElement {
10
+ static styles = css`
11
+ :host {
12
+ display: block;
13
+ position: absolute;
14
+ top: 0;
15
+ left: 402px;
16
+ width: 400px;
17
+ height: 100%;
18
+ background: #fff;
19
+ border-right: 1px solid #e0e0e0;
20
+ z-index: 1000;
21
+ overflow: hidden;
22
+ display: flex;
23
+ flex-direction: column;
24
+ box-shadow: 2px 0 8px rgba(0, 0, 0, 0.1);
25
+ }
26
+ .popup-content {
27
+ padding: 24px;
28
+ overflow-y: auto;
29
+ flex: 1;
30
+ }
31
+ .popup-header {
32
+ display: flex;
33
+ justify-content: space-between;
34
+ align-items: center;
35
+ padding: 20px 24px;
36
+ border-bottom: 1px solid #e0e0e0;
37
+ background: #fff;
38
+ height: 70px;
39
+ box-sizing: border-box;
40
+ }
41
+ .popup-title {
42
+ font-size: 1.2rem;
43
+ font-weight: bold;
44
+ color: #333;
45
+ }
46
+ .popup-close {
47
+ width: 32px;
48
+ height: 32px;
49
+ border: none;
50
+ background: #fff;
51
+ border-radius: 50%;
52
+ cursor: pointer;
53
+ display: flex;
54
+ align-items: center;
55
+ justify-content: center;
56
+ font-size: 1.2rem;
57
+ color: #666;
58
+ transition: all 0.2s;
59
+ }
60
+ .popup-close:hover {
61
+ background: #e9ecef;
62
+ color: #333;
63
+ }
64
+ .sub-title {
65
+ font-size: 1rem;
66
+ font-weight: 600;
67
+ margin-bottom: 16px;
68
+ color: #495057;
69
+ }
70
+ .chart-section {
71
+ background: #f8f9fa;
72
+ border-radius: 8px;
73
+ padding: 16px;
74
+ margin-bottom: 20px;
75
+ }
76
+ .chart-toggle {
77
+ display: flex;
78
+ gap: 8px;
79
+ margin-bottom: 16px;
80
+ }
81
+ .toggle-button {
82
+ padding: 8px 16px;
83
+ border: 1px solid #ced4da;
84
+ background: #fff;
85
+ border-radius: 6px;
86
+ cursor: pointer;
87
+ font-size: 0.9rem;
88
+ transition: all 0.2s;
89
+ }
90
+ .toggle-button.active {
91
+ background: #667eea;
92
+ color: white;
93
+ border-color: #667eea;
94
+ }
95
+ .chart-container {
96
+ height: 300px;
97
+ display: flex;
98
+ align-items: center;
99
+ justify-content: center;
100
+ background: white;
101
+ border-radius: 6px;
102
+ border: 1px solid #e9ecef;
103
+ }
104
+ .period-selector {
105
+ display: flex;
106
+ gap: 8px;
107
+ margin-bottom: 16px;
108
+ }
109
+ .period-button {
110
+ padding: 6px 12px;
111
+ border: 1px solid #ced4da;
112
+ background: #fff;
113
+ border-radius: 4px;
114
+ cursor: pointer;
115
+ font-size: 0.85rem;
116
+ transition: all 0.2s;
117
+ }
118
+ .period-button.active {
119
+ background: #667eea;
120
+ color: white;
121
+ border-color: #667eea;
122
+ }
123
+ .date-range-selector {
124
+ display: flex;
125
+ align-items: center;
126
+ gap: 8px;
127
+ margin-bottom: 16px;
128
+ }
129
+ .date-select {
130
+ padding: 6px 12px;
131
+ border: 1px solid #ced4da;
132
+ border-radius: 4px;
133
+ background: white;
134
+ font-size: 0.9rem;
135
+ }
136
+ `
137
+
138
+ @property({ type: String }) selectedRegion: string | null = null
139
+ @property({ type: String }) selectedChartType = 'boxplot'
140
+ @property({ type: String }) selectedPeriod = '월'
141
+ @property({ type: String }) startDate = '전체'
142
+ @property({ type: String }) endDate = '전체'
143
+
144
+ @state() private chartData: any[] = []
145
+ @state() private chartCategories: string[] = []
146
+ @state() private trendData: { date: string; value: number; color?: string }[] = []
147
+
148
+ connectedCallback() {
149
+ super.connectedCallback()
150
+ this.generateChartData()
151
+ this.generateTrendData()
152
+ }
153
+
154
+ private generateChartData() {
155
+ // 선택된 지역의 KPI 데이터 생성
156
+ const categories = ['일정 성과', '비용 성과', '품질 성과', '안전 성과', '환경 성과']
157
+ this.chartCategories = categories
158
+
159
+ if (this.selectedChartType === 'radar') {
160
+ // 레이더 차트용 데이터 생성
161
+ this.chartData = categories.map(category => ({
162
+ category,
163
+ value: Math.random() * 50 + 25, // 25-75 범위
164
+ group: this.selectedRegion || '전체'
165
+ }))
166
+ } else {
167
+ // 박스플롯용 데이터 생성
168
+ this.chartData = categories.map(category => {
169
+ const baseValue = Math.random() * 50 + 25 // 25-75 범위
170
+ const variation = Math.random() * 20 // 변동폭
171
+
172
+ // 각 카테고리별로 20개의 데이터 포인트 생성
173
+ const dataPoints: { value: number; group: string }[] = []
174
+ for (let i = 0; i < 20; i++) {
175
+ dataPoints.push({
176
+ value: baseValue + (Math.random() - 0.5) * variation,
177
+ group: category
178
+ })
179
+ }
180
+
181
+ // 통계값 계산
182
+ const values = dataPoints.map(d => d.value).sort((a, b) => a - b)
183
+ const min = Math.min(...values)
184
+ const max = Math.max(...values)
185
+ const q1 = values[Math.floor(values.length * 0.25)]
186
+ const q3 = values[Math.floor(values.length * 0.75)]
187
+ const median = values[Math.floor(values.length * 0.5)]
188
+ const mean = values.reduce((a, b) => a + b, 0) / values.length
189
+
190
+ return {
191
+ group: category,
192
+ min: min,
193
+ max: max,
194
+ q1: q1,
195
+ q3: q3,
196
+ median: median,
197
+ mean: mean,
198
+ value: mean
199
+ }
200
+ })
201
+ }
202
+ }
203
+
204
+ private generateTrendData() {
205
+ // 최근 30일간의 트렌드 데이터 생성
206
+ const today = new Date()
207
+ this.trendData = []
208
+
209
+ for (let i = 29; i >= 0; i--) {
210
+ const date = new Date(today)
211
+ date.setDate(date.getDate() - i)
212
+
213
+ // 기본값에서 약간의 변동을 주어 트렌드 생성
214
+ const baseValue = 50
215
+ const trend = Math.sin(i * 0.2) * 10 // 사인파로 변동
216
+ const noise = (Math.random() - 0.5) * 5 // 랜덤 노이즈
217
+ const value = Math.max(0, Math.min(100, baseValue + trend + noise))
218
+
219
+ this.trendData.push({
220
+ date: date.toISOString().split('T')[0], // YYYY-MM-DD 형식
221
+ value: Math.round(value),
222
+ color: value > 60 ? '#4caf50' : value > 40 ? '#ff9800' : '#f44336'
223
+ })
224
+ }
225
+ }
226
+
227
+ private onClose() {
228
+ this.dispatchEvent(
229
+ new CustomEvent('popup-close', {
230
+ bubbles: true,
231
+ composed: true
232
+ })
233
+ )
234
+ }
235
+
236
+ private onChartTypeChange(type: string) {
237
+ this.selectedChartType = type
238
+ this.generateChartData()
239
+ }
240
+
241
+ private onPeriodChange(period: string) {
242
+ this.dispatchEvent(
243
+ new CustomEvent('period-change', {
244
+ detail: { period },
245
+ bubbles: true,
246
+ composed: true
247
+ })
248
+ )
249
+ this.generateTrendData()
250
+ }
251
+
252
+ private onDateRangeChange() {
253
+ this.dispatchEvent(
254
+ new CustomEvent('date-range-change', {
255
+ detail: { startDate: this.startDate, endDate: this.endDate },
256
+ bubbles: true,
257
+ composed: true
258
+ })
259
+ )
260
+ }
261
+
262
+ render() {
263
+ if (!this.selectedRegion) {
264
+ return nothing
265
+ }
266
+
267
+ return html`
268
+ <div class="popup-header">
269
+ <div class="popup-title">${this.selectedRegion} KPI</div>
270
+ <button class="popup-close" @click=${this.onClose}>×</button>
271
+ </div>
272
+ <div class="popup-content">
273
+ <!-- 종합 성과 -->
274
+ <div class="chart-section">
275
+ <div class="sub-title">종합 성과</div>
276
+ <div class="chart-toggle">
277
+ <button
278
+ class="toggle-button ${this.selectedChartType === 'boxplot' ? 'active' : ''}"
279
+ @click=${() => this.onChartTypeChange('boxplot')}
280
+ >
281
+ 박스플롯
282
+ </button>
283
+ <button
284
+ class="toggle-button ${this.selectedChartType === 'radar' ? 'active' : ''}"
285
+ @click=${() => this.onChartTypeChange('radar')}
286
+ >
287
+ 레이더차트
288
+ </button>
289
+ </div>
290
+ <div class="chart-container">
291
+ ${this.selectedChartType === 'boxplot'
292
+ ? html`
293
+ <kpi-boxplot-chart
294
+ .data=${this.chartData}
295
+ .groups=${this.chartCategories}
296
+ .valueKey=${'value'}
297
+ .currentGroup=${this.selectedRegion || '전체'}
298
+ ></kpi-boxplot-chart>
299
+ `
300
+ : html`
301
+ <kpi-radar-chart
302
+ .data=${this.chartData}
303
+ .categories=${this.chartCategories}
304
+ .valueKey=${'value'}
305
+ .currentGroup=${this.selectedRegion || '전체'}
306
+ ></kpi-radar-chart>
307
+ `}
308
+ </div>
309
+ </div>
310
+
311
+ <!-- 기간별 성과 추이 -->
312
+ <div class="trend-section">
313
+ <div class="sub-title">기간별 성과 추이</div>
314
+ <div class="trend-chart-container" style="height: 200px; margin-bottom: 16px;">
315
+ <kpi-trend-chart
316
+ .data=${this.trendData}
317
+ .lineColor=${'#2196f3'}
318
+ .strokeWidth=${2}
319
+ .showPoints=${true}
320
+ .pointRadius=${4}
321
+ ></kpi-trend-chart>
322
+ </div>
323
+ <div class="trend-table">
324
+ <table style="width: 100%; border-collapse: collapse; font-size: 0.85rem;">
325
+ <thead>
326
+ <tr style="border-bottom: 2px solid #f1f3f4; background-color: #f8f9fa;">
327
+ <th style="padding: 8px; text-align: left; font-weight: 600;">날짜</th>
328
+ <th style="padding: 8px; text-align: right; font-weight: 600;">성과</th>
329
+ <th style="padding: 8px; text-align: center; font-weight: 600;">추이</th>
330
+ </tr>
331
+ </thead>
332
+ <tbody>
333
+ ${this.trendData.slice(-10).map(
334
+ (item: any) => html`
335
+ <tr style="border-bottom: 1px solid #f1f3f4;">
336
+ <td style="padding: 8px; font-size: 0.85rem;">${item.date}</td>
337
+ <td style="padding: 8px; text-align: right; font-size: 0.85rem; font-weight: 600;">
338
+ ${item.value}
339
+ </td>
340
+ <td style="padding: 8px; text-align: center; font-size: 0.85rem;">
341
+ <span style="color: ${item.color};">
342
+ ${item.value > 60 ? '▲' : item.value > 40 ? '▲' : '▼'}
343
+ </span>
344
+ </td>
345
+ </tr>
346
+ `
347
+ )}
348
+ </tbody>
349
+ </table>
350
+ </div>
351
+ </div>
352
+ </div>
353
+ `
354
+ }
355
+ }
@@ -0,0 +1,243 @@
1
+ import gql from 'graphql-tag'
2
+ import { html, css, nothing } from 'lit'
3
+ import { customElement, state } from 'lit/decorators.js'
4
+
5
+ import { PageView } from '@operato/shell'
6
+ import { ScrollbarStyles } from '@operato/styles'
7
+ import { client } from '@operato/graphql'
8
+ import { notify } from '@operato/layout'
9
+
10
+ import './components/kpi-map-panel'
11
+ import './components/kpi-left-panel'
12
+ import './components/kpi-region-popup'
13
+
14
+ @customElement('kpi-dashboard-map')
15
+ export class KpiDashboardMapPage extends PageView {
16
+ static styles = [
17
+ ScrollbarStyles,
18
+ css`
19
+ :host {
20
+ display: flex;
21
+ flex-direction: column;
22
+ overflow: hidden;
23
+ position: relative;
24
+ }
25
+
26
+ .dashboard-container {
27
+ display: flex;
28
+ flex: 1;
29
+ overflow: hidden;
30
+ }
31
+
32
+ .right-panel {
33
+ flex: 1;
34
+ background: #f8f9fa;
35
+ overflow: hidden;
36
+ position: relative;
37
+ }
38
+ `
39
+ ]
40
+
41
+ @state() selectedCategory = '전체 KPI'
42
+ @state() selectedPeriod = '월'
43
+ @state() startDate = '전체'
44
+ @state() endDate = '전체'
45
+ @state() nationalData: any = null
46
+ @state() seoulData: any = null
47
+ @state() mapData: any = null
48
+ // 팝업 관련 상태 추가
49
+ @state() selectedRegion: string | null = null
50
+ @state() showRegionPopup = false
51
+
52
+ connectedCallback() {
53
+ super.connectedCallback()
54
+ this.fetchDashboardData()
55
+ this.setupKeyboardEvents()
56
+ }
57
+
58
+ private setupKeyboardEvents() {
59
+ document.addEventListener('keydown', e => {
60
+ if (e.key === 'Escape' && this.showRegionPopup) {
61
+ this.closeRegionPopup()
62
+ }
63
+ })
64
+ }
65
+
66
+ async fetchDashboardData() {
67
+ try {
68
+ // 전국 KPI 데이터 조회
69
+ const nationalResponse = await client.query({
70
+ query: gql`
71
+ query {
72
+ kpiStatistics {
73
+ items {
74
+ id
75
+ valueDate
76
+ periodType
77
+ mean
78
+ median
79
+ standardDeviation
80
+ kpi {
81
+ id
82
+ name
83
+ category {
84
+ id
85
+ name
86
+ }
87
+ }
88
+ }
89
+ }
90
+ }
91
+ `
92
+ })
93
+
94
+ this.nationalData = nationalResponse.data.kpiStatistics.items || []
95
+
96
+ // 서울특별시 데이터는 전국 데이터에서 필터링
97
+ this.seoulData = this.nationalData.filter((item: any) => item.kpi?.category?.name === '서울특별시')
98
+
99
+ // 지도 데이터 생성 (시도별)
100
+ this.generateMapData()
101
+ } catch (e) {
102
+ notify({
103
+ level: 'error',
104
+ message: '대시보드 데이터를 불러오지 못했습니다.'
105
+ })
106
+ }
107
+ }
108
+
109
+ private generateMapData() {
110
+ // 시도별 샘플 데이터 생성 (실제 좌표 포함)
111
+ this.mapData = [
112
+ { region: '강원', kpi: 101.5, change: -0.08, trend: 'down', lat: 37.8228, lng: 128.1555 },
113
+ { region: '경북', kpi: 94.8, change: -0.1, trend: 'down', lat: 36.4919, lng: 128.8889 },
114
+ { region: '대구', kpi: 79.4, change: -0.2, trend: 'down', lat: 35.8714, lng: 128.6014 },
115
+ { region: '경남', kpi: 92.1, change: -0.08, trend: 'down', lat: 35.4606, lng: 128.2132 },
116
+ { region: '부산', kpi: 85.2, change: -0.18, trend: 'down', lat: 35.1796, lng: 129.0756 },
117
+ { region: '울산', kpi: 91.4, change: 0.19, trend: 'up', lat: 35.5384, lng: 129.3114 },
118
+ { region: '서울', kpi: 98.1, change: 1.28, trend: 'up', lat: 37.5665, lng: 126.978 },
119
+ { region: '인천', kpi: 87.3, change: 0.05, trend: 'up', lat: 37.4563, lng: 126.7052 },
120
+ { region: '대전', kpi: 89.7, change: -0.12, trend: 'down', lat: 36.3504, lng: 127.3845 },
121
+ { region: '광주', kpi: 91.1, change: -0.16, trend: 'down', lat: 35.1595, lng: 126.8526 },
122
+ { region: '전북', kpi: 99.3, change: 0.15, trend: 'up', lat: 35.7175, lng: 127.153 },
123
+ { region: '전남', kpi: 91.8, change: -0.12, trend: 'down', lat: 34.8679, lng: 126.991 },
124
+ { region: '충북', kpi: 93.2, change: 0.08, trend: 'up', lat: 36.8, lng: 127.7 },
125
+ { region: '충남', kpi: 88.9, change: -0.05, trend: 'down', lat: 36.5184, lng: 126.8 },
126
+ { region: '제주', kpi: 95.6, change: 0.22, trend: 'up', lat: 33.4996, lng: 126.5312 }
127
+ ]
128
+ }
129
+
130
+ private downloadExcel() {
131
+ // 엑셀 다운로드 기능
132
+ console.log('엑셀 다운로드')
133
+ }
134
+
135
+ private onRegionClick(region: string) {
136
+ this.selectedRegion = region
137
+ this.showRegionPopup = true
138
+ this.fetchRegionData(region)
139
+ }
140
+
141
+ private closeRegionPopup() {
142
+ this.showRegionPopup = false
143
+ this.selectedRegion = null
144
+ }
145
+
146
+ private async fetchRegionData(region: string) {
147
+ try {
148
+ const response = await client.query({
149
+ query: gql`
150
+ query GetRegionKpiData($region: String) {
151
+ kpiStatistics(filters: [{ field: "kpi.category.name", operator: "eq", value: $region }]) {
152
+ items {
153
+ id
154
+ valueDate
155
+ periodType
156
+ mean
157
+ median
158
+ standardDeviation
159
+ kpi {
160
+ id
161
+ name
162
+ category {
163
+ id
164
+ name
165
+ }
166
+ }
167
+ }
168
+ }
169
+ }
170
+ `,
171
+ variables: { region }
172
+ })
173
+
174
+ this.seoulData = response.data.kpiStatistics.items || []
175
+ } catch (e) {
176
+ notify({
177
+ level: 'error',
178
+ message: '지역 데이터를 불러오지 못했습니다.'
179
+ })
180
+ }
181
+ }
182
+
183
+ get context() {
184
+ return {
185
+ title: '전국 KPI 대시보드',
186
+ description: '전국 및 지역별 KPI 성과 분석 대시보드'
187
+ }
188
+ }
189
+
190
+ render() {
191
+ return html`
192
+ <div class="dashboard-container">
193
+ <!-- 좌측 패널: 전국 KPI -->
194
+ <kpi-left-panel
195
+ .selectedCategory=${this.selectedCategory}
196
+ .mapData=${this.mapData}
197
+ @category-change=${(e: CustomEvent) => {
198
+ this.selectedCategory = e.detail.category
199
+ this.fetchDashboardData()
200
+ }}
201
+ @region-click=${(e: CustomEvent) => {
202
+ this.onRegionClick(e.detail.region)
203
+ }}
204
+ @download-excel=${this.downloadExcel}
205
+ ></kpi-left-panel>
206
+
207
+ <!-- 우측 패널: 지도 -->
208
+ <kpi-map-panel
209
+ class="right-panel"
210
+ .selectedCategory=${this.selectedCategory}
211
+ .mapData=${this.mapData}
212
+ @category-change=${(e: CustomEvent) => {
213
+ this.selectedCategory = e.detail.category
214
+ this.fetchDashboardData()
215
+ }}
216
+ @region-click=${(e: CustomEvent) => {
217
+ this.onRegionClick(e.detail.region)
218
+ }}
219
+ ></kpi-map-panel>
220
+
221
+ <!-- 지역 상세 팝업 -->
222
+ ${this.showRegionPopup
223
+ ? html`
224
+ <kpi-region-popup
225
+ .selectedRegion=${this.selectedRegion}
226
+ .selectedPeriod=${this.selectedPeriod}
227
+ .startDate=${this.startDate}
228
+ .endDate=${this.endDate}
229
+ @popup-close=${this.closeRegionPopup}
230
+ @period-change=${(e: CustomEvent) => {
231
+ this.selectedPeriod = e.detail.period
232
+ }}
233
+ @date-range-change=${(e: CustomEvent) => {
234
+ this.startDate = e.detail.startDate
235
+ this.endDate = e.detail.endDate
236
+ }}
237
+ ></kpi-region-popup>
238
+ `
239
+ : nothing}
240
+ </div>
241
+ `
242
+ }
243
+ }