diff --git a/backend/src/genome/genome.service.ts b/backend/src/genome/genome.service.ts index 1e7acf7..e0c096c 100644 --- a/backend/src/genome/genome.service.ts +++ b/backend/src/genome/genome.service.ts @@ -1511,14 +1511,38 @@ export class GenomeService { const maxEpd = Math.max(...epdValues.map(v => v.epd)); const range = maxEpd - minEpd; - // 구간 크기 결정 (전체 범위를 약 20-30개 구간으로 나눔) - const binSize = range > 0 ? Math.ceil(range / 25) : 1; + // rate 형질 여부 확인 (형질명에 'rate' 또는 'Rate' 포함) + const isRateTrait = traitName.toLowerCase().includes('rate'); + + // 구간 크기 결정 + let binSize: number; + if (isRateTrait) { + // rate 형질: 소수점 binSize 사용 (더 촘촘한 구간) + binSize = range > 0 ? range / 25 : 0.1; + // 너무 작으면 최소값 보장 + if (binSize < 0.1) binSize = 0.1; + // 소수점 둘째자리까지 반올림 + binSize = Math.round(binSize * 100) / 100; + + console.log(`📊 [${traitName}] rate 형질 히스토그램 생성:`, { + 범위: `${minEpd.toFixed(2)} ~ ${maxEpd.toFixed(2)}`, + range: range.toFixed(2), + binSize: binSize.toFixed(2), + 구간방식: '소수점' + }); + } else { + // 일반 형질: 기존 로직 (정수 binSize) + binSize = range > 0 ? Math.ceil(range / 25) : 1; + } // 구간별 집계 const binMap = new Map(); epdValues.forEach(({ epd, farmNo: scoreFarmNo }) => { - const binStart = Math.floor(epd / binSize) * binSize; + // rate 형질은 소수점 구간, 일반 형질은 정수 구간 + const binStart = isRateTrait + ? Math.round((Math.floor(epd / binSize) * binSize) * 100) / 100 // 소수점 둘째자리까지 + : Math.floor(epd / binSize) * binSize; const existing = binMap.get(binStart) || { count: 0, farmCount: 0 }; existing.count += 1; if (scoreFarmNo === farmNo) { @@ -1528,10 +1552,21 @@ export class GenomeService { }); // Map을 배열로 변환 및 정렬 - histogram.push(...Array.from(binMap.entries()) + const sortedHistogram = Array.from(binMap.entries()) .map(([bin, data]) => ({ bin, count: data.count, farmCount: data.farmCount })) - .sort((a, b) => a.bin - b.bin) - ); + .sort((a, b) => a.bin - b.bin); + + histogram.push(...sortedHistogram); + + // rate 형질일 때만 로그 출력 + if (isRateTrait && sortedHistogram.length > 0) { + console.log(`📊 [${traitName}] 최종 히스토그램:`, { + 구간수: sortedHistogram.length, + 첫구간: sortedHistogram[0].bin, + 마지막구간: sortedHistogram[sortedHistogram.length - 1].bin, + 샘플: sortedHistogram.slice(0, 5).map(h => `${h.bin.toFixed(2)}(${h.count}마리)`) + }); + } } } diff --git a/frontend/src/app/cow/[cowNo]/genome/_components/category-evaluation-card.tsx b/frontend/src/app/cow/[cowNo]/genome/_components/category-evaluation-card.tsx index 6511322..35e9853 100644 --- a/frontend/src/app/cow/[cowNo]/genome/_components/category-evaluation-card.tsx +++ b/frontend/src/app/cow/[cowNo]/genome/_components/category-evaluation-card.tsx @@ -1,6 +1,6 @@ 'use client' -import { useState } from 'react' +import { useState, useEffect, useMemo } from 'react' import { Button } from "@/components/ui/button" import { Dialog, @@ -74,19 +74,53 @@ export function CategoryEvaluationCard({ // 차트에 표시할 형질 목록 (커스텀 가능) const [chartTraits, setChartTraits] = useState([...DEFAULT_TRAITS]) + // 활성화된 형질 목록 (차트에 표시할 형질) + const [activeTraits, setActiveTraits] = useState>(new Set([...DEFAULT_TRAITS])) + // 형질 추가 모달/드로어 상태 const [isTraitSelectorOpen, setIsTraitSelectorOpen] = useState(false) // 선택된 형질 (터치/클릭 시 정보 표시용) const [selectedTraitName, setSelectedTraitName] = useState(null) + // 차트 로딩 상태 + const [isChartLoading, setIsChartLoading] = useState(false) + // 모바일 여부 확인 const isDesktop = useMediaQuery("(min-width: 768px)") + // 형질 활성화/비활성화 토글 + const toggleTraitActive = (traitName: string) => { + setActiveTraits(prev => { + const newSet = new Set(prev) + if (newSet.has(traitName)) { + // 비활성화 시 제한 없음 (2개 이하일 때 차트 비활성화로 처리) + newSet.delete(traitName) + } else { + newSet.add(traitName) + } + return newSet + }) + } + + // 차트 데이터 변경 시 로딩 처리 + useEffect(() => { + setIsChartLoading(true) + const timer = setTimeout(() => { + setIsChartLoading(false) + }, 300) // 차트 렌더링 시뮬레이션 + return () => clearTimeout(timer) + }, [activeTraits]) + // 형질 제거 const removeTrait = (traitName: string) => { if (chartTraits.length > 3) { // 최소 3개는 유지 setChartTraits(prev => prev.filter(t => t !== traitName)) + setActiveTraits(prev => { + const newSet = new Set(prev) + newSet.delete(traitName) + return newSet + }) } } @@ -94,16 +128,21 @@ export function CategoryEvaluationCard({ const addTrait = (traitName: string) => { if (chartTraits.length < 7 && !chartTraits.includes(traitName)) { // 최대 7개 setChartTraits(prev => [...prev, traitName]) + setActiveTraits(prev => new Set([...prev, traitName])) } } // 기본값으로 초기화 const resetToDefault = () => { setChartTraits([...DEFAULT_TRAITS]) + setActiveTraits(new Set([...DEFAULT_TRAITS])) } - // 폴리곤 차트용 데이터 생성 (선택된 형질 기반) - 보은군, 농가, 이 개체 비교 - const traitChartData = chartTraits.map(traitName => { + // 폴리곤 차트용 데이터 생성 (활성화된 형질만 포함) - 보은군, 농가, 이 개체 비교 + const traitChartData = useMemo(() => { + return chartTraits + .filter(traitName => activeTraits.has(traitName)) + .map(traitName => { const trait = allTraits.find((t: GenomeCowTraitDto) => t.traitName === traitName) // 형질별 평균 데이터에서 해당 형질 찾기 @@ -131,17 +170,50 @@ export function CategoryEvaluationCard({ diff: trait?.breedVal ?? 0, hasData: !!trait } - }) + }) + }, [chartTraits, activeTraits, allTraits, traitComparisonAverages]) // 가장 높은 형질 찾기 (이 개체 기준) const bestTraitName = traitChartData.reduce((best, current) => current.breedVal > best.breedVal ? current : best , traitChartData[0])?.shortName - // 동적 스케일 계산 (모든 값의 최대 절대값 기준) - const allValues = traitChartData.flatMap(d => [d.breedVal, d.regionVal, d.farmVal]) - const maxAbsValue = Math.max(...allValues.map(Math.abs), 0.3) // 최소 0.3 - const dynamicDomain = Math.ceil(maxAbsValue * 1.2 * 10) / 10 // 20% 여유 + // 동적 스케일 계산 (실제 데이터 범위를 기반으로, min/max 각각에 5% 여유분만 추가) + // useMemo를 사용하는 이유: traitChartData가 변경될 때만 재계산하여 성능 최적화 + // - traitChartData는 activeTraits, chartTraits, allTraits, traitComparisonAverages에 의존 + // - 이 값들이 변경될 때마다 스케일을 다시 계산해야 함 + // - useMemo를 사용하면 의존성이 변경되지 않으면 이전 계산 결과를 재사용 + const dynamicDomain = useMemo(() => { + if (traitChartData.length === 0) return [-0.3, 0.3] + + // 모든 값 수집 (breedVal, regionVal, farmVal) + const allValues = traitChartData.flatMap(d => [d.breedVal, d.regionVal, d.farmVal]) + + // 실제 데이터의 최소값과 최대값 찾기 + const minValue = Math.min(...allValues) + const maxValue = Math.max(...allValues) + + // 데이터 범위 계산 + const dataRange = maxValue - minValue + + // 데이터 범위가 너무 작으면 최소 범위 보장 (0.3) + const effectiveRange = Math.max(dataRange, 0.3) + + // min/max 각각에 범위의 10%만큼 여유분 추가 (대칭 처리하지 않음) + const padding = effectiveRange * 0.10 + let domainMin = minValue - padding + let domainMax = maxValue + padding + + // 소수점 첫째자리까지 반올림 + domainMin = Math.floor(domainMin * 10) / 10 + domainMax = Math.ceil(domainMax * 10) / 10 + + return [domainMin, domainMax] + }, [traitChartData]) + + // 활성화된 형질 개수 + const activeTraitsCount = activeTraits.size + const hasEnoughTraits = activeTraitsCount >= 3 // 형질 이름으로 원본 형질명 찾기 (shortName -> name) const findTraitNameByShortName = (shortName: string) => { @@ -189,7 +261,7 @@ export function CategoryEvaluationCard({ y={0} dy={5} textAnchor="middle" - fontSize={15} + fontSize={isDesktop ? 17 : 15} fontWeight={isSelected ? 700 : 600} fill={isSelected ? '#ffffff' : '#334155'} > @@ -227,11 +299,12 @@ export function CategoryEvaluationCard({ }`} > {getTraitDisplayName(trait)} - {traitData && traitData.breedVal !== undefined && ( + {/* 육종가(EBV) 값 표시 (주석 처리) */} + {/* {traitData && traitData.breedVal !== undefined && ( ({traitData.breedVal > 0 ? '+' : ''}{traitData.breedVal.toFixed(1)}) - )} + )} */} ) })} @@ -282,38 +355,52 @@ export function CategoryEvaluationCard({ return (
{/* 2열 레이아웃: 왼쪽 차트 + 오른쪽 카드 */} -
+
{/* 형질 선택 칩 영역 */} -
-
- 비교 형질을 선택해주세요 -
-
- {chartTraits.map(trait => ( - - {getTraitDisplayName(trait)} - {chartTraits.length > 3 && ( - - )} - - ))} - -
{/* 형질 편집 모달 - 웹: Dialog, 모바일: Drawer */} {isDesktop ? ( @@ -342,18 +429,47 @@ export function CategoryEvaluationCard({
{/* 폴리곤 차트 */}
-
+
- - + {/* 범례 - 좌측 상단 */} +
+
+
+ 보은군 평균 +
+
+
+ 농가 평균 +
+
+
+ {formatCowNoShort(cowNo)} 개체 +
+
+ + {/* 로딩 상태 또는 최소 형질 개수 미달 */} + {(isChartLoading || !hasEnoughTraits) ? ( +
+ {isChartLoading ? ( + <> +
+

차트 데이터 로딩 중...

+ + ) : ( +

비교 형질 3개 이상 선택해주세요.

+ )} +
+ ) : ( + + @@ -362,8 +478,9 @@ export function CategoryEvaluationCard({ name="보은군 평균" dataKey="regionVal" stroke="#10b981" - fill="#10b981" - fillOpacity={0.2} + // fill="#10b981" + // fillOpacity={0.2} + fill="transparent" strokeWidth={2} dot={false} /> @@ -372,8 +489,9 @@ export function CategoryEvaluationCard({ name="농가 평균" dataKey="farmVal" stroke="#1F3A8F" - fill="#1F3A8F" - fillOpacity={0.3} + // fill="#1F3A8F" + // fillOpacity={0.3} + fill="transparent" strokeWidth={2.5} dot={false} /> @@ -382,8 +500,9 @@ export function CategoryEvaluationCard({ name={formatCowNo(cowNo)} dataKey="breedVal" stroke="#1482B0" - fill="#1482B0" - fillOpacity={0.35} + // fill="#1482B0" + // fillOpacity={0.35} + fill="transparent" strokeWidth={isDesktop ? 3 : 2} dot={{ fill: '#1482B0', @@ -399,6 +518,7 @@ export function CategoryEvaluationCard({ tickLine={false} /> { if (active && payload && payload.length) { const item = payload[0]?.payload @@ -408,8 +528,8 @@ export function CategoryEvaluationCard({ return (
-

{item?.name}

-
+

{item?.name}

+
@@ -440,22 +560,7 @@ export function CategoryEvaluationCard({ /> -
- - {/* 범례 */} -
-
-
- 보은군 평균 -
-
-
- 농가 평균 -
-
-
- {formatCowNoShort(cowNo)} 개체 -
+ )}
{/* 선택된 형질 정보 표시 - 육종가(EPD) 값으로 표시 */} diff --git a/frontend/src/app/cow/[cowNo]/genome/_components/normal-distribution-chart.tsx b/frontend/src/app/cow/[cowNo]/genome/_components/normal-distribution-chart.tsx index dd9a4e0..5c1327c 100644 --- a/frontend/src/app/cow/[cowNo]/genome/_components/normal-distribution-chart.tsx +++ b/frontend/src/app/cow/[cowNo]/genome/_components/normal-distribution-chart.tsx @@ -273,8 +273,8 @@ export function NormalDistributionChart({ // 전체 선발지수: selectionIndexHistogram 사용 if (chartFilterTrait === 'overall' && selectionIndexHistogram.length > 0) { const bins = selectionIndexHistogram.map(item => item.bin - cowScore) - // 내 개체(0)도 범위에 포함 - const allValues = [...bins, 0] + // 내 개체(0), 농가 평균, 보은군 평균도 범위에 포함 + const allValues = [...bins, 0, chartDisplayValues.farmScore, chartDisplayValues.regionScore] const minData = Math.min(...allValues) const maxData = Math.max(...allValues) @@ -309,11 +309,19 @@ export function NormalDistributionChart({ // 형질별: traitRankData.histogram 사용 if (chartFilterTrait !== 'overall' && traitRankData?.histogram && traitRankData.histogram.length > 0) { const bins = traitRankData.histogram.map(item => item.bin - cowScore) - // 내 개체(0)도 범위에 포함 - const allValues = [...bins, 0] + // 내 개체(0), 농가 평균, 보은군 평균도 범위에 포함 + const allValues = [...bins, 0, chartDisplayValues.farmScore, chartDisplayValues.regionScore] const minData = Math.min(...allValues) const maxData = Math.max(...allValues) + console.log(`[${chartFilterTrait}] X축 범위 계산:`, { + bins: `${bins[0].toFixed(2)} ~ ${bins[bins.length-1].toFixed(2)}`, + 내개체: 0, + 농가평균위치: chartDisplayValues.farmScore.toFixed(2), + 보은군평균위치: chartDisplayValues.regionScore.toFixed(2), + allValues범위: `${minData.toFixed(2)} ~ ${maxData.toFixed(2)}`, + }) + // 데이터의 중심점 계산 const center = (minData + maxData) / 2 // 데이터 범위에 20% 여유 추가 @@ -507,17 +515,17 @@ export function NormalDistributionChart({ return bins }, [xAxisConfig, chartFilterTrait, traitRankData, chartDisplayValues.originalScore, selectionIndexHistogram]) - // Y축 범위 (실제 데이터에 맞게 조정) - const maxPercent = useMemo(() => { - if (histogramData.length === 0) return 40 + // Y축 범위 (실제 데이터에 맞게 조정 - 개체수 기준) + const maxCount = useMemo(() => { + if (histogramData.length === 0) return 100 - const maxValue = Math.max(...histogramData.map(d => d.percent || 0)) + const maxValue = Math.max(...histogramData.map(d => ('count' in d ? d.count : 0) || 0)) // 실제 최대값에 20% 여유만 추가 (너무 빡빡하지 않게) const calculatedMax = Math.ceil(maxValue * 1.2) - // 최소 5% 보장 (데이터가 너무 작을 때만) - return Math.max(5, calculatedMax) + // 최소 10개체 보장 (데이터가 너무 작을 때만) + return Math.max(10, calculatedMax) }, [histogramData]) @@ -708,7 +716,7 @@ export function NormalDistributionChart({
{/* 농가 내 순위 */}
- 농가 내 순위 + 농가 내 순위
{traitRankLoading && chartFilterTrait !== 'overall' ? ( ... @@ -738,7 +746,7 @@ export function NormalDistributionChart({ {/* 보은군 내 순위 */}
- 보은군 내 순위 + 보은군 내 순위
{traitRankLoading && chartFilterTrait !== 'overall' ? ( ... @@ -768,7 +776,7 @@ export function NormalDistributionChart({ {/* 농가 평균 대비 */}
= 0 ? 'border-green-300' : 'border-red-300'}`}> - 농가 평균 대비 + 농가 평균 대비
{traitRankLoading && chartFilterTrait !== 'overall' ? ( ... @@ -788,7 +796,7 @@ export function NormalDistributionChart({ {/* 보은군 평균 대비 */}
= 0 ? 'border-green-300' : 'border-red-300'}`}> - 보은군 평균 대비 + 보은군 평균 대비
{traitRankLoading && chartFilterTrait !== 'overall' ? ( ... @@ -857,21 +865,25 @@ export function NormalDistributionChart({ type="number" domain={[xAxisConfig.min, xAxisConfig.max]} ticks={xTicks} - tick={{ fontSize: isMobileView ? 11 : 13, fill: '#64748b', fontWeight: 600 }} + tick={{ fontSize: isMobileView ? 16 : 18, fill: '#64748b', fontWeight: 700 }} tickLine={false} axisLine={{ stroke: '#cbd5e1', strokeWidth: 2 }} tickFormatter={(value) => { - if (value === 0) return '내 개체' + if (value === 0) { + // cowNo의 뒤에서 5번째부터 2번째까지 4자리 추출 (예: KOR002203259861 -> 5986) + const shortId = cowNo ? cowNo.slice(-5, -1) : '' + return shortId || '0' + } return value > 0 ? `+${value}` : `${value}` }} /> `${Math.round(value)}%`} + tick={{ fontSize: isMobileView ? 15 : 17, fill: '#64748b', fontWeight: 700 }} + width={isMobileView ? 45 : 60} + domain={[0, Math.ceil(maxCount)]} + tickFormatter={(value) => `${Math.round(value)}`} /> {/* Tooltip */} @@ -908,7 +920,7 @@ export function NormalDistributionChart({ {/* 실제 데이터 분포 (Area 그래프 + 점 표시) */} - 내 개체 + {cowNo ? cowNo.slice(-5, -1) : '0'} = { @@ -23,13 +24,14 @@ interface TraitDistributionChartsProps { regionAvgZ: number farmAvgZ: number cowName?: string + cowNo?: string // API 호출용 개체번호 totalCowCount?: number selectedTraits?: GenomeCowTraitDto[] traitWeights?: Record } -// 리스트 뷰 컴포넌트 -function TraitListView({ traits, cowName }: { +// 테이블 뷰 컴포넌트 (데스크탑) +function TraitTableView({ traits, traitRanks }: { traits: Array<{ traitName?: string; shortName: string; @@ -39,62 +41,69 @@ function TraitListView({ traits, cowName }: { traitVal?: number; hasData?: boolean; }>; - cowName: string + traitRanks: Record }) { return ( - +
- +
- - - - + + + + + - {traits.map((trait, idx) => ( - - - + + + - + - - - ))} + + + + ) + })}
형질명카테고리육종가전국 백분위유전형질유전체 육종가전국 백분위농가 내 순위보은군 내 순위
- {trait.shortName} - - {trait.traitCategory && ( - - {trait.traitCategory} + {traits.map((trait, idx) => { + const rankData = trait.traitName ? traitRanks[trait.traitName] : null + return ( +
+ {trait.shortName} + +
+ { + const value = trait.traitVal ?? 0 + const isNegativeTrait = NEGATIVE_TRAITS.includes(trait.traitName || '') + if (value === 0) return 'text-muted-foreground' + if (isNegativeTrait) { + return value < 0 ? 'text-green-600' : 'text-red-600' + } + return value > 0 ? 'text-green-600' : 'text-red-600' + })()}`}> + {trait.traitVal !== undefined ? ( + <>{trait.traitVal > 0 ? '+' : ''}{trait.traitVal.toFixed(1)} + ) : '-'} + +
+
+ + 상위 {(trait.percentile || 0).toFixed(0)}% - )} - -
- { - const value = trait.traitVal ?? 0 - const isNegativeTrait = NEGATIVE_TRAITS.includes(trait.traitName || '') - // 등지방두께: 음수가 좋음(녹색), 양수가 나쁨(빨간색) - // 나머지: 양수가 좋음(녹색), 음수가 나쁨(빨간색) - if (value === 0) return 'text-muted-foreground' - if (isNegativeTrait) { - return value < 0 ? 'text-green-600' : 'text-red-600' - } - return value > 0 ? 'text-green-600' : 'text-red-600' - })()}`}> - {trait.traitVal !== undefined ? ( - <>{trait.traitVal > 0 ? '+' : ''}{trait.traitVal.toFixed(1)} +
+ + {rankData?.farmRank && rankData.farmTotal ? ( + `${rankData.farmRank}위/${rankData.farmTotal}두` ) : '-'} - - - - 상위 {(trait.percentile || 0).toFixed(0)}% - -
+ + {rankData?.regionRank && rankData.regionTotal ? ( + `${rankData.regionRank}위/${rankData.regionTotal}두` + ) : '-'} + +
@@ -103,12 +112,96 @@ function TraitListView({ traits, cowName }: { ) } +// 카드 뷰 컴포넌트 (모바일) +function TraitCardView({ traits, traitRanks }: { + traits: Array<{ + traitName?: string; + shortName: string; + breedVal: number; + percentile?: number; + traitCategory?: string; + traitVal?: number; + hasData?: boolean; + }>; + traitRanks: Record +}) { + return ( +
+ {traits.map((trait, idx) => { + const rankData = trait.traitName ? traitRanks[trait.traitName] : null + const value = trait.traitVal ?? 0 + const isNegativeTrait = NEGATIVE_TRAITS.includes(trait.traitName || '') + const valueColor = (() => { + if (value === 0) return 'text-muted-foreground' + if (isNegativeTrait) { + return value < 0 ? 'text-green-600' : 'text-red-600' + } + return value > 0 ? 'text-green-600' : 'text-red-600' + })() + + return ( + + +
+ {/* 형질명 */} +
+ 유전형질 + {trait.shortName} +
+ + {/* 유전체 육종가 */} +
+ 유전체 육종가 + + {trait.traitVal !== undefined ? ( + <>{trait.traitVal > 0 ? '+' : ''}{trait.traitVal.toFixed(1)} + ) : '-'} + +
+ + {/* 전국 백분위 */} +
+ 전국 백분위 + + 상위 {(trait.percentile || 0).toFixed(0)}% + +
+ + {/* 농가 내 순위 */} +
+ 농가 내 순위 + + {rankData?.farmRank && rankData.farmTotal ? ( + `${rankData.farmRank}위/${rankData.farmTotal}두` + ) : '-'} + +
+ + {/* 보은군 내 순위 */} +
+ 보은군 내 순위 + + {rankData?.regionRank && rankData.regionTotal ? ( + `${rankData.regionRank}위/${rankData.regionTotal}두` + ) : '-'} + +
+
+
+
+ ) + })} +
+ ) +} + // 메인 컴포넌트 export function TraitDistributionCharts({ allTraits, regionAvgZ, farmAvgZ, cowName = '개체', + cowNo, totalCowCount = 100, selectedTraits = [], traitWeights = {} @@ -153,6 +246,53 @@ export function TraitDistributionCharts({ }) }, [allTraits, selectedTraits, traitWeights]) + // 표시할 형질명 목록 (순위 조회용) + const traitNames = useMemo(() => { + return displayTraits + .filter(trait => trait.traitName && trait.hasData) + .map(trait => trait.traitName!) + .sort() // 정렬하여 안정적인 키 생성 + }, [displayTraits]) + + // 형질명 목록의 안정적인 키 (dependency용) + const traitNamesKey = useMemo(() => { + return traitNames.join(',') + }, [traitNames]) + + // 각 형질의 순위 정보 가져오기 + const [traitRanks, setTraitRanks] = useState>({}) + const [loadingRanks, setLoadingRanks] = useState(false) + + useEffect(() => { + if (!cowNo || traitNames.length === 0) return + + const fetchRanks = async () => { + setLoadingRanks(true) + try { + const rankPromises = traitNames.map(traitName => + genomeApi.getTraitRank(cowNo, traitName) + .then(rank => ({ traitName, rank })) + .catch(() => null) + ) + + const results = await Promise.all(rankPromises) + const ranksMap: Record = {} + results.forEach(result => { + if (result) { + ranksMap[result.traitName] = result.rank + } + }) + setTraitRanks(ranksMap) + } catch (error) { + console.error('순위 정보 로드 실패:', error) + } finally { + setLoadingRanks(false) + } + } + + fetchRanks() + }, [cowNo, traitNamesKey]) + return ( <> {/* 헤더 */} @@ -166,8 +306,11 @@ export function TraitDistributionCharts({
- {/* 리스트 뷰 */} - + {/* 테이블 뷰 (데스크탑) */} + + + {/* 카드 뷰 (모바일) */} + ) } diff --git a/frontend/src/app/cow/[cowNo]/page.tsx b/frontend/src/app/cow/[cowNo]/page.tsx index 32acc17..91574e7 100644 --- a/frontend/src/app/cow/[cowNo]/page.tsx +++ b/frontend/src/app/cow/[cowNo]/page.tsx @@ -90,7 +90,16 @@ export default function CowOverviewPage() { const [geneDataLoaded, setGeneDataLoaded] = useState(false) const [geneDataLoading, setGeneDataLoading] = useState(false) const [loading, setLoading] = useState(true) - const [activeTab, setActiveTab] = useState('genome') + const [activeTab, setActiveTab] = useState(() => { + // 목록에서 진입 시 초기화 + if (from === 'list') return 'genome' + // 그 외에는 localStorage에서 복원 + if (typeof window !== 'undefined') { + const saved = localStorage.getItem(`cowDetailActiveTab_${cowNo}`) + return saved || 'genome' + } + return 'genome' + }) // 2. 검사 상태 const [hasGenomeData, setHasGenomeData] = useState(false) @@ -142,14 +151,74 @@ export default function CowOverviewPage() { }) // 7. 유전자 탭 필터/정렬 - const [geneSearchInput, setGeneSearchInput] = useState('') - const [geneSearchKeyword, setGeneSearchKeyword] = useState('') + const [geneSearchInput, setGeneSearchInput] = useState(() => { + if (typeof window !== 'undefined' && from !== 'list') { + const saved = localStorage.getItem('geneSearchInput') + return saved || '' + } + return '' + }) + const [geneSearchKeyword, setGeneSearchKeyword] = useState(() => { + if (typeof window !== 'undefined' && from !== 'list') { + const saved = localStorage.getItem('geneSearchKeyword') + return saved || '' + } + return '' + }) const [geneTypeFilter, setGeneTypeFilter] = useState<'all' | 'QTY' | 'QLT'>('all') const [genotypeFilter, setGenotypeFilter] = useState<'all' | 'homozygous' | 'heterozygous'>('all') const [geneSortBy, setGeneSortBy] = useState<'snpName' | 'chromosome' | 'position' | 'snpType' | 'allele1' | 'allele2' | 'remarks'>('snpName') const [geneSortOrder, setGeneSortOrder] = useState<'asc' | 'desc'>('asc') - const [geneCurrentPage, setGeneCurrentPage] = useState(1) - const GENES_PER_PAGE = 50 + + // 무한 스크롤 페이지네이션 + const [geneCurrentLoadedPage, setGeneCurrentLoadedPage] = useState(() => { + if (typeof window !== 'undefined' && from !== 'list') { + const saved = localStorage.getItem('geneCurrentLoadedPage') + return saved ? parseInt(saved, 10) : 1 + } + return 1 + }) + const [genesPerPage, setGenesPerPage] = useState(() => { + if (typeof window !== 'undefined' && from !== 'list') { + const saved = localStorage.getItem('genesPerPage') + return saved ? parseInt(saved, 10) : 50 + } + return 50 + }) + const [isLoadingMoreGenes, setIsLoadingMoreGenes] = useState(false) + + // ======================================== + // useEffect - localStorage 저장 (유전자 탭) + // ======================================== + useEffect(() => { + if (typeof window !== 'undefined') { + localStorage.setItem('geneSearchInput', geneSearchInput) + } + }, [geneSearchInput]) + + useEffect(() => { + if (typeof window !== 'undefined') { + localStorage.setItem('geneSearchKeyword', geneSearchKeyword) + } + }, [geneSearchKeyword]) + + useEffect(() => { + if (typeof window !== 'undefined') { + localStorage.setItem('genesPerPage', genesPerPage.toString()) + } + }, [genesPerPage]) + + // 검색어 또는 genesPerPage 변경 시 1페이지로 리셋 + useEffect(() => { + setGeneCurrentLoadedPage(1) + }, [geneSearchKeyword, genesPerPage]) + + // activeTab 변경 시 localStorage 저장 (목록에서 진입 시 제외) + useEffect(() => { + if (typeof window !== 'undefined' && from !== 'list') { + localStorage.setItem(`cowDetailActiveTab_${cowNo}`, activeTab) + } + }, [activeTab, cowNo, from]) // ======================================== // useEffect - UI 이벤트 @@ -176,11 +245,18 @@ export default function CowOverviewPage() { useEffect(() => { const timer = setTimeout(() => { setGeneSearchKeyword(geneSearchInput) - setGeneCurrentPage(1) + setGeneCurrentLoadedPage(1) }, 300) return () => clearTimeout(timer) }, [geneSearchInput]) + // 유전자 테이블 무한 스크롤: geneCurrentLoadedPage가 변경되면 localStorage에 저장 + useEffect(() => { + if (typeof window !== 'undefined') { + localStorage.setItem('geneCurrentLoadedPage', geneCurrentLoadedPage.toString()) + } + }, [geneCurrentLoadedPage]) + // ======================================== // 헬퍼 함수 // ======================================== @@ -342,11 +418,13 @@ export default function CowOverviewPage() { setHasReproductionData(false) } - // 5. 탭 자동 선택 - if (genomeExists) { - setActiveTab('genome') - } else if (geneData && geneData.length > 0) { - setActiveTab('gene') + // 5. 탭 자동 선택 (목록에서 진입하거나 저장된 탭이 없을 때만) + if (from === 'list' || (typeof window !== 'undefined' && !localStorage.getItem(`cowDetailActiveTab_${cowNo}`))) { + if (genomeExists) { + setActiveTab('genome') + } else if (geneData && geneData.length > 0) { + setActiveTab('gene') + } } // 6. 비교 데이터 + 선발지수 조회 @@ -481,6 +559,86 @@ export default function CowOverviewPage() { // 정규분포 곡선 데이터 (전국/지역/농가 비교 차트) const multiDistribution = useMemo(() => generateMultipleDistributions(0, 1, regionAvgZ, 1, farmAvgZ, 1), [regionAvgZ, farmAvgZ]) + // 유전자 데이터 필터링 및 정렬 (useMemo로 최상위에서 관리) + const filteredAndSortedGeneData = useMemo(() => { + const filteredData = geneData.filter(gene => { + // 검색 필터 + if (geneSearchKeyword) { + const keyword = geneSearchKeyword.toLowerCase() + const snpName = (gene.snpName || '').toLowerCase() + const chromosome = (gene.chromosome || '').toLowerCase() + const position = (gene.position || '').toLowerCase() + const snpType = (gene.snpType || '').toLowerCase() + const allele1 = (gene.allele1 || '').toLowerCase() + const allele2 = (gene.allele2 || '').toLowerCase() + const remarks = (gene.remarks || '').toLowerCase() + if (!snpName.includes(keyword) && + !chromosome.includes(keyword) && + !position.includes(keyword) && + !snpType.includes(keyword) && + !allele1.includes(keyword) && + !allele2.includes(keyword) && + !remarks.includes(keyword)) { + return false + } + } + // 유전자형 필터 + if (genotypeFilter !== 'all') { + const isHomozygous = gene.allele1 === gene.allele2 + if (genotypeFilter === 'homozygous' && !isHomozygous) return false + if (genotypeFilter === 'heterozygous' && isHomozygous) return false + } + return true + }) + + // 정렬 + return [...filteredData].sort((a, b) => { + let aVal: string | number = '' + let bVal: string | number = '' + + switch (geneSortBy) { + case 'snpName': + aVal = a.snpName || '' + bVal = b.snpName || '' + break + case 'chromosome': + aVal = parseInt(a.chromosome || '0') || 0 + bVal = parseInt(b.chromosome || '0') || 0 + break + case 'position': + aVal = parseInt(a.position || '0') || 0 + bVal = parseInt(b.position || '0') || 0 + break + case 'snpType': + aVal = a.snpType || '' + bVal = b.snpType || '' + break + case 'allele1': + aVal = a.allele1 || '' + bVal = b.allele1 || '' + break + case 'allele2': + aVal = a.allele2 || '' + bVal = b.allele2 || '' + break + case 'remarks': + aVal = a.remarks || '' + bVal = b.remarks || '' + break + } + + if (typeof aVal === 'number' && typeof bVal === 'number') { + return geneSortOrder === 'asc' ? aVal - bVal : bVal - aVal + } + + const strA = String(aVal) + const strB = String(bVal) + return geneSortOrder === 'asc' + ? strA.localeCompare(strB) + : strB.localeCompare(strA) + }) + }, [geneData, geneSearchKeyword, genotypeFilter, geneSortBy, geneSortOrder]) + const toggleTraitSelection = (traitId: number) => { setSelectedTraits(prev => prev.includes(traitId) @@ -489,6 +647,24 @@ export default function CowOverviewPage() { ) } + // 유전자 테이블 스크롤 핸들러 (간단하게 함수로만 정의) + const handleGeneTableScroll = (e: React.UIEvent) => { + const target = e.currentTarget + const { scrollTop, scrollHeight, clientHeight } = target + const isNearBottom = scrollHeight - scrollTop - clientHeight < 100 + + if (isNearBottom && !isLoadingMoreGenes) { + const totalPages = Math.ceil(filteredAndSortedGeneData.length / genesPerPage) + if (geneCurrentLoadedPage < totalPages) { + setIsLoadingMoreGenes(true) + setTimeout(() => { + setGeneCurrentLoadedPage(prev => prev + 1) + setIsLoadingMoreGenes(false) + }, 300) + } + } + } + if (loading) { return ( @@ -513,9 +689,9 @@ export default function CowOverviewPage() { -
+
{/* 메인 컨테이너 여백 : p-6 */} -
+
{/* 헤더: 뒤로가기 + 타이틀 + 다운로드 */}
@@ -547,13 +723,13 @@ export default function CowOverviewPage() { {/* 탭 네비게이션 */} - + - 유전체 + 유전체 {hasGenomeData && isValidGenomeAnalysis(genomeRequest?.chipSireName, genomeRequest?.chipDamName, cow?.cowId) ? '완료' : '미검사'} @@ -563,7 +739,7 @@ export default function CowOverviewPage() { className="flex items-center justify-center gap-1.5 sm:gap-3 rounded-none border-b-2 border-transparent data-[state=active]:border-b-primary data-[state=active]:text-primary bg-transparent px-1.5 sm:px-5 py-2.5 sm:py-5 text-muted-foreground data-[state=active]:shadow-none" > - 유전자 + 유전자 {hasGeneData && !(isExcludedCow(cow?.cowId) || genomeRequest?.chipSireName === '분석불가' || genomeRequest?.chipSireName === '정보없음') ? '완료' : '미검사'} @@ -573,1144 +749,1007 @@ export default function CowOverviewPage() { className="flex items-center justify-center gap-1.5 sm:gap-3 rounded-none border-b-2 border-transparent data-[state=active]:border-b-primary data-[state=active]:text-primary bg-transparent px-1.5 sm:px-5 py-2.5 sm:py-5 text-muted-foreground data-[state=active]:shadow-none" > - 번식능력 + 번식능력 {hasReproductionData ? '완료' : '미검사'} - {/* 유전체 분석 탭 */} - - {hasGenomeData ? ( - <> - {/* 개체 정보 섹션 */} -

개체 정보

+ {/* 탭 콘텐츠 영역 */} +
+ {/* 유전체 분석 탭 */} + + {hasGenomeData ? ( + <> + {/* 개체 정보 섹션 */} +

개체 정보

- - - {/* 모바일: 세로 리스트 / 데스크탑: 가로 그리드 */} -
-
-
- 개체번호 + + + {/* 모바일: 세로 리스트 / 데스크탑: 가로 그리드 */} +
+
+
+ 개체번호 +
+
+ +
-
- +
+
+ 생년월일 +
+
+ + {cow?.cowBirthDt ? new Date(cow.cowBirthDt).toLocaleDateString('ko-KR') : '-'} + +
-
-
-
- 생년월일 +
+
+ 월령 (분석일 기준) +
+
+ + {cow?.cowBirthDt && genomeData[0]?.request?.requestDt + ? `${Math.floor((new Date(genomeData[0].request.requestDt).getTime() - new Date(cow.cowBirthDt).getTime()) / (1000 * 60 * 60 * 24 * 30.44))}개월` + : '-'} + +
-
- - {cow?.cowBirthDt ? new Date(cow.cowBirthDt).toLocaleDateString('ko-KR') : '-'} - -
-
-
-
- 월령 (분석일 기준) -
-
- - {cow?.cowBirthDt && genomeData[0]?.request?.requestDt - ? `${Math.floor((new Date(genomeData[0].request.requestDt).getTime() - new Date(cow.cowBirthDt).getTime()) / (1000 * 60 * 60 * 24 * 30.44))}개월` - : '-'} - -
-
-
-
- 유전체 분석일자 -
-
- - {genomeData[0]?.request?.requestDt - ? new Date(genomeData[0].request.requestDt).toLocaleDateString('ko-KR') - : '-'} - -
-
-
- {/* 모바일: 좌우 배치 리스트 */} -
-
- 개체번호 -
- -
-
-
- 생년월일 - - {cow?.cowBirthDt ? new Date(cow.cowBirthDt).toLocaleDateString('ko-KR') : '-'} - -
-
- 월령 (분석) - - {cow?.cowBirthDt && genomeData[0]?.request?.requestDt - ? `${Math.floor((new Date(genomeData[0].request.requestDt).getTime() - new Date(cow.cowBirthDt).getTime()) / (1000 * 60 * 60 * 24 * 30.44))}개월` - : '-'} - -
-
- 분석일자 - - {genomeData[0]?.request?.requestDt - ? new Date(genomeData[0].request.requestDt).toLocaleDateString('ko-KR') - : '-'} - -
-
- - - - {/* 친자확인 섹션 */} -

혈통정보

- - - - {/* 데스크탑: 가로 그리드 */} -
-
-
- 부 KPN번호 -
-
- - {cow?.sireKpn && cow.sireKpn !== '0' ? cow.sireKpn : '-'} - - {renderSireBadge(genomeRequest?.chipSireName)} -
-
-
-
- 모 개체번호 -
-
- {cow?.damCowId && cow.damCowId !== '0' ? ( - - ) : ( - - - )} - {renderDamBadge(genomeRequest?.chipDamName)} -
-
-
- {/* 모바일: 좌우 배치 리스트 */} -
-
- 부 KPN번호 -
- - {cow?.sireKpn && cow.sireKpn !== '0' ? cow.sireKpn : '-'} - - {renderSireBadge(genomeRequest?.chipSireName, 'sm')} -
-
-
- 모 개체번호 -
- {cow?.damCowId && cow.damCowId !== '0' ? ( - - ) : ( - - - )} - {renderDamBadge(genomeRequest?.chipDamName, 'sm')} -
-
-
-
-
- - {/* 친자확인 결과에 따른 분기 (유효성 조건: 아비일치 + 어미불일치/이력제부재 제외 + 제외목록) */} - {isValidGenomeAnalysis(genomeData[0]?.request?.chipSireName, genomeData[0]?.request?.chipDamName, cow?.cowId) ? ( - <> - {/* 농가 및 보은군 내 개체 위치 */} -

농가 및 보은군 내 개체 위치

-
- { }} - onToggleTraitSelection={toggleTraitSelection} - onClearSelectedTraits={() => setSelectedTraits([])} - onOpenChartModal={() => setIsChartModalOpen(true)} - distributionData={distributionData} - totalCowCount={totalCowCount} - traitComparisons={traitComparisons} - cowEpd={cowAvgEpd} - farmAvgEpd={farmAvgEpdValue} - regionAvgEpd={regionAvgEpdValue} - farmRank={selectionIndex?.farmRank} - farmTotal={selectionIndex?.farmTotal} - regionRank={selectionIndex?.regionRank} - highlightMode={highlightMode} - onHighlightModeChange={setHighlightMode} - selectionIndexHistogram={selectionIndex?.histogram || []} - regionTotal={selectionIndex?.regionTotal} - chartFilterTrait={chartFilterTrait} - onChartFilterTraitChange={setChartFilterTrait} - /> -
- - {/* 유전체 형질별 육종가 비교 */} -

유전체 형질별 육종가 비교

- - -

선택 형질 상세

- - - -

분석 정보

- - - -
-
-
접수일
-
+
+
+ 유전체 분석일자 +
+
+ {genomeData[0]?.request?.requestDt ? new Date(genomeData[0].request.requestDt).toLocaleDateString('ko-KR') : '-'} -
+
-
-
분석 완료일
-
- {genomeData[0]?.request?.chipReportDt - ? new Date(genomeData[0].request.chipReportDt).toLocaleDateString('ko-KR') - : '-'} -
-
-
-
칩 종류
-
- {genomeData[0]?.request?.chipType || '-'} -
-
-
- - - - ) : ( - <> -

유전체 분석 결과

- - - -
- - {getInvalidReason(genomeData[0]?.request?.chipSireName, genomeData[0]?.request?.chipDamName, cow?.cowId) || '분석 불가'} - -
-
-

- {getInvalidMessage(genomeData[0]?.request?.chipSireName, genomeData[0]?.request?.chipDamName, cow?.cowId)} -

-
-

안내사항

-
    -
  • - - 유전체 분석 보고서는 친자확인이 완료된 개체에 한해 제공됩니다. -
  • -
  • - - 정확한 분석을 위해 재검사 또는 KPN 정보 확인이 필요합니다. -
  • -
-
-
-
-
- - )} - - ) : ( - <> - {/* 개체 정보 섹션 */} -

개체 정보

- - - - {/* 모바일: 세로 리스트 / 데스크탑: 가로 그리드 */} -
-
-
- 개체번호 -
-
-
-
-
- 생년월일 + {/* 모바일: 좌우 배치 리스트 */} +
+
+ 개체번호 +
+ +
-
- +
+ 생년월일 + {cow?.cowBirthDt ? new Date(cow.cowBirthDt).toLocaleDateString('ko-KR') : '-'}
-
-
-
- 월령 (분석일 기준) -
-
- +
+ 월령 (분석) + {cow?.cowBirthDt && genomeData[0]?.request?.requestDt ? `${Math.floor((new Date(genomeData[0].request.requestDt).getTime() - new Date(cow.cowBirthDt).getTime()) / (1000 * 60 * 60 * 24 * 30.44))}개월` : '-'}
-
-
-
- 유전체 분석일자 -
-
- - -
-
-
- {/* 모바일: 좌우 배치 리스트 */} -
-
- 개체번호 -
- -
-
-
- 생년월일 - - {cow?.cowBirthDt ? new Date(cow.cowBirthDt).toLocaleDateString('ko-KR') : '-'} - -
-
- 월령 (분석) - - {cow?.cowBirthDt && genomeData[0]?.request?.requestDt - ? `${Math.floor((new Date(genomeData[0].request.requestDt).getTime() - new Date(cow.cowBirthDt).getTime()) / (1000 * 60 * 60 * 24 * 30.44))}개월` - : '-'} - -
-
- 분석일자 - - -
-
- - - - {/* 친자확인 섹션 */} -

혈통정보

- - - - {/* 데스크탑: 가로 그리드 */} -
-
-
- 부 KPN번호 -
-
- {cow?.sireKpn && cow.sireKpn !== '0' ? cow.sireKpn : '-'} - {renderSireBadge(genomeRequest?.chipSireName)} -
-
-
-
- 모 개체번호 -
-
- {cow?.damCowId && cow.damCowId !== '0' ? ( - - ) : ( - - - )} - {renderDamBadge(genomeRequest?.chipDamName)} -
-
-
- {/* 모바일: 세로 리스트 */} -
-
- 부 KPN -
- {cow?.sireKpn && cow.sireKpn !== '0' ? cow.sireKpn : '-'} - {renderSireBadge(genomeRequest?.chipSireName, 'sm')} -
-
-
- 모 개체번호 -
- {cow?.damCowId && cow.damCowId !== '0' ? ( - - ) : ( - - - )} - {renderDamBadge(genomeRequest?.chipDamName, 'sm')} -
-
-
-
-
- - {/* 분석불가 메시지 */} -

유전체 분석 결과

- - - -

- {genomeRequest ? '유전체 분석 불가' : '유전체 분석불가'} -

-

- {genomeRequest - ? getInvalidMessage(genomeRequest?.chipSireName, genomeRequest?.chipDamName, cow?.cowId) - : '이 개체는 아직 유전체 분석이 진행되지 않았습니다.' - } -

-
-
- - )} - - - {/* 유전자 분석 탭 */} - - {geneDataLoading ? ( -
-
-
-

데이터를 불러오는 중...

-
-
- ) : hasGeneData ? ( - <> - {/* 개체 정보 섹션 (유전체 탭과 동일) */} -

개체 정보

- - - - {/* 데스크탑: 가로 그리드 */} -
-
-
- 개체번호 -
-
- -
-
-
-
- 생년월일 -
-
- - {cow?.cowBirthDt ? new Date(cow.cowBirthDt).toLocaleDateString('ko-KR') : '-'} - -
-
-
-
- 월령 (분석일 기준) -
-
- - {cow?.cowBirthDt && genomeData[0]?.request?.requestDt - ? `${Math.floor((new Date(genomeData[0].request.requestDt).getTime() - new Date(cow.cowBirthDt).getTime()) / (1000 * 60 * 60 * 24 * 30.44))}개월` - : '-'} - -
-
-
-
- 유전자 분석일자 -
-
- +
+ 분석일자 + {genomeData[0]?.request?.requestDt ? new Date(genomeData[0].request.requestDt).toLocaleDateString('ko-KR') : '-'}
-
- {/* 모바일: 좌우 배치 리스트 */} -
-
- 개체번호 -
- -
-
-
- 생년월일 - - {cow?.cowBirthDt ? new Date(cow.cowBirthDt).toLocaleDateString('ko-KR') : '-'} - -
-
- 월령 (분석) - - {cow?.cowBirthDt && genomeData[0]?.request?.requestDt - ? `${Math.floor((new Date(genomeData[0].request.requestDt).getTime() - new Date(cow.cowBirthDt).getTime()) / (1000 * 60 * 60 * 24 * 30.44))}개월` - : '-'} - -
-
- 분석일자 - - {genomeData[0]?.request?.requestDt - ? new Date(genomeData[0].request.requestDt).toLocaleDateString('ko-KR') - : '-'} - -
-
- - + + - {/* 친자확인 결과 섹션 (유전체 탭과 동일) */} -

혈통정보

+ {/* 친자확인 섹션 */} +

혈통정보

- - - {/* 데스크탑: 가로 그리드 */} -
-
-
- 부 KPN번호 + + + {/* 데스크탑: 가로 그리드 */} +
+
+
+ 부 KPN번호 +
+
+ + {cow?.sireKpn && cow.sireKpn !== '0' ? cow.sireKpn : '-'} + + {renderSireBadge(genomeRequest?.chipSireName)} +
-
- - {cow?.sireKpn && cow.sireKpn !== '0' ? cow.sireKpn : '-'} - - {renderSireBadge(genomeRequest?.chipSireName)} +
+
+ 모 개체번호 +
+
+ {cow?.damCowId && cow.damCowId !== '0' ? ( + + ) : ( + - + )} + {renderDamBadge(genomeRequest?.chipDamName)} +
-
-
- 모 개체번호 + {/* 모바일: 좌우 배치 리스트 */} +
+
+ 부 KPN번호 +
+ + {cow?.sireKpn && cow.sireKpn !== '0' ? cow.sireKpn : '-'} + + {renderSireBadge(genomeRequest?.chipSireName, 'sm')} +
-
- {cow?.damCowId && cow.damCowId !== '0' ? ( - - ) : ( +
+ 모 개체번호 +
+ {cow?.damCowId && cow.damCowId !== '0' ? ( + + ) : ( + - + )} + {renderDamBadge(genomeRequest?.chipDamName, 'sm')} +
+
+
+ + + + {/* 친자확인 결과에 따른 분기 (유효성 조건: 아비일치 + 어미불일치/이력제부재 제외 + 제외목록) */} + {isValidGenomeAnalysis(genomeData[0]?.request?.chipSireName, genomeData[0]?.request?.chipDamName, cow?.cowId) ? ( + <> + {/* 농가 및 보은군 내 개체 위치 */} +

농가 및 보은군 내 개체 위치

+
+ { }} + onToggleTraitSelection={toggleTraitSelection} + onClearSelectedTraits={() => setSelectedTraits([])} + onOpenChartModal={() => setIsChartModalOpen(true)} + distributionData={distributionData} + totalCowCount={totalCowCount} + traitComparisons={traitComparisons} + cowEpd={cowAvgEpd} + farmAvgEpd={farmAvgEpdValue} + regionAvgEpd={regionAvgEpdValue} + farmRank={selectionIndex?.farmRank} + farmTotal={selectionIndex?.farmTotal} + regionRank={selectionIndex?.regionRank} + highlightMode={highlightMode} + onHighlightModeChange={setHighlightMode} + selectionIndexHistogram={selectionIndex?.histogram || []} + regionTotal={selectionIndex?.regionTotal} + chartFilterTrait={chartFilterTrait} + onChartFilterTraitChange={setChartFilterTrait} + /> +
+ + {/* 유전체 형질별 육종가 비교 */} +

유전체 형질별 육종가 비교

+ + +

선택 형질 상세

+ + + +

분석 정보

+
+

본 유전체 분석 결과는 국가단위 '한우암소 유전체 분석 서비스'에서 제공하는 자료입니다.

+

농림축산식품부-국립축산과학원-농협한우개량사업소-도축산연구소는 협력 체계를 구축하여 농가 암소의 유전체 유전능력을 조기에 분석하여 개량에 활용할 수 있도록 서비스 하고 있습니다.

+

암소의 유전체 유전능력은 국가단위 보증씨수소 유전능력 평가결과를 활용하여 6개월 단위로 자료를 갱신하고 있으며, 이번 평가결과는 '25.8.1. ~ '26.1.31.까지 유효합니다.

+

씨수소 참조집단의 유전체(SNP) 분석칩과 암소의 능력 계산에 이용하는 암소의 유전체 분석칩이 다를 경우, 암소의 형질별 유전체 육종가 값이 일부 차이가 날 수 있습니다.

+
+ + +
+
+
접수일
+
+ {genomeData[0]?.request?.requestDt + ? new Date(genomeData[0].request.requestDt).toLocaleDateString('ko-KR') + : '-'} +
+
+
+
분석 완료일
+
+ {genomeData[0]?.request?.chipReportDt + ? new Date(genomeData[0].request.chipReportDt).toLocaleDateString('ko-KR') + : '-'} +
+
+
+
칩 종류
+
+ {genomeData[0]?.request?.chipType || '-'} +
+
+
+
+
+ + ) : ( + <> +

유전체 분석 결과

+ + + +
+ + {getInvalidReason(genomeData[0]?.request?.chipSireName, genomeData[0]?.request?.chipDamName, cow?.cowId) || '분석 불가'} + +
+
+

+ {getInvalidMessage(genomeData[0]?.request?.chipSireName, genomeData[0]?.request?.chipDamName, cow?.cowId)} +

+
+

안내사항

+
    +
  • + + 유전체 분석 보고서는 친자확인이 완료된 개체에 한해 제공됩니다. +
  • +
  • + + 정확한 분석을 위해 재검사 또는 KPN 정보 확인이 필요합니다. +
  • +
+
+
+
+
+ + )} + + ) : ( + <> + {/* 개체 정보 섹션 */} +

개체 정보

+ + + + {/* 모바일: 세로 리스트 / 데스크탑: 가로 그리드 */} +
+
+
+ 개체번호 +
+
+ +
+
+
+
+ 생년월일 +
+
+ + {cow?.cowBirthDt ? new Date(cow.cowBirthDt).toLocaleDateString('ko-KR') : '-'} + +
+
+
+
+ 월령 (분석일 기준) +
+
+ + {cow?.cowBirthDt && genomeData[0]?.request?.requestDt + ? `${Math.floor((new Date(genomeData[0].request.requestDt).getTime() - new Date(cow.cowBirthDt).getTime()) / (1000 * 60 * 60 * 24 * 30.44))}개월` + : '-'} + +
+
+
+
+ 유전체 분석일자 +
+
- - )} - {renderDamBadge(genomeRequest?.chipDamName)} +
-
- {/* 모바일: 좌우 배치 리스트 */} -
-
- 부 KPN번호 -
- - {cow?.sireKpn && cow.sireKpn !== '0' ? cow.sireKpn : '-'} + {/* 모바일: 좌우 배치 리스트 */} +
+
+ 개체번호 +
+ +
+
+
+ 생년월일 + + {cow?.cowBirthDt ? new Date(cow.cowBirthDt).toLocaleDateString('ko-KR') : '-'} - {renderSireBadge(genomeRequest?.chipSireName, 'sm')} +
+
+ 월령 (분석) + + {cow?.cowBirthDt && genomeData[0]?.request?.requestDt + ? `${Math.floor((new Date(genomeData[0].request.requestDt).getTime() - new Date(cow.cowBirthDt).getTime()) / (1000 * 60 * 60 * 24 * 30.44))}개월` + : '-'} + +
+
+ 분석일자 + -
-
- 모 개체번호 -
- {cow?.damCowId && cow.damCowId !== '0' ? ( - - ) : ( - - - )} - {renderDamBadge(genomeRequest?.chipDamName, 'sm')} + + + + {/* 친자확인 섹션 */} +

혈통정보

+ + + + {/* 데스크탑: 가로 그리드 */} +
+
+
+ 부 KPN번호 +
+
+ {cow?.sireKpn && cow.sireKpn !== '0' ? cow.sireKpn : '-'} + {renderSireBadge(genomeRequest?.chipSireName)} +
+
+
+
+ 모 개체번호 +
+
+ {cow?.damCowId && cow.damCowId !== '0' ? ( + + ) : ( + - + )} + {renderDamBadge(genomeRequest?.chipDamName)} +
-
- - + {/* 모바일: 세로 리스트 */} +
+
+ 부 KPN +
+ {cow?.sireKpn && cow.sireKpn !== '0' ? cow.sireKpn : '-'} + {renderSireBadge(genomeRequest?.chipSireName, 'sm')} +
+
+
+ 모 개체번호 +
+ {cow?.damCowId && cow.damCowId !== '0' ? ( + + ) : ( + - + )} + {renderDamBadge(genomeRequest?.chipDamName, 'sm')} +
+
+
+ + - {/* 유전자 검색 및 필터 섹션 */} -

유전자 분석 결과

+ {/* 분석불가 메시지 */} +

유전체 분석 결과

+ + + +

+ {genomeRequest ? '유전체 분석 불가' : '유전체 분석불가'} +

+

+ {genomeRequest + ? getInvalidMessage(genomeRequest?.chipSireName, genomeRequest?.chipDamName, cow?.cowId) + : '이 개체는 아직 유전체 분석이 진행되지 않았습니다.' + } +

+
+
+ + )} + - {/* 유전자 탭 분기: 분석불가/정보없음만 차단, 불일치/이력제부재는 유전자 데이터 표시 */} - {!(isExcludedCow(cow?.cowId) || genomeRequest?.chipSireName === '분석불가' || genomeRequest?.chipSireName === '정보없음') ? ( - <> -
- {/* 검색창 */} -
- - setGeneSearchInput(e.target.value)} - /> + {/* 유전자 분석 탭 */} + + {geneDataLoading ? ( +
+
+
+

데이터를 불러오는 중...

+
+ ) : hasGeneData ? ( + <> + {/* 개체 정보 섹션 (유전체 탭과 동일) */} +

개체 정보

- {/* 필터 옵션들 */} -
- {/* 유전자 타입 필터 */} -
- 구분: -
- - - + + + {/* 데스크탑: 가로 그리드 */} +
+
+
+ 개체번호 +
+
+ +
+
+
+
+ 생년월일 +
+
+ + {cow?.cowBirthDt ? new Date(cow.cowBirthDt).toLocaleDateString('ko-KR') : '-'} + +
+
+
+
+ 월령 (분석일 기준) +
+
+ + {cow?.cowBirthDt && genomeData[0]?.request?.requestDt + ? `${Math.floor((new Date(genomeData[0].request.requestDt).getTime() - new Date(cow.cowBirthDt).getTime()) / (1000 * 60 * 60 * 24 * 30.44))}개월` + : '-'} + +
+
+
+
+ 유전자 분석일자 +
+
+ + {genomeData[0]?.request?.requestDt + ? new Date(genomeData[0].request.requestDt).toLocaleDateString('ko-KR') + : '-'} + +
+
-
+ {/* 모바일: 좌우 배치 리스트 */} +
+
+ 개체번호 +
+ +
+
+
+ 생년월일 + + {cow?.cowBirthDt ? new Date(cow.cowBirthDt).toLocaleDateString('ko-KR') : '-'} + +
+
+ 월령 (분석) + + {cow?.cowBirthDt && genomeData[0]?.request?.requestDt + ? `${Math.floor((new Date(genomeData[0].request.requestDt).getTime() - new Date(cow.cowBirthDt).getTime()) / (1000 * 60 * 60 * 24 * 30.44))}개월` + : '-'} + +
+
+ 분석일자 + + {genomeData[0]?.request?.requestDt + ? new Date(genomeData[0].request.requestDt).toLocaleDateString('ko-KR') + : '-'} + +
+
+ + - {/* 정렬 드롭다운 */} -
- - { + setGenesPerPage(parseInt(value, 10)) + setGeneCurrentLoadedPage(1) + }}> + - 오름차순 - 내림차순 + 50개 + 100개 + 1000개
-
- {/* 유전자 테이블/카드 */} - {(() => { - const filteredData = geneData.filter(gene => { - // 검색 필터 (테이블의 모든 필드 검색) - if (geneSearchKeyword) { - const keyword = geneSearchKeyword.toLowerCase() - const snpName = (gene.snpName || '').toLowerCase() - const chromosome = (gene.chromosome || '').toLowerCase() - const position = (gene.position || '').toLowerCase() - const snpType = (gene.snpType || '').toLowerCase() - const allele1 = (gene.allele1 || '').toLowerCase() - const allele2 = (gene.allele2 || '').toLowerCase() - const remarks = (gene.remarks || '').toLowerCase() - if (!snpName.includes(keyword) && - !chromosome.includes(keyword) && - !position.includes(keyword) && - !snpType.includes(keyword) && - !allele1.includes(keyword) && - !allele2.includes(keyword) && - !remarks.includes(keyword)) { - return false - } - } - // 유전자형 필터 - if (genotypeFilter !== 'all') { - const isHomozygous = gene.allele1 === gene.allele2 - if (genotypeFilter === 'homozygous' && !isHomozygous) return false - if (genotypeFilter === 'heterozygous' && isHomozygous) return false - } - return true - }) + {/* 유전자 탭 분기: 분석불가/정보없음만 차단, 불일치/이력제부재는 유전자 데이터 표시 */} + {!(isExcludedCow(cow?.cowId) || genomeRequest?.chipSireName === '분석불가' || genomeRequest?.chipSireName === '정보없음') ? ( + <> +
+ {/* 검색창 */} +
+ + setGeneSearchInput(e.target.value)} + /> +
- // 정렬 - const sortedData = [...filteredData].sort((a, b) => { - let aVal: string | number = '' - let bVal: string | number = '' + {/* 필터 옵션들 */} + {/*
*/} + {/* 유전자 타입 필터 */} + {/*
+ 구분: +
+ + + +
+
*/} - switch (geneSortBy) { - case 'snpName': - aVal = a.snpName || '' - bVal = b.snpName || '' - break - case 'chromosome': - aVal = parseInt(a.chromosome || '0') || 0 - bVal = parseInt(b.chromosome || '0') || 0 - break - case 'position': - aVal = parseInt(a.position || '0') || 0 - bVal = parseInt(b.position || '0') || 0 - break - case 'snpType': - aVal = a.snpType || '' - bVal = b.snpType || '' - break - case 'allele1': - aVal = a.allele1 || '' - bVal = b.allele1 || '' - break - case 'allele2': - aVal = a.allele2 || '' - bVal = b.allele2 || '' - break - case 'remarks': - aVal = a.remarks || '' - bVal = b.remarks || '' - break - } + {/* 정렬 드롭다운 */} + {/*
+ + +
*/} + {/*
*/} +
- if (typeof aVal === 'number' && typeof bVal === 'number') { - return geneSortOrder === 'asc' ? aVal - bVal : bVal - aVal - } - - const strA = String(aVal) - const strB = String(bVal) - return geneSortOrder === 'asc' - ? strA.localeCompare(strB) - : strB.localeCompare(strA) - }) - - // 페이지네이션 계산 - const totalPages = Math.ceil(sortedData.length / GENES_PER_PAGE) - const startIndex = (geneCurrentPage - 1) * GENES_PER_PAGE - const endIndex = startIndex + GENES_PER_PAGE - const displayData = sortedData.length > 0 - ? sortedData.slice(startIndex, endIndex) - : Array(10).fill(null) - - // 페이지네이션 UI 컴포넌트 - const PaginationUI = () => { - if (sortedData.length <= GENES_PER_PAGE) return null - - // 표시할 페이지 번호들 계산 (모바일: 3개 단순, 데스크탑: 5개 + 1/마지막 고정) - const getPageNumbers = () => { - const pages: (number | string)[] = [] - const showPages = isMobile ? 3 : 5 - const offset = isMobile ? 1 : 2 - let start = Math.max(1, geneCurrentPage - offset) - let end = Math.min(totalPages, start + showPages - 1) - - if (end - start < showPages - 1) { - start = Math.max(1, end - showPages + 1) - } - - // 모바일: 현재 페이지 기준 앞뒤만 표시 (1, 마지막 고정 없음) - if (isMobile) { - for (let i = start; i <= end; i++) { - pages.push(i) - } - return pages - } - - // 데스크탑: 1과 마지막 페이지 고정 - if (start > 1) { - pages.push(1) - if (start > 2) pages.push('...') - } - - for (let i = start; i <= end; i++) { - pages.push(i) - } - - if (end < totalPages) { - if (end < totalPages - 1) pages.push('...') - pages.push(totalPages) - } - - return pages - } + {/* 유전자 테이블/카드 */} + {(() => { + // 무한 스크롤 계산 + const totalItems = geneCurrentLoadedPage * genesPerPage + const displayData = filteredAndSortedGeneData.length > 0 + ? filteredAndSortedGeneData.slice(0, totalItems) + : [] return ( -
- - 전체 {sortedData.length.toLocaleString()}개 중 {startIndex + 1}-{Math.min(endIndex, sortedData.length)}번째 - -
- - - {getPageNumbers().map((page, idx) => ( - typeof page === 'number' ? ( - - ) : ( - ... - ) - ))} - - + <> + {/* 데스크톱: 테이블 */} +
+ + +
+ + + + + + + + + + + + + + {displayData.map((gene, idx) => ( + + + + + + + + + + ))} + {isLoadingMoreGenes && ( + + + + )} + +
SNP 이름염색체 위치PositionSNP 구분첫번째 대립유전자두번째 대립유전자설명
{gene.snpName || '-'}{gene.chromosome || '-'}{gene.position || '-'}{gene.snpType || '-'}{gene.allele1 || '-'}{gene.allele2 || '-'}{gene.remarks || '-'}
+ 로딩 중... +
+
+
+
+ {/* 현황 정보 표시 */} +
+ + {filteredAndSortedGeneData.length > 0 ? ( + <> + 전체 {filteredAndSortedGeneData.length.toLocaleString()}개 중 1-{displayData.length.toLocaleString()}번째 + {isLoadingMoreGenes && ' (로딩 중...)'} + + ) : ( + '데이터 없음' + )} + +
+
+ + {/* 모바일: 카드 뷰 */} +
+
+ {displayData.map((gene, idx) => ( + + +
+ SNP 이름 + {gene?.snpName || '-'} +
+
+ 염색체 위치 + {gene?.chromosome || '-'} +
+
+ Position + {gene?.position || '-'} +
+
+ SNP 구분 + {gene?.snpType || '-'} +
+
+ 첫번째 대립유전자 + {gene?.allele1 || '-'} +
+
+ 두번째 대립유전자 + {gene?.allele2 || '-'} +
+
+ 설명 + {gene?.remarks || '-'} +
+
+
+ ))} + {isLoadingMoreGenes && ( +
+ 로딩 중... +
+ )} +
+ {/* 현황 정보 표시 */} +
+ + {filteredAndSortedGeneData.length > 0 ? ( + <> + 전체 {filteredAndSortedGeneData.length.toLocaleString()}개 중 1-{displayData.length.toLocaleString()}번째 + {isLoadingMoreGenes && ' (로딩 중...)'} + + ) : ( + '데이터 없음' + )} + +
+
+ + ) + })()} + + ) : ( + + + +

+ {getInvalidReason(genomeRequest?.chipSireName, genomeRequest?.chipDamName, cow?.cowId) || '유전자 분석 불가'} +

+

+ {getInvalidMessage(genomeRequest?.chipSireName, genomeRequest?.chipDamName, cow?.cowId).replace('유전체', '유전자')} +

+
+
+ )} + + ) : ( + <> + {/* 개체 정보 섹션 */} +

개체 정보

+ + + + {/* 데스크탑: 가로 그리드 */} +
+
+
+ 개체번호 +
+
+ +
+
+
+
+ 생년월일 +
+
+ + {cow?.cowBirthDt ? new Date(cow.cowBirthDt).toLocaleDateString('ko-KR') : '-'} + +
+
+
+
+ 월령 (분석일 기준) +
+
+ - +
+
+
+
+ 분석일자 +
+
+ - +
- ) - } - - return ( - <> - {/* 데스크톱: 테이블 */} - - -
- - - - - - - - - - - - - - {displayData.map((gene, idx) => { - if (!gene) { - return ( - - - - - - - - - - ) - } - return ( - - - - - - - - - - ) - })} - -
SNP 이름염색체 위치PositionSNP 구분첫번째 대립유전자두번째 대립유전자설명
-------
{gene.snpName || '-'}{gene.chromosome || '-'}{gene.position || '-'}{gene.snpType || '-'}{gene.allele1 || '-'}{gene.allele2 || '-'}{gene.remarks || '-'}
+ {/* 모바일: 좌우 배치 리스트 */} +
+
+ 개체번호 +
+
- - - +
+
+ 생년월일 + + {cow?.cowBirthDt ? new Date(cow.cowBirthDt).toLocaleDateString('ko-KR') : '-'} + +
+
+ 월령 (분석) + + {cow?.cowBirthDt && genomeData[0]?.request?.requestDt + ? `${Math.floor((new Date(genomeData[0].request.requestDt).getTime() - new Date(cow.cowBirthDt).getTime()) / (1000 * 60 * 60 * 24 * 30.44))}개월` + : '-'} + +
+
+ 분석일자 + - +
+
+ + - {/* 모바일: 카드 뷰 */} -
- {displayData.map((gene, idx) => { - return ( - - -
- SNP 이름 - {gene?.snpName || '-'} -
-
- 염색체 위치 - {gene?.chromosome || '-'} -
-
- Position - {gene?.position || '-'} -
-
- SNP 구분 - {gene?.snpType || '-'} -
-
- 첫번째 대립유전자 - {gene?.allele1 || '-'} -
-
- 두번째 대립유전자 - {gene?.allele2 || '-'} -
-
- 설명 - {gene?.remarks || '-'} -
-
-
- ) - })} + {/* 혈통정보 섹션 */} +

혈통정보

+ + + + {/* 데스크탑: 가로 그리드 */} +
+
+
+ 부 KPN번호 +
+
+ {cow?.sireKpn && cow.sireKpn !== '0' ? cow.sireKpn : '-'} + {renderSireBadge(genomeRequest?.chipSireName)} +
+
+
+
+ 모 개체번호 +
+
+ {cow?.damCowId && cow.damCowId !== '0' ? ( + + ) : ( + - + )} + {renderDamBadge(genomeRequest?.chipDamName)} +
+
-
- + {/* 모바일: 세로 리스트 */} +
+
+ 부 KPN +
+ {cow?.sireKpn && cow.sireKpn !== '0' ? cow.sireKpn : '-'} + {renderSireBadge(genomeRequest?.chipSireName, 'sm')} +
+
+
+ 모 개체번호 +
+ {cow?.damCowId && cow.damCowId !== '0' ? ( + + ) : ( + - + )} + {renderDamBadge(genomeRequest?.chipDamName, 'sm')} +
+
- - ) - })()} - - ) : ( + + + + {/* 유전자 분석 결과 섹션 */} +

유전자 분석 결과

- {getInvalidReason(genomeRequest?.chipSireName, genomeRequest?.chipDamName, cow?.cowId) || '유전자 분석 불가'} + {genomeRequest ? '유전자 분석 불가' : '유전자 분석불가'}

- {getInvalidMessage(genomeRequest?.chipSireName, genomeRequest?.chipDamName, cow?.cowId).replace('유전체', '유전자')} + {genomeRequest + ? getInvalidMessage(genomeRequest?.chipSireName, genomeRequest?.chipDamName, cow?.cowId).replace('유전체', '유전자') + : '이 개체는 아직 유전자(SNP) 분석이 진행되지 않았습니다.' + }

- )} - - ) : ( - <> - {/* 개체 정보 섹션 */} -

개체 정보

+ + )} + - - - {/* 데스크탑: 가로 그리드 */} -
-
-
- 개체번호 -
-
- -
-
-
-
- 생년월일 -
-
- - {cow?.cowBirthDt ? new Date(cow.cowBirthDt).toLocaleDateString('ko-KR') : '-'} - -
-
-
-
- 월령 (분석일 기준) -
-
- - -
-
-
-
- 분석일자 -
-
- - -
-
-
- {/* 모바일: 좌우 배치 리스트 */} -
-
- 개체번호 -
- -
-
-
- 생년월일 - - {cow?.cowBirthDt ? new Date(cow.cowBirthDt).toLocaleDateString('ko-KR') : '-'} - -
-
- 월령 (분석) - - {cow?.cowBirthDt && genomeData[0]?.request?.requestDt - ? `${Math.floor((new Date(genomeData[0].request.requestDt).getTime() - new Date(cow.cowBirthDt).getTime()) / (1000 * 60 * 60 * 24 * 30.44))}개월` - : '-'} - -
-
- 분석일자 - - -
-
-
-
- - {/* 혈통정보 섹션 */} -

혈통정보

- - - - {/* 데스크탑: 가로 그리드 */} -
-
-
- 부 KPN번호 -
-
- {cow?.sireKpn && cow.sireKpn !== '0' ? cow.sireKpn : '-'} - {renderSireBadge(genomeRequest?.chipSireName)} -
-
-
-
- 모 개체번호 -
-
- {cow?.damCowId && cow.damCowId !== '0' ? ( - - ) : ( - - - )} - {renderDamBadge(genomeRequest?.chipDamName)} -
-
-
- {/* 모바일: 세로 리스트 */} -
-
- 부 KPN -
- {cow?.sireKpn && cow.sireKpn !== '0' ? cow.sireKpn : '-'} - {renderSireBadge(genomeRequest?.chipSireName, 'sm')} -
-
-
- 모 개체번호 -
- {cow?.damCowId && cow.damCowId !== '0' ? ( - - ) : ( - - - )} - {renderDamBadge(genomeRequest?.chipDamName, 'sm')} -
-
-
-
-
- - {/* 유전자 분석 결과 섹션 */} -

유전자 분석 결과

- - - -

- {genomeRequest ? '유전자 분석 불가' : '유전자 분석불가'} -

-

- {genomeRequest - ? getInvalidMessage(genomeRequest?.chipSireName, genomeRequest?.chipDamName, cow?.cowId).replace('유전체', '유전자') - : '이 개체는 아직 유전자(SNP) 분석이 진행되지 않았습니다.' - } -

-
-
- - )} - - - - {/* 번식능력 탭 */} - - {/* 혈액화학검사(MPT) 테이블 */} - - + {/* 번식능력 탭 */} + + {/* 혈액화학검사(MPT) 테이블 */} + + +
diff --git a/frontend/src/app/cow/[cowNo]/reproduction/_components/mpt-table.tsx b/frontend/src/app/cow/[cowNo]/reproduction/_components/mpt-table.tsx index 8623a61..75aff84 100644 --- a/frontend/src/app/cow/[cowNo]/reproduction/_components/mpt-table.tsx +++ b/frontend/src/app/cow/[cowNo]/reproduction/_components/mpt-table.tsx @@ -90,7 +90,7 @@ export function MptTable({ cowShortNo, cowNo, farmNo, cow, genomeRequest }: MptT return (
{/* 개체 정보 섹션 */} -

개체 정보

+

개체 정보

{/* 데스크탑: 가로 그리드 */} @@ -161,7 +161,7 @@ export function MptTable({ cowShortNo, cowNo, farmNo, cow, genomeRequest }: MptT {/* 검사 정보 */} {selectedMpt && ( <> -

검사 정보

+

검사 정보

@@ -245,22 +245,22 @@ export function MptTable({ cowShortNo, cowNo, farmNo, cow, genomeRequest }: MptT {/* 혈액화학검사 결과 테이블 - selectedMpt가 있을 때만 표시 */} {selectedMpt ? ( <> -

혈액화학검사 결과

+

혈액화학검사 결과

{/* 데스크탑: 테이블 */}
- +
- - - - - - - + + + + + + + @@ -275,14 +275,14 @@ export function MptTable({ cowShortNo, cowNo, farmNo, cow, genomeRequest }: MptT {itemIdx === 0 && ( )} - + - - - + + +
카테고리검사항목측정값하한값상한값단위상태카테고리검사항목측정값하한값상한값단위상태
{category.name} {ref?.name || itemKey}{ref?.name || itemKey} - {ref?.lowerLimit ?? '-'}{ref?.upperLimit ?? '-'}{ref?.unit || '-'}{ref?.lowerLimit ?? '-'}{ref?.upperLimit ?? '-'}{ref?.unit || '-'} {value !== null && value !== undefined ? ( - - + - + 50두 100두 @@ -814,7 +814,7 @@ function MyCowContent() { {/* 데스크톱 테이블 뷰 */} {(
-
+
{/* */} @@ -1067,9 +1067,19 @@ function MyCowContent() { {cow.genomeScore.toFixed(2)} ) : ( - - 분석불가 - + + cow.anlysDt ? ( + + 분석불가 + + ) : ( + + 검사결과없음 + + ) + // + // 분석불가 + // )} {selectedDisplayGenes.length > 0 && ( @@ -1293,9 +1303,15 @@ function MyCowContent() { {cow.genomeScore.toFixed(2)} ) : ( + cow.anlysDt ? ( 분석불가 + ) : ( + + 검사결과없음 + + ) )} diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css index 1c05373..07d196d 100644 --- a/frontend/src/app/globals.css +++ b/frontend/src/app/globals.css @@ -110,7 +110,7 @@ } .text-base { - font-size: 1rem; /* 16px */ + font-size: 1.0rem; /* 16px */ line-height: 1.6; } diff --git a/frontend/src/components/ui/tabs.tsx b/frontend/src/components/ui/tabs.tsx index 497ba5e..3aa9bdd 100644 --- a/frontend/src/components/ui/tabs.tsx +++ b/frontend/src/components/ui/tabs.tsx @@ -42,6 +42,7 @@ function TabsTrigger({