Files
genome2025/frontend/src/components/genome/genome-traits-table.tsx
2025-12-24 08:25:44 +09:00

200 lines
7.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client'
import { BarChart3 } from "lucide-react"
import { useEffect, useState } from "react"
import apiClient from "@/lib/api-client"
interface TraitData {
trait: string
regional: number
myFarm: number
}
interface GenomeTraitsTableProps {
farmNo?: number | null
}
export function GenomeTraitsTable({ farmNo }: GenomeTraitsTableProps) {
const [traitData, setTraitData] = useState<TraitData[]>([])
const [loading, setLoading] = useState(true)
useEffect(() => {
const fetchTraitData = async () => {
if (!farmNo) {
setLoading(false)
return
}
try {
const traits = [
{ name: '12개월령체중', key: '12개월령체중' },
{ name: '도체중', key: '도체중' },
{ name: '근내지방도', key: '근내지방도' },
{ name: '등심단면적', key: '등심단면적' },
{ name: '등지방두께', key: '등지방두께' },
]
const results: TraitData[] = []
for (const trait of traits) {
try {
const farmResponse = await apiClient.post('/cow/ranking', {
filterOptions: { farmNo: farmNo },
rankingOptions: {
criteriaType: 'GENOME',
traitConditions: [{ traitNm: trait.key, weight: 1.0 }]
}
})
const globalResponse = await apiClient.post('/cow/ranking/global', {
rankingOptions: {
criteriaType: 'GENOME',
traitConditions: [{ traitNm: trait.key, weight: 1.0 }]
}
})
const farmResult = farmResponse.data || farmResponse
const globalResult = globalResponse.data || globalResponse
const farmScores = farmResult.items?.map((item: any) => item.sortValue) || []
const farmAvg = farmScores.length > 0
? farmScores.reduce((sum: number, score: number) => sum + score, 0) / farmScores.length
: 0
const globalScores = globalResult.items?.map((item: any) => item.sortValue) || []
const regionalAvg = globalScores.length > 0
? globalScores.reduce((sum: number, score: number) => sum + score, 0) / globalScores.length
: 0
results.push({
trait: trait.name,
myFarm: parseFloat(farmAvg.toFixed(2)),
regional: parseFloat(regionalAvg.toFixed(2))
})
} catch (error) {
console.error(`[형질 테이블] ${trait.name} 데이터 로드 실패:`, error)
}
}
setTraitData(results)
} catch (error) {
console.error('[형질 테이블] 전체 데이터 로드 실패:', error)
setTraitData([])
} finally {
setLoading(false)
}
}
fetchTraitData()
}, [farmNo])
const getTraitShortName = (name: string) => {
const shortNames: Record<string, string> = {
'12개월령체중': '12개월령체중',
'등심단면적': '등심단면적',
'등지방두께': '등지방두께',
'근내지방도': '근내지방도',
'도체중': '도체중'
}
return shortNames[name] || name
}
if (loading) {
return (
<div className="bg-white rounded-xl border border-slate-100 p-4">
<div className="flex items-center justify-center h-[180px]">
<div className="w-6 h-6 border-2 border-slate-200 border-t-[#1482B0] rounded-full animate-spin"></div>
</div>
</div>
)
}
return (
<div className="bg-white rounded-xl border border-slate-100 overflow-hidden">
{/* 헤더 */}
<div className="px-4 py-3 border-b border-slate-100">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2.5">
<div className="w-8 h-8 rounded-lg bg-gradient-to-br from-[#1482B0] to-[#0d5f82] flex items-center justify-center shadow-md shadow-[#1482B0]/20">
<BarChart3 className="w-4 h-4 text-white" />
</div>
<div>
<h3 className="text-sm font-semibold text-slate-900"> </h3>
<p className="text-[10px] text-slate-500"> </p>
</div>
</div>
<span className="px-2 py-0.5 bg-slate-100 text-slate-600 rounded-lg text-[10px] font-semibold">5 </span>
</div>
</div>
{/* 콘텐츠 */}
<div className="p-5">
{traitData.length === 0 ? (
<div className="flex items-center justify-center py-8 text-xs text-slate-400">
.
</div>
) : (
<div className="space-y-4">
{traitData.map((item, idx) => {
const diff = item.myFarm - item.regional
const isPositive = diff >= 0
// σ를 0~100 스케일로 변환 (-3σ~+3σ → 0~100)
const toPercent = (sigma: number) => Math.min(100, Math.max(0, ((sigma + 3) / 6) * 100))
return (
<div key={idx} className="space-y-1.5">
{/* 형질명 + 차이 */}
<div className="flex items-center justify-between">
<span className="text-xs font-semibold text-slate-800">
{getTraitShortName(item.trait)}
</span>
<span className={`text-xs font-bold px-2.5 py-1 rounded-lg ${diff >= 0.3 ? 'bg-emerald-50 text-emerald-700 border border-emerald-200 shadow-sm' :
diff <= -0.3 ? 'bg-amber-50 text-amber-700 border border-amber-200 shadow-sm' :
'bg-slate-50 text-slate-700 border border-slate-200'
}`}>
{diff > 0 ? '+' : ''}{diff.toFixed(1)}σ
</span>
</div>
{/* 비교 바 */}
<div className="relative h-6 bg-slate-100 rounded-lg overflow-hidden shadow-inner">
{/* 보은군 바 */}
<div
className="absolute top-0 left-0 h-full bg-slate-300/80 rounded-lg transition-all duration-500"
style={{ width: `${toPercent(item.regional)}%` }}
/>
{/* 내농장 바 */}
<div
className={`absolute top-0 left-0 h-full rounded-lg transition-all duration-500 shadow-sm ${isPositive
? 'bg-gradient-to-r from-[#1482B0] via-[#1482B0] to-[#0d5f82]'
: 'bg-gradient-to-r from-slate-400 to-slate-500'
}`}
style={{ width: `${toPercent(item.myFarm)}%` }}
/>
</div>
{/* 라벨 */}
<div className="flex items-center justify-between text-[11px]">
<div className="flex items-center gap-1.5">
<div className="w-2.5 h-2.5 rounded-full bg-[#1482B0] shadow-sm"></div>
<span className="text-slate-600 font-medium">
<span className="font-bold text-slate-900">{item.myFarm > 0 ? '+' : ''}{item.myFarm}σ</span>
</span>
</div>
<div className="flex items-center gap-1.5">
<div className="w-2.5 h-2.5 rounded-full bg-slate-300 shadow-sm"></div>
<span className="text-slate-500 font-medium">
{item.regional > 0 ? '+' : ''}{item.regional}σ
</span>
</div>
</div>
</div>
)
})}
</div>
)}
</div>
</div>
)
}