@things-factory/kpi 9.0.13 → 9.0.15

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 (122) hide show
  1. package/client/pages/kpi/kpi-grade-editor.ts +294 -0
  2. package/client/pages/kpi/kpi-list-page.ts +69 -1
  3. package/client/pages/kpi/kpi-overview.ts +5 -11
  4. package/client/pages/kpi-dashboard/kpi-dashboard.ts +43 -15
  5. package/client/pages/kpi-dashboard/kpi-performance-summary.ts +17 -10
  6. package/client/pages/kpi-value/kpi-value-manual-entry-form.ts +1 -0
  7. package/client/route.ts +0 -4
  8. package/dist-client/pages/kpi/kpi-grade-editor.d.ts +35 -0
  9. package/dist-client/pages/kpi/kpi-grade-editor.js +278 -0
  10. package/dist-client/pages/kpi/kpi-grade-editor.js.map +1 -0
  11. package/dist-client/pages/kpi/kpi-list-page.d.ts +4 -0
  12. package/dist-client/pages/kpi/kpi-list-page.js +64 -1
  13. package/dist-client/pages/kpi/kpi-list-page.js.map +1 -1
  14. package/dist-client/pages/kpi/kpi-overview.js +3 -11
  15. package/dist-client/pages/kpi/kpi-overview.js.map +1 -1
  16. package/dist-client/pages/kpi-dashboard/kpi-dashboard.js +44 -12
  17. package/dist-client/pages/kpi-dashboard/kpi-dashboard.js.map +1 -1
  18. package/dist-client/pages/kpi-dashboard/kpi-performance-summary.js +17 -10
  19. package/dist-client/pages/kpi-dashboard/kpi-performance-summary.js.map +1 -1
  20. package/dist-client/pages/kpi-value/kpi-value-manual-entry-form.js +1 -0
  21. package/dist-client/pages/kpi-value/kpi-value-manual-entry-form.js.map +1 -1
  22. package/dist-client/route.d.ts +1 -1
  23. package/dist-client/route.js +0 -3
  24. package/dist-client/route.js.map +1 -1
  25. package/dist-client/tsconfig.tsbuildinfo +1 -1
  26. package/dist-server/index.d.ts +0 -1
  27. package/dist-server/index.js +0 -1
  28. package/dist-server/index.js.map +1 -1
  29. package/dist-server/service/index.d.ts +2 -4
  30. package/dist-server/service/index.js +0 -5
  31. package/dist-server/service/index.js.map +1 -1
  32. package/dist-server/service/kpi/aggregate-kpi.js +4 -0
  33. package/dist-server/service/kpi/aggregate-kpi.js.map +1 -1
  34. package/dist-server/service/kpi/kpi-grade.types.d.ts +21 -0
  35. package/dist-server/service/kpi/kpi-grade.types.js +3 -0
  36. package/dist-server/service/kpi/kpi-grade.types.js.map +1 -0
  37. package/dist-server/service/kpi/kpi-history.d.ts +2 -0
  38. package/dist-server/service/kpi/kpi-history.js +8 -0
  39. package/dist-server/service/kpi/kpi-history.js.map +1 -1
  40. package/dist-server/service/kpi/kpi-query.d.ts +0 -2
  41. package/dist-server/service/kpi/kpi-query.js +0 -11
  42. package/dist-server/service/kpi/kpi-query.js.map +1 -1
  43. package/dist-server/service/kpi/kpi-type.d.ts +3 -0
  44. package/dist-server/service/kpi/kpi-type.js +14 -0
  45. package/dist-server/service/kpi/kpi-type.js.map +1 -1
  46. package/dist-server/service/kpi/kpi.d.ts +2 -2
  47. package/dist-server/service/kpi/kpi.js +5 -3
  48. package/dist-server/service/kpi/kpi.js.map +1 -1
  49. package/dist-server/service/kpi-alert/kpi-alert-query.js +13 -13
  50. package/dist-server/service/kpi-alert/kpi-alert-query.js.map +1 -1
  51. package/dist-server/service/kpi-category/kpi-category-type.d.ts +1 -0
  52. package/dist-server/service/kpi-category/kpi-category-type.js +5 -1
  53. package/dist-server/service/kpi-category/kpi-category-type.js.map +1 -1
  54. package/dist-server/service/kpi-value/kpi-value-grade.service.d.ts +34 -0
  55. package/dist-server/service/kpi-value/kpi-value-grade.service.js +117 -0
  56. package/dist-server/service/kpi-value/kpi-value-grade.service.js.map +1 -0
  57. package/dist-server/service/kpi-value/kpi-value-mutation.d.ts +1 -0
  58. package/dist-server/service/kpi-value/kpi-value-mutation.js +15 -0
  59. package/dist-server/service/kpi-value/kpi-value-mutation.js.map +1 -1
  60. package/dist-server/service/kpi-value/kpi-value-query.d.ts +2 -0
  61. package/dist-server/service/kpi-value/kpi-value-query.js +12 -0
  62. package/dist-server/service/kpi-value/kpi-value-query.js.map +1 -1
  63. package/dist-server/tsconfig.tsbuildinfo +1 -1
  64. package/package.json +3 -3
  65. package/server/index.ts +0 -1
  66. package/server/service/index.ts +0 -5
  67. package/server/service/kpi/aggregate-kpi.ts +6 -0
  68. package/server/service/kpi/kpi-grade.types.ts +27 -0
  69. package/server/service/kpi/kpi-history.ts +9 -1
  70. package/server/service/kpi/kpi-query.ts +0 -6
  71. package/server/service/kpi/kpi-type.ts +13 -0
  72. package/server/service/kpi/kpi.ts +8 -4
  73. package/server/service/kpi-alert/kpi-alert-query.ts +13 -14
  74. package/server/service/kpi-category/kpi-category-type.ts +4 -1
  75. package/server/service/kpi-value/kpi-value-grade.service.ts +127 -0
  76. package/server/service/kpi-value/kpi-value-mutation.ts +9 -0
  77. package/server/service/kpi-value/kpi-value-query.ts +7 -0
  78. package/things-factory.config.js +0 -1
  79. package/client/pages/kpi-grade/kpi-grade-importer.ts +0 -90
  80. package/client/pages/kpi-grade/kpi-grade-list-page.ts +0 -405
  81. package/dist-client/pages/kpi-grade/kpi-grade-importer.d.ts +0 -23
  82. package/dist-client/pages/kpi-grade/kpi-grade-importer.js +0 -92
  83. package/dist-client/pages/kpi-grade/kpi-grade-importer.js.map +0 -1
  84. package/dist-client/pages/kpi-grade/kpi-grade-list-page.d.ts +0 -66
  85. package/dist-client/pages/kpi-grade/kpi-grade-list-page.js +0 -387
  86. package/dist-client/pages/kpi-grade/kpi-grade-list-page.js.map +0 -1
  87. package/dist-server/migrations/1752188906708-SeedKpiCategory.d.ts +0 -5
  88. package/dist-server/migrations/1752188906708-SeedKpiCategory.js +0 -56
  89. package/dist-server/migrations/1752188906708-SeedKpiCategory.js.map +0 -1
  90. package/dist-server/migrations/1752190849681-SeedKpi.d.ts +0 -5
  91. package/dist-server/migrations/1752190849681-SeedKpi.js +0 -107
  92. package/dist-server/migrations/1752190849681-SeedKpi.js.map +0 -1
  93. package/dist-server/migrations/1752191090459-SeedKpiGrade.d.ts +0 -5
  94. package/dist-server/migrations/1752191090459-SeedKpiGrade.js +0 -271
  95. package/dist-server/migrations/1752191090459-SeedKpiGrade.js.map +0 -1
  96. package/dist-server/migrations/index.d.ts +0 -1
  97. package/dist-server/migrations/index.js +0 -12
  98. package/dist-server/migrations/index.js.map +0 -1
  99. package/dist-server/service/kpi-grade/index.d.ts +0 -6
  100. package/dist-server/service/kpi-grade/index.js +0 -10
  101. package/dist-server/service/kpi-grade/index.js.map +0 -1
  102. package/dist-server/service/kpi-grade/kpi-grade-mutation.d.ts +0 -10
  103. package/dist-server/service/kpi-grade/kpi-grade-mutation.js +0 -151
  104. package/dist-server/service/kpi-grade/kpi-grade-mutation.js.map +0 -1
  105. package/dist-server/service/kpi-grade/kpi-grade-query.d.ts +0 -13
  106. package/dist-server/service/kpi-grade/kpi-grade-query.js +0 -92
  107. package/dist-server/service/kpi-grade/kpi-grade-query.js.map +0 -1
  108. package/dist-server/service/kpi-grade/kpi-grade-type.d.ts +0 -29
  109. package/dist-server/service/kpi-grade/kpi-grade-type.js +0 -113
  110. package/dist-server/service/kpi-grade/kpi-grade-type.js.map +0 -1
  111. package/dist-server/service/kpi-grade/kpi-grade.d.ts +0 -24
  112. package/dist-server/service/kpi-grade/kpi-grade.js +0 -117
  113. package/dist-server/service/kpi-grade/kpi-grade.js.map +0 -1
  114. package/server/migrations/1752188906708-SeedKpiCategory.ts +0 -61
  115. package/server/migrations/1752190849681-SeedKpi.ts +0 -112
  116. package/server/migrations/1752191090459-SeedKpiGrade.ts +0 -270
  117. package/server/migrations/index.ts +0 -9
  118. package/server/service/kpi-grade/index.ts +0 -7
  119. package/server/service/kpi-grade/kpi-grade-mutation.ts +0 -146
  120. package/server/service/kpi-grade/kpi-grade-query.ts +0 -58
  121. package/server/service/kpi-grade/kpi-grade-type.ts +0 -82
  122. package/server/service/kpi-grade/kpi-grade.ts +0 -101
