20260330_add_proxy_input_fields.sql이 startup 마이그레이션에 등록 안 되어 tbm_sessions.is_proxy_input 컬럼 없어서 500 에러. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
154 lines
4.4 KiB
JavaScript
154 lines
4.4 KiB
JavaScript
/**
|
|
* 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'];
|
|
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;
|