diff --git a/backend/doc/FRONTEND_IMPLEMENTATION_GUIDE.md b/backend/doc/FRONTEND_IMPLEMENTATION_GUIDE.md deleted file mode 100644 index c42d518..0000000 --- a/backend/doc/FRONTEND_IMPLEMENTATION_GUIDE.md +++ /dev/null @@ -1,2521 +0,0 @@ -# 유전능력 컨설팅 서비스 프론트엔드 구현 가이드 - -**작성일**: 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 ( - - - - -
- - - 저장된 교배 조합 - - - {/* 검색 */} -
- setSearchKeyword(e.target.value)} - /> -
- - {/* 목록 */} -
- {data?.items.map((breeding) => ( - handleDelete(breeding.pkSaveNo)} - onUpdateMemo={(memo) => handleUpdateMemo(breeding.pkSaveNo, memo)} - /> - ))} -
- - {/* 페이지네이션 */} - -
-
-
-
-
- ) -} -``` - ---- - -### 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 ( - - - - - - - 유전자 선택 - - - {/* 검색 */} - setSearchQuery(e.target.value)} - /> - - {/* 타입 필터 */} - - 전체 - 육량형 - 육질형 - - - {/* 유전자 목록 (가상 스크롤) */} - -
- {genes?.items.map((gene) => ( -
handleToggle(gene.markerNm)} - > -
-
- -

{gene.markerNm}

- {gene.fkMarkerType} -
-
- {gene.markerDesc && ( -

{gene.markerDesc}

- )} -
- ))} -
-
- - {/* 선택된 유전자 */} -
-

선택된 유전자 ({selectedGenes.length}개)

-
- {selectedGenes.map((gene) => ( - - {gene} - - - ))} -
-
- - - - -
-
- ) -} -``` - -#### 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 - ---- - -**문서 끝** - -이 문서를 기반으로 완벽한 프론트엔드 애플리케이션을 구현하세요! 🚀 diff --git a/backend/doc/mpt 카테고리 권장수치.md b/backend/doc/mpt 카테고리 권장수치.md new file mode 100644 index 0000000..da7487d --- /dev/null +++ b/backend/doc/mpt 카테고리 권장수치.md @@ -0,0 +1,78 @@ +# 혈액대사판정시험(MPT) 검사항목 및 권장수치 + +## 개요 +번식능력 검사를 위한 혈액대사판정시험(MPT) 검사항목으로, 5개 카테고리 총 16개 항목으로 구성됩니다. + +--- + +## 1. 에너지 카테고리 + +| 항목 | 권장수치 | 단위 | +|------|---------|------| +| 혈당 | 40-84 | mg/dL | +| 콜레스테롤 | 74-252 | mg/dL | +| 유리지방산(NEFA) | 115-660 | μEq/L | + +--- + +## 2. 단백질 카테고리 + +| 항목 | 권장수치 | 단위 | +|------|---------|------| +| 총단백질 | 6.2-7.7 | g/dL | +| 알부민 | 3.3-4.3 | g/dL | +| 총글로블린 | 9.1-36.1 | g/dL | +| A/G | 0.1-0.4 | - | +| 요소태질소(BUN) | 11.7-18.9 | mg/dL | + +--- + +## 3. 간기능 카테고리 + +| 항목 | 권장수치 | 단위 | +|------|---------|------| +| AST | 47-92 | U/L | +| GGT | 11-32 | U/L | +| 지방간 지수 | -1.2 ~ 9.9 | - | + +--- + +## 4. 미네랄 카테고리 + +| 항목 | 권장수치 | 단위 | +|------|---------|------| +| 칼슘 | 8.1-10.6 | mg/dL | +| 인 | 6.2-8.9 | mg/dL | +| 칼슘/인 | 1.2-1.3 | - | +| 마그네슘 | 1.6-3.3 | mg/dL | + +--- + +## 5. 별도 카테고리 + +| 항목 | 권장수치 | 단위 | +|------|---------|------| +| 크레아틴 | 1.0-1.3 | mg/dL | + +--- + +## 결과 판정 기준 + +| 판정 | 설명 | +|------|------| +| 낮음 | 권장수치 하한 미만 | +| 권장범위 | 권장수치 범위 내 | +| 높음 | 권장수치 상한 초과 | + +--- + +## 시각화 방식 +- **폴리곤(레이더) 차트**: 5개 카테고리를 5각형 구조로 표현 +- **가로 막대 도표**: 각 항목별 낮음/권장범위/높음 표시 +- **색상 구분**: 우수/적정/부족으로 구분 + +--- + +## 참고 +> 혈액대사판정시험의 주요 5가지 항목에 대한 시군 및 농가 수치비교를 위해 표준화한 자료입니다. +> 본 결과자료를 통한 종합평가는 보은군 평균과 농가 평균을 비교하여 상대적 차이의 수준을 나타냅니다. diff --git a/backend/doc/ux-detail.md b/backend/doc/ux-detail.md deleted file mode 100644 index 7e63e4a..0000000 --- a/backend/doc/ux-detail.md +++ /dev/null @@ -1,1044 +0,0 @@ -# KPN 추천 시스템 UX 상세 설계서 - -> 참조 프로젝트 기반 UX 구현 가이드 -> 작성일: 2025-01-XX -> 기준: KPN Recommendation System 참조 프로젝트 - ---- - -## 📋 목차 - -1. [UX 설계 철학](#1-ux-설계-철학) -2. [메뉴 구조](#2-메뉴-구조) -3. [페이지별 상세 설계](#3-페이지별-상세-설계) -4. [네비게이션 플로우](#4-네비게이션-플로우) -5. [API 연동 명세](#5-api-연동-명세) -6. [컴포넌트 재사용 전략](#6-컴포넌트-재사용-전략) -7. [모바일 반응형 가이드](#7-모바일-반응형-가이드) - ---- - -## 1. UX 설계 철학 - -### 1.1 핵심 원칙 - -#### **양방향 탐색 지원** -- **KPN → 소**: "이 KPN을 누구한테 쓸 수 있지?" -- **소 → KPN**: "이 소한테 뭘 써야 하지?" -- **농장 전체**: "우리 농장엔 어떤 KPN이 필요하지?" - -#### **3가지 독립적 진입점** -``` -사용자 니즈별 진입점: -├── KPN 중심 사고 → KPN 관리 메뉴 -├── 소 중심 사고 → 내 소보기 메뉴 -└── 전략적 사고 → 홈 또는 KPN 관리 > 농장 추천 -``` - -#### **정보 아키텍처** -``` -일관된 3-패널 레이아웃 (Desktop): -┌────────┬──────────────┬──────────────┐ -│사이드바 │ 메인 콘텐츠 │ 상세 패널 │ -│(고정) │ (목록/그리드)│ (슬라이드) │ -└────────┴──────────────┴──────────────┘ -``` - ---- - -## 2. 메뉴 구조 - -### 2.1 사이드바 메뉴 (최종안) - -```typescript -interface MenuItem { - label: string; - href: string; - icon: React.ReactNode; - badge?: string; // 알림 배지 -} - -const menuItems: MenuItem[] = [ - { - label: "홈", - href: "/home", - icon: - }, - { - label: "내 소보기", - href: "/cow", - icon: - }, - { - label: "KPN 관리", // ⭐ 추가 - href: "/kpn", - icon: - }, - { - label: "교배 계획", // ⭐ 추가 (선택사항) - href: "/breeding", - icon: - } -] -``` - -### 2.2 PRD vs 최종 메뉴 비교 - -| 구분 | PRD (기능요구사항20.md) | 최종 구현안 | 사유 | -|------|----------------------|-----------|------| -| 메뉴 개수 | 2개 (홈, 내 소보기) | 4개 (홈, 내 소보기, KPN 관리, 교배 계획) | 사용성 개선 | -| KPN 접근 | 홈 섹션 / 소 상세 서브 | 독립 메뉴 | 양방향 탐색 지원 | -| 구매 계획 | 홈 섹션 | KPN 관리 > 농장 추천 | 전용 페이지로 분리 | -| 교배 이력 | 미정의 | 독립 메뉴 (선택사항) | 저장된 계획 관리 | - ---- - -## 3. 페이지별 상세 설계 - -### 3.1 KPN 목록 페이지 (`/kpn`) - -#### **페이지 목적** -- 전체 KPN 한눈에 조망 -- 보유/미보유 KPN 필터링 -- KPN 상세 정보 및 적합한 소 확인 - -#### **핵심 기능** - -##### 1) 필터 상태 (3가지) -```typescript -type FilterStatus = 'all' | 'owned' | 'needed'; - -// 전체: 모든 KPN -// 보유: 농가가 보유 중인 KPN (초록 배지) -// 필요: 구매가 필요한 KPN -``` - -##### 2) 정렬 옵션 -```typescript -type SortBy = 'matching' | 'inbreeding'; - -// matching: 매칭률 (우량형확률) 높은 순 -// inbreeding: 근친도 낮은 순 -``` - -##### 3) 액션 버튼 (2개) -```typescript - - - -``` - -#### **API 연동** - -```typescript -// 1. KPN 목록 조회 -const response = await fetch('/api/kpn/ranking', { - method: 'POST', - body: JSON.stringify({ - filterOptions: { - filters: [ - // 필터 조건 (선택사항) - ] - }, - rankingOptions: { - criteriaType: 'GENE', - order: 'DESC', - weights: {} - } - }) -}); - -// 2. 보유 KPN 확인 -const ownedKpns = await fetch('/api/kpn/owned'); -// Response: { isOwned: boolean } 각 KPN별 -``` - -#### **UI 구성** - -```tsx -
- {/* 헤더 */} -
- - {/* 통계 카드 */} - - - {/* 액션 버튼 */} - - - - - - {/* 필터 & 정렬 */} - - - {/* KPN 그리드 */} - -
-``` - -#### **KPN 카드 디자인** - -```tsx - - {/* 헤더 */} -
-
- #{rank} - {isOwned && 보유} -

