Merge branch 'main' of http://gitea.turbosoft.kr:3080/turbosoft/genome2025
This commit is contained in:
@@ -27,6 +27,13 @@ import {
|
|||||||
} from './dto/ranking-request.dto';
|
} from './dto/ranking-request.dto';
|
||||||
import { isValidGenomeAnalysis } from '../common/config/GenomeAnalysisConfig';
|
import { isValidGenomeAnalysis } from '../common/config/GenomeAnalysisConfig';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 낮을수록 좋은 형질 목록 (부호 반전 필요)
|
||||||
|
* - 등지방두께: 지방이 얇을수록(EBV가 낮을수록) 좋은 형질
|
||||||
|
* - 선발지수 계산 시 EBV 부호를 반전하여 적용
|
||||||
|
*/
|
||||||
|
const NEGATIVE_TRAITS = ['등지방두께'];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 개체(소) 관리 서비스
|
* 개체(소) 관리 서비스
|
||||||
*
|
*
|
||||||
@@ -291,13 +298,16 @@ export class CowService {
|
|||||||
if (trait && trait.traitEbv !== null) {
|
if (trait && trait.traitEbv !== null) {
|
||||||
// EBV 값이 있으면 가중치 적용하여 합산
|
// EBV 값이 있으면 가중치 적용하여 합산
|
||||||
const ebv = Number(trait.traitEbv);
|
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; // 가중치 누적
|
totalWeight += weight; // 가중치 누적
|
||||||
|
|
||||||
// 상세 내역 저장 (응답용)
|
// 상세 내역 저장 (응답용)
|
||||||
details.push({
|
details.push({
|
||||||
code: condition.traitNm, // 형질명
|
code: condition.traitNm, // 형질명
|
||||||
value: ebv, // EBV 값
|
value: adjustedEbv, // EBV 값 (부호 반전 적용)
|
||||||
weight, // 적용된 가중치
|
weight, // 적용된 가중치
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -10,6 +10,13 @@ import { FarmModel } from '../farm/entities/farm.entity';
|
|||||||
import { GenomeRequestModel } from './entities/genome-request.entity';
|
import { GenomeRequestModel } from './entities/genome-request.entity';
|
||||||
import { GenomeTraitDetailModel } from './entities/genome-trait-detail.entity';
|
import { GenomeTraitDetailModel } from './entities/genome-trait-detail.entity';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 낮을수록 좋은 형질 목록 (부호 반전 필요)
|
||||||
|
* - 등지방두께: 지방이 얇을수록(EBV가 낮을수록) 좋은 형질
|
||||||
|
* - 선발지수 계산 시 EBV 부호를 반전하여 적용
|
||||||
|
*/
|
||||||
|
const NEGATIVE_TRAITS = ['등지방두께'];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 형질명 → 카테고리 매핑 상수
|
* 형질명 → 카테고리 매핑 상수
|
||||||
* - 성장: 월령별 체중 관련 형질
|
* - 성장: 월령별 체중 관련 형질
|
||||||
@@ -587,9 +594,17 @@ export class GenomeService {
|
|||||||
let rank: number | null = null;
|
let rank: number | null = null;
|
||||||
const farmData = rankings.find(r => r.farmNo === farmNo);
|
const farmData = rankings.find(r => r.farmNo === farmNo);
|
||||||
if (farmData) {
|
if (farmData) {
|
||||||
// 나보다 높은 점수를 가진 농장 수 + 1 = 내 순위
|
// 등지방두께 등 낮을수록 좋은 형질은 순위 계산 반전
|
||||||
const higherCount = rankings.filter(r => r.avgEbv > farmData.avgEbv).length;
|
const isNegativeTrait = NEGATIVE_TRAITS.includes(traitName);
|
||||||
rank = higherCount + 1;
|
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;
|
const percentile = rank !== null && totalFarms > 0 ? Math.round((rank / totalFarms) * 100) : null;
|
||||||
@@ -1368,7 +1383,10 @@ export class GenomeService {
|
|||||||
|
|
||||||
if (trait && trait.traitEbv !== null) {
|
if (trait && trait.traitEbv !== null) {
|
||||||
const ebv = Number(trait.traitEbv);
|
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;
|
weightedSum += contribution;
|
||||||
totalWeight += weight;
|
totalWeight += weight;
|
||||||
@@ -1505,7 +1523,11 @@ export class GenomeService {
|
|||||||
const weight = condition.weight || 1;
|
const weight = condition.weight || 1;
|
||||||
|
|
||||||
if (trait && trait.traitEbv !== null) {
|
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;
|
totalWeight += weight;
|
||||||
} else {
|
} else {
|
||||||
hasAllTraits = false;
|
hasAllTraits = false;
|
||||||
@@ -1813,7 +1835,11 @@ export class GenomeService {
|
|||||||
const weight = condition.weight || 1;
|
const weight = condition.weight || 1;
|
||||||
|
|
||||||
if (trait && trait.traitEbv !== null) {
|
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;
|
totalWeight += weight;
|
||||||
} else {
|
} else {
|
||||||
hasAllTraits = false;
|
hasAllTraits = false;
|
||||||
|
|||||||
@@ -18,6 +18,9 @@ import {
|
|||||||
import { genomeApi, TraitRankDto } from "@/lib/api/genome.api"
|
import { genomeApi, TraitRankDto } from "@/lib/api/genome.api"
|
||||||
import { useGlobalFilter } from "@/contexts/GlobalFilterContext"
|
import { useGlobalFilter } from "@/contexts/GlobalFilterContext"
|
||||||
|
|
||||||
|
// 낮을수록 좋은 형질 (부호 반전 필요)
|
||||||
|
const NEGATIVE_TRAITS = ['등지방두께']
|
||||||
|
|
||||||
// 카테고리 색상 (모던 & 다이나믹 - 생동감 있는 색상)
|
// 카테고리 색상 (모던 & 다이나믹 - 생동감 있는 색상)
|
||||||
const CATEGORY_COLORS: Record<string, string> = {
|
const CATEGORY_COLORS: Record<string, string> = {
|
||||||
'성장': '#3b82f6', // 블루
|
'성장': '#3b82f6', // 블루
|
||||||
@@ -285,8 +288,15 @@ export function NormalDistributionChart({
|
|||||||
|
|
||||||
// "내 개체 중심" 방식: 개체를 0에 고정
|
// "내 개체 중심" 방식: 개체를 0에 고정
|
||||||
// 농가/보은군은 개체 대비 상대 위치로 표시 (음수 = 개체보다 뒤처짐)
|
// 농가/보은군은 개체 대비 상대 위치로 표시 (음수 = 개체보다 뒤처짐)
|
||||||
const cowVsFarm = baseScore - baseFarmScore // 농가 대비 개체 차이
|
let cowVsFarm = baseScore - baseFarmScore // 농가 대비 개체 차이
|
||||||
const cowVsRegion = baseScore - baseRegionScore // 보은군 대비 개체 차이
|
let cowVsRegion = baseScore - baseRegionScore // 보은군 대비 개체 차이
|
||||||
|
|
||||||
|
// 등지방두께 등 낮을수록 좋은 형질은 부호 반전
|
||||||
|
// (개체가 농가보다 낮으면 실제로는 더 좋은 것이므로 양수로 표시)
|
||||||
|
if (NEGATIVE_TRAITS.includes(chartFilterTrait)) {
|
||||||
|
cowVsFarm = -cowVsFarm
|
||||||
|
cowVsRegion = -cowVsRegion
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
score: 0, // 개체는 항상 0 (중심)
|
score: 0, // 개체는 항상 0 (중심)
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ import { Card, CardContent } from "@/components/ui/card"
|
|||||||
// 기본 7개 형질
|
// 기본 7개 형질
|
||||||
const DEFAULT_TRAITS = ['도체중', '등심단면적', '등지방두께', '근내지방도', '체장', '체고', '등심weight']
|
const DEFAULT_TRAITS = ['도체중', '등심단면적', '등지방두께', '근내지방도', '체장', '체고', '등심weight']
|
||||||
|
|
||||||
|
// 낮을수록 좋은 형질 (부호 반전 색상 적용)
|
||||||
|
const NEGATIVE_TRAITS = ['등지방두께']
|
||||||
|
|
||||||
// 형질명 표시 (전체 이름)
|
// 형질명 표시 (전체 이름)
|
||||||
const TRAIT_SHORT_NAMES: Record<string, string> = {
|
const TRAIT_SHORT_NAMES: Record<string, string> = {
|
||||||
'도체중': '도체중',
|
'도체중': '도체중',
|
||||||
@@ -82,12 +85,17 @@ function TraitListView({ traits, cowName }: { traits: Array<{ name: string; shor
|
|||||||
</td>
|
</td>
|
||||||
<td className="px-3 sm:px-5 py-4 text-left">
|
<td className="px-3 sm:px-5 py-4 text-left">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className={`text-base sm:text-xl font-bold ${(trait.actualValue ?? 0) > 0
|
<span className={`text-base sm:text-xl font-bold ${(() => {
|
||||||
? 'text-green-600'
|
const value = trait.actualValue ?? 0
|
||||||
: (trait.actualValue ?? 0) < 0
|
const isNegativeTrait = NEGATIVE_TRAITS.includes(trait.name)
|
||||||
? 'text-red-600'
|
// 등지방두께: 음수가 좋음(녹색), 양수가 나쁨(빨간색)
|
||||||
: 'text-muted-foreground'
|
// 나머지: 양수가 좋음(녹색), 음수가 나쁨(빨간색)
|
||||||
}`}>
|
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 !== undefined ? (
|
||||||
<>{trait.actualValue > 0 ? '+' : ''}{trait.actualValue.toFixed(1)}</>
|
<>{trait.actualValue > 0 ? '+' : ''}{trait.actualValue.toFixed(1)}</>
|
||||||
) : '-'}
|
) : '-'}
|
||||||
|
|||||||
@@ -58,6 +58,9 @@ const TRAIT_CATEGORIES: Record<string, string[]> = {
|
|||||||
'비율': ['안심rate', '등심rate', '채끝rate', '목심rate', '앞다리rate', '우둔rate', '설도rate', '사태rate', '양지rate', '갈비rate'],
|
'비율': ['안심rate', '등심rate', '채끝rate', '목심rate', '앞다리rate', '우둔rate', '설도rate', '사태rate', '양지rate', '갈비rate'],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 낮을수록 좋은 형질 (부호 반전 필요)
|
||||||
|
const NEGATIVE_TRAITS = ['등지방두께']
|
||||||
|
|
||||||
export default function DashboardPage() {
|
export default function DashboardPage() {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const { user } = useAuthStore()
|
const { user } = useAuthStore()
|
||||||
@@ -294,7 +297,15 @@ export default function DashboardPage() {
|
|||||||
if (traitData) {
|
if (traitData) {
|
||||||
const farmEpd = traitData.avgEpd ?? 0
|
const farmEpd = traitData.avgEpd ?? 0
|
||||||
const regionEpd = traitData.regionAvgEpd ?? 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)
|
regionScore = 0 // 보은군 = 기준점 (0)
|
||||||
originalFarmScore = farmEpd
|
originalFarmScore = farmEpd
|
||||||
originalRegionScore = regionEpd
|
originalRegionScore = regionEpd
|
||||||
|
|||||||
Reference in New Issue
Block a user