개체분석 상태 값 수정

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

@@ -0,0 +1,146 @@
# 유전체/유전자 검사 가능 조건 요약
## 1. DB 상태값 정의
### chipSireName (아비명)
| DB 값 | 의미 | 분석 가능 여부 |
|-------|------|----------------|
| `일치` | 친자감별 일치 | 가능 |
| `불일치` | 친자감별 불일치 | 유전체 불가 / 유전자 가능 |
| `분석불가` | 모근 오염/불량 등 기타 사유 | 불가 |
| `정보없음` | 개체 식별번호/형식 오류 | 불가 |
| `null` | 미분석 (의뢰 없음) | - 표시 |
## - 아비명 가능한 개체에 대해서 어미명 판단 진행
### chipDamName (어미명)
| DB 값 | 의미 | 분석 가능 여부 |
|-------|------|----------------|
| `일치` | 친자감별 일치 | 통과 |
| `불일치` | 친자감별 불일치 | 유전체 불가 / 유전자 가능 |
| `이력제부재` | 모 이력제 정보 없음 | 유전체 불가 / 유전자 가능 |
| `정보없음` | 정보 없음 | 통과 |
| `null` | 정보 없음 | 통과 |
---
## 2. 탭별 검사 가능 조건
### 유전체 탭
```
유효 조건 (모두 충족해야 함):
1. chipSireName === '일치'
2. chipDamName !== '불일치'
3. chipDamName !== '이력제부재'
4. cowId가 EXCLUDED_COW_IDS에 포함되지 않음
```
### 유전자 탭
```
유효 조건:
1. chipSireName !== '분석불가'
2. chipSireName !== '정보없음'
3. cowId가 EXCLUDED_COW_IDS에 포함되지 않음
※ 불일치/이력제부재도 유전자 데이터가 있으면 표시 가능
```
---
## 3. 개체 목록 배지 표시 (unavailableReason)
### 분석일자 컬럼
| unavailableReason | 배지 색상 | 표시 텍스트 |
|-------------------|-----------|-------------|
| `null` | - | `-` |
| `분석불가` | 회색 | 분석불가 |
| `부 불일치` | 빨간색 | 부 불일치 |
| `모 불일치` | 주황색 | 모 불일치 |
| `모 이력제부재` | 주황색 | 모 이력제부재 |
| `형질정보없음` | 회색 | 형질정보없음 |
### unavailableReason 결정 로직 (cow.service.ts)
```typescript
if (!latestRequest || !latestRequest.chipSireName) {
unavailableReason = null; // '-' 표시
} else if (chipSireName === '분석불가' || chipSireName === '정보없음') {
unavailableReason = '분석불가';
} else if (chipSireName !== '일치') {
unavailableReason = '부 불일치';
} else if (chipDamName === '불일치') {
unavailableReason = '모 불일치';
} else if (chipDamName === '이력제부재') {
unavailableReason = '모 이력제부재';
}
// 형질 데이터 없으면
unavailableReason = '형질정보없음';
```
---
## 4. 개체 상세 페이지 배지
### 부 KPN 배지 (renderSireBadge)
| 조건 | 배지 색상 | 표시 |
|------|-----------|------|
| `EXCLUDED_COW_IDS` 포함 | 회색 | 분석불가 |
| `chipSireName === '분석불가'` | 회색 | 분석불가 |
| `chipSireName === '정보없음'` | 회색 | 분석불가 |
| `chipSireName === '일치'` | 초록색 | 일치 |
| 그 외 | 빨간색 | 불일치 |
| `null` | - | 표시 안 함 |
### 모 개체 배지 (renderDamBadge)
| 조건 | 배지 색상 | 표시 |
|------|-----------|------|
| `chipDamName === '일치'` | 초록색 | 일치 |
| `chipDamName === '불일치'` | 빨간색 | 불일치 |
| `chipDamName === '이력제부재'` | 주황색 | 이력제부재 |
| 그 외/null | - | 표시 안 함 |
---
## 5. 분석불가 안내 문구
| 상태 | 안내 문구 |
|------|-----------|
| `분석불가` (DB) | 모근 오염 및 불량 등 기타 사유로 유전체 분석 보고서를 제공할 수 없습니다. |
| `정보없음` (DB) | 개체 식별번호 및 형식오류로 유전체 분석 보고서를 제공할 수 없습니다. |
| `부 불일치` | 부 친자감별 결과가 불일치하여 유전체 분석 보고서를 제공할 수 없습니다. |
| `모 불일치` | 모 친자감별 결과가 불일치하여 유전체 분석 보고서를 제공할 수 없습니다. |
| `모 이력제부재` | 모 이력제 정보가 부재하여 유전체 분석 보고서를 제공할 수 없습니다. |
| `EXCLUDED_COW_IDS` | 모근 오염 및 불량 등 기타 사유로 유전체 분석 보고서를 제공할 수 없습니다. |
---
## 6. 관련 파일
### 백엔드
- `backend/src/common/config/GenomeAnalysisConfig.ts` - 유효성 검사 함수
- `backend/src/cow/cow.service.ts` - unavailableReason 결정 로직
### 프론트엔드
- `frontend/src/lib/utils/genome-analysis-config.ts` - 유효성 검사, 메시지 함수
- `frontend/src/app/cow/page.tsx` - 개체 목록 배지
- `frontend/src/app/cow/[cowNo]/page.tsx` - 개체 상세 배지, 탭 조건
---
## 7. 제외 개체 목록 (EXCLUDED_COW_IDS)
특수 사유로 분석 불가한 개체를 하드코딩으로 관리:
```typescript
export const EXCLUDED_COW_IDS = [
'KOR002191642861', // 모근상태 불량으로 인한 DNA분해
];
```
> 이 목록에 포함된 개체는 유전체/유전자 탭 모두 분석불가로 처리됨

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