@things-factory/kpi 9.0.30 → 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 (39) 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 +45 -7
  4. package/client/google-map/google-map-loader.ts +1 -1
  5. package/client/pages/kpi-dashboard/components/kpi-chart-toggle.ts +1 -2
  6. package/client/pages/kpi-dashboard/components/kpi-left-panel.ts +399 -0
  7. package/client/pages/kpi-dashboard/components/kpi-map-panel.ts +110 -30
  8. package/client/pages/kpi-dashboard/components/kpi-region-popup.ts +355 -0
  9. package/client/pages/kpi-dashboard/kpi-dashboard-map.ts +46 -589
  10. package/dist-client/charts/kpi-mini-trend-chart.d.ts +14 -0
  11. package/dist-client/charts/kpi-mini-trend-chart.js +148 -0
  12. package/dist-client/charts/kpi-mini-trend-chart.js.map +1 -0
  13. package/dist-client/charts/kpi-trend-chart.d.ts +25 -0
  14. package/dist-client/charts/kpi-trend-chart.js +186 -0
  15. package/dist-client/charts/kpi-trend-chart.js.map +1 -0
  16. package/dist-client/google-map/common-google-map.js +40 -7
  17. package/dist-client/google-map/common-google-map.js.map +1 -1
  18. package/dist-client/google-map/google-map-loader.js +1 -1
  19. package/dist-client/google-map/google-map-loader.js.map +1 -1
  20. package/dist-client/pages/kpi-dashboard/components/kpi-chart-toggle.js +1 -2
  21. package/dist-client/pages/kpi-dashboard/components/kpi-chart-toggle.js.map +1 -1
  22. package/dist-client/pages/kpi-dashboard/components/kpi-left-panel.d.ts +22 -0
  23. package/dist-client/pages/kpi-dashboard/components/kpi-left-panel.js +404 -0
  24. package/dist-client/pages/kpi-dashboard/components/kpi-left-panel.js.map +1 -0
  25. package/dist-client/pages/kpi-dashboard/components/kpi-map-panel.d.ts +6 -1
  26. package/dist-client/pages/kpi-dashboard/components/kpi-map-panel.js +100 -25
  27. package/dist-client/pages/kpi-dashboard/components/kpi-map-panel.js.map +1 -1
  28. package/dist-client/pages/kpi-dashboard/components/kpi-region-popup.d.ts +23 -0
  29. package/dist-client/pages/kpi-dashboard/components/kpi-region-popup.js +368 -0
  30. package/dist-client/pages/kpi-dashboard/components/kpi-region-popup.js.map +1 -0
  31. package/dist-client/pages/kpi-dashboard/kpi-dashboard-map.d.ts +6 -15
  32. package/dist-client/pages/kpi-dashboard/kpi-dashboard-map.js +43 -585
  33. package/dist-client/pages/kpi-dashboard/kpi-dashboard-map.js.map +1 -1
  34. package/dist-client/tsconfig.tsbuildinfo +1 -1
  35. package/package.json +2 -2
  36. package/client/google-map/script-loader.ts +0 -173
  37. package/dist-client/google-map/script-loader.d.ts +0 -3
  38. package/dist-client/google-map/script-loader.js +0 -144
  39. package/dist-client/google-map/script-loader.js.map +0 -1
@@ -0,0 +1,22 @@
1
+ import { LitElement } from 'lit';
2
+ import '../../../charts/kpi-radar-chart.js';
3
+ import '../../../charts/kpi-boxplot-chart.js';
4
+ import '../../../charts/kpi-mini-trend-chart.js';
5
+ export declare class KpiLeftPanel extends LitElement {
6
+ static styles: import("lit").CSSResult;
7
+ selectedCategory: string;
8
+ selectedChartType: string;
9
+ mapData: any[];
10
+ private chartData;
11
+ private chartCategories;
12
+ connectedCallback(): void;
13
+ private generateTrendData;
14
+ private generateChartData;
15
+ private onCategoryChange;
16
+ private onChartTypeChange;
17
+ private onRegionClick;
18
+ private downloadExcel;
19
+ private getChangeRateClass;
20
+ private getChangeIcon;
21
+ render(): import("lit-html").TemplateResult<1>;
22
+ }
@@ -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"]}
@@ -10,14 +10,19 @@ export declare class KpiMapPanel extends LitElement {
10
10
  selectedCategory: string;
11
11
  mapData: any[];
12
12
  private map;
13
- private mapInitialized;
14
13
  get mapLocations(): {
15
14
  lat: any;
16
15
  lng: any;
17
16
  title: any;
17
+ region: any;
18
+ markerContent: string;
18
19
  content: string;
19
20
  }[];
20
21
  private onCategoryButtonClick;
21
22
  private onMapChange;
23
+ private onRegionClick;
24
+ private zoomIn;
25
+ private zoomOut;
26
+ private resetView;
22
27
  render(): import("lit-html").TemplateResult<1>;
23
28
  }
