@things-factory/kpi 9.0.23 → 9.0.25

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 (76) hide show
  1. package/client/pages/kpi/kpi-viz-editor.ts +1 -1
  2. package/client/pages/kpi-category-value/kpi-category-value-list-page.ts +404 -0
  3. package/client/pages/kpi-metric-value/kpi-metric-value-editor-page.ts +763 -0
  4. package/client/pages/kpi-metric-value/kpi-metric-value-list-page.ts +12 -0
  5. package/client/pages/kpi-value/kpi-value-editor-page.ts +774 -0
  6. package/client/pages/kpi-value/kpi-value-list-page.ts +13 -0
  7. package/client/route.ts +16 -0
  8. package/dist-client/pages/kpi/kpi-viz-editor.d.ts +0 -1
  9. package/dist-client/pages/kpi/kpi-viz-editor.js +1 -1
  10. package/dist-client/pages/kpi/kpi-viz-editor.js.map +1 -1
  11. package/dist-client/pages/kpi-category-value/kpi-category-value-list-page.d.ts +63 -0
  12. package/dist-client/pages/kpi-category-value/kpi-category-value-list-page.js +393 -0
  13. package/dist-client/pages/kpi-category-value/kpi-category-value-list-page.js.map +1 -0
  14. package/dist-client/pages/kpi-metric-value/kpi-metric-value-editor-page.d.ts +58 -0
  15. package/dist-client/pages/kpi-metric-value/kpi-metric-value-editor-page.js +736 -0
  16. package/dist-client/pages/kpi-metric-value/kpi-metric-value-editor-page.js.map +1 -0
  17. package/dist-client/pages/kpi-metric-value/kpi-metric-value-list-page.d.ts +1 -0
  18. package/dist-client/pages/kpi-metric-value/kpi-metric-value-list-page.js +11 -0
  19. package/dist-client/pages/kpi-metric-value/kpi-metric-value-list-page.js.map +1 -1
  20. package/dist-client/pages/kpi-value/kpi-value-editor-page.d.ts +55 -0
  21. package/dist-client/pages/kpi-value/kpi-value-editor-page.js +748 -0
  22. package/dist-client/pages/kpi-value/kpi-value-editor-page.js.map +1 -0
  23. package/dist-client/pages/kpi-value/kpi-value-list-page.d.ts +9 -2
  24. package/dist-client/pages/kpi-value/kpi-value-list-page.js +12 -0
  25. package/dist-client/pages/kpi-value/kpi-value-list-page.js.map +1 -1
  26. package/dist-client/route.d.ts +1 -1
  27. package/dist-client/route.js +12 -0
  28. package/dist-client/route.js.map +1 -1
  29. package/dist-client/tsconfig.tsbuildinfo +1 -1
  30. package/dist-server/service/index.d.ts +4 -2
  31. package/dist-server/service/index.js +5 -0
  32. package/dist-server/service/index.js.map +1 -1
  33. package/dist-server/service/kpi-category/kpi-category-mutation.d.ts +2 -3
  34. package/dist-server/service/kpi-category/kpi-category-mutation.js +149 -78
  35. package/dist-server/service/kpi-category/kpi-category-mutation.js.map +1 -1
  36. package/dist-server/service/kpi-category/kpi-category-query.d.ts +1 -9
  37. package/dist-server/service/kpi-category/kpi-category-query.js +3 -165
  38. package/dist-server/service/kpi-category/kpi-category-query.js.map +1 -1
  39. package/dist-server/service/kpi-category-value/index.d.ts +6 -0
  40. package/dist-server/service/kpi-category-value/index.js +10 -0
  41. package/dist-server/service/kpi-category-value/index.js.map +1 -0
  42. package/dist-server/service/kpi-category-value/kpi-category-value-mutation.d.ts +8 -0
  43. package/dist-server/service/kpi-category-value/kpi-category-value-mutation.js +102 -0
  44. package/dist-server/service/kpi-category-value/kpi-category-value-mutation.js.map +1 -0
  45. package/dist-server/service/kpi-category-value/kpi-category-value-query.d.ts +13 -0
  46. package/dist-server/service/kpi-category-value/kpi-category-value-query.js +91 -0
  47. package/dist-server/service/kpi-category-value/kpi-category-value-query.js.map +1 -0
  48. package/dist-server/service/kpi-category-value/kpi-category-value-type.d.ts +19 -0
  49. package/dist-server/service/kpi-category-value/kpi-category-value-type.js +73 -0
  50. package/dist-server/service/kpi-category-value/kpi-category-value-type.js.map +1 -0
  51. package/dist-server/service/kpi-category-value/kpi-category-value.d.ts +19 -0
  52. package/dist-server/service/kpi-category-value/kpi-category-value.js +91 -0
  53. package/dist-server/service/kpi-category-value/kpi-category-value.js.map +1 -0
  54. package/dist-server/service/kpi-metric-value/kpi-metric-value-mutation.js +20 -0
  55. package/dist-server/service/kpi-metric-value/kpi-metric-value-mutation.js.map +1 -1
  56. package/dist-server/service/kpi-value/kpi-value-mutation.d.ts +1 -0
  57. package/dist-server/service/kpi-value/kpi-value-mutation.js +60 -0
  58. package/dist-server/service/kpi-value/kpi-value-mutation.js.map +1 -1
  59. package/dist-server/tsconfig.tsbuildinfo +1 -1
  60. package/package.json +5 -5
  61. package/server/service/index.ts +5 -0
  62. package/server/service/kpi-category/kpi-category-mutation.ts +154 -81
  63. package/server/service/kpi-category/kpi-category-query.ts +1 -155
  64. package/server/service/kpi-category-value/index.ts +7 -0
  65. package/server/service/kpi-category-value/kpi-category-value-mutation.ts +88 -0
  66. package/server/service/kpi-category-value/kpi-category-value-query.ts +62 -0
  67. package/server/service/kpi-category-value/kpi-category-value-type.ts +48 -0
  68. package/server/service/kpi-category-value/kpi-category-value.ts +79 -0
  69. package/server/service/kpi-metric-value/kpi-metric-value-mutation.ts +28 -0
  70. package/server/service/kpi-value/kpi-value-mutation.ts +66 -0
  71. package/things-factory.config.js +3 -0
  72. package/translations/en.json +3 -0
  73. package/translations/ja.json +3 -0
  74. package/translations/ko.json +3 -0
  75. package/translations/ms.json +3 -0
  76. package/translations/zh.json +3 -0
