INIT
This commit is contained in:
90
backend/src/cow/cow.controller.ts
Normal file
90
backend/src/cow/cow.controller.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
/**
|
||||
* ============================================================
|
||||
* 개체(Cow) 컨트롤러
|
||||
* ============================================================
|
||||
*
|
||||
* 사용 페이지: 개체 목록 페이지 (/cow)
|
||||
*
|
||||
* 엔드포인트:
|
||||
* - GET /cow - 기본 개체 목록 조회
|
||||
* - GET /cow/:id - 개체 상세 조회
|
||||
* - POST /cow/ranking - 랭킹 적용 개체 목록 조회
|
||||
* - POST /cow/ranking/global - 전체 개체 랭킹 조회
|
||||
* ============================================================
|
||||
*/
|
||||
|
||||
import { Controller, Get, Post, Put, Delete, Body, Param, Query } from '@nestjs/common';
|
||||
import { CowService } from './cow.service';
|
||||
import { CowModel } from './entities/cow.entity';
|
||||
import { RankingRequestDto } from './dto/ranking-request.dto';
|
||||
|
||||
@Controller('cow')
|
||||
export class CowController {
|
||||
constructor(private readonly cowService: CowService) {}
|
||||
|
||||
/**
|
||||
* GET /cow
|
||||
* 기본 개체 목록 조회
|
||||
*/
|
||||
@Get()
|
||||
findAll(@Query('farmId') farmId?: string) {
|
||||
if (farmId) {
|
||||
return this.cowService.findByFarmId(+farmId);
|
||||
}
|
||||
return this.cowService.findAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /cow/ranking
|
||||
* 랭킹이 적용된 개체 목록 조회
|
||||
*
|
||||
* 사용 페이지: 개체 목록 페이지
|
||||
*/
|
||||
@Post('ranking')
|
||||
findAllWithRanking(@Body() rankingRequest: RankingRequestDto) {
|
||||
return this.cowService.findAllWithRanking(rankingRequest);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /cow/ranking/global
|
||||
* 전체 개체 랭킹 조회 (모든 농장 포함)
|
||||
*
|
||||
* 사용 페이지: 대시보드 (농장 순위 비교)
|
||||
*/
|
||||
@Post('ranking/global')
|
||||
findAllWithGlobalRanking(@Body() rankingRequest: RankingRequestDto) {
|
||||
// farmNo 필터 없이 전체 개체 랭킹 조회
|
||||
const globalRequest = {
|
||||
...rankingRequest,
|
||||
filterOptions: {
|
||||
...rankingRequest.filterOptions,
|
||||
farmNo: undefined,
|
||||
},
|
||||
};
|
||||
return this.cowService.findAllWithRanking(globalRequest);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /cow/:cowId
|
||||
* 개체 상세 조회 (cowId: 개체식별번호 KOR로 시작)
|
||||
*/
|
||||
@Get(':cowId')
|
||||
findOne(@Param('cowId') cowId: string) {
|
||||
return this.cowService.findByCowId(cowId);
|
||||
}
|
||||
|
||||
@Post()
|
||||
create(@Body() data: Partial<CowModel>) {
|
||||
return this.cowService.create(data);
|
||||
}
|
||||
|
||||
@Put(':id')
|
||||
update(@Param('id') id: string, @Body() data: Partial<CowModel>) {
|
||||
return this.cowService.update(+id, data);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
remove(@Param('id') id: string) {
|
||||
return this.cowService.remove(+id);
|
||||
}
|
||||
}
|
||||
37
backend/src/cow/cow.module.ts
Normal file
37
backend/src/cow/cow.module.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* ============================================================
|
||||
* 개체(Cow) 모듈
|
||||
* ============================================================
|
||||
*
|
||||
* 사용 페이지: 개체 목록 페이지 (/cow)
|
||||
*
|
||||
* 등록된 엔티티:
|
||||
* - CowModel: 개체 기본 정보
|
||||
* - GenomeRequestModel: 유전체 분석 의뢰
|
||||
* - GenomeTraitDetailModel: 유전체 형질 상세 (35개 형질)
|
||||
* ============================================================
|
||||
*/
|
||||
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { CowController } from './cow.controller';
|
||||
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 { FilterEngineModule } from '../shared/filter/filter-engine.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([
|
||||
CowModel, // 개체 기본 정보 (tb_cow)
|
||||
GenomeRequestModel, // 유전체 분석 의뢰 (tb_genome_request)
|
||||
GenomeTraitDetailModel, // 유전체 형질 상세 (tb_genome_trait_detail)
|
||||
]),
|
||||
FilterEngineModule, // 필터 엔진 모듈
|
||||
],
|
||||
controllers: [CowController],
|
||||
providers: [CowService],
|
||||
exports: [CowService],
|
||||
})
|
||||
export class CowModule {}
|
||||
394
backend/src/cow/cow.service.ts
Normal file
394
backend/src/cow/cow.service.ts
Normal file
@@ -0,0 +1,394 @@
|
||||
/**
|
||||
* ============================================================
|
||||
* 개체(Cow) 서비스
|
||||
* ============================================================
|
||||
*
|
||||
* 사용 페이지: 개체 목록 페이지 (/cow)
|
||||
*
|
||||
* 주요 기능:
|
||||
* 1. 기본 개체 목록 조회 (findAll, findByFarmId)
|
||||
* 2. 개체 단건 조회 (findOne, findByCowId)
|
||||
* 3. 랭킹 적용 개체 목록 조회 (findAllWithRanking)
|
||||
* - GENOME: 35개 형질 EBV 가중 평균
|
||||
* 4. 개체 CRUD (create, update, remove)
|
||||
* ============================================================
|
||||
*/
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
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 { FilterEngineService } from '../shared/filter/filter-engine.service';
|
||||
import {
|
||||
RankingRequestDto,
|
||||
RankingCriteriaType,
|
||||
TraitRankingCondition,
|
||||
} from './dto/ranking-request.dto';
|
||||
import { isValidGenomeAnalysis } from '../common/config/GenomeAnalysisConfig';
|
||||
|
||||
/**
|
||||
* 개체(소) 관리 서비스
|
||||
*
|
||||
* 담당 기능:
|
||||
* - 개체 CRUD 작업
|
||||
* - 유전체 기반 랭킹 계산
|
||||
* - 필터링 및 정렬
|
||||
*/
|
||||
@Injectable()
|
||||
export class CowService {
|
||||
constructor(
|
||||
// 개체(소) 테이블 Repository
|
||||
@InjectRepository(CowModel)
|
||||
private readonly cowRepository: Repository<CowModel>,
|
||||
|
||||
// 유전체 분석 의뢰 Repository (형질 데이터 접근용)
|
||||
@InjectRepository(GenomeRequestModel)
|
||||
private readonly genomeRequestRepository: Repository<GenomeRequestModel>,
|
||||
|
||||
// 유전체 형질 상세 Repository (EBV 값 접근용)
|
||||
@InjectRepository(GenomeTraitDetailModel)
|
||||
private readonly genomeTraitDetailRepository: Repository<GenomeTraitDetailModel>,
|
||||
|
||||
// 동적 필터링 서비스 (검색, 정렬, 페이지네이션)
|
||||
private readonly filterEngineService: FilterEngineService,
|
||||
) { }
|
||||
|
||||
// ============================================================
|
||||
// 기본 조회 메서드
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* 전체 개체 목록 조회
|
||||
*
|
||||
* @returns 삭제되지 않은 모든 개체 목록
|
||||
* - farm 관계 데이터 포함
|
||||
* - 등록일(regDt) 기준 내림차순 정렬 (최신순)
|
||||
*/
|
||||
async findAll(): Promise<CowModel[]> {
|
||||
return this.cowRepository.find({
|
||||
where: { delDt: IsNull() }, // 삭제되지 않은 데이터만
|
||||
relations: ['farm'], // 농장 정보 JOIN
|
||||
order: { regDt: 'DESC' }, // 최신순 정렬
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 농장별 개체 목록 조회
|
||||
*
|
||||
* @param farmNo - 농장 PK 번호
|
||||
* @returns 해당 농장의 모든 개체 목록 (최신순)
|
||||
*/
|
||||
async findByFarmId(farmNo: number): Promise<CowModel[]> {
|
||||
return this.cowRepository.find({
|
||||
where: { fkFarmNo: farmNo, delDt: IsNull() },
|
||||
relations: ['farm'],
|
||||
order: { regDt: 'DESC' },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 개체 PK로 단건 조회
|
||||
*
|
||||
* @param id - 개체 PK 번호 (pkCowNo)
|
||||
* @returns 개체 정보 (farm 포함)
|
||||
* @throws NotFoundException - 개체를 찾을 수 없는 경우
|
||||
*/
|
||||
async findOne(id: number): Promise<CowModel> {
|
||||
const cow = await this.cowRepository.findOne({
|
||||
where: { pkCowNo: id, delDt: IsNull() },
|
||||
relations: ['farm'],
|
||||
});
|
||||
if (!cow) {
|
||||
throw new NotFoundException(`Cow #${id} not found`);
|
||||
}
|
||||
return cow;
|
||||
}
|
||||
|
||||
/**
|
||||
* 개체식별번호(cowId)로 단건 조회
|
||||
*
|
||||
* @param cowId - 개체식별번호 (예: KOR002119144049)
|
||||
* @returns 개체 정보 (farm 포함)
|
||||
* @throws NotFoundException - 개체를 찾을 수 없는 경우
|
||||
*/
|
||||
async findByCowId(cowId: string): Promise<CowModel> {
|
||||
const cow = await this.cowRepository.findOne({
|
||||
where: { cowId: cowId, delDt: IsNull() },
|
||||
relations: ['farm'],
|
||||
});
|
||||
if (!cow) {
|
||||
throw new NotFoundException(`Cow with cowId ${cowId} not found`);
|
||||
}
|
||||
return cow;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 랭킹 적용 조회 메서드
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* 랭킹 적용 개체 목록 조회 (메인 API)
|
||||
*
|
||||
* POST /cow/ranking 에서 호출
|
||||
*
|
||||
* 기능:
|
||||
* 1. 필터 조건으로 개체 목록 조회
|
||||
* 2. 랭킹 기준(GENOME/GENE)에 따라 점수 계산
|
||||
* 3. 점수 기준 정렬 후 순위 부여
|
||||
*
|
||||
* @param rankingRequest - 필터 옵션 + 랭킹 옵션
|
||||
* @returns 순위가 적용된 개체 목록
|
||||
*/
|
||||
async findAllWithRanking(rankingRequest: RankingRequestDto): Promise<any> {
|
||||
// Step 1: 요청에서 필터 옵션과 랭킹 옵션 추출
|
||||
const { filterOptions, rankingOptions } = rankingRequest;
|
||||
const { criteriaType } = rankingOptions;
|
||||
|
||||
// Step 2: 필터 조건에 맞는 개체 목록 조회
|
||||
const cows = await this.getFilteredCows(filterOptions);
|
||||
|
||||
// Step 3: 랭킹 기준에 따라 분기 처리
|
||||
switch (criteriaType) {
|
||||
// 유전체 형질 기반 랭킹 (35개 형질 EBV 가중 평균)
|
||||
case RankingCriteriaType.GENOME:
|
||||
return this.applyGenomeRanking(cows, rankingOptions.traitConditions || []);
|
||||
|
||||
// 기본값: 랭킹 없이 순서대로 반환
|
||||
default:
|
||||
return {
|
||||
items: cows.map((cow, index) => ({
|
||||
entity: cow,
|
||||
rank: index + 1,
|
||||
sortValue: 0,
|
||||
})),
|
||||
total: cows.length,
|
||||
criteriaType,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 필터 조건에 맞는 개체 목록 조회 (Private)
|
||||
*
|
||||
* @param filterOptions - 필터/정렬/페이지네이션 옵션
|
||||
* @returns 필터링된 개체 목록
|
||||
*/
|
||||
private async getFilteredCows(filterOptions?: any): Promise<CowModel[]> {
|
||||
// QueryBuilder로 기본 쿼리 구성
|
||||
const queryBuilder = this.cowRepository
|
||||
.createQueryBuilder('cow')
|
||||
.leftJoinAndSelect('cow.farm', 'farm') // 농장 정보 JOIN
|
||||
.where('cow.delDt IS NULL'); // 삭제되지 않은 데이터만
|
||||
|
||||
// farmNo가 직접 전달된 경우 처리 (프론트엔드 호환성)
|
||||
if (filterOptions?.farmNo) {
|
||||
queryBuilder.andWhere('cow.fkFarmNo = :farmNo', {
|
||||
farmNo: filterOptions.farmNo
|
||||
});
|
||||
}
|
||||
|
||||
// FilterEngine 사용하여 동적 필터 적용
|
||||
if (filterOptions?.filters) {
|
||||
const result = await this.filterEngineService.executeFilteredQuery(
|
||||
queryBuilder,
|
||||
filterOptions,
|
||||
);
|
||||
return result.data;
|
||||
}
|
||||
|
||||
// 필터 없으면 전체 조회 (최신순)
|
||||
return queryBuilder.orderBy('cow.regDt', 'DESC').getMany();
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 유전체(GENOME) 랭킹 메서드
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* 유전체 형질 기반 랭킹 적용 (Private)
|
||||
*
|
||||
* 계산 방식: 선택한 형질들의 EBV 가중 평균
|
||||
* - 각 형질에 weight(가중치) 적용 가능
|
||||
* - 모든 선택 형질이 있어야 점수 계산
|
||||
*
|
||||
* @param cows - 필터링된 개체 목록
|
||||
* @param traitConditions - 형질별 가중치 조건 배열
|
||||
* @returns 순위가 적용된 개체 목록
|
||||
* @example
|
||||
* traitConditions = [
|
||||
* { traitNm: '도체중', weight: 8 },
|
||||
* { traitNm: '근내지방도', weight: 10 }
|
||||
* ]
|
||||
*/
|
||||
private async applyGenomeRanking(
|
||||
cows: CowModel[],
|
||||
traitConditions: TraitRankingCondition[],
|
||||
): Promise<any> {
|
||||
// 각 개체별로 점수 계산
|
||||
const cowsWithScore = await Promise.all(
|
||||
cows.map(async (cow) => {
|
||||
// Step 1: cowId로 직접 형질 상세 데이터 조회 (35개 형질의 EBV 값)
|
||||
const traitDetails = await this.genomeTraitDetailRepository.find({
|
||||
where: { cowId: cow.cowId, delDt: IsNull() },
|
||||
});
|
||||
|
||||
// 형질 데이터가 없으면 점수 null
|
||||
if (traitDetails.length === 0) {
|
||||
return { entity: cow, sortValue: null, details: [] };
|
||||
}
|
||||
|
||||
// Step 2: 해당 개체의 최신 유전체 분석 의뢰 조회 (친자감별 확인용)
|
||||
const latestRequest = await this.genomeRequestRepository.findOne({
|
||||
where: { fkCowNo: cow.pkCowNo, delDt: IsNull() },
|
||||
order: { requestDt: 'DESC', regDt: 'DESC' },
|
||||
});
|
||||
|
||||
// Step 3: 친자감별 확인 - 아비 KPN "일치"가 아니면 분석 불가
|
||||
if (!latestRequest || !isValidGenomeAnalysis(latestRequest.chipSireName, latestRequest.chipDamName, cow.cowId)) {
|
||||
return { entity: cow, sortValue: null, details: [] };
|
||||
}
|
||||
|
||||
// Step 4: 가중 평균 계산
|
||||
let weightedSum = 0; // 가중치 적용된 EBV 합계
|
||||
let totalWeight = 0; // 총 가중치
|
||||
let hasAllTraits = true; // 모든 선택 형질 존재 여부
|
||||
const details: any[] = []; // 계산 상세 내역
|
||||
|
||||
// 사용자가 선택한 각 형질에 대해 처리
|
||||
for (const condition of traitConditions) {
|
||||
// 형질명으로 해당 형질 데이터 찾기
|
||||
const trait = traitDetails.find((d) => d.traitName === condition.traitNm);
|
||||
const weight = condition.weight || 1; // 가중치 (기본값: 1)
|
||||
|
||||
if (trait && trait.traitEbv !== null) {
|
||||
// EBV 값이 있으면 가중치 적용하여 합산
|
||||
const ebv = Number(trait.traitEbv);
|
||||
weightedSum += ebv * weight; // EBV × 가중치
|
||||
totalWeight += weight; // 가중치 누적
|
||||
|
||||
// 상세 내역 저장 (응답용)
|
||||
details.push({
|
||||
code: condition.traitNm, // 형질명
|
||||
value: ebv, // EBV 값
|
||||
weight, // 적용된 가중치
|
||||
});
|
||||
} else {
|
||||
// 형질이 없으면 플래그 설정
|
||||
hasAllTraits = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Step 6: 최종 점수 계산 (가중 평균)
|
||||
// 모든 선택 형질이 있어야만 점수 계산
|
||||
const sortValue = (hasAllTraits && totalWeight > 0)
|
||||
? weightedSum / totalWeight // 가중 평균 = 가중합 / 총가중치
|
||||
: null;
|
||||
|
||||
// Step 7: 응답 데이터 구성
|
||||
return {
|
||||
entity: {
|
||||
...cow,
|
||||
anlysDt: latestRequest.requestDt, // 분석일자 추가
|
||||
},
|
||||
sortValue, // 계산된 종합 점수 (선발지수)
|
||||
details, // 점수 계산에 사용된 형질별 상세
|
||||
ranking: {
|
||||
requestNo: latestRequest.pkRequestNo, // 분석 의뢰 번호
|
||||
requestDt: latestRequest.requestDt, // 분석 의뢰일
|
||||
traits: traitDetails.map((d) => ({ // 전체 형질 데이터
|
||||
traitName: d.traitName, // 형질명
|
||||
traitVal: d.traitVal, // 실측값
|
||||
traitEbv: d.traitEbv, // EBV (표준화육종가)
|
||||
traitPercentile: d.traitPercentile, // 백분위
|
||||
})),
|
||||
},
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
// ========================================
|
||||
// 백엔드 응답 예시
|
||||
// ========================================
|
||||
// {
|
||||
// "items": [
|
||||
// {
|
||||
// "entity": { "cowId": "KOR123456", "cowNm": "뽀삐", ... },
|
||||
// "sortValue": 85.5, // 가중 평균 점수
|
||||
// "rank": 1, // 순위
|
||||
// "ranking": {
|
||||
// "requestNo": 100,
|
||||
// "traits": [
|
||||
// { "traitName": "도체중", "traitVal": 450, "traitEbv": 12.5 },
|
||||
// { "traitName": "등심단면적", "traitVal": 95, "traitEbv": 8.3 }
|
||||
// ]
|
||||
// }
|
||||
// }
|
||||
// ],
|
||||
// "total": 100,
|
||||
// "criteriaType": "GENOME"
|
||||
// }
|
||||
|
||||
// Step 8: 점수 기준 내림차순 정렬
|
||||
const sorted = cowsWithScore.sort((a, b) => {
|
||||
// null 값은 맨 뒤로
|
||||
if (a.sortValue === null && b.sortValue === null) return 0;
|
||||
if (a.sortValue === null) return 1;
|
||||
if (b.sortValue === null) return -1;
|
||||
// 점수 높은 순 (내림차순)
|
||||
return b.sortValue - a.sortValue;
|
||||
});
|
||||
|
||||
// Step 9: 순위 부여 후 반환
|
||||
return {
|
||||
items: sorted.map((item, index) => ({
|
||||
...item,
|
||||
rank: index + 1, // 1부터 시작하는 순위
|
||||
})),
|
||||
total: sorted.length,
|
||||
criteriaType: RankingCriteriaType.GENOME,
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// CRUD 메서드
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* 새로운 개체 생성
|
||||
*
|
||||
* @param data - 생성할 개체 데이터
|
||||
* @returns 생성된 개체 엔티티
|
||||
*/
|
||||
async create(data: Partial<CowModel>): Promise<CowModel> {
|
||||
const cow = this.cowRepository.create(data);
|
||||
return this.cowRepository.save(cow);
|
||||
}
|
||||
|
||||
/**
|
||||
* 개체 정보 수정
|
||||
*
|
||||
* @param id - 개체 PK 번호
|
||||
* @param data - 수정할 데이터
|
||||
* @returns 수정된 개체 엔티티
|
||||
* @throws NotFoundException - 개체를 찾을 수 없는 경우
|
||||
*/
|
||||
async update(id: number, data: Partial<CowModel>): Promise<CowModel> {
|
||||
await this.findOne(id); // 존재 여부 확인
|
||||
await this.cowRepository.update(id, data);
|
||||
return this.findOne(id); // 수정된 데이터 반환
|
||||
}
|
||||
|
||||
/**
|
||||
* 개체 삭제 (Soft Delete)
|
||||
*
|
||||
* 실제 삭제가 아닌 delDt 컬럼에 삭제 시간 기록
|
||||
*
|
||||
* @param id - 개체 PK 번호
|
||||
* @throws NotFoundException - 개체를 찾을 수 없는 경우
|
||||
*/
|
||||
async remove(id: number): Promise<void> {
|
||||
const cow = await this.findOne(id); // 존재 여부 확인
|
||||
await this.cowRepository.softRemove(cow);
|
||||
}
|
||||
}
|
||||
129
backend/src/cow/dto/ranking-request.dto.ts
Normal file
129
backend/src/cow/dto/ranking-request.dto.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
/**
|
||||
* ============================================================
|
||||
* 랭킹 요청 DTO
|
||||
* ============================================================
|
||||
*
|
||||
* 사용 페이지: 개체 목록 페이지 (/cow)
|
||||
*
|
||||
* 프론트에서 POST /cow/ranking 호출 시 사용
|
||||
*
|
||||
* 지원하는 랭킹 기준:
|
||||
* 1. GENOME - 35개 유전체 형질 EBV 가중치 기반
|
||||
* ============================================================
|
||||
*/
|
||||
|
||||
/**
|
||||
* 랭킹 기준 타입
|
||||
* - GENOME: 유전체 형질 기반 랭킹 (35개 형질 EBV 가중 평균)
|
||||
*/
|
||||
export enum RankingCriteriaType {
|
||||
GENOME = 'GENOME',
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 필터 관련 타입 (FilterEngine에서 사용)
|
||||
// ============================================================
|
||||
|
||||
export type FilterOperator =
|
||||
| 'eq' // 같음
|
||||
| 'ne' // 같지 않음
|
||||
| 'gt' // 초과
|
||||
| 'gte' // 이상
|
||||
| 'lt' // 미만
|
||||
| 'lte' // 이하
|
||||
| 'like' // 포함 (문자열)
|
||||
| 'in' // 배열 내 포함
|
||||
| 'between'; // 범위
|
||||
|
||||
export type SortOrder = 'ASC' | 'DESC';
|
||||
|
||||
/**
|
||||
* 필터 조건
|
||||
* 예: { field: 'cowSex', operator: 'eq', value: 'F' }
|
||||
*/
|
||||
export interface FilterCondition {
|
||||
field: string;
|
||||
operator: FilterOperator;
|
||||
value: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* 정렬 옵션
|
||||
*/
|
||||
export interface SortOption {
|
||||
field: string;
|
||||
order: SortOrder;
|
||||
}
|
||||
|
||||
/**
|
||||
* 페이지네이션 옵션
|
||||
*/
|
||||
export interface PaginationOption {
|
||||
page: number; // 페이지 번호 (1부터 시작)
|
||||
limit: number; // 페이지당 개수
|
||||
}
|
||||
|
||||
/**
|
||||
* 필터 엔진 옵션
|
||||
* - 개체 목록 필터링에 사용
|
||||
*/
|
||||
export interface FilterEngineOptions {
|
||||
filters?: FilterCondition[];
|
||||
sorts?: SortOption[];
|
||||
pagination?: PaginationOption;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 랭킹 조건 타입
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* 유전체 형질 랭킹 조건
|
||||
* - 35개 형질 중 사용자가 선택한 형질만 대상
|
||||
* - weight: 1~10 가중치 (10이 100%)
|
||||
*
|
||||
* 예: { traitNm: '도체중', weight: 8 }
|
||||
*/
|
||||
export interface TraitRankingCondition {
|
||||
traitNm: string; // 형질명 (예: '도체중', '근내지방도')
|
||||
weight?: number; // 가중치 1~10 (기본값: 1)
|
||||
}
|
||||
|
||||
/**
|
||||
* 랭킹 옵션
|
||||
*/
|
||||
export interface RankingOptions {
|
||||
criteriaType: RankingCriteriaType; // 랭킹 기준 타입
|
||||
traitConditions?: TraitRankingCondition[]; // GENOME용: 형질별 가중치
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 메인 요청 DTO
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* 랭킹 요청 DTO
|
||||
*
|
||||
* 프론트에서 POST /cow/ranking 호출 시 Body로 전송
|
||||
*
|
||||
* @example
|
||||
* {
|
||||
* filterOptions: {
|
||||
* filters: [{ field: 'cowSex', operator: 'eq', value: 'F' }],
|
||||
* pagination: { page: 1, limit: 20 }
|
||||
* },
|
||||
* rankingOptions: {
|
||||
* criteriaType: 'GENOME',
|
||||
* traitConditions: [
|
||||
* { traitNm: '도체중', weight: 8 },
|
||||
* { traitNm: '근내지방도', weight: 10 }
|
||||
* ]
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
export interface RankingRequestDto {
|
||||
filterOptions?: FilterEngineOptions; // 필터/정렬/페이지네이션
|
||||
rankingOptions: RankingOptions; // 랭킹 조건
|
||||
}
|
||||
89
backend/src/cow/entities/cow.entity.ts
Normal file
89
backend/src/cow/entities/cow.entity.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import { BaseModel } from 'src/common/entities/base.entity';
|
||||
import { FarmModel } from 'src/farm/entities/farm.entity';
|
||||
import {
|
||||
Column,
|
||||
Entity,
|
||||
JoinColumn,
|
||||
ManyToOne,
|
||||
PrimaryGeneratedColumn,
|
||||
} from 'typeorm';
|
||||
|
||||
/**
|
||||
* 개체 기본 정보 (tb_cow)
|
||||
* 암소/수소, 부모 혈통 포함
|
||||
*/
|
||||
@Entity({ name: 'tb_cow' })
|
||||
export class CowModel extends BaseModel {
|
||||
@PrimaryGeneratedColumn({
|
||||
name: 'pk_cow_no',
|
||||
type: 'int',
|
||||
comment: '내부 PK (자동증가)',
|
||||
})
|
||||
pkCowNo: number;
|
||||
|
||||
@Column({
|
||||
name: 'cow_id',
|
||||
type: 'varchar',
|
||||
length: 20,
|
||||
nullable: true,
|
||||
comment: '개체식별번호 (KOR 또는 KPN)',
|
||||
})
|
||||
cowId: string;
|
||||
|
||||
@Column({
|
||||
name: 'cow_sex',
|
||||
type: 'varchar',
|
||||
length: 1,
|
||||
nullable: true,
|
||||
comment: '성별 (M/F)',
|
||||
})
|
||||
cowSex: string;
|
||||
|
||||
@Column({
|
||||
name: 'cow_birth_dt',
|
||||
type: 'date',
|
||||
nullable: true,
|
||||
comment: '생년월일',
|
||||
})
|
||||
cowBirthDt: Date;
|
||||
|
||||
@Column({
|
||||
name: 'sire_kpn',
|
||||
type: 'varchar',
|
||||
length: 20,
|
||||
nullable: true,
|
||||
comment: '부(씨수소) KPN번호',
|
||||
})
|
||||
sireKpn: string;
|
||||
|
||||
@Column({
|
||||
name: 'dam_cow_id',
|
||||
type: 'varchar',
|
||||
length: 20,
|
||||
nullable: true,
|
||||
comment: '모(어미소) 개체식별번호 (KOR)',
|
||||
})
|
||||
damCowId: string;
|
||||
|
||||
@Column({
|
||||
name: 'fk_farm_no',
|
||||
type: 'int',
|
||||
nullable: true,
|
||||
comment: '농장번호 FK',
|
||||
})
|
||||
fkFarmNo: number;
|
||||
|
||||
@Column({
|
||||
name: 'cow_status',
|
||||
type: 'varchar',
|
||||
length: 20,
|
||||
nullable: true,
|
||||
comment: '개체상태',
|
||||
})
|
||||
cowStatus: string;
|
||||
|
||||
// Relations
|
||||
@ManyToOne(() => FarmModel, { onDelete: 'CASCADE' })
|
||||
@JoinColumn({ name: 'fk_farm_no' })
|
||||
farm: FarmModel;
|
||||
}
|
||||
Reference in New Issue
Block a user