Files
genome2025/backend/src/dashboard/dashboard.service.ts
2025-12-09 17:02:27 +09:00

549 lines
16 KiB
TypeScript

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, IsNull } from 'typeorm';
import { CowModel } from '../cow/entities/cow.entity';
import { FarmModel } from '../farm/entities/farm.entity';
import { GenomeRequestModel } from '../genome/entities/genome-request.entity';
import { GenomeTraitDetailModel } from '../genome/entities/genome-trait-detail.entity';
import { DashboardFilterDto } from './dto/dashboard-filter.dto';
import { isValidGenomeAnalysis, VALID_CHIP_SIRE_NAME } from '../common/config/GenomeAnalysisConfig';
@Injectable()
export class DashboardService {
constructor(
@InjectRepository(CowModel)
private readonly cowRepository: Repository<CowModel>,
@InjectRepository(FarmModel)
private readonly farmRepository: Repository<FarmModel>,
@InjectRepository(GenomeRequestModel)
private readonly genomeRequestRepository: Repository<GenomeRequestModel>,
@InjectRepository(GenomeTraitDetailModel)
private readonly genomeTraitDetailRepository: Repository<GenomeTraitDetailModel>,
) {}
/**
* 농장 현황 요약
*/
async getFarmSummary(farmNo: number, filter?: DashboardFilterDto) {
// 농장 정보 조회
const farm = await this.farmRepository.findOne({
where: { pkFarmNo: farmNo, delDt: IsNull() },
});
// 농장 소 목록 조회
const cows = await this.cowRepository.find({
where: { fkFarmNo: farmNo, delDt: IsNull() },
});
const totalCowCount = cows.length;
const maleCowCount = cows.filter(cow => cow.cowSex === 'M').length;
const femaleCowCount = cows.filter(cow => cow.cowSex === 'F').length;
return {
farmNo,
farmName: farm?.farmerName || '농장',
totalCowCount,
maleCowCount,
femaleCowCount,
};
}
/**
* 분석 완료 현황
*/
async getAnalysisCompletion(farmNo: number, filter?: DashboardFilterDto) {
// 농장의 모든 유전체 분석 의뢰 조회
const requests = await this.genomeRequestRepository.find({
where: { fkFarmNo: farmNo, delDt: IsNull() },
relations: ['cow'],
});
const farmAnlysCnt = requests.length;
const matchCnt = requests.filter(r => r.chipSireName === '일치').length;
const failCnt = requests.filter(r => r.chipSireName && r.chipSireName !== '일치').length;
const noHistCnt = requests.filter(r => !r.chipSireName).length;
return {
farmAnlysCnt,
matchCnt,
failCnt,
noHistCnt,
paternities: requests.map(r => ({
cowNo: r.fkCowNo,
cowId: r.cow?.cowId,
fatherMatch: r.chipSireName === '일치' ? '일치' : (r.chipSireName ? '불일치' : '미확인'),
requestDt: r.requestDt,
})),
};
}
/**
* 농장 종합 평가
*/
async getFarmEvaluation(farmNo: number, filter?: DashboardFilterDto) {
const cows = await this.cowRepository.find({
where: { fkFarmNo: farmNo, delDt: IsNull() },
});
// 각 개체의 유전체 점수 계산
const scores: number[] = [];
for (const cow of cows) {
// cowId로 직접 형질 데이터 조회
const traitDetails = await this.genomeTraitDetailRepository.find({
where: { cowId: cow.cowId, delDt: IsNull() },
});
if (traitDetails.length === 0) continue;
// 모든 형질의 EBV 평균 계산
const ebvValues = traitDetails
.filter(d => d.traitEbv !== null)
.map(d => Number(d.traitEbv));
if (ebvValues.length > 0) {
const avgEbv = ebvValues.reduce((sum, v) => sum + v, 0) / ebvValues.length;
scores.push(avgEbv);
}
}
const farmAverage = scores.length > 0
? scores.reduce((sum, s) => sum + s, 0) / scores.length
: 0;
// 등급 산정 (표준화육종가 기준)
let grade = 'C';
if (farmAverage >= 1.0) grade = 'A';
else if (farmAverage >= 0.5) grade = 'B';
else if (farmAverage >= -0.5) grade = 'C';
else if (farmAverage >= -1.0) grade = 'D';
else grade = 'E';
return {
farmNo,
farmAverage: Math.round(farmAverage * 100) / 100,
grade,
analyzedCount: scores.length,
totalCount: cows.length,
};
}
/**
* 보은군 비교 분석
*/
async getRegionComparison(farmNo: number, filter?: DashboardFilterDto) {
// 내 농장 평균 계산
const farmEval = await this.getFarmEvaluation(farmNo, filter);
// 전체 농장 평균 계산 (보은군 대비)
const allFarms = await this.farmRepository.find({
where: { delDt: IsNull() },
});
const farmScores: { farmNo: number; avgScore: number }[] = [];
for (const farm of allFarms) {
const farmCows = await this.cowRepository.find({
where: { fkFarmNo: farm.pkFarmNo, delDt: IsNull() },
});
const scores: number[] = [];
for (const cow of farmCows) {
// cowId로 직접 형질 데이터 조회
const traitDetails = await this.genomeTraitDetailRepository.find({
where: { cowId: cow.cowId, delDt: IsNull() },
});
if (traitDetails.length === 0) continue;
const ebvValues = traitDetails
.filter(d => d.traitEbv !== null)
.map(d => Number(d.traitEbv));
if (ebvValues.length > 0) {
const avgEbv = ebvValues.reduce((sum, v) => sum + v, 0) / ebvValues.length;
scores.push(avgEbv);
}
}
if (scores.length > 0) {
farmScores.push({
farmNo: farm.pkFarmNo,
avgScore: scores.reduce((sum, s) => sum + s, 0) / scores.length,
});
}
}
// 내 농장 순위 계산
farmScores.sort((a, b) => b.avgScore - a.avgScore);
const myFarmRank = farmScores.findIndex(f => f.farmNo === farmNo) + 1;
const totalFarmCount = farmScores.length;
const topPercent = totalFarmCount > 0 ? Math.round((myFarmRank / totalFarmCount) * 100) : 0;
// 지역 평균
const regionAverage = farmScores.length > 0
? farmScores.reduce((sum, f) => sum + f.avgScore, 0) / farmScores.length
: 0;
return {
farmNo,
farmAverage: farmEval.farmAverage,
regionAverage: Math.round(regionAverage * 100) / 100,
farmRank: myFarmRank || 1,
totalFarmCount: totalFarmCount || 1,
topPercent: topPercent || 100,
};
}
/**
* 개체 분포 분석
*/
async getCowDistribution(farmNo: number, filter?: DashboardFilterDto) {
const cows = await this.cowRepository.find({
where: { fkFarmNo: farmNo, delDt: IsNull() },
});
const distribution = {
A: 0,
B: 0,
C: 0,
D: 0,
E: 0,
};
for (const cow of cows) {
// cowId로 직접 형질 데이터 조회
const traitDetails = await this.genomeTraitDetailRepository.find({
where: { cowId: cow.cowId, delDt: IsNull() },
});
if (traitDetails.length === 0) continue;
const ebvValues = traitDetails
.filter(d => d.traitEbv !== null)
.map(d => Number(d.traitEbv));
if (ebvValues.length > 0) {
const avgEbv = ebvValues.reduce((sum, v) => sum + v, 0) / ebvValues.length;
if (avgEbv >= 1.0) distribution.A++;
else if (avgEbv >= 0.5) distribution.B++;
else if (avgEbv >= -0.5) distribution.C++;
else if (avgEbv >= -1.0) distribution.D++;
else distribution.E++;
}
}
return {
farmNo,
distribution,
total: cows.length,
};
}
/**
* KPN 추천 집계
*/
async getKpnRecommendationAggregation(farmNo: number, filter?: DashboardFilterDto) {
// 타겟 유전자 기반 KPN 추천 로직
const targetGenes = filter?.targetGenes || [];
// 농장 소 목록 조회
const cows = await this.cowRepository.find({
where: { fkFarmNo: farmNo, delDt: IsNull() },
});
// 간단한 KPN 추천 집계 (실제 로직은 더 복잡할 수 있음)
const kpnAggregations = [
{
kpnNumber: 'KPN001',
kpnName: '한우왕',
avgMatchingScore: 85.5,
recommendedCowCount: Math.floor(cows.length * 0.3),
percentage: 30,
rank: 1,
isOwned: false,
sampleCowIds: cows.slice(0, 3).map(c => c.cowId),
},
{
kpnNumber: 'KPN002',
kpnName: '육량대왕',
avgMatchingScore: 82.3,
recommendedCowCount: Math.floor(cows.length * 0.25),
percentage: 25,
rank: 2,
isOwned: true,
sampleCowIds: cows.slice(3, 6).map(c => c.cowId),
},
{
kpnNumber: 'KPN003',
kpnName: '품질명가',
avgMatchingScore: 79.1,
recommendedCowCount: Math.floor(cows.length * 0.2),
percentage: 20,
rank: 3,
isOwned: false,
sampleCowIds: cows.slice(6, 9).map(c => c.cowId),
},
];
return {
farmNo,
targetGenes,
kpnAggregations,
totalCows: cows.length,
};
}
/**
* 농장 보유 KPN 목록
*/
async getFarmKpnInventory(farmNo: number) {
// 실제 구현에서는 별도의 KPN 보유 테이블을 조회
return {
farmNo,
kpnList: [
{ kpnNumber: 'KPN002', kpnName: '육량대왕', stockCount: 10 },
],
};
}
/**
* 분석 이력 연도 목록
*/
async getAnalysisYears(farmNo: number): Promise<number[]> {
const requests = await this.genomeRequestRepository.find({
where: { fkFarmNo: farmNo, delDt: IsNull() },
select: ['requestDt'],
});
const years = new Set<number>();
for (const req of requests) {
if (req.requestDt) {
years.add(new Date(req.requestDt).getFullYear());
}
}
return Array.from(years).sort((a, b) => b - a);
}
/**
* 최신 분석 연도
*/
async getLatestAnalysisYear(farmNo: number): Promise<number> {
const years = await this.getAnalysisYears(farmNo);
return years[0] || new Date().getFullYear();
}
/**
* 3개년 비교 분석
*/
async getYearComparison(farmNo: number) {
const currentYear = new Date().getFullYear();
const years = [currentYear, currentYear - 1, currentYear - 2];
const comparison = [];
for (const year of years) {
// 해당 연도의 분석 데이터 집계
const requests = await this.genomeRequestRepository.find({
where: { fkFarmNo: farmNo, delDt: IsNull() },
});
const yearRequests = requests.filter(r => {
if (!r.requestDt) return false;
return new Date(r.requestDt).getFullYear() === year;
});
comparison.push({
year,
analysisCount: yearRequests.length,
matchCount: yearRequests.filter(r => r.chipSireName === '일치').length,
});
}
return { farmNo, comparison };
}
/**
* 번식 효율성 분석 (더미 데이터)
*/
async getReproEfficiency(farmNo: number, filter?: DashboardFilterDto) {
return {
farmNo,
avgCalvingInterval: 12.5,
avgFirstCalvingAge: 24,
conceptionRate: 65.5,
};
}
/**
* 우수개체 추천
*/
async getExcellentCows(farmNo: number, filter?: DashboardFilterDto) {
const limit = filter?.limit || 5;
const cows = await this.cowRepository.find({
where: { fkFarmNo: farmNo, delDt: IsNull() },
});
const cowsWithScore: Array<{ cow: CowModel; score: number }> = [];
for (const cow of cows) {
// cowId로 직접 형질 데이터 조회
const traitDetails = await this.genomeTraitDetailRepository.find({
where: { cowId: cow.cowId, delDt: IsNull() },
});
if (traitDetails.length === 0) continue;
const ebvValues = traitDetails
.filter(d => d.traitEbv !== null)
.map(d => Number(d.traitEbv));
if (ebvValues.length > 0) {
const avgEbv = ebvValues.reduce((sum, v) => sum + v, 0) / ebvValues.length;
cowsWithScore.push({ cow, score: avgEbv });
}
}
// 점수 내림차순 정렬
cowsWithScore.sort((a, b) => b.score - a.score);
return {
farmNo,
excellentCows: cowsWithScore.slice(0, limit).map((item, index) => ({
rank: index + 1,
cowNo: item.cow.pkCowNo,
cowId: item.cow.cowId,
score: Math.round(item.score * 100) / 100,
})),
};
}
/**
* 도태개체 추천
*/
async getCullCows(farmNo: number, filter?: DashboardFilterDto) {
const limit = filter?.limit || 5;
const cows = await this.cowRepository.find({
where: { fkFarmNo: farmNo, delDt: IsNull() },
});
const cowsWithScore: Array<{ cow: CowModel; score: number }> = [];
for (const cow of cows) {
// cowId로 직접 형질 데이터 조회
const traitDetails = await this.genomeTraitDetailRepository.find({
where: { cowId: cow.cowId, delDt: IsNull() },
});
if (traitDetails.length === 0) continue;
const ebvValues = traitDetails
.filter(d => d.traitEbv !== null)
.map(d => Number(d.traitEbv));
if (ebvValues.length > 0) {
const avgEbv = ebvValues.reduce((sum, v) => sum + v, 0) / ebvValues.length;
cowsWithScore.push({ cow, score: avgEbv });
}
}
// 점수 오름차순 정렬 (낮은 점수가 도태 대상)
cowsWithScore.sort((a, b) => a.score - b.score);
return {
farmNo,
cullCows: cowsWithScore.slice(0, limit).map((item, index) => ({
rank: index + 1,
cowNo: item.cow.pkCowNo,
cowId: item.cow.cowId,
score: Math.round(item.score * 100) / 100,
})),
};
}
/**
* 보은군 내 소 개별 순위
*/
async getCattleRankingInRegion(farmNo: number, filter?: DashboardFilterDto) {
// 전체 소 목록과 점수 계산
const allCows = await this.cowRepository.find({
where: { delDt: IsNull() },
relations: ['farm'],
});
const cowsWithScore: Array<{
cow: CowModel;
score: number;
farmNo: number;
}> = [];
for (const cow of allCows) {
// cowId로 직접 형질 데이터 조회
const traitDetails = await this.genomeTraitDetailRepository.find({
where: { cowId: cow.cowId, delDt: IsNull() },
});
if (traitDetails.length === 0) continue;
const ebvValues = traitDetails
.filter(d => d.traitEbv !== null)
.map(d => Number(d.traitEbv));
if (ebvValues.length > 0) {
const avgEbv = ebvValues.reduce((sum, v) => sum + v, 0) / ebvValues.length;
cowsWithScore.push({
cow,
score: avgEbv,
farmNo: cow.fkFarmNo,
});
}
}
// 점수 내림차순 정렬
cowsWithScore.sort((a, b) => b.score - a.score);
// 순위 부여
const rankedCows = cowsWithScore.map((item, index) => ({
...item,
rank: index + 1,
percentile: Math.round(((index + 1) / cowsWithScore.length) * 100),
}));
// 내 농장 소만 필터링
const myFarmCows = rankedCows.filter(item => item.farmNo === farmNo);
const farm = await this.farmRepository.findOne({
where: { pkFarmNo: farmNo, delDt: IsNull() },
});
return {
farmNo,
farmName: farm?.farmerName || '농장',
regionName: farm?.regionSi || '보은군',
totalCattle: cowsWithScore.length,
farmCattleCount: myFarmCows.length,
rankings: myFarmCows.map(item => ({
cowNo: item.cow.cowId,
cowName: `KOR ${item.cow.cowId}`,
genomeScore: Math.round(item.score * 100) / 100,
rank: item.rank,
totalCattle: cowsWithScore.length,
percentile: item.percentile,
})),
statistics: {
bestRank: myFarmCows.length > 0 ? myFarmCows[0].rank : 0,
averageRank: myFarmCows.length > 0
? Math.round(myFarmCows.reduce((sum, c) => sum + c.rank, 0) / myFarmCows.length)
: 0,
topPercentCount: myFarmCows.filter(c => c.percentile <= 10).length,
},
};
}
}