@things-factory/kpi 9.0.21 → 9.0.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. package/client/charts/kpi-boxplot-chart.ts +163 -0
  2. package/client/charts/kpi-radar-chart.ts +128 -0
  3. package/client/pages/kpi/kpi-list-page.ts +180 -22
  4. package/client/pages/kpi-category/kpi-category-list-page.ts +76 -3
  5. package/client/pages/kpi-category/kpi-category-value-calculator.ts +233 -0
  6. package/client/pages/kpi-dashboard/kpi-dashboard.ts +188 -0
  7. package/client/pages/kpi-metric/kpi-metric-list-page.ts +13 -1
  8. package/client/pages/kpi-metric-value/kpi-metric-value-list-page.ts +43 -1
  9. package/client/pages/kpi-metric-value/kpi-metric-value-manual-entry-form.ts +3 -13
  10. package/client/pages/kpi-metric-value/kpi-metric-value-manual-entry-page.ts +13 -1
  11. package/client/pages/kpi-value/kpi-value-list-page.ts +45 -1
  12. package/dist-client/charts/kpi-boxplot-chart.d.ts +22 -0
  13. package/dist-client/charts/kpi-boxplot-chart.js +198 -0
  14. package/dist-client/charts/kpi-boxplot-chart.js.map +1 -0
  15. package/dist-client/charts/kpi-radar-chart.d.ts +16 -0
  16. package/dist-client/charts/kpi-radar-chart.js +138 -0
  17. package/dist-client/charts/kpi-radar-chart.js.map +1 -0
  18. package/dist-client/pages/kpi/kpi-list-page.d.ts +2 -1
  19. package/dist-client/pages/kpi/kpi-list-page.js +180 -22
  20. package/dist-client/pages/kpi/kpi-list-page.js.map +1 -1
  21. package/dist-client/pages/kpi-category/kpi-category-list-page.d.ts +3 -0
  22. package/dist-client/pages/kpi-category/kpi-category-list-page.js +71 -3
  23. package/dist-client/pages/kpi-category/kpi-category-list-page.js.map +1 -1
  24. package/dist-client/pages/kpi-category/kpi-category-value-calculator.d.ts +13 -0
  25. package/dist-client/pages/kpi-category/kpi-category-value-calculator.js +256 -0
  26. package/dist-client/pages/kpi-category/kpi-category-value-calculator.js.map +1 -0
  27. package/dist-client/pages/kpi-dashboard/kpi-dashboard.d.ts +11 -0
  28. package/dist-client/pages/kpi-dashboard/kpi-dashboard.js +185 -0
  29. package/dist-client/pages/kpi-dashboard/kpi-dashboard.js.map +1 -1
  30. package/dist-client/pages/kpi-metric/kpi-metric-list-page.js +13 -1
  31. package/dist-client/pages/kpi-metric/kpi-metric-list-page.js.map +1 -1
  32. package/dist-client/pages/kpi-metric-value/kpi-metric-value-list-page.d.ts +4 -1
  33. package/dist-client/pages/kpi-metric-value/kpi-metric-value-list-page.js +39 -2
  34. package/dist-client/pages/kpi-metric-value/kpi-metric-value-list-page.js.map +1 -1
  35. package/dist-client/pages/kpi-metric-value/kpi-metric-value-manual-entry-form.js +3 -13
  36. package/dist-client/pages/kpi-metric-value/kpi-metric-value-manual-entry-form.js.map +1 -1
  37. package/dist-client/pages/kpi-metric-value/kpi-metric-value-manual-entry-page.js +13 -1
  38. package/dist-client/pages/kpi-metric-value/kpi-metric-value-manual-entry-page.js.map +1 -1
  39. package/dist-client/pages/kpi-value/kpi-value-list-page.d.ts +1 -0
  40. package/dist-client/pages/kpi-value/kpi-value-list-page.js +45 -1
  41. package/dist-client/pages/kpi-value/kpi-value-list-page.js.map +1 -1
  42. package/dist-client/tsconfig.tsbuildinfo +1 -1
  43. package/dist-server/calculator/evaluator.d.ts +8 -0
  44. package/dist-server/calculator/evaluator.js +42 -0
  45. package/dist-server/calculator/evaluator.js.map +1 -0
  46. package/dist-server/calculator/functions.d.ts +3 -0
  47. package/dist-server/calculator/functions.js +62 -0
  48. package/dist-server/calculator/functions.js.map +1 -0
  49. package/dist-server/calculator/index.d.ts +4 -0
  50. package/dist-server/calculator/index.js +8 -0
  51. package/dist-server/calculator/index.js.map +1 -0
  52. package/dist-server/calculator/parser.d.ts +21 -0
  53. package/dist-server/calculator/parser.js +121 -0
  54. package/dist-server/calculator/parser.js.map +1 -0
  55. package/dist-server/calculator/provider.d.ts +8 -0
  56. package/dist-server/calculator/provider.js +13 -0
  57. package/dist-server/calculator/provider.js.map +1 -0
  58. package/dist-server/controllers/kpi-metric-value-provider.d.ts +11 -0
  59. package/dist-server/controllers/kpi-metric-value-provider.js +63 -0
  60. package/dist-server/controllers/kpi-metric-value-provider.js.map +1 -0
  61. package/dist-server/controllers/kpi-value-provider.d.ts +11 -0
  62. package/dist-server/controllers/kpi-value-provider.js +46 -0
  63. package/dist-server/controllers/kpi-value-provider.js.map +1 -0
  64. package/dist-server/service/index.d.ts +2 -2
  65. package/dist-server/service/kpi/aggregate-kpi.js +4 -4
  66. package/dist-server/service/kpi/aggregate-kpi.js.map +1 -1
  67. package/dist-server/service/kpi/kpi-grade.types.d.ts +11 -10
  68. package/dist-server/service/kpi/kpi-grade.types.js.map +1 -1
  69. package/dist-server/service/kpi/kpi-history.d.ts +2 -2
  70. package/dist-server/service/kpi/kpi-history.js.map +1 -1
  71. package/dist-server/service/kpi/kpi-mutation.d.ts +2 -0
  72. package/dist-server/service/kpi/kpi-mutation.js +126 -4
  73. package/dist-server/service/kpi/kpi-mutation.js.map +1 -1
  74. package/dist-server/service/kpi/kpi-type.d.ts +8 -5
  75. package/dist-server/service/kpi/kpi-type.js +22 -8
  76. package/dist-server/service/kpi/kpi-type.js.map +1 -1
  77. package/dist-server/service/kpi/kpi.d.ts +6 -3
  78. package/dist-server/service/kpi/kpi.js +29 -9
  79. package/dist-server/service/kpi/kpi.js.map +1 -1
  80. package/dist-server/service/kpi-category/kpi-category-mutation.d.ts +1 -1
  81. package/dist-server/service/kpi-category/kpi-category-mutation.js +3 -3
  82. package/dist-server/service/kpi-category/kpi-category-mutation.js.map +1 -1
  83. package/dist-server/service/kpi-category/kpi-category-query.d.ts +13 -0
  84. package/dist-server/service/kpi-category/kpi-category-query.js +180 -0
  85. package/dist-server/service/kpi-category/kpi-category-query.js.map +1 -1
  86. package/dist-server/service/kpi-category/kpi-category-type.d.ts +3 -0
  87. package/dist-server/service/kpi-category/kpi-category-type.js +16 -1
  88. package/dist-server/service/kpi-category/kpi-category-type.js.map +1 -1
  89. package/dist-server/service/kpi-category/kpi-category.d.ts +2 -0
  90. package/dist-server/service/kpi-category/kpi-category.js +10 -1
  91. package/dist-server/service/kpi-category/kpi-category.js.map +1 -1
  92. package/dist-server/service/kpi-metric/kpi-metric-type.d.ts +5 -3
  93. package/dist-server/service/kpi-metric/kpi-metric-type.js +5 -3
  94. package/dist-server/service/kpi-metric/kpi-metric-type.js.map +1 -1
  95. package/dist-server/service/kpi-metric/kpi-metric.d.ts +2 -8
  96. package/dist-server/service/kpi-metric/kpi-metric.js +3 -14
  97. package/dist-server/service/kpi-metric/kpi-metric.js.map +1 -1
  98. package/dist-server/service/kpi-metric-value/kpi-metric-value-mutation.js +67 -45
  99. package/dist-server/service/kpi-metric-value/kpi-metric-value-mutation.js.map +1 -1
  100. package/dist-server/service/kpi-metric-value/kpi-metric-value.js +3 -2
  101. package/dist-server/service/kpi-metric-value/kpi-metric-value.js.map +1 -1
  102. package/dist-server/service/kpi-value/kpi-value-mutation.d.ts +2 -1
  103. package/dist-server/service/kpi-value/kpi-value-mutation.js +114 -6
  104. package/dist-server/service/kpi-value/kpi-value-mutation.js.map +1 -1
  105. package/dist-server/service/kpi-value/kpi-value-query.d.ts +0 -2
  106. package/dist-server/service/kpi-value/kpi-value-query.js +0 -12
  107. package/dist-server/service/kpi-value/kpi-value-query.js.map +1 -1
  108. package/dist-server/service/kpi-value/kpi-value-score.service.d.ts +26 -0
  109. package/dist-server/service/kpi-value/kpi-value-score.service.js +97 -0
  110. package/dist-server/service/kpi-value/kpi-value-score.service.js.map +1 -0
  111. package/dist-server/service/kpi-value/kpi-value-type.d.ts +2 -0
  112. package/dist-server/service/kpi-value/kpi-value-type.js +14 -0
  113. package/dist-server/service/kpi-value/kpi-value-type.js.map +1 -1
  114. package/dist-server/service/kpi-value/kpi-value.d.ts +1 -0
  115. package/dist-server/service/kpi-value/kpi-value.js +9 -1
  116. package/dist-server/service/kpi-value/kpi-value.js.map +1 -1
  117. package/dist-server/service/utils/value-date-util.d.ts +3 -0
  118. package/dist-server/service/utils/value-date-util.js +76 -0
  119. package/dist-server/service/utils/value-date-util.js.map +1 -0
  120. package/dist-server/tsconfig.tsbuildinfo +1 -1
  121. package/package.json +2 -2
  122. package/server/calculator/evaluator.ts +45 -0
  123. package/server/calculator/functions.ts +67 -0
  124. package/server/calculator/index.ts +4 -0
  125. package/server/calculator/parser.ts +128 -0
  126. package/server/calculator/provider.ts +10 -0
  127. package/server/controllers/kpi-metric-value-provider.ts +66 -0
  128. package/server/controllers/kpi-value-provider.ts +51 -0
  129. package/server/service/kpi/aggregate-kpi.ts +4 -4
  130. package/server/service/kpi/kpi-grade.types.ts +11 -10
  131. package/server/service/kpi/kpi-history.ts +2 -2
  132. package/server/service/kpi/kpi-mutation.ts +128 -4
  133. package/server/service/kpi/kpi-type.ts +21 -9
  134. package/server/service/kpi/kpi.ts +32 -10
  135. package/server/service/kpi-category/kpi-category-mutation.ts +3 -3
  136. package/server/service/kpi-category/kpi-category-query.ts +175 -1
  137. package/server/service/kpi-category/kpi-category-type.ts +17 -6
  138. package/server/service/kpi-category/kpi-category.ts +10 -1
  139. package/server/service/kpi-metric/kpi-metric-type.ts +7 -5
  140. package/server/service/kpi-metric/kpi-metric.ts +3 -15
  141. package/server/service/kpi-metric-value/kpi-metric-value-mutation.ts +67 -47
  142. package/server/service/kpi-metric-value/kpi-metric-value.ts +4 -2
  143. package/server/service/kpi-value/kpi-value-mutation.ts +110 -6
  144. package/server/service/kpi-value/kpi-value-query.ts +2 -8
  145. package/server/service/kpi-value/kpi-value-score.service.ts +112 -0
  146. package/server/service/kpi-value/kpi-value-type.ts +12 -0
  147. package/server/service/kpi-value/kpi-value.ts +8 -1
  148. package/server/service/utils/value-date-util.ts +72 -0
  149. package/dist-server/service/kpi-value/kpi-value-grade.service.d.ts +0 -34
  150. package/dist-server/service/kpi-value/kpi-value-grade.service.js +0 -117
  151. package/dist-server/service/kpi-value/kpi-value-grade.service.js.map +0 -1
  152. package/server/service/kpi-value/kpi-value-grade.service.ts +0 -127
