@things-factory/kpi 9.1.19 → 10.0.0-beta.2

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 (140) hide show
  1. package/client/pages/kpi/kpi-list-page.ts +339 -525
  2. package/client/pages/kpi/kpi-tree-page.ts +135 -207
  3. package/client/pages/kpi-metric/kpi-metric-list-page.ts +146 -226
  4. package/client/pages/kpi-metric-value/kpi-metric-value-editor-page.ts +187 -295
  5. package/client/pages/kpi-metric-value/kpi-metric-value-list-page.ts +123 -194
  6. package/client/pages/kpi-metric-value/kpi-metric-value-manual-entry-page.ts +57 -91
  7. package/client/pages/kpi-statistic/kpi-statistic-editor-page.ts +180 -278
  8. package/client/pages/kpi-statistic/kpi-statistic-list-page.ts +186 -286
  9. package/client/pages/kpi-value/kpi-value-editor-page.ts +189 -292
  10. package/client/pages/kpi-value/kpi-value-list-page.ts +170 -264
  11. package/dist-client/pages/kpi/kpi-list-page.d.ts +0 -6
  12. package/dist-client/pages/kpi/kpi-list-page.js +150 -282
  13. package/dist-client/pages/kpi/kpi-list-page.js.map +1 -1
  14. package/dist-client/pages/kpi/kpi-tree-page.d.ts +1 -7
  15. package/dist-client/pages/kpi/kpi-tree-page.js +76 -127
  16. package/dist-client/pages/kpi/kpi-tree-page.js.map +1 -1
  17. package/dist-client/pages/kpi-metric/kpi-metric-list-page.d.ts +0 -6
  18. package/dist-client/pages/kpi-metric/kpi-metric-list-page.js +62 -116
  19. package/dist-client/pages/kpi-metric/kpi-metric-list-page.js.map +1 -1
  20. package/dist-client/pages/kpi-metric-value/kpi-metric-value-editor-page.d.ts +1 -7
  21. package/dist-client/pages/kpi-metric-value/kpi-metric-value-editor-page.js +82 -140
  22. package/dist-client/pages/kpi-metric-value/kpi-metric-value-editor-page.js.map +1 -1
  23. package/dist-client/pages/kpi-metric-value/kpi-metric-value-list-page.d.ts +0 -6
  24. package/dist-client/pages/kpi-metric-value/kpi-metric-value-list-page.js +54 -98
  25. package/dist-client/pages/kpi-metric-value/kpi-metric-value-list-page.js.map +1 -1
  26. package/dist-client/pages/kpi-metric-value/kpi-metric-value-manual-entry-page.d.ts +1 -7
  27. package/dist-client/pages/kpi-metric-value/kpi-metric-value-manual-entry-page.js +30 -57
  28. package/dist-client/pages/kpi-metric-value/kpi-metric-value-manual-entry-page.js.map +1 -1
  29. package/dist-client/pages/kpi-statistic/kpi-statistic-editor-page.d.ts +1 -7
  30. package/dist-client/pages/kpi-statistic/kpi-statistic-editor-page.js +91 -153
  31. package/dist-client/pages/kpi-statistic/kpi-statistic-editor-page.js.map +1 -1
  32. package/dist-client/pages/kpi-statistic/kpi-statistic-list-page.d.ts +0 -6
  33. package/dist-client/pages/kpi-statistic/kpi-statistic-list-page.js +81 -155
  34. package/dist-client/pages/kpi-statistic/kpi-statistic-list-page.js.map +1 -1
  35. package/dist-client/pages/kpi-value/kpi-value-editor-page.d.ts +1 -7
  36. package/dist-client/pages/kpi-value/kpi-value-editor-page.js +80 -136
  37. package/dist-client/pages/kpi-value/kpi-value-editor-page.js.map +1 -1
  38. package/dist-client/pages/kpi-value/kpi-value-list-page.d.ts +0 -6
  39. package/dist-client/pages/kpi-value/kpi-value-list-page.js +73 -134
  40. package/dist-client/pages/kpi-value/kpi-value-list-page.js.map +1 -1
  41. package/dist-client/tsconfig.tsbuildinfo +1 -1
  42. package/dist-server/service/index.d.ts +1 -1
  43. package/dist-server/tsconfig.tsbuildinfo +1 -1
  44. package/package.json +18 -18
  45. package/client/tsconfig.json +0 -11
  46. package/dist-server/tsconfig.json +0 -10
  47. package/server/@types/index.d.ts +0 -11
  48. package/server/calculator/evaluator.ts +0 -45
  49. package/server/calculator/functions.ts +0 -67
  50. package/server/calculator/index.ts +0 -4
  51. package/server/calculator/parser.ts +0 -137
  52. package/server/calculator/provider.ts +0 -10
  53. package/server/controllers/index.ts +0 -2
  54. package/server/controllers/kpi-metric-value-provider.ts +0 -79
  55. package/server/controllers/kpi-value-provider.ts +0 -51
  56. package/server/index.ts +0 -6
  57. package/server/migrations/1752190849680-seed-kpi-metrics.ts +0 -124
  58. package/server/migrations/1752190849681-seed-kpi.ts +0 -356
  59. package/server/migrations/1752192090123-add-grades-to-kpi.ts +0 -67
  60. package/server/migrations/1752192090124-add-kpi-statistics.ts +0 -719
  61. package/server/migrations/1752192090128-seed-kpi-org-scope.ts +0 -132
  62. package/server/migrations/1752192090129-seed-kpi-values.ts +0 -207
  63. package/server/migrations/grade-data/x11-performance-table.json +0 -962
  64. package/server/migrations/grade-data/x12-performance-table.json +0 -611
  65. package/server/migrations/grade-data/x14-performance-table.json +0 -42
  66. package/server/migrations/grade-data/x21-performance-table.json +0 -889
  67. package/server/migrations/grade-data/x22-performance-table.json +0 -1064
  68. package/server/migrations/grade-data/x23-performance-table.json +0 -42
  69. package/server/migrations/grade-data/x31-performance-table.json +0 -644
  70. package/server/migrations/grade-data/x32-performance-table.json +0 -993
  71. package/server/migrations/grade-data/x33-performance-table.json +0 -195
  72. package/server/migrations/grade-data/x34-performance-table.json +0 -12
  73. package/server/migrations/grade-data/x35-performance-table.json +0 -42
  74. package/server/migrations/grade-data/x41-performance-table.json +0 -825
  75. package/server/migrations/grade-data/x42-performance-table.json +0 -786
  76. package/server/migrations/grade-data/x43-performance-table.json +0 -12
  77. package/server/migrations/grade-data/x44-performance-table.json +0 -42
  78. package/server/migrations/grade-data/x51-performance-table.json +0 -924
  79. package/server/migrations/grade-data/x52-performance-table.json +0 -42
  80. package/server/migrations/grade-data/x61-performance-table.json +0 -261
  81. package/server/migrations/grade-data/x62-performance-table.json +0 -42
  82. package/server/migrations/index.ts +0 -9
  83. package/server/migrations/seed-data/kpi-metrics-seed.json +0 -454
  84. package/server/migrations/seed-data/kpi-org-scope-seed.json +0 -1676
  85. package/server/migrations/seed-data/kpi-scopes-seed.json +0 -121
  86. package/server/migrations/seed-data/kpi-values-seed.json +0 -402
  87. package/server/migrations/seed-data/kpis-seed.json +0 -488
  88. package/server/migrations/seed-data/scope-definitions-seed.json +0 -90
  89. package/server/routes.ts +0 -81
  90. package/server/service/index.ts +0 -51
  91. package/server/service/kpi/aggregate-kpi.ts +0 -103
  92. package/server/service/kpi/event-subscriber.ts +0 -29
  93. package/server/service/kpi/index.ts +0 -9
  94. package/server/service/kpi/kpi-formula.service.ts +0 -164
  95. package/server/service/kpi/kpi-grade.types.ts +0 -28
  96. package/server/service/kpi/kpi-history.ts +0 -126
  97. package/server/service/kpi/kpi-mutation.ts +0 -553
  98. package/server/service/kpi/kpi-query.ts +0 -224
  99. package/server/service/kpi/kpi-type.ts +0 -151
  100. package/server/service/kpi/kpi.ts +0 -254
  101. package/server/service/kpi-alert/index.ts +0 -3
  102. package/server/service/kpi-alert/kpi-alert-query.ts +0 -59
  103. package/server/service/kpi-alert/kpi-alert-type.ts +0 -20
  104. package/server/service/kpi-metric/aggregate-kpi-metric.ts +0 -132
  105. package/server/service/kpi-metric/index.ts +0 -7
  106. package/server/service/kpi-metric/kpi-metric-mutation.ts +0 -309
  107. package/server/service/kpi-metric/kpi-metric-query.ts +0 -70
  108. package/server/service/kpi-metric/kpi-metric-type.ts +0 -111
  109. package/server/service/kpi-metric/kpi-metric.ts +0 -134
  110. package/server/service/kpi-metric-value/index.ts +0 -7
  111. package/server/service/kpi-metric-value/kpi-metric-value-mutation.ts +0 -270
  112. package/server/service/kpi-metric-value/kpi-metric-value-query.ts +0 -62
  113. package/server/service/kpi-metric-value/kpi-metric-value-type.ts +0 -82
  114. package/server/service/kpi-metric-value/kpi-metric-value.ts +0 -93
  115. package/server/service/kpi-org-scope/index.ts +0 -6
  116. package/server/service/kpi-org-scope/kpi-org-scope-mutation.ts +0 -173
  117. package/server/service/kpi-org-scope/kpi-org-scope-query.ts +0 -127
  118. package/server/service/kpi-org-scope/kpi-org-scope-type.ts +0 -68
  119. package/server/service/kpi-org-scope/kpi-org-scope.ts +0 -123
  120. package/server/service/kpi-scope/index.ts +0 -11
  121. package/server/service/kpi-scope/kpi-scope-mutation.ts +0 -129
  122. package/server/service/kpi-scope/kpi-scope-query.ts +0 -63
  123. package/server/service/kpi-scope/kpi-scope-type.ts +0 -96
  124. package/server/service/kpi-scope/kpi-scope.ts +0 -143
  125. package/server/service/kpi-statistic/index.ts +0 -7
  126. package/server/service/kpi-statistic/kpi-statistic-batch.service.ts +0 -231
  127. package/server/service/kpi-statistic/kpi-statistic-calculation.service.ts +0 -410
  128. package/server/service/kpi-statistic/kpi-statistic-mutation.ts +0 -291
  129. package/server/service/kpi-statistic/kpi-statistic-query.ts +0 -146
  130. package/server/service/kpi-statistic/kpi-statistic-type.ts +0 -152
  131. package/server/service/kpi-statistic/kpi-statistic.ts +0 -199
  132. package/server/service/kpi-value/index.ts +0 -7
  133. package/server/service/kpi-value/kpi-value-mutation.ts +0 -432
  134. package/server/service/kpi-value/kpi-value-query.ts +0 -61
  135. package/server/service/kpi-value/kpi-value-score.service.ts +0 -106
  136. package/server/service/kpi-value/kpi-value-type.ts +0 -122
  137. package/server/service/kpi-value/kpi-value.ts +0 -160
  138. package/server/service/utils/value-date-util.ts +0 -119
  139. package/server/tsconfig.json +0 -10
  140. package/server/types/global.d.ts +0 -8
