@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,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
- }