/** * 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); // JSON body parser 미리 적용 (마이그레이션용) app.use(express.json()); // 임시 분석 테스트 엔드포인트 - 실행 후 삭제! app.get('/api/test-analysis', async (req, res) => { try { const { getDb } = require('./dbPool'); const db = await getDb(); // 수정된 COALESCE 로직 테스트 (tasks 우선) const [results] = await db.query(` SELECT dwr.id, w.worker_name, dwr.report_date, dwr.work_type_id as original_work_type_id, COALESCE( CASE WHEN t.task_id IS NOT NULL THEN t.work_type_id ELSE NULL END, wt.id ) as resolved_work_type_id, COALESCE( CASE WHEN t.task_id IS NOT NULL THEN wt2.name ELSE NULL END, wt.name ) as work_type_name, t.task_name, wt.name as direct_match_work_type, wt2.name as task_work_type FROM daily_work_reports dwr LEFT JOIN workers w ON dwr.worker_id = w.worker_id LEFT JOIN work_types wt ON dwr.work_type_id = wt.id LEFT JOIN tasks t ON dwr.work_type_id = t.task_id LEFT JOIN work_types wt2 ON t.work_type_id = wt2.id WHERE w.worker_name LIKE '%조승민%' OR w.worker_name LIKE '%최광욱%' ORDER BY dwr.report_date DESC LIMIT 20 `); res.json({ success: true, message: 'tasks 테이블 우선 조회 결과', data: results.map(r => ({ id: r.id, worker: r.worker_name, date: r.report_date, original_id: r.original_work_type_id, resolved_work_type: r.work_type_name, task: r.task_name, note: `원래 ID ${r.original_work_type_id} → ${r.work_type_name}` })) }); } catch (error) { console.error('테스트 실패:', error); res.status(500).json({ success: false, error: error.message }); } }); // 임시 진단 엔드포인트 - 실행 후 삭제! app.get('/api/diagnose-work-type-id', async (req, res) => { try { const { getDb } = require('./dbPool'); const db = await getDb(); // 1. 전체 작업보고서 현황 const [totalStats] = await db.query(` SELECT COUNT(*) as total_reports, COUNT(tbm_assignment_id) as tbm_reports, COUNT(CASE WHEN tbm_assignment_id IS NULL THEN 1 END) as non_tbm_reports FROM daily_work_reports `); // 2. work_type_id 값 분포 (상위 20개) const [workTypeDistribution] = await db.query(` SELECT dwr.work_type_id, COUNT(*) as count, wt.name as if_work_type, t.task_name as if_task, wt2.name as task_work_type FROM daily_work_reports dwr LEFT JOIN work_types wt ON dwr.work_type_id = wt.id LEFT JOIN tasks t ON dwr.work_type_id = t.task_id LEFT JOIN work_types wt2 ON t.work_type_id = wt2.id GROUP BY dwr.work_type_id ORDER BY count DESC LIMIT 20 `); // 3. 특정 작업자 데이터 확인 (조승민, 최광욱) const [workerSamples] = await db.query(` SELECT dwr.id, w.worker_name, dwr.work_type_id, dwr.tbm_assignment_id, wt.name as direct_work_type, t.task_name, wt2.name as task_work_type, dwr.report_date FROM daily_work_reports dwr LEFT JOIN workers w ON dwr.worker_id = w.worker_id LEFT JOIN work_types wt ON dwr.work_type_id = wt.id LEFT JOIN tasks t ON dwr.work_type_id = t.task_id LEFT JOIN work_types wt2 ON t.work_type_id = wt2.id WHERE w.worker_name LIKE '%조승민%' OR w.worker_name LIKE '%최광욱%' ORDER BY dwr.report_date DESC LIMIT 20 `); res.json({ success: true, data: { total_stats: totalStats[0], work_type_distribution: workTypeDistribution, worker_samples: workerSamples } }); } catch (error) { console.error('진단 실패:', error); res.status(500).json({ success: false, error: error.message }); } }); // 임시 마이그레이션 엔드포인트 (인증 없이 실행) - 실행 후 삭제! app.post('/api/migrate-work-type-id', async (req, res) => { try { const { getDb } = require('./dbPool'); const db = await getDb(); console.log('🔄 TBM 기반 작업보고서 work_type_id 수정 시작...'); // 1. 수정 대상 확인 const [checkResult] = await db.query(` SELECT dwr.id, dwr.work_type_id as current_work_type_id, ta.task_id as correct_task_id, w.worker_name, dwr.report_date FROM daily_work_reports dwr INNER JOIN tbm_team_assignments ta ON dwr.tbm_assignment_id = ta.assignment_id INNER JOIN workers w ON dwr.worker_id = w.worker_id WHERE dwr.tbm_assignment_id IS NOT NULL AND ta.task_id IS NOT NULL AND dwr.work_type_id != ta.task_id ORDER BY dwr.report_date DESC `); console.log('📊 수정 대상:', checkResult.length, '개 레코드'); if (checkResult.length === 0) { return res.json({ success: true, message: '수정할 데이터가 없습니다.', data: { affected_rows: 0 } }); } // 2. 업데이트 실행 const [updateResult] = await db.query(` UPDATE daily_work_reports dwr INNER JOIN tbm_team_assignments ta ON dwr.tbm_assignment_id = ta.assignment_id SET dwr.work_type_id = ta.task_id WHERE dwr.tbm_assignment_id IS NOT NULL AND ta.task_id IS NOT NULL AND dwr.work_type_id != ta.task_id `); console.log('✅ 업데이트 완료:', updateResult.affectedRows, '개 레코드 수정됨'); // 3. 수정 후 확인 const [samples] = await db.query(` SELECT dwr.id, dwr.work_type_id, t.task_name, wt.name as work_type_name, w.worker_name, dwr.report_date FROM daily_work_reports dwr INNER JOIN tbm_team_assignments ta ON dwr.tbm_assignment_id = ta.assignment_id LEFT JOIN tasks t ON dwr.work_type_id = t.task_id LEFT JOIN work_types wt ON t.work_type_id = wt.id LEFT JOIN workers w ON dwr.worker_id = w.worker_id WHERE dwr.tbm_assignment_id IS NOT NULL ORDER BY dwr.report_date DESC LIMIT 10 `); res.json({ success: true, message: updateResult.affectedRows + '개 레코드가 수정되었습니다.', data: { affected_rows: updateResult.affectedRows, before_count: checkResult.length, samples: samples.map(s => ({ id: s.id, worker: s.worker_name, date: s.report_date, task: s.task_name, work_type: s.work_type_name })) } }); } catch (error) { console.error('마이그레이션 실패:', error); res.status(500).json({ success: false, error: '마이그레이션 실패: ' + error.message }); } }); // 미들웨어 설정 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 }); console.log(`\n🚀 서버가 포트 ${PORT}에서 실행 중입니다.`); console.log(`📚 API 문서: http://localhost:${PORT}/api-docs\n`); }); // Graceful Shutdown const gracefulShutdown = (signal) => { logger.info(`${signal} 신호 수신 - 서버 종료 시작`); console.log(`\n🛑 ${signal} 신호를 받았습니다. 서버를 종료합니다...`); server.close(async () => { logger.info('HTTP 서버 종료 완료'); console.log('✅ 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('캐시 시스템 초기화 완료'); console.log('✅ 캐시 시스템 초기화 완료'); } } catch (error) { logger.warn('캐시 시스템 초기화 실패 - 계속 진행', { error: error.message }); console.warn('⚠️ 캐시 시스템 초기화 실패:', error.message); } })(); module.exports = app;