549 lines
16 KiB
TypeScript
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,
|
|
},
|
|
};
|
|
}
|
|
}
|