@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,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
- }