@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,404 @@
1
+ import { __decorate, __metadata } from "tslib";
2
+ import { LitElement, html, css } from 'lit';
3
+ import { customElement, property, state } from 'lit/decorators.js';
4
+ import '../../../charts/kpi-radar-chart.js';
5
+ import '../../../charts/kpi-boxplot-chart.js';
6
+ import '../../../charts/kpi-mini-trend-chart.js';
7
+ let KpiLeftPanel = class KpiLeftPanel extends LitElement {
8
+ constructor() {
9
+ super(...arguments);
10
+ this.selectedCategory = '전체 KPI';
11
+ this.selectedChartType = 'boxplot';
12
+ this.mapData = [];
13
+ this.chartData = [];
14
+ this.chartCategories = [];
15
+ }
16
+ static { this.styles = css `
17
+ :host {
18
+ display: block;
19
+ width: 400px;
20
+ background: #fff;
21
+ border-right: 1px solid #e0e0e0;
22
+ overflow: hidden;
23
+ display: flex;
24
+ flex-direction: column;
25
+ box-shadow: 2px 0 8px rgba(0, 0, 0, 0.1);
26
+ }
27
+ .panel-content {
28
+ padding: 24px;
29
+ overflow-y: auto;
30
+ flex: 1;
31
+ }
32
+ .panel-header {
33
+ display: flex;
34
+ justify-content: space-between;
35
+ align-items: center;
36
+ padding: 20px 24px;
37
+ border-bottom: 1px solid #e0e0e0;
38
+ background: #fff;
39
+ height: 70px;
40
+ box-sizing: border-box;
41
+ }
42
+ .panel-title {
43
+ font-size: 1.3rem;
44
+ font-weight: bold;
45
+ color: #333;
46
+ margin: 0;
47
+ }
48
+ .panel-close {
49
+ width: 32px;
50
+ height: 32px;
51
+ border: none;
52
+ background: #fff;
53
+ border-radius: 50%;
54
+ cursor: pointer;
55
+ display: flex;
56
+ align-items: center;
57
+ justify-content: center;
58
+ font-size: 1.2rem;
59
+ color: #666;
60
+ transition: all 0.2s;
61
+ }
62
+ .panel-close:hover {
63
+ background: #e9ecef;
64
+ color: #333;
65
+ }
66
+ .sub-title {
67
+ font-size: 1rem;
68
+ font-weight: 600;
69
+ margin-bottom: 16px;
70
+ color: #495057;
71
+ }
72
+ .chart-section {
73
+ background: #f8f9fa;
74
+ border-radius: 8px;
75
+ padding: 16px;
76
+ margin-bottom: 20px;
77
+ }
78
+ .chart-toggle {
79
+ display: flex;
80
+ gap: 8px;
81
+ margin-bottom: 16px;
82
+ }
83
+ .toggle-button {
84
+ padding: 8px 16px;
85
+ border: 1px solid #ced4da;
86
+ background: #fff;
87
+ border-radius: 6px;
88
+ cursor: pointer;
89
+ font-size: 0.9rem;
90
+ transition: all 0.2s;
91
+ }
92
+ .toggle-button.active {
93
+ background: #667eea;
94
+ color: white;
95
+ border-color: #667eea;
96
+ }
97
+ .chart-container {
98
+ height: 300px;
99
+ display: flex;
100
+ align-items: center;
101
+ justify-content: center;
102
+ background: white;
103
+ border-radius: 6px;
104
+ border: 1px solid #e9ecef;
105
+ }
106
+ .performance-table {
107
+ width: 100%;
108
+ border-collapse: collapse;
109
+ margin-top: 16px;
110
+ }
111
+ .performance-table th,
112
+ .performance-table td {
113
+ padding: 12px 8px;
114
+ text-align: left;
115
+ border-bottom: 1px solid #e9ecef;
116
+ }
117
+ .performance-table th {
118
+ background: #f8f9fa;
119
+ font-weight: 600;
120
+ color: #495057;
121
+ }
122
+ .performance-table td {
123
+ color: #333;
124
+ }
125
+ .change-rate {
126
+ display: flex;
127
+ align-items: center;
128
+ gap: 4px;
129
+ }
130
+ .change-up {
131
+ color: #dc3545;
132
+ }
133
+ .change-down {
134
+ color: #198754;
135
+ }
136
+ .change-neutral {
137
+ color: #6c757d;
138
+ }
139
+ .trend-chart {
140
+ width: 60px;
141
+ height: 30px;
142
+ background: #f8f9fa;
143
+ border-radius: 4px;
144
+ display: flex;
145
+ align-items: center;
146
+ justify-content: center;
147
+ font-size: 0.8rem;
148
+ color: #666;
149
+ }
150
+ .download-button {
151
+ margin-top: 16px;
152
+ padding: 8px 16px;
153
+ background: #28a745;
154
+ color: white;
155
+ border: none;
156
+ border-radius: 6px;
157
+ cursor: pointer;
158
+ font-size: 0.9rem;
159
+ display: flex;
160
+ align-items: center;
161
+ gap: 8px;
162
+ }
163
+ .download-button:hover {
164
+ background: #218838;
165
+ }
166
+ .category-select {
167
+ width: 100%;
168
+ padding: 8px 12px;
169
+ border: 1px solid #ced4da;
170
+ border-radius: 6px;
171
+ background: white;
172
+ margin-bottom: 20px;
173
+ }
174
+ `; }
175
+ connectedCallback() {
176
+ super.connectedCallback();
177
+ this.generateChartData();
178
+ }
179
+ generateTrendData(region) {
180
+ // 각 지역별로 7일간의 트렌드 데이터 생성
181
+ const baseValue = Math.random() * 30 + 60; // 60-90 범위
182
+ const trendData = [];
183
+ for (let i = 0; i < 7; i++) {
184
+ const trend = Math.sin(i * 0.5) * 5; // 사인파로 변동
185
+ const noise = (Math.random() - 0.5) * 3; // 랜덤 노이즈
186
+ const value = Math.max(0, Math.min(100, baseValue + trend + noise));
187
+ trendData.push(Math.round(value));
188
+ }
189
+ return trendData;
190
+ }
191
+ generateChartData() {
192
+ // 선택된 카테고리에 따른 차트 데이터 생성
193
+ const categories = ['일정 성과', '비용 성과', '품질 성과', '안전 성과', '환경 성과'];
194
+ this.chartCategories = categories;
195
+ if (this.selectedChartType === 'radar') {
196
+ // 레이더 차트용 데이터 생성
197
+ this.chartData = categories.map(category => ({
198
+ category,
199
+ value: Math.random() * 50 + 25, // 25-75 범위
200
+ group: this.selectedCategory
201
+ }));
202
+ }
203
+ else {
204
+ // 박스플롯용 데이터 생성
205
+ this.chartData = categories.map(category => {
206
+ const baseValue = Math.random() * 50 + 25; // 25-75 범위
207
+ const variation = Math.random() * 20; // 변동폭
208
+ // 각 카테고리별로 20개의 데이터 포인트 생성
209
+ const dataPoints = [];
210
+ for (let i = 0; i < 20; i++) {
211
+ dataPoints.push({
212
+ value: baseValue + (Math.random() - 0.5) * variation,
213
+ group: category
214
+ });
215
+ }
216
+ // 통계값 계산
217
+ const values = dataPoints.map(d => d.value).sort((a, b) => a - b);
218
+ const min = Math.min(...values);
219
+ const max = Math.max(...values);
220
+ const q1 = values[Math.floor(values.length * 0.25)];
221
+ const q3 = values[Math.floor(values.length * 0.75)];
222
+ const median = values[Math.floor(values.length * 0.5)];
223
+ const mean = values.reduce((a, b) => a + b, 0) / values.length;
224
+ return {
225
+ group: category,
226
+ min: min,
227
+ max: max,
228
+ q1: q1,
229
+ q3: q3,
230
+ median: median,
231
+ mean: mean,
232
+ value: mean
233
+ };
234
+ });
235
+ }
236
+ }
237
+ onCategoryChange(event) {
238
+ const target = event.target;
239
+ this.dispatchEvent(new CustomEvent('category-change', {
240
+ detail: { category: target.value },
241
+ bubbles: true,
242
+ composed: true
243
+ }));
244
+ this.generateChartData();
245
+ }
246
+ onChartTypeChange(type) {
247
+ this.selectedChartType = type;
248
+ this.generateChartData();
249
+ }
250
+ onRegionClick(region) {
251
+ this.dispatchEvent(new CustomEvent('region-click', {
252
+ detail: { region },
253
+ bubbles: true,
254
+ composed: true
255
+ }));
256
+ }
257
+ downloadExcel() {
258
+ this.dispatchEvent(new CustomEvent('download-excel', {
259
+ bubbles: true,
260
+ composed: true
261
+ }));
262
+ }
263
+ getChangeRateClass(change) {
264
+ if (change > 0)
265
+ return 'change-up';
266
+ if (change < 0)
267
+ return 'change-down';
268
+ return 'change-neutral';
269
+ }
270
+ getChangeIcon(change) {
271
+ if (change > 0)
272
+ return '▲';
273
+ if (change < 0)
274
+ return '▼';
275
+ return '─';
276
+ }
277
+ render() {
278
+ return html `
279
+ <div class="panel-header">
280
+ <div class="panel-title">전국 KPI</div>
281
+ <button class="panel-close" style="visibility: hidden;">×</button>
282
+ </div>
283
+ <div class="panel-content">
284
+ <!-- KPI 카테고리 선택 -->
285
+ <select class="category-select" .value=${this.selectedCategory} @change=${this.onCategoryChange}>
286
+ <option value="전체 KPI">전체 KPI</option>
287
+ <option value="일정 성과">일정 성과</option>
288
+ <option value="비용 성과">비용 성과</option>
289
+ <option value="품질 성과">품질 성과</option>
290
+ <option value="안전 성과">안전 성과</option>
291
+ <option value="환경 성과">환경 성과</option>
292
+ </select>
293
+
294
+ <!-- 종합 성과 -->
295
+ <div class="chart-section">
296
+ <div class="sub-title">종합 성과</div>
297
+ <div class="chart-toggle">
298
+ <button
299
+ class="toggle-button ${this.selectedChartType === 'boxplot' ? 'active' : ''}"
300
+ @click=${() => this.onChartTypeChange('boxplot')}
301
+ >
302
+ 박스플롯
303
+ </button>
304
+ <button
305
+ class="toggle-button ${this.selectedChartType === 'radar' ? 'active' : ''}"
306
+ @click=${() => this.onChartTypeChange('radar')}
307
+ >
308
+ 레이더차트
309
+ </button>
310
+ </div>
311
+ <div class="chart-container">
312
+ ${this.selectedChartType === 'boxplot'
313
+ ? html `
314
+ <kpi-boxplot-chart
315
+ .data=${this.chartData}
316
+ .groups=${this.chartCategories}
317
+ .valueKey=${'value'}
318
+ .currentGroup=${this.selectedCategory}
319
+ ></kpi-boxplot-chart>
320
+ `
321
+ : html `
322
+ <kpi-radar-chart
323
+ .data=${this.chartData}
324
+ .categories=${this.chartCategories}
325
+ .valueKey=${'value'}
326
+ .currentGroup=${this.selectedCategory}
327
+ ></kpi-radar-chart>
328
+ `}
329
+ </div>
330
+ </div>
331
+
332
+ <!-- 시도별 성과 -->
333
+ <div class="chart-section">
334
+ <div class="sub-title">시도별 성과</div>
335
+ <table class="performance-table">
336
+ <thead>
337
+ <tr>
338
+ <th>지역명</th>
339
+ <th>KPI</th>
340
+ <th>변동률(%)</th>
341
+ <th>성과 추이</th>
342
+ </tr>
343
+ </thead>
344
+ <tbody>
345
+ ${this.mapData?.slice(0, 5).map((item) => html `
346
+ <tr
347
+ style="cursor: pointer; transition: background-color 0.2s;"
348
+ @click=${() => this.onRegionClick(item.region)}
349
+ @mouseenter=${(e) => (e.target.style.backgroundColor = '#f8f9fa')}
350
+ @mouseleave=${(e) => (e.target.style.backgroundColor = '')}
351
+ >
352
+ <td>${item.region}</td>
353
+ <td>${item.kpi}</td>
354
+ <td>
355
+ <div class="change-rate ${this.getChangeRateClass(item.change)}">
356
+ ${this.getChangeIcon(item.change)}${Math.abs(item.change)}%
357
+ </div>
358
+ </td>
359
+ <td>
360
+ <kpi-mini-trend-chart
361
+ .data=${this.generateTrendData(item.region)}
362
+ .width=${60}
363
+ .height=${30}
364
+ .lineColor=${'#2196f3'}
365
+ .strokeWidth=${1.5}
366
+ .showPoints=${true}
367
+ .pointRadius=${1.5}
368
+ ></kpi-mini-trend-chart>
369
+ </td>
370
+ </tr>
371
+ `)}
372
+ </tbody>
373
+ </table>
374
+ <button class="download-button" @click=${this.downloadExcel}>📊 엑셀 다운로드</button>
375
+ </div>
376
+ </div>
377
+ `;
378
+ }
379
+ };
380
+ __decorate([
381
+ property({ type: String }),
382
+ __metadata("design:type", Object)
383
+ ], KpiLeftPanel.prototype, "selectedCategory", void 0);
384
+ __decorate([
385
+ property({ type: String }),
386
+ __metadata("design:type", Object)
387
+ ], KpiLeftPanel.prototype, "selectedChartType", void 0);
388
+ __decorate([
389
+ property({ type: Array }),
390
+ __metadata("design:type", Array)
391
+ ], KpiLeftPanel.prototype, "mapData", void 0);
392
+ __decorate([
393
+ state(),
394
+ __metadata("design:type", Array)
395
+ ], KpiLeftPanel.prototype, "chartData", void 0);
396
+ __decorate([
397
+ state(),
398
+ __metadata("design:type", Array)
399
+ ], KpiLeftPanel.prototype, "chartCategories", void 0);
400
+ KpiLeftPanel = __decorate([
401
+ customElement('kpi-left-panel')
402
+ ], KpiLeftPanel);
403
+ export { KpiLeftPanel };
404
+ //# sourceMappingURL=kpi-left-panel.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"kpi-left-panel.js","sourceRoot":"","sources":["../../../../client/pages/kpi-dashboard/components/kpi-left-panel.ts"],"names":[],"mappings":"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","sourcesContent":["import { LitElement, html, css } from 'lit'\nimport { customElement, property, state } from 'lit/decorators.js'\n\nimport '../../../charts/kpi-radar-chart.js'\nimport '../../../charts/kpi-boxplot-chart.js'\nimport '../../../charts/kpi-mini-trend-chart.js'\n\n@customElement('kpi-left-panel')\nexport class KpiLeftPanel extends LitElement {\n static styles = css`\n :host {\n display: block;\n width: 400px;\n background: #fff;\n border-right: 1px solid #e0e0e0;\n overflow: hidden;\n display: flex;\n flex-direction: column;\n box-shadow: 2px 0 8px rgba(0, 0, 0, 0.1);\n }\n .panel-content {\n padding: 24px;\n overflow-y: auto;\n flex: 1;\n }\n .panel-header {\n display: flex;\n justify-content: space-between;\n align-items: center;\n padding: 20px 24px;\n border-bottom: 1px solid #e0e0e0;\n background: #fff;\n height: 70px;\n box-sizing: border-box;\n }\n .panel-title {\n font-size: 1.3rem;\n font-weight: bold;\n color: #333;\n margin: 0;\n }\n .panel-close {\n width: 32px;\n height: 32px;\n border: none;\n background: #fff;\n border-radius: 50%;\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 1.2rem;\n color: #666;\n transition: all 0.2s;\n }\n .panel-close:hover {\n background: #e9ecef;\n color: #333;\n }\n .sub-title {\n font-size: 1rem;\n font-weight: 600;\n margin-bottom: 16px;\n color: #495057;\n }\n .chart-section {\n background: #f8f9fa;\n border-radius: 8px;\n padding: 16px;\n margin-bottom: 20px;\n }\n .chart-toggle {\n display: flex;\n gap: 8px;\n margin-bottom: 16px;\n }\n .toggle-button {\n padding: 8px 16px;\n border: 1px solid #ced4da;\n background: #fff;\n border-radius: 6px;\n cursor: pointer;\n font-size: 0.9rem;\n transition: all 0.2s;\n }\n .toggle-button.active {\n background: #667eea;\n color: white;\n border-color: #667eea;\n }\n .chart-container {\n height: 300px;\n display: flex;\n align-items: center;\n justify-content: center;\n background: white;\n border-radius: 6px;\n border: 1px solid #e9ecef;\n }\n .performance-table {\n width: 100%;\n border-collapse: collapse;\n margin-top: 16px;\n }\n .performance-table th,\n .performance-table td {\n padding: 12px 8px;\n text-align: left;\n border-bottom: 1px solid #e9ecef;\n }\n .performance-table th {\n background: #f8f9fa;\n font-weight: 600;\n color: #495057;\n }\n .performance-table td {\n color: #333;\n }\n .change-rate {\n display: flex;\n align-items: center;\n gap: 4px;\n }\n .change-up {\n color: #dc3545;\n }\n .change-down {\n color: #198754;\n }\n .change-neutral {\n color: #6c757d;\n }\n .trend-chart {\n width: 60px;\n height: 30px;\n background: #f8f9fa;\n border-radius: 4px;\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 0.8rem;\n color: #666;\n }\n .download-button {\n margin-top: 16px;\n padding: 8px 16px;\n background: #28a745;\n color: white;\n border: none;\n border-radius: 6px;\n cursor: pointer;\n font-size: 0.9rem;\n display: flex;\n align-items: center;\n gap: 8px;\n }\n .download-button:hover {\n background: #218838;\n }\n .category-select {\n width: 100%;\n padding: 8px 12px;\n border: 1px solid #ced4da;\n border-radius: 6px;\n background: white;\n margin-bottom: 20px;\n }\n `\n\n @property({ type: String }) selectedCategory = '전체 KPI'\n @property({ type: String }) selectedChartType = 'boxplot'\n @property({ type: Array }) mapData: any[] = []\n\n @state() private chartData: any[] = []\n @state() private chartCategories: string[] = []\n\n connectedCallback() {\n super.connectedCallback()\n this.generateChartData()\n }\n\n private generateTrendData(region: string): number[] {\n // 각 지역별로 7일간의 트렌드 데이터 생성\n const baseValue = Math.random() * 30 + 60 // 60-90 범위\n const trendData: number[] = []\n\n for (let i = 0; i < 7; i++) {\n const trend = Math.sin(i * 0.5) * 5 // 사인파로 변동\n const noise = (Math.random() - 0.5) * 3 // 랜덤 노이즈\n const value = Math.max(0, Math.min(100, baseValue + trend + noise))\n trendData.push(Math.round(value))\n }\n\n return trendData\n }\n\n private generateChartData() {\n // 선택된 카테고리에 따른 차트 데이터 생성\n const categories = ['일정 성과', '비용 성과', '품질 성과', '안전 성과', '환경 성과']\n this.chartCategories = categories\n\n if (this.selectedChartType === 'radar') {\n // 레이더 차트용 데이터 생성\n this.chartData = categories.map(category => ({\n category,\n value: Math.random() * 50 + 25, // 25-75 범위\n group: this.selectedCategory\n }))\n } else {\n // 박스플롯용 데이터 생성\n this.chartData = categories.map(category => {\n const baseValue = Math.random() * 50 + 25 // 25-75 범위\n const variation = Math.random() * 20 // 변동폭\n\n // 각 카테고리별로 20개의 데이터 포인트 생성\n const dataPoints: { value: number; group: string }[] = []\n for (let i = 0; i < 20; i++) {\n dataPoints.push({\n value: baseValue + (Math.random() - 0.5) * variation,\n group: category\n })\n }\n\n // 통계값 계산\n const values = dataPoints.map(d => d.value).sort((a, b) => a - b)\n const min = Math.min(...values)\n const max = Math.max(...values)\n const q1 = values[Math.floor(values.length * 0.25)]\n const q3 = values[Math.floor(values.length * 0.75)]\n const median = values[Math.floor(values.length * 0.5)]\n const mean = values.reduce((a, b) => a + b, 0) / values.length\n\n return {\n group: category,\n min: min,\n max: max,\n q1: q1,\n q3: q3,\n median: median,\n mean: mean,\n value: mean\n }\n })\n }\n }\n\n private onCategoryChange(event: Event) {\n const target = event.target as HTMLSelectElement\n this.dispatchEvent(\n new CustomEvent('category-change', {\n detail: { category: target.value },\n bubbles: true,\n composed: true\n })\n )\n this.generateChartData()\n }\n\n private onChartTypeChange(type: string) {\n this.selectedChartType = type\n this.generateChartData()\n }\n\n private onRegionClick(region: string) {\n this.dispatchEvent(\n new CustomEvent('region-click', {\n detail: { region },\n bubbles: true,\n composed: true\n })\n )\n }\n\n private downloadExcel() {\n this.dispatchEvent(\n new CustomEvent('download-excel', {\n bubbles: true,\n composed: true\n })\n )\n }\n\n private getChangeRateClass(change: number): string {\n if (change > 0) return 'change-up'\n if (change < 0) return 'change-down'\n return 'change-neutral'\n }\n\n private getChangeIcon(change: number): string {\n if (change > 0) return '▲'\n if (change < 0) return '▼'\n return '─'\n }\n\n render() {\n return html`\n <div class=\"panel-header\">\n <div class=\"panel-title\">전국 KPI</div>\n <button class=\"panel-close\" style=\"visibility: hidden;\">×</button>\n </div>\n <div class=\"panel-content\">\n <!-- KPI 카테고리 선택 -->\n <select class=\"category-select\" .value=${this.selectedCategory} @change=${this.onCategoryChange}>\n <option value=\"전체 KPI\">전체 KPI</option>\n <option value=\"일정 성과\">일정 성과</option>\n <option value=\"비용 성과\">비용 성과</option>\n <option value=\"품질 성과\">품질 성과</option>\n <option value=\"안전 성과\">안전 성과</option>\n <option value=\"환경 성과\">환경 성과</option>\n </select>\n\n <!-- 종합 성과 -->\n <div class=\"chart-section\">\n <div class=\"sub-title\">종합 성과</div>\n <div class=\"chart-toggle\">\n <button\n class=\"toggle-button ${this.selectedChartType === 'boxplot' ? 'active' : ''}\"\n @click=${() => this.onChartTypeChange('boxplot')}\n >\n 박스플롯\n </button>\n <button\n class=\"toggle-button ${this.selectedChartType === 'radar' ? 'active' : ''}\"\n @click=${() => this.onChartTypeChange('radar')}\n >\n 레이더차트\n </button>\n </div>\n <div class=\"chart-container\">\n ${this.selectedChartType === 'boxplot'\n ? html`\n <kpi-boxplot-chart\n .data=${this.chartData}\n .groups=${this.chartCategories}\n .valueKey=${'value'}\n .currentGroup=${this.selectedCategory}\n ></kpi-boxplot-chart>\n `\n : html`\n <kpi-radar-chart\n .data=${this.chartData}\n .categories=${this.chartCategories}\n .valueKey=${'value'}\n .currentGroup=${this.selectedCategory}\n ></kpi-radar-chart>\n `}\n </div>\n </div>\n\n <!-- 시도별 성과 -->\n <div class=\"chart-section\">\n <div class=\"sub-title\">시도별 성과</div>\n <table class=\"performance-table\">\n <thead>\n <tr>\n <th>지역명</th>\n <th>KPI</th>\n <th>변동률(%)</th>\n <th>성과 추이</th>\n </tr>\n </thead>\n <tbody>\n ${this.mapData?.slice(0, 5).map(\n (item: any) => html`\n <tr\n style=\"cursor: pointer; transition: background-color 0.2s;\"\n @click=${() => this.onRegionClick(item.region)}\n @mouseenter=${(e: Event) => ((e.target as HTMLElement).style.backgroundColor = '#f8f9fa')}\n @mouseleave=${(e: Event) => ((e.target as HTMLElement).style.backgroundColor = '')}\n >\n <td>${item.region}</td>\n <td>${item.kpi}</td>\n <td>\n <div class=\"change-rate ${this.getChangeRateClass(item.change)}\">\n ${this.getChangeIcon(item.change)}${Math.abs(item.change)}%\n </div>\n </td>\n <td>\n <kpi-mini-trend-chart\n .data=${this.generateTrendData(item.region)}\n .width=${60}\n .height=${30}\n .lineColor=${'#2196f3'}\n .strokeWidth=${1.5}\n .showPoints=${true}\n .pointRadius=${1.5}\n ></kpi-mini-trend-chart>\n </td>\n </tr>\n `\n )}\n </tbody>\n </table>\n <button class=\"download-button\" @click=${this.downloadExcel}>📊 엑셀 다운로드</button>\n </div>\n </div>\n `\n }\n}\n"]}
@@ -0,0 +1,28 @@
1
+ import { LitElement } from 'lit';
2
+ import '../../../google-map/common-google-map.js';
3
+ declare global {
4
+ interface Window {
5
+ google: any;
6
+ }
7
+ }
8
+ export declare class KpiMapPanel extends LitElement {
9
+ static styles: import("lit").CSSResult;
10
+ selectedCategory: string;
11
+ mapData: any[];
12
+ private map;
13
+ get mapLocations(): {
14
+ lat: any;
15
+ lng: any;
16
+ title: any;
17
+ region: any;
18
+ markerContent: string;
19
+ content: string;
20
+ }[];
21
+ private onCategoryButtonClick;
22
+ private onMapChange;
23
+ private onRegionClick;
24
+ private zoomIn;
25
+ private zoomOut;
26
+ private resetView;
27
+ render(): import("lit-html").TemplateResult<1>;
28
+ }