@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.
Files changed (137) hide show
  1. package/client/bootstrap.ts +8 -0
  2. package/client/pages/kpi/kpi-list-page.ts +99 -11
  3. package/client/pages/kpi/kpi-viz-editor.ts +214 -14
  4. package/client/pages/kpi-category/kpi-category-list-page.ts +80 -8
  5. package/client/pages/kpi-history/kpi-history-list-page.ts +1 -1
  6. package/client/pages/kpi-metric/kpi-metric-list-page.ts +31 -7
  7. package/client/pages/kpi-metric-value/kpi-metric-value-importer.ts +65 -0
  8. package/client/pages/kpi-metric-value/kpi-metric-value-list-page.ts +299 -0
  9. package/client/pages/{kpi-value/kpi-value-manual-entry-form.ts → kpi-metric-value/kpi-metric-value-manual-entry-form.ts} +18 -44
  10. package/client/pages/{kpi-value/kpi-value-manual-entry-page.ts → kpi-metric-value/kpi-metric-value-manual-entry-page.ts} +21 -21
  11. package/client/pages/kpi-value/kpi-value-list-page.ts +4 -6
  12. package/client/route.ts +6 -2
  13. package/dist-client/bootstrap.d.ts +2 -0
  14. package/dist-client/bootstrap.js +7 -0
  15. package/dist-client/bootstrap.js.map +1 -0
  16. package/dist-client/pages/kpi/kpi-list-page.d.ts +6 -0
  17. package/dist-client/pages/kpi/kpi-list-page.js +100 -11
  18. package/dist-client/pages/kpi/kpi-list-page.js.map +1 -1
  19. package/dist-client/pages/kpi/kpi-viz-editor.js +208 -14
  20. package/dist-client/pages/kpi/kpi-viz-editor.js.map +1 -1
  21. package/dist-client/pages/kpi-category/kpi-category-list-page.d.ts +5 -0
  22. package/dist-client/pages/kpi-category/kpi-category-list-page.js +83 -8
  23. package/dist-client/pages/kpi-category/kpi-category-list-page.js.map +1 -1
  24. package/dist-client/pages/kpi-history/kpi-history-list-page.js +1 -1
  25. package/dist-client/pages/kpi-history/kpi-history-list-page.js.map +1 -1
  26. package/dist-client/pages/kpi-metric/kpi-metric-list-page.js +29 -5
  27. package/dist-client/pages/kpi-metric/kpi-metric-list-page.js.map +1 -1
  28. package/dist-client/pages/kpi-metric-value/kpi-metric-value-importer.d.ts +23 -0
  29. package/dist-client/pages/kpi-metric-value/kpi-metric-value-importer.js +75 -0
  30. package/dist-client/pages/kpi-metric-value/kpi-metric-value-importer.js.map +1 -0
  31. package/dist-client/pages/kpi-metric-value/kpi-metric-value-list-page.d.ts +61 -0
  32. package/dist-client/pages/kpi-metric-value/kpi-metric-value-list-page.js +301 -0
  33. package/dist-client/pages/kpi-metric-value/kpi-metric-value-list-page.js.map +1 -0
  34. 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
  35. package/dist-client/pages/{kpi-value/kpi-value-manual-entry-form.js → kpi-metric-value/kpi-metric-value-manual-entry-form.js} +27 -56
  36. package/dist-client/pages/kpi-metric-value/kpi-metric-value-manual-entry-form.js.map +1 -0
  37. 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
  38. package/dist-client/pages/{kpi-value/kpi-value-manual-entry-page.js → kpi-metric-value/kpi-metric-value-manual-entry-page.js} +28 -28
  39. package/dist-client/pages/kpi-metric-value/kpi-metric-value-manual-entry-page.js.map +1 -0
  40. package/dist-client/pages/kpi-value/kpi-value-list-page.js +4 -6
  41. package/dist-client/pages/kpi-value/kpi-value-list-page.js.map +1 -1
  42. package/dist-client/route.d.ts +1 -1
  43. package/dist-client/route.js +5 -2
  44. package/dist-client/route.js.map +1 -1
  45. package/dist-client/tsconfig.tsbuildinfo +1 -1
  46. package/dist-server/service/index.d.ts +4 -2
  47. package/dist-server/service/index.js +6 -1
  48. package/dist-server/service/index.js.map +1 -1
  49. package/dist-server/service/kpi/aggregate-kpi.js +5 -7
  50. package/dist-server/service/kpi/aggregate-kpi.js.map +1 -1
  51. package/dist-server/service/kpi/kpi-history.d.ts +3 -1
  52. package/dist-server/service/kpi/kpi-history.js +10 -0
  53. package/dist-server/service/kpi/kpi-history.js.map +1 -1
  54. package/dist-server/service/kpi/kpi-mutation.js +1 -1
  55. package/dist-server/service/kpi/kpi-mutation.js.map +1 -1
  56. package/dist-server/service/kpi/kpi-type.d.ts +2 -0
  57. package/dist-server/service/kpi/kpi-type.js +8 -0
  58. package/dist-server/service/kpi/kpi-type.js.map +1 -1
  59. package/dist-server/service/kpi/kpi.d.ts +9 -0
  60. package/dist-server/service/kpi/kpi.js +23 -1
  61. package/dist-server/service/kpi/kpi.js.map +1 -1
  62. package/dist-server/service/kpi-category/kpi-category-mutation.js +0 -8
  63. package/dist-server/service/kpi-category/kpi-category-mutation.js.map +1 -1
  64. package/dist-server/service/kpi-category/kpi-category-type.d.ts +4 -2
  65. package/dist-server/service/kpi-category/kpi-category-type.js +16 -8
  66. package/dist-server/service/kpi-category/kpi-category-type.js.map +1 -1
  67. package/dist-server/service/kpi-category/kpi-category.d.ts +2 -2
  68. package/dist-server/service/kpi-category/kpi-category.js +8 -8
  69. package/dist-server/service/kpi-category/kpi-category.js.map +1 -1
  70. package/dist-server/service/kpi-metric/aggregate-kpi-metric.js +31 -74
  71. package/dist-server/service/kpi-metric/aggregate-kpi-metric.js.map +1 -1
  72. package/dist-server/service/kpi-metric/kpi-metric-mutation.d.ts +1 -1
  73. package/dist-server/service/kpi-metric/kpi-metric-mutation.js +15 -28
  74. package/dist-server/service/kpi-metric/kpi-metric-mutation.js.map +1 -1
  75. package/dist-server/service/kpi-metric/kpi-metric-type.d.ts +6 -4
  76. package/dist-server/service/kpi-metric/kpi-metric-type.js +20 -12
  77. package/dist-server/service/kpi-metric/kpi-metric-type.js.map +1 -1
  78. package/dist-server/service/kpi-metric/kpi-metric.d.ts +15 -2
  79. package/dist-server/service/kpi-metric/kpi-metric.js +34 -14
  80. package/dist-server/service/kpi-metric/kpi-metric.js.map +1 -1
  81. package/dist-server/service/kpi-metric-value/index.d.ts +6 -0
  82. package/dist-server/service/kpi-metric-value/index.js +10 -0
  83. package/dist-server/service/kpi-metric-value/index.js.map +1 -0
  84. package/dist-server/service/kpi-metric-value/kpi-metric-value-mutation.d.ts +11 -0
  85. package/dist-server/service/kpi-metric-value/kpi-metric-value-mutation.js +229 -0
  86. package/dist-server/service/kpi-metric-value/kpi-metric-value-mutation.js.map +1 -0
  87. package/dist-server/service/kpi-metric-value/kpi-metric-value-query.d.ts +13 -0
  88. package/dist-server/service/kpi-metric-value/kpi-metric-value-query.js +95 -0
  89. package/dist-server/service/kpi-metric-value/kpi-metric-value-query.js.map +1 -0
  90. package/dist-server/service/kpi-metric-value/kpi-metric-value-type.d.ts +26 -0
  91. package/dist-server/service/kpi-metric-value/kpi-metric-value-type.js +112 -0
  92. package/dist-server/service/kpi-metric-value/kpi-metric-value-type.js.map +1 -0
  93. package/dist-server/service/kpi-metric-value/kpi-metric-value.d.ts +23 -0
  94. package/dist-server/service/kpi-metric-value/kpi-metric-value.js +106 -0
  95. package/dist-server/service/kpi-metric-value/kpi-metric-value.js.map +1 -0
  96. package/dist-server/service/kpi-value/kpi-value-mutation.js +1 -2
  97. package/dist-server/service/kpi-value/kpi-value-mutation.js.map +1 -1
  98. package/dist-server/service/kpi-value/kpi-value-query.js +1 -1
  99. package/dist-server/service/kpi-value/kpi-value-query.js.map +1 -1
  100. package/dist-server/service/kpi-value/kpi-value-type.d.ts +2 -4
  101. package/dist-server/service/kpi-value/kpi-value-type.js +4 -18
  102. package/dist-server/service/kpi-value/kpi-value-type.js.map +1 -1
  103. package/dist-server/service/kpi-value/kpi-value.d.ts +3 -3
  104. package/dist-server/service/kpi-value/kpi-value.js +13 -14
  105. package/dist-server/service/kpi-value/kpi-value.js.map +1 -1
  106. package/dist-server/tsconfig.tsbuildinfo +1 -1
  107. package/package.json +3 -3
  108. package/server/service/index.ts +6 -1
  109. package/server/service/kpi/aggregate-kpi.ts +5 -8
  110. package/server/service/kpi/kpi-history.ts +9 -1
  111. package/server/service/kpi/kpi-mutation.ts +1 -1
  112. package/server/service/kpi/kpi-type.ts +6 -0
  113. package/server/service/kpi/kpi.ts +21 -0
  114. package/server/service/kpi-category/kpi-category-mutation.ts +0 -10
  115. package/server/service/kpi-category/kpi-category-type.ts +12 -6
  116. package/server/service/kpi-category/kpi-category.ts +6 -6
  117. package/server/service/kpi-metric/aggregate-kpi-metric.ts +29 -69
  118. package/server/service/kpi-metric/kpi-metric-mutation.ts +15 -26
  119. package/server/service/kpi-metric/kpi-metric-type.ts +17 -12
  120. package/server/service/kpi-metric/kpi-metric.ts +32 -11
  121. package/server/service/kpi-metric-value/index.ts +7 -0
  122. package/server/service/kpi-metric-value/kpi-metric-value-mutation.ts +215 -0
  123. package/server/service/kpi-metric-value/kpi-metric-value-query.ts +60 -0
  124. package/server/service/kpi-metric-value/kpi-metric-value-type.ts +82 -0
  125. package/server/service/kpi-metric-value/kpi-metric-value.ts +91 -0
  126. package/server/service/kpi-value/kpi-value-mutation.ts +1 -2
  127. package/server/service/kpi-value/kpi-value-query.ts +1 -1
  128. package/server/service/kpi-value/kpi-value-type.ts +4 -16
  129. package/server/service/kpi-value/kpi-value.ts +14 -14
  130. package/things-factory.config.js +5 -3
  131. package/translations/en.json +8 -1
  132. package/translations/ja.json +8 -1
  133. package/translations/ko.json +9 -2
  134. package/translations/ms.json +9 -2
  135. package/translations/zh.json +8 -1
  136. package/dist-client/pages/kpi-value/kpi-value-manual-entry-form.js.map +0 -1
  137. 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.17",
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.15",
44
+ "@things-factory/dataset": "^9.0.19",
45
45
  "@things-factory/shell": "^9.0.0"
