From 6731eec8028085205c72d0ea674df77bb48dbce9 Mon Sep 17 00:00:00 2001 From: chu eun ju Date: Wed, 10 Dec 2025 12:02:40 +0900 Subject: [PATCH] =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20=ED=99=94?= =?UTF-8?q?=EB=A9=B4=20=EC=88=98=EC=A0=95=20=EB=B0=8F=20dockerfile=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/gene/dto | 0 .../src/gene/entities/gene-detail.entity.ts | 113 ++++ backend/src/gene/gene.controller.ts | 61 +- backend/src/gene/gene.module.ts | 7 + backend/src/gene/gene.service.ts | 129 +++- backend/src/genome/dto | 0 frontend/Dockerfile | 1 + frontend/next.config.ts | 2 +- frontend/src/app/cow/[cowNo]/page.tsx | 636 +++++++++++++++--- frontend/src/lib/api/gene.api.ts | 96 ++- frontend/src/lib/api/index.ts | 1 + 11 files changed, 931 insertions(+), 115 deletions(-) create mode 100644 backend/src/gene/dto create mode 100644 backend/src/gene/entities/gene-detail.entity.ts create mode 100644 backend/src/genome/dto diff --git a/backend/src/gene/dto b/backend/src/gene/dto new file mode 100644 index 0000000..e69de29 diff --git a/backend/src/gene/entities/gene-detail.entity.ts b/backend/src/gene/entities/gene-detail.entity.ts new file mode 100644 index 0000000..89da031 --- /dev/null +++ b/backend/src/gene/entities/gene-detail.entity.ts @@ -0,0 +1,113 @@ +import { BaseModel } from 'src/common/entities/base.entity'; +import { GenomeRequestModel } from 'src/genome/entities/genome-request.entity'; +import { + Column, + Entity, + Index, + JoinColumn, + ManyToOne, + PrimaryGeneratedColumn, +} from 'typeorm'; + +/** + * 유전자 상세 정보 (tb_gene_detail) + * 1개체당 N개 SNP 마커 결과 저장 + * 예: KOR002108023350, 1:110900379, 1, 110900379, [A/T], A, A, 설명 + */ +@Entity({ name: 'tb_gene_detail' }) +@Index('idx_gene_detail_cow_id', ['cowId']) +@Index('idx_gene_detail_snp_name', ['snpName']) +@Index('idx_gene_detail_cow_snp', ['cowId', 'snpName']) +export class GeneDetailModel extends BaseModel { + @PrimaryGeneratedColumn({ + name: 'pk_gene_detail_no', + type: 'int', + comment: '유전자상세번호 PK', + }) + pkGeneDetailNo: number; + + @Column({ + name: 'fk_request_no', + type: 'int', + nullable: true, + comment: '의뢰번호 FK', + }) + fkRequestNo: number; + + @Column({ + name: 'cow_id', + type: 'varchar', + length: 20, + nullable: true, + comment: '개체식별번호 (KOR)', + }) + cowId: string; + + @Column({ + name: 'snp_name', + type: 'varchar', + length: 100, + nullable: true, + comment: 'SNP 이름 (예: 1:110900379)', + }) + snpName: string; + + @Column({ + name: 'chromosome', + type: 'varchar', + length: 10, + nullable: true, + comment: '염색체 위치 (Chr)', + }) + chromosome: string; + + @Column({ + name: 'position', + type: 'varchar', + length: 20, + nullable: true, + comment: '위치 (Position)', + }) + position: string; + + @Column({ + name: 'snp_type', + type: 'varchar', + length: 20, + nullable: true, + comment: 'SNP 구분 (예: [A/T])', + }) + snpType: string; + + @Column({ + name: 'allele1', + type: 'varchar', + length: 10, + nullable: true, + comment: '첫번째 대립유전자 (Allele1...Top)', + }) + allele1: string; + + @Column({ + name: 'allele2', + type: 'varchar', + length: 10, + nullable: true, + comment: '두번째 대립유전자 (Allele2...Top)', + }) + allele2: string; + + @Column({ + name: 'remarks', + type: 'varchar', + length: 500, + nullable: true, + comment: '비고 (설명)', + }) + remarks: string; + + // Relations + @ManyToOne(() => GenomeRequestModel, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'fk_request_no' }) + genomeRequest: GenomeRequestModel; +} diff --git a/backend/src/gene/gene.controller.ts b/backend/src/gene/gene.controller.ts index 7dd0dd4..a46b7a6 100644 --- a/backend/src/gene/gene.controller.ts +++ b/backend/src/gene/gene.controller.ts @@ -1,7 +1,66 @@ -import { Controller } from '@nestjs/common'; +import { Controller, Get, Param, Post, Body } from '@nestjs/common'; import { GeneService } from './gene.service'; +import { GeneDetailModel } from './entities/gene-detail.entity'; @Controller('gene') export class GeneController { constructor(private readonly geneService: GeneService) {} + + /** + * 개체식별번호로 유전자 상세 정보 조회 + * GET /gene/:cowId + */ + @Get(':cowId') + async findByCowId(@Param('cowId') cowId: string): Promise { + return this.geneService.findByCowId(cowId); + } + + /** + * 개체별 유전자 요약 정보 조회 + * GET /gene/summary/:cowId + */ + @Get('summary/:cowId') + async getGeneSummary(@Param('cowId') cowId: string): Promise<{ + total: number; + homozygousCount: number; + heterozygousCount: number; + }> { + return this.geneService.getGeneSummary(cowId); + } + + /** + * 의뢰번호로 유전자 상세 정보 조회 + * GET /gene/request/:requestNo + */ + @Get('request/:requestNo') + async findByRequestNo(@Param('requestNo') requestNo: number): Promise { + return this.geneService.findByRequestNo(requestNo); + } + + /** + * 유전자 상세 정보 단건 조회 + * GET /gene/detail/:geneDetailNo + */ + @Get('detail/:geneDetailNo') + async findOne(@Param('geneDetailNo') geneDetailNo: number): Promise { + return this.geneService.findOne(geneDetailNo); + } + + /** + * 유전자 상세 정보 생성 + * POST /gene + */ + @Post() + async create(@Body() data: Partial): Promise { + return this.geneService.create(data); + } + + /** + * 유전자 상세 정보 일괄 생성 + * POST /gene/bulk + */ + @Post('bulk') + async createBulk(@Body() dataList: Partial[]): Promise { + return this.geneService.createBulk(dataList); + } } diff --git a/backend/src/gene/gene.module.ts b/backend/src/gene/gene.module.ts index ad8b5b1..e616ef7 100644 --- a/backend/src/gene/gene.module.ts +++ b/backend/src/gene/gene.module.ts @@ -1,9 +1,16 @@ import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; import { GeneService } from './gene.service'; import { GeneController } from './gene.controller'; +import { GeneDetailModel } from './entities/gene-detail.entity'; +import { GenomeRequestModel } from '../genome/entities/genome-request.entity'; @Module({ + imports: [ + TypeOrmModule.forFeature([GeneDetailModel, GenomeRequestModel]), + ], controllers: [GeneController], providers: [GeneService], + exports: [GeneService], }) export class GeneModule {} diff --git a/backend/src/gene/gene.service.ts b/backend/src/gene/gene.service.ts index 8e06e18..9bd7ed8 100644 --- a/backend/src/gene/gene.service.ts +++ b/backend/src/gene/gene.service.ts @@ -1,4 +1,129 @@ -import { Injectable } from '@nestjs/common'; +import { Injectable, NotFoundException } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { IsNull, Repository } from 'typeorm'; +import { GeneDetailModel } from './entities/gene-detail.entity'; +import { GenomeRequestModel } from '../genome/entities/genome-request.entity'; @Injectable() -export class GeneService {} +export class GeneService { + constructor( + @InjectRepository(GeneDetailModel) + private readonly geneDetailRepository: Repository, + @InjectRepository(GenomeRequestModel) + private readonly genomeRequestRepository: Repository, + ) {} + + /** + * 개체식별번호(cowId)로 유전자 상세 정보 조회 + * @param cowId 개체식별번호 (KOR...) + * @returns 유전자 상세 정보 배열 + */ + async findByCowId(cowId: string): Promise { + const results = await this.geneDetailRepository.find({ + where: { + cowId, + delDt: IsNull(), + }, + relations: ['genomeRequest'], + order: { + chromosome: 'ASC', + position: 'ASC', + }, + }); + + return results; + } + + /** + * 의뢰번호(requestNo)로 유전자 상세 정보 조회 + * @param requestNo 의뢰번호 + * @returns 유전자 상세 정보 배열 + */ + async findByRequestNo(requestNo: number): Promise { + const results = await this.geneDetailRepository.find({ + where: { + fkRequestNo: requestNo, + delDt: IsNull(), + }, + order: { + chromosome: 'ASC', + position: 'ASC', + }, + }); + + return results; + } + + /** + * 개체별 유전자 요약 정보 조회 + * @param cowId 개체식별번호 + * @returns 동형접합/이형접합 개수 요약 + */ + async getGeneSummary(cowId: string): Promise<{ + total: number; + homozygousCount: number; + heterozygousCount: number; + }> { + const geneDetails = await this.findByCowId(cowId); + + let homozygousCount = 0; + let heterozygousCount = 0; + + geneDetails.forEach((gene) => { + if (gene.allele1 && gene.allele2) { + if (gene.allele1 === gene.allele2) { + homozygousCount++; + } else { + heterozygousCount++; + } + } + }); + + return { + total: geneDetails.length, + homozygousCount, + heterozygousCount, + }; + } + + /** + * 유전자 상세 정보 단건 조회 + * @param geneDetailNo 유전자상세번호 + * @returns 유전자 상세 정보 + */ + async findOne(geneDetailNo: number): Promise { + const result = await this.geneDetailRepository.findOne({ + where: { + pkGeneDetailNo: geneDetailNo, + delDt: IsNull(), + }, + relations: ['genomeRequest'], + }); + + if (!result) { + throw new NotFoundException(`유전자 상세 정보를 찾을 수 없습니다. (geneDetailNo: ${geneDetailNo})`); + } + + return result; + } + + /** + * 유전자 상세 정보 생성 + * @param data 생성할 데이터 + * @returns 생성된 유전자 상세 정보 + */ + async create(data: Partial): Promise { + const geneDetail = this.geneDetailRepository.create(data); + return await this.geneDetailRepository.save(geneDetail); + } + + /** + * 유전자 상세 정보 일괄 생성 + * @param dataList 생성할 데이터 배열 + * @returns 생성된 유전자 상세 정보 배열 + */ + async createBulk(dataList: Partial[]): Promise { + const geneDetails = this.geneDetailRepository.create(dataList); + return await this.geneDetailRepository.save(geneDetails); + } +} diff --git a/backend/src/genome/dto b/backend/src/genome/dto new file mode 100644 index 0000000..e69de29 diff --git a/frontend/Dockerfile b/frontend/Dockerfile index 3fd053b..3185e3c 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -17,6 +17,7 @@ COPY . . # 빌드 시 필요한 환경 변수 설정 ENV NEXT_TELEMETRY_DISABLED=1 ENV NODE_ENV=production +ENV NEXT_PUBLIC_API_URL=/backend/api # Next.js 빌드 RUN npm run build diff --git a/frontend/next.config.ts b/frontend/next.config.ts index 38babdf..9f2ccf9 100644 --- a/frontend/next.config.ts +++ b/frontend/next.config.ts @@ -13,7 +13,7 @@ const nextConfig: NextConfig = { return [ { source: '/backend/api/:path*', // /api가 붙은 모든 요청 - destination: 'http://backend:4000/:path*', // 백엔드 API로 요청 + destination: 'http://192.168.11.249:4000/:path*', // 백엔드 API로 요청 }, ]; }, diff --git a/frontend/src/app/cow/[cowNo]/page.tsx b/frontend/src/app/cow/[cowNo]/page.tsx index b4c806d..a97c604 100644 --- a/frontend/src/app/cow/[cowNo]/page.tsx +++ b/frontend/src/app/cow/[cowNo]/page.tsx @@ -10,7 +10,7 @@ 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 { ComparisonAveragesDto, TraitComparisonAveragesDto, cowApi, genomeApi } from "@/lib/api" +import { ComparisonAveragesDto, TraitComparisonAveragesDto, cowApi, genomeApi, geneApi, GeneDetail } from "@/lib/api" import { CowDetail } from "@/types/cow.types" import { GenomeTrait } from "@/types/genome.types" import { useGlobalFilter } from "@/contexts/GlobalFilterContext" @@ -23,7 +23,10 @@ import { Activity, X, XCircle, + Search, } from 'lucide-react' +import { Input } from "@/components/ui/input" +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" import { useEffect, useMemo, useRef, useState } from 'react' import { CategoryEvaluationCard } from "./genome/_components/category-evaluation-card" import { NormalDistributionChart } from "./genome/_components/normal-distribution-chart" @@ -144,7 +147,7 @@ export default function CowOverviewPage() { const [cow, setCow] = useState(null) const [genomeData, setGenomeData] = useState([]) - const [geneData, setGeneData] = useState([]) + const [geneData, setGeneData] = useState([]) const [loading, setLoading] = useState(true) const [activeTab, setActiveTab] = useState('genome') @@ -193,6 +196,13 @@ export default function CowOverviewPage() { // 차트 형질 필터 (전체 선발지수 또는 개별 형질) const [chartFilterTrait, setChartFilterTrait] = useState('overall') + // 유전자 탭 필터 상태 + const [geneSearchKeyword, setGeneSearchKeyword] = useState('') + const [geneTypeFilter, setGeneTypeFilter] = useState<'all' | 'QTY' | 'QLT'>('all') + const [genotypeFilter, setGenotypeFilter] = useState<'all' | 'homozygous' | 'heterozygous'>('all') + const [geneSortBy, setGeneSortBy] = useState<'snpName' | 'chromosome' | 'position' | 'genotype'>('snpName') + const [geneSortOrder, setGeneSortOrder] = useState<'asc' | 'desc'>('asc') + // 농가/보은군 배지 클릭 시 차트로 스크롤 + 하이라이트 const handleComparisonClick = (mode: 'farm' | 'region') => { // 토글: 같은 모드 클릭 시 해제 @@ -236,10 +246,17 @@ export default function CowOverviewPage() { const genomeExists = genomeDataResult.length > 0 && !!genomeDataResult[0].genomeCows && genomeDataResult[0].genomeCows.length > 0 setHasGenomeData(genomeExists) - // 유전자(SNP) 데이터 가져오기 (gene.api 제거됨 - 추후 백엔드 구현 시 복구) - // TODO: gene API 구현 후 복구 - setGeneData([]) - setHasGeneData(false) + // 유전자(SNP) 데이터 가져오기 + try { + const geneDataResult = await geneApi.findByCowId(cowNo) + setGeneData(geneDataResult) + setHasGeneData(geneDataResult.length > 0) + } catch (geneErr) { + console.error('유전자 데이터 조회 실패:', geneErr) + setGeneData([]) + // UI 확인을 위해 임시로 true 설정 (데이터 없어도 UI는 보여줌) + setHasGeneData(true) + } // 번식능력 데이터 (현재는 목업 - 추후 API 연동) // TODO: 번식능력 API 연동 @@ -481,9 +498,6 @@ export default function CowOverviewPage() { > 유전자 - - {hasGeneData ? '완료' : '미검사'} - 번식능력 - - {hasReproductionData ? '완료' : '미검사'} - @@ -899,90 +910,533 @@ export default function CowOverviewPage() { {hasGeneData ? ( <> -

유전자(SNP) 분석 결과

+ {/* 개체 정보 섹션 (유전체 탭과 동일) */} +

개체 정보

- {/* 유전자 타입별 요약 */} -
- - -
-
- -
-
-

육량형 유전자

-

체중, 성장 관련

-
-
-
- {geneData.filter(g => g.snpInfo?.markerSnps?.some((ms: any) => ms.marker?.markerTypeCd === 'QTY')).slice(0, 5).map((gene, idx) => ( -
- {gene.snpInfo?.markerSnps?.[0]?.marker?.markerNm || gene.snpInfo?.snpNm} - {gene.genotype || `${gene.allele1Top}${gene.allele2Top}`} -
- ))} -
-
-
- - - -
-
- -
-
-

육질형 유전자

-

근내지방, 연도 관련

-
-
-
- {geneData.filter(g => g.snpInfo?.markerSnps?.some((ms: any) => ms.marker?.markerTypeCd === 'QLT')).slice(0, 5).map((gene, idx) => ( -
- {gene.snpInfo?.markerSnps?.[0]?.marker?.markerNm || gene.snpInfo?.snpNm} - {gene.genotype || `${gene.allele1Top}${gene.allele2Top}`} -
- ))} -
-
-
-
- - {/* 전체 유전자 목록 */} -

전체 유전자 목록

- + -
- - - - - - - - - - - {geneData.slice(0, 50).map((gene, idx) => ( - - - - - - - ))} - -
SNP마커염색체유전자형
{gene.snpInfo?.snpNm || '-'}{gene.snpInfo?.markerSnps?.[0]?.marker?.markerNm || '-'}{gene.snpInfo?.chr || '-'} - {gene.genotype || `${gene.allele1Top || ''}${gene.allele2Top || ''}`} -
-
- {geneData.length > 50 && ( -
- 전체 {geneData.length}개 중 50개 표시 + {/* 데스크탑: 가로 그리드 */} +
+
+
+ 개체번호 +
+
+ +
- )} +
+
+ 생년월일 +
+
+ + {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))}개월` + : '-'} + +
+
+
+
+ 유전자 분석일자 +
+
+ + {genomeData[0]?.request?.chipReportDt + ? new Date(genomeData[0].request.chipReportDt).toLocaleDateString('ko-KR') + : '-'} + +
+
+
+ {/* 모바일: 좌우 배치 리스트 */} +
+
+ 개체번호 +
+ +
+
+
+ 생년월일 + + {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))}개월` + : '-'} + +
+
+ 분석일자 + + {genomeData[0]?.request?.chipReportDt + ? new Date(genomeData[0].request.chipReportDt).toLocaleDateString('ko-KR') + : '-'} + +
+
+ + {/* 친자확인 결과 섹션 (유전체 탭과 동일) */} +