@@ -1,12 +1,20 @@
1
1
  import { Resolver, Mutation, Arg, Ctx, Directive } from 'type-graphql'
2
- import { In } from 'typeorm'
2
+ import { In, Between } from 'typeorm'
3
3
  import { getRepository } from '@things-factory/shell'
4
4
 
5
5
  import { KpiValue } from './kpi-value'
6
6
  import { NewKpiValue, KpiValuePatch } from './kpi-value-type'
7
7
  import { Kpi } from '../kpi/kpi'
8
+ import { KpiMetric } from '../kpi-metric/kpi-metric'
9
+ import { KpiMetricValue } from '../kpi-metric-value/kpi-metric-value'
10
+ import { KpiPeriodType } from '../kpi/kpi'
11
+ import { getDefaultValueDate } from '../utils/value-date-util'
8
12
  import { KpiValueInputType } from './kpi-value'
9
- import { KpiValueGradeService } from './kpi-value-grade.service'
13
+ import { KpiValueScoreService } from './kpi-value-score.service'
14
+ import { parseFormula } from '../../calculator/parser'
15
+ import { evaluateFormula } from '../../calculator/evaluator'
16
+ import { builtinFunctions } from '../../calculator/functions'
17
+ import { KpiMetricValueProvider } from '../../controllers/kpi-metric-value-provider'
10
18
 
