@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.
- package/client/pages/kpi/kpi-list-page.ts +339 -525
- package/client/pages/kpi/kpi-tree-page.ts +135 -207
- package/client/pages/kpi-metric/kpi-metric-list-page.ts +146 -226
- package/client/pages/kpi-metric-value/kpi-metric-value-editor-page.ts +187 -295
- package/client/pages/kpi-metric-value/kpi-metric-value-list-page.ts +123 -194
- package/client/pages/kpi-metric-value/kpi-metric-value-manual-entry-page.ts +57 -91
- package/client/pages/kpi-statistic/kpi-statistic-editor-page.ts +180 -278
- package/client/pages/kpi-statistic/kpi-statistic-list-page.ts +186 -286
- package/client/pages/kpi-value/kpi-value-editor-page.ts +189 -292
- package/client/pages/kpi-value/kpi-value-list-page.ts +170 -264
- package/dist-client/pages/kpi/kpi-list-page.d.ts +0 -6
- package/dist-client/pages/kpi/kpi-list-page.js +150 -282
- package/dist-client/pages/kpi/kpi-list-page.js.map +1 -1
- package/dist-client/pages/kpi/kpi-tree-page.d.ts +1 -7
- package/dist-client/pages/kpi/kpi-tree-page.js +76 -127
- package/dist-client/pages/kpi/kpi-tree-page.js.map +1 -1
- package/dist-client/pages/kpi-metric/kpi-metric-list-page.d.ts +0 -6
- package/dist-client/pages/kpi-metric/kpi-metric-list-page.js +62 -116
- package/dist-client/pages/kpi-metric/kpi-metric-list-page.js.map +1 -1
- package/dist-client/pages/kpi-metric-value/kpi-metric-value-editor-page.d.ts +1 -7
- package/dist-client/pages/kpi-metric-value/kpi-metric-value-editor-page.js +82 -140
- package/dist-client/pages/kpi-metric-value/kpi-metric-value-editor-page.js.map +1 -1
- package/dist-client/pages/kpi-metric-value/kpi-metric-value-list-page.d.ts +0 -6
- package/dist-client/pages/kpi-metric-value/kpi-metric-value-list-page.js +54 -98
- package/dist-client/pages/kpi-metric-value/kpi-metric-value-list-page.js.map +1 -1
- package/dist-client/pages/kpi-metric-value/kpi-metric-value-manual-entry-page.d.ts +1 -7
- package/dist-client/pages/kpi-metric-value/kpi-metric-value-manual-entry-page.js +30 -57
- package/dist-client/pages/kpi-metric-value/kpi-metric-value-manual-entry-page.js.map +1 -1
- package/dist-client/pages/kpi-statistic/kpi-statistic-editor-page.d.ts +1 -7
- package/dist-client/pages/kpi-statistic/kpi-statistic-editor-page.js +91 -153
- package/dist-client/pages/kpi-statistic/kpi-statistic-editor-page.js.map +1 -1
- package/dist-client/pages/kpi-statistic/kpi-statistic-list-page.d.ts +0 -6
- package/dist-client/pages/kpi-statistic/kpi-statistic-list-page.js +81 -155
- package/dist-client/pages/kpi-statistic/kpi-statistic-list-page.js.map +1 -1
- package/dist-client/pages/kpi-value/kpi-value-editor-page.d.ts +1 -7
- package/dist-client/pages/kpi-value/kpi-value-editor-page.js +80 -136
- package/dist-client/pages/kpi-value/kpi-value-editor-page.js.map +1 -1
- package/dist-client/pages/kpi-value/kpi-value-list-page.d.ts +0 -6
- package/dist-client/pages/kpi-value/kpi-value-list-page.js +73 -134
- package/dist-client/pages/kpi-value/kpi-value-list-page.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-server/service/index.d.ts +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +18 -18
- package/client/tsconfig.json +0 -11
- package/dist-server/tsconfig.json +0 -10
- package/server/@types/index.d.ts +0 -11
- package/server/calculator/evaluator.ts +0 -45
- package/server/calculator/functions.ts +0 -67
- package/server/calculator/index.ts +0 -4
- package/server/calculator/parser.ts +0 -137
- package/server/calculator/provider.ts +0 -10
- package/server/controllers/index.ts +0 -2
- package/server/controllers/kpi-metric-value-provider.ts +0 -79
- package/server/controllers/kpi-value-provider.ts +0 -51
- package/server/index.ts +0 -6
- package/server/migrations/1752190849680-seed-kpi-metrics.ts +0 -124
- package/server/migrations/1752190849681-seed-kpi.ts +0 -356
- package/server/migrations/1752192090123-add-grades-to-kpi.ts +0 -67
- package/server/migrations/1752192090124-add-kpi-statistics.ts +0 -719
- package/server/migrations/1752192090128-seed-kpi-org-scope.ts +0 -132
- package/server/migrations/1752192090129-seed-kpi-values.ts +0 -207
- package/server/migrations/grade-data/x11-performance-table.json +0 -962
- package/server/migrations/grade-data/x12-performance-table.json +0 -611
- package/server/migrations/grade-data/x14-performance-table.json +0 -42
- package/server/migrations/grade-data/x21-performance-table.json +0 -889
- package/server/migrations/grade-data/x22-performance-table.json +0 -1064
- package/server/migrations/grade-data/x23-performance-table.json +0 -42
- package/server/migrations/grade-data/x31-performance-table.json +0 -644
- package/server/migrations/grade-data/x32-performance-table.json +0 -993
- package/server/migrations/grade-data/x33-performance-table.json +0 -195
- package/server/migrations/grade-data/x34-performance-table.json +0 -12
- package/server/migrations/grade-data/x35-performance-table.json +0 -42
- package/server/migrations/grade-data/x41-performance-table.json +0 -825
- package/server/migrations/grade-data/x42-performance-table.json +0 -786
- package/server/migrations/grade-data/x43-performance-table.json +0 -12
- package/server/migrations/grade-data/x44-performance-table.json +0 -42
- package/server/migrations/grade-data/x51-performance-table.json +0 -924
- package/server/migrations/grade-data/x52-performance-table.json +0 -42
- package/server/migrations/grade-data/x61-performance-table.json +0 -261
- package/server/migrations/grade-data/x62-performance-table.json +0 -42
- package/server/migrations/index.ts +0 -9
- package/server/migrations/seed-data/kpi-metrics-seed.json +0 -454
- package/server/migrations/seed-data/kpi-org-scope-seed.json +0 -1676
- package/server/migrations/seed-data/kpi-scopes-seed.json +0 -121
- package/server/migrations/seed-data/kpi-values-seed.json +0 -402
- package/server/migrations/seed-data/kpis-seed.json +0 -488
- package/server/migrations/seed-data/scope-definitions-seed.json +0 -90
- package/server/routes.ts +0 -81
- package/server/service/index.ts +0 -51
- package/server/service/kpi/aggregate-kpi.ts +0 -103
- package/server/service/kpi/event-subscriber.ts +0 -29
- package/server/service/kpi/index.ts +0 -9
- package/server/service/kpi/kpi-formula.service.ts +0 -164
- package/server/service/kpi/kpi-grade.types.ts +0 -28
- package/server/service/kpi/kpi-history.ts +0 -126
- package/server/service/kpi/kpi-mutation.ts +0 -553
- package/server/service/kpi/kpi-query.ts +0 -224
- package/server/service/kpi/kpi-type.ts +0 -151
- package/server/service/kpi/kpi.ts +0 -254
- package/server/service/kpi-alert/index.ts +0 -3
- package/server/service/kpi-alert/kpi-alert-query.ts +0 -59
- package/server/service/kpi-alert/kpi-alert-type.ts +0 -20
- package/server/service/kpi-metric/aggregate-kpi-metric.ts +0 -132
- package/server/service/kpi-metric/index.ts +0 -7
- package/server/service/kpi-metric/kpi-metric-mutation.ts +0 -309
- package/server/service/kpi-metric/kpi-metric-query.ts +0 -70
- package/server/service/kpi-metric/kpi-metric-type.ts +0 -111
- package/server/service/kpi-metric/kpi-metric.ts +0 -134
- package/server/service/kpi-metric-value/index.ts +0 -7
- package/server/service/kpi-metric-value/kpi-metric-value-mutation.ts +0 -270
- package/server/service/kpi-metric-value/kpi-metric-value-query.ts +0 -62
- package/server/service/kpi-metric-value/kpi-metric-value-type.ts +0 -82
- package/server/service/kpi-metric-value/kpi-metric-value.ts +0 -93
- package/server/service/kpi-org-scope/index.ts +0 -6
- package/server/service/kpi-org-scope/kpi-org-scope-mutation.ts +0 -173
- package/server/service/kpi-org-scope/kpi-org-scope-query.ts +0 -127
- package/server/service/kpi-org-scope/kpi-org-scope-type.ts +0 -68
- package/server/service/kpi-org-scope/kpi-org-scope.ts +0 -123
- package/server/service/kpi-scope/index.ts +0 -11
- package/server/service/kpi-scope/kpi-scope-mutation.ts +0 -129
- package/server/service/kpi-scope/kpi-scope-query.ts +0 -63
- package/server/service/kpi-scope/kpi-scope-type.ts +0 -96
- package/server/service/kpi-scope/kpi-scope.ts +0 -143
- package/server/service/kpi-statistic/index.ts +0 -7
- package/server/service/kpi-statistic/kpi-statistic-batch.service.ts +0 -231
- package/server/service/kpi-statistic/kpi-statistic-calculation.service.ts +0 -410
- package/server/service/kpi-statistic/kpi-statistic-mutation.ts +0 -291
- package/server/service/kpi-statistic/kpi-statistic-query.ts +0 -146
- package/server/service/kpi-statistic/kpi-statistic-type.ts +0 -152
- package/server/service/kpi-statistic/kpi-statistic.ts +0 -199
- package/server/service/kpi-value/index.ts +0 -7
- package/server/service/kpi-value/kpi-value-mutation.ts +0 -432
- package/server/service/kpi-value/kpi-value-query.ts +0 -61
- package/server/service/kpi-value/kpi-value-score.service.ts +0 -106
- package/server/service/kpi-value/kpi-value-type.ts +0 -122
- package/server/service/kpi-value/kpi-value.ts +0 -160
- package/server/service/utils/value-date-util.ts +0 -119
- package/server/tsconfig.json +0 -10
- 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
|
-
}
|