From f711a721ecf99e56deaede75e7afa67177b203dc Mon Sep 17 00:00:00 2001 From: Hyungi Ahn Date: Mon, 16 Mar 2026 11:23:17 +0900 Subject: [PATCH] =?UTF-8?q?feat(tkuser):=20=ED=98=91=EB=A0=A5=EC=97=85?= =?UTF-8?q?=EC=B2=B4=20CRUD=20=EA=B6=8C=ED=95=9C=EC=9D=84=20permission=20?= =?UTF-8?q?=EC=8B=9C=EC=8A=A4=ED=85=9C=EC=9C=BC=EB=A1=9C=20=ED=99=95?= =?UTF-8?q?=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit tkuser.partners 권한이 부여된 일반 사용자도 업체/작업자 등록·수정·비활성화 가능. 완전삭제는 admin 전용 유지. Co-Authored-By: Claude Opus 4.6 --- user-management/api/middleware/auth.js | 23 ++++++++++++++++- user-management/api/routes/partnerRoutes.js | 15 +++++------ .../web/static/js/tkuser-partners.js | 25 +++++++++++++------ 3 files changed, 48 insertions(+), 15 deletions(-) diff --git a/user-management/api/middleware/auth.js b/user-management/api/middleware/auth.js index 21d5669..4b7b512 100644 --- a/user-management/api/middleware/auth.js +++ b/user-management/api/middleware/auth.js @@ -55,4 +55,25 @@ function requireAdmin(req, res, next) { } } -module.exports = { extractToken, requireAuth, requireAdmin }; +/** + * 관리자 또는 특정 페이지 권한 보유자 미들웨어 팩토리 + */ +function requireAdminOrPermission(pageName) { + return async (req, res, next) => { + const token = extractToken(req); + if (!token) return res.status(401).json({ success: false, error: '인증이 필요합니다' }); + try { + const decoded = jwt.verify(token, JWT_SECRET); + req.user = decoded; + if (['admin', 'system'].includes((decoded.role || '').toLowerCase())) return next(); + const { checkAccess } = require('../models/permissionModel'); + const result = await checkAccess(decoded.user_id || decoded.id, pageName); + if (result.can_access) return next(); + return res.status(403).json({ success: false, error: '권한이 없습니다' }); + } catch { + return res.status(401).json({ success: false, error: '유효하지 않은 토큰입니다' }); + } + }; +} + +module.exports = { extractToken, requireAuth, requireAdmin, requireAdminOrPermission }; diff --git a/user-management/api/routes/partnerRoutes.js b/user-management/api/routes/partnerRoutes.js index 95b7921..d86d04b 100644 --- a/user-management/api/routes/partnerRoutes.js +++ b/user-management/api/routes/partnerRoutes.js @@ -1,7 +1,8 @@ const express = require('express'); const router = express.Router(); -const { requireAuth, requireAdmin } = require('../middleware/auth'); +const { requireAuth, requireAdmin, requireAdminOrPermission } = require('../middleware/auth'); const ctrl = require('../controllers/partnerController'); +const partnerPerm = requireAdminOrPermission('tkuser.partners'); router.use(requireAuth); @@ -9,13 +10,13 @@ router.get('/', ctrl.list); router.get('/:id/delete-info', requireAdmin, ctrl.getDeleteInfo); router.delete('/:id/permanent', requireAdmin, ctrl.permanentDelete); router.get('/:id', ctrl.getById); -router.post('/', requireAdmin, ctrl.create); -router.put('/:id', requireAdmin, ctrl.update); -router.delete('/:id', requireAdmin, ctrl.deactivate); +router.post('/', partnerPerm, ctrl.create); +router.put('/:id', partnerPerm, ctrl.update); +router.delete('/:id', partnerPerm, ctrl.deactivate); router.get('/:id/workers', ctrl.listWorkers); -router.post('/:id/workers', requireAdmin, ctrl.createWorker); -router.put('/workers/:id', requireAdmin, ctrl.updateWorker); -router.delete('/workers/:id', requireAdmin, ctrl.deactivateWorker); +router.post('/:id/workers', partnerPerm, ctrl.createWorker); +router.put('/workers/:id', partnerPerm, ctrl.updateWorker); +router.delete('/workers/:id', partnerPerm, ctrl.deactivateWorker); module.exports = router; diff --git a/user-management/web/static/js/tkuser-partners.js b/user-management/web/static/js/tkuser-partners.js index 7e9dfa9..0c8f5a3 100644 --- a/user-management/web/static/js/tkuser-partners.js +++ b/user-management/web/static/js/tkuser-partners.js @@ -1,4 +1,13 @@ /* ===== tkuser 협력업체 CRUD ===== */ +function hasPartnerPermission() { + if (!currentUser) return false; + if (['admin', 'system'].includes(currentUser.role)) return true; + return typeof currentUserAllowedTabs !== 'undefined' && currentUserAllowedTabs.has('partners'); +} +function isAdminOnly() { + return currentUser && ['admin', 'system'].includes(currentUser.role); +} + let partnersLoaded = false; let partnersList = []; let partnerWorkersList = []; @@ -8,7 +17,7 @@ let editingWorkerIdTkuser = null; async function loadPartnersTab() { if (partnersLoaded) return; partnersLoaded = true; - if (currentUser && ['admin', 'system'].includes(currentUser.role)) { + if (hasPartnerPermission()) { document.getElementById('btnAddPartnerTkuser')?.classList.remove('hidden'); } await loadPartnersList(); @@ -35,7 +44,8 @@ function renderPartnersListTkuser() { c.innerHTML = '

등록된 협력업체가 없습니다.

'; return; } - const isAdmin = currentUser && ['admin', 'system'].includes(currentUser.role); + const canManage = hasPartnerPermission(); + const isAdmin = isAdminOnly(); c.innerHTML = partnersList.map(p => { const types = tryParseJsonTkuser(p.business_type) || []; const typeStr = types.map(t => `${escHtml(t)}`).join(' '); @@ -53,10 +63,10 @@ function renderPartnersListTkuser() { ${typeStr} - ${isAdmin ? `
+ ${canManage ? `
${p.is_active ? `` : ''} - + ${isAdmin ? `` : ''}
` : ''}
`; }).join(''); @@ -80,7 +90,8 @@ async function selectPartnerTkuser(id) { function renderPartnerDetailTkuser(p) { const types = tryParseJsonTkuser(p.business_type) || []; const workers = p.workers || []; - const isAdmin = currentUser && ['admin', 'system'].includes(currentUser.role); + const canManage = hasPartnerPermission(); + const isAdmin = isAdminOnly(); document.getElementById('partnerDetailTkuser').innerHTML = `
@@ -101,7 +112,7 @@ function renderPartnerDetailTkuser(p) {

소속 작업자 (${workers.length}명)

- ${isAdmin ? `` : ''} + ${canManage ? `` : ''}
${workers.length ? workers.map(w => `
@@ -116,7 +127,7 @@ function renderPartnerDetailTkuser(p) { ${w.phone ? `${escHtml(w.phone)}` : ''} ${w.safety_training_date ? `안전교육: ${formatDate(w.safety_training_date)}` : ''}
- ${isAdmin ? `
+ ${canManage ? `
${w.is_active ? `` : ''}
` : ''}