From c8bd04f1243062efa36c6c92c747b2948163a7b2 Mon Sep 17 00:00:00 2001 From: chu eun ju Date: Fri, 19 Dec 2025 15:19:50 +0900 Subject: [PATCH] =?UTF-8?q?=EA=B0=9C=EC=B2=B4=EB=B6=84=EC=84=9D=20?= =?UTF-8?q?=EC=83=81=ED=83=9C=20=EA=B0=92=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/doc/검사가능조건요약.md | 146 ++++ .../src/common/config/GenomeAnalysisConfig.ts | 8 +- backend/src/common/config/MptNormalRanges.ts | 8 +- .../src/common/config/RecommendationConfig.ts | 105 --- backend/src/cow/cow.module.ts | 2 + backend/src/cow/cow.service.ts | 55 +- backend/src/mpt/entities/mpt.entity.ts | 4 +- frontend/src/app/cow/[cowNo]/page.tsx | 622 ++++++-------- .../reproduction/_components/mpt-table.tsx | 2 +- .../src/app/cow/[cowNo]/reproduction/page.tsx | 4 +- frontend/src/app/cow/page.tsx | 36 +- frontend/src/app/mpt/page.tsx | 4 +- .../src/components/common/AnimatedDesktop.tsx | 57 -- frontend/src/components/common/data-table.tsx | 807 ------------------ .../common/global-filter-dialog.tsx | 29 +- .../components/common/region-benchmark.tsx | 56 -- .../components/genome/gene-filter-modal.tsx | 37 +- .../components/genome/gene-search-modal.tsx | 19 +- frontend/src/constants/mpt-reference.ts | 15 +- frontend/src/lib/api/mpt.api.ts | 2 +- frontend/src/lib/api/repro.api.ts | 2 +- .../src/lib/utils/genome-analysis-config.ts | 65 +- frontend/src/types/cow.types.ts | 6 + .../types/{reprompt.types.ts => mpt.types.ts} | 4 +- 24 files changed, 596 insertions(+), 1499 deletions(-) create mode 100644 backend/doc/검사가능조건요약.md delete mode 100644 backend/src/common/config/RecommendationConfig.ts delete mode 100644 frontend/src/components/common/AnimatedDesktop.tsx delete mode 100644 frontend/src/components/common/data-table.tsx delete mode 100644 frontend/src/components/common/region-benchmark.tsx rename frontend/src/types/{reprompt.types.ts => mpt.types.ts} (97%) diff --git a/backend/doc/검사가능조건요약.md b/backend/doc/검사가능조건요약.md new file mode 100644 index 0000000..5b64c4b --- /dev/null +++ b/backend/doc/검사가능조건요약.md @@ -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분해 +]; +``` + +> 이 목록에 포함된 개체는 유전체/유전자 탭 모두 분석불가로 처리됨 diff --git a/backend/src/common/config/GenomeAnalysisConfig.ts b/backend/src/common/config/GenomeAnalysisConfig.ts index 6ece3d9..12bf369 100644 --- a/backend/src/common/config/GenomeAnalysisConfig.ts +++ b/backend/src/common/config/GenomeAnalysisConfig.ts @@ -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; } diff --git a/backend/src/common/config/MptNormalRanges.ts b/backend/src/common/config/MptNormalRanges.ts index acac3d4..fc5686e 100644 --- a/backend/src/common/config/MptNormalRanges.ts +++ b/backend/src/common/config/MptNormalRanges.ts @@ -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; /** diff --git a/backend/src/common/config/RecommendationConfig.ts b/backend/src/common/config/RecommendationConfig.ts deleted file mode 100644 index f85f6db..0000000 --- a/backend/src/common/config/RecommendationConfig.ts +++ /dev/null @@ -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; diff --git a/backend/src/cow/cow.module.ts b/backend/src/cow/cow.module.ts index b1e8680..46f31ef 100644 --- a/backend/src/cow/cow.module.ts +++ b/backend/src/cow/cow.module.ts @@ -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, // 필터 엔진 모듈 ], diff --git a/backend/src/cow/cow.service.ts b/backend/src/cow/cow.service.ts index 59b2e62..8fb6dd1 100644 --- a/backend/src/cow/cow.service.ts +++ b/backend/src/cow/cow.service.ts @@ -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, + // 유전자 상세 Repository (SNP 데이터 접근용) + @InjectRepository(GeneDetailModel) + private readonly geneDetailRepository: Repository, + // 동적 필터링 서비스 (검색, 정렬, 페이지네이션) 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 { + async findByCowId(cowId: string): Promise { 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: [] }; } diff --git a/backend/src/mpt/entities/mpt.entity.ts b/backend/src/mpt/entities/mpt.entity.ts index 2e07be2..64edbe9 100644 --- a/backend/src/mpt/entities/mpt.entity.ts +++ b/backend/src/mpt/entities/mpt.entity.ts @@ -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' }) diff --git a/frontend/src/app/cow/[cowNo]/page.tsx b/frontend/src/app/cow/[cowNo]/page.tsx index 09e549c..15f5c17 100644 --- a/frontend/src/app/cow/[cowNo]/page.tsx +++ b/frontend/src/app/cow/[cowNo]/page.tsx @@ -1,43 +1,40 @@ 'use client' -import { useSearchParams, useParams, useRouter } from "next/navigation" +import { AuthGuard } from "@/components/auth/auth-guard" +import { CowNumberDisplay } from "@/components/common/cow-number-display" import { AppSidebar } from "@/components/layout/app-sidebar" import { SiteHeader } from "@/components/layout/site-header" -import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar" import { Button } from "@/components/ui/button" import { Card, CardContent } from "@/components/ui/card" import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog" -import { Badge } from "@/components/ui/badge" -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" -import { useToast } from "@/hooks/use-toast" -import { useMediaQuery } from "@/hooks/use-media-query" -import { ComparisonAveragesDto, TraitComparisonAveragesDto, cowApi, genomeApi, geneApi, GeneDetail, GenomeRequestDto, mptApi, MptDto } from "@/lib/api" -import { CowDetail } from "@/types/cow.types" -import { GenomeTrait } from "@/types/genome.types" -import { useGlobalFilter } from "@/contexts/GlobalFilterContext" -import { - ArrowLeft, - ArrowUp, - BarChart3, - CheckCircle2, - Download, - Dna, - Activity, - X, - XCircle, - Search, -} from 'lucide-react' import { Input } from "@/components/ui/input" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" +import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar" +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" +import { useGlobalFilter } from "@/contexts/GlobalFilterContext" +import { useMediaQuery } from "@/hooks/use-media-query" +import { useToast } from "@/hooks/use-toast" +import { ComparisonAveragesDto, cowApi, geneApi, GeneDetail, genomeApi, GenomeRequestDto, TraitComparisonAveragesDto } from "@/lib/api" +import { getInvalidMessage, getInvalidReason, isExcludedCow, isValidGenomeAnalysis } from "@/lib/utils/genome-analysis-config" +import { CowDetail } from "@/types/cow.types" +import { GenomeTrait } from "@/types/genome.types" +import { + Activity, + ArrowLeft, + BarChart3, + CheckCircle2, + ChevronUp, + Dna, + Search, + X, + XCircle +} from 'lucide-react' +import { useParams, useRouter, useSearchParams } from "next/navigation" import { useEffect, useMemo, useRef, useState } from 'react' import { CategoryEvaluationCard } from "./genome/_components/category-evaluation-card" +import { TraitComparison } from "./genome/_components/genome-integrated-comparison" import { NormalDistributionChart } from "./genome/_components/normal-distribution-chart" import { TraitDistributionCharts } from "./genome/_components/trait-distribution-charts" -import { TraitComparison } from "./genome/_components/genome-integrated-comparison" -import { CowNumberDisplay } from "@/components/common/cow-number-display" -import { isValidGenomeAnalysis, getInvalidReason, getInvalidMessage } from "@/lib/utils/genome-analysis-config" -import { AuthGuard } from "@/components/auth/auth-guard" -import { MptTable } from "./reproduction/_components/mpt-table" // 형질명 → 카테고리 매핑 (한우 35개 형질) const TRAIT_CATEGORY_MAP: Record = { @@ -254,6 +251,74 @@ export default function CowOverviewPage() { const [geneSortBy, setGeneSortBy] = useState<'snpName' | 'chromosome' | 'position' | 'snpType' | 'allele1' | 'allele2' | 'remarks'>('snpName') const [geneSortOrder, setGeneSortOrder] = useState<'asc' | 'desc'>('asc') + // 부 KPN 배지 렌더링 (분석불가/일치/불일치) + const renderSireBadge = (chipSireName: string | null | undefined, size: 'sm' | 'lg' = 'lg') => { + const sizeClasses = size === 'lg' + ? 'gap-1.5 text-sm px-3 py-1.5' + : 'gap-1 text-xs px-2 py-1' + const iconSize = size === 'lg' ? 'w-4 h-4' : 'w-3 h-3' + + // 분석불가 개체 먼저 체크 (EXCLUDED_COW_IDS 또는 DB에서 '분석불가'/'정보없음'으로 저장된 경우) + if (isExcludedCow(cow?.cowId) || chipSireName === '분석불가' || chipSireName === '정보없음') { + return ( + + + 분석불가 + + ) + } else if (chipSireName === '일치') { + return ( + + + 일치 + + ) + } else if (chipSireName && chipSireName !== '일치') { + return ( + + + 불일치 + + ) + } + return null + } + + // 모 개체 배지 렌더링 (일치/불일치/이력제부재) + const renderDamBadge = (chipDamName: string | null | undefined, size: 'sm' | 'lg' = 'lg') => { + // 분석불가 개체는 어미 배지 표시 안 함 + if (isExcludedCow(cow?.cowId)) return null + + const sizeClasses = size === 'lg' + ? 'gap-1.5 text-sm px-3 py-1.5' + : 'gap-1 text-xs px-2 py-1' + const iconSize = size === 'lg' ? 'w-4 h-4' : 'w-3 h-3' + + if (chipDamName === '일치') { + return ( + + + 일치 + + ) + } else if (chipDamName === '불일치') { + return ( + + + 불일치 + + ) + } else if (chipDamName === '이력제부재') { + return ( + + + 이력제부재 + + ) + } + return null + } + // 유전자 데이터 지연 로드 함수 const loadGeneData = async () => { if (geneDataLoaded || geneDataLoading) return // 이미 로드했거나 로딩 중이면 스킵 @@ -264,10 +329,12 @@ export default function CowOverviewPage() { const geneList = geneDataResult || [] setGeneData(geneList) setGeneDataLoaded(true) + setHasGeneData(geneList.length > 0) } catch (geneErr) { console.error('유전자 데이터 조회 실패:', geneErr) setGeneData([]) setGeneDataLoaded(true) + setHasGeneData(false) } finally { setGeneDataLoading(false) } @@ -318,6 +385,11 @@ export default function CowOverviewPage() { } setCow(cowDetail) + // dataStatus에서 데이터 존재 여부 설정 (백엔드에서 가벼운 COUNT 쿼리로 확인) + if (cowData.dataStatus) { + setHasGeneData(cowData.dataStatus.hasGeneData) + } + // 유전체 데이터 가져오기 const genomeDataResult = await genomeApi.findByCowNo(cowNo) setGenomeData(genomeDataResult) @@ -333,9 +405,6 @@ export default function CowOverviewPage() { setGenomeRequest(null) } - // 유전자(SNP) 데이터는 탭 클릭 시 로드 (지연 로딩) - setHasGeneData(true) // 탭은 보여주되, 데이터는 나중에 로드 - // 번식능력 데이터 (현재는 목업 - 추후 API 연동) // TODO: 번식능력 API 연동 setHasReproductionData(false) @@ -579,8 +648,8 @@ export default function CowOverviewPage() { > 유전자 - - {hasGeneData && isValidGenomeAnalysis(genomeRequest?.chipSireName, genomeRequest?.chipDamName, cow?.cowId) ? '완료' : '미검사'} + + {hasGeneData && !(isExcludedCow(cow?.cowId) || genomeRequest?.chipSireName === '분석불가' || genomeRequest?.chipSireName === '정보없음') ? '완료' : '미검사'} {cow?.sireKpn && cow.sireKpn !== '0' ? cow.sireKpn : '-'} - {(() => { - const chipSireName = genomeRequest?.chipSireName - if (chipSireName === '일치') { - return ( - - - 일치 - - ) - } else if (chipSireName && chipSireName !== '일치') { - return ( - - - 불일치 - - ) - } else { - return ( - - 분석불가 - - ) - } - })()} + {renderSireBadge(genomeRequest?.chipSireName)}
@@ -731,34 +777,7 @@ export default function CowOverviewPage() { ) : ( - )} - {(() => { - const chipDamName = genomeRequest?.chipDamName - if (chipDamName === '일치') { - return ( - - - 일치 - - ) - } else if (chipDamName === '불일치') { - return ( - - - 불일치 - - ) - } else if (chipDamName === '이력제부재') { - return ( - - - 이력제부재 - - ) - } else { - // 정보없음(null/undefined)일 때는 배지 표시 안함 - return null - } - })()} + {renderDamBadge(genomeRequest?.chipDamName)}
@@ -770,30 +789,7 @@ export default function CowOverviewPage() { {cow?.sireKpn && cow.sireKpn !== '0' ? cow.sireKpn : '-'} - {(() => { - const chipSireName = genomeRequest?.chipSireName - if (chipSireName === '일치') { - return ( - - - 일치 - - ) - } else if (chipSireName && chipSireName !== '일치') { - return ( - - - 불일치 - - ) - } else { - return ( - - 분석불가 - - ) - } - })()} + {renderSireBadge(genomeRequest?.chipSireName, 'sm')}
@@ -804,34 +800,7 @@ export default function CowOverviewPage() { ) : ( - )} - {(() => { - const chipDamName = genomeRequest?.chipDamName - if (chipDamName === '일치') { - return ( - - - 일치 - - ) - } else if (chipDamName === '불일치') { - return ( - - - 불일치 - - ) - } else if (chipDamName === '이력제부재') { - return ( - - - 이력제부재 - - ) - } else { - // 정보없음(null/undefined)일 때는 배지 표시 안함 - return null - } - })()} + {renderDamBadge(genomeRequest?.chipDamName, 'sm')}
@@ -1062,30 +1031,7 @@ export default function CowOverviewPage() {
{cow?.sireKpn && cow.sireKpn !== '0' ? cow.sireKpn : '-'} - {(() => { - const chipSireName = genomeRequest?.chipSireName - if (chipSireName === '일치') { - return ( - - - 일치 - - ) - } else if (chipSireName && chipSireName !== '일치') { - return ( - - - 불일치 - - ) - } else { - return ( - - 분석불가 - - ) - } - })()} + {renderSireBadge(genomeRequest?.chipSireName)}
@@ -1098,34 +1044,7 @@ export default function CowOverviewPage() { ) : ( - )} - {(() => { - const chipDamName = genomeRequest?.chipDamName - if (chipDamName === '일치') { - return ( - - - 일치 - - ) - } else if (chipDamName === '불일치') { - return ( - - - 불일치 - - ) - } else if (chipDamName === '이력제부재') { - return ( - - - 이력제부재 - - ) - } else { - // 정보없음(null/undefined)일 때는 배지 표시 안함 - return null - } - })()} + {renderDamBadge(genomeRequest?.chipDamName)}
@@ -1135,30 +1054,7 @@ export default function CowOverviewPage() { 부 KPN
{cow?.sireKpn && cow.sireKpn !== '0' ? cow.sireKpn : '-'} - {(() => { - const chipSireName = genomeRequest?.chipSireName - if (chipSireName === '일치') { - return ( - - - 일치 - - ) - } else if (chipSireName && chipSireName !== '일치') { - return ( - - - 불일치 - - ) - } else { - return ( - - 분석불가 - - ) - } - })()} + {renderSireBadge(genomeRequest?.chipSireName, 'sm')}
@@ -1169,34 +1065,7 @@ export default function CowOverviewPage() { ) : ( - )} - {(() => { - const chipDamName = genomeRequest?.chipDamName - if (chipDamName === '일치') { - return ( - - - 일치 - - ) - } else if (chipDamName === '불일치') { - return ( - - - 불일치 - - ) - } else if (chipDamName === '이력제부재') { - return ( - - - 이력제부재 - - ) - } else { - // 정보없음(null/undefined)일 때는 배지 표시 안함 - return null - } - })()} + {renderDamBadge(genomeRequest?.chipDamName, 'sm')}
@@ -1333,30 +1202,7 @@ export default function CowOverviewPage() { {cow?.sireKpn && cow.sireKpn !== '0' ? cow.sireKpn : '-'} - {(() => { - const chipSireName = genomeRequest?.chipSireName - if (chipSireName === '일치') { - return ( - - - 일치 - - ) - } else if (chipSireName && chipSireName !== '일치') { - return ( - - - 불일치 - - ) - } else { - return ( - - 분석불가 - - ) - } - })()} + {renderSireBadge(genomeRequest?.chipSireName)}
@@ -1369,33 +1215,7 @@ export default function CowOverviewPage() { ) : ( - )} - {(() => { - const chipDamName = genomeRequest?.chipDamName - if (chipDamName === '일치') { - return ( - - - 일치 - - ) - } else if (chipDamName === '불일치') { - return ( - - - 불일치 - - ) - } else if (chipDamName === '이력제부재') { - return ( - - - 이력제부재 - - ) - } else { - return null - } - })()} + {renderDamBadge(genomeRequest?.chipDamName)}
@@ -1407,30 +1227,7 @@ export default function CowOverviewPage() { {cow?.sireKpn && cow.sireKpn !== '0' ? cow.sireKpn : '-'} - {(() => { - const chipSireName = genomeRequest?.chipSireName - if (chipSireName === '일치') { - return ( - - - 일치 - - ) - } else if (chipSireName && chipSireName !== '일치') { - return ( - - - 불일치 - - ) - } else { - return ( - - 분석불가 - - ) - } - })()} + {renderSireBadge(genomeRequest?.chipSireName, 'sm')}
@@ -1441,33 +1238,7 @@ export default function CowOverviewPage() { ) : ( - )} - {(() => { - const chipDamName = genomeRequest?.chipDamName - if (chipDamName === '일치') { - return ( - - - 일치 - - ) - } else if (chipDamName === '불일치') { - return ( - - - 불일치 - - ) - } else if (chipDamName === '이력제부재') { - return ( - - - 이력제부재 - - ) - } else { - return null - } - })()} + {renderDamBadge(genomeRequest?.chipDamName, 'sm')}
@@ -1477,8 +1248,8 @@ export default function CowOverviewPage() { {/* 유전자 검색 및 필터 섹션 */}

유전자 분석 결과

- {/* 친자확인 결과에 따른 분기 */} - {isValidGenomeAnalysis(genomeRequest?.chipSireName, genomeRequest?.chipDamName, cow?.cowId) ? ( + {/* 유전자 탭 분기: 분석불가/정보없음만 차단, 불일치/이력제부재는 유전자 데이터 표시 */} + {!(isExcludedCow(cow?.cowId) || genomeRequest?.chipSireName === '분석불가' || genomeRequest?.chipSireName === '정보없음') ? ( <>
{/* 검색창 */} @@ -1867,15 +1638,154 @@ export default function CowOverviewPage() { )} ) : ( - - - -

유전자 분석 데이터 없음

-

- 이 개체는 아직 유전자(SNP) 분석이 완료되지 않았습니다. -

-
-
+ <> + {/* 개체 정보 섹션 */} +

개체 정보

+ + + + {/* 데스크탑: 가로 그리드 */} +
+
+
+ 개체번호 +
+
+ +
+
+
+
+ 생년월일 +
+
+ + {cow?.cowBirthDt ? new Date(cow.cowBirthDt).toLocaleDateString('ko-KR') : '-'} + +
+
+
+
+ 월령 +
+
+ + {cow?.cowBirthDt + ? `${Math.floor((new Date().getTime() - new Date(cow.cowBirthDt).getTime()) / (1000 * 60 * 60 * 24 * 30.44))}개월` + : '-'} + +
+
+
+
+ 분석일자 +
+
+ - +
+
+
+ {/* 모바일: 좌우 배치 리스트 */} +
+
+ 개체번호 +
+ +
+
+
+ 생년월일 + + {cow?.cowBirthDt ? new Date(cow.cowBirthDt).toLocaleDateString('ko-KR') : '-'} + +
+
+ 월령 + + {cow?.cowBirthDt + ? `${Math.floor((new Date().getTime() - new Date(cow.cowBirthDt).getTime()) / (1000 * 60 * 60 * 24 * 30.44))}개월` + : '-'} + +
+
+ 분석일자 + - +
+
+
+
+ + {/* 혈통정보 섹션 */} +

혈통정보

+ + + + {/* 데스크탑: 가로 그리드 */} +
+
+
+ 부 KPN번호 +
+
+ {cow?.sireKpn && cow.sireKpn !== '0' ? cow.sireKpn : '-'} + {renderSireBadge(genomeRequest?.chipSireName)} +
+
+
+
+ 모 개체번호 +
+
+ {cow?.damCowId && cow.damCowId !== '0' ? ( + + ) : ( + - + )} + {renderDamBadge(genomeRequest?.chipDamName)} +
+
+
+ {/* 모바일: 세로 리스트 */} +
+
+ 부 KPN +
+ {cow?.sireKpn && cow.sireKpn !== '0' ? cow.sireKpn : '-'} + {renderSireBadge(genomeRequest?.chipSireName, 'sm')} +
+
+
+ 모 개체번호 +
+ {cow?.damCowId && cow.damCowId !== '0' ? ( + + ) : ( + - + )} + {renderDamBadge(genomeRequest?.chipDamName, 'sm')} +
+
+
+
+
+ + {/* 유전자 분석 결과 섹션 */} +

유전자 분석 결과

+ + + +

+ {genomeRequest ? '유전자 분석 불가' : '유전자 분석불가'} +

+

+ {genomeRequest + ? getInvalidMessage(genomeRequest?.chipSireName, genomeRequest?.chipDamName, cow?.cowId).replace('유전체', '유전자') + : '이 개체는 아직 유전자(SNP) 분석이 진행되지 않았습니다.' + } +

+
+
+ )} @@ -1955,14 +1865,14 @@ export default function CowOverviewPage() { - {/* 플로팅 맨 위로 버튼 */} + {/* 플로팅 맨 위로 버튼 - 글래스모피즘 */} {showScrollTop && ( )} diff --git a/frontend/src/app/cow/[cowNo]/reproduction/_components/mpt-table.tsx b/frontend/src/app/cow/[cowNo]/reproduction/_components/mpt-table.tsx index bcdbbbd..7de57d9 100644 --- a/frontend/src/app/cow/[cowNo]/reproduction/_components/mpt-table.tsx +++ b/frontend/src/app/cow/[cowNo]/reproduction/_components/mpt-table.tsx @@ -16,7 +16,7 @@ const MPT_CATEGORIES = [ { name: '단백질 대사', items: ['totalProtein', 'albumin', 'globulin', 'agRatio', 'bun'], 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: ['creatinine'], color: 'bg-muted/50' }, + { name: '기타', items: ['creatine'], color: 'bg-muted/50' }, ] // 측정값 상태 판정 diff --git a/frontend/src/app/cow/[cowNo]/reproduction/page.tsx b/frontend/src/app/cow/[cowNo]/reproduction/page.tsx index 8129f86..173af2d 100644 --- a/frontend/src/app/cow/[cowNo]/reproduction/page.tsx +++ b/frontend/src/app/cow/[cowNo]/reproduction/page.tsx @@ -10,7 +10,7 @@ import { Badge } from "@/components/ui/badge" import { Progress } from "@/components/ui/progress" import { cowApi, reproApi } from "@/lib/api" import { CowDetail } from "@/types/cow.types" -import { ReproMpt } from "@/types/reprompt.types" +import { ReproMpt } from "@/types/mpt.types" import { Activity, AlertCircle, CheckCircle } from "lucide-react" import { CowNavigation } from "../_components/navigation" import { useToast } from "@/hooks/use-toast" @@ -140,7 +140,7 @@ export default function ReproductionPage() { { name: '인', value: reproMpt[0].phosphorus, fieldName: 'phosphorus' }, { name: '칼슘/인', value: reproMpt[0].caPRatio, fieldName: 'caPRatio' }, { name: '마그네슘', value: reproMpt[0].magnesium, fieldName: 'magnesium' }, - { name: '크레아틴', value: reproMpt[0].creatinine, fieldName: 'creatinine' }, + { name: '크레아틴', value: reproMpt[0].creatine, fieldName: 'creatine' }, ] : [] const normalItems = mptItems.filter(item => { diff --git a/frontend/src/app/cow/page.tsx b/frontend/src/app/cow/page.tsx index 4c6b9ea..dbcaa27 100644 --- a/frontend/src/app/cow/page.tsx +++ b/frontend/src/app/cow/page.tsx @@ -680,7 +680,7 @@ function MyCowContent() {
{/* 제목 */}
-

개체 목록

+

개체 목록

{'농장'} 보유 개체 현황

@@ -737,12 +737,12 @@ function MyCowContent() { {/* 필터 옵션들 - 모바일: 2행, 데스크톱: 1행 */}
{/* 랭킹/정렬 그룹 */} -
+
- - ), - }, - { - accessorKey: "limit", - header: () =>
Limit
, - cell: ({ row }) => ( -
{ - e.preventDefault() - toast.promise(new Promise((resolve) => setTimeout(resolve, 1000)), { - loading: `Saving ${row.original.header}`, - success: "Done", - error: "Error", - }) - }} - > - - -
- ), - }, - { - accessorKey: "reviewer", - header: "Reviewer", - cell: ({ row }) => { - const isAssigned = row.original.reviewer !== "Assign reviewer" - - if (isAssigned) { - return row.original.reviewer - } - - return ( - <> - - - - ) - }, - }, - { - id: "actions", - cell: () => ( - - - - - - Edit - Make a copy - Favorite - - Delete - - - ), - }, -] - -function DraggableRow({ row }: { row: Row> }) { - const { transform, transition, setNodeRef, isDragging } = useSortable({ - id: row.original.id, - }) - - return ( - - {row.getVisibleCells().map((cell) => ( - - {flexRender(cell.column.columnDef.cell, cell.getContext())} - - ))} - - ) -} - -export function DataTable({ - data: initialData, -}: { - data: z.infer[] -}) { - const [data, setData] = React.useState(() => initialData) - const [rowSelection, setRowSelection] = React.useState({}) - const [columnVisibility, setColumnVisibility] = - React.useState({}) - const [columnFilters, setColumnFilters] = React.useState( - [] - ) - const [sorting, setSorting] = React.useState([]) - const [pagination, setPagination] = React.useState({ - pageIndex: 0, - pageSize: 10, - }) - const sortableId = React.useId() - const sensors = useSensors( - useSensor(MouseSensor, {}), - useSensor(TouchSensor, {}), - useSensor(KeyboardSensor, {}) - ) - - const dataIds = React.useMemo( - () => data?.map(({ id }) => id) || [], - [data] - ) - - const table = useReactTable({ - data, - columns, - state: { - sorting, - columnVisibility, - rowSelection, - columnFilters, - pagination, - }, - getRowId: (row) => row.id.toString(), - enableRowSelection: true, - onRowSelectionChange: setRowSelection, - onSortingChange: setSorting, - onColumnFiltersChange: setColumnFilters, - onColumnVisibilityChange: setColumnVisibility, - onPaginationChange: setPagination, - getCoreRowModel: getCoreRowModel(), - getFilteredRowModel: getFilteredRowModel(), - getPaginationRowModel: getPaginationRowModel(), - getSortedRowModel: getSortedRowModel(), - getFacetedRowModel: getFacetedRowModel(), - getFacetedUniqueValues: getFacetedUniqueValues(), - }) - - function handleDragEnd(event: DragEndEvent) { - const { active, over } = event - if (active && over && active.id !== over.id) { - setData((data) => { - const oldIndex = dataIds.indexOf(active.id) - const newIndex = dataIds.indexOf(over.id) - return arrayMove(data, oldIndex, newIndex) - }) - } - } - - return ( - -
- - - - Outline - - Past Performance 3 - - - Key Personnel 2 - - Focus Documents - -
- - - - - - {table - .getAllColumns() - .filter( - (column) => - typeof column.accessorFn !== "undefined" && - column.getCanHide() - ) - .map((column) => { - return ( - - column.toggleVisibility(!!value) - } - > - {column.id} - - ) - })} - - - -
-
- -
- - - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => { - return ( - - {header.isPlaceholder - ? null - : flexRender( - header.column.columnDef.header, - header.getContext() - )} - - ) - })} - - ))} - - - {table.getRowModel().rows?.length ? ( - - {table.getRowModel().rows.map((row) => ( - - ))} - - ) : ( - - - No results. - - - )} - -
-
-
-
-
- {table.getFilteredSelectedRowModel().rows.length} of{" "} - {table.getFilteredRowModel().rows.length} row(s) selected. -
-
-
- - -
-
- Page {table.getState().pagination.pageIndex + 1} of{" "} - {table.getPageCount()} -
-
- - - - -
-
-
-
- -
-
- -
-
- -
-
-
- ) -} - -const chartData = [ - { month: "January", desktop: 186, mobile: 80 }, - { month: "February", desktop: 305, mobile: 200 }, - { month: "March", desktop: 237, mobile: 120 }, - { month: "April", desktop: 73, mobile: 190 }, - { month: "May", desktop: 209, mobile: 130 }, - { month: "June", desktop: 214, mobile: 140 }, -] - -const chartConfig = { - desktop: { - label: "Desktop", - color: "var(--primary)", - }, - mobile: { - label: "Mobile", - color: "var(--primary)", - }, -} satisfies ChartConfig - -function TableCellViewer({ item }: { item: z.infer }) { - const isMobile = useIsMobile() - - return ( - - - - - - - {item.header} - - Showing total visitors for the last 6 months - - -
- {!isMobile && ( - <> - - - - value.slice(0, 3)} - hide - /> - } - /> - - - - - -
-
- Trending up by 5.2% this month{" "} - -
-
- Showing total visitors for the last 6 months. This is just - some random text to test the layout. It spans multiple lines - and should wrap around. -
-
- - - )} -
-
- - -
-
-
- - -
-
- - -
-
-
-
- - -
-
- - -
-
-
- - -
-
-
- - - - - - -
-
- ) -} diff --git a/frontend/src/components/common/global-filter-dialog.tsx b/frontend/src/components/common/global-filter-dialog.tsx index b99ae84..39e38d1 100644 --- a/frontend/src/components/common/global-filter-dialog.tsx +++ b/frontend/src/components/common/global-filter-dialog.tsx @@ -295,21 +295,22 @@ export function GlobalFilterDialog({ externalOpen, onExternalOpenChange, geneCou }, [open, filters]) // 전체 유전자 로드 + // TODO: 백엔드 /gene/markers API 구현 후 활성화 const loadAllGenes = async () => { - try { - setLoadingGenes(true) - const [qtyGenes, qltGenes] = await Promise.all([ - geneApi.getGenesByType('QTY'), - geneApi.getGenesByType('QLT'), - ]) - setQuantityGenes(qtyGenes) - setQualityGenes(qltGenes) - } catch { - setQuantityGenes([]) - setQualityGenes([]) - } finally { - setLoadingGenes(false) - } + // try { + // setLoadingGenes(true) + // const [qtyGenes, qltGenes] = await Promise.all([ + // geneApi.getGenesByType('QTY'), + // geneApi.getGenesByType('QLT'), + // ]) + // setQuantityGenes(qtyGenes) + // setQualityGenes(qltGenes) + // } catch { + // setQuantityGenes([]) + // setQualityGenes([]) + // } finally { + // setLoadingGenes(false) + // } } // 필터 활성화 여부 diff --git a/frontend/src/components/common/region-benchmark.tsx b/frontend/src/components/common/region-benchmark.tsx deleted file mode 100644 index 6cbebaa..0000000 --- a/frontend/src/components/common/region-benchmark.tsx +++ /dev/null @@ -1,56 +0,0 @@ -'use client' - -import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card" -import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, Cell } from 'recharts' - -// 벤치마크 데이터 -const benchmarkData = [ - { category: '유전체 점수', myFarm: 79.8, regional: 65.2, top10: 88.5 }, - { category: 'MPT 충족률', myFarm: 74.6, regional: 72.0, top10: 85.2 }, - { category: 'A등급 비율', myFarm: 37.5, regional: 18.8, top10: 45.0 }, - { category: '번식능력', myFarm: 72, regional: 70, top10: 82 }, -] - -export function RegionBenchmark() { - return ( - - - 보은군 비교 벤치마크 - - 내농장 vs 보은군 평균 vs 상위 10% - - - - - - - - - `${value.toFixed(1)}`} - /> - - - - - - - - - - ) -} diff --git a/frontend/src/components/genome/gene-filter-modal.tsx b/frontend/src/components/genome/gene-filter-modal.tsx index 945c9fe..d7460c5 100644 --- a/frontend/src/components/genome/gene-filter-modal.tsx +++ b/frontend/src/components/genome/gene-filter-modal.tsx @@ -61,27 +61,28 @@ export function GeneFilterModal({ open, onOpenChange, selectedGenes, onConfirm } } }, [open]) + // TODO: 백엔드 /gene/markers API 구현 후 활성화 const fetchMarkers = async () => { - try { - setLoading(true) - setError(null) - const markers = await geneApi.getAllMarkers() as unknown as MarkerData[] + // try { + // setLoading(true) + // setError(null) + // const markers = await geneApi.getAllMarkers() as unknown as MarkerData[] - // API 데이터를 GeneOption 형식으로 변환 - const geneOptions: GeneOption[] = markers.map(marker => ({ - name: marker.markerNm, - description: marker.relatedTrait || marker.markerDesc || '', - type: marker.markerTypeCd as 'QTY' | 'QLT', - relatedTrait: marker.relatedTrait || '' - })) + // // API 데이터를 GeneOption 형식으로 변환 + // const geneOptions: GeneOption[] = markers.map(marker => ({ + // name: marker.markerNm, + // description: marker.relatedTrait || marker.markerDesc || '', + // type: marker.markerTypeCd as 'QTY' | 'QLT', + // relatedTrait: marker.relatedTrait || '' + // })) - setAllMarkers(geneOptions) - } catch (err) { - console.error('Failed to fetch markers:', err) - setError('유전자 목록을 불러오는데 실패했습니다.') - } finally { - setLoading(false) - } + // setAllMarkers(geneOptions) + // } catch (err) { + // console.error('Failed to fetch markers:', err) + // setError('유전자 목록을 불러오는데 실패했습니다.') + // } finally { + // setLoading(false) + // } } // 육량형/육질형 필터링 diff --git a/frontend/src/components/genome/gene-search-modal.tsx b/frontend/src/components/genome/gene-search-modal.tsx index 5babac7..6672361 100644 --- a/frontend/src/components/genome/gene-search-modal.tsx +++ b/frontend/src/components/genome/gene-search-modal.tsx @@ -30,16 +30,17 @@ export function GeneSearchModal({ open, onOpenChange, selectedGenes, onGenesChan } }, [open]) + // TODO: 백엔드 /gene/markers API 구현 후 활성화 const loadAllGenes = async () => { - try { - setLoading(true) - const genes = await geneApi.getAllMarkers() - setAllGenes(genes) - } catch { - // 유전자 로드 실패 시 빈 배열 유지 - } finally { - setLoading(false) - } + // try { + // setLoading(true) + // const genes = await geneApi.getAllMarkers() + // setAllGenes(genes) + // } catch { + // // 유전자 로드 실패 시 빈 배열 유지 + // } finally { + // setLoading(false) + // } } // 검색 및 필터링 diff --git a/frontend/src/constants/mpt-reference.ts b/frontend/src/constants/mpt-reference.ts index 50d0fb5..3095ca0 100644 --- a/frontend/src/constants/mpt-reference.ts +++ b/frontend/src/constants/mpt-reference.ts @@ -38,15 +38,6 @@ export const MPT_REFERENCE_RANGES: Record = { category: '에너지', description: '혈액 내 유리지방산 수치', }, - bcs: { - name: 'BCS', - upperLimit: 3.5, - lowerLimit: 2.5, - unit: '-', - category: '에너지', - description: '혈액 내 BCS 수치', - }, - // 단백질 카테고리 totalProtein: { name: '총단백질', @@ -150,13 +141,13 @@ export const MPT_REFERENCE_RANGES: Record = { }, // 기타 카테고리 - creatinine: { - name: '크레아티닌', + creatine: { + name: '크레아틴', upperLimit: 1.3, lowerLimit: 1.0, unit: 'mg/dL', category: '기타', - description: '혈액 내 크레아티닌 수치', + description: '혈액 내 크레아틴 수치', }, }; diff --git a/frontend/src/lib/api/mpt.api.ts b/frontend/src/lib/api/mpt.api.ts index d5c7736..39ff8ae 100644 --- a/frontend/src/lib/api/mpt.api.ts +++ b/frontend/src/lib/api/mpt.api.ts @@ -31,7 +31,7 @@ export interface MptDto { phosphorus: number; caPRatio: number; magnesium: number; - creatinine: number; + creatine: number; // Relations farm?: { pkFarmNo: number; diff --git a/frontend/src/lib/api/repro.api.ts b/frontend/src/lib/api/repro.api.ts index ff7d3d3..4a16dda 100644 --- a/frontend/src/lib/api/repro.api.ts +++ b/frontend/src/lib/api/repro.api.ts @@ -1,5 +1,5 @@ import apiClient from '../api-client'; -import { ReproMpt } from '@/types/reprompt.types'; +import { ReproMpt } from '@/types/mpt.types'; /** * 번식정보(Reproduction) 관련 API - MPT(혈액검사) 전용 diff --git a/frontend/src/lib/utils/genome-analysis-config.ts b/frontend/src/lib/utils/genome-analysis-config.ts index c165bea..34e8cf8 100644 --- a/frontend/src/lib/utils/genome-analysis-config.ts +++ b/frontend/src/lib/utils/genome-analysis-config.ts @@ -5,13 +5,13 @@ * 유전체 분석 데이터가 유효한지 판단하는 조건 정의 * 백엔드 GenomeAnalysisConfig.ts와 동일한 로직 유지 * - * =================유효 조건======================= + * ====================유효 조건==================== * 1. chipSireName === '일치' (아비 칩 데이터 일치) * 2. chipDamName !== '불일치' (어미 칩 데이터 불일치가 아님) * 3. chipDamName !== '이력제부재' (어미 이력제 부재가 아님) * 4. cowId가 EXCLUDED_COW_IDS에 포함되지 않음 * - * 제외되는 경우: + * ====================제외되는 경우==================== * - chipSireName !== '일치' (아비 불일치, 이력제부재 등) * - chipDamName === '불일치' (어미 불일치) * - chipDamName === '이력제부재' (어미 이력제 부재) @@ -29,6 +29,17 @@ export const EXCLUDED_COW_IDS = [ 'KOR002191642861', // 모근량 갯수 적은데 1회분은 가능 아비명도 일치하는데 유전체 분석 정보가 없음 ]; +/** + * 분석불가 개체인지 확인 (모근 오염/불량 등 특수 사유) + * + * @param cowId - 개체식별번호 + * @returns 분석불가 개체 여부 + */ +export function isExcludedCow(cowId?: string | null): boolean { + if (!cowId) return false; + return EXCLUDED_COW_IDS.includes(cowId); +} + /** * 유전체 분석 데이터 유효성 검사 * @@ -54,9 +65,15 @@ export function isValidGenomeAnalysis( chipDamName: string | null | undefined, cowId?: string | null, ): boolean { - // 개별 제외 개체만 확인 (부/모 불일치여도 유전자 데이터 있으면 표시) + // 1. 분석불가 개체 (모근 오염/불량 등 특수 사유) if (cowId && EXCLUDED_COW_IDS.includes(cowId)) return false; + // 2. 아비명 일치 필수 + if (chipSireName !== VALID_CHIP_SIRE_NAME) return false; + + // 3. 어미명 불일치/이력제부재 제외 + if (chipDamName && INVALID_CHIP_DAM_NAMES.includes(chipDamName)) return false; + return true; } @@ -73,18 +90,23 @@ export function getInvalidReason( chipDamName: string | null | undefined, cowId?: string | null, ): string | null { - if (chipSireName !== VALID_CHIP_SIRE_NAME) { - if (!chipSireName) return '친자확인 정보 없음'; - return '부 KPN 친자 불일치'; - } - - if (chipDamName && INVALID_CHIP_DAM_NAMES.includes(chipDamName)) { - if (chipDamName === '이력제부재') return '모 이력제 부재'; - return '모 친자 불일치'; - } - + // 1. 개별 제외 개체 if (cowId && EXCLUDED_COW_IDS.includes(cowId)) { - return '분석 불가 개체'; + return '분석불가'; + } + + // 2. 아비명 상태별 분류 + if (chipSireName !== VALID_CHIP_SIRE_NAME) { + if (chipSireName === '분석불가') return '분석불가'; + if (chipSireName === '정보없음') return '분석불가'; + if (!chipSireName) return null; // null은 '-' 표시 + return '부 불일치'; // 불일치 등 + } + + // 3. 어미명 상태별 분류 + if (chipDamName && INVALID_CHIP_DAM_NAMES.includes(chipDamName)) { + if (chipDamName === '이력제부재') return '모 이력제부재'; + return '모 불일치'; } return null; @@ -103,19 +125,24 @@ export function getInvalidMessage( chipDamName: string | null | undefined, cowId?: string | null, ): string { + // 1. 개별 제외 개체 + if (cowId && EXCLUDED_COW_IDS.includes(cowId)) { + return '모근 오염 및 불량 등 기타 사유로 유전체 분석 보고서를 제공할 수 없습니다.'; + } + + // 2. 아비명 상태별 분류 if (chipSireName !== VALID_CHIP_SIRE_NAME) { - if (!chipSireName) return '친자확인 정보가 없어 유전체 분석 보고서를 제공할 수 없습니다.'; + if (chipSireName === '분석불가') return '모근 오염 및 불량 등 기타 사유로 유전체 분석 보고서를 제공할 수 없습니다.'; + if (chipSireName === '정보없음') return '개체 식별번호 및 형식오류로 유전체 분석 보고서를 제공할 수 없습니다.'; + if (!chipSireName) return ''; // null은 '-' 표시 return '부 친자감별 결과가 불일치하여 유전체 분석 보고서를 제공할 수 없습니다.'; } + // 3. 어미명 상태별 분류 if (chipDamName && INVALID_CHIP_DAM_NAMES.includes(chipDamName)) { if (chipDamName === '이력제부재') return '모 이력제 정보가 부재하여 유전체 분석 보고서를 제공할 수 없습니다.'; return '모 친자감별 결과가 불일치하여 유전체 분석 보고서를 제공할 수 없습니다.'; } - if (cowId && EXCLUDED_COW_IDS.includes(cowId)) { - return '해당 개체는 분석 불가 사유로 인해 유전체 분석 보고서를 제공할 수 없습니다.'; - } - return '유전체 분석 보고서를 제공할 수 없습니다.'; } diff --git a/frontend/src/types/cow.types.ts b/frontend/src/types/cow.types.ts index 7429525..2d27ebc 100644 --- a/frontend/src/types/cow.types.ts +++ b/frontend/src/types/cow.types.ts @@ -49,6 +49,12 @@ export interface CowDetailResponseDto extends CowDto { totalCows?: number; // 농장 총 개체 수 inbreedingCoef?: number; // 근친계수 (0.0~1.0) calvingCount?: number; // 분만회차 + + // 데이터 상태 (백엔드에서 조회) + dataStatus?: { + hasGenomeData: boolean; // 유전체 데이터 존재 여부 + hasGeneData: boolean; // 유전자 데이터 존재 여부 + }; } /** diff --git a/frontend/src/types/reprompt.types.ts b/frontend/src/types/mpt.types.ts similarity index 97% rename from frontend/src/types/reprompt.types.ts rename to frontend/src/types/mpt.types.ts index bdc3b26..4f0b502 100644 --- a/frontend/src/types/reprompt.types.ts +++ b/frontend/src/types/mpt.types.ts @@ -41,7 +41,7 @@ export interface MptDto extends BaseFields { phosphorus?: number; // 인 (mg/dL) caPRatio?: number; // 칼슘/인 비율 magnesium?: number; // 마그네슘 (mg/dL) - creatinine?: number; // 크레아틴 (mg/dL) + creatine?: number; // 크레아틴 (mg/dL) delDt?: string; // 삭제일시 (Soft Delete) } @@ -98,7 +98,7 @@ export interface ReproMpt { phosphorus?: number; caPRatio?: number; magnesium?: number; - creatinine?: number; + creatine?: number; reproMptNote?: string; regDt?: string;