11
19
  @Resolver(KpiValue)
12
20
  export class KpiValueMutation {
@@ -174,10 +182,106 @@ export class KpiValueMutation {
174
182
  }
175
183
 
176
184
  @Directive('@transaction')
177
- @Mutation(returns => Boolean, { description: 'Recalculate grades for all KpiValues of a specific KPI' })
178
- async recalculateGradesForKpi(@Arg('kpiId') kpiId: string, @Ctx() context: ResolverContext): Promise<boolean> {
179
- const gradeService = new KpiValueGradeService()
180
- await gradeService.recalculateGradesForKpi(kpiId, context)
185
+ @Mutation(returns => Boolean, { description: 'Recalculate scores for all KpiValues of a specific KPI' })
186
+ async recalculateScoresForKpi(@Arg('kpiId') kpiId: string, @Ctx() context: ResolverContext): Promise<boolean> {
187
+ const scoreService = new KpiValueScoreService()
188
+ await scoreService.recalculateScoresForKpi(kpiId, context)
181
189
  return true
182
190
  }
191
+
192
+ @Directive('@transaction')
193
+ @Mutation(returns => KpiValue, { description: '기존 KPI Value 인스턴스를 현재 formula/metric 값으로 재계산' })
194
+ async recalculateKpiValue(@Arg('id') id: string, @Ctx() context: ResolverContext): Promise<KpiValue> {
195
+ const { domain, user, tx } = context.state
196
+ const kpiValueRepo = getRepository(KpiValue, tx)
197
+ const kpiRepo = getRepository(Kpi, tx)
198
+ const metricRepo = getRepository(KpiMetric, tx)
199
+ const metricValueRepo = getRepository(KpiMetricValue, tx)
200
+
201
+ // 1. 기존 KPI Value 인스턴스 조회
202
+ const kpiValue = await kpiValueRepo.findOne({
203
+ where: { id, domain: { id: domain.id } },
204
+ relations: ['kpi']
205
+ })
206
+ if (!kpiValue) throw new Error('KPI Value not found')
207
+ const kpi = kpiValue.kpi || (await kpiRepo.findOne({ where: { id: kpiValue.kpiId, domain: { id: domain.id } } }))
208
+ if (!kpi) throw new Error('KPI 정보 없음')
209
+ if (!kpi.formula) throw new Error('KPI formula 없음')
210
+ const periodType = kpi.periodType || KpiPeriodType.DAY
211
+ const valueDate = kpiValue.valueDate
212
+ const group = kpiValue.group
213
+ const version = kpiValue.version
214
+
215
+ // 2. formula에서 metric code 추출
216
+ const metricCodes = (kpi.formula.match(/[a-zA-Z_][a-zA-Z0-9_]*/g) || []).filter(
217
+ code => code !== 'null' && code !== 'undefined'
218
+ )
219
+ const metricMap: Record<string, KpiMetric> = {}
220
+ for (const code of metricCodes) {
221
+ const metric = await metricRepo.findOne({ where: { name: code, domain: { id: domain.id } } })
222
+ if (!metric) throw new Error(`KPI formula metric '${code}' not found`)
223
+ metricMap[code] = metric
224
+ }
225
+
226
+ // 3. metric 값 집계 (periodType 변환/집계)
227
+ const metricValues: Record<string, number> = {}
228
+ for (const code of metricCodes) {
229
+ const metric = metricMap[code]
230
+ let value = null
231
+ if (metric.periodType === periodType) {
232
+ const mv = await metricValueRepo.findOne({
233
+ where: {
234
+ metric: { id: metric.id },
235
+ valueDate,
236
+ periodType,
237
+ group: group ?? '',
238
+ domain: { id: domain.id }
239
+ }
240
+ })
241
+ value = mv?.value ?? null
242
+ } else {
243
+ let startDate: string, endDate: string
244
+ if (periodType === KpiPeriodType.MONTH && metric.periodType === KpiPeriodType.DAY) {
245
+ startDate = valueDate + '-01'
246
+ endDate = valueDate + '-31'
247
+ const mvs = await metricValueRepo.find({
248
+ where: {
249
+ metric: { id: metric.id },
250
+ valueDate: Between(startDate, endDate),
251
+ periodType: metric.periodType,
252
+ group: group ?? '',
253
+ domain: { id: domain.id }
254
+ }
255
+ })
256
+ value = mvs.reduce((sum, mv) => sum + (mv.value ?? 0), 0)
257
+ } else {
258
+ throw new Error('KPI/Metric periodType 조합 집계 미지원')
259
+ }
260
+ }
261
+ if (value == null) throw new Error(`Metric '${code}' 값 없음`)
262
+ metricValues[code] = value
263
+ }
264
+
265
+ // 4. formula 계산 (calculator 기반)
266
+ const ast = parseFormula(kpi.formula)
267
+ const provider = new KpiMetricValueProvider({
268
+ valueDate,
269
+ group,
270
+ domainId: domain.id,
271
+ tx
272
+ })
273
+ const evalContext = { functions: builtinFunctions, provider }
274
+ const kpiValueResult = await evaluateFormula(ast, evalContext)
275
+ if (kpiValueResult == null || isNaN(kpiValueResult)) throw new Error('KPI formula 결과값 없음')
276
+
277
+ // 5. KPI Value 업데이트
278
+ kpiValue.value = kpiValueResult
279
+ kpiValue.updater = user
280
+
281
+ // 6. 성과 점수 자동 계산 및 저장
282
+ const scoreService = new KpiValueScoreService()
283
+ await scoreService.calculateAndSaveScore(kpiValue, kpi)
284
+
285
+ return await kpiValueRepo.save(kpiValue)
286
+ }
183
287
  }
@@ -1,10 +1,10 @@
1
- import { Resolver, Query, FieldResolver, Root, Args, Arg, Ctx, Directive } from 'type-graphql'
1
+ import { Resolver, Query, FieldResolver, Root, Args, Arg, Ctx, Directive, Float } from 'type-graphql'
2
2
  import { Domain, getQueryBuilderFromListParams, getRepository, ListParam } from '@things-factory/shell'
3
3
  import { User } from '@things-factory/auth-base'
4
4
  import { KpiValue } from './kpi-value'
5
5
  import { KpiValueList } from './kpi-value-type'
6
6
  import { Kpi } from '../kpi/kpi'
7
- import { KpiValueGradeService, GradeInfo } from './kpi-value-grade.service'
7
+ import { KpiValueScoreService } from './kpi-value-score.service'
8
8
 
9
9
  @Resolver(KpiValue)
10
10
  export class KpiValueQuery {
@@ -56,10 +56,4 @@ export class KpiValueQuery {
56
56
  if (!kpiValue.kpiId) return null
57
57
  return await getRepository(Kpi).findOneBy({ id: kpiValue.kpiId })
58
58
  }
59
-
60
- @FieldResolver(type => GradeInfo, { nullable: true })
61
- async grade(@Root() kpiValue: KpiValue): Promise<GradeInfo | null> {
62
- const gradeService = new KpiValueGradeService()
63
- return await gradeService.getGradeForKpiValue(kpiValue)
64
- }
65
59
  }
@@ -0,0 +1,112 @@
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
+ // score를 0~1 범위로 제한
37
+ const clampedScore = Math.max(0, Math.min(1, performanceScore))
38
+
39
+ return {
40
+ score: clampedScore,
41
+ calculationMethod: 'formula'
42
+ }
43
+ }
44
+ } catch (error) {
45
+ console.error('Score formula evaluation error:', error)
46
+ }
47
+
48
+ return null
49
+ }
50
+
51
+ /**
52
+ * KPI의 grades lookup table을 이용한 성과 점수 변환
53
+ * 복잡한 성과 점수 변환을 수식으로 표현하기 어려운 경우 사용
54
+ * @deprecated 향후 grades 필드 제거 예정. scoreFormula로 대체 권장
55
+ */
56
+ calculateScoreFromLookup(kpi: Kpi, value: number): ScoreResult | null {
57
+ if (!kpi.grades || kpi.grades.length === 0) {
58
+ return null
59
+ }
60
+
61
+ // grades lookup table에서 해당 값의 범위에 맞는 성과 점수 찾기
62
+ const grade = kpi.grades.find(g => value >= g.minValue && value <= g.maxValue)
63
+
64
+ if (!grade) {
65
+ return null
66
+ }
67
+
68
+ // score를 0~1 범위로 제한
69
+ const clampedScore = Math.max(0, Math.min(1, grade.score))
70
+
71
+ return {
72
+ score: clampedScore,
73
+ calculationMethod: 'lookup'
74
+ }
75
+ }
76
+
77
+ /**
78
+ * KpiValue 생성/수정 시 성과 점수 자동 계산 및 저장
79
+ */
80
+ async calculateAndSaveScore(kpiValue: KpiValue, kpi: Kpi): Promise<void> {
81
+ // 1. scoreFormula 우선 시도
82
+ let scoreResult = await this.calculateScoreFromFormula(kpi, kpiValue.value)
83
+
84
+ // 2. scoreFormula가 없거나 실패하면 grades lookup table 사용 (deprecated)
85
+ if (!scoreResult) {
86
+ scoreResult = this.calculateScoreFromLookup(kpi, kpiValue.value)
87
+ }
88
+
89
+ if (scoreResult) {
90
+ // KpiValue의 score 필드에 성과 점수 저장
91
+ kpiValue.score = scoreResult.score
92
+ }
93
+ }
94
+
95
+ /**
96
+ * 기존 KpiValue들의 성과 점수 재계산 (배치 처리)
97
+ */
98
+ async recalculateScoresForKpi(kpiId: string, context: ResolverContext): Promise<void> {
99
+ const kpi = await getRepository(Kpi).findOne({ where: { id: kpiId } })
100
+ if (!kpi) return
101
+
102
+ const kpiValues = await getRepository(KpiValue).find({
103
+ where: { kpiId, version: kpi.version }
104
+ })
105
+
106
+ for (const kpiValue of kpiValues) {
107
+ await this.calculateAndSaveScore(kpiValue, kpi)
108
+ }
109
+
110
+ await getRepository(KpiValue).save(kpiValues)
111
+ }
112
+ }
@@ -20,6 +20,12 @@ export class NewKpiValue {
20
20
  @Field(type => Float, { description: 'The calculated numeric value for this KPI and period.' })
21
21
  value: number
22
22
 
23
+ @Field(type => Float, {
24
+ nullable: true,
25
+ description: 'Performance score calculated from KPI value using scoreFormula or grades lookup table. Range: 0-1.'
26
+ })
27
+ score?: number
28
+
23
29
  @Field(type => ScalarObject, {
24
30
  nullable: true,
25
31
  description:
@@ -65,6 +71,12 @@ export class KpiValuePatch {
65
71
  @Field(type => Float, { nullable: true, description: 'The calculated numeric value for this KPI and period.' })
66
72
  value?: number
67
73
 
74
+ @Field(type => Float, {
75
+ nullable: true,
76
+ description: 'Performance score calculated from KPI value using scoreFormula or grades lookup table. Range: 0-1.'
77
+ })
78
+ score?: number
79
+
68
80
  @Field(type => ScalarObject, {
69
81
  nullable: true,
70
82
  description:
@@ -71,7 +71,14 @@ export class KpiValue {
71
71
  @Field(type => Float, { description: 'The calculated numeric value for this KPI and period.' })
72
72
  value: number
73
73
 
74
- @Column({ nullable: true })
74
+ @Column('float', { nullable: true })
75
+ @Field(type => Float, {
76
+ nullable: true,
77
+ description: 'Performance score calculated from KPI value using scoreFormula or grades lookup table. Range: 0-1.'
78
+ })
79
+ score?: number
80
+
81
+ @Column({ default: '' })
75
82
  @Field({ nullable: true, description: 'Group key for this value (organization, line, user, etc.)' })
76
83
  group?: string
77
84
 
@@ -0,0 +1,72 @@
1
+ import { KpiPeriodType } from '../kpi/kpi'
2
+
3
+ export function getISOWeek(date: Date): number {
4
+ const tmp = new Date(date.getTime())
5
+ tmp.setHours(0, 0, 0, 0)
6
+ tmp.setDate(tmp.getDate() + 4 - (tmp.getDay() || 7))
7
+ const yearStart = new Date(tmp.getFullYear(), 0, 1)
8
+ const weekNo = Math.ceil(((tmp.getTime() - yearStart.getTime()) / 86400000 + 1) / 7)
9
+ return weekNo
10
+ }
11
+
12
+ export function getDefaultValueDate(periodType: KpiPeriodType, mode: 'current' | 'last' = 'current'): string {
13
+ const now = new Date()
14
+ if (mode === 'last') {
15
+ switch (periodType) {
16
+ case KpiPeriodType.DAY: {
17
+ const d = new Date(now)
18
+ d.setDate(d.getDate() - 1)
19
+ return d.toISOString().slice(0, 10)
20
+ }
21
+ case KpiPeriodType.MONTH: {
22
+ const d = new Date(now)
23
+ d.setMonth(d.getMonth() - 1)
24
+ return d.toISOString().slice(0, 7)
25
+ }
26
+ case KpiPeriodType.QUARTER: {
27
+ let year = now.getFullYear()
28
+ let quarter = Math.floor(now.getMonth() / 3)
29
+ if (quarter === 0) {
30
+ year -= 1
31
+ quarter = 4
32
+ }
33
+ return `${year}-Q${quarter}`
34
+ }
35
+ case KpiPeriodType.WEEK: {
36
+ const d = new Date(now)
37
+ d.setDate(d.getDate() - 7)
38
+ const year = d.getFullYear()
39
+ const week = getISOWeek(d)
40
+ return `${year}-W${week}`
41
+ }
42
+ case KpiPeriodType.ALLTIME:
43
+ return 'ALLTIME'
44
+ default: {
45
+ const d = new Date(now)
46
+ d.setDate(d.getDate() - 1)
47
+ return d.toISOString().slice(0, 10)
48
+ }
49
+ }
50
+ } else {
51
+ switch (periodType) {
52
+ case KpiPeriodType.DAY:
53
+ return now.toISOString().slice(0, 10)
54
+ case KpiPeriodType.MONTH:
55
+ return now.toISOString().slice(0, 7)
56
+ case KpiPeriodType.QUARTER: {
57
+ const year = now.getFullYear()
58
+ const quarter = Math.floor(now.getMonth() / 3) + 1
59
+ return `${year}-Q${quarter}`
60
+ }
61
+ case KpiPeriodType.WEEK: {
62
+ const year = now.getFullYear()
63
+ const week = getISOWeek(now)
64
+ return `${year}-W${week}`
65
+ }
66
+ case KpiPeriodType.ALLTIME:
67
+ return 'ALLTIME'
68
+ default:
69
+ return now.toISOString().slice(0, 10)
70
+ }
71
+ }
72
+ }
@@ -1,34 +0,0 @@
1
- import { Kpi } from '../kpi/kpi';
2
- import { KpiValue } from './kpi-value';
3
- export interface GradeResult {
4
- name: string;
5
- score?: number;
6
- color?: string;
7
- description?: string;
8
- minValue: number;
9
- maxValue: number;
10
- }
11
- export declare class GradeInfo {
12
- name: string;
13
- score?: number;
14
- color?: string;
15
- description?: string;
16
- }
17
- export declare class KpiValueGradeService {
18
- /**
19
- * KPI의 grades 정보를 이용하여 KpiValue의 등급 계산
20
- */
21
- calculateGradeFromKpiGrades(kpi: Kpi, value: number): GradeResult | null;
22
- /**
23
- * KpiValue 생성/수정 시 등급 자동 계산 및 저장
24
- */
25
- calculateAndSaveGrade(kpiValue: KpiValue, kpi: Kpi): Promise<void>;
26
- /**
27
- * 기존 KpiValue들의 등급 재계산 (배치 처리)
28
- */
29
- recalculateGradesForKpi(kpiId: string, context: ResolverContext): Promise<void>;
30
- /**
31
- * 특정 KpiValue의 등급 정보 조회
32
- */
33
- getGradeForKpiValue(kpiValue: KpiValue): Promise<GradeInfo | null>;
34
- }
@@ -1,117 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.KpiValueGradeService = exports.GradeInfo = void 0;
4
- const tslib_1 = require("tslib");
5
- const shell_1 = require("@things-factory/shell");
6
- const type_graphql_1 = require("type-graphql");
7
- const kpi_1 = require("../kpi/kpi");
8
- const kpi_value_1 = require("./kpi-value");
9
- let GradeInfo = class GradeInfo {
10
- };
11
- exports.GradeInfo = GradeInfo;
12
- tslib_1.__decorate([
13
- (0, type_graphql_1.Field)({ description: 'Grade name (e.g., A, B, C, 우수, 양호)' }),
14
- tslib_1.__metadata("design:type", String)
15
- ], GradeInfo.prototype, "name", void 0);
16
- tslib_1.__decorate([
17
- (0, type_graphql_1.Field)({ nullable: true, description: 'Grade score or performance value' }),
18
- tslib_1.__metadata("design:type", Number)
19
- ], GradeInfo.prototype, "score", void 0);
20
- tslib_1.__decorate([
21
- (0, type_graphql_1.Field)({ nullable: true, description: 'Color code for visualization' }),
22
- tslib_1.__metadata("design:type", String)
23
- ], GradeInfo.prototype, "color", void 0);
24
- tslib_1.__decorate([
25
- (0, type_graphql_1.Field)({ nullable: true, description: 'Grade description' }),
26
- tslib_1.__metadata("design:type", String)
27
- ], GradeInfo.prototype, "description", void 0);
28
- exports.GradeInfo = GradeInfo = tslib_1.__decorate([
29
- (0, type_graphql_1.ObjectType)({ description: 'Grade information for KPI value' })
30
- ], GradeInfo);
31
- class KpiValueGradeService {
32
- /**
33
- * KPI의 grades 정보를 이용하여 KpiValue의 등급 계산
34
- */
35
- calculateGradeFromKpiGrades(kpi, value) {
36
- if (!kpi.grades || kpi.grades.length === 0) {
37
- return null;
38
- }
39
- // grades 배열에서 해당 값의 범위에 맞는 등급 찾기
40
- const grade = kpi.grades.find(g => value >= g.minValue && value <= g.maxValue);
41
- if (!grade) {
42
- return null;
43
- }
44
- return {
45
- name: grade.name,
46
- score: grade.score,
47
- color: grade.color,
48
- description: grade.description,
49
- minValue: grade.minValue,
50
- maxValue: grade.maxValue
51
- };
52
- }
53
- /**
54
- * KpiValue 생성/수정 시 등급 자동 계산 및 저장
55
- */
56
- async calculateAndSaveGrade(kpiValue, kpi) {
57
- const gradeResult = this.calculateGradeFromKpiGrades(kpi, kpiValue.value);
58
- if (gradeResult) {
59
- // KpiValue의 meta 필드에 등급 정보 저장
60
- kpiValue.meta = {
61
- ...kpiValue.meta,
62
- grade: {
63
- name: gradeResult.name,
64
- score: gradeResult.score,
65
- color: gradeResult.color,
66
- description: gradeResult.description,
67
- calculatedAt: new Date().toISOString()
68
- }
69
- };
70
- }
71
- }
72
- /**
73
- * 기존 KpiValue들의 등급 재계산 (배치 처리)
74
- */
75
- async recalculateGradesForKpi(kpiId, context) {
76
- const kpi = await (0, shell_1.getRepository)(kpi_1.Kpi).findOne({ where: { id: kpiId } });
77
- if (!kpi)
78
- return;
79
- const kpiValues = await (0, shell_1.getRepository)(kpi_value_1.KpiValue).find({
80
- where: { kpiId, version: kpi.version }
81
- });
82
- for (const kpiValue of kpiValues) {
83
- await this.calculateAndSaveGrade(kpiValue, kpi);
84
- }
85
- await (0, shell_1.getRepository)(kpi_value_1.KpiValue).save(kpiValues);
86
- }
87
- /**
88
- * 특정 KpiValue의 등급 정보 조회
89
- */
90
- async getGradeForKpiValue(kpiValue) {
91
- // meta 필드에서 등급 정보 조회
92
- if (kpiValue.meta?.grade) {
93
- return {
94
- name: kpiValue.meta.grade.name,
95
- score: kpiValue.meta.grade.score,
96
- color: kpiValue.meta.grade.color,
97
- description: kpiValue.meta.grade.description
98
- };
99
- }
100
- // 실시간 계산 (필요시)
101
- const kpi = await (0, shell_1.getRepository)(kpi_1.Kpi).findOne({ where: { id: kpiValue.kpiId } });
102
- if (kpi) {
103
- const gradeResult = this.calculateGradeFromKpiGrades(kpi, kpiValue.value);
104
- if (gradeResult) {
105
- return {
106
- name: gradeResult.name,
107
- score: gradeResult.score,
108
- color: gradeResult.color,
109
- description: gradeResult.description
110
- };
111
- }
112
- }
113
- return null;
114
- }
115
- }
116
- exports.KpiValueGradeService = KpiValueGradeService;
117
- //# sourceMappingURL=kpi-value-grade.service.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"kpi-value-grade.service.js","sourceRoot":"","sources":["../../../server/service/kpi-value/kpi-value-grade.service.ts"],"names":[],"mappings":"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","sourcesContent":["import { getRepository } from '@things-factory/shell'\nimport { ObjectType, Field } from 'type-graphql'\nimport { Kpi } from '../kpi/kpi'\nimport { KpiValue } from './kpi-value'\nimport { KpiGrades } from '../kpi/kpi-grade.types'\n\nexport interface GradeResult {\n name: string\n score?: number\n color?: string\n description?: string\n minValue: number\n maxValue: number\n}\n\n@ObjectType({ description: 'Grade information for KPI value' })\nexport class GradeInfo {\n @Field({ description: 'Grade name (e.g., A, B, C, 우수, 양호)' })\n name: string\n\n @Field({ nullable: true, description: 'Grade score or performance value' })\n score?: number\n\n @Field({ nullable: true, description: 'Color code for visualization' })\n color?: string\n\n @Field({ nullable: true, description: 'Grade description' })\n description?: string\n}\n\nexport class KpiValueGradeService {\n /**\n * KPI의 grades 정보를 이용하여 KpiValue의 등급 계산\n */\n calculateGradeFromKpiGrades(kpi: Kpi, value: number): GradeResult | null {\n if (!kpi.grades || kpi.grades.length === 0) {\n return null\n }\n\n // grades 배열에서 해당 값의 범위에 맞는 등급 찾기\n const grade = kpi.grades.find(g => value >= g.minValue && value <= g.maxValue)\n\n if (!grade) {\n return null\n }\n\n return {\n name: grade.name,\n score: grade.score,\n color: grade.color,\n description: grade.description,\n minValue: grade.minValue,\n maxValue: grade.maxValue\n }\n }\n\n /**\n * KpiValue 생성/수정 시 등급 자동 계산 및 저장\n */\n async calculateAndSaveGrade(kpiValue: KpiValue, kpi: Kpi): Promise<void> {\n const gradeResult = this.calculateGradeFromKpiGrades(kpi, kpiValue.value)\n\n if (gradeResult) {\n // KpiValue의 meta 필드에 등급 정보 저장\n kpiValue.meta = {\n ...kpiValue.meta,\n grade: {\n name: gradeResult.name,\n score: gradeResult.score,\n color: gradeResult.color,\n description: gradeResult.description,\n calculatedAt: new Date().toISOString()\n }\n }\n }\n }\n\n /**\n * 기존 KpiValue들의 등급 재계산 (배치 처리)\n */\n async recalculateGradesForKpi(kpiId: string, context: ResolverContext): Promise<void> {\n const kpi = await getRepository(Kpi).findOne({ where: { id: kpiId } })\n if (!kpi) return\n\n const kpiValues = await getRepository(KpiValue).find({\n where: { kpiId, version: kpi.version }\n })\n\n for (const kpiValue of kpiValues) {\n await this.calculateAndSaveGrade(kpiValue, kpi)\n }\n\n await getRepository(KpiValue).save(kpiValues)\n }\n\n /**\n * 특정 KpiValue의 등급 정보 조회\n */\n async getGradeForKpiValue(kpiValue: KpiValue): Promise<GradeInfo | null> {\n // meta 필드에서 등급 정보 조회\n if (kpiValue.meta?.grade) {\n return {\n name: kpiValue.meta.grade.name,\n score: kpiValue.meta.grade.score,\n color: kpiValue.meta.grade.color,\n description: kpiValue.meta.grade.description\n }\n }\n\n // 실시간 계산 (필요시)\n const kpi = await getRepository(Kpi).findOne({ where: { id: kpiValue.kpiId } })\n if (kpi) {\n const gradeResult = this.calculateGradeFromKpiGrades(kpi, kpiValue.value)\n\n if (gradeResult) {\n return {\n name: gradeResult.name,\n score: gradeResult.score,\n color: gradeResult.color,\n description: gradeResult.description\n }\n }\n }\n\n return null\n }\n}\n"]}
@@ -1,127 +0,0 @@
1
- import { getRepository } from '@things-factory/shell'
2
- import { ObjectType, Field } from 'type-graphql'
3
- import { Kpi } from '../kpi/kpi'
4
- import { KpiValue } from './kpi-value'
5
- import { KpiGrades } from '../kpi/kpi-grade.types'
6
-
7
- export interface GradeResult {
8
- name: string
9
- score?: number
10
- color?: string
11
- description?: string
12
- minValue: number
13
- maxValue: number
14
- }
15
-
16
- @ObjectType({ description: 'Grade information for KPI value' })
17
- export class GradeInfo {
18
- @Field({ description: 'Grade name (e.g., A, B, C, 우수, 양호)' })
19
- name: string
20
-
21
- @Field({ nullable: true, description: 'Grade score or performance value' })
22
- score?: number
23
-
24
- @Field({ nullable: true, description: 'Color code for visualization' })
25
- color?: string
26
-
27
- @Field({ nullable: true, description: 'Grade description' })
28
- description?: string
29
- }
30
-
31
- export class KpiValueGradeService {
32
- /**
33
- * KPI의 grades 정보를 이용하여 KpiValue의 등급 계산
34
- */
35
- calculateGradeFromKpiGrades(kpi: Kpi, value: number): GradeResult | null {
36
- if (!kpi.grades || kpi.grades.length === 0) {
37
- return null
38
- }
39
-
40
- // grades 배열에서 해당 값의 범위에 맞는 등급 찾기
41
- const grade = kpi.grades.find(g => value >= g.minValue && value <= g.maxValue)
42
-
43
- if (!grade) {
44
- return null
45
- }
46
-
47
- return {
48
- name: grade.name,
49
- score: grade.score,
50
- color: grade.color,
51
- description: grade.description,
52
- minValue: grade.minValue,
53
- maxValue: grade.maxValue
54
- }
55
- }
56
-
57
- /**
58
- * KpiValue 생성/수정 시 등급 자동 계산 및 저장
59
- */
60
- async calculateAndSaveGrade(kpiValue: KpiValue, kpi: Kpi): Promise<void> {
61
- const gradeResult = this.calculateGradeFromKpiGrades(kpi, kpiValue.value)
62
-
63
- if (gradeResult) {
64
- // KpiValue의 meta 필드에 등급 정보 저장
65
- kpiValue.meta = {
66
- ...kpiValue.meta,
67
- grade: {
68
- name: gradeResult.name,
69
- score: gradeResult.score,
70
- color: gradeResult.color,
71
- description: gradeResult.description,
72
- calculatedAt: new Date().toISOString()
73
- }
74
- }
75
- }
76
- }
77
-
78
- /**
79
- * 기존 KpiValue들의 등급 재계산 (배치 처리)
80
- */
81
- async recalculateGradesForKpi(kpiId: string, context: ResolverContext): Promise<void> {
82
- const kpi = await getRepository(Kpi).findOne({ where: { id: kpiId } })
83
- if (!kpi) return
84
-
85
- const kpiValues = await getRepository(KpiValue).find({
86
- where: { kpiId, version: kpi.version }
87
- })
88
-
89
- for (const kpiValue of kpiValues) {
90
- await this.calculateAndSaveGrade(kpiValue, kpi)
91
- }
92
-
93
- await getRepository(KpiValue).save(kpiValues)
94
- }
95
-
96
- /**
97
- * 특정 KpiValue의 등급 정보 조회
98
- */
99
- async getGradeForKpiValue(kpiValue: KpiValue): Promise<GradeInfo | null> {
100
- // meta 필드에서 등급 정보 조회
101
- if (kpiValue.meta?.grade) {
102
- return {
103
- name: kpiValue.meta.grade.name,
104
- score: kpiValue.meta.grade.score,
105
- color: kpiValue.meta.grade.color,
106
- description: kpiValue.meta.grade.description
107
- }
108
- }
109
-
110
- // 실시간 계산 (필요시)
111
- const kpi = await getRepository(Kpi).findOne({ where: { id: kpiValue.kpiId } })
112
- if (kpi) {
113
- const gradeResult = this.calculateGradeFromKpiGrades(kpi, kpiValue.value)
114
-
115
- if (gradeResult) {
116
- return {
117
- name: gradeResult.name,
118
- score: gradeResult.score,
119
- color: gradeResult.color,
120
- description: gradeResult.description
121
- }
122
- }
123
- }
124
-
125
- return null
126
- }
127
- }