친자확인 결과

+ + + + {/* 데스크탑: 가로 그리드 */} +
+
+
+ 부 KPN번호 +
+
+ + {cow?.sireKpn || '-'} + + {(() => { + const chipSireName = genomeData[0]?.request?.chipSireName + if (chipSireName === '일치') { + return ( + + + 일치 + + ) + } else if (chipSireName && chipSireName !== '일치') { + return ( + + + 불일치 + + ) + } else { + return ( + + 정보없음 + + ) + } + })()} +
+
+
+
+ 모 개체번호 +
+
+ {cow?.damCowId ? ( + + ) : ( + - + )} + {(() => { + const chipDamName = genomeData[0]?.request?.chipDamName + if (chipDamName === '일치') { + return ( + + + 일치 + + ) + } else if (chipDamName === '불일치') { + return ( + + + 불일치 + + ) + } else if (chipDamName === '이력제부재') { + return ( + + + 이력제부재 + + ) + } else { + return null + } + })()} +
+
+
+ {/* 모바일: 좌우 배치 리스트 */} +
+
+ 부 KPN번호 +
+ + {cow?.sireKpn || '-'} + + {(() => { + const chipSireName = genomeData[0]?.request?.chipSireName + if (chipSireName === '일치') { + return ( + + + 일치 + + ) + } else if (chipSireName && chipSireName !== '일치') { + return ( + + + 불일치 + + ) + } else { + return ( + + 정보없음 + + ) + } + })()} +
+
+
+ 모 개체번호 +
+ {cow?.damCowId ? ( + + ) : ( + - + )} + {(() => { + const chipDamName = genomeData[0]?.request?.chipDamName + if (chipDamName === '일치') { + return ( + + + 일치 + + ) + } else if (chipDamName === '불일치') { + return ( + + + 불일치 + + ) + } else if (chipDamName === '이력제부재') { + return ( + + + 이력제부재 + + ) + } else { + return null + } + })()} +
+
+
+
+
+ + {/* 유전자 검색 및 필터 섹션 */} +

