@things-factory/kpi 9.0.28 → 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 (131) 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/pages/kpi-statistic/kpi-statistic-editor-page.ts +761 -0
  12. package/client/pages/kpi-statistic/kpi-statistic-importer.ts +90 -0
  13. package/client/pages/kpi-statistic/kpi-statistic-list-page.ts +505 -0
  14. package/client/route.ts +12 -0
  15. package/dist-client/google-map/common-google-map.d.ts +34 -0
  16. package/dist-client/google-map/common-google-map.js +300 -0
  17. package/dist-client/google-map/common-google-map.js.map +1 -0
  18. package/dist-client/google-map/google-map-loader.d.ts +6 -0
  19. package/dist-client/google-map/google-map-loader.js +22 -0
  20. package/dist-client/google-map/google-map-loader.js.map +1 -0
  21. package/dist-client/google-map/script-loader.d.ts +3 -0
  22. package/dist-client/google-map/script-loader.js +144 -0
  23. package/dist-client/google-map/script-loader.js.map +1 -0
  24. package/dist-client/pages/kpi-dashboard/cards/kpi-level1-card.d.ts +17 -0
  25. package/dist-client/pages/kpi-dashboard/cards/kpi-level1-card.js +279 -0
  26. package/dist-client/pages/kpi-dashboard/cards/kpi-level1-card.js.map +1 -0
  27. package/dist-client/pages/kpi-dashboard/cards/kpi-level2-comparison.d.ts +19 -0
  28. package/dist-client/pages/kpi-dashboard/cards/kpi-level2-comparison.js +385 -0
  29. package/dist-client/pages/kpi-dashboard/cards/kpi-level2-comparison.js.map +1 -0
  30. package/dist-client/pages/kpi-dashboard/cards/kpi-level3-comparison.d.ts +23 -0
  31. package/dist-client/pages/kpi-dashboard/cards/kpi-level3-comparison.js +465 -0
  32. package/dist-client/pages/kpi-dashboard/cards/kpi-level3-comparison.js.map +1 -0
  33. package/dist-client/pages/kpi-dashboard/components/kpi-chart-toggle.d.ts +8 -0
  34. package/dist-client/pages/kpi-dashboard/components/kpi-chart-toggle.js +79 -0
  35. package/dist-client/pages/kpi-dashboard/components/kpi-chart-toggle.js.map +1 -0
  36. package/dist-client/pages/kpi-dashboard/components/kpi-map-panel.d.ts +23 -0
  37. package/dist-client/pages/kpi-dashboard/components/kpi-map-panel.js +223 -0
  38. package/dist-client/pages/kpi-dashboard/components/kpi-map-panel.js.map +1 -0
  39. package/dist-client/pages/kpi-dashboard/kpi-dashboard-map.d.ts +38 -0
  40. package/dist-client/pages/kpi-dashboard/kpi-dashboard-map.js +813 -0
  41. package/dist-client/pages/kpi-dashboard/kpi-dashboard-map.js.map +1 -0
  42. package/dist-client/pages/kpi-dashboard/kpi-dashboard.d.ts +21 -0
  43. package/dist-client/pages/kpi-dashboard/kpi-dashboard.js +398 -0
  44. package/dist-client/pages/kpi-dashboard/kpi-dashboard.js.map +1 -1
  45. package/dist-client/pages/kpi-statistic/kpi-statistic-editor-page.d.ts +53 -0
  46. package/dist-client/pages/kpi-statistic/kpi-statistic-editor-page.js +718 -0
  47. package/dist-client/pages/kpi-statistic/kpi-statistic-editor-page.js.map +1 -0
  48. package/dist-client/pages/kpi-statistic/kpi-statistic-importer.d.ts +23 -0
  49. package/dist-client/pages/kpi-statistic/kpi-statistic-importer.js +92 -0
  50. package/dist-client/pages/kpi-statistic/kpi-statistic-importer.js.map +1 -0
  51. package/dist-client/pages/kpi-statistic/kpi-statistic-list-page.d.ts +66 -0
  52. package/dist-client/pages/kpi-statistic/kpi-statistic-list-page.js +487 -0
  53. package/dist-client/pages/kpi-statistic/kpi-statistic-list-page.js.map +1 -0
  54. package/dist-client/route.d.ts +1 -1
  55. package/dist-client/route.js +9 -0
  56. package/dist-client/route.js.map +1 -1
  57. package/dist-client/tsconfig.tsbuildinfo +1 -1
  58. package/dist-server/index.d.ts +1 -0
  59. package/dist-server/index.js +1 -0
  60. package/dist-server/index.js.map +1 -1
  61. package/dist-server/migrations/index.d.ts +1 -0
  62. package/dist-server/migrations/index.js +12 -0
  63. package/dist-server/migrations/index.js.map +1 -0
  64. package/dist-server/service/index.d.ts +3 -2
  65. package/dist-server/service/index.js +4 -0
  66. package/dist-server/service/index.js.map +1 -1
  67. package/dist-server/service/kpi/kpi-mutation.js +9 -0
  68. package/dist-server/service/kpi/kpi-mutation.js.map +1 -1
  69. package/dist-server/service/kpi/kpi-query.js +2 -0
  70. package/dist-server/service/kpi/kpi-query.js.map +1 -1
  71. package/dist-server/service/kpi-alert/kpi-alert-query.js +1 -0
  72. package/dist-server/service/kpi-alert/kpi-alert-query.js.map +1 -1
  73. package/dist-server/service/kpi-category/kpi-category-mutation.js +4 -0
  74. package/dist-server/service/kpi-category/kpi-category-mutation.js.map +1 -1
  75. package/dist-server/service/kpi-category/kpi-category-query.js +2 -0
  76. package/dist-server/service/kpi-category/kpi-category-query.js.map +1 -1
  77. package/dist-server/service/kpi-metric/kpi-metric-mutation.js +6 -0
  78. package/dist-server/service/kpi-metric/kpi-metric-mutation.js.map +1 -1
  79. package/dist-server/service/kpi-metric/kpi-metric-query.js +2 -0
  80. package/dist-server/service/kpi-metric/kpi-metric-query.js.map +1 -1
  81. package/dist-server/service/kpi-metric-value/kpi-metric-value-mutation.js +7 -0
  82. package/dist-server/service/kpi-metric-value/kpi-metric-value-mutation.js.map +1 -1
  83. package/dist-server/service/kpi-metric-value/kpi-metric-value-query.js +2 -0
  84. package/dist-server/service/kpi-metric-value/kpi-metric-value-query.js.map +1 -1
  85. package/dist-server/service/kpi-statistic/index.d.ts +6 -0
  86. package/dist-server/service/kpi-statistic/index.js +10 -0
  87. package/dist-server/service/kpi-statistic/index.js.map +1 -0
  88. package/dist-server/service/kpi-statistic/kpi-statistic-mutation.d.ts +10 -0
  89. package/dist-server/service/kpi-statistic/kpi-statistic-mutation.js +198 -0
  90. package/dist-server/service/kpi-statistic/kpi-statistic-mutation.js.map +1 -0
  91. package/dist-server/service/kpi-statistic/kpi-statistic-query.d.ts +13 -0
  92. package/dist-server/service/kpi-statistic/kpi-statistic-query.js +92 -0
  93. package/dist-server/service/kpi-statistic/kpi-statistic-query.js.map +1 -0
  94. package/dist-server/service/kpi-statistic/kpi-statistic-type.d.ts +59 -0
  95. package/dist-server/service/kpi-statistic/kpi-statistic-type.js +200 -0
  96. package/dist-server/service/kpi-statistic/kpi-statistic-type.js.map +1 -0
  97. package/dist-server/service/kpi-statistic/kpi-statistic.d.ts +38 -0
  98. package/dist-server/service/kpi-statistic/kpi-statistic.js +177 -0
  99. package/dist-server/service/kpi-statistic/kpi-statistic.js.map +1 -0
  100. package/dist-server/service/kpi-value/kpi-value-mutation.js +9 -0
  101. package/dist-server/service/kpi-value/kpi-value-mutation.js.map +1 -1
  102. package/dist-server/service/kpi-value/kpi-value-query.js +2 -0
  103. package/dist-server/service/kpi-value/kpi-value-query.js.map +1 -1
  104. package/dist-server/tsconfig.tsbuildinfo +1 -1
  105. package/helps/kpi/kpi-statistic.md +160 -0
  106. package/package.json +2 -2
  107. package/server/index.ts +1 -0
  108. package/server/migrations/index.ts +9 -0
  109. package/server/service/index.ts +8 -0
  110. package/server/service/kpi/kpi-mutation.ts +9 -0
  111. package/server/service/kpi/kpi-query.ts +2 -0
  112. package/server/service/kpi-alert/kpi-alert-query.ts +2 -1
  113. package/server/service/kpi-category/kpi-category-mutation.ts +4 -0
  114. package/server/service/kpi-category/kpi-category-query.ts +2 -0
  115. package/server/service/kpi-metric/kpi-metric-mutation.ts +6 -0
  116. package/server/service/kpi-metric/kpi-metric-query.ts +2 -0
  117. package/server/service/kpi-metric-value/kpi-metric-value-mutation.ts +7 -0
  118. package/server/service/kpi-metric-value/kpi-metric-value-query.ts +3 -1
  119. package/server/service/kpi-statistic/index.ts +7 -0
  120. package/server/service/kpi-statistic/kpi-statistic-mutation.ts +194 -0
  121. package/server/service/kpi-statistic/kpi-statistic-query.ts +59 -0
  122. package/server/service/kpi-statistic/kpi-statistic-type.ts +152 -0
  123. package/server/service/kpi-statistic/kpi-statistic.ts +167 -0
  124. package/server/service/kpi-value/kpi-value-mutation.ts +9 -0
  125. package/server/service/kpi-value/kpi-value-query.ts +2 -0
  126. package/things-factory.config.js +4 -1
  127. package/translations/en.json +24 -1
  128. package/translations/ja.json +43 -20
  129. package/translations/ko.json +30 -7
  130. package/translations/ms.json +35 -12
  131. package/translations/zh.json +39 -16
