import { html, css } from 'lit' import { customElement, state } from 'lit/decorators.js' import { LitElement } from 'lit' import { client } from '@operato/graphql' import gql from 'graphql-tag' @customElement('kpi-level2-comparison') export class KpiLevel2Comparison extends LitElement { static styles = css` :host { display: block; } .comparison-container { background: #fff; border-radius: 16px; padding: 24px; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08); border: 1px solid #e0e0e0; width: 100%; } .comparison-title { font-size: 1.3rem; font-weight: bold; margin-bottom: 20px; color: #333; text-align: center; } .charts-section { display: flex; gap: 24px; margin-bottom: 20px; } .chart-card { flex: 1; background: #f8f9fa; border-radius: 12px; padding: 20px; border: 1px solid #e9ecef; } .chart-title { font-size: 1.1rem; font-weight: 600; margin-bottom: 16px; color: #495057; text-align: center; } .chart-container { height: 300px; display: flex; align-items: center; justify-content: center; } .loading { color: #666; font-size: 1rem; } .error { color: #d32f2f; font-size: 1rem; } .no-data { color: #666; font-size: 1rem; text-align: center; } .summary-info { display: flex; justify-content: space-around; padding: 16px; background: #f8f9fa; border-radius: 8px; margin-top: 16px; } .summary-item { text-align: center; } .summary-value { font-size: 1.2rem; font-weight: bold; color: #333; margin-bottom: 4px; } .summary-label { font-size: 0.9rem; color: #666; } ` @state() loading = true @state() error = '' @state() radarData: any[] = [] @state() boxplotData: any[] = [] @state() categories: string[] = [] @state() currentMonth = '' @state() totalCategories = 0 @state() totalKpis = 0 @state() averageScore = 0 connectedCallback() { super.connectedCallback() this.currentMonth = this.getCurrentMonth() this.fetchCategoryComparison() } private getCurrentMonth(): string { const now = new Date() const year = now.getFullYear() const month = String(now.getMonth() + 1).padStart(2, '0') return `${year}-${month}` } async fetchCategoryComparison() { this.loading = true this.error = '' try { const response = await client.query({ query: gql` query { kpiStatistics { items { id valueDate periodType mean median standardDeviation minimum maximum percentile25 percentile75 kpi { id name targetValue unit category: parent { id name } } } } } ` }) const statistics = response.data.kpiStatistics.items || [] // MONTH 타입의 현재 월 데이터만 필터링 const currentMonthStats = statistics.filter( stat => stat.periodType === 'MONTH' && stat.valueDate === this.currentMonth ) if (currentMonthStats.length === 0) { this.error = '현재 월의 데이터가 없습니다.' return } // 카테고리별로 데이터 그룹화 const categoryStats = new Map() currentMonthStats.forEach(stat => { if (stat.kpi?.category?.name) { const categoryName = stat.kpi.category.name if (!categoryStats.has(categoryName)) { categoryStats.set(categoryName, []) } categoryStats.get(categoryName)!.push(stat) } }) this.categories = Array.from(categoryStats.keys()) this.totalCategories = this.categories.length this.totalKpis = currentMonthStats.length // 레이더 차트 데이터 생성 this.generateRadarData(categoryStats) // 박스플롯 데이터 생성 this.generateBoxplotData(categoryStats) // 평균 점수 계산 const scores = currentMonthStats.map(stat => { const mean = stat.mean || 0 const targetValue = stat.kpi?.targetValue || 100 if (targetValue === 0) return 0 const achievement = Math.min((mean / targetValue) * 100, 100) return Math.max(achievement, 0) }) this.averageScore = Math.round(scores.reduce((sum, score) => sum + score, 0) / scores.length) } catch (e) { console.error('카테고리 비교 데이터를 불러오지 못했습니다:', e) this.error = '카테고리 비교 데이터를 불러오지 못했습니다.' } finally { this.loading = false } } private generateRadarData(categoryStats: Map) { const result: any[] = [] this.categories.forEach(category => { const stats = categoryStats.get(category)! const means = stats.map(s => s.mean || 0).filter(v => v > 0) const medians = stats.map(s => s.median || 0).filter(v => v > 0) const stdDevs = stats.map(s => s.standardDeviation || 0).filter(v => v > 0) if (means.length > 0) { result.push({ group: '평균', category, value: means.reduce((a, b) => a + b, 0) / means.length }) } if (medians.length > 0) { result.push({ group: '중앙값', category, value: medians.reduce((a, b) => a + b, 0) / medians.length }) } if (stdDevs.length > 0) { result.push({ group: '표준편차', category, value: stdDevs.reduce((a, b) => a + b, 0) / stdDevs.length }) } }) this.radarData = result } private generateBoxplotData(categoryStats: Map) { const result: any[] = [] this.categories.forEach(category => { const stats = categoryStats.get(category)! // 각 KPI의 평균값들을 수집 const allMeans = stats.map(s => s.mean || 0).filter(v => v > 0) const allMedians = stats.map(s => s.median || 0).filter(v => v > 0) const allMins = stats.map(s => s.minimum || 0).filter(v => v > 0) const allMaxs = stats.map(s => s.maximum || 0).filter(v => v > 0) const allQ1s = stats.map(s => s.percentile25 || 0).filter(v => v > 0) const allQ3s = stats.map(s => s.percentile75 || 0).filter(v => v > 0) if (allMeans.length > 0) { const sortedMeans = [...allMeans].sort((a, b) => a - b) const min = sortedMeans[0] const max = sortedMeans[sortedMeans.length - 1] const mean = allMeans.reduce((a, b) => a + b, 0) / allMeans.length const median = allMedians.length > 0 ? allMedians.reduce((a, b) => a + b, 0) / allMedians.length : mean const q1 = sortedMeans[Math.floor(sortedMeans.length / 4)] const q3 = sortedMeans[Math.floor((sortedMeans.length * 3) / 4)] result.push({ group: category, min, q1, median, q3, max, mean, value: mean }) } }) this.boxplotData = result } render() { if (this.loading) { return html`
카테고리 비교 분석
레이더 차트
데이터 로딩 중...
박스플롯
데이터 로딩 중...
` } if (this.error) { return html`
카테고리 비교 분석
레이더 차트
${this.error}
박스플롯
${this.error}
` } return html`
카테고리 비교 분석 (${this.currentMonth})
레이더 차트
${this.radarData.length > 0 ? html`` : html`
데이터가 없습니다.
`}
박스플롯
${this.boxplotData.length > 0 ? html`` : html`
데이터가 없습니다.
`}
${this.totalCategories}
카테고리
${this.totalKpis}
KPI
${this.averageScore}
평균 점수
` } }