INIT
This commit is contained in:
20
backend/src/common/common.controller.spec.ts
Normal file
20
backend/src/common/common.controller.spec.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { CommonController } from './common.controller';
|
||||
import { CommonService } from './common.service';
|
||||
|
||||
describe('CommonController', () => {
|
||||
let controller: CommonController;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [CommonController],
|
||||
providers: [CommonService],
|
||||
}).compile();
|
||||
|
||||
controller = module.get<CommonController>(CommonController);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(controller).toBeDefined();
|
||||
});
|
||||
});
|
||||
7
backend/src/common/common.controller.ts
Normal file
7
backend/src/common/common.controller.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { Controller } from '@nestjs/common';
|
||||
import { CommonService } from './common.service';
|
||||
|
||||
@Controller('common')
|
||||
export class CommonController {
|
||||
constructor(private readonly commonService: CommonService) {}
|
||||
}
|
||||
32
backend/src/common/common.module.ts
Normal file
32
backend/src/common/common.module.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { CommonController } from './common.controller';
|
||||
import { CommonService } from './common.service';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { JwtStrategy } from './jwt/jwt.strategy';
|
||||
import { JwtAuthGuard } from './guards/jwt-auth.guard';
|
||||
import { HttpExceptionFilter } from './filters/http-exception.filter';
|
||||
import { AllExceptionsFilter } from './filters/all-exceptions.filter';
|
||||
import { LoggingInterceptor } from './interceptors/logging.interceptor';
|
||||
import { TransformInterceptor } from './interceptors/transform.interceptor';
|
||||
|
||||
@Module({
|
||||
controllers: [CommonController],
|
||||
providers: [
|
||||
CommonService,
|
||||
JwtStrategy,
|
||||
JwtAuthGuard,
|
||||
HttpExceptionFilter,
|
||||
AllExceptionsFilter,
|
||||
LoggingInterceptor,
|
||||
TransformInterceptor,
|
||||
],
|
||||
exports: [
|
||||
JwtStrategy,
|
||||
JwtAuthGuard,
|
||||
HttpExceptionFilter,
|
||||
AllExceptionsFilter,
|
||||
LoggingInterceptor,
|
||||
TransformInterceptor,
|
||||
],
|
||||
})
|
||||
export class CommonModule {}
|
||||
18
backend/src/common/common.service.spec.ts
Normal file
18
backend/src/common/common.service.spec.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { CommonService } from './common.service';
|
||||
|
||||
describe('CommonService', () => {
|
||||
let service: CommonService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [CommonService],
|
||||
}).compile();
|
||||
|
||||
service = module.get<CommonService>(CommonService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
});
|
||||
4
backend/src/common/common.service.ts
Normal file
4
backend/src/common/common.service.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class CommonService {}
|
||||
61
backend/src/common/config/CowPurposeConfig.ts
Normal file
61
backend/src/common/config/CowPurposeConfig.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* 소 용도 분류 설정
|
||||
*
|
||||
* @description
|
||||
* 소의 용도를 결정하는 비즈니스 로직 임계값 정의
|
||||
* - 도태 (Culling): 낮은 수태율, 번식 능력 부족
|
||||
* - 인공수정 (Artificial Insemination): 높은 수태율 + 우수 등급
|
||||
* - 공란우 (Donor): 중간 수태율 + 우수 등급
|
||||
* - 수란우 (Recipient): 높은 수태율 + 낮은 등급
|
||||
*/
|
||||
export const COW_PURPOSE_CONFIG = {
|
||||
/**
|
||||
* 수태율 기반 임계값 (%)
|
||||
*/
|
||||
CONCEPTION_RATE_THRESHOLDS: {
|
||||
/**
|
||||
* 도태 대상 최대 수태율 (30% 미만)
|
||||
* 수태율이 이 값보다 낮으면 번식 능력이 부족하여 도태 대상
|
||||
*/
|
||||
CULLING_MAX: 30,
|
||||
|
||||
/**
|
||||
* 공란우 최대 수태율 (50% 미만)
|
||||
* 수태율이 낮지만 우수한 유전자 보유 시 수정란 공급
|
||||
*/
|
||||
DONOR_MAX: 50,
|
||||
|
||||
/**
|
||||
* 수란우 최소 수태율 (65% 이상)
|
||||
* 높은 수태율을 가진 소에게 우수 수정란 이식
|
||||
*/
|
||||
RECIPIENT_MIN: 65,
|
||||
|
||||
/**
|
||||
* 인공수정 최소 수태율 (65% 이상)
|
||||
* 높은 수태율 + 우수 등급 → 일반 인공수정 대상
|
||||
*/
|
||||
INSEMINATION_MIN: 65,
|
||||
},
|
||||
|
||||
/**
|
||||
* 나이 기반 임계값 (년)
|
||||
*/
|
||||
AGE_THRESHOLDS: {
|
||||
/**
|
||||
* 노령우 기준 (10년 이상)
|
||||
* 이 나이 이상이면 도태 고려 대상
|
||||
*/
|
||||
OLD_AGE_YEARS: 10,
|
||||
|
||||
/**
|
||||
* 번식 적정 최소 나이 (2년)
|
||||
*/
|
||||
BREEDING_MIN_AGE: 2,
|
||||
|
||||
/**
|
||||
* 번식 적정 최대 나이 (8년)
|
||||
*/
|
||||
BREEDING_MAX_AGE: 8,
|
||||
},
|
||||
} as const;
|
||||
85
backend/src/common/config/GenomeAnalysisConfig.ts
Normal file
85
backend/src/common/config/GenomeAnalysisConfig.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
* 유전체 분석 유효성 조건 설정
|
||||
*
|
||||
* @description
|
||||
* 유전체 분석 데이터가 유효한지 판단하는 조건 정의
|
||||
*
|
||||
* 유효 조건:
|
||||
* 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회 분석 반려내역서 재분석 불가능
|
||||
];
|
||||
//=================================================================================================================
|
||||
|
||||
/** @deprecated INVALID_CHIP_DAM_NAMES 사용 권장 */
|
||||
export const INVALID_CHIP_DAM_NAME = '불일치';
|
||||
|
||||
/**
|
||||
* 유전체 분석 데이터 유효성 검사
|
||||
*
|
||||
* @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;
|
||||
}
|
||||
|
||||
/**
|
||||
* SQL WHERE 조건 생성 (TypeORM QueryBuilder용)
|
||||
* 주의: cowId 제외 목록은 SQL에 포함되지 않으므로 별도 필터링 필요
|
||||
*
|
||||
* @param alias - 테이블 별칭 (예: 'request', 'genome')
|
||||
* @returns SQL 조건 문자열
|
||||
*
|
||||
* @example
|
||||
* queryBuilder.andWhere(getValidGenomeConditionSQL('request'));
|
||||
*/
|
||||
export function getValidGenomeConditionSQL(alias: string): string {
|
||||
const damConditions = INVALID_CHIP_DAM_NAMES.map(name => `${alias}.chipDamName != '${name}'`).join(' AND ');
|
||||
return `${alias}.chipSireName = '${VALID_CHIP_SIRE_NAME}' AND (${alias}.chipDamName IS NULL OR (${damConditions}))`;
|
||||
}
|
||||
73
backend/src/common/config/InbreedingConfig.ts
Normal file
73
backend/src/common/config/InbreedingConfig.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* 근친도 관련 설정 상수
|
||||
*
|
||||
* @description
|
||||
* Wright's Coefficient of Inbreeding 알고리즘 기반 근친도 계산 및 위험도 판정 기준
|
||||
*
|
||||
* @source PRD 기능요구사항20.md SFR-COW-016-3
|
||||
* @reference Wright, S. (1922). Coefficients of Inbreeding and Relationship
|
||||
*/
|
||||
export const INBREEDING_CONFIG = {
|
||||
/**
|
||||
* 위험도 판정 기준 (%)
|
||||
* - 정상: < 15%
|
||||
* - 주의: 15-20%
|
||||
* - 위험: > 20%
|
||||
*/
|
||||
RISK_LEVELS: {
|
||||
NORMAL_MAX: 15, // 정상 상한선 (< 15%)
|
||||
WARNING_MIN: 15, // 주의 하한선 (>= 15%)
|
||||
WARNING_MAX: 20, // 주의 상한선 (<= 20%)
|
||||
DANGER_MIN: 20, // 위험 하한선 (> 20%)
|
||||
},
|
||||
|
||||
/**
|
||||
* 다세대 시뮬레이션 위험도 판정 기준 (%)
|
||||
* - 정상: < 6.25%
|
||||
* - 주의: 6.25% ~ 임계값
|
||||
* - 위험: > 임계값
|
||||
*/
|
||||
MULTI_GENERATION_RISK_LEVELS: {
|
||||
SAFE_MAX: 6.25, // 안전 상한선 (< 6.25%)
|
||||
// WARNING: 6.25% ~ inbreedingThreshold (사용자 지정)
|
||||
// DANGER: > inbreedingThreshold (사용자 지정)
|
||||
},
|
||||
|
||||
/**
|
||||
* 기본 근친도 임계값 (%)
|
||||
* Wright's Coefficient 기준 안전 임계값
|
||||
*/
|
||||
DEFAULT_THRESHOLD: 12.5,
|
||||
|
||||
/**
|
||||
* 세대별 근친도 영향 감소율
|
||||
* - 1세대: 100% 영향
|
||||
* - 2세대: 50% 영향 (1/2)
|
||||
* - 3세대: 25% 영향 (1/4)
|
||||
* - 4세대: 12.5% 영향 (1/8)
|
||||
*/
|
||||
GENERATION_DECAY: {
|
||||
GEN_1: 1.0, // 100%
|
||||
GEN_2: 0.5, // 50%
|
||||
GEN_3: 0.25, // 25%
|
||||
GEN_4: 0.125, // 12.5%
|
||||
GEN_5: 0.0625, // 6.25%
|
||||
},
|
||||
|
||||
/**
|
||||
* KPN 순환 전략 설정
|
||||
*/
|
||||
ROTATION_STRATEGY: {
|
||||
CYCLE_GENERATIONS: 2, // N세대마다 순환 (기본값: 2세대)
|
||||
},
|
||||
|
||||
/**
|
||||
* 유리형 비율 평가 기준 (%)
|
||||
*/
|
||||
FAVORABLE_RATE_THRESHOLDS: {
|
||||
EXCELLENT: 75, // 매우 우수 (>= 75%)
|
||||
GOOD: 60, // 양호 (>= 60%)
|
||||
AVERAGE: 50, // 보통 (>= 50%)
|
||||
POOR: 70, // 권장사항 생성 기준 (>= 70%)
|
||||
},
|
||||
} as const;
|
||||
90
backend/src/common/config/MptNormalRanges.ts
Normal file
90
backend/src/common/config/MptNormalRanges.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
/**
|
||||
* MPT 혈액대사검사 정상 범위 기준값
|
||||
*
|
||||
* @description
|
||||
* 각 MPT 항목별 권장 정상 범위를 정의합니다.
|
||||
* 이 범위 내에 있으면 "우수" 판정을 받습니다.
|
||||
*
|
||||
* @export
|
||||
* @constant
|
||||
*/
|
||||
export const MPT_NORMAL_RANGES = {
|
||||
/**
|
||||
* 알부민 (Albumin)
|
||||
* 단위: g/dL
|
||||
*/
|
||||
albumin: { min: 3.3, max: 4.3 },
|
||||
|
||||
/**
|
||||
* 총 글로불린 (Total Globulin)
|
||||
* 단위: g/L
|
||||
*/
|
||||
totalGlobulin: { min: 9.1, max: 36.1 },
|
||||
|
||||
/**
|
||||
* A/G 비율 (Albumin/Globulin Ratio)
|
||||
* 단위: 비율
|
||||
*/
|
||||
agRatio: { min: 0.1, max: 0.4 },
|
||||
|
||||
/**
|
||||
* 혈중요소질소 (Blood Urea Nitrogen)
|
||||
* 단위: mg/dL
|
||||
*/
|
||||
bun: { min: 11.7, max: 18.9 },
|
||||
|
||||
/**
|
||||
* AST (Aspartate Aminotransferase)
|
||||
* 단위: U/L
|
||||
*/
|
||||
ast: { min: 47, max: 92 },
|
||||
|
||||
/**
|
||||
* GGT (Gamma-Glutamyl Transferase)
|
||||
* 단위: U/L
|
||||
*/
|
||||
ggt: { min: 11, max: 32 },
|
||||
|
||||
/**
|
||||
* 지방간 지수 (Fatty Liver Index)
|
||||
* 단위: 지수
|
||||
*/
|
||||
fattyLiverIndex: { min: -1.2, max: 9.9 },
|
||||
|
||||
/**
|
||||
* 칼슘 (Calcium)
|
||||
* 단위: mg/dL
|
||||
*/
|
||||
calcium: { min: 8.1, max: 10.6 },
|
||||
|
||||
/**
|
||||
* 인 (Phosphorus)
|
||||
* 단위: mg/dL
|
||||
*/
|
||||
phosphorus: { min: 6.2, max: 8.9 },
|
||||
|
||||
/**
|
||||
* Ca/P 비율 (Calcium/Phosphorus Ratio)
|
||||
* 단위: 비율
|
||||
*/
|
||||
caPRatio: { min: 1.2, max: 1.3 },
|
||||
|
||||
/**
|
||||
* 마그네슘 (Magnesium)
|
||||
* 단위: mg/dL
|
||||
*/
|
||||
magnesium: { min: 1.6, max: 3.3 },
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* MPT 항목 타입
|
||||
*/
|
||||
export type MptCriteriaKey = keyof typeof MPT_NORMAL_RANGES;
|
||||
|
||||
/**
|
||||
* MPT 범위 타입
|
||||
*/
|
||||
export interface MptRange {
|
||||
min: number;
|
||||
max: number;
|
||||
}
|
||||
62
backend/src/common/config/PaginationConfig.ts
Normal file
62
backend/src/common/config/PaginationConfig.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* 페이징 설정 상수
|
||||
*
|
||||
* @description
|
||||
* 목록 조회, 검색, 가상 스크롤링 등의 페이징 기본값
|
||||
*/
|
||||
export const PAGINATION_CONFIG = {
|
||||
/**
|
||||
* 목록별 기본 페이지당 항목 수
|
||||
*/
|
||||
DEFAULTS: {
|
||||
/**
|
||||
* 개체 목록
|
||||
* 일반 개체 목록 조회 시 기본값
|
||||
*/
|
||||
COW_LIST: 10,
|
||||
|
||||
/**
|
||||
* 교배조합 목록
|
||||
*/
|
||||
BREED_SAVE: 10,
|
||||
|
||||
/**
|
||||
* 검색 결과
|
||||
* 키워드 검색 결과 표시 기본값
|
||||
*/
|
||||
SEARCH_RESULT: 20,
|
||||
|
||||
/**
|
||||
* 유전자 검색
|
||||
* 5000개 이상 유전자 검색 시 기본값
|
||||
*/
|
||||
GENE_SEARCH: 50,
|
||||
|
||||
/**
|
||||
* 가상 스크롤링
|
||||
* 무한 스크롤 방식 로딩 단위
|
||||
*/
|
||||
VIRTUAL_SCROLL: 50,
|
||||
},
|
||||
|
||||
/**
|
||||
* 제한값
|
||||
*/
|
||||
LIMITS: {
|
||||
/**
|
||||
* 최소 페이지당 항목 수
|
||||
*/
|
||||
MIN: 1,
|
||||
|
||||
/**
|
||||
* 최대 페이지당 항목 수
|
||||
* 성능 및 메모리 고려
|
||||
*/
|
||||
MAX: 100,
|
||||
|
||||
/**
|
||||
* 기본 페이지 번호
|
||||
*/
|
||||
DEFAULT_PAGE: 1,
|
||||
},
|
||||
} as const;
|
||||
105
backend/src/common/config/RecommendationConfig.ts
Normal file
105
backend/src/common/config/RecommendationConfig.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
/**
|
||||
* 추천 시스템 설정 상수
|
||||
*
|
||||
* @description
|
||||
* KPN 추천, 개체 추천, 패키지 추천 등 추천 시스템 관련 설정값
|
||||
*
|
||||
* @source PRD 기능요구사항20.md SFR-COW-016, SFR-COW-037
|
||||
*/
|
||||
export const RECOMMENDATION_CONFIG = {
|
||||
/**
|
||||
* 유전자 매칭 점수 관련
|
||||
*/
|
||||
GENE_SCORE: {
|
||||
/**
|
||||
* 점수 차이 임계값
|
||||
* 유전자 매칭 점수 차이가 이 값보다 작으면 근친도를 우선 고려
|
||||
*/
|
||||
DIFF_THRESHOLD: 5,
|
||||
},
|
||||
|
||||
/**
|
||||
* 기본값
|
||||
*/
|
||||
DEFAULTS: {
|
||||
/**
|
||||
* 근친도 임계값 (%)
|
||||
* Wright's Coefficient 기준
|
||||
*/
|
||||
INBREEDING_THRESHOLD: 12.5,
|
||||
|
||||
/**
|
||||
* 추천 개수
|
||||
* 상위 N개의 KPN/개체를 추천
|
||||
*/
|
||||
RECOMMENDATION_LIMIT: 10,
|
||||
|
||||
/**
|
||||
* 세대제약 기준
|
||||
* 최근 N세대 이내 사용된 KPN을 추천에서 제외
|
||||
*/
|
||||
GENERATION_THRESHOLD: 3,
|
||||
},
|
||||
|
||||
/**
|
||||
* KPN 패키지 설정
|
||||
*/
|
||||
PACKAGE: {
|
||||
/**
|
||||
* 기본 패키지 크기
|
||||
* 추천할 KPN 세트 개수
|
||||
*/
|
||||
DEFAULT_SIZE: 5,
|
||||
|
||||
/**
|
||||
* 최소 패키지 크기
|
||||
*/
|
||||
MIN_SIZE: 3,
|
||||
|
||||
/**
|
||||
* 최대 패키지 크기
|
||||
*/
|
||||
MAX_SIZE: 10,
|
||||
},
|
||||
|
||||
/**
|
||||
* 커버리지 기준 (%)
|
||||
* 유전자 목표 달성률 평가 기준
|
||||
*/
|
||||
COVERAGE: {
|
||||
/**
|
||||
* 우수 기준
|
||||
* 50% 이상 커버리지
|
||||
*/
|
||||
EXCELLENT: 50,
|
||||
|
||||
/**
|
||||
* 양호 기준
|
||||
* 30% 이상 커버리지
|
||||
*/
|
||||
GOOD: 30,
|
||||
|
||||
/**
|
||||
* 최소 기준
|
||||
* 20% 이상 커버리지
|
||||
*/
|
||||
MINIMUM: 20,
|
||||
},
|
||||
|
||||
/**
|
||||
* KPN 순환 전략
|
||||
*/
|
||||
ROTATION: {
|
||||
/**
|
||||
* 최소 KPN 개수
|
||||
* 순환 전략 적용 최소 개수
|
||||
*/
|
||||
MIN_KPN_COUNT: 3,
|
||||
|
||||
/**
|
||||
* 재사용 안전 세대
|
||||
* 동일 KPN을 이 세대 이후에 재사용 가능
|
||||
*/
|
||||
SAFE_REUSE_GENERATION: 4,
|
||||
},
|
||||
} as const;
|
||||
36
backend/src/common/config/VerificationConfig.ts
Normal file
36
backend/src/common/config/VerificationConfig.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* 인증 관련 설정 (이메일)
|
||||
*
|
||||
* @description
|
||||
* 이메일 인증, 비밀번호 재설정 등의 인증 관련 상수 값 정의
|
||||
*/
|
||||
export const VERIFICATION_CONFIG = {
|
||||
/**
|
||||
* 인증 코드 만료 시간 (초)
|
||||
* 인증시간 3분 = 180초
|
||||
*/
|
||||
CODE_EXPIRY_SECONDS: 180,
|
||||
|
||||
/**
|
||||
* 비밀번호 재설정 토큰 만료 시간 (초)
|
||||
* 30분 = 1800초
|
||||
*/
|
||||
RESET_TOKEN_EXPIRY_SECONDS: 1800,
|
||||
|
||||
/**
|
||||
* 랜덤 토큰 생성을 위한 바이트 길이
|
||||
* 32바이트 = 64자의 16진수 문자열
|
||||
*/
|
||||
TOKEN_BYTES_LENGTH: 32,
|
||||
|
||||
/**
|
||||
* 인증 코드 길이 (6자리 숫자)
|
||||
*/
|
||||
CODE_LENGTH: 6,
|
||||
|
||||
/**
|
||||
* 인증 코드 재전송 대기 시간 (초)
|
||||
* 1분 = 60초
|
||||
*/
|
||||
RESEND_DELAY_SECONDS: 60,
|
||||
} as const;
|
||||
6
backend/src/common/const/AccountStatusType.ts
Normal file
6
backend/src/common/const/AccountStatusType.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
// 계정 상태 Enum
|
||||
export enum AccountStatusType {
|
||||
ACTIVE = "ACTIVE", // 정상
|
||||
INACTIVE = "INACTIVE", // 비활성
|
||||
SUSPENDED = "SUSPENDED", // 정지
|
||||
}
|
||||
5
backend/src/common/const/AnimalType.ts
Normal file
5
backend/src/common/const/AnimalType.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
// 개체 타입 Enum
|
||||
export enum AnimalType {
|
||||
COW = 'COW', // 개체
|
||||
KPN = 'KPN', // KPN
|
||||
}
|
||||
12
backend/src/common/const/AnlysStatType.ts
Normal file
12
backend/src/common/const/AnlysStatType.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* 분석 현황 상태 값 Enum
|
||||
*
|
||||
* @export
|
||||
* @enum {number}
|
||||
*/
|
||||
export enum AnlysStatType {
|
||||
MATCH = '친자일치',
|
||||
MISMATCH = '친자불일치',
|
||||
IMPOSSIBLE = '분석불가',
|
||||
NO_HISTORY = '이력제부재',
|
||||
}
|
||||
13
backend/src/common/const/BreedingRecommendationType.ts
Normal file
13
backend/src/common/const/BreedingRecommendationType.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* 사육/도태 추천 타입 Enum
|
||||
*
|
||||
* @export
|
||||
* @enum {string}
|
||||
*/
|
||||
export enum BreedingRecommendationType {
|
||||
/** 사육 추천 */
|
||||
BREED = '사육추천',
|
||||
|
||||
/** 도태 추천 */
|
||||
CULL = '도태추천',
|
||||
}
|
||||
7
backend/src/common/const/CowReproType.ts
Normal file
7
backend/src/common/const/CowReproType.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
// 개체 번식 타입 Enum
|
||||
export enum CowReproType {
|
||||
DONOR = "공란우",
|
||||
RECIPIENT = "수란우",
|
||||
AI = "인공수정",
|
||||
CULL = "도태대상",
|
||||
}
|
||||
7
backend/src/common/const/CowStatusType.ts
Normal file
7
backend/src/common/const/CowStatusType.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
// 개체 상태 Enum
|
||||
export enum CowStatusType {
|
||||
NORMAL = "정상",
|
||||
DEAD = "폐사",
|
||||
SLAUGHTER = "도축",
|
||||
SALE = "매각",
|
||||
}
|
||||
55
backend/src/common/const/FileType.ts
Normal file
55
backend/src/common/const/FileType.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* 파일 타입 Enum
|
||||
*
|
||||
* @description
|
||||
* 엑셀 업로드 시 지원되는 파일 유형을 정의합니다.
|
||||
* 각 파일 유형별로 고유한 파싱 로직과 대상 테이블이 매핑됩니다.
|
||||
*
|
||||
* @reference SFR-ADMIN-001 (기능요구사항20.md)
|
||||
*
|
||||
* 파일 유형별 매핑:
|
||||
* - COW: 개체(암소) 정보 → tb_cow
|
||||
* - GENE: 유전자(SNP) 데이터 → tb_snp_cow
|
||||
* - GENOME: 유전체(유전능력) 데이터 → tb_genome_cow
|
||||
* - MPT: 혈액대사검사(MPT) (1행: 카테고리, 2행: 항목명, 3행~: 데이터) → tb_repro_mpt, tb_repro_mpt_item
|
||||
* - FERTILITY: 수태율 데이터 → tb_fertility_rate
|
||||
* - KPN_GENE: KPN 유전자 데이터 → tb_kpn_snp
|
||||
* - KPN_GENOME: KPN 유전체 데이터 → tb_kpn_genome
|
||||
* - KPN_MPT: KPN 혈액대사검사 → tb_kpn_mpt
|
||||
* - REGION_COW: 지역 개체 정보 → tb_region_cow
|
||||
* - REGION_GENE: 지역 유전자 데이터 → tb_region_snp
|
||||
* - REGION_GENOME: 지역 유전체 데이터 → tb_region_genome
|
||||
* - REGION_MPT: 지역 혈액대사검사 → tb_region_mpt
|
||||
* - HELP: 도움말 데이터 (유전자/유전체/번식능력 설명) → tb_help_content
|
||||
* - MARKER: 마커(유전자) 정보 (마커명, 관련형질, 목표유전자형 등) → tb_marker
|
||||
* 목표유전자형(target_genotype): KPN 추천 시 각 유전자의 우량형 기준 (AA, GG, CC 등)
|
||||
*/
|
||||
export enum FileType {
|
||||
// 개체(암소) 데이터
|
||||
COW = '개체',
|
||||
|
||||
// 유전 데이터
|
||||
GENE = '유전자',
|
||||
GENOME = '유전체',
|
||||
|
||||
// 번식 데이터
|
||||
MPT = '혈액대사검사',
|
||||
FERTILITY = '수태율',
|
||||
|
||||
// KPN 데이터
|
||||
KPN_GENE = 'KPN유전자',
|
||||
KPN_GENOME = 'KPN유전체',
|
||||
KPN_MPT = 'KPN혈액대사검사',
|
||||
|
||||
// 지역 개체 데이터 (보은군 비교용)
|
||||
REGION_COW = '지역개체',
|
||||
REGION_GENE = '지역유전자',
|
||||
REGION_GENOME = '지역유전체',
|
||||
REGION_MPT = '지역혈액대사검사',
|
||||
|
||||
// 도움말 데이터
|
||||
HELP = '도움말',
|
||||
|
||||
// 마커(유전자) 정보
|
||||
MARKER = '마커정보',
|
||||
}
|
||||
11
backend/src/common/const/UserSeType.ts
Normal file
11
backend/src/common/const/UserSeType.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* 사용자 타입 구분 Enum
|
||||
*
|
||||
* @export
|
||||
* @enum {number}
|
||||
*/
|
||||
export enum UserSeType {
|
||||
FARM = 'FARM', // 농가
|
||||
CNSLT = 'CNSLT', // 컨설턴트
|
||||
ORGAN = 'ORGAN', // 기관담당자
|
||||
}
|
||||
41
backend/src/common/decorators/current-user.decorator.ts
Normal file
41
backend/src/common/decorators/current-user.decorator.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
|
||||
|
||||
/**
|
||||
* 현재 로그인한 사용자 정보를 가져오는 Decorator
|
||||
*
|
||||
* @description
|
||||
* 인증 미들웨어(JWT, Passport 등)가 req.user에 추가한 사용자 정보를 추출합니다.
|
||||
* 인증되지 않은 경우 기본값을 반환합니다.
|
||||
*
|
||||
* @example
|
||||
* // 전체 user 객체 가져오기
|
||||
* async method(@CurrentUser() user: any) {
|
||||
* console.log(user.userId, user.email);
|
||||
* }
|
||||
*
|
||||
* @example
|
||||
* // 특정 속성만 가져오기
|
||||
* async method(@CurrentUser('userId') userId: string) {
|
||||
* console.log(userId); // 'user123' or 'system'
|
||||
* }
|
||||
*/
|
||||
export const CurrentUser = createParamDecorator(
|
||||
(data: string | undefined, ctx: ExecutionContext) => {
|
||||
const request = ctx.switchToHttp().getRequest();
|
||||
const user = request.user;
|
||||
|
||||
// 사용자 정보가 없으면 기본값 반환
|
||||
if (!user) {
|
||||
// userId를 요청한 경우 'system' 반환
|
||||
if (data === 'userId') {
|
||||
return 'system';
|
||||
}
|
||||
// 전체 user 객체를 요청한 경우 null 반환
|
||||
return null;
|
||||
}
|
||||
|
||||
// 특정 속성을 요청한 경우 해당 속성 반환
|
||||
// 전체 user 객체를 요청한 경우 user 반환
|
||||
return data ? user[data] : user;
|
||||
},
|
||||
);
|
||||
29
backend/src/common/decorators/public.decorator.ts
Normal file
29
backend/src/common/decorators/public.decorator.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { SetMetadata } from '@nestjs/common';
|
||||
|
||||
/**
|
||||
* Public 데코레이터
|
||||
*
|
||||
* @description
|
||||
* 인증이 필요 없는 공개 엔드포인트를 표시하는 데코레이터입니다.
|
||||
* JwtAuthGuard와 함께 사용하여 특정 엔드포인트의 인증을 건너뜁니다.
|
||||
*
|
||||
* @example
|
||||
* // 로그인, 회원가입 등 인증 없이 접근 가능한 엔드포인트
|
||||
* @Public()
|
||||
* @Post('login')
|
||||
* async login(@Body() loginDto: LoginDto) {
|
||||
* return this.authService.login(loginDto);
|
||||
* }
|
||||
*
|
||||
* @example
|
||||
* // 전체 컨트롤러를 공개로 설정
|
||||
* @Public()
|
||||
* @Controller('public')
|
||||
* export class PublicController {
|
||||
* // 모든 엔드포인트가 인증 없이 접근 가능
|
||||
* }
|
||||
*
|
||||
* @export
|
||||
* @constant Public
|
||||
*/
|
||||
export const Public = () => SetMetadata('isPublic', true);
|
||||
37
backend/src/common/decorators/user.decorator.ts
Normal file
37
backend/src/common/decorators/user.decorator.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
|
||||
|
||||
/**
|
||||
* User 데코레이터
|
||||
*
|
||||
* @description
|
||||
* JWT 인증 후 Request 객체에서 사용자 정보를 추출하는 데코레이터입니다.
|
||||
* @Req() req 대신 사용하여 더 간결하게 사용자 정보를 가져올 수 있습니다.
|
||||
*
|
||||
* @example
|
||||
* // 전체 사용자 정보 가져오기
|
||||
* @Get('profile')
|
||||
* @UseGuards(JwtAuthGuard)
|
||||
* getProfile(@User() user: any) {
|
||||
* return user; // { userId: '...', userNo: 1, role: 'user' }
|
||||
* }
|
||||
*
|
||||
* @example
|
||||
* // 특정 필드만 가져오기
|
||||
* @Get('my-data')
|
||||
* @UseGuards(JwtAuthGuard)
|
||||
* getMyData(@User('userId') userId: string) {
|
||||
* return `Your ID is ${userId}`;
|
||||
* }
|
||||
*
|
||||
* @export
|
||||
* @constant User
|
||||
*/
|
||||
export const User = createParamDecorator(
|
||||
(data: string, ctx: ExecutionContext) => {
|
||||
const request = ctx.switchToHttp().getRequest();
|
||||
const user = request.user;
|
||||
|
||||
// 특정 필드만 반환
|
||||
return data ? user?.[data] : user;
|
||||
},
|
||||
);
|
||||
87
backend/src/common/entities/base.entity.ts
Normal file
87
backend/src/common/entities/base.entity.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import { Column, CreateDateColumn, DeleteDateColumn, UpdateDateColumn } from 'typeorm';
|
||||
|
||||
/**
|
||||
* TypeORM Entity
|
||||
*
|
||||
* @export
|
||||
* @abstract
|
||||
* @class BaseModel
|
||||
* @typedef {BaseModel}
|
||||
*/
|
||||
export abstract class BaseModel {
|
||||
/**
|
||||
* 모든 테이블의 기본 키 (Primary Key)
|
||||
*/
|
||||
// @PrimaryGeneratedColumn()
|
||||
// no: number;
|
||||
|
||||
/**
|
||||
* 데이터가 처음 생성된 시간
|
||||
*/
|
||||
@CreateDateColumn({
|
||||
name: 'reg_dt',
|
||||
type: 'timestamp',
|
||||
nullable: false,
|
||||
default: () => 'NOW()',
|
||||
comment: '등록일시',
|
||||
})
|
||||
regDt: Date;
|
||||
|
||||
/**
|
||||
*데이터가 마지막으로 업데이트된 시간
|
||||
*/
|
||||
@UpdateDateColumn({
|
||||
name: 'updt_dt',
|
||||
type: 'timestamp',
|
||||
nullable: true,
|
||||
comment: '수정일시',
|
||||
})
|
||||
updtDt: Date;
|
||||
|
||||
/**
|
||||
* Soft Delete 삭제일시 (NULL이면 활성 데이터)
|
||||
*/
|
||||
@DeleteDateColumn({
|
||||
name: 'del_dt',
|
||||
type: 'timestamp',
|
||||
nullable: true,
|
||||
comment: '삭제일시 (Soft Delete, NULL=활성)',
|
||||
})
|
||||
delDt: Date;
|
||||
|
||||
@Column({
|
||||
name: 'reg_ip',
|
||||
type: 'varchar',
|
||||
length: 50,
|
||||
nullable: true,
|
||||
comment: '등록 IP',
|
||||
})
|
||||
regIp: string;
|
||||
|
||||
@Column({
|
||||
name: 'reg_user_id',
|
||||
type: 'varchar',
|
||||
length: 100,
|
||||
nullable: true,
|
||||
comment: '등록자 ID',
|
||||
})
|
||||
regUserId: string;
|
||||
|
||||
@Column({
|
||||
name: 'updt_ip',
|
||||
type: 'varchar',
|
||||
length: 50,
|
||||
nullable: true,
|
||||
comment: '수정 IP',
|
||||
})
|
||||
updtIp: string;
|
||||
|
||||
@Column({
|
||||
name: 'updt_user_id',
|
||||
type: 'varchar',
|
||||
length: 100,
|
||||
nullable: true,
|
||||
comment: '수정자 ID',
|
||||
})
|
||||
updtUserId: string;
|
||||
}
|
||||
80
backend/src/common/filters/all-exceptions.filter.ts
Normal file
80
backend/src/common/filters/all-exceptions.filter.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import {
|
||||
ExceptionFilter,
|
||||
Catch,
|
||||
ArgumentsHost,
|
||||
HttpException,
|
||||
HttpStatus,
|
||||
} from '@nestjs/common';
|
||||
import { Request, Response } from 'express';
|
||||
|
||||
/**
|
||||
* 모든 예외 필터
|
||||
*
|
||||
* @description
|
||||
* HTTP 예외뿐만 아니라 모든 예외를 잡아서 처리합니다.
|
||||
* 예상치 못한 에러도 일관된 형식으로 응답합니다.
|
||||
*
|
||||
* @example
|
||||
* // main.ts에서 전역 적용
|
||||
* app.useGlobalFilters(new AllExceptionsFilter());
|
||||
*
|
||||
* @export
|
||||
* @class AllExceptionsFilter
|
||||
* @implements {ExceptionFilter}
|
||||
*/
|
||||
@Catch()
|
||||
export class AllExceptionsFilter implements ExceptionFilter {
|
||||
catch(exception: unknown, host: ArgumentsHost) {
|
||||
const ctx = host.switchToHttp();
|
||||
const response = ctx.getResponse<Response>();
|
||||
const request = ctx.getRequest<Request>();
|
||||
|
||||
let status: number;
|
||||
let message: string | string[];
|
||||
|
||||
if (exception instanceof HttpException) {
|
||||
// HTTP 예외 처리
|
||||
status = exception.getStatus();
|
||||
const exceptionResponse = exception.getResponse();
|
||||
|
||||
if (typeof exceptionResponse === 'string') {
|
||||
message = exceptionResponse;
|
||||
} else if (typeof exceptionResponse === 'object' && exceptionResponse !== null) {
|
||||
message = (exceptionResponse as any).message || exception.message;
|
||||
} else {
|
||||
message = exception.message;
|
||||
}
|
||||
} else {
|
||||
// 예상치 못한 에러 처리
|
||||
status = HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
message = '서버 내부 오류가 발생했습니다.';
|
||||
|
||||
// 개발 환경에서는 실제 에러 메시지 표시
|
||||
if (process.env.NODE_ENV === 'development' && exception instanceof Error) {
|
||||
message = exception.message;
|
||||
}
|
||||
}
|
||||
|
||||
const errorResponse = {
|
||||
success: false,
|
||||
statusCode: status,
|
||||
timestamp: new Date().toISOString(),
|
||||
path: request.url,
|
||||
method: request.method,
|
||||
message: Array.isArray(message) ? message : [message],
|
||||
};
|
||||
|
||||
// 개발 환경에서는 스택 트레이스 포함
|
||||
if (process.env.NODE_ENV === 'development' && exception instanceof Error) {
|
||||
(errorResponse as any).stack = exception.stack;
|
||||
}
|
||||
|
||||
// 에러 로깅
|
||||
console.error(
|
||||
`[${errorResponse.timestamp}] ${request.method} ${request.url} - ${status}`,
|
||||
exception,
|
||||
);
|
||||
|
||||
response.status(status).json(errorResponse);
|
||||
}
|
||||
}
|
||||
93
backend/src/common/filters/http-exception.filter.ts
Normal file
93
backend/src/common/filters/http-exception.filter.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import {
|
||||
ExceptionFilter,
|
||||
Catch,
|
||||
ArgumentsHost,
|
||||
HttpException,
|
||||
HttpStatus,
|
||||
} from '@nestjs/common';
|
||||
import { Request, Response } from 'express';
|
||||
|
||||
/**
|
||||
* HTTP 예외 필터
|
||||
*
|
||||
* @description
|
||||
* 모든 HTTP 예외를 잡아서 일관된 형식으로 응답을 반환합니다.
|
||||
*
|
||||
* @example
|
||||
* // main.ts에서 전역 적용
|
||||
* app.useGlobalFilters(new HttpExceptionFilter());
|
||||
*
|
||||
* @export
|
||||
* @class HttpExceptionFilter
|
||||
* @implements {ExceptionFilter}
|
||||
*/
|
||||
@Catch(HttpException)
|
||||
export class HttpExceptionFilter implements ExceptionFilter {
|
||||
catch(exception: HttpException, host: ArgumentsHost) {
|
||||
const ctx = host.switchToHttp();
|
||||
const response = ctx.getResponse<Response>();
|
||||
const request = ctx.getRequest<Request>();
|
||||
const status = exception.getStatus();
|
||||
const exceptionResponse = exception.getResponse();
|
||||
|
||||
// 에러 메시지 추출
|
||||
let message: string | string[];
|
||||
if (typeof exceptionResponse === 'string') {
|
||||
message = exceptionResponse;
|
||||
} else if (typeof exceptionResponse === 'object' && exceptionResponse !== null) {
|
||||
message = (exceptionResponse as any).message || exception.message;
|
||||
} else {
|
||||
message = exception.message;
|
||||
}
|
||||
|
||||
// 일관된 에러 응답 형식
|
||||
const errorResponse = {
|
||||
success: false,
|
||||
statusCode: status,
|
||||
timestamp: new Date().toISOString(),
|
||||
path: request.url,
|
||||
method: request.method,
|
||||
message: Array.isArray(message) ? message : [message],
|
||||
error: this.getErrorName(status),
|
||||
};
|
||||
|
||||
// 개발 환경에서는 스택 트레이스 포함
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
(errorResponse as any).stack = exception.stack;
|
||||
}
|
||||
|
||||
// 로깅
|
||||
console.error(
|
||||
`[${errorResponse.timestamp}] ${request.method} ${request.url} - ${status}`,
|
||||
message,
|
||||
);
|
||||
|
||||
response.status(status).json(errorResponse);
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP 상태 코드에 따른 에러 이름 반환
|
||||
*
|
||||
* @private
|
||||
* @param {number} status - HTTP 상태 코드
|
||||
* @returns {string}
|
||||
*/
|
||||
private getErrorName(status: number): string {
|
||||
switch (status) {
|
||||
case HttpStatus.BAD_REQUEST:
|
||||
return 'Bad Request';
|
||||
case HttpStatus.UNAUTHORIZED:
|
||||
return 'Unauthorized';
|
||||
case HttpStatus.FORBIDDEN:
|
||||
return 'Forbidden';
|
||||
case HttpStatus.NOT_FOUND:
|
||||
return 'Not Found';
|
||||
case HttpStatus.CONFLICT:
|
||||
return 'Conflict';
|
||||
case HttpStatus.INTERNAL_SERVER_ERROR:
|
||||
return 'Internal Server Error';
|
||||
default:
|
||||
return 'Error';
|
||||
}
|
||||
}
|
||||
}
|
||||
78
backend/src/common/guards/jwt-auth.guard.ts
Normal file
78
backend/src/common/guards/jwt-auth.guard.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { Injectable, ExecutionContext, UnauthorizedException } from '@nestjs/common';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
|
||||
/**
|
||||
* JWT 인증 가드
|
||||
*
|
||||
* @description
|
||||
* JWT 토큰 검증 가드입니다.
|
||||
* @Public() 데코레이터가 있는 엔드포인트는 인증을 건너뜁니다.
|
||||
*
|
||||
* @example
|
||||
* // 사용법 1: 특정 엔드포인트에 적용
|
||||
* @UseGuards(JwtAuthGuard)
|
||||
* @Get('profile')
|
||||
* getProfile(@Req() req) {
|
||||
* return req.user;
|
||||
* }
|
||||
*
|
||||
* @example
|
||||
* // 사용법 2: 전역 적용 (main.ts)
|
||||
* app.useGlobalGuards(new JwtAuthGuard(new Reflector()));
|
||||
*
|
||||
* @example
|
||||
* // 사용법 3: 인증 제외
|
||||
* @Public()
|
||||
* @Get('public')
|
||||
* getPublicData() {
|
||||
* return 'Anyone can access';
|
||||
* }
|
||||
*
|
||||
* @export
|
||||
* @class JwtAuthGuard
|
||||
* @extends {AuthGuard('jwt')}
|
||||
*/
|
||||
@Injectable()
|
||||
export class JwtAuthGuard extends AuthGuard('jwt') {
|
||||
constructor(private reflector: Reflector) {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* 인증 확인
|
||||
*
|
||||
* @param {ExecutionContext} context - 실행 컨텍스트
|
||||
* @returns {boolean | Promise<boolean>}
|
||||
*/
|
||||
canActivate(context: ExecutionContext) {
|
||||
// @Public() 데코레이터 확인
|
||||
const isPublic = this.reflector.getAllAndOverride<boolean>('isPublic', [
|
||||
context.getHandler(),
|
||||
context.getClass(),
|
||||
]);
|
||||
|
||||
// Public 엔드포인트는 인증 건너뜀
|
||||
if (isPublic) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// JWT 검증 수행
|
||||
return super.canActivate(context);
|
||||
}
|
||||
|
||||
/**
|
||||
* 요청 처리
|
||||
*
|
||||
* @param {any} err - 에러
|
||||
* @param {any} user - 사용자 정보
|
||||
* @param {any} info - 추가 정보
|
||||
* @returns {any}
|
||||
*/
|
||||
handleRequest(err: any, user: any, info: any) {
|
||||
if (err || !user) {
|
||||
throw err || new UnauthorizedException('인증이 필요합니다. 로그인 후 이용해주세요.');
|
||||
}
|
||||
return user;
|
||||
}
|
||||
}
|
||||
54
backend/src/common/interceptors/logging.interceptor.ts
Normal file
54
backend/src/common/interceptors/logging.interceptor.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import {
|
||||
Injectable,
|
||||
NestInterceptor,
|
||||
ExecutionContext,
|
||||
CallHandler,
|
||||
} from '@nestjs/common';
|
||||
import { Observable } from 'rxjs';
|
||||
import { tap } from 'rxjs/operators';
|
||||
import { Request } from 'express';
|
||||
|
||||
/**
|
||||
* 로깅 인터셉터
|
||||
*
|
||||
* @description
|
||||
* API 요청/응답을 로깅하고 실행 시간을 측정합니다.
|
||||
*
|
||||
* @example
|
||||
* // main.ts에서 전역 적용
|
||||
* app.useGlobalInterceptors(new LoggingInterceptor());
|
||||
*
|
||||
* @export
|
||||
* @class LoggingInterceptor
|
||||
* @implements {NestInterceptor}
|
||||
*/
|
||||
@Injectable()
|
||||
export class LoggingInterceptor implements NestInterceptor {
|
||||
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
|
||||
const request = context.switchToHttp().getRequest<Request>();
|
||||
const { method, url, ip } = request;
|
||||
const userAgent = request.get('user-agent') || '';
|
||||
const now = Date.now();
|
||||
|
||||
console.log(
|
||||
`[${new Date().toISOString()}] Incoming Request: ${method} ${url} - ${ip} - ${userAgent}`,
|
||||
);
|
||||
|
||||
return next.handle().pipe(
|
||||
tap({
|
||||
next: (data) => {
|
||||
const responseTime = Date.now() - now;
|
||||
console.log(
|
||||
`[${new Date().toISOString()}] Response: ${method} ${url} - ${responseTime}ms`,
|
||||
);
|
||||
},
|
||||
error: (error) => {
|
||||
const responseTime = Date.now() - now;
|
||||
console.error(
|
||||
`[${new Date().toISOString()}] Error Response: ${method} ${url} - ${responseTime}ms - ${error.message}`,
|
||||
);
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
56
backend/src/common/interceptors/transform.interceptor.ts
Normal file
56
backend/src/common/interceptors/transform.interceptor.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import {
|
||||
Injectable,
|
||||
NestInterceptor,
|
||||
ExecutionContext,
|
||||
CallHandler,
|
||||
} from '@nestjs/common';
|
||||
import { Observable } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
|
||||
/**
|
||||
* 응답 인터페이스 (이메일)
|
||||
*/
|
||||
export interface Response<T> {
|
||||
success: boolean;
|
||||
data: T;
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 응답 변환 인터셉터
|
||||
*
|
||||
* @description
|
||||
* 모든 API 응답을 일관된 형식으로 변환합니다.
|
||||
* { success: true, data: ..., timestamp: ... } 형태로 래핑합니다.
|
||||
*
|
||||
* @example
|
||||
* // main.ts에서 전역 적용
|
||||
* app.useGlobalInterceptors(new TransformInterceptor());
|
||||
*
|
||||
* @example
|
||||
* // 원래 응답: { name: "홍길동" }
|
||||
* // 변환 후: { success: true, data: { name: "홍길동" }, timestamp: "2024-01-01T00:00:00.000Z" }
|
||||
* TransformInterceptor는 모든 응답을 { success, data, timestamp } 구조로 감싼다.
|
||||
* response.data.data.verified → true
|
||||
*
|
||||
* @export
|
||||
* @class TransformInterceptor
|
||||
* @implements {NestInterceptor<T, Response<T>>}
|
||||
*/
|
||||
@Injectable()
|
||||
export class TransformInterceptor<T>
|
||||
implements NestInterceptor<T, Response<T>>
|
||||
{
|
||||
intercept(
|
||||
context: ExecutionContext,
|
||||
next: CallHandler,
|
||||
): Observable<Response<T>> {
|
||||
return next.handle().pipe(
|
||||
map((data) => ({
|
||||
success: true,
|
||||
data, //여기에 return 데이터 객체 전체 응답
|
||||
timestamp: new Date().toISOString(),
|
||||
})),
|
||||
);
|
||||
}
|
||||
}
|
||||
23
backend/src/common/jwt/jwt.module.ts
Normal file
23
backend/src/common/jwt/jwt.module.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { JwtModule as NestJwtModule, JwtModuleOptions } from '@nestjs/jwt';
|
||||
import { PassportModule } from '@nestjs/passport';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
PassportModule.register({ defaultStrategy: 'jwt' }),
|
||||
NestJwtModule.registerAsync({
|
||||
imports: [ConfigModule],
|
||||
inject: [ConfigService],
|
||||
useFactory: (configService: ConfigService): JwtModuleOptions => ({
|
||||
secret: configService.get<string>('JWT_SECRET'),
|
||||
signOptions: {
|
||||
expiresIn: configService.get<string>('JWT_EXPIRES_IN') || '24h',
|
||||
} as any,
|
||||
global: true,
|
||||
}),
|
||||
}),
|
||||
],
|
||||
exports: [NestJwtModule, PassportModule],
|
||||
})
|
||||
export class JwtModule {}
|
||||
45
backend/src/common/jwt/jwt.strategy.ts
Normal file
45
backend/src/common/jwt/jwt.strategy.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
||||
import { PassportStrategy } from '@nestjs/passport';
|
||||
import { ExtractJwt, Strategy } from 'passport-jwt';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
/**
|
||||
* JWT 전략
|
||||
*
|
||||
* @description
|
||||
* JWT 토큰을 검증하고 사용자 정보를 추출하는 전략입니다.
|
||||
* Bearer 토큰에서 JWT를 추출하여 검증합니다.
|
||||
*
|
||||
* @export
|
||||
* @class JwtStrategy
|
||||
* @extends {PassportStrategy(Strategy)}
|
||||
*/
|
||||
@Injectable()
|
||||
export class JwtStrategy extends PassportStrategy(Strategy) {
|
||||
constructor(private configService: ConfigService) {
|
||||
super({
|
||||
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||
ignoreExpiration: false, //false로 설정하면 토큰 만료 시 인증 실패
|
||||
secretOrKey: configService.get<string>('JWT_SECRET') || 'your-secret-key',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* JWT 페이로드 검증 및 사용자 정보 반환
|
||||
*
|
||||
* @param {any} payload - JWT 페이로드
|
||||
* @returns {any} 검증된 사용자 정보
|
||||
*/
|
||||
validate(payload: any) {
|
||||
if (!payload.userId) {
|
||||
throw new UnauthorizedException('유효하지 않은 토큰입니다.');
|
||||
}
|
||||
|
||||
// Request 객체의 user 속성에 저장됨
|
||||
return {
|
||||
userId: payload.userId,
|
||||
userNo: payload.userNo,
|
||||
role: payload.role || 'user',
|
||||
};
|
||||
}
|
||||
}
|
||||
52
backend/src/common/utils/get-client-ip.ts
Normal file
52
backend/src/common/utils/get-client-ip.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { Request } from 'express';
|
||||
|
||||
/**
|
||||
* 클라이언트의 실제 IP 주소를 추출합니다.
|
||||
*
|
||||
* @description
|
||||
* Proxy, Load Balancer, CDN 뒤에 있어도 실제 클라이언트 IP를 정확하게 가져옵니다.
|
||||
* 다음 순서로 IP를 확인합니다:
|
||||
* 1. X-Forwarded-For 헤더 (Proxy/Load Balancer)
|
||||
* 2. X-Real-IP 헤더 (Nginx)
|
||||
* 3. req.ip (Express 기본)
|
||||
* 4. req.socket.remoteAddress (직접 연결)
|
||||
* 5. 'unknown' (IP를 찾을 수 없는 경우)
|
||||
*
|
||||
* @param req - Express Request 객체
|
||||
* @returns 클라이언트 IP 주소
|
||||
*
|
||||
* @example
|
||||
* const ip = getClientIp(req);
|
||||
* console.log(ip); // '203.123.45.67' or 'unknown'
|
||||
*/
|
||||
export function getClientIp(req: Request): string {
|
||||
// 1. X-Forwarded-For 헤더 확인 (Proxy/Load Balancer 환경)
|
||||
// 형식: "client IP, proxy1 IP, proxy2 IP"
|
||||
const forwardedFor = req.headers['x-forwarded-for'];
|
||||
if (forwardedFor) {
|
||||
// 배열이면 첫 번째 요소, 문자열이면 콤마로 split
|
||||
const ips = Array.isArray(forwardedFor)
|
||||
? forwardedFor[0]
|
||||
: forwardedFor.split(',')[0];
|
||||
return ips.trim();
|
||||
}
|
||||
|
||||
// 2. X-Real-IP 헤더 확인 (Nginx 환경)
|
||||
const realIp = req.headers['x-real-ip'];
|
||||
if (realIp && typeof realIp === 'string') {
|
||||
return realIp.trim();
|
||||
}
|
||||
|
||||
// 3. Express가 제공하는 req.ip
|
||||
if (req.ip) {
|
||||
return req.ip;
|
||||
}
|
||||
|
||||
// 4. Socket의 remoteAddress
|
||||
if (req.socket?.remoteAddress) {
|
||||
return req.socket.remoteAddress;
|
||||
}
|
||||
|
||||
// 5. IP를 찾을 수 없는 경우
|
||||
return 'unknown';
|
||||
}
|
||||
Reference in New Issue
Block a user