711 lines
27 KiB
TypeScript
711 lines
27 KiB
TypeScript
'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
|
||
name: string
|
||
category: string
|
||
breedVal: number
|
||
percentile: number
|
||
description: string
|
||
actualValue: number
|
||
unit: string
|
||
}
|
||
|
||
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<IntegratedStats | null>(null)
|
||
const [loading, setLoading] = useState(true)
|
||
|
||
// 형질별 순위 데이터
|
||
const [traitRank, setTraitRank] = useState<TraitRankDto | null>(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<string, string> = {
|
||
'도체중': '도체중',
|
||
'근내지방도': '근내지방도',
|
||
'등심단면적': '등심단면적',
|
||
'등지방두께': '등지방두께',
|
||
'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<number, number[]> = {}
|
||
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<number, { analyzedCount: number; avgEbv: number }>()
|
||
|
||
// 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 (
|
||
<div className="bg-white rounded-2xl border border-border p-6">
|
||
<div className="flex items-center justify-center h-[260px]">
|
||
<div className="w-8 h-8 border-2 border-border border-t-primary rounded-full animate-spin"></div>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
if (!stats) {
|
||
return (
|
||
<div className="bg-white rounded-2xl border border-border p-6">
|
||
<div className="flex items-center justify-center h-[260px] text-muted-foreground text-base">
|
||
데이터를 불러올 수 없습니다.
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
// 형질 필터에 따른 데이터 계산
|
||
const isTraitMode = chartFilterTrait !== 'overall'
|
||
|
||
// 개별 형질 모드일 때 해당 형질의 데이터 찾기
|
||
const selectedTrait = isTraitMode
|
||
? selectedTraitData.find(t => t.name === chartFilterTrait)
|
||
: null
|
||
|
||
const traitComparison = isTraitMode
|
||
? externalTraitComparisons.find(tc => tc.trait === chartFilterTrait)
|
||
: null
|
||
|
||
// 표시할 값 결정
|
||
const displayScore = isTraitMode && selectedTrait ? selectedTrait.breedVal : overallScore
|
||
const displayPercentile = isTraitMode && selectedTrait ? selectedTrait.percentile : (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 (
|
||
<div className="bg-white rounded-2xl border border-border overflow-hidden">
|
||
{/* 콘텐츠 */}
|
||
<div className="p-4 sm:p-5 lg:p-6">
|
||
<div className="flex flex-col lg:flex-row lg:items-stretch gap-4 lg:gap-6">
|
||
|
||
{/* 선발지수/형질 - 타이포 중심 */}
|
||
<div className="flex-shrink-0 flex flex-col items-center justify-center py-4 lg:py-6 lg:px-8 lg:border-r lg:border-border">
|
||
<span className="text-sm text-muted-foreground mb-2">{displayLabel}</span>
|
||
<span className={`text-4xl sm:text-5xl font-black tracking-tight ${displayScore >= 0 ? 'text-primary' : 'text-red-500'}`}>
|
||
{displayScore > 0 ? '+' : ''}{displayScore.toFixed(2)}
|
||
</span>
|
||
<span className="text-sm text-muted-foreground mt-2">
|
||
상위 <span className="font-semibold text-foreground">{displayPercentile.toFixed(0)}%</span>
|
||
</span>
|
||
</div>
|
||
|
||
{/* 순위 + 평균 대비 */}
|
||
<div className="flex-1 grid grid-cols-2 gap-3">
|
||
{/* 농가 내 순위 */}
|
||
<div className="bg-muted/40 rounded-xl p-3 sm:p-4 flex flex-col justify-center">
|
||
<span className="text-xs sm:text-sm text-muted-foreground">농가 내 순위</span>
|
||
<div className="mt-1">
|
||
{traitRankLoading && isTraitMode ? (
|
||
<span className="text-xl sm:text-2xl font-bold text-muted-foreground">...</span>
|
||
) : (
|
||
<>
|
||
<span className="text-xl sm:text-2xl font-bold text-foreground">
|
||
{isTraitMode ? (traitRank?.farmRank || '-') : (selectionIndex?.farmRank || '-')}위
|
||
</span>
|
||
<span className="text-sm text-muted-foreground ml-1">
|
||
/ {isTraitMode ? (traitRank?.farmTotal || 0) : (selectionIndex?.farmTotal || 0)}두
|
||
</span>
|
||
</>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* 보은군 내 순위 */}
|
||
<div className="bg-muted/40 rounded-xl p-3 sm:p-4 flex flex-col justify-center">
|
||
<span className="text-xs sm:text-sm text-muted-foreground">보은군 내 순위</span>
|
||
<div className="mt-1">
|
||
{traitRankLoading && isTraitMode ? (
|
||
<span className="text-xl sm:text-2xl font-bold text-muted-foreground">...</span>
|
||
) : (
|
||
<>
|
||
<span className="text-xl sm:text-2xl font-bold text-foreground">
|
||
{isTraitMode ? (traitRank?.regionRank || '-') : (selectionIndex?.regionRank || '-')}위
|
||
</span>
|
||
<span className="text-sm text-muted-foreground ml-1">
|
||
/ {isTraitMode ? (traitRank?.regionTotal || 0) : (selectionIndex?.regionTotal || 0)}두
|
||
</span>
|
||
</>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* 농가 평균 대비 - 클릭 가능 */}
|
||
<button
|
||
onClick={() => onComparisonClick?.('farm')}
|
||
className={`rounded-xl p-3 sm:p-4 flex flex-col justify-center text-left transition-all duration-200 ${(displayScore - displayFarmAvg) >= 0 ? 'bg-amber-50' : 'bg-red-50'
|
||
} ${highlightMode === 'farm'
|
||
? 'ring-2 ring-amber-500 ring-offset-2 shadow-lg scale-[1.02]'
|
||
: 'hover:ring-2 hover:ring-amber-300 hover:shadow-md cursor-pointer'
|
||
} ${isTraitMode ? 'col-span-1' : ''}`}
|
||
>
|
||
<div className="flex items-center gap-2">
|
||
<span className="text-xs sm:text-sm text-muted-foreground">농가평균</span>
|
||
<span className="text-sm sm:text-base font-semibold text-amber-700">{displayFarmAvg > 0 ? '+' : ''}{displayFarmAvg.toFixed(2)}</span>
|
||
{highlightMode === 'farm' && (
|
||
<span className="ml-auto text-[10px] bg-amber-500 text-white px-1.5 py-0.5 rounded-full font-medium">비교중</span>
|
||
)}
|
||
</div>
|
||
<div className="mt-1 flex items-baseline gap-1">
|
||
<span className={`text-xl sm:text-2xl font-black ${(displayScore - displayFarmAvg) >= 0 ? 'text-amber-600' : 'text-red-500'}`}>
|
||
농가 대비 {(displayScore - displayFarmAvg) >= 0 ? '+' : ''}{(displayScore - displayFarmAvg).toFixed(2)}
|
||
</span>
|
||
<span className={`text-sm font-medium ${(displayScore - displayFarmAvg) >= 0 ? 'text-amber-600' : 'text-red-500'}`}>
|
||
{(displayScore - displayFarmAvg) >= 0 ? '높음' : '낮음'}
|
||
</span>
|
||
</div>
|
||
<span className="text-[10px] text-muted-foreground mt-1">클릭하여 분포 확인 →</span>
|
||
</button>
|
||
|
||
{/* 보은군 평균 대비 - 클릭 가능 */}
|
||
<button
|
||
onClick={() => onComparisonClick?.('region')}
|
||
className={`rounded-xl p-3 sm:p-4 flex flex-col justify-center text-left transition-all duration-200 ${(displayScore - displayRegionAvg) >= 0 ? 'bg-blue-50' : 'bg-red-50'
|
||
} ${highlightMode === 'region'
|
||
? 'ring-2 ring-blue-500 ring-offset-2 shadow-lg scale-[1.02]'
|
||
: 'hover:ring-2 hover:ring-blue-300 hover:shadow-md cursor-pointer'
|
||
} ${isTraitMode ? 'col-span-1' : ''}`}
|
||
>
|
||
<div className="flex items-center gap-2">
|
||
<span className="text-xs sm:text-sm text-muted-foreground">보은군평균</span>
|
||
<span className="text-sm sm:text-base font-semibold text-blue-700">{displayRegionAvg > 0 ? '+' : ''}{displayRegionAvg.toFixed(2)}</span>
|
||
{highlightMode === 'region' && (
|
||
<span className="ml-auto text-[10px] bg-blue-500 text-white px-1.5 py-0.5 rounded-full font-medium">비교중</span>
|
||
)}
|
||
</div>
|
||
<div className="mt-1 flex items-baseline gap-1">
|
||
<span className={`text-xl sm:text-2xl font-black ${(displayScore - displayRegionAvg) >= 0 ? 'text-blue-600' : 'text-red-500'}`}>
|
||
보은군 대비 {(displayScore - displayRegionAvg) >= 0 ? '+' : ''}{(displayScore - displayRegionAvg).toFixed(2)}
|
||
</span>
|
||
<span className={`text-sm font-medium ${(displayScore - displayRegionAvg) >= 0 ? 'text-blue-600' : 'text-red-500'}`}>
|
||
{(displayScore - displayRegionAvg) >= 0 ? '높음' : '낮음'}
|
||
</span>
|
||
</div>
|
||
<span className="text-[10px] text-muted-foreground mt-1">클릭하여 분포 확인 →</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|