@things-factory/kpi 9.0.17 → 9.0.19

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 (137) hide show
  1. package/client/bootstrap.ts +8 -0
  2. package/client/pages/kpi/kpi-list-page.ts +99 -11
  3. package/client/pages/kpi/kpi-viz-editor.ts +214 -14
  4. package/client/pages/kpi-category/kpi-category-list-page.ts +80 -8
  5. package/client/pages/kpi-history/kpi-history-list-page.ts +1 -1
  6. package/client/pages/kpi-metric/kpi-metric-list-page.ts +31 -7
  7. package/client/pages/kpi-metric-value/kpi-metric-value-importer.ts +65 -0
  8. package/client/pages/kpi-metric-value/kpi-metric-value-list-page.ts +299 -0
  9. package/client/pages/{kpi-value/kpi-value-manual-entry-form.ts → kpi-metric-value/kpi-metric-value-manual-entry-form.ts} +18 -44
  10. package/client/pages/{kpi-value/kpi-value-manual-entry-page.ts → kpi-metric-value/kpi-metric-value-manual-entry-page.ts} +21 -21
  11. package/client/pages/kpi-value/kpi-value-list-page.ts +4 -6
  12. package/client/route.ts +6 -2
  13. package/dist-client/bootstrap.d.ts +2 -0
  14. package/dist-client/bootstrap.js +7 -0
  15. package/dist-client/bootstrap.js.map +1 -0
  16. package/dist-client/pages/kpi/kpi-list-page.d.ts +6 -0
  17. package/dist-client/pages/kpi/kpi-list-page.js +100 -11
  18. package/dist-client/pages/kpi/kpi-list-page.js.map +1 -1
  19. package/dist-client/pages/kpi/kpi-viz-editor.js +208 -14
  20. package/dist-client/pages/kpi/kpi-viz-editor.js.map +1 -1
  21. package/dist-client/pages/kpi-category/kpi-category-list-page.d.ts +5 -0
  22. package/dist-client/pages/kpi-category/kpi-category-list-page.js +83 -8
  23. package/dist-client/pages/kpi-category/kpi-category-list-page.js.map +1 -1
  24. package/dist-client/pages/kpi-history/kpi-history-list-page.js +1 -1
  25. package/dist-client/pages/kpi-history/kpi-history-list-page.js.map +1 -1
  26. package/dist-client/pages/kpi-metric/kpi-metric-list-page.js +29 -5
  27. package/dist-client/pages/kpi-metric/kpi-metric-list-page.js.map +1 -1
  28. package/dist-client/pages/kpi-metric-value/kpi-metric-value-importer.d.ts +23 -0
  29. package/dist-client/pages/kpi-metric-value/kpi-metric-value-importer.js +75 -0
  30. package/dist-client/pages/kpi-metric-value/kpi-metric-value-importer.js.map +1 -0
  31. package/dist-client/pages/kpi-metric-value/kpi-metric-value-list-page.d.ts +61 -0
  32. package/dist-client/pages/kpi-metric-value/kpi-metric-value-list-page.js +301 -0
  33. package/dist-client/pages/kpi-metric-value/kpi-metric-value-list-page.js.map +1 -0
  34. package/dist-client/pages/{kpi-value/kpi-value-manual-entry-form.d.ts → kpi-metric-value/kpi-metric-value-manual-entry-form.d.ts} +3 -5
  35. package/dist-client/pages/{kpi-value/kpi-value-manual-entry-form.js → kpi-metric-value/kpi-metric-value-manual-entry-form.js} +27 -56
  36. package/dist-client/pages/kpi-metric-value/kpi-metric-value-manual-entry-form.js.map +1 -0
  37. package/dist-client/pages/{kpi-value/kpi-value-manual-entry-page.d.ts → kpi-metric-value/kpi-metric-value-manual-entry-page.d.ts} +5 -5
  38. package/dist-client/pages/{kpi-value/kpi-value-manual-entry-page.js → kpi-metric-value/kpi-metric-value-manual-entry-page.js} +28 -28
  39. package/dist-client/pages/kpi-metric-value/kpi-metric-value-manual-entry-page.js.map +1 -0
  40. package/dist-client/pages/kpi-value/kpi-value-list-page.js +4 -6
  41. package/dist-client/pages/kpi-value/kpi-value-list-page.js.map +1 -1
  42. package/dist-client/route.d.ts +1 -1
  43. package/dist-client/route.js +5 -2
  44. package/dist-client/route.js.map +1 -1
  45. package/dist-client/tsconfig.tsbuildinfo +1 -1
  46. package/dist-server/service/index.d.ts +4 -2
  47. package/dist-server/service/index.js +6 -1
  48. package/dist-server/service/index.js.map +1 -1
  49. package/dist-server/service/kpi/aggregate-kpi.js +5 -7
  50. package/dist-server/service/kpi/aggregate-kpi.js.map +1 -1
  51. package/dist-server/service/kpi/kpi-history.d.ts +3 -1
  52. package/dist-server/service/kpi/kpi-history.js +10 -0
  53. package/dist-server/service/kpi/kpi-history.js.map +1 -1
  54. package/dist-server/service/kpi/kpi-mutation.js +1 -1
  55. package/dist-server/service/kpi/kpi-mutation.js.map +1 -1
  56. package/dist-server/service/kpi/kpi-type.d.ts +2 -0
  57. package/dist-server/service/kpi/kpi-type.js +8 -0
  58. package/dist-server/service/kpi/kpi-type.js.map +1 -1
  59. package/dist-server/service/kpi/kpi.d.ts +9 -0
  60. package/dist-server/service/kpi/kpi.js +23 -1
  61. package/dist-server/service/kpi/kpi.js.map +1 -1
  62. package/dist-server/service/kpi-category/kpi-category-mutation.js +0 -8
  63. package/dist-server/service/kpi-category/kpi-category-mutation.js.map +1 -1
  64. package/dist-server/service/kpi-category/kpi-category-type.d.ts +4 -2
  65. package/dist-server/service/kpi-category/kpi-category-type.js +16 -8
  66. package/dist-server/service/kpi-category/kpi-category-type.js.map +1 -1
  67. package/dist-server/service/kpi-category/kpi-category.d.ts +2 -2
  68. package/dist-server/service/kpi-category/kpi-category.js +8 -8
  69. package/dist-server/service/kpi-category/kpi-category.js.map +1 -1
  70. package/dist-server/service/kpi-metric/aggregate-kpi-metric.js +31 -74
  71. package/dist-server/service/kpi-metric/aggregate-kpi-metric.js.map +1 -1
  72. package/dist-server/service/kpi-metric/kpi-metric-mutation.d.ts +1 -1
  73. package/dist-server/service/kpi-metric/kpi-metric-mutation.js +15 -28
  74. package/dist-server/service/kpi-metric/kpi-metric-mutation.js.map +1 -1
  75. package/dist-server/service/kpi-metric/kpi-metric-type.d.ts +6 -4
  76. package/dist-server/service/kpi-metric/kpi-metric-type.js +20 -12
  77. package/dist-server/service/kpi-metric/kpi-metric-type.js.map +1 -1
  78. package/dist-server/service/kpi-metric/kpi-metric.d.ts +15 -2
  79. package/dist-server/service/kpi-metric/kpi-metric.js +34 -14
  80. package/dist-server/service/kpi-metric/kpi-metric.js.map +1 -1
  81. package/dist-server/service/kpi-metric-value/index.d.ts +6 -0
  82. package/dist-server/service/kpi-metric-value/index.js +10 -0
  83. package/dist-server/service/kpi-metric-value/index.js.map +1 -0
  84. package/dist-server/service/kpi-metric-value/kpi-metric-value-mutation.d.ts +11 -0
  85. package/dist-server/service/kpi-metric-value/kpi-metric-value-mutation.js +229 -0
  86. package/dist-server/service/kpi-metric-value/kpi-metric-value-mutation.js.map +1 -0
  87. package/dist-server/service/kpi-metric-value/kpi-metric-value-query.d.ts +13 -0
  88. package/dist-server/service/kpi-metric-value/kpi-metric-value-query.js +95 -0
  89. package/dist-server/service/kpi-metric-value/kpi-metric-value-query.js.map +1 -0
  90. package/dist-server/service/kpi-metric-value/kpi-metric-value-type.d.ts +26 -0
  91. package/dist-server/service/kpi-metric-value/kpi-metric-value-type.js +112 -0
  92. package/dist-server/service/kpi-metric-value/kpi-metric-value-type.js.map +1 -0
  93. package/dist-server/service/kpi-metric-value/kpi-metric-value.d.ts +23 -0
  94. package/dist-server/service/kpi-metric-value/kpi-metric-value.js +106 -0
  95. package/dist-server/service/kpi-metric-value/kpi-metric-value.js.map +1 -0
  96. package/dist-server/service/kpi-value/kpi-value-mutation.js +1 -2
  97. package/dist-server/service/kpi-value/kpi-value-mutation.js.map +1 -1
  98. package/dist-server/service/kpi-value/kpi-value-query.js +1 -1
  99. package/dist-server/service/kpi-value/kpi-value-query.js.map +1 -1
  100. package/dist-server/service/kpi-value/kpi-value-type.d.ts +2 -4
  101. package/dist-server/service/kpi-value/kpi-value-type.js +4 -18
  102. package/dist-server/service/kpi-value/kpi-value-type.js.map +1 -1
  103. package/dist-server/service/kpi-value/kpi-value.d.ts +3 -3
  104. package/dist-server/service/kpi-value/kpi-value.js +13 -14
  105. package/dist-server/service/kpi-value/kpi-value.js.map +1 -1
  106. package/dist-server/tsconfig.tsbuildinfo +1 -1
  107. package/package.json +3 -3
  108. package/server/service/index.ts +6 -1
  109. package/server/service/kpi/aggregate-kpi.ts +5 -8
  110. package/server/service/kpi/kpi-history.ts +9 -1
  111. package/server/service/kpi/kpi-mutation.ts +1 -1
  112. package/server/service/kpi/kpi-type.ts +6 -0
  113. package/server/service/kpi/kpi.ts +21 -0
  114. package/server/service/kpi-category/kpi-category-mutation.ts +0 -10
  115. package/server/service/kpi-category/kpi-category-type.ts +12 -6
  116. package/server/service/kpi-category/kpi-category.ts +6 -6
  117. package/server/service/kpi-metric/aggregate-kpi-metric.ts +29 -69
  118. package/server/service/kpi-metric/kpi-metric-mutation.ts +15 -26
  119. package/server/service/kpi-metric/kpi-metric-type.ts +17 -12
  120. package/server/service/kpi-metric/kpi-metric.ts +32 -11
  121. package/server/service/kpi-metric-value/index.ts +7 -0
  122. package/server/service/kpi-metric-value/kpi-metric-value-mutation.ts +215 -0
  123. package/server/service/kpi-metric-value/kpi-metric-value-query.ts +60 -0
  124. package/server/service/kpi-metric-value/kpi-metric-value-type.ts +82 -0
  125. package/server/service/kpi-metric-value/kpi-metric-value.ts +91 -0
  126. package/server/service/kpi-value/kpi-value-mutation.ts +1 -2
  127. package/server/service/kpi-value/kpi-value-query.ts +1 -1
  128. package/server/service/kpi-value/kpi-value-type.ts +4 -16
  129. package/server/service/kpi-value/kpi-value.ts +14 -14
  130. package/things-factory.config.js +5 -3
  131. package/translations/en.json +8 -1
  132. package/translations/ja.json +8 -1
  133. package/translations/ko.json +9 -2
  134. package/translations/ms.json +9 -2
  135. package/translations/zh.json +8 -1
  136. package/dist-client/pages/kpi-value/kpi-value-manual-entry-form.js.map +0 -1
  137. package/dist-client/pages/kpi-value/kpi-value-manual-entry-page.js.map +0 -1
