개체분석 상태 값 수정

This commit is contained in:
2025-12-19 15:19:50 +09:00
parent abc2f20495
commit c8bd04f124
24 changed files with 596 additions and 1499 deletions

View File

@@ -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;
}

View File

@@ -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;
/**

View File

@@ -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;

View File

@@ -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, // 필터 엔진 모듈
],

View File

@@ -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: [] };
}

View File

@@ -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' })