@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.
- package/client/charts/kpi-mini-trend-chart.ts +125 -0
- package/client/charts/kpi-trend-chart.ts +163 -0
- package/client/google-map/common-google-map.ts +45 -7
- package/client/google-map/google-map-loader.ts +1 -1
- package/client/pages/kpi-dashboard/components/kpi-chart-toggle.ts +1 -2
- package/client/pages/kpi-dashboard/components/kpi-left-panel.ts +399 -0
- package/client/pages/kpi-dashboard/components/kpi-map-panel.ts +110 -30
- package/client/pages/kpi-dashboard/components/kpi-region-popup.ts +355 -0
- package/client/pages/kpi-dashboard/kpi-dashboard-map.ts +46 -589
- package/dist-client/charts/kpi-mini-trend-chart.d.ts +14 -0
- package/dist-client/charts/kpi-mini-trend-chart.js +148 -0
- package/dist-client/charts/kpi-mini-trend-chart.js.map +1 -0
- package/dist-client/charts/kpi-trend-chart.d.ts +25 -0
- package/dist-client/charts/kpi-trend-chart.js +186 -0
- package/dist-client/charts/kpi-trend-chart.js.map +1 -0
- package/dist-client/google-map/common-google-map.js +40 -7
- package/dist-client/google-map/common-google-map.js.map +1 -1
- package/dist-client/google-map/google-map-loader.js +1 -1
- package/dist-client/google-map/google-map-loader.js.map +1 -1
- package/dist-client/pages/kpi-dashboard/components/kpi-chart-toggle.js +1 -2
- package/dist-client/pages/kpi-dashboard/components/kpi-chart-toggle.js.map +1 -1
- package/dist-client/pages/kpi-dashboard/components/kpi-left-panel.d.ts +22 -0
- package/dist-client/pages/kpi-dashboard/components/kpi-left-panel.js +404 -0
- package/dist-client/pages/kpi-dashboard/components/kpi-left-panel.js.map +1 -0
- package/dist-client/pages/kpi-dashboard/components/kpi-map-panel.d.ts +6 -1
- package/dist-client/pages/kpi-dashboard/components/kpi-map-panel.js +100 -25
- package/dist-client/pages/kpi-dashboard/components/kpi-map-panel.js.map +1 -1
- package/dist-client/pages/kpi-dashboard/components/kpi-region-popup.d.ts +23 -0
- package/dist-client/pages/kpi-dashboard/components/kpi-region-popup.js +368 -0
- package/dist-client/pages/kpi-dashboard/components/kpi-region-popup.js.map +1 -0
- package/dist-client/pages/kpi-dashboard/kpi-dashboard-map.d.ts +6 -15
- package/dist-client/pages/kpi-dashboard/kpi-dashboard-map.js +43 -585
- package/dist-client/pages/kpi-dashboard/kpi-dashboard-map.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/client/google-map/script-loader.ts +0 -173
- package/dist-client/google-map/script-loader.d.ts +0 -3
- package/dist-client/google-map/script-loader.js +0 -144
- 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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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);
|