'use client' import { useEffect, useState } from "react" import apiClient from "@/lib/api-client" import { genomeApi, TraitRankDto } from "@/lib/api/genome.api" import { useFilterStore } from "@/store/filter-store" import { useAnalysisYear } from "@/contexts/AnalysisYearContext" import { CowNumberDisplay } from "@/components/common/cow-number-display" import { ALL_TRAITS } from "@/constants/traits" // 분포 데이터 타입 interface DistributionBin { range: string count: number // 보은군 전체 두수 farmCount: number // 우리 농가 두수 min: number max: number } // 형질 데이터 타입 interface GenomicTrait { id?: number traitName?: string traitCategory?: string breedVal?: number percentile?: number traitVal?: number } interface GenomeIntegratedComparisonProps { farmNo: number | null cowNo?: string // 선발지수 데이터 추가 selectionIndex?: { score: number | null percentile: number | null farmRank: number | null farmTotal: number regionRank: number | null regionTotal: number regionName: string | null farmerName: string | null } | null overallScore?: number // 분포 데이터 콜백 onDistributionDataChange?: (data: { distributionData: DistributionBin[] totalCowCount: number farmCowCount: number farmAvgScore: number // 우리농장 평균 선발지수 regionAvgScore: number // 보은군 평균 선발지수 traitComparisons: TraitComparison[] // 형질별 농가/보은군 평균 비교 }) => void // 하이라이트 모드 (농가/보은군 비교 클릭 시) highlightMode?: 'farm' | 'region' | null onComparisonClick?: (mode: 'farm' | 'region') => void // 차트 형질 필터 연동 chartFilterTrait?: string selectedTraitData?: GenomicTrait[] traitComparisons?: TraitComparison[] } export interface TraitComparison { trait: string shortName: string myFarm: number region: number diff: number } interface IntegratedStats { farmBreedVal: number farmPercentile: number regionBreedVal: number regionPercentile: number difference: number selectedTraitCount: number totalCowCount: number traitComparisons: TraitComparison[] // 농장 순위 관련 farmRank: number totalFarmCount: number topPercent: number regionTopPercent: number farmAvgTopPercent: number // 우리 농가 평균 퍼센트 } // 유전체 종합보고서 보은군 내 농장 순위 가로바 차트 export function GenomeIntegratedComparison({ farmNo, cowNo, selectionIndex, overallScore = 0, onDistributionDataChange, highlightMode, onComparisonClick, chartFilterTrait = 'overall', selectedTraitData = [], traitComparisons: externalTraitComparisons = [] }: GenomeIntegratedComparisonProps) { // =======================개체번호 포맷팅: KOR 제외 + 002 1696 8353 8 형식====================== // 개체번호 포맷팅 함수 formatCowNo / 유전체 보은 군 내 농장 순위 가로바 차트에서 사용 const formatCowNo = (no?: string) => { if (!no) return '' // KOR 제거 const numOnly = no.replace(/^KOR/i, '') // 002 1696 8353 8 형식으로 포맷팅 if (numOnly.length === 12) { return `${numOnly.slice(0, 3)} ${numOnly.slice(3, 7)} ${numOnly.slice(7, 11)} ${numOnly.slice(11)}` } return numOnly } //=========================================================================================== const { filters } = useFilterStore() const { selectedYear } = useAnalysisYear() const [stats, setStats] = useState(null) const [loading, setLoading] = useState(true) // 형질별 순위 데이터 const [traitRank, setTraitRank] = useState(null) const [traitRankLoading, setTraitRankLoading] = useState(false) // 연도별 추이 데이터 const [yearlyTrendData, setYearlyTrendData] = useState<{ year: number analyzedCount: number // 분석 두수 avgEbv: number // 평균 표준화 육종가 }[]>([]) const [trendLoading, setTrendLoading] = useState(true) // 형질 조건 생성 (형질명 + 가중치) const getTraitConditions = () => { const selected = Object.entries(filters.traitWeights) .filter(([_, weight]) => weight > 0) .map(([traitNm, weight]) => ({ traitNm, weight })) // 선택된 형질이 없으면 전체 35개 형질에 가중치 1 적용 if (selected.length === 0) { return ALL_TRAITS.map(traitNm => ({ traitNm, weight: 1 })) } return selected } const traitShortNames: Record = { '도체중': '도체중', '근내지방도': '근내지방도', '등심단면적': '등심단면적', '등지방두께': '등지방두께', '12개월령체중': '12개월령체중', // 체형형질 '체고': '체고', '십자': '십자', '체장': '체장', '흉심': '흉심', '흉폭': '흉폭', '고장': '고장', '요각폭': '요각폭', '좌골폭': '좌골폭', '곤폭': '곤폭', '흉위': '흉위', // 부위별무게 '안심weight': '안심무게', '등심weight': '등심무게', '채끝weight': '채끝무게', '목심weight': '목심무게', '앞다리weight': '앞다리무게', '우둔weight': '우둔무게', '설도weight': '설도무게', '사태weight': '사태무게', '양지weight': '양지무게', '갈비weight': '갈비무게', // 부위별비율 '안심rate': '안심비율', '등심rate': '등심비율', '채끝rate': '채끝비율', '목심rate': '목심비율', '앞다리rate': '앞다리비율', '우둔rate': '우둔비율', '설도rate': '설도비율', '사태rate': '사태비율', '양지rate': '양지비율', '갈비rate': '갈비비율', } useEffect(() => { const fetchIntegratedStats = async () => { if (!farmNo) { setLoading(false) return } setLoading(true) try { const traitConditions = getTraitConditions() // API 2번만 호출 (병렬 처리) const [farmResponse, globalResponse] = await Promise.all([ // 1. 내 농장 데이터 apiClient.post('/cow/ranking', { filterOptions: { farmNo }, rankingOptions: { criteriaType: 'GENOME', traitConditions } }), // 2. 전체 유저(보은군) 데이터 apiClient.post('/cow/ranking/global', { rankingOptions: { criteriaType: 'GENOME', traitConditions } }) ]) const farmResult = farmResponse.data || farmResponse const globalResult = globalResponse.data || globalResponse // 분석완료 개체만 필터링 (sortValue !== null) const farmItems = (farmResult.items || []).filter((item: any) => item.sortValue !== null) const globalItems = (globalResult.items || []).filter((item: any) => item.sortValue !== null) if (farmItems.length === 0) { setStats(null) return } // 내 농장 평균 (필터 가중치 적용된 선발지수의 평균) const farmScores = farmItems.map((item: any) => item.sortValue || 0) const farmBreedVal = farmScores.reduce((sum: number, s: number) => sum + s, 0) / farmScores.length // 전체 유저(보은군) 평균 (필터 가중치 적용된 선발지수의 평균) const globalScores = globalItems.map((item: any) => item.sortValue || 0) const regionBreedVal = globalScores.length > 0 ? globalScores.reduce((sum: number, s: number) => sum + s, 0) / globalScores.length : 0 // 형질별 비교 데이터 생성 - 개체별 traits에서 형질별 평균 계산 const selectedTraitNames = traitConditions.map(t => t.traitNm) const traitComparisons: TraitComparison[] = selectedTraitNames.map(traitNm => { // 내 농장 형질별 평균 // ranking.traits 배열에서 traitName으로 찾아서 traitEbv 값 사용 const farmTraitValues = farmItems .map((item: any) => { const traitsArray = item.ranking?.traits || [] const trait = traitsArray.find((t: any) => t.traitName === traitNm) return trait?.traitEbv ?? null }) .filter((v: any) => v !== null) const farmTraitAvg = farmTraitValues.length > 0 ? farmTraitValues.reduce((sum: number, v: number) => sum + v, 0) / farmTraitValues.length : 0 // 전체 유저 형질별 평균 const globalTraitValues = globalItems .map((item: any) => { const traitsArray = item.ranking?.traits || [] const trait = traitsArray.find((t: any) => t.traitName === traitNm) return trait?.traitEbv ?? null }) .filter((v: any) => v !== null) const globalTraitAvg = globalTraitValues.length > 0 ? globalTraitValues.reduce((sum: number, v: number) => sum + v, 0) / globalTraitValues.length : 0 return { trait: traitNm, shortName: traitShortNames[traitNm] || traitNm.slice(0, 4), myFarm: parseFloat(farmTraitAvg.toFixed(2)), region: parseFloat(globalTraitAvg.toFixed(2)), diff: parseFloat((farmTraitAvg - globalTraitAvg).toFixed(2)) } }) // 개체 단위 순위 계산 let farmRank = 1 let totalFarmCount = 1 let topPercent = 50 let regionTopPercent = 50 let farmAvgTopPercent = 50 if (globalItems.length > 0) { // 전체 개체 점수 배열 (내림차순 정렬) const allCowScores = globalItems .map((item: any) => item.sortValue || 0) .sort((a: number, b: number) => b - a) const totalCowCount = allCowScores.length // 농가 평균(farmBreedVal)이 전체 개체 중 상위 몇 %인지 계산 const farmAvgRank = allCowScores.filter((score: number) => score > farmBreedVal).length + 1 farmAvgTopPercent = Math.round((farmAvgRank / totalCowCount) * 100) // 보은군 평균(regionBreedVal)이 전체 개체 중 상위 몇 %인지 계산 const regionRank = allCowScores.filter((score: number) => score > regionBreedVal).length + 1 regionTopPercent = Math.round((regionRank / totalCowCount) * 100) // 농장별 그룹핑 (농장 순위용) const farmScoresMap: Record = {} globalItems.forEach((item: any) => { const itemFarmNo = item.entity?.farmNo || item.entity?.farm?.pkFarmNo || item.entity?.pkFarmNo || item.farmNo || item.entity?.fkFarmNo if (itemFarmNo) { if (!farmScoresMap[itemFarmNo]) { farmScoresMap[itemFarmNo] = [] } farmScoresMap[itemFarmNo].push(item.sortValue || 0) } }) // 각 농장의 평균 계산 및 정렬 const farmAverages = Object.entries(farmScoresMap) .map(([fNo, scores]) => ({ farmNo: parseInt(fNo), avg: scores.reduce((sum, s) => sum + s, 0) / scores.length })) .sort((a, b) => b.avg - a.avg) totalFarmCount = farmAverages.length || 1 const myFarmIndex = farmAverages.findIndex(f => f.farmNo === farmNo) farmRank = myFarmIndex >= 0 ? myFarmIndex + 1 : 1 topPercent = Math.round((farmRank / totalFarmCount) * 100) } // 분포 데이터 계산 (히스토그램용) if (onDistributionDataChange && globalItems.length > 0) { const bins: DistributionBin[] = [ { range: '-3σ ~ -2.5σ', min: -3, max: -2.5, count: 0, farmCount: 0 }, { range: '-2.5σ ~ -2σ', min: -2.5, max: -2, count: 0, farmCount: 0 }, { range: '-2σ ~ -1.5σ', min: -2, max: -1.5, count: 0, farmCount: 0 }, { range: '-1.5σ ~ -1σ', min: -1.5, max: -1, count: 0, farmCount: 0 }, { range: '-1σ ~ -0.5σ', min: -1, max: -0.5, count: 0, farmCount: 0 }, { range: '-0.5σ ~ 0σ', min: -0.5, max: 0, count: 0, farmCount: 0 }, { range: '0σ ~ 0.5σ', min: 0, max: 0.5, count: 0, farmCount: 0 }, { range: '0.5σ ~ 1σ', min: 0.5, max: 1, count: 0, farmCount: 0 }, { range: '1σ ~ 1.5σ', min: 1, max: 1.5, count: 0, farmCount: 0 }, { range: '1.5σ ~ 2σ', min: 1.5, max: 2, count: 0, farmCount: 0 }, { range: '2σ ~ 2.5σ', min: 2, max: 2.5, count: 0, farmCount: 0 }, { range: '2.5σ ~ 3σ', min: 2.5, max: 3, count: 0, farmCount: 0 }, ] // 전체 개체(보은군)의 선발지수를 구간별로 카운트 globalItems.forEach((item: any) => { const score = item.sortValue ?? 0 // -3 미만은 첫 번째 구간에 if (score < -3) { bins[0].count++ return } // 3 이상은 마지막 구간에 if (score >= 3) { bins[bins.length - 1].count++ return } // 일반 구간 매칭 (마지막 구간은 >= 포함) for (let i = 0; i < bins.length; i++) { const bin = bins[i] const isLastBin = i === bins.length - 1 if (isLastBin) { if (score >= bin.min && score <= bin.max) { bin.count++ break } } else { if (score >= bin.min && score < bin.max) { bin.count++ break } } } }) // 우리 농가 개체의 선발지수를 구간별로 카운트 farmItems.forEach((item: any) => { const score = item.sortValue ?? 0 // -3 미만은 첫 번째 구간에 if (score < -3) { bins[0].farmCount++ return } // 3 이상은 마지막 구간에 if (score >= 3) { bins[bins.length - 1].farmCount++ return } // 일반 구간 매칭 (마지막 구간은 >= 포함) for (let i = 0; i < bins.length; i++) { const bin = bins[i] const isLastBin = i === bins.length - 1 if (isLastBin) { if (score >= bin.min && score <= bin.max) { bin.farmCount++ break } } else { if (score >= bin.min && score < bin.max) { bin.farmCount++ break } } } }) onDistributionDataChange({ distributionData: bins, totalCowCount: selectionIndex?.regionTotal || globalItems.length, farmCowCount: selectionIndex?.farmTotal || farmItems.length, farmAvgScore: farmBreedVal, regionAvgScore: regionBreedVal, traitComparisons }) } setStats({ farmBreedVal: parseFloat(farmBreedVal.toFixed(2)), farmPercentile: normalCdfToPercentile(farmBreedVal), regionBreedVal: parseFloat(regionBreedVal.toFixed(2)), regionPercentile: normalCdfToPercentile(regionBreedVal), difference: parseFloat((farmBreedVal - regionBreedVal).toFixed(2)), selectedTraitCount: traitConditions.length, totalCowCount: farmItems.length, traitComparisons, farmRank, totalFarmCount, topPercent, regionTopPercent, farmAvgTopPercent }) } catch (error) { console.error('데이터 로드 실패:', error) setStats(null) } finally { setLoading(false) } } fetchIntegratedStats() }, [farmNo, filters.traitWeights, selectionIndex?.regionTotal]) // 연도별 추이 데이터 가져오기 useEffect(() => { const fetchYearlyTrend = async () => { if (!farmNo) { setTrendLoading(false) return } setTrendLoading(true) try { const ebvStats = await genomeApi.getYearlyEbvStats(farmNo) // yearlyStats와 yearlyAvgEbv 합치기 const yearlyStats = ebvStats.yearlyStats || [] const yearlyAvgEbv = ebvStats.yearlyAvgEbv || [] // 연도별 데이터 맵 생성 const yearMap = new Map() // yearlyStats에서 분석 두수 가져오기 yearlyStats.forEach(stat => { yearMap.set(stat.year, { analyzedCount: stat.analyzedCount || 0, avgEbv: 0 }) }) // yearlyAvgEbv에서 평균 육종가 가져오기 yearlyAvgEbv.forEach(avg => { if (yearMap.has(avg.year)) { yearMap.get(avg.year)!.avgEbv = avg.farmAvgEbv } else { yearMap.set(avg.year, { analyzedCount: 0, avgEbv: avg.farmAvgEbv }) } }) // 배열로 변환하고 연도 오름차순 정렬 const trendData = Array.from(yearMap.entries()) .map(([year, data]) => ({ year, analyzedCount: data.analyzedCount, avgEbv: data.avgEbv })) .sort((a, b) => a.year - b.year) setYearlyTrendData(trendData) } catch (error) { console.error('[연도별추이] 데이터 로드 실패:', error) setYearlyTrendData([]) } finally { setTrendLoading(false) } } fetchYearlyTrend() }, [farmNo]) // 형질별 순위 조회 (형질 필터 변경 시) useEffect(() => { const fetchTraitRank = async () => { // 전체 선발지수 모드면 순위 조회 안 함 if (chartFilterTrait === 'overall' || !cowNo) { setTraitRank(null) return } setTraitRankLoading(true) try { const rankData = await genomeApi.getTraitRank(cowNo, chartFilterTrait) setTraitRank(rankData) } catch (error) { console.error('[형질순위] 데이터 로드 실패:', error) setTraitRank(null) } finally { setTraitRankLoading(false) } } fetchTraitRank() }, [chartFilterTrait, cowNo]) const normalCdfToPercentile = (z: number): number => { const a1 = 0.254829592, a2 = -0.284496736, a3 = 1.421413741 const a4 = -1.453152027, a5 = 1.061405429, p = 0.3275911 const sign = z < 0 ? -1 : 1 const absZ = Math.abs(z) / Math.sqrt(2) const t = 1.0 / (1.0 + p * absZ) const y = 1.0 - (((((a5 * t + a4) * t) + a3) * t + a2) * t + a1) * t * Math.exp(-absZ * absZ) const cdf = 0.5 * (1.0 + sign * y) return Math.round((1 - cdf) * 100) } if (loading) { return (
) } if (!stats) { return (
데이터를 불러올 수 없습니다.
) } // 형질 필터에 따른 데이터 계산 const isTraitMode = chartFilterTrait !== 'overall' // 개별 형질 모드일 때 해당 형질의 데이터 찾기 const selectedTrait = isTraitMode ? selectedTraitData.find(t => t.traitName === chartFilterTrait) : null const traitComparison = isTraitMode ? externalTraitComparisons.find(tc => tc.trait === chartFilterTrait) : null // 표시할 값 결정 const displayScore = isTraitMode && selectedTrait ? (selectedTrait.breedVal ?? 0) : overallScore const displayPercentile = isTraitMode && selectedTrait ? (selectedTrait.percentile ?? 50) : (selectionIndex?.percentile || 50) // 형질 모드일 때는 API에서 가져온 평균값 사용, 없으면 traitComparison 사용 const displayFarmAvg = isTraitMode ? (traitRank?.farmAvgEbv ?? traitComparison?.myFarm ?? 0) : stats.farmBreedVal const displayRegionAvg = isTraitMode ? (traitRank?.regionAvgEbv ?? traitComparison?.region ?? 0) : stats.regionBreedVal const displayLabel = isTraitMode ? chartFilterTrait : '선발지수' return (
{/* 콘텐츠 */}
{/* 선발지수/형질 - 타이포 중심 */}
{displayLabel} = 0 ? 'text-primary' : 'text-red-500'}`}> {displayScore > 0 ? '+' : ''}{displayScore.toFixed(2)} 상위 {displayPercentile.toFixed(0)}%
{/* 순위 + 평균 대비 */}
{/* 농가 내 순위 */}
농가 내 순위
{traitRankLoading && isTraitMode ? ( ... ) : ( <> {isTraitMode ? (traitRank?.farmRank || '-') : (selectionIndex?.farmRank || '-')}위 / {isTraitMode ? (traitRank?.farmTotal || 0) : (selectionIndex?.farmTotal || 0)}두 )}
{/* 보은군 내 순위 */}
보은군 내 순위
{traitRankLoading && isTraitMode ? ( ... ) : ( <> {isTraitMode ? (traitRank?.regionRank || '-') : (selectionIndex?.regionRank || '-')}위 / {isTraitMode ? (traitRank?.regionTotal || 0) : (selectionIndex?.regionTotal || 0)}두 )}
{/* 농가 평균 대비 - 클릭 가능 */} {/* 보은군 평균 대비 - 클릭 가능 */}
) }