feat: Sprint 002 리뷰 수정 + Sprint 003 대시보드 API/UI 구현
Sprint 002: - proxyInput created_by_name 누락 수정 - tbm-mobile 하단 네비에 현황 탭 추가 - proxy-input 저장 버튼 스피너 추가 Sprint 003: - GET /api/dashboard/my-summary API (연차/연장근로/페이지 통합) - 생산팀 대시보드 UI (프로필카드 + 아이콘 그리드) - dashboard-new.html 교체 (기존 .bak 백업) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
89
system1-factory/api/controllers/dashboardController.js
Normal file
89
system1-factory/api/controllers/dashboardController.js
Normal file
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* 대시보드 컨트롤러
|
||||
* Sprint 003 — 개인 요약 API
|
||||
*/
|
||||
const DashboardModel = require('../models/dashboardModel');
|
||||
const logger = require('../../shared/utils/logger');
|
||||
|
||||
const DashboardController = {
|
||||
/**
|
||||
* GET /api/dashboard/my-summary
|
||||
* 연차 잔여 + 월간 연장근로 + 접근 가능 페이지
|
||||
*/
|
||||
getMySummary: async (req, res) => {
|
||||
try {
|
||||
const userId = req.user.user_id || req.user.id;
|
||||
const now = new Date();
|
||||
const year = now.getFullYear();
|
||||
const month = now.getMonth() + 1;
|
||||
|
||||
// 1단계: 사용자 정보 먼저 조회 (worker_id 필요)
|
||||
const userInfo = await DashboardModel.getUserInfo(userId);
|
||||
if (!userInfo) {
|
||||
return res.status(404).json({ success: false, message: '사용자 정보를 찾을 수 없습니다.' });
|
||||
}
|
||||
|
||||
const workerId = userInfo.worker_id;
|
||||
const departmentId = userInfo.department_id;
|
||||
const role = userInfo.role;
|
||||
|
||||
// 2단계: 나머지 3개 병렬 조회
|
||||
const [vacationRows, overtime, quickAccess] = await Promise.all([
|
||||
DashboardModel.getVacationBalance(workerId, year),
|
||||
DashboardModel.getMonthlyOvertime(userId, year, month),
|
||||
DashboardModel.getQuickAccess(userId, departmentId, role)
|
||||
]);
|
||||
|
||||
// 연차 응답 가공
|
||||
const details = vacationRows.map(v => ({
|
||||
type_name: v.type_name,
|
||||
type_code: v.type_code,
|
||||
total: parseFloat(v.total_days) || 0,
|
||||
used: parseFloat(v.used_days) || 0,
|
||||
remaining: parseFloat(v.remaining_days) || 0
|
||||
}));
|
||||
|
||||
const annualRow = vacationRows.find(v => v.type_code === 'ANNUAL');
|
||||
const totalDays = annualRow ? parseFloat(annualRow.total_days) || 0 : 0;
|
||||
const usedDays = annualRow ? parseFloat(annualRow.used_days) || 0 : 0;
|
||||
const remainingDays = annualRow ? parseFloat(annualRow.remaining_days) || 0 : 0;
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
user: {
|
||||
user_id: userInfo.user_id,
|
||||
name: userInfo.name,
|
||||
worker_name: userInfo.worker_name || userInfo.name,
|
||||
job_type: userInfo.job_type || '',
|
||||
department_name: userInfo.department_name,
|
||||
department_id: userInfo.department_id,
|
||||
role: userInfo.role
|
||||
},
|
||||
vacation: {
|
||||
year,
|
||||
total_days: totalDays,
|
||||
used_days: usedDays,
|
||||
remaining_days: remainingDays,
|
||||
details
|
||||
},
|
||||
overtime: {
|
||||
year,
|
||||
month,
|
||||
total_overtime_hours: parseFloat(overtime.total_overtime_hours) || 0,
|
||||
overtime_days: parseInt(overtime.overtime_days) || 0,
|
||||
total_work_days: parseInt(overtime.total_work_days) || 0,
|
||||
total_work_hours: parseFloat(overtime.total_work_hours) || 0,
|
||||
avg_daily_hours: parseFloat(parseFloat(overtime.avg_daily_hours || 0).toFixed(1))
|
||||
},
|
||||
quick_access: quickAccess
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
logger.error('대시보드 요약 조회 오류:', err);
|
||||
res.status(500).json({ success: false, message: '대시보드 데이터 조회 중 오류가 발생했습니다.', error: err.message });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = DashboardController;
|
||||
@@ -116,7 +116,8 @@ const ProxyInputController = {
|
||||
note: entry.note || '',
|
||||
tbm_session_id: sessionId,
|
||||
tbm_assignment_id: assignmentId,
|
||||
created_by: userId
|
||||
created_by: userId,
|
||||
created_by_name: req.user.name || req.user.username || ''
|
||||
});
|
||||
|
||||
createdWorkers.push({
|
||||
|
||||
150
system1-factory/api/models/dashboardModel.js
Normal file
150
system1-factory/api/models/dashboardModel.js
Normal file
@@ -0,0 +1,150 @@
|
||||
/**
|
||||
* 대시보드 개인 요약 모델
|
||||
* Sprint 003 — 연차/연장근로/접근 페이지 통합 조회
|
||||
*/
|
||||
const { getDb } = require('../config/database');
|
||||
|
||||
const OVERTIME_THRESHOLD = 8; // 연장근로 기준 시간
|
||||
|
||||
const DashboardModel = {
|
||||
/**
|
||||
* 사용자 정보 조회 (쿼리 1 — 먼저 실행)
|
||||
*/
|
||||
getUserInfo: async (userId) => {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.execute(`
|
||||
SELECT u.user_id, u.name, u.role,
|
||||
w.worker_id, w.worker_name, w.job_type, w.department_id,
|
||||
COALESCE(d.department_name, '미배정') AS department_name
|
||||
FROM sso_users u
|
||||
LEFT JOIN workers w ON u.user_id = w.user_id
|
||||
LEFT JOIN departments d ON w.department_id = d.department_id
|
||||
WHERE u.user_id = ?
|
||||
`, [userId]);
|
||||
return rows[0] || null;
|
||||
},
|
||||
|
||||
/**
|
||||
* 연차 현황 조회 (쿼리 2)
|
||||
*/
|
||||
getVacationBalance: async (workerId, year) => {
|
||||
if (!workerId) return [];
|
||||
const db = await getDb();
|
||||
const [rows] = await db.execute(`
|
||||
SELECT vbd.vacation_type_id, vbd.total_days, vbd.used_days, vbd.remaining_days,
|
||||
vt.type_name, vt.type_code
|
||||
FROM vacation_balance_details vbd
|
||||
JOIN vacation_types vt ON vbd.vacation_type_id = vt.id
|
||||
WHERE vbd.worker_id = ? AND vbd.year = ?
|
||||
ORDER BY vt.priority
|
||||
`, [workerId, year]);
|
||||
return rows;
|
||||
},
|
||||
|
||||
/**
|
||||
* 월간 연장근로 조회 (쿼리 3)
|
||||
*/
|
||||
getMonthlyOvertime: async (userId, year, month) => {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.execute(`
|
||||
SELECT
|
||||
COUNT(CASE WHEN dar.total_work_hours > ${OVERTIME_THRESHOLD} THEN 1 END) AS overtime_days,
|
||||
COALESCE(SUM(CASE WHEN dar.total_work_hours > ${OVERTIME_THRESHOLD} THEN dar.total_work_hours - ${OVERTIME_THRESHOLD} ELSE 0 END), 0) AS total_overtime_hours,
|
||||
COUNT(*) AS total_work_days,
|
||||
COALESCE(SUM(dar.total_work_hours), 0) AS total_work_hours,
|
||||
COALESCE(AVG(dar.total_work_hours), 0) AS avg_daily_hours
|
||||
FROM daily_attendance_records dar
|
||||
WHERE dar.user_id = ? AND YEAR(dar.record_date) = ? AND MONTH(dar.record_date) = ?
|
||||
AND dar.total_work_hours > 0
|
||||
`, [userId, year, month]);
|
||||
return rows[0] || { overtime_days: 0, total_overtime_hours: 0, total_work_days: 0, total_work_hours: 0, avg_daily_hours: 0 };
|
||||
},
|
||||
|
||||
/**
|
||||
* 접근 가능 페이지 조회 (쿼리 4)
|
||||
*/
|
||||
getQuickAccess: async (userId, departmentId, role) => {
|
||||
const db = await getDb();
|
||||
const isAdmin = ['admin', 'system'].includes((role || '').toLowerCase());
|
||||
|
||||
// 모든 페이지 조회
|
||||
const [allPages] = await db.execute(`
|
||||
SELECT id, page_key, page_name, page_path, category, is_admin_only,
|
||||
COALESCE(icon, '') AS icon
|
||||
FROM pages
|
||||
ORDER BY display_order, page_name
|
||||
`);
|
||||
|
||||
if (isAdmin) {
|
||||
const adminPages = allPages.filter(p => p.is_admin_only);
|
||||
const normalPages = allPages.filter(p => !p.is_admin_only);
|
||||
return {
|
||||
department_pages: normalPages.map(formatPage),
|
||||
personal_pages: [],
|
||||
admin_pages: adminPages.map(formatPage)
|
||||
};
|
||||
}
|
||||
|
||||
// 부서 권한 페이지
|
||||
let deptPageIds = new Set();
|
||||
if (departmentId) {
|
||||
const [deptRows] = await db.execute(`
|
||||
SELECT dpp.page_id
|
||||
FROM department_page_permissions dpp
|
||||
WHERE dpp.department_id = ? AND dpp.can_access = 1
|
||||
`, [departmentId]);
|
||||
deptRows.forEach(r => deptPageIds.add(r.page_id));
|
||||
}
|
||||
|
||||
// 개인 권한 페이지
|
||||
const [personalRows] = await db.execute(`
|
||||
SELECT upa.page_id
|
||||
FROM user_page_access upa
|
||||
WHERE upa.user_id = ? AND upa.can_access = 1
|
||||
`, [userId]);
|
||||
const personalPageIds = new Set(personalRows.map(r => r.page_id));
|
||||
|
||||
// 기본 접근 페이지
|
||||
const defaultPages = allPages.filter(p => !p.is_admin_only);
|
||||
|
||||
// 분류
|
||||
const departmentPages = [];
|
||||
const personalPages = [];
|
||||
|
||||
for (const page of allPages) {
|
||||
if (page.is_admin_only) continue;
|
||||
|
||||
if (deptPageIds.has(page.id)) {
|
||||
departmentPages.push(formatPage(page));
|
||||
} else if (personalPageIds.has(page.id)) {
|
||||
personalPages.push(formatPage(page));
|
||||
}
|
||||
}
|
||||
|
||||
// 기본 접근 페이지 중 부서/개인에 없는 것 추가
|
||||
const addedIds = new Set([...departmentPages.map(p => p.page_key), ...personalPages.map(p => p.page_key)]);
|
||||
for (const page of defaultPages) {
|
||||
if (!addedIds.has(page.page_key) && !page.is_admin_only) {
|
||||
departmentPages.push(formatPage(page));
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
department_pages: departmentPages,
|
||||
personal_pages: personalPages,
|
||||
admin_pages: []
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
function formatPage(page) {
|
||||
return {
|
||||
page_key: page.page_key,
|
||||
page_name: page.page_name,
|
||||
page_path: page.page_path,
|
||||
icon: page.icon || '',
|
||||
category: page.category || ''
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = DashboardModel;
|
||||
@@ -59,9 +59,9 @@ const ProxyInputModel = {
|
||||
*/
|
||||
createWorkReport: async (conn, data) => {
|
||||
const [result] = await conn.query(`
|
||||
INSERT INTO daily_work_reports (report_date, user_id, project_id, work_type_id, task_id, work_status_id, work_hours, start_time, end_time, note, tbm_session_id, tbm_assignment_id, created_by, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW())
|
||||
`, [data.report_date, data.user_id, data.project_id, data.work_type_id, data.task_id || null, data.work_status_id || 1, data.work_hours, data.start_time || null, data.end_time || null, data.note || '', data.tbm_session_id, data.tbm_assignment_id, data.created_by]);
|
||||
INSERT INTO daily_work_reports (report_date, user_id, project_id, work_type_id, task_id, work_status_id, work_hours, start_time, end_time, note, tbm_session_id, tbm_assignment_id, created_by, created_by_name, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW())
|
||||
`, [data.report_date, data.user_id, data.project_id, data.work_type_id, data.task_id || null, data.work_status_id || 1, data.work_hours, data.start_time || null, data.end_time || null, data.note || '', data.tbm_session_id, data.tbm_assignment_id, data.created_by, data.created_by_name || '']);
|
||||
return result;
|
||||
},
|
||||
|
||||
|
||||
@@ -154,6 +154,7 @@ function setupRoutes(app) {
|
||||
app.use('/api/vacation-balances', vacationBalanceRoutes); // 휴가 잔액 관리
|
||||
app.use('/api/tbm', tbmRoutes); // TBM 시스템
|
||||
app.use('/api/proxy-input', require('./routes/proxyInputRoutes')); // 대리입력 + 일별현황
|
||||
app.use('/api/dashboard', require('./routes/dashboardRoutes')); // 대시보드 개인 요약
|
||||
app.use('/api/work-issues', workIssueRoutes); // 카테고리/아이템 + 신고 조회 (같은 MariaDB 공유)
|
||||
app.use('/api/departments', departmentRoutes); // 부서 관리
|
||||
app.use('/api/patrol', patrolRoutes); // 일일순회점검 시스템
|
||||
|
||||
13
system1-factory/api/routes/dashboardRoutes.js
Normal file
13
system1-factory/api/routes/dashboardRoutes.js
Normal file
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* 대시보드 라우터
|
||||
* Sprint 003 — 개인 요약 API
|
||||
*/
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const dashboardController = require('../controllers/dashboardController');
|
||||
const { verifyToken } = require('../middlewares/auth');
|
||||
|
||||
// 모든 인증된 사용자 접근 가능
|
||||
router.get('/my-summary', verifyToken, dashboardController.getMySummary);
|
||||
|
||||
module.exports = router;
|
||||
Reference in New Issue
Block a user