- {/* 좌측 영역: 보은군 내 농가 위치 + 순위 통합 */}
+ {/* === 좌측: 보은군 내 농가 위치 차트 === */}
@@ -931,7 +1051,7 @@ export default function DashboardPage() {
)}
- {/* 우측 영역: 연도별 육종가 추이 */}
+ {/* === 우측: 연도별 육종가 추이 차트 === */}
연도별 육종가 추이
@@ -940,7 +1060,7 @@ export default function DashboardPage() {
보은군
- {/* 형질 선택 드롭다운 */}
+ {/* 형질 선택 드롭다운 (카테고리별 그룹화) */}
- {/* ========== 3. 카테고리별 보은군 대비 비교 - 전체 너비 ========== */}
+ {/* ========================================
+ 4. 카테고리별 보은군 대비 비교
+ - 레이더 차트 (좌측): 5개 카테고리 시각화
+ - 바 차트 (우측): 카테고리별 상세 육종가
+ ======================================== */}
보은군 대비 카테고리별 육종가 평균
{(stats?.summary.genomeCowCount || 0) > 0 && stats?.traitAverages && stats.traitAverages.length > 0 ? (
(() => {
+ // 5개 카테고리 분류 (성장/생산/체형/무게/비율)
const categories = ['성장', '생산', '체형', '무게', '비율']
+
+ /**
+ * 카테고리별 평균 계산
+ * @description
+ * - 각 카테고리에 속한 형질들의 EPD 평균
+ * - 백분위(percentile)도 평균 계산
+ */
const categoryData = categories.map(cat => {
const traits = stats.traitAverages.filter(t => t.category === cat)
const avgEpd = traits.length > 0
@@ -1168,17 +1300,30 @@ export default function DashboardPage() {
traitCount: traits.length
}
})
+
+ // 바 차트 최대 길이 계산용
const maxAbs = Math.max(...categoryData.map(d => Math.abs(d.avgEpd)), 1)
- // 레이더 차트용 설정
+ /**
+ * 레이더 차트 좌표 계산
+ * @description
+ * - 중심점: (140, 150)
+ * - 반지름: 95px
+ * - 5개 카테고리를 정오각형으로 배치
+ */
const centerX = 140
const centerY = 150
const maxRadius = 95
- const angleStep = (2 * Math.PI) / categories.length
- const startAngle = -Math.PI / 2
+ const angleStep = (2 * Math.PI) / categories.length // 72도씩
+ const startAngle = -Math.PI / 2 // 12시 방향부터 시작
- // 농가 데이터 다각형 좌표 (EPD 기반)
- // 육종가 범위에 맞게 스케일 조정
+ /**
+ * 농가 데이터 다각형 좌표 계산
+ * @description
+ * - 보은군 평균(0) = 반지름 50%
+ * - ±epdScale = 반지름 0%/100%
+ * - EPD 값을 반지름으로 변환
+ */
const epdScale = Math.max(...categoryData.map(d => Math.abs(d.avgEpd)), 10)
const farmPoints = categoryData.map((d, i) => {
const angle = startAngle + i * angleStep
@@ -1188,32 +1333,43 @@ export default function DashboardPage() {
return { x: centerX + radius * Math.cos(angle), y: centerY + radius * Math.sin(angle) }
})
- // 보은군 평균 (50% 지점 = EBV 0 기준)
+ /**
+ * 보은군 평균 다각형 (기준선)
+ * @description
+ * - 항상 50% 위치 (EBV = 0)
+ * - 점선으로 표시
+ */
const regionPoints = categories.map((_, i) => {
const angle = startAngle + i * angleStep
- const radius = 0.5 * maxRadius
+ const radius = 0.5 * maxRadius // 50% 고정
return { x: centerX + radius * Math.cos(angle), y: centerY + radius * Math.sin(angle) }
})
const farmPolygon = farmPoints.map(p => `${p.x},${p.y}`).join(' ')
const regionPolygon = regionPoints.map(p => `${p.x},${p.y}`).join(' ')
- // 호버된 포인트 인덱스를 위한 로컬 컴포넌트
+ /**
+ * 레이더 차트 SVG 컴포넌트
+ * @description
+ * - 호버/클릭으로 카테고리별 상세 정보 표시
+ * - 모바일에서는 터치로 토글
+ * - 툴팁은 차트 중앙에 고정 표시
+ */
const RadarChart = () => {
const [hoveredIndex, setHoveredIndex] = useState
(null)
const [clickedIndex, setClickedIndex] = useState(null)
- // 실제 표시할 인덱스 (클릭된 것 우선, 없으면 호버된 것)
+ // 실제 표시할 인덱스 (클릭 우선, 없으면 호버)
const activeIndex = clickedIndex !== null ? clickedIndex : hoveredIndex
- // 클릭/터치 핸들러: 토글 방식
+ // 클릭/터치 핸들러: 토글 방식 (같은 거 다시 누르면 닫힘)
const handleClick = (e: React.MouseEvent | React.TouchEvent, index: number) => {
e.preventDefault()
e.stopPropagation()
setClickedIndex(prev => prev === index ? null : index)
}
- // 호버 핸들러: 클릭된 상태가 아닐 때만 동작
+ // 호버 핸들러: 클릭 상태가 아닐 때만 동작
const handleMouseEnter = (index: number) => {
if (clickedIndex === null) {
setHoveredIndex(index)
diff --git a/frontend/src/app/dashboard/top-cows/page.tsx b/frontend/src/app/dashboard/top-cows/page.tsx
deleted file mode 100644
index 5d81523..0000000
--- a/frontend/src/app/dashboard/top-cows/page.tsx
+++ /dev/null
@@ -1,455 +0,0 @@
-'use client'
-
-import { AppSidebar } from "@/components/layout/app-sidebar"
-import { SiteHeader } from "@/components/layout/site-header"
-import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar"
-import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
-import { Badge } from "@/components/ui/badge"
-import { Button } from "@/components/ui/button"
-import { Trophy, ChevronLeft, TrendingUp, Award, Star } from "lucide-react"
-import { useRouter } from "next/navigation"
-import { AuthGuard } from "@/components/auth/auth-guard"
-
-export default function TopCowsPage() {
- const router = useRouter()
-
- // 더미 데이터 - 실제로는 백엔드에서 가져와야 함
- const topCows = [
- {
- rank: 1,
- cowNo: '001122334401',
- name: 'KOR 001122334401',
- score: 85.2,
- grade: 'A',
- birthDate: '2021-03-15',
- age: '3년 8개월',
- lactationCount: 2,
- traits: {
- carcassWeight: 92,
- marbling: 88,
- eyeMuscleArea: 90,
- backfatThickness: 82,
- bodyConformation: 95
- },
- strengths: ['체형', '도체중', '등심단면적'],
- weaknesses: ['등지방두께'],
- recentPerformance: {
- carcassWeight: 485,
- marblingScore: 7,
- eyeMuscleArea: 95
- },
- recommendations: '체형이 매우 우수한 개체입니다. 등지방두께 개선을 위한 KPN 선택을 권장합니다.'
- },
- {
- rank: 2,
- cowNo: '001122334402',
- name: 'KOR 001122334402',
- score: 82.7,
- grade: 'A',
- birthDate: '2020-11-20',
- age: '4년 0개월',
- lactationCount: 3,
- traits: {
- carcassWeight: 88,
- marbling: 94,
- eyeMuscleArea: 86,
- backfatThickness: 85,
- bodyConformation: 90
- },
- strengths: ['근내지방도', '체형', '도체중'],
- weaknesses: ['등심단면적'],
- recentPerformance: {
- carcassWeight: 465,
- marblingScore: 8,
- eyeMuscleArea: 88
- },
- recommendations: '근내지방도가 탁월한 개체입니다. 등심단면적 개선을 위한 교배 전략이 필요합니다.'
- },
- {
- rank: 3,
- cowNo: '001122334403',
- name: 'KOR 001122334403',
- score: 79.1,
- grade: 'A',
- birthDate: '2021-05-10',
- age: '3년 6개월',
- lactationCount: 2,
- traits: {
- carcassWeight: 90,
- marbling: 85,
- eyeMuscleArea: 88,
- backfatThickness: 75,
- bodyConformation: 87
- },
- strengths: ['도체중', '등심단면적'],
- weaknesses: ['등지방두께', '체형'],
- recentPerformance: {
- carcassWeight: 495,
- marblingScore: 6,
- eyeMuscleArea: 92
- },
- recommendations: '도체중이 우수한 개체입니다. 등지방두께와 체형 개선에 집중할 필요가 있습니다.'
- },
- {
- rank: 4,
- cowNo: '001122334404',
- name: 'KOR 001122334404',
- score: 76.5,
- grade: 'A',
- birthDate: '2020-08-25',
- age: '4년 3개월',
- lactationCount: 3,
- traits: {
- carcassWeight: 84,
- marbling: 82,
- eyeMuscleArea: 85,
- backfatThickness: 90,
- bodyConformation: 88
- },
- strengths: ['등지방두께', '체형'],
- weaknesses: ['근내지방도'],
- recentPerformance: {
- carcassWeight: 455,
- marblingScore: 5,
- eyeMuscleArea: 87
- },
- recommendations: '등지방두께가 매우 우수한 개체입니다. 근내지방도 개선을 위한 KPN 선택이 필요합니다.'
- },
- {
- rank: 5,
- cowNo: '001122334405',
- name: 'KOR 001122334405',
- score: 73.8,
- grade: 'A',
- birthDate: '2021-01-18',
- age: '3년 10개월',
- lactationCount: 2,
- traits: {
- carcassWeight: 86,
- marbling: 80,
- eyeMuscleArea: 83,
- backfatThickness: 88,
- bodyConformation: 85
- },
- strengths: ['등지방두께', '도체중'],
- weaknesses: ['근내지방도', '등심단면적'],
- recentPerformance: {
- carcassWeight: 470,
- marblingScore: 5,
- eyeMuscleArea: 85
- },
- recommendations: '균형 잡힌 개체입니다. 근내지방도 향상을 위한 교배가 권장됩니다.'
- },
- {
- rank: 6,
- cowNo: '001122334406',
- name: 'KOR 001122334406',
- score: 71.2,
- grade: 'B',
- birthDate: '2020-12-05',
- age: '3년 11개월',
- lactationCount: 2,
- traits: {
- carcassWeight: 82,
- marbling: 78,
- eyeMuscleArea: 81,
- backfatThickness: 84,
- bodyConformation: 83
- },
- strengths: ['등지방두께', '도체중'],
- weaknesses: ['근내지방도', '체형'],
- recentPerformance: {
- carcassWeight: 445,
- marblingScore: 4,
- eyeMuscleArea: 82
- },
- recommendations: '전반적으로 평균 이상인 개체입니다. 근내지방도와 체형 개선이 필요합니다.'
- },
- {
- rank: 7,
- cowNo: '001122334407',
- name: 'KOR 001122334407',
- score: 68.9,
- grade: 'B',
- birthDate: '2021-06-22',
- age: '3년 5개월',
- lactationCount: 2,
- traits: {
- carcassWeight: 80,
- marbling: 76,
- eyeMuscleArea: 79,
- backfatThickness: 82,
- bodyConformation: 81
- },
- strengths: ['등지방두께'],
- weaknesses: ['근내지방도', '등심단면적', '체형'],
- recentPerformance: {
- carcassWeight: 430,
- marblingScore: 4,
- eyeMuscleArea: 80
- },
- recommendations: '개선 여지가 많은 개체입니다. 근내지방도와 체형 강화가 필요합니다.'
- },
- {
- rank: 8,
- cowNo: '001122334408',
- name: 'KOR 001122334408',
- score: 65.5,
- grade: 'B',
- birthDate: '2020-09-30',
- age: '4년 2개월',
- lactationCount: 3,
- traits: {
- carcassWeight: 78,
- marbling: 74,
- eyeMuscleArea: 77,
- backfatThickness: 80,
- bodyConformation: 79
- },
- strengths: ['등지방두께'],
- weaknesses: ['근내지방도', '등심단면적', '도체중', '체형'],
- recentPerformance: {
- carcassWeight: 415,
- marblingScore: 3,
- eyeMuscleArea: 78
- },
- recommendations: '전반적인 개선이 필요한 개체입니다. 종합적인 개량 전략을 수립하세요.'
- }
- ]
-
- return (
-
-
-
-
-
-
- {/* 헤더 */}
-
-
-
-
우수 개체 순위
-
- 유전체 점수 기준 전체 개체 순위
-
-
-
-
- {/* 요약 카드 */}
-
-
-
- 전체 개체
- {topCows.length}두
-
-
-
-
- A등급
-
- {topCows.filter(cow => cow.grade === 'A').length}두
-
-
-
-
-
- 평균 점수
-
- {(topCows.reduce((sum, cow) => sum + cow.score, 0) / topCows.length).toFixed(1)}
-
-
-
-
-
- 최고 점수
-
- {Math.max(...topCows.map(cow => cow.score)).toFixed(1)}
-
-
-
-
-
- {/* 개체 상세 리스트 */}
-
- {topCows.map((cow, idx) => (
-
-
-
-
-
- {cow.rank}
-
-
-
- {cow.name}
-
- {cow.grade}등급
-
- {cow.rank <= 3 && (
-
- )}
-
-
- 개체번호: {cow.cowNo} · 나이: {cow.age} · 산차: {cow.lactationCount}산
-
-
-
-
-
-
-
-
- {/* 좌측: 형질 점수 */}
-
-
-
- 형질별 점수
-
-
- {Object.entries(cow.traits).map(([trait, score]) => {
- const traitNames: Record
= {
- carcassWeight: '도체중',
- marbling: '근내지방도',
- eyeMuscleArea: '등심단면적',
- backfatThickness: '등지방두께',
- bodyConformation: '체형'
- }
- const getScoreColor = (score: number) => {
- if (score >= 90) return 'bg-blue-600'
- if (score >= 80) return 'bg-blue-500'
- if (score >= 70) return 'bg-blue-400'
- return 'bg-slate-400'
- }
- return (
-
-
- {traitNames[trait]}
- {score}점
-
-
-
- )
- })}
-
-
-
- {/* 우측: 성능 데이터 및 분석 */}
-
- {/* 최근 성적 */}
-
-
-
- 예상 도체 성적
-
-
-
-
도체중
-
- {cow.recentPerformance.carcassWeight}
-
-
kg
-
-
-
근내지방
-
- {cow.recentPerformance.marblingScore}
-
-
번
-
-
-
등심단면적
-
- {cow.recentPerformance.eyeMuscleArea}
-
-
cm²
-
-
-
-
- {/* 강점/약점 */}
-
-
강점 / 약점
-
-
-
강점:
-
- {cow.strengths.map((strength, i) => (
-
- {strength}
-
- ))}
-
-
-
-
약점:
-
- {cow.weaknesses.map((weakness, i) => (
-
- {weakness}
-
- ))}
-
-
-
-
-
- {/* 권장사항 */}
-
-
💡 권장사항
-
- {cow.recommendations}
-
-
-
-
-
- {/* 하단 액션 버튼 */}
-
-
-
-
-
-
- ))}
-
-
-
-
-
- )
-}
diff --git a/frontend/src/components/charts/index.ts b/frontend/src/components/charts/index.ts
deleted file mode 100644
index 68e6687..0000000
--- a/frontend/src/components/charts/index.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-// ============================================
-// 차트 컴포넌트
-// ============================================
-export { MptGaugeBar } from './mpt-gauge-bar';
diff --git a/frontend/src/components/charts/mpt-gauge-bar.tsx b/frontend/src/components/charts/mpt-gauge-bar.tsx
deleted file mode 100644
index 72d5c14..0000000
--- a/frontend/src/components/charts/mpt-gauge-bar.tsx
+++ /dev/null
@@ -1,261 +0,0 @@
-'use client'
-
-/**
- * MPT 혈액대사검사 게이지/바 차트
- * 정상 범위 대비 현재 값의 위치를 시각적으로 표시
- */
-
-interface MptGaugeBarProps {
- name: string
- value: number
- unit: string
- lowerLimit: number | null
- upperLimit: number | null
- category: string
- regionAverage?: number // 보은군 평균 (선택)
-}
-
-export function MptGaugeBar({
- name,
- value,
- unit,
- lowerLimit,
- upperLimit,
- category,
- regionAverage
-}: MptGaugeBarProps) {
- // 상태 계산
- const getStatus = (): 'low' | 'normal' | 'high' => {
- if (upperLimit !== null && value > upperLimit) return 'high'
- if (lowerLimit !== null && value < lowerLimit) return 'low'
- return 'normal'
- }
-
- const status = getStatus()
-
- // 범위와 값 기반 시각화 계산
- const calculateVisualization = () => {
- if (lowerLimit === null || upperLimit === null) {
- // 범위 정보 없음 - 단순 값 표시
- return { position: 50, hasRange: false }
- }
-
- // 모든 값 수집 (현재값, 보은군평균, 정상범위)
- const allValues = [value, lowerLimit, upperLimit]
- if (regionAverage !== undefined) {
- allValues.push(regionAverage)
- }
-
- const dataMin = Math.min(...allValues)
- const dataMax = Math.max(...allValues)
- const dataRange = dataMax - dataMin
-
- // 데이터 범위의 20% 패딩 추가
- const padding = dataRange * 0.2
- const chartMin = dataMin - padding
- const chartMax = dataMax + padding
- const chartRange = chartMax - chartMin
-
- // 값의 위치 계산 (0-100%)
- let position = ((value - chartMin) / chartRange) * 100
- position = Math.max(0, Math.min(100, position)) // 0-100% 범위로 제한
-
- // 정상범위 시작/끝 위치
- const normalStart = ((lowerLimit - chartMin) / chartRange) * 100
- const normalEnd = ((upperLimit - chartMin) / chartRange) * 100
-
- return {
- position,
- normalStart,
- normalEnd,
- hasRange: true,
- chartMin,
- chartMax,
- chartRange,
- }
- }
-
- const viz = calculateVisualization()
-
- // 카테고리별 색상 (제거 - 사용 안 함)
-
- // 상태별 색상 (부드러운 톤)
- const getStatusColor = () => {
- switch (status) {
- case 'high': return {
- bg: 'bg-gradient-to-r from-red-400 to-red-500',
- text: 'text-red-700',
- badgeBg: 'bg-red-50',
- badgeBorder: 'border-red-200',
- barBg: 'linear-gradient(to right, #f87171, #ef4444)' // red-400 to red-500
- }
- case 'low': return {
- bg: 'bg-gradient-to-r from-blue-400 to-blue-500',
- text: 'text-blue-700',
- badgeBg: 'bg-blue-50',
- badgeBorder: 'border-blue-200',
- barBg: 'linear-gradient(to right, #60a5fa, #3b82f6)' // blue-400 to blue-500
- }
- case 'normal': return {
- bg: 'bg-gradient-to-r from-green-400 to-green-500',
- text: 'text-green-700',
- badgeBg: 'bg-green-50',
- badgeBorder: 'border-green-200',
- barBg: 'linear-gradient(to right, #4ade80, #22c55e)' // green-400 to green-500
- }
- }
- }
-
- const getStatusText = () => {
- switch (status) {
- case 'high': return '높음 ↑'
- case 'low': return '낮음 ↓'
- case 'normal': return '정상'
- }
- }
-
- const statusColors = getStatusColor()
-
- return (
-
- {/* 항목명과 현재값 */}
-
-
{name}
-
-
- {value.toFixed(1)}
-
- {unit}
-
-
-
- {/* 게이지 바 */}
- {viz.hasRange ? (
-
- {/* 현재값 및 보은군 평균 표시 (바 위) */}
-
- {/* 보은군 평균 표시 */}
- {regionAverage !== undefined && viz.hasRange && (() => {
- const regionPosition = ((regionAverage - viz.chartMin!) / viz.chartRange!) * 100;
- return (
-
-
- 보은군: {regionAverage.toFixed(1)}
-
- {/* 화살표 */}
-
-
- );
- })()}
-
- {/* 현재값 표시 */}
-
-
- {value.toFixed(1)}
- {regionAverage !== undefined && Math.abs(value - regionAverage) > 0.1 && (
-
- ({value > regionAverage ? '+' : ''}{(value - regionAverage).toFixed(1)})
-
- )}
-
- {/* 화살표 */}
-
-
-
-
-
- {/* 정상 범위 영역 */}
-
-
- {/* 보은군 평균 인디케이터 (회색 실선) */}
- {regionAverage !== undefined && viz.hasRange && (() => {
- const regionPosition = ((regionAverage - viz.chartMin!) / viz.chartRange!) * 100;
- return (
-
- );
- })()}
-
- {/* 현재 값 인디케이터 */}
-
-
-
- {/* 정상 범위 수치 표기 (바 양옆) */}
-
-
- 최소
- {lowerLimit}
-
-
-
- {getStatusText()}
-
-
-
- 최대
- {upperLimit}
-
-
-
- ) : (
-
-
-
-
- {getStatusText()}
-
-
-
- )}
-
- )
-}
diff --git a/frontend/src/components/genome/CowCompareModal.tsx b/frontend/src/components/genome/CowCompareModal.tsx
deleted file mode 100644
index 89be351..0000000
--- a/frontend/src/components/genome/CowCompareModal.tsx
+++ /dev/null
@@ -1,235 +0,0 @@
-'use client'
-
-import { Button } from "@/components/ui/button"
-import { Badge } from "@/components/ui/badge"
-import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
-import { User, CheckCircle2, BarChart3 } from "lucide-react"
-import { CowDetail } from "@/types/cow.types"
-import { GenomeTrait as GenomeTraitType } from "@/types/genome.types"
-import { ResponsiveContainer, RadarChart, PolarGrid, PolarAngleAxis, PolarRadiusAxis, Radar, Legend, Tooltip as RechartsTooltip } from 'recharts'
-
-interface CowCompareModalProps {
- isOpen: boolean
- onClose: () => void
- compareCowsData: { cow: CowDetail; genome: GenomeTraitType[] }[]
- transformGenomeData: (genomeData: GenomeTraitType[]) => any[]
- CATEGORIES: string[]
- TRAIT_COLORS: string[]
-}
-
-export function CowCompareModal({
- isOpen,
- onClose,
- compareCowsData,
- transformGenomeData,
- CATEGORIES,
- TRAIT_COLORS
-}: CowCompareModalProps) {
- if (!isOpen || compareCowsData.length === 0) return null
-
- return (
-
-
- {/* 헤더 */}
-
-
-
-
- 개체 유전체 비교
-
-
- {compareCowsData.length}개 개체 비교
-
-
-
-
-
- {/* 비교 내용 */}
-
- {/* 개체 카드들 */}
-
- {compareCowsData.map((cowData, idx) => {
- const genomeTraits = transformGenomeData(cowData.genome)
- const avgBreedVal = genomeTraits.length > 0
- ? genomeTraits.reduce((sum: number, t: any) => sum + t.breedVal, 0) / genomeTraits.length
- : 0
- const avgPercentile = genomeTraits.length > 0
- ? genomeTraits.reduce((sum: number, t: any) => sum + t.percentile, 0) / genomeTraits.length
- : 0
-
- return (
-
-
-
-
- {cowData.cow.cowId || cowData.cow.pkCowNo}
- {cowData.cow.cowId && (
- {cowData.cow.cowId}
- )}
-
- {idx === 0 && (
-
현재 개체
- )}
-
-
-
-
-
-
종합 육종가
-
- {avgBreedVal > 0 ? '+' : ''}{avgBreedVal.toFixed(2)}σ
-
-
- 상위 {(100 - avgPercentile).toFixed(1)}%
-
-
-
-
-
- 생년월일
-
- {cowData.cow.cowBirthDt
- ? new Date(cowData.cow.cowBirthDt).toLocaleDateString('ko-KR')
- : 'N/A'}
-
-
-
- 나이
-
- {cowData.cow.age ? `${cowData.cow.age}세` : 'N/A'}
-
-
-
- 평가 형질 수
- {genomeTraits.length}개
-
-
-
-
-
- )
- })}
-
-
- {/* 카테고리별 비교 차트 */}
-
-
- 카테고리별 육종가 비교
- 각 개체의 카테고리별 평균 표준화육종가
-
-
-
-
-
-
-
-
-
-
- {compareCowsData.map((cowData, idx) => {
- const genomeTraits = transformGenomeData(cowData.genome)
- const categoryData = CATEGORIES.map(cat => {
- const traits = genomeTraits.filter((t: any) => t.category === cat)
- const avgBreedVal = traits.length > 0
- ? traits.reduce((sum: number, t: any) => sum + t.breedVal, 0) / traits.length
- : 0
- return {
- category: cat,
- value: avgBreedVal
- }
- })
-
- const cowLabel = cowData.cow.cowId || String(cowData.cow.pkCowNo)
- return (
-
- )
- })}
-
-
-
-
-
-
- {/* 형질별 비교 테이블 */}
-
-
- 주요 형질 비교 (Top 10)
-
-
-
-
-
-
- | 형질명 |
- {compareCowsData.map((cowData, idx) => {
- const cowLabel = cowData.cow.cowId || String(cowData.cow.pkCowNo)
- return (
-
- {idx === 0 ? `${cowLabel}\n(현재)` : cowLabel}
- |
- )
- })}
-
-
-
- {transformGenomeData(compareCowsData[0].genome).slice(0, 10).map((trait: any) => (
-
- |
- {trait.name}
- |
- {compareCowsData.map((cowData) => {
- const genomeTraits = transformGenomeData(cowData.genome)
- const matchTrait = genomeTraits.find((t: any) => t.name === trait.name)
- return (
-
- {matchTrait ? (
-
- 0 ? 'text-primary' : 'text-muted-foreground'}`}>
- {matchTrait.breedVal > 0 ? '+' : ''}{matchTrait.breedVal.toFixed(2)}σ
-
-
- {matchTrait.percentile.toFixed(1)}%
-
-
- ) : (
- N/A
- )}
- |
- )
- })}
-
- ))}
-
-
-
-
-
-
-
- {/* 푸터 */}
-
-
-
-
-
- )
-}
diff --git a/frontend/src/components/genome/CowSelectSheet.tsx b/frontend/src/components/genome/CowSelectSheet.tsx
deleted file mode 100644
index b4d4c77..0000000
--- a/frontend/src/components/genome/CowSelectSheet.tsx
+++ /dev/null
@@ -1,159 +0,0 @@
-'use client'
-
-import { Button } from "@/components/ui/button"
-import { Badge } from "@/components/ui/badge"
-import { User, CheckCircle2 } from "lucide-react"
-import { CowDetail } from "@/types/cow.types"
-
-interface CowSelectSheetProps {
- isOpen: boolean
- onClose: () => void
- farmCows: CowDetail[]
- selectedCowsForCompare: number[]
- toggleCowForCompare: (cowNo: number) => void
- onCompare: () => void
- onClearSelection: () => void
-}
-
-export function CowSelectSheet({
- isOpen,
- onClose,
- farmCows,
- selectedCowsForCompare,
- toggleCowForCompare,
- onCompare,
- onClearSelection
-}: CowSelectSheetProps) {
- if (!isOpen) return null
-
- return (
-
-
- {/* 헤더 */}
-
-
-
-
- 농장 내 개체 비교
-
-
- 비교할 개체를 선택하세요 ({selectedCowsForCompare.length}/{farmCows.length})
-
-
-
-
-
- {/* 개체 목록 */}
-
- {farmCows.length === 0 ? (
-
- ) : (
-
- {farmCows.map((farmCow) => {
- const isSelected = selectedCowsForCompare.includes(farmCow.pkCowNo)
- return (
-
toggleCowForCompare(farmCow.pkCowNo)}
- className={`p-4 rounded-lg border-2 cursor-pointer transition-all ${
- isSelected
- ? 'border-primary bg-primary/5'
- : 'border-border hover:border-primary/50 hover:bg-muted/50'
- }`}
- >
-
- {/* 체크박스 */}
-
-
- {isSelected && (
-
- )}
-
-
-
- {/* 개체 정보 */}
-
-
-
-
{farmCow.cowId || farmCow.pkCowNo}
- {farmCow.cowId && (
-
{farmCow.cowId}
- )}
-
-
-
- {/* 상세 정보 */}
-
-
-
생년월일
-
- {farmCow.cowBirthDt
- ? new Date(farmCow.cowBirthDt).toLocaleDateString('ko-KR', {
- year: '2-digit',
- month: '2-digit',
- day: '2-digit'
- })
- : 'N/A'}
-
-
-
-
나이
-
- {farmCow.age ? `${farmCow.age}세` : 'N/A'}
-
-
-
-
성별
-
- {farmCow.cowSex === 'F' ? '암' : farmCow.cowSex === 'M' ? '수' : 'N/A'}
-
-
-
-
-
-
- )
- })}
-
- )}
-
-
- {/* 푸터 */}
-
-
- {selectedCowsForCompare.length}개 개체 선택됨
-
-
-
-
-
-
-
-
- )
-}
diff --git a/frontend/src/components/genome/gene-filter-modal.tsx b/frontend/src/components/genome/gene-filter-modal.tsx
deleted file mode 100644
index d7460c5..0000000
--- a/frontend/src/components/genome/gene-filter-modal.tsx
+++ /dev/null
@@ -1,301 +0,0 @@
-'use client'
-
-import { useState, useMemo, useEffect } from "react"
-import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"
-import { Button } from "@/components/ui/button"
-import { Input } from "@/components/ui/input"
-import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
-import { Checkbox } from "@/components/ui/checkbox"
-import { Label } from "@/components/ui/label"
-import { ScrollArea } from "@/components/ui/scroll-area"
-import { Search, Loader2 } from "lucide-react"
-import { geneApi } from "@/lib/api/gene.api"
-
-/**
- * 마커 데이터 타입 (API에서 받아오는 형식)
- */
-interface MarkerData {
- pkMarkerNo: number
- markerNm: string
- markerDesc: string
- markerTypeCd: string
- relatedTrait: string
- favorableAllele: string
- useYn: string
- markerTypeInfo?: {
- pkTypeCd: string
- typeNm: string
- typeDesc: string
- }
-}
-
-/**
- * 유전자 필터에서 사용할 간소화된 타입
- */
-interface GeneOption {
- name: string
- description: string
- type: 'QTY' | 'QLT'
- relatedTrait: string
-}
-
-interface GeneFilterModalProps {
- open: boolean
- onOpenChange: (open: boolean) => void
- selectedGenes: string[]
- onConfirm: (genes: string[]) => void
-}
-
-export function GeneFilterModal({ open, onOpenChange, selectedGenes, onConfirm }: GeneFilterModalProps) {
- const [tempSelectedGenes, setTempSelectedGenes] = useState(selectedGenes)
- const [searchQuery, setSearchQuery] = useState('')
- const [sortBy, setSortBy] = useState<'name' | 'type'>('name')
- const [allMarkers, setAllMarkers] = useState([])
- const [loading, setLoading] = useState(false)
- const [error, setError] = useState(null)
-
- // API에서 마커 목록 가져오기
- useEffect(() => {
- if (open) {
- fetchMarkers()
- }
- }, [open])
-
- // TODO: 백엔드 /gene/markers API 구현 후 활성화
- const fetchMarkers = async () => {
- // try {
- // setLoading(true)
- // setError(null)
- // const markers = await geneApi.getAllMarkers() as unknown as MarkerData[]
-
- // // API 데이터를 GeneOption 형식으로 변환
- // const geneOptions: GeneOption[] = markers.map(marker => ({
- // name: marker.markerNm,
- // description: marker.relatedTrait || marker.markerDesc || '',
- // type: marker.markerTypeCd as 'QTY' | 'QLT',
- // relatedTrait: marker.relatedTrait || ''
- // }))
-
- // setAllMarkers(geneOptions)
- // } catch (err) {
- // console.error('Failed to fetch markers:', err)
- // setError('유전자 목록을 불러오는데 실패했습니다.')
- // } finally {
- // setLoading(false)
- // }
- }
-
- // 육량형/육질형 필터링
- const quantityGenes = useMemo(() => {
- return allMarkers.filter(g => g.type === 'QTY').sort((a, b) => a.name.localeCompare(b.name))
- }, [allMarkers])
-
- const qualityGenes = useMemo(() => {
- return allMarkers.filter(g => g.type === 'QLT').sort((a, b) => a.name.localeCompare(b.name))
- }, [allMarkers])
-
- // 전체 유전자 목록 (정렬)
- const allGenes = useMemo(() => {
- return [...allMarkers].sort((a, b) => {
- if (sortBy === 'type') {
- if (a.type !== b.type) {
- return a.type.localeCompare(b.type)
- }
- }
- return a.name.localeCompare(b.name)
- })
- }, [allMarkers, sortBy])
-
- const filteredGenes = useMemo(() => {
- if (!searchQuery) return allGenes
- return allGenes.filter(gene =>
- gene.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
- gene.description.toLowerCase().includes(searchQuery.toLowerCase())
- )
- }, [allGenes, searchQuery])
-
- const toggleGene = (geneName: string) => {
- setTempSelectedGenes(prev =>
- prev.includes(geneName)
- ? prev.filter(g => g !== geneName)
- : [...prev, geneName]
- )
- }
-
- const handleConfirm = () => {
- onConfirm(tempSelectedGenes)
- onOpenChange(false)
- }
-
- const handleCancel = () => {
- setTempSelectedGenes(selectedGenes)
- onOpenChange(false)
- }
-
- return (
-
- )
-}
diff --git a/frontend/src/components/genome/gene-search-modal.tsx b/frontend/src/components/genome/gene-search-modal.tsx
deleted file mode 100644
index 206d052..0000000
--- a/frontend/src/components/genome/gene-search-modal.tsx
+++ /dev/null
@@ -1,246 +0,0 @@
-'use client'
-
-import { useState, useEffect } from "react"
-import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog"
-import { Input } from "@/components/ui/input"
-import { Badge } from "@/components/ui/badge"
-import { Button } from "@/components/ui/button"
-import { ScrollArea } from "@/components/ui/scroll-area"
-import { Search, X, Filter, Sparkles } from "lucide-react"
-import { geneApi } from "@/lib/api/gene.api"
-import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"
-
-interface GeneSearchDrawerProps {
- open: boolean
- onOpenChange: (open: boolean) => void
- selectedGenes: string[]
- onGenesChange: (genes: string[]) => void
-}
-
-export function GeneSearchModal({ open, onOpenChange, selectedGenes, onGenesChange }: GeneSearchDrawerProps) {
- const [allGenes, setAllGenes] = useState([])
- const [loading, setLoading] = useState(false)
- const [searchQuery, setSearchQuery] = useState("")
- const [filterType, setFilterType] = useState<'ALL' | 'QTY' | 'QLT'>('ALL')
-
- // 모달 열릴 때 전체 유전자 로드
- useEffect(() => {
- if (open) {
- loadAllGenes()
- }
- }, [open])
-
- // TODO: 백엔드 /gene/markers API 구현 후 활성화
- const loadAllGenes = async () => {
- // try {
- // setLoading(true)
- // const genes = await geneApi.getAllMarkers()
- // setAllGenes(genes)
- // } catch {
- // // 유전자 로드 실패 시 빈 배열 유지
- // } finally {
- // setLoading(false)
- // }
- }
-
- // 검색 및 필터링
- const filteredGenes = allGenes.filter((gene) => {
- // 타입 필터
- if (filterType !== 'ALL' && gene.markerTypeCd !== filterType) {
- return false
- }
-
- // 검색어 필터
- if (searchQuery) {
- const query = searchQuery.toLowerCase()
- return (
- gene.markerNm.toLowerCase().includes(query) ||
- gene.markerDesc?.toLowerCase().includes(query) ||
- gene.relatedTrait?.toLowerCase().includes(query)
- )
- }
-
- return true
- })
-
- const toggleGene = (markerNm: string) => {
- if (selectedGenes.includes(markerNm)) {
- onGenesChange(selectedGenes.filter(g => g !== markerNm))
- } else {
- onGenesChange([...selectedGenes, markerNm])
- }
- }
-
- const selectAllFiltered = () => {
- const newGenes = [...selectedGenes]
- filteredGenes.forEach(gene => {
- if (!newGenes.includes(gene.markerNm)) {
- newGenes.push(gene.markerNm)
- }
- })
- onGenesChange(newGenes)
- }
-
- const clearAll = () => {
- onGenesChange([])
- }
-
- return (
-
- )
-}
diff --git a/frontend/src/components/genome/genome-distribution-donut.tsx b/frontend/src/components/genome/genome-distribution-donut.tsx
deleted file mode 100644
index bd8fe4b..0000000
--- a/frontend/src/components/genome/genome-distribution-donut.tsx
+++ /dev/null
@@ -1,181 +0,0 @@
-'use client'
-
-import { PieChart as PieChartIcon } from "lucide-react"
-import { useEffect, useState } from "react"
-import apiClient from "@/lib/api-client"
-import { ResponsiveContainer, PieChart, Pie, Cell, Tooltip } from 'recharts'
-
-interface DistributionData {
- name: string
- value: number
- color: string
- range: string
- description: string
-}
-
-interface GenomeDistributionDonutProps {
- farmNo: number | null
-}
-
-export function GenomeDistributionDonut({ farmNo }: GenomeDistributionDonutProps) {
- const [data, setData] = useState([])
- const [totalCount, setTotalCount] = useState(0)
- const [loading, setLoading] = useState(true)
-
- useEffect(() => {
- const fetchData = async () => {
- if (!farmNo) {
- setLoading(false)
- return
- }
-
- try {
- const response = await apiClient.post('/cow/ranking', {
- filterOptions: { farmNo },
- rankingOptions: {
- criteriaType: 'GENOME',
- traitConditions: [
- { traitNm: '도체중', weight: 0.25 },
- { traitNm: '근내지방도', weight: 0.25 },
- { traitNm: '등심단면적', weight: 0.25 },
- { traitNm: '등지방두께', weight: 0.25 },
- ]
- }
- })
-
- const result = response.data || response
- const items = result.items || []
- setTotalCount(items.length)
-
- const distribution = {
- top: 0, // 0σ 이상
- middle: 0, // -1.0σ ~ 0σ
- bottom: 0 // -1.0σ 이하
- }
-
- items.forEach((item: any) => {
- const score = item.sortValue || 0
- if (score >= 0) distribution.top++
- else if (score >= -1.0) distribution.middle++
- else distribution.bottom++
- })
-
- setData([
- { name: '우수', value: distribution.top, color: '#10b981', range: '0σ 이상', description: '평균보다 우수해요' },
- { name: '양호', value: distribution.middle, color: '#1482B0', range: '-1.0σ ~ 0σ', description: '평균 수준이에요' },
- { name: '개선필요', value: distribution.bottom, color: '#94a3b8', range: '-1.0σ 이하', description: '조금 더 신경써요' },
- ].filter(d => d.value > 0))
-
- } catch (error) {
- console.error('분포 데이터 로드 실패:', error)
- } finally {
- setLoading(false)
- }
- }
-
- fetchData()
- }, [farmNo])
-
- if (loading) {
- return (
-
- )
- }
-
- return (
-
- {/* 헤더 */}
-
-
-
-
-
-
우리 소들의 등급
-
총 {totalCount}두 분포
-
-
-
{totalCount}두
-
-
-
- {/* 차트 */}
-
-
-
-
-
-
- {data.map((entry, index) => (
- |
- ))}
-
- {
- if (active && payload && payload.length) {
- const item = payload[0].payload
- return (
-
-
{item.name}
-
{item.description}
-
-
{item.value}두 ({Math.round(item.value / totalCount * 100)}%)
-
{item.range}
-
-
- )
- }
- return null
- }}
- />
-
-
- {/* 중앙 텍스트 */}
-
- {totalCount}
- 전체
-
-
-
- {/* 범례 */}
-
-
- {data.map((item, idx) => (
-
-
-
-
- {item.name}
- {item.description}
-
-
-
-
{item.value}두
-
{Math.round(item.value / totalCount * 100)}%
-
-
- ))}
-
-
- σ(시그마)는 유전능력 수준을 나타내요
- 0보다 클수록 우수해요
-
-
-
-
-
- )
-}
diff --git a/frontend/src/components/genome/genome-radar-chart.tsx b/frontend/src/components/genome/genome-radar-chart.tsx
deleted file mode 100644
index 10b2836..0000000
--- a/frontend/src/components/genome/genome-radar-chart.tsx
+++ /dev/null
@@ -1,250 +0,0 @@
-'use client'
-
-import { Target } from "lucide-react"
-import { useEffect, useState } from "react"
-import apiClient from "@/lib/api-client"
-import { ResponsiveContainer, RadarChart, PolarGrid, PolarAngleAxis, PolarRadiusAxis, Radar, Tooltip } from 'recharts'
-
-interface TraitScore {
- trait: string
- diff: number
- myFarm: number
- region: number
-}
-
-interface GenomeRadarChartProps {
- farmNo: number | null
-}
-
-export function GenomeRadarChart({ farmNo }: GenomeRadarChartProps) {
- const [data, setData] = useState([])
- const [loading, setLoading] = useState(true)
-
- useEffect(() => {
- const fetchData = async () => {
- if (!farmNo) {
- setLoading(false)
- return
- }
-
- try {
- const traits = [
- { name: '도체중', key: '도체중' },
- { name: '근내지방', key: '근내지방도' },
- { name: '등심단면적', key: '등심단면적' },
- { name: '등지방', key: '등지방두께' },
- { name: '12개월체중', key: '12개월령체중' },
- ]
-
- const results: TraitScore[] = []
-
- for (const trait of traits) {
- try {
- const farmResponse = await apiClient.post('/cow/ranking', {
- filterOptions: { 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) => {
- const traitDetail = item.details?.find((d: any) => d.code === trait.key)
- return traitDetail?.value ?? item.sortValue ?? 0
- }) || []
- const farmAvgScore = farmScores.length > 0
- ? farmScores.reduce((sum: number, s: number) => sum + s, 0) / farmScores.length
- : 0
-
- const globalScores = globalResult.items?.map((item: any) => {
- const traitDetail = item.details?.find((d: any) => d.code === trait.key)
- return traitDetail?.value ?? item.sortValue ?? 0
- }) || []
- const regionAvgScore = globalScores.length > 0
- ? globalScores.reduce((sum: number, s: number) => sum + s, 0) / globalScores.length
- : 0
-
- const diff = farmAvgScore - regionAvgScore
-
- results.push({
- trait: trait.name,
- diff: parseFloat(diff.toFixed(2)),
- myFarm: parseFloat(farmAvgScore.toFixed(2)),
- region: parseFloat(regionAvgScore.toFixed(2))
- })
- } catch (error) {
- console.error(`형질 ${trait.name} 로드 실패:`, error)
- }
- }
-
- setData(results)
- } catch (error) {
- console.error('레이더 차트 데이터 로드 실패:', error)
- } finally {
- setLoading(false)
- }
- }
-
- fetchData()
- }, [farmNo])
-
- if (loading) {
- return (
-
- )
- }
-
- const validDiffs = data.filter(d => !isNaN(d.diff))
- const avgDiff = validDiffs.length > 0
- ? validDiffs.reduce((sum, d) => sum + d.diff, 0) / validDiffs.length
- : 0
-
- return (
-
- {/* 헤더 */}
-
-
-
-
-
-
-
-
카테고리별 결과
-
주요 형질 평균 비교
-
-
-
= 0
- ? 'bg-emerald-50 text-emerald-700 border-emerald-200'
- : 'bg-red-50 text-red-600 border-red-200'
- }`}>
- 평균 {avgDiff > 0 ? '+' : ''}{avgDiff.toFixed(2)}σ
-
-
-
-
- {/* 차트 */}
-
-
-
-
-
-
-
-
-
-
-
-
-
- 0}
- stroke="#94a3b8"
- fill="none"
- strokeWidth={2}
- strokeDasharray="5 3"
- strokeOpacity={0.6}
- />
-
- {
- if (active && payload && payload.length) {
- const item = payload[0]?.payload
- const diff = item?.diff ?? 0
- const myFarm = item?.myFarm ?? 0
- const region = item?.region ?? 0
-
- return (
-
-
{item?.trait}
-
-
내농장: {myFarm > 0 ? '+' : ''}{myFarm}σ
-
보은군: {region > 0 ? '+' : ''}{region}σ
-
-
= 0.3 ? 'text-emerald-400' :
- diff <= -0.3 ? 'text-amber-400' :
- 'text-slate-300'
- }`}>
- {diff >= 0.3 ? '▲' : diff <= -0.3 ? '▼' : '='} {diff > 0 ? '+' : ''}{diff.toFixed(2)}σ
-
-
- )
- }
- return null
- }}
- />
-
-
-
-
- {/* 범례 */}
-
-
- {/* 형질별 수치 */}
-
- {data.map((item, idx) => (
-
-
{item.trait}
-
= 0.3 ? 'text-emerald-600' :
- item.diff <= -0.3 ? 'text-amber-600' :
- 'text-slate-700'
- }`}>
- {item.diff > 0 ? '+' : ''}{item.diff.toFixed(1)}
-
-
- ))}
-
-
-
- )
-}
diff --git a/frontend/src/components/genome/genome-strengths-weaknesses.tsx b/frontend/src/components/genome/genome-strengths-weaknesses.tsx
deleted file mode 100644
index ab99c48..0000000
--- a/frontend/src/components/genome/genome-strengths-weaknesses.tsx
+++ /dev/null
@@ -1,208 +0,0 @@
-'use client'
-
-import { ArrowUpRight, ArrowDownRight, TrendingUp, TrendingDown } from "lucide-react"
-import { useEffect, useState } from "react"
-import apiClient from "@/lib/api-client"
-
-interface GenomeData {
- trait: string
- score: number
- type: string
-}
-
-interface GenomeStrengthsWeaknessesProps {
- farmNo?: number | null
-}
-
-export function GenomeStrengthsWeaknesses({ farmNo }: GenomeStrengthsWeaknessesProps) {
- const [allMetrics, setAllMetrics] = useState([])
- const [loading, setLoading] = useState(true)
-
- useEffect(() => {
- const fetchTraitScores = async () => {
- if (!farmNo) {
- setLoading(false)
- return
- }
-
- try {
- const traits = [
- { name: '도체중', key: '도체중' },
- { name: '근내지방도', key: '근내지방도' },
- { name: '등심단면적', key: '등심단면적' },
- { name: '등지방두께', key: '등지방두께' },
- { name: '12개월령체중', key: '12개월령체중' },
- { name: '체고', key: '체고' },
- ]
-
- const traitScores: GenomeData[] = []
-
- for (const trait of traits) {
- try {
- const rankingRequest = {
- filterOptions: { farmNo: farmNo },
- rankingOptions: {
- criteriaType: 'GENOME',
- traitConditions: [{ traitNm: trait.key, weight: 1.0 }]
- }
- }
-
- const response = await apiClient.post('/cow/ranking', rankingRequest)
- const rankingResult = response.data || response
-
- const scores = rankingResult.items?.map((item: any) => item.sortValue) || []
- const avgScore = scores.length > 0
- ? scores.reduce((sum: number, score: number) => sum + score, 0) / scores.length
- : 0
-
- traitScores.push({
- trait: trait.name,
- score: parseFloat(avgScore.toFixed(2)),
- type: '유전체'
- })
- } catch (error) {
- console.error(`[강점/약점] 형질 ${trait.name} 데이터 로드 실패:`, error)
- }
- }
-
- setAllMetrics(traitScores)
- } catch (error) {
- console.error('형질 점수 로드 실패:', error)
- setAllMetrics([])
- } finally {
- setLoading(false)
- }
- }
-
- fetchTraitScores()
- }, [farmNo])
-
- const strengths = [...allMetrics].sort((a, b) => b.score - a.score).slice(0, 3)
- const weaknesses = [...allMetrics].sort((a, b) => a.score - b.score).slice(0, 3)
-
- if (loading) {
- return (
-
- {[1, 2].map((i) => (
-
- ))}
-
- )
- }
-
- return (
-
- {/* 강점 */}
-
window.location.href = '/dashboard/strengths-weaknesses'}
- >
-
-
-
-
-
-
-
-
이 부분이 강해요
-
보은군보다 우수한 형질
-
-
-
TOP 3
-
-
-
- {strengths.length === 0 ? (
-
- 데이터가 없습니다.
-
- ) : (
-
- {strengths.map((item, idx) => (
-
-
-
- {idx + 1}
-
- {item.trait}
-
-
-
- {item.score > 0 ? '+' : ''}{item.score}
-
- σ
-
-
- ))}
-
- )}
-
-
-
- {/* 약점 */}
-
window.location.href = '/dashboard/strengths-weaknesses'}
- >
-
-
-
-
-
-
-
-
더 좋아질 수 있어요
-
개선하면 좋을 형질
-
-
-
BOTTOM 3
-
-
-
- {weaknesses.length === 0 ? (
-
- 데이터가 없습니다.
-
- ) : (
-
- {weaknesses.map((item, idx) => (
-
-
-
- {idx + 1}
-
- {item.trait}
-
-
-
- {item.score > 0 ? '+' : ''}{item.score}
-
- σ
-
-
- ))}
-
- )}
-
-
-
- )
-}
diff --git a/frontend/src/components/genome/genome-traits-table.tsx b/frontend/src/components/genome/genome-traits-table.tsx
deleted file mode 100644
index dd52eb3..0000000
--- a/frontend/src/components/genome/genome-traits-table.tsx
+++ /dev/null
@@ -1,199 +0,0 @@
-'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([])
- 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 = {
- '12개월령체중': '12개월령체중',
- '등심단면적': '등심단면적',
- '등지방두께': '등지방두께',
- '근내지방도': '근내지방도',
- '도체중': '도체중'
- }
- return shortNames[name] || name
- }
-
- if (loading) {
- return (
-
- )
- }
-
- return (
-
- {/* 헤더 */}
-
-
-
-
-
-
-
-
형질별 점수
-
보은군과 비교한 수치에요
-
-
-
5개 형질
-
-
-
- {/* 콘텐츠 */}
-
- {traitData.length === 0 ? (
-
- 데이터가 없습니다.
-
- ) : (
-
- {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 (
-
- {/* 형질명 + 차이 */}
-
-
- {getTraitShortName(item.trait)}
-
- = 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)}σ
-
-
-
- {/* 비교 바 */}
-
- {/* 보은군 바 */}
-
- {/* 내농장 바 */}
-
-
-
- {/* 라벨 */}
-
-
-
-
- 내농장 {item.myFarm > 0 ? '+' : ''}{item.myFarm}σ
-
-
-
-
-
- 보은군 {item.regional > 0 ? '+' : ''}{item.regional}σ
-
-
-
-
- )
- })}
-
- )}
-
-
- )
-}
diff --git a/frontend/src/types/auth.types.ts b/frontend/src/types/auth.types.ts
index dedae0d..9aab6ac 100644
--- a/frontend/src/types/auth.types.ts
+++ b/frontend/src/types/auth.types.ts
@@ -1,122 +1,143 @@
/**
- * 인증 관련 타입 정의
- * 백엔드 UserModel Entity 및 Auth DTOs 기준으로 작성
+ * ========================================
+ * 인증(Auth) 관련 타입 정의
+ * ========================================
+ *
+ * @description
+ * - 백엔드 UserModel Entity 및 Auth DTOs와 1:1 매핑
+ * - 회원가입, 로그인, 프로필 관리 포함
*/
+// ========================================
+// 1. 사용자 기본 정보
+// ========================================
+
/**
* 사용자 정보
- * 백엔드 UserModel Entity와 일치
+ *
+ * @backend UserModel Entity
+ * @description 시스템 사용자 기본 정보
+ * @usage
+ * - 로그인 후 세션 정보
+ * - 사용자 프로필 조회
*/
export interface UserDto {
- pkUserNo: number; // 내부 PK (자동증가)
- userId: string; // 로그인 ID
- userName: string; // 이름
- userPhone?: string; // 핸드폰번호
- userEmail?: string; // 이메일 (인증용)
- userRole: 'USER' | 'ADMIN'; // 권한 (USER: 일반, ADMIN: 관리자)
- delDt?: string; // 삭제일시 (Soft Delete)
+ // === 기본 키 ===
+ pkUserNo: number // 사용자 내부 번호 (Primary Key, 자동증가)
+ userId: string // 로그인 ID (아이디)
+
+ // === 사용자 정보 ===
+ userName: string // 이름
+ userPhone?: string // 핸드폰 번호
+ userEmail?: string // 이메일 (인증용)
+
+ // === 권한 ===
+ userRole: 'USER' | 'ADMIN' // 권한 (USER: 일반 사용자, ADMIN: 관리자)
+
+ // === 시스템 필드 ===
+ delDt?: string // 삭제일시 (Soft Delete)
}
+// ========================================
+// 2. 회원가입 관련 타입
+// ========================================
+
/**
- * 회원가입 DTO
- * 백엔드 SignupDto와 일치
+ * 회원가입 요청
+ *
+ * @backend SignupDto
+ * @description 신규 사용자 등록 정보
+ * @usage POST /auth/signup 요청 바디
*/
export interface SignupDto {
- userSe: 'FARM' | 'CNSLT' | 'ORGAN'; // 사용자 구분 (FARM/CNSLT/ORGAN)
- userInstName?: string; // 농장명/기관명
- userId: string; // 사용자 ID (4자 이상)
- userPassword: string; // 비밀번호 (6자 이상)
- userName: string; // 이름
- userPhone: string; // 휴대폰 번호
- userBirth?: string; // 생년월일
- userEmail: string; // 이메일
- userAddress?: string; // 주소
- userBizNo?: string; // 사업자등록번호
+ // === 사용자 구분 ===
+ userSe: 'FARM' | 'CNSLT' | 'ORGAN' // 구분 (농가/컨설턴트/기관)
+ userInstName?: string // 농장명 또는 기관명
+
+ // === 계정 정보 ===
+ userId: string // 사용자 ID (4자 이상)
+ userPassword: string // 비밀번호 (6자 이상)
+
+ // === 개인 정보 ===
+ userName: string // 이름
+ userPhone: string // 휴대폰 번호
+ userEmail: string // 이메일
+ userBirth?: string // 생년월일
+ userAddress?: string // 주소
+
+ // === 사업자 정보 ===
+ userBizNo?: string // 사업자등록번호
}
/**
- * 로그인 DTO
- * 백엔드 LoginDto와 일치
- */
-export interface LoginDto {
- userId: string; // 사용자 ID (로그인 ID)
- userPassword: string; // 비밀번호
-}
-
-/**
- * 로그인 응답 DTO
- * 백엔드 LoginResponseDto와 일치
- */
-export interface LoginResponseDto {
- message: string;
- accessToken?: string;
- user: {
- pkUserNo: number;
- userId: string;
- userName: string;
- userEmail?: string;
- userRole: 'USER' | 'ADMIN';
- };
-}
-
-/**
- * 회원가입 응답 DTO
- * 백엔드 SignupResponseDto와 일치
- */
-export interface SignupResponseDto {
- message: string;
- redirectUrl: string;
- userId?: string;
- userNo?: number;
-}
-
-/**
- * 사용자 프로필 DTO (프론트엔드 확장)
- */
-export interface UserProfileDto extends UserDto {
- farmCount?: number; // 보유 농장 수
- cowCount?: number; // 보유 개체 수
-}
-
-/**
- * 프로필 수정 DTO
- */
-export interface UpdateProfileDto {
- userName?: string; // 이름 수정
- userPhone?: string; // 전화번호 수정
- userEmail?: string; // 이메일 수정
-}
-
-/**
- * 회원가입 폼 데이터 (클라이언트 전용)
+ * 회원가입 폼 데이터
+ *
+ * @description
+ * - 클라이언트 전용 (백엔드 전송 전 변환 필요)
+ * - 이메일 분리 입력 등 UI 편의 필드 포함
+ *
+ * @usage 회원가입 페이지 폼
*/
export interface SignupFormData {
- userSe: string;
- userId: string;
- userPassword: string;
- confirmPassword: string;
- userName: string;
- userPhone: string;
- userEmail: string;
- emailId: string;
- emailDomain: string;
- customDomain?: string;
- userInstName?: string;
- userBirth?: string;
- userAddress?: string;
- userBizNo?: string;
+ userSe: string // 사용자 구분
+ userId: string // 사용자 ID
+ userPassword: string // 비밀번호
+ confirmPassword: string // 비밀번호 확인
+ userName: string // 이름
+ userPhone: string // 휴대폰 번호
+ userEmail: string // 이메일 전체
+ emailId: string // 이메일 ID 부분
+ emailDomain: string // 이메일 도메인 부분
+ customDomain?: string // 직접 입력 도메인
+ userInstName?: string // 농장명/기관명
+ userBirth?: string // 생년월일
+ userAddress?: string // 주소
+ userBizNo?: string // 사업자등록번호
+}
+
+// ========================================
+// 3. 로그인 관련 타입
+// ========================================
+
+/**
+ * 로그인 요청
+ *
+ * @backend LoginDto
+ * @description 로그인 인증 정보
+ * @usage POST /auth/login 요청 바디
+ */
+export interface LoginDto {
+ userId: string // 사용자 ID
+ userPassword: string // 비밀번호
}
/**
- * 인증 응답 DTO (통합)
- * 로그인/회원가입 응답에 사용
+ * 인증 응답 (통합)
+ *
+ * @description
+ * - 로그인/회원가입 공통 응답 형식
+ * - 프론트엔드에서 확장하여 사용
*/
export interface AuthResponseDto {
- message?: string;
- accessToken: string;
- user: UserDto;
+ message?: string // 결과 메시지
+ accessToken: string // JWT 액세스 토큰
+ user: UserDto // 사용자 정보
}
-// 타입 alias (호환성)
-export type User = UserDto;
-export type AuthResponse = LoginResponseDto;
+// ========================================
+// 4. 프로필 관련 타입
+// ========================================
+
+/**
+ * 사용자 프로필
+ *
+ * @description
+ * - UserDto를 확장하여 통계 정보 추가
+ * - 프론트엔드에서 확장
+ *
+ * @usage 마이페이지 프로필 표시
+ */
+export interface UserProfileDto extends UserDto {
+ farmCount?: number // 보유 농장 수
+ cowCount?: number // 보유 개체 수
+}
diff --git a/frontend/src/types/cow.types.ts b/frontend/src/types/cow.types.ts
index 5641a13..52c272f 100644
--- a/frontend/src/types/cow.types.ts
+++ b/frontend/src/types/cow.types.ts
@@ -1,134 +1,108 @@
/**
+ * ========================================
* 개체(Cow) 관련 타입 정의
- * 백엔드 CowModel Entity 기준으로 작성
+ * ========================================
+ *
+ * @description
+ * - 실제 사용되는 타입만 정의
+ * - 백엔드 CowModel Entity와 1:1 매핑
*/
+// ========================================
+// 1. 개체 기본 정보
+// ========================================
+
/**
* 개체 기본 정보
- * 백엔드 CowModel Entity와 일치
+ *
+ * @backend CowModel Entity
+ * @description 한우 개체 1마리의 기본 정보
*/
export interface CowDto {
- pkCowNo: number; // 내부 PK (자동증가)
- cowId: string; // 개체식별번호 (KOR 또는 KPN) - 필수
- cowSex?: string; // 성별 (M/F)
- cowBirthDt?: string; // 생년월일
- sireKpn?: string; // 부(씨수소) KPN번호
- damCowId?: string; // 모(어미소) 개체식별번호 (KOR)
- fkFarmNo?: number; // 농장번호 FK
- cowStatus?: string; // 개체상태
- delDt?: string; // 삭제일시 (Soft Delete)
- anlysDt?: string; // 분석일자
- unavailableReason?: string; // 분석불가 사유
- hasMpt?: boolean; // 번식능력검사(MPT) 여부
- mptTestDt?: string; // MPT 검사일
- mptMonthAge?: number; // MPT 검사일 기준 월령
+ // === 기본 키 ===
+ pkCowNo: number // 개체 내부 번호 (Primary Key)
+ cowId: string // 개체식별번호 (KOR 또는 KPN)
- // Relations
+ // === 개체 정보 ===
+ cowSex?: string // 성별 (M/F)
+ cowBirthDt?: string // 생년월일 (YYYY-MM-DD)
+ cowStatus?: string // 개체 상태
+
+ // === 혈통 정보 ===
+ sireKpn?: string // 부(씨수소) KPN 번호
+ damCowId?: string // 모(어미소) 개체식별번호
+
+ // === 연결 정보 ===
+ fkFarmNo?: number // 농장번호 (Foreign Key)
+
+ // === 분석 정보 ===
+ anlysDt?: string // 분석일자
+ unavailableReason?: string // 분석 불가 사유
+
+ // === 번식능력검사(MPT) 정보 ===
+ hasMpt?: boolean // MPT 검사 여부
+ mptTestDt?: string // MPT 검사일
+ mptMonthAge?: number // MPT 검사 월령
+
+ // === 시스템 필드 ===
+ delDt?: string // 삭제일시
+
+ // === 관계 데이터 ===
farm?: {
- pkFarmNo: number;
- farmNm?: string;
- farmAddr?: string;
- }; // 농장 정보 (조인)
+ pkFarmNo: number
+ farmNm?: string
+ farmAddr?: string
+ }
}
/**
- * 개체 목록 응답
- */
-export interface CowListResponseDto {
- totalCount: number; // 전체 개체 수
- cows: CowDto[]; // 개체 목록
-}
-
-/**
- * 개체 상세 응답 (프론트엔드 확장)
+ * 개체 상세 정보
+ *
+ * @description CowDto 확장 + 계산 필드 + 분석 정보
*/
export interface CowDetailResponseDto extends CowDto {
- // 계산된 필드 (프론트엔드에서 계산)
- age?: number; // 나이 (년)
- cowShortNo?: string; // 개체 요약번호 (4자리, cowId에서 추출)
+ // === 계산 필드 ===
+ age?: number // 나이 (년 단위)
+ cowShortNo?: string // 개체 요약번호 (뒷 4자리)
- // 추가 분석 정보 (백엔드 별도 API에서 조회)
- genomeScore?: number; // 유전체 점수
- farmRank?: number; // 농장 내 순위
- totalCows?: number; // 농장 총 개체 수
- inbreedingCoef?: number; // 근친계수 (0.0~1.0)
- calvingCount?: number; // 분만회차
+ // === 유전체 분석 정보 ===
+ genomeScore?: number // 유전체 종합 점수
+ farmRank?: number // 농장 내 순위
+ totalCows?: number // 농장 총 개체 수
+ inbreedingCoef?: number // 근친계수 (0.0~1.0)
+ calvingCount?: number // 분만 회차
- // 데이터 상태 (백엔드에서 조회)
+ // === 데이터 상태 ===
dataStatus?: {
- hasGenomeData: boolean; // 유전체 데이터 존재 여부
- hasGeneData: boolean; // 유전자 데이터 존재 여부
- };
+ hasGenomeData: boolean
+ hasGeneData: boolean
+ }
}
-/**
- * 개체 검색 DTO
- */
-export interface CowSearchDto {
- keyword?: string; // 검색 키워드 (개체번호, 이름 등)
- farmNo?: number; // 농장 번호로 필터링
- gender?: 'M' | 'F'; // 성별로 필터링
- minBirthDate?: string; // 최소 생년월일 (YYYY-MM-DD)
- maxBirthDate?: string; // 최대 생년월일 (YYYY-MM-DD)
-}
+// ========================================
+// 2. 확장 타입 (순위 포함)
+// ========================================
/**
- * 개체 생성 DTO
+ * 유전자 정보 포함 개체
+ *
+ * @description 개체 목록에서 사용 (유전자 + 순위 정보)
*/
-export interface CreateCowDto {
- cowId: string; // 개체식별번호 (필수)
- cowSex: 'M' | 'F'; // 성별 (필수)
- cowBirthDt?: string; // 생년월일 (YYYY-MM-DD)
- fkFarmNo: number; // 농장 번호 (필수)
- sireKpn?: string; // 부(씨수소) KPN번호
- damCowId?: string; // 모(어미소) 개체식별번호
- cowStatus?: string; // 개체상태
-}
-
-/**
- * 개체 수정 DTO
- */
-export interface UpdateCowDto {
- cowBirthDt?: string; // 생년월일 수정 (YYYY-MM-DD)
- sireKpn?: string; // 부(씨수소) KPN번호 수정
- damCowId?: string; // 모(어미소) 개체식별번호 수정
- cowStatus?: string; // 개체상태 수정
-}
-
-// 타입 alias (호환성)
-export type Cow = CowDto;
-export type CowList = CowListResponseDto;
-export type CowDetail = CowDetailResponseDto;
-
-/**
- * 형질 데이터 (육종가/형질값)
- */
-export interface TraitData {
- breedVal: number | null
- traitVal: number | null
-}
-
-/**
- * 유전자 정보를 포함한 개체 (개체 목록용)
- */
-export interface CowWithGenes extends Cow {
+export interface CowWithGenes extends CowDto {
genes?: { name: string; genotype: string }[]
- traits?: Record
+ traits?: Record
rank?: number
genomeScore?: number
cowShortNo?: string
- anlysDt?: string
- unavailableReason?: string
- hasMpt?: boolean
- mptTestDt?: string
- mptMonthAge?: number
}
/**
- * Ranking API 응답 아이템
+ * 순위 API 응답 항목
+ *
+ * @description /cow/ranking API 응답의 개별 항목
*/
export interface RankingItem {
- entity: Cow & {
+ entity: CowDto & {
genes?: Record
calvingCount?: number
bcs?: number
@@ -141,9 +115,23 @@ export interface RankingItem {
mptTestDt?: string
mptMonthAge?: number
}
- rank: number
- sortValue: number
+ rank: number // 순위
+ sortValue: number // 정렬 기준값
ranking?: {
- traits?: { traitName: string; traitEbv: number | null; traitVal: number | null }[]
+ traits?: {
+ traitName: string
+ traitEbv: number | null
+ traitVal: number | null
+ }[]
}
}
+
+// ========================================
+// 3. 타입 별칭
+// ========================================
+
+/**
+ * @description 실제 사용 중인 별칭
+ */
+export type Cow = CowDto
+export type CowDetail = CowDetailResponseDto
diff --git a/frontend/src/types/filter.types.ts b/frontend/src/types/filter.types.ts
index a2c9c72..bdbd8d9 100644
--- a/frontend/src/types/filter.types.ts
+++ b/frontend/src/types/filter.types.ts
@@ -1,128 +1,95 @@
/**
- * 전역 필터 타입 정의 /
+ * ========================================
+ * 전역 필터 타입 정의
+ * ========================================
*
+ * @description
+ * - 사용자별 맞춤 필터 설정
+ * - 유전자/형질 선택 및 가중치 설정
+ * - 모든 페이지에 공통 적용
*/
-/**
- * 분석 지표 (유전자/유전능력)
- */
-export type AnalysisIndex = "GENE" | "ABILITY";
-
-/**
- * 유전자 정보
- */
-export interface GeneInfo {
- name: string;
- category: "QUANTITY" | "QUALITY";
- description: string;
- importance: number;
-}
-
-/**
- * 형질 정보
- */
-export interface TraitInfo {
- name: string;
- category: "GROWTH" | "ECONOMIC" | "BODY" | "WEIGHT" | "RATE";
- description: string;
-}
-
/**
* 전역 필터 설정
- * 사용자가 로그인해서 설정하면 모든 페이지에서 이 필터가 적용됨
+ *
+ * @description
+ * - 사용자별 맞춤 필터 설정
+ * - Zustand 스토어에 저장
+ * - 개체 목록 필터링 및 선발지수 계산에 사용
*/
export interface GlobalFilterSettings {
- // 분석 지표
- analysisIndex?: AnalysisIndex;
+ // === 분석 지표 ===
+ analysisIndex?: 'GENE' | 'ABILITY' // 유전자(GENE) 또는 유전능력(ABILITY) 기반
- // 선택된 유전자 목록 (유전자 기반)
- selectedGenes: string[];
+ // === 유전자 선택 (GENE 모드) ===
+ selectedGenes: string[] // 선택된 유전자 목록
+ pinnedGenes?: string[] // 고정 유전자 (최대 5개)
- // 고정 유전자 (최대 5개, 항상 맨 앞 표시)
- pinnedGenes?: string[];
-
- // 선택된 형질 목록
- selectedTraits?: string[];
-
- // 고정 형질 (최대 5개, 항상 맨 앞 표시)
- pinnedTraits?: string[];
-
- // 형질 가중치 (유전체 기반)
- // ======== DB의 trait_nm과 일치해야 함==============
- // JavaScript/TypeScript 문법 규칙 :
- // "12개월령체중": number; // 필수 - 숫자로 시작
- // "my-key": number; // 필수 - 하이픈(특수문자)
- // "my key": number; // 필수 - 공백 포함
+ // === 형질 선택 (ABILITY 모드) ===
+ selectedTraits?: string[] // 선택된 형질 목록
+ pinnedTraits?: string[] // 고정 형질 (최대 5개)
+ // === 형질 가중치 ===
+ // 35개 형질 (DB의 trait_nm과 일치)
traitWeights: {
// 성장형질 (1개)
- "12개월령체중": number; // 따옴표
+ "12개월령체중": number
// 경제형질 (4개)
- 도체중: number;
- 등심단면적: number;
- 등지방두께: number;
- 근내지방도: number;
+ 도체중: number
+ 등심단면적: number
+ 등지방두께: number
+ 근내지방도: number
- // 체형형질 (10개)
- 체고: number;
- 십자: number; // 십자부고 → 십자
- 체장: number;
- 흉심: number;
- 흉폭: number;
- 고장: number; // 요각장 → 고장
- 요각폭: number;
- 좌골폭: number;
- 곤폭: number; // 좌골단폭 → 곤폭
- 흉위: number;
+ // 체형형질 (10개)
+ 체고: number
+ 십자: number
+ 체장: number
+ 흉심: number
+ 흉폭: number
+ 고장: number
+ 요각폭: number
+ 좌골폭: number
+ 곤폭: number
+ 흉위: number
- // 부위별무게 (10개) - DB 형질명과 일치 (무게와 비율은 영문 붙여서 구분)
- 안심weight: number;
- 등심weight: number;
- 채끝weight: number;
- 목심weight: number;
- 앞다리weight: number;
- 우둔weight: number;
- 설도weight: number;
- 사태weight: number;
- 양지weight: number;
- 갈비weight: number;
+ // 부위별무게 (10개)
+ 안심weight: number
+ 등심weight: number
+ 채끝weight: number
+ 목심weight: number
+ 앞다리weight: number
+ 우둔weight: number
+ 설도weight: number
+ 사태weight: number
+ 양지weight: number
+ 갈비weight: number
- // 부위별비율 (10개) - DB 형질명과 일치 (무게와 비율은 영문 붙여서 구분)
- 안심rate: number;
- 등심rate: number;
- 채끝rate: number;
- 목심rate: number;
- 앞다리rate: number;
- 우둔rate: number;
- 설도rate: number;
- 사태rate: number;
- 양지rate: number;
- 갈비rate: number;
- };
+ // 부위별비율 (10개)
+ 안심rate: number
+ 등심rate: number
+ 채끝rate: number
+ 목심rate: number
+ 앞다리rate: number
+ 우둔rate: number
+ 설도rate: number
+ 사태rate: number
+ 양지rate: number
+ 갈비rate: number
+ }
- // 근친도 임계값 (%)
- inbreedingThreshold: number;
-
- // 필터 활성화 여부
- isActive: boolean;
-
- // 마지막 업데이트 시간
- updtDt: Date;
+ // === 기타 설정 ===
+ inbreedingThreshold: number // 근친도 임계값 (%)
+ isActive: boolean // 필터 활성화 여부
+ updtDt: Date // 마지막 업데이트 시간
}
/**
- * ====================================================================================================
- * 기본 필터 초기값 설정
- * 사용자가 해당 형질 선택하지 않았을때 필터의 초기 값 0 세팅
- * ====================================================================================================
- * const [filterSettings, setFilterSettings] = useState(DEFAULT_FILTER_SETTINGS);
- * function resetFilter() {
- setFilterSettings(DEFAULT_FILTER_SETTINGS); // 초기화
- }
- if (!user.filterSettings) {
- user.filterSettings = DEFAULT_FILTER_SETTINGS; // 기본값 적용
- }
+ * 기본 필터 초기값
+ *
+ * @description
+ * - 사용자가 필터를 설정하지 않았을 때 기본값
+ * - 기본 7개 형질 선택 (도체중, 등심단면적, 등지방두께, 근내지방도, 등심중량, 체장, 체고)
*/
export const DEFAULT_FILTER_SETTINGS: GlobalFilterSettings = {
analysisIndex: "GENE",
@@ -131,16 +98,16 @@ export const DEFAULT_FILTER_SETTINGS: GlobalFilterSettings = {
selectedTraits: ["도체중", "등심단면적", "등지방두께", "근내지방도", "등심weight", "체장", "체고"],
pinnedTraits: [],
traitWeights: {
- // 성장형질 (점수: 1 ~ 10, 미선택 시 0)
+ // 성장형질
"12개월령체중": 0,
- // 경제형질 (점수: 1 ~ 10, 미선택 시 0)
+ // 경제형질 (기본 선택: 가중치 1)
도체중: 1,
등심단면적: 1,
등지방두께: 1,
근내지방도: 1,
- // 체형형질 (점수: 1 ~ 10, 미선택 시 0) - DB 형질명과 일치
+ // 체형형질
체고: 1,
십자: 0,
체장: 1,
@@ -152,7 +119,7 @@ export const DEFAULT_FILTER_SETTINGS: GlobalFilterSettings = {
곤폭: 0,
흉위: 0,
- // 부위별무게 (점수: 1 ~ 10, 미선택 시 0) - DB 형질명과 일치
+ // 부위별무게
안심weight: 0,
등심weight: 1,
채끝weight: 0,
@@ -164,7 +131,7 @@ export const DEFAULT_FILTER_SETTINGS: GlobalFilterSettings = {
양지weight: 0,
갈비weight: 0,
- // 부위별비율 (점수: 1 ~ 10, 미선택 시 0) - DB 형질명과 일치
+ // 부위별비율
안심rate: 0,
등심rate: 0,
채끝rate: 0,
@@ -176,35 +143,7 @@ export const DEFAULT_FILTER_SETTINGS: GlobalFilterSettings = {
양지rate: 0,
갈비rate: 0,
},
- inbreedingThreshold: 0, // 근친도 기본값 0
- isActive: true, // 기본 7개 형질이 선택되어 있으므로 활성화
- // 기본 각각 1점으로 세팅
+ inbreedingThreshold: 0,
+ isActive: true,
updtDt: new Date(),
-};
-
-/**
- * 주요 유전자 7개 (육량형 3개 + 육질형 4개)
- * 나머지 유전자는 "전체 보기" 검색 버튼을 통해 선택 가능
- */
-export const MAJOR_GENES: GeneInfo[] = [
- // 육량형 3개
- { name: "PLAG1", category: "QUANTITY", description: "성장 및 체중 증가에 영향", importance: 20 },
- { name: "NCAPG2", category: "QUANTITY", description: "체구 크기와 성장 속도", importance: 19 },
- { name: "BTB", category: "QUANTITY", description: "도체 크기", importance: 18 },
-
- // 육질형 4개
- { name: "NT5E", category: "QUALITY", description: "근내지방도", importance: 20 },
- { name: "SCD", category: "QUALITY", description: "지방산 불포화도", importance: 19 },
- { name: "FASN", category: "QUALITY", description: "지방 합성", importance: 18 },
- { name: "CAPN1", category: "QUALITY", description: "육질 연도에 영향", importance: 17 },
-];
-
-/**
- * 경제형질 4개
- */
-export const ECONOMIC_TRAITS: TraitInfo[] = [
- { name: "도체중", category: "ECONOMIC", description: "도체중" },
- { name: "등심단면적", category: "ECONOMIC", description: "등심단면적" },
- { name: "등지방두께", category: "ECONOMIC", description: "등지방두께" },
- { name: "근내지방도", category: "ECONOMIC", description: "근내지방도" },
-];
+}
diff --git a/frontend/src/types/genome.types.ts b/frontend/src/types/genome.types.ts
index 9f18f54..b4b9356 100644
--- a/frontend/src/types/genome.types.ts
+++ b/frontend/src/types/genome.types.ts
@@ -1,145 +1,70 @@
/**
- * 유전체 관련 타입 정의
- * 백엔드 API 응답 형식 기준으로 작성
- */
-
-/**
- * 유전체 분석 의뢰 정보
- * 백엔드 GenomeRequestModel Entity와 일치
- */
-export interface GenomeRequestDto {
- pkRequestNo: number; // No (PK)
- fkFarmNo?: number; // 농장번호 FK
- fkCowNo?: number; // 개체번호 FK
- cowRemarks?: string; // 개체 비고
- requestDt?: string; // 접수일자
- snpTest?: string; // SNP 검사
- msTest?: string; // MS 검사
- sampleAmount?: string; // 모근량
- sampleRemarks?: string; // 모근 비고
-
- // 칩 분석 정보
- chipNo?: string; // 분석 Chip 번호
- chipType?: string; // 분석 칩 종류
- chipInfo?: string; // 칩정보
- chipRemarks?: string; // 칩 비고
- chipSireName?: string; // 칩분석 아비명
- chipDamName?: string; // 칩분석 어미명
- chipReportDt?: string; // 칩분석 보고일자
-
- // MS 검사 결과
- msResultStatus?: string; // MS 감정결과
- msFatherEstimate?: string; // MS 추정부
- msReportDt?: string; // MS 보고일자
-
- delDt?: string; // 삭제일시 (Soft Delete)
-}
-
-/**
- * 형질 정보 (API 응답용)
- */
-export interface TraitInfo {
- traitNm: string; // 형질명
- traitCtgry?: string; // 형질 카테고리
- traitDesc?: string; // 형질 설명
- [key: string]: any;
-}
-
-/**
- * 유전체 개체 형질 상세 (API 응답용 - genomeCows 배열 항목)
- */
-export interface GenomeCow {
- traitVal?: number; // 형질 측정값
- breedVal?: number; // EBV (추정육종가)
- percentile?: number; // 백분위 순위
- traitName?: string; // 형질명 (평평한 구조)
- traitCategory?: string; // 형질 카테고리
- traitDesc?: string; // 형질 설명
- [key: string]: any;
-}
-
-/**
- * 유전체 형질 정보 (API 응답용)
- * 백엔드 findByCowId 응답 형식과 일치
+ * ========================================
+ * 유전체 분석 관련 타입 정의
+ * ========================================
*
- * NOTE: genome_trait 테이블이 삭제되어 genome_trait_detail이 직접 genome_request와 연결됨
+ * @description
+ * - 실제 사용되는 타입만 정의
+ * - 백엔드 findByCowId API 응답 형식
+ */
+
+/**
+ * 형질별 육종가 정보
+ *
+ * @backend GenomeTraitDto.genomeCows 배열 항목
+ * @description 개체의 형질별 유전체 분석 결과
+ *
+ * @usage
+ * - 개체 상세 페이지 형질 데이터 표시
+ * - 형질 비교 차트 컴포넌트
+ */
+export interface GenomeCowTraitDto {
+ traitName?: string // 형질명 (예: "도체중", "등심단면적")
+ breedVal?: number // EBV: 표준화 육종가 (σ 단위)
+ traitVal?: number // EPD: 육종가 원본값
+ percentile?: number // 백분위 순위 (0-100, 낮을수록 상위)
+ traitCategory?: string // 형질 카테고리
+ traitDesc?: string // 형질 설명
+ [key: string]: any
+}
+
+/**
+ * 유전체 형질 정보
+ *
+ * @backend findByCowId API 응답
+ * @description 개체의 유전체 분석 결과 (의뢰 정보 + 35개 형질 데이터)
+ *
+ * @usage
+ * - genome.api.findByCowNo() 응답
+ * - 개체 상세 페이지에서 형질 데이터 표시
*/
export interface GenomeTraitDto {
- fkRequestNo?: number; // 의뢰번호 FK
+ // === 연결 정보 ===
+ fkRequestNo?: number // 분석 의뢰번호
- // API 응답 필드
- request?: GenomeRequestDto; // 분석 의뢰 정보
- trait?: any; // 형질 기본 정보
- genomeCows?: GenomeCow[]; // 형질별 상세 데이터 (EBV, 백분위 등)
+ // === 관계 데이터 ===
+ request?: { // 분석 의뢰 기본 정보
+ pkRequestNo?: number
+ requestDt?: string
+ chipSireName?: string
+ [key: string]: any
+ }
- // 계산된 필드 (프론트엔드용)
- anlysDt?: string; // 분석일자
+ trait?: any // 형질 메타데이터 (legacy)
- delDt?: string; // 삭제일시 (Soft Delete)
- [key: string]: any;
+ genomeCows?: GenomeCowTraitDto[] // 형질별 육종가/백분위 배열 (35개)
+
+ // === 계산 필드 ===
+ anlysDt?: string // 분석일자
+
+ // === 시스템 필드 ===
+ delDt?: string // 삭제일시
+ [key: string]: any // 추가 동적 필드
}
/**
- * 유전체 형질 상세 정보
- * 백엔드 GenomeTraitDetailModel Entity와 일치
+ * GenomeTrait 별칭
*
- * NOTE: fk_trait_no가 fk_request_no로 변경됨 (genome_trait 테이블 삭제로 인해)
+ * @description 실제 사용 중 (genome.api.ts, cow/[cowNo]/page.tsx 등)
*/
-export interface GenomeTraitDetailDto {
- pkTraitDetailNo: number; // 형질상세번호 PK
- fkRequestNo: number; // 의뢰번호 FK (변경됨: fkTraitNo → fkRequestNo)
- cowId?: string; // 개체식별번호 (KOR...) - 추가됨
- traitName?: string; // 형질명 (예: "12개월령체중", "도체중", "등심단면적")
- traitVal?: number; // 실측값
- traitEbv?: number; // 표준화육종가 (EBV: Estimated Breeding Value)
- traitPercentile?: number; // 백분위수 (전국 대비 순위)
- delDt?: string; // 삭제일시 (Soft Delete)
-}
-
-/**
- * 유전능력 평가 요청
- */
-export interface GeneticAbilityRequest {
- selectedTraits?: {
- [traitName: string]: number; // 형질명: 가중치
- };
-}
-
-/**
- * 유전능력 평가 응답 (프론트엔드 확장)
- * 형질별 표준화육종가 기반 평가
- */
-export interface GeneticAbilityDto {
- // 종합 평가
- overallScore?: number; // 종합 점수 (표준화 점수)
- grade?: 'A' | 'B' | 'C' | 'D' | 'E'; // 등급
- farmRank?: number; // 농장 내 순위
- totalCows?: number; // 농장 전체 개체 수
-
- // 35개 형질 데이터 (형질명: 표준화육종가)
- traits?: Record;
-
- // 경제형질 (4개) - 표준화육종가 (백엔드 응답 필드명)
- carcassWeight_breedVal?: number; // 도체중
- eyeMuscleArea_breedVal?: number; // 등심단면적
- backfatThickness_breedVal?: number; // 등지방두께
- marbling_breedVal?: number; // 근내지방도
-
- // 체형형질 (10개) - 표준화육종가 (백엔드 응답 필드명)
- bodyHeight_breedVal?: number; // 체고
- crossHeight_breedVal?: number; // 십자
- bodyLength_breedVal?: number; // 체장
- chestDepth_breedVal?: number; // 흉심
- chestWidth_breedVal?: number; // 흉폭
- hipLength_breedVal?: number; // 고장
- hipWidth_breedVal?: number; // 요각폭
- sitBoneWidth_breedVal?: number; // 곤폭
- ischiumWidth_breedVal?: number; // 좌골폭
- chestGirth_breedVal?: number; // 흉위
-}
-
-// 타입 alias (호환성)
-export type GenomeRequest = GenomeRequestDto;
-export type GenomeTrait = GenomeTraitDto;
-export type GenomeTraitDetail = GenomeTraitDetailDto;
-export type GeneticAbility = GeneticAbilityDto;
+export type GenomeTrait = GenomeTraitDto
diff --git a/frontend/src/types/ranking.types.ts b/frontend/src/types/ranking.types.ts
index cac8936..c8bbeec 100644
--- a/frontend/src/types/ranking.types.ts
+++ b/frontend/src/types/ranking.types.ts
@@ -1,209 +1,101 @@
/**
- * 랭킹 관련 타입 정의
- * 백엔드 ranking.interface.ts와 동기화
+ * ========================================
+ * 랭킹(Ranking) 관련 타입 정의
+ * ========================================
+ *
+ * @description
+ * - 실제 사용되는 타입만 정의
+ * - POST /cow/ranking API 요청/응답
*/
-/**
- * 랭킹 기준 타입
- */
-export enum RankingCriteriaType {
- GENE = 'GENE', // 유전자 기반 랭킹
- GENOME = 'GENOME', // 유전체 형질 기반 랭킹
- CONCEPTION_RATE = 'CONCEPTION_RATE', // 수태율 랭킹
- BCS = 'BCS', // BCS 랭킹
- MPT = 'MPT', // 혈액대사검사 랭킹
- INBREEDING = 'INBREEDING', // 근친도 기반 랭킹
- COW_PURPOSE = 'COW_PURPOSE', // 암소 용도별 추천
- COMPOSITE = 'COMPOSITE', // 복합 평가
-}
+// ========================================
+// 1. 랭킹 조건
+// ========================================
/**
- * 정렬 방향
- */
-export enum RankingOrder {
- ASC = 'ASC', // 오름차순
- DESC = 'DESC', // 내림차순
-}
-
-/**
- * 유전자 기반 랭킹 조건
- */
-export interface GeneRankingCondition {
- markerNm: string; // 유전자 마커명
- order?: RankingOrder;
-}
-
-/**
- * 유전체 형질 기반 랭킹 조건
+ * 형질 기반 랭킹 조건
+ *
+ * @description 선발지수 계산용 형질별 가중치
+ * @example { traitNm: "도체중", weight: 2.0 }
*/
export interface TraitRankingCondition {
- traitNm: string; // 형질 이름
- weight?: number; // 가중치
+ traitNm: string // 형질명 (예: "도체중", "등심단면적")
+ weight?: number // 가중치 (기본값: 1)
}
/**
- * 수태율 기반 랭킹 조건
+ * 필터 조건
+ *
+ * @description WHERE 조건 (현재 사용 안 함, 향후 확장용)
*/
-export interface ConceptionRateCondition {
- order?: RankingOrder;
+export interface FilterCondition {
+ field: string // 컬럼명
+ operator: string // 연산자 (eq, gt, lt 등)
+ value: any // 비교값
}
-/**
- * BCS 기반 랭킹 조건
- */
-export interface BCSCondition {
- order?: RankingOrder;
-}
+// ========================================
+// 2. 랭킹 옵션
+// ========================================
/**
- * MPT (혈액대사검사) 기반 랭킹 조건
+ * 필터 엔진 옵션
+ *
+ * @description 필터링 옵션 (현재 farmNo만 사용)
+ * @usage filterOptions: { farmNo: 1 }
*/
-export interface MptRankingCondition {
- criteria: string[]; // 평가할 MPT 항목
- normalRanges?: Record;
-}
-
-/**
- * 근친도 기반 랭킹 조건
- */
-export interface InbreedingCondition {
- targetKpnNo?: string; // 특정 KPN과의 근친도 계산
- maxThreshold?: number; // 최대 허용 근친도
- order?: RankingOrder;
+export interface FilterEngineOptions {
+ farmNo?: number // 농장 번호 필터
+ filters?: FilterCondition[] // 추가 필터 조건 (향후 확장용)
}
/**
* 랭킹 옵션
+ *
+ * @description 순위 계산 설정
+ * @usage
+ * {
+ * criteriaType: 'GENOME',
+ * traitConditions: [{ traitNm: "도체중", weight: 2 }]
+ * }
*/
export interface RankingOptions {
- criteriaType: RankingCriteriaType;
- geneConditions?: GeneRankingCondition[];
- traitConditions?: TraitRankingCondition[];
- conceptionRateCondition?: ConceptionRateCondition;
- bcsCondition?: BCSCondition;
- mptCondition?: MptRankingCondition;
- inbreedingCondition?: InbreedingCondition;
- limit?: number;
- offset?: number;
+ criteriaType: 'GENE' | 'GENOME' // 유전자 또는 유전체 기반
+ traitConditions?: TraitRankingCondition[] // 형질 조건 배열
+ limit?: number // 결과 개수 제한 (향후 확장용)
+ offset?: number // 시작 위치 (향후 확장용)
}
-/**
- * 암소 용도별 추천 타입
- */
-export enum CowRecommendationType {
- EMBRYO_DONOR = 'EMBRYO_DONOR', // 공란우
- EMBRYO_RECIPIENT = 'EMBRYO_RECIPIENT', // 수란우
- ARTIFICIAL_INSEMINATION = 'ARTIFICIAL_INSEMINATION', // 인공수정
- CULLING = 'CULLING', // 도태대상
- GENERAL = 'GENERAL', // 일반
-}
+// ========================================
+// 3. 랭킹 요청 DTO
+// ========================================
/**
- * 랭킹 상세 정보
- */
-export interface RankingDetail {
- code: string; // 평가 항목 명
- value: number; // 실체 측정 값
- weight?: number; // 가중치
-}
-
-/**
- * 랭킹 결과 아이템
- */
-export interface RankingResultItem {
- entity: T; // 원본 엔티티
- rank: number; // 순위
- sortValue: number; // 정렬에 사용된 값
- details?: RankingDetail[];
- recommendationType?: CowRecommendationType;
- compositeScores?: {
- genomeScore: number;
- geneScore: number;
- inbreedingPercent: number;
- };
-}
-
-/**
- * 랭킹 결과
- */
-export interface RankingResult {
- items: RankingResultItem[];
- total: number;
- criteriaType: RankingCriteriaType;
- timestamp: Date;
-}
-
-/**
- * 필터 연산자 타입
- */
-export type FilterOperator =
- | 'eq' // 같음
- | 'ne' // 같지 않음
- | 'gt' // 초과
- | 'gte' // 이상
- | 'lt' // 미만
- | 'lte' // 이하
- | 'like' // 포함 (문자열)
- | 'in' // 배열 내 포함
- | 'between'; // 범위
-
-/**
- * 필터 조건
- */
-export interface FilterCondition {
- field: string; // 필터링할 컬럼명
- operator: FilterOperator; // 연산자
- value: any; // 비교 값 (between인 경우 [min, max] 배열)
-}
-
-/**
- * 정렬 방향
- */
-export type SortOrder = 'ASC' | 'DESC';
-
-/**
- * 정렬 옵션
- */
-export interface SortOption {
- field: string; // 정렬할 컬럼명
- order: SortOrder; // 정렬 방향
-}
-
-/**
- * 페이지네이션 옵션
- */
-export interface PaginationOption {
- page: number; // 페이지 번호 (1부터 시작)
- limit: number; // 페이지당 아이템 수
-}
-
-/**
- * FilterEngine 옵션
- */
-export interface FilterEngineOptions {
- filters?: FilterCondition[]; // 필터 조건 배열
- sorts?: SortOption[]; // 정렬 옵션 배열
- pagination?: PaginationOption; // 페이지네이션 옵션
-}
-
-/**
- * FilterEngine 실행 결과
- */
-export interface FilterEngineResult {
- data: T[]; // 조회된 데이터
- total: number; // 전체 데이터 개수 (페이지네이션 전)
- page?: number; // 현재 페이지
- limit?: number; // 페이지당 아이템 수
- totalPages?: number; // 전체 페이지 수
-}
-
-/**
- * 랭킹 요청 DTO
+ * 랭킹 요청
+ *
+ * @description POST /cow/ranking 요청 바디
+ * @example
+ * {
+ * filterOptions: { farmNo: 1 },
+ * rankingOptions: {
+ * criteriaType: "GENOME",
+ * traitConditions: [
+ * { traitNm: "도체중", weight: 2 },
+ * { traitNm: "근내지방도", weight: 3 }
+ * ]
+ * }
+ * }
*/
export interface RankingRequestDto {
- filterOptions?: FilterEngineOptions;
- rankingOptions: RankingOptions;
+ filterOptions?: FilterEngineOptions // 필터링 옵션
+ rankingOptions: RankingOptions // 랭킹 계산 옵션
}
-// 타입 alias
-export type RankingRequest = RankingRequestDto;
+// ========================================
+// 4. 타입 별칭
+// ========================================
+
+/**
+ * @description 실제 사용 중인 별칭
+ */
+export type RankingRequest = RankingRequestDto