refactor: 보안 취약점 제거 + 데드코드 정리 + 프론트엔드 중복 통합

- 인증 없는 임시 엔드포인트 삭제 (index.js, healthRoutes.js, publicPaths)
- skipAuth 우회 라우트 삭제 (workAnalysis.js)
- 하드코딩 유저 백도어 삭제 (routes/auth.js)
- 안전체크 CRUD에 admin 권한 추가 (tbmRoutes.js)
- deprecated shim 3개 삭제 + 8개 소비 파일 import 정리 (auth.js 직접 참조)
- 미사용 pageAccessController, db.js, common/security.js 삭제
- escapeHtml() 5곳 로컬 중복 제거 → api-base.js 전역 사용
- userPageAccess_v2_v2 캐시 키 버그 수정 (app-init.js)
- system3 .bak 파일 삭제, PROGRESS.md 업데이트

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Hyungi Ahn
2026-02-25 08:19:01 +09:00
parent 7637be33f3
commit 93edf9529a
28 changed files with 136 additions and 2192 deletions

View File

@@ -9,7 +9,7 @@
const swaggerUi = require('swagger-ui-express');
const swaggerSpec = require('./swagger');
const { verifyToken } = require('../middlewares/authMiddleware');
const { verifyToken } = require('../middlewares/auth');
const { activityLogger } = require('../middlewares/activityLogger');
const logger = require('../utils/logger');
@@ -107,10 +107,7 @@ function setupRoutes(app) {
'/api/setup/migrate-existing-data',
'/api/setup/check-data-status',
'/api/monthly-status/calendar',
'/api/monthly-status/daily-details',
'/api/migrate-work-type-id', // 임시 마이그레이션 - 실행 후 삭제!
'/api/diagnose-work-type-id', // 임시 진단 - 실행 후 삭제!
'/api/test-analysis' // 임시 분석 테스트 - 실행 후 삭제!
'/api/monthly-status/daily-details'
];
// 인증 미들웨어 - 공개 경로를 제외한 모든 API (rate limiter보다 먼저 실행)

View File

