@things-factory/kpi 9.0.15 → 9.0.17

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 (28) hide show
  1. package/client/pages/kpi/kpi-grade-editor.ts +80 -207
  2. package/client/pages/kpi/kpi-list-page.ts +53 -11
  3. package/client/pages/kpi/kpi-viz-editor.ts +353 -0
  4. package/client/pages/kpi-dashboard/kpi-dashboard.ts +128 -1
  5. package/dist-client/pages/kpi/kpi-grade-editor.d.ts +13 -13
  6. package/dist-client/pages/kpi/kpi-grade-editor.js +84 -197
  7. package/dist-client/pages/kpi/kpi-grade-editor.js.map +1 -1
  8. package/dist-client/pages/kpi/kpi-list-page.d.ts +6 -2
  9. package/dist-client/pages/kpi/kpi-list-page.js +50 -10
  10. package/dist-client/pages/kpi/kpi-list-page.js.map +1 -1
  11. package/dist-client/pages/kpi/kpi-viz-editor.d.ts +20 -0
  12. package/dist-client/pages/kpi/kpi-viz-editor.js +364 -0
  13. package/dist-client/pages/kpi/kpi-viz-editor.js.map +1 -0
  14. package/dist-client/pages/kpi-dashboard/kpi-dashboard.d.ts +2 -0
  15. package/dist-client/pages/kpi-dashboard/kpi-dashboard.js +117 -1
  16. package/dist-client/pages/kpi-dashboard/kpi-dashboard.js.map +1 -1
  17. package/dist-client/tsconfig.tsbuildinfo +1 -1
  18. package/dist-server/service/index.d.ts +2 -2
  19. package/dist-server/service/kpi/kpi-type.d.ts +3 -3
  20. package/dist-server/service/kpi/kpi-type.js +4 -4
  21. package/dist-server/service/kpi/kpi-type.js.map +1 -1
  22. package/dist-server/service/kpi/kpi.d.ts +18 -1
  23. package/dist-server/service/kpi/kpi.js +26 -4
  24. package/dist-server/service/kpi/kpi.js.map +1 -1
  25. package/dist-server/tsconfig.tsbuildinfo +1 -1
  26. package/package.json +2 -2
  27. package/server/service/kpi/kpi-type.ts +7 -7
  28. package/server/service/kpi/kpi.ts +27 -4
@@ -2,261 +2,144 @@ import { __decorate, __metadata } from "tslib";
2
2
  import '@material/web/button/elevated-button.js';
3
3
  import '@material/web/button/filled-button.js';
4
4
  import '@material/web/button/text-button.js';
5
- import '@material/web/textfield/outlined-text-field.js';
6
- import '@material/web/select/outlined-select.js';
7
- import '@material/web/select/select-option.js';
8
5
  import '@material/web/icon/icon.js';
6
+ import '@operato/data-grist/ox-grist.js';
7
+ import deepEquals from 'lodash-es/isEqual';
8
+ import gql from 'graphql-tag';
9
9
  import { LitElement, css, html } from 'lit';
10
- import { customElement, property, state } from 'lit/decorators.js';
11
- import { ScopedElementsMixin } from '@open-wc/scoped-elements';
10
+ import { customElement, property, state, query } from 'lit/decorators.js';
11
+ import { DataGrist } from '@operato/data-grist/ox-grist.js';
12
+ import { i18next, localize } from '@operato/i18n';
13
+ import { client } from '@operato/graphql';
12
14
  import { notify } from '@operato/layout';
