/** * TK-FB-Project API Server * * 작업 관리 시스템의 메인 서버 애플리케이션 * * @author TK-FB-Project * @since 2025-12-11 * @version 2.2.0 */ require('dotenv').config(); const express = require('express'); const setupMiddlewares = require('./config/middleware'); const setupRoutes = require('./config/routes'); const { errorHandler } = require('./middlewares/errorHandler'); const logger = require('./utils/logger'); const cache = require('./utils/cache'); const app = express(); const PORT = process.env.PORT || 20005; // Trust proxy for accurate IP addresses app.set('trust proxy', 1); // 미들웨어 설정 setupMiddlewares(app); // 라우트 설정 setupRoutes(app); // 에러 핸들러 (모든 라우트 이후에 위치) app.use(errorHandler); // 404 핸들러 app.use((req, res) => { logger.warn('404 Not Found', { url: req.originalUrl, method: req.method }); res.status(404).json({ success: false, error: '요청하신 경로를 찾을 수 없습니다', path: req.originalUrl }); }); // Startup: 마이그레이션 후 서버 시작 async function runStartupMigrations() { try { const { getDb } = require('./dbPool'); const fs = require('fs'); const path = require('path'); const db = await getDb(); const migrationFiles = ['20260326_schedule_extensions.sql', '20260330_add_proxy_input_fields.sql', '20260330_create_monthly_work_confirmations.sql', '20260331_fix_deduct_days_precision.sql']; for (const file of migrationFiles) { const sqlPath = path.join(__dirname, 'db', 'migrations', file); if (!fs.existsSync(sqlPath)) continue; const sql = fs.readFileSync(sqlPath, 'utf8'); const stmts = sql.split(';').map(s => s.trim()).filter(s => s.length > 0); for (const stmt of stmts) { try { await db.query(stmt); } catch (err) { if (['ER_DUP_FIELDNAME', 'ER_TABLE_EXISTS_ERROR', 'ER_DUP_KEYNAME', 'ER_FK_DUP_NAME'].includes(err.code)) { // 이미 적용됨 — 무시 } else if (err.code === 'ER_NO_REFERENCED_ROW_2' || err.message.includes('Cannot add foreign key')) { // product_types 테이블 미존재 (tkuser 미시작) — skip, 재시작 시 retry logger.warn(`Migration FK skip (dependency not ready): ${err.message}`); } else { throw err; } } } logger.info(`[system1] Migration ${file} completed`); } } catch (err) { logger.error('Migration error:', err.message); } } let server; runStartupMigrations().then(() => { server = app.listen(PORT, () => { logger.info(`서버 시작 완료`, { port: PORT, env: process.env.NODE_ENV || 'development', nodeVersion: process.version }); }); }); // Graceful Shutdown const gracefulShutdown = (signal) => { logger.info(`${signal} 신호 수신 - 서버 종료 시작`); if (!server) return process.exit(0); server.close(async () => { logger.info('HTTP 서버 종료 완료'); try { if (cache.redis) { await cache.redis.quit(); logger.info('캐시 시스템 종료 완료'); } } catch (error) { logger.error('리소스 정리 중 오류 발생', { error: error.message }); } process.exit(0); }); setTimeout(() => { logger.error('강제 종료 - 정상 종료 시간 초과'); process.exit(1); }, 30000); }; process.on('SIGTERM', () => gracefulShutdown('SIGTERM')); process.on('SIGINT', () => gracefulShutdown('SIGINT')); // 처리되지 않은 Promise 거부 process.on('unhandledRejection', (reason, promise) => { logger.error('처리되지 않은 Promise 거부', { reason: reason, promise: promise }); console.error(' 처리되지 않은 Promise 거부:', reason); if (process.env.NODE_ENV === 'development') { process.exit(1); } }); // 처리되지 않은 예외 process.on('uncaughtException', (error) => { logger.error('처리되지 않은 예외', { error: error.message, stack: error.stack }); console.error(' 처리되지 않은 예외:', error); gracefulShutdown('UNCAUGHT_EXCEPTION'); }); // 캐시 시스템 초기화 (선택적) (async () => { try { if (cache.initRedis) { await cache.initRedis(); logger.info('캐시 시스템 초기화 완료'); } } catch (error) { logger.warn('캐시 시스템 초기화 실패 - 계속 진행', { error: error.message }); console.warn(' 캐시 시스템 초기화 실패:', error.message); } })(); module.exports = app;