fix: Login 500, DB schema sync, Dashboard missing data, Major Refactoring
This commit is contained in:
19
DEV_LOG.md
19
DEV_LOG.md
@@ -8,6 +8,25 @@
|
|||||||
- **[UPDATE] Model Layer**: Raw SQL 쿼리를 Controller에서 Model(`models/WorkAnalysis.js`)로 이동. `getProjectWorkTypeRawData` 메서드 추가.
|
- **[UPDATE] Model Layer**: Raw SQL 쿼리를 Controller에서 Model(`models/WorkAnalysis.js`)로 이동. `getProjectWorkTypeRawData` 메서드 추가.
|
||||||
- **[CLEANUP] Controller**: `getProjectWorkTypeAnalysis` 메서드가 Service를 호출하도록 단순화.
|
- **[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)
|
## 🛡보안 및 검토 리포트 (History)
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ function setupRoutes(app) {
|
|||||||
app.use('/api/setup', setupRoutes);
|
app.use('/api/setup', setupRoutes);
|
||||||
|
|
||||||
// Health check
|
// Health check
|
||||||
app.use('/api', healthRoutes);
|
app.use('/api/health', healthRoutes);
|
||||||
|
|
||||||
// 일반 API에 속도 제한 적용
|
// 일반 API에 속도 제한 적용
|
||||||
app.use('/api/', apiLimiter);
|
app.use('/api/', apiLimiter);
|
||||||
|
|||||||
@@ -30,29 +30,29 @@ const login = asyncHandler(async (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// ✅ 사용자 등록 기능 추가
|
// ✅ 사용자 등록 기능 추가
|
||||||
exports.register = async (req, res) => {
|
const register = async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { username, password, name, access_level, worker_id } = req.body;
|
const { username, password, name, access_level, worker_id } = req.body;
|
||||||
const db = await getDb();
|
const db = await getDb();
|
||||||
|
|
||||||
// 필수 필드 검증
|
// 필수 필드 검증
|
||||||
if (!username || !password || !name || !access_level) {
|
if (!username || !password || !name || !access_level) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
error: '필수 정보가 누락되었습니다.'
|
error: '필수 정보가 누락되었습니다.'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 중복 아이디 확인
|
// 중복 아이디 확인
|
||||||
const [existing] = await db.query(
|
const [existing] = await db.query(
|
||||||
'SELECT user_id FROM Users WHERE username = ?',
|
'SELECT user_id FROM users WHERE username = ?',
|
||||||
[username]
|
[username]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (existing.length > 0) {
|
if (existing.length > 0) {
|
||||||
return res.status(409).json({
|
return res.status(409).json({
|
||||||
success: false,
|
success: false,
|
||||||
error: '이미 존재하는 아이디입니다.'
|
error: '이미 존재하는 아이디입니다.'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,17 +71,17 @@ exports.register = async (req, res) => {
|
|||||||
|
|
||||||
// 사용자 등록
|
// 사용자 등록
|
||||||
const [result] = await db.query(
|
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 (?, ?, ?, ?, ?, ?)`,
|
VALUES (?, ?, ?, ?, ?, ?)`,
|
||||||
[username, hashedPassword, name, role, access_level, worker_id]
|
[username, hashedPassword, name, role, access_level, worker_id]
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log('[사용자 등록 성공]', username);
|
console.log('[사용자 등록 성공]', username);
|
||||||
|
|
||||||
return res.status(201).json({
|
return res.status(201).json({
|
||||||
success: true,
|
success: true,
|
||||||
message: '사용자 등록이 완료되었습니다.',
|
message: '사용자 등록이 완료되었습니다.',
|
||||||
user_id: result.insertId
|
user_id: result.insertId
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -95,32 +95,32 @@ exports.register = async (req, res) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// ✅ 사용자 삭제 기능 추가
|
// ✅ 사용자 삭제 기능 추가
|
||||||
exports.deleteUser = async (req, res) => {
|
const deleteUser = async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const db = await getDb();
|
const db = await getDb();
|
||||||
|
|
||||||
// 사용자 존재 확인
|
// 사용자 존재 확인
|
||||||
const [user] = await db.query(
|
const [user] = await db.query(
|
||||||
'SELECT user_id FROM Users WHERE user_id = ?',
|
'SELECT user_id FROM users WHERE user_id = ?',
|
||||||
[id]
|
[id]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (user.length === 0) {
|
if (user.length === 0) {
|
||||||
return res.status(404).json({
|
return res.status(404).json({
|
||||||
success: false,
|
success: false,
|
||||||
error: '해당 사용자를 찾을 수 없습니다.'
|
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);
|
console.log('[사용자 삭제 성공] ID:', id);
|
||||||
|
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
success: true,
|
success: true,
|
||||||
message: '사용자가 삭제되었습니다.'
|
message: '사용자가 삭제되었습니다.'
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -134,17 +134,17 @@ exports.deleteUser = async (req, res) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 모든 사용자 목록 조회
|
// 모든 사용자 목록 조회
|
||||||
exports.getAllUsers = async (req, res) => {
|
const getAllUsers = async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const db = await getDb();
|
const db = await getDb();
|
||||||
|
|
||||||
// 비밀번호 제외하고 조회
|
// 비밀번호 제외하고 조회
|
||||||
const [rows] = await db.query(
|
const [rows] = await db.query(
|
||||||
`SELECT user_id, username, name, role, access_level, worker_id, created_at
|
`SELECT user_id, username, name, role, access_level, worker_id, created_at
|
||||||
FROM Users
|
FROM users
|
||||||
ORDER BY created_at DESC`
|
ORDER BY created_at DESC`
|
||||||
);
|
);
|
||||||
|
|
||||||
res.status(200).json(rows);
|
res.status(200).json(rows);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[사용자 목록 조회 실패]', err);
|
console.error('[사용자 목록 조회 실패]', err);
|
||||||
@@ -153,5 +153,8 @@ exports.getAllUsers = async (req, res) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
login
|
login,
|
||||||
|
register,
|
||||||
|
deleteUser,
|
||||||
|
getAllUsers
|
||||||
};
|
};
|
||||||
@@ -7,7 +7,7 @@ class MonthlyStatusModel {
|
|||||||
// 월별 일자별 요약 조회 (캘린더용)
|
// 월별 일자별 요약 조회 (캘린더용)
|
||||||
static async getMonthlySummary(year, month) {
|
static async getMonthlySummary(year, month) {
|
||||||
const db = await getDb();
|
const db = await getDb();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const [rows] = await db.execute(`
|
const [rows] = await db.execute(`
|
||||||
SELECT
|
SELECT
|
||||||
@@ -32,24 +32,24 @@ class MonthlyStatusModel {
|
|||||||
WHERE year = ? AND month = ?
|
WHERE year = ? AND month = ?
|
||||||
ORDER BY date ASC
|
ORDER BY date ASC
|
||||||
`, [year, month]);
|
`, [year, month]);
|
||||||
|
|
||||||
return rows;
|
return rows;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('월별 요약 조회 오류:', error);
|
console.error('월별 요약 조회 오류:', error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 특정 날짜의 작업자별 상태 조회 (모달용)
|
// 특정 날짜의 작업자별 상태 조회 (모달용)
|
||||||
// ✅ 리팩토링: 집계 테이블 대신 daily_work_reports에서 직접 조회 (중복 문제 완전 해결)
|
// ✅ 리팩토링: 집계 테이블 대신 daily_work_reports에서 직접 조회 (중복 문제 완전 해결)
|
||||||
static async getDailyWorkerStatus(date) {
|
static async getDailyWorkerStatus(date) {
|
||||||
const db = await getDb();
|
const db = await getDb();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// daily_work_reports에서 직접 집계하여 조회 (중복 없음 보장)
|
// daily_work_reports에서 직접 집계하여 조회 (중복 없음 보장)
|
||||||
const [rows] = await db.query(`
|
const [rows] = await db.query(`
|
||||||
SELECT
|
SELECT
|
||||||
dwr.worker_id,
|
w.worker_id,
|
||||||
w.worker_name,
|
w.worker_name,
|
||||||
w.job_type,
|
w.job_type,
|
||||||
YEAR(?) as year,
|
YEAR(?) as year,
|
||||||
@@ -58,7 +58,7 @@ class MonthlyStatusModel {
|
|||||||
COALESCE(SUM(dwr.work_hours), 0) as total_work_hours,
|
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 actual_work_hours,
|
||||||
COALESCE(SUM(CASE WHEN dwr.project_id = 13 THEN dwr.work_hours ELSE 0 END), 0) as vacation_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.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,
|
COUNT(CASE WHEN dwr.work_status_id = 2 THEN 1 END) as error_work_count,
|
||||||
CASE
|
CASE
|
||||||
@@ -76,24 +76,24 @@ class MonthlyStatusModel {
|
|||||||
ELSE 0
|
ELSE 0
|
||||||
END as has_issues,
|
END as has_issues,
|
||||||
MAX(dwr.created_at) as last_updated
|
MAX(dwr.created_at) as last_updated
|
||||||
FROM daily_work_reports dwr
|
FROM workers w
|
||||||
JOIN workers w ON dwr.worker_id = w.worker_id
|
LEFT JOIN daily_work_reports dwr ON w.worker_id = dwr.worker_id AND dwr.report_date = ?
|
||||||
WHERE dwr.report_date = ?
|
WHERE w.status = 'active'
|
||||||
GROUP BY dwr.worker_id, w.worker_name, w.job_type
|
GROUP BY w.worker_id, w.worker_name, w.job_type
|
||||||
ORDER BY w.worker_name ASC
|
ORDER BY w.worker_name ASC
|
||||||
`, [date, date, date, date]);
|
`, [date, date, date, date]);
|
||||||
|
|
||||||
return rows;
|
return rows;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('일별 작업자 상태 조회 오류:', error);
|
console.error('일별 작업자 상태 조회 오류:', error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 월별 집계 데이터 강제 재계산 (관리용)
|
// 월별 집계 데이터 강제 재계산 (관리용)
|
||||||
static async recalculateMonth(year, month) {
|
static async recalculateMonth(year, month) {
|
||||||
const db = await getDb();
|
const db = await getDb();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 해당 월의 모든 날짜와 작업자 조합을 찾아서 재계산
|
// 해당 월의 모든 날짜와 작업자 조합을 찾아서 재계산
|
||||||
const [workDates] = await db.execute(`
|
const [workDates] = await db.execute(`
|
||||||
@@ -101,27 +101,27 @@ class MonthlyStatusModel {
|
|||||||
FROM daily_work_reports
|
FROM daily_work_reports
|
||||||
WHERE YEAR(report_date) = ? AND MONTH(report_date) = ?
|
WHERE YEAR(report_date) = ? AND MONTH(report_date) = ?
|
||||||
`, [year, month]);
|
`, [year, month]);
|
||||||
|
|
||||||
let updatedCount = 0;
|
let updatedCount = 0;
|
||||||
|
|
||||||
for (const { report_date, worker_id } of workDates) {
|
for (const { report_date, worker_id } of workDates) {
|
||||||
await db.execute('CALL UpdateMonthlyWorkerStatus(?, ?)', [report_date, worker_id]);
|
await db.execute('CALL UpdateMonthlyWorkerStatus(?, ?)', [report_date, worker_id]);
|
||||||
updatedCount++;
|
updatedCount++;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`✅ ${year}년 ${month}월 집계 재계산 완료: ${updatedCount}건`);
|
console.log(`✅ ${year}년 ${month}월 집계 재계산 완료: ${updatedCount}건`);
|
||||||
return { success: true, updatedCount };
|
return { success: true, updatedCount };
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('월별 집계 재계산 오류:', error);
|
console.error('월별 집계 재계산 오류:', error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 특정 날짜 집계 강제 업데이트
|
// 특정 날짜 집계 강제 업데이트
|
||||||
static async updateDateSummary(date, workerId = null) {
|
static async updateDateSummary(date, workerId = null) {
|
||||||
const db = await getDb();
|
const db = await getDb();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (workerId) {
|
if (workerId) {
|
||||||
// 특정 작업자만 업데이트
|
// 특정 작업자만 업데이트
|
||||||
@@ -133,23 +133,23 @@ class MonthlyStatusModel {
|
|||||||
FROM daily_work_reports
|
FROM daily_work_reports
|
||||||
WHERE report_date = ?
|
WHERE report_date = ?
|
||||||
`, [date]);
|
`, [date]);
|
||||||
|
|
||||||
for (const { worker_id } of workers) {
|
for (const { worker_id } of workers) {
|
||||||
await db.execute('CALL UpdateMonthlyWorkerStatus(?, ?)', [date, worker_id]);
|
await db.execute('CALL UpdateMonthlyWorkerStatus(?, ?)', [date, worker_id]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('날짜별 집계 업데이트 오류:', error);
|
console.error('날짜별 집계 업데이트 오류:', error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 집계 테이블 상태 확인
|
// 집계 테이블 상태 확인
|
||||||
static async getStatusInfo() {
|
static async getStatusInfo() {
|
||||||
const db = await getDb();
|
const db = await getDb();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const [summaryCount] = await db.execute(`
|
const [summaryCount] = await db.execute(`
|
||||||
SELECT
|
SELECT
|
||||||
@@ -159,7 +159,7 @@ class MonthlyStatusModel {
|
|||||||
MAX(last_updated) as last_update
|
MAX(last_updated) as last_update
|
||||||
FROM monthly_summary
|
FROM monthly_summary
|
||||||
`);
|
`);
|
||||||
|
|
||||||
const [workerStatusCount] = await db.execute(`
|
const [workerStatusCount] = await db.execute(`
|
||||||
SELECT
|
SELECT
|
||||||
COUNT(*) as total_records,
|
COUNT(*) as total_records,
|
||||||
@@ -168,7 +168,7 @@ class MonthlyStatusModel {
|
|||||||
MAX(last_updated) as last_update
|
MAX(last_updated) as last_update
|
||||||
FROM monthly_worker_status
|
FROM monthly_worker_status
|
||||||
`);
|
`);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
summary: summaryCount[0],
|
summary: summaryCount[0],
|
||||||
workerStatus: workerStatusCount[0]
|
workerStatus: workerStatusCount[0]
|
||||||
|
|||||||
@@ -2,16 +2,16 @@ const { getDb } = require('../dbPool');
|
|||||||
|
|
||||||
// 사용자 조회
|
// 사용자 조회
|
||||||
const findByUsername = async (username) => {
|
const findByUsername = async (username) => {
|
||||||
try {
|
try {
|
||||||
const db = await getDb();
|
const db = await getDb();
|
||||||
const [rows] = await db.query(
|
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]
|
'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];
|
return rows[0];
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('DB 오류 - 사용자 조회 실패:', err);
|
console.error('DB 오류 - 사용자 조회 실패:', err);
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -21,7 +21,7 @@ const findByUsername = async (username) => {
|
|||||||
const incrementFailedLoginAttempts = async (userId) => {
|
const incrementFailedLoginAttempts = async (userId) => {
|
||||||
try {
|
try {
|
||||||
const db = await getDb();
|
const db = await getDb();
|
||||||
await db.execute(
|
await db.query(
|
||||||
'UPDATE users SET failed_login_attempts = failed_login_attempts + 1 WHERE user_id = ?',
|
'UPDATE users SET failed_login_attempts = failed_login_attempts + 1 WHERE user_id = ?',
|
||||||
[userId]
|
[userId]
|
||||||
);
|
);
|
||||||
@@ -38,7 +38,7 @@ const incrementFailedLoginAttempts = async (userId) => {
|
|||||||
const lockUserAccount = async (userId) => {
|
const lockUserAccount = async (userId) => {
|
||||||
try {
|
try {
|
||||||
const db = await getDb();
|
const db = await getDb();
|
||||||
await db.execute(
|
await db.query(
|
||||||
'UPDATE users SET locked_until = DATE_ADD(NOW(), INTERVAL 15 MINUTE) WHERE user_id = ?',
|
'UPDATE users SET locked_until = DATE_ADD(NOW(), INTERVAL 15 MINUTE) WHERE user_id = ?',
|
||||||
[userId]
|
[userId]
|
||||||
);
|
);
|
||||||
@@ -55,7 +55,7 @@ const lockUserAccount = async (userId) => {
|
|||||||
const resetLoginAttempts = async (userId) => {
|
const resetLoginAttempts = async (userId) => {
|
||||||
try {
|
try {
|
||||||
const db = await getDb();
|
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 = ?',
|
'UPDATE users SET last_login_at = NOW(), failed_login_attempts = 0, locked_until = NULL WHERE user_id = ?',
|
||||||
[userId]
|
[userId]
|
||||||
);
|
);
|
||||||
@@ -68,8 +68,8 @@ const resetLoginAttempts = async (userId) => {
|
|||||||
|
|
||||||
// 명확한 내보내기
|
// 명확한 내보내기
|
||||||
module.exports = {
|
module.exports = {
|
||||||
findByUsername,
|
findByUsername,
|
||||||
incrementFailedLoginAttempts,
|
incrementFailedLoginAttempts,
|
||||||
lockUserAccount,
|
lockUserAccount,
|
||||||
resetLoginAttempts
|
resetLoginAttempts
|
||||||
};
|
};
|
||||||
@@ -7,7 +7,7 @@ const { getDb } = require('../dbPool');
|
|||||||
const recordLoginHistory = async (userId, success, ipAddress, userAgent, failureReason = null) => {
|
const recordLoginHistory = async (userId, success, ipAddress, userAgent, failureReason = null) => {
|
||||||
try {
|
try {
|
||||||
const db = await getDb();
|
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)
|
`INSERT INTO login_logs (user_id, login_time, ip_address, user_agent, login_status, failure_reason)
|
||||||
VALUES (?, NOW(), ?, ?, ?, ?)`,
|
VALUES (?, NOW(), ?, ?, ?, ?)`,
|
||||||
[userId, ipAddress || 'unknown', userAgent || 'unknown', success ? 'success' : 'failed', failureReason]
|
[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);
|
const isValid = await bcrypt.compare(password, user.password);
|
||||||
if (!isValid) {
|
if (!isValid) {
|
||||||
console.log(`[로그인 실패] 비밀번호 불일치: ${username}`);
|
console.log(`[로그인 실패] 비밀번호 불일치: ${username}`);
|
||||||
|
|
||||||
// 모델 함수를 사용하여 로그인 실패 처리
|
// 모델 함수를 사용하여 로그인 실패 처리
|
||||||
await userModel.incrementFailedLoginAttempts(user.user_id);
|
await userModel.incrementFailedLoginAttempts(user.user_id);
|
||||||
|
|
||||||
if (user.failed_login_attempts >= 4) {
|
if (user.failed_login_attempts >= 4) {
|
||||||
await userModel.lockUserAccount(user.user_id);
|
await userModel.lockUserAccount(user.user_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
await recordLoginHistory(user.user_id, false, ipAddress, userAgent, 'invalid_password');
|
await recordLoginHistory(user.user_id, false, ipAddress, userAgent, 'invalid_password');
|
||||||
return { success: false, status: 401, error: '아이디 또는 비밀번호가 올바르지 않습니다.' };
|
return { success: false, status: 401, error: '아이디 또는 비밀번호가 올바르지 않습니다.' };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,9 +29,13 @@ services:
|
|||||||
context: ./api.hyungi.net
|
context: ./api.hyungi.net
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
container_name: tkfb_api
|
container_name: tkfb_api
|
||||||
|
env_file:
|
||||||
|
- ./.env
|
||||||
depends_on:
|
depends_on:
|
||||||
db:
|
db:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
redis: # Add Redis dependency
|
||||||
|
condition: service_started
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
- "20005:3005"
|
- "20005:3005"
|
||||||
@@ -48,6 +52,8 @@ services:
|
|||||||
- JWT_EXPIRES_IN=${JWT_EXPIRES_IN:-7d}
|
- JWT_EXPIRES_IN=${JWT_EXPIRES_IN:-7d}
|
||||||
- JWT_REFRESH_SECRET=${JWT_REFRESH_SECRET}
|
- JWT_REFRESH_SECRET=${JWT_REFRESH_SECRET}
|
||||||
- JWT_REFRESH_EXPIRES_IN=${JWT_REFRESH_EXPIRES_IN:-30d}
|
- JWT_REFRESH_EXPIRES_IN=${JWT_REFRESH_EXPIRES_IN:-30d}
|
||||||
|
- REDIS_HOST=redis # New Redis host
|
||||||
|
- REDIS_PORT=6379 # New Redis port
|
||||||
volumes:
|
volumes:
|
||||||
- ./api.hyungi.net/public/img:/usr/src/app/public/img:ro
|
- ./api.hyungi.net/public/img:/usr/src/app/public/img:ro
|
||||||
- ./api.hyungi.net/uploads:/usr/src/app/uploads
|
- ./api.hyungi.net/uploads:/usr/src/app/uploads
|
||||||
@@ -100,6 +106,16 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
- tkfb_network
|
- 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
|
||||||
phpmyadmin:
|
phpmyadmin:
|
||||||
image: phpmyadmin/phpmyadmin:latest
|
image: phpmyadmin/phpmyadmin:latest
|
||||||
|
|||||||
Reference in New Issue
Block a user