17 KiB
17 KiB
프론트엔드 API 연동 가이드
작성일: 2025-10-26 버전: 1.0 대상: 한우 유전체 분석 시스템 프론트엔드 개발자
📋 목차
1. 개요
1.1 시스템 구조
사용자 (User) 1:N 농장 (Farm) 1:N 개체 (Cow)
- User: 로그인한 사용자 (농가, 컨설턴트, 기관담당자)
- Farm: 사용자가 소유한 농장 (한 사용자가 여러 농장 소유 가능)
- Cow: 농장에 속한 개체 (한우)
1.2 주요 기술 스택
백엔드:
- NestJS 10.x
- TypeORM
- JWT 인증
- PostgreSQL
프론트엔드:
- Next.js 15.5.3 (App Router)
- TypeScript
- Zustand (상태관리)
- Axios (HTTP 클라이언트)
1.3 API Base URL
개발: http://localhost:4000
운영: 환경변수 NEXT_PUBLIC_API_URL 사용
2. 인증 흐름
2.1 JWT 기반 인증
모든 API 요청은 JWT 토큰을 필요로 합니다 (일부 Public 엔드포인트 제외).
전역 Guard 적용:
// backend/src/main.ts
app.useGlobalGuards(new JwtAuthGuard(reflector));
2.2 로그인 프로세스
// 1. 사용자 로그인
const response = await authApi.login({
userId: 'user123',
userPassword: 'password123'
});
// 2. 응답 구조
{
accessToken: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
refreshToken: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
user: {
userNo: 1,
userName: '홍길동',
userEmail: 'hong@example.com',
// ...
}
}
// 3. 토큰 자동 저장 (auth-store.ts에서 처리)
localStorage.setItem('accessToken', response.accessToken);
localStorage.setItem('refreshToken', response.refreshToken);
2.3 자동 토큰 주입
apiClient가 모든 요청에 자동으로 토큰을 추가합니다:
// frontend/src/lib/api-client.ts (자동 처리)
apiClient.interceptors.request.use((config) => {
const token = localStorage.getItem('accessToken');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
개발자는 별도로 토큰을 관리할 필요 없음 ✅
3. 사용자-농장-개체 관계
3.1 데이터 모델
// User Entity
{
pkUserNo: number; // 사용자 번호
userId: string; // 로그인 ID
userName: string; // 이름
userSe: 'FARM' | 'CNSLT' | 'ORGAN'; // 사용자 구분
farms: FarmModel[]; // 소유 농장 목록
}
// Farm Entity
{
pkFarmNo: number; // 농장 번호
fkUserNo: number; // 사용자 번호 (FK)
farmCode: string; // 농장 코드 (F000001)
farmName: string; // 농장명
farmAddress: string; // 농장 주소
cows: CowModel[]; // 농장의 개체 목록
}
// Cow Entity
{
pkCowNo: string; // 개체번호 (12자리)
fkFarmNo: number; // 농장 번호 (FK)
cowSex: 'M' | 'F'; // 성별
cowBirthDt: Date; // 생년월일
cowStatus: string; // 개체 상태
delYn: 'Y' | 'N'; // 삭제 여부
}
3.2 관계 API 호출 순서
// 올바른 순서:
// 1. 로그인한 사용자의 농장 목록 조회
const farms = await farmApi.findAll(); // GET /farm
// 2. 특정 농장의 개체 조회
const cows = await cowApi.findByFarmNo(farms[0].pkFarmNo); // GET /cow/farm/:farmNo
❌ 잘못된 방법: farmNo를 하드코딩
const cows = await cowApi.findByFarmNo(1); // ❌ 다른 사용자의 데이터 접근 불가
4. API 연동 방법
4.1 API 모듈 구조
frontend/src/lib/api/
├── api-client.ts # Axios 인스턴스 + 인터셉터
├── index.ts # 모든 API export
├── auth.api.ts # 인증 API
├── farm.api.ts # 농장 API
├── cow.api.ts # 개체 API
├── kpn.api.ts # KPN API
└── dashboard.api.ts # 대시보드 API
4.2 Import 방법
import { cowApi, farmApi, authApi } from '@/lib/api';
4.3 주요 Farm API
// 1. 현재 사용자의 농장 목록 조회
const farms = await farmApi.findAll();
// GET /farm
// 응답: FarmDto[]
// 2. 농장 상세 조회
const farm = await farmApi.findOne(farmNo);
// GET /farm/:id
// 응답: FarmDto
// 3. 농장 생성
const newFarm = await farmApi.create({
userNo: 1,
farmName: '행복농장',
farmAddress: '충청북도 보은군...',
farmBizNo: '123-45-67890'
});
// POST /farm
4.4 주요 Cow API
// 1. 특정 농장의 개체 목록 조회
const cows = await cowApi.findByFarmNo(farmNo);
// GET /cow/farm/:farmNo
// 응답: CowDto[]
// 2. 개체 상세 조회
const cow = await cowApi.findOne(cowNo);
// GET /cow/:cowNo
// 응답: CowDto
// 3. 개체 검색
const results = await cowApi.search('KOR001', farmNo, 20);
// GET /cow/search?keyword=KOR001&farmNo=1&limit=20
// 응답: CowDto[]
// 4. 개체 랭킹 조회 (필터 + 정렬)
const ranking = await cowApi.getRanking({
filterOptions: {
filters: [/* 필터 조건 */]
},
rankingOptions: {
criteria: 'GENE',
order: 'DESC'
}
});
// POST /cow/ranking
// 응답: RankingResult<CowDto>
5. 주요 구현 사례
5.1 개체 목록 페이지 (/cow/page.tsx)
완전한 구현 예시 (하드코딩 없음):
'use client'
import { useState, useEffect } from 'react'
import { cowApi, farmApi } from '@/lib/api'
import type { Cow } from '@/types/cow.types'
export default function CowListPage() {
const [cows, setCows] = useState<Cow[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
const fetchCows = async () => {
try {
setLoading(true)
setError(null)
// 1단계: 사용자의 농장 목록 조회
const farms = await farmApi.findAll()
if (!farms || farms.length === 0) {
setError('등록된 농장이 없습니다. 농장을 먼저 등록해주세요.')
setLoading(false)
return
}
// 2단계: 첫 번째 농장의 개체 조회
// TODO: 여러 농장이 있을 경우 사용자가 선택할 수 있도록 UI 추가
const farmNo = farms[0].pkFarmNo
const data = await cowApi.findByFarmNo(farmNo)
setCows(data)
} catch (err) {
console.error('개체 데이터 조회 실패:', err)
setError(err instanceof Error ? err.message : '데이터를 불러오는데 실패했습니다')
} finally {
setLoading(false)
}
}
fetchCows()
}, [])
if (loading) return <div>로딩 중...</div>
if (error) return <div>에러: {error}</div>
return (
<div>
<h1>개체 목록</h1>
{cows.map(cow => (
<div key={cow.pkCowNo}>
<p>개체번호: {cow.pkCowNo}</p>
<p>성별: {cow.cowSex === 'M' ? '수소' : '암소'}</p>
</div>
))}
</div>
)
}
5.2 개체 상세 페이지 (/cow/[cowNo]/page.tsx)
'use client'
import { useState, useEffect } from 'react'
import { useParams } from 'next/navigation'
import { cowApi } from '@/lib/api'
import type { Cow } from '@/types/cow.types'
export default function CowDetailPage() {
const params = useParams()
const cowNo = params.cowNo as string
const [cow, setCow] = useState<Cow | null>(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
const fetchCow = async () => {
try {
const data = await cowApi.findOne(cowNo)
setCow(data)
} catch (err) {
console.error('개체 상세 조회 실패:', err)
} finally {
setLoading(false)
}
}
fetchCow()
}, [cowNo])
if (loading) return <div>로딩 중...</div>
if (!cow) return <div>개체를 찾을 수 없습니다</div>
return (
<div>
<h1>개체 상세: {cow.pkCowNo}</h1>
<p>농장번호: {cow.fkFarmNo}</p>
<p>성별: {cow.cowSex === 'M' ? '수소' : '암소'}</p>
<p>생년월일: {cow.cowBirthDt ? new Date(cow.cowBirthDt).toLocaleDateString() : '-'}</p>
</div>
)
}
5.3 Dashboard 통계 페이지
'use client'
import { useState, useEffect } from 'react'
import { farmApi, cowApi } from '@/lib/api'
export default function DashboardPage() {
const [stats, setStats] = useState({
totalFarms: 0,
totalCows: 0,
farms: []
})
useEffect(() => {
const fetchStats = async () => {
try {
// 1. 사용자의 농장 목록 조회
const farms = await farmApi.findAll()
// 2. 각 농장의 개체 수 집계
let totalCows = 0
const farmsWithCows = await Promise.all(
farms.map(async (farm) => {
const cows = await cowApi.findByFarmNo(farm.pkFarmNo)
totalCows += cows.length
return {
...farm,
cowCount: cows.length
}
})
)
setStats({
totalFarms: farms.length,
totalCows,
farms: farmsWithCows
})
} catch (err) {
console.error('통계 조회 실패:', err)
}
}
fetchStats()
}, [])
return (
<div>
<h1>대시보드</h1>
<p>총 농장 수: {stats.totalFarms}개</p>
<p>총 개체 수: {stats.totalCows}마리</p>
{stats.farms.map(farm => (
<div key={farm.pkFarmNo}>
<p>{farm.farmName}: {farm.cowCount}마리</p>
</div>
))}
</div>
)
}
6. 문제 해결
6.1 인증 에러 (401 Unauthorized)
증상:
{"statusCode":401,"message":["인증이 필요합니다. 로그인 후 이용해주세요."]}
원인:
- localStorage에 토큰이 없음
- 토큰 만료
해결 방법:
// 1. 로그인 상태 확인
const { isAuthenticated } = useAuthStore()
if (!isAuthenticated) {
router.push('/login')
return
}
// 2. 토큰 갱신 (필요 시)
await authApi.refreshToken(refreshToken)
6.2 농장이 없는 경우
증상:
등록된 농장이 없습니다.
해결 방법:
if (!farms || farms.length === 0) {
return (
<div>
<p>등록된 농장이 없습니다.</p>
<button onClick={() => router.push('/farm/create')}>
농장 등록하기
</button>
</div>
)
}
6.3 CORS 에러
증상:
Access to XMLHttpRequest has been blocked by CORS policy
해결 방법:
백엔드에서 CORS 설정 확인:
// backend/src/main.ts
app.enableCors({
origin: 'http://localhost:3000',
credentials: true,
});
7. 추가 구현 권장사항
7.1 여러 농장 선택 UI
현재는 첫 번째 농장만 사용하지만, 사용자가 여러 농장을 소유한 경우 선택할 수 있어야 합니다.
구현 예시:
'use client'
import { useState, useEffect } from 'react'
import { farmApi, cowApi } from '@/lib/api'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
export default function CowListPage() {
const [farms, setFarms] = useState([])
const [selectedFarmNo, setSelectedFarmNo] = useState<number | null>(null)
const [cows, setCows] = useState([])
useEffect(() => {
const fetchFarms = async () => {
const data = await farmApi.findAll()
setFarms(data)
// 첫 번째 농장 자동 선택
if (data.length > 0) {
setSelectedFarmNo(data[0].pkFarmNo)
}
}
fetchFarms()
}, [])
useEffect(() => {
if (!selectedFarmNo) return
const fetchCows = async () => {
const data = await cowApi.findByFarmNo(selectedFarmNo)
setCows(data)
}
fetchCows()
}, [selectedFarmNo])
return (
<div>
<Select value={String(selectedFarmNo)} onValueChange={(val) => setSelectedFarmNo(Number(val))}>
<SelectTrigger>
<SelectValue placeholder="농장 선택" />
</SelectTrigger>
<SelectContent>
{farms.map(farm => (
<SelectItem key={farm.pkFarmNo} value={String(farm.pkFarmNo)}>
{farm.farmName} ({farm.farmCode})
</SelectItem>
))}
</SelectContent>
</Select>
<div>
{cows.map(cow => (
<div key={cow.pkCowNo}>{cow.pkCowNo}</div>
))}
</div>
</div>
)
}
7.2 유전자 정보 포함 API 사용
현재 GET /cow/farm/:farmNo는 기본 정보만 반환합니다.
유전자 정보가 필요한 경우 POST /cow/ranking API를 사용하세요.
구현 예시:
// 유전자 정보를 포함한 개체 조회
const ranking = await cowApi.getRanking({
filterOptions: {
filters: [
{
field: 'cow.fkFarmNo',
operator: 'equals',
value: farmNo
}
]
},
rankingOptions: {
criteria: 'GENE',
order: 'DESC'
}
})
// ranking.items에 SNP, Trait 등 모든 관계 데이터 포함됨
const cowsWithGenes = ranking.items
백엔드 참조:
backend/src/cow/cow.service.ts:122-140-createRankingQueryBuilder()- SNP, Trait, Repro, MPT 데이터를 모두 leftJoin으로 포함
7.3 에러 바운더리 추가
// components/ErrorBoundary.tsx
'use client'
import { useEffect } from 'react'
export default function ErrorBoundary({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
useEffect(() => {
console.error('에러 발생:', error)
}, [error])
return (
<div className="flex flex-col items-center justify-center min-h-screen">
<h2 className="text-2xl font-bold mb-4">문제가 발생했습니다</h2>
<p className="mb-4">{error.message}</p>
<button
onClick={reset}
className="px-4 py-2 bg-blue-500 text-white rounded"
>
다시 시도
</button>
</div>
)
}
7.4 로딩 스켈레톤 UI
// components/CowListSkeleton.tsx
export default function CowListSkeleton() {
return (
<div className="grid grid-cols-3 gap-4">
{[1, 2, 3, 4, 5, 6].map((i) => (
<div key={i} className="border rounded p-4 animate-pulse">
<div className="h-6 bg-gray-200 rounded mb-2"></div>
<div className="h-4 bg-gray-200 rounded"></div>
</div>
))}
</div>
)
}
// 사용
if (loading) return <CowListSkeleton />
8. 참고 자료
8.1 백엔드 API 문서
- Cow API:
backend/src/cow/cow.controller.ts - Farm API:
backend/src/farm/farm.controller.ts - Auth API:
backend/src/auth/auth.controller.ts
8.2 프론트엔드 코드 위치
frontend/src/
├── app/
│ ├── cow/
│ │ ├── page.tsx # 개체 목록 (완성)
│ │ └── [cowNo]/page.tsx # 개체 상세
│ └── dashboard/page.tsx # 대시보드
├── lib/api/
│ ├── cow.api.ts # Cow API
│ ├── farm.api.ts # Farm API (신규 추가)
│ └── index.ts # API export
├── types/
│ ├── cow.types.ts # Cow 타입
│ └── auth.types.ts # Auth 타입
└── store/
└── auth-store.ts # 인증 상태 관리
8.3 타입 정의 참조
백엔드 Entity → 프론트엔드 Types 매핑:
| 백엔드 Entity | 프론트엔드 Types | 설명 |
|---|---|---|
UsersModel |
UserDto |
사용자 |
FarmModel |
FarmDto |
농장 |
CowModel |
CowDto |
개체 |
KpnModel |
KpnDto |
KPN |
필드명 주의사항:
- 백엔드:
pkCowNo,fkFarmNo,cowBirthDt,cowSex - 프론트엔드도 동일하게 사용 (DTO 변환 없음)
9. 체크리스트
개발 시 확인사항:
- JWT 토큰이 localStorage에 저장되는가?
- API 호출 시 Authorization 헤더가 자동으로 추가되는가?
- farmNo를 하드코딩하지 않고
farmApi.findAll()로 조회하는가? - 농장이 없는 경우를 처리했는가?
- 에러 발생 시 사용자에게 적절한 메시지를 보여주는가?
- 로딩 상태를 표시하는가?
- 여러 농장이 있는 경우를 고려했는가?
10. 문의
질문이나 문제가 있는 경우:
- 백엔드 API 문서 확인:
backend/doc/기능요구사항전체정리.md - PRD 문서 확인:
E:/repo5/prd/ - 코드 참조:
- 완성된
/cow/page.tsx구현 참조 lib/api-client.ts인터셉터 참조store/auth-store.ts인증 흐름 참조
- 완성된
문서 작성: Claude Code 최종 수정일: 2025-10-26