@things-factory/kpi 9.0.21 → 9.0.23

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 (152) hide show
  1. package/client/charts/kpi-boxplot-chart.ts +163 -0
  2. package/client/charts/kpi-radar-chart.ts +128 -0
  3. package/client/pages/kpi/kpi-list-page.ts +180 -22
  4. package/client/pages/kpi-category/kpi-category-list-page.ts +76 -3
  5. package/client/pages/kpi-category/kpi-category-value-calculator.ts +233 -0
  6. package/client/pages/kpi-dashboard/kpi-dashboard.ts +188 -0
  7. package/client/pages/kpi-metric/kpi-metric-list-page.ts +13 -1
  8. package/client/pages/kpi-metric-value/kpi-metric-value-list-page.ts +43 -1
  9. package/client/pages/kpi-metric-value/kpi-metric-value-manual-entry-form.ts +3 -13
  10. package/client/pages/kpi-metric-value/kpi-metric-value-manual-entry-page.ts +13 -1
  11. package/client/pages/kpi-value/kpi-value-list-page.ts +45 -1
  12. package/dist-client/charts/kpi-boxplot-chart.d.ts +22 -0
  13. package/dist-client/charts/kpi-boxplot-chart.js +198 -0
  14. package/dist-client/charts/kpi-boxplot-chart.js.map +1 -0
  15. package/dist-client/charts/kpi-radar-chart.d.ts +16 -0
  16. package/dist-client/charts/kpi-radar-chart.js +138 -0
  17. package/dist-client/charts/kpi-radar-chart.js.map +1 -0
  18. package/dist-client/pages/kpi/kpi-list-page.d.ts +2 -1
  19. package/dist-client/pages/kpi/kpi-list-page.js +180 -22
  20. package/dist-client/pages/kpi/kpi-list-page.js.map +1 -1
  21. package/dist-client/pages/kpi-category/kpi-category-list-page.d.ts +3 -0
  22. package/dist-client/pages/kpi-category/kpi-category-list-page.js +71 -3
  23. package/dist-client/pages/kpi-category/kpi-category-list-page.js.map +1 -1
  24. package/dist-client/pages/kpi-category/kpi-category-value-calculator.d.ts +13 -0
  25. package/dist-client/pages/kpi-category/kpi-category-value-calculator.js +256 -0
  26. package/dist-client/pages/kpi-category/kpi-category-value-calculator.js.map +1 -0
  27. package/dist-client/pages/kpi-dashboard/kpi-dashboard.d.ts +11 -0
  28. package/dist-client/pages/kpi-dashboard/kpi-dashboard.js +185 -0
  29. package/dist-client/pages/kpi-dashboard/kpi-dashboard.js.map +1 -1
  30. package/dist-client/pages/kpi-metric/kpi-metric-list-page.js +13 -1
  31. package/dist-client/pages/kpi-metric/kpi-metric-list-page.js.map +1 -1
  32. package/dist-client/pages/kpi-metric-value/kpi-metric-value-list-page.d.ts +4 -1
  33. package/dist-client/pages/kpi-metric-value/kpi-metric-value-list-page.js +39 -2
  34. package/dist-client/pages/kpi-metric-value/kpi-metric-value-list-page.js.map +1 -1
  35. package/dist-client/pages/kpi-metric-value/kpi-metric-value-manual-entry-form.js +3 -13
  36. package/dist-client/pages/kpi-metric-value/kpi-metric-value-manual-entry-form.js.map +1 -1
  37. package/dist-client/pages/kpi-metric-value/kpi-metric-value-manual-entry-page.js +13 -1
  38. package/dist-client/pages/kpi-metric-value/kpi-metric-value-manual-entry-page.js.map +1 -1
  39. package/dist-client/pages/kpi-value/kpi-value-list-page.d.ts +1 -0
  40. package/dist-client/pages/kpi-value/kpi-value-list-page.js +45 -1
  41. package/dist-client/pages/kpi-value/kpi-value-list-page.js.map +1 -1
  42. package/dist-client/tsconfig.tsbuildinfo +1 -1
  43. package/dist-server/calculator/evaluator.d.ts +8 -0
  44. package/dist-server/calculator/evaluator.js +42 -0
  45. package/dist-server/calculator/evaluator.js.map +1 -0
  46. package/dist-server/calculator/functions.d.ts +3 -0
  47. package/dist-server/calculator/functions.js +62 -0
  48. package/dist-server/calculator/functions.js.map +1 -0
  49. package/dist-server/calculator/index.d.ts +4 -0
  50. package/dist-server/calculator/index.js +8 -0
  51. package/dist-server/calculator/index.js.map +1 -0
  52. package/dist-server/calculator/parser.d.ts +21 -0
  53. package/dist-server/calculator/parser.js +121 -0
  54. package/dist-server/calculator/parser.js.map +1 -0
  55. package/dist-server/calculator/provider.d.ts +8 -0
  56. package/dist-server/calculator/provider.js +13 -0
  57. package/dist-server/calculator/provider.js.map +1 -0
  58. package/dist-server/controllers/kpi-metric-value-provider.d.ts +11 -0
  59. package/dist-server/controllers/kpi-metric-value-provider.js +63 -0
  60. package/dist-server/controllers/kpi-metric-value-provider.js.map +1 -0
  61. package/dist-server/controllers/kpi-value-provider.d.ts +11 -0
  62. package/dist-server/controllers/kpi-value-provider.js +46 -0
  63. package/dist-server/controllers/kpi-value-provider.js.map +1 -0
  64. package/dist-server/service/index.d.ts +2 -2
  65. package/dist-server/service/kpi/aggregate-kpi.js +4 -4
  66. package/dist-server/service/kpi/aggregate-kpi.js.map +1 -1
  67. package/dist-server/service/kpi/kpi-grade.types.d.ts +11 -10
  68. package/dist-server/service/kpi/kpi-grade.types.js.map +1 -1
  69. package/dist-server/service/kpi/kpi-history.d.ts +2 -2
  70. package/dist-server/service/kpi/kpi-history.js.map +1 -1
  71. package/dist-server/service/kpi/kpi-mutation.d.ts +2 -0
  72. package/dist-server/service/kpi/kpi-mutation.js +126 -4
  73. package/dist-server/service/kpi/kpi-mutation.js.map +1 -1
  74. package/dist-server/service/kpi/kpi-type.d.ts +8 -5
  75. package/dist-server/service/kpi/kpi-type.js +22 -8
  76. package/dist-server/service/kpi/kpi-type.js.map +1 -1
  77. package/dist-server/service/kpi/kpi.d.ts +6 -3
  78. package/dist-server/service/kpi/kpi.js +29 -9
  79. package/dist-server/service/kpi/kpi.js.map +1 -1
  80. package/dist-server/service/kpi-category/kpi-category-mutation.d.ts +1 -1
  81. package/dist-server/service/kpi-category/kpi-category-mutation.js +3 -3
  82. package/dist-server/service/kpi-category/kpi-category-mutation.js.map +1 -1
  83. package/dist-server/service/kpi-category/kpi-category-query.d.ts +13 -0
  84. package/dist-server/service/kpi-category/kpi-category-query.js +180 -0
  85. package/dist-server/service/kpi-category/kpi-category-query.js.map +1 -1
  86. package/dist-server/service/kpi-category/kpi-category-type.d.ts +3 -0
  87. package/dist-server/service/kpi-category/kpi-category-type.js +16 -1
  88. package/dist-server/service/kpi-category/kpi-category-type.js.map +1 -1
  89. package/dist-server/service/kpi-category/kpi-category.d.ts +2 -0
  90. package/dist-server/service/kpi-category/kpi-category.js +10 -1
  91. package/dist-server/service/kpi-category/kpi-category.js.map +1 -1
  92. package/dist-server/service/kpi-metric/kpi-metric-type.d.ts +5 -3
  93. package/dist-server/service/kpi-metric/kpi-metric-type.js +5 -3
  94. package/dist-server/service/kpi-metric/kpi-metric-type.js.map +1 -1
  95. package/dist-server/service/kpi-metric/kpi-metric.d.ts +2 -8
  96. package/dist-server/service/kpi-metric/kpi-metric.js +3 -14
  97. package/dist-server/service/kpi-metric/kpi-metric.js.map +1 -1
  98. package/dist-server/service/kpi-metric-value/kpi-metric-value-mutation.js +67 -45
  99. package/dist-server/service/kpi-metric-value/kpi-metric-value-mutation.js.map +1 -1
  100. package/dist-server/service/kpi-metric-value/kpi-metric-value.js +3 -2
  101. package/dist-server/service/kpi-metric-value/kpi-metric-value.js.map +1 -1
  102. package/dist-server/service/kpi-value/kpi-value-mutation.d.ts +2 -1
  103. package/dist-server/service/kpi-value/kpi-value-mutation.js +114 -6
  104. package/dist-server/service/kpi-value/kpi-value-mutation.js.map +1 -1
  105. package/dist-server/service/kpi-value/kpi-value-query.d.ts +0 -2
  106. package/dist-server/service/kpi-value/kpi-value-query.js +0 -12
  107. package/dist-server/service/kpi-value/kpi-value-query.js.map +1 -1
  108. package/dist-server/service/kpi-value/kpi-value-score.service.d.ts +26 -0
  109. package/dist-server/service/kpi-value/kpi-value-score.service.js +97 -0
  110. package/dist-server/service/kpi-value/kpi-value-score.service.js.map +1 -0
  111. package/dist-server/service/kpi-value/kpi-value-type.d.ts +2 -0
  112. package/dist-server/service/kpi-value/kpi-value-type.js +14 -0
  113. package/dist-server/service/kpi-value/kpi-value-type.js.map +1 -1
  114. package/dist-server/service/kpi-value/kpi-value.d.ts +1 -0
  115. package/dist-server/service/kpi-value/kpi-value.js +9 -1
  116. package/dist-server/service/kpi-value/kpi-value.js.map +1 -1
  117. package/dist-server/service/utils/value-date-util.d.ts +3 -0
  118. package/dist-server/service/utils/value-date-util.js +76 -0
  119. package/dist-server/service/utils/value-date-util.js.map +1 -0
  120. package/dist-server/tsconfig.tsbuildinfo +1 -1
  121. package/package.json +2 -2
  122. package/server/calculator/evaluator.ts +45 -0
  123. package/server/calculator/functions.ts +67 -0
  124. package/server/calculator/index.ts +4 -0
  125. package/server/calculator/parser.ts +128 -0
  126. package/server/calculator/provider.ts +10 -0
  127. package/server/controllers/kpi-metric-value-provider.ts +66 -0
  128. package/server/controllers/kpi-value-provider.ts +51 -0
  129. package/server/service/kpi/aggregate-kpi.ts +4 -4
  130. package/server/service/kpi/kpi-grade.types.ts +11 -10
  131. package/server/service/kpi/kpi-history.ts +2 -2
  132. package/server/service/kpi/kpi-mutation.ts +128 -4
  133. package/server/service/kpi/kpi-type.ts +21 -9
  134. package/server/service/kpi/kpi.ts +32 -10
  135. package/server/service/kpi-category/kpi-category-mutation.ts +3 -3
  136. package/server/service/kpi-category/kpi-category-query.ts +175 -1
  137. package/server/service/kpi-category/kpi-category-type.ts +17 -6
  138. package/server/service/kpi-category/kpi-category.ts +10 -1
  139. package/server/service/kpi-metric/kpi-metric-type.ts +7 -5
  140. package/server/service/kpi-metric/kpi-metric.ts +3 -15
  141. package/server/service/kpi-metric-value/kpi-metric-value-mutation.ts +67 -47
  142. package/server/service/kpi-metric-value/kpi-metric-value.ts +4 -2
  143. package/server/service/kpi-value/kpi-value-mutation.ts +110 -6
  144. package/server/service/kpi-value/kpi-value-query.ts +2 -8
  145. package/server/service/kpi-value/kpi-value-score.service.ts +112 -0
  146. package/server/service/kpi-value/kpi-value-type.ts +12 -0
  147. package/server/service/kpi-value/kpi-value.ts +8 -1
  148. package/server/service/utils/value-date-util.ts +72 -0
  149. package/dist-server/service/kpi-value/kpi-value-grade.service.d.ts +0 -34
  150. package/dist-server/service/kpi-value/kpi-value-grade.service.js +0 -117
  151. package/dist-server/service/kpi-value/kpi-value-grade.service.js.map +0 -1
  152. package/server/service/kpi-value/kpi-value-grade.service.ts +0 -127
