From d768b8dcef28a5c9f125b80d33c383353991af13 Mon Sep 17 00:00:00 2001 From: Hyoseong Jo Date: Mon, 15 Dec 2025 10:15:28 +0900 Subject: [PATCH] =?UTF-8?q?redis=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/.env | 61 +++++++ backend/.gitignore | 6 - backend/package.json | 4 +- backend/src/app.module.ts | 88 +++++----- backend/src/redis/redis.module.ts | 32 ---- .../verification/verification.module.ts | 11 +- .../verification/verification.service.ts | 166 ++++++++---------- backend/src/system/system.service.ts | 38 +--- frontend/.env | 18 ++ frontend/.gitignore | 9 - 10 files changed, 205 insertions(+), 228 deletions(-) create mode 100644 backend/.env delete mode 100644 backend/src/redis/redis.module.ts create mode 100644 frontend/.env diff --git a/backend/.env b/backend/.env new file mode 100644 index 0000000..70929a9 --- /dev/null +++ b/backend/.env @@ -0,0 +1,61 @@ +# ============================================== +# DATABASE CONFIGURATION +# ============================================== +POSTGRES_HOST=192.168.11.46 +POSTGRES_USER=genome +POSTGRES_PASSWORD=genome1@3 +POSTGRES_DB=genome_db +POSTGRES_PORT=5431 +POSTGRES_SYNCHRONIZE=true +POSTGRES_LOGGING=true + +# ============================================== +# BACKEND CONFIGURATION +# ============================================== +BACKEND_PORT=4000 +NODE_ENV=development + +# ============================================== +# JWT AUTHENTICATION +# ============================================== +JWT_SECRET=your-super-secret-jwt-key-change-this-in-production +JWT_EXPIRES_IN=24h +JWT_REFRESH_SECRET=your-refresh-token-secret +JWT_REFRESH_EXPIRES_IN=7d + +# ============================================== +# CORS CONFIGURATION +# ============================================== +CORS_ORIGIN=http://localhost:3000,http://192.168.11.46:3000,http://123.143.174.11:5244 +CORS_CREDENTIALS=true + +# ============================================== +# SECURITY SETTINGS +# ============================================== +RATE_LIMIT_WINDOW_MS=900000 +RATE_LIMIT_MAX_REQUESTS=100 +BCRYPT_SALT_ROUNDS=12 + +# ============================================== +# FILE UPLOAD +# ============================================== +MAX_FILE_SIZE=10485760 +UPLOAD_DESTINATION=./uploads +ALLOWED_FILE_TYPES=jpg,jpeg,png,gif,pdf,doc,docx + +# ============================================== +# EMAIL CONFIGURATION (SMTP) +# ============================================== +SMTP_HOST=smtp.gmail.com +SMTP_PORT=587 +SMTP_USER=turbosoft11@gmail.com +SMTP_PASS="kojl sxbx pdfi yhxz" +FROM_EMAIL=turbosoft11@gmail.com + +# ============================================== +# LOGGING +# ============================================== +LOG_LEVEL=debug +LOG_FORMAT=dev +LOG_FILE_ENABLED=true +LOG_FILE_PATH=./logs diff --git a/backend/.gitignore b/backend/.gitignore index b46f203..2b2f1e6 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -13,12 +13,6 @@ package-lock.json # ============================================== # Environment Variables (민감정보) # ============================================== -.env -.env.local -.env.development.local -.env.test.local -.env.production.local -# .env.example은 커밋 가능 # ============================================== # Logs diff --git a/backend/package.json b/backend/package.json index a8f5740..116bbf6 100644 --- a/backend/package.json +++ b/backend/package.json @@ -20,7 +20,7 @@ "test:e2e": "jest --config ./test/jest-e2e.json" }, "dependencies": { - "@nestjs-modules/ioredis": "^2.0.2", + "@nestjs/common": "^11.0.1", "@nestjs/config": "^4.0.2", "@nestjs/core": "^11.0.1", @@ -37,7 +37,7 @@ "bcrypt": "^6.0.0", "class-transformer": "^0.5.1", "class-validator": "^0.14.2", - "ioredis": "^5.8.1", + "multer": "^2.0.2", "nodemailer": "^7.0.9", "passport": "^0.7.0", diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index 0e0001c..bced14d 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -3,7 +3,6 @@ import { AppController } from './app.controller'; import { AppService } from './app.service'; import { TypeOrmModule } from '@nestjs/typeorm'; import { ConfigModule, ConfigService } from '@nestjs/config'; -import { RedisModule } from './redis/redis.module'; import { AuthModule } from './auth/auth.module'; import { UserModule } from './user/user.module'; import { CommonModule } from './common/common.module'; @@ -22,49 +21,48 @@ import { GeneModule } from './gene/gene.module'; import { SystemModule } from './system/system.module'; @Module({ - imports: [ - ConfigModule.forRoot({ - isGlobal: true, - }), - TypeOrmModule.forRootAsync({ - imports: [ConfigModule], - inject: [ConfigService], - useFactory: (configService: ConfigService) => ({ - type: 'postgres', - host: configService.get('POSTGRES_HOST'), - port: configService.get('POSTGRES_PORT'), - username: configService.get('POSTGRES_USER'), - password: configService.get('POSTGRES_PASSWORD'), - database: configService.get('POSTGRES_DB'), - synchronize: configService.get('POSTGRES_SYNCHRONIZE'), - logging: configService.get('POSTGRES_LOGGING'), - autoLoadEntities: true, - entities: [], - }), - }), - // 인프라 모듈 - RedisModule, - JwtModule, - CommonModule, - SharedModule, - - // 인증/사용자 모듈 - AuthModule, - UserModule, - - // 비즈니스 모듈 - FarmModule, - CowModule, - GenomeModule, - GeneModule, - MptModule, - DashboardModule, - - // 기타 - HelpModule, - SystemModule, - ], - controllers: [AppController], - providers: [AppService, JwtStrategy], + imports: [ + ConfigModule.forRoot({ + isGlobal: true, + }), + TypeOrmModule.forRootAsync({ + imports: [ConfigModule], + inject: [ConfigService], + useFactory: (configService: ConfigService) => ({ + type: 'postgres', + host: configService.get('POSTGRES_HOST'), + port: configService.get('POSTGRES_PORT'), + username: configService.get('POSTGRES_USER'), + password: configService.get('POSTGRES_PASSWORD'), + database: configService.get('POSTGRES_DB'), + synchronize: configService.get('POSTGRES_SYNCHRONIZE'), + logging: configService.get('POSTGRES_LOGGING'), + autoLoadEntities: true, + entities: [], + }), + }), + // 인프라 모듈 + JwtModule, + CommonModule, + SharedModule, + + // 인증/사용자 모듈 + AuthModule, + UserModule, + + // 비즈니스 모듈 + FarmModule, + CowModule, + GenomeModule, + GeneModule, + MptModule, + DashboardModule, + + // 기타 + HelpModule, + SystemModule, + ], + controllers: [AppController], + providers: [AppService, JwtStrategy], }) export class AppModule {} diff --git a/backend/src/redis/redis.module.ts b/backend/src/redis/redis.module.ts deleted file mode 100644 index 5589a7c..0000000 --- a/backend/src/redis/redis.module.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { Module, Global } from '@nestjs/common'; -import { RedisModule as NestRedisModule } from '@nestjs-modules/ioredis'; -import { ConfigModule, ConfigService } from '@nestjs/config'; - -/** - * RedisModule - * - * @description - * Redis 연결 설정을 담당하는 전역 모듈입니다. - * 캐시, 세션 관리, 인증번호 저장 등에 사용됩니다. - * - * @Global 데코레이터로 전역에서 사용 가능하므로, - * 필요한 서비스에서 @InjectRedis()로 바로 주입할 수 있습니다. - * - * @export - * @class RedisModule - */ -@Global() -@Module({ - imports: [ - NestRedisModule.forRootAsync({ - imports: [ConfigModule], - inject: [ConfigService], - useFactory: (configService: ConfigService) => ({ - type: 'single', - url: configService.get('REDIS_URL'), - }), - }), - ], - exports: [NestRedisModule], -}) -export class RedisModule {} diff --git a/backend/src/shared/verification/verification.module.ts b/backend/src/shared/verification/verification.module.ts index 487b73c..16dd47a 100644 --- a/backend/src/shared/verification/verification.module.ts +++ b/backend/src/shared/verification/verification.module.ts @@ -6,20 +6,15 @@ import { VerificationService } from './verification.service'; * * @description * 인증번호 생성 및 검증 기능을 제공하는 모듈입니다. - * Redis를 사용하여 인증번호를 임시 저장하고 검증합니다. + * 메모리 Map을 사용하여 인증번호를 임시 저장하고 검증합니다. * * 사용 예: * - 아이디 찾기 인증번호 발송 * - 비밀번호 재설정 인증번호 발송 * - 회원가입 이메일 인증 - * - * RedisModule이 @Global로 설정되어 있어 자동으로 주입됩니다. - * - * @export - * @class VerificationModule */ @Module({ - providers: [VerificationService], - exports: [VerificationService], + providers: [VerificationService], + exports: [VerificationService], }) export class VerificationModule {} diff --git a/backend/src/shared/verification/verification.service.ts b/backend/src/shared/verification/verification.service.ts index e7dff22..a52ed5f 100644 --- a/backend/src/shared/verification/verification.service.ts +++ b/backend/src/shared/verification/verification.service.ts @@ -1,32 +1,40 @@ import { Injectable, Logger } from '@nestjs/common'; -import { InjectRedis } from '@nestjs-modules/ioredis'; -import Redis from 'ioredis'; import * as crypto from 'crypto'; import { VERIFICATION_CONFIG } from 'src/common/config/VerificationConfig'; /** - * 인증번호 생성 및 검증 서비스 (Redis 기반) + * 인증번호 생성 및 검증 서비스 (메모리 기반) */ @Injectable() export class VerificationService { private readonly logger = new Logger(VerificationService.name); + + // 메모리 저장소 (key -> { code, expiresAt }) + private readonly store = new Map(); - constructor(@InjectRedis() private readonly redis: Redis) { - this.logger.log(`[REDIS] VerificationService 초기화`); - process.stdout.write(`[REDIS] VerificationService initialized\n`); + constructor() { + this.logger.log(`[VERIFY] VerificationService 초기화 (메모리 모드)`); - // Redis 연결 상태 로깅 - this.checkRedisConnection(); + // 만료된 항목 정리 (1분마다) + setInterval(() => this.cleanup(), 60000); } - private async checkRedisConnection(): Promise { - try { - const pong = await this.redis.ping(); - this.logger.log(`[REDIS] 연결 상태: ${pong}`); - process.stdout.write(`[REDIS] Connection status: ${pong}\n`); - } catch (error) { - this.logger.error(`[REDIS] 연결 실패: ${error.message}`); - process.stdout.write(`[REDIS] Connection FAILED: ${error.message}\n`); + /** + * 만료된 항목 정리 + */ + private cleanup(): void { + const now = Date.now(); + let cleaned = 0; + + for (const [key, data] of this.store.entries()) { + if (data.expiresAt < now) { + this.store.delete(key); + cleaned++; + } + } + + if (cleaned > 0) { + this.logger.debug(`[VERIFY] 만료된 ${cleaned}개 항목 정리됨`); } } @@ -35,109 +43,89 @@ export class VerificationService { */ generateCode(): string { const code = Math.floor(100000 + Math.random() * 900000).toString(); - this.logger.log(`[REDIS] 인증번호 생성: ${code}`); + this.logger.log(`[VERIFY] 인증번호 생성: ${code}`); return code; } /** - * 인증번호 저장 (Redis) + * 인증번호 저장 (TTL 적용) */ async saveCode(key: string, code: string): Promise { - this.logger.log(`[REDIS] ========== 코드 저장 시작 ==========`); - this.logger.log(`[REDIS] Key: ${key}`); - this.logger.log(`[REDIS] Code: ${code}`); - this.logger.log(`[REDIS] TTL: ${VERIFICATION_CONFIG.CODE_EXPIRY_SECONDS}초`); - process.stdout.write(`[REDIS] Saving - Key: ${key}, Code: ${code}\n`); - - try { - const result = await this.redis.set(key, code, 'EX', VERIFICATION_CONFIG.CODE_EXPIRY_SECONDS); - this.logger.log(`[REDIS] 저장 결과: ${result}`); - process.stdout.write(`[REDIS] Save result: ${result}\n`); - } catch (error) { - this.logger.error(`[REDIS] ========== 저장 실패 ==========`); - this.logger.error(`[REDIS] Error: ${error.message}`); - this.logger.error(`[REDIS] Stack: ${error.stack}`); - process.stdout.write(`[REDIS] SAVE ERROR: ${error.message}\n`); - process.stdout.write(`[REDIS] STACK: ${error.stack}\n`); - throw error; - } + const expiresAt = Date.now() + (VERIFICATION_CONFIG.CODE_EXPIRY_SECONDS * 1000); + + this.store.set(key, { value: code, expiresAt }); + + this.logger.log(`[VERIFY] 코드 저장 - Key: ${key}, TTL: ${VERIFICATION_CONFIG.CODE_EXPIRY_SECONDS}초`); } /** * 인증번호 검증 */ async verifyCode(key: string, code: string): Promise { - this.logger.log(`[REDIS] ========== 코드 검증 시작 ==========`); - this.logger.log(`[REDIS] Key: ${key}`); - this.logger.log(`[REDIS] Input Code: ${code}`); - process.stdout.write(`[REDIS] Verifying - Key: ${key}, Input: ${code}\n`); + this.logger.log(`[VERIFY] 코드 검증 - Key: ${key}, Input: ${code}`); - try { - const savedCode = await this.redis.get(key); - this.logger.log(`[REDIS] Saved Code: ${savedCode}`); - process.stdout.write(`[REDIS] Saved code: ${savedCode}\n`); + const data = this.store.get(key); - if (!savedCode) { - this.logger.warn(`[REDIS] 저장된 코드 없음 (만료 또는 미발급)`); - return false; - } - - if (savedCode !== code) { - this.logger.warn(`[REDIS] 코드 불일치 - Saved: ${savedCode}, Input: ${code}`); - return false; - } - - await this.redis.del(key); - this.logger.log(`[REDIS] 검증 성공, 코드 삭제됨`); - return true; - - } catch (error) { - this.logger.error(`[REDIS] ========== 검증 실패 ==========`); - this.logger.error(`[REDIS] Error: ${error.message}`); - this.logger.error(`[REDIS] Stack: ${error.stack}`); - process.stdout.write(`[REDIS] VERIFY ERROR: ${error.message}\n`); - throw error; + if (!data) { + this.logger.warn(`[VERIFY] 저장된 코드 없음 (만료 또는 미발급)`); + return false; } + + // 만료 체크 + if (data.expiresAt < Date.now()) { + this.logger.warn(`[VERIFY] 코드 만료됨`); + this.store.delete(key); + return false; + } + + if (data.value !== code) { + this.logger.warn(`[VERIFY] 코드 불일치 - Saved: ${data.value}, Input: ${code}`); + return false; + } + + // 검증 성공 시 삭제 (1회용) + this.store.delete(key); + this.logger.log(`[VERIFY] 검증 성공, 코드 삭제됨`); + return true; } /** * 비밀번호 재설정 토큰 생성 및 저장 */ async generateResetToken(userId: string): Promise { - this.logger.log(`[REDIS] 리셋 토큰 생성 - userId: ${userId}`); + this.logger.log(`[VERIFY] 리셋 토큰 생성 - userId: ${userId}`); - try { - const token = crypto.randomBytes(VERIFICATION_CONFIG.TOKEN_BYTES_LENGTH).toString('hex'); - await this.redis.set(`reset:${token}`, userId, 'EX', VERIFICATION_CONFIG.RESET_TOKEN_EXPIRY_SECONDS); - this.logger.log(`[REDIS] 리셋 토큰 저장 완료`); - return token; - } catch (error) { - this.logger.error(`[REDIS] 리셋 토큰 생성 실패: ${error.message}`); - throw error; - } + const token = crypto.randomBytes(VERIFICATION_CONFIG.TOKEN_BYTES_LENGTH).toString('hex'); + const expiresAt = Date.now() + (VERIFICATION_CONFIG.RESET_TOKEN_EXPIRY_SECONDS * 1000); + + this.store.set(`reset:${token}`, { value: userId, expiresAt }); + + this.logger.log(`[VERIFY] 리셋 토큰 저장 완료`); + return token; } /** * 비밀번호 재설정 토큰 검증 */ async verifyResetToken(token: string): Promise { - this.logger.log(`[REDIS] 리셋 토큰 검증`); + this.logger.log(`[VERIFY] 리셋 토큰 검증`); - try { - const userId = await this.redis.get(`reset:${token}`); + const key = `reset:${token}`; + const data = this.store.get(key); - if (!userId) { - this.logger.warn(`[REDIS] 리셋 토큰 없음 또는 만료`); - return null; - } - - await this.redis.del(`reset:${token}`); - this.logger.log(`[REDIS] 리셋 토큰 검증 성공 - userId: ${userId}`); - return userId; - - } catch (error) { - this.logger.error(`[REDIS] 리셋 토큰 검증 실패: ${error.message}`); - throw error; + if (!data) { + this.logger.warn(`[VERIFY] 리셋 토큰 없음 또는 만료`); + return null; } + + if (data.expiresAt < Date.now()) { + this.logger.warn(`[VERIFY] 리셋 토큰 만료됨`); + this.store.delete(key); + return null; + } + + this.store.delete(key); + this.logger.log(`[VERIFY] 리셋 토큰 검증 성공 - userId: ${data.value}`); + return data.value; } } diff --git a/backend/src/system/system.service.ts b/backend/src/system/system.service.ts index f0be4c0..b4c4e51 100644 --- a/backend/src/system/system.service.ts +++ b/backend/src/system/system.service.ts @@ -2,8 +2,6 @@ import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { InjectDataSource } from '@nestjs/typeorm'; import { DataSource } from 'typeorm'; -import { InjectRedis } from '@nestjs-modules/ioredis'; -import Redis from 'ioredis'; export interface SystemHealthResponse { status: 'ok' | 'error'; @@ -17,12 +15,6 @@ export interface SystemHealthResponse { status: 'connected' | 'disconnected'; error?: string; }; - redis: { - host: string; - port: number; - status: 'connected' | 'disconnected'; - error?: string; - }; } @Injectable() @@ -30,19 +22,16 @@ export class SystemService { constructor( private configService: ConfigService, @InjectDataSource() private dataSource: DataSource, - @InjectRedis() private redis: Redis, ) {} async getHealth(): Promise { const dbHealth = await this.checkDatabase(); - const redisHealth = await this.checkRedis(); return { - status: dbHealth.status === 'connected' && redisHealth.status === 'connected' ? 'ok' : 'error', + status: dbHealth.status === 'connected' ? 'ok' : 'error', timestamp: new Date().toISOString(), environment: this.configService.get('NODE_ENV') || 'development', database: dbHealth, - redis: redisHealth, }; } @@ -61,29 +50,4 @@ export class SystemService { return { ...config, status: 'disconnected' as const, error: error.message }; } } - - private async checkRedis() { - const redisUrl = this.configService.get('REDIS_URL') || ''; - let host = this.configService.get('REDIS_HOST') || 'unknown'; - let port = parseInt(this.configService.get('REDIS_PORT')) || 6379; - - if (redisUrl) { - try { - const url = new URL(redisUrl); - host = url.hostname; - port = parseInt(url.port) || 6379; - } catch {} - } - - try { - const pong = await this.redis.ping(); - return { - host, - port, - status: pong === 'PONG' ? 'connected' as const : 'disconnected' as const, - }; - } catch (error) { - return { host, port, status: 'disconnected' as const, error: error.message }; - } - } } diff --git a/frontend/.env b/frontend/.env new file mode 100644 index 0000000..7ddcbf8 --- /dev/null +++ b/frontend/.env @@ -0,0 +1,18 @@ +# ============================================== +# FRONTEND CONFIGURATION +# ============================================== +NODE_ENV=development + +# ============================================== +# API CONFIGURATION +# ============================================== +# 로컬 개발: http://localhost:4000 +# Docker 개발: http://backend:4000 +# 프로덕션: https://your-domain.com +NEXT_PUBLIC_API_URL=/backend/api + +# ============================================== +# APP SETTINGS +# ============================================== +NEXT_PUBLIC_APP_NAME=한우 유전능력 시스템 +NEXT_TELEMETRY_DISABLED=1 diff --git a/frontend/.gitignore b/frontend/.gitignore index df78db4..ca0989d 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -18,15 +18,6 @@ package-lock.json /out/ /build -# ============================================== -# Environment Variables (민감정보) -# ============================================== -.env -.env.local -.env.development.local -.env.test.local -.env.production.local -# .env.example은 커밋 가능 # ============================================== # Testing