service 로직 수정2
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user