# 유전능력 컨설팅 서비스 프론트엔드 구현 가이드
**작성일**: 2025-10-26
**버전**: 1.0
**목적**: 백엔드 API 완전 활용한 완벽한 프론트엔드 구현
---
## 목차
1. [개요](#1-개요)
2. [기술 스택](#2-기술-스택)
3. [프로젝트 구조](#3-프로젝트-구조)
4. [백엔드 API 매핑](#4-백엔드-api-매핑)
5. [페이지별 구현 가이드](#5-페이지별-구현-가이드)
6. [컴포넌트 설계](#6-컴포넌트-설계)
7. [상태 관리](#7-상태-관리)
8. [API 클라이언트](#8-api-클라이언트)
9. [개발 우선순위](#9-개발-우선순위)
10. [테스트 가이드](#10-테스트-가이드)
---
## 1. 개요
### 1.1 프로젝트 소개
한우 유전능력 컨설팅 서비스의 프론트엔드 애플리케이션으로, 농장주가 개체별 유전능력, 유전자 정보를 확인하고 최적의 KPN(종모우)을 추천받아 교배 계획을 수립할 수 있는 웹 애플리케이션입니다.
### 1.2 주요 기능
- **농장 대시보드**: 농장 종합 현황, 분석연도별 비교, 등급 분포
- **개체 관리**: 개체 목록 조회, 상세 정보, 유전자/유전능력 분석
- **KPN 추천 시스템**:
- 암소→KPN 추천 (4단계 알고리즘)
- KPN→암소 추천 (역방향)
- 대립유전자 시뮬레이션
- 근친도 계산
- **교배 조합 관리**: 교배 조합 저장, 수정, 삭제, 조회
- **다세대 시뮬레이션**: 1~20세대 교배 시뮬레이션, KPN 순환 전략
- **농장 패키지 추천**: 농장 전체 최적 KPN 패키지 추천
### 1.3 PRD 참조
- 기능 요구사항: `E:\repo5\prd\기능요구사항14.md`
- 백엔드 태스크: `E:\repo5\prd\backend-tasklist.md`
- 백엔드 상세: `E:\repo5\prd\backend-detailed-tasks.md`
- DB 스키마: `E:\repo5\prd\GENE_TABLE_SPEC.md`
---
## 2. 기술 스택
### 2.1 프레임워크 및 라이브러리
| 카테고리 | 기술 | 버전 | 용도 |
|---------|------|------|------|
| **프레임워크** | Next.js | 14+ | React 기반 풀스택 프레임워크 |
| **언어** | TypeScript | 5+ | 타입 안전성 |
| **UI 라이브러리** | shadcn/ui | latest | 재사용 가능한 컴포넌트 |
| **스타일링** | Tailwind CSS | 3+ | 유틸리티 기반 CSS |
| **상태 관리** | Zustand | 4+ | 경량 상태 관리 |
| **API 클라이언트** | Axios | 1+ | HTTP 클라이언트 |
| **차트** | Recharts | 2+ | 데이터 시각화 |
| **폼 관리** | React Hook Form | 7+ | 폼 상태 관리 |
| **유효성 검사** | Zod | 3+ | 스키마 기반 검증 |
### 2.2 개발 환경
- **Node.js**: 18+
- **패키지 매니저**: npm or yarn
- **IDE**: VSCode (권장)
- **브라우저**: Chrome, Edge (최신 버전)
---
## 3. 프로젝트 구조
```
frontend/
├── src/
│ ├── app/ # Next.js App Router
│ │ ├── (auth)/ # 인증 관련 라우트
│ │ │ ├── login/
│ │ │ ├── signup/
│ │ │ ├── findid/
│ │ │ └── findpw/
│ │ ├── dashboard/ # 대시보드
│ │ │ ├── page.tsx
│ │ │ └── cows/ # 개체 관리 서브 페이지
│ │ │ ├── list/
│ │ │ ├── excellent/
│ │ │ └── cull/
│ │ ├── cow/ # 개체 상세
│ │ │ ├── page.tsx
│ │ │ ├── [cowNo]/
│ │ │ │ ├── page.tsx
│ │ │ │ └── genes/
│ │ │ │ ├── [category]/
│ │ │ │ │ └── page.tsx
│ │ ├── kpn/ # KPN 관리
│ │ │ ├── page.tsx
│ │ │ ├── [kpnNo]/
│ │ │ │ └── page.tsx
│ │ │ ├── recommend/
│ │ │ │ └── page.tsx
│ │ │ └── package/
│ │ │ └── page.tsx
│ │ ├── breeding/ # 교배 관리
│ │ │ ├── page.tsx
│ │ │ ├── saved/
│ │ │ │ └── page.tsx
│ │ │ └── simulator/
│ │ │ └── page.tsx
│ │ └── layout.tsx
│ │
│ ├── components/ # 재사용 컴포넌트
│ │ ├── ui/ # shadcn/ui 기본 컴포넌트
│ │ ├── dashboard/ # 대시보드 전용 컴포넌트
│ │ │ ├── section-cards.tsx
│ │ │ ├── year-comparison.tsx
│ │ │ ├── grade-distribution.tsx
│ │ │ └── gene-status.tsx
│ │ ├── cow/ # 개체 관련 컴포넌트
│ │ │ ├── cow-list-table.tsx
│ │ │ ├── cow-detail-card.tsx
│ │ │ └── cow-ranking-table.tsx
│ │ ├── kpn/ # KPN 관련 컴포넌트
│ │ │ ├── kpn-recommendation-card.tsx
│ │ │ ├── kpn-detail-modal.tsx
│ │ │ └── allele-simulation-chart.tsx
│ │ ├── breeding/ # 교배 관련 컴포넌트
│ │ │ ├── breeding-combination-card.tsx
│ │ │ └── multi-generation-timeline.tsx
│ │ ├── shared/ # 공통 컴포넌트
│ │ │ ├── app-sidebar.tsx
│ │ │ ├── site-header.tsx
│ │ │ ├── filter-panel.tsx
│ │ │ └── pagination.tsx
│ │ └── charts/ # 차트 컴포넌트
│ │ ├── bar-chart.tsx
│ │ ├── line-chart.tsx
│ │ └── radar-chart.tsx
│ │
│ ├── lib/ # 유틸리티 및 헬퍼
│ │ ├── api/ # API 클라이언트
│ │ │ ├── client.ts # Axios 설정
│ │ │ ├── cow.api.ts
│ │ │ ├── kpn.api.ts
│ │ │ ├── dashboard.api.ts
│ │ │ ├── breed.api.ts
│ │ │ ├── gene.api.ts
│ │ │ ├── genome.api.ts
│ │ │ └── index.ts
│ │ ├── utils.ts # 공통 유틸
│ │ └── constants.ts # 상수
│ │
│ ├── types/ # TypeScript 타입 정의
│ │ ├── cow.types.ts
│ │ ├── kpn.types.ts
│ │ ├── gene.types.ts
│ │ ├── genome.types.ts
│ │ ├── breeding.types.ts
│ │ ├── dashboard.types.ts
│ │ └── api.types.ts
│ │
│ ├── store/ # Zustand 스토어
│ │ ├── auth-store.ts
│ │ ├── filter-store.ts
│ │ └── farm-store.ts
│ │
│ ├── contexts/ # React Context
│ │ ├── GlobalFilterContext.tsx
│ │ └── AnalysisYearContext.tsx
│ │
│ └── hooks/ # Custom Hooks
│ ├── use-cow-data.ts
│ ├── use-kpn-recommendation.ts
│ └── use-pagination.ts
│
├── public/ # 정적 파일
└── package.json
```
---
## 4. 백엔드 API 매핑
### 4.1 API 베이스 URL
- **개발**: `http://localhost:4000`
- **프로덕션**: `https://api.example.com`
### 4.2 인증
모든 API 요청은 JWT 토큰을 포함해야 합니다:
```typescript
headers: {
Authorization: `Bearer ${token}`
}
```
### 4.3 API 엔드포인트 전체 목록
#### 4.3.1 인증 (`/auth`)
| 메서드 | 엔드포인트 | 설명 | 요청 | 응답 |
|--------|-----------|------|------|------|
| POST | `/auth/login` | 로그인 | `{ userId, password }` | `{ accessToken, user }` |
| POST | `/auth/signup` | 회원가입 | `CreateUserDto` | `{ success: true }` |
| POST | `/auth/find-id` | 아이디 찾기 | `{ name, phone }` | `{ userId }` |
| POST | `/auth/reset-password` | 비밀번호 재설정 | `{ userId, phone, newPassword }` | `{ success: true }` |
#### 4.3.2 개체 관리 (`/cow`)
| 메서드 | 엔드포인트 | 설명 | PRD 참조 |
|--------|-----------|------|----------|
| GET | `/cow` | 전체 개체 목록 | SFR-COW-001 |
| GET | `/cow/paginated?page=1&limit=10` | 페이징 목록 | SFR-COW-001 |
| GET | `/cow/farm/:farmNo` | 농장별 개체 목록 | - |
| GET | `/cow/search?keyword=...&farmNo=...` | 개체 검색 | SFR-COW-002 |
| GET | `/cow/:cowNo` | 개체 상세 정보 | SFR-COW-004 |
| POST | `/cow/ranking` | 개체 랭킹 (필터+정렬) | SFR-COW-003 |
| POST | `/cow/:cowNo/recommendations` | KPN 추천 | SFR-COW-016 |
| POST | `/cow/:cowNo/recommendations/:kpnNo/next-generation` | 다음 세대 KPN 추천 | - |
| POST | `/cow/simulate-breeding` | 교배 시뮬레이션 | SFR-COW-015 |
| POST | `/cow/simulate-multi-generation` | 다세대 시뮬레이션 | SFR-COW-038, 039 |
| POST | `/cow/farm-package-recommendation` | 농장 패키지 추천 | SFR-COW-037 |
| POST | `/cow/rotation-strategy` | KPN 순환 전략 | SFR-COW-038 |
**POST /cow/ranking 요청 예시**:
```typescript
{
"filterOptions": {
"filters": [
{ "field": "grade", "operator": "in", "value": ["A", "B"] }
],
"sorts": [
{ "field": "overallScore", "direction": "DESC" }
],
"pagination": { "page": 1, "limit": 10 }
},
"rankingOptions": {
"criteriaType": "GENOME",
"traitConditions": [
{ "traitNm": "근내지방도", "weight": 1.0 }
],
"limit": 100
}
}
```
**POST /cow/:cowNo/recommendations 요청 예시**:
```typescript
{
"targetGenes": ["PLAG1", "CAPN1", "FASN"],
"inbreedingThreshold": 12.5,
"limit": 10
}
```
**응답 예시**:
```typescript
{
"cow": {
"cowNo": "KOR002108023350",
"genes": [
{ "markerNm": "PLAG1", "genotype": "AG" }
]
},
"recommendations": [
{
"kpn": { "kpnNo": "KPN1385", "kpnNm": "..." },
"rank": 1,
"matchingScore": 90,
"inbreeding": {
"generation1": 6.25,
"generation2": 3.125,
"riskLevel": "normal"
},
"recommendationReason": "PLAG1 AA 보유로 자손 50% AA 고정 가능",
"geneMatching": [
{
"markerNm": "PLAG1",
"cowGenotype": "AG",
"kpnGenotype": "AA",
"offspringProbability": { "AA": 0.5, "AG": 0.5, "GG": 0.0 },
"favorableProbability": 0.5
}
],
"isOwned": true,
"isSaved": false
}
],
"totalCount": 10
}
```
#### 4.3.3 KPN 관리 (`/kpn`)
| 메서드 | 엔드포인트 | 설명 | PRD 참조 |
|--------|-----------|------|----------|
| GET | `/kpn/search?keyword=...` | KPN 검색 | - |
| GET | `/kpn/owned` | 보유 KPN 목록 | SFR-COW-026 |
| GET | `/kpn/owned/check/:kpnNo` | 보유 여부 확인 | SFR-COW-028 |
| POST | `/kpn/ranking` | KPN 랭킹 | - |
| POST | `/kpn/:kpnNo/recommendations` | KPN→암소 역추천 | SFR-COW-016-4 |
| POST | `/kpn/owned` | 보유 KPN 등록 | SFR-COW-025 |
| PATCH | `/kpn/owned/:id` | 보유 KPN 수정 | - |
| DELETE | `/kpn/owned/:id` | 보유 KPN 삭제 | SFR-COW-027 |
#### 4.3.4 대시보드 (`/dashboard`)
| 메서드 | 엔드포인트 | 설명 | PRD 참조 |
|--------|-----------|------|----------|
| GET | `/dashboard/summary/:farmNo` | 농장 요약 통계 | SFR-HOME-001 |
| GET | `/dashboard/analysis-completion/:farmNo` | 분석 완료 현황 | - |
| GET | `/dashboard/evaluation/:farmNo` | 농장 종합 평가 | SFR-FARM-002 |
| GET | `/dashboard/region-comparison/:farmNo` | 보은군 비교 | SFR-FARM-003 |
| GET | `/dashboard/cow-distribution/:farmNo` | 등급별 분포 | - |
| GET | `/dashboard/kpn-aggregation/:farmNo` | KPN 추천 집계 | SFR-HOME-009 |
| GET | `/dashboard/farm-kpn-inventory/:farmNo` | KPN 재고 현황 | SFR-COW-024 |
| GET | `/dashboard/analysis-years/:farmNo` | 분석연도 목록 | SFR-HOME-002 |
| GET | `/dashboard/analysis-years/:farmNo/latest` | 최신 분석연도 | - |
| GET | `/dashboard/year-comparison/:farmNo` | 3개년 비교 | SFR-HOME-011 |
| GET | `/dashboard/gene-status/:farmNo` | 유전자 보유 현황 | - |
| GET | `/dashboard/repro-efficiency/:farmNo` | 번식 효율 분석 | - |
| GET | `/dashboard/excellent-cows/:farmNo?limit=10` | 우수 개체 추천 | SFR-HOME-007 |
| GET | `/dashboard/cull-cows/:farmNo` | 도태 후보 추천 | SFR-HOME-008 |
**응답 예시 - /dashboard/summary/:farmNo**:
```typescript
{
"totalCows": 50,
"analysisSummary": {
"geneAnalyzed": 45,
"genomeAnalyzed": 48,
"fertilityAnalyzed": 40
},
"breedingTypeDistribution": {
"AI": 30,
"Donor": 10,
"Recipient": 8,
"Cull": 2
},
"cowStatusDistribution": {
"Active": 45,
"Sold": 3,
"Dead": 2
}
}
```
**응답 예시 - /dashboard/year-comparison/:farmNo**:
```typescript
{
"years": [2024, 2023, 2022],
"compositeScores": [82.5, 80.0, 78.5],
"traitTrends": {
"coldCarcassWeight": [78, 76, 75],
"marbling": [85, 83, 80]
},
"genePossessionTrends": {
"meatQuality": [65.0, 62.0, 60.0],
"meatQuantity": [68.0, 66.0, 65.0]
},
"trendAnalysis": {
"overall": "improvement",
"change": "+4.0",
"insights": "최근 3년간 지속적 개선 중"
}
}
```
#### 4.3.5 교배 조합 (`/breed`)
| 메서드 | 엔드포인트 | 설명 | PRD 참조 |
|--------|-----------|------|----------|
| GET | `/breed` | 교배 조합 목록 | SFR-COW-018 |
| GET | `/breed/search?keyword=...` | 교배 조합 검색 | - |
| GET | `/breed/:id` | 교배 조합 상세 | - |
| GET | `/breed/cow/:cowNo` | 암소별 교배 이력 | SFR-COW-020 |
| GET | `/breed/kpn/:kpnNo` | KPN별 사용 이력 | SFR-COW-021 |
| GET | `/breed/user/:userNo` | 사용자별 조회 | - |
| POST | `/breed` | 교배 조합 저장 | SFR-COW-017 |
| PATCH | `/breed/:id` | 교배 조합 수정 | - |
| DELETE | `/breed/:id` | 교배 조합 삭제 | SFR-COW-019 |
**POST /breed 요청 예시**:
```typescript
{
"fkUserNo": 1,
"fkCowNo": "002012345678",
"fkKpnNo": "KPN1385",
"saveMemo": "육질 개선을 위한 교배 조합"
}
```
#### 4.3.6 농장 관리 (`/farm`)
| 메서드 | 엔드포인트 | 설명 |
|--------|-----------|------|
| GET | `/farm` | 현재 사용자 농장 목록 |
| GET | `/farm/:id` | 농장 상세 정보 |
| GET | `/farm/:farmNo/kpn` | 농장 보유 KPN 목록 |
| GET | `/farm/:farmNo/kpn/:kpnNo/check` | KPN 보유 여부 확인 |
| POST | `/farm/:farmNo/kpn` | 보유 KPN 등록 |
| DELETE | `/farm/:farmNo/kpn/:kpnNo` | 보유 KPN 삭제 |
#### 4.3.7 유전자 (`/gene`)
| 메서드 | 엔드포인트 | 설명 |
|--------|-----------|------|
| GET | `/gene` | 전체 유전자 목록 |
| GET | `/gene/major` | 주요 유전자 20개 |
| GET | `/gene/types` | 유전자 타입 (QTY/QLT) |
| GET | `/gene/type/:typeCd` | 타입별 유전자 |
| GET | `/gene/search?keyword=...` | 유전자 검색 |
| GET | `/gene/paginated` | 페이징 검색 (가상 스크롤) |
| GET | `/gene/marker/:markerName/cows` | 특정 유전자 보유 개체 |
| GET | `/gene/cows/multi?genes=PLAG1,CAPN1` | 다중 유전자 보유 개체 (AND) |
| GET | `/gene/cow-profile/:cowNo` | 개체 유전자 프로필 |
| GET | `/gene/:cowNo` | 개체 SNP 데이터 |
#### 4.3.8 유전체 (`/genome`)
| 메서드 | 엔드포인트 | 설명 |
|--------|-----------|------|
| GET | `/genome` | 전체 유전체 데이터 |
| GET | `/genome/:cowNo` | 개체 유전체 데이터 |
---
## 5. 페이지별 구현 가이드
### 5.1 대시보드 (`/dashboard`)
#### 5.1.1 페이지 개요
**목적**: 농장 전체 현황을 한눈에 파악
**PRD 참조**: SFR-HOME-001~011, SFR-FARM-001~004
#### 5.1.2 레이아웃
```
┌─────────────────────────────────────────────────────┐
│ 사이드바 │ 헤더 (분석연도 선택, 전역 필터) │
├─────────────────────────────────────────────────────┤
│ │ 전역 필터 적용 현황 카드 │
│ ├─────────────────────────────┤
│ │ 기본 정보 카드 (4개) │
│ 네비게이션 │ - 전체 개체 수 │
│ - 대시보드 │ - 분석 완료율 │
│ - 개체 관리 │ - 평균 유전능력 점수 │
│ - KPN 추천 │ - 농장 종합 등급 │
│ - 교배 관리 ├─────────────────────────────┤
│ - 설정 │ 농장 현황 섹션 │
│ │ ├─ 연도별 비교 차트 │
│ │ └─ 등급별 분포 차트 │
│ ├─────────────────────────────┤
│ │ 유전자 보유 현황 │
│ │ (주요 마커별 보유율 레이더 차트) │
│ ├─────────────────────────────┤
│ │ 주요 개체 및 추천 │
│ │ ├─ 우수 개체 TOP 3 │
│ │ ├─ 도태 후보 추천 │
│ │ └─ 추천 KPN TOP 3 │
└─────────────────────────────────────────────────────┘
```
#### 5.1.3 컴포넌트 구성
**1. SectionCards (기본 정보 카드)**
API: `GET /dashboard/summary/:farmNo`
```tsx
// src/components/dashboard/section-cards.tsx
export function SectionCards({ farmNo }: { farmNo: number }) {
const { data, isLoading } = useDashboardSummary(farmNo)
return (
전체 개체 수
{data?.totalCows || 0}두
분석 완료율
{Math.round((data?.analysisSummary.geneAnalyzed / data?.totalCows) * 100)}%
{/* 평균 유전능력 점수, 농장 종합 등급 */}
)
}
```
**2. YearOverYearComparison (연도별 비교)**
API: `GET /dashboard/year-comparison/:farmNo`
```tsx
// src/components/dashboard/year-over-year-comparison.tsx
export function YearOverYearComparison({ farmNo }: { farmNo: number }) {
const { data } = useYearComparison(farmNo)
return (
연도별 비교 (3개년)
({
year,
score: data.compositeScores[idx],
meatQuality: data.genePossessionTrends.meatQuality[idx],
meatQuantity: data.genePossessionTrends.meatQuantity[idx]
}))}
xKey="year"
lines={[
{ key: 'score', label: '종합점수', color: '#8884d8' },
{ key: 'meatQuality', label: '육질형', color: '#82ca9d' },
{ key: 'meatQuantity', label: '육량형', color: '#ffc658' }
]}
/>
{/* 추세 분석 표시 */}
{data?.trendAnalysis.insights}
)
}
```
**3. GradeDistribution (등급별 분포)**
API: `GET /dashboard/cow-distribution/:farmNo`
```tsx
// src/components/dashboard/grade-distribution.tsx
export function GradeDistribution({ farmNo }: { farmNo: number }) {
const { data } = useCowDistribution(farmNo)
return (
등급별 분포
)
}
```
**4. GenePossessionStatus (유전자 보유 현황)**
API: `GET /dashboard/gene-status/:farmNo`
```tsx
// src/components/dashboard/gene-possession-status.tsx
export function GenePossessionStatus({ farmNo }: { farmNo: number }) {
const { data } = useGeneStatus(farmNo)
return (
주요 유전자 보유 현황
({
marker: marker.markerNm,
possession: marker.possessionRate,
target: 80 // 목표 보유율
}))}
keys={['possession', 'target']}
/>
)
}
```
**5. Top3Lists (우수 개체 및 추천)**
API:
- `GET /dashboard/excellent-cows/:farmNo?limit=3`
- `GET /dashboard/cull-cows/:farmNo`
- `GET /dashboard/kpn-aggregation/:farmNo`
```tsx
// src/components/dashboard/top3-lists.tsx
export function Top3Lists({ farmNo }: { farmNo: number }) {
const { data: excellentCows } = useExcellentCows(farmNo, 3)
const { data: cullCows } = useCullCows(farmNo)
const { data: kpnAggregation } = useKpnAggregation(farmNo)
return (
{/* 우수 개체 TOP 3 */}
우수 개체 TOP 3
{excellentCows?.map((cow, idx) => (
{idx + 1}
{cow.cowNo}
{cow.grade}등급 | {cow.overallScore}점
))}
{/* 도태 후보 추천 */}
도태 후보 추천
{cullCows?.map((cow) => (
{cow.cowNo}
{cow.cullReason}
))}
{/* 추천 KPN TOP 3 */}
추천 KPN TOP 3
{kpnAggregation?.slice(0, 3).map((kpn, idx) => (
{idx + 1}
{kpn.kpnNo}
{kpn.recommendedCount}두 추천 | 평균 {kpn.avgMatchingScore}점
))}
)
}
```
#### 5.1.4 전역 필터 기능
**GlobalFilterContext 구현**:
```tsx
// src/contexts/GlobalFilterContext.tsx
interface GlobalFilterState {
isActive: boolean
viewMode: 'QUANTITY' | 'QUALITY'
analysisIndex: 'GENE' | 'GENOME'
selectedGenes: string[]
selectedTraits: string[]
inbreedingThreshold: number
}
export const GlobalFilterProvider = ({ children }) => {
const [filters, setFilters] = useState({
isActive: false,
viewMode: 'QUANTITY',
analysisIndex: 'GENE',
selectedGenes: [],
selectedTraits: [],
inbreedingThreshold: 12.5
})
return (
{children}
)
}
```
#### 5.1.5 분석연도 선택
**AnalysisYearContext 구현**:
```tsx
// src/contexts/AnalysisYearContext.tsx
export const AnalysisYearProvider = ({ children, farmNo }) => {
const [selectedYear, setSelectedYear] = useState(null)
const [availableYears, setAvailableYears] = useState([])
useEffect(() => {
// API 호출: GET /dashboard/analysis-years/:farmNo
dashboardApi.getAnalysisYears(farmNo).then(years => {
setAvailableYears(years)
setSelectedYear(years[0]) // 최신 연도 자동 선택
})
}, [farmNo])
return (
{children}
)
}
```
---
### 5.2 개체 관리 페이지
#### 5.2.1 개체 목록 (`/dashboard/cows/list`)
**목적**: 필터, 정렬, 랭킹 기능을 갖춘 개체 목록
**PRD 참조**: SFR-COW-001, SFR-COW-003
**레이아웃**:
```
┌─────────────────────────────────────────────────────┐
│ 개체 관리 │
├─────────────────────────────────────────────────────┤
│ [필터 패널] │
│ - 유전자 선택 (다중 선택) │
│ - 등급 필터 (A/B/C/D/E) │
│ - 정렬 기준 (유전자 순위/등급/점수/개체번호) │
├─────────────────────────────────────────────────────┤
│ [개체 테이블] │
│ 순위 | 개체번호 | 등급 | 점수 | PLAG1 | CAPN1 | ... │
│ 1 | KOR... | A | 95 | AA | CC | ... │
│ 2 | KOR... | A | 92 | AG | CC | ... │
│ ... │
├─────────────────────────────────────────────────────┤
│ [페이지네이션] [총 50개체 / 10개씩 표시] │
└─────────────────────────────────────────────────────┘
```
**구현**:
```tsx
// src/app/dashboard/cows/list/page.tsx
'use client'
export default function CowListPage() {
const { filters } = useGlobalFilter()
const [selectedGenes, setSelectedGenes] = useState([])
const [gradeFilter, setGradeFilter] = useState([])
const [sortBy, setSortBy] = useState<'gene-rank' | 'grade' | 'score'>('gene-rank')
const [page, setPage] = useState(1)
const limit = 10
// API 호출: POST /cow/ranking
const { data, isLoading } = useCowRanking({
filterOptions: {
filters: [
...(gradeFilter.length > 0 ? [{ field: 'grade', operator: 'in', value: gradeFilter }] : []),
],
sorts: [
{ field: sortBy === 'score' ? 'overallScore' : sortBy, direction: 'DESC' }
],
pagination: { page, limit }
},
rankingOptions: {
criteriaType: filters.analysisIndex === 'GENE' ? 'GENE' : 'GENOME',
geneConditions: selectedGenes.map(gene => ({ markerNm: gene, order: 'DESC' })),
limit: 100
}
})
// 동적 컬럼 생성
const columns = [
{ key: 'rank', label: '순위' },
{ key: 'cowNo', label: '개체번호' },
{ key: 'grade', label: '등급' },
{ key: 'score', label: '점수' },
...selectedGenes.map(gene => ({ key: gene, label: gene }))
]
return (
{/* 필터 패널 */}
필터 및 정렬
{/* 유전자 선택 */}
{/* 등급 필터 */}
A
B
C
D
E
{/* 정렬 기준 */}
{/* 개체 테이블 */}
router.push(`/cow/${row.entity.pkCowNo}`)}
/>
{/* 페이지네이션 */}
)
}
```
#### 5.2.2 개체 상세 (`/cow/[cowNo]`)
**이미 잘 구현되어 있음!** 추가 개선 사항:
1. **유전자 전체 보기 페이지** (`/cow/[cowNo]/genes/[category]`)
- API: `GET /gene/cow-profile/:cowNo`
- 육량형/육질형 전체 유전자 목록 (2500개)
- 가상 스크롤링 적용
2. **KPN 추천 바로가기** 버튼 개선
- 현재: `/kpn/recommend?cowNo=${cowNo}`
- 개선: 버튼 클릭 시 즉시 추천 API 호출 후 결과 표시
---
### 5.3 KPN 추천 페이지 (`/kpn/recommend`)
#### 5.3.1 페이지 개요
**목적**: 암소에 최적의 KPN을 추천받고 교배 조합 저장
**PRD 참조**: SFR-COW-013, SFR-COW-016
#### 5.3.2 레이아웃
```
┌─────────────────────────────────────────────────────┐
│ KPN 추천 │
├─────────────────────────────────────────────────────┤
│ [암소 선택 섹션] │
│ - 암소 검색 (개체번호 또는 이름) │
│ - 선택된 암소 정보 카드 │
│ - 개체번호, 등급, 유전자 요약 │
├─────────────────────────────────────────────────────┤
│ [필터 설정] │
│ - 타겟 유전자 선택 │
│ - 근친도 임계값 (슬라이더) │
│ - 성별 필터 (수컷/암컷) │
│ [추천 받기 버튼] │
├─────────────────────────────────────────────────────┤
│ [추천 결과] │
│ ┌─────────────────────────────────────────────┐ │
│ │ 순위 1 | KPN1385 | 매칭점수: 90점 │ │
│ │ 근친도: 6.25% (안전) | 보유 여부: O │ │
│ │ 추천 이유: PLAG1 AA 보유로 자손 50% AA... │ │
│ │ [유전자 매칭 상세] [저장] [상세보기] │ │
│ └─────────────────────────────────────────────┘ │
│ ┌─────────────────────────────────────────────┐ │
│ │ 순위 2 | KPN2456 | 매칭점수: 88점 │ │
│ │ ... │ │
│ └─────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘
```
#### 5.3.3 구현
```tsx
// src/app/kpn/recommend/page.tsx
'use client'
export default function KpnRecommendPage() {
const router = useRouter()
const searchParams = useSearchParams()
const cowNoParam = searchParams.get('cowNo')
const [selectedCow, setSelectedCow] = useState(null)
const [targetGenes, setTargetGenes] = useState([])
const [inbreedingThreshold, setInbreedingThreshold] = useState(12.5)
const [limit, setLimit] = useState(10)
// 암소 검색
const { data: searchResults, isSearching } = useCowSearch(searchQuery)
// KPN 추천 API 호출
const { data: recommendations, isLoading, mutate: getRecommendations } = useKpnRecommendation()
useEffect(() => {
if (cowNoParam) {
// URL 파라미터로 전달된 암소 자동 로드
cowApi.findOne(cowNoParam).then(setSelectedCow)
}
}, [cowNoParam])
const handleRecommend = () => {
if (!selectedCow) return
getRecommendations({
cowNo: selectedCow.pkCowNo,
targetGenes,
inbreedingThreshold,
limit
})
}
const handleSaveBreeding = async (kpn: KpnRecommendation) => {
if (!selectedCow) return
try {
await breedApi.create({
fkUserNo: user.pkUserNo,
fkCowNo: selectedCow.pkCowNo,
fkKpnNo: kpn.kpn.kpnNo,
saveMemo: `매칭점수: ${kpn.matchingScore}점, 근친도: ${kpn.inbreeding.generation1}%`
})
toast.success('교배 조합이 저장되었습니다')
} catch (error) {
toast.error('저장 실패')
}
}
return (
{/* 1. 암소 선택 섹션 */}
암소 선택
{!selectedCow ? (
setSearchQuery(e.target.value)}
/>
{/* 검색 결과 */}
{searchResults && searchResults.length > 0 && (
{searchResults.map(cow => (
setSelectedCow(cow)}
>
{cow.pkCowNo}
{cow.grade}등급 | {cow.cowSex === 'F' ? '암소' : '수소'}
))}
)}
) : (
{selectedCow.pkCowNo}
{selectedCow.grade}등급 | 생년월일: {selectedCow.cowBirthDt}
)}
{/* 2. 필터 설정 */}
{selectedCow && (
추천 필터 설정
{/* 타겟 유전자 선택 */}
{targetGenes.length}개 선택됨
{/* 근친도 임계값 */}
setInbreedingThreshold(value[0])}
/>
{inbreedingThreshold < 10 ? '엄격' : inbreedingThreshold < 15 ? '보통' : '관대'}
{/* 추천 개수 */}
{/* 추천 받기 버튼 */}
)}
{/* 3. 추천 결과 */}
{recommendations && (
추천 결과 (총 {recommendations.totalCount}개)
{recommendations.recommendations.map((rec) => (
handleSaveBreeding(rec)}
onViewDetail={() => router.push(`/kpn/${rec.kpn.kpnNo}?cowNo=${selectedCow.pkCowNo}`)}
/>
))}
{/* 제외된 KPN 정보 */}
{recommendations.excludedKpns && recommendations.excludedKpns.count > 0 && (
교배 이력으로 제외된 KPN ({recommendations.excludedKpns.count}개)
{recommendations.excludedKpns.list.map((excluded) => (
{excluded.kpnNumber} - 마지막 사용: {excluded.lastUsedDate}
(재사용 가능: {excluded.reusableDate})
))}
)}
)}
)
}
```
#### 5.3.4 KPN 추천 카드 컴포넌트
```tsx
// src/components/kpn/kpn-recommendation-card.tsx
interface KpnRecommendationCardProps {
recommendation: KpnRecommendation
onSave: () => void
onViewDetail: () => void
}
export function KpnRecommendationCard({
recommendation: rec,
onSave,
onViewDetail
}: KpnRecommendationCardProps) {
const [showGeneMatching, setShowGeneMatching] = useState(false)
const getRiskColor = (riskLevel: string) => {
if (riskLevel === 'normal') return 'text-green-600'
if (riskLevel === 'warning') return 'text-yellow-600'
return 'text-red-600'
}
return (
{/* 헤더 */}
순위 {rec.rank}
{rec.kpn.kpnNo}
{rec.kpn.kpnNm}
매칭점수
{rec.matchingScore}점
{/* 근친도 정보 */}
1세대 근친도
{rec.inbreeding.generation1}%
2세대
{rec.inbreeding.generation2}%
3세대
{rec.inbreeding.generation3}%
{/* 보유 여부 및 저장 여부 */}
{rec.isOwned && 보유 중}
{rec.isSaved && 저장됨}
{rec.isExcludedByHistory && 이력 제외}
{/* 추천 이유 */}
추천 이유
{rec.recommendationReason}
전략: {rec.strategy}
{/* 유전자 매칭 상세 (토글) */}
{showGeneMatching && (
{rec.geneMatching.map((match, idx) => (
{match.markerNm}
0.5 ? 'default' : 'outline'}>
우량확률 {Math.round(match.favorableProbability * 100)}%
암소 유전자형
{match.cowGenotype}
KPN 유전자형
{match.kpnGenotype}
자손 유전자형 확률
{Object.entries(match.offspringProbability).map(([genotype, prob]) => (
{genotype}
{Math.round(prob * 100)}%
))}
{match.improvementReason && (
{match.improvementReason}
)}
))}
)}
{/* 액션 버튼 */}
)
}
```
---
### 5.4 KPN 상세 페이지 (`/kpn/[kpnNo]`)
#### 5.4.1 페이지 개요
**목적**: KPN 상세 정보 + 유전자 매칭 시뮬레이션
**PRD 참조**: SFR-COW-014
#### 5.4.2 구현
```tsx
// src/app/kpn/[kpnNo]/page.tsx
'use client'
export default function KpnDetailPage() {
const params = useParams()
const searchParams = useSearchParams()
const kpnNo = params.kpnNo as string
const cowNo = searchParams.get('cowNo') // 암소 선택 시 시뮬레이션
const { data: kpn, isLoading } = useKpnDetail(kpnNo, cowNo)
return (
{/* 1. KPN 기본 정보 */}
KPN 기본 정보
KPN 번호
{kpn?.basicInfo.kpnNumber}
출생지
{kpn?.basicInfo.origin}
생년월일
{kpn?.basicInfo.birthDate}
모색
{kpn?.basicInfo.coatColor}
{/* 보유 여부 */}
{kpn?.isOwned ? (
보유 중
) : (
미보유
)}
{kpn?.isSaved && 저장됨}
{/* 2. 혈통 정보 (3대) */}
혈통 정보 (3대)
{/* 3. 유전자 정보 */}
보유 유전자
육질형
육량형
{kpn?.genes.meatQuality.map((gene) => (
{gene.geneName}: {gene.genotype}
))}
{kpn?.genes.meatQuantity.map((gene) => (
{gene.geneName}: {gene.genotype}
))}
{/* 4. 유전능력 평가 */}
유전능력 종합 평가
종합점수
{kpn?.geneticAbility.compositeScore}
등급
{kpn?.geneticAbility.grade}
{/* 경제형질 추가 */}
{/* 5. 유전자 매칭 시뮬레이션 (cowNo 있을 때만) */}
{cowNo && kpn?.geneMatchingSimulation && (
유전자 매칭 시뮬레이션
선택한 암소({cowNo})와의 교배 시 예상 결과
{kpn.geneMatchingSimulation.detailedSimulation.map((sim, idx) => (
))}
{/* 종합 요약 */}
종합 요약
육질형: {kpn.geneMatchingSimulation.overallSummary.meatQuality}
육량형: {kpn.geneMatchingSimulation.overallSummary.meatQuantity}
)}
)
}
```
---
### 5.5 교배 조합 관리 (`/breeding/saved`)
#### 5.5.1 구현
```tsx
// src/app/breeding/saved/page.tsx
'use client'
export default function SavedBreedingPage() {
const { user } = useAuthStore()
const [searchKeyword, setSearchKeyword] = useState('')
const [page, setPage] = useState(1)
const limit = 20
// API 호출: GET /breed/search?keyword=...&userNo=...
const { data, isLoading } = useSavedBreedings({
keyword: searchKeyword,
userNo: user?.pkUserNo,
limit,
page
})
const handleDelete = async (id: number) => {
if (!confirm('삭제하시겠습니까?')) return
try {
await breedApi.remove(id)
toast.success('삭제되었습니다')
mutate() // 목록 새로고침
} catch (error) {
toast.error('삭제 실패')
}
}
const handleUpdateMemo = async (id: number, memo: string) => {
try {
await breedApi.update(id, { saveMemo: memo })
toast.success('메모가 수정되었습니다')
} catch (error) {
toast.error('수정 실패')
}
}
return (
)
}
```
---
### 5.6 다세대 교배 시뮬레이터 (`/breeding/simulator`)
#### 5.6.1 페이지 개요
**목적**: 1~20세대 교배 시뮬레이션, KPN 순환 전략
**PRD 참조**: SFR-COW-038, SFR-COW-039
#### 5.6.2 레이아웃
```
┌─────────────────────────────────────────────────────┐
│ 다세대 교배 시뮬레이터 │
├─────────────────────────────────────────────────────┤
│ [설정 패널] │
│ - 암소 선택 │
│ - 초기 KPN 선택 │
│ - 대체 KPN 선택 (최대 5개) │
│ - 세대 수 (1~20) │
│ - 근친도 임계값 │
│ [시뮬레이션 실행] │
├─────────────────────────────────────────────────────┤
│ [시뮬레이션 결과] │
│ ┌─────────────────────────────────────────────┐ │
│ │ 타임라인 (세대별 KPN 배정) │ │
│ │ Gen1: KPN1385 (근친도 6.25%) │ │
│ │ Gen2: KPN2456 (근친도 3.12%) │ │
│ │ Gen3: KPN3789 (근친도 1.56%) │ │
│ │ Gen4: KPN1385 🔄 (근친도 0.78%, 순환) │ │
│ └─────────────────────────────────────────────┘ │
│ ┌─────────────────────────────────────────────┐ │
│ │ 근친도 변화 그래프 (세로축: %, 가로축: 세대)│ │
│ └─────────────────────────────────────────────┘ │
│ ┌─────────────────────────────────────────────┐ │
│ │ 인사이트 │ │
│ │ - 순환 시작 세대: 4세대 │ │
│ │ - 권장 순환 주기: 4-5세대마다 │ │
│ │ - 근친도 영향 감소: 세대마다 50% │ │
│ └─────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘
```
#### 5.6.3 구현
```tsx
// src/app/breeding/simulator/page.tsx
'use client'
export default function MultiGenerationSimulatorPage() {
const [selectedCow, setSelectedCow] = useState(null)
const [initialKpn, setInitialKpn] = useState('')
const [alternativeKpns, setAlternativeKpns] = useState([])
const [generations, setGenerations] = useState(10)
const [inbreedingThreshold, setInbreedingThreshold] = useState(12.5)
const { data: result, isLoading, mutate: simulate } = useMultiGenerationSimulation()
const handleSimulate = () => {
if (!selectedCow || !initialKpn) {
toast.error('암소와 초기 KPN을 선택해주세요')
return
}
simulate({
cowNo: selectedCow.pkCowNo,
kpnNo: initialKpn,
targetGenes: [], // 전역 필터에서 가져오기
generations,
inbreedingThreshold,
alternativeKpns
})
}
return (
{/* 설정 패널 */}
시뮬레이션 설정
{/* 암소 선택 */}
{/* 초기 KPN */}
{/* 대체 KPN (최대 5개) */}
{/* 세대 수 */}
setGenerations(v[0])}
/>
{/* 근친도 임계값 */}
setInbreedingThreshold(v[0])}
/>
{/* 시뮬레이션 결과 */}
{result && (
<>
{/* 타임라인 */}
세대별 KPN 배정 타임라인
{/* 근친도 그래프 */}
세대별 근친도 변화
({
generation: gen.generation,
inbreeding: gen.cumulativeInbreeding,
threshold: inbreedingThreshold
}))}
xKey="generation"
lines={[
{ key: 'inbreeding', label: '누적 근친도', color: '#8884d8' },
{ key: 'threshold', label: '임계값', color: '#ff0000', strokeDasharray: '5 5' }
]}
/>
{/* 인사이트 */}
시뮬레이션 인사이트
순환 시작 세대
{result.rotationInsights.rotationStartGeneration}세대
권장 순환 주기
{result.rotationInsights.rotationCycle}
근친도 감소 원리
{result.rotationInsights.inbreedingReductionPrinciple}
{/* 경고 */}
{result.warnings && result.warnings.length > 0 && (
경고
{result.warnings.map((warning, idx) => (
{warning}
))}
)}
>
)}
)
}
```
---
### 5.7 농장 KPN 패키지 추천 (`/kpn/package`)
#### 5.7.1 페이지 개요
**목적**: 농장 전체 최적 KPN 패키지 추천
**PRD 참조**: SFR-COW-037
#### 5.7.2 구현
```tsx
// src/app/kpn/package/page.tsx
'use client'
export default function KpnPackagePage() {
const { user } = useAuthStore()
const [farmNo, setFarmNo] = useState(null)
const [targetGenes, setTargetGenes] = useState([])
const [inbreedingThreshold, setInbreedingThreshold] = useState(12.5)
const [maxPackageSize, setMaxPackageSize] = useState(5)
const { data: result, isLoading, mutate: getPackage } = useFarmPackageRecommendation()
const handleRecommend = () => {
if (!farmNo) return
getPackage({
farmNo,
targetGenes,
inbreedingThreshold,
maxPackageSize
})
}
return (
{/* 설정 */}
농장 KPN 패키지 추천 설정
setInbreedingThreshold(v[0])}
/>
{/* 추천 결과 */}
{result && (
<>
{/* 추천 KPN 패키지 */}
추천 KPN 패키지
{result.packageRecommendation.map((kpn, idx) => (
{idx + 1}
{kpn.kpnNumber}
{kpn.classification === 'essential' ? '필수' : '추천'}
추천 두수
{kpn.recommendedCount}두
평균 매칭점수
{kpn.avgMatchingScore}점
1세대 수요
{kpn.generationDemand.generation1}개
2세대 수요
{kpn.generationDemand.generation2}개
{/* 적용 가능 암소 */}
적용 가능 암소 ({kpn.applicableCows.length}두)
{kpn.applicableCows.slice(0, 10).map((cow) => (
{cow.cowNumber}
))}
{kpn.applicableCows.length > 10 && (
+{kpn.applicableCows.length - 10}두
)}
))}
{/* 보유 KPN 비교 */}
보유 KPN 비교 및 구매 가이드
보유 중인 KPN
{result.ownedComparison.ownedKpns.map((kpn) => (
{kpn}
))}
구매 필요 KPN
{result.ownedComparison.missingKpns.map((kpn) => (
{kpn}
))}
구매 가이드
{result.ownedComparison.purchaseGuide}
{/* 패키지 효과 분석 */}
패키지 적용 시 예상 효과
육질형 개선 두수
+{result.packageEffect.meatQualityImprovement}두
육량형 개선 두수
+{result.packageEffect.meatQuantityImprovement}두
평균 등급 상승
{result.packageEffect.expectedGradeIncrease}
>
)}
)
}
```
---
## 6. 컴포넌트 설계
### 6.1 공통 컴포넌트
#### 6.1.1 GeneFilterModal (유전자 선택 모달)
```tsx
// src/components/gene-filter-modal.tsx
interface GeneFilterModalProps {
selectedGenes: string[]
onSelectionChange: (genes: string[]) => void
maxSelection?: number
}
export function GeneFilterModal({
selectedGenes,
onSelectionChange,
maxSelection = 20
}: GeneFilterModalProps) {
const [open, setOpen] = useState(false)
const [searchQuery, setSearchQuery] = useState('')
const [selectedType, setSelectedType] = useState<'QTY' | 'QLT' | 'ALL'>('ALL')
// API: GET /gene/paginated?keyword=...&typeCd=...
const { data: genes } = useGenes({
keyword: searchQuery,
typeCd: selectedType === 'ALL' ? undefined : selectedType,
page: 1,
limit: 100
})
const handleToggle = (geneName: string) => {
if (selectedGenes.includes(geneName)) {
onSelectionChange(selectedGenes.filter(g => g !== geneName))
} else {
if (selectedGenes.length >= maxSelection) {
toast.error(`최대 ${maxSelection}개까지 선택 가능합니다`)
return
}
onSelectionChange([...selectedGenes, geneName])
}
}
return (
)
}
```
#### 6.1.2 DataTable (재사용 가능한 테이블)
```tsx
// src/components/data-table.tsx
interface Column {
key: string
label: string
render?: (value: any, row: any) => React.ReactNode
}
interface DataTableProps {
columns: Column[]
data: any[]
loading?: boolean
onRowClick?: (row: any) => void
}
export function DataTable({ columns, data, loading, onRowClick }: DataTableProps) {
if (loading) {
return
}
return (
{columns.map((col) => (
{col.label}
))}
{data.map((row, idx) => (
onRowClick?.(row)}
>
{columns.map((col) => (
{col.render ? col.render(row[col.key], row) : row[col.key]}
))}
))}
)
}
```
---
## 7. 상태 관리
### 7.1 Zustand Store
#### 7.1.1 AuthStore
```tsx
// src/store/auth-store.ts
interface AuthState {
user: User | null
token: string | null
setUser: (user: User) => void
setToken: (token: string) => void
logout: () => void
}
export const useAuthStore = create()(
persist(
(set) => ({
user: null,
token: null,
setUser: (user) => set({ user }),
setToken: (token) => set({ token }),
logout: () => set({ user: null, token: null })
}),
{
name: 'auth-storage'
}
)
)
```
#### 7.1.2 FilterStore
```tsx
// src/store/filter-store.ts
interface FilterState {
globalFilters: GlobalFilterState
setGlobalFilters: (filters: GlobalFilterState) => void
resetFilters: () => void
}
export const useFilterStore = create((set) => ({
globalFilters: {
isActive: false,
viewMode: 'QUANTITY',
analysisIndex: 'GENE',
selectedGenes: [],
selectedTraits: [],
inbreedingThreshold: 12.5
},
setGlobalFilters: (filters) => set({ globalFilters: filters }),
resetFilters: () => set({
globalFilters: {
isActive: false,
viewMode: 'QUANTITY',
analysisIndex: 'GENE',
selectedGenes: [],
selectedTraits: [],
inbreedingThreshold: 12.5
}
})
}))
```
---
## 8. API 클라이언트
### 8.1 Axios 설정
```tsx
// src/lib/api/client.ts
import axios from 'axios'
import { useAuthStore } from '@/store/auth-store'
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:4000'
export const apiClient = axios.create({
baseURL: API_BASE_URL,
headers: {
'Content-Type': 'application/json'
}
})
// 요청 인터셉터: JWT 토큰 자동 추가
apiClient.interceptors.request.use((config) => {
const token = useAuthStore.getState().token
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
})
// 응답 인터셉터: 자동 언래핑
apiClient.interceptors.response.use(
(response) => {
// 백엔드가 { data: ... } 형태로 응답하는 경우 자동 언래핑
if (response.data && response.data.data !== undefined) {
return response.data.data
}
return response.data
},
(error) => {
if (error.response?.status === 401) {
// 토큰 만료 시 로그아웃
useAuthStore.getState().logout()
window.location.href = '/login'
}
return Promise.reject(error)
}
)
```
### 8.2 API 함수 예시
```tsx
// src/lib/api/cow.api.ts
export const cowApi = {
findAll: () => apiClient.get('/cow'),
findOne: (cowNo: string) => apiClient.get(`/cow/${cowNo}`),
search: (keyword: string, farmNo?: number, limit = 20) =>
apiClient.get('/cow/search', {
params: { keyword, farmNo, limit }
}),
ranking: (payload: CowRankingRequest) =>
apiClient.post('/cow/ranking', payload),
getRecommendations: (cowNo: string, payload: RecommendationRequest) =>
apiClient.post(`/cow/${cowNo}/recommendations`, payload),
simulateBreeding: (payload: BreedingSimulationRequest) =>
apiClient.post('/cow/simulate-breeding', payload),
simulateMultiGeneration: (payload: MultiGenerationRequest) =>
apiClient.post('/cow/simulate-multi-generation', payload),
farmPackageRecommendation: (payload: FarmPackageRequest) =>
apiClient.post('/cow/farm-package-recommendation', payload)
}
```
---
## 9. 개발 우선순위
### 9.1 Phase 1: 핵심 기능 (우선순위: 높음)
**기간**: 2-3주
1. **대시보드 완성** (3일)
- 모든 컴포넌트 백엔드 API 연동
- 차트 및 시각화 개선
2. **개체 목록 고도화** (2일)
- 랭킹 시스템 구현
- 동적 컬럼 생성
3. **KPN 추천 페이지** (5일)
- 암소→KPN 추천
- 추천 결과 상세 표시
- 유전자 매칭 시뮬레이션
4. **교배 조합 저장/관리** (2일)
- CRUD 기능 완성
### 9.2 Phase 2: 고급 기능 (우선순위: 중간)
**기간**: 2-3주
1. **다세대 시뮬레이터** (5일)
- 타임라인 UI
- 근친도 그래프
2. **농장 KPN 패키지 추천** (3일)
- 패키지 추천 로직
- 효과 분석 UI
3. **보유 KPN 관리** (2일)
- 등록/수정/삭제
### 9.3 Phase 3: UX 개선 (우선순위: 낮음)
**기간**: 1-2주
1. 로딩 상태 개선
2. 에러 핸들링 강화
3. 반응형 디자인 최적화
4. 애니메이션 및 트랜지션
---
## 10. 테스트 가이드
### 10.1 단위 테스트
```tsx
// __tests__/components/KpnRecommendationCard.test.tsx
import { render, screen } from '@testing-library/react'
import { KpnRecommendationCard } from '@/components/kpn/kpn-recommendation-card'
describe('KpnRecommendationCard', () => {
it('renders recommendation correctly', () => {
const mockRecommendation = {
rank: 1,
kpn: { kpnNo: 'KPN1385', kpnNm: 'Test KPN' },
matchingScore: 90,
inbreeding: { generation1: 6.25, riskLevel: 'normal' }
}
render()
expect(screen.getByText('KPN1385')).toBeInTheDocument()
expect(screen.getByText('90점')).toBeInTheDocument()
})
})
```
### 10.2 E2E 테스트 (Playwright)
```tsx
// e2e/kpn-recommendation.spec.ts
import { test, expect } from '@playwright/test'
test('KPN recommendation flow', async ({ page }) => {
await page.goto('/login')
await page.fill('input[name="userId"]', 'testuser')
await page.fill('input[name="password"]', 'password123')
await page.click('button[type="submit"]')
await page.goto('/kpn/recommend')
await page.fill('input[placeholder="개체번호"]', 'KOR002108023350')
await page.click('button:has-text("추천 받기")')
await expect(page.locator('.recommendation-card').first()).toBeVisible()
})
```
---
## 부록 A: TypeScript 타입 정의
```typescript
// src/types/cow.types.ts
export interface Cow {
pkCowNo: string
cowShortNo?: string
fkFarmNo: number
cowSex: 'F' | 'M'
cowBirthDt?: string
cowReproType?: 'AI' | 'Donor' | 'Recipient' | 'Cull'
analysStat?: 'Match' | 'Mismatch' | 'NotAnalyzed' | 'NoRecord'
cowStatus: 'Active' | 'Dead' | 'Sold' | 'Slaughtered'
grade?: 'A' | 'B' | 'C' | 'D' | 'E'
overallScore?: number
}
export interface CowRankingRequest {
filterOptions?: {
filters?: FilterCondition[]
sorts?: SortOption[]
pagination?: { page: number; limit: number }
}
rankingOptions: {
criteriaType: 'GENE' | 'GENOME' | 'CONCEPTION_RATE' | 'BCS' | 'MPT' | 'INBREEDING' | 'COMPOSITE'
geneConditions?: { markerNm: string; order: 'ASC' | 'DESC' }[]
traitConditions?: { traitNm: string; weight: number }[]
limit?: number
offset?: number
}
}
export interface RecommendationRequest {
targetGenes: string[]
inbreedingThreshold: number
limit?: number
}
export interface RecommendationResponse {
cow: {
cowNo: string
genes: { markerNm: string; genotype: string }[]
}
recommendations: KpnRecommendation[]
excludedKpns?: {
count: number
list: { kpnNumber: string; lastUsedDate: string; reusableDate: string }[]
}
totalCount: number
}
export interface KpnRecommendation {
kpn: { kpnNo: string; kpnNm: string }
rank: number
matchingScore: number
inbreeding: {
generation1: number
generation2: number
generation3: number
riskLevel: 'normal' | 'warning' | 'danger'
}
recommendationReason: string
strategy: string
isOwned: boolean
isSaved: boolean
isExcludedByHistory: boolean
geneMatching: GeneMatching[]
}
export interface GeneMatching {
markerNm: string
cowGenotype: string
kpnGenotype: string
offspringProbability: { [key: string]: number }
favorableProbability: number
improvementReason?: string
}
```
---
## 부록 B: 환경 변수 설정
```env
# .env.local
NEXT_PUBLIC_API_URL=http://localhost:4000
NEXT_PUBLIC_APP_NAME=유전능력 컨설팅 서비스
```
---
## 부록 C: 참고 자료
- **백엔드 API 문서**: `http://localhost:4000/api-docs` (Swagger)
- **PRD 문서**: `E:\repo5\prd\기능요구사항14.md`
- **DB 스키마**: `E:\repo5\prd\GENE_TABLE_SPEC.md`
- **Next.js 공식 문서**: https://nextjs.org/docs
- **shadcn/ui 컴포넌트**: https://ui.shadcn.com
- **Recharts 문서**: https://recharts.org
---
**문서 끝**
이 문서를 기반으로 완벽한 프론트엔드 애플리케이션을 구현하세요! 🚀