1045 lines
25 KiB
Markdown
1045 lines
25 KiB
Markdown
# 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: <Home />
|
|
},
|
|
{
|
|
label: "내 소보기",
|
|
href: "/cow",
|
|
icon: <Cow />
|
|
},
|
|
{
|
|
label: "KPN 관리", // ⭐ 추가
|
|
href: "/kpn",
|
|
icon: <Dna />
|
|
},
|
|
{
|
|
label: "교배 계획", // ⭐ 추가 (선택사항)
|
|
href: "/breeding",
|
|
icon: <Calendar />
|
|
}
|
|
]
|
|
```
|
|
|
|
### 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
|
|
<Button onClick={() => router.push('/kpn/inventory')}>
|
|
KPN 보유 등록
|
|
</Button>
|
|
|
|
<Button onClick={() => router.push('/kpn/farm-recommend')}>
|
|
농장 추천
|
|
</Button>
|
|
```
|
|
|
|
#### **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
|
|
<div className="space-y-6">
|
|
{/* 헤더 */}
|
|
<Header
|
|
title="KPN 추천"
|
|
description={`전체 ${totalCows}두 암소 분석 완료`}
|
|
/>
|
|
|
|
{/* 통계 카드 */}
|
|
<StatsGrid
|
|
totalKpns={kpns.length}
|
|
avgMatchingRate={avgRate}
|
|
ownedCount={ownedKpns.length}
|
|
/>
|
|
|
|
{/* 액션 버튼 */}
|
|
<ActionButtons>
|
|
<Button variant="primary" href="/kpn/inventory">
|
|
KPN 보유 등록
|
|
</Button>
|
|
<Button variant="secondary" href="/kpn/farm-recommend">
|
|
농장 추천
|
|
</Button>
|
|
</ActionButtons>
|
|
|
|
{/* 필터 & 정렬 */}
|
|
<FilterBar
|
|
filterStatus={filterStatus}
|
|
setFilterStatus={setFilterStatus}
|
|
sortBy={sortBy}
|
|
setSortBy={setSortBy}
|
|
/>
|
|
|
|
{/* KPN 그리드 */}
|
|
<KPNGrid kpns={filteredKpns} onClick={handleKpnClick} />
|
|
</div>
|
|
```
|
|
|
|
#### **KPN 카드 디자인**
|
|
|
|
```tsx
|
|
<KPNCard>
|
|
{/* 헤더 */}
|
|
<div className="flex justify-between">
|
|
<div>
|
|
<Badge>#{rank}</Badge>
|
|
{isOwned && <Badge variant="success">보유</Badge>}
|
|
<h3>{kpn.pkKpnNo}</h3>
|
|
<p className="text-muted">{kpn.origin}</p>
|
|
</div>
|
|
<ChevronRight />
|
|
</div>
|
|
|
|
{/* 주요 유전자 */}
|
|
<div className="flex gap-2">
|
|
{kpn.genes.map(gene => (
|
|
<Badge key={gene} variant="outline">{gene}</Badge>
|
|
))}
|
|
</div>
|
|
|
|
{/* 통계 */}
|
|
<div className="grid grid-cols-2 gap-2">
|
|
<Stat label="매칭률" value={`${kpn.matchingRate}%`} />
|
|
<Stat label="근친도" value={`${kpn.inbreeding}%`} />
|
|
</div>
|
|
|
|
{/* 추천 이유 */}
|
|
<p className="text-sm border-t pt-2">
|
|
{kpn.recommendationReason}
|
|
</p>
|
|
</KPNCard>
|
|
```
|
|
|
|
---
|
|
|
|
### 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
|
|
<div className="space-y-6">
|
|
{/* 헤더 */}
|
|
<Header
|
|
title="KPN 보유 등록"
|
|
description="보유한 KPN을 등록하고 관리하세요"
|
|
/>
|
|
|
|
{/* 등록 폼 */}
|
|
<Card>
|
|
<h3>새 KPN 등록</h3>
|
|
<SearchInput
|
|
placeholder="KPN 번호 검색..."
|
|
onSearch={handleSearch}
|
|
/>
|
|
<Input label="수량" type="number" />
|
|
<Textarea label="메모" />
|
|
<Button onClick={handleRegister}>등록</Button>
|
|
</Card>
|
|
|
|
{/* 보유 KPN 목록 */}
|
|
<Card>
|
|
<h3>보유 KPN 목록 ({ownedKpns.length}개)</h3>
|
|
<Table>
|
|
<thead>
|
|
<tr>
|
|
<th>KPN 번호</th>
|
|
<th>수량</th>
|
|
<th>등록일</th>
|
|
<th>메모</th>
|
|
<th>작업</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{ownedKpns.map(kpn => (
|
|
<tr key={kpn.id}>
|
|
<td>{kpn.kpnNo}</td>
|
|
<td>{kpn.quantity}</td>
|
|
<td>{kpn.registeredAt}</td>
|
|
<td>{kpn.memo}</td>
|
|
<td>
|
|
<Button size="sm" onClick={() => handleEdit(kpn)}>
|
|
수정
|
|
</Button>
|
|
<Button size="sm" variant="destructive" onClick={() => handleDelete(kpn.id)}>
|
|
삭제
|
|
</Button>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</Table>
|
|
</Card>
|
|
</div>
|
|
```
|
|
|
|
---
|
|
|
|
### 3.3 농장 전체 KPN 추천 페이지 (`/kpn/farm-recommend`)
|
|
|
|
#### **페이지 목적**
|
|
- 농장 전체 암소 분석하여 최적 KPN 세트 추천
|
|
- 세대별 KPN 수요량 예측
|
|
- 장기 교배 전략 제공
|
|
|
|
#### **핵심 기능**
|
|
|
|
##### 1) 농장 전체 분석
|
|
```typescript
|
|
const result = await fetch('/api/cow/farm-package-recommendation', {
|
|
method: 'POST',
|
|
body: JSON.stringify({
|
|
farmNo: 1,
|
|
targetGenes: ['PLAG1', 'CAPN1', 'FASN'],
|
|
inbreedingThreshold: 10,
|
|
maxPackageSize: 5
|
|
})
|
|
});
|
|
|
|
// Response:
|
|
{
|
|
farmNo: 1,
|
|
totalCowCount: 65,
|
|
recommendedPackageCount: 5,
|
|
totalCoverage: 89.2,
|
|
package: [
|
|
{
|
|
kpnNo: 'KPN1385',
|
|
category: 'ESSENTIAL', // 또는 'RECOMMENDED'
|
|
applicableCowCount: 28,
|
|
coverage: 43.1,
|
|
averageMatchingScore: 85.2,
|
|
majorGenes: ['PLAG1 AA', 'CAPN1 CC', 'FASN GG'],
|
|
generationDemand: {
|
|
generation1: 28,
|
|
generation2: 14,
|
|
generation3: 7,
|
|
total: 49
|
|
},
|
|
isOwned: false
|
|
}
|
|
],
|
|
purchaseGuide: {
|
|
ownedKpns: ['KPN1234'],
|
|
recommendedToPurchase: ['KPN1385', 'KPN2471']
|
|
},
|
|
summary: {
|
|
essentialKpnCount: 3,
|
|
recommendedKpnCount: 2,
|
|
averageCoverage: 78.4,
|
|
totalDemand: 245
|
|
}
|
|
}
|
|
```
|
|
|
|
##### 2) 세대별 순환 전략
|
|
```typescript
|
|
const strategy = await fetch('/api/cow/rotation-strategy', {
|
|
method: 'POST',
|
|
body: JSON.stringify({
|
|
farmNo: 1,
|
|
targetGenes: ['PLAG1', 'CAPN1'],
|
|
inbreedingThreshold: 6.25,
|
|
maxPackageSize: 5,
|
|
generations: 20
|
|
})
|
|
});
|
|
|
|
// Response:
|
|
{
|
|
selectedKpns: ['KPN1385', 'KPN2471', ...],
|
|
rotationCycle: 4, // 4세대마다 순환
|
|
cowBreedingPlans: [
|
|
{
|
|
cowNo: 'KOR001',
|
|
generationPlans: [
|
|
{ generation: 1, recommendedKpn: 'KPN1385', inbreeding: 8.2 },
|
|
{ generation: 2, recommendedKpn: 'KPN2471', inbreeding: 4.1 },
|
|
{ generation: 3, recommendedKpn: 'KPN3692', inbreeding: 2.1 },
|
|
{ generation: 4, recommendedKpn: 'KPN4125', inbreeding: 1.0 },
|
|
{ generation: 5, recommendedKpn: 'KPN1385', inbreeding: 0.5 } // 순환
|
|
]
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
#### **UI 구성**
|
|
|
|
```tsx
|
|
<div className="space-y-6">
|
|
{/* 헤더 */}
|
|
<Header
|
|
title="농장 전체 KPN 추천"
|
|
description={`${totalCows}두 암소 분석 완료`}
|
|
/>
|
|
|
|
{/* 필터 설정 */}
|
|
<Card>
|
|
<h3>분석 조건</h3>
|
|
<GlobalFilterDisplay filters={currentFilters} />
|
|
<Button variant="outline" onClick={openFilterDialog}>
|
|
필터 조건 변경
|
|
</Button>
|
|
</Card>
|
|
|
|
{/* 추천 KPN 패키지 */}
|
|
<Card>
|
|
<h3>추천 KPN 패키지 (5개)</h3>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
{packageItems.map((item, index) => (
|
|
<KPNPackageCard key={item.kpnNo}>
|
|
{/* 우선순위 배지 */}
|
|
<Badge variant={item.category === 'ESSENTIAL' ? 'primary' : 'secondary'}>
|
|
{item.category === 'ESSENTIAL' ? '필수' : '권장'}
|
|
</Badge>
|
|
|
|
{/* KPN 정보 */}
|
|
<h4>#{index + 1} {item.kpnNo}</h4>
|
|
<p>적용 가능: {item.applicableCowCount}마리 ({item.coverage}%)</p>
|
|
|
|
{/* 주요 유전자 */}
|
|
<div className="flex gap-1">
|
|
{item.majorGenes.map(gene => (
|
|
<Badge key={gene} size="sm">{gene}</Badge>
|
|
))}
|
|
</div>
|
|
|
|
{/* 세대별 수요량 */}
|
|
<div className="border-t pt-2 mt-2">
|
|
<p className="text-sm text-muted">세대별 스트로 수요</p>
|
|
<div className="grid grid-cols-4 gap-2 text-xs">
|
|
<div>1세대: {item.generationDemand.generation1}</div>
|
|
<div>2세대: {item.generationDemand.generation2}</div>
|
|
<div>3세대: {item.generationDemand.generation3}</div>
|
|
<div>합계: {item.generationDemand.total}</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 보유 상태 */}
|
|
{item.isOwned ? (
|
|
<Badge variant="success">보유 중</Badge>
|
|
) : (
|
|
<Badge variant="warning">구매 필요</Badge>
|
|
)}
|
|
</KPNPackageCard>
|
|
))}
|
|
</div>
|
|
</Card>
|
|
|
|
{/* 구매 가이드 */}
|
|
<Card>
|
|
<h3>구매 가이드</h3>
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<h4>보유 중인 KPN ({ownedKpns.length}개)</h4>
|
|
<ul>
|
|
{ownedKpns.map(kpn => (
|
|
<li key={kpn}>{kpn}</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
<div>
|
|
<h4>구매 추천 KPN ({recommendedToPurchase.length}개)</h4>
|
|
<ul>
|
|
{recommendedToPurchase.map(kpn => (
|
|
<li key={kpn}>{kpn}</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
|
|
{/* 요약 통계 */}
|
|
<Card>
|
|
<h3>요약</h3>
|
|
<StatsGrid>
|
|
<Stat label="필수 KPN" value={essentialKpnCount} />
|
|
<Stat label="권장 KPN" value={recommendedKpnCount} />
|
|
<Stat label="평균 커버리지" value={`${averageCoverage}%`} />
|
|
<Stat label="총 수요량" value={totalDemand} />
|
|
</StatsGrid>
|
|
</Card>
|
|
</div>
|
|
```
|
|
|
|
---
|
|
|
|
### 3.4 KPN → 소 추천 페이지 (`/kpn/[kpnNo]/recommend`)
|
|
|
|
#### **페이지 목적**
|
|
- 특정 KPN에 적합한 암소 목록 표시
|
|
- 암소별 매칭률 및 근친도 계산
|
|
- 암소 상세 정보 확인
|
|
|
|
#### **핵심 기능**
|
|
|
|
```typescript
|
|
// KPN → 소 추천 (역방향)
|
|
const result = await fetch(`/api/kpn/${kpnNo}/recommendations`, {
|
|
method: 'POST',
|
|
body: JSON.stringify({
|
|
targetGenes: ['PLAG1', 'CAPN1'],
|
|
inbreedingThreshold: 10,
|
|
limit: 20,
|
|
farmNo: 1 // 특정 농장만 (선택사항)
|
|
})
|
|
});
|
|
|
|
// Response:
|
|
{
|
|
kpnId: 'KPN1385',
|
|
targetGenes: ['PLAG1', 'CAPN1'],
|
|
inbreedingThreshold: 10,
|
|
recommendations: [
|
|
{
|
|
rank: 1,
|
|
cowId: 'KOR001',
|
|
cowNumber: 'KOR002108023350',
|
|
matchingScore: 85.2,
|
|
inbreeding1: 8.2,
|
|
inbreeding2: 4.1,
|
|
inbreeding3: 2.1,
|
|
riskLevel: 'normal',
|
|
strategy: 'COMPLEMENT',
|
|
recommendationReason: 'PLAG1, CAPN1 유전자 보완 가능, 근친도 낮음',
|
|
geneMatchingDetails: [...],
|
|
cowInfo: {
|
|
farmNo: 1,
|
|
birthDate: '2021-03-15',
|
|
sex: 'F'
|
|
}
|
|
}
|
|
],
|
|
totalCount: 28
|
|
}
|
|
```
|
|
|
|
#### **UI 구성**
|
|
|
|
```tsx
|
|
<div className="space-y-6">
|
|
{/* KPN 정보 */}
|
|
<Card>
|
|
<h2>{kpnNo} 적합 암소</h2>
|
|
<p>총 {totalCount}마리 추천</p>
|
|
<KPNBasicInfo kpnNo={kpnNo} />
|
|
</Card>
|
|
|
|
{/* 필터 조건 */}
|
|
<FilterDisplay
|
|
targetGenes={targetGenes}
|
|
inbreedingThreshold={inbreedingThreshold}
|
|
/>
|
|
|
|
{/* 추천 암소 목록 */}
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
{recommendations.map(cow => (
|
|
<CowRecommendationCard key={cow.cowId}>
|
|
{/* 순위 배지 */}
|
|
<Badge>#{cow.rank}</Badge>
|
|
{cow.rank <= 3 && <Medal rank={cow.rank} />}
|
|
|
|
{/* 소 기본 정보 */}
|
|
<h3>{cow.cowNumber}</h3>
|
|
<p>농장: {cow.cowInfo.farmNo}</p>
|
|
<p>생년월일: {cow.cowInfo.birthDate}</p>
|
|
|
|
{/* 매칭 정보 */}
|
|
<StatsGrid>
|
|
<Stat label="매칭 점수" value={cow.matchingScore} />
|
|
<Stat label="근친도 (1세대)" value={`${cow.inbreeding1}%`} />
|
|
</StatsGrid>
|
|
|
|
{/* 추천 이유 */}
|
|
<p className="text-sm">{cow.recommendationReason}</p>
|
|
|
|
{/* 액션 */}
|
|
<Button onClick={() => viewCowDetail(cow.cowId)}>
|
|
상세 보기
|
|
</Button>
|
|
</CowRecommendationCard>
|
|
))}
|
|
</div>
|
|
</div>
|
|
```
|
|
|
|
---
|
|
|
|
### 3.5 소 → KPN 추천 페이지 (`/cow/[cowNo]/kpn/recommend`)
|
|
|
|
#### **페이지 목적**
|
|
- 특정 암소에 최적 KPN 추천
|
|
- 유전자 매칭 시뮬레이션
|
|
- 교배 계획 저장
|
|
|
|
#### **핵심 기능**
|
|
|
|
```typescript
|
|
// 소 → KPN 추천 (정방향)
|
|
const result = await fetch(`/api/cow/${cowNo}/recommendations`, {
|
|
method: 'POST',
|
|
body: JSON.stringify({
|
|
targetGenes: ['PLAG1', 'CAPN1'],
|
|
inbreedingThreshold: 10,
|
|
limit: 10
|
|
})
|
|
});
|
|
|
|
// Response:
|
|
{
|
|
cowId: 'KOR002108023350',
|
|
targetGenes: ['PLAG1', 'CAPN1'],
|
|
inbreedingThreshold: 10,
|
|
recommendations: [
|
|
{
|
|
rank: 1,
|
|
kpnId: 'KPN1385',
|
|
kpnNumber: 'KPN1385',
|
|
matchingScore: 92.5,
|
|
inbreeding1: 8.2,
|
|
inbreeding2: 4.1,
|
|
inbreeding3: 2.1,
|
|
riskLevel: 'normal',
|
|
strategy: 'COMPLEMENT',
|
|
recommendationReason: 'PLAG1, CAPN1 유전자 보완 가능',
|
|
geneMatchingDetails: [
|
|
{
|
|
geneName: 'PLAG1',
|
|
cowGenotype: 'AG',
|
|
kpnGenotype: 'AA',
|
|
offspringProbability: [
|
|
{ genotype: 'AA', probability: 50, isFavorable: true },
|
|
{ genotype: 'AG', probability: 50, isFavorable: true }
|
|
],
|
|
favorableProbability: 75,
|
|
improvementReason: '우량형 향상 가능 (목표: AA)'
|
|
}
|
|
]
|
|
}
|
|
],
|
|
totalCount: 15
|
|
}
|
|
```
|
|
|
|
#### **UI 구성**
|
|
|
|
```tsx
|
|
<div className="space-y-6">
|
|
{/* 암소 정보 */}
|
|
<Card>
|
|
<h2>{cowNo} 맞춤 KPN 추천</h2>
|
|
<CowBasicInfo cowNo={cowNo} />
|
|
</Card>
|
|
|
|
{/* TOP 3 추천 (메달) */}
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
{recommendations.slice(0, 3).map((kpn, index) => (
|
|
<TopKPNCard key={kpn.kpnId} rank={index + 1}>
|
|
<Medal rank={index + 1} />
|
|
<h3>{kpn.kpnNumber}</h3>
|
|
<Badge>매칭 {kpn.matchingScore}%</Badge>
|
|
<Button onClick={() => viewKpnDetail(kpn.kpnId)}>
|
|
상세 보기
|
|
</Button>
|
|
<Button variant="outline" onClick={() => saveBreedingPlan(kpn)}>
|
|
♥ 저장
|
|
</Button>
|
|
</TopKPNCard>
|
|
))}
|
|
</div>
|
|
|
|
{/* 전체 추천 KPN */}
|
|
<Card>
|
|
<h3>전체 추천 KPN ({totalCount}개)</h3>
|
|
<Table>
|
|
<thead>
|
|
<tr>
|
|
<th>순위</th>
|
|
<th>KPN 번호</th>
|
|
<th>매칭 점수</th>
|
|
<th>근친도</th>
|
|
<th>추천 이유</th>
|
|
<th>작업</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{recommendations.map(kpn => (
|
|
<tr key={kpn.kpnId}>
|
|
<td>#{kpn.rank}</td>
|
|
<td>{kpn.kpnNumber}</td>
|
|
<td>{kpn.matchingScore}%</td>
|
|
<td>{kpn.inbreeding1}%</td>
|
|
<td>{kpn.recommendationReason}</td>
|
|
<td>
|
|
<Button size="sm" onClick={() => viewKpnDetail(kpn.kpnId)}>
|
|
상세
|
|
</Button>
|
|
<Button size="sm" variant="outline" onClick={() => saveBreedingPlan(kpn)}>
|
|
저장
|
|
</Button>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</Table>
|
|
</Card>
|
|
|
|
{/* 유전자 매칭 시뮬레이션 */}
|
|
<Card>
|
|
<h3>유전자 매칭 시뮬레이션</h3>
|
|
{selectedKpn && (
|
|
<GeneMatchingSimulation
|
|
cow={cowData}
|
|
kpn={selectedKpn}
|
|
targetGenes={targetGenes}
|
|
/>
|
|
)}
|
|
</Card>
|
|
</div>
|
|
```
|
|
|
|
---
|
|
|
|
## 4. 네비게이션 플로우
|
|
|
|
### 4.1 전체 네비게이션 맵
|
|
|
|
```
|
|
홈 (/home)
|
|
├── 농장 현황
|
|
├── KPN 구매 계획 섹션 → [상세 분석] → /kpn/farm-recommend
|
|
└── 개체 관리 의사결정
|
|
|
|
내 소보기 (/cow)
|
|
├── 소 목록
|
|
└── 소 상세 (/cow/:cowNo)
|
|
├── 기본 정보
|
|
├── 유전능력
|
|
├── 보유 유전자
|
|
└── [KPN 추천받기] → /cow/:cowNo/kpn/recommend
|
|
|
|
KPN 관리 (/kpn)
|
|
├── KPN 목록
|
|
├── [KPN 보유 등록] → /kpn/inventory
|
|
├── [농장 추천] → /kpn/farm-recommend
|
|
└── KPN 클릭 → /kpn/:kpnNo/recommend
|
|
|
|
교배 계획 (/breeding)
|
|
└── 저장된 교배 조합 목록
|
|
```
|
|
|
|
### 4.2 사용자 시나리오별 플로우
|
|
|
|
#### **시나리오 1: KPN 구매 계획 수립**
|
|
```
|
|
1. 사이드바 "KPN 관리" 클릭
|
|
2. [농장 추천] 버튼 클릭
|
|
3. /kpn/farm-recommend 진입
|
|
4. 5개 KPN 패키지 확인
|
|
5. 구매할 KPN 선택
|
|
6. [KPN 보유 등록] 버튼 (페이지 내 링크)
|
|
7. /kpn/inventory 진입
|
|
8. KPN 등록 완료
|
|
```
|
|
|
|
#### **시나리오 2: 특정 소에 KPN 추천**
|
|
```
|
|
1. 사이드바 "내 소보기" 클릭
|
|
2. /cow 페이지에서 소 목록 확인
|
|
3. 001번 소 클릭 → /cow/KOR001
|
|
4. [KPN 추천받기] 버튼 클릭
|
|
5. /cow/KOR001/kpn/recommend 진입
|
|
6. TOP 3 KPN 확인
|
|
7. KPN1385 [상세 보기] 클릭 → 모달 또는 /kpn/KPN1385
|
|
8. [저장] 클릭 → 교배 계획 저장
|
|
```
|
|
|
|
#### **시나리오 3: 특정 KPN에 맞는 소 찾기**
|
|
```
|
|
1. 사이드바 "KPN 관리" 클릭
|
|
2. /kpn 페이지에서 KPN 목록 확인
|
|
3. KPN1385 카드 클릭
|
|
4. /kpn/KPN1385/recommend 진입
|
|
5. 적합한 암소 28마리 목록 확인
|
|
6. 001번 소 클릭 → 소 상세 정보 (모달 또는 /cow/KOR001)
|
|
```
|
|
|
|
---
|
|
|
|
## 5. API 연동 명세
|
|
|
|
### 5.1 API 엔드포인트 매핑
|
|
|
|
| 페이지 | API 엔드포인트 | Method | 용도 |
|
|
|--------|--------------|--------|------|
|
|
| `/kpn` | `/api/kpn/ranking` | POST | KPN 목록 + 필터 |
|
|
| `/kpn` | `/api/kpn/owned` | GET | 보유 KPN 확인 |
|
|
| `/kpn/inventory` | `/api/kpn/owned` | POST | KPN 보유 등록 |
|
|
| `/kpn/inventory` | `/api/kpn/owned` | GET | 보유 목록 조회 |
|
|
| `/kpn/inventory` | `/api/kpn/owned/:id` | PATCH | 보유 정보 수정 |
|
|
| `/kpn/inventory` | `/api/kpn/owned/:id` | DELETE | 보유 삭제 |
|
|
| `/kpn/farm-recommend` | `/api/cow/farm-package-recommendation` | POST | 농장 전체 KPN 추천 |
|
|
| `/kpn/farm-recommend` | `/api/cow/rotation-strategy` | POST | 세대별 순환 전략 (선택) |
|
|
| `/kpn/:kpnNo/recommend` | `/api/kpn/:kpnNo/recommendations` | POST | KPN → 소 추천 |
|
|
| `/cow/:cowNo/kpn/recommend` | `/api/cow/:cowNo/recommendations` | POST | 소 → KPN 추천 |
|
|
|
|
### 5.2 공통 Request/Response 형식
|
|
|
|
#### **Global Filter 구조**
|
|
```typescript
|
|
interface GlobalFilterOptions {
|
|
viewMode: 'QUANTITY' | 'QUALITY';
|
|
analysisIndex: 'GENE' | 'ABILITY';
|
|
selectedGenes: string[];
|
|
selectedTraits: string[];
|
|
inbreedingThreshold: number;
|
|
isActive: boolean;
|
|
}
|
|
```
|
|
|
|
#### **공통 Response 구조**
|
|
```typescript
|
|
interface BaseRecommendationResponse {
|
|
targetGenes: string[];
|
|
inbreedingThreshold: number;
|
|
totalCount: number;
|
|
recommendations: Array<{
|
|
rank: number;
|
|
matchingScore: number;
|
|
inbreeding1: number;
|
|
inbreeding2: number;
|
|
inbreeding3: number;
|
|
riskLevel: 'normal' | 'low' | 'high' | 'very_high';
|
|
strategy: 'COMPLEMENT' | 'STRENGTHEN' | 'BALANCED';
|
|
recommendationReason: string;
|
|
geneMatchingDetails: GeneMatchingDetail[];
|
|
}>;
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 6. 컴포넌트 재사용 전략
|
|
|
|
### 6.1 공통 컴포넌트
|
|
|
|
```typescript
|
|
// 1. 필터 바
|
|
<FilterBar
|
|
filterStatus={filterStatus}
|
|
setFilterStatus={setFilterStatus}
|
|
sortBy={sortBy}
|
|
setSortBy={setSortBy}
|
|
/>
|
|
|
|
// 2. 통계 그리드
|
|
<StatsGrid>
|
|
<Stat label="전체 KPN" value={totalKpns} />
|
|
<Stat label="평균 매칭률" value={`${avgRate}%`} />
|
|
<Stat label="보유 중" value={ownedCount} />
|
|
</StatsGrid>
|
|
|
|
// 3. KPN 카드
|
|
<KPNCard
|
|
kpn={kpn}
|
|
rank={index + 1}
|
|
isOwned={isOwned}
|
|
onClick={handleClick}
|
|
/>
|
|
|
|
// 4. 암소 카드
|
|
<CowCard
|
|
cow={cow}
|
|
rank={index + 1}
|
|
onClick={handleClick}
|
|
/>
|
|
|
|
// 5. 유전자 매칭 시뮬레이션
|
|
<GeneMatchingSimulation
|
|
cowGenotype={cowGenes}
|
|
kpnGenotype={kpnGenes}
|
|
targetGenes={targetGenes}
|
|
/>
|
|
```
|
|
|
|
### 6.2 Layout 구조
|
|
|
|
```tsx
|
|
// App Layout (공통)
|
|
<SidebarProvider>
|
|
<AppSidebar activeMenu="kpn" />
|
|
<SidebarInset>
|
|
<SiteHeader />
|
|
<main className="flex-1 overflow-y-auto p-4 md:p-6 lg:p-8">
|
|
{children}
|
|
</main>
|
|
</SidebarInset>
|
|
</SidebarProvider>
|
|
```
|
|
|
|
---
|
|
|
|
## 7. 모바일 반응형 가이드
|
|
|
|
### 7.1 브레이크포인트
|
|
|
|
```css
|
|
/* Tailwind 기준 */
|
|
sm: 640px /* 모바일 가로 */
|
|
md: 768px /* 태블릿 */
|
|
lg: 1024px /* 작은 데스크톱 */
|
|
xl: 1280px /* 데스크톱 */
|
|
2xl: 1536px /* 큰 데스크톱 */
|
|
```
|
|
|
|
### 7.2 모바일 우선 설계
|
|
|
|
```tsx
|
|
// 모바일: 전체 너비, 수직 스택
|
|
// 태블릿: 2열 그리드
|
|
// 데스크톱: 3열 그리드
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
{items.map(item => <Card key={item.id}>{item.content}</Card>)}
|
|
</div>
|
|
```
|
|
|
|
### 7.3 터치 최적화
|
|
|
|
```tsx
|
|
// 버튼 최소 크기: 44x44px (iOS 가이드라인)
|
|
<Button className="min-h-[44px] min-w-[44px]">
|
|
클릭
|
|
</Button>
|
|
|
|
// Active 상태 피드백
|
|
<Card className="active:scale-[0.98] transition-transform">
|
|
{content}
|
|
</Card>
|
|
```
|
|
|
|
---
|
|
|
|
## 8. 구현 우선순위
|
|
|
|
### Phase 1: 기반 구축 (1주)
|
|
- [x] UX 설계 문서 작성
|
|
- [ ] `/kpn/page.tsx` 개선 (필터 상태, 액션 버튼)
|
|
- [ ] 레이아웃 메뉴 추가 ("KPN 관리")
|
|
- [ ] API 연동 (`POST /kpn/ranking`, `GET /kpn/owned`)
|
|
|
|
### Phase 2: KPN 관리 기능 (1주)
|
|
- [ ] `/kpn/inventory/page.tsx` 생성
|
|
- [ ] `/kpn/farm-recommend/page.tsx` 생성
|
|
- [ ] API 연동 (보유 등록, 농장 추천)
|
|
|
|
### Phase 3: 양방향 추천 (1주)
|
|
- [ ] `/kpn/[kpnNo]/recommend/page.tsx` 생성
|
|
- [ ] `/cow/[cowNo]/kpn/recommend/page.tsx` 개선
|
|
- [ ] API 연동 (KPN→소, 소→KPN)
|
|
|
|
### Phase 4: 교배 계획 (선택사항)
|
|
- [ ] `/breeding/page.tsx` 생성
|
|
- [ ] 교배 조합 저장/관리 기능
|
|
|
|
---
|
|
|
|
## 9. 참조 자료
|
|
|
|
### 9.1 백엔드 API 문서
|
|
- `E:\repo5\repo5\next_nest_docker_template-main\backend\src\cow\cow.controller.ts`
|
|
- `E:\repo5\repo5\next_nest_docker_template-main\backend\src\kpn\kpn.controller.ts`
|
|
|
|
### 9.2 참조 프로젝트
|
|
- `E:\repo5\KPN Recommendation System\src\pages\KPNListPage.tsx`
|
|
- `E:\repo5\KPN Recommendation System\src\pages\FarmKPNRecommendPage.tsx`
|
|
- `E:\repo5\KPN Recommendation System\src\MOBILE_DESIGN_SPECS.md`
|
|
|
|
### 9.3 PRD 문서
|
|
- `E:\repo5\prd\기능요구사항20.md`
|
|
- `E:\repo5\prd\GENE_TABLE_SPEC.md`
|
|
|
|
---
|
|
|
|
**작성일**: 2025-01-XX
|
|
**최종 수정**: 2025-01-XX
|
|
**작성자**: Claude
|
|
**버전**: 1.0
|