@things-factory/kpi 9.0.17 → 9.0.19
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/bootstrap.ts +8 -0
- package/client/pages/kpi/kpi-list-page.ts +99 -11
- package/client/pages/kpi/kpi-viz-editor.ts +214 -14
- package/client/pages/kpi-category/kpi-category-list-page.ts +80 -8
- package/client/pages/kpi-history/kpi-history-list-page.ts +1 -1
- package/client/pages/kpi-metric/kpi-metric-list-page.ts +31 -7
- package/client/pages/kpi-metric-value/kpi-metric-value-importer.ts +65 -0
- package/client/pages/kpi-metric-value/kpi-metric-value-list-page.ts +299 -0
- package/client/pages/{kpi-value/kpi-value-manual-entry-form.ts → kpi-metric-value/kpi-metric-value-manual-entry-form.ts} +18 -44
- package/client/pages/{kpi-value/kpi-value-manual-entry-page.ts → kpi-metric-value/kpi-metric-value-manual-entry-page.ts} +21 -21
- package/client/pages/kpi-value/kpi-value-list-page.ts +4 -6
- package/client/route.ts +6 -2
- package/dist-client/bootstrap.d.ts +2 -0
- package/dist-client/bootstrap.js +7 -0
- package/dist-client/bootstrap.js.map +1 -0
- package/dist-client/pages/kpi/kpi-list-page.d.ts +6 -0
- package/dist-client/pages/kpi/kpi-list-page.js +100 -11
- package/dist-client/pages/kpi/kpi-list-page.js.map +1 -1
- package/dist-client/pages/kpi/kpi-viz-editor.js +208 -14
- package/dist-client/pages/kpi/kpi-viz-editor.js.map +1 -1
- package/dist-client/pages/kpi-category/kpi-category-list-page.d.ts +5 -0
- package/dist-client/pages/kpi-category/kpi-category-list-page.js +83 -8
- package/dist-client/pages/kpi-category/kpi-category-list-page.js.map +1 -1
- package/dist-client/pages/kpi-history/kpi-history-list-page.js +1 -1
- package/dist-client/pages/kpi-history/kpi-history-list-page.js.map +1 -1
- package/dist-client/pages/kpi-metric/kpi-metric-list-page.js +29 -5
- package/dist-client/pages/kpi-metric/kpi-metric-list-page.js.map +1 -1
- package/dist-client/pages/kpi-metric-value/kpi-metric-value-importer.d.ts +23 -0
- package/dist-client/pages/kpi-metric-value/kpi-metric-value-importer.js +75 -0
- package/dist-client/pages/kpi-metric-value/kpi-metric-value-importer.js.map +1 -0
- package/dist-client/pages/kpi-metric-value/kpi-metric-value-list-page.d.ts +61 -0
- package/dist-client/pages/kpi-metric-value/kpi-metric-value-list-page.js +301 -0
- package/dist-client/pages/kpi-metric-value/kpi-metric-value-list-page.js.map +1 -0
- package/dist-client/pages/{kpi-value/kpi-value-manual-entry-form.d.ts → kpi-metric-value/kpi-metric-value-manual-entry-form.d.ts} +3 -5
- package/dist-client/pages/{kpi-value/kpi-value-manual-entry-form.js → kpi-metric-value/kpi-metric-value-manual-entry-form.js} +27 -56
- package/dist-client/pages/kpi-metric-value/kpi-metric-value-manual-entry-form.js.map +1 -0
- package/dist-client/pages/{kpi-value/kpi-value-manual-entry-page.d.ts → kpi-metric-value/kpi-metric-value-manual-entry-page.d.ts} +5 -5
- package/dist-client/pages/{kpi-value/kpi-value-manual-entry-page.js → kpi-metric-value/kpi-metric-value-manual-entry-page.js} +28 -28
- package/dist-client/pages/kpi-metric-value/kpi-metric-value-manual-entry-page.js.map +1 -0
- package/dist-client/pages/kpi-value/kpi-value-list-page.js +4 -6
- package/dist-client/pages/kpi-value/kpi-value-list-page.js.map +1 -1
- package/dist-client/route.d.ts +1 -1
- package/dist-client/route.js +5 -2
- package/dist-client/route.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-server/service/index.d.ts +4 -2
- package/dist-server/service/index.js +6 -1
- package/dist-server/service/index.js.map +1 -1
- package/dist-server/service/kpi/aggregate-kpi.js +5 -7
- package/dist-server/service/kpi/aggregate-kpi.js.map +1 -1
- package/dist-server/service/kpi/kpi-history.d.ts +3 -1
- package/dist-server/service/kpi/kpi-history.js +10 -0
- package/dist-server/service/kpi/kpi-history.js.map +1 -1
- package/dist-server/service/kpi/kpi-mutation.js +1 -1
- package/dist-server/service/kpi/kpi-mutation.js.map +1 -1
- package/dist-server/service/kpi/kpi-type.d.ts +2 -0
- package/dist-server/service/kpi/kpi-type.js +8 -0
- package/dist-server/service/kpi/kpi-type.js.map +1 -1
- package/dist-server/service/kpi/kpi.d.ts +9 -0
- package/dist-server/service/kpi/kpi.js +23 -1
- package/dist-server/service/kpi/kpi.js.map +1 -1
- package/dist-server/service/kpi-category/kpi-category-mutation.js +0 -8
- package/dist-server/service/kpi-category/kpi-category-mutation.js.map +1 -1
- package/dist-server/service/kpi-category/kpi-category-type.d.ts +4 -2
- package/dist-server/service/kpi-category/kpi-category-type.js +16 -8
- package/dist-server/service/kpi-category/kpi-category-type.js.map +1 -1
- package/dist-server/service/kpi-category/kpi-category.d.ts +2 -2
- package/dist-server/service/kpi-category/kpi-category.js +8 -8
- package/dist-server/service/kpi-category/kpi-category.js.map +1 -1
- package/dist-server/service/kpi-metric/aggregate-kpi-metric.js +31 -74
- package/dist-server/service/kpi-metric/aggregate-kpi-metric.js.map +1 -1
- package/dist-server/service/kpi-metric/kpi-metric-mutation.d.ts +1 -1
- package/dist-server/service/kpi-metric/kpi-metric-mutation.js +15 -28
- package/dist-server/service/kpi-metric/kpi-metric-mutation.js.map +1 -1
- package/dist-server/service/kpi-metric/kpi-metric-type.d.ts +6 -4
- package/dist-server/service/kpi-metric/kpi-metric-type.js +20 -12
- package/dist-server/service/kpi-metric/kpi-metric-type.js.map +1 -1
- package/dist-server/service/kpi-metric/kpi-metric.d.ts +15 -2
- package/dist-server/service/kpi-metric/kpi-metric.js +34 -14
- package/dist-server/service/kpi-metric/kpi-metric.js.map +1 -1
- package/dist-server/service/kpi-metric-value/index.d.ts +6 -0
- package/dist-server/service/kpi-metric-value/index.js +10 -0
- package/dist-server/service/kpi-metric-value/index.js.map +1 -0
- package/dist-server/service/kpi-metric-value/kpi-metric-value-mutation.d.ts +11 -0
- package/dist-server/service/kpi-metric-value/kpi-metric-value-mutation.js +229 -0
- package/dist-server/service/kpi-metric-value/kpi-metric-value-mutation.js.map +1 -0
- package/dist-server/service/kpi-metric-value/kpi-metric-value-query.d.ts +13 -0
- package/dist-server/service/kpi-metric-value/kpi-metric-value-query.js +95 -0
- package/dist-server/service/kpi-metric-value/kpi-metric-value-query.js.map +1 -0
- package/dist-server/service/kpi-metric-value/kpi-metric-value-type.d.ts +26 -0
- package/dist-server/service/kpi-metric-value/kpi-metric-value-type.js +112 -0
- package/dist-server/service/kpi-metric-value/kpi-metric-value-type.js.map +1 -0
- package/dist-server/service/kpi-metric-value/kpi-metric-value.d.ts +23 -0
- package/dist-server/service/kpi-metric-value/kpi-metric-value.js +106 -0
- package/dist-server/service/kpi-metric-value/kpi-metric-value.js.map +1 -0
- package/dist-server/service/kpi-value/kpi-value-mutation.js +1 -2
- package/dist-server/service/kpi-value/kpi-value-mutation.js.map +1 -1
- package/dist-server/service/kpi-value/kpi-value-query.js +1 -1
- package/dist-server/service/kpi-value/kpi-value-query.js.map +1 -1
- package/dist-server/service/kpi-value/kpi-value-type.d.ts +2 -4
- package/dist-server/service/kpi-value/kpi-value-type.js +4 -18
- package/dist-server/service/kpi-value/kpi-value-type.js.map +1 -1
- package/dist-server/service/kpi-value/kpi-value.d.ts +3 -3
- package/dist-server/service/kpi-value/kpi-value.js +13 -14
- package/dist-server/service/kpi-value/kpi-value.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -3
- package/server/service/index.ts +6 -1
- package/server/service/kpi/aggregate-kpi.ts +5 -8
- package/server/service/kpi/kpi-history.ts +9 -1
- package/server/service/kpi/kpi-mutation.ts +1 -1
- package/server/service/kpi/kpi-type.ts +6 -0
- package/server/service/kpi/kpi.ts +21 -0
- package/server/service/kpi-category/kpi-category-mutation.ts +0 -10
- package/server/service/kpi-category/kpi-category-type.ts +12 -6
- package/server/service/kpi-category/kpi-category.ts +6 -6
- package/server/service/kpi-metric/aggregate-kpi-metric.ts +29 -69
- package/server/service/kpi-metric/kpi-metric-mutation.ts +15 -26
- package/server/service/kpi-metric/kpi-metric-type.ts +17 -12
- package/server/service/kpi-metric/kpi-metric.ts +32 -11
- package/server/service/kpi-metric-value/index.ts +7 -0
- package/server/service/kpi-metric-value/kpi-metric-value-mutation.ts +215 -0
- package/server/service/kpi-metric-value/kpi-metric-value-query.ts +60 -0
- package/server/service/kpi-metric-value/kpi-metric-value-type.ts +82 -0
- package/server/service/kpi-metric-value/kpi-metric-value.ts +91 -0
- package/server/service/kpi-value/kpi-value-mutation.ts +1 -2
- package/server/service/kpi-value/kpi-value-query.ts +1 -1
- package/server/service/kpi-value/kpi-value-type.ts +4 -16
- package/server/service/kpi-value/kpi-value.ts +14 -14
- package/things-factory.config.js +5 -3
- package/translations/en.json +8 -1
- package/translations/ja.json +8 -1
- package/translations/ko.json +9 -2
- package/translations/ms.json +9 -2
- package/translations/zh.json +8 -1
- package/dist-client/pages/kpi-value/kpi-value-manual-entry-form.js.map +0 -1
- package/dist-client/pages/kpi-value/kpi-value-manual-entry-page.js.map +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@things-factory/kpi",
|
|
3
|
-
"version": "9.0.
|
|
3
|
+
"version": "9.0.19",
|
|
4
4
|
"main": "dist-server/index.js",
|
|
5
5
|
"browser": "dist-client/index.js",
|
|
6
6
|
"things-factory": true,
|
|
@@ -41,8 +41,8 @@
|
|
|
41
41
|
"@operato/styles": "^9.0.0",
|
|
42
42
|
"@operato/utils": "^9.0.0",
|
|
43
43
|
"@things-factory/auth-base": "^9.0.0",
|
|
44
|
-
"@things-factory/dataset": "^9.0.
|
|
44
|
+
"@things-factory/dataset": "^9.0.19",
|
|
45
45
|
"@things-factory/shell": "^9.0.0"
|
|
46
46
|
},
|
|
47
|
-
"gitHead": "
|
|
47
|
+
"gitHead": "8a547ab7c7dd693d849121201dfeeeeefbc2f020"
|
|
48
48
|
}
|
package/server/service/index.ts
CHANGED
|
@@ -7,6 +7,8 @@ export * from './kpi-value/kpi-value'
|
|
|
7
7
|
export * from './kpi-value/kpi-value-type'
|
|
8
8
|
export * from './kpi-metric/kpi-metric'
|
|
9
9
|
export * from './kpi-metric/kpi-metric-type'
|
|
10
|
+
export * from './kpi-metric-value/kpi-metric-value'
|
|
11
|
+
export * from './kpi-metric-value/kpi-metric-value-type'
|
|
10
12
|
export * from './kpi-alert'
|
|
11
13
|
|
|
12
14
|
/* IMPORT ENTITIES AND RESOLVERS */
|
|
@@ -14,6 +16,7 @@ import { entities as KpiEntities, resolvers as KpiResolvers } from './kpi'
|
|
|
14
16
|
import { entities as KpiCategoryEntities, resolvers as KpiCategoryResolvers } from './kpi-category'
|
|
15
17
|
import { entities as KpiValueEntities, resolvers as KpiValueResolvers } from './kpi-value'
|
|
16
18
|
import { entities as KpiMetricEntities, resolvers as KpiMetricResolvers } from './kpi-metric'
|
|
19
|
+
import { entities as KpiMetricValueEntities, resolvers as KpiMetricValueResolvers } from './kpi-metric-value'
|
|
17
20
|
import { resolvers as KpiAlertResolvers } from './kpi-alert'
|
|
18
21
|
|
|
19
22
|
export const entities = [
|
|
@@ -21,7 +24,8 @@ export const entities = [
|
|
|
21
24
|
...KpiEntities,
|
|
22
25
|
...KpiCategoryEntities,
|
|
23
26
|
...KpiValueEntities,
|
|
24
|
-
...KpiMetricEntities
|
|
27
|
+
...KpiMetricEntities,
|
|
28
|
+
...KpiMetricValueEntities
|
|
25
29
|
]
|
|
26
30
|
|
|
27
31
|
export const schema = {
|
|
@@ -31,6 +35,7 @@ export const schema = {
|
|
|
31
35
|
...KpiCategoryResolvers,
|
|
32
36
|
...KpiValueResolvers,
|
|
33
37
|
...KpiMetricResolvers,
|
|
38
|
+
...KpiMetricValueResolvers,
|
|
34
39
|
...KpiAlertResolvers
|
|
35
40
|
]
|
|
36
41
|
}
|
|
@@ -34,8 +34,7 @@ export async function aggregateKpiValue(kpiId: string, domainId: string, context
|
|
|
34
34
|
codeValueMap[code] = await aggregateKpiMetricValue(metric.id, domainId, context)
|
|
35
35
|
}
|
|
36
36
|
// group/date/period별로 값 매핑(가장 최근 기준, group key 조합)
|
|
37
|
-
const groupKey = v =>
|
|
38
|
-
[v.date, v.period, v.group?.key01, v.group?.key02, v.group?.key03, v.group?.key04, v.group?.key05].join('|')
|
|
37
|
+
const groupKey = v => [v.date, v.period, v.group].join('|')
|
|
39
38
|
const groupMap: Record<string, any> = {}
|
|
40
39
|
for (const code of metricCodes) {
|
|
41
40
|
for (const v of codeValueMap[code]) {
|
|
@@ -55,14 +54,13 @@ export async function aggregateKpiValue(kpiId: string, domainId: string, context
|
|
|
55
54
|
value = null
|
|
56
55
|
}
|
|
57
56
|
const valueDate = groupMap[key].date
|
|
58
|
-
const
|
|
59
|
-
const groupType = groupMap[key].group?.key02
|
|
57
|
+
const group = groupMap[key].group
|
|
60
58
|
const version = kpi.version || 1
|
|
61
59
|
if (value == null || isNaN(value)) continue
|
|
62
|
-
// upsert(동일 KPI, valueDate,
|
|
60
|
+
// upsert(동일 KPI, valueDate, group, version) 기준으로 저장
|
|
63
61
|
const repo = getRepository(KpiValue, context.state?.tx)
|
|
64
62
|
const existing = await repo.findOne({
|
|
65
|
-
where: { kpi: { id: kpi.id }, valueDate,
|
|
63
|
+
where: { kpi: { id: kpi.id }, valueDate, group, version, domain: { id: domainId } }
|
|
66
64
|
})
|
|
67
65
|
let entity = existing || repo.create()
|
|
68
66
|
entity.kpi = kpi
|
|
@@ -70,8 +68,7 @@ export async function aggregateKpiValue(kpiId: string, domainId: string, context
|
|
|
70
68
|
entity.version = version
|
|
71
69
|
entity.valueDate = valueDate
|
|
72
70
|
entity.value = value
|
|
73
|
-
entity.
|
|
74
|
-
entity.groupType = groupType
|
|
71
|
+
entity.group = group
|
|
75
72
|
entity.inputType = KpiValueInputType.AUTO
|
|
76
73
|
entity.source = 'AUTO'
|
|
77
74
|
entity.domain = kpi.domain
|
|
@@ -11,7 +11,7 @@ import { Role, User } from '@things-factory/auth-base'
|
|
|
11
11
|
import { config } from '@things-factory/env'
|
|
12
12
|
import { Domain, ScalarObject } from '@things-factory/shell'
|
|
13
13
|
|
|
14
|
-
import { Kpi, KpiStatus } from './kpi'
|
|
14
|
+
import { Kpi, KpiStatus, KpiPeriodType } from './kpi'
|
|
15
15
|
import { KpiCategory } from '../kpi-category/kpi-category'
|
|
16
16
|
import { KpiGrades } from './kpi-grade.types'
|
|
17
17
|
|
|
@@ -77,6 +77,14 @@ export class KpiHistory implements HistoryEntityInterface<Kpi> {
|
|
|
77
77
|
})
|
|
78
78
|
grades?: KpiGrades
|
|
79
79
|
|
|
80
|
+
@Column({ type: 'float', nullable: true, default: 1 })
|
|
81
|
+
@Field({ nullable: true, description: 'Weight for aggregation in parent category.' })
|
|
82
|
+
weight?: number
|
|
83
|
+
|
|
84
|
+
@Column({ default: 'DAY' })
|
|
85
|
+
@Field(type => KpiPeriodType, { nullable: true })
|
|
86
|
+
periodType: KpiPeriodType
|
|
87
|
+
|
|
80
88
|
@Column({ nullable: true })
|
|
81
89
|
@Field({ nullable: true })
|
|
82
90
|
createdAt?: Date
|
|
@@ -189,7 +189,7 @@ export class KpiMutation {
|
|
|
189
189
|
}
|
|
190
190
|
|
|
191
191
|
@Directive('@transaction')
|
|
192
|
-
@Mutation(returns =>
|
|
192
|
+
@Mutation(returns => [Kpi], { description: "To modify multiple Kpis' information" })
|
|
193
193
|
async updateMultipleKpi(
|
|
194
194
|
@Arg('patches', type => [KpiPatch]) patches: KpiPatch[],
|
|
195
195
|
@Ctx() context: ResolverContext
|
|
@@ -52,6 +52,9 @@ export class NewKpi {
|
|
|
52
52
|
@Field({ nullable: true, description: 'Timezone for the KPI schedule.' })
|
|
53
53
|
timezone?: string
|
|
54
54
|
|
|
55
|
+
@Field({ nullable: true, description: 'Weight for aggregation in parent category.' })
|
|
56
|
+
weight?: number
|
|
57
|
+
|
|
55
58
|
@Field(type => ScalarObject, {
|
|
56
59
|
nullable: true,
|
|
57
60
|
description: 'Grade configuration for this KPI version'
|
|
@@ -110,6 +113,9 @@ export class KpiPatch {
|
|
|
110
113
|
@Field({ nullable: true, description: 'Timezone for the KPI schedule.' })
|
|
111
114
|
timezone?: string
|
|
112
115
|
|
|
116
|
+
@Field({ nullable: true, description: 'Weight for aggregation in parent category.' })
|
|
117
|
+
weight?: number
|
|
118
|
+
|
|
113
119
|
@Field(type => ScalarObject, {
|
|
114
120
|
nullable: true,
|
|
115
121
|
description: 'Grade configuration for this KPI version'
|
|
@@ -48,6 +48,14 @@ export enum KpiVizType {
|
|
|
48
48
|
TABLE = 'TABLE'
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
export enum KpiPeriodType {
|
|
52
|
+
DAY = 'DAY',
|
|
53
|
+
WEEK = 'WEEK',
|
|
54
|
+
MONTH = 'MONTH',
|
|
55
|
+
QUARTER = 'QUARTER',
|
|
56
|
+
RANGE = 'RANGE'
|
|
57
|
+
}
|
|
58
|
+
|
|
51
59
|
registerEnumType(KpiStatus, {
|
|
52
60
|
name: 'KpiStatus',
|
|
53
61
|
description: 'State enumeration of a KPI (DRAFT, RELEASED, ARCHIVED)'
|
|
@@ -58,6 +66,11 @@ registerEnumType(KpiVizType, {
|
|
|
58
66
|
description: 'Visualization type for KPI display (CARD, GAUGE, PROGRESS, etc.)'
|
|
59
67
|
})
|
|
60
68
|
|
|
69
|
+
registerEnumType(KpiPeriodType, {
|
|
70
|
+
name: 'KpiPeriodType',
|
|
71
|
+
description: 'Aggregation period type for KPI (DAY, WEEK, MONTH, QUARTER, RANGE)'
|
|
72
|
+
})
|
|
73
|
+
|
|
61
74
|
@Entity()
|
|
62
75
|
@Index('ix_kpi_0', (kpi: Kpi) => [kpi.domain, kpi.name], {
|
|
63
76
|
where: '"deleted_at" IS NULL',
|
|
@@ -161,6 +174,14 @@ export class Kpi {
|
|
|
161
174
|
@Field({ nullable: true, description: 'Timezone for the KPI schedule.' })
|
|
162
175
|
timezone?: string
|
|
163
176
|
|
|
177
|
+
@Column({ default: 'DAY' })
|
|
178
|
+
@Field(type => KpiPeriodType, { description: 'Aggregation period type for this KPI.' })
|
|
179
|
+
periodType: KpiPeriodType
|
|
180
|
+
|
|
181
|
+
@Column({ type: 'float', nullable: true, default: 1 })
|
|
182
|
+
@Field({ nullable: true, description: 'Weight for aggregation in parent category.' })
|
|
183
|
+
weight?: number
|
|
184
|
+
|
|
164
185
|
@CreateDateColumn()
|
|
165
186
|
@Field({ nullable: true, description: 'Timestamp when this KPI was created.' })
|
|
166
187
|
createdAt?: Date
|
|
@@ -16,13 +16,8 @@ export class KpiCategoryMutation {
|
|
|
16
16
|
): Promise<KpiCategory> {
|
|
17
17
|
const { domain, user, tx } = context.state
|
|
18
18
|
|
|
19
|
-
let parent = category.parentId
|
|
20
|
-
? await getRepository(KpiCategory).findOne({ where: { id: category.parentId } })
|
|
21
|
-
: undefined
|
|
22
|
-
|
|
23
19
|
const result = await getRepository(KpiCategory, tx).save({
|
|
24
20
|
...category,
|
|
25
|
-
parent,
|
|
26
21
|
domain,
|
|
27
22
|
creator: user,
|
|
28
23
|
updater: user
|
|
@@ -45,14 +40,9 @@ export class KpiCategoryMutation {
|
|
|
45
40
|
where: { domain: { id: domain.id }, id }
|
|
46
41
|
})
|
|
47
42
|
|
|
48
|
-
let parent = patch.parentId
|
|
49
|
-
? await getRepository(KpiCategory).findOne({ where: { id: patch.parentId } })
|
|
50
|
-
: kpiCategory.parent
|
|
51
|
-
|
|
52
43
|
const result = await repository.save({
|
|
53
44
|
...kpiCategory,
|
|
54
45
|
...patch,
|
|
55
|
-
parent,
|
|
56
46
|
updater: user
|
|
57
47
|
})
|
|
58
48
|
|
|
@@ -16,11 +16,14 @@ export class NewKpiCategory {
|
|
|
16
16
|
@Field({ nullable: true, description: 'Detailed description of this KPI category.' })
|
|
17
17
|
description?: string
|
|
18
18
|
|
|
19
|
-
@Field(type => ID, { nullable: true, description: 'ID of the parent category, if any.' })
|
|
20
|
-
parentId?: string
|
|
21
|
-
|
|
22
19
|
@Field({ nullable: true, description: 'Whether this category is active (usable) or not.' })
|
|
23
20
|
active?: boolean
|
|
21
|
+
|
|
22
|
+
@Field({ nullable: true, description: 'Aggregation formula using child KPI codes.' })
|
|
23
|
+
formula?: string
|
|
24
|
+
|
|
25
|
+
@Field({ nullable: true, description: 'Weight for aggregation in higher-level summary.' })
|
|
26
|
+
weight?: number
|
|
24
27
|
}
|
|
25
28
|
|
|
26
29
|
@InputType({
|
|
@@ -36,12 +39,15 @@ export class KpiCategoryPatch {
|
|
|
36
39
|
@Field({ nullable: true, description: 'Detailed description of this KPI category.' })
|
|
37
40
|
description?: string
|
|
38
41
|
|
|
39
|
-
@Field(type => ID, { nullable: true, description: 'ID of the parent category, if any.' })
|
|
40
|
-
parentId?: string
|
|
41
|
-
|
|
42
42
|
@Field({ nullable: true, description: 'Whether this category is active (usable) or not.' })
|
|
43
43
|
active?: boolean
|
|
44
44
|
|
|
45
|
+
@Field({ nullable: true, description: 'Aggregation formula using child KPI codes.' })
|
|
46
|
+
formula?: string
|
|
47
|
+
|
|
48
|
+
@Field({ nullable: true, description: 'Weight for aggregation in higher-level summary.' })
|
|
49
|
+
weight?: number
|
|
50
|
+
|
|
45
51
|
@Field({ nullable: true, description: 'Custom flag for update operations (internal use).' })
|
|
46
52
|
cuFlag?: string
|
|
47
53
|
}
|
|
@@ -48,13 +48,13 @@ export class KpiCategory {
|
|
|
48
48
|
@Field({ nullable: true, description: 'Whether this category is active (usable) or not.' })
|
|
49
49
|
active?: boolean
|
|
50
50
|
|
|
51
|
-
@
|
|
52
|
-
@Field(
|
|
53
|
-
|
|
51
|
+
@Column({ nullable: true })
|
|
52
|
+
@Field({ nullable: true, description: 'Aggregation formula using child KPI codes.' })
|
|
53
|
+
formula?: string
|
|
54
54
|
|
|
55
|
-
@
|
|
56
|
-
@Field({ nullable: true, description: '
|
|
57
|
-
|
|
55
|
+
@Column({ type: 'float', nullable: true, default: 1 })
|
|
56
|
+
@Field({ nullable: true, description: 'Weight for aggregation in higher-level summary.' })
|
|
57
|
+
weight?: number
|
|
58
58
|
|
|
59
59
|
@ManyToOne(type => User, { nullable: true })
|
|
60
60
|
@Field(type => User, { nullable: true, description: 'User who created this KPI category.' })
|
|
@@ -23,90 +23,51 @@ export async function aggregateKpiMetricValue(metricId: string, domainId: string
|
|
|
23
23
|
if (!metric) throw new Error('Metric 정보 없음')
|
|
24
24
|
if (!metric.active) throw new Error('비활성화된 Metric')
|
|
25
25
|
|
|
26
|
-
//
|
|
26
|
+
// formula 분기 제거: metric은 항상 dataset 집계만 수행
|
|
27
27
|
let values: any[] = []
|
|
28
|
-
if (metric.
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
if (
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
groupMap[key] = groupMap[key] || { ...v, _values: {} }
|
|
48
|
-
groupMap[key]._values[code] = v.value
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
// formula 계산 (js eval, 보안상 제한적 사용)
|
|
52
|
-
for (const key in groupMap) {
|
|
53
|
-
const ctx = groupMap[key]._values
|
|
54
|
-
let value = null
|
|
55
|
-
try {
|
|
56
|
-
value = Function(...Object.keys(ctx), `return (${metric.formula})`)(...Object.values(ctx))
|
|
57
|
-
} catch (e) {
|
|
58
|
-
value = null
|
|
59
|
-
}
|
|
60
|
-
values.push({ ...groupMap[key], value })
|
|
61
|
-
}
|
|
62
|
-
} else {
|
|
63
|
-
// 3. dataset 집계/마감 실행 (최신 기간 기준)
|
|
64
|
-
if (!metric.dataSetId || !metric.fieldName) throw new Error('Metric 매핑 정보 누락')
|
|
65
|
-
await finalizeLatestDataCollection(metric.dataSetId, context)
|
|
66
|
-
// 4. 집계 결과(summary)에서 metric.fieldName 값 추출
|
|
67
|
-
const summaries = await getRepository(DataSummary).find({
|
|
68
|
-
where: { dataSet: { id: metric.dataSetId }, domain: { id: domainId } },
|
|
69
|
-
order: { date: 'DESC', period: 'DESC' }
|
|
70
|
-
})
|
|
71
|
-
values = summaries
|
|
72
|
-
.map(summary => {
|
|
73
|
-
if (summary.summary && metric.fieldName in summary.summary) {
|
|
74
|
-
return {
|
|
75
|
-
date: summary.date,
|
|
76
|
-
period: summary.period,
|
|
77
|
-
value: summary.summary[metric.fieldName],
|
|
78
|
-
group: {
|
|
79
|
-
key01: summary.key01,
|
|
80
|
-
key02: summary.key02,
|
|
81
|
-
key03: summary.key03,
|
|
82
|
-
key04: summary.key04,
|
|
83
|
-
key05: summary.key05
|
|
84
|
-
}
|
|
28
|
+
if (!metric.dataSetId || !metric.fieldName) throw new Error('Metric 매핑 정보 누락')
|
|
29
|
+
await finalizeLatestDataCollection(metric.dataSetId, context)
|
|
30
|
+
const summaries = await getRepository(DataSummary).find({
|
|
31
|
+
where: { dataSet: { id: metric.dataSetId }, domain: { id: domainId } },
|
|
32
|
+
order: { date: 'DESC', period: 'DESC' }
|
|
33
|
+
})
|
|
34
|
+
values = summaries
|
|
35
|
+
.map(summary => {
|
|
36
|
+
if (summary.summary && metric.fieldName in summary.summary) {
|
|
37
|
+
return {
|
|
38
|
+
date: summary.date,
|
|
39
|
+
period: summary.period,
|
|
40
|
+
value: summary.summary[metric.fieldName],
|
|
41
|
+
group: {
|
|
42
|
+
key01: summary.key01,
|
|
43
|
+
key02: summary.key02,
|
|
44
|
+
key03: summary.key03,
|
|
45
|
+
key04: summary.key04,
|
|
46
|
+
key05: summary.key05
|
|
85
47
|
}
|
|
86
48
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
49
|
+
}
|
|
50
|
+
return null
|
|
51
|
+
})
|
|
52
|
+
.filter(Boolean)
|
|
91
53
|
|
|
92
54
|
// 5. KPI Value로 저장 (metric 단위, KPI 단위 formula는 후속)
|
|
93
55
|
// metric이 속한 KPI 정보 조회 (여기서는 metric.name == kpi.name인 KPI를 임시로 매핑, 실제 연동 구조에 맞게 보완 필요)
|
|
94
56
|
const kpi = await getRepository(Kpi).findOne({ where: { name: metric.name, domain: { id: domainId } } })
|
|
95
57
|
if (!kpi) throw new Error('KPI 정보 없음 (metric.name과 동일한 KPI name 기준, 실제 연동 구조에 맞게 보완 필요)')
|
|
96
58
|
|
|
97
|
-
// KPI Value version, valueDate,
|
|
59
|
+
// KPI Value version, valueDate, group 등 매핑
|
|
98
60
|
const savedValues = []
|
|
99
61
|
for (const v of values) {
|
|
100
62
|
const valueDate = v.date
|
|
101
|
-
const
|
|
102
|
-
const groupType = v.group?.key02
|
|
63
|
+
const group = v.group
|
|
103
64
|
const version = kpi.version || 1
|
|
104
65
|
const value = v.value
|
|
105
66
|
if (value == null || isNaN(value)) continue
|
|
106
|
-
// upsert(동일 KPI, valueDate,
|
|
67
|
+
// upsert(동일 KPI, valueDate, group, version) 기준으로 저장
|
|
107
68
|
const repo = getRepository(KpiValue, context.state?.tx)
|
|
108
69
|
const existing = await repo.findOne({
|
|
109
|
-
where: { kpi: { id: kpi.id }, valueDate,
|
|
70
|
+
where: { kpi: { id: kpi.id }, valueDate, group, version, domain: { id: domainId } }
|
|
110
71
|
})
|
|
111
72
|
let entity = existing || repo.create()
|
|
112
73
|
entity.kpi = kpi
|
|
@@ -114,8 +75,7 @@ export async function aggregateKpiMetricValue(metricId: string, domainId: string
|
|
|
114
75
|
entity.version = version
|
|
115
76
|
entity.valueDate = valueDate
|
|
116
77
|
entity.value = value
|
|
117
|
-
entity.
|
|
118
|
-
entity.groupType = groupType
|
|
78
|
+
entity.group = group
|
|
119
79
|
entity.inputType = KpiValueInputType.AUTO
|
|
120
80
|
entity.source = 'AUTO'
|
|
121
81
|
entity.domain = kpi.domain
|
|
@@ -14,25 +14,20 @@ export class KpiMetricMutation {
|
|
|
14
14
|
@Directive('@transaction')
|
|
15
15
|
@Mutation(returns => KpiMetric, { description: 'Create a new KPI metric with the provided details.' })
|
|
16
16
|
async createKpiMetric(
|
|
17
|
-
@Arg('
|
|
17
|
+
@Arg('kpiMetric', { description: 'Input object containing details for the new KPI metric.' })
|
|
18
|
+
kpiMetric: NewKpiMetric,
|
|
18
19
|
@Ctx() context: ResolverContext
|
|
19
20
|
): Promise<KpiMetric> {
|
|
20
21
|
const { domain, user, tx } = context.state
|
|
21
22
|
|
|
22
|
-
let dataSet =
|
|
23
|
-
? await getRepository(DataSet).findOne({ where: { id:
|
|
23
|
+
let dataSet = kpiMetric.dataSetId
|
|
24
|
+
? await getRepository(DataSet).findOne({ where: { id: kpiMetric.dataSetId } })
|
|
24
25
|
: undefined
|
|
25
26
|
|
|
26
|
-
|
|
27
|
-
const formulaService = new KpiFormulaService()
|
|
28
|
-
const result = await formulaService.validateFormula(metric.formula)
|
|
29
|
-
if (!result.valid) {
|
|
30
|
-
throw new Error(result.errors.join('\n'))
|
|
31
|
-
}
|
|
32
|
-
}
|
|
27
|
+
// formula 관련 로직 완전 제거
|
|
33
28
|
|
|
34
29
|
const result = await getRepository(KpiMetric, tx).save({
|
|
35
|
-
...
|
|
30
|
+
...kpiMetric,
|
|
36
31
|
dataSet,
|
|
37
32
|
domain,
|
|
38
33
|
creator: user,
|
|
@@ -40,9 +35,9 @@ export class KpiMetricMutation {
|
|
|
40
35
|
})
|
|
41
36
|
|
|
42
37
|
// 스케줄러 등록
|
|
43
|
-
if (
|
|
38
|
+
if (kpiMetric.schedule) {
|
|
44
39
|
const handle = await registerSchedule({
|
|
45
|
-
name:
|
|
40
|
+
name: kpiMetric.name,
|
|
46
41
|
client: {
|
|
47
42
|
application: Application,
|
|
48
43
|
group: `${domain.id}`,
|
|
@@ -51,8 +46,8 @@ export class KpiMetricMutation {
|
|
|
51
46
|
operation: 'aggregate'
|
|
52
47
|
},
|
|
53
48
|
type: 'cron',
|
|
54
|
-
schedule:
|
|
55
|
-
timezone:
|
|
49
|
+
schedule: kpiMetric.schedule,
|
|
50
|
+
timezone: kpiMetric.timezone,
|
|
56
51
|
task: {
|
|
57
52
|
type: 'rest',
|
|
58
53
|
connection: {
|
|
@@ -73,16 +68,16 @@ export class KpiMetricMutation {
|
|
|
73
68
|
}
|
|
74
69
|
})
|
|
75
70
|
result.scheduleId = handle
|
|
76
|
-
result.timezone =
|
|
71
|
+
result.timezone = kpiMetric.timezone
|
|
77
72
|
await getRepository(KpiMetric, tx).save(result)
|
|
78
73
|
}
|
|
79
74
|
|
|
80
|
-
if (
|
|
75
|
+
if (kpiMetric.thumbnail) {
|
|
81
76
|
await createAttachment(
|
|
82
77
|
null,
|
|
83
78
|
{
|
|
84
79
|
attachment: {
|
|
85
|
-
file:
|
|
80
|
+
file: kpiMetric.thumbnail,
|
|
86
81
|
refType: KpiMetric.name,
|
|
87
82
|
refBy: result.id
|
|
88
83
|
}
|
|
@@ -112,13 +107,7 @@ export class KpiMetricMutation {
|
|
|
112
107
|
? await getRepository(DataSet).findOne({ where: { id: patch.dataSetId } })
|
|
113
108
|
: kpiMetric.dataSet
|
|
114
109
|
|
|
115
|
-
|
|
116
|
-
const formulaService = new KpiFormulaService()
|
|
117
|
-
const result = await formulaService.validateFormula(patch.formula)
|
|
118
|
-
if (!result.valid) {
|
|
119
|
-
throw new Error(result.errors.join('\n'))
|
|
120
|
-
}
|
|
121
|
-
}
|
|
110
|
+
// formula 관련 로직 완전 제거
|
|
122
111
|
|
|
123
112
|
const result = await repository.save({
|
|
124
113
|
...kpiMetric,
|
|
@@ -128,7 +117,7 @@ export class KpiMetricMutation {
|
|
|
128
117
|
})
|
|
129
118
|
|
|
130
119
|
// 스케줄러 해제/등록 (변경 시)
|
|
131
|
-
if (kpiMetric.scheduleId && (patch.
|
|
120
|
+
if (kpiMetric.scheduleId && (patch.scheduleId !== kpiMetric.scheduleId || !patch.scheduleId)) {
|
|
132
121
|
await unregisterSchedule(kpiMetric.scheduleId)
|
|
133
122
|
result.scheduleId = null
|
|
134
123
|
}
|
|
@@ -2,9 +2,7 @@ import type { FileUpload } from 'graphql-upload/GraphQLUpload.js'
|
|
|
2
2
|
import GraphQLUpload from 'graphql-upload/GraphQLUpload.js'
|
|
3
3
|
import { ObjectType, Field, InputType, Int, ID, registerEnumType } from 'type-graphql'
|
|
4
4
|
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
import { KpiMetric } from './kpi-metric'
|
|
5
|
+
import { KpiMetric, KpiMetricPeriodType, KpiMetricCollectType } from './kpi-metric'
|
|
8
6
|
|
|
9
7
|
@InputType({ description: 'Input type for creating a new KPI metric. Used in mutations to provide metric details.' })
|
|
10
8
|
export class NewKpiMetric {
|
|
@@ -29,9 +27,6 @@ export class NewKpiMetric {
|
|
|
29
27
|
@Field({ nullable: true, description: 'Indicates whether this metric is active and usable.' })
|
|
30
28
|
active?: boolean
|
|
31
29
|
|
|
32
|
-
@Field({ nullable: true, description: 'Calculation formula for the metric, using other metric codes and operators.' })
|
|
33
|
-
formula?: string
|
|
34
|
-
|
|
35
30
|
@Field({
|
|
36
31
|
nullable: true,
|
|
37
32
|
description: 'Cron schedule string for periodic KPI value aggregation (e.g., "0 0 * * *" for daily).'
|
|
@@ -45,6 +40,12 @@ export class NewKpiMetric {
|
|
|
45
40
|
timezone?: string
|
|
46
41
|
@Field({ nullable: true, description: 'Schedule ID for the registered cron job.' })
|
|
47
42
|
scheduleId?: string
|
|
43
|
+
|
|
44
|
+
@Field(type => KpiMetricPeriodType, { nullable: true, description: 'Aggregation period type for this metric.' })
|
|
45
|
+
periodType?: KpiMetricPeriodType
|
|
46
|
+
|
|
47
|
+
@Field(type => KpiMetricCollectType, { nullable: true, description: '데이터 수집 방식' })
|
|
48
|
+
collectType?: KpiMetricCollectType
|
|
48
49
|
}
|
|
49
50
|
|
|
50
51
|
@InputType()
|
|
@@ -73,9 +74,6 @@ export class KpiMetricPatch {
|
|
|
73
74
|
@Field({ nullable: true })
|
|
74
75
|
active?: boolean
|
|
75
76
|
|
|
76
|
-
@Field({ nullable: true, description: 'Calculation formula for the metric, using other metric codes and operators.' })
|
|
77
|
-
formula?: string
|
|
78
|
-
|
|
79
77
|
@Field({
|
|
80
78
|
nullable: true,
|
|
81
79
|
description: 'Cron schedule string for periodic KPI value aggregation (e.g., "0 0 * * *" for daily).'
|
|
@@ -85,13 +83,20 @@ export class KpiMetricPatch {
|
|
|
85
83
|
@Field(type => GraphQLUpload, { nullable: true })
|
|
86
84
|
thumbnail?: FileUpload
|
|
87
85
|
|
|
88
|
-
@Field({ nullable: true })
|
|
89
|
-
cuFlag?: string
|
|
90
|
-
|
|
91
86
|
@Field({ nullable: true, description: 'Timezone for the schedule.' })
|
|
92
87
|
timezone?: string
|
|
88
|
+
|
|
93
89
|
@Field({ nullable: true, description: 'Schedule ID for the registered cron job.' })
|
|
94
90
|
scheduleId?: string
|
|
91
|
+
|
|
92
|
+
@Field(type => KpiMetricPeriodType, { nullable: true, description: 'Aggregation period type for this metric.' })
|
|
93
|
+
periodType?: KpiMetricPeriodType
|
|
94
|
+
|
|
95
|
+
@Field({ nullable: true })
|
|
96
|
+
cuFlag?: string
|
|
97
|
+
|
|
98
|
+
@Field(type => KpiMetricCollectType, { nullable: true, description: '데이터 수집 방식' })
|
|
99
|
+
collectType?: KpiMetricCollectType
|
|
95
100
|
}
|
|
96
101
|
|
|
97
102
|
@ObjectType()
|
|
@@ -15,6 +15,30 @@ import { Domain } from '@things-factory/shell'
|
|
|
15
15
|
import { User } from '@things-factory/auth-base'
|
|
16
16
|
import { DataSet } from '@things-factory/dataset'
|
|
17
17
|
|
|
18
|
+
export enum KpiMetricPeriodType {
|
|
19
|
+
DAY = 'DAY',
|
|
20
|
+
WEEK = 'WEEK',
|
|
21
|
+
MONTH = 'MONTH',
|
|
22
|
+
QUARTER = 'QUARTER',
|
|
23
|
+
RANGE = 'RANGE'
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
registerEnumType(KpiMetricPeriodType, {
|
|
27
|
+
name: 'KpiMetricPeriodType',
|
|
28
|
+
description: 'Aggregation period type for metric (DAY, WEEK, MONTH, QUARTER, RANGE)'
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
export enum KpiMetricCollectType {
|
|
32
|
+
AUTO = 'AUTO', // 데이터셋 등 자동 수집
|
|
33
|
+
MANUAL = 'MANUAL', // 수동 입력
|
|
34
|
+
IMPORT = 'IMPORT', // 외부 파일 등 임포트
|
|
35
|
+
EXTERNAL = 'EXTERNAL' // 외부 API 등
|
|
36
|
+
}
|
|
37
|
+
registerEnumType(KpiMetricCollectType, {
|
|
38
|
+
name: 'KpiMetricCollectType',
|
|
39
|
+
description: '방식: AUTO(자동), MANUAL(수동), IMPORT(임포트), EXTERNAL(외부API)'
|
|
40
|
+
})
|
|
41
|
+
|
|
18
42
|
@Entity()
|
|
19
43
|
@Index('ix_kpi_metric_0', (kpiMetric: KpiMetric) => [kpiMetric.domain, kpiMetric.name], {
|
|
20
44
|
where: '"deleted_at" IS NULL',
|
|
@@ -66,17 +90,6 @@ export class KpiMetric {
|
|
|
66
90
|
@Field({ nullable: true, description: 'Indicates whether this metric is active and usable.' })
|
|
67
91
|
active?: boolean
|
|
68
92
|
|
|
69
|
-
@Column({ nullable: true })
|
|
70
|
-
@Field({ nullable: true, description: 'Calculation formula for the metric, using other metric codes and operators.' })
|
|
71
|
-
formula?: string
|
|
72
|
-
|
|
73
|
-
@Column({ nullable: true })
|
|
74
|
-
@Field({
|
|
75
|
-
nullable: true,
|
|
76
|
-
description: 'Cron schedule string for periodic KPI value aggregation (e.g., "0 0 * * *" for daily).'
|
|
77
|
-
})
|
|
78
|
-
schedule?: string
|
|
79
|
-
|
|
80
93
|
@Column({ nullable: true })
|
|
81
94
|
@Field({ nullable: true, description: 'Schedule ID for the registered cron job.' })
|
|
82
95
|
scheduleId?: string
|
|
@@ -85,6 +98,14 @@ export class KpiMetric {
|
|
|
85
98
|
@Field({ nullable: true, description: 'Timezone for the schedule.' })
|
|
86
99
|
timezone?: string
|
|
87
100
|
|
|
101
|
+
@Column({ type: 'enum', enum: KpiMetricCollectType, default: KpiMetricCollectType.AUTO })
|
|
102
|
+
@Field(type => KpiMetricCollectType, { description: '데이터 수집 방식' })
|
|
103
|
+
collectType: KpiMetricCollectType
|
|
104
|
+
|
|
105
|
+
@Column({ default: 'DAY' })
|
|
106
|
+
@Field(type => KpiMetricPeriodType, { description: 'Aggregation period type for this metric.' })
|
|
107
|
+
periodType: KpiMetricPeriodType
|
|
108
|
+
|
|
88
109
|
@CreateDateColumn()
|
|
89
110
|
@Field({ nullable: true, description: 'Timestamp when this KPI metric was created.' })
|
|
90
111
|
createdAt?: Date
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { KpiMetricValue } from './kpi-metric-value'
|
|
2
|
+
import { KpiMetricValueQuery } from './kpi-metric-value-query'
|
|
3
|
+
import { KpiMetricValueMutation } from './kpi-metric-value-mutation'
|
|
4
|
+
|
|
5
|
+
export const entities = [KpiMetricValue]
|
|
6
|
+
export const resolvers = [KpiMetricValueQuery, KpiMetricValueMutation]
|
|
7
|
+
export const subscribers = []
|