@things-factory/kpi 9.0.32 → 9.0.34

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 (61) hide show
  1. package/client/pages/kpi-dashboard/kpi-dashboard-map.ts +291 -49
  2. package/dist-client/pages/kpi-dashboard/kpi-dashboard-map.d.ts +3 -1
  3. package/dist-client/pages/kpi-dashboard/kpi-dashboard-map.js +268 -47
  4. package/dist-client/pages/kpi-dashboard/kpi-dashboard-map.js.map +1 -1
  5. package/dist-client/tsconfig.tsbuildinfo +1 -1
  6. package/dist-server/migrations/seed-data/kpi-scopes-seed.json +121 -0
  7. package/dist-server/migrations/seed-data/scope-definitions-seed.json +90 -0
  8. package/dist-server/service/index.d.ts +4 -3
  9. package/dist-server/service/index.js +7 -3
  10. package/dist-server/service/index.js.map +1 -1
  11. package/dist-server/service/kpi/kpi-query.js.map +1 -1
  12. package/dist-server/service/kpi-org-scope/index.d.ts +0 -4
  13. package/dist-server/service/kpi-org-scope/index.js +0 -5
  14. package/dist-server/service/kpi-org-scope/index.js.map +1 -1
  15. package/dist-server/service/kpi-scope/index.d.ts +9 -0
  16. package/dist-server/service/kpi-scope/index.js +14 -0
  17. package/dist-server/service/kpi-scope/index.js.map +1 -0
  18. package/dist-server/service/kpi-scope/kpi-scope-mutation.d.ts +9 -0
  19. package/dist-server/service/kpi-scope/kpi-scope-mutation.js +135 -0
  20. package/dist-server/service/kpi-scope/kpi-scope-mutation.js.map +1 -0
  21. package/dist-server/service/kpi-scope/kpi-scope-query.d.ts +11 -0
  22. package/dist-server/service/kpi-scope/kpi-scope-query.js +89 -0
  23. package/dist-server/service/kpi-scope/kpi-scope-query.js.map +1 -0
  24. package/dist-server/service/kpi-scope/kpi-scope-type.d.ts +35 -0
  25. package/dist-server/service/kpi-scope/kpi-scope-type.js +138 -0
  26. package/dist-server/service/kpi-scope/kpi-scope-type.js.map +1 -0
  27. package/dist-server/service/kpi-scope/kpi-scope.d.ts +38 -0
  28. package/dist-server/service/kpi-scope/kpi-scope.js +144 -0
  29. package/dist-server/service/kpi-scope/kpi-scope.js.map +1 -0
  30. package/dist-server/service/kpi-statistic/kpi-statistic-batch.service.d.ts +43 -0
  31. package/dist-server/service/kpi-statistic/kpi-statistic-batch.service.js +181 -0
  32. package/dist-server/service/kpi-statistic/kpi-statistic-batch.service.js.map +1 -0
  33. package/dist-server/service/kpi-statistic/kpi-statistic-calculation.service.d.ts +50 -0
  34. package/dist-server/service/kpi-statistic/kpi-statistic-calculation.service.js +324 -0
  35. package/dist-server/service/kpi-statistic/kpi-statistic-calculation.service.js.map +1 -0
  36. package/dist-server/service/kpi-statistic/kpi-statistic-mutation.d.ts +4 -0
  37. package/dist-server/service/kpi-statistic/kpi-statistic-mutation.js +76 -0
  38. package/dist-server/service/kpi-statistic/kpi-statistic-mutation.js.map +1 -1
  39. package/dist-server/service/kpi-statistic/kpi-statistic-query.d.ts +5 -1
  40. package/dist-server/service/kpi-statistic/kpi-statistic-query.js +92 -1
  41. package/dist-server/service/kpi-statistic/kpi-statistic-query.js.map +1 -1
  42. package/dist-server/service/kpi-statistic/kpi-statistic.d.ts +3 -0
  43. package/dist-server/service/kpi-statistic/kpi-statistic.js +23 -1
  44. package/dist-server/service/kpi-statistic/kpi-statistic.js.map +1 -1
  45. package/dist-server/tsconfig.tsbuildinfo +1 -1
  46. package/package.json +5 -5
  47. package/server/migrations/seed-data/kpi-scopes-seed.json +121 -0
  48. package/server/migrations/seed-data/scope-definitions-seed.json +90 -0
  49. package/server/service/index.ts +7 -3
  50. package/server/service/kpi/kpi-query.ts +1 -0
  51. package/server/service/kpi-org-scope/index.ts +1 -6
  52. package/server/service/kpi-scope/index.ts +11 -0
  53. package/server/service/kpi-scope/kpi-scope-mutation.ts +129 -0
  54. package/server/service/kpi-scope/kpi-scope-query.ts +63 -0
  55. package/server/service/kpi-scope/kpi-scope-type.ts +96 -0
  56. package/server/service/kpi-scope/kpi-scope.ts +143 -0
  57. package/server/service/kpi-statistic/kpi-statistic-batch.service.ts +231 -0
  58. package/server/service/kpi-statistic/kpi-statistic-calculation.service.ts +410 -0
  59. package/server/service/kpi-statistic/kpi-statistic-mutation.ts +97 -0
  60. package/server/service/kpi-statistic/kpi-statistic-query.ts +89 -2
  61. package/server/service/kpi-statistic/kpi-statistic.ts +23 -1