@@ -1,432 +0,0 @@
1
- import { Resolver, Mutation, Arg, Ctx, Directive } from 'type-graphql'
2
- import { In, Between } from 'typeorm'
3
- import { getRepository } from '@things-factory/shell'
4
-
5
- import { KpiValue } from './kpi-value'
6
- import { NewKpiValue, KpiValuePatch } from './kpi-value-type'
7
- import { Kpi } from '../kpi/kpi'
8
- import { KpiMetric } from '../kpi-metric/kpi-metric'
9
- import { KpiOrgScope } from '../kpi-org-scope/kpi-org-scope'
10
- import { KpiMetricValue } from '../kpi-metric-value/kpi-metric-value'
11
- import { KpiPeriodType } from '../kpi/kpi'
12
- import { getDefaultValueDate } from '../utils/value-date-util'
13
- import { KpiValueInputType } from './kpi-value'
14
- import { KpiValueScoreService } from './kpi-value-score.service'
15
- import { parseFormula } from '../../calculator/parser'
16
- import { evaluateFormula } from '../../calculator/evaluator'
17
- import { builtinFunctions } from '../../calculator/functions'
18
- import { KpiMetricValueProvider } from '../../controllers/kpi-metric-value-provider'
19
-
20
- @Resolver(KpiValue)
21
- export class KpiValueMutation {
22
- @Directive('@privilege(category: "kpi", privilege: "mutation", domainOwnerGranted: true, superUserGranted: true)')
23
- @Directive('@transaction')
24
- @Mutation(returns => KpiValue, { description: 'Create a new KPI value with the provided details.' })
25
- async createKpiValue(
26
- @Arg('kpiValue', { description: 'Input object containing details for the new KPI value.' }) kpiValue: NewKpiValue,
27
- @Ctx() context: ResolverContext
28
- ): Promise<KpiValue> {
29
- const { domain, user, tx } = context.state
30
-
31
- let kpi = kpiValue.kpiId ? await getRepository(Kpi).findOne({ where: { id: kpiValue.kpiId } }) : undefined
32
-
33
- let inputType: KpiValueInputType | undefined = undefined
34
- if (kpiValue.inputType) {
35
- if (typeof kpiValue.inputType === 'string') {
36
- inputType = KpiValueInputType[kpiValue.inputType as keyof typeof KpiValueInputType]
37
- } else {
38
- inputType = kpiValue.inputType
39
- }
40
- }
41
-
42
- // KpiOrgScope 처리
43
- let kpiOrgScope: KpiOrgScope | undefined
44
- if (kpiValue.kpiOrgScopeId) {
45
- kpiOrgScope = await getRepository(KpiOrgScope, context.state?.tx).findOne({
46
- where: { id: kpiValue.kpiOrgScopeId, domain: { id: domain.id } }
47
- })
48
- if (!kpiOrgScope) {
49
- throw new Error(`KpiOrgScope not found: ${kpiValue.kpiOrgScopeId}`)
50
- }
51
- } else if (kpiValue.org) {
52
- // Legacy org 처리 - org 문자열로 KpiOrgScope 찾기 또는 생성
53
- kpiOrgScope = await getRepository(KpiOrgScope, context.state?.tx).findOne({
54
- where: { org: kpiValue.org, domain: { id: domain.id } }
55
- })
56
- if (!kpiOrgScope) {
57
- // 새 KpiOrgScope 생성
58
- kpiOrgScope = await getRepository(KpiOrgScope, context.state?.tx).save({
59
- entityType: 'Manual',
60
- entityId: `manual-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
61
- entityName: kpiValue.org,
62
- org: kpiValue.org,
63
- domain,
64
- creator: user,
65
- updater: user
66
- })
67
- }
68
- }
69
-
70
- const entity: Partial<KpiValue> = {
71
- kpi,
72
- kpiId: kpiValue.kpiId,
73
- version: kpiValue.version,
74
- valueDate: kpiValue.valueDate,
75
- value: kpiValue.value,
76
- source: kpiValue.source,
77
- domain: domain,
78
- creator: user,
79
- updater: user
80
- }
81
-
82
- if (kpiOrgScope) {
83
- entity.kpiOrgScope = kpiOrgScope
84
- entity.kpiOrgScopeId = kpiOrgScope.id
85
- }
86
- if (inputType) entity.inputType = inputType
87
-
88
- return await getRepository(KpiValue, tx).save(entity)
89
- }
90
-
91
- @Directive('@transaction')
92
- @Directive('@privilege(category: "kpi", privilege: "mutation", domainOwnerGranted: true, superUserGranted: true)')
93
- @Mutation(returns => [KpiValue], { description: 'Create multiple KPI values with the provided details.' })
94
- async createMultipleKpiValue(
95
- @Arg('values', type => [NewKpiValue], {
96
- description: 'Array of input objects containing details for the new KPI values.'
97
- })
98
- values: NewKpiValue[],
99
- @Ctx() context: ResolverContext
100
- ): Promise<KpiValue[]> {
101
- const { domain, user, tx } = context.state
102
- const repository = getRepository(KpiValue, tx)
103
- const scoreService = new KpiValueScoreService()
104
-
105
- const entities: KpiValue[] = []
106
-
107
- for (const kpiValue of values) {
108
- let kpi = kpiValue.kpiId ? await getRepository(Kpi).findOne({ where: { id: kpiValue.kpiId } }) : undefined
109
-
110
- let inputType: KpiValueInputType | undefined = undefined
111
- if (kpiValue.inputType) {
112
- if (typeof kpiValue.inputType === 'string') {
113
- inputType = KpiValueInputType[kpiValue.inputType as keyof typeof KpiValueInputType]
114
- } else {
115
- inputType = kpiValue.inputType
116
- }
117
- }
118
-
119
- // KpiOrgScope 처리
120
- let kpiOrgScope: KpiOrgScope | undefined
121
- if (kpiValue.kpiOrgScopeId) {
122
- kpiOrgScope = await getRepository(KpiOrgScope, context.state?.tx).findOne({
123
- where: { id: kpiValue.kpiOrgScopeId, domain: { id: domain.id } }
124
- })
125
- if (!kpiOrgScope) {
126
- throw new Error(`KpiOrgScope not found: ${kpiValue.kpiOrgScopeId}`)
127
- }
128
- } else if (kpiValue.org) {
129
- // Legacy org 처리
130
- kpiOrgScope = await getRepository(KpiOrgScope, context.state?.tx).findOne({
131
- where: { org: kpiValue.org, domain: { id: domain.id } }
132
- })
133
- if (!kpiOrgScope) {
134
- // 새 KpiOrgScope 생성
135
- kpiOrgScope = await getRepository(KpiOrgScope, context.state?.tx).save({
136
- entityType: 'Manual',
137
- entityId: `manual-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
138
- entityName: kpiValue.org,
139
- org: kpiValue.org,
140
- domain,
141
- creator: user,
142
- updater: user
143
- })
144
- }
145
- }
146
-
147
- const entity: Partial<KpiValue> = {
148
- kpi,
149
- kpiId: kpiValue.kpiId,
150
- version: kpiValue.version || 1,
151
- valueDate: kpiValue.valueDate,
152
- value: kpiValue.value,
153
- source: kpiValue.source || 'MANUAL',
154
- domain: domain,
155
- creator: user,
156
- updater: user
157
- }
158
-
159
- if (kpiOrgScope) {
160
- entity.kpiOrgScope = kpiOrgScope
161
- entity.kpiOrgScopeId = kpiOrgScope.id
162
- }
163
- if (inputType) entity.inputType = inputType
164
-
165
- const savedEntity = await repository.save(entity)
166
-
167
- // 성과 점수 자동 계산 및 저장
168
- if (kpi) {
169
- await scoreService.calculateAndSaveScore(savedEntity, kpi)
170
- }
171
-
172
- entities.push(savedEntity)
173
- }
174
-
175
- return entities
176
- }
177
-
178
- @Directive('@transaction')
179
- @Directive('@privilege(category: "kpi", privilege: "mutation", domainOwnerGranted: true, superUserGranted: true)')
180
- @Mutation(returns => KpiValue, { description: 'To modify KpiValue information' })
181
- async updateKpiValue(
182
- @Arg('id') id: string,
183
- @Arg('patch') patch: KpiValuePatch,
184
- @Ctx() context: ResolverContext
185
- ): Promise<KpiValue> {
186
- const { domain, user, tx } = context.state
187
-
188
- const repository = getRepository(KpiValue, tx)
189
- const kpiValue = await repository.findOne({
190
- where: { domain: { id: domain.id }, id }
191
- })
192
-
193
- let kpi = patch.kpiId ? await getRepository(Kpi).findOne({ where: { id: patch.kpiId } }) : kpiValue.kpi
194
-
195
- let inputType: KpiValueInputType | undefined = undefined
196
- if (patch.inputType) {
197
- if (typeof patch.inputType === 'string') {
198
- inputType = KpiValueInputType[patch.inputType as keyof typeof KpiValueInputType]
199
- } else {
200
- inputType = patch.inputType
201
- }
202
- } else {
203
- inputType = kpiValue.inputType
204
- }
205
-
206
- const entity: any = {
207
- ...kpiValue,
208
- ...patch,
209
- kpi,
210
- updater: user
211
- }
212
- if (inputType) entity.inputType = inputType
213
-
214
- return await repository.save(entity)
215
- }
216
-
217
- @Directive('@transaction')
218
- @Directive('@privilege(category: "kpi", privilege: "mutation", domainOwnerGranted: true, superUserGranted: true)')
219
- @Mutation(returns => [KpiValue], { description: "To modify multiple KpiValues' information" })
220
- async updateMultipleKpiValue(
221
- @Arg('patches', type => [KpiValuePatch]) patches: KpiValuePatch[],
222
- @Ctx() context: ResolverContext
223
- ): Promise<KpiValue[]> {
224
- const { domain, user, tx } = context.state
225
-
226
- let results = []
227
- const _createRecords = patches.filter((patch: any) => patch.cuFlag.toUpperCase() === '+')
228
- const _updateRecords = patches.filter((patch: any) => patch.cuFlag.toUpperCase() === 'M')
229
- const kpiValueRepo = getRepository(KpiValue, tx)
230
-
231
- if (_createRecords.length > 0) {
232
- for (let i = 0; i < _createRecords.length; i++) {
233
- const newRecord = _createRecords[i]
234
-
235
- const kpi = await getRepository(Kpi, tx).findOne({
236
- where: { domain: { id: domain.id }, id: newRecord.kpiId }
237
- })
238
-
239
- const result = await kpiValueRepo.save({
240
- ...newRecord,
241
- kpi,
242
- version: newRecord.version || kpi.version,
243
- domain,
244
- creator: user,
245
- updater: user
246
- } as any)
247
-
248
- results.push({ ...result, cuFlag: '+' })
249
- }
250
- }
251
-
252
- if (_updateRecords.length > 0) {
253
- for (let i = 0; i < _updateRecords.length; i++) {
254
- const updateRecord = _updateRecords[i]
255
- const kpiValue = await kpiValueRepo.findOneBy({ id: updateRecord.id })
256
-
257
- const kpi = await getRepository(Kpi, tx).findOne({
258
- where: { domain: { id: domain.id }, id: updateRecord.kpiId }
259
- })
260
-
261
- const result = await kpiValueRepo.save({
262
- ...kpiValue,
263
- ...updateRecord,
264
- kpi,
265
- version: updateRecord.version || kpi.version,
266
- updater: user
267
- } as any)
268
-
269
- results.push({ ...result, cuFlag: 'M' })
270
- }
271
- }
272
-
273
- return results
274
- }
275
-
276
- @Directive('@transaction')
277
- @Directive('@privilege(category: "kpi", privilege: "mutation", domainOwnerGranted: true, superUserGranted: true)')
278
- @Mutation(returns => Boolean, { description: 'To delete KpiValue' })
279
- async deleteKpiValue(@Arg('id') id: string, @Ctx() context: ResolverContext): Promise<boolean> {
280
- const { domain, tx } = context.state
281
-
282
- await getRepository(KpiValue, tx).delete({ domain: { id: domain.id }, id })
283
-
284
- return true
285
- }
286
-
287
- @Directive('@transaction')
288
- @Directive('@privilege(category: "kpi", privilege: "mutation", domainOwnerGranted: true, superUserGranted: true)')
289
- @Mutation(returns => Boolean, { description: 'To delete multiple KpiValues' })
290
- async deleteKpiValues(
291
- @Arg('ids', type => [String]) ids: string[],
292
- @Ctx() context: ResolverContext
293
- ): Promise<boolean> {
294
- const { domain, tx } = context.state
295
-
296
- await getRepository(KpiValue, tx).delete({
297
- domain: { id: domain.id },
298
- id: In(ids)
299
- })
300
-
301
- return true
302
- }
303
-
304
- @Directive('@transaction')
305
- @Directive('@privilege(category: "kpi", privilege: "mutation", domainOwnerGranted: true, superUserGranted: true)')
306
- @Mutation(returns => Boolean, { description: 'To import multiple KpiValues' })
307
- async importKpiValues(
308
- @Arg('kpiValues', type => [KpiValuePatch]) kpiValues: KpiValuePatch[],
309
- @Ctx() context: ResolverContext
310
- ): Promise<boolean> {
311
- const { domain, tx } = context.state
312
-
313
- await Promise.all(
314
- kpiValues.map(async (kpiValue: KpiValuePatch) => {
315
- const createdKpiValue: KpiValue = await getRepository(KpiValue, tx).save({ domain, ...kpiValue } as any)
316
- })
317
- )
318
-
319
- return true
320
- }
321
-
322
- @Directive('@transaction')
323
- @Directive('@privilege(category: "kpi", privilege: "mutation", domainOwnerGranted: true, superUserGranted: true)')
324
- @Mutation(returns => Boolean, { description: 'Recalculate scores for all KpiValues of a specific KPI' })
325
- async recalculateScoresForKpi(@Arg('kpiId') kpiId: string, @Ctx() context: ResolverContext): Promise<boolean> {
326
- const scoreService = new KpiValueScoreService()
327
- await scoreService.recalculateScoresForKpi(kpiId, context)
328
- return true
329
- }
330
-
331
- @Directive('@transaction')
332
- @Directive('@privilege(category: "kpi", privilege: "mutation", domainOwnerGranted: true, superUserGranted: true)')
333
- @Mutation(returns => KpiValue, { description: '기존 KPI Value 인스턴스를 현재 formula/metric 값으로 재계산' })
334
- async recalculateKpiValue(@Arg('id') id: string, @Ctx() context: ResolverContext): Promise<KpiValue> {
335
- const { domain, user, tx } = context.state
336
- const kpiValueRepo = getRepository(KpiValue, tx)
337
- const kpiRepo = getRepository(Kpi, tx)
338
- const metricRepo = getRepository(KpiMetric, tx)
339
- const metricValueRepo = getRepository(KpiMetricValue, tx)
340
-
341
- // 1. 기존 KPI Value 인스턴스 조회
342
- const kpiValue = await kpiValueRepo.findOne({
343
- where: { id, domain: { id: domain.id } },
344
- relations: ['kpi', 'kpiOrgScope']
345
- })
346
- if (!kpiValue) throw new Error('KPI Value not found')
347
- // 항상 최신 버전의 KPI를 조회하여 사용
348
- const kpi = await kpiRepo.findOne({
349
- where: { id: kpiValue.kpiId, domain: { id: domain.id } },
350
- order: { version: 'DESC' }
351
- })
352
- if (!kpi) throw new Error('KPI 정보 없음')
353
- if (!kpi.formula) throw new Error('KPI formula 없음')
354
- const periodType = kpi.periodType || KpiPeriodType.DAY
355
- const valueDate = kpiValue.valueDate
356
- const kpiOrgScope = kpiValue.kpiOrgScope
357
- const org = kpiOrgScope?.entityName || kpiOrgScope?.org || 'unknown' // KpiOrgScope에서 org 정보 추출
358
-
359
- // 2. formula에서 metric code 추출
360
- const metricCodes = (kpi.formula.match(/[a-zA-Z_][a-zA-Z0-9_]*/g) || []).filter(
361
- code => code !== 'null' && code !== 'undefined'
362
- )
363
- const metricMap: Record<string, KpiMetric> = {}
364
- for (const code of metricCodes) {
365
- const metric = await metricRepo.findOne({ where: { name: code, domain: { id: domain.id } } })
366
- if (!metric) throw new Error(`KPI formula metric '${code}' not found`)
367
- metricMap[code] = metric
368
- }
369
-
370
- // 3. metric 값 집계 (periodType 변환/집계)
371
- const metricValues: Record<string, number> = {}
372
- for (const code of metricCodes) {
373
- const metric = metricMap[code]
374
- let value = null
375
- if (metric.periodType === periodType) {
376
- const mv = await metricValueRepo.findOne({
377
- where: {
378
- metric: { id: metric.id },
379
- valueDate,
380
- periodType,
381
- org: org ?? '',
382
- domain: { id: domain.id }
383
- }
384
- })
385
- value = mv?.value ?? null
386
- } else {
387
- let startDate: string, endDate: string
388
- if (periodType === KpiPeriodType.MONTH && metric.periodType === KpiPeriodType.DAY) {
389
- startDate = valueDate + '-01'
390
- endDate = valueDate + '-31'
391
- const mvs = await metricValueRepo.find({
392
- where: {
393
- metric: { id: metric.id },
394
- valueDate: Between(startDate, endDate),
395
- periodType: metric.periodType,
396
- org: org ?? '',
397
- domain: { id: domain.id }
398
- }
399
- })
400
- value = mvs.reduce((sum, mv) => sum + (mv.value ?? 0), 0)
401
- } else {
402
- throw new Error('KPI/Metric periodType 조합 집계 미지원')
403
- }
404
- }
405
- if (value == null) throw new Error(`Metric '${code}' 값 없음`)
406
- metricValues[code] = value
407
- }
408
-
409
- // 4. formula 계산 (calculator 기반)
410
- const ast = parseFormula(kpi.formula)
411
- const provider = new KpiMetricValueProvider({
412
- valueDate,
413
- org,
414
- domainId: domain.id,
415
- tx
416
- })
417
- const evalContext = { functions: builtinFunctions, provider }
418
- const kpiValueResult = await evaluateFormula(ast, evalContext)
419
- if (kpiValueResult == null || isNaN(kpiValueResult)) throw new Error('KPI formula 결과값 없음')
420
-
421
- // 5. KPI Value 업데이트 (최신 버전으로)
422
- kpiValue.value = kpiValueResult
423
- kpiValue.version = kpi.version || 1
424
- kpiValue.updater = user
425
-
426
- // 6. 성과 점수 자동 계산 및 저장
427
- const scoreService = new KpiValueScoreService()
428
- await scoreService.calculateAndSaveScore(kpiValue, kpi)
429
-
430
- return await kpiValueRepo.save(kpiValue)
431
- }
432
- }
@@ -1,61 +0,0 @@
1
- import { Resolver, Query, FieldResolver, Root, Args, Arg, Ctx, Directive, Float } from 'type-graphql'
2
- import { Domain, getQueryBuilderFromListParams, getRepository, ListParam } from '@things-factory/shell'
3
- import { User } from '@things-factory/auth-base'
4
- import { KpiValue } from './kpi-value'
5
- import { KpiValueList } from './kpi-value-type'
6
- import { Kpi } from '../kpi/kpi'
7
- import { KpiValueScoreService } from './kpi-value-score.service'
8
-
9
- @Resolver(KpiValue)
10
- export class KpiValueQuery {
11
- @Directive('@privilege(category: "kpi", privilege: "query", domainOwnerGranted: true, superUserGranted: true)')
12
- @Query(returns => KpiValue!, { nullable: true, description: 'Fetch a single KPI value by its unique identifier.' })
13
- async kpiValue(
14
- @Arg('id', { description: 'Unique identifier of the KPI value to fetch.' }) id: string,
15
- @Ctx() context: ResolverContext
16
- ): Promise<KpiValue> {
17
- const { domain } = context.state
18
-
19
- return await getRepository(KpiValue).findOne({
20
- where: { domain: { id: domain.id }, id }
21
- })
22
- }
23
-
24
- @Directive('@privilege(category: "kpi", privilege: "query", domainOwnerGranted: true, superUserGranted: true)')
25
- @Query(returns => KpiValueList, { description: 'To fetch multiple KpiValues' })
26
- async kpiValues(@Args(type => ListParam) params: ListParam, @Ctx() context: ResolverContext): Promise<KpiValueList> {
27
- const { domain } = context.state
28
-
29
- const queryBuilder = getQueryBuilderFromListParams({
30
- domain,
31
- params,
32
- repository: await getRepository(KpiValue),
33
- searchables: ['kpi', 'group', 'valueDate']
34
- })
35
-
36
- const [items, total] = await queryBuilder.getManyAndCount()
37
-
38
- return { items, total }
39
- }
40
-
41
- @FieldResolver(type => Domain)
42
- async domain(@Root() kpiValue: KpiValue): Promise<Domain> {
43
- return kpiValue.domainId && (await getRepository(Domain).findOneBy({ id: kpiValue.domainId }))
44
- }
45
-
46
- @FieldResolver(type => User)
47
- async updater(@Root() kpiValue: KpiValue): Promise<User> {
48
- return kpiValue.updaterId && (await getRepository(User).findOneBy({ id: kpiValue.updaterId }))
49
- }
50
-
51
- @FieldResolver(type => User)
52
- async creator(@Root() kpiValue: KpiValue): Promise<User> {
53
- return kpiValue.creatorId && (await getRepository(User).findOneBy({ id: kpiValue.creatorId }))
54
- }
55
-
56
- @FieldResolver(type => Kpi)
57
- async kpi(@Root() kpiValue: KpiValue): Promise<Kpi | null> {
58
- if (!kpiValue.kpiId) return null
59
- return await getRepository(Kpi).findOneBy({ id: kpiValue.kpiId })
60
- }
61
- }
@@ -1,106 +0,0 @@
1
- import { getRepository } from '@things-factory/shell'
2
- import { Kpi } from '../kpi/kpi'
3
- import { KpiValue } from './kpi-value'
4
- import { parseFormula } from '../../calculator/parser'
5
- import { evaluateFormula } from '../../calculator/evaluator'
6
- import { builtinFunctions } from '../../calculator/functions'
7
-
8
- export interface ScoreResult {
9
- score: number
10
- calculationMethod?: 'formula' | 'lookup'
11
- }
12
-
13
- export class KpiValueScoreService {
14
- /**
15
- * scoreFormula를 사용한 성과 점수 계산
16
- */
17
- async calculateScoreFromFormula(kpi: Kpi, value: number): Promise<ScoreResult | null> {
18
- if (!kpi.scoreFormula) {
19
- return null
20
- }
21
-
22
- try {
23
- const ast = parseFormula(kpi.scoreFormula)
24
- const provider = {
25
- get: async (name: string) => {
26
- if (name === 'value') return value
27
- return null
28
- }
29
- }
30
- const evalContext = { functions: builtinFunctions, provider }
31
- const result = await evaluateFormula(ast, evalContext)
32
-
33
- if (result !== null && result !== undefined && !isNaN(result)) {
34
- const performanceScore = Number(result)
35
-
36
- return {
37
- score: performanceScore,
38
- calculationMethod: 'formula'
39
- }
40
- }
41
- } catch (error) {
42
- console.error(`Score formula evaluation for '${kpi.name}: ${kpi.scoreFormula}' error:`, error)
43
- }
44
-
45
- return null
46
- }
47
-
48
- /**
49
- * KPI의 grades lookup table을 이용한 성과 점수 변환
50
- * 복잡한 성과 점수 변환을 수식으로 표현하기 어려운 경우 사용
51
- * @deprecated 향후 grades 필드 제거 예정. scoreFormula로 대체 권장
52
- */
53
- calculateScoreFromLookup(kpi: Kpi, value: number): ScoreResult | null {
54
- if (!kpi.grades || kpi.grades.length === 0) {
55
- return null
56
- }
57
-
58
- // grades lookup table에서 해당 값의 범위에 맞는 성과 점수 찾기
59
- const grade = kpi.grades.find(g => value >= g.minValue && value <= g.maxValue)
60
-
61
- if (!grade) {
62
- return null
63
- }
64
-
65
- return {
66
- score: grade.score,
67
- calculationMethod: 'lookup'
68
- }
69
- }
70
-
71
- /**
72
- * KpiValue 생성/수정 시 성과 점수 자동 계산 및 저장
73
- */
74
- async calculateAndSaveScore(kpiValue: KpiValue, kpi: Kpi): Promise<void> {
75
- // 1. scoreFormula 우선 시도
76
- let scoreResult = await this.calculateScoreFromFormula(kpi, kpiValue.value)
77
-
78
- // 2. scoreFormula가 없거나 실패하면 grades lookup table 사용 (deprecated)
79
- if (!scoreResult) {
80
- scoreResult = this.calculateScoreFromLookup(kpi, kpiValue.value)
81
- }
82
-
83
- if (scoreResult) {
84
- // KpiValue의 score 필드에 성과 점수 저장
85
- kpiValue.score = scoreResult.score
86
- }
87
- }
88
-
89
- /**
90
- * 기존 KpiValue들의 성과 점수 재계산 (배치 처리)
91
- */
92
- async recalculateScoresForKpi(kpiId: string, context: ResolverContext): Promise<void> {
93
- const kpi = await getRepository(Kpi).findOne({ where: { id: kpiId } })
94
- if (!kpi) return
95
-
96
- const kpiValues = await getRepository(KpiValue).find({
97
- where: { kpiId, version: kpi.version }
98
- })
99
-
100
- for (const kpiValue of kpiValues) {
101
- await this.calculateAndSaveScore(kpiValue, kpi)
102
- }
103
-
104
- await getRepository(KpiValue).save(kpiValues)
105
- }
106
- }