feat: 다수 기능 개선 - 순찰, 출근, 작업분석, 모바일 UI 등

- 순찰/점검 기능 개선 (zone-detail 페이지 추가)
- 출근/근태 시스템 개선 (연차 조회, 근무현황)
- 작업분석 대분류 그룹화 및 마이그레이션 스크립트
- 모바일 네비게이션 UI 추가
- NAS 배포 도구 및 문서 추가

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Hyungi Ahn
2026-02-09 14:41:01 +09:00
parent 1548253f56
commit 2b1c7bfb88
633 changed files with 361224 additions and 1090 deletions

View File

@@ -0,0 +1,10 @@
// /routes/analysisRoutes.js
const express = require('express');
const router = express.Router();
const { getAnalysisData } = require('../controllers/analysisController');
const { verifyToken } = require('../middlewares/authMiddleware'); // 인증 미들웨어 추가
// GET /api/analysis?startDate=...&endDate=...
router.get('/', verifyToken, getAnalysisData);
module.exports = router;

View File

@@ -0,0 +1,46 @@
const express = require('express');
const router = express.Router();
const AttendanceController = require('../controllers/attendanceController');
const { verifyToken } = require('../middlewares/authMiddleware');
// 모든 라우트에 인증 미들웨어 적용
router.use(verifyToken);
// 일일 근태 현황 조회 (대시보드용)
router.get('/daily-status', AttendanceController.getDailyAttendanceStatus);
// 일일 근태 기록 조회
router.get('/daily-records', AttendanceController.getDailyAttendanceRecords);
// 기간별 근태 기록 조회 (월별 조회용)
router.get('/records', AttendanceController.getAttendanceRecordsByRange);
// 근태 기록 생성/업데이트
router.post('/records', AttendanceController.upsertAttendanceRecord);
router.put('/records', AttendanceController.upsertAttendanceRecord);
// 휴가 처리
router.post('/vacation', AttendanceController.processVacation);
// 초과근무 승인
router.post('/overtime/approve', AttendanceController.approveOvertime);
// 근로 유형 목록 조회
router.get('/attendance-types', AttendanceController.getAttendanceTypes);
// 휴가 유형 목록 조회
router.get('/vacation-types', AttendanceController.getVacationTypes);
// 작업자 휴가 잔여 조회
router.get('/vacation-balance/:worker_id', AttendanceController.getWorkerVacationBalance);
// 월별 근태 통계
router.get('/monthly-stats', AttendanceController.getMonthlyAttendanceStats);
// 출근 체크 목록 조회 (아침용, 휴가 정보 포함)
router.get('/checkin-list', AttendanceController.getCheckinList);
// 출근 체크 일괄 저장
router.post('/checkins', AttendanceController.saveCheckins);
module.exports = router;

View File

@@ -0,0 +1,215 @@
// routes/auth.js
const express = require('express');
const bcrypt = require('bcrypt');
const jwt = require('jsonwebtoken');
const { requireAuth, requireRole } = require('../middlewares/auth');
const router = express.Router();
// 임시 사용자 데이터 (실제 운영 시 DB 사용 필수)
// 비밀번호 해시는 bcrypt.hash('password', 10)으로 생성됨
let users = [
{
user_id: 1,
username: 'admin',
password: '$2b$10$N9qo8uLOickgx2ZMRZoMyeIjZRGdjGj/n3.w7VtL.r8yR.nTfC7Hy', // 'password' 해시
name: '관리자',
access_level: 'admin',
worker_id: null,
created_at: new Date()
},
{
user_id: 2,
username: 'group_leader1',
password: '$2b$10$N9qo8uLOickgx2ZMRZoMyeIjZRGdjGj/n3.w7VtL.r8yR.nTfC7Hy', // 'password' 해시
name: '김그룹장',
access_level: 'group_leader',
worker_id: 1,
created_at: new Date()
}
];
// 보안 경고: 운영 환경에서는 반드시 .env의 JWT_SECRET을 설정해야 합니다
if (!process.env.JWT_SECRET) {
console.warn('⚠️ WARNING: JWT_SECRET이 설정되지 않았습니다. 운영 환경에서는 반드시 설정하세요!');
}
/**
* 로그인
*/
router.post('/login', async (req, res) => {
try {
const { username, password } = req.body;
if (!username || !password) {
return res.status(400).json({ error: '사용자명과 비밀번호를 입력해주세요.' });
}
const user = users.find(u => u.username === username);
if (!user) {
return res.status(401).json({ error: '사용자를 찾을 수 없습니다.' });
}
// 비밀번호 확인 (bcrypt.compare 사용)
const isValid = await bcrypt.compare(password, user.password);
if (!isValid) {
return res.status(401).json({ error: '비밀번호가 올바르지 않습니다.' });
}
// JWT 토큰 생성
const token = jwt.sign(
{
user_id: user.user_id,
username: user.username,
access_level: user.access_level,
worker_id: user.worker_id
},
process.env.JWT_SECRET,
{ expiresIn: '24h' }
);
res.json({
success: true,
token,
user: {
user_id: user.user_id,
username: user.username,
name: user.name,
access_level: user.access_level,
worker_id: user.worker_id
}
});
} catch (error) {
console.error('Login error:', error);
res.status(500).json({ error: '서버 오류가 발생했습니다.' });
}
});
/**
* 현재 사용자 정보 조회
*/
router.get('/me', requireAuth, (req, res) => {
try {
const userId = req.user.user_id;
const user = users.find(u => u.user_id === userId);
if (!user) {
return res.status(404).json({ error: '사용자를 찾을 수 없습니다.' });
}
res.json({
user_id: user.user_id,
username: user.username,
name: user.name,
access_level: user.access_level,
worker_id: user.worker_id
});
} catch (error) {
console.error('Get current user error:', error);
res.status(500).json({ error: '서버 오류가 발생했습니다.' });
}
});
/**
* 사용자 등록 (관리자만)
*/
router.post('/register', requireAuth, requireRole('admin', 'system'), async (req, res) => {
try {
const { username, password, name, access_level, worker_id } = req.body;
if (!username || !password || !name || !access_level) {
return res.status(400).json({ error: '필수 항목을 모두 입력해주세요.' });
}
// 사용자명 중복 체크
const existingUser = users.find(u => u.username === username);
if (existingUser) {
return res.status(409).json({ error: '이미 존재하는 사용자명입니다.' });
}
// 비밀번호 해시
const hashedPassword = await bcrypt.hash(password, 10);
const newUser = {
user_id: users.length + 1,
username,
password: hashedPassword,
name,
access_level,
worker_id: worker_id || null,
created_at: new Date()
};
users.push(newUser);
res.json({
success: true,
message: '사용자가 성공적으로 등록되었습니다.',
user: {
user_id: newUser.user_id,
username: newUser.username,
name: newUser.name,
access_level: newUser.access_level,
worker_id: newUser.worker_id
}
});
} catch (error) {
console.error('Register error:', error);
res.status(500).json({ error: '서버 오류가 발생했습니다.' });
}
});
/**
* 사용자 목록 조회 (관리자만)
*/
router.get('/users', requireAuth, requireRole('admin', 'system'), (req, res) => {
try {
const userList = users.map(user => ({
user_id: user.user_id,
username: user.username,
name: user.name,
access_level: user.access_level,
worker_id: user.worker_id,
created_at: user.created_at
}));
res.json(userList);
} catch (error) {
console.error('Get users error:', error);
res.status(500).json({ error: '서버 오류가 발생했습니다.' });
}
});
/**
* 사용자 삭제 (관리자만)
*/
router.delete('/users/:id', requireAuth, requireRole('admin', 'system'), (req, res) => {
try {
const userId = parseInt(req.params.id);
// 자기 자신 삭제 방지
if (userId === req.user.user_id) {
return res.status(400).json({ error: '자기 자신은 삭제할 수 없습니다.' });
}
const userIndex = users.findIndex(u => u.user_id === userId);
if (userIndex === -1) {
return res.status(404).json({ error: '사용자를 찾을 수 없습니다.' });
}
users.splice(userIndex, 1);
res.json({
success: true,
message: '사용자가 성공적으로 삭제되었습니다.'
});
} catch (error) {
console.error('Delete user error:', error);
res.status(500).json({ error: '서버 오류가 발생했습니다.' });
}
});
module.exports = router;

View File