@@ -0,0 +1,443 @@
1
+ import { html, css, nothing } 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-level3-comparison')
8
+ export class KpiLevel3Comparison 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
+ .category-selector {
29
+ display: flex;
30
+ align-items: center;
31
+ gap: 12px;
32
+ margin-bottom: 24px;
33
+ padding: 16px;
34
+ background: #f8f9fa;
35
+ border-radius: 8px;
36
+ border: 1px solid #e9ecef;
37
+ }
38
+ .selector-label {
39
+ font-weight: 600;
40
+ color: #495057;
41
+ min-width: 80px;
42
+ }
43
+ .category-select {
44
+ padding: 8px 12px;
45
+ border: 1px solid #ced4da;
46
+ border-radius: 6px;
47
+ background: white;
48
+ font-size: 1rem;
49
+ min-width: 200px;
50
+ }
51
+ .category-select:focus {
52
+ outline: none;
53
+ border-color: #667eea;
54
+ box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.2);
55
+ }
56
+ .charts-section {
57
+ display: flex;
58
+ gap: 24px;
59
+ margin-bottom: 20px;
60
+ }
61
+ .chart-card {
62
+ flex: 1;
63
+ background: #f8f9fa;
64
+ border-radius: 12px;
65
+ padding: 20px;
66
+ border: 1px solid #e9ecef;
67
+ }
68
+ .chart-title {
69
+ font-size: 1.1rem;
70
+ font-weight: 600;
71
+ margin-bottom: 16px;
72
+ color: #495057;
73
+ text-align: center;
74
+ }
75
+ .chart-container {
76
+ height: 300px;
77
+ display: flex;
78
+ align-items: center;
79
+ justify-content: center;
80
+ }
81
+ .loading {
82
+ color: #666;
83
+ font-size: 1rem;
84
+ }
85
+ .error {
86
+ color: #d32f2f;
87
+ font-size: 1rem;
88
+ }
89
+ .no-data {
90
+ color: #666;
91
+ font-size: 1rem;
92
+ text-align: center;
93
+ }
94
+ .no-category {
95
+ color: #666;
96
+ font-size: 1rem;
97
+ text-align: center;
98
+ font-style: italic;
99
+ }
100
+ .summary-info {
101
+ display: flex;
102
+ justify-content: space-around;
103
+ padding: 16px;
104
+ background: #f8f9fa;
105
+ border-radius: 8px;
106
+ margin-top: 16px;
107
+ }
108
+ .summary-item {
109
+ text-align: center;
110
+ }
111
+ .summary-value {
112
+ font-size: 1.2rem;
113
+ font-weight: bold;
114
+ color: #333;
115
+ margin-bottom: 4px;
116
+ }
117
+ .summary-label {
118
+ font-size: 0.9rem;
119
+ color: #666;
120
+ }
121
+ `
122
+
123
+ @state() loading = true
124
+ @state() error = ''
125
+ @state() radarData: any[] = []
126
+ @state() boxplotData: any[] = []
127
+ @state() kpis: string[] = []
128
+ @state() availableCategories: string[] = []
129
+ @state() selectedCategory = ''
130
+ @state() currentMonth = ''
131
+ @state() totalKpis = 0
132
+ @state() averageScore = 0
133
+ @state() categoryStats = new Map<string, any[]>()
134
+
135
+ connectedCallback() {
136
+ super.connectedCallback()
137
+ this.currentMonth = this.getCurrentMonth()
138
+ this.fetchKpiComparison()
139
+ }
140
+
141
+ private getCurrentMonth(): string {
142
+ const now = new Date()
143
+ const year = now.getFullYear()
144
+ const month = String(now.getMonth() + 1).padStart(2, '0')
145
+ return `${year}-${month}`
146
+ }
147
+
148
+ async fetchKpiComparison() {
149
+ this.loading = true
150
+ this.error = ''
151
+
152
+ try {
153
+ const response = await client.query({
154
+ query: gql`
155
+ query {
156
+ kpiStatistics {
157
+ items {
158
+ id
159
+ valueDate
160
+ periodType
161
+ mean
162
+ median
163
+ standardDeviation
164
+ minimum
165
+ maximum
166
+ percentile25
167
+ percentile75
168
+ kpi {
169
+ id
170
+ name
171
+ targetValue
172
+ unit
173
+ category {
174
+ id
175
+ name
176
+ }
177
+ }
178
+ }
179
+ }
180
+ }
181
+ `
182
+ })
183
+
184
+ const statistics = response.data.kpiStatistics.items || []
185
+
186
+ // MONTH 타입의 현재 월 데이터만 필터링
187
+ const currentMonthStats = statistics.filter(
188
+ stat => stat.periodType === 'MONTH' && stat.valueDate === this.currentMonth
189
+ )
190
+
191
+ if (currentMonthStats.length === 0) {
192
+ this.error = '현재 월의 데이터가 없습니다.'
193
+ return
194
+ }
195
+
196
+ // 카테고리별로 데이터 그룹화
197
+ const categoryStats = new Map<string, any[]>()
198
+
199
+ currentMonthStats.forEach(stat => {
200
+ if (stat.kpi?.category?.name) {
201
+ const categoryName = stat.kpi.category.name
202
+ if (!categoryStats.has(categoryName)) {
203
+ categoryStats.set(categoryName, [])
204
+ }
205
+ categoryStats.get(categoryName)!.push(stat)
206
+ }
207
+ })
208
+
209
+ this.categoryStats = categoryStats
210
+ this.availableCategories = Array.from(categoryStats.keys())
211
+
212
+ // 첫 번째 카테고리를 기본 선택
213
+ if (this.availableCategories.length > 0 && !this.selectedCategory) {
214
+ this.selectedCategory = this.availableCategories[0]
215
+ }
216
+
217
+ // 선택된 카테고리의 데이터로 차트 생성
218
+ if (this.selectedCategory) {
219
+ this.generateKpiCharts()
220
+ }
221
+ } catch (e) {
222
+ console.error('KPI 비교 데이터를 불러오지 못했습니다:', e)
223
+ this.error = 'KPI 비교 데이터를 불러오지 못했습니다.'
224
+ } finally {
225
+ this.loading = false
226
+ }
227
+ }
228
+
229
+ private onCategoryChange(event: Event) {
230
+ const target = event.target as HTMLSelectElement
231
+ this.selectedCategory = target.value
232
+ this.generateKpiCharts()
233
+ }
234
+
235
+ private generateKpiCharts() {
236
+ if (!this.selectedCategory || !this.categoryStats.has(this.selectedCategory)) {
237
+ this.radarData = []
238
+ this.boxplotData = []
239
+ this.kpis = []
240
+ this.totalKpis = 0
241
+ this.averageScore = 0
242
+ return
243
+ }
244
+
245
+ const categoryData = this.categoryStats.get(this.selectedCategory)!
246
+ this.kpis = categoryData.map(stat => stat.kpi?.name || '').filter(Boolean)
247
+ this.totalKpis = this.kpis.length
248
+
249
+ // 레이더 차트 데이터 생성
250
+ this.generateRadarData(categoryData)
251
+
252
+ // 박스플롯 데이터 생성
253
+ this.generateBoxplotData(categoryData)
254
+
255
+ // 평균 점수 계산
256
+ const scores = categoryData.map(stat => {
257
+ const mean = stat.mean || 0
258
+ const targetValue = stat.kpi?.targetValue || 100
259
+ if (targetValue === 0) return 0
260
+ const achievement = Math.min((mean / targetValue) * 100, 100)
261
+ return Math.max(achievement, 0)
262
+ })
263
+ this.averageScore = Math.round(scores.reduce((sum, score) => sum + score, 0) / scores.length)
264
+ }
265
+
266
+ private generateRadarData(categoryData: any[]) {
267
+ const result: any[] = []
268
+
269
+ this.kpis.forEach(kpiName => {
270
+ const stat = categoryData.find(s => s.kpi?.name === kpiName)
271
+ if (stat) {
272
+ const mean = stat.mean || 0
273
+ const median = stat.median || 0
274
+ const stdDev = stat.standardDeviation || 0
275
+
276
+ if (mean > 0) {
277
+ result.push({ group: '평균', category: kpiName, value: mean })
278
+ }
279
+ if (median > 0) {
280
+ result.push({ group: '중앙값', category: kpiName, value: median })
281
+ }
282
+ if (stdDev > 0) {
283
+ result.push({ group: '표준편차', category: kpiName, value: stdDev })
284
+ }
285
+ }
286
+ })
287
+
288
+ this.radarData = result
289
+ }
290
+
291
+ private generateBoxplotData(categoryData: any[]) {
292
+ const result: any[] = []
293
+
294
+ this.kpis.forEach(kpiName => {
295
+ const stat = categoryData.find(s => s.kpi?.name === kpiName)
296
+ if (stat) {
297
+ const mean = stat.mean || 0
298
+ const median = stat.median || 0
299
+ const min = stat.minimum || 0
300
+ const max = stat.maximum || 0
301
+ const q1 = stat.percentile25 || 0
302
+ const q3 = stat.percentile75 || 0
303
+
304
+ if (mean > 0) {
305
+ result.push({
306
+ group: kpiName,
307
+ min,
308
+ q1,
309
+ median,
310
+ q3,
311
+ max,
312
+ mean,
313
+ value: mean
314
+ })
315
+ }
316
+ }
317
+ })
318
+
319
+ this.boxplotData = result
320
+ }
321
+
322
+ render() {
323
+ if (this.loading) {
324
+ return html`
325
+ <div class="comparison-container">
326
+ <div class="comparison-title">KPI 상세 비교 분석</div>
327
+ <div class="category-selector">
328
+ <div class="selector-label">카테고리:</div>
329
+ <select class="category-select" disabled>
330
+ <option>로딩 중...</option>
331
+ </select>
332
+ </div>
333
+ <div class="charts-section">
334
+ <div class="chart-card">
335
+ <div class="chart-title">레이더 차트</div>
336
+ <div class="chart-container">
337
+ <div class="loading">데이터 로딩 중...</div>
338
+ </div>
339
+ </div>
340
+ <div class="chart-card">
341
+ <div class="chart-title">박스플롯</div>
342
+ <div class="chart-container">
343
+ <div class="loading">데이터 로딩 중...</div>
344
+ </div>
345
+ </div>
346
+ </div>
347
+ </div>
348
+ `
349
+ }
350
+
351
+ if (this.error) {
352
+ return html`
353
+ <div class="comparison-container">
354
+ <div class="comparison-title">KPI 상세 비교 분석</div>
355
+ <div class="category-selector">
356
+ <div class="selector-label">카테고리:</div>
357
+ <select class="category-select" disabled>
358
+ <option>오류 발생</option>
359
+ </select>
360
+ </div>
361
+ <div class="charts-section">
362
+ <div class="chart-card">
363
+ <div class="chart-title">레이더 차트</div>
364
+ <div class="chart-container">
365
+ <div class="error">${this.error}</div>
366
+ </div>
367
+ </div>
368
+ <div class="chart-card">
369
+ <div class="chart-title">박스플롯</div>
370
+ <div class="chart-container">
371
+ <div class="error">${this.error}</div>
372
+ </div>
373
+ </div>
374
+ </div>
375
+ </div>
376
+ `
377
+ }
378
+
379
+ return html`
380
+ <div class="comparison-container">
381
+ <div class="comparison-title">KPI 상세 비교 분석 (${this.currentMonth})</div>
382
+
383
+ <div class="category-selector">
384
+ <div class="selector-label">카테고리:</div>
385
+ <select class="category-select" .value=${this.selectedCategory} @change=${this.onCategoryChange}>
386
+ ${this.availableCategories.map(category => html`<option value="${category}">${category}</option>`)}
387
+ </select>
388
+ </div>
389
+
390
+ <div class="charts-section">
391
+ <div class="chart-card">
392
+ <div class="chart-title">레이더 차트</div>
393
+ <div class="chart-container">
394
+ ${this.selectedCategory
395
+ ? this.radarData.length > 0
396
+ ? html`<kpi-radar-chart
397
+ .data=${this.radarData}
398
+ .categories=${this.kpis}
399
+ .currentGroup=${'평균'}
400
+ ></kpi-radar-chart>`
401
+ : html`<div class="no-data">선택된 카테고리에 데이터가 없습니다.</div>`
402
+ : html`<div class="no-category">카테고리를 선택해주세요.</div>`}
403
+ </div>
404
+ </div>
405
+
406
+ <div class="chart-card">
407
+ <div class="chart-title">박스플롯</div>
408
+ <div class="chart-container">
409
+ ${this.selectedCategory
410
+ ? this.boxplotData.length > 0
411
+ ? html`<kpi-boxplot-chart
412
+ .data=${this.boxplotData}
413
+ .groups=${this.kpis}
414
+ .currentGroup=${'평균'}
415
+ ></kpi-boxplot-chart>`
416
+ : html`<div class="no-data">선택된 카테고리에 데이터가 없습니다.</div>`
417
+ : html`<div class="no-category">카테고리를 선택해주세요.</div>`}
418
+ </div>
419
+ </div>
420
+ </div>
421
+
422
+ ${this.selectedCategory
423
+ ? html`
424
+ <div class="summary-info">
425
+ <div class="summary-item">
426
+ <div class="summary-value">${this.selectedCategory}</div>
427
+ <div class="summary-label">선택된 카테고리</div>
428
+ </div>
429
+ <div class="summary-item">
430
+ <div class="summary-value">${this.totalKpis}</div>
431
+ <div class="summary-label">KPI</div>
432
+ </div>
433
+ <div class="summary-item">
434
+ <div class="summary-value">${this.averageScore}</div>
435
+ <div class="summary-label">평균 점수</div>
436
+ </div>
437
+ </div>
438
+ `
439
+ : nothing}
440
+ </div>
441
+ `
442
+ }
443
+ }
@@ -0,0 +1,73 @@
1
+ import { html, css } from 'lit'
2
+ import { customElement, property } from 'lit/decorators.js'
3
+ import { LitElement } from 'lit'
4
+
5
+ @customElement('kpi-chart-toggle')
6
+ export class KpiChartToggle extends LitElement {
7
+ static styles = css`
8
+ :host {
9
+ display: block;
10
+ }
11
+ .chart-toggle {
12
+ display: flex;
13
+ gap: 8px;
14
+ margin-bottom: 16px;
15
+ }
16
+ .toggle-button {
17
+ padding: 8px 16px;
18
+ border: 1px solid #ced4da;
19
+ background: #fff;
20
+ border-radius: 6px;
21
+ cursor: pointer;
22
+ font-size: 0.9rem;
23
+ transition: all 0.2s;
24
+ }
25
+ .toggle-button:hover {
26
+ background: #f8f9fa;
27
+ }
28
+ .toggle-button.active {
29
+ background: #667eea;
30
+ color: white;
31
+ border-color: #667eea;
32
+ }
33
+ `
34
+
35
+ @property({ type: String })
36
+ selectedType = 'boxplot'
37
+
38
+ @property({ type: Function })
39
+ onTypeChange?: (type: string) => void
40
+
41
+ private handleTypeChange(type: string) {
42
+ this.selectedType = type
43
+ if (this.onTypeChange) {
44
+ this.onTypeChange(type)
45
+ }
46
+ this.dispatchEvent(
47
+ new CustomEvent('type-change', {
48
+ detail: { type },
49
+ bubbles: true,
50
+ composed: true
51
+ })
52
+ )
53
+ }
54
+
55
+ render() {
56
+ return html`
57
+ <div class="chart-toggle">
58
+ <button
59
+ class="toggle-button ${this.selectedType === 'boxplot' ? 'active' : ''}"
60
+ @click=${() => this.handleTypeChange('boxplot')}
61
+ >
62
+ 박스플롯
63
+ </button>
64
+ <button
65
+ class="toggle-button ${this.selectedType === 'radar' ? 'active' : ''}"
66
+ @click=${() => this.handleTypeChange('radar')}
67
+ >
68
+ 레이더차트
69
+ </button>
70
+ </div>
71
+ `
72
+ }
73
+ }