Merge branch 'main' of http://gitea.turbosoft.kr:80/turbosoft/genome2025
This commit is contained in:
102
.env
102
.env
@@ -1,102 +0,0 @@
|
|||||||
# ==============================================
|
|
||||||
# DEVELOPMENT ENVIRONMENT VARIABLES
|
|
||||||
# ==============================================
|
|
||||||
# Copy this file to .env.local for local development
|
|
||||||
# DO NOT commit sensitive values to version control
|
|
||||||
|
|
||||||
# ==============================================
|
|
||||||
# DATABASE CONFIGURATION
|
|
||||||
# ==============================================
|
|
||||||
DATABASE_URL=postgresql://genome:genome1@3@192.168.11.46:5431/genome_db
|
|
||||||
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
|
|
||||||
|
|
||||||
# ==============================================
|
|
||||||
# REDIS CONFIGURATION
|
|
||||||
# ==============================================
|
|
||||||
REDIS_URL=redis://192.168.11.46:6379
|
|
||||||
REDIS_HOST=192.168.11.46
|
|
||||||
REDIS_PORT=6379
|
|
||||||
|
|
||||||
# ==============================================
|
|
||||||
# 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.249: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_HOST=smtp.gmail.com
|
|
||||||
SMTP_PORT=587
|
|
||||||
SMTP_SECURE=false
|
|
||||||
SMTP_USER=your-email@gmail.com
|
|
||||||
SMTP_PASS=your-app-password
|
|
||||||
FROM_EMAIL=noreply@yourdomain.com
|
|
||||||
|
|
||||||
# ==============================================
|
|
||||||
# LOGGING
|
|
||||||
# ==============================================
|
|
||||||
LOG_LEVEL=debug
|
|
||||||
LOG_FORMAT=dev
|
|
||||||
LOG_FILE_ENABLED=true
|
|
||||||
LOG_FILE_PATH=./logs
|
|
||||||
|
|
||||||
# ==============================================
|
|
||||||
# EXTERNAL SERVICES
|
|
||||||
# ==============================================
|
|
||||||
# AWS_ACCESS_KEY_ID=your-aws-access-key
|
|
||||||
# AWS_SECRET_ACCESS_KEY=your-aws-secret
|
|
||||||
# AWS_REGION=us-east-1
|
|
||||||
# AWS_S3_BUCKET=your-bucket-name
|
|
||||||
|
|
||||||
# ==============================================
|
|
||||||
# MONITORING
|
|
||||||
# ==============================================
|
|
||||||
# SENTRY_DSN=your-sentry-dsn
|
|
||||||
# HEALTH_CHECK_ENABLED=true
|
|
||||||
|
|
||||||
# ==============================================
|
|
||||||
# FRONTEND CONFIGURATION
|
|
||||||
# ==============================================
|
|
||||||
FRONTEND_PORT=3000
|
|
||||||
NEXT_PUBLIC_API_URL=/api
|
|
||||||
|
|
||||||
# ==============================================
|
|
||||||
# NGINX CONFIGURATION
|
|
||||||
# ==============================================
|
|
||||||
NGINX_HTTP_PORT=80
|
|
||||||
NGINX_HTTPS_PORT=443
|
|
||||||
102
.env.development
102
.env.development
@@ -1,102 +0,0 @@
|
|||||||
# ==============================================
|
|
||||||
# DEVELOPMENT ENVIRONMENT VARIABLES
|
|
||||||
# ==============================================
|
|
||||||
# Copy this file to .env.local for local development
|
|
||||||
# DO NOT commit sensitive values to version control
|
|
||||||
|
|
||||||
# ==============================================
|
|
||||||
# DATABASE CONFIGURATION
|
|
||||||
# ==============================================
|
|
||||||
DATABASE_URL=postgresql://user:password@localhost:5432/genome_db
|
|
||||||
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
|
|
||||||
|
|
||||||
# ==============================================
|
|
||||||
# REDIS CONFIGURATION
|
|
||||||
# ==============================================
|
|
||||||
REDIS_URL=redis://192.168.11.46:6379
|
|
||||||
REDIS_HOST=localhost
|
|
||||||
REDIS_PORT=6379
|
|
||||||
|
|
||||||
# ==============================================
|
|
||||||
# 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_HOST=smtp.gmail.com
|
|
||||||
SMTP_PORT=587
|
|
||||||
SMTP_SECURE=false
|
|
||||||
SMTP_USER=your-email@gmail.com
|
|
||||||
SMTP_PASS=your-app-password
|
|
||||||
FROM_EMAIL=noreply@yourdomain.com
|
|
||||||
|
|
||||||
# ==============================================
|
|
||||||
# LOGGING
|
|
||||||
# ==============================================
|
|
||||||
LOG_LEVEL=debug
|
|
||||||
LOG_FORMAT=dev
|
|
||||||
LOG_FILE_ENABLED=true
|
|
||||||
LOG_FILE_PATH=./logs
|
|
||||||
|
|
||||||
# ==============================================
|
|
||||||
# EXTERNAL SERVICES
|
|
||||||
# ==============================================
|
|
||||||
# AWS_ACCESS_KEY_ID=your-aws-access-key
|
|
||||||
# AWS_SECRET_ACCESS_KEY=your-aws-secret
|
|
||||||
# AWS_REGION=us-east-1
|
|
||||||
# AWS_S3_BUCKET=your-bucket-name
|
|
||||||
|
|
||||||
# ==============================================
|
|
||||||
# MONITORING
|
|
||||||
# ==============================================
|
|
||||||
# SENTRY_DSN=your-sentry-dsn
|
|
||||||
# HEALTH_CHECK_ENABLED=true
|
|
||||||
|
|
||||||
# ==============================================
|
|
||||||
# FRONTEND CONFIGURATION
|
|
||||||
# ==============================================
|
|
||||||
FRONTEND_PORT=3000
|
|
||||||
NEXT_PUBLIC_API_URL=/api
|
|
||||||
|
|
||||||
# ==============================================
|
|
||||||
# NGINX CONFIGURATION
|
|
||||||
# ==============================================
|
|
||||||
NGINX_HTTP_PORT=80
|
|
||||||
NGINX_HTTPS_PORT=443
|
|
||||||
79
.env.example
79
.env.example
@@ -1,79 +0,0 @@
|
|||||||
# ==============================================
|
|
||||||
# ENVIRONMENT VARIABLES TEMPLATE
|
|
||||||
# ==============================================
|
|
||||||
# Copy this file to .env and fill in your actual values
|
|
||||||
# NEVER commit real credentials to version control
|
|
||||||
|
|
||||||
# ==============================================
|
|
||||||
# DATABASE CONFIGURATION
|
|
||||||
# ==============================================
|
|
||||||
DATABASE_URL=postgresql://username:password@localhost:5432/database_name
|
|
||||||
POSTGRES_USER=your_db_user
|
|
||||||
POSTGRES_PASSWORD=your_secure_password
|
|
||||||
POSTGRES_DB=your_database_name
|
|
||||||
|
|
||||||
# ==============================================
|
|
||||||
# REDIS CONFIGURATION
|
|
||||||
# ==============================================
|
|
||||||
REDIS_URL=redis://localhost:6379
|
|
||||||
REDIS_HOST=localhost
|
|
||||||
REDIS_PORT=6379
|
|
||||||
REDIS_PASSWORD=your_redis_password_if_needed
|
|
||||||
|
|
||||||
# ==============================================
|
|
||||||
# BACKEND CONFIGURATION
|
|
||||||
# ==============================================
|
|
||||||
BACKEND_PORT=4000
|
|
||||||
NODE_ENV=development
|
|
||||||
JWT_SECRET=your-super-secret-jwt-key-minimum-32-characters
|
|
||||||
JWT_EXPIRES_IN=24h
|
|
||||||
API_PREFIX=/api/v1
|
|
||||||
|
|
||||||
# ==============================================
|
|
||||||
# FRONTEND CONFIGURATION
|
|
||||||
# ==============================================
|
|
||||||
FRONTEND_PORT=3000
|
|
||||||
NEXT_PUBLIC_API_URL=http://localhost:4000
|
|
||||||
NEXT_PUBLIC_APP_NAME=Next Nest Docker Template
|
|
||||||
|
|
||||||
# ==============================================
|
|
||||||
# NGINX CONFIGURATION
|
|
||||||
# ==============================================
|
|
||||||
NGINX_HTTP_PORT=80
|
|
||||||
NGINX_HTTPS_PORT=443
|
|
||||||
|
|
||||||
# ==============================================
|
|
||||||
# CORS CONFIGURATION
|
|
||||||
# ==============================================
|
|
||||||
CORS_ORIGIN=http://localhost:3000,http://localhost:80
|
|
||||||
CORS_CREDENTIALS=true
|
|
||||||
|
|
||||||
# ==============================================
|
|
||||||
# SECURITY SETTINGS
|
|
||||||
# ==============================================
|
|
||||||
RATE_LIMIT_WINDOW_MS=900000
|
|
||||||
RATE_LIMIT_MAX_REQUESTS=100
|
|
||||||
BCRYPT_SALT_ROUNDS=12
|
|
||||||
|
|
||||||
# ==============================================
|
|
||||||
# EMAIL CONFIGURATION (Optional)
|
|
||||||
# ==============================================
|
|
||||||
# SMTP_HOST=smtp.gmail.com
|
|
||||||
# SMTP_PORT=587
|
|
||||||
# SMTP_USER=your-email@gmail.com
|
|
||||||
# SMTP_PASS=your-app-password
|
|
||||||
|
|
||||||
# ==============================================
|
|
||||||
# EXTERNAL SERVICES (Optional)
|
|
||||||
# ==============================================
|
|
||||||
# AWS_ACCESS_KEY_ID=your-aws-access-key
|
|
||||||
# AWS_SECRET_ACCESS_KEY=your-aws-secret-key
|
|
||||||
# AWS_REGION=us-east-1
|
|
||||||
# AWS_S3_BUCKET=your-bucket-name
|
|
||||||
|
|
||||||
# ==============================================
|
|
||||||
# MONITORING & LOGGING (Optional)
|
|
||||||
# ==============================================
|
|
||||||
# LOG_LEVEL=info
|
|
||||||
# SENTRY_DSN=your-sentry-dsn
|
|
||||||
# MONITORING_ENABLED=true
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
# ==============================================
|
|
||||||
# PRODUCTION ENVIRONMENT VARIABLES
|
|
||||||
# ==============================================
|
|
||||||
# This file contains production environment variable templates
|
|
||||||
# DO NOT use default values in production!
|
|
||||||
|
|
||||||
# ==============================================
|
|
||||||
# DATABASE CONFIGURATION
|
|
||||||
# ==============================================
|
|
||||||
DATABASE_URL=postgresql://prod_user:STRONG_PASSWORD@postgres:5432/prod_db
|
|
||||||
POSTGRES_USER=prod_user
|
|
||||||
POSTGRES_PASSWORD=CHANGE_THIS_STRONG_PASSWORD
|
|
||||||
POSTGRES_DB=prod_db
|
|
||||||
|
|
||||||
# ==============================================
|
|
||||||
# REDIS CONFIGURATION
|
|
||||||
# ==============================================
|
|
||||||
REDIS_URL=redis://redis:6379
|
|
||||||
REDIS_HOST=redis
|
|
||||||
REDIS_PORT=6379
|
|
||||||
REDIS_PASSWORD=REDIS_STRONG_PASSWORD
|
|
||||||
|
|
||||||
# ==============================================
|
|
||||||
# BACKEND CONFIGURATION
|
|
||||||
# ==============================================
|
|
||||||
BACKEND_PORT=4000
|
|
||||||
NODE_ENV=production
|
|
||||||
JWT_SECRET=SUPER_SECURE_JWT_SECRET_AT_LEAST_32_CHARACTERS_LONG
|
|
||||||
JWT_EXPIRES_IN=1h
|
|
||||||
API_PREFIX=/api/v1
|
|
||||||
|
|
||||||
# ==============================================
|
|
||||||
# FRONTEND CONFIGURATION
|
|
||||||
# ==============================================
|
|
||||||
FRONTEND_PORT=3000
|
|
||||||
NEXT_PUBLIC_API_URL=https://your-domain.com
|
|
||||||
NEXT_PUBLIC_APP_NAME=Your App Name
|
|
||||||
|
|
||||||
# ==============================================
|
|
||||||
# NGINX CONFIGURATION
|
|
||||||
# ==============================================
|
|
||||||
NGINX_HTTP_PORT=80
|
|
||||||
NGINX_HTTPS_PORT=443
|
|
||||||
|
|
||||||
# ==============================================
|
|
||||||
# CORS CONFIGURATION
|
|
||||||
# ==============================================
|
|
||||||
CORS_ORIGIN=https://your-domain.com
|
|
||||||
CORS_CREDENTIALS=true
|
|
||||||
|
|
||||||
# ==============================================
|
|
||||||
# SECURITY SETTINGS
|
|
||||||
# ==============================================
|
|
||||||
RATE_LIMIT_WINDOW_MS=900000
|
|
||||||
RATE_LIMIT_MAX_REQUESTS=50
|
|
||||||
BCRYPT_SALT_ROUNDS=15
|
|
||||||
|
|
||||||
# ==============================================
|
|
||||||
# SSL CONFIGURATION
|
|
||||||
# ==============================================
|
|
||||||
SSL_CERT_PATH=/etc/nginx/ssl/cert.pem
|
|
||||||
SSL_KEY_PATH=/etc/nginx/ssl/key.pem
|
|
||||||
|
|
||||||
# ==============================================
|
|
||||||
# MONITORING & LOGGING
|
|
||||||
# ==============================================
|
|
||||||
LOG_LEVEL=warn
|
|
||||||
SENTRY_DSN=https://your-sentry-dsn@sentry.io/project-id
|
|
||||||
MONITORING_ENABLED=true
|
|
||||||
|
|
||||||
# ==============================================
|
|
||||||
# EXTERNAL SERVICES
|
|
||||||
# ==============================================
|
|
||||||
# AWS_ACCESS_KEY_ID=your-production-aws-key
|
|
||||||
# AWS_SECRET_ACCESS_KEY=your-production-aws-secret
|
|
||||||
# AWS_REGION=us-east-1
|
|
||||||
# AWS_S3_BUCKET=your-production-bucket
|
|
||||||
101
.gitignore
vendored
101
.gitignore
vendored
@@ -1,101 +0,0 @@
|
|||||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
|
||||||
|
|
||||||
# dependencies
|
|
||||||
/node_modules
|
|
||||||
/.pnp
|
|
||||||
.pnp.*
|
|
||||||
.yarn/*
|
|
||||||
!.yarn/patches
|
|
||||||
!.yarn/plugins
|
|
||||||
!.yarn/releases
|
|
||||||
!.yarn/versions
|
|
||||||
|
|
||||||
# testing
|
|
||||||
/coverage
|
|
||||||
|
|
||||||
# next.js
|
|
||||||
/.next/
|
|
||||||
/out/
|
|
||||||
|
|
||||||
# production
|
|
||||||
/build
|
|
||||||
/dist
|
|
||||||
|
|
||||||
# misc
|
|
||||||
.DS_Store
|
|
||||||
*.pem
|
|
||||||
|
|
||||||
# debug
|
|
||||||
npm-debug.log*
|
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
|
||||||
.pnpm-debug.log*
|
|
||||||
|
|
||||||
# env files (can opt-in for committing if needed)
|
|
||||||
# .env*
|
|
||||||
|
|
||||||
# vercel
|
|
||||||
.vercel
|
|
||||||
|
|
||||||
# typescript
|
|
||||||
*.tsbuildinfo
|
|
||||||
next-env.d.ts
|
|
||||||
|
|
||||||
# Docker
|
|
||||||
.dockerignore
|
|
||||||
|
|
||||||
# IDE and editors
|
|
||||||
/.idea
|
|
||||||
.project
|
|
||||||
.classpath
|
|
||||||
.c9/
|
|
||||||
*.launch
|
|
||||||
.settings/
|
|
||||||
*.sublime-workspace
|
|
||||||
.vscode/*
|
|
||||||
!.vscode/settings.json
|
|
||||||
!.vscode/tasks.json
|
|
||||||
!.vscode/launch.json
|
|
||||||
!.vscode/extensions.json
|
|
||||||
|
|
||||||
# Database
|
|
||||||
*.sqlite
|
|
||||||
*.sqlite3
|
|
||||||
*.db
|
|
||||||
|
|
||||||
# Logs
|
|
||||||
logs
|
|
||||||
*.log
|
|
||||||
|
|
||||||
# Runtime data
|
|
||||||
pids
|
|
||||||
*.pid
|
|
||||||
*.seed
|
|
||||||
*.pid.lock
|
|
||||||
|
|
||||||
# Coverage directory used by tools like istanbul
|
|
||||||
coverage/
|
|
||||||
*.lcov
|
|
||||||
|
|
||||||
# nyc test coverage
|
|
||||||
.nyc_output
|
|
||||||
|
|
||||||
# Temporary folders
|
|
||||||
tmp/
|
|
||||||
temp/
|
|
||||||
.tmp/
|
|
||||||
.temp/
|
|
||||||
|
|
||||||
# OS generated files
|
|
||||||
Thumbs.db
|
|
||||||
ehthumbs.db
|
|
||||||
|
|
||||||
# SSL certificates
|
|
||||||
*.key
|
|
||||||
*.crt
|
|
||||||
*.pem
|
|
||||||
ssl/
|
|
||||||
|
|
||||||
# Backup files
|
|
||||||
*.bak
|
|
||||||
*.backup
|
|
||||||
10
.idea/.gitignore
generated
vendored
Normal file
10
.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# 디폴트 무시된 파일
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# 쿼리 파일을 포함한 무시된 디폴트 폴더
|
||||||
|
/queries/
|
||||||
|
# Datasource local storage ignored files
|
||||||
|
/dataSources/
|
||||||
|
/dataSources.local.xml
|
||||||
|
# 에디터 기반 HTTP 클라이언트 요청
|
||||||
|
/httpRequests/
|
||||||
6
.idea/PMDPlugin.xml
generated
Normal file
6
.idea/PMDPlugin.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="PMDPlugin">
|
||||||
|
<option name="skipTestSources" value="false" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
8
.idea/genome2025.iml
generated
Normal file
8
.idea/genome2025.iml
generated
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="WEB_MODULE" version="4">
|
||||||
|
<component name="NewModuleRootManager">
|
||||||
|
<content url="file://$MODULE_DIR$" />
|
||||||
|
<orderEntry type="inheritedJdk" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
</module>
|
||||||
6
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
6
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<profile version="1.0">
|
||||||
|
<option name="myName" value="Project Default" />
|
||||||
|
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
</profile>
|
||||||
|
</component>
|
||||||
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/genome2025.iml" filepath="$PROJECT_DIR$/.idea/genome2025.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
6
.idea/prettier.xml
generated
Normal file
6
.idea/prettier.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="PrettierConfiguration">
|
||||||
|
<option name="myConfigurationMode" value="AUTOMATIC" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
11
.project
Normal file
11
.project
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<projectDescription>
|
||||||
|
<name>genome2025</name>
|
||||||
|
<comment></comment>
|
||||||
|
<projects>
|
||||||
|
</projects>
|
||||||
|
<buildSpec>
|
||||||
|
</buildSpec>
|
||||||
|
<natures>
|
||||||
|
</natures>
|
||||||
|
</projectDescription>
|
||||||
290
README.md
290
README.md
@@ -1,290 +0,0 @@
|
|||||||
# Next.js + NestJS + Docker Template
|
|
||||||
|
|
||||||
A full-stack TypeScript template with Next.js frontend, NestJS backend, PostgreSQL, Redis, and Nginx, all containerized with Docker.
|
|
||||||
|
|
||||||
## 🚀 Quick Start
|
|
||||||
## 개발 환경 구축
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Clone the repository
|
|
||||||
git clone <repository-url>
|
|
||||||
cd next_nest_docker_template
|
|
||||||
|
|
||||||
# Copy environment variables
|
|
||||||
cp .env.example .env
|
|
||||||
|
|
||||||
# Start development environment
|
|
||||||
docker compose up -d
|
|
||||||
|
|
||||||
# View logs
|
|
||||||
docker compose logs -f
|
|
||||||
```
|
|
||||||
|
|
||||||
Your application will be available at:
|
|
||||||
- **Frontend**: http://localhost:3000
|
|
||||||
- **Backend API**: http://localhost:4000
|
|
||||||
- **Nginx Proxy**: http://localhost:80
|
|
||||||
|
|
||||||
## 📁 Project Structure
|
|
||||||
|
|
||||||
```
|
|
||||||
├── frontend/ # Next.js application
|
|
||||||
│ ├── src/ # Source code
|
|
||||||
│ ├── Dockerfile # Development Dockerfile
|
|
||||||
│ └── .env.local.example # Frontend environment variables
|
|
||||||
├── backend/ # NestJS application
|
|
||||||
│ ├── src/ # Source code
|
|
||||||
│ ├── Dockerfile # Development Dockerfile
|
|
||||||
│ └── .env.example # Backend environment variables
|
|
||||||
├── nginx/ # Nginx configuration
|
|
||||||
│ ├── nginx.conf # Proxy configuration
|
|
||||||
│ └── ssl/ # SSL certificates directory
|
|
||||||
├── .env # Main environment variables
|
|
||||||
├── .env.example # Environment template
|
|
||||||
├── .env.production # Production environment template
|
|
||||||
├── docker-compose.yml # Development containers
|
|
||||||
└── docker-compose.prod.yml # Production containers
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🛠️ Technology Stack
|
|
||||||
|
|
||||||
### Frontend
|
|
||||||
- **Next.js 15.5.3** - React framework with Turbopack
|
|
||||||
- **React 19.1.0** - UI library
|
|
||||||
- **TypeScript** - Type safety
|
|
||||||
- **Tailwind CSS** - Styling
|
|
||||||
|
|
||||||
### Backend
|
|
||||||
- **NestJS 11** - Node.js framework
|
|
||||||
- **TypeScript** - Type safety
|
|
||||||
- **Express** - HTTP server
|
|
||||||
|
|
||||||
### Database & Cache
|
|
||||||
- **PostgreSQL 15** - Primary database
|
|
||||||
- **Redis 7** - Caching and sessions
|
|
||||||
|
|
||||||
### Infrastructure
|
|
||||||
- **Docker & Docker Compose** - Containerization
|
|
||||||
- **Nginx** - Reverse proxy and load balancer
|
|
||||||
|
|
||||||
## 🔧 Environment Configuration
|
|
||||||
|
|
||||||
### Development Setup
|
|
||||||
|
|
||||||
1. Copy the environment template:
|
|
||||||
```bash
|
|
||||||
cp .env.example .env
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Configure your variables in `.env`:
|
|
||||||
```env
|
|
||||||
# Database
|
|
||||||
POSTGRES_USER=user
|
|
||||||
POSTGRES_PASSWORD=password
|
|
||||||
POSTGRES_DB=mydb
|
|
||||||
|
|
||||||
# Security
|
|
||||||
JWT_SECRET=your-super-secret-jwt-key
|
|
||||||
CORS_ORIGIN=http://localhost:3000
|
|
||||||
|
|
||||||
# Ports
|
|
||||||
FRONTEND_PORT=3000
|
|
||||||
BACKEND_PORT=4000
|
|
||||||
```
|
|
||||||
|
|
||||||
### Production Setup
|
|
||||||
|
|
||||||
1. Copy the production template:
|
|
||||||
```bash
|
|
||||||
cp .env.production .env
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Update with your production values:
|
|
||||||
```env
|
|
||||||
# Use strong passwords in production
|
|
||||||
POSTGRES_PASSWORD=STRONG_PRODUCTION_PASSWORD
|
|
||||||
JWT_SECRET=SUPER_SECURE_JWT_SECRET_AT_LEAST_32_CHARACTERS_LONG
|
|
||||||
|
|
||||||
# Use your domain
|
|
||||||
NEXT_PUBLIC_API_URL=https://your-domain.com
|
|
||||||
CORS_ORIGIN=https://your-domain.com
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🐳 Docker Commands
|
|
||||||
|
|
||||||
### Development
|
|
||||||
```bash
|
|
||||||
# Start all services
|
|
||||||
docker compose up -d
|
|
||||||
|
|
||||||
# Rebuild and start
|
|
||||||
docker compose up -d --build
|
|
||||||
|
|
||||||
# View logs
|
|
||||||
docker compose logs -f [service-name]
|
|
||||||
|
|
||||||
# Stop services
|
|
||||||
docker compose down
|
|
||||||
|
|
||||||
# Remove volumes (⚠️ deletes data)
|
|
||||||
docker compose down -v
|
|
||||||
```
|
|
||||||
|
|
||||||
### Production
|
|
||||||
```bash
|
|
||||||
# Start production environment
|
|
||||||
docker compose -f docker-compose.prod.yml up -d
|
|
||||||
|
|
||||||
# Build and deploy
|
|
||||||
docker compose -f docker-compose.prod.yml up -d --build
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🔍 Service Details
|
|
||||||
|
|
||||||
### Frontend (Next.js)
|
|
||||||
- **Port**: 3000 (configurable via `FRONTEND_PORT`)
|
|
||||||
- **Development**: Hot reload enabled
|
|
||||||
- **Build**: Optimized production build with Turbopack
|
|
||||||
|
|
||||||
### Backend (NestJS)
|
|
||||||
- **Port**: 4000 (configurable via `BACKEND_PORT`)
|
|
||||||
- **Development**: Watch mode enabled
|
|
||||||
- **Features**: JWT auth, CORS, rate limiting
|
|
||||||
|
|
||||||
### Database (PostgreSQL)
|
|
||||||
- **Port**: 5432 (configurable via `POSTGRES_PORT`)
|
|
||||||
- **Volume**: `postgres_data` for persistence
|
|
||||||
- **Health Check**: Built-in readiness check
|
|
||||||
|
|
||||||
### Cache (Redis)
|
|
||||||
- **Port**: 6379 (configurable via `REDIS_PORT`)
|
|
||||||
- **Volume**: `redis_data` for persistence
|
|
||||||
- **Health Check**: Ping command
|
|
||||||
|
|
||||||
### Proxy (Nginx)
|
|
||||||
- **Ports**: 80 (HTTP), 443 (HTTPS)
|
|
||||||
- **Features**: Load balancing, SSL termination
|
|
||||||
- **Configuration**: `nginx/nginx.conf`
|
|
||||||
|
|
||||||
## 🔐 Security Features
|
|
||||||
|
|
||||||
- **JWT Authentication** - Secure API access
|
|
||||||
- **CORS Configuration** - Cross-origin request control
|
|
||||||
- **Rate Limiting** - API abuse prevention
|
|
||||||
- **Environment Variables** - Secure configuration management
|
|
||||||
- **SSL Support** - HTTPS encryption ready
|
|
||||||
|
|
||||||
## 🚦 Health Checks
|
|
||||||
|
|
||||||
All services include health checks:
|
|
||||||
- **Frontend**: HTTP GET to `/`
|
|
||||||
- **Backend**: HTTP GET to `/`
|
|
||||||
- **PostgreSQL**: `pg_isready` command
|
|
||||||
- **Redis**: `redis-cli ping` command
|
|
||||||
|
|
||||||
## 📝 Development Workflow
|
|
||||||
|
|
||||||
1. **Setup Environment**:
|
|
||||||
```bash
|
|
||||||
cp .env.example .env
|
|
||||||
# Edit .env with your settings
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Start Development**:
|
|
||||||
```bash
|
|
||||||
docker compose up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Develop**:
|
|
||||||
- Frontend: Edit files in `frontend/src/`
|
|
||||||
- Backend: Edit files in `backend/src/`
|
|
||||||
- Changes are automatically reflected due to volume mounts
|
|
||||||
|
|
||||||
4. **View Logs**:
|
|
||||||
```bash
|
|
||||||
docker compose logs -f frontend
|
|
||||||
docker compose logs -f backend
|
|
||||||
```
|
|
||||||
|
|
||||||
5. **Database Access**:
|
|
||||||
```bash
|
|
||||||
docker exec -it postgres-db psql -U user -d mydb
|
|
||||||
```
|
|
||||||
|
|
||||||
6. **Redis Access**:
|
|
||||||
```bash
|
|
||||||
docker exec -it redis-cache redis-cli
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🔧 Customization
|
|
||||||
|
|
||||||
### Adding New Services
|
|
||||||
1. Add service to `docker-compose.yml`
|
|
||||||
2. Update environment variables
|
|
||||||
3. Configure networking and dependencies
|
|
||||||
|
|
||||||
### SSL Configuration
|
|
||||||
1. Place certificates in `nginx/ssl/`
|
|
||||||
2. Update `nginx.conf` for HTTPS
|
|
||||||
3. Update environment variables for HTTPS URLs
|
|
||||||
|
|
||||||
### Environment Variables
|
|
||||||
All configurable values use environment variables with sensible defaults:
|
|
||||||
- See `.env.example` for full list
|
|
||||||
- Override any value in your `.env` file
|
|
||||||
- Production values in `.env.production`
|
|
||||||
|
|
||||||
## 🐛 Troubleshooting
|
|
||||||
|
|
||||||
### Common Issues
|
|
||||||
|
|
||||||
**Port Conflicts**:
|
|
||||||
```bash
|
|
||||||
# Change ports in .env
|
|
||||||
FRONTEND_PORT=3001
|
|
||||||
BACKEND_PORT=4001
|
|
||||||
```
|
|
||||||
|
|
||||||
**Permission Issues**:
|
|
||||||
```bash
|
|
||||||
# Fix file permissions
|
|
||||||
sudo chown -R $USER:$USER .
|
|
||||||
```
|
|
||||||
|
|
||||||
**Database Connection**:
|
|
||||||
```bash
|
|
||||||
# Check database logs
|
|
||||||
docker compose logs postgres
|
|
||||||
```
|
|
||||||
|
|
||||||
**Container Not Starting**:
|
|
||||||
```bash
|
|
||||||
# Check specific service logs
|
|
||||||
docker compose logs [service-name]
|
|
||||||
```
|
|
||||||
|
|
||||||
### Reset Everything
|
|
||||||
```bash
|
|
||||||
# Stop and remove everything
|
|
||||||
docker compose down -v
|
|
||||||
docker system prune -f
|
|
||||||
|
|
||||||
# Start fresh
|
|
||||||
docker compose up -d --build
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📄 License
|
|
||||||
|
|
||||||
This project is licensed under the MIT License.
|
|
||||||
|
|
||||||
## 🤝 Contributing
|
|
||||||
|
|
||||||
1. Fork the repository
|
|
||||||
2. Create a feature branch
|
|
||||||
3. Commit your changes
|
|
||||||
4. Push to the branch
|
|
||||||
5. Create a Pull Request
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Happy coding!** 🎉
|
|
||||||
49
backend/.env
Normal file
49
backend/.env
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
# ==============================================
|
||||||
|
# 로컬 개발용 (npm run start:dev)
|
||||||
|
# ==============================================
|
||||||
|
|
||||||
|
# DATABASE
|
||||||
|
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
|
||||||
|
BACKEND_PORT=4000
|
||||||
|
NODE_ENV=development
|
||||||
|
|
||||||
|
# JWT
|
||||||
|
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
|
||||||
|
CORS_ORIGIN=http://localhost:3000,http://192.168.11.46:3000
|
||||||
|
CORS_CREDENTIALS=true
|
||||||
|
|
||||||
|
# SECURITY
|
||||||
|
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 (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
|
||||||
49
backend/.env.dev
Normal file
49
backend/.env.dev
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
# ==============================================
|
||||||
|
# Docker 개발 환경용 (docker-compose)
|
||||||
|
# ==============================================
|
||||||
|
|
||||||
|
# DATABASE - Docker에서 호스트 DB 접근
|
||||||
|
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
|
||||||
|
BACKEND_PORT=4000
|
||||||
|
NODE_ENV=development
|
||||||
|
|
||||||
|
# JWT
|
||||||
|
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
|
||||||
|
CORS_ORIGIN=http://localhost:3000,http://192.168.11.46:3000,https://genome2025.turbosoft.kr
|
||||||
|
CORS_CREDENTIALS=true
|
||||||
|
|
||||||
|
# SECURITY
|
||||||
|
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 (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
|
||||||
49
backend/.env.prod
Normal file
49
backend/.env.prod
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
# ==============================================
|
||||||
|
# 프로덕션 환경용 (배포)
|
||||||
|
# ==============================================
|
||||||
|
|
||||||
|
# DATABASE
|
||||||
|
POSTGRES_HOST=192.168.11.46
|
||||||
|
POSTGRES_USER=genome
|
||||||
|
POSTGRES_PASSWORD=genome1@3
|
||||||
|
POSTGRES_DB=genome_db
|
||||||
|
POSTGRES_PORT=5431
|
||||||
|
POSTGRES_SYNCHRONIZE=false
|
||||||
|
POSTGRES_LOGGING=false
|
||||||
|
|
||||||
|
# BACKEND
|
||||||
|
BACKEND_PORT=4000
|
||||||
|
NODE_ENV=production
|
||||||
|
|
||||||
|
# JWT
|
||||||
|
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
|
||||||
|
CORS_ORIGIN=https://genome2025.turbosoft.kr
|
||||||
|
CORS_CREDENTIALS=true
|
||||||
|
|
||||||
|
# SECURITY
|
||||||
|
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 (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=warn
|
||||||
|
LOG_FORMAT=combined
|
||||||
|
LOG_FILE_ENABLED=true
|
||||||
|
LOG_FILE_PATH=./logs
|
||||||
58
backend/.gitignore
vendored
58
backend/.gitignore
vendored
@@ -1,9 +1,21 @@
|
|||||||
# compiled output
|
# ==============================================
|
||||||
/dist
|
# Dependencies
|
||||||
|
# ==============================================
|
||||||
/node_modules
|
/node_modules
|
||||||
|
|
||||||
|
# ==============================================
|
||||||
|
# Build Output
|
||||||
|
# ==============================================
|
||||||
|
/dist
|
||||||
/build
|
/build
|
||||||
|
|
||||||
|
# ==============================================
|
||||||
|
# Environment Variables (민감정보)
|
||||||
|
# ==============================================
|
||||||
|
|
||||||
|
# ==============================================
|
||||||
# Logs
|
# Logs
|
||||||
|
# ==============================================
|
||||||
logs
|
logs
|
||||||
*.log
|
*.log
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
@@ -12,14 +24,15 @@ yarn-debug.log*
|
|||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
lerna-debug.log*
|
lerna-debug.log*
|
||||||
|
|
||||||
# OS
|
# ==============================================
|
||||||
.DS_Store
|
# Testing
|
||||||
|
# ==============================================
|
||||||
# Tests
|
|
||||||
/coverage
|
/coverage
|
||||||
/.nyc_output
|
/.nyc_output
|
||||||
|
|
||||||
# IDEs and editors
|
# ==============================================
|
||||||
|
# IDE & Editors
|
||||||
|
# ==============================================
|
||||||
/.idea
|
/.idea
|
||||||
.project
|
.project
|
||||||
.classpath
|
.classpath
|
||||||
@@ -27,30 +40,35 @@ lerna-debug.log*
|
|||||||
*.launch
|
*.launch
|
||||||
.settings/
|
.settings/
|
||||||
*.sublime-workspace
|
*.sublime-workspace
|
||||||
|
|
||||||
# IDE - VSCode
|
|
||||||
.vscode/*
|
.vscode/*
|
||||||
!.vscode/settings.json
|
!.vscode/settings.json
|
||||||
!.vscode/tasks.json
|
!.vscode/tasks.json
|
||||||
!.vscode/launch.json
|
!.vscode/launch.json
|
||||||
!.vscode/extensions.json
|
!.vscode/extensions.json
|
||||||
|
|
||||||
# dotenv environment variable files
|
# ==============================================
|
||||||
.env
|
# OS Files
|
||||||
.env.development.local
|
# ==============================================
|
||||||
.env.test.local
|
.DS_Store
|
||||||
.env.production.local
|
Thumbs.db
|
||||||
.env.local
|
|
||||||
|
|
||||||
# temp directory
|
# ==============================================
|
||||||
|
# Temp & Runtime
|
||||||
|
# ==============================================
|
||||||
.temp
|
.temp
|
||||||
.tmp
|
.tmp
|
||||||
|
|
||||||
# Runtime data
|
|
||||||
pids
|
pids
|
||||||
*.pid
|
*.pid
|
||||||
*.seed
|
*.seed
|
||||||
*.pid.lock
|
*.pid.lock
|
||||||
|
|
||||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
# ==============================================
|
||||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
# Diagnostic Reports
|
||||||
|
# ==============================================
|
||||||
|
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||||
|
|
||||||
|
# ==============================================
|
||||||
|
# Uploads (로컬 테스트용)
|
||||||
|
# ==============================================
|
||||||
|
/uploads/*
|
||||||
|
!/uploads/.gitkeep
|
||||||
|
|||||||
@@ -5,20 +5,27 @@ WORKDIR /app
|
|||||||
# 필요한 패키지 설치
|
# 필요한 패키지 설치
|
||||||
RUN apk add --no-cache curl
|
RUN apk add --no-cache curl
|
||||||
|
|
||||||
|
# 환경변수 설정
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
ENV NODE_OPTIONS="--enable-source-maps"
|
||||||
|
|
||||||
# package.json 복사
|
# package.json 복사
|
||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
|
|
||||||
# 의존성 설치
|
# 의존성 설치 (devDependencies 포함 - nest CLI 필요)
|
||||||
RUN npm install
|
RUN npm install --include=dev
|
||||||
|
|
||||||
# 소스 코드 복사
|
# 소스 코드 복사
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
# NestJS 빌드
|
# NestJS 빌드
|
||||||
RUN npm run build
|
RUN npx nest build
|
||||||
|
|
||||||
|
# devDependencies 제거
|
||||||
|
RUN npm prune --production
|
||||||
|
|
||||||
# 포트 노출
|
# 포트 노출
|
||||||
EXPOSE 4000
|
EXPOSE 4000
|
||||||
|
|
||||||
# 애플리케이션 실행
|
# 애플리케이션 실행
|
||||||
CMD ["npm", "run", "start:prod"]
|
CMD ["node", "dist/main.js"]
|
||||||
|
|||||||
3613
backend/package-lock.json
generated
3613
backend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -20,7 +20,7 @@
|
|||||||
"test:e2e": "jest --config ./test/jest-e2e.json"
|
"test:e2e": "jest --config ./test/jest-e2e.json"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nestjs-modules/ioredis": "^2.0.2",
|
|
||||||
"@nestjs/common": "^11.0.1",
|
"@nestjs/common": "^11.0.1",
|
||||||
"@nestjs/config": "^4.0.2",
|
"@nestjs/config": "^4.0.2",
|
||||||
"@nestjs/core": "^11.0.1",
|
"@nestjs/core": "^11.0.1",
|
||||||
@@ -37,7 +37,7 @@
|
|||||||
"bcrypt": "^6.0.0",
|
"bcrypt": "^6.0.0",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.14.2",
|
"class-validator": "^0.14.2",
|
||||||
"ioredis": "^5.8.1",
|
|
||||||
"multer": "^2.0.2",
|
"multer": "^2.0.2",
|
||||||
"nodemailer": "^7.0.9",
|
"nodemailer": "^7.0.9",
|
||||||
"passport": "^0.7.0",
|
"passport": "^0.7.0",
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { AppController } from './app.controller';
|
|||||||
import { AppService } from './app.service';
|
import { AppService } from './app.service';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||||
import { RedisModule } from './redis/redis.module';
|
|
||||||
import { AuthModule } from './auth/auth.module';
|
import { AuthModule } from './auth/auth.module';
|
||||||
import { UserModule } from './user/user.module';
|
import { UserModule } from './user/user.module';
|
||||||
import { CommonModule } from './common/common.module';
|
import { CommonModule } from './common/common.module';
|
||||||
@@ -19,50 +18,51 @@ import { GenomeModule } from './genome/genome.module';
|
|||||||
import { MptModule } from './mpt/mpt.module';
|
import { MptModule } from './mpt/mpt.module';
|
||||||
import { DashboardModule } from './dashboard/dashboard.module';
|
import { DashboardModule } from './dashboard/dashboard.module';
|
||||||
import { GeneModule } from './gene/gene.module';
|
import { GeneModule } from './gene/gene.module';
|
||||||
|
import { SystemModule } from './system/system.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
ConfigModule.forRoot({
|
ConfigModule.forRoot({
|
||||||
isGlobal: true,
|
isGlobal: true,
|
||||||
}),
|
}),
|
||||||
TypeOrmModule.forRootAsync({
|
TypeOrmModule.forRootAsync({
|
||||||
imports: [ConfigModule],
|
imports: [ConfigModule],
|
||||||
inject: [ConfigService],
|
inject: [ConfigService],
|
||||||
useFactory: (configService: ConfigService) => ({
|
useFactory: (configService: ConfigService) => ({
|
||||||
type: 'postgres',
|
type: 'postgres',
|
||||||
host: configService.get('POSTGRES_HOST'),
|
host: configService.get('POSTGRES_HOST'),
|
||||||
port: configService.get('POSTGRES_PORT'),
|
port: configService.get('POSTGRES_PORT'),
|
||||||
username: configService.get('POSTGRES_USER'),
|
username: configService.get('POSTGRES_USER'),
|
||||||
password: configService.get('POSTGRES_PASSWORD'),
|
password: configService.get('POSTGRES_PASSWORD'),
|
||||||
database: configService.get('POSTGRES_DB'),
|
database: configService.get('POSTGRES_DB'),
|
||||||
synchronize: configService.get('POSTGRES_SYNCHRONIZE'),
|
synchronize: configService.get('POSTGRES_SYNCHRONIZE'),
|
||||||
logging: configService.get('POSTGRES_LOGGING'),
|
logging: configService.get('POSTGRES_LOGGING'),
|
||||||
autoLoadEntities: true,
|
autoLoadEntities: true,
|
||||||
entities: [],
|
entities: [],
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
// 인프라 모듈
|
// 인프라 모듈
|
||||||
RedisModule,
|
JwtModule,
|
||||||
JwtModule,
|
CommonModule,
|
||||||
CommonModule,
|
SharedModule,
|
||||||
SharedModule,
|
|
||||||
|
// 인증/사용자 모듈
|
||||||
// 인증/사용자 모듈
|
AuthModule,
|
||||||
AuthModule,
|
UserModule,
|
||||||
UserModule,
|
|
||||||
|
// 비즈니스 모듈
|
||||||
// 비즈니스 모듈
|
FarmModule,
|
||||||
FarmModule,
|
CowModule,
|
||||||
CowModule,
|
GenomeModule,
|
||||||
GenomeModule,
|
GeneModule,
|
||||||
GeneModule,
|
MptModule,
|
||||||
MptModule,
|
DashboardModule,
|
||||||
DashboardModule,
|
|
||||||
|
// 기타
|
||||||
// 기타
|
HelpModule,
|
||||||
HelpModule,
|
SystemModule,
|
||||||
],
|
],
|
||||||
controllers: [AppController],
|
controllers: [AppController],
|
||||||
providers: [AppService, JwtStrategy],
|
providers: [AppService, JwtStrategy],
|
||||||
})
|
})
|
||||||
export class AppModule {}
|
export class AppModule {}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {
|
|||||||
Injectable,
|
Injectable,
|
||||||
NotFoundException,
|
NotFoundException,
|
||||||
UnauthorizedException,
|
UnauthorizedException,
|
||||||
|
Logger,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { UserModel } from '../user/entities/user.entity';
|
import { UserModel } from '../user/entities/user.entity';
|
||||||
@@ -30,6 +31,8 @@ import { ConfigService } from '@nestjs/config';
|
|||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AuthService {
|
export class AuthService {
|
||||||
|
private readonly logger = new Logger(AuthService.name);
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@InjectRepository(UserModel)
|
@InjectRepository(UserModel)
|
||||||
private readonly userRepository: Repository<UserModel>,
|
private readonly userRepository: Repository<UserModel>,
|
||||||
@@ -41,57 +44,49 @@ export class AuthService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 유저 로그인
|
* 유저 로그인
|
||||||
*
|
|
||||||
* @async
|
|
||||||
* @param {LoginDto} loginDto
|
|
||||||
* @returns {Promise<LoginResponseDto>}
|
|
||||||
*/
|
*/
|
||||||
async login(loginDto: LoginDto): Promise<LoginResponseDto> {
|
async login(loginDto: LoginDto): Promise<LoginResponseDto> {
|
||||||
const { userId, userPassword } = loginDto;
|
const { userId, userPassword } = loginDto;
|
||||||
|
this.logger.log(`[LOGIN] 로그인 시도 - userId: ${userId}`);
|
||||||
|
|
||||||
// 1. userId로 유저 찾기
|
|
||||||
const user = await this.userRepository.findOne({
|
const user = await this.userRepository.findOne({
|
||||||
where: { userId },
|
where: { userId },
|
||||||
});
|
});
|
||||||
// 2. user 없으면 에러
|
|
||||||
if (!user) {
|
|
||||||
throw new UnauthorizedException('아이디 또는 비밀번호가 틀렸습니다'); //HTTP 401 상태 코드 예외
|
|
||||||
}
|
|
||||||
// 3. 비밀번호 비교 (bcrypt)
|
|
||||||
const tempHash = await bcrypt.hash(userPassword, 10);
|
|
||||||
console.log('=========input password bcrypt hash========:', tempHash);
|
|
||||||
|
|
||||||
const isPasswordValid = await bcrypt.compare(userPassword, user.userPw);
|
if (!user) {
|
||||||
if (!isPasswordValid) {
|
this.logger.warn(`[LOGIN] 사용자 없음 - userId: ${userId}`);
|
||||||
|
throw new UnauthorizedException('아이디 또는 비밀번호가 틀렸습니다');
|
||||||
|
}
|
||||||
|
|
||||||
|
const isPasswordValid = await bcrypt.compare(userPassword, user.userPw);
|
||||||
|
if (!isPasswordValid) {
|
||||||
|
this.logger.warn(`[LOGIN] 비밀번호 불일치 - userId: ${userId}`);
|
||||||
throw new UnauthorizedException('아이디 또는 비밀번호가 틀렸습니다');
|
throw new UnauthorizedException('아이디 또는 비밀번호가 틀렸습니다');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. 탈퇴 여부 확인
|
|
||||||
if (user.delDt !== null) {
|
if (user.delDt !== null) {
|
||||||
|
this.logger.warn(`[LOGIN] 탈퇴 계정 - userId: ${userId}`);
|
||||||
throw new UnauthorizedException('탈퇴한 계정입니다');
|
throw new UnauthorizedException('탈퇴한 계정입니다');
|
||||||
}
|
}
|
||||||
// 6. JWT 토큰 생성
|
|
||||||
const payload = {
|
const payload = {
|
||||||
userId: user.userId,
|
userId: user.userId,
|
||||||
userNo: user.pkUserNo,
|
userNo: user.pkUserNo,
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Access Token 생성 (기본 설정 사용)
|
|
||||||
const accessToken = this.jwtService.sign(payload as any);
|
const accessToken = this.jwtService.sign(payload as any);
|
||||||
|
|
||||||
// Refresh Token 생성 (별도 secret과 만료시간 사용)
|
|
||||||
const refreshOptions = {
|
const refreshOptions = {
|
||||||
secret: this.configService.get<string>('JWT_REFRESH_SECRET')!,
|
secret: this.configService.get<string>('JWT_REFRESH_SECRET')!,
|
||||||
expiresIn: this.configService.get<string>('JWT_REFRESH_EXPIRES_IN') || '7d',
|
expiresIn: this.configService.get<string>('JWT_REFRESH_EXPIRES_IN') || '7d',
|
||||||
};
|
};
|
||||||
const refreshToken = this.jwtService.sign(payload as any, refreshOptions as any);
|
const refreshToken = this.jwtService.sign(payload as any, refreshOptions as any);
|
||||||
|
|
||||||
// 7. 로그인 응답 생성 (LoginResponseDto)
|
this.logger.log(`[LOGIN] 로그인 성공 - userId: ${userId}`);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
message: '로그인 성공',
|
message: '로그인 성공',
|
||||||
accessToken, // JWT 토큰 추가
|
accessToken,
|
||||||
refreshToken, // JWT 토큰 추가
|
refreshToken,
|
||||||
user: {
|
user: {
|
||||||
pkUserNo: user.pkUserNo,
|
pkUserNo: user.pkUserNo,
|
||||||
userId: user.userId,
|
userId: user.userId,
|
||||||
@@ -104,32 +99,27 @@ export class AuthService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 회원가입
|
* 회원가입
|
||||||
*
|
|
||||||
* @async
|
|
||||||
* @param {SignupDto} signupDto
|
|
||||||
* @returns {Promise<SignupResponseDto>}
|
|
||||||
*/
|
*/
|
||||||
async register(signupDto: SignupDto, clientIp: string): Promise<SignupResponseDto> {
|
async register(signupDto: SignupDto, clientIp: string): Promise<SignupResponseDto> {
|
||||||
const { userId, userEmail, userPhone } = signupDto;
|
const { userId, userEmail, userPhone } = signupDto;
|
||||||
|
this.logger.log(`[REGISTER] 회원가입 시도 - userId: ${userId}, email: ${userEmail}`);
|
||||||
|
|
||||||
// 0. 이메일 인증 확인 (Redis에 인증 완료 여부 체크)
|
|
||||||
const verifiedKey = `signup-verified:${userEmail}`;
|
const verifiedKey = `signup-verified:${userEmail}`;
|
||||||
const isEmailVerified = await this.verificationService.verifyCode(verifiedKey, 'true');
|
const isEmailVerified = await this.verificationService.verifyCode(verifiedKey, 'true');
|
||||||
|
|
||||||
if (!isEmailVerified) {
|
if (!isEmailVerified) {
|
||||||
|
this.logger.warn(`[REGISTER] 이메일 미인증 - email: ${userEmail}`);
|
||||||
throw new UnauthorizedException('이메일 인증이 완료되지 않았습니다');
|
throw new UnauthorizedException('이메일 인증이 완료되지 않았습니다');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1. 중복 체크 (ID, 이메일, 전화번호, 사업자번호)
|
|
||||||
const whereConditions = [{ userId }, { userEmail }, { userPhone }];
|
const whereConditions = [{ userId }, { userEmail }, { userPhone }];
|
||||||
|
|
||||||
const existingUser = await this.userRepository.findOne({
|
const existingUser = await this.userRepository.findOne({
|
||||||
where: whereConditions,
|
where: whereConditions,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (existingUser) {
|
if (existingUser) {
|
||||||
if (existingUser.userId === userId) {
|
if (existingUser.userId === userId) {
|
||||||
throw new ConflictException('이미 사용 중인 아이디입니다'); //HTTP 409 상태 코드 예외
|
throw new ConflictException('이미 사용 중인 아이디입니다');
|
||||||
}
|
}
|
||||||
if (existingUser.userEmail === userEmail) {
|
if (existingUser.userEmail === userEmail) {
|
||||||
throw new ConflictException('이미 사용 중인 이메일입니다');
|
throw new ConflictException('이미 사용 중인 이메일입니다');
|
||||||
@@ -137,28 +127,24 @@ export class AuthService {
|
|||||||
if (existingUser.userPhone === userPhone) {
|
if (existingUser.userPhone === userPhone) {
|
||||||
throw new ConflictException('이미 사용 중인 전화번호입니다');
|
throw new ConflictException('이미 사용 중인 전화번호입니다');
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 비밀번호 해싱 (bcrypt)
|
|
||||||
const saltRounds = parseInt(this.configService.get<string>('BCRYPT_SALT_ROUNDS') || '10', 10);
|
const saltRounds = parseInt(this.configService.get<string>('BCRYPT_SALT_ROUNDS') || '10', 10);
|
||||||
const hashedPassword = await bcrypt.hash(signupDto.userPassword, saltRounds);
|
const hashedPassword = await bcrypt.hash(signupDto.userPassword, saltRounds);
|
||||||
// 3. 사용자 생성
|
|
||||||
const newUser = this.userRepository.create({
|
const newUser = this.userRepository.create({
|
||||||
userId: signupDto.userId,
|
userId: signupDto.userId,
|
||||||
userPw: hashedPassword,
|
userPw: hashedPassword,
|
||||||
userName: signupDto.userName,
|
userName: signupDto.userName,
|
||||||
userPhone: signupDto.userPhone,
|
userPhone: signupDto.userPhone,
|
||||||
userEmail: signupDto.userEmail,
|
userEmail: signupDto.userEmail,
|
||||||
|
regIp: clientIp,
|
||||||
regIp: clientIp, // 등록 ip
|
|
||||||
regUserId: signupDto.userId,
|
regUserId: signupDto.userId,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 4. DB에 저장
|
|
||||||
const savedUser = await this.userRepository.save(newUser);
|
const savedUser = await this.userRepository.save(newUser);
|
||||||
|
this.logger.log(`[REGISTER] 회원가입 성공 - userId: ${savedUser.userId}`);
|
||||||
|
|
||||||
// 5. 응답 구조 생성 (SignupResponseDto 반환)
|
|
||||||
return {
|
return {
|
||||||
message: '회원가입이 완료되었습니다',
|
message: '회원가입이 완료되었습니다',
|
||||||
redirectUrl: '/dashboard',
|
redirectUrl: '/dashboard',
|
||||||
@@ -169,10 +155,6 @@ export class AuthService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 이메일 중복 체크
|
* 이메일 중복 체크
|
||||||
*
|
|
||||||
* @async
|
|
||||||
* @param {string} userEmail
|
|
||||||
* @returns {Promise<{ available: boolean; message: string }>}
|
|
||||||
*/
|
*/
|
||||||
async checkEmailDuplicate(userEmail: string): Promise<{
|
async checkEmailDuplicate(userEmail: string): Promise<{
|
||||||
available: boolean;
|
available: boolean;
|
||||||
@@ -197,10 +179,6 @@ export class AuthService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 회원가입 - 이메일 인증번호 발송
|
* 회원가입 - 이메일 인증번호 발송
|
||||||
*
|
|
||||||
* @async
|
|
||||||
* @param {SendSignupCodeDto} dto
|
|
||||||
* @returns {Promise<{ success: boolean; message: string; expiresIn: number }>}
|
|
||||||
*/
|
*/
|
||||||
async sendSignupCode(dto: SendSignupCodeDto): Promise<{
|
async sendSignupCode(dto: SendSignupCodeDto): Promise<{
|
||||||
success: boolean;
|
success: boolean;
|
||||||
@@ -208,42 +186,60 @@ export class AuthService {
|
|||||||
expiresIn: number;
|
expiresIn: number;
|
||||||
}> {
|
}> {
|
||||||
const { userEmail } = dto;
|
const { userEmail } = dto;
|
||||||
|
this.logger.log(`[SEND-CODE] ========== 인증번호 발송 시작 ==========`);
|
||||||
|
this.logger.log(`[SEND-CODE] 이메일: ${userEmail}`);
|
||||||
|
process.stdout.write(`[SEND-CODE] 이메일: ${userEmail}\n`);
|
||||||
|
|
||||||
// 1. 이메일 중복 체크
|
try {
|
||||||
const existingUser = await this.userRepository.findOne({
|
// 1. 이메일 중복 체크
|
||||||
where: { userEmail },
|
this.logger.log(`[SEND-CODE] Step 1: 이메일 중복 체크`);
|
||||||
});
|
const existingUser = await this.userRepository.findOne({
|
||||||
|
where: { userEmail },
|
||||||
|
});
|
||||||
|
|
||||||
if (existingUser) {
|
if (existingUser) {
|
||||||
throw new ConflictException('이미 사용 중인 이메일입니다');
|
this.logger.warn(`[SEND-CODE] 이메일 중복 - ${userEmail}`);
|
||||||
|
throw new ConflictException('이미 사용 중인 이메일입니다');
|
||||||
|
}
|
||||||
|
this.logger.log(`[SEND-CODE] Step 1 완료: 중복 없음`);
|
||||||
|
|
||||||
|
// 2. 인증번호 생성
|
||||||
|
this.logger.log(`[SEND-CODE] Step 2: 인증번호 생성`);
|
||||||
|
const code = this.verificationService.generateCode();
|
||||||
|
this.logger.log(`[SEND-CODE] 생성된 인증번호: ${code}`);
|
||||||
|
process.stdout.write(`[SEND-CODE] 생성된 인증번호: ${code}\n`);
|
||||||
|
|
||||||
|
// 3. Redis에 저장
|
||||||
|
this.logger.log(`[SEND-CODE] Step 3: Redis 저장`);
|
||||||
|
const key = `signup:${userEmail}`;
|
||||||
|
await this.verificationService.saveCode(key, code);
|
||||||
|
this.logger.log(`[SEND-CODE] Redis 저장 완료 - key: ${key}`);
|
||||||
|
|
||||||
|
// 4. 이메일 발송
|
||||||
|
this.logger.log(`[SEND-CODE] Step 4: 이메일 발송 시작`);
|
||||||
|
await this.emailService.sendVerificationCode(userEmail, code);
|
||||||
|
this.logger.log(`[SEND-CODE] 이메일 발송 완료`);
|
||||||
|
|
||||||
|
this.logger.log(`[SEND-CODE] ========== 인증번호 발송 성공 ==========`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: '인증번호가 이메일로 발송되었습니다',
|
||||||
|
expiresIn: VERIFICATION_CONFIG.CODE_EXPIRY_SECONDS,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`[SEND-CODE] ========== 에러 발생 ==========`);
|
||||||
|
this.logger.error(`[SEND-CODE] Error Name: ${error.name}`);
|
||||||
|
this.logger.error(`[SEND-CODE] Error Message: ${error.message}`);
|
||||||
|
this.logger.error(`[SEND-CODE] Stack: ${error.stack}`);
|
||||||
|
process.stdout.write(`[SEND-CODE] ERROR: ${error.message}\n`);
|
||||||
|
process.stdout.write(`[SEND-CODE] STACK: ${error.stack}\n`);
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 인증번호 생성
|
|
||||||
const code = this.verificationService.generateCode();
|
|
||||||
console.log(`[DEBUG] Generated code for ${userEmail}: ${code}`);
|
|
||||||
|
|
||||||
// 3. Redis에 저장 (key: signup:이메일)
|
|
||||||
const key = `signup:${userEmail}`;
|
|
||||||
await this.verificationService.saveCode(key, code);
|
|
||||||
console.log(`[DEBUG] Saved code to Redis with key: ${key}`);
|
|
||||||
|
|
||||||
// 4. 이메일 발송
|
|
||||||
await this.emailService.sendVerificationCode(userEmail, code);
|
|
||||||
console.log(`[DEBUG] Email sent to: ${userEmail}`);
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
message: '인증번호가 이메일로 발송되었습니다',
|
|
||||||
expiresIn: VERIFICATION_CONFIG.CODE_EXPIRY_SECONDS,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 회원가입 - 이메일 인증번호 검증
|
* 회원가입 - 이메일 인증번호 검증
|
||||||
*
|
|
||||||
* @async
|
|
||||||
* @param {VerifySignupCodeDto} dto
|
|
||||||
* @returns {Promise<{ success: boolean; message: string; verified: boolean }>}
|
|
||||||
*/
|
*/
|
||||||
async verifySignupCode(dto: VerifySignupCodeDto): Promise<{
|
async verifySignupCode(dto: VerifySignupCodeDto): Promise<{
|
||||||
success: boolean;
|
success: boolean;
|
||||||
@@ -251,21 +247,21 @@ export class AuthService {
|
|||||||
verified: boolean;
|
verified: boolean;
|
||||||
}> {
|
}> {
|
||||||
const { userEmail, code } = dto;
|
const { userEmail, code } = dto;
|
||||||
console.log(`[DEBUG] Verifying code for ${userEmail}: ${code}`);
|
this.logger.log(`[VERIFY-CODE] 인증번호 검증 - email: ${userEmail}`);
|
||||||
|
|
||||||
// Redis에서 검증
|
|
||||||
const key = `signup:${userEmail}`;
|
const key = `signup:${userEmail}`;
|
||||||
const isValid = await this.verificationService.verifyCode(key, code);
|
const isValid = await this.verificationService.verifyCode(key, code);
|
||||||
console.log(`[DEBUG] Verification result: ${isValid}`);
|
|
||||||
|
|
||||||
if (!isValid) {
|
if (!isValid) {
|
||||||
|
this.logger.warn(`[VERIFY-CODE] 인증 실패 - email: ${userEmail}`);
|
||||||
throw new UnauthorizedException('인증번호가 일치하지 않거나 만료되었습니다');
|
throw new UnauthorizedException('인증번호가 일치하지 않거나 만료되었습니다');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 검증 완료 표시 (5분간 유효)
|
|
||||||
const verifiedKey = `signup-verified:${userEmail}`;
|
const verifiedKey = `signup-verified:${userEmail}`;
|
||||||
await this.verificationService.saveCode(verifiedKey, 'true');
|
await this.verificationService.saveCode(verifiedKey, 'true');
|
||||||
|
|
||||||
|
this.logger.log(`[VERIFY-CODE] 인증 성공 - email: ${userEmail}`);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message: '이메일 인증이 완료되었습니다',
|
message: '이메일 인증이 완료되었습니다',
|
||||||
@@ -274,19 +270,14 @@ export class AuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 아이디 찾기 - 인증번호 발송 (이메일 인증)
|
* 아이디 찾기 - 인증번호 발송
|
||||||
*
|
|
||||||
* @async
|
|
||||||
* @param {SendFindIdCodeDto} dto
|
|
||||||
* @returns {Promise<{ success: boolean; message: string; expiresIn: number }>}
|
|
||||||
*/
|
*/
|
||||||
async sendFindIdCode(
|
async sendFindIdCode(
|
||||||
dto: SendFindIdCodeDto,
|
dto: SendFindIdCodeDto,
|
||||||
): Promise<{ success: boolean; message: string; expiresIn: number }> {
|
): Promise<{ success: boolean; message: string; expiresIn: number }> {
|
||||||
const { userName, userEmail } = dto;
|
const { userName, userEmail } = dto;
|
||||||
console.log(`[아이디 찾기] 인증번호 발송 요청 - 이름: ${userName}, 이메일: ${userEmail}`);
|
this.logger.log(`[FIND-ID] 인증번호 발송 - name: ${userName}, email: ${userEmail}`);
|
||||||
|
|
||||||
// 1. 사용자 확인
|
|
||||||
const user = await this.userRepository.findOne({
|
const user = await this.userRepository.findOne({
|
||||||
where: { userName, userEmail },
|
where: { userName, userEmail },
|
||||||
});
|
});
|
||||||
@@ -295,18 +286,12 @@ export class AuthService {
|
|||||||
throw new NotFoundException('일치하는 사용자 정보를 찾을 수 없습니다');
|
throw new NotFoundException('일치하는 사용자 정보를 찾을 수 없습니다');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 인증번호 생성
|
|
||||||
const code = this.verificationService.generateCode();
|
const code = this.verificationService.generateCode();
|
||||||
console.log(`[아이디 찾기] 생성된 인증번호: ${code} (이메일: ${userEmail})`);
|
|
||||||
|
|
||||||
// 3. Redis에 저장 (key: find-id:이메일)
|
|
||||||
const key = `find-id:${userEmail}`;
|
const key = `find-id:${userEmail}`;
|
||||||
await this.verificationService.saveCode(key, code);
|
await this.verificationService.saveCode(key, code);
|
||||||
console.log(`[아이디 찾기] Redis 저장 완료 - Key: ${key}`);
|
|
||||||
|
|
||||||
// 4. 이메일 발송
|
|
||||||
await this.emailService.sendVerificationCode(userEmail, code);
|
await this.emailService.sendVerificationCode(userEmail, code);
|
||||||
console.log(`[아이디 찾기] 이메일 발송 완료 - 수신자: ${userEmail}`);
|
|
||||||
|
this.logger.log(`[FIND-ID] 인증번호 발송 완료 - email: ${userEmail}`);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
@@ -317,26 +302,18 @@ export class AuthService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 아이디 찾기 - 인증번호 검증
|
* 아이디 찾기 - 인증번호 검증
|
||||||
*
|
|
||||||
* @async
|
|
||||||
* @param {VerifyFindIdCodeDto} dto
|
|
||||||
* @returns {Promise<FindIdResponseDto>}
|
|
||||||
*/
|
*/
|
||||||
async verifyFindIdCode(dto: VerifyFindIdCodeDto): Promise<FindIdResponseDto> {
|
async verifyFindIdCode(dto: VerifyFindIdCodeDto): Promise<FindIdResponseDto> {
|
||||||
const { userEmail, verificationCode } = dto;
|
const { userEmail, verificationCode } = dto;
|
||||||
console.log(`[아이디 찾기] 인증번호 검증 요청 - 이메일: ${userEmail}, 입력 코드: ${verificationCode}`);
|
this.logger.log(`[FIND-ID] 인증번호 검증 - email: ${userEmail}`);
|
||||||
|
|
||||||
// 1. 인증번호 검증
|
|
||||||
const key = `find-id:${userEmail}`;
|
const key = `find-id:${userEmail}`;
|
||||||
console.log(`[아이디 찾기] 검증 Key: ${key}`);
|
|
||||||
const isValid = await this.verificationService.verifyCode(key, verificationCode);
|
const isValid = await this.verificationService.verifyCode(key, verificationCode);
|
||||||
console.log(`[아이디 찾기] 검증 결과: ${isValid}`);
|
|
||||||
|
|
||||||
if (!isValid) {
|
if (!isValid) {
|
||||||
throw new UnauthorizedException('인증번호가 일치하지 않거나 만료되었습니다');
|
throw new UnauthorizedException('인증번호가 일치하지 않거나 만료되었습니다');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 사용자 정보 조회
|
|
||||||
const user = await this.userRepository.findOne({
|
const user = await this.userRepository.findOne({
|
||||||
where: { userEmail },
|
where: { userEmail },
|
||||||
});
|
});
|
||||||
@@ -345,7 +322,6 @@ export class AuthService {
|
|||||||
throw new NotFoundException('사용자를 찾을 수 없습니다');
|
throw new NotFoundException('사용자를 찾을 수 없습니다');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 아이디 마스킹
|
|
||||||
const maskedUserId = this.maskUserId(user.userId);
|
const maskedUserId = this.maskUserId(user.userId);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -355,13 +331,6 @@ export class AuthService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 아이디 마스킹 (앞 4자리만 표시)
|
|
||||||
*
|
|
||||||
* @private
|
|
||||||
* @param {string} userId
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
private maskUserId(userId: string): string {
|
private maskUserId(userId: string): string {
|
||||||
if (userId.length <= 4) {
|
if (userId.length <= 4) {
|
||||||
return userId;
|
return userId;
|
||||||
@@ -372,19 +341,14 @@ export class AuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 비밀번호 재설정 - 인증번호 발송 (이메일 인증)
|
* 비밀번호 재설정 - 인증번호 발송
|
||||||
*
|
|
||||||
* @async
|
|
||||||
* @param {SendResetPasswordCodeDto} dto
|
|
||||||
* @returns {Promise<{ success: boolean; message: string; expiresIn: number }>}
|
|
||||||
*/
|
*/
|
||||||
async sendResetPasswordCode(
|
async sendResetPasswordCode(
|
||||||
dto: SendResetPasswordCodeDto,
|
dto: SendResetPasswordCodeDto,
|
||||||
): Promise<{ success: boolean; message: string; expiresIn: number }> {
|
): Promise<{ success: boolean; message: string; expiresIn: number }> {
|
||||||
const { userId, userEmail } = dto;
|
const { userId, userEmail } = dto;
|
||||||
console.log(`[비밀번호 찾기] 인증번호 발송 요청 - 아이디: ${userId}, 이메일: ${userEmail}`);
|
this.logger.log(`[RESET-PW] 인증번호 발송 - userId: ${userId}, email: ${userEmail}`);
|
||||||
|
|
||||||
// 1. 사용자 확인
|
|
||||||
const user = await this.userRepository.findOne({
|
const user = await this.userRepository.findOne({
|
||||||
where: { userId, userEmail },
|
where: { userId, userEmail },
|
||||||
});
|
});
|
||||||
@@ -393,18 +357,12 @@ export class AuthService {
|
|||||||
throw new NotFoundException('일치하는 사용자 정보를 찾을 수 없습니다');
|
throw new NotFoundException('일치하는 사용자 정보를 찾을 수 없습니다');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 인증번호 생성
|
|
||||||
const code = this.verificationService.generateCode();
|
const code = this.verificationService.generateCode();
|
||||||
console.log(`[비밀번호 찾기] 생성된 인증번호: ${code} (이메일: ${userEmail})`);
|
|
||||||
|
|
||||||
// 3. Redis에 저장 (key: reset-pw:이메일)
|
|
||||||
const key = `reset-pw:${userEmail}`;
|
const key = `reset-pw:${userEmail}`;
|
||||||
await this.verificationService.saveCode(key, code);
|
await this.verificationService.saveCode(key, code);
|
||||||
console.log(`[비밀번호 찾기] Redis 저장 완료 - Key: ${key}`);
|
|
||||||
|
|
||||||
// 4. 이메일 발송
|
|
||||||
await this.emailService.sendVerificationCode(userEmail, code);
|
await this.emailService.sendVerificationCode(userEmail, code);
|
||||||
console.log(`[비밀번호 찾기] 이메일 발송 완료 - 수신자: ${userEmail}`);
|
|
||||||
|
this.logger.log(`[RESET-PW] 인증번호 발송 완료 - email: ${userEmail}`);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
@@ -414,29 +372,21 @@ export class AuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 비밀번호 재설정 - 인증번호 검증 및 재설정 토큰 발급
|
* 비밀번호 재설정 - 인증번호 검증
|
||||||
*
|
|
||||||
* @async
|
|
||||||
* @param {VerifyResetPasswordCodeDto} dto
|
|
||||||
* @returns {Promise<{ success: boolean; message: string; resetToken: string }>}
|
|
||||||
*/
|
*/
|
||||||
async verifyResetPasswordCode(
|
async verifyResetPasswordCode(
|
||||||
dto: VerifyResetPasswordCodeDto,
|
dto: VerifyResetPasswordCodeDto,
|
||||||
): Promise<{ success: boolean; message: string; resetToken: string }> {
|
): Promise<{ success: boolean; message: string; resetToken: string }> {
|
||||||
const { userId, userEmail, verificationCode } = dto;
|
const { userId, userEmail, verificationCode } = dto;
|
||||||
console.log(`[비밀번호 찾기] 인증번호 검증 요청 - 아이디: ${userId}, 이메일: ${userEmail}, 입력 코드: ${verificationCode}`);
|
this.logger.log(`[RESET-PW] 인증번호 검증 - userId: ${userId}, email: ${userEmail}`);
|
||||||
|
|
||||||
// 1. 인증번호 검증
|
|
||||||
const key = `reset-pw:${userEmail}`;
|
const key = `reset-pw:${userEmail}`;
|
||||||
console.log(`[비밀번호 찾기] 검증 Key: ${key}`);
|
|
||||||
const isValid = await this.verificationService.verifyCode(key, verificationCode);
|
const isValid = await this.verificationService.verifyCode(key, verificationCode);
|
||||||
console.log(`[비밀번호 찾기] 검증 결과: ${isValid}`);
|
|
||||||
|
|
||||||
if (!isValid) {
|
if (!isValid) {
|
||||||
throw new UnauthorizedException('인증번호가 일치하지 않거나 만료되었습니다');
|
throw new UnauthorizedException('인증번호가 일치하지 않거나 만료되었습니다');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 사용자 확인
|
|
||||||
const user = await this.userRepository.findOne({
|
const user = await this.userRepository.findOne({
|
||||||
where: { userId, userEmail },
|
where: { userId, userEmail },
|
||||||
});
|
});
|
||||||
@@ -445,7 +395,6 @@ export class AuthService {
|
|||||||
throw new NotFoundException('사용자를 찾을 수 없습니다');
|
throw new NotFoundException('사용자를 찾을 수 없습니다');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 비밀번호 재설정 토큰 생성 및 저장 (30분 유효)
|
|
||||||
const resetToken = await this.verificationService.generateResetToken(userId);
|
const resetToken = await this.verificationService.generateResetToken(userId);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -457,22 +406,17 @@ export class AuthService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 비밀번호 재설정 - 새 비밀번호로 변경
|
* 비밀번호 재설정 - 새 비밀번호로 변경
|
||||||
*
|
|
||||||
* @async
|
|
||||||
* @param {ResetPasswordDto} dto
|
|
||||||
* @returns {Promise<ResetPasswordResponseDto>}
|
|
||||||
*/
|
*/
|
||||||
async resetPassword(dto: ResetPasswordDto): Promise<ResetPasswordResponseDto> {
|
async resetPassword(dto: ResetPasswordDto): Promise<ResetPasswordResponseDto> {
|
||||||
const { resetToken, newPassword } = dto; // 요청
|
const { resetToken, newPassword } = dto;
|
||||||
|
this.logger.log(`[RESET-PW] 비밀번호 변경 시도`);
|
||||||
|
|
||||||
// 1. 재설정 토큰 검증
|
|
||||||
const userId = await this.verificationService.verifyResetToken(resetToken);
|
const userId = await this.verificationService.verifyResetToken(resetToken);
|
||||||
|
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
throw new UnauthorizedException('유효하지 않거나 만료된 토큰입니다');
|
throw new UnauthorizedException('유효하지 않거나 만료된 토큰입니다');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 사용자 조회
|
|
||||||
const user = await this.userRepository.findOne({
|
const user = await this.userRepository.findOne({
|
||||||
where: { userId },
|
where: { userId },
|
||||||
});
|
});
|
||||||
@@ -481,14 +425,14 @@ export class AuthService {
|
|||||||
throw new NotFoundException('사용자를 찾을 수 없습니다');
|
throw new NotFoundException('사용자를 찾을 수 없습니다');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 새 비밀번호 해싱
|
|
||||||
const saltRounds = parseInt(this.configService.get<string>('BCRYPT_SALT_ROUNDS') || '10', 10);
|
const saltRounds = parseInt(this.configService.get<string>('BCRYPT_SALT_ROUNDS') || '10', 10);
|
||||||
const hashedPassword = await bcrypt.hash(newPassword, saltRounds);
|
const hashedPassword = await bcrypt.hash(newPassword, saltRounds);
|
||||||
|
|
||||||
// 4. 비밀번호 업데이트
|
|
||||||
user.userPw = hashedPassword;
|
user.userPw = hashedPassword;
|
||||||
await this.userRepository.save(user);
|
await this.userRepository.save(user);
|
||||||
|
|
||||||
|
this.logger.log(`[RESET-PW] 비밀번호 변경 완료 - userId: ${userId}`);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
message: '비밀번호가 변경되었습니다',
|
message: '비밀번호가 변경되었습니다',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,26 +4,17 @@ import {
|
|||||||
ArgumentsHost,
|
ArgumentsHost,
|
||||||
HttpException,
|
HttpException,
|
||||||
HttpStatus,
|
HttpStatus,
|
||||||
|
Logger,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 모든 예외 필터
|
* 모든 예외 필터 - Docker 환경에서 로그 출력 보장
|
||||||
*
|
|
||||||
* @description
|
|
||||||
* HTTP 예외뿐만 아니라 모든 예외를 잡아서 처리합니다.
|
|
||||||
* 예상치 못한 에러도 일관된 형식으로 응답합니다.
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* // main.ts에서 전역 적용
|
|
||||||
* app.useGlobalFilters(new AllExceptionsFilter());
|
|
||||||
*
|
|
||||||
* @export
|
|
||||||
* @class AllExceptionsFilter
|
|
||||||
* @implements {ExceptionFilter}
|
|
||||||
*/
|
*/
|
||||||
@Catch()
|
@Catch()
|
||||||
export class AllExceptionsFilter implements ExceptionFilter {
|
export class AllExceptionsFilter implements ExceptionFilter {
|
||||||
|
private readonly logger = new Logger('ExceptionFilter');
|
||||||
|
|
||||||
catch(exception: unknown, host: ArgumentsHost) {
|
catch(exception: unknown, host: ArgumentsHost) {
|
||||||
const ctx = host.switchToHttp();
|
const ctx = host.switchToHttp();
|
||||||
const response = ctx.getResponse<Response>();
|
const response = ctx.getResponse<Response>();
|
||||||
@@ -33,7 +24,6 @@ export class AllExceptionsFilter implements ExceptionFilter {
|
|||||||
let message: string | string[];
|
let message: string | string[];
|
||||||
|
|
||||||
if (exception instanceof HttpException) {
|
if (exception instanceof HttpException) {
|
||||||
// HTTP 예외 처리
|
|
||||||
status = exception.getStatus();
|
status = exception.getStatus();
|
||||||
const exceptionResponse = exception.getResponse();
|
const exceptionResponse = exception.getResponse();
|
||||||
|
|
||||||
@@ -45,14 +35,8 @@ export class AllExceptionsFilter implements ExceptionFilter {
|
|||||||
message = exception.message;
|
message = exception.message;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 예상치 못한 에러 처리
|
|
||||||
status = HttpStatus.INTERNAL_SERVER_ERROR;
|
status = HttpStatus.INTERNAL_SERVER_ERROR;
|
||||||
message = '서버 내부 오류가 발생했습니다.';
|
message = exception instanceof Error ? exception.message : '서버 내부 오류가 발생했습니다.';
|
||||||
|
|
||||||
// 개발 환경에서는 실제 에러 메시지 표시
|
|
||||||
if (process.env.NODE_ENV === 'development' && exception instanceof Error) {
|
|
||||||
message = exception.message;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const errorResponse = {
|
const errorResponse = {
|
||||||
@@ -64,16 +48,46 @@ export class AllExceptionsFilter implements ExceptionFilter {
|
|||||||
message: Array.isArray(message) ? message : [message],
|
message: Array.isArray(message) ? message : [message],
|
||||||
};
|
};
|
||||||
|
|
||||||
// 개발 환경에서는 스택 트레이스 포함
|
// 스택 트레이스 포함
|
||||||
if (process.env.NODE_ENV === 'development' && exception instanceof Error) {
|
if (exception instanceof Error) {
|
||||||
(errorResponse as any).stack = exception.stack;
|
(errorResponse as any).stack = exception.stack;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 에러 로깅
|
// ========== 상세 로깅 (stdout으로 즉시 출력) ==========
|
||||||
console.error(
|
const logMessage = [
|
||||||
`[${errorResponse.timestamp}] ${request.method} ${request.url} - ${status}`,
|
'',
|
||||||
exception,
|
'╔══════════════════════════════════════════════════════════════╗',
|
||||||
);
|
'║ EXCEPTION OCCURRED ║',
|
||||||
|
'╚══════════════════════════════════════════════════════════════╝',
|
||||||
|
` Timestamp : ${errorResponse.timestamp}`,
|
||||||
|
` Method : ${request.method}`,
|
||||||
|
` Path : ${request.url}`,
|
||||||
|
` Status : ${status}`,
|
||||||
|
` Message : ${JSON.stringify(message)}`,
|
||||||
|
];
|
||||||
|
|
||||||
|
if (exception instanceof Error) {
|
||||||
|
logMessage.push(` Error Name: ${exception.name}`);
|
||||||
|
logMessage.push(` Stack :`);
|
||||||
|
logMessage.push(exception.stack || 'No stack trace');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request Body 로깅 (민감정보 마스킹)
|
||||||
|
const safeBody = { ...request.body };
|
||||||
|
if (safeBody.password) safeBody.password = '****';
|
||||||
|
if (safeBody.token) safeBody.token = '****';
|
||||||
|
logMessage.push(` Body : ${JSON.stringify(safeBody)}`);
|
||||||
|
logMessage.push('══════════════════════════════════════════════════════════════════');
|
||||||
|
logMessage.push('');
|
||||||
|
|
||||||
|
// NestJS Logger 사용 (Docker stdout으로 출력)
|
||||||
|
this.logger.error(logMessage.join('\n'));
|
||||||
|
|
||||||
|
// 추가로 console.error도 출력 (백업)
|
||||||
|
console.error(logMessage.join('\n'));
|
||||||
|
|
||||||
|
// process.stdout으로 직접 출력 (버퍼링 우회)
|
||||||
|
process.stdout.write(logMessage.join('\n') + '\n');
|
||||||
|
|
||||||
response.status(status).json(errorResponse);
|
response.status(status).json(errorResponse);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,50 +3,42 @@ import {
|
|||||||
NestInterceptor,
|
NestInterceptor,
|
||||||
ExecutionContext,
|
ExecutionContext,
|
||||||
CallHandler,
|
CallHandler,
|
||||||
|
Logger,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { tap } from 'rxjs/operators';
|
import { tap } from 'rxjs/operators';
|
||||||
import { Request } from 'express';
|
import { Request } from 'express';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 로깅 인터셉터
|
* 로깅 인터셉터 - Docker 환경에서 로그 출력 보장
|
||||||
*
|
|
||||||
* @description
|
|
||||||
* API 요청/응답을 로깅하고 실행 시간을 측정합니다.
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* // main.ts에서 전역 적용
|
|
||||||
* app.useGlobalInterceptors(new LoggingInterceptor());
|
|
||||||
*
|
|
||||||
* @export
|
|
||||||
* @class LoggingInterceptor
|
|
||||||
* @implements {NestInterceptor}
|
|
||||||
*/
|
*/
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class LoggingInterceptor implements NestInterceptor {
|
export class LoggingInterceptor implements NestInterceptor {
|
||||||
|
private readonly logger = new Logger('HTTP');
|
||||||
|
|
||||||
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
|
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
|
||||||
const request = context.switchToHttp().getRequest<Request>();
|
const request = context.switchToHttp().getRequest<Request>();
|
||||||
const { method, url, ip } = request;
|
const { method, url, ip } = request;
|
||||||
const userAgent = request.get('user-agent') || '';
|
const userAgent = request.get('user-agent') || '';
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
|
|
||||||
console.log(
|
const incomingLog = `[REQUEST] ${method} ${url} - IP: ${ip}`;
|
||||||
`[${new Date().toISOString()}] Incoming Request: ${method} ${url} - ${ip} - ${userAgent}`,
|
this.logger.log(incomingLog);
|
||||||
);
|
process.stdout.write(`${new Date().toISOString()} - ${incomingLog}\n`);
|
||||||
|
|
||||||
return next.handle().pipe(
|
return next.handle().pipe(
|
||||||
tap({
|
tap({
|
||||||
next: (data) => {
|
next: () => {
|
||||||
const responseTime = Date.now() - now;
|
const responseTime = Date.now() - now;
|
||||||
console.log(
|
const successLog = `[RESPONSE] ${method} ${url} - ${responseTime}ms - SUCCESS`;
|
||||||
`[${new Date().toISOString()}] Response: ${method} ${url} - ${responseTime}ms`,
|
this.logger.log(successLog);
|
||||||
);
|
process.stdout.write(`${new Date().toISOString()} - ${successLog}\n`);
|
||||||
},
|
},
|
||||||
error: (error) => {
|
error: (error) => {
|
||||||
const responseTime = Date.now() - now;
|
const responseTime = Date.now() - now;
|
||||||
console.error(
|
const errorLog = `[RESPONSE] ${method} ${url} - ${responseTime}ms - ERROR: ${error.message}`;
|
||||||
`[${new Date().toISOString()}] Error Response: ${method} ${url} - ${responseTime}ms - ${error.message}`,
|
this.logger.error(errorLog);
|
||||||
);
|
process.stdout.write(`${new Date().toISOString()} - ${errorLog}\n`);
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,64 +1,97 @@
|
|||||||
|
// ========== 즉시 로그 출력 (Docker 로그 테스트) ==========
|
||||||
|
console.log('========================================');
|
||||||
|
console.log('[BOOT] main.ts loaded at', new Date().toISOString());
|
||||||
|
console.log('[BOOT] Node version:', process.version);
|
||||||
|
console.log('[BOOT] ENV:', process.env.NODE_ENV);
|
||||||
|
console.log('========================================');
|
||||||
|
|
||||||
import { NestFactory, Reflector } from '@nestjs/core';
|
import { NestFactory, Reflector } from '@nestjs/core';
|
||||||
import { AppModule } from './app.module';
|
import { AppModule } from './app.module';
|
||||||
import { ValidationPipe } from '@nestjs/common';
|
import { ValidationPipe, Logger } from '@nestjs/common';
|
||||||
import { JwtAuthGuard } from './common/guards/jwt-auth.guard';
|
import { JwtAuthGuard } from './common/guards/jwt-auth.guard';
|
||||||
import { AllExceptionsFilter } from './common/filters/all-exceptions.filter';
|
import { AllExceptionsFilter } from './common/filters/all-exceptions.filter';
|
||||||
import { LoggingInterceptor } from './common/interceptors/logging.interceptor';
|
import { LoggingInterceptor } from './common/interceptors/logging.interceptor';
|
||||||
import { TransformInterceptor } from './common/interceptors/transform.interceptor';
|
import { TransformInterceptor } from './common/interceptors/transform.interceptor';
|
||||||
import { types } from 'pg';
|
import { types } from 'pg';
|
||||||
|
|
||||||
// PostgreSQL numeric/decimal 타입을 JavaScript number로 자동 변환
|
// ========== 전역 에러 핸들러 ==========
|
||||||
// 1700 = numeric type OID
|
process.on('uncaughtException', (error) => {
|
||||||
|
console.error('╔═══════════ UNCAUGHT EXCEPTION ═══════════╗');
|
||||||
|
console.error(`Timestamp: ${new Date().toISOString()}`);
|
||||||
|
console.error(`Error: ${error.message}`);
|
||||||
|
console.error(`Stack: ${error.stack}`);
|
||||||
|
console.error('╚══════════════════════════════════════════╝');
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on('unhandledRejection', (reason) => {
|
||||||
|
console.error('╔═══════════ UNHANDLED REJECTION ═══════════╗');
|
||||||
|
console.error(`Timestamp: ${new Date().toISOString()}`);
|
||||||
|
console.error(`Reason: ${reason}`);
|
||||||
|
console.error('╚═══════════════════════════════════════════╝');
|
||||||
|
});
|
||||||
|
|
||||||
|
// PostgreSQL numeric 타입 변환
|
||||||
types.setTypeParser(1700, parseFloat);
|
types.setTypeParser(1700, parseFloat);
|
||||||
|
|
||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
const app = await NestFactory.create(AppModule);
|
const logger = new Logger('Bootstrap');
|
||||||
|
|
||||||
|
logger.log('========================================');
|
||||||
|
logger.log('Application starting...');
|
||||||
|
logger.log(`NODE_ENV: ${process.env.NODE_ENV}`);
|
||||||
|
logger.log(`POSTGRES_HOST: ${process.env.POSTGRES_HOST}`);
|
||||||
|
logger.log('========================================');
|
||||||
|
|
||||||
// CORS 추가
|
try {
|
||||||
app.enableCors({
|
const app = await NestFactory.create(AppModule, {
|
||||||
origin: (origin, callback) => {
|
logger: ['error', 'warn', 'log', 'debug', 'verbose'],
|
||||||
// origin이 없는 경우 (서버 간 요청, Postman 등) 허용
|
bufferLogs: false,
|
||||||
if (!origin) return callback(null, true);
|
});
|
||||||
|
|
||||||
const allowedPatterns = [
|
|
||||||
/^http:\/\/localhost:\d+$/, // localhost 모든 포트
|
|
||||||
/^http:\/\/127\.0\.0\.1:\d+$/, // 127.0.0.1 모든 포트
|
|
||||||
/^http:\/\/192\.168\.11\.\d+:\d+$/, // 192.168.11.* 대역
|
|
||||||
/^https?:\/\/.*\.turbosoft\.kr$/, // *.turbosoft.kr
|
|
||||||
];
|
|
||||||
|
|
||||||
const isAllowed = allowedPatterns.some(pattern => pattern.test(origin));
|
|
||||||
callback(null, isAllowed);
|
|
||||||
},
|
|
||||||
credentials: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
// ValidationPipe 추가 (Body 파싱과 Dto 유효성 global 설정)
|
logger.log('NestFactory.create() 완료');
|
||||||
app.useGlobalPipes(
|
|
||||||
new ValidationPipe({
|
app.enableCors({
|
||||||
transform: true,
|
origin: (origin, callback) => {
|
||||||
whitelist: false, // nested object를 위해 whitelist 비활성화
|
if (!origin) return callback(null, true);
|
||||||
transformOptions: {
|
|
||||||
enableImplicitConversion: true,
|
const allowedPatterns = [
|
||||||
|
/^http:\/\/localhost:\d+$/,
|
||||||
|
/^http:\/\/127\.0\.0\.1:\d+$/,
|
||||||
|
/^http:\/\/192\.168\.11\.\d+:\d+$/,
|
||||||
|
/^https?:\/\/.*\.turbosoft\.kr$/,
|
||||||
|
];
|
||||||
|
|
||||||
|
const isAllowed = allowedPatterns.some(pattern => pattern.test(origin));
|
||||||
|
callback(null, isAllowed);
|
||||||
},
|
},
|
||||||
}),
|
credentials: true,
|
||||||
);
|
});
|
||||||
|
|
||||||
// 전역 필터 적용 - 모든 예외를 잡아서 일관된 형식으로 응답
|
app.useGlobalPipes(
|
||||||
app.useGlobalFilters(new AllExceptionsFilter());
|
new ValidationPipe({
|
||||||
|
transform: true,
|
||||||
|
whitelist: false,
|
||||||
|
transformOptions: { enableImplicitConversion: true },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
// 전역 인터셉터 적용
|
app.useGlobalFilters(new AllExceptionsFilter());
|
||||||
app.useGlobalInterceptors(
|
app.useGlobalInterceptors(new LoggingInterceptor(), new TransformInterceptor());
|
||||||
new LoggingInterceptor(), // 요청/응답 로깅
|
|
||||||
new TransformInterceptor(), // 일관된 응답 변환 (success, data, timestamp)
|
|
||||||
//backend\src\common\interceptors\transform.interceptor.ts 구현체
|
|
||||||
);
|
|
||||||
|
|
||||||
// 전역 JWT 인증 가드 적용 (@Public 데코레이터가 있는 엔드포인트는 제외)
|
const reflector = app.get(Reflector);
|
||||||
const reflector = app.get(Reflector);
|
app.useGlobalGuards(new JwtAuthGuard(reflector));
|
||||||
app.useGlobalGuards(new JwtAuthGuard(reflector));
|
|
||||||
|
|
||||||
await app.listen(process.env.PORT ?? 4000); // 로컬 개발환경
|
const port = process.env.PORT ?? 4000;
|
||||||
//await app.listen(process.env.PORT ?? 4000, '0.0.0.0'); // 모든 네트워크 외부 바인딩
|
await app.listen(port, '0.0.0.0');
|
||||||
|
|
||||||
|
logger.log(`✅ Server running on port ${port}`);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('❌ Application failed to start!');
|
||||||
|
logger.error(`Error: ${error.message}`);
|
||||||
|
logger.error(`Stack: ${error.stack}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
bootstrap();
|
|
||||||
|
bootstrap();
|
||||||
|
|||||||
@@ -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 {}
|
|
||||||
@@ -1,55 +1,88 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import * as nodemailer from 'nodemailer';
|
import * as nodemailer from 'nodemailer';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 이메일 발송 서비스
|
* 이메일 발송 서비스
|
||||||
*
|
|
||||||
* @export
|
|
||||||
* @class EmailService
|
|
||||||
*/
|
*/
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class EmailService {
|
export class EmailService {
|
||||||
private transporter; // nodemailer 전송 객체
|
private readonly logger = new Logger(EmailService.name);
|
||||||
|
private transporter;
|
||||||
|
|
||||||
constructor(private configService: ConfigService) {
|
constructor(private configService: ConfigService) {
|
||||||
// SMTP 서버 설정 (AWS SES, Gmail 등)
|
const smtpHost = this.configService.get('SMTP_HOST');
|
||||||
this.transporter = nodemailer.createTransport({ // .env 파일 EMAIL CONFIGURATION
|
const smtpPort = this.configService.get('SMTP_PORT');
|
||||||
host: this.configService.get('SMTP_HOST'),
|
const smtpUser = this.configService.get('SMTP_USER');
|
||||||
port: parseInt(this.configService.get('SMTP_PORT')),
|
const smtpPass = this.configService.get('SMTP_PASS');
|
||||||
secure: this.configService.get('SMTP_PORT') === '465',
|
|
||||||
|
this.logger.log(`[EMAIL] SMTP 설정 초기화`);
|
||||||
|
this.logger.log(`[EMAIL] Host: ${smtpHost}`);
|
||||||
|
this.logger.log(`[EMAIL] Port: ${smtpPort}`);
|
||||||
|
this.logger.log(`[EMAIL] User: ${smtpUser}`);
|
||||||
|
this.logger.log(`[EMAIL] Pass: ${smtpPass ? '****설정됨' : '미설정!!'}`);
|
||||||
|
process.stdout.write(`[EMAIL] SMTP Config - Host: ${smtpHost}, Port: ${smtpPort}, User: ${smtpUser}\n`);
|
||||||
|
|
||||||
|
if (!smtpHost || !smtpPort || !smtpUser || !smtpPass) {
|
||||||
|
this.logger.error(`[EMAIL] SMTP 설정 누락! 환경변수를 확인하세요.`);
|
||||||
|
process.stdout.write(`[EMAIL] ERROR: SMTP 설정 누락!\n`);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.transporter = nodemailer.createTransport({
|
||||||
|
host: smtpHost,
|
||||||
|
port: parseInt(smtpPort || '587'),
|
||||||
|
secure: smtpPort === '465',
|
||||||
auth: {
|
auth: {
|
||||||
user: this.configService.get('SMTP_USER'),
|
user: smtpUser,
|
||||||
pass: this.configService.get('SMTP_PASS'),
|
pass: smtpPass,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 인증번호 이메일 발송
|
* 인증번호 이메일 발송
|
||||||
*
|
|
||||||
* @async
|
|
||||||
* @param {string} email - 수신자 이메일
|
|
||||||
* @param {string} code - 6자리 인증번호
|
|
||||||
* @returns {Promise<void>}
|
|
||||||
*/
|
*/
|
||||||
async sendVerificationCode(email: string, code: string): Promise<void> {
|
async sendVerificationCode(email: string, code: string): Promise<void> {
|
||||||
await this.transporter.sendMail({
|
const fromEmail = this.configService.get('FROM_EMAIL');
|
||||||
from: this.configService.get('FROM_EMAIL'),
|
|
||||||
to: email,
|
this.logger.log(`[EMAIL] ========== 이메일 발송 시작 ==========`);
|
||||||
subject: '[한우 유전능력 시스템] 인증번호 안내',
|
this.logger.log(`[EMAIL] From: ${fromEmail}`);
|
||||||
html: `
|
this.logger.log(`[EMAIL] To: ${email}`);
|
||||||
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
|
this.logger.log(`[EMAIL] Code: ${code}`);
|
||||||
<h2 style="color: #333;">인증번호 안내</h2>
|
process.stdout.write(`[EMAIL] Sending to: ${email}, code: ${code}\n`);
|
||||||
<p>아래 인증번호를 입력해주세요.</p>
|
|
||||||
<div style="background-color: #f5f5f5; padding: 20px; text-align: center; margin: 20px 0;">
|
try {
|
||||||
<h1 style="color: #4CAF50; font-size: 32px; margin: 0;">${code}</h1>
|
const result = await this.transporter.sendMail({
|
||||||
|
from: fromEmail,
|
||||||
|
to: email,
|
||||||
|
subject: '[한우 유전능력 시스템] 인증번호 안내',
|
||||||
|
html: `
|
||||||
|
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
|
||||||
|
<h2 style="color: #333;">인증번호 안내</h2>
|
||||||
|
<p>아래 인증번호를 입력해주세요.</p>
|
||||||
|
<div style="background-color: #f5f5f5; padding: 20px; text-align: center; margin: 20px 0;">
|
||||||
|
<h1 style="color: #4CAF50; font-size: 32px; margin: 0;">${code}</h1>
|
||||||
|
</div>
|
||||||
|
<p style="color: #666;">인증번호는 3분간 유효합니다.</p>
|
||||||
|
<hr style="border: none; border-top: 1px solid #eee; margin: 20px 0;">
|
||||||
|
<p style="color: #999; font-size: 12px;">본 메일은 발신 전용입니다.</p>
|
||||||
</div>
|
</div>
|
||||||
<p style="color: #666;">인증번호는 3분간 유효합니다.</p>
|
`,
|
||||||
<hr style="border: none; border-top: 1px solid #eee; margin: 20px 0;">
|
});
|
||||||
<p style="color: #999; font-size: 12px;">본 메일은 발신 전용입니다.</p>
|
|
||||||
</div>
|
this.logger.log(`[EMAIL] 발송 성공 - MessageId: ${result.messageId}`);
|
||||||
`,
|
this.logger.log(`[EMAIL] Response: ${JSON.stringify(result)}`);
|
||||||
});
|
process.stdout.write(`[EMAIL] SUCCESS - MessageId: ${result.messageId}\n`);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`[EMAIL] ========== 발송 실패 ==========`);
|
||||||
|
this.logger.error(`[EMAIL] Error Name: ${error.name}`);
|
||||||
|
this.logger.error(`[EMAIL] Error Message: ${error.message}`);
|
||||||
|
this.logger.error(`[EMAIL] Error Code: ${error.code}`);
|
||||||
|
this.logger.error(`[EMAIL] Stack: ${error.stack}`);
|
||||||
|
process.stdout.write(`[EMAIL] ERROR: ${error.message}\n`);
|
||||||
|
process.stdout.write(`[EMAIL] STACK: ${error.stack}\n`);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,20 +6,15 @@ import { VerificationService } from './verification.service';
|
|||||||
*
|
*
|
||||||
* @description
|
* @description
|
||||||
* 인증번호 생성 및 검증 기능을 제공하는 모듈입니다.
|
* 인증번호 생성 및 검증 기능을 제공하는 모듈입니다.
|
||||||
* Redis를 사용하여 인증번호를 임시 저장하고 검증합니다.
|
* 메모리 Map을 사용하여 인증번호를 임시 저장하고 검증합니다.
|
||||||
*
|
*
|
||||||
* 사용 예:
|
* 사용 예:
|
||||||
* - 아이디 찾기 인증번호 발송
|
* - 아이디 찾기 인증번호 발송
|
||||||
* - 비밀번호 재설정 인증번호 발송
|
* - 비밀번호 재설정 인증번호 발송
|
||||||
* - 회원가입 이메일 인증
|
* - 회원가입 이메일 인증
|
||||||
*
|
|
||||||
* RedisModule이 @Global로 설정되어 있어 자동으로 주입됩니다.
|
|
||||||
*
|
|
||||||
* @export
|
|
||||||
* @class VerificationModule
|
|
||||||
*/
|
*/
|
||||||
@Module({
|
@Module({
|
||||||
providers: [VerificationService],
|
providers: [VerificationService],
|
||||||
exports: [VerificationService],
|
exports: [VerificationService],
|
||||||
})
|
})
|
||||||
export class VerificationModule {}
|
export class VerificationModule {}
|
||||||
|
|||||||
@@ -1,97 +1,131 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
import { InjectRedis } from '@nestjs-modules/ioredis';
|
|
||||||
import Redis from 'ioredis';
|
|
||||||
import * as crypto from 'crypto';
|
import * as crypto from 'crypto';
|
||||||
import { VERIFICATION_CONFIG } from 'src/common/config/VerificationConfig';
|
import { VERIFICATION_CONFIG } from 'src/common/config/VerificationConfig';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 인증번호 생성 및 검증 서비스 (Redis 기반)
|
* 인증번호 생성 및 검증 서비스 (메모리 기반)
|
||||||
*
|
|
||||||
* @export
|
|
||||||
* @class VerificationService
|
|
||||||
*/
|
*/
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class VerificationService {
|
export class VerificationService {
|
||||||
constructor(@InjectRedis() private readonly redis: Redis) {}
|
private readonly logger = new Logger(VerificationService.name);
|
||||||
|
|
||||||
|
// 메모리 저장소 (key -> { code, expiresAt })
|
||||||
|
private readonly store = new Map<string, { value: string; expiresAt: number }>();
|
||||||
|
|
||||||
/**
|
constructor() {
|
||||||
* 6자리 인증번호 생성
|
this.logger.log(`[VERIFY] VerificationService 초기화 (메모리 모드)`);
|
||||||
*
|
|
||||||
* @returns {string} 6자리 숫자
|
// 만료된 항목 정리 (1분마다)
|
||||||
*/
|
setInterval(() => this.cleanup(), 60000);
|
||||||
generateCode(): string {
|
}
|
||||||
return Math.floor(100000 + Math.random() * 900000).toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 인증번호 저장 (Redis 3분 후 자동 삭제)
|
* 만료된 항목 정리
|
||||||
*
|
*/
|
||||||
* @async
|
private cleanup(): void {
|
||||||
* @param {string} key - Redis 키 (예: find-id:test@example.com)
|
const now = Date.now();
|
||||||
* @param {string} code - 인증번호
|
let cleaned = 0;
|
||||||
* @returns {Promise<void>}
|
|
||||||
*/
|
for (const [key, data] of this.store.entries()) {
|
||||||
async saveCode(key: string, code: string): Promise<void> {
|
if (data.expiresAt < now) {
|
||||||
await this.redis.set(key, code, 'EX', VERIFICATION_CONFIG.CODE_EXPIRY_SECONDS);
|
this.store.delete(key);
|
||||||
}
|
cleaned++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cleaned > 0) {
|
||||||
|
this.logger.debug(`[VERIFY] 만료된 ${cleaned}개 항목 정리됨`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 인증번호 검증
|
* 6자리 인증번호 생성
|
||||||
*
|
*/
|
||||||
* @async
|
generateCode(): string {
|
||||||
* @param {string} key - Redis 키
|
const code = Math.floor(100000 + Math.random() * 900000).toString();
|
||||||
* @param {string} code - 사용자가 입력한 인증번호
|
this.logger.log(`[VERIFY] 인증번호 생성: ${code}`);
|
||||||
* @returns {Promise<boolean>} 검증 성공 여부
|
return code;
|
||||||
*/
|
}
|
||||||
async verifyCode(key: string, code: string): Promise<boolean> {
|
|
||||||
const savedCode = await this.redis.get(key);
|
|
||||||
console.log(`[DEBUG VerificationService] Key: ${key}, Input code: ${code}, Saved code: ${savedCode}`);
|
|
||||||
|
|
||||||
if (!savedCode) {
|
/**
|
||||||
console.log(`[DEBUG VerificationService] No saved code found for key: ${key}`);
|
* 인증번호 저장 (TTL 적용)
|
||||||
return false; // 인증번호 없음 (만료 또는 미발급)
|
*/
|
||||||
}
|
async saveCode(key: string, code: string): Promise<void> {
|
||||||
|
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}초`);
|
||||||
|
}
|
||||||
|
|
||||||
if (savedCode !== code) {
|
/**
|
||||||
console.log(`[DEBUG VerificationService] Code mismatch - Saved: ${savedCode}, Input: ${code}`);
|
* 인증번호 검증
|
||||||
return false; // 인증번호 불일치
|
*/
|
||||||
}
|
async verifyCode(key: string, code: string): Promise<boolean> {
|
||||||
|
this.logger.log(`[VERIFY] 코드 검증 - Key: ${key}, Input: ${code}`);
|
||||||
|
|
||||||
// 검증 성공 시 Redis에서 삭제 (1회용)
|
const data = this.store.get(key);
|
||||||
await this.redis.del(key);
|
|
||||||
console.log(`[DEBUG VerificationService] Code verified successfully!`);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
if (!data) {
|
||||||
* 비밀번호 재설정 토큰 생성 및 저장
|
this.logger.warn(`[VERIFY] 저장된 코드 없음 (만료 또는 미발급)`);
|
||||||
*
|
return false;
|
||||||
* @async
|
}
|
||||||
* @param {string} userId - 사용자 ID
|
|
||||||
* @returns {Promise<string>} 재설정 토큰
|
|
||||||
*/
|
|
||||||
async generateResetToken(userId: string): Promise<string> {
|
|
||||||
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);
|
|
||||||
return token;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
// 만료 체크
|
||||||
* 비밀번호 재설정 토큰 검증
|
if (data.expiresAt < Date.now()) {
|
||||||
*
|
this.logger.warn(`[VERIFY] 코드 만료됨`);
|
||||||
* @async
|
this.store.delete(key);
|
||||||
* @param {string} token - 재설정 토큰
|
return false;
|
||||||
* @returns {Promise<string | null>} 사용자 ID 또는 null
|
}
|
||||||
*/
|
|
||||||
async verifyResetToken(token: string): Promise<string | null> {
|
|
||||||
const userId = await this.redis.get(`reset:${token}`);
|
|
||||||
|
|
||||||
if (!userId) {
|
if (data.value !== code) {
|
||||||
return null; // 토큰 없음 (만료 또는 미발급)
|
this.logger.warn(`[VERIFY] 코드 불일치 - Saved: ${data.value}, Input: ${code}`);
|
||||||
}
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// 검증 성공 시 토큰 삭제 (1회용)
|
// 검증 성공 시 삭제 (1회용)
|
||||||
await this.redis.del(`reset:${token}`);
|
this.store.delete(key);
|
||||||
return userId;
|
this.logger.log(`[VERIFY] 검증 성공, 코드 삭제됨`);
|
||||||
}
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 비밀번호 재설정 토큰 생성 및 저장
|
||||||
|
*/
|
||||||
|
async generateResetToken(userId: string): Promise<string> {
|
||||||
|
this.logger.log(`[VERIFY] 리셋 토큰 생성 - userId: ${userId}`);
|
||||||
|
|
||||||
|
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<string | null> {
|
||||||
|
this.logger.log(`[VERIFY] 리셋 토큰 검증`);
|
||||||
|
|
||||||
|
const key = `reset:${token}`;
|
||||||
|
const data = this.store.get(key);
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
14
backend/src/system/system.controller.ts
Normal file
14
backend/src/system/system.controller.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { Controller, Get } from '@nestjs/common';
|
||||||
|
import { SystemService, SystemHealthResponse } from './system.service';
|
||||||
|
import { Public } from '../common/decorators/public.decorator';
|
||||||
|
|
||||||
|
@Controller('system')
|
||||||
|
export class SystemController {
|
||||||
|
constructor(private readonly systemService: SystemService) {}
|
||||||
|
|
||||||
|
@Public()
|
||||||
|
@Get('health')
|
||||||
|
async getHealth(): Promise<SystemHealthResponse> {
|
||||||
|
return this.systemService.getHealth();
|
||||||
|
}
|
||||||
|
}
|
||||||
9
backend/src/system/system.module.ts
Normal file
9
backend/src/system/system.module.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { SystemController } from './system.controller';
|
||||||
|
import { SystemService } from './system.service';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
controllers: [SystemController],
|
||||||
|
providers: [SystemService],
|
||||||
|
})
|
||||||
|
export class SystemModule {}
|
||||||
53
backend/src/system/system.service.ts
Normal file
53
backend/src/system/system.service.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { InjectDataSource } from '@nestjs/typeorm';
|
||||||
|
import { DataSource } from 'typeorm';
|
||||||
|
|
||||||
|
export interface SystemHealthResponse {
|
||||||
|
status: 'ok' | 'error';
|
||||||
|
timestamp: string;
|
||||||
|
environment: string;
|
||||||
|
database: {
|
||||||
|
host: string;
|
||||||
|
port: number;
|
||||||
|
database: string;
|
||||||
|
user: string;
|
||||||
|
status: 'connected' | 'disconnected';
|
||||||
|
error?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class SystemService {
|
||||||
|
constructor(
|
||||||
|
private configService: ConfigService,
|
||||||
|
@InjectDataSource() private dataSource: DataSource,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async getHealth(): Promise<SystemHealthResponse> {
|
||||||
|
const dbHealth = await this.checkDatabase();
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: dbHealth.status === 'connected' ? 'ok' : 'error',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
environment: this.configService.get('NODE_ENV') || 'development',
|
||||||
|
database: dbHealth,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async checkDatabase() {
|
||||||
|
const config = {
|
||||||
|
host: this.configService.get('POSTGRES_HOST') || 'unknown',
|
||||||
|
port: parseInt(this.configService.get('POSTGRES_PORT')) || 5432,
|
||||||
|
database: this.configService.get('POSTGRES_DB') || 'unknown',
|
||||||
|
user: this.configService.get('POSTGRES_USER') || 'unknown',
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.dataSource.query('SELECT 1');
|
||||||
|
return { ...config, status: 'connected' as const };
|
||||||
|
} catch (error) {
|
||||||
|
return { ...config, status: 'disconnected' as const, error: error.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
74
commands.txt
74
commands.txt
@@ -1,74 +0,0 @@
|
|||||||
[BACKEND]
|
|
||||||
nest g resource
|
|
||||||
npm run start:dev
|
|
||||||
|
|
||||||
[FRONTEND]
|
|
||||||
npm run build : 배포용, TypeScript/ESLint 오류가 있으면 빌드 실패
|
|
||||||
npm run start
|
|
||||||
npm run dev : 개발모드, TypeScript 오류가 있어도 실행, 개발할 때는 편하게 작업, 배포할 때는 버그 없는 코드를 보장
|
|
||||||
|
|
||||||
[DOCKER]
|
|
||||||
docker ps : 실행 중인 컨테이너 조회
|
|
||||||
docker ps -a : 모든 컨테이너 조회
|
|
||||||
docker compose up -d : 백그라운드 compose 실행
|
|
||||||
docker compose down : 종료
|
|
||||||
docker-compose up -d redis : redis 서버 실행
|
|
||||||
docker-compose down -v : 볼륨 삭제 (데이터도 같이 삭제 / TypeORM이 테이블 구조는 자동 생성)
|
|
||||||
docker-compose rm -f frontend
|
|
||||||
|
|
||||||
docker-compose restart backend 서비스 재시작
|
|
||||||
docker-compose up -d backend 컨테이너가 없으면 새로 생성하고, 이미 있으면 재시작
|
|
||||||
|
|
||||||
dev 모드 docker 외부망 실행
|
|
||||||
docker-compose -f docker-compose.dev.yml up -d
|
|
||||||
docker-compose -f docker-compose.dev.yml down
|
|
||||||
docker-compose -f docker-compose.dev.yml restart backend
|
|
||||||
docker compose -f docker-compose.dev.yml build
|
|
||||||
docker compose -f docker-compose.dev.yml up --build
|
|
||||||
|
|
||||||
|
|
||||||
[캐시삭제]
|
|
||||||
.next 폴더는 Next.js의 빌드 캐시 폴더
|
|
||||||
삭제해도 안전한 이유:
|
|
||||||
1. 자동 재생성: 개발 서버(npm run dev)를 실행하면 자동으로 다시 생성됩니다
|
|
||||||
2. 캐시만 포함: 소스 코드가 아닌 컴파일된 결과물만 저장됩니다
|
|
||||||
3. 원본 보존: src/ 폴더의 실제 코드는 전혀 영향 없습니다
|
|
||||||
|
|
||||||
.next 폴더에 들어있는 것:
|
|
||||||
|
|
||||||
.next/
|
|
||||||
├── cache/ # Turbopack/Webpack 캐시
|
|
||||||
├── server/ # 서버 사이드 빌드 파일
|
|
||||||
├── static/ # 정적 에셋
|
|
||||||
└── types/ # 자동 생성된 타입 정의
|
|
||||||
|
|
||||||
삭제하는 이유:
|
|
||||||
- 캐시 손상: 패키지 설치 후 캐시가 오래된 버전 참조
|
|
||||||
- 빌드 오류: 이전 빌드 오류가 캐시에 남아있을 때
|
|
||||||
- 모듈 해결 문제: 새로 설치한 패키지(cmdk)를 인식 못할 때
|
|
||||||
|
|
||||||
|
|
||||||
[방법]
|
|
||||||
Windows에서 .next 폴더 삭제 방법:
|
|
||||||
|
|
||||||
방법 1: 파일 탐색기 (가장 쉬움)
|
|
||||||
|
|
||||||
1. C:\Users\COCOON\Desktop\repo14\repo14\next_nest_docker_template-main\frontend 폴더 열기
|
|
||||||
2. .next 폴더 찾기 (숨김 파일 보기 활성화 필요할 수 있음)
|
|
||||||
3. .next 폴더 우클릭 → 삭제
|
|
||||||
4. 휴지통 비우기 (선택사항)
|
|
||||||
|
|
||||||
방법 2: 명령 프롬프트 (CMD)
|
|
||||||
|
|
||||||
cd C:\Users\COCOON\Desktop\repo14\repo14\next_nest_docker_template-main\frontend
|
|
||||||
rmdir /s /q .next
|
|
||||||
|
|
||||||
방법 3: PowerShell
|
|
||||||
|
|
||||||
cd C:\Users\COCOON\Desktop\repo14\repo14\next_nest_docker_template-main\frontend
|
|
||||||
Remove-Item -Recurse -Force .next
|
|
||||||
|
|
||||||
방법 4: Git Bash (사용한 방법)
|
|
||||||
|
|
||||||
cd /c/Users/COCOON/Desktop/repo14/repo14/next_nest_docker_template-main/frontend
|
|
||||||
rm -rf .next
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
version: '3.8'
|
|
||||||
|
|
||||||
services:
|
|
||||||
frontend:
|
|
||||||
build:
|
|
||||||
context: ./frontend
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
container_name: nextjs-app
|
|
||||||
ports:
|
|
||||||
- "3000:3000"
|
|
||||||
environment:
|
|
||||||
- NODE_ENV=development
|
|
||||||
- NEXT_PUBLIC_API_URL=/backend/api
|
|
||||||
volumes:
|
|
||||||
- ./frontend:/app
|
|
||||||
- /app/node_modules
|
|
||||||
depends_on:
|
|
||||||
- backend
|
|
||||||
networks:
|
|
||||||
- app-network
|
|
||||||
|
|
||||||
backend:
|
|
||||||
build:
|
|
||||||
context: ./backend
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
container_name: nestjs-app
|
|
||||||
ports:
|
|
||||||
- "4000:4000"
|
|
||||||
environment:
|
|
||||||
- NODE_ENV=development
|
|
||||||
- DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB}
|
|
||||||
- POSTGRES_HOST=postgres
|
|
||||||
- REDIS_URL=redis://redis:6379
|
|
||||||
- REDIS_HOST=redis
|
|
||||||
volumes:
|
|
||||||
- ./backend:/app
|
|
||||||
- /app/node_modules
|
|
||||||
depends_on:
|
|
||||||
- postgres
|
|
||||||
- redis
|
|
||||||
networks:
|
|
||||||
- app-network
|
|
||||||
|
|
||||||
postgres:
|
|
||||||
image: postgres:15-alpine
|
|
||||||
container_name: postgres-db
|
|
||||||
ports:
|
|
||||||
- "5431:5432"
|
|
||||||
environment:
|
|
||||||
- POSTGRES_USER=${POSTGRES_USER}
|
|
||||||
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
|
|
||||||
- POSTGRES_DB=${POSTGRES_DB}
|
|
||||||
volumes:
|
|
||||||
- postgres_data:/var/lib/postgresql/data
|
|
||||||
networks:
|
|
||||||
- app-network
|
|
||||||
|
|
||||||
redis:
|
|
||||||
image: redis:7-alpine
|
|
||||||
container_name: redis-cache
|
|
||||||
ports:
|
|
||||||
- "6379:6379"
|
|
||||||
volumes:
|
|
||||||
- redis_data:/data
|
|
||||||
networks:
|
|
||||||
- app-network
|
|
||||||
|
|
||||||
# nginx:
|
|
||||||
# image: nginx:alpine
|
|
||||||
# container_name: nginx-proxy
|
|
||||||
# ports:
|
|
||||||
# - "80:80"
|
|
||||||
# - "443:443"
|
|
||||||
# volumes:
|
|
||||||
# - ./nginx/nginx.conf:/etc/nginx/nginx.conf
|
|
||||||
# - ./nginx/ssl:/etc/nginx/ssl
|
|
||||||
# depends_on:
|
|
||||||
# - frontend
|
|
||||||
# - backend
|
|
||||||
# networks:
|
|
||||||
# - app-network
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
postgres_data:
|
|
||||||
redis_data:
|
|
||||||
|
|
||||||
networks:
|
|
||||||
app-network:
|
|
||||||
driver: bridge
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
version: '3.8'
|
|
||||||
|
|
||||||
services:
|
|
||||||
frontend:
|
|
||||||
build:
|
|
||||||
context: ./frontend
|
|
||||||
dockerfile: Dockerfile.prod
|
|
||||||
container_name: nextjs-app-prod
|
|
||||||
environment:
|
|
||||||
- NODE_ENV=production
|
|
||||||
- NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL:-https://your-domain.com}
|
|
||||||
depends_on:
|
|
||||||
- backend
|
|
||||||
networks:
|
|
||||||
- app-network
|
|
||||||
|
|
||||||
backend:
|
|
||||||
build:
|
|
||||||
context: ./backend
|
|
||||||
dockerfile: Dockerfile.prod
|
|
||||||
container_name: nestjs-app-prod
|
|
||||||
environment:
|
|
||||||
- NODE_ENV=production
|
|
||||||
- DATABASE_URL=postgresql://${POSTGRES_USER:-prod_user}:${POSTGRES_PASSWORD:-CHANGE_THIS_STRONG_PASSWORD}@postgres:5432/${POSTGRES_DB:-prod_db}
|
|
||||||
- REDIS_URL=redis://redis:${REDIS_PORT:-6379}
|
|
||||||
- JWT_SECRET=${JWT_SECRET:-SUPER_SECURE_JWT_SECRET_AT_LEAST_32_CHARACTERS_LONG}
|
|
||||||
- JWT_EXPIRES_IN=${JWT_EXPIRES_IN:-1h}
|
|
||||||
- CORS_ORIGIN=${CORS_ORIGIN:-https://your-domain.com}
|
|
||||||
- RATE_LIMIT_WINDOW_MS=${RATE_LIMIT_WINDOW_MS:-900000}
|
|
||||||
- RATE_LIMIT_MAX_REQUESTS=${RATE_LIMIT_MAX_REQUESTS:-50}
|
|
||||||
depends_on:
|
|
||||||
- postgres
|
|
||||||
- redis
|
|
||||||
networks:
|
|
||||||
- app-network
|
|
||||||
|
|
||||||
postgres:
|
|
||||||
image: postgres:15-alpine
|
|
||||||
container_name: postgres-db-prod
|
|
||||||
environment:
|
|
||||||
- POSTGRES_USER=${POSTGRES_USER:-prod_user}
|
|
||||||
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-CHANGE_THIS_STRONG_PASSWORD}
|
|
||||||
- POSTGRES_DB=${POSTGRES_DB:-prod_db}
|
|
||||||
volumes:
|
|
||||||
- postgres_data:/var/lib/postgresql/data
|
|
||||||
networks:
|
|
||||||
- app-network
|
|
||||||
|
|
||||||
redis:
|
|
||||||
image: redis:7-alpine
|
|
||||||
container_name: redis-cache-prod
|
|
||||||
environment:
|
|
||||||
- REDIS_PASSWORD=${REDIS_PASSWORD:-}
|
|
||||||
volumes:
|
|
||||||
- redis_data:/data
|
|
||||||
networks:
|
|
||||||
- app-network
|
|
||||||
|
|
||||||
nginx:
|
|
||||||
image: nginx:alpine
|
|
||||||
container_name: nginx-proxy-prod
|
|
||||||
ports:
|
|
||||||
- "${NGINX_HTTP_PORT:-80}:80"
|
|
||||||
- "${NGINX_HTTPS_PORT:-443}:443"
|
|
||||||
volumes:
|
|
||||||
- ./nginx/nginx.conf:/etc/nginx/nginx.conf
|
|
||||||
- ./nginx/ssl:/etc/nginx/ssl
|
|
||||||
depends_on:
|
|
||||||
- frontend
|
|
||||||
- backend
|
|
||||||
networks:
|
|
||||||
- app-network
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
postgres_data:
|
|
||||||
redis_data:
|
|
||||||
|
|
||||||
networks:
|
|
||||||
app-network:
|
|
||||||
driver: bridge
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
version: "3.8"
|
|
||||||
|
|
||||||
services:
|
|
||||||
postgres:
|
|
||||||
image: postgres:15-alpine
|
|
||||||
container_name: postgres-db
|
|
||||||
ports:
|
|
||||||
- "${POSTGRES_PORT:-5432}:5432"
|
|
||||||
environment:
|
|
||||||
- POSTGRES_USER=${POSTGRES_USER:-postgres}
|
|
||||||
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-turbo123}
|
|
||||||
- POSTGRES_DB=${POSTGRES_DB:-genome_db}
|
|
||||||
volumes:
|
|
||||||
- postgres_data:/var/lib/postgresql/data
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "pg_isready", "-U", "${POSTGRES_USER:-user}"]
|
|
||||||
interval: 10s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 5
|
|
||||||
networks:
|
|
||||||
- app-network
|
|
||||||
|
|
||||||
redis:
|
|
||||||
image: redis:7-alpine
|
|
||||||
container_name: redis-cache
|
|
||||||
ports:
|
|
||||||
- "${REDIS_PORT:-6379}:6379"
|
|
||||||
volumes:
|
|
||||||
- redis_data:/data
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "redis-cli", "ping"]
|
|
||||||
interval: 10s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 5
|
|
||||||
networks:
|
|
||||||
- app-network
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
postgres_data:
|
|
||||||
redis_data:
|
|
||||||
|
|
||||||
networks:
|
|
||||||
app-network:
|
|
||||||
driver: bridge
|
|
||||||
14
frontend/.env
Normal file
14
frontend/.env
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
# ==============================================
|
||||||
|
# 로컬 개발용 (npm run dev)
|
||||||
|
# ==============================================
|
||||||
|
NODE_ENV=development
|
||||||
|
|
||||||
|
# 클라이언트 API URL
|
||||||
|
NEXT_PUBLIC_API_URL=http://localhost:4000
|
||||||
|
|
||||||
|
# 서버사이드 프록시용 (next.config.ts rewrites)
|
||||||
|
BACKEND_INTERNAL_URL=http://localhost:4000
|
||||||
|
|
||||||
|
# 앱 설정
|
||||||
|
NEXT_PUBLIC_APP_NAME=한우 유전능력 시스템
|
||||||
|
NEXT_TELEMETRY_DISABLED=1
|
||||||
14
frontend/.env.dev
Normal file
14
frontend/.env.dev
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
# ==============================================
|
||||||
|
# Docker 개발 환경용 (docker-compose)
|
||||||
|
# ==============================================
|
||||||
|
NODE_ENV=development
|
||||||
|
|
||||||
|
# 클라이언트 API URL (브라우저에서 호출)
|
||||||
|
NEXT_PUBLIC_API_URL=/backend/api
|
||||||
|
|
||||||
|
# 서버사이드 프록시용 (next.config.ts rewrites)
|
||||||
|
BACKEND_INTERNAL_URL=http://host.docker.internal:4000
|
||||||
|
|
||||||
|
# 앱 설정
|
||||||
|
NEXT_PUBLIC_APP_NAME=한우 유전능력 시스템
|
||||||
|
NEXT_TELEMETRY_DISABLED=1
|
||||||
14
frontend/.env.prod
Normal file
14
frontend/.env.prod
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
# ==============================================
|
||||||
|
# 프로덕션 환경용 (배포)
|
||||||
|
# ==============================================
|
||||||
|
NODE_ENV=production
|
||||||
|
|
||||||
|
# 클라이언트 API URL (브라우저에서 호출)
|
||||||
|
NEXT_PUBLIC_API_URL=/backend/api
|
||||||
|
|
||||||
|
# 서버사이드 프록시용 (next.config.ts rewrites)
|
||||||
|
BACKEND_INTERNAL_URL=http://host.docker.internal:4000
|
||||||
|
|
||||||
|
# 앱 설정
|
||||||
|
NEXT_PUBLIC_APP_NAME=한우 유전능력 시스템
|
||||||
|
NEXT_TELEMETRY_DISABLED=1
|
||||||
68
frontend/.gitignore
vendored
68
frontend/.gitignore
vendored
@@ -1,6 +1,6 @@
|
|||||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
# ==============================================
|
||||||
|
# Dependencies
|
||||||
# dependencies
|
# ==============================================
|
||||||
/node_modules
|
/node_modules
|
||||||
/.pnp
|
/.pnp
|
||||||
.pnp.*
|
.pnp.*
|
||||||
@@ -10,32 +10,68 @@
|
|||||||
!.yarn/releases
|
!.yarn/releases
|
||||||
!.yarn/versions
|
!.yarn/versions
|
||||||
|
|
||||||
# testing
|
# ==============================================
|
||||||
/coverage
|
# Build Output
|
||||||
|
# ==============================================
|
||||||
# next.js
|
|
||||||
/.next/
|
/.next/
|
||||||
/out/
|
/out/
|
||||||
|
|
||||||
# production
|
|
||||||
/build
|
/build
|
||||||
|
|
||||||
# misc
|
|
||||||
|
# ==============================================
|
||||||
|
# Testing
|
||||||
|
# ==============================================
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# ==============================================
|
||||||
|
# IDE & Editors
|
||||||
|
# ==============================================
|
||||||
|
/.idea
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/settings.json
|
||||||
|
!.vscode/tasks.json
|
||||||
|
!.vscode/launch.json
|
||||||
|
!.vscode/extensions.json
|
||||||
|
|
||||||
|
# ==============================================
|
||||||
|
# OS Files
|
||||||
|
# ==============================================
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
*.pem
|
*.pem
|
||||||
|
|
||||||
# debug
|
# ==============================================
|
||||||
|
# Debug Logs
|
||||||
|
# ==============================================
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
.pnpm-debug.log*
|
.pnpm-debug.log*
|
||||||
|
|
||||||
# env files (can opt-in for committing if needed)
|
# ==============================================
|
||||||
.env*
|
# Vercel
|
||||||
|
# ==============================================
|
||||||
# vercel
|
|
||||||
.vercel
|
.vercel
|
||||||
|
|
||||||
# typescript
|
# ==============================================
|
||||||
|
# TypeScript
|
||||||
|
# ==============================================
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
next-env.d.ts
|
next-env.d.ts
|
||||||
|
|
||||||
|
# ==============================================
|
||||||
|
# Misc
|
||||||
|
# ==============================================
|
||||||
|
.temp
|
||||||
|
.tmp
|
||||||
|
|
||||||
|
# ==============================================
|
||||||
|
# Environment Variables
|
||||||
|
# ==============================================
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.prod
|
||||||
|
# .env.dev는 허용 (배포용)
|
||||||
|
!.env.dev
|
||||||
|
!.env.example
|
||||||
|
|
||||||
|
|||||||
@@ -11,13 +11,11 @@ COPY package*.json ./
|
|||||||
# 의존성 설치
|
# 의존성 설치
|
||||||
RUN npm install
|
RUN npm install
|
||||||
|
|
||||||
# 소스 코드 복사
|
# 소스 코드 복사 (node_modules 제외 - .dockerignore)
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
# 빌드 시 필요한 환경 변수 설정
|
# 빌드 시 필요한 환경 변수 설정
|
||||||
ENV NEXT_TELEMETRY_DISABLED=1
|
ENV NEXT_TELEMETRY_DISABLED=1
|
||||||
ENV NODE_ENV=production
|
|
||||||
ENV NEXT_PUBLIC_API_URL=/backend/api
|
|
||||||
|
|
||||||
# Next.js 빌드
|
# Next.js 빌드
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|||||||
@@ -1,19 +1,22 @@
|
|||||||
import type { NextConfig } from "next";
|
import type { NextConfig } from "next";
|
||||||
|
|
||||||
// Next.js 핵심 설정 파일, Next.js가 시작할 때 이 파일을 찾아서 읽음
|
// 백엔드 URL 설정
|
||||||
// 여기에 Next.js 설정 옵션을 정의할 수 있음
|
// 로컬: http://localhost:4000
|
||||||
|
// Docker: http://host.docker.internal:4000
|
||||||
|
const BACKEND_URL = process.env.BACKEND_INTERNAL_URL || 'http://localhost:4000';
|
||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
eslint: {
|
eslint: {
|
||||||
ignoreDuringBuilds: true, // 빌드 시 ESLint warning 무시
|
ignoreDuringBuilds: true,
|
||||||
},
|
},
|
||||||
typescript: {
|
typescript: {
|
||||||
ignoreBuildErrors: true, // 빌드 시 TypeScript 에러 무시 (임시)
|
ignoreBuildErrors: true,
|
||||||
},
|
},
|
||||||
async rewrites() {
|
async rewrites() {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
source: '/backend/api/:path*', // /api가 붙은 모든 요청
|
source: '/backend/api/:path*',
|
||||||
destination: 'http://192.168.11.249:4000/:path*', // 백엔드 API로 요청
|
destination: `${BACKEND_URL}/:path*`,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
import type { NextConfig } from "next";
|
|
||||||
|
|
||||||
// Next.js 핵심 설정 파일, Next.js가 시작할 때 이 파일을 찾아서 읽음
|
|
||||||
// 여기에 Next.js 설정 옵션을 정의할 수 있음
|
|
||||||
const nextConfig: NextConfig = {
|
|
||||||
eslint: {
|
|
||||||
ignoreDuringBuilds: true, // 빌드 시 ESLint warning 무시
|
|
||||||
},
|
|
||||||
typescript: {
|
|
||||||
ignoreBuildErrors: true, // 빌드 시 TypeScript 에러 무시 (임시)
|
|
||||||
},
|
|
||||||
async rewrites() {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
source: '/backend/api/:path*', // /api가 붙은 모든 요청
|
|
||||||
destination: 'http://192.168.11.249:4000/:path*', // 백엔드 API로 요청
|
|
||||||
},
|
|
||||||
];
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default nextConfig;
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
events {
|
|
||||||
worker_connections 1024;
|
|
||||||
}
|
|
||||||
|
|
||||||
http {
|
|
||||||
upstream frontend {
|
|
||||||
server frontend:3000;
|
|
||||||
}
|
|
||||||
|
|
||||||
upstream backend {
|
|
||||||
server backend:4000;
|
|
||||||
}
|
|
||||||
|
|
||||||
server {
|
|
||||||
listen 80;
|
|
||||||
server_name _;
|
|
||||||
|
|
||||||
# Frontend routes
|
|
||||||
location / {
|
|
||||||
proxy_pass http://frontend;
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
}
|
|
||||||
|
|
||||||
# Backend API routes
|
|
||||||
location /api/ {
|
|
||||||
proxy_pass http://backend/;
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
}
|
|
||||||
|
|
||||||
# WebSocket support for Next.js hot reload
|
|
||||||
location /_next/webpack-hmr {
|
|
||||||
proxy_pass http://frontend;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
|
||||||
proxy_set_header Connection "upgrade";
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
6
package-lock.json
generated
6
package-lock.json
generated
@@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "next_nest_docker_template-main",
|
|
||||||
"lockfileVersion": 3,
|
|
||||||
"requires": true,
|
|
||||||
"packages": {}
|
|
||||||
}
|
|
||||||
102
postgres/.env
102
postgres/.env
@@ -1,102 +0,0 @@
|
|||||||
# ==============================================
|
|
||||||
# DEVELOPMENT ENVIRONMENT VARIABLES
|
|
||||||
# ==============================================
|
|
||||||
# Copy this file to .env.local for local development
|
|
||||||
# DO NOT commit sensitive values to version control
|
|
||||||
|
|
||||||
# ==============================================
|
|
||||||
# DATABASE CONFIGURATION
|
|
||||||
# ==============================================
|
|
||||||
DATABASE_URL=postgresql://user:password@localhost:5431/genome_db
|
|
||||||
POSTGRES_HOST=localhost
|
|
||||||
POSTGRES_USER=postgres
|
|
||||||
POSTGRES_PASSWORD=turbo123
|
|
||||||
POSTGRES_DB=postgres
|
|
||||||
POSTGRES_PORT=5431
|
|
||||||
POSTGRES_SYNCHRONIZE=true
|
|
||||||
POSTGRES_LOGGING=true
|
|
||||||
|
|
||||||
# ==============================================
|
|
||||||
# REDIS CONFIGURATION
|
|
||||||
# ==============================================
|
|
||||||
REDIS_URL=redis://localhost:6379
|
|
||||||
REDIS_HOST=localhost
|
|
||||||
REDIS_PORT=6379
|
|
||||||
|
|
||||||
# ==============================================
|
|
||||||
# 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.249:3000,http://123.143.174.11:5243
|
|
||||||
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_HOST=smtp.gmail.com
|
|
||||||
SMTP_PORT=587
|
|
||||||
SMTP_SECURE=false
|
|
||||||
SMTP_USER=your-email@gmail.com
|
|
||||||
SMTP_PASS=your-app-password
|
|
||||||
FROM_EMAIL=noreply@yourdomain.com
|
|
||||||
|
|
||||||
# ==============================================
|
|
||||||
# LOGGING
|
|
||||||
# ==============================================
|
|
||||||
LOG_LEVEL=debug
|
|
||||||
LOG_FORMAT=dev
|
|
||||||
LOG_FILE_ENABLED=true
|
|
||||||
LOG_FILE_PATH=./logs
|
|
||||||
|
|
||||||
# ==============================================
|
|
||||||
# EXTERNAL SERVICES
|
|
||||||
# ==============================================
|
|
||||||
# AWS_ACCESS_KEY_ID=your-aws-access-key
|
|
||||||
# AWS_SECRET_ACCESS_KEY=your-aws-secret
|
|
||||||
# AWS_REGION=us-east-1
|
|
||||||
# AWS_S3_BUCKET=your-bucket-name
|
|
||||||
|
|
||||||
# ==============================================
|
|
||||||
# MONITORING
|
|
||||||
# ==============================================
|
|
||||||
# SENTRY_DSN=your-sentry-dsn
|
|
||||||
# HEALTH_CHECK_ENABLED=true
|
|
||||||
|
|
||||||
# ==============================================
|
|
||||||
# FRONTEND CONFIGURATION
|
|
||||||
# ==============================================
|
|
||||||
FRONTEND_PORT=3000
|
|
||||||
NEXT_PUBLIC_API_URL=http://192.168.11.249:4000
|
|
||||||
|
|
||||||
# ==============================================
|
|
||||||
# NGINX CONFIGURATION
|
|
||||||
# ==============================================
|
|
||||||
NGINX_HTTP_PORT=80
|
|
||||||
NGINX_HTTPS_PORT=443
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
version: "1.0"
|
|
||||||
# 개발 편의를 위한 옵션 폴더
|
|
||||||
# 개발 편의성 (컨테이너 재빌드 안 함)
|
|
||||||
# DB PostgreSQL, Redis만 Docker 컨테이너로 실행
|
|
||||||
# Frontend/Backend는 로컬에서 직접 실행
|
|
||||||
services:
|
|
||||||
postgres:
|
|
||||||
image: postgres:15-alpine
|
|
||||||
container_name: postgres-db
|
|
||||||
ports:
|
|
||||||
- "${POSTGRES_PORT:-5431}:5432"
|
|
||||||
environment:
|
|
||||||
- POSTGRES_USER=${POSTGRES_USER:-postgres}
|
|
||||||
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-turbo123}
|
|
||||||
- POSTGRES_DB=${POSTGRES_DB:-genomic}
|
|
||||||
volumes:
|
|
||||||
- postgres_data:/var/lib/postgresql/data
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "pg_isready", "-U", "${POSTGRES_USER:-user}"]
|
|
||||||
interval: 10s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 5
|
|
||||||
networks:
|
|
||||||
- app-network
|
|
||||||
|
|
||||||
redis:
|
|
||||||
image: redis:7-alpine
|
|
||||||
container_name: redis-cache
|
|
||||||
ports:
|
|
||||||
- "${REDIS_PORT:-6379}:6379"
|
|
||||||
volumes:
|
|
||||||
- redis_data:/data
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "redis-cli", "ping"]
|
|
||||||
interval: 10s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 5
|
|
||||||
networks:
|
|
||||||
- app-network
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
postgres_data:
|
|
||||||
redis_data:
|
|
||||||
|
|
||||||
networks:
|
|
||||||
app-network:
|
|
||||||
driver: bridge
|
|
||||||
Reference in New Issue
Block a user