@@ -0,0 +1,978 @@
/**
* @swagger
* tags:
* name: Authentication
* description: 사용자 인증 및 권한 관리 API
*/
// routes/authRoutes.js - 비밀번호 변경 및 보안 기능 포함 완전판
const express = require('express');
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const mysql = require('mysql2/promise');
const { verifyToken } = require('../middlewares/authMiddleware');
const { validatePassword, getPasswordError } = require('../utils/passwordValidator');
const router = express.Router();
const authController = require('../controllers/authController');
// DB 연결 설정
const dbConfig = {
host: process.env.DB_HOST || 'db_hyungi_net',
user: process.env.DB_USER || 'hyungi',
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME || 'hyungi'
};
// 로그인 시도 추적 (메모리 기반 - 실제로는 Redis 권장)
const loginAttempts = new Map();
// 로그인 시도 체크 미들웨어
const checkLoginAttempts = (req, res, next) => {
const { username } = req.body;
const key = `login_${username}`;
const attempts = loginAttempts.get(key) || { count: 0, blockedUntil: null };
// 차단 확인
if (attempts.blockedUntil && attempts.blockedUntil > Date.now()) {
const remainingTime = Math.ceil((attempts.blockedUntil - Date.now()) / 1000 / 60);
return res.status(429).json({
error: `너무 많은 로그인 시도입니다. ${remainingTime}분 후에 다시 시도하세요.`
});
}
req.loginAttempts = attempts;
next();
};
// 로그인 시도 기록
const recordLoginAttempt = (username, success = false) => {
const key = `login_${username}`;
const attempts = loginAttempts.get(key) || { count: 0, blockedUntil: null };
if (success) {
loginAttempts.delete(key);
} else {
attempts.count += 1;
// 5회 실패 시 15분 차단
if (attempts.count >= 5) {
attempts.blockedUntil = Date.now() + (15 * 60 * 1000);
console.log(`[로그인 차단] 사용자: ${username} - 15분간 차단`);
}
loginAttempts.set(key, attempts);
}
};
// 로그인 이력 기록
const recordLoginHistory = async (connection, userId, success, ipAddress, userAgent, failureReason = null) => {
try {
await connection.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);
// 로그 테이블이 없어도 계속 진행
}
};
/**
* @swagger
* /api/auth/login:
* post:
* tags: [Authentication]
* summary: 사용자 로그인
* description: 사용자명과 비밀번호로 로그인하여 JWT 토큰을 발급받습니다.
* requestBody:
* required: true
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/LoginRequest'
* responses:
* 200:
* description: 로그인 성공
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/LoginResponse'
* 400:
* description: 잘못된 요청 (사용자명 또는 비밀번호 누락)
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ErrorResponse'
* 401:
* description: 인증 실패 (잘못된 사용자명 또는 비밀번호)
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ErrorResponse'
* 429:
* description: 너무 많은 로그인 시도
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ErrorResponse'
* 500:
* description: 서버 내부 오류
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ErrorResponse'
*/
router.post('/login', authController.login);
/**
* 토큰 갱신
*/
router.post('/refresh-token', async (req, res) => {
try {
const { refreshToken } = req.body;
if (!refreshToken) {
return res.status(401).json({ error: '리프레시 토큰이 필요합니다.' });
}
// 리프레시 토큰 검증
const decoded = jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET || process.env.JWT_SECRET);
if (decoded.type !== 'refresh') {
return res.status(401).json({ error: '유효하지 않은 토큰입니다.' });
}
const connection = await mysql.createConnection(dbConfig);
// 사용자 정보 조회
const [users] = await connection.execute(
'SELECT * FROM Users WHERE user_id = ? AND is_active = TRUE',
[decoded.user_id]
);
await connection.end();
if (users.length === 0) {
return res.status(404).json({ error: '사용자를 찾을 수 없습니다.' });
}
const user = users[0];
// 새 토큰 발급
const newToken = jwt.sign(
{
user_id: user.user_id,
username: user.username,
access_level: user.access_level,
worker_id: user.worker_id,
name: user.name || user.username
},
process.env.JWT_SECRET || 'your-secret-key',
{ expiresIn: process.env.JWT_EXPIRES_IN || '24h' }
);
const newRefreshToken = jwt.sign(
{
user_id: user.user_id,
type: 'refresh'
},
process.env.JWT_REFRESH_SECRET || process.env.JWT_SECRET,
{ expiresIn: process.env.JWT_REFRESH_EXPIRES_IN || '7d' }
);
res.json({
success: true,
token: newToken,
refreshToken: newRefreshToken
});
} catch (error) {
if (error.name === 'TokenExpiredError') {
return res.status(401).json({ error: '리프레시 토큰이 만료되었습니다.' });
}
console.error('Token refresh error:', error);
res.status(500).json({ error: '서버 오류가 발생했습니다.' });
}
});
/**
* 본인 비밀번호 변경
*/
router.post('/change-password', verifyToken, async (req, res) => {
let connection;
try {
const { currentPassword, newPassword } = req.body;
const userId = req.user.user_id;
// 입력값 검증
if (!currentPassword || !newPassword) {
return res.status(400).json({
success: false,
error: '현재 비밀번호와 새 비밀번호를 입력해주세요.'
});
}
// 비밀번호 강도 검증 (12자 이상, 대/소문자, 숫자, 특수문자 필수)
const passwordValidation = validatePassword(newPassword);
if (!passwordValidation.valid) {
return res.status(400).json({
success: false,
error: '비밀번호가 보안 요구사항을 충족하지 않습니다.',
details: passwordValidation.errors,
code: 'WEAK_PASSWORD'
});
}
connection = await mysql.createConnection(dbConfig);
// 현재 사용자의 비밀번호 조회
const [users] = await connection.execute(
'SELECT password FROM Users WHERE user_id = ?',
[userId]
);
if (users.length === 0) {
return res.status(404).json({
success: false,
error: '사용자를 찾을 수 없습니다.'
});
}
// 현재 비밀번호 확인
const isValidPassword = await bcrypt.compare(currentPassword, users[0].password);
if (!isValidPassword) {
console.log(`[비밀번호 변경 실패] 현재 비밀번호 불일치 - 사용자: ${req.user.username}`);
return res.status(401).json({
success: false,
error: '현재 비밀번호가 올바르지 않습니다.'
});
}
// 새 비밀번호와 현재 비밀번호 동일 여부 확인
const isSamePassword = await bcrypt.compare(newPassword, users[0].password);
if (isSamePassword) {
return res.status(400).json({
success: false,
error: '새 비밀번호는 현재 비밀번호와 달라야 합니다.'
});
}
// 새 비밀번호 해시화
const hashedPassword = await bcrypt.hash(newPassword, 10);
// 비밀번호 업데이트
await connection.execute(
'UPDATE Users SET password = ?, password_changed_at = NOW(), updated_at = NOW() WHERE user_id = ?',
[hashedPassword, userId]
);
// 비밀번호 변경 로그 기록
try {
await connection.execute(
'INSERT INTO password_change_logs (user_id, changed_by_user_id, changed_at, change_type) VALUES (?, ?, NOW(), ?)',
[userId, userId, 'self']
);
} catch (logError) {
console.log('비밀번호 변경 로그 기록 실패 (테이블 없음)');
}
console.log(`[비밀번호 변경 성공] 사용자: ${req.user.username}`);
res.json({
success: true,
message: '비밀번호가 성공적으로 변경되었습니다.'
});
} catch (error) {
console.error('Change password error:', error);
res.status(500).json({
success: false,
error: '서버 오류가 발생했습니다.'
});
} finally {
if (connection) {
await connection.end();
}
}
});
/**
* 시스템 관리자의 타인 비밀번호 변경
*/
router.post('/admin/change-password', verifyToken, async (req, res) => {
let connection;
try {
const { userId, newPassword } = req.body;
const adminId = req.user.user_id;
// 권한 확인 (system 레벨만 허용)
if (req.user.access_level !== 'system') {
return res.status(403).json({
success: false,
error: '시스템 관리자만 사용할 수 있는 기능입니다.'
});
}
// 입력값 검증
if (!userId || !newPassword) {
return res.status(400).json({
success: false,
error: '사용자 ID와 새 비밀번호를 입력해주세요.'
});
}
// 비밀번호 강도 검증 (12자 이상, 대/소문자, 숫자, 특수문자 필수)
const passwordValidation = validatePassword(newPassword);
if (!passwordValidation.valid) {
return res.status(400).json({
success: false,
error: '비밀번호가 보안 요구사항을 충족하지 않습니다.',
details: passwordValidation.errors,
code: 'WEAK_PASSWORD'
});
}
connection = await mysql.createConnection(dbConfig);
// 대상 사용자 확인
const [users] = await connection.execute(
'SELECT username, name FROM Users WHERE user_id = ?',
[userId]
);
if (users.length === 0) {
return res.status(404).json({
success: false,
error: '대상 사용자를 찾을 수 없습니다.'
});
}
const targetUser = users[0];
// 자기 자신의 비밀번호는 이 API로 변경 불가
if (parseInt(userId) === adminId) {
return res.status(400).json({
success: false,
error: '본인의 비밀번호는 일반 비밀번호 변경 기능을 사용하세요.'
});
}
// 새 비밀번호 해시화
const hashedPassword = await bcrypt.hash(newPassword, 10);
// 비밀번호 업데이트
await connection.execute(
'UPDATE Users SET password = ?, password_changed_at = NOW(), updated_at = NOW(), failed_login_attempts = 0, locked_until = NULL WHERE user_id = ?',
[hashedPassword, userId]
);
// 비밀번호 변경 로그 기록
try {
await connection.execute(
'INSERT INTO password_change_logs (user_id, changed_by_user_id, changed_at, change_type) VALUES (?, ?, NOW(), ?)',
[userId, adminId, 'admin']
);
} catch (logError) {
console.log('비밀번호 변경 로그 기록 실패 (테이블 없음)');
}
console.log(`[관리자 비밀번호 변경] 대상: ${targetUser.username} - 관리자: ${req.user.username}`);
res.json({
success: true,
message: `${targetUser.name}(${targetUser.username})님의 비밀번호가 변경되었습니다.`
});
} catch (error) {
console.error('Admin change password error:', error);
res.status(500).json({
success: false,
error: '서버 오류가 발생했습니다.'
});
} finally {
if (connection) {
await connection.end();
}
}
});
/**
* 비밀번호 강도 체크
*/
router.post('/check-password-strength', (req, res) => {
const { password } = req.body;
if (!password) {
return res.json({ strength: 0, message: '비밀번호를 입력하세요.' });
}
let strength = 0;
const feedback = [];
// 길이 체크
if (password.length >= 8) strength += 1;
if (password.length >= 12) strength += 1;
else feedback.push('12자 이상 사용을 권장합니다.');
// 대문자 포함
if (/[A-Z]/.test(password)) strength += 1;
else feedback.push('대문자를 포함하세요.');
// 소문자 포함
if (/[a-z]/.test(password)) strength += 1;
else feedback.push('소문자를 포함하세요.');
// 숫자 포함
if (/\d/.test(password)) strength += 1;
else feedback.push('숫자를 포함하세요.');
// 특수문자 포함
if (/[!@#$%^&*(),.?":{}|<>]/.test(password)) strength += 1;
else feedback.push('특수문자를 포함하세요.');
const strengthLevels = ['매우 약함', '약함', '보통', '강함', '매우 강함'];
const level = Math.min(Math.floor((strength / 6) * 5), 4);
res.json({
strength: level,
strengthText: strengthLevels[level],
score: strength,
maxScore: 6,
feedback: feedback
});
});
/**
* 현재 사용자 정보 조회
*/
router.get('/me', verifyToken, async (req, res) => {
let connection;
try {
const userId = req.user.user_id;
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 = ?',
[userId]
);
if (rows.length === 0) {
return res.status(404).json({ error: '사용자를 찾을 수 없습니다.' });
}
const user = rows[0];
res.json({
user_id: user.user_id,
username: user.username,
name: user.name || user.username,
email: user.email,
access_level: user.access_level,
worker_id: user.worker_id,
last_login_at: user.last_login_at,
created_at: user.created_at
});
} catch (error) {
console.error('Get current user error:', error);
res.status(500).json({ error: '서버 오류가 발생했습니다.' });
} finally {
if (connection) {
await connection.end();
}
}
});
/**
* 사용자 등록
*/
router.post('/register', verifyToken, async (req, res) => {
let connection;
try {
const { username, password, name, email, access_level, worker_id } = req.body;
// 권한 확인 (admin 이상만 사용자 등록 가능)
if (!['admin', 'system'].includes(req.user.access_level)) {
return res.status(403).json({
success: false,
error: '사용자 등록 권한이 없습니다.'
});
}
if (!username || !password || !name || !access_level) {
return res.status(400).json({
success: false,
error: '필수 항목을 모두 입력해주세요.'
});
}
// 비밀번호 강도 검증
if (password.length < 6) {
return res.status(400).json({
success: false,
error: '비밀번호는 최소 6자 이상이어야 합니다.'
});
}
connection = await mysql.createConnection(dbConfig);
// 사용자명 중복 체크
const [existing] = await connection.execute(
'SELECT user_id FROM Users WHERE username = ?',
[username]
);
if (existing.length > 0) {
return res.status(409).json({
success: false,
error: '이미 존재하는 사용자명입니다.'
});
}
// 이메일 중복 체크 (이메일이 제공된 경우)
if (email) {
const [existingEmail] = await connection.execute(
'SELECT user_id FROM Users WHERE email = ?',
[email]
);
if (existingEmail.length > 0) {
return res.status(409).json({
success: false,
error: '이미 사용 중인 이메일입니다.'
});
}
}
// 비밀번호 해시
const hashedPassword = await bcrypt.hash(password, 10);
// 사용자 추가
const [result] = await connection.execute(
`INSERT INTO Users (username, password, name, email, access_level, worker_id, is_active, created_at, password_changed_at)
VALUES (?, ?, ?, ?, ?, ?, TRUE, NOW(), NOW())`,
[username, hashedPassword, name, email || null, access_level, worker_id || null]
);
// 비밀번호 변경 로그 기록 (초기 설정)
try {
await connection.execute(
'INSERT INTO password_change_logs (user_id, changed_by_user_id, changed_at, change_type) VALUES (?, ?, NOW(), ?)',
[result.insertId, req.user.user_id, 'initial']
);
} catch (logError) {
console.log('비밀번호 변경 로그 기록 실패 (테이블 없음)');
}
console.log(`[사용자 등록] 새 사용자: ${username} (${access_level}) - 등록자: ${req.user.username}`);
res.json({
success: true,
message: '사용자가 성공적으로 등록되었습니다.',
user: {
user_id: result.insertId,
username,
name,
email: email || null,
access_level,
worker_id: worker_id || null
}
});
} catch (error) {
console.error('Register error:', error);
res.status(500).json({
success: false,
error: '서버 오류가 발생했습니다.'
});
} finally {
if (connection) {
await connection.end();
}
}
});
/**
* 사용자 목록 조회
*/
router.get('/users', verifyToken, async (req, res) => {
let connection;
try {
connection = await mysql.createConnection(dbConfig);
// 기본 쿼리 (role 테이블과 JOIN)
let query = `
SELECT
u.user_id,
u.username,
u.name,
u.email,
u.role_id,
r.name as role_name,
u._access_level_old as access_level,
u.worker_id,
u.is_active,
u.last_login_at,
u.created_at
FROM users u
LEFT JOIN roles r ON u.role_id = r.id
WHERE 1=1
`;
const params = [];
// 필터링 옵션
if (req.query.active !== undefined) {
query += ' AND u.is_active = ?';
params.push(req.query.active === 'true');
}
// role_name으로 필터링 (access_level 대신)
if (req.query.access_level) {
query += ' AND (u._access_level_old = ? OR r.name = ?)';
params.push(req.query.access_level, req.query.access_level);
}
query += ' ORDER BY u.created_at DESC';
const [rows] = await connection.execute(query, params);
const userList = rows.map(user => ({
user_id: user.user_id,
username: user.username,
name: user.name || user.username,
email: user.email,
role_id: user.role_id,
role_name: user.role_name,
access_level: user.access_level || user.role_name?.toLowerCase(), // 하위 호환성
worker_id: user.worker_id,
is_active: user.is_active,
last_login_at: user.last_login_at,
created_at: user.created_at
}));
res.json(userList);
} catch (error) {
console.error('Get users error:', error);
res.status(500).json({ error: '서버 오류가 발생했습니다.' });
} finally {
if (connection) {
await connection.end();
}
}
});
/**
* 사용자 정보 수정
*/
router.put('/users/:id', verifyToken, async (req, res) => {
let connection;
try {
const userId = parseInt(req.params.id);
const { name, email, access_level, worker_id, password, is_active } = req.body;
// 권한 확인
if (!['admin', 'system'].includes(req.user.access_level)) {
// 일반 사용자는 자신의 정보만 수정 가능 (이름, 이메일만)
if (userId !== req.user.user_id) {
return res.status(403).json({
success: false,
error: '다른 사용자의 정보를 수정할 권한이 없습니다.'
});
}
if (access_level || worker_id || is_active !== undefined) {
return res.status(403).json({
success: false,
error: '권한, 작업자 ID, 활성화 상태는 관리자만 수정할 수 있습니다.'
});
}
}
connection = await mysql.createConnection(dbConfig);
// 사용자 존재 확인
const [existing] = await connection.execute(
'SELECT user_id, username FROM Users WHERE user_id = ?',
[userId]
);
if (existing.length === 0) {
return res.status(404).json({
success: false,
error: '사용자를 찾을 수 없습니다.'
});
}
// 업데이트할 필드들 준비
let updateFields = [];
let updateValues = [];
if (name !== undefined) {
updateFields.push('name = ?');
updateValues.push(name);
}
if (email !== undefined) {
// 이메일 중복 체크
if (email) {
const [emailCheck] = await connection.execute(
'SELECT user_id FROM Users WHERE email = ? AND user_id != ?',
[email, userId]
);
if (emailCheck.length > 0) {
return res.status(409).json({
success: false,
error: '이미 사용 중인 이메일입니다.'
});
}
}
updateFields.push('email = ?');
updateValues.push(email || null);
}
if (access_level !== undefined && ['admin', 'system'].includes(req.user.access_level)) {
updateFields.push('access_level = ?');
updateValues.push(access_level);
}
if (worker_id !== undefined && ['admin', 'system'].includes(req.user.access_level)) {
updateFields.push('worker_id = ?');
updateValues.push(worker_id || null);
}
if (is_active !== undefined && ['admin', 'system'].includes(req.user.access_level)) {
updateFields.push('is_active = ?');
updateValues.push(is_active);
}
if (password) {
if (password.length < 6) {
return res.status(400).json({
success: false,
error: '비밀번호는 최소 6자 이상이어야 합니다.'
});
}
updateFields.push('password = ?');
updateValues.push(await bcrypt.hash(password, 10));
updateFields.push('password_changed_at = NOW()');
// 비밀번호 변경 로그
try {
await connection.execute(
'INSERT INTO password_change_logs (user_id, changed_by_user_id, changed_at, change_type) VALUES (?, ?, NOW(), ?)',
[userId, req.user.user_id, userId === req.user.user_id ? 'self' : 'admin']
);
} catch (logError) {
console.log('비밀번호 변경 로그 기록 실패');
}
}
if (updateFields.length === 0) {
return res.status(400).json({
success: false,
error: '수정할 내용이 없습니다.'
});
}
updateFields.push('updated_at = NOW()');
updateValues.push(userId);
// 업데이트 실행
await connection.execute(
`UPDATE Users SET ${updateFields.join(', ')} WHERE user_id = ?`,
updateValues
);
// 업데이트된 사용자 정보 조회
const [updated] = await connection.execute(
'SELECT user_id, username, name, email, access_level, worker_id, is_active FROM Users WHERE user_id = ?',
[userId]
);
console.log(`[사용자 수정] 대상: ${existing[0].username} - 수정자: ${req.user.username}`);
res.json({
success: true,
message: '사용자 정보가 성공적으로 수정되었습니다.',
user: updated[0]
});
} catch (error) {
console.error('Update user error:', error);
res.status(500).json({
success: false,
error: '서버 오류가 발생했습니다.'
});
} finally {
if (connection) {
await connection.end();
}
}
});
/**
* 사용자 삭제
*/
router.delete('/users/:id', verifyToken, async (req, res) => {
let connection;
try {
const userId = parseInt(req.params.id);
// 권한 확인 (system만 삭제 가능)
if (req.user.access_level !== 'system') {
return res.status(403).json({
success: false,
error: '사용자 삭제 권한이 없습니다.'
});
}
// 자기 자신 삭제 방지
if (userId === req.user.user_id) {
return res.status(400).json({
success: false,
error: '자기 자신은 삭제할 수 없습니다.'
});
}
connection = await mysql.createConnection(dbConfig);
// 사용자 존재 확인
const [existing] = await connection.execute(
'SELECT username FROM users WHERE user_id = ?',
[userId]
);
if (existing.length === 0) {
return res.status(404).json({
success: false,
error: '사용자를 찾을 수 없습니다.'
});
}
// 소프트 삭제 (실제로는 비활성화)
await connection.execute(
'UPDATE users SET is_active = FALSE, updated_at = NOW() WHERE user_id = ?',
[userId]
);
// 또는 하드 삭제 (실제로 삭제)
// await connection.execute('DELETE FROM users WHERE user_id = ?', [userId]);
console.log(`[사용자 삭제] 대상: ${existing[0].username} - 삭제자: ${req.user.username}`);
res.json({
success: true,
message: '사용자가 성공적으로 삭제되었습니다.'
});
} catch (error) {
console.error('Delete user error:', error);
res.status(500).json({
success: false,
error: '서버 오류가 발생했습니다.'
});
} finally {
if (connection) {
await connection.end();
}
}
});
/**
* 로그아웃
*/
router.post('/logout', verifyToken, async (req, res) => {
console.log(`[로그아웃] 사용자: ${req.user.username}`);
// 로그아웃 시간 기록 (선택사항)
let connection;
try {
connection = await mysql.createConnection(dbConfig);
await connection.execute(
'UPDATE login_logs SET logout_time = NOW() WHERE user_id = ? AND logout_time IS NULL ORDER BY login_time DESC LIMIT 1',
[req.user.user_id]
);
} catch (error) {
console.error('로그아웃 기록 실패:', error);
} finally {
if (connection) {
await connection.end();
}
}
res.json({
success: true,
message: '성공적으로 로그아웃되었습니다.'
});
});
/**
* 로그인 이력 조회
*/
router.get('/login-history', verifyToken, async (req, res) => {
let connection;
try {
const { limit = 50, offset = 0 } = req.query;
const userId = req.user.user_id;
connection = await mysql.createConnection(dbConfig);
// 본인의 로그인 이력만 조회 (관리자는 전체 조회 가능)
let query = `
SELECT
log_id,
user_id,
login_time,
logout_time,
ip_address,
user_agent,
login_status
FROM login_logs
`;
const params = [];
if (!['admin', 'system'].includes(req.user.access_level)) {
query += ' WHERE user_id = ?';
params.push(userId);
} else if (req.query.user_id) {
query += ' WHERE user_id = ?';
params.push(req.query.user_id);
}
query += ' ORDER BY login_time DESC LIMIT ? OFFSET ?';
params.push(parseInt(limit), parseInt(offset));
const [rows] = await connection.execute(query, params);
res.json({
success: true,
data: rows,
pagination: {
limit: parseInt(limit),
offset: parseInt(offset)
}
});
} catch (error) {
console.error('Get login history error:', error);
res.status(500).json({ error: '서버 오류가 발생했습니다.' });
} finally {
if (connection) {
await connection.end();
}
}
});
module.exports = router;

View File

@@ -0,0 +1,20 @@
// routes/dailyIssueReportRoutes.js
const express = require('express');
const router = express.Router();
const {
createDailyIssueReport,
getDailyIssuesByDate,
removeDailyIssue
} = require('../controllers/dailyIssueReportController');
// 1. 등록 (단일 또는 다중)
router.post('/', createDailyIssueReport);
// 2. 날짜별 조회 (?date=YYYY-MM-DD 형식)
router.get('/', getDailyIssuesByDate);
// 3. 삭제
router.delete('/:id', removeDailyIssue);
module.exports = router;

View File

@@ -0,0 +1,97 @@
// routes/dailyWorkReportRoutes.js - 누적입력 방식 + 모든 기존 기능 포함
const express = require('express');
const router = express.Router();
const dailyWorkReportController = require('../controllers/dailyWorkReportController');
const workReportController = require('../controllers/workReportController');
// 📋 마스터 데이터 조회 라우트들 (모든 인증된 사용자)
router.get('/work-types', dailyWorkReportController.getWorkTypes);
router.get('/work-status-types', dailyWorkReportController.getWorkStatusTypes);
router.get('/error-types', dailyWorkReportController.getErrorTypes);
// 📝 마스터 데이터 CRUD 라우트들 (관리자만)
// 작업 유형 CRUD
router.post('/work-types', dailyWorkReportController.createWorkType);
router.put('/work-types/:id', dailyWorkReportController.updateWorkType);
router.delete('/work-types/:id', dailyWorkReportController.deleteWorkType);
// 작업 상태 CRUD
router.post('/work-status-types', dailyWorkReportController.createWorkStatus);
router.put('/work-status-types/:id', dailyWorkReportController.updateWorkStatus);
router.delete('/work-status-types/:id', dailyWorkReportController.deleteWorkStatus);
// 오류 유형 CRUD
router.post('/error-types', dailyWorkReportController.createErrorType);
router.put('/error-types/:id', dailyWorkReportController.updateErrorType);
router.delete('/error-types/:id', dailyWorkReportController.deleteErrorType);
// 🔄 누적 관련 새로운 라우트들 (누적입력 시스템 전용)
router.get('/accumulated', dailyWorkReportController.getAccumulatedReports); // ?date=2024-06-16&worker_id=1
router.get('/contributors', dailyWorkReportController.getContributorsSummary); // ?date=2024-06-16&worker_id=1
router.get('/my-data', dailyWorkReportController.getMyAccumulatedData); // ?date=2024-06-16&worker_id=1
// ✅ check-overwrite 엔드포인트 추가 (누락된 엔드포인트)
router.get('/check-overwrite', (req, res) => {
const { date, worker_id } = req.query;
if (!date || !worker_id) {
return res.status(400).json({
error: 'date와 worker_id가 필요합니다.',
example: 'date=2025-06-16&worker_id=1'
});
}
console.log(`🔍 덮어쓰기 권한 확인: 날짜=${date}, 작업자=${worker_id} (누적입력모드)`);
// 누적입력 시스템에서는 항상 덮어쓰기 가능 (실제로는 누적만 함)
res.json({
canOverwrite: true,
reason: 'accumulate_mode',
message: '누적입력 모드에서는 항상 추가 가능합니다.',
date,
worker_id,
timestamp: new Date().toISOString()
});
});
router.delete('/my-entry/:id', dailyWorkReportController.removeMyEntry); // 개별 항목 삭제 (본인 것만)
// 📅 월간 요약 (반드시 다른 라우트보다 먼저 정의)
router.get('/summary/monthly', dailyWorkReportController.getMonthlySummary);
// 📊 일일 근무 요약 조회
router.get('/summary', dailyWorkReportController.getDailySummary);
// 🔍 검색 (페이지네이션 포함)
router.get('/search', dailyWorkReportController.searchWorkReports);
// 📈 통계
router.get('/stats', dailyWorkReportController.getWorkReportStats);
// 📝 일일 작업보고서 생성 (누적 방식 - 덮어쓰기 없음!)
router.post('/', dailyWorkReportController.createDailyWorkReport);
// 📝 TBM 기반 작업보고서 생성
router.post('/from-tbm', dailyWorkReportController.createFromTbm);
// 📊 일일 작업보고서 조회 (날짜별 - 경로 파라미터)
router.get('/date/:date', dailyWorkReportController.getDailyWorkReportsByDate);
// 📊 일일 작업보고서 조회 (쿼리 파라미터 기반 - 작성자별 필터링)
router.get('/', dailyWorkReportController.getDailyWorkReports);
// ✏️ 작업보고서 수정
router.put('/:id', dailyWorkReportController.updateWorkReport);
// 🗑️ 작업자의 특정 날짜 전체 삭제
router.delete('/date/:date/worker/:worker_id', dailyWorkReportController.removeDailyWorkReportByDateAndWorker);
// 🗑️ 특정 작업보고서 삭제 (항상 가장 마지막에 정의)
router.delete('/:id', dailyWorkReportController.removeDailyWorkReport);
// 📋 부적합 관리 (workReportController 사용)
router.get('/:reportId/defects', workReportController.getReportDefects);
router.put('/:reportId/defects', workReportController.saveReportDefects);
router.post('/:reportId/defects', workReportController.addReportDefect);
module.exports = router;

View File

@@ -0,0 +1,31 @@
// routes/departmentRoutes.js
const express = require('express');
const router = express.Router();
const departmentController = require('../controllers/departmentController');
const { requireAuth, requireRole } = require('../middlewares/authMiddleware');
// 부서 목록 조회 (인증 필요)
router.get('/', requireAuth, departmentController.getAll);
// 부서 상세 조회
router.get('/:id', requireAuth, departmentController.getById);
// 부서별 작업자 조회
router.get('/:id/workers', requireAuth, departmentController.getWorkers);
// 부서 생성 (관리자만)
router.post('/', requireAuth, requireRole(['Admin', 'System Admin']), departmentController.create);
// 부서 수정 (관리자만)
router.put('/:id', requireAuth, requireRole(['Admin', 'System Admin']), departmentController.update);
// 부서 삭제 (관리자만)
router.delete('/:id', requireAuth, requireRole(['Admin', 'System Admin']), departmentController.delete);
// 작업자 부서 이동 (관리자만)
router.post('/move-worker', requireAuth, requireRole(['Admin', 'System Admin']), departmentController.moveWorker);
// 여러 작업자 부서 일괄 이동 (관리자만)
router.post('/move-workers', requireAuth, requireRole(['Admin', 'System Admin']), departmentController.moveWorkers);
module.exports = router;

View File

@@ -0,0 +1,103 @@
// routes/equipmentRoutes.js
const express = require('express');
const router = express.Router();
const equipmentController = require('../controllers/equipmentController');
// ==================== 설비 관리 ====================
// CREATE 설비
router.post('/', equipmentController.createEquipment);
// READ ALL 설비 (쿼리 파라미터로 필터링 가능)
// ?workplace_id=1&equipment_type=CNC&status=active&search=설비명
router.get('/', equipmentController.getAllEquipments);
// READ ACTIVE 설비
router.get('/active/list', equipmentController.getActiveEquipments);
// READ 설비 유형 목록
router.get('/types', equipmentController.getEquipmentTypes);
// GET NEXT EQUIPMENT CODE - 다음 관리번호 자동 생성
// ?prefix=TKP (기본값: TKP)
router.get('/next-code', equipmentController.getNextEquipmentCode);
// READ 작업장별 설비
router.get('/workplace/:workplaceId', equipmentController.getEquipmentsByWorkplace);
// ==================== 임시 이동 (목록) ====================
// 임시 이동된 설비 목록
router.get('/moved/list', equipmentController.getTemporarilyMoved);
// ==================== 외부 반출 (목록) ====================
// 현재 외부 반출 중인 설비 목록
router.get('/exported/list', equipmentController.getExportedEquipments);
// 반출 로그 반입 처리
router.post('/external-logs/:logId/return', equipmentController.returnEquipment);
// ==================== 수리 ====================
// 수리 항목 목록 조회
router.get('/repair-categories', equipmentController.getRepairCategories);
// 새 수리 항목 추가
router.post('/repair-categories', equipmentController.addRepairCategory);
// ==================== 사진 관리 ====================
// 사진 삭제 (설비 ID 없이 photo_id만으로)
router.delete('/photos/:photoId', equipmentController.deletePhoto);
// ==================== 개별 설비 ====================
// READ ONE 설비
router.get('/:id', equipmentController.getEquipmentById);
// UPDATE 설비
router.put('/:id', equipmentController.updateEquipment);
// UPDATE 설비 지도 위치
router.patch('/:id/map-position', equipmentController.updateMapPosition);
// DELETE 설비
router.delete('/:id', equipmentController.deleteEquipment);
// ==================== 설비 사진 ====================
// 설비 사진 추가
router.post('/:id/photos', equipmentController.addPhoto);
// 설비 사진 목록
router.get('/:id/photos', equipmentController.getPhotos);
// ==================== 설비 임시 이동 ====================
// 설비 임시 이동
router.post('/:id/move', equipmentController.moveTemporarily);
// 설비 원위치 복귀
router.post('/:id/return', equipmentController.returnToOriginal);
// 설비 이동 이력
router.get('/:id/move-logs', equipmentController.getMoveLogs);
// ==================== 설비 외부 반출 ====================
// 설비 외부 반출
router.post('/:id/export', equipmentController.exportEquipment);
// 설비 외부 반출 이력
router.get('/:id/external-logs', equipmentController.getExternalLogs);
// ==================== 설비 수리 ====================
// 수리 신청
router.post('/:id/repair-request', equipmentController.createRepairRequest);
// 수리 이력 조회
router.get('/:id/repair-history', equipmentController.getRepairHistory);
module.exports = router;

View File

@@ -0,0 +1,123 @@
// routes/healthRoutes.js
const express = require('express');
const router = express.Router();
// 헬스체크 엔드포인트
router.get('/', (req, res) => {
res.json({
status: 'healthy',
service: 'Technical Korea API',
timestamp: new Date().toISOString(),
uptime: process.uptime()
});
});
// 상세 헬스체크 (옵션)
router.get('/detail', (req, res) => {
res.json({
status: 'healthy',
service: 'Technical Korea API',
version: '2.1.0',
timestamp: new Date().toISOString(),
uptime: process.uptime(),
memory: process.memoryUsage(),
environment: process.env.NODE_ENV || 'development'
});
});
// 임시 마이그레이션 엔드포인트 - TBM work_type_id 수정
// 실행 후 이 코드를 삭제하세요!
router.post('/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 }
});
}
// 수정 전 샘플 로깅
console.log('수정 전 샘플:', checkResult.slice(0, 5));
// 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
});
}
});
module.exports = router;

