화면 선발지수 수정 반영
This commit is contained in:
@@ -192,14 +192,19 @@ export function CategoryEvaluationCard({
|
||||
// 보은군/농가 형질별 평균 (데이터 없으면 0)
|
||||
const regionTraitAvg = traitAvgRegion?.avgEbv ?? 0
|
||||
const farmTraitAvg = traitAvgFarm?.avgEbv ?? 0
|
||||
// 보은군/농가 형질별 EPD(육종가) 평균
|
||||
const regionEpdAvg = traitAvgRegion?.avgEpd ?? 0
|
||||
const farmEpdAvg = traitAvgFarm?.avgEpd ?? 0
|
||||
|
||||
return {
|
||||
name: traitName,
|
||||
shortName: TRAIT_SHORT_NAMES[traitName] || traitName,
|
||||
breedVal: trait?.breedVal ?? 0, // 이 개체 표준화육종가 (σ)
|
||||
epd: trait?.actualValue ?? 0, // 이 개체 EPD (예상후대차이)
|
||||
regionVal: regionTraitAvg, // 보은군 평균
|
||||
farmVal: farmTraitAvg, // 농가 평균
|
||||
epd: trait?.actualValue ?? 0, // 이 개체 EPD (육종가)
|
||||
regionVal: regionTraitAvg, // 보은군 평균 (표준화육종가)
|
||||
farmVal: farmTraitAvg, // 농가 평균 (표준화육종가)
|
||||
regionEpd: regionEpdAvg, // 보은군 평균 (육종가)
|
||||
farmEpd: farmEpdAvg, // 농가 평균 (육종가)
|
||||
percentile: trait?.percentile ?? 50,
|
||||
category: trait?.category ?? '체형',
|
||||
diff: trait?.breedVal ?? 0,
|
||||
@@ -524,47 +529,45 @@ export function CategoryEvaluationCard({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 선택된 형질 정보 표시 (모바일 친화적) */}
|
||||
{/* 선택된 형질 정보 표시 - 육종가(EPD) 값으로 표시 */}
|
||||
{selectedTraitName && (() => {
|
||||
const selectedTrait = traitChartData.find(t => t.name === selectedTraitName)
|
||||
if (!selectedTrait) return null
|
||||
return (
|
||||
<div className="mx-4 mb-4 p-4 bg-slate-50 rounded-xl border border-slate-200 animate-fade-in">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<span className="text-lg font-bold text-foreground">{selectedTrait.shortName}</span>
|
||||
{/* 헤더: 형질명 + 닫기 */}
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<span className="px-4 py-1.5 bg-slate-200 text-slate-700 text-base font-bold rounded-full">
|
||||
{selectedTrait.shortName} 조회 기준
|
||||
</span>
|
||||
<button
|
||||
onClick={() => setSelectedTraitName(null)}
|
||||
className="text-muted-foreground hover:text-foreground p-1"
|
||||
className="text-muted-foreground hover:text-foreground p-1 hover:bg-slate-200 rounded-full transition-colors"
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="flex items-center gap-2">
|
||||
<span className="w-3 h-3 rounded" style={{ backgroundColor: '#10b981' }}></span>
|
||||
<span className="text-sm text-muted-foreground">보은군</span>
|
||||
</span>
|
||||
<span className="text-sm font-semibold text-foreground">
|
||||
{selectedTrait.regionVal > 0 ? '+' : ''}{selectedTrait.regionVal.toFixed(2)}σ
|
||||
{/* 3개 카드 그리드 */}
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
{/* 보은군 카드 */}
|
||||
<div className="flex flex-col items-center justify-center p-3 bg-white rounded-xl border-2 border-emerald-300 shadow-sm">
|
||||
<span className="text-xs text-muted-foreground mb-1 font-medium">보은군 평균</span>
|
||||
<span className="text-lg font-bold text-emerald-600">
|
||||
{selectedTrait.regionEpd?.toFixed(2) ?? '-'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="flex items-center gap-2">
|
||||
<span className="w-3 h-3 rounded" style={{ backgroundColor: '#1F3A8F' }}></span>
|
||||
<span className="text-sm text-muted-foreground">농가</span>
|
||||
</span>
|
||||
<span className="text-sm font-semibold text-foreground">
|
||||
{selectedTrait.farmVal > 0 ? '+' : ''}{selectedTrait.farmVal.toFixed(2)}σ
|
||||
{/* 농가 카드 */}
|
||||
<div className="flex flex-col items-center justify-center p-3 bg-white rounded-xl border-2 border-[#1F3A8F]/30 shadow-sm">
|
||||
<span className="text-xs text-muted-foreground mb-1 font-medium">농가 평균</span>
|
||||
<span className="text-lg font-bold text-[#1F3A8F]">
|
||||
{selectedTrait.farmEpd?.toFixed(2) ?? '-'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="flex items-center gap-2">
|
||||
<span className="w-3 h-3 rounded" style={{ backgroundColor: '#1482B0' }}></span>
|
||||
<span className="text-sm font-medium text-foreground">{formatCowNoShort(cowNo)} 개체</span>
|
||||
</span>
|
||||
<span className="text-sm font-bold text-primary">
|
||||
{selectedTrait.breedVal > 0 ? '+' : ''}{selectedTrait.breedVal.toFixed(2)}σ
|
||||
{/* 개체 카드 */}
|
||||
<div className="flex flex-col items-center justify-center p-3 bg-white rounded-xl border-2 border-[#1482B0]/30 shadow-sm">
|
||||
<span className="text-xs text-muted-foreground mb-1 font-medium">내 개체</span>
|
||||
<span className="text-lg font-bold text-[#1482B0]">
|
||||
{selectedTrait.epd?.toFixed(2) ?? '-'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -259,6 +259,8 @@ export function NormalDistributionChart({
|
||||
|
||||
// 차트 필터에 따른 표시 값 계산 (개체/농가/보은군 모두)
|
||||
// "내 개체 중심" 방식: 개체를 0에 고정하고 농가/보은군을 상대 위치로 표시
|
||||
// - 전체 선발지수: 선발지수 값 사용
|
||||
// - 개별 형질: 육종가(EPD) 값 사용
|
||||
const chartDisplayValues = useMemo(() => {
|
||||
let baseScore = overallScore
|
||||
let basePercentile = overallPercentile
|
||||
@@ -267,16 +269,17 @@ export function NormalDistributionChart({
|
||||
let baseRegionScore = regionAvgZ
|
||||
|
||||
if (chartFilterTrait !== 'overall') {
|
||||
// 선택된 형질 찾기
|
||||
const selectedTrait = selectedTraitData.find(t => t.name === chartFilterTrait)
|
||||
// 모든 형질에서 찾기 (selectedTraitData는 선택된 형질만 포함하므로 allTraits에서 찾아야 함)
|
||||
const selectedTrait = allTraits.find(t => t.name === chartFilterTrait)
|
||||
|
||||
if (selectedTrait) {
|
||||
baseScore = selectedTrait.breedVal
|
||||
// 개별 형질 선택 시: 육종가(EPD) 값 사용
|
||||
baseScore = selectedTrait.actualValue ?? 0 // 개체 육종가
|
||||
basePercentile = selectedTrait.percentile
|
||||
baseLabel = selectedTrait.name
|
||||
// API에서 가져온 형질별 농가/보은군 평균 사용 (없으면 0으로 - 데이터 로딩 중)
|
||||
baseFarmScore = traitRankData?.farmAvgEbv ?? 0
|
||||
baseRegionScore = traitRankData?.regionAvgEbv ?? 0
|
||||
// API에서 가져온 형질별 농가/보은군 평균 육종가(EPD) 사용
|
||||
baseFarmScore = traitRankData?.farmAvgEpd ?? 0
|
||||
baseRegionScore = traitRankData?.regionAvgEpd ?? 0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -299,19 +302,34 @@ export function NormalDistributionChart({
|
||||
cowVsFarm,
|
||||
cowVsRegion
|
||||
}
|
||||
}, [chartFilterTrait, overallScore, overallPercentile, selectedTraitData, traitRankData, farmAvgZ, regionAvgZ])
|
||||
}, [chartFilterTrait, overallScore, overallPercentile, allTraits, traitRankData, farmAvgZ, regionAvgZ])
|
||||
|
||||
// X축 범위 및 간격 계산 (내 개체 중심 방식)
|
||||
const xAxisConfig = useMemo(() => {
|
||||
const { cowVsFarm, cowVsRegion } = chartDisplayValues
|
||||
const maxDiff = Math.max(Math.abs(cowVsFarm), Math.abs(cowVsRegion))
|
||||
|
||||
// 차이가 3 초과면 1 단위, 이하면 0.5 단위
|
||||
const step = maxDiff > 3 ? 1 : 0.5
|
||||
// 데이터가 차트의 약 70% 영역에 들어오도록 범위 계산
|
||||
// maxDiff가 range의 70%가 되도록: range = maxDiff / 0.7
|
||||
const targetRange = maxDiff / 0.7
|
||||
|
||||
// 범위 계산 (최소 2.5, step 단위로 올림)
|
||||
const minRange = step === 1 ? 3 : 2.5
|
||||
const range = Math.max(minRange, Math.ceil(maxDiff / step) * step + step)
|
||||
// step 계산: 범위에 따라 적절한 간격 선택
|
||||
let step: number
|
||||
if (targetRange <= 1) {
|
||||
step = 0.2
|
||||
} else if (targetRange <= 3) {
|
||||
step = 0.5
|
||||
} else if (targetRange <= 10) {
|
||||
step = 1
|
||||
} else if (targetRange <= 30) {
|
||||
step = 5
|
||||
} else {
|
||||
step = 10
|
||||
}
|
||||
|
||||
// 범위를 step 단위로 올림 (최소값 보장)
|
||||
const minRange = step * 3 // 최소 3개의 step
|
||||
const range = Math.max(minRange, Math.ceil(targetRange / step) * step)
|
||||
|
||||
return { min: -range, max: range, step }
|
||||
}, [chartDisplayValues])
|
||||
@@ -401,19 +419,7 @@ export function NormalDistributionChart({
|
||||
</div>
|
||||
{categoryTraits.map((trait) => (
|
||||
<SelectItem key={trait.id} value={trait.name}>
|
||||
<div className="flex items-center gap-2.5">
|
||||
<div className={`w-5 h-5 rounded border-2 flex items-center justify-center flex-shrink-0 ${chartFilterTrait === trait.name
|
||||
? 'bg-primary border-primary'
|
||||
: 'border-muted-foreground/50'
|
||||
}`}>
|
||||
{chartFilterTrait === trait.name && (
|
||||
<svg className="w-3.5 h-3.5 text-white" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
<span>{trait.name}</span>
|
||||
</div>
|
||||
{trait.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</div>
|
||||
@@ -423,16 +429,6 @@ export function NormalDistributionChart({
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{/* 선택된 형질 해제 버튼 */}
|
||||
{chartFilterTrait !== 'overall' && (
|
||||
<button
|
||||
onClick={() => setChartFilterTrait('overall')}
|
||||
className="flex items-center gap-1 px-2 py-1 text-xs font-medium text-muted-foreground hover:text-foreground hover:bg-muted rounded transition-colors"
|
||||
>
|
||||
<X className="w-3 h-3" />
|
||||
<span className="hidden sm:inline">해제</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{/* 확대 버튼 */}
|
||||
<button
|
||||
@@ -449,10 +445,10 @@ export function NormalDistributionChart({
|
||||
<div className="mb-3 sm:mb-5 px-4 py-3 sm:p-5 bg-slate-50 rounded-xl border border-slate-200">
|
||||
{/* 모바일: 명확한 레이아웃 */}
|
||||
<div className="sm:hidden space-y-2.5">
|
||||
{/* 상위 % + 기준 표시 */}
|
||||
{/* 상위 % + 조회 기준 표시 */}
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="px-3 py-1 bg-slate-200 text-slate-700 text-sm font-semibold rounded-full">
|
||||
{chartFilterTrait === 'overall' ? '선발지수' : chartFilterTrait} 기준
|
||||
<span className="px-3 py-1 bg-slate-200 text-slate-700 text-sm font-bold rounded-full">
|
||||
{chartFilterTrait === 'overall' ? '선발지수' : chartFilterTrait} 조회 기준
|
||||
</span>
|
||||
<span className="px-3 py-1 bg-primary text-white text-sm font-bold rounded-full">
|
||||
상위 {chartDisplayValues.percentile?.toFixed(0) || 0}%
|
||||
@@ -558,10 +554,10 @@ export function NormalDistributionChart({
|
||||
|
||||
{/* 데스크탑: 기존 레이아웃 */}
|
||||
<div className="hidden sm:block">
|
||||
{/* 현재 보고 있는 기준 표시 */}
|
||||
{/* 현재 보고 있는 조회 기준 표시 */}
|
||||
<div className="flex items-center justify-center mb-4">
|
||||
<span className="px-4 py-1.5 bg-slate-200 text-slate-700 text-base font-semibold rounded-full">
|
||||
{chartFilterTrait === 'overall' ? '전체 선발지수' : chartFilterTrait} 기준
|
||||
<span className="px-4 py-1.5 bg-slate-200 text-slate-700 text-base font-bold rounded-full">
|
||||
{chartFilterTrait === 'overall' ? '전체 선발지수' : chartFilterTrait} 조회 기준
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -192,7 +192,7 @@ function MyCowContent() {
|
||||
.filter(([, weight]) => weight > 0)
|
||||
.map(([traitNm, weight]) => ({
|
||||
traitNm,
|
||||
weight: weight / 100 // 0-100 → 0-1로 정규화
|
||||
weight // 0-10 가중치 그대로 사용
|
||||
}))
|
||||
|
||||
// 랭킹 모드에 따라 criteriaType 결정
|
||||
@@ -987,8 +987,12 @@ function MyCowContent() {
|
||||
month: '2-digit',
|
||||
day: '2-digit'
|
||||
}) : (
|
||||
<span className={cow.unavailableReason ? 'text-red-500 font-medium' : 'text-slate-400'}>
|
||||
{cow.unavailableReason || '미분석'}
|
||||
<span className={`inline-flex items-center px-2 py-0.5 rounded-full text-xs font-semibold ${
|
||||
cow.unavailableReason?.includes('부') ? 'bg-red-100 text-red-700' :
|
||||
cow.unavailableReason?.includes('모') ? 'bg-orange-100 text-orange-700' :
|
||||
'bg-slate-100 text-slate-600'
|
||||
}`}>
|
||||
{cow.unavailableReason || '분석불가'}
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
@@ -1190,12 +1194,16 @@ function MyCowContent() {
|
||||
<span className="text-muted-foreground">부</span>
|
||||
<span className="font-medium">{cow.sireKpn && cow.sireKpn !== '0' ? cow.sireKpn : '-'}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">분석일</span>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-muted-foreground">{cow.anlysDt ? '분석일' : '분석결과'}</span>
|
||||
<span className="font-medium">
|
||||
{cow.anlysDt ? new Date(cow.anlysDt).toLocaleDateString('ko-KR', { year: '2-digit', month: 'numeric', day: 'numeric' }) : (
|
||||
<span className={cow.unavailableReason ? 'text-red-500' : 'text-slate-400'}>
|
||||
{cow.unavailableReason || '미분석'}
|
||||
<span className={`inline-flex items-center px-2 py-0.5 rounded-full text-xs font-semibold ${
|
||||
cow.unavailableReason?.includes('부') ? 'bg-red-100 text-red-700' :
|
||||
cow.unavailableReason?.includes('모') ? 'bg-orange-100 text-orange-700' :
|
||||
'bg-slate-100 text-slate-600'
|
||||
}`}>
|
||||
{cow.unavailableReason || '분석불가'}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -45,7 +45,7 @@ export function CowNumberDisplay({ cowId, cowShortNo, className = '', variant =
|
||||
case 'highlight':
|
||||
return 'bg-primary/15 text-primary font-bold px-1.5 py-0.5 rounded'
|
||||
default:
|
||||
return 'bg-blue-100 text-blue-700 font-bold px-1.5 py-0.5 rounded'
|
||||
return 'bg-blue-100 text-primary font-bold px-1.5 py-0.5 rounded'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ export interface TraitAverageDto {
|
||||
traitName: string; // 형질명
|
||||
category: string; // 카테고리
|
||||
avgEbv: number; // 평균 EBV (표준화 육종가)
|
||||
avgEpd: number; // 평균 EPD (육종가 원본값)
|
||||
count: number; // 데이터 개수
|
||||
}
|
||||
|
||||
@@ -225,12 +226,15 @@ export interface YearlyTraitTrendDto {
|
||||
export interface TraitRankDto {
|
||||
traitName: string;
|
||||
cowEbv: number | null;
|
||||
cowEpd: number | null; // 개체 육종가(EPD)
|
||||
farmRank: number | null;
|
||||
farmTotal: number;
|
||||
regionRank: number | null;
|
||||
regionTotal: number;
|
||||
farmAvgEbv: number | null;
|
||||
regionAvgEbv: number | null;
|
||||
farmAvgEpd: number | null; // 농가 평균 육종가(EPD)
|
||||
regionAvgEpd: number | null; // 보은군 평균 육종가(EPD)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -14,7 +14,7 @@ export { cowApi } from './cow.api';
|
||||
export { dashboardApi } from './dashboard.api';
|
||||
export { farmApi } from './farm.api';
|
||||
export { geneApi, type GeneDetail, type GeneSummary } from './gene.api';
|
||||
export { genomeApi, type ComparisonAveragesDto, type CategoryAverageDto, type FarmTraitComparisonDto, type TraitComparisonAveragesDto, type TraitAverageDto } from './genome.api';
|
||||
export { genomeApi, type ComparisonAveragesDto, type CategoryAverageDto, type FarmTraitComparisonDto, type TraitComparisonAveragesDto, type TraitAverageDto, type GenomeRequestDto } from './genome.api';
|
||||
export { reproApi } from './repro.api';
|
||||
export { breedApi } from './breed.api';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user