@@ -1,200 +0,0 @@
// controllers/pageAccessController.js
const PageAccessModel = require('../models/pageAccessModel');
const PageAccessController = {
// 사용자의 페이지 권한 조회
getUserPageAccess: (req, res) => {
const userId = parseInt(req.params.userId);
if (isNaN(userId)) {
return res.status(400).json({
success: false,
message: '유효하지 않은 사용자 ID입니다.'
});
}
PageAccessModel.getUserPageAccess(userId, (err, results) => {
if (err) {
console.error('페이지 권한 조회 오류:', err);
return res.status(500).json({
success: false,
message: '페이지 권한 조회 중 오류가 발생했습니다.',
error: err.message
});
}
res.json({
success: true,
data: results
});
});
},
// 모든 페이지 목록 조회
getAllPages: (req, res) => {
PageAccessModel.getAllPages((err, results) => {
if (err) {
console.error('페이지 목록 조회 오류:', err);
return res.status(500).json({
success: false,
message: '페이지 목록 조회 중 오류가 발생했습니다.',
error: err.message
});
}
res.json({
success: true,
data: results
});
});
},
// 페이지 권한 부여
grantPageAccess: (req, res) => {
const userId = parseInt(req.params.userId);
const { pageId } = req.body;
const grantedBy = req.user.user_id;
if (isNaN(userId) || !pageId) {
return res.status(400).json({
success: false,
message: '필수 파라미터가 누락되었습니다.'
});
}
PageAccessModel.grantPageAccess(userId, pageId, grantedBy, (err, result) => {
if (err) {
console.error('페이지 권한 부여 오류:', err);
return res.status(500).json({
success: false,
message: '페이지 권한 부여 중 오류가 발생했습니다.',
error: err.message
});
}
res.json({
success: true,
message: '페이지 권한이 부여되었습니다.',
data: result
});
});
},
// 페이지 권한 회수
revokePageAccess: (req, res) => {
const userId = parseInt(req.params.userId);
const pageId = parseInt(req.params.pageId);
if (isNaN(userId) || isNaN(pageId)) {
return res.status(400).json({
success: false,
message: '유효하지 않은 파라미터입니다.'
});
}
PageAccessModel.revokePageAccess(userId, pageId, (err, result) => {
if (err) {
console.error('페이지 권한 회수 오류:', err);
return res.status(500).json({
success: false,
message: '페이지 권한 회수 중 오류가 발생했습니다.',
error: err.message
});
}
res.json({
success: true,
message: '페이지 권한이 회수되었습니다.',
data: result
});
});
},
// 사용자 페이지 권한 일괄 설정
setUserPageAccess: (req, res) => {
const userId = parseInt(req.params.userId);
const { pageIds } = req.body;
const grantedBy = req.user.user_id;
if (isNaN(userId)) {
return res.status(400).json({
success: false,
message: '유효하지 않은 사용자 ID입니다.'
});
}
if (!Array.isArray(pageIds)) {
return res.status(400).json({
success: false,
message: 'pageIds는 배열이어야 합니다.'
});
}
PageAccessModel.setUserPageAccess(userId, pageIds, grantedBy, (err, result) => {
if (err) {
console.error('페이지 권한 설정 오류:', err);
return res.status(500).json({
success: false,
message: '페이지 권한 설정 중 오류가 발생했습니다.',
error: err.message
});
}
res.json({
success: true,
message: '페이지 권한이 설정되었습니다.',
data: result
});
});
},
// 특정 페이지 접근 권한 확인
checkPageAccess: (req, res) => {
const userId = req.user.user_id;
const { pageKey } = req.params;
if (!pageKey) {
return res.status(400).json({
success: false,
message: '페이지 키가 필요합니다.'
});
}
PageAccessModel.checkPageAccess(userId, pageKey, (err, result) => {
if (err) {
console.error('페이지 접근 권한 확인 오류:', err);
return res.status(500).json({
success: false,
message: '페이지 접근 권한 확인 중 오류가 발생했습니다.',
error: err.message
});
}
res.json({
success: true,
data: result
});
});
},
// 계정이 있는 사용자 목록 조회 (권한 관리용)
getUsersWithAccounts: (req, res) => {
PageAccessModel.getUsersWithAccounts((err, results) => {
if (err) {
console.error('사용자 목록 조회 오류:', err);
return res.status(500).json({
success: false,
message: '사용자 목록 조회 중 오류가 발생했습니다.',
error: err.message
});
}
res.json({
success: true,
data: results
});
});
}
};
module.exports = PageAccessController;

View File

@@ -1,35 +0,0 @@
require('dotenv').config();
const mysql = require('mysql2/promise');
const retry = require('async-retry');
// 초기화된 pool을 export 하기 위한 변수
let pool = null;
const initPool = async () => {
if (pool) return pool; // 이미 초기화된 경우 재사용
await retry(async () => {
pool = mysql.createPool({
host: process.env.DB_HOST,
port: process.env.DB_PORT || 3306,
user: process.env.DB_USER,
password: process.env.DB_PASS,
database: process.env.DB_NAME,
waitForConnections: true,
connectionLimit: 10,
queueLimit: 0
});
const conn = await pool.getConnection();
await conn.query('SET FOREIGN_KEY_CHECKS = 1');
console.log(`✅ MariaDB 연결 성공: ${process.env.DB_HOST}:${process.env.DB_PORT || 3306}/${process.env.DB_NAME}`);
conn.release();
}, {
retries: 10,
minTimeout: 3000
});
return pool;
};
module.exports = initPool;

View File

