UI 수정:화면 수정

This commit is contained in:
2025-12-11 20:07:19 +09:00
parent b906ec1851
commit 7d15c9be7c
26 changed files with 2629 additions and 557 deletions

View File

@@ -25,7 +25,10 @@ export const INVALID_CHIP_DAM_NAMES = ['불일치', '이력제부재'];
/** ===============================개별 제외 개체 목록 (분석불가 등 특수 사유) 하단 개체 정보없음 확인필요=================*/
export const EXCLUDED_COW_IDS = [
'KOR002191642861', // 1회 분석 반려내역서 재분석 불가능
'KOR002191642861',
// 일치인데 정보가 없음 / 김정태님 유전체 내역 빠짐 1두
// 일단 모근 1회분량이고 재검사어려움 , 모근상태 불량으로 인한 DNA분해로 인해 분석불가 상태로 넣음
];
//=================================================================================================================

View File

@@ -228,25 +228,36 @@ export class CowService {
// 각 개체별로 점수 계산
const cowsWithScore = await Promise.all(
cows.map(async (cow) => {
// Step 1: cowId로 직접 형질 상세 데이터 조회 (35개 형질의 EBV 값)
const traitDetails = await this.genomeTraitDetailRepository.find({
where: { cowId: cow.cowId, delDt: IsNull() },
});
// 형질 데이터가 없으면 점수 null
if (traitDetails.length === 0) {
return { entity: cow, sortValue: null, details: [] };
}
// Step 2: 해당 개체의 최신 유전체 분석 의뢰 조회 (친자감별 확인용)
// Step 1: 해당 개체의 최신 유전체 분석 의뢰 조회 (친자감별 확인용)
const latestRequest = await this.genomeRequestRepository.findOne({
where: { fkCowNo: cow.pkCowNo, delDt: IsNull() },
order: { requestDt: 'DESC', regDt: 'DESC' },
});
// Step 3: 친자감별 확인 - 아비 KPN "일치"가 아니면 분석 불가
// Step 2: 친자감별 확인 - 유효하지 않으면 분석 불가
if (!latestRequest || !isValidGenomeAnalysis(latestRequest.chipSireName, latestRequest.chipDamName, cow.cowId)) {
return { entity: cow, sortValue: null, details: [] };
// 분석불가 사유 결정
let unavailableReason = '미분석';
if (latestRequest) {
if (latestRequest.chipSireName !== '일치') {
unavailableReason = '부 불일치';
} else if (latestRequest.chipDamName === '불일치') {
unavailableReason = '모 불일치';
} else if (latestRequest.chipDamName === '이력제부재') {
unavailableReason = '모 이력제부재';
}
}
return { entity: { ...cow, unavailableReason }, sortValue: null, details: [] };
}
// Step 3: cowId로 직접 형질 상세 데이터 조회 (35개 형질의 EBV 값)
const traitDetails = await this.genomeTraitDetailRepository.find({
where: { cowId: cow.cowId, delDt: IsNull() },
});
// 형질 데이터가 없으면 점수 null (친자는 일치하지만 형질 데이터 없음)
if (traitDetails.length === 0) {
return { entity: { ...cow, unavailableReason: '형질정보없음' }, sortValue: null, details: [] };
}
// Step 4: 가중 평균 계산

View File

@@ -45,9 +45,12 @@ export class GenomeController {
* 농가의 보은군 내 순위 조회 (대시보드용)
* @param farmNo - 농장 번호
*/
@Get('farm-region-ranking/:farmNo')
getFarmRegionRanking(@Param('farmNo') farmNo: string) {
return this.genomeService.getFarmRegionRanking(+farmNo);
@Post('farm-region-ranking/:farmNo')
getFarmRegionRanking(
@Param('farmNo') farmNo: string,
@Body() body: { traitConditions?: { traitNm: string; weight?: number }[] }
) {
return this.genomeService.getFarmRegionRanking(+farmNo, body.traitConditions);
}
/**

View File

@@ -1322,13 +1322,18 @@ export class GenomeService {
};
}
// Step 4: 가중 평균 계산
// Step 4: 가중 평균 계산 ================================================================================
let weightedSum = 0; // Σ(EBV × 가중치)
let totalWeight = 0; // Σ(가중치)
let percentileSum = 0; // 백분위 합계 (평균 계산용)
let percentileCount = 0; // 백분위 개수
let hasAllTraits = true; // 모든 선택 형질 존재 여부 (리스트와 동일 로직)
const details: { traitNm: string; ebv: number; weight: number; contribution: number }[] = [];
const details: {
traitNm: string;
ebv: number;
weight: number;
contribution: number
}[] = [];
for (const condition of traitConditions) {
const trait = traitDetails.find((d) => d.traitName === condition.traitNm);
@@ -1359,8 +1364,8 @@ export class GenomeService {
}
}
// Step 6: 최종 점수 계산 (모든 선택 형질이 있어야만 계산)
const score = (hasAllTraits && totalWeight > 0) ? weightedSum / totalWeight : null;
// Step 6: 최종 점수 계산 (모든 선택 형질이 있어야만 계산) ================================================================
const score = (hasAllTraits && totalWeight > 0) ? weightedSum : null;
const percentile = percentileCount > 0 ? percentileSum / percentileCount : null;
// Step 7: 현재 개체의 농장/지역 정보 조회
@@ -1482,7 +1487,7 @@ export class GenomeService {
// 모든 선택 형질이 있는 경우만 점수에 포함
if (hasAllTraits && totalWeight > 0) {
const score = weightedSum / totalWeight;
const score = weightedSum;
allScores.push({
cowId: request.cow.cowId,
score: Math.round(score * 100) / 100,
@@ -1494,6 +1499,7 @@ export class GenomeService {
// 점수 기준 내림차순 정렬
allScores.sort((a, b) => b.score - a.score);
console.log('[calculateRanks] 샘플 점수:', allScores.slice(0, 5).map(s => ({ cowId: s.cowId, score: s.score })));
// 농가 순위 및 평균 선발지수 계산
let farmRank: number | null = null;
@@ -1667,7 +1673,10 @@ export class GenomeService {
* 성능 최적화: N+1 쿼리 문제 해결 - 모든 데이터를 한 번에 조회 후 메모리에서 처리
* @param farmNo - 농장 번호
*/
async getFarmRegionRanking(farmNo: number): Promise<{
async getFarmRegionRanking(
farmNo: number,
inputTraitConditions?: { traitNm: string; weight?: number }[]
): Promise<{
farmNo: number;
farmerName: string | null;
farmAvgScore: number | null;
@@ -1728,7 +1737,12 @@ export class GenomeService {
'안심rate', '등심rate', '채끝rate', '목심rate', '앞다리rate',
'우둔rate', '설도rate', '사태rate', '양지rate', '갈비rate',
];
const traitConditions = ALL_TRAITS.map(traitNm => ({ traitNm, weight: 1 }));
// inputTraitConditions가 있으면 사용, 없으면 35개 형질 기본값 사용
const traitConditions = inputTraitConditions && inputTraitConditions.length > 0
? inputTraitConditions
: ALL_TRAITS.map(traitNm => ({ traitNm, weight: 1 }));
console.log('[getFarmRegionRanking] traitConditions:', traitConditions.length, 'traits');
// 5. 각 개체별 점수 계산 (메모리에서 처리 - DB 쿼리 없음)
const allScores: { cowId: string; score: number; farmNo: number | null }[] = [];
@@ -1758,7 +1772,7 @@ export class GenomeService {
}
if (hasAllTraits && totalWeight > 0) {
const score = weightedSum / totalWeight;
const score = weightedSum;
allScores.push({
cowId: request.cow.cowId,
score: Math.round(score * 100) / 100,
@@ -1815,6 +1829,14 @@ export class GenomeService {
? Math.round((allScores.reduce((sum, s) => sum + s.score, 0) / allScores.length) * 100) / 100
: null;
// 디버깅: 상위 5개 농가 점수 출력
console.log('[getFarmRegionRanking] 결과:', {
myFarmRank: farmRankInRegion,
myFarmScore: myFarmData?.avgScore,
totalFarms: farmAverages.length,
top5: farmAverages.slice(0, 5).map(f => ({ farmNo: f.farmNo, score: f.avgScore }))
});
return {
farmNo,
farmerName: farm.farmerName || null,