@@ -1,7 +1,6 @@
1
1
  import { __decorate, __metadata } from "tslib";
2
- import { html, css } from 'lit';
2
+ import { LitElement, html, css } from 'lit';
3
3
  import { customElement, property, state } from 'lit/decorators.js';
4
- import { LitElement } from 'lit';
5
4
  import '../../../google-map/common-google-map.js';
6
5
  let KpiMapPanel = class KpiMapPanel extends LitElement {
7
6
  constructor() {
@@ -9,7 +8,6 @@ let KpiMapPanel = class KpiMapPanel extends LitElement {
9
8
  this.selectedCategory = '전체 KPI';
10
9
  this.mapData = [];
11
10
  this.map = null;
12
- this.mapInitialized = false;
13
11
  }
14
12
  static { this.styles = css `
15
13
  :host {
@@ -86,7 +84,7 @@ let KpiMapPanel = class KpiMapPanel extends LitElement {
86
84
  .map-control-button:hover {
87
85
  background: #f8f9fa;
88
86
  }
89
- .map-scale {
87
+ .map-scale-direction {
90
88
  position: absolute;
91
89
  bottom: 16px;
92
90
  right: 16px;
@@ -97,6 +95,15 @@ let KpiMapPanel = class KpiMapPanel extends LitElement {
97
95
  font-size: 0.8rem;
98
96
  color: #666;
99
97
  z-index: 10;
98
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
99
+ text-align: center;
100
+ }
101
+ .north-arrow {
102
+ font-size: 1rem;
103
+ margin-bottom: 4px;
104
+ }
105
+ .scale-info {
106
+ font-size: 0.7rem;
100
107
  }
101
108
  common-google-map {
102
109
  width: 100%;
@@ -109,15 +116,56 @@ let KpiMapPanel = class KpiMapPanel extends LitElement {
109
116
  lat: item.lat,
110
117
  lng: item.lng,
111
118
  title: item.region,
119
+ region: item.region, // 지역명 추가
120
+ // 커스텀 마커 콘텐츠 생성
121
+ markerContent: `
122
+ <div style="
123
+ background: white;
124
+ border-radius: 8px;
125
+ padding: 8px 12px;
126
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
127
+ border: 1px solid #e9ecef;
128
+ min-width: 80px;
129
+ text-align: center;
130
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
131
+ cursor: pointer;
132
+ ">
133
+ <div style="
134
+ font-size: 11px;
135
+ font-weight: 600;
136
+ color: #495057;
137
+ margin-bottom: 2px;
138
+ white-space: nowrap;
139
+ ">${item.region}</div>
140
+ <div style="
141
+ font-size: 13px;
142
+ font-weight: 700;
143
+ color: #212529;
144
+ margin-bottom: 2px;
145
+ ">${item.kpi}</div>
146
+ <div style="
147
+ font-size: 10px;
148
+ color: ${item.change > 0 ? '#dc3545' : '#198754'};
149
+ font-weight: 500;
150
+ ">${item.change > 0 ? '▲' : '▼'} ${Math.abs(item.change)}%</div>
151
+ </div>
152
+ `,
112
153
  content: `
113
- <div style="padding: 8px;">
114
- <h3 style="margin: 0 0 8px 0; font-size: 14px;">${item.region}</h3>
115
- <p style="margin: 0 0 4px 0; font-size: 12px;">KPI: ${item.kpi}</p>
116
- <p style="margin: 0; font-size: 12px; color: ${item.change > 0 ? '#dc3545' : '#198754'}">
117
- ${item.change > 0 ? '▲' : '▼'} ${Math.abs(item.change)}%
118
- </p>
119
- </div>
120
- `
154
+ <div style="padding: 12px; min-width: 200px;">
155
+ <h3 style="margin: 0 0 8px 0; font-size: 16px; color: #212529;">${item.region}</h3>
156
+ <div style="margin-bottom: 8px;">
157
+ <span style="font-size: 14px; color: #6c757d;">KPI: </span>
158
+ <span style="font-size: 16px; font-weight: 600; color: #212529;">${item.kpi}</span>
159
+ </div>
160
+ <div style="
161
+ font-size: 14px;
162
+ color: ${item.change > 0 ? '#dc3545' : '#198754'};
163
+ font-weight: 500;
164
+ ">
165
+ ${item.change > 0 ? '▲' : '▼'} ${Math.abs(item.change)}% 변화
166
+ </div>
167
+ </div>
168
+ `
121
169
  })) || []);
