@things-factory/kpi 9.1.19 → 10.0.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/client/pages/kpi/kpi-list-page.ts +339 -525
- package/client/pages/kpi/kpi-tree-page.ts +135 -207
- package/client/pages/kpi-metric/kpi-metric-list-page.ts +146 -226
- package/client/pages/kpi-metric-value/kpi-metric-value-editor-page.ts +187 -295
- package/client/pages/kpi-metric-value/kpi-metric-value-list-page.ts +123 -194
- package/client/pages/kpi-metric-value/kpi-metric-value-manual-entry-page.ts +57 -91
- package/client/pages/kpi-statistic/kpi-statistic-editor-page.ts +180 -278
- package/client/pages/kpi-statistic/kpi-statistic-list-page.ts +186 -286
- package/client/pages/kpi-value/kpi-value-editor-page.ts +189 -292
- package/client/pages/kpi-value/kpi-value-list-page.ts +170 -264
- package/dist-client/pages/kpi/kpi-list-page.d.ts +0 -6
- package/dist-client/pages/kpi/kpi-list-page.js +150 -282
- package/dist-client/pages/kpi/kpi-list-page.js.map +1 -1
- package/dist-client/pages/kpi/kpi-tree-page.d.ts +1 -7
- package/dist-client/pages/kpi/kpi-tree-page.js +76 -127
- package/dist-client/pages/kpi/kpi-tree-page.js.map +1 -1
- package/dist-client/pages/kpi-metric/kpi-metric-list-page.d.ts +0 -6
- package/dist-client/pages/kpi-metric/kpi-metric-list-page.js +62 -116
- package/dist-client/pages/kpi-metric/kpi-metric-list-page.js.map +1 -1
- package/dist-client/pages/kpi-metric-value/kpi-metric-value-editor-page.d.ts +1 -7
- package/dist-client/pages/kpi-metric-value/kpi-metric-value-editor-page.js +82 -140
- package/dist-client/pages/kpi-metric-value/kpi-metric-value-editor-page.js.map +1 -1
- package/dist-client/pages/kpi-metric-value/kpi-metric-value-list-page.d.ts +0 -6
- package/dist-client/pages/kpi-metric-value/kpi-metric-value-list-page.js +54 -98
- 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-page.d.ts +1 -7
- package/dist-client/pages/kpi-metric-value/kpi-metric-value-manual-entry-page.js +30 -57
- package/dist-client/pages/kpi-metric-value/kpi-metric-value-manual-entry-page.js.map +1 -1
- package/dist-client/pages/kpi-statistic/kpi-statistic-editor-page.d.ts +1 -7
- package/dist-client/pages/kpi-statistic/kpi-statistic-editor-page.js +91 -153
- package/dist-client/pages/kpi-statistic/kpi-statistic-editor-page.js.map +1 -1
- package/dist-client/pages/kpi-statistic/kpi-statistic-list-page.d.ts +0 -6
- package/dist-client/pages/kpi-statistic/kpi-statistic-list-page.js +81 -155
- package/dist-client/pages/kpi-statistic/kpi-statistic-list-page.js.map +1 -1
- package/dist-client/pages/kpi-value/kpi-value-editor-page.d.ts +1 -7
- package/dist-client/pages/kpi-value/kpi-value-editor-page.js +80 -136
- package/dist-client/pages/kpi-value/kpi-value-editor-page.js.map +1 -1
- package/dist-client/pages/kpi-value/kpi-value-list-page.d.ts +0 -6
- package/dist-client/pages/kpi-value/kpi-value-list-page.js +73 -134
- package/dist-client/pages/kpi-value/kpi-value-list-page.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-server/service/index.d.ts +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +18 -18
- package/client/tsconfig.json +0 -11
- package/dist-server/tsconfig.json +0 -10
- package/server/@types/index.d.ts +0 -11
- package/server/calculator/evaluator.ts +0 -45
- package/server/calculator/functions.ts +0 -67
- package/server/calculator/index.ts +0 -4
- package/server/calculator/parser.ts +0 -137
- package/server/calculator/provider.ts +0 -10
- package/server/controllers/index.ts +0 -2
- package/server/controllers/kpi-metric-value-provider.ts +0 -79
- package/server/controllers/kpi-value-provider.ts +0 -51
- package/server/index.ts +0 -6
- package/server/migrations/1752190849680-seed-kpi-metrics.ts +0 -124
- package/server/migrations/1752190849681-seed-kpi.ts +0 -356
- package/server/migrations/1752192090123-add-grades-to-kpi.ts +0 -67
- package/server/migrations/1752192090124-add-kpi-statistics.ts +0 -719
- package/server/migrations/1752192090128-seed-kpi-org-scope.ts +0 -132
- package/server/migrations/1752192090129-seed-kpi-values.ts +0 -207
- package/server/migrations/grade-data/x11-performance-table.json +0 -962
- package/server/migrations/grade-data/x12-performance-table.json +0 -611
- package/server/migrations/grade-data/x14-performance-table.json +0 -42
- package/server/migrations/grade-data/x21-performance-table.json +0 -889
- package/server/migrations/grade-data/x22-performance-table.json +0 -1064
- package/server/migrations/grade-data/x23-performance-table.json +0 -42
- package/server/migrations/grade-data/x31-performance-table.json +0 -644
- package/server/migrations/grade-data/x32-performance-table.json +0 -993
- package/server/migrations/grade-data/x33-performance-table.json +0 -195
- package/server/migrations/grade-data/x34-performance-table.json +0 -12
- package/server/migrations/grade-data/x35-performance-table.json +0 -42
- package/server/migrations/grade-data/x41-performance-table.json +0 -825
- package/server/migrations/grade-data/x42-performance-table.json +0 -786
- package/server/migrations/grade-data/x43-performance-table.json +0 -12
- package/server/migrations/grade-data/x44-performance-table.json +0 -42
- package/server/migrations/grade-data/x51-performance-table.json +0 -924
- package/server/migrations/grade-data/x52-performance-table.json +0 -42
- package/server/migrations/grade-data/x61-performance-table.json +0 -261
- package/server/migrations/grade-data/x62-performance-table.json +0 -42
- package/server/migrations/index.ts +0 -9
- package/server/migrations/seed-data/kpi-metrics-seed.json +0 -454
- package/server/migrations/seed-data/kpi-org-scope-seed.json +0 -1676
- package/server/migrations/seed-data/kpi-scopes-seed.json +0 -121
- package/server/migrations/seed-data/kpi-values-seed.json +0 -402
- package/server/migrations/seed-data/kpis-seed.json +0 -488
- package/server/migrations/seed-data/scope-definitions-seed.json +0 -90
- package/server/routes.ts +0 -81
- package/server/service/index.ts +0 -51
- package/server/service/kpi/aggregate-kpi.ts +0 -103
- package/server/service/kpi/event-subscriber.ts +0 -29
- package/server/service/kpi/index.ts +0 -9
- package/server/service/kpi/kpi-formula.service.ts +0 -164
- package/server/service/kpi/kpi-grade.types.ts +0 -28
- package/server/service/kpi/kpi-history.ts +0 -126
- package/server/service/kpi/kpi-mutation.ts +0 -553
- package/server/service/kpi/kpi-query.ts +0 -224
- package/server/service/kpi/kpi-type.ts +0 -151
- package/server/service/kpi/kpi.ts +0 -254
- package/server/service/kpi-alert/index.ts +0 -3
- package/server/service/kpi-alert/kpi-alert-query.ts +0 -59
- package/server/service/kpi-alert/kpi-alert-type.ts +0 -20
- package/server/service/kpi-metric/aggregate-kpi-metric.ts +0 -132
- package/server/service/kpi-metric/index.ts +0 -7
- package/server/service/kpi-metric/kpi-metric-mutation.ts +0 -309
- package/server/service/kpi-metric/kpi-metric-query.ts +0 -70
- package/server/service/kpi-metric/kpi-metric-type.ts +0 -111
- package/server/service/kpi-metric/kpi-metric.ts +0 -134
- package/server/service/kpi-metric-value/index.ts +0 -7
- package/server/service/kpi-metric-value/kpi-metric-value-mutation.ts +0 -270
- package/server/service/kpi-metric-value/kpi-metric-value-query.ts +0 -62
- package/server/service/kpi-metric-value/kpi-metric-value-type.ts +0 -82
- package/server/service/kpi-metric-value/kpi-metric-value.ts +0 -93
- package/server/service/kpi-org-scope/index.ts +0 -6
- package/server/service/kpi-org-scope/kpi-org-scope-mutation.ts +0 -173
- package/server/service/kpi-org-scope/kpi-org-scope-query.ts +0 -127
- package/server/service/kpi-org-scope/kpi-org-scope-type.ts +0 -68
- package/server/service/kpi-org-scope/kpi-org-scope.ts +0 -123
- package/server/service/kpi-scope/index.ts +0 -11
- package/server/service/kpi-scope/kpi-scope-mutation.ts +0 -129
- package/server/service/kpi-scope/kpi-scope-query.ts +0 -63
- package/server/service/kpi-scope/kpi-scope-type.ts +0 -96
- package/server/service/kpi-scope/kpi-scope.ts +0 -143
- package/server/service/kpi-statistic/index.ts +0 -7
- package/server/service/kpi-statistic/kpi-statistic-batch.service.ts +0 -231
- package/server/service/kpi-statistic/kpi-statistic-calculation.service.ts +0 -410
- package/server/service/kpi-statistic/kpi-statistic-mutation.ts +0 -291
- package/server/service/kpi-statistic/kpi-statistic-query.ts +0 -146
- package/server/service/kpi-statistic/kpi-statistic-type.ts +0 -152
- package/server/service/kpi-statistic/kpi-statistic.ts +0 -199
- package/server/service/kpi-value/index.ts +0 -7
- package/server/service/kpi-value/kpi-value-mutation.ts +0 -432
- package/server/service/kpi-value/kpi-value-query.ts +0 -61
- package/server/service/kpi-value/kpi-value-score.service.ts +0 -106
- package/server/service/kpi-value/kpi-value-type.ts +0 -122
- package/server/service/kpi-value/kpi-value.ts +0 -160
- package/server/service/utils/value-date-util.ts +0 -119
- package/server/tsconfig.json +0 -10
- package/server/types/global.d.ts +0 -8
|
@@ -1,224 +0,0 @@
|
|
|
1
|
-
import { IsNull } from 'typeorm'
|
|
2
|
-
import { Resolver, Query, FieldResolver, Root, Args, Arg, Ctx, Directive } from 'type-graphql'
|
|
3
|
-
import { Attachment } from '@things-factory/attachment-base'
|
|
4
|
-
import { Domain, getQueryBuilderFromListParams, getRepository, ListParam } from '@things-factory/shell'
|
|
5
|
-
import { User } from '@things-factory/auth-base'
|
|
6
|
-
import type ResolverContext from '@things-factory/auth-base'
|
|
7
|
-
import { Kpi } from './kpi'
|
|
8
|
-
import { KpiList } from './kpi-type'
|
|
9
|
-
import { KpiValue } from '../kpi-value/kpi-value'
|
|
10
|
-
import { KpiHistory } from './kpi-history'
|
|
11
|
-
import { Int } from 'type-graphql'
|
|
12
|
-
|
|
13
|
-
@Resolver(Kpi)
|
|
14
|
-
export class KpiQuery {
|
|
15
|
-
@Directive('@privilege(category: "kpi", privilege: "query", domainOwnerGranted: true, superUserGranted: true)')
|
|
16
|
-
@Query(returns => [Kpi], { description: 'Fetch root KPIs (KPIs without parent)' })
|
|
17
|
-
async kpisLevel0(@Ctx() context: ResolverContext): Promise<Kpi[]> {
|
|
18
|
-
const { domain } = context.state
|
|
19
|
-
|
|
20
|
-
return await getRepository(Kpi).find({
|
|
21
|
-
where: { domain: { id: domain.id }, parent: null },
|
|
22
|
-
order: { name: 'ASC' }
|
|
23
|
-
})
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
@Directive('@privilege(category: "kpi", privilege: "query", domainOwnerGranted: true, superUserGranted: true)')
|
|
27
|
-
@Query(returns => [Kpi], {
|
|
28
|
-
description:
|
|
29
|
-
'Fetch first level child KPIs. If rootId is provided, get children of that parent. If not, get all level 1 KPIs (children of root KPIs)'
|
|
30
|
-
})
|
|
31
|
-
async kpisLevel1(
|
|
32
|
-
@Arg('rootId', {
|
|
33
|
-
nullable: true,
|
|
34
|
-
description: 'ID of the parent KPI to get level 1 children. If not provided, returns all level 1 KPIs'
|
|
35
|
-
})
|
|
36
|
-
rootId: string,
|
|
37
|
-
@Ctx() context: ResolverContext
|
|
38
|
-
): Promise<Kpi[]> {
|
|
39
|
-
const { domain } = context.state
|
|
40
|
-
|
|
41
|
-
let whereCondition: any = { domain: { id: domain.id } }
|
|
42
|
-
|
|
43
|
-
if (rootId) {
|
|
44
|
-
// 특정 부모의 자식들
|
|
45
|
-
whereCondition.parent = { id: rootId }
|
|
46
|
-
} else {
|
|
47
|
-
// 모든 level 1 KPI들 (root KPI들의 직계 자식들)
|
|
48
|
-
// 부모가 있으면서 부모의 부모가 null인 KPI들
|
|
49
|
-
const kpis = await getRepository(Kpi)
|
|
50
|
-
.createQueryBuilder('kpi')
|
|
51
|
-
.leftJoinAndSelect('kpi.parent', 'parent')
|
|
52
|
-
.leftJoinAndSelect('kpi.children', 'children')
|
|
53
|
-
.leftJoinAndSelect('children.children', 'grandchildren')
|
|
54
|
-
.where('kpi.domain = :domainId', { domainId: domain.id })
|
|
55
|
-
.andWhere('parent.id IS NOT NULL')
|
|
56
|
-
.andWhere('parent.parent IS NULL')
|
|
57
|
-
.orderBy('kpi.name', 'ASC')
|
|
58
|
-
.getMany()
|
|
59
|
-
|
|
60
|
-
return kpis
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// rootId 제공된 경우
|
|
64
|
-
const kpis = await getRepository(Kpi).find({
|
|
65
|
-
where: whereCondition,
|
|
66
|
-
relations: ['children', 'children.children'],
|
|
67
|
-
order: { name: 'ASC' }
|
|
68
|
-
})
|
|
69
|
-
|
|
70
|
-
return kpis
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
@Directive('@privilege(category: "kpi", privilege: "query", domainOwnerGranted: true, superUserGranted: true)')
|
|
74
|
-
@Query(returns => Kpi!, { nullable: true, description: 'Fetch a single KPI by its unique identifier.' })
|
|
75
|
-
async kpi(
|
|
76
|
-
@Arg('id', { description: 'Unique identifier of the KPI to fetch.' }) id: string,
|
|
77
|
-
@Ctx() context: ResolverContext
|
|
78
|
-
): Promise<Kpi> {
|
|
79
|
-
const { domain } = context.state
|
|
80
|
-
|
|
81
|
-
return await getRepository(Kpi).findOne({
|
|
82
|
-
where: { domain: { id: domain.id }, id }
|
|
83
|
-
})
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
@Directive('@privilege(category: "kpi", privilege: "query", domainOwnerGranted: true, superUserGranted: true)')
|
|
87
|
-
@Query(returns => KpiList, { description: 'To fetch multiple Kpis' })
|
|
88
|
-
async kpis(@Args(type => ListParam) params: ListParam, @Ctx() context: ResolverContext): Promise<KpiList> {
|
|
89
|
-
const { domain } = context.state
|
|
90
|
-
|
|
91
|
-
const queryBuilder = getQueryBuilderFromListParams({
|
|
92
|
-
domain,
|
|
93
|
-
params,
|
|
94
|
-
repository: await getRepository(Kpi),
|
|
95
|
-
searchables: ['name', 'description'],
|
|
96
|
-
filtersMap: {
|
|
97
|
-
parent: { columnName: 'id', relationColumn: 'parent' }
|
|
98
|
-
}
|
|
99
|
-
})
|
|
100
|
-
|
|
101
|
-
const [items, total] = await queryBuilder.getManyAndCount()
|
|
102
|
-
|
|
103
|
-
return { items, total }
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
@FieldResolver(type => [Kpi])
|
|
107
|
-
children(@Root() kpi: Kpi): Promise<Kpi[]> {
|
|
108
|
-
return getRepository(Kpi).find({
|
|
109
|
-
where: { parent: { id: kpi.id } }
|
|
110
|
-
})
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
@FieldResolver(type => String)
|
|
114
|
-
async thumbnail(@Root() kpi: Kpi): Promise<string | undefined> {
|
|
115
|
-
const attachment: Attachment = await getRepository(Attachment).findOne({
|
|
116
|
-
where: {
|
|
117
|
-
domain: { id: kpi.domainId },
|
|
118
|
-
refType: Kpi.name,
|
|
119
|
-
refBy: kpi.id
|
|
120
|
-
}
|
|
121
|
-
})
|
|
122
|
-
|
|
123
|
-
return attachment?.fullpath
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
@FieldResolver(type => Domain)
|
|
127
|
-
async domain(@Root() kpi: Kpi): Promise<Domain> {
|
|
128
|
-
return kpi.domainId && (await getRepository(Domain).findOneBy({ id: kpi.domainId }))
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
@FieldResolver(type => User)
|
|
132
|
-
async updater(@Root() kpi: Kpi): Promise<User> {
|
|
133
|
-
return kpi.updaterId && (await getRepository(User).findOneBy({ id: kpi.updaterId }))
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
@FieldResolver(type => User)
|
|
137
|
-
async creator(@Root() kpi: Kpi): Promise<User> {
|
|
138
|
-
return kpi.creatorId && (await getRepository(User).findOneBy({ id: kpi.creatorId }))
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
@FieldResolver(type => KpiValue, { nullable: true })
|
|
142
|
-
async value(
|
|
143
|
-
@Root() kpi: Kpi,
|
|
144
|
-
@Ctx() context,
|
|
145
|
-
@Arg('orgScope', { nullable: true, description: 'Organization scope filter for value' }) orgScope?: string
|
|
146
|
-
): Promise<KpiValue | null> {
|
|
147
|
-
const { domain } = context.state
|
|
148
|
-
|
|
149
|
-
let whereCondition: any = {
|
|
150
|
-
domain: { id: domain.id },
|
|
151
|
-
kpi: { id: kpi.id }
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// orgScope 필터가 있으면 KpiOrgScope로 필터링
|
|
155
|
-
if (orgScope) {
|
|
156
|
-
whereCondition.kpiOrgScope = { org: orgScope }
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
return await getRepository(KpiValue).findOne({
|
|
160
|
-
where: whereCondition,
|
|
161
|
-
relations: ['kpiOrgScope'],
|
|
162
|
-
order: { valueDate: 'DESC' }
|
|
163
|
-
})
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
@FieldResolver(type => Number, { nullable: true })
|
|
167
|
-
targetValue(@Root() kpi: Kpi): number | undefined {
|
|
168
|
-
return kpi.vizMeta?.targetValue
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
@FieldResolver(type => String, { nullable: true })
|
|
172
|
-
unit(@Root() kpi: Kpi): string | undefined {
|
|
173
|
-
return kpi.vizMeta?.unit
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
@FieldResolver(type => Kpi, { nullable: true })
|
|
177
|
-
async parent(@Root() kpi: Kpi): Promise<Kpi | null> {
|
|
178
|
-
if (!kpi.parentId) return null
|
|
179
|
-
return await getRepository(Kpi).findOneBy({ id: kpi.parentId })
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
@FieldResolver(type => [KpiHistory], { nullable: true })
|
|
183
|
-
async histories(
|
|
184
|
-
@Root() kpi: Kpi,
|
|
185
|
-
@Ctx() context,
|
|
186
|
-
@Arg('limit', type => Int, { nullable: true }) limit?: number
|
|
187
|
-
): Promise<KpiHistory[]> {
|
|
188
|
-
const { domain } = context.state
|
|
189
|
-
const qb = getRepository(KpiHistory)
|
|
190
|
-
.createQueryBuilder('history')
|
|
191
|
-
.where('history.domain = :domainId', { domainId: domain.id })
|
|
192
|
-
.andWhere('history.originalId = :kpiId', { kpiId: kpi.id })
|
|
193
|
-
.orderBy('history.updatedAt', 'DESC')
|
|
194
|
-
if (limit) qb.limit(limit)
|
|
195
|
-
else qb.limit(1)
|
|
196
|
-
return await qb.getMany()
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
@Directive('@privilege(category: "kpi", privilege: "query", domainOwnerGranted: true, superUserGranted: true)')
|
|
200
|
-
@Query(returns => [Kpi], { description: 'Fetch KPIs in hierarchical tree structure' })
|
|
201
|
-
async kpiTree(@Ctx() context: ResolverContext): Promise<Kpi[]> {
|
|
202
|
-
const { domain } = context.state
|
|
203
|
-
|
|
204
|
-
return await getRepository(Kpi).find({
|
|
205
|
-
where: { domain: { id: domain.id }, parent: IsNull() },
|
|
206
|
-
relations: ['children', 'children.children', 'children.children.children'],
|
|
207
|
-
order: { name: 'ASC' }
|
|
208
|
-
})
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
@Directive('@privilege(category: "kpi", privilege: "query", domainOwnerGranted: true, superUserGranted: true)')
|
|
212
|
-
@Query(returns => [Kpi], { description: 'Fetch child KPIs of a given parent KPI' })
|
|
213
|
-
async kpiChildren(
|
|
214
|
-
@Arg('parentId', { description: 'ID of the parent KPI' }) parentId: string,
|
|
215
|
-
@Ctx() context: ResolverContext
|
|
216
|
-
): Promise<Kpi[]> {
|
|
217
|
-
const { domain } = context.state
|
|
218
|
-
|
|
219
|
-
return await getRepository(Kpi).find({
|
|
220
|
-
where: { domain: { id: domain.id }, parent: { id: parentId } },
|
|
221
|
-
order: { name: 'ASC' }
|
|
222
|
-
})
|
|
223
|
-
}
|
|
224
|
-
}
|
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
import type { FileUpload } from 'graphql-upload/GraphQLUpload.js'
|
|
2
|
-
import GraphQLUpload from 'graphql-upload/GraphQLUpload.js'
|
|
3
|
-
import { ObjectType, Field, InputType, Int, ID, registerEnumType } from 'type-graphql'
|
|
4
|
-
|
|
5
|
-
import { ObjectRef, ScalarObject } from '@things-factory/shell'
|
|
6
|
-
|
|
7
|
-
import { Kpi, KpiStatus, KpiVizType } from './kpi'
|
|
8
|
-
import { KpiScores } from './kpi-grade.types'
|
|
9
|
-
|
|
10
|
-
@InputType({ description: 'Input type for creating a new KPI. Used in mutations to provide KPI details.' })
|
|
11
|
-
export class NewKpi {
|
|
12
|
-
@Field({ description: 'Name of the KPI.' })
|
|
13
|
-
name: string
|
|
14
|
-
|
|
15
|
-
@Field({ nullable: true, description: 'Detailed description of the KPI.' })
|
|
16
|
-
description?: string
|
|
17
|
-
|
|
18
|
-
@Field(type => ObjectRef, { nullable: true, description: 'Reference to the parent KPI in hierarchical structure.' })
|
|
19
|
-
parent?: ObjectRef
|
|
20
|
-
|
|
21
|
-
@Field({ nullable: true, description: 'Indicates whether this KPI is a leaf node (has actual measured values).' })
|
|
22
|
-
isLeaf?: boolean
|
|
23
|
-
|
|
24
|
-
@Field({ nullable: true, description: 'Calculation formula for the KPI, using metric codes and operators.' })
|
|
25
|
-
formula?: string
|
|
26
|
-
|
|
27
|
-
@Field({ nullable: true, description: 'Indicates whether this KPI is active and usable.' })
|
|
28
|
-
active?: boolean
|
|
29
|
-
|
|
30
|
-
@Field(type => KpiStatus, { nullable: true, description: 'Current state of the KPI (DRAFT, RELEASED, ARCHIVED).' })
|
|
31
|
-
state?: KpiStatus
|
|
32
|
-
|
|
33
|
-
@Field(type => GraphQLUpload, { nullable: true, description: 'Thumbnail image or file for this KPI.' })
|
|
34
|
-
thumbnail?: FileUpload
|
|
35
|
-
|
|
36
|
-
@Field(type => KpiVizType, {
|
|
37
|
-
nullable: true,
|
|
38
|
-
description: 'Visualization type for this KPI (e.g., CARD, GAUGE, PROGRESS, BAR, LINE, etc.).'
|
|
39
|
-
})
|
|
40
|
-
vizType?: KpiVizType
|
|
41
|
-
|
|
42
|
-
@Field(type => ScalarObject, {
|
|
43
|
-
nullable: true,
|
|
44
|
-
description:
|
|
45
|
-
'Visualization options and metadata for this KPI, such as color, icon, thresholds, unit, decimal places, etc.'
|
|
46
|
-
})
|
|
47
|
-
vizMeta?: any
|
|
48
|
-
|
|
49
|
-
@Field({
|
|
50
|
-
nullable: true,
|
|
51
|
-
description: 'Cron schedule string for periodic KPI value aggregation (e.g., "0 0 * * *" for daily).'
|
|
52
|
-
})
|
|
53
|
-
schedule?: string
|
|
54
|
-
|
|
55
|
-
@Field({ nullable: true, description: 'Timezone for the KPI schedule.' })
|
|
56
|
-
timezone?: string
|
|
57
|
-
|
|
58
|
-
@Field({ nullable: true, description: 'Weight for aggregation in parent category.' })
|
|
59
|
-
weight?: number
|
|
60
|
-
|
|
61
|
-
@Field(type => ScalarObject, {
|
|
62
|
-
nullable: true,
|
|
63
|
-
description: 'Score lookup table for this KPI version'
|
|
64
|
-
})
|
|
65
|
-
grades?: KpiScores
|
|
66
|
-
|
|
67
|
-
@Field({
|
|
68
|
-
nullable: true,
|
|
69
|
-
description: 'Score calculation formula for this KPI. Converts KPI value to performance score (0-1).'
|
|
70
|
-
})
|
|
71
|
-
scoreFormula?: string
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
@InputType({ description: 'Input type for updating an existing KPI. Used in mutations to patch KPI details.' })
|
|
75
|
-
export class KpiPatch {
|
|
76
|
-
@Field(type => ID, { nullable: true, description: 'ID of the KPI to update.' })
|
|
77
|
-
id?: string
|
|
78
|
-
|
|
79
|
-
@Field({ nullable: true, description: 'Name of the KPI.' })
|
|
80
|
-
name?: string
|
|
81
|
-
|
|
82
|
-
@Field({ nullable: true, description: 'Detailed description of the KPI.' })
|
|
83
|
-
description?: string
|
|
84
|
-
|
|
85
|
-
@Field(type => ObjectRef, { nullable: true, description: 'Reference to the parent KPI in hierarchical structure.' })
|
|
86
|
-
parent?: ObjectRef
|
|
87
|
-
|
|
88
|
-
@Field({ nullable: true, description: 'Indicates whether this KPI is a leaf node (has actual measured values).' })
|
|
89
|
-
isLeaf?: boolean
|
|
90
|
-
|
|
91
|
-
@Field({ nullable: true, description: 'Calculation formula for the KPI, using metric codes and operators.' })
|
|
92
|
-
formula?: string
|
|
93
|
-
|
|
94
|
-
@Field({ nullable: true, description: 'Indicates whether this KPI is active and usable.' })
|
|
95
|
-
active?: boolean
|
|
96
|
-
|
|
97
|
-
@Field(type => KpiStatus, { nullable: true, description: 'Current state of the KPI (DRAFT, RELEASED, ARCHIVED).' })
|
|
98
|
-
state?: KpiStatus
|
|
99
|
-
|
|
100
|
-
@Field(type => GraphQLUpload, { nullable: true, description: 'Thumbnail image or file for this KPI.' })
|
|
101
|
-
thumbnail?: FileUpload
|
|
102
|
-
|
|
103
|
-
@Field(type => KpiVizType, {
|
|
104
|
-
nullable: true,
|
|
105
|
-
description: 'Visualization type for this KPI (e.g., CARD, GAUGE, PROGRESS, BAR, LINE, etc.).'
|
|
106
|
-
})
|
|
107
|
-
vizType?: KpiVizType
|
|
108
|
-
|
|
109
|
-
@Field(type => ScalarObject, {
|
|
110
|
-
nullable: true,
|
|
111
|
-
description:
|
|
112
|
-
'Visualization options and metadata for this KPI, such as color, icon, thresholds, unit, decimal places, etc.'
|
|
113
|
-
})
|
|
114
|
-
vizMeta?: any
|
|
115
|
-
|
|
116
|
-
@Field({ nullable: true, description: 'Custom flag for update operations (internal use).' })
|
|
117
|
-
cuFlag?: string
|
|
118
|
-
|
|
119
|
-
@Field({
|
|
120
|
-
nullable: true,
|
|
121
|
-
description: 'Cron schedule string for periodic KPI value aggregation (e.g., "0 0 * * *" for daily).'
|
|
122
|
-
})
|
|
123
|
-
schedule?: string
|
|
124
|
-
|
|
125
|
-
@Field({ nullable: true, description: 'Timezone for the KPI schedule.' })
|
|
126
|
-
timezone?: string
|
|
127
|
-
|
|
128
|
-
@Field({ nullable: true, description: 'Weight for aggregation in parent category.' })
|
|
129
|
-
weight?: number
|
|
130
|
-
|
|
131
|
-
@Field(type => ScalarObject, {
|
|
132
|
-
nullable: true,
|
|
133
|
-
description: 'Score lookup table for this KPI version'
|
|
134
|
-
})
|
|
135
|
-
grades?: KpiScores
|
|
136
|
-
|
|
137
|
-
@Field({
|
|
138
|
-
nullable: true,
|
|
139
|
-
description: 'Score calculation formula for this KPI. Converts KPI value to performance score (0-1).'
|
|
140
|
-
})
|
|
141
|
-
scoreFormula?: string
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
@ObjectType()
|
|
145
|
-
export class KpiList {
|
|
146
|
-
@Field(type => [Kpi])
|
|
147
|
-
items: Kpi[]
|
|
148
|
-
|
|
149
|
-
@Field(type => Int)
|
|
150
|
-
total: number
|
|
151
|
-
}
|
|
@@ -1,254 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
CreateDateColumn,
|
|
3
|
-
UpdateDateColumn,
|
|
4
|
-
DeleteDateColumn,
|
|
5
|
-
Entity,
|
|
6
|
-
Index,
|
|
7
|
-
Column,
|
|
8
|
-
RelationId,
|
|
9
|
-
ManyToOne,
|
|
10
|
-
OneToMany,
|
|
11
|
-
VersionColumn,
|
|
12
|
-
PrimaryGeneratedColumn
|
|
13
|
-
} from 'typeorm'
|
|
14
|
-
import { ObjectType, Field, Int, ID, registerEnumType } from 'type-graphql'
|
|
15
|
-
|
|
16
|
-
import { Domain } from '@things-factory/shell'
|
|
17
|
-
import { User } from '@things-factory/auth-base'
|
|
18
|
-
import { config } from '@things-factory/env'
|
|
19
|
-
import { ScalarObject } from '@things-factory/shell'
|
|
20
|
-
|
|
21
|
-
import { KpiScores } from './kpi-grade.types'
|
|
22
|
-
|
|
23
|
-
const ORMCONFIG = config.get('ormconfig', {})
|
|
24
|
-
const DATABASE_TYPE = ORMCONFIG.type
|
|
25
|
-
|
|
26
|
-
export enum KpiStatus {
|
|
27
|
-
DRAFT = 'DRAFT',
|
|
28
|
-
RELEASE = 'RELEASE',
|
|
29
|
-
ARCHIVED = 'ARCHIVED'
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export enum KpiVizType {
|
|
33
|
-
CARD = 'CARD',
|
|
34
|
-
GAUGE = 'GAUGE',
|
|
35
|
-
PROGRESS = 'PROGRESS',
|
|
36
|
-
BAR = 'BAR',
|
|
37
|
-
LINE = 'LINE',
|
|
38
|
-
PIE = 'PIE',
|
|
39
|
-
DONUT = 'DONUT',
|
|
40
|
-
RADAR = 'RADAR',
|
|
41
|
-
BULLET = 'BULLET',
|
|
42
|
-
THERMOMETER = 'THERMOMETER',
|
|
43
|
-
SPEEDOMETER = 'SPEEDOMETER',
|
|
44
|
-
ICON = 'ICON',
|
|
45
|
-
BADGE = 'BADGE',
|
|
46
|
-
TEXT = 'TEXT',
|
|
47
|
-
TABLE = 'TABLE'
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export enum KpiPeriodType {
|
|
51
|
-
DAY = 'DAY',
|
|
52
|
-
WEEK = 'WEEK',
|
|
53
|
-
MONTH = 'MONTH',
|
|
54
|
-
QUARTER = 'QUARTER',
|
|
55
|
-
YEAR = 'YEAR',
|
|
56
|
-
RANGE = 'RANGE',
|
|
57
|
-
ALLTIME = 'ALLTIME'
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
registerEnumType(KpiStatus, {
|
|
61
|
-
name: 'KpiStatus',
|
|
62
|
-
description: 'State enumeration of a KPI (DRAFT, RELEASED, ARCHIVED)'
|
|
63
|
-
})
|
|
64
|
-
|
|
65
|
-
registerEnumType(KpiVizType, {
|
|
66
|
-
name: 'KpiVizType',
|
|
67
|
-
description: 'Visualization type for KPI display (CARD, GAUGE, PROGRESS, etc.)'
|
|
68
|
-
})
|
|
69
|
-
|
|
70
|
-
registerEnumType(KpiPeriodType, {
|
|
71
|
-
name: 'KpiPeriodType',
|
|
72
|
-
description: 'Aggregation period type for KPI (DAY, WEEK, MONTH, QUARTER, RANGE, ALLTIME)'
|
|
73
|
-
})
|
|
74
|
-
|
|
75
|
-
@Entity()
|
|
76
|
-
@Index('ix_kpi_0', (kpi: Kpi) => [kpi.domain, kpi.name], {
|
|
77
|
-
where: '"deleted_at" IS NULL',
|
|
78
|
-
unique: true
|
|
79
|
-
})
|
|
80
|
-
@Index('ix_kpi_hierarchy', (kpi: Kpi) => [kpi.domain, kpi.parent], {
|
|
81
|
-
where: '"deleted_at" IS NULL'
|
|
82
|
-
})
|
|
83
|
-
@ObjectType({
|
|
84
|
-
description:
|
|
85
|
-
'KPI entity. Represents a key performance indicator with calculation formula, target, category, and other attributes.'
|
|
86
|
-
})
|
|
87
|
-
export class Kpi {
|
|
88
|
-
@PrimaryGeneratedColumn('uuid')
|
|
89
|
-
@Field(type => ID, { description: 'Unique identifier for this KPI.' })
|
|
90
|
-
readonly id: string
|
|
91
|
-
|
|
92
|
-
@VersionColumn()
|
|
93
|
-
@Field({
|
|
94
|
-
nullable: true,
|
|
95
|
-
description:
|
|
96
|
-
'Version number of the KPI. Increments on each modification. When the KPI is released, a snapshot is saved in kpi-history and the status becomes RELEASED. Editing after release increases the version and sets status to DRAFT.'
|
|
97
|
-
})
|
|
98
|
-
version?: number = 1
|
|
99
|
-
|
|
100
|
-
@ManyToOne(type => Domain)
|
|
101
|
-
@Field({ nullable: true, description: 'Domain (tenant) to which this KPI belongs.' })
|
|
102
|
-
domain?: Domain
|
|
103
|
-
|
|
104
|
-
@RelationId((kpi: Kpi) => kpi.domain)
|
|
105
|
-
@Field({ nullable: true, description: 'ID of the domain (tenant) for this KPI.' })
|
|
106
|
-
domainId?: string
|
|
107
|
-
|
|
108
|
-
@Column()
|
|
109
|
-
@Field({ nullable: true, description: 'Name of the KPI.' })
|
|
110
|
-
name?: string
|
|
111
|
-
|
|
112
|
-
@Column({ nullable: true })
|
|
113
|
-
@Field({ nullable: true, description: 'Detailed description of the KPI.' })
|
|
114
|
-
description?: string
|
|
115
|
-
|
|
116
|
-
@ManyToOne(() => Kpi, { nullable: true })
|
|
117
|
-
@Field(type => Kpi, { nullable: true, description: 'Parent KPI in hierarchical structure.' })
|
|
118
|
-
parent?: Kpi
|
|
119
|
-
|
|
120
|
-
@RelationId((kpi: Kpi) => kpi.parent)
|
|
121
|
-
@Field({ nullable: true, description: 'ID of the parent KPI.' })
|
|
122
|
-
parentId?: string
|
|
123
|
-
|
|
124
|
-
@OneToMany(() => Kpi, kpi => kpi.parent)
|
|
125
|
-
@Field(type => [Kpi], { nullable: true, description: 'Child KPIs in hierarchical structure.' })
|
|
126
|
-
children?: Kpi[]
|
|
127
|
-
|
|
128
|
-
@Column({ nullable: false, default: true })
|
|
129
|
-
@Field({ nullable: true, description: 'Indicates whether this KPI is a leaf node (has actual measured values).' })
|
|
130
|
-
isLeaf?: boolean
|
|
131
|
-
|
|
132
|
-
@Column({
|
|
133
|
-
nullable: true,
|
|
134
|
-
type:
|
|
135
|
-
DATABASE_TYPE == 'mysql' || DATABASE_TYPE == 'mariadb'
|
|
136
|
-
? 'longtext'
|
|
137
|
-
: DATABASE_TYPE == 'oracle'
|
|
138
|
-
? 'clob'
|
|
139
|
-
: DATABASE_TYPE == 'mssql'
|
|
140
|
-
? 'nvarchar'
|
|
141
|
-
: 'varchar',
|
|
142
|
-
length: DATABASE_TYPE == 'mssql' ? 'MAX' : undefined
|
|
143
|
-
})
|
|
144
|
-
@Field({
|
|
145
|
-
nullable: true,
|
|
146
|
-
description:
|
|
147
|
-
'Calculation formula for the KPI. Expressed as a string using metric codes and operators, e.g., "defect_count / total_count * 100".'
|
|
148
|
-
})
|
|
149
|
-
formula?: string
|
|
150
|
-
|
|
151
|
-
@Column({ nullable: false, default: false })
|
|
152
|
-
@Field({ nullable: true, description: 'Indicates whether this KPI is active and usable.' })
|
|
153
|
-
active?: boolean
|
|
154
|
-
|
|
155
|
-
@Column({ nullable: true })
|
|
156
|
-
@Field({ nullable: true, description: 'Current state of the KPI (DRAFT, RELEASED, ARCHIVED).' })
|
|
157
|
-
state?: KpiStatus
|
|
158
|
-
|
|
159
|
-
@Column({ nullable: true })
|
|
160
|
-
@Field(type => KpiVizType, {
|
|
161
|
-
nullable: true,
|
|
162
|
-
description: 'Visualization type for this KPI (e.g., CARD, GAUGE, PROGRESS, BAR, LINE, etc.).'
|
|
163
|
-
})
|
|
164
|
-
vizType?: KpiVizType
|
|
165
|
-
|
|
166
|
-
@Column({ type: 'json', nullable: true })
|
|
167
|
-
@Field(type => ScalarObject, {
|
|
168
|
-
nullable: true,
|
|
169
|
-
description:
|
|
170
|
-
'Visualization options and metadata for this KPI, such as color, icon, thresholds, unit, decimal places, min/max values, etc.'
|
|
171
|
-
})
|
|
172
|
-
vizMeta?: any
|
|
173
|
-
|
|
174
|
-
@Column({ nullable: true })
|
|
175
|
-
@Field({
|
|
176
|
-
nullable: true,
|
|
177
|
-
description: 'Cron schedule string for periodic KPI value aggregation (e.g., "0 0 * * *" for daily).'
|
|
178
|
-
})
|
|
179
|
-
schedule?: string
|
|
180
|
-
|
|
181
|
-
@Column({ nullable: true })
|
|
182
|
-
@Field({ nullable: true, description: 'Schedule ID for the KPI (used for scheduler registration).' })
|
|
183
|
-
scheduleId?: string
|
|
184
|
-
|
|
185
|
-
@Column({ nullable: true })
|
|
186
|
-
@Field({ nullable: true, description: 'Timezone for the KPI schedule.' })
|
|
187
|
-
timezone?: string
|
|
188
|
-
|
|
189
|
-
@Column({ default: 'DAY' })
|
|
190
|
-
@Field(type => KpiPeriodType, { description: 'Aggregation period type for this KPI.' })
|
|
191
|
-
periodType: KpiPeriodType
|
|
192
|
-
|
|
193
|
-
@Column({ type: 'float', nullable: true, default: 1 })
|
|
194
|
-
@Field({ nullable: true, description: 'Weight for aggregation in parent category.' })
|
|
195
|
-
weight?: number
|
|
196
|
-
|
|
197
|
-
@Column({ type: 'simple-json', nullable: true })
|
|
198
|
-
@Field(type => ScalarObject, {
|
|
199
|
-
nullable: true,
|
|
200
|
-
description:
|
|
201
|
-
'Performance index lookup table for complex transformations. @deprecated 향후 제거 예정. performanceFormula 사용 권장.'
|
|
202
|
-
})
|
|
203
|
-
grades?: KpiScores
|
|
204
|
-
|
|
205
|
-
@Column({
|
|
206
|
-
nullable: true,
|
|
207
|
-
type:
|
|
208
|
-
DATABASE_TYPE == 'mysql' || DATABASE_TYPE == 'mariadb'
|
|
209
|
-
? 'longtext'
|
|
210
|
-
: DATABASE_TYPE == 'oracle'
|
|
211
|
-
? 'clob'
|
|
212
|
-
: DATABASE_TYPE == 'mssql'
|
|
213
|
-
? 'nvarchar'
|
|
214
|
-
: 'varchar',
|
|
215
|
-
length: DATABASE_TYPE == 'mssql' ? 'MAX' : undefined
|
|
216
|
-
})
|
|
217
|
-
@Field({
|
|
218
|
-
nullable: true,
|
|
219
|
-
description:
|
|
220
|
-
'Score calculation formula for this KPI. Converts KPI value to performance score (0-100). For complex mappings, use grades lookup table instead. Example: "if(value >= 90, 100, if(value >= 80, 85, 70))" or "value * 0.8 + 20"'
|
|
221
|
-
})
|
|
222
|
-
scoreFormula?: string
|
|
223
|
-
|
|
224
|
-
@CreateDateColumn()
|
|
225
|
-
@Field({ nullable: true, description: 'Timestamp when this KPI was created.' })
|
|
226
|
-
createdAt?: Date
|
|
227
|
-
|
|
228
|
-
@UpdateDateColumn()
|
|
229
|
-
@Field({ nullable: true, description: 'Timestamp when this KPI was last updated.' })
|
|
230
|
-
updatedAt?: Date
|
|
231
|
-
|
|
232
|
-
@DeleteDateColumn()
|
|
233
|
-
@Field({ nullable: true, description: 'Timestamp when this KPI was deleted (soft delete).' })
|
|
234
|
-
deletedAt?: Date
|
|
235
|
-
|
|
236
|
-
@ManyToOne(type => User, { nullable: true })
|
|
237
|
-
@Field(type => User, { nullable: true, description: 'User who created this KPI.' })
|
|
238
|
-
creator?: User
|
|
239
|
-
|
|
240
|
-
@RelationId((kpi: Kpi) => kpi.creator)
|
|
241
|
-
@Field({ nullable: true, description: 'ID of the user who created this KPI.' })
|
|
242
|
-
creatorId?: string
|
|
243
|
-
|
|
244
|
-
@ManyToOne(type => User, { nullable: true })
|
|
245
|
-
@Field(type => User, { nullable: true, description: 'User who last updated this KPI.' })
|
|
246
|
-
updater?: User
|
|
247
|
-
|
|
248
|
-
@RelationId((kpi: Kpi) => kpi.updater)
|
|
249
|
-
@Field({ nullable: true, description: 'ID of the user who last updated this KPI.' })
|
|
250
|
-
updaterId?: string
|
|
251
|
-
|
|
252
|
-
@Field(type => String, { nullable: true, description: 'Thumbnail image or file path for this KPI.' })
|
|
253
|
-
thumbnail?: string
|
|
254
|
-
}
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
import { Directive, Resolver, Query, Ctx } from 'type-graphql'
|
|
2
|
-
import { getRepository } from '@things-factory/shell'
|
|
3
|
-
import { Kpi } from '../kpi/kpi'
|
|
4
|
-
import { KpiValue } from '../kpi-value/kpi-value'
|
|
5
|
-
import { KpiAlert } from './kpi-alert-type'
|
|
6
|
-
|
|
7
|
-
@Resolver()
|
|
8
|
-
export class KpiAlertQuery {
|
|
9
|
-
@Directive('@privilege(category: "kpi", privilege: "query", domainOwnerGranted: true, superUserGranted: true)')
|
|
10
|
-
@Query(returns => [KpiAlert], { description: 'KPI 실적/등급/목표 미달 등 경고/알림 리스트 반환' })
|
|
11
|
-
async kpiAlerts(@Ctx() context): Promise<KpiAlert[]> {
|
|
12
|
-
const { domain } = context.state
|
|
13
|
-
// 1. KPI 전체 조회
|
|
14
|
-
const kpis = await getRepository(Kpi).find({ where: { domain: { id: domain.id } } })
|
|
15
|
-
// 2. KPI별 최신 실적값 조회
|
|
16
|
-
const alerts: KpiAlert[] = []
|
|
17
|
-
for (const kpi of kpis) {
|
|
18
|
-
const value = await getRepository(KpiValue).findOne({
|
|
19
|
-
where: { domain: { id: domain.id }, kpi: { id: kpi.id } },
|
|
20
|
-
order: { valueDate: 'DESC' }
|
|
21
|
-
})
|
|
22
|
-
if (!value) {
|
|
23
|
-
alerts.push({
|
|
24
|
-
id: `${kpi.id}-no-value`,
|
|
25
|
-
kpi,
|
|
26
|
-
message: `${kpi.name} 실적값 미입력`,
|
|
27
|
-
level: 'info',
|
|
28
|
-
createdAt: new Date()
|
|
29
|
-
})
|
|
30
|
-
continue
|
|
31
|
-
}
|
|
32
|
-
// 목표값(예: kpi.vizMeta?.targetValue) 기준 경고
|
|
33
|
-
const target = kpi.vizMeta?.targetValue
|
|
34
|
-
if (target !== undefined && value.value < target) {
|
|
35
|
-
alerts.push({
|
|
36
|
-
id: `${kpi.id}-target-miss`,
|
|
37
|
-
kpi,
|
|
38
|
-
message: `${kpi.name} 실적(${value.value})이 목표(${target}) 미달`,
|
|
39
|
-
level: 'warning',
|
|
40
|
-
createdAt: new Date()
|
|
41
|
-
})
|
|
42
|
-
}
|
|
43
|
-
// 등급 기준 경고(예: 최하위 등급)
|
|
44
|
-
// if (kpi.grades?.length) {
|
|
45
|
-
// const grade = kpi.grades.find(g => value.value >= g.minValue && value.value <= g.maxValue)
|
|
46
|
-
// if (grade && grade.score !== undefined && grade.score <= 2) {
|
|
47
|
-
// alerts.push({
|
|
48
|
-
// id: `${kpi.id}-grade-low`,
|
|
49
|
-
// kpi,
|
|
50
|
-
// message: `${kpi.name} 등급(${grade.name})(${grade.score}점)`,
|
|
51
|
-
// level: 'critical',
|
|
52
|
-
// createdAt: new Date()
|
|
53
|
-
// })
|
|
54
|
-
// }
|
|
55
|
-
// }
|
|
56
|
-
}
|
|
57
|
-
return alerts
|
|
58
|
-
}
|
|
59
|
-
}
|