46
46
  },
47
- "gitHead": "125c0077d357017ce01c73bba49d805274ce6295"
47
+ "gitHead": "8a547ab7c7dd693d849121201dfeeeeefbc2f020"
48
48
  }
@@ -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 groupId = groupMap[key].group?.key01
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, groupId, groupType, version) 기준으로 저장
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, groupId, groupType, version, domain: { id: domainId } }
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.groupId = groupId
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 => Boolean, { description: "To modify multiple Kpis' information" })
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
- @ManyToOne(type => KpiCategory, { nullable: true })
52
- @Field(type => KpiCategory, { nullable: true, description: 'Parent category for hierarchical category structure.' })
53
- parent?: KpiCategory
51
+ @Column({ nullable: true })
52
+ @Field({ nullable: true, description: 'Aggregation formula using child KPI codes.' })
53
+ formula?: string
54
54
 
55
- @RelationId((category: KpiCategory) => category.parent)
56
- @Field({ nullable: true, description: 'ID of the parent category, if any.' })
57
- parentId?: string
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
- // 2. formula 있으면 산식 계산, 없으면 dataset 집계
26
+ // formula 분기 제거: metric은 항상 dataset 집계만 수행
27
27
  let values: any[] = []
28
- if (metric.formula) {
29
- // formula 파싱 및 metric code별 값 계산
30
- const formulaService = new KpiFormulaService()
31
- const metricCodes = formulaService.extractMetricCodes(metric.formula)
32
- // code별 집계 (동일 기간/그룹 기준)
33
- const codeValueMap: Record<string, any[]> = {}
34
- for (const code of metricCodes) {
35
- const subMetric = await getRepository(KpiMetric).findOne({ where: { name: code, domain: { id: domainId } } })
36
- if (!subMetric) throw new Error(`Formula metric '${code}' not found`)
37
- codeValueMap[code] = await aggregateKpiMetricValue(subMetric.id, domainId, context)
38
- }
39
- // group/date/period별로 값 매핑
40
- // (여기서는 가장 최근 기간 기준, group key 조합으로 매핑)
41
- const groupKey = v =>
42
- [v.date, v.period, v.group?.key01, v.group?.key02, v.group?.key03, v.group?.key04, v.group?.key05].join('|')
43
- const groupMap: Record<string, any> = {}
44
- for (const code of metricCodes) {
45
- for (const v of codeValueMap[code]) {
46
- const key = groupKey(v)
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
- return null
88
- })
89
- .filter(Boolean)
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, groupId, groupType 등 매핑
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 groupId = v.group?.key01
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, groupId, groupType, version) 기준으로 저장
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, groupId, groupType, version, domain: { id: domainId } }
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.groupId = groupId
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('metric', { description: 'Input object containing details for the new KPI metric.' }) metric: NewKpiMetric,
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 = metric.dataSetId
23
- ? await getRepository(DataSet).findOne({ where: { id: metric.dataSetId } })
23
+ let dataSet = kpiMetric.dataSetId
24
+ ? await getRepository(DataSet).findOne({ where: { id: kpiMetric.dataSetId } })
24
25
  : undefined
25
26
 
26
- if (metric.formula) {
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
- ...metric,
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 (metric.schedule) {
38
+ if (kpiMetric.schedule) {
44
39
  const handle = await registerSchedule({
45
- name: metric.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: metric.schedule,
55
- timezone: metric.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 = metric.timezone
71
+ result.timezone = kpiMetric.timezone
77
72
  await getRepository(KpiMetric, tx).save(result)
78
73
  }
79
74
 
80
- if (metric.thumbnail) {
75
+ if (kpiMetric.thumbnail) {
81
76
  await createAttachment(
82
77
  null,
83
78
  {
84
79
  attachment: {
85
- file: metric.thumbnail,
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
- if (patch.formula) {
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.schedule !== kpiMetric.schedule || !patch.schedule)) {
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 { ObjectRef, ScalarObject } from '@things-factory/shell'
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 = []