@things-factory/kpi 9.2.5 → 10.0.0-beta.10

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 (139) 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/tsconfig.tsbuildinfo +1 -1
  43. package/package.json +18 -18
  44. package/client/tsconfig.json +0 -11
  45. package/dist-server/tsconfig.json +0 -10
  46. package/server/@types/index.d.ts +0 -11
  47. package/server/calculator/evaluator.ts +0 -45
  48. package/server/calculator/functions.ts +0 -67
  49. package/server/calculator/index.ts +0 -4
  50. package/server/calculator/parser.ts +0 -137
  51. package/server/calculator/provider.ts +0 -10
  52. package/server/controllers/index.ts +0 -2
  53. package/server/controllers/kpi-metric-value-provider.ts +0 -79
  54. package/server/controllers/kpi-value-provider.ts +0 -51
  55. package/server/index.ts +0 -6
  56. package/server/migrations/1752190849680-seed-kpi-metrics.ts +0 -124
  57. package/server/migrations/1752190849681-seed-kpi.ts +0 -356
  58. package/server/migrations/1752192090123-add-grades-to-kpi.ts +0 -67
  59. package/server/migrations/1752192090124-add-kpi-statistics.ts +0 -719
  60. package/server/migrations/1752192090128-seed-kpi-org-scope.ts +0 -132
  61. package/server/migrations/1752192090129-seed-kpi-values.ts +0 -207
  62. package/server/migrations/grade-data/x11-performance-table.json +0 -962
  63. package/server/migrations/grade-data/x12-performance-table.json +0 -611
  64. package/server/migrations/grade-data/x14-performance-table.json +0 -42
  65. package/server/migrations/grade-data/x21-performance-table.json +0 -889
  66. package/server/migrations/grade-data/x22-performance-table.json +0 -1064
  67. package/server/migrations/grade-data/x23-performance-table.json +0 -42
  68. package/server/migrations/grade-data/x31-performance-table.json +0 -644
  69. package/server/migrations/grade-data/x32-performance-table.json +0 -993
  70. package/server/migrations/grade-data/x33-performance-table.json +0 -195
  71. package/server/migrations/grade-data/x34-performance-table.json +0 -12
  72. package/server/migrations/grade-data/x35-performance-table.json +0 -42
  73. package/server/migrations/grade-data/x41-performance-table.json +0 -825
  74. package/server/migrations/grade-data/x42-performance-table.json +0 -786
  75. package/server/migrations/grade-data/x43-performance-table.json +0 -12
  76. package/server/migrations/grade-data/x44-performance-table.json +0 -42
  77. package/server/migrations/grade-data/x51-performance-table.json +0 -924
  78. package/server/migrations/grade-data/x52-performance-table.json +0 -42
  79. package/server/migrations/grade-data/x61-performance-table.json +0 -261
  80. package/server/migrations/grade-data/x62-performance-table.json +0 -42
  81. package/server/migrations/index.ts +0 -9
  82. package/server/migrations/seed-data/kpi-metrics-seed.json +0 -454
  83. package/server/migrations/seed-data/kpi-org-scope-seed.json +0 -1676
  84. package/server/migrations/seed-data/kpi-scopes-seed.json +0 -121
  85. package/server/migrations/seed-data/kpi-values-seed.json +0 -402
  86. package/server/migrations/seed-data/kpis-seed.json +0 -488
  87. package/server/migrations/seed-data/scope-definitions-seed.json +0 -90
  88. package/server/routes.ts +0 -81
  89. package/server/service/index.ts +0 -51
  90. package/server/service/kpi/aggregate-kpi.ts +0 -103
  91. package/server/service/kpi/event-subscriber.ts +0 -29
  92. package/server/service/kpi/index.ts +0 -9
  93. package/server/service/kpi/kpi-formula.service.ts +0 -164
  94. package/server/service/kpi/kpi-grade.types.ts +0 -28
  95. package/server/service/kpi/kpi-history.ts +0 -126
  96. package/server/service/kpi/kpi-mutation.ts +0 -553
  97. package/server/service/kpi/kpi-query.ts +0 -224
  98. package/server/service/kpi/kpi-type.ts +0 -151
  99. package/server/service/kpi/kpi.ts +0 -254
  100. package/server/service/kpi-alert/index.ts +0 -3
  101. package/server/service/kpi-alert/kpi-alert-query.ts +0 -59
  102. package/server/service/kpi-alert/kpi-alert-type.ts +0 -20
  103. package/server/service/kpi-metric/aggregate-kpi-metric.ts +0 -132
  104. package/server/service/kpi-metric/index.ts +0 -7
  105. package/server/service/kpi-metric/kpi-metric-mutation.ts +0 -309
  106. package/server/service/kpi-metric/kpi-metric-query.ts +0 -70
  107. package/server/service/kpi-metric/kpi-metric-type.ts +0 -111
  108. package/server/service/kpi-metric/kpi-metric.ts +0 -134
  109. package/server/service/kpi-metric-value/index.ts +0 -7
  110. package/server/service/kpi-metric-value/kpi-metric-value-mutation.ts +0 -270
  111. package/server/service/kpi-metric-value/kpi-metric-value-query.ts +0 -62
  112. package/server/service/kpi-metric-value/kpi-metric-value-type.ts +0 -82
  113. package/server/service/kpi-metric-value/kpi-metric-value.ts +0 -93
  114. package/server/service/kpi-org-scope/index.ts +0 -6
  115. package/server/service/kpi-org-scope/kpi-org-scope-mutation.ts +0 -173
  116. package/server/service/kpi-org-scope/kpi-org-scope-query.ts +0 -127
  117. package/server/service/kpi-org-scope/kpi-org-scope-type.ts +0 -68
  118. package/server/service/kpi-org-scope/kpi-org-scope.ts +0 -123
  119. package/server/service/kpi-scope/index.ts +0 -11
  120. package/server/service/kpi-scope/kpi-scope-mutation.ts +0 -129
  121. package/server/service/kpi-scope/kpi-scope-query.ts +0 -63
  122. package/server/service/kpi-scope/kpi-scope-type.ts +0 -96
  123. package/server/service/kpi-scope/kpi-scope.ts +0 -143
  124. package/server/service/kpi-statistic/index.ts +0 -7
  125. package/server/service/kpi-statistic/kpi-statistic-batch.service.ts +0 -231
  126. package/server/service/kpi-statistic/kpi-statistic-calculation.service.ts +0 -410
  127. package/server/service/kpi-statistic/kpi-statistic-mutation.ts +0 -291
  128. package/server/service/kpi-statistic/kpi-statistic-query.ts +0 -146
  129. package/server/service/kpi-statistic/kpi-statistic-type.ts +0 -152
  130. package/server/service/kpi-statistic/kpi-statistic.ts +0 -199
  131. package/server/service/kpi-value/index.ts +0 -7
  132. package/server/service/kpi-value/kpi-value-mutation.ts +0 -432
  133. package/server/service/kpi-value/kpi-value-query.ts +0 -61
  134. package/server/service/kpi-value/kpi-value-score.service.ts +0 -106
  135. package/server/service/kpi-value/kpi-value-type.ts +0 -122
  136. package/server/service/kpi-value/kpi-value.ts +0 -160
  137. package/server/service/utils/value-date-util.ts +0 -119
  138. package/server/tsconfig.json +0 -10
  139. 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
- }