13
- import { CommonHeaderStyles, ScrollbarStyles } from '@operato/styles';
14
- let KpiGradeEditor = class KpiGradeEditor extends ScopedElementsMixin(LitElement) {
15
+ import { CommonHeaderStyles } from '@operato/styles';
16
+ let KpiGradeEditor = class KpiGradeEditor extends localize(i18next)(LitElement) {
15
17
  constructor() {
16
18
  super(...arguments);
17
- this.grades = [];
18
- this.isDirty = false;
19
+ this.kpi = { grades: [] };
20
+ this.grades = this.kpi?.grades || [];
21
+ this.gristConfig = null;
19
22
  }
20
23
  static { this.styles = [
21
24
  CommonHeaderStyles,
22
- ScrollbarStyles,
23
25
  css `
24
26
  :host {
25
27
  display: flex;
26
28
  flex-direction: column;
27
- background-color: var(--md-sys-color-surface, #f4f6fa);
28
- }
29
-
30
- .grade-list {
31
- flex: 1;
32
- margin-bottom: 20px;
33
- overflow-y: auto;
34
- }
35
-
36
- .grade-item {
37
- display: flex;
38
- align-items: center;
39
- gap: 10px;
40
- padding: 10px;
41
- border: 1px solid #ddd;
42
- border-radius: 4px;
43
- margin-bottom: 10px;
44
- background: #f9f9f9;
45
- }
46
29
 
47
- .grade-item:hover {
48
- background: #f0f0f0;
30
+ background-color: var(--md-sys-color-surface);
49
31
  }
50
32
 
51
- .grade-inputs {
52
- display: flex;
53
- gap: 10px;
33
+ ox-grist {
54
34
  flex: 1;
55
35
  }
56
-
57
- .grade-inputs md-outlined-text-field {
58
- flex: 1;
59
- }
60
-
61
- .grade-actions {
62
- display: flex;
63
- gap: 5px;
64
- }
65
-
66
- .footer span {
67
- font-size: 0.8em;
68
- color: var(--md-sys-color-on-surface);
69
- line-height: 1.5;
70
- padding: 10px;
71
- }
72
36
  `
73
37
  ]; }
74
- connectedCallback() {
75
- super.connectedCallback();
38
+ async firstUpdated() {
76
39
  if (this.kpi?.grades) {
77
40
  this.grades = [...this.kpi.grades];
78
41
  }
42
+ this.gristConfig = {
43
+ list: { fields: ['name', 'minValue', 'maxValue', 'score', 'color', 'description'] },
44
+ columns: [
45
+ { type: 'gutter', gutterName: 'row-selector', multiple: true, fixed: true },
46
+ { type: 'gutter', gutterName: 'sequence', fixed: true },
47
+ { type: 'gutter', gutterName: 'button', fixed: true, icon: 'add', handlers: { click: 'record-copy' } },
48
+ { type: 'gutter', gutterName: 'button', fixed: true, icon: 'arrow_upward', handlers: { click: 'move-up' } },
49
+ { type: 'gutter', gutterName: 'button', fixed: true, icon: 'arrow_downward', handlers: { click: 'move-down' } },
50
+ { type: 'string', name: 'name', header: '등급명', record: { editable: true }, width: 100 },
51
+ { type: 'number', name: 'minValue', header: '최소값', record: { editable: true }, width: 100 },
52
+ { type: 'number', name: 'maxValue', header: '최대값', record: { editable: true }, width: 100 },
53
+ { type: 'number', name: 'score', header: '점수', record: { editable: true }, width: 80 },
54
+ { type: 'color', name: 'color', header: '색상', record: { editable: true }, width: 100 },
55
+ { type: 'string', name: 'description', header: '설명', record: { editable: true }, width: 200 }
56
+ ],
57
+ rows: { selectable: { multiple: true } },
58
+ pagination: { infinite: true }
59
+ };
79
60
  }
80
61
  render() {
81
62
  return html `
82
- <div class="grade-list">${this.grades.map((grade, index) => this._renderGradeItem(grade, index))}</div>
83
-
63
+ <ox-grist .mode=${'GRID'} .config=${this.gristConfig} .fetchHandler=${this.fetchHandler.bind(this)}></ox-grist>
84
64
  <div class="footer">
85
- <button type="button" @click=${this._addGrade}><md-icon>add</md-icon>등급 추가</button>
86
65
  <div filler></div>
87
- <button type="button" @click=${this._cancel}><md-icon>cancel</md-icon>취소</button>
88
- <button type="button" @click=${this._save} ?disabled=${!this.isDirty} done><md-icon>save</md-icon>저장</button>
89
- </div>
90
- `;
91
- }
92
- _renderGradeItem(grade, index) {
93
- return html `
94
- <div class="grade-item">
95
- <div class="grade-inputs">
96
- <md-outlined-text-field
97
- label="등급명"
98
- value=${grade.name}
99
- @input=${(e) => this._updateGrade(index, 'name', e.target.value)}
100
- ></md-outlined-text-field>
101
-
102
- <md-outlined-text-field
103
- label="최소값"
104
- type="number"
105
- value=${grade.minValue}
106
- @input=${(e) => this._updateGrade(index, 'minValue', parseFloat(e.target.value))}
107
- ></md-outlined-text-field>
108
-
109
- <md-outlined-text-field
110
- label="최대값"
111
- type="number"
112
- value=${grade.maxValue}
113
- @input=${(e) => this._updateGrade(index, 'maxValue', parseFloat(e.target.value))}
114
- ></md-outlined-text-field>
115
-
116
- <md-outlined-text-field
117
- label="점수"
118
- type="number"
119
- value=${grade.score || ''}
120
- @input=${(e) => this._updateGrade(index, 'score', parseFloat(e.target.value))}
121
- ></md-outlined-text-field>
122
-
123
- <md-outlined-text-field
124
- label="색상"
125
- value=${grade.color || ''}
126
- @input=${(e) => this._updateGrade(index, 'color', e.target.value)}
127
- ></md-outlined-text-field>
128
- </div>
129
-
130
- <div class="grade-actions">
131
- <md-icon-button @click=${() => this._removeGrade(index)}>
132
- <md-icon>delete</md-icon>
133
- </md-icon-button>
134
- </div>
66
+ <button danger @click=${this._deleteGrades.bind(this)}>
67
+ <md-icon>delete</md-icon>${i18next.t('button.delete')}
68
+ </button>
69
+ <button done type="button" @click=${this._updateGrades}><md-icon>save</md-icon>저장</button>
135
70
  </div>
136
71
  `;
137
72
  }
138
- _updateGrade(index, field, value) {
139
- this.grades[index] = { ...this.grades[index], [field]: value };
140
- this.isDirty = true;
141
- this.requestUpdate();
142
- }
143
- _addGrade() {
144
- const newGrade = {
145
- name: '',
146
- minValue: 0,
147
- maxValue: 0,
148
- score: 0,
149
- color: '#4caf50',
150
- description: ''
73
+ async fetchHandler({ page, limit, sorters = [] }) {
74
+ return {
75
+ total: this.grades.length,
76
+ records: this.grades
151
77
  };
152
- this.grades.push(newGrade);
153
- this.isDirty = true;
154
- this.requestUpdate();
155
- }
156
- _removeGrade(index) {
157
- this.grades.splice(index, 1);
158
- this.isDirty = true;
159
- this.requestUpdate();
160
78
  }
161
- _loadTemplate(template) {
162
- switch (template) {
163
- case '5grade':
164
- this.grades = [
165
- { name: 'A', minValue: 0, maxValue: 1.5, score: 95, color: '#4caf50', description: '우수' },
166
- { name: 'B', minValue: 1.5, maxValue: 2.5, score: 85, color: '#ff9800', description: '양호' },
167
- { name: 'C', minValue: 2.5, maxValue: 3.5, score: 75, color: '#ffc107', description: '보통' },
168
- { name: 'D', minValue: 3.5, maxValue: 4.5, score: 65, color: '#ff9800', description: '미흡' },
169
- { name: 'E', minValue: 4.5, maxValue: 999, score: 55, color: '#f44336', description: '불량' }
170
- ];
171
- break;
172
- case '3grade':
173
- this.grades = [
174
- { name: '우수', minValue: 0, maxValue: 1.5, score: 95, color: '#4caf50', description: '목표 달성' },
175
- { name: '양호', minValue: 1.5, maxValue: 2.5, score: 85, color: '#ff9800', description: '기준 달성' },
176
- { name: '미흡', minValue: 2.5, maxValue: 999, score: 75, color: '#f44336', description: '개선 필요' }
177
- ];
178
- break;
179
- case 'continuous':
180
- this.grades = [
181
- { name: '0.999999', minValue: 0, maxValue: 0.025, score: 0.999999, color: '#4caf50' },
182
- { name: '0.944189368', minValue: 0.025, maxValue: 0.05, score: 0.944189368, color: '#4caf50' },
183
- { name: '0.888379735', minValue: 0.05, maxValue: 0.075, score: 0.888379735, color: '#4caf50' }
184
- ];
185
- break;
186
- case 'clear':
187
- this.grades = [];
188
- break;
79
+ async _updateGrades() {
80
+ this.grades = this.grist.dirtyData.records
81
+ .map(patch => {
82
+ const { name, minValue, maxValue, score, color, description } = patch;
83
+ return { name, minValue, maxValue, score, color, description };
84
+ })
85
+ .sort((a, b) => a.minValue - b.minValue);
86
+ if (!this._validateGrades()) {
87
+ return;
189
88
  }
190
- this.isDirty = true;
191
- this.requestUpdate();
192
- }
193
- async _save() {
194
- try {
195
- // 등급 유효성 검사
196
- if (!this._validateGrades()) {
197
- return;
89
+ if (!deepEquals(this.kpi?.grades, this.grades)) {
90
+ try {
91
+ const response = await client.mutate({
92
+ mutation: gql `
93
+ mutation ($id: String!, $patch: KpiPatch!) {
94
+ updateKpi(id: $id, patch: $patch) {
95
+ id
96
+ name
97
+ grades
98
+ }
99
+ }
100
+ `,
101
+ variables: {
102
+ id: this.kpi.id,
103
+ patch: { grades: this.grades }
104
+ }
105
+ });
106
+ this.grades = response.data.updateKpi.grades;
107
+ this.grist.fetch();
108
+ }
109
+ catch (error) {
110
+ notify({ message: '등급 저장 중 오류가 발생했습니다.' });
198
111
  }
199
- // 정렬 (minValue 기준)
200
- this.grades.sort((a, b) => a.minValue - b.minValue);
201
- this.dispatchEvent(new CustomEvent('grades-updated', {
202
- detail: {
203
- kpiId: this.kpi.id,
204
- grades: this.grades
205
- }
206
- }));
207
- this.isDirty = false;
208
- }
209
- catch (error) {
210
- notify({
211
- message: '등급 저장 중 오류가 발생했습니다.'
212
- });
213
112
  }
214
113
  }
215
114
  _validateGrades() {
216
- // 최소 1개 등급 필요
217
115
  if (this.grades.length === 0) {
218
- notify({
219
- message: '최소 1개 이상의 등급을 설정해야 합니다.'
220
- });
116
+ notify({ message: '최소 1개 이상의 등급을 설정해야 합니다.' });
221
117
  return false;
222
118
  }
223
- // 등급명 중복 체크
224
119
  const names = this.grades.map(g => g.name);
225
120
  const uniqueNames = new Set(names);
226
121
  if (names.length !== uniqueNames.size) {
227
- notify({
228
- message: '등급명이 중복되었습니다.'
229
- });
122
+ notify({ message: '등급명이 중복되었습니다.' });
230
123
  return false;
231
124
  }
232
- // 값 범위 체크
233
125
  for (let i = 0; i < this.grades.length; i++) {
234
126
  const grade = this.grades[i];
235
127
  if (grade.minValue >= grade.maxValue) {
236
- notify({
237
- message: `등급 "${grade.name}"의 최소값이 최대값보다 크거나 같습니다.`
238
- });
128
+ notify({ message: `등급 "${grade.name}"의 최소값이 최대값보다 크거나 같습니다.` });
239
129
  return false;
240
130
  }
241
- // 연속성 체크
242
131
  if (i > 0) {
243
132
  const prevGrade = this.grades[i - 1];
244
133
  if (prevGrade.maxValue !== grade.minValue) {
245
- notify({
246
- message: `등급 "${prevGrade.name}"과 "${grade.name}" 사이에 간격이 있습니다.`
247
- });
134
+ notify({ message: `등급 "${prevGrade.name}"과 "${grade.name}" 사이에 간격이 있습니다.` });
248
135
  return false;
249
136
  }
250
137
  }
251
138
  }
252
139
  return true;
253
140
  }
254
- _cancel() {
255
- // 팝업 닫기
256
- const popup = this.closest('ox-popup');
257
- if (popup && popup.close) {
258
- popup.close();
259
- }
141
+ async _deleteGrades() {
142
+ this.grist.deleteSelectedRecords(true);
260
143
  }
261
144
  };
262
145
  __decorate([
@@ -270,7 +153,11 @@ __decorate([
270
153
  __decorate([
271
154
  state(),
272
155
  __metadata("design:type", Object)
273
- ], KpiGradeEditor.prototype, "isDirty", void 0);
156
+ ], KpiGradeEditor.prototype, "gristConfig", void 0);
157
+ __decorate([
158
+ query('ox-grist'),
159
+ __metadata("design:type", DataGrist)
160
+ ], KpiGradeEditor.prototype, "grist", void 0);
274
161
  KpiGradeEditor = __decorate([
275
162
  customElement('kpi-grade-editor')
276
163
  ], KpiGradeEditor);
@@ -1 +1 @@
1
- {"version":3,"file":"kpi-grade-editor.js","sourceRoot":"","sources":["../../../client/pages/kpi/kpi-grade-editor.ts"],"names":[],"mappings":"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","sourcesContent":["import '@material/web/button/elevated-button.js'\nimport '@material/web/button/filled-button.js'\nimport '@material/web/button/text-button.js'\nimport '@material/web/textfield/outlined-text-field.js'\nimport '@material/web/select/outlined-select.js'\nimport '@material/web/select/select-option.js'\nimport '@material/web/icon/icon.js'\n\nimport { LitElement, css, html } from 'lit'\nimport { customElement, property, state } from 'lit/decorators.js'\nimport { ScopedElementsMixin } from '@open-wc/scoped-elements'\nimport { client } from '@operato/graphql'\nimport { notify } from '@operato/layout'\nimport gql from 'graphql-tag'\nimport { CommonButtonStyles, CommonHeaderStyles, CommonGristStyles, ScrollbarStyles } from '@operato/styles'\n\n// KPI 등급 타입 정의 (서버와 동일한 구조)\ninterface KpiGrade {\n name: string\n minValue: number\n maxValue: number\n score?: number\n color?: string\n description?: string\n}\n\ntype KpiGrades = KpiGrade[]\n\n@customElement('kpi-grade-editor')\nexport class KpiGradeEditor extends ScopedElementsMixin(LitElement) {\n static styles = [\n CommonHeaderStyles,\n ScrollbarStyles,\n css`\n :host {\n display: flex;\n flex-direction: column;\n background-color: var(--md-sys-color-surface, #f4f6fa);\n }\n\n .grade-list {\n flex: 1;\n margin-bottom: 20px;\n overflow-y: auto;\n }\n\n .grade-item {\n display: flex;\n align-items: center;\n gap: 10px;\n padding: 10px;\n border: 1px solid #ddd;\n border-radius: 4px;\n margin-bottom: 10px;\n background: #f9f9f9;\n }\n\n .grade-item:hover {\n background: #f0f0f0;\n }\n\n .grade-inputs {\n display: flex;\n gap: 10px;\n flex: 1;\n }\n\n .grade-inputs md-outlined-text-field {\n flex: 1;\n }\n\n .grade-actions {\n display: flex;\n gap: 5px;\n }\n\n .footer span {\n font-size: 0.8em;\n color: var(--md-sys-color-on-surface);\n line-height: 1.5;\n padding: 10px;\n }\n `\n ]\n\n @property({ type: Object }) kpi: any\n @state() grades: KpiGrades = []\n @state() isDirty = false\n\n connectedCallback() {\n super.connectedCallback()\n if (this.kpi?.grades) {\n this.grades = [...this.kpi.grades]\n }\n }\n\n render() {\n return html`\n <div class=\"grade-list\">${this.grades.map((grade, index) => this._renderGradeItem(grade, index))}</div>\n\n <div class=\"footer\">\n <button type=\"button\" @click=${this._addGrade}><md-icon>add</md-icon>등급 추가</button>\n <div filler></div>\n <button type=\"button\" @click=${this._cancel}><md-icon>cancel</md-icon>취소</button>\n <button type=\"button\" @click=${this._save} ?disabled=${!this.isDirty} done><md-icon>save</md-icon>저장</button>\n </div>\n `\n }\n\n _renderGradeItem(grade: KpiGrade, index: number) {\n return html`\n <div class=\"grade-item\">\n <div class=\"grade-inputs\">\n <md-outlined-text-field\n label=\"등급명\"\n value=${grade.name}\n @input=${(e: any) => this._updateGrade(index, 'name', e.target.value)}\n ></md-outlined-text-field>\n\n <md-outlined-text-field\n label=\"최소값\"\n type=\"number\"\n value=${grade.minValue}\n @input=${(e: any) => this._updateGrade(index, 'minValue', parseFloat(e.target.value))}\n ></md-outlined-text-field>\n\n <md-outlined-text-field\n label=\"최대값\"\n type=\"number\"\n value=${grade.maxValue}\n @input=${(e: any) => this._updateGrade(index, 'maxValue', parseFloat(e.target.value))}\n ></md-outlined-text-field>\n\n <md-outlined-text-field\n label=\"점수\"\n type=\"number\"\n value=${grade.score || ''}\n @input=${(e: any) => this._updateGrade(index, 'score', parseFloat(e.target.value))}\n ></md-outlined-text-field>\n\n <md-outlined-text-field\n label=\"색상\"\n value=${grade.color || ''}\n @input=${(e: any) => this._updateGrade(index, 'color', e.target.value)}\n ></md-outlined-text-field>\n </div>\n\n <div class=\"grade-actions\">\n <md-icon-button @click=${() => this._removeGrade(index)}>\n <md-icon>delete</md-icon>\n </md-icon-button>\n </div>\n </div>\n `\n }\n\n _updateGrade(index: number, field: keyof KpiGrade, value: any) {\n this.grades[index] = { ...this.grades[index], [field]: value }\n this.isDirty = true\n this.requestUpdate()\n }\n\n _addGrade() {\n const newGrade: KpiGrade = {\n name: '',\n minValue: 0,\n maxValue: 0,\n score: 0,\n color: '#4caf50',\n description: ''\n }\n this.grades.push(newGrade)\n this.isDirty = true\n this.requestUpdate()\n }\n\n _removeGrade(index: number) {\n this.grades.splice(index, 1)\n this.isDirty = true\n this.requestUpdate()\n }\n\n _loadTemplate(template: string) {\n switch (template) {\n case '5grade':\n this.grades = [\n { name: 'A', minValue: 0, maxValue: 1.5, score: 95, color: '#4caf50', description: '우수' },\n { name: 'B', minValue: 1.5, maxValue: 2.5, score: 85, color: '#ff9800', description: '양호' },\n { name: 'C', minValue: 2.5, maxValue: 3.5, score: 75, color: '#ffc107', description: '보통' },\n { name: 'D', minValue: 3.5, maxValue: 4.5, score: 65, color: '#ff9800', description: '미흡' },\n { name: 'E', minValue: 4.5, maxValue: 999, score: 55, color: '#f44336', description: '불량' }\n ]\n break\n case '3grade':\n this.grades = [\n { name: '우수', minValue: 0, maxValue: 1.5, score: 95, color: '#4caf50', description: '목표 달성' },\n { name: '양호', minValue: 1.5, maxValue: 2.5, score: 85, color: '#ff9800', description: '기준 달성' },\n { name: '미흡', minValue: 2.5, maxValue: 999, score: 75, color: '#f44336', description: '개선 필요' }\n ]\n break\n case 'continuous':\n this.grades = [\n { name: '0.999999', minValue: 0, maxValue: 0.025, score: 0.999999, color: '#4caf50' },\n { name: '0.944189368', minValue: 0.025, maxValue: 0.05, score: 0.944189368, color: '#4caf50' },\n { name: '0.888379735', minValue: 0.05, maxValue: 0.075, score: 0.888379735, color: '#4caf50' }\n ]\n break\n case 'clear':\n this.grades = []\n break\n }\n this.isDirty = true\n this.requestUpdate()\n }\n\n async _save() {\n try {\n // 등급 유효성 검사\n if (!this._validateGrades()) {\n return\n }\n\n // 정렬 (minValue 기준)\n this.grades.sort((a, b) => a.minValue - b.minValue)\n\n this.dispatchEvent(\n new CustomEvent('grades-updated', {\n detail: {\n kpiId: this.kpi.id,\n grades: this.grades\n }\n })\n )\n\n this.isDirty = false\n } catch (error) {\n notify({\n message: '등급 저장 중 오류가 발생했습니다.'\n })\n }\n }\n\n _validateGrades(): boolean {\n // 최소 1개 등급 필요\n if (this.grades.length === 0) {\n notify({\n message: '최소 1개 이상의 등급을 설정해야 합니다.'\n })\n return false\n }\n\n // 등급명 중복 체크\n const names = this.grades.map(g => g.name)\n const uniqueNames = new Set(names)\n if (names.length !== uniqueNames.size) {\n notify({\n message: '등급명이 중복되었습니다.'\n })\n return false\n }\n\n // 값 범위 체크\n for (let i = 0; i < this.grades.length; i++) {\n const grade = this.grades[i]\n if (grade.minValue >= grade.maxValue) {\n notify({\n message: `등급 \"${grade.name}\"의 최소값이 최대값보다 크거나 같습니다.`\n })\n return false\n }\n\n // 연속성 체크\n if (i > 0) {\n const prevGrade = this.grades[i - 1]\n if (prevGrade.maxValue !== grade.minValue) {\n notify({\n message: `등급 \"${prevGrade.name}\"과 \"${grade.name}\" 사이에 간격이 있습니다.`\n })\n return false\n }\n }\n }\n\n return true\n }\n\n _cancel() {\n // 팝업 닫기\n const popup = this.closest('ox-popup') as any\n if (popup && popup.close) {\n popup.close()\n }\n }\n}\n"]}
1
+ {"version":3,"file":"kpi-grade-editor.js","sourceRoot":"","sources":["../../../client/pages/kpi/kpi-grade-editor.ts"],"names":[],"mappings":";AAAA,OAAO,yCAAyC,CAAA;AAChD,OAAO,uCAAuC,CAAA;AAC9C,OAAO,qCAAqC,CAAA;AAC5C,OAAO,4BAA4B,CAAA;AACnC,OAAO,iCAAiC,CAAA;AACxC,OAAO,UAAU,MAAM,mBAAmB,CAAA;AAE1C,OAAO,GAAG,MAAM,aAAa,CAAA;AAC7B,OAAO,EAAE,UAAU,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,KAAK,CAAA;AAC3C,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAA;AACzE,OAAO,EAAE,SAAS,EAAE,MAAM,iCAAiC,CAAA;AAC3D,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAA;AACjD,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAA;AACzC,OAAO,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAA;AAExC,OAAO,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAA;AAc7C,IAAM,cAAc,GAApB,MAAM,cAAe,SAAQ,QAAQ,CAAC,OAAO,CAAC,CAAC,UAAU,CAAC;IAA1D;;QAiBuB,QAAG,GAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,CAAA;QAE5C,WAAM,GAAc,IAAI,CAAC,GAAG,EAAE,MAAM,IAAI,EAAE,CAAA;QAC1C,gBAAW,GAAQ,IAAI,CAAA;IAqHlC,CAAC;aAxIQ,WAAM,GAAG;QACd,kBAAkB;QAClB,GAAG,CAAA;;;;;;;;;;;KAWF;KACF,AAdY,CAcZ;IAQD,KAAK,CAAC,YAAY;QAChB,IAAI,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,CAAC;YACrB,IAAI,CAAC,MAAM,GAAG,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,CAAA;QACpC,CAAC;QAED,IAAI,CAAC,WAAW,GAAG;YACjB,IAAI,EAAE,EAAE,MAAM,EAAE,CAAC,MAAM,EAAE,UAAU,EAAE,UAAU,EAAE,OAAO,EAAE,OAAO,EAAE,aAAa,CAAC,EAAE;YACnF,OAAO,EAAE;gBACP,EAAE,IAAI,EAAE,QAAQ,EAAE,UAAU,EAAE,cAAc,EAAE,QAAQ,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE;gBAC3E,EAAE,IAAI,EAAE,QAAQ,EAAE,UAAU,EAAE,UAAU,EAAE,KAAK,EAAE,IAAI,EAAE;gBACvD,EAAE,IAAI,EAAE,QAAQ,EAAE,UAAU,EAAE,QAAQ,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,KAAK,EAAE,aAAa,EAAE,EAAE;gBACtG,EAAE,IAAI,EAAE,QAAQ,EAAE,UAAU,EAAE,QAAQ,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,cAAc,EAAE,QAAQ,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,EAAE;gBAC3G,EAAE,IAAI,EAAE,QAAQ,EAAE,UAAU,EAAE,QAAQ,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,gBAAgB,EAAE,QAAQ,EAAE,EAAE,KAAK,EAAE,WAAW,EAAE,EAAE;gBAC/G,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE;gBACvF,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE;gBAC3F,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE;gBAC3F,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE;gBACtF,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE;gBACtF,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,aAAa,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE;aAC9F;YACD,IAAI,EAAE,EAAE,UAAU,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,EAAE;YACxC,UAAU,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE;SAC/B,CAAA;IACH,CAAC;IAED,MAAM;QACJ,OAAO,IAAI,CAAA;wBACS,MAAM,YAAY,IAAI,CAAC,WAAW,kBAAkB,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC;;;gCAGxE,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC;qCACxB,OAAO,CAAC,CAAC,CAAC,eAAe,CAAC;;4CAEnB,IAAI,CAAC,aAAa;;KAEzD,CAAA;IACH,CAAC;IAED,KAAK,CAAC,YAAY,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,GAAG,EAAE,EAAe;QAC3D,OAAO;YACL,KAAK,EAAE,IAAI,CAAC,MAAM,CAAC,MAAM;YACzB,OAAO,EAAE,IAAI,CAAC,MAAM;SACrB,CAAA;IACH,CAAC;IAED,KAAK,CAAC,aAAa;QACjB,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,OAAO;aACvC,GAAG,CAAC,KAAK,CAAC,EAAE;YACX,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,QAAQ,EAAE,KAAK,EAAE,KAAK,EAAE,WAAW,EAAE,GAAG,KAAK,CAAA;YACrE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,QAAQ,EAAE,KAAK,EAAE,KAAK,EAAE,WAAW,EAAE,CAAA;QAChE,CAAC,CAAC;aACD,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,GAAG,CAAC,CAAC,QAAQ,CAAQ,CAAA;QAEjD,IAAI,CAAC,IAAI,CAAC,eAAe,EAAE,EAAE,CAAC;YAC5B,OAAM;QACR,CAAC;QAED,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;YAC/C,IAAI,CAAC;gBACH,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC;oBACnC,QAAQ,EAAE,GAAG,CAAA;;;;;;;;WAQZ;oBACD,SAAS,EAAE;wBACT,EAAE,EAAE,IAAI,CAAC,GAAG,CAAC,EAAE;wBACf,KAAK,EAAE,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE;qBAC/B;iBACF,CAAC,CAAA;gBAEF,IAAI,CAAC,MAAM,GAAG,QAAQ,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAA;gBAC5C,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAA;YACpB,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,MAAM,CAAC,EAAE,OAAO,EAAE,qBAAqB,EAAE,CAAC,CAAA;YAC5C,CAAC;QACH,CAAC;IACH,CAAC;IAED,eAAe;QACb,IAAI,IAAI,CAAC,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC7B,MAAM,CAAC,EAAE,OAAO,EAAE,yBAAyB,EAAE,CAAC,CAAA;YAC9C,OAAO,KAAK,CAAA;QACd,CAAC;QACD,MAAM,KAAK,GAAG,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAA;QAC1C,MAAM,WAAW,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,CAAA;QAClC,IAAI,KAAK,CAAC,MAAM,KAAK,WAAW,CAAC,IAAI,EAAE,CAAC;YACtC,MAAM,CAAC,EAAE,OAAO,EAAE,eAAe,EAAE,CAAC,CAAA;YACpC,OAAO,KAAK,CAAA;QACd,CAAC;QACD,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YAC5C,MAAM,KAAK,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAA;YAC5B,IAAI,KAAK,CAAC,QAAQ,IAAI,KAAK,CAAC,QAAQ,EAAE,CAAC;gBACrC,MAAM,CAAC,EAAE,OAAO,EAAE,OAAO,KAAK,CAAC,IAAI,yBAAyB,EAAE,CAAC,CAAA;gBAC/D,OAAO,KAAK,CAAA;YACd,CAAC;YACD,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;gBACV,MAAM,SAAS,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,CAAC,CAAA;gBACpC,IAAI,SAAS,CAAC,QAAQ,KAAK,KAAK,CAAC,QAAQ,EAAE,CAAC;oBAC1C,MAAM,CAAC,EAAE,OAAO,EAAE,OAAO,SAAS,CAAC,IAAI,OAAO,KAAK,CAAC,IAAI,iBAAiB,EAAE,CAAC,CAAA;oBAC5E,OAAO,KAAK,CAAA;gBACd,CAAC;YACH,CAAC;QACH,CAAC;QACD,OAAO,IAAI,CAAA;IACb,CAAC;IAED,KAAK,CAAC,aAAa;QACjB,IAAI,CAAC,KAAK,CAAC,qBAAqB,CAAC,IAAI,CAAC,CAAA;IACxC,CAAC;;AAvH2B;IAA3B,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;;2CAA0B;AAE5C;IAAR,KAAK,EAAE;;8CAA2C;AAC1C;IAAR,KAAK,EAAE;;mDAAwB;AACb;IAAlB,KAAK,CAAC,UAAU,CAAC;8BAAS,SAAS;6CAAA;AArBzB,cAAc;IAD1B,aAAa,CAAC,kBAAkB,CAAC;GACrB,cAAc,CAyI1B","sourcesContent":["import '@material/web/button/elevated-button.js'\nimport '@material/web/button/filled-button.js'\nimport '@material/web/button/text-button.js'\nimport '@material/web/icon/icon.js'\nimport '@operato/data-grist/ox-grist.js'\nimport deepEquals from 'lodash-es/isEqual'\n\nimport gql from 'graphql-tag'\nimport { LitElement, css, html } from 'lit'\nimport { customElement, property, state, query } from 'lit/decorators.js'\nimport { DataGrist } from '@operato/data-grist/ox-grist.js'\nimport { i18next, localize } from '@operato/i18n'\nimport { client } from '@operato/graphql'\nimport { notify } from '@operato/layout'\nimport { FetchOption } from '@operato/data-grist'\nimport { CommonHeaderStyles } from '@operato/styles'\n\ninterface KpiGrade {\n name: string\n minValue: number\n maxValue: number\n score?: number\n color?: string\n description?: string\n}\n\ntype KpiGrades = KpiGrade[]\n\n@customElement('kpi-grade-editor')\nexport class KpiGradeEditor extends localize(i18next)(LitElement) {\n static styles = [\n CommonHeaderStyles,\n css`\n :host {\n display: flex;\n flex-direction: column;\n\n background-color: var(--md-sys-color-surface);\n }\n\n ox-grist {\n flex: 1;\n }\n `\n ]\n\n @property({ type: Object }) kpi: any = { grades: [] }\n\n @state() grades: KpiGrades = this.kpi?.grades || []\n @state() gristConfig: any = null\n @query('ox-grist') grist!: DataGrist\n\n async firstUpdated() {\n if (this.kpi?.grades) {\n this.grades = [...this.kpi.grades]\n }\n\n this.gristConfig = {\n list: { fields: ['name', 'minValue', 'maxValue', 'score', 'color', 'description'] },\n columns: [\n { type: 'gutter', gutterName: 'row-selector', multiple: true, fixed: true },\n { type: 'gutter', gutterName: 'sequence', fixed: true },\n { type: 'gutter', gutterName: 'button', fixed: true, icon: 'add', handlers: { click: 'record-copy' } },\n { type: 'gutter', gutterName: 'button', fixed: true, icon: 'arrow_upward', handlers: { click: 'move-up' } },\n { type: 'gutter', gutterName: 'button', fixed: true, icon: 'arrow_downward', handlers: { click: 'move-down' } },\n { type: 'string', name: 'name', header: '등급명', record: { editable: true }, width: 100 },\n { type: 'number', name: 'minValue', header: '최소값', record: { editable: true }, width: 100 },\n { type: 'number', name: 'maxValue', header: '최대값', record: { editable: true }, width: 100 },\n { type: 'number', name: 'score', header: '점수', record: { editable: true }, width: 80 },\n { type: 'color', name: 'color', header: '색상', record: { editable: true }, width: 100 },\n { type: 'string', name: 'description', header: '설명', record: { editable: true }, width: 200 }\n ],\n rows: { selectable: { multiple: true } },\n pagination: { infinite: true }\n }\n }\n\n render() {\n return html`\n <ox-grist .mode=${'GRID'} .config=${this.gristConfig} .fetchHandler=${this.fetchHandler.bind(this)}></ox-grist>\n <div class=\"footer\">\n <div filler></div>\n <button danger @click=${this._deleteGrades.bind(this)}>\n <md-icon>delete</md-icon>${i18next.t('button.delete')}\n </button>\n <button done type=\"button\" @click=${this._updateGrades}><md-icon>save</md-icon>저장</button>\n </div>\n `\n }\n\n async fetchHandler({ page, limit, sorters = [] }: FetchOption) {\n return {\n total: this.grades.length,\n records: this.grades\n }\n }\n\n async _updateGrades() {\n this.grades = this.grist.dirtyData.records\n .map(patch => {\n const { name, minValue, maxValue, score, color, description } = patch\n return { name, minValue, maxValue, score, color, description }\n })\n .sort((a, b) => a.minValue - b.minValue) as any\n\n if (!this._validateGrades()) {\n return\n }\n\n if (!deepEquals(this.kpi?.grades, this.grades)) {\n try {\n const response = await client.mutate({\n mutation: gql`\n mutation ($id: String!, $patch: KpiPatch!) {\n updateKpi(id: $id, patch: $patch) {\n id\n name\n grades\n }\n }\n `,\n variables: {\n id: this.kpi.id,\n patch: { grades: this.grades }\n }\n })\n\n this.grades = response.data.updateKpi.grades\n this.grist.fetch()\n } catch (error) {\n notify({ message: '등급 저장 중 오류가 발생했습니다.' })\n }\n }\n }\n\n _validateGrades(): boolean {\n if (this.grades.length === 0) {\n notify({ message: '최소 1개 이상의 등급을 설정해야 합니다.' })\n return false\n }\n const names = this.grades.map(g => g.name)\n const uniqueNames = new Set(names)\n if (names.length !== uniqueNames.size) {\n notify({ message: '등급명이 중복되었습니다.' })\n return false\n }\n for (let i = 0; i < this.grades.length; i++) {\n const grade = this.grades[i]\n if (grade.minValue >= grade.maxValue) {\n notify({ message: `등급 \"${grade.name}\"의 최소값이 최대값보다 크거나 같습니다.` })\n return false\n }\n if (i > 0) {\n const prevGrade = this.grades[i - 1]\n if (prevGrade.maxValue !== grade.minValue) {\n notify({ message: `등급 \"${prevGrade.name}\"과 \"${grade.name}\" 사이에 간격이 있습니다.` })\n return false\n }\n }\n }\n return true\n }\n\n async _deleteGrades() {\n this.grist.deleteSelectedRecords(true)\n }\n}\n"]}
@@ -3,22 +3,25 @@ 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-viz-editor.js';
6
7
  import { PageView } from '@operato/shell';
7
8
  import { FetchOption } from '@operato/data-grist';
8
9
  import { KpiImporter } from './kpi-importer';
9
10
  import { KpiGradeEditor } from './kpi-grade-editor';
11
+ import { KpiVizEditor } from './kpi-viz-editor';
10
12
  declare const KpiListPage_base: (new (...args: any[]) => {
11
13
  _storeUnsubscribe: import("redux").Unsubscribe;
12
14
  connectedCallback(): void;
13
15
  disconnectedCallback(): void;
14
16
  stateChanged(_state: unknown): void;
15
17
  readonly isConnected: boolean;
16
- }) & (new (...args: any[]) => import("lit").LitElement) & typeof PageView & import("@open-wc/dedupe-mixin").Constructor<import("@open-wc/scoped-elements/types/src/types").ScopedElementsHost>;
18
+ }) & (new (...args: any[]) => import("lit").LitElement) & typeof PageView & import("@open-wc/dedupe-mixin").Constructor<import("@open-wc/scoped-elements/types/src/types.js").ScopedElementsHost>;
17
19
  export declare class KpiListPage extends KpiListPage_base {
18
20
  static styles: import("lit").CSSResult[];
19
21
  static get scopedElements(): {
20
22
  'kpi-importer': typeof KpiImporter;
21
23
  'kpi-grade-editor': typeof KpiGradeEditor;
24
+ 'kpi-viz-editor': typeof KpiVizEditor;
22
25
  };
23
26
  gristConfig: any;
24
27
  mode: 'CARD' | 'GRID' | 'LIST';
@@ -65,6 +68,7 @@ export declare class KpiListPage extends KpiListPage_base {
65
68
  exportHandler(): Promise<{}[]>;
66
69
  importHandler(records: any): Promise<void>;
67
70
  _editGrades(kpi: any): Promise<void>;
68
- _onGradesUpdated(detail: any): Promise<void>;
71
+ _editViz(kpi: any): Promise<void>;
72
+ _onVizUpdated(kpiId: string, vizType: string, vizMeta: any): Promise<void>;
69
73
  }
70
74
  export {};
@@ -4,6 +4,7 @@ import '@material/web/button/elevated-button.js';
4
4
  import '@operato/data-grist/ox-grist.js';
5
5
  import '@operato/data-grist/ox-filters-form.js';
6
6
  import '@operato/data-grist/ox-record-creator.js';
7
+ import './kpi-viz-editor.js';
7
8
  import { CommonButtonStyles, CommonHeaderStyles, CommonGristStyles, ScrollbarStyles } from '@operato/styles';
8
9
  import { PageView, store } from '@operato/shell';
9
10
  import { css, html } from 'lit';
@@ -19,6 +20,7 @@ import { connect } from 'pwa-helpers/connect-mixin';
19
20
  import gql from 'graphql-tag';
20
21
  import { KpiImporter } from './kpi-importer';
21
22
  import { KpiGradeEditor } from './kpi-grade-editor';
23
+ import { KpiVizEditor } from './kpi-viz-editor';
22
24
  let KpiListPage = class KpiListPage extends connect(store)(localize(i18next)(ScopedElementsMixin(PageView))) {
23
25
  constructor() {
24
26
  super(...arguments);
@@ -51,7 +53,8 @@ let KpiListPage = class KpiListPage extends connect(store)(localize(i18next)(Sco
51
53
  static get scopedElements() {
52
54
  return {
53
55
  'kpi-importer': KpiImporter,
54
- 'kpi-grade-editor': KpiGradeEditor
56
+ 'kpi-grade-editor': KpiGradeEditor,
57
+ 'kpi-viz-editor': KpiVizEditor
55
58
  };
56
59
  }
57
60
  get context() {
@@ -228,7 +231,29 @@ let KpiListPage = class KpiListPage extends connect(store)(localize(i18next)(Sco
228
231
  width: 60
229
232
  },
230
233
  { type: 'string', name: 'state', header: '상태', record: { editable: false }, width: 100 },
231
- { type: 'string', name: 'vizType', header: '시각화유형', record: { editable: true }, width: 120 },
234
+ {
235
+ type: 'string',
236
+ name: 'vizType',
237
+ header: '시각화 설정',
238
+ record: {
239
+ editable: false,
240
+ renderer: (v, c, r) => {
241
+ const vizType = r.vizType || 'CARD';
242
+ const vizMeta = r.vizMeta || {};
243
+ const color = vizMeta.color || '#2196f3';
244
+ const icon = vizMeta.icon || 'dashboard';
245
+ return html `
246
+ <div style="display:flex;align-items:center;gap:8px;">
247
+ <span style="color:${color};cursor:pointer;" @click=${() => this._editViz(r)}> ${vizType} </span>
248
+ <md-icon style="color:${color};font-size:16px;cursor:pointer;" @click=${() => this._editViz(r)}>
249
+ ${icon}
250
+ </md-icon>
251
+ </div>
252
+ `;
253
+ }
254
+ },
255
+ width: 150
256
+ },
232
257
  { type: 'string', name: 'schedule', header: '스케줄', record: { editable: true }, width: 120 },
233
258
  { type: 'string', name: 'scheduleId', header: '스케줄ID', record: { editable: false }, width: 120 },
234
259
  { type: 'string', name: 'timezone', header: '타임존', record: { editable: true }, width: 100 },
@@ -284,6 +309,8 @@ let KpiListPage = class KpiListPage extends connect(store)(localize(i18next)(Sco
284
309
  description
285
310
  active
286
311
  grades
312
+ vizType
313
+ vizMeta
287
314
  category {
288
315
  id
289
316
  name
@@ -440,15 +467,27 @@ let KpiListPage = class KpiListPage extends connect(store)(localize(i18next)(Sco
440
467
  };
441
468
  }
442
469
  async _editGrades(kpi) {
470
+ const popup = await openPopup(html ` <kpi-grade-editor .kpi=${kpi}></kpi-grade-editor> `, {
471
+ title: `${kpi.name} - 등급 설정`,
472
+ size: 'large'
473
+ });
474
+ popup.onclosed = () => {
475
+ this.grist.fetch();
476
+ };
477
+ }
478
+ async _editViz(kpi) {
443
479
  const popup = await openPopup(html `
444
- <kpi-grade-editor .kpi=${kpi} @grades-updated=${(e) => this._onGradesUpdated(e.detail)}></kpi-grade-editor>
480
+ <kpi-viz-editor
481
+ .kpi=${kpi}
482
+ .onSave=${(vizType, vizMeta) => this._onVizUpdated(kpi.id, vizType, vizMeta)}
483
+ .onCancel=${() => popup.close()}
484
+ ></kpi-viz-editor>
445
485
  `, {
446
- title: `${kpi.name} - 등급 설정`,
486
+ title: `${kpi.name} - 시각화 설정`,
447
487
  size: 'large'
448
488
  });
449
489
  }
450
- async _onGradesUpdated(detail) {
451
- const { kpiId, grades } = detail;
490
+ async _onVizUpdated(kpiId, vizType, vizMeta) {
452
491
  try {
453
492
  const response = await client.mutate({
454
493
  mutation: gql `
@@ -456,25 +495,26 @@ let KpiListPage = class KpiListPage extends connect(store)(localize(i18next)(Sco
456
495
  updateKpi(id: $id, patch: $patch) {
457
496
  id
458
497
  name
459
- grades
498
+ vizType
499
+ vizMeta
460
500
  }
461
501
  }
462
502
  `,
463
503
  variables: {
464
504
  id: kpiId,
465
- patch: { grades }
505
+ patch: { vizType, vizMeta }
466
506
  }
467
507
  });
468
508
  if (!response.errors) {
469
509
  this.grist.fetch();
470
510
  notify({
471
- message: '등급 설정이 성공적으로 업데이트되었습니다.'
511
+ message: '시각화 설정이 성공적으로 업데이트되었습니다.'
472
512
  });
473
513
  }
474
514
  }
475
515
  catch (error) {
476
516
  notify({
477
- message: '등급 설정 업데이트 중 오류가 발생했습니다.'
517
+ message: '시각화 설정 업데이트 중 오류가 발생했습니다.'
478
518
  });
479
519
  }
480
520
  }