@things-factory/kpi 9.0.14 → 9.0.16

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 (27) hide show
  1. package/client/pages/kpi/kpi-list-page.ts +78 -2
  2. package/client/pages/kpi/kpi-viz-editor.ts +353 -0
  3. package/client/pages/kpi-dashboard/kpi-dashboard.ts +128 -1
  4. package/dist-client/pages/kpi/kpi-list-page.d.ts +6 -1
  5. package/dist-client/pages/kpi/kpi-list-page.js +72 -2
  6. package/dist-client/pages/kpi/kpi-list-page.js.map +1 -1
  7. package/dist-client/pages/kpi/kpi-viz-editor.d.ts +20 -0
  8. package/dist-client/pages/kpi/kpi-viz-editor.js +364 -0
  9. package/dist-client/pages/kpi/kpi-viz-editor.js.map +1 -0
  10. package/dist-client/pages/kpi-dashboard/kpi-dashboard.d.ts +2 -0
  11. package/dist-client/pages/kpi-dashboard/kpi-dashboard.js +117 -1
  12. package/dist-client/pages/kpi-dashboard/kpi-dashboard.js.map +1 -1
  13. package/dist-client/tsconfig.tsbuildinfo +1 -1
  14. package/dist-server/service/kpi/kpi-type.d.ts +3 -3
  15. package/dist-server/service/kpi/kpi-type.js +4 -4
  16. package/dist-server/service/kpi/kpi-type.js.map +1 -1
  17. package/dist-server/service/kpi/kpi.d.ts +18 -1
  18. package/dist-server/service/kpi/kpi.js +26 -4
  19. package/dist-server/service/kpi/kpi.js.map +1 -1
  20. package/dist-server/service/kpi-category/kpi-category-type.d.ts +1 -0
  21. package/dist-server/service/kpi-category/kpi-category-type.js +5 -1
  22. package/dist-server/service/kpi-category/kpi-category-type.js.map +1 -1
  23. package/dist-server/tsconfig.tsbuildinfo +1 -1
  24. package/package.json +3 -3
  25. package/server/service/kpi/kpi-type.ts +7 -7
  26. package/server/service/kpi/kpi.ts +27 -4
  27. package/server/service/kpi-category/kpi-category-type.ts +4 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"kpi-viz-editor.js","sourceRoot":"","sources":["../../../client/pages/kpi/kpi-viz-editor.ts"],"names":[],"mappings":"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","sourcesContent":["import '@material/web/button/elevated-button.js'\nimport '@material/web/select/outlined-select.js'\nimport '@material/web/textfield/outlined-text-field.js'\nimport '@material/web/icon/icon.js'\n\nimport { PageView } from '@operato/shell'\nimport { css, html, LitElement } from 'lit'\nimport { customElement, property } from 'lit/decorators.js'\nimport { client } from '@operato/graphql'\nimport { i18next, localize } from '@operato/i18n'\nimport { notify } from '@operato/layout'\nimport { OxPopup } from '@operato/popup'\nimport gql from 'graphql-tag'\nimport { CommonHeaderStyles, ScrollbarStyles } from '@operato/styles'\n\nconst VIZ_TYPES = [\n { value: 'CARD', label: '카드', icon: 'dashboard' },\n { value: 'GAUGE', label: '게이지', icon: 'speed' },\n { value: 'PROGRESS', label: '진행률', icon: 'linear_scale' },\n { value: 'BAR', label: '막대 차트', icon: 'bar_chart' },\n { value: 'LINE', label: '선 차트', icon: 'show_chart' },\n { value: 'PIE', label: '파이 차트', icon: 'pie_chart' },\n { value: 'DONUT', label: '도넛 차트', icon: 'donut_large' },\n { value: 'RADAR', label: '레이더 차트', icon: 'radar' },\n { value: 'BULLET', label: '불릿 차트', icon: 'track_changes' },\n { value: 'THERMOMETER', label: '온도계', icon: 'thermostat' },\n { value: 'SPEEDOMETER', label: '속도계', icon: 'speed' },\n { value: 'ICON', label: '아이콘', icon: 'emoji_events' },\n { value: 'BADGE', label: '배지', icon: 'badge' },\n { value: 'TEXT', label: '텍스트', icon: 'text_fields' },\n { value: 'TABLE', label: '테이블', icon: 'table_chart' }\n]\n\n@customElement('kpi-viz-editor')\nexport class KpiVizEditor extends localize(i18next)(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 .viz-editor {\n flex: 1;\n display: flex;\n flex-direction: column;\n overflow-y: auto;\n\n background: white;\n padding: 24px;\n }\n\n .form-group {\n margin-bottom: 20px;\n }\n\n .form-group label {\n display: block;\n margin-bottom: 8px;\n font-weight: 500;\n color: #555;\n }\n\n .form-options {\n flex: 1;\n display: flex;\n flex-direction: row;\n }\n\n .viz-type-grid {\n display: grid;\n grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));\n gap: 12px;\n margin-bottom: 20px;\n }\n\n .viz-type-option {\n display: flex;\n flex-direction: column;\n align-items: center;\n padding: 16px 12px;\n border: 2px solid #e0e0e0;\n border-radius: 8px;\n cursor: pointer;\n transition: all 0.2s;\n text-align: center;\n }\n\n .viz-type-option:hover {\n border-color: #2196f3;\n background: #f5f9ff;\n }\n\n .viz-type-option.selected {\n border-color: #2196f3;\n background: #e3f2fd;\n }\n\n .viz-type-option md-icon {\n font-size: 24px;\n margin-bottom: 8px;\n color: #666;\n }\n\n .viz-type-option.selected md-icon {\n color: #2196f3;\n }\n\n .viz-type-option .label {\n font-size: 0.9rem;\n font-weight: 500;\n color: #333;\n }\n\n .viz-meta-section {\n margin-top: 24px;\n padding-top: 20px;\n border-top: 1px solid #eee;\n }\n\n .color-picker {\n display: flex;\n gap: 12px;\n align-items: center;\n margin-bottom: 16px;\n }\n\n .color-input {\n width: 60px;\n height: 40px;\n border: none;\n border-radius: 6px;\n cursor: pointer;\n }\n\n .buttons {\n display: flex;\n gap: 12px;\n justify-content: flex-end;\n margin-top: 24px;\n padding-top: 20px;\n border-top: 1px solid #eee;\n }\n\n .preview {\n flex: 1;\n margin: 30px 16px 16px 16px;\n padding: 16px;\n background: #f8f9fa;\n border-radius: 8px;\n border: 1px solid #e9ecef;\n }\n\n .preview h4 {\n margin: 0 0 12px 0;\n color: #495057;\n font-size: 0.95rem;\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 = null\n @property({ type: String }) selectedVizType: string = 'CARD'\n @property({ type: Object }) vizMeta: any = {}\n @property({ type: Object }) onSave: Function = () => {}\n @property({ type: Object }) onCancel: Function = () => {}\n\n connectedCallback() {\n super.connectedCallback()\n if (this.kpi) {\n this.selectedVizType = this.kpi.vizType || 'CARD'\n this.vizMeta = this.kpi.vizMeta || {}\n }\n }\n\n _selectVizType(type: string) {\n this.selectedVizType = type\n this.requestUpdate()\n }\n\n _updateVizMeta(key: string, value: any) {\n this.vizMeta = { ...this.vizMeta, [key]: value }\n this.requestUpdate()\n }\n\n _renderPreview() {\n const kpiValue = this.kpi?.value?.value || 75\n const targetValue = this.kpi?.targetValue || 100\n const unit = this.kpi?.unit || ''\n const color = this.vizMeta.color || '#2196f3'\n const icon = this.vizMeta.icon || 'trending_up'\n\n switch (this.selectedVizType) {\n case 'CARD':\n return html`\n <div\n style=\"display:flex;align-items:center;gap:12px;padding:16px;background:white;border-radius:8px;border:1px solid #e0e0e0;\"\n >\n <md-icon style=\"color:${color};font-size:32px;\">${icon}</md-icon>\n <div>\n <div style=\"font-size:1.5rem;font-weight:bold;color:${color};\">${kpiValue}${unit}</div>\n <div style=\"font-size:0.9rem;color:#666;\">목표: ${targetValue}${unit}</div>\n </div>\n </div>\n `\n case 'GAUGE':\n const percentage = Math.min((kpiValue / targetValue) * 100, 100)\n return html`\n <div style=\"text-align:center;padding:16px;\">\n <div\n style=\"width:120px;height:60px;border-radius:60px 60px 0 0;background:conic-gradient(${color} 0deg ${percentage *\n 3.6}deg, #e0e0e0 ${percentage * 3.6}deg 360deg);margin:0 auto;position:relative;\"\n >\n <div\n style=\"position:absolute;bottom:0;left:50%;transform:translateX(-50%);font-size:1.2rem;font-weight:bold;color:${color};\"\n >\n ${kpiValue}${unit}\n </div>\n </div>\n </div>\n `\n case 'PROGRESS':\n const progressPercentage = Math.min((kpiValue / targetValue) * 100, 100)\n return html`\n <div style=\"padding:16px;\">\n <div style=\"background:#e0e0e0;height:20px;border-radius:10px;overflow:hidden;\">\n <div style=\"background:${color};height:100%;width:${progressPercentage}%;transition:width 0.3s;\"></div>\n </div>\n <div style=\"text-align:center;margin-top:8px;font-weight:bold;color:${color};\">\n ${kpiValue}${unit} / ${targetValue}${unit}\n </div>\n </div>\n `\n case 'ICON':\n return html`\n <div style=\"text-align:center;padding:16px;\">\n <md-icon style=\"color:${color};font-size:48px;\">${icon}</md-icon>\n <div style=\"font-size:1.2rem;font-weight:bold;color:${color};margin-top:8px;\">${kpiValue}${unit}</div>\n </div>\n `\n default:\n return html` <div style=\"padding:16px;text-align:center;color:#666;\">${this.selectedVizType} 미리보기</div> `\n }\n }\n\n render() {\n return html`\n <div class=\"viz-editor\">\n <div class=\"form-group\">\n <label>시각화 타입 선택</label>\n <div class=\"viz-type-grid\">\n ${VIZ_TYPES.map(\n type => html`\n <div\n class=\"viz-type-option ${this.selectedVizType === type.value ? 'selected' : ''}\"\n @click=${() => this._selectVizType(type.value)}\n >\n <md-icon>${type.icon}</md-icon>\n <div class=\"label\">${type.label}</div>\n </div>\n `\n )}\n </div>\n </div>\n\n <div class=\"form-options\">\n <div class=\"viz-meta-section\">\n <label>시각화 옵션</label>\n\n <div class=\"form-group\">\n <label>색상</label>\n <div class=\"color-picker\">\n <input\n type=\"color\"\n class=\"color-input\"\n .value=${this.vizMeta.color || '#2196f3'}\n @change=${(e: any) => this._updateVizMeta('color', e.target.value)}\n />\n <md-outlined-text-field\n label=\"색상 코드\"\n .value=${this.vizMeta.color || '#2196f3'}\n @change=${(e: any) => this._updateVizMeta('color', e.target.value)}\n ></md-outlined-text-field>\n </div>\n </div>\n\n <div class=\"form-group\">\n <label>아이콘</label>\n <md-outlined-text-field\n label=\"Material Icons 이름\"\n .value=${this.vizMeta.icon || 'trending_up'}\n @change=${(e: any) => this._updateVizMeta('icon', e.target.value)}\n ></md-outlined-text-field>\n </div>\n\n <div class=\"form-group\">\n <label>최소값</label>\n <md-outlined-text-field\n type=\"number\"\n label=\"최소값\"\n .value=${this.vizMeta.minValue || 0}\n @change=${(e: any) => this._updateVizMeta('minValue', parseFloat(e.target.value))}\n ></md-outlined-text-field>\n </div>\n\n <div class=\"form-group\">\n <label>최대값</label>\n <md-outlined-text-field\n type=\"number\"\n label=\"최대값\"\n .value=${this.vizMeta.maxValue || 100}\n @change=${(e: any) => this._updateVizMeta('maxValue', parseFloat(e.target.value))}\n ></md-outlined-text-field>\n </div>\n\n <div class=\"form-group\">\n <label>소수점 자릿수</label>\n <md-outlined-text-field\n type=\"number\"\n label=\"소수점 자릿수\"\n .value=${this.vizMeta.decimalPlaces || 0}\n @change=${(e: any) => this._updateVizMeta('decimalPlaces', parseInt(e.target.value))}\n ></md-outlined-text-field>\n </div>\n </div>\n\n <div class=\"preview\">\n <h4>미리보기</h4>\n ${this._renderPreview()}\n </div>\n </div>\n </div>\n\n <div class=\"footer\">\n <div filler></div>\n <button type=\"button\" @click=${this.onCancel}><md-icon>cancel</md-icon>취소</button>\n <button type=\"button\" @click=${() => this.onSave(this.selectedVizType, this.vizMeta)} done>\n <md-icon>save</md-icon>저장\n </button>\n </div>\n `\n }\n}\n"]}