@@ -22,222 +22,6 @@ const PORT = process.env.PORT || 20005;
// Trust proxy for accurate IP addresses
app.set('trust proxy', 1);
// JSON body parser 미리 적용 (마이그레이션용)
app.use(express.json());
// 임시 분석 테스트 엔드포인트 - 실행 후 삭제!
app.get('/api/test-analysis', async (req, res) => {
try {
const { getDb } = require('./dbPool');
const db = await getDb();
// 수정된 COALESCE 로직 테스트 (tasks 우선)
const [results] = await db.query(`
SELECT
dwr.id,
w.worker_name,
dwr.report_date,
dwr.work_type_id as original_work_type_id,
COALESCE(
CASE WHEN t.task_id IS NOT NULL THEN t.work_type_id ELSE NULL END,
wt.id
) as resolved_work_type_id,
COALESCE(
CASE WHEN t.task_id IS NOT NULL THEN wt2.name ELSE NULL END,
wt.name
) as work_type_name,
t.task_name,
wt.name as direct_match_work_type,
wt2.name as task_work_type
FROM daily_work_reports dwr
LEFT JOIN workers w ON dwr.worker_id = w.worker_id
LEFT JOIN work_types wt ON dwr.work_type_id = wt.id
LEFT JOIN tasks t ON dwr.work_type_id = t.task_id
LEFT JOIN work_types wt2 ON t.work_type_id = wt2.id
WHERE w.worker_name LIKE '%조승민%' OR w.worker_name LIKE '%최광욱%'
ORDER BY dwr.report_date DESC
LIMIT 20
`);
res.json({
success: true,
message: 'tasks 테이블 우선 조회 결과',
data: results.map(r => ({
id: r.id,
worker: r.worker_name,
date: r.report_date,
original_id: r.original_work_type_id,
resolved_work_type: r.work_type_name,
task: r.task_name,
note: `원래 ID ${r.original_work_type_id}${r.work_type_name}`
}))
});
} catch (error) {
console.error('테스트 실패:', error);
res.status(500).json({ success: false, error: error.message });
}
});
// 임시 진단 엔드포인트 - 실행 후 삭제!
app.get('/api/diagnose-work-type-id', async (req, res) => {
try {
const { getDb } = require('./dbPool');
const db = await getDb();
// 1. 전체 작업보고서 현황
const [totalStats] = await db.query(`
SELECT
COUNT(*) as total_reports,
COUNT(tbm_assignment_id) as tbm_reports,
COUNT(CASE WHEN tbm_assignment_id IS NULL THEN 1 END) as non_tbm_reports
FROM daily_work_reports
`);
// 2. work_type_id 값 분포 (상위 20개)
const [workTypeDistribution] = await db.query(`
SELECT
dwr.work_type_id,
COUNT(*) as count,
wt.name as if_work_type,
t.task_name as if_task,
wt2.name as task_work_type
FROM daily_work_reports dwr
LEFT JOIN work_types wt ON dwr.work_type_id = wt.id
LEFT JOIN tasks t ON dwr.work_type_id = t.task_id
LEFT JOIN work_types wt2 ON t.work_type_id = wt2.id
GROUP BY dwr.work_type_id
ORDER BY count DESC
LIMIT 20
`);
// 3. 특정 작업자 데이터 확인 (조승민, 최광욱)
const [workerSamples] = await db.query(`
SELECT
dwr.id,
w.worker_name,
dwr.work_type_id,
dwr.tbm_assignment_id,
wt.name as direct_work_type,
t.task_name,
wt2.name as task_work_type,
dwr.report_date
FROM daily_work_reports dwr
LEFT JOIN workers w ON dwr.worker_id = w.worker_id
LEFT JOIN work_types wt ON dwr.work_type_id = wt.id
LEFT JOIN tasks t ON dwr.work_type_id = t.task_id
LEFT JOIN work_types wt2 ON t.work_type_id = wt2.id
WHERE w.worker_name LIKE '%조승민%' OR w.worker_name LIKE '%최광욱%'
ORDER BY dwr.report_date DESC
LIMIT 20
`);
res.json({
success: true,
data: {
total_stats: totalStats[0],
work_type_distribution: workTypeDistribution,
worker_samples: workerSamples
}
});
} catch (error) {
console.error('진단 실패:', error);
res.status(500).json({ success: false, error: error.message });
}
});
// 임시 마이그레이션 엔드포인트 (인증 없이 실행) - 실행 후 삭제!
app.post('/api/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 }
});
}
// 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
});
}
});
// 미들웨어 설정
setupMiddlewares(app);

