service 로직 수정3

This commit is contained in:
2025-12-26 07:58:40 +09:00
parent c84dc1e96d
commit 5204000d34
2 changed files with 63 additions and 8 deletions

View File

@@ -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',

View File

@@ -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<string, {
farmNo: number;
year: number;
regionSi: string | null;
chipSireName: string | null;
chipDamName: string | null;
traits: { traitName: string; traitVal: number }[];
@@ -1713,6 +1758,7 @@ export class GenomeService {
cowDataMap.set(cowId, {
farmNo: row.reqFarmNo,
year: row.year || new Date().getFullYear(),
regionSi: row.regionSi || null,
chipSireName: row.chipSireName,
chipDamName: row.chipDamName,
traits: [],
@@ -1734,6 +1780,12 @@ export class GenomeService {
for (const [cowId, data] of cowDataMap) {
// 유효한 분석인지 확인
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;
// 대상 형질의 평균 육종가 계산