# 프론트엔드 API 연동 가이드 > **작성일**: 2025-10-26 > **버전**: 1.0 > **대상**: 한우 유전체 분석 시스템 프론트엔드 개발자 ## 📋 목차 1. [개요](#1-개요) 2. [인증 흐름](#2-인증-흐름) 3. [사용자-농장-개체 관계](#3-사용자-농장-개체-관계) 4. [API 연동 방법](#4-api-연동-방법) 5. [주요 구현 사례](#5-주요-구현-사례) 6. [문제 해결](#6-문제-해결) 7. [추가 구현 권장사항](#7-추가-구현-권장사항) --- ## 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 적용**: ```typescript // backend/src/main.ts app.useGlobalGuards(new JwtAuthGuard(reflector)); ``` ### 2.2 로그인 프로세스 ```typescript // 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`가 모든 요청에 자동으로 토큰을 추가합니다: ```typescript // 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 데이터 모델 ```typescript // 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 호출 순서 ```typescript // 올바른 순서: // 1. 로그인한 사용자의 농장 목록 조회 const farms = await farmApi.findAll(); // GET /farm // 2. 특정 농장의 개체 조회 const cows = await cowApi.findByFarmNo(farms[0].pkFarmNo); // GET /cow/farm/:farmNo ``` **❌ 잘못된 방법**: farmNo를 하드코딩 ```typescript 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 방법 ```typescript import { cowApi, farmApi, authApi } from '@/lib/api'; ``` ### 4.3 주요 Farm API ```typescript // 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 ```typescript // 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 ``` --- ## 5. 주요 구현 사례 ### 5.1 개체 목록 페이지 (/cow/page.tsx) **완전한 구현 예시** (하드코딩 없음): ```typescript '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([]) const [loading, setLoading] = useState(true) const [error, setError] = useState(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
로딩 중...
if (error) return
에러: {error}
return (

개체 목록

{cows.map(cow => (

개체번호: {cow.pkCowNo}

성별: {cow.cowSex === 'M' ? '수소' : '암소'}

))}
) } ``` ### 5.2 개체 상세 페이지 (/cow/[cowNo]/page.tsx) ```typescript '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(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
로딩 중...
if (!cow) return
개체를 찾을 수 없습니다
return (

개체 상세: {cow.pkCowNo}

농장번호: {cow.fkFarmNo}

성별: {cow.cowSex === 'M' ? '수소' : '암소'}

생년월일: {cow.cowBirthDt ? new Date(cow.cowBirthDt).toLocaleDateString() : '-'}

) } ``` ### 5.3 Dashboard 통계 페이지 ```typescript '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 (

대시보드

총 농장 수: {stats.totalFarms}개

총 개체 수: {stats.totalCows}마리

{stats.farms.map(farm => (

{farm.farmName}: {farm.cowCount}마리

))}
) } ``` --- ## 6. 문제 해결 ### 6.1 인증 에러 (401 Unauthorized) **증상**: ``` {"statusCode":401,"message":["인증이 필요합니다. 로그인 후 이용해주세요."]} ``` **원인**: - localStorage에 토큰이 없음 - 토큰 만료 **해결 방법**: ```typescript // 1. 로그인 상태 확인 const { isAuthenticated } = useAuthStore() if (!isAuthenticated) { router.push('/login') return } // 2. 토큰 갱신 (필요 시) await authApi.refreshToken(refreshToken) ``` ### 6.2 농장이 없는 경우 **증상**: ``` 등록된 농장이 없습니다. ``` **해결 방법**: ```typescript if (!farms || farms.length === 0) { return (

등록된 농장이 없습니다.

) } ``` ### 6.3 CORS 에러 **증상**: ``` Access to XMLHttpRequest has been blocked by CORS policy ``` **해결 방법**: 백엔드에서 CORS 설정 확인: ```typescript // backend/src/main.ts app.enableCors({ origin: 'http://localhost:3000', credentials: true, }); ``` --- ## 7. 추가 구현 권장사항 ### 7.1 여러 농장 선택 UI 현재는 첫 번째 농장만 사용하지만, 사용자가 여러 농장을 소유한 경우 선택할 수 있어야 합니다. **구현 예시**: ```typescript '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(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 (
{cows.map(cow => (
{cow.pkCowNo}
))}
) } ``` ### 7.2 유전자 정보 포함 API 사용 현재 `GET /cow/farm/:farmNo`는 기본 정보만 반환합니다. 유전자 정보가 필요한 경우 `POST /cow/ranking` API를 사용하세요. **구현 예시**: ```typescript // 유전자 정보를 포함한 개체 조회 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 에러 바운더리 추가 ```typescript // 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 (

문제가 발생했습니다

{error.message}

) } ``` ### 7.4 로딩 스켈레톤 UI ```typescript // components/CowListSkeleton.tsx export default function CowListSkeleton() { return (
{[1, 2, 3, 4, 5, 6].map((i) => (
))}
) } // 사용 if (loading) return ``` --- ## 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. 문의 질문이나 문제가 있는 경우: 1. 백엔드 API 문서 확인: `backend/doc/기능요구사항전체정리.md` 2. PRD 문서 확인: `E:/repo5/prd/` 3. 코드 참조: - 완성된 `/cow/page.tsx` 구현 참조 - `lib/api-client.ts` 인터셉터 참조 - `store/auth-store.ts` 인증 흐름 참조 --- **문서 작성**: Claude Code **최종 수정일**: 2025-10-26