Files
genome2025/backend/doc/ux-detail.md
2025-12-09 17:02:27 +09:00

25 KiB

KPN 추천 시스템 UX 상세 설계서

참조 프로젝트 기반 UX 구현 가이드 작성일: 2025-01-XX 기준: KPN Recommendation System 참조 프로젝트


📋 목차

  1. UX 설계 철학
  2. 메뉴 구조
  3. 페이지별 상세 설계
  4. 네비게이션 플로우
  5. API 연동 명세
  6. 컴포넌트 재사용 전략
  7. 모바일 반응형 가이드

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.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