@@ -0,0 +1,8 @@
1
+ import '@operato/app/filter-renderer.js' /* register resource-object filter renderer */
2
+
3
+ import { registerEditor as registerGristEditor } from '@operato/data-grist'
4
+ import { OxGristEditorFormula } from '@operato/grist-editor/ox-grist-editor-formula.js'
5
+
6
+ export default function bootstrap() {
7
+ registerGristEditor('formula', OxGristEditorFormula)
8
+ }
@@ -4,11 +4,12 @@ import '@operato/data-grist/ox-grist.js'
4
4
  import '@operato/data-grist/ox-filters-form.js'
5
5
  import '@operato/data-grist/ox-record-creator.js'
6
6
  import './kpi-viz-editor.js'
7
+ import './kpi-grade-editor.js'
7
8
 
8
9
  import { CommonButtonStyles, CommonHeaderStyles, CommonGristStyles, ScrollbarStyles } from '@operato/styles'
9
10
  import { PageView, store } from '@operato/shell'
10
11
  import { css, html } from 'lit'
11
- import { customElement, property, query } from 'lit/decorators.js'
12
+ import { customElement, property, query, state } from 'lit/decorators.js'
12
13
  import { ScopedElementsMixin } from '@open-wc/scoped-elements'
13
14
  import { ColumnConfig, DataGrist, FetchOption } from '@operato/data-grist'