View File

@@ -1,9 +0,0 @@
// utils/access.js
exports.requireAccess = (...allowed) => {
return (req, res, next) => {
if (!req.user || !allowed.includes(req.user.access_level)) {
return res.status(403).json({ error: '접근 권한이 없습니다' });
}
next();
};
};

View File

@@ -1,29 +0,0 @@
/**
* @deprecated 이 파일은 하위 호환성을 위해 유지됩니다.
* 새로운 코드에서는 '../middlewares/auth'의 requireMinLevel을 사용하세요.
*
* @example
* // 이전 방식 (deprecated)
* const { requireAccess, ACCESS_LEVELS } = require('../middlewares/accessMiddleware');
* router.get('/admin', requireAccess('admin'), handler);
*
* // 새로운 방식 (권장)
* const { requireMinLevel, ACCESS_LEVELS } = require('../middlewares/auth');
* router.get('/admin', requireAuth, requireMinLevel('admin'), handler);
*/
const { requireMinLevel, ACCESS_LEVELS } = require('./auth');
/**
* @deprecated requireMinLevel을 사용하세요
*/
const requireAccess = (requiredLevel) => {
return requireMinLevel(requiredLevel);
};
module.exports = {
requireAccess,
ACCESS_LEVELS,
// 새로운 API
requireMinLevel
};

View File

@@ -1,36 +0,0 @@
/**
* @deprecated 이 파일은 하위 호환성을 위해 유지됩니다.
* 새로운 코드에서는 './auth'를 직접 import하세요.
*
* @example
* // 이전 방식 (deprecated)
* const { verifyToken, requireAdmin } = require('../middlewares/authMiddleware');
*
* // 새로운 방식 (권장)
* const { requireAuth, requireRole } = require('../middlewares/auth');
*/
const {
requireAuth,
requireRole,
requireMinLevel,
requireOwnerOrAdmin,
verifyToken,
requireAdmin,
requireSystem,
ACCESS_LEVELS
} = require('./auth');
module.exports = {
// 레거시 별칭 (하위 호환성)
verifyToken,
requireAdmin,
requireSystem,
// 새로운 API (권장)
requireAuth,
requireRole,
requireMinLevel,
requireOwnerOrAdmin,
ACCESS_LEVELS
};

View File

@@ -2,7 +2,7 @@
const express = require('express');
const router = express.Router();
const { getAnalysisData } = require('../controllers/analysisController');
const { verifyToken } = require('../middlewares/authMiddleware'); // 인증 미들웨어 추가
const { verifyToken } = require('../middlewares/auth');
// GET /api/analysis?startDate=...&endDate=...
router.get('/', verifyToken, getAnalysisData);

View File

@@ -1,7 +1,7 @@
const express = require('express');
const router = express.Router();
const AttendanceController = require('../controllers/attendanceController');
const { verifyToken } = require('../middlewares/authMiddleware');
const { verifyToken } = require('../middlewares/auth');
// 모든 라우트에 인증 미들웨어 적용
router.use(verifyToken);

View File

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

View File

@@ -10,7 +10,7 @@ const express = require('express');
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const mysql = require('mysql2/promise');
const { verifyToken } = require('../middlewares/authMiddleware');
const { verifyToken } = require('../middlewares/auth');
const { validatePassword, getPasswordError } = require('../utils/passwordValidator');
const router = express.Router();
const authController = require('../controllers/authController');

View File

@@ -2,7 +2,7 @@
const express = require('express');
const router = express.Router();
const departmentController = require('../controllers/departmentController');
const { requireAuth, requireRole } = require('../middlewares/authMiddleware');
const { requireAuth, requireRole } = require('../middlewares/auth');
// 부서 목록 조회 (인증 필요)
router.get('/', requireAuth, departmentController.getAll);

View File

