This commit is contained in:
2025-12-09 17:02:27 +09:00
parent 26f8e1dab2
commit 83127da569
275 changed files with 139682 additions and 1 deletions

View File

@@ -0,0 +1,129 @@
import axios from "axios";
import { useAuthStore } from "@/store/auth-store";
/**
* API 클라이언트
*
* @description
* 백엔드 API와 통신하기 위한 axios 전역 인스턴스
* apiClient는 모든 API 요청을 만들고 baseURL, timeout, 인증 토큰 등을 자동으로 처리
* 요청/응답 인터셉터를 통해 요청구조 및 응답, 에러 처리를 일관되게 관리
* 모듈 로드(import)되는 순간 처음 로드될때 인터셉터들이 생성되어 재사용
*/
const apiClient = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_URL || "http://localhost:4000",
timeout: 60000, // 60초
headers: {
"Content-Type": "application/json", // 기본 요청 본문 타입
},
});
/**
* 요청 인터셉터(request.use)
* 모든 요청에 자동으로 JWT 토큰을 추가
*/
apiClient.interceptors.request.use(
(config) => {
// 상태 스토어에서 토큰 가져오기 (persist가 관리)
const token = useAuthStore.getState().accessToken;
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => {
console.error(" [API 요청 에러]:", error);
return Promise.reject(error);
}
);
/**
* 응답 인터셉터(response.use)
*
* @description
* 1. 백엔드 응답을 자동으로 언래핑(unwrapping)합니다.
* 래핑 응답을 유지하고 메타정보(success, timestamp) 쓰기
* 2. 에러를 일관된 형식으로 처리
*
*/
apiClient.interceptors.response.use(
(response) => {
// 백엔드 TransformInterceptor가 래핑한 응답 자동 언래핑 (backend\src\main.ts)
// axios가 반환하는 response 객체에 기본적으로 서버의 바디가 response.data에 들어감
// Nest의 TransformInterceptor가 그 바디를 다시 { success, data, timestamp }로 감싸줌
// 따라서 data 필드만 반환하도록 처리
// 응답 형식 변경 시 한 곳(api-client)만 수정하면 전체 반영
if (
response.data &&
typeof response.data === "object" &&
"data" in response.data
) {
return response.data.data; // 실제 payload 백엔드 래핑 제거
}
// 그 외의 경우 data를 그대로 반환
return response.data;
},
(error) => {
// 에러 처리
if (error.response) {
// 서버가 응답한 에러
const { status, data, config } = error.response;
const url = config?.url || "unknown";
switch (status) {
case 401:
// 인증 실패 - 로그인 페이지로 리다이렉트
console.error(` [API 401 에러] ${url} - 인증 실패`);
console.error(` - 응답 데이터:`, data);
console.error(` - 에러 메시지:`, data?.message || error.message);
console.error(` - 상태 초기화 및 로그인 페이지로 리다이렉트`);
// 스토어 상태 초기화 (persist가 자동으로 저장소 동기화)
try {
useAuthStore.getState().clearAuth();
} catch (e) {
console.error(" - 상태 초기화 중 오류:", e);
}
if (typeof window !== "undefined") {
console.error(` - 로그인 페이지로 이동`);
window.location.href = "/login";
}
break;
case 403:
console.error(`[API 403] ${url} - 접근 권한이 없습니다.`);
break;
case 404:
console.error(`[API 404] ${url} - 요청한 리소스를 찾을 수 없습니다.`);
break;
case 500:
console.error(`[API 500] ${url} - 서버 오류가 발생했습니다.`);
break;
default:
console.error(`[API ${status}] ${url} - 에러 발생`);
}
// 백엔드 에러 메시지 전달
throw new Error(
data.message?.[0] || data.message || "요청 처리 중 오류가 발생했습니다."
);
} else if (error.request) {
// 요청은 보냈지만 응답을 받지 못한 경우
console.error(
"[API] 서버 응답 없음:",
error.request?.responseURL || "unknown"
);
throw new Error(
"서버와 통신할 수 없습니다. 네트워크 연결을 확인해주세요."
);
} else {
// 요청 설정 중 오류 발생
console.error("[API] 요청 설정 오류:", error.message);
throw new Error(error.message || "알 수 없는 오류가 발생했습니다.");
}
}
);
export default apiClient;

View File

@@ -0,0 +1,134 @@
import apiClient from '../api-client'; // authApi 정의 (auth.api.ts)
import {
SignupDto,
LoginDto,
AuthResponseDto,
UserProfileDto,
UpdateProfileDto,
} from '@/types/auth.types';
/**
* @description 인증 관련 API
* @see ../api-client.ts : apiClient 인스턴스 참조
* @see ../api/index.ts : 통합 Export
* 각 함수는 백엔드의 인증 관련 엔드포인트와 통신
* 인터셉터가 요청/응답을 자동으로 처리
*
*
*/
export const authApi = {
/**
* 로그인 인증
*/
login: async (dto: LoginDto): Promise<AuthResponseDto> => {
// 인터셉터가 자동으로 언래핑
return await apiClient.post('/auth/login', dto);
},
/**
* 회원가입
*/
signup: async (dto: SignupDto): Promise<AuthResponseDto> => {
// 인터셉터가 자동으로 언래핑
return await apiClient.post('/auth/register', dto);
},
/**
* 회원가입 이메일 인증번호 발송
*/
sendSignupCode: async (userEmail: string): Promise<{ success: boolean; message: string; expiresIn: number }> => {
// 인터셉터가 자동으로 언래핑
return await apiClient.post('/auth/signup/send-code', { userEmail });
},
/**
* 회원가입 이메일 인증번호 확인
*/
verifySignupCode: async (userEmail: string, code: string): Promise<{ success: boolean; message: string; verified: boolean }> => {
// 인터셉터가 자동으로 언래핑
return await apiClient.post('/auth/signup/verify-code', { userEmail, code });
},
/**
* 로그아웃
*/
logout: async (): Promise<void> => {
// 백엔드에 로그아웃 엔드포인트가 없으면 클라이언트에서만 처리
// await apiClient.post('/auth/logout');
},
/**
* 내 프로필 조회 : 미구현
*/
getProfile: async (): Promise<UserProfileDto> => {
// 인터셉터가 자동으로 언래핑
return await apiClient.get('/users/profile');
},
/**
* 프로필 수정 : 미구현
*/
updateProfile: async (dto: UpdateProfileDto): Promise<UserProfileDto> => {
// 인터셉터가 자동으로 언래핑
return await apiClient.patch('/users/profile', dto);
},
/**
* 토큰 갱신
*/
refreshToken: async (refreshToken: string): Promise<AuthResponseDto> => {
// 인터셉터가 자동으로 언래핑
return await apiClient.post('/auth/refresh', { refreshToken });
},
/**
* 비밀번호 변경
*/
changePassword: async (oldPassword: string, newPassword: string): Promise<void> => {
await apiClient.post('/auth/reset-password', { oldPassword, newPassword });
},
/**
* 이메일 중복 체크
*/
checkEmail: async (email: string): Promise<{ available: boolean; message: string }> => {
// 인터셉터가 자동으로 언래핑
return await apiClient.get('/auth/check-email', { params: { email } });
},
/**
* 아이디 찾기 - 인증번호 발송
*/
sendFindIdCode: async (userName: string, userEmail: string): Promise<{ success: boolean; message: string; expiresIn: number }> => {
return await apiClient.post('/auth/find-id/send-code', { userName, userEmail });
},
/**
* 아이디 찾기 - 인증번호 검증
*/
verifyFindIdCode: async (userEmail: string, verificationCode: string): Promise<{ success: boolean; message: string; userId: string; maskedUserId: string }> => {
return await apiClient.post('/auth/find-id/verify-code', { userEmail, verificationCode });
},
/**
* 비밀번호 찾기 - 인증번호 발송
*/
sendResetPasswordCode: async (userId: string, userEmail: string): Promise<{ success: boolean; message: string; expiresIn: number }> => {
return await apiClient.post('/auth/reset-password/send-code', { userId, userEmail });
},
/**
* 비밀번호 찾기 - 인증번호 검증
*/
verifyResetPasswordCode: async (userId: string, userEmail: string, verificationCode: string): Promise<{ success: boolean; message: string; resetToken: string }> => {
return await apiClient.post('/auth/reset-password/verify-code', { userId, userEmail, verificationCode });
},
/**
* 비밀번호 재설정
*/
resetPassword: async (resetToken: string, newPassword: string): Promise<{ success: boolean; message: string }> => {
return await apiClient.post('/auth/reset-password', { resetToken, newPassword });
},
};

View File

@@ -0,0 +1,132 @@
import apiClient from '../api-client';
/**
* 교배 조합 저장 관련 API
*/
export interface BreedSave {
pkSaveNo: number;
fkUserNo: number;
fkCowNo: string;
fkKpnNo: string;
saveMemo?: string;
delYn: 'Y' | 'N';
regDt: Date;
updtDt: Date;
scheduledDate?: string; // 교배 예정일 (선택)
completed?: boolean; // 교배 완료 여부 (선택)
completedDate?: string; // 교배 완료일 (선택)
cow?: any;
kpn?: any;
user?: any;
}
export interface CreateBreedSaveDto {
fkUserNo: number;
fkCowNo: string;
fkKpnNo: string;
saveMemo?: string;
scheduledDate?: string;
}
export interface UpdateBreedSaveDto {
saveMemo?: string;
scheduledDate?: string;
completed?: boolean;
completedDate?: string;
}
export interface FilterBreedSaveDto {
fkUserNo?: number;
fkCowNo?: string;
fkKpnNo?: string;
startDate?: string;
endDate?: string;
page?: number;
limit?: number;
sortBy?: string;
sortOrder?: 'ASC' | 'DESC';
}
export const breedApi = {
/**
* POST /breed - 교배 조합 저장
*/
create: async (data: CreateBreedSaveDto): Promise<BreedSave> => {
return await apiClient.post('/breed', data) as unknown as BreedSave;
},
/**
* GET /breed - 교배 조합 목록 조회 (필터링 + 페이징)
*/
findAll: async (filter?: FilterBreedSaveDto): Promise<{
data: BreedSave[];
total: number;
page: number;
limit: number;
}> => {
return await apiClient.get('/breed', { params: filter });
},
/**
* GET /breed/search - 교배 조합 검색
*
* @param keyword - 검색어 (개체번호, KPN번호, 메모)
* @param userNo - 사용자 번호 (선택)
* @param limit - 결과 제한 (기본 20)
*/
search: async (keyword: string, userNo?: number, limit: number = 20): Promise<BreedSave[]> => {
return await apiClient.get('/breed/search', {
params: { keyword, userNo, limit },
});
},
/**
* GET /breed/:id - 교배 조합 단건 조회
*/
findOne: async (id: number): Promise<BreedSave> => {
return await apiClient.get(`/breed/${id}`);
},
/**
* GET /breed/cow/:cowNo - 암소별 교배 조합 조회
*/
findByCow: async (cowNo: string): Promise<BreedSave[]> => {
return await apiClient.get(`/breed/cow/${cowNo}`);
},
/**
* GET /breed/kpn/:kpnNo - KPN별 교배 조합 조회
*/
findByKpn: async (kpnNo: string): Promise<BreedSave[]> => {
return await apiClient.get(`/breed/kpn/${kpnNo}`);
},
/**
* GET /breed/user/:userNo - 사용자별 교배 조합 조회
*/
findByUser: async (userNo: number): Promise<BreedSave[]> => {
return await apiClient.get(`/breed/user/${userNo}`);
},
/**
* GET /breed/date-range/:startDate/:endDate - 날짜 범위로 조회
*/
findByDateRange: async (startDate: string, endDate: string): Promise<BreedSave[]> => {
return await apiClient.get(`/breed/date-range/${startDate}/${endDate}`);
},
/**
* PATCH /breed/:id - 교배 조합 수정
*/
update: async (id: number, data: UpdateBreedSaveDto): Promise<BreedSave> => {
return await apiClient.patch(`/breed/${id}`, data);
},
/**
* DELETE /breed/:id - 교배 조합 삭제 (소프트 삭제)
*/
remove: async (id: number): Promise<void> => {
await apiClient.delete(`/breed/${id}`);
},
};

View File

@@ -0,0 +1,69 @@
import apiClient from "../api-client";
import { RankingRequest } from "@/types/ranking.types";
import { CowDto, CowDetailResponseDto } from "@/types/cow.types";
/**
* 개체(Cow) 관련 API
*/
export const cowApi = {
/**
* GET /cow - 전체 개체 목록
*/
findAll: async (): Promise<CowDto[]> => {
return await apiClient.get("/cow");
},
/**
* GET /cow/paginated - 페이지네이션 목록
*/
findAllWithPagination: async (
page: number = 1,
limit: number = 10
): Promise<{ data: CowDto[]; total: number; page: number; limit: number }> => {
return await apiClient.get("/cow/paginated", {
params: { page, limit },
});
},
/**
* GET /cow/farm/:farmNo - 특정 농장의 개체 목록
*/
findByFarmNo: async (farmNo: number): Promise<CowDto[]> => {
return await apiClient.get(`/cow/farm/${farmNo}`);
},
/**
* GET /cow/search - 개체 검색 (개체번호 또는 개체명)
*/
search: async (
keyword: string,
farmNo?: number,
limit: number = 20
): Promise<CowDto[]> => {
return await apiClient.get("/cow/search", {
params: { keyword, farmNo, limit },
});
},
/**
* GET /cow/:cowNo - 개체 상세 조회
*/
findOne: async (cowNo: string): Promise<CowDetailResponseDto> => {
return await apiClient.get(`/cow/${cowNo}`);
},
/**
* POST /cow/ranking - 암소 랭킹 조회 (필터 + 랭킹 통합)
*/
getRanking: async (rankingRequest: RankingRequest): Promise<any> => {
return await apiClient.post("/cow/ranking", rankingRequest);
},
/**
* POST /cow/ranking/global - 전체 농가 개체 대상 랭킹 조회
*/
getGlobalRanking: async (rankingRequest: RankingRequest): Promise<any> => {
return await apiClient.post("/cow/ranking/global", rankingRequest);
},
};

View File

@@ -0,0 +1,177 @@
import apiClient from '../api-client';
/**
* 대시보드 필터 DTO
*/
export interface DashboardFilterDto {
anlysStatus?: string;
reproType?: string;
geneGrades?: string[];
genomeGrades?: string[];
reproGrades?: string[];
targetGenes?: string[];
minScore?: number;
limit?: number; // 결과 개수 제한 (예: Top 3, Top 5)
regionNm?: string; // 지역명 (예: 보은군)
}
/**
* 개별 소 순위 정보
*/
export interface CattleRankingDto {
cowNo: string; // 개체 번호
cowName: string; // 개체 이름
genomeScore: number; // 유전체 점수
rank: number; // 보은군 내 순위
totalCattle: number; // 보은군 전체 소 수
percentile: number; // 상위 몇 %
}
/**
* 농장 내 소 순위 목록
*/
export interface FarmCattleRankingsDto {
farmNo: number;
farmName: string;
regionName: string;
totalCattle: number; // 보은군 전체 소 수
farmCattleCount: number; // 농장 내 소 수
rankings: CattleRankingDto[];
statistics: {
bestRank: number; // 최고 순위
averageRank: number; // 평균 순위
topPercentCount: number; // 상위 10% 개체 수
};
}
/**
* 대시보드 관련 API
*/
export const dashboardApi = {
/**
* GET /dashboard/summary/:farmNo - 농장 현황 요약
*/
getFarmSummary: async (farmNo: number, filter?: DashboardFilterDto): Promise<any> => {
return await apiClient.get(`/dashboard/summary/${farmNo}`, {
params: filter,
});
},
/**
* GET /dashboard/analysis-completion/:farmNo - 분석 완료 현황
*/
getAnalysisCompletion: async (farmNo: number, filter?: DashboardFilterDto): Promise<any> => {
return await apiClient.get(`/dashboard/analysis-completion/${farmNo}`, {
params: filter,
});
},
/**
* GET /dashboard/evaluation/:farmNo - 농장 종합 평가
*/
getFarmEvaluation: async (farmNo: number, filter?: DashboardFilterDto): Promise<any> => {
return await apiClient.get(`/dashboard/evaluation/${farmNo}`, {
params: filter,
});
},
/**
* GET /dashboard/region-comparison/:farmNo - 보은군 비교 분석
*/
getRegionComparison: async (farmNo: number, filter?: DashboardFilterDto): Promise<any> => {
return await apiClient.get(`/dashboard/region-comparison/${farmNo}`, {
params: filter,
});
},
/**
* GET /dashboard/cow-distribution/:farmNo - 개체 분포 분석
*/
getCowDistribution: async (farmNo: number, filter?: DashboardFilterDto): Promise<any> => {
return await apiClient.get(`/dashboard/cow-distribution/${farmNo}`, {
params: filter,
});
},
/**
* GET /dashboard/kpn-aggregation/:farmNo - KPN 추천 집계
*/
getKpnRecommendationAggregation: async (farmNo: number, filter?: DashboardFilterDto): Promise<any> => {
return await apiClient.get(`/dashboard/kpn-aggregation/${farmNo}`, {
params: filter,
});
},
/**
* GET /dashboard/farm-kpn-inventory/:farmNo - 농장 보유 KPN 목록
*/
getFarmKpnInventory: async (farmNo: number): Promise<any> => {
return await apiClient.get(`/dashboard/farm-kpn-inventory/${farmNo}`);
},
/**
* GET /dashboard/analysis-years/:farmNo - 농장 분석 이력 연도 목록
*/
getAnalysisYears: async (farmNo: number): Promise<number[]> => {
return await apiClient.get(`/dashboard/analysis-years/${farmNo}`);
},
/**
* GET /dashboard/analysis-years/:farmNo/latest - 최신 분석 연도 조회
*/
getLatestAnalysisYear: async (farmNo: number): Promise<number> => {
return await apiClient.get(`/dashboard/analysis-years/${farmNo}/latest`);
},
/**
* GET /dashboard/year-comparison/:farmNo - 3개년 비교 분석
*/
getYearComparison: async (farmNo: number): Promise<any> => {
return await apiClient.get(`/dashboard/year-comparison/${farmNo}`);
},
/**
* GET /dashboard/gene-status/:farmNo - 유전자 보유 현황 분석
*/
getGeneStatus: async (farmNo: number, filter?: DashboardFilterDto): Promise<any> => {
return await apiClient.get(`/dashboard/gene-status/${farmNo}`, {
params: filter,
});
},
/**
* GET /dashboard/repro-efficiency/:farmNo - 번식 효율성 분석
*/
getReproEfficiency: async (farmNo: number, filter?: DashboardFilterDto): Promise<any> => {
return await apiClient.get(`/dashboard/repro-efficiency/${farmNo}`, {
params: filter,
});
},
/**
* GET /dashboard/excellent-cows/:farmNo - 우수개체 추천
*/
getExcellentCows: async (farmNo: number, filter?: DashboardFilterDto): Promise<any> => {
return await apiClient.get(`/dashboard/excellent-cows/${farmNo}`, {
params: filter,
});
},
/**
* GET /dashboard/cull-cows/:farmNo - 도태개체 추천
*/
getCullCows: async (farmNo: number, filter?: DashboardFilterDto): Promise<any> => {
return await apiClient.get(`/dashboard/cull-cows/${farmNo}`, {
params: filter,
});
},
/**
* GET /dashboard/cattle-ranking/:farmNo - 보은군 내 소 개별 순위 조회
*/
getCattleRankingInRegion: async (farmNo: number, filter?: DashboardFilterDto): Promise<FarmCattleRankingsDto> => {
return await apiClient.get(`/dashboard/cattle-ranking/${farmNo}`, {
params: filter,
});
},
};

View File

@@ -0,0 +1,121 @@
import apiClient from '../api-client';
/**
* 농장(Farm) 관련 API
*/
export interface FarmDto {
pkFarmNo: number;
fkUserNo: number;
farmCode: string;
farmName: string;
farmAddress: string;
farmBizNo: string;
farmGovNo?: string;
delYn: 'Y' | 'N';
regDt: Date;
updtDt: Date;
}
export interface FarmPaternityDto {
pkFarmPaternityNo: number;
fkFarmAnlysNo: number;
receiptDate: Date;
farmOwnerName: string;
individualNo: string;
kpnNo: string;
motherIndividualNo: string;
hairRootQuality: string;
remarks: string;
fatherMatch: string; // '일치', '불일치', '정보없음'
motherMatch: string; // '일치', '불일치', '정보없음'
reportDate: Date;
}
export interface FarmAnalysisRequestDto {
pkFarmAnlysNo: number;
fkFileNo: number;
fkFarmNo: number;
farmAnlysNm: string;
anlysReqDt: Date;
region: string;
city: string;
anlysReqCnt: number;
farmAnlysCnt: number;
matchCnt: number;
mismatchCnt: number;
failCnt: number;
noHistCnt: number;
matchRate: number;
msAnlysCnt: number;
anlysRmrk: string;
delYn: 'Y' | 'N';
regDt: Date;
updtDt: Date;
paternities?: FarmPaternityDto[]; // 친자확인 목록
}
export const farmApi = {
/**
* GET /farm - 현재 로그인한 사용자의 농장 목록 조회
*/
findAll: async (): Promise<FarmDto[]> => {
return await apiClient.get('/farm');
},
/**
* GET /farm/:id - 농장 상세 조회
*/
findOne: async (farmNo: number): Promise<FarmDto> => {
return await apiClient.get(`/farm/${farmNo}`);
},
/**
* POST /farm - 농장 생성
*/
create: async (data: {
userNo: number;
farmName: string;
farmAddress: string;
farmBizNo: string;
farmGovNo?: string;
}): Promise<FarmDto> => {
return await apiClient.post('/farm', data);
},
/**
* PATCH /farm/:id - 농장 수정
*/
update: async (
farmNo: number,
data: {
farmName?: string;
farmAddress?: string;
farmBizNo?: string;
farmGovNo?: string;
}
): Promise<FarmDto> => {
return await apiClient.patch(`/farm/${farmNo}`, data);
},
/**
* DELETE /farm/:id - 농장 삭제 (소프트 삭제)
*/
remove: async (farmNo: number): Promise<void> => {
await apiClient.delete(`/farm/${farmNo}`);
},
/**
* GET /farm/:farmNo/analysis-latest - 농장 최신 분석 의뢰 정보 조회
*/
getLatestAnalysisRequest: async (farmNo: number): Promise<FarmAnalysisRequestDto | null> => {
return await apiClient.get(`/farm/${farmNo}/analysis-latest`);
},
/**
* GET /farm/:farmNo/analysis-all - 농장 전체 분석 의뢰 목록 조회
*/
getAllAnalysisRequests: async (farmNo: number): Promise<FarmAnalysisRequestDto[]> => {
return await apiClient.get(`/farm/${farmNo}/analysis-all`);
},
};

View File

@@ -0,0 +1,40 @@
/**
* Gene API (임시 Mock)
* TODO: 백엔드 구현 후 실제 API로 교체
*/
import apiClient from "../api-client";
export interface MarkerModel {
markerNm: string;
markerTypeCd: string; // 'QTY' | 'QLT'
markerDesc?: string;
relatedTrait?: string;
favorableAllele?: string;
}
export const geneApi = {
/**
* 전체 마커 목록 조회 (임시 빈 배열 반환)
*/
getAllMarkers: async (): Promise<MarkerModel[]> => {
// TODO: 백엔드 구현 후 실제 API 연동
return [];
},
/**
* 타입별 마커 목록 조회 (임시 빈 배열 반환)
*/
getGenesByType: async (_typeCd: string): Promise<MarkerModel[]> => {
// TODO: 백엔드 구현 후 실제 API 연동
return [];
},
/**
* 개체별 유전자(SNP) 데이터 조회 (임시 빈 배열 반환)
*/
findByCowNo: async (_cowNo: string | number): Promise<any[]> => {
// TODO: 백엔드 구현 후 실제 API 연동
return [];
},
};

View File

@@ -0,0 +1,367 @@
import apiClient from "../api-client";
import {
GenomeTrait,
GeneticAbility,
GeneticAbilityRequest,
} from "@/types/genome.types";
export interface CategoryAverageDto {
category: string;
avgEbv: number; // 표준화 육종가 평균
avgEpd: number; // EPD (원래 육종가) 평균
count: number;
}
export interface ComparisonAveragesDto {
nationwide: CategoryAverageDto[];
region: CategoryAverageDto[];
farm: CategoryAverageDto[];
}
export interface TraitAverageDto {
traitName: string; // 형질명
category: string; // 카테고리
avgEbv: number; // 평균 EBV (표준화 육종가)
count: number; // 데이터 개수
}
export interface TraitComparisonAveragesDto {
nationwide: TraitAverageDto[];
region: TraitAverageDto[];
farm: TraitAverageDto[];
}
/**
* 유전체(Genome) 관련 API
*/
/**
* 유전체 분석 의뢰 정보 타입
*/
export interface GenomeRequestDto {
pkRequestNo: number;
fkFarmNo: number | null;
fkCowNo: number | null;
cowRemarks: string | null;
requestDt: string | null; // 접수일자
snpTest: string | null; // SNP 검사
msTest: string | null; // MS 검사
sampleAmount: string | null; // 모근량
sampleRemarks: string | null; // 모근 비고
chipNo: string | null; // 분석 Chip 번호
chipType: string | null; // 분석 칩 종류
chipInfo: string | null; // 칩정보
chipRemarks: string | null; // 칩 비고
chipSireName: string | null; // 칩분석 아비명 (친자감별 결과)
chipDamName: string | null; // 칩분석 어미명
chipReportDt: string | null; // 칩분석 보고일자
msResultStatus: string | null; // MS 감정결과
msFatherEstimate: string | null; // MS 추정부
msReportDt: string | null; // MS 보고일자
regDt?: string;
modDt?: string;
}
export const genomeApi = {
/**
* GET /genome - 모든 유전체 데이터 조회
*/
findAll: async (): Promise<GenomeTrait[]> => {
return await apiClient.get("/genome");
},
/**
* GET /genome/request/:cowNo - 개체식별번호로 유전체 분석 의뢰 정보 조회
*
* @param cowNo - 개체식별번호 (예: KOR002115897818)
*/
getRequest: async (cowNo: string | number): Promise<GenomeRequestDto | null> => {
return await apiClient.get(`/genome/request/${cowNo}`);
},
/**
* GET /genome/:cowNo - 특정 개체의 유전체 데이터 조회
*
* @param cowNo - 개체 번호
*/
findByCowNo: async (cowNo: string | number): Promise<GenomeTrait[]> => {
return await apiClient.get(`/genome/${cowNo}`);
},
/**
* POST /genome/:cowNo/genetic-ability - 개체의 유전능력 평가 조회
*
* @param cowNo - 개체 번호
* @param request - 사용자가 선택한 유전체 형질과 가중치
*/
getGeneticAbility: async (
cowNo: string | number,
request: GeneticAbilityRequest = {}
): Promise<GeneticAbility> => {
return await apiClient.post(`/genome/${cowNo}/genetic-ability`, request);
},
/**
* GET /genome/comparison-averages/:cowNo - 전국/지역/농장 카테고리별 평균 비교
*
* @param cowNo - 개체 번호 (KOR...)
*/
getComparisonAverages: async (
cowNo: string | number
): Promise<ComparisonAveragesDto> => {
return await apiClient.get(`/genome/comparison-averages/${cowNo}`);
},
/**
* GET /genome/trait-comparison-averages/:cowNo - 전국/지역/농장 형질별 평균 비교
* (폴리곤 차트용 - 형질 단위 비교)
*
* @param cowNo - 개체 번호 (KOR...)
*/
getTraitComparisonAverages: async (
cowNo: string | number
): Promise<TraitComparisonAveragesDto> => {
return await apiClient.get(`/genome/trait-comparison-averages/${cowNo}`);
},
/**
* POST /genome/selection-index/:cowId - 선발지수(가중 평균) 계산
*/
getSelectionIndex: async (
cowId: string,
traitConditions: { traitNm: string; weight?: number }[]
): Promise<{
score: number | null;
percentile: number | null;
farmRank: number | null; // 농가 순위
farmTotal: number; // 농가 전체 수
regionRank: number | null; // 지역(보은군) 순위
regionTotal: number; // 지역 전체 수
regionName: string | null; // 지역명
farmerName: string | null; // 농가명 (농장주명)
farmAvgScore: number | null; // 농가 평균 선발지수
regionAvgScore: number | null; // 보은군(지역) 평균 선발지수
details: { traitNm: string; ebv: number; weight: number; contribution: number }[];
message?: string;
}> => {
return await apiClient.post(`/genome/selection-index/${cowId}`, { traitConditions });
},
/**
* GET /genome/dashboard-stats/:farmNo - 대시보드용 유전체 통계
*/
getDashboardStats: async (farmNo: number): Promise<DashboardStatsDto> => {
return await apiClient.get(`/genome/dashboard-stats/${farmNo}`);
},
/**
* GET /genome/farm-trait-comparison/:farmNo - 농가별 형질 비교 (농가 vs 지역 vs 전국)
*/
getFarmTraitComparison: async (farmNo: number): Promise<FarmTraitComparisonDto> => {
return await apiClient.get(`/genome/farm-trait-comparison/${farmNo}`);
},
/**
* GET /genome/farm-region-ranking/:farmNo - 농가의 보은군 내 순위 조회 (대시보드용)
*/
getFarmRegionRanking: async (farmNo: number): Promise<FarmRegionRankingDto> => {
return await apiClient.get(`/genome/farm-region-ranking/${farmNo}`);
},
/**
* GET /genome/trait-rank/:cowId/:traitName - 개별 형질 기준 순위 조회
*/
getTraitRank: async (cowId: string, traitName: string): Promise<TraitRankDto> => {
return await apiClient.get(`/genome/trait-rank/${cowId}/${encodeURIComponent(traitName)}`);
},
/**
* GET /genome/yearly-trait-trend/:farmNo - 연도별 유전능력 추이 (형질별/카테고리별)
* @param farmNo - 농장 번호
* @param category - 카테고리명 (성장/생산/체형/무게/비율)
* @param traitName - 형질명 (선택, 없으면 카테고리 전체 평균)
*/
getYearlyTraitTrend: async (
farmNo: number,
category: string,
traitName?: string
): Promise<YearlyTraitTrendDto> => {
const params = new URLSearchParams({ category });
if (traitName) params.append('traitName', traitName);
return await apiClient.get(`/genome/yearly-trait-trend/${farmNo}?${params.toString()}`);
},
};
/**
* 연도별 유전능력 추이 데이터 타입
*/
export interface YearlyTraitTrendDto {
category: string;
traitName: string | null; // null이면 카테고리 전체 평균
yearlyData: {
year: number;
farmAvgEbv: number;
regionAvgEbv: number;
farmCount: number;
regionCount: number;
}[];
traitList: string[]; // 해당 카테고리 내 형질 목록
// 농가 순위 정보
farmRank: {
rank: number | null; // 보은군 내 순위
totalFarms: number; // 전체 농가 수
percentile: number | null; // 상위 백분위
farmAvgEbv: number | null; // 농가 평균 EBV
regionAvgEbv: number; // 보은군 평균 EBV
};
}
/**
* 개별 형질 기준 순위 데이터 타입
*/
export interface TraitRankDto {
traitName: string;
cowEbv: number | null;
farmRank: number | null;
farmTotal: number;
regionRank: number | null;
regionTotal: number;
farmAvgEbv: number | null;
regionAvgEbv: number | null;
}
/**
* 농가의 보은군 내 순위 데이터 타입
*/
export interface FarmRegionRankingDto {
farmNo: number;
farmerName: string | null;
farmAvgScore: number | null;
regionAvgScore: number | null;
farmRankInRegion: number | null;
totalFarmsInRegion: number;
percentile: number | null;
farmCowCount: number;
regionCowCount: number;
}
/**
* 농가별 형질 비교 데이터 타입
*/
export interface FarmTraitComparisonDto {
farmName: string;
regionName: string;
totalFarmAnimals: number;
totalRegionAnimals: number;
traits: {
traitName: string;
category: string;
// 농가 데이터
farmAvgEbv: number;
farmCount: number;
farmPercentile: number;
// 지역 데이터
regionAvgEbv: number;
regionCount: number;
// 전국 데이터
nationAvgEbv: number;
nationCount: number;
// 비교
diffFromRegion: number;
diffFromNation: number;
}[];
}
/**
* 대시보드 통계 데이터 타입
*/
export interface DashboardStatsDto {
// 연도별 분석 현황
yearlyStats: {
year: number;
totalRequests: number;
analyzedCount: number;
pendingCount: number;
sireMatchCount: number; // 친자 일치 수
analyzeRate: number; // 분석 완료율 (%)
sireMatchRate: number; // 친자 일치율 (%)
}[];
// 형질별 농장 평균
traitAverages: {
traitName: string;
category: string;
avgEbv: number;
avgEpd: number; // 육종가(EPD) 평균
avgPercentile: number;
count: number;
rank: number | null; // 보은군 내 농가 순위
totalFarms: number; // 보은군 내 총 농가 수
percentile: number | null; // 상위 백분율
}[];
// 접수 내역 목록
requestHistory: {
pkRequestNo: number;
cowId: string;
cowRemarks: string | null;
requestDt: string | null;
chipSireName: string | null;
chipReportDt: string | null;
status: string;
}[];
// 요약
summary: {
totalRequests: number;
analyzedCount: number;
pendingCount: number;
mismatchCount: number;
maleCount: number; // 수컷 수
femaleCount: number; // 암컷 수
};
// 검사 종류별 현황
testTypeStats: {
snp: { total: number; completed: number };
ms: { total: number; completed: number };
};
// 친자감별 결과 현황 (상호 배타적 분류)
paternityStats: {
analysisComplete: number; // 분석 완료 (부 일치 + 모 일치/null/정보없음)
sireMismatch: number; // 부 불일치
damMismatch: number; // 모 불일치 (부 일치 + 모 불일치)
damNoRecord: number; // 모 이력제부재 (부 일치 + 모 이력제부재)
pending: number; // 대기
};
// 월별 접수 현황
monthlyStats: {
month: number;
count: number;
}[];
// 칩 종류별 분포
chipTypeStats: {
chipType: string;
count: number;
}[];
// 모근량별 분포
sampleAmountStats: {
sampleAmount: string;
count: number;
}[];
// 연도별 주요 형질 평균 (차트용)
yearlyTraitAverages: {
year: number;
traits: { traitName: string; avgEbv: number | null }[];
}[];
// 연도별 평균 표준화육종가 (농가 vs 보은군 비교)
yearlyAvgEbv: {
year: number;
farmAvgEbv: number; // 농가 평균
regionAvgEbv: number; // 보은군 평균
traitCount: number;
}[];
// 우수 개체 TOP 5 (옵션)
topAnimals?: {
animalId?: string;
identNo?: string;
birthDt?: string;
avgEbv?: number;
}[];
}

View File

@@ -0,0 +1,21 @@
/**
* API 모듈 통합 Export
*
* @description
* 모든 API 함수를 중앙에서 관리하고 export합니다.
* 한 곳에서 내보내기 다른 파일에서 @/lib/api로 import하면 자동으로 index.ts를 통해 관리
*
* @example
* import { cowApi, authApi } from '@/lib/api';
*/
export { authApi } from './auth.api'; // 인증 API
export { cowApi } from './cow.api';
export { dashboardApi } from './dashboard.api';
export { farmApi } from './farm.api';
export { genomeApi, type ComparisonAveragesDto, type CategoryAverageDto, type FarmTraitComparisonDto, type TraitComparisonAveragesDto, type TraitAverageDto } from './genome.api';
export { reproApi } from './repro.api';
export { breedApi } from './breed.api';
// API 클라이언트도 export (필요 시 직접 사용 가능)
export { default as apiClient } from '../api-client';

View File

@@ -0,0 +1,31 @@
import apiClient from '../api-client';
import { ReproMpt } from '@/types/reprompt.types';
/**
* 번식정보(Reproduction) 관련 API - MPT(혈액검사) 전용
*/
export const reproApi = {
/**
* GET /repro/mpt - 전체 혈액검사(MPT) 정보 조회
*/
findAllMpt: async (): Promise<ReproMpt[]> => {
return await apiClient.get('/repro/mpt');
},
/**
* GET /repro/mpt/:cowNo - 특정 개체의 혈액검사 정보 조회
*
* @param cowNo - 개체 번호
*/
findMptByCowNo: async (cowNo: string | number): Promise<ReproMpt[]> => {
return await apiClient.get(`/repro/mpt/${cowNo}`);
},
/**
* GET /repro/mpt-average/category - 전체 농가 MPT 평균 (카테고리별)
*/
getMptAverageByCategory: async (): Promise<Array<{ category: string; score: number }>> => {
return await apiClient.get('/repro/mpt-average/category');
},
};

View File

@@ -0,0 +1,29 @@
/**
* 차트 색상 및 스타일 정의
*/
export const CHART_COLORS = {
primary: '#3b82f6',
secondary: '#8b5cf6',
success: '#10b981',
warning: '#f59e0b',
danger: '#ef4444',
info: '#06b6d4',
muted: '#94a3b8',
grid: '#e2e8f0',
}
export const TOOLTIP_STYLE = {
contentStyle: {
backgroundColor: 'white',
border: '1px solid #e2e8f0',
borderRadius: '8px',
boxShadow: '0 4px 6px -1px rgba(0, 0, 0, 0.1)',
},
}
export const AXIS_STYLE = {
axisLine: { stroke: '#e2e8f0' },
tickLine: { stroke: '#e2e8f0' },
tick: { fill: '#64748b', fontSize: 12 },
}

24
frontend/src/lib/utils.ts Normal file
View File

@@ -0,0 +1,24 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
/**
* 개체번호를 포맷팅합니다 (예: "001122334401" -> "001 1223 3440 1")
* @param cowNo 개체번호
* @returns 포맷팅된 개체번호
*/
export function formatCowNo(cowNo: string | undefined | null): string {
if (!cowNo) return ''
// 숫자만 추출
const cleaned = cowNo.replace(/\D/g, '')
// 12자리가 아니면 원본 반환
if (cleaned.length !== 12) return cowNo
// 3-4-4-1 형식으로 포맷팅
return `${cleaned.slice(0, 3)} ${cleaned.slice(3, 7)} ${cleaned.slice(7, 11)} ${cleaned.slice(11)}`
}

View File

@@ -0,0 +1,127 @@
/**
* 유전체 분석 유효성 조건 설정
*
* @description
* 유전체 분석 데이터가 유효한지 판단하는 조건 정의
* 백엔드 GenomeAnalysisConfig.ts와 동일한 로직 유지
*
* =================유효 조건=======================
* 1. chipSireName === '일치' (아비 칩 데이터 일치)
* 2. chipDamName !== '불일치' (어미 칩 데이터 불일치가 아님)
* 3. chipDamName !== '이력제부재' (어미 이력제 부재가 아님)
* 4. cowId가 EXCLUDED_COW_IDS에 포함되지 않음
*
* 제외되는 경우:
* - chipSireName !== '일치' (아비 불일치, 이력제부재 등)
* - chipDamName === '불일치' (어미 불일치)
* - chipDamName === '이력제부재' (어미 이력제 부재)
* - 분석불가 개체 (EXCLUDED_COW_IDS)
*/
/** 유효한 아비 칩 이름 값 */
export const VALID_CHIP_SIRE_NAME = '일치';
/** 제외할 어미 칩 이름 값 목록 */
export const INVALID_CHIP_DAM_NAMES = ['불일치', '이력제부재'];
/** 개별 제외 개체 목록 (분석불가 등 특수 사유) */
export const EXCLUDED_COW_IDS = [
'KOR002191642861', // 모근량 갯수 적은데 1회분은 가능 아비명도 일치하는데 유전체 분석 정보가 없음
];
/**
* 유전체 분석 데이터 유효성 검사
*
* @param chipSireName - 아비 칩 이름 (친자감별 결과)
* @param chipDamName - 어미 칩 이름 (친자감별 결과)
* @param cowId - 개체식별번호 (선택, 개별 제외 목록 확인용)
* @returns 유효한 분석 데이터인지 여부
*
* @example
* // 유효한 경우
* isValidGenomeAnalysis('일치', '일치') // true
* isValidGenomeAnalysis('일치', null) // true
* isValidGenomeAnalysis('일치', '정보없음') // true
*
* // 유효하지 않은 경우
* isValidGenomeAnalysis('불일치', '일치') // false (아비 불일치)
* isValidGenomeAnalysis('일치', '불일치') // false (어미 불일치)
* isValidGenomeAnalysis('일치', '이력제부재') // false (어미 이력제부재)
* isValidGenomeAnalysis('일치', '일치', 'KOR002191642861') // false (제외 개체)
*/
export function isValidGenomeAnalysis(
chipSireName: string | null | undefined,
chipDamName: string | null | undefined,
cowId?: string | null,
): boolean {
// 1. 아비 일치 확인
if (chipSireName !== VALID_CHIP_SIRE_NAME) return false;
// 2. 어미 제외 조건 확인
if (chipDamName && INVALID_CHIP_DAM_NAMES.includes(chipDamName)) return false;
// 3. 개별 제외 개체 확인
if (cowId && EXCLUDED_COW_IDS.includes(cowId)) return false;
return true;
}
/**
* 유효성 검사 실패 사유 반환
*
* @param chipSireName - 아비 칩 이름
* @param chipDamName - 어미 칩 이름
* @param cowId - 개체식별번호
* @returns 실패 사유 문자열 또는 null (유효한 경우)
*/
export function getInvalidReason(
chipSireName: string | null | undefined,
chipDamName: string | null | undefined,
cowId?: string | null,
): string | null {
if (chipSireName !== VALID_CHIP_SIRE_NAME) {
if (!chipSireName) return '친자확인 정보 없음';
return '부 KPN 친자 불일치';
}
if (chipDamName && INVALID_CHIP_DAM_NAMES.includes(chipDamName)) {
if (chipDamName === '이력제부재') return '모 이력제 부재';
return '모 친자 불일치';
}
if (cowId && EXCLUDED_COW_IDS.includes(cowId)) {
return '분석 불가 개체';
}
return null;
}
/**
* 유효성 검사 실패 시 안내 메시지 반환
*
* @param chipSireName - 아비 칩 이름
* @param chipDamName - 어미 칩 이름
* @param cowId - 개체식별번호
* @returns 안내 메시지
*/
export function getInvalidMessage(
chipSireName: string | null | undefined,
chipDamName: string | null | undefined,
cowId?: string | null,
): string {
if (chipSireName !== VALID_CHIP_SIRE_NAME) {
if (!chipSireName) return '친자확인 정보가 없어 유전체 분석 보고서를 제공할 수 없습니다.';
return '부 친자감별 결과가 불일치하여 유전체 분석 보고서를 제공할 수 없습니다.';
}
if (chipDamName && INVALID_CHIP_DAM_NAMES.includes(chipDamName)) {
if (chipDamName === '이력제부재') return '모 이력제 정보가 부재하여 유전체 분석 보고서를 제공할 수 없습니다.';
return '모 친자감별 결과가 불일치하여 유전체 분석 보고서를 제공할 수 없습니다.';
}
if (cowId && EXCLUDED_COW_IDS.includes(cowId)) {
return '해당 개체는 분석 불가 사유로 인해 유전체 분석 보고서를 제공할 수 없습니다.';
}
return '유전체 분석 보고서를 제공할 수 없습니다.';
}