## 주요 변경사항 ### 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>
576 lines
15 KiB
JavaScript
576 lines
15 KiB
JavaScript
// 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;
|