페이지 수정사항 반영

This commit is contained in:
2025-12-12 08:01:59 +09:00
parent 7d15c9be7c
commit dce58470b6
20 changed files with 1080 additions and 155 deletions

View File

@@ -10,7 +10,7 @@ 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 { ComparisonAveragesDto, TraitComparisonAveragesDto, cowApi, genomeApi, geneApi, GeneDetail } from "@/lib/api"
import { ComparisonAveragesDto, TraitComparisonAveragesDto, cowApi, genomeApi, geneApi, GeneDetail, GenomeRequestDto } from "@/lib/api"
import { CowDetail } from "@/types/cow.types"
import { GenomeTrait } from "@/types/genome.types"
import { useGlobalFilter } from "@/contexts/GlobalFilterContext"
@@ -34,6 +34,7 @@ import { TraitDistributionCharts } from "./genome/_components/trait-distribution
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"
// 형질명 → 카테고리 매핑 (한우 35개 형질)
const TRAIT_CATEGORY_MAP: Record<string, string> = {
@@ -156,6 +157,9 @@ export default function CowOverviewPage() {
const [hasGeneData, setHasGeneData] = useState(false)
const [hasReproductionData, setHasReproductionData] = useState(false)
// 분석 의뢰 정보 (친자감별 결과 포함)
const [genomeRequest, setGenomeRequest] = useState<GenomeRequestDto | null>(null)
// 선발지수 상태
const [selectionIndex, setSelectionIndex] = useState<{
score: number | null;
@@ -259,6 +263,15 @@ export default function CowOverviewPage() {
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) 데이터 가져오기
try {
const geneDataResult = await geneApi.findByCowId(cowNo)
@@ -305,11 +318,11 @@ export default function CowOverviewPage() {
'우둔rate', '설도rate', '사태rate', '양지rate', '갈비rate',
]
// 필터가 활성화되고 형질이 선택되어 있으면 가중치 조건 생성 (대시보드와 동일 로직)
// 필터가 활성화되고 형질이 선택되어 있으면 가중치 조건 생성 (리스트와 동일 로직)
const finalConditions = filters.isActive && filters.selectedTraits && filters.selectedTraits.length > 0
? filters.selectedTraits.map(traitNm => ({
traitNm,
weight: (filters.traitWeights as Record<string, number>)[traitNm] || 1
weight: ((filters.traitWeights as Record<string, number>)[traitNm] || 100) / 100 // 0-100 → 0-1로 정규화
}))
: ALL_TRAITS.map(traitNm => ({ traitNm, weight: 1 }))
@@ -463,6 +476,7 @@ export default function CowOverviewPage() {
}
return (
<AuthGuard>
<SidebarProvider>
<AppSidebar />
<SidebarInset>
@@ -625,7 +639,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 break-all">
{cow?.sireKpn || '-'}
{cow?.sireKpn && cow.sireKpn !== '0' ? cow.sireKpn : '-'}
</span>
{(() => {
const chipSireName = genomeData[0]?.request?.chipSireName
@@ -658,8 +672,8 @@ export default function CowOverviewPage() {
<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 ? (
<CowNumberDisplay cowId={cow?.damCowId} variant="highlight" className="text-2xl font-bold text-foreground" />
{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>
)}
@@ -700,7 +714,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 break-all">
{cow?.sireKpn || '-'}
{cow?.sireKpn && cow.sireKpn !== '0' ? cow.sireKpn : '-'}
</span>
{(() => {
const chipSireName = genomeData[0]?.request?.chipSireName
@@ -731,8 +745,8 @@ export default function CowOverviewPage() {
<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 ? (
<CowNumberDisplay cowId={cow?.damCowId} variant="highlight" className="text-base font-bold text-foreground" />
{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>
)}
@@ -842,7 +856,7 @@ export default function CowOverviewPage() {
<Card className="bg-white border border-border rounded-xl overflow-hidden">
<CardContent className="p-0">
<div className="grid grid-cols-2 sm:grid-cols-4 divide-x divide-y sm:divide-y-0 divide-border">
<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">
@@ -865,12 +879,6 @@ export default function CowOverviewPage() {
{genomeData[0]?.request?.chipType || '-'}
</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?.chipNo || '-'}
</div>
</div>
</div>
</CardContent>
</Card>
@@ -910,15 +918,253 @@ export default function CowOverviewPage() {
)}
</>
) : (
<Card className="bg-slate-50 border border-border rounded-2xl">
<CardContent className="p-8 text-center">
<BarChart3 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>
<>
{/* 개체 정보 섹션 */}
<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>
@@ -1023,7 +1269,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 break-all">
{cow?.sireKpn || '-'}
{cow?.sireKpn && cow.sireKpn !== '0' ? cow.sireKpn : '-'}
</span>
{(() => {
const chipSireName = genomeData[0]?.request?.chipSireName
@@ -1056,8 +1302,8 @@ export default function CowOverviewPage() {
<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 ? (
<CowNumberDisplay cowId={cow?.damCowId} variant="highlight" className="text-2xl font-bold text-foreground" />
{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>
)}
@@ -1097,7 +1343,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 break-all">
{cow?.sireKpn || '-'}
{cow?.sireKpn && cow.sireKpn !== '0' ? cow.sireKpn : '-'}
</span>
{(() => {
const chipSireName = genomeData[0]?.request?.chipSireName
@@ -1128,8 +1374,8 @@ export default function CowOverviewPage() {
<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 ? (
<CowNumberDisplay cowId={cow?.damCowId} variant="highlight" className="text-base font-bold text-foreground" />
{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>
)}
@@ -1220,43 +1466,6 @@ export default function CowOverviewPage() {
</div>
</div>
{/* 유전자형 필터 */}
<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={() => setGenotypeFilter('all')}
className={`px-3 py-1.5 rounded-md text-sm font-medium transition-colors ${
genotypeFilter === 'all'
? 'bg-white text-slate-900 shadow-sm'
: 'text-slate-500 hover:text-slate-700'
}`}
>
</button>
<button
onClick={() => setGenotypeFilter('homozygous')}
className={`px-3 py-1.5 rounded-md text-sm font-medium transition-colors ${
genotypeFilter === 'homozygous'
? 'bg-white text-slate-900 shadow-sm'
: 'text-slate-500 hover:text-slate-700'
}`}
>
</button>
<button
onClick={() => setGenotypeFilter('heterozygous')}
className={`px-3 py-1.5 rounded-md text-sm font-medium transition-colors ${
genotypeFilter === 'heterozygous'
? '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
@@ -1545,5 +1754,6 @@ export default function CowOverviewPage() {
</DialogContent>
</Dialog>
</SidebarProvider>
</AuthGuard>
)
}