@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,410 +0,0 @@
1
- import { getRepository } from '@things-factory/shell'
2
- import type ResolverContext from '@things-factory/auth-base'
3
- import { KpiStatistic } from './kpi-statistic'
4
- import { KpiValue } from '../kpi-value/kpi-value'
5
- import { Kpi, KpiPeriodType } from '../kpi/kpi'
6
- import { KpiOrgScope } from '../kpi-org-scope/kpi-org-scope'
7
- import { KpiScope } from '../kpi-scope/kpi-scope'
8
-
9
- /**
10
- * KPI 통계 계산 서비스
11
- * KpiValue 데이터로부터 통계값을 계산하여 KpiStatistic에 저장
12
- */
13
- export class KpiStatisticCalculationService {
14
- /**
15
- * 전체 KPI에 대해 통계를 계산
16
- */
17
- static async calculateAllStatistics(
18
- periodType: KpiPeriodType,
19
- valueDate: string,
20
- context: ResolverContext
21
- ): Promise<KpiStatistic[]> {
22
- const { domain } = context.state
23
-
24
- // Leaf KPI들만 대상으로 함 (실제 값을 가진 KPI)
25
- const leafKpis = await getRepository(Kpi).find({
26
- where: { domain: { id: domain.id }, isLeaf: true }
27
- })
28
-
29
- const results: KpiStatistic[] = []
30
-
31
- for (const kpi of leafKpis) {
32
- // 1. 전체 통계 (scope 없음)
33
- const overallStats = await this.calculateKpiStatistics(
34
- kpi.id,
35
- periodType,
36
- valueDate,
37
- null, // kpiOrgScope 없음 = 전체 통계
38
- context
39
- )
40
- if (overallStats) results.push(overallStats)
41
-
42
- // 2. 스코프별 통계
43
- const scopedStats = await this.calculateScopedStatistics(kpi.id, periodType, valueDate, context)
44
- results.push(...scopedStats)
45
- }
46
-
47
- return results
48
- }
49
-
50
- /**
51
- * 특정 KPI의 통계를 계산
52
- */
53
- static async calculateKpiStatistics(
54
- kpiId: string,
55
- periodType: KpiPeriodType,
56
- valueDate: string,
57
- kpiOrgScope: KpiOrgScope | null,
58
- context: ResolverContext
59
- ): Promise<KpiStatistic | null> {
60
- const { domain, user, tx } = context.state
61
-
62
- // KPI 값들 조회
63
- const whereCondition: any = {
64
- kpi: { id: kpiId },
65
- domain: { id: domain.id },
66
- valueDate: this.buildDateFilter(valueDate, periodType)
67
- }
68
-
69
- if (kpiOrgScope) {
70
- whereCondition.kpiOrgScope = { id: kpiOrgScope.id }
71
- }
72
-
73
- const kpiValues = await getRepository(KpiValue, tx).find({
74
- where: whereCondition,
75
- relations: ['kpi', 'kpiOrgScope']
76
- })
77
-
78
- if (kpiValues.length === 0) {
79
- return null
80
- }
81
-
82
- // 통계 계산
83
- const statistics = this.calculateStatistics(kpiValues.map(v => v.value).filter(v => v !== null))
84
-
85
- // KpiStatistic 엔티티 생성/업데이트
86
- const kpiStatisticRepo = getRepository(KpiStatistic, tx)
87
-
88
- // 기존 통계 조회
89
- const existingWhere: any = {
90
- kpi: { id: kpiId },
91
- domain: { id: domain.id },
92
- periodType,
93
- valueDate
94
- }
95
-
96
- if (kpiOrgScope) {
97
- existingWhere.kpiOrgScope = { id: kpiOrgScope.id }
98
- } else {
99
- existingWhere.kpiOrgScope = null
100
- }
101
-
102
- let kpiStatistic = await kpiStatisticRepo.findOne({ where: existingWhere })
103
-
104
- if (!kpiStatistic) {
105
- kpiStatistic = kpiStatisticRepo.create()
106
- kpiStatistic.kpi = kpiValues[0].kpi
107
- kpiStatistic.kpiId = kpiId
108
- kpiStatistic.domain = { id: domain.id } as any
109
- kpiStatistic.periodType = periodType
110
- kpiStatistic.valueDate = valueDate
111
- kpiStatistic.creator = user
112
- kpiStatistic.kpiOrgScope = kpiOrgScope
113
- kpiStatistic.kpiOrgScopeId = kpiOrgScope?.id
114
- }
115
-
116
- // 통계값 적용
117
- Object.assign(kpiStatistic, statistics)
118
- kpiStatistic.updater = user
119
- kpiStatistic.metadata = {
120
- ...kpiStatistic.metadata,
121
- calculationMethod: 'auto-calculated',
122
- lastCalculated: new Date(),
123
- dataCount: kpiValues.length,
124
- calculatedFrom: 'kpi-values'
125
- }
126
-
127
- return await kpiStatisticRepo.save(kpiStatistic)
128
- }
129
-
130
- /**
131
- * 스코프별 통계 계산 (KpiScope 기반)
132
- */
133
- static async calculateScopedStatistics(
134
- kpiId: string,
135
- periodType: KpiPeriodType,
136
- valueDate: string,
137
- context: ResolverContext
138
- ): Promise<KpiStatistic[]> {
139
- const { domain } = context.state
140
-
141
- // 통계 계산에 사용할 스코프 정의 조회
142
- const kpiScopes = await getRepository(KpiScope).find({
143
- where: {
144
- domain: { id: domain.id },
145
- active: true,
146
- includeInStatistics: true
147
- },
148
- order: { displayOrder: 'ASC' }
149
- })
150
-
151
- const results: KpiStatistic[] = []
152
-
153
- for (const kpiScope of kpiScopes) {
154
- const scopedStats = await this.calculateStatisticsByScope(kpiId, periodType, valueDate, kpiScope, context)
155
- results.push(...scopedStats)
156
- }
157
-
158
- return results
159
- }
160
-
161
- /**
162
- * 특정 스코프 차원별 통계 계산
163
- */
164
- static async calculateStatisticsByScope(
165
- kpiId: string,
166
- periodType: KpiPeriodType,
167
- valueDate: string,
168
- kpiScope: KpiScope,
169
- context: ResolverContext
170
- ): Promise<KpiStatistic[]> {
171
- const { domain } = context.state
172
- const scopeLevel = kpiScope.level
173
- const fieldName = `scope${scopeLevel.toString().padStart(2, '0')}`
174
-
175
- // 해당 스코프 레벨의 유니크한 값들 조회
176
- const scopeValues = kpiScope.validValues || (await this.getDistinctScopeValues(fieldName, domain.id))
177
-
178
- const results: KpiStatistic[] = []
179
-
180
- for (const scopeValue of scopeValues) {
181
- // 해당 스코프 값을 가진 KpiOrgScope들 조회
182
- const kpiOrgScopes = await getRepository(KpiOrgScope).find({
183
- where: {
184
- domain: { id: domain.id },
185
- [fieldName]: scopeValue
186
- }
187
- })
188
-
189
- // 각 KpiOrgScope에 대해 통계 계산
190
- for (const orgScope of kpiOrgScopes) {
191
- const statistic = await this.calculateKpiStatistics(kpiId, periodType, valueDate, orgScope, context)
192
- if (statistic) {
193
- results.push(statistic)
194
- }
195
- }
196
-
197
- // 스코프 값별 집계 통계도 생성 (선택적)
198
- if (kpiOrgScopes.length > 1) {
199
- const aggregatedStats = await this.calculateAggregatedScopeStatistics(
200
- kpiId,
201
- periodType,
202
- valueDate,
203
- kpiScope,
204
- scopeValue,
205
- kpiOrgScopes,
206
- context
207
- )
208
- if (aggregatedStats) {
209
- results.push(aggregatedStats)
210
- }
211
- }
212
- }
213
-
214
- return results
215
- }
216
-
217
- /**
218
- * 스코프 값별 집계 통계 계산 (예: 서울 전체 평균)
219
- */
220
- private static async calculateAggregatedScopeStatistics(
221
- kpiId: string,
222
- periodType: KpiPeriodType,
223
- valueDate: string,
224
- kpiScope: KpiScope,
225
- scopeValue: string,
226
- kpiOrgScopes: KpiOrgScope[],
227
- context: ResolverContext
228
- ): Promise<KpiStatistic | null> {
229
- const { domain, user, tx } = context.state
230
-
231
- // 모든 KpiOrgScope에서 KpiValue들 수집
232
- const allValues: number[] = []
233
- for (const orgScope of kpiOrgScopes) {
234
- const kpiValues = await getRepository(KpiValue, tx).find({
235
- where: {
236
- kpi: { id: kpiId },
237
- domain: { id: domain.id },
238
- kpiOrgScope: { id: orgScope.id },
239
- valueDate: this.buildDateFilter(valueDate, periodType)
240
- }
241
- })
242
- allValues.push(...kpiValues.map(v => v.value).filter(v => v !== null))
243
- }
244
-
245
- if (allValues.length === 0) return null
246
-
247
- // 집계 통계 계산
248
- const statistics = this.calculateStatistics(allValues)
249
-
250
- // 가상의 KpiOrgScope 생성 (집계용)
251
- const aggregatedOrgScope = {
252
- id: `aggregated-${kpiScope.level}-${scopeValue}`,
253
- entityType: 'AGGREGATED',
254
- entityName: `${kpiScope.name}: ${scopeValue}`,
255
- org: scopeValue,
256
- [`scope${kpiScope.level.toString().padStart(2, '0')}`]: scopeValue
257
- }
258
-
259
- // KpiStatistic 생성
260
- const kpiStatisticRepo = getRepository(KpiStatistic, tx)
261
- const kpiStatistic = kpiStatisticRepo.create()
262
-
263
- const kpi = await getRepository(Kpi).findOneBy({ id: kpiId })
264
- kpiStatistic.kpi = kpi
265
- kpiStatistic.kpiId = kpiId
266
- kpiStatistic.domain = { id: domain.id } as any
267
- kpiStatistic.periodType = periodType
268
- kpiStatistic.valueDate = valueDate
269
- kpiStatistic.creator = user
270
- kpiStatistic.updater = user
271
-
272
- // 통계값 적용
273
- Object.assign(kpiStatistic, statistics)
274
- kpiStatistic.metadata = {
275
- ...kpiStatistic.metadata,
276
- calculationMethod: 'scope-aggregated',
277
- lastCalculated: new Date(),
278
- dataCount: allValues.length,
279
- kpiScope: {
280
- id: kpiScope.id,
281
- name: kpiScope.name,
282
- level: kpiScope.level,
283
- scopeType: kpiScope.scopeType
284
- },
285
- aggregatedScopeValue: scopeValue,
286
- orgScopeCount: kpiOrgScopes.length
287
- }
288
-
289
- return await kpiStatisticRepo.save(kpiStatistic)
290
- }
291
-
292
- /**
293
- * 특정 스코프 필드의 유니크한 값들 조회
294
- */
295
- private static async getDistinctScopeValues(fieldName: string, domainId: string): Promise<string[]> {
296
- const query = `
297
- SELECT DISTINCT ${fieldName} as value
298
- FROM kpi_org_scope
299
- WHERE domainId = ? AND ${fieldName} IS NOT NULL
300
- ORDER BY ${fieldName}
301
- `
302
-
303
- const results = await getRepository(KpiOrgScope).query(query, [domainId])
304
- return results.map((r: any) => r.value).filter(Boolean)
305
- }
306
-
307
- /**
308
- * 통계값 계산 로직
309
- */
310
- private static calculateStatistics(values: number[]) {
311
- if (values.length === 0) return {}
312
-
313
- const sortedValues = [...values].sort((a, b) => a - b)
314
- const count = values.length
315
- const sum = values.reduce((acc, val) => acc + val, 0)
316
- const mean = sum / count
317
-
318
- // 중앙값
319
- const median =
320
- count % 2 === 0
321
- ? (sortedValues[Math.floor(count / 2) - 1] + sortedValues[Math.floor(count / 2)]) / 2
322
- : sortedValues[Math.floor(count / 2)]
323
-
324
- // 분산 및 표준편차
325
- const variance = values.reduce((acc, val) => acc + Math.pow(val - mean, 2), 0) / count
326
- const standardDeviation = Math.sqrt(variance)
327
-
328
- // 최소/최대값 및 범위
329
- const minimum = Math.min(...values)
330
- const maximum = Math.max(...values)
331
- const range = maximum - minimum
332
-
333
- // 분위수 계산
334
- const q1Index = Math.floor(count * 0.25)
335
- const q3Index = Math.floor(count * 0.75)
336
- const percentile25 = sortedValues[q1Index]
337
- const percentile75 = sortedValues[q3Index]
338
- const iqr = percentile75 - percentile25
339
-
340
- // 이상치 감지를 위한 fence
341
- const lowerFence = percentile25 - 1.5 * iqr
342
- const upperFence = percentile75 + 1.5 * iqr
343
-
344
- return {
345
- count,
346
- sum,
347
- range,
348
- mean,
349
- median,
350
- minimum,
351
- maximum,
352
- standardDeviation,
353
- variance,
354
- percentile25,
355
- percentile75,
356
- iqr,
357
- lowerFence,
358
- upperFence,
359
- additionalStatistics: {
360
- coefficientOfVariation: standardDeviation / mean,
361
- skewness: this.calculateSkewness(values, mean, standardDeviation),
362
- kurtosis: this.calculateKurtosis(values, mean, standardDeviation)
363
- }
364
- }
365
- }
366
-
367
- /**
368
- * 왜도 계산
369
- */
370
- private static calculateSkewness(values: number[], mean: number, stdDev: number): number {
371
- if (stdDev === 0) return 0
372
- const n = values.length
373
- const sum = values.reduce((acc, val) => acc + Math.pow((val - mean) / stdDev, 3), 0)
374
- return (n / ((n - 1) * (n - 2))) * sum
375
- }
376
-
377
- /**
378
- * 첨도 계산
379
- */
380
- private static calculateKurtosis(values: number[], mean: number, stdDev: number): number {
381
- if (stdDev === 0) return 0
382
- const n = values.length
383
- const sum = values.reduce((acc, val) => acc + Math.pow((val - mean) / stdDev, 4), 0)
384
- const kurtosis = ((n * (n + 1)) / ((n - 1) * (n - 2) * (n - 3))) * sum
385
- const adjustment = (3 * Math.pow(n - 1, 2)) / ((n - 2) * (n - 3))
386
- return kurtosis - adjustment
387
- }
388
-
389
- /**
390
- * 기간 타입에 따른 날짜 필터 생성
391
- */
392
- private static buildDateFilter(valueDate: string, periodType: KpiPeriodType): string {
393
- switch (periodType) {
394
- case KpiPeriodType.DAY:
395
- return valueDate // 정확한 날짜 매치
396
- case KpiPeriodType.WEEK:
397
- return `${valueDate.substring(0, 8)}%` // YYYY-MM-DD% -> 주 단위
398
- case KpiPeriodType.MONTH:
399
- return `${valueDate.substring(0, 7)}%` // YYYY-MM%
400
- case KpiPeriodType.QUARTER:
401
- const year = valueDate.substring(0, 4)
402
- const quarter = valueDate.substring(5, 7) // Q1, Q2, Q3, Q4
403
- return `${year}-${quarter}%`
404
- case KpiPeriodType.YEAR:
405
- return `${valueDate.substring(0, 4)}%` // YYYY%
406
- default:
407
- return valueDate
408
- }
409
- }
410
- }
@@ -1,291 +0,0 @@
1
- import { Resolver, Mutation, Arg, Ctx, Directive } from 'type-graphql'
2
- import { In } from 'typeorm'
3
- import { getRepository } from '@things-factory/shell'
4
- import type ResolverContext from '@things-factory/auth-base'
5
-
6
- import { KpiStatistic } from './kpi-statistic.js'
7
- import { NewKpiStatistic, KpiStatisticPatch } from './kpi-statistic-type.js'
8
- import { KpiStatisticCalculationService } from './kpi-statistic-calculation.service.js'
9
- import { KpiPeriodType } from '../kpi/kpi.js'
10
-
11
- @Resolver(KpiStatistic)
12
- export class KpiStatisticMutation {
13
- @Directive('@transaction')
14
- @Directive('@privilege(category: "kpi", privilege: "mutation", domainOwnerGranted: true, superUserGranted: true)')
15
- @Mutation(returns => KpiStatistic, { description: 'To create new KpiStatistic' })
16
- async createKpiStatistic(
17
- @Arg('kpiStatistic') kpiStatistic: NewKpiStatistic,
18
- @Ctx() context: ResolverContext
19
- ): Promise<KpiStatistic> {
20
- const { domain, user, tx } = context.state
21
-
22
- // 메타데이터 자동 설정
23
- const enrichedKpiStatistic = {
24
- ...kpiStatistic,
25
- metadata: {
26
- calculationMethod: kpiStatistic.metadata?.calculationMethod || 'manual',
27
- lastCalculated: new Date(),
28
- dataCount: kpiStatistic.count || 0
29
- }
30
- }
31
-
32
- const result = await getRepository(KpiStatistic, tx).save({
33
- ...enrichedKpiStatistic,
34
- domain,
35
- creator: user,
36
- updater: user
37
- })
38
-
39
- return result
40
- }
41
-
42
- @Directive('@transaction')
43
- @Directive('@privilege(category: "kpi", privilege: "mutation", domainOwnerGranted: true, superUserGranted: true)')
44
- @Mutation(returns => KpiStatistic, { description: 'To modify KpiStatistic information' })
45
- async updateKpiStatistic(
46
- @Arg('id') id: string,
47
- @Arg('patch') patch: KpiStatisticPatch,
48
- @Ctx() context: ResolverContext
49
- ): Promise<KpiStatistic> {
50
- const { domain, user, tx } = context.state
51
-
52
- const repository = getRepository(KpiStatistic, tx)
53
- const kpiStatistic = await repository.findOne({
54
- where: { domain: { id: domain.id }, id }
55
- })
56
-
57
- // 메타데이터 업데이트
58
- const updatedMetadata = {
59
- ...kpiStatistic.metadata,
60
- ...patch.metadata,
61
- lastCalculated: new Date()
62
- }
63
-
64
- const result = await repository.save({
65
- ...kpiStatistic,
66
- ...patch,
67
- metadata: updatedMetadata,
68
- updater: user
69
- })
70
-
71
- return result
72
- }
73
-
74
- @Directive('@transaction')
75
- @Directive('@privilege(category: "kpi", privilege: "mutation", domainOwnerGranted: true, superUserGranted: true)')
76
- @Mutation(returns => [KpiStatistic], { description: "To modify multiple KpiStatistics' information" })
77
- async updateMultipleKpiStatistic(
78
- @Arg('patches', type => [KpiStatisticPatch]) patches: KpiStatisticPatch[],
79
- @Ctx() context: ResolverContext
80
- ): Promise<KpiStatistic[]> {
81
- const { domain, user, tx } = context.state
82
-
83
- let results = []
84
- const _createRecords = patches.filter((patch: any) => patch.cuFlag.toUpperCase() === '+')
85
- const _updateRecords = patches.filter((patch: any) => patch.cuFlag.toUpperCase() === 'M')
86
- const kpiStatisticRepo = getRepository(KpiStatistic, tx)
87
-
88
- if (_createRecords.length > 0) {
89
- for (let i = 0; i < _createRecords.length; i++) {
90
- const newRecord = _createRecords[i]
91
-
92
- // 메타데이터 자동 설정
93
- const enrichedRecord = {
94
- ...newRecord,
95
- metadata: {
96
- calculationMethod: newRecord.metadata?.calculationMethod || 'manual',
97
- lastCalculated: new Date(),
98
- dataCount: newRecord.count || 0
99
- }
100
- }
101
-
102
- const result = await kpiStatisticRepo.save({
103
- ...enrichedRecord,
104
- domain,
105
- creator: user,
106
- updater: user
107
- })
108
-
109
- results.push({ ...result, cuFlag: '+' })
110
- }
111
- }
112
-
113
- if (_updateRecords.length > 0) {
114
- for (let i = 0; i < _updateRecords.length; i++) {
115
- const updateRecord = _updateRecords[i]
116
- const kpiStatistic = await kpiStatisticRepo.findOneBy({ id: updateRecord.id })
117
-
118
- // 메타데이터 업데이트
119
- const updatedMetadata = {
120
- ...kpiStatistic.metadata,
121
- ...updateRecord.metadata,
122
- lastCalculated: new Date()
123
- }
124
-
125
- const result = await kpiStatisticRepo.save({
126
- ...kpiStatistic,
127
- ...updateRecord,
128
- metadata: updatedMetadata,
129
- updater: user
130
- })
131
-
132
- results.push({ ...result, cuFlag: 'M' })
133
- }
134
- }
135
-
136
- return results
137
- }
138
-
139
- @Directive('@transaction')
140
- @Directive('@privilege(category: "kpi", privilege: "mutation", domainOwnerGranted: true, superUserGranted: true)')
141
- @Mutation(returns => Boolean, { description: 'To delete KpiStatistic' })
142
- async deleteKpiStatistic(@Arg('id') id: string, @Ctx() context: ResolverContext): Promise<boolean> {
143
- const { domain, tx } = context.state
144
-
145
- await getRepository(KpiStatistic, tx).delete({ domain: { id: domain.id }, id })
146
-
147
- return true
148
- }
149
-
150
- @Directive('@transaction')
151
- @Directive('@privilege(category: "kpi", privilege: "mutation", domainOwnerGranted: true, superUserGranted: true)')
152
- @Mutation(returns => Boolean, { description: 'To delete multiple KpiStatistics' })
153
- async deleteKpiStatistics(
154
- @Arg('ids', type => [String]) ids: string[],
155
- @Ctx() context: ResolverContext
156
- ): Promise<boolean> {
157
- const { domain, tx } = context.state
158
-
159
- await getRepository(KpiStatistic, tx).delete({
160
- domain: { id: domain.id },
161
- id: In(ids)
162
- })
163
-
164
- return true
165
- }
166
-
167
- @Directive('@transaction')
168
- @Directive('@privilege(category: "kpi", privilege: "mutation", domainOwnerGranted: true, superUserGranted: true)')
169
- @Mutation(returns => Boolean, { description: 'To import KpiStatistics' })
170
- async importKpiStatistics(
171
- @Arg('kpiStatistics', type => [KpiStatisticPatch]) kpiStatistics: KpiStatisticPatch[],
172
- @Ctx() context: ResolverContext
173
- ): Promise<boolean> {
174
- const { domain, user, tx } = context.state
175
-
176
- for (const kpiStatistic of kpiStatistics) {
177
- // 메타데이터 자동 설정
178
- const enrichedKpiStatistic = {
179
- ...kpiStatistic,
180
- metadata: {
181
- calculationMethod: kpiStatistic.metadata?.calculationMethod || 'import',
182
- lastCalculated: new Date(),
183
- dataCount: kpiStatistic.count || 0
184
- }
185
- }
186
-
187
- await getRepository(KpiStatistic, tx).save({
188
- ...enrichedKpiStatistic,
189
- domain,
190
- creator: user,
191
- updater: user
192
- })
193
- }
194
-
195
- return true
196
- }
197
-
198
- @Directive('@transaction')
199
- @Directive('@privilege(category: "kpi", privilege: "mutation", domainOwnerGranted: true, superUserGranted: true)')
200
- @Mutation(returns => [KpiStatistic], {
201
- description: 'Calculate statistics for a specific KPI and period from KpiValue data'
202
- })
203
- async calculateKpiStatistics(
204
- @Arg('kpiId') kpiId: string,
205
- @Arg('periodType', type => KpiPeriodType) periodType: KpiPeriodType,
206
- @Arg('valueDate') valueDate: string,
207
- @Ctx() context: ResolverContext
208
- ): Promise<KpiStatistic[]> {
209
- const overallStats = await KpiStatisticCalculationService.calculateKpiStatistics(
210
- kpiId,
211
- periodType,
212
- valueDate,
213
- null, // 전체 통계
214
- context
215
- )
216
-
217
- const scopedStats = await KpiStatisticCalculationService.calculateScopedStatistics(
218
- kpiId,
219
- periodType,
220
- valueDate,
221
- context
222
- )
223
-
224
- return [overallStats, ...scopedStats].filter(Boolean)
225
- }
226
-
227
- @Directive('@transaction')
228
- @Directive('@privilege(category: "kpi", privilege: "mutation", domainOwnerGranted: true, superUserGranted: true)')
229
- @Mutation(returns => [KpiStatistic], {
230
- description: 'Calculate statistics for all KPIs in a specific period from KpiValue data'
231
- })
232
- async calculateAllKpiStatistics(
233
- @Arg('periodType', type => KpiPeriodType) periodType: KpiPeriodType,
234
- @Arg('valueDate') valueDate: string,
235
- @Ctx() context: ResolverContext
236
- ): Promise<KpiStatistic[]> {
237
- return await KpiStatisticCalculationService.calculateAllStatistics(
238
- periodType,
239
- valueDate,
240
- context
241
- )
242
- }
243
-
244
- @Directive('@transaction')
245
- @Directive('@privilege(category: "kpi", privilege: "mutation", domainOwnerGranted: true, superUserGranted: true)')
246
- @Mutation(returns => [KpiStatistic], {
247
- description: 'Recalculate statistics for dashboard regions (scope02-based statistics)'
248
- })
249
- async calculateRegionalStatistics(
250
- @Arg('periodType', type => KpiPeriodType, { defaultValue: KpiPeriodType.MONTH }) periodType: KpiPeriodType,
251
- @Arg('valueDate') valueDate: string,
252
- @Ctx() context: ResolverContext
253
- ): Promise<KpiStatistic[]> {
254
- const { domain, user, tx } = context.state
255
-
256
- // 지역별(scope02) 통계만 계산
257
- const kpiStatistics = await getRepository(KpiStatistic, tx)
258
- .createQueryBuilder('stat')
259
- .leftJoinAndSelect('stat.kpiOrgScope', 'orgScope')
260
- .leftJoinAndSelect('stat.kpi', 'kpi')
261
- .where('stat.domain = :domainId', { domainId: domain.id })
262
- .andWhere('stat.periodType = :periodType', { periodType })
263
- .andWhere('stat.valueDate = :valueDate', { valueDate })
264
- .andWhere('orgScope.scope02 IS NOT NULL') // 지역 정보가 있는 것만
265
- .getMany()
266
-
267
- // 계산 결과가 없으면 새로 계산
268
- if (kpiStatistics.length === 0) {
269
- return await KpiStatisticCalculationService.calculateAllStatistics(
270
- periodType,
271
- valueDate,
272
- context
273
- )
274
- }
275
-
276
- // 기존 통계 재계산
277
- const results: KpiStatistic[] = []
278
- for (const stat of kpiStatistics) {
279
- const recalculated = await KpiStatisticCalculationService.calculateKpiStatistics(
280
- stat.kpiId,
281
- periodType,
282
- valueDate,
283
- stat.kpiOrgScope,
284
- context
285
- )
286
- if (recalculated) results.push(recalculated)
287
- }
288
-
289
- return results
290
- }
291
- }