update_cow_list_ui
This commit is contained in:
@@ -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;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 회원가입
|
||||
*/
|
||||
|
||||
@@ -11,4 +11,5 @@ export class LoginResponseDto {
|
||||
userEmail: string;
|
||||
userRole: 'USER' | 'ADMIN';
|
||||
};
|
||||
defaultAnalysisYear: number; // 최근 검사 년도
|
||||
}
|
||||
|
||||
@@ -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(개체식별번호)로 유전체 데이터 조회
|
||||
|
||||
@@ -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
@@ -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에 오버레이 */
|
||||
|
||||
@@ -31,12 +31,12 @@ const userNavMain = [
|
||||
},
|
||||
{
|
||||
title: "개체 조회",
|
||||
url: "/cow",
|
||||
url: "/cow?reset=true",
|
||||
icon: IconListDetails,
|
||||
items: [
|
||||
{
|
||||
title: "개체 목록",
|
||||
url: "/cow",
|
||||
url: "/cow?reset=true",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -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())
|
||||
|
||||
Reference in New Issue
Block a user