개체분석 상태 값 수정

This commit is contained in:
2025-12-19 15:19:50 +09:00
parent abc2f20495
commit c8bd04f124
24 changed files with 596 additions and 1499 deletions

View File

@@ -1,43 +1,40 @@
'use client'
import { useSearchParams, useParams, useRouter } from "next/navigation"
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 { 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,
ArrowUp,
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 { SidebarInset, SidebarProvider } from "@/components/ui/sidebar"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { useGlobalFilter } from "@/contexts/GlobalFilterContext"
import { useMediaQuery } from "@/hooks/use-media-query"
import { useToast } from "@/hooks/use-toast"
import { ComparisonAveragesDto, cowApi, geneApi, GeneDetail, genomeApi, GenomeRequestDto, TraitComparisonAveragesDto } from "@/lib/api"
import { getInvalidMessage, getInvalidReason, isExcludedCow, isValidGenomeAnalysis } from "@/lib/utils/genome-analysis-config"
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 { 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> = {
@@ -254,6 +251,74 @@ export default function CowOverviewPage() {
const [geneSortBy, setGeneSortBy] = useState<'snpName' | 'chromosome' | 'position' | 'snpType' | 'allele1' | 'allele2' | 'remarks'>('snpName')
const [geneSortOrder, setGeneSortOrder] = useState<'asc' | 'desc'>('asc')
// 부 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 (
<span className={`flex items-center ${sizeClasses} bg-slate-400 text-white font-semibold rounded-full shrink-0`}>
<XCircle className={iconSize} />
<span></span>
</span>
)
} else if (chipSireName === '일치') {
return (
<span className={`flex items-center ${sizeClasses} bg-primary text-primary-foreground font-semibold rounded-full shrink-0`}>
<CheckCircle2 className={iconSize} />
<span></span>
</span>
)
} else if (chipSireName && chipSireName !== '일치') {
return (
<span className={`flex items-center ${sizeClasses} bg-red-500 text-white font-semibold rounded-full shrink-0`}>
<XCircle className={iconSize} />
<span></span>
</span>
)
}
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 (
<span className={`flex items-center ${sizeClasses} bg-primary text-primary-foreground font-semibold rounded-full shrink-0`}>
<CheckCircle2 className={iconSize} />
<span></span>
</span>
)
} else if (chipDamName === '불일치') {
return (
<span className={`flex items-center ${sizeClasses} bg-red-500 text-white font-semibold rounded-full shrink-0`}>
<XCircle className={iconSize} />
<span></span>
</span>
)
} else if (chipDamName === '이력제부재') {
return (
<span className={`flex items-center ${sizeClasses} bg-amber-500 text-white font-semibold rounded-full shrink-0`}>
<XCircle className={iconSize} />
<span></span>
</span>
)
}
return null
}
// 유전자 데이터 지연 로드 함수
const loadGeneData = async () => {
if (geneDataLoaded || geneDataLoading) return // 이미 로드했거나 로딩 중이면 스킵
@@ -264,10 +329,12 @@ export default function CowOverviewPage() {
const geneList = geneDataResult || []
setGeneData(geneList)
setGeneDataLoaded(true)
setHasGeneData(geneList.length > 0)
} catch (geneErr) {
console.error('유전자 데이터 조회 실패:', geneErr)
setGeneData([])
setGeneDataLoaded(true)
setHasGeneData(false)
} finally {
setGeneDataLoading(false)
}
@@ -318,6 +385,11 @@ export default function CowOverviewPage() {
}
setCow(cowDetail)
// dataStatus에서 데이터 존재 여부 설정 (백엔드에서 가벼운 COUNT 쿼리로 확인)
if (cowData.dataStatus) {
setHasGeneData(cowData.dataStatus.hasGeneData)
}
// 유전체 데이터 가져오기
const genomeDataResult = await genomeApi.findByCowNo(cowNo)
setGenomeData(genomeDataResult)
@@ -333,9 +405,6 @@ export default function CowOverviewPage() {
setGenomeRequest(null)
}
// 유전자(SNP) 데이터는 탭 클릭 시 로드 (지연 로딩)
setHasGeneData(true) // 탭은 보여주되, 데이터는 나중에 로드
// 번식능력 데이터 (현재는 목업 - 추후 API 연동)
// TODO: 번식능력 API 연동
setHasReproductionData(false)
@@ -579,8 +648,8 @@ export default function CowOverviewPage() {
>
<Dna 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 ${hasGeneData && isValidGenomeAnalysis(genomeRequest?.chipSireName, genomeRequest?.chipDamName, cow?.cowId) ? 'bg-green-500 text-white' : 'bg-slate-300 text-slate-600'}`}>
{hasGeneData && isValidGenomeAnalysis(genomeRequest?.chipSireName, genomeRequest?.chipDamName, cow?.cowId) ? '완료' : '미검사'}
<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 ${hasGeneData && !(isExcludedCow(cow?.cowId) || genomeRequest?.chipSireName === '분석불가' || genomeRequest?.chipSireName === '정보없음') ? 'bg-green-500 text-white' : 'bg-slate-300 text-slate-600'}`}>
{hasGeneData && !(isExcludedCow(cow?.cowId) || genomeRequest?.chipSireName === '분석불가' || genomeRequest?.chipSireName === '정보없음') ? '완료' : '미검사'}
</span>
</TabsTrigger>
<TabsTrigger
@@ -695,30 +764,7 @@ export default function CowOverviewPage() {
<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>
)
}
})()}
{renderSireBadge(genomeRequest?.chipSireName)}
</div>
</div>
<div>
@@ -731,34 +777,7 @@ export default function CowOverviewPage() {
) : (
<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
}
})()}
{renderDamBadge(genomeRequest?.chipDamName)}
</div>
</div>
</div>
@@ -770,30 +789,7 @@ export default function CowOverviewPage() {
<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>
)
}
})()}
{renderSireBadge(genomeRequest?.chipSireName, 'sm')}
</div>
</div>
<div className="flex items-center">
@@ -804,34 +800,7 @@ export default function CowOverviewPage() {
) : (
<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
}
})()}
{renderDamBadge(genomeRequest?.chipDamName, 'sm')}
</div>
</div>
</div>
@@ -1062,30 +1031,7 @@ export default function CowOverviewPage() {
</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>
)
}
})()}
{renderSireBadge(genomeRequest?.chipSireName)}
</div>
</div>
<div>
@@ -1098,34 +1044,7 @@ export default function CowOverviewPage() {
) : (
<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
}
})()}
{renderDamBadge(genomeRequest?.chipDamName)}
</div>
</div>
</div>
@@ -1135,30 +1054,7 @@ export default function CowOverviewPage() {
<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>
)
}
})()}
{renderSireBadge(genomeRequest?.chipSireName, 'sm')}
</div>
</div>
<div className="flex items-center">
@@ -1169,34 +1065,7 @@ export default function CowOverviewPage() {
) : (
<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
}
})()}
{renderDamBadge(genomeRequest?.chipDamName, 'sm')}
</div>
</div>
</div>
@@ -1333,30 +1202,7 @@ export default function CowOverviewPage() {
<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>
)
}
})()}
{renderSireBadge(genomeRequest?.chipSireName)}
</div>
</div>
<div>
@@ -1369,33 +1215,7 @@ export default function CowOverviewPage() {
) : (
<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
}
})()}
{renderDamBadge(genomeRequest?.chipDamName)}
</div>
</div>
</div>
@@ -1407,30 +1227,7 @@ export default function CowOverviewPage() {
<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>
)
}
})()}
{renderSireBadge(genomeRequest?.chipSireName, 'sm')}
</div>
</div>
<div className="flex items-center">
@@ -1441,33 +1238,7 @@ export default function CowOverviewPage() {
) : (
<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
}
})()}
{renderDamBadge(genomeRequest?.chipDamName, 'sm')}
</div>
</div>
</div>
@@ -1477,8 +1248,8 @@ export default function CowOverviewPage() {
{/* 유전자 검색 및 필터 섹션 */}
<h3 className="text-lg lg:text-xl font-bold text-foreground"> </h3>
{/* 친자확인 결과에 따른 분기 */}
{isValidGenomeAnalysis(genomeRequest?.chipSireName, genomeRequest?.chipDamName, cow?.cowId) ? (
{/* 유전자 탭 분기: 분석불가/정보없음만 차단, 불일치/이력제부재는 유전자 데이터 표시 */}
{!(isExcludedCow(cow?.cowId) || genomeRequest?.chipSireName === '분석불가' || genomeRequest?.chipSireName === '정보없음') ? (
<>
<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">
{/* 검색창 */}
@@ -1867,15 +1638,154 @@ export default function CowOverviewPage() {
)}
</>
) : (
<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>
<>
{/* 개체 정보 섹션 */}
<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>
{renderSireBadge(genomeRequest?.chipSireName)}
</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>
)}
{renderDamBadge(genomeRequest?.chipDamName)}
</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>
{renderSireBadge(genomeRequest?.chipSireName, 'sm')}
</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>
)}
{renderDamBadge(genomeRequest?.chipDamName, 'sm')}
</div>
</div>
</div>
</CardContent>
</Card>
{/* 유전자 분석 결과 섹션 */}
<h3 className="text-lg lg:text-xl font-bold text-foreground"> </h3>
<Card className="bg-slate-100 border border-slate-300 rounded-2xl">
<CardContent className="p-8 text-center">
<Dna 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).replace('유전체', '유전자')
: '이 개체는 아직 유전자(SNP) 분석이 진행되지 않았습니다.'
}
</p>
</CardContent>
</Card>
</>
)}
</TabsContent>
@@ -1955,14 +1865,14 @@ export default function CowOverviewPage() {
</DialogContent>
</Dialog>
{/* 플로팅 맨 위로 버튼 */}
{/* 플로팅 맨 위로 버튼 - 글래스모피즘 */}
{showScrollTop && (
<button
onClick={scrollToTop}
className="fixed bottom-6 right-6 z-50 w-12 h-12 bg-primary text-white rounded-full shadow-lg hover:bg-primary/90 transition-all duration-300 flex items-center justify-center hover:scale-110 active:scale-95"
className="fixed bottom-4 right-4 sm:bottom-6 sm:right-6 z-50 w-10 h-10 sm:w-12 sm:h-12 bg-white/80 backdrop-blur-md border border-white/50 text-slate-700 rounded-full shadow-lg hover:bg-white/90 transition-all duration-300 flex items-center justify-center hover:scale-110 active:scale-95"
aria-label="맨 위로"
>
<ArrowUp className="w-6 h-6" />
<ChevronUp className="w-5 h-5 sm:w-6 sm:h-6" />
</button>
)}
</SidebarProvider>