feat: 다수 기능 개선 - 순찰, 출근, 작업분석, 모바일 UI 등
- 순찰/점검 기능 개선 (zone-detail 페이지 추가) - 출근/근태 시스템 개선 (연차 조회, 근무현황) - 작업분석 대분류 그룹화 및 마이그레이션 스크립트 - 모바일 네비게이션 UI 추가 - NAS 배포 도구 및 문서 추가 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
10
deploy/tkfb-package/api.hyungi.net/routes/analysisRoutes.js
Normal file
10
deploy/tkfb-package/api.hyungi.net/routes/analysisRoutes.js
Normal 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;
|
||||
@@ -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;
|
||||
215
deploy/tkfb-package/api.hyungi.net/routes/auth.js
Normal file
215
deploy/tkfb-package/api.hyungi.net/routes/auth.js
Normal 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;
|
||||
978
deploy/tkfb-package/api.hyungi.net/routes/authRoutes.js
Normal file
978
deploy/tkfb-package/api.hyungi.net/routes/authRoutes.js
Normal 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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
103
deploy/tkfb-package/api.hyungi.net/routes/equipmentRoutes.js
Normal file
103
deploy/tkfb-package/api.hyungi.net/routes/equipmentRoutes.js
Normal 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;
|
||||
123
deploy/tkfb-package/api.hyungi.net/routes/healthRoutes.js
Normal file
123
deploy/tkfb-package/api.hyungi.net/routes/healthRoutes.js
Normal 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;
|
||||
10
deploy/tkfb-package/api.hyungi.net/routes/issueTypeRoutes.js
Normal file
10
deploy/tkfb-package/api.hyungi.net/routes/issueTypeRoutes.js
Normal 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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
237
deploy/tkfb-package/api.hyungi.net/routes/pageAccessRoutes.js
Normal file
237
deploy/tkfb-package/api.hyungi.net/routes/pageAccessRoutes.js
Normal 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;
|
||||
126
deploy/tkfb-package/api.hyungi.net/routes/patrolRoutes.js
Normal file
126
deploy/tkfb-package/api.hyungi.net/routes/patrolRoutes.js
Normal 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;
|
||||
388
deploy/tkfb-package/api.hyungi.net/routes/performanceRoutes.js
Normal file
388
deploy/tkfb-package/api.hyungi.net/routes/performanceRoutes.js
Normal 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;
|
||||
19
deploy/tkfb-package/api.hyungi.net/routes/projectRoutes.js
Normal file
19
deploy/tkfb-package/api.hyungi.net/routes/projectRoutes.js
Normal 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;
|
||||
717
deploy/tkfb-package/api.hyungi.net/routes/setupRoutes.js
Normal file
717
deploy/tkfb-package/api.hyungi.net/routes/setupRoutes.js
Normal 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;
|
||||
378
deploy/tkfb-package/api.hyungi.net/routes/systemRoutes.js
Normal file
378
deploy/tkfb-package/api.hyungi.net/routes/systemRoutes.js
Normal 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;
|
||||
27
deploy/tkfb-package/api.hyungi.net/routes/taskRoutes.js
Normal file
27
deploy/tkfb-package/api.hyungi.net/routes/taskRoutes.js
Normal 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;
|
||||
103
deploy/tkfb-package/api.hyungi.net/routes/tbmRoutes.js
Normal file
103
deploy/tkfb-package/api.hyungi.net/routes/tbmRoutes.js
Normal 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;
|
||||
16
deploy/tkfb-package/api.hyungi.net/routes/toolsRoute.js
Normal file
16
deploy/tkfb-package/api.hyungi.net/routes/toolsRoute.js
Normal 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;
|
||||
52
deploy/tkfb-package/api.hyungi.net/routes/uploadBgRoutes.js
Normal file
52
deploy/tkfb-package/api.hyungi.net/routes/uploadBgRoutes.js
Normal 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;
|
||||
10
deploy/tkfb-package/api.hyungi.net/routes/uploadRoutes.js
Normal file
10
deploy/tkfb-package/api.hyungi.net/routes/uploadRoutes.js
Normal 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;
|
||||
167
deploy/tkfb-package/api.hyungi.net/routes/userRoutes.js
Normal file
167
deploy/tkfb-package/api.hyungi.net/routes/userRoutes.js
Normal 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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
74
deploy/tkfb-package/api.hyungi.net/routes/workAnalysis.js
Normal file
74
deploy/tkfb-package/api.hyungi.net/routes/workAnalysis.js
Normal 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;
|
||||
@@ -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;
|
||||
92
deploy/tkfb-package/api.hyungi.net/routes/workIssueRoutes.js
Normal file
92
deploy/tkfb-package/api.hyungi.net/routes/workIssueRoutes.js
Normal 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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
228
deploy/tkfb-package/api.hyungi.net/routes/workerRoutes.js
Normal file
228
deploy/tkfb-package/api.hyungi.net/routes/workerRoutes.js
Normal 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;
|
||||
103
deploy/tkfb-package/api.hyungi.net/routes/workplaceRoutes.js
Normal file
103
deploy/tkfb-package/api.hyungi.net/routes/workplaceRoutes.js
Normal 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;
|
||||
Reference in New Issue
Block a user