미사용 파일정리
This commit is contained in:
@@ -5,6 +5,12 @@ import {
|
||||
isValidGenomeAnalysis,
|
||||
VALID_CHIP_SIRE_NAME
|
||||
} from '../common/config/GenomeAnalysisConfig';
|
||||
import {
|
||||
ALL_TRAITS,
|
||||
NEGATIVE_TRAITS,
|
||||
TRAIT_CATEGORY_MAP,
|
||||
getTraitCategory,
|
||||
} from '../common/const/TraitTypes';
|
||||
import { CowModel } from '../cow/entities/cow.entity';
|
||||
import { FarmModel } from '../farm/entities/farm.entity';
|
||||
import { GenomeRequestModel } from './entities/genome-request.entity';
|
||||
@@ -12,68 +18,6 @@ import { GenomeTraitDetailModel } from './entities/genome-trait-detail.entity';
|
||||
import { MptModel } from '../mpt/entities/mpt.entity';
|
||||
import { GeneDetailModel } from '../gene/entities/gene-detail.entity';
|
||||
|
||||
/**
|
||||
* 낮을수록 좋은 형질 목록 (부호 반전 필요)
|
||||
* - 등지방두께: 지방이 얇을수록(EBV가 낮을수록) 좋은 형질
|
||||
* - 선발지수 계산 시 EBV 부호를 반전하여 적용
|
||||
*/
|
||||
const NEGATIVE_TRAITS = ['등지방두께'];
|
||||
|
||||
/**
|
||||
* 형질명 → 카테고리 매핑 상수
|
||||
* - 성장: 월령별 체중 관련 형질
|
||||
* - 생산: 도체(도축 후 고기) 품질 관련 형질
|
||||
* - 체형: 소의 신체 구조/외형 관련 형질
|
||||
* - 무게: 각 부위별 실제 무게 (단위: kg)
|
||||
* - 비율: 각 부위별 비율 (단위: %)
|
||||
*/
|
||||
const TRAIT_CATEGORY_MAP: Record<string, string> = {
|
||||
// 성장 카테고리 - 월령별 체중
|
||||
'12개월령체중': '성장',
|
||||
|
||||
// 생산 카테고리 - 도체(도축 후 고기) 품질
|
||||
'도체중': '생산', // 도축 후 고기 무게
|
||||
'등심단면적': '생산', // 등심의 단면 크기 (넓을수록 좋음)
|
||||
'등지방두께': '생산', // 등 부위 지방 두께 (적당해야 좋음)
|
||||
'근내지방도': '생산', // 마블링 정도 (높을수록 고급육)
|
||||
|
||||
// 체형 카테고리 - 소의 신체 구조/외형
|
||||
'체고': '체형', // 어깨 높이
|
||||
'십자': '체형', // 십자부(엉덩이) 높이
|
||||
'체장': '체형', // 몸통 길이
|
||||
'흉심': '체형', // 가슴 깊이
|
||||
'흉폭': '체형', // 가슴 너비
|
||||
'고장': '체형', // 엉덩이 길이
|
||||
'요각폭': '체형', // 허리뼈 너비
|
||||
'곤폭': '체형', // 좌골(엉덩이뼈) 너비
|
||||
'좌골폭': '체형', // 좌골 너비
|
||||
'흉위': '체형', // 가슴둘레
|
||||
|
||||
// 무게 카테고리 - 부위별 실제 무게 (kg)
|
||||
'안심weight': '무게', // 안심 무게
|
||||
'등심weight': '무게', // 등심 무게
|
||||
'채끝weight': '무게', // 채끝 무게
|
||||
'목심weight': '무게', // 목심 무게
|
||||
'앞다리weight': '무게', // 앞다리 무게
|
||||
'우둔weight': '무게', // 우둔 무게
|
||||
'설도weight': '무게', // 설도 무게
|
||||
'사태weight': '무게', // 사태 무게
|
||||
'양지weight': '무게', // 양지 무게
|
||||
'갈비weight': '무게', // 갈비 무게
|
||||
|
||||
// 비율 카테고리 - 부위별 비율 (%)
|
||||
'안심rate': '비율', // 안심 비율
|
||||
'등심rate': '비율', // 등심 비율
|
||||
'채끝rate': '비율', // 채끝 비율
|
||||
'목심rate': '비율', // 목심 비율
|
||||
'앞다리rate': '비율', // 앞다리 비율
|
||||
'우둔rate': '비율', // 우둔 비율
|
||||
'설도rate': '비율', // 설도 비율
|
||||
'사태rate': '비율', // 사태 비율
|
||||
'양지rate': '비율', // 양지 비율
|
||||
'갈비rate': '비율', // 갈비 비율
|
||||
};
|
||||
|
||||
/**
|
||||
* 카테고리별 평균 EBV(추정육종가) 응답 DTO
|
||||
*/
|
||||
@@ -155,177 +99,6 @@ export class GenomeService {
|
||||
// 대시보드 통계 관련 메서드
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* 농가별 형질 비교 데이터 (농가 vs 지역 vs 전국)
|
||||
* - 각 형질별로 원본 EBV, 중요도(가중치), 적용 EBV
|
||||
* - 보은군 전체 평균, 농가 평균 비교
|
||||
*
|
||||
* @param farmNo - 농장 번호
|
||||
*/
|
||||
async getFarmTraitComparison(farmNo: number): Promise<{
|
||||
farmName: string;
|
||||
regionName: string;
|
||||
totalFarmAnimals: number;
|
||||
totalRegionAnimals: number;
|
||||
traits: {
|
||||
traitName: string;
|
||||
category: string;
|
||||
// 농가 데이터
|
||||
farmAvgEbv: number;
|
||||
farmCount: number;
|
||||
farmPercentile: number;
|
||||
// 지역(보은군) 데이터
|
||||
regionAvgEbv: number;
|
||||
regionCount: number;
|
||||
// 전국 데이터
|
||||
nationAvgEbv: number;
|
||||
nationCount: number;
|
||||
// 비교
|
||||
diffFromRegion: number; // 지역 대비 차이
|
||||
diffFromNation: number; // 전국 대비 차이
|
||||
}[];
|
||||
}> {
|
||||
// Step 1: 농장 정보 조회
|
||||
const farm = await this.farmRepository.findOne({
|
||||
where: { pkFarmNo: farmNo, delDt: IsNull() },
|
||||
});
|
||||
|
||||
const regionSi = farm?.regionSi || '보은군';
|
||||
const farmName = farm?.farmerName || '농장';
|
||||
|
||||
// Step 2: 농가의 분석 완료된 개체들의 형질 데이터 조회
|
||||
const farmRequestsRaw = await this.genomeRequestRepository.find({
|
||||
where: { fkFarmNo: farmNo, chipSireName: VALID_CHIP_SIRE_NAME, delDt: IsNull() },
|
||||
relations: ['cow'],
|
||||
});
|
||||
// 유효 조건 필터 적용 (chipDamName 제외 조건 + cowId 제외 목록)
|
||||
const farmRequests = farmRequestsRaw.filter(r =>
|
||||
isValidGenomeAnalysis(r.chipSireName, r.chipDamName, r.cow?.cowId)
|
||||
);
|
||||
|
||||
const farmTraitMap = new Map<string, { sum: number; percentileSum: number; count: number; category: string }>();
|
||||
|
||||
for (const request of farmRequests) {
|
||||
// cowId로 직접 형질 데이터 조회
|
||||
const details = await this.genomeTraitDetailRepository.find({
|
||||
where: { cowId: request.cow?.cowId, delDt: IsNull() },
|
||||
});
|
||||
if (details.length === 0) continue;
|
||||
|
||||
for (const detail of details) {
|
||||
if (detail.traitEbv !== null && detail.traitName) {
|
||||
const traitName = detail.traitName;
|
||||
const category = TRAIT_CATEGORY_MAP[traitName] || '기타';
|
||||
|
||||
if (!farmTraitMap.has(traitName)) {
|
||||
farmTraitMap.set(traitName, { sum: 0, percentileSum: 0, count: 0, category });
|
||||
}
|
||||
|
||||
const t = farmTraitMap.get(traitName)!;
|
||||
t.sum += Number(detail.traitEbv);
|
||||
t.percentileSum += Number(detail.traitPercentile) || 50;
|
||||
t.count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Step 3: 지역(보은군) 전체 형질 데이터 조회
|
||||
const regionDetails = await this.genomeTraitDetailRepository
|
||||
.createQueryBuilder('detail')
|
||||
.innerJoin('tb_genome_request', 'request', 'detail.fk_request_no = request.pk_request_no')
|
||||
.innerJoin('tb_farm', 'farm', 'request.fk_farm_no = farm.pk_farm_no')
|
||||
.where('detail.delDt IS NULL')
|
||||
.andWhere('detail.traitEbv IS NOT NULL')
|
||||
.andWhere('request.chip_sire_name = :match', { match: '일치' })
|
||||
.andWhere('farm.region_si = :regionSi', { regionSi })
|
||||
.select(['detail.traitName', 'detail.traitEbv'])
|
||||
.getRawMany();
|
||||
|
||||
const regionTraitMap = new Map<string, { sum: number; count: number }>();
|
||||
for (const detail of regionDetails) {
|
||||
const traitName = detail.detail_trait_name;
|
||||
const ebv = parseFloat(detail.detail_trait_ebv);
|
||||
if (!traitName || isNaN(ebv)) continue;
|
||||
|
||||
if (!regionTraitMap.has(traitName)) {
|
||||
regionTraitMap.set(traitName, { sum: 0, count: 0 });
|
||||
}
|
||||
const t = regionTraitMap.get(traitName)!;
|
||||
t.sum += ebv;
|
||||
t.count++;
|
||||
}
|
||||
|
||||
// Step 4: 전국 형질 데이터 조회
|
||||
const nationDetails = await this.genomeTraitDetailRepository
|
||||
.createQueryBuilder('detail')
|
||||
.innerJoin('tb_genome_request', 'request', 'detail.fk_request_no = request.pk_request_no')
|
||||
.where('detail.delDt IS NULL')
|
||||
.andWhere('detail.traitEbv IS NOT NULL')
|
||||
.andWhere('request.chip_sire_name = :match', { match: '일치' })
|
||||
.select(['detail.traitName', 'detail.traitEbv'])
|
||||
.getRawMany();
|
||||
|
||||
const nationTraitMap = new Map<string, { sum: number; count: number }>();
|
||||
for (const detail of nationDetails) {
|
||||
const traitName = detail.detail_trait_name;
|
||||
const ebv = parseFloat(detail.detail_trait_ebv);
|
||||
if (!traitName || isNaN(ebv)) continue;
|
||||
|
||||
if (!nationTraitMap.has(traitName)) {
|
||||
nationTraitMap.set(traitName, { sum: 0, count: 0 });
|
||||
}
|
||||
const t = nationTraitMap.get(traitName)!;
|
||||
t.sum += ebv;
|
||||
t.count++;
|
||||
}
|
||||
|
||||
// Step 5: 결과 조합 (35개 전체 형질)
|
||||
const traits: any[] = [];
|
||||
const allTraits = Object.keys(TRAIT_CATEGORY_MAP);
|
||||
|
||||
for (const traitName of allTraits) {
|
||||
const farmData = farmTraitMap.get(traitName);
|
||||
const regionData = regionTraitMap.get(traitName);
|
||||
const nationData = nationTraitMap.get(traitName);
|
||||
|
||||
const farmAvgEbv = farmData ? Math.round((farmData.sum / farmData.count) * 100) / 100 : 0;
|
||||
const farmPercentile = farmData ? Math.round((farmData.percentileSum / farmData.count) * 100) / 100 : 50;
|
||||
const regionAvgEbv = regionData ? Math.round((regionData.sum / regionData.count) * 100) / 100 : 0;
|
||||
const nationAvgEbv = nationData ? Math.round((nationData.sum / nationData.count) * 100) / 100 : 0;
|
||||
|
||||
traits.push({
|
||||
traitName,
|
||||
category: TRAIT_CATEGORY_MAP[traitName] || '기타',
|
||||
farmAvgEbv,
|
||||
farmCount: farmData?.count || 0,
|
||||
farmPercentile,
|
||||
regionAvgEbv,
|
||||
regionCount: regionData?.count || 0,
|
||||
nationAvgEbv,
|
||||
nationCount: nationData?.count || 0,
|
||||
diffFromRegion: Math.round((farmAvgEbv - regionAvgEbv) * 100) / 100,
|
||||
diffFromNation: Math.round((farmAvgEbv - nationAvgEbv) * 100) / 100,
|
||||
});
|
||||
}
|
||||
|
||||
// 지역 개체 수 계산
|
||||
const regionAnimalCount = await this.genomeRequestRepository
|
||||
.createQueryBuilder('request')
|
||||
.innerJoin('tb_farm', 'farm', 'request.fk_farm_no = farm.pk_farm_no')
|
||||
.where('request.chip_sire_name = :match', { match: '일치' })
|
||||
.andWhere('request.del_dt IS NULL')
|
||||
.andWhere('farm.region_si = :regionSi', { regionSi })
|
||||
.getCount();
|
||||
|
||||
return {
|
||||
farmName,
|
||||
regionName: regionSi,
|
||||
totalFarmAnimals: farmRequests.length,
|
||||
totalRegionAnimals: regionAnimalCount,
|
||||
traits,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 대시보드용 농가 통계 데이터
|
||||
* - 연도별 분석 현황
|
||||
@@ -917,35 +690,6 @@ export class GenomeService {
|
||||
// 유전체 분석 의뢰 (Genome Request) 관련 메서드
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* 전체 유전체 분석 의뢰 목록 조회
|
||||
*
|
||||
* @returns 삭제되지 않은 모든 분석 의뢰 목록
|
||||
* - cow, farm 관계 데이터 포함
|
||||
* - 등록일(regDt) 기준 내림차순 정렬 (최신순)
|
||||
*/
|
||||
async findAllRequests(): Promise<GenomeRequestModel[]> {
|
||||
return this.genomeRequestRepository.find({
|
||||
where: { delDt: IsNull() }, // 삭제되지 않은 데이터만
|
||||
relations: ['cow', 'farm'], // 개체, 농장 정보 JOIN
|
||||
order: { regDt: 'DESC' }, // 최신순 정렬
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 개체 PK 번호로 해당 개체의 분석 의뢰 목록 조회
|
||||
*
|
||||
* @param cowNo - 개체 PK 번호 (pkCowNo)
|
||||
* @returns 해당 개체의 모든 분석 의뢰 목록 (최신순)
|
||||
*/
|
||||
async findRequestsByCowId(cowNo: number): Promise<GenomeRequestModel[]> {
|
||||
return this.genomeRequestRepository.find({
|
||||
where: { fkCowNo: cowNo, delDt: IsNull() },
|
||||
relations: ['cow', 'farm'],
|
||||
order: { regDt: 'DESC' },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 개체식별번호(cowId)로 유전체 데이터 조회
|
||||
* 가장 최근 분석 의뢰의 형질 상세 정보까지 포함하여 반환
|
||||
@@ -991,19 +735,7 @@ export class GenomeService {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Step 4: 형질명으로 카테고리를 추정하는 내부 함수
|
||||
const getCategoryFromTraitName = (traitName: string): string => {
|
||||
// 성장 카테고리: 월령별 체중
|
||||
if (['12개월령체중', '18개월령체중', '24개월령체중'].includes(traitName)) return '성장';
|
||||
// 생산 카테고리: 도체 품질
|
||||
if (['도체중', '등심단면적', '등지방두께', '근내지방도'].includes(traitName)) return '생산';
|
||||
// 체형 카테고리: 신체 구조
|
||||
if (traitName.includes('체형') || traitName.includes('체고') || traitName.includes('십자부')) return '체형';
|
||||
// 그 외 기타
|
||||
return '기타';
|
||||
};
|
||||
|
||||
// Step 5: 프론트엔드에서 사용할 형식으로 데이터 가공하여 반환
|
||||
// Step 4: 프론트엔드에서 사용할 형식으로 데이터 가공하여 반환
|
||||
return [{
|
||||
request: latestRequest, // 분석 의뢰 정보
|
||||
genomeCows: traitDetails.map(detail => ({
|
||||
@@ -1012,27 +744,13 @@ export class GenomeService {
|
||||
percentile: detail.traitPercentile, // 백분위 순위
|
||||
traitInfo: {
|
||||
traitNm: detail.traitName, // 형질명
|
||||
traitCtgry: getCategoryFromTraitName(detail.traitName || ''), // 카테고리
|
||||
traitCtgry: getTraitCategory(detail.traitName || ''), // 카테고리 (공통 함수 사용)
|
||||
traitDesc: '', // 형질 설명 (빈값)
|
||||
},
|
||||
})),
|
||||
}];
|
||||
}
|
||||
|
||||
/**
|
||||
* 농장 PK 번호로 해당 농장의 분석 의뢰 목록 조회
|
||||
*
|
||||
* @param farmNo - 농장 PK 번호 (pkFarmNo)
|
||||
* @returns 해당 농장의 모든 분석 의뢰 목록 (최신순)
|
||||
*/
|
||||
async findRequestsByFarmId(farmNo: number): Promise<GenomeRequestModel[]> {
|
||||
return this.genomeRequestRepository.find({
|
||||
where: { fkFarmNo: farmNo, delDt: IsNull() },
|
||||
relations: ['cow', 'farm'],
|
||||
order: { regDt: 'DESC' },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 개체식별번호(cowId)로 유전체 분석 의뢰 정보 조회
|
||||
*
|
||||
@@ -1059,61 +777,6 @@ export class GenomeService {
|
||||
return request || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* ===========================================================================================
|
||||
* 유전체 분석 요청 관련 메서드
|
||||
* ===========================================================================================
|
||||
* 새로운 유전체 분석 의뢰 생성
|
||||
*
|
||||
* @param data - 생성할 분석 의뢰 데이터 (Partial: 일부 필드만 입력 가능)
|
||||
* @returns 생성된 분석 의뢰 엔티티
|
||||
*/
|
||||
async createRequest(data: Partial<GenomeRequestModel>): Promise<GenomeRequestModel> {
|
||||
// 엔티티 인스턴스 생성
|
||||
const request = this.genomeRequestRepository.create(data);
|
||||
// DB에 저장 후 반환
|
||||
return this.genomeRequestRepository.save(request);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 형질 상세 (Genome Trait Detail) 관련 메서드
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* 분석 의뢰 PK로 해당 의뢰의 형질 상세 목록 조회
|
||||
*
|
||||
* @param requestNo - 분석 의뢰 PK 번호 (pkRequestNo)
|
||||
* @returns 해당 의뢰의 모든 형질 상세 목록
|
||||
*/
|
||||
async findTraitDetailsByRequestId(requestNo: number): Promise<GenomeTraitDetailModel[]> {
|
||||
return this.genomeTraitDetailRepository.find({
|
||||
where: { fkRequestNo: requestNo, delDt: IsNull() },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* cowId로 해당 개체의 형질 상세 목록 조회
|
||||
*
|
||||
* @param cowId - 개체식별번호 (KOR...)
|
||||
* @returns 해당 개체의 모든 형질 상세 목록
|
||||
*/
|
||||
async findTraitDetailsByCowId(cowId: string): Promise<GenomeTraitDetailModel[]> {
|
||||
return this.genomeTraitDetailRepository.find({
|
||||
where: { cowId: cowId, delDt: IsNull() },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 새로운 형질 상세 데이터 생성
|
||||
*
|
||||
* @param data - 생성할 형질 상세 데이터
|
||||
* @returns 생성된 형질 상세 엔티티
|
||||
*/
|
||||
async createTraitDetail(data: Partial<GenomeTraitDetailModel>): Promise<GenomeTraitDetailModel> {
|
||||
const detail = this.genomeTraitDetailRepository.create(data);
|
||||
return this.genomeTraitDetailRepository.save(detail);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 비교 분석 (Comparison) 관련 메서드
|
||||
// ============================================
|
||||
@@ -1874,17 +1537,7 @@ export class GenomeService {
|
||||
traitDetailsByCowId.get(detail.cowId)!.push(detail);
|
||||
}
|
||||
|
||||
// 4. 35개 전체 형질 조건 (기본값)
|
||||
const ALL_TRAITS = [
|
||||
'12개월령체중',
|
||||
'도체중', '등심단면적', '등지방두께', '근내지방도',
|
||||
'체고', '십자', '체장', '흉심', '흉폭', '고장', '요각폭', '좌골폭', '곤폭', '흉위',
|
||||
'안심weight', '등심weight', '채끝weight', '목심weight', '앞다리weight',
|
||||
'우둔weight', '설도weight', '사태weight', '양지weight', '갈비weight',
|
||||
'안심rate', '등심rate', '채끝rate', '목심rate', '앞다리rate',
|
||||
'우둔rate', '설도rate', '사태rate', '양지rate', '갈비rate',
|
||||
];
|
||||
// inputTraitConditions가 있으면 사용, 없으면 35개 형질 기본값 사용
|
||||
// 4. inputTraitConditions가 있으면 사용, 없으면 35개 형질 기본값 사용 (ALL_TRAITS는 공통 상수에서 import)
|
||||
const traitConditions = inputTraitConditions && inputTraitConditions.length > 0
|
||||
? inputTraitConditions // 프론트에서 보낸 형질사용
|
||||
: ALL_TRAITS.map(traitNm => ({ traitNm, weight: 1 })); // 기본값 사용
|
||||
@@ -2003,41 +1656,6 @@ export class GenomeService {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 개체들의 상세 정보 조회 (디버깅용)
|
||||
*/
|
||||
async checkSpecificCows(cowIds: string[]): Promise<any[]> {
|
||||
const results = [];
|
||||
for (const cowId of cowIds) {
|
||||
const request = await this.genomeRequestRepository.findOne({
|
||||
where: { delDt: IsNull() },
|
||||
relations: ['cow'],
|
||||
});
|
||||
|
||||
// cowId로 조회
|
||||
const cow = await this.cowRepository.findOne({
|
||||
where: { cowId, delDt: IsNull() },
|
||||
});
|
||||
|
||||
if (cow) {
|
||||
const req = await this.genomeRequestRepository.findOne({
|
||||
where: { fkCowNo: cow.pkCowNo, delDt: IsNull() },
|
||||
});
|
||||
|
||||
results.push({
|
||||
cowId,
|
||||
chipSireName: req?.chipSireName,
|
||||
chipDamName: req?.chipDamName,
|
||||
requestDt: req?.requestDt,
|
||||
cowRemarks: req?.cowRemarks,
|
||||
});
|
||||
} else {
|
||||
results.push({ cowId, error: 'cow not found' });
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* 연도별 유전능력 추이 (형질별/카테고리별)
|
||||
* 최적화: N+1 쿼리 문제 해결 - 단일 쿼리로 모든 데이터 조회
|
||||
|
||||
Reference in New Issue
Block a user