feat: TBM 시스템 구축 및 페이지 권한 관리 기능 추가
## 주요 변경사항 ### 1. TBM (Tool Box Meeting) 시스템 구축 - **데이터베이스 스키마** (5개 테이블 생성) - tbm_sessions: TBM 세션 관리 - tbm_team_assignments: 팀 구성 관리 - tbm_safety_checks: 안전 체크리스트 마스터 (17개 항목) - tbm_safety_records: 안전 체크 기록 - team_handovers: 작업 인계 관리 - **API 엔드포인트** (17개) - TBM 세션 CRUD - 팀 구성 관리 - 안전 체크리스트 - 작업 인계 - 통계 및 리포트 - **프론트엔드** - TBM 관리 페이지 (/pages/work/tbm.html) - 모달 기반 UI (세션 생성, 팀 구성, 안전 체크) ### 2. 페이지 권한 관리 시스템 - 페이지별 접근 권한 설정 기능 - 관리자 페이지 (/pages/admin/page-access.html) - 사용자별 페이지 권한 부여/회수 - TBM 페이지 등록 및 권한 연동 ### 3. 네비게이션 role 표시 버그 수정 - load-navbar.js: case-insensitive role 매칭 적용 - JWT의 "Admin" role이 "관리자"로 정상 표시 - admin-only 메뉴 항목 정상 표시 ### 4. 대시보드 개선 - 작업 현황 테이블 가독성 향상 - 고대비 색상 및 명확한 구분선 적용 - 이모지 제거 및 SVG 아이콘 적용 ### 5. 문서화 - TBM 배포 가이드 작성 (docs/TBM_DEPLOYMENT_GUIDE.md) - 데이터베이스 스키마 상세 기록 - 배포 절차 및 체크리스트 제공 ## 기술 스택 - Backend: Node.js, Express, MySQL - Frontend: Vanilla JavaScript, HTML5, CSS3 - Database: MySQL (InnoDB) ## 파일 변경사항 ### 신규 파일 - api.hyungi.net/db/migrations/20260120000000_create_tbm_system.js - api.hyungi.net/db/migrations/20260120000001_add_tbm_page.js - api.hyungi.net/models/tbmModel.js - api.hyungi.net/models/pageAccessModel.js - api.hyungi.net/controllers/tbmController.js - api.hyungi.net/controllers/pageAccessController.js - api.hyungi.net/routes/tbmRoutes.js - web-ui/pages/work/tbm.html - web-ui/pages/admin/page-access.html - web-ui/js/page-access-management.js - docs/TBM_DEPLOYMENT_GUIDE.md ### 수정 파일 - api.hyungi.net/config/routes.js (TBM 라우트 추가) - web-ui/js/load-navbar.js (role 매칭 버그 수정) - web-ui/pages/admin/workers.html (HTML 구조 수정) - web-ui/pages/dashboard.html (이모지 제거) - web-ui/css/design-system.css (색상 팔레트 추가) - web-ui/css/modern-dashboard.css (가독성 개선) - web-ui/js/modern-dashboard.js (SVG 아이콘 적용) ## 배포 시 주의사항 ⚠️ 본 서버 배포 시 반드시 마이그레이션 실행 필요: ```bash npm run db:migrate ``` 상세한 배포 절차는 docs/TBM_DEPLOYMENT_GUIDE.md 참조 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -40,6 +40,7 @@ function setupRoutes(app) {
|
||||
const attendanceRoutes = require('../routes/attendanceRoutes');
|
||||
const monthlyStatusRoutes = require('../routes/monthlyStatusRoutes');
|
||||
const pageAccessRoutes = require('../routes/pageAccessRoutes');
|
||||
const tbmRoutes = require('../routes/tbmRoutes');
|
||||
|
||||
// Rate Limiters 설정
|
||||
const rateLimit = require('express-rate-limit');
|
||||
@@ -127,6 +128,7 @@ function setupRoutes(app) {
|
||||
app.use('/api/tools', toolsRoute);
|
||||
app.use('/api/users', userRoutes);
|
||||
app.use('/api', pageAccessRoutes); // 페이지 접근 권한 관리
|
||||
app.use('/api/tbm', tbmRoutes); // TBM 시스템
|
||||
app.use('/api', uploadBgRoutes);
|
||||
|
||||
// Swagger API 문서
|
||||
|
||||
200
api.hyungi.net/controllers/pageAccessController.js
Normal file
200
api.hyungi.net/controllers/pageAccessController.js
Normal file
@@ -0,0 +1,200 @@
|
||||
// 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;
|
||||
575
api.hyungi.net/controllers/tbmController.js
Normal file
575
api.hyungi.net/controllers/tbmController.js
Normal file
@@ -0,0 +1,575 @@
|
||||
// controllers/tbmController.js - TBM 시스템 컨트롤러
|
||||
const TbmModel = require('../models/tbmModel');
|
||||
|
||||
const TbmController = {
|
||||
// ==================== TBM 세션 관련 ====================
|
||||
|
||||
/**
|
||||
* TBM 세션 생성
|
||||
*/
|
||||
createSession: (req, res) => {
|
||||
const sessionData = {
|
||||
session_date: req.body.session_date,
|
||||
leader_id: req.body.leader_id,
|
||||
project_id: req.body.project_id || null,
|
||||
work_location: req.body.work_location || null,
|
||||
work_description: req.body.work_description || null,
|
||||
safety_notes: req.body.safety_notes || null,
|
||||
start_time: req.body.start_time || null,
|
||||
created_by: req.user.user_id
|
||||
};
|
||||
|
||||
// 필수 필드 검증
|
||||
if (!sessionData.session_date || !sessionData.leader_id) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'TBM 날짜와 팀장 정보는 필수입니다.'
|
||||
});
|
||||
}
|
||||
|
||||
TbmModel.createSession(sessionData, (err, result) => {
|
||||
if (err) {
|
||||
console.error('TBM 세션 생성 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: 'TBM 세션 생성 중 오류가 발생했습니다.',
|
||||
error: err.message
|
||||
});
|
||||
}
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: 'TBM 세션이 생성되었습니다.',
|
||||
data: {
|
||||
session_id: result.insertId,
|
||||
...sessionData
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 특정 날짜의 TBM 세션 목록 조회
|
||||
*/
|
||||
getSessionsByDate: (req, res) => {
|
||||
const { date } = req.params;
|
||||
|
||||
if (!date) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '날짜 정보가 필요합니다.'
|
||||
});
|
||||
}
|
||||
|
||||
TbmModel.getSessionsByDate(date, (err, results) => {
|
||||
if (err) {
|
||||
console.error('TBM 세션 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: 'TBM 세션 조회 중 오류가 발생했습니다.',
|
||||
error: err.message
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: results
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* TBM 세션 상세 조회
|
||||
*/
|
||||
getSessionById: (req, res) => {
|
||||
const { sessionId } = req.params;
|
||||
|
||||
TbmModel.getSessionById(sessionId, (err, results) => {
|
||||
if (err) {
|
||||
console.error('TBM 세션 상세 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: 'TBM 세션 상세 조회 중 오류가 발생했습니다.',
|
||||
error: err.message
|
||||
});
|
||||
}
|
||||
|
||||
if (results.length === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'TBM 세션을 찾을 수 없습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: results[0]
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* TBM 세션 수정
|
||||
*/
|
||||
updateSession: (req, res) => {
|
||||
const { sessionId } = req.params;
|
||||
const sessionData = {
|
||||
project_id: req.body.project_id,
|
||||
work_location: req.body.work_location,
|
||||
work_description: req.body.work_description,
|
||||
safety_notes: req.body.safety_notes,
|
||||
status: req.body.status || 'draft'
|
||||
};
|
||||
|
||||
TbmModel.updateSession(sessionId, sessionData, (err, result) => {
|
||||
if (err) {
|
||||
console.error('TBM 세션 수정 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: 'TBM 세션 수정 중 오류가 발생했습니다.',
|
||||
error: err.message
|
||||
});
|
||||
}
|
||||
|
||||
if (result.affectedRows === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'TBM 세션을 찾을 수 없습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'TBM 세션이 수정되었습니다.'
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* TBM 세션 완료 처리
|
||||
*/
|
||||
completeSession: (req, res) => {
|
||||
const { sessionId } = req.params;
|
||||
const endTime = req.body.end_time || new Date().toTimeString().slice(0, 8);
|
||||
|
||||
TbmModel.completeSession(sessionId, endTime, (err, result) => {
|
||||
if (err) {
|
||||
console.error('TBM 세션 완료 처리 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: 'TBM 세션 완료 처리 중 오류가 발생했습니다.',
|
||||
error: err.message
|
||||
});
|
||||
}
|
||||
|
||||
if (result.affectedRows === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'TBM 세션을 찾을 수 없습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'TBM 세션이 완료되었습니다.'
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
// ==================== 팀 구성 관련 ====================
|
||||
|
||||
/**
|
||||
* 팀원 추가
|
||||
*/
|
||||
addTeamMember: (req, res) => {
|
||||
const assignmentData = {
|
||||
session_id: req.params.sessionId,
|
||||
worker_id: req.body.worker_id,
|
||||
assigned_role: req.body.assigned_role || null,
|
||||
work_detail: req.body.work_detail || null,
|
||||
is_present: req.body.is_present,
|
||||
absence_reason: req.body.absence_reason || null
|
||||
};
|
||||
|
||||
if (!assignmentData.worker_id) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '작업자 ID가 필요합니다.'
|
||||
});
|
||||
}
|
||||
|
||||
TbmModel.addTeamMember(assignmentData, (err, result) => {
|
||||
if (err) {
|
||||
console.error('팀원 추가 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '팀원 추가 중 오류가 발생했습니다.',
|
||||
error: err.message
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '팀원이 추가되었습니다.'
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 팀 구성 일괄 추가
|
||||
*/
|
||||
addTeamMembers: (req, res) => {
|
||||
const { sessionId } = req.params;
|
||||
const { members } = req.body;
|
||||
|
||||
if (!Array.isArray(members) || members.length === 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '팀원 목록이 필요합니다.'
|
||||
});
|
||||
}
|
||||
|
||||
TbmModel.addTeamMembers(sessionId, members, (err, result) => {
|
||||
if (err) {
|
||||
console.error('팀 구성 일괄 추가 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '팀 구성 추가 중 오류가 발생했습니다.',
|
||||
error: err.message
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: `${members.length}명의 팀원이 추가되었습니다.`,
|
||||
data: { count: members.length }
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* TBM 세션의 팀 구성 조회
|
||||
*/
|
||||
getTeamMembers: (req, res) => {
|
||||
const { sessionId } = req.params;
|
||||
|
||||
TbmModel.getTeamMembers(sessionId, (err, results) => {
|
||||
if (err) {
|
||||
console.error('팀 구성 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '팀 구성 조회 중 오류가 발생했습니다.',
|
||||
error: err.message
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: results
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 팀원 제거
|
||||
*/
|
||||
removeTeamMember: (req, res) => {
|
||||
const { sessionId, workerId } = req.params;
|
||||
|
||||
TbmModel.removeTeamMember(sessionId, workerId, (err, result) => {
|
||||
if (err) {
|
||||
console.error('팀원 제거 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '팀원 제거 중 오류가 발생했습니다.',
|
||||
error: err.message
|
||||
});
|
||||
}
|
||||
|
||||
if (result.affectedRows === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '팀원을 찾을 수 없습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '팀원이 제거되었습니다.'
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
// ==================== 안전 체크리스트 관련 ====================
|
||||
|
||||
/**
|
||||
* 모든 안전 체크 항목 조회
|
||||
*/
|
||||
getAllSafetyChecks: (req, res) => {
|
||||
TbmModel.getAllSafetyChecks((err, results) => {
|
||||
if (err) {
|
||||
console.error('안전 체크 항목 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '안전 체크 항목 조회 중 오류가 발생했습니다.',
|
||||
error: err.message
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: results
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* TBM 세션의 안전 체크 기록 조회
|
||||
*/
|
||||
getSafetyRecords: (req, res) => {
|
||||
const { sessionId } = req.params;
|
||||
|
||||
TbmModel.getSafetyRecords(sessionId, (err, results) => {
|
||||
if (err) {
|
||||
console.error('안전 체크 기록 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '안전 체크 기록 조회 중 오류가 발생했습니다.',
|
||||
error: err.message
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: results
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 안전 체크 일괄 저장
|
||||
*/
|
||||
saveSafetyRecords: (req, res) => {
|
||||
const { sessionId } = req.params;
|
||||
const { records } = req.body;
|
||||
|
||||
if (!Array.isArray(records) || records.length === 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '안전 체크 기록이 필요합니다.'
|
||||
});
|
||||
}
|
||||
|
||||
const checkedBy = req.user.user_id;
|
||||
|
||||
TbmModel.saveSafetyRecords(sessionId, records, checkedBy, (err, result) => {
|
||||
if (err) {
|
||||
console.error('안전 체크 저장 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '안전 체크 저장 중 오류가 발생했습니다.',
|
||||
error: err.message
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '안전 체크가 저장되었습니다.',
|
||||
data: { count: records.length }
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
// ==================== 작업 인계 관련 ====================
|
||||
|
||||
/**
|
||||
* 작업 인계 생성
|
||||
*/
|
||||
createHandover: (req, res) => {
|
||||
const handoverData = {
|
||||
session_id: req.body.session_id,
|
||||
from_leader_id: req.body.from_leader_id,
|
||||
to_leader_id: req.body.to_leader_id,
|
||||
handover_date: req.body.handover_date,
|
||||
handover_time: req.body.handover_time || null,
|
||||
reason: req.body.reason,
|
||||
handover_notes: req.body.handover_notes || null,
|
||||
worker_ids: req.body.worker_ids || []
|
||||
};
|
||||
|
||||
// 필수 필드 검증
|
||||
if (!handoverData.session_id || !handoverData.from_leader_id ||
|
||||
!handoverData.to_leader_id || !handoverData.handover_date || !handoverData.reason) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '필수 정보가 누락되었습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
TbmModel.createHandover(handoverData, (err, result) => {
|
||||
if (err) {
|
||||
console.error('작업 인계 생성 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '작업 인계 생성 중 오류가 발생했습니다.',
|
||||
error: err.message
|
||||
});
|
||||
}
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: '작업 인계가 생성되었습니다.',
|
||||
data: { handover_id: result.insertId }
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 작업 인계 확인
|
||||
*/
|
||||
confirmHandover: (req, res) => {
|
||||
const { handoverId } = req.params;
|
||||
const confirmedBy = req.user.user_id;
|
||||
|
||||
TbmModel.confirmHandover(handoverId, confirmedBy, (err, result) => {
|
||||
if (err) {
|
||||
console.error('작업 인계 확인 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '작업 인계 확인 중 오류가 발생했습니다.',
|
||||
error: err.message
|
||||
});
|
||||
}
|
||||
|
||||
if (result.affectedRows === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '작업 인계 건을 찾을 수 없습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '작업 인계가 확인되었습니다.'
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 특정 날짜의 작업 인계 목록 조회
|
||||
*/
|
||||
getHandoversByDate: (req, res) => {
|
||||
const { date } = req.params;
|
||||
|
||||
TbmModel.getHandoversByDate(date, (err, results) => {
|
||||
if (err) {
|
||||
console.error('작업 인계 목록 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '작업 인계 목록 조회 중 오류가 발생했습니다.',
|
||||
error: err.message
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: results
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 나에게 온 미확인 인계 건 조회
|
||||
*/
|
||||
getMyPendingHandovers: (req, res) => {
|
||||
// worker_id는 req.user에서 가져옴
|
||||
const toLeaderId = req.user.worker_id;
|
||||
|
||||
if (!toLeaderId) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '작업자 정보를 찾을 수 없습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
TbmModel.getPendingHandovers(toLeaderId, (err, results) => {
|
||||
if (err) {
|
||||
console.error('미확인 인계 건 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '미확인 인계 건 조회 중 오류가 발생했습니다.',
|
||||
error: err.message
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: results
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
// ==================== 통계 및 리포트 ====================
|
||||
|
||||
/**
|
||||
* TBM 통계 조회
|
||||
*/
|
||||
getTbmStatistics: (req, res) => {
|
||||
const { startDate, endDate } = req.query;
|
||||
|
||||
if (!startDate || !endDate) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '시작일과 종료일이 필요합니다.'
|
||||
});
|
||||
}
|
||||
|
||||
TbmModel.getTbmStatistics(startDate, endDate, (err, results) => {
|
||||
if (err) {
|
||||
console.error('TBM 통계 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: 'TBM 통계 조회 중 오류가 발생했습니다.',
|
||||
error: err.message
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: results
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 리더별 TBM 진행 현황 조회
|
||||
*/
|
||||
getLeaderStatistics: (req, res) => {
|
||||
const { startDate, endDate } = req.query;
|
||||
|
||||
if (!startDate || !endDate) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '시작일과 종료일이 필요합니다.'
|
||||
});
|
||||
}
|
||||
|
||||
TbmModel.getLeaderStatistics(startDate, endDate, (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 = TbmController;
|
||||
158
api.hyungi.net/db/migrations/20260120000000_create_tbm_system.js
Normal file
158
api.hyungi.net/db/migrations/20260120000000_create_tbm_system.js
Normal file
@@ -0,0 +1,158 @@
|
||||
/**
|
||||
* 마이그레이션: TBM (Tool Box Meeting) 시스템
|
||||
* 작성일: 2026-01-20
|
||||
*
|
||||
* 생성 테이블:
|
||||
* - tbm_sessions: TBM 세션 (아침 미팅 기록)
|
||||
* - tbm_team_assignments: TBM 팀 구성 (리더가 선택한 작업자들)
|
||||
* - tbm_safety_checks: TBM 안전 체크리스트
|
||||
* - tbm_safety_records: TBM 안전 체크 기록
|
||||
* - team_handovers: 작업 인계 기록 (반차/조퇴 시)
|
||||
*/
|
||||
|
||||
exports.up = async function(knex) {
|
||||
console.log('⏳ TBM 시스템 테이블 생성 중...');
|
||||
|
||||
// 1. TBM 세션 테이블 (아침 미팅)
|
||||
await knex.schema.createTable('tbm_sessions', (table) => {
|
||||
table.increments('session_id').primary();
|
||||
table.date('session_date').notNullable().comment('TBM 날짜');
|
||||
table.integer('leader_id').notNullable().comment('팀장 worker_id');
|
||||
table.integer('project_id').nullable().comment('프로젝트 ID');
|
||||
table.string('work_location', 200).nullable().comment('작업 장소');
|
||||
table.text('work_description').nullable().comment('작업 내용');
|
||||
table.text('safety_notes').nullable().comment('안전 관련 특이사항');
|
||||
table.enum('status', ['draft', 'completed', 'cancelled']).defaultTo('draft').comment('상태');
|
||||
table.time('start_time').nullable().comment('TBM 시작 시간');
|
||||
table.time('end_time').nullable().comment('TBM 종료 시간');
|
||||
table.integer('created_by').notNullable().comment('생성자 user_id');
|
||||
table.timestamp('created_at').defaultTo(knex.fn.now());
|
||||
table.timestamp('updated_at').defaultTo(knex.fn.now());
|
||||
|
||||
// 인덱스 및 제약조건
|
||||
table.index(['session_date', 'leader_id']);
|
||||
table.foreign('leader_id').references('workers.worker_id');
|
||||
table.foreign('project_id').references('projects.project_id').onDelete('SET NULL');
|
||||
table.foreign('created_by').references('users.user_id');
|
||||
});
|
||||
console.log('✅ tbm_sessions 테이블 생성 완료');
|
||||
|
||||
// 2. TBM 팀 구성 테이블 (리더가 선택한 팀원들)
|
||||
await knex.schema.createTable('tbm_team_assignments', (table) => {
|
||||
table.increments('assignment_id').primary();
|
||||
table.integer('session_id').unsigned().notNullable().comment('TBM 세션 ID');
|
||||
table.integer('worker_id').notNullable().comment('팀원 worker_id');
|
||||
table.string('assigned_role', 100).nullable().comment('역할/담당');
|
||||
table.text('work_detail').nullable().comment('세부 작업 내용');
|
||||
table.boolean('is_present').defaultTo(true).comment('출석 여부');
|
||||
table.text('absence_reason').nullable().comment('결석 사유');
|
||||
table.timestamp('assigned_at').defaultTo(knex.fn.now());
|
||||
|
||||
// 인덱스 및 제약조건
|
||||
table.unique(['session_id', 'worker_id']);
|
||||
table.foreign('session_id').references('tbm_sessions.session_id').onDelete('CASCADE');
|
||||
table.foreign('worker_id').references('workers.worker_id');
|
||||
});
|
||||
console.log('✅ tbm_team_assignments 테이블 생성 완료');
|
||||
|
||||
// 3. TBM 안전 체크리스트 마스터 테이블
|
||||
await knex.schema.createTable('tbm_safety_checks', (table) => {
|
||||
table.increments('check_id').primary();
|
||||
table.string('check_category', 50).notNullable().comment('카테고리 (장비, PPE, 환경 등)');
|
||||
table.string('check_item', 200).notNullable().comment('체크 항목');
|
||||
table.text('description').nullable().comment('설명');
|
||||
table.integer('display_order').defaultTo(0).comment('표시 순서');
|
||||
table.boolean('is_required').defaultTo(true).comment('필수 체크 여부');
|
||||
table.boolean('is_active').defaultTo(true).comment('활성 여부');
|
||||
table.timestamp('created_at').defaultTo(knex.fn.now());
|
||||
table.timestamp('updated_at').defaultTo(knex.fn.now());
|
||||
|
||||
table.index('check_category');
|
||||
});
|
||||
console.log('✅ tbm_safety_checks 테이블 생성 완료');
|
||||
|
||||
// 초기 안전 체크리스트 데이터
|
||||
await knex('tbm_safety_checks').insert([
|
||||
// PPE (개인 보호 장비)
|
||||
{ check_category: 'PPE', check_item: '안전모 착용 확인', display_order: 1, is_required: true },
|
||||
{ check_category: 'PPE', check_item: '안전화 착용 확인', display_order: 2, is_required: true },
|
||||
{ check_category: 'PPE', check_item: '안전조끼 착용 확인', display_order: 3, is_required: true },
|
||||
{ check_category: 'PPE', check_item: '안전벨트 착용 확인 (고소작업 시)', display_order: 4, is_required: false },
|
||||
{ check_category: 'PPE', check_item: '보안경/마스크 착용 확인', display_order: 5, is_required: false },
|
||||
|
||||
// 장비 점검
|
||||
{ check_category: 'EQUIPMENT', check_item: '작업 도구 점검 완료', display_order: 10, is_required: true },
|
||||
{ check_category: 'EQUIPMENT', check_item: '전동공구 안전 점검', display_order: 11, is_required: true },
|
||||
{ check_category: 'EQUIPMENT', check_item: '사다리/비계 안전 확인', display_order: 12, is_required: false },
|
||||
{ check_category: 'EQUIPMENT', check_item: '차량/중장비 점검 완료', display_order: 13, is_required: false },
|
||||
|
||||
// 작업 환경
|
||||
{ check_category: 'ENVIRONMENT', check_item: '작업 장소 정리정돈 확인', display_order: 20, is_required: true },
|
||||
{ check_category: 'ENVIRONMENT', check_item: '위험 구역 표시 확인', display_order: 21, is_required: true },
|
||||
{ check_category: 'ENVIRONMENT', check_item: '기상 상태 확인 (우천, 강풍 등)', display_order: 22, is_required: true },
|
||||
{ check_category: 'ENVIRONMENT', check_item: '작업 동선 안전 확인', display_order: 23, is_required: true },
|
||||
|
||||
// 비상 대응
|
||||
{ check_category: 'EMERGENCY', check_item: '비상연락망 공유 완료', display_order: 30, is_required: true },
|
||||
{ check_category: 'EMERGENCY', check_item: '소화기 위치 확인', display_order: 31, is_required: true },
|
||||
{ check_category: 'EMERGENCY', check_item: '응급처치 키트 위치 확인', display_order: 32, is_required: true },
|
||||
]);
|
||||
console.log('✅ tbm_safety_checks 초기 데이터 입력 완료');
|
||||
|
||||
// 4. TBM 안전 체크 기록 테이블
|
||||
await knex.schema.createTable('tbm_safety_records', (table) => {
|
||||
table.increments('record_id').primary();
|
||||
table.integer('session_id').unsigned().notNullable().comment('TBM 세션 ID');
|
||||
table.integer('check_id').unsigned().notNullable().comment('체크 항목 ID');
|
||||
table.boolean('is_checked').defaultTo(false).comment('체크 여부');
|
||||
table.text('notes').nullable().comment('비고/특이사항');
|
||||
table.integer('checked_by').nullable().comment('체크한 user_id');
|
||||
table.timestamp('checked_at').nullable().comment('체크 시간');
|
||||
|
||||
// 인덱스 및 제약조건
|
||||
table.unique(['session_id', 'check_id']);
|
||||
table.foreign('session_id').references('tbm_sessions.session_id').onDelete('CASCADE');
|
||||
table.foreign('check_id').references('tbm_safety_checks.check_id');
|
||||
table.foreign('checked_by').references('users.user_id');
|
||||
});
|
||||
console.log('✅ tbm_safety_records 테이블 생성 완료');
|
||||
|
||||
// 5. 작업 인계 테이블 (반차/조퇴 시)
|
||||
await knex.schema.createTable('team_handovers', (table) => {
|
||||
table.increments('handover_id').primary();
|
||||
table.integer('session_id').unsigned().notNullable().comment('TBM 세션 ID');
|
||||
table.integer('from_leader_id').notNullable().comment('인계자 worker_id');
|
||||
table.integer('to_leader_id').notNullable().comment('인수자 worker_id');
|
||||
table.date('handover_date').notNullable().comment('인계 날짜');
|
||||
table.time('handover_time').nullable().comment('인계 시간');
|
||||
table.enum('reason', ['half_day', 'early_leave', 'emergency', 'other']).notNullable().comment('인계 사유');
|
||||
table.text('handover_notes').nullable().comment('인계 내용');
|
||||
table.text('worker_ids').nullable().comment('인계하는 작업자 IDs (JSON array)');
|
||||
table.boolean('is_confirmed').defaultTo(false).comment('인수 확인 여부');
|
||||
table.timestamp('confirmed_at').nullable().comment('인수 확인 시간');
|
||||
table.integer('confirmed_by').nullable().comment('인수 확인자 user_id');
|
||||
table.timestamp('created_at').defaultTo(knex.fn.now());
|
||||
|
||||
// 인덱스 및 제약조건
|
||||
table.index(['session_id', 'handover_date']);
|
||||
table.foreign('session_id').references('tbm_sessions.session_id').onDelete('CASCADE');
|
||||
table.foreign('from_leader_id').references('workers.worker_id');
|
||||
table.foreign('to_leader_id').references('workers.worker_id');
|
||||
table.foreign('confirmed_by').references('users.user_id');
|
||||
});
|
||||
console.log('✅ team_handovers 테이블 생성 완료');
|
||||
|
||||
console.log('✅ 모든 TBM 시스템 테이블 생성 완료');
|
||||
};
|
||||
|
||||
exports.down = async function(knex) {
|
||||
console.log('⏳ TBM 시스템 테이블 제거 중...');
|
||||
|
||||
await knex.schema.dropTableIfExists('team_handovers');
|
||||
await knex.schema.dropTableIfExists('tbm_safety_records');
|
||||
await knex.schema.dropTableIfExists('tbm_safety_checks');
|
||||
await knex.schema.dropTableIfExists('tbm_team_assignments');
|
||||
await knex.schema.dropTableIfExists('tbm_sessions');
|
||||
|
||||
console.log('✅ 모든 TBM 시스템 테이블 제거 완료');
|
||||
};
|
||||
33
api.hyungi.net/db/migrations/20260120000001_add_tbm_page.js
Normal file
33
api.hyungi.net/db/migrations/20260120000001_add_tbm_page.js
Normal file
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* 마이그레이션: TBM 페이지 등록
|
||||
* 작성일: 2026-01-20
|
||||
*
|
||||
* pages 테이블에 TBM 페이지 추가
|
||||
*/
|
||||
|
||||
exports.up = async function(knex) {
|
||||
console.log('⏳ TBM 페이지 등록 중...');
|
||||
|
||||
// TBM 페이지 추가
|
||||
await knex('pages').insert([
|
||||
{
|
||||
page_key: 'tbm',
|
||||
page_name: 'TBM 관리',
|
||||
page_path: '/pages/work/tbm.html',
|
||||
category: 'work',
|
||||
description: 'Tool Box Meeting - 아침 안전 회의 및 팀 구성 관리',
|
||||
is_admin_only: false,
|
||||
display_order: 10
|
||||
}
|
||||
]);
|
||||
|
||||
console.log('✅ TBM 페이지 등록 완료');
|
||||
};
|
||||
|
||||
exports.down = async function(knex) {
|
||||
console.log('⏳ TBM 페이지 제거 중...');
|
||||
|
||||
await knex('pages').where('page_key', 'tbm').del();
|
||||
|
||||
console.log('✅ TBM 페이지 제거 완료');
|
||||
};
|
||||
160
api.hyungi.net/models/pageAccessModel.js
Normal file
160
api.hyungi.net/models/pageAccessModel.js
Normal file
@@ -0,0 +1,160 @@
|
||||
// models/pageAccessModel.js
|
||||
const db = require('../db/connection');
|
||||
|
||||
const PageAccessModel = {
|
||||
// 사용자의 페이지 권한 조회
|
||||
getUserPageAccess: (userId, callback) => {
|
||||
const sql = `
|
||||
SELECT
|
||||
p.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,
|
||||
upa.granted_by,
|
||||
granter.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 granter ON upa.granted_by = granter.user_id
|
||||
WHERE p.is_admin_only = 0
|
||||
ORDER BY p.category, p.display_order
|
||||
`;
|
||||
|
||||
db.query(sql, [userId], callback);
|
||||
},
|
||||
|
||||
// 모든 페이지 목록 조회
|
||||
getAllPages: (callback) => {
|
||||
const sql = `
|
||||
SELECT
|
||||
id,
|
||||
page_key,
|
||||
page_name,
|
||||
page_path,
|
||||
category,
|
||||
description,
|
||||
is_admin_only,
|
||||
display_order
|
||||
FROM pages
|
||||
WHERE is_admin_only = 0
|
||||
ORDER BY category, display_order
|
||||
`;
|
||||
|
||||
db.query(sql, callback);
|
||||
},
|
||||
|
||||
// 페이지 권한 부여
|
||||
grantPageAccess: (userId, pageId, grantedBy, callback) => {
|
||||
const sql = `
|
||||
INSERT INTO user_page_access (user_id, page_id, can_access, granted_by, granted_at)
|
||||
VALUES (?, ?, 1, ?, NOW())
|
||||
ON DUPLICATE KEY UPDATE
|
||||
can_access = 1,
|
||||
granted_by = ?,
|
||||
granted_at = NOW()
|
||||
`;
|
||||
|
||||
db.query(sql, [userId, pageId, grantedBy, grantedBy], callback);
|
||||
},
|
||||
|
||||
// 페이지 권한 회수
|
||||
revokePageAccess: (userId, pageId, callback) => {
|
||||
const sql = `
|
||||
DELETE FROM user_page_access
|
||||
WHERE user_id = ? AND page_id = ?
|
||||
`;
|
||||
|
||||
db.query(sql, [userId, pageId], callback);
|
||||
},
|
||||
|
||||
// 여러 페이지 권한 일괄 설정
|
||||
setUserPageAccess: (userId, pageIds, grantedBy, callback) => {
|
||||
db.beginTransaction((err) => {
|
||||
if (err) return callback(err);
|
||||
|
||||
// 기존 권한 모두 삭제
|
||||
const deleteSql = 'DELETE FROM user_page_access WHERE user_id = ?';
|
||||
|
||||
db.query(deleteSql, [userId], (err) => {
|
||||
if (err) {
|
||||
return db.rollback(() => callback(err));
|
||||
}
|
||||
|
||||
// 새 권한이 없으면 커밋하고 종료
|
||||
if (!pageIds || pageIds.length === 0) {
|
||||
return db.commit((err) => {
|
||||
if (err) return db.rollback(() => callback(err));
|
||||
callback(null, { affectedRows: 0 });
|
||||
});
|
||||
}
|
||||
|
||||
// 새 권한 추가
|
||||
const values = pageIds.map(pageId => [userId, pageId, 1, grantedBy]);
|
||||
const insertSql = `
|
||||
INSERT INTO user_page_access (user_id, page_id, can_access, granted_by, granted_at)
|
||||
VALUES ?
|
||||
`;
|
||||
|
||||
db.query(insertSql, [values], (err, result) => {
|
||||
if (err) {
|
||||
return db.rollback(() => callback(err));
|
||||
}
|
||||
|
||||
db.commit((err) => {
|
||||
if (err) return db.rollback(() => callback(err));
|
||||
callback(null, result);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
// 특정 페이지 접근 권한 확인
|
||||
checkPageAccess: (userId, pageKey, callback) => {
|
||||
const sql = `
|
||||
SELECT
|
||||
COALESCE(upa.can_access, 0) as can_access,
|
||||
p.is_admin_only
|
||||
FROM pages p
|
||||
LEFT JOIN user_page_access upa ON p.id = upa.page_id AND upa.user_id = ?
|
||||
WHERE p.page_key = ?
|
||||
`;
|
||||
|
||||
db.query(sql, [userId, pageKey], (err, results) => {
|
||||
if (err) return callback(err);
|
||||
if (results.length === 0) return callback(null, { can_access: false });
|
||||
callback(null, results[0]);
|
||||
});
|
||||
},
|
||||
|
||||
// 계정이 있는 작업자 목록 조회 (권한 관리용)
|
||||
getUsersWithAccounts: (callback) => {
|
||||
const sql = `
|
||||
SELECT
|
||||
u.user_id,
|
||||
u.username,
|
||||
u.name,
|
||||
u.role_id,
|
||||
r.name as role_name,
|
||||
u.worker_id,
|
||||
w.worker_name,
|
||||
w.job_type,
|
||||
COUNT(upa.page_id) as granted_pages_count
|
||||
FROM users u
|
||||
LEFT JOIN roles r ON u.role_id = r.id
|
||||
LEFT JOIN workers w ON u.worker_id = w.worker_id
|
||||
LEFT JOIN user_page_access upa ON u.user_id = upa.user_id AND upa.can_access = 1
|
||||
WHERE u.is_active = 1
|
||||
AND u.role_id IN (4, 5)
|
||||
GROUP BY u.user_id
|
||||
ORDER BY w.worker_name, u.username
|
||||
`;
|
||||
|
||||
db.query(sql, callback);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = PageAccessModel;
|
||||
453
api.hyungi.net/models/tbmModel.js
Normal file
453
api.hyungi.net/models/tbmModel.js
Normal file
@@ -0,0 +1,453 @@
|
||||
// models/tbmModel.js - TBM 시스템 모델
|
||||
const db = require('../db/connection');
|
||||
|
||||
const TbmModel = {
|
||||
// ==================== TBM 세션 관련 ====================
|
||||
|
||||
/**
|
||||
* TBM 세션 생성
|
||||
*/
|
||||
createSession: (sessionData, callback) => {
|
||||
const sql = `
|
||||
INSERT INTO tbm_sessions
|
||||
(session_date, leader_id, project_id, work_location, work_description,
|
||||
safety_notes, start_time, created_by)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`;
|
||||
|
||||
const values = [
|
||||
sessionData.session_date,
|
||||
sessionData.leader_id,
|
||||
sessionData.project_id,
|
||||
sessionData.work_location,
|
||||
sessionData.work_description,
|
||||
sessionData.safety_notes,
|
||||
sessionData.start_time,
|
||||
sessionData.created_by
|
||||
];
|
||||
|
||||
db.query(sql, values, callback);
|
||||
},
|
||||
|
||||
/**
|
||||
* 특정 날짜의 TBM 세션 조회
|
||||
*/
|
||||
getSessionsByDate: (date, callback) => {
|
||||
const sql = `
|
||||
SELECT
|
||||
s.*,
|
||||
w.worker_name as leader_name,
|
||||
w.job_type as leader_job_type,
|
||||
p.project_name,
|
||||
p.job_no,
|
||||
u.username as created_by_username,
|
||||
COUNT(DISTINCT ta.worker_id) as team_member_count
|
||||
FROM tbm_sessions s
|
||||
LEFT JOIN workers w ON s.leader_id = w.worker_id
|
||||
LEFT JOIN projects p ON s.project_id = p.project_id
|
||||
LEFT JOIN users u ON s.created_by = u.user_id
|
||||
LEFT JOIN tbm_team_assignments ta ON s.session_id = ta.session_id
|
||||
WHERE s.session_date = ?
|
||||
GROUP BY s.session_id
|
||||
ORDER BY s.start_time DESC
|
||||
`;
|
||||
|
||||
db.query(sql, [date], callback);
|
||||
},
|
||||
|
||||
/**
|
||||
* TBM 세션 상세 조회
|
||||
*/
|
||||
getSessionById: (sessionId, callback) => {
|
||||
const sql = `
|
||||
SELECT
|
||||
s.*,
|
||||
w.worker_name as leader_name,
|
||||
w.job_type as leader_job_type,
|
||||
w.phone_number as leader_phone,
|
||||
p.project_name,
|
||||
p.job_no,
|
||||
p.site,
|
||||
u.username as created_by_username,
|
||||
u.name as created_by_name
|
||||
FROM tbm_sessions s
|
||||
LEFT JOIN workers w ON s.leader_id = w.worker_id
|
||||
LEFT JOIN projects p ON s.project_id = p.project_id
|
||||
LEFT JOIN users u ON s.created_by = u.user_id
|
||||
WHERE s.session_id = ?
|
||||
`;
|
||||
|
||||
db.query(sql, [sessionId], callback);
|
||||
},
|
||||
|
||||
/**
|
||||
* TBM 세션 수정
|
||||
*/
|
||||
updateSession: (sessionId, sessionData, callback) => {
|
||||
const sql = `
|
||||
UPDATE tbm_sessions
|
||||
SET
|
||||
project_id = ?,
|
||||
work_location = ?,
|
||||
work_description = ?,
|
||||
safety_notes = ?,
|
||||
status = ?,
|
||||
updated_at = NOW()
|
||||
WHERE session_id = ?
|
||||
`;
|
||||
|
||||
const values = [
|
||||
sessionData.project_id,
|
||||
sessionData.work_location,
|
||||
sessionData.work_description,
|
||||
sessionData.safety_notes,
|
||||
sessionData.status,
|
||||
sessionId
|
||||
];
|
||||
|
||||
db.query(sql, values, callback);
|
||||
},
|
||||
|
||||
/**
|
||||
* TBM 세션 완료 처리
|
||||
*/
|
||||
completeSession: (sessionId, endTime, callback) => {
|
||||
const sql = `
|
||||
UPDATE tbm_sessions
|
||||
SET
|
||||
status = 'completed',
|
||||
end_time = ?,
|
||||
updated_at = NOW()
|
||||
WHERE session_id = ?
|
||||
`;
|
||||
|
||||
db.query(sql, [endTime, sessionId], callback);
|
||||
},
|
||||
|
||||
// ==================== 팀 구성 관련 ====================
|
||||
|
||||
/**
|
||||
* 팀원 추가
|
||||
*/
|
||||
addTeamMember: (assignmentData, callback) => {
|
||||
const sql = `
|
||||
INSERT INTO tbm_team_assignments
|
||||
(session_id, worker_id, assigned_role, work_detail, is_present, absence_reason)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
assigned_role = VALUES(assigned_role),
|
||||
work_detail = VALUES(work_detail),
|
||||
is_present = VALUES(is_present),
|
||||
absence_reason = VALUES(absence_reason)
|
||||
`;
|
||||
|
||||
const values = [
|
||||
assignmentData.session_id,
|
||||
assignmentData.worker_id,
|
||||
assignmentData.assigned_role,
|
||||
assignmentData.work_detail,
|
||||
assignmentData.is_present !== undefined ? assignmentData.is_present : true,
|
||||
assignmentData.absence_reason
|
||||
];
|
||||
|
||||
db.query(sql, values, callback);
|
||||
},
|
||||
|
||||
/**
|
||||
* 팀 구성 일괄 추가
|
||||
*/
|
||||
addTeamMembers: (sessionId, members, callback) => {
|
||||
if (!members || members.length === 0) {
|
||||
return callback(null, { affectedRows: 0 });
|
||||
}
|
||||
|
||||
const values = members.map(m => [
|
||||
sessionId,
|
||||
m.worker_id,
|
||||
m.assigned_role || null,
|
||||
m.work_detail || null,
|
||||
m.is_present !== undefined ? m.is_present : true,
|
||||
m.absence_reason || null
|
||||
]);
|
||||
|
||||
const sql = `
|
||||
INSERT INTO tbm_team_assignments
|
||||
(session_id, worker_id, assigned_role, work_detail, is_present, absence_reason)
|
||||
VALUES ?
|
||||
`;
|
||||
|
||||
db.query(sql, [values], callback);
|
||||
},
|
||||
|
||||
/**
|
||||
* TBM 세션의 팀 구성 조회
|
||||
*/
|
||||
getTeamMembers: (sessionId, callback) => {
|
||||
const sql = `
|
||||
SELECT
|
||||
ta.*,
|
||||
w.worker_name,
|
||||
w.job_type,
|
||||
w.phone_number,
|
||||
w.department
|
||||
FROM tbm_team_assignments ta
|
||||
INNER JOIN workers w ON ta.worker_id = w.worker_id
|
||||
WHERE ta.session_id = ?
|
||||
ORDER BY ta.assigned_at DESC
|
||||
`;
|
||||
|
||||
db.query(sql, [sessionId], callback);
|
||||
},
|
||||
|
||||
/**
|
||||
* 팀원 제거
|
||||
*/
|
||||
removeTeamMember: (sessionId, workerId, callback) => {
|
||||
const sql = `
|
||||
DELETE FROM tbm_team_assignments
|
||||
WHERE session_id = ? AND worker_id = ?
|
||||
`;
|
||||
|
||||
db.query(sql, [sessionId, workerId], callback);
|
||||
},
|
||||
|
||||
// ==================== 안전 체크리스트 관련 ====================
|
||||
|
||||
/**
|
||||
* 모든 안전 체크 항목 조회
|
||||
*/
|
||||
getAllSafetyChecks: (callback) => {
|
||||
const sql = `
|
||||
SELECT *
|
||||
FROM tbm_safety_checks
|
||||
WHERE is_active = 1
|
||||
ORDER BY check_category, display_order
|
||||
`;
|
||||
|
||||
db.query(sql, callback);
|
||||
},
|
||||
|
||||
/**
|
||||
* 카테고리별 안전 체크 항목 조회
|
||||
*/
|
||||
getSafetyChecksByCategory: (category, callback) => {
|
||||
const sql = `
|
||||
SELECT *
|
||||
FROM tbm_safety_checks
|
||||
WHERE check_category = ? AND is_active = 1
|
||||
ORDER BY display_order
|
||||
`;
|
||||
|
||||
db.query(sql, [category], callback);
|
||||
},
|
||||
|
||||
/**
|
||||
* TBM 세션의 안전 체크 기록 조회
|
||||
*/
|
||||
getSafetyRecords: (sessionId, callback) => {
|
||||
const sql = `
|
||||
SELECT
|
||||
sr.*,
|
||||
sc.check_category,
|
||||
sc.check_item,
|
||||
sc.description,
|
||||
sc.is_required,
|
||||
u.username as checked_by_username,
|
||||
u.name as checked_by_name
|
||||
FROM tbm_safety_records sr
|
||||
INNER JOIN tbm_safety_checks sc ON sr.check_id = sc.check_id
|
||||
LEFT JOIN users u ON sr.checked_by = u.user_id
|
||||
WHERE sr.session_id = ?
|
||||
ORDER BY sc.check_category, sc.display_order
|
||||
`;
|
||||
|
||||
db.query(sql, [sessionId], callback);
|
||||
},
|
||||
|
||||
/**
|
||||
* 안전 체크 기록 저장/업데이트
|
||||
*/
|
||||
saveSafetyRecord: (recordData, callback) => {
|
||||
const sql = `
|
||||
INSERT INTO tbm_safety_records
|
||||
(session_id, check_id, is_checked, notes, checked_by, checked_at)
|
||||
VALUES (?, ?, ?, ?, ?, NOW())
|
||||
ON DUPLICATE KEY UPDATE
|
||||
is_checked = VALUES(is_checked),
|
||||
notes = VALUES(notes),
|
||||
checked_by = VALUES(checked_by),
|
||||
checked_at = NOW()
|
||||
`;
|
||||
|
||||
const values = [
|
||||
recordData.session_id,
|
||||
recordData.check_id,
|
||||
recordData.is_checked,
|
||||
recordData.notes,
|
||||
recordData.checked_by
|
||||
];
|
||||
|
||||
db.query(sql, values, callback);
|
||||
},
|
||||
|
||||
/**
|
||||
* 안전 체크 일괄 저장
|
||||
*/
|
||||
saveSafetyRecords: (sessionId, records, checkedBy, callback) => {
|
||||
if (!records || records.length === 0) {
|
||||
return callback(null, { affectedRows: 0 });
|
||||
}
|
||||
|
||||
const values = records.map(r => [
|
||||
sessionId,
|
||||
r.check_id,
|
||||
r.is_checked,
|
||||
r.notes || null,
|
||||
checkedBy
|
||||
]);
|
||||
|
||||
const sql = `
|
||||
INSERT INTO tbm_safety_records
|
||||
(session_id, check_id, is_checked, notes, checked_by, checked_at)
|
||||
VALUES ?
|
||||
ON DUPLICATE KEY UPDATE
|
||||
is_checked = VALUES(is_checked),
|
||||
notes = VALUES(notes),
|
||||
checked_by = VALUES(checked_by),
|
||||
checked_at = NOW()
|
||||
`;
|
||||
|
||||
db.query(sql, [values], callback);
|
||||
},
|
||||
|
||||
// ==================== 작업 인계 관련 ====================
|
||||
|
||||
/**
|
||||
* 작업 인계 생성
|
||||
*/
|
||||
createHandover: (handoverData, callback) => {
|
||||
const sql = `
|
||||
INSERT INTO team_handovers
|
||||
(session_id, from_leader_id, to_leader_id, handover_date, handover_time,
|
||||
reason, handover_notes, worker_ids)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`;
|
||||
|
||||
const values = [
|
||||
handoverData.session_id,
|
||||
handoverData.from_leader_id,
|
||||
handoverData.to_leader_id,
|
||||
handoverData.handover_date,
|
||||
handoverData.handover_time,
|
||||
handoverData.reason,
|
||||
handoverData.handover_notes,
|
||||
JSON.stringify(handoverData.worker_ids || [])
|
||||
];
|
||||
|
||||
db.query(sql, values, callback);
|
||||
},
|
||||
|
||||
/**
|
||||
* 작업 인계 확인
|
||||
*/
|
||||
confirmHandover: (handoverId, confirmedBy, callback) => {
|
||||
const sql = `
|
||||
UPDATE team_handovers
|
||||
SET
|
||||
is_confirmed = 1,
|
||||
confirmed_at = NOW(),
|
||||
confirmed_by = ?
|
||||
WHERE handover_id = ?
|
||||
`;
|
||||
|
||||
db.query(sql, [confirmedBy, handoverId], callback);
|
||||
},
|
||||
|
||||
/**
|
||||
* 특정 날짜의 작업 인계 목록 조회
|
||||
*/
|
||||
getHandoversByDate: (date, callback) => {
|
||||
const sql = `
|
||||
SELECT
|
||||
h.*,
|
||||
w1.worker_name as from_leader_name,
|
||||
w2.worker_name as to_leader_name,
|
||||
u.username as confirmed_by_username,
|
||||
u.name as confirmed_by_name
|
||||
FROM team_handovers h
|
||||
INNER JOIN workers w1 ON h.from_leader_id = w1.worker_id
|
||||
INNER JOIN workers w2 ON h.to_leader_id = w2.worker_id
|
||||
LEFT JOIN users u ON h.confirmed_by = u.user_id
|
||||
WHERE h.handover_date = ?
|
||||
ORDER BY h.handover_time DESC
|
||||
`;
|
||||
|
||||
db.query(sql, [date], callback);
|
||||
},
|
||||
|
||||
/**
|
||||
* 인수자가 받은 미확인 인계 건 조회
|
||||
*/
|
||||
getPendingHandovers: (toLeaderId, callback) => {
|
||||
const sql = `
|
||||
SELECT
|
||||
h.*,
|
||||
w1.worker_name as from_leader_name,
|
||||
w1.phone_number as from_leader_phone,
|
||||
s.work_location,
|
||||
s.work_description
|
||||
FROM team_handovers h
|
||||
INNER JOIN workers w1 ON h.from_leader_id = w1.worker_id
|
||||
LEFT JOIN tbm_sessions s ON h.session_id = s.session_id
|
||||
WHERE h.to_leader_id = ? AND h.is_confirmed = 0
|
||||
ORDER BY h.handover_date DESC, h.handover_time DESC
|
||||
`;
|
||||
|
||||
db.query(sql, [toLeaderId], callback);
|
||||
},
|
||||
|
||||
// ==================== 통계 및 리포트 ====================
|
||||
|
||||
/**
|
||||
* 특정 기간의 TBM 통계
|
||||
*/
|
||||
getTbmStatistics: (startDate, endDate, callback) => {
|
||||
const sql = `
|
||||
SELECT
|
||||
DATE(session_date) as date,
|
||||
COUNT(DISTINCT session_id) as session_count,
|
||||
COUNT(DISTINCT leader_id) as leader_count,
|
||||
SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) as completed_count
|
||||
FROM tbm_sessions
|
||||
WHERE session_date BETWEEN ? AND ?
|
||||
GROUP BY DATE(session_date)
|
||||
ORDER BY date DESC
|
||||
`;
|
||||
|
||||
db.query(sql, [startDate, endDate], callback);
|
||||
},
|
||||
|
||||
/**
|
||||
* 리더별 TBM 진행 현황
|
||||
*/
|
||||
getLeaderStatistics: (startDate, endDate, callback) => {
|
||||
const sql = `
|
||||
SELECT
|
||||
s.leader_id,
|
||||
w.worker_name as leader_name,
|
||||
COUNT(DISTINCT s.session_id) as total_sessions,
|
||||
SUM(CASE WHEN s.status = 'completed' THEN 1 ELSE 0 END) as completed_sessions,
|
||||
COUNT(DISTINCT ta.worker_id) as total_team_members
|
||||
FROM tbm_sessions s
|
||||
INNER JOIN workers w ON s.leader_id = w.worker_id
|
||||
LEFT JOIN tbm_team_assignments ta ON s.session_id = ta.session_id
|
||||
WHERE s.session_date BETWEEN ? AND ?
|
||||
GROUP BY s.leader_id
|
||||
ORDER BY total_sessions DESC
|
||||
`;
|
||||
|
||||
db.query(sql, [startDate, endDate], callback);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = TbmModel;
|
||||
71
api.hyungi.net/routes/tbmRoutes.js
Normal file
71
api.hyungi.net/routes/tbmRoutes.js
Normal file
@@ -0,0 +1,71 @@
|
||||
// routes/tbmRoutes.js - TBM 시스템 라우트
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const TbmController = require('../controllers/tbmController');
|
||||
const { authenticateToken } = require('../middlewares/auth');
|
||||
|
||||
// ==================== TBM 세션 관련 ====================
|
||||
|
||||
// TBM 세션 생성
|
||||
router.post('/sessions', authenticateToken, TbmController.createSession);
|
||||
|
||||
// 특정 날짜의 TBM 세션 목록 조회
|
||||
router.get('/sessions/date/:date', authenticateToken, TbmController.getSessionsByDate);
|
||||
|
||||
// TBM 세션 상세 조회
|
||||
router.get('/sessions/:sessionId', authenticateToken, TbmController.getSessionById);
|
||||
|
||||
// TBM 세션 수정
|
||||
router.put('/sessions/:sessionId', authenticateToken, TbmController.updateSession);
|
||||
|
||||
// TBM 세션 완료 처리
|
||||
router.post('/sessions/:sessionId/complete', authenticateToken, TbmController.completeSession);
|
||||
|
||||
// ==================== 팀 구성 관련 ====================
|
||||
|
||||
// 팀원 추가 (단일)
|
||||
router.post('/sessions/:sessionId/team', authenticateToken, TbmController.addTeamMember);
|
||||
|
||||
// 팀 구성 일괄 추가
|
||||
router.post('/sessions/:sessionId/team/batch', authenticateToken, TbmController.addTeamMembers);
|
||||
|
||||
// TBM 세션의 팀 구성 조회
|
||||
router.get('/sessions/:sessionId/team', authenticateToken, TbmController.getTeamMembers);
|
||||
|
||||
// 팀원 제거
|
||||
router.delete('/sessions/:sessionId/team/:workerId', authenticateToken, TbmController.removeTeamMember);
|
||||
|
||||
// ==================== 안전 체크리스트 관련 ====================
|
||||
|
||||
// 모든 안전 체크 항목 조회
|
||||
router.get('/safety-checks', authenticateToken, TbmController.getAllSafetyChecks);
|
||||
|
||||
// TBM 세션의 안전 체크 기록 조회
|
||||
router.get('/sessions/:sessionId/safety', authenticateToken, TbmController.getSafetyRecords);
|
||||
|
||||
// 안전 체크 일괄 저장
|
||||
router.post('/sessions/:sessionId/safety', authenticateToken, TbmController.saveSafetyRecords);
|
||||
|
||||
// ==================== 작업 인계 관련 ====================
|
||||
|
||||
// 작업 인계 생성
|
||||
router.post('/handovers', authenticateToken, TbmController.createHandover);
|
||||
|
||||
// 작업 인계 확인
|
||||
router.post('/handovers/:handoverId/confirm', authenticateToken, TbmController.confirmHandover);
|
||||
|
||||
// 특정 날짜의 작업 인계 목록 조회
|
||||
router.get('/handovers/date/:date', authenticateToken, TbmController.getHandoversByDate);
|
||||
|
||||
// 나에게 온 미확인 인계 건 조회
|
||||
router.get('/handovers/pending', authenticateToken, TbmController.getMyPendingHandovers);
|
||||
|
||||
// ==================== 통계 및 리포트 ====================
|
||||
|
||||
// TBM 통계 조회
|
||||
router.get('/statistics/tbm', authenticateToken, TbmController.getTbmStatistics);
|
||||
|
||||
// 리더별 TBM 진행 현황 조회
|
||||
router.get('/statistics/leaders', authenticateToken, TbmController.getLeaderStatistics);
|
||||
|
||||
module.exports = router;
|
||||
Reference in New Issue
Block a user