@@ -0,0 +1,233 @@
1
+ import '@material/web/icon/icon.js'
2
+
3
+ import { css, html, LitElement } from 'lit'
4
+ import { customElement, property, state } from 'lit/decorators.js'
5
+ import { client } from '@operato/graphql'
6
+ import { notify } from '@operato/layout'
7
+ import gql from 'graphql-tag'
8
+
9
+ // KpiPeriodType enum 정의 (서버와 동일하게)
10
+ enum KpiPeriodType {
11
+ DAY = 'DAY',
12
+ WEEK = 'WEEK',
13
+ MONTH = 'MONTH',
14
+ QUARTER = 'QUARTER',
15
+ YEAR = 'YEAR',
16
+ RANGE = 'RANGE',
17
+ ALLTIME = 'ALLTIME'
18
+ }
19
+
20
+ // periodType에 따른 마지막 일자 계산 함수
21
+ function getLastValueDate(periodType: KpiPeriodType = KpiPeriodType.DAY): string {
22
+ const now = new Date()
23
+
24
+ switch (periodType) {
25
+ case KpiPeriodType.DAY: {
26
+ const d = new Date(now)
27
+ d.setDate(d.getDate() - 1)
28
+ return d.toISOString().slice(0, 10)
29
+ }
30
+ case KpiPeriodType.MONTH: {
31
+ const d = new Date(now)
32
+ d.setMonth(d.getMonth() - 1)
33
+ return d.toISOString().slice(0, 7)
34
+ }
35
+ case KpiPeriodType.QUARTER: {
36
+ let year = now.getFullYear()
37
+ let quarter = Math.floor(now.getMonth() / 3)
38
+ if (quarter === 0) {
39
+ year -= 1
40
+ quarter = 4
41
+ }
42
+ return `${year}-Q${quarter}`
43
+ }
44
+ case KpiPeriodType.WEEK: {
45
+ const d = new Date(now)
46
+ d.setDate(d.getDate() - 7)
47
+ const year = d.getFullYear()
48
+ const week = getISOWeek(d)
49
+ return `${year}-W${week}`
50
+ }
51
+ case KpiPeriodType.ALLTIME:
52
+ return 'ALLTIME'
53
+ default: {
54
+ const d = new Date(now)
55
+ d.setDate(d.getDate() - 1)
56
+ return d.toISOString().slice(0, 10)
57
+ }
58
+ }
59
+ }
60
+
61
+ // ISO 주차 계산 함수
62
+ function getISOWeek(date: Date): number {
63
+ const tmp = new Date(date.getTime())
64
+ tmp.setHours(0, 0, 0, 0)
65
+ tmp.setDate(tmp.getDate() + 4 - (tmp.getDay() || 7))
66
+ const yearStart = new Date(tmp.getFullYear(), 0, 1)
67
+ const weekNo = Math.ceil(((tmp.getTime() - yearStart.getTime()) / 86400000 + 1) / 7)
68
+ return weekNo
69
+ }
70
+
71
+ @customElement('kpi-category-value-calculator')
72
+ export class KpiCategoryValueCalculator extends LitElement {
73
+ static styles = [
74
+ css`
75
+ :host {
76
+ display: block;
77
+ padding: 20px;
78
+ min-width: 400px;
79
+ }
80
+
81
+ .container {
82
+ display: flex;
83
+ flex-direction: column;
84
+ gap: 10px;
85
+ }
86
+
87
+ .form-group {
88
+ display: flex;
89
+ flex-direction: column;
90
+ margin-bottom: 15px;
91
+ }
92
+
93
+ label {
94
+ display: block;
95
+ margin-bottom: 5px;
96
+ font-weight: bold;
97
+ }
98
+
99
+ input,
100
+ select {
101
+ padding: 8px;
102
+ border: 1px solid #ccc;
103
+ border-radius: 4px;
104
+ }
105
+
106
+ .result {
107
+ margin-top: 20px;
108
+ padding: 15px;
109
+ background-color: #f5f5f5;
110
+ border-radius: 4px;
111
+ }
112
+
113
+ .result-value {
114
+ font-size: 18px;
115
+ font-weight: bold;
116
+ color: #2196f3;
117
+ }
118
+
119
+ .kpi-values {
120
+ margin-top: 10px;
121
+ }
122
+
123
+ .kpi-value-item {
124
+ display: flex;
125
+ justify-content: space-between;
126
+ padding: 5px 0;
127
+ border-bottom: 1px solid #eee;
128
+ }
129
+ `
130
+ ]
131
+
132
+ @property({ type: String }) categoryId = ''
133
+ @property({ type: String }) categoryName = ''
134
+ @state() valueDate = getLastValueDate()
135
+ @state() group = ''
136
+ @state() result: any = null
137
+ @state() loading = false
138
+
139
+ render() {
140
+ return html`
141
+ <div class="container">
142
+ <h3>${this.categoryName} - KPI 값 계산</h3>
143
+
144
+ <div class="form-group">
145
+ <label>계산 기준일</label>
146
+ <input type="date" .value=${this.valueDate} @input=${e => (this.valueDate = e.target.value)} />
147
+ </div>
148
+
149
+ <div class="form-group">
150
+ <label>그룹</label>
151
+ <input
152
+ type="text"
153
+ placeholder="그룹명 (선택사항)"
154
+ .value=${this.group}
155
+ @input=${e => (this.group = e.target.value)}
156
+ />
157
+ </div>
158
+
159
+ <button
160
+ @click=${this.calculateValue}
161
+ ?disabled=${this.loading}
162
+ style="width: 100%; padding: 10px; background: #2196f3; color: white; border: none; border-radius: 4px; cursor: pointer;"
163
+ >
164
+ ${this.loading ? '계산 중...' : '계산하기'}
165
+ </button>
166
+
167
+ ${this.result
168
+ ? html`
169
+ <div class="result">
170
+ <div class="result-value">
171
+ 계산 결과: ${this.result.value !== null ? this.result.value.toFixed(2) : 'N/A'}
172
+ </div>
173
+ ${this.result.kpiValues && this.result.kpiValues.length > 0
174
+ ? html`
175
+ <div class="kpi-values">
176
+ <h4>개별 KPI 값:</h4>
177
+ ${this.result.kpiValues.map(
178
+ item => html`
179
+ <div class="kpi-value-item">
180
+ <span>KPI ID: ${item.kpiId}</span>
181
+ <span>${item.value !== null ? item.value.toFixed(2) : 'N/A'}</span>
182
+ </div>
183
+ `
184
+ )}
185
+ </div>
186
+ `
187
+ : ''}
188
+ </div>
189
+ `
190
+ : ''}
191
+ </div>
192
+ `
193
+ }
194
+
195
+ async calculateValue() {
196
+ if (!this.categoryId) return
197
+
198
+ this.loading = true
199
+ try {
200
+ const response = await client.query({
201
+ query: gql`
202
+ query ($categoryId: String!, $valueDate: String, $group: String) {
203
+ calculateKpiValue(categoryId: $categoryId, valueDate: $valueDate, group: $group) {
204
+ value
205
+ valueDate
206
+ group
207
+ kpiValues {
208
+ kpiId
209
+ value
210
+ }
211
+ }
212
+ }
213
+ `,
214
+ variables: {
215
+ categoryId: this.categoryId,
216
+ valueDate: this.valueDate || null,
217
+ group: this.group || null
218
+ }
219
+ })
220
+
221
+ if (!response.errors) {
222
+ this.result = response.data.calculateKpiValue
223
+ }
224
+ } catch (error) {
225
+ console.error('KPI 값 계산 중 오류:', error)
226
+ notify({
227
+ message: 'KPI 값 계산 중 오류가 발생했습니다.'
228
+ })
229
+ } finally {
230
+ this.loading = false
231
+ }
232
+ }
233
+ }
@@ -13,6 +13,8 @@ import './kpi-history-viewer'
13
13
  import './kpi-list-summary'