View File

@@ -0,0 +1,10 @@
const express = require('express');
const router = express.Router();
const issueTypeController = require('../controllers/issueTypeController');
router.post('/', issueTypeController.createIssueType);
router.get('/', issueTypeController.getAllIssueTypes);
router.put('/:id', issueTypeController.updateIssueType);
router.delete('/:id', issueTypeController.removeIssueType);
module.exports = router;

View File

@@ -0,0 +1,24 @@
// routes/monthlyStatusRoutes.js
// 월별 작업자 상태 집계 라우트
const express = require('express');
const router = express.Router();
const MonthlyStatusController = require('../controllers/monthlyStatusController');
const { verifyToken } = require('../middlewares/authMiddleware');
// 모든 라우트에 인증 미들웨어 적용 (임시로 주석 처리 - 테스트용)
// router.use(verifyToken);
// 월별 캘린더 데이터 조회 (캘린더 페이지용)
router.get('/calendar', MonthlyStatusController.getMonthlyCalendarData);
// 특정 날짜의 작업자별 상세 상태 조회 (모달용)
router.get('/daily-details', MonthlyStatusController.getDailyWorkerDetails);
// 월별 집계 재계산 (관리자용)
router.post('/recalculate', MonthlyStatusController.recalculateMonth);
// 집계 테이블 상태 확인 (관리자용)
router.get('/status', MonthlyStatusController.getStatusInfo);
module.exports = router;

View File

@@ -0,0 +1,28 @@
// routes/notificationRecipientRoutes.js
const express = require('express');
const router = express.Router();
const notificationRecipientController = require('../controllers/notificationRecipientController');
const { verifyToken, requireMinLevel } = require('../middlewares/authMiddleware');
// 모든 라우트에 인증 필요
router.use(verifyToken);
// 알림 유형 목록
router.get('/types', notificationRecipientController.getTypes);
// 전체 수신자 목록 (유형별 그룹화)
router.get('/', notificationRecipientController.getAll);
// 유형별 수신자 조회
router.get('/:type', notificationRecipientController.getByType);
// 수신자 추가 (관리자만)
router.post('/', requireMinLevel('admin'), notificationRecipientController.add);
// 유형별 수신자 일괄 설정 (관리자만)
router.put('/:type', requireMinLevel('admin'), notificationRecipientController.setRecipients);
// 수신자 제거 (관리자만)
router.delete('/:type/:userId', requireMinLevel('admin'), notificationRecipientController.remove);
module.exports = router;

View File

@@ -0,0 +1,31 @@
// routes/notificationRoutes.js
const express = require('express');
const router = express.Router();
const notificationController = require('../controllers/notificationController');
const { requireAuth, requireMinLevel } = require('../middlewares/auth');
// 모든 알림 라우트는 인증 필요
router.use(requireAuth);
// 읽지 않은 알림 조회 (본인 알림만)
router.get('/unread', notificationController.getUnread);
// 읽지 않은 알림 개수
router.get('/unread/count', notificationController.getUnreadCount);
// 전체 알림 조회 (페이징)
router.get('/', notificationController.getAll);
// 알림 생성 (시스템/관리자용)
router.post('/', requireMinLevel('support_team'), notificationController.create);
// 모든 알림 읽음 처리 (본인 알림만)
router.post('/read-all', notificationController.markAllAsRead);
// 특정 알림 읽음 처리 (본인 알림만)
router.post('/:id/read', notificationController.markAsRead);
// 알림 삭제 (본인 알림만)
router.delete('/:id', notificationController.delete);
module.exports = router;

View File

@@ -0,0 +1,237 @@
const express = require('express');
const router = express.Router();
const { getDb } = require('../dbPool');
const { requireAuth, requireAdmin } = require('../middlewares/auth');
/**
* 모든 페이지 목록 조회
* GET /api/pages
*/
router.get('/pages', requireAuth, async (req, res) => {
try {
const db = await getDb();
const [pages] = await db.query(`
SELECT id, page_key, page_name, page_path, category, description, is_admin_only, display_order
FROM pages
ORDER BY display_order, page_name
`);
res.json({ success: true, data: pages });
} catch (error) {
console.error('페이지 목록 조회 오류:', error);
res.status(500).json({ success: false, error: '페이지 목록을 불러오는데 실패했습니다.' });
}
});
/**
* 특정 사용자의 페이지 접근 권한 조회
* GET /api/users/:userId/page-access
*/
router.get('/users/:userId/page-access', requireAuth, async (req, res) => {
try {
const { userId } = req.params;
const db = await getDb();
// 사용자의 역할 확인
const [userRows] = await db.query(`
SELECT u.user_id, u.username, u.role_id, r.name as role_name
FROM users u
LEFT JOIN roles r ON u.role_id = r.id
WHERE u.user_id = ?
`, [userId]);
if (userRows.length === 0) {
return res.status(404).json({ success: false, error: '사용자를 찾을 수 없습니다.' });
}
const user = userRows[0];
// Admin/System Admin인 경우 모든 페이지 접근 가능
if (user.role_name === 'Admin' || user.role_name === 'System Admin') {
const [allPages] = await db.query(`
SELECT id, page_key, page_name, page_path, category, is_admin_only
FROM pages
ORDER BY display_order, page_name
`);
const pageAccess = allPages.map(page => ({
page_id: page.id,
page_key: page.page_key,
page_name: page.page_name,
page_path: page.page_path,
category: page.category,
is_admin_only: page.is_admin_only,
can_access: true,
is_default: true // Admin은 기본적으로 모든 권한 보유
}));
return res.json({ success: true, data: { user, pageAccess } });
}
// 일반 사용자의 페이지 접근 권한 조회
const [pageAccess] = await db.query(`
SELECT
p.id as page_id,
p.page_key,
p.page_name,
p.page_path,
p.category,
p.is_admin_only,
COALESCE(upa.can_access, 0) as can_access,
upa.granted_at,
u2.username as granted_by_username
FROM pages p
LEFT JOIN user_page_access upa ON p.id = upa.page_id AND upa.user_id = ?
LEFT JOIN users u2 ON upa.granted_by = u2.user_id
WHERE p.is_admin_only = 0
ORDER BY p.display_order, p.page_name
`, [userId]);
res.json({ success: true, data: { user, pageAccess } });
} catch (error) {
console.error('페이지 접근 권한 조회 오류:', error);
res.status(500).json({ success: false, error: '페이지 접근 권한을 불러오는데 실패했습니다.' });
}
});
/**
* 사용자에게 페이지 접근 권한 부여/회수
* POST /api/users/:userId/page-access
* Body: { pageIds: [1, 2, 3], canAccess: true }
*/
router.post('/users/:userId/page-access', requireAuth, async (req, res) => {
try {
const { userId } = req.params;
const { pageIds, canAccess } = req.body;
const adminUserId = req.user.user_id; // 권한을 부여하는 Admin의 user_id
// Admin 권한 확인
const db = await getDb();
const [adminRows] = await db.query(`
SELECT u.role_id, r.name as role_name
FROM users u
LEFT JOIN roles r ON u.role_id = r.id
WHERE u.user_id = ?
`, [adminUserId]);
if (adminRows.length === 0 || (adminRows[0].role_name !== 'Admin' && adminRows[0].role_name !== 'System Admin')) {
return res.status(403).json({ success: false, error: '권한이 없습니다. Admin 계정만 사용자 권한을 관리할 수 있습니다.' });
}
// 사용자 존재 확인
const [userRows] = await db.query('SELECT user_id FROM users WHERE user_id = ?', [userId]);
if (userRows.length === 0) {
return res.status(404).json({ success: false, error: '사용자를 찾을 수 없습니다.' });
}
// 페이지 접근 권한 업데이트
for (const pageId of pageIds) {
// 기존 권한 확인
const [existing] = await db.query(
'SELECT * FROM user_page_access WHERE user_id = ? AND page_id = ?',
[userId, pageId]
);
if (existing.length > 0) {
// 업데이트
await db.query(
'UPDATE user_page_access SET can_access = ?, granted_at = NOW(), granted_by = ? WHERE user_id = ? AND page_id = ?',
[canAccess ? 1 : 0, adminUserId, userId, pageId]
);
} else {
// 삽입
await db.query(
'INSERT INTO user_page_access (user_id, page_id, can_access, granted_by) VALUES (?, ?, ?, ?)',
[userId, pageId, canAccess ? 1 : 0, adminUserId]
);
}
}
res.json({ success: true, message: '페이지 접근 권한이 업데이트되었습니다.' });
} catch (error) {
console.error('페이지 접근 권한 부여 오류:', error);
res.status(500).json({ success: false, error: '페이지 접근 권한을 업데이트하는데 실패했습니다.' });
}
});
/**
* 특정 페이지의 접근 권한 회수
* DELETE /api/users/:userId/page-access/:pageId
*/
router.delete('/users/:userId/page-access/:pageId', requireAuth, async (req, res) => {
try {
const { userId, pageId } = req.params;
const adminUserId = req.user.user_id;
// Admin 권한 확인
const db = await getDb();
const [adminRows] = await db.query(`
SELECT u.role_id, r.name as role_name
FROM users u
LEFT JOIN roles r ON u.role_id = r.id
WHERE u.user_id = ?
`, [adminUserId]);
if (adminRows.length === 0 || (adminRows[0].role_name !== 'Admin' && adminRows[0].role_name !== 'System Admin')) {
return res.status(403).json({ success: false, error: '권한이 없습니다.' });
}
// 접근 권한 삭제
await db.query(
'DELETE FROM user_page_access WHERE user_id = ? AND page_id = ?',
[userId, pageId]
);
res.json({ success: true, message: '페이지 접근 권한이 회수되었습니다.' });
} catch (error) {
console.error('페이지 접근 권한 회수 오류:', error);
res.status(500).json({ success: false, error: '페이지 접근 권한을 회수하는데 실패했습니다.' });
}
});
/**
* 모든 사용자의 페이지 접근 권한 요약 조회 (Admin용)
* GET /api/page-access/summary
*/
router.get('/page-access/summary', requireAuth, async (req, res) => {
try {
const adminUserId = req.user.user_id;
// Admin 권한 확인
const db = await getDb();
const [adminRows] = await db.query(`
SELECT u.role_id, r.name as role_name
FROM users u
LEFT JOIN roles r ON u.role_id = r.id
WHERE u.user_id = ?
`, [adminUserId]);
if (adminRows.length === 0 || (adminRows[0].role_name !== 'Admin' && adminRows[0].role_name !== 'System Admin')) {
return res.status(403).json({ success: false, error: '권한이 없습니다.' });
}
// 모든 사용자와 페이지 권한 조회
const [summary] = await db.query(`
SELECT
u.user_id,
u.username,
u.name,
r.name as role_name,
COUNT(DISTINCT upa.page_id) as accessible_pages_count,
(SELECT COUNT(*) FROM pages WHERE is_admin_only = 0) as total_pages_count
FROM users u
LEFT JOIN roles r ON u.role_id = r.id
LEFT JOIN user_page_access upa ON u.user_id = upa.user_id AND upa.can_access = 1
WHERE r.name NOT IN ('Admin', 'System Admin')
GROUP BY u.user_id, u.username, u.name, r.name
ORDER BY u.username
`);
res.json({ success: true, data: summary });
} catch (error) {
console.error('페이지 접근 권한 요약 조회 오류:', error);
res.status(500).json({ success: false, error: '페이지 접근 권한 요약을 불러오는데 실패했습니다.' });
}
});
module.exports = router;

