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;
}[];
}> {
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<number, { total: number; analyzed: number; pending: number; sireMatch: number }>();
@@ -238,61 +242,52 @@ export class GenomeService {
}
// 형질별 평균 계산 (순위 계산을 위해 보은군 내 모든 농가 데이터 필요)
// 보은군 내 모든 농가의 형질별 평균 EBV 계산
const allFarmsTraitMap = new Map<number, Map<string, { sum: number; count: number }>>();
// 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<string, { farmNo: number; avgEbv: number }[]>();
for (const [reqFarmNo, traitsMap] of allFarmsTraitMap) {
for (const [traitName, data] of traitsMap) {
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: reqFarmNo,
avgEbv: data.sum / data.count,
});
}
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<string, { sum: number; count: number }>();
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<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();
// 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,