122
170
  }
123
171
  onCategoryButtonClick(category) {
@@ -129,7 +177,29 @@ let KpiMapPanel = class KpiMapPanel extends LitElement {
129
177
  }
130
178
  onMapChange(event) {
131
179
  this.map = event.detail;
132
- this.mapInitialized = true;
180
+ }
181
+ onRegionClick(event) {
182
+ this.dispatchEvent(new CustomEvent('region-click', {
183
+ detail: event.detail,
184
+ bubbles: true,
185
+ composed: true
186
+ }));
187
+ }
188
+ zoomIn() {
189
+ if (this.map) {
190
+ this.map.setZoom(this.map.getZoom() + 1);
191
+ }
192
+ }
193
+ zoomOut() {
194
+ if (this.map) {
195
+ this.map.setZoom(this.map.getZoom() - 1);
196
+ }
197
+ }
198
+ resetView() {
199
+ if (this.map) {
200
+ this.map.setCenter({ lat: 36.5, lng: 127.5 });
201
+ this.map.setZoom(7);
202
+ }
133
203
  }
134
204
  render() {
135
205
  return html `
@@ -175,17 +245,17 @@ let KpiMapPanel = class KpiMapPanel extends LitElement {
175
245
  </div>
176
246
 
177
247
  <div class="map-container">
178
- <!-- 지도 컨트롤 -->
248
+ <!-- 지도 컨트롤 (오른쪽 상단) -->
179
249
  <div class="map-controls">
180
- <button class="map-control-button" title="확대">+</button>
181
- <button class="map-control-button" title="축소">-</button>
182
- <button class="map-control-button" title="뷰 초기화">⌖</button>
250
+ <button class="map-control-button" title="확대" @click=${() => this.zoomIn()}>+</button>
251
+ <button class="map-control-button" title="축소" @click=${() => this.zoomOut()}>-</button>
252
+ <button class="map-control-button" title="뷰 초기화" @click=${() => this.resetView()}>⌖</button>
183
253
  </div>
184
254
 
185
- <!-- 지도 스케일 -->
186
- <div class="map-scale">
187
- N<br />
188
- 25km
255
+ <!-- 스케일 및 방향 정보 (오른쪽 하단) -->
256
+ <div class="map-scale-direction">
257
+ <div class="north-arrow">↑ N</div>
258
+ <div class="scale-info">25km</div>
189
259
  </div>
190
260
 
191
261
  <!-- 공통 Google Maps 컴포넌트 사용 -->
@@ -194,7 +264,16 @@ let KpiMapPanel = class KpiMapPanel extends LitElement {
194
264
  .zoom=${7}
195
265
  .locations=${this.mapLocations}
196
266
  .clusterZoom=${10}
267
+ .controls=${{
268
+ zoomControl: false,
269
+ mapTypeControl: false,
270
+ scaleControl: false,
271
+ streetViewControl: false,
272
+ rotateControl: false,
273
+ fullscreenControl: false
274
+ }}
197
275
  @map-change=${this.onMapChange}
276
+ @region-click=${this.onRegionClick}
198
277
  ></common-google-map>
199
278
  </div>
200
279
  `;
@@ -212,10 +291,6 @@ __decorate([
212
291
  state(),
213
292
  __metadata("design:type", Object)
214
293
  ], KpiMapPanel.prototype, "map", void 0);
215
- __decorate([
216
- state(),
217
- __metadata("design:type", Object)
218
- ], KpiMapPanel.prototype, "mapInitialized", void 0);
219
294
  KpiMapPanel = __decorate([
220
295
  customElement('kpi-map-panel')
221
296
  ], KpiMapPanel);