@things-factory/kpi 9.1.19 → 10.0.0-beta.2

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