@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,553 +0,0 @@
1
- import { Resolver, Mutation, Arg, Ctx, Directive } from 'type-graphql'
2
- import { In } from 'typeorm'
3
- import { getRepository } from '@things-factory/shell'
4
-
5
- import { createAttachment, deleteAttachmentsByRef } from '@things-factory/attachment-base'
6
- import { Kpi } from './kpi'
7
- import { NewKpi, KpiPatch } from './kpi-type'
8
- import { KpiHistory } from './kpi-history'
9
- import { KpiFormulaService } from './kpi-formula.service'
10
- import { Application, CallbackBase, registerSchedule, unregisterSchedule } from '@things-factory/scheduler-client'
11
- import { KpiValue } from '../kpi-value/kpi-value'
12
- import { KpiOrgScope } from '../kpi-org-scope/kpi-org-scope'
13
- import { Between } from 'typeorm'
14
- import { KpiMetric } from '../kpi-metric/kpi-metric'
15
- import { KpiMetricValue } from '../kpi-metric-value/kpi-metric-value'
16
- import { parseFormula } from '../../calculator/parser'
17
- import { evaluateFormula } from '../../calculator/evaluator'
18
- import { builtinFunctions } from '../../calculator/functions'
19
- import { KpiMetricValueProvider } from '../../controllers/kpi-metric-value-provider'
20
-
21
- @Resolver(Kpi)
22
- export class KpiMutation {
23
- @Directive('@transaction')
24
- @Directive('@privilege(category: "kpi", privilege: "mutation", domainOwnerGranted: true, superUserGranted: true)')
25
- @Mutation(returns => Kpi, { description: 'Create a new KPI with the provided details.' })
26
- async createKpi(
27
- @Arg('kpi', { description: 'Input object containing details for the new KPI.' }) kpi: NewKpi,
28
- @Ctx() context: ResolverContext
29
- ): Promise<Kpi> {
30
- const { domain, user, tx } = context.state
31
-
32
- let parent = kpi.parent
33
- ? await getRepository(Kpi).findOne({ where: { id: kpi.parent.id, domain: { id: domain.id } } })
34
- : undefined
35
-
36
- // 순환 참조 검사
37
- if (parent) {
38
- const formulaService = new KpiFormulaService()
39
- const hasCircular = await formulaService.hasCircularReference(parent.id, parent.id)
40
- if (hasCircular) {
41
- throw new Error('Circular reference detected in KPI hierarchy')
42
- }
43
- }
44
-
45
- const result = await getRepository(Kpi, tx).save({
46
- ...kpi,
47
- parent,
48
- domain,
49
- creator: user,
50
- updater: user
51
- })
52
-
53
- if (kpi.thumbnail) {
54
- await createAttachment(
55
- null,
56
- {
57
- attachment: {
58
- file: kpi.thumbnail,
59
- refType: Kpi.name,
60
- refBy: result.id
61
- }
62
- },
63
- context
64
- )
65
- }
66
-
67
- if (kpi.formula) {
68
- const formulaService = new KpiFormulaService()
69
- const result = await formulaService.validateFormula(kpi.formula)
70
- if (!result.valid) {
71
- throw new Error(result.errors.join('\n'))
72
- }
73
- }
74
-
75
- // KPI 스케줄러 등록
76
- if (kpi.schedule) {
77
- const handle = await registerSchedule({
78
- name: result.name,
79
- client: {
80
- application: Application,
81
- group: `${domain.id}`,
82
- type: 'kpi',
83
- key: result.id,
84
- operation: 'schedule'
85
- },
86
- type: 'cron',
87
- schedule: kpi.schedule,
88
- timezone: kpi.timezone,
89
- task: {
90
- type: 'rest',
91
- connection: {
92
- host: `${CallbackBase}/callback-schedule-for-kpi`,
93
- headers: {
94
- 'Content-Type': 'application/json',
95
- accept: '*/*'
96
- }
97
- },
98
- data: {
99
- domainId: domain.id,
100
- kpiId: result.id
101
- },
102
- history_check: true,
103
- failed_policy: 'retry_dlq',
104
- max_retry_count: 3,
105
- retry_period: 60
106
- }
107
- })
108
- result.scheduleId = handle
109
- await getRepository(Kpi, tx).save(result)
110
- }
111
-
112
- return result
113
- }
114
-
115
- @Directive('@transaction')
116
- @Directive('@privilege(category: "kpi", privilege: "mutation", domainOwnerGranted: true, superUserGranted: true)')
117
- @Mutation(returns => Kpi, { description: 'To modify Kpi information' })
118
- async updateKpi(@Arg('id') id: string, @Arg('patch') patch: KpiPatch, @Ctx() context: ResolverContext): Promise<Kpi> {
119
- const { domain, user, tx } = context.state
120
-
121
- const repository = getRepository(Kpi, tx)
122
- const kpi = await repository.findOne({
123
- where: { domain: { id: domain.id }, id }
124
- })
125
-
126
- let parent = patch.parent
127
- ? await getRepository(Kpi).findOne({ where: { id: patch.parent.id, domain: { id: domain.id } } })
128
- : patch.parent === null ? null : kpi.parent
129
-
130
- // 순환 참조 검사 (parent가 변경되는 경우)
131
- if (parent && parent.id !== kpi.parentId) {
132
- const formulaService = new KpiFormulaService()
133
- const hasCircular = await formulaService.hasCircularReference(kpi.id, parent.id)
134
- if (hasCircular) {
135
- throw new Error('Circular reference detected in KPI hierarchy')
136
- }
137
- }
138
-
139
- const result = await repository.save({
140
- ...kpi,
141
- ...patch,
142
- parent,
143
- updater: user
144
- })
145
-
146
- if (patch.thumbnail) {
147
- await deleteAttachmentsByRef(null, { refBys: [result.id] }, context)
148
- await createAttachment(
149
- null,
150
- {
151
- attachment: {
152
- file: patch.thumbnail,
153
- refType: Kpi.name,
154
- refBy: result.id
155
- }
156
- },
157
- context
158
- )
159
- }
160
-
161
- if (patch.formula) {
162
- const formulaService = new KpiFormulaService()
163
- const result = await formulaService.validateFormula(patch.formula)
164
- if (!result.valid) {
165
- throw new Error(result.errors.join('\n'))
166
- }
167
- }
168
-
169
- // KPI 스케줄러 등록/해제 갱신
170
- if (patch.schedule) {
171
- // 기존 스케줄러 해제
172
- if (kpi.scheduleId) {
173
- await unregisterSchedule(kpi.scheduleId)
174
- }
175
- // 새 스케줄러 등록
176
- const handle = await registerSchedule({
177
- name: result.name,
178
- client: {
179
- application: Application,
180
- group: `${domain.id}`,
181
- type: 'kpi',
182
- key: result.id,
183
- operation: 'schedule'
184
- },
185
- type: 'cron',
186
- schedule: patch.schedule,
187
- timezone: patch.timezone,
188
- task: {
189
- type: 'rest',
190
- connection: {
191
- host: `${CallbackBase}/callback-schedule-for-kpi`,
192
- headers: {
193
- 'Content-Type': 'application/json',
194
- accept: '*/*'
195
- }
196
- },
197
- data: {
198
- domainId: domain.id,
199
- kpiId: result.id
200
- },
201
- history_check: true,
202
- failed_policy: 'retry_dlq',
203
- max_retry_count: 3,
204
- retry_period: 60
205
- }
206
- })
207
- result.scheduleId = handle
208
- await repository.save(result)
209
- } else if (patch.schedule === null && kpi.scheduleId) {
210
- // 스케줄 해제 요청
211
- await unregisterSchedule(kpi.scheduleId)
212
- result.scheduleId = null
213
- await repository.save(result)
214
- }
215
-
216
- return result
217
- }
218
-
219
- @Directive('@transaction')
220
- @Directive('@privilege(category: "kpi", privilege: "mutation", domainOwnerGranted: true, superUserGranted: true)')
221
- @Mutation(returns => [Kpi], { description: "To modify multiple Kpis' information" })
222
- async updateMultipleKpi(
223
- @Arg('patches', type => [KpiPatch]) patches: KpiPatch[],
224
- @Ctx() context: ResolverContext
225
- ): Promise<Kpi[]> {
226
- const { domain, user, tx } = context.state
227
-
228
- let results = []
229
- const _createRecords = patches.filter((patch: any) => patch.cuFlag.toUpperCase() === '+')
230
- const _updateRecords = patches.filter((patch: any) => patch.cuFlag.toUpperCase() === 'M')
231
- const kpiRepo = getRepository(Kpi, tx)
232
-
233
- if (_createRecords.length > 0) {
234
- for (let i = 0; i < _createRecords.length; i++) {
235
- const newRecord = _createRecords[i]
236
-
237
- const result = await kpiRepo.save({
238
- ...newRecord,
239
- domain,
240
- creator: user,
241
- updater: user
242
- })
243
-
244
- if (newRecord.thumbnail) {
245
- await createAttachment(
246
- null,
247
- {
248
- attachment: {
249
- file: newRecord.thumbnail,
250
- refType: Kpi.name,
251
- refBy: result.id
252
- }
253
- },
254
- context
255
- )
256
- }
257
-
258
- results.push({ ...result, cuFlag: '+' })
259
- }
260
- }
261
-
262
- if (_updateRecords.length > 0) {
263
- for (let i = 0; i < _updateRecords.length; i++) {
264
- const updateRecord = _updateRecords[i]
265
- const kpi = await kpiRepo.findOneBy({ id: updateRecord.id })
266
-
267
- const result = await kpiRepo.save({
268
- ...kpi,
269
- ...updateRecord,
270
- updater: user
271
- })
272
-
273
- if (updateRecord.thumbnail) {
274
- await deleteAttachmentsByRef(null, { refBys: [result.id] }, context)
275
- await createAttachment(
276
- null,
277
- {
278
- attachment: {
279
- file: updateRecord.thumbnail,
280
- refType: Kpi.name,
281
- refBy: result.id
282
- }
283
- },
284
- context
285
- )
286
- }
287
-
288
- results.push({ ...result, cuFlag: 'M' })
289
- }
290
- }
291
-
292
- return results
293
- }
294
-
295
- @Directive('@transaction')
296
- @Directive('@privilege(category: "kpi", privilege: "mutation", domainOwnerGranted: true, superUserGranted: true)')
297
- @Mutation(returns => Boolean, { description: 'To delete Kpi' })
298
- async deleteKpi(@Arg('id') id: string, @Ctx() context: ResolverContext): Promise<boolean> {
299
- const { domain, tx } = context.state
300
-
301
- const kpi = await getRepository(Kpi, tx).findOne({ where: { domain: { id: domain.id }, id } })
302
- if (kpi && kpi.scheduleId) {
303
- await unregisterSchedule(kpi.scheduleId)
304
- }
305
-
306
- await getRepository(Kpi, tx).delete({ domain: { id: domain.id }, id })
307
- await deleteAttachmentsByRef(null, { refBys: [id] }, context)
308
-
309
- return true
310
- }
311
-
312
- @Directive('@transaction')
313
- @Directive('@privilege(category: "kpi", privilege: "mutation", domainOwnerGranted: true, superUserGranted: true)')
314
- @Mutation(returns => Boolean, { description: 'To delete multiple Kpis' })
315
- async deleteKpis(@Arg('ids', type => [String]) ids: string[], @Ctx() context: ResolverContext): Promise<boolean> {
316
- const { domain, tx } = context.state
317
-
318
- await getRepository(Kpi, tx).delete({
319
- domain: { id: domain.id },
320
- id: In(ids)
321
- })
322
-
323
- await deleteAttachmentsByRef(null, { refBys: ids }, context)
324
-
325
- return true
326
- }
327
-
328
- @Directive('@transaction')
329
- @Directive('@privilege(category: "kpi", privilege: "mutation", domainOwnerGranted: true, superUserGranted: true)')
330
- @Mutation(returns => Boolean, { description: 'To import multiple Kpis' })
331
- async importKpis(
332
- @Arg('kpis', type => [KpiPatch]) kpis: KpiPatch[],
333
- @Ctx() context: ResolverContext
334
- ): Promise<boolean> {
335
- const { domain, tx } = context.state
336
-
337
- await Promise.all(
338
- kpis.map(async (kpi: KpiPatch) => {
339
- const createdKpi: Kpi = await getRepository(Kpi, tx).save({ domain, ...kpi })
340
- })
341
- )
342
-
343
- return true
344
- }
345
-
346
- @Directive('@transaction')
347
- @Directive('@privilege(category: "kpi", privilege: "mutation", domainOwnerGranted: true, superUserGranted: true)')
348
- @Mutation(returns => Kpi, { description: 'Release a KPI and create a version history.' })
349
- async releaseKpi(@Arg('id') id: string, @Ctx() context: ResolverContext): Promise<Kpi> {
350
- const { domain, user, tx } = context.state
351
- const repository = getRepository(Kpi, tx)
352
- const historyRepository = getRepository(KpiHistory, tx)
353
-
354
- const kpi = await repository.findOne({
355
- where: { domain: { id: domain.id }, id }
356
- })
357
- if (!kpi) throw `KPI given id(${id}) is not found`
358
- if (kpi.state == 'RELEASE') throw `KPI given id(${id}) is already released`
359
-
360
- // 히스토리에서 max version 가져오기
361
- const maxHistory = await historyRepository
362
- .createQueryBuilder('history')
363
- .select('MAX(history.version)', 'max')
364
- .where('history.originalId = :id', { id: kpi.id })
365
- .getRawOne()
366
- const nextVersion = maxHistory?.max ? Number(maxHistory.max) + 1 : 1
367
-
368
- const updated = await repository.save({
369
- ...kpi,
370
- version: nextVersion,
371
- state: 'RELEASE',
372
- updater: user
373
- } as any)
374
- return updated
375
- }
376
-
377
- @Directive('@transaction')
378
- @Directive('@privilege(category: "kpi", privilege: "mutation", domainOwnerGranted: true, superUserGranted: true)')
379
- @Mutation(returns => Kpi, { description: 'Revert a KPI to a specific historical version.' })
380
- async revertKpiVersion(
381
- @Arg('id') id: string,
382
- @Arg('version') version: number,
383
- @Ctx() context: ResolverContext
384
- ): Promise<Kpi> {
385
- const { domain, user, tx } = context.state
386
- const repository = getRepository(Kpi, tx)
387
- const historyRepository = getRepository(KpiHistory, tx)
388
-
389
- const kpi = await repository.findOne({
390
- where: { domain: { id: domain.id }, id }
391
- })
392
- if (!kpi) throw `KPI with id(${id}) is not found`
393
-
394
- const kpiHistory = await historyRepository.findOne({
395
- where: { domain: { id: domain.id }, originalId: id, version },
396
- order: { version: 'DESC' }
397
- })
398
- if (!kpiHistory) throw `KPI with id:version(${id}:${version}) is not found`
399
-
400
- const updated = await repository.save({
401
- ...kpi,
402
- name: kpiHistory.name,
403
- description: kpiHistory.description,
404
- formula: kpiHistory.formula,
405
- active: kpiHistory.active,
406
- state: 'DRAFT',
407
- thumbnail: kpiHistory.thumbnail,
408
- updater: user
409
- } as any)
410
- return updated
411
- }
412
-
413
- @Directive('@transaction')
414
- @Directive('@privilege(category: "kpi", privilege: "mutation", domainOwnerGranted: true, superUserGranted: true)')
415
- @Mutation(returns => KpiValue, { description: 'KPI 기준으로 formula 계산 및 KPI Value upsert' })
416
- async calculateKpiValue(
417
- @Arg('kpiId') kpiId: string,
418
- @Arg('valueDate', { nullable: true }) valueDate: string,
419
- @Arg('org', { nullable: true }) org: string,
420
- @Ctx() context: ResolverContext
421
- ): Promise<KpiValue> {
422
- const { domain, user, tx } = context.state
423
- const kpiRepo = getRepository(Kpi, tx)
424
- const metricRepo = getRepository<KpiMetric>(require('../kpi-metric/kpi-metric').KpiMetric, tx)
425
- const metricValueRepo = getRepository<KpiMetricValue>(
426
- require('../kpi-metric-value/kpi-metric-value').KpiMetricValue,
427
- tx
428
- )
429
- const kpiValueRepo = getRepository<KpiValue>(KpiValue, tx)
430
- const { KpiPeriodType } = require('./kpi')
431
- const { getDefaultValueDate } = require('../utils/value-date-util')
432
-
433
- // 1. KPI 정보 조회
434
- const kpi = await kpiRepo.findOne({ where: { id: kpiId, domain: { id: domain.id } } })
435
- if (!kpi) throw new Error('KPI 정보 없음')
436
- if (!kpi.formula) throw new Error('KPI formula 없음')
437
- const periodType = kpi.periodType || KpiPeriodType.DAY
438
- const valueDateToUse = valueDate || getDefaultValueDate(periodType, 'last')
439
-
440
- // 2. formula에서 metric code 추출
441
- const metricCodes = (kpi.formula.match(/[a-zA-Z_][a-zA-Z0-9_]*/g) || []).filter(
442
- code => code !== 'null' && code !== 'undefined'
443
- )
444
- const metricMap: Record<string, any> = {}
445
- for (const code of metricCodes) {
446
- const metric = await metricRepo.findOne({ where: { name: code, domain: { id: domain.id } } })
447
- if (!metric) throw new Error(`KPI formula metric '${code}' not found`)
448
- metricMap[code] = metric
449
- }
450
-
451
- // 3. metric 값 집계 (periodType 변환/집계)
452
- const metricValues: Record<string, number> = {}
453
- for (const code of metricCodes) {
454
- const metric = metricMap[code]
455
- let value = null
456
- if (metric.periodType === periodType) {
457
- const mv = await metricValueRepo.findOne({
458
- where: {
459
- metric: { id: metric.id },
460
- valueDate: valueDateToUse,
461
- periodType,
462
- org: org ?? '',
463
- domain: { id: domain.id }
464
- }
465
- })
466
- value = mv?.value ?? null
467
- } else {
468
- let startDate: string, endDate: string
469
- if (periodType === KpiPeriodType.MONTH && metric.periodType === KpiPeriodType.DAY) {
470
- startDate = valueDateToUse + '-01'
471
- endDate = valueDateToUse + '-31'
472
- const mvs = await metricValueRepo.find({
473
- where: {
474
- metric: { id: metric.id },
475
- valueDate: Between(startDate, endDate),
476
- periodType: metric.periodType,
477
- org: org ?? '',
478
- domain: { id: domain.id }
479
- }
480
- })
481
- value = mvs.reduce((sum, mv) => sum + (mv.value ?? 0), 0)
482
- } else {
483
- throw new Error('KPI/Metric periodType 조합 집계 미지원')
484
- }
485
- }
486
- if (value == null) throw new Error(`Metric '${code}' 값 없음`)
487
- metricValues[code] = value
488
- }
489
-
490
- // 4. formula 계산 (calculator 기반)
491
- const ast = parseFormula(kpi.formula)
492
- const provider = new KpiMetricValueProvider({
493
- valueDate: valueDateToUse,
494
- org,
495
- domainId: domain.id,
496
- tx
497
- })
498
- const evalContext = { functions: builtinFunctions, provider }
499
- const kpiValueResult = await evaluateFormula(ast, evalContext)
500
-
501
- // 5. KpiOrgScope 처리
502
- let kpiOrgScope: KpiOrgScope | null = null
503
- if (org) {
504
- kpiOrgScope = await getRepository(KpiOrgScope, tx).findOne({
505
- where: { org: org, domain: { id: domain.id } }
506
- })
507
- if (!kpiOrgScope) {
508
- // 새 KpiOrgScope 생성
509
- kpiOrgScope = await getRepository(KpiOrgScope, tx).save({
510
- entityType: 'KpiCalculation',
511
- entityId: `calc-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
512
- entityName: org,
513
- org: org,
514
- domain,
515
- creator: user,
516
- updater: user
517
- })
518
- }
519
- }
520
-
521
- // 6. KPI Value upsert
522
- let kpiValue = await kpiValueRepo.findOne({
523
- where: {
524
- kpi: { id: kpi.id },
525
- valueDate: valueDateToUse,
526
- kpiOrgScope: kpiOrgScope ? { id: kpiOrgScope.id } : null,
527
- version: kpi.version,
528
- domain: { id: domain.id }
529
- }
530
- })
531
- if (kpiValue) {
532
- kpiValue.value = kpiValueResult
533
- kpiValue.updater = user
534
- } else {
535
- const createData: any = {
536
- kpi,
537
- kpiId: kpi.id,
538
- version: kpi.version,
539
- valueDate: valueDateToUse,
540
- value: kpiValueResult,
541
- domain,
542
- creator: user,
543
- updater: user
544
- }
545
- if (kpiOrgScope) {
546
- createData.kpiOrgScope = kpiOrgScope
547
- createData.kpiOrgScopeId = kpiOrgScope.id
548
- }
549
- kpiValue = Object.assign(new KpiValue(), createData)
550
- }
551
- return await kpiValueRepo.save(kpiValue)
552
- }
553
- }