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:
@@ -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보다 먼저 실행)
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
};
|
||||
@@ -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
|
||||
};
|
||||
@@ -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
|
||||
};
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
@@ -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');
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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');
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
@@ -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() {
|
||||
|
||||
@@ -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 = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": ''',
|
||||
'/': '/',
|
||||
'`': '`',
|
||||
'=': '='
|
||||
};
|
||||
|
||||
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);
|
||||
@@ -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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
// escapeHtml은 api-base.js에서 window.escapeHtml로 전역 제공
|
||||
|
||||
// 이미지 URL 헬퍼 함수 (정적 파일용 - /api 경로 제외)
|
||||
function getImageUrl(path) {
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
// escapeHtml은 api-base.js에서 window.escapeHtml로 전역 제공
|
||||
|
||||
// axios 설정 대기
|
||||
function waitForAxiosConfig() {
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user