This commit is contained in:
2025-12-09 17:02:27 +09:00
parent 26f8e1dab2
commit 83127da569
275 changed files with 139682 additions and 1 deletions

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

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

View 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);
}
}

View 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 {}

File diff suppressed because it is too large Load Diff