@things-factory/kpi 9.0.21 → 9.0.23

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 (152) hide show
  1. package/client/charts/kpi-boxplot-chart.ts +163 -0
  2. package/client/charts/kpi-radar-chart.ts +128 -0
  3. package/client/pages/kpi/kpi-list-page.ts +180 -22
  4. package/client/pages/kpi-category/kpi-category-list-page.ts +76 -3
  5. package/client/pages/kpi-category/kpi-category-value-calculator.ts +233 -0
  6. package/client/pages/kpi-dashboard/kpi-dashboard.ts +188 -0
  7. package/client/pages/kpi-metric/kpi-metric-list-page.ts +13 -1
  8. package/client/pages/kpi-metric-value/kpi-metric-value-list-page.ts +43 -1
  9. package/client/pages/kpi-metric-value/kpi-metric-value-manual-entry-form.ts +3 -13
  10. package/client/pages/kpi-metric-value/kpi-metric-value-manual-entry-page.ts +13 -1
  11. package/client/pages/kpi-value/kpi-value-list-page.ts +45 -1
  12. package/dist-client/charts/kpi-boxplot-chart.d.ts +22 -0
  13. package/dist-client/charts/kpi-boxplot-chart.js +198 -0
  14. package/dist-client/charts/kpi-boxplot-chart.js.map +1 -0
  15. package/dist-client/charts/kpi-radar-chart.d.ts +16 -0
  16. package/dist-client/charts/kpi-radar-chart.js +138 -0
  17. package/dist-client/charts/kpi-radar-chart.js.map +1 -0
  18. package/dist-client/pages/kpi/kpi-list-page.d.ts +2 -1
  19. package/dist-client/pages/kpi/kpi-list-page.js +180 -22
  20. package/dist-client/pages/kpi/kpi-list-page.js.map +1 -1
  21. package/dist-client/pages/kpi-category/kpi-category-list-page.d.ts +3 -0
  22. package/dist-client/pages/kpi-category/kpi-category-list-page.js +71 -3
  23. package/dist-client/pages/kpi-category/kpi-category-list-page.js.map +1 -1
  24. package/dist-client/pages/kpi-category/kpi-category-value-calculator.d.ts +13 -0
  25. package/dist-client/pages/kpi-category/kpi-category-value-calculator.js +256 -0
  26. package/dist-client/pages/kpi-category/kpi-category-value-calculator.js.map +1 -0
  27. package/dist-client/pages/kpi-dashboard/kpi-dashboard.d.ts +11 -0
  28. package/dist-client/pages/kpi-dashboard/kpi-dashboard.js +185 -0
  29. package/dist-client/pages/kpi-dashboard/kpi-dashboard.js.map +1 -1
  30. package/dist-client/pages/kpi-metric/kpi-metric-list-page.js +13 -1
  31. package/dist-client/pages/kpi-metric/kpi-metric-list-page.js.map +1 -1
  32. package/dist-client/pages/kpi-metric-value/kpi-metric-value-list-page.d.ts +4 -1
  33. package/dist-client/pages/kpi-metric-value/kpi-metric-value-list-page.js +39 -2
  34. package/dist-client/pages/kpi-metric-value/kpi-metric-value-list-page.js.map +1 -1
  35. package/dist-client/pages/kpi-metric-value/kpi-metric-value-manual-entry-form.js +3 -13
  36. package/dist-client/pages/kpi-metric-value/kpi-metric-value-manual-entry-form.js.map +1 -1
  37. package/dist-client/pages/kpi-metric-value/kpi-metric-value-manual-entry-page.js +13 -1
  38. package/dist-client/pages/kpi-metric-value/kpi-metric-value-manual-entry-page.js.map +1 -1
  39. package/dist-client/pages/kpi-value/kpi-value-list-page.d.ts +1 -0
  40. package/dist-client/pages/kpi-value/kpi-value-list-page.js +45 -1
  41. package/dist-client/pages/kpi-value/kpi-value-list-page.js.map +1 -1
  42. package/dist-client/tsconfig.tsbuildinfo +1 -1
  43. package/dist-server/calculator/evaluator.d.ts +8 -0
  44. package/dist-server/calculator/evaluator.js +42 -0
  45. package/dist-server/calculator/evaluator.js.map +1 -0
  46. package/dist-server/calculator/functions.d.ts +3 -0
  47. package/dist-server/calculator/functions.js +62 -0
  48. package/dist-server/calculator/functions.js.map +1 -0
  49. package/dist-server/calculator/index.d.ts +4 -0
  50. package/dist-server/calculator/index.js +8 -0
  51. package/dist-server/calculator/index.js.map +1 -0
  52. package/dist-server/calculator/parser.d.ts +21 -0
  53. package/dist-server/calculator/parser.js +121 -0
  54. package/dist-server/calculator/parser.js.map +1 -0
  55. package/dist-server/calculator/provider.d.ts +8 -0
  56. package/dist-server/calculator/provider.js +13 -0
  57. package/dist-server/calculator/provider.js.map +1 -0
  58. package/dist-server/controllers/kpi-metric-value-provider.d.ts +11 -0
  59. package/dist-server/controllers/kpi-metric-value-provider.js +63 -0
  60. package/dist-server/controllers/kpi-metric-value-provider.js.map +1 -0
  61. package/dist-server/controllers/kpi-value-provider.d.ts +11 -0
  62. package/dist-server/controllers/kpi-value-provider.js +46 -0
  63. package/dist-server/controllers/kpi-value-provider.js.map +1 -0
  64. package/dist-server/service/index.d.ts +2 -2
  65. package/dist-server/service/kpi/aggregate-kpi.js +4 -4
  66. package/dist-server/service/kpi/aggregate-kpi.js.map +1 -1
  67. package/dist-server/service/kpi/kpi-grade.types.d.ts +11 -10
  68. package/dist-server/service/kpi/kpi-grade.types.js.map +1 -1
  69. package/dist-server/service/kpi/kpi-history.d.ts +2 -2
  70. package/dist-server/service/kpi/kpi-history.js.map +1 -1
  71. package/dist-server/service/kpi/kpi-mutation.d.ts +2 -0
  72. package/dist-server/service/kpi/kpi-mutation.js +126 -4
  73. package/dist-server/service/kpi/kpi-mutation.js.map +1 -1
  74. package/dist-server/service/kpi/kpi-type.d.ts +8 -5
  75. package/dist-server/service/kpi/kpi-type.js +22 -8
  76. package/dist-server/service/kpi/kpi-type.js.map +1 -1
  77. package/dist-server/service/kpi/kpi.d.ts +6 -3
  78. package/dist-server/service/kpi/kpi.js +29 -9
  79. package/dist-server/service/kpi/kpi.js.map +1 -1
  80. package/dist-server/service/kpi-category/kpi-category-mutation.d.ts +1 -1
  81. package/dist-server/service/kpi-category/kpi-category-mutation.js +3 -3
  82. package/dist-server/service/kpi-category/kpi-category-mutation.js.map +1 -1
  83. package/dist-server/service/kpi-category/kpi-category-query.d.ts +13 -0
  84. package/dist-server/service/kpi-category/kpi-category-query.js +180 -0
  85. package/dist-server/service/kpi-category/kpi-category-query.js.map +1 -1
  86. package/dist-server/service/kpi-category/kpi-category-type.d.ts +3 -0
  87. package/dist-server/service/kpi-category/kpi-category-type.js +16 -1
  88. package/dist-server/service/kpi-category/kpi-category-type.js.map +1 -1
  89. package/dist-server/service/kpi-category/kpi-category.d.ts +2 -0
  90. package/dist-server/service/kpi-category/kpi-category.js +10 -1
  91. package/dist-server/service/kpi-category/kpi-category.js.map +1 -1
  92. package/dist-server/service/kpi-metric/kpi-metric-type.d.ts +5 -3
  93. package/dist-server/service/kpi-metric/kpi-metric-type.js +5 -3
  94. package/dist-server/service/kpi-metric/kpi-metric-type.js.map +1 -1
  95. package/dist-server/service/kpi-metric/kpi-metric.d.ts +2 -8
  96. package/dist-server/service/kpi-metric/kpi-metric.js +3 -14
  97. package/dist-server/service/kpi-metric/kpi-metric.js.map +1 -1
  98. package/dist-server/service/kpi-metric-value/kpi-metric-value-mutation.js +67 -45
  99. package/dist-server/service/kpi-metric-value/kpi-metric-value-mutation.js.map +1 -1
  100. package/dist-server/service/kpi-metric-value/kpi-metric-value.js +3 -2
  101. package/dist-server/service/kpi-metric-value/kpi-metric-value.js.map +1 -1
  102. package/dist-server/service/kpi-value/kpi-value-mutation.d.ts +2 -1
  103. package/dist-server/service/kpi-value/kpi-value-mutation.js +114 -6
  104. package/dist-server/service/kpi-value/kpi-value-mutation.js.map +1 -1
  105. package/dist-server/service/kpi-value/kpi-value-query.d.ts +0 -2
  106. package/dist-server/service/kpi-value/kpi-value-query.js +0 -12
  107. package/dist-server/service/kpi-value/kpi-value-query.js.map +1 -1
  108. package/dist-server/service/kpi-value/kpi-value-score.service.d.ts +26 -0
  109. package/dist-server/service/kpi-value/kpi-value-score.service.js +97 -0
  110. package/dist-server/service/kpi-value/kpi-value-score.service.js.map +1 -0
  111. package/dist-server/service/kpi-value/kpi-value-type.d.ts +2 -0
  112. package/dist-server/service/kpi-value/kpi-value-type.js +14 -0
  113. package/dist-server/service/kpi-value/kpi-value-type.js.map +1 -1
  114. package/dist-server/service/kpi-value/kpi-value.d.ts +1 -0
  115. package/dist-server/service/kpi-value/kpi-value.js +9 -1
  116. package/dist-server/service/kpi-value/kpi-value.js.map +1 -1
  117. package/dist-server/service/utils/value-date-util.d.ts +3 -0
  118. package/dist-server/service/utils/value-date-util.js +76 -0
  119. package/dist-server/service/utils/value-date-util.js.map +1 -0
  120. package/dist-server/tsconfig.tsbuildinfo +1 -1
  121. package/package.json +2 -2
  122. package/server/calculator/evaluator.ts +45 -0
  123. package/server/calculator/functions.ts +67 -0
  124. package/server/calculator/index.ts +4 -0
  125. package/server/calculator/parser.ts +128 -0
  126. package/server/calculator/provider.ts +10 -0
  127. package/server/controllers/kpi-metric-value-provider.ts +66 -0
  128. package/server/controllers/kpi-value-provider.ts +51 -0
  129. package/server/service/kpi/aggregate-kpi.ts +4 -4
  130. package/server/service/kpi/kpi-grade.types.ts +11 -10
  131. package/server/service/kpi/kpi-history.ts +2 -2
  132. package/server/service/kpi/kpi-mutation.ts +128 -4
  133. package/server/service/kpi/kpi-type.ts +21 -9
  134. package/server/service/kpi/kpi.ts +32 -10
  135. package/server/service/kpi-category/kpi-category-mutation.ts +3 -3
  136. package/server/service/kpi-category/kpi-category-query.ts +175 -1
  137. package/server/service/kpi-category/kpi-category-type.ts +17 -6
  138. package/server/service/kpi-category/kpi-category.ts +10 -1
  139. package/server/service/kpi-metric/kpi-metric-type.ts +7 -5
  140. package/server/service/kpi-metric/kpi-metric.ts +3 -15
  141. package/server/service/kpi-metric-value/kpi-metric-value-mutation.ts +67 -47
  142. package/server/service/kpi-metric-value/kpi-metric-value.ts +4 -2
  143. package/server/service/kpi-value/kpi-value-mutation.ts +110 -6
  144. package/server/service/kpi-value/kpi-value-query.ts +2 -8
  145. package/server/service/kpi-value/kpi-value-score.service.ts +112 -0
  146. package/server/service/kpi-value/kpi-value-type.ts +12 -0
  147. package/server/service/kpi-value/kpi-value.ts +8 -1
  148. package/server/service/utils/value-date-util.ts +72 -0
  149. package/dist-server/service/kpi-value/kpi-value-grade.service.d.ts +0 -34
  150. package/dist-server/service/kpi-value/kpi-value-grade.service.js +0 -117
  151. package/dist-server/service/kpi-value/kpi-value-grade.service.js.map +0 -1
  152. package/server/service/kpi-value/kpi-value-grade.service.ts +0 -127
