diff --git a/DEV_LOG.md b/DEV_LOG.md index 822d607..9f4ede8 100644 --- a/DEV_LOG.md +++ b/DEV_LOG.md @@ -8,6 +8,25 @@ - **[UPDATE] Model Layer**: Raw SQL 쿼리를 Controller에서 Model(`models/WorkAnalysis.js`)로 이동. `getProjectWorkTypeRawData` 메서드 추가. - **[CLEANUP] Controller**: `getProjectWorkTypeAnalysis` 메서드가 Service를 호출하도록 단순화. +### 🐛 심각한 버그 수정 및 시스템 정상화 (2025-12-19) +**개요**: 로그인 500 에러 및 대시보드 데이터 미표시 문제 해결. + +1. **DB 정상화 (Login 500 Fix)** + - **원인**: 초기화된 DB(Empty) 및 테이블명 대소문자 불일치(`Users` vs `users`). + - **조치**: `hyungi.sql` 복원 후, 컨벤션에 맞춰 테이블명 일괄 변경(`Users`→`users`, `Projects`→`projects`, `Workers`→`workers`, `Tasks`→`tasks`). + - **코드 수정**: `userModel.js`, `authController.js` 등 관련 코드의 테이블 참조 수정. + +2. **프로젝트 조회 오류 수정 (Project API 500 Fix)** + - **원인**: 구버전 스키마 복원으로 인한 `projects` 테이블 컬럼 부족(`is_active`, `project_status`, `completed_date`). + - **조치**: 마이그레이션 실행하여 누락된 컬럼 추가. + +3. **대시보드 작업자 미표시 수정** + - **원인 1 (Data)**: `workers` 테이블 내 `status` 값이 유효하지 않음(`..`). → `active`로 일괄 수정. + - **원인 2 (Logic)**: `INNER JOIN` 사용으로 통계가 없는 작업자 누락. → `LEFT JOIN`으로 쿼리 개선(`MonthlyStatusModel.js`). + +4. **테스트 계정 생성** + - `tester` / `000000` 관리자(Leader) 계정 생성. + --- ## 🛡보안 및 검토 리포트 (History) diff --git a/api.hyungi.net/config/routes.js b/api.hyungi.net/config/routes.js index 0229776..d1701f2 100644 --- a/api.hyungi.net/config/routes.js +++ b/api.hyungi.net/config/routes.js @@ -69,7 +69,7 @@ function setupRoutes(app) { app.use('/api/setup', setupRoutes); // Health check - app.use('/api', healthRoutes); + app.use('/api/health', healthRoutes); // 일반 API에 속도 제한 적용 app.use('/api/', apiLimiter); diff --git a/api.hyungi.net/controllers/authController.js b/api.hyungi.net/controllers/authController.js index 0341059..e8db790 100644 --- a/api.hyungi.net/controllers/authController.js +++ b/api.hyungi.net/controllers/authController.js @@ -30,29 +30,29 @@ const login = asyncHandler(async (req, res) => { }); // ✅ 사용자 등록 기능 추가 -exports.register = async (req, res) => { +const register = async (req, res) => { try { const { username, password, name, access_level, worker_id } = req.body; const db = await getDb(); // 필수 필드 검증 if (!username || !password || !name || !access_level) { - return res.status(400).json({ - success: false, - error: '필수 정보가 누락되었습니다.' + return res.status(400).json({ + success: false, + error: '필수 정보가 누락되었습니다.' }); } // 중복 아이디 확인 const [existing] = await db.query( - 'SELECT user_id FROM Users WHERE username = ?', + 'SELECT user_id FROM users WHERE username = ?', [username] ); if (existing.length > 0) { - return res.status(409).json({ - success: false, - error: '이미 존재하는 아이디입니다.' + return res.status(409).json({ + success: false, + error: '이미 존재하는 아이디입니다.' }); } @@ -71,17 +71,17 @@ exports.register = async (req, res) => { // 사용자 등록 const [result] = await db.query( - `INSERT INTO Users (username, password, name, role, access_level, worker_id) + `INSERT INTO users (username, password, name, role, access_level, worker_id) VALUES (?, ?, ?, ?, ?, ?)`, [username, hashedPassword, name, role, access_level, worker_id] ); console.log('[사용자 등록 성공]', username); - - return res.status(201).json({ - success: true, + + return res.status(201).json({ + success: true, message: '사용자 등록이 완료되었습니다.', - user_id: result.insertId + user_id: result.insertId }); } catch (err) { @@ -95,32 +95,32 @@ exports.register = async (req, res) => { }; // ✅ 사용자 삭제 기능 추가 -exports.deleteUser = async (req, res) => { +const deleteUser = async (req, res) => { try { const { id } = req.params; const db = await getDb(); // 사용자 존재 확인 const [user] = await db.query( - 'SELECT user_id FROM Users WHERE user_id = ?', + 'SELECT user_id FROM users WHERE user_id = ?', [id] ); if (user.length === 0) { - return res.status(404).json({ - success: false, - error: '해당 사용자를 찾을 수 없습니다.' + return res.status(404).json({ + success: false, + error: '해당 사용자를 찾을 수 없습니다.' }); } // 사용자 삭제 - await db.query('DELETE FROM Users WHERE user_id = ?', [id]); + await db.query('DELETE FROM users WHERE user_id = ?', [id]); console.log('[사용자 삭제 성공] ID:', id); - - return res.status(200).json({ - success: true, - message: '사용자가 삭제되었습니다.' + + return res.status(200).json({ + success: true, + message: '사용자가 삭제되었습니다.' }); } catch (err) { @@ -134,17 +134,17 @@ exports.deleteUser = async (req, res) => { }; // 모든 사용자 목록 조회 -exports.getAllUsers = async (req, res) => { +const getAllUsers = async (req, res) => { try { const db = await getDb(); - + // 비밀번호 제외하고 조회 const [rows] = await db.query( `SELECT user_id, username, name, role, access_level, worker_id, created_at - FROM Users + FROM users ORDER BY created_at DESC` ); - + res.status(200).json(rows); } catch (err) { console.error('[사용자 목록 조회 실패]', err); @@ -153,5 +153,8 @@ exports.getAllUsers = async (req, res) => { }; module.exports = { - login + login, + register, + deleteUser, + getAllUsers }; \ No newline at end of file diff --git a/api.hyungi.net/models/monthlyStatusModel.js b/api.hyungi.net/models/monthlyStatusModel.js index 7f97e07..d7e86e4 100644 --- a/api.hyungi.net/models/monthlyStatusModel.js +++ b/api.hyungi.net/models/monthlyStatusModel.js @@ -7,7 +7,7 @@ class MonthlyStatusModel { // 월별 일자별 요약 조회 (캘린더용) static async getMonthlySummary(year, month) { const db = await getDb(); - + try { const [rows] = await db.execute(` SELECT @@ -32,24 +32,24 @@ class MonthlyStatusModel { WHERE year = ? AND month = ? ORDER BY date ASC `, [year, month]); - + return rows; } catch (error) { console.error('월별 요약 조회 오류:', error); throw error; } } - + // 특정 날짜의 작업자별 상태 조회 (모달용) // ✅ 리팩토링: 집계 테이블 대신 daily_work_reports에서 직접 조회 (중복 문제 완전 해결) static async getDailyWorkerStatus(date) { const db = await getDb(); - + try { // daily_work_reports에서 직접 집계하여 조회 (중복 없음 보장) const [rows] = await db.query(` SELECT - dwr.worker_id, + w.worker_id, w.worker_name, w.job_type, YEAR(?) as year, @@ -58,7 +58,7 @@ class MonthlyStatusModel { COALESCE(SUM(dwr.work_hours), 0) as total_work_hours, COALESCE(SUM(CASE WHEN dwr.project_id != 13 THEN dwr.work_hours ELSE 0 END), 0) as actual_work_hours, COALESCE(SUM(CASE WHEN dwr.project_id = 13 THEN dwr.work_hours ELSE 0 END), 0) as vacation_hours, - COUNT(*) as total_work_count, + COUNT(dwr.id) as total_work_count, COUNT(CASE WHEN dwr.project_id != 13 AND dwr.work_status_id != 2 THEN 1 END) as regular_work_count, COUNT(CASE WHEN dwr.work_status_id = 2 THEN 1 END) as error_work_count, CASE @@ -76,24 +76,24 @@ class MonthlyStatusModel { ELSE 0 END as has_issues, MAX(dwr.created_at) as last_updated - FROM daily_work_reports dwr - JOIN workers w ON dwr.worker_id = w.worker_id - WHERE dwr.report_date = ? - GROUP BY dwr.worker_id, w.worker_name, w.job_type + FROM workers w + LEFT JOIN daily_work_reports dwr ON w.worker_id = dwr.worker_id AND dwr.report_date = ? + WHERE w.status = 'active' + GROUP BY w.worker_id, w.worker_name, w.job_type ORDER BY w.worker_name ASC `, [date, date, date, date]); - + return rows; } catch (error) { console.error('일별 작업자 상태 조회 오류:', error); throw error; } } - + // 월별 집계 데이터 강제 재계산 (관리용) static async recalculateMonth(year, month) { const db = await getDb(); - + try { // 해당 월의 모든 날짜와 작업자 조합을 찾아서 재계산 const [workDates] = await db.execute(` @@ -101,27 +101,27 @@ class MonthlyStatusModel { FROM daily_work_reports WHERE YEAR(report_date) = ? AND MONTH(report_date) = ? `, [year, month]); - + let updatedCount = 0; - + for (const { report_date, worker_id } of workDates) { await db.execute('CALL UpdateMonthlyWorkerStatus(?, ?)', [report_date, worker_id]); updatedCount++; } - + console.log(`✅ ${year}년 ${month}월 집계 재계산 완료: ${updatedCount}건`); return { success: true, updatedCount }; - + } catch (error) { console.error('월별 집계 재계산 오류:', error); throw error; } } - + // 특정 날짜 집계 강제 업데이트 static async updateDateSummary(date, workerId = null) { const db = await getDb(); - + try { if (workerId) { // 특정 작업자만 업데이트 @@ -133,23 +133,23 @@ class MonthlyStatusModel { FROM daily_work_reports WHERE report_date = ? `, [date]); - + for (const { worker_id } of workers) { await db.execute('CALL UpdateMonthlyWorkerStatus(?, ?)', [date, worker_id]); } } - + return { success: true }; } catch (error) { console.error('날짜별 집계 업데이트 오류:', error); throw error; } } - + // 집계 테이블 상태 확인 static async getStatusInfo() { const db = await getDb(); - + try { const [summaryCount] = await db.execute(` SELECT @@ -159,7 +159,7 @@ class MonthlyStatusModel { MAX(last_updated) as last_update FROM monthly_summary `); - + const [workerStatusCount] = await db.execute(` SELECT COUNT(*) as total_records, @@ -168,7 +168,7 @@ class MonthlyStatusModel { MAX(last_updated) as last_update FROM monthly_worker_status `); - + return { summary: summaryCount[0], workerStatus: workerStatusCount[0] diff --git a/api.hyungi.net/models/userModel.js b/api.hyungi.net/models/userModel.js index e495f3d..c952cff 100644 --- a/api.hyungi.net/models/userModel.js +++ b/api.hyungi.net/models/userModel.js @@ -2,16 +2,16 @@ const { getDb } = require('../dbPool'); // 사용자 조회 const findByUsername = async (username) => { - try { - const db = await getDb(); - const [rows] = await db.query( - 'SELECT user_id, username, password, name, email, role, access_level, worker_id, is_active, last_login_at, password_changed_at, failed_login_attempts, locked_until, created_at, updated_at FROM users WHERE username = ?', [username] - ); - return rows[0]; - } catch (err) { - console.error('DB 오류 - 사용자 조회 실패:', err); - throw err; - } + try { + const db = await getDb(); + const [rows] = await db.query( + 'SELECT user_id, username, password, name, email, role, access_level, worker_id, is_active, last_login_at, password_changed_at, failed_login_attempts, locked_until, created_at, updated_at FROM users WHERE username = ?', [username] + ); + return rows[0]; + } catch (err) { + console.error('DB 오류 - 사용자 조회 실패:', err); + throw err; + } }; /** @@ -21,7 +21,7 @@ const findByUsername = async (username) => { const incrementFailedLoginAttempts = async (userId) => { try { const db = await getDb(); - await db.execute( + await db.query( 'UPDATE users SET failed_login_attempts = failed_login_attempts + 1 WHERE user_id = ?', [userId] ); @@ -38,7 +38,7 @@ const incrementFailedLoginAttempts = async (userId) => { const lockUserAccount = async (userId) => { try { const db = await getDb(); - await db.execute( + await db.query( 'UPDATE users SET locked_until = DATE_ADD(NOW(), INTERVAL 15 MINUTE) WHERE user_id = ?', [userId] ); @@ -55,7 +55,7 @@ const lockUserAccount = async (userId) => { const resetLoginAttempts = async (userId) => { try { const db = await getDb(); - await db.execute( + await db.query( 'UPDATE users SET last_login_at = NOW(), failed_login_attempts = 0, locked_until = NULL WHERE user_id = ?', [userId] ); @@ -68,8 +68,8 @@ const resetLoginAttempts = async (userId) => { // 명확한 내보내기 module.exports = { - findByUsername, - incrementFailedLoginAttempts, - lockUserAccount, - resetLoginAttempts + findByUsername, + incrementFailedLoginAttempts, + lockUserAccount, + resetLoginAttempts }; \ No newline at end of file diff --git a/api.hyungi.net/services/auth.service.js b/api.hyungi.net/services/auth.service.js index 621b2a6..1970149 100644 --- a/api.hyungi.net/services/auth.service.js +++ b/api.hyungi.net/services/auth.service.js @@ -7,7 +7,7 @@ const { getDb } = require('../dbPool'); const recordLoginHistory = async (userId, success, ipAddress, userAgent, failureReason = null) => { try { const db = await getDb(); - await db.execute( + await db.query( `INSERT INTO login_logs (user_id, login_time, ip_address, user_agent, login_status, failure_reason) VALUES (?, NOW(), ?, ?, ?, ?)`, [userId, ipAddress || 'unknown', userAgent || 'unknown', success ? 'success' : 'failed', failureReason] @@ -40,14 +40,14 @@ const loginService = async (username, password, ipAddress, userAgent) => { const isValid = await bcrypt.compare(password, user.password); if (!isValid) { console.log(`[로그인 실패] 비밀번호 불일치: ${username}`); - + // 모델 함수를 사용하여 로그인 실패 처리 await userModel.incrementFailedLoginAttempts(user.user_id); if (user.failed_login_attempts >= 4) { await userModel.lockUserAccount(user.user_id); } - + await recordLoginHistory(user.user_id, false, ipAddress, userAgent, 'invalid_password'); return { success: false, status: 401, error: '아이디 또는 비밀번호가 올바르지 않습니다.' }; } diff --git a/docker-compose.yml b/docker-compose.yml index a624df1..2b6b02e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -29,9 +29,13 @@ services: context: ./api.hyungi.net dockerfile: Dockerfile container_name: tkfb_api + env_file: + - ./.env depends_on: db: condition: service_healthy + redis: # Add Redis dependency + condition: service_started restart: unless-stopped ports: - "20005:3005" @@ -48,6 +52,8 @@ services: - JWT_EXPIRES_IN=${JWT_EXPIRES_IN:-7d} - JWT_REFRESH_SECRET=${JWT_REFRESH_SECRET} - JWT_REFRESH_EXPIRES_IN=${JWT_REFRESH_EXPIRES_IN:-30d} + - REDIS_HOST=redis # New Redis host + - REDIS_PORT=6379 # New Redis port volumes: - ./api.hyungi.net/public/img:/usr/src/app/public/img:ro - ./api.hyungi.net/uploads:/usr/src/app/uploads @@ -100,6 +106,16 @@ services: networks: - tkfb_network + # Redis Cache + redis: + image: redis:6-alpine # Using alpine for smaller image size + container_name: tkfb_redis + restart: unless-stopped + expose: + - "6379" # Redis default port + networks: + - tkfb_network + # phpMyAdmin phpmyadmin: image: phpmyadmin/phpmyadmin:latest