미사용 파일정리

This commit is contained in:
2025-12-24 08:25:44 +09:00
parent 1644fcf241
commit 05d89fdfcd
120 changed files with 817 additions and 85913 deletions

View File

@@ -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 쿼리 문제 해결 - 단일 쿼리로 모든 데이터 조회