@things-factory/kpi 9.0.13 → 9.0.14

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 (118) hide show
  1. package/client/pages/kpi/kpi-grade-editor.ts +294 -0
  2. package/client/pages/kpi/kpi-list-page.ts +69 -1
  3. package/client/pages/kpi/kpi-overview.ts +5 -11
  4. package/client/pages/kpi-dashboard/kpi-dashboard.ts +43 -15
  5. package/client/pages/kpi-dashboard/kpi-performance-summary.ts +17 -10
  6. package/client/pages/kpi-value/kpi-value-manual-entry-form.ts +1 -0
  7. package/client/route.ts +0 -4
  8. package/dist-client/pages/kpi/kpi-grade-editor.d.ts +35 -0
  9. package/dist-client/pages/kpi/kpi-grade-editor.js +278 -0
  10. package/dist-client/pages/kpi/kpi-grade-editor.js.map +1 -0
  11. package/dist-client/pages/kpi/kpi-list-page.d.ts +4 -0
  12. package/dist-client/pages/kpi/kpi-list-page.js +64 -1
  13. package/dist-client/pages/kpi/kpi-list-page.js.map +1 -1
  14. package/dist-client/pages/kpi/kpi-overview.js +3 -11
  15. package/dist-client/pages/kpi/kpi-overview.js.map +1 -1
  16. package/dist-client/pages/kpi-dashboard/kpi-dashboard.js +44 -12
  17. package/dist-client/pages/kpi-dashboard/kpi-dashboard.js.map +1 -1
  18. package/dist-client/pages/kpi-dashboard/kpi-performance-summary.js +17 -10
  19. package/dist-client/pages/kpi-dashboard/kpi-performance-summary.js.map +1 -1
  20. package/dist-client/pages/kpi-value/kpi-value-manual-entry-form.js +1 -0
  21. package/dist-client/pages/kpi-value/kpi-value-manual-entry-form.js.map +1 -1
  22. package/dist-client/route.d.ts +1 -1
  23. package/dist-client/route.js +0 -3
  24. package/dist-client/route.js.map +1 -1
  25. package/dist-client/tsconfig.tsbuildinfo +1 -1
  26. package/dist-server/index.d.ts +0 -1
  27. package/dist-server/index.js +0 -1
  28. package/dist-server/index.js.map +1 -1
  29. package/dist-server/service/index.d.ts +2 -4
  30. package/dist-server/service/index.js +0 -5
  31. package/dist-server/service/index.js.map +1 -1
  32. package/dist-server/service/kpi/aggregate-kpi.js +4 -0
  33. package/dist-server/service/kpi/aggregate-kpi.js.map +1 -1
  34. package/dist-server/service/kpi/kpi-grade.types.d.ts +21 -0
  35. package/dist-server/service/kpi/kpi-grade.types.js +3 -0
  36. package/dist-server/service/kpi/kpi-grade.types.js.map +1 -0
  37. package/dist-server/service/kpi/kpi-history.d.ts +2 -0
  38. package/dist-server/service/kpi/kpi-history.js +8 -0
  39. package/dist-server/service/kpi/kpi-history.js.map +1 -1
  40. package/dist-server/service/kpi/kpi-query.d.ts +0 -2
  41. package/dist-server/service/kpi/kpi-query.js +0 -11
  42. package/dist-server/service/kpi/kpi-query.js.map +1 -1
  43. package/dist-server/service/kpi/kpi-type.d.ts +3 -0
  44. package/dist-server/service/kpi/kpi-type.js +14 -0
  45. package/dist-server/service/kpi/kpi-type.js.map +1 -1
  46. package/dist-server/service/kpi/kpi.d.ts +2 -2
  47. package/dist-server/service/kpi/kpi.js +5 -3
  48. package/dist-server/service/kpi/kpi.js.map +1 -1
  49. package/dist-server/service/kpi-alert/kpi-alert-query.js +13 -13
  50. package/dist-server/service/kpi-alert/kpi-alert-query.js.map +1 -1
  51. package/dist-server/service/kpi-value/kpi-value-grade.service.d.ts +34 -0
  52. package/dist-server/service/kpi-value/kpi-value-grade.service.js +117 -0
  53. package/dist-server/service/kpi-value/kpi-value-grade.service.js.map +1 -0
  54. package/dist-server/service/kpi-value/kpi-value-mutation.d.ts +1 -0
  55. package/dist-server/service/kpi-value/kpi-value-mutation.js +15 -0
  56. package/dist-server/service/kpi-value/kpi-value-mutation.js.map +1 -1
  57. package/dist-server/service/kpi-value/kpi-value-query.d.ts +2 -0
  58. package/dist-server/service/kpi-value/kpi-value-query.js +12 -0
  59. package/dist-server/service/kpi-value/kpi-value-query.js.map +1 -1
  60. package/dist-server/tsconfig.tsbuildinfo +1 -1
  61. package/package.json +3 -3
  62. package/server/index.ts +0 -1
  63. package/server/service/index.ts +0 -5
  64. package/server/service/kpi/aggregate-kpi.ts +6 -0
  65. package/server/service/kpi/kpi-grade.types.ts +27 -0
  66. package/server/service/kpi/kpi-history.ts +9 -1
  67. package/server/service/kpi/kpi-query.ts +0 -6
  68. package/server/service/kpi/kpi-type.ts +13 -0
  69. package/server/service/kpi/kpi.ts +8 -4
  70. package/server/service/kpi-alert/kpi-alert-query.ts +13 -14
  71. package/server/service/kpi-value/kpi-value-grade.service.ts +127 -0
  72. package/server/service/kpi-value/kpi-value-mutation.ts +9 -0
  73. package/server/service/kpi-value/kpi-value-query.ts +7 -0
  74. package/things-factory.config.js +0 -1
  75. package/client/pages/kpi-grade/kpi-grade-importer.ts +0 -90
  76. package/client/pages/kpi-grade/kpi-grade-list-page.ts +0 -405
  77. package/dist-client/pages/kpi-grade/kpi-grade-importer.d.ts +0 -23
  78. package/dist-client/pages/kpi-grade/kpi-grade-importer.js +0 -92
  79. package/dist-client/pages/kpi-grade/kpi-grade-importer.js.map +0 -1
  80. package/dist-client/pages/kpi-grade/kpi-grade-list-page.d.ts +0 -66
  81. package/dist-client/pages/kpi-grade/kpi-grade-list-page.js +0 -387
  82. package/dist-client/pages/kpi-grade/kpi-grade-list-page.js.map +0 -1
  83. package/dist-server/migrations/1752188906708-SeedKpiCategory.d.ts +0 -5
  84. package/dist-server/migrations/1752188906708-SeedKpiCategory.js +0 -56
  85. package/dist-server/migrations/1752188906708-SeedKpiCategory.js.map +0 -1
  86. package/dist-server/migrations/1752190849681-SeedKpi.d.ts +0 -5
  87. package/dist-server/migrations/1752190849681-SeedKpi.js +0 -107
  88. package/dist-server/migrations/1752190849681-SeedKpi.js.map +0 -1
  89. package/dist-server/migrations/1752191090459-SeedKpiGrade.d.ts +0 -5
  90. package/dist-server/migrations/1752191090459-SeedKpiGrade.js +0 -271
  91. package/dist-server/migrations/1752191090459-SeedKpiGrade.js.map +0 -1
  92. package/dist-server/migrations/index.d.ts +0 -1
  93. package/dist-server/migrations/index.js +0 -12
  94. package/dist-server/migrations/index.js.map +0 -1
  95. package/dist-server/service/kpi-grade/index.d.ts +0 -6
  96. package/dist-server/service/kpi-grade/index.js +0 -10
  97. package/dist-server/service/kpi-grade/index.js.map +0 -1
  98. package/dist-server/service/kpi-grade/kpi-grade-mutation.d.ts +0 -10
  99. package/dist-server/service/kpi-grade/kpi-grade-mutation.js +0 -151
  100. package/dist-server/service/kpi-grade/kpi-grade-mutation.js.map +0 -1
  101. package/dist-server/service/kpi-grade/kpi-grade-query.d.ts +0 -13
  102. package/dist-server/service/kpi-grade/kpi-grade-query.js +0 -92
  103. package/dist-server/service/kpi-grade/kpi-grade-query.js.map +0 -1
  104. package/dist-server/service/kpi-grade/kpi-grade-type.d.ts +0 -29
  105. package/dist-server/service/kpi-grade/kpi-grade-type.js +0 -113
  106. package/dist-server/service/kpi-grade/kpi-grade-type.js.map +0 -1
  107. package/dist-server/service/kpi-grade/kpi-grade.d.ts +0 -24
  108. package/dist-server/service/kpi-grade/kpi-grade.js +0 -117
  109. package/dist-server/service/kpi-grade/kpi-grade.js.map +0 -1
  110. package/server/migrations/1752188906708-SeedKpiCategory.ts +0 -61
  111. package/server/migrations/1752190849681-SeedKpi.ts +0 -112
  112. package/server/migrations/1752191090459-SeedKpiGrade.ts +0 -270
  113. package/server/migrations/index.ts +0 -9
  114. package/server/service/kpi-grade/index.ts +0 -7
  115. package/server/service/kpi-grade/kpi-grade-mutation.ts +0 -146
  116. package/server/service/kpi-grade/kpi-grade-query.ts +0 -58
  117. package/server/service/kpi-grade/kpi-grade-type.ts +0 -82
  118. package/server/service/kpi-grade/kpi-grade.ts +0 -101
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@things-factory/kpi",
3
- "version": "9.0.13",
3
+ "version": "9.0.14",
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.13",
44
+ "@things-factory/dataset": "^9.0.14",
45
45
  "@things-factory/shell": "^9.0.0"
