diff --git a/frontend/src/app/cow/[cowNo]/_components/header.tsx b/frontend/src/app/cow/[cowNo]/_components/header.tsx deleted file mode 100644 index eab3b30..0000000 --- a/frontend/src/app/cow/[cowNo]/_components/header.tsx +++ /dev/null @@ -1,44 +0,0 @@ -'use client' - -import { Button } from "@/components/ui/button" -import { ArrowLeft } from "lucide-react" -import { useRouter } from "next/navigation" - -interface CowHeaderProps { - from?: string | null -} - -export function CowHeader({ from }: CowHeaderProps) { - const router = useRouter() - - const handleBack = () => { - if (from === 'ranking') { - router.push('/ranking') - } else if (from === 'list') { - router.push('/list') - } else { - router.push('/cow') - } - } - - return ( -
- {/* 뒤로가기 버튼 */} - - - {/* 페이지 헤더 카드 */} -
-

개체 상세 정보

-

개체의 기본 정보와 분석 현황을 확인할 수 있습니다.

-
-
- ) -} 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 1c89c2d..6511322 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 @@ -26,46 +26,8 @@ import { ResponsiveContainer } from 'recharts' import { useMediaQuery } from "@/hooks/use-media-query" -import { DEFAULT_TRAITS, ALL_TRAITS, TRAIT_CATEGORIES } from "@/constants/traits" - -// 형질명 표시 (전체 이름) -const TRAIT_SHORT_NAMES: Record = { - '도체중': '도체중', - '등심단면적': '등심단면적', - '등지방두께': '등지방두께', - '근내지방도': '근내지방도', - '체장': '체장', - '체고': '체고', - '등심weight': '등심중량', - '12개월령체중': '12개월령체중', - '십자': '십자', - '흉심': '흉심', - '흉폭': '흉폭', - '고장': '고장', - '요각폭': '요각폭', - '좌골폭': '좌골폭', - '곤폭': '곤폭', - '흉위': '흉위', - '안심weight': '안심무게', - '채끝weight': '채끝무게', - '목심weight': '목심무게', - '앞다리weight': '앞다리무게', - '우둔weight': '우둔무게', - '설도weight': '설도무게', - '사태weight': '사태무게', - '양지weight': '양지무게', - '갈비weight': '갈비무게', - '안심rate': '안심비율', - '등심rate': '등심비율', - '채끝rate': '채끝비율', - '목심rate': '목심비율', - '앞다리rate': '앞다리비율', - '우둔rate': '우둔비율', - '설도rate': '설도비율', - '사태rate': '사태비율', - '양지rate': '양지비율', - '갈비rate': '갈비비율', -} +import { DEFAULT_TRAITS, ALL_TRAITS, TRAIT_CATEGORIES, getTraitDisplayName, TRAIT_DISPLAY_NAMES } from "@/constants/traits" +import { GenomeCowTraitDto } from "@/types/genome.types" interface CategoryStat { category: string @@ -74,22 +36,13 @@ interface CategoryStat { count: number } -interface TraitData { - id?: number - traitName?: string // 형질명 - traitCategory?: string // 카테고리 - breedVal?: number // 표준화육종가 (σ 단위) - percentile?: number - traitVal?: number // EPD (예상후대차이) 원래 값 -} - interface CategoryEvaluationCardProps { categoryStats: CategoryStat[] comparisonAverages: ComparisonAveragesDto | null traitComparisonAverages?: TraitComparisonAveragesDto | null // 형질별 평균 비교 데이터 (폴리곤 차트용) regionAvgZ: number farmAvgZ: number - allTraits?: TraitData[] + allTraits?: GenomeCowTraitDto[] cowNo?: string hideTraitCards?: boolean // 형질 카드 숨김 여부 } @@ -151,7 +104,7 @@ export function CategoryEvaluationCard({ // 폴리곤 차트용 데이터 생성 (선택된 형질 기반) - 보은군, 농가, 이 개체 비교 const traitChartData = chartTraits.map(traitName => { - const trait = allTraits.find((t: TraitData) => t.traitName === traitName) + const trait = allTraits.find((t: GenomeCowTraitDto) => t.traitName === traitName) // 형질별 평균 데이터에서 해당 형질 찾기 const traitAvgRegion = traitComparisonAverages?.region?.find(t => t.traitName === traitName) @@ -166,7 +119,7 @@ export function CategoryEvaluationCard({ return { name: traitName, - shortName: TRAIT_SHORT_NAMES[traitName] || traitName, + shortName: getTraitDisplayName(traitName), breedVal: trait?.breedVal ?? 0, // 이 개체 표준화육종가 (σ) epd: trait?.traitVal ?? 0, // 이 개체 EPD (육종가) regionVal: regionTraitAvg, // 보은군 평균 (표준화육종가) @@ -192,7 +145,7 @@ export function CategoryEvaluationCard({ // 형질 이름으로 원본 형질명 찾기 (shortName -> name) const findTraitNameByShortName = (shortName: string) => { - const entry = Object.entries(TRAIT_SHORT_NAMES).find(([, short]) => short === shortName) + const entry = Object.entries(TRAIT_DISPLAY_NAMES).find(([, short]) => short === shortName) return entry ? entry[0] : shortName } @@ -262,7 +215,7 @@ export function CategoryEvaluationCard({
{traits.map(trait => { const isSelected = chartTraits.includes(trait) - const traitData = allTraits.find((t: TraitData) => t.traitName === trait) + const traitData = allTraits.find((t: GenomeCowTraitDto) => t.traitName === trait) return ( -
- - -
- {filteredGenes.length === 0 ? ( -

- 검색 결과가 없습니다. -

- ) : ( - filteredGenes.map((gene) => ( -
- toggleGene(gene.name)} - /> -
- -

{gene.description}

-
-
- )) - )} -
-
- - - )} - -
- 선택된 유전자: {tempSelectedGenes.length}개 -
- - - - - - - - ) -} 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 ( - - - {/* 헤더 */} - -
-
- -
-
- 유전자 검색 및 선택 - - 전체 {allGenes.length.toLocaleString()}개 / 선택 {selectedGenes.length}개 - -
-
-
- - {/* 검색 및 필터 */} -
- {/* 검색바 */} -
- - setSearchQuery(e.target.value)} - className="pl-9 pr-9 h-10 text-sm bg-background" - autoFocus - /> - {searchQuery && ( - - )} -
- - {/* 필터 탭 및 액션 버튼 */} -
- setFilterType(v as any)} className="flex-1"> - - - 전체 ({allGenes.length}) - - -
-
- 육량 ({allGenes.filter(g => g.markerTypeCd === 'QTY').length}) -
-
- -
-
- 육질 ({allGenes.filter(g => g.markerTypeCd === 'QLT').length}) -
-
-
-
- -
- - -
-
-
- - {/* 유전자 목록 */} -
- {loading ? ( -
-
-
-

유전자 데이터 로딩 중...

-
-
- ) : filteredGenes.length > 0 ? ( - -
- {filteredGenes.map((gene) => { - const isSelected = selectedGenes.includes(gene.markerNm) - const isQuantity = gene.markerTypeCd === 'QTY' - - return ( - toggleGene(gene.markerNm)} - title={`${gene.markerNm}\n${gene.markerDesc || ''}\n${gene.relatedTrait ? `관련 형질: ${gene.relatedTrait}` : ''}`} - > - {gene.markerNm} - - ) - })} -
-
- ) : ( -
-
- -

검색 결과가 없습니다

-

다른 검색어나 필터를 시도해보세요

-
-
- )} -
- - {/* 하단 버튼 */} -
-
- {searchQuery && ( - - 검색: {filteredGenes.length.toLocaleString()}개 - - )} - - 선택: {selectedGenes.length}개 - -
-
- - -
-
-
-
- ) -} 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