INIT
This commit is contained in:
192
backend/src/genome/entities/genome-request.entity.ts
Normal file
192
backend/src/genome/entities/genome-request.entity.ts
Normal file
@@ -0,0 +1,192 @@
|
||||
import { BaseModel } from 'src/common/entities/base.entity';
|
||||
import { CowModel } from 'src/cow/entities/cow.entity';
|
||||
import { FarmModel } from 'src/farm/entities/farm.entity';
|
||||
import {
|
||||
Column,
|
||||
Entity,
|
||||
JoinColumn,
|
||||
ManyToOne,
|
||||
PrimaryGeneratedColumn,
|
||||
} from 'typeorm';
|
||||
|
||||
/**
|
||||
* 유전체 분석 의뢰 (tb_genome_request)
|
||||
* 1개체 N의뢰 관계
|
||||
*/
|
||||
@Entity({ name: 'tb_genome_request' })
|
||||
export class GenomeRequestModel extends BaseModel {
|
||||
@PrimaryGeneratedColumn({
|
||||
name: 'pk_request_no',
|
||||
type: 'int',
|
||||
comment: 'No (PK)',
|
||||
})
|
||||
pkRequestNo: number;
|
||||
|
||||
@Column({
|
||||
name: 'fk_farm_no',
|
||||
type: 'int',
|
||||
nullable: true,
|
||||
comment: '농장번호 FK',
|
||||
})
|
||||
fkFarmNo: number;
|
||||
|
||||
@Column({
|
||||
name: 'fk_cow_no',
|
||||
type: 'int',
|
||||
nullable: true,
|
||||
comment: '개체번호 FK',
|
||||
})
|
||||
fkCowNo: number;
|
||||
|
||||
@Column({
|
||||
name: 'cow_remarks',
|
||||
type: 'varchar',
|
||||
length: 500,
|
||||
nullable: true,
|
||||
comment: '개체 비고',
|
||||
})
|
||||
cowRemarks: string;
|
||||
|
||||
@Column({
|
||||
name: 'request_dt',
|
||||
type: 'date',
|
||||
nullable: true,
|
||||
comment: '접수일자',
|
||||
})
|
||||
requestDt: Date;
|
||||
|
||||
@Column({
|
||||
name: 'snp_test',
|
||||
type: 'varchar',
|
||||
length: 10,
|
||||
nullable: true,
|
||||
comment: 'SNP 검사',
|
||||
})
|
||||
snpTest: string;
|
||||
|
||||
@Column({
|
||||
name: 'ms_test',
|
||||
type: 'varchar',
|
||||
length: 10,
|
||||
nullable: true,
|
||||
comment: 'MS 검사',
|
||||
})
|
||||
msTest: string;
|
||||
|
||||
@Column({
|
||||
name: 'sample_amount',
|
||||
type: 'varchar',
|
||||
length: 50,
|
||||
nullable: true,
|
||||
comment: '모근량',
|
||||
})
|
||||
sampleAmount: string;
|
||||
|
||||
@Column({
|
||||
name: 'sample_remarks',
|
||||
type: 'varchar',
|
||||
length: 500,
|
||||
nullable: true,
|
||||
comment: '모근 비고',
|
||||
})
|
||||
sampleRemarks: string;
|
||||
|
||||
// 칩 분석 정보
|
||||
@Column({
|
||||
name: 'chip_no',
|
||||
type: 'varchar',
|
||||
length: 50,
|
||||
nullable: true,
|
||||
comment: '분석 Chip 번호',
|
||||
})
|
||||
chipNo: string;
|
||||
|
||||
@Column({
|
||||
name: 'chip_type',
|
||||
type: 'varchar',
|
||||
length: 50,
|
||||
nullable: true,
|
||||
comment: '분석 칩 종류',
|
||||
})
|
||||
chipType: string;
|
||||
|
||||
@Column({
|
||||
name: 'chip_info',
|
||||
type: 'varchar',
|
||||
length: 200,
|
||||
nullable: true,
|
||||
comment: '칩정보',
|
||||
})
|
||||
chipInfo: string;
|
||||
|
||||
@Column({
|
||||
name: 'chip_remarks',
|
||||
type: 'varchar',
|
||||
length: 500,
|
||||
nullable: true,
|
||||
comment: '칩 비고',
|
||||
})
|
||||
chipRemarks: string;
|
||||
|
||||
@Column({
|
||||
name: 'chip_sire_name',
|
||||
type: 'varchar',
|
||||
length: 100,
|
||||
nullable: true,
|
||||
comment: '칩분석 아비명',
|
||||
})
|
||||
chipSireName: string;
|
||||
|
||||
@Column({
|
||||
name: 'chip_dam_name',
|
||||
type: 'varchar',
|
||||
length: 100,
|
||||
nullable: true,
|
||||
comment: '칩분석 어미명',
|
||||
})
|
||||
chipDamName: string;
|
||||
|
||||
@Column({
|
||||
name: 'chip_report_dt',
|
||||
type: 'date',
|
||||
nullable: true,
|
||||
comment: '칩분석 보고일자',
|
||||
})
|
||||
chipReportDt: Date;
|
||||
|
||||
// MS 검사 결과
|
||||
@Column({
|
||||
name: 'ms_result_status',
|
||||
type: 'varchar',
|
||||
length: 50,
|
||||
nullable: true,
|
||||
comment: 'MS 감정결과',
|
||||
})
|
||||
msResultStatus: string;
|
||||
|
||||
@Column({
|
||||
name: 'ms_father_estimate',
|
||||
type: 'varchar',
|
||||
length: 100,
|
||||
nullable: true,
|
||||
comment: 'MS 추정부',
|
||||
})
|
||||
msFatherEstimate: string;
|
||||
|
||||
@Column({
|
||||
name: 'ms_report_dt',
|
||||
type: 'date',
|
||||
nullable: true,
|
||||
comment: 'MS 보고일자',
|
||||
})
|
||||
msReportDt: Date;
|
||||
|
||||
// Relations
|
||||
@ManyToOne(() => CowModel, { onDelete: 'CASCADE' })
|
||||
@JoinColumn({ name: 'fk_cow_no' })
|
||||
cow: CowModel;
|
||||
|
||||
@ManyToOne(() => FarmModel, { onDelete: 'CASCADE' })
|
||||
@JoinColumn({ name: 'fk_farm_no' })
|
||||
farm: FarmModel;
|
||||
}
|
||||
88
backend/src/genome/entities/genome-trait-detail.entity.ts
Normal file
88
backend/src/genome/entities/genome-trait-detail.entity.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import { BaseModel } from 'src/common/entities/base.entity';
|
||||
import { GenomeRequestModel } from './genome-request.entity';
|
||||
import {
|
||||
Column,
|
||||
Entity,
|
||||
Index,
|
||||
JoinColumn,
|
||||
ManyToOne,
|
||||
PrimaryGeneratedColumn,
|
||||
} from 'typeorm';
|
||||
|
||||
/**
|
||||
* 유전체 형질 상세 정보 (tb_genome_trait_detail)
|
||||
* 1개체당 35개 형질 → 35개 행으로 저장
|
||||
*/
|
||||
@Entity({ name: 'tb_genome_trait_detail' })
|
||||
@Index('idx_genome_trait_cow_id', ['cowId'])
|
||||
@Index('idx_genome_trait_name', ['traitName'])
|
||||
@Index('idx_genome_trait_cow_trait', ['cowId', 'traitName'])
|
||||
export class GenomeTraitDetailModel extends BaseModel {
|
||||
@PrimaryGeneratedColumn({
|
||||
name: 'pk_trait_detail_no',
|
||||
type: 'int',
|
||||
comment: '형질상세번호 PK',
|
||||
})
|
||||
pkTraitDetailNo: 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: 'trait_name',
|
||||
type: 'varchar',
|
||||
length: 100,
|
||||
nullable: true,
|
||||
comment: '형질명 (예: "12개월령체중", "도체중", "등심단면적")',
|
||||
})
|
||||
traitName: string;
|
||||
|
||||
@Column({
|
||||
name: 'trait_val',
|
||||
type: 'decimal',
|
||||
precision: 15,
|
||||
scale: 6,
|
||||
nullable: true,
|
||||
comment: '실측값',
|
||||
})
|
||||
traitVal: number;
|
||||
|
||||
@Column({
|
||||
name: 'trait_ebv',
|
||||
type: 'decimal',
|
||||
precision: 15,
|
||||
scale: 6,
|
||||
nullable: true,
|
||||
comment: '표준화육종가 (EBV: Estimated Breeding Value)',
|
||||
})
|
||||
traitEbv: number;
|
||||
|
||||
@Column({
|
||||
name: 'trait_percentile',
|
||||
type: 'decimal',
|
||||
precision: 10,
|
||||
scale: 4,
|
||||
nullable: true,
|
||||
comment: '백분위수 (전국 대비 순위)',
|
||||
})
|
||||
traitPercentile: number;
|
||||
|
||||
// Relations
|
||||
@ManyToOne(() => GenomeRequestModel, { onDelete: 'CASCADE' })
|
||||
@JoinColumn({ name: 'fk_request_no' })
|
||||
genomeRequest: GenomeRequestModel;
|
||||
}
|
||||
185
backend/src/genome/genome.controller.ts
Normal file
185
backend/src/genome/genome.controller.ts
Normal file
@@ -0,0 +1,185 @@
|
||||
import { Controller, Get, Post, Body, Param, Query } from '@nestjs/common';
|
||||
import { Public } from '../common/decorators/public.decorator';
|
||||
import { GenomeService } from './genome.service';
|
||||
import { GenomeRequestModel } from './entities/genome-request.entity';
|
||||
import { GenomeTraitDetailModel } from './entities/genome-trait-detail.entity';
|
||||
|
||||
export interface CategoryAverageDto {
|
||||
category: string;
|
||||
avgEbv: number;
|
||||
count: number;
|
||||
}
|
||||
|
||||
export interface ComparisonAveragesDto {
|
||||
nationwide: CategoryAverageDto[];
|
||||
region: CategoryAverageDto[];
|
||||
farm: CategoryAverageDto[];
|
||||
}
|
||||
|
||||
@Controller('genome')
|
||||
export class GenomeController {
|
||||
constructor(private readonly genomeService: GenomeService) { }
|
||||
|
||||
/**
|
||||
* GET /genome/dashboard-stats/:farmNo
|
||||
* 대시보드용 유전체 분석 통계 데이터
|
||||
* @param farmNo - 농장 번호
|
||||
*/
|
||||
@Get('dashboard-stats/:farmNo')
|
||||
getDashboardStats(@Param('farmNo') farmNo: string) {
|
||||
return this.genomeService.getDashboardStats(+farmNo);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /genome/farm-trait-comparison/:farmNo
|
||||
* 농가별 형질 비교 데이터 (농가 vs 지역 vs 전국)
|
||||
* @param farmNo - 농장 번호
|
||||
*/
|
||||
@Get('farm-trait-comparison/:farmNo')
|
||||
getFarmTraitComparison(@Param('farmNo') farmNo: string) {
|
||||
return this.genomeService.getFarmTraitComparison(+farmNo);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /genome/farm-region-ranking/:farmNo
|
||||
* 농가의 보은군 내 순위 조회 (대시보드용)
|
||||
* @param farmNo - 농장 번호
|
||||
*/
|
||||
@Get('farm-region-ranking/:farmNo')
|
||||
getFarmRegionRanking(@Param('farmNo') farmNo: string) {
|
||||
return this.genomeService.getFarmRegionRanking(+farmNo);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /genome/trait-rank/:cowId/:traitName
|
||||
* 개별 형질 기준 순위 조회
|
||||
* @param cowId - 개체식별번호 (KOR...)
|
||||
* @param traitName - 형질명 (도체중, 근내지방도 등)
|
||||
*/
|
||||
@Get('trait-rank/:cowId/:traitName')
|
||||
getTraitRank(
|
||||
@Param('cowId') cowId: string,
|
||||
@Param('traitName') traitName: string
|
||||
) {
|
||||
return this.genomeService.getTraitRank(cowId, traitName);
|
||||
}
|
||||
|
||||
// Genome Request endpoints
|
||||
@Get('request')
|
||||
findAllRequests(
|
||||
@Query('cowId') cowId?: string,
|
||||
@Query('farmId') farmId?: string,
|
||||
) {
|
||||
if (cowId) {
|
||||
return this.genomeService.findRequestsByCowId(+cowId);
|
||||
}
|
||||
if (farmId) {
|
||||
return this.genomeService.findRequestsByFarmId(+farmId);
|
||||
}
|
||||
return this.genomeService.findAllRequests();
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /genome/request/:cowId
|
||||
* 개체식별번호(KOR...)로 유전체 분석 의뢰 정보 조회
|
||||
* @param cowId - 개체식별번호
|
||||
*/
|
||||
@Get('request/:cowId')
|
||||
findRequestByCowIdentifier(@Param('cowId') cowId: string) {
|
||||
return this.genomeService.findRequestByCowIdentifier(cowId);
|
||||
}
|
||||
|
||||
@Post('request')
|
||||
createRequest(@Body() data: Partial<GenomeRequestModel>) {
|
||||
return this.genomeService.createRequest(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /genome/comparison-averages/:cowId
|
||||
* 개체 기준 전국/지역/농장 카테고리별 평균 EBV 비교 데이터
|
||||
* @param cowId - 개체식별번호 (KOR...)
|
||||
*/
|
||||
@Get('comparison-averages/:cowId')
|
||||
getComparisonAverages(@Param('cowId') cowId: string): Promise<ComparisonAveragesDto> {
|
||||
return this.genomeService.getComparisonAverages(cowId);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /genome/trait-comparison-averages/:cowId
|
||||
* 개체 기준 전국/지역/농장 형질별 평균 EBV 비교 데이터
|
||||
* (폴리곤 차트용 - 형질 단위 비교)
|
||||
* @param cowId - 개체식별번호 (KOR...)
|
||||
*/
|
||||
@Get('trait-comparison-averages/:cowId')
|
||||
getTraitComparisonAverages(@Param('cowId') cowId: string) {
|
||||
return this.genomeService.getTraitComparisonAverages(cowId);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /genome/selection-index/:cowId
|
||||
* 선발지수(가중 평균) 계산 + 농가/지역 순위
|
||||
* @param cowId - 개체식별번호 (KOR...)
|
||||
* @param body.traitConditions - 형질별 가중치 조건
|
||||
*/
|
||||
@Post('selection-index/:cowId')
|
||||
getSelectionIndex(
|
||||
@Param('cowId') cowId: string,
|
||||
@Body() body: { traitConditions: { traitNm: string; weight?: number }[] }
|
||||
) {
|
||||
return this.genomeService.getSelectionIndex(cowId, body.traitConditions);
|
||||
}
|
||||
|
||||
|
||||
// Genome Trait Detail endpoints
|
||||
@Get('trait-detail/:requestId')
|
||||
findTraitDetailsByRequestId(@Param('requestId') requestId: string) {
|
||||
return this.genomeService.findTraitDetailsByRequestId(+requestId);
|
||||
}
|
||||
|
||||
@Get('trait-detail/cow/:cowId')
|
||||
findTraitDetailsByCowId(@Param('cowId') cowId: string) {
|
||||
return this.genomeService.findTraitDetailsByCowId(cowId);
|
||||
}
|
||||
|
||||
@Post('trait-detail')
|
||||
createTraitDetail(@Body() data: Partial<GenomeTraitDetailModel>) {
|
||||
return this.genomeService.createTraitDetail(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /genome/check-cow/:cowId
|
||||
* 특정 개체 상세 정보 조회 (디버깅용)
|
||||
*/
|
||||
@Public()
|
||||
@Get('check-cow/:cowId')
|
||||
checkSpecificCow(@Param('cowId') cowId: string) {
|
||||
return this.genomeService.checkSpecificCows([cowId]);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /genome/yearly-trait-trend/:farmNo
|
||||
* 연도별 유전능력 추이 (형질별/카테고리별)
|
||||
* @param farmNo - 농장 번호
|
||||
* @param category - 카테고리명 (성장/생산/체형/무게/비율)
|
||||
* @param traitName - 형질명 (선택, 없으면 카테고리 전체)
|
||||
*/
|
||||
@Get('yearly-trait-trend/:farmNo')
|
||||
getYearlyTraitTrend(
|
||||
@Param('farmNo') farmNo: string,
|
||||
@Query('category') category: string,
|
||||
@Query('traitName') traitName?: string,
|
||||
) {
|
||||
return this.genomeService.getYearlyTraitTrend(+farmNo, category, traitName);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /genome/:cowId
|
||||
* cowId(개체식별번호)로 유전체 데이터 조회
|
||||
* @Get(':cowId')가 /genome/request 요청을 가로챔
|
||||
* 구체적인 경로들(request)이 위에, 와일드카드 경로(@Get(':cowId'))가 맨 아래
|
||||
*/
|
||||
@Get(':cowId')
|
||||
findByCowId(@Param('cowId') cowId: string) {
|
||||
return this.genomeService.findByCowId(cowId);
|
||||
}
|
||||
}
|
||||
23
backend/src/genome/genome.module.ts
Normal file
23
backend/src/genome/genome.module.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { GenomeController } from './genome.controller';
|
||||
import { GenomeService } from './genome.service';
|
||||
import { GenomeRequestModel } from './entities/genome-request.entity';
|
||||
import { GenomeTraitDetailModel } from './entities/genome-trait-detail.entity';
|
||||
import { CowModel } from '../cow/entities/cow.entity';
|
||||
import { FarmModel } from '../farm/entities/farm.entity';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([
|
||||
GenomeRequestModel,
|
||||
GenomeTraitDetailModel,
|
||||
CowModel,
|
||||
FarmModel,
|
||||
]),
|
||||
],
|
||||
controllers: [GenomeController],
|
||||
providers: [GenomeService],
|
||||
exports: [GenomeService],
|
||||
})
|
||||
export class GenomeModule {}
|
||||
2043
backend/src/genome/genome.service.ts
Normal file
2043
backend/src/genome/genome.service.ts
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user