유전자 분석 결과

+ +
+ {/* 검색창 */} +
+ + setGeneSearchKeyword(e.target.value)} + /> +
+ + {/* 필터 옵션들 */} +
+ {/* 유전자 타입 필터 */} +
+ 구분: +
+ + + +
+
+ + {/* 유전자형 필터 */} +
+ 유전자형: +
+ + + +
+
+ + {/* 정렬 드롭다운 */} +
+ + +
+
+
+ + {/* 유전자 테이블/카드 */} + {(() => { + const filteredData = geneData.filter(gene => { + // 검색 필터 + if (geneSearchKeyword) { + const keyword = geneSearchKeyword.toLowerCase() + const snpName = (gene.snpName || '').toLowerCase() + const chromosome = (gene.chromosome || '').toLowerCase() + const position = (gene.position || '').toLowerCase() + if (!snpName.includes(keyword) && !chromosome.includes(keyword) && !position.includes(keyword)) { + return false + } + } + // 유전자형 필터 + if (genotypeFilter !== 'all') { + const isHomozygous = gene.allele1 === gene.allele2 + if (genotypeFilter === 'homozygous' && !isHomozygous) return false + if (genotypeFilter === 'heterozygous' && isHomozygous) return false + } + return true + }) + + // 정렬 + const sortedData = [...filteredData].sort((a, b) => { + let aVal: string | number = '' + let bVal: string | number = '' + + switch (geneSortBy) { + case 'snpName': + aVal = a.snpName || '' + bVal = b.snpName || '' + break + case 'chromosome': + aVal = parseInt(a.chromosome || '0') || 0 + bVal = parseInt(b.chromosome || '0') || 0 + break + case 'position': + aVal = parseInt(a.position || '0') || 0 + bVal = parseInt(b.position || '0') || 0 + break + case 'genotype': + aVal = `${a.allele1 || ''}${a.allele2 || ''}` + bVal = `${b.allele1 || ''}${b.allele2 || ''}` + break + } + + if (typeof aVal === 'number' && typeof bVal === 'number') { + return geneSortOrder === 'asc' ? aVal - bVal : bVal - aVal + } + + const strA = String(aVal) + const strB = String(bVal) + return geneSortOrder === 'asc' + ? strA.localeCompare(strB) + : strB.localeCompare(strA) + }) + + const displayData = sortedData.length > 0 + ? sortedData.slice(0, 50) + : Array(10).fill(null) + + return ( + <> + {/* 데스크톱: 테이블 */} + + +
+ + + + + + + + + + + + + + {displayData.map((gene, idx) => { + if (!gene) { + return ( + + + + + + + + + + ) + } + return ( + + + + + + + + + + ) + })} + +
SNP 이름염색체 위치PositionSNP 구분첫번째 대립유전자두번째 대립유전자설명
-------
{gene.snpName || '-'}{gene.chromosome || '-'}{gene.position || '-'}{gene.snpType || '-'}{gene.allele1 || '-'}{gene.allele2 || '-'}{gene.remarks || '-'}
+
+ {geneData.length > 50 && ( +
+ 전체 {geneData.length}개 중 50개 표시 +
+ )} +
+
+ + {/* 모바일: 카드 뷰 */} +
+ {displayData.map((gene, idx) => { + return ( + + +
+ SNP 이름 + {gene?.snpName || '-'} +
+
+ 염색체 위치 + {gene?.chromosome || '-'} +
+
+ Position + {gene?.position || '-'} +
+
+ SNP 구분 + {gene?.snpType || '-'} +
+
+ 첫번째 대립유전자 + {gene?.allele1 || '-'} +
+
+ 두번째 대립유전자 + {gene?.allele2 || '-'} +
+
+ 설명 + {gene?.remarks || '-'} +
+
+
+ ) + })} + {geneData.length > 50 && ( +
+ 전체 {geneData.length}개 중 50개 표시 +
+ )} +
+ + ) + })()} ) : ( diff --git a/frontend/src/lib/api/gene.api.ts b/frontend/src/lib/api/gene.api.ts index fd354a4..2ef7064 100644 --- a/frontend/src/lib/api/gene.api.ts +++ b/frontend/src/lib/api/gene.api.ts @@ -1,40 +1,96 @@ /** - * Gene API (임시 Mock) - * TODO: 백엔드 구현 후 실제 API로 교체 + * Gene API + * 유전자(SNP) 분석 결과 조회 API */ import apiClient from "../api-client"; -export interface MarkerModel { - markerNm: string; - markerTypeCd: string; // 'QTY' | 'QLT' - markerDesc?: string; - relatedTrait?: string; - favorableAllele?: string; +/** + * 유전자 상세 정보 타입 + */ +export interface GeneDetail { + pkGeneDetailNo: number; + fkRequestNo: number | null; + cowId: string | null; + snpName: string | null; + chromosome: string | null; + position: string | null; + snpType: string | null; + allele1: string | null; + allele2: string | null; + remarks: string | null; + regDt?: string; + updtDt?: string; + genomeRequest?: { + pkRequestNo: number; + requestDt: string | null; + chipReportDt: string | null; + chipSireName: string | null; + chipDamName: string | null; + }; +} + +/** + * 유전자 요약 정보 타입 + */ +export interface GeneSummary { + total: number; + homozygousCount: number; + heterozygousCount: number; } export const geneApi = { /** - * 전체 마커 목록 조회 (임시 빈 배열 반환) + * 개체식별번호로 유전자 상세 정보 조회 + * GET /gene/:cowId */ - getAllMarkers: async (): Promise => { - // TODO: 백엔드 구현 후 실제 API 연동 - return []; + findByCowId: async (cowId: string): Promise => { + const response = await apiClient.get(`/gene/${cowId}`); + return response.data; }, /** - * 타입별 마커 목록 조회 (임시 빈 배열 반환) + * 개체별 유전자 요약 정보 조회 + * GET /gene/summary/:cowId */ - getGenesByType: async (_typeCd: string): Promise => { - // TODO: 백엔드 구현 후 실제 API 연동 - return []; + getGeneSummary: async (cowId: string): Promise => { + const response = await apiClient.get(`/gene/summary/${cowId}`); + return response.data; }, /** - * 개체별 유전자(SNP) 데이터 조회 (임시 빈 배열 반환) + * 의뢰번호로 유전자 상세 정보 조회 + * GET /gene/request/:requestNo */ - findByCowNo: async (_cowNo: string | number): Promise => { - // TODO: 백엔드 구현 후 실제 API 연동 - return []; + findByRequestNo: async (requestNo: number): Promise => { + const response = await apiClient.get(`/gene/request/${requestNo}`); + return response.data; + }, + + /** + * 유전자 상세 정보 단건 조회 + * GET /gene/detail/:geneDetailNo + */ + findOne: async (geneDetailNo: number): Promise => { + const response = await apiClient.get(`/gene/detail/${geneDetailNo}`); + return response.data; + }, + + /** + * 유전자 상세 정보 생성 + * POST /gene + */ + create: async (data: Partial): Promise => { + const response = await apiClient.post('/gene', data); + return response.data; + }, + + /** + * 유전자 상세 정보 일괄 생성 + * POST /gene/bulk + */ + createBulk: async (dataList: Partial[]): Promise => { + const response = await apiClient.post('/gene/bulk', dataList); + return response.data; }, }; diff --git a/frontend/src/lib/api/index.ts b/frontend/src/lib/api/index.ts index d6e8498..403934c 100644 --- a/frontend/src/lib/api/index.ts +++ b/frontend/src/lib/api/index.ts @@ -13,6 +13,7 @@ export { authApi } from './auth.api'; // 인증 API export { cowApi } from './cow.api'; export { dashboardApi } from './dashboard.api'; export { farmApi } from './farm.api'; +export { geneApi, type GeneDetail, type GeneSummary } from './gene.api'; export { genomeApi, type ComparisonAveragesDto, type CategoryAverageDto, type FarmTraitComparisonDto, type TraitComparisonAveragesDto, type TraitAverageDto } from './genome.api'; export { reproApi } from './repro.api'; export { breedApi } from './breed.api';