46
46
  },
47
- "gitHead": "8629e384d819c0d232f76def3e04ed0c1030a3d0"
47
+ "gitHead": "b092ace8cad6c994b41ce6c06b929085d886b134"
48
48
  }
package/server/index.ts CHANGED
@@ -1,4 +1,3 @@
1
1
  export * from './service/index.js'
2
- export * from './migrations/index.js'
3
2
 
4
3
  import './routes'
@@ -3,8 +3,6 @@ export * from './kpi/kpi'
3
3
  export * from './kpi/kpi-type'
4
4
  export * from './kpi-category/kpi-category'
5
5
  export * from './kpi-category/kpi-category-type'
6
- export * from './kpi-grade/kpi-grade'
7
- export * from './kpi-grade/kpi-grade-type'
8
6
  export * from './kpi-value/kpi-value'
9
7
  export * from './kpi-value/kpi-value-type'
10
8
  export * from './kpi-metric/kpi-metric'
@@ -14,7 +12,6 @@ export * from './kpi-alert'
14
12
  /* IMPORT ENTITIES AND RESOLVERS */
15
13
  import { entities as KpiEntities, resolvers as KpiResolvers } from './kpi'
16
14
  import { entities as KpiCategoryEntities, resolvers as KpiCategoryResolvers } from './kpi-category'
