Files
tk-factory-services/system1-factory/api/index.js
Hyungi Ahn 7fd646e9ba feat: 실시간 알림 시스템 (Web Push + 알림 벨 + 서비스간 알림 연동)
- Phase 1: 모든 서비스 헤더에 알림 벨 UI 추가 (notification-bell.js)
- Phase 2: VAPID Web Push 구독/전송 (push-sw.js, pushSubscription API)
- Phase 3: 내부 알림 API + notifyHelper로 서비스간 알림 연동
  - tksafety: 출입 승인/반려, 안전교육 완료, 방문자 체크인
  - tkpurchase: 일용공 신청, 작업보고서 제출
  - system2-report: 신고 접수/확인/처리완료
- Phase 4: 30일 이상 알림 자동 정리 cron, Redis 캐싱
- CORS에 tkuser/tkpurchase/tksafety 서브도메인 추가
- HTML cache busting 버전 갱신

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 15:01:44 +09:00

141 lines
3.8 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
});
});
// 서버 시작
const 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} 신호 수신 - 서버 종료 시작`);
server.close(async () => {
logger.info('HTTP 서버 종료 완료');
// 리소스 정리
try {
// DB 연결 종료는 각 요청에서 pool을 사용하므로 불필요
// Redis 종료 (사용 중인 경우)
if (cache.redis) {
await cache.redis.quit();
logger.info('캐시 시스템 종료 완료');
}
} catch (error) {
logger.error('리소스 정리 중 오류 발생', { error: error.message });
}
process.exit(0);
});
// 30초 후 강제 종료
setTimeout(() => {
logger.error('강제 종료 - 정상 종료 시간 초과');
console.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);
}
})();
// 오래된 알림 정리 cron (매일 03:00 KST)
(function scheduleNotificationCleanup() {
const notificationModel = require('./models/notificationModel');
function runCleanup() {
const now = new Date();
const kstHour = (now.getUTCHours() + 9) % 24;
if (kstHour === 3 && now.getMinutes() < 1) {
notificationModel.deleteOld(30).then(count => {
if (count > 0) logger.info(`오래된 알림 ${count}건 정리 완료`);
}).catch(err => {
logger.error('알림 정리 실패:', { error: err.message });
});
}
}
setInterval(runCleanup, 60000); // 1분마다 확인
})();
module.exports = app;