INIT
This commit is contained in:
129
frontend/src/lib/api-client.ts
Normal file
129
frontend/src/lib/api-client.ts
Normal 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;
|
||||
134
frontend/src/lib/api/auth.api.ts
Normal file
134
frontend/src/lib/api/auth.api.ts
Normal 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 });
|
||||
},
|
||||
};
|
||||
132
frontend/src/lib/api/breed.api.ts
Normal file
132
frontend/src/lib/api/breed.api.ts
Normal 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}`);
|
||||
},
|
||||
};
|
||||
69
frontend/src/lib/api/cow.api.ts
Normal file
69
frontend/src/lib/api/cow.api.ts
Normal 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);
|
||||
},
|
||||
|
||||
};
|
||||
177
frontend/src/lib/api/dashboard.api.ts
Normal file
177
frontend/src/lib/api/dashboard.api.ts
Normal 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,
|
||||
});
|
||||
},
|
||||
};
|
||||
121
frontend/src/lib/api/farm.api.ts
Normal file
121
frontend/src/lib/api/farm.api.ts
Normal 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`);
|
||||
},
|
||||
};
|
||||
40
frontend/src/lib/api/gene.api.ts
Normal file
40
frontend/src/lib/api/gene.api.ts
Normal 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 [];
|
||||
},
|
||||
};
|
||||
367
frontend/src/lib/api/genome.api.ts
Normal file
367
frontend/src/lib/api/genome.api.ts
Normal 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;
|
||||
}[];
|
||||
}
|
||||
21
frontend/src/lib/api/index.ts
Normal file
21
frontend/src/lib/api/index.ts
Normal 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';
|
||||
31
frontend/src/lib/api/repro.api.ts
Normal file
31
frontend/src/lib/api/repro.api.ts
Normal 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');
|
||||
},
|
||||
};
|
||||
29
frontend/src/lib/chart-colors.ts
Normal file
29
frontend/src/lib/chart-colors.ts
Normal 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
24
frontend/src/lib/utils.ts
Normal 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)}`
|
||||
}
|
||||
127
frontend/src/lib/utils/genome-analysis-config.ts
Normal file
127
frontend/src/lib/utils/genome-analysis-config.ts
Normal 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 '유전체 분석 보고서를 제공할 수 없습니다.';
|
||||
}
|
||||
Reference in New Issue
Block a user