14
15
  import { client } from '@operato/graphql'
@@ -64,6 +65,36 @@ export class KpiListPage extends connect(store)(localize(i18next)(ScopedElements
64
65
 
65
66
  @query('ox-grist') private grist!: DataGrist
66
67
 
68
+ @state() availableVariables: any[] = []
69
+ @state() availableVariablesLoaded = false
70
+
71
+ async getAvailableVariables() {
72
+ if (this.availableVariablesLoaded) {
73
+ return this.availableVariables
74
+ }
75
+ const response = await client.query({
76
+ query: gql`
77
+ query {
78
+ kpiMetrics {
79
+ items {
80
+ name
81
+ description
82
+ unit
83
+ }
84
+ }
85
+ }
86
+ `
87
+ })
88
+ this.availableVariables = (response.data.kpiMetrics.items || []).map(metric => ({
89
+ name: metric.name,
90
+ description: metric.description,
91
+ type: 'kpi-metric',
92
+ unit: metric.unit
93
+ }))
94
+ this.availableVariablesLoaded = true
95
+ return this.availableVariables
96
+ }
97
+
67
98
  get context() {
68
99
  return {
69
100
  title: i18next.t('title.kpi list'),
@@ -127,6 +158,11 @@ export class KpiListPage extends connect(store)(localize(i18next)(ScopedElements
127
158
  `
128
159
  }
129
160
 
161
+ connectedCallback() {
162
+ super.connectedCallback()
163
+ this.fetchKpiMetrics()
164
+ }
165
+
130
166
  async pageInitialized(lifecycle: any) {
131
167
  this.gristConfig = {
132
168
  list: {
@@ -187,13 +223,6 @@ export class KpiListPage extends connect(store)(localize(i18next)(ScopedElements
187
223
  filter: 'search',
188
224
  width: 200
189
225
  },
190
- // {
191
- // type: 'string',
192
- // name: 'category',
193
- // header: '카테고리',
194
- // record: { editable: false, renderer: (v, c, r) => r.category?.name },
195
- // width: 120
196
- // },
197
226
  {
198
227
  type: 'resource-object',
199
228
  name: 'category',
@@ -208,7 +237,18 @@ export class KpiListPage extends connect(store)(localize(i18next)(ScopedElements
208
237
  },
209
238
  width: 200
210
239
  },
211
- { type: 'string', name: 'formula', header: '산식', record: { editable: true }, width: 200 },
240
+ {
241
+ type: 'formula',
242
+ name: 'formula',
243
+ header: '산식',
244
+ record: {
245
+ editable: true,
246
+ availableVariables: async () => {
247
+ return await this.getAvailableVariables()
248
+ }
249
+ },
250
+ width: 320
251
+ },
212
252
  {
213
253
  type: 'string',
214
254
  name: 'grades',
@@ -229,6 +269,15 @@ export class KpiListPage extends connect(store)(localize(i18next)(ScopedElements
229
269
  },
230
270
  width: 150
231
271
  },
272
+ {
273
+ type: 'number',
274
+ name: 'weight',
275
+ header: '가중치',
276
+ record: { editable: true },
277
+ filter: true,
278
+ sortable: true,
279
+ width: 80
280
+ },
232
281
  {
233
282
  type: 'checkbox',
234
283
  name: 'active',
@@ -264,9 +313,9 @@ export class KpiListPage extends connect(store)(localize(i18next)(ScopedElements
264
313
  },
265
314
  width: 150
266
315
  },
267
- { type: 'string', name: 'schedule', header: '스케줄', record: { editable: true }, width: 120 },
316
+ { type: 'crontab', name: 'schedule', header: '스케줄', record: { editable: true }, width: 120 },
268
317
  { type: 'string', name: 'scheduleId', header: '스케줄ID', record: { editable: false }, width: 120 },
269
- { type: 'string', name: 'timezone', header: '타임존', record: { editable: true }, width: 100 },
318
+ { type: 'timezone', name: 'timezone', header: '타임존', record: { editable: true }, width: 120 },
270
319
  { type: 'number', name: 'version', header: '버전', record: { editable: false }, width: 80 },
271
320
  { type: 'datetime', name: 'createdAt', header: '생성일', record: { editable: false }, width: 180 },
272
321
  {
@@ -323,6 +372,12 @@ export class KpiListPage extends connect(store)(localize(i18next)(ScopedElements
323
372
  grades
324
373
  vizType
325
374
  vizMeta
375
+ weight
376
+ formula
377
+ schedule
378
+ scheduleId
379
+ timezone
380
+ version
326
381
  category {
327
382
  id
328
383
  name
@@ -356,6 +411,31 @@ export class KpiListPage extends connect(store)(localize(i18next)(ScopedElements
356
411
  }
357
412
  }
358
413
 
414
+ async fetchKpiMetrics() {
415
+ const response = await client.query({
416
+ query: gql`
417
+ query {
418
+ kpiMetrics {
419
+ items {
420
+ name
421
+ description
422
+ unit
423
+ }
424
+ }
425
+ }
426
+ `
427
+ })
428
+
429
+ if (!response.errors) {
430
+ this.availableVariables = (response.data.kpiMetrics.items || []).map(metric => ({
431
+ name: metric.name,
432
+ description: metric.description,
433
+ type: 'kpi-metric',
434
+ unit: metric.unit
435
+ }))
436
+ }
437
+ }
438
+
359
439
  async _deleteKpi() {
360
440
  if (
361
441
  await OxPrompt.open({
@@ -503,6 +583,14 @@ export class KpiListPage extends connect(store)(localize(i18next)(ScopedElements
503
583
  }
504
584
 
505
585
  async _editGrades(kpi: any) {
586
+ if (!kpi.id) {
587
+ notify({
588
+ message: 'KPI를 먼저 저장한 후에 등급 설정을 할 수 있습니다.'
589
+ })
590
+
591
+ return
592
+ }
593
+
506
594
  const popup = await openPopup(html` <kpi-grade-editor .kpi=${kpi}></kpi-grade-editor> `, {
507
595
  title: `${kpi.name} - 등급 설정`,
508
596
  size: 'large'
@@ -194,11 +194,13 @@ export class KpiVizEditor extends localize(i18next)(LitElement) {
194
194
  }
195
195
 
196
196
  _renderPreview() {
197
- const kpiValue = this.kpi?.value?.value || 75
198
- const targetValue = this.kpi?.targetValue || 100
199
- const unit = this.kpi?.unit || ''
197
+ const kpiValue = this.kpi?.value?.value ?? 75
198
+ const targetValue = this.kpi?.targetValue ?? 100
199
+ const unit = this.kpi?.unit ?? ''
200
200
  const color = this.vizMeta.color || '#2196f3'
201
201
  const icon = this.vizMeta.icon || 'trending_up'
202
+ const min = this.vizMeta.minValue ?? 0
203
+ const max = this.vizMeta.maxValue ?? 100
202
204
 
203
205
  switch (this.selectedVizType) {
204
206
  case 'CARD':
@@ -213,22 +215,161 @@ export class KpiVizEditor extends localize(i18next)(LitElement) {
213
215
  </div>
214
216
  </div>
215
217
  `
216
- case 'GAUGE':
217
- const percentage = Math.min((kpiValue / targetValue) * 100, 100)
218
+ case 'GAUGE': {
219
+ const value = Math.max(min, Math.min(kpiValue, max))
220
+ const percent = max - min > 0 ? (value - min) / (max - min) : 0
221
+ const r = 60
222
+ const cx = 90
223
+ const cy = 90
224
+ const startX = cx - r
225
+ const startY = cy
226
+ const endX = cx + r * Math.cos(Math.PI * (1 - percent))
227
+ const endY = cy - r * Math.sin(Math.PI * (1 - percent))
228
+ const needleAngle = Math.PI - Math.PI * percent
229
+ const needleX = cx + r * Math.cos(needleAngle)
230
+ const needleY = cy - r * Math.sin(needleAngle)
218
231
  return html`
219
232
  <div style="text-align:center;padding:16px;">
220
- <div
221
- style="width:120px;height:60px;border-radius:60px 60px 0 0;background:conic-gradient(${color} 0deg ${percentage *
222
- 3.6}deg, #e0e0e0 ${percentage * 3.6}deg 360deg);margin:0 auto;position:relative;"
223
- >
224
- <div
225
- style="position:absolute;bottom:0;left:50%;transform:translateX(-50%);font-size:1.2rem;font-weight:bold;color:${color};"
233
+ <svg width="180" height="110" viewBox="0 0 180 110">
234
+ <!-- 배경 arc -->
235
+ <path
236
+ d="M${startX},${startY} A${r},${r} 0 0,1 ${cx + r},${cy}"
237
+ fill="none"
238
+ stroke="#e0e0e0"
239
+ stroke-width="16"
240
+ />
241
+ <!-- 값 arc -->
242
+ <path
243
+ d="M${startX},${startY} A${r},${r} 0 0,1 ${endX},${endY}"
244
+ fill="none"
245
+ stroke="${color}"
246
+ stroke-width="16"
247
+ />
248
+ <!-- 바늘 -->
249
+ <line x1="${cx}" y1="${cy}" x2="${needleX}" y2="${needleY}" stroke="#333" stroke-width="4" />
250
+ <!-- 중심 원 -->
251
+ <circle cx="${cx}" cy="${cy}" r="7" fill="#333" />
252
+ <!-- 중앙값 -->
253
+ <text x="${cx}" y="${cy - 25}" text-anchor="middle" font-size="22" fill="${color}" font-weight="bold">
254
+ ${value}${unit}
255
+ </text>
256
+ <!-- min/max -->
257
+ <text x="${cx - r}" y="${cy + 20}" text-anchor="middle" font-size="12" fill="#888">${min}</text>
258
+ <text x="${cx + r}" y="${cy + 20}" text-anchor="middle" font-size="12" fill="#888">${max}</text>
259
+ </svg>
260
+ </div>
261
+ `
262
+ }
263
+ case 'SPEEDOMETER': {
264
+ const value = Math.max(min, Math.min(kpiValue, max))
265
+ const percent = max - min > 0 ? (value - min) / (max - min) : 0
266
+ const r = 60
267
+ const cx = 90
268
+ const cy = 90
269
+ const startX = cx - r
270
+ const startY = cy
271
+ const endX = cx + r * Math.cos(Math.PI * (1 - percent))
272
+ const endY = cy - r * Math.sin(Math.PI * (1 - percent))
273
+ const needleAngle = Math.PI - Math.PI * percent
274
+ const needleX = cx + r * Math.cos(needleAngle)
275
+ const needleY = cy - r * Math.sin(needleAngle)
276
+ // 중간 눈금 (5개)
277
+ const ticks = Array.from({ length: 6 }, (_, i) => {
278
+ const tickAngle = Math.PI - (Math.PI * i) / 5
279
+ const tx1 = cx + (r - 8) * Math.cos(tickAngle)
280
+ const ty1 = cy - (r - 8) * Math.sin(tickAngle)
281
+ const tx2 = cx + (r + 8) * Math.cos(tickAngle)
282
+ const ty2 = cy - (r + 8) * Math.sin(tickAngle)
283
+ const label = Math.round(min + (max - min) * (i / 5))
284
+ const lx = cx + (r + 22) * Math.cos(tickAngle)
285
+ const ly = cy - (r + 22) * Math.sin(tickAngle) + 6
286
+ return { tx1, ty1, tx2, ty2, label, lx, ly }
287
+ })
288
+ return html`
289
+ <div style="text-align:center;padding:16px;">
290
+ <svg width="200" height="120" viewBox="0 0 200 120">
291
+ <!-- 배경 arc (더 두껍게) -->
292
+ <path
293
+ d="M${startX + 10},${startY} A${r},${r} 0 0,1 ${cx + r + 10},${cy}"
294
+ fill="none"
295
+ stroke="#e0e0e0"
296
+ stroke-width="28"
297
+ />
298
+ <!-- 값 arc -->
299
+ <path
300
+ d="M${startX + 10},${startY} A${r},${r} 0 0,1 ${endX + 10},${endY}"
301
+ fill="none"
302
+ stroke="${color}"
303
+ stroke-width="28"
304
+ />
305
+ <!-- 눈금 -->
306
+ ${ticks.map(
307
+ t =>
308
+ html`<line
309
+ x1="${t.tx1 + 10}"
310
+ y1="${t.ty1}"
311
+ x2="${t.tx2 + 10}"
312
+ y2="${t.ty2}"
313
+ stroke="#888"
314
+ stroke-width="2"
315
+ />`
316
+ )}
317
+ <!-- 눈금 숫자 -->
318
+ ${ticks.map(
319
+ t =>
320
+ html`<text
321
+ x="${t.lx + 10}"
322
+ y="${t.ly}"
323
+ text-anchor="middle"
324
+ font-size="14"
325
+ fill="#333"
326
+ font-weight="bold"
327
+ >${t.label}</text
328
+ >`
329
+ )}
330
+ <!-- 바늘 (빨간색) -->
331
+ <line x1="${cx + 10}" y1="${cy}" x2="${needleX + 10}" y2="${needleY}" stroke="#d32f2f" stroke-width="6" />
332
+ <!-- 중심 원 -->
333
+ <circle cx="${cx + 10}" cy="${cy}" r="13" fill="#333" />
334
+ <!-- 중앙값 -->
335
+ <text
336
+ x="${cx + 10}"
337
+ y="${cy - 32}"
338
+ text-anchor="middle"
339
+ font-size="26"
340
+ fill="${color}"
341
+ font-weight="bold"
226
342
  >
227
- ${kpiValue}${unit}
228
- </div>
229
- </div>
343
+ ${value}${unit}
344
+ </text>
345
+ <!-- min/max 포인트 -->
346
+ <circle cx="${startX + 10}" cy="${startY}" r="7" fill="#fff" stroke="#888" stroke-width="2" />
347
+ <circle cx="${cx + r + 10}" cy="${cy}" r="7" fill="#fff" stroke="#888" stroke-width="2" />
348
+ <!-- min/max 숫자 크게 -->
349
+ <text
350
+ x="${startX + 10}"
351
+ y="${startY + 32}"
352
+ text-anchor="middle"
353
+ font-size="16"
354
+ fill="#333"
355
+ font-weight="bold"
356
+ >
357
+ ${min}
358
+ </text>
359
+ <text
360
+ x="${cx + r + 10}"
361
+ y="${cy + 32}"
362
+ text-anchor="middle"
363
+ font-size="16"
364
+ fill="#333"
365
+ font-weight="bold"
366
+ >
367
+ ${max}
368
+ </text>
369
+ </svg>
230
370
  </div>
231
371
  `
372
+ }
232
373
  case 'PROGRESS':
233
374
  const progressPercentage = Math.min((kpiValue / targetValue) * 100, 100)
234
375
  return html`
@@ -241,6 +382,65 @@ export class KpiVizEditor extends localize(i18next)(LitElement) {
241
382
  </div>
242
383
  </div>
243
384
  `
385
+ case 'THERMOMETER': {
386
+ const value = Math.max(min, Math.min(kpiValue, max))
387
+ const percent = max - min > 0 ? (value - min) / (max - min) : 0
388
+ const barHeight = 120
389
+ const barWidth = 24
390
+ const x = 100
391
+ const yTop = 30
392
+ const yBottom = yTop + barHeight
393
+ const fillY = yBottom - percent * barHeight
394
+ return html`
395
+ <div style="text-align:center;padding:16px;">
396
+ <svg width="200" height="180" viewBox="0 0 200 180">
397
+ <!-- 바깥 테두리 -->
398
+ <rect
399
+ x="${x - barWidth / 2 - 4}"
400
+ y="${yTop - 4}"
401
+ width="${barWidth + 8}"
402
+ height="${barHeight + 8}"
403
+ rx="16"
404
+ fill="#f5f5f5"
405
+ stroke="#bbb"
406
+ stroke-width="2"
407
+ />
408
+ <!-- 빈 막대 -->
409
+ <rect
410
+ x="${x - barWidth / 2}"
411
+ y="${yTop}"
412
+ width="${barWidth}"
413
+ height="${barHeight}"
414
+ rx="12"
415
+ fill="#e0e0e0"
416
+ />
417
+ <!-- 채워진 부분 -->
418
+ <rect
419
+ x="${x - barWidth / 2}"
420
+ y="${fillY}"
421
+ width="${barWidth}"
422
+ height="${yBottom - fillY}"
423
+ rx="12"
424
+ fill="${color}"
425
+ />
426
+ <!-- 하단 구슬 -->
427
+ <circle cx="${x}" cy="${yBottom + 18}" r="22" fill="#e0e0e0" stroke="#bbb" stroke-width="2" />
428
+ <circle cx="${x}" cy="${yBottom + 18}" r="18" fill="${color}" />
429
+ <!-- 현재값 -->
430
+ <text x="${x}" y="${fillY - 12}" text-anchor="middle" font-size="22" fill="${color}" font-weight="bold">
431
+ ${value}${unit}
432
+ </text>
433
+ <!-- min/max -->
434
+ <text x="${x}" y="${yBottom + 52}" text-anchor="middle" font-size="16" fill="#333" font-weight="bold">
435
+ ${min}
436
+ </text>
437
+ <text x="${x}" y="${yTop - 12}" text-anchor="middle" font-size="16" fill="#333" font-weight="bold">
438
+ ${max}
439
+ </text>
440
+ </svg>
441
+ </div>
442
+ `
443
+ }
244
444
  case 'ICON':
245
445
  return html`
246
446
  <div style="text-align:center;padding:16px;">
@@ -7,7 +7,7 @@ import '@operato/data-grist/ox-record-creator.js'
7
7
  import { CommonButtonStyles, CommonHeaderStyles, CommonGristStyles, ScrollbarStyles } from '@operato/styles'
8
8
  import { PageView, store } from '@operato/shell'
9
9
  import { css, html } from 'lit'
10
- import { customElement, property, query } from 'lit/decorators.js'
10
+ import { customElement, property, query, state } from 'lit/decorators.js'
11
11
  import { ScopedElementsMixin } from '@open-wc/scoped-elements'
12
12
  import { ColumnConfig, DataGrist, FetchOption } from '@operato/data-grist'
13
13
  import { client } from '@operato/graphql'
@@ -59,6 +59,35 @@ export class KpiCategoryListPage extends connect(store)(localize(i18next)(Scoped
59
59
 
60
60
  @query('ox-grist') private grist!: DataGrist
61
61
 
62
+ @state() availableVariables: any[] = []
63
+ @state() availableVariablesLoaded = false
64
+
65
+ async getAvailableVariables() {
66
+ if (this.availableVariablesLoaded) {
67
+ return this.availableVariables
68
+ }
69
+ const response = await client.query({
70
+ query: gql`
71
+ query {
72
+ kpis {
73
+ items {
74
+ name
75
+ description
76
+ }
77
+ }
78
+ }
79
+ `
80
+ })
81
+ this.availableVariables = (response.data.kpis.items || []).map(kpi => ({
82
+ name: kpi.name,
83
+ description: kpi.description,
84
+ type: 'kpi',
85
+ unit: ''
86
+ }))
87
+ this.availableVariablesLoaded = true
88
+ return this.availableVariables
89
+ }
90
+
62
91
  get context() {
63
92
  return {
64
93
  title: i18next.t('title.kpi category list'),
@@ -122,11 +151,16 @@ export class KpiCategoryListPage extends connect(store)(localize(i18next)(Scoped
122
151
  `
123
152
  }
124
153
 
154
+ connectedCallback() {
155
+ super.connectedCallback()
156
+ this.fetchKpis()
157
+ }
158
+
125
159
  async pageInitialized(lifecycle: any) {
126
160
  this.gristConfig = {
127
161
  list: {
128
- fields: ['name', 'description', 'active', 'parent', 'createdAt', 'updatedAt', 'creator', 'updater'],
129
- details: ['name', 'description', 'active', 'parent', 'createdAt', 'updatedAt', 'creator', 'updater']
162
+ fields: ['name', 'description', 'active', 'formula', 'weight', 'createdAt', 'updatedAt', 'creator', 'updater'],
163
+ details: ['name', 'description', 'active', 'formula', 'weight', 'createdAt', 'updatedAt', 'creator', 'updater']
130
164
  },
131
165
  columns: [
132
166
  { type: 'gutter', gutterName: 'sequence' },
@@ -149,11 +183,23 @@ export class KpiCategoryListPage extends connect(store)(localize(i18next)(Scoped
149
183
  width: 200
150
184
  },
151
185
  {
152
- type: 'string',
153
- name: 'parent',
154
- header: '상위분류',
155
- record: { editable: false, renderer: (v, c, r) => r.parent?.name },
156
- width: 120
186
+ type: 'formula',
187
+ name: 'formula',
188
+ header: '산식',
189
+ record: {
190
+ editable: true,
191
+ availableVariables: async () => {
192
+ return await this.getAvailableVariables()
193
+ }
194
+ },
195
+ width: 320
196
+ },
197
+ {
198
+ type: 'number',
199
+ name: 'weight',
200
+ header: '가중치',
201
+ record: { editable: true },
202
+ width: 80
157
203
  },
158
204
  {
159
205
  type: 'checkbox',
@@ -216,6 +262,8 @@ export class KpiCategoryListPage extends connect(store)(localize(i18next)(Scoped
216
262
  name
217
263
  description
218
264
  active
265
+ formula
266
+ weight
219
267
  updater {
220
268
  id
221
269
  name
@@ -244,6 +292,30 @@ export class KpiCategoryListPage extends connect(store)(localize(i18next)(Scoped
244
292
  }
245
293
  }
246
294
 
295
+ async fetchKpis() {
296
+ const response = await client.query({
297
+ query: gql`
298
+ query {
299
+ kpis {
300
+ items {
301
+ name
302
+ description
303
+ }
304
+ }
305
+ }
306
+ `
307
+ })
308
+
309
+ if (!response.errors) {
310
+ this.availableVariables = (response.data.kpis.items || []).map(kpi => ({
311
+ name: kpi.name,
312
+ description: kpi.description,
313
+ type: 'kpi',
314
+ unit: ''
315
+ }))
316
+ }
317
+ }
318
+
247
319
  async _deleteKpiCategory() {
248
320
  if (
249
321
  await OxPrompt.open({
@@ -105,7 +105,7 @@ export class KpiHistoryListPage extends localize(i18next)(PageView) {
105
105
  record: { editable: false, renderer: (v, c, r) => r.category?.name },
106
106
  width: 120
107
107
  },
108
- { type: 'string', name: 'formula', header: '산식', record: { editable: false }, width: 200 },
108
+ { type: 'formula', name: 'formula', header: '산식', record: { editable: false }, width: 200 },
109
109
  { type: 'checkbox', name: 'active', label: true, header: '활성', record: { editable: false }, width: 60 },
110
110
  { type: 'string', name: 'state', header: '상태', record: { editable: false }, width: 100 },
111
111
  { type: 'datetime', name: 'createdAt', header: '생성일', record: { editable: false }, width: 180 },
@@ -6,8 +6,8 @@ import '@operato/data-grist/ox-record-creator.js'
6
6
 
7
7
  import { CommonButtonStyles, CommonHeaderStyles, CommonGristStyles, ScrollbarStyles } from '@operato/styles'
8
8
  import { PageView, store } from '@operato/shell'
9
- import { css, html } from 'lit'
10
- import { customElement, property, query } from 'lit/decorators.js'
9
+ import { css, html, PropertyValues } from 'lit'
10
+ import { customElement, property, query, state } from 'lit/decorators.js'
11
11
  import { ScopedElementsMixin } from '@open-wc/scoped-elements'
12
12
  import { ColumnConfig, DataGrist, FetchOption } from '@operato/data-grist'
13
13
  import { client } from '@operato/graphql'
@@ -132,7 +132,7 @@ export class KpiMetricListPage extends connect(store)(localize(i18next)(ScopedEl
132
132
  'source',
133
133
  'dataSet',
134
134
  'fieldName',
135
- 'formula',
135
+ 'periodType',
136
136
  'active',
137
137
  'schedule',
138
138
  'scheduleId',
@@ -149,7 +149,7 @@ export class KpiMetricListPage extends connect(store)(localize(i18next)(ScopedEl
149
149
  'source',
150
150
  'dataSet',
151
151
  'fieldName',
152
- 'formula',
152
+ 'periodType',
153
153
  'active',
154
154
  'schedule',
155
155
  'scheduleId',
@@ -183,11 +183,24 @@ export class KpiMetricListPage extends connect(store)(localize(i18next)(ScopedEl
183
183
  width: 120
184
184
  },
185
185
  { type: 'string', name: 'fieldName', header: '필드명', record: { editable: true }, width: 120 },
186
- { type: 'string', name: 'formula', header: '산식', record: { editable: true }, width: 200 },
186
+ {
187
+ type: 'select',
188
+ name: 'collectType',
189
+ header: '수집방법',
190
+ record: { editable: true, options: ['', 'AUTO', 'MANUAL', 'IMPORT', 'EXTERNAL'] },
191
+ width: 120
192
+ },
193
+ {
194
+ type: 'select',
195
+ name: 'periodType',
196
+ header: '주기',
197
+ record: { editable: true, options: ['', 'DAY', 'WEEK', 'MONTH', 'QUARTER', 'YEAR', 'RANGE'] },
198
+ width: 80
199
+ },
187
200
  { type: 'checkbox', name: 'active', label: true, header: '활성', record: { editable: true }, width: 60 },
188
- { type: 'string', name: 'schedule', header: '스케줄', record: { editable: true }, width: 120 },
201
+ { type: 'crontab', name: 'schedule', header: '스케줄', record: { editable: true }, width: 120 },
189
202
  { type: 'string', name: 'scheduleId', header: '스케줄ID', record: { editable: false }, width: 120 },
190
- { type: 'string', name: 'timezone', header: '타임존', record: { editable: true }, width: 100 },
203
+ { type: 'timezone', name: 'timezone', header: '타임존', record: { editable: true }, width: 120 },
191
204
  { type: 'datetime', name: 'createdAt', header: '생성일', record: { editable: false }, width: 180 },
192
205
  { type: 'datetime', name: 'updatedAt', header: '수정일', record: { editable: false }, width: 180 },
193
206
  {
@@ -230,7 +243,18 @@ export class KpiMetricListPage extends connect(store)(localize(i18next)(ScopedEl
230
243
  id
231
244
  name
232
245
  description
246
+ periodType
233
247
  active
248
+ unit
249
+ source
250
+ collectType
251
+ dataSet {
252
+ id
253
+ name
254
+ }
255
+ fieldName
256
+ schedule
257
+ timezone
234
258
  updater {
235
259
  id
236
260
  name