Files
genome2025/frontend/src/app/cow/[cowNo]/page.tsx

1924 lines
109 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client'
import { useSearchParams, useParams, useRouter } from "next/navigation"
import { AppSidebar } from "@/components/layout/app-sidebar"
import { SiteHeader } from "@/components/layout/site-header"
import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar"
import { Button } from "@/components/ui/button"
import { Card, CardContent } from "@/components/ui/card"
import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog"
import { Badge } from "@/components/ui/badge"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { useToast } from "@/hooks/use-toast"
import { useMediaQuery } from "@/hooks/use-media-query"
import { ComparisonAveragesDto, TraitComparisonAveragesDto, cowApi, genomeApi, geneApi, GeneDetail, GenomeRequestDto, mptApi, MptDto } from "@/lib/api"
import { CowDetail } from "@/types/cow.types"
import { GenomeTrait } from "@/types/genome.types"
import { useGlobalFilter } from "@/contexts/GlobalFilterContext"
import {
ArrowLeft,
BarChart3,
CheckCircle2,
Download,
Dna,
Activity,
X,
XCircle,
Search,
} from 'lucide-react'
import { Input } from "@/components/ui/input"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { useEffect, useMemo, useRef, useState } from 'react'
import { CategoryEvaluationCard } from "./genome/_components/category-evaluation-card"
import { NormalDistributionChart } from "./genome/_components/normal-distribution-chart"
import { TraitDistributionCharts } from "./genome/_components/trait-distribution-charts"
import { TraitComparison } from "./genome/_components/genome-integrated-comparison"
import { CowNumberDisplay } from "@/components/common/cow-number-display"
import { isValidGenomeAnalysis, getInvalidReason, getInvalidMessage } from "@/lib/utils/genome-analysis-config"
import { AuthGuard } from "@/components/auth/auth-guard"
import { MptTable } from "./reproduction/_components/mpt-table"
// 형질명 → 카테고리 매핑 (한우 35개 형질)
const TRAIT_CATEGORY_MAP: Record<string, string> = {
// 성장형질 (1개)
'12개월령체중': '성장',
// 생산형질 (4개)
'도체중': '생산',
'등심단면적': '생산',
'등지방두께': '생산',
'근내지방도': '생산',
// 체형형질 (10개)
'체고': '체형',
'십자': '체형',
'체장': '체형',
'흉심': '체형',
'흉폭': '체형',
'고장': '체형',
'요각폭': '체형',
'곤폭': '체형',
'좌골폭': '체형',
'흉위': '체형',
// 부위별 weight (10개)
'안심weight': '무게',
'등심weight': '무게',
'채끝weight': '무게',
'목심weight': '무게',
'앞다리weight': '무게',
'우둔weight': '무게',
'설도weight': '무게',
'사태weight': '무게',
'양지weight': '무게',
'갈비weight': '무게',
// 부위별 rate (10개)
'안심rate': '비율',
'등심rate': '비율',
'채끝rate': '비율',
'목심rate': '비율',
'앞다리rate': '비율',
'우둔rate': '비율',
'설도rate': '비율',
'사태rate': '비율',
'양지rate': '비율',
'갈비rate': '비율',
}
// 형질명으로 카테고리 찾기
function getTraitCategory(traitName: string): string {
if (TRAIT_CATEGORY_MAP[traitName]) {
return TRAIT_CATEGORY_MAP[traitName]
}
if (traitName.includes('체중') || traitName.includes('개월령')) return '성장'
if (traitName.includes('도체') || traitName.includes('등심단면적') || traitName.includes('지방') || traitName.includes('근내')) return '생산'
if (traitName.includes('weight')) return '무게'
if (traitName.includes('rate')) return '비율'
return '체형'
}
// API 데이터를 화면 표시용으로 변환
function transformGenomeData(genomeData: GenomeTrait[]) {
if (genomeData.length === 0) return []
return genomeData[0].genomeCows?.map((trait, index) => {
const traitName = trait.traitInfo?.traitNm || ''
return {
id: index + 1,
name: traitName,
category: getTraitCategory(traitName),
breedVal: trait.breedVal || 0,
percentile: trait.percentile || 0,
actualValue: trait.traitVal || 0,
unit: '',
description: trait.traitInfo?.traitDesc || '',
importance: 'medium' as 'low' | 'medium' | 'high' | 'critical',
}
}) || []
}
// 3개의 정규분포 곡선 생성
function generateMultipleDistributions(
nationwideMean: number, nationwideStd: number,
regionMean: number, regionStd: number,
farmMean: number, farmStd: number
) {
const data = []
for (let i = -30; i <= 30; i++) {
const x = i / 10
const xVal = parseFloat(x.toFixed(1))
const yNationwide = (1 / (nationwideStd * Math.sqrt(2 * Math.PI))) *
Math.exp(-0.5 * Math.pow((x - nationwideMean) / nationwideStd, 2))
const yRegion = (1 / (regionStd * Math.sqrt(2 * Math.PI))) *
Math.exp(-0.5 * Math.pow((x - regionMean) / regionStd, 2))
const yFarm = (1 / (farmStd * Math.sqrt(2 * Math.PI))) *
Math.exp(-0.5 * Math.pow((x - farmMean) / farmStd, 2))
data.push({
x: xVal,
nationwide: yNationwide * 100,
region: yRegion * 100,
farm: yFarm * 100
})
}
return data
}
export default function CowOverviewPage() {
const params = useParams()
const searchParams = useSearchParams()
const router = useRouter()
const cowNo = params.cowNo as string
const from = searchParams.get('from')
const { toast } = useToast()
const { filters } = useGlobalFilter()
const isMobile = useMediaQuery("(max-width: 640px)")
const [cow, setCow] = useState<CowDetail | null>(null)
const [genomeData, setGenomeData] = useState<GenomeTrait[]>([])
const [geneData, setGeneData] = useState<GeneDetail[]>([])
const [geneDataLoaded, setGeneDataLoaded] = useState(false) // 유전자 데이터 로드 여부
const [geneDataLoading, setGeneDataLoading] = useState(false) // 유전자 데이터 로딩 중
const [loading, setLoading] = useState(true)
const [activeTab, setActiveTab] = useState<string>('genome')
// 검사 상태
const [hasGenomeData, setHasGenomeData] = useState(false)
const [hasGeneData, setHasGeneData] = useState(false)
const [hasReproductionData, setHasReproductionData] = useState(false)
// 분석 의뢰 정보 (친자감별 결과 포함)
const [genomeRequest, setGenomeRequest] = useState<GenomeRequestDto | null>(null)
// 선발지수 상태
const [selectionIndex, setSelectionIndex] = useState<{
score: number | null;
percentile: number | null;
farmRank: number | null;
farmTotal: number;
regionRank: number | null;
regionTotal: number;
regionName: string | null;
farmerName: string | null;
farmAvgScore: number | null;
regionAvgScore: number | null;
} | null>(null)
// 분포 데이터
const [comparisonAverages, setComparisonAverages] = useState<ComparisonAveragesDto | null>(null)
const [traitComparisonAverages, setTraitComparisonAverages] = useState<TraitComparisonAveragesDto | null>(null)
const [distributionData, setDistributionData] = useState<{ range: string; count: number; farmCount: number; min: number; max: number }[]>([])
const [totalCowCount, setTotalCowCount] = useState(0)
const [, setFarmCowCount] = useState(0)
const [farmAvgScore, setFarmAvgScore] = useState(0)
const [regionAvgScore, setRegionAvgScore] = useState(0)
const [traitComparisons, setTraitComparisons] = useState<TraitComparison[]>([])
const [showAllAppliedTraits, setShowAllAppliedTraits] = useState(false)
const [isChartModalOpen, setIsChartModalOpen] = useState(false)
// 분포 곡선 표시 토글
const [showNationwide] = useState(true)
const [showRegion] = useState(true)
const [showFarm] = useState(true)
const [showAllTraits] = useState(false)
const [selectedTraits, setSelectedTraits] = useState<number[]>([])
const [geneCurrentPage, setGeneCurrentPage] = useState(1)
const GENES_PER_PAGE = 50
// 농가/보은군 비교 하이라이트 모드
const [highlightMode, setHighlightMode] = useState<'farm' | 'region' | null>(null)
const distributionChartRef = useRef<HTMLDivElement>(null)
// 필터에서 고정된 첫 번째 형질 (없으면 '도체중')
const firstPinnedTrait = filters.pinnedTraits?.[0] || '도체중'
// 차트 형질 필터 (전체 선발지수 또는 개별 형질)
// 필터 비활성 시 기본값은 첫 번째 고정 형질
const [chartFilterTrait, setChartFilterTrait] = useState<string>(() => {
return filters.isActive ? 'overall' : firstPinnedTrait
})
// 필터 활성 상태 변경 시 기본값 업데이트
useEffect(() => {
if (!filters.isActive && chartFilterTrait === 'overall') {
setChartFilterTrait(firstPinnedTrait)
}
}, [filters.isActive, firstPinnedTrait, chartFilterTrait])
// 유전자 탭 필터 상태
const [geneSearchInput, setGeneSearchInput] = useState('') // 실시간 입력값
const [geneSearchKeyword, setGeneSearchKeyword] = useState('') // 디바운스된 검색값
const [geneTypeFilter, setGeneTypeFilter] = useState<'all' | 'QTY' | 'QLT'>('all')
// 검색어 디바운스 (300ms) 실시간 필터링 너무 느림
// 타이핑이 멈추고 0.3초 후에 검색이 실행
useEffect(() => {
const timer = setTimeout(() => {
setGeneSearchKeyword(geneSearchInput)
setGeneCurrentPage(1)
}, 300)
return () => clearTimeout(timer)
}, [geneSearchInput])
const [genotypeFilter, setGenotypeFilter] = useState<'all' | 'homozygous' | 'heterozygous'>('all')
const [geneSortBy, setGeneSortBy] = useState<'snpName' | 'chromosome' | 'position' | 'snpType' | 'allele1' | 'allele2' | 'remarks'>('snpName')
const [geneSortOrder, setGeneSortOrder] = useState<'asc' | 'desc'>('asc')
// 유전자 데이터 지연 로드 함수
const loadGeneData = async () => {
if (geneDataLoaded || geneDataLoading) return // 이미 로드했거나 로딩 중이면 스킵
setGeneDataLoading(true)
try {
const geneDataResult = await geneApi.findByCowId(cowNo)
const geneList = geneDataResult || []
setGeneData(geneList)
setGeneDataLoaded(true)
} catch (geneErr) {
console.error('유전자 데이터 조회 실패:', geneErr)
setGeneData([])
setGeneDataLoaded(true)
} finally {
setGeneDataLoading(false)
}
}
// 탭 변경 핸들러
const handleTabChange = (value: string) => {
setActiveTab(value)
if (value === 'gene' && !geneDataLoaded) {
loadGeneData()
}
}
// 농가/보은군 배지 클릭 시 차트로 스크롤 + 하이라이트
const handleComparisonClick = (mode: 'farm' | 'region') => {
// 토글: 같은 모드 클릭 시 해제
setHighlightMode(prev => prev === mode ? null : mode)
// 차트로 스크롤
if (distributionChartRef.current) {
distributionChartRef.current.scrollIntoView({
behavior: 'smooth',
block: 'center'
})
}
}
const handleBack = () => {
if (from === 'ranking') {
router.push('/ranking')
} else if (from === 'list') {
router.push('/list')
} else {
router.push('/cow')
}
}
useEffect(() => {
const fetchData = async () => {
try {
setLoading(true)
const cowData = await cowApi.findOne(cowNo)
const cowDetail: CowDetail = {
...cowData,
age: cowData.cowBirthDt
? Math.floor((new Date().getTime() - new Date(cowData.cowBirthDt).getTime()) / (1000 * 60 * 60 * 24 * 365))
: undefined,
}
setCow(cowDetail)
// 유전체 데이터 가져오기
const genomeDataResult = await genomeApi.findByCowNo(cowNo)
setGenomeData(genomeDataResult)
const genomeExists = genomeDataResult.length > 0 && !!genomeDataResult[0].genomeCows && genomeDataResult[0].genomeCows.length > 0
setHasGenomeData(genomeExists)
// 분석 의뢰 정보 가져오기 (친자감별 결과 포함)
try {
const requestData = await genomeApi.getRequest(cowNo)
setGenomeRequest(requestData)
} catch (reqErr) {
console.error('분석 의뢰 정보 조회 실패:', reqErr)
setGenomeRequest(null)
}
// 유전자(SNP) 데이터는 탭 클릭 시 로드 (지연 로딩)
setHasGeneData(true) // 탭은 보여주되, 데이터는 나중에 로드
// 번식능력 데이터 (현재는 목업 - 추후 API 연동)
// TODO: 번식능력 API 연동
setHasReproductionData(false)
// 첫 번째 사용 가능한 탭 자동 선택
if (genomeExists) {
setActiveTab('genome')
} else if (geneData && geneData.length > 0) {
setActiveTab('gene')
}
// 비교 데이터 가져오기
if (genomeDataResult.length > 0) {
try {
const comparisonData = await genomeApi.getComparisonAverages(cowNo)
setComparisonAverages(comparisonData)
// 형질별 비교 평균 가져오기 (폴리곤 차트용)
const traitComparisonData = await genomeApi.getTraitComparisonAverages(cowNo)
setTraitComparisonAverages(traitComparisonData)
// 선발지수 계산
const ALL_TRAITS = [
'12개월령체중',
'도체중', '등심단면적', '등지방두께', '근내지방도',
'체고', '십자', '체장', '흉심', '흉폭', '고장', '요각폭', '좌골폭', '곤폭', '흉위',
'안심weight', '등심weight', '채끝weight', '목심weight', '앞다리weight',
'우둔weight', '설도weight', '사태weight', '양지weight', '갈비weight',
'안심rate', '등심rate', '채끝rate', '목심rate', '앞다리rate',
'우둔rate', '설도rate', '사태rate', '양지rate', '갈비rate',
]
// 필터가 활성화되어 있으면 가중치 > 0인 형질만 사용 (리스트와 동일 로직)
const traitConditions = Object.entries(filters.traitWeights as Record<string, number>)
.filter(([, weight]) => weight > 0)
.map(([traitNm, weight]) => ({ traitNm, weight }))
const finalConditions = filters.isActive && traitConditions.length > 0
? traitConditions
: ALL_TRAITS.map(traitNm => ({ traitNm, weight: 1 }))
const indexResult = await genomeApi.getSelectionIndex(cowNo, finalConditions)
setSelectionIndex(indexResult)
} catch (compErr) {
console.error('비교 데이터 조회 실패:', compErr)
}
}
} catch (err) {
console.error('데이터 조회 실패:', err)
toast({
variant: "destructive",
title: "데이터 로드 실패",
description: "개체 정보를 불러올 수 없습니다",
})
} finally {
setLoading(false)
}
}
fetchData()
}, [cowNo, toast, filters.isActive, filters.selectedTraits, filters.traitWeights])
// API 데이터를 화면용으로 변환
const GENOMIC_TRAITS = useMemo(() => {
return transformGenomeData(genomeData)
}, [genomeData])
// 고유 카테고리 목록
const CATEGORIES = useMemo(() => {
return [...new Set(GENOMIC_TRAITS.map(t => t.category).filter(Boolean))]
}, [GENOMIC_TRAITS])
// 종합 지표
const overallScore = useMemo(() => {
if (selectionIndex?.score !== null && selectionIndex?.score !== undefined) {
return selectionIndex.score // 내개체
}
if (GENOMIC_TRAITS.length === 0) return 0
return GENOMIC_TRAITS.reduce((sum, t) => sum + t.breedVal, 0) / GENOMIC_TRAITS.length
}, [GENOMIC_TRAITS, selectionIndex])
const overallPercentile = useMemo(() => {
if (selectionIndex?.percentile !== null && selectionIndex?.percentile !== undefined) {
return selectionIndex.percentile
}
if (overallScore !== 0) {
const cdf = 1 / (1 + Math.exp(-1.702 * overallScore))
return (1 - cdf) * 100
}
if (GENOMIC_TRAITS.length === 0) return 50
return GENOMIC_TRAITS.reduce((sum, t) => sum + t.percentile, 0) / GENOMIC_TRAITS.length
}, [GENOMIC_TRAITS, selectionIndex, overallScore])
// 카테고리별 평균
const categoryStats = useMemo(() => {
return CATEGORIES.map(cat => {
const traits = GENOMIC_TRAITS.filter(t => t.category === cat)
const avgBreedVal = traits.reduce((sum, t) => sum + t.breedVal, 0) / traits.length
const avgPercentile = traits.reduce((sum, t) => sum + t.percentile, 0) / traits.length
return { category: cat, avgBreedVal, avgPercentile, count: traits.length }
})
}, [CATEGORIES, GENOMIC_TRAITS])
// 평균 Z-Score
const farmAvgZ = useMemo(() => {
if (!comparisonAverages?.farm || comparisonAverages.farm.length === 0) {
return overallScore > 0.5 ? overallScore * 0.5 : 0.3
}
const totalEbv = comparisonAverages.farm.reduce((sum, cat) => sum + cat.avgEbv, 0)
return totalEbv / comparisonAverages.farm.length
}, [comparisonAverages, overallScore])
const regionAvgZ = useMemo(() => {
if (!comparisonAverages?.region || comparisonAverages.region.length === 0) {
return -0.2
}
const totalEbv = comparisonAverages.region.reduce((sum, cat) => sum + cat.avgEbv, 0)
return totalEbv / comparisonAverages.region.length
}, [comparisonAverages])
// 개체 EPD 평균 (선택된 형질 기준)
const cowAvgEpd = useMemo(() => {
const selectedTraitNames = Object.entries(filters.traitWeights)
.filter(([, weight]) => weight > 0)
.map(([traitNm]) => traitNm)
const targetTraits = selectedTraitNames.length > 0
? GENOMIC_TRAITS.filter(t => selectedTraitNames.includes(t.name))
: GENOMIC_TRAITS
if (targetTraits.length === 0) return null
const totalEpd = targetTraits.reduce((sum, t) => sum + t.actualValue, 0)
return totalEpd / targetTraits.length
}, [GENOMIC_TRAITS, filters.traitWeights])
// 농가 EPD 평균
const farmAvgEpdValue = useMemo(() => {
if (!comparisonAverages?.farm || comparisonAverages.farm.length === 0) return null
const totalEpd = comparisonAverages.farm.reduce((sum, cat) => sum + (cat.avgEpd || 0), 0)
return totalEpd / comparisonAverages.farm.length
}, [comparisonAverages])
// 보은군 EPD 평균
const regionAvgEpdValue = useMemo(() => {
if (!comparisonAverages?.region || comparisonAverages.region.length === 0) return null
const totalEpd = comparisonAverages.region.reduce((sum, cat) => sum + (cat.avgEpd || 0), 0)
return totalEpd / comparisonAverages.region.length
}, [comparisonAverages])
// 필터에서 선택한 형질 데이터
const filterSelectedTraitData = useMemo(() => {
const selectedTraitNames = Object.entries(filters.traitWeights)
.filter(([, weight]) => weight > 0)
.map(([traitNm]) => traitNm)
if (selectedTraitNames.length === 0) return []
return GENOMIC_TRAITS.filter(t => selectedTraitNames.includes(t.name))
}, [filters.traitWeights, GENOMIC_TRAITS])
// 정규분포 데이터
const multiDistribution = useMemo(() => {
return generateMultipleDistributions(0, 1, regionAvgZ, 1, farmAvgZ, 1)
}, [regionAvgZ, farmAvgZ])
const toggleTraitSelection = (traitId: number) => {
setSelectedTraits(prev =>
prev.includes(traitId)
? prev.filter(id => id !== traitId)
: [...prev, traitId]
)
}
if (loading) {
return (
<SidebarProvider>
<AppSidebar />
<SidebarInset>
<SiteHeader />
<div className="flex items-center justify-center h-64 md:h-96">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary mx-auto mb-4"></div>
<p className="text-muted-foreground"> ...</p>
</div>
</div>
</SidebarInset>
</SidebarProvider>
)
}
return (
<AuthGuard>
<SidebarProvider>
<AppSidebar />
<SidebarInset>
<SiteHeader />
<main className="flex-1 overflow-y-auto bg-white min-h-screen">
{/* 메인 컨테이너 여백 : p-6 */}
<div className="w-full p-6 sm:px-6 lg:px-8 sm:py-6 space-y-6">
{/* 헤더: 뒤로가기 + 타이틀 + 다운로드 */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-2 sm:gap-4">
{/* 뒤로가기 버튼 */}
<Button
onClick={handleBack}
variant="ghost"
size="sm"
className="text-muted-foreground hover:text-foreground hover:bg-muted gap-1.5 -ml-2 px-2 sm:px-3"
>
<ArrowLeft className="h-7 w-7 sm:h-4 sm:w-4" />
<span className="hidden sm:inline text-sm"></span>
</Button>
{/* 아이콘 */}
<div className="w-10 h-10 sm:w-14 sm:h-14 bg-primary rounded-xl flex items-center justify-center flex-shrink-0">
<BarChart3 className="h-5 w-5 sm:h-7 sm:w-7 text-primary-foreground" />
</div>
{/* 타이틀 */}
<div>
<h1 className="text-lg sm:text-3xl lg:text-4xl font-bold text-foreground"> </h1>
<p className="text-sm sm:text-lg text-muted-foreground">Analysis Report</p>
</div>
</div>
</div>
{/* 탭 네비게이션 */}
<Tabs value={activeTab} onValueChange={handleTabChange} className="w-full">
<TabsList className="w-full grid grid-cols-3 bg-transparent p-0 h-auto gap-0 rounded-none border-b border-border">
<TabsTrigger
value="genome"
className="flex items-center justify-center gap-1.5 sm:gap-3 rounded-none border-b-2 border-transparent data-[state=active]:border-b-primary data-[state=active]:text-primary bg-transparent px-1.5 sm:px-5 py-2.5 sm:py-5 text-muted-foreground data-[state=active]:shadow-none"
>
<BarChart3 className="hidden sm:block h-6 w-6 shrink-0" />
<span className="font-bold text-sm sm:text-xl"></span>
<span className={`text-xs sm:text-sm px-1.5 sm:px-2.5 py-0.5 sm:py-1 rounded font-semibold shrink-0 ${hasGenomeData ? 'bg-green-500 text-white' : 'bg-slate-300 text-slate-600'}`}>
{hasGenomeData ? '완료' : '미검사'}
</span>
</TabsTrigger>
<TabsTrigger
value="gene"
className="flex items-center justify-center gap-1.5 sm:gap-3 rounded-none border-b-2 border-transparent data-[state=active]:border-b-primary data-[state=active]:text-primary bg-transparent px-1.5 sm:px-5 py-2.5 sm:py-5 text-muted-foreground data-[state=active]:shadow-none"
>
<Dna className="hidden sm:block h-6 w-6 shrink-0" />
<span className="font-bold text-sm sm:text-xl"></span>
</TabsTrigger>
<TabsTrigger
value="reproduction"
className="flex items-center justify-center gap-1.5 sm:gap-3 rounded-none border-b-2 border-transparent data-[state=active]:border-b-primary data-[state=active]:text-primary bg-transparent px-1.5 sm:px-5 py-2.5 sm:py-5 text-muted-foreground data-[state=active]:shadow-none"
>
<Activity className="hidden sm:block h-6 w-6 shrink-0" />
<span className="font-bold text-sm sm:text-xl"></span>
</TabsTrigger>
</TabsList>
{/* 유전체 분석 탭 */}
<TabsContent value="genome" className="mt-6 space-y-6">
{hasGenomeData ? (
<>
{/* 개체 정보 섹션 */}
<h3 className="text-lg lg:text-xl font-bold text-foreground"> </h3>
<Card className="bg-white border border-border shadow-sm rounded-2xl overflow-hidden">
<CardContent className="p-0">
{/* 모바일: 세로 리스트 / 데스크탑: 가로 그리드 */}
<div className="hidden lg:grid lg:grid-cols-4 divide-x divide-border">
<div>
<div className="bg-muted/50 px-5 py-3 border-b border-border">
<span className="text-base font-semibold text-muted-foreground"></span>
</div>
<div className="px-5 py-4">
<CowNumberDisplay cowId={cowNo} variant="highlight" className="text-2xl font-bold text-foreground" />
</div>
</div>
<div>
<div className="bg-muted/50 px-5 py-3 border-b border-border">
<span className="text-base font-semibold text-muted-foreground"></span>
</div>
<div className="px-5 py-4">
<span className="text-2xl font-bold text-foreground">
{cow?.cowBirthDt ? new Date(cow.cowBirthDt).toLocaleDateString('ko-KR') : '-'}
</span>
</div>
</div>
<div>
<div className="bg-muted/50 px-5 py-3 border-b border-border">
<span className="text-base font-semibold text-muted-foreground"></span>
</div>
<div className="px-5 py-4">
<span className="text-2xl font-bold text-foreground">
{cow?.cowBirthDt
? `${Math.floor((new Date().getTime() - new Date(cow.cowBirthDt).getTime()) / (1000 * 60 * 60 * 24 * 30.44))}개월`
: '-'}
</span>
</div>
</div>
<div>
<div className="bg-muted/50 px-5 py-3 border-b border-border">
<span className="text-base font-semibold text-muted-foreground"> </span>
</div>
<div className="px-5 py-4">
<span className="text-2xl font-bold text-foreground">
{genomeData[0]?.request?.requestDt
? new Date(genomeData[0].request.requestDt).toLocaleDateString('ko-KR')
: '-'}
</span>
</div>
</div>
</div>
{/* 모바일: 좌우 배치 리스트 */}
<div className="lg:hidden divide-y divide-border">
<div className="flex items-center">
<span className="w-28 shrink-0 bg-muted/50 px-4 py-3.5 text-base font-medium text-muted-foreground"></span>
<div className="flex-1 px-4 py-3.5">
<CowNumberDisplay cowId={cowNo} variant="highlight" className="text-base font-bold text-foreground" />
</div>
</div>
<div className="flex items-center">
<span className="w-28 shrink-0 bg-muted/50 px-4 py-3.5 text-base font-medium text-muted-foreground"></span>
<span className="flex-1 px-4 py-3.5 text-base font-bold text-foreground">
{cow?.cowBirthDt ? new Date(cow.cowBirthDt).toLocaleDateString('ko-KR') : '-'}
</span>
</div>
<div className="flex items-center">
<span className="w-28 shrink-0 bg-muted/50 px-4 py-3.5 text-base font-medium text-muted-foreground"></span>
<span className="flex-1 px-4 py-3.5 text-base font-bold text-foreground">
{cow?.cowBirthDt
? `${Math.floor((new Date().getTime() - new Date(cow.cowBirthDt).getTime()) / (1000 * 60 * 60 * 24 * 30.44))}개월`
: '-'}
</span>
</div>
<div className="flex items-center">
<span className="w-28 shrink-0 bg-muted/50 px-4 py-3.5 text-base font-medium text-muted-foreground"></span>
<span className="flex-1 px-4 py-3.5 text-base font-bold text-foreground">
{genomeData[0]?.request?.requestDt
? new Date(genomeData[0].request.requestDt).toLocaleDateString('ko-KR')
: '-'}
</span>
</div>
</div>
</CardContent>
</Card>
{/* 친자확인 섹션 */}
<h3 className="text-lg lg:text-xl font-bold text-foreground"></h3>
<Card className="bg-white border border-border shadow-sm rounded-2xl overflow-hidden">
<CardContent className="p-0">
{/* 데스크탑: 가로 그리드 */}
<div className="hidden lg:grid lg:grid-cols-2 divide-x divide-border">
<div>
<div className="bg-muted/50 px-5 py-3 border-b border-border">
<span className="text-base font-semibold text-muted-foreground"> KPN번호</span>
</div>
<div className="px-5 py-4 flex items-center justify-between gap-3">
<span className="text-2xl font-bold text-foreground break-all">
{cow?.sireKpn && cow.sireKpn !== '0' ? cow.sireKpn : '-'}
</span>
{(() => {
const chipSireName = genomeRequest?.chipSireName
if (chipSireName === '일치') {
return (
<span className="flex items-center gap-1.5 bg-primary text-primary-foreground text-sm font-semibold px-3 py-1.5 rounded-full shrink-0">
<CheckCircle2 className="w-4 h-4" />
<span></span>
</span>
)
} else if (chipSireName && chipSireName !== '일치') {
return (
<span className="flex items-center gap-1.5 bg-red-500 text-white text-sm font-semibold px-3 py-1.5 rounded-full shrink-0">
<XCircle className="w-4 h-4" />
<span></span>
</span>
)
} else {
return (
<span className="flex items-center gap-1.5 bg-slate-400 text-white text-sm font-semibold px-3 py-1.5 rounded-full shrink-0">
<span></span>
</span>
)
}
})()}
</div>
</div>
<div>
<div className="bg-muted/50 px-5 py-3 border-b border-border">
<span className="text-base font-semibold text-muted-foreground"> </span>
</div>
<div className="px-5 py-4 flex items-center justify-between gap-3">
{cow?.damCowId && cow.damCowId !== '0' ? (
<CowNumberDisplay cowId={cow.damCowId} variant="highlight" className="text-2xl font-bold text-foreground" />
) : (
<span className="text-2xl font-bold text-foreground">-</span>
)}
{(() => {
const chipDamName = genomeRequest?.chipDamName
if (chipDamName === '일치') {
return (
<span className="flex items-center gap-1.5 bg-primary text-primary-foreground text-sm font-semibold px-3 py-1.5 rounded-full shrink-0">
<CheckCircle2 className="w-4 h-4" />
<span></span>
</span>
)
} else if (chipDamName === '불일치') {
return (
<span className="flex items-center gap-1.5 bg-red-500 text-white text-sm font-semibold px-3 py-1.5 rounded-full shrink-0">
<XCircle className="w-4 h-4" />
<span></span>
</span>
)
} else if (chipDamName === '이력제부재') {
return (
<span className="flex items-center gap-1.5 bg-amber-500 text-white text-sm font-semibold px-3 py-1.5 rounded-full shrink-0">
<XCircle className="w-4 h-4" />
<span></span>
</span>
)
} else {
// 정보없음(null/undefined)일 때는 배지 표시 안함
return null
}
})()}
</div>
</div>
</div>
{/* 모바일: 좌우 배치 리스트 */}
<div className="lg:hidden divide-y divide-border">
<div className="flex items-center">
<span className="w-28 shrink-0 bg-muted/50 px-4 py-3.5 text-base font-medium text-muted-foreground"> KPN번호</span>
<div className="flex-1 px-4 py-3.5 flex items-center justify-between gap-2">
<span className="text-base font-bold text-foreground break-all">
{cow?.sireKpn && cow.sireKpn !== '0' ? cow.sireKpn : '-'}
</span>
{(() => {
const chipSireName = genomeRequest?.chipSireName
if (chipSireName === '일치') {
return (
<span className="flex items-center gap-1 bg-primary text-primary-foreground text-xs font-semibold px-2 py-1 rounded-full shrink-0">
<CheckCircle2 className="w-3 h-3" />
<span></span>
</span>
)
} else if (chipSireName && chipSireName !== '일치') {
return (
<span className="flex items-center gap-1 bg-red-500 text-white text-xs font-semibold px-2 py-1 rounded-full shrink-0">
<XCircle className="w-3 h-3" />
<span></span>
</span>
)
} else {
return (
<span className="flex items-center gap-1 bg-slate-400 text-white text-xs font-semibold px-2 py-1 rounded-full shrink-0">
<span></span>
</span>
)
}
})()}
</div>
</div>
<div className="flex items-center">
<span className="w-28 shrink-0 bg-muted/50 px-4 py-3.5 text-base font-medium text-muted-foreground"> </span>
<div className="flex-1 px-4 py-3.5 flex items-center justify-between gap-2">
{cow?.damCowId && cow.damCowId !== '0' ? (
<CowNumberDisplay cowId={cow.damCowId} variant="highlight" className="text-base font-bold text-foreground" />
) : (
<span className="text-base font-bold text-foreground">-</span>
)}
{(() => {
const chipDamName = genomeRequest?.chipDamName
if (chipDamName === '일치') {
return (
<span className="flex items-center gap-1 bg-primary text-primary-foreground text-xs font-semibold px-2 py-1 rounded-full shrink-0">
<CheckCircle2 className="w-3 h-3" />
<span></span>
</span>
)
} else if (chipDamName === '불일치') {
return (
<span className="flex items-center gap-1 bg-red-500 text-white text-xs font-semibold px-2 py-1 rounded-full shrink-0">
<XCircle className="w-3 h-3" />
<span></span>
</span>
)
} else if (chipDamName === '이력제부재') {
return (
<span className="flex items-center gap-1 bg-amber-500 text-white text-xs font-semibold px-2 py-1 rounded-full shrink-0">
<XCircle className="w-3 h-3" />
<span></span>
</span>
)
} else {
// 정보없음(null/undefined)일 때는 배지 표시 안함
return null
}
})()}
</div>
</div>
</div>
</CardContent>
</Card>
{/* 친자확인 결과에 따른 분기 (유효성 조건: 아비일치 + 어미불일치/이력제부재 제외 + 제외목록) */}
{isValidGenomeAnalysis(genomeData[0]?.request?.chipSireName, genomeData[0]?.request?.chipDamName, cow?.cowId) ? (
<>
{/* 농가 및 보은군 내 개체 위치 */}
<h3 className="text-lg lg:text-xl font-bold text-foreground"> </h3>
<div ref={distributionChartRef}>
<NormalDistributionChart
multiDistribution={multiDistribution}
cowName={cow?.cowId || cowNo}
cowNo={cow?.cowId || cowNo}
overallScore={overallScore}
overallPercentile={overallPercentile}
regionAvgZ={selectionIndex?.regionAvgScore ?? regionAvgScore}
farmAvgZ={selectionIndex?.farmAvgScore ?? farmAvgScore}
showNationwide={showNationwide}
showRegion={showRegion}
showFarm={showFarm}
allTraits={GENOMIC_TRAITS}
selectedTraitData={filterSelectedTraitData}
selectedTraitsCount={filterSelectedTraitData.length}
showAllTraits={showAllTraits}
traitWeights={filters.traitWeights}
onOpenTraitSheet={() => { }}
onToggleTraitSelection={toggleTraitSelection}
onClearSelectedTraits={() => setSelectedTraits([])}
onOpenChartModal={() => setIsChartModalOpen(true)}
distributionData={distributionData}
totalCowCount={totalCowCount}
traitComparisons={traitComparisons}
cowEpd={cowAvgEpd}
farmAvgEpd={farmAvgEpdValue}
regionAvgEpd={regionAvgEpdValue}
farmRank={selectionIndex?.farmRank}
farmTotal={selectionIndex?.farmTotal}
regionRank={selectionIndex?.regionRank}
highlightMode={highlightMode}
onHighlightModeChange={setHighlightMode}
regionTotal={selectionIndex?.regionTotal}
chartFilterTrait={chartFilterTrait}
onChartFilterTraitChange={setChartFilterTrait}
/>
</div>
{/* 유전체 형질별 육종가 비교 */}
<h3 className="text-lg lg:text-xl font-bold text-foreground"> </h3>
<CategoryEvaluationCard
categoryStats={categoryStats}
comparisonAverages={comparisonAverages}
traitComparisonAverages={traitComparisonAverages}
regionAvgZ={regionAvgZ}
farmAvgZ={farmAvgZ}
allTraits={GENOMIC_TRAITS}
cowNo={cowNo}
hideTraitCards={true}
/>
<h3 className="text-lg lg:text-xl font-bold text-foreground mt-6"> </h3>
<TraitDistributionCharts
allTraits={GENOMIC_TRAITS}
regionAvgZ={regionAvgZ}
farmAvgZ={farmAvgZ}
cowName={cow?.cowId || cowNo}
totalCowCount={totalCowCount}
selectedTraits={filterSelectedTraitData}
traitWeights={filters.traitWeights}
/>
<h3 className="text-lg lg:text-xl font-bold text-foreground mt-6"> </h3>
<Card className="bg-white border border-border rounded-xl overflow-hidden">
<CardContent className="p-0">
<div className="grid grid-cols-3 divide-x divide-border">
<div className="p-4">
<div className="text-xs font-medium text-muted-foreground mb-1"></div>
<div className="text-sm font-semibold text-foreground truncate">
{genomeData[0]?.request?.requestDt
? new Date(genomeData[0].request.requestDt).toLocaleDateString('ko-KR')
: '-'}
</div>
</div>
<div className="p-4">
<div className="text-xs font-medium text-muted-foreground mb-1"> </div>
<div className="text-sm font-semibold text-foreground truncate">
{genomeData[0]?.request?.chipReportDt
? new Date(genomeData[0].request.chipReportDt).toLocaleDateString('ko-KR')
: '-'}
</div>
</div>
<div className="p-4">
<div className="text-xs font-medium text-muted-foreground mb-1"> </div>
<div className="text-sm font-semibold text-foreground truncate">
{genomeData[0]?.request?.chipType || '-'}
</div>
</div>
</div>
</CardContent>
</Card>
</>
) : (
<>
<h3 className="text-lg lg:text-xl font-bold text-foreground"> </h3>
<Card className="bg-white border border-border shadow-sm rounded-2xl overflow-hidden">
<CardContent className="p-0">
<div className="bg-muted/50 px-5 py-3 border-b border-border">
<span className="text-base font-semibold text-muted-foreground">
{getInvalidReason(genomeData[0]?.request?.chipSireName, genomeData[0]?.request?.chipDamName, cow?.cowId) || '분석 불가'}
</span>
</div>
<div className="px-5 py-6">
<p className="text-base text-muted-foreground mb-5">
{getInvalidMessage(genomeData[0]?.request?.chipSireName, genomeData[0]?.request?.chipDamName, cow?.cowId)}
</p>
<div className="bg-slate-50 border border-slate-200 rounded-xl p-5">
<h4 className="text-base font-semibold text-foreground mb-3"></h4>
<ul className="text-base text-muted-foreground space-y-2">
<li className="flex items-start gap-2">
<span className="text-slate-400 mt-1"></span>
<span> .</span>
</li>
<li className="flex items-start gap-2">
<span className="text-slate-400 mt-1"></span>
<span> KPN .</span>
</li>
</ul>
</div>
</div>
</CardContent>
</Card>
</>
)}
</>
) : (
<>
{/* 개체 정보 섹션 */}
<h3 className="text-lg lg:text-xl font-bold text-foreground"> </h3>
<Card className="bg-white border border-border shadow-sm rounded-2xl overflow-hidden">
<CardContent className="p-0">
{/* 모바일: 세로 리스트 / 데스크탑: 가로 그리드 */}
<div className="hidden lg:grid lg:grid-cols-4 divide-x divide-border">
<div>
<div className="bg-muted/50 px-5 py-3 border-b border-border">
<span className="text-base font-semibold text-muted-foreground"></span>
</div>
<div className="px-5 py-4">
<CowNumberDisplay cowId={cowNo} variant="highlight" className="text-2xl font-bold text-foreground" />
</div>
</div>
<div>
<div className="bg-muted/50 px-5 py-3 border-b border-border">
<span className="text-base font-semibold text-muted-foreground"></span>
</div>
<div className="px-5 py-4">
<span className="text-2xl font-bold text-foreground">
{cow?.cowBirthDt ? new Date(cow.cowBirthDt).toLocaleDateString('ko-KR') : '-'}
</span>
</div>
</div>
<div>
<div className="bg-muted/50 px-5 py-3 border-b border-border">
<span className="text-base font-semibold text-muted-foreground"></span>
</div>
<div className="px-5 py-4">
<span className="text-2xl font-bold text-foreground">
{cow?.cowBirthDt
? `${Math.floor((new Date().getTime() - new Date(cow.cowBirthDt).getTime()) / (1000 * 60 * 60 * 24 * 30.44))}개월`
: '-'}
</span>
</div>
</div>
<div>
<div className="bg-muted/50 px-5 py-3 border-b border-border">
<span className="text-base font-semibold text-muted-foreground"> </span>
</div>
<div className="px-5 py-4">
<span className="text-2xl font-bold text-foreground">-</span>
</div>
</div>
</div>
{/* 모바일: 좌우 배치 리스트 */}
<div className="lg:hidden divide-y divide-border">
<div className="flex items-center">
<span className="w-28 shrink-0 bg-muted/50 px-4 py-3.5 text-base font-medium text-muted-foreground"></span>
<div className="flex-1 px-4 py-3.5">
<CowNumberDisplay cowId={cowNo} variant="highlight" className="text-base font-bold text-foreground" />
</div>
</div>
<div className="flex items-center">
<span className="w-28 shrink-0 bg-muted/50 px-4 py-3.5 text-base font-medium text-muted-foreground"></span>
<span className="flex-1 px-4 py-3.5 text-base font-bold text-foreground">
{cow?.cowBirthDt ? new Date(cow.cowBirthDt).toLocaleDateString('ko-KR') : '-'}
</span>
</div>
<div className="flex items-center">
<span className="w-28 shrink-0 bg-muted/50 px-4 py-3.5 text-base font-medium text-muted-foreground"></span>
<span className="flex-1 px-4 py-3.5 text-base font-bold text-foreground">
{cow?.cowBirthDt
? `${Math.floor((new Date().getTime() - new Date(cow.cowBirthDt).getTime()) / (1000 * 60 * 60 * 24 * 30.44))}개월`
: '-'}
</span>
</div>
<div className="flex items-center">
<span className="w-28 shrink-0 bg-muted/50 px-4 py-3.5 text-base font-medium text-muted-foreground"></span>
<span className="flex-1 px-4 py-3.5 text-base font-bold text-foreground">-</span>
</div>
</div>
</CardContent>
</Card>
{/* 친자확인 섹션 */}
<h3 className="text-lg lg:text-xl font-bold text-foreground"></h3>
<Card className="bg-white border border-border shadow-sm rounded-2xl overflow-hidden">
<CardContent className="p-0">
{/* 데스크탑: 가로 그리드 */}
<div className="hidden lg:grid lg:grid-cols-2 divide-x divide-border">
<div>
<div className="bg-muted/50 px-5 py-3 border-b border-border">
<span className="text-base font-semibold text-muted-foreground"> KPN번호</span>
</div>
<div className="px-5 py-4 flex items-center justify-between gap-3">
<span className="text-2xl font-bold text-foreground">{cow?.sireKpn && cow.sireKpn !== '0' ? cow.sireKpn : '-'}</span>
{(() => {
const chipSireName = genomeRequest?.chipSireName
if (chipSireName === '일치') {
return (
<span className="flex items-center gap-1.5 bg-primary text-primary-foreground text-sm font-semibold px-3 py-1.5 rounded-full shrink-0">
<CheckCircle2 className="w-4 h-4" />
<span></span>
</span>
)
} else if (chipSireName && chipSireName !== '일치') {
return (
<span className="flex items-center gap-1.5 bg-red-500 text-white text-sm font-semibold px-3 py-1.5 rounded-full shrink-0">
<XCircle className="w-4 h-4" />
<span></span>
</span>
)
} else {
return (
<span className="flex items-center gap-1.5 bg-slate-400 text-white text-sm font-semibold px-3 py-1.5 rounded-full shrink-0">
<span></span>
</span>
)
}
})()}
</div>
</div>
<div>
<div className="bg-muted/50 px-5 py-3 border-b border-border">
<span className="text-base font-semibold text-muted-foreground"> </span>
</div>
<div className="px-5 py-4 flex items-center justify-between gap-3">
{cow?.damCowId && cow.damCowId !== '0' ? (
<CowNumberDisplay cowId={cow.damCowId} variant="highlight" className="text-2xl font-bold text-foreground" />
) : (
<span className="text-2xl font-bold text-foreground">-</span>
)}
{(() => {
const chipDamName = genomeRequest?.chipDamName
if (chipDamName === '일치') {
return (
<span className="flex items-center gap-1.5 bg-primary text-primary-foreground text-sm font-semibold px-3 py-1.5 rounded-full shrink-0">
<CheckCircle2 className="w-4 h-4" />
<span></span>
</span>
)
} else if (chipDamName === '불일치') {
return (
<span className="flex items-center gap-1.5 bg-red-500 text-white text-sm font-semibold px-3 py-1.5 rounded-full shrink-0">
<XCircle className="w-4 h-4" />
<span></span>
</span>
)
} else if (chipDamName === '이력제부재') {
return (
<span className="flex items-center gap-1.5 bg-amber-500 text-white text-sm font-semibold px-3 py-1.5 rounded-full shrink-0">
<XCircle className="w-4 h-4" />
<span></span>
</span>
)
} else {
// 정보없음(null/undefined)일 때는 배지 표시 안함
return null
}
})()}
</div>
</div>
</div>
{/* 모바일: 세로 리스트 */}
<div className="lg:hidden divide-y divide-border">
<div className="flex items-center">
<span className="w-28 shrink-0 bg-muted/50 px-4 py-3.5 text-base font-medium text-muted-foreground"> KPN</span>
<div className="flex-1 px-4 py-3.5 flex items-center justify-between gap-2">
<span className="text-base font-bold text-foreground">{cow?.sireKpn && cow.sireKpn !== '0' ? cow.sireKpn : '-'}</span>
{(() => {
const chipSireName = genomeRequest?.chipSireName
if (chipSireName === '일치') {
return (
<span className="flex items-center gap-1 bg-primary text-primary-foreground text-xs font-semibold px-2 py-1 rounded-full shrink-0">
<CheckCircle2 className="w-3 h-3" />
<span></span>
</span>
)
} else if (chipSireName && chipSireName !== '일치') {
return (
<span className="flex items-center gap-1 bg-red-500 text-white text-xs font-semibold px-2 py-1 rounded-full shrink-0">
<XCircle className="w-3 h-3" />
<span></span>
</span>
)
} else {
return (
<span className="flex items-center gap-1 bg-slate-400 text-white text-xs font-semibold px-2 py-1 rounded-full shrink-0">
<span></span>
</span>
)
}
})()}
</div>
</div>
<div className="flex items-center">
<span className="w-28 shrink-0 bg-muted/50 px-4 py-3.5 text-base font-medium text-muted-foreground"> </span>
<div className="flex-1 px-4 py-3.5 flex items-center justify-between gap-2">
{cow?.damCowId && cow.damCowId !== '0' ? (
<CowNumberDisplay cowId={cow.damCowId} variant="highlight" className="text-base font-bold text-foreground" />
) : (
<span className="text-base font-bold text-foreground">-</span>
)}
{(() => {
const chipDamName = genomeRequest?.chipDamName
if (chipDamName === '일치') {
return (
<span className="flex items-center gap-1 bg-primary text-primary-foreground text-xs font-semibold px-2 py-1 rounded-full shrink-0">
<CheckCircle2 className="w-3 h-3" />
<span></span>
</span>
)
} else if (chipDamName === '불일치') {
return (
<span className="flex items-center gap-1 bg-red-500 text-white text-xs font-semibold px-2 py-1 rounded-full shrink-0">
<XCircle className="w-3 h-3" />
<span></span>
</span>
)
} else if (chipDamName === '이력제부재') {
return (
<span className="flex items-center gap-1 bg-amber-500 text-white text-xs font-semibold px-2 py-1 rounded-full shrink-0">
<XCircle className="w-3 h-3" />
<span></span>
</span>
)
} else {
// 정보없음(null/undefined)일 때는 배지 표시 안함
return null
}
})()}
</div>
</div>
</div>
</CardContent>
</Card>
{/* 분석불가 메시지 */}
<Card className="bg-slate-100 border border-slate-300 rounded-2xl">
<CardContent className="p-8 text-center">
<BarChart3 className="h-12 w-12 text-slate-400 mx-auto mb-4" />
<h3 className="text-lg font-semibold text-slate-700 mb-2">
{genomeRequest ? '유전체 분석 불가' : '유전체 분석불가'}
</h3>
<p className="text-sm text-slate-500">
{genomeRequest
? getInvalidMessage(genomeRequest?.chipSireName, genomeRequest?.chipDamName, cow?.cowId)
: '이 개체는 아직 유전체 분석이 진행되지 않았습니다.'
}
</p>
</CardContent>
</Card>
</>
)}
</TabsContent>
{/* 유전자 분석 탭 */}
<TabsContent value="gene" className="mt-6 space-y-6">
{geneDataLoading ? (
<div className="flex items-center justify-center h-64 md:h-96">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary mx-auto mb-4"></div>
<p className="text-muted-foreground"> ...</p>
</div>
</div>
) : hasGeneData ? (
<>
{/* 개체 정보 섹션 (유전체 탭과 동일) */}
<h3 className="text-lg lg:text-xl font-bold text-foreground"> </h3>
<Card className="bg-white border border-border shadow-sm rounded-2xl overflow-hidden">
<CardContent className="p-0">
{/* 데스크탑: 가로 그리드 */}
<div className="hidden lg:grid lg:grid-cols-4 divide-x divide-border">
<div>
<div className="bg-muted/50 px-5 py-3 border-b border-border">
<span className="text-base font-semibold text-muted-foreground"></span>
</div>
<div className="px-5 py-4">
<CowNumberDisplay cowId={cowNo} variant="highlight" className="text-2xl font-bold text-foreground" />
</div>
</div>
<div>
<div className="bg-muted/50 px-5 py-3 border-b border-border">
<span className="text-base font-semibold text-muted-foreground"></span>
</div>
<div className="px-5 py-4">
<span className="text-2xl font-bold text-foreground">
{cow?.cowBirthDt ? new Date(cow.cowBirthDt).toLocaleDateString('ko-KR') : '-'}
</span>
</div>
</div>
<div>
<div className="bg-muted/50 px-5 py-3 border-b border-border">
<span className="text-base font-semibold text-muted-foreground"></span>
</div>
<div className="px-5 py-4">
<span className="text-2xl font-bold text-foreground">
{cow?.cowBirthDt
? `${Math.floor((new Date().getTime() - new Date(cow.cowBirthDt).getTime()) / (1000 * 60 * 60 * 24 * 30.44))}개월`
: '-'}
</span>
</div>
</div>
<div>
<div className="bg-muted/50 px-5 py-3 border-b border-border">
<span className="text-base font-semibold text-muted-foreground"> </span>
</div>
<div className="px-5 py-4">
<span className="text-2xl font-bold text-foreground">
{genomeData[0]?.request?.requestDt
? new Date(genomeData[0].request.requestDt).toLocaleDateString('ko-KR')
: '-'}
</span>
</div>
</div>
</div>
{/* 모바일: 좌우 배치 리스트 */}
<div className="lg:hidden divide-y divide-border">
<div className="flex items-center">
<span className="w-28 shrink-0 bg-muted/50 px-4 py-3.5 text-base font-medium text-muted-foreground"></span>
<div className="flex-1 px-4 py-3.5">
<CowNumberDisplay cowId={cowNo} variant="highlight" className="text-base font-bold text-foreground" />
</div>
</div>
<div className="flex items-center">
<span className="w-28 shrink-0 bg-muted/50 px-4 py-3.5 text-base font-medium text-muted-foreground"></span>
<span className="flex-1 px-4 py-3.5 text-base font-bold text-foreground">
{cow?.cowBirthDt ? new Date(cow.cowBirthDt).toLocaleDateString('ko-KR') : '-'}
</span>
</div>
<div className="flex items-center">
<span className="w-28 shrink-0 bg-muted/50 px-4 py-3.5 text-base font-medium text-muted-foreground"></span>
<span className="flex-1 px-4 py-3.5 text-base font-bold text-foreground">
{cow?.cowBirthDt
? `${Math.floor((new Date().getTime() - new Date(cow.cowBirthDt).getTime()) / (1000 * 60 * 60 * 24 * 30.44))}개월`
: '-'}
</span>
</div>
<div className="flex items-center">
<span className="w-28 shrink-0 bg-muted/50 px-4 py-3.5 text-base font-medium text-muted-foreground"></span>
<span className="flex-1 px-4 py-3.5 text-base font-bold text-foreground">
{genomeData[0]?.request?.requestDt
? new Date(genomeData[0].request.requestDt).toLocaleDateString('ko-KR')
: '-'}
</span>
</div>
</div>
</CardContent>
</Card>
{/* 친자확인 결과 섹션 (유전체 탭과 동일) */}
<h3 className="text-lg lg:text-xl font-bold text-foreground"></h3>
<Card className="bg-white border border-border shadow-sm rounded-2xl overflow-hidden">
<CardContent className="p-0">
{/* 데스크탑: 가로 그리드 */}
<div className="hidden lg:grid lg:grid-cols-2 divide-x divide-border">
<div>
<div className="bg-muted/50 px-5 py-3 border-b border-border">
<span className="text-base font-semibold text-muted-foreground"> KPN번호</span>
</div>
<div className="px-5 py-4 flex items-center justify-between gap-3">
<span className="text-2xl font-bold text-foreground break-all">
{cow?.sireKpn && cow.sireKpn !== '0' ? cow.sireKpn : '-'}
</span>
{(() => {
const chipSireName = genomeRequest?.chipSireName
if (chipSireName === '일치') {
return (
<span className="flex items-center gap-1.5 bg-primary text-primary-foreground text-sm font-semibold px-3 py-1.5 rounded-full shrink-0">
<CheckCircle2 className="w-4 h-4" />
<span></span>
</span>
)
} else if (chipSireName && chipSireName !== '일치') {
return (
<span className="flex items-center gap-1.5 bg-red-500 text-white text-sm font-semibold px-3 py-1.5 rounded-full shrink-0">
<XCircle className="w-4 h-4" />
<span></span>
</span>
)
} else {
return (
<span className="flex items-center gap-1.5 bg-slate-400 text-white text-sm font-semibold px-3 py-1.5 rounded-full shrink-0">
<span></span>
</span>
)
}
})()}
</div>
</div>
<div>
<div className="bg-muted/50 px-5 py-3 border-b border-border">
<span className="text-base font-semibold text-muted-foreground"> </span>
</div>
<div className="px-5 py-4 flex items-center justify-between gap-3">
{cow?.damCowId && cow.damCowId !== '0' ? (
<CowNumberDisplay cowId={cow.damCowId} variant="highlight" className="text-2xl font-bold text-foreground" />
) : (
<span className="text-2xl font-bold text-foreground">-</span>
)}
{(() => {
const chipDamName = genomeRequest?.chipDamName
if (chipDamName === '일치') {
return (
<span className="flex items-center gap-1.5 bg-primary text-primary-foreground text-sm font-semibold px-3 py-1.5 rounded-full shrink-0">
<CheckCircle2 className="w-4 h-4" />
<span></span>
</span>
)
} else if (chipDamName === '불일치') {
return (
<span className="flex items-center gap-1.5 bg-red-500 text-white text-sm font-semibold px-3 py-1.5 rounded-full shrink-0">
<XCircle className="w-4 h-4" />
<span></span>
</span>
)
} else if (chipDamName === '이력제부재') {
return (
<span className="flex items-center gap-1.5 bg-amber-500 text-white text-sm font-semibold px-3 py-1.5 rounded-full shrink-0">
<XCircle className="w-4 h-4" />
<span></span>
</span>
)
} else {
return null
}
})()}
</div>
</div>
</div>
{/* 모바일: 좌우 배치 리스트 */}
<div className="lg:hidden divide-y divide-border">
<div className="flex items-center">
<span className="w-28 shrink-0 bg-muted/50 px-4 py-3.5 text-base font-medium text-muted-foreground"> KPN번호</span>
<div className="flex-1 px-4 py-3.5 flex items-center justify-between gap-2">
<span className="text-base font-bold text-foreground break-all">
{cow?.sireKpn && cow.sireKpn !== '0' ? cow.sireKpn : '-'}
</span>
{(() => {
const chipSireName = genomeRequest?.chipSireName
if (chipSireName === '일치') {
return (
<span className="flex items-center gap-1 bg-primary text-primary-foreground text-xs font-semibold px-2 py-1 rounded-full shrink-0">
<CheckCircle2 className="w-3 h-3" />
<span></span>
</span>
)
} else if (chipSireName && chipSireName !== '일치') {
return (
<span className="flex items-center gap-1 bg-red-500 text-white text-xs font-semibold px-2 py-1 rounded-full shrink-0">
<XCircle className="w-3 h-3" />
<span></span>
</span>
)
} else {
return (
<span className="flex items-center gap-1 bg-slate-400 text-white text-xs font-semibold px-2 py-1 rounded-full shrink-0">
<span></span>
</span>
)
}
})()}
</div>
</div>
<div className="flex items-center">
<span className="w-28 shrink-0 bg-muted/50 px-4 py-3.5 text-base font-medium text-muted-foreground"> </span>
<div className="flex-1 px-4 py-3.5 flex items-center justify-between gap-2">
{cow?.damCowId && cow.damCowId !== '0' ? (
<CowNumberDisplay cowId={cow.damCowId} variant="highlight" className="text-base font-bold text-foreground" />
) : (
<span className="text-base font-bold text-foreground">-</span>
)}
{(() => {
const chipDamName = genomeRequest?.chipDamName
if (chipDamName === '일치') {
return (
<span className="flex items-center gap-1 bg-primary text-primary-foreground text-xs font-semibold px-2 py-1 rounded-full shrink-0">
<CheckCircle2 className="w-3 h-3" />
<span></span>
</span>
)
} else if (chipDamName === '불일치') {
return (
<span className="flex items-center gap-1 bg-red-500 text-white text-xs font-semibold px-2 py-1 rounded-full shrink-0">
<XCircle className="w-3 h-3" />
<span></span>
</span>
)
} else if (chipDamName === '이력제부재') {
return (
<span className="flex items-center gap-1 bg-amber-500 text-white text-xs font-semibold px-2 py-1 rounded-full shrink-0">
<XCircle className="w-3 h-3" />
<span></span>
</span>
)
} else {
return null
}
})()}
</div>
</div>
</div>
</CardContent>
</Card>
{/* 유전자 검색 및 필터 섹션 */}
<h3 className="text-lg lg:text-xl font-bold text-foreground"> </h3>
<div className="flex flex-col gap-3 sm:gap-3 p-3.5 max-sm:p-3 sm:px-4 sm:py-3 rounded-xl bg-slate-50/50 border border-slate-200/50">
{/* 검색창 */}
<div className="relative w-full">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
<Input
placeholder="SNP 이름, 염색체 검색..."
className="pl-9 h-11 max-sm:h-10 text-base max-sm:text-sm border-slate-200 bg-white focus:border-blue-400 focus:ring-blue-100"
value={geneSearchInput}
onChange={(e) => setGeneSearchInput(e.target.value)}
/>
</div>
{/* 필터 옵션들 */}
<div className="flex flex-col sm:flex-row sm:items-center gap-3 sm:gap-4 border-t border-slate-200/70 pt-3 sm:pt-3">
{/* 유전자 타입 필터 */}
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-slate-600 shrink-0">:</span>
<div className="flex bg-slate-100 rounded-lg p-1 gap-1">
<button
onClick={() => setGeneTypeFilter('all')}
className={`px-3 py-1.5 rounded-md text-sm font-medium transition-colors ${geneTypeFilter === 'all'
? 'bg-white text-slate-900 shadow-sm'
: 'text-slate-500 hover:text-slate-700'
}`}
>
</button>
<button
onClick={() => setGeneTypeFilter('QTY')}
className={`px-3 py-1.5 rounded-md text-sm font-medium transition-colors ${geneTypeFilter === 'QTY'
? 'bg-white text-slate-900 shadow-sm'
: 'text-slate-500 hover:text-slate-700'
}`}
>
</button>
<button
onClick={() => setGeneTypeFilter('QLT')}
className={`px-3 py-1.5 rounded-md text-sm font-medium transition-colors ${geneTypeFilter === 'QLT'
? 'bg-white text-slate-900 shadow-sm'
: 'text-slate-500 hover:text-slate-700'
}`}
>
</button>
</div>
</div>
{/* 정렬 드롭다운 */}
<div className="flex items-center gap-2 sm:ml-auto">
<Select
value={geneSortBy}
onValueChange={(value: 'snpName' | 'chromosome' | 'position' | 'snpType' | 'allele1' | 'allele2' | 'remarks') => setGeneSortBy(value)}
>
<SelectTrigger className="w-[160px] h-9 text-sm border-slate-200 bg-white">
<SelectValue placeholder="정렬 기준" />
</SelectTrigger>
<SelectContent>
<SelectItem value="snpName">SNP </SelectItem>
<SelectItem value="chromosome"> </SelectItem>
<SelectItem value="position">Position</SelectItem>
<SelectItem value="snpType">SNP </SelectItem>
<SelectItem value="allele1"> </SelectItem>
<SelectItem value="allele2"> </SelectItem>
<SelectItem value="remarks"></SelectItem>
</SelectContent>
</Select>
<Select
value={geneSortOrder}
onValueChange={(value: 'asc' | 'desc') => setGeneSortOrder(value)}
>
<SelectTrigger className="w-[110px] h-9 text-sm border-slate-200 bg-white">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="asc"></SelectItem>
<SelectItem value="desc"></SelectItem>
</SelectContent>
</Select>
</div>
</div>
</div>
{/* 유전자 테이블/카드 */}
{(() => {
const filteredData = geneData.filter(gene => {
// 검색 필터 (테이블의 모든 필드 검색)
if (geneSearchKeyword) {
const keyword = geneSearchKeyword.toLowerCase()
const snpName = (gene.snpName || '').toLowerCase()
const chromosome = (gene.chromosome || '').toLowerCase()
const position = (gene.position || '').toLowerCase()
const snpType = (gene.snpType || '').toLowerCase()
const allele1 = (gene.allele1 || '').toLowerCase()
const allele2 = (gene.allele2 || '').toLowerCase()
const remarks = (gene.remarks || '').toLowerCase()
if (!snpName.includes(keyword) &&
!chromosome.includes(keyword) &&
!position.includes(keyword) &&
!snpType.includes(keyword) &&
!allele1.includes(keyword) &&
!allele2.includes(keyword) &&
!remarks.includes(keyword)) {
return false
}
}
// 유전자형 필터
if (genotypeFilter !== 'all') {
const isHomozygous = gene.allele1 === gene.allele2
if (genotypeFilter === 'homozygous' && !isHomozygous) return false
if (genotypeFilter === 'heterozygous' && isHomozygous) return false
}
return true
})
// 정렬
const sortedData = [...filteredData].sort((a, b) => {
let aVal: string | number = ''
let bVal: string | number = ''
switch (geneSortBy) {
case 'snpName':
aVal = a.snpName || ''
bVal = b.snpName || ''
break
case 'chromosome':
aVal = parseInt(a.chromosome || '0') || 0
bVal = parseInt(b.chromosome || '0') || 0
break
case 'position':
aVal = parseInt(a.position || '0') || 0
bVal = parseInt(b.position || '0') || 0
break
case 'snpType':
aVal = a.snpType || ''
bVal = b.snpType || ''
break
case 'allele1':
aVal = a.allele1 || ''
bVal = b.allele1 || ''
break
case 'allele2':
aVal = a.allele2 || ''
bVal = b.allele2 || ''
break
case 'remarks':
aVal = a.remarks || ''
bVal = b.remarks || ''
break
}
if (typeof aVal === 'number' && typeof bVal === 'number') {
return geneSortOrder === 'asc' ? aVal - bVal : bVal - aVal
}
const strA = String(aVal)
const strB = String(bVal)
return geneSortOrder === 'asc'
? strA.localeCompare(strB)
: strB.localeCompare(strA)
})
// 페이지네이션 계산
const totalPages = Math.ceil(sortedData.length / GENES_PER_PAGE)
const startIndex = (geneCurrentPage - 1) * GENES_PER_PAGE
const endIndex = startIndex + GENES_PER_PAGE
const displayData = sortedData.length > 0
? sortedData.slice(startIndex, endIndex)
: Array(10).fill(null)
// 페이지네이션 UI 컴포넌트
const PaginationUI = () => {
if (sortedData.length <= GENES_PER_PAGE) return null
// 표시할 페이지 번호들 계산 (모바일: 3개 단순, 데스크탑: 5개 + 1/마지막 고정)
const getPageNumbers = () => {
const pages: (number | string)[] = []
const showPages = isMobile ? 3 : 5
const offset = isMobile ? 1 : 2
let start = Math.max(1, geneCurrentPage - offset)
let end = Math.min(totalPages, start + showPages - 1)
if (end - start < showPages - 1) {
start = Math.max(1, end - showPages + 1)
}
// 모바일: 현재 페이지 기준 앞뒤만 표시 (1, 마지막 고정 없음)
if (isMobile) {
for (let i = start; i <= end; i++) {
pages.push(i)
}
return pages
}
// 데스크탑: 1과 마지막 페이지 고정
if (start > 1) {
pages.push(1)
if (start > 2) pages.push('...')
}
for (let i = start; i <= end; i++) {
pages.push(i)
}
if (end < totalPages) {
if (end < totalPages - 1) pages.push('...')
pages.push(totalPages)
}
return pages
}
return (
<div className="px-3 sm:px-4 py-3 bg-muted/30 border-t flex flex-col sm:flex-row items-center justify-between gap-3">
<span className="text-sm text-muted-foreground">
{sortedData.length.toLocaleString()} {startIndex + 1}-{Math.min(endIndex, sortedData.length)}
</span>
<div className="flex items-center gap-1">
<Button
variant="outline"
size="sm"
onClick={() => setGeneCurrentPage(1)}
disabled={geneCurrentPage === 1}
className="px-2.5 h-9 text-sm"
>
«
</Button>
<Button
variant="outline"
size="sm"
onClick={() => setGeneCurrentPage(p => Math.max(1, p - 1))}
disabled={geneCurrentPage === 1}
className="px-2.5 h-9 text-sm"
>
</Button>
{getPageNumbers().map((page, idx) => (
typeof page === 'number' ? (
<Button
key={idx}
variant={geneCurrentPage === page ? "default" : "outline"}
size="sm"
onClick={() => setGeneCurrentPage(page)}
className="px-2.5 min-w-[36px] h-9 text-sm"
>
{page}
</Button>
) : (
<span key={idx} className="px-1 text-sm text-muted-foreground">...</span>
)
))}
<Button
variant="outline"
size="sm"
onClick={() => setGeneCurrentPage(p => Math.min(totalPages, p + 1))}
disabled={geneCurrentPage === totalPages}
className="px-2.5 h-9 text-sm"
>
</Button>
<Button
variant="outline"
size="sm"
onClick={() => setGeneCurrentPage(totalPages)}
disabled={geneCurrentPage === totalPages}
className="px-2.5 h-9 text-sm"
>
»
</Button>
</div>
</div>
)
}
return (
<>
{/* 데스크톱: 테이블 */}
<Card className="hidden lg:block bg-white border border-border shadow-sm rounded-2xl overflow-hidden">
<CardContent className="p-0">
<div>
<table className="w-full table-fixed">
<thead className="bg-muted/50 border-b border-border">
<tr>
<th className="px-4 py-3 text-center text-base font-semibold text-muted-foreground w-[22%]">SNP </th>
<th className="px-3 py-3 text-center text-base font-semibold text-muted-foreground w-[10%]"> </th>
<th className="px-3 py-3 text-center text-base font-semibold text-muted-foreground w-[11%]">Position</th>
<th className="px-3 py-3 text-center text-base font-semibold text-muted-foreground w-[11%]">SNP </th>
<th className="px-2 py-3 text-center text-base font-semibold text-muted-foreground w-[13%]"> </th>
<th className="px-2 py-3 text-center text-base font-semibold text-muted-foreground w-[13%]"> </th>
<th className="px-4 py-3 text-center text-base font-semibold text-muted-foreground w-[20%]"></th>
</tr>
</thead>
<tbody className="divide-y divide-border">
{displayData.map((gene, idx) => {
if (!gene) {
return (
<tr key={idx} className="hover:bg-muted/30">
<td className="px-4 py-3 text-base text-center text-muted-foreground">-</td>
<td className="px-3 py-3 text-base text-center text-muted-foreground">-</td>
<td className="px-3 py-3 text-base text-center text-muted-foreground">-</td>
<td className="px-3 py-3 text-base text-center text-muted-foreground">-</td>
<td className="px-2 py-3 text-base text-center text-muted-foreground">-</td>
<td className="px-2 py-3 text-base text-center text-muted-foreground">-</td>
<td className="px-4 py-3 text-base text-center text-muted-foreground">-</td>
</tr>
)
}
return (
<tr key={idx} className="hover:bg-muted/30">
<td className="px-4 py-3 text-center text-base font-medium text-foreground">{gene.snpName || '-'}</td>
<td className="px-3 py-3 text-center text-base text-foreground">{gene.chromosome || '-'}</td>
<td className="px-3 py-3 text-center text-base text-foreground">{gene.position || '-'}</td>
<td className="px-3 py-3 text-center text-base text-foreground">{gene.snpType || '-'}</td>
<td className="px-2 py-3 text-center text-base text-foreground">{gene.allele1 || '-'}</td>
<td className="px-2 py-3 text-center text-base text-foreground">{gene.allele2 || '-'}</td>
<td className="px-4 py-3 text-center text-base text-muted-foreground">{gene.remarks || '-'}</td>
</tr>
)
})}
</tbody>
</table>
</div>
<PaginationUI />
</CardContent>
</Card>
{/* 모바일: 카드 뷰 */}
<div className="lg:hidden space-y-3">
{displayData.map((gene, idx) => {
return (
<Card key={idx} className="bg-white border border-border shadow-sm rounded-xl">
<CardContent className="p-4 space-y-2">
<div className="flex items-center justify-between">
<span className="text-sm font-medium text-muted-foreground">SNP </span>
<span className="text-base font-semibold text-foreground">{gene?.snpName || '-'}</span>
</div>
<div className="flex items-center justify-between">
<span className="text-sm font-medium text-muted-foreground"> </span>
<span className="text-base text-foreground">{gene?.chromosome || '-'}</span>
</div>
<div className="flex items-center justify-between">
<span className="text-sm font-medium text-muted-foreground">Position</span>
<span className="text-base text-foreground">{gene?.position || '-'}</span>
</div>
<div className="flex items-center justify-between">
<span className="text-sm font-medium text-muted-foreground">SNP </span>
<span className="text-base text-foreground">{gene?.snpType || '-'}</span>
</div>
<div className="flex items-center justify-between">
<span className="text-sm font-medium text-muted-foreground"> </span>
<span className="text-base text-foreground">{gene?.allele1 || '-'}</span>
</div>
<div className="flex items-center justify-between">
<span className="text-sm font-medium text-muted-foreground"> </span>
<span className="text-base text-foreground">{gene?.allele2 || '-'}</span>
</div>
<div className="flex items-center justify-between">
<span className="text-sm font-medium text-muted-foreground"></span>
<span className="text-base text-muted-foreground">{gene?.remarks || '-'}</span>
</div>
</CardContent>
</Card>
)
})}
</div>
<div className="lg:hidden">
<PaginationUI />
</div>
</>
)
})()}
</>
) : (
<Card className="bg-slate-50 border border-border rounded-2xl">
<CardContent className="p-8 text-center">
<Dna className="h-12 w-12 text-muted-foreground mx-auto mb-4" />
<h3 className="text-lg font-semibold text-foreground mb-2"> </h3>
<p className="text-sm text-muted-foreground">
(SNP) .
</p>
</CardContent>
</Card>
)}
</TabsContent>
{/* 번식능력 탭 */}
<TabsContent value="reproduction" className="mt-6 space-y-6">
{/* 혈액화학검사(MPT) 테이블 - 추후 사용
<MptTable cowShortNo={cowNo?.slice(-4)} cowNo={cowNo} farmNo={cow?.fkFarmNo} cow={cow} genomeRequest={genomeRequest} />
*/}
<Card className="bg-slate-50 border border-border rounded-2xl">
<CardContent className="p-8 text-center">
<Activity className="h-12 w-12 text-muted-foreground mx-auto mb-4" />
<h3 className="text-lg font-semibold text-foreground mb-2"> </h3>
<p className="text-sm text-muted-foreground">
.
</p>
</CardContent>
</Card>
</TabsContent>
</Tabs>
</div>
</main>
</SidebarInset>
{/* 차트 전체화면 모달 */}
<Dialog open={isChartModalOpen} onOpenChange={(open) => setIsChartModalOpen(open ?? false)}>
<DialogContent className="!max-w-[95vw] !w-[95vw] !h-[90vh] !max-h-[90vh] p-0 flex flex-col" showCloseButton={false}>
{/* 모달 헤더 */}
<div className="flex items-center justify-between px-4 py-3 border-b bg-white shrink-0">
<DialogTitle className="text-lg font-bold"> </DialogTitle>
<button
onClick={() => setIsChartModalOpen(false)}
className="p-2 hover:bg-muted rounded-lg transition-colors"
>
<X className="w-5 h-5" />
</button>
</div>
{/* 모달 콘텐츠 - 차트 */}
<div className="flex-1 overflow-y-auto p-4 bg-muted/20">
<NormalDistributionChart
multiDistribution={multiDistribution}
cowName={cow?.cowId || cowNo}
cowNo={cow?.cowId || cowNo}
overallScore={overallScore}
overallPercentile={overallPercentile}
regionAvgZ={selectionIndex?.regionAvgScore ?? regionAvgScore}
farmAvgZ={selectionIndex?.farmAvgScore ?? farmAvgScore}
showNationwide={showNationwide}
showRegion={showRegion}
showFarm={showFarm}
allTraits={GENOMIC_TRAITS}
selectedTraitData={filterSelectedTraitData}
selectedTraitsCount={filterSelectedTraitData.length}
showAllTraits={showAllTraits}
traitWeights={filters.traitWeights}
onOpenTraitSheet={() => { }}
onToggleTraitSelection={toggleTraitSelection}
onClearSelectedTraits={() => setSelectedTraits([])}
onOpenChartModal={() => { }}
distributionData={distributionData}
totalCowCount={totalCowCount}
traitComparisons={traitComparisons}
cowEpd={cowAvgEpd}
farmAvgEpd={farmAvgEpdValue}
regionAvgEpd={regionAvgEpdValue}
farmRank={selectionIndex?.farmRank}
farmTotal={selectionIndex?.farmTotal}
regionRank={selectionIndex?.regionRank}
regionTotal={selectionIndex?.regionTotal}
highlightMode={highlightMode}
onHighlightModeChange={setHighlightMode}
chartFilterTrait={chartFilterTrait}
onChartFilterTraitChange={setChartFilterTrait}
/>
</div>
</DialogContent>
</Dialog>
</SidebarProvider>
</AuthGuard>
)
}