'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 = { // 성장형질 (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(null) const [genomeData, setGenomeData] = useState([]) const [geneData, setGeneData] = useState([]) const [geneDataLoaded, setGeneDataLoaded] = useState(false) // 유전자 데이터 로드 여부 const [geneDataLoading, setGeneDataLoading] = useState(false) // 유전자 데이터 로딩 중 const [loading, setLoading] = useState(true) const [activeTab, setActiveTab] = useState('genome') // 검사 상태 const [hasGenomeData, setHasGenomeData] = useState(false) const [hasGeneData, setHasGeneData] = useState(false) const [hasReproductionData, setHasReproductionData] = useState(false) // 분석 의뢰 정보 (친자감별 결과 포함) const [genomeRequest, setGenomeRequest] = useState(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(null) const [traitComparisonAverages, setTraitComparisonAverages] = useState(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([]) 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([]) const [geneCurrentPage, setGeneCurrentPage] = useState(1) const GENES_PER_PAGE = 50 // 농가/보은군 비교 하이라이트 모드 const [highlightMode, setHighlightMode] = useState<'farm' | 'region' | null>(null) const distributionChartRef = useRef(null) // 필터에서 고정된 첫 번째 형질 (없으면 '도체중') const firstPinnedTrait = filters.pinnedTraits?.[0] || '도체중' // 차트 형질 필터 (전체 선발지수 또는 개별 형질) // 필터 비활성 시 기본값은 첫 번째 고정 형질 const [chartFilterTrait, setChartFilterTrait] = useState(() => { 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) .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 (