@@ -0,0 +1,294 @@
1
+ import '@material/web/button/elevated-button.js'
2
+ import '@material/web/button/filled-button.js'
3
+ import '@material/web/button/text-button.js'
4
+ import '@material/web/textfield/outlined-text-field.js'
5
+ import '@material/web/select/outlined-select.js'
6
+ import '@material/web/select/select-option.js'
7
+ import '@material/web/icon/icon.js'
8
+
9
+ import { LitElement, css, html } from 'lit'
10
+ import { customElement, property, state } from 'lit/decorators.js'
11
+ import { ScopedElementsMixin } from '@open-wc/scoped-elements'
12
+ import { client } from '@operato/graphql'
13
+ import { notify } from '@operato/layout'
14
+ import gql from 'graphql-tag'
15
+ import { CommonButtonStyles, CommonHeaderStyles, CommonGristStyles, ScrollbarStyles } from '@operato/styles'
16
+
17
+ // KPI 등급 타입 정의 (서버와 동일한 구조)
18
+ interface KpiGrade {
19
+ name: string
20
+ minValue: number
21
+ maxValue: number
22
+ score?: number
23
+ color?: string
24
+ description?: string
25
+ }
26
+
27
+ type KpiGrades = KpiGrade[]
28
+
29
+ @customElement('kpi-grade-editor')
30
+ export class KpiGradeEditor extends ScopedElementsMixin(LitElement) {
31
+ static styles = [
32
+ CommonHeaderStyles,
33
+ ScrollbarStyles,
34
+ css`
35
+ :host {
36
+ display: flex;
37
+ flex-direction: column;
38
+ background-color: var(--md-sys-color-surface, #f4f6fa);
39
+ }
40
+
41
+ .grade-list {
42
+ flex: 1;
43
+ margin-bottom: 20px;
44
+ overflow-y: auto;
45
+ }
46
+
47
+ .grade-item {
48
+ display: flex;
49
+ align-items: center;
50
+ gap: 10px;
51
+ padding: 10px;
52
+ border: 1px solid #ddd;
53
+ border-radius: 4px;
54
+ margin-bottom: 10px;
55
+ background: #f9f9f9;
56
+ }
57
+
58
+ .grade-item:hover {
59
+ background: #f0f0f0;
60
+ }
61
+
62
+ .grade-inputs {
63
+ display: flex;
64
+ gap: 10px;
65
+ flex: 1;
66
+ }
67
+
68
+ .grade-inputs md-outlined-text-field {
69
+ flex: 1;
70
+ }
71
+
72
+ .grade-actions {
73
+ display: flex;
74
+ gap: 5px;
75
+ }
76
+
77
+ .footer span {
78
+ font-size: 0.8em;
79
+ color: var(--md-sys-color-on-surface);
80
+ line-height: 1.5;
81
+ padding: 10px;
82
+ }
83
+ `
84
+ ]
85
+
86
+ @property({ type: Object }) kpi: any
87
+ @state() grades: KpiGrades = []
88
+ @state() isDirty = false
89
+
90
+ connectedCallback() {
91
+ super.connectedCallback()
92
+ if (this.kpi?.grades) {
93
+ this.grades = [...this.kpi.grades]
94
+ }
95
+ }
96
+
97
+ render() {
98
+ return html`
99
+ <div class="grade-list">${this.grades.map((grade, index) => this._renderGradeItem(grade, index))}</div>
100
+
101
+ <div class="footer">
102
+ <button type="button" @click=${this._addGrade}><md-icon>add</md-icon>등급 추가</button>
103
+ <div filler></div>
104
+ <button type="button" @click=${this._cancel}><md-icon>cancel</md-icon>취소</button>
105
+ <button type="button" @click=${this._save} ?disabled=${!this.isDirty} done><md-icon>save</md-icon>저장</button>
106
+ </div>
107
+ `
108
+ }
109
+
110
+ _renderGradeItem(grade: KpiGrade, index: number) {
111
+ return html`
112
+ <div class="grade-item">
113
+ <div class="grade-inputs">
114
+ <md-outlined-text-field
115
+ label="등급명"
116
+ value=${grade.name}
117
+ @input=${(e: any) => this._updateGrade(index, 'name', e.target.value)}
118
+ ></md-outlined-text-field>
119
+
120
+ <md-outlined-text-field
121
+ label="최소값"
122
+ type="number"
123
+ value=${grade.minValue}
124
+ @input=${(e: any) => this._updateGrade(index, 'minValue', parseFloat(e.target.value))}
125
+ ></md-outlined-text-field>
126
+
127
+ <md-outlined-text-field
128
+ label="최대값"
129
+ type="number"
130
+ value=${grade.maxValue}
131
+ @input=${(e: any) => this._updateGrade(index, 'maxValue', parseFloat(e.target.value))}
132
+ ></md-outlined-text-field>
133
+
134
+ <md-outlined-text-field
135
+ label="점수"
136
+ type="number"
137
+ value=${grade.score || ''}
138
+ @input=${(e: any) => this._updateGrade(index, 'score', parseFloat(e.target.value))}
139
+ ></md-outlined-text-field>
140
+
141
+ <md-outlined-text-field
142
+ label="색상"
143
+ value=${grade.color || ''}
144
+ @input=${(e: any) => this._updateGrade(index, 'color', e.target.value)}
145
+ ></md-outlined-text-field>
146
+ </div>
147
+
148
+ <div class="grade-actions">
149
+ <md-icon-button @click=${() => this._removeGrade(index)}>
150
+ <md-icon>delete</md-icon>
151
+ </md-icon-button>
152
+ </div>
153
+ </div>
154
+ `
155
+ }
156
+
157
+ _updateGrade(index: number, field: keyof KpiGrade, value: any) {
158
+ this.grades[index] = { ...this.grades[index], [field]: value }
159
+ this.isDirty = true
160
+ this.requestUpdate()
161
+ }
162
+
163
+ _addGrade() {
164
+ const newGrade: KpiGrade = {
165
+ name: '',
166
+ minValue: 0,
167
+ maxValue: 0,
168
+ score: 0,
169
+ color: '#4caf50',
170
+ description: ''
171
+ }
172
+ this.grades.push(newGrade)
173
+ this.isDirty = true
174
+ this.requestUpdate()
175
+ }
176
+
177
+ _removeGrade(index: number) {
178
+ this.grades.splice(index, 1)
179
+ this.isDirty = true
180
+ this.requestUpdate()
181
+ }
182
+
183
+ _loadTemplate(template: string) {
184
+ switch (template) {
185
+ case '5grade':
186
+ this.grades = [
187
+ { name: 'A', minValue: 0, maxValue: 1.5, score: 95, color: '#4caf50', description: '우수' },
188
+ { name: 'B', minValue: 1.5, maxValue: 2.5, score: 85, color: '#ff9800', description: '양호' },
189
+ { name: 'C', minValue: 2.5, maxValue: 3.5, score: 75, color: '#ffc107', description: '보통' },
190
+ { name: 'D', minValue: 3.5, maxValue: 4.5, score: 65, color: '#ff9800', description: '미흡' },
191
+ { name: 'E', minValue: 4.5, maxValue: 999, score: 55, color: '#f44336', description: '불량' }
192
+ ]
193
+ break
194
+ case '3grade':
195
+ this.grades = [
196
+ { name: '우수', minValue: 0, maxValue: 1.5, score: 95, color: '#4caf50', description: '목표 달성' },
197
+ { name: '양호', minValue: 1.5, maxValue: 2.5, score: 85, color: '#ff9800', description: '기준 달성' },
198
+ { name: '미흡', minValue: 2.5, maxValue: 999, score: 75, color: '#f44336', description: '개선 필요' }
199
+ ]
200
+ break
201
+ case 'continuous':
202
+ this.grades = [
203
+ { name: '0.999999', minValue: 0, maxValue: 0.025, score: 0.999999, color: '#4caf50' },
204
+ { name: '0.944189368', minValue: 0.025, maxValue: 0.05, score: 0.944189368, color: '#4caf50' },
205
+ { name: '0.888379735', minValue: 0.05, maxValue: 0.075, score: 0.888379735, color: '#4caf50' }
206
+ ]
207
+ break
208
+ case 'clear':
209
+ this.grades = []
210
+ break
211
+ }
212
+ this.isDirty = true
213
+ this.requestUpdate()
214
+ }
215
+
216
+ async _save() {
217
+ try {
218
+ // 등급 유효성 검사
219
+ if (!this._validateGrades()) {
220
+ return
221
+ }
222
+
223
+ // 정렬 (minValue 기준)
224
+ this.grades.sort((a, b) => a.minValue - b.minValue)
225
+
226
+ this.dispatchEvent(
227
+ new CustomEvent('grades-updated', {
228
+ detail: {
229
+ kpiId: this.kpi.id,
230
+ grades: this.grades
231
+ }
232
+ })
233
+ )
234
+
235
+ this.isDirty = false
236
+ } catch (error) {
237
+ notify({
238
+ message: '등급 저장 중 오류가 발생했습니다.'
239
+ })
240
+ }
241
+ }
242
+
243
+ _validateGrades(): boolean {
244
+ // 최소 1개 등급 필요
245
+ if (this.grades.length === 0) {
246
+ notify({
247
+ message: '최소 1개 이상의 등급을 설정해야 합니다.'
248
+ })
249
+ return false
250
+ }
251
+
252
+ // 등급명 중복 체크
253
+ const names = this.grades.map(g => g.name)
254
+ const uniqueNames = new Set(names)
255
+ if (names.length !== uniqueNames.size) {
256
+ notify({
257
+ message: '등급명이 중복되었습니다.'
258
+ })
259
+ return false
260
+ }
261
+
262
+ // 값 범위 체크
263
+ for (let i = 0; i < this.grades.length; i++) {
264
+ const grade = this.grades[i]
265
+ if (grade.minValue >= grade.maxValue) {
266
+ notify({
267
+ message: `등급 "${grade.name}"의 최소값이 최대값보다 크거나 같습니다.`
268
+ })
269
+ return false
270
+ }
271
+
272
+ // 연속성 체크
273
+ if (i > 0) {
274
+ const prevGrade = this.grades[i - 1]
275
+ if (prevGrade.maxValue !== grade.minValue) {
276
+ notify({
277
+ message: `등급 "${prevGrade.name}"과 "${grade.name}" 사이에 간격이 있습니다.`
278
+ })
279
+ return false
280
+ }
281
+ }
282
+ }
283
+
284
+ return true
285
+ }
286
+
287
+ _cancel() {
288
+ // 팝업 닫기
289
+ const popup = this.closest('ox-popup') as any
290
+ if (popup && popup.close) {
291
+ popup.close()
292
+ }
293
+ }
294
+ }
@@ -20,6 +20,7 @@ import { connect } from 'pwa-helpers/connect-mixin'
20
20
  import gql from 'graphql-tag'
