feat: 작업자-계정 통합 및 연차/출근 관리 시스템 구축
모든 작업자가 개인 계정으로 로그인하여 본인의 연차와 출근 기록을 확인할 수 있는 시스템을 구축했습니다. 주요 기능: - 작업자-계정 1:1 통합 (기존 작업자 자동 계정 생성) - 연차 관리 시스템 (연도별 잔액 관리) - 출근 기록 시스템 (일일 근태 기록) - 나의 대시보드 페이지 (개인 정보 조회) 데이터베이스: - workers 테이블에 salary, base_annual_leave 컬럼 추가 - work_attendance_types, vacation_types 테이블 생성 - daily_attendance_records 테이블 생성 - worker_vacation_balance 테이블 생성 - 기존 작업자 자동 계정 생성 (username: 이름 기반) - Guest 역할 추가 백엔드 API: - 한글→영문 변환 유틸리티 (hangulToRoman.js) - UserRoutes에 개인 정보 조회 API 추가 - GET /api/users/me (내 정보) - GET /api/users/me/attendance-records (출근 기록) - GET /api/users/me/vacation-balance (연차 잔액) - GET /api/users/me/work-reports (작업 보고서) - GET /api/users/me/monthly-stats (월별 통계) 프론트엔드: - 나의 대시보드 페이지 (my-dashboard.html) - 연차 정보 위젯 (총/사용/잔여) - 월별 출근 캘린더 - 근무 시간 통계 - 최근 작업 보고서 목록 - 네비게이션 바에 "나의 대시보드" 메뉴 추가 배포 시 주의사항: - 마이그레이션 실행 필요 - 자동 생성된 계정 초기 비밀번호: 1234 - 작업자들에게 첫 로그인 후 비밀번호 변경 안내 필요 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -598,37 +598,41 @@ router.get('/users', verifyToken, async (req, res) => {
|
||||
try {
|
||||
connection = await mysql.createConnection(dbConfig);
|
||||
|
||||
// 기본 쿼리
|
||||
// 기본 쿼리 (role 테이블과 JOIN)
|
||||
let query = `
|
||||
SELECT
|
||||
user_id,
|
||||
username,
|
||||
name,
|
||||
email,
|
||||
access_level,
|
||||
worker_id,
|
||||
is_active,
|
||||
last_login_at,
|
||||
created_at
|
||||
FROM Users
|
||||
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 is_active = ?';
|
||||
query += ' AND u.is_active = ?';
|
||||
params.push(req.query.active === 'true');
|
||||
}
|
||||
|
||||
|
||||
// role_name으로 필터링 (access_level 대신)
|
||||
if (req.query.access_level) {
|
||||
query += ' AND access_level = ?';
|
||||
params.push(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 created_at DESC';
|
||||
|
||||
|
||||
query += ' ORDER BY u.created_at DESC';
|
||||
|
||||
const [rows] = await connection.execute(query, params);
|
||||
|
||||
const userList = rows.map(user => ({
|
||||
@@ -636,7 +640,9 @@ router.get('/users', verifyToken, async (req, res) => {
|
||||
username: user.username,
|
||||
name: user.name || user.username,
|
||||
email: user.email,
|
||||
access_level: user.access_level,
|
||||
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,
|
||||
|
||||
237
api.hyungi.net/routes/pageAccessRoutes.js
Normal file
237
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;
|
||||
@@ -38,6 +38,71 @@ const adminOnly = (req, res, next) => {
|
||||
}
|
||||
};
|
||||
|
||||
// ========== 개인 정보 조회 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 db = require('../config/database');
|
||||
const stats = await db.query(
|
||||
`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 });
|
||||
}
|
||||
});
|
||||
|
||||
// ========== 관리자 전용 API ==========
|
||||
/**
|
||||
* 모든 라우트에 관리자 권한 적용
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user