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