// src/app.js // AI Assistant API 서버 메인 엔트리포인트 require('dotenv').config(); const express = require('express'); const cors = require('cors'); const helmet = require('helmet'); const compression = require('compression'); const rateLimit = require('express-rate-limit'); const swaggerUi = require('swagger-ui-express'); const swaggerSpec = require('./config/swagger.config'); const logger = require('./config/logger.config'); const { sequelize } = require('./models'); const routes = require('./routes'); const errorHandler = require('./middlewares/error-handler.middleware'); const app = express(); // VEXPLOR 내장 시 backend-node가 이 포트로 프록시하므로 기본 3100 사용 const PORT = process.env.PORT || 3100; // =========================================== // 미들웨어 설정 // =========================================== // Trust proxy (Docker/Nginx 환경) app.set('trust proxy', 1); // CORS 설정 (helmet보다 먼저 설정) app.use(cors({ origin: true, // 모든 origin 허용 credentials: true, methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'], allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With'], })); // Preflight 요청 처리 app.options('*', cors()); // 보안 헤더 (CORS 이후에 설정) app.use(helmet({ crossOriginResourcePolicy: { policy: 'cross-origin' }, crossOriginOpenerPolicy: { policy: 'unsafe-none' }, })); // 요청 본문 파싱 app.use(express.json({ limit: '10mb' })); app.use(express.urlencoded({ extended: true })); // 압축 app.use(compression()); // Rate Limiting (전역) const limiter = rateLimit({ windowMs: parseInt(process.env.RATE_LIMIT_WINDOW_MS, 10) || 60000, max: parseInt(process.env.RATE_LIMIT_MAX_REQUESTS, 10) || 100, message: { success: false, error: { code: 'RATE_LIMIT_EXCEEDED', message: '요청 한도를 초과했습니다. 잠시 후 다시 시도해주세요.', }, }, standardHeaders: true, legacyHeaders: false, }); app.use(limiter); // 요청 로깅 app.use((req, res, next) => { const start = Date.now(); res.on('finish', () => { const duration = Date.now() - start; logger.info(`${req.method} ${req.originalUrl} ${res.statusCode} ${duration}ms`); }); next(); }); // =========================================== // 헬스 체크 // =========================================== app.get('/health', (req, res) => { res.json({ success: true, data: { status: 'healthy', timestamp: new Date().toISOString(), uptime: process.uptime(), }, }); }); // =========================================== // Swagger API 문서 // =========================================== app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec, { explorer: true, customCss: '.swagger-ui .topbar { display: none }', customSiteTitle: 'AI Assistant API 문서', swaggerOptions: { persistAuthorization: true, displayRequestDuration: true, }, })); // Swagger JSON app.get('/api-docs.json', (req, res) => { res.setHeader('Content-Type', 'application/json'); res.send(swaggerSpec); }); // =========================================== // API 라우트 // =========================================== app.use('/api/v1', routes); // =========================================== // 404 처리 // =========================================== app.use((req, res) => { res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: `요청한 리소스를 찾을 수 없습니다: ${req.method} ${req.originalUrl}`, }, }); }); // =========================================== // 에러 핸들러 // =========================================== app.use(errorHandler); // =========================================== // 서버 시작 // =========================================== async function startServer() { try { // 데이터베이스 연결 await sequelize.authenticate(); logger.info('✅ 데이터베이스 연결 성공'); // 테이블 동기화 (테이블이 없으면 생성) await sequelize.sync(); logger.info('✅ 데이터베이스 스키마 동기화 완료'); // 초기 데이터 설정 (관리자 계정, LLM 프로바이더) const initService = require('./services/init.service'); await initService.initialize(); // 서버 시작 app.listen(PORT, () => { logger.info(`🚀 AI Assistant API 서버가 포트 ${PORT}에서 실행 중입니다`); logger.info(`📚 API 문서 (Swagger): http://localhost:${PORT}/api-docs`); logger.info(`📚 API 엔드포인트: http://localhost:${PORT}/api/v1`); }); } catch (error) { logger.error('❌ 서버 시작 실패:', error); process.exit(1); } } // 프로세스 종료 처리 process.on('SIGTERM', async () => { logger.info('SIGTERM 신호 수신, 서버 종료 중...'); await sequelize.close(); process.exit(0); }); process.on('SIGINT', async () => { logger.info('SIGINT 신호 수신, 서버 종료 중...'); await sequelize.close(); process.exit(0); }); startServer(); module.exports = app;