17
- import { entities as KpiGradeEntities, resolvers as KpiGradeResolvers } from './kpi-grade'
18
15
  import { entities as KpiValueEntities, resolvers as KpiValueResolvers } from './kpi-value'
19
16
  import { entities as KpiMetricEntities, resolvers as KpiMetricResolvers } from './kpi-metric'
20
17
  import { resolvers as KpiAlertResolvers } from './kpi-alert'
@@ -23,7 +20,6 @@ export const entities = [
23
20
  /* ENTITIES */
24
21
  ...KpiEntities,
25
22
  ...KpiCategoryEntities,
26
- ...KpiGradeEntities,
27
23
  ...KpiValueEntities,
28
24
  ...KpiMetricEntities
29
25
  ]
@@ -33,7 +29,6 @@ export const schema = {
33
29
  /* RESOLVER CLASSES */
34
30
  ...KpiResolvers,
35
31
  ...KpiCategoryResolvers,
36
- ...KpiGradeResolvers,
37
32
  ...KpiValueResolvers,
38
33
  ...KpiMetricResolvers,
39
34
  ...KpiAlertResolvers
@@ -4,6 +4,7 @@ import { KpiMetric } from '../kpi-metric/kpi-metric'
4
4
  import { KpiValue, KpiValueInputType } from '../kpi-value/kpi-value'
5
5
  import { KpiFormulaService } from './kpi-formula.service'
6
6
  import { aggregateKpiMetricValue } from '../kpi-metric/aggregate-kpi-metric'
7
+ import { KpiValueGradeService } from '../kpi-value/kpi-value-grade.service'
7
8
 
8
9
  /**
9
10
  * KPI 단위 집계/산식 자동화 함수
@@ -14,6 +15,7 @@ import { aggregateKpiMetricValue } from '../kpi-metric/aggregate-kpi-metric'
14
15
  */
15
16
  export async function aggregateKpiValue(kpiId: string, domainId: string, context: ResolverContext) {
16
17
  const tx = context.state?.tx || getRepository(Kpi).manager
18
+ const gradeService = new KpiValueGradeService()
17
19
 
18
20
  // 1. KPI 정보 조회
19
21
  const kpi = await getRepository(Kpi).findOne({ where: { id: kpiId, domain: { id: domainId } } })
@@ -75,6 +77,10 @@ export async function aggregateKpiValue(kpiId: string, domainId: string, context
75
77
  entity.domain = kpi.domain
76
78
  entity.creator = context.state?.user
77
79
  entity.updater = context.state?.user
80
+
81
+ // 등급 자동 계산 및 저장
82
+ await gradeService.calculateAndSaveGrade(entity, kpi)
83
+
78
84
  entity = await repo.save(entity)
79
85
  savedValues.push(entity)
80
86
  }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * KPI 등급 설정 타입 정의
3
+ */
4
+ export interface KpiGrade {
5
+ /** 등급명 (예: A, B, C, 우수, 양호 등) */
6
+ name: string
7
+
8
+ /** 등급의 최소값 */
9
+ minValue: number
10
+
11
+ /** 등급의 최대값 */
12
+ maxValue: number
13
+
14
+ /** 등급 점수 (선택사항) */
15
+ score?: number
16
+
17
+ /** 등급 색상 (선택사항) */
18
+ color?: string
19
+
20
+ /** 등급 설명 (선택사항) */
21
+ description?: string
22
+ }
23
+
24
+ /**
25
+ * KPI 등급 배열 타입
26
+ */
27
+ export type KpiGrades = KpiGrade[]
@@ -9,10 +9,11 @@ import {
9
9
  } from '@operato/typeorm-history'
10
10
  import { Role, User } from '@things-factory/auth-base'
11
11
  import { config } from '@things-factory/env'
12
- import { Domain } from '@things-factory/shell'
12
+ import { Domain, ScalarObject } from '@things-factory/shell'
13
13
 
14
14
  import { Kpi, KpiStatus } from './kpi'
15
15
  import { KpiCategory } from '../kpi-category/kpi-category'
16
+ import { KpiGrades } from './kpi-grade.types'
16
17
 
17
18
  const ORMCONFIG = config.get('ormconfig', {})
18
19
  const DATABASE_TYPE = ORMCONFIG.type
@@ -69,6 +70,13 @@ export class KpiHistory implements HistoryEntityInterface<Kpi> {
69
70
  @Field(type => String, { nullable: true, description: 'Thumbnail image or file path for this KPI.' })
70
71
  thumbnail?: string
71
72
 
73
+ @Column({ type: 'simple-json', nullable: true })
74
+ @Field(type => ScalarObject, {
75
+ nullable: true,
76
+ description: 'Grade configuration for this KPI version'
77
+ })
78
+ grades?: KpiGrades
79
+
72
80
  @Column({ nullable: true })
73
81
  @Field({ nullable: true })
74
82
  createdAt?: Date
@@ -4,7 +4,6 @@ import { Domain, getQueryBuilderFromListParams, getRepository, ListParam } from
4
4
  import { User } from '@things-factory/auth-base'
5
5
  import { Kpi } from './kpi'
6
6
  import { KpiList } from './kpi-type'
7
- import { KpiGrade } from '../kpi-grade/kpi-grade'
8
7
  import { KpiValue } from '../kpi-value/kpi-value'
9
8
  import { KpiHistory } from './kpi-history'
10
9
  import { Int } from 'type-graphql'
@@ -53,11 +52,6 @@ export class KpiQuery {
53
52
  return attachment?.fullpath
54
53
  }
55
54
 
56
- @FieldResolver(type => [KpiGrade])
57
- async grades(@Root() kpi: Kpi): Promise<KpiGrade[]> {
58
- return await getRepository(KpiGrade).find({ where: { domain: { id: kpi.domainId }, kpi: { id: kpi.id } } })
59
- }
60
-
61
55
  @FieldResolver(type => Domain)
62
56
  async domain(@Root() kpi: Kpi): Promise<Domain> {
63
57
  return kpi.domainId && (await getRepository(Domain).findOneBy({ id: kpi.domainId }))
@@ -5,6 +5,7 @@ import { ObjectType, Field, InputType, Int, ID, registerEnumType } from 'type-gr
5
5
  import { ObjectRef, ScalarObject } from '@things-factory/shell'
6
6
 
7
7
  import { Kpi, KpiStatus } from './kpi'
8
+ import { KpiGrades } from './kpi-grade.types'
8
9
 
9
10
  @InputType({ description: 'Input type for creating a new KPI. Used in mutations to provide KPI details.' })
10
11
  export class NewKpi {
@@ -50,6 +51,12 @@ export class NewKpi {
50
51
 
51
52
  @Field({ nullable: true, description: 'Timezone for the KPI schedule.' })
52
53
  timezone?: string
54
+
55
+ @Field(type => ScalarObject, {
56
+ nullable: true,
57
+ description: 'Grade configuration for this KPI version'
58
+ })
59
+ grades?: KpiGrades
53
60
  }
54
61
 
55
62
  @InputType({ description: 'Input type for updating an existing KPI. Used in mutations to patch KPI details.' })
@@ -102,6 +109,12 @@ export class KpiPatch {
102
109
 
103
110
  @Field({ nullable: true, description: 'Timezone for the KPI schedule.' })
104
111
  timezone?: string
112
+
113
+ @Field(type => ScalarObject, {
114
+ nullable: true,
115
+ description: 'Grade configuration for this KPI version'
116
+ })
117
+ grades?: KpiGrades
105
118
  }
106
119
 
107
120
  @ObjectType()
@@ -16,10 +16,11 @@ import { ObjectType, Field, Int, ID, registerEnumType } from 'type-graphql'
16
16
  import { Domain } from '@things-factory/shell'
17
17
  import { User } from '@things-factory/auth-base'
18
18
  import { KpiCategory } from '../kpi-category/kpi-category'
19
- import { KpiGrade } from '../kpi-grade/kpi-grade'
20
19
  import { config } from '@things-factory/env'
21
20
  import { ScalarObject } from '@things-factory/shell'
22
21
 
22
+ import { KpiGrades } from './kpi-grade.types'
23
+
23
24
  const ORMCONFIG = config.get('ormconfig', {})
24
25
  const DATABASE_TYPE = ORMCONFIG.type
25
26
 
@@ -168,7 +169,10 @@ export class Kpi {
168
169
  @Field(type => String, { nullable: true, description: 'Thumbnail image or file path for this KPI.' })
169
170
  thumbnail?: string
170
171
 
171
- @OneToMany(type => KpiGrade, kpiGrade => kpiGrade.kpi)
172
- @Field(type => [KpiGrade], { nullable: true, description: 'List of grades for this KPI.' })
173
- grades?: KpiGrade[]
172
+ @Column({ type: 'simple-json', nullable: true })
173
+ @Field(type => ScalarObject, {
174
+ nullable: true,
175
+ description: 'Grade configuration for this KPI version'
176
+ })
177
+ grades?: KpiGrades
174
178
  }
@@ -2,7 +2,6 @@ import { Resolver, Query, Ctx } from 'type-graphql'
2
2
  import { getRepository } from '@things-factory/shell'
3
3
  import { Kpi } from '../kpi/kpi'
4
4
  import { KpiValue } from '../kpi-value/kpi-value'
5
- import { KpiGrade } from '../kpi-grade/kpi-grade'
6
5
  import { KpiAlert } from './kpi-alert-type'
7
6
 
8
7
  @Resolver()
@@ -11,7 +10,7 @@ export class KpiAlertQuery {
11
10
  async kpiAlerts(@Ctx() context): Promise<KpiAlert[]> {
12
11
  const { domain } = context.state
13
12
  // 1. KPI 전체 조회
14
- const kpis = await getRepository(Kpi).find({ where: { domain: { id: domain.id } }, relations: ['grades'] })
13
+ const kpis = await getRepository(Kpi).find({ where: { domain: { id: domain.id } } })
15
14
  // 2. KPI별 최신 실적값 조회
16
15
  const alerts: KpiAlert[] = []
17
16
  for (const kpi of kpis) {
@@ -41,18 +40,18 @@ export class KpiAlertQuery {
41
40
  })
42
41
  }
43
42
  // 등급 기준 경고(예: 최하위 등급)
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
- }
43
+ // if (kpi.grades?.length) {
44
+ // const grade = kpi.grades.find(g => value.value >= g.minValue && value.value <= g.maxValue)
45
+ // if (grade && grade.score !== undefined && grade.score <= 2) {
46
+ // alerts.push({
47
+ // id: `${kpi.id}-grade-low`,
48
+ // kpi,
49
+ // message: `${kpi.name} 등급(${grade.name})(${grade.score}점)`,
50
+ // level: 'critical',
51
+ // createdAt: new Date()
52
+ // })
53
+ // }
54
+ // }
56
55
  }
57
56
  return alerts
58
57
  }
@@ -0,0 +1,127 @@
1
+ import { getRepository } from '@things-factory/shell'
2
+ import { ObjectType, Field } from 'type-graphql'
3
+ import { Kpi } from '../kpi/kpi'
4
+ import { KpiValue } from './kpi-value'
5
+ import { KpiGrades } from '../kpi/kpi-grade.types'
6
+
7
+ export interface GradeResult {
8
+ name: string
9
+ score?: number
10
+ color?: string
11
+ description?: string
12
+ minValue: number
13
+ maxValue: number
14
+ }
15
+
16
+ @ObjectType({ description: 'Grade information for KPI value' })
17
+ export class GradeInfo {
18
+ @Field({ description: 'Grade name (e.g., A, B, C, 우수, 양호)' })
19
+ name: string
20
+
21
+ @Field({ nullable: true, description: 'Grade score or performance value' })
22
+ score?: number
23
+
24
+ @Field({ nullable: true, description: 'Color code for visualization' })
25
+ color?: string
26
+
27
+ @Field({ nullable: true, description: 'Grade description' })
28
+ description?: string
29
+ }
30
+
31
+ export class KpiValueGradeService {
32
+ /**
33
+ * KPI의 grades 정보를 이용하여 KpiValue의 등급 계산
34
+ */
35
+ calculateGradeFromKpiGrades(kpi: Kpi, value: number): GradeResult | null {
36
+ if (!kpi.grades || kpi.grades.length === 0) {
37
+ return null
38
+ }
39
+
40
+ // grades 배열에서 해당 값의 범위에 맞는 등급 찾기
41
+ const grade = kpi.grades.find(g => value >= g.minValue && value <= g.maxValue)
42
+
43
+ if (!grade) {
44
+ return null
45
+ }
46
+
47
+ return {
48
+ name: grade.name,
49
+ score: grade.score,
50
+ color: grade.color,
51
+ description: grade.description,
52
+ minValue: grade.minValue,
53
+ maxValue: grade.maxValue
54
+ }
55
+ }
56
+
57
+ /**
58
+ * KpiValue 생성/수정 시 등급 자동 계산 및 저장
59
+ */
60
+ async calculateAndSaveGrade(kpiValue: KpiValue, kpi: Kpi): Promise<void> {
61
+ const gradeResult = this.calculateGradeFromKpiGrades(kpi, kpiValue.value)
62
+
63
+ if (gradeResult) {
64
+ // KpiValue의 meta 필드에 등급 정보 저장
65
+ kpiValue.meta = {
66
+ ...kpiValue.meta,
67
+ grade: {
68
+ name: gradeResult.name,
69
+ score: gradeResult.score,
70
+ color: gradeResult.color,
71
+ description: gradeResult.description,
72
+ calculatedAt: new Date().toISOString()
73
+ }
74
+ }
75
+ }
76
+ }
77
+
78
+ /**
79
+ * 기존 KpiValue들의 등급 재계산 (배치 처리)
80
+ */
81
+ async recalculateGradesForKpi(kpiId: string, context: ResolverContext): Promise<void> {
82
+ const kpi = await getRepository(Kpi).findOne({ where: { id: kpiId } })
83
+ if (!kpi) return
84
+
85
+ const kpiValues = await getRepository(KpiValue).find({
86
+ where: { kpiId, version: kpi.version }
87
+ })
88
+
89
+ for (const kpiValue of kpiValues) {
90
+ await this.calculateAndSaveGrade(kpiValue, kpi)
91
+ }
92
+
93
+ await getRepository(KpiValue).save(kpiValues)
94
+ }
95
+
96
+ /**
97
+ * 특정 KpiValue의 등급 정보 조회
98
+ */
99
+ async getGradeForKpiValue(kpiValue: KpiValue): Promise<GradeInfo | null> {
100
+ // meta 필드에서 등급 정보 조회
101
+ if (kpiValue.meta?.grade) {
102
+ return {
103
+ name: kpiValue.meta.grade.name,
104
+ score: kpiValue.meta.grade.score,
105
+ color: kpiValue.meta.grade.color,
106
+ description: kpiValue.meta.grade.description
107
+ }
108
+ }
109
+
110
+ // 실시간 계산 (필요시)
111
+ const kpi = await getRepository(Kpi).findOne({ where: { id: kpiValue.kpiId } })
112
+ if (kpi) {
113
+ const gradeResult = this.calculateGradeFromKpiGrades(kpi, kpiValue.value)
114
+
115
+ if (gradeResult) {
116
+ return {
117
+ name: gradeResult.name,
118
+ score: gradeResult.score,
119
+ color: gradeResult.color,
120
+ description: gradeResult.description
121
+ }
122
+ }
123
+ }
124
+
125
+ return null
126
+ }
127
+ }
@@ -6,6 +6,7 @@ import { KpiValue } from './kpi-value'
6
6
  import { NewKpiValue, KpiValuePatch } from './kpi-value-type'
7
7
  import { Kpi } from '../kpi/kpi'
8
8
  import { KpiValueInputType } from './kpi-value'
9
+ import { KpiValueGradeService } from './kpi-value-grade.service'
9
10
 
10
11
  @Resolver(KpiValue)
11
12
  export class KpiValueMutation {
@@ -172,4 +173,12 @@ export class KpiValueMutation {
172
173
 
173
174
  return true
174
175
  }
176
+
177
+ @Directive('@transaction')
178
+ @Mutation(returns => Boolean, { description: 'Recalculate grades for all KpiValues of a specific KPI' })
179
+ async recalculateGradesForKpi(@Arg('kpiId') kpiId: string, @Ctx() context: ResolverContext): Promise<boolean> {
180
+ const gradeService = new KpiValueGradeService()
181
+ await gradeService.recalculateGradesForKpi(kpiId, context)
182
+ return true
183
+ }
175
184
  }
@@ -4,6 +4,7 @@ import { User } from '@things-factory/auth-base'
4
4
  import { KpiValue } from './kpi-value'
5
5
  import { KpiValueList } from './kpi-value-type'
6
6
  import { Kpi } from '../kpi/kpi'
7
+ import { KpiValueGradeService, GradeInfo } from './kpi-value-grade.service'
7
8
 
8
9
  @Resolver(KpiValue)
9
10
  export class KpiValueQuery {
@@ -55,4 +56,10 @@ export class KpiValueQuery {
55
56
  if (!kpiValue.kpiId) return null
56
57
  return await getRepository(Kpi).findOneBy({ id: kpiValue.kpiId })
57
58
  }
59
+
60
+ @FieldResolver(type => GradeInfo, { nullable: true })
61
+ async grade(@Root() kpiValue: KpiValue): Promise<GradeInfo | null> {
62
+ const gradeService = new KpiValueGradeService()
63
+ return await gradeService.getGradeForKpiValue(kpiValue)
64
+ }
58
65
  }
@@ -11,7 +11,6 @@ export default {
11
11
  { tagname: 'kpi-metric-list-page', page: 'kpi-metric-list' },
12
12
  { tagname: 'kpi-value-list-page', page: 'kpi-value-list' },
13
13
  { tagname: 'kpi-value-manual-entry-page', page: 'kpi-value-manual-entry' },
14
- { tagname: 'kpi-grade-list-page', page: 'kpi-grade-list' },
15
14
  { tagname: 'kpi-history-list-page', page: 'kpi-history-list' }
16
15
  ]
17
16
  // bootstrap
@@ -1,90 +0,0 @@
1
- import '@material/web/icon/icon.js'
2
- import '@operato/data-grist'
3
-
4
- import gql from 'graphql-tag'
5
- import { css, html, LitElement } from 'lit'
6
- import { property } from 'lit/decorators.js'
7
-
8
- import { client } from '@operato/graphql'
9
- import { i18next } from '@operato/i18n'
10
- import { isMobileDevice } from '@operato/utils'
11
- import { ButtonContainerStyles } from '@operato/styles'
12
-
13
- export class KpiGradeImporter extends LitElement {
14
- static styles = [
15
- ButtonContainerStyles,
16
- css`
17
- :host {
18
- display: flex;
19
- flex-direction: column;
20
-
21
- background-color: #fff;
22
- }
23
-
24
- ox-grist {
25
- flex: 1;
26
- }
27
- `
28
- ]
29
-
30
- @property({ type: Array }) kpiGrades: any[] = []
31
- @property({ type: Object }) columns = {
32
- list: { fields: ['name', 'description'] },
33
- pagination: { infinite: true },
34
- columns: [
35
- {
36
- type: 'string',
37
- name: 'name',
38
- header: i18next.t('field.name'),
39
- width: 150
40
- },
41
- {
42
- type: 'string',
43
- name: 'description',
44
- header: i18next.t('field.description'),
45
- width: 200
46
- },
47
- {
48
- type: 'checkbox',
49
- name: 'active',
50
- header: i18next.t('field.active'),
51
- width: 60
52
- }
53
- ]
54
- }
55
-
56
-
57
- render() {
58
- return html`
59
- <ox-grist
60
- .mode=${isMobileDevice() ? 'LIST' : 'GRID'}
61
- .config=${this.columns}
62
- .data=${
63
- {
64
- records: this.kpiGrades
65
- }
66
- }
67
- ></ox-grist>
68
-
69
- <div class="button-container">
70
- <button @click="${this.save.bind(this)}"><md-icon>save</md-icon>${i18next.t('button.save')}</button>
71
- </div>
72
- `
73
- }
74
-
75
- async save() {
76
- const response = await client.mutate({
77
- mutation: gql`
78
- mutation importKpiGrades($kpiGrades: [KpiGradePatch!]!) {
79
- importKpiGrades(kpiGrades: $kpiGrades)
80
- }
81
- `,
82
- variables: { kpiGrades: this.kpiGrades }
83
- })
84
-
85
- if (response.errors?.length) return
86
-
87
- this.dispatchEvent(new CustomEvent('imported'))
88
- }
89
- }
90
-