파일 정리
This commit is contained in:
@@ -3,7 +3,6 @@ import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { IsNull, Repository } from 'typeorm';
|
||||
import {
|
||||
isValidGenomeAnalysis,
|
||||
VALID_CHIP_SIRE_NAME
|
||||
} from '../common/config/GenomeAnalysisConfig';
|
||||
import {
|
||||
ALL_TRAITS,
|
||||
@@ -17,46 +16,8 @@ import { GenomeRequestModel } from './entities/genome-request.entity';
|
||||
import { GenomeTraitDetailModel } from './entities/genome-trait-detail.entity';
|
||||
import { MptModel } from '../mpt/entities/mpt.entity';
|
||||
import { GeneDetailModel } from '../gene/entities/gene-detail.entity';
|
||||
|
||||
/**
|
||||
* 카테고리별 평균 EBV(추정육종가) 응답 DTO
|
||||
*/
|
||||
interface CategoryAverageDto {
|
||||
category: string; // 카테고리명 (성장/생산/체형/무게/비율)
|
||||
avgEbv: number; // 평균 EBV 값 (표준화 육종가)
|
||||
avgEpd: number; // 평균 EPD 값 (원래 육종가)
|
||||
count: number; // 해당 카테고리의 데이터 개수
|
||||
}
|
||||
|
||||
/**
|
||||
* 비교 평균 응답 DTO
|
||||
* 전국/지역/농장 3단계로 평균값 제공
|
||||
*/
|
||||
interface ComparisonAveragesDto {
|
||||
nationwide: CategoryAverageDto[]; // 전국 평균
|
||||
region: CategoryAverageDto[]; // 지역(군) 평균
|
||||
farm: CategoryAverageDto[]; // 농장 평균
|
||||
}
|
||||
|
||||
/**
|
||||
* 형질별 평균 EBV 응답 DTO
|
||||
*/
|
||||
interface TraitAverageDto {
|
||||
traitName: string; // 형질명
|
||||
category: string; // 카테고리
|
||||
avgEbv: number; // 평균 EBV (표준화 육종가)
|
||||
avgEpd: number; // 평균 EPD (육종가 원본값)
|
||||
count: number; // 데이터 개수
|
||||
}
|
||||
|
||||
/**
|
||||
* 형질별 비교 평균 응답 DTO
|
||||
*/
|
||||
export interface TraitComparisonAveragesDto {
|
||||
nationwide: TraitAverageDto[]; // 전국 평균
|
||||
region: TraitAverageDto[]; // 지역(군) 평균
|
||||
farm: TraitAverageDto[]; // 농장 평균
|
||||
}
|
||||
import { CategoryAverageDto, ComparisonAveragesDto } from './dto/comparison-averages.dto';
|
||||
import { TraitAverageDto, TraitComparisonAveragesDto } from './dto/trait-comparison.dto';
|
||||
|
||||
/**
|
||||
* 유전체 분석 서비스
|
||||
@@ -105,6 +66,8 @@ export class GenomeService {
|
||||
* - 형질별 농장 평균 EBV
|
||||
* - 접수 내역 목록
|
||||
*
|
||||
* @usedBy /dashboard - 대시보드 페이지
|
||||
* @usedBy /cow/[cowNo]/genome - 개체 상세 > 유전체 통합 비교
|
||||
* @param farmNo - 농장 번호
|
||||
*/
|
||||
async getDashboardStats(farmNo: number): Promise<{
|
||||
@@ -164,7 +127,7 @@ export class GenomeService {
|
||||
sireMismatch: number; // 부 불일치
|
||||
damMismatch: number; // 모 불일치 (부 일치 + 모 불일치)
|
||||
damNoRecord: number; // 모 이력제부재 (부 일치 + 모 이력제부재)
|
||||
pending: number; // 대기
|
||||
notAnalyzed: number; // 미분석
|
||||
};
|
||||
// 월별 접수 현황
|
||||
monthlyStats: { month: number; count: number }[];
|
||||
@@ -185,28 +148,13 @@ export class GenomeService {
|
||||
traitCount: number;
|
||||
}[];
|
||||
}> {
|
||||
// Step 1: 농장의 모든 분석 의뢰 조회
|
||||
// Step 1: 농장의 모든 분석 의뢰 조회 (traitDetails 포함)
|
||||
const requests = await this.genomeRequestRepository.find({
|
||||
where: { fkFarmNo: farmNo, delDt: IsNull() },
|
||||
relations: ['cow'],
|
||||
relations: ['cow', 'traitDetails'],
|
||||
order: { requestDt: 'DESC', regDt: 'DESC' },
|
||||
});
|
||||
|
||||
// Step 1.5: 모든 형질 상세 데이터를 한 번에 조회 (N+1 문제 해결)
|
||||
const allTraitDetails = await this.genomeTraitDetailRepository.find({
|
||||
where: { delDt: IsNull() },
|
||||
});
|
||||
|
||||
// cowId로 형질 데이터를 그룹화 (Map으로 빠른 조회)
|
||||
const traitDetailsByCowId = new Map<string, typeof allTraitDetails>();
|
||||
for (const detail of allTraitDetails) {
|
||||
if (!detail.cowId) continue;
|
||||
if (!traitDetailsByCowId.has(detail.cowId)) {
|
||||
traitDetailsByCowId.set(detail.cowId, []);
|
||||
}
|
||||
traitDetailsByCowId.get(detail.cowId)!.push(detail);
|
||||
}
|
||||
|
||||
// Step 2: 연도별 통계 계산
|
||||
const yearMap = new Map<number, { total: number; analyzed: number; pending: number; sireMatch: number }>();
|
||||
|
||||
@@ -243,7 +191,7 @@ export class GenomeService {
|
||||
sireMatchRate: stat.total > 0 ? Math.round((stat.sireMatch / stat.total) * 100) : 0,
|
||||
}));
|
||||
|
||||
// Step 3: 분석 완료된 개체의 형질 데이터 수집 (메모리에서 처리)
|
||||
// Step 3: 분석 완료된 개체의 형질 데이터 수집
|
||||
const validRequests = requests.filter(r => r.chipSireName === '일치');
|
||||
const traitDataMap = new Map<string, { sum: number; epdSum: number; percentileSum: number; count: number; category: string }>();
|
||||
|
||||
@@ -251,8 +199,8 @@ export class GenomeService {
|
||||
const yearlyTraitMap = new Map<number, Map<string, { sum: number; count: number }>>();
|
||||
|
||||
for (const request of validRequests) {
|
||||
// Map에서 형질 데이터 조회 (DB 쿼리 없이 O(1) 조회)
|
||||
const details = traitDetailsByCowId.get(request.cow?.cowId || '') || [];
|
||||
// relations로 조회된 traitDetails 사용
|
||||
const details = request.traitDetails || [];
|
||||
if (details.length === 0) continue;
|
||||
|
||||
// 연도 추출
|
||||
@@ -290,14 +238,15 @@ export class GenomeService {
|
||||
}
|
||||
|
||||
// 형질별 평균 계산 (순위 계산을 위해 보은군 내 모든 농가 데이터 필요)
|
||||
// Step: 보은군 내 모든 농가의 형질별 평균 EBV 계산 (메모리에서 처리)
|
||||
// 보은군 내 모든 농가의 형질별 평균 EBV 계산
|
||||
const allFarmsTraitMap = new Map<number, Map<string, { sum: number; count: number }>>();
|
||||
|
||||
// 보은군 내 모든 분석 완료된 요청 조회
|
||||
// 보은군 내 모든 분석 완료된 요청 조회 (traitDetails 포함)
|
||||
const allRegionValidRequests = await this.genomeRequestRepository
|
||||
.createQueryBuilder('req')
|
||||
.leftJoinAndSelect('req.cow', 'cow')
|
||||
.leftJoinAndSelect('req.farm', 'farm')
|
||||
.leftJoinAndSelect('req.traitDetails', 'traitDetails')
|
||||
.where('req.delDt IS NULL')
|
||||
.andWhere('req.chipSireName = :match', { match: '일치' })
|
||||
.getMany();
|
||||
@@ -306,8 +255,8 @@ export class GenomeService {
|
||||
const reqFarmNo = req.fkFarmNo;
|
||||
if (!reqFarmNo) continue;
|
||||
|
||||
// Map에서 형질 데이터 조회 (DB 쿼리 없이 O(1) 조회)
|
||||
const details = traitDetailsByCowId.get(req.cow?.cowId || '') || [];
|
||||
// relations로 조회된 traitDetails 사용
|
||||
const details = req.traitDetails || [];
|
||||
if (details.length === 0) continue;
|
||||
|
||||
if (!allFarmsTraitMap.has(reqFarmNo)) {
|
||||
@@ -350,7 +299,7 @@ export class GenomeService {
|
||||
// 보은군 전체 형질별 평균 EPD 계산 (모든 농가의 모든 개체 데이터 사용)
|
||||
const regionTraitEpdMap = new Map<string, { sum: number; count: number }>();
|
||||
for (const req of allRegionValidRequests) {
|
||||
const details = traitDetailsByCowId.get(req.cow?.cowId || '') || [];
|
||||
const details = req.traitDetails || [];
|
||||
for (const detail of details) {
|
||||
if (detail.traitVal !== null && detail.traitName) {
|
||||
const traitName = detail.traitName;
|
||||
@@ -421,21 +370,21 @@ export class GenomeService {
|
||||
})),
|
||||
}));
|
||||
|
||||
// 보은군 전체 연도별 평균 계산을 위한 데이터 조회
|
||||
// 보은군 전체 연도별 평균 계산을 위한 데이터 조회 (traitDetails 포함)
|
||||
const allRegionRequests = await this.genomeRequestRepository.find({
|
||||
where: { delDt: IsNull() },
|
||||
relations: ['cow'],
|
||||
relations: ['cow', 'traitDetails'],
|
||||
});
|
||||
|
||||
// 보은군 연도별 형질 데이터 수집 (메모리에서 처리)
|
||||
// 보은군 연도별 형질 데이터 수집
|
||||
const regionYearlyTraitMap = new Map<number, Map<string, { sum: number; count: number }>>();
|
||||
for (const req of allRegionRequests) {
|
||||
if (!isValidGenomeAnalysis(req.chipSireName, req.chipDamName, req.cow?.cowId)) continue;
|
||||
|
||||
const year = req.requestDt ? new Date(req.requestDt).getFullYear() : new Date().getFullYear();
|
||||
|
||||
// Map에서 형질 데이터 조회 (DB 쿼리 없이 O(1) 조회)
|
||||
const details = traitDetailsByCowId.get(req.cow?.cowId || '') || [];
|
||||
// relations로 조회된 traitDetails 사용
|
||||
const details = req.traitDetails || [];
|
||||
if (details.length === 0) continue;
|
||||
|
||||
if (!regionYearlyTraitMap.has(year)) {
|
||||
@@ -565,12 +514,18 @@ export class GenomeService {
|
||||
const farmMptCowIds = mptCowIds.filter(id => farmCowIds.has(id));
|
||||
|
||||
// 합집합 계산 (중복 제외) - genomeRequest 포함 (리스트 페이지와 일치)
|
||||
const allTestedCowIds = new Set([
|
||||
...farmGenomeRequestCowIds,
|
||||
...farmGenomeCowIds,
|
||||
...farmGeneCowIds,
|
||||
...farmMptCowIds,
|
||||
]);
|
||||
const TEST_FARM_NO = 26; // 코쿤 테스트 농장
|
||||
const isTestFarm = farmNo === TEST_FARM_NO;
|
||||
|
||||
// 테스트 농장(26번)은 tb_cow 전체, 그 외는 검사 받은 개체만
|
||||
const allTestedCowIds = isTestFarm
|
||||
? farmCowIds
|
||||
: new Set([
|
||||
...farmGenomeRequestCowIds,
|
||||
...farmGenomeCowIds,
|
||||
...farmGeneCowIds,
|
||||
...farmMptCowIds,
|
||||
]);
|
||||
|
||||
const totalCows = allTestedCowIds.size;
|
||||
const genomeCowCount = farmGenomeCowIds.length;
|
||||
@@ -620,8 +575,8 @@ export class GenomeService {
|
||||
damMismatch: requests.filter(r => r.chipSireName === '일치' && r.chipDamName === '불일치').length,
|
||||
// 모 이력제부재 (부 일치 + 모 이력제부재)
|
||||
damNoRecord: requests.filter(r => r.chipSireName === '일치' && r.chipDamName === '이력제부재').length,
|
||||
// 대기 (chipSireName이 없는 경우)
|
||||
pending: requests.filter(r => !r.chipSireName).length,
|
||||
// 미분석 (chipSireName이 없는 경우)
|
||||
notAnalyzed: requests.filter(r => !r.chipSireName).length,
|
||||
};
|
||||
|
||||
// Step 8: 월별 접수 현황 (올해 기준)
|
||||
@@ -694,6 +649,7 @@ export class GenomeService {
|
||||
* 개체식별번호(cowId)로 유전체 데이터 조회
|
||||
* 가장 최근 분석 의뢰의 형질 상세 정보까지 포함하여 반환
|
||||
*
|
||||
* @usedBy /cow/[cowNo] - 개체 상세 페이지 (유전체 데이터 조회)
|
||||
* @param cowId - 개체식별번호 (예: KOR123456789)
|
||||
* @returns 유전체 분석 결과 배열
|
||||
* - request: 분석 의뢰 정보
|
||||
@@ -754,6 +710,7 @@ export class GenomeService {
|
||||
/**
|
||||
* 개체식별번호(cowId)로 유전체 분석 의뢰 정보 조회
|
||||
*
|
||||
* @usedBy /cow/[cowNo] - 개체 상세 페이지 (분석 의뢰 정보 조회)
|
||||
* @param cowId - 개체식별번호 (예: KOR002115897818)
|
||||
* @returns 최신 분석 의뢰 정보 (없으면 null)
|
||||
*/
|
||||
@@ -787,6 +744,7 @@ export class GenomeService {
|
||||
* 사용 목적: 특정 개체의 유전능력을 전국 평균, 같은 지역(군) 평균,
|
||||
* 같은 농장 평균과 비교하여 상대적 위치 파악
|
||||
*
|
||||
* @usedBy /cow/[cowNo] - 개체 상세 페이지 (카테고리별 레이더 차트)
|
||||
* @param cowId - 개체식별번호 (예: KOR123456789)
|
||||
* @returns 전국/지역/농장별 카테고리 평균 EBV
|
||||
* @throws NotFoundException - 개체를 찾을 수 없는 경우
|
||||
@@ -850,6 +808,7 @@ export class GenomeService {
|
||||
* 사용 목적: 폴리곤 차트에서 형질별로 정확한 비교를 위해
|
||||
* 전국/지역/농장 평균을 형질 단위로 제공
|
||||
*
|
||||
* @usedBy /cow/[cowNo] - 개체 상세 페이지 (형질별 폴리곤 차트)
|
||||
* @param cowId - 개체식별번호 (예: KOR123456789)
|
||||
* @returns 전국/지역/농장별 형질별 평균 EBV
|
||||
* @throws NotFoundException - 개체를 찾을 수 없는 경우
|
||||
@@ -1036,6 +995,7 @@ export class GenomeService {
|
||||
/**
|
||||
* 단일 개체 선발지수(가중 평균) 계산 + 전국/지역 순위
|
||||
*
|
||||
* @usedBy /cow/[cowNo] - 개체 상세 페이지 (선발지수 계산)
|
||||
* @param cowId - 개체식별번호 (예: KOR002119144049)
|
||||
* @param traitConditions - 형질별 가중치 조건 배열
|
||||
* @returns 선발지수 점수, 순위, 상세 내역
|
||||
@@ -1333,6 +1293,8 @@ export class GenomeService {
|
||||
|
||||
/**
|
||||
* 개별 형질 기준 순위 조회
|
||||
*
|
||||
* @usedBy /cow/[cowNo]/genome - 개체 상세 > 유전체 통합 비교, 정규분포 차트
|
||||
* @param cowId - 개체식별번호 (KOR...)
|
||||
* @param traitName - 형질명 (도체중, 근내지방도 등)
|
||||
*/
|
||||
@@ -1480,7 +1442,9 @@ export class GenomeService {
|
||||
|
||||
/**
|
||||
* 농가의 보은군 내 순위 조회 (대시보드용)
|
||||
* 성능 최적화: N+1 쿼리 문제 해결 - 모든 데이터를 한 번에 조회 후 메모리에서 처리
|
||||
* JOIN으로 한 번에 조회
|
||||
*
|
||||
* @usedBy /dashboard - 대시보드 페이지 (농가 순위 카드)
|
||||
* @param farmNo - 농장 번호
|
||||
*/
|
||||
async getFarmRegionRanking(
|
||||
@@ -1516,43 +1480,28 @@ export class GenomeService {
|
||||
};
|
||||
}
|
||||
|
||||
// 2. 모든 유전체 분석 의뢰 조회
|
||||
// 2. 모든 유전체 분석 의뢰 조회 (traitDetails 포함)
|
||||
const allRequests = await this.genomeRequestRepository.find({
|
||||
where: { delDt: IsNull() },
|
||||
relations: ['cow', 'farm'],
|
||||
relations: ['cow', 'farm', 'traitDetails'],
|
||||
});
|
||||
|
||||
// 3. 모든 형질 상세 데이터를 한 번에 조회 (N+1 문제 해결)
|
||||
const allTraitDetails = await this.genomeTraitDetailRepository.find({
|
||||
where: { delDt: IsNull() },
|
||||
});
|
||||
|
||||
// cowId로 형질 데이터를 그룹화 (Map으로 빠른 조회)
|
||||
const traitDetailsByCowId = new Map<string, typeof allTraitDetails>();
|
||||
for (const detail of allTraitDetails) {
|
||||
if (!detail.cowId) continue;
|
||||
if (!traitDetailsByCowId.has(detail.cowId)) {
|
||||
traitDetailsByCowId.set(detail.cowId, []);
|
||||
}
|
||||
traitDetailsByCowId.get(detail.cowId)!.push(detail);
|
||||
}
|
||||
|
||||
// 4. inputTraitConditions가 있으면 사용, 없으면 35개 형질 기본값 사용 (ALL_TRAITS는 공통 상수에서 import)
|
||||
// 3. inputTraitConditions가 있으면 사용, 없으면 35개 형질 기본값 사용 (ALL_TRAITS는 공통 상수에서 import)
|
||||
const traitConditions = inputTraitConditions && inputTraitConditions.length > 0
|
||||
? inputTraitConditions // 프론트에서 보낸 형질사용
|
||||
: ALL_TRAITS.map(traitNm => ({ traitNm, weight: 1 })); // 기본값 사용
|
||||
|
||||
console.log('[getFarmRegionRanking] traitConditions:', traitConditions.length, 'traits');
|
||||
|
||||
// 5. 각 개체별 점수 계산 (메모리에서 처리 - DB 쿼리 없음)
|
||||
// 4. 각 개체별 점수 계산
|
||||
const allScores: { cowId: string; score: number; farmNo: number | null }[] = [];
|
||||
|
||||
for (const request of allRequests) {
|
||||
if (!request.cow?.cowId) continue;
|
||||
if (!isValidGenomeAnalysis(request.chipSireName, request.chipDamName, request.cow?.cowId)) continue;
|
||||
|
||||
// Map에서 형질 데이터 조회 (DB 쿼리 없이 O(1) 조회)
|
||||
const traitDetails = traitDetailsByCowId.get(request.cow.cowId);
|
||||
// relations로 조회된 traitDetails 사용
|
||||
const traitDetails = request.traitDetails;
|
||||
if (!traitDetails || traitDetails.length === 0) continue;
|
||||
|
||||
let weightedSum = 0;
|
||||
@@ -1657,9 +1606,41 @@ export class GenomeService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 연도별 유전능력 추이 (형질별/카테고리별)
|
||||
* 최적화: N+1 쿼리 문제 해결 - 단일 쿼리로 모든 데이터 조회
|
||||
* 연도별 EBV 통계 조회 (개체상세용)
|
||||
* getDashboardStats의 yearlyStats와 yearlyAvgEbv 부분만 반환
|
||||
*
|
||||
* @usedBy /cow/[cowNo]/genome - 개체 상세 > 유전체 통합 비교
|
||||
* @param farmNo - 농장 번호
|
||||
*/
|
||||
async getYearlyEbvStats(farmNo: number): Promise<{
|
||||
yearlyStats: {
|
||||
year: number;
|
||||
totalRequests: number;
|
||||
analyzedCount: number;
|
||||
pendingCount: number;
|
||||
sireMatchCount: number;
|
||||
analyzeRate: number;
|
||||
sireMatchRate: number;
|
||||
}[];
|
||||
yearlyAvgEbv: {
|
||||
year: number;
|
||||
farmAvgEbv: number;
|
||||
regionAvgEbv: number;
|
||||
traitCount: number;
|
||||
}[];
|
||||
}> {
|
||||
const dashboardStats = await this.getDashboardStats(farmNo);
|
||||
return {
|
||||
yearlyStats: dashboardStats.yearlyStats,
|
||||
yearlyAvgEbv: dashboardStats.yearlyAvgEbv,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 연도별 유전능력 추이 (형질별/카테고리별)
|
||||
* JOIN으로 한 번에 조회
|
||||
*
|
||||
* @usedBy /dashboard - 대시보드 페이지 (연도별 추이 차트)
|
||||
* @param farmNo - 농장 번호
|
||||
* @param traitName - 형질명 (선택, 없으면 카테고리 전체)
|
||||
* @param category - 카테고리명 (성장/생산/체형/무게/비율)
|
||||
@@ -1695,8 +1676,7 @@ export class GenomeService {
|
||||
// 대상 형질 결정
|
||||
const targetTraits = traitName ? [traitName] : traitsInCategory;
|
||||
|
||||
// 단일 쿼리로 모든 데이터 조회 (N+1 문제 해결)
|
||||
// genome_request + cow + genome_trait_detail을 한번에 조인
|
||||
// JOIN으로 한 번에 조회 (genome_request + cow + genome_trait_detail)
|
||||
const allData = await this.genomeRequestRepository
|
||||
.createQueryBuilder('r')
|
||||
.innerJoin('r.cow', 'c')
|
||||
|
||||
Reference in New Issue
Block a user