import '@material/web/icon/icon.js' import { css, html, LitElement } from 'lit' import { customElement, property, state } from 'lit/decorators.js' import { CommonHeaderStyles } from '@operato/styles' /** * KpiMetric.meta.input(입력 위젯 힌트) 구조화 에디터. * * metric 관리 화면(kpi-metric-list-page)에서 팝업으로 열려 meta.input 을 편집한다. * raw JSON 을 직접 만지지 않고 위젯/필드를 폼으로 구성 → 내부적으로 meta 객체 생성. * meta 의 다른 키(kind/source…)는 보존하고 input 만 교체/삭제한다. * * - `metric-meta-save` detail:{ meta } — 저장 (widget 미선택 시 input 제거된 meta) * - `metric-meta-cancel` — 취소 */ @customElement('kpi-metric-meta-editor') export class KpiMetricMetaEditor extends LitElement { static styles = [ CommonHeaderStyles, css` :host { display: flex; flex-direction: column; height: 100%; box-sizing: border-box; width: 100%; min-width: 520px; font-size: 14px; color: #212529; } /* 스크롤되는 내용부 — 버튼은 아래에 고정 (kpi-metric-value-manual-entry-form 패턴) */ .content { flex: 1; overflow-y: auto; padding: 20px 24px 8px 24px; } h3 { margin: 0 0 4px 0; font-size: 15px; } .sub { color: #6b7684; margin-bottom: 14px; } .field { display: grid; grid-template-columns: 96px 1fr; align-items: center; gap: 8px 10px; margin-bottom: 10px; } label { color: #35618e; font-weight: 500; } input[type='text'], input[type='number'], select, textarea { background-color: var(--input-field-background, var(--md-sys-color-surface-container-highest)); color: var(--input-field-color, var(--md-sys-color-on-surface)); width: 100%; box-sizing: border-box; padding: 6px 8px; border: 1px solid #cdd5df; border-radius: 5px; font-size: 13px; } textarea { min-height: 72px; resize: vertical; font-family: monospace; } .checks { display: flex; gap: 16px; flex-wrap: wrap; margin: 4px 0 12px 0; } .checks label { display: inline-flex; align-items: center; gap: 5px; color: #212529; font-weight: 400; } .hint { color: #8b95a1; font-size: 11px; grid-column: 2; margin-top: -4px; } ` ] /** { name, meta, collectType } */ @property({ type: Object }) metric: any = {} @state() widget = '' @state() min = '' @state() max = '' @state() step = '' @state() decimals = '' @state() placeholder = '' @state() optionsText = '' @state() readonly = false @state() required = false @state() hasPlan = false connectedCallback() { super.connectedCallback() const input = this.metric?.meta?.input || {} this.widget = input.widget || '' this.min = input.min !== undefined ? String(input.min) : '' this.max = input.max !== undefined ? String(input.max) : '' this.step = input.step !== undefined ? String(input.step) : '' this.decimals = input.decimals !== undefined ? String(input.decimals) : '' this.placeholder = input.placeholder || '' this.optionsText = (input.options || []).map((o: any) => `${o.value}|${o.label ?? o.value}`).join('\n') this.readonly = !!input.readonly this.required = !!input.required this.hasPlan = !!input.hasPlan } render() { const w = this.widget return html`

입력 힌트 · ${this.metric?.name || ''}

값 입력 화면(월별/완료평가 등)이 이 힌트로 위젯을 그립니다. 위젯을 비우면 힌트가 제거되어 숫자 입력으로 폴백합니다.
${w === 'number' || w === 'rating' ? html` ${w === 'number' ? html`
(this.min = e.target.value)} />
` : ''}
(this.max = e.target.value)} /> ${w === 'rating' ? html`별 개수 (기본 5)` : ''}
(this.step = e.target.value)} /> ${w === 'rating' ? html`1=정수, 0.5=반별 (기본 1)` : ''}
${w === 'number' ? html`
(this.decimals = e.target.value)} />
` : ''} ` : ''} ${w === 'select' ? html`
한 줄당 값|라벨 (라벨 생략 시 값 사용)
` : ''} ${w === 'text' ? html`
(this.placeholder = e.target.value)} />
` : ''} ${w ? html`
` : ''}
` } private _num(v: string): number | undefined { if (v === '' || v === null || v === undefined) return undefined const n = Number(v) return Number.isNaN(n) ? undefined : n } private _buildInput(): any { if (!this.widget) return undefined const input: any = { widget: this.widget } if (this.widget === 'number' || this.widget === 'rating') { const min = this._num(this.min) const max = this._num(this.max) const step = this._num(this.step) const decimals = this._num(this.decimals) if (this.widget === 'number' && min !== undefined) input.min = min if (max !== undefined) input.max = max if (step !== undefined) input.step = step if (this.widget === 'number' && decimals !== undefined) input.decimals = decimals } if (this.widget === 'select') { const options = this.optionsText .split('\n') .map(line => line.trim()) .filter(Boolean) .map(line => { const [value, label] = line.split('|').map(s => s.trim()) return { value, label: label || value } }) if (options.length) input.options = options } if (this.widget === 'text' && this.placeholder) input.placeholder = this.placeholder if (this.readonly) input.readonly = true if (this.required) input.required = true if (this.hasPlan) input.hasPlan = true return input } /** 기존 meta 는 보존하고 input 만 교체(또는 삭제). */ private _buildMeta(): any { const rest = { ...(this.metric?.meta || {}) } delete rest.input const input = this._buildInput() const meta = input ? { ...rest, input } : rest return Object.keys(meta).length ? meta : null } private _save() { this.dispatchEvent( new CustomEvent('metric-meta-save', { detail: { meta: this._buildMeta() }, bubbles: true, composed: true }) ) } private _clear() { this.widget = '' this.dispatchEvent( new CustomEvent('metric-meta-save', { detail: { meta: this._buildMeta() }, bubbles: true, composed: true }) ) } private _cancel() { this.dispatchEvent(new CustomEvent('metric-meta-cancel', { bubbles: true, composed: true })) } }