{kpn.pkKpnNo}

-

{kpn.origin}

-
- -
- - {/* 주요 유전자 */} -
- {kpn.genes.map(gene => ( - {gene} - ))} -
- - {/* 통계 */} -
- - -
- - {/* 추천 이유 */} -

- {kpn.recommendationReason} -

-
-``` - ---- - -### 3.2 KPN 보유 등록 페이지 (`/kpn/inventory`) - -#### **페이지 목적** -- 농가가 보유한 KPN 등록 및 관리 -- 보유 KPN 목록 조회 -- 보유 수량 및 메모 관리 - -#### **핵심 기능** - -##### 1) KPN 검색 및 등록 -```typescript -// KPN 검색 -const kpns = await fetch(`/api/kpn/search?keyword=${keyword}`); - -// 보유 등록 -await fetch('/api/kpn/owned', { - method: 'POST', - body: JSON.stringify({ - kpnNo: 'KPN1385', - quantity: 5, - memo: '2024년 구매' - }) -}); -``` - -##### 2) 보유 KPN 목록 -```typescript -const ownedList = await fetch('/api/kpn/owned'); -// Response: -{ - totalCount: 3, - ownedKpns: [ - { - id: 1, - kpnNo: 'KPN1385', - quantity: 5, - memo: '2024년 구매', - registeredAt: '2024-03-15', - kpnInfo: { /* KPN 상세 정보 */ } - } - ] -} -``` - -##### 3) 수정 및 삭제 -```typescript -// 수정 -await fetch(`/api/kpn/owned/${id}`, { - method: 'PATCH', - body: JSON.stringify({ quantity: 3, memo: '사용 중' }) -}); - -// 삭제 -await fetch(`/api/kpn/owned/${id}`, { - method: 'DELETE' -}); -``` - -#### **UI 구성** - -```tsx -
- {/* 헤더 */} -
- - {/* 등록 폼 */} - -

새 KPN 등록

- - -