import '@material/web/button/elevated-button.js' // import '@material/web/select/outlined-select.js' import '@material/web/textfield/outlined-text-field.js' import '@material/web/icon/icon.js' import { PageView } from '@operato/shell' import { css, html, LitElement } from 'lit' import { customElement, property } from 'lit/decorators.js' import { client } from '@operato/graphql' import { i18next, localize } from '@operato/i18n' import { notify } from '@operato/layout' import { OxPopup } from '@operato/popup' import gql from 'graphql-tag' import { CommonHeaderStyles, ScrollbarStyles } from '@operato/styles' const VIZ_TYPES = [ { value: 'CARD', label: '카드', icon: 'dashboard' }, { value: 'GAUGE', label: '게이지', icon: 'speed' }, { value: 'PROGRESS', label: '진행률', icon: 'linear_scale' }, { value: 'BAR', label: '막대 차트', icon: 'bar_chart' }, { value: 'LINE', label: '선 차트', icon: 'show_chart' }, { value: 'PIE', label: '파이 차트', icon: 'pie_chart' }, { value: 'DONUT', label: '도넛 차트', icon: 'donut_large' }, { value: 'RADAR', label: '레이더 차트', icon: 'radar' }, { value: 'BULLET', label: '불릿 차트', icon: 'track_changes' }, { value: 'THERMOMETER', label: '온도계', icon: 'thermostat' }, { value: 'SPEEDOMETER', label: '속도계', icon: 'speed' }, { value: 'ICON', label: '아이콘', icon: 'emoji_events' }, { value: 'BADGE', label: '배지', icon: 'badge' }, { value: 'TEXT', label: '텍스트', icon: 'text_fields' }, { value: 'TABLE', label: '테이블', icon: 'table_chart' } ] @customElement('kpi-viz-editor') export class KpiVizEditor extends localize(i18next)(LitElement) { static styles = [ CommonHeaderStyles, ScrollbarStyles, css` :host { display: flex; flex-direction: column; background-color: var(--md-sys-color-surface, #f4f6fa); } .viz-editor { flex: 1; display: flex; flex-direction: column; overflow-y: auto; background: white; padding: 24px; } .form-group { margin-bottom: 20px; } .form-group label { display: block; margin-bottom: 8px; font-weight: 500; color: #555; } .form-options { flex: 1; display: flex; flex-direction: row; } .viz-type-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap: 12px; margin-bottom: 20px; } .viz-type-option { display: flex; flex-direction: column; align-items: center; padding: 16px 12px; border: 2px solid #e0e0e0; border-radius: 8px; cursor: pointer; transition: all 0.2s; text-align: center; } .viz-type-option:hover { border-color: #2196f3; background: #f5f9ff; } .viz-type-option.selected { border-color: #2196f3; background: #e3f2fd; } .viz-type-option md-icon { font-size: 24px; margin-bottom: 8px; color: #666; } .viz-type-option.selected md-icon { color: #2196f3; } .viz-type-option .label { font-size: 0.9rem; font-weight: 500; color: #333; } .viz-meta-section { margin-top: 24px; padding-top: 20px; border-top: 1px solid #eee; } .color-picker { display: flex; gap: 12px; align-items: center; margin-bottom: 16px; } .color-input { width: 60px; height: 40px; border: none; border-radius: 6px; cursor: pointer; } .buttons { display: flex; gap: 12px; justify-content: flex-end; margin-top: 24px; padding-top: 20px; border-top: 1px solid #eee; } .preview { flex: 1; margin: 30px 16px 16px 16px; padding: 16px; background: #f8f9fa; border-radius: 8px; border: 1px solid #e9ecef; } .preview h4 { margin: 0 0 12px 0; color: #495057; font-size: 0.95rem; } .footer span { font-size: 0.8em; color: var(--md-sys-color-on-surface); line-height: 1.5; padding: 10px; } ` ] @property({ type: Object }) kpi: any = null @property({ type: String }) selectedVizType: string = 'CARD' @property({ type: Object }) vizMeta: any = {} @property({ type: Object }) onSave: Function = () => {} @property({ type: Object }) onCancel: Function = () => {} connectedCallback() { super.connectedCallback() if (this.kpi) { this.selectedVizType = this.kpi.vizType || 'CARD' this.vizMeta = this.kpi.vizMeta || {} } } _selectVizType(type: string) { this.selectedVizType = type this.requestUpdate() } _updateVizMeta(key: string, value: any) { this.vizMeta = { ...this.vizMeta, [key]: value } this.requestUpdate() } _renderPreview() { const kpiValue = this.kpi?.value?.value ?? 75 const targetValue = this.kpi?.targetValue ?? 100 const unit = this.kpi?.unit ?? '' const color = this.vizMeta.color || '#2196f3' const icon = this.vizMeta.icon || 'trending_up' const min = this.vizMeta.minValue ?? 0 const max = this.vizMeta.maxValue ?? 100 switch (this.selectedVizType) { case 'CARD': return html`
${icon}
${kpiValue}${unit}
목표: ${targetValue}${unit}
` case 'GAUGE': { const value = Math.max(min, Math.min(kpiValue, max)) const percent = max - min > 0 ? (value - min) / (max - min) : 0 const r = 60 const cx = 90 const cy = 90 const startX = cx - r const startY = cy const endX = cx + r * Math.cos(Math.PI * (1 - percent)) const endY = cy - r * Math.sin(Math.PI * (1 - percent)) const needleAngle = Math.PI - Math.PI * percent const needleX = cx + r * Math.cos(needleAngle) const needleY = cy - r * Math.sin(needleAngle) return html`
${value}${unit} ${min} ${max}
` } case 'SPEEDOMETER': { const value = Math.max(min, Math.min(kpiValue, max)) const percent = max - min > 0 ? (value - min) / (max - min) : 0 const r = 60 const cx = 90 const cy = 90 const startX = cx - r const startY = cy const endX = cx + r * Math.cos(Math.PI * (1 - percent)) const endY = cy - r * Math.sin(Math.PI * (1 - percent)) const needleAngle = Math.PI - Math.PI * percent const needleX = cx + r * Math.cos(needleAngle) const needleY = cy - r * Math.sin(needleAngle) // 중간 눈금 (5개) const ticks = Array.from({ length: 6 }, (_, i) => { const tickAngle = Math.PI - (Math.PI * i) / 5 const tx1 = cx + (r - 8) * Math.cos(tickAngle) const ty1 = cy - (r - 8) * Math.sin(tickAngle) const tx2 = cx + (r + 8) * Math.cos(tickAngle) const ty2 = cy - (r + 8) * Math.sin(tickAngle) const label = Math.round(min + (max - min) * (i / 5)) const lx = cx + (r + 22) * Math.cos(tickAngle) const ly = cy - (r + 22) * Math.sin(tickAngle) + 6 return { tx1, ty1, tx2, ty2, label, lx, ly } }) return html`
${ticks.map( t => html`` )} ${ticks.map( t => html`${t.label}` )} ${value}${unit} ${min} ${max}
` } case 'PROGRESS': const progressPercentage = Math.min((kpiValue / targetValue) * 100, 100) return html`
${kpiValue}${unit} / ${targetValue}${unit}
` case 'THERMOMETER': { const value = Math.max(min, Math.min(kpiValue, max)) const percent = max - min > 0 ? (value - min) / (max - min) : 0 const barHeight = 120 const barWidth = 24 const x = 100 const yTop = 30 const yBottom = yTop + barHeight const fillY = yBottom - percent * barHeight return html`
${value}${unit} ${min} ${max}
` } case 'ICON': return html`
${icon}
${kpiValue}${unit}
` default: return html`
${this.selectedVizType} 미리보기
` } } render() { return html`
${VIZ_TYPES.map( type => html`
this._selectVizType(type.value)} > ${type.icon}
${type.label}
` )}
this._updateVizMeta('color', e.target.value)} /> this._updateVizMeta('color', e.target.value)} >
this._updateVizMeta('icon', e.target.value)} >
this._updateVizMeta('minValue', parseFloat(e.target.value))} >
this._updateVizMeta('maxValue', parseFloat(e.target.value))} >
this._updateVizMeta('decimalPlaces', parseInt(e.target.value))} >

미리보기

${this._renderPreview()}
` } }