update_cow_list_ui

This commit is contained in:
NYD
2026-01-07 15:13:42 +09:00
parent 0780f2e47c
commit dae3808221
10 changed files with 1081 additions and 502 deletions

View File

@@ -806,7 +806,7 @@ export class AdminService {
}
return true;
} catch (error) {
this.logger.error(`[배치업로드] 데이터 저장 실패 (cowId: ${item.cowId}, testDt: ${item.testDt}): ${error.message}`);
this.logger.error(`[배치업로드] 데이터 저장 실패 (cowId: ${item.cowId}, ${error.message}`);
return false;
}
});

View File

@@ -3,6 +3,8 @@ import { TypeOrmModule } from '@nestjs/typeorm';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { UserModel } from '../user/entities/user.entity';
import { FarmModel } from '../farm/entities/farm.entity';
import { GenomeRequestModel } from '../genome/entities/genome-request.entity';
import { JwtModule } from 'src/common/jwt/jwt.module';
import { EmailModule } from 'src/shared/email/email.module';
import { VerificationModule } from 'src/shared/verification/verification.module';
@@ -13,7 +15,7 @@ import { VerificationModule } from 'src/shared/verification/verification.module'
*/
@Module({
imports: [
TypeOrmModule.forFeature([UserModel]),
TypeOrmModule.forFeature([UserModel, FarmModel, GenomeRequestModel]),
JwtModule,
EmailModule,
VerificationModule,

View File

@@ -7,7 +7,9 @@ import {
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { UserModel } from '../user/entities/user.entity';
import { Repository } from 'typeorm';
import { FarmModel } from '../farm/entities/farm.entity';
import { GenomeRequestModel } from '../genome/entities/genome-request.entity';
import { Repository, IsNull } from 'typeorm';
import { LoginDto } from './dto/login.dto';
import { LoginResponseDto } from './dto/login-response.dto';
import { SignupDto } from './dto/signup.dto';
@@ -36,6 +38,10 @@ export class AuthService {
constructor(
@InjectRepository(UserModel)
private readonly userRepository: Repository<UserModel>,
@InjectRepository(FarmModel)
private readonly farmRepository: Repository<FarmModel>,
@InjectRepository(GenomeRequestModel)
private readonly genomeRequestRepository: Repository<GenomeRequestModel>,
private readonly emailService: EmailService,
private readonly verificationService: VerificationService,
private readonly jwtService: JwtService,
@@ -78,7 +84,10 @@ export class AuthService {
const accessToken = this.jwtService.sign(payload as any);
this.logger.log(`[LOGIN] 로그인 성공 - userId: ${userId}`);
// 최근 검사 년도 조회
const defaultAnalysisYear = await this.getDefaultAnalysisYear(user.pkUserNo);
this.logger.log(`[LOGIN] 로그인 성공 - userId: ${userId}, defaultAnalysisYear: ${defaultAnalysisYear}`);
return {
message: '로그인 성공',
@@ -90,9 +99,61 @@ export class AuthService {
userEmail: user.userEmail,
userRole: user.userRole || 'USER',
},
defaultAnalysisYear,
};
}
/**
* 사용자의 최근 검사 년도 조회
* @param userNo - 사용자 번호
* @returns 최근 검사 년도 (없으면 현재 년도)
*/
private async getDefaultAnalysisYear(userNo: number): Promise<number> {
try {
// 1. 사용자의 농장 번호 조회
const farm = await this.farmRepository.findOne({
where: { fkUserNo: userNo, delDt: IsNull() },
select: ['pkFarmNo'],
});
if (!farm) {
this.logger.log(`[getDefaultAnalysisYear] userNo: ${userNo}, No farm found, returning current year`);
return new Date().getFullYear();
}
// 2. 농장의 검사 이력에서 최신 날짜 조회
const result = await this.genomeRequestRepository
.createQueryBuilder('request')
.select('MAX(request.chipReportDt)', 'maxChipDt')
.addSelect('MAX(request.msReportDt)', 'maxMsDt')
.where('request.fkFarmNo = :farmNo', { farmNo: farm.pkFarmNo })
.andWhere('request.delDt IS NULL')
.getRawOne();
const maxChipDt = result?.maxChipDt ? new Date(result.maxChipDt) : null;
const maxMsDt = result?.maxMsDt ? new Date(result.maxMsDt) : null;
// 둘 중 최신 날짜 선택
let latestDate: Date | null = null;
if (maxChipDt && maxMsDt) {
latestDate = maxChipDt > maxMsDt ? maxChipDt : maxMsDt;
} else if (maxChipDt) {
latestDate = maxChipDt;
} else if (maxMsDt) {
latestDate = maxMsDt;
}
const year = latestDate ? latestDate.getFullYear() : new Date().getFullYear();
this.logger.log(`[getDefaultAnalysisYear] userNo: ${userNo}, farmNo: ${farm.pkFarmNo}, maxChipDt: ${maxChipDt?.toISOString()}, maxMsDt: ${maxMsDt?.toISOString()}, year: ${year}`);
return year;
} catch (error) {
this.logger.error(`[getDefaultAnalysisYear] Error: ${error.message}`);
return new Date().getFullYear();
}
}
/**
* 회원가입
*/

View File

@@ -11,4 +11,5 @@ export class LoginResponseDto {
userEmail: string;
userRole: 'USER' | 'ADMIN';
};
defaultAnalysisYear: number; // 최근 검사 년도
}

View File

@@ -115,6 +115,16 @@ export class GenomeController {
return this.genomeService.getYearlyTraitTrend(+farmNo, category, traitName);
}
/**
* GET /genome/latest-analysis-year/:farmNo
* 농장의 가장 최근 분석 연도 조회 (chip_report_dt 또는 ms_report_dt 기준)
* @param farmNo - 농장 번호
*/
@Get('latest-analysis-year/:farmNo')
getLatestAnalysisYear(@Param('farmNo') farmNo: string) {
return this.genomeService.getLatestAnalysisYear(+farmNo);
}
/**
* GET /genome/:cowId
* cowId(개체식별번호)로 유전체 데이터 조회

View File

@@ -1874,4 +1874,69 @@ export class GenomeService {
},
};
}
/**
* 농장의 가장 최근 분석 연도 조회
* chip_report_dt 또는 ms_report_dt 중 가장 최근 날짜의 년도 반환
* 둘 다 없으면 현재 년도 반환
*
* @param farmNo - 농장 번호
* @returns { year: number } - 가장 최근 분석 연도
*/
async getLatestAnalysisYear(farmNo: number): Promise<{ year: number }> {
console.log(`[getLatestAnalysisYear] farmNo: ${farmNo}`);
// 농장의 모든 분석 의뢰 조회
const requests = await this.genomeRequestRepository.find({
where: { fkFarmNo: farmNo, delDt: IsNull() },
select: ['chipReportDt', 'msReportDt'],
});
console.log(`[getLatestAnalysisYear] Found ${requests?.length || 0} requests`);
if (!requests || requests.length === 0) {
console.log('[getLatestAnalysisYear] No requests found, returning current year');
return { year: new Date().getFullYear() };
}
// chip_report_dt와 ms_report_dt 중 가장 최근 날짜 찾기
let latestDate: Date | null = null;
let latestChipDate: Date | null = null;
let latestMsDate: Date | null = null;
for (const request of requests) {
// chip_report_dt 확인
if (request.chipReportDt) {
const chipDate = new Date(request.chipReportDt);
if (!latestChipDate || chipDate > latestChipDate) {
latestChipDate = chipDate;
}
if (!latestDate || chipDate > latestDate) {
latestDate = chipDate;
}
}
// ms_report_dt 확인
if (request.msReportDt) {
const msDate = new Date(request.msReportDt);
if (!latestMsDate || msDate > latestMsDate) {
latestMsDate = msDate;
}
if (!latestDate || msDate > latestDate) {
latestDate = msDate;
}
}
}
console.log(`[getLatestAnalysisYear] Latest chip_report_dt: ${latestChipDate?.toISOString()}`);
console.log(`[getLatestAnalysisYear] Latest ms_report_dt: ${latestMsDate?.toISOString()}`);
console.log(`[getLatestAnalysisYear] Latest date overall: ${latestDate?.toISOString()}`);
// 가장 최근 날짜가 있으면 그 연도, 없으면 현재 연도
const year = latestDate ? latestDate.getFullYear() : new Date().getFullYear();
console.log(`[getLatestAnalysisYear] Returning year: ${year}`);
return { year };
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -404,14 +404,30 @@
/* 테이블 헤더 셀 */
.cow-table-header {
@apply text-center py-3 px-3 font-semibold;
font-size: 0.9375rem; /* 15px */
@apply text-center font-bold;
padding: 0.5rem 0.5rem; /* py-2 px-2 */
font-size: 1.25rem; /* 20px */
}
@media (min-width: 640px) {
.cow-table-header {
padding: 0.625rem 0.625rem; /* py-2.5 px-2.5 */
font-size: 1.375rem; /* 22px */
}
}
@media (min-width: 768px) {
.cow-table-header {
padding: 0.75rem 0.75rem; /* py-3 px-3 */
font-size: 1.5rem; /* 24px */
}
}
/* 테이블 바디 셀 */
.cow-table-cell {
@apply text-center py-3 px-3;
font-size: 0.9375rem; /* 15px */
@apply text-center;
padding: 0.5rem 0.375rem; /* py-2 px-1.5 */
font-size: 1.35rem; /* 21.6px */
}
/* 분석불가 행 - 각 td에 오버레이 */

View File

@@ -31,12 +31,12 @@ const userNavMain = [
},
{
title: "개체 조회",
url: "/cow",
url: "/cow?reset=true",
icon: IconListDetails,
items: [
{
title: "개체 목록",
url: "/cow",
url: "/cow?reset=true",
},
],
},

View File

@@ -6,6 +6,7 @@
* 기능:
* - 현재 연도부터 5년 전까지 선택 가능 (예: 2025~2020)
* - URL 파라미터 ?year=2024 와 동기화
* - 농장의 가장 최근 분석 연도를 기본값으로 사용
*
* 사용처:
* - site-header.tsx: 헤더 연도 선택 드롭다운
@@ -33,28 +34,59 @@ function AnalysisYearProviderInner({ children }: { children: React.ReactNode })
const currentYear = new Date().getFullYear()
const availableYears = Array.from({ length: 6 }, (_, i) => currentYear - i)
// URL 파라미터에서 연도 가져오기, 없으면 현재 연도 사용
const yearFromUrl = searchParams.get('year')
const initialYear = yearFromUrl && !isNaN(Number(yearFromUrl))
? Number(yearFromUrl)
: currentYear
// 초기 년도는 현재 년도로 설정 (클라이언트에서 useEffect로 업데이트)
const [selectedYear, setSelectedYearState] = useState<number>(currentYear)
const [isInitialized, setIsInitialized] = useState<boolean>(false)
const [selectedYear, setSelectedYearState] = useState<number>(initialYear)
// URL 파라미터와 동기화
// 클라이언트 사이드에서만 실행: localStorage와 URL에서 초기 년도 설정
useEffect(() => {
if (isInitialized || typeof window === 'undefined') return
const yearFromUrl = searchParams.get('year')
if (yearFromUrl && !isNaN(Number(yearFromUrl))) {
console.log('[AnalysisYear] Initial year from URL:', yearFromUrl)
setSelectedYearState(Number(yearFromUrl))
setIsInitialized(true)
return
}
const savedYear = localStorage.getItem('defaultAnalysisYear')
if (savedYear && !isNaN(Number(savedYear))) {
console.log('[AnalysisYear] Initial year from localStorage:', savedYear)
const year = Number(savedYear)
setSelectedYearState(year)
// URL에 year 파라미터 추가
const params = new URLSearchParams(searchParams.toString())
params.set('year', year.toString())
router.replace(`${pathname}?${params.toString()}`)
}
setIsInitialized(true)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []) // 의도적으로 빈 배열 사용 (최초 1회만 실행)
// URL 파라미터와 동기화 (초기화 이후에만 실행)
useEffect(() => {
if (!isInitialized) return
const yearParam = searchParams.get('year')
if (yearParam && !isNaN(Number(yearParam))) {
const year = Number(yearParam)
if (availableYears.includes(year)) {
if (availableYears.includes(year) && year !== selectedYear) {
setSelectedYearState(year)
}
}
}, [searchParams, availableYears])
}, [searchParams, availableYears, isInitialized, selectedYear])
const setSelectedYear = (year: number) => {
console.log('[AnalysisYear] setSelectedYear:', year)
setSelectedYearState(year)
// localStorage 업데이트 (사용자가 선택한 년도를 디폴트로 저장)
if (typeof window !== 'undefined') {
localStorage.setItem('defaultAnalysisYear', year.toString())
}
// URL 파라미터 업데이트
const params = new URLSearchParams(searchParams.toString())
params.set('year', year.toString())