redis 제거

This commit is contained in:
2025-12-15 10:15:28 +09:00
parent 2d06f2fcbc
commit d768b8dcef
10 changed files with 205 additions and 228 deletions

61
backend/.env Normal file
View File

@@ -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

6
backend/.gitignore vendored
View File

@@ -13,12 +13,6 @@ package-lock.json
# ==============================================
# Environment Variables (민감정보)
# ==============================================
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# .env.example은 커밋 가능
# ==============================================
# Logs

View File

@@ -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",

View File

@@ -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';
@@ -43,7 +42,6 @@ import { SystemModule } from './system/system.module';
}),
}),
// 인프라 모듈
RedisModule,
JwtModule,
CommonModule,
SharedModule,

View File

@@ -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 {}

View File

@@ -6,17 +6,12 @@ import { VerificationService } from './verification.service';
*
* @description
* 인증번호 생성 및 검증 기능을 제공하는 모듈입니다.
* Redis를 사용하여 인증번호를 임시 저장하고 검증합니다.
* 메모리 Map을 사용하여 인증번호를 임시 저장하고 검증합니다.
*
* 사용 예:
* - 아이디 찾기 인증번호 발송
* - 비밀번호 재설정 인증번호 발송
* - 회원가입 이메일 인증
*
* RedisModule이 @Global로 설정되어 있어 자동으로 주입됩니다.
*
* @export
* @class VerificationModule
*/
@Module({
providers: [VerificationService],

View File

@@ -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);
constructor(@InjectRedis() private readonly redis: Redis) {
this.logger.log(`[REDIS] VerificationService 초기화`);
process.stdout.write(`[REDIS] VerificationService initialized\n`);
// 메모리 저장소 (key -> { code, expiresAt })
private readonly store = new Map<string, { value: string; expiresAt: number }>();
// Redis 연결 상태 로깅
this.checkRedisConnection();
constructor() {
this.logger.log(`[VERIFY] VerificationService 초기화 (메모리 모드)`);
// 만료된 항목 정리 (1분마다)
setInterval(() => this.cleanup(), 60000);
}
private async checkRedisConnection(): Promise<void> {
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<void> {
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`);
const expiresAt = Date.now() + (VERIFICATION_CONFIG.CODE_EXPIRY_SECONDS * 1000);
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;
}
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<boolean> {
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] 저장된 코드 없음 (만료 또는 미발급)`);
if (!data) {
this.logger.warn(`[VERIFY] 저장된 코드 없음 (만료 또는 미발급)`);
return false;
}
if (savedCode !== code) {
this.logger.warn(`[REDIS] 코드 불일치 - Saved: ${savedCode}, Input: ${code}`);
// 만료 체크
if (data.expiresAt < Date.now()) {
this.logger.warn(`[VERIFY] 코드 만료됨`);
this.store.delete(key);
return false;
}
await this.redis.del(key);
this.logger.log(`[REDIS] 검증 성공, 코드 삭제됨`);
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;
} 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;
}
}
/**
* 비밀번호 재설정 토큰 생성 및 저장
*/
async generateResetToken(userId: string): Promise<string> {
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] 리셋 토큰 저장 완료`);
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;
} catch (error) {
this.logger.error(`[REDIS] 리셋 토큰 생성 실패: ${error.message}`);
throw error;
}
}
/**
* 비밀번호 재설정 토큰 검증
*/
async verifyResetToken(token: string): Promise<string | null> {
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] 리셋 토큰 없음 또는 만료`);
if (!data) {
this.logger.warn(`[VERIFY] 리셋 토큰 없음 또는 만료`);
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.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;
}
}

View File

@@ -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<SystemHealthResponse> {
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 };
}
}
}

18
frontend/.env Normal file
View File

@@ -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

9
frontend/.gitignore vendored
View File

@@ -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