@things-factory/kpi 9.0.29 → 9.0.30
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/google-map/common-google-map.ts +332 -0
- package/client/google-map/google-map-loader.ts +29 -0
- package/client/google-map/script-loader.ts +173 -0
- package/client/pages/kpi-dashboard/cards/kpi-level1-card.ts +248 -0
- package/client/pages/kpi-dashboard/cards/kpi-level2-comparison.ts +369 -0
- package/client/pages/kpi-dashboard/cards/kpi-level3-comparison.ts +443 -0
- package/client/pages/kpi-dashboard/components/kpi-chart-toggle.ts +73 -0
- package/client/pages/kpi-dashboard/components/kpi-map-panel.ts +222 -0
- package/client/pages/kpi-dashboard/kpi-dashboard-map.ts +786 -0
- package/client/pages/kpi-dashboard/kpi-dashboard.ts +416 -0
- package/client/route.ts +4 -0
- package/dist-client/google-map/common-google-map.d.ts +34 -0
- package/dist-client/google-map/common-google-map.js +300 -0
- package/dist-client/google-map/common-google-map.js.map +1 -0
- package/dist-client/google-map/google-map-loader.d.ts +6 -0
- package/dist-client/google-map/google-map-loader.js +22 -0
- package/dist-client/google-map/google-map-loader.js.map +1 -0
- package/dist-client/google-map/script-loader.d.ts +3 -0
- package/dist-client/google-map/script-loader.js +144 -0
- package/dist-client/google-map/script-loader.js.map +1 -0
- package/dist-client/pages/kpi-dashboard/cards/kpi-level1-card.d.ts +17 -0
- package/dist-client/pages/kpi-dashboard/cards/kpi-level1-card.js +279 -0
- package/dist-client/pages/kpi-dashboard/cards/kpi-level1-card.js.map +1 -0
- package/dist-client/pages/kpi-dashboard/cards/kpi-level2-comparison.d.ts +19 -0
- package/dist-client/pages/kpi-dashboard/cards/kpi-level2-comparison.js +385 -0
- package/dist-client/pages/kpi-dashboard/cards/kpi-level2-comparison.js.map +1 -0
- package/dist-client/pages/kpi-dashboard/cards/kpi-level3-comparison.d.ts +23 -0
- package/dist-client/pages/kpi-dashboard/cards/kpi-level3-comparison.js +465 -0
- package/dist-client/pages/kpi-dashboard/cards/kpi-level3-comparison.js.map +1 -0
- package/dist-client/pages/kpi-dashboard/components/kpi-chart-toggle.d.ts +8 -0
- package/dist-client/pages/kpi-dashboard/components/kpi-chart-toggle.js +79 -0
- package/dist-client/pages/kpi-dashboard/components/kpi-chart-toggle.js.map +1 -0
- package/dist-client/pages/kpi-dashboard/components/kpi-map-panel.d.ts +23 -0
- package/dist-client/pages/kpi-dashboard/components/kpi-map-panel.js +223 -0
- package/dist-client/pages/kpi-dashboard/components/kpi-map-panel.js.map +1 -0
- package/dist-client/pages/kpi-dashboard/kpi-dashboard-map.d.ts +38 -0
- package/dist-client/pages/kpi-dashboard/kpi-dashboard-map.js +813 -0
- package/dist-client/pages/kpi-dashboard/kpi-dashboard-map.js.map +1 -0
- package/dist-client/pages/kpi-dashboard/kpi-dashboard.d.ts +21 -0
- package/dist-client/pages/kpi-dashboard/kpi-dashboard.js +398 -0
- package/dist-client/pages/kpi-dashboard/kpi-dashboard.js.map +1 -1
- package/dist-client/route.d.ts +1 -1
- package/dist-client/route.js +3 -0
- package/dist-client/route.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-server/index.d.ts +1 -0
- package/dist-server/index.js +1 -0
- package/dist-server/index.js.map +1 -1
- package/dist-server/migrations/index.d.ts +1 -0
- package/dist-server/migrations/index.js +12 -0
- package/dist-server/migrations/index.js.map +1 -0
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/server/index.ts +1 -0
- package/server/migrations/index.ts +9 -0
- package/things-factory.config.js +2 -1
- package/translations/en.json +1 -0
- package/translations/ja.json +1 -0
- package/translations/ko.json +1 -0
- package/translations/ms.json +1 -0
- package/translations/zh.json +1 -0
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { html, css } from 'lit'
|
|
2
|
+
import { customElement, state } from 'lit/decorators.js'
|
|
3
|
+
import { LitElement } from 'lit'
|
|
4
|
+
import { client } from '@operato/graphql'
|
|
5
|
+
import gql from 'graphql-tag'
|
|
6
|
+
|
|
7
|
+
@customElement('kpi-level1-card')
|
|
8
|
+
export class KpiLevel1Card extends LitElement {
|
|
9
|
+
static styles = css`
|
|
10
|
+
:host {
|
|
11
|
+
display: block;
|
|
12
|
+
}
|
|
13
|
+
.score-card {
|
|
14
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
15
|
+
border-radius: 16px;
|
|
16
|
+
padding: 32px;
|
|
17
|
+
color: white;
|
|
18
|
+
box-shadow: 0 8px 32px rgba(102, 126, 234, 0.3);
|
|
19
|
+
text-align: center;
|
|
20
|
+
position: relative;
|
|
21
|
+
overflow: hidden;
|
|
22
|
+
}
|
|
23
|
+
.score-card::before {
|
|
24
|
+
content: '';
|
|
25
|
+
position: absolute;
|
|
26
|
+
top: 0;
|
|
27
|
+
left: 0;
|
|
28
|
+
right: 0;
|
|
29
|
+
bottom: 0;
|
|
30
|
+
background: rgba(255, 255, 255, 0.1);
|
|
31
|
+
border-radius: 16px;
|
|
32
|
+
}
|
|
33
|
+
.score-content {
|
|
34
|
+
position: relative;
|
|
35
|
+
z-index: 1;
|
|
36
|
+
}
|
|
37
|
+
.score-title {
|
|
38
|
+
font-size: 1.2rem;
|
|
39
|
+
font-weight: 600;
|
|
40
|
+
margin-bottom: 16px;
|
|
41
|
+
opacity: 0.9;
|
|
42
|
+
}
|
|
43
|
+
.score-value {
|
|
44
|
+
font-size: 4rem;
|
|
45
|
+
font-weight: bold;
|
|
46
|
+
margin-bottom: 8px;
|
|
47
|
+
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
|
48
|
+
}
|
|
49
|
+
.score-grade {
|
|
50
|
+
font-size: 1.5rem;
|
|
51
|
+
font-weight: 600;
|
|
52
|
+
margin-bottom: 16px;
|
|
53
|
+
opacity: 0.9;
|
|
54
|
+
}
|
|
55
|
+
.score-period {
|
|
56
|
+
font-size: 0.9rem;
|
|
57
|
+
opacity: 0.7;
|
|
58
|
+
margin-bottom: 16px;
|
|
59
|
+
}
|
|
60
|
+
.score-details {
|
|
61
|
+
display: flex;
|
|
62
|
+
justify-content: space-around;
|
|
63
|
+
margin-top: 24px;
|
|
64
|
+
padding-top: 16px;
|
|
65
|
+
border-top: 1px solid rgba(255, 255, 255, 0.2);
|
|
66
|
+
}
|
|
67
|
+
.detail-item {
|
|
68
|
+
text-align: center;
|
|
69
|
+
}
|
|
70
|
+
.detail-value {
|
|
71
|
+
font-size: 1.4rem;
|
|
72
|
+
font-weight: bold;
|
|
73
|
+
margin-bottom: 4px;
|
|
74
|
+
}
|
|
75
|
+
.detail-label {
|
|
76
|
+
font-size: 0.8rem;
|
|
77
|
+
opacity: 0.8;
|
|
78
|
+
}
|
|
79
|
+
.loading {
|
|
80
|
+
display: flex;
|
|
81
|
+
align-items: center;
|
|
82
|
+
justify-content: center;
|
|
83
|
+
height: 200px;
|
|
84
|
+
color: #666;
|
|
85
|
+
font-size: 1.1rem;
|
|
86
|
+
}
|
|
87
|
+
.error {
|
|
88
|
+
display: flex;
|
|
89
|
+
align-items: center;
|
|
90
|
+
justify-content: center;
|
|
91
|
+
height: 200px;
|
|
92
|
+
color: #d32f2f;
|
|
93
|
+
font-size: 1.1rem;
|
|
94
|
+
}
|
|
95
|
+
`
|
|
96
|
+
|
|
97
|
+
@state() loading = true
|
|
98
|
+
@state() error = ''
|
|
99
|
+
@state() totalScore = 0
|
|
100
|
+
@state() grade = ''
|
|
101
|
+
@state() totalKpis = 0
|
|
102
|
+
@state() totalCategories = 0
|
|
103
|
+
@state() averageScore = 0
|
|
104
|
+
@state() currentMonth = ''
|
|
105
|
+
|
|
106
|
+
connectedCallback() {
|
|
107
|
+
super.connectedCallback()
|
|
108
|
+
this.currentMonth = this.getCurrentMonth()
|
|
109
|
+
this.fetchTotalScore()
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
private getCurrentMonth(): string {
|
|
113
|
+
const now = new Date()
|
|
114
|
+
const year = now.getFullYear()
|
|
115
|
+
const month = String(now.getMonth() + 1).padStart(2, '0')
|
|
116
|
+
return `${year}-${month}`
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
private calculateGrade(score: number): string {
|
|
120
|
+
if (score >= 90) return 'A+'
|
|
121
|
+
if (score >= 85) return 'A'
|
|
122
|
+
if (score >= 80) return 'A-'
|
|
123
|
+
if (score >= 75) return 'B+'
|
|
124
|
+
if (score >= 70) return 'B'
|
|
125
|
+
if (score >= 65) return 'B-'
|
|
126
|
+
if (score >= 60) return 'C+'
|
|
127
|
+
if (score >= 55) return 'C'
|
|
128
|
+
if (score >= 50) return 'C-'
|
|
129
|
+
return 'D'
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async fetchTotalScore() {
|
|
133
|
+
this.loading = true
|
|
134
|
+
this.error = ''
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
const response = await client.query({
|
|
138
|
+
query: gql`
|
|
139
|
+
query {
|
|
140
|
+
kpiStatistics {
|
|
141
|
+
items {
|
|
142
|
+
id
|
|
143
|
+
valueDate
|
|
144
|
+
periodType
|
|
145
|
+
mean
|
|
146
|
+
median
|
|
147
|
+
kpi {
|
|
148
|
+
id
|
|
149
|
+
name
|
|
150
|
+
targetValue
|
|
151
|
+
unit
|
|
152
|
+
category {
|
|
153
|
+
id
|
|
154
|
+
name
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
`
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
const statistics = response.data.kpiStatistics.items || []
|
|
164
|
+
|
|
165
|
+
// MONTH 타입의 현재 월 데이터만 필터링
|
|
166
|
+
const currentMonthStats = statistics.filter(
|
|
167
|
+
stat => stat.periodType === 'MONTH' && stat.valueDate === this.currentMonth
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
if (currentMonthStats.length === 0) {
|
|
171
|
+
this.error = '현재 월의 데이터가 없습니다.'
|
|
172
|
+
return
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// 총점 계산 (평균값 기준)
|
|
176
|
+
const scores = currentMonthStats.map(stat => {
|
|
177
|
+
const mean = stat.mean || 0
|
|
178
|
+
const targetValue = stat.kpi?.targetValue || 100
|
|
179
|
+
|
|
180
|
+
if (targetValue === 0) return 0
|
|
181
|
+
|
|
182
|
+
// 목표 대비 달성률 계산 (최대 100점)
|
|
183
|
+
const achievement = Math.min((mean / targetValue) * 100, 100)
|
|
184
|
+
return Math.max(achievement, 0) // 음수 방지
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
this.totalScore = Math.round(scores.reduce((sum, score) => sum + score, 0) / scores.length)
|
|
188
|
+
this.grade = this.calculateGrade(this.totalScore)
|
|
189
|
+
this.totalKpis = currentMonthStats.length
|
|
190
|
+
|
|
191
|
+
// 카테고리 수 계산
|
|
192
|
+
const categories = new Set(currentMonthStats.map(stat => stat.kpi?.category?.name).filter(Boolean))
|
|
193
|
+
this.totalCategories = categories.size
|
|
194
|
+
|
|
195
|
+
// 평균 점수 계산
|
|
196
|
+
this.averageScore = Math.round(scores.reduce((sum, score) => sum + score, 0) / scores.length)
|
|
197
|
+
} catch (e) {
|
|
198
|
+
console.error('총점 데이터를 불러오지 못했습니다:', e)
|
|
199
|
+
this.error = '총점 데이터를 불러오지 못했습니다.'
|
|
200
|
+
} finally {
|
|
201
|
+
this.loading = false
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
render() {
|
|
206
|
+
if (this.loading) {
|
|
207
|
+
return html`
|
|
208
|
+
<div class="score-card">
|
|
209
|
+
<div class="loading">총점 계산 중...</div>
|
|
210
|
+
</div>
|
|
211
|
+
`
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (this.error) {
|
|
215
|
+
return html`
|
|
216
|
+
<div class="score-card">
|
|
217
|
+
<div class="error">${this.error}</div>
|
|
218
|
+
</div>
|
|
219
|
+
`
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return html`
|
|
223
|
+
<div class="score-card">
|
|
224
|
+
<div class="score-content">
|
|
225
|
+
<div class="score-title">그룹 총점</div>
|
|
226
|
+
<div class="score-value">${this.totalScore}</div>
|
|
227
|
+
<div class="score-grade">${this.grade}</div>
|
|
228
|
+
<div class="score-period">${this.currentMonth} 기준</div>
|
|
229
|
+
|
|
230
|
+
<div class="score-details">
|
|
231
|
+
<div class="detail-item">
|
|
232
|
+
<div class="detail-value">${this.totalKpis}</div>
|
|
233
|
+
<div class="detail-label">KPI</div>
|
|
234
|
+
</div>
|
|
235
|
+
<div class="detail-item">
|
|
236
|
+
<div class="detail-value">${this.totalCategories}</div>
|
|
237
|
+
<div class="detail-label">카테고리</div>
|
|
238
|
+
</div>
|
|
239
|
+
<div class="detail-item">
|
|
240
|
+
<div class="detail-value">${this.averageScore}</div>
|
|
241
|
+
<div class="detail-label">평균</div>
|
|
242
|
+
</div>
|
|
243
|
+
</div>
|
|
244
|
+
</div>
|
|
245
|
+
</div>
|
|
246
|
+
`
|
|
247
|
+
}
|
|
248
|
+
}
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
import { html, css } from 'lit'
|
|
2
|
+
import { customElement, state } from 'lit/decorators.js'
|
|
3
|
+
import { LitElement } from 'lit'
|
|
4
|
+
import { client } from '@operato/graphql'
|
|
5
|
+
import gql from 'graphql-tag'
|
|
6
|
+
|
|
7
|
+
@customElement('kpi-level2-comparison')
|
|
8
|
+
export class KpiLevel2Comparison extends LitElement {
|
|
9
|
+
static styles = css`
|
|
10
|
+
:host {
|
|
11
|
+
display: block;
|
|
12
|
+
}
|
|
13
|
+
.comparison-container {
|
|
14
|
+
background: #fff;
|
|
15
|
+
border-radius: 16px;
|
|
16
|
+
padding: 24px;
|
|
17
|
+
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
|
|
18
|
+
border: 1px solid #e0e0e0;
|
|
19
|
+
width: 100%;
|
|
20
|
+
}
|
|
21
|
+
.comparison-title {
|
|
22
|
+
font-size: 1.3rem;
|
|
23
|
+
font-weight: bold;
|
|
24
|
+
margin-bottom: 20px;
|
|
25
|
+
color: #333;
|
|
26
|
+
text-align: center;
|
|
27
|
+
}
|
|
28
|
+
.charts-section {
|
|
29
|
+
display: flex;
|
|
30
|
+
gap: 24px;
|
|
31
|
+
margin-bottom: 20px;
|
|
32
|
+
}
|
|
33
|
+
.chart-card {
|
|
34
|
+
flex: 1;
|
|
35
|
+
background: #f8f9fa;
|
|
36
|
+
border-radius: 12px;
|
|
37
|
+
padding: 20px;
|
|
38
|
+
border: 1px solid #e9ecef;
|
|
39
|
+
}
|
|
40
|
+
.chart-title {
|
|
41
|
+
font-size: 1.1rem;
|
|
42
|
+
font-weight: 600;
|
|
43
|
+
margin-bottom: 16px;
|
|
44
|
+
color: #495057;
|
|
45
|
+
text-align: center;
|
|
46
|
+
}
|
|
47
|
+
.chart-container {
|
|
48
|
+
height: 300px;
|
|
49
|
+
display: flex;
|
|
50
|
+
align-items: center;
|
|
51
|
+
justify-content: center;
|
|
52
|
+
}
|
|
53
|
+
.loading {
|
|
54
|
+
color: #666;
|
|
55
|
+
font-size: 1rem;
|
|
56
|
+
}
|
|
57
|
+
.error {
|
|
58
|
+
color: #d32f2f;
|
|
59
|
+
font-size: 1rem;
|
|
60
|
+
}
|
|
61
|
+
.no-data {
|
|
62
|
+
color: #666;
|
|
63
|
+
font-size: 1rem;
|
|
64
|
+
text-align: center;
|
|
65
|
+
}
|
|
66
|
+
.summary-info {
|
|
67
|
+
display: flex;
|
|
68
|
+
justify-content: space-around;
|
|
69
|
+
padding: 16px;
|
|
70
|
+
background: #f8f9fa;
|
|
71
|
+
border-radius: 8px;
|
|
72
|
+
margin-top: 16px;
|
|
73
|
+
}
|
|
74
|
+
.summary-item {
|
|
75
|
+
text-align: center;
|
|
76
|
+
}
|
|
77
|
+
.summary-value {
|
|
78
|
+
font-size: 1.2rem;
|
|
79
|
+
font-weight: bold;
|
|
80
|
+
color: #333;
|
|
81
|
+
margin-bottom: 4px;
|
|
82
|
+
}
|
|
83
|
+
.summary-label {
|
|
84
|
+
font-size: 0.9rem;
|
|
85
|
+
color: #666;
|
|
86
|
+
}
|
|
87
|
+
`
|
|
88
|
+
|
|
89
|
+
@state() loading = true
|
|
90
|
+
@state() error = ''
|
|
91
|
+
@state() radarData: any[] = []
|
|
92
|
+
@state() boxplotData: any[] = []
|
|
93
|
+
@state() categories: string[] = []
|
|
94
|
+
@state() currentMonth = ''
|
|
95
|
+
@state() totalCategories = 0
|
|
96
|
+
@state() totalKpis = 0
|
|
97
|
+
@state() averageScore = 0
|
|
98
|
+
|
|
99
|
+
connectedCallback() {
|
|
100
|
+
super.connectedCallback()
|
|
101
|
+
this.currentMonth = this.getCurrentMonth()
|
|
102
|
+
this.fetchCategoryComparison()
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
private getCurrentMonth(): string {
|
|
106
|
+
const now = new Date()
|
|
107
|
+
const year = now.getFullYear()
|
|
108
|
+
const month = String(now.getMonth() + 1).padStart(2, '0')
|
|
109
|
+
return `${year}-${month}`
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async fetchCategoryComparison() {
|
|
113
|
+
this.loading = true
|
|
114
|
+
this.error = ''
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
const response = await client.query({
|
|
118
|
+
query: gql`
|
|
119
|
+
query {
|
|
120
|
+
kpiStatistics {
|
|
121
|
+
items {
|
|
122
|
+
id
|
|
123
|
+
valueDate
|
|
124
|
+
periodType
|
|
125
|
+
mean
|
|
126
|
+
median
|
|
127
|
+
standardDeviation
|
|
128
|
+
minimum
|
|
129
|
+
maximum
|
|
130
|
+
percentile25
|
|
131
|
+
percentile75
|
|
132
|
+
kpi {
|
|
133
|
+
id
|
|
134
|
+
name
|
|
135
|
+
targetValue
|
|
136
|
+
unit
|
|
137
|
+
category {
|
|
138
|
+
id
|
|
139
|
+
name
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
`
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
const statistics = response.data.kpiStatistics.items || []
|
|
149
|
+
|
|
150
|
+
// MONTH 타입의 현재 월 데이터만 필터링
|
|
151
|
+
const currentMonthStats = statistics.filter(
|
|
152
|
+
stat => stat.periodType === 'MONTH' && stat.valueDate === this.currentMonth
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
if (currentMonthStats.length === 0) {
|
|
156
|
+
this.error = '현재 월의 데이터가 없습니다.'
|
|
157
|
+
return
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// 카테고리별로 데이터 그룹화
|
|
161
|
+
const categoryStats = new Map<string, any[]>()
|
|
162
|
+
|
|
163
|
+
currentMonthStats.forEach(stat => {
|
|
164
|
+
if (stat.kpi?.category?.name) {
|
|
165
|
+
const categoryName = stat.kpi.category.name
|
|
166
|
+
if (!categoryStats.has(categoryName)) {
|
|
167
|
+
categoryStats.set(categoryName, [])
|
|
168
|
+
}
|
|
169
|
+
categoryStats.get(categoryName)!.push(stat)
|
|
170
|
+
}
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
this.categories = Array.from(categoryStats.keys())
|
|
174
|
+
this.totalCategories = this.categories.length
|
|
175
|
+
this.totalKpis = currentMonthStats.length
|
|
176
|
+
|
|
177
|
+
// 레이더 차트 데이터 생성
|
|
178
|
+
this.generateRadarData(categoryStats)
|
|
179
|
+
|
|
180
|
+
// 박스플롯 데이터 생성
|
|
181
|
+
this.generateBoxplotData(categoryStats)
|
|
182
|
+
|
|
183
|
+
// 평균 점수 계산
|
|
184
|
+
const scores = currentMonthStats.map(stat => {
|
|
185
|
+
const mean = stat.mean || 0
|
|
186
|
+
const targetValue = stat.kpi?.targetValue || 100
|
|
187
|
+
if (targetValue === 0) return 0
|
|
188
|
+
const achievement = Math.min((mean / targetValue) * 100, 100)
|
|
189
|
+
return Math.max(achievement, 0)
|
|
190
|
+
})
|
|
191
|
+
this.averageScore = Math.round(scores.reduce((sum, score) => sum + score, 0) / scores.length)
|
|
192
|
+
} catch (e) {
|
|
193
|
+
console.error('카테고리 비교 데이터를 불러오지 못했습니다:', e)
|
|
194
|
+
this.error = '카테고리 비교 데이터를 불러오지 못했습니다.'
|
|
195
|
+
} finally {
|
|
196
|
+
this.loading = false
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
private generateRadarData(categoryStats: Map<string, any[]>) {
|
|
201
|
+
const result: any[] = []
|
|
202
|
+
|
|
203
|
+
this.categories.forEach(category => {
|
|
204
|
+
const stats = categoryStats.get(category)!
|
|
205
|
+
const means = stats.map(s => s.mean || 0).filter(v => v > 0)
|
|
206
|
+
const medians = stats.map(s => s.median || 0).filter(v => v > 0)
|
|
207
|
+
const stdDevs = stats.map(s => s.standardDeviation || 0).filter(v => v > 0)
|
|
208
|
+
|
|
209
|
+
if (means.length > 0) {
|
|
210
|
+
result.push({
|
|
211
|
+
group: '평균',
|
|
212
|
+
category,
|
|
213
|
+
value: means.reduce((a, b) => a + b, 0) / means.length
|
|
214
|
+
})
|
|
215
|
+
}
|
|
216
|
+
if (medians.length > 0) {
|
|
217
|
+
result.push({
|
|
218
|
+
group: '중앙값',
|
|
219
|
+
category,
|
|
220
|
+
value: medians.reduce((a, b) => a + b, 0) / medians.length
|
|
221
|
+
})
|
|
222
|
+
}
|
|
223
|
+
if (stdDevs.length > 0) {
|
|
224
|
+
result.push({
|
|
225
|
+
group: '표준편차',
|
|
226
|
+
category,
|
|
227
|
+
value: stdDevs.reduce((a, b) => a + b, 0) / stdDevs.length
|
|
228
|
+
})
|
|
229
|
+
}
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
this.radarData = result
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
private generateBoxplotData(categoryStats: Map<string, any[]>) {
|
|
236
|
+
const result: any[] = []
|
|
237
|
+
|
|
238
|
+
this.categories.forEach(category => {
|
|
239
|
+
const stats = categoryStats.get(category)!
|
|
240
|
+
|
|
241
|
+
// 각 KPI의 평균값들을 수집
|
|
242
|
+
const allMeans = stats.map(s => s.mean || 0).filter(v => v > 0)
|
|
243
|
+
const allMedians = stats.map(s => s.median || 0).filter(v => v > 0)
|
|
244
|
+
const allMins = stats.map(s => s.minimum || 0).filter(v => v > 0)
|
|
245
|
+
const allMaxs = stats.map(s => s.maximum || 0).filter(v => v > 0)
|
|
246
|
+
const allQ1s = stats.map(s => s.percentile25 || 0).filter(v => v > 0)
|
|
247
|
+
const allQ3s = stats.map(s => s.percentile75 || 0).filter(v => v > 0)
|
|
248
|
+
|
|
249
|
+
if (allMeans.length > 0) {
|
|
250
|
+
const sortedMeans = [...allMeans].sort((a, b) => a - b)
|
|
251
|
+
const min = sortedMeans[0]
|
|
252
|
+
const max = sortedMeans[sortedMeans.length - 1]
|
|
253
|
+
const mean = allMeans.reduce((a, b) => a + b, 0) / allMeans.length
|
|
254
|
+
const median = allMedians.length > 0 ? allMedians.reduce((a, b) => a + b, 0) / allMedians.length : mean
|
|
255
|
+
|
|
256
|
+
const q1 = sortedMeans[Math.floor(sortedMeans.length / 4)]
|
|
257
|
+
const q3 = sortedMeans[Math.floor((sortedMeans.length * 3) / 4)]
|
|
258
|
+
|
|
259
|
+
result.push({
|
|
260
|
+
group: category,
|
|
261
|
+
min,
|
|
262
|
+
q1,
|
|
263
|
+
median,
|
|
264
|
+
q3,
|
|
265
|
+
max,
|
|
266
|
+
mean,
|
|
267
|
+
value: mean
|
|
268
|
+
})
|
|
269
|
+
}
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
this.boxplotData = result
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
render() {
|
|
276
|
+
if (this.loading) {
|
|
277
|
+
return html`
|
|
278
|
+
<div class="comparison-container">
|
|
279
|
+
<div class="comparison-title">카테고리 비교 분석</div>
|
|
280
|
+
<div class="charts-section">
|
|
281
|
+
<div class="chart-card">
|
|
282
|
+
<div class="chart-title">레이더 차트</div>
|
|
283
|
+
<div class="chart-container">
|
|
284
|
+
<div class="loading">데이터 로딩 중...</div>
|
|
285
|
+
</div>
|
|
286
|
+
</div>
|
|
287
|
+
<div class="chart-card">
|
|
288
|
+
<div class="chart-title">박스플롯</div>
|
|
289
|
+
<div class="chart-container">
|
|
290
|
+
<div class="loading">데이터 로딩 중...</div>
|
|
291
|
+
</div>
|
|
292
|
+
</div>
|
|
293
|
+
</div>
|
|
294
|
+
</div>
|
|
295
|
+
`
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (this.error) {
|
|
299
|
+
return html`
|
|
300
|
+
<div class="comparison-container">
|
|
301
|
+
<div class="comparison-title">카테고리 비교 분석</div>
|
|
302
|
+
<div class="charts-section">
|
|
303
|
+
<div class="chart-card">
|
|
304
|
+
<div class="chart-title">레이더 차트</div>
|
|
305
|
+
<div class="chart-container">
|
|
306
|
+
<div class="error">${this.error}</div>
|
|
307
|
+
</div>
|
|
308
|
+
</div>
|
|
309
|
+
<div class="chart-card">
|
|
310
|
+
<div class="chart-title">박스플롯</div>
|
|
311
|
+
<div class="chart-container">
|
|
312
|
+
<div class="error">${this.error}</div>
|
|
313
|
+
</div>
|
|
314
|
+
</div>
|
|
315
|
+
</div>
|
|
316
|
+
</div>
|
|
317
|
+
`
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return html`
|
|
321
|
+
<div class="comparison-container">
|
|
322
|
+
<div class="comparison-title">카테고리 비교 분석 (${this.currentMonth})</div>
|
|
323
|
+
|
|
324
|
+
<div class="charts-section">
|
|
325
|
+
<div class="chart-card">
|
|
326
|
+
<div class="chart-title">레이더 차트</div>
|
|
327
|
+
<div class="chart-container">
|
|
328
|
+
${this.radarData.length > 0
|
|
329
|
+
? html`<kpi-radar-chart
|
|
330
|
+
.data=${this.radarData}
|
|
331
|
+
.categories=${this.categories}
|
|
332
|
+
.currentGroup=${'평균'}
|
|
333
|
+
></kpi-radar-chart>`
|
|
334
|
+
: html`<div class="no-data">데이터가 없습니다.</div>`}
|
|
335
|
+
</div>
|
|
336
|
+
</div>
|
|
337
|
+
|
|
338
|
+
<div class="chart-card">
|
|
339
|
+
<div class="chart-title">박스플롯</div>
|
|
340
|
+
<div class="chart-container">
|
|
341
|
+
${this.boxplotData.length > 0
|
|
342
|
+
? html`<kpi-boxplot-chart
|
|
343
|
+
.data=${this.boxplotData}
|
|
344
|
+
.groups=${this.categories}
|
|
345
|
+
.currentGroup=${'평균'}
|
|
346
|
+
></kpi-boxplot-chart>`
|
|
347
|
+
: html`<div class="no-data">데이터가 없습니다.</div>`}
|
|
348
|
+
</div>
|
|
349
|
+
</div>
|
|
350
|
+
</div>
|
|
351
|
+
|
|
352
|
+
<div class="summary-info">
|
|
353
|
+
<div class="summary-item">
|
|
354
|
+
<div class="summary-value">${this.totalCategories}</div>
|
|
355
|
+
<div class="summary-label">카테고리</div>
|
|
356
|
+
</div>
|
|
357
|
+
<div class="summary-item">
|
|
358
|
+
<div class="summary-value">${this.totalKpis}</div>
|
|
359
|
+
<div class="summary-label">KPI</div>
|
|
360
|
+
</div>
|
|
361
|
+
<div class="summary-item">
|
|
362
|
+
<div class="summary-value">${this.averageScore}</div>
|
|
363
|
+
<div class="summary-label">평균 점수</div>
|
|
364
|
+
</div>
|
|
365
|
+
</div>
|
|
366
|
+
</div>
|
|
367
|
+
`
|
|
368
|
+
}
|
|
369
|
+
}
|