View File

@@ -0,0 +1,126 @@
// patrolRoutes.js
// 일일순회점검 시스템 라우트
const express = require('express');
const router = express.Router();
const multer = require('multer');
const path = require('path');
const patrolController = require('../controllers/patrolController');
// Multer 설정 - 구역 현황 사진 업로드
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, path.join(__dirname, '../uploads'));
},
filename: (req, file, cb) => {
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
const ext = path.extname(file.originalname).toLowerCase();
cb(null, `zone-item-${uniqueSuffix}${ext}`);
}
});
const upload = multer({
storage,
limits: { fileSize: 10 * 1024 * 1024 }, // 10MB
fileFilter: (req, file, cb) => {
const allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
if (allowedTypes.includes(file.mimetype)) {
cb(null, true);
} else {
cb(new Error('이미지 파일만 업로드 가능합니다.'), false);
}
}
});
// ==================== 순회점검 세션 ====================
// 세션 목록 조회
// GET /patrol/sessions?patrol_date=2026-02-04&patrol_time=morning&category_id=1
router.get('/sessions', patrolController.getSessions);
// 세션 시작/조회 (POST로 생성하거나 기존 세션 반환)
// POST /patrol/sessions { patrol_date, patrol_time, category_id }
router.post('/sessions', patrolController.getOrCreateSession);
// 세션 상세 조회
router.get('/sessions/:sessionId', patrolController.getSession);
// 세션 완료
router.patch('/sessions/:sessionId/complete', patrolController.completeSession);
// 세션 메모 업데이트
router.patch('/sessions/:sessionId/notes', patrolController.updateSessionNotes);
// 세션별 작업장 점검 현황
router.get('/sessions/:sessionId/workplace-status', patrolController.getWorkplaceCheckStatus);
// ==================== 체크리스트 항목 ====================
// 체크리스트 항목 조회 (필터링 가능)
// GET /patrol/checklist?category_id=1&workplace_id=2
router.get('/checklist', patrolController.getChecklistItems);
// 체크리스트 항목 CRUD
router.post('/checklist', patrolController.createChecklistItem);
router.put('/checklist/:itemId', patrolController.updateChecklistItem);
router.delete('/checklist/:itemId', patrolController.deleteChecklistItem);
// ==================== 체크 기록 ====================
// 세션별 체크 기록 조회
// GET /patrol/sessions/:sessionId/records?workplace_id=1
router.get('/sessions/:sessionId/records', patrolController.getCheckRecords);
// 체크 기록 저장 (단건)
router.post('/sessions/:sessionId/records', patrolController.saveCheckRecord);
// 체크 기록 일괄 저장
router.post('/sessions/:sessionId/records/batch', patrolController.saveCheckRecords);
// ==================== 작업장 물품 현황 ====================
// 작업장별 물품 조회
router.get('/workplaces/:workplaceId/items', patrolController.getWorkplaceItems);
// 물품 CRUD
router.post('/workplaces/:workplaceId/items', patrolController.createWorkplaceItem);
router.put('/items/:itemId', patrolController.updateWorkplaceItem);
router.delete('/items/:itemId', patrolController.deleteWorkplaceItem);
// ==================== 물품 유형 ====================
// 물품 유형 목록
router.get('/item-types', patrolController.getItemTypes);
// ==================== 대시보드/통계 ====================
// 오늘 순회점검 현황
router.get('/today-status', patrolController.getTodayStatus);
// ==================== 작업장 상세 정보 ====================
// 작업장 상세 정보 조회 (시설물, 안전신고, 부적합, 출입, TBM 통합)
// GET /patrol/workplaces/:workplaceId/detail?date=2026-02-05
router.get('/workplaces/:workplaceId/detail', patrolController.getWorkplaceDetail);
// ==================== 구역 내 등록 물품/시설물 ====================
// 구역 내 등록된 물품/시설물 목록 조회
router.get('/workplaces/:workplaceId/zone-items', patrolController.getZoneItems);
// 구역 내 물품/시설물 등록
router.post('/workplaces/:workplaceId/zone-items', patrolController.createZoneItem);
// 구역 내 물품/시설물 수정
router.put('/zone-items/:itemId', patrolController.updateZoneItem);
// 구역 내 물품/시설물 삭제
router.delete('/zone-items/:itemId', patrolController.deleteZoneItem);
// 구역 현황 사진 업로드
router.post('/zone-items/photos', upload.single('photo'), patrolController.uploadZoneItemPhoto);
// 구역 현황 이력 조회
router.get('/zone-items/:itemId/history', patrolController.getZoneItemHistory);
module.exports = router;

View File

@@ -0,0 +1,388 @@
/**
* @swagger
* tags:
* name: Performance
* description: 성능 모니터링 및 최적화 API
*/
const express = require('express');
const router = express.Router();
const { asyncHandler } = require('../utils/errorHandler');
const cache = require('../utils/cache');
const { getPerformanceStats, suggestIndexes, analyzeQuery } = require('../utils/queryOptimizer');
/**
* @swagger
* /api/performance/cache/stats:
* get:
* tags: [Performance]
* summary: 캐시 통계 조회
* description: 현재 캐시 시스템의 상태와 통계를 조회합니다.
* security:
* - bearerAuth: []
* responses:
* 200:
* description: 캐시 통계 조회 성공
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* example: true
* message:
* type: string
* example: "캐시 통계 조회 성공"
* data:
* type: object
* properties:
* type:
* type: string
* example: "memory"
* connected:
* type: boolean
* example: true
* keys:
* type: integer
* example: 42
* hits:
* type: integer
* example: 150
* misses:
* type: integer
* example: 25
* hitRate:
* type: number
* example: 0.857
* 401:
* description: 인증 필요
* 500:
* description: 서버 오류
*/
router.get('/cache/stats', asyncHandler(async (req, res) => {
const stats = cache.getStats();
res.success(stats, '캐시 통계 조회 성공');
}));
/**
* @swagger
* /api/performance/cache/flush:
* post:
* tags: [Performance]
* summary: 캐시 초기화
* description: 모든 캐시 데이터를 삭제합니다. (관리자 전용)
* security:
* - bearerAuth: []
* responses:
* 200:
* description: 캐시 초기화 성공
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* example: true
* message:
* type: string
* example: "캐시가 성공적으로 초기화되었습니다."
* 401:
* description: 인증 필요
* 403:
* description: 권한 부족
* 500:
* description: 서버 오류
*/
router.post('/cache/flush', asyncHandler(async (req, res) => {
// 관리자 권한 확인
if (req.user?.access_level !== 'admin' && req.user?.access_level !== 'system') {
return res.status(403).json({
success: false,
error: '캐시 초기화 권한이 없습니다.'
});
}
await cache.flush();
res.success(null, '캐시가 성공적으로 초기화되었습니다.');
}));
/**
* @swagger
* /api/performance/database/stats:
* get:
* tags: [Performance]
* summary: 데이터베이스 성능 통계
* description: 데이터베이스 연결 상태와 성능 지표를 조회합니다.
* security:
* - bearerAuth: []
* responses:
* 200:
* description: DB 성능 통계 조회 성공
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* example: true
* message:
* type: string
* example: "DB 성능 통계 조회 성공"
* data:
* type: object
* properties:
* connections:
* type: object
* properties:
* current:
* type: integer
* example: 5
* max:
* type: integer
* example: 151
* slowQueries:
* type: integer
* example: 0
* timestamp:
* type: string
* format: date-time
* 401:
* description: 인증 필요
* 500:
* description: 서버 오류
*/
router.get('/database/stats', asyncHandler(async (req, res) => {
const stats = await getPerformanceStats();
res.success(stats, 'DB 성능 통계 조회 성공');
}));
/**
* @swagger
* /api/performance/database/indexes/{tableName}:
* get:
* tags: [Performance]
* summary: 인덱스 최적화 제안
* description: 특정 테이블의 인덱스 상태를 분석하고 최적화 제안을 제공합니다.
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: tableName
* required: true
* schema:
* type: string
* description: 분석할 테이블명
* example: "workers"
* responses:
* 200:
* description: 인덱스 분석 성공
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* example: true
* message:
* type: string
* example: "인덱스 분석 완료"
* data:
* type: object
* properties:
* tableName:
* type: string
* example: "workers"
* currentIndexes:
* type: array
* items:
* type: object
* properties:
* name:
* type: string
* column:
* type: string
* unique:
* type: boolean
* suggestions:
* type: array
* items:
* type: object
* properties:
* type:
* type: string
* column:
* type: string
* reason:
* type: string
* sql:
* type: string
* 401:
* description: 인증 필요
* 500:
* description: 서버 오류
*/
router.get('/database/indexes/:tableName', asyncHandler(async (req, res) => {
const { tableName } = req.params;
const analysis = await suggestIndexes(tableName);
res.success(analysis, '인덱스 분석 완료');
}));
/**
* @swagger
* /api/performance/query/analyze:
* post:
* tags: [Performance]
* summary: 쿼리 성능 분석
* description: SQL 쿼리의 실행 계획과 성능을 분석합니다. (관리자 전용)
* security:
* - bearerAuth: []
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - query
* properties:
* query:
* type: string
* example: "SELECT * FROM workers WHERE department = ?"
* params:
* type: array
* items:
* type: string
* example: ["생산부"]
* responses:
* 200:
* description: 쿼리 분석 성공
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* example: true
* message:
* type: string
* example: "쿼리 분석 완료"
* data:
* type: object
* properties:
* executionTime:
* type: integer
* example: 15
* explainResult:
* type: array
* items:
* type: object
* recommendations:
* type: array
* items:
* type: object
* properties:
* type:
* type: string
* message:
* type: string
* suggestion:
* type: string
* 401:
* description: 인증 필요
* 403:
* description: 권한 부족
* 500:
* description: 서버 오류
*/
router.post('/query/analyze', asyncHandler(async (req, res) => {
// 관리자 권한 확인
if (req.user?.access_level !== 'admin' && req.user?.access_level !== 'system') {
return res.status(403).json({
success: false,
error: '쿼리 분석 권한이 없습니다.'
});
}
const { query, params = [] } = req.body;
if (!query) {
return res.status(400).json({
success: false,
error: '분석할 쿼리가 필요합니다.'
});
}
const analysis = await analyzeQuery(query, params);
res.success(analysis, '쿼리 분석 완료');
}));
/**
* @swagger
* /api/performance/system/info:
* get:
* tags: [Performance]
* summary: 시스템 정보 조회
* description: 서버의 메모리, CPU, 업타임 등 시스템 정보를 조회합니다.
* security:
* - bearerAuth: []
* responses:
* 200:
* description: 시스템 정보 조회 성공
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* example: true
* message:
* type: string
* example: "시스템 정보 조회 성공"
* data:
* type: object
* properties:
* uptime:
* type: number
* example: 3600.5
* memory:
* type: object
* properties:
* rss:
* type: integer
* heapTotal:
* type: integer
* heapUsed:
* type: integer
* external:
* type: integer
* nodeVersion:
* type: string
* example: "v18.17.0"
* platform:
* type: string
* example: "linux"
* 401:
* description: 인증 필요
* 500:
* description: 서버 오류
*/
router.get('/system/info', asyncHandler(async (req, res) => {
const systemInfo = {
uptime: process.uptime(),
memory: process.memoryUsage(),
nodeVersion: process.version,
platform: process.platform,
cpuUsage: process.cpuUsage(),
timestamp: new Date().toISOString()
};
res.success(systemInfo, '시스템 정보 조회 성공');
}));
module.exports = router;

View File

@@ -0,0 +1,19 @@
// routes/projectRoutes.js
const express = require('express');
const router = express.Router();
const projectController = require('../controllers/projectController');
const { requireAuth, requireMinLevel } = require('../middlewares/auth');
// READ - 인증된 사용자
router.get('/', requireAuth, projectController.getAllProjects);
router.get('/active/list', requireAuth, projectController.getActiveProjects);
router.get('/:project_id', requireAuth, projectController.getProjectById);
// CREATE/UPDATE - support_team 이상 권한 필요
router.post('/', requireAuth, requireMinLevel('support_team'), projectController.createProject);
router.put('/:project_id', requireAuth, requireMinLevel('support_team'), projectController.updateProject);
// DELETE - admin 이상 권한 필요
router.delete('/:project_id', requireAuth, requireMinLevel('admin'), projectController.removeProject);
module.exports = router;

View File

