번식 능력 검사 리스트 및 보고서 수정

This commit is contained in:
2025-12-22 19:52:38 +09:00
parent d3dda3d929
commit 1644fcf241
15 changed files with 916 additions and 407 deletions

View File

@@ -56,8 +56,11 @@ interface CowWithGenes extends Cow {
rank?: number // 랭킹 순위
cowShortNo?: string // 개체 요약번호
cowReproType?: string // 번식 타입
anlysDt?: string // 분석일자
anlysDt?: string // 분석일자 (유전체)
unavailableReason?: string // 분석불가 사유 (부불일치, 모불일치, 모이력제부재 등)
hasMpt?: boolean // 번식능력검사(MPT) 여부
mptTestDt?: string // MPT 검사일
mptMonthAge?: number // MPT 검사일 기준 월령
}
function MyCowContent() {
@@ -75,7 +78,7 @@ function MyCowContent() {
const [rankingMode, setRankingMode] = useState<'gene' | 'genome'>('gene') // 유전자순/유전체순
const [sortBy, setSortBy] = useState<string>('rank') // 정렬 기준 (기본: 순위)
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('asc') // 정렬 방향
const [analysisFilter, setAnalysisFilter] = useState<'all' | 'completed' | 'unavailable'>('all') // 분석 상태 필터
const [analysisFilter, setAnalysisFilter] = useState<'all' | 'completed' | 'mptOnly' | 'unavailable'>('all') // 분석 상태 필터
// 커스텀 컬럼 표시 필터
const [selectedDisplayGenes, setSelectedDisplayGenes] = useState<string[]>([]) // 테이블에 표시할 유전자
@@ -355,6 +358,12 @@ function MyCowContent() {
anlysDt: item.entity.anlysDt ?? null,
// 분석불가 사유
unavailableReason: item.entity.unavailableReason ?? null,
// 번식능력검사(MPT) 여부
hasMpt: item.entity.hasMpt ?? false,
// MPT 검사일
mptTestDt: item.entity.mptTestDt ?? null,
// MPT 월령
mptMonthAge: item.entity.mptMonthAge ?? null,
//====================================================================================================================
// 형질 데이터 (백엔드에서 계산됨, 형질명 → 표준화육종가 매핑)
// 백엔드 응답: { traitName: string, traitVal: number, traitEbv: number, traitPercentile: number }
@@ -513,9 +522,14 @@ function MyCowContent() {
// 분석 상태 필터
if (analysisFilter === 'completed') {
// 유전체 완료
result = result.filter(cow => cow.genomeScore !== undefined && cow.genomeScore !== null)
} else if (analysisFilter === 'mptOnly') {
// 번식능력 검사 완료 (유전체 유무 상관없이)
result = result.filter(cow => cow.hasMpt === true)
} else if (analysisFilter === 'unavailable') {
result = result.filter(cow => cow.genomeScore === undefined || cow.genomeScore === null)
// 유전체 분석불가 (부불일치, 모불일치 등)
result = result.filter(cow => cow.unavailableReason !== null && cow.unavailableReason !== undefined)
}
// 정렬 (sortBy가 'none'이면 정렬하지 않음 - 전역 필터 순서 유지)
@@ -684,11 +698,11 @@ function MyCowContent() {
<p className="text-base max-sm:text-sm text-slate-500 mt-1">{'농장'} </p>
</div>
{/* 분석 상태 탭 필터 */}
<div className="flex rounded-lg border border-slate-200 bg-slate-50 p-1.5 gap-1.5 max-sm:p-1 max-sm:gap-1">
{/* 분석 상태 탭 필터 - 모바일: 2x2 그리드, 데스크톱: 가로 배치 */}
<div className="grid grid-cols-2 sm:grid-cols-4 rounded-lg border border-slate-200 bg-slate-50 p-1.5 gap-1.5 max-sm:p-1 max-sm:gap-1">
<button
onClick={() => setAnalysisFilter('all')}
className={`flex-1 px-3 py-2.5 max-sm:px-2 max-sm:py-2 rounded-md text-base max-sm:text-sm font-medium transition-colors ${analysisFilter === 'all'
className={`px-3 py-2.5 max-sm:px-2 max-sm:py-2 rounded-md text-base max-sm:text-sm font-medium transition-colors ${analysisFilter === 'all'
? 'bg-white text-slate-900 shadow-sm border border-slate-200'
: 'text-slate-500 hover:text-slate-700'
}`}
@@ -697,26 +711,38 @@ function MyCowContent() {
</button>
<button
onClick={() => setAnalysisFilter('completed')}
className={`flex-1 px-3 py-2.5 max-sm:px-2 max-sm:py-2 rounded-md text-base max-sm:text-sm font-medium transition-colors ${analysisFilter === 'completed'
className={`px-3 py-2.5 max-sm:px-2 max-sm:py-2 rounded-md text-base max-sm:text-sm font-medium transition-colors ${analysisFilter === 'completed'
? 'bg-white text-emerald-600 shadow-sm border border-slate-200'
: 'text-slate-500 hover:text-slate-700'
}`}
>
<span className="flex items-center justify-center gap-1.5 max-sm:gap-1">
<span className="w-2 h-2 rounded-full bg-emerald-500"></span>
<span className="font-bold">{cows.filter(c => c.genomeScore !== undefined && c.genomeScore !== null).length}</span>
<span className="font-bold">{cows.filter(c => c.genomeScore !== undefined && c.genomeScore !== null).length}</span>
</span>
</button>
<button
onClick={() => setAnalysisFilter('unavailable')}
className={`flex-1 px-3 py-2.5 max-sm:px-2 max-sm:py-2 rounded-md text-base max-sm:text-sm font-medium transition-colors ${analysisFilter === 'unavailable'
? 'bg-white text-slate-600 shadow-sm border border-slate-200'
onClick={() => setAnalysisFilter('mptOnly')}
className={`px-3 py-2.5 max-sm:px-2 max-sm:py-2 rounded-md text-base max-sm:text-sm font-medium transition-colors ${analysisFilter === 'mptOnly'
? 'bg-white text-amber-600 shadow-sm border border-slate-200'
: 'text-slate-500 hover:text-slate-700'
}`}
>
<span className="flex items-center justify-center gap-1.5 max-sm:gap-1">
<span className="w-2 h-2 rounded-full bg-slate-400"></span>
<span className="font-bold">{cows.filter(c => c.genomeScore === undefined || c.genomeScore === null).length}</span>
<span className="w-2 h-2 rounded-full bg-amber-500"></span>
<span className="font-bold">{cows.filter(c => c.hasMpt === true).length}</span>
</span>
</button>
<button
onClick={() => setAnalysisFilter('unavailable')}
className={`px-3 py-2.5 max-sm:px-2 max-sm:py-2 rounded-md text-base max-sm:text-sm font-medium transition-colors ${analysisFilter === 'unavailable'
? 'bg-white text-red-600 shadow-sm border border-slate-200'
: 'text-slate-500 hover:text-slate-700'
}`}
>
<span className="flex items-center justify-center gap-1.5 max-sm:gap-1">
<span className="w-2 h-2 rounded-full bg-red-400"></span>
<span className="font-bold">{cows.filter(c => c.unavailableReason !== null && c.unavailableReason !== undefined).length}</span>
</span>
</button>
</div>
@@ -923,11 +949,15 @@ function MyCowContent() {
<th className="cow-table-header" style={{ width: '50px' }}></th>
<th className="cow-table-header" style={{ width: '220px' }}></th>
<th className="cow-table-header" style={{ width: '90px' }}></th>
<th className="cow-table-header" style={{ width: '70px' }}></th>
<th className="cow-table-header" style={{ width: '60px' }}></th>
<th className="cow-table-header" style={{ width: '100px' }}></th>
<th className="cow-table-header" style={{ width: '90px' }}> KPN</th>
<th className="cow-table-header" style={{ width: '90px' }}></th>
<th className="cow-table-header" style={{ width: '80px' }}>
{analysisFilter === 'mptOnly' ? '월령(검사일)' : '월령(분석일)'}
</th>
<th className="cow-table-header" style={{ width: '90px' }}>
{analysisFilter === 'mptOnly' ? '검사일자' : '분석일자'}
</th>
<th className="cow-table-header border-r-2 border-r-gray-300" style={{ width: '100px' }}>
</th>
@@ -958,19 +988,26 @@ function MyCowContent() {
</div>
</td>
<td className="cow-table-cell">
{cow.cowBirthDt && new Date(cow.cowBirthDt).toLocaleDateString('ko-KR', {
year: '2-digit',
month: '2-digit',
day: '2-digit'
})}
</td>
<td className="cow-table-cell">
{cow.cowBirthDt ? (() => {
const birthDate = new Date(cow.cowBirthDt)
const today = new Date()
const ageInMonths = Math.floor((today.getTime() - birthDate.getTime()) / (1000 * 60 * 60 * 24 * 30.44))
return `${ageInMonths}개월`
})() : '-'}
{(() => {
// 번식능력만 있는 개체 판단 (유전체 데이터 없음)
const hasMptOnly = cow.hasMpt && !cow.genomeScore && !cow.anlysDt
// 번식능력 탭이거나 번식능력만 있는 개체: cowBirthDt 없으면 MPT로 역산
if ((analysisFilter === 'mptOnly' || hasMptOnly) && !cow.cowBirthDt && cow.mptTestDt && cow.mptMonthAge) {
const testDate = new Date(cow.mptTestDt)
const birthDate = new Date(testDate)
birthDate.setMonth(birthDate.getMonth() - cow.mptMonthAge)
return birthDate.toLocaleDateString('ko-KR', {
year: '2-digit',
month: '2-digit',
day: '2-digit'
})
}
return cow.cowBirthDt ? new Date(cow.cowBirthDt).toLocaleDateString('ko-KR', {
year: '2-digit',
month: '2-digit',
day: '2-digit'
}) : '-'
})()}
</td>
<td className="cow-table-cell">
{cow.cowSex === "수" ? "수소" : "암소"}
@@ -982,19 +1019,52 @@ function MyCowContent() {
{cow.sireKpn && cow.sireKpn !== '0' ? cow.sireKpn : '-'}
</td>
<td className="cow-table-cell">
{cow.anlysDt ? new Date(cow.anlysDt).toLocaleDateString('ko-KR', {
year: '2-digit',
month: '2-digit',
day: '2-digit'
}) : 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>
) : '-'}
{(() => {
// 번식능력만 있는 개체 판단
const hasMptOnly = cow.hasMpt && !cow.genomeScore && !cow.anlysDt
// 번식능력 탭이거나 번식능력만 있는 개체: MPT 월령 사용
if (analysisFilter === 'mptOnly' || hasMptOnly) {
return cow.mptMonthAge ? `${cow.mptMonthAge}개월` : '-'
}
if (cow.cowBirthDt && cow.anlysDt) {
const birthDate = new Date(cow.cowBirthDt)
const refDate = new Date(cow.anlysDt)
const ageInMonths = Math.floor((refDate.getTime() - birthDate.getTime()) / (1000 * 60 * 60 * 24 * 30.44))
return `${ageInMonths}개월`
}
return '-'
})()}
</td>
<td className="cow-table-cell">
{(() => {
// 번식능력만 있는 개체 판단
const hasMptOnly = cow.hasMpt && !cow.genomeScore && !cow.anlysDt
// 번식능력 탭이거나 번식능력만 있는 개체: MPT 검사일 사용
if (analysisFilter === 'mptOnly' || hasMptOnly) {
return cow.mptTestDt ? new Date(cow.mptTestDt).toLocaleDateString('ko-KR', {
year: '2-digit',
month: '2-digit',
day: '2-digit'
}) : '-'
}
// 유전체 탭: unavailableReason 있으면 배지, 없으면 분석일자
if (cow.unavailableReason) {
return (
<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>
)
}
return cow.anlysDt ? new Date(cow.anlysDt).toLocaleDateString('ko-KR', {
year: '2-digit',
month: '2-digit',
day: '2-digit'
}) : '-'
})()}
</td>
<td className="cow-table-cell border-r-2 border-r-gray-300 !py-2 !px-0.5">
{(cow.genomeScore !== undefined && cow.genomeScore !== null) ? (
@@ -1169,13 +1239,40 @@ function MyCowContent() {
<div className="flex justify-between">
<span className="text-muted-foreground"></span>
<span className="font-medium">
{cow.cowBirthDt ? new Date(cow.cowBirthDt).toLocaleDateString('ko-KR', { year: '2-digit', month: 'numeric', day: 'numeric' }) : '-'}
{(() => {
// 번식능력만 있는 개체 판단
const hasMptOnly = cow.hasMpt && !cow.genomeScore && !cow.anlysDt
// 번식능력 탭이거나 번식능력만 있는 개체: cowBirthDt 없으면 MPT로 역산
if ((analysisFilter === 'mptOnly' || hasMptOnly) && !cow.cowBirthDt && cow.mptTestDt && cow.mptMonthAge) {
const testDate = new Date(cow.mptTestDt)
const birthDate = new Date(testDate)
birthDate.setMonth(birthDate.getMonth() - cow.mptMonthAge)
return birthDate.toLocaleDateString('ko-KR', { year: '2-digit', month: 'numeric', day: 'numeric' })
}
return cow.cowBirthDt ? new Date(cow.cowBirthDt).toLocaleDateString('ko-KR', { year: '2-digit', month: 'numeric', day: 'numeric' }) : '-'
})()}
</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground"></span>
<span className="text-muted-foreground">
{(() => {
const hasMptOnly = cow.hasMpt && !cow.genomeScore && !cow.anlysDt
return (analysisFilter === 'mptOnly' || hasMptOnly) ? '월령 (검사일)' : '월령 (분석일)'
})()}
</span>
<span className="font-medium">
{cow.cowBirthDt ? `${Math.floor((new Date().getTime() - new Date(cow.cowBirthDt).getTime()) / (1000 * 60 * 60 * 24 * 30.44))}개월` : '-'}
{(() => {
// 번식능력만 있는 개체 판단
const hasMptOnly = cow.hasMpt && !cow.genomeScore && !cow.anlysDt
// 번식능력 탭이거나 번식능력만 있는 개체: MPT 월령 사용
if (analysisFilter === 'mptOnly' || hasMptOnly) {
return cow.mptMonthAge ? `${cow.mptMonthAge}개월` : '-'
}
if (cow.cowBirthDt && cow.anlysDt) {
return `${Math.floor((new Date(cow.anlysDt).getTime() - new Date(cow.cowBirthDt).getTime()) / (1000 * 60 * 60 * 24 * 30.44))}개월`
}
return '-'
})()}
</span>
</div>
<div className="flex justify-between">
@@ -1195,17 +1292,36 @@ function MyCowContent() {
<span className="font-medium">{cow.sireKpn && cow.sireKpn !== '0' ? cow.sireKpn : '-'}</span>
</div>
<div className="flex justify-between items-center">
<span className="text-muted-foreground">{cow.anlysDt ? '분석일' : '분석결과'}</span>
<span className="text-muted-foreground">
{(() => {
const hasMptOnly = cow.hasMpt && !cow.genomeScore && !cow.anlysDt
return (analysisFilter === 'mptOnly' || hasMptOnly) ? '검사일' : (cow.anlysDt ? '분석일' : '분석결과')
})()}
</span>
<span className="font-medium">
{cow.anlysDt ? new Date(cow.anlysDt).toLocaleDateString('ko-KR', { year: '2-digit', month: 'numeric', day: 'numeric' }) : 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>
) : '-'}
{(() => {
// 번식능력만 있는 개체 판단
const hasMptOnly = cow.hasMpt && !cow.genomeScore && !cow.anlysDt
// 번식능력 탭이거나 번식능력만 있는 개체: MPT 검사일 사용
if (analysisFilter === 'mptOnly' || hasMptOnly) {
return cow.mptTestDt ? new Date(cow.mptTestDt).toLocaleDateString('ko-KR', { year: '2-digit', month: 'numeric', day: 'numeric' }) : '-'
}
if (cow.anlysDt) {
return new Date(cow.anlysDt).toLocaleDateString('ko-KR', { year: '2-digit', month: 'numeric', day: 'numeric' })
}
if (cow.unavailableReason) {
return (
<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>
)
}
return '-'
})()}
</span>
</div>
</div>