25 KiB
25 KiB
KPN 추천 시스템 UX 상세 설계서
참조 프로젝트 기반 UX 구현 가이드 작성일: 2025-01-XX 기준: KPN Recommendation System 참조 프로젝트
📋 목차
1. UX 설계 철학
1.1 핵심 원칙
양방향 탐색 지원
- KPN → 소: "이 KPN을 누구한테 쓸 수 있지?"
- 소 → KPN: "이 소한테 뭘 써야 하지?"
- 농장 전체: "우리 농장엔 어떤 KPN이 필요하지?"
3가지 독립적 진입점
사용자 니즈별 진입점:
├── KPN 중심 사고 → KPN 관리 메뉴
├── 소 중심 사고 → 내 소보기 메뉴
└── 전략적 사고 → 홈 또는 KPN 관리 > 농장 추천
정보 아키텍처
일관된 3-패널 레이아웃 (Desktop):
┌────────┬──────────────┬──────────────┐
│사이드바 │ 메인 콘텐츠 │ 상세 패널 │
│(고정) │ (목록/그리드)│ (슬라이드) │
└────────┴──────────────┴──────────────┘
2. 메뉴 구조
2.1 사이드바 메뉴 (최종안)
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가지)
type FilterStatus = 'all' | 'owned' | 'needed';
// 전체: 모든 KPN
// 보유: 농가가 보유 중인 KPN (초록 배지)
// 필요: 구매가 필요한 KPN
2) 정렬 옵션
type SortBy = 'matching' | 'inbreeding';
// matching: 매칭률 (우량형확률) 높은 순
// inbreeding: 근친도 낮은 순
3) 액션 버튼 (2개)
<Button onClick={() => router.push('/kpn/inventory')}>
KPN 보유 등록
</Button>
<Button onClick={() => router.push('/kpn/farm-recommend')}>
농장 추천
</Button>
API 연동
// 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 구성
<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 카드 디자인
<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 검색 및 등록
// 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 목록
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) 수정 및 삭제
// 수정
await fetch(`/api/kpn/owned/${id}`, {
method: 'PATCH',
body: JSON.stringify({ quantity: 3, memo: '사용 중' })
});
// 삭제
await fetch(`/api/kpn/owned/${id}`, {
method: 'DELETE'
});
UI 구성
<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) 농장 전체 분석
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) 세대별 순환 전략
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 구성
<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에 적합한 암소 목록 표시
- 암소별 매칭률 및 근친도 계산
- 암소 상세 정보 확인
핵심 기능
// 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 구성
<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 추천
- 유전자 매칭 시뮬레이션
- 교배 계획 저장
핵심 기능
// 소 → 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 구성
<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 구조
interface GlobalFilterOptions {
viewMode: 'QUANTITY' | 'QUALITY';
analysisIndex: 'GENE' | 'ABILITY';
selectedGenes: string[];
selectedTraits: string[];
inbreedingThreshold: number;
isActive: boolean;
}
공통 Response 구조
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 공통 컴포넌트
// 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 구조
// 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 브레이크포인트
/* Tailwind 기준 */
sm: 640px /* 모바일 가로 */
md: 768px /* 태블릿 */
lg: 1024px /* 작은 데스크톱 */
xl: 1280px /* 데스크톱 */
2xl: 1536px /* 큰 데스크톱 */
7.2 모바일 우선 설계
// 모바일: 전체 너비, 수직 스택
// 태블릿: 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 터치 최적화
// 버튼 최소 크기: 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주)
- 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.tsE:\repo5\repo5\next_nest_docker_template-main\backend\src\kpn\kpn.controller.ts
9.2 참조 프로젝트
E:\repo5\KPN Recommendation System\src\pages\KPNListPage.tsxE:\repo5\KPN Recommendation System\src\pages\FarmKPNRecommendPage.tsxE:\repo5\KPN Recommendation System\src\MOBILE_DESIGN_SPECS.md
9.3 PRD 문서
E:\repo5\prd\기능요구사항20.mdE:\repo5\prd\GENE_TABLE_SPEC.md
작성일: 2025-01-XX 최종 수정: 2025-01-XX 작성자: Claude 버전: 1.0