@@ -0,0 +1,717 @@
const express = require('express');
const router = express.Router();
const { getDb } = require('../dbPool');
// DB 설정 엔드포인트 (개발용 - 인증 없이 접근 가능)
// 월별 집계 테이블 설정
router.post('/setup-monthly-status', async (req, res) => {
try {
const db = await getDb();
console.log('📊 월별 집계 테이블 생성 중...');
// 1. 월별 작업자 상태 집계 테이블
await db.execute(`
CREATE TABLE IF NOT EXISTS monthly_worker_status (
id INT PRIMARY KEY AUTO_INCREMENT,
year INT NOT NULL COMMENT '연도',
month INT NOT NULL COMMENT '월 (1-12)',
worker_id INT NOT NULL COMMENT '작업자 ID',
date DATE NOT NULL COMMENT '날짜',
total_work_hours DECIMAL(5,2) DEFAULT 0.00 COMMENT '총 작업시간',
actual_work_hours DECIMAL(5,2) DEFAULT 0.00 COMMENT '실제 작업시간 (휴가 제외)',
vacation_hours DECIMAL(5,2) DEFAULT 0.00 COMMENT '휴가 시간',
total_work_count INT DEFAULT 0 COMMENT '총 작업 건수',
regular_work_count INT DEFAULT 0 COMMENT '정규 작업 건수',
error_work_count INT DEFAULT 0 COMMENT '오류 작업 건수',
work_status ENUM(
'incomplete', 'partial', 'complete', 'overtime',
'vacation-full', 'vacation-half', 'vacation-quarter', 'vacation-half-half',
'error', 'overtime-warning'
) NOT NULL DEFAULT 'incomplete' COMMENT '작업 상태',
has_vacation BOOLEAN DEFAULT FALSE COMMENT '휴가 여부',
has_error BOOLEAN DEFAULT FALSE COMMENT '오류 여부',
has_issues BOOLEAN DEFAULT FALSE COMMENT '문제 여부 (미입력/부분입력)',
last_updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY unique_worker_date (worker_id, date),
KEY idx_year_month (year, month),
KEY idx_worker_year_month (worker_id, year, month),
KEY idx_status (work_status),
KEY idx_has_issues (has_issues),
KEY idx_has_error (has_error),
FOREIGN KEY (worker_id) REFERENCES workers(worker_id) ON DELETE CASCADE
) COMMENT='월별 작업자 상태 집계 테이블'
`);
// 2. 월별 집계 요약 테이블
await db.execute(`
CREATE TABLE IF NOT EXISTS monthly_summary (
id INT PRIMARY KEY AUTO_INCREMENT,
year INT NOT NULL COMMENT '연도',
month INT NOT NULL COMMENT '월 (1-12)',
date DATE NOT NULL COMMENT '날짜',
total_workers INT DEFAULT 0 COMMENT '총 작업자 수',
working_workers INT DEFAULT 0 COMMENT '작업한 작업자 수',
incomplete_workers INT DEFAULT 0 COMMENT '미입력 작업자 수',
partial_workers INT DEFAULT 0 COMMENT '부분입력 작업자 수',
complete_workers INT DEFAULT 0 COMMENT '완료 작업자 수',
overtime_workers INT DEFAULT 0 COMMENT '연장근로 작업자 수',
vacation_workers INT DEFAULT 0 COMMENT '휴가 작업자 수',
error_workers INT DEFAULT 0 COMMENT '오류 작업자 수',
total_work_hours DECIMAL(8,2) DEFAULT 0.00 COMMENT '총 작업시간',
total_work_count INT DEFAULT 0 COMMENT '총 작업 건수',
total_error_count INT DEFAULT 0 COMMENT '총 오류 건수',
has_issues BOOLEAN DEFAULT FALSE COMMENT '문제 있음 (미입력/부분입력)',
has_errors BOOLEAN DEFAULT FALSE COMMENT '오류 있음',
last_updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY unique_date (date),
KEY idx_year_month (year, month),
KEY idx_has_issues (has_issues),
KEY idx_has_errors (has_errors)
) COMMENT='월별 일자별 요약 테이블 (캘린더 최적화용)'
`);
console.log('📊 집계 프로시저 생성 중...');
// 3. 집계 업데이트 프로시저
await db.execute(`DROP PROCEDURE IF EXISTS UpdateMonthlyWorkerStatus`);
await db.execute(`
CREATE PROCEDURE UpdateMonthlyWorkerStatus(
IN p_date DATE,
IN p_worker_id INT
)
BEGIN
DECLARE v_year INT;
DECLARE v_month INT;
DECLARE v_total_hours DECIMAL(5,2);
DECLARE v_actual_hours DECIMAL(5,2);
DECLARE v_vacation_hours DECIMAL(5,2);
DECLARE v_total_count INT;
DECLARE v_regular_count INT;
DECLARE v_error_count INT;
DECLARE v_has_vacation BOOLEAN;
DECLARE v_has_error BOOLEAN;
DECLARE v_has_issues BOOLEAN;
DECLARE v_status VARCHAR(20);
SET v_year = YEAR(p_date);
SET v_month = MONTH(p_date);
SELECT
COALESCE(SUM(work_hours), 0),
COALESCE(SUM(CASE WHEN project_id != 13 THEN work_hours ELSE 0 END), 0),
COALESCE(SUM(CASE WHEN project_id = 13 THEN work_hours ELSE 0 END), 0),
COUNT(*),
COUNT(CASE WHEN project_id != 13 AND work_status_id != 2 THEN 1 END),
COUNT(CASE WHEN work_status_id = 2 THEN 1 END),
MAX(CASE WHEN project_id = 13 THEN 1 ELSE 0 END),
MAX(CASE WHEN work_status_id = 2 THEN 1 ELSE 0 END)
INTO
v_total_hours, v_actual_hours, v_vacation_hours,
v_total_count, v_regular_count, v_error_count,
v_has_vacation, v_has_error
FROM daily_work_reports
WHERE report_date = p_date AND worker_id = p_worker_id;
IF v_has_error THEN
SET v_status = 'error';
SET v_has_issues = FALSE;
ELSEIF v_total_hours > 12 THEN
SET v_status = 'overtime-warning';
SET v_has_issues = TRUE;
ELSEIF v_has_vacation AND v_vacation_hours > 0 THEN
CASE v_vacation_hours
WHEN 8 THEN SET v_status = 'vacation-full';
WHEN 6 THEN SET v_status = 'vacation-half-half';
WHEN 4 THEN SET v_status = 'vacation-half';
WHEN 2 THEN SET v_status = 'vacation-quarter';
ELSE SET v_status = 'vacation-full';
END CASE;
SET v_has_issues = FALSE;
ELSEIF v_total_hours > 8 THEN
SET v_status = 'overtime';
SET v_has_issues = FALSE;
ELSEIF v_total_hours = 8 THEN
SET v_status = 'complete';
SET v_has_issues = FALSE;
ELSEIF v_total_hours > 0 THEN
SET v_status = 'partial';
SET v_has_issues = TRUE;
ELSE
SET v_status = 'incomplete';
SET v_has_issues = TRUE;
END IF;
INSERT INTO monthly_worker_status (
year, month, worker_id, date,
total_work_hours, actual_work_hours, vacation_hours,
total_work_count, regular_work_count, error_work_count,
work_status, has_vacation, has_error, has_issues
) VALUES (
v_year, v_month, p_worker_id, p_date,
v_total_hours, v_actual_hours, v_vacation_hours,
v_total_count, v_regular_count, v_error_count,
v_status, v_has_vacation, v_has_error, v_has_issues
) ON DUPLICATE KEY UPDATE
total_work_hours = v_total_hours,
actual_work_hours = v_actual_hours,
vacation_hours = v_vacation_hours,
total_work_count = v_total_count,
regular_work_count = v_regular_count,
error_work_count = v_error_count,
work_status = v_status,
has_vacation = v_has_vacation,
has_error = v_has_error,
has_issues = v_has_issues,
last_updated = CURRENT_TIMESTAMP;
CALL UpdateDailySummary(p_date);
END
`);
await db.execute(`DROP PROCEDURE IF EXISTS UpdateDailySummary`);
await db.execute(`
CREATE PROCEDURE UpdateDailySummary(
IN p_date DATE
)
BEGIN
DECLARE v_year INT;
DECLARE v_month INT;
SET v_year = YEAR(p_date);
SET v_month = MONTH(p_date);
INSERT INTO monthly_summary (
year, month, date,
total_workers, working_workers,
incomplete_workers, partial_workers, complete_workers,
overtime_workers, vacation_workers, error_workers,
total_work_hours, total_work_count, total_error_count,
has_issues, has_errors
)
SELECT
v_year, v_month, p_date,
COUNT(*) as total_workers,
COUNT(CASE WHEN work_status != 'incomplete' THEN 1 END) as working_workers,
COUNT(CASE WHEN work_status = 'incomplete' THEN 1 END) as incomplete_workers,
COUNT(CASE WHEN work_status = 'partial' THEN 1 END) as partial_workers,
COUNT(CASE WHEN work_status IN ('complete') THEN 1 END) as complete_workers,
COUNT(CASE WHEN work_status = 'overtime' THEN 1 END) as overtime_workers,
COUNT(CASE WHEN work_status LIKE 'vacation%' THEN 1 END) as vacation_workers,
COUNT(CASE WHEN work_status = 'error' THEN 1 END) as error_workers,
SUM(total_work_hours) as total_work_hours,
SUM(total_work_count) as total_work_count,
SUM(error_work_count) as total_error_count,
MAX(has_issues) as has_issues,
MAX(has_error) as has_errors
FROM monthly_worker_status
WHERE date = p_date
ON DUPLICATE KEY UPDATE
total_workers = VALUES(total_workers),
working_workers = VALUES(working_workers),
incomplete_workers = VALUES(incomplete_workers),
partial_workers = VALUES(partial_workers),
complete_workers = VALUES(complete_workers),
overtime_workers = VALUES(overtime_workers),
vacation_workers = VALUES(vacation_workers),
error_workers = VALUES(error_workers),
total_work_hours = VALUES(total_work_hours),
total_work_count = VALUES(total_work_count),
total_error_count = VALUES(total_error_count),
has_issues = VALUES(has_issues),
has_errors = VALUES(has_errors),
last_updated = CURRENT_TIMESTAMP;
END
`);
console.log('📊 트리거 생성 중...');
// 4. 트리거 생성
await db.execute(`DROP TRIGGER IF EXISTS tr_daily_work_reports_insert`);
await db.execute(`
CREATE TRIGGER tr_daily_work_reports_insert
AFTER INSERT ON daily_work_reports
FOR EACH ROW
BEGIN
CALL UpdateMonthlyWorkerStatus(NEW.report_date, NEW.worker_id);
END
`);
await db.execute(`DROP TRIGGER IF EXISTS tr_daily_work_reports_update`);
await db.execute(`
CREATE TRIGGER tr_daily_work_reports_update
AFTER UPDATE ON daily_work_reports
FOR EACH ROW
BEGIN
CALL UpdateMonthlyWorkerStatus(OLD.report_date, OLD.worker_id);
IF OLD.report_date != NEW.report_date OR OLD.worker_id != NEW.worker_id THEN
CALL UpdateMonthlyWorkerStatus(NEW.report_date, NEW.worker_id);
END IF;
END
`);
await db.execute(`DROP TRIGGER IF EXISTS tr_daily_work_reports_delete`);
await db.execute(`
CREATE TRIGGER tr_daily_work_reports_delete
AFTER DELETE ON daily_work_reports
FOR EACH ROW
BEGIN
CALL UpdateMonthlyWorkerStatus(OLD.report_date, OLD.worker_id);
END
`);
console.log('📊 기존 데이터로 집계 테이블 초기화 중...');
// 5. 기존 작업 데이터로 집계 테이블 초기화
const [existingDates] = await db.execute(`
SELECT DISTINCT report_date, worker_id
FROM daily_work_reports
WHERE report_date >= '2025-01-01'
ORDER BY report_date DESC, worker_id ASC
`);
let processedCount = 0;
const batchSize = 50;
for (let i = 0; i < existingDates.length; i += batchSize) {
const batch = existingDates.slice(i, i + batchSize);
for (const { report_date, worker_id } of batch) {
try {
await db.execute('CALL UpdateMonthlyWorkerStatus(?, ?)', [report_date, worker_id]);
processedCount++;
} catch (error) {
console.warn(`집계 처리 실패: ${report_date}, worker ${worker_id}:`, error.message);
}
}
if (i % 100 === 0) {
console.log(`📊 집계 초기화 진행률: ${processedCount}/${existingDates.length}`);
}
}
res.json({
success: true,
message: '월별 집계 시스템이 성공적으로 설정되었습니다.',
data: {
tables_created: [
'monthly_worker_status',
'monthly_summary'
],
procedures_created: [
'UpdateMonthlyWorkerStatus',
'UpdateDailySummary'
],
triggers_created: [
'tr_daily_work_reports_insert',
'tr_daily_work_reports_update',
'tr_daily_work_reports_delete'
],
initialized_records: processedCount,
total_dates: existingDates.length
}
});
} catch (error) {
console.error('❌ 월별 집계 시스템 설정 오류:', error);
res.status(500).json({
success: false,
message: '월별 집계 시스템 설정 중 오류가 발생했습니다.',
error: error.message
});
}
});
router.post('/setup-attendance-db', async (req, res) => {
try {
console.log('🚀 근태 관리 DB 설정 API 호출됨');
const db = await getDb();
// 1. 근로 유형 테이블 생성
console.log('📋 근로 유형 테이블 생성 중...');
await db.execute(`
CREATE TABLE IF NOT EXISTS work_attendance_types (
id INT PRIMARY KEY AUTO_INCREMENT,
type_code VARCHAR(20) NOT NULL UNIQUE COMMENT '근로 유형 코드',
type_name VARCHAR(50) NOT NULL COMMENT '근로 유형명',
description TEXT COMMENT '설명',
is_active BOOLEAN DEFAULT TRUE COMMENT '활성 상태',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) COMMENT='근로 유형 관리 테이블'
`);
// 2. 휴가 유형 테이블 생성
console.log('🏖️ 휴가 유형 테이블 생성 중...');
await db.execute(`
CREATE TABLE IF NOT EXISTS vacation_types (
id INT PRIMARY KEY AUTO_INCREMENT,
type_code VARCHAR(20) NOT NULL UNIQUE COMMENT '휴가 유형 코드',
type_name VARCHAR(50) NOT NULL COMMENT '휴가 유형명',
hours_deduction DECIMAL(4,2) NOT NULL COMMENT '차감 시간',
description TEXT COMMENT '설명',
is_active BOOLEAN DEFAULT TRUE COMMENT '활성 상태',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) COMMENT='휴가 유형 관리 테이블'
`);
// 3. 일일 근태 기록 테이블 생성
console.log('📊 일일 근태 기록 테이블 생성 중...');
await db.execute(`
CREATE TABLE IF NOT EXISTS daily_attendance_records (
id INT PRIMARY KEY AUTO_INCREMENT,
record_date DATE NOT NULL COMMENT '기록 날짜',
worker_id INT NOT NULL COMMENT '작업자 ID',
work_attendance_type_id INT COMMENT '근로 유형 ID (정시, 연장, 부분, 휴가)',
total_work_hours DECIMAL(4,2) DEFAULT 0.00 COMMENT '총 작업 시간',
vacation_type_id INT COMMENT '휴가 유형 ID (연차, 반차 등)',
is_overtime_approved BOOLEAN DEFAULT FALSE COMMENT '연장근로 승인 여부',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY unique_worker_date (worker_id, record_date),
FOREIGN KEY (worker_id) REFERENCES workers(worker_id) ON DELETE CASCADE,
FOREIGN KEY (work_attendance_type_id) REFERENCES work_attendance_types(id) ON DELETE SET NULL,
FOREIGN KEY (vacation_type_id) REFERENCES vacation_types(id) ON DELETE SET NULL
) COMMENT='일일 근태 기록 테이블'
`);
// 4. 작업자별 휴가 잔여 관리 테이블 생성
console.log('👥 작업자별 휴가 잔여 관리 테이블 생성 중...');
await db.execute(`
CREATE TABLE IF NOT EXISTS worker_vacation_balance (
id INT PRIMARY KEY AUTO_INCREMENT,
worker_id INT NOT NULL UNIQUE COMMENT '작업자 ID',
annual_leave_total DECIMAL(5,2) DEFAULT 15.00 COMMENT '총 연차 일수',
annual_leave_used DECIMAL(5,2) DEFAULT 0.00 COMMENT '사용 연차 일수',
sick_leave_total DECIMAL(5,2) DEFAULT 10.00 COMMENT '총 병가 일수',
sick_leave_used DECIMAL(5,2) DEFAULT 0.00 COMMENT '사용 병가 일수',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (worker_id) REFERENCES workers(worker_id) ON DELETE CASCADE
) COMMENT='작업자별 휴가 잔여 관리 테이블'
`);
// 5. 기본 데이터 삽입
console.log('📝 기본 데이터 삽입 중...');
// 근로 유형 기본 데이터
await db.execute(`
INSERT IGNORE INTO work_attendance_types (type_code, type_name, description) VALUES
('REGULAR', '정시근로', '8시간 정규 근무'),
('OVERTIME', '연장근로', '8시간 초과 근무'),
('PARTIAL', '부분근로', '8시간 미만 근무'),
('VACATION', '휴가근로', '휴가와 함께하는 부분 근무')
`);
// 휴가 유형 기본 데이터
await db.execute(`
INSERT IGNORE INTO vacation_types (type_code, type_name, hours_deduction, description) VALUES
('ANNUAL_FULL', '연차', 8.0, '하루 전체 연차'),
('ANNUAL_HALF', '반차', 4.0, '반일 연차'),
('ANNUAL_QUARTER', '반반차', 2.0, '1/4일 연차'),
('SICK_FULL', '병가', 8.0, '하루 전체 병가'),
('SICK_HALF', '반일병가', 4.0, '반일 병가')
`);
res.json({
success: true,
message: '근태 관리 DB 설정이 완료되었습니다.',
data: {
tables_created: [
'work_attendance_types',
'vacation_types',
'daily_attendance_records',
'worker_vacation_balance'
],
basic_data_inserted: true
}
});
} catch (error) {
console.error('❌ DB 설정 API 오류:', error);
res.status(500).json({
success: false,
message: 'DB 설정 중 오류가 발생했습니다.',
error: error.message
});
}
});
// 12시간 초과 상태 컬럼 추가
router.post('/add-overtime-warning', async (req, res) => {
try {
const db = await getDb();
console.log('⚠️ 12시간 초과 상태 컬럼 추가 중...');
// 1. monthly_summary 테이블에 컬럼 추가
try {
await db.execute(`
ALTER TABLE monthly_summary
ADD COLUMN overtime_warning_workers INT DEFAULT 0 COMMENT '확인필요(12시간초과) 작업자 수' AFTER error_workers
`);
console.log('✅ overtime_warning_workers 컬럼 추가 완료');
} catch (error) {
if (error.code === 'ER_DUP_FIELDNAME') {
console.log(' overtime_warning_workers 컬럼이 이미 존재합니다.');
} else {
throw error;
}
}
try {
await db.execute(`
ALTER TABLE monthly_summary
ADD COLUMN has_overtime_warning BOOLEAN DEFAULT FALSE COMMENT '확인필요 상태 있음' AFTER has_errors
`);
console.log('✅ has_overtime_warning 컬럼 추가 완료');
} catch (error) {
if (error.code === 'ER_DUP_FIELDNAME') {
console.log(' has_overtime_warning 컬럼이 이미 존재합니다.');
} else {
throw error;
}
}
// 2. UpdateDailySummary 프로시저 업데이트
await db.execute(`DROP PROCEDURE IF EXISTS UpdateDailySummary`);
await db.execute(`
CREATE PROCEDURE UpdateDailySummary(
IN p_date DATE
)
BEGIN
DECLARE v_year INT;
DECLARE v_month INT;
SET v_year = YEAR(p_date);
SET v_month = MONTH(p_date);
INSERT INTO monthly_summary (
year, month, date,
total_workers, working_workers,
incomplete_workers, partial_workers, complete_workers,
overtime_workers, vacation_workers, error_workers, overtime_warning_workers,
total_work_hours, total_work_count, total_error_count,
has_issues, has_errors, has_overtime_warning
)
SELECT
v_year, v_month, p_date,
COUNT(*) as total_workers,
COUNT(CASE WHEN work_status != 'incomplete' THEN 1 END) as working_workers,
COUNT(CASE WHEN work_status = 'incomplete' THEN 1 END) as incomplete_workers,
COUNT(CASE WHEN work_status = 'partial' THEN 1 END) as partial_workers,
COUNT(CASE WHEN work_status IN ('complete', 'overtime', 'vacation-full', 'vacation-half', 'vacation-quarter', 'vacation-half-half') THEN 1 END) as complete_workers,
COUNT(CASE WHEN work_status = 'overtime' THEN 1 END) as overtime_workers,
COUNT(CASE WHEN work_status LIKE 'vacation%' THEN 1 END) as vacation_workers,
COUNT(CASE WHEN work_status = 'error' THEN 1 END) as error_workers,
COUNT(CASE WHEN work_status = 'overtime-warning' THEN 1 END) as overtime_warning_workers,
SUM(total_work_hours) as total_work_hours,
SUM(total_work_count) as total_work_count,
SUM(error_work_count) as total_error_count,
MAX(has_issues) as has_issues,
MAX(has_error) as has_errors,
MAX(CASE WHEN work_status = 'overtime-warning' THEN 1 ELSE 0 END) as has_overtime_warning
FROM monthly_worker_status
WHERE date = p_date
ON DUPLICATE KEY UPDATE
total_workers = VALUES(total_workers),
working_workers = VALUES(working_workers),
incomplete_workers = VALUES(incomplete_workers),
partial_workers = VALUES(partial_workers),
complete_workers = VALUES(complete_workers),
overtime_workers = VALUES(overtime_workers),
vacation_workers = VALUES(vacation_workers),
error_workers = VALUES(error_workers),
overtime_warning_workers = VALUES(overtime_warning_workers),
total_work_hours = VALUES(total_work_hours),
total_work_count = VALUES(total_work_count),
total_error_count = VALUES(total_error_count),
has_issues = VALUES(has_issues),
has_errors = VALUES(has_errors),
has_overtime_warning = VALUES(has_overtime_warning),
last_updated = CURRENT_TIMESTAMP;
END
`);
console.log('✅ UpdateDailySummary 프로시저 업데이트 완료');
res.json({
success: true,
message: '12시간 초과 상태 컬럼 추가 완료',
columns_added: ['overtime_warning_workers', 'has_overtime_warning'],
procedure_updated: 'UpdateDailySummary'
});
} catch (error) {
console.error('❌ 12시간 초과 상태 설정 오류:', error);
res.status(500).json({
success: false,
message: '12시간 초과 상태 설정 실패',
error: error.message
});
}
});
// 기존 데이터를 월별 집계 테이블로 마이그레이션
router.post('/migrate-existing-data', async (req, res) => {
try {
const db = await getDb();
console.log('🔄 기존 데이터 마이그레이션 시작...');
// 1. 기존 데이터 범위 확인
const [dateRange] = await db.execute(`
SELECT
MIN(report_date) as min_date,
MAX(report_date) as max_date,
COUNT(*) as total_reports
FROM daily_work_reports
`);
if (dateRange.length === 0 || !dateRange[0].min_date) {
return res.json({
success: true,
message: '마이그레이션할 데이터가 없습니다.',
migrated_count: 0
});
}
const { min_date, max_date, total_reports } = dateRange[0];
console.log(`📊 데이터 범위: ${min_date} ~ ${max_date} (총 ${total_reports}건)`);
// 2. 기존 monthly_worker_status, monthly_summary 데이터 삭제
await db.execute('DELETE FROM monthly_summary');
await db.execute('DELETE FROM monthly_worker_status');
console.log('🗑️ 기존 집계 데이터 삭제 완료');
// 3. 날짜별로 작업자별 상태 재계산
const [allDates] = await db.execute(`
SELECT DISTINCT report_date, worker_id
FROM daily_work_reports
WHERE report_date BETWEEN ? AND ?
ORDER BY report_date, worker_id
`, [min_date, max_date]);
console.log(`🔄 ${allDates.length}개 날짜-작업자 조합 처리 중...`);
let processedCount = 0;
for (const { report_date, worker_id } of allDates) {
try {
// UpdateMonthlyWorkerStatus 프로시저 호출
await db.execute('CALL UpdateMonthlyWorkerStatus(?, ?)', [report_date, worker_id]);
processedCount++;
if (processedCount % 50 === 0) {
console.log(`📈 진행률: ${processedCount}/${allDates.length} (${Math.round(processedCount/allDates.length*100)}%)`);
}
} catch (error) {
console.error(`${report_date} ${worker_id} 처리 오류:`, error.message);
}
}
// 4. 결과 확인
const [workerStatusCount] = await db.execute('SELECT COUNT(*) as count FROM monthly_worker_status');
const [summaryCount] = await db.execute('SELECT COUNT(*) as count FROM monthly_summary');
console.log(`✅ 마이그레이션 완료:`);
console.log(` - monthly_worker_status: ${workerStatusCount[0].count}`);
console.log(` - monthly_summary: ${summaryCount[0].count}`);
res.json({
success: true,
message: '기존 데이터 마이그레이션 완료',
original_reports: total_reports,
processed_combinations: processedCount,
worker_status_records: workerStatusCount[0].count,
summary_records: summaryCount[0].count,
date_range: {
from: min_date,
to: max_date
}
});
} catch (error) {
console.error('❌ 데이터 마이그레이션 오류:', error);
res.status(500).json({
success: false,
message: '데이터 마이그레이션 실패',
error: error.message
});
}
});
// DB 상태 확인
router.get('/check-data-status', async (req, res) => {
try {
const db = await getDb();
const [dailyReports] = await db.execute('SELECT COUNT(*) as count FROM daily_work_reports');
const [workerStatus] = await db.execute('SELECT COUNT(*) as count FROM monthly_worker_status');
const [monthlySummary] = await db.execute('SELECT COUNT(*) as count FROM monthly_summary');
// 최근 데이터 확인
const [recentData] = await db.execute(`
SELECT
DATE(report_date) as date,
COUNT(*) as reports
FROM daily_work_reports
WHERE report_date >= DATE_SUB(CURDATE(), INTERVAL 30 DAY)
GROUP BY DATE(report_date)
ORDER BY report_date DESC
LIMIT 5
`);
const [recentSummary] = await db.execute(`
SELECT
date,
total_workers,
has_issues,
has_errors,
has_overtime_warning
FROM monthly_summary
WHERE date >= DATE_SUB(CURDATE(), INTERVAL 30 DAY)
ORDER BY date DESC
LIMIT 5
`);
res.json({
success: true,
data: {
daily_work_reports: dailyReports[0].count,
monthly_worker_status: workerStatus[0].count,
monthly_summary: monthlySummary[0].count,
recent_daily_reports: recentData,
recent_summary: recentSummary,
migration_needed: workerStatus[0].count === 0 && dailyReports[0].count > 0
}
});
} catch (error) {
console.error('❌ DB 상태 확인 오류:', error);
res.status(500).json({
success: false,
message: 'DB 상태 확인 실패',
error: error.message
});
}
});
module.exports = router;