데이터를 불러오는 중...

) } return (
{/* 메인 컨테이너 여백 : p-6 */}
{/* 헤더: 뒤로가기 + 타이틀 + 다운로드 */}
{/* 뒤로가기 버튼 */} {/* 아이콘 */}
{/* 타이틀 */}

개체 분석 보고서

Analysis Report

{/* 탭 네비게이션 */} 유전체 {hasGenomeData ? '완료' : '미검사'} 유전자 번식능력 {/* 유전체 분석 탭 */} {hasGenomeData ? ( <> {/* 개체 정보 섹션 */}

개체 정보

{/* 모바일: 세로 리스트 / 데스크탑: 가로 그리드 */}
개체번호
생년월일
{cow?.cowBirthDt ? new Date(cow.cowBirthDt).toLocaleDateString('ko-KR') : '-'}
월령
{cow?.cowBirthDt ? `${Math.floor((new Date().getTime() - new Date(cow.cowBirthDt).getTime()) / (1000 * 60 * 60 * 24 * 30.44))}개월` : '-'}
유전체 분석일자
{genomeData[0]?.request?.requestDt ? new Date(genomeData[0].request.requestDt).toLocaleDateString('ko-KR') : '-'}
{/* 모바일: 좌우 배치 리스트 */}
개체번호
생년월일 {cow?.cowBirthDt ? new Date(cow.cowBirthDt).toLocaleDateString('ko-KR') : '-'}
월령 {cow?.cowBirthDt ? `${Math.floor((new Date().getTime() - new Date(cow.cowBirthDt).getTime()) / (1000 * 60 * 60 * 24 * 30.44))}개월` : '-'}
분석일자 {genomeData[0]?.request?.requestDt ? new Date(genomeData[0].request.requestDt).toLocaleDateString('ko-KR') : '-'}
{/* 친자확인 섹션 */}

혈통정보

{/* 데스크탑: 가로 그리드 */}
부 KPN번호
{cow?.sireKpn && cow.sireKpn !== '0' ? cow.sireKpn : '-'} {(() => { const chipSireName = genomeRequest?.chipSireName if (chipSireName === '일치') { return ( 일치 ) } else if (chipSireName && chipSireName !== '일치') { return ( 불일치 ) } else { return ( 분석불가 ) } })()}
모 개체번호
{cow?.damCowId && cow.damCowId !== '0' ? ( ) : ( - )} {(() => { const chipDamName = genomeRequest?.chipDamName if (chipDamName === '일치') { return ( 일치 ) } else if (chipDamName === '불일치') { return ( 불일치 ) } else if (chipDamName === '이력제부재') { return ( 이력제부재 ) } else { // 정보없음(null/undefined)일 때는 배지 표시 안함 return null } })()}
{/* 모바일: 좌우 배치 리스트 */}
부 KPN번호
{cow?.sireKpn && cow.sireKpn !== '0' ? cow.sireKpn : '-'} {(() => { const chipSireName = genomeRequest?.chipSireName if (chipSireName === '일치') { return ( 일치 ) } else if (chipSireName && chipSireName !== '일치') { return ( 불일치 ) } else { return ( 분석불가 ) } })()}
모 개체번호
{cow?.damCowId && cow.damCowId !== '0' ? ( ) : ( - )} {(() => { const chipDamName = genomeRequest?.chipDamName if (chipDamName === '일치') { return ( 일치 ) } else if (chipDamName === '불일치') { return ( 불일치 ) } else if (chipDamName === '이력제부재') { return ( 이력제부재 ) } else { // 정보없음(null/undefined)일 때는 배지 표시 안함 return null } })()}
{/* 친자확인 결과에 따른 분기 (유효성 조건: 아비일치 + 어미불일치/이력제부재 제외 + 제외목록) */} {isValidGenomeAnalysis(genomeData[0]?.request?.chipSireName, genomeData[0]?.request?.chipDamName, cow?.cowId) ? ( <> {/* 농가 및 보은군 내 개체 위치 */}

농가 및 보은군 내 개체 위치

{ }} 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} />
{/* 유전체 형질별 육종가 비교 */}

유전체 형질별 육종가 비교

선택 형질 상세

분석 정보

접수일
{genomeData[0]?.request?.requestDt ? new Date(genomeData[0].request.requestDt).toLocaleDateString('ko-KR') : '-'}
분석 완료일
{genomeData[0]?.request?.chipReportDt ? new Date(genomeData[0].request.chipReportDt).toLocaleDateString('ko-KR') : '-'}
칩 종류
{genomeData[0]?.request?.chipType || '-'}
) : ( <>

유전체 분석 결과

{getInvalidReason(genomeData[0]?.request?.chipSireName, genomeData[0]?.request?.chipDamName, cow?.cowId) || '분석 불가'}

{getInvalidMessage(genomeData[0]?.request?.chipSireName, genomeData[0]?.request?.chipDamName, cow?.cowId)}

안내사항

  • 유전체 분석 보고서는 친자확인이 완료된 개체에 한해 제공됩니다.
  • 정확한 분석을 위해 재검사 또는 KPN 정보 확인이 필요합니다.
)} ) : ( <> {/* 개체 정보 섹션 */}

개체 정보

{/* 모바일: 세로 리스트 / 데스크탑: 가로 그리드 */}
개체번호
생년월일
{cow?.cowBirthDt ? new Date(cow.cowBirthDt).toLocaleDateString('ko-KR') : '-'}
월령
{cow?.cowBirthDt ? `${Math.floor((new Date().getTime() - new Date(cow.cowBirthDt).getTime()) / (1000 * 60 * 60 * 24 * 30.44))}개월` : '-'}
유전체 분석일자
-
{/* 모바일: 좌우 배치 리스트 */}
개체번호
생년월일 {cow?.cowBirthDt ? new Date(cow.cowBirthDt).toLocaleDateString('ko-KR') : '-'}
월령 {cow?.cowBirthDt ? `${Math.floor((new Date().getTime() - new Date(cow.cowBirthDt).getTime()) / (1000 * 60 * 60 * 24 * 30.44))}개월` : '-'}
분석일자 -
{/* 친자확인 섹션 */}

혈통정보

{/* 데스크탑: 가로 그리드 */}
부 KPN번호
{cow?.sireKpn && cow.sireKpn !== '0' ? cow.sireKpn : '-'} {(() => { const chipSireName = genomeRequest?.chipSireName if (chipSireName === '일치') { return ( 일치 ) } else if (chipSireName && chipSireName !== '일치') { return ( 불일치 ) } else { return ( 분석불가 ) } })()}
모 개체번호
{cow?.damCowId && cow.damCowId !== '0' ? ( ) : ( - )} {(() => { const chipDamName = genomeRequest?.chipDamName if (chipDamName === '일치') { return ( 일치 ) } else if (chipDamName === '불일치') { return ( 불일치 ) } else if (chipDamName === '이력제부재') { return ( 이력제부재 ) } else { // 정보없음(null/undefined)일 때는 배지 표시 안함 return null } })()}
{/* 모바일: 세로 리스트 */}
부 KPN
{cow?.sireKpn && cow.sireKpn !== '0' ? cow.sireKpn : '-'} {(() => { const chipSireName = genomeRequest?.chipSireName if (chipSireName === '일치') { return ( 일치 ) } else if (chipSireName && chipSireName !== '일치') { return ( 불일치 ) } else { return ( 분석불가 ) } })()}
모 개체번호
{cow?.damCowId && cow.damCowId !== '0' ? ( ) : ( - )} {(() => { const chipDamName = genomeRequest?.chipDamName if (chipDamName === '일치') { return ( 일치 ) } else if (chipDamName === '불일치') { return ( 불일치 ) } else if (chipDamName === '이력제부재') { return ( 이력제부재 ) } else { // 정보없음(null/undefined)일 때는 배지 표시 안함 return null } })()}
{/* 분석불가 메시지 */}

{genomeRequest ? '유전체 분석 불가' : '유전체 분석불가'}

{genomeRequest ? getInvalidMessage(genomeRequest?.chipSireName, genomeRequest?.chipDamName, cow?.cowId) : '이 개체는 아직 유전체 분석이 진행되지 않았습니다.' }

)}
{/* 유전자 분석 탭 */} {geneDataLoading ? (

데이터를 불러오는 중...

) : hasGeneData ? ( <> {/* 개체 정보 섹션 (유전체 탭과 동일) */}

개체 정보

{/* 데스크탑: 가로 그리드 */}
개체번호
생년월일
{cow?.cowBirthDt ? new Date(cow.cowBirthDt).toLocaleDateString('ko-KR') : '-'}
월령
{cow?.cowBirthDt ? `${Math.floor((new Date().getTime() - new Date(cow.cowBirthDt).getTime()) / (1000 * 60 * 60 * 24 * 30.44))}개월` : '-'}
유전자 분석일자
{genomeData[0]?.request?.requestDt ? new Date(genomeData[0].request.requestDt).toLocaleDateString('ko-KR') : '-'}
{/* 모바일: 좌우 배치 리스트 */}
개체번호
생년월일 {cow?.cowBirthDt ? new Date(cow.cowBirthDt).toLocaleDateString('ko-KR') : '-'}
월령 {cow?.cowBirthDt ? `${Math.floor((new Date().getTime() - new Date(cow.cowBirthDt).getTime()) / (1000 * 60 * 60 * 24 * 30.44))}개월` : '-'}
분석일자 {genomeData[0]?.request?.requestDt ? new Date(genomeData[0].request.requestDt).toLocaleDateString('ko-KR') : '-'}
{/* 친자확인 결과 섹션 (유전체 탭과 동일) */}

혈통정보

{/* 데스크탑: 가로 그리드 */}
부 KPN번호
{cow?.sireKpn && cow.sireKpn !== '0' ? cow.sireKpn : '-'} {(() => { const chipSireName = genomeRequest?.chipSireName if (chipSireName === '일치') { return ( 일치 ) } else if (chipSireName && chipSireName !== '일치') { return ( 불일치 ) } else { return ( 분석불가 ) } })()}
모 개체번호
{cow?.damCowId && cow.damCowId !== '0' ? ( ) : ( - )} {(() => { const chipDamName = genomeRequest?.chipDamName if (chipDamName === '일치') { return ( 일치 ) } else if (chipDamName === '불일치') { return ( 불일치 ) } else if (chipDamName === '이력제부재') { return ( 이력제부재 ) } else { return null } })()}
{/* 모바일: 좌우 배치 리스트 */}
부 KPN번호
{cow?.sireKpn && cow.sireKpn !== '0' ? cow.sireKpn : '-'} {(() => { const chipSireName = genomeRequest?.chipSireName if (chipSireName === '일치') { return ( 일치 ) } else if (chipSireName && chipSireName !== '일치') { return ( 불일치 ) } else { return ( 분석불가 ) } })()}
모 개체번호
{cow?.damCowId && cow.damCowId !== '0' ? ( ) : ( - )} {(() => { const chipDamName = genomeRequest?.chipDamName if (chipDamName === '일치') { return ( 일치 ) } else if (chipDamName === '불일치') { return ( 불일치 ) } else if (chipDamName === '이력제부재') { return ( 이력제부재 ) } else { return null } })()}
{/* 유전자 검색 및 필터 섹션 */}

유전자 분석 결과

{/* 검색창 */}
setGeneSearchInput(e.target.value)} />
{/* 필터 옵션들 */}
{/* 유전자 타입 필터 */}
구분:
{/* 정렬 드롭다운 */}
{/* 유전자 테이블/카드 */} {(() => { 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 (
전체 {sortedData.length.toLocaleString()}개 중 {startIndex + 1}-{Math.min(endIndex, sortedData.length)}번째
{getPageNumbers().map((page, idx) => ( typeof page === 'number' ? ( ) : ( ... ) ))}
) } return ( <> {/* 데스크톱: 테이블 */}
{displayData.map((gene, idx) => { if (!gene) { return ( ) } return ( ) })}
SNP 이름 염색체 위치 Position SNP 구분 첫번째 대립유전자 두번째 대립유전자 설명
- - - - - - -
{gene.snpName || '-'} {gene.chromosome || '-'} {gene.position || '-'} {gene.snpType || '-'} {gene.allele1 || '-'} {gene.allele2 || '-'} {gene.remarks || '-'}
{/* 모바일: 카드 뷰 */}
{displayData.map((gene, idx) => { return (
SNP 이름 {gene?.snpName || '-'}
염색체 위치 {gene?.chromosome || '-'}
Position {gene?.position || '-'}
SNP 구분 {gene?.snpType || '-'}
첫번째 대립유전자 {gene?.allele1 || '-'}
두번째 대립유전자 {gene?.allele2 || '-'}
설명 {gene?.remarks || '-'}
) })}
) })()} ) : (

유전자 분석 데이터 없음

이 개체는 아직 유전자(SNP) 분석이 완료되지 않았습니다.

)}
{/* 번식능력 탭 */} {/* 혈액화학검사(MPT) 테이블 - 추후 사용 */}

번식능력 분석 데이터 없음

이 개체는 아직 번식능력 분석이 완료되지 않았습니다.

{/* 차트 전체화면 모달 */} setIsChartModalOpen(open ?? false)}> {/* 모달 헤더 */}
개체 분포 위치
{/* 모달 콘텐츠 - 차트 */}
{ }} 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} />
) }