fix: Login 500, DB schema sync, Dashboard missing data, Major Refactoring

This commit is contained in:
Hyungi Ahn
2025-12-19 15:47:15 +09:00
parent 770fa91366
commit 3549710325
7 changed files with 112 additions and 74 deletions

View File

@@ -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)

View File

@@ -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);

View File

@@ -30,7 +30,7 @@ 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();
@@ -45,7 +45,7 @@ exports.register = async (req, res) => {
// 중복 아이디 확인
const [existing] = await db.query(
'SELECT user_id FROM Users WHERE username = ?',
'SELECT user_id FROM users WHERE username = ?',
[username]
);
@@ -71,7 +71,7 @@ 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]
);
@@ -95,14 +95,14 @@ 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]
);
@@ -114,7 +114,7 @@ exports.deleteUser = async (req, res) => {
}
// 사용자 삭제
await db.query('DELETE FROM Users WHERE user_id = ?', [id]);
await db.query('DELETE FROM users WHERE user_id = ?', [id]);
console.log('[사용자 삭제 성공] ID:', id);
@@ -134,14 +134,14 @@ 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`
);
@@ -153,5 +153,8 @@ exports.getAllUsers = async (req, res) => {
};
module.exports = {
login
login,
register,
deleteUser,
getAllUsers
};

View File

@@ -49,7 +49,7 @@ class MonthlyStatusModel {
// 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,10 +76,10 @@ 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]);

View File

@@ -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]
);

View File

@@ -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]

View File

@@ -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