View File

@@ -0,0 +1,378 @@
// 시스템 관리 라우트
const express = require('express');
const router = express.Router();
const systemController = require('../controllers/systemController');
const { requireAuth, requireRole } = require('../middlewares/auth');
// 모든 라우트에 인증 및 시스템 권한 확인 적용
router.use(requireAuth);
router.use(requireRole('system'));
// ===== 시스템 상태 관련 =====
/**
* GET /api/system/status
* 시스템 전체 상태 확인
*/
router.get('/status', systemController.getSystemStatus);
/**
* GET /api/system/db-status
* 데이터베이스 상태 확인
*/
router.get('/db-status', systemController.getDatabaseStatus);
/**
* GET /api/system/alerts
* 시스템 알림 조회
*/
router.get('/alerts', systemController.getSystemAlerts);
/**
* GET /api/system/recent-activities
* 최근 시스템 활동 조회
*/
router.get('/recent-activities', systemController.getRecentActivities);
// ===== 사용자 관리 관련 =====
/**
* GET /api/system/users/stats
* 사용자 통계 조회
*/
router.get('/users/stats', systemController.getUserStats);
/**
* GET /api/system/users
* 모든 사용자 목록 조회
*/
router.get('/users', systemController.getAllUsers);
/**
* POST /api/system/users
* 새 사용자 생성
*/
router.post('/users', systemController.createUser);
/**
* PUT /api/system/users/:id
* 사용자 정보 수정
*/
router.put('/users/:id', systemController.updateUser);
/**
* DELETE /api/system/users/:id
* 사용자 삭제
*/
router.delete('/users/:id', systemController.deleteUser);
/**
* POST /api/system/users/:id/reset-password
* 사용자 비밀번호 재설정
*/
router.post('/users/:id/reset-password', systemController.resetUserPassword);
// ===== 시스템 로그 관련 =====
/**
* GET /api/system/logs/login
* 로그인 로그 조회
*/
router.get('/logs/login', async (req, res) => {
try {
const { getDb } = require('../dbPool');
const db = await getDb();
const { page = 1, limit = 50, status, user_id, start_date, end_date } = req.query;
const offset = (page - 1) * limit;
let whereClause = '1=1';
const params = [];
if (status) {
whereClause += ' AND ll.login_status = ?';
params.push(status);
}
if (user_id) {
whereClause += ' AND ll.user_id = ?';
params.push(user_id);
}
if (start_date) {
whereClause += ' AND ll.login_time >= ?';
params.push(start_date);
}
if (end_date) {
whereClause += ' AND ll.login_time <= ?';
params.push(end_date);
}
const [logs] = await db.query(`
SELECT
ll.log_id,
ll.user_id,
u.username,
u.name,
ll.login_time,
ll.ip_address,
ll.user_agent,
ll.login_status,
ll.failure_reason
FROM login_logs ll
LEFT JOIN Users u ON ll.user_id = u.user_id
WHERE ${whereClause}
ORDER BY ll.login_time DESC
LIMIT ? OFFSET ?
`, [...params, parseInt(limit), parseInt(offset)]);
const [totalCount] = await db.query(`
SELECT COUNT(*) as count
FROM login_logs ll
WHERE ${whereClause}
`, params);
res.json({
success: true,
data: {
logs,
pagination: {
page: parseInt(page),
limit: parseInt(limit),
total: totalCount[0].count,
pages: Math.ceil(totalCount[0].count / limit)
}
}
});
} catch (error) {
console.error('로그인 로그 조회 오류:', error);
res.status(500).json({
success: false,
error: '로그인 로그를 조회할 수 없습니다.'
});
}
});
/**
* GET /api/system/logs/password-changes
* 비밀번호 변경 로그 조회
*/
router.get('/logs/password-changes', async (req, res) => {
try {
const { getDb } = require('../dbPool');
const db = await getDb();
const { page = 1, limit = 50 } = req.query;
const offset = (page - 1) * limit;
const [logs] = await db.query(`
SELECT
pcl.log_id,
pcl.user_id,
u.username,
u.name,
pcl.changed_by_user_id,
admin.username as changed_by_username,
admin.name as changed_by_name,
pcl.changed_at,
pcl.change_type,
pcl.ip_address
FROM password_change_logs pcl
LEFT JOIN Users u ON pcl.user_id = u.user_id
LEFT JOIN Users admin ON pcl.changed_by_user_id = admin.user_id
ORDER BY pcl.changed_at DESC
LIMIT ? OFFSET ?
`, [parseInt(limit), parseInt(offset)]);
const [totalCount] = await db.query('SELECT COUNT(*) as count FROM password_change_logs');
res.json({
success: true,
data: {
logs,
pagination: {
page: parseInt(page),
limit: parseInt(limit),
total: totalCount[0].count,
pages: Math.ceil(totalCount[0].count / limit)
}
}
});
} catch (error) {
console.error('비밀번호 변경 로그 조회 오류:', error);
res.status(500).json({
success: false,
error: '비밀번호 변경 로그를 조회할 수 없습니다.'
});
}
});
/**
* POST /api/system/migrations/fix-work-type-id
* TBM 기반 작업보고서의 work_type_id를 task_id로 수정
*/
router.post('/migrations/fix-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
`);
if (checkResult.length === 0) {
return res.json({
success: true,
message: '수정할 데이터가 없습니다.',
data: { affected_rows: 0, samples: [] }
});
}
// 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
});
}
});
/**
* GET /api/system/logs/activity
* 활동 로그 조회 (activity_logs 테이블이 있는 경우)
*/
router.get('/logs/activity', async (req, res) => {
try {
const { getDb } = require('../dbPool');
const db = await getDb();
const { page = 1, limit = 50, activity_type, user_id } = req.query;
const offset = (page - 1) * limit;
let whereClause = '1=1';
const params = [];
if (activity_type) {
whereClause += ' AND al.activity_type = ?';
params.push(activity_type);
}
if (user_id) {
whereClause += ' AND al.user_id = ?';
params.push(user_id);
}
const [logs] = await db.query(`
SELECT
al.log_id,
al.user_id,
u.username,
u.name,
al.activity_type,
al.table_name,
al.record_id,
al.action,
al.ip_address,
al.user_agent,
al.created_at
FROM activity_logs al
LEFT JOIN Users u ON al.user_id = u.user_id
WHERE ${whereClause}
ORDER BY al.created_at DESC
LIMIT ? OFFSET ?
`, [...params, parseInt(limit), parseInt(offset)]);
const [totalCount] = await db.query(`
SELECT COUNT(*) as count
FROM activity_logs al
WHERE ${whereClause}
`, params);
res.json({
success: true,
data: {
logs,
pagination: {
page: parseInt(page),
limit: parseInt(limit),
total: totalCount[0].count,
pages: Math.ceil(totalCount[0].count / limit)
}
}
});
} catch (error) {
console.error('활동 로그 조회 오류:', error);
res.status(500).json({
success: false,
error: '활동 로그를 조회할 수 없습니다.'
});
}
});
module.exports = router;

View File

@@ -0,0 +1,27 @@
// routes/taskRoutes.js
const express = require('express');
const router = express.Router();
const taskController = require('../controllers/taskController');
// CREATE 작업
router.post('/', taskController.createTask);
// READ ALL 작업
router.get('/', taskController.getAllTasks);
// READ ACTIVE 작업
router.get('/active/list', taskController.getActiveTasks);
// READ BY WORK TYPE (공정별)
router.get('/by-work-type/:work_type_id', taskController.getTasksByWorkType);
// READ ONE 작업
router.get('/:id', taskController.getTaskById);
// UPDATE 작업
router.put('/:id', taskController.updateTask);
// DELETE 작업
router.delete('/:id', taskController.deleteTask);
module.exports = router;

View File

@@ -0,0 +1,103 @@
// routes/tbmRoutes.js - TBM 시스템 라우트
const express = require('express');
const router = express.Router();
const TbmController = require('../controllers/tbmController');
const { requireAuth } = require('../middlewares/auth');
// ==================== TBM 세션 관련 ====================
// TBM 세션 생성
router.post('/sessions', requireAuth, TbmController.createSession);
// 작업보고서가 작성되지 않은 TBM 팀 배정 조회 (구체적인 경로이므로 먼저 정의)
router.get('/sessions/incomplete-reports', requireAuth, TbmController.getIncompleteWorkReports);
// 특정 날짜의 TBM 세션 목록 조회
router.get('/sessions/date/:date', requireAuth, TbmController.getSessionsByDate);
// TBM 세션 상세 조회
router.get('/sessions/:sessionId', requireAuth, TbmController.getSessionById);
// TBM 세션 수정
router.put('/sessions/:sessionId', requireAuth, TbmController.updateSession);
// TBM 세션 완료 처리
router.post('/sessions/:sessionId/complete', requireAuth, TbmController.completeSession);
// ==================== 팀 구성 관련 ====================
// 팀원 추가 (단일)
router.post('/sessions/:sessionId/team', requireAuth, TbmController.addTeamMember);
// 팀 구성 일괄 추가
router.post('/sessions/:sessionId/team/batch', requireAuth, TbmController.addTeamMembers);
// TBM 세션의 팀 구성 조회
router.get('/sessions/:sessionId/team', requireAuth, TbmController.getTeamMembers);
// 팀원 전체 삭제 (수정 시 사용) - 더 구체적인 경로이므로 먼저 정의
router.delete('/sessions/:sessionId/team/clear', requireAuth, TbmController.clearAllTeamMembers);
// 팀원 제거
router.delete('/sessions/:sessionId/team/:workerId', requireAuth, TbmController.removeTeamMember);
// ==================== 안전 체크리스트 관련 ====================
// 모든 안전 체크 항목 조회
router.get('/safety-checks', requireAuth, TbmController.getAllSafetyChecks);
// 안전 체크 항목 생성 (관리자용)
router.post('/safety-checks', requireAuth, TbmController.createSafetyCheck);
// 안전 체크 항목 수정 (관리자용)
router.put('/safety-checks/:checkId', requireAuth, TbmController.updateSafetyCheck);
// 안전 체크 항목 삭제 (관리자용)
router.delete('/safety-checks/:checkId', requireAuth, TbmController.deleteSafetyCheck);
// TBM 세션의 안전 체크 기록 조회
router.get('/sessions/:sessionId/safety', requireAuth, TbmController.getSafetyRecords);
// 안전 체크 일괄 저장
router.post('/sessions/:sessionId/safety', requireAuth, TbmController.saveSafetyRecords);
// 필터링된 안전 체크리스트 조회 (기본 + 날씨 + 작업별)
router.get('/sessions/:sessionId/safety-checks/filtered', requireAuth, TbmController.getFilteredSafetyChecks);
// ==================== 날씨 관련 ====================
// 현재 날씨 조회
router.get('/weather/current', requireAuth, TbmController.getCurrentWeather);
// 날씨 조건 목록 조회
router.get('/weather/conditions', requireAuth, TbmController.getWeatherConditions);
// 세션 날씨 정보 조회
router.get('/sessions/:sessionId/weather', requireAuth, TbmController.getSessionWeather);
// 세션 날씨 정보 저장
router.post('/sessions/:sessionId/weather', requireAuth, TbmController.saveSessionWeather);
// ==================== 작업 인계 관련 ====================
// 작업 인계 생성
router.post('/handovers', requireAuth, TbmController.createHandover);
// 작업 인계 확인
router.post('/handovers/:handoverId/confirm', requireAuth, TbmController.confirmHandover);
// 특정 날짜의 작업 인계 목록 조회
router.get('/handovers/date/:date', requireAuth, TbmController.getHandoversByDate);
// 나에게 온 미확인 인계 건 조회
router.get('/handovers/pending', requireAuth, TbmController.getMyPendingHandovers);
// ==================== 통계 및 리포트 ====================
// TBM 통계 조회
router.get('/statistics/tbm', requireAuth, TbmController.getTbmStatistics);
// 리더별 TBM 진행 현황 조회
router.get('/statistics/leaders', requireAuth, TbmController.getLeaderStatistics);
module.exports = router;

View File

@@ -0,0 +1,16 @@
// routes/toolsRoute.js
const express = require('express');
const router = express.Router();
const controller = require('../controllers/toolsController');
const { requireAuth, requireMinLevel } = require('../middlewares/auth');
// 읽기 작업: 인증된 사용자
router.get('/', requireAuth, controller.getAll);
router.get('/:id', requireAuth, controller.getById);
// 쓰기 작업: group_leader 이상 권한 필요
router.post('/', requireAuth, requireMinLevel('group_leader'), controller.create);
router.put('/:id', requireAuth, requireMinLevel('group_leader'), controller.update);
router.delete('/:id', requireAuth, requireMinLevel('admin'), controller.delete);
module.exports = router;

View File

@@ -0,0 +1,52 @@
// ✅ routes/uploadBgRoutes.js (배경 이미지 전용 업로드 라우터 - 보안 강화)
const express = require('express');
const router = express.Router();
const multer = require('multer');
const path = require('path');
const { requireAuth, requireMinLevel } = require('../middlewares/auth');
const { createFileFilter, validateUploadedFile } = require('../utils/fileUploadSecurity');
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, path.join(__dirname, '../public/img'));
},
filename: (req, file, cb) => {
cb(null, 'login-bg.jpeg'); // 고정된 파일명으로 덮어쓰기
}
});
// 보안 강화된 파일 필터 (이미지만 허용)
const imageFileFilter = createFileFilter({
allowedExtensions: ['.jpg', '.jpeg', '.png', '.gif', '.webp'],
allowedMimes: ['image/jpeg', 'image/png', 'image/gif', 'image/webp']
});
const upload = multer({
storage,
fileFilter: imageFileFilter,
limits: {
fileSize: 10 * 1024 * 1024, // 10MB 제한 (배경 이미지는 크기가 클 수 있음)
files: 1
}
});
// 관리자 권한 필요
router.post('/upload-bg', requireAuth, requireMinLevel('admin'), upload.single('image'), async (req, res) => {
if (!req.file) {
return res.status(400).json({ success: false, message: '파일이 없습니다.' });
}
// 업로드된 파일의 실제 내용 검증 (Magic number)
const validation = await validateUploadedFile(req.file.path, req.file.mimetype);
if (!validation.valid) {
return res.status(400).json({
success: false,
message: validation.message,
code: 'INVALID_FILE_TYPE'
});
}
res.json({ success: true, path: '/img/login-bg.jpeg' });
});
module.exports = router;

View File

@@ -0,0 +1,10 @@
// ✅ routes/uploadRoutes.js (기존 업로드 라우터)
const express = require('express');
const router = express.Router();
const uploadController = require('../controllers/uploadController');
// 기존 업로드 등록/조회
router.post('/', uploadController.createUpload);
router.get('/', uploadController.getUploads);
module.exports = router;

View File

@@ -0,0 +1,167 @@
/**
* 사용자 관리 라우터
*
* 사용자 CRUD 및 상태 관리를 위한 API 라우트 정의
*
* @author TK-FB-Project
* @since 2025-12-11
*/
const express = require('express');
const router = express.Router();
const userController = require('../controllers/userController');
const { verifyToken } = require('../middlewares/authMiddleware');
const logger = require('../utils/logger');
/**
* 모든 라우트에 인증 미들웨어 적용
*/
router.use(verifyToken);
/**
* 관리자 권한 확인 미들웨어
* role 또는 access_level로 관리자 확인
*/
const adminOnly = (req, res, next) => {
const userRole = req.user?.role?.toLowerCase();
const accessLevel = req.user?.access_level?.toLowerCase();
// role 기반 확인
const isAdminByRole = userRole === 'admin' || userRole === 'system' || userRole === 'system admin';
// access_level 기반 확인 (role이 없는 경우 대비)
const isAdminByAccessLevel = accessLevel === 'admin' || accessLevel === 'system';
if (req.user && (isAdminByRole || isAdminByAccessLevel)) {
next();
} else {
logger.warn('관리자 권한 없는 접근 시도', {
userId: req.user?.user_id,
username: req.user?.username,
role: req.user?.role,
accessLevel: req.user?.access_level,
url: req.originalUrl
});
return res.status(403).json({
success: false,
message: '관리자 권한이 필요합니다'
});
}
};
// ========== 개인 정보 조회 API (관리자 권한 불필요) ==========
// 내 정보 조회
router.get('/me', userController.getMyInfo || ((req, res) => res.json({ success: true, data: req.user })));
// 내 출근 기록 조회
router.get('/me/attendance-records', async (req, res) => {
try {
const { year, month } = req.query;
const AttendanceModel = require('../models/attendanceModel');
const startDate = `${year}-${String(month).padStart(2, '0')}-01`;
const endDate = `${year}-${String(month).padStart(2, '0')}-31`;
const records = await AttendanceModel.getDailyRecords(startDate, endDate, req.user.worker_id);
res.json({ success: true, data: records });
} catch (error) {
res.status(500).json({ success: false, error: error.message });
}
});
// 내 연차 잔액 조회
router.get('/me/vacation-balance', async (req, res) => {
try {
const AttendanceModel = require('../models/attendanceModel');
const year = req.query.year || new Date().getFullYear();
const balance = await AttendanceModel.getWorkerVacationBalance(req.user.worker_id, year);
res.json({ success: true, data: balance });
} catch (error) {
res.status(500).json({ success: false, error: error.message });
}
});
// 내 작업 보고서 조회
router.get('/me/work-reports', async (req, res) => {
try {
const { startDate, endDate } = req.query;
const db = require('../config/database');
const reports = await db.query(
'SELECT * FROM daily_work_reports WHERE worker_id = ? AND report_date BETWEEN ? AND ? ORDER BY report_date DESC',
[req.user.worker_id, startDate, endDate]
);
res.json({ success: true, data: reports });
} catch (error) {
res.status(500).json({ success: false, error: error.message });
}
});
// 내 월별 통계
router.get('/me/monthly-stats', async (req, res) => {
try {
const { year, month } = req.query;
const { getDb } = require('../dbPool');
const db = await getDb();
const [stats] = await db.execute(
`SELECT
SUM(total_work_hours) as month_hours,
COUNT(DISTINCT record_date) as work_days
FROM daily_attendance_records
WHERE worker_id = ? AND YEAR(record_date) = ? AND MONTH(record_date) = ?`,
[req.user.worker_id, year, month]
);
res.json({ success: true, data: stats[0] || { month_hours: 0, work_days: 0 } });
} catch (error) {
res.status(500).json({ success: false, error: error.message });
}
});
// ========== 자신의 페이지 권한 조회 (Admin 불필요) ==========
// 📄 사용자 페이지 접근 권한 조회 (자신 또는 Admin)
router.get('/:id/page-access', (req, res, next) => {
const requestedId = parseInt(req.params.id);
const currentUserId = req.user?.user_id;
const userRole = req.user?.role?.toLowerCase();
// 자신의 권한 조회이거나 Admin인 경우 허용
if (requestedId === currentUserId || userRole === 'admin' || userRole === 'system admin') {
return userController.getUserPageAccess(req, res, next);
}
return res.status(403).json({
success: false,
message: '자신의 페이지 권한만 조회할 수 있습니다'
});
});
// ========== 관리자 전용 API ==========
/**
* 모든 라우트에 관리자 권한 적용
*/
router.use(adminOnly);
// 📋 사용자 목록 조회
router.get('/', userController.getAllUsers);
// 👤 특정 사용자 조회
router.get('/:id', userController.getUserById);
// 새 사용자 생성
router.post('/', userController.createUser);
// ✏️ 사용자 정보 수정
router.put('/:id', userController.updateUser);
// 🔄 사용자 상태 변경
router.put('/:id/status', userController.updateUserStatus);
// 🔑 사용자 비밀번호 초기화 (000000)
router.post('/:id/reset-password', userController.resetUserPassword);
// 🗑️ 사용자 비활성화 (Soft Delete)
router.delete('/:id', userController.deleteUser);
// 💀 사용자 영구 삭제 (Hard Delete)
router.delete('/:id/permanent', userController.permanentDeleteUser);
// 🔐 사용자 페이지 접근 권한 업데이트 (Admin만)
router.put('/:id/page-access', userController.updateUserPageAccess);
module.exports = router;

View File

@@ -0,0 +1,34 @@
/**
* vacationBalanceRoutes.js
* 휴가 잔액 관련 라우트
*/
const express = require('express');
const router = express.Router();
const vacationBalanceController = require('../controllers/vacationBalanceController');
// 모든 작업자의 휴가 잔액 조회 (특정 연도)
router.get('/year/:year', vacationBalanceController.getAllByYear);
// 특정 작업자의 휴가 잔액 조회 (특정 연도)
router.get('/worker/:workerId/year/:year', vacationBalanceController.getByWorkerAndYear);
// 작업자의 사용 가능한 휴가 일수 조회
router.get('/worker/:workerId/year/:year/available', vacationBalanceController.getAvailableDays);
// 근속년수 기반 연차 자동 계산 및 생성 (관리자만)
router.post('/auto-calculate', vacationBalanceController.autoCalculateAndCreate);
// 휴가 잔액 생성 (관리자만)
router.post('/', vacationBalanceController.createBalance);
// 휴가 잔액 일괄 저장 (upsert)
router.post('/bulk-upsert', vacationBalanceController.bulkUpsert);
// 휴가 잔액 수정 (관리자만)
router.put('/:id', vacationBalanceController.updateBalance);
// 휴가 잔액 삭제 (관리자만)
router.delete('/:id', vacationBalanceController.deleteBalance);
module.exports = router;

View File

@@ -0,0 +1,34 @@
/**
* vacationRequestRoutes.js
* 휴가 신청 관련 라우트
*/
const express = require('express');
const router = express.Router();
const vacationRequestController = require('../controllers/vacationRequestController');
// 휴가 신청 생성
router.post('/', vacationRequestController.createRequest);
// 휴가 신청 목록 조회
router.get('/', vacationRequestController.getAllRequests);
// 대기 중인 휴가 신청 목록 (관리자용)
router.get('/pending', vacationRequestController.getPendingRequests);
// 특정 휴가 신청 조회
router.get('/:id', vacationRequestController.getRequestById);
// 휴가 신청 수정
router.put('/:id', vacationRequestController.updateRequest);
// 휴가 신청 삭제
router.delete('/:id', vacationRequestController.deleteRequest);
// 휴가 신청 승인
router.patch('/:id/approve', vacationRequestController.approveRequest);
// 휴가 신청 거부
router.patch('/:id/reject', vacationRequestController.rejectRequest);
module.exports = router;

View File

@@ -0,0 +1,31 @@
/**
* vacationTypeRoutes.js
* 휴가 유형 관련 라우트
*/
const express = require('express');
const router = express.Router();
const vacationTypeController = require('../controllers/vacationTypeController');
// 모든 활성 휴가 유형 조회
router.get('/', vacationTypeController.getAllTypes);
// 시스템 기본 휴가 유형 조회
router.get('/system', vacationTypeController.getSystemTypes);
// 특별 휴가 유형 조회
router.get('/special', vacationTypeController.getSpecialTypes);
// 휴가 유형 우선순위 일괄 업데이트 (관리자만)
router.put('/priorities', vacationTypeController.updatePriorities);
// 특별 휴가 유형 생성 (관리자만)
router.post('/', vacationTypeController.createType);
// 휴가 유형 수정 (관리자만)
router.put('/:id', vacationTypeController.updateType);
// 특별 휴가 유형 삭제 (관리자만, 시스템 기본 휴가는 삭제 불가)
router.delete('/:id', vacationTypeController.deleteType);
module.exports = router;

View File

@@ -0,0 +1,66 @@
const express = require('express');
const router = express.Router();
const visitRequestController = require('../controllers/visitRequestController');
const { verifyToken } = require('../middlewares/authMiddleware');
// 모든 라우트에 인증 미들웨어 적용
router.use(verifyToken);
// ==================== 출입 신청 관리 ====================
// 출입 신청 생성
router.post('/requests', visitRequestController.createVisitRequest);
// 출입 신청 목록 조회 (필터: status, visit_date, start_date, end_date, requester_id, category_id)
router.get('/requests', visitRequestController.getAllVisitRequests);
// 출입 신청 상세 조회
router.get('/requests/:id', visitRequestController.getVisitRequestById);
// 출입 신청 수정
router.put('/requests/:id', visitRequestController.updateVisitRequest);
// 출입 신청 삭제
router.delete('/requests/:id', visitRequestController.deleteVisitRequest);
// 출입 신청 승인
router.put('/requests/:id/approve', visitRequestController.approveVisitRequest);
// 출입 신청 반려
router.put('/requests/:id/reject', visitRequestController.rejectVisitRequest);
// ==================== 방문 목적 관리 ====================
// 모든 방문 목적 조회
router.get('/purposes', visitRequestController.getAllVisitPurposes);
// 활성 방문 목적만 조회
router.get('/purposes/active', visitRequestController.getActiveVisitPurposes);
// 방문 목적 추가
router.post('/purposes', visitRequestController.createVisitPurpose);
// 방문 목적 수정
router.put('/purposes/:id', visitRequestController.updateVisitPurpose);
// 방문 목적 삭제
router.delete('/purposes/:id', visitRequestController.deleteVisitPurpose);
// ==================== 안전교육 기록 관리 ====================
// 안전교육 기록 생성
router.post('/training', visitRequestController.createTrainingRecord);
// 안전교육 기록 목록 조회 (필터: training_date, start_date, end_date, trainer_id)
router.get('/training', visitRequestController.getTrainingRecords);
// 특정 출입 신청의 안전교육 기록 조회
router.get('/training/request/:requestId', visitRequestController.getTrainingRecordByRequestId);
// 안전교육 기록 수정
router.put('/training/:id', visitRequestController.updateTrainingRecord);
// 안전교육 완료 (서명 포함)
router.post('/training/:id/complete', visitRequestController.completeTraining);
module.exports = router;

View File

@@ -0,0 +1,74 @@
// routes/workAnalysis.js
const express = require('express');
const router = express.Router();
const workAnalysisController = require('../controllers/workAnalysisController');
// 🔒 분석 기능은 admin 또는 system 권한만 접근 가능
const requireAnalysisAccess = (req, res, next) => {
if (!req.user) {
return res.status(401).json({ error: '인증이 필요합니다.' });
}
const allowedLevels = ['admin', 'system'];
if (!allowedLevels.includes(req.user.access_level)) {
return res.status(403).json({
error: '분석 기능 접근 권한이 없습니다. 관리자 권한이 필요합니다.',
required: 'admin 또는 system',
current: req.user.access_level
});
}
console.log(`🔓 분석 기능 접근 허용: ${req.user.username} (${req.user.access_level})`);
next();
};
// 임시로 권한 체크 건너뛰기 (테스트용)
const skipAuth = (req, res, next) => {
console.log('⚠️ 임시로 권한 체크 건너뛰기');
next();
};
// 기본 통계 조회 - 임시로 권한 체크 비활성화
router.get('/stats', skipAuth, workAnalysisController.getStats);
// 일별 작업시간 추이 - 임시로 권한 체크 비활성화
router.get('/daily-trend', skipAuth, workAnalysisController.getDailyTrend);
// 작업자별 통계 - 임시로 권한 체크 비활성화
router.get('/worker-stats', skipAuth, workAnalysisController.getWorkerStats);
// 프로젝트별 통계 - 임시로 권한 체크 비활성화
router.get('/project-stats', skipAuth, workAnalysisController.getProjectStats);
// 작업유형별 통계 - 임시로 권한 체크 비활성화
router.get('/worktype-stats', skipAuth, workAnalysisController.getWorkTypeStats);
// 최근 작업 현황 - 임시로 권한 체크 비활성화
router.get('/recent-work', skipAuth, workAnalysisController.getRecentWork);
// 요일별 패턴 분석
router.get('/weekday-pattern', requireAnalysisAccess, workAnalysisController.getWeekdayPattern);
// 에러 분석
router.get('/error-analysis', requireAnalysisAccess, workAnalysisController.getErrorAnalysis);
// 월별 비교 분석
router.get('/monthly-comparison', requireAnalysisAccess, workAnalysisController.getMonthlyComparison);
// 작업자별 전문분야 분석
router.get('/worker-specialization', requireAnalysisAccess, workAnalysisController.getWorkerSpecialization);
// 대시보드용 종합 데이터 (한 번에 여러 데이터 조회)
router.get('/dashboard', requireAnalysisAccess, workAnalysisController.getDashboardData);
// 헬스체크 - 인증 없이 접근 가능
router.get('/health', (req, res) => {
res.status(200).json({
success: true,
message: 'Work Analysis API is running',
timestamp: new Date().toISOString()
});
});
module.exports = router;

View File

@@ -0,0 +1,65 @@
// routes/workAnalysisRoutes.js
const express = require('express');
const router = express.Router();
const workAnalysisController = require('../controllers/workAnalysisController');
// 🏠 대시보드용 종합 데이터 (가장 많이 사용될 것 같아서 맨 위에)
router.get('/dashboard', workAnalysisController.getDashboardData);
// 📊 기본 통계
router.get('/stats', workAnalysisController.getStats);
// 📈 일별 작업시간 추이
router.get('/daily-trend', workAnalysisController.getDailyTrend);
// 👥 작업자별 통계
router.get('/worker-stats', workAnalysisController.getWorkerStats);
// 📋 프로젝트별 통계
router.get('/project-stats', workAnalysisController.getProjectStats);
// 🔧 작업유형별 통계
router.get('/work-type-stats', workAnalysisController.getWorkTypeStats);
// 🕐 최근 작업 현황
router.get('/recent-work', workAnalysisController.getRecentWork);
// 📅 요일별 패턴 분석
router.get('/weekday-pattern', workAnalysisController.getWeekdayPattern);
// ❌ 에러 분석
router.get('/error-analysis', workAnalysisController.getErrorAnalysis);
// 📊 월별 비교 분석
router.get('/monthly-comparison', workAnalysisController.getMonthlyComparison);
// 🎯 작업자별 전문분야 분석
router.get('/worker-specialization', workAnalysisController.getWorkerSpecialization);
// 🏗️ 프로젝트별-작업별 시간 분석 (총시간, 정규시간, 에러시간)
router.get('/project-worktype-analysis', workAnalysisController.getProjectWorkTypeAnalysis);
// 📋 헬스체크 및 API 정보
router.get('/health', (req, res) => {
res.json({
success: true,
message: '작업 분석 API가 정상 작동 중입니다.',
endpoints: [
'GET /work-analysis/dashboard - 대시보드 종합 데이터',
'GET /work-analysis/stats - 기본 통계',
'GET /work-analysis/daily-trend - 일별 추이',
'GET /work-analysis/worker-stats - 작업자별 통계',
'GET /work-analysis/project-stats - 프로젝트별 통계',
'GET /work-analysis/work-type-stats - 작업유형별 통계',
'GET /work-analysis/recent-work - 최근 작업 현황',
'GET /work-analysis/weekday-pattern - 요일별 패턴',
'GET /work-analysis/error-analysis - 에러 분석',
'GET /work-analysis/monthly-comparison - 월별 비교',
'GET /work-analysis/worker-specialization - 작업자 전문분야',
'GET /work-analysis/project-worktype-analysis - 프로젝트별-작업별 시간 분석'
],
timestamp: new Date().toISOString()
});
});
module.exports = router;

View File

@@ -0,0 +1,92 @@
/**
* 작업 중 문제 신고 라우터
*/
const express = require('express');
const router = express.Router();
const workIssueController = require('../controllers/workIssueController');
const { requireMinLevel } = require('../middlewares/auth');
// ==================== 카테고리 관리 ====================
// 모든 카테고리 조회
router.get('/categories', workIssueController.getAllCategories);
// 타입별 카테고리 조회 (nonconformity/safety)
router.get('/categories/type/:type', workIssueController.getCategoriesByType);
// 카테고리 생성 (admin 이상)
router.post('/categories', requireMinLevel('admin'), workIssueController.createCategory);
// 카테고리 수정 (admin 이상)
router.put('/categories/:id', requireMinLevel('admin'), workIssueController.updateCategory);
// 카테고리 삭제 (admin 이상)
router.delete('/categories/:id', requireMinLevel('admin'), workIssueController.deleteCategory);
// ==================== 사전 정의 항목 관리 ====================
// 모든 항목 조회
router.get('/items', workIssueController.getAllItems);
// 카테고리별 항목 조회
router.get('/items/category/:categoryId', workIssueController.getItemsByCategory);
// 항목 생성 (admin 이상)
router.post('/items', requireMinLevel('admin'), workIssueController.createItem);
// 항목 수정 (admin 이상)
router.put('/items/:id', requireMinLevel('admin'), workIssueController.updateItem);
// 항목 삭제 (admin 이상)
router.delete('/items/:id', requireMinLevel('admin'), workIssueController.deleteItem);
// ==================== 통계 ====================
// 통계 요약 (support_team 이상)
router.get('/stats/summary', requireMinLevel('support_team'), workIssueController.getStatsSummary);
// 카테고리별 통계 (support_team 이상)
router.get('/stats/by-category', requireMinLevel('support_team'), workIssueController.getStatsByCategory);
// 작업장별 통계 (support_team 이상)
router.get('/stats/by-workplace', requireMinLevel('support_team'), workIssueController.getStatsByWorkplace);
// ==================== 문제 신고 관리 ====================
// 신고 목록 조회
router.get('/', workIssueController.getAllReports);
// 신고 생성
router.post('/', workIssueController.createReport);
// 신고 상세 조회
router.get('/:id', workIssueController.getReportById);
// 신고 수정
router.put('/:id', workIssueController.updateReport);
// 신고 삭제
router.delete('/:id', workIssueController.deleteReport);
// ==================== 상태 관리 ====================
// 신고 접수 (support_team 이상)
router.put('/:id/receive', requireMinLevel('support_team'), workIssueController.receiveReport);
// 담당자 배정 (support_team 이상)
router.put('/:id/assign', requireMinLevel('support_team'), workIssueController.assignReport);
// 처리 시작
router.put('/:id/start', workIssueController.startProcessing);
// 처리 완료
router.put('/:id/complete', workIssueController.completeReport);
// 신고 종료 (admin 이상)
router.put('/:id/close', requireMinLevel('admin'), workIssueController.closeReport);
// 상태 변경 이력 조회
router.get('/:id/logs', workIssueController.getStatusLogs);
module.exports = router;

View File

@@ -0,0 +1,23 @@
// routes/workReportAnalysisRoutes.js - 데일리 워크 레포트 분석 라우트
const express = require('express');
const router = express.Router();
const workReportAnalysisController = require('../controllers/workReportAnalysisController');
const { requireAuth, requireRole } = require('../middlewares/auth');
// 🔒 모든 분석 라우트에 인증 + Admin 권한 필요
router.use(requireAuth);
router.use(requireRole('admin', 'system'));
// 📋 분석용 필터 데이터 조회 (프로젝트, 작업자, 작업유형 목록)
router.get('/filters', workReportAnalysisController.getAnalysisFilters);
// 📊 기간별 종합 분석
router.get('/period', workReportAnalysisController.getAnalyticsByPeriod);
// 📈 프로젝트별 상세 분석
router.get('/project', workReportAnalysisController.getProjectAnalysis);
// 👤 작업자별 상세 분석
router.get('/worker', workReportAnalysisController.getWorkerAnalysis);
module.exports = router;

View File

@@ -0,0 +1,39 @@
const express = require('express');
const router = express.Router();
const workReportController = require('../controllers/workReportController');
// CREATE
router.post('/', workReportController.createWorkReport);
// READ BY DATE
router.get('/date/:date', workReportController.getWorkReportsByDate);
// ✅ summary 라우트는 반드시 아래보다 위에 둬야 작동합니다
router.get('/summary', workReportController.getSummary);
// READ IN RANGE
router.get('/', workReportController.getWorkReportsInRange);
// READ ONE (항상 가장 마지막)
router.get('/:id', workReportController.getWorkReportById);
// UPDATE
router.put('/:id', workReportController.updateWorkReport);
// DELETE
router.delete('/:id', workReportController.removeWorkReport);
// ========== 부적합 원인 관리 ==========
// 작업 보고서의 부적합 원인 목록 조회
router.get('/:reportId/defects', workReportController.getReportDefects);
// 부적합 원인 저장 (전체 교체)
router.put('/:reportId/defects', workReportController.saveReportDefects);
// 부적합 원인 추가 (단일)
router.post('/:reportId/defects', workReportController.addReportDefect);
// 부적합 원인 삭제
router.delete('/defects/:defectId', workReportController.removeReportDefect);
module.exports = router;

View File

@@ -0,0 +1,228 @@
/**
* @swagger
* tags:
* name: Workers
* description: 작업자 관리 API
*/
const express = require('express');
const router = express.Router();
const workerController = require('../controllers/workerController');
/**
* @swagger
* /api/workers:
* post:
* tags: [Workers]
* summary: 작업자 생성
* description: 새로운 작업자를 생성합니다.
* security:
* - bearerAuth: []
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - worker_name
* properties:
* worker_name:
* type: string
* example: "김철수"
* position:
* type: string
* example: "용접공"
* department:
* type: string
* example: "생산부"
* phone:
* type: string
* example: "010-1234-5678"
* email:
* type: string
* format: email
* example: "worker@technicalkorea.com"
* responses:
* 201:
* description: 작업자 생성 성공
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/SuccessResponse'
* 400:
* description: 잘못된 요청
* 401:
* description: 인증 필요
* 500:
* description: 서버 오류
* get:
* tags: [Workers]
* summary: 전체 작업자 조회
* description: 모든 작업자 목록을 조회합니다.
* security:
* - bearerAuth: []
* responses:
* 200:
* description: 작업자 목록 조회 성공
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* example: true
* message:
* type: string
* example: "작업자 목록 조회 성공"
* data:
* type: array
* items:
* $ref: '#/components/schemas/Worker'
* meta:
* type: object
* properties:
* count:
* type: integer
* example: 10
* 401:
* description: 인증 필요
* 500:
* description: 서버 오류
*/
router.post('/', workerController.createWorker);
router.get('/', workerController.getAllWorkers);
/**
* @swagger
* /api/workers/{worker_id}:
* get:
* tags: [Workers]
* summary: 특정 작업자 조회
* description: ID로 특정 작업자 정보를 조회합니다.
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: worker_id
* required: true
* schema:
* type: integer
* description: 작업자 ID
* responses:
* 200:
* description: 작업자 조회 성공
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* example: true
* message:
* type: string
* example: "작업자 조회 성공"
* data:
* $ref: '#/components/schemas/Worker'
* 400:
* description: 잘못된 작업자 ID
* 401:
* description: 인증 필요
* 404:
* description: 작업자를 찾을 수 없음
* 500:
* description: 서버 오류
* put:
* tags: [Workers]
* summary: 작업자 정보 수정
* description: 작업자 정보를 수정합니다.
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: worker_id
* required: true
* schema:
* type: integer
* description: 작업자 ID
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* worker_name:
* type: string
* example: "김철수"
* position:
* type: string
* example: "용접공"
* department:
* type: string
* example: "생산부"
* phone:
* type: string
* example: "010-1234-5678"
* email:
* type: string
* format: email
* example: "worker@technicalkorea.com"
* responses:
* 200:
* description: 작업자 수정 성공
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/SuccessResponse'
* 400:
* description: 잘못된 요청
* 401:
* description: 인증 필요
* 404:
* description: 작업자를 찾을 수 없음
* 500:
* description: 서버 오류
* delete:
* tags: [Workers]
* summary: 작업자 삭제
* description: 작업자를 삭제합니다.
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: worker_id
* required: true
* schema:
* type: integer
* description: 작업자 ID
* responses:
* 200:
* description: 작업자 삭제 성공
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* example: true
* message:
* type: string
* example: "작업자가 성공적으로 삭제되었습니다."
* 400:
* description: 잘못된 작업자 ID
* 401:
* description: 인증 필요
* 404:
* description: 작업자를 찾을 수 없음
* 500:
* description: 서버 오류
*/
router.get('/:worker_id', workerController.getWorkerById);
router.put('/:worker_id', workerController.updateWorker);
router.delete('/:worker_id', workerController.removeWorker);
module.exports = router;

View File

@@ -0,0 +1,103 @@
// routes/workplaceRoutes.js
const express = require('express');
const router = express.Router();
const multer = require('multer');
const path = require('path');
const workplaceController = require('../controllers/workplaceController');
const {
generateSafeFilename,
createFileFilter,
ALLOWED_IMAGE_EXTENSIONS
} = require('../utils/fileUploadSecurity');
// Multer 설정 - 작업장 레이아웃 이미지 업로드 (보안 강화)
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, path.join(__dirname, '../uploads'));
},
filename: (req, file, cb) => {
// 안전한 랜덤 파일명 생성 (원본 파일명 노출 방지)
const safeName = generateSafeFilename(file.originalname);
cb(null, `workplace-layout-${safeName}`);
}
});
// 보안 강화된 파일 필터
const imageFileFilter = createFileFilter({
allowedExtensions: ALLOWED_IMAGE_EXTENSIONS,
allowedMimes: ['image/jpeg', 'image/png', 'image/gif', 'image/webp']
});
const upload = multer({
storage,
fileFilter: imageFileFilter,
limits: {
fileSize: 5 * 1024 * 1024, // 5MB 제한
files: 1 // 단일 파일만 허용
}
});
// ==================== 카테고리(공장) 관리 ====================
// CREATE 카테고리
router.post('/categories', workplaceController.createCategory);
// READ ALL 카테고리
router.get('/categories', workplaceController.getAllCategories);
// READ ACTIVE 카테고리
router.get('/categories/active/list', workplaceController.getActiveCategories);
// READ ONE 카테고리
router.get('/categories/:id', workplaceController.getCategoryById);
// UPDATE 카테고리
router.put('/categories/:id', workplaceController.updateCategory);
// DELETE 카테고리
router.delete('/categories/:id', workplaceController.deleteCategory);
// ==================== 작업장 관리 ====================
// CREATE 작업장
router.post('/', workplaceController.createWorkplace);
// READ ALL 작업장 (쿼리 파라미터로 카테고리 필터링 가능: ?category_id=1)
router.get('/', workplaceController.getAllWorkplaces);
// READ ACTIVE 작업장
router.get('/active/list', workplaceController.getActiveWorkplaces);
// READ ONE 작업장
router.get('/:id', workplaceController.getWorkplaceById);
// UPDATE 작업장
router.put('/:id', workplaceController.updateWorkplace);
// DELETE 작업장
router.delete('/:id', workplaceController.deleteWorkplace);
// ==================== 작업장 지도 영역 관리 ====================
// 카테고리 레이아웃 이미지 업로드
router.post('/categories/:id/layout-image', upload.single('image'), workplaceController.uploadCategoryLayoutImage);
// 작업장 레이아웃 이미지 업로드
router.post('/:id/layout-image', upload.single('image'), workplaceController.uploadWorkplaceLayoutImage);
// CREATE 지도 영역
router.post('/map-regions', workplaceController.createMapRegion);
// READ 카테고리별 지도 영역
router.get('/categories/:categoryId/map-regions', workplaceController.getMapRegionsByCategory);
// READ 작업장별 지도 영역
router.get('/map-regions/workplace/:workplaceId', workplaceController.getMapRegionByWorkplace);
// UPDATE 지도 영역
router.put('/map-regions/:id', workplaceController.updateMapRegion);
// DELETE 지도 영역
router.delete('/map-regions/:id', workplaceController.deleteMapRegion);
module.exports = router;