feat: TBM 모바일 시스템 + 작업 분할/이동 + 권한 통합
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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); // 부서 관리
|
||||
|
||||
@@ -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 팀 배정 조회
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
`);
|
||||
};
|
||||
@@ -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
|
||||
`);
|
||||
};
|
||||
@@ -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`);
|
||||
};
|
||||
@@ -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
|
||||
`);
|
||||
});
|
||||
};
|
||||
@@ -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,
|
||||
|
||||
294
system1-factory/api/models/tbmTransferModel.js
Normal file
294
system1-factory/api/models/tbmTransferModel.js
Normal file
@@ -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;
|
||||
@@ -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: '페이지 접근 권한을 불러오는데 실패했습니다.' });
|
||||
|
||||
@@ -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);
|
||||
|
||||
// ==================== 작업 인계 관련 ====================
|
||||
|
||||
// 작업 인계 생성
|
||||
|
||||
@@ -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 ==========
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user