service 로직 수정2

This commit is contained in:
2025-12-25 16:47:37 +09:00
parent 0d1663e698
commit c84dc1e96d

View File

@@ -148,12 +148,16 @@ export class GenomeService {
traitCount: number; traitCount: number;
}[]; }[];
}> { }> {
const startTime = Date.now();
console.log('[Dashboard] 시작');
// Step 1: 농장의 모든 분석 의뢰 조회 (traitDetails 포함) // Step 1: 농장의 모든 분석 의뢰 조회 (traitDetails 포함)
const requests = await this.genomeRequestRepository.find({ const requests = await this.genomeRequestRepository.find({
where: { fkFarmNo: farmNo, delDt: IsNull() }, where: { fkFarmNo: farmNo, delDt: IsNull() },
relations: ['cow', 'traitDetails'], relations: ['cow', 'traitDetails'],
order: { requestDt: 'DESC', regDt: 'DESC' }, order: { requestDt: 'DESC', regDt: 'DESC' },
}); });
console.log(`[Dashboard] Step1 농장 요청 조회: ${Date.now() - startTime}ms`);
// Step 2: 연도별 통계 계산 // Step 2: 연도별 통계 계산
const yearMap = new Map<number, { total: number; analyzed: number; pending: number; sireMatch: number }>(); const yearMap = new Map<number, { total: number; analyzed: number; pending: number; sireMatch: number }>();
@@ -238,61 +242,52 @@ export class GenomeService {
} }
// 형질별 평균 계산 (순위 계산을 위해 보은군 내 모든 농가 데이터 필요) // 형질별 평균 계산 (순위 계산을 위해 보은군 내 모든 농가 데이터 필요)
// 보은군 내 모든 농가의 형질별 평균 EBV 계산 // DB 집계로 최적화 + 병렬 실행
const allFarmsTraitMap = new Map<number, Map<string, { sum: number; count: number }>>(); console.log(`[Dashboard] Step3 DB 집계 쿼리 시작 (병렬): ${Date.now() - startTime}ms`);
// 보은군 전체 요청 조회 (한번만 조회하여 재사용) // 1, 2번 쿼리 병렬 실행
const allRegionRequests = await this.genomeRequestRepository const [farmTraitAvgResults, regionEpdResults] = await Promise.all([
.createQueryBuilder('req') // 1. 농가별 형질 평균 EBV (DB 집계)
.leftJoinAndSelect('req.cow', 'cow') this.genomeTraitDetailRepository
.leftJoinAndSelect('req.farm', 'farm') .createQueryBuilder('detail')
.leftJoinAndSelect('req.traitDetails', 'traitDetails') .innerJoin('detail.genomeRequest', 'req')
.where('req.delDt IS NULL') .select('req.fkFarmNo', 'farmNo')
.getMany(); .addSelect('detail.traitName', 'traitName')
.addSelect('AVG(detail.traitEbv)', 'avgEbv')
.where('detail.delDt IS NULL')
.andWhere('req.delDt IS NULL')
.andWhere('req.chipSireName = :match', { match: '일치' })
.andWhere('detail.traitEbv IS NOT NULL')
.groupBy('req.fkFarmNo')
.addGroupBy('detail.traitName')
.getRawMany(),
// 분석 완료된 요청만 필터링 (chipSireName = '일치') // 2. 보은군 전체 형질별 평균 EPD (DB 집계)
const allRegionValidRequests = allRegionRequests.filter( this.genomeTraitDetailRepository
r => r.chipSireName === '일치' .createQueryBuilder('detail')
); .innerJoin('detail.genomeRequest', 'req')
.select('detail.traitName', 'traitName')
.addSelect('AVG(detail.traitVal)', 'avgEpd')
.addSelect('COUNT(*)', 'count')
.where('detail.delDt IS NULL')
.andWhere('req.delDt IS NULL')
.andWhere('req.chipSireName = :match', { match: '일치' })
.andWhere('detail.traitVal IS NOT NULL')
.groupBy('detail.traitName')
.getRawMany(),
]);
for (const req of allRegionValidRequests) { // 형질별로 모든 농가의 평균 EBV 정렬 (순위용)
const reqFarmNo = req.fkFarmNo;
if (!reqFarmNo) continue;
// relations로 조회된 traitDetails 사용
const details = req.traitDetails || [];
if (details.length === 0) continue;
if (!allFarmsTraitMap.has(reqFarmNo)) {
allFarmsTraitMap.set(reqFarmNo, new Map());
}
const farmTraits = allFarmsTraitMap.get(reqFarmNo)!;
for (const detail of details) {
if (detail.traitEbv !== null && detail.traitName) {
const traitName = detail.traitName;
if (!farmTraits.has(traitName)) {
farmTraits.set(traitName, { sum: 0, count: 0 });
}
const t = farmTraits.get(traitName)!;
t.sum += Number(detail.traitEbv);
t.count++;
}
}
}
// 형질별로 모든 농가의 평균 EBV 계산 및 순위 정렬
const traitRankingMap = new Map<string, { farmNo: number; avgEbv: number }[]>(); const traitRankingMap = new Map<string, { farmNo: number; avgEbv: number }[]>();
for (const [reqFarmNo, traitsMap] of allFarmsTraitMap) { for (const row of farmTraitAvgResults) {
for (const [traitName, data] of traitsMap) { const traitName = row.traitName;
if (!traitRankingMap.has(traitName)) { const farmNo = Number(row.farmNo);
traitRankingMap.set(traitName, []); const avgEbv = parseFloat(row.avgEbv);
}
traitRankingMap.get(traitName)!.push({ if (!traitRankingMap.has(traitName)) {
farmNo: reqFarmNo, traitRankingMap.set(traitName, []);
avgEbv: data.sum / data.count,
});
} }
traitRankingMap.get(traitName)!.push({ farmNo, avgEbv });
} }
// 각 형질별로 EBV 내림차순 정렬 (높을수록 좋음) // 각 형질별로 EBV 내림차순 정렬 (높을수록 좋음)
@@ -300,23 +295,17 @@ export class GenomeService {
farms.sort((a, b) => b.avgEbv - a.avgEbv); farms.sort((a, b) => b.avgEbv - a.avgEbv);
} }
// 보은군 전체 형질별 평균 EPD 계산 (모든 농가의 모든 개체 데이터 사용)
const regionTraitEpdMap = new Map<string, { sum: number; count: number }>(); const regionTraitEpdMap = new Map<string, { sum: number; count: number }>();
for (const req of allRegionValidRequests) { for (const row of regionEpdResults) {
const details = req.traitDetails || []; const count = parseInt(row.count);
for (const detail of details) { regionTraitEpdMap.set(row.traitName, {
if (detail.traitVal !== null && detail.traitName) { sum: parseFloat(row.avgEpd) * count, // 평균 * count = 합계
const traitName = detail.traitName; count,
if (!regionTraitEpdMap.has(traitName)) { });
regionTraitEpdMap.set(traitName, { sum: 0, count: 0 });
}
const t = regionTraitEpdMap.get(traitName)!;
t.sum += Number(detail.traitVal);
t.count++;
}
}
} }
console.log(`[Dashboard] Step3 DB 집계 쿼리 완료: ${Date.now() - startTime}ms`);
// 형질별 평균 및 순위 계산 (표준 경쟁 순위 방식: 동률 시 같은 순위, 다음 순위 건너뜀) // 형질별 평균 및 순위 계산 (표준 경쟁 순위 방식: 동률 시 같은 순위, 다음 순위 건너뜀)
const traitAverages = Array.from(traitDataMap.entries()).map(([traitName, data]) => { const traitAverages = Array.from(traitDataMap.entries()).map(([traitName, data]) => {
const avgEbv = Math.round((data.sum / data.count) * 100) / 100; const avgEbv = Math.round((data.sum / data.count) * 100) / 100;
@@ -374,32 +363,35 @@ export class GenomeService {
})), })),
})); }));
// 보은군 연도별 형질 데이터 수집 (위에서 조회한 allRegionRequests 재사용) // 3. 보은군 연도별 형질 데이터 (DB 집계)
const regionYearlyResults = await this.genomeTraitDetailRepository
.createQueryBuilder('detail')
.innerJoin('detail.genomeRequest', 'req')
.select('EXTRACT(YEAR FROM req.requestDt)', 'year')
.addSelect('detail.traitName', 'traitName')
.addSelect('SUM(detail.traitEbv)', 'sum')
.addSelect('COUNT(*)', 'count')
.where('detail.delDt IS NULL')
.andWhere('req.delDt IS NULL')
.andWhere('req.chipSireName = :match', { match: '일치' })
.andWhere('detail.traitEbv IS NOT NULL')
.andWhere('req.requestDt IS NOT NULL')
.groupBy('EXTRACT(YEAR FROM req.requestDt)')
.addGroupBy('detail.traitName')
.getRawMany();
const regionYearlyTraitMap = new Map<number, Map<string, { sum: number; count: number }>>(); const regionYearlyTraitMap = new Map<number, Map<string, { sum: number; count: number }>>();
for (const req of allRegionRequests) { for (const row of regionYearlyResults) {
if (!isValidGenomeAnalysis(req.chipSireName, req.chipDamName, req.cow?.cowId)) continue; const year = parseInt(row.year);
const traitName = row.traitName;
const year = req.requestDt ? new Date(req.requestDt).getFullYear() : new Date().getFullYear();
// relations로 조회된 traitDetails 사용
const details = req.traitDetails || [];
if (details.length === 0) continue;
if (!regionYearlyTraitMap.has(year)) { if (!regionYearlyTraitMap.has(year)) {
regionYearlyTraitMap.set(year, new Map()); regionYearlyTraitMap.set(year, new Map());
} }
const yearTraits = regionYearlyTraitMap.get(year)!; regionYearlyTraitMap.get(year)!.set(traitName, {
sum: parseFloat(row.sum),
for (const detail of details) { count: parseInt(row.count),
if (detail.traitEbv === null) continue; });
const traitName = detail.traitName;
if (!yearTraits.has(traitName)) {
yearTraits.set(traitName, { sum: 0, count: 0 });
}
const traitData = yearTraits.get(traitName)!;
traitData.sum += detail.traitEbv;
traitData.count++;
}
} }
// 연도별 평균 표준화육종가 (농가 vs 보은군 비교) // 연도별 평균 표준화육종가 (농가 vs 보은군 비교)
@@ -472,44 +464,52 @@ export class GenomeService {
const farmCowIds = new Set(farmCows.map(c => c.cowId).filter(Boolean)); const farmCowIds = new Set(farmCows.map(c => c.cowId).filter(Boolean));
const farmCowMap = new Map(farmCows.map(c => [c.cowId, c])); const farmCowMap = new Map(farmCows.map(c => [c.cowId, c]));
// 각 검사별 cowId 조회 (병렬 처리) - genomeRequest도 포함 (리스트 페이지와 일치) // 각 검사별 cowId 조회 (병렬 처리 + cow JOIN으로 farmNo 조건)
const [genomeRequestCowIds, genomeCowIds, geneCowIds, mptCowIds] = await Promise.all([ console.log(`[Dashboard] Step5 4개 테이블 조회 시작: ${Date.now() - startTime}ms`);
// 유전체 분석 의뢰가 있는 개체 (분석불가 포함)
const [farmGenomeRequestCowIds, farmGenomeCowIds, farmGeneCowIds, farmMptCowIds] = await Promise.all([
// 유전체 분석 의뢰가 있는 개체 (cow JOIN으로 farmNo 필터)
this.genomeRequestRepository this.genomeRequestRepository
.createQueryBuilder('request') .createQueryBuilder('request')
.innerJoin('request.cow', 'cow') .innerJoin('request.cow', 'cow')
.select('DISTINCT cow.cowId', 'cowId') .select('DISTINCT cow.cowId', 'cowId')
.where('request.delDt IS NULL') .where('request.delDt IS NULL')
.andWhere('cow.fkFarmNo = :farmNo', { farmNo })
.andWhere('cow.delDt IS NULL')
.getRawMany() .getRawMany()
.then(rows => rows.map((r: { cowId: string }) => r.cowId).filter(Boolean)), .then(rows => rows.map((r: { cowId: string }) => r.cowId).filter(Boolean)),
// 유전체 분석 개체 (형질 데이터 보유) // 유전체 분석 개체 (cow JOIN으로 farmNo 필터)
this.genomeTraitDetailRepository this.genomeTraitDetailRepository
.createQueryBuilder('trait') .createQueryBuilder('trait')
.innerJoin('tb_cow', 'cow', 'trait.cowId = cow.cowId')
.select('DISTINCT trait.cowId', 'cowId') .select('DISTINCT trait.cowId', 'cowId')
.where('trait.delDt IS NULL') .where('trait.delDt IS NULL')
.andWhere('cow.fkFarmNo = :farmNo', { farmNo })
.andWhere('cow.delDt IS NULL')
.getRawMany() .getRawMany()
.then(rows => rows.map((r: { cowId: string }) => r.cowId).filter(Boolean)), .then(rows => rows.map((r: { cowId: string }) => r.cowId).filter(Boolean)),
// 유전자검사 개체 // 유전자검사 개체 (cow JOIN으로 farmNo 필터)
this.geneDetailRepository this.geneDetailRepository
.createQueryBuilder('gene') .createQueryBuilder('gene')
.innerJoin('tb_cow', 'cow', 'gene.cowId = cow.cowId')
.select('DISTINCT gene.cowId', 'cowId') .select('DISTINCT gene.cowId', 'cowId')
.where('gene.delDt IS NULL') .where('gene.delDt IS NULL')
.andWhere('cow.fkFarmNo = :farmNo', { farmNo })
.andWhere('cow.delDt IS NULL')
.getRawMany() .getRawMany()
.then(rows => rows.map((r: { cowId: string }) => r.cowId).filter(Boolean)), .then(rows => rows.map((r: { cowId: string }) => r.cowId).filter(Boolean)),
// 번식능력검사 개체 // 번식능력검사 개체 (cow JOIN으로 farmNo 필터)
this.mptRepository this.mptRepository
.createQueryBuilder('mpt') .createQueryBuilder('mpt')
.innerJoin('tb_cow', 'cow', 'mpt.cowId = cow.cowId')
.select('DISTINCT mpt.cowId', 'cowId') .select('DISTINCT mpt.cowId', 'cowId')
.where('mpt.delDt IS NULL') .where('mpt.delDt IS NULL')
.andWhere('cow.fkFarmNo = :farmNo', { farmNo })
.andWhere('cow.delDt IS NULL')
.getRawMany() .getRawMany()
.then(rows => rows.map((r: { cowId: string }) => r.cowId).filter(Boolean)), .then(rows => rows.map((r: { cowId: string }) => r.cowId).filter(Boolean)),
]); ]);
console.log(`[Dashboard] Step5 4개 테이블 조회 완료: ${Date.now() - startTime}ms`);
// 농장 소유 개체만 필터링
const farmGenomeRequestCowIds = genomeRequestCowIds.filter(id => farmCowIds.has(id));
const farmGenomeCowIds = genomeCowIds.filter(id => farmCowIds.has(id));
const farmGeneCowIds = geneCowIds.filter(id => farmCowIds.has(id));
const farmMptCowIds = mptCowIds.filter(id => farmCowIds.has(id));
// 합집합 계산 (중복 제외) - genomeRequest 포함 (리스트 페이지와 일치) // 합집합 계산 (중복 제외) - genomeRequest 포함 (리스트 페이지와 일치)
const TEST_FARM_NO = 26; // 코쿤 테스트 농장 const TEST_FARM_NO = 26; // 코쿤 테스트 농장
@@ -613,6 +613,8 @@ export class GenomeService {
count, count,
})); }));
console.log(`[Dashboard] 완료: ${Date.now() - startTime}ms`);
return { return {
yearlyStats, yearlyStats,
traitAverages, traitAverages,