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, @InjectRepository(FarmModel) private readonly farmRepository: Repository, @InjectRepository(GenomeRequestModel) private readonly genomeRequestRepository: Repository, @InjectRepository(GenomeTraitDetailModel) private readonly genomeTraitDetailRepository: Repository, ) {} /** * 농장 현황 요약 */ 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 { const requests = await this.genomeRequestRepository.find({ where: { fkFarmNo: farmNo, delDt: IsNull() }, select: ['requestDt'], }); const years = new Set(); 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 { 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, }, }; } }