From c84dc1e96d522ac999c4ca9c600c987b807da9bd Mon Sep 17 00:00:00 2001 From: chu eun ju Date: Thu, 25 Dec 2025 16:47:37 +0900 Subject: [PATCH] =?UTF-8?q?service=20=EB=A1=9C=EC=A7=81=20=EC=88=98?= =?UTF-8?q?=EC=A0=952?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/genome/genome.service.ts | 196 ++++++++++++++------------- 1 file changed, 99 insertions(+), 97 deletions(-) diff --git a/backend/src/genome/genome.service.ts b/backend/src/genome/genome.service.ts index d2e81e0..055ca47 100644 --- a/backend/src/genome/genome.service.ts +++ b/backend/src/genome/genome.service.ts @@ -148,12 +148,16 @@ export class GenomeService { traitCount: number; }[]; }> { + const startTime = Date.now(); + console.log('[Dashboard] 시작'); + // Step 1: 농장의 모든 분석 의뢰 조회 (traitDetails 포함) const requests = await this.genomeRequestRepository.find({ where: { fkFarmNo: farmNo, delDt: IsNull() }, relations: ['cow', 'traitDetails'], order: { requestDt: 'DESC', regDt: 'DESC' }, }); + console.log(`[Dashboard] Step1 농장 요청 조회: ${Date.now() - startTime}ms`); // Step 2: 연도별 통계 계산 const yearMap = new Map(); @@ -238,61 +242,52 @@ export class GenomeService { } // 형질별 평균 계산 (순위 계산을 위해 보은군 내 모든 농가 데이터 필요) - // 보은군 내 모든 농가의 형질별 평균 EBV 계산 - const allFarmsTraitMap = new Map>(); + // DB 집계로 최적화 + 병렬 실행 + console.log(`[Dashboard] Step3 DB 집계 쿼리 시작 (병렬): ${Date.now() - startTime}ms`); - // 보은군 전체 요청 조회 (한번만 조회하여 재사용) - const allRegionRequests = await this.genomeRequestRepository - .createQueryBuilder('req') - .leftJoinAndSelect('req.cow', 'cow') - .leftJoinAndSelect('req.farm', 'farm') - .leftJoinAndSelect('req.traitDetails', 'traitDetails') - .where('req.delDt IS NULL') - .getMany(); + // 1, 2번 쿼리 병렬 실행 + const [farmTraitAvgResults, regionEpdResults] = await Promise.all([ + // 1. 농가별 형질 평균 EBV (DB 집계) + this.genomeTraitDetailRepository + .createQueryBuilder('detail') + .innerJoin('detail.genomeRequest', 'req') + .select('req.fkFarmNo', 'farmNo') + .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 = '일치') - const allRegionValidRequests = allRegionRequests.filter( - r => r.chipSireName === '일치' - ); + // 2. 보은군 전체 형질별 평균 EPD (DB 집계) + this.genomeTraitDetailRepository + .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) { - 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 계산 및 순위 정렬 + // 형질별로 모든 농가의 평균 EBV 정렬 (순위용) const traitRankingMap = new Map(); - for (const [reqFarmNo, traitsMap] of allFarmsTraitMap) { - for (const [traitName, data] of traitsMap) { - if (!traitRankingMap.has(traitName)) { - traitRankingMap.set(traitName, []); - } - traitRankingMap.get(traitName)!.push({ - farmNo: reqFarmNo, - avgEbv: data.sum / data.count, - }); + for (const row of farmTraitAvgResults) { + const traitName = row.traitName; + const farmNo = Number(row.farmNo); + const avgEbv = parseFloat(row.avgEbv); + + if (!traitRankingMap.has(traitName)) { + traitRankingMap.set(traitName, []); } + traitRankingMap.get(traitName)!.push({ farmNo, avgEbv }); } // 각 형질별로 EBV 내림차순 정렬 (높을수록 좋음) @@ -300,23 +295,17 @@ export class GenomeService { farms.sort((a, b) => b.avgEbv - a.avgEbv); } - // 보은군 전체 형질별 평균 EPD 계산 (모든 농가의 모든 개체 데이터 사용) const regionTraitEpdMap = new Map(); - for (const req of allRegionValidRequests) { - const details = req.traitDetails || []; - for (const detail of details) { - if (detail.traitVal !== null && detail.traitName) { - const traitName = detail.traitName; - if (!regionTraitEpdMap.has(traitName)) { - regionTraitEpdMap.set(traitName, { sum: 0, count: 0 }); - } - const t = regionTraitEpdMap.get(traitName)!; - t.sum += Number(detail.traitVal); - t.count++; - } - } + for (const row of regionEpdResults) { + const count = parseInt(row.count); + regionTraitEpdMap.set(row.traitName, { + sum: parseFloat(row.avgEpd) * count, // 평균 * count = 합계 + count, + }); } + console.log(`[Dashboard] Step3 DB 집계 쿼리 완료: ${Date.now() - startTime}ms`); + // 형질별 평균 및 순위 계산 (표준 경쟁 순위 방식: 동률 시 같은 순위, 다음 순위 건너뜀) const traitAverages = Array.from(traitDataMap.entries()).map(([traitName, data]) => { 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>(); - 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(); - - // relations로 조회된 traitDetails 사용 - const details = req.traitDetails || []; - if (details.length === 0) continue; + for (const row of regionYearlyResults) { + const year = parseInt(row.year); + const traitName = row.traitName; if (!regionYearlyTraitMap.has(year)) { regionYearlyTraitMap.set(year, new Map()); } - const yearTraits = regionYearlyTraitMap.get(year)!; - - for (const detail of details) { - 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++; - } + regionYearlyTraitMap.get(year)!.set(traitName, { + sum: parseFloat(row.sum), + count: parseInt(row.count), + }); } // 연도별 평균 표준화육종가 (농가 vs 보은군 비교) @@ -472,44 +464,52 @@ export class GenomeService { const farmCowIds = new Set(farmCows.map(c => c.cowId).filter(Boolean)); const farmCowMap = new Map(farmCows.map(c => [c.cowId, c])); - // 각 검사별 cowId 조회 (병렬 처리) - genomeRequest도 포함 (리스트 페이지와 일치) - const [genomeRequestCowIds, genomeCowIds, geneCowIds, mptCowIds] = await Promise.all([ - // 유전체 분석 의뢰가 있는 개체 (분석불가 포함) + // 각 검사별 cowId 조회 (병렬 처리 + cow JOIN으로 farmNo 조건) + console.log(`[Dashboard] Step5 4개 테이블 조회 시작: ${Date.now() - startTime}ms`); + + const [farmGenomeRequestCowIds, farmGenomeCowIds, farmGeneCowIds, farmMptCowIds] = await Promise.all([ + // 유전체 분석 의뢰가 있는 개체 (cow JOIN으로 farmNo 필터) this.genomeRequestRepository .createQueryBuilder('request') .innerJoin('request.cow', 'cow') .select('DISTINCT cow.cowId', 'cowId') .where('request.delDt IS NULL') + .andWhere('cow.fkFarmNo = :farmNo', { farmNo }) + .andWhere('cow.delDt IS NULL') .getRawMany() .then(rows => rows.map((r: { cowId: string }) => r.cowId).filter(Boolean)), - // 유전체 분석 개체 (형질 데이터 보유) + // 유전체 분석 개체 (cow JOIN으로 farmNo 필터) this.genomeTraitDetailRepository .createQueryBuilder('trait') + .innerJoin('tb_cow', 'cow', 'trait.cowId = cow.cowId') .select('DISTINCT trait.cowId', 'cowId') .where('trait.delDt IS NULL') + .andWhere('cow.fkFarmNo = :farmNo', { farmNo }) + .andWhere('cow.delDt IS NULL') .getRawMany() .then(rows => rows.map((r: { cowId: string }) => r.cowId).filter(Boolean)), - // 유전자검사 개체 + // 유전자검사 개체 (cow JOIN으로 farmNo 필터) this.geneDetailRepository .createQueryBuilder('gene') + .innerJoin('tb_cow', 'cow', 'gene.cowId = cow.cowId') .select('DISTINCT gene.cowId', 'cowId') .where('gene.delDt IS NULL') + .andWhere('cow.fkFarmNo = :farmNo', { farmNo }) + .andWhere('cow.delDt IS NULL') .getRawMany() .then(rows => rows.map((r: { cowId: string }) => r.cowId).filter(Boolean)), - // 번식능력검사 개체 + // 번식능력검사 개체 (cow JOIN으로 farmNo 필터) this.mptRepository .createQueryBuilder('mpt') + .innerJoin('tb_cow', 'cow', 'mpt.cowId = cow.cowId') .select('DISTINCT mpt.cowId', 'cowId') .where('mpt.delDt IS NULL') + .andWhere('cow.fkFarmNo = :farmNo', { farmNo }) + .andWhere('cow.delDt IS NULL') .getRawMany() .then(rows => rows.map((r: { cowId: string }) => r.cowId).filter(Boolean)), ]); - - // 농장 소유 개체만 필터링 - 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)); + console.log(`[Dashboard] Step5 4개 테이블 조회 완료: ${Date.now() - startTime}ms`); // 합집합 계산 (중복 제외) - genomeRequest 포함 (리스트 페이지와 일치) const TEST_FARM_NO = 26; // 코쿤 테스트 농장 @@ -613,6 +613,8 @@ export class GenomeService { count, })); + console.log(`[Dashboard] 완료: ${Date.now() - startTime}ms`); + return { yearlyStats, traitAverages,