From 7637be33f35c6fad2f660f0c4dd0af7122512ec2 Mon Sep 17 00:00:00 2001
From: Hyungi Ahn
Date: Wed, 25 Feb 2026 07:46:21 +0900
Subject: [PATCH] =?UTF-8?q?feat:=20TBM=20=EB=AA=A8=EB=B0=94=EC=9D=BC=20?=
=?UTF-8?q?=EC=8B=9C=EC=8A=A4=ED=85=9C=20+=20=EC=9E=91=EC=97=85=20?=
=?UTF-8?q?=EB=B6=84=ED=95=A0/=EC=9D=B4=EB=8F=99=20+=20=EA=B6=8C=ED=95=9C?=
=?UTF-8?q?=20=ED=86=B5=ED=95=A9?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
TBM 시스템:
- 4단계 워크플로우 (draft→세부편집→완료→작업보고)
- 모바일 전용 TBM 페이지 (tbm-mobile.html) + 3단계 생성 위자드
- 작업자 작업 분할 (work_hours + split_seq)
- 작업자 이동 보내기/빼오기 (tbm_transfers 테이블)
- 생성 시 중복 배정 방지 (당일 배정 현황 조회)
- 데스크탑 TBM 페이지 세부편집 기능 추가
작업보고서:
- 모바일 전용 작업보고서 페이지 (report-create-mobile.html)
- TBM에서 사전 등록된 work_hours 자동 반영
권한 시스템:
- tkuser user_page_permissions 테이블과 system1 페이지 접근 연동
- pageAccessRoutes를 userRoutes보다 먼저 등록 (라우트 우선순위 수정)
- TKUSER_DEFAULT_ACCESS 폴백 추가 (개인→부서→기본값 3단계)
- 권한 캐시키 갱신 (userPageAccess_v2)
기타:
- app-init.js 캐시 버스팅 (v=5)
- iOS Safari touch-action: manipulation 적용
- KST 타임존 날짜 버그 수정 (toISOString UTC 이슈)
Co-Authored-By: Claude Opus 4.6
---
.../controllers/authController.js | 3 +
system1-factory/api/config/routes.js | 2 +-
.../api/controllers/tbmController.js | 178 +-
..._add_attendance_type_to_tbm_assignments.js | 26 +
.../20260225000001_add_work_hours.js | 21 +
.../20260225000002_create_tbm_transfers.js | 28 +
.../20260225000003_add_split_seq.js | 43 +
system1-factory/api/models/tbmModel.js | 178 +-
.../api/models/tbmTransferModel.js | 294 +++
.../api/routes/pageAccessRoutes.js | 166 +-
system1-factory/api/routes/tbmRoutes.js | 17 +
system1-factory/api/routes/userRoutes.js | 20 +-
.../web/components/mobile-nav.html | 4 +-
.../web/css/daily-work-report-mobile.css | 1318 ++++++++++
system1-factory/web/css/mobile.css | 286 ++-
system1-factory/web/css/tbm.css | 109 +
system1-factory/web/js/app-init.js | 18 +-
system1-factory/web/js/auth-check.js | 10 +-
.../web/js/daily-work-report-mobile.js | 1584 ++++++++++++
system1-factory/web/js/daily-work-report.js | 55 +-
system1-factory/web/js/mobile-dashboard.js | 443 ++++
system1-factory/web/js/tbm-create.js | 573 +++++
system1-factory/web/js/tbm.js | 594 ++++-
system1-factory/web/js/tbm/api.js | 7 +-
system1-factory/web/js/tbm/state.js | 3 +-
system1-factory/web/pages/admin/accounts.html | 2 +-
.../web/pages/admin/attendance-report.html | 2 +-
.../web/pages/admin/departments.html | 2 +-
.../web/pages/admin/equipment-detail.html | 2 +-
.../web/pages/admin/equipments.html | 2 +-
.../web/pages/admin/issue-categories.html | 2 +-
.../web/pages/admin/notifications.html | 2 +-
system1-factory/web/pages/admin/projects.html | 2 +-
.../web/pages/admin/repair-management.html | 2 +-
system1-factory/web/pages/admin/tasks.html | 2 +-
system1-factory/web/pages/admin/workers.html | 2 +-
.../web/pages/admin/workplaces.html | 2 +-
.../web/pages/attendance/annual-overview.html | 2 +-
.../web/pages/attendance/checkin.html | 2 +-
.../web/pages/attendance/daily.html | 2 +-
.../web/pages/attendance/monthly.html | 2 +-
.../pages/attendance/my-vacation-info.html | 2 +-
.../pages/attendance/vacation-allocation.html | 2 +-
.../pages/attendance/vacation-approval.html | 2 +-
.../web/pages/attendance/vacation-input.html | 2 +-
.../pages/attendance/vacation-management.html | 2 +-
.../pages/attendance/vacation-request.html | 2 +-
.../web/pages/attendance/work-status.html | 2 +-
system1-factory/web/pages/dashboard.html | 23 +-
.../web/pages/inspection/daily-patrol.html | 2 +-
.../web/pages/inspection/zone-detail.html | 2 +-
.../web/pages/safety/checklist-manage.html | 2 +-
.../web/pages/safety/management.html | 2 +-
system1-factory/web/pages/safety/report.html | 2 +-
.../web/pages/safety/training-conduct.html | 2 +-
.../web/pages/safety/visit-request.html | 2 +-
system1-factory/web/pages/work/analysis.html | 2 +-
.../web/pages/work/nonconformity.html | 2 +-
.../web/pages/work/report-create-mobile.html | 194 ++
.../web/pages/work/report-create.html | 12 +-
.../web/pages/work/tbm-create.html | 823 ++++++
.../web/pages/work/tbm-mobile.html | 2212 +++++++++++++++++
system1-factory/web/pages/work/tbm.html | 77 +-
system2-report/web/js/issue-report.js | 213 ++
.../web/pages/safety/issue-report.html | 112 +
65 files changed, 9470 insertions(+), 240 deletions(-)
create mode 100644 system1-factory/api/db/migrations/20260224000001_add_attendance_type_to_tbm_assignments.js
create mode 100644 system1-factory/api/db/migrations/20260225000001_add_work_hours.js
create mode 100644 system1-factory/api/db/migrations/20260225000002_create_tbm_transfers.js
create mode 100644 system1-factory/api/db/migrations/20260225000003_add_split_seq.js
create mode 100644 system1-factory/api/models/tbmTransferModel.js
create mode 100644 system1-factory/web/css/daily-work-report-mobile.css
create mode 100644 system1-factory/web/js/daily-work-report-mobile.js
create mode 100644 system1-factory/web/js/mobile-dashboard.js
create mode 100644 system1-factory/web/js/tbm-create.js
create mode 100644 system1-factory/web/pages/work/report-create-mobile.html
create mode 100644 system1-factory/web/pages/work/tbm-create.html
create mode 100644 system1-factory/web/pages/work/tbm-mobile.html
diff --git a/sso-auth-service/controllers/authController.js b/sso-auth-service/controllers/authController.js
index c7a5941..d88b468 100644
--- a/sso-auth-service/controllers/authController.js
+++ b/sso-auth-service/controllers/authController.js
@@ -21,6 +21,7 @@ function createTokenPayload(user) {
id: user.user_id,
username: user.username,
name: user.name,
+ worker_id: user.worker_id || null,
department: user.department,
role: user.role,
access_level: user.role,
@@ -84,6 +85,7 @@ async function login(req, res, next) {
name: user.name,
department: user.department,
role: user.role,
+ worker_id: user.worker_id || null,
system_access: payload.system_access
}
});
@@ -159,6 +161,7 @@ async function validate(req, res, next) {
name: user.name,
department: user.department,
role: user.role,
+ worker_id: user.worker_id || null,
system_access: {
system1: user.system1_access,
system2: user.system2_access,
diff --git a/system1-factory/api/config/routes.js b/system1-factory/api/config/routes.js
index faef062..a03b560 100644
--- a/system1-factory/api/config/routes.js
+++ b/system1-factory/api/config/routes.js
@@ -149,6 +149,7 @@ function setupRoutes(app) {
app.use('/api/performance', performanceRoutes);
app.use('/api/projects', projectRoutes);
app.use('/api/tools', toolsRoute);
+ app.use('/api', pageAccessRoutes); // 페이지 접근 권한 관리 (userRoutes보다 먼저 등록 - /users/:id/page-access 매칭 우선)
app.use('/api/users', userRoutes);
app.use('/api/workplaces', workplaceRoutes);
app.use('/api/equipments', equipmentRoutes);
@@ -157,7 +158,6 @@ function setupRoutes(app) {
app.use('/api/vacation-types', vacationTypeRoutes); // 휴가 유형 관리
app.use('/api/vacation-balances', vacationBalanceRoutes); // 휴가 잔액 관리
app.use('/api/workplace-visits', visitRequestRoutes); // 출입 신청 및 안전교육 관리
- app.use('/api', pageAccessRoutes); // 페이지 접근 권한 관리
app.use('/api/tbm', tbmRoutes); // TBM 시스템
app.use('/api/work-issues', workIssueRoutes); // 카테고리/아이템 + 신고 조회 (같은 MariaDB 공유)
app.use('/api/departments', departmentRoutes); // 부서 관리
diff --git a/system1-factory/api/controllers/tbmController.js b/system1-factory/api/controllers/tbmController.js
index 5ab7dc6..8baba23 100644
--- a/system1-factory/api/controllers/tbmController.js
+++ b/system1-factory/api/controllers/tbmController.js
@@ -1,5 +1,6 @@
// controllers/tbmController.js - TBM 시스템 컨트롤러
const TbmModel = require('../models/tbmModel');
+const TbmTransferModel = require('../models/tbmTransferModel');
const TbmController = {
// ==================== TBM 세션 관련 ====================
@@ -151,7 +152,37 @@ const TbmController = {
completeSession: (req, res) => {
const { sessionId } = req.params;
const endTime = req.body.end_time || new Date().toTimeString().slice(0, 8);
+ const attendanceData = req.body.attendance_data;
+ // 근태 데이터가 있으면 새 메서드 사용
+ if (attendanceData && Array.isArray(attendanceData) && attendanceData.length > 0) {
+ const createdBy = req.user.user_id;
+ TbmModel.completeSessionWithAttendance(sessionId, endTime, attendanceData, createdBy, (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 세션이 완료되었습니다.'
+ });
+ });
+ return;
+ }
+
+ // 기존 방식 (하위 호환)
TbmModel.completeSession(sessionId, endTime, (err, result) => {
if (err) {
console.error('TBM 세션 완료 처리 오류:', err);
@@ -223,7 +254,8 @@ const TbmController = {
work_type_id: req.body.work_type_id || null,
task_id: req.body.task_id || null,
workplace_category_id: req.body.workplace_category_id || null,
- workplace_id: req.body.workplace_id || null
+ workplace_id: req.body.workplace_id || null,
+ work_hours: req.body.work_hours !== undefined ? req.body.work_hours : undefined
};
if (!assignmentData.worker_id) {
@@ -250,6 +282,34 @@ const TbmController = {
});
},
+ /**
+ * 분할 항목 추가 (같은 작업자의 추가 배정)
+ */
+ addSplitAssignment: (req, res) => {
+ const assignmentData = {
+ session_id: req.params.sessionId,
+ worker_id: req.body.worker_id,
+ work_hours: req.body.work_hours,
+ project_id: req.body.project_id || null,
+ work_type_id: req.body.work_type_id || null,
+ task_id: req.body.task_id || null,
+ workplace_category_id: req.body.workplace_category_id || null,
+ workplace_id: req.body.workplace_id || null
+ };
+
+ if (!assignmentData.worker_id || !assignmentData.work_hours) {
+ return res.status(400).json({ success: false, message: '작업자 ID와 작업시간이 필요합니다.' });
+ }
+
+ TbmModel.addSplitAssignment(assignmentData, (err, result) => {
+ if (err) {
+ console.error('분할 항목 추가 오류:', err);
+ return res.status(500).json({ success: false, message: '분할 항목 추가 중 오류가 발생했습니다.' });
+ }
+ res.json({ success: true, data: result });
+ });
+ },
+
/**
* 팀 구성 일괄 추가
*/
@@ -892,6 +952,122 @@ const TbmController = {
});
},
+ // ==================== 작업자 이동 관련 ====================
+
+ /**
+ * 이동 실행 (보내기/빼오기)
+ */
+ createTransfer: (req, res) => {
+ const { transfer_type, worker_id, source_session_id, dest_session_id, hours,
+ project_id, work_type_id, task_id, workplace_category_id, workplace_id } = req.body;
+
+ if (!transfer_type || !worker_id || !source_session_id || !dest_session_id || !hours) {
+ return res.status(400).json({
+ success: false,
+ message: '필수 정보가 누락되었습니다.'
+ });
+ }
+
+ const today = new Date();
+ const transferDate = today.getFullYear() + '-' +
+ String(today.getMonth() + 1).padStart(2, '0') + '-' +
+ String(today.getDate()).padStart(2, '0');
+
+ const transferData = {
+ transfer_type, worker_id, source_session_id, dest_session_id,
+ hours, initiated_by: req.user.user_id, transfer_date: transferDate,
+ project_id, work_type_id, task_id, workplace_category_id, workplace_id
+ };
+
+ TbmTransferModel.createTransfer(transferData, (err, result) => {
+ if (err) {
+ console.error('이동 실행 오류:', err);
+ return res.status(500).json({
+ success: false,
+ message: '이동 실행 중 오류가 발생했습니다.',
+ error: err.message
+ });
+ }
+
+ if (!result.success) {
+ return res.status(400).json(result);
+ }
+
+ res.json({
+ success: true,
+ message: '이동이 완료되었습니다.',
+ data: result
+ });
+ });
+ },
+
+ /**
+ * 당일 이동 내역 조회
+ */
+ getTransfersByDate: (req, res) => {
+ const { date } = req.params;
+
+ TbmTransferModel.getTransfersByDate(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 });
+ });
+ },
+
+ /**
+ * 이동 취소 (원복)
+ */
+ cancelTransfer: (req, res) => {
+ const { transferId } = req.params;
+
+ TbmTransferModel.cancelTransfer(transferId, (err, result) => {
+ if (err) {
+ console.error('이동 취소 오류:', err);
+ return res.status(500).json({
+ success: false,
+ message: '이동 취소 중 오류가 발생했습니다.',
+ error: err.message
+ });
+ }
+
+ if (!result.success) {
+ return res.status(400).json(result);
+ }
+
+ res.json({
+ success: true,
+ message: '이동이 취소되었습니다.'
+ });
+ });
+ },
+
+ /**
+ * 당일 전 작업자 배정 현황
+ */
+ getWorkerAssignmentsByDate: (req, res) => {
+ const { date } = req.params;
+
+ TbmTransferModel.getWorkerAssignmentsByDate(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 });
+ });
+ },
+
/**
* 작업보고서가 작성되지 않은 TBM 팀 배정 조회
*/
diff --git a/system1-factory/api/db/migrations/20260224000001_add_attendance_type_to_tbm_assignments.js b/system1-factory/api/db/migrations/20260224000001_add_attendance_type_to_tbm_assignments.js
new file mode 100644
index 0000000..153ef7f
--- /dev/null
+++ b/system1-factory/api/db/migrations/20260224000001_add_attendance_type_to_tbm_assignments.js
@@ -0,0 +1,26 @@
+/**
+ * TBM 팀 배정에 근태 유형 컬럼 추가
+ * - attendance_type: 근태유형 (overtime/regular/annual/half/quarter/early)
+ * - attendance_hours: 추가시간(overtime) 또는 실제근무시간(early)
+ *
+ * @since 2026-02-24
+ */
+
+exports.up = function(knex) {
+ return knex.raw(`
+ ALTER TABLE tbm_team_assignments
+ ADD COLUMN attendance_type ENUM('overtime','regular','annual','half','quarter','early')
+ DEFAULT NULL
+ COMMENT '근태유형',
+ ADD COLUMN attendance_hours DECIMAL(4,1) DEFAULT NULL
+ COMMENT '추가시간(overtime) 또는 실제근무시간(early)'
+ `);
+};
+
+exports.down = function(knex) {
+ return knex.raw(`
+ ALTER TABLE tbm_team_assignments
+ DROP COLUMN attendance_type,
+ DROP COLUMN attendance_hours
+ `);
+};
diff --git a/system1-factory/api/db/migrations/20260225000001_add_work_hours.js b/system1-factory/api/db/migrations/20260225000001_add_work_hours.js
new file mode 100644
index 0000000..960391c
--- /dev/null
+++ b/system1-factory/api/db/migrations/20260225000001_add_work_hours.js
@@ -0,0 +1,21 @@
+/**
+ * TBM 팀 배정에 작업시간 컬럼 추가
+ * - work_hours: 작업시간 (NULL=종일 8h, 값=해당 시간만)
+ *
+ * @since 2026-02-25
+ */
+
+exports.up = function(knex) {
+ return knex.raw(`
+ ALTER TABLE tbm_team_assignments
+ ADD COLUMN work_hours DECIMAL(4,1) DEFAULT NULL
+ COMMENT 'NULL=종일(8h), 값 있으면 해당 시간만'
+ `);
+};
+
+exports.down = function(knex) {
+ return knex.raw(`
+ ALTER TABLE tbm_team_assignments
+ DROP COLUMN work_hours
+ `);
+};
diff --git a/system1-factory/api/db/migrations/20260225000002_create_tbm_transfers.js b/system1-factory/api/db/migrations/20260225000002_create_tbm_transfers.js
new file mode 100644
index 0000000..7830f94
--- /dev/null
+++ b/system1-factory/api/db/migrations/20260225000002_create_tbm_transfers.js
@@ -0,0 +1,28 @@
+/**
+ * TBM 작업자 이동 로그 테이블 생성
+ * - 작업자를 다른 TBM 세션으로 보내기/빼오기 기록
+ *
+ * @since 2026-02-25
+ */
+
+exports.up = function(knex) {
+ return knex.raw(`
+ CREATE TABLE tbm_transfers (
+ transfer_id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
+ transfer_date DATE NOT NULL,
+ worker_id INT NOT NULL,
+ source_session_id INT UNSIGNED NOT NULL,
+ dest_session_id INT UNSIGNED NOT NULL,
+ hours DECIMAL(4,1) NOT NULL,
+ transfer_type ENUM('send','pull') NOT NULL,
+ initiated_by INT NOT NULL,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ INDEX idx_date (transfer_date),
+ INDEX idx_worker (worker_id, transfer_date)
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
+ `);
+};
+
+exports.down = function(knex) {
+ return knex.raw(`DROP TABLE IF EXISTS tbm_transfers`);
+};
diff --git a/system1-factory/api/db/migrations/20260225000003_add_split_seq.js b/system1-factory/api/db/migrations/20260225000003_add_split_seq.js
new file mode 100644
index 0000000..7f6643e
--- /dev/null
+++ b/system1-factory/api/db/migrations/20260225000003_add_split_seq.js
@@ -0,0 +1,43 @@
+// 20260225000003_add_split_seq.js
+// 같은 작업자가 같은 세션에서 여러 분할 항목을 가질 수 있도록 split_seq 추가
+
+exports.up = function(knex) {
+ return knex.raw(`
+ ALTER TABLE tbm_team_assignments
+ ADD COLUMN split_seq INT UNSIGNED DEFAULT 0 AFTER work_hours
+ `).then(() => {
+ return knex.raw(`
+ ALTER TABLE tbm_team_assignments
+ DROP INDEX tbm_team_assignments_session_id_worker_id_unique
+ `);
+ }).then(() => {
+ return knex.raw(`
+ ALTER TABLE tbm_team_assignments
+ ADD UNIQUE INDEX uniq_session_worker_seq (session_id, worker_id, split_seq)
+ `);
+ });
+};
+
+exports.down = function(knex) {
+ return knex.raw(`
+ ALTER TABLE tbm_team_assignments
+ DROP INDEX uniq_session_worker_seq
+ `).then(() => {
+ return knex.raw(`
+ DELETE t1 FROM tbm_team_assignments t1
+ INNER JOIN tbm_team_assignments t2
+ WHERE t1.assignment_id > t2.assignment_id
+ AND t1.session_id = t2.session_id
+ AND t1.worker_id = t2.worker_id
+ `);
+ }).then(() => {
+ return knex.raw(`
+ ALTER TABLE tbm_team_assignments
+ ADD UNIQUE INDEX tbm_team_assignments_session_id_worker_id_unique (session_id, worker_id)
+ `);
+ }).then(() => {
+ return knex.raw(`
+ ALTER TABLE tbm_team_assignments DROP COLUMN split_seq
+ `);
+ });
+};
diff --git a/system1-factory/api/models/tbmModel.js b/system1-factory/api/models/tbmModel.js
index 6100b4b..b459dfe 100644
--- a/system1-factory/api/models/tbmModel.js
+++ b/system1-factory/api/models/tbmModel.js
@@ -47,6 +47,11 @@ const TbmModel = {
u.username as created_by_username,
u.name as created_by_name,
COUNT(DISTINCT ta.worker_id) as team_member_count,
+ GROUP_CONCAT(DISTINCT w2.worker_name ORDER BY ta.assignment_id SEPARATOR ', ') as team_member_names,
+ -- 이동 수 (이 세션이 source 또는 dest인 이동 건수)
+ (SELECT COUNT(*) FROM tbm_transfers tf
+ WHERE (tf.source_session_id = s.session_id OR tf.dest_session_id = s.session_id)
+ AND tf.transfer_date = s.session_date) as transfer_count,
-- 첫 번째 팀원의 작업 정보 가져오기
first_ta.project_id,
first_ta.work_type_id,
@@ -58,8 +63,9 @@ const TbmModel = {
first_wp.workplace_name as work_location
FROM tbm_sessions s
LEFT JOIN workers w ON s.leader_id = w.worker_id
- LEFT JOIN users u ON s.created_by = u.user_id
+ LEFT JOIN sso_users u ON s.created_by = u.user_id
LEFT JOIN tbm_team_assignments ta ON s.session_id = ta.session_id
+ LEFT JOIN workers w2 ON ta.worker_id = w2.worker_id
-- 첫 번째 팀원 정보 (가장 먼저 등록된 작업)
LEFT JOIN (
SELECT * FROM tbm_team_assignments
@@ -110,7 +116,7 @@ const TbmModel = {
first_wc.category_name as workplace_category_name
FROM tbm_sessions s
LEFT JOIN workers w ON s.leader_id = w.worker_id
- LEFT JOIN users u ON s.created_by = u.user_id
+ LEFT JOIN sso_users u ON s.created_by = u.user_id
LEFT JOIN tbm_team_assignments ta ON s.session_id = ta.session_id
LEFT JOIN (
SELECT * FROM tbm_team_assignments
@@ -188,6 +194,107 @@ const TbmModel = {
}
},
+ /**
+ * TBM 세션 완료 처리 (근태 유형 포함)
+ * @param {number} sessionId - TBM 세션 ID
+ * @param {string} endTime - 종료 시간
+ * @param {Array} attendanceData - [{worker_id, attendance_type, attendance_hours}]
+ * @param {number} createdBy - 처리자 user_id
+ */
+ completeSessionWithAttendance: async (sessionId, endTime, attendanceData, createdBy, callback) => {
+ let conn;
+ try {
+ const db = await getDb();
+ conn = await db.getConnection();
+ await conn.beginTransaction();
+
+ // 1. 세션 정보 조회 (날짜 확인용)
+ const [sessionRows] = await conn.query(
+ 'SELECT session_date FROM tbm_sessions WHERE session_id = ?',
+ [sessionId]
+ );
+ if (sessionRows.length === 0) {
+ await conn.rollback();
+ conn.release();
+ return callback(null, { affectedRows: 0 });
+ }
+ const sessionDate = sessionRows[0].session_date;
+ // sessionDate를 YYYY-MM-DD 형식으로 변환
+ let reportDate;
+ if (sessionDate instanceof Date) {
+ reportDate = sessionDate.toISOString().split('T')[0];
+ } else if (typeof sessionDate === 'string') {
+ reportDate = sessionDate.split('T')[0];
+ } else {
+ reportDate = new Date(sessionDate).toISOString().split('T')[0];
+ }
+
+ // 2. 각 작업자의 근태 유형 업데이트
+ for (const item of attendanceData) {
+ await conn.query(
+ `UPDATE tbm_team_assignments
+ SET attendance_type = ?, attendance_hours = ?
+ WHERE session_id = ? AND worker_id = ?`,
+ [item.attendance_type, item.attendance_hours || null, sessionId, item.worker_id]
+ );
+ }
+
+ // 3. 연차(annual) 작업자 → 작업보고서 자동 생성 (project_id=13, 8h)
+ const annualWorkers = attendanceData.filter(a => a.attendance_type === 'annual');
+ for (const aw of annualWorkers) {
+ // 해당 작업자의 assignment_id 조회
+ const [assignRows] = await conn.query(
+ 'SELECT assignment_id FROM tbm_team_assignments WHERE session_id = ? AND worker_id = ?',
+ [sessionId, aw.worker_id]
+ );
+ if (assignRows.length > 0) {
+ // 이미 보고서가 있는지 확인
+ const [existingReport] = await conn.query(
+ 'SELECT id FROM daily_work_reports WHERE tbm_assignment_id = ?',
+ [assignRows[0].assignment_id]
+ );
+ if (existingReport.length === 0) {
+ await conn.query(
+ `INSERT INTO daily_work_reports
+ (report_date, worker_id, project_id, work_hours, work_status_id, created_by, tbm_assignment_id, created_at)
+ VALUES (?, ?, 13, 8, 1, ?, ?, NOW())`,
+ [reportDate, aw.worker_id, createdBy, assignRows[0].assignment_id]
+ );
+ }
+ }
+ }
+
+ // 4. 세션 완료 처리
+ await conn.query(
+ `UPDATE tbm_sessions
+ SET status = 'completed', end_time = ?, updated_at = NOW()
+ WHERE session_id = ?`,
+ [endTime, sessionId]
+ );
+
+ await conn.commit();
+ conn.release();
+
+ // 5. 연차 작업자 근태 동기화
+ for (const aw of annualWorkers) {
+ try {
+ const AttendanceModel = require('./attendanceModel');
+ await AttendanceModel.syncWithWorkReports(aw.worker_id, reportDate);
+ } catch (syncErr) {
+ console.error('근태 동기화 오류 (무시됨):', syncErr);
+ }
+ }
+
+ callback(null, { affectedRows: 1 });
+ } catch (err) {
+ if (conn) {
+ try { await conn.rollback(); } catch (e) {}
+ conn.release();
+ }
+ callback(err);
+ }
+ },
+
/**
* TBM 세션 삭제 (draft 상태만 가능)
*/
@@ -215,9 +322,9 @@ const TbmModel = {
const db = await getDb();
const sql = `
INSERT INTO tbm_team_assignments
- (session_id, worker_id, assigned_role, work_detail, is_present, absence_reason,
- project_id, work_type_id, task_id, workplace_category_id, workplace_id)
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+ (session_id, worker_id, split_seq, assigned_role, work_detail, is_present, absence_reason,
+ project_id, work_type_id, task_id, workplace_category_id, workplace_id, work_hours)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
assigned_role = VALUES(assigned_role),
work_detail = VALUES(work_detail),
@@ -227,12 +334,14 @@ const TbmModel = {
work_type_id = VALUES(work_type_id),
task_id = VALUES(task_id),
workplace_category_id = VALUES(workplace_category_id),
- workplace_id = VALUES(workplace_id)
+ workplace_id = VALUES(workplace_id),
+ work_hours = COALESCE(VALUES(work_hours), work_hours)
`;
const values = [
assignmentData.session_id,
assignmentData.worker_id,
+ assignmentData.split_seq || 0,
assignmentData.assigned_role,
assignmentData.work_detail,
assignmentData.is_present !== undefined ? assignmentData.is_present : true,
@@ -241,7 +350,8 @@ const TbmModel = {
assignmentData.work_type_id || null,
assignmentData.task_id || null,
assignmentData.workplace_category_id || null,
- assignmentData.workplace_id || null
+ assignmentData.workplace_id || null,
+ assignmentData.work_hours !== undefined ? assignmentData.work_hours : null
];
const [result] = await db.query(sql, values);
@@ -251,6 +361,42 @@ const TbmModel = {
}
},
+ /**
+ * 분할 항목 추가 (같은 세션+작업자에 split_seq 자동 증가)
+ */
+ addSplitAssignment: async (assignmentData, callback) => {
+ try {
+ const db = await getDb();
+ // 현재 최대 split_seq 조회
+ const [maxRows] = await db.query(
+ 'SELECT COALESCE(MAX(split_seq), -1) as max_seq FROM tbm_team_assignments WHERE session_id = ? AND worker_id = ?',
+ [assignmentData.session_id, assignmentData.worker_id]
+ );
+ const nextSeq = (maxRows[0].max_seq || 0) + 1;
+
+ const sql = `
+ INSERT INTO tbm_team_assignments
+ (session_id, worker_id, split_seq, work_hours, project_id, work_type_id,
+ task_id, workplace_category_id, workplace_id, is_present)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 1)
+ `;
+ const [result] = await db.query(sql, [
+ assignmentData.session_id,
+ assignmentData.worker_id,
+ nextSeq,
+ assignmentData.work_hours,
+ assignmentData.project_id || null,
+ assignmentData.work_type_id || null,
+ assignmentData.task_id || null,
+ assignmentData.workplace_category_id || null,
+ assignmentData.workplace_id || null
+ ]);
+ callback(null, { assignment_id: result.insertId, split_seq: nextSeq });
+ } catch (err) {
+ callback(err);
+ }
+ },
+
/**
* 팀 구성 일괄 추가 (작업자별 상세 정보 포함)
*/
@@ -420,7 +566,7 @@ const TbmModel = {
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
+ LEFT JOIN sso_users u ON sr.checked_by = u.user_id
WHERE sr.session_id = ?
ORDER BY sc.check_category, sc.display_order
`;
@@ -571,7 +717,7 @@ const TbmModel = {
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
+ LEFT JOIN sso_users u ON h.confirmed_by = u.user_id
WHERE h.handover_date = ?
ORDER BY h.handover_time DESC
`;
@@ -673,9 +819,13 @@ const TbmModel = {
const db = await getDb();
// WHERE 조건 동적 생성
+ // TBM 완료(근태 입력) 후에만 작업보고서 작성 가능
let whereClause = `
WHERE dwr.id IS NULL
- AND s.status = 'draft'
+ AND s.status = 'completed'
+ AND (ta.attendance_type IS NULL OR ta.attendance_type != 'annual')
+ AND ta.task_id IS NOT NULL
+ AND ta.workplace_id IS NOT NULL
`;
const params = [];
@@ -684,7 +834,10 @@ const TbmModel = {
whereClause = `
WHERE s.created_by = ?
AND dwr.id IS NULL
- AND s.status = 'draft'
+ AND s.status = 'completed'
+ AND (ta.attendance_type IS NULL OR ta.attendance_type != 'annual')
+ AND ta.task_id IS NOT NULL
+ AND ta.workplace_id IS NOT NULL
`;
params.push(userId);
}
@@ -699,6 +852,9 @@ const TbmModel = {
ta.task_id,
ta.workplace_category_id,
ta.workplace_id,
+ ta.attendance_type,
+ ta.attendance_hours,
+ ta.work_hours,
s.session_date,
s.status as session_status,
s.created_by,
diff --git a/system1-factory/api/models/tbmTransferModel.js b/system1-factory/api/models/tbmTransferModel.js
new file mode 100644
index 0000000..5935443
--- /dev/null
+++ b/system1-factory/api/models/tbmTransferModel.js
@@ -0,0 +1,294 @@
+// models/tbmTransferModel.js - TBM 작업자 이동 모델
+const { getDb } = require('../dbPool');
+
+const TbmTransferModel = {
+ /**
+ * 작업자 이동 실행 (보내기/빼오기)
+ * 트랜잭션: source work_hours 업데이트 + dest INSERT + 로그 INSERT
+ */
+ createTransfer: async (transferData, callback) => {
+ let conn;
+ try {
+ const db = await getDb();
+ conn = await db.getConnection();
+ await conn.beginTransaction();
+
+ const {
+ transfer_type, worker_id, source_session_id, dest_session_id,
+ hours, initiated_by, transfer_date,
+ project_id, work_type_id, task_id, workplace_category_id, workplace_id
+ } = transferData;
+
+ // 1. source 세션에서 해당 작업자의 work_hours 업데이트
+ const [sourceRows] = await conn.query(
+ 'SELECT assignment_id, work_hours FROM tbm_team_assignments WHERE session_id = ? AND worker_id = ?',
+ [source_session_id, worker_id]
+ );
+
+ if (sourceRows.length === 0) {
+ await conn.rollback();
+ conn.release();
+ return callback(null, { success: false, message: '원본 세션에서 해당 작업자를 찾을 수 없습니다.' });
+ }
+
+ const currentHours = sourceRows[0].work_hours === null ? 8 : parseFloat(sourceRows[0].work_hours);
+ const newSourceHours = currentHours - parseFloat(hours);
+
+ if (newSourceHours < 0) {
+ await conn.rollback();
+ conn.release();
+ return callback(null, { success: false, message: '이동 시간이 현재 배정 시간보다 큽니다.' });
+ }
+
+ await conn.query(
+ 'UPDATE tbm_team_assignments SET work_hours = ? WHERE session_id = ? AND worker_id = ?',
+ [newSourceHours, source_session_id, worker_id]
+ );
+
+ // 2. dest 세션에 작업자 INSERT (이미 있으면 work_hours 증가)
+ const [destRows] = await conn.query(
+ 'SELECT assignment_id, work_hours FROM tbm_team_assignments WHERE session_id = ? AND worker_id = ?',
+ [dest_session_id, worker_id]
+ );
+
+ if (destRows.length > 0) {
+ // 이미 있으면 시간만 추가
+ const existingHours = destRows[0].work_hours === null ? 8 : parseFloat(destRows[0].work_hours);
+ await conn.query(
+ 'UPDATE tbm_team_assignments SET work_hours = ? WHERE session_id = ? AND worker_id = ?',
+ [existingHours + parseFloat(hours), dest_session_id, worker_id]
+ );
+ } else {
+ // 새로 INSERT
+ await conn.query(
+ `INSERT INTO tbm_team_assignments
+ (session_id, worker_id, work_hours, project_id, work_type_id, task_id, workplace_category_id, workplace_id, is_present)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, 1)`,
+ [dest_session_id, worker_id, parseFloat(hours),
+ project_id || null, work_type_id || null, task_id || null,
+ workplace_category_id || null, workplace_id || null]
+ );
+ }
+
+ // 3. tbm_transfers에 로그 INSERT
+ const [logResult] = await conn.query(
+ `INSERT INTO tbm_transfers
+ (transfer_date, worker_id, source_session_id, dest_session_id, hours, transfer_type, initiated_by)
+ VALUES (?, ?, ?, ?, ?, ?, ?)`,
+ [transfer_date, worker_id, source_session_id, dest_session_id, parseFloat(hours), transfer_type, initiated_by]
+ );
+
+ // 4. 합계 시간 검증 (경고만, 차단 안함)
+ const [totalRows] = await conn.query(
+ `SELECT SUM(COALESCE(work_hours, 8)) as total_hours
+ FROM tbm_team_assignments
+ WHERE worker_id = ?
+ AND session_id IN (SELECT session_id FROM tbm_sessions WHERE session_date = ?)`,
+ [worker_id, transfer_date]
+ );
+ const totalHours = totalRows[0] ? parseFloat(totalRows[0].total_hours) : 0;
+
+ await conn.commit();
+ conn.release();
+
+ const result = {
+ success: true,
+ transfer_id: logResult.insertId,
+ total_hours: totalHours
+ };
+
+ if (totalHours > 8) {
+ result.warning = `해당 작업자의 당일 합계가 ${totalHours}h입니다 (8h 초과).`;
+ }
+
+ callback(null, result);
+ } catch (err) {
+ if (conn) {
+ try { await conn.rollback(); } catch (e) {}
+ conn.release();
+ }
+ callback(err);
+ }
+ },
+
+ /**
+ * 이동 취소 (원복)
+ */
+ cancelTransfer: async (transferId, callback) => {
+ let conn;
+ try {
+ const db = await getDb();
+ conn = await db.getConnection();
+ await conn.beginTransaction();
+
+ // 1. 이동 로그 조회
+ const [transfers] = await conn.query(
+ 'SELECT * FROM tbm_transfers WHERE transfer_id = ?',
+ [transferId]
+ );
+
+ if (transfers.length === 0) {
+ await conn.rollback();
+ conn.release();
+ return callback(null, { success: false, message: '이동 기록을 찾을 수 없습니다.' });
+ }
+
+ const t = transfers[0];
+
+ // 2. dest 세션에서 작업자 work_hours 감소 (또는 삭제)
+ const [destRows] = await conn.query(
+ 'SELECT assignment_id, work_hours FROM tbm_team_assignments WHERE session_id = ? AND worker_id = ?',
+ [t.dest_session_id, t.worker_id]
+ );
+
+ if (destRows.length > 0) {
+ const destHours = destRows[0].work_hours === null ? 8 : parseFloat(destRows[0].work_hours);
+ const newDestHours = destHours - parseFloat(t.hours);
+
+ if (newDestHours <= 0) {
+ // 삭제
+ await conn.query(
+ 'DELETE FROM tbm_team_assignments WHERE session_id = ? AND worker_id = ?',
+ [t.dest_session_id, t.worker_id]
+ );
+ } else {
+ await conn.query(
+ 'UPDATE tbm_team_assignments SET work_hours = ? WHERE session_id = ? AND worker_id = ?',
+ [newDestHours, t.dest_session_id, t.worker_id]
+ );
+ }
+ }
+
+ // 3. source 세션에서 작업자 work_hours 복원
+ const [sourceRows] = await conn.query(
+ 'SELECT assignment_id, work_hours FROM tbm_team_assignments WHERE session_id = ? AND worker_id = ?',
+ [t.source_session_id, t.worker_id]
+ );
+
+ if (sourceRows.length > 0) {
+ const sourceHours = sourceRows[0].work_hours === null ? 8 : parseFloat(sourceRows[0].work_hours);
+ const restoredHours = sourceHours + parseFloat(t.hours);
+ // 8이면 NULL로 복원 (종일)
+ await conn.query(
+ 'UPDATE tbm_team_assignments SET work_hours = ? WHERE session_id = ? AND worker_id = ?',
+ [restoredHours >= 8 ? null : restoredHours, t.source_session_id, t.worker_id]
+ );
+ }
+
+ // 4. 이동 로그 삭제
+ await conn.query('DELETE FROM tbm_transfers WHERE transfer_id = ?', [transferId]);
+
+ await conn.commit();
+ conn.release();
+
+ callback(null, { success: true });
+ } catch (err) {
+ if (conn) {
+ try { await conn.rollback(); } catch (e) {}
+ conn.release();
+ }
+ callback(err);
+ }
+ },
+
+ /**
+ * 당일 이동 내역 조회
+ */
+ getTransfersByDate: async (date, callback) => {
+ try {
+ const db = await getDb();
+ const sql = `
+ SELECT
+ t.*,
+ w.worker_name,
+ w.job_type,
+ sl.worker_name as source_leader_name,
+ dl.worker_name as dest_leader_name,
+ u.name as initiated_by_name
+ FROM tbm_transfers t
+ INNER JOIN workers w ON t.worker_id = w.worker_id
+ LEFT JOIN tbm_sessions ss ON t.source_session_id = ss.session_id
+ LEFT JOIN workers sl ON ss.leader_id = sl.worker_id
+ LEFT JOIN tbm_sessions ds ON t.dest_session_id = ds.session_id
+ LEFT JOIN workers dl ON ds.leader_id = dl.worker_id
+ LEFT JOIN sso_users u ON t.initiated_by = u.user_id
+ WHERE t.transfer_date = ?
+ ORDER BY t.created_at DESC
+ `;
+ const [rows] = await db.query(sql, [date]);
+ callback(null, rows);
+ } catch (err) {
+ callback(err);
+ }
+ },
+
+ /**
+ * 당일 전 작업자 배정 현황 조회
+ */
+ getWorkerAssignmentsByDate: async (date, callback) => {
+ try {
+ const db = await getDb();
+
+ // 1. 해당 날짜의 모든 배정 가져오기
+ const [assignments] = await db.query(`
+ SELECT
+ ta.worker_id,
+ ta.session_id,
+ ta.work_hours,
+ w.worker_name,
+ w.job_type,
+ s.leader_id,
+ lw.worker_name as leader_name,
+ s.status as session_status
+ FROM tbm_team_assignments ta
+ INNER JOIN tbm_sessions s ON ta.session_id = s.session_id
+ INNER JOIN workers w ON ta.worker_id = w.worker_id
+ LEFT JOIN workers lw ON s.leader_id = lw.worker_id
+ WHERE s.session_date = ?
+ ORDER BY w.worker_name
+ `, [date]);
+
+ // 2. 모든 작업자 가져오기 (배정 안 된 사람도 포함)
+ const [allWorkers] = await db.query(
+ "SELECT worker_id, worker_name, job_type FROM workers WHERE status = 'active' AND department = '생산' ORDER BY worker_name"
+ );
+
+ // 3. 작업자별 배정 현황 구성
+ const workerMap = {};
+ allWorkers.forEach(w => {
+ workerMap[w.worker_id] = {
+ worker_id: w.worker_id,
+ worker_name: w.worker_name,
+ job_type: w.job_type,
+ sessions: [],
+ total_hours: 0,
+ available: true
+ };
+ });
+
+ assignments.forEach(a => {
+ const hours = a.work_hours === null ? 8 : parseFloat(a.work_hours);
+ if (workerMap[a.worker_id]) {
+ workerMap[a.worker_id].sessions.push({
+ session_id: a.session_id,
+ leader_name: a.leader_name,
+ work_hours: hours,
+ session_status: a.session_status
+ });
+ workerMap[a.worker_id].total_hours += hours;
+ }
+ });
+
+ // available 판단
+ Object.values(workerMap).forEach(w => {
+ w.available = w.total_hours < 8;
+ });
+
+ callback(null, Object.values(workerMap));
+ } catch (err) {
+ callback(err);
+ }
+ }
+};
+
+module.exports = TbmTransferModel;
diff --git a/system1-factory/api/routes/pageAccessRoutes.js b/system1-factory/api/routes/pageAccessRoutes.js
index 9c46223..14779b4 100644
--- a/system1-factory/api/routes/pageAccessRoutes.js
+++ b/system1-factory/api/routes/pageAccessRoutes.js
@@ -3,6 +3,71 @@ const router = express.Router();
const { getDb } = require('../dbPool');
const { requireAuth, requireAdmin } = require('../middlewares/auth');
+// tkuser page_name → default_access 매핑 (permissionModel.js의 DEFAULT_PAGES와 동기화)
+const TKUSER_DEFAULT_ACCESS = {
+ 's1.dashboard': true,
+ 's1.work.tbm': true,
+ 's1.work.report_create': true,
+ 's1.work.analysis': false,
+ 's1.work.nonconformity': true,
+ 's1.factory.repair_management': false,
+ 's1.inspection.daily_patrol': false,
+ 's1.inspection.checkin': true,
+ 's1.inspection.work_status': false,
+ 's1.safety.visit_request': true,
+ 's1.safety.management': false,
+ 's1.safety.checklist_manage': false,
+ 's1.attendance.my_vacation_info': true,
+ 's1.attendance.monthly': true,
+ 's1.attendance.vacation_request': true,
+ 's1.attendance.vacation_management': false,
+ 's1.attendance.vacation_allocation': false,
+ 's1.attendance.annual_overview': false,
+ 's1.admin.workers': false,
+ 's1.admin.projects': false,
+ 's1.admin.tasks': false,
+ 's1.admin.workplaces': false,
+ 's1.admin.equipments': false,
+ 's1.admin.issue_categories': false,
+ 's1.admin.attendance_report': false,
+};
+
+// system1 page_key → tkuser page_name 매핑
+const PAGEKEY_TO_TKUSER = {
+ 'dashboard': 's1.dashboard',
+ 'work.tbm': 's1.work.tbm',
+ 'work.report-create': 's1.work.report_create',
+ 'work.report-view': 's1.work.report_create',
+ 'work.analysis': 's1.work.analysis',
+ 'work.visit-request': 's1.safety.visit_request',
+ 'work.issue-report': 's1.work.nonconformity',
+ 'work.issue-list': 's1.work.nonconformity',
+ 'work.issue-detail': 's1.work.nonconformity',
+ 'safety.issue_report': 's1.work.nonconformity',
+ 'safety.issue_list': 's1.work.nonconformity',
+ 'safety.issue_detail': 's1.work.nonconformity',
+ 'safety.checklist_manage': 's1.safety.checklist_manage',
+ 'admin.workers': 's1.admin.workers',
+ 'admin.projects': 's1.admin.projects',
+ 'admin.tasks': 's1.admin.tasks',
+ 'admin.workplaces': 's1.admin.workplaces',
+ 'admin.equipments': 's1.admin.equipments',
+ 'admin.codes': 's1.admin.tasks',
+ 'admin.safety-management': 's1.safety.management',
+ 'admin.safety-training-conduct': 's1.safety.management',
+ 'admin.attendance-report-comparison': 's1.admin.attendance_report',
+ 'admin.departments': 's1.admin.workers',
+ 'common.daily-attendance': 's1.inspection.checkin',
+ 'common.monthly-attendance': 's1.attendance.monthly',
+ 'common.vacation-request': 's1.attendance.vacation_request',
+ 'common.vacation-management': 's1.attendance.vacation_management',
+ 'common.annual-vacation-overview': 's1.attendance.annual_overview',
+ 'common.vacation-allocation': 's1.attendance.vacation_allocation',
+ 'inspection.daily_patrol': 's1.inspection.daily_patrol',
+ 'attendance.vacation_approval': 's1.attendance.vacation_management',
+ 'attendance.vacation_input': 's1.attendance.vacation_allocation',
+};
+
/**
* 모든 페이지 목록 조회
* GET /api/pages
@@ -29,25 +94,22 @@ router.get('/pages', requireAuth, async (req, res) => {
*/
router.get('/users/:userId/page-access', requireAuth, async (req, res) => {
try {
- const { userId } = req.params;
+ const ssoUserId = req.params.userId;
const db = await getDb();
- // 사용자의 역할 확인
- const [userRows] = await db.query(`
- SELECT u.user_id, u.username, u.role_id, r.name as role_name
- FROM users u
- LEFT JOIN roles r ON u.role_id = r.id
- WHERE u.user_id = ?
- `, [userId]);
-
- if (userRows.length === 0) {
+ // SSO 사용자 조회 (department_id 포함)
+ const [ssoRows] = await db.query(
+ 'SELECT user_id, username, name, role, department_id FROM sso_users WHERE user_id = ?',
+ [ssoUserId]
+ );
+ if (ssoRows.length === 0) {
return res.status(404).json({ success: false, error: '사용자를 찾을 수 없습니다.' });
}
+ const ssoUser = ssoRows[0];
- const user = userRows[0];
-
- // Admin/System Admin인 경우 모든 페이지 접근 가능
- if (user.role_name === 'Admin' || user.role_name === 'System Admin') {
+ // SSO role로 Admin 체크
+ const ssoRole = (ssoUser.role || '').toLowerCase();
+ if (ssoRole === 'admin' || ssoRole === 'system') {
const [allPages] = await db.query(`
SELECT id, page_key, page_name, page_path, category, is_admin_only
FROM pages
@@ -62,32 +124,66 @@ router.get('/users/:userId/page-access', requireAuth, async (req, res) => {
category: page.category,
is_admin_only: page.is_admin_only,
can_access: true,
- is_default: true // Admin은 기본적으로 모든 권한 보유
+ is_default: true
}));
- return res.json({ success: true, data: { user, pageAccess } });
+ return res.json({ success: true, data: { user: ssoUser, pageAccess } });
}
- // 일반 사용자의 페이지 접근 권한 조회
- const [pageAccess] = await db.query(`
- SELECT
- p.id as page_id,
- p.page_key,
- p.page_name,
- p.page_path,
- p.category,
- p.is_admin_only,
- COALESCE(upa.can_access, 0) as can_access,
- upa.granted_at,
- u2.username as granted_by_username
- FROM pages p
- LEFT JOIN user_page_access upa ON p.id = upa.page_id AND upa.user_id = ?
- LEFT JOIN users u2 ON upa.granted_by = u2.user_id
- WHERE p.is_admin_only = 0
- ORDER BY p.display_order, p.page_name
- `, [userId]);
+ // 일반 사용자: tkuser 권한 테이블에서 조회
+ // 1) 개인 권한 (user_page_permissions)
+ const [userPerms] = await db.query(
+ 'SELECT page_name, can_access FROM user_page_permissions WHERE user_id = ?',
+ [ssoUserId]
+ );
+ const userPermMap = {};
+ userPerms.forEach(p => { userPermMap[p.page_name] = !!p.can_access; });
- res.json({ success: true, data: { user, pageAccess } });
+ // 2) 부서 권한 (department_page_permissions)
+ const deptPermMap = {};
+ if (ssoUser.department_id) {
+ const [deptPerms] = await db.query(
+ 'SELECT page_name, can_access FROM department_page_permissions WHERE department_id = ?',
+ [ssoUser.department_id]
+ );
+ deptPerms.forEach(p => { deptPermMap[p.page_name] = !!p.can_access; });
+ }
+
+ // 3) 페이지 목록 조회 + 권한 매핑
+ const [pages] = await db.query(`
+ SELECT id, page_key, page_name, page_path, category, is_admin_only
+ FROM pages
+ WHERE is_admin_only = 0
+ ORDER BY display_order, page_name
+ `);
+
+ const pageAccess = pages.map(page => {
+ const tkuserKey = PAGEKEY_TO_TKUSER[page.page_key];
+ let canAccess = false;
+
+ if (tkuserKey) {
+ // 우선순위: 개인 권한 > 부서 권한 > default_access
+ if (tkuserKey in userPermMap) {
+ canAccess = userPermMap[tkuserKey];
+ } else if (tkuserKey in deptPermMap) {
+ canAccess = deptPermMap[tkuserKey];
+ } else if (tkuserKey in TKUSER_DEFAULT_ACCESS) {
+ canAccess = TKUSER_DEFAULT_ACCESS[tkuserKey];
+ }
+ }
+
+ return {
+ page_id: page.id,
+ page_key: page.page_key,
+ page_name: page.page_name,
+ page_path: page.page_path,
+ category: page.category,
+ is_admin_only: page.is_admin_only,
+ can_access: canAccess ? 1 : 0
+ };
+ });
+
+ res.json({ success: true, data: { user: ssoUser, pageAccess } });
} catch (error) {
console.error('페이지 접근 권한 조회 오류:', error);
res.status(500).json({ success: false, error: '페이지 접근 권한을 불러오는데 실패했습니다.' });
diff --git a/system1-factory/api/routes/tbmRoutes.js b/system1-factory/api/routes/tbmRoutes.js
index aa7e8eb..0db3eb2 100644
--- a/system1-factory/api/routes/tbmRoutes.js
+++ b/system1-factory/api/routes/tbmRoutes.js
@@ -12,6 +12,9 @@ router.post('/sessions', requireAuth, TbmController.createSession);
// 작업보고서가 작성되지 않은 TBM 팀 배정 조회 (구체적인 경로이므로 먼저 정의)
router.get('/sessions/incomplete-reports', requireAuth, TbmController.getIncompleteWorkReports);
+// 당일 전 작업자 배정 현황 (더 구체적인 경로이므로 먼저 정의)
+router.get('/sessions/date/:date/assignments', requireAuth, TbmController.getWorkerAssignmentsByDate);
+
// 특정 날짜의 TBM 세션 목록 조회
router.get('/sessions/date/:date', requireAuth, TbmController.getSessionsByDate);
@@ -32,6 +35,9 @@ router.delete('/sessions/:sessionId', requireAuth, TbmController.deleteSession);
// 팀원 추가 (단일)
router.post('/sessions/:sessionId/team', requireAuth, TbmController.addTeamMember);
+// 분할 항목 추가 (같은 작업자의 추가 배정)
+router.post('/sessions/:sessionId/team/split', requireAuth, TbmController.addSplitAssignment);
+
// 팀 구성 일괄 추가
router.post('/sessions/:sessionId/team/batch', requireAuth, TbmController.addTeamMembers);
@@ -81,6 +87,17 @@ router.get('/sessions/:sessionId/weather', requireAuth, TbmController.getSession
// 세션 날씨 정보 저장
router.post('/sessions/:sessionId/weather', requireAuth, TbmController.saveSessionWeather);
+// ==================== 작업자 이동 관련 ====================
+
+// 이동 실행 (보내기/빼오기)
+router.post('/transfers', requireAuth, TbmController.createTransfer);
+
+// 당일 이동 내역 조회
+router.get('/transfers/date/:date', requireAuth, TbmController.getTransfersByDate);
+
+// 이동 취소 (원복)
+router.delete('/transfers/:transferId', requireAuth, TbmController.cancelTransfer);
+
// ==================== 작업 인계 관련 ====================
// 작업 인계 생성
diff --git a/system1-factory/api/routes/userRoutes.js b/system1-factory/api/routes/userRoutes.js
index 9de69f9..610fefe 100644
--- a/system1-factory/api/routes/userRoutes.js
+++ b/system1-factory/api/routes/userRoutes.js
@@ -113,23 +113,9 @@ router.get('/me/monthly-stats', async (req, res) => {
}
});
-// ========== 자신의 페이지 권한 조회 (Admin 불필요) ==========
-// 📄 사용자 페이지 접근 권한 조회 (자신 또는 Admin)
-router.get('/:id/page-access', (req, res, next) => {
- const requestedId = parseInt(req.params.id);
- const currentUserId = req.user?.user_id;
- const userRole = req.user?.role?.toLowerCase();
-
- // 자신의 권한 조회이거나 Admin인 경우 허용
- if (requestedId === currentUserId || userRole === 'admin' || userRole === 'system admin') {
- return userController.getUserPageAccess(req, res, next);
- }
-
- return res.status(403).json({
- success: false,
- message: '자신의 페이지 권한만 조회할 수 있습니다'
- });
-});
+// ========== 페이지 권한 조회는 pageAccessRoutes.js에서 처리 ==========
+// GET /:id/page-access → /api/users/:userId/page-access (pageAccessRoutes.js)
+// tkuser의 user_page_permissions 테이블을 조회하는 통합 핸들러 사용
// ========== 관리자 전용 API ==========
/**
diff --git a/system1-factory/web/components/mobile-nav.html b/system1-factory/web/components/mobile-nav.html
index a525486..7d72119 100644
--- a/system1-factory/web/components/mobile-nav.html
+++ b/system1-factory/web/components/mobile-nav.html
@@ -8,14 +8,14 @@
홈
-
+
TBM
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
📝
+
수동으로 작업보고서를 추가하세요
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+