번식 능력 검사 리스트 및 보고서 수정
This commit is contained in:
@@ -20,6 +20,7 @@ import { CowModel } from './entities/cow.entity';
|
|||||||
import { GenomeRequestModel } from '../genome/entities/genome-request.entity';
|
import { GenomeRequestModel } from '../genome/entities/genome-request.entity';
|
||||||
import { GenomeTraitDetailModel } from '../genome/entities/genome-trait-detail.entity';
|
import { GenomeTraitDetailModel } from '../genome/entities/genome-trait-detail.entity';
|
||||||
import { GeneDetailModel } from '../gene/entities/gene-detail.entity';
|
import { GeneDetailModel } from '../gene/entities/gene-detail.entity';
|
||||||
|
import { MptModel } from '../mpt/entities/mpt.entity';
|
||||||
import { FilterEngineModule } from '../shared/filter/filter-engine.module';
|
import { FilterEngineModule } from '../shared/filter/filter-engine.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
@@ -29,6 +30,7 @@ import { FilterEngineModule } from '../shared/filter/filter-engine.module';
|
|||||||
GenomeRequestModel, // 유전체 분석 의뢰 (tb_genome_request)
|
GenomeRequestModel, // 유전체 분석 의뢰 (tb_genome_request)
|
||||||
GenomeTraitDetailModel, // 유전체 형질 상세 (tb_genome_trait_detail)
|
GenomeTraitDetailModel, // 유전체 형질 상세 (tb_genome_trait_detail)
|
||||||
GeneDetailModel, // 유전자 상세 (tb_gene_detail)
|
GeneDetailModel, // 유전자 상세 (tb_gene_detail)
|
||||||
|
MptModel, // 번식능력 (tb_mpt)
|
||||||
]),
|
]),
|
||||||
FilterEngineModule, // 필터 엔진 모듈
|
FilterEngineModule, // 필터 엔진 모듈
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -20,13 +20,14 @@ import { CowModel } from './entities/cow.entity';
|
|||||||
import { GenomeRequestModel } from '../genome/entities/genome-request.entity';
|
import { GenomeRequestModel } from '../genome/entities/genome-request.entity';
|
||||||
import { GenomeTraitDetailModel } from '../genome/entities/genome-trait-detail.entity';
|
import { GenomeTraitDetailModel } from '../genome/entities/genome-trait-detail.entity';
|
||||||
import { GeneDetailModel } from '../gene/entities/gene-detail.entity';
|
import { GeneDetailModel } from '../gene/entities/gene-detail.entity';
|
||||||
|
import { MptModel } from '../mpt/entities/mpt.entity';
|
||||||
import { FilterEngineService } from '../shared/filter/filter-engine.service';
|
import { FilterEngineService } from '../shared/filter/filter-engine.service';
|
||||||
import {
|
import {
|
||||||
RankingRequestDto,
|
RankingRequestDto,
|
||||||
RankingCriteriaType,
|
RankingCriteriaType,
|
||||||
TraitRankingCondition,
|
TraitRankingCondition,
|
||||||
} from './dto/ranking-request.dto';
|
} from './dto/ranking-request.dto';
|
||||||
import { isValidGenomeAnalysis } from '../common/config/GenomeAnalysisConfig';
|
import { isValidGenomeAnalysis, EXCLUDED_COW_IDS } from '../common/config/GenomeAnalysisConfig';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 낮을수록 좋은 형질 목록 (부호 반전 필요)
|
* 낮을수록 좋은 형질 목록 (부호 반전 필요)
|
||||||
@@ -62,6 +63,10 @@ export class CowService {
|
|||||||
@InjectRepository(GeneDetailModel)
|
@InjectRepository(GeneDetailModel)
|
||||||
private readonly geneDetailRepository: Repository<GeneDetailModel>,
|
private readonly geneDetailRepository: Repository<GeneDetailModel>,
|
||||||
|
|
||||||
|
// 번식능력 Repository (MPT 데이터 접근용)
|
||||||
|
@InjectRepository(MptModel)
|
||||||
|
private readonly mptRepository: Repository<MptModel>,
|
||||||
|
|
||||||
// 동적 필터링 서비스 (검색, 정렬, 페이지네이션)
|
// 동적 필터링 서비스 (검색, 정렬, 페이지네이션)
|
||||||
private readonly filterEngineService: FilterEngineService,
|
private readonly filterEngineService: FilterEngineService,
|
||||||
) { }
|
) { }
|
||||||
@@ -176,61 +181,133 @@ export class CowService {
|
|||||||
const { filterOptions, rankingOptions } = rankingRequest;
|
const { filterOptions, rankingOptions } = rankingRequest;
|
||||||
const { criteriaType } = rankingOptions;
|
const { criteriaType } = rankingOptions;
|
||||||
|
|
||||||
// Step 2: 필터 조건에 맞는 개체 목록 조회
|
// Step 2: 필터 조건에 맞는 개체 목록 조회 (+ MPT cowId Set)
|
||||||
const cows = await this.getFilteredCows(filterOptions);
|
const { cows, mptCowIdMap } = await this.getFilteredCows(filterOptions);
|
||||||
|
|
||||||
// Step 3: 랭킹 기준에 따라 분기 처리
|
// Step 3: 랭킹 기준에 따라 분기 처리
|
||||||
switch (criteriaType) {
|
switch (criteriaType) {
|
||||||
// 유전체 형질 기반 랭킹 (35개 형질 EBV 가중 평균)
|
// 유전체 형질 기반 랭킹 (35개 형질 EBV 가중 평균)
|
||||||
case RankingCriteriaType.GENOME:
|
case RankingCriteriaType.GENOME:
|
||||||
return this.applyGenomeRanking(cows, rankingOptions.traitConditions || []);
|
return this.applyGenomeRanking(cows, rankingOptions.traitConditions || [], mptCowIdMap);
|
||||||
|
|
||||||
// 기본값: 랭킹 없이 순서대로 반환
|
// 기본값: 랭킹 없이 순서대로 반환
|
||||||
default:
|
default:
|
||||||
return {
|
return {
|
||||||
items: cows.map((cow, index) => ({
|
items: cows.map((cow, index) => {
|
||||||
entity: cow,
|
const mptData = mptCowIdMap.get(cow.cowId);
|
||||||
rank: index + 1,
|
return {
|
||||||
sortValue: 0,
|
entity: {
|
||||||
})),
|
...cow,
|
||||||
|
hasMpt: mptCowIdMap.has(cow.cowId),
|
||||||
|
mptTestDt: mptData?.testDt || null,
|
||||||
|
mptMonthAge: mptData?.monthAge || null,
|
||||||
|
},
|
||||||
|
rank: index + 1,
|
||||||
|
sortValue: 0,
|
||||||
|
};
|
||||||
|
}),
|
||||||
total: cows.length,
|
total: cows.length,
|
||||||
criteriaType,
|
criteriaType,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 필터 조건에 맞는 개체 목록 조회 (Private)
|
* 필터 조건에 맞는 개체 목록 조회 (Private)
|
||||||
*
|
* 유전체 분석 의뢰/유전체 형질/유전자/번식능력(MPT) 데이터 중 하나라도 있는 개체 조회
|
||||||
* @param filterOptions - 필터/정렬/페이지네이션 옵션
|
*
|
||||||
* @returns 필터링된 개체 목록
|
* @param filterOptions - 필터/정렬/페이지네이션 옵션
|
||||||
*/
|
* @returns { cows: 필터링된 개체 목록, mptCowIdMap: MPT cowId -> { testDt, monthAge } Map }
|
||||||
private async getFilteredCows(filterOptions?: any): Promise<CowModel[]> {
|
*/
|
||||||
// QueryBuilder로 기본 쿼리 구성
|
private async getFilteredCows(filterOptions?: any): Promise<{ cows: CowModel[], mptCowIdMap: Map<string, { testDt: string; monthAge: number }> }> {
|
||||||
const queryBuilder = this.cowRepository
|
// Step 1: 4가지 데이터 소스에서 cowId 수집 (병렬 처리)
|
||||||
.createQueryBuilder('cow')
|
const [genomeRequestCowIds, genomeCowIds, geneCowIds, mptCowIds] = await Promise.all([
|
||||||
.leftJoinAndSelect('cow.farm', 'farm') // 농장 정보 JOIN
|
// 유전체 분석 의뢰가 있는 개체의 cowId (cow 테이블 조인)
|
||||||
.where('cow.delDt IS NULL'); // 삭제되지 않은 데이터만
|
this.genomeRequestRepository
|
||||||
|
.createQueryBuilder('request')
|
||||||
|
.innerJoin('request.cow', 'cow')
|
||||||
|
.select('DISTINCT cow.cowId', 'cowId')
|
||||||
|
.where('request.delDt IS NULL')
|
||||||
|
.getRawMany(),
|
||||||
|
// 유전체 형질 데이터가 있는 cowId
|
||||||
|
this.genomeTraitDetailRepository
|
||||||
|
.createQueryBuilder('trait')
|
||||||
|
.select('DISTINCT trait.cowId', 'cowId')
|
||||||
|
.where('trait.delDt IS NULL')
|
||||||
|
.getRawMany(),
|
||||||
|
// 유전자 데이터가 있는 cowId
|
||||||
|
this.geneDetailRepository
|
||||||
|
.createQueryBuilder('gene')
|
||||||
|
.select('DISTINCT gene.cowId', 'cowId')
|
||||||
|
.where('gene.delDt IS NULL')
|
||||||
|
.getRawMany(),
|
||||||
|
// 번식능력(MPT) 데이터가 있는 cowId와 최신 검사일/월령
|
||||||
|
// 서브쿼리로 최신 검사일 기준 데이터 가져오기
|
||||||
|
this.mptRepository
|
||||||
|
.createQueryBuilder('mpt')
|
||||||
|
.select('mpt.cowId', 'cowId')
|
||||||
|
.addSelect('mpt.testDt', 'testDt')
|
||||||
|
.addSelect('mpt.monthAge', 'monthAge')
|
||||||
|
.where('mpt.delDt IS NULL')
|
||||||
|
.andWhere(qb => {
|
||||||
|
const subQuery = qb.subQuery()
|
||||||
|
.select('MAX(sub.testDt)')
|
||||||
|
.from('tb_mpt', 'sub')
|
||||||
|
.where('sub.cow_id = mpt.cowId')
|
||||||
|
.andWhere('sub.del_dt IS NULL')
|
||||||
|
.getQuery();
|
||||||
|
return `mpt.testDt = ${subQuery}`;
|
||||||
|
})
|
||||||
|
.getRawMany(),
|
||||||
|
]);
|
||||||
|
|
||||||
// farmNo가 직접 전달된 경우 처리 (프론트엔드 호환성)
|
// Step 2: cowId 통합 (중복 제거)
|
||||||
if (filterOptions?.farmNo) {
|
const allCowIds = [...new Set([
|
||||||
queryBuilder.andWhere('cow.fkFarmNo = :farmNo', {
|
...genomeRequestCowIds.map(c => c.cowId).filter(Boolean),
|
||||||
farmNo: filterOptions.farmNo
|
...genomeCowIds.map(c => c.cowId).filter(Boolean),
|
||||||
});
|
...geneCowIds.map(c => c.cowId).filter(Boolean),
|
||||||
}
|
...mptCowIds.map(c => c.cowId).filter(Boolean),
|
||||||
|
])];
|
||||||
|
|
||||||
// FilterEngine 사용하여 동적 필터 적용
|
// MPT cowId -> { testDt, monthAge } Map 생성
|
||||||
if (filterOptions?.filters) {
|
const mptCowIdMap = new Map<string, { testDt: string; monthAge: number }>(
|
||||||
const result = await this.filterEngineService.executeFilteredQuery(
|
mptCowIds
|
||||||
queryBuilder,
|
.filter(c => c.cowId)
|
||||||
filterOptions,
|
.map(c => [c.cowId, { testDt: c.testDt, monthAge: c.monthAge }])
|
||||||
);
|
);
|
||||||
return result.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 필터 없으면 전체 조회 (최신순)
|
// 데이터가 있는 개체가 없으면 빈 배열 반환
|
||||||
return queryBuilder.orderBy('cow.regDt', 'DESC').getMany();
|
if (allCowIds.length === 0) {
|
||||||
}
|
return { cows: [], mptCowIdMap };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 3: 해당 cowId로 개체 조회
|
||||||
|
const queryBuilder = this.cowRepository
|
||||||
|
.createQueryBuilder('cow')
|
||||||
|
.leftJoinAndSelect('cow.farm', 'farm')
|
||||||
|
.where('cow.cowId IN (:...cowIds)', { cowIds: allCowIds })
|
||||||
|
.andWhere('cow.delDt IS NULL');
|
||||||
|
|
||||||
|
// farmNo가 직접 전달된 경우 처리 (프론트엔드 호환성)
|
||||||
|
if (filterOptions?.farmNo) {
|
||||||
|
queryBuilder.andWhere('cow.fkFarmNo = :farmNo', {
|
||||||
|
farmNo: filterOptions.farmNo
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// FilterEngine 사용하여 동적 필터 적용
|
||||||
|
if (filterOptions?.filters) {
|
||||||
|
const result = await this.filterEngineService.executeFilteredQuery(
|
||||||
|
queryBuilder,
|
||||||
|
filterOptions,
|
||||||
|
);
|
||||||
|
return { cows: result.data, mptCowIdMap };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 필터 없으면 전체 조회 (최신순)
|
||||||
|
const cows = await queryBuilder.orderBy('cow.regDt', 'DESC').getMany();
|
||||||
|
return { cows, mptCowIdMap };
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// 유전체(GENOME) 랭킹 메서드
|
// 유전체(GENOME) 랭킹 메서드
|
||||||
@@ -255,6 +332,7 @@ export class CowService {
|
|||||||
private async applyGenomeRanking(
|
private async applyGenomeRanking(
|
||||||
cows: CowModel[],
|
cows: CowModel[],
|
||||||
inputTraitConditions: TraitRankingCondition[],
|
inputTraitConditions: TraitRankingCondition[],
|
||||||
|
mptCowIdMap: Map<string, { testDt: string; monthAge: number }>,
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
// 35개 전체 형질 (기본값)
|
// 35개 전체 형질 (기본값)
|
||||||
const ALL_TRAITS = [
|
const ALL_TRAITS = [
|
||||||
@@ -286,7 +364,10 @@ export class CowService {
|
|||||||
// 분석불가 사유 결정
|
// 분석불가 사유 결정
|
||||||
let unavailableReason: string | null = null;
|
let unavailableReason: string | null = null;
|
||||||
|
|
||||||
if (!latestRequest || !latestRequest.chipSireName) {
|
// EXCLUDED_COW_IDS에 포함된 개체 (모근 오염/불량 등 기타 사유)
|
||||||
|
if (EXCLUDED_COW_IDS.includes(cow.cowId)) {
|
||||||
|
unavailableReason = '분석불가';
|
||||||
|
} else if (!latestRequest || !latestRequest.chipSireName) {
|
||||||
// latestRequest 없거나 chipSireName이 null → '-' 표시 (프론트에서 null은 '-'로 표시)
|
// latestRequest 없거나 chipSireName이 null → '-' 표시 (프론트에서 null은 '-'로 표시)
|
||||||
unavailableReason = null;
|
unavailableReason = null;
|
||||||
} else if (latestRequest.chipSireName === '분석불가' || latestRequest.chipSireName === '정보없음') {
|
} else if (latestRequest.chipSireName === '분석불가' || latestRequest.chipSireName === '정보없음') {
|
||||||
@@ -301,7 +382,19 @@ export class CowService {
|
|||||||
unavailableReason = '모 이력제부재';
|
unavailableReason = '모 이력제부재';
|
||||||
}
|
}
|
||||||
|
|
||||||
return { entity: { ...cow, unavailableReason }, sortValue: null, details: [] };
|
const mptData = mptCowIdMap.get(cow.cowId);
|
||||||
|
return {
|
||||||
|
entity: {
|
||||||
|
...cow,
|
||||||
|
unavailableReason,
|
||||||
|
hasMpt: mptCowIdMap.has(cow.cowId),
|
||||||
|
mptTestDt: mptData?.testDt || null,
|
||||||
|
mptMonthAge: mptData?.monthAge || null,
|
||||||
|
anlysDt: latestRequest?.requestDt ?? null,
|
||||||
|
},
|
||||||
|
sortValue: null,
|
||||||
|
details: [],
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 3: cowId로 직접 형질 상세 데이터 조회 (35개 형질의 EBV 값)
|
// Step 3: cowId로 직접 형질 상세 데이터 조회 (35개 형질의 EBV 값)
|
||||||
@@ -311,7 +404,18 @@ export class CowService {
|
|||||||
|
|
||||||
// 형질 데이터가 없으면 점수 null (친자는 일치하지만 형질 데이터 없음)
|
// 형질 데이터가 없으면 점수 null (친자는 일치하지만 형질 데이터 없음)
|
||||||
if (traitDetails.length === 0) {
|
if (traitDetails.length === 0) {
|
||||||
return { entity: { ...cow, unavailableReason: '형질정보없음' }, sortValue: null, details: [] };
|
const mptData = mptCowIdMap.get(cow.cowId);
|
||||||
|
return {
|
||||||
|
entity: {
|
||||||
|
...cow,
|
||||||
|
unavailableReason: '형질정보없음',
|
||||||
|
hasMpt: mptCowIdMap.has(cow.cowId),
|
||||||
|
mptTestDt: mptData?.testDt || null,
|
||||||
|
mptMonthAge: mptData?.monthAge || null,
|
||||||
|
},
|
||||||
|
sortValue: null,
|
||||||
|
details: [],
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 4: 가중 합계 계산
|
// Step 4: 가중 합계 계산
|
||||||
@@ -354,10 +458,14 @@ export class CowService {
|
|||||||
: null;
|
: null;
|
||||||
|
|
||||||
// Step 7: 응답 데이터 구성
|
// Step 7: 응답 데이터 구성
|
||||||
|
const mptData = mptCowIdMap.get(cow.cowId);
|
||||||
return {
|
return {
|
||||||
entity: {
|
entity: {
|
||||||
...cow,
|
...cow,
|
||||||
anlysDt: latestRequest.requestDt, // 분석일자 추가
|
anlysDt: latestRequest.requestDt, // 분석일자 추가
|
||||||
|
hasMpt: mptCowIdMap.has(cow.cowId), // MPT 검사 여부
|
||||||
|
mptTestDt: mptData?.testDt || null, // MPT 검사일
|
||||||
|
mptMonthAge: mptData?.monthAge || null, // MPT 월령
|
||||||
},
|
},
|
||||||
sortValue, // 계산된 종합 점수 (선발지수)
|
sortValue, // 계산된 종합 점수 (선발지수)
|
||||||
details, // 점수 계산에 사용된 형질별 상세
|
details, // 점수 계산에 사용된 형질별 상세
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import { GenomeRequestModel } from './entities/genome-request.entity';
|
|||||||
import { GenomeTraitDetailModel } from './entities/genome-trait-detail.entity';
|
import { GenomeTraitDetailModel } from './entities/genome-trait-detail.entity';
|
||||||
import { CowModel } from '../cow/entities/cow.entity';
|
import { CowModel } from '../cow/entities/cow.entity';
|
||||||
import { FarmModel } from '../farm/entities/farm.entity';
|
import { FarmModel } from '../farm/entities/farm.entity';
|
||||||
|
import { MptModel } from '../mpt/entities/mpt.entity';
|
||||||
|
import { GeneDetailModel } from '../gene/entities/gene-detail.entity';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -14,6 +16,8 @@ import { FarmModel } from '../farm/entities/farm.entity';
|
|||||||
GenomeTraitDetailModel,
|
GenomeTraitDetailModel,
|
||||||
CowModel,
|
CowModel,
|
||||||
FarmModel,
|
FarmModel,
|
||||||
|
MptModel,
|
||||||
|
GeneDetailModel,
|
||||||
]),
|
]),
|
||||||
],
|
],
|
||||||
controllers: [GenomeController],
|
controllers: [GenomeController],
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import { CowModel } from '../cow/entities/cow.entity';
|
|||||||
import { FarmModel } from '../farm/entities/farm.entity';
|
import { FarmModel } from '../farm/entities/farm.entity';
|
||||||
import { GenomeRequestModel } from './entities/genome-request.entity';
|
import { GenomeRequestModel } from './entities/genome-request.entity';
|
||||||
import { GenomeTraitDetailModel } from './entities/genome-trait-detail.entity';
|
import { GenomeTraitDetailModel } from './entities/genome-trait-detail.entity';
|
||||||
|
import { MptModel } from '../mpt/entities/mpt.entity';
|
||||||
|
import { GeneDetailModel } from '../gene/entities/gene-detail.entity';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 낮을수록 좋은 형질 목록 (부호 반전 필요)
|
* 낮을수록 좋은 형질 목록 (부호 반전 필요)
|
||||||
@@ -139,6 +141,14 @@ export class GenomeService {
|
|||||||
// 농장 정보 Repository
|
// 농장 정보 Repository
|
||||||
@InjectRepository(FarmModel)
|
@InjectRepository(FarmModel)
|
||||||
private readonly farmRepository: Repository<FarmModel>,
|
private readonly farmRepository: Repository<FarmModel>,
|
||||||
|
|
||||||
|
// 번식능력검사 Repository
|
||||||
|
@InjectRepository(MptModel)
|
||||||
|
private readonly mptRepository: Repository<MptModel>,
|
||||||
|
|
||||||
|
// 유전자검사 상세 Repository
|
||||||
|
@InjectRepository(GeneDetailModel)
|
||||||
|
private readonly geneDetailRepository: Repository<GeneDetailModel>,
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
@@ -359,7 +369,11 @@ export class GenomeService {
|
|||||||
}[];
|
}[];
|
||||||
// 요약
|
// 요약
|
||||||
summary: {
|
summary: {
|
||||||
totalRequests: number;
|
totalCows: number; // 검사 받은 전체 개체 수 (합집합, 중복 제외)
|
||||||
|
genomeCowCount: number; // 유전체 분석 개체 수
|
||||||
|
geneCowCount: number; // 유전자검사 개체 수
|
||||||
|
mptCowCount: number; // 번식능력검사 개체 수
|
||||||
|
totalRequests: number; // 유전체 의뢰 건수 (기존 호환성)
|
||||||
analyzedCount: number;
|
analyzedCount: number;
|
||||||
pendingCount: number;
|
pendingCount: number;
|
||||||
mismatchCount: number;
|
mismatchCount: number;
|
||||||
@@ -729,21 +743,79 @@ export class GenomeService {
|
|||||||
const mismatchCount = requests.filter(r => r.chipSireName && r.chipSireName !== '일치').length;
|
const mismatchCount = requests.filter(r => r.chipSireName && r.chipSireName !== '일치').length;
|
||||||
const pendingCount = totalRequests - analyzedCount - mismatchCount;
|
const pendingCount = totalRequests - analyzedCount - mismatchCount;
|
||||||
|
|
||||||
// 성별 체크 - 디버깅 강화
|
// Step 5.1: 검사 유형별 개체 수 계산 (합집합, 중복 제외)
|
||||||
// 실제 성별 값 분석
|
// 농장 소유 개체의 cowId 목록 조회
|
||||||
const sexAnalysis = requests.map(r => ({
|
const farmCows = await this.cowRepository.find({
|
||||||
cowId: r.cow?.cowId,
|
where: { fkFarmNo: farmNo, delDt: IsNull() },
|
||||||
cowSex: r.cow?.cowSex,
|
select: ['cowId', 'cowSex'],
|
||||||
hasCow: !!r.cow,
|
});
|
||||||
}));
|
const farmCowIds = new Set(farmCows.map(c => c.cowId).filter(Boolean));
|
||||||
|
const farmCowMap = new Map(farmCows.map(c => [c.cowId, c]));
|
||||||
|
|
||||||
// 성별 체크 (M/수/1 = 수컷, 그 외 모두 암컷으로 처리)
|
// 각 검사별 cowId 조회 (병렬 처리) - genomeRequest도 포함 (리스트 페이지와 일치)
|
||||||
const maleCount = requests.filter(r => {
|
const [genomeRequestCowIds, genomeCowIds, geneCowIds, mptCowIds] = await Promise.all([
|
||||||
const sex = r.cow?.cowSex?.toUpperCase();
|
// 유전체 분석 의뢰가 있는 개체 (분석불가 포함)
|
||||||
return sex === 'M' || sex === '수' || sex === '1';
|
this.genomeRequestRepository
|
||||||
}).length;
|
.createQueryBuilder('request')
|
||||||
// 수컷이 아니면 모두 암컷으로 처리 (null 포함)
|
.innerJoin('request.cow', 'cow')
|
||||||
const femaleCount = requests.length - maleCount;
|
.select('DISTINCT cow.cowId', 'cowId')
|
||||||
|
.where('request.delDt IS NULL')
|
||||||
|
.getRawMany()
|
||||||
|
.then(rows => rows.map((r: { cowId: string }) => r.cowId).filter(Boolean)),
|
||||||
|
// 유전체 분석 개체 (형질 데이터 보유)
|
||||||
|
this.genomeTraitDetailRepository
|
||||||
|
.createQueryBuilder('trait')
|
||||||
|
.select('DISTINCT trait.cowId', 'cowId')
|
||||||
|
.where('trait.delDt IS NULL')
|
||||||
|
.getRawMany()
|
||||||
|
.then(rows => rows.map((r: { cowId: string }) => r.cowId).filter(Boolean)),
|
||||||
|
// 유전자검사 개체
|
||||||
|
this.geneDetailRepository
|
||||||
|
.createQueryBuilder('gene')
|
||||||
|
.select('DISTINCT gene.cowId', 'cowId')
|
||||||
|
.where('gene.delDt IS NULL')
|
||||||
|
.getRawMany()
|
||||||
|
.then(rows => rows.map((r: { cowId: string }) => r.cowId).filter(Boolean)),
|
||||||
|
// 번식능력검사 개체
|
||||||
|
this.mptRepository
|
||||||
|
.createQueryBuilder('mpt')
|
||||||
|
.select('DISTINCT mpt.cowId', 'cowId')
|
||||||
|
.where('mpt.delDt IS NULL')
|
||||||
|
.getRawMany()
|
||||||
|
.then(rows => rows.map((r: { cowId: string }) => r.cowId).filter(Boolean)),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 농장 소유 개체만 필터링
|
||||||
|
const farmGenomeRequestCowIds = genomeRequestCowIds.filter(id => farmCowIds.has(id));
|
||||||
|
const farmGenomeCowIds = genomeCowIds.filter(id => farmCowIds.has(id));
|
||||||
|
const farmGeneCowIds = geneCowIds.filter(id => farmCowIds.has(id));
|
||||||
|
const farmMptCowIds = mptCowIds.filter(id => farmCowIds.has(id));
|
||||||
|
|
||||||
|
// 합집합 계산 (중복 제외) - genomeRequest 포함 (리스트 페이지와 일치)
|
||||||
|
const allTestedCowIds = new Set([
|
||||||
|
...farmGenomeRequestCowIds,
|
||||||
|
...farmGenomeCowIds,
|
||||||
|
...farmGeneCowIds,
|
||||||
|
...farmMptCowIds,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const totalCows = allTestedCowIds.size;
|
||||||
|
const genomeCowCount = farmGenomeCowIds.length;
|
||||||
|
const geneCowCount = farmGeneCowIds.length;
|
||||||
|
const mptCowCount = farmMptCowIds.length;
|
||||||
|
|
||||||
|
// 성별 체크 - 전체 검사 개체 기준 (M/수/1 = 수컷, 그 외 모두 암컷으로 처리)
|
||||||
|
let maleCount = 0;
|
||||||
|
let femaleCount = 0;
|
||||||
|
for (const cowId of allTestedCowIds) {
|
||||||
|
const cow = farmCowMap.get(cowId);
|
||||||
|
const sex = cow?.cowSex?.toUpperCase();
|
||||||
|
if (sex === 'M' || sex === '수' || sex === '1') {
|
||||||
|
maleCount++;
|
||||||
|
} else {
|
||||||
|
femaleCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Step 6: 검사 종류별 현황 (SNP, MS)
|
// Step 6: 검사 종류별 현황 (SNP, MS)
|
||||||
const testTypeStats = {
|
const testTypeStats = {
|
||||||
@@ -822,7 +894,11 @@ export class GenomeService {
|
|||||||
yearlyAvgEbv,
|
yearlyAvgEbv,
|
||||||
requestHistory,
|
requestHistory,
|
||||||
summary: {
|
summary: {
|
||||||
totalRequests,
|
totalCows, // 검사 받은 전체 개체 수 (합집합)
|
||||||
|
genomeCowCount, // 유전체 분석 개체 수
|
||||||
|
geneCowCount, // 유전자검사 개체 수
|
||||||
|
mptCowCount, // 번식능력검사 개체 수
|
||||||
|
totalRequests, // 유전체 의뢰 건수 (기존 호환성)
|
||||||
analyzedCount,
|
analyzedCount,
|
||||||
pendingCount,
|
pendingCount,
|
||||||
mismatchCount,
|
mismatchCount,
|
||||||
|
|||||||
@@ -10,16 +10,30 @@ export class MptController {
|
|||||||
findAll(
|
findAll(
|
||||||
@Query('farmId') farmId?: string,
|
@Query('farmId') farmId?: string,
|
||||||
@Query('cowShortNo') cowShortNo?: string,
|
@Query('cowShortNo') cowShortNo?: string,
|
||||||
|
@Query('cowId') cowId?: string,
|
||||||
) {
|
) {
|
||||||
if (farmId) {
|
if (farmId) {
|
||||||
return this.mptService.findByFarmId(+farmId);
|
return this.mptService.findByFarmId(+farmId);
|
||||||
}
|
}
|
||||||
|
if (cowId) {
|
||||||
|
return this.mptService.findByCowId(cowId);
|
||||||
|
}
|
||||||
if (cowShortNo) {
|
if (cowShortNo) {
|
||||||
return this.mptService.findByCowShortNo(cowShortNo);
|
return this.mptService.findByCowShortNo(cowShortNo);
|
||||||
}
|
}
|
||||||
return this.mptService.findAll();
|
return this.mptService.findAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 농장별 MPT 통계 조회
|
||||||
|
* - 카테고리별 정상/주의/위험 개체 수
|
||||||
|
* - 위험 개체 목록
|
||||||
|
*/
|
||||||
|
@Get('statistics/:farmNo')
|
||||||
|
getMptStatistics(@Param('farmNo') farmNo: string) {
|
||||||
|
return this.mptService.getMptStatistics(+farmNo);
|
||||||
|
}
|
||||||
|
|
||||||
@Get(':id')
|
@Get(':id')
|
||||||
findOne(@Param('id') id: string) {
|
findOne(@Param('id') id: string) {
|
||||||
return this.mptService.findOne(+id);
|
return this.mptService.findOne(+id);
|
||||||
|
|||||||
@@ -3,6 +3,49 @@ import { InjectRepository } from '@nestjs/typeorm';
|
|||||||
import { Repository, IsNull } from 'typeorm';
|
import { Repository, IsNull } from 'typeorm';
|
||||||
import { MptModel } from './entities/mpt.entity';
|
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()
|
@Injectable()
|
||||||
export class MptService {
|
export class MptService {
|
||||||
constructor(
|
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> {
|
async findOne(id: number): Promise<MptModel> {
|
||||||
const mpt = await this.mptRepository.findOne({
|
const mpt = await this.mptRepository.findOne({
|
||||||
where: { pkMptNo: id, delDt: IsNull() },
|
where: { pkMptNo: id, delDt: IsNull() },
|
||||||
@@ -65,4 +116,143 @@ export class MptService {
|
|||||||
const mpt = await this.findOne(id);
|
const mpt = await this.findOne(id);
|
||||||
await this.mptRepository.softRemove(mpt);
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
|||||||
import { useGlobalFilter } from "@/contexts/GlobalFilterContext"
|
import { useGlobalFilter } from "@/contexts/GlobalFilterContext"
|
||||||
import { useMediaQuery } from "@/hooks/use-media-query"
|
import { useMediaQuery } from "@/hooks/use-media-query"
|
||||||
import { useToast } from "@/hooks/use-toast"
|
import { useToast } from "@/hooks/use-toast"
|
||||||
import { ComparisonAveragesDto, cowApi, geneApi, GeneDetail, genomeApi, GenomeRequestDto, TraitComparisonAveragesDto } from "@/lib/api"
|
import { ComparisonAveragesDto, cowApi, geneApi, GeneDetail, genomeApi, GenomeRequestDto, TraitComparisonAveragesDto, mptApi } from "@/lib/api"
|
||||||
import { getInvalidMessage, getInvalidReason, isExcludedCow, isValidGenomeAnalysis } from "@/lib/utils/genome-analysis-config"
|
import { getInvalidMessage, getInvalidReason, isExcludedCow, isValidGenomeAnalysis } from "@/lib/utils/genome-analysis-config"
|
||||||
import { CowDetail } from "@/types/cow.types"
|
import { CowDetail } from "@/types/cow.types"
|
||||||
import { GenomeTrait } from "@/types/genome.types"
|
import { GenomeTrait } from "@/types/genome.types"
|
||||||
@@ -35,6 +35,7 @@ import { CategoryEvaluationCard } from "./genome/_components/category-evaluation
|
|||||||
import { TraitComparison } from "./genome/_components/genome-integrated-comparison"
|
import { TraitComparison } from "./genome/_components/genome-integrated-comparison"
|
||||||
import { NormalDistributionChart } from "./genome/_components/normal-distribution-chart"
|
import { NormalDistributionChart } from "./genome/_components/normal-distribution-chart"
|
||||||
import { TraitDistributionCharts } from "./genome/_components/trait-distribution-charts"
|
import { TraitDistributionCharts } from "./genome/_components/trait-distribution-charts"
|
||||||
|
import { MptTable } from "./reproduction/_components/mpt-table"
|
||||||
|
|
||||||
// 형질명 → 카테고리 매핑 (한우 35개 형질)
|
// 형질명 → 카테고리 매핑 (한우 35개 형질)
|
||||||
const TRAIT_CATEGORY_MAP: Record<string, string> = {
|
const TRAIT_CATEGORY_MAP: Record<string, string> = {
|
||||||
@@ -405,9 +406,13 @@ export default function CowOverviewPage() {
|
|||||||
setGenomeRequest(null)
|
setGenomeRequest(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 번식능력 데이터 (현재는 목업 - 추후 API 연동)
|
// 번식능력 데이터 조회
|
||||||
// TODO: 번식능력 API 연동
|
try {
|
||||||
setHasReproductionData(false)
|
const mptData = await mptApi.findByCowId(cowNo)
|
||||||
|
setHasReproductionData(mptData && mptData.length > 0)
|
||||||
|
} catch {
|
||||||
|
setHasReproductionData(false)
|
||||||
|
}
|
||||||
|
|
||||||
// 첫 번째 사용 가능한 탭 자동 선택
|
// 첫 번째 사용 가능한 탭 자동 선택
|
||||||
if (genomeExists) {
|
if (genomeExists) {
|
||||||
@@ -617,15 +622,19 @@ export default function CowOverviewPage() {
|
|||||||
<ArrowLeft className="h-7 w-7 sm:h-4 sm:w-4" />
|
<ArrowLeft className="h-7 w-7 sm:h-4 sm:w-4" />
|
||||||
<span className="hidden sm:inline text-sm">목록으로</span>
|
<span className="hidden sm:inline text-sm">목록으로</span>
|
||||||
</Button>
|
</Button>
|
||||||
{/* 아이콘 */}
|
{/* 아이콘 + 타이틀 (클릭시 새로고침) */}
|
||||||
<div className="w-10 h-10 sm:w-14 sm:h-14 bg-primary rounded-xl flex items-center justify-center flex-shrink-0">
|
<button
|
||||||
<BarChart3 className="h-5 w-5 sm:h-7 sm:w-7 text-primary-foreground" />
|
onClick={() => window.location.reload()}
|
||||||
</div>
|
className="flex items-center gap-3 sm:gap-4 hover:opacity-80 transition-opacity cursor-pointer"
|
||||||
{/* 타이틀 */}
|
>
|
||||||
<div>
|
<div className="w-10 h-10 sm:w-14 sm:h-14 bg-primary rounded-xl flex items-center justify-center flex-shrink-0">
|
||||||
<h1 className="text-lg sm:text-3xl lg:text-4xl font-bold text-foreground">개체 분석 보고서</h1>
|
<BarChart3 className="h-5 w-5 sm:h-7 sm:w-7 text-primary-foreground" />
|
||||||
<p className="text-sm sm:text-lg text-muted-foreground">Analysis Report</p>
|
</div>
|
||||||
</div>
|
<div className="text-left">
|
||||||
|
<h1 className="text-lg sm:text-3xl lg:text-4xl font-bold text-foreground">개체 분석 보고서</h1>
|
||||||
|
<p className="text-sm sm:text-lg text-muted-foreground">Analysis Report</p>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -658,6 +667,9 @@ export default function CowOverviewPage() {
|
|||||||
>
|
>
|
||||||
<Activity className="hidden sm:block h-6 w-6 shrink-0" />
|
<Activity className="hidden sm:block h-6 w-6 shrink-0" />
|
||||||
<span className="font-bold text-sm sm:text-xl">번식능력</span>
|
<span className="font-bold text-sm sm:text-xl">번식능력</span>
|
||||||
|
<span className={`text-xs sm:text-sm px-1.5 sm:px-2.5 py-0.5 sm:py-1 rounded font-semibold shrink-0 ${hasReproductionData ? 'bg-green-500 text-white' : 'bg-slate-300 text-slate-600'}`}>
|
||||||
|
{hasReproductionData ? '완료' : '미검사'}
|
||||||
|
</span>
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
@@ -692,12 +704,12 @@ export default function CowOverviewPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="bg-muted/50 px-5 py-3 border-b border-border">
|
<div className="bg-muted/50 px-5 py-3 border-b border-border">
|
||||||
<span className="text-base font-semibold text-muted-foreground">월령</span>
|
<span className="text-base font-semibold text-muted-foreground">월령 (분석일 기준)</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="px-5 py-4">
|
<div className="px-5 py-4">
|
||||||
<span className="text-2xl font-bold text-foreground">
|
<span className="text-2xl font-bold text-foreground">
|
||||||
{cow?.cowBirthDt
|
{cow?.cowBirthDt && genomeData[0]?.request?.requestDt
|
||||||
? `${Math.floor((new Date().getTime() - new Date(cow.cowBirthDt).getTime()) / (1000 * 60 * 60 * 24 * 30.44))}개월`
|
? `${Math.floor((new Date(genomeData[0].request.requestDt).getTime() - new Date(cow.cowBirthDt).getTime()) / (1000 * 60 * 60 * 24 * 30.44))}개월`
|
||||||
: '-'}
|
: '-'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -730,10 +742,10 @@ export default function CowOverviewPage() {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<span className="w-28 shrink-0 bg-muted/50 px-4 py-3.5 text-base font-medium text-muted-foreground">월령</span>
|
<span className="w-28 shrink-0 bg-muted/50 px-4 py-3.5 text-base font-medium text-muted-foreground">월령 (분석)</span>
|
||||||
<span className="flex-1 px-4 py-3.5 text-base font-bold text-foreground">
|
<span className="flex-1 px-4 py-3.5 text-base font-bold text-foreground">
|
||||||
{cow?.cowBirthDt
|
{cow?.cowBirthDt && genomeData[0]?.request?.requestDt
|
||||||
? `${Math.floor((new Date().getTime() - new Date(cow.cowBirthDt).getTime()) / (1000 * 60 * 60 * 24 * 30.44))}개월`
|
? `${Math.floor((new Date(genomeData[0].request.requestDt).getTime() - new Date(cow.cowBirthDt).getTime()) / (1000 * 60 * 60 * 24 * 30.44))}개월`
|
||||||
: '-'}
|
: '-'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -969,12 +981,12 @@ export default function CowOverviewPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="bg-muted/50 px-5 py-3 border-b border-border">
|
<div className="bg-muted/50 px-5 py-3 border-b border-border">
|
||||||
<span className="text-base font-semibold text-muted-foreground">월령</span>
|
<span className="text-base font-semibold text-muted-foreground">월령 (분석일 기준)</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="px-5 py-4">
|
<div className="px-5 py-4">
|
||||||
<span className="text-2xl font-bold text-foreground">
|
<span className="text-2xl font-bold text-foreground">
|
||||||
{cow?.cowBirthDt
|
{cow?.cowBirthDt && genomeData[0]?.request?.requestDt
|
||||||
? `${Math.floor((new Date().getTime() - new Date(cow.cowBirthDt).getTime()) / (1000 * 60 * 60 * 24 * 30.44))}개월`
|
? `${Math.floor((new Date(genomeData[0].request.requestDt).getTime() - new Date(cow.cowBirthDt).getTime()) / (1000 * 60 * 60 * 24 * 30.44))}개월`
|
||||||
: '-'}
|
: '-'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -1003,10 +1015,10 @@ export default function CowOverviewPage() {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<span className="w-28 shrink-0 bg-muted/50 px-4 py-3.5 text-base font-medium text-muted-foreground">월령</span>
|
<span className="w-28 shrink-0 bg-muted/50 px-4 py-3.5 text-base font-medium text-muted-foreground">월령 (분석)</span>
|
||||||
<span className="flex-1 px-4 py-3.5 text-base font-bold text-foreground">
|
<span className="flex-1 px-4 py-3.5 text-base font-bold text-foreground">
|
||||||
{cow?.cowBirthDt
|
{cow?.cowBirthDt && genomeData[0]?.request?.requestDt
|
||||||
? `${Math.floor((new Date().getTime() - new Date(cow.cowBirthDt).getTime()) / (1000 * 60 * 60 * 24 * 30.44))}개월`
|
? `${Math.floor((new Date(genomeData[0].request.requestDt).getTime() - new Date(cow.cowBirthDt).getTime()) / (1000 * 60 * 60 * 24 * 30.44))}개월`
|
||||||
: '-'}
|
: '-'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -1130,12 +1142,12 @@ export default function CowOverviewPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="bg-muted/50 px-5 py-3 border-b border-border">
|
<div className="bg-muted/50 px-5 py-3 border-b border-border">
|
||||||
<span className="text-base font-semibold text-muted-foreground">월령</span>
|
<span className="text-base font-semibold text-muted-foreground">월령 (분석일 기준)</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="px-5 py-4">
|
<div className="px-5 py-4">
|
||||||
<span className="text-2xl font-bold text-foreground">
|
<span className="text-2xl font-bold text-foreground">
|
||||||
{cow?.cowBirthDt
|
{cow?.cowBirthDt && genomeData[0]?.request?.requestDt
|
||||||
? `${Math.floor((new Date().getTime() - new Date(cow.cowBirthDt).getTime()) / (1000 * 60 * 60 * 24 * 30.44))}개월`
|
? `${Math.floor((new Date(genomeData[0].request.requestDt).getTime() - new Date(cow.cowBirthDt).getTime()) / (1000 * 60 * 60 * 24 * 30.44))}개월`
|
||||||
: '-'}
|
: '-'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -1168,10 +1180,10 @@ export default function CowOverviewPage() {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<span className="w-28 shrink-0 bg-muted/50 px-4 py-3.5 text-base font-medium text-muted-foreground">월령</span>
|
<span className="w-28 shrink-0 bg-muted/50 px-4 py-3.5 text-base font-medium text-muted-foreground">월령 (분석)</span>
|
||||||
<span className="flex-1 px-4 py-3.5 text-base font-bold text-foreground">
|
<span className="flex-1 px-4 py-3.5 text-base font-bold text-foreground">
|
||||||
{cow?.cowBirthDt
|
{cow?.cowBirthDt && genomeData[0]?.request?.requestDt
|
||||||
? `${Math.floor((new Date().getTime() - new Date(cow.cowBirthDt).getTime()) / (1000 * 60 * 60 * 24 * 30.44))}개월`
|
? `${Math.floor((new Date(genomeData[0].request.requestDt).getTime() - new Date(cow.cowBirthDt).getTime()) / (1000 * 60 * 60 * 24 * 30.44))}개월`
|
||||||
: '-'}
|
: '-'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -1666,14 +1678,10 @@ export default function CowOverviewPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="bg-muted/50 px-5 py-3 border-b border-border">
|
<div className="bg-muted/50 px-5 py-3 border-b border-border">
|
||||||
<span className="text-base font-semibold text-muted-foreground">월령</span>
|
<span className="text-base font-semibold text-muted-foreground">월령 (분석일 기준)</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="px-5 py-4">
|
<div className="px-5 py-4">
|
||||||
<span className="text-2xl font-bold text-foreground">
|
<span className="text-2xl font-bold text-foreground">-</span>
|
||||||
{cow?.cowBirthDt
|
|
||||||
? `${Math.floor((new Date().getTime() - new Date(cow.cowBirthDt).getTime()) / (1000 * 60 * 60 * 24 * 30.44))}개월`
|
|
||||||
: '-'}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -1700,10 +1708,10 @@ export default function CowOverviewPage() {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<span className="w-28 shrink-0 bg-muted/50 px-4 py-3.5 text-base font-medium text-muted-foreground">월령</span>
|
<span className="w-28 shrink-0 bg-muted/50 px-4 py-3.5 text-base font-medium text-muted-foreground">월령 (분석)</span>
|
||||||
<span className="flex-1 px-4 py-3.5 text-base font-bold text-foreground">
|
<span className="flex-1 px-4 py-3.5 text-base font-bold text-foreground">
|
||||||
{cow?.cowBirthDt
|
{cow?.cowBirthDt && genomeData[0]?.request?.requestDt
|
||||||
? `${Math.floor((new Date().getTime() - new Date(cow.cowBirthDt).getTime()) / (1000 * 60 * 60 * 24 * 30.44))}개월`
|
? `${Math.floor((new Date(genomeData[0].request.requestDt).getTime() - new Date(cow.cowBirthDt).getTime()) / (1000 * 60 * 60 * 24 * 30.44))}개월`
|
||||||
: '-'}
|
: '-'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -1792,19 +1800,8 @@ export default function CowOverviewPage() {
|
|||||||
|
|
||||||
{/* 번식능력 탭 */}
|
{/* 번식능력 탭 */}
|
||||||
<TabsContent value="reproduction" className="mt-6 space-y-6">
|
<TabsContent value="reproduction" className="mt-6 space-y-6">
|
||||||
{/* 혈액화학검사(MPT) 테이블 - 추후 사용
|
{/* 혈액화학검사(MPT) 테이블 */}
|
||||||
<MptTable cowShortNo={cowNo?.slice(-4)} cowNo={cowNo} farmNo={cow?.fkFarmNo} cow={cow} genomeRequest={genomeRequest} />
|
<MptTable cowShortNo={cowNo?.slice(-4)} cowNo={cowNo} farmNo={cow?.fkFarmNo} cow={cow} genomeRequest={genomeRequest} />
|
||||||
*/}
|
|
||||||
|
|
||||||
<Card className="bg-slate-50 border border-border rounded-2xl">
|
|
||||||
<CardContent className="p-8 text-center">
|
|
||||||
<Activity className="h-12 w-12 text-muted-foreground mx-auto mb-4" />
|
|
||||||
<h3 className="text-lg font-semibold text-foreground mb-2">번식능력 분석 데이터 없음</h3>
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
이 개체는 아직 번식능력 분석이 완료되지 않았습니다.
|
|
||||||
</p>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -12,22 +12,21 @@ import { MPT_REFERENCE_RANGES } from "@/constants/mpt-reference"
|
|||||||
|
|
||||||
// 혈액화학검사 카테고리별 항목
|
// 혈액화학검사 카테고리별 항목
|
||||||
const MPT_CATEGORIES = [
|
const MPT_CATEGORIES = [
|
||||||
{ name: '에너지 대사', items: ['glucose', 'cholesterol', 'nefa', 'bcs'], color: 'bg-muted/50' },
|
{ name: '에너지', items: ['glucose', 'cholesterol', 'nefa', 'bcs'], color: 'bg-muted/50' },
|
||||||
{ name: '단백질 대사', items: ['totalProtein', 'albumin', 'globulin', 'agRatio', 'bun'], color: 'bg-muted/50' },
|
{ name: '단백질', items: ['totalProtein', 'albumin', 'globulin', 'agRatio', 'bun'], color: 'bg-muted/50' },
|
||||||
{ name: '간기능', items: ['ast', 'ggt', 'fattyLiverIdx'], color: 'bg-muted/50' },
|
{ name: '간기능', items: ['ast', 'ggt', 'fattyLiverIdx'], color: 'bg-muted/50' },
|
||||||
{ name: '미네랄', items: ['calcium', 'phosphorus', 'caPRatio', 'magnesium'], color: 'bg-muted/50' },
|
{ name: '미네랄', items: ['calcium', 'phosphorus', 'caPRatio', 'magnesium'], color: 'bg-muted/50' },
|
||||||
{ name: '기타', items: ['creatine'], color: 'bg-muted/50' },
|
{ name: '별도', items: ['creatine'], color: 'bg-muted/50' },
|
||||||
]
|
]
|
||||||
|
|
||||||
// 측정값 상태 판정
|
// 측정값 상태 판정: 안전(safe) / 주의(caution)
|
||||||
function getMptValueStatus(key: string, value: number | null): 'normal' | 'warning' | 'danger' | 'unknown' {
|
function getMptValueStatus(key: string, value: number | null): 'safe' | 'caution' | 'unknown' {
|
||||||
if (value === null || value === undefined) return 'unknown'
|
if (value === null || value === undefined) return 'unknown'
|
||||||
const ref = MPT_REFERENCE_RANGES[key]
|
const ref = MPT_REFERENCE_RANGES[key]
|
||||||
if (!ref || ref.lowerLimit === null || ref.upperLimit === null) return 'unknown'
|
if (!ref || ref.lowerLimit === null || ref.upperLimit === null) return 'unknown'
|
||||||
if (value >= ref.lowerLimit && value <= ref.upperLimit) return 'normal'
|
// 하한값 ~ 상한값 사이면 안전, 그 외 주의
|
||||||
const margin = (ref.upperLimit - ref.lowerLimit) * 0.1
|
if (value >= ref.lowerLimit && value <= ref.upperLimit) return 'safe'
|
||||||
if (value >= ref.lowerLimit - margin && value <= ref.upperLimit + margin) return 'warning'
|
return 'caution'
|
||||||
return 'danger'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MptTableProps {
|
interface MptTableProps {
|
||||||
@@ -45,11 +44,11 @@ export function MptTable({ cowShortNo, cowNo, farmNo, cow, genomeRequest }: MptT
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchMptData = async () => {
|
const fetchMptData = async () => {
|
||||||
if (!cowShortNo) return
|
if (!cowNo) return
|
||||||
|
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
try {
|
try {
|
||||||
const data = await mptApi.findByCowShortNo(cowShortNo)
|
const data = await mptApi.findByCowId(cowNo)
|
||||||
setMptData(data)
|
setMptData(data)
|
||||||
if (data.length > 0) {
|
if (data.length > 0) {
|
||||||
setSelectedMpt(data[0])
|
setSelectedMpt(data[0])
|
||||||
@@ -62,7 +61,7 @@ export function MptTable({ cowShortNo, cowNo, farmNo, cow, genomeRequest }: MptT
|
|||||||
}
|
}
|
||||||
|
|
||||||
fetchMptData()
|
fetchMptData()
|
||||||
}, [cowShortNo])
|
}, [cowNo])
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
@@ -82,7 +81,7 @@ export function MptTable({ cowShortNo, cowNo, farmNo, cow, genomeRequest }: MptT
|
|||||||
<Card className="bg-white border border-border shadow-sm rounded-2xl overflow-hidden">
|
<Card className="bg-white border border-border shadow-sm rounded-2xl overflow-hidden">
|
||||||
<CardContent className="p-0">
|
<CardContent className="p-0">
|
||||||
{/* 데스크탑: 가로 그리드 */}
|
{/* 데스크탑: 가로 그리드 */}
|
||||||
<div className="hidden lg:grid lg:grid-cols-4 divide-x divide-border">
|
<div className="hidden lg:grid lg:grid-cols-3 divide-x divide-border">
|
||||||
<div>
|
<div>
|
||||||
<div className="bg-muted/50 px-5 py-3 border-b border-border">
|
<div className="bg-muted/50 px-5 py-3 border-b border-border">
|
||||||
<span className="text-base font-semibold text-muted-foreground">개체번호</span>
|
<span className="text-base font-semibold text-muted-foreground">개체번호</span>
|
||||||
@@ -101,25 +100,18 @@ export function MptTable({ cowShortNo, cowNo, farmNo, cow, genomeRequest }: MptT
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<div className="bg-muted/50 px-5 py-3 border-b border-border">
|
|
||||||
<span className="text-base font-semibold text-muted-foreground">월령</span>
|
|
||||||
</div>
|
|
||||||
<div className="px-5 py-4">
|
|
||||||
<span className="text-2xl font-bold text-foreground">
|
|
||||||
{cow?.cowBirthDt
|
|
||||||
? `${Math.floor((new Date().getTime() - new Date(cow.cowBirthDt).getTime()) / (1000 * 60 * 60 * 24 * 30.44))}개월`
|
|
||||||
: '-'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
<div>
|
||||||
<div className="bg-muted/50 px-5 py-3 border-b border-border">
|
<div className="bg-muted/50 px-5 py-3 border-b border-border">
|
||||||
<span className="text-base font-semibold text-muted-foreground">성별</span>
|
<span className="text-base font-semibold text-muted-foreground">성별</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="px-5 py-4">
|
<div className="px-5 py-4">
|
||||||
<span className="text-2xl font-bold text-foreground">
|
<span className="text-2xl font-bold text-foreground">
|
||||||
{cow?.cowSex === 'F' ? '암' : cow?.cowSex === 'M' ? '수' : '-'}
|
{(() => {
|
||||||
|
const sex = cow?.cowSex?.toUpperCase?.() || cow?.cowSex
|
||||||
|
if (sex === 'F' || sex === '암' || sex === '2') return '암소'
|
||||||
|
if (sex === 'M' || sex === '수' || sex === '1') return '수소'
|
||||||
|
return '-'
|
||||||
|
})()}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -138,171 +130,21 @@ export function MptTable({ cowShortNo, cowNo, farmNo, cow, genomeRequest }: MptT
|
|||||||
{cow?.cowBirthDt ? new Date(cow.cowBirthDt).toLocaleDateString('ko-KR') : '-'}
|
{cow?.cowBirthDt ? new Date(cow.cowBirthDt).toLocaleDateString('ko-KR') : '-'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center">
|
|
||||||
<span className="w-28 shrink-0 bg-muted/50 px-4 py-3.5 text-base font-medium text-muted-foreground">월령</span>
|
|
||||||
<span className="flex-1 px-4 py-3.5 text-base font-bold text-foreground">
|
|
||||||
{cow?.cowBirthDt
|
|
||||||
? `${Math.floor((new Date().getTime() - new Date(cow.cowBirthDt).getTime()) / (1000 * 60 * 60 * 24 * 30.44))}개월`
|
|
||||||
: '-'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<span className="w-28 shrink-0 bg-muted/50 px-4 py-3.5 text-base font-medium text-muted-foreground">성별</span>
|
<span className="w-28 shrink-0 bg-muted/50 px-4 py-3.5 text-base font-medium text-muted-foreground">성별</span>
|
||||||
<span className="flex-1 px-4 py-3.5 text-base font-bold text-foreground">
|
<span className="flex-1 px-4 py-3.5 text-base font-bold text-foreground">
|
||||||
{cow?.cowSex === 'F' ? '암' : cow?.cowSex === 'M' ? '수' : '-'}
|
{(() => {
|
||||||
|
const sex = cow?.cowSex?.toUpperCase?.() || cow?.cowSex
|
||||||
|
if (sex === 'F' || sex === '암' || sex === '2') return '암소'
|
||||||
|
if (sex === 'M' || sex === '수' || sex === '1') return '수소'
|
||||||
|
return '-'
|
||||||
|
})()}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* 혈통정보 섹션 */}
|
|
||||||
<h3 className="text-lg lg:text-xl font-bold text-foreground">혈통정보</h3>
|
|
||||||
<Card className="bg-white border border-border shadow-sm rounded-2xl overflow-hidden">
|
|
||||||
<CardContent className="p-0">
|
|
||||||
{/* 데스크탑: 가로 그리드 */}
|
|
||||||
<div className="hidden lg:grid lg:grid-cols-2 divide-x divide-border">
|
|
||||||
<div>
|
|
||||||
<div className="bg-muted/50 px-5 py-3 border-b border-border">
|
|
||||||
<span className="text-base font-semibold text-muted-foreground">부 KPN번호</span>
|
|
||||||
</div>
|
|
||||||
<div className="px-5 py-4 flex items-center justify-between gap-3">
|
|
||||||
<span className="text-2xl font-bold text-foreground break-all">
|
|
||||||
{cow?.sireKpn && cow.sireKpn !== '0' ? cow.sireKpn : '-'}
|
|
||||||
</span>
|
|
||||||
{(() => {
|
|
||||||
const chipSireName = genomeRequest?.chipSireName
|
|
||||||
if (chipSireName === '일치') {
|
|
||||||
return (
|
|
||||||
<span className="flex items-center gap-1.5 bg-muted/50 text-primary-foreground text-sm font-semibold px-3 py-1.5 rounded-full shrink-0">
|
|
||||||
<CheckCircle2 className="w-4 h-4" />
|
|
||||||
<span>일치</span>
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
} else if (chipSireName && chipSireName !== '일치') {
|
|
||||||
return (
|
|
||||||
<span className="flex items-center gap-1.5 bg-red-500 text-white text-sm font-semibold px-3 py-1.5 rounded-full shrink-0">
|
|
||||||
<XCircle className="w-4 h-4" />
|
|
||||||
<span>불일치</span>
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
})()}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className="bg-muted/50 px-5 py-3 border-b border-border">
|
|
||||||
<span className="text-base font-semibold text-muted-foreground">모 개체번호</span>
|
|
||||||
</div>
|
|
||||||
<div className="px-5 py-4 flex items-center justify-between gap-3">
|
|
||||||
{cow?.damCowId && cow.damCowId !== '0' ? (
|
|
||||||
<CowNumberDisplay cowId={cow.damCowId} variant="highlight" className="text-2xl font-bold text-foreground" />
|
|
||||||
) : (
|
|
||||||
<span className="text-2xl font-bold text-foreground">-</span>
|
|
||||||
)}
|
|
||||||
{(() => {
|
|
||||||
const chipDamName = genomeRequest?.chipDamName
|
|
||||||
if (chipDamName === '일치') {
|
|
||||||
return (
|
|
||||||
<span className="flex items-center gap-1.5 bg-muted/50 text-primary-foreground text-sm font-semibold px-3 py-1.5 rounded-full shrink-0">
|
|
||||||
<CheckCircle2 className="w-4 h-4" />
|
|
||||||
<span>일치</span>
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
} else if (chipDamName === '불일치') {
|
|
||||||
return (
|
|
||||||
<span className="flex items-center gap-1.5 bg-red-500 text-white text-sm font-semibold px-3 py-1.5 rounded-full shrink-0">
|
|
||||||
<XCircle className="w-4 h-4" />
|
|
||||||
<span>불일치</span>
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
} else if (chipDamName === '이력제부재') {
|
|
||||||
return (
|
|
||||||
<span className="flex items-center gap-1.5 bg-amber-500 text-white text-sm font-semibold px-3 py-1.5 rounded-full shrink-0">
|
|
||||||
<XCircle className="w-4 h-4" />
|
|
||||||
<span>이력제부재</span>
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
})()}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/* 모바일: 좌우 배치 리스트 */}
|
|
||||||
<div className="lg:hidden divide-y divide-border">
|
|
||||||
<div className="flex items-center">
|
|
||||||
<span className="w-28 shrink-0 bg-muted/50 px-4 py-3.5 text-base font-medium text-muted-foreground">부 KPN번호</span>
|
|
||||||
<div className="flex-1 px-4 py-3.5 flex items-center justify-between gap-2">
|
|
||||||
<span className="text-base font-bold text-foreground break-all">
|
|
||||||
{cow?.sireKpn && cow.sireKpn !== '0' ? cow.sireKpn : '-'}
|
|
||||||
</span>
|
|
||||||
{(() => {
|
|
||||||
const chipSireName = genomeRequest?.chipSireName
|
|
||||||
if (chipSireName === '일치') {
|
|
||||||
return (
|
|
||||||
<span className="flex items-center gap-1 bg-muted/50 text-primary-foreground text-xs font-semibold px-2 py-1 rounded-full shrink-0">
|
|
||||||
<CheckCircle2 className="w-3 h-3" />
|
|
||||||
<span>일치</span>
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
} else if (chipSireName && chipSireName !== '일치') {
|
|
||||||
return (
|
|
||||||
<span className="flex items-center gap-1 bg-red-500 text-white text-xs font-semibold px-2 py-1 rounded-full shrink-0">
|
|
||||||
<XCircle className="w-3 h-3" />
|
|
||||||
<span>불일치</span>
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
})()}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<span className="w-28 shrink-0 bg-muted/50 px-4 py-3.5 text-base font-medium text-muted-foreground">모 개체번호</span>
|
|
||||||
<div className="flex-1 px-4 py-3.5 flex items-center justify-between gap-2">
|
|
||||||
{cow?.damCowId && cow.damCowId !== '0' ? (
|
|
||||||
<CowNumberDisplay cowId={cow.damCowId} variant="highlight" className="text-base font-bold text-foreground" />
|
|
||||||
) : (
|
|
||||||
<span className="text-base font-bold text-foreground">-</span>
|
|
||||||
)}
|
|
||||||
{(() => {
|
|
||||||
const chipDamName = genomeRequest?.chipDamName
|
|
||||||
if (chipDamName === '일치') {
|
|
||||||
return (
|
|
||||||
<span className="flex items-center gap-1 bg-muted/50 text-primary-foreground text-xs font-semibold px-2 py-1 rounded-full shrink-0">
|
|
||||||
<CheckCircle2 className="w-3 h-3" />
|
|
||||||
<span>일치</span>
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
} else if (chipDamName === '불일치') {
|
|
||||||
return (
|
|
||||||
<span className="flex items-center gap-1 bg-red-500 text-white text-xs font-semibold px-2 py-1 rounded-full shrink-0">
|
|
||||||
<XCircle className="w-3 h-3" />
|
|
||||||
<span>불일치</span>
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
} else if (chipDamName === '이력제부재') {
|
|
||||||
return (
|
|
||||||
<span className="flex items-center gap-1 bg-amber-500 text-white text-xs font-semibold px-2 py-1 rounded-full shrink-0">
|
|
||||||
<XCircle className="w-3 h-3" />
|
|
||||||
<span>이력제부재</span>
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
})()}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* 검사 정보 */}
|
{/* 검사 정보 */}
|
||||||
{selectedMpt && (
|
{selectedMpt && (
|
||||||
<>
|
<>
|
||||||
@@ -383,7 +225,9 @@ export function MptTable({ cowShortNo, cowNo, farmNo, cow, genomeRequest }: MptT
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 혈액화학검사 결과 테이블 */}
|
{/* 혈액화학검사 결과 테이블 - selectedMpt가 있을 때만 표시 */}
|
||||||
|
{selectedMpt ? (
|
||||||
|
<>
|
||||||
<h3 className="text-lg lg:text-xl font-bold text-foreground">혈액화학검사 결과</h3>
|
<h3 className="text-lg lg:text-xl font-bold text-foreground">혈액화학검사 결과</h3>
|
||||||
<Card className="bg-white border border-border shadow-sm rounded-2xl overflow-hidden">
|
<Card className="bg-white border border-border shadow-sm rounded-2xl overflow-hidden">
|
||||||
<CardContent className="p-0">
|
<CardContent className="p-0">
|
||||||
@@ -391,13 +235,13 @@ export function MptTable({ cowShortNo, cowNo, farmNo, cow, genomeRequest }: MptT
|
|||||||
<table className="w-full">
|
<table className="w-full">
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="bg-muted/50 border-b border-border">
|
<tr className="bg-muted/50 border-b border-border">
|
||||||
<th className="px-4 py-3 text-left text-sm font-semibold text-muted-foreground w-28">카테고리</th>
|
<th className="px-4 py-3 text-center text-sm font-semibold text-muted-foreground" style={{ width: '12%' }}>카테고리</th>
|
||||||
<th className="px-4 py-3 text-left text-sm font-semibold text-muted-foreground">검사항목</th>
|
<th className="px-4 py-3 text-left text-sm font-semibold text-muted-foreground" style={{ width: '18%' }}>검사항목</th>
|
||||||
<th className="px-4 py-3 text-center text-sm font-semibold text-muted-foreground w-24">측정값</th>
|
<th className="px-4 py-3 text-center text-sm font-semibold text-muted-foreground" style={{ width: '15%' }}>측정값</th>
|
||||||
<th className="px-4 py-3 text-center text-sm font-semibold text-muted-foreground w-20">하한값</th>
|
<th className="px-4 py-3 text-center text-sm font-semibold text-muted-foreground" style={{ width: '12%' }}>하한값</th>
|
||||||
<th className="px-4 py-3 text-center text-sm font-semibold text-muted-foreground w-20">상한값</th>
|
<th className="px-4 py-3 text-center text-sm font-semibold text-muted-foreground" style={{ width: '12%' }}>상한값</th>
|
||||||
<th className="px-4 py-3 text-center text-sm font-semibold text-muted-foreground w-20">단위</th>
|
<th className="px-4 py-3 text-center text-sm font-semibold text-muted-foreground" style={{ width: '15%' }}>단위</th>
|
||||||
<th className="px-4 py-3 text-center text-sm font-semibold text-muted-foreground w-20">상태</th>
|
<th className="px-4 py-3 text-center text-sm font-semibold text-muted-foreground" style={{ width: '16%' }}>상태</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -420,9 +264,8 @@ export function MptTable({ cowShortNo, cowNo, farmNo, cow, genomeRequest }: MptT
|
|||||||
<td className="px-4 py-3 text-sm font-medium text-foreground">{ref?.name || itemKey}</td>
|
<td className="px-4 py-3 text-sm font-medium text-foreground">{ref?.name || itemKey}</td>
|
||||||
<td className="px-4 py-3 text-center">
|
<td className="px-4 py-3 text-center">
|
||||||
<span className={`text-lg font-bold ${
|
<span className={`text-lg font-bold ${
|
||||||
status === 'normal' ? 'text-green-600' :
|
status === 'safe' ? 'text-green-600' :
|
||||||
status === 'warning' ? 'text-amber-600' :
|
status === 'caution' ? 'text-amber-600' :
|
||||||
status === 'danger' ? 'text-red-600' :
|
|
||||||
'text-muted-foreground'
|
'text-muted-foreground'
|
||||||
}`}>
|
}`}>
|
||||||
{value !== null && value !== undefined ? value.toFixed(2) : '-'}
|
{value !== null && value !== undefined ? value.toFixed(2) : '-'}
|
||||||
@@ -434,14 +277,11 @@ export function MptTable({ cowShortNo, cowNo, farmNo, cow, genomeRequest }: MptT
|
|||||||
<td className="px-4 py-3 text-center">
|
<td className="px-4 py-3 text-center">
|
||||||
{value !== null && value !== undefined ? (
|
{value !== null && value !== undefined ? (
|
||||||
<span className={`inline-flex items-center px-2 py-1 rounded-full text-xs font-semibold ${
|
<span className={`inline-flex items-center px-2 py-1 rounded-full text-xs font-semibold ${
|
||||||
status === 'normal' ? 'bg-green-100 text-green-700' :
|
status === 'safe' ? 'bg-green-100 text-green-700' :
|
||||||
status === 'warning' ? 'bg-amber-100 text-amber-700' :
|
status === 'caution' ? 'bg-amber-100 text-amber-700' :
|
||||||
status === 'danger' ? 'bg-red-100 text-red-700' :
|
|
||||||
'bg-slate-100 text-slate-500'
|
'bg-slate-100 text-slate-500'
|
||||||
}`}>
|
}`}>
|
||||||
{status === 'normal' ? '정상' :
|
{status === 'safe' ? '안전' : status === 'caution' ? '주의' : '-'}
|
||||||
status === 'warning' ? '주의' :
|
|
||||||
status === 'danger' ? '이상' : '-'}
|
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<span className="text-muted-foreground">-</span>
|
<span className="text-muted-foreground">-</span>
|
||||||
@@ -479,19 +319,19 @@ export function MptTable({ cowShortNo, cowNo, farmNo, cow, genomeRequest }: MptT
|
|||||||
</Card>
|
</Card>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
</>
|
||||||
{/* 데이터 없음 안내 */}
|
) : (
|
||||||
{/* {!selectedMpt && (
|
/* 데이터 없음 안내 */
|
||||||
<Card className="bg-slate-50 border border-border rounded-2xl">
|
<Card className="bg-slate-50 border border-border rounded-2xl">
|
||||||
<CardContent className="p-8 text-center">
|
<CardContent className="p-8 text-center">
|
||||||
<Activity className="h-12 w-12 text-muted-foreground mx-auto mb-4" />
|
<Activity className="h-12 w-12 text-muted-foreground mx-auto mb-4" />
|
||||||
<h3 className="text-lg font-semibold text-foreground mb-2">혈액화학검사 데이터 없음</h3>
|
<h3 className="text-lg font-semibold text-foreground mb-2">번식능력검사 데이터 없음</h3>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
이 개체는 아직 혈액화학검사(MPT) 결과가 등록되지 않았습니다.
|
이 개체는 번식능력검사를 진행하지 않아 분석보고서를 제공할 수 없습니다.
|
||||||
</p>
|
</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
)} */}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,8 +56,11 @@ interface CowWithGenes extends Cow {
|
|||||||
rank?: number // 랭킹 순위
|
rank?: number // 랭킹 순위
|
||||||
cowShortNo?: string // 개체 요약번호
|
cowShortNo?: string // 개체 요약번호
|
||||||
cowReproType?: string // 번식 타입
|
cowReproType?: string // 번식 타입
|
||||||
anlysDt?: string // 분석일자
|
anlysDt?: string // 분석일자 (유전체)
|
||||||
unavailableReason?: string // 분석불가 사유 (부불일치, 모불일치, 모이력제부재 등)
|
unavailableReason?: string // 분석불가 사유 (부불일치, 모불일치, 모이력제부재 등)
|
||||||
|
hasMpt?: boolean // 번식능력검사(MPT) 여부
|
||||||
|
mptTestDt?: string // MPT 검사일
|
||||||
|
mptMonthAge?: number // MPT 검사일 기준 월령
|
||||||
}
|
}
|
||||||
|
|
||||||
function MyCowContent() {
|
function MyCowContent() {
|
||||||
@@ -75,7 +78,7 @@ function MyCowContent() {
|
|||||||
const [rankingMode, setRankingMode] = useState<'gene' | 'genome'>('gene') // 유전자순/유전체순
|
const [rankingMode, setRankingMode] = useState<'gene' | 'genome'>('gene') // 유전자순/유전체순
|
||||||
const [sortBy, setSortBy] = useState<string>('rank') // 정렬 기준 (기본: 순위)
|
const [sortBy, setSortBy] = useState<string>('rank') // 정렬 기준 (기본: 순위)
|
||||||
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('asc') // 정렬 방향
|
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('asc') // 정렬 방향
|
||||||
const [analysisFilter, setAnalysisFilter] = useState<'all' | 'completed' | 'unavailable'>('all') // 분석 상태 필터
|
const [analysisFilter, setAnalysisFilter] = useState<'all' | 'completed' | 'mptOnly' | 'unavailable'>('all') // 분석 상태 필터
|
||||||
|
|
||||||
// 커스텀 컬럼 표시 필터
|
// 커스텀 컬럼 표시 필터
|
||||||
const [selectedDisplayGenes, setSelectedDisplayGenes] = useState<string[]>([]) // 테이블에 표시할 유전자
|
const [selectedDisplayGenes, setSelectedDisplayGenes] = useState<string[]>([]) // 테이블에 표시할 유전자
|
||||||
@@ -355,6 +358,12 @@ function MyCowContent() {
|
|||||||
anlysDt: item.entity.anlysDt ?? null,
|
anlysDt: item.entity.anlysDt ?? null,
|
||||||
// 분석불가 사유
|
// 분석불가 사유
|
||||||
unavailableReason: item.entity.unavailableReason ?? null,
|
unavailableReason: item.entity.unavailableReason ?? null,
|
||||||
|
// 번식능력검사(MPT) 여부
|
||||||
|
hasMpt: item.entity.hasMpt ?? false,
|
||||||
|
// MPT 검사일
|
||||||
|
mptTestDt: item.entity.mptTestDt ?? null,
|
||||||
|
// MPT 월령
|
||||||
|
mptMonthAge: item.entity.mptMonthAge ?? null,
|
||||||
//====================================================================================================================
|
//====================================================================================================================
|
||||||
// 형질 데이터 (백엔드에서 계산됨, 형질명 → 표준화육종가 매핑)
|
// 형질 데이터 (백엔드에서 계산됨, 형질명 → 표준화육종가 매핑)
|
||||||
// 백엔드 응답: { traitName: string, traitVal: number, traitEbv: number, traitPercentile: number }
|
// 백엔드 응답: { traitName: string, traitVal: number, traitEbv: number, traitPercentile: number }
|
||||||
@@ -513,9 +522,14 @@ function MyCowContent() {
|
|||||||
|
|
||||||
// 분석 상태 필터
|
// 분석 상태 필터
|
||||||
if (analysisFilter === 'completed') {
|
if (analysisFilter === 'completed') {
|
||||||
|
// 유전체 완료
|
||||||
result = result.filter(cow => cow.genomeScore !== undefined && cow.genomeScore !== null)
|
result = result.filter(cow => cow.genomeScore !== undefined && cow.genomeScore !== null)
|
||||||
|
} else if (analysisFilter === 'mptOnly') {
|
||||||
|
// 번식능력 검사 완료 (유전체 유무 상관없이)
|
||||||
|
result = result.filter(cow => cow.hasMpt === true)
|
||||||
} else if (analysisFilter === 'unavailable') {
|
} else if (analysisFilter === 'unavailable') {
|
||||||
result = result.filter(cow => cow.genomeScore === undefined || cow.genomeScore === null)
|
// 유전체 분석불가 (부불일치, 모불일치 등)
|
||||||
|
result = result.filter(cow => cow.unavailableReason !== null && cow.unavailableReason !== undefined)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 정렬 (sortBy가 'none'이면 정렬하지 않음 - 전역 필터 순서 유지)
|
// 정렬 (sortBy가 'none'이면 정렬하지 않음 - 전역 필터 순서 유지)
|
||||||
@@ -684,11 +698,11 @@ function MyCowContent() {
|
|||||||
<p className="text-base max-sm:text-sm text-slate-500 mt-1">{'농장'} 보유 개체 현황</p>
|
<p className="text-base max-sm:text-sm text-slate-500 mt-1">{'농장'} 보유 개체 현황</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 분석 상태 탭 필터 */}
|
{/* 분석 상태 탭 필터 - 모바일: 2x2 그리드, 데스크톱: 가로 배치 */}
|
||||||
<div className="flex rounded-lg border border-slate-200 bg-slate-50 p-1.5 gap-1.5 max-sm:p-1 max-sm:gap-1">
|
<div className="grid grid-cols-2 sm:grid-cols-4 rounded-lg border border-slate-200 bg-slate-50 p-1.5 gap-1.5 max-sm:p-1 max-sm:gap-1">
|
||||||
<button
|
<button
|
||||||
onClick={() => setAnalysisFilter('all')}
|
onClick={() => setAnalysisFilter('all')}
|
||||||
className={`flex-1 px-3 py-2.5 max-sm:px-2 max-sm:py-2 rounded-md text-base max-sm:text-sm font-medium transition-colors ${analysisFilter === 'all'
|
className={`px-3 py-2.5 max-sm:px-2 max-sm:py-2 rounded-md text-base max-sm:text-sm font-medium transition-colors ${analysisFilter === 'all'
|
||||||
? 'bg-white text-slate-900 shadow-sm border border-slate-200'
|
? 'bg-white text-slate-900 shadow-sm border border-slate-200'
|
||||||
: 'text-slate-500 hover:text-slate-700'
|
: 'text-slate-500 hover:text-slate-700'
|
||||||
}`}
|
}`}
|
||||||
@@ -697,26 +711,38 @@ function MyCowContent() {
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setAnalysisFilter('completed')}
|
onClick={() => setAnalysisFilter('completed')}
|
||||||
className={`flex-1 px-3 py-2.5 max-sm:px-2 max-sm:py-2 rounded-md text-base max-sm:text-sm font-medium transition-colors ${analysisFilter === 'completed'
|
className={`px-3 py-2.5 max-sm:px-2 max-sm:py-2 rounded-md text-base max-sm:text-sm font-medium transition-colors ${analysisFilter === 'completed'
|
||||||
? 'bg-white text-emerald-600 shadow-sm border border-slate-200'
|
? 'bg-white text-emerald-600 shadow-sm border border-slate-200'
|
||||||
: 'text-slate-500 hover:text-slate-700'
|
: 'text-slate-500 hover:text-slate-700'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<span className="flex items-center justify-center gap-1.5 max-sm:gap-1">
|
<span className="flex items-center justify-center gap-1.5 max-sm:gap-1">
|
||||||
<span className="w-2 h-2 rounded-full bg-emerald-500"></span>
|
<span className="w-2 h-2 rounded-full bg-emerald-500"></span>
|
||||||
완료 <span className="font-bold">{cows.filter(c => c.genomeScore !== undefined && c.genomeScore !== null).length}</span>
|
유전체 <span className="font-bold">{cows.filter(c => c.genomeScore !== undefined && c.genomeScore !== null).length}</span>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setAnalysisFilter('unavailable')}
|
onClick={() => setAnalysisFilter('mptOnly')}
|
||||||
className={`flex-1 px-3 py-2.5 max-sm:px-2 max-sm:py-2 rounded-md text-base max-sm:text-sm font-medium transition-colors ${analysisFilter === 'unavailable'
|
className={`px-3 py-2.5 max-sm:px-2 max-sm:py-2 rounded-md text-base max-sm:text-sm font-medium transition-colors ${analysisFilter === 'mptOnly'
|
||||||
? 'bg-white text-slate-600 shadow-sm border border-slate-200'
|
? 'bg-white text-amber-600 shadow-sm border border-slate-200'
|
||||||
: 'text-slate-500 hover:text-slate-700'
|
: 'text-slate-500 hover:text-slate-700'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<span className="flex items-center justify-center gap-1.5 max-sm:gap-1">
|
<span className="flex items-center justify-center gap-1.5 max-sm:gap-1">
|
||||||
<span className="w-2 h-2 rounded-full bg-slate-400"></span>
|
<span className="w-2 h-2 rounded-full bg-amber-500"></span>
|
||||||
미검사 <span className="font-bold">{cows.filter(c => c.genomeScore === undefined || c.genomeScore === null).length}</span>
|
번식능력 <span className="font-bold">{cows.filter(c => c.hasMpt === true).length}</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setAnalysisFilter('unavailable')}
|
||||||
|
className={`px-3 py-2.5 max-sm:px-2 max-sm:py-2 rounded-md text-base max-sm:text-sm font-medium transition-colors ${analysisFilter === 'unavailable'
|
||||||
|
? 'bg-white text-red-600 shadow-sm border border-slate-200'
|
||||||
|
: 'text-slate-500 hover:text-slate-700'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span className="flex items-center justify-center gap-1.5 max-sm:gap-1">
|
||||||
|
<span className="w-2 h-2 rounded-full bg-red-400"></span>
|
||||||
|
분석불가 <span className="font-bold">{cows.filter(c => c.unavailableReason !== null && c.unavailableReason !== undefined).length}</span>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -923,11 +949,15 @@ function MyCowContent() {
|
|||||||
<th className="cow-table-header" style={{ width: '50px' }}>순위</th>
|
<th className="cow-table-header" style={{ width: '50px' }}>순위</th>
|
||||||
<th className="cow-table-header" style={{ width: '220px' }}>개체번호</th>
|
<th className="cow-table-header" style={{ width: '220px' }}>개체번호</th>
|
||||||
<th className="cow-table-header" style={{ width: '90px' }}>생년월일</th>
|
<th className="cow-table-header" style={{ width: '90px' }}>생년월일</th>
|
||||||
<th className="cow-table-header" style={{ width: '70px' }}>월령</th>
|
|
||||||
<th className="cow-table-header" style={{ width: '60px' }}>성별</th>
|
<th className="cow-table-header" style={{ width: '60px' }}>성별</th>
|
||||||
<th className="cow-table-header" style={{ width: '100px' }}>모개체번호</th>
|
<th className="cow-table-header" style={{ width: '100px' }}>모개체번호</th>
|
||||||
<th className="cow-table-header" style={{ width: '90px' }}>아비 KPN</th>
|
<th className="cow-table-header" style={{ width: '90px' }}>아비 KPN</th>
|
||||||
<th className="cow-table-header" style={{ width: '90px' }}>분석일자</th>
|
<th className="cow-table-header" style={{ width: '80px' }}>
|
||||||
|
{analysisFilter === 'mptOnly' ? '월령(검사일)' : '월령(분석일)'}
|
||||||
|
</th>
|
||||||
|
<th className="cow-table-header" style={{ width: '90px' }}>
|
||||||
|
{analysisFilter === 'mptOnly' ? '검사일자' : '분석일자'}
|
||||||
|
</th>
|
||||||
<th className="cow-table-header border-r-2 border-r-gray-300" style={{ width: '100px' }}>
|
<th className="cow-table-header border-r-2 border-r-gray-300" style={{ width: '100px' }}>
|
||||||
선발지수
|
선발지수
|
||||||
</th>
|
</th>
|
||||||
@@ -958,19 +988,26 @@ function MyCowContent() {
|
|||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="cow-table-cell">
|
<td className="cow-table-cell">
|
||||||
{cow.cowBirthDt && new Date(cow.cowBirthDt).toLocaleDateString('ko-KR', {
|
{(() => {
|
||||||
year: '2-digit',
|
// 번식능력만 있는 개체 판단 (유전체 데이터 없음)
|
||||||
month: '2-digit',
|
const hasMptOnly = cow.hasMpt && !cow.genomeScore && !cow.anlysDt
|
||||||
day: '2-digit'
|
// 번식능력 탭이거나 번식능력만 있는 개체: cowBirthDt 없으면 MPT로 역산
|
||||||
})}
|
if ((analysisFilter === 'mptOnly' || hasMptOnly) && !cow.cowBirthDt && cow.mptTestDt && cow.mptMonthAge) {
|
||||||
</td>
|
const testDate = new Date(cow.mptTestDt)
|
||||||
<td className="cow-table-cell">
|
const birthDate = new Date(testDate)
|
||||||
{cow.cowBirthDt ? (() => {
|
birthDate.setMonth(birthDate.getMonth() - cow.mptMonthAge)
|
||||||
const birthDate = new Date(cow.cowBirthDt)
|
return birthDate.toLocaleDateString('ko-KR', {
|
||||||
const today = new Date()
|
year: '2-digit',
|
||||||
const ageInMonths = Math.floor((today.getTime() - birthDate.getTime()) / (1000 * 60 * 60 * 24 * 30.44))
|
month: '2-digit',
|
||||||
return `${ageInMonths}개월`
|
day: '2-digit'
|
||||||
})() : '-'}
|
})
|
||||||
|
}
|
||||||
|
return cow.cowBirthDt ? new Date(cow.cowBirthDt).toLocaleDateString('ko-KR', {
|
||||||
|
year: '2-digit',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit'
|
||||||
|
}) : '-'
|
||||||
|
})()}
|
||||||
</td>
|
</td>
|
||||||
<td className="cow-table-cell">
|
<td className="cow-table-cell">
|
||||||
{cow.cowSex === "수" ? "수소" : "암소"}
|
{cow.cowSex === "수" ? "수소" : "암소"}
|
||||||
@@ -982,19 +1019,52 @@ function MyCowContent() {
|
|||||||
{cow.sireKpn && cow.sireKpn !== '0' ? cow.sireKpn : '-'}
|
{cow.sireKpn && cow.sireKpn !== '0' ? cow.sireKpn : '-'}
|
||||||
</td>
|
</td>
|
||||||
<td className="cow-table-cell">
|
<td className="cow-table-cell">
|
||||||
{cow.anlysDt ? new Date(cow.anlysDt).toLocaleDateString('ko-KR', {
|
{(() => {
|
||||||
year: '2-digit',
|
// 번식능력만 있는 개체 판단
|
||||||
month: '2-digit',
|
const hasMptOnly = cow.hasMpt && !cow.genomeScore && !cow.anlysDt
|
||||||
day: '2-digit'
|
// 번식능력 탭이거나 번식능력만 있는 개체: MPT 월령 사용
|
||||||
}) : cow.unavailableReason ? (
|
if (analysisFilter === 'mptOnly' || hasMptOnly) {
|
||||||
<span className={`inline-flex items-center px-2 py-0.5 rounded-full text-xs font-semibold ${
|
return cow.mptMonthAge ? `${cow.mptMonthAge}개월` : '-'
|
||||||
cow.unavailableReason.includes('부') ? 'bg-red-100 text-red-700' :
|
}
|
||||||
cow.unavailableReason.includes('모') ? 'bg-orange-100 text-orange-700' :
|
if (cow.cowBirthDt && cow.anlysDt) {
|
||||||
'bg-slate-100 text-slate-600'
|
const birthDate = new Date(cow.cowBirthDt)
|
||||||
}`}>
|
const refDate = new Date(cow.anlysDt)
|
||||||
{cow.unavailableReason}
|
const ageInMonths = Math.floor((refDate.getTime() - birthDate.getTime()) / (1000 * 60 * 60 * 24 * 30.44))
|
||||||
</span>
|
return `${ageInMonths}개월`
|
||||||
) : '-'}
|
}
|
||||||
|
return '-'
|
||||||
|
})()}
|
||||||
|
</td>
|
||||||
|
<td className="cow-table-cell">
|
||||||
|
{(() => {
|
||||||
|
// 번식능력만 있는 개체 판단
|
||||||
|
const hasMptOnly = cow.hasMpt && !cow.genomeScore && !cow.anlysDt
|
||||||
|
// 번식능력 탭이거나 번식능력만 있는 개체: MPT 검사일 사용
|
||||||
|
if (analysisFilter === 'mptOnly' || hasMptOnly) {
|
||||||
|
return cow.mptTestDt ? new Date(cow.mptTestDt).toLocaleDateString('ko-KR', {
|
||||||
|
year: '2-digit',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit'
|
||||||
|
}) : '-'
|
||||||
|
}
|
||||||
|
// 유전체 탭: unavailableReason 있으면 배지, 없으면 분석일자
|
||||||
|
if (cow.unavailableReason) {
|
||||||
|
return (
|
||||||
|
<span className={`inline-flex items-center px-2 py-0.5 rounded-full text-xs font-semibold ${
|
||||||
|
cow.unavailableReason.includes('부') ? 'bg-red-100 text-red-700' :
|
||||||
|
cow.unavailableReason.includes('모') ? 'bg-orange-100 text-orange-700' :
|
||||||
|
'bg-slate-100 text-slate-600'
|
||||||
|
}`}>
|
||||||
|
{cow.unavailableReason}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return cow.anlysDt ? new Date(cow.anlysDt).toLocaleDateString('ko-KR', {
|
||||||
|
year: '2-digit',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit'
|
||||||
|
}) : '-'
|
||||||
|
})()}
|
||||||
</td>
|
</td>
|
||||||
<td className="cow-table-cell border-r-2 border-r-gray-300 !py-2 !px-0.5">
|
<td className="cow-table-cell border-r-2 border-r-gray-300 !py-2 !px-0.5">
|
||||||
{(cow.genomeScore !== undefined && cow.genomeScore !== null) ? (
|
{(cow.genomeScore !== undefined && cow.genomeScore !== null) ? (
|
||||||
@@ -1169,13 +1239,40 @@ function MyCowContent() {
|
|||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<span className="text-muted-foreground">생년월일</span>
|
<span className="text-muted-foreground">생년월일</span>
|
||||||
<span className="font-medium">
|
<span className="font-medium">
|
||||||
{cow.cowBirthDt ? new Date(cow.cowBirthDt).toLocaleDateString('ko-KR', { year: '2-digit', month: 'numeric', day: 'numeric' }) : '-'}
|
{(() => {
|
||||||
|
// 번식능력만 있는 개체 판단
|
||||||
|
const hasMptOnly = cow.hasMpt && !cow.genomeScore && !cow.anlysDt
|
||||||
|
// 번식능력 탭이거나 번식능력만 있는 개체: cowBirthDt 없으면 MPT로 역산
|
||||||
|
if ((analysisFilter === 'mptOnly' || hasMptOnly) && !cow.cowBirthDt && cow.mptTestDt && cow.mptMonthAge) {
|
||||||
|
const testDate = new Date(cow.mptTestDt)
|
||||||
|
const birthDate = new Date(testDate)
|
||||||
|
birthDate.setMonth(birthDate.getMonth() - cow.mptMonthAge)
|
||||||
|
return birthDate.toLocaleDateString('ko-KR', { year: '2-digit', month: 'numeric', day: 'numeric' })
|
||||||
|
}
|
||||||
|
return cow.cowBirthDt ? new Date(cow.cowBirthDt).toLocaleDateString('ko-KR', { year: '2-digit', month: 'numeric', day: 'numeric' }) : '-'
|
||||||
|
})()}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<span className="text-muted-foreground">월령</span>
|
<span className="text-muted-foreground">
|
||||||
|
{(() => {
|
||||||
|
const hasMptOnly = cow.hasMpt && !cow.genomeScore && !cow.anlysDt
|
||||||
|
return (analysisFilter === 'mptOnly' || hasMptOnly) ? '월령 (검사일)' : '월령 (분석일)'
|
||||||
|
})()}
|
||||||
|
</span>
|
||||||
<span className="font-medium">
|
<span className="font-medium">
|
||||||
{cow.cowBirthDt ? `${Math.floor((new Date().getTime() - new Date(cow.cowBirthDt).getTime()) / (1000 * 60 * 60 * 24 * 30.44))}개월` : '-'}
|
{(() => {
|
||||||
|
// 번식능력만 있는 개체 판단
|
||||||
|
const hasMptOnly = cow.hasMpt && !cow.genomeScore && !cow.anlysDt
|
||||||
|
// 번식능력 탭이거나 번식능력만 있는 개체: MPT 월령 사용
|
||||||
|
if (analysisFilter === 'mptOnly' || hasMptOnly) {
|
||||||
|
return cow.mptMonthAge ? `${cow.mptMonthAge}개월` : '-'
|
||||||
|
}
|
||||||
|
if (cow.cowBirthDt && cow.anlysDt) {
|
||||||
|
return `${Math.floor((new Date(cow.anlysDt).getTime() - new Date(cow.cowBirthDt).getTime()) / (1000 * 60 * 60 * 24 * 30.44))}개월`
|
||||||
|
}
|
||||||
|
return '-'
|
||||||
|
})()}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
@@ -1195,17 +1292,36 @@ function MyCowContent() {
|
|||||||
<span className="font-medium">{cow.sireKpn && cow.sireKpn !== '0' ? cow.sireKpn : '-'}</span>
|
<span className="font-medium">{cow.sireKpn && cow.sireKpn !== '0' ? cow.sireKpn : '-'}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<span className="text-muted-foreground">{cow.anlysDt ? '분석일' : '분석결과'}</span>
|
<span className="text-muted-foreground">
|
||||||
|
{(() => {
|
||||||
|
const hasMptOnly = cow.hasMpt && !cow.genomeScore && !cow.anlysDt
|
||||||
|
return (analysisFilter === 'mptOnly' || hasMptOnly) ? '검사일' : (cow.anlysDt ? '분석일' : '분석결과')
|
||||||
|
})()}
|
||||||
|
</span>
|
||||||
<span className="font-medium">
|
<span className="font-medium">
|
||||||
{cow.anlysDt ? new Date(cow.anlysDt).toLocaleDateString('ko-KR', { year: '2-digit', month: 'numeric', day: 'numeric' }) : cow.unavailableReason ? (
|
{(() => {
|
||||||
<span className={`inline-flex items-center px-2 py-0.5 rounded-full text-xs font-semibold ${
|
// 번식능력만 있는 개체 판단
|
||||||
cow.unavailableReason.includes('부') ? 'bg-red-100 text-red-700' :
|
const hasMptOnly = cow.hasMpt && !cow.genomeScore && !cow.anlysDt
|
||||||
cow.unavailableReason.includes('모') ? 'bg-orange-100 text-orange-700' :
|
// 번식능력 탭이거나 번식능력만 있는 개체: MPT 검사일 사용
|
||||||
'bg-slate-100 text-slate-600'
|
if (analysisFilter === 'mptOnly' || hasMptOnly) {
|
||||||
}`}>
|
return cow.mptTestDt ? new Date(cow.mptTestDt).toLocaleDateString('ko-KR', { year: '2-digit', month: 'numeric', day: 'numeric' }) : '-'
|
||||||
{cow.unavailableReason}
|
}
|
||||||
</span>
|
if (cow.anlysDt) {
|
||||||
) : '-'}
|
return new Date(cow.anlysDt).toLocaleDateString('ko-KR', { year: '2-digit', month: 'numeric', day: 'numeric' })
|
||||||
|
}
|
||||||
|
if (cow.unavailableReason) {
|
||||||
|
return (
|
||||||
|
<span className={`inline-flex items-center px-2 py-0.5 rounded-full text-xs font-semibold ${
|
||||||
|
cow.unavailableReason.includes('부') ? 'bg-red-100 text-red-700' :
|
||||||
|
cow.unavailableReason.includes('모') ? 'bg-orange-100 text-orange-700' :
|
||||||
|
'bg-slate-100 text-slate-600'
|
||||||
|
}`}>
|
||||||
|
{cow.unavailableReason}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return '-'
|
||||||
|
})()}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
} from "@/components/ui/select"
|
} from "@/components/ui/select"
|
||||||
import { apiClient, farmApi } from "@/lib/api"
|
import { apiClient, farmApi } from "@/lib/api"
|
||||||
import { DashboardStatsDto, FarmRegionRankingDto, YearlyTraitTrendDto, genomeApi } from "@/lib/api/genome.api"
|
import { DashboardStatsDto, FarmRegionRankingDto, YearlyTraitTrendDto, genomeApi } from "@/lib/api/genome.api"
|
||||||
|
import { mptApi, MptStatisticsDto } from "@/lib/api/mpt.api"
|
||||||
import { useAuthStore } from "@/store/auth-store"
|
import { useAuthStore } from "@/store/auth-store"
|
||||||
import { useGlobalFilter } from "@/contexts/GlobalFilterContext"
|
import { useGlobalFilter } from "@/contexts/GlobalFilterContext"
|
||||||
import {
|
import {
|
||||||
@@ -69,6 +70,7 @@ export default function DashboardPage() {
|
|||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [stats, setStats] = useState<DashboardStatsDto | null>(null)
|
const [stats, setStats] = useState<DashboardStatsDto | null>(null)
|
||||||
const [farmRanking, setFarmRanking] = useState<FarmRegionRankingDto | null>(null)
|
const [farmRanking, setFarmRanking] = useState<FarmRegionRankingDto | null>(null)
|
||||||
|
const [mptStats, setMptStats] = useState<MptStatisticsDto | null>(null)
|
||||||
|
|
||||||
// 모바일 감지 (반응형)
|
// 모바일 감지 (반응형)
|
||||||
const [isMobile, setIsMobile] = useState(false)
|
const [isMobile, setIsMobile] = useState(false)
|
||||||
@@ -167,12 +169,14 @@ export default function DashboardPage() {
|
|||||||
}))
|
}))
|
||||||
: undefined
|
: undefined
|
||||||
try {
|
try {
|
||||||
const [statsData, rankingData] = await Promise.all([
|
const [statsData, rankingData, mptStatsData] = await Promise.all([
|
||||||
genomeApi.getDashboardStats(farmNo),
|
genomeApi.getDashboardStats(farmNo),
|
||||||
genomeApi.getFarmRegionRanking(farmNo, traitConditions)
|
genomeApi.getFarmRegionRanking(farmNo, traitConditions),
|
||||||
|
mptApi.getMptStatistics(farmNo).catch(() => null)
|
||||||
])
|
])
|
||||||
setStats(statsData)
|
setStats(statsData)
|
||||||
setFarmRanking(rankingData)
|
setFarmRanking(rankingData)
|
||||||
|
setMptStats(mptStatsData)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('대시보드 통계 로드 실패:', error)
|
console.error('대시보드 통계 로드 실패:', error)
|
||||||
}
|
}
|
||||||
@@ -413,45 +417,38 @@ export default function DashboardPage() {
|
|||||||
<>
|
<>
|
||||||
{/* ========== 1. 핵심 KPI 카드 (2개) ========== */}
|
{/* ========== 1. 핵심 KPI 카드 (2개) ========== */}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 max-sm:gap-3">
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 max-sm:gap-3">
|
||||||
{/* 총 분석 두수 + 암/수 */}
|
{/* 총 검사 개체 수 (합집합) */}
|
||||||
<div
|
<div
|
||||||
className="bg-gradient-to-br from-primary/5 to-primary/10 rounded-xl border border-primary/20 p-5 pb-5 max-sm:p-4 max-sm:pb-4 shadow-sm hover:shadow-lg hover:border-primary/30 transition-all cursor-pointer"
|
className="bg-gradient-to-br from-primary/5 to-primary/10 rounded-xl border border-primary/20 p-5 pb-5 max-sm:p-4 max-sm:pb-4 shadow-sm hover:shadow-lg hover:border-primary/30 transition-all cursor-pointer"
|
||||||
onClick={() => router.push('/cow')}
|
onClick={() => router.push('/cow')}
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between mb-2">
|
<div className="flex items-center justify-between mb-2">
|
||||||
<p className="text-base max-sm:text-sm font-semibold text-primary/70">유전체 총 분석</p>
|
<p className="text-base max-sm:text-sm font-semibold text-primary/70">총 검사 개체 수</p>
|
||||||
<span className="text-xs max-sm:text-[10px] px-2 py-1 rounded-full bg-primary/10 text-primary font-medium">
|
|
||||||
분석일자{' '}
|
|
||||||
{(() => {
|
|
||||||
const latestRequest = stats?.requestHistory
|
|
||||||
?.filter(r => r.requestDt)
|
|
||||||
?.sort((a, b) => new Date(b.requestDt!).getTime() - new Date(a.requestDt!).getTime())[0]
|
|
||||||
if (latestRequest?.requestDt) {
|
|
||||||
return new Date(latestRequest.requestDt).toLocaleDateString('ko-KR', { year: 'numeric', month: 'numeric', day: 'numeric' })
|
|
||||||
}
|
|
||||||
return '-'
|
|
||||||
})()}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<p className="text-4xl max-sm:text-3xl font-bold text-primary">
|
<p className="text-4xl max-sm:text-3xl font-bold text-primary">
|
||||||
{stats?.summary.totalRequests || 0}
|
{stats?.summary.totalCows || 0}
|
||||||
<span className="text-lg max-sm:text-base font-normal text-primary/60 ml-1">두</span>
|
<span className="text-lg max-sm:text-base font-normal text-primary/60 ml-1">두</span>
|
||||||
</p>
|
</p>
|
||||||
<div className="flex items-center gap-4 max-sm:gap-3 mt-3 pt-3 border-t border-primary/10">
|
<div className="flex flex-wrap items-center gap-3 max-sm:gap-2 mt-3 pt-3 border-t border-primary/10 text-sm max-sm:text-xs text-primary/60">
|
||||||
<div className="flex items-center gap-1.5 max-sm:gap-1">
|
<span>유전체 <span className="font-bold text-primary">{stats?.summary.genomeCowCount || 0}</span></span>
|
||||||
<span className="text-sm max-sm:text-xs text-primary/60">♂ 수</span>
|
<span className="text-primary/30">·</span>
|
||||||
<span className="text-base max-sm:text-sm font-bold text-primary">{stats?.summary.maleCount || 0}</span>
|
<span>유전자 <span className="font-bold text-primary">{stats?.summary.geneCowCount || 0}</span></span>
|
||||||
</div>
|
<span className="text-primary/30">·</span>
|
||||||
<div className="flex items-center gap-1.5 max-sm:gap-1">
|
<span>번식능력 <span className="font-bold text-primary">{stats?.summary.mptCowCount || 0}</span></span>
|
||||||
<span className="text-sm max-sm:text-xs text-primary/60">♀ 암</span>
|
|
||||||
<span className="text-base max-sm:text-sm font-bold text-primary">{stats?.summary.femaleCount || 0}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 친자감별 결과 (넓게) */}
|
{/* 친자감별 결과 (넓게) */}
|
||||||
<div className="md:col-span-2 bg-white rounded-xl border border-slate-200 p-5 max-sm:p-4 shadow-sm hover:shadow-md transition-shadow">
|
<div className="md:col-span-2 bg-white rounded-xl border border-slate-200 p-5 max-sm:p-4 shadow-sm hover:shadow-md transition-shadow">
|
||||||
<p className="text-base max-sm:text-sm font-semibold text-slate-700 mb-4">유전체 친자감별 결과</p>
|
<p className="text-base max-sm:text-sm font-semibold text-slate-700 mb-4">유전체 친자감별 결과</p>
|
||||||
|
{(stats?.summary.genomeCowCount || 0) === 0 ? (
|
||||||
|
<div className="flex items-center justify-center h-32 text-slate-400">
|
||||||
|
<div className="text-center">
|
||||||
|
<Dna className="w-8 h-8 mx-auto mb-2 opacity-50" />
|
||||||
|
<p className="text-sm">유전체 검사 데이터가 없습니다</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
<div className="flex items-center gap-6 max-sm:flex-col max-sm:gap-4">
|
<div className="flex items-center gap-6 max-sm:flex-col max-sm:gap-4">
|
||||||
{/* 도넛 차트 */}
|
{/* 도넛 차트 */}
|
||||||
<div className="w-28 h-28 max-sm:w-24 max-sm:h-24 shrink-0 relative">
|
<div className="w-28 h-28 max-sm:w-24 max-sm:h-24 shrink-0 relative">
|
||||||
@@ -529,9 +526,104 @@ export default function DashboardPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* ========== 1-2. 번식능력검사 현황 ========== */}
|
||||||
|
{mptStats && mptStats.totalMptCows > 0 && (
|
||||||
|
<div className="bg-white rounded-xl border border-slate-200 p-5 max-sm:p-4 shadow-sm hover:shadow-md transition-shadow">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<p className="text-base max-sm:text-sm font-semibold text-slate-700">번식능력검사 현황</p>
|
||||||
|
<span className="text-xs max-sm:text-[10px] px-2 py-1 rounded-full bg-pink-50 text-pink-700 font-medium">
|
||||||
|
검사 {mptStats.totalMptCows}두
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 max-sm:gap-3">
|
||||||
|
{/* 에너지 균형 */}
|
||||||
|
<div className="bg-slate-50 rounded-lg p-4 max-sm:p-3">
|
||||||
|
<p className="text-sm max-sm:text-xs text-slate-500 mb-3 font-medium">에너지 균형</p>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="flex items-center gap-1.5 text-sm max-sm:text-xs text-green-600">
|
||||||
|
<CheckCircle2 className="w-4 h-4 max-sm:w-3 max-sm:h-3" /> 안전
|
||||||
|
</span>
|
||||||
|
<span className="text-lg max-sm:text-base font-bold text-green-600">{mptStats.categories.energy.safe}</span>
|
||||||
|
</div>
|
||||||
|
{mptStats.categories.energy.caution > 0 && (
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="flex items-center gap-1.5 text-sm max-sm:text-xs text-amber-600">
|
||||||
|
<AlertCircle className="w-4 h-4 max-sm:w-3 max-sm:h-3" /> 주의
|
||||||
|
</span>
|
||||||
|
<span className="text-lg max-sm:text-base font-bold text-amber-600">{mptStats.categories.energy.caution}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* 단백질 상태 */}
|
||||||
|
<div className="bg-slate-50 rounded-lg p-4 max-sm:p-3">
|
||||||
|
<p className="text-sm max-sm:text-xs text-slate-500 mb-3 font-medium">단백질 상태</p>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="flex items-center gap-1.5 text-sm max-sm:text-xs text-green-600">
|
||||||
|
<CheckCircle2 className="w-4 h-4 max-sm:w-3 max-sm:h-3" /> 안전
|
||||||
|
</span>
|
||||||
|
<span className="text-lg max-sm:text-base font-bold text-green-600">{mptStats.categories.protein.safe}</span>
|
||||||
|
</div>
|
||||||
|
{mptStats.categories.protein.caution > 0 && (
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="flex items-center gap-1.5 text-sm max-sm:text-xs text-amber-600">
|
||||||
|
<AlertCircle className="w-4 h-4 max-sm:w-3 max-sm:h-3" /> 주의
|
||||||
|
</span>
|
||||||
|
<span className="text-lg max-sm:text-base font-bold text-amber-600">{mptStats.categories.protein.caution}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* 간 건강 */}
|
||||||
|
<div className="bg-slate-50 rounded-lg p-4 max-sm:p-3">
|
||||||
|
<p className="text-sm max-sm:text-xs text-slate-500 mb-3 font-medium">간 건강</p>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="flex items-center gap-1.5 text-sm max-sm:text-xs text-green-600">
|
||||||
|
<CheckCircle2 className="w-4 h-4 max-sm:w-3 max-sm:h-3" /> 안전
|
||||||
|
</span>
|
||||||
|
<span className="text-lg max-sm:text-base font-bold text-green-600">{mptStats.categories.liver.safe}</span>
|
||||||
|
</div>
|
||||||
|
{mptStats.categories.liver.caution > 0 && (
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="flex items-center gap-1.5 text-sm max-sm:text-xs text-amber-600">
|
||||||
|
<AlertCircle className="w-4 h-4 max-sm:w-3 max-sm:h-3" /> 주의
|
||||||
|
</span>
|
||||||
|
<span className="text-lg max-sm:text-base font-bold text-amber-600">{mptStats.categories.liver.caution}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* 미네랄 균형 */}
|
||||||
|
<div className="bg-slate-50 rounded-lg p-4 max-sm:p-3">
|
||||||
|
<p className="text-sm max-sm:text-xs text-slate-500 mb-3 font-medium">미네랄 균형</p>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="flex items-center gap-1.5 text-sm max-sm:text-xs text-green-600">
|
||||||
|
<CheckCircle2 className="w-4 h-4 max-sm:w-3 max-sm:h-3" /> 안전
|
||||||
|
</span>
|
||||||
|
<span className="text-lg max-sm:text-base font-bold text-green-600">{mptStats.categories.mineral.safe}</span>
|
||||||
|
</div>
|
||||||
|
{mptStats.categories.mineral.caution > 0 && (
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="flex items-center gap-1.5 text-sm max-sm:text-xs text-amber-600">
|
||||||
|
<AlertCircle className="w-4 h-4 max-sm:w-3 max-sm:h-3" /> 주의
|
||||||
|
</span>
|
||||||
|
<span className="text-lg max-sm:text-base font-bold text-amber-600">{mptStats.categories.mineral.caution}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* ========== 2. 좌측(농가위치+순위) + 우측(연도별 육종가) - 높이 맞춤 ========== */}
|
{/* ========== 2. 좌측(농가위치+순위) + 우측(연도별 육종가) - 높이 맞춤 ========== */}
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 max-sm:gap-4">
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 max-sm:gap-4">
|
||||||
{/* 좌측 영역: 보은군 내 농가 위치 + 순위 통합 */}
|
{/* 좌측 영역: 보은군 내 농가 위치 + 순위 통합 */}
|
||||||
@@ -565,7 +657,7 @@ export default function DashboardPage() {
|
|||||||
<span className="flex items-center gap-1.5 max-sm:gap-1 font-medium"><span className="w-3.5 h-3.5 max-sm:w-2.5 max-sm:h-2.5 rounded bg-slate-400"></span> 보은군</span>
|
<span className="flex items-center gap-1.5 max-sm:gap-1 font-medium"><span className="w-3.5 h-3.5 max-sm:w-2.5 max-sm:h-2.5 rounded bg-slate-400"></span> 보은군</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{farmRanking && (farmRanking.farmAvgScore !== null || distributionBasis !== 'overall') ? (
|
{(stats?.summary.genomeCowCount || 0) > 0 && farmRanking && (farmRanking.farmAvgScore !== null || distributionBasis !== 'overall') ? (
|
||||||
<div className="bg-gradient-to-b from-slate-50 to-slate-100/50 rounded-xl p-4 max-sm:p-2 max-sm:-mx-2">
|
<div className="bg-gradient-to-b from-slate-50 to-slate-100/50 rounded-xl p-4 max-sm:p-2 max-sm:-mx-2">
|
||||||
<ResponsiveContainer width="100%" height={350}>
|
<ResponsiveContainer width="100%" height={350}>
|
||||||
<ComposedChart
|
<ComposedChart
|
||||||
@@ -796,14 +888,14 @@ export default function DashboardPage() {
|
|||||||
) : (
|
) : (
|
||||||
<div className="h-[280px] flex items-center justify-center text-slate-400 text-sm bg-slate-50 rounded-xl">
|
<div className="h-[280px] flex items-center justify-center text-slate-400 text-sm bg-slate-50 rounded-xl">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<MapPin className="w-10 h-10 mx-auto mb-3 opacity-50" />
|
<Dna className="w-10 h-10 mx-auto mb-3 opacity-50" />
|
||||||
<p className="text-base">농가 데이터 없음</p>
|
<p className="text-base">유전체 검사 데이터가 없습니다</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 순위 정보 (차트 하단에 통합) - 드롭다운 선택에 따라 연동 */}
|
{/* 순위 정보 (차트 하단에 통합) - 드롭다운 선택에 따라 연동 */}
|
||||||
{(farmPositionData.rank !== null || farmRanking?.farmAvgScore !== null) && (
|
{(stats?.summary.genomeCowCount || 0) > 0 && (farmPositionData.rank !== null || farmRanking?.farmAvgScore !== null) && (
|
||||||
<div className="mt-4 pt-4 border-t border-slate-200">
|
<div className="mt-4 pt-4 border-t border-slate-200">
|
||||||
{/* 현재 선택된 기준 표시 */}
|
{/* 현재 선택된 기준 표시 */}
|
||||||
<p className="text-xs text-slate-400 mb-2 text-center">
|
<p className="text-xs text-slate-400 mb-2 text-center">
|
||||||
@@ -879,7 +971,14 @@ export default function DashboardPage() {
|
|||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
{/* 차트 */}
|
{/* 차트 */}
|
||||||
{traitTrendLoading ? (
|
{(stats?.summary.genomeCowCount || 0) === 0 ? (
|
||||||
|
<div className="h-[280px] flex items-center justify-center text-slate-400 bg-slate-50 rounded-xl">
|
||||||
|
<div className="text-center">
|
||||||
|
<Dna className="w-10 h-10 mx-auto mb-3 opacity-50" />
|
||||||
|
<p className="text-base">유전체 검사 데이터가 없습니다</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : traitTrendLoading ? (
|
||||||
<div className="h-[180px] flex items-center justify-center text-slate-400">
|
<div className="h-[180px] flex items-center justify-center text-slate-400">
|
||||||
<div className="animate-spin rounded-full h-6 w-6 border-2 border-[#1F3A8F] border-t-transparent"></div>
|
<div className="animate-spin rounded-full h-6 w-6 border-2 border-[#1F3A8F] border-t-transparent"></div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1061,7 +1160,7 @@ export default function DashboardPage() {
|
|||||||
{/* ========== 3. 카테고리별 보은군 대비 비교 - 전체 너비 ========== */}
|
{/* ========== 3. 카테고리별 보은군 대비 비교 - 전체 너비 ========== */}
|
||||||
<div className="bg-white rounded-xl border border-slate-200 p-5 max-sm:p-4 shadow-sm">
|
<div className="bg-white rounded-xl border border-slate-200 p-5 max-sm:p-4 shadow-sm">
|
||||||
<h3 className="font-semibold text-slate-900 text-lg max-sm:text-base mb-4 max-sm:mb-3">보은군 대비 카테고리별 육종가 평균</h3>
|
<h3 className="font-semibold text-slate-900 text-lg max-sm:text-base mb-4 max-sm:mb-3">보은군 대비 카테고리별 육종가 평균</h3>
|
||||||
{stats?.traitAverages && stats.traitAverages.length > 0 ? (
|
{(stats?.summary.genomeCowCount || 0) > 0 && stats?.traitAverages && stats.traitAverages.length > 0 ? (
|
||||||
(() => {
|
(() => {
|
||||||
const categories = ['성장', '생산', '체형', '무게', '비율']
|
const categories = ['성장', '생산', '체형', '무게', '비율']
|
||||||
const categoryData = categories.map(cat => {
|
const categoryData = categories.map(cat => {
|
||||||
@@ -1372,15 +1471,20 @@ export default function DashboardPage() {
|
|||||||
)
|
)
|
||||||
})()
|
})()
|
||||||
) : (
|
) : (
|
||||||
<div className="h-[150px] flex items-center justify-center text-slate-400 text-sm max-sm:text-xs">
|
<div className="h-[280px] flex items-center justify-center text-slate-400 bg-slate-50 rounded-xl">
|
||||||
데이터 없음
|
<div className="text-center">
|
||||||
|
<Dna className="w-10 h-10 mx-auto mb-3 opacity-50" />
|
||||||
|
<p className="text-base">유전체 검사 데이터가 없습니다</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{/* 범례 */}
|
{/* 범례 */}
|
||||||
|
{(stats?.summary.genomeCowCount || 0) > 0 && (
|
||||||
<div className="mt-4 pt-3 border-t border-slate-100 flex flex-wrap items-center justify-center gap-4 text-xs text-slate-500">
|
<div className="mt-4 pt-3 border-t border-slate-100 flex flex-wrap items-center justify-center gap-4 text-xs text-slate-500">
|
||||||
<span className="flex items-center gap-1.5"><span className="w-3 h-3 rounded bg-[#1F3A8F]"></span>우리농가</span>
|
<span className="flex items-center gap-1.5"><span className="w-3 h-3 rounded bg-[#1F3A8F]"></span>우리농가</span>
|
||||||
<span className="flex items-center gap-1.5"><span className="w-3 h-3 rounded bg-slate-400 opacity-50"></span>보은군평균</span>
|
<span className="flex items-center gap-1.5"><span className="w-3 h-3 rounded bg-slate-400 opacity-50"></span>보은군평균</span>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export interface MptReferenceRange {
|
|||||||
upperLimit: number | null;
|
upperLimit: number | null;
|
||||||
lowerLimit: number | null;
|
lowerLimit: number | null;
|
||||||
unit: string;
|
unit: string;
|
||||||
category: '에너지' | '단백질' | '간기능' | '미네랄' | '기타';
|
category: '에너지' | '단백질' | '간기능' | '미네랄' | '별도';
|
||||||
description?: string; // 항목 설명 (선택)
|
description?: string; // 항목 설명 (선택)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,6 +38,15 @@ export const MPT_REFERENCE_RANGES: Record<string, MptReferenceRange> = {
|
|||||||
category: '에너지',
|
category: '에너지',
|
||||||
description: '혈액 내 유리지방산 수치',
|
description: '혈액 내 유리지방산 수치',
|
||||||
},
|
},
|
||||||
|
bcs: {
|
||||||
|
name: 'BCS',
|
||||||
|
upperLimit: 3.5,
|
||||||
|
lowerLimit: 2.5,
|
||||||
|
unit: '-',
|
||||||
|
category: '에너지',
|
||||||
|
description: '체충실지수(Body Condition Score)',
|
||||||
|
},
|
||||||
|
|
||||||
// 단백질 카테고리
|
// 단백질 카테고리
|
||||||
totalProtein: {
|
totalProtein: {
|
||||||
name: '총단백질',
|
name: '총단백질',
|
||||||
@@ -140,13 +149,13 @@ export const MPT_REFERENCE_RANGES: Record<string, MptReferenceRange> = {
|
|||||||
description: '혈액 내 마그네슘 수치',
|
description: '혈액 내 마그네슘 수치',
|
||||||
},
|
},
|
||||||
|
|
||||||
// 기타 카테고리
|
// 별도 카테고리
|
||||||
creatine: {
|
creatine: {
|
||||||
name: '크레아틴',
|
name: '크레아틴',
|
||||||
upperLimit: 1.3,
|
upperLimit: 1.3,
|
||||||
lowerLimit: 1.0,
|
lowerLimit: 1.0,
|
||||||
unit: 'mg/dL',
|
unit: 'mg/dL',
|
||||||
category: '기타',
|
category: '별도',
|
||||||
description: '혈액 내 크레아틴 수치',
|
description: '혈액 내 크레아틴 수치',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -154,7 +163,7 @@ export const MPT_REFERENCE_RANGES: Record<string, MptReferenceRange> = {
|
|||||||
/**
|
/**
|
||||||
* MPT 카테고리 목록 (표시 순서)
|
* MPT 카테고리 목록 (표시 순서)
|
||||||
*/
|
*/
|
||||||
export const MPT_CATEGORIES = ['에너지', '단백질', '간기능', '미네랄', '기타'] as const;
|
export const MPT_CATEGORIES = ['에너지', '단백질', '간기능', '미네랄', '별도'] as const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 측정값이 정상 범위 내에 있는지 확인
|
* 측정값이 정상 범위 내에 있는지 확인
|
||||||
|
|||||||
@@ -318,7 +318,11 @@ export interface DashboardStatsDto {
|
|||||||
}[];
|
}[];
|
||||||
// 요약
|
// 요약
|
||||||
summary: {
|
summary: {
|
||||||
totalRequests: number;
|
totalCows: number; // 검사 받은 전체 개체 수 (합집합, 중복 제외)
|
||||||
|
genomeCowCount: number; // 유전체 분석 개체 수
|
||||||
|
geneCowCount: number; // 유전자검사 개체 수
|
||||||
|
mptCowCount: number; // 번식능력검사 개체 수
|
||||||
|
totalRequests: number; // 유전체 의뢰 건수 (기존 호환성)
|
||||||
analyzedCount: number;
|
analyzedCount: number;
|
||||||
pendingCount: number;
|
pendingCount: number;
|
||||||
mismatchCount: number;
|
mismatchCount: number;
|
||||||
|
|||||||
@@ -1,5 +1,26 @@
|
|||||||
import apiClient from "../api-client";
|
import apiClient from "../api-client";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MPT 통계 응답 DTO
|
||||||
|
*/
|
||||||
|
export interface MptStatisticsDto {
|
||||||
|
totalMptCows: number;
|
||||||
|
latestTestDate: string | 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';
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* MPT(혈액화학검사) 결과 DTO
|
* MPT(혈액화학검사) 결과 DTO
|
||||||
*/
|
*/
|
||||||
@@ -60,7 +81,7 @@ export const mptApi = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GET /mpt?cowShortNo=:cowShortNo - 특정 개체의 검사 결과
|
* GET /mpt?cowShortNo=:cowShortNo - 특정 개체의 검사 결과 (뒤 4자리)
|
||||||
*/
|
*/
|
||||||
findByCowShortNo: async (cowShortNo: string): Promise<MptDto[]> => {
|
findByCowShortNo: async (cowShortNo: string): Promise<MptDto[]> => {
|
||||||
return await apiClient.get("/mpt", {
|
return await apiClient.get("/mpt", {
|
||||||
@@ -68,6 +89,15 @@ export const mptApi = {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /mpt?cowId=:cowId - 특정 개체의 검사 결과 (전체 개체번호)
|
||||||
|
*/
|
||||||
|
findByCowId: async (cowId: string): Promise<MptDto[]> => {
|
||||||
|
return await apiClient.get("/mpt", {
|
||||||
|
params: { cowId },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GET /mpt/:id - 검사 결과 상세 조회
|
* GET /mpt/:id - 검사 결과 상세 조회
|
||||||
*/
|
*/
|
||||||
@@ -102,4 +132,13 @@ export const mptApi = {
|
|||||||
remove: async (id: number): Promise<void> => {
|
remove: async (id: number): Promise<void> => {
|
||||||
return await apiClient.delete(`/mpt/${id}`);
|
return await apiClient.delete(`/mpt/${id}`);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /mpt/statistics/:farmNo - 농장별 MPT 통계 조회
|
||||||
|
* - 카테고리별 정상/주의/위험 개체 수
|
||||||
|
* - 위험 개체 목록
|
||||||
|
*/
|
||||||
|
getMptStatistics: async (farmNo: number): Promise<MptStatisticsDto> => {
|
||||||
|
return await apiClient.get(`/mpt/statistics/${farmNo}`);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ export const INVALID_CHIP_DAM_NAMES = ['불일치', '이력제부재'];
|
|||||||
/** 개별 제외 개체 목록 (분석불가 등 특수 사유) */
|
/** 개별 제외 개체 목록 (분석불가 등 특수 사유) */
|
||||||
export const EXCLUDED_COW_IDS = [
|
export const EXCLUDED_COW_IDS = [
|
||||||
'KOR002191642861', // 모근량 갯수 적은데 1회분은 가능 아비명도 일치하는데 유전체 분석 정보가 없음
|
'KOR002191642861', // 모근량 갯수 적은데 1회분은 가능 아비명도 일치하는데 유전체 분석 정보가 없음
|
||||||
|
// 김정태님
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -20,6 +20,11 @@ export interface CowDto extends BaseFields {
|
|||||||
fkFarmNo?: number; // 농장번호 FK
|
fkFarmNo?: number; // 농장번호 FK
|
||||||
cowStatus?: string; // 개체상태
|
cowStatus?: string; // 개체상태
|
||||||
delDt?: string; // 삭제일시 (Soft Delete)
|
delDt?: string; // 삭제일시 (Soft Delete)
|
||||||
|
anlysDt?: string; // 분석일자
|
||||||
|
unavailableReason?: string; // 분석불가 사유
|
||||||
|
hasMpt?: boolean; // 번식능력검사(MPT) 여부
|
||||||
|
mptTestDt?: string; // MPT 검사일
|
||||||
|
mptMonthAge?: number; // MPT 검사일 기준 월령
|
||||||
|
|
||||||
// Relations
|
// Relations
|
||||||
farm?: FarmDto; // 농장 정보 (조인)
|
farm?: FarmDto; // 농장 정보 (조인)
|
||||||
|
|||||||
Reference in New Issue
Block a user