@@ -0,0 +1,748 @@
1
+ import { __decorate, __metadata } from "tslib";
2
+ import '@material/web/icon/icon.js';
3
+ import '@material/web/button/elevated-button.js';
4
+ import '@material/web/textfield/outlined-text-field.js';
5
+ import { CommonButtonStyles, CommonHeaderStyles, ScrollbarStyles } from '@operato/styles';
6
+ import { PageView, store } from '@operato/shell';
7
+ import { css, html } from 'lit';
8
+ import { customElement, property, state } from 'lit/decorators.js';
9
+ import { ScopedElementsMixin } from '@open-wc/scoped-elements';
10
+ import { client } from '@operato/graphql';
11
+ import { i18next, localize } from '@operato/i18n';
12
+ import { notify } from '@operato/layout';
13
+ import { connect } from 'pwa-helpers/connect-mixin';
14
+ import gql from 'graphql-tag';
15
+ let KpiValueEditorPage = class KpiValueEditorPage extends connect(store)(localize(i18next)(ScopedElementsMixin(PageView))) {
16
+ constructor() {
17
+ super(...arguments);
18
+ this.group = '';
19
+ this.startDate = '';
20
+ this.endDate = '';
21
+ this.kpis = [];
22
+ this.dates = [];
23
+ this.loading = false;
24
+ this.error = '';
25
+ this.editingCell = null;
26
+ this._existingValues = [];
27
+ }
28
+ static { this.styles = [
29
+ CommonHeaderStyles,
30
+ ScrollbarStyles,
31
+ css `
32
+ :host {
33
+ display: flex;
34
+ flex-direction: column;
35
+ padding: 20px;
36
+ overflow-x: auto;
37
+ }
38
+
39
+ .header {
40
+ display: flex;
41
+ gap: 16px;
42
+ align-items: center;
43
+ margin-bottom: 20px;
44
+ padding: 16px;
45
+ background: var(--md-sys-color-surface-container);
46
+ border-radius: 8px;
47
+ }
48
+
49
+ .controls {
50
+ display: flex;
51
+ gap: 12px;
52
+ align-items: center;
53
+ }
54
+
55
+ .table-container {
56
+ flex: 1;
57
+ overflow: auto;
58
+ border: 1px solid var(--md-sys-color-outline);
59
+ border-radius: 8px;
60
+ }
61
+
62
+ table {
63
+ width: 100%;
64
+ border-collapse: collapse;
65
+ min-width: max-content;
66
+ }
67
+
68
+ th {
69
+ background: var(--md-sys-color-surface-container-low);
70
+ font-weight: 500;
71
+ padding: 8px 12px;
72
+ border: 1px solid var(--md-sys-color-outline-variant);
73
+ min-width: 80px;
74
+ height: 120px;
75
+ vertical-align: middle;
76
+ }
77
+
78
+ td {
79
+ padding: 8px 12px;
80
+ border: 1px solid var(--md-sys-color-outline-variant);
81
+ min-width: 80px;
82
+ height: 60px;
83
+ text-align: right;
84
+ vertical-align: middle;
85
+ }
86
+
87
+ .kpi-header {
88
+ position: sticky;
89
+ left: 0;
90
+ top: 0;
91
+ z-index: 3;
92
+ }
93
+
94
+ .kpi-name {
95
+ position: sticky;
96
+ left: 0;
97
+ background: var(--md-sys-color-surface);
98
+ font-weight: 500;
99
+ min-width: 200px;
100
+ text-align: left;
101
+ z-index: 2;
102
+ }
103
+
104
+ tr:hover {
105
+ background: var(--md-sys-color-surface-container-high);
106
+ }
107
+
108
+ th.date-header {
109
+ position: sticky;
110
+ top: 0;
111
+ z-index: 2;
112
+ text-align: center;
113
+ font-size: 11px;
114
+ color: var(--md-sys-color-on-surface-variant);
115
+ writing-mode: vertical-rl;
116
+ text-orientation: mixed;
117
+ min-height: 120px;
118
+ display: flex;
119
+ align-items: center;
120
+ justify-content: center;
121
+ }
122
+
123
+ td.editable-cell {
124
+ cursor: pointer;
125
+ background: var(--md-sys-color-primary-container);
126
+ color: var(--md-sys-color-on-primary-container);
127
+ }
128
+
129
+ td.editable-cell:hover {
130
+ background: var(--md-sys-color-primary-container-high);
131
+ }
132
+
133
+ td.highlighted-cell {
134
+ background: var(--md-sys-color-secondary-container);
135
+ color: var(--md-sys-color-on-secondary-container);
136
+ font-weight: 500;
137
+ }
138
+
139
+ td.highlighted-cell:hover {
140
+ background: var(--md-sys-color-secondary-container-high);
141
+ }
142
+
143
+ td.disabled-cell {
144
+ background: var(--md-sys-color-surface-container);
145
+ color: var(--md-sys-color-on-surface-variant);
146
+ cursor: not-allowed;
147
+ }
148
+
149
+ .value-input {
150
+ width: 100%;
151
+ text-align: center;
152
+ border: none;
153
+ background: transparent;
154
+ color: inherit;
155
+ font-size: 12px;
156
+ padding: 2px;
157
+ }
158
+
159
+ .value-input:focus {
160
+ outline: 2px solid var(--md-sys-color-primary);
161
+ border-radius: 4px;
162
+ }
163
+
164
+ .period-type-badge {
165
+ font-size: 10px;
166
+ padding: 2px 6px;
167
+ border-radius: 4px;
168
+ background: var(--md-sys-color-tertiary-container);
169
+ color: var(--md-sys-color-on-tertiary-container);
170
+ margin-left: 8px;
171
+ }
172
+
173
+ .loading {
174
+ display: flex;
175
+ justify-content: center;
176
+ align-items: center;
177
+ height: 200px;
178
+ color: var(--md-sys-color-on-surface-variant);
179
+ }
180
+
181
+ .error {
182
+ color: var(--md-sys-color-error);
183
+ padding: 16px;
184
+ text-align: center;
185
+ }
186
+
187
+ .legend {
188
+ display: flex;
189
+ gap: 16px;
190
+ margin-top: 16px;
191
+ font-size: 12px;
192
+ }
193
+
194
+ .legend-item {
195
+ display: flex;
196
+ align-items: center;
197
+ gap: 4px;
198
+ }
199
+
200
+ .legend-color {
201
+ width: 16px;
202
+ height: 16px;
203
+ border-radius: 2px;
204
+ }
205
+ `
206
+ ]; }
207
+ get context() {
208
+ return {
209
+ title: i18next.t('title.kpi value editor'),
210
+ actions: [
211
+ {
212
+ title: i18next.t('button.save'),
213
+ action: this._saveValues.bind(this),
214
+ ...CommonButtonStyles.save
215
+ },
216
+ {
217
+ title: i18next.t('button.cancel'),
218
+ action: this._cancel.bind(this),
219
+ ...CommonButtonStyles.cancel
220
+ }
221
+ ]
222
+ };
223
+ }
224
+ render() {
225
+ if (this.loading) {
226
+ return html `
227
+ <div class="loading">
228
+ <md-icon>hourglass_empty</md-icon>
229
+ <span>데이터를 불러오는 중...</span>
230
+ </div>
231
+ `;
232
+ }
233
+ if (this.error) {
234
+ return html `
235
+ <div class="error">
236
+ <md-icon>error</md-icon>
237
+ <span>${this.error}</span>
238
+ </div>
239
+ `;
240
+ }
241
+ return html `
242
+ <div class="header">
243
+ <div class="controls">
244
+ <md-outlined-text-field
245
+ label="그룹"
246
+ .value=${this.group}
247
+ @input=${(e) => (this.group = e.target.value)}
248
+ style="width: 150px;"
249
+ ></md-outlined-text-field>
250
+
251
+ <md-outlined-text-field
252
+ label="시작일"
253
+ type="date"
254
+ .value=${this.startDate}
255
+ @input=${(e) => (this.startDate = e.target.value)}
256
+ style="width: 150px;"
257
+ ></md-outlined-text-field>
258
+
259
+ <md-outlined-text-field
260
+ label="종료일"
261
+ type="date"
262
+ .value=${this.endDate}
263
+ @input=${(e) => (this.endDate = e.target.value)}
264
+ style="width: 150px;"
265
+ ></md-outlined-text-field>
266
+
267
+ <md-elevated-button @click=${this._loadData}>
268
+ <md-icon slot="icon">refresh</md-icon>
269
+ 새로고침
270
+ </md-elevated-button>
271
+ </div>
272
+ </div>
273
+
274
+ <div class="table-container">
275
+ <!-- 디버깅 정보 -->
276
+ <table style="border-collapse: collapse; width: 100%;">
277
+ <thead>
278
+ <tr>
279
+ <th
280
+ style="border: 1px solid #ccc; padding: 8px; background: #f5f5f5; min-width: 200px; left: 0; top: 0;"
281
+ class="kpi-header"
282
+ >
283
+ KPI명
284
+ </th>
285
+ ${this.dates.map(date => html `
286
+ <th
287
+ style="border: 1px solid #ccc; padding: 8px; background: #f5f5f5; min-width: 80px; position: sticky; top: 0;"
288
+ >
289
+ ${this._formatDate(date)}
290
+ </th>
291
+ `)}
292
+ </tr>
293
+ </thead>
294
+ <tbody>
295
+ ${this.kpis.map(kpi => html `
296
+ <tr>
297
+ <td
298
+ style="border: 1px solid #ccc; padding: 8px; background: #fff; min-width: 200px;"
299
+ class="kpi-name"
300
+ >
301
+ ${kpi.kpiName}
302
+ <span
303
+ style="font-size: 10px; padding: 2px 6px; background: #e0e0e0; border-radius: 4px; margin-left: 8px;"
304
+ >${kpi.periodType}</span
305
+ >
306
+ </td>
307
+ ${this.dates.map(date => html `
308
+ <td
309
+ style="border: 1px solid #ccc; padding: 8px; background: #e3f2fd; text-align: center; min-width: 80px; cursor: pointer;"
310
+ @click=${() => this._startEdit(kpi.kpiId, date)}
311
+ >
312
+ ${this._renderCellContent(kpi, date)}
313
+ </td>
314
+ `)}
315
+ </tr>
316
+ `)}
317
+ </tbody>
318
+ </table>
319
+ </div>
320
+
321
+ <div class="legend">
322
+ <div class="legend-item">
323
+ <div class="legend-color" style="background: var(--md-sys-color-primary-container);"></div>
324
+ <span>편집 가능</span>
325
+ </div>
326
+ <div class="legend-item">
327
+ <div class="legend-color" style="background: var(--md-sys-color-secondary-container);"></div>
328
+ <span>하이라이트 (PeriodType에 따라)</span>
329
+ </div>
330
+ <div class="legend-item">
331
+ <div class="legend-color" style="background: var(--md-sys-color-surface-container);"></div>
332
+ <span>편집 불가</span>
333
+ </div>
334
+ </div>
335
+ `;
336
+ }
337
+ _renderKpiCells(kpi) {
338
+ const cells = this._getCellsForKpi(kpi);
339
+ return cells.map(cell => {
340
+ const isEditing = this.editingCell?.kpiId === kpi.kpiId && this.editingCell?.date === cell.date;
341
+ const cellClass = this._getCellClass(cell);
342
+ return html `
343
+ <div class="cell ${cellClass}" @click=${() => this._startEdit(kpi.kpiId, cell.date)}>
344
+ ${isEditing
345
+ ? html `
346
+ <input
347
+ class="value-input"
348
+ type="number"
349
+ step="0.01"
350
+ .value=${(cell.value || '').toString()}
351
+ @blur=${(e) => this._finishEdit(kpi.kpiId, cell.date, parseFloat(e.target.value) || 0)}
352
+ @keydown=${(e) => e.key === 'Enter' && e.target.blur()}
353
+ autofocus
354
+ />
355
+ `
356
+ : html ` <span>${cell.value !== null ? cell.value.toLocaleString() : '-'}</span> `}
357
+ </div>
358
+ `;
359
+ });
360
+ }
361
+ _getCellsForKpi(kpi) {
362
+ const cells = [];
363
+ const periodType = kpi.periodType;
364
+ this.dates.forEach(date => {
365
+ const isEditable = this._isDateEditableForPeriodType(date, periodType);
366
+ const isHighlighted = this._isDateHighlightedForPeriodType(date, periodType);
367
+ cells.push({
368
+ date,
369
+ value: kpi.values[date]?.value || null,
370
+ score: kpi.values[date]?.score,
371
+ isEditable,
372
+ isHighlighted
373
+ });
374
+ });
375
+ return cells;
376
+ }
377
+ _isDateEditableForPeriodType(date, periodType) {
378
+ const targetDate = new Date(date);
379
+ switch (periodType) {
380
+ case 'DAY':
381
+ return true; // 모든 날짜 편집 가능
382
+ case 'WEEK':
383
+ // 해당 주의 첫 번째 날짜만 편집 가능
384
+ const weekStart = new Date(targetDate);
385
+ weekStart.setDate(targetDate.getDate() - targetDate.getDay());
386
+ return date === weekStart.toISOString().split('T')[0];
387
+ case 'MONTH':
388
+ // 해당 월의 첫 번째 날짜만 편집 가능
389
+ return targetDate.getDate() === 1;
390
+ case 'QUARTER':
391
+ // 해당 분기의 첫 번째 날짜만 편집 가능
392
+ const quarterStart = new Date(targetDate.getFullYear(), Math.floor(targetDate.getMonth() / 3) * 3, 1);
393
+ return date === quarterStart.toISOString().split('T')[0];
394
+ case 'YEAR':
395
+ // 해당 연도의 첫 번째 날짜만 편집 가능
396
+ const yearStart = new Date(targetDate.getFullYear(), 0, 1);
397
+ return date === yearStart.toISOString().split('T')[0];
398
+ default:
399
+ return true;
400
+ }
401
+ }
402
+ _isDateHighlightedForPeriodType(date, periodType) {
403
+ const targetDate = new Date(date);
404
+ switch (periodType) {
405
+ case 'DAY':
406
+ return false; // 하이라이트 없음
407
+ case 'WEEK':
408
+ // 해당 주의 모든 날짜 하이라이트
409
+ const weekStart = new Date(targetDate);
410
+ weekStart.setDate(targetDate.getDate() - targetDate.getDay());
411
+ const weekEnd = new Date(weekStart);
412
+ weekEnd.setDate(weekStart.getDate() + 6);
413
+ return date >= weekStart.toISOString().split('T')[0] && date <= weekEnd.toISOString().split('T')[0];
414
+ case 'MONTH':
415
+ // 해당 월의 모든 날짜 하이라이트
416
+ const monthStart = new Date(targetDate.getFullYear(), targetDate.getMonth(), 1);
417
+ const monthEnd = new Date(targetDate.getFullYear(), targetDate.getMonth() + 1, 0);
418
+ return date >= monthStart.toISOString().split('T')[0] && date <= monthEnd.toISOString().split('T')[0];
419
+ case 'QUARTER':
420
+ // 해당 분기의 모든 날짜 하이라이트
421
+ const quarter = Math.floor(targetDate.getMonth() / 3);
422
+ const quarterStart = new Date(targetDate.getFullYear(), quarter * 3, 1);
423
+ const quarterEnd = new Date(targetDate.getFullYear(), (quarter + 1) * 3, 0);
424
+ return date >= quarterStart.toISOString().split('T')[0] && date <= quarterEnd.toISOString().split('T')[0];
425
+ case 'YEAR':
426
+ // 해당 연도의 모든 날짜 하이라이트
427
+ const yearStart = new Date(targetDate.getFullYear(), 0, 1);
428
+ const yearEnd = new Date(targetDate.getFullYear(), 11, 31);
429
+ return date >= yearStart.toISOString().split('T')[0] && date <= yearEnd.toISOString().split('T')[0];
430
+ default:
431
+ return false;
432
+ }
433
+ }
434
+ _getCellClass(cell) {
435
+ if (!cell.isEditable) {
436
+ return 'disabled-cell';
437
+ }
438
+ if (cell.isHighlighted) {
439
+ return 'highlighted-cell';
440
+ }
441
+ if (cell.isEditable) {
442
+ return 'editable-cell';
443
+ }
444
+ return '';
445
+ }
446
+ _formatDate(date) {
447
+ const d = new Date(date);
448
+ return d.toLocaleDateString('ko-KR', { month: 'numeric', day: 'numeric' });
449
+ }
450
+ _startEdit(kpiId, date) {
451
+ this.editingCell = { kpiId, date };
452
+ }
453
+ _finishEdit(kpiId, date, value) {
454
+ const kpi = this.kpis.find(k => k.kpiId === kpiId);
455
+ if (kpi) {
456
+ if (!kpi.values[date]) {
457
+ kpi.values[date] = { value: 0, isDirty: false };
458
+ }
459
+ // 값이 변경되었는지 확인
460
+ const originalValue = this._findExistingValue(kpiId, date)?.value;
461
+ const isChanged = originalValue !== value;
462
+ kpi.values[date].value = value;
463
+ kpi.values[date].isDirty = isChanged;
464
+ // score는 저장 시 서버에서 계산되므로 여기서는 제거
465
+ delete kpi.values[date].score;
466
+ }
467
+ this.editingCell = null;
468
+ }
469
+ _renderCellContent(kpi, date) {
470
+ const isEditing = this.editingCell?.kpiId === kpi.kpiId && this.editingCell?.date === date;
471
+ const value = kpi.values[date]?.value;
472
+ const score = kpi.values[date]?.score;
473
+ if (isEditing) {
474
+ return html `
475
+ <input
476
+ type="number"
477
+ step="0.01"
478
+ .value=${(value || '').toString()}
479
+ @blur=${(e) => this._finishEdit(kpi.kpiId, date, parseFloat(e.target.value) || 0)}
480
+ @keydown=${(e) => e.key === 'Enter' && e.target.blur()}
481
+ style="width: 100%; text-align: center; border: none; background: transparent;"
482
+ autofocus
483
+ />
484
+ `;
485
+ }
486
+ return html `
487
+ <div style="display: flex; flex-direction: column; gap: 2px;">
488
+ <span style="font-weight: 500; color: ${value !== null && value !== undefined ? 'inherit' : '#999'};">
489
+ ${value !== null && value !== undefined ? value.toLocaleString() : '클릭하여 입력'}
490
+ </span>
491
+ ${score !== null && score !== undefined
492
+ ? html `<span style="font-size: 10px; color: #666;">Score: ${score.toFixed(3)}</span>`
493
+ : ''}
494
+ </div>
495
+ `;
496
+ }
497
+ _calculateScore(value, periodType) {
498
+ // 간단한 score 계산 로직 (0-1 범위)
499
+ // 실제로는 KPI의 formula나 기준값을 사용해야 함
500
+ if (value <= 0)
501
+ return 0;
502
+ if (value >= 1000)
503
+ return 1;
504
+ return Math.min(value / 1000, 1);
505
+ }
506
+ async _loadData() {
507
+ if (!this.startDate || !this.endDate) {
508
+ this.error = '시작일, 종료일을 모두 입력해주세요.';
509
+ return;
510
+ }
511
+ this.loading = true;
512
+ this.error = '';
513
+ try {
514
+ // KPI 목록 조회
515
+ const kpisResponse = await client.query({
516
+ query: gql `
517
+ query ($filters: [Filter!]) {
518
+ kpis(filters: $filters) {
519
+ items {
520
+ id
521
+ name
522
+ periodType
523
+ active
524
+ }
525
+ total
526
+ }
527
+ }
528
+ `,
529
+ variables: {
530
+ filters: [{ name: 'active', operator: 'eq', value: true }]
531
+ }
532
+ });
533
+ // KPI Value 데이터 조회
534
+ const valuesResponse = await client.query({
535
+ query: gql `
536
+ query ($filters: [Filter!]) {
537
+ kpiValues(filters: $filters) {
538
+ items {
539
+ id
540
+ kpiId
541
+ valueDate
542
+ value
543
+ score
544
+ group
545
+ }
546
+ total
547
+ }
548
+ }
549
+ `,
550
+ variables: {
551
+ filters: [
552
+ ...(this.group ? [{ name: 'group', operator: 'eq', value: this.group }] : []),
553
+ { name: 'valueDate', operator: 'between', value: [this.startDate, this.endDate] }
554
+ ]
555
+ }
556
+ });
557
+ // 날짜 배열 생성
558
+ this.dates = this._generateDateArray(this.startDate, this.endDate);
559
+ console.log('시작일:', this.startDate);
560
+ console.log('종료일:', this.endDate);
561
+ console.log('생성된 날짜 배열:', this.dates);
562
+ // KPI 목록을 기준으로 데이터 구성
563
+ this.kpis = kpisResponse.data.kpis.items.map((kpi) => ({
564
+ kpiId: kpi.id,
565
+ kpiName: kpi.name,
566
+ periodType: kpi.periodType,
567
+ values: {}
568
+ }));
569
+ // 기존 KPI Value 데이터 저장
570
+ this._existingValues = valuesResponse.data.kpiValues.items;
571
+ // KPI Value 데이터를 해당 KPI에 매핑
572
+ valuesResponse.data.kpiValues.items.forEach((value) => {
573
+ const kpi = this.kpis.find(k => k.kpiId === value.kpiId);
574
+ if (kpi) {
575
+ kpi.values[value.valueDate] = {
576
+ value: value.value,
577
+ score: value.score,
578
+ isDirty: false
579
+ };
580
+ }
581
+ });
582
+ // KPI Value가 없는 KPI도 빈 값으로 초기화
583
+ this.kpis.forEach(kpi => {
584
+ this.dates.forEach(date => {
585
+ if (!kpi.values[date]) {
586
+ kpi.values[date] = { value: null, score: undefined, isDirty: false };
587
+ }
588
+ });
589
+ });
590
+ // 디버깅 정보 출력
591
+ console.log('KPI 총 개수:', kpisResponse.data.kpis.total);
592
+ console.log('KPI 목록:', kpisResponse.data.kpis.items);
593
+ console.log('KPI Value 총 개수:', valuesResponse.data.kpiValues.total);
594
+ console.log('KPI Value 목록:', valuesResponse.data.kpiValues.items);
595
+ console.log('구성된 KPI 데이터:', this.kpis);
596
+ console.log('날짜 배열:', this.dates);
597
+ // 강제 업데이트
598
+ this.requestUpdate();
599
+ }
600
+ catch (error) {
601
+ console.error('데이터 로드 중 오류:', error);
602
+ this.error = '데이터를 불러오는 중 오류가 발생했습니다.';
603
+ }
604
+ finally {
605
+ this.loading = false;
606
+ }
607
+ }
608
+ _generateDateArray(startDate, endDate) {
609
+ const dates = [];
610
+ const start = new Date(startDate);
611
+ const end = new Date(endDate);
612
+ for (let d = new Date(start); d <= end; d.setDate(d.getDate() + 1)) {
613
+ dates.push(d.toLocaleDateString('sv-SE'));
614
+ }
615
+ return dates;
616
+ }
617
+ async _saveValues() {
618
+ try {
619
+ const patches = [];
620
+ this.kpis.forEach(kpi => {
621
+ Object.entries(kpi.values).forEach(([date, data]) => {
622
+ // dirty 상태인 데이터만 처리
623
+ if (data.isDirty && data.value !== null && data.value !== undefined) {
624
+ const existingValue = this._findExistingValue(kpi.kpiId, date);
625
+ if (existingValue) {
626
+ // 기존 데이터 업데이트
627
+ patches.push({
628
+ id: existingValue.id,
629
+ value: data.value,
630
+ group: this.group,
631
+ cuFlag: 'M'
632
+ });
633
+ }
634
+ else {
635
+ // 새로운 데이터 생성
636
+ patches.push({
637
+ kpiId: kpi.kpiId,
638
+ valueDate: date,
639
+ value: data.value,
640
+ group: this.group,
641
+ cuFlag: '+'
642
+ });
643
+ }
644
+ }
645
+ });
646
+ });
647
+ if (patches.length === 0) {
648
+ notify({ message: '저장할 데이터가 없습니다.' });
649
+ return;
650
+ }
651
+ // 단일 mutation으로 생성과 업데이트 처리
652
+ const response = await client.mutate({
653
+ mutation: gql `
654
+ mutation ($patches: [KpiValuePatch!]!) {
655
+ updateMultipleKpiValue(patches: $patches) {
656
+ id
657
+ value
658
+ score
659
+ valueDate
660
+ kpiId
661
+ }
662
+ }
663
+ `,
664
+ variables: { patches }
665
+ });
666
+ if (!response.errors) {
667
+ // 서버에서 계산된 score로 UI 업데이트
668
+ response.data.updateMultipleKpiValue.forEach((savedValue) => {
669
+ const kpi = this.kpis.find(k => k.kpiId === savedValue.kpiId);
670
+ if (kpi && kpi.values[savedValue.valueDate]) {
671
+ kpi.values[savedValue.valueDate].score = savedValue.score;
672
+ kpi.values[savedValue.valueDate].isDirty = false; // 저장 후 dirty 상태 해제
673
+ }
674
+ });
675
+ notify({ message: 'KPI 값이 성공적으로 저장되었습니다.' });
676
+ this.requestUpdate(); // UI 강제 업데이트
677
+ }
678
+ }
679
+ catch (error) {
680
+ console.error('저장 중 오류:', error);
681
+ notify({ message: '저장 중 오류가 발생했습니다.' });
682
+ }
683
+ }
684
+ _findExistingValue(kpiId, date) {
685
+ // 기존 로드된 KPI Value 데이터에서 찾기
686
+ return this._existingValues?.find((v) => v.kpiId === kpiId && v.valueDate === date);
687
+ }
688
+ _cancel() {
689
+ // 편집 취소 로직
690
+ this.editingCell = null;
691
+ this._loadData(); // 원본 데이터로 복원
692
+ }
693
+ async pageInitialized(lifecycle) {
694
+ // 기본값 설정 - 지난 1개월
695
+ if (!this.startDate) {
696
+ const today = new Date();
697
+ const oneMonthAgo = new Date(today.getFullYear(), today.getMonth() - 1, today.getDate());
698
+ this.startDate = oneMonthAgo.toLocaleDateString('sv-SE');
699
+ }
700
+ if (!this.endDate) {
701
+ const today = new Date();
702
+ this.endDate = today.toLocaleDateString('sv-SE');
703
+ }
704
+ // 페이지 초기화 시 자동으로 데이터 로드
705
+ await this._loadData();
706
+ }
707
+ };
708
+ __decorate([
709
+ property({ type: String }),
710
+ __metadata("design:type", String)
711
+ ], KpiValueEditorPage.prototype, "group", void 0);
712
+ __decorate([
713
+ property({ type: String }),
714
+ __metadata("design:type", String)
715
+ ], KpiValueEditorPage.prototype, "startDate", void 0);
716
+ __decorate([
717
+ property({ type: String }),
718
+ __metadata("design:type", String)
719
+ ], KpiValueEditorPage.prototype, "endDate", void 0);
720
+ __decorate([
721
+ state(),
722
+ __metadata("design:type", Array)
723
+ ], KpiValueEditorPage.prototype, "kpis", void 0);
724
+ __decorate([
725
+ state(),
726
+ __metadata("design:type", Array)
727
+ ], KpiValueEditorPage.prototype, "dates", void 0);
728
+ __decorate([
729
+ state(),
730
+ __metadata("design:type", Boolean)
731
+ ], KpiValueEditorPage.prototype, "loading", void 0);
732
+ __decorate([
733
+ state(),
734
+ __metadata("design:type", String)
735
+ ], KpiValueEditorPage.prototype, "error", void 0);
736
+ __decorate([
737
+ state(),
738
+ __metadata("design:type", Object)
739
+ ], KpiValueEditorPage.prototype, "editingCell", void 0);
740
+ __decorate([
741
+ state(),
742
+ __metadata("design:type", Array)
743
+ ], KpiValueEditorPage.prototype, "_existingValues", void 0);
744
+ KpiValueEditorPage = __decorate([
745
+ customElement('kpi-value-editor-page')
746
+ ], KpiValueEditorPage);
747
+ export { KpiValueEditorPage };
748
+ //# sourceMappingURL=kpi-value-editor-page.js.map