import { Logger, Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { ConfigModule } from '@nestjs/config'; import { ScheduleModule } from '@nestjs/schedule'; import { ThrottlerGuard, ThrottlerModule } from '@nestjs/throttler'; import { CacheModule } from '@nestjs/cache-manager'; import { createKeyv } from '@keyv/redis'; import type Keyv from 'keyv'; import { APP_GUARD } from '@nestjs/core'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import { HealthModule } from './modules/health/health.module'; import { UserModule } from './modules/user/user.module'; import { AuthModule } from './modules/auth/auth.module'; import { RepoModule } from './modules/repo/repo.module'; import { TaskModule } from './modules/task/task.module'; import { ActivityModule } from './modules/activity/activity.module'; import { NotificationModule } from './modules/notification/notification.module'; @Module({ imports: [ ScheduleModule.forRoot(), ConfigModule.forRoot({ isGlobal: true, envFilePath: `.env.${process.env.NODE_ENV || 'development'}`, }), // 전역 캐시 매니저 — REDIS_* 설정 시 Redis(keyv) 스토어, 없으면 인메모리 폴백. // docker-compose 가 REDIS_HOST/PORT/PASSWORD 를 주입한다(단독 실행/테스트는 미설정 → 인메모리). CacheModule.registerAsync({ isGlobal: true, useFactory: (): { ttl: number; stores?: Keyv[] } => { const logger = new Logger('CacheModule'); const options: { ttl: number; stores?: Keyv[] } = { ttl: 60_000 }; const host = process.env.REDIS_HOST; if (!host) { // Redis 미설정 — 인메모리 폴백(LRU). 로컬/테스트에서 Redis 없이도 동작. // 주의: 다중 인스턴스를 인메모리 모드로 띄우면 인스턴스별 캐시·무효화가 분리됨(단일 인스턴스 전용) logger.log('캐시: 인메모리 모드(단일 인스턴스) — REDIS_HOST 미설정'); return options; } const port = process.env.REDIS_PORT || '6379'; const password = process.env.REDIS_PASSWORD; const auth = password ? `:${encodeURIComponent(password)}@` : ''; const keyv = createKeyv(`redis://${auth}${host}:${port}`); // Redis 연결 오류가 unhandled 로 프로세스를 죽이지 않도록 흡수(캐시 장애는 DB 조회로 폴백) keyv.on('error', (err: unknown) => { logger.warn( `Redis 캐시 오류(무시, DB 폴백): ${err instanceof Error ? err.message : String(err)}`, ); }); options.stores = [keyv]; logger.log('캐시: Redis 모드(다중 인스턴스 무효화 정합)'); return options; }, }), // 전역 Rate Limiter — main.ts 의 express-rate-limit 와 별개로 라우트 단위 제어 가능 ThrottlerModule.forRoot([ { ttl: 60_000, limit: 100, }, ]), TypeOrmModule.forRootAsync({ useFactory: () => { const isProduction = process.env.NODE_ENV === 'production'; return { type: 'postgres', host: process.env.DB_HOST || 'db', port: parseInt(process.env.DB_PORT || '5432', 10), username: process.env.DB_USER || process.env.DB_USERNAME || 'postgres', password: process.env.DB_PASSWORD || 'password', database: process.env.DB_NAME || process.env.DB_DATABASE || 'project_db', autoLoadEntities: true, // 운영 환경에서는 절대 synchronize 사용 금지 (스키마 자동 변경 → 데이터 손실 위험) synchronize: !isProduction, migrationsRun: isProduction, logging: !isProduction, extra: { max: 100, connectionTimeoutMillis: 5000, idleTimeoutMillis: 30000, }, entities: [__dirname + '/**/*.entity{.ts,.js}'], migrations: [__dirname + '/migrations/*{.ts,.js}'], }; }, }), HealthModule, UserModule, AuthModule, RepoModule, TaskModule, ActivityModule, NotificationModule, ], controllers: [AppController], providers: [ AppService, { provide: APP_GUARD, useClass: ThrottlerGuard, }, ], }) export class AppModule {}