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:
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;
|
||||
Reference in New Issue
Block a user