14
14
  import './kpi-value-entry'
15
15
  import './kpi-alert-panel'
16
+ import '../../charts/kpi-radar-chart'
17
+ import '../../charts/kpi-boxplot-chart'
16
18
 
17
19
  @customElement('kpi-dashboard')
18
20
  export class KpiDashboardPage extends PageView {
@@ -28,6 +30,35 @@ export class KpiDashboardPage extends PageView {
28
30
  flex: 1;
29
31
  padding: 24px;
30
32
  }
33
+ .sample-charts-section {
34
+ display: flex;
35
+ gap: 40px;
36
+ margin-bottom: 48px;
37
+ align-items: flex-start;
38
+ }
39
+ .sample-chart-card {
40
+ background: #fff;
41
+ border-radius: 12px;
42
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
43
+ border: 1px solid #ececec;
44
+ padding: 24px 32px;
45
+ min-width: 340px;
46
+ flex: 1;
47
+ display: flex;
48
+ flex-direction: column;
49
+ align-items: stretch;
50
+ min-height: 500px;
51
+ }
52
+ .sample-chart-title {
53
+ font-size: 1.1rem;
54
+ font-weight: bold;
55
+ margin-bottom: 12px;
56
+ }
57
+ .sample-chart-container {
58
+ width: 100%;
59
+ height: 340px;
60
+ min-height: 340px;
61
+ }
31
62
  .category-section {
32
63
  margin-bottom: 40px;
33
64
  }
@@ -79,6 +110,127 @@ export class KpiDashboardPage extends PageView {
79
110
  @state() modalHistories: any[] = []
80
111
  @state() modalKpiName: string = ''
81
112
 
113
+ // 샘플 데이터
114
+ private get sampleCategories(): string[] {
115
+ return ['생산성', '품질', '안전', '환경', '비용', '일정']
116
+ }
117
+ private get sampleGroups(): string[] {
118
+ return ['A', 'B', 'C']
119
+ }
120
+ private get sampleSeriesData(): any[] {
121
+ // 각 카테고리별로 10개 이상의 다양한 값(평균, 분산, 이상치 포함)
122
+ // 생산성: 평균 높고 분산 큼, 이상치 포함
123
+ const 생산성 = [95, 92, 90, 88, 85, 80, 78, 75, 70, 60, 100] // 100은 이상치
124
+ // 품질: 평균 높고 분산 작음
125
+ const 품질 = [90, 89, 88, 87, 86, 85, 84, 83, 82, 80, 70]
126
+ // 안전: 평균 중간, 이상치 포함
127
+ const 안전 = [92, 91, 90, 89, 88, 87, 86, 85, 84, 65, 60] // 60은 이상치
128
+ // 환경: 낮은 값에 몰림, 분산 큼
129
+ const 환경 = [95, 90, 85, 80, 75, 70, 65, 60, 60, 60, 55]
130
+ // 비용: 전체적으로 낮음, 이상치 포함
131
+ const 비용 = [80, 78, 76, 74, 72, 70, 68, 66, 64, 62, 50] // 50은 이상치
132
+ // 일정: 분산 큼, 이상치 포함
133
+ const 일정 = [90, 88, 86, 84, 82, 80, 78, 76, 74, 60, 100] // 100은 이상치
134
+
135
+ const categories = this.sampleCategories
136
+ const groups = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K']
137
+ const all: any[] = []
138
+ categories.forEach((cat, i) => {
139
+ let arr: number[] = []
140
+ switch (cat) {
141
+ case '생산성':
142
+ arr = 생산성
143
+ break
144
+ case '품질':
145
+ arr = 품질
146
+ break
147
+ case '안전':
148
+ arr = 안전
149
+ break
150
+ case '환경':
151
+ arr = 환경
152
+ break
153
+ case '비용':
154
+ arr = 비용
155
+ break
156
+ case '일정':
157
+ arr = 일정
158
+ break
159
+ }
160
+ arr.forEach((v, idx) => {
161
+ all.push({ group: groups[idx] ?? `G${idx + 1}`, category: cat, value: v })
162
+ })
163
+ })
164
+ return all
165
+ }
166
+ private get sampleRadarData(): any[] {
167
+ // 카테고리별로 min, max, avg, A(자신의 그룹)만 반환
168
+ const categories = this.sampleCategories
169
+ const data = this.sampleSeriesData
170
+ const minData: any = { group: 'Min' }
171
+ const maxData: any = { group: 'Max' }
172
+ const avgData: any = { group: 'Avg' }
173
+ const aData: any = { group: 'A' }
174
+ categories.forEach(category => {
175
+ const values = data.filter(d => d.category === category).map(d => d.value)
176
+ minData[category] = Math.min(...values)
177
+ maxData[category] = Math.max(...values)
178
+ avgData[category] = values.reduce((a, b) => a + b, 0) / values.length
179
+ aData[category] = data.find(d => d.category === category && d.group === 'A')?.value ?? avgData[category]
180
+ })
181
+ // kpi-radar-chart가 기대하는 구조로 변환: [{group, category, value} ...]
182
+ const result: any[] = []
183
+ categories.forEach(category => {
184
+ result.push({ group: 'Min', category, value: minData[category] })
185
+ result.push({ group: 'Max', category, value: maxData[category] })
186
+ result.push({ group: 'Avg', category, value: avgData[category] })
187
+ result.push({ group: 'A', category, value: aData[category] })
188
+ })
189
+ return result
190
+ }
191
+ private get sampleRadarCategories(): string[] {
192
+ return this.sampleCategories
193
+ }
194
+ private get sampleRadarGroups(): string[] {
195
+ return this.sampleGroups
196
+ }
197
+ private get sampleBoxplotData(): any[] {
198
+ // 카테고리별로 그룹별 value의 분포 계산
199
+ const categories = this.sampleCategories
200
+ const data = this.sampleSeriesData
201
+ return categories.map(category => {
202
+ const values = data.filter(d => d.category === category).map(d => d.value)
203
+ const sorted = [...values].sort((a, b) => a - b)
204
+ const min = sorted[0]
205
+ const max = sorted[sorted.length - 1]
206
+ const mean = values.reduce((a, b) => a + b, 0) / values.length
207
+ const median =
208
+ sorted.length % 2 === 0
209
+ ? (sorted[sorted.length / 2 - 1] + sorted[sorted.length / 2]) / 2
210
+ : sorted[Math.floor(sorted.length / 2)]
211
+ const q1 = sorted[Math.floor(sorted.length / 4)]
212
+ const q3 = sorted[Math.floor((sorted.length * 3) / 4)]
213
+ // value: A그룹의 실제값(강조)
214
+ const value = data.find(d => d.category === category && d.group === 'A')?.value ?? mean
215
+ return {
216
+ group: category, // x축: 카테고리
217
+ min,
218
+ q1,
219
+ median,
220
+ q3,
221
+ max,
222
+ mean,
223
+ value
224
+ }
225
+ })
226
+ }
227
+ private get sampleBoxplotGroups(): string[] {
228
+ return this.sampleCategories
229
+ }
230
+ private get sampleCurrentGroup() {
231
+ return 'A'
232
+ }
233
+
82
234
  connectedCallback() {
83
235
  super.connectedCallback()
84
236
  this.fetchCategories()
@@ -313,6 +465,42 @@ export class KpiDashboardPage extends PageView {
313
465
  if (this.error) return html`<div>${this.error}</div>`
314
466
  return html`
315
467
  <div class="dashboard-root">
468
+ <!-- 샘플 차트 섹션 -->
469
+ <div class="sample-charts-section">
470
+ <div class="sample-chart-card">
471
+ <div class="sample-chart-title">그룹별 KPI 비교 (Radar)</div>
472
+ <div class="sample-chart-container">
473
+ <kpi-radar-chart
474
+ .data=${this.sampleRadarData ?? []}
475
+ .categories=${this.sampleRadarCategories ?? []}
476
+ .currentGroup=${this.sampleCurrentGroup}
477
+ ></kpi-radar-chart>
478
+ </div>
479
+ <div style="margin-top:12px;color:#666;font-size:0.98em;">
480
+ ※ <b>Radar 차트</b>는 각 카테고리별로 최소(Min), 최대(Max), 평균(Avg), 그리고 이 프로젝트의 값을 한눈에
481
+ 비교할 수 있도록 시각화합니다.<br />
482
+ <b>진한 파란색</b> 다각형이 이 프로젝트의 성과이며, 회색 다각형은 기준값(최소/최대/평균)입니다.
483
+ </div>
484
+ </div>
485
+ <div class="sample-chart-card">
486
+ <div class="sample-chart-title">그룹별 분포 (Boxplot)</div>
487
+ <div class="sample-chart-container">
488
+ <kpi-boxplot-chart
489
+ .data=${this.sampleBoxplotData ?? []}
490
+ .groups=${this.sampleBoxplotGroups ?? []}
491
+ .currentGroup=${this.sampleCurrentGroup}
492
+ ></kpi-boxplot-chart>
493
+ </div>
494
+ <div style="margin-top:12px;color:#666;font-size:0.98em;">
495
+ ※ <b>Boxplot(박스플롯)</b>은 각 카테고리별로 값의 분포(최소, 1사분위, 중앙값, 3사분위, 최대, 평균, 이상치
496
+ 등)를 보여줍니다.<br />
497
+ <b>박스</b>는 중앙 50% 구간, <b>수염</b>은 전체 범위, <b>굵은 검정색 가로선</b>은 중앙값(메디안),
498
+ <b>주황색 원</b>은 평균값(Mean)을 의미합니다.<br />
499
+ <b>이 프로젝트의 값</b>은 <b>진한 오렌지색 원</b>으로 별도 강조되어 표시되며, 중앙값/평균과 다를 수
500
+ 있습니다.<br />
501
+ </div>
502
+ </div>
503
+ </div>
316
504
  ${this.showHistoryModal
317
505
  ? html`
318
506
  <div
@@ -194,7 +194,19 @@ export class KpiMetricListPage extends connect(store)(localize(i18next)(ScopedEl
194
194
  type: 'select',
195
195
  name: 'periodType',
196
196
  header: '주기',
197
- record: { editable: true, options: ['', 'DAY', 'WEEK', 'MONTH', 'QUARTER', 'YEAR', 'RANGE'] },
197
+ record: {
198
+ editable: true,
199
+ options: [
200
+ { value: '', display: '' },
201
+ { value: 'DAY', display: '일' },
202
+ { value: 'WEEK', display: '주' },
203
+ { value: 'MONTH', display: '월' },
204
+ { value: 'QUARTER', display: '분기' },
205
+ { value: 'YEAR', display: '년' },
206
+ { value: 'RANGE', display: '범위' },
207
+ { value: 'ALLTIME', display: '전체' }
208
+ ]
209
+ },
198
210
  width: 80
199
211
  },
200
212
  { type: 'checkbox', name: 'active', label: true, header: '활성', record: { editable: true }, width: 60 },
@@ -3,6 +3,7 @@ import '@material/web/button/elevated-button.js'
3
3
  import '@operato/data-grist/ox-grist.js'
4
4
  import '@operato/data-grist/ox-filters-form.js'
5
5
  import '@operato/data-grist/ox-record-creator.js'
6
+ import './kpi-metric-value-manual-entry-form.js'
6
7
 
7
8
  import { CommonButtonStyles, CommonHeaderStyles, CommonGristStyles, ScrollbarStyles } from '@operato/styles'
8
9
  import { PageView, store } from '@operato/shell'
@@ -113,6 +114,18 @@ export class KpiMetricValueListPage extends connect(store)(localize(i18next)(Sco
113
114
  columns: [
114
115
  { type: 'gutter', gutterName: 'sequence' },
115
116
  { type: 'gutter', gutterName: 'row-selector', multiple: true },
117
+ // KPI Metric Value 수정 버튼 추가
118
+ {
119
+ type: 'gutter',
120
+ gutterName: 'button',
121
+ icon: 'edit',
122
+ title: '수정',
123
+ handlers: {
124
+ click: (columns, data, column, record, rowIndex) => {
125
+ this._editKpiMetricValue(record)
126
+ }
127
+ }
128
+ },
116
129
  {
117
130
  type: 'string',
118
131
  name: 'metric',
@@ -120,7 +133,7 @@ export class KpiMetricValueListPage extends connect(store)(localize(i18next)(Sco
120
133
  record: { editable: false, renderer: (v, c, r) => r.metric?.name },
121
134
  width: 150
122
135
  },
123
- { type: 'date', name: 'valueDate', header: '날짜', record: { editable: true }, width: 120 },
136
+ { type: 'string', name: 'valueDate', header: '날짜', record: { editable: true }, width: 120 },
124
137
  { type: 'number', name: 'value', header: '값', record: { editable: true }, width: 120 },
125
138
  { type: 'string', name: 'group', header: '그룹', record: { editable: false }, width: 120 },
126
139
  { type: 'object', name: 'meta', header: '메타', record: { editable: false }, width: 120 },
@@ -151,6 +164,12 @@ export class KpiMetricValueListPage extends connect(store)(localize(i18next)(Sco
151
164
  }
152
165
  }
153
166
 
167
+ pageUpdated(changes: any, lifecycle: any) {
168
+ if (this.active) {
169
+ this.grist.fetch()
170
+ }
171
+ }
172
+
154
173
  async fetchHandler({ page = 1, limit = 100, sortings = [], filters = [] }: FetchOption) {
155
174
  const response = await client.query({
156
175
  query: gql`
@@ -296,4 +315,27 @@ export class KpiMetricValueListPage extends connect(store)(localize(i18next)(Sco
296
315
  async importHandler(records) {
297
316
  // 임포트 팝업 등은 추후 구현
298
317
  }
318
+
319
+ async _editKpiMetricValue(metricValue) {
320
+ const popup = await openPopup(
321
+ html`
322
+ <kpi-metric-value-manual-entry-form
323
+ .metric=${metricValue.metric}
324
+ .valueDate=${metricValue.valueDate}
325
+ .value=${metricValue.value}
326
+ .group=${metricValue.group}
327
+ .meta=${metricValue.meta ? JSON.stringify(metricValue.meta) : ''}
328
+ @saved=${() => {
329
+ this.grist.fetch()
330
+ popup.close()
331
+ }}
332
+ ></kpi-metric-value-manual-entry-form>
333
+ `,
334
+ {
335
+ title: `${metricValue.metric?.name || ''} 값 수정`,
336
+ size: 'medium',
337
+ backdrop: true
338
+ }
339
+ )
340
+ }
299
341
  }
@@ -111,24 +111,14 @@ export class KpiMetricValueManualEntryForm extends LitElement {
111
111
  try {
112
112
  await client.mutate({
113
113
  mutation: gql`
114
- mutation ($metricId: ID!, $valueDate: String!, $value: Float!, $group: String, $meta: Object) {
115
- createKpiMetricValue(
116
- metricValue: {
117
- metricId: $metricId
118
- valueDate: $valueDate
119
- value: $value
120
- group: $group
121
- meta: $meta
122
- periodType: DAY
123
- }
124
- ) {
114
+ mutation ($metricName: String!, $value: Float!, $group: String, $meta: Object) {
115
+ recordKpiMetricValue(metricName: $metricName, value: $value, meta: $meta, group: $group) {
125
116
  id
126
117
  }
127
118
  }
128
119
  `,
129
120
  variables: {
130
- metricId: this.metric.id,
131
- valueDate: this.valueDate,
121
+ metricName: this.metric.name,
132
122
  value: parseFloat(this.value),
133
123
  group: this.group || undefined,
134
124
  meta: parsedMeta
@@ -99,7 +99,19 @@ export class KpiMetricValueManualEntryPage extends connect(store)(localize(i18ne
99
99
  type: 'select',
100
100
  name: 'periodType',
101
101
  header: '주기',
102
- record: { editable: true, options: ['', 'DAY', 'WEEK', 'MONTH', 'QUARTER', 'YEAR', 'RANGE'] },
102
+ record: {
103
+ editable: true,
104
+ options: [
105
+ { value: '', display: '' },
106
+ { value: 'DAY', display: '일' },
107
+ { value: 'WEEK', display: '주' },
108
+ { value: 'MONTH', display: '월' },
109
+ { value: 'QUARTER', display: '분기' },
110
+ { value: 'YEAR', display: '년' },
111
+ { value: 'RANGE', display: '범위' },
112
+ { value: 'ALLTIME', display: '전체' }
113
+ ]
114
+ },
103
115
  width: 80
104
116
  },
105
117
  {
@@ -130,6 +130,7 @@ export class KpiValueListPage extends connect(store)(localize(i18next)(ScopedEle
130
130
  'version',
131
131
  'valueDate',
132
132
  'value',
133
+ 'score',
133
134
  'group',
134
135
  'inputType',
135
136
  'source',
@@ -144,6 +145,7 @@ export class KpiValueListPage extends connect(store)(localize(i18next)(ScopedEle
144
145
  'version',
145
146
  'valueDate',
146
147
  'value',
148
+ 'score',
147
149
  'group',
148
150
  'inputType',
149
151
  'source',
@@ -157,6 +159,18 @@ export class KpiValueListPage extends connect(store)(localize(i18next)(ScopedEle
157
159
  columns: [
158
160
  { type: 'gutter', gutterName: 'sequence' },
159
161
  { type: 'gutter', gutterName: 'row-selector', multiple: true },
162
+ // KPI Value 재계산 버튼 추가
163
+ {
164
+ type: 'gutter',
165
+ gutterName: 'button',
166
+ icon: 'refresh',
167
+ title: '재계산',
168
+ handlers: {
169
+ click: (columns, data, column, record, rowIndex) => {
170
+ this._recalculateKpiValue(record)
171
+ }
172
+ }
173
+ },
160
174
  {
161
175
  type: 'string',
162
176
  name: 'kpi',
@@ -165,8 +179,9 @@ export class KpiValueListPage extends connect(store)(localize(i18next)(ScopedEle
165
179
  width: 150
166
180
  },
167
181
  { type: 'number', name: 'version', header: '버전', record: { editable: false }, width: 80 },
168
- { type: 'date', name: 'valueDate', header: '실적일', record: { editable: true }, width: 120 },
182
+ { type: 'string', name: 'valueDate', header: '실적일', record: { editable: true }, width: 120 },
169
183
  { type: 'number', name: 'value', header: '실적값', record: { editable: true }, width: 120 },
184
+ { type: 'number', name: 'score', header: '성과점수', record: { editable: false }, width: 120 },
170
185
  { type: 'string', name: 'group', header: '그룹', record: { editable: false }, width: 120 },
171
186
  { type: 'string', name: 'inputType', header: '입력방식', record: { editable: false }, width: 100 },
172
187
  { type: 'string', name: 'source', header: '수집출처', record: { editable: false }, width: 120 },
@@ -218,6 +233,7 @@ export class KpiValueListPage extends connect(store)(localize(i18next)(ScopedEle
218
233
  version
219
234
  valueDate
220
235
  value
236
+ score
221
237
  inputType
222
238
  source
223
239
  meta
@@ -308,6 +324,7 @@ export class KpiValueListPage extends connect(store)(localize(i18next)(ScopedEle
308
324
  version
309
325
  valueDate
310
326
  value
327
+ score
311
328
  }
312
329
  }
313
330
  `,
@@ -322,6 +339,33 @@ export class KpiValueListPage extends connect(store)(localize(i18next)(ScopedEle
322
339
  }
323
340
  }
324
341
 
342
+ async _recalculateKpiValue(kpiValue) {
343
+ try {
344
+ const response = await client.mutate({
345
+ mutation: gql`
346
+ mutation ($id: String!) {
347
+ recalculateKpiValue(id: $id) {
348
+ id
349
+ value
350
+ score
351
+ valueDate
352
+ group
353
+ }
354
+ }
355
+ `,
356
+ variables: {
357
+ id: kpiValue.id
358
+ }
359
+ })
360
+ if (!response.errors) {
361
+ notify({ message: 'KPI Value가 성공적으로 재계산되었습니다.' })
362
+ this.grist.fetch()
363
+ }
364
+ } catch (error) {
365
+ notify({ message: 'KPI Value 재계산 중 오류가 발생했습니다.' })
366
+ }
367
+ }
368
+
325
369
  async creationCallback(kpiValue) {
326
370
  try {
327
371
  const response = await client.query({