개체분석 상태 값 수정
This commit is contained in:
@@ -64,9 +64,15 @@ export function isValidGenomeAnalysis(
|
||||
chipDamName: string | null | undefined,
|
||||
cowId?: string | null,
|
||||
): boolean {
|
||||
// 개별 제외 개체만 확인 (부/모 불일치여도 유전자 데이터 있으면 표시)
|
||||
// 1. 개별 제외 개체 확인
|
||||
if (cowId && EXCLUDED_COW_IDS.includes(cowId)) return false;
|
||||
|
||||
// 2. 아비명이 '일치'가 아니면 무효 (null, 불일치, 분석불가, 정보없음 등)
|
||||
if (chipSireName !== VALID_CHIP_SIRE_NAME) return false;
|
||||
|
||||
// 3. 어미명이 '불일치' 또는 '이력제부재'면 무효
|
||||
if (chipDamName && INVALID_CHIP_DAM_NAMES.includes(chipDamName)) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ export const MPT_NORMAL_RANGES = {
|
||||
* 총 글로불린 (Total Globulin)
|
||||
* 단위: g/dL
|
||||
*/
|
||||
totalGlobulin: { min: 9.1, max: 36.1 },
|
||||
globulin: { min: 9.1, max: 36.1 },
|
||||
|
||||
/**
|
||||
* A/G 비율 (Albumin/Globulin Ratio)
|
||||
@@ -75,7 +75,7 @@ export const MPT_NORMAL_RANGES = {
|
||||
* 지방간 지수 (Fatty Liver Index)
|
||||
* 단위: 지수
|
||||
*/
|
||||
fattyLiverIndex: { min: -1.2, max: 9.9 },
|
||||
fattyLiverIdx: { min: -1.2, max: 9.9 },
|
||||
|
||||
/**
|
||||
* 칼슘 (Calcium)
|
||||
@@ -103,10 +103,10 @@ export const MPT_NORMAL_RANGES = {
|
||||
|
||||
// ========== 기타 카테고리 ==========
|
||||
/**
|
||||
* 크레아티닌 (Creatinine)
|
||||
* 크레아틴 (Creatine)
|
||||
* 단위: mg/dL
|
||||
*/
|
||||
creatinine: { min: 1.0, max: 1.3 },
|
||||
creatine: { min: 1.0, max: 1.3 },
|
||||
} as const;
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
/**
|
||||
* 추천 시스템 설정 상수
|
||||
*
|
||||
* @description
|
||||
* KPN 추천, 개체 추천, 패키지 추천 등 추천 시스템 관련 설정값
|
||||
*
|
||||
* @source PRD 기능요구사항20.md SFR-COW-016, SFR-COW-037
|
||||
*/
|
||||
export const RECOMMENDATION_CONFIG = {
|
||||
/**
|
||||
* 유전자 매칭 점수 관련
|
||||
*/
|
||||
GENE_SCORE: {
|
||||
/**
|
||||
* 점수 차이 임계값
|
||||
* 유전자 매칭 점수 차이가 이 값보다 작으면 근친도를 우선 고려
|
||||
*/
|
||||
DIFF_THRESHOLD: 5,
|
||||
},
|
||||
|
||||
/**
|
||||
* 기본값
|
||||
*/
|
||||
DEFAULTS: {
|
||||
/**
|
||||
* 근친도 임계값 (%)
|
||||
* Wright's Coefficient 기준
|
||||
*/
|
||||
INBREEDING_THRESHOLD: 12.5,
|
||||
|
||||
/**
|
||||
* 추천 개수
|
||||
* 상위 N개의 KPN/개체를 추천
|
||||
*/
|
||||
RECOMMENDATION_LIMIT: 10,
|
||||
|
||||
/**
|
||||
* 세대제약 기준
|
||||
* 최근 N세대 이내 사용된 KPN을 추천에서 제외
|
||||
*/
|
||||
GENERATION_THRESHOLD: 3,
|
||||
},
|
||||
|
||||
/**
|
||||
* KPN 패키지 설정
|
||||
*/
|
||||
PACKAGE: {
|
||||
/**
|
||||
* 기본 패키지 크기
|
||||
* 추천할 KPN 세트 개수
|
||||
*/
|
||||
DEFAULT_SIZE: 5,
|
||||
|
||||
/**
|
||||
* 최소 패키지 크기
|
||||
*/
|
||||
MIN_SIZE: 3,
|
||||
|
||||
/**
|
||||
* 최대 패키지 크기
|
||||
*/
|
||||
MAX_SIZE: 10,
|
||||
},
|
||||
|
||||
/**
|
||||
* 커버리지 기준 (%)
|
||||
* 유전자 목표 달성률 평가 기준
|
||||
*/
|
||||
COVERAGE: {
|
||||
/**
|
||||
* 우수 기준
|
||||
* 50% 이상 커버리지
|
||||
*/
|
||||
EXCELLENT: 50,
|
||||
|
||||
/**
|
||||
* 양호 기준
|
||||
* 30% 이상 커버리지
|
||||
*/
|
||||
GOOD: 30,
|
||||
|
||||
/**
|
||||
* 최소 기준
|
||||
* 20% 이상 커버리지
|
||||
*/
|
||||
MINIMUM: 20,
|
||||
},
|
||||
|
||||
/**
|
||||
* KPN 순환 전략
|
||||
*/
|
||||
ROTATION: {
|
||||
/**
|
||||
* 최소 KPN 개수
|
||||
* 순환 전략 적용 최소 개수
|
||||
*/
|
||||
MIN_KPN_COUNT: 3,
|
||||
|
||||
/**
|
||||
* 재사용 안전 세대
|
||||
* 동일 KPN을 이 세대 이후에 재사용 가능
|
||||
*/
|
||||
SAFE_REUSE_GENERATION: 4,
|
||||
},
|
||||
} as const;
|
||||
@@ -19,6 +19,7 @@ import { CowService } from './cow.service';
|
||||
import { CowModel } from './entities/cow.entity';
|
||||
import { GenomeRequestModel } from '../genome/entities/genome-request.entity';
|
||||
import { GenomeTraitDetailModel } from '../genome/entities/genome-trait-detail.entity';
|
||||
import { GeneDetailModel } from '../gene/entities/gene-detail.entity';
|
||||
import { FilterEngineModule } from '../shared/filter/filter-engine.module';
|
||||
|
||||
@Module({
|
||||
@@ -27,6 +28,7 @@ import { FilterEngineModule } from '../shared/filter/filter-engine.module';
|
||||
CowModel, // 개체 기본 정보 (tb_cow)
|
||||
GenomeRequestModel, // 유전체 분석 의뢰 (tb_genome_request)
|
||||
GenomeTraitDetailModel, // 유전체 형질 상세 (tb_genome_trait_detail)
|
||||
GeneDetailModel, // 유전자 상세 (tb_gene_detail)
|
||||
]),
|
||||
FilterEngineModule, // 필터 엔진 모듈
|
||||
],
|
||||
|
||||
@@ -19,6 +19,7 @@ import { Repository, IsNull } from 'typeorm';
|
||||
import { CowModel } from './entities/cow.entity';
|
||||
import { GenomeRequestModel } from '../genome/entities/genome-request.entity';
|
||||
import { GenomeTraitDetailModel } from '../genome/entities/genome-trait-detail.entity';
|
||||
import { GeneDetailModel } from '../gene/entities/gene-detail.entity';
|
||||
import { FilterEngineService } from '../shared/filter/filter-engine.service';
|
||||
import {
|
||||
RankingRequestDto,
|
||||
@@ -57,6 +58,10 @@ export class CowService {
|
||||
@InjectRepository(GenomeTraitDetailModel)
|
||||
private readonly genomeTraitDetailRepository: Repository<GenomeTraitDetailModel>,
|
||||
|
||||
// 유전자 상세 Repository (SNP 데이터 접근용)
|
||||
@InjectRepository(GeneDetailModel)
|
||||
private readonly geneDetailRepository: Repository<GeneDetailModel>,
|
||||
|
||||
// 동적 필터링 서비스 (검색, 정렬, 페이지네이션)
|
||||
private readonly filterEngineService: FilterEngineService,
|
||||
) { }
|
||||
@@ -116,10 +121,10 @@ export class CowService {
|
||||
* 개체식별번호(cowId)로 단건 조회
|
||||
*
|
||||
* @param cowId - 개체식별번호 (예: KOR002119144049)
|
||||
* @returns 개체 정보 (farm 포함)
|
||||
* @returns 개체 정보 (farm 포함) + dataStatus (데이터 존재 여부)
|
||||
* @throws NotFoundException - 개체를 찾을 수 없는 경우
|
||||
*/
|
||||
async findByCowId(cowId: string): Promise<CowModel> {
|
||||
async findByCowId(cowId: string): Promise<CowModel & { dataStatus: { hasGenomeData: boolean; hasGeneData: boolean } }> {
|
||||
const cow = await this.cowRepository.findOne({
|
||||
where: { cowId: cowId, delDt: IsNull() },
|
||||
relations: ['farm'],
|
||||
@@ -127,7 +132,26 @@ export class CowService {
|
||||
if (!cow) {
|
||||
throw new NotFoundException(`Cow with cowId ${cowId} not found`);
|
||||
}
|
||||
return cow;
|
||||
|
||||
// 데이터 존재 여부 확인 (가벼운 COUNT 쿼리)
|
||||
const [genomeCount, geneCount] = await Promise.all([
|
||||
this.genomeTraitDetailRepository.count({
|
||||
where: { cowId, delDt: IsNull() },
|
||||
take: 1,
|
||||
}),
|
||||
this.geneDetailRepository.count({
|
||||
where: { cowId, delDt: IsNull() },
|
||||
take: 1,
|
||||
}),
|
||||
]);
|
||||
|
||||
return {
|
||||
...cow,
|
||||
dataStatus: {
|
||||
hasGenomeData: genomeCount > 0,
|
||||
hasGeneData: geneCount > 0,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
@@ -260,16 +284,23 @@ export class CowService {
|
||||
// Step 2: 친자감별 확인 - 유효하지 않으면 분석 불가
|
||||
if (!latestRequest || !isValidGenomeAnalysis(latestRequest.chipSireName, latestRequest.chipDamName, cow.cowId)) {
|
||||
// 분석불가 사유 결정
|
||||
let unavailableReason = '분석불가';
|
||||
if (latestRequest) {
|
||||
if (latestRequest.chipSireName !== '일치') {
|
||||
unavailableReason = '부 불일치';
|
||||
} else if (latestRequest.chipDamName === '불일치') {
|
||||
unavailableReason = '모 불일치';
|
||||
} else if (latestRequest.chipDamName === '이력제부재') {
|
||||
unavailableReason = '모 이력제부재';
|
||||
}
|
||||
let unavailableReason: string | null = null;
|
||||
|
||||
if (!latestRequest || !latestRequest.chipSireName) {
|
||||
// latestRequest 없거나 chipSireName이 null → '-' 표시 (프론트에서 null은 '-'로 표시)
|
||||
unavailableReason = null;
|
||||
} else if (latestRequest.chipSireName === '분석불가' || latestRequest.chipSireName === '정보없음') {
|
||||
// 분석불가, 정보없음 → 분석불가
|
||||
unavailableReason = '분석불가';
|
||||
} else if (latestRequest.chipSireName !== '일치') {
|
||||
// 불일치 등 그 외 → 부 불일치
|
||||
unavailableReason = '부 불일치';
|
||||
} else if (latestRequest.chipDamName === '불일치') {
|
||||
unavailableReason = '모 불일치';
|
||||
} else if (latestRequest.chipDamName === '이력제부재') {
|
||||
unavailableReason = '모 이력제부재';
|
||||
}
|
||||
|
||||
return { entity: { ...cow, unavailableReason }, sortValue: null, details: [] };
|
||||
}
|
||||
|
||||
|
||||
@@ -232,14 +232,14 @@ export class MptModel extends BaseModel {
|
||||
magnesium: number;
|
||||
|
||||
@Column({
|
||||
name: 'creatinine',
|
||||
name: 'creatine',
|
||||
type: 'decimal',
|
||||
precision: 10,
|
||||
scale: 2,
|
||||
nullable: true,
|
||||
comment: '크레아틴',
|
||||
})
|
||||
creatinine: number;
|
||||
creatine: number;
|
||||
|
||||
// Relations
|
||||
@ManyToOne(() => FarmModel, { onDelete: 'CASCADE' })
|
||||
|
||||
Reference in New Issue
Block a user