번식 능력 검사 리스트 및 보고서 수정
This commit is contained in:
@@ -10,16 +10,30 @@ export class MptController {
|
||||
findAll(
|
||||
@Query('farmId') farmId?: string,
|
||||
@Query('cowShortNo') cowShortNo?: string,
|
||||
@Query('cowId') cowId?: string,
|
||||
) {
|
||||
if (farmId) {
|
||||
return this.mptService.findByFarmId(+farmId);
|
||||
}
|
||||
if (cowId) {
|
||||
return this.mptService.findByCowId(cowId);
|
||||
}
|
||||
if (cowShortNo) {
|
||||
return this.mptService.findByCowShortNo(cowShortNo);
|
||||
}
|
||||
return this.mptService.findAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* 농장별 MPT 통계 조회
|
||||
* - 카테고리별 정상/주의/위험 개체 수
|
||||
* - 위험 개체 목록
|
||||
*/
|
||||
@Get('statistics/:farmNo')
|
||||
getMptStatistics(@Param('farmNo') farmNo: string) {
|
||||
return this.mptService.getMptStatistics(+farmNo);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
findOne(@Param('id') id: string) {
|
||||
return this.mptService.findOne(+id);
|
||||
|
||||
@@ -3,6 +3,49 @@ import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository, IsNull } from 'typeorm';
|
||||
import { MptModel } from './entities/mpt.entity';
|
||||
|
||||
/**
|
||||
* MPT 참조값 범위 (정상/주의/위험 판단 기준)
|
||||
*/
|
||||
const MPT_REFERENCE_RANGES: Record<string, { upper: number; lower: number; category: string }> = {
|
||||
glucose: { lower: 40, upper: 84, category: 'energy' },
|
||||
cholesterol: { lower: 74, upper: 252, category: 'energy' },
|
||||
nefa: { lower: 115, upper: 660, category: 'energy' },
|
||||
bcs: { lower: 2.5, upper: 3.5, category: 'energy' },
|
||||
totalProtein: { lower: 6.2, upper: 7.7, category: 'protein' },
|
||||
albumin: { lower: 3.3, upper: 4.3, category: 'protein' },
|
||||
globulin: { lower: 9.1, upper: 36.1, category: 'protein' },
|
||||
agRatio: { lower: 0.1, upper: 0.4, category: 'protein' },
|
||||
bun: { lower: 11.7, upper: 18.9, category: 'protein' },
|
||||
ast: { lower: 47, upper: 92, category: 'liver' },
|
||||
ggt: { lower: 11, upper: 32, category: 'liver' },
|
||||
fattyLiverIdx: { lower: -1.2, upper: 9.9, category: 'liver' },
|
||||
calcium: { lower: 8.1, upper: 10.6, category: 'mineral' },
|
||||
phosphorus: { lower: 6.2, upper: 8.9, category: 'mineral' },
|
||||
caPRatio: { lower: 1.2, upper: 1.3, category: 'mineral' },
|
||||
magnesium: { lower: 1.6, upper: 3.3, category: 'mineral' },
|
||||
};
|
||||
|
||||
/**
|
||||
* MPT 통계 응답 DTO
|
||||
*/
|
||||
export interface MptStatisticsDto {
|
||||
totalMptCows: number;
|
||||
latestTestDate: Date | null;
|
||||
categories: {
|
||||
energy: { safe: number; caution: number };
|
||||
protein: { safe: number; caution: number };
|
||||
liver: { safe: number; caution: number };
|
||||
mineral: { safe: number; caution: number };
|
||||
};
|
||||
riskyCows: Array<{
|
||||
cowId: string;
|
||||
category: string;
|
||||
itemName: string;
|
||||
value: number;
|
||||
status: 'high' | 'low';
|
||||
}>;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class MptService {
|
||||
constructor(
|
||||
@@ -34,6 +77,14 @@ export class MptService {
|
||||
});
|
||||
}
|
||||
|
||||
async findByCowId(cowId: string): Promise<MptModel[]> {
|
||||
return this.mptRepository.find({
|
||||
where: { cowId: cowId, delDt: IsNull() },
|
||||
relations: ['farm'],
|
||||
order: { testDt: 'DESC' },
|
||||
});
|
||||
}
|
||||
|
||||
async findOne(id: number): Promise<MptModel> {
|
||||
const mpt = await this.mptRepository.findOne({
|
||||
where: { pkMptNo: id, delDt: IsNull() },
|
||||
@@ -65,4 +116,143 @@ export class MptService {
|
||||
const mpt = await this.findOne(id);
|
||||
await this.mptRepository.softRemove(mpt);
|
||||
}
|
||||
|
||||
/**
|
||||
* 농장별 MPT 통계 조회
|
||||
* - 개체별 최신 검사 결과 기준
|
||||
* - 카테고리별 정상/주의/위험 개체 수
|
||||
* - 위험 개체 목록 (Top 5)
|
||||
*/
|
||||
async getMptStatistics(farmNo: number): Promise<MptStatisticsDto> {
|
||||
// 농장의 모든 MPT 데이터 조회
|
||||
const allMptData = await this.mptRepository.find({
|
||||
where: { fkFarmNo: farmNo, delDt: IsNull() },
|
||||
order: { testDt: 'DESC' },
|
||||
});
|
||||
|
||||
if (allMptData.length === 0) {
|
||||
return {
|
||||
totalMptCows: 0,
|
||||
latestTestDate: null,
|
||||
categories: {
|
||||
energy: { safe: 0, caution: 0 },
|
||||
protein: { safe: 0, caution: 0 },
|
||||
liver: { safe: 0, caution: 0 },
|
||||
mineral: { safe: 0, caution: 0 },
|
||||
},
|
||||
riskyCows: [],
|
||||
};
|
||||
}
|
||||
|
||||
// 개체별 최신 검사 데이터만 추출
|
||||
const latestByCoW = new Map<string, MptModel>();
|
||||
for (const mpt of allMptData) {
|
||||
if (!mpt.cowId) continue;
|
||||
if (!latestByCoW.has(mpt.cowId)) {
|
||||
latestByCoW.set(mpt.cowId, mpt);
|
||||
}
|
||||
}
|
||||
|
||||
const latestMptData = Array.from(latestByCoW.values());
|
||||
const totalMptCows = latestMptData.length;
|
||||
const latestTestDate = latestMptData[0]?.testDt || null;
|
||||
|
||||
// 카테고리별 통계 초기화 (안전/주의 2단계)
|
||||
const categoryStats = {
|
||||
energy: { safe: 0, caution: 0 },
|
||||
protein: { safe: 0, caution: 0 },
|
||||
liver: { safe: 0, caution: 0 },
|
||||
mineral: { safe: 0, caution: 0 },
|
||||
};
|
||||
|
||||
// 주의 개체 목록
|
||||
const riskyCowsList: MptStatisticsDto['riskyCows'] = [];
|
||||
|
||||
// 각 개체별로 카테고리별 상태 평가
|
||||
for (const mpt of latestMptData) {
|
||||
// 각 카테고리별로 이상 항목이 있는지 체크 (안전/주의 2단계)
|
||||
const categoryStatus: Record<string, 'safe' | 'caution'> = {
|
||||
energy: 'safe',
|
||||
protein: 'safe',
|
||||
liver: 'safe',
|
||||
mineral: 'safe',
|
||||
};
|
||||
|
||||
// 각 항목별 체크 (범위 내 = 안전, 범위 밖 = 주의)
|
||||
const checkItem = (value: number | null, itemKey: string) => {
|
||||
if (value === null || value === undefined) return;
|
||||
const ref = MPT_REFERENCE_RANGES[itemKey];
|
||||
if (!ref) return;
|
||||
|
||||
const category = ref.category as keyof typeof categoryStatus;
|
||||
|
||||
// 범위 밖이면 주의
|
||||
if (value > ref.upper || value < ref.lower) {
|
||||
categoryStatus[category] = 'caution';
|
||||
|
||||
// 주의 개체 목록에 추가
|
||||
riskyCowsList.push({
|
||||
cowId: mpt.cowId,
|
||||
category,
|
||||
itemName: itemKey,
|
||||
value,
|
||||
status: value > ref.upper ? 'high' : 'low',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 에너지 카테고리
|
||||
checkItem(mpt.glucose, 'glucose');
|
||||
checkItem(mpt.cholesterol, 'cholesterol');
|
||||
checkItem(mpt.nefa, 'nefa');
|
||||
checkItem(mpt.bcs, 'bcs');
|
||||
|
||||
// 단백질 카테고리
|
||||
checkItem(mpt.totalProtein, 'totalProtein');
|
||||
checkItem(mpt.albumin, 'albumin');
|
||||
checkItem(mpt.globulin, 'globulin');
|
||||
checkItem(mpt.agRatio, 'agRatio');
|
||||
checkItem(mpt.bun, 'bun');
|
||||
|
||||
// 간기능 카테고리
|
||||
checkItem(mpt.ast, 'ast');
|
||||
checkItem(mpt.ggt, 'ggt');
|
||||
checkItem(mpt.fattyLiverIdx, 'fattyLiverIdx');
|
||||
|
||||
// 미네랄 카테고리
|
||||
checkItem(mpt.calcium, 'calcium');
|
||||
checkItem(mpt.phosphorus, 'phosphorus');
|
||||
checkItem(mpt.caPRatio, 'caPRatio');
|
||||
checkItem(mpt.magnesium, 'magnesium');
|
||||
|
||||
// 카테고리별 통계 업데이트
|
||||
for (const [cat, status] of Object.entries(categoryStatus)) {
|
||||
const category = cat as keyof typeof categoryStats;
|
||||
categoryStats[category][status]++;
|
||||
}
|
||||
}
|
||||
|
||||
// 위험 개체 정렬 및 상위 5개만
|
||||
const sortedRiskyCows = riskyCowsList
|
||||
.sort((a, b) => {
|
||||
// 범위 이탈 정도로 정렬
|
||||
const refA = MPT_REFERENCE_RANGES[a.itemName];
|
||||
const refB = MPT_REFERENCE_RANGES[b.itemName];
|
||||
const deviationA = a.status === 'high'
|
||||
? (a.value - refA.upper) / (refA.upper - refA.lower)
|
||||
: (refA.lower - a.value) / (refA.upper - refA.lower);
|
||||
const deviationB = b.status === 'high'
|
||||
? (b.value - refB.upper) / (refB.upper - refB.lower)
|
||||
: (refB.lower - b.value) / (refB.upper - refB.lower);
|
||||
return deviationB - deviationA;
|
||||
})
|
||||
.slice(0, 5);
|
||||
|
||||
return {
|
||||
totalMptCows,
|
||||
latestTestDate,
|
||||
categories: categoryStats,
|
||||
riskyCows: sortedRiskyCows,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user