sso_users.user_id를 단일 식별자로 통합. JWT에서 worker_id 제거, department_id/is_production 추가. 백엔드 15개 모델, 11개 컨트롤러, 4개 서비스, 7개 라우트, 프론트엔드 32+ JS/11+ HTML 변환. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
272 lines
9.0 KiB
JavaScript
272 lines
9.0 KiB
JavaScript
// models/tbmTransferModel.js - TBM 작업자 이동 모델
|
|
const { getDb } = require('../dbPool');
|
|
|
|
const TbmTransferModel = {
|
|
/**
|
|
* 작업자 이동 실행 (보내기/빼오기)
|
|
* 트랜잭션: source work_hours 업데이트 + dest INSERT + 로그 INSERT
|
|
*/
|
|
async createTransfer(transferData) {
|
|
const db = await getDb();
|
|
const conn = await db.getConnection();
|
|
try {
|
|
await conn.beginTransaction();
|
|
|
|
const {
|
|
transfer_type, user_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 user_id = ?',
|
|
[source_session_id, user_id]
|
|
);
|
|
|
|
if (sourceRows.length === 0) {
|
|
await conn.rollback();
|
|
return { 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();
|
|
return { success: false, message: '이동 시간이 현재 배정 시간보다 큽니다.' };
|
|
}
|
|
|
|
await conn.query(
|
|
'UPDATE tbm_team_assignments SET work_hours = ? WHERE session_id = ? AND user_id = ?',
|
|
[newSourceHours, source_session_id, user_id]
|
|
);
|
|
|
|
// 2. dest 세션에 작업자 INSERT (이미 있으면 work_hours 증가)
|
|
const [destRows] = await conn.query(
|
|
'SELECT assignment_id, work_hours FROM tbm_team_assignments WHERE session_id = ? AND user_id = ?',
|
|
[dest_session_id, user_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 user_id = ?',
|
|
[existingHours + parseFloat(hours), dest_session_id, user_id]
|
|
);
|
|
} else {
|
|
await conn.query(
|
|
`INSERT INTO tbm_team_assignments
|
|
(session_id, user_id, work_hours, project_id, work_type_id, task_id, workplace_category_id, workplace_id, is_present)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 1)`,
|
|
[dest_session_id, user_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, user_id, source_session_id, dest_session_id, hours, transfer_type, initiated_by)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
[transfer_date, user_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 user_id = ?
|
|
AND session_id IN (SELECT session_id FROM tbm_sessions WHERE session_date = ?)`,
|
|
[user_id, transfer_date]
|
|
);
|
|
const totalHours = totalRows[0] ? parseFloat(totalRows[0].total_hours) : 0;
|
|
|
|
await conn.commit();
|
|
|
|
const result = {
|
|
success: true,
|
|
transfer_id: logResult.insertId,
|
|
total_hours: totalHours
|
|
};
|
|
|
|
if (totalHours > 8) {
|
|
result.warning = `해당 작업자의 당일 합계가 ${totalHours}h입니다 (8h 초과).`;
|
|
}
|
|
|
|
return result;
|
|
} catch (err) {
|
|
try { await conn.rollback(); } catch (e) {}
|
|
throw err;
|
|
} finally {
|
|
conn.release();
|
|
}
|
|
},
|
|
|
|
/**
|
|
* 이동 취소 (원복)
|
|
*/
|
|
async cancelTransfer(transferId) {
|
|
const db = await getDb();
|
|
const conn = await db.getConnection();
|
|
try {
|
|
await conn.beginTransaction();
|
|
|
|
// 1. 이동 로그 조회
|
|
const [transfers] = await conn.query(
|
|
'SELECT * FROM tbm_transfers WHERE transfer_id = ?',
|
|
[transferId]
|
|
);
|
|
|
|
if (transfers.length === 0) {
|
|
await conn.rollback();
|
|
return { 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 user_id = ?',
|
|
[t.dest_session_id, t.user_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 user_id = ?',
|
|
[t.dest_session_id, t.user_id]
|
|
);
|
|
} else {
|
|
await conn.query(
|
|
'UPDATE tbm_team_assignments SET work_hours = ? WHERE session_id = ? AND user_id = ?',
|
|
[newDestHours, t.dest_session_id, t.user_id]
|
|
);
|
|
}
|
|
}
|
|
|
|
// 3. source 세션에서 작업자 work_hours 복원
|
|
const [sourceRows] = await conn.query(
|
|
'SELECT assignment_id, work_hours FROM tbm_team_assignments WHERE session_id = ? AND user_id = ?',
|
|
[t.source_session_id, t.user_id]
|
|
);
|
|
|
|
if (sourceRows.length > 0) {
|
|
const sourceHours = sourceRows[0].work_hours === null ? 8 : parseFloat(sourceRows[0].work_hours);
|
|
const restoredHours = sourceHours + parseFloat(t.hours);
|
|
await conn.query(
|
|
'UPDATE tbm_team_assignments SET work_hours = ? WHERE session_id = ? AND user_id = ?',
|
|
[restoredHours >= 8 ? null : restoredHours, t.source_session_id, t.user_id]
|
|
);
|
|
}
|
|
|
|
// 4. 이동 로그 삭제
|
|
await conn.query('DELETE FROM tbm_transfers WHERE transfer_id = ?', [transferId]);
|
|
|
|
await conn.commit();
|
|
return { success: true };
|
|
} catch (err) {
|
|
try { await conn.rollback(); } catch (e) {}
|
|
throw err;
|
|
} finally {
|
|
conn.release();
|
|
}
|
|
},
|
|
|
|
/**
|
|
* 당일 이동 내역 조회
|
|
*/
|
|
async getTransfersByDate(date) {
|
|
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.user_id = w.user_id
|
|
LEFT JOIN tbm_sessions ss ON t.source_session_id = ss.session_id
|
|
LEFT JOIN workers sl ON ss.leader_id = sl.user_id
|
|
LEFT JOIN tbm_sessions ds ON t.dest_session_id = ds.session_id
|
|
LEFT JOIN workers dl ON ds.leader_id = dl.user_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]);
|
|
return rows;
|
|
},
|
|
|
|
/**
|
|
* 당일 전 작업자 배정 현황 조회
|
|
*/
|
|
async getWorkerAssignmentsByDate(date) {
|
|
const db = await getDb();
|
|
|
|
// 1. 해당 날짜의 모든 배정 가져오기
|
|
const [assignments] = await db.query(`
|
|
SELECT
|
|
ta.user_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.user_id = w.user_id
|
|
LEFT JOIN workers lw ON s.leader_id = lw.user_id
|
|
WHERE s.session_date = ?
|
|
ORDER BY w.worker_name
|
|
`, [date]);
|
|
|
|
// 2. 모든 작업자 가져오기 (배정 안 된 사람도 포함)
|
|
const [allWorkers] = await db.query(
|
|
"SELECT user_id, worker_name, job_type FROM workers WHERE status = 'active' AND department = '생산' ORDER BY worker_name"
|
|
);
|
|
|
|
// 3. 작업자별 배정 현황 구성
|
|
const workerMap = {};
|
|
allWorkers.forEach(w => {
|
|
workerMap[w.user_id] = {
|
|
user_id: w.user_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.user_id]) {
|
|
workerMap[a.user_id].sessions.push({
|
|
session_id: a.session_id,
|
|
leader_name: a.leader_name,
|
|
work_hours: hours,
|
|
session_status: a.session_status
|
|
});
|
|
workerMap[a.user_id].total_hours += hours;
|
|
}
|
|
});
|
|
|
|
Object.values(workerMap).forEach(w => {
|
|
w.available = w.total_hours < 8;
|
|
});
|
|
|
|
return Object.values(workerMap);
|
|
}
|
|
};
|
|
|
|
module.exports = TbmTransferModel;
|