@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.
- package/client/charts/kpi-boxplot-chart.ts +163 -0
- package/client/charts/kpi-radar-chart.ts +128 -0
- package/client/pages/kpi/kpi-list-page.ts +180 -22
- package/client/pages/kpi-category/kpi-category-list-page.ts +76 -3
- package/client/pages/kpi-category/kpi-category-value-calculator.ts +233 -0
- package/client/pages/kpi-dashboard/kpi-dashboard.ts +188 -0
- package/client/pages/kpi-metric/kpi-metric-list-page.ts +13 -1
- package/client/pages/kpi-metric-value/kpi-metric-value-list-page.ts +43 -1
- package/client/pages/kpi-metric-value/kpi-metric-value-manual-entry-form.ts +3 -13
- package/client/pages/kpi-metric-value/kpi-metric-value-manual-entry-page.ts +13 -1
- package/client/pages/kpi-value/kpi-value-list-page.ts +45 -1
- package/dist-client/charts/kpi-boxplot-chart.d.ts +22 -0
- package/dist-client/charts/kpi-boxplot-chart.js +198 -0
- package/dist-client/charts/kpi-boxplot-chart.js.map +1 -0
- package/dist-client/charts/kpi-radar-chart.d.ts +16 -0
- package/dist-client/charts/kpi-radar-chart.js +138 -0
- package/dist-client/charts/kpi-radar-chart.js.map +1 -0
- package/dist-client/pages/kpi/kpi-list-page.d.ts +2 -1
- package/dist-client/pages/kpi/kpi-list-page.js +180 -22
- package/dist-client/pages/kpi/kpi-list-page.js.map +1 -1
- package/dist-client/pages/kpi-category/kpi-category-list-page.d.ts +3 -0
- package/dist-client/pages/kpi-category/kpi-category-list-page.js +71 -3
- package/dist-client/pages/kpi-category/kpi-category-list-page.js.map +1 -1
- package/dist-client/pages/kpi-category/kpi-category-value-calculator.d.ts +13 -0
- package/dist-client/pages/kpi-category/kpi-category-value-calculator.js +256 -0
- package/dist-client/pages/kpi-category/kpi-category-value-calculator.js.map +1 -0
- package/dist-client/pages/kpi-dashboard/kpi-dashboard.d.ts +11 -0
- package/dist-client/pages/kpi-dashboard/kpi-dashboard.js +185 -0
- package/dist-client/pages/kpi-dashboard/kpi-dashboard.js.map +1 -1
- package/dist-client/pages/kpi-metric/kpi-metric-list-page.js +13 -1
- package/dist-client/pages/kpi-metric/kpi-metric-list-page.js.map +1 -1
- package/dist-client/pages/kpi-metric-value/kpi-metric-value-list-page.d.ts +4 -1
- package/dist-client/pages/kpi-metric-value/kpi-metric-value-list-page.js +39 -2
- package/dist-client/pages/kpi-metric-value/kpi-metric-value-list-page.js.map +1 -1
- package/dist-client/pages/kpi-metric-value/kpi-metric-value-manual-entry-form.js +3 -13
- package/dist-client/pages/kpi-metric-value/kpi-metric-value-manual-entry-form.js.map +1 -1
- package/dist-client/pages/kpi-metric-value/kpi-metric-value-manual-entry-page.js +13 -1
- package/dist-client/pages/kpi-metric-value/kpi-metric-value-manual-entry-page.js.map +1 -1
- package/dist-client/pages/kpi-value/kpi-value-list-page.d.ts +1 -0
- package/dist-client/pages/kpi-value/kpi-value-list-page.js +45 -1
- package/dist-client/pages/kpi-value/kpi-value-list-page.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-server/calculator/evaluator.d.ts +8 -0
- package/dist-server/calculator/evaluator.js +42 -0
- package/dist-server/calculator/evaluator.js.map +1 -0
- package/dist-server/calculator/functions.d.ts +3 -0
- package/dist-server/calculator/functions.js +62 -0
- package/dist-server/calculator/functions.js.map +1 -0
- package/dist-server/calculator/index.d.ts +4 -0
- package/dist-server/calculator/index.js +8 -0
- package/dist-server/calculator/index.js.map +1 -0
- package/dist-server/calculator/parser.d.ts +21 -0
- package/dist-server/calculator/parser.js +121 -0
- package/dist-server/calculator/parser.js.map +1 -0
- package/dist-server/calculator/provider.d.ts +8 -0
- package/dist-server/calculator/provider.js +13 -0
- package/dist-server/calculator/provider.js.map +1 -0
- package/dist-server/controllers/kpi-metric-value-provider.d.ts +11 -0
- package/dist-server/controllers/kpi-metric-value-provider.js +63 -0
- package/dist-server/controllers/kpi-metric-value-provider.js.map +1 -0
- package/dist-server/controllers/kpi-value-provider.d.ts +11 -0
- package/dist-server/controllers/kpi-value-provider.js +46 -0
- package/dist-server/controllers/kpi-value-provider.js.map +1 -0
- package/dist-server/service/index.d.ts +2 -2
- package/dist-server/service/kpi/aggregate-kpi.js +4 -4
- package/dist-server/service/kpi/aggregate-kpi.js.map +1 -1
- package/dist-server/service/kpi/kpi-grade.types.d.ts +11 -10
- package/dist-server/service/kpi/kpi-grade.types.js.map +1 -1
- package/dist-server/service/kpi/kpi-history.d.ts +2 -2
- package/dist-server/service/kpi/kpi-history.js.map +1 -1
- package/dist-server/service/kpi/kpi-mutation.d.ts +2 -0
- package/dist-server/service/kpi/kpi-mutation.js +126 -4
- package/dist-server/service/kpi/kpi-mutation.js.map +1 -1
- package/dist-server/service/kpi/kpi-type.d.ts +8 -5
- package/dist-server/service/kpi/kpi-type.js +22 -8
- package/dist-server/service/kpi/kpi-type.js.map +1 -1
- package/dist-server/service/kpi/kpi.d.ts +6 -3
- package/dist-server/service/kpi/kpi.js +29 -9
- package/dist-server/service/kpi/kpi.js.map +1 -1
- package/dist-server/service/kpi-category/kpi-category-mutation.d.ts +1 -1
- package/dist-server/service/kpi-category/kpi-category-mutation.js +3 -3
- package/dist-server/service/kpi-category/kpi-category-mutation.js.map +1 -1
- package/dist-server/service/kpi-category/kpi-category-query.d.ts +13 -0
- package/dist-server/service/kpi-category/kpi-category-query.js +180 -0
- package/dist-server/service/kpi-category/kpi-category-query.js.map +1 -1
- package/dist-server/service/kpi-category/kpi-category-type.d.ts +3 -0
- package/dist-server/service/kpi-category/kpi-category-type.js +16 -1
- package/dist-server/service/kpi-category/kpi-category-type.js.map +1 -1
- package/dist-server/service/kpi-category/kpi-category.d.ts +2 -0
- package/dist-server/service/kpi-category/kpi-category.js +10 -1
- package/dist-server/service/kpi-category/kpi-category.js.map +1 -1
- package/dist-server/service/kpi-metric/kpi-metric-type.d.ts +5 -3
- package/dist-server/service/kpi-metric/kpi-metric-type.js +5 -3
- package/dist-server/service/kpi-metric/kpi-metric-type.js.map +1 -1
- package/dist-server/service/kpi-metric/kpi-metric.d.ts +2 -8
- package/dist-server/service/kpi-metric/kpi-metric.js +3 -14
- package/dist-server/service/kpi-metric/kpi-metric.js.map +1 -1
- package/dist-server/service/kpi-metric-value/kpi-metric-value-mutation.js +67 -45
- package/dist-server/service/kpi-metric-value/kpi-metric-value-mutation.js.map +1 -1
- package/dist-server/service/kpi-metric-value/kpi-metric-value.js +3 -2
- package/dist-server/service/kpi-metric-value/kpi-metric-value.js.map +1 -1
- package/dist-server/service/kpi-value/kpi-value-mutation.d.ts +2 -1
- package/dist-server/service/kpi-value/kpi-value-mutation.js +114 -6
- package/dist-server/service/kpi-value/kpi-value-mutation.js.map +1 -1
- package/dist-server/service/kpi-value/kpi-value-query.d.ts +0 -2
- package/dist-server/service/kpi-value/kpi-value-query.js +0 -12
- package/dist-server/service/kpi-value/kpi-value-query.js.map +1 -1
- package/dist-server/service/kpi-value/kpi-value-score.service.d.ts +26 -0
- package/dist-server/service/kpi-value/kpi-value-score.service.js +97 -0
- package/dist-server/service/kpi-value/kpi-value-score.service.js.map +1 -0
- package/dist-server/service/kpi-value/kpi-value-type.d.ts +2 -0
- package/dist-server/service/kpi-value/kpi-value-type.js +14 -0
- package/dist-server/service/kpi-value/kpi-value-type.js.map +1 -1
- package/dist-server/service/kpi-value/kpi-value.d.ts +1 -0
- package/dist-server/service/kpi-value/kpi-value.js +9 -1
- package/dist-server/service/kpi-value/kpi-value.js.map +1 -1
- package/dist-server/service/utils/value-date-util.d.ts +3 -0
- package/dist-server/service/utils/value-date-util.js +76 -0
- package/dist-server/service/utils/value-date-util.js.map +1 -0
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/server/calculator/evaluator.ts +45 -0
- package/server/calculator/functions.ts +67 -0
- package/server/calculator/index.ts +4 -0
- package/server/calculator/parser.ts +128 -0
- package/server/calculator/provider.ts +10 -0
- package/server/controllers/kpi-metric-value-provider.ts +66 -0
- package/server/controllers/kpi-value-provider.ts +51 -0
- package/server/service/kpi/aggregate-kpi.ts +4 -4
- package/server/service/kpi/kpi-grade.types.ts +11 -10
- package/server/service/kpi/kpi-history.ts +2 -2
- package/server/service/kpi/kpi-mutation.ts +128 -4
- package/server/service/kpi/kpi-type.ts +21 -9
- package/server/service/kpi/kpi.ts +32 -10
- package/server/service/kpi-category/kpi-category-mutation.ts +3 -3
- package/server/service/kpi-category/kpi-category-query.ts +175 -1
- package/server/service/kpi-category/kpi-category-type.ts +17 -6
- package/server/service/kpi-category/kpi-category.ts +10 -1
- package/server/service/kpi-metric/kpi-metric-type.ts +7 -5
- package/server/service/kpi-metric/kpi-metric.ts +3 -15
- package/server/service/kpi-metric-value/kpi-metric-value-mutation.ts +67 -47
- package/server/service/kpi-metric-value/kpi-metric-value.ts +4 -2
- package/server/service/kpi-value/kpi-value-mutation.ts +110 -6
- package/server/service/kpi-value/kpi-value-query.ts +2 -8
- package/server/service/kpi-value/kpi-value-score.service.ts +112 -0
- package/server/service/kpi-value/kpi-value-type.ts +12 -0
- package/server/service/kpi-value/kpi-value.ts +8 -1
- package/server/service/utils/value-date-util.ts +72 -0
- package/dist-server/service/kpi-value/kpi-value-grade.service.d.ts +0 -34
- package/dist-server/service/kpi-value/kpi-value-grade.service.js +0 -117
- package/dist-server/service/kpi-value/kpi-value-grade.service.js.map +0 -1
- 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 {
|
|
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
|
|
178
|
-
async
|
|
179
|
-
const
|
|
180
|
-
await
|
|
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 {
|
|
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
|
-
}
|