@@ -1,5 +1,6 @@
1
1
  import { nothing } from 'lit';
2
2
  import { PageView } from '@operato/shell';
3
+ import '@material/web/icon/icon.js';
3
4
  import './kpi-performance-summary';
4
5
  import './kpi-grade-visualization';
5
6
  import './kpi-history-viewer';
@@ -20,6 +21,7 @@ export declare class KpiDashboardPage extends PageView {
20
21
  fetchCategories(): Promise<void>;
21
22
  openHistoryModal(kpi: any): Promise<void>;
22
23
  closeHistoryModal(): void;
24
+ _renderKpiValue(kpi: any): import("lit-html").TemplateResult<1>;
23
25
  get context(): any;
24
26
  render(): import("lit-html").TemplateResult<1> | typeof nothing;
25
27
  }
@@ -6,6 +6,7 @@ import { ScrollbarStyles } from '@operato/styles';
6
6
  import { client } from '@operato/graphql';
7
7
  import gql from 'graphql-tag';
8
8
  import { state } from 'lit/decorators.js';
9
+ import '@material/web/icon/icon.js';
9
10
  import './kpi-performance-summary';
10
11
  import './kpi-grade-visualization';
11
12
  import './kpi-history-viewer';
@@ -107,6 +108,8 @@ let KpiDashboardPage = class KpiDashboardPage extends PageView {
107
108
  targetValue
108
109
  unit
109
110
  grades
111
+ vizType
112
+ vizMeta
110
113
  histories(limit: 1) {
111
114
  version
112
115
  updatedAt
@@ -172,6 +175,119 @@ let KpiDashboardPage = class KpiDashboardPage extends PageView {
172
175
  this.modalHistories = [];
173
176
  this.modalKpiName = '';
174
177
  }
178
+ _renderKpiValue(kpi) {
179
+ const kpiValue = kpi.value?.value ?? 0;
180
+ const targetValue = kpi.targetValue ?? 100;
181
+ const unit = kpi.unit ?? '';
182
+ const vizType = kpi.vizType || 'CARD';
183
+ const vizMeta = kpi.vizMeta || {};
184
+ const color = vizMeta.color || '#3a3ad6';
185
+ const icon = vizMeta.icon || 'trending_up';
186
+ const minValue = vizMeta.minValue || 0;
187
+ const maxValue = vizMeta.maxValue || 100;
188
+ const decimalPlaces = vizMeta.decimalPlaces || 0;
189
+ const formattedValue = typeof kpiValue === 'number' ? kpiValue.toFixed(decimalPlaces) : kpiValue;
190
+ switch (vizType) {
191
+ case 'GAUGE':
192
+ const gaugePercentage = Math.min(((kpiValue - minValue) / (maxValue - minValue)) * 100, 100);
193
+ return html `
194
+ <div style="text-align:center;margin:16px 0;">
195
+ <div
196
+ style="width:120px;height:60px;border-radius:60px 60px 0 0;background:conic-gradient(${color} 0deg ${gaugePercentage *
197
+ 3.6}deg, #e0e0e0 ${gaugePercentage * 3.6}deg 360deg);margin:0 auto;position:relative;"
198
+ >
199
+ <div
200
+ style="position:absolute;bottom:0;left:50%;transform:translateX(-50%);font-size:1.2rem;font-weight:bold;color:${color};"
201
+ >
202
+ ${formattedValue}${unit}
203
+ </div>
204
+ </div>
205
+ </div>
206
+ `;
207
+ case 'PROGRESS':
208
+ const progressPercentage = Math.min(((kpiValue - minValue) / (maxValue - minValue)) * 100, 100);
209
+ return html `
210
+ <div style="margin:16px 0;">
211
+ <div style="background:#e0e0e0;height:20px;border-radius:10px;overflow:hidden;">
212
+ <div style="background:${color};height:100%;width:${progressPercentage}%;transition:width 0.3s;"></div>
213
+ </div>
214
+ <div style="text-align:center;margin-top:8px;font-weight:bold;color:${color};font-size:1.2rem;">
215
+ ${formattedValue}${unit}
216
+ </div>
217
+ </div>
218
+ `;
219
+ case 'ICON':
220
+ return html `
221
+ <div style="text-align:center;margin:16px 0;">
222
+ <md-icon style="color:${color};font-size:48px;">${icon}</md-icon>
223
+ <div style="font-size:1.5rem;font-weight:bold;color:${color};margin-top:8px;">${formattedValue}${unit}</div>
224
+ </div>
225
+ `;
226
+ case 'THERMOMETER':
227
+ const thermoPercentage = Math.min(((kpiValue - minValue) / (maxValue - minValue)) * 100, 100);
228
+ return html `
229
+ <div style="text-align:center;margin:16px 0;">
230
+ <div
231
+ style="width:40px;height:120px;background:#e0e0e0;border-radius:20px;margin:0 auto;position:relative;overflow:hidden;"
232
+ >
233
+ <div
234
+ style="position:absolute;bottom:0;width:100%;height:${thermoPercentage}%;background:${color};border-radius:20px;"
235
+ ></div>
236
+ </div>
237
+ <div style="font-size:1.2rem;font-weight:bold;color:${color};margin-top:8px;">${formattedValue}${unit}</div>
238
+ </div>
239
+ `;
240
+ case 'SPEEDOMETER':
241
+ const speedPercentage = Math.min(((kpiValue - minValue) / (maxValue - minValue)) * 100, 100);
242
+ const angle = (speedPercentage / 100) * 180 - 90; // -90도에서 90도까지
243
+ return html `
244
+ <div style="text-align:center;margin:16px 0;">
245
+ <div
246
+ style="width:120px;height:60px;border-radius:60px 60px 0 0;background:conic-gradient(${color} -90deg ${angle}deg, #e0e0e0 ${angle}deg 90deg);margin:0 auto;position:relative;"
247
+ >
248
+ <div
249
+ style="position:absolute;bottom:0;left:50%;transform:translateX(-50%);font-size:1.2rem;font-weight:bold;color:${color};"
250
+ >
251
+ ${formattedValue}${unit}
252
+ </div>
253
+ </div>
254
+ </div>
255
+ `;
256
+ case 'BULLET':
257
+ const bulletPercentage = Math.min(((kpiValue - minValue) / (maxValue - minValue)) * 100, 100);
258
+ const targetPercentage = Math.min(((targetValue - minValue) / (maxValue - minValue)) * 100, 100);
259
+ return html `
260
+ <div style="margin:16px 0;">
261
+ <div style="background:#e0e0e0;height:30px;border-radius:15px;overflow:hidden;position:relative;">
262
+ <div style="background:${color};height:100%;width:${bulletPercentage}%;transition:width 0.3s;"></div>
263
+ <div
264
+ style="position:absolute;top:0;left:${targetPercentage}%;width:2px;height:100%;background:#333;"
265
+ ></div>
266
+ </div>
267
+ <div style="text-align:center;margin-top:8px;font-weight:bold;color:${color};font-size:1.2rem;">
268
+ ${formattedValue}${unit}
269
+ </div>
270
+ </div>
271
+ `;
272
+ case 'TEXT':
273
+ return html `
274
+ <div style="text-align:center;margin:16px 0;">
275
+ <div style="font-size:1.5rem;font-weight:bold;color:${color};">${formattedValue}${unit}</div>
276
+ </div>
277
+ `;
278
+ case 'BADGE':
279
+ return html `
280
+ <div style="text-align:center;margin:16px 0;">
281
+ <span
282
+ style="display:inline-block;padding:8px 16px;background:${color};color:white;border-radius:20px;font-size:1.2rem;font-weight:bold;"
283
+ >${formattedValue}${unit}</span
284
+ >
285
+ </div>
286
+ `;
287
+ default: // CARD, BAR, LINE, PIE, DONUT, RADAR, TABLE
288
+ return html ` <div class="kpi-value">${formattedValue}${unit}</div> `;
289
+ }
290
+ }
175
291
  get context() {
176
292
  return {
177
293
  title: 'KPI 대시보드',
@@ -217,7 +333,7 @@ let KpiDashboardPage = class KpiDashboardPage extends PageView {
217
333
  ${(cat.kpis || []).map(kpi => html `
218
334
  <div class="kpi-card">
219
335
  <div class="kpi-name">${kpi.name}</div>
220
- <div class="kpi-value">${kpi.value?.value ?? '데이터 없음'}${kpi.unit ?? ''}</div>
336
+ ${this._renderKpiValue(kpi)}
221
337
  <div class="kpi-target">목표: ${kpi.targetValue ?? '-'}${kpi.unit ?? ''}</div>
222
338
  <!-- 등급 시각화 -->
223
339
  <div style="margin-top:8px;">
@@ -1 +1 @@
1
- {"version":3,"file":"kpi-dashboard.js","sourceRoot":"","sources":["../../../client/pages/kpi-dashboard/kpi-dashboard.ts"],"names":[],"mappings":"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","sourcesContent":["import { html, css, nothing } from 'lit'\nimport { customElement } from 'lit/decorators.js'\nimport { PageView } from '@operato/shell'\nimport { ScrollbarStyles } from '@operato/styles'\nimport { client } from '@operato/graphql'\nimport gql from 'graphql-tag'\nimport { state } from 'lit/decorators.js'\n\nimport './kpi-performance-summary'\nimport './kpi-grade-visualization'\nimport './kpi-history-viewer'\nimport './kpi-list-summary'\nimport './kpi-value-entry'\nimport './kpi-alert-panel'\n\n@customElement('kpi-dashboard')\nexport class KpiDashboardPage extends PageView {\n static styles = [\n ScrollbarStyles,\n css`\n :host {\n display: flex;\n flex-direction: column;\n overflow-y: auto;\n }\n .dashboard-root {\n flex: 1;\n padding: 24px;\n }\n .category-section {\n margin-bottom: 40px;\n }\n .category-title {\n font-size: 1.3rem;\n font-weight: bold;\n margin-bottom: 16px;\n }\n .kpi-cards {\n display: flex;\n gap: 24px;\n flex-wrap: wrap;\n }\n .kpi-card {\n background: #fff;\n border-radius: 12px;\n padding: 24px 32px;\n min-width: 220px;\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n margin-bottom: 8px;\n border: 1px solid #ececec;\n }\n .kpi-name {\n font-size: 1.1rem;\n font-weight: 500;\n margin-bottom: 8px;\n }\n .kpi-value {\n font-size: 2.2rem;\n font-weight: bold;\n color: #3a3ad6;\n margin-bottom: 4px;\n }\n .kpi-target {\n font-size: 1rem;\n color: #888;\n }\n `\n ]\n\n @state() categories: any[] = []\n @state() loading = true\n @state() error = ''\n @state() alerts: any[] = []\n @state() showHistoryModal: boolean = false\n @state() modalHistories: any[] = []\n @state() modalKpiName: string = ''\n\n connectedCallback() {\n super.connectedCallback()\n this.fetchCategories()\n }\n\n pageUpdated(changes: any, lifecycle: any) {\n if (this.active) {\n this.fetchCategories()\n }\n }\n\n async fetchCategories() {\n this.loading = true\n this.error = ''\n try {\n const response = await client.query({\n query: gql`\n query {\n kpiCategories {\n items {\n id\n name\n kpis {\n id\n name\n value {\n value\n valueDate\n }\n targetValue\n unit\n grades\n histories(limit: 1) {\n version\n updatedAt\n updater {\n name\n }\n }\n }\n }\n }\n kpiAlerts {\n id\n kpi {\n id\n }\n message\n level\n createdAt\n }\n }\n `\n })\n this.categories = response.data.kpiCategories.items || []\n this.alerts = response.data.kpiAlerts || []\n } catch (e) {\n this.error = 'KPI 카테고리 데이터를 불러오지 못했습니다.'\n } finally {\n this.loading = false\n }\n }\n\n async openHistoryModal(kpi) {\n // 전체 이력 fetch (limit 없이)\n try {\n const response = await client.query({\n query: gql`\n query ($kpiId: String!) {\n kpi(id: $kpiId) {\n name\n histories {\n version\n updatedAt\n updater {\n name\n }\n }\n }\n }\n `,\n variables: { kpiId: kpi.id }\n })\n this.modalHistories = response.data.kpi.histories || []\n this.modalKpiName = response.data.kpi.name || ''\n this.showHistoryModal = true\n } catch (e) {\n alert('이력 데이터를 불러오지 못했습니다.')\n }\n }\n closeHistoryModal() {\n this.showHistoryModal = false\n this.modalHistories = []\n this.modalKpiName = ''\n }\n\n get context() {\n return {\n title: 'KPI 대시보드',\n description: '조직 KPI 실적, 등급, 이력, 시각화 등 KPI 전용 대시보드'\n } as any\n }\n\n render() {\n if (this.loading) return nothing\n if (this.error) return html`<div>${this.error}</div>`\n return html`\n <div class=\"dashboard-root\">\n ${this.showHistoryModal\n ? html`\n <div\n style=\"position:fixed;top:0;left:0;width:100vw;height:100vh;background:rgba(0,0,0,0.25);z-index:1000;display:flex;align-items:center;justify-content:center;\"\n >\n <div\n style=\"background:#fff;padding:32px 40px;border-radius:16px;min-width:340px;max-width:90vw;max-height:80vh;overflow:auto;box-shadow:0 4px 24px rgba(0,0,0,0.15);\"\n >\n <div style=\"font-size:1.2rem;font-weight:bold;margin-bottom:16px;\">\n ${this.modalKpiName} 변경 이력\n </div>\n <ul style=\"padding:0;list-style:none;\">\n ${this.modalHistories.length === 0\n ? html`<li style=\"color:#bbb;\">이력이 없습니다.</li>`\n : this.modalHistories.map(\n h =>\n html`<li style=\"margin-bottom:8px;\">\n v${h.version} (${h.updatedAt?.slice(0, 10) ?? ''} ${h.updater?.name ?? ''})\n </li>`\n )}\n </ul>\n <button style=\"margin-top:16px;padding:6px 18px;\" @click=${() => this.closeHistoryModal()}>\n 닫기\n </button>\n </div>\n </div>\n `\n : nothing}\n ${this.categories.map(\n cat => html`\n <div class=\"category-section\">\n <div class=\"category-title\">${cat.name}</div>\n <div class=\"kpi-cards\">\n ${(cat.kpis || []).map(\n kpi => html`\n <div class=\"kpi-card\">\n <div class=\"kpi-name\">${kpi.name}</div>\n <div class=\"kpi-value\">${kpi.value?.value ?? '데이터 없음'}${kpi.unit ?? ''}</div>\n <div class=\"kpi-target\">목표: ${kpi.targetValue ?? '-'}${kpi.unit ?? ''}</div>\n <!-- 등급 시각화 -->\n <div style=\"margin-top:8px;\">\n <b>등급:</b>\n ${(() => {\n const grades = kpi.grades || []\n const kpiValue = kpi.value?.value\n\n if (grades.length > 5) {\n // 5개를 넘으면 현재 값에 해당하는 grade만 표시\n const currentGrade = grades.find(g => kpiValue >= g.minValue && kpiValue <= g.maxValue)\n if (currentGrade) {\n return html`<span\n style=\"display:inline-block;margin-right:8px;padding:4px 12px;border-radius:12px;background:${currentGrade.color ??\n '#3a3ad6'};color:white;font-size:1em;font-weight:bold;box-shadow:0 2px 8px rgba(58,58,214,0.3);border:2px solid ${currentGrade.color ??\n '#3a3ad6'};\"\n >${currentGrade.name}(${currentGrade.minValue}~${currentGrade.maxValue}${kpi.unit ??\n ''},\n ${currentGrade.score ?? ''}점)</span\n >`\n } else {\n return html`<span style=\"color:#bbb;font-size:0.95em;\">등급 없음</span>`\n }\n } else {\n // 5개 이하면 전체 리스트 표시하되 현재 등급은 강조\n return grades.map(g => {\n const isCurrentGrade = kpiValue >= g.minValue && kpiValue <= g.maxValue\n return html`<span\n style=\"display:inline-block;margin-right:8px;padding:${isCurrentGrade\n ? '4px 12px'\n : '2px 8px'};border-radius:${isCurrentGrade\n ? '12px'\n : '8px'};background:${isCurrentGrade\n ? (g.color ?? '#3a3ad6')\n : (g.color ?? '#eee')};color:${isCurrentGrade\n ? 'white'\n : '#222'};font-size:${isCurrentGrade ? '1em' : '0.95em'};font-weight:${isCurrentGrade\n ? 'bold'\n : 'normal'};box-shadow:${isCurrentGrade\n ? '0 2px 8px rgba(58,58,214,0.3)'\n : 'none'};border:${isCurrentGrade ? `2px solid ${g.color ?? '#3a3ad6'}` : 'none'};\"\n >${g.name}(${g.minValue}~${g.maxValue}${kpi.unit ?? ''}, ${g.score ?? ''}점)</span\n >`\n })\n }\n })()}\n </div>\n <!-- 최근 변경 이력(1건만) -->\n <div style=\"margin-top:8px;font-size:0.95em;color:#888;\">\n <b>최근 변경:</b>\n ${kpi.histories && kpi.histories.length > 0\n ? html`<span style=\"margin-right:8px;\"\n >v${kpi.histories[0].version} (${kpi.histories[0].updatedAt?.slice(0, 10) ?? ''}\n ${kpi.histories[0].updater?.name ?? ''})</span\n >`\n : html`<span style=\"color:#bbb\">없음</span>`}\n <button\n style=\"margin-left:8px;font-size:0.95em;padding:2px 8px;\"\n @click=${() => this.openHistoryModal(kpi)}\n >\n 더보기\n </button>\n </div>\n <!-- 경고/알림 -->\n <div style=\"margin-top:8px;\">\n ${this.alerts.filter(a => a.kpi?.id === kpi.id).length > 0\n ? this.alerts\n .filter(a => a.kpi?.id === kpi.id)\n .map(\n a =>\n html`<span\n style=\"display:inline-block;margin-right:8px;padding:2px 8px;border-radius:8px;background:${a.level ===\n 'critical'\n ? '#ffb3b3'\n : a.level === 'warning'\n ? '#ffe082'\n : '#e0e0e0'};color:#b71c1c;font-size:0.95em;\"\n >${a.message}</span\n >`\n )\n : html`<span style=\"color:#4caf50;font-size:0.95em;\">이상/경고 없음</span>`}\n </div>\n </div>\n `\n )}\n </div>\n </div>\n `\n )}\n </div>\n `\n }\n}\n"]}
1
+ {"version":3,"file":"kpi-dashboard.js","sourceRoot":"","sources":["../../../client/pages/kpi-dashboard/kpi-dashboard.ts"],"names":[],"mappings":"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","sourcesContent":["import { html, css, nothing } from 'lit'\nimport { customElement } from 'lit/decorators.js'\nimport { PageView } from '@operato/shell'\nimport { ScrollbarStyles } from '@operato/styles'\nimport { client } from '@operato/graphql'\nimport gql from 'graphql-tag'\nimport { state } from 'lit/decorators.js'\nimport '@material/web/icon/icon.js'\n\nimport './kpi-performance-summary'\nimport './kpi-grade-visualization'\nimport './kpi-history-viewer'\nimport './kpi-list-summary'\nimport './kpi-value-entry'\nimport './kpi-alert-panel'\n\n@customElement('kpi-dashboard')\nexport class KpiDashboardPage extends PageView {\n static styles = [\n ScrollbarStyles,\n css`\n :host {\n display: flex;\n flex-direction: column;\n overflow-y: auto;\n }\n .dashboard-root {\n flex: 1;\n padding: 24px;\n }\n .category-section {\n margin-bottom: 40px;\n }\n .category-title {\n font-size: 1.3rem;\n font-weight: bold;\n margin-bottom: 16px;\n }\n .kpi-cards {\n display: flex;\n gap: 24px;\n flex-wrap: wrap;\n }\n .kpi-card {\n background: #fff;\n border-radius: 12px;\n padding: 24px 32px;\n min-width: 220px;\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n margin-bottom: 8px;\n border: 1px solid #ececec;\n }\n .kpi-name {\n font-size: 1.1rem;\n font-weight: 500;\n margin-bottom: 8px;\n }\n .kpi-value {\n font-size: 2.2rem;\n font-weight: bold;\n color: #3a3ad6;\n margin-bottom: 4px;\n }\n .kpi-target {\n font-size: 1rem;\n color: #888;\n }\n `\n ]\n\n @state() categories: any[] = []\n @state() loading = true\n @state() error = ''\n @state() alerts: any[] = []\n @state() showHistoryModal: boolean = false\n @state() modalHistories: any[] = []\n @state() modalKpiName: string = ''\n\n connectedCallback() {\n super.connectedCallback()\n this.fetchCategories()\n }\n\n pageUpdated(changes: any, lifecycle: any) {\n if (this.active) {\n this.fetchCategories()\n }\n }\n\n async fetchCategories() {\n this.loading = true\n this.error = ''\n try {\n const response = await client.query({\n query: gql`\n query {\n kpiCategories {\n items {\n id\n name\n kpis {\n id\n name\n value {\n value\n valueDate\n }\n targetValue\n unit\n grades\n vizType\n vizMeta\n histories(limit: 1) {\n version\n updatedAt\n updater {\n name\n }\n }\n }\n }\n }\n kpiAlerts {\n id\n kpi {\n id\n }\n message\n level\n createdAt\n }\n }\n `\n })\n this.categories = response.data.kpiCategories.items || []\n this.alerts = response.data.kpiAlerts || []\n } catch (e) {\n this.error = 'KPI 카테고리 데이터를 불러오지 못했습니다.'\n } finally {\n this.loading = false\n }\n }\n\n async openHistoryModal(kpi) {\n // 전체 이력 fetch (limit 없이)\n try {\n const response = await client.query({\n query: gql`\n query ($kpiId: String!) {\n kpi(id: $kpiId) {\n name\n histories {\n version\n updatedAt\n updater {\n name\n }\n }\n }\n }\n `,\n variables: { kpiId: kpi.id }\n })\n this.modalHistories = response.data.kpi.histories || []\n this.modalKpiName = response.data.kpi.name || ''\n this.showHistoryModal = true\n } catch (e) {\n alert('이력 데이터를 불러오지 못했습니다.')\n }\n }\n closeHistoryModal() {\n this.showHistoryModal = false\n this.modalHistories = []\n this.modalKpiName = ''\n }\n\n _renderKpiValue(kpi: any) {\n const kpiValue = kpi.value?.value ?? 0\n const targetValue = kpi.targetValue ?? 100\n const unit = kpi.unit ?? ''\n const vizType = kpi.vizType || 'CARD'\n const vizMeta = kpi.vizMeta || {}\n const color = vizMeta.color || '#3a3ad6'\n const icon = vizMeta.icon || 'trending_up'\n const minValue = vizMeta.minValue || 0\n const maxValue = vizMeta.maxValue || 100\n const decimalPlaces = vizMeta.decimalPlaces || 0\n\n const formattedValue = typeof kpiValue === 'number' ? kpiValue.toFixed(decimalPlaces) : kpiValue\n\n switch (vizType) {\n case 'GAUGE':\n const gaugePercentage = Math.min(((kpiValue - minValue) / (maxValue - minValue)) * 100, 100)\n return html`\n <div style=\"text-align:center;margin:16px 0;\">\n <div\n style=\"width:120px;height:60px;border-radius:60px 60px 0 0;background:conic-gradient(${color} 0deg ${gaugePercentage *\n 3.6}deg, #e0e0e0 ${gaugePercentage * 3.6}deg 360deg);margin:0 auto;position:relative;\"\n >\n <div\n style=\"position:absolute;bottom:0;left:50%;transform:translateX(-50%);font-size:1.2rem;font-weight:bold;color:${color};\"\n >\n ${formattedValue}${unit}\n </div>\n </div>\n </div>\n `\n\n case 'PROGRESS':\n const progressPercentage = Math.min(((kpiValue - minValue) / (maxValue - minValue)) * 100, 100)\n return html`\n <div style=\"margin:16px 0;\">\n <div style=\"background:#e0e0e0;height:20px;border-radius:10px;overflow:hidden;\">\n <div style=\"background:${color};height:100%;width:${progressPercentage}%;transition:width 0.3s;\"></div>\n </div>\n <div style=\"text-align:center;margin-top:8px;font-weight:bold;color:${color};font-size:1.2rem;\">\n ${formattedValue}${unit}\n </div>\n </div>\n `\n\n case 'ICON':\n return html`\n <div style=\"text-align:center;margin:16px 0;\">\n <md-icon style=\"color:${color};font-size:48px;\">${icon}</md-icon>\n <div style=\"font-size:1.5rem;font-weight:bold;color:${color};margin-top:8px;\">${formattedValue}${unit}</div>\n </div>\n `\n\n case 'THERMOMETER':\n const thermoPercentage = Math.min(((kpiValue - minValue) / (maxValue - minValue)) * 100, 100)\n return html`\n <div style=\"text-align:center;margin:16px 0;\">\n <div\n style=\"width:40px;height:120px;background:#e0e0e0;border-radius:20px;margin:0 auto;position:relative;overflow:hidden;\"\n >\n <div\n style=\"position:absolute;bottom:0;width:100%;height:${thermoPercentage}%;background:${color};border-radius:20px;\"\n ></div>\n </div>\n <div style=\"font-size:1.2rem;font-weight:bold;color:${color};margin-top:8px;\">${formattedValue}${unit}</div>\n </div>\n `\n\n case 'SPEEDOMETER':\n const speedPercentage = Math.min(((kpiValue - minValue) / (maxValue - minValue)) * 100, 100)\n const angle = (speedPercentage / 100) * 180 - 90 // -90도에서 90도까지\n return html`\n <div style=\"text-align:center;margin:16px 0;\">\n <div\n style=\"width:120px;height:60px;border-radius:60px 60px 0 0;background:conic-gradient(${color} -90deg ${angle}deg, #e0e0e0 ${angle}deg 90deg);margin:0 auto;position:relative;\"\n >\n <div\n style=\"position:absolute;bottom:0;left:50%;transform:translateX(-50%);font-size:1.2rem;font-weight:bold;color:${color};\"\n >\n ${formattedValue}${unit}\n </div>\n </div>\n </div>\n `\n\n case 'BULLET':\n const bulletPercentage = Math.min(((kpiValue - minValue) / (maxValue - minValue)) * 100, 100)\n const targetPercentage = Math.min(((targetValue - minValue) / (maxValue - minValue)) * 100, 100)\n return html`\n <div style=\"margin:16px 0;\">\n <div style=\"background:#e0e0e0;height:30px;border-radius:15px;overflow:hidden;position:relative;\">\n <div style=\"background:${color};height:100%;width:${bulletPercentage}%;transition:width 0.3s;\"></div>\n <div\n style=\"position:absolute;top:0;left:${targetPercentage}%;width:2px;height:100%;background:#333;\"\n ></div>\n </div>\n <div style=\"text-align:center;margin-top:8px;font-weight:bold;color:${color};font-size:1.2rem;\">\n ${formattedValue}${unit}\n </div>\n </div>\n `\n\n case 'TEXT':\n return html`\n <div style=\"text-align:center;margin:16px 0;\">\n <div style=\"font-size:1.5rem;font-weight:bold;color:${color};\">${formattedValue}${unit}</div>\n </div>\n `\n\n case 'BADGE':\n return html`\n <div style=\"text-align:center;margin:16px 0;\">\n <span\n style=\"display:inline-block;padding:8px 16px;background:${color};color:white;border-radius:20px;font-size:1.2rem;font-weight:bold;\"\n >${formattedValue}${unit}</span\n >\n </div>\n `\n\n default: // CARD, BAR, LINE, PIE, DONUT, RADAR, TABLE\n return html` <div class=\"kpi-value\">${formattedValue}${unit}</div> `\n }\n }\n\n get context() {\n return {\n title: 'KPI 대시보드',\n description: '조직 KPI 실적, 등급, 이력, 시각화 등 KPI 전용 대시보드'\n } as any\n }\n\n render() {\n if (this.loading) return nothing\n if (this.error) return html`<div>${this.error}</div>`\n return html`\n <div class=\"dashboard-root\">\n ${this.showHistoryModal\n ? html`\n <div\n style=\"position:fixed;top:0;left:0;width:100vw;height:100vh;background:rgba(0,0,0,0.25);z-index:1000;display:flex;align-items:center;justify-content:center;\"\n >\n <div\n style=\"background:#fff;padding:32px 40px;border-radius:16px;min-width:340px;max-width:90vw;max-height:80vh;overflow:auto;box-shadow:0 4px 24px rgba(0,0,0,0.15);\"\n >\n <div style=\"font-size:1.2rem;font-weight:bold;margin-bottom:16px;\">\n ${this.modalKpiName} 변경 이력\n </div>\n <ul style=\"padding:0;list-style:none;\">\n ${this.modalHistories.length === 0\n ? html`<li style=\"color:#bbb;\">이력이 없습니다.</li>`\n : this.modalHistories.map(\n h =>\n html`<li style=\"margin-bottom:8px;\">\n v${h.version} (${h.updatedAt?.slice(0, 10) ?? ''} ${h.updater?.name ?? ''})\n </li>`\n )}\n </ul>\n <button style=\"margin-top:16px;padding:6px 18px;\" @click=${() => this.closeHistoryModal()}>\n 닫기\n </button>\n </div>\n </div>\n `\n : nothing}\n ${this.categories.map(\n cat => html`\n <div class=\"category-section\">\n <div class=\"category-title\">${cat.name}</div>\n <div class=\"kpi-cards\">\n ${(cat.kpis || []).map(\n kpi => html`\n <div class=\"kpi-card\">\n <div class=\"kpi-name\">${kpi.name}</div>\n ${this._renderKpiValue(kpi)}\n <div class=\"kpi-target\">목표: ${kpi.targetValue ?? '-'}${kpi.unit ?? ''}</div>\n <!-- 등급 시각화 -->\n <div style=\"margin-top:8px;\">\n <b>등급:</b>\n ${(() => {\n const grades = kpi.grades || []\n const kpiValue = kpi.value?.value\n\n if (grades.length > 5) {\n // 5개를 넘으면 현재 값에 해당하는 grade만 표시\n const currentGrade = grades.find(g => kpiValue >= g.minValue && kpiValue <= g.maxValue)\n if (currentGrade) {\n return html`<span\n style=\"display:inline-block;margin-right:8px;padding:4px 12px;border-radius:12px;background:${currentGrade.color ??\n '#3a3ad6'};color:white;font-size:1em;font-weight:bold;box-shadow:0 2px 8px rgba(58,58,214,0.3);border:2px solid ${currentGrade.color ??\n '#3a3ad6'};\"\n >${currentGrade.name}(${currentGrade.minValue}~${currentGrade.maxValue}${kpi.unit ??\n ''},\n ${currentGrade.score ?? ''}점)</span\n >`\n } else {\n return html`<span style=\"color:#bbb;font-size:0.95em;\">등급 없음</span>`\n }\n } else {\n // 5개 이하면 전체 리스트 표시하되 현재 등급은 강조\n return grades.map(g => {\n const isCurrentGrade = kpiValue >= g.minValue && kpiValue <= g.maxValue\n return html`<span\n style=\"display:inline-block;margin-right:8px;padding:${isCurrentGrade\n ? '4px 12px'\n : '2px 8px'};border-radius:${isCurrentGrade\n ? '12px'\n : '8px'};background:${isCurrentGrade\n ? (g.color ?? '#3a3ad6')\n : (g.color ?? '#eee')};color:${isCurrentGrade\n ? 'white'\n : '#222'};font-size:${isCurrentGrade ? '1em' : '0.95em'};font-weight:${isCurrentGrade\n ? 'bold'\n : 'normal'};box-shadow:${isCurrentGrade\n ? '0 2px 8px rgba(58,58,214,0.3)'\n : 'none'};border:${isCurrentGrade ? `2px solid ${g.color ?? '#3a3ad6'}` : 'none'};\"\n >${g.name}(${g.minValue}~${g.maxValue}${kpi.unit ?? ''}, ${g.score ?? ''}점)</span\n >`\n })\n }\n })()}\n </div>\n <!-- 최근 변경 이력(1건만) -->\n <div style=\"margin-top:8px;font-size:0.95em;color:#888;\">\n <b>최근 변경:</b>\n ${kpi.histories && kpi.histories.length > 0\n ? html`<span style=\"margin-right:8px;\"\n >v${kpi.histories[0].version} (${kpi.histories[0].updatedAt?.slice(0, 10) ?? ''}\n ${kpi.histories[0].updater?.name ?? ''})</span\n >`\n : html`<span style=\"color:#bbb\">없음</span>`}\n <button\n style=\"margin-left:8px;font-size:0.95em;padding:2px 8px;\"\n @click=${() => this.openHistoryModal(kpi)}\n >\n 더보기\n </button>\n </div>\n <!-- 경고/알림 -->\n <div style=\"margin-top:8px;\">\n ${this.alerts.filter(a => a.kpi?.id === kpi.id).length > 0\n ? this.alerts\n .filter(a => a.kpi?.id === kpi.id)\n .map(\n a =>\n html`<span\n style=\"display:inline-block;margin-right:8px;padding:2px 8px;border-radius:8px;background:${a.level ===\n 'critical'\n ? '#ffb3b3'\n : a.level === 'warning'\n ? '#ffe082'\n : '#e0e0e0'};color:#b71c1c;font-size:0.95em;\"\n >${a.message}</span\n >`\n )\n : html`<span style=\"color:#4caf50;font-size:0.95em;\">이상/경고 없음</span>`}\n </div>\n </div>\n `\n )}\n </div>\n </div>\n `\n )}\n </div>\n `\n }\n}\n"]}