@@ -25,99 +25,4 @@ router.get('/detail', (req, res) => {
});
});
// 임시 마이그레이션 엔드포인트 - TBM work_type_id 수정
// 실행 후 이 코드를 삭제하세요!
router.post('/migrate-work-type-id', async (req, res) => {
try {
const { getDb } = require('../dbPool');
const db = await getDb();
console.log('🔄 TBM 기반 작업보고서 work_type_id 수정 시작...');
// 1. 수정 대상 확인
const [checkResult] = await db.query(`
SELECT
dwr.id,
dwr.work_type_id as current_work_type_id,
ta.task_id as correct_task_id,
w.worker_name,
dwr.report_date
FROM daily_work_reports dwr
INNER JOIN tbm_team_assignments ta ON dwr.tbm_assignment_id = ta.assignment_id
INNER JOIN workers w ON dwr.worker_id = w.worker_id
WHERE dwr.tbm_assignment_id IS NOT NULL
AND ta.task_id IS NOT NULL
AND dwr.work_type_id != ta.task_id
ORDER BY dwr.report_date DESC
`);
console.log(`📊 수정 대상: ${checkResult.length}개 레코드`);
if (checkResult.length === 0) {
return res.json({
success: true,
message: '수정할 데이터가 없습니다.',
data: { affected_rows: 0 }
});
}
// 수정 전 샘플 로깅
console.log('수정 전 샘플:', checkResult.slice(0, 5));
// 2. 업데이트 실행
const [updateResult] = await db.query(`
UPDATE daily_work_reports dwr
INNER JOIN tbm_team_assignments ta ON dwr.tbm_assignment_id = ta.assignment_id
SET dwr.work_type_id = ta.task_id
WHERE dwr.tbm_assignment_id IS NOT NULL
AND ta.task_id IS NOT NULL
AND dwr.work_type_id != ta.task_id
`);
console.log(`✅ 업데이트 완료: ${updateResult.affectedRows}개 레코드 수정됨`);
// 3. 수정 후 확인
const [samples] = await db.query(`
SELECT
dwr.id,
dwr.work_type_id,
t.task_name,
wt.name as work_type_name,
w.worker_name,
dwr.report_date
FROM daily_work_reports dwr
INNER JOIN tbm_team_assignments ta ON dwr.tbm_assignment_id = ta.assignment_id
LEFT JOIN tasks t ON dwr.work_type_id = t.task_id
LEFT JOIN work_types wt ON t.work_type_id = wt.id
LEFT JOIN workers w ON dwr.worker_id = w.worker_id
WHERE dwr.tbm_assignment_id IS NOT NULL
ORDER BY dwr.report_date DESC
LIMIT 10
`);
res.json({
success: true,
message: `${updateResult.affectedRows}개 레코드가 수정되었습니다.`,
data: {
affected_rows: updateResult.affectedRows,
before_count: checkResult.length,
samples: samples.map(s => ({
id: s.id,
worker: s.worker_name,
date: s.report_date,
task: s.task_name,
work_type: s.work_type_name
}))
}
});
} catch (error) {
console.error('마이그레이션 실패:', error);
res.status(500).json({
success: false,
error: '마이그레이션 실패: ' + error.message
});
}
});
module.exports = router;

View File

@@ -4,7 +4,7 @@
const express = require('express');
const router = express.Router();
const MonthlyStatusController = require('../controllers/monthlyStatusController');
const { verifyToken } = require('../middlewares/authMiddleware');
const { verifyToken } = require('../middlewares/auth');
// 모든 라우트에 인증 미들웨어 적용 (임시로 주석 처리 - 테스트용)
// router.use(verifyToken);

View File

@@ -2,7 +2,7 @@
const express = require('express');
const router = express.Router();
const notificationRecipientController = require('../controllers/notificationRecipientController');
const { verifyToken, requireMinLevel } = require('../middlewares/authMiddleware');
const { verifyToken, requireMinLevel } = require('../middlewares/auth');
// 모든 라우트에 인증 필요
router.use(verifyToken);

View File

