'use client' import { AuthGuard } from "@/components/auth/auth-guard" import { CowNumberDisplay } from "@/components/common/cow-number-display" import { AppSidebar } from "@/components/layout/app-sidebar" import { SiteHeader } from "@/components/layout/site-header" import { Button } from "@/components/ui/button" import { Card, CardContent } from "@/components/ui/card" import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog" import { Input } from "@/components/ui/input" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar" import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" import { useFilterStore } from "@/store/filter-store" import { useMediaQuery } from "@/hooks/use-media-query" import { useToast } from "@/hooks/use-toast" import { cowApi } from "@/lib/api/cow.api" import { geneApi, GeneDetail } from "@/lib/api/gene.api" import { genomeApi, ComparisonAveragesDto, GenomeRequestDto, TraitComparisonAveragesDto } from "@/lib/api/genome.api" import { mptApi } from "@/lib/api/mpt.api" import { getInvalidMessage, getInvalidReason, isExcludedCow, isValidGenomeAnalysis } from "@/lib/utils/genome-analysis-config" import { ALL_TRAITS } from "@/constants/traits" import { CowDetail } from "@/types/cow.types" import { GenomeTrait } from "@/types/genome.types" import { Activity, ArrowLeft, BarChart3, CheckCircle2, ChevronUp, Dna, Search, X, XCircle } from 'lucide-react' import { useParams, useRouter, useSearchParams } from "next/navigation" import { useEffect, useMemo, useRef, useState } from 'react' import { CategoryEvaluationCard } from "./genome/_components/category-evaluation-card" import { TraitComparison } from "./genome/_components/genome-integrated-comparison" import { NormalDistributionChart } from "./genome/_components/normal-distribution-chart" import { TraitDistributionCharts } from "./genome/_components/trait-distribution-charts" import { MptTable } from "./reproduction/_components/mpt-table" // 유전체 차트 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 } = useFilterStore() const isMobile = useMediaQuery("(max-width: 640px)") // ======================================== // 상태 정의 // ======================================== // 1. 개체/유전체 데이터 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(() => { // 목록에서 진입 시 초기화 if (from === 'list') return 'genome' // 그 외에는 localStorage에서 복원 if (typeof window !== 'undefined') { const saved = localStorage.getItem(`cowDetailActiveTab_${cowNo}`) return saved || 'genome' } return 'genome' }) // 2. 검사 상태 const [hasGenomeData, setHasGenomeData] = useState(false) const [hasGeneData, setHasGeneData] = useState(false) const [hasReproductionData, setHasReproductionData] = useState(false) const [genomeRequest, setGenomeRequest] = useState(null) // 3. 선발지수 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; histogram: { bin: number; count: number; farmCount: number }[]; } | null>(null) // 4. 분포/비교 데이터 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([]) // 5. UI 상태 const [showScrollTop, setShowScrollTop] = useState(false) 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 [highlightMode, setHighlightMode] = useState<'farm' | 'region' | null>(null) const distributionChartRef = useRef(null) // 6. 차트 필터 const firstPinnedTrait = filters.pinnedTraits?.[0] || '도체중' const [chartFilterTrait, setChartFilterTrait] = useState(() => { return filters.isActive ? 'overall' : firstPinnedTrait }) // 7. 유전자 탭 필터/정렬 const [geneSearchInput, setGeneSearchInput] = useState(() => { if (typeof window !== 'undefined' && from !== 'list') { const saved = localStorage.getItem('geneSearchInput') return saved || '' } return '' }) const [geneSearchKeyword, setGeneSearchKeyword] = useState(() => { if (typeof window !== 'undefined' && from !== 'list') { const saved = localStorage.getItem('geneSearchKeyword') return saved || '' } return '' }) const [geneTypeFilter, setGeneTypeFilter] = useState<'all' | 'QTY' | 'QLT'>('all') 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 [geneCurrentLoadedPage, setGeneCurrentLoadedPage] = useState(() => { if (typeof window !== 'undefined' && from !== 'list') { const saved = localStorage.getItem('geneCurrentLoadedPage') return saved ? parseInt(saved, 10) : 1 } return 1 }) const [genesPerPage, setGenesPerPage] = useState(() => { if (typeof window !== 'undefined' && from !== 'list') { const saved = localStorage.getItem('genesPerPage') return saved ? parseInt(saved, 10) : 50 } return 50 }) const [isLoadingMoreGenes, setIsLoadingMoreGenes] = useState(false) // ======================================== // useEffect - localStorage 저장 (유전자 탭) // ======================================== useEffect(() => { if (typeof window !== 'undefined') { localStorage.setItem('geneSearchInput', geneSearchInput) } }, [geneSearchInput]) useEffect(() => { if (typeof window !== 'undefined') { localStorage.setItem('geneSearchKeyword', geneSearchKeyword) } }, [geneSearchKeyword]) useEffect(() => { if (typeof window !== 'undefined') { localStorage.setItem('genesPerPage', genesPerPage.toString()) } }, [genesPerPage]) // 검색어 또는 genesPerPage 변경 시 1페이지로 리셋 useEffect(() => { setGeneCurrentLoadedPage(1) }, [geneSearchKeyword, genesPerPage]) // activeTab 변경 시 localStorage 저장 (목록에서 진입 시 제외) useEffect(() => { if (typeof window !== 'undefined' && from !== 'list') { localStorage.setItem(`cowDetailActiveTab_${cowNo}`, activeTab) } }, [activeTab, cowNo, from]) // ======================================== // useEffect - UI 이벤트 // ======================================== // 필터 활성 상태 변경 시 차트 기본값 업데이트 useEffect(() => { if (!filters.isActive && chartFilterTrait === 'overall') { setChartFilterTrait(firstPinnedTrait) } }, [filters.isActive, firstPinnedTrait, chartFilterTrait]) // 스크롤 투 탑 버튼 표시 useEffect(() => { const handleScroll = () => { setShowScrollTop(window.scrollY > 400) } window.addEventListener('scroll', handleScroll) return () => window.removeEventListener('scroll', handleScroll) }, []) // 검색어 디바운스 (300ms) // 유전자 데이터가 너무 많아서 검색창에 입력할 때마다 모든 데이터를 필터링하지 않고 // 검색어가 변경된 후 300ms 후에 필터링을 적용 useEffect(() => { const timer = setTimeout(() => { setGeneSearchKeyword(geneSearchInput) setGeneCurrentLoadedPage(1) }, 300) return () => clearTimeout(timer) }, [geneSearchInput]) // 유전자 테이블 무한 스크롤: geneCurrentLoadedPage가 변경되면 localStorage에 저장 useEffect(() => { if (typeof window !== 'undefined') { localStorage.setItem('geneCurrentLoadedPage', geneCurrentLoadedPage.toString()) } }, [geneCurrentLoadedPage]) // ======================================== // 헬퍼 함수 // ======================================== const scrollToTop = () => { window.scrollTo({ top: 0, behavior: 'smooth' }) } // 부 KPN 배지 렌더링 (분석불가/일치/불일치) // 항상 분석 불가 상태를 체크 후에 데이터를 보여줘야함 const renderSireBadge = (chipSireName: string | null | undefined, size: 'sm' | 'lg' = 'lg') => { const sizeClasses = size === 'lg' ? 'gap-1.5 text-sm px-3 py-1.5' : 'gap-1 text-xs px-2 py-1' const iconSize = size === 'lg' ? 'w-4 h-4' : 'w-3 h-3' // 분석불가 개체 먼저 체크 (EXCLUDED_COW_IDS 또는 DB에서 '분석불가'/'정보없음'으로 저장된 경우) if (isExcludedCow(cow?.cowId) || chipSireName === '분석불가' || chipSireName === '정보없음') { return ( 분석불가 ) } else if (chipSireName === '일치') { return ( 일치 ) } else if (chipSireName && chipSireName !== '일치') { return ( 불일치 ) } return null } // 모 개체 배지 렌더링 (일치/불일치/이력제부재) // 모 불일치일 경우도 유전체 분석결과가 안나옴 체크 후 데이터를 보여줘야함 const renderDamBadge = (chipDamName: string | null | undefined, size: 'sm' | 'lg' = 'lg') => { // 분석불가 개체는 어미 배지 표시 안 함 if (isExcludedCow(cow?.cowId)) return null const sizeClasses = size === 'lg' ? 'gap-1.5 text-sm px-3 py-1.5' : 'gap-1 text-xs px-2 py-1' const iconSize = size === 'lg' ? 'w-4 h-4' : 'w-3 h-3' if (chipDamName === '일치') { return ( 일치 ) } else if (chipDamName === '불일치') { return ( 불일치 ) } else if (chipDamName === '이력제부재') { return ( 이력제부재 ) } return null } // 유전자 데이터 지연 로드 함수 const loadGeneData = async () => { if (geneDataLoaded || geneDataLoading) return // 이미 로드했거나 로딩 중이면 스킵 setGeneDataLoading(true) try { const geneDataResult = await geneApi.findByCowId(cowNo) const geneList = geneDataResult || [] setGeneData(geneList) setGeneDataLoaded(true) setHasGeneData(geneList.length > 0) } catch (geneErr) { console.error('유전자 데이터 조회 실패:', geneErr) setGeneData([]) setGeneDataLoaded(true) setHasGeneData(false) } finally { setGeneDataLoading(false) } } // 탭 변경 핸들러 // 유전자 탭이 활성화되면 유전자 데이터 로드 const handleTabChange = (value: string) => { setActiveTab(value) if (value === 'gene' && !geneDataLoaded) { loadGeneData() } } 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) // 1. 개체 정보 조회 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) if (cowData.dataStatus) { setHasGeneData(cowData.dataStatus.hasGeneData) } // 2. 유전체 데이터 조회 const genomeDataResult = await genomeApi.findByCowNo(cowNo) setGenomeData(genomeDataResult) const genomeExists = genomeDataResult.length > 0 && !!genomeDataResult[0].genomeCows && genomeDataResult[0].genomeCows.length > 0 setHasGenomeData(genomeExists) // 3. 분석 의뢰 정보 조회 (친자감별 결과) try { const requestData = await genomeApi.getRequest(cowNo) setGenomeRequest(requestData) } catch (reqErr) { console.error('분석 의뢰 정보 조회 실패:', reqErr) setGenomeRequest(null) } // 4. 번식능력(MPT) 데이터 조회 try { const mptData = await mptApi.findByCowId(cowNo) setHasReproductionData(mptData && mptData.length > 0) } catch { setHasReproductionData(false) } // 5. 탭 자동 선택 (목록에서 진입하거나 저장된 탭이 없을 때만) if (from === 'list' || (typeof window !== 'undefined' && !localStorage.getItem(`cowDetailActiveTab_${cowNo}`))) { if (genomeExists) { setActiveTab('genome') } else if (geneData && geneData.length > 0) { setActiveTab('gene') } } // 6. 비교 데이터 + 선발지수 조회 if (genomeDataResult.length > 0) { try { const comparisonData = await genomeApi.getComparisonAverages(cowNo) setComparisonAverages(comparisonData) const traitComparisonData = await genomeApi.getTraitComparisonAverages(cowNo) setTraitComparisonAverages(traitComparisonData) // 선발지수 계산 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]) // ======================================== // 계산된 데이터 (useMemo) // - 의존성 변경 시에만 재계산하여 성능 최적화 // ======================================== // API 응답 형질 데이터 (변환 없이 직접 사용) const GENOMIC_TRAITS = useMemo(() => genomeData[0]?.genomeCows || [], [genomeData]) // 형질 카테고리 목록 (성장/생산/체형/무게/비율) const CATEGORIES = useMemo(() => [...new Set(GENOMIC_TRAITS.map(t => t.traitCategory).filter((cat): cat is string => !!cat))], [GENOMIC_TRAITS]) // 종합 선발지수 점수 (API 값 우선, 없으면 형질 평균) 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), 0) / GENOMIC_TRAITS.length }, [GENOMIC_TRAITS, selectionIndex]) // 종합 백분위 (API 값 우선, 없으면 CDF 계산) 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), 0) / GENOMIC_TRAITS.length }, [GENOMIC_TRAITS, selectionIndex, overallScore]) // 카테고리별 평균 육종가/백분위 (카드 표시용) const categoryStats = useMemo(() => { return CATEGORIES.map(cat => { const traits = GENOMIC_TRAITS.filter(t => t.traitCategory === cat) const avgBreedVal = traits.reduce((sum, t) => sum + (t.breedVal || 0), 0) / traits.length const avgPercentile = traits.reduce((sum, t) => sum + (t.percentile || 0), 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 } return comparisonAverages.farm.reduce((sum, cat) => sum + cat.avgEbv, 0) / comparisonAverages.farm.length }, [comparisonAverages, overallScore]) // 지역 평균 Z-Score (정규분포 차트용) const regionAvgZ = useMemo(() => { if (!comparisonAverages?.region || comparisonAverages.region.length === 0) return -0.2 return comparisonAverages.region.reduce((sum, cat) => sum + cat.avgEbv, 0) / 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.traitName || '')) : GENOMIC_TRAITS if (targetTraits.length === 0) return null return targetTraits.reduce((sum, t) => sum + (t.traitVal || 0), 0) / targetTraits.length }, [GENOMIC_TRAITS, filters.traitWeights]) // 농가 EPD 평균 const farmAvgEpdValue = useMemo(() => { if (!comparisonAverages?.farm || comparisonAverages.farm.length === 0) return null return comparisonAverages.farm.reduce((sum, cat) => sum + (cat.avgEpd || 0), 0) / comparisonAverages.farm.length }, [comparisonAverages]) // 지역 EPD 평균 const regionAvgEpdValue = useMemo(() => { if (!comparisonAverages?.region || comparisonAverages.region.length === 0) return null return comparisonAverages.region.reduce((sum, cat) => sum + (cat.avgEpd || 0), 0) / 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.traitName || '')) }, [filters.traitWeights, GENOMIC_TRAITS]) // 정규분포 곡선 데이터 (전국/지역/농가 비교 차트) const multiDistribution = useMemo(() => generateMultipleDistributions(0, 1, regionAvgZ, 1, farmAvgZ, 1), [regionAvgZ, farmAvgZ]) // 유전자 데이터 필터링 및 정렬 (useMemo로 최상위에서 관리) const filteredAndSortedGeneData = useMemo(() => { 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 }) // 정렬 return [...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) }) }, [geneData, geneSearchKeyword, genotypeFilter, geneSortBy, geneSortOrder]) const toggleTraitSelection = (traitId: number) => { setSelectedTraits(prev => prev.includes(traitId) ? prev.filter(id => id !== traitId) : [...prev, traitId] ) } // 유전자 테이블 스크롤 핸들러 (간단하게 함수로만 정의) const handleGeneTableScroll = (e: React.UIEvent) => { const target = e.currentTarget const { scrollTop, scrollHeight, clientHeight } = target const isNearBottom = scrollHeight - scrollTop - clientHeight < 100 if (isNearBottom && !isLoadingMoreGenes) { const totalPages = Math.ceil(filteredAndSortedGeneData.length / genesPerPage) if (geneCurrentLoadedPage < totalPages) { setIsLoadingMoreGenes(true) setTimeout(() => { setGeneCurrentLoadedPage(prev => prev + 1) setIsLoadingMoreGenes(false) }, 300) } } } if (loading) { return (

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

) } // 본문시작 ==================================================================================================== return (
{/* 메인 컨테이너 여백 : p-6 */}
{/* 헤더: 뒤로가기 + 타이틀 + 다운로드 */}
{/* 뒤로가기 버튼 */} {/* 아이콘 + 타이틀 (클릭시 새로고침) */}
{/* 탭 네비게이션 */} 유전체 {hasGenomeData && isValidGenomeAnalysis(genomeRequest?.chipSireName, genomeRequest?.chipDamName, cow?.cowId) ? '완료' : '미검사'} 유전자 {hasGeneData && !(isExcludedCow(cow?.cowId) || genomeRequest?.chipSireName === '분석불가' || genomeRequest?.chipSireName === '정보없음') ? '완료' : '미검사'} 번식능력 {hasReproductionData ? '완료' : '미검사'} {/* 탭 콘텐츠 영역 */}
{/* 유전체 분석 탭 */} {hasGenomeData ? ( <> {/* 개체 정보 섹션 */}

개체 정보

{/* 모바일: 세로 리스트 / 데스크탑: 가로 그리드 */}
개체번호
생년월일
{cow?.cowBirthDt ? new Date(cow.cowBirthDt).toLocaleDateString('ko-KR') : '-'}
월령 (분석일 기준)
{cow?.cowBirthDt && genomeData[0]?.request?.requestDt ? `${Math.floor((new Date(genomeData[0].request.requestDt).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 && genomeData[0]?.request?.requestDt ? `${Math.floor((new Date(genomeData[0].request.requestDt).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 : '-'} {renderSireBadge(genomeRequest?.chipSireName)}
모 개체번호
{cow?.damCowId && cow.damCowId !== '0' ? ( ) : ( - )} {renderDamBadge(genomeRequest?.chipDamName)}
{/* 모바일: 좌우 배치 리스트 */}
부 KPN번호
{cow?.sireKpn && cow.sireKpn !== '0' ? cow.sireKpn : '-'} {renderSireBadge(genomeRequest?.chipSireName, 'sm')}
모 개체번호
{cow?.damCowId && cow.damCowId !== '0' ? ( ) : ( - )} {renderDamBadge(genomeRequest?.chipDamName, 'sm')}
{/* 친자확인 결과에 따른 분기 (유효성 조건: 아비일치 + 어미불일치/이력제부재 제외 + 제외목록) */} {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} selectionIndexHistogram={selectionIndex?.histogram || []} regionTotal={selectionIndex?.regionTotal} chartFilterTrait={chartFilterTrait} onChartFilterTraitChange={setChartFilterTrait} />
{/* 유전체 형질별 육종가 비교 */}

유전체 형질별 육종가 비교

선택 형질 상세

분석 정보

본 유전체 분석 결과는 국가단위 '한우암소 유전체 분석 서비스'에서 제공하는 자료입니다.

농림축산식품부-국립축산과학원-농협한우개량사업소-도축산연구소는 협력 체계를 구축하여 농가 암소의 유전체 유전능력을 조기에 분석하여 개량에 활용할 수 있도록 서비스 하고 있습니다.

암소의 유전체 유전능력은 국가단위 보증씨수소 유전능력 평가결과를 활용하여 6개월 단위로 자료를 갱신하고 있으며, 이번 평가결과는 '25.8.1. ~ '26.1.31.까지 유효합니다.

씨수소 참조집단의 유전체(SNP) 분석칩과 암소의 능력 계산에 이용하는 암소의 유전체 분석칩이 다를 경우, 암소의 형질별 유전체 육종가 값이 일부 차이가 날 수 있습니다.

접수일
{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 && genomeData[0]?.request?.requestDt ? `${Math.floor((new Date(genomeData[0].request.requestDt).getTime() - new Date(cow.cowBirthDt).getTime()) / (1000 * 60 * 60 * 24 * 30.44))}개월` : '-'}
유전체 분석일자
-
{/* 모바일: 좌우 배치 리스트 */}
개체번호
생년월일 {cow?.cowBirthDt ? new Date(cow.cowBirthDt).toLocaleDateString('ko-KR') : '-'}
월령 (분석) {cow?.cowBirthDt && genomeData[0]?.request?.requestDt ? `${Math.floor((new Date(genomeData[0].request.requestDt).getTime() - new Date(cow.cowBirthDt).getTime()) / (1000 * 60 * 60 * 24 * 30.44))}개월` : '-'}
분석일자 -
{/* 친자확인 섹션 */}

혈통정보

{/* 데스크탑: 가로 그리드 */}
부 KPN번호
{cow?.sireKpn && cow.sireKpn !== '0' ? cow.sireKpn : '-'} {renderSireBadge(genomeRequest?.chipSireName)}
모 개체번호
{cow?.damCowId && cow.damCowId !== '0' ? ( ) : ( - )} {renderDamBadge(genomeRequest?.chipDamName)}
{/* 모바일: 세로 리스트 */}
부 KPN
{cow?.sireKpn && cow.sireKpn !== '0' ? cow.sireKpn : '-'} {renderSireBadge(genomeRequest?.chipSireName, 'sm')}
모 개체번호
{cow?.damCowId && cow.damCowId !== '0' ? ( ) : ( - )} {renderDamBadge(genomeRequest?.chipDamName, 'sm')}
{/* 분석불가 메시지 */}

유전체 분석 결과

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

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

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

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

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

개체 정보

{/* 데스크탑: 가로 그리드 */}
개체번호
생년월일
{cow?.cowBirthDt ? new Date(cow.cowBirthDt).toLocaleDateString('ko-KR') : '-'}
월령 (분석일 기준)
{cow?.cowBirthDt && genomeData[0]?.request?.requestDt ? `${Math.floor((new Date(genomeData[0].request.requestDt).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 && genomeData[0]?.request?.requestDt ? `${Math.floor((new Date(genomeData[0].request.requestDt).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 : '-'} {renderSireBadge(genomeRequest?.chipSireName)}
모 개체번호
{cow?.damCowId && cow.damCowId !== '0' ? ( ) : ( - )} {renderDamBadge(genomeRequest?.chipDamName)}
{/* 모바일: 좌우 배치 리스트 */}
부 KPN번호
{cow?.sireKpn && cow.sireKpn !== '0' ? cow.sireKpn : '-'} {renderSireBadge(genomeRequest?.chipSireName, 'sm')}
모 개체번호
{cow?.damCowId && cow.damCowId !== '0' ? ( ) : ( - )} {renderDamBadge(genomeRequest?.chipDamName, 'sm')}
{/* 유전자 검색 및 필터 섹션 */}

유전자 분석 결과

{/* 유전자 탭 분기: 분석불가/정보없음만 차단, 불일치/이력제부재는 유전자 데이터 표시 */} {!(isExcludedCow(cow?.cowId) || genomeRequest?.chipSireName === '분석불가' || genomeRequest?.chipSireName === '정보없음') ? ( <>
{/* 검색창 */}
setGeneSearchInput(e.target.value)} />
{/* 필터 옵션들 */} {/*
*/} {/* 유전자 타입 필터 */} {/*
구분:
*/} {/* 정렬 드롭다운 */} {/*
*/} {/*
*/}
{/* 유전자 테이블/카드 */} {(() => { // 무한 스크롤 계산 const totalItems = geneCurrentLoadedPage * genesPerPage const displayData = filteredAndSortedGeneData.length > 0 ? filteredAndSortedGeneData.slice(0, totalItems) : [] return ( <> {/* 데스크톱: 테이블 */}
{displayData.map((gene, idx) => ( ))} {isLoadingMoreGenes && ( )}
SNP 이름 염색체 위치 Position SNP 구분 첫번째 대립유전자 두번째 대립유전자 설명
{gene.snpName || '-'} {gene.chromosome || '-'} {gene.position || '-'} {gene.snpType || '-'} {gene.allele1 || '-'} {gene.allele2 || '-'} {gene.remarks || '-'}
로딩 중...
{/* 현황 정보 표시 */}
{filteredAndSortedGeneData.length > 0 ? ( <> 전체 {filteredAndSortedGeneData.length.toLocaleString()}개 중 1-{displayData.length.toLocaleString()}번째 {isLoadingMoreGenes && ' (로딩 중...)'} ) : ( '데이터 없음' )}
{/* 모바일: 카드 뷰 */}
{displayData.map((gene, idx) => (
SNP 이름 {gene?.snpName || '-'}
염색체 위치 {gene?.chromosome || '-'}
Position {gene?.position || '-'}
SNP 구분 {gene?.snpType || '-'}
첫번째 대립유전자 {gene?.allele1 || '-'}
두번째 대립유전자 {gene?.allele2 || '-'}
설명 {gene?.remarks || '-'}
))} {isLoadingMoreGenes && (
로딩 중...
)}
{/* 현황 정보 표시 */}
{filteredAndSortedGeneData.length > 0 ? ( <> 전체 {filteredAndSortedGeneData.length.toLocaleString()}개 중 1-{displayData.length.toLocaleString()}번째 {isLoadingMoreGenes && ' (로딩 중...)'} ) : ( '데이터 없음' )}
) })()} ) : (

{getInvalidReason(genomeRequest?.chipSireName, genomeRequest?.chipDamName, cow?.cowId) || '유전자 분석 불가'}

{getInvalidMessage(genomeRequest?.chipSireName, genomeRequest?.chipDamName, cow?.cowId).replace('유전체', '유전자')}

)} ) : ( <> {/* 개체 정보 섹션 */}

개체 정보

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

혈통정보

{/* 데스크탑: 가로 그리드 */}
부 KPN번호
{cow?.sireKpn && cow.sireKpn !== '0' ? cow.sireKpn : '-'} {renderSireBadge(genomeRequest?.chipSireName)}
모 개체번호
{cow?.damCowId && cow.damCowId !== '0' ? ( ) : ( - )} {renderDamBadge(genomeRequest?.chipDamName)}
{/* 모바일: 세로 리스트 */}
부 KPN
{cow?.sireKpn && cow.sireKpn !== '0' ? cow.sireKpn : '-'} {renderSireBadge(genomeRequest?.chipSireName, 'sm')}
모 개체번호
{cow?.damCowId && cow.damCowId !== '0' ? ( ) : ( - )} {renderDamBadge(genomeRequest?.chipDamName, 'sm')}
{/* 유전자 분석 결과 섹션 */}

유전자 분석 결과

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

{genomeRequest ? getInvalidMessage(genomeRequest?.chipSireName, genomeRequest?.chipDamName, cow?.cowId).replace('유전체', '유전자') : '이 개체는 아직 유전자(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} selectionIndexHistogram={selectionIndex?.histogram || []} onHighlightModeChange={setHighlightMode} chartFilterTrait={chartFilterTrait} onChartFilterTraitChange={setChartFilterTrait} />
{/* 플로팅 맨 위로 버튼 - 글래스모피즘 */} {showScrollTop && ( )}
) }