해당 서비스 도커화 성공, 룰 추가, 로그인 오류 수정, 소문자 룰 어느정도 해결

This commit is contained in:
Hyungi Ahn
2025-08-01 15:55:27 +09:00
parent ef06cec8d6
commit 809b2af53e
6418 changed files with 1922672 additions and 69 deletions

View File

@@ -69,7 +69,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]
);
@@ -95,7 +95,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]
);
@@ -126,7 +126,7 @@ exports.deleteUser = async (req, res) => {
// 사용자 존재 확인
const [user] = await db.query(
'SELECT user_id FROM Users WHERE user_id = ?',
'SELECT user_id FROM users WHERE user_id = ?',
[id]
);
@@ -138,7 +138,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);
@@ -165,7 +165,7 @@ exports.getAllUsers = async (req, res) => {
// 비밀번호 제외하고 조회
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`
);

View File

@@ -94,8 +94,7 @@ services:
volumes:
db_data:
external: true
name: 7a5a13668b77b18bc1efaf1811d09560aa3be0e722d782e8460cb74f37328d81 # 기존 볼륨명으로 연결
driver: local
# redis_data: # Redis 사용 시 주석 해제
networks:

View File

@@ -6,15 +6,7 @@ const helmet = require('helmet');
const rateLimit = require('express-rate-limit');
const app = express();
// ✅ Health check (맨 처음에 등록 - 모든 미들웨어보다 우선)
app.get('/api/health', (req, res) => {
console.log('🟢 Health check 호출됨!');
res.status(200).json({
status: 'healthy',
service: 'Hyungi API',
timestamp: new Date().toISOString()
});
});
// 헬스체크와 개발용 엔드포인트는 CORS 이후에 등록
// ✅ 보안 헤더 설정 (Helmet)
app.use(helmet({
@@ -39,12 +31,77 @@ app.use(helmet({
app.use(express.urlencoded({ extended: true, limit: '50mb' }));
app.use(express.json({ limit: '50mb' }));
//개발용
//개발용 CORS 설정 (수정됨)
app.use(cors({
origin: true, // 모든 origin 허용 (개발용)
credentials: true
origin: function (origin, callback) {
// 개발 환경에서는 모든 origin 허용
console.log('🌐 CORS Origin 요청:', origin);
const allowedOrigins = [
'http://localhost:20000', // 웹 UI
'http://localhost:3005', // API 서버
'http://localhost:3000', // 개발 포트
'http://127.0.0.1:20000', // 로컬호스트 대체
];
// origin이 없는 경우 (직접 접근) 허용
if (!origin) {
console.log('✅ Origin 없음 - 허용');
return callback(null, true);
}
// 허용된 origin인지 확인
if (allowedOrigins.includes(origin)) {
console.log('✅ 허용된 Origin:', origin);
return callback(null, true);
}
// 개발 환경에서는 모든 localhost 허용
if (origin.includes('localhost') || origin.includes('127.0.0.1')) {
console.log('✅ 로컬호스트 허용:', origin);
return callback(null, true);
}
console.log('❌ 차단된 Origin:', origin);
callback(null, false);
},
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With']
}));
// ✅ Health check (CORS 이후에 등록)
app.get('/api/health', (req, res) => {
console.log('🟢 Health check 호출됨!');
res.status(200).json({
status: 'healthy',
service: 'Hyungi API',
timestamp: new Date().toISOString()
});
});
// ✅ 개발용 Ping 엔드포인트
app.get('/api/ping', (req, res) => {
console.log('🏓 Ping 요청 받음!');
res.status(200).json({
message: 'pong',
timestamp: new Date().toISOString()
});
});
// ✅ 서버 상태 엔드포인트
app.get('/api/status', (req, res) => {
console.log('📊 Status 요청 받음!');
res.status(200).json({
status: 'running',
service: 'Hyungi API',
version: '2.1.0',
environment: process.env.NODE_ENV || 'development',
uptime: process.uptime(),
timestamp: new Date().toISOString()
});
});
// ✅ CORS 설정: 허용 origin 명시 (수정된 버전)
//app.use(cors({
// origin: function (origin, callback) {
@@ -106,11 +163,11 @@ const apiLimiter = rateLimit({
legacyHeaders: false,
});
// 로그인 API 속도 제한 (더 엄격하게)
// 로그인 API 속도 제한 (개발 환경에서 완화됨)
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15분
max: process.env.LOGIN_RATE_LIMIT_MAX_REQUESTS || 5,
message: '너무 많은 로그인 시도입니다. 15분 후에 다시 시도하세요.',
windowMs: 5 * 60 * 1000, // 5분으로 단축
max: process.env.LOGIN_RATE_LIMIT_MAX_REQUESTS || 20, // 5 -> 20으로 증가
message: '너무 많은 로그인 시도입니다. 5분 후에 다시 시도하세요.',
standardHeaders: true,
legacyHeaders: false,
skipSuccessfulRequests: true, // 성공한 요청은 카운트하지 않음
@@ -210,7 +267,9 @@ app.use('/api/*', (req, res, next) => {
'/api/auth/login',
'/api/auth/refresh-token',
'/api/auth/check-password-strength',
'/api/health'
'/api/health',
'/api/ping', // 개발용 핑
'/api/status' // 서버 상태
];
// 정확한 경로 매칭 확인

View File

@@ -0,0 +1,56 @@
-- migrations/003_normalize_table_names.sql
-- 모든 테이블명을 snake_case로 변경하여 룰 준수
-- 기존 PascalCase 테이블들을 snake_case로 변경
-- 1. Users -> users
RENAME TABLE Users TO users;
-- 2. CuttingPlan -> cutting_plan
RENAME TABLE CuttingPlan TO cutting_plan;
-- 3. DailyIssueReports -> daily_issue_reports
RENAME TABLE DailyIssueReports TO daily_issue_reports;
-- 4. EquipmentList -> equipment_list
RENAME TABLE EquipmentList TO equipment_list;
-- 5. FactoryInfo -> factory_info
RENAME TABLE FactoryInfo TO factory_info;
-- 6. IssueTypes -> issue_types
RENAME TABLE IssueTypes TO issue_types;
-- 7. PipeSpecs -> pipe_specs
RENAME TABLE PipeSpecs TO pipe_specs;
-- 8. Processes -> processes (이미 소문자이지만 통일성 위해)
-- RENAME TABLE Processes TO processes; -- 이미 소문자면 스킵
-- 9. Projects -> projects
RENAME TABLE Projects TO projects;
-- 10. Tasks -> tasks
RENAME TABLE Tasks TO tasks;
-- 11. WorkReports -> work_reports
RENAME TABLE WorkReports TO work_reports;
-- 12. Workers -> workers
RENAME TABLE Workers TO workers;
-- 이미 snake_case인 테이블들은 그대로 유지:
-- activity_logs
-- daily_work_reports
-- daily_worker_summary
-- error_types
-- login_logs
-- password_change_logs
-- uploaded_documents
-- work_report_audit_log
-- work_status_types
-- work_types
-- worker_groups
-- 변경 완료 로그
SELECT 'Table names normalized to snake_case according to project rules' as migration_status;

View File

@@ -1,19 +1,16 @@
const dbPool = require('../dbPool');
const { getDb } = require('../dbPool');
// 사용자 조회
const findByUsername = async (username) => {
let connection;
try {
connection = await dbPool.getConnection();
const [rows] = await connection.query(
const db = await getDb();
const [rows] = await db.query(
'SELECT * FROM users WHERE username = ?', [username]
);
return rows[0];
} catch (err) {
console.error('DB 오류 - 사용자 조회 실패:', err);
throw err;
} finally {
if (connection) connection.release();
}
};
@@ -22,18 +19,15 @@ const findByUsername = async (username) => {
* @param {number} userId - 사용자 ID
*/
const incrementFailedLoginAttempts = async (userId) => {
let connection;
try {
connection = await dbPool.getConnection();
await connection.execute(
const db = await getDb();
await db.execute(
'UPDATE users SET failed_login_attempts = failed_login_attempts + 1 WHERE user_id = ?',
[userId]
);
} catch (err) {
console.error('DB 오류 - 로그인 실패 횟수 증가 실패:', err);
throw err;
} finally {
if (connection) connection.release();
}
};
@@ -42,18 +36,15 @@ const incrementFailedLoginAttempts = async (userId) => {
* @param {number} userId - 사용자 ID
*/
const lockUserAccount = async (userId) => {
let connection;
try {
connection = await dbPool.getConnection();
await connection.execute(
const db = await getDb();
await db.execute(
'UPDATE users SET locked_until = DATE_ADD(NOW(), INTERVAL 15 MINUTE) WHERE user_id = ?',
[userId]
);
} catch (err) {
console.error('DB 오류 - 계정 잠금 실패:', err);
throw err;
} finally {
if (connection) connection.release();
}
};
@@ -62,18 +53,15 @@ const lockUserAccount = async (userId) => {
* @param {number} userId - 사용자 ID
*/
const resetLoginAttempts = async (userId) => {
let connection;
try {
connection = await dbPool.getConnection();
await connection.execute(
const db = await getDb();
await db.execute(
'UPDATE users SET last_login_at = NOW(), failed_login_attempts = 0, locked_until = NULL WHERE user_id = ?',
[userId]
);
} catch (err) {
console.error('DB 오류 - 로그인 상태 초기화 실패:', err);
throw err;
} finally {
if (connection) connection.release();
}
};

View File

@@ -97,7 +97,7 @@ router.post('/refresh-token', async (req, res) => {
// 사용자 정보 조회
const [users] = await connection.execute(
'SELECT * FROM Users WHERE user_id = ? AND is_active = TRUE',
'SELECT * FROM users WHERE user_id = ? AND is_active = TRUE',
[decoded.user_id]
);
@@ -176,7 +176,7 @@ router.post('/change-password', verifyToken, async (req, res) => {
// 현재 사용자의 비밀번호 조회
const [users] = await connection.execute(
'SELECT password FROM Users WHERE user_id = ?',
'SELECT password FROM users WHERE user_id = ?',
[userId]
);
@@ -283,7 +283,7 @@ router.post('/admin/change-password', verifyToken, async (req, res) => {
// 대상 사용자 확인
const [users] = await connection.execute(
'SELECT username, name FROM Users WHERE user_id = ?',
'SELECT username, name FROM users WHERE user_id = ?',
[userId]
);
@@ -400,7 +400,7 @@ router.get('/me', verifyToken, async (req, res) => {
connection = await mysql.createConnection(dbConfig);
const [rows] = await connection.execute(
'SELECT user_id, username, name, email, access_level, worker_id, last_login_at, created_at FROM Users WHERE user_id = ?',
'SELECT user_id, username, name, email, access_level, worker_id, last_login_at, created_at FROM users WHERE user_id = ?',
[userId]
);
@@ -466,7 +466,7 @@ router.post('/register', verifyToken, async (req, res) => {
// 사용자명 중복 체크
const [existing] = await connection.execute(
'SELECT user_id FROM Users WHERE username = ?',
'SELECT user_id FROM users WHERE username = ?',
[username]
);
@@ -480,7 +480,7 @@ router.post('/register', verifyToken, async (req, res) => {
// 이메일 중복 체크 (이메일이 제공된 경우)
if (email) {
const [existingEmail] = await connection.execute(
'SELECT user_id FROM Users WHERE email = ?',
'SELECT user_id FROM users WHERE email = ?',
[email]
);
@@ -561,7 +561,7 @@ router.get('/users', verifyToken, async (req, res) => {
is_active,
last_login_at,
created_at
FROM Users
FROM users
WHERE 1=1
`;
@@ -638,7 +638,7 @@ router.put('/users/:id', verifyToken, async (req, res) => {
// 사용자 존재 확인
const [existing] = await connection.execute(
'SELECT user_id, username FROM Users WHERE user_id = ?',
'SELECT user_id, username FROM users WHERE user_id = ?',
[userId]
);
@@ -662,7 +662,7 @@ router.put('/users/:id', verifyToken, async (req, res) => {
// 이메일 중복 체크
if (email) {
const [emailCheck] = await connection.execute(
'SELECT user_id FROM Users WHERE email = ? AND user_id != ?',
'SELECT user_id FROM users WHERE email = ? AND user_id != ?',
[email, userId]
);
@@ -732,7 +732,7 @@ router.put('/users/:id', verifyToken, async (req, res) => {
// 업데이트된 사용자 정보 조회
const [updated] = await connection.execute(
'SELECT user_id, username, name, email, access_level, worker_id, is_active FROM Users WHERE user_id = ?',
'SELECT user_id, username, name, email, access_level, worker_id, is_active FROM users WHERE user_id = ?',
[userId]
);
@@ -786,7 +786,7 @@ router.delete('/users/:id', verifyToken, async (req, res) => {
// 사용자 존재 확인
const [existing] = await connection.execute(
'SELECT username FROM Users WHERE user_id = ?',
'SELECT username FROM users WHERE user_id = ?',
[userId]
);

View File

@@ -1,22 +1,19 @@
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const userModel = require('../models/userModel');
const dbPool = require('../dbPool');
const { getDb } = require('../dbPool');
// 로그인 이력 기록 (서비스 내부 헬퍼 함수)
const recordLoginHistory = async (userId, success, ipAddress, userAgent, failureReason = null) => {
let connection;
try {
connection = await dbPool.getConnection();
await connection.execute(
const db = await getDb();
await db.execute(
`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]
);
} catch (error) {
console.error('로그인 이력 기록 실패:', error);
} finally {
if (connection) connection.release();
}
};