import { LitElement, html, css, nothing } from 'lit' import { customElement, property, state } from 'lit/decorators.js' import '../../../charts/kpi-radar-chart.js' import '../../../charts/kpi-boxplot-chart.js' import '../../../charts/kpi-trend-chart.js' @customElement('kpi-region-popup') export class KpiRegionPopup extends LitElement { static styles = css` :host { display: block; position: absolute; top: 0; left: 402px; width: 400px; height: 100%; background: #fff; border-right: 1px solid #e0e0e0; z-index: 1000; overflow: hidden; display: flex; flex-direction: column; box-shadow: 2px 0 8px rgba(0, 0, 0, 0.1); } .popup-content { padding: 20px; overflow-y: auto; flex: 1; } .popup-header { display: flex; justify-content: space-between; align-items: center; padding: 20px; border-bottom: 1px solid #e0e0e0; background: #fff; height: 70px; box-sizing: border-box; } .popup-title { font-size: 1.2rem; font-weight: bold; color: #333; } .popup-close { width: 32px; height: 32px; border: none; background: #fff; border-radius: 50%; cursor: pointer; display: flex; align-items: center; justify-content: center; font-size: 1.2rem; color: #666; transition: all 0.2s; } .popup-close:hover { background: #e9ecef; color: #333; } .sub-title { font-size: 1rem; font-weight: 600; margin-bottom: 16px; color: #495057; } .chart-section { background: #f8f9fa; border-radius: 8px; padding: 16px; margin-bottom: 20px; } .chart-toggle { display: flex; gap: 8px; margin-bottom: 16px; } .toggle-button { padding: 8px 16px; border: 1px solid #ced4da; background: #fff; border-radius: 6px; cursor: pointer; font-size: 0.9rem; transition: all 0.2s; } .toggle-button.active { background: #667eea; color: white; border-color: #667eea; } .chart-container { height: 300px; display: flex; align-items: center; justify-content: center; background: white; border-radius: 6px; border: 1px solid #e9ecef; } .period-selector { display: flex; gap: 8px; margin-bottom: 16px; } .period-button { padding: 6px 12px; border: 1px solid #ced4da; background: #fff; border-radius: 4px; cursor: pointer; font-size: 0.85rem; transition: all 0.2s; } .period-button.active { background: #667eea; color: white; border-color: #667eea; } .date-range-selector { display: flex; align-items: center; gap: 8px; margin-bottom: 16px; } .date-select { padding: 6px 12px; border: 1px solid #ced4da; border-radius: 4px; background: white; font-size: 0.9rem; } ` @property({ type: String }) selectedRegion: string | null = null @property({ type: String }) selectedChartType = 'boxplot' @property({ type: String }) selectedPeriod = '월' @property({ type: String }) startDate = '전체' @property({ type: String }) endDate = '전체' @state() private chartData: any[] = [] @state() private chartCategories: string[] = [] @state() private trendData: { date: string; value: number; color?: string }[] = [] connectedCallback() { super.connectedCallback() this.generateChartData() this.generateTrendData() } private generateChartData() { // 선택된 지역의 KPI 데이터 생성 const categories = ['일정 성과', '비용 성과', '품질 성과', '안전 성과', '환경 성과'] this.chartCategories = categories if (this.selectedChartType === 'radar') { // 레이더 차트용 데이터 생성 this.chartData = categories.map(category => ({ category, value: Math.random() * 50 + 25, // 25-75 범위 group: this.selectedRegion || '전체' })) } else { // 박스플롯용 데이터 생성 this.chartData = categories.map(category => { const baseValue = Math.random() * 50 + 25 // 25-75 범위 const variation = Math.random() * 20 // 변동폭 // 각 카테고리별로 20개의 데이터 포인트 생성 const dataPoints: { value: number; group: string }[] = [] for (let i = 0; i < 20; i++) { dataPoints.push({ value: baseValue + (Math.random() - 0.5) * variation, group: category }) } // 통계값 계산 const values = dataPoints.map(d => d.value).sort((a, b) => a - b) const min = Math.min(...values) const max = Math.max(...values) const q1 = values[Math.floor(values.length * 0.25)] const q3 = values[Math.floor(values.length * 0.75)] const median = values[Math.floor(values.length * 0.5)] const mean = values.reduce((a, b) => a + b, 0) / values.length return { group: category, min: min, max: max, q1: q1, q3: q3, median: median, mean: mean, value: mean } }) } } private generateTrendData() { // 최근 30일간의 트렌드 데이터 생성 const today = new Date() this.trendData = [] for (let i = 29; i >= 0; i--) { const date = new Date(today) date.setDate(date.getDate() - i) // 기본값에서 약간의 변동을 주어 트렌드 생성 const baseValue = 50 const trend = Math.sin(i * 0.2) * 10 // 사인파로 변동 const noise = (Math.random() - 0.5) * 5 // 랜덤 노이즈 const value = Math.max(0, Math.min(100, baseValue + trend + noise)) this.trendData.push({ date: date.toISOString().split('T')[0], // YYYY-MM-DD 형식 value: Math.round(value), color: value > 60 ? '#4caf50' : value > 40 ? '#ff9800' : '#f44336' }) } } private onClose() { this.dispatchEvent( new CustomEvent('popup-close', { bubbles: true, composed: true }) ) } private onChartTypeChange(type: string) { this.selectedChartType = type this.generateChartData() } private onPeriodChange(period: string) { this.dispatchEvent( new CustomEvent('period-change', { detail: { period }, bubbles: true, composed: true }) ) this.generateTrendData() } private onDateRangeChange() { this.dispatchEvent( new CustomEvent('date-range-change', { detail: { startDate: this.startDate, endDate: this.endDate }, bubbles: true, composed: true }) ) } render() { if (!this.selectedRegion) { return nothing } return html` ` } }