@@ -0,0 +1,163 @@
1
+ import { LitElement, html, css } from 'lit'
2
+ import { customElement, property } from 'lit/decorators.js'
3
+ import * as d3 from 'd3'
4
+
5
+ @customElement('kpi-boxplot-chart')
6
+ export class KpiBoxplotChart extends LitElement {
7
+ @property({ type: Array }) data: any[] = []
8
+ @property({ type: Array }) groups: string[] = []
9
+ @property({ type: String }) minKey: string = 'min'
10
+ @property({ type: String }) maxKey: string = 'max'
11
+ @property({ type: String }) meanKey: string = 'mean'
12
+ @property({ type: String }) medianKey: string = 'median'
13
+ @property({ type: String }) q1Key: string = 'q1'
14
+ @property({ type: String }) q3Key: string = 'q3'
15
+ @property({ type: String }) valueKey: string = 'value'
16
+ @property({ type: String }) currentGroup: string = ''
17
+
18
+ static styles = css`
19
+ :host {
20
+ display: block;
21
+ width: 100%;
22
+ height: 100%;
23
+ }
24
+ svg {
25
+ width: 100%;
26
+ height: 100%;
27
+ display: block;
28
+ }
29
+ `
30
+
31
+ private chartWidth = 0
32
+ private chartHeight = 0
33
+ private resizeObserver?: ResizeObserver
34
+
35
+ render() {
36
+ return html`<svg
37
+ id="boxplot"
38
+ width=${this.chartWidth}
39
+ height=${this.chartHeight}
40
+ viewBox="0 0 ${this.chartWidth} ${this.chartHeight}"
41
+ preserveAspectRatio="xMidYMid meet"
42
+ ></svg>`
43
+ }
44
+
45
+ connectedCallback() {
46
+ super.connectedCallback()
47
+ this.resizeObserver = new ResizeObserver(entries => {
48
+ for (const entry of entries) {
49
+ const rect = entry.contentRect
50
+ this.chartWidth = rect.width
51
+ this.chartHeight = rect.height
52
+ this.requestUpdate()
53
+ }
54
+ })
55
+ this.resizeObserver.observe(this)
56
+ }
57
+
58
+ disconnectedCallback() {
59
+ this.resizeObserver?.disconnect()
60
+ super.disconnectedCallback()
61
+ }
62
+
63
+ updated() {
64
+ this.drawBoxplot()
65
+ }
66
+
67
+ drawBoxplot() {
68
+ const svg = d3.select(this.renderRoot.querySelector('#boxplot'))
69
+ svg.selectAll('*').remove()
70
+ const w = this.chartWidth || 300
71
+ const h = this.chartHeight || 300
72
+ const margin = { top: 20, right: 20, bottom: 40, left: 40 }
73
+ const plotW = w - margin.left - margin.right
74
+ const plotH = h - margin.top - margin.bottom
75
+
76
+ // x축: 그룹
77
+ const x = d3.scaleBand().domain(this.groups).range([0, plotW]).padding(0.4)
78
+ // y축: 값
79
+ const allValues = this.data.flatMap(d => [
80
+ d[this.minKey],
81
+ d[this.maxKey],
82
+ d[this.q1Key],
83
+ d[this.q3Key],
84
+ d[this.medianKey],
85
+ d[this.meanKey],
86
+ d[this.valueKey]
87
+ ])
88
+ const y = d3
89
+ .scaleLinear()
90
+ .domain([d3.min(allValues) ?? 0, d3.max(allValues) ?? 1])
91
+ .nice()
92
+ .range([plotH, 0])
93
+
94
+ const g = svg
95
+ .attr('width', w)
96
+ .attr('height', h)
97
+ .append('g')
98
+ .attr('transform', `translate(${margin.left},${margin.top})`)
99
+
100
+ // 축
101
+ g.append('g').call(d3.axisLeft(y))
102
+ g.append('g').attr('transform', `translate(0,${plotH})`).call(d3.axisBottom(x))
103
+
104
+ // 박스플롯
105
+ this.data.forEach(d => {
106
+ const gx = x(d.group) ?? 0
107
+ // 박스
108
+ g.append('rect')
109
+ .attr('x', gx)
110
+ .attr('y', y(d[this.q3Key]))
111
+ .attr('width', x.bandwidth())
112
+ .attr('height', y(d[this.q1Key]) - y(d[this.q3Key]))
113
+ .attr('fill', d.group === this.currentGroup ? '#2196f3' : '#bbb')
114
+ .attr('opacity', 0.5)
115
+ // 중앙선(중앙값)
116
+ g.append('line')
117
+ .attr('x1', gx)
118
+ .attr('x2', gx + x.bandwidth())
119
+ .attr('y1', y(d[this.medianKey]))
120
+ .attr('y2', y(d[this.medianKey]))
121
+ .attr('stroke', '#333')
122
+ .attr('stroke-width', 2)
123
+ // 수염(min-max)
124
+ g.append('line')
125
+ .attr('x1', gx + x.bandwidth() / 2)
126
+ .attr('x2', gx + x.bandwidth() / 2)
127
+ .attr('y1', y(d[this.minKey]))
128
+ .attr('y2', y(d[this.maxKey]))
129
+ .attr('stroke', '#333')
130
+ // min/max
131
+ g.append('line')
132
+ .attr('x1', gx + x.bandwidth() / 4)
133
+ .attr('x2', gx + (x.bandwidth() * 3) / 4)
134
+ .attr('y1', y(d[this.minKey]))
135
+ .attr('y2', y(d[this.minKey]))
136
+ .attr('stroke', '#333')
137
+ g.append('line')
138
+ .attr('x1', gx + x.bandwidth() / 4)
139
+ .attr('x2', gx + (x.bandwidth() * 3) / 4)
140
+ .attr('y1', y(d[this.maxKey]))
141
+ .attr('y2', y(d[this.maxKey]))
142
+ .attr('stroke', '#333')
143
+ // 평균값
144
+ g.append('circle')
145
+ .attr('cx', gx + x.bandwidth() / 2)
146
+ .attr('cy', y(d[this.meanKey]))
147
+ .attr('r', 4)
148
+ .attr('fill', 'orange')
149
+ })
150
+ // 현재 그룹 값 강조
151
+ this.data.forEach(d => {
152
+ if (d.group === this.currentGroup) {
153
+ g.append('circle')
154
+ .attr('cx', x(d.group) + x.bandwidth() / 2)
155
+ .attr('cy', y(d[this.valueKey]))
156
+ .attr('r', 6)
157
+ .attr('fill', '#e91e63')
158
+ .attr('stroke', '#fff')
159
+ .attr('stroke-width', 2)
160
+ }
161
+ })
162
+ }
163
+ }
@@ -0,0 +1,128 @@
1
+ import { LitElement, html, css } from 'lit'
2
+ import { customElement, property } from 'lit/decorators.js'
3
+ import * as d3 from 'd3'
4
+
5
+ @customElement('kpi-radar-chart')
6
+ export class KpiRadarChart extends LitElement {
7
+ @property({ type: Array }) data: any[] = []
8
+ @property({ type: Array }) categories: string[] = []
9
+ @property({ type: String }) valueKey: string = 'value'
10
+ @property({ type: String }) currentGroup: string = ''
11
+
12
+ static styles = css`
13
+ :host {
14
+ display: block;
15
+ width: 100%;
16
+ height: 100%;
17
+ }
18
+ svg {
19
+ width: 100%;
20
+ height: 100%;
21
+ display: block;
22
+ }
23
+ `
24
+
25
+ private chartWidth = 0
26
+ private chartHeight = 0
27
+ private resizeObserver?: ResizeObserver
28
+
29
+ render() {
30
+ return html`<svg
31
+ id="radar"
32
+ width=${this.chartWidth}
33
+ height=${this.chartHeight}
34
+ viewBox="0 0 ${this.chartWidth} ${this.chartHeight}"
35
+ preserveAspectRatio="xMidYMid meet"
36
+ ></svg>`
37
+ }
38
+
39
+ connectedCallback() {
40
+ super.connectedCallback()
41
+ this.resizeObserver = new ResizeObserver(entries => {
42
+ for (const entry of entries) {
43
+ const rect = entry.contentRect
44
+ this.chartWidth = rect.width
45
+ this.chartHeight = rect.height
46
+ this.requestUpdate()
47
+ }
48
+ })
49
+ this.resizeObserver.observe(this)
50
+ }
51
+
52
+ disconnectedCallback() {
53
+ this.resizeObserver?.disconnect()
54
+ super.disconnectedCallback()
55
+ }
56
+
57
+ updated() {
58
+ this.drawRadar()
59
+ }
60
+
61
+ drawRadar() {
62
+ const svg = d3.select(this.renderRoot.querySelector('#radar'))
63
+ svg.selectAll('*').remove()
64
+ const w = this.chartWidth || 300
65
+ const h = this.chartHeight || 300
66
+ const r = Math.min(w, h) / 2 - 40
67
+ const angleSlice = (2 * Math.PI) / (this.categories.length || 1)
68
+
69
+ // 데이터 변환: { group, values: [ {category, value} ... ] }
70
+ const groupData = d3
71
+ .groups(this.data, d => d.group)
72
+ .map(([group, values]) => ({
73
+ group,
74
+ values: this.categories.map((cat, i) => {
75
+ const found = values.find(v => v.category === cat)
76
+ return { category: cat, value: found ? found[this.valueKey] : 0 }
77
+ })
78
+ }))
79
+
80
+ // 스케일
81
+ const maxValue = d3.max(this.data, d => d[this.valueKey]) || 1
82
+ const radius = d3.scaleLinear().domain([0, maxValue]).range([0, r])
83
+
84
+ // SVG 기본
85
+ svg.attr('width', w).attr('height', h)
86
+ const g = svg.append('g').attr('transform', `translate(${w / 2},${h / 2})`)
87
+
88
+ // 그리드/축
89
+ for (let i = 1; i <= 5; i++) {
90
+ g.append('circle')
91
+ .attr('r', (r / 5) * i)
92
+ .attr('fill', 'none')
93
+ .attr('stroke', '#ccc')
94
+ }
95
+ this.categories.forEach((cat, i) => {
96
+ const angle = i * angleSlice - Math.PI / 2
97
+ g.append('line')
98
+ .attr('x1', 0)
99
+ .attr('y1', 0)
100
+ .attr('x2', radius(maxValue) * Math.cos(angle))
101
+ .attr('y2', radius(maxValue) * Math.sin(angle))
102
+ .attr('stroke', '#ccc')
103
+ g.append('text')
104
+ .attr('x', (radius(maxValue) + 10) * Math.cos(angle))
105
+ .attr('y', (radius(maxValue) + 10) * Math.sin(angle))
106
+ .attr('text-anchor', 'middle')
107
+ .attr('alignment-baseline', 'middle')
108
+ .attr('font-size', 12)
109
+ .text(cat)
110
+ })
111
+
112
+ // 그룹별 폴리곤
113
+ groupData.forEach(gd => {
114
+ // 마지막에 첫 점을 한 번 더 추가
115
+ const closedValues = [...gd.values, gd.values[0]]
116
+ const line = d3
117
+ .lineRadial()
118
+ .radius((d: any) => radius(d.value))
119
+ .angle((d, i) => i * angleSlice)
120
+ g.append('path')
121
+ .datum(closedValues)
122
+ .attr('d', line as any)
123
+ .attr('fill', gd.group === this.currentGroup ? 'rgba(33,150,243,0.4)' : 'rgba(200,200,200,0.2)')
124
+ .attr('stroke', gd.group === this.currentGroup ? '#2196f3' : '#aaa')
125
+ .attr('stroke-width', gd.group === this.currentGroup ? 3 : 1)
126
+ })
127
+ }
128
+ }
@@ -68,7 +68,7 @@ export class KpiListPage extends connect(store)(localize(i18next)(ScopedElements
68
68
  @state() availableVariables: any[] = []
69
69
  @state() availableVariablesLoaded = false
70
70
 
71
- async getAvailableVariables() {
71
+ async getAvailableKpiMetricVariables() {
72
72
  if (this.availableVariablesLoaded) {
73
73
  return this.availableVariables
74
74
  }
@@ -166,24 +166,7 @@ export class KpiListPage extends connect(store)(localize(i18next)(ScopedElements
166
166
  async pageInitialized(lifecycle: any) {
167
167
  this.gristConfig = {
168
168
  list: {
169
- fields: [
170
- 'name',
171
- 'description',
172
- 'category',
173
- 'formula',
174
- 'active',
175
- 'state',
176
- 'vizType',
177
- 'schedule',
178
- 'scheduleId',
179
- 'timezone',
180
- 'version',
181
- 'createdAt',
182
- 'updatedAt',
183
- 'creator',
184
- 'updater',
185
- 'thumbnail'
186
- ],
169
+ fields: ['name', 'description'],
187
170
  details: [
188
171
  'name',
189
172
  'description',
@@ -206,6 +189,18 @@ export class KpiListPage extends connect(store)(localize(i18next)(ScopedElements
206
189
  columns: [
207
190
  { type: 'gutter', gutterName: 'sequence' },
208
191
  { type: 'gutter', gutterName: 'row-selector', multiple: true },
192
+ // KPI 실적값 계산 버튼 추가
193
+ {
194
+ type: 'gutter',
195
+ gutterName: 'button',
196
+ icon: 'calculate',
197
+ title: '실적값 계산',
198
+ handlers: {
199
+ click: (columns, data, column, record, rowIndex) => {
200
+ this._calculateKpiValue(record)
201
+ }
202
+ }
203
+ },
209
204
  {
210
205
  type: 'string',
211
206
  name: 'name',
@@ -244,15 +239,150 @@ export class KpiListPage extends connect(store)(localize(i18next)(ScopedElements
244
239
  record: {
245
240
  editable: true,
246
241
  availableVariables: async () => {
247
- return await this.getAvailableVariables()
242
+ return await this.getAvailableKpiMetricVariables()
248
243
  }
249
244
  },
250
245
  width: 320
251
246
  },
247
+ {
248
+ type: 'select',
249
+ name: 'periodType',
250
+ header: '계산주기',
251
+ record: {
252
+ editable: true,
253
+ options: [
254
+ { value: '', display: '' },
255
+ { value: 'DAY', display: '일' },
256
+ { value: 'WEEK', display: '주' },
257
+ { value: 'MONTH', display: '월' },
258
+ { value: 'QUARTER', display: '분기' },
259
+ { value: 'YEAR', display: '년' },
260
+ { value: 'RANGE', display: '범위' },
261
+ { value: 'ALLTIME', display: '전체' }
262
+ ]
263
+ },
264
+ width: 80
265
+ },
266
+ {
267
+ type: 'formula',
268
+ name: 'scoreFormula',
269
+ header: '성과점수수식',
270
+ record: {
271
+ editable: true,
272
+ availableVariables: async () => {
273
+ return [
274
+ {
275
+ name: 'value',
276
+ description: 'KPI 실적값',
277
+ type: 'kpi-value',
278
+ unit: ''
279
+ }
280
+ ]
281
+ },
282
+ includeDefaultFunctions: false,
283
+ availableFunctions: [
284
+ {
285
+ name: 'INTEGRATE()',
286
+ description: '수치 적분',
287
+ template: 'INTEGRATE({func}, {a}, {b}, {n})',
288
+ syntax: 'INTEGRATE(func, a, b, n)',
289
+ parameters: ['func - 적분할 함수', 'a - 하한값', 'b - 상한값', 'n - 분할 수 (기본값: 1000)'],
290
+ returnType: 'number',
291
+ help: '사다리꼴 적분법을 사용하여 수치 적분을 계산합니다.',
292
+ examples: ['INTEGRATE(x => x*x, 0, 1, 1000)', 'INTEGRATE([효율성], 0, 1)']
293
+ },
294
+ {
295
+ name: 'BETA_FUNCTION()',
296
+ description: '베타 함수',
297
+ template: 'BETA_FUNCTION({x}, {alpha}, {beta})',
298
+ syntax: 'BETA_FUNCTION(x, alpha, beta)',
299
+ parameters: ['x - 변수 (0 ≤ x ≤ 1)', 'alpha - 첫 번째 모수', 'beta - 두 번째 모수'],
300
+ returnType: 'number',
301
+ help: '베타 분포 함수를 계산합니다. t^(α-1) × (1-t)^(β-1)',
302
+ examples: ['BETA_FUNCTION(0.5, 2, 3)', 'BETA_FUNCTION([품질지수], 3, 2)']
303
+ },
304
+ {
305
+ name: 'INCOMPLETE_BETA()',
306
+ description: '불완전 베타 함수',
307
+ template: 'INCOMPLETE_BETA({x}, {alpha}, {beta})',
308
+ syntax: 'INCOMPLETE_BETA(x, alpha, beta)',
309
+ parameters: ['x - 상한값 (0 ≤ x ≤ 1)', 'alpha - 첫 번째 모수', 'beta - 두 번째 모수'],
310
+ returnType: 'number',
311
+ help: '불완전 베타 함수를 수치 적분으로 계산합니다.',
312
+ examples: ['INCOMPLETE_BETA(0.7, 2, 3)', 'INCOMPLETE_BETA([목표달성률]/100, 2, 3)']
313
+ },
314
+ {
315
+ name: 'COMPLETE_BETA()',
316
+ description: '완전 베타 함수',
317
+ template: 'COMPLETE_BETA({alpha}, {beta})',
318
+ syntax: 'COMPLETE_BETA(alpha, beta)',
319
+ parameters: ['alpha - 첫 번째 모수', 'beta - 두 번째 모수'],
320
+ returnType: 'number',
321
+ help: '완전 베타 함수 B(α,β)를 계산합니다.',
322
+ examples: ['COMPLETE_BETA(2, 3)', 'COMPLETE_BETA(3, 2)']
323
+ },
324
+ {
325
+ name: 'PERFORMANCE_INDEX()',
326
+ description: '성과 지수',
327
+ template: 'PERFORMANCE_INDEX({x}, {alpha1}, {beta1}, {alpha2}, {beta2})',
328
+ syntax: 'PERFORMANCE_INDEX(x, alpha1, beta1, alpha2, beta2)',
329
+ parameters: [
330
+ 'x - 성과 값 (0 ≤ x ≤ 1)',
331
+ 'alpha1, beta1 - 분자 베타 함수 모수',
332
+ 'alpha2, beta2 - 분모 베타 함수 모수'
333
+ ],
334
+ returnType: 'number',
335
+ help: '성과 지수를 계산합니다: 1 - (불완전 베타 / 완전 베타)',
336
+ examples: ['PERFORMANCE_INDEX(0.8, 2, 3, 2, 3)', 'PERFORMANCE_INDEX([성과점수]/100, 2, 3, 2, 3)']
337
+ },
338
+ {
339
+ name: 'EXP()',
340
+ description: '지수 함수',
341
+ template: 'EXP({x})',
342
+ syntax: 'EXP(x)',
343
+ parameters: ['x - 지수'],
344
+ returnType: 'number',
345
+ help: '자연상수 e의 x제곱을 계산합니다.',
346
+ examples: ['EXP(1)', 'EXP([효율성])']
347
+ },
348
+ {
349
+ name: 'LOG()',
350
+ description: '자연 로그',
351
+ template: 'LOG({x})',
352
+ syntax: 'LOG(x)',
353
+ parameters: ['x - 로그를 취할 값'],
354
+ returnType: 'number',
355
+ help: '자연 로그 ln(x)를 계산합니다.',
356
+ examples: ['LOG(2.718)', 'LOG([성과점수])']
357
+ },
358
+ {
359
+ name: 'POW()',
360
+ description: '거듭제곱',
361
+ template: 'POW({x}, {y})',
362
+ syntax: 'POW(x, y)',
363
+ parameters: ['x - 밑수', 'y - 지수'],
364
+ returnType: 'number',
365
+ help: 'x의 y제곱을 계산합니다.',
366
+ examples: ['POW(2, 3)', 'POW([효율성], 2)']
367
+ },
368
+ {
369
+ name: 'EXPONENTIAL_DECAY()',
370
+ description: '지수 감쇠',
371
+ template: 'EXPONENTIAL_DECAY({value}, {scale}, {power})',
372
+ syntax: 'EXPONENTIAL_DECAY(value, scale, power)',
373
+ parameters: ['value - 입력 값', 'scale - 스케일 파라미터', 'power - 지수 파라미터'],
374
+ returnType: 'number',
375
+ help: '지수 감쇠 함수 exp(-(value/scale)^power)를 계산합니다.',
376
+ examples: ['EXPONENTIAL_DECAY(50, 100, 2)', 'EXPONENTIAL_DECAY([목표달성률], 50, 2)']
377
+ }
378
+ ]
379
+ },
380
+ width: 200
381
+ },
252
382
  {
253
383
  type: 'string',
254
384
  name: 'grades',
255
- header: '등급 설정',
385
+ header: '성과지수 Lookup',
256
386
  record: {
257
387
  editable: false,
258
388
  renderer: (v, c, r) => {
@@ -369,11 +499,13 @@ export class KpiListPage extends connect(store)(localize(i18next)(ScopedElements
369
499
  name
370
500
  description
371
501
  active
502
+ formula
503
+ periodType
504
+ scoreFormula
372
505
  grades
373
506
  vizType
374
507
  vizMeta
375
508
  weight
376
- formula
377
509
  schedule
378
510
  scheduleId
379
511
  timezone
@@ -648,4 +780,30 @@ export class KpiListPage extends connect(store)(localize(i18next)(ScopedElements
648
780
  })
649
781
  }
650
782
  }
783
+
784
+ async _calculateKpiValue(kpi) {
785
+ try {
786
+ const response = await client.mutate({
787
+ mutation: gql`
788
+ mutation ($kpiId: String!) {
789
+ calculateKpiValue(kpiId: $kpiId) {
790
+ id
791
+ value
792
+ valueDate
793
+ group
794
+ }
795
+ }
796
+ `,
797
+ variables: {
798
+ kpiId: kpi.id
799
+ }
800
+ })
801
+ if (!response.errors) {
802
+ notify({ message: 'KPI 실적값이 성공적으로 계산되었습니다.' })
803
+ this.grist.fetch()
804
+ }
805
+ } catch (error) {
806
+ notify({ message: 'KPI 실적값 계산 중 오류가 발생했습니다.' })
807
+ }
808
+ }
651
809
  }
@@ -20,6 +20,7 @@ import { connect } from 'pwa-helpers/connect-mixin'
20
20
  import gql from 'graphql-tag'
21
21
 
22
22
  import { KpiCategoryImporter } from './kpi-category-importer'
23
+ import { KpiCategoryValueCalculator } from './kpi-category-value-calculator'
23
24
 
24
25
  @customElement('kpi-category-list-page')
25
26
  export class KpiCategoryListPage extends connect(store)(localize(i18next)(ScopedElementsMixin(PageView))) {
@@ -50,7 +51,8 @@ export class KpiCategoryListPage extends connect(store)(localize(i18next)(Scoped
50
51
 
51
52
  static get scopedElements() {
52
53
  return {
53
- 'kpi-category-importer': KpiCategoryImporter
54
+ 'kpi-category-importer': KpiCategoryImporter,
55
+ 'kpi-category-value-calculator': KpiCategoryValueCalculator
54
56
  }
55
57
  }
56
58
 
@@ -159,12 +161,45 @@ export class KpiCategoryListPage extends connect(store)(localize(i18next)(Scoped
159
161
  async pageInitialized(lifecycle: any) {
160
162
  this.gristConfig = {
161
163
  list: {
162
- fields: ['name', 'description', 'active', 'formula', 'weight', 'createdAt', 'updatedAt', 'creator', 'updater'],
163
- details: ['name', 'description', 'active', 'formula', 'weight', 'createdAt', 'updatedAt', 'creator', 'updater']
164
+ fields: [
165
+ 'name',
166
+ 'description',
167
+ 'active',
168
+ 'formula',
169
+ 'weight',
170
+ 'periodType',
171
+ 'createdAt',
172
+ 'updatedAt',
173
+ 'creator',
174
+ 'updater'
175
+ ],
176
+ details: [
177
+ 'name',
178
+ 'description',
179
+ 'active',
180
+ 'formula',
181
+ 'weight',
182
+ 'periodType',
183
+ 'createdAt',
184
+ 'updatedAt',
185
+ 'creator',
186
+ 'updater'
187
+ ]
164
188
  },
165
189
  columns: [
166
190
  { type: 'gutter', gutterName: 'sequence' },
167
191
  { type: 'gutter', gutterName: 'row-selector', multiple: true },
192
+ {
193
+ type: 'gutter',
194
+ gutterName: 'button',
195
+ icon: 'calculate',
196
+ title: '실적값 계산',
197
+ handlers: {
198
+ click: (columns, data, column, record, rowIndex) => {
199
+ this.openCalculator(record)
200
+ }
201
+ }
202
+ },
168
203
  {
169
204
  type: 'string',
170
205
  name: 'name',
@@ -201,6 +236,26 @@ export class KpiCategoryListPage extends connect(store)(localize(i18next)(Scoped
201
236
  record: { editable: true },
202
237
  width: 80
203
238
  },
239
+ {
240
+ type: 'select',
241
+ name: 'periodType',
242
+ header: '계산주기',
243
+ record: {
244
+ editable: true,
245
+ options: [
246
+ { value: '', display: '' },
247
+ { value: 'DAY', display: '일' },
248
+ { value: 'WEEK', display: '주' },
249
+ { value: 'MONTH', display: '월' },
250
+ { value: 'QUARTER', display: '분기' },
251
+ { value: 'YEAR', display: '년' },
252
+ { value: 'RANGE', display: '범위' },
253
+ { value: 'ALLTIME', display: '전체' }
254
+ ]
255
+ },
256
+
257
+ width: 80
258
+ },
204
259
  {
205
260
  type: 'checkbox',
206
261
  name: 'active',
@@ -264,6 +319,7 @@ export class KpiCategoryListPage extends connect(store)(localize(i18next)(Scoped
264
319
  active
265
320
  formula
266
321
  weight
322
+ periodType
267
323
  updater {
268
324
  id
269
325
  name
@@ -461,4 +517,21 @@ export class KpiCategoryListPage extends connect(store)(localize(i18next)(Scoped
461
517
  this.grist.fetch()
462
518
  }
463
519
  }
520
+
521
+ private async openCalculator(record: any) {
522
+ const popup = openPopup(
523
+ html`
524
+ <kpi-category-value-calculator
525
+ .categoryId=${record.id}
526
+ .categoryName=${record.name}
527
+ @closed=${() => popup.close()}
528
+ ></kpi-category-value-calculator>
529
+ `,
530
+ {
531
+ backdrop: true,
532
+ size: 'large',
533
+ title: i18next.t('title.calculate kpi value')
534
+ }
535
+ )
536
+ }
464
537
  }