@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.
- package/client/charts/kpi-boxplot-chart.ts +163 -0
- package/client/charts/kpi-radar-chart.ts +128 -0
- package/client/pages/kpi/kpi-list-page.ts +180 -22
- package/client/pages/kpi-category/kpi-category-list-page.ts +76 -3
- package/client/pages/kpi-category/kpi-category-value-calculator.ts +233 -0
- package/client/pages/kpi-dashboard/kpi-dashboard.ts +188 -0
- package/client/pages/kpi-metric/kpi-metric-list-page.ts +13 -1
- package/client/pages/kpi-metric-value/kpi-metric-value-list-page.ts +43 -1
- package/client/pages/kpi-metric-value/kpi-metric-value-manual-entry-form.ts +3 -13
- package/client/pages/kpi-metric-value/kpi-metric-value-manual-entry-page.ts +13 -1
- package/client/pages/kpi-value/kpi-value-list-page.ts +45 -1
- package/dist-client/charts/kpi-boxplot-chart.d.ts +22 -0
- package/dist-client/charts/kpi-boxplot-chart.js +198 -0
- package/dist-client/charts/kpi-boxplot-chart.js.map +1 -0
- package/dist-client/charts/kpi-radar-chart.d.ts +16 -0
- package/dist-client/charts/kpi-radar-chart.js +138 -0
- package/dist-client/charts/kpi-radar-chart.js.map +1 -0
- package/dist-client/pages/kpi/kpi-list-page.d.ts +2 -1
- package/dist-client/pages/kpi/kpi-list-page.js +180 -22
- package/dist-client/pages/kpi/kpi-list-page.js.map +1 -1
- package/dist-client/pages/kpi-category/kpi-category-list-page.d.ts +3 -0
- package/dist-client/pages/kpi-category/kpi-category-list-page.js +71 -3
- package/dist-client/pages/kpi-category/kpi-category-list-page.js.map +1 -1
- package/dist-client/pages/kpi-category/kpi-category-value-calculator.d.ts +13 -0
- package/dist-client/pages/kpi-category/kpi-category-value-calculator.js +256 -0
- package/dist-client/pages/kpi-category/kpi-category-value-calculator.js.map +1 -0
- package/dist-client/pages/kpi-dashboard/kpi-dashboard.d.ts +11 -0
- package/dist-client/pages/kpi-dashboard/kpi-dashboard.js +185 -0
- package/dist-client/pages/kpi-dashboard/kpi-dashboard.js.map +1 -1
- package/dist-client/pages/kpi-metric/kpi-metric-list-page.js +13 -1
- package/dist-client/pages/kpi-metric/kpi-metric-list-page.js.map +1 -1
- package/dist-client/pages/kpi-metric-value/kpi-metric-value-list-page.d.ts +4 -1
- package/dist-client/pages/kpi-metric-value/kpi-metric-value-list-page.js +39 -2
- 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-form.js +3 -13
- package/dist-client/pages/kpi-metric-value/kpi-metric-value-manual-entry-form.js.map +1 -1
- package/dist-client/pages/kpi-metric-value/kpi-metric-value-manual-entry-page.js +13 -1
- package/dist-client/pages/kpi-metric-value/kpi-metric-value-manual-entry-page.js.map +1 -1
- package/dist-client/pages/kpi-value/kpi-value-list-page.d.ts +1 -0
- package/dist-client/pages/kpi-value/kpi-value-list-page.js +45 -1
- package/dist-client/pages/kpi-value/kpi-value-list-page.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-server/calculator/evaluator.d.ts +8 -0
- package/dist-server/calculator/evaluator.js +42 -0
- package/dist-server/calculator/evaluator.js.map +1 -0
- package/dist-server/calculator/functions.d.ts +3 -0
- package/dist-server/calculator/functions.js +62 -0
- package/dist-server/calculator/functions.js.map +1 -0
- package/dist-server/calculator/index.d.ts +4 -0
- package/dist-server/calculator/index.js +8 -0
- package/dist-server/calculator/index.js.map +1 -0
- package/dist-server/calculator/parser.d.ts +21 -0
- package/dist-server/calculator/parser.js +121 -0
- package/dist-server/calculator/parser.js.map +1 -0
- package/dist-server/calculator/provider.d.ts +8 -0
- package/dist-server/calculator/provider.js +13 -0
- package/dist-server/calculator/provider.js.map +1 -0
- package/dist-server/controllers/kpi-metric-value-provider.d.ts +11 -0
- package/dist-server/controllers/kpi-metric-value-provider.js +63 -0
- package/dist-server/controllers/kpi-metric-value-provider.js.map +1 -0
- package/dist-server/controllers/kpi-value-provider.d.ts +11 -0
- package/dist-server/controllers/kpi-value-provider.js +46 -0
- package/dist-server/controllers/kpi-value-provider.js.map +1 -0
- package/dist-server/service/index.d.ts +2 -2
- package/dist-server/service/kpi/aggregate-kpi.js +4 -4
- package/dist-server/service/kpi/aggregate-kpi.js.map +1 -1
- package/dist-server/service/kpi/kpi-grade.types.d.ts +11 -10
- package/dist-server/service/kpi/kpi-grade.types.js.map +1 -1
- package/dist-server/service/kpi/kpi-history.d.ts +2 -2
- package/dist-server/service/kpi/kpi-history.js.map +1 -1
- package/dist-server/service/kpi/kpi-mutation.d.ts +2 -0
- package/dist-server/service/kpi/kpi-mutation.js +126 -4
- package/dist-server/service/kpi/kpi-mutation.js.map +1 -1
- package/dist-server/service/kpi/kpi-type.d.ts +8 -5
- package/dist-server/service/kpi/kpi-type.js +22 -8
- package/dist-server/service/kpi/kpi-type.js.map +1 -1
- package/dist-server/service/kpi/kpi.d.ts +6 -3
- package/dist-server/service/kpi/kpi.js +29 -9
- package/dist-server/service/kpi/kpi.js.map +1 -1
- package/dist-server/service/kpi-category/kpi-category-mutation.d.ts +1 -1
- package/dist-server/service/kpi-category/kpi-category-mutation.js +3 -3
- package/dist-server/service/kpi-category/kpi-category-mutation.js.map +1 -1
- package/dist-server/service/kpi-category/kpi-category-query.d.ts +13 -0
- package/dist-server/service/kpi-category/kpi-category-query.js +180 -0
- package/dist-server/service/kpi-category/kpi-category-query.js.map +1 -1
- package/dist-server/service/kpi-category/kpi-category-type.d.ts +3 -0
- package/dist-server/service/kpi-category/kpi-category-type.js +16 -1
- package/dist-server/service/kpi-category/kpi-category-type.js.map +1 -1
- package/dist-server/service/kpi-category/kpi-category.d.ts +2 -0
- package/dist-server/service/kpi-category/kpi-category.js +10 -1
- package/dist-server/service/kpi-category/kpi-category.js.map +1 -1
- package/dist-server/service/kpi-metric/kpi-metric-type.d.ts +5 -3
- package/dist-server/service/kpi-metric/kpi-metric-type.js +5 -3
- package/dist-server/service/kpi-metric/kpi-metric-type.js.map +1 -1
- package/dist-server/service/kpi-metric/kpi-metric.d.ts +2 -8
- package/dist-server/service/kpi-metric/kpi-metric.js +3 -14
- package/dist-server/service/kpi-metric/kpi-metric.js.map +1 -1
- package/dist-server/service/kpi-metric-value/kpi-metric-value-mutation.js +67 -45
- package/dist-server/service/kpi-metric-value/kpi-metric-value-mutation.js.map +1 -1
- package/dist-server/service/kpi-metric-value/kpi-metric-value.js +3 -2
- package/dist-server/service/kpi-metric-value/kpi-metric-value.js.map +1 -1
- package/dist-server/service/kpi-value/kpi-value-mutation.d.ts +2 -1
- package/dist-server/service/kpi-value/kpi-value-mutation.js +114 -6
- package/dist-server/service/kpi-value/kpi-value-mutation.js.map +1 -1
- package/dist-server/service/kpi-value/kpi-value-query.d.ts +0 -2
- package/dist-server/service/kpi-value/kpi-value-query.js +0 -12
- package/dist-server/service/kpi-value/kpi-value-query.js.map +1 -1
- package/dist-server/service/kpi-value/kpi-value-score.service.d.ts +26 -0
- package/dist-server/service/kpi-value/kpi-value-score.service.js +97 -0
- package/dist-server/service/kpi-value/kpi-value-score.service.js.map +1 -0
- package/dist-server/service/kpi-value/kpi-value-type.d.ts +2 -0
- package/dist-server/service/kpi-value/kpi-value-type.js +14 -0
- package/dist-server/service/kpi-value/kpi-value-type.js.map +1 -1
- package/dist-server/service/kpi-value/kpi-value.d.ts +1 -0
- package/dist-server/service/kpi-value/kpi-value.js +9 -1
- package/dist-server/service/kpi-value/kpi-value.js.map +1 -1
- package/dist-server/service/utils/value-date-util.d.ts +3 -0
- package/dist-server/service/utils/value-date-util.js +76 -0
- package/dist-server/service/utils/value-date-util.js.map +1 -0
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/server/calculator/evaluator.ts +45 -0
- package/server/calculator/functions.ts +67 -0
- package/server/calculator/index.ts +4 -0
- package/server/calculator/parser.ts +128 -0
- package/server/calculator/provider.ts +10 -0
- package/server/controllers/kpi-metric-value-provider.ts +66 -0
- package/server/controllers/kpi-value-provider.ts +51 -0
- package/server/service/kpi/aggregate-kpi.ts +4 -4
- package/server/service/kpi/kpi-grade.types.ts +11 -10
- package/server/service/kpi/kpi-history.ts +2 -2
- package/server/service/kpi/kpi-mutation.ts +128 -4
- package/server/service/kpi/kpi-type.ts +21 -9
- package/server/service/kpi/kpi.ts +32 -10
- package/server/service/kpi-category/kpi-category-mutation.ts +3 -3
- package/server/service/kpi-category/kpi-category-query.ts +175 -1
- package/server/service/kpi-category/kpi-category-type.ts +17 -6
- package/server/service/kpi-category/kpi-category.ts +10 -1
- package/server/service/kpi-metric/kpi-metric-type.ts +7 -5
- package/server/service/kpi-metric/kpi-metric.ts +3 -15
- package/server/service/kpi-metric-value/kpi-metric-value-mutation.ts +67 -47
- package/server/service/kpi-metric-value/kpi-metric-value.ts +4 -2
- package/server/service/kpi-value/kpi-value-mutation.ts +110 -6
- package/server/service/kpi-value/kpi-value-query.ts +2 -8
- package/server/service/kpi-value/kpi-value-score.service.ts +112 -0
- package/server/service/kpi-value/kpi-value-type.ts +12 -0
- package/server/service/kpi-value/kpi-value.ts +8 -1
- package/server/service/utils/value-date-util.ts +72 -0
- package/dist-server/service/kpi-value/kpi-value-grade.service.d.ts +0 -34
- package/dist-server/service/kpi-value/kpi-value-grade.service.js +0 -117
- package/dist-server/service/kpi-value/kpi-value-grade.service.js.map +0 -1
- 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: {
|
|
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: '
|
|
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 ($
|
|
115
|
-
|
|
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
|
-
|
|
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: {
|
|
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: '
|
|
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({
|