@things-factory/kpi 9.2.5 → 10.0.0-beta.10

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 (139) 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/tsconfig.tsbuildinfo +1 -1
  43. package/package.json +18 -18
  44. package/client/tsconfig.json +0 -11
  45. package/dist-server/tsconfig.json +0 -10
  46. package/server/@types/index.d.ts +0 -11
  47. package/server/calculator/evaluator.ts +0 -45
  48. package/server/calculator/functions.ts +0 -67
  49. package/server/calculator/index.ts +0 -4
  50. package/server/calculator/parser.ts +0 -137
  51. package/server/calculator/provider.ts +0 -10
  52. package/server/controllers/index.ts +0 -2
  53. package/server/controllers/kpi-metric-value-provider.ts +0 -79
  54. package/server/controllers/kpi-value-provider.ts +0 -51
  55. package/server/index.ts +0 -6
  56. package/server/migrations/1752190849680-seed-kpi-metrics.ts +0 -124
  57. package/server/migrations/1752190849681-seed-kpi.ts +0 -356
  58. package/server/migrations/1752192090123-add-grades-to-kpi.ts +0 -67
  59. package/server/migrations/1752192090124-add-kpi-statistics.ts +0 -719
  60. package/server/migrations/1752192090128-seed-kpi-org-scope.ts +0 -132
  61. package/server/migrations/1752192090129-seed-kpi-values.ts +0 -207
  62. package/server/migrations/grade-data/x11-performance-table.json +0 -962
  63. package/server/migrations/grade-data/x12-performance-table.json +0 -611
  64. package/server/migrations/grade-data/x14-performance-table.json +0 -42
  65. package/server/migrations/grade-data/x21-performance-table.json +0 -889
  66. package/server/migrations/grade-data/x22-performance-table.json +0 -1064
  67. package/server/migrations/grade-data/x23-performance-table.json +0 -42
  68. package/server/migrations/grade-data/x31-performance-table.json +0 -644
  69. package/server/migrations/grade-data/x32-performance-table.json +0 -993
  70. package/server/migrations/grade-data/x33-performance-table.json +0 -195
  71. package/server/migrations/grade-data/x34-performance-table.json +0 -12
  72. package/server/migrations/grade-data/x35-performance-table.json +0 -42
  73. package/server/migrations/grade-data/x41-performance-table.json +0 -825
  74. package/server/migrations/grade-data/x42-performance-table.json +0 -786
  75. package/server/migrations/grade-data/x43-performance-table.json +0 -12
  76. package/server/migrations/grade-data/x44-performance-table.json +0 -42
  77. package/server/migrations/grade-data/x51-performance-table.json +0 -924
  78. package/server/migrations/grade-data/x52-performance-table.json +0 -42
  79. package/server/migrations/grade-data/x61-performance-table.json +0 -261
  80. package/server/migrations/grade-data/x62-performance-table.json +0 -42
  81. package/server/migrations/index.ts +0 -9
  82. package/server/migrations/seed-data/kpi-metrics-seed.json +0 -454
  83. package/server/migrations/seed-data/kpi-org-scope-seed.json +0 -1676
  84. package/server/migrations/seed-data/kpi-scopes-seed.json +0 -121
  85. package/server/migrations/seed-data/kpi-values-seed.json +0 -402
  86. package/server/migrations/seed-data/kpis-seed.json +0 -488
  87. package/server/migrations/seed-data/scope-definitions-seed.json +0 -90
  88. package/server/routes.ts +0 -81
  89. package/server/service/index.ts +0 -51
  90. package/server/service/kpi/aggregate-kpi.ts +0 -103
  91. package/server/service/kpi/event-subscriber.ts +0 -29
  92. package/server/service/kpi/index.ts +0 -9
  93. package/server/service/kpi/kpi-formula.service.ts +0 -164
  94. package/server/service/kpi/kpi-grade.types.ts +0 -28
  95. package/server/service/kpi/kpi-history.ts +0 -126
  96. package/server/service/kpi/kpi-mutation.ts +0 -553
  97. package/server/service/kpi/kpi-query.ts +0 -224
  98. package/server/service/kpi/kpi-type.ts +0 -151
  99. package/server/service/kpi/kpi.ts +0 -254
  100. package/server/service/kpi-alert/index.ts +0 -3
  101. package/server/service/kpi-alert/kpi-alert-query.ts +0 -59
  102. package/server/service/kpi-alert/kpi-alert-type.ts +0 -20
  103. package/server/service/kpi-metric/aggregate-kpi-metric.ts +0 -132
  104. package/server/service/kpi-metric/index.ts +0 -7
  105. package/server/service/kpi-metric/kpi-metric-mutation.ts +0 -309
  106. package/server/service/kpi-metric/kpi-metric-query.ts +0 -70
  107. package/server/service/kpi-metric/kpi-metric-type.ts +0 -111
  108. package/server/service/kpi-metric/kpi-metric.ts +0 -134
  109. package/server/service/kpi-metric-value/index.ts +0 -7
  110. package/server/service/kpi-metric-value/kpi-metric-value-mutation.ts +0 -270
  111. package/server/service/kpi-metric-value/kpi-metric-value-query.ts +0 -62
  112. package/server/service/kpi-metric-value/kpi-metric-value-type.ts +0 -82
  113. package/server/service/kpi-metric-value/kpi-metric-value.ts +0 -93
  114. package/server/service/kpi-org-scope/index.ts +0 -6
  115. package/server/service/kpi-org-scope/kpi-org-scope-mutation.ts +0 -173
  116. package/server/service/kpi-org-scope/kpi-org-scope-query.ts +0 -127
  117. package/server/service/kpi-org-scope/kpi-org-scope-type.ts +0 -68
  118. package/server/service/kpi-org-scope/kpi-org-scope.ts +0 -123
  119. package/server/service/kpi-scope/index.ts +0 -11
  120. package/server/service/kpi-scope/kpi-scope-mutation.ts +0 -129
  121. package/server/service/kpi-scope/kpi-scope-query.ts +0 -63
  122. package/server/service/kpi-scope/kpi-scope-type.ts +0 -96
  123. package/server/service/kpi-scope/kpi-scope.ts +0 -143
  124. package/server/service/kpi-statistic/index.ts +0 -7
  125. package/server/service/kpi-statistic/kpi-statistic-batch.service.ts +0 -231
  126. package/server/service/kpi-statistic/kpi-statistic-calculation.service.ts +0 -410
  127. package/server/service/kpi-statistic/kpi-statistic-mutation.ts +0 -291
  128. package/server/service/kpi-statistic/kpi-statistic-query.ts +0 -146
  129. package/server/service/kpi-statistic/kpi-statistic-type.ts +0 -152
  130. package/server/service/kpi-statistic/kpi-statistic.ts +0 -199
  131. package/server/service/kpi-value/index.ts +0 -7
  132. package/server/service/kpi-value/kpi-value-mutation.ts +0 -432
  133. package/server/service/kpi-value/kpi-value-query.ts +0 -61
  134. package/server/service/kpi-value/kpi-value-score.service.ts +0 -106
  135. package/server/service/kpi-value/kpi-value-type.ts +0 -122
  136. package/server/service/kpi-value/kpi-value.ts +0 -160
  137. package/server/service/utils/value-date-util.ts +0 -119
  138. package/server/tsconfig.json +0 -10
  139. 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
- }