@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.
Files changed (61) hide show
  1. package/client/google-map/common-google-map.ts +332 -0
  2. package/client/google-map/google-map-loader.ts +29 -0
  3. package/client/google-map/script-loader.ts +173 -0
  4. package/client/pages/kpi-dashboard/cards/kpi-level1-card.ts +248 -0
  5. package/client/pages/kpi-dashboard/cards/kpi-level2-comparison.ts +369 -0
  6. package/client/pages/kpi-dashboard/cards/kpi-level3-comparison.ts +443 -0
  7. package/client/pages/kpi-dashboard/components/kpi-chart-toggle.ts +73 -0
  8. package/client/pages/kpi-dashboard/components/kpi-map-panel.ts +222 -0
  9. package/client/pages/kpi-dashboard/kpi-dashboard-map.ts +786 -0
  10. package/client/pages/kpi-dashboard/kpi-dashboard.ts +416 -0
  11. package/client/route.ts +4 -0
  12. package/dist-client/google-map/common-google-map.d.ts +34 -0
  13. package/dist-client/google-map/common-google-map.js +300 -0
  14. package/dist-client/google-map/common-google-map.js.map +1 -0
  15. package/dist-client/google-map/google-map-loader.d.ts +6 -0
  16. package/dist-client/google-map/google-map-loader.js +22 -0
  17. package/dist-client/google-map/google-map-loader.js.map +1 -0
  18. package/dist-client/google-map/script-loader.d.ts +3 -0
  19. package/dist-client/google-map/script-loader.js +144 -0
  20. package/dist-client/google-map/script-loader.js.map +1 -0
  21. package/dist-client/pages/kpi-dashboard/cards/kpi-level1-card.d.ts +17 -0
  22. package/dist-client/pages/kpi-dashboard/cards/kpi-level1-card.js +279 -0
  23. package/dist-client/pages/kpi-dashboard/cards/kpi-level1-card.js.map +1 -0
  24. package/dist-client/pages/kpi-dashboard/cards/kpi-level2-comparison.d.ts +19 -0
  25. package/dist-client/pages/kpi-dashboard/cards/kpi-level2-comparison.js +385 -0
  26. package/dist-client/pages/kpi-dashboard/cards/kpi-level2-comparison.js.map +1 -0
  27. package/dist-client/pages/kpi-dashboard/cards/kpi-level3-comparison.d.ts +23 -0
  28. package/dist-client/pages/kpi-dashboard/cards/kpi-level3-comparison.js +465 -0
  29. package/dist-client/pages/kpi-dashboard/cards/kpi-level3-comparison.js.map +1 -0
  30. package/dist-client/pages/kpi-dashboard/components/kpi-chart-toggle.d.ts +8 -0
  31. package/dist-client/pages/kpi-dashboard/components/kpi-chart-toggle.js +79 -0
  32. package/dist-client/pages/kpi-dashboard/components/kpi-chart-toggle.js.map +1 -0
  33. package/dist-client/pages/kpi-dashboard/components/kpi-map-panel.d.ts +23 -0
  34. package/dist-client/pages/kpi-dashboard/components/kpi-map-panel.js +223 -0
  35. package/dist-client/pages/kpi-dashboard/components/kpi-map-panel.js.map +1 -0
  36. package/dist-client/pages/kpi-dashboard/kpi-dashboard-map.d.ts +38 -0
  37. package/dist-client/pages/kpi-dashboard/kpi-dashboard-map.js +813 -0
  38. package/dist-client/pages/kpi-dashboard/kpi-dashboard-map.js.map +1 -0
  39. package/dist-client/pages/kpi-dashboard/kpi-dashboard.d.ts +21 -0
  40. package/dist-client/pages/kpi-dashboard/kpi-dashboard.js +398 -0
  41. package/dist-client/pages/kpi-dashboard/kpi-dashboard.js.map +1 -1
  42. package/dist-client/route.d.ts +1 -1
  43. package/dist-client/route.js +3 -0
  44. package/dist-client/route.js.map +1 -1
  45. package/dist-client/tsconfig.tsbuildinfo +1 -1
  46. package/dist-server/index.d.ts +1 -0
  47. package/dist-server/index.js +1 -0
  48. package/dist-server/index.js.map +1 -1
  49. package/dist-server/migrations/index.d.ts +1 -0
  50. package/dist-server/migrations/index.js +12 -0
  51. package/dist-server/migrations/index.js.map +1 -0
  52. package/dist-server/tsconfig.tsbuildinfo +1 -1
  53. package/package.json +2 -2
  54. package/server/index.ts +1 -0
  55. package/server/migrations/index.ts +9 -0
  56. package/things-factory.config.js +2 -1
  57. package/translations/en.json +1 -0
  58. package/translations/ja.json +1 -0
  59. package/translations/ko.json +1 -0
  60. package/translations/ms.json +1 -0
  61. 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
+ }