21
21
 
22
22
  import { KpiImporter } from './kpi-importer'
23
+ import { KpiGradeEditor } from './kpi-grade-editor'
23
24
 
24
25
  @customElement('kpi-list-page')
25
26
  export class KpiListPage extends connect(store)(localize(i18next)(ScopedElementsMixin(PageView))) {
@@ -50,7 +51,8 @@ export class KpiListPage extends connect(store)(localize(i18next)(ScopedElements
50
51
 
51
52
  static get scopedElements() {
52
53
  return {
53
- 'kpi-importer': KpiImporter
54
+ 'kpi-importer': KpiImporter,
55
+ 'kpi-grade-editor': KpiGradeEditor
54
56
  }
55
57
  }
56
58
 
@@ -204,6 +206,26 @@ export class KpiListPage extends connect(store)(localize(i18next)(ScopedElements
204
206
  width: 200
205
207
  },
206
208
  { type: 'string', name: 'formula', header: '산식', record: { editable: true }, width: 200 },
209
+ {
210
+ type: 'string',
211
+ name: 'grades',
212
+ header: '등급 설정',
213
+ record: {
214
+ editable: false,
215
+ renderer: (v, c, r) => {
216
+ if (r.grades && r.grades.length > 0) {
217
+ return html`<span style="color: #4caf50; cursor: pointer;" @click=${() => this._editGrades(r)}>
218
+ ${r.grades.length}개 등급 설정됨
219
+ </span>`
220
+ } else {
221
+ return html`<span style="color: #999; cursor: pointer;" @click=${() => this._editGrades(r)}>
222
+ 등급 설정 없음
223
+ </span>`
224
+ }
225
+ }
226
+ },
227
+ width: 150
228
+ },
207
229
  {
208
230
  type: 'checkbox',
209
231
  name: 'active',
@@ -272,6 +294,7 @@ export class KpiListPage extends connect(store)(localize(i18next)(ScopedElements
272
294
  name
273
295
  description
274
296
  active
297
+ grades
275
298
  category {
276
299
  id
277
300
  name
@@ -450,4 +473,49 @@ export class KpiListPage extends connect(store)(localize(i18next)(ScopedElements
450
473
  this.grist.fetch()
451
474
  }
452
475
  }
476
+
477
+ async _editGrades(kpi: any) {
478
+ const popup = await openPopup(
479
+ html`
480
+ <kpi-grade-editor .kpi=${kpi} @grades-updated=${(e: any) => this._onGradesUpdated(e.detail)}></kpi-grade-editor>
481
+ `,
482
+ {
483
+ title: `${kpi.name} - 등급 설정`,
484
+ size: 'large'
485
+ }
486
+ )
487
+ }
488
+
489
+ async _onGradesUpdated(detail: any) {
490
+ const { kpiId, grades } = detail
491
+
492
+ try {
493
+ const response = await client.mutate({
494
+ mutation: gql`
495
+ mutation ($id: String!, $patch: KpiPatch!) {
496
+ updateKpi(id: $id, patch: $patch) {
497
+ id
498
+ name
499
+ grades
500
+ }
501
+ }
502
+ `,
503
+ variables: {
504
+ id: kpiId,
505
+ patch: { grades }
506
+ }
507
+ })
508
+
509
+ if (!response.errors) {
510
+ this.grist.fetch()
511
+ notify({
512
+ message: '등급 설정이 성공적으로 업데이트되었습니다.'
513
+ })
514
+ }
515
+ } catch (error) {
516
+ notify({
517
+ message: '등급 설정 업데이트 중 오류가 발생했습니다.'
518
+ })
519
+ }
520
+ }
453
521
  }
@@ -20,15 +20,7 @@ const GET_KPI_OVERVIEW = gql`
20
20
  description
21
21
  formula
22
22
  active
23
- grades {
24
- id
25
- name
26
- minValue
27
- maxValue
28
- score
29
- color
30
- description
31
- }
23
+ grades
32
24
  }
33
25
  }
34
26
  }
@@ -246,11 +238,13 @@ export class KpiOverview extends PageView {
246
238
  <div style="font-size:1.5rem;font-weight:bold;margin-bottom:16px;">${kpi?.name || ''}</div>
247
239
  <div style="margin-bottom:16px;">${kpi?.formula ? html`<b>산식:</b> ${kpi.formula}` : ''}</div>
248
240
  <div .innerHTML=${marked(md)}></div>
249
- ${kpi?.grades?.length
241
+ ${kpi?.grades && Array.isArray(kpi.grades) && kpi.grades.length > 0
250
242
  ? html`<div style="margin-top:32px;">
251
243
  <b>등급 구간</b>
252
244
  <ul>
253
- ${kpi.grades.map(g => html`<li>${g.name}: ${g.minValue}~${g.maxValue} (${g.description || ''})</li>`)}
245
+ ${kpi.grades.map(
246
+ (g: any) => html`<li>${g.name}: ${g.minValue}~${g.maxValue} (${g.description || ''})</li>`
247
+ )}
254
248
  </ul>
255
249
  </div>`
256
250
  : ''}
@@ -109,13 +109,7 @@ export class KpiDashboardPage extends PageView {
109
109
  }
110
110
  targetValue
111
111
  unit
112
- grades {
113
- name
114
- minValue
115
- maxValue
116
- score
117
- color
118
- }
112
+ grades
119
113
  histories(limit: 1) {
120
114
  version
121
115
  updatedAt
@@ -234,14 +228,48 @@ export class KpiDashboardPage extends PageView {
234
228
  <!-- 등급 시각화 -->
235
229
  <div style="margin-top:8px;">
236
230
  <b>등급:</b>
237
- ${(kpi.grades || []).map(
238
- g =>
239
- html`<span
240
- style="display:inline-block;margin-right:8px;padding:2px 8px;border-radius:8px;background:${g.color ??
241
- '#eee'};color:#222;font-size:0.95em;"
242
- >${g.name}(${g.minValue}~${g.maxValue}${kpi.unit ?? ''}, ${g.score ?? ''}점)</span
243
- >`
244
- )}
231
+ ${(() => {
232
+ const grades = kpi.grades || []
233
+ const kpiValue = kpi.value?.value
234
+
235
+ if (grades.length > 5) {
236
+ // 5개를 넘으면 현재 값에 해당하는 grade만 표시
237
+ const currentGrade = grades.find(g => kpiValue >= g.minValue && kpiValue <= g.maxValue)
238
+ if (currentGrade) {
239
+ return html`<span
240
+ style="display:inline-block;margin-right:8px;padding:4px 12px;border-radius:12px;background:${currentGrade.color ??
241
+ '#3a3ad6'};color:white;font-size:1em;font-weight:bold;box-shadow:0 2px 8px rgba(58,58,214,0.3);border:2px solid ${currentGrade.color ??
242
+ '#3a3ad6'};"
243
+ >${currentGrade.name}(${currentGrade.minValue}~${currentGrade.maxValue}${kpi.unit ??
244
+ ''},
245
+ ${currentGrade.score ?? ''}점)</span
246
+ >`
247
+ } else {
248
+ return html`<span style="color:#bbb;font-size:0.95em;">등급 없음</span>`
249
+ }
250
+ } else {
251
+ // 5개 이하면 전체 리스트 표시하되 현재 등급은 강조
252
+ return grades.map(g => {
253
+ const isCurrentGrade = kpiValue >= g.minValue && kpiValue <= g.maxValue
254
+ return html`<span
255
+ style="display:inline-block;margin-right:8px;padding:${isCurrentGrade
256
+ ? '4px 12px'
257
+ : '2px 8px'};border-radius:${isCurrentGrade
258
+ ? '12px'
259
+ : '8px'};background:${isCurrentGrade
260
+ ? (g.color ?? '#3a3ad6')
261
+ : (g.color ?? '#eee')};color:${isCurrentGrade
262
+ ? 'white'
263
+ : '#222'};font-size:${isCurrentGrade ? '1em' : '0.95em'};font-weight:${isCurrentGrade
264
+ ? 'bold'
265
+ : 'normal'};box-shadow:${isCurrentGrade
266
+ ? '0 2px 8px rgba(58,58,214,0.3)'
267
+ : 'none'};border:${isCurrentGrade ? `2px solid ${g.color ?? '#3a3ad6'}` : 'none'};"
268
+ >${g.name}(${g.minValue}~${g.maxValue}${kpi.unit ?? ''}, ${g.score ?? ''}점)</span
269
+ >`
270
+ })
271
+ }
272
+ })()}
245
273
  </div>
246
274
  <!-- 최근 변경 이력(1건만) -->
247
275
  <div style="margin-top:8px;font-size:0.95em;color:#888;">
@@ -72,10 +72,7 @@ export class KpiPerformanceSummary extends LitElement {
72
72
  id
73
73
  name
74
74
  description
75
- value {
76
- value
77
- valueDate
78
- }
75
+ value
79
76
  targetValue
80
77
  unit
81
78
  }
@@ -84,12 +81,22 @@ export class KpiPerformanceSummary extends LitElement {
84
81
  }
85
82
  `
86
83
  })
87
- this.kpis = (response.data.kpis.items || []).map(kpi => ({
88
- name: kpi.name,
89
- value: kpi.value?.value ?? '-',
90
- target: kpi.targetValue ?? '-',
91
- unit: kpi.unit ?? ''
92
- }))
84
+ this.kpis = (response.data.kpis.items || []).map(kpi => {
85
+ // value가 JSON 형태로 반환되므로 적절히 처리
86
+ let value = '-'
87
+ if (kpi.value && typeof kpi.value === 'object') {
88
+ value = kpi.value.value ?? kpi.value.latestValue ?? '-'
89
+ } else if (kpi.value) {
90
+ value = kpi.value
91
+ }
92
+
93
+ return {
94
+ name: kpi.name,
95
+ value: value,
96
+ target: kpi.targetValue ?? '-',
97
+ unit: kpi.unit ?? ''
98
+ }
99
+ })
93
100
  } catch (e) {
94
101
  this.error = 'KPI 데이터를 불러오지 못했습니다.'
95
102
  } finally {
@@ -102,6 +102,7 @@ export class KpiValueManualEntryForm extends LitElement {
102
102
  <div class="desc">입력방식: MANUAL, Source: MANUAL (자동 지정)</div>
103
103
  </div>
104
104
  </form>
105
+
105
106
  <div class="footer">
106
107
  <div filler></div>
107
108
  <button type="button" ?disabled=${this.loading} done @click=${this.save}><md-icon>save</md-icon>저장</button>
package/client/route.ts CHANGED
@@ -28,10 +28,6 @@ export default function route(page: string) {
28
28
  import('./pages/kpi-value/kpi-value-manual-entry-page')
29
29
  return page
30
30
 
31
- case 'kpi-grade-list':
32
- import('./pages/kpi-grade/kpi-grade-list-page')
33
- return page
34
-
35
31
  case 'kpi-history-list':
36
32
  import('./pages/kpi-history/kpi-history-list-page')
37
33
  return page
@@ -0,0 +1,35 @@
1
+ import '@material/web/button/elevated-button.js';
2
+ import '@material/web/button/filled-button.js';
3
+ import '@material/web/button/text-button.js';
4
+ import '@material/web/textfield/outlined-text-field.js';
5
+ import '@material/web/select/outlined-select.js';
6
+ import '@material/web/select/select-option.js';
7
+ import '@material/web/icon/icon.js';
8
+ import { LitElement } from 'lit';
9
+ interface KpiGrade {
10
+ name: string;
11
+ minValue: number;
12
+ maxValue: number;
13
+ score?: number;
14
+ color?: string;
15
+ description?: string;
16
+ }
17
+ type KpiGrades = KpiGrade[];
18
+ declare const KpiGradeEditor_base: typeof LitElement & import("@open-wc/dedupe-mixin").Constructor<import("@open-wc/scoped-elements/types/src/types").ScopedElementsHost>;
19
+ export declare class KpiGradeEditor extends KpiGradeEditor_base {
20
+ static styles: import("lit").CSSResult[];
21
+ kpi: any;
22
+ grades: KpiGrades;
23
+ isDirty: boolean;
24
+ connectedCallback(): void;
25
+ render(): import("lit-html").TemplateResult<1>;
26
+ _renderGradeItem(grade: KpiGrade, index: number): import("lit-html").TemplateResult<1>;
27
+ _updateGrade(index: number, field: keyof KpiGrade, value: any): void;
28
+ _addGrade(): void;
29
+ _removeGrade(index: number): void;
30
+ _loadTemplate(template: string): void;
31
+ _save(): Promise<void>;
32
+ _validateGrades(): boolean;
33
+ _cancel(): void;
34
+ }
35
+ export {};