From b9117f231b4ee93574f96656c0deb743e6cd0f2c Mon Sep 17 00:00:00 2001 From: chu eun ju Date: Fri, 12 Dec 2025 17:03:30 +0900 Subject: [PATCH 1/2] =?UTF-8?q?=EA=B0=9C=EC=B2=B4=20=EC=83=81=EC=84=B8?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/cow/cow.service.ts | 14 +++++++++-- backend/src/genome/genome.service.ts | 24 ++++++++++++++++--- .../_components/trait-distribution-charts.tsx | 20 +++++++++++----- 3 files changed, 47 insertions(+), 11 deletions(-) diff --git a/backend/src/cow/cow.service.ts b/backend/src/cow/cow.service.ts index 3272e13..59b2e62 100644 --- a/backend/src/cow/cow.service.ts +++ b/backend/src/cow/cow.service.ts @@ -27,6 +27,13 @@ import { } from './dto/ranking-request.dto'; import { isValidGenomeAnalysis } from '../common/config/GenomeAnalysisConfig'; +/** + * 낮을수록 좋은 형질 목록 (부호 반전 필요) + * - 등지방두께: 지방이 얇을수록(EBV가 낮을수록) 좋은 형질 + * - 선발지수 계산 시 EBV 부호를 반전하여 적용 + */ +const NEGATIVE_TRAITS = ['등지방두께']; + /** * 개체(소) 관리 서비스 * @@ -291,13 +298,16 @@ export class CowService { if (trait && trait.traitEbv !== null) { // EBV 값이 있으면 가중치 적용하여 합산 const ebv = Number(trait.traitEbv); - weightedSum += ebv * weight; // EBV × 가중치 + // 등지방두께 등 낮을수록 좋은 형질은 부호 반전 + const isNegativeTrait = NEGATIVE_TRAITS.includes(condition.traitNm); + const adjustedEbv = isNegativeTrait ? -ebv : ebv; + weightedSum += adjustedEbv * weight; // EBV × 가중치 totalWeight += weight; // 가중치 누적 // 상세 내역 저장 (응답용) details.push({ code: condition.traitNm, // 형질명 - value: ebv, // EBV 값 + value: adjustedEbv, // EBV 값 (부호 반전 적용) weight, // 적용된 가중치 }); } else { diff --git a/backend/src/genome/genome.service.ts b/backend/src/genome/genome.service.ts index c18b585..743ef4f 100644 --- a/backend/src/genome/genome.service.ts +++ b/backend/src/genome/genome.service.ts @@ -10,6 +10,13 @@ import { FarmModel } from '../farm/entities/farm.entity'; import { GenomeRequestModel } from './entities/genome-request.entity'; import { GenomeTraitDetailModel } from './entities/genome-trait-detail.entity'; +/** + * 낮을수록 좋은 형질 목록 (부호 반전 필요) + * - 등지방두께: 지방이 얇을수록(EBV가 낮을수록) 좋은 형질 + * - 선발지수 계산 시 EBV 부호를 반전하여 적용 + */ +const NEGATIVE_TRAITS = ['등지방두께']; + /** * 형질명 → 카테고리 매핑 상수 * - 성장: 월령별 체중 관련 형질 @@ -1368,7 +1375,10 @@ export class GenomeService { if (trait && trait.traitEbv !== null) { const ebv = Number(trait.traitEbv); - const contribution = ebv * weight; // EBV × 가중치 + // 등지방두께 등 낮을수록 좋은 형질은 부호 반전 + const isNegativeTrait = NEGATIVE_TRAITS.includes(condition.traitNm); + const adjustedEbv = isNegativeTrait ? -ebv : ebv; + const contribution = adjustedEbv * weight; // EBV × 가중치 weightedSum += contribution; totalWeight += weight; @@ -1505,7 +1515,11 @@ export class GenomeService { const weight = condition.weight || 1; if (trait && trait.traitEbv !== null) { - weightedSum += Number(trait.traitEbv) * weight; + // 등지방두께 등 낮을수록 좋은 형질은 부호 반전 + const ebv = Number(trait.traitEbv); + const isNegativeTrait = NEGATIVE_TRAITS.includes(condition.traitNm); + const adjustedEbv = isNegativeTrait ? -ebv : ebv; + weightedSum += adjustedEbv * weight; totalWeight += weight; } else { hasAllTraits = false; @@ -1813,7 +1827,11 @@ export class GenomeService { const weight = condition.weight || 1; if (trait && trait.traitEbv !== null) { - weightedSum += Number(trait.traitEbv) * weight; + // 등지방두께 등 낮을수록 좋은 형질은 부호 반전 + const ebv = Number(trait.traitEbv); + const isNegativeTrait = NEGATIVE_TRAITS.includes(condition.traitNm); + const adjustedEbv = isNegativeTrait ? -ebv : ebv; + weightedSum += adjustedEbv * weight; totalWeight += weight; } else { hasAllTraits = false; diff --git a/frontend/src/app/cow/[cowNo]/genome/_components/trait-distribution-charts.tsx b/frontend/src/app/cow/[cowNo]/genome/_components/trait-distribution-charts.tsx index 4755704..10ddac4 100644 --- a/frontend/src/app/cow/[cowNo]/genome/_components/trait-distribution-charts.tsx +++ b/frontend/src/app/cow/[cowNo]/genome/_components/trait-distribution-charts.tsx @@ -6,6 +6,9 @@ import { Card, CardContent } from "@/components/ui/card" // 기본 7개 형질 const DEFAULT_TRAITS = ['도체중', '등심단면적', '등지방두께', '근내지방도', '체장', '체고', '등심weight'] +// 낮을수록 좋은 형질 (부호 반전 색상 적용) +const NEGATIVE_TRAITS = ['등지방두께'] + // 형질명 표시 (전체 이름) const TRAIT_SHORT_NAMES: Record = { '도체중': '도체중', @@ -82,12 +85,17 @@ function TraitListView({ traits, cowName }: { traits: Array<{ name: string; shor
- 0 - ? 'text-green-600' - : (trait.actualValue ?? 0) < 0 - ? 'text-red-600' - : 'text-muted-foreground' - }`}> + { + const value = trait.actualValue ?? 0 + const isNegativeTrait = NEGATIVE_TRAITS.includes(trait.name) + // 등지방두께: 음수가 좋음(녹색), 양수가 나쁨(빨간색) + // 나머지: 양수가 좋음(녹색), 음수가 나쁨(빨간색) + if (value === 0) return 'text-muted-foreground' + if (isNegativeTrait) { + return value < 0 ? 'text-green-600' : 'text-red-600' + } + return value > 0 ? 'text-green-600' : 'text-red-600' + })()}`}> {trait.actualValue !== undefined ? ( <>{trait.actualValue > 0 ? '+' : ''}{trait.actualValue.toFixed(1)} ) : '-'} From 5f45b517fb0527ad4b6853e504d25e5b90645b49 Mon Sep 17 00:00:00 2001 From: chu eun ju Date: Fri, 12 Dec 2025 17:22:48 +0900 Subject: [PATCH 2/2] =?UTF-8?q?=ED=99=94=EB=A9=B4=20=EC=B0=A8=ED=8A=B8=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/genome/genome.service.ts | 14 +++++++++++--- .../_components/normal-distribution-chart.tsx | 14 ++++++++++++-- frontend/src/app/dashboard/page.tsx | 13 ++++++++++++- 3 files changed, 35 insertions(+), 6 deletions(-) diff --git a/backend/src/genome/genome.service.ts b/backend/src/genome/genome.service.ts index 743ef4f..d7bacb8 100644 --- a/backend/src/genome/genome.service.ts +++ b/backend/src/genome/genome.service.ts @@ -594,9 +594,17 @@ export class GenomeService { let rank: number | null = null; const farmData = rankings.find(r => r.farmNo === farmNo); if (farmData) { - // 나보다 높은 점수를 가진 농장 수 + 1 = 내 순위 - const higherCount = rankings.filter(r => r.avgEbv > farmData.avgEbv).length; - rank = higherCount + 1; + // 등지방두께 등 낮을수록 좋은 형질은 순위 계산 반전 + const isNegativeTrait = NEGATIVE_TRAITS.includes(traitName); + if (isNegativeTrait) { + // 나보다 낮은 점수를 가진 농장 수 + 1 = 내 순위 (낮을수록 좋음) + const lowerCount = rankings.filter(r => r.avgEbv < farmData.avgEbv).length; + rank = lowerCount + 1; + } else { + // 나보다 높은 점수를 가진 농장 수 + 1 = 내 순위 (높을수록 좋음) + const higherCount = rankings.filter(r => r.avgEbv > farmData.avgEbv).length; + rank = higherCount + 1; + } } const percentile = rank !== null && totalFarms > 0 ? Math.round((rank / totalFarms) * 100) : null; diff --git a/frontend/src/app/cow/[cowNo]/genome/_components/normal-distribution-chart.tsx b/frontend/src/app/cow/[cowNo]/genome/_components/normal-distribution-chart.tsx index e2cf88a..608c8b8 100644 --- a/frontend/src/app/cow/[cowNo]/genome/_components/normal-distribution-chart.tsx +++ b/frontend/src/app/cow/[cowNo]/genome/_components/normal-distribution-chart.tsx @@ -18,6 +18,9 @@ import { import { genomeApi, TraitRankDto } from "@/lib/api/genome.api" import { useGlobalFilter } from "@/contexts/GlobalFilterContext" +// 낮을수록 좋은 형질 (부호 반전 필요) +const NEGATIVE_TRAITS = ['등지방두께'] + // 카테고리 색상 (모던 & 다이나믹 - 생동감 있는 색상) const CATEGORY_COLORS: Record = { '성장': '#3b82f6', // 블루 @@ -285,8 +288,15 @@ export function NormalDistributionChart({ // "내 개체 중심" 방식: 개체를 0에 고정 // 농가/보은군은 개체 대비 상대 위치로 표시 (음수 = 개체보다 뒤처짐) - const cowVsFarm = baseScore - baseFarmScore // 농가 대비 개체 차이 - const cowVsRegion = baseScore - baseRegionScore // 보은군 대비 개체 차이 + let cowVsFarm = baseScore - baseFarmScore // 농가 대비 개체 차이 + let cowVsRegion = baseScore - baseRegionScore // 보은군 대비 개체 차이 + + // 등지방두께 등 낮을수록 좋은 형질은 부호 반전 + // (개체가 농가보다 낮으면 실제로는 더 좋은 것이므로 양수로 표시) + if (NEGATIVE_TRAITS.includes(chartFilterTrait)) { + cowVsFarm = -cowVsFarm + cowVsRegion = -cowVsRegion + } return { score: 0, // 개체는 항상 0 (중심) diff --git a/frontend/src/app/dashboard/page.tsx b/frontend/src/app/dashboard/page.tsx index 66bf01b..34be54e 100644 --- a/frontend/src/app/dashboard/page.tsx +++ b/frontend/src/app/dashboard/page.tsx @@ -58,6 +58,9 @@ const TRAIT_CATEGORIES: Record = { '비율': ['안심rate', '등심rate', '채끝rate', '목심rate', '앞다리rate', '우둔rate', '설도rate', '사태rate', '양지rate', '갈비rate'], } +// 낮을수록 좋은 형질 (부호 반전 필요) +const NEGATIVE_TRAITS = ['등지방두께'] + export default function DashboardPage() { const router = useRouter() const { user } = useAuthStore() @@ -294,7 +297,15 @@ export default function DashboardPage() { if (traitData) { const farmEpd = traitData.avgEpd ?? 0 const regionEpd = traitData.regionAvgEpd ?? 0 - farmScore = farmEpd - regionEpd // 보은군 대비 차이 + let diff = farmEpd - regionEpd // 보은군 대비 차이 + + // 등지방두께 등 낮을수록 좋은 형질은 부호 반전 + // (농가가 보은군보다 낮으면 실제로는 더 좋은 것이므로 양수로 표시) + if (NEGATIVE_TRAITS.includes(distributionBasis)) { + diff = -diff + } + + farmScore = diff regionScore = 0 // 보은군 = 기준점 (0) originalFarmScore = farmEpd originalRegionScore = regionEpd