@@ -0,0 +1,143 @@
1
+ import {
2
+ CreateDateColumn,
3
+ UpdateDateColumn,
4
+ Entity,
5
+ Index,
6
+ Column,
7
+ RelationId,
8
+ ManyToOne,
9
+ PrimaryGeneratedColumn
10
+ } from 'typeorm'
11
+ import { ObjectType, Field, ID, registerEnumType, Int } from 'type-graphql'
12
+
13
+ import { Domain, ScalarObject } from '@things-factory/shell'
14
+ import { User } from '@things-factory/auth-base'
15
+
16
+ export enum ScopeType {
17
+ GEOGRAPHIC = 'GEOGRAPHIC', // 지리적 차원 (지역, 시도, 구군)
18
+ ORGANIZATIONAL = 'ORGANIZATIONAL', // 조직 차원 (회사, 부서, 팀)
19
+ BUSINESS = 'BUSINESS', // 비즈니스 차원 (규모, 유형, 등급)
20
+ TECHNICAL = 'TECHNICAL', // 기술적 차원 (공종, 기술, 방법)
21
+ TEMPORAL = 'TEMPORAL', // 시간적 차원 (분기, 년도, 기간)
22
+ CUSTOM = 'CUSTOM' // 사용자 정의
23
+ }
24
+
25
+ registerEnumType(ScopeType, {
26
+ name: 'ScopeType',
27
+ description: 'Types of organizational scope dimensions'
28
+ })
29
+
30
+ @Entity()
31
+ @Index('ix_kpi_scope_domain', (kpiScope: KpiScope) => [kpiScope.domain])
32
+ @Index('ix_kpi_scope_level', (kpiScope: KpiScope) => [kpiScope.domain, kpiScope.level])
33
+ @ObjectType({
34
+ description: 'Defines the meaning and metadata for each scope level (scope01~05) in KpiOrgScope'
35
+ })
36
+ export class KpiScope {
37
+ @PrimaryGeneratedColumn('uuid')
38
+ @Field(type => ID)
39
+ readonly id: string
40
+
41
+ // 스코프 레벨 (1-5)
42
+ @Column({ type: 'int' })
43
+ @Field(type => Int, { description: 'Scope level (1-5) corresponding to scope01-scope05 in KpiOrgScope' })
44
+ level: number
45
+
46
+ // 스코프 차원 이름
47
+ @Column()
48
+ @Field({ description: 'Display name for this scope dimension (e.g., "지역", "회사", "프로젝트규모")' })
49
+ name: string
50
+
51
+ // 스코프 차원 설명
52
+ @Column({ nullable: true })
53
+ @Field({ nullable: true, description: 'Detailed description of what this scope dimension represents' })
54
+ description?: string
55
+
56
+ // 스코프 타입 (지리적, 조직적, 비즈니스 등)
57
+ @Column()
58
+ @Field(type => ScopeType, { description: 'Type category of this scope dimension' })
59
+ scopeType: ScopeType
60
+
61
+ // 계층 정보
62
+ @Column({ nullable: true })
63
+ @Field(type => Int, { nullable: true, description: 'Parent scope level if this forms a hierarchy' })
64
+ parentLevel?: number
65
+
66
+ @Column({ default: 1 })
67
+ @Field(type => Int, { description: 'Order/priority for display and processing' })
68
+ displayOrder: number
69
+
70
+ // 유효한 값들 (선택사항)
71
+ @Column('simple-json', { nullable: true })
72
+ @Field(type => ScalarObject, {
73
+ nullable: true,
74
+ description: 'List of valid values for this scope (e.g., ["서울", "부산", "대구"] for regions)'
75
+ })
76
+ validValues?: string[]
77
+
78
+ // 정규표현식 검증 (선택사항)
79
+ @Column({ nullable: true })
80
+ @Field({
81
+ nullable: true,
82
+ description: 'Regex pattern for validating values in this scope dimension'
83
+ })
84
+ validationPattern?: string
85
+
86
+ // 활성화 여부
87
+ @Column({ default: true })
88
+ @Field({ description: 'Whether this scope definition is active and should be used' })
89
+ active: boolean
90
+
91
+ // 통계 계산에 사용 여부
92
+ @Column({ default: true })
93
+ @Field({ description: 'Whether this scope should be included in statistical calculations' })
94
+ includeInStatistics: boolean
95
+
96
+ // 대시보드에 표시 여부
97
+ @Column({ default: false })
98
+ @Field({ description: 'Whether this scope should be displayed in dashboard visualizations' })
99
+ showInDashboard: boolean
100
+
101
+ // 메타데이터
102
+ @Column('simple-json', { nullable: true })
103
+ @Field(type => ScalarObject, {
104
+ nullable: true,
105
+ description: 'Additional metadata for this scope dimension (colors, icons, etc.)'
106
+ })
107
+ metadata?: {
108
+ color?: string
109
+ icon?: string
110
+ unit?: string
111
+ [key: string]: any
112
+ }
113
+
114
+ // === 표준 필드들 ===
115
+ @ManyToOne(type => Domain)
116
+ @Field({ nullable: true, description: 'Domain this scope definition belongs to' })
117
+ domain?: Domain
118
+
119
+ @RelationId((kpiScope: KpiScope) => kpiScope.domain)
120
+ domainId?: string
121
+
122
+ @CreateDateColumn()
123
+ @Field({ nullable: true })
124
+ createdAt?: Date
125
+
126
+ @UpdateDateColumn()
127
+ @Field({ nullable: true })
128
+ updatedAt?: Date
129
+
130
+ @ManyToOne(type => User, { nullable: true })
131
+ @Field(type => User, { nullable: true })
132
+ creator?: User
133
+
134
+ @RelationId((kpiScope: KpiScope) => kpiScope.creator)
135
+ creatorId?: string
136
+
137
+ @ManyToOne(type => User, { nullable: true })
138
+ @Field(type => User, { nullable: true })
139
+ updater?: User
140
+
141
+ @RelationId((kpiScope: KpiScope) => kpiScope.updater)
142
+ updaterId?: string
143
+ }
@@ -0,0 +1,231 @@
1
+ import { getRepository } from '@things-factory/shell'
2
+ import type ResolverContext from '@things-factory/auth-base'
3
+ import { KpiStatisticCalculationService } from './kpi-statistic-calculation.service'
4
+ import { KpiPeriodType } from '../kpi/kpi'
5
+ import { Domain } from '@things-factory/shell'
6
+
7
+ /**
8
+ * KPI 통계 배치 계산 서비스
9
+ * 정기적으로 실행하여 KpiValue 데이터를 집계해서 KpiStatistic을 생성/업데이트
10
+ */
11
+ export class KpiStatisticBatchService {
12
+
13
+ /**
14
+ * 일일 배치: 어제 날짜의 모든 통계 계산
15
+ */
16
+ static async runDailyBatch(domainId?: string): Promise<void> {
17
+ console.log('[KPI Statistics] Starting daily batch calculation...')
18
+
19
+ const yesterday = new Date()
20
+ yesterday.setDate(yesterday.getDate() - 1)
21
+ const valueDate = yesterday.toISOString().split('T')[0] // YYYY-MM-DD
22
+
23
+ await this.calculateForAllDomains(KpiPeriodType.DAY, valueDate, domainId)
24
+
25
+ console.log(`[KPI Statistics] Daily batch completed for ${valueDate}`)
26
+ }
27
+
28
+ /**
29
+ * 월간 배치: 지난 달의 모든 통계 계산
30
+ */
31
+ static async runMonthlyBatch(domainId?: string): Promise<void> {
32
+ console.log('[KPI Statistics] Starting monthly batch calculation...')
33
+
34
+ const lastMonth = new Date()
35
+ lastMonth.setMonth(lastMonth.getMonth() - 1)
36
+ const valueDate = lastMonth.toISOString().substring(0, 7) // YYYY-MM
37
+
38
+ await this.calculateForAllDomains(KpiPeriodType.MONTH, valueDate, domainId)
39
+
40
+ console.log(`[KPI Statistics] Monthly batch completed for ${valueDate}`)
41
+ }
42
+
43
+ /**
44
+ * 분기 배치: 지난 분기의 모든 통계 계산
45
+ */
46
+ static async runQuarterlyBatch(domainId?: string): Promise<void> {
47
+ console.log('[KPI Statistics] Starting quarterly batch calculation...')
48
+
49
+ const now = new Date()
50
+ const currentQuarter = Math.floor(now.getMonth() / 3) + 1
51
+ const lastQuarter = currentQuarter === 1 ? 4 : currentQuarter - 1
52
+ const year = currentQuarter === 1 ? now.getFullYear() - 1 : now.getFullYear()
53
+ const valueDate = `${year}-Q${lastQuarter}`
54
+
55
+ await this.calculateForAllDomains(KpiPeriodType.QUARTER, valueDate, domainId)
56
+
57
+ console.log(`[KPI Statistics] Quarterly batch completed for ${valueDate}`)
58
+ }
59
+
60
+ /**
61
+ * 연간 배치: 작년의 모든 통계 계산
62
+ */
63
+ static async runYearlyBatch(domainId?: string): Promise<void> {
64
+ console.log('[KPI Statistics] Starting yearly batch calculation...')
65
+
66
+ const lastYear = new Date().getFullYear() - 1
67
+ const valueDate = lastYear.toString()
68
+
69
+ await this.calculateForAllDomains(KpiPeriodType.YEAR, valueDate, domainId)
70
+
71
+ console.log(`[KPI Statistics] Yearly batch completed for ${valueDate}`)
72
+ }
73
+
74
+ /**
75
+ * 대시보드용 지역별 통계 계산
76
+ */
77
+ static async runDashboardBatch(
78
+ periodType: KpiPeriodType = KpiPeriodType.MONTH,
79
+ valueDate?: string,
80
+ domainId?: string
81
+ ): Promise<void> {
82
+ console.log('[KPI Statistics] Starting dashboard batch calculation...')
83
+
84
+ if (!valueDate) {
85
+ const now = new Date()
86
+ if (periodType === KpiPeriodType.MONTH) {
87
+ now.setMonth(now.getMonth() - 1)
88
+ valueDate = now.toISOString().substring(0, 7) // YYYY-MM
89
+ } else {
90
+ now.setDate(now.getDate() - 1)
91
+ valueDate = now.toISOString().split('T')[0] // YYYY-MM-DD
92
+ }
93
+ }
94
+
95
+ await this.calculateForAllDomains(periodType, valueDate, domainId)
96
+
97
+ console.log(`[KPI Statistics] Dashboard batch completed for ${valueDate} (${periodType})`)
98
+ }
99
+
100
+ /**
101
+ * 모든 도메인에 대해 통계 계산
102
+ */
103
+ private static async calculateForAllDomains(
104
+ periodType: KpiPeriodType,
105
+ valueDate: string,
106
+ domainId?: string
107
+ ): Promise<void> {
108
+ try {
109
+ const domains = domainId
110
+ ? await getRepository(Domain).find({ where: { id: domainId } })
111
+ : await getRepository(Domain).find()
112
+
113
+ for (const domain of domains) {
114
+ try {
115
+ await this.calculateForDomain(domain, periodType, valueDate)
116
+ } catch (error) {
117
+ console.error(`[KPI Statistics] Domain ${domain.name} calculation failed, continuing with next domain:`, error)
118
+ // 개별 도메인 실패가 전체 배치를 중단시키지 않도록 계속 진행
119
+ continue
120
+ }
121
+ }
122
+ } catch (error) {
123
+ console.error('[KPI Statistics] Batch calculation failed:', error)
124
+ throw error
125
+ }
126
+ }
127
+
128
+ /**
129
+ * 특정 도메인에 대해 통계 계산
130
+ */
131
+ private static async calculateForDomain(
132
+ domain: Domain,
133
+ periodType: KpiPeriodType,
134
+ valueDate: string
135
+ ): Promise<void> {
136
+ console.log(`[KPI Statistics] Calculating for domain: ${domain.name} (${domain.id})`)
137
+
138
+ // 가짜 ResolverContext 생성 (배치 작업용)
139
+ const context: ResolverContext = {
140
+ state: {
141
+ domain,
142
+ user: { id: 'system', name: 'System Batch' }, // 시스템 사용자
143
+ tx: null // 트랜잭션 없음 (각 계산마다 개별 트랜잭션)
144
+ }
145
+ } as any
146
+
147
+ try {
148
+ const statistics = await KpiStatisticCalculationService.calculateAllStatistics(
149
+ periodType,
150
+ valueDate,
151
+ context
152
+ )
153
+
154
+ console.log(`[KPI Statistics] Generated ${statistics.length} statistics for domain ${domain.name}`)
155
+
156
+ // 성공 로깅
157
+ this.logBatchResult(domain.id, periodType, valueDate, statistics.length, 'SUCCESS')
158
+
159
+ } catch (error) {
160
+ console.error(`[KPI Statistics] Failed to calculate for domain ${domain.name}:`, error)
161
+
162
+ // 실패 로깅
163
+ this.logBatchResult(domain.id, periodType, valueDate, 0, 'FAILED', error.message)
164
+
165
+ // 에러를 던져서 상위에서 처리
166
+ throw error
167
+ }
168
+ }
169
+
170
+ /**
171
+ * 배치 실행 결과 로깅
172
+ */
173
+ private static logBatchResult(
174
+ domainId: string,
175
+ periodType: KpiPeriodType,
176
+ valueDate: string,
177
+ statisticsCount: number,
178
+ status: 'SUCCESS' | 'FAILED',
179
+ errorMessage?: string
180
+ ): void {
181
+ const logData = {
182
+ timestamp: new Date().toISOString(),
183
+ domainId,
184
+ periodType,
185
+ valueDate,
186
+ statisticsCount,
187
+ status,
188
+ errorMessage
189
+ }
190
+
191
+ // 실제 운영에서는 로깅 시스템에 저장
192
+ console.log('[KPI Statistics Batch Log]', JSON.stringify(logData))
193
+ }
194
+
195
+ /**
196
+ * 배치 작업 스케줄링 설정 예시
197
+ */
198
+ static setupBatchSchedule(): void {
199
+ // 실제 구현시에는 cron 등의 스케줄러 사용
200
+ console.log('[KPI Statistics] Batch schedule setup example:')
201
+ console.log('Daily batch: Run at 02:00 AM every day')
202
+ console.log('Monthly batch: Run at 03:00 AM on 1st of every month')
203
+ console.log('Quarterly batch: Run at 04:00 AM on 1st of every quarter')
204
+ console.log('Yearly batch: Run at 05:00 AM on January 1st')
205
+
206
+ /*
207
+ // Node-cron 사용 예시:
208
+ import cron from 'node-cron'
209
+
210
+ // 매일 새벽 2시
211
+ cron.schedule('0 2 * * *', () => {
212
+ this.runDailyBatch()
213
+ })
214
+
215
+ // 매월 1일 새벽 3시
216
+ cron.schedule('0 3 1 * *', () => {
217
+ this.runMonthlyBatch()
218
+ })
219
+
220
+ // 매 분기 첫날 새벽 4시 (1, 4, 7, 10월)
221
+ cron.schedule('0 4 1 1,4,7,10 *', () => {
222
+ this.runQuarterlyBatch()
223
+ })
224
+
225
+ // 매년 1월 1일 새벽 5시
226
+ cron.schedule('0 5 1 1 *', () => {
227
+ this.runYearlyBatch()
228
+ })
229
+ */
230
+ }
231
+ }