service 로직 수정3
This commit is contained in:
@@ -23,6 +23,9 @@ export const VALID_CHIP_SIRE_NAME = '일치';
|
|||||||
/** 제외할 어미 칩 이름 값 목록 */
|
/** 제외할 어미 칩 이름 값 목록 */
|
||||||
export const INVALID_CHIP_DAM_NAMES = ['불일치', '이력제부재'];
|
export const INVALID_CHIP_DAM_NAMES = ['불일치', '이력제부재'];
|
||||||
|
|
||||||
|
/** 순위/평균 집계 대상 지역 (이 지역만 집계에 포함, 테스트/기관 계정은 다른 regionSi 사용) */
|
||||||
|
export const VALID_REGION = '보은군';
|
||||||
|
|
||||||
/** ===============================개별 제외 개체 목록 (분석불가 등 특수 사유) 하단 개체 정보없음 확인필요=================*/
|
/** ===============================개별 제외 개체 목록 (분석불가 등 특수 사유) 하단 개체 정보없음 확인필요=================*/
|
||||||
export const EXCLUDED_COW_IDS = [
|
export const EXCLUDED_COW_IDS = [
|
||||||
'KOR002191642861',
|
'KOR002191642861',
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { InjectRepository } from '@nestjs/typeorm';
|
|||||||
import { IsNull, Repository } from 'typeorm';
|
import { IsNull, Repository } from 'typeorm';
|
||||||
import {
|
import {
|
||||||
isValidGenomeAnalysis,
|
isValidGenomeAnalysis,
|
||||||
|
VALID_REGION,
|
||||||
} from '../common/config/GenomeAnalysisConfig';
|
} from '../common/config/GenomeAnalysisConfig';
|
||||||
import {
|
import {
|
||||||
ALL_TRAITS,
|
ALL_TRAITS,
|
||||||
@@ -151,6 +152,12 @@ export class GenomeService {
|
|||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
console.log('[Dashboard] 시작');
|
console.log('[Dashboard] 시작');
|
||||||
|
|
||||||
|
// Step 0: 조회자의 농장 정보 확인 (테스트 농가 여부 판단)
|
||||||
|
const viewerFarm = await this.farmRepository.findOne({
|
||||||
|
where: { pkFarmNo: farmNo, delDt: IsNull() },
|
||||||
|
});
|
||||||
|
const isViewerTestFarm = viewerFarm?.regionSi !== VALID_REGION;
|
||||||
|
|
||||||
// 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() },
|
||||||
@@ -245,12 +252,13 @@ export class GenomeService {
|
|||||||
// DB 집계로 최적화 + 병렬 실행
|
// DB 집계로 최적화 + 병렬 실행
|
||||||
console.log(`[Dashboard] Step3 DB 집계 쿼리 시작 (병렬): ${Date.now() - startTime}ms`);
|
console.log(`[Dashboard] Step3 DB 집계 쿼리 시작 (병렬): ${Date.now() - startTime}ms`);
|
||||||
|
|
||||||
// 1, 2번 쿼리 병렬 실행
|
// 1, 2번 쿼리 병렬 실행 (보은군 농가만, 테스트 농가가 조회 시 자기 데이터도 포함)
|
||||||
const [farmTraitAvgResults, regionEpdResults] = await Promise.all([
|
const [farmTraitAvgResults, regionEpdResults] = await Promise.all([
|
||||||
// 1. 농가별 형질 평균 EBV (DB 집계)
|
// 1. 농가별 형질 평균 EBV (DB 집계)
|
||||||
this.genomeTraitDetailRepository
|
this.genomeTraitDetailRepository
|
||||||
.createQueryBuilder('detail')
|
.createQueryBuilder('detail')
|
||||||
.innerJoin('detail.genomeRequest', 'req')
|
.innerJoin('detail.genomeRequest', 'req')
|
||||||
|
.innerJoin('tb_farm', 'farm', 'req.fkFarmNo = farm.pkFarmNo')
|
||||||
.select('req.fkFarmNo', 'farmNo')
|
.select('req.fkFarmNo', 'farmNo')
|
||||||
.addSelect('detail.traitName', 'traitName')
|
.addSelect('detail.traitName', 'traitName')
|
||||||
.addSelect('AVG(detail.traitEbv)', 'avgEbv')
|
.addSelect('AVG(detail.traitEbv)', 'avgEbv')
|
||||||
@@ -258,14 +266,18 @@ export class GenomeService {
|
|||||||
.andWhere('req.delDt IS NULL')
|
.andWhere('req.delDt IS NULL')
|
||||||
.andWhere('req.chipSireName = :match', { match: '일치' })
|
.andWhere('req.chipSireName = :match', { match: '일치' })
|
||||||
.andWhere('detail.traitEbv IS NOT NULL')
|
.andWhere('detail.traitEbv IS NOT NULL')
|
||||||
|
// 보은군 농가 또는 조회자 자신(테스트 농가일 때)
|
||||||
|
.andWhere('(farm.regionSi = :region OR req.fkFarmNo = :viewerFarmNo)',
|
||||||
|
{ region: VALID_REGION, viewerFarmNo: isViewerTestFarm ? farmNo : -1 })
|
||||||
.groupBy('req.fkFarmNo')
|
.groupBy('req.fkFarmNo')
|
||||||
.addGroupBy('detail.traitName')
|
.addGroupBy('detail.traitName')
|
||||||
.getRawMany(),
|
.getRawMany(),
|
||||||
|
|
||||||
// 2. 보은군 전체 형질별 평균 EPD (DB 집계)
|
// 2. 보은군 전체 형질별 평균 EPD (DB 집계) - 순수하게 보은군만
|
||||||
this.genomeTraitDetailRepository
|
this.genomeTraitDetailRepository
|
||||||
.createQueryBuilder('detail')
|
.createQueryBuilder('detail')
|
||||||
.innerJoin('detail.genomeRequest', 'req')
|
.innerJoin('detail.genomeRequest', 'req')
|
||||||
|
.innerJoin('tb_farm', 'farm', 'req.fkFarmNo = farm.pkFarmNo')
|
||||||
.select('detail.traitName', 'traitName')
|
.select('detail.traitName', 'traitName')
|
||||||
.addSelect('AVG(detail.traitVal)', 'avgEpd')
|
.addSelect('AVG(detail.traitVal)', 'avgEpd')
|
||||||
.addSelect('COUNT(*)', 'count')
|
.addSelect('COUNT(*)', 'count')
|
||||||
@@ -273,6 +285,7 @@ export class GenomeService {
|
|||||||
.andWhere('req.delDt IS NULL')
|
.andWhere('req.delDt IS NULL')
|
||||||
.andWhere('req.chipSireName = :match', { match: '일치' })
|
.andWhere('req.chipSireName = :match', { match: '일치' })
|
||||||
.andWhere('detail.traitVal IS NOT NULL')
|
.andWhere('detail.traitVal IS NOT NULL')
|
||||||
|
.andWhere('farm.regionSi = :region', { region: VALID_REGION })
|
||||||
.groupBy('detail.traitName')
|
.groupBy('detail.traitName')
|
||||||
.getRawMany(),
|
.getRawMany(),
|
||||||
]);
|
]);
|
||||||
@@ -363,10 +376,11 @@ export class GenomeService {
|
|||||||
})),
|
})),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// 3. 보은군 연도별 형질 데이터 (DB 집계)
|
// 3. 보은군 연도별 형질 데이터 (DB 집계) - 순수하게 보은군만
|
||||||
const regionYearlyResults = await this.genomeTraitDetailRepository
|
const regionYearlyResults = await this.genomeTraitDetailRepository
|
||||||
.createQueryBuilder('detail')
|
.createQueryBuilder('detail')
|
||||||
.innerJoin('detail.genomeRequest', 'req')
|
.innerJoin('detail.genomeRequest', 'req')
|
||||||
|
.innerJoin('tb_farm', 'farm', 'req.fkFarmNo = farm.pkFarmNo')
|
||||||
.select('EXTRACT(YEAR FROM req.requestDt)', 'year')
|
.select('EXTRACT(YEAR FROM req.requestDt)', 'year')
|
||||||
.addSelect('detail.traitName', 'traitName')
|
.addSelect('detail.traitName', 'traitName')
|
||||||
.addSelect('SUM(detail.traitEbv)', 'sum')
|
.addSelect('SUM(detail.traitEbv)', 'sum')
|
||||||
@@ -376,6 +390,7 @@ export class GenomeService {
|
|||||||
.andWhere('req.chipSireName = :match', { match: '일치' })
|
.andWhere('req.chipSireName = :match', { match: '일치' })
|
||||||
.andWhere('detail.traitEbv IS NOT NULL')
|
.andWhere('detail.traitEbv IS NOT NULL')
|
||||||
.andWhere('req.requestDt IS NOT NULL')
|
.andWhere('req.requestDt IS NOT NULL')
|
||||||
|
.andWhere('farm.regionSi = :region', { region: VALID_REGION })
|
||||||
.groupBy('EXTRACT(YEAR FROM req.requestDt)')
|
.groupBy('EXTRACT(YEAR FROM req.requestDt)')
|
||||||
.addGroupBy('detail.traitName')
|
.addGroupBy('detail.traitName')
|
||||||
.getRawMany();
|
.getRawMany();
|
||||||
@@ -512,12 +527,22 @@ export class GenomeService {
|
|||||||
console.log(`[Dashboard] Step5 4개 테이블 조회 완료: ${Date.now() - startTime}ms`);
|
console.log(`[Dashboard] Step5 4개 테이블 조회 완료: ${Date.now() - startTime}ms`);
|
||||||
|
|
||||||
// 합집합 계산 (중복 제외) - genomeRequest 포함 (리스트 페이지와 일치)
|
// 합집합 계산 (중복 제외) - genomeRequest 포함 (리스트 페이지와 일치)
|
||||||
const TEST_FARM_NO = 26; // 코쿤 테스트 농장
|
const isTestFarm = viewerFarm?.regionSi !== VALID_REGION;
|
||||||
const isTestFarm = farmNo === TEST_FARM_NO;
|
|
||||||
|
|
||||||
// 테스트 농장(26번)은 tb_cow 전체, 그 외는 검사 받은 개체만
|
/**
|
||||||
|
* 대시보드 개체 수 집계 로직
|
||||||
|
*
|
||||||
|
* [일반 농가 (regionSi = '보은군')]
|
||||||
|
* - 실제 검사(유전체/유전자/번식능력)를 받은 개체만 집계
|
||||||
|
* - 검사 데이터가 없는 개체는 대시보드에 표시되지 않음
|
||||||
|
*
|
||||||
|
* [테스트/기관 농가 (regionSi != '보은군', 예: '코쿤')]
|
||||||
|
* - 검사 여부와 관계없이 농장 소유 전체 개체를 집계
|
||||||
|
* - UI 테스트 목적으로 가짜 데이터 없이도 개체 목록 확인 가능
|
||||||
|
* - 실제 운영 데이터에는 영향 없음 (순위/평균 계산에서 제외됨)
|
||||||
|
*/
|
||||||
const allTestedCowIds = isTestFarm
|
const allTestedCowIds = isTestFarm
|
||||||
? farmCowIds
|
? farmCowIds // 테스트 농가: 모든 소
|
||||||
: new Set([
|
: new Set([
|
||||||
...farmGenomeRequestCowIds,
|
...farmGenomeRequestCowIds,
|
||||||
...farmGenomeCowIds,
|
...farmGenomeCowIds,
|
||||||
@@ -1202,6 +1227,11 @@ export class GenomeService {
|
|||||||
for (const request of allRequests) {
|
for (const request of allRequests) {
|
||||||
if (!request.cow?.cowId) continue;
|
if (!request.cow?.cowId) continue;
|
||||||
|
|
||||||
|
// 보은군 농가만 포함 (테스트 농가가 조회 시 자기 데이터도 포함)
|
||||||
|
const isValidRegion = request.farm?.regionSi === VALID_REGION;
|
||||||
|
const isViewerOwnData = request.fkFarmNo === farmNo;
|
||||||
|
if (!isValidRegion && !isViewerOwnData) continue;
|
||||||
|
|
||||||
// 친자감별 결과가 '일치'인 경우만 포함 (분석불가 개체 제외)
|
// 친자감별 결과가 '일치'인 경우만 포함 (분석불가 개체 제외)
|
||||||
if (!isValidGenomeAnalysis(request.chipSireName, request.chipDamName, request.cow?.cowId)) continue;
|
if (!isValidGenomeAnalysis(request.chipSireName, request.chipDamName, request.cow?.cowId)) continue;
|
||||||
|
|
||||||
@@ -1366,6 +1396,12 @@ export class GenomeService {
|
|||||||
|
|
||||||
for (const request of allRequests) {
|
for (const request of allRequests) {
|
||||||
if (!request.cow?.cowId) continue;
|
if (!request.cow?.cowId) continue;
|
||||||
|
|
||||||
|
// 보은군 농가만 포함 (테스트 농가가 조회 시 자기 데이터도 포함)
|
||||||
|
const isValidRegion = request.farm?.regionSi === VALID_REGION;
|
||||||
|
const isViewerOwnData = request.fkFarmNo === farmNo;
|
||||||
|
if (!isValidRegion && !isViewerOwnData) continue;
|
||||||
|
|
||||||
if (!isValidGenomeAnalysis(request.chipSireName, request.chipDamName, request.cow?.cowId)) continue;
|
if (!isValidGenomeAnalysis(request.chipSireName, request.chipDamName, request.cow?.cowId)) continue;
|
||||||
|
|
||||||
const traitDetail = await this.genomeTraitDetailRepository.findOne({
|
const traitDetail = await this.genomeTraitDetailRepository.findOne({
|
||||||
@@ -1498,6 +1534,12 @@ export class GenomeService {
|
|||||||
|
|
||||||
for (const request of allRequests) {
|
for (const request of allRequests) {
|
||||||
if (!request.cow?.cowId) continue;
|
if (!request.cow?.cowId) continue;
|
||||||
|
|
||||||
|
// 보은군 농가만 포함 (테스트 농가가 조회 시 자기 데이터도 포함)
|
||||||
|
const isValidRegion = request.farm?.regionSi === VALID_REGION;
|
||||||
|
const isViewerOwnData = request.fkFarmNo === farmNo;
|
||||||
|
if (!isValidRegion && !isViewerOwnData) continue;
|
||||||
|
|
||||||
if (!isValidGenomeAnalysis(request.chipSireName, request.chipDamName, request.cow?.cowId)) continue;
|
if (!isValidGenomeAnalysis(request.chipSireName, request.chipDamName, request.cow?.cowId)) continue;
|
||||||
|
|
||||||
// relations로 조회된 traitDetails 사용
|
// relations로 조회된 traitDetails 사용
|
||||||
@@ -1676,10 +1718,11 @@ export class GenomeService {
|
|||||||
// 대상 형질 결정
|
// 대상 형질 결정
|
||||||
const targetTraits = traitName ? [traitName] : traitsInCategory;
|
const targetTraits = traitName ? [traitName] : traitsInCategory;
|
||||||
|
|
||||||
// JOIN으로 한 번에 조회 (genome_request + cow + genome_trait_detail)
|
// JOIN으로 한 번에 조회 (genome_request + cow + genome_trait_detail + farm)
|
||||||
const allData = await this.genomeRequestRepository
|
const allData = await this.genomeRequestRepository
|
||||||
.createQueryBuilder('r')
|
.createQueryBuilder('r')
|
||||||
.innerJoin('r.cow', 'c')
|
.innerJoin('r.cow', 'c')
|
||||||
|
.innerJoin('r.farm', 'f')
|
||||||
.innerJoin(
|
.innerJoin(
|
||||||
GenomeTraitDetailModel,
|
GenomeTraitDetailModel,
|
||||||
'd',
|
'd',
|
||||||
@@ -1692,6 +1735,7 @@ export class GenomeService {
|
|||||||
.addSelect('EXTRACT(YEAR FROM r.request_dt)', 'year')
|
.addSelect('EXTRACT(YEAR FROM r.request_dt)', 'year')
|
||||||
.addSelect('d.trait_name', 'traitName')
|
.addSelect('d.trait_name', 'traitName')
|
||||||
.addSelect('d.trait_val', 'traitVal')
|
.addSelect('d.trait_val', 'traitVal')
|
||||||
|
.addSelect('f.region_si', 'regionSi')
|
||||||
.where('r.del_dt IS NULL')
|
.where('r.del_dt IS NULL')
|
||||||
.andWhere('d.trait_name IN (:...targetTraits)', { targetTraits })
|
.andWhere('d.trait_name IN (:...targetTraits)', { targetTraits })
|
||||||
.getRawMany();
|
.getRawMany();
|
||||||
@@ -1700,6 +1744,7 @@ export class GenomeService {
|
|||||||
const cowDataMap = new Map<string, {
|
const cowDataMap = new Map<string, {
|
||||||
farmNo: number;
|
farmNo: number;
|
||||||
year: number;
|
year: number;
|
||||||
|
regionSi: string | null;
|
||||||
chipSireName: string | null;
|
chipSireName: string | null;
|
||||||
chipDamName: string | null;
|
chipDamName: string | null;
|
||||||
traits: { traitName: string; traitVal: number }[];
|
traits: { traitName: string; traitVal: number }[];
|
||||||
@@ -1713,6 +1758,7 @@ export class GenomeService {
|
|||||||
cowDataMap.set(cowId, {
|
cowDataMap.set(cowId, {
|
||||||
farmNo: row.reqFarmNo,
|
farmNo: row.reqFarmNo,
|
||||||
year: row.year || new Date().getFullYear(),
|
year: row.year || new Date().getFullYear(),
|
||||||
|
regionSi: row.regionSi || null,
|
||||||
chipSireName: row.chipSireName,
|
chipSireName: row.chipSireName,
|
||||||
chipDamName: row.chipDamName,
|
chipDamName: row.chipDamName,
|
||||||
traits: [],
|
traits: [],
|
||||||
@@ -1734,6 +1780,12 @@ export class GenomeService {
|
|||||||
for (const [cowId, data] of cowDataMap) {
|
for (const [cowId, data] of cowDataMap) {
|
||||||
// 유효한 분석인지 확인
|
// 유효한 분석인지 확인
|
||||||
if (!isValidGenomeAnalysis(data.chipSireName, data.chipDamName, cowId)) continue;
|
if (!isValidGenomeAnalysis(data.chipSireName, data.chipDamName, cowId)) continue;
|
||||||
|
|
||||||
|
// 보은군 농가만 포함 (테스트 농가가 조회 시 자기 데이터도 포함)
|
||||||
|
const isValidRegion = data.regionSi === VALID_REGION;
|
||||||
|
const isViewerOwnData = data.farmNo === farmNo;
|
||||||
|
if (!isValidRegion && !isViewerOwnData) continue;
|
||||||
|
|
||||||
if (data.traits.length === 0) continue;
|
if (data.traits.length === 0) continue;
|
||||||
|
|
||||||
// 대상 형질의 평균 육종가 계산
|
// 대상 형질의 평균 육종가 계산
|
||||||
|
|||||||
Reference in New Issue
Block a user