@@ -2,7 +2,7 @@
const express = require('express');
const router = express.Router();
const TbmController = require('../controllers/tbmController');
const { requireAuth } = require('../middlewares/auth');
const { requireAuth, requireRole } = require('../middlewares/auth');
// ==================== TBM 세션 관련 ====================
@@ -56,13 +56,13 @@ router.delete('/sessions/:sessionId/team/:workerId', requireAuth, TbmController.
router.get('/safety-checks', requireAuth, TbmController.getAllSafetyChecks);
// 안전 체크 항목 생성 (관리자용)
router.post('/safety-checks', requireAuth, TbmController.createSafetyCheck);
router.post('/safety-checks', requireAuth, requireRole('admin', 'system'), TbmController.createSafetyCheck);
// 안전 체크 항목 수정 (관리자용)
router.put('/safety-checks/:checkId', requireAuth, TbmController.updateSafetyCheck);
router.put('/safety-checks/:checkId', requireAuth, requireRole('admin', 'system'), TbmController.updateSafetyCheck);
// 안전 체크 항목 삭제 (관리자용)
router.delete('/safety-checks/:checkId', requireAuth, TbmController.deleteSafetyCheck);
router.delete('/safety-checks/:checkId', requireAuth, requireRole('admin', 'system'), TbmController.deleteSafetyCheck);
// TBM 세션의 안전 체크 기록 조회
router.get('/sessions/:sessionId/safety', requireAuth, TbmController.getSafetyRecords);

View File

@@ -10,7 +10,7 @@
const express = require('express');
const router = express.Router();
const userController = require('../controllers/userController');
const { verifyToken } = require('../middlewares/authMiddleware');
const { verifyToken } = require('../middlewares/auth');
const logger = require('../utils/logger');
/**

View File

@@ -1,7 +1,7 @@
const express = require('express');
const router = express.Router();
const visitRequestController = require('../controllers/visitRequestController');
const { verifyToken } = require('../middlewares/authMiddleware');
const { verifyToken } = require('../middlewares/auth');
// 모든 라우트에 인증 미들웨어 적용
router.use(verifyToken);

View File

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

View File

@@ -36,7 +36,7 @@
if (!currentUser || !currentUser.user_id) return null;
// 캐시 확인
const cached = localStorage.getItem('userPageAccess_v2_v2');
const cached = localStorage.getItem('userPageAccess_v2');
if (cached) {
try {
const cacheData = JSON.parse(cached);
@@ -388,12 +388,8 @@
return date.toLocaleDateString('ko-KR', { month: 'short', day: 'numeric' });
}
function escapeHtml(text) {
if (!text) return '';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// escapeHtml은 api-base.js에서 window.escapeHtml로 전역 제공
var escapeHtml = window.escapeHtml;
// ===== 날짜/시간 업데이트 =====
function updateDateTime() {

View File

@@ -1,259 +0,0 @@
/**
* Security Utilities - 보안 관련 유틸리티 함수
*
* XSS 방지, 입력값 검증, 안전한 DOM 조작을 위한 함수 모음
*
* @author TK-FB-Project
* @since 2026-02-04
*/
(function(global) {
'use strict';
const SecurityUtils = {
/**
* HTML 특수문자 이스케이프 (XSS 방지)
* innerHTML에 사용자 입력을 삽입할 때 반드시 사용
*
* @param {string} str - 이스케이프할 문자열
* @returns {string} 이스케이프된 문자열
*
* @example
* element.innerHTML = `<span>${SecurityUtils.escapeHtml(userInput)}</span>`;
*/
escapeHtml: function(str) {
if (str === null || str === undefined) return '';
if (typeof str !== 'string') str = String(str);
const htmlEntities = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;',
'/': '&#x2F;',
'`': '&#x60;',
'=': '&#x3D;'
};
return str.replace(/[&<>"'`=\/]/g, function(char) {
return htmlEntities[char];
});
},
/**
* URL 파라미터 이스케이프
* URL에 사용자 입력을 포함할 때 사용
*
* @param {string} str - 이스케이프할 문자열
* @returns {string} URL 인코딩된 문자열
*/
escapeUrl: function(str) {
if (str === null || str === undefined) return '';
return encodeURIComponent(String(str));
},
/**
* JavaScript 문자열 이스케이프
* 동적 JavaScript 생성 시 사용 (권장하지 않음)
*
* @param {string} str - 이스케이프할 문자열
* @returns {string} 이스케이프된 문자열
*/
escapeJs: function(str) {
if (str === null || str === undefined) return '';
return String(str)
.replace(/\\/g, '\\\\')
.replace(/'/g, "\\'")
.replace(/"/g, '\\"')
.replace(/\n/g, '\\n')
.replace(/\r/g, '\\r')
.replace(/\t/g, '\\t');
},
/**
* 안전한 텍스트 설정
* innerHTML 대신 textContent 사용 권장
*
* @param {Element} element - DOM 요소
* @param {string} text - 설정할 텍스트
*/
setTextSafe: function(element, text) {
if (element && element.nodeType === 1) {
element.textContent = text;
}
},
/**
* 안전한 HTML 삽입
* 사용자 입력이 포함된 HTML을 삽입할 때 사용
*
* @param {Element} element - DOM 요소
* @param {string} template - HTML 템플릿 ({{변수}} 형식)
* @param {Object} data - 삽입할 데이터 (자동 이스케이프됨)
*
* @example
* SecurityUtils.setHtmlSafe(div, '<span>{{name}}</span>', { name: userInput });
*/
setHtmlSafe: function(element, template, data) {
if (!element || element.nodeType !== 1) return;
const self = this;
const safeHtml = template.replace(/\{\{(\w+)\}\}/g, function(match, key) {
return data.hasOwnProperty(key) ? self.escapeHtml(data[key]) : '';
});
element.innerHTML = safeHtml;
},
/**
* 입력값 검증 - 숫자
*
* @param {any} value - 검증할 값
* @param {Object} options - 옵션 { min, max, allowFloat }
* @returns {number|null} 유효한 숫자 또는 null
*/
validateNumber: function(value, options) {
options = options || {};
const num = options.allowFloat ? parseFloat(value) : parseInt(value, 10);
if (isNaN(num)) return null;
if (options.min !== undefined && num < options.min) return null;
if (options.max !== undefined && num > options.max) return null;
return num;
},
/**
* 입력값 검증 - 이메일
*
* @param {string} email - 검증할 이메일
* @returns {boolean} 유효 여부
*/
validateEmail: function(email) {
if (!email || typeof email !== 'string') return false;
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
},
/**
* 입력값 검증 - 길이
*
* @param {string} str - 검증할 문자열
* @param {Object} options - 옵션 { min, max }
* @returns {boolean} 유효 여부
*/
validateLength: function(str, options) {
options = options || {};
if (!str || typeof str !== 'string') return false;
const len = str.length;
if (options.min !== undefined && len < options.min) return false;
if (options.max !== undefined && len > options.max) return false;
return true;
},
/**
* 안전한 JSON 파싱
*
* @param {string} jsonString - 파싱할 JSON 문자열
* @param {any} defaultValue - 파싱 실패 시 기본값
* @returns {any} 파싱된 객체 또는 기본값
*/
parseJsonSafe: function(jsonString, defaultValue) {
defaultValue = defaultValue === undefined ? null : defaultValue;
try {
return JSON.parse(jsonString);
} catch (e) {
console.warn('[SecurityUtils] JSON 파싱 실패:', e.message);
return defaultValue;
}
},
/**
* localStorage에서 안전하게 데이터 가져오기
*
* @param {string} key - 키
* @param {any} defaultValue - 기본값
* @returns {any} 저장된 값 또는 기본값
*/
getStorageSafe: function(key, defaultValue) {
try {
const item = localStorage.getItem(key);
if (item === null) return defaultValue;
return this.parseJsonSafe(item, defaultValue);
} catch (e) {
console.warn('[SecurityUtils] localStorage 접근 실패:', e.message);
return defaultValue;
}
},
/**
* URL 파라미터 안전하게 가져오기
*
* @param {string} name - 파라미터 이름
* @param {string} defaultValue - 기본값
* @returns {string} 파라미터 값 (이스케이프됨)
*/
getUrlParamSafe: function(name, defaultValue) {
defaultValue = defaultValue === undefined ? '' : defaultValue;
try {
const urlParams = new URLSearchParams(window.location.search);
const value = urlParams.get(name);
return value !== null ? value : defaultValue;
} catch (e) {
return defaultValue;
}
},
/**
* ID 파라미터 안전하게 가져오기 (숫자 검증)
*
* @param {string} name - 파라미터 이름
* @returns {number|null} 유효한 ID 또는 null
*/
getIdParamSafe: function(name) {
const value = this.getUrlParamSafe(name);
return this.validateNumber(value, { min: 1 });
},
/**
* Content Security Policy 위반 리포터
*
* @param {string} reportUri - 리포트 전송 URL
*/
enableCspReporting: function(reportUri) {
document.addEventListener('securitypolicyviolation', function(e) {
console.error('[CSP Violation]', {
blockedUri: e.blockedURI,
violatedDirective: e.violatedDirective,
originalPolicy: e.originalPolicy
});
if (reportUri) {
fetch(reportUri, {
method: 'POST',
body: JSON.stringify({
blocked_uri: e.blockedURI,
violated_directive: e.violatedDirective,
document_uri: e.documentURI,
timestamp: new Date().toISOString()
}),
headers: { 'Content-Type': 'application/json' }
}).catch(function() {});
}
});
}
};
// 전역 노출
global.SecurityUtils = SecurityUtils;
// 편의를 위한 단축 함수
global.escapeHtml = SecurityUtils.escapeHtml.bind(SecurityUtils);
global.escapeUrl = SecurityUtils.escapeUrl.bind(SecurityUtils);
console.log('[Module] common/security.js 로드 완료');
})(typeof window !== 'undefined' ? window : this);

View File

@@ -12,16 +12,7 @@ let workplaceItems = []; // 현재 작업장 물품
let isItemEditMode = false;
let workplaceDetail = null; // 작업장 상세 정보
// XSS 방지를 위한 HTML 이스케이프 함수
function escapeHtml(str) {
if (str === null || str === undefined) return '';
return String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}
// escapeHtml은 api-base.js에서 window.escapeHtml로 전역 제공
// 이미지 URL 헬퍼 함수 (정적 파일용 - /api 경로 제외)
function getImageUrl(path) {

View File

@@ -441,15 +441,7 @@ function formatTimeAgo(dateString) {
return date.toLocaleDateString('ko-KR', { month: 'short', day: 'numeric' });
}
/**
* HTML 이스케이프
*/
function escapeHtml(text) {
if (!text) return '';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// escapeHtml은 api-base.js에서 window.escapeHtml로 전역 제공
// 메인 로직: DOMContentLoaded 시 실행
document.addEventListener('DOMContentLoaded', async () => {

View File

@@ -19,12 +19,7 @@
// ==================== 유틸리티 ====================
function escapeHtml(text) {
if (!text) return '';
var div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// escapeHtml은 api-base.js에서 window.escapeHtml로 전역 제공
function waitForApi(timeout) {
timeout = timeout || 5000;

View File

@@ -9,16 +9,7 @@ let isAddingItem = false;
let selectionStart = null;
let selectionBox = null;
// XSS 방지를 위한 HTML 이스케이프 함수
function escapeHtml(str) {
if (str === null || str === undefined) return '';
return String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}
// escapeHtml은 api-base.js에서 window.escapeHtml로 전역 제공
// axios 설정 대기
function waitForAxiosConfig() {

View File

@@ -818,6 +818,6 @@
<script src="/js/tbm/state.js"></script>
<script src="/js/tbm/utils.js"></script>
<script src="/js/tbm/api.js"></script>
<script src="/js/tbm-create.js?v=12"></script>
<script src="/js/tbm-create.js?v=13"></script>
</body>
</html>