refactor: worker_id → user_id 전체 마이그레이션 (Phase 1-4)

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>
This commit is contained in:
Hyungi Ahn
2026-03-05 13:13:10 +09:00
parent 2197cdb3d5
commit abd7564e6b
90 changed files with 1790 additions and 925 deletions

View File

@@ -21,7 +21,9 @@ function createTokenPayload(user) {
id: user.user_id, id: user.user_id,
username: user.username, username: user.username,
name: user.name, name: user.name,
worker_id: user.worker_id || null, department_id: user.department_id || null,
department_name: user.department_name || user.department || null,
is_production: user.is_production || false,
department: user.department, department: user.department,
role: user.role, role: user.role,
access_level: user.role, access_level: user.role,
@@ -78,8 +80,10 @@ async function login(req, res, next) {
username: user.username, username: user.username,
name: user.name, name: user.name,
department: user.department, department: user.department,
department_id: user.department_id || null,
department_name: user.department_name || user.department || null,
is_production: user.is_production || false,
role: user.role, role: user.role,
worker_id: user.worker_id || null,
system_access: payload.system_access system_access: payload.system_access
} }
}); });
@@ -154,8 +158,10 @@ async function validate(req, res, next) {
username: user.username, username: user.username,
name: user.name, name: user.name,
department: user.department, department: user.department,
department_id: user.department_id || null,
department_name: user.department_name || user.department || null,
is_production: user.is_production || false,
role: user.role, role: user.role,
worker_id: user.worker_id || null,
system_access: { system_access: {
system1: user.system1_access, system1: user.system1_access,
system2: user.system2_access, system2: user.system2_access,

View File

@@ -84,7 +84,10 @@ async function hashPassword(password) {
async function findByUsername(username) { async function findByUsername(username) {
const db = getPool(); const db = getPool();
const [rows] = await db.query( const [rows] = await db.query(
'SELECT * FROM sso_users WHERE username = ? AND is_active = TRUE', `SELECT s.*, d.department_id, d.department_name, d.is_production
FROM sso_users s
LEFT JOIN departments d ON s.department_id = d.department_id
WHERE s.username = ? AND s.is_active = TRUE`,
[username] [username]
); );
return rows[0] || null; return rows[0] || null;
@@ -93,7 +96,10 @@ async function findByUsername(username) {
async function findById(userId) { async function findById(userId) {
const db = getPool(); const db = getPool();
const [rows] = await db.query( const [rows] = await db.query(
'SELECT * FROM sso_users WHERE user_id = ?', `SELECT s.*, d.department_id, d.department_name, d.is_production
FROM sso_users s
LEFT JOIN departments d ON s.department_id = d.department_id
WHERE s.user_id = ?`,
[userId] [userId]
); );
return rows[0] || null; return rows[0] || null;

View File

@@ -154,10 +154,10 @@ const swaggerDefinition = {
example: 'admin', example: 'admin',
description: '접근 권한 레벨' description: '접근 권한 레벨'
}, },
worker_id: { user_id: {
type: 'integer', type: 'integer',
example: 1, example: 1,
description: '연결된 작업자 ID' description: '연결된 사용자 ID (sso_users)'
}, },
is_active: { is_active: {
type: 'boolean', type: 'boolean',
@@ -186,10 +186,10 @@ const swaggerDefinition = {
Worker: { Worker: {
type: 'object', type: 'object',
properties: { properties: {
worker_id: { user_id: {
type: 'integer', type: 'integer',
example: 1, example: 1,
description: '작업자 ID' description: '사용자 ID (sso_users)'
}, },
worker_name: { worker_name: {
type: 'string', type: 'string',
@@ -347,10 +347,10 @@ const swaggerDefinition = {
example: '2024-01-01', example: '2024-01-01',
description: '작업 날짜' description: '작업 날짜'
}, },
worker_id: { user_id: {
type: 'integer', type: 'integer',
example: 1, example: 1,
description: '작업자 ID' description: '사용자 ID (sso_users)'
}, },
project_id: { project_id: {
type: 'integer', type: 'integer',

View File

@@ -28,8 +28,8 @@ const getDailyAttendanceStatus = asyncHandler(async (req, res) => {
* 일일 근태 기록 조회 * 일일 근태 기록 조회
*/ */
const getDailyAttendanceRecords = asyncHandler(async (req, res) => { const getDailyAttendanceRecords = asyncHandler(async (req, res) => {
const { date, worker_id } = req.query; const { date, user_id } = req.query;
const data = await attendanceService.getDailyAttendanceRecordsService(date, worker_id); const data = await attendanceService.getDailyAttendanceRecordsService(date, user_id);
res.json({ res.json({
success: true, success: true,
@@ -42,8 +42,8 @@ const getDailyAttendanceRecords = asyncHandler(async (req, res) => {
* 기간별 근태 기록 조회 (월별 조회용) * 기간별 근태 기록 조회 (월별 조회용)
*/ */
const getAttendanceRecordsByRange = asyncHandler(async (req, res) => { const getAttendanceRecordsByRange = asyncHandler(async (req, res) => {
const { start_date, end_date, worker_id } = req.query; const { start_date, end_date, user_id } = req.query;
const data = await attendanceService.getAttendanceRecordsByRangeService(start_date, end_date, worker_id); const data = await attendanceService.getAttendanceRecordsByRangeService(start_date, end_date, user_id);
res.json({ res.json({
success: true, success: true,
@@ -76,7 +76,7 @@ const upsertAttendanceRecord = asyncHandler(async (req, res) => {
const processVacation = asyncHandler(async (req, res) => { const processVacation = asyncHandler(async (req, res) => {
const vacationData = { const vacationData = {
record_date: req.body.date, record_date: req.body.date,
worker_id: req.body.worker_id, user_id: req.body.user_id,
vacation_type_id: req.body.vacation_type, vacation_type_id: req.body.vacation_type,
created_by: req.user?.user_id || req.user?.id created_by: req.user?.user_id || req.user?.id
}; };
@@ -96,7 +96,7 @@ const processVacation = asyncHandler(async (req, res) => {
const approveOvertime = asyncHandler(async (req, res) => { const approveOvertime = asyncHandler(async (req, res) => {
const overtimeData = { const overtimeData = {
record_date: req.body.date, record_date: req.body.date,
worker_id: req.body.worker_id, user_id: req.body.user_id,
overtime_approved: true, overtime_approved: true,
approved_by: req.user?.user_id || req.user?.id approved_by: req.user?.user_id || req.user?.id
}; };
@@ -140,8 +140,8 @@ const getVacationTypes = asyncHandler(async (req, res) => {
* 작업자 휴가 잔여 조회 * 작업자 휴가 잔여 조회
*/ */
const getWorkerVacationBalance = asyncHandler(async (req, res) => { const getWorkerVacationBalance = asyncHandler(async (req, res) => {
const { worker_id } = req.params; const { user_id } = req.params;
const data = await attendanceService.getWorkerVacationBalanceService(parseInt(worker_id)); const data = await attendanceService.getWorkerVacationBalanceService(parseInt(user_id));
res.json({ res.json({
success: true, success: true,
@@ -154,11 +154,11 @@ const getWorkerVacationBalance = asyncHandler(async (req, res) => {
* 월별 근태 통계 * 월별 근태 통계
*/ */
const getMonthlyAttendanceStats = asyncHandler(async (req, res) => { const getMonthlyAttendanceStats = asyncHandler(async (req, res) => {
const { year, month, worker_id } = req.query; const { year, month, user_id } = req.query;
const data = await attendanceService.getMonthlyAttendanceStatsService( const data = await attendanceService.getMonthlyAttendanceStatsService(
parseInt(year), parseInt(year),
parseInt(month), parseInt(month),
worker_id ? parseInt(worker_id) : null user_id ? parseInt(user_id) : null
); );
res.json({ res.json({
@@ -186,7 +186,7 @@ const getCheckinList = asyncHandler(async (req, res) => {
* 출근 체크 저장 (일괄 처리) * 출근 체크 저장 (일괄 처리)
*/ */
const saveCheckins = asyncHandler(async (req, res) => { const saveCheckins = asyncHandler(async (req, res) => {
const { date, checkins } = req.body; // checkins: [{worker_id, is_present}, ...] const { date, checkins } = req.body; // checkins: [{user_id, is_present}, ...]
const result = await attendanceService.saveCheckinsService(date, checkins); const result = await attendanceService.saveCheckinsService(date, checkins);
res.json({ res.json({

View File

@@ -14,10 +14,10 @@ const { asyncHandler } = require('../middlewares/errorHandler');
* 일일 이슈 보고서 생성 * 일일 이슈 보고서 생성
*/ */
const createDailyIssueReport = asyncHandler(async (req, res) => { const createDailyIssueReport = asyncHandler(async (req, res) => {
// 프론트엔드에서 worker_ids 또는 worker_id로 보낼 수 있음 // 프론트엔드에서 user_ids 또는 user_id로 보낼 수 있음
const issueData = { const issueData = {
...req.body, ...req.body,
worker_ids: req.body.worker_ids || req.body.worker_id user_ids: req.body.user_ids || req.body.user_id
}; };
const result = await dailyIssueReportService.createDailyIssueReportService(issueData); const result = await dailyIssueReportService.createDailyIssueReportService(issueData);

View File

@@ -36,19 +36,19 @@ const createDailyWorkReport = asyncHandler(async (req, res) => {
* 기여자별 요약 조회 * 기여자별 요약 조회
*/ */
const getContributorsSummary = asyncHandler(async (req, res) => { const getContributorsSummary = asyncHandler(async (req, res) => {
const { date, worker_id } = req.query; const { date, user_id } = req.query;
if (!date || !worker_id) { if (!date || !user_id) {
return res.status(400).json({ error: 'date와 worker_id가 필요합니다.' }); return res.status(400).json({ error: 'date와 user_id가 필요합니다.' });
} }
const data = await dailyWorkReportModel.getContributorsByDate(date, worker_id); const data = await dailyWorkReportModel.getContributorsByDate(date, user_id);
const totalHours = data.reduce((sum, contributor) => sum + parseFloat(contributor.total_hours || 0), 0); const totalHours = data.reduce((sum, contributor) => sum + parseFloat(contributor.total_hours || 0), 0);
const result = { const result = {
date, date,
worker_id, user_id,
contributors: data, contributors: data,
total_contributors: data.length, total_contributors: data.length,
grand_total_hours: totalHours grand_total_hours: totalHours
@@ -61,13 +61,13 @@ const getContributorsSummary = asyncHandler(async (req, res) => {
* 개인 누적 현황 조회 * 개인 누적 현황 조회
*/ */
const getMyAccumulatedData = async (req, res) => { const getMyAccumulatedData = async (req, res) => {
const { date, worker_id } = req.query; const { date, user_id } = req.query;
const created_by = req.user?.user_id || req.user?.id; const created_by = req.user?.user_id || req.user?.id;
if (!date || !worker_id) { if (!date || !user_id) {
return res.status(400).json({ return res.status(400).json({
error: 'date와 worker_id가 필요합니다.', error: 'date와 user_id가 필요합니다.',
example: 'date=2024-06-16&worker_id=1' example: 'date=2024-06-16&user_id=1'
}); });
} }
@@ -78,11 +78,11 @@ const getMyAccumulatedData = async (req, res) => {
} }
try { try {
const data = await dailyWorkReportModel.getMyAccumulatedHours(date, worker_id, created_by); const data = await dailyWorkReportModel.getMyAccumulatedHours(date, user_id, created_by);
res.json({ res.json({
date, date,
worker_id, user_id,
created_by, created_by,
my_data: data, my_data: data,
timestamp: new Date().toISOString() timestamp: new Date().toISOString()
@@ -187,14 +187,14 @@ const getDailyWorkReportsByDate = async (req, res) => {
* 작업보고서 검색 (페이지네이션 포함) * 작업보고서 검색 (페이지네이션 포함)
*/ */
const searchWorkReports = async (req, res) => { const searchWorkReports = async (req, res) => {
const { start_date, end_date, worker_id, project_id, work_status_id, page = 1, limit = 20 } = req.query; const { start_date, end_date, user_id, project_id, work_status_id, page = 1, limit = 20 } = req.query;
const created_by = req.user?.user_id || req.user?.id; const created_by = req.user?.user_id || req.user?.id;
if (!start_date || !end_date) { if (!start_date || !end_date) {
return res.status(400).json({ return res.status(400).json({
error: 'start_date와 end_date가 필요합니다.', error: 'start_date와 end_date가 필요합니다.',
example: 'start_date=2024-01-01&end_date=2024-01-31', example: 'start_date=2024-01-01&end_date=2024-01-31',
optional: ['worker_id', 'project_id', 'work_status_id', 'page', 'limit'] optional: ['user_id', 'project_id', 'work_status_id', 'page', 'limit']
}); });
} }
@@ -207,7 +207,7 @@ const searchWorkReports = async (req, res) => {
const searchParams = { const searchParams = {
start_date, start_date,
end_date, end_date,
worker_id: worker_id ? parseInt(worker_id) : null, user_id: user_id ? parseInt(user_id) : null,
project_id: project_id ? parseInt(project_id) : null, project_id: project_id ? parseInt(project_id) : null,
work_status_id: work_status_id ? parseInt(work_status_id) : null, work_status_id: work_status_id ? parseInt(work_status_id) : null,
created_by, created_by,
@@ -377,7 +377,7 @@ const removeDailyWorkReport = async (req, res) => {
* 작업자의 특정 날짜 전체 삭제 * 작업자의 특정 날짜 전체 삭제
*/ */
const removeDailyWorkReportByDateAndWorker = async (req, res) => { const removeDailyWorkReportByDateAndWorker = async (req, res) => {
const { date, worker_id } = req.params; const { date, user_id } = req.params;
const deleted_by = req.user?.user_id || req.user?.id; const deleted_by = req.user?.user_id || req.user?.id;
const access_level = req.user?.access_level || req.user?.role; const access_level = req.user?.access_level || req.user?.role;
@@ -397,20 +397,20 @@ const removeDailyWorkReportByDateAndWorker = async (req, res) => {
} }
try { try {
const affectedRows = await dailyWorkReportModel.removeByDateAndWorker(date, worker_id, deleted_by); const affectedRows = await dailyWorkReportModel.removeByDateAndWorker(date, user_id, deleted_by);
if (affectedRows === 0) { if (affectedRows === 0) {
return res.status(404).json({ return res.status(404).json({
error: '삭제할 작업보고서를 찾을 수 없습니다.', error: '삭제할 작업보고서를 찾을 수 없습니다.',
date: date, date: date,
worker_id: worker_id user_id: user_id
}); });
} }
res.json({ res.json({
message: `${date} 날짜의 작업자 ${worker_id} 작업보고서 ${affectedRows}개가 삭제되었습니다.`, message: `${date} 날짜의 작업자 ${user_id} 작업보고서 ${affectedRows}개가 삭제되었습니다.`,
date, date,
worker_id, user_id,
affected_rows: affectedRows, affected_rows: affectedRows,
deleted_by, deleted_by,
timestamp: new Date().toISOString() timestamp: new Date().toISOString()
@@ -642,21 +642,21 @@ const deleteErrorType = asyncHandler(async (req, res) => {
* 누적 현황 조회 * 누적 현황 조회
*/ */
const getAccumulatedReports = async (req, res) => { const getAccumulatedReports = async (req, res) => {
const { date, worker_id } = req.query; const { date, user_id } = req.query;
if (!date || !worker_id) { if (!date || !user_id) {
return res.status(400).json({ return res.status(400).json({
error: 'date와 worker_id가 필요합니다.', error: 'date와 user_id가 필요합니다.',
example: 'date=2024-06-16&worker_id=1' example: 'date=2024-06-16&user_id=1'
}); });
} }
try { try {
const data = await dailyWorkReportModel.getAccumulatedReportsByDate(date, worker_id); const data = await dailyWorkReportModel.getAccumulatedReportsByDate(date, user_id);
res.json({ res.json({
date, date,
worker_id, user_id,
total_entries: data.length, total_entries: data.length,
accumulated_data: data, accumulated_data: data,
timestamp: new Date().toISOString() timestamp: new Date().toISOString()
@@ -678,7 +678,7 @@ const createFromTbm = async (req, res) => {
const { const {
tbm_assignment_id, tbm_assignment_id,
tbm_session_id, tbm_session_id,
worker_id, user_id,
project_id, project_id,
work_type_id, work_type_id,
report_date, report_date,
@@ -691,10 +691,10 @@ const createFromTbm = async (req, res) => {
} = req.body; } = req.body;
// 필수 필드 검증 // 필수 필드 검증
if (!tbm_assignment_id || !tbm_session_id || !worker_id || !report_date || !total_hours) { if (!tbm_assignment_id || !tbm_session_id || !user_id || !report_date || !total_hours) {
return res.status(400).json({ return res.status(400).json({
success: false, success: false,
message: '필수 필드가 누락되었습니다. (assignment_id, session_id, worker_id, report_date, total_hours)' message: '필수 필드가 누락되었습니다. (assignment_id, session_id, user_id, report_date, total_hours)'
}); });
} }
@@ -704,7 +704,7 @@ const createFromTbm = async (req, res) => {
const reportData = { const reportData = {
tbm_assignment_id, tbm_assignment_id,
tbm_session_id, tbm_session_id,
worker_id, user_id,
project_id, project_id,
work_type_id, work_type_id,
report_date, report_date,

View File

@@ -101,7 +101,7 @@ const getDailyWorkerDetails = asyncHandler(async (req, res) => {
// 데이터 변환 // 데이터 변환
const formattedData = workerDetails.map(worker => ({ const formattedData = workerDetails.map(worker => ({
workerId: worker.worker_id, userId: worker.user_id,
workerName: worker.worker_name, workerName: worker.worker_name,
jobType: worker.job_type, jobType: worker.job_type,
totalHours: parseFloat(worker.total_work_hours || 0), totalHours: parseFloat(worker.total_work_hours || 0),

View File

@@ -416,7 +416,7 @@ const PatrolController = {
LEFT JOIN tasks t ON ts.task_id = t.task_id LEFT JOIN tasks t ON ts.task_id = t.task_id
LEFT JOIN work_types wt ON t.work_type_id = wt.id LEFT JOIN work_types wt ON t.work_type_id = wt.id
LEFT JOIN users u ON ts.leader_id = u.user_id LEFT JOIN users u ON ts.leader_id = u.user_id
LEFT JOIN workers w ON ts.leader_worker_id = w.worker_id LEFT JOIN workers w ON ts.leader_user_id = w.user_id
WHERE ts.category_id = ? AND ts.session_date = ? WHERE ts.category_id = ? AND ts.session_date = ?
ORDER BY ts.created_at DESC ORDER BY ts.created_at DESC
`, [categoryId, targetDate]); `, [categoryId, targetDate]);
@@ -433,7 +433,7 @@ const PatrolController = {
SELECT tta.assignment_id, w.worker_name, w.occupation, SELECT tta.assignment_id, w.worker_name, w.occupation,
tta.attendance_status, tta.signature_image tta.attendance_status, tta.signature_image
FROM tbm_team_assignments tta FROM tbm_team_assignments tta
JOIN workers w ON tta.worker_id = w.worker_id JOIN workers w ON tta.user_id = w.user_id
WHERE tta.session_id = ? WHERE tta.session_id = ?
ORDER BY w.worker_name ORDER BY w.worker_name
`, [session.session_id]); `, [session.session_id]);

View File

@@ -10,7 +10,7 @@ const TbmController = {
try { try {
const sessionData = { const sessionData = {
session_date: req.body.session_date, session_date: req.body.session_date,
leader_id: req.body.leader_id || null, leader_user_id: req.body.leader_user_id || null,
project_id: req.body.project_id || null, project_id: req.body.project_id || null,
work_location: req.body.work_location || null, work_location: req.body.work_location || null,
work_description: req.body.work_description || null, work_description: req.body.work_description || null,
@@ -135,7 +135,7 @@ const TbmController = {
try { try {
const assignmentData = { const assignmentData = {
session_id: req.params.sessionId, session_id: req.params.sessionId,
worker_id: req.body.worker_id, user_id: req.body.user_id,
assigned_role: req.body.assigned_role || null, assigned_role: req.body.assigned_role || null,
work_detail: req.body.work_detail || null, work_detail: req.body.work_detail || null,
is_present: req.body.is_present, is_present: req.body.is_present,
@@ -148,7 +148,7 @@ const TbmController = {
work_hours: req.body.work_hours !== undefined ? req.body.work_hours : undefined work_hours: req.body.work_hours !== undefined ? req.body.work_hours : undefined
}; };
if (!assignmentData.worker_id) { if (!assignmentData.user_id) {
return res.status(400).json({ success: false, message: '작업자 ID가 필요합니다.' }); return res.status(400).json({ success: false, message: '작업자 ID가 필요합니다.' });
} }
@@ -164,7 +164,7 @@ const TbmController = {
try { try {
const assignmentData = { const assignmentData = {
session_id: req.params.sessionId, session_id: req.params.sessionId,
worker_id: req.body.worker_id, user_id: req.body.user_id,
work_hours: req.body.work_hours, work_hours: req.body.work_hours,
project_id: req.body.project_id || null, project_id: req.body.project_id || null,
work_type_id: req.body.work_type_id || null, work_type_id: req.body.work_type_id || null,
@@ -173,7 +173,7 @@ const TbmController = {
workplace_id: req.body.workplace_id || null workplace_id: req.body.workplace_id || null
}; };
if (!assignmentData.worker_id || !assignmentData.work_hours) { if (!assignmentData.user_id || !assignmentData.work_hours) {
return res.status(400).json({ success: false, message: '작업자 ID와 작업시간이 필요합니다.' }); return res.status(400).json({ success: false, message: '작업자 ID와 작업시간이 필요합니다.' });
} }
@@ -218,8 +218,8 @@ const TbmController = {
removeTeamMember: async (req, res) => { removeTeamMember: async (req, res) => {
try { try {
const { sessionId, workerId } = req.params; const { sessionId, userId } = req.params;
const result = await TbmModel.removeTeamMember(sessionId, workerId); const result = await TbmModel.removeTeamMember(sessionId, userId);
if (result.affectedRows === 0) { if (result.affectedRows === 0) {
return res.status(404).json({ success: false, message: '팀원을 찾을 수 없습니다.' }); return res.status(404).json({ success: false, message: '팀원을 찾을 수 없습니다.' });
@@ -432,17 +432,17 @@ const TbmController = {
try { try {
const handoverData = { const handoverData = {
session_id: req.body.session_id, session_id: req.body.session_id,
from_leader_id: req.body.from_leader_id, from_leader_user_id: req.body.from_leader_user_id,
to_leader_id: req.body.to_leader_id, to_leader_user_id: req.body.to_leader_user_id,
handover_date: req.body.handover_date, handover_date: req.body.handover_date,
handover_time: req.body.handover_time || null, handover_time: req.body.handover_time || null,
reason: req.body.reason, reason: req.body.reason,
handover_notes: req.body.handover_notes || null, handover_notes: req.body.handover_notes || null,
worker_ids: req.body.worker_ids || [] user_ids: req.body.user_ids || []
}; };
if (!handoverData.session_id || !handoverData.from_leader_id || if (!handoverData.session_id || !handoverData.from_leader_user_id ||
!handoverData.to_leader_id || !handoverData.handover_date || !handoverData.reason) { !handoverData.to_leader_user_id || !handoverData.handover_date || !handoverData.reason) {
return res.status(400).json({ success: false, message: '필수 정보가 누락되었습니다.' }); return res.status(400).json({ success: false, message: '필수 정보가 누락되었습니다.' });
} }
@@ -483,7 +483,7 @@ const TbmController = {
getMyPendingHandovers: async (req, res) => { getMyPendingHandovers: async (req, res) => {
try { try {
const toLeaderId = req.user.worker_id; const toLeaderId = req.user.user_id;
if (!toLeaderId) { if (!toLeaderId) {
return res.status(400).json({ success: false, message: '작업자 정보를 찾을 수 없습니다.' }); return res.status(400).json({ success: false, message: '작업자 정보를 찾을 수 없습니다.' });
} }
@@ -532,10 +532,10 @@ const TbmController = {
createTransfer: async (req, res) => { createTransfer: async (req, res) => {
try { try {
const { transfer_type, worker_id, source_session_id, dest_session_id, hours, const { transfer_type, user_id, source_session_id, dest_session_id, hours,
project_id, work_type_id, task_id, workplace_category_id, workplace_id } = req.body; 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) { if (!transfer_type || !user_id || !source_session_id || !dest_session_id || !hours) {
return res.status(400).json({ success: false, message: '필수 정보가 누락되었습니다.' }); return res.status(400).json({ success: false, message: '필수 정보가 누락되었습니다.' });
} }
@@ -545,7 +545,7 @@ const TbmController = {
String(today.getDate()).padStart(2, '0'); String(today.getDate()).padStart(2, '0');
const transferData = { const transferData = {
transfer_type, worker_id, source_session_id, dest_session_id, transfer_type, user_id, source_session_id, dest_session_id,
hours, initiated_by: req.user.user_id, transfer_date: transferDate, hours, initiated_by: req.user.user_id, transfer_date: transferDate,
project_id, work_type_id, task_id, workplace_category_id, workplace_id project_id, work_type_id, task_id, workplace_category_id, workplace_id
}; };

View File

@@ -10,12 +10,12 @@ const logger = require('../utils/logger');
const vacationBalanceController = { const vacationBalanceController = {
/** /**
* 특정 작업자의 휴가 잔액 조회 (특정 연도) * 특정 작업자의 휴가 잔액 조회 (특정 연도)
* GET /api/vacation-balances/worker/:workerId/year/:year * GET /api/vacation-balances/user/:userId/year/:year
*/ */
async getByWorkerAndYear(req, res) { async getByWorkerAndYear(req, res) {
try { try {
const { workerId, year } = req.params; const { userId, year } = req.params;
const results = await vacationBalanceModel.getByWorkerAndYear(workerId, year); const results = await vacationBalanceModel.getByWorkerAndYear(userId, year);
res.json({ success: true, data: results }); res.json({ success: true, data: results });
} catch (error) { } catch (error) {
logger.error('휴가 잔액 조회 오류:', error); logger.error('휴가 잔액 조회 오류:', error);
@@ -44,18 +44,18 @@ const vacationBalanceController = {
*/ */
async createBalance(req, res) { async createBalance(req, res) {
try { try {
const { worker_id, vacation_type_id, year, total_days, used_days, notes } = req.body; const { user_id, vacation_type_id, year, total_days, used_days, notes } = req.body;
const created_by = req.user.user_id; const created_by = req.user.user_id;
if (!worker_id || !vacation_type_id || !year || total_days === undefined) { if (!user_id || !vacation_type_id || !year || total_days === undefined) {
return res.status(400).json({ return res.status(400).json({
success: false, success: false,
message: '필수 필드가 누락되었습니다 (worker_id, vacation_type_id, year, total_days)' message: '필수 필드가 누락되었습니다 (user_id, vacation_type_id, year, total_days)'
}); });
} }
// 중복 체크 // 중복 체크
const existing = await vacationBalanceModel.getByWorkerTypeYear(worker_id, vacation_type_id, year); const existing = await vacationBalanceModel.getByWorkerTypeYear(user_id, vacation_type_id, year);
if (existing && existing.length > 0) { if (existing && existing.length > 0) {
return res.status(400).json({ return res.status(400).json({
success: false, success: false,
@@ -64,7 +64,7 @@ const vacationBalanceController = {
} }
const balanceData = { const balanceData = {
worker_id, user_id,
vacation_type_id, vacation_type_id,
year, year,
total_days, total_days,
@@ -142,13 +142,13 @@ const vacationBalanceController = {
*/ */
async autoCalculateAndCreate(req, res) { async autoCalculateAndCreate(req, res) {
try { try {
const { worker_id, hire_date, year } = req.body; const { user_id, hire_date, year } = req.body;
const created_by = req.user.user_id; const created_by = req.user.user_id;
if (!worker_id || !hire_date || !year) { if (!user_id || !hire_date || !year) {
return res.status(400).json({ return res.status(400).json({
success: false, success: false,
message: '필수 필드가 누락되었습니다 (worker_id, hire_date, year)' message: '필수 필드가 누락되었습니다 (user_id, hire_date, year)'
}); });
} }
@@ -163,7 +163,7 @@ const vacationBalanceController = {
const annualTypeId = types[0].id; const annualTypeId = types[0].id;
// 중복 체크 // 중복 체크
const existing = await vacationBalanceModel.getByWorkerTypeYear(worker_id, annualTypeId, year); const existing = await vacationBalanceModel.getByWorkerTypeYear(user_id, annualTypeId, year);
if (existing && existing.length > 0) { if (existing && existing.length > 0) {
return res.status(400).json({ return res.status(400).json({
success: false, success: false,
@@ -172,7 +172,7 @@ const vacationBalanceController = {
} }
const balanceData = { const balanceData = {
worker_id, user_id,
vacation_type_id: annualTypeId, vacation_type_id: annualTypeId,
year, year,
total_days: annualDays, total_days: annualDays,
@@ -213,9 +213,9 @@ const vacationBalanceController = {
let errorCount = 0; let errorCount = 0;
for (const balance of balances) { for (const balance of balances) {
const { worker_id, vacation_type_id, year, total_days, notes } = balance; const { user_id, vacation_type_id, year, total_days, notes } = balance;
if (!worker_id || !vacation_type_id || !year || total_days === undefined) { if (!user_id || !vacation_type_id || !year || total_days === undefined) {
errorCount++; errorCount++;
continue; continue;
} }
@@ -223,7 +223,7 @@ const vacationBalanceController = {
try { try {
const query = ` const query = `
INSERT INTO vacation_balance_details INSERT INTO vacation_balance_details
(worker_id, vacation_type_id, year, total_days, used_days, notes, created_by) (user_id, vacation_type_id, year, total_days, used_days, notes, created_by)
VALUES (?, ?, ?, ?, 0, ?, ?) VALUES (?, ?, ?, ?, 0, ?, ?)
ON DUPLICATE KEY UPDATE ON DUPLICATE KEY UPDATE
total_days = VALUES(total_days), total_days = VALUES(total_days),
@@ -231,7 +231,7 @@ const vacationBalanceController = {
updated_at = NOW() updated_at = NOW()
`; `;
await db.query(query, [worker_id, vacation_type_id, year, total_days, notes || null, created_by]); await db.query(query, [user_id, vacation_type_id, year, total_days, notes || null, created_by]);
successCount++; successCount++;
} catch (err) { } catch (err) {
logger.error('휴가 잔액 저장 오류:', err); logger.error('휴가 잔액 저장 오류:', err);
@@ -252,12 +252,12 @@ const vacationBalanceController = {
/** /**
* 작업자의 사용 가능한 휴가 일수 조회 * 작업자의 사용 가능한 휴가 일수 조회
* GET /api/vacation-balances/worker/:workerId/year/:year/available * GET /api/vacation-balances/user/:userId/year/:year/available
*/ */
async getAvailableDays(req, res) { async getAvailableDays(req, res) {
try { try {
const { workerId, year } = req.params; const { userId, year } = req.params;
const results = await vacationBalanceModel.getAvailableVacationDays(workerId, year); const results = await vacationBalanceModel.getAvailableVacationDays(userId, year);
res.json({ success: true, data: results }); res.json({ success: true, data: results });
} catch (error) { } catch (error) {
logger.error('사용 가능 휴가 조회 오류:', error); logger.error('사용 가능 휴가 조회 오류:', error);

View File

@@ -12,10 +12,10 @@ const vacationRequestController = {
*/ */
async createRequest(req, res) { async createRequest(req, res) {
try { try {
const { worker_id, vacation_type_id, start_date, end_date, days_used, reason } = req.body; const { user_id, vacation_type_id, start_date, end_date, days_used, reason } = req.body;
const requested_by = req.user.user_id; const requested_by = req.user.user_id;
if (!worker_id || !vacation_type_id || !start_date || !end_date || !days_used) { if (!user_id || !vacation_type_id || !start_date || !end_date || !days_used) {
return res.status(400).json({ success: false, message: '필수 필드가 누락되었습니다' }); return res.status(400).json({ success: false, message: '필수 필드가 누락되었습니다' });
} }
@@ -24,13 +24,13 @@ const vacationRequestController = {
} }
// 기간 중복 체크 // 기간 중복 체크
const overlapRows = await vacationRequestModel.checkOverlap(worker_id, start_date, end_date); const overlapRows = await vacationRequestModel.checkOverlap(user_id, start_date, end_date);
if (overlapRows[0].count > 0) { if (overlapRows[0].count > 0) {
return res.status(400).json({ success: false, message: '해당 기간에 이미 신청된 휴가가 있습니다' }); return res.status(400).json({ success: false, message: '해당 기간에 이미 신청된 휴가가 있습니다' });
} }
const result = await vacationRequestModel.create({ const result = await vacationRequestModel.create({
worker_id, vacation_type_id, start_date, end_date, user_id, vacation_type_id, start_date, end_date,
days_used, reason: reason || null, status: 'pending', requested_by days_used, reason: reason || null, status: 'pending', requested_by
}); });
@@ -51,7 +51,7 @@ const vacationRequestController = {
async getAllRequests(req, res) { async getAllRequests(req, res) {
try { try {
const filters = { const filters = {
worker_id: req.query.worker_id, user_id: req.query.user_id,
status: req.query.status, status: req.query.status,
start_date: req.query.start_date, start_date: req.query.start_date,
end_date: req.query.end_date, end_date: req.query.end_date,
@@ -60,8 +60,8 @@ const vacationRequestController = {
// 일반 사용자는 자신의 신청만 조회 가능 // 일반 사용자는 자신의 신청만 조회 가능
if (req.user.access_level !== 'system') { if (req.user.access_level !== 'system') {
if (req.user.worker_id) { if (req.user.user_id) {
filters.worker_id = req.user.worker_id; filters.user_id = req.user.user_id;
} else { } else {
return res.status(403).json({ success: false, message: '권한이 없습니다' }); return res.status(403).json({ success: false, message: '권한이 없습니다' });
} }
@@ -88,7 +88,7 @@ const vacationRequestController = {
const request = results[0]; const request = results[0];
if (req.user.access_level !== 'system' && req.user.worker_id !== request.worker_id) { if (req.user.access_level !== 'system' && req.user.user_id !== request.user_id) {
return res.status(403).json({ success: false, message: '권한이 없습니다' }); return res.status(403).json({ success: false, message: '권한이 없습니다' });
} }
@@ -114,7 +114,7 @@ const vacationRequestController = {
const existingRequest = results[0]; const existingRequest = results[0];
if (req.user.access_level !== 'system' && req.user.worker_id !== existingRequest.worker_id) { if (req.user.access_level !== 'system' && req.user.user_id !== existingRequest.user_id) {
return res.status(403).json({ success: false, message: '권한이 없습니다' }); return res.status(403).json({ success: false, message: '권한이 없습니다' });
} }
@@ -134,7 +134,7 @@ const vacationRequestController = {
const newEndDate = end_date || existingRequest.end_date; const newEndDate = end_date || existingRequest.end_date;
const overlapRows = await vacationRequestModel.checkOverlap( const overlapRows = await vacationRequestModel.checkOverlap(
existingRequest.worker_id, newStartDate, newEndDate, id existingRequest.user_id, newStartDate, newEndDate, id
); );
if (overlapRows[0].count > 0) { if (overlapRows[0].count > 0) {
return res.status(400).json({ success: false, message: '해당 기간에 이미 신청된 휴가가 있습니다' }); return res.status(400).json({ success: false, message: '해당 기간에 이미 신청된 휴가가 있습니다' });
@@ -163,7 +163,7 @@ const vacationRequestController = {
const existingRequest = results[0]; const existingRequest = results[0];
if (req.user.access_level !== 'system' && req.user.worker_id !== existingRequest.worker_id) { if (req.user.access_level !== 'system' && req.user.user_id !== existingRequest.user_id) {
return res.status(403).json({ success: false, message: '권한이 없습니다' }); return res.status(403).json({ success: false, message: '권한이 없습니다' });
} }

View File

@@ -353,10 +353,10 @@ const getWorkerSpecialization = asyncHandler(async (req, res) => {
// 작업자별로 그룹화하여 정리 // 작업자별로 그룹화하여 정리
const groupedData = specializationData.reduce((acc, item) => { const groupedData = specializationData.reduce((acc, item) => {
if (!acc[item.worker_id]) { if (!acc[item.user_id]) {
acc[item.worker_id] = []; acc[item.user_id] = [];
} }
acc[item.worker_id].push({ acc[item.user_id].push({
work_type_id: item.work_type_id, work_type_id: item.work_type_id,
project_id: item.project_id, project_id: item.project_id,
totalHours: item.totalHours, totalHours: item.totalHours,

View File

@@ -31,9 +31,9 @@ const getAnalysisFilters = asyncHandler(async (req, res) => {
// 작업자 목록 // 작업자 목록
const [workers] = await db.query(` const [workers] = await db.query(`
SELECT DISTINCT w.worker_id, w.worker_name SELECT DISTINCT w.user_id, w.worker_name
FROM workers w FROM workers w
INNER JOIN daily_work_reports dwr ON w.worker_id = dwr.worker_id INNER JOIN daily_work_reports dwr ON w.user_id = dwr.user_id
ORDER BY w.worker_name ORDER BY w.worker_name
`); `);
@@ -79,7 +79,7 @@ const getAnalysisFilters = asyncHandler(async (req, res) => {
* 기간별 작업 분석 데이터 조회 * 기간별 작업 분석 데이터 조회
*/ */
const getAnalyticsByPeriod = asyncHandler(async (req, res) => { const getAnalyticsByPeriod = asyncHandler(async (req, res) => {
const { start_date, end_date, project_id, worker_id } = req.query; const { start_date, end_date, project_id, user_id } = req.query;
if (!start_date || !end_date) { if (!start_date || !end_date) {
throw new ValidationError('start_date와 end_date가 필요합니다', { throw new ValidationError('start_date와 end_date가 필요합니다', {
@@ -93,7 +93,7 @@ const getAnalyticsByPeriod = asyncHandler(async (req, res) => {
start_date, start_date,
end_date, end_date,
project_id, project_id,
worker_id user_id
}); });
const db = await getDb(); const db = await getDb();
@@ -108,9 +108,9 @@ const getAnalyticsByPeriod = asyncHandler(async (req, res) => {
queryParams.push(project_id); queryParams.push(project_id);
} }
if (worker_id) { if (user_id) {
whereConditions.push('dwr.worker_id = ?'); whereConditions.push('dwr.user_id = ?');
queryParams.push(worker_id); queryParams.push(user_id);
} }
const whereClause = whereConditions.join(' AND '); const whereClause = whereConditions.join(' AND ');
@@ -120,7 +120,7 @@ const getAnalyticsByPeriod = asyncHandler(async (req, res) => {
SELECT SELECT
COUNT(*) as total_entries, COUNT(*) as total_entries,
SUM(dwr.work_hours) as total_hours, SUM(dwr.work_hours) as total_hours,
COUNT(DISTINCT dwr.worker_id) as unique_workers, COUNT(DISTINCT dwr.user_id) as unique_workers,
COUNT(DISTINCT dwr.project_id) as unique_projects, COUNT(DISTINCT dwr.project_id) as unique_projects,
COUNT(DISTINCT dwr.report_date) as working_days, COUNT(DISTINCT dwr.report_date) as working_days,
AVG(dwr.work_hours) as avg_hours_per_entry, AVG(dwr.work_hours) as avg_hours_per_entry,
@@ -139,7 +139,7 @@ const getAnalyticsByPeriod = asyncHandler(async (req, res) => {
dwr.report_date, dwr.report_date,
SUM(dwr.work_hours) as daily_hours, SUM(dwr.work_hours) as daily_hours,
COUNT(*) as daily_entries, COUNT(*) as daily_entries,
COUNT(DISTINCT dwr.worker_id) as daily_workers COUNT(DISTINCT dwr.user_id) as daily_workers
FROM daily_work_reports dwr FROM daily_work_reports dwr
WHERE ${whereClause} WHERE ${whereClause}
GROUP BY dwr.report_date GROUP BY dwr.report_date
@@ -202,7 +202,7 @@ const getAnalyticsByPeriod = asyncHandler(async (req, res) => {
// 6. 작업자별 성과 분석 // 6. 작업자별 성과 분석
const workerAnalysisSql = ` const workerAnalysisSql = `
SELECT SELECT
w.worker_id, w.user_id,
w.worker_name, w.worker_name,
COUNT(*) as total_entries, COUNT(*) as total_entries,
SUM(dwr.work_hours) as total_hours, SUM(dwr.work_hours) as total_hours,
@@ -212,9 +212,9 @@ const getAnalyticsByPeriod = asyncHandler(async (req, res) => {
COUNT(CASE WHEN dwr.error_type_id IS NOT NULL THEN 1 END) as error_count, COUNT(CASE WHEN dwr.error_type_id IS NOT NULL THEN 1 END) as error_count,
ROUND((COUNT(CASE WHEN dwr.error_type_id IS NOT NULL THEN 1 END) / COUNT(*)) * 100, 2) as error_rate ROUND((COUNT(CASE WHEN dwr.error_type_id IS NOT NULL THEN 1 END) / COUNT(*)) * 100, 2) as error_rate
FROM daily_work_reports dwr FROM daily_work_reports dwr
LEFT JOIN workers w ON dwr.worker_id = w.worker_id LEFT JOIN workers w ON dwr.user_id = w.user_id
WHERE ${whereClause} WHERE ${whereClause}
GROUP BY w.worker_id, w.worker_name GROUP BY w.user_id, w.worker_name
ORDER BY total_hours DESC ORDER BY total_hours DESC
`; `;
@@ -227,7 +227,7 @@ const getAnalyticsByPeriod = asyncHandler(async (req, res) => {
p.project_name, p.project_name,
COUNT(*) as total_entries, COUNT(*) as total_entries,
SUM(dwr.work_hours) as total_hours, SUM(dwr.work_hours) as total_hours,
COUNT(DISTINCT dwr.worker_id) as workers_count, COUNT(DISTINCT dwr.user_id) as workers_count,
COUNT(DISTINCT dwr.report_date) as working_days, COUNT(DISTINCT dwr.report_date) as working_days,
AVG(dwr.work_hours) as avg_hours_per_entry, AVG(dwr.work_hours) as avg_hours_per_entry,
COUNT(CASE WHEN dwr.error_type_id IS NOT NULL THEN 1 END) as error_count, COUNT(CASE WHEN dwr.error_type_id IS NOT NULL THEN 1 END) as error_count,
@@ -259,7 +259,7 @@ const getAnalyticsByPeriod = asyncHandler(async (req, res) => {
workerAnalysis, workerAnalysis,
projectAnalysis, projectAnalysis,
period: { start_date, end_date }, period: { start_date, end_date },
filters: { project_id, worker_id } filters: { project_id, user_id }
}, },
message: '기간별 분석 데이터 조회 성공' message: '기간별 분석 데이터 조회 성공'
}); });
@@ -311,7 +311,7 @@ const getProjectAnalysis = asyncHandler(async (req, res) => {
p.project_name, p.project_name,
SUM(dwr.work_hours) as total_hours, SUM(dwr.work_hours) as total_hours,
COUNT(*) as total_entries, COUNT(*) as total_entries,
COUNT(DISTINCT dwr.worker_id) as workers_count, COUNT(DISTINCT dwr.user_id) as workers_count,
COUNT(DISTINCT dwr.report_date) as working_days, COUNT(DISTINCT dwr.report_date) as working_days,
AVG(dwr.work_hours) as avg_hours_per_entry AVG(dwr.work_hours) as avg_hours_per_entry
FROM daily_work_reports dwr FROM daily_work_reports dwr
@@ -351,7 +351,7 @@ const getProjectAnalysis = asyncHandler(async (req, res) => {
* 작업자별 상세 분석 * 작업자별 상세 분석
*/ */
const getWorkerAnalysis = asyncHandler(async (req, res) => { const getWorkerAnalysis = asyncHandler(async (req, res) => {
const { start_date, end_date, worker_id } = req.query; const { start_date, end_date, user_id } = req.query;
if (!start_date || !end_date) { if (!start_date || !end_date) {
throw new ValidationError('start_date와 end_date가 필요합니다', { throw new ValidationError('start_date와 end_date가 필요합니다', {
@@ -363,7 +363,7 @@ const getWorkerAnalysis = asyncHandler(async (req, res) => {
logger.info('작업자별 분석 조회 요청', { logger.info('작업자별 분석 조회 요청', {
start_date, start_date,
end_date, end_date,
worker_id user_id
}); });
const db = await getDb(); const db = await getDb();
@@ -372,16 +372,16 @@ const getWorkerAnalysis = asyncHandler(async (req, res) => {
let whereConditions = ['dwr.report_date BETWEEN ? AND ?']; let whereConditions = ['dwr.report_date BETWEEN ? AND ?'];
let queryParams = [start_date, end_date]; let queryParams = [start_date, end_date];
if (worker_id) { if (user_id) {
whereConditions.push('dwr.worker_id = ?'); whereConditions.push('dwr.user_id = ?');
queryParams.push(worker_id); queryParams.push(user_id);
} }
const whereClause = whereConditions.join(' AND '); const whereClause = whereConditions.join(' AND ');
const workerStatsSql = ` const workerStatsSql = `
SELECT SELECT
dwr.worker_id, dwr.user_id,
w.worker_name, w.worker_name,
SUM(dwr.work_hours) as total_hours, SUM(dwr.work_hours) as total_hours,
COUNT(*) as total_entries, COUNT(*) as total_entries,
@@ -389,9 +389,9 @@ const getWorkerAnalysis = asyncHandler(async (req, res) => {
COUNT(DISTINCT dwr.report_date) as working_days, COUNT(DISTINCT dwr.report_date) as working_days,
AVG(dwr.work_hours) as avg_hours_per_entry AVG(dwr.work_hours) as avg_hours_per_entry
FROM daily_work_reports dwr FROM daily_work_reports dwr
LEFT JOIN workers w ON dwr.worker_id = w.worker_id LEFT JOIN workers w ON dwr.user_id = w.user_id
WHERE ${whereClause} WHERE ${whereClause}
GROUP BY dwr.worker_id GROUP BY dwr.user_id
ORDER BY total_hours DESC ORDER BY total_hours DESC
`; `;

View File

@@ -103,7 +103,7 @@ exports.getAllWorkers = asyncHandler(async (req, res) => {
* 단일 작업자 조회 * 단일 작업자 조회
*/ */
exports.getWorkerById = asyncHandler(async (req, res) => { exports.getWorkerById = asyncHandler(async (req, res) => {
const id = parseInt(req.params.worker_id, 10); const id = parseInt(req.params.user_id, 10);
if (isNaN(id)) { if (isNaN(id)) {
throw new ValidationError('유효하지 않은 작업자 ID입니다'); throw new ValidationError('유효하지 않은 작업자 ID입니다');
@@ -126,7 +126,7 @@ exports.getWorkerById = asyncHandler(async (req, res) => {
* 작업자 수정 * 작업자 수정
*/ */
exports.updateWorker = asyncHandler(async (req, res) => { exports.updateWorker = asyncHandler(async (req, res) => {
const id = parseInt(req.params.worker_id, 10); const id = parseInt(req.params.user_id, 10);
if (isNaN(id)) { if (isNaN(id)) {
throw new ValidationError('유효하지 않은 작업자 ID입니다'); throw new ValidationError('유효하지 않은 작업자 ID입니다');
@@ -252,7 +252,7 @@ exports.updateWorker = asyncHandler(async (req, res) => {
* 작업자 삭제 * 작업자 삭제
*/ */
exports.removeWorker = asyncHandler(async (req, res) => { exports.removeWorker = asyncHandler(async (req, res) => {
const id = parseInt(req.params.worker_id, 10); const id = parseInt(req.params.user_id, 10);
if (isNaN(id)) { if (isNaN(id)) {
throw new ValidationError('유효하지 않은 작업자 ID입니다'); throw new ValidationError('유효하지 않은 작업자 ID입니다');

View File

@@ -0,0 +1,233 @@
/**
* worker_id → user_id 통합 마이그레이션 (Phase 1)
*
* 비파괴적 추가: 기존 worker_id 컬럼은 유지하면서 user_id 컬럼을 추가하고 백필.
* 코드 변경 전이므로 기존 시스템 동작에 영향 없음.
*
* 대상 테이블:
* 1. departments - is_production 플래그 추가
* 2. sso_users - department_id 추가 (없는 경우)
* 3. workers - user_id 추가 + 매핑 백필
* 4~15. 12개 참조 테이블 - user_id 추가 + 백필
*
* @since 2026-03-05
*/
exports.up = async function(knex) {
// ============================================================
// 1. departments 테이블에 is_production 플래그 추가
// ============================================================
const hasIsProduction = await knex.schema.hasColumn('departments', 'is_production');
if (!hasIsProduction) {
await knex.schema.table('departments', (table) => {
table.boolean('is_production').defaultTo(false).comment('생산직 부서 여부');
});
await knex.raw(`UPDATE departments SET is_production = TRUE WHERE department_name LIKE '%생산%'`);
console.log('✅ departments.is_production 추가 완료');
}
// ============================================================
// 2. sso_users에 department_id 추가 (없는 경우)
// ============================================================
const hasSsoDeptId = await knex.schema.hasColumn('sso_users', 'department_id');
if (!hasSsoDeptId) {
await knex.schema.table('sso_users', (table) => {
table.integer('department_id').unsigned().defaultTo(null).comment('소속 부서 ID');
});
// 기존 department(문자열) → department_id(FK) 매핑
await knex.raw(`
UPDATE sso_users s
INNER JOIN departments d ON s.department = d.department_name
SET s.department_id = d.department_id
WHERE s.department IS NOT NULL
`);
console.log('✅ sso_users.department_id 추가 및 백필 완료');
}
// ============================================================
// 3. workers 테이블에 user_id 추가 + 매핑 백필
// ============================================================
const hasWorkersUserId = await knex.schema.hasColumn('workers', 'user_id');
if (!hasWorkersUserId) {
await knex.schema.table('workers', (table) => {
table.integer('user_id').unsigned().defaultTo(null).after('worker_id')
.comment('sso_users.user_id 매핑');
});
// users 테이블을 경유하여 sso_users.user_id 매핑
await knex.raw(`
UPDATE workers w
INNER JOIN users u ON u.worker_id = w.worker_id
INNER JOIN sso_users s ON s.username = u.username
SET w.user_id = s.user_id
`);
// user_id에 인덱스 추가
await knex.raw(`ALTER TABLE workers ADD INDEX idx_workers_user_id (user_id)`);
console.log('✅ workers.user_id 추가 및 백필 완료');
}
// ============================================================
// 4~15. 12개 참조 테이블에 user_id 컬럼 추가 + 백필
// ============================================================
// worker_id 컬럼을 가진 테이블들
const tablesWithWorkerId = [
'tbm_team_assignments',
'tbm_transfers',
'daily_work_reports',
'daily_attendance_records',
'worker_vacation_balance',
'vacation_requests',
'vacation_balance_details',
'worker_groups',
'monthly_worker_status',
];
for (const tableName of tablesWithWorkerId) {
const tableExists = await knex.schema.hasTable(tableName);
if (!tableExists) {
console.log(`⏭️ ${tableName} 테이블이 존재하지 않음, 건너뜀`);
continue;
}
const hasUserId = await knex.schema.hasColumn(tableName, 'user_id');
if (!hasUserId) {
await knex.schema.table(tableName, (table) => {
table.integer('user_id').unsigned().defaultTo(null).comment('sso_users.user_id');
});
// 백필: workers 테이블의 user_id 매핑 사용
await knex.raw(`
UPDATE ${tableName} t
INNER JOIN workers w ON t.worker_id = w.worker_id
SET t.user_id = w.user_id
WHERE w.user_id IS NOT NULL
`);
// 인덱스 추가
await knex.raw(`ALTER TABLE ${tableName} ADD INDEX idx_${tableName}_user_id (user_id)`);
console.log(`${tableName}.user_id 추가 및 백필 완료`);
}
}
// DailyIssueReports (대소문자 다른 테이블명)
const hasDIR = await knex.schema.hasTable('DailyIssueReports');
if (hasDIR) {
const hasDIRUserId = await knex.schema.hasColumn('DailyIssueReports', 'user_id');
if (!hasDIRUserId) {
await knex.schema.table('DailyIssueReports', (table) => {
table.integer('user_id').unsigned().defaultTo(null).comment('sso_users.user_id');
});
await knex.raw(`
UPDATE DailyIssueReports t
INNER JOIN workers w ON t.worker_id = w.worker_id
SET t.user_id = w.user_id
WHERE w.user_id IS NOT NULL
`);
console.log('✅ DailyIssueReports.user_id 추가 및 백필 완료');
}
}
// WorkReports (대소문자 다른 테이블명)
const hasWR = await knex.schema.hasTable('WorkReports');
if (hasWR) {
const hasWRUserId = await knex.schema.hasColumn('WorkReports', 'user_id');
if (!hasWRUserId) {
await knex.schema.table('WorkReports', (table) => {
table.integer('user_id').unsigned().defaultTo(null).comment('sso_users.user_id');
});
await knex.raw(`
UPDATE WorkReports t
INNER JOIN workers w ON t.worker_id = w.worker_id
SET t.user_id = w.user_id
WHERE w.user_id IS NOT NULL
`);
console.log('✅ WorkReports.user_id 추가 및 백필 완료');
}
}
// tbm_sessions: leader_id → leader_user_id 추가
const hasLeaderUserId = await knex.schema.hasColumn('tbm_sessions', 'leader_user_id');
if (!hasLeaderUserId) {
await knex.schema.table('tbm_sessions', (table) => {
table.integer('leader_user_id').unsigned().defaultTo(null).comment('조장 sso_users.user_id');
});
await knex.raw(`
UPDATE tbm_sessions t
INNER JOIN workers w ON t.leader_id = w.worker_id
SET t.leader_user_id = w.user_id
WHERE w.user_id IS NOT NULL
`);
await knex.raw(`ALTER TABLE tbm_sessions ADD INDEX idx_tbm_sessions_leader_user_id (leader_user_id)`);
console.log('✅ tbm_sessions.leader_user_id 추가 및 백필 완료');
}
// team_handovers: from/to_leader_id → from/to_leader_user_id 추가
const hasFromLeaderUserId = await knex.schema.hasColumn('team_handovers', 'from_leader_user_id');
if (!hasFromLeaderUserId) {
await knex.schema.table('team_handovers', (table) => {
table.integer('from_leader_user_id').unsigned().defaultTo(null).comment('인계자 sso_users.user_id');
table.integer('to_leader_user_id').unsigned().defaultTo(null).comment('인수자 sso_users.user_id');
});
await knex.raw(`
UPDATE team_handovers t
INNER JOIN workers w1 ON t.from_leader_id = w1.worker_id
SET t.from_leader_user_id = w1.user_id
WHERE w1.user_id IS NOT NULL
`);
await knex.raw(`
UPDATE team_handovers t
INNER JOIN workers w2 ON t.to_leader_id = w2.worker_id
SET t.to_leader_user_id = w2.user_id
WHERE w2.user_id IS NOT NULL
`);
console.log('✅ team_handovers.from/to_leader_user_id 추가 및 백필 완료');
}
console.log('🎉 Phase 1 마이그레이션 완료: 모든 테이블에 user_id 컬럼 추가 및 백필 완료');
};
exports.down = async function(knex) {
// user_id 컬럼 제거 (롤백)
const columnsToRemove = [
['departments', 'is_production'],
['workers', 'user_id'],
['tbm_team_assignments', 'user_id'],
['tbm_transfers', 'user_id'],
['daily_work_reports', 'user_id'],
['daily_attendance_records', 'user_id'],
['worker_vacation_balance', 'user_id'],
['vacation_requests', 'user_id'],
['vacation_balance_details', 'user_id'],
['worker_groups', 'user_id'],
['monthly_worker_status', 'user_id'],
['tbm_sessions', 'leader_user_id'],
['team_handovers', 'from_leader_user_id'],
['team_handovers', 'to_leader_user_id'],
];
for (const [tableName, columnName] of columnsToRemove) {
const tableExists = await knex.schema.hasTable(tableName);
if (!tableExists) continue;
const hasColumn = await knex.schema.hasColumn(tableName, columnName);
if (hasColumn) {
await knex.schema.table(tableName, (table) => {
table.dropColumn(columnName);
});
console.log(`↩️ ${tableName}.${columnName} 제거`);
}
}
// 대소문자 다른 테이블
for (const tableName of ['DailyIssueReports', 'WorkReports']) {
const tableExists = await knex.schema.hasTable(tableName);
if (tableExists) {
const hasColumn = await knex.schema.hasColumn(tableName, 'user_id');
if (hasColumn) {
await knex.schema.table(tableName, (table) => {
table.dropColumn('user_id');
});
console.log(`↩️ ${tableName}.user_id 제거`);
}
}
}
// sso_users.department_id는 유지 (다른 마이그레이션에서 관리)
};

View File

@@ -229,7 +229,7 @@ const requireMinLevel = (minLevel) => {
* requireAuth 미들웨어가 먼저 실행되어야 합니다. * requireAuth 미들웨어가 먼저 실행되어야 합니다.
* *
* @param {Object} options - 옵션 객체 * @param {Object} options - 옵션 객체
* @param {string} options.resourceField - 리소스 ID를 가져올 req 필드 (예: 'params.user_id', 'body.worker_id') * @param {string} options.resourceField - 리소스 ID를 가져올 req 필드 (예: 'params.user_id', 'body.user_id')
* @param {string} options.userField - 사용자 ID 필드명 (기본값: 'user_id', 'id'도 자동 시도) * @param {string} options.userField - 사용자 ID 필드명 (기본값: 'user_id', 'id'도 자동 시도)
* @param {string[]} options.adminRoles - 관리자로 인정할 역할들 (기본값: ['admin', 'system']) * @param {string[]} options.adminRoles - 관리자로 인정할 역할들 (기본값: ['admin', 'system'])
* @returns {Function} Express 미들웨어 함수 * @returns {Function} Express 미들웨어 함수
@@ -242,9 +242,9 @@ const requireMinLevel = (minLevel) => {
* resourceField: 'params.user_id' * resourceField: 'params.user_id'
* }), updateUser); * }), updateUser);
* *
* // 요청 body의 worker_id로 체크, support_team도 관리자로 인정 * // 요청 body의 user_id로 체크, support_team도 관리자로 인정
* router.delete('/reports/:id', requireAuth, requireOwnerOrAdmin({ * router.delete('/reports/:id', requireAuth, requireOwnerOrAdmin({
* resourceField: 'body.worker_id', * resourceField: 'body.user_id',
* adminRoles: ['admin', 'system', 'support_team'] * adminRoles: ['admin', 'system', 'support_team']
* }), deleteReport); * }), deleteReport);
*/ */

View File

@@ -12,7 +12,7 @@ class WorkAnalysis {
COALESCE(SUM(work_hours), 0) as total_hours, COALESCE(SUM(work_hours), 0) as total_hours,
COUNT(*) as total_reports, COUNT(*) as total_reports,
COUNT(DISTINCT project_id) as active_projects, COUNT(DISTINCT project_id) as active_projects,
COUNT(DISTINCT worker_id) as active_workers, COUNT(DISTINCT user_id) as active_workers,
SUM(CASE WHEN work_status_id = 2 THEN 1 ELSE 0 END) as error_reports, SUM(CASE WHEN work_status_id = 2 THEN 1 ELSE 0 END) as error_reports,
ROUND(AVG(work_hours), 2) as avg_hours_per_report ROUND(AVG(work_hours), 2) as avg_hours_per_report
FROM daily_work_reports FROM daily_work_reports
@@ -47,7 +47,7 @@ class WorkAnalysis {
report_date as date, report_date as date,
SUM(work_hours) as hours, SUM(work_hours) as hours,
COUNT(*) as reports, COUNT(*) as reports,
COUNT(DISTINCT worker_id) as workers, COUNT(DISTINCT user_id) as workers,
SUM(CASE WHEN work_status_id = 2 THEN 1 ELSE 0 END) as errors SUM(CASE WHEN work_status_id = 2 THEN 1 ELSE 0 END) as errors
FROM daily_work_reports FROM daily_work_reports
WHERE report_date BETWEEN ? AND ? WHERE report_date BETWEEN ? AND ?
@@ -73,7 +73,7 @@ class WorkAnalysis {
async getWorkerStats(startDate, endDate) { async getWorkerStats(startDate, endDate) {
const query = ` const query = `
SELECT SELECT
dwr.worker_id, dwr.user_id,
w.worker_name, w.worker_name,
SUM(dwr.work_hours) as totalHours, SUM(dwr.work_hours) as totalHours,
COUNT(*) as totalReports, COUNT(*) as totalReports,
@@ -82,17 +82,17 @@ class WorkAnalysis {
SUM(CASE WHEN dwr.work_status_id = 2 THEN 1 ELSE 0 END) as errorCount, SUM(CASE WHEN dwr.work_status_id = 2 THEN 1 ELSE 0 END) as errorCount,
COUNT(DISTINCT dwr.report_date) as workingDays COUNT(DISTINCT dwr.report_date) as workingDays
FROM daily_work_reports dwr FROM daily_work_reports dwr
LEFT JOIN workers w ON dwr.worker_id = w.worker_id LEFT JOIN workers w ON dwr.user_id = w.user_id
WHERE dwr.report_date BETWEEN ? AND ? WHERE dwr.report_date BETWEEN ? AND ?
GROUP BY dwr.worker_id, w.worker_name GROUP BY dwr.user_id, w.worker_name
ORDER BY totalHours DESC ORDER BY totalHours DESC
`; `;
try { try {
const [results] = await this.db.execute(query, [startDate, endDate]); const [results] = await this.db.execute(query, [startDate, endDate]);
return results.map(row => ({ return results.map(row => ({
worker_id: row.worker_id, user_id: row.user_id,
worker_name: row.worker_name || `작업자 ${row.worker_id}`, worker_name: row.worker_name || `작업자 ${row.user_id}`,
totalHours: parseFloat(row.totalHours) || 0, totalHours: parseFloat(row.totalHours) || 0,
totalReports: parseInt(row.totalReports) || 0, totalReports: parseInt(row.totalReports) || 0,
avgHours: parseFloat(row.avgHours) || 0, avgHours: parseFloat(row.avgHours) || 0,
@@ -114,7 +114,7 @@ class WorkAnalysis {
p.project_name, p.project_name,
SUM(dwr.work_hours) as totalHours, SUM(dwr.work_hours) as totalHours,
COUNT(*) as totalReports, COUNT(*) as totalReports,
COUNT(DISTINCT dwr.worker_id) as workerCount, COUNT(DISTINCT dwr.user_id) as workerCount,
ROUND(AVG(dwr.work_hours), 2) as avgHours, ROUND(AVG(dwr.work_hours), 2) as avgHours,
SUM(CASE WHEN dwr.work_status_id = 2 THEN 1 ELSE 0 END) as errorCount, SUM(CASE WHEN dwr.work_status_id = 2 THEN 1 ELSE 0 END) as errorCount,
COUNT(DISTINCT dwr.report_date) as activeDays COUNT(DISTINCT dwr.report_date) as activeDays
@@ -152,7 +152,7 @@ class WorkAnalysis {
SUM(dwr.work_hours) as totalHours, SUM(dwr.work_hours) as totalHours,
COUNT(*) as totalReports, COUNT(*) as totalReports,
ROUND(AVG(dwr.work_hours), 2) as avgHours, ROUND(AVG(dwr.work_hours), 2) as avgHours,
COUNT(DISTINCT dwr.worker_id) as workerCount, COUNT(DISTINCT dwr.user_id) as workerCount,
SUM(CASE WHEN dwr.work_status_id = 2 THEN 1 ELSE 0 END) as errorCount, SUM(CASE WHEN dwr.work_status_id = 2 THEN 1 ELSE 0 END) as errorCount,
COUNT(DISTINCT dwr.project_id) as projectCount COUNT(DISTINCT dwr.project_id) as projectCount
FROM daily_work_reports dwr FROM daily_work_reports dwr
@@ -190,7 +190,7 @@ class WorkAnalysis {
SELECT SELECT
dwr.id, dwr.id,
dwr.report_date, dwr.report_date,
dwr.worker_id, dwr.user_id,
w.worker_name, w.worker_name,
dwr.project_id, dwr.project_id,
p.project_name, p.project_name,
@@ -215,7 +215,7 @@ class WorkAnalysis {
u.name as created_by_name, u.name as created_by_name,
dwr.created_at dwr.created_at
FROM daily_work_reports dwr FROM daily_work_reports dwr
LEFT JOIN workers w ON dwr.worker_id = w.worker_id LEFT JOIN workers w ON dwr.user_id = w.user_id
LEFT JOIN projects p ON dwr.project_id = p.project_id LEFT JOIN projects p ON dwr.project_id = p.project_id
LEFT JOIN work_types wt ON dwr.work_type_id = wt.id LEFT JOIN work_types wt ON dwr.work_type_id = wt.id
LEFT JOIN tasks t ON dwr.work_type_id = t.task_id LEFT JOIN tasks t ON dwr.work_type_id = t.task_id
@@ -234,8 +234,8 @@ class WorkAnalysis {
return results.map(row => ({ return results.map(row => ({
id: row.id, id: row.id,
report_date: row.report_date, report_date: row.report_date,
worker_id: row.worker_id, user_id: row.user_id,
worker_name: row.worker_name || `작업자 ${row.worker_id}`, worker_name: row.worker_name || `작업자 ${row.user_id}`,
project_id: row.project_id, project_id: row.project_id,
project_name: row.project_name || `프로젝트 ${row.project_id}`, project_name: row.project_name || `프로젝트 ${row.project_id}`,
job_no: row.job_no || 'N/A', job_no: row.job_no || 'N/A',
@@ -274,7 +274,7 @@ class WorkAnalysis {
SUM(work_hours) as total_hours, SUM(work_hours) as total_hours,
COUNT(*) as total_reports, COUNT(*) as total_reports,
ROUND(AVG(work_hours), 2) as avg_hours, ROUND(AVG(work_hours), 2) as avg_hours,
COUNT(DISTINCT worker_id) as active_workers COUNT(DISTINCT user_id) as active_workers
FROM daily_work_reports FROM daily_work_reports
WHERE report_date BETWEEN ? AND ? WHERE report_date BETWEEN ? AND ?
GROUP BY DAYOFWEEK(report_date) GROUP BY DAYOFWEEK(report_date)
@@ -306,7 +306,7 @@ class WorkAnalysis {
irc.category_name as error_category_name, irc.category_name as error_category_name,
COUNT(*) as error_count, COUNT(*) as error_count,
SUM(dwr.work_hours) as total_hours, SUM(dwr.work_hours) as total_hours,
COUNT(DISTINCT dwr.worker_id) as affected_workers, COUNT(DISTINCT dwr.user_id) as affected_workers,
COUNT(DISTINCT dwr.project_id) as affected_projects COUNT(DISTINCT dwr.project_id) as affected_projects
FROM daily_work_reports dwr FROM daily_work_reports dwr
LEFT JOIN issue_report_items iri ON dwr.error_type_id = iri.item_id LEFT JOIN issue_report_items iri ON dwr.error_type_id = iri.item_id
@@ -341,7 +341,7 @@ class WorkAnalysis {
MONTHNAME(report_date) as month_name, MONTHNAME(report_date) as month_name,
SUM(work_hours) as total_hours, SUM(work_hours) as total_hours,
COUNT(*) as total_reports, COUNT(*) as total_reports,
COUNT(DISTINCT worker_id) as active_workers, COUNT(DISTINCT user_id) as active_workers,
COUNT(DISTINCT project_id) as active_projects, COUNT(DISTINCT project_id) as active_projects,
SUM(CASE WHEN work_status_id = 2 THEN 1 ELSE 0 END) as error_count SUM(CASE WHEN work_status_id = 2 THEN 1 ELSE 0 END) as error_count
FROM daily_work_reports FROM daily_work_reports
@@ -371,7 +371,7 @@ class WorkAnalysis {
async getWorkerSpecialization(startDate, endDate) { async getWorkerSpecialization(startDate, endDate) {
const query = ` const query = `
SELECT SELECT
dwr.worker_id, dwr.user_id,
w.worker_name, w.worker_name,
dwr.work_type_id, dwr.work_type_id,
wt.name as work_type_name, wt.name as work_type_name,
@@ -382,24 +382,24 @@ class WorkAnalysis {
ROUND((SUM(dwr.work_hours) / ( ROUND((SUM(dwr.work_hours) / (
SELECT SUM(work_hours) SELECT SUM(work_hours)
FROM daily_work_reports FROM daily_work_reports
WHERE worker_id = dwr.worker_id WHERE user_id = dwr.user_id
AND report_date BETWEEN ? AND ? AND report_date BETWEEN ? AND ?
)) * 100, 2) as percentage )) * 100, 2) as percentage
FROM daily_work_reports dwr FROM daily_work_reports dwr
LEFT JOIN workers w ON dwr.worker_id = w.worker_id LEFT JOIN workers w ON dwr.user_id = w.user_id
LEFT JOIN work_types wt ON dwr.work_type_id = wt.id LEFT JOIN work_types wt ON dwr.work_type_id = wt.id
LEFT JOIN projects p ON dwr.project_id = p.project_id LEFT JOIN projects p ON dwr.project_id = p.project_id
WHERE dwr.report_date BETWEEN ? AND ? WHERE dwr.report_date BETWEEN ? AND ?
GROUP BY dwr.worker_id, w.worker_name, dwr.work_type_id, wt.name, dwr.project_id, p.project_name GROUP BY dwr.user_id, w.worker_name, dwr.work_type_id, wt.name, dwr.project_id, p.project_name
HAVING totalHours > 0 HAVING totalHours > 0
ORDER BY dwr.worker_id, totalHours DESC ORDER BY dwr.user_id, totalHours DESC
`; `;
try { try {
const [results] = await this.db.execute(query, [startDate, endDate, startDate, endDate]); const [results] = await this.db.execute(query, [startDate, endDate, startDate, endDate]);
return results.map(row => ({ return results.map(row => ({
worker_id: row.worker_id, user_id: row.user_id,
worker_name: row.worker_name || `작업자 ${row.worker_id}`, worker_name: row.worker_name || `작업자 ${row.user_id}`,
work_type_id: row.work_type_id, work_type_id: row.work_type_id,
work_type_name: row.work_type_name || `작업유형 ${row.work_type_id}`, work_type_name: row.work_type_name || `작업유형 ${row.work_type_id}`,
project_id: row.project_id, project_id: row.project_id,

View File

@@ -26,7 +26,7 @@ const getAnalysis = async (startDate, endDate) => {
const summarySql = ` const summarySql = `
SELECT SELECT
COUNT(DISTINCT dwr.project_id) as totalProjects, COUNT(DISTINCT dwr.project_id) as totalProjects,
COUNT(DISTINCT dwr.worker_id) as totalworkers, COUNT(DISTINCT dwr.user_id) as totalworkers,
COUNT(DISTINCT dwr.task_id) as totalTasks, COUNT(DISTINCT dwr.task_id) as totalTasks,
SUM(${workHoursCalc}) as totalHours SUM(${workHoursCalc}) as totalHours
FROM DailyWorkReports dwr FROM DailyWorkReports dwr
@@ -35,7 +35,7 @@ const getAnalysis = async (startDate, endDate) => {
// 2. 프로젝트별 집계 쿼리 // 2. 프로젝트별 집계 쿼리
const byProjectSql = ` const byProjectSql = `
SELECT p.project_name as name, SUM(${workHoursCalc}) as hours, COUNT(DISTINCT dwr.worker_id) as participants SELECT p.project_name as name, SUM(${workHoursCalc}) as hours, COUNT(DISTINCT dwr.user_id) as participants
FROM DailyWorkReports dwr FROM DailyWorkReports dwr
JOIN projects p ON dwr.project_id = p.project_id JOIN projects p ON dwr.project_id = p.project_id
${whereClause} ${whereClause}
@@ -48,7 +48,7 @@ const getAnalysis = async (startDate, endDate) => {
const byWorkerSql = ` const byWorkerSql = `
SELECT w.worker_name as name, SUM(${workHoursCalc}) as hours, COUNT(DISTINCT dwr.project_id) as participants SELECT w.worker_name as name, SUM(${workHoursCalc}) as hours, COUNT(DISTINCT dwr.project_id) as participants
FROM DailyWorkReports dwr FROM DailyWorkReports dwr
JOIN workers w ON dwr.worker_id = w.worker_id JOIN workers w ON dwr.user_id = w.user_id
${whereClause} ${whereClause}
GROUP BY w.worker_name GROUP BY w.worker_name
HAVING hours > 0 HAVING hours > 0
@@ -57,7 +57,7 @@ const getAnalysis = async (startDate, endDate) => {
// 4. 작업별 집계 쿼리 // 4. 작업별 집계 쿼리
const byTaskSql = ` const byTaskSql = `
SELECT t.category as name, SUM(${workHoursCalc}) as hours, COUNT(DISTINCT dwr.worker_id) as participants SELECT t.category as name, SUM(${workHoursCalc}) as hours, COUNT(DISTINCT dwr.user_id) as participants
FROM DailyWorkReports dwr FROM DailyWorkReports dwr
JOIN Tasks t ON dwr.task_id = t.task_id JOIN Tasks t ON dwr.task_id = t.task_id
${whereClause} ${whereClause}
@@ -74,7 +74,7 @@ const getAnalysis = async (startDate, endDate) => {
(${workHoursCalc}) as work_hours, dwr.memo (${workHoursCalc}) as work_hours, dwr.memo
FROM DailyWorkReports dwr FROM DailyWorkReports dwr
JOIN projects p ON dwr.project_id = p.project_id JOIN projects p ON dwr.project_id = p.project_id
JOIN workers w ON dwr.worker_id = w.worker_id JOIN workers w ON dwr.user_id = w.user_id
JOIN Tasks t ON dwr.task_id = t.task_id JOIN Tasks t ON dwr.task_id = t.task_id
${whereClause} ${whereClause}
HAVING work_hours > 0 HAVING work_hours > 0

View File

@@ -2,7 +2,7 @@ const { getDb } = require('../dbPool');
class AttendanceModel { class AttendanceModel {
// 일일 근태 기록 조회 // 일일 근태 기록 조회
static async getDailyAttendanceRecords(date, workerId = null) { static async getDailyAttendanceRecords(date, userId = null) {
const db = await getDb(); const db = await getDb();
let query = ` let query = `
SELECT SELECT
@@ -15,7 +15,7 @@ class AttendanceModel {
vt.type_code as vacation_type_code, vt.type_code as vacation_type_code,
vt.deduct_days as vacation_days vt.deduct_days as vacation_days
FROM daily_attendance_records dar FROM daily_attendance_records dar
LEFT JOIN workers w ON dar.worker_id = w.worker_id LEFT JOIN workers w ON dar.user_id = w.user_id
LEFT JOIN work_attendance_types wat ON dar.attendance_type_id = wat.id LEFT JOIN work_attendance_types wat ON dar.attendance_type_id = wat.id
LEFT JOIN vacation_types vt ON dar.vacation_type_id = vt.id LEFT JOIN vacation_types vt ON dar.vacation_type_id = vt.id
WHERE dar.record_date = ? WHERE dar.record_date = ?
@@ -23,9 +23,9 @@ class AttendanceModel {
const params = [date]; const params = [date];
if (workerId) { if (userId) {
query += ' AND dar.worker_id = ?'; query += ' AND dar.user_id = ?';
params.push(workerId); params.push(userId);
} }
query += ' ORDER BY w.worker_name'; query += ' ORDER BY w.worker_name';
@@ -35,7 +35,7 @@ class AttendanceModel {
} }
// 기간별 근태 기록 조회 (월별 조회용) // 기간별 근태 기록 조회 (월별 조회용)
static async getDailyRecords(startDate, endDate, workerId = null) { static async getDailyRecords(startDate, endDate, userId = null) {
const db = await getDb(); const db = await getDb();
let query = ` let query = `
SELECT SELECT
@@ -48,7 +48,7 @@ class AttendanceModel {
vt.type_code as vacation_type_code, vt.type_code as vacation_type_code,
vt.deduct_days as vacation_days vt.deduct_days as vacation_days
FROM daily_attendance_records dar FROM daily_attendance_records dar
LEFT JOIN workers w ON dar.worker_id = w.worker_id LEFT JOIN workers w ON dar.user_id = w.user_id
LEFT JOIN work_attendance_types wat ON dar.attendance_type_id = wat.id LEFT JOIN work_attendance_types wat ON dar.attendance_type_id = wat.id
LEFT JOIN vacation_types vt ON dar.vacation_type_id = vt.id LEFT JOIN vacation_types vt ON dar.vacation_type_id = vt.id
WHERE dar.record_date BETWEEN ? AND ? WHERE dar.record_date BETWEEN ? AND ?
@@ -56,9 +56,9 @@ class AttendanceModel {
const params = [startDate, endDate]; const params = [startDate, endDate];
if (workerId) { if (userId) {
query += ' AND dar.worker_id = ?'; query += ' AND dar.user_id = ?';
params.push(workerId); params.push(userId);
} }
query += ' ORDER BY dar.record_date ASC'; query += ' ORDER BY dar.record_date ASC';
@@ -68,7 +68,7 @@ class AttendanceModel {
} }
// 작업 보고서와 근태 기록 동기화 (시간 합산 및 상태 업데이트) // 작업 보고서와 근태 기록 동기화 (시간 합산 및 상태 업데이트)
static async syncWithWorkReports(workerId, date) { static async syncWithWorkReports(userId, date) {
const db = await getDb(); const db = await getDb();
// 1. 해당 날짜의 총 작업 시간 계산 // 1. 해당 날짜의 총 작업 시간 계산
@@ -77,8 +77,8 @@ class AttendanceModel {
COALESCE(SUM(work_hours), 0) as total_hours, COALESCE(SUM(work_hours), 0) as total_hours,
COUNT(*) as report_count COUNT(*) as report_count
FROM daily_work_reports FROM daily_work_reports
WHERE worker_id = ? AND report_date = ? WHERE user_id = ? AND report_date = ?
`, [workerId, date]); `, [userId, date]);
const totalHours = parseFloat(reportStats[0].total_hours || 0); const totalHours = parseFloat(reportStats[0].total_hours || 0);
const reportCount = reportStats[0].report_count; const reportCount = reportStats[0].report_count;
@@ -110,8 +110,8 @@ class AttendanceModel {
// 3. 기록 업데이트 (휴가 정보는 유지) // 3. 기록 업데이트 (휴가 정보는 유지)
// 기존 기록 조회 // 기존 기록 조회
const [existing] = await db.execute( const [existing] = await db.execute(
'SELECT id, vacation_type_id FROM daily_attendance_records WHERE worker_id = ? AND record_date = ?', 'SELECT id, vacation_type_id FROM daily_attendance_records WHERE user_id = ? AND record_date = ?',
[workerId, date] [userId, date]
); );
if (existing.length > 0) { if (existing.length > 0) {
@@ -138,9 +138,9 @@ class AttendanceModel {
// 생성자가 명확하지 않으므로 시스템(1) 또는 알 수 없음 처리 // 생성자가 명확하지 않으므로 시스템(1) 또는 알 수 없음 처리
await db.execute(` await db.execute(`
INSERT INTO daily_attendance_records INSERT INTO daily_attendance_records
(record_date, worker_id, total_work_hours, attendance_type_id, status, created_by) (record_date, user_id, total_work_hours, attendance_type_id, status, created_by)
VALUES (?, ?, ?, ?, ?, 1) VALUES (?, ?, ?, ?, ?, 1)
`, [date, workerId, totalHours, typeId, status]); `, [date, userId, totalHours, typeId, status]);
return { synced: true, totalHours, status, created: true }; return { synced: true, totalHours, status, created: true };
} }
@@ -152,14 +152,14 @@ class AttendanceModel {
// 1. 활성 작업자 조회 // 1. 활성 작업자 조회
const [workers] = await db.execute( const [workers] = await db.execute(
'SELECT worker_id FROM workers WHERE status = "active"' // is_active check not needed as status covers it based on previous fix? Wait, previous fix used status='active'. 'SELECT user_id FROM workers WHERE status = "active" AND user_id IS NOT NULL'
); );
if (workers.length === 0) return { inserted: 0 }; if (workers.length === 0) return { inserted: 0 };
// 2. 일일 근태 레코드 일괄 생성 (이미 존재하면 무시) // 2. 일일 근태 레코드 일괄 생성 (이미 존재하면 무시)
// VALUES (...), (...), ... // VALUES (...), (...), ...
const values = workers.map(w => [date, w.worker_id, 'incomplete', createdBy]); const values = workers.map(w => [date, w.user_id, 'incomplete', createdBy]);
// Bulk INSERT IGNORE // Bulk INSERT IGNORE
// Note: mysql2 execute doesn't support nested arrays for bulk insert easily with placeholder ? // Note: mysql2 execute doesn't support nested arrays for bulk insert easily with placeholder ?
@@ -176,9 +176,9 @@ class AttendanceModel {
for (const w of workers) { for (const w of workers) {
const [result] = await conn.execute(` const [result] = await conn.execute(`
INSERT IGNORE INTO daily_attendance_records INSERT IGNORE INTO daily_attendance_records
(record_date, worker_id, status, created_by) (record_date, user_id, status, created_by)
VALUES (?, ?, 'incomplete', ?) VALUES (?, ?, 'incomplete', ?)
`, [date, w.worker_id, createdBy]); `, [date, w.user_id, createdBy]);
insertedCount += result.affectedRows; insertedCount += result.affectedRows;
} }
@@ -200,7 +200,7 @@ class AttendanceModel {
const { const {
record_date, record_date,
worker_id, user_id,
total_work_hours = 8, total_work_hours = 8,
work_attendance_type_id = 1, work_attendance_type_id = 1,
vacation_type_id = null, vacation_type_id = null,
@@ -212,8 +212,8 @@ class AttendanceModel {
// 기존 기록 확인 // 기존 기록 확인
const [existing] = await db.execute( const [existing] = await db.execute(
'SELECT id FROM daily_attendance_records WHERE worker_id = ? AND record_date = ?', 'SELECT id FROM daily_attendance_records WHERE user_id = ? AND record_date = ?',
[worker_id, record_date] [user_id, record_date]
); );
if (existing.length > 0) { if (existing.length > 0) {
@@ -240,12 +240,12 @@ class AttendanceModel {
// 생성 // 생성
const [result] = await db.execute(` const [result] = await db.execute(`
INSERT INTO daily_attendance_records ( INSERT INTO daily_attendance_records (
record_date, worker_id, total_work_hours, attendance_type_id, record_date, user_id, total_work_hours, attendance_type_id,
vacation_type_id, is_overtime_approved, created_by vacation_type_id, is_overtime_approved, created_by
) VALUES (?, ?, ?, ?, ?, ?, ?) ) VALUES (?, ?, ?, ?, ?, ?, ?)
`, [ `, [
record_date, record_date,
worker_id, user_id,
total_work_hours, total_work_hours,
attendance_type_id, attendance_type_id,
vacation_type_id, vacation_type_id,
@@ -264,7 +264,7 @@ class AttendanceModel {
// 모든 작업자와 해당 날짜의 근태 기록을 조회 // 모든 작업자와 해당 날짜의 근태 기록을 조회
const [rows] = await db.execute(` const [rows] = await db.execute(`
SELECT SELECT
w.worker_id, w.user_id,
w.worker_name, w.worker_name,
w.job_type, w.job_type,
COALESCE(dar.total_work_hours, 0) as total_work_hours, COALESCE(dar.total_work_hours, 0) as total_work_hours,
@@ -278,15 +278,15 @@ class AttendanceModel {
dar.notes, dar.notes,
-- 작업 건수 계산 -- 작업 건수 계산
(SELECT COUNT(*) FROM daily_work_reports dwr (SELECT COUNT(*) FROM daily_work_reports dwr
WHERE dwr.worker_id = w.worker_id AND dwr.report_date = ?) as work_count, WHERE dwr.user_id = w.user_id AND dwr.report_date = ?) as work_count,
-- 오류 건수 계산 -- 오류 건수 계산
(SELECT COUNT(*) FROM daily_work_reports dwr (SELECT COUNT(*) FROM daily_work_reports dwr
WHERE dwr.worker_id = w.worker_id AND dwr.report_date = ? AND dwr.work_status_id = 2) as error_count WHERE dwr.user_id = w.user_id AND dwr.report_date = ? AND dwr.work_status_id = 2) as error_count
FROM workers w FROM workers w
LEFT JOIN daily_attendance_records dar ON w.worker_id = dar.worker_id AND dar.record_date = ? LEFT JOIN daily_attendance_records dar ON w.user_id = dar.user_id AND dar.record_date = ?
LEFT JOIN work_attendance_types wat ON dar.attendance_type_id = wat.id LEFT JOIN work_attendance_types wat ON dar.attendance_type_id = wat.id
LEFT JOIN vacation_types vt ON dar.vacation_type_id = vt.id LEFT JOIN vacation_types vt ON dar.vacation_type_id = vt.id
WHERE w.is_active = TRUE WHERE w.status = 'active' AND w.user_id IS NOT NULL
ORDER BY w.worker_name ORDER BY w.worker_name
`, [date, date, date]); `, [date, date, date]);
@@ -294,7 +294,7 @@ class AttendanceModel {
} }
// 휴가 처리 // 휴가 처리
static async processVacation(workerId, date, vacationType, createdBy) { static async processVacation(userId, date, vacationType, createdBy) {
const db = await getDb(); const db = await getDb();
// 휴가 유형 정보 조회 // 휴가 유형 정보 조회
@@ -313,8 +313,8 @@ class AttendanceModel {
const [workHours] = await db.execute(` const [workHours] = await db.execute(`
SELECT COALESCE(SUM(work_hours), 0) as total_hours SELECT COALESCE(SUM(work_hours), 0) as total_hours
FROM daily_work_reports FROM daily_work_reports
WHERE worker_id = ? AND report_date = ? WHERE user_id = ? AND report_date = ?
`, [workerId, date]); `, [userId, date]);
const currentHours = parseFloat(workHours[0].total_hours); const currentHours = parseFloat(workHours[0].total_hours);
// deduct_days를 시간으로 변환 (1일 = 8시간) // deduct_days를 시간으로 변환 (1일 = 8시간)
@@ -338,12 +338,12 @@ class AttendanceModel {
// 휴가 작업 기록 생성 (프로젝트 ID 13 = "연차/휴무", work_type_id 1 = 기본) // 휴가 작업 기록 생성 (프로젝트 ID 13 = "연차/휴무", work_type_id 1 = 기본)
await db.execute(` await db.execute(`
INSERT INTO daily_work_reports ( INSERT INTO daily_work_reports (
report_date, worker_id, project_id, work_type_id, work_status_id, report_date, user_id, project_id, work_type_id, work_status_id,
work_hours, description, created_by work_hours, description, created_by
) VALUES (?, ?, 13, 1, 1, ?, ?, ?) ) VALUES (?, ?, 13, 1, 1, ?, ?, ?)
`, [ `, [
date, date,
workerId, userId,
vacationHours, vacationHours,
`${vacationTypeInfo.type_name} 처리`, `${vacationTypeInfo.type_name} 처리`,
createdBy createdBy
@@ -352,7 +352,7 @@ class AttendanceModel {
// 근태 기록 업데이트 // 근태 기록 업데이트
const attendanceData = { const attendanceData = {
record_date: date, record_date: date,
worker_id: workerId, user_id: userId,
total_work_hours: totalHours, total_work_hours: totalHours,
work_attendance_type_id: attendanceTypes[0]?.id, work_attendance_type_id: attendanceTypes[0]?.id,
vacation_type_id: vacationTypeInfo.id, vacation_type_id: vacationTypeInfo.id,
@@ -364,7 +364,7 @@ class AttendanceModel {
} }
// 초과근무 승인 // 초과근무 승인
static async approveOvertime(workerId, date, approvedBy) { static async approveOvertime(userId, date, approvedBy) {
const db = await getDb(); const db = await getDb();
const [result] = await db.execute(` const [result] = await db.execute(`
@@ -375,8 +375,8 @@ class AttendanceModel {
overtime_approved_at = CURRENT_TIMESTAMP, overtime_approved_at = CURRENT_TIMESTAMP,
updated_by = ?, updated_by = ?,
updated_at = CURRENT_TIMESTAMP updated_at = CURRENT_TIMESTAMP
WHERE worker_id = ? AND record_date = ? WHERE user_id = ? AND record_date = ?
`, [approvedBy, approvedBy, workerId, date]); `, [approvedBy, approvedBy, userId, date]);
return result.affectedRows > 0; return result.affectedRows > 0;
} }
@@ -400,24 +400,24 @@ class AttendanceModel {
} }
// 작업자 휴가 잔여 조회 // 작업자 휴가 잔여 조회
static async getWorkerVacationBalance(workerId, year = null) { static async getWorkerVacationBalance(userId, year = null) {
const db = await getDb(); const db = await getDb();
const currentYear = year || new Date().getFullYear(); const currentYear = year || new Date().getFullYear();
const [rows] = await db.execute(` const [rows] = await db.execute(`
SELECT id, worker_id, year, total_annual_leave, used_annual_leave, notes, created_at, updated_at FROM worker_vacation_balance SELECT id, user_id, year, total_annual_leave, used_annual_leave, notes, created_at, updated_at FROM worker_vacation_balance
WHERE worker_id = ? AND year = ? WHERE user_id = ? AND year = ?
`, [workerId, currentYear]); `, [userId, currentYear]);
if (rows.length === 0) { if (rows.length === 0) {
// 기본 연차 생성 (15일) // 기본 연차 생성 (15일)
await db.execute(` await db.execute(`
INSERT INTO worker_vacation_balance (worker_id, year, total_annual_leave) INSERT INTO worker_vacation_balance (user_id, year, total_annual_leave)
VALUES (?, ?, 15.0) VALUES (?, ?, 15.0)
`, [workerId, currentYear]); `, [userId, currentYear]);
return { return {
worker_id: workerId, user_id: userId,
year: currentYear, year: currentYear,
total_annual_leave: 15.0, total_annual_leave: 15.0,
used_annual_leave: 0, used_annual_leave: 0,
@@ -429,14 +429,14 @@ class AttendanceModel {
} }
// 월별 근태 통계 // 월별 근태 통계
static async getMonthlyAttendanceStats(year, month, workerId = null) { static async getMonthlyAttendanceStats(year, month, userId = null) {
const db = await getDb(); const db = await getDb();
// work_attendance_types: 1=NORMAL, 2=LATE, 3=EARLY_LEAVE, 4=ABSENT, 5=VACATION // work_attendance_types: 1=NORMAL, 2=LATE, 3=EARLY_LEAVE, 4=ABSENT, 5=VACATION
// vacation_types: 1=ANNUAL(연차), 2=HALF_ANNUAL(반차), 3=SICK(병가), 4=SPECIAL(경조사) // vacation_types: 1=ANNUAL(연차), 2=HALF_ANNUAL(반차), 3=SICK(병가), 4=SPECIAL(경조사)
let query = ` let query = `
SELECT SELECT
w.worker_id, w.user_id,
w.worker_name, w.worker_name,
COUNT(CASE WHEN dar.attendance_type_id = 1 AND (dar.is_overtime_approved = 0 OR dar.is_overtime_approved IS NULL) THEN 1 END) as regular_days, COUNT(CASE WHEN dar.attendance_type_id = 1 AND (dar.is_overtime_approved = 0 OR dar.is_overtime_approved IS NULL) THEN 1 END) as regular_days,
COUNT(CASE WHEN dar.is_overtime_approved = 1 OR dar.total_work_hours > 8 THEN 1 END) as overtime_days, COUNT(CASE WHEN dar.is_overtime_approved = 1 OR dar.total_work_hours > 8 THEN 1 END) as overtime_days,
@@ -446,19 +446,19 @@ class AttendanceModel {
COALESCE(SUM(dar.total_work_hours), 0) as total_work_hours, COALESCE(SUM(dar.total_work_hours), 0) as total_work_hours,
COALESCE(AVG(dar.total_work_hours), 0) as avg_work_hours COALESCE(AVG(dar.total_work_hours), 0) as avg_work_hours
FROM workers w FROM workers w
LEFT JOIN daily_attendance_records dar ON w.worker_id = dar.worker_id LEFT JOIN daily_attendance_records dar ON w.user_id = dar.user_id
AND YEAR(dar.record_date) = ? AND MONTH(dar.record_date) = ? AND YEAR(dar.record_date) = ? AND MONTH(dar.record_date) = ?
WHERE w.employment_status = 'employed' WHERE w.employment_status = 'employed'
`; `;
const params = [year, month]; const params = [year, month];
if (workerId) { if (userId) {
query += ' AND w.worker_id = ?'; query += ' AND w.user_id = ?';
params.push(workerId); params.push(userId);
} }
query += ' GROUP BY w.worker_id, w.worker_name ORDER BY w.worker_name'; query += ' GROUP BY w.user_id, w.worker_name ORDER BY w.worker_name';
const [rows] = await db.execute(query, params); const [rows] = await db.execute(query, params);
return rows; return rows;
@@ -467,12 +467,12 @@ class AttendanceModel {
// 출근 체크 기록 생성 또는 업데이트 // 출근 체크 기록 생성 또는 업데이트
static async upsertCheckin(checkinData) { static async upsertCheckin(checkinData) {
const db = await getDb(); const db = await getDb();
const { worker_id, record_date, is_present } = checkinData; const { user_id, record_date, is_present } = checkinData;
// 해당 날짜에 기록이 있는지 확인 // 해당 날짜에 기록이 있는지 확인
const [existing] = await db.execute( const [existing] = await db.execute(
'SELECT id FROM daily_attendance_records WHERE worker_id = ? AND record_date = ?', 'SELECT id FROM daily_attendance_records WHERE user_id = ? AND record_date = ?',
[worker_id, record_date] [user_id, record_date]
); );
if (existing.length > 0) { if (existing.length > 0) {
@@ -486,9 +486,9 @@ class AttendanceModel {
// 새로 생성 (기본값으로) // 새로 생성 (기본값으로)
const [result] = await db.execute( const [result] = await db.execute(
`INSERT INTO daily_attendance_records `INSERT INTO daily_attendance_records
(worker_id, record_date, is_present, attendance_type_id, created_by) (user_id, record_date, is_present, attendance_type_id, created_by)
VALUES (?, ?, ?, 1, 1)`, VALUES (?, ?, ?, 1, 1)`,
[worker_id, record_date, is_present] [user_id, record_date, is_present]
); );
return result.insertId; return result.insertId;
} }
@@ -499,7 +499,7 @@ class AttendanceModel {
const db = await getDb(); const db = await getDb();
const query = ` const query = `
SELECT SELECT
w.worker_id, w.user_id,
w.worker_name, w.worker_name,
w.job_type, w.job_type,
w.employment_status, w.employment_status,
@@ -512,9 +512,9 @@ class AttendanceModel {
vr.days_used as vacation_days vr.days_used as vacation_days
FROM workers w FROM workers w
LEFT JOIN daily_attendance_records dar LEFT JOIN daily_attendance_records dar
ON w.worker_id = dar.worker_id AND dar.record_date = ? ON w.user_id = dar.user_id AND dar.record_date = ?
LEFT JOIN vacation_requests vr LEFT JOIN vacation_requests vr
ON w.worker_id = vr.worker_id ON w.user_id = vr.user_id
AND ? BETWEEN vr.start_date AND vr.end_date AND ? BETWEEN vr.start_date AND vr.end_date
AND vr.status = 'approved' AND vr.status = 'approved'
LEFT JOIN vacation_types vt ON vr.vacation_type_id = vt.id LEFT JOIN vacation_types vt ON vr.vacation_type_id = vt.id

View File

@@ -12,13 +12,13 @@ const createMany = async (reports) => {
const insertedIds = []; const insertedIds = [];
const sql = ` const sql = `
INSERT INTO DailyIssueReports INSERT INTO DailyIssueReports
(date, worker_id, project_id, start_time, end_time, issue_type_id) (date, user_id, project_id, start_time, end_time, issue_type_id)
VALUES (?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?)
`; `;
for (const report of reports) { for (const report of reports) {
const { date, worker_id, project_id, start_time, end_time, issue_type_id } = report; const { date, user_id, project_id, start_time, end_time, issue_type_id } = report;
const [result] = await conn.query(sql, [date, worker_id, project_id, start_time, end_time, issue_type_id]); const [result] = await conn.query(sql, [date, user_id, project_id, start_time, end_time, issue_type_id]);
insertedIds.push(result.insertId); insertedIds.push(result.insertId);
} }
@@ -43,7 +43,7 @@ const getAllByDate = async (date) => {
d.id, d.date, w.worker_name, p.project_name, d.start_time, d.end_time, d.id, d.date, w.worker_name, p.project_name, d.start_time, d.end_time,
t.category, t.subcategory, d.description t.category, t.subcategory, d.description
FROM DailyIssueReports d FROM DailyIssueReports d
LEFT JOIN workers w ON d.worker_id = w.worker_id LEFT JOIN workers w ON d.user_id = w.user_id
LEFT JOIN projects p ON d.project_id = p.project_id LEFT JOIN projects p ON d.project_id = p.project_id
LEFT JOIN IssueTypes t ON d.issue_type_id = t.issue_type_id LEFT JOIN IssueTypes t ON d.issue_type_id = t.issue_type_id
WHERE d.date = ? WHERE d.date = ?
@@ -58,7 +58,7 @@ const getAllByDate = async (date) => {
*/ */
const getById = async (id) => { const getById = async (id) => {
const db = await getDb(); const db = await getDb();
const [rows] = await db.query(`SELECT id, date, worker_id, project_id, issue_type_id, description, created_at, start_time, end_time FROM DailyIssueReports WHERE id = ?`, [id]); const [rows] = await db.query(`SELECT id, date, user_id, project_id, issue_type_id, description, created_at, start_time, end_time FROM DailyIssueReports WHERE id = ?`, [id]);
return rows[0]; return rows[0];
}; };

View File

@@ -39,22 +39,22 @@ const getAllErrorTypes = async () => {
* 누적 추가 전용 함수 (createDailyReport 대체) - 절대 삭제 안함! * 누적 추가 전용 함수 (createDailyReport 대체) - 절대 삭제 안함!
*/ */
const createDailyReport = async (reportData) => { const createDailyReport = async (reportData) => {
const { report_date, worker_id, work_entries, created_by, created_by_name, total_hours } = reportData; const { report_date, user_id, work_entries, created_by, created_by_name, total_hours } = reportData;
const db = await getDb(); const db = await getDb();
const conn = await db.getConnection(); const conn = await db.getConnection();
try { try {
await conn.beginTransaction(); await conn.beginTransaction();
console.log(`${created_by_name}${report_date} ${worker_id}번 작업자에게 데이터 추가 중...`); console.log(`${created_by_name}${report_date} ${user_id}번 작업자에게 데이터 추가 중...`);
const [existingReports] = await conn.query( const [existingReports] = await conn.query(
`SELECT dwr.created_by, u.name as created_by_name, COUNT(*) as count, SUM(dwr.work_hours) as total_hours `SELECT dwr.created_by, u.name as created_by_name, COUNT(*) as count, SUM(dwr.work_hours) as total_hours
FROM daily_work_reports dwr FROM daily_work_reports dwr
LEFT JOIN users u ON dwr.created_by = u.user_id LEFT JOIN sso_users u ON dwr.created_by = u.user_id
WHERE dwr.report_date = ? AND dwr.worker_id = ? WHERE dwr.report_date = ? AND dwr.user_id = ?
GROUP BY dwr.created_by`, GROUP BY dwr.created_by`,
[report_date, worker_id] [report_date, user_id]
); );
console.log('기존 데이터 (삭제하지 않음):', existingReports); console.log('기존 데이터 (삭제하지 않음):', existingReports);
@@ -66,9 +66,9 @@ const createDailyReport = async (reportData) => {
const [insertResult] = await conn.query( const [insertResult] = await conn.query(
`INSERT INTO daily_work_reports `INSERT INTO daily_work_reports
(report_date, worker_id, project_id, work_type_id, work_status_id, error_type_id, work_hours, created_by, created_at) (report_date, user_id, project_id, work_type_id, work_status_id, error_type_id, work_hours, created_by, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, NOW())`, VALUES (?, ?, ?, ?, ?, ?, ?, ?, NOW())`,
[report_date, worker_id, project_id, work_type_id, work_status_id || 1, error_type_id || null, work_hours, created_by] [report_date, user_id, project_id, work_type_id, work_status_id || 1, error_type_id || null, work_hours, created_by]
); );
insertedIds.push(insertResult.insertId); insertedIds.push(insertResult.insertId);
@@ -77,10 +77,10 @@ const createDailyReport = async (reportData) => {
const [finalReports] = await conn.query( const [finalReports] = await conn.query(
`SELECT dwr.created_by, u.name as created_by_name, COUNT(*) as count, SUM(dwr.work_hours) as total_hours `SELECT dwr.created_by, u.name as created_by_name, COUNT(*) as count, SUM(dwr.work_hours) as total_hours
FROM daily_work_reports dwr FROM daily_work_reports dwr
LEFT JOIN users u ON dwr.created_by = u.user_id LEFT JOIN sso_users u ON dwr.created_by = u.user_id
WHERE dwr.report_date = ? AND dwr.worker_id = ? WHERE dwr.report_date = ? AND dwr.user_id = ?
GROUP BY dwr.created_by`, GROUP BY dwr.created_by`,
[report_date, worker_id] [report_date, user_id]
); );
const grandTotal = finalReports.reduce((sum, report) => sum + parseFloat(report.total_hours || 0), 0); const grandTotal = finalReports.reduce((sum, report) => sum + parseFloat(report.total_hours || 0), 0);
@@ -103,7 +103,7 @@ const createDailyReport = async (reportData) => {
insertedIds[0] || null, insertedIds[0] || null,
JSON.stringify({ JSON.stringify({
report_date, report_date,
worker_id, user_id,
work_entries_count: work_entries.length, work_entries_count: work_entries.length,
added_hours: total_hours, added_hours: total_hours,
my_total: myTotal, my_total: myTotal,
@@ -123,7 +123,7 @@ const createDailyReport = async (reportData) => {
// 근태 기록 동기화 // 근태 기록 동기화
try { try {
const AttendanceModel = require('./attendanceModel'); const AttendanceModel = require('./attendanceModel');
await AttendanceModel.syncWithWorkReports(worker_id, report_date); await AttendanceModel.syncWithWorkReports(user_id, report_date);
} catch (syncErr) { } catch (syncErr) {
console.error('근태 기록 동기화 실패:', syncErr); console.error('근태 기록 동기화 실패:', syncErr);
} }
@@ -153,7 +153,7 @@ const createDailyReport = async (reportData) => {
/** /**
* 특정 날짜 + 작업자 + 작성자의 누적 현황 조회 * 특정 날짜 + 작업자 + 작성자의 누적 현황 조회
*/ */
const getMyAccumulatedHours = async (date, worker_id, created_by) => { const getMyAccumulatedHours = async (date, user_id, created_by) => {
const db = await getDb(); const db = await getDb();
const sql = ` const sql = `
@@ -166,32 +166,32 @@ const getMyAccumulatedHours = async (date, worker_id, created_by) => {
) as my_entries ) as my_entries
FROM daily_work_reports dwr FROM daily_work_reports dwr
LEFT JOIN projects p ON dwr.project_id = p.project_id LEFT JOIN projects p ON dwr.project_id = p.project_id
WHERE dwr.report_date = ? AND dwr.worker_id = ? AND dwr.created_by = ? WHERE dwr.report_date = ? AND dwr.user_id = ? AND dwr.created_by = ?
`; `;
const [rows] = await db.query(sql, [date, worker_id, created_by]); const [rows] = await db.query(sql, [date, user_id, created_by]);
return rows[0] || { my_total_hours: 0, my_entry_count: 0, my_entries: null }; return rows[0] || { my_total_hours: 0, my_entry_count: 0, my_entries: null };
}; };
/** /**
* 누적 현황 조회 - 날짜+작업자별 (모든 기여자) * 누적 현황 조회 - 날짜+작업자별 (모든 기여자)
*/ */
const getAccumulatedReportsByDate = async (date, worker_id) => { const getAccumulatedReportsByDate = async (date, user_id) => {
const db = await getDb(); const db = await getDb();
const sql = getSelectQuery() + ` const sql = getSelectQuery() + `
WHERE dwr.report_date = ? AND dwr.worker_id = ? WHERE dwr.report_date = ? AND dwr.user_id = ?
ORDER BY dwr.created_by, dwr.created_at ASC ORDER BY dwr.created_by, dwr.created_at ASC
`; `;
const [rows] = await db.query(sql, [date, worker_id]); const [rows] = await db.query(sql, [date, user_id]);
return rows; return rows;
}; };
/** /**
* 기여자별 요약 조회 * 기여자별 요약 조회
*/ */
const getContributorsByDate = async (date, worker_id) => { const getContributorsByDate = async (date, user_id) => {
const db = await getDb(); const db = await getDb();
const sql = ` const sql = `
@@ -207,14 +207,14 @@ const getContributorsByDate = async (date, worker_id) => {
ORDER BY dwr.created_at SEPARATOR ', ' ORDER BY dwr.created_at SEPARATOR ', '
) as entry_details ) as entry_details
FROM daily_work_reports dwr FROM daily_work_reports dwr
LEFT JOIN users u ON dwr.created_by = u.user_id LEFT JOIN sso_users u ON dwr.created_by = u.user_id
LEFT JOIN projects p ON dwr.project_id = p.project_id LEFT JOIN projects p ON dwr.project_id = p.project_id
WHERE dwr.report_date = ? AND dwr.worker_id = ? WHERE dwr.report_date = ? AND dwr.user_id = ?
GROUP BY dwr.created_by GROUP BY dwr.created_by
ORDER BY total_hours DESC, first_entry ASC ORDER BY total_hours DESC, first_entry ASC
`; `;
const [rows] = await db.query(sql, [date, worker_id]); const [rows] = await db.query(sql, [date, user_id]);
return rows; return rows;
}; };
@@ -232,9 +232,9 @@ const removeSpecificEntry = async (entry_id, deleted_by) => {
const [entryInfo] = await conn.query( const [entryInfo] = await conn.query(
`SELECT dwr.*, w.worker_name, p.project_name, u.name as created_by_name `SELECT dwr.*, w.worker_name, p.project_name, u.name as created_by_name
FROM daily_work_reports dwr FROM daily_work_reports dwr
LEFT JOIN workers w ON dwr.worker_id = w.worker_id LEFT JOIN workers w ON dwr.user_id = w.user_id
LEFT JOIN projects p ON dwr.project_id = p.project_id LEFT JOIN projects p ON dwr.project_id = p.project_id
LEFT JOIN users u ON dwr.created_by = u.user_id LEFT JOIN sso_users u ON dwr.created_by = u.user_id
WHERE dwr.id = ?`, WHERE dwr.id = ?`,
[entry_id] [entry_id]
); );
@@ -282,7 +282,7 @@ const getSelectQuery = () => `
SELECT SELECT
dwr.id, dwr.id,
dwr.report_date, dwr.report_date,
dwr.worker_id, dwr.user_id,
dwr.project_id, dwr.project_id,
dwr.work_type_id, dwr.work_type_id,
dwr.work_status_id, dwr.work_status_id,
@@ -299,13 +299,13 @@ const getSelectQuery = () => `
dwr.created_at, dwr.created_at,
dwr.updated_at dwr.updated_at
FROM daily_work_reports dwr FROM daily_work_reports dwr
LEFT JOIN workers w ON dwr.worker_id = w.worker_id LEFT JOIN workers w ON dwr.user_id = w.user_id
LEFT JOIN projects p ON dwr.project_id = p.project_id LEFT JOIN projects p ON dwr.project_id = p.project_id
LEFT JOIN work_types wt ON dwr.work_type_id = wt.id LEFT JOIN work_types wt ON dwr.work_type_id = wt.id
LEFT JOIN work_status_types wst ON dwr.work_status_id = wst.id LEFT JOIN work_status_types wst ON dwr.work_status_id = wst.id
LEFT JOIN issue_report_items iri ON dwr.error_type_id = iri.item_id LEFT JOIN issue_report_items iri ON dwr.error_type_id = iri.item_id
LEFT JOIN issue_report_categories irc ON iri.category_id = irc.category_id LEFT JOIN issue_report_categories irc ON iri.category_id = irc.category_id
LEFT JOIN users u ON dwr.created_by = u.user_id LEFT JOIN sso_users u ON dwr.created_by = u.user_id
`; `;
/** /**
@@ -347,26 +347,26 @@ const getByDateAndCreator = async (date, created_by) => {
/** /**
* 10. 일일 작업보고서 조회 (작업자별) * 10. 일일 작업보고서 조회 (작업자별)
*/ */
const getByWorker = async (worker_id) => { const getByWorker = async (user_id) => {
const db = await getDb(); const db = await getDb();
const sql = getSelectQuery() + ` const sql = getSelectQuery() + `
WHERE dwr.worker_id = ? WHERE dwr.user_id = ?
ORDER BY dwr.report_date DESC, dwr.id ASC ORDER BY dwr.report_date DESC, dwr.id ASC
`; `;
const [rows] = await db.query(sql, [worker_id]); const [rows] = await db.query(sql, [user_id]);
return rows; return rows;
}; };
/** /**
* 11. 일일 작업보고서 조회 (날짜 + 작업자) * 11. 일일 작업보고서 조회 (날짜 + 작업자)
*/ */
const getByDateAndWorker = async (date, worker_id) => { const getByDateAndWorker = async (date, user_id) => {
const db = await getDb(); const db = await getDb();
const sql = getSelectQuery() + ` const sql = getSelectQuery() + `
WHERE dwr.report_date = ? AND dwr.worker_id = ? WHERE dwr.report_date = ? AND dwr.user_id = ?
ORDER BY dwr.id ASC ORDER BY dwr.id ASC
`; `;
const [rows] = await db.query(sql, [date, worker_id]); const [rows] = await db.query(sql, [date, user_id]);
return rows; return rows;
}; };
@@ -387,7 +387,7 @@ const getByRange = async (start_date, end_date) => {
* 13. 상세 검색 (페이지네이션 포함) * 13. 상세 검색 (페이지네이션 포함)
*/ */
const searchWithDetails = async (params) => { const searchWithDetails = async (params) => {
const { start_date, end_date, worker_id, project_id, work_status_id, created_by, page, limit } = params; const { start_date, end_date, user_id, project_id, work_status_id, created_by, page, limit } = params;
const db = await getDb(); const db = await getDb();
@@ -395,9 +395,9 @@ const searchWithDetails = async (params) => {
let whereConditions = ['dwr.report_date BETWEEN ? AND ?']; let whereConditions = ['dwr.report_date BETWEEN ? AND ?'];
let queryParams = [start_date, end_date]; let queryParams = [start_date, end_date];
if (worker_id) { if (user_id) {
whereConditions.push('dwr.worker_id = ?'); whereConditions.push('dwr.user_id = ?');
queryParams.push(worker_id); queryParams.push(user_id);
} }
if (project_id) { if (project_id) {
@@ -448,16 +448,16 @@ const getSummaryByDate = async (date) => {
const sql = ` const sql = `
SELECT SELECT
dwr.worker_id, dwr.user_id,
w.worker_name, w.worker_name,
dwr.report_date, dwr.report_date,
SUM(dwr.work_hours) as total_hours, SUM(dwr.work_hours) as total_hours,
COUNT(*) as work_entries_count, COUNT(*) as work_entries_count,
SUM(CASE WHEN dwr.work_status_id = 2 THEN 1 ELSE 0 END) as error_count SUM(CASE WHEN dwr.work_status_id = 2 THEN 1 ELSE 0 END) as error_count
FROM daily_work_reports dwr FROM daily_work_reports dwr
LEFT JOIN workers w ON dwr.worker_id = w.worker_id LEFT JOIN workers w ON dwr.user_id = w.user_id
WHERE dwr.report_date = ? WHERE dwr.report_date = ?
GROUP BY dwr.worker_id, dwr.report_date GROUP BY dwr.user_id, dwr.report_date
ORDER BY w.worker_name ASC ORDER BY w.worker_name ASC
`; `;
const [rows] = await db.query(sql, [date]); const [rows] = await db.query(sql, [date]);
@@ -467,24 +467,24 @@ const getSummaryByDate = async (date) => {
/** /**
* 15. 일일 근무 요약 조회 (작업자별) * 15. 일일 근무 요약 조회 (작업자별)
*/ */
const getSummaryByWorker = async (worker_id) => { const getSummaryByWorker = async (user_id) => {
const db = await getDb(); const db = await getDb();
const sql = ` const sql = `
SELECT SELECT
dwr.report_date, dwr.report_date,
dwr.worker_id, dwr.user_id,
w.worker_name, w.worker_name,
SUM(dwr.work_hours) as total_hours, SUM(dwr.work_hours) as total_hours,
COUNT(*) as work_entries_count, COUNT(*) as work_entries_count,
SUM(CASE WHEN dwr.work_status_id = 2 THEN 1 ELSE 0 END) as error_count SUM(CASE WHEN dwr.work_status_id = 2 THEN 1 ELSE 0 END) as error_count
FROM daily_work_reports dwr FROM daily_work_reports dwr
LEFT JOIN workers w ON dwr.worker_id = w.worker_id LEFT JOIN workers w ON dwr.user_id = w.user_id
WHERE dwr.worker_id = ? WHERE dwr.user_id = ?
GROUP BY dwr.report_date, dwr.worker_id GROUP BY dwr.report_date, dwr.user_id
ORDER BY dwr.report_date DESC ORDER BY dwr.report_date DESC
`; `;
const [rows] = await db.query(sql, [worker_id]); const [rows] = await db.query(sql, [user_id]);
return rows; return rows;
}; };
@@ -499,7 +499,7 @@ const getMonthlySummary = async (year, month) => {
const sql = ` const sql = `
SELECT SELECT
dwr.report_date, dwr.report_date,
dwr.worker_id, dwr.user_id,
w.worker_name, w.worker_name,
SUM(dwr.work_hours) as total_work_hours, SUM(dwr.work_hours) as total_work_hours,
COUNT(DISTINCT dwr.project_id) as project_count, COUNT(DISTINCT dwr.project_id) as project_count,
@@ -508,11 +508,11 @@ const getMonthlySummary = async (year, month) => {
GROUP_CONCAT(DISTINCT p.project_name ORDER BY p.project_name) as projects, GROUP_CONCAT(DISTINCT p.project_name ORDER BY p.project_name) as projects,
GROUP_CONCAT(DISTINCT wt.name ORDER BY wt.name) as work_types GROUP_CONCAT(DISTINCT wt.name ORDER BY wt.name) as work_types
FROM daily_work_reports dwr FROM daily_work_reports dwr
LEFT JOIN workers w ON dwr.worker_id = w.worker_id LEFT JOIN workers w ON dwr.user_id = w.user_id
LEFT JOIN projects p ON dwr.project_id = p.project_id LEFT JOIN projects p ON dwr.project_id = p.project_id
LEFT JOIN work_types wt ON dwr.work_type_id = wt.id LEFT JOIN work_types wt ON dwr.work_type_id = wt.id
WHERE dwr.report_date BETWEEN ? AND ? WHERE dwr.report_date BETWEEN ? AND ?
GROUP BY dwr.report_date, dwr.worker_id GROUP BY dwr.report_date, dwr.user_id
ORDER BY dwr.report_date DESC, w.worker_name ASC ORDER BY dwr.report_date DESC, w.worker_name ASC
`; `;
const [rows] = await db.query(sql, [start, end]); const [rows] = await db.query(sql, [start, end]);
@@ -567,11 +567,11 @@ const updateById = async (id, updateData) => {
// [Sync] 근태 기록 동기화 // [Sync] 근태 기록 동기화
try { try {
const [targetReport] = await db.query('SELECT worker_id, report_date FROM daily_work_reports WHERE id = ?', [id]); const [targetReport] = await db.query('SELECT user_id, report_date FROM daily_work_reports WHERE id = ?', [id]);
if (targetReport.length > 0) { if (targetReport.length > 0) {
const { worker_id, report_date } = targetReport[0]; const { user_id, report_date } = targetReport[0];
const AttendanceModel = require('./attendanceModel'); const AttendanceModel = require('./attendanceModel');
await AttendanceModel.syncWithWorkReports(worker_id, report_date); await AttendanceModel.syncWithWorkReports(user_id, report_date);
} }
} catch (syncErr) { } catch (syncErr) {
console.error('근태 기록 동기화 실패 (Update):', syncErr); console.error('근태 기록 동기화 실패 (Update):', syncErr);
@@ -615,9 +615,9 @@ const removeById = async (id, deletedBy) => {
// [Sync] 근태 기록 동기화 // [Sync] 근태 기록 동기화
if (reportInfo.length > 0) { if (reportInfo.length > 0) {
try { try {
const { worker_id, report_date } = reportInfo[0]; const { user_id, report_date } = reportInfo[0];
const AttendanceModel = require('./attendanceModel'); const AttendanceModel = require('./attendanceModel');
await AttendanceModel.syncWithWorkReports(worker_id, report_date); await AttendanceModel.syncWithWorkReports(user_id, report_date);
} catch (syncErr) { } catch (syncErr) {
console.error('근태 기록 동기화 실패 (Delete):', syncErr); console.error('근태 기록 동기화 실패 (Delete):', syncErr);
} }
@@ -635,7 +635,7 @@ const removeById = async (id, deletedBy) => {
/** /**
* 19. 작업자의 특정 날짜 전체 삭제 * 19. 작업자의 특정 날짜 전체 삭제
*/ */
const removeByDateAndWorker = async (date, worker_id, deletedBy) => { const removeByDateAndWorker = async (date, user_id, deletedBy) => {
const db = await getDb(); const db = await getDb();
const conn = await db.getConnection(); const conn = await db.getConnection();
@@ -644,14 +644,14 @@ const removeByDateAndWorker = async (date, worker_id, deletedBy) => {
// 삭제 전 정보 저장 (감사 로그용) // 삭제 전 정보 저장 (감사 로그용)
const [reportInfos] = await conn.query( const [reportInfos] = await conn.query(
'SELECT id, report_date, worker_id, project_id, work_type_id, work_status_id, error_type_id, work_hours, created_at, updated_at, created_by, updated_by FROM daily_work_reports WHERE report_date = ? AND worker_id = ?', 'SELECT id, report_date, user_id, project_id, work_type_id, work_status_id, error_type_id, work_hours, created_at, updated_at, created_by, updated_by FROM daily_work_reports WHERE report_date = ? AND user_id = ?',
[date, worker_id] [date, user_id]
); );
// 작업보고서 삭제 // 작업보고서 삭제
const [result] = await conn.query( const [result] = await conn.query(
'DELETE FROM daily_work_reports WHERE report_date = ? AND worker_id = ?', 'DELETE FROM daily_work_reports WHERE report_date = ? AND user_id = ?',
[date, worker_id] [date, user_id]
); );
// 감사 로그 추가 // 감사 로그 추가
@@ -673,7 +673,7 @@ const removeByDateAndWorker = async (date, worker_id, deletedBy) => {
// [Sync] 근태 기록 동기화 // [Sync] 근태 기록 동기화
try { try {
const AttendanceModel = require('./attendanceModel'); const AttendanceModel = require('./attendanceModel');
await AttendanceModel.syncWithWorkReports(worker_id, date); await AttendanceModel.syncWithWorkReports(user_id, date);
} catch (syncErr) { } catch (syncErr) {
console.error('근태 기록 동기화 실패 (Batch Delete):', syncErr); console.error('근태 기록 동기화 실패 (Batch Delete):', syncErr);
} }
@@ -697,7 +697,7 @@ const getStatistics = async (start_date, end_date) => {
SELECT SELECT
COUNT(*) as total_reports, COUNT(*) as total_reports,
SUM(work_hours) as total_hours, SUM(work_hours) as total_hours,
COUNT(DISTINCT worker_id) as unique_workers, COUNT(DISTINCT user_id) as unique_workers,
COUNT(DISTINCT project_id) as unique_projects COUNT(DISTINCT project_id) as unique_projects
FROM daily_work_reports FROM daily_work_reports
WHERE report_date BETWEEN ? AND ? WHERE report_date BETWEEN ? AND ?
@@ -708,7 +708,7 @@ const getStatistics = async (start_date, end_date) => {
SELECT SELECT
report_date, report_date,
SUM(work_hours) as daily_hours, SUM(work_hours) as daily_hours,
COUNT(DISTINCT worker_id) as daily_workers COUNT(DISTINCT user_id) as daily_workers
FROM daily_work_reports FROM daily_work_reports
WHERE report_date BETWEEN ? AND ? WHERE report_date BETWEEN ? AND ?
GROUP BY report_date GROUP BY report_date
@@ -727,7 +727,7 @@ const getStatistics = async (start_date, end_date) => {
* @param {object} modelData - 서비스 레이어에서 전달된 데이터 * @param {object} modelData - 서비스 레이어에서 전달된 데이터
* @returns {Promise<object>} 삽입된 항목의 ID 배열과 개수 * @returns {Promise<object>} 삽입된 항목의 ID 배열과 개수
*/ */
const createReportEntries = async ({ report_date, worker_id, entries }) => { const createReportEntries = async ({ report_date, user_id, entries }) => {
const db = await getDb(); const db = await getDb();
const conn = await db.getConnection(); const conn = await db.getConnection();
try { try {
@@ -736,7 +736,7 @@ const createReportEntries = async ({ report_date, worker_id, entries }) => {
const insertedIds = []; const insertedIds = [];
const sql = ` const sql = `
INSERT INTO daily_work_reports INSERT INTO daily_work_reports
(report_date, worker_id, project_id, work_type_id, work_hours, work_status_id, error_type_id, created_by) (report_date, user_id, project_id, work_type_id, work_hours, work_status_id, error_type_id, created_by)
VALUES (?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
`; `;
@@ -744,7 +744,7 @@ const createReportEntries = async ({ report_date, worker_id, entries }) => {
const { project_id, work_type_id, work_hours, work_status_id, error_type_id, created_by } = entry; const { project_id, work_type_id, work_hours, work_status_id, error_type_id, created_by } = entry;
const [result] = await conn.query(sql, [ const [result] = await conn.query(sql, [
report_date, report_date,
worker_id, user_id,
project_id, project_id,
work_type_id, work_type_id,
work_hours, work_hours,
@@ -760,7 +760,7 @@ const createReportEntries = async ({ report_date, worker_id, entries }) => {
// [Sync] 근태 기록 동기화 // [Sync] 근태 기록 동기화
try { try {
const AttendanceModel = require('./attendanceModel'); const AttendanceModel = require('./attendanceModel');
await AttendanceModel.syncWithWorkReports(worker_id, report_date); await AttendanceModel.syncWithWorkReports(user_id, report_date);
} catch (syncErr) { } catch (syncErr) {
console.error('근태 기록 동기화 실패 (V2 Create):', syncErr); console.error('근태 기록 동기화 실패 (V2 Create):', syncErr);
} }
@@ -788,7 +788,7 @@ const getSelectQueryV2 = () => `
SELECT SELECT
dwr.id, dwr.id,
dwr.report_date, dwr.report_date,
dwr.worker_id, dwr.user_id,
dwr.project_id, dwr.project_id,
dwr.work_type_id, dwr.work_type_id,
dwr.work_status_id, dwr.work_status_id,
@@ -805,19 +805,19 @@ const getSelectQueryV2 = () => `
u.name as created_by_name, u.name as created_by_name,
dwr.created_at dwr.created_at
FROM daily_work_reports dwr FROM daily_work_reports dwr
LEFT JOIN workers w ON dwr.worker_id = w.worker_id LEFT JOIN workers w ON dwr.user_id = w.user_id
LEFT JOIN projects p ON dwr.project_id = p.project_id LEFT JOIN projects p ON dwr.project_id = p.project_id
LEFT JOIN tasks t ON dwr.work_type_id = t.task_id LEFT JOIN tasks t ON dwr.work_type_id = t.task_id
LEFT JOIN work_types wt ON t.work_type_id = wt.id LEFT JOIN work_types wt ON t.work_type_id = wt.id
LEFT JOIN work_status_types wst ON dwr.work_status_id = wst.id LEFT JOIN work_status_types wst ON dwr.work_status_id = wst.id
LEFT JOIN issue_report_items iri ON dwr.error_type_id = iri.item_id LEFT JOIN issue_report_items iri ON dwr.error_type_id = iri.item_id
LEFT JOIN issue_report_categories irc ON iri.category_id = irc.category_id LEFT JOIN issue_report_categories irc ON iri.category_id = irc.category_id
LEFT JOIN users u ON dwr.created_by = u.user_id LEFT JOIN sso_users u ON dwr.created_by = u.user_id
`; `;
/** /**
* [V2] 옵션 기반으로 작업 보고서를 조회합니다. * [V2] 옵션 기반으로 작업 보고서를 조회합니다.
* @param {object} options - 조회 조건 (date, worker_id, created_by_user_id 등) * @param {object} options - 조회 조건 (date, user_id, created_by_user_id 등)
* @returns {Promise<Array>} 조회된 작업 보고서 배열 * @returns {Promise<Array>} 조회된 작업 보고서 배열
*/ */
const getReportsWithOptions = async (options) => { const getReportsWithOptions = async (options) => {
@@ -834,9 +834,9 @@ const getReportsWithOptions = async (options) => {
queryParams.push(options.start_date, options.end_date); queryParams.push(options.start_date, options.end_date);
} }
if (options.worker_id) { if (options.user_id) {
whereConditions.push('dwr.worker_id = ?'); whereConditions.push('dwr.user_id = ?');
queryParams.push(options.worker_id); queryParams.push(options.user_id);
} }
if (options.created_by_user_id) { if (options.created_by_user_id) {
whereConditions.push('dwr.created_by = ?'); whereConditions.push('dwr.created_by = ?');
@@ -890,7 +890,7 @@ const updateReportById = async (reportId, updateData) => {
// [Sync] 업데이트 전 정보 조회 (동기화를 위해) // [Sync] 업데이트 전 정보 조회 (동기화를 위해)
let targetInfo = null; let targetInfo = null;
try { try {
const [rows] = await db.query('SELECT worker_id, report_date FROM daily_work_reports WHERE id = ?', [reportId]); const [rows] = await db.query('SELECT user_id, report_date FROM daily_work_reports WHERE id = ?', [reportId]);
if (rows.length > 0) targetInfo = rows[0]; if (rows.length > 0) targetInfo = rows[0];
} catch (e) { console.warn('Sync fetch failed', e); } } catch (e) { console.warn('Sync fetch failed', e); }
@@ -902,7 +902,7 @@ const updateReportById = async (reportId, updateData) => {
if (targetInfo) { if (targetInfo) {
try { try {
const AttendanceModel = require('./attendanceModel'); const AttendanceModel = require('./attendanceModel');
await AttendanceModel.syncWithWorkReports(targetInfo.worker_id, targetInfo.report_date); await AttendanceModel.syncWithWorkReports(targetInfo.user_id, targetInfo.report_date);
} catch (syncErr) { } catch (syncErr) {
console.error('근태 기록 동기화 실패 (V2 Update):', syncErr); console.error('근태 기록 동기화 실패 (V2 Update):', syncErr);
} }
@@ -925,7 +925,7 @@ const removeReportById = async (reportId, deletedByUserId) => {
await conn.beginTransaction(); await conn.beginTransaction();
// 감사 로그를 위해 삭제 전 정보 조회 // 감사 로그를 위해 삭제 전 정보 조회
const [reportInfo] = await conn.query('SELECT id, report_date, worker_id, project_id, work_type_id, work_status_id, error_type_id, work_hours, created_at, updated_at, created_by, updated_by FROM daily_work_reports WHERE id = ?', [reportId]); const [reportInfo] = await conn.query('SELECT id, report_date, user_id, project_id, work_type_id, work_status_id, error_type_id, work_hours, created_at, updated_at, created_by, updated_by FROM daily_work_reports WHERE id = ?', [reportId]);
// 실제 삭제 작업 // 실제 삭제 작업
const [result] = await conn.query('DELETE FROM daily_work_reports WHERE id = ?', [reportId]); const [result] = await conn.query('DELETE FROM daily_work_reports WHERE id = ?', [reportId]);
@@ -940,9 +940,9 @@ const removeReportById = async (reportId, deletedByUserId) => {
// [Sync] 근태 기록 동기화 // [Sync] 근태 기록 동기화
if (reportInfo.length > 0) { if (reportInfo.length > 0) {
try { try {
const { worker_id, report_date } = reportInfo[0]; const { user_id, report_date } = reportInfo[0];
const AttendanceModel = require('./attendanceModel'); const AttendanceModel = require('./attendanceModel');
await AttendanceModel.syncWithWorkReports(worker_id, report_date); await AttendanceModel.syncWithWorkReports(user_id, report_date);
} catch (syncErr) { } catch (syncErr) {
console.error('근태 기록 동기화 실패 (V2 Delete):', syncErr); console.error('근태 기록 동기화 실패 (V2 Delete):', syncErr);
} }
@@ -1075,7 +1075,7 @@ const createFromTbmAssignment = async (reportData) => {
const { const {
tbm_assignment_id, tbm_assignment_id,
tbm_session_id, tbm_session_id,
worker_id, user_id,
project_id, project_id,
work_type_id, work_type_id,
report_date, report_date,
@@ -1098,7 +1098,7 @@ const createFromTbmAssignment = async (reportData) => {
// 1. 작업보고서 생성 // 1. 작업보고서 생성
const sql = ` const sql = `
INSERT INTO daily_work_reports INSERT INTO daily_work_reports
(tbm_session_id, tbm_assignment_id, report_date, worker_id, project_id, work_type_id, (tbm_session_id, tbm_assignment_id, report_date, user_id, project_id, work_type_id,
start_time, end_time, work_hours, total_hours, regular_hours, error_hours, start_time, end_time, work_hours, total_hours, regular_hours, error_hours,
work_status_id, error_type_id, created_by, created_at) work_status_id, error_type_id, created_by, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW()) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW())
@@ -1108,7 +1108,7 @@ const createFromTbmAssignment = async (reportData) => {
tbm_session_id, tbm_session_id,
tbm_assignment_id, tbm_assignment_id,
report_date, report_date,
worker_id, user_id,
project_id, project_id,
work_type_id, work_type_id,
start_time || null, start_time || null,
@@ -1150,7 +1150,7 @@ const createFromTbmAssignment = async (reportData) => {
// 4. 근태 기록 동기화 // 4. 근태 기록 동기화
try { try {
const AttendanceModel = require('./attendanceModel'); const AttendanceModel = require('./attendanceModel');
await AttendanceModel.syncWithWorkReports(worker_id, report_date); await AttendanceModel.syncWithWorkReports(user_id, report_date);
} catch (syncErr) { } catch (syncErr) {
console.error('근태 기록 동기화 실패 (TBM Report):', syncErr); console.error('근태 기록 동기화 실패 (TBM Report):', syncErr);
} }

View File

@@ -91,7 +91,7 @@ const departmentModel = {
SELECT w.*, d.department_name, u.user_id, u.username SELECT w.*, d.department_name, u.user_id, u.username
FROM workers w FROM workers w
LEFT JOIN departments d ON w.department_id = d.department_id LEFT JOIN departments d ON w.department_id = d.department_id
LEFT JOIN users u ON u.worker_id = w.worker_id LEFT JOIN users u ON u.user_id = w.user_id
WHERE w.department_id = ? WHERE w.department_id = ?
ORDER BY w.worker_name ORDER BY w.worker_name
`, [departmentId]); `, [departmentId]);
@@ -99,20 +99,20 @@ const departmentModel = {
}, },
// 작업자 부서 변경 // 작업자 부서 변경
async moveWorker(workerId, departmentId) { async moveWorker(userId, departmentId) {
const db = await getDb(); const db = await getDb();
const [result] = await db.query(` const [result] = await db.query(`
UPDATE workers SET department_id = ? WHERE worker_id = ? UPDATE workers SET department_id = ? WHERE user_id = ?
`, [departmentId, workerId]); `, [departmentId, userId]);
return result.affectedRows > 0; return result.affectedRows > 0;
}, },
// 여러 작업자 부서 일괄 변경 // 여러 작업자 부서 일괄 변경
async moveWorkers(workerIds, departmentId) { async moveWorkers(userIds, departmentId) {
const db = await getDb(); const db = await getDb();
const [result] = await db.query(` const [result] = await db.query(`
UPDATE workers SET department_id = ? WHERE worker_id IN (?) UPDATE workers SET department_id = ? WHERE user_id IN (?)
`, [departmentId, workerIds]); `, [departmentId, userIds]);
return result.affectedRows; return result.affectedRows;
} }
}; };

View File

@@ -49,7 +49,7 @@ class MonthlyStatusModel {
// daily_work_reports에서 직접 집계하여 조회 (중복 없음 보장) // daily_work_reports에서 직접 집계하여 조회 (중복 없음 보장)
const [rows] = await db.query(` const [rows] = await db.query(`
SELECT SELECT
w.worker_id, w.user_id,
w.worker_name, w.worker_name,
w.job_type, w.job_type,
YEAR(?) as year, YEAR(?) as year,
@@ -77,9 +77,9 @@ class MonthlyStatusModel {
END as has_issues, END as has_issues,
MAX(dwr.created_at) as last_updated MAX(dwr.created_at) as last_updated
FROM workers w FROM workers w
LEFT JOIN daily_work_reports dwr ON w.worker_id = dwr.worker_id AND dwr.report_date = ? LEFT JOIN daily_work_reports dwr ON w.user_id = dwr.user_id AND dwr.report_date = ?
WHERE w.status = 'active' WHERE w.status = 'active'
GROUP BY w.worker_id, w.worker_name, w.job_type GROUP BY w.user_id, w.worker_name, w.job_type
ORDER BY w.worker_name ASC ORDER BY w.worker_name ASC
`, [date, date, date, date]); `, [date, date, date, date]);
@@ -97,15 +97,15 @@ class MonthlyStatusModel {
try { try {
// 해당 월의 모든 날짜와 작업자 조합을 찾아서 재계산 // 해당 월의 모든 날짜와 작업자 조합을 찾아서 재계산
const [workDates] = await db.execute(` const [workDates] = await db.execute(`
SELECT DISTINCT report_date, worker_id SELECT DISTINCT report_date, user_id
FROM daily_work_reports FROM daily_work_reports
WHERE YEAR(report_date) = ? AND MONTH(report_date) = ? WHERE YEAR(report_date) = ? AND MONTH(report_date) = ?
`, [year, month]); `, [year, month]);
let updatedCount = 0; let updatedCount = 0;
for (const { report_date, worker_id } of workDates) { for (const { report_date, user_id } of workDates) {
await db.execute('CALL UpdateMonthlyWorkerStatus(?, ?)', [report_date, worker_id]); await db.execute('CALL UpdateMonthlyWorkerStatus(?, ?)', [report_date, user_id]);
updatedCount++; updatedCount++;
} }
@@ -119,23 +119,23 @@ class MonthlyStatusModel {
} }
// 특정 날짜 집계 강제 업데이트 // 특정 날짜 집계 강제 업데이트
static async updateDateSummary(date, workerId = null) { static async updateDateSummary(date, userId = null) {
const db = await getDb(); const db = await getDb();
try { try {
if (workerId) { if (userId) {
// 특정 작업자만 업데이트 // 특정 작업자만 업데이트
await db.execute('CALL UpdateMonthlyWorkerStatus(?, ?)', [date, workerId]); await db.execute('CALL UpdateMonthlyWorkerStatus(?, ?)', [date, userId]);
} else { } else {
// 해당 날짜의 모든 작업자 업데이트 // 해당 날짜의 모든 작업자 업데이트
const [workers] = await db.execute(` const [workers] = await db.execute(`
SELECT DISTINCT worker_id SELECT DISTINCT user_id
FROM daily_work_reports FROM daily_work_reports
WHERE report_date = ? WHERE report_date = ?
`, [date]); `, [date]);
for (const { worker_id } of workers) { for (const { user_id } of workers) {
await db.execute('CALL UpdateMonthlyWorkerStatus(?, ?)', [date, worker_id]); await db.execute('CALL UpdateMonthlyWorkerStatus(?, ?)', [date, user_id]);
} }
} }
@@ -163,7 +163,7 @@ class MonthlyStatusModel {
const [workerStatusCount] = await db.execute(` const [workerStatusCount] = await db.execute(`
SELECT SELECT
COUNT(*) as total_records, COUNT(*) as total_records,
COUNT(DISTINCT worker_id) as unique_workers, COUNT(DISTINCT user_id) as unique_workers,
COUNT(DISTINCT date) as unique_dates, COUNT(DISTINCT date) as unique_dates,
MAX(last_updated) as last_update MAX(last_updated) as last_update
FROM monthly_worker_status FROM monthly_worker_status

View File

@@ -139,13 +139,13 @@ const PageAccessModel = {
u.name, u.name,
u.role_id, u.role_id,
r.name as role_name, r.name as role_name,
u.worker_id, u.user_id as worker_user_id,
w.worker_name, w.worker_name,
w.job_type, w.job_type,
COUNT(upa.page_id) as granted_pages_count COUNT(upa.page_id) as granted_pages_count
FROM users u FROM users u
LEFT JOIN roles r ON u.role_id = r.id LEFT JOIN roles r ON u.role_id = r.id
LEFT JOIN workers w ON u.worker_id = w.worker_id LEFT JOIN workers w ON u.user_id = w.user_id
LEFT JOIN user_page_access upa ON u.user_id = upa.user_id AND upa.can_access = 1 LEFT JOIN user_page_access upa ON u.user_id = upa.user_id AND upa.can_access = 1
WHERE u.is_active = 1 WHERE u.is_active = 1
AND u.role_id IN (4, 5) AND u.role_id IN (4, 5)

View File

@@ -8,11 +8,11 @@ const TbmModel = {
const db = await getDb(); const db = await getDb();
const [result] = await db.query( const [result] = await db.query(
`INSERT INTO tbm_sessions `INSERT INTO tbm_sessions
(session_date, leader_id, project_id, work_type_id, task_id, work_location, created_by) (session_date, leader_user_id, project_id, work_type_id, task_id, work_location, created_by)
VALUES (?, ?, ?, ?, ?, ?, ?)`, VALUES (?, ?, ?, ?, ?, ?, ?)`,
[ [
sessionData.session_date, sessionData.session_date,
sessionData.leader_id, sessionData.leader_user_id,
sessionData.project_id || null, sessionData.project_id || null,
sessionData.work_type_id || null, sessionData.work_type_id || null,
sessionData.task_id || null, sessionData.task_id || null,
@@ -32,7 +32,7 @@ const TbmModel = {
w.job_type as leader_job_type, w.job_type as leader_job_type,
u.username as created_by_username, u.username as created_by_username,
u.name as created_by_name, u.name as created_by_name,
COUNT(DISTINCT ta.worker_id) as team_member_count, COUNT(DISTINCT ta.user_id) as team_member_count,
GROUP_CONCAT(DISTINCT w2.worker_name ORDER BY ta.assignment_id SEPARATOR ', ') as team_member_names, GROUP_CONCAT(DISTINCT w2.worker_name ORDER BY ta.assignment_id SEPARATOR ', ') as team_member_names,
(SELECT COUNT(*) FROM tbm_transfers tf (SELECT COUNT(*) FROM tbm_transfers tf
WHERE (tf.source_session_id = s.session_id OR tf.dest_session_id = s.session_id) WHERE (tf.source_session_id = s.session_id OR tf.dest_session_id = s.session_id)
@@ -46,10 +46,10 @@ const TbmModel = {
first_t.task_name, first_t.task_name,
first_wp.workplace_name as work_location first_wp.workplace_name as work_location
FROM tbm_sessions s FROM tbm_sessions s
LEFT JOIN workers w ON s.leader_id = w.worker_id LEFT JOIN workers w ON s.leader_user_id = w.user_id
LEFT JOIN sso_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 tbm_team_assignments ta ON s.session_id = ta.session_id
LEFT JOIN workers w2 ON ta.worker_id = w2.worker_id LEFT JOIN workers w2 ON ta.user_id = w2.user_id
LEFT JOIN ( LEFT JOIN (
SELECT * FROM tbm_team_assignments SELECT * FROM tbm_team_assignments
WHERE (session_id, assignment_id) IN ( WHERE (session_id, assignment_id) IN (
@@ -80,7 +80,7 @@ const TbmModel = {
w.phone_number as leader_phone, w.phone_number as leader_phone,
u.username as created_by_username, u.username as created_by_username,
u.name as created_by_name, u.name as created_by_name,
COUNT(DISTINCT ta.worker_id) as team_member_count, COUNT(DISTINCT ta.user_id) as team_member_count,
first_p.project_name, first_p.project_name,
first_p.job_no, first_p.job_no,
first_wt.name as work_type_name, first_wt.name as work_type_name,
@@ -90,7 +90,7 @@ const TbmModel = {
first_wp.workplace_name as work_location, first_wp.workplace_name as work_location,
first_wc.category_name as workplace_category_name first_wc.category_name as workplace_category_name
FROM tbm_sessions s FROM tbm_sessions s
LEFT JOIN workers w ON s.leader_id = w.worker_id LEFT JOIN workers w ON s.leader_user_id = w.user_id
LEFT JOIN sso_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 tbm_team_assignments ta ON s.session_id = ta.session_id
LEFT JOIN ( LEFT JOIN (
@@ -165,8 +165,8 @@ const TbmModel = {
await conn.query( await conn.query(
`UPDATE tbm_team_assignments `UPDATE tbm_team_assignments
SET attendance_type = ?, attendance_hours = ? SET attendance_type = ?, attendance_hours = ?
WHERE session_id = ? AND worker_id = ?`, WHERE session_id = ? AND user_id = ?`,
[item.attendance_type, item.attendance_hours || null, sessionId, item.worker_id] [item.attendance_type, item.attendance_hours || null, sessionId, item.user_id]
); );
} }
@@ -174,8 +174,8 @@ const TbmModel = {
const annualWorkers = attendanceData.filter(a => a.attendance_type === 'annual'); const annualWorkers = attendanceData.filter(a => a.attendance_type === 'annual');
for (const aw of annualWorkers) { for (const aw of annualWorkers) {
const [assignRows] = await conn.query( const [assignRows] = await conn.query(
'SELECT assignment_id FROM tbm_team_assignments WHERE session_id = ? AND worker_id = ?', 'SELECT assignment_id FROM tbm_team_assignments WHERE session_id = ? AND user_id = ?',
[sessionId, aw.worker_id] [sessionId, aw.user_id]
); );
if (assignRows.length > 0) { if (assignRows.length > 0) {
const [existingReport] = await conn.query( const [existingReport] = await conn.query(
@@ -185,9 +185,9 @@ const TbmModel = {
if (existingReport.length === 0) { if (existingReport.length === 0) {
await conn.query( await conn.query(
`INSERT INTO daily_work_reports `INSERT INTO daily_work_reports
(report_date, worker_id, project_id, work_hours, work_status_id, created_by, tbm_assignment_id, created_at) (report_date, user_id, project_id, work_hours, work_status_id, created_by, tbm_assignment_id, created_at)
VALUES (?, ?, 13, 8, 1, ?, ?, NOW())`, VALUES (?, ?, 13, 8, 1, ?, ?, NOW())`,
[reportDate, aw.worker_id, createdBy, assignRows[0].assignment_id] [reportDate, aw.user_id, createdBy, assignRows[0].assignment_id]
); );
} }
} }
@@ -207,7 +207,7 @@ const TbmModel = {
for (const aw of annualWorkers) { for (const aw of annualWorkers) {
try { try {
const AttendanceModel = require('./attendanceModel'); const AttendanceModel = require('./attendanceModel');
await AttendanceModel.syncWithWorkReports(aw.worker_id, reportDate); await AttendanceModel.syncWithWorkReports(aw.user_id, reportDate);
} catch (syncErr) { } catch (syncErr) {
// 근태 동기화 오류 (무시됨) // 근태 동기화 오류 (무시됨)
} }
@@ -237,7 +237,7 @@ const TbmModel = {
const db = await getDb(); const db = await getDb();
const [result] = await db.query( const [result] = await db.query(
`INSERT INTO tbm_team_assignments `INSERT INTO tbm_team_assignments
(session_id, worker_id, split_seq, assigned_role, work_detail, is_present, absence_reason, (session_id, user_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) project_id, work_type_id, task_id, workplace_category_id, workplace_id, work_hours)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE ON DUPLICATE KEY UPDATE
@@ -253,7 +253,7 @@ const TbmModel = {
work_hours = COALESCE(VALUES(work_hours), work_hours)`, work_hours = COALESCE(VALUES(work_hours), work_hours)`,
[ [
assignmentData.session_id, assignmentData.session_id,
assignmentData.worker_id, assignmentData.user_id,
assignmentData.split_seq || 0, assignmentData.split_seq || 0,
assignmentData.assigned_role, assignmentData.assigned_role,
assignmentData.work_detail, assignmentData.work_detail,
@@ -273,19 +273,19 @@ const TbmModel = {
addSplitAssignment: async (assignmentData) => { addSplitAssignment: async (assignmentData) => {
const db = await getDb(); const db = await getDb();
const [maxRows] = await db.query( const [maxRows] = await db.query(
'SELECT COALESCE(MAX(split_seq), -1) as max_seq FROM tbm_team_assignments WHERE session_id = ? AND worker_id = ?', 'SELECT COALESCE(MAX(split_seq), -1) as max_seq FROM tbm_team_assignments WHERE session_id = ? AND user_id = ?',
[assignmentData.session_id, assignmentData.worker_id] [assignmentData.session_id, assignmentData.user_id]
); );
const nextSeq = (maxRows[0].max_seq || 0) + 1; const nextSeq = (maxRows[0].max_seq || 0) + 1;
const [result] = await db.query( const [result] = await db.query(
`INSERT INTO tbm_team_assignments `INSERT INTO tbm_team_assignments
(session_id, worker_id, split_seq, work_hours, project_id, work_type_id, (session_id, user_id, split_seq, work_hours, project_id, work_type_id,
task_id, workplace_category_id, workplace_id, is_present) task_id, workplace_category_id, workplace_id, is_present)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 1)`, VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 1)`,
[ [
assignmentData.session_id, assignmentData.session_id,
assignmentData.worker_id, assignmentData.user_id,
nextSeq, nextSeq,
assignmentData.work_hours, assignmentData.work_hours,
assignmentData.project_id || null, assignmentData.project_id || null,
@@ -306,7 +306,7 @@ const TbmModel = {
const db = await getDb(); const db = await getDb();
const values = members.map(m => [ const values = members.map(m => [
sessionId, sessionId,
m.worker_id, m.user_id,
m.assigned_role || null, m.assigned_role || null,
m.work_detail || null, m.work_detail || null,
m.is_present !== undefined ? m.is_present : true, m.is_present !== undefined ? m.is_present : true,
@@ -320,7 +320,7 @@ const TbmModel = {
const [result] = await db.query( const [result] = await db.query(
`INSERT INTO tbm_team_assignments `INSERT INTO tbm_team_assignments
(session_id, worker_id, assigned_role, work_detail, is_present, absence_reason, (session_id, user_id, assigned_role, work_detail, is_present, absence_reason,
project_id, work_type_id, task_id, workplace_category_id, workplace_id) project_id, work_type_id, task_id, workplace_category_id, workplace_id)
VALUES ?`, VALUES ?`,
[values] [values]
@@ -343,7 +343,7 @@ const TbmModel = {
wc.category_name AS workplace_category_name, wc.category_name AS workplace_category_name,
wp.workplace_name wp.workplace_name
FROM tbm_team_assignments ta FROM tbm_team_assignments ta
INNER JOIN workers w ON ta.worker_id = w.worker_id INNER JOIN workers w ON ta.user_id = w.user_id
LEFT JOIN projects p ON ta.project_id = p.project_id LEFT JOIN projects p ON ta.project_id = p.project_id
LEFT JOIN work_types wt ON ta.work_type_id = wt.id LEFT JOIN work_types wt ON ta.work_type_id = wt.id
LEFT JOIN tasks t ON ta.task_id = t.task_id LEFT JOIN tasks t ON ta.task_id = t.task_id
@@ -359,7 +359,7 @@ const TbmModel = {
removeTeamMember: async (sessionId, workerId) => { removeTeamMember: async (sessionId, workerId) => {
const db = await getDb(); const db = await getDb();
const [result] = await db.query( const [result] = await db.query(
`DELETE FROM tbm_team_assignments WHERE session_id = ? AND worker_id = ?`, `DELETE FROM tbm_team_assignments WHERE session_id = ? AND user_id = ?`,
[sessionId, workerId] [sessionId, workerId]
); );
return result; return result;
@@ -464,13 +464,13 @@ const TbmModel = {
const db = await getDb(); const db = await getDb();
const [result] = await db.query( const [result] = await db.query(
`INSERT INTO team_handovers `INSERT INTO team_handovers
(session_id, from_leader_id, to_leader_id, handover_date, handover_time, (session_id, from_leader_user_id, to_leader_user_id, handover_date, handover_time,
reason, handover_notes, worker_ids) reason, handover_notes, worker_ids)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
[ [
handoverData.session_id, handoverData.session_id,
handoverData.from_leader_id, handoverData.from_leader_user_id,
handoverData.to_leader_id, handoverData.to_leader_user_id,
handoverData.handover_date, handoverData.handover_date,
handoverData.handover_time, handoverData.handover_time,
handoverData.reason, handoverData.reason,
@@ -502,8 +502,8 @@ const TbmModel = {
u.username as confirmed_by_username, u.username as confirmed_by_username,
u.name as confirmed_by_name u.name as confirmed_by_name
FROM team_handovers h FROM team_handovers h
INNER JOIN workers w1 ON h.from_leader_id = w1.worker_id INNER JOIN workers w1 ON h.from_leader_user_id = w1.user_id
INNER JOIN workers w2 ON h.to_leader_id = w2.worker_id INNER JOIN workers w2 ON h.to_leader_user_id = w2.user_id
LEFT JOIN sso_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 = ? WHERE h.handover_date = ?
ORDER BY h.handover_time DESC`, ORDER BY h.handover_time DESC`,
@@ -521,9 +521,9 @@ const TbmModel = {
w1.phone_number as from_leader_phone, w1.phone_number as from_leader_phone,
s.work_location s.work_location
FROM team_handovers h FROM team_handovers h
INNER JOIN workers w1 ON h.from_leader_id = w1.worker_id INNER JOIN workers w1 ON h.from_leader_user_id = w1.user_id
LEFT JOIN tbm_sessions s ON h.session_id = s.session_id LEFT JOIN tbm_sessions s ON h.session_id = s.session_id
WHERE h.to_leader_id = ? AND h.is_confirmed = 0 WHERE h.to_leader_user_id = ? AND h.is_confirmed = 0
ORDER BY h.handover_date DESC, h.handover_time DESC`, ORDER BY h.handover_date DESC, h.handover_time DESC`,
[toLeaderId] [toLeaderId]
); );
@@ -538,7 +538,7 @@ const TbmModel = {
`SELECT `SELECT
DATE(session_date) as date, DATE(session_date) as date,
COUNT(DISTINCT session_id) as session_count, COUNT(DISTINCT session_id) as session_count,
COUNT(DISTINCT leader_id) as leader_count, COUNT(DISTINCT leader_user_id) as leader_count,
SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) as completed_count SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) as completed_count
FROM tbm_sessions FROM tbm_sessions
WHERE session_date BETWEEN ? AND ? WHERE session_date BETWEEN ? AND ?
@@ -553,16 +553,16 @@ const TbmModel = {
const db = await getDb(); const db = await getDb();
const [rows] = await db.query( const [rows] = await db.query(
`SELECT `SELECT
s.leader_id, s.leader_user_id,
w.worker_name as leader_name, w.worker_name as leader_name,
COUNT(DISTINCT s.session_id) as total_sessions, COUNT(DISTINCT s.session_id) as total_sessions,
SUM(CASE WHEN s.status = 'completed' THEN 1 ELSE 0 END) as completed_sessions, SUM(CASE WHEN s.status = 'completed' THEN 1 ELSE 0 END) as completed_sessions,
COUNT(DISTINCT ta.worker_id) as total_team_members COUNT(DISTINCT ta.user_id) as total_team_members
FROM tbm_sessions s FROM tbm_sessions s
INNER JOIN workers w ON s.leader_id = w.worker_id INNER JOIN workers w ON s.leader_user_id = w.user_id
LEFT JOIN tbm_team_assignments ta ON s.session_id = ta.session_id LEFT JOIN tbm_team_assignments ta ON s.session_id = ta.session_id
WHERE s.session_date BETWEEN ? AND ? WHERE s.session_date BETWEEN ? AND ?
GROUP BY s.leader_id GROUP BY s.leader_user_id
ORDER BY total_sessions DESC`, ORDER BY total_sessions DESC`,
[startDate, endDate] [startDate, endDate]
); );
@@ -597,7 +597,7 @@ const TbmModel = {
`SELECT `SELECT
ta.assignment_id, ta.assignment_id,
ta.session_id, ta.session_id,
ta.worker_id, ta.user_id,
ta.project_id, ta.project_id,
ta.work_type_id, ta.work_type_id,
ta.task_id, ta.task_id,
@@ -620,9 +620,9 @@ const TbmModel = {
lw.worker_name as leader_name lw.worker_name as leader_name
FROM tbm_team_assignments ta FROM tbm_team_assignments ta
INNER JOIN tbm_sessions s ON ta.session_id = s.session_id INNER JOIN tbm_sessions s ON ta.session_id = s.session_id
INNER JOIN workers w ON ta.worker_id = w.worker_id INNER JOIN workers w ON ta.user_id = w.user_id
LEFT JOIN sso_users creator ON s.created_by = creator.user_id LEFT JOIN sso_users creator ON s.created_by = creator.user_id
LEFT JOIN workers lw ON s.leader_id = lw.worker_id LEFT JOIN workers lw ON s.leader_user_id = lw.user_id
LEFT JOIN projects p ON ta.project_id = p.project_id LEFT JOIN projects p ON ta.project_id = p.project_id
LEFT JOIN work_types wt ON ta.work_type_id = wt.id LEFT JOIN work_types wt ON ta.work_type_id = wt.id
LEFT JOIN tasks t ON ta.task_id = t.task_id LEFT JOIN tasks t ON ta.task_id = t.task_id

View File

@@ -13,15 +13,15 @@ const TbmTransferModel = {
await conn.beginTransaction(); await conn.beginTransaction();
const { const {
transfer_type, worker_id, source_session_id, dest_session_id, transfer_type, user_id, source_session_id, dest_session_id,
hours, initiated_by, transfer_date, hours, initiated_by, transfer_date,
project_id, work_type_id, task_id, workplace_category_id, workplace_id project_id, work_type_id, task_id, workplace_category_id, workplace_id
} = transferData; } = transferData;
// 1. source 세션에서 해당 작업자의 work_hours 업데이트 // 1. source 세션에서 해당 작업자의 work_hours 업데이트
const [sourceRows] = await conn.query( const [sourceRows] = await conn.query(
'SELECT assignment_id, work_hours FROM tbm_team_assignments WHERE session_id = ? AND worker_id = ?', 'SELECT assignment_id, work_hours FROM tbm_team_assignments WHERE session_id = ? AND user_id = ?',
[source_session_id, worker_id] [source_session_id, user_id]
); );
if (sourceRows.length === 0) { if (sourceRows.length === 0) {
@@ -38,28 +38,28 @@ const TbmTransferModel = {
} }
await conn.query( await conn.query(
'UPDATE tbm_team_assignments SET work_hours = ? WHERE session_id = ? AND worker_id = ?', 'UPDATE tbm_team_assignments SET work_hours = ? WHERE session_id = ? AND user_id = ?',
[newSourceHours, source_session_id, worker_id] [newSourceHours, source_session_id, user_id]
); );
// 2. dest 세션에 작업자 INSERT (이미 있으면 work_hours 증가) // 2. dest 세션에 작업자 INSERT (이미 있으면 work_hours 증가)
const [destRows] = await conn.query( const [destRows] = await conn.query(
'SELECT assignment_id, work_hours FROM tbm_team_assignments WHERE session_id = ? AND worker_id = ?', 'SELECT assignment_id, work_hours FROM tbm_team_assignments WHERE session_id = ? AND user_id = ?',
[dest_session_id, worker_id] [dest_session_id, user_id]
); );
if (destRows.length > 0) { if (destRows.length > 0) {
const existingHours = destRows[0].work_hours === null ? 8 : parseFloat(destRows[0].work_hours); const existingHours = destRows[0].work_hours === null ? 8 : parseFloat(destRows[0].work_hours);
await conn.query( await conn.query(
'UPDATE tbm_team_assignments SET work_hours = ? WHERE session_id = ? AND worker_id = ?', 'UPDATE tbm_team_assignments SET work_hours = ? WHERE session_id = ? AND user_id = ?',
[existingHours + parseFloat(hours), dest_session_id, worker_id] [existingHours + parseFloat(hours), dest_session_id, user_id]
); );
} else { } else {
await conn.query( await conn.query(
`INSERT INTO tbm_team_assignments `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) (session_id, user_id, work_hours, project_id, work_type_id, task_id, workplace_category_id, workplace_id, is_present)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 1)`, VALUES (?, ?, ?, ?, ?, ?, ?, ?, 1)`,
[dest_session_id, worker_id, parseFloat(hours), [dest_session_id, user_id, parseFloat(hours),
project_id || null, work_type_id || null, task_id || null, project_id || null, work_type_id || null, task_id || null,
workplace_category_id || null, workplace_id || null] workplace_category_id || null, workplace_id || null]
); );
@@ -68,18 +68,18 @@ const TbmTransferModel = {
// 3. tbm_transfers에 로그 INSERT // 3. tbm_transfers에 로그 INSERT
const [logResult] = await conn.query( const [logResult] = await conn.query(
`INSERT INTO tbm_transfers `INSERT INTO tbm_transfers
(transfer_date, worker_id, source_session_id, dest_session_id, hours, transfer_type, initiated_by) (transfer_date, user_id, source_session_id, dest_session_id, hours, transfer_type, initiated_by)
VALUES (?, ?, ?, ?, ?, ?, ?)`, VALUES (?, ?, ?, ?, ?, ?, ?)`,
[transfer_date, worker_id, source_session_id, dest_session_id, parseFloat(hours), transfer_type, initiated_by] [transfer_date, user_id, source_session_id, dest_session_id, parseFloat(hours), transfer_type, initiated_by]
); );
// 4. 합계 시간 검증 (경고만, 차단 안함) // 4. 합계 시간 검증 (경고만, 차단 안함)
const [totalRows] = await conn.query( const [totalRows] = await conn.query(
`SELECT SUM(COALESCE(work_hours, 8)) as total_hours `SELECT SUM(COALESCE(work_hours, 8)) as total_hours
FROM tbm_team_assignments FROM tbm_team_assignments
WHERE worker_id = ? WHERE user_id = ?
AND session_id IN (SELECT session_id FROM tbm_sessions WHERE session_date = ?)`, AND session_id IN (SELECT session_id FROM tbm_sessions WHERE session_date = ?)`,
[worker_id, transfer_date] [user_id, transfer_date]
); );
const totalHours = totalRows[0] ? parseFloat(totalRows[0].total_hours) : 0; const totalHours = totalRows[0] ? parseFloat(totalRows[0].total_hours) : 0;
@@ -128,8 +128,8 @@ const TbmTransferModel = {
// 2. dest 세션에서 작업자 work_hours 감소 (또는 삭제) // 2. dest 세션에서 작업자 work_hours 감소 (또는 삭제)
const [destRows] = await conn.query( const [destRows] = await conn.query(
'SELECT assignment_id, work_hours FROM tbm_team_assignments WHERE session_id = ? AND worker_id = ?', 'SELECT assignment_id, work_hours FROM tbm_team_assignments WHERE session_id = ? AND user_id = ?',
[t.dest_session_id, t.worker_id] [t.dest_session_id, t.user_id]
); );
if (destRows.length > 0) { if (destRows.length > 0) {
@@ -138,29 +138,29 @@ const TbmTransferModel = {
if (newDestHours <= 0) { if (newDestHours <= 0) {
await conn.query( await conn.query(
'DELETE FROM tbm_team_assignments WHERE session_id = ? AND worker_id = ?', 'DELETE FROM tbm_team_assignments WHERE session_id = ? AND user_id = ?',
[t.dest_session_id, t.worker_id] [t.dest_session_id, t.user_id]
); );
} else { } else {
await conn.query( await conn.query(
'UPDATE tbm_team_assignments SET work_hours = ? WHERE session_id = ? AND worker_id = ?', 'UPDATE tbm_team_assignments SET work_hours = ? WHERE session_id = ? AND user_id = ?',
[newDestHours, t.dest_session_id, t.worker_id] [newDestHours, t.dest_session_id, t.user_id]
); );
} }
} }
// 3. source 세션에서 작업자 work_hours 복원 // 3. source 세션에서 작업자 work_hours 복원
const [sourceRows] = await conn.query( const [sourceRows] = await conn.query(
'SELECT assignment_id, work_hours FROM tbm_team_assignments WHERE session_id = ? AND worker_id = ?', 'SELECT assignment_id, work_hours FROM tbm_team_assignments WHERE session_id = ? AND user_id = ?',
[t.source_session_id, t.worker_id] [t.source_session_id, t.user_id]
); );
if (sourceRows.length > 0) { if (sourceRows.length > 0) {
const sourceHours = sourceRows[0].work_hours === null ? 8 : parseFloat(sourceRows[0].work_hours); const sourceHours = sourceRows[0].work_hours === null ? 8 : parseFloat(sourceRows[0].work_hours);
const restoredHours = sourceHours + parseFloat(t.hours); const restoredHours = sourceHours + parseFloat(t.hours);
await conn.query( await conn.query(
'UPDATE tbm_team_assignments SET work_hours = ? WHERE session_id = ? AND worker_id = ?', 'UPDATE tbm_team_assignments SET work_hours = ? WHERE session_id = ? AND user_id = ?',
[restoredHours >= 8 ? null : restoredHours, t.source_session_id, t.worker_id] [restoredHours >= 8 ? null : restoredHours, t.source_session_id, t.user_id]
); );
} }
@@ -191,11 +191,11 @@ const TbmTransferModel = {
dl.worker_name as dest_leader_name, dl.worker_name as dest_leader_name,
u.name as initiated_by_name u.name as initiated_by_name
FROM tbm_transfers t FROM tbm_transfers t
INNER JOIN workers w ON t.worker_id = w.worker_id 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 tbm_sessions ss ON t.source_session_id = ss.session_id
LEFT JOIN workers sl ON ss.leader_id = sl.worker_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 tbm_sessions ds ON t.dest_session_id = ds.session_id
LEFT JOIN workers dl ON ds.leader_id = dl.worker_id LEFT JOIN workers dl ON ds.leader_id = dl.user_id
LEFT JOIN sso_users u ON t.initiated_by = u.user_id LEFT JOIN sso_users u ON t.initiated_by = u.user_id
WHERE t.transfer_date = ? WHERE t.transfer_date = ?
ORDER BY t.created_at DESC ORDER BY t.created_at DESC
@@ -213,7 +213,7 @@ const TbmTransferModel = {
// 1. 해당 날짜의 모든 배정 가져오기 // 1. 해당 날짜의 모든 배정 가져오기
const [assignments] = await db.query(` const [assignments] = await db.query(`
SELECT SELECT
ta.worker_id, ta.user_id,
ta.session_id, ta.session_id,
ta.work_hours, ta.work_hours,
w.worker_name, w.worker_name,
@@ -223,22 +223,22 @@ const TbmTransferModel = {
s.status as session_status s.status as session_status
FROM tbm_team_assignments ta FROM tbm_team_assignments ta
INNER JOIN tbm_sessions s ON ta.session_id = s.session_id INNER JOIN tbm_sessions s ON ta.session_id = s.session_id
INNER JOIN workers w ON ta.worker_id = w.worker_id INNER JOIN workers w ON ta.user_id = w.user_id
LEFT JOIN workers lw ON s.leader_id = lw.worker_id LEFT JOIN workers lw ON s.leader_id = lw.user_id
WHERE s.session_date = ? WHERE s.session_date = ?
ORDER BY w.worker_name ORDER BY w.worker_name
`, [date]); `, [date]);
// 2. 모든 작업자 가져오기 (배정 안 된 사람도 포함) // 2. 모든 작업자 가져오기 (배정 안 된 사람도 포함)
const [allWorkers] = await db.query( const [allWorkers] = await db.query(
"SELECT worker_id, worker_name, job_type FROM workers WHERE status = 'active' AND department = '생산' ORDER BY worker_name" "SELECT user_id, worker_name, job_type FROM workers WHERE status = 'active' AND department = '생산' ORDER BY worker_name"
); );
// 3. 작업자별 배정 현황 구성 // 3. 작업자별 배정 현황 구성
const workerMap = {}; const workerMap = {};
allWorkers.forEach(w => { allWorkers.forEach(w => {
workerMap[w.worker_id] = { workerMap[w.user_id] = {
worker_id: w.worker_id, user_id: w.user_id,
worker_name: w.worker_name, worker_name: w.worker_name,
job_type: w.job_type, job_type: w.job_type,
sessions: [], sessions: [],
@@ -249,14 +249,14 @@ const TbmTransferModel = {
assignments.forEach(a => { assignments.forEach(a => {
const hours = a.work_hours === null ? 8 : parseFloat(a.work_hours); const hours = a.work_hours === null ? 8 : parseFloat(a.work_hours);
if (workerMap[a.worker_id]) { if (workerMap[a.user_id]) {
workerMap[a.worker_id].sessions.push({ workerMap[a.user_id].sessions.push({
session_id: a.session_id, session_id: a.session_id,
leader_name: a.leader_name, leader_name: a.leader_name,
work_hours: hours, work_hours: hours,
session_status: a.session_status session_status: a.session_status
}); });
workerMap[a.worker_id].total_hours += hours; workerMap[a.user_id].total_hours += hours;
} }
}); });

View File

@@ -7,7 +7,7 @@ const findByUsername = async (username) => {
const [rows] = await db.query( const [rows] = await db.query(
`SELECT u.user_id, u.username, u.password, u.name, u.email, `SELECT u.user_id, u.username, u.password, u.name, u.email,
u.role_id, r.name as role_name, u.role_id, r.name as role_name,
u._access_level_old as access_level, u.worker_id, u.is_active, u._access_level_old as access_level, u.is_active,
u.last_login_at, u.password_changed_at, u.failed_login_attempts, u.last_login_at, u.password_changed_at, u.failed_login_attempts,
u.locked_until, u.created_at, u.updated_at u.locked_until, u.created_at, u.updated_at
FROM users u FROM users u

View File

@@ -9,7 +9,7 @@ const vacationBalanceModel = {
/** /**
* 특정 작업자의 모든 휴가 잔액 조회 (특정 연도) * 특정 작업자의 모든 휴가 잔액 조회 (특정 연도)
*/ */
async getByWorkerAndYear(workerId, year) { async getByWorkerAndYear(userId, year) {
const db = await getDb(); const db = await getDb();
const [rows] = await db.query(` const [rows] = await db.query(`
SELECT SELECT
@@ -20,16 +20,16 @@ const vacationBalanceModel = {
vt.is_special vt.is_special
FROM vacation_balance_details vbd FROM vacation_balance_details vbd
INNER JOIN vacation_types vt ON vbd.vacation_type_id = vt.id INNER JOIN vacation_types vt ON vbd.vacation_type_id = vt.id
WHERE vbd.worker_id = ? AND vbd.year = ? WHERE vbd.user_id = ? AND vbd.year = ?
ORDER BY vt.priority ASC, vt.type_name ASC ORDER BY vt.priority ASC, vt.type_name ASC
`, [workerId, year]); `, [userId, year]);
return rows; return rows;
}, },
/** /**
* 특정 작업자의 특정 휴가 유형 잔액 조회 * 특정 작업자의 특정 휴가 유형 잔액 조회
*/ */
async getByWorkerTypeYear(workerId, vacationTypeId, year) { async getByWorkerTypeYear(userId, vacationTypeId, year) {
const db = await getDb(); const db = await getDb();
const [rows] = await db.query(` const [rows] = await db.query(`
SELECT SELECT
@@ -38,10 +38,10 @@ const vacationBalanceModel = {
vt.type_code vt.type_code
FROM vacation_balance_details vbd FROM vacation_balance_details vbd
INNER JOIN vacation_types vt ON vbd.vacation_type_id = vt.id INNER JOIN vacation_types vt ON vbd.vacation_type_id = vt.id
WHERE vbd.worker_id = ? WHERE vbd.user_id = ?
AND vbd.vacation_type_id = ? AND vbd.vacation_type_id = ?
AND vbd.year = ? AND vbd.year = ?
`, [workerId, vacationTypeId, year]); `, [userId, vacationTypeId, year]);
return rows; return rows;
}, },
@@ -59,7 +59,7 @@ const vacationBalanceModel = {
vt.type_code, vt.type_code,
vt.priority vt.priority
FROM vacation_balance_details vbd FROM vacation_balance_details vbd
INNER JOIN workers w ON vbd.worker_id = w.worker_id INNER JOIN workers w ON vbd.user_id = w.user_id
INNER JOIN vacation_types vt ON vbd.vacation_type_id = vt.id INNER JOIN vacation_types vt ON vbd.vacation_type_id = vt.id
WHERE vbd.year = ? WHERE vbd.year = ?
AND w.employment_status = 'employed' AND w.employment_status = 'employed'
@@ -98,39 +98,39 @@ const vacationBalanceModel = {
/** /**
* 작업자의 휴가 사용 일수 업데이트 (차감) * 작업자의 휴가 사용 일수 업데이트 (차감)
*/ */
async deductDays(workerId, vacationTypeId, year, daysToDeduct) { async deductDays(userId, vacationTypeId, year, daysToDeduct) {
const db = await getDb(); const db = await getDb();
const [result] = await db.query(` const [result] = await db.query(`
UPDATE vacation_balance_details UPDATE vacation_balance_details
SET used_days = used_days + ?, SET used_days = used_days + ?,
updated_at = NOW() updated_at = NOW()
WHERE worker_id = ? WHERE user_id = ?
AND vacation_type_id = ? AND vacation_type_id = ?
AND year = ? AND year = ?
`, [daysToDeduct, workerId, vacationTypeId, year]); `, [daysToDeduct, userId, vacationTypeId, year]);
return result; return result;
}, },
/** /**
* 작업자의 휴가 사용 일수 복구 (취소) * 작업자의 휴가 사용 일수 복구 (취소)
*/ */
async restoreDays(workerId, vacationTypeId, year, daysToRestore) { async restoreDays(userId, vacationTypeId, year, daysToRestore) {
const db = await getDb(); const db = await getDb();
const [result] = await db.query(` const [result] = await db.query(`
UPDATE vacation_balance_details UPDATE vacation_balance_details
SET used_days = GREATEST(0, used_days - ?), SET used_days = GREATEST(0, used_days - ?),
updated_at = NOW() updated_at = NOW()
WHERE worker_id = ? WHERE user_id = ?
AND vacation_type_id = ? AND vacation_type_id = ?
AND year = ? AND year = ?
`, [daysToRestore, workerId, vacationTypeId, year]); `, [daysToRestore, userId, vacationTypeId, year]);
return result; return result;
}, },
/** /**
* 특정 작업자의 사용 가능한 휴가 일수 확인 * 특정 작업자의 사용 가능한 휴가 일수 확인
*/ */
async getAvailableVacationDays(workerId, year) { async getAvailableVacationDays(userId, year) {
const db = await getDb(); const db = await getDb();
const [rows] = await db.query(` const [rows] = await db.query(`
SELECT SELECT
@@ -144,11 +144,11 @@ const vacationBalanceModel = {
vbd.remaining_days vbd.remaining_days
FROM vacation_balance_details vbd FROM vacation_balance_details vbd
INNER JOIN vacation_types vt ON vbd.vacation_type_id = vt.id INNER JOIN vacation_types vt ON vbd.vacation_type_id = vt.id
WHERE vbd.worker_id = ? WHERE vbd.user_id = ?
AND vbd.year = ? AND vbd.year = ?
AND vbd.remaining_days > 0 AND vbd.remaining_days > 0
ORDER BY vt.priority ASC ORDER BY vt.priority ASC
`, [workerId, year]); `, [userId, year]);
return rows; return rows;
}, },
@@ -162,11 +162,11 @@ const vacationBalanceModel = {
const db = await getDb(); const db = await getDb();
const query = `INSERT INTO vacation_balance_details const query = `INSERT INTO vacation_balance_details
(worker_id, vacation_type_id, year, total_days, used_days, notes, created_by) (user_id, vacation_type_id, year, total_days, used_days, notes, created_by)
VALUES ?`; VALUES ?`;
const values = balances.map(b => [ const values = balances.map(b => [
b.worker_id, b.user_id,
b.vacation_type_id, b.vacation_type_id,
b.year, b.year,
b.total_days || 0, b.total_days || 0,
@@ -202,7 +202,7 @@ const vacationBalanceModel = {
/** /**
* 휴가 사용 시 우선순위에 따라 잔액에서 차감 * 휴가 사용 시 우선순위에 따라 잔액에서 차감
*/ */
async deductByPriority(workerId, year, daysToDeduct) { async deductByPriority(userId, year, daysToDeduct) {
const db = await getDb(); const db = await getDb();
const [balances] = await db.query(` const [balances] = await db.query(`
@@ -211,13 +211,13 @@ const vacationBalanceModel = {
vt.type_code, vt.type_name, vt.priority vt.type_code, vt.type_name, vt.priority
FROM vacation_balance_details vbd FROM vacation_balance_details vbd
INNER JOIN vacation_types vt ON vbd.vacation_type_id = vt.id INNER JOIN vacation_types vt ON vbd.vacation_type_id = vt.id
WHERE vbd.worker_id = ? AND vbd.year = ? WHERE vbd.user_id = ? AND vbd.year = ?
AND (vbd.total_days - vbd.used_days) > 0 AND (vbd.total_days - vbd.used_days) > 0
ORDER BY vt.priority ASC ORDER BY vt.priority ASC
`, [workerId, year]); `, [userId, year]);
if (balances.length === 0) { if (balances.length === 0) {
console.warn(`[VacationBalance] 작업자 ${workerId}${year}년 휴가 잔액이 없습니다`); console.warn(`[VacationBalance] 작업자 ${userId}${year}년 휴가 잔액이 없습니다`);
return { success: false, message: '휴가 잔액이 없습니다', deducted: 0 }; return { success: false, message: '휴가 잔액이 없습니다', deducted: 0 };
} }
@@ -248,14 +248,14 @@ const vacationBalanceModel = {
} }
} }
console.log(`[VacationBalance] 작업자 ${workerId}: ${daysToDeduct}일 차감 완료`, deductions); console.log(`[VacationBalance] 작업자 ${userId}: ${daysToDeduct}일 차감 완료`, deductions);
return { success: true, deductions, totalDeducted: daysToDeduct - remaining }; return { success: true, deductions, totalDeducted: daysToDeduct - remaining };
}, },
/** /**
* 휴가 취소 시 우선순위 역순으로 복구 * 휴가 취소 시 우선순위 역순으로 복구
*/ */
async restoreByPriority(workerId, year, daysToRestore) { async restoreByPriority(userId, year, daysToRestore) {
const db = await getDb(); const db = await getDb();
const [balances] = await db.query(` const [balances] = await db.query(`
@@ -263,10 +263,10 @@ const vacationBalanceModel = {
vt.type_code, vt.type_name, vt.priority vt.type_code, vt.type_name, vt.priority
FROM vacation_balance_details vbd FROM vacation_balance_details vbd
INNER JOIN vacation_types vt ON vbd.vacation_type_id = vt.id INNER JOIN vacation_types vt ON vbd.vacation_type_id = vt.id
WHERE vbd.worker_id = ? AND vbd.year = ? WHERE vbd.user_id = ? AND vbd.year = ?
AND vbd.used_days > 0 AND vbd.used_days > 0
ORDER BY vt.priority DESC ORDER BY vt.priority DESC
`, [workerId, year]); `, [userId, year]);
let remaining = daysToRestore; let remaining = daysToRestore;
const restorations = []; const restorations = [];
@@ -295,7 +295,7 @@ const vacationBalanceModel = {
} }
} }
console.log(`[VacationBalance] 작업자 ${workerId}: ${daysToRestore}일 복구 완료`, restorations); console.log(`[VacationBalance] 작업자 ${userId}: ${daysToRestore}일 복구 완료`, restorations);
return { success: true, restorations, totalRestored: daysToRestore - remaining }; return { success: true, restorations, totalRestored: daysToRestore - remaining };
}, },
@@ -311,7 +311,7 @@ const vacationBalanceModel = {
vt.type_name, vt.type_name,
vt.type_code vt.type_code
FROM vacation_balance_details vbd FROM vacation_balance_details vbd
INNER JOIN workers w ON vbd.worker_id = w.worker_id INNER JOIN workers w ON vbd.user_id = w.user_id
INNER JOIN vacation_types vt ON vbd.vacation_type_id = vt.id INNER JOIN vacation_types vt ON vbd.vacation_type_id = vt.id
WHERE vbd.id = ? WHERE vbd.id = ?
`, [id]); `, [id]);

View File

@@ -30,7 +30,7 @@ const vacationRequestModel = {
requester.name as requester_name, requester.name as requester_name,
reviewer.name as reviewer_name reviewer.name as reviewer_name
FROM vacation_requests vr FROM vacation_requests vr
INNER JOIN workers w ON vr.worker_id = w.worker_id INNER JOIN workers w ON vr.user_id = w.user_id
INNER JOIN vacation_types vt ON vr.vacation_type_id = vt.id INNER JOIN vacation_types vt ON vr.vacation_type_id = vt.id
LEFT JOIN users requester ON vr.requested_by = requester.user_id LEFT JOIN users requester ON vr.requested_by = requester.user_id
LEFT JOIN users reviewer ON vr.reviewed_by = reviewer.user_id LEFT JOIN users reviewer ON vr.reviewed_by = reviewer.user_id
@@ -39,9 +39,9 @@ const vacationRequestModel = {
const params = []; const params = [];
if (filters.worker_id) { if (filters.user_id) {
query += ` AND vr.worker_id = ?`; query += ` AND vr.user_id = ?`;
params.push(filters.worker_id); params.push(filters.user_id);
} }
if (filters.status) { if (filters.status) {
query += ` AND vr.status = ?`; query += ` AND vr.status = ?`;
@@ -85,7 +85,7 @@ const vacationRequestModel = {
reviewer.name as reviewer_name, reviewer.name as reviewer_name,
reviewer.username as reviewer_username reviewer.username as reviewer_username
FROM vacation_requests vr FROM vacation_requests vr
INNER JOIN workers w ON vr.worker_id = w.worker_id INNER JOIN workers w ON vr.user_id = w.user_id
INNER JOIN vacation_types vt ON vr.vacation_type_id = vt.id INNER JOIN vacation_types vt ON vr.vacation_type_id = vt.id
LEFT JOIN users requester ON vr.requested_by = requester.user_id LEFT JOIN users requester ON vr.requested_by = requester.user_id
LEFT JOIN users reviewer ON vr.reviewed_by = reviewer.user_id LEFT JOIN users reviewer ON vr.reviewed_by = reviewer.user_id
@@ -128,35 +128,35 @@ const vacationRequestModel = {
/** /**
* 특정 작업자의 대기 중인 휴가 신청 수 * 특정 작업자의 대기 중인 휴가 신청 수
*/ */
async getPendingCount(workerId) { async getPendingCount(userId) {
const db = await getDb(); const db = await getDb();
const [rows] = await db.query(` const [rows] = await db.query(`
SELECT COUNT(*) as count FROM vacation_requests SELECT COUNT(*) as count FROM vacation_requests
WHERE worker_id = ? AND status = 'pending' WHERE user_id = ? AND status = 'pending'
`, [workerId]); `, [userId]);
return rows; return rows;
}, },
/** /**
* 특정 작업자의 승인된 휴가 일수 합계 (특정 기간) * 특정 작업자의 승인된 휴가 일수 합계 (특정 기간)
*/ */
async getApprovedDaysInPeriod(workerId, startDate, endDate) { async getApprovedDaysInPeriod(userId, startDate, endDate) {
const db = await getDb(); const db = await getDb();
const [rows] = await db.query(` const [rows] = await db.query(`
SELECT COALESCE(SUM(days_used), 0) as total_days FROM vacation_requests SELECT COALESCE(SUM(days_used), 0) as total_days FROM vacation_requests
WHERE worker_id = ? AND status = 'approved' AND start_date >= ? AND end_date <= ? WHERE user_id = ? AND status = 'approved' AND start_date >= ? AND end_date <= ?
`, [workerId, startDate, endDate]); `, [userId, startDate, endDate]);
return rows; return rows;
}, },
/** /**
* 휴가 기간 중복 체크 * 휴가 기간 중복 체크
*/ */
async checkOverlap(workerId, startDate, endDate, excludeRequestId = null) { async checkOverlap(userId, startDate, endDate, excludeRequestId = null) {
const db = await getDb(); const db = await getDb();
let query = ` let query = `
SELECT COUNT(*) as count FROM vacation_requests SELECT COUNT(*) as count FROM vacation_requests
WHERE worker_id = ? WHERE user_id = ?
AND status IN ('pending', 'approved') AND status IN ('pending', 'approved')
AND ( AND (
(start_date <= ? AND end_date >= ?) OR (start_date <= ? AND end_date >= ?) OR
@@ -164,7 +164,7 @@ const vacationRequestModel = {
(start_date >= ? AND end_date <= ?) (start_date >= ? AND end_date <= ?)
) )
`; `;
const params = [workerId, startDate, startDate, endDate, endDate, startDate, endDate]; const params = [userId, startDate, startDate, endDate, endDate, startDate, endDate];
if (excludeRequestId) { if (excludeRequestId) {
query += ` AND request_id != ?`; query += ` AND request_id != ?`;
@@ -187,7 +187,7 @@ const vacationRequestModel = {
vt.type_name as vacation_type_name, vt.type_name as vacation_type_name,
requester.name as requester_name requester.name as requester_name
FROM vacation_requests vr FROM vacation_requests vr
INNER JOIN workers w ON vr.worker_id = w.worker_id INNER JOIN workers w ON vr.user_id = w.user_id
INNER JOIN vacation_types vt ON vr.vacation_type_id = vt.id INNER JOIN vacation_types vt ON vr.vacation_type_id = vt.id
LEFT JOIN users requester ON vr.requested_by = requester.user_id LEFT JOIN users requester ON vr.requested_by = requester.user_id
WHERE vr.status = 'pending' WHERE vr.status = 'pending'

View File

@@ -12,14 +12,14 @@ const createBatch = async (reports) => {
const sql = ` const sql = `
INSERT INTO WorkReports INSERT INTO WorkReports
(\`date\`, worker_id, project_id, task_id, overtime_hours, work_details, memo) (\`date\`, user_id, project_id, task_id, overtime_hours, work_details, memo)
VALUES (?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?)
`; `;
for (const rpt of reports) { for (const rpt of reports) {
const params = [ const params = [
rpt.date, rpt.date,
rpt.worker_id, rpt.user_id,
rpt.project_id, rpt.project_id,
rpt.task_id || null, rpt.task_id || null,
rpt.overtime_hours || null, rpt.overtime_hours || null,
@@ -44,18 +44,18 @@ const createBatch = async (reports) => {
const create = async (report) => { const create = async (report) => {
const db = await getDb(); const db = await getDb();
const { const {
date, worker_id, project_id, date, user_id, project_id,
task_id, overtime_hours, task_id, overtime_hours,
work_details, memo work_details, memo
} = report; } = report;
const [result] = await db.query( const [result] = await db.query(
`INSERT INTO WorkReports `INSERT INTO WorkReports
(\`date\`, worker_id, project_id, task_id, overtime_hours, work_details, memo) (\`date\`, user_id, project_id, task_id, overtime_hours, work_details, memo)
VALUES (?, ?, ?, ?, ?, ?, ?)`, VALUES (?, ?, ?, ?, ?, ?, ?)`,
[ [
date, date,
worker_id, user_id,
project_id, project_id,
task_id || null, task_id || null,
overtime_hours || null, overtime_hours || null,
@@ -74,7 +74,7 @@ const getAllByDate = async (date) => {
const db = await getDb(); const db = await getDb();
const sql = ` const sql = `
SELECT SELECT
wr.worker_id, wr.user_id,
wr.id, wr.id,
wr.\`date\`, wr.\`date\`,
w.worker_name, w.worker_name,
@@ -84,7 +84,7 @@ const getAllByDate = async (date) => {
wr.work_details, wr.work_details,
wr.memo wr.memo
FROM WorkReports wr FROM WorkReports wr
LEFT JOIN workers w ON wr.worker_id = w.worker_id LEFT JOIN workers w ON wr.user_id = w.user_id
LEFT JOIN projects p ON wr.project_id = p.project_id LEFT JOIN projects p ON wr.project_id = p.project_id
LEFT JOIN Tasks t ON wr.task_id = t.task_id LEFT JOIN Tasks t ON wr.task_id = t.task_id
WHERE wr.\`date\` = ? WHERE wr.\`date\` = ?
@@ -100,7 +100,7 @@ const getAllByDate = async (date) => {
const getByRange = async (start, end) => { const getByRange = async (start, end) => {
const db = await getDb(); const db = await getDb();
const [rows] = await db.query( const [rows] = await db.query(
`SELECT id, \`date\`, worker_id, project_id, morning_task_id, afternoon_task_id, overtime_hours, overtime_task_id, work_details, note, memo, created_at, updated_at, morning_project_id, afternoon_project_id, overtime_project_id, task_id FROM WorkReports `SELECT id, \`date\`, user_id, project_id, morning_task_id, afternoon_task_id, overtime_hours, overtime_task_id, work_details, note, memo, created_at, updated_at, morning_project_id, afternoon_project_id, overtime_project_id, task_id FROM WorkReports
WHERE \`date\` BETWEEN ? AND ? WHERE \`date\` BETWEEN ? AND ?
ORDER BY \`date\` ASC`, ORDER BY \`date\` ASC`,
[start, end] [start, end]
@@ -114,7 +114,7 @@ const getByRange = async (start, end) => {
const getById = async (id) => { const getById = async (id) => {
const db = await getDb(); const db = await getDb();
const [rows] = await db.query( const [rows] = await db.query(
`SELECT id, \`date\`, worker_id, project_id, morning_task_id, afternoon_task_id, overtime_hours, overtime_task_id, work_details, note, memo, created_at, updated_at, morning_project_id, afternoon_project_id, overtime_project_id, task_id FROM WorkReports WHERE id = ?`, `SELECT id, \`date\`, user_id, project_id, morning_task_id, afternoon_task_id, overtime_hours, overtime_task_id, work_details, note, memo, created_at, updated_at, morning_project_id, afternoon_project_id, overtime_project_id, task_id FROM WorkReports WHERE id = ?`,
[id] [id]
); );
return rows[0]; return rows[0];
@@ -126,7 +126,7 @@ const getById = async (id) => {
const update = async (id, report) => { const update = async (id, report) => {
const db = await getDb(); const db = await getDb();
const { const {
date, worker_id, project_id, date, user_id, project_id,
task_id, overtime_hours, task_id, overtime_hours,
work_details, memo work_details, memo
} = report; } = report;
@@ -134,7 +134,7 @@ const update = async (id, report) => {
const [result] = await db.query( const [result] = await db.query(
`UPDATE WorkReports `UPDATE WorkReports
SET \`date\` = ?, SET \`date\` = ?,
worker_id = ?, user_id = ?,
project_id = ?, project_id = ?,
task_id = ?, task_id = ?,
overtime_hours = ?, overtime_hours = ?,
@@ -144,7 +144,7 @@ const update = async (id, report) => {
WHERE id = ?`, WHERE id = ?`,
[ [
date, date,
worker_id, user_id,
project_id, project_id,
task_id || null, task_id || null,
overtime_hours || null, overtime_hours || null,
@@ -172,11 +172,11 @@ const remove = async (id) => {
/** /**
* 8. 중복 확인 * 8. 중복 확인
*/ */
const existsByDateAndWorker = async (date, worker_id) => { const existsByDateAndWorker = async (date, user_id) => {
const db = await getDb(); const db = await getDb();
const [rows] = await db.query( const [rows] = await db.query(
`SELECT 1 FROM WorkReports WHERE \`date\` = ? AND worker_id = ? LIMIT 1`, `SELECT 1 FROM WorkReports WHERE \`date\` = ? AND user_id = ? LIMIT 1`,
[date, worker_id] [date, user_id]
); );
return rows.length > 0; return rows.length > 0;
}; };

View File

@@ -39,39 +39,60 @@ const getAll = async () => {
const [rows] = await db.query(` const [rows] = await db.query(`
SELECT SELECT
w.*, w.*,
w.user_id,
CASE WHEN w.status = 'active' THEN 1 ELSE 0 END AS is_active, CASE WHEN w.status = 'active' THEN 1 ELSE 0 END AS is_active,
u.user_id, su.username,
d.department_name d.department_name
FROM workers w FROM workers w
LEFT JOIN users u ON w.worker_id = u.worker_id LEFT JOIN sso_users su ON w.user_id = su.user_id
LEFT JOIN departments d ON w.department_id = d.department_id LEFT JOIN departments d ON w.department_id = d.department_id
ORDER BY w.worker_id DESC ORDER BY w.worker_name ASC
`); `);
return rows; return rows;
}; };
// 3. 단일 조회 // 3. 단일 조회 (worker_id 기준 - 하위 호환성 유지)
const getById = async (worker_id) => { const getById = async (worker_id) => {
const db = await getDb(); const db = await getDb();
const [rows] = await db.query(` const [rows] = await db.query(`
SELECT SELECT
w.*, w.*,
w.user_id,
CASE WHEN w.status = 'active' THEN 1 ELSE 0 END AS is_active, CASE WHEN w.status = 'active' THEN 1 ELSE 0 END AS is_active,
u.user_id, su.username,
d.department_name d.department_name
FROM workers w FROM workers w
LEFT JOIN users u ON w.worker_id = u.worker_id LEFT JOIN sso_users su ON w.user_id = su.user_id
LEFT JOIN departments d ON w.department_id = d.department_id LEFT JOIN departments d ON w.department_id = d.department_id
WHERE w.worker_id = ? WHERE w.worker_id = ?
`, [worker_id]); `, [worker_id]);
return rows[0]; return rows[0];
}; };
// 4. 작업자 수정 // 3-1. 단일 조회 (user_id 기준)
const getByUserId = async (userId) => {
const db = await getDb();
const [rows] = await db.query(`
SELECT
w.*,
w.user_id,
CASE WHEN w.status = 'active' THEN 1 ELSE 0 END AS is_active,
su.username,
d.department_name
FROM workers w
LEFT JOIN sso_users su ON w.user_id = su.user_id
LEFT JOIN departments d ON w.department_id = d.department_id
WHERE w.user_id = ?
`, [userId]);
return rows[0];
};
// 4. 작업자 수정 (worker_id 또는 user_id 기준)
const update = async (worker) => { const update = async (worker) => {
const db = await getDb(); const db = await getDb();
const { const {
worker_id, worker_id,
user_id,
worker_name, worker_name,
job_type, job_type,
status, status,
@@ -123,12 +144,22 @@ const update = async (worker) => {
throw new Error('업데이트할 필드가 없습니다'); throw new Error('업데이트할 필드가 없습니다');
} }
values.push(worker_id); // WHERE 조건 // WHERE 조건: user_id 우선, 없으면 worker_id
let whereClause;
if (user_id !== undefined) {
whereClause = 'user_id = ?';
values.push(user_id);
} else if (worker_id !== undefined) {
whereClause = 'worker_id = ?';
values.push(worker_id);
} else {
throw new Error('worker_id 또는 user_id가 필요합니다');
}
const query = `UPDATE workers SET ${updates.join(', ')} WHERE worker_id = ?`; const query = `UPDATE workers SET ${updates.join(', ')} WHERE ${whereClause}`;
console.log('🔍 실행할 SQL:', query); console.log('실행할 SQL:', query);
console.log('🔍 SQL 파라미터:', values); console.log('SQL 파라미터:', values);
const [result] = await db.query(query, values); const [result] = await db.query(query, values);
@@ -192,6 +223,7 @@ module.exports = {
create, create,
getAll, getAll,
getById, getById,
getByUserId,
update, update,
remove remove
}; };

View File

@@ -539,7 +539,7 @@
charts.workerStats = new Chart(ctx, { charts.workerStats = new Chart(ctx, {
type: 'bar', type: 'bar',
data: { data: {
labels: data.map(item => `작업자 ${item.worker_id}`), labels: data.map(item => `작업자 ${item.user_id}`),
datasets: [{ datasets: [{
label: '총 작업시간', label: '총 작업시간',
data: data.map(item => item.totalHours), data: data.map(item => item.totalHours),
@@ -666,7 +666,7 @@
${data.map(item => ` ${data.map(item => `
<tr> <tr>
<td>${item.report_date}</td> <td>${item.report_date}</td>
<td>작업자 ${item.worker_id}</td> <td>작업자 ${item.user_id}</td>
<td>프로젝트 ${item.project_id}</td> <td>프로젝트 ${item.project_id}</td>
<td>유형 ${item.work_type_id}</td> <td>유형 ${item.work_type_id}</td>
<td>${item.work_hours}시간</td> <td>${item.work_hours}시간</td>

View File

@@ -32,7 +32,7 @@ router.get('/attendance-types', AttendanceController.getAttendanceTypes);
router.get('/vacation-types', AttendanceController.getVacationTypes); router.get('/vacation-types', AttendanceController.getVacationTypes);
// 작업자 휴가 잔여 조회 // 작업자 휴가 잔여 조회
router.get('/vacation-balance/:worker_id', AttendanceController.getWorkerVacationBalance); router.get('/vacation-balance/:user_id', AttendanceController.getWorkerVacationBalance);
// 월별 근태 통계 // 월별 근태 통계
router.get('/monthly-stats', AttendanceController.getMonthlyAttendanceStats); router.get('/monthly-stats', AttendanceController.getMonthlyAttendanceStats);

View File

@@ -26,22 +26,22 @@ router.put('/error-types/:id', dailyWorkReportController.updateErrorType);
router.delete('/error-types/:id', dailyWorkReportController.deleteErrorType); router.delete('/error-types/:id', dailyWorkReportController.deleteErrorType);
// 🔄 누적 관련 새로운 라우트들 (누적입력 시스템 전용) // 🔄 누적 관련 새로운 라우트들 (누적입력 시스템 전용)
router.get('/accumulated', dailyWorkReportController.getAccumulatedReports); // ?date=2024-06-16&worker_id=1 router.get('/accumulated', dailyWorkReportController.getAccumulatedReports); // ?date=2024-06-16&user_id=1
router.get('/contributors', dailyWorkReportController.getContributorsSummary); // ?date=2024-06-16&worker_id=1 router.get('/contributors', dailyWorkReportController.getContributorsSummary); // ?date=2024-06-16&user_id=1
router.get('/my-data', dailyWorkReportController.getMyAccumulatedData); // ?date=2024-06-16&worker_id=1 router.get('/my-data', dailyWorkReportController.getMyAccumulatedData); // ?date=2024-06-16&user_id=1
// ✅ check-overwrite 엔드포인트 추가 (누락된 엔드포인트) // ✅ check-overwrite 엔드포인트 추가 (누락된 엔드포인트)
router.get('/check-overwrite', (req, res) => { router.get('/check-overwrite', (req, res) => {
const { date, worker_id } = req.query; const { date, user_id } = req.query;
if (!date || !worker_id) { if (!date || !user_id) {
return res.status(400).json({ return res.status(400).json({
error: 'date와 worker_id가 필요합니다.', error: 'date와 user_id가 필요합니다.',
example: 'date=2025-06-16&worker_id=1' example: 'date=2025-06-16&user_id=1'
}); });
} }
console.log(`🔍 덮어쓰기 권한 확인: 날짜=${date}, 작업자=${worker_id} (누적입력모드)`); console.log(`🔍 덮어쓰기 권한 확인: 날짜=${date}, 작업자=${user_id} (누적입력모드)`);
// 누적입력 시스템에서는 항상 덮어쓰기 가능 (실제로는 누적만 함) // 누적입력 시스템에서는 항상 덮어쓰기 가능 (실제로는 누적만 함)
res.json({ res.json({
@@ -49,7 +49,7 @@ router.get('/check-overwrite', (req, res) => {
reason: 'accumulate_mode', reason: 'accumulate_mode',
message: '누적입력 모드에서는 항상 추가 가능합니다.', message: '누적입력 모드에서는 항상 추가 가능합니다.',
date, date,
worker_id, user_id,
timestamp: new Date().toISOString() timestamp: new Date().toISOString()
}); });
}); });
@@ -84,7 +84,7 @@ router.get('/', dailyWorkReportController.getDailyWorkReports);
router.put('/:id', dailyWorkReportController.updateWorkReport); router.put('/:id', dailyWorkReportController.updateWorkReport);
// 🗑️ 작업자의 특정 날짜 전체 삭제 // 🗑️ 작업자의 특정 날짜 전체 삭제
router.delete('/date/:date/worker/:worker_id', dailyWorkReportController.removeDailyWorkReportByDateAndWorker); router.delete('/date/:date/worker/:user_id', dailyWorkReportController.removeDailyWorkReportByDateAndWorker);
// 🗑️ 특정 작업보고서 삭제 (항상 가장 마지막에 정의) // 🗑️ 특정 작업보고서 삭제 (항상 가장 마지막에 정의)
router.delete('/:id', dailyWorkReportController.removeDailyWorkReport); router.delete('/:id', dailyWorkReportController.removeDailyWorkReport);

View File

@@ -231,7 +231,7 @@ router.post('/migrations/fix-work-type-id', async (req, res) => {
dwr.report_date dwr.report_date
FROM daily_work_reports dwr FROM daily_work_reports dwr
INNER JOIN tbm_team_assignments ta ON dwr.tbm_assignment_id = ta.assignment_id INNER JOIN tbm_team_assignments ta ON dwr.tbm_assignment_id = ta.assignment_id
INNER JOIN workers w ON dwr.worker_id = w.worker_id INNER JOIN workers w ON dwr.user_id = w.user_id
WHERE dwr.tbm_assignment_id IS NOT NULL WHERE dwr.tbm_assignment_id IS NOT NULL
AND ta.task_id IS NOT NULL AND ta.task_id IS NOT NULL
AND dwr.work_type_id != ta.task_id AND dwr.work_type_id != ta.task_id
@@ -271,7 +271,7 @@ router.post('/migrations/fix-work-type-id', async (req, res) => {
INNER JOIN tbm_team_assignments ta ON dwr.tbm_assignment_id = ta.assignment_id INNER JOIN tbm_team_assignments ta ON dwr.tbm_assignment_id = ta.assignment_id
LEFT JOIN tasks t ON dwr.work_type_id = t.task_id LEFT JOIN tasks t ON dwr.work_type_id = t.task_id
LEFT JOIN work_types wt ON t.work_type_id = wt.id LEFT JOIN work_types wt ON t.work_type_id = wt.id
LEFT JOIN workers w ON dwr.worker_id = w.worker_id LEFT JOIN workers w ON dwr.user_id = w.user_id
WHERE dwr.tbm_assignment_id IS NOT NULL WHERE dwr.tbm_assignment_id IS NOT NULL
ORDER BY dwr.report_date DESC ORDER BY dwr.report_date DESC
LIMIT 10 LIMIT 10

View File

@@ -48,7 +48,7 @@ router.get('/sessions/:sessionId/team', requireAuth, TbmController.getTeamMember
router.delete('/sessions/:sessionId/team/clear', requireAuth, TbmController.clearAllTeamMembers); router.delete('/sessions/:sessionId/team/clear', requireAuth, TbmController.clearAllTeamMembers);
// 팀원 제거 // 팀원 제거
router.delete('/sessions/:sessionId/team/:workerId', requireAuth, TbmController.removeTeamMember); router.delete('/sessions/:sessionId/team/:userId', requireAuth, TbmController.removeTeamMember);
// ==================== 안전 체크리스트 관련 ==================== // ==================== 안전 체크리스트 관련 ====================

View File

@@ -59,7 +59,7 @@ router.get('/me/attendance-records', async (req, res) => {
const AttendanceModel = require('../models/attendanceModel'); const AttendanceModel = require('../models/attendanceModel');
const startDate = `${year}-${String(month).padStart(2, '0')}-01`; const startDate = `${year}-${String(month).padStart(2, '0')}-01`;
const endDate = `${year}-${String(month).padStart(2, '0')}-31`; const endDate = `${year}-${String(month).padStart(2, '0')}-31`;
const records = await AttendanceModel.getDailyRecords(startDate, endDate, req.user.worker_id); const records = await AttendanceModel.getDailyRecords(startDate, endDate, req.user.user_id);
res.json({ success: true, data: records }); res.json({ success: true, data: records });
} catch (error) { } catch (error) {
res.status(500).json({ success: false, error: error.message }); res.status(500).json({ success: false, error: error.message });
@@ -71,7 +71,7 @@ router.get('/me/vacation-balance', async (req, res) => {
try { try {
const AttendanceModel = require('../models/attendanceModel'); const AttendanceModel = require('../models/attendanceModel');
const year = req.query.year || new Date().getFullYear(); const year = req.query.year || new Date().getFullYear();
const balance = await AttendanceModel.getWorkerVacationBalance(req.user.worker_id, year); const balance = await AttendanceModel.getWorkerVacationBalance(req.user.user_id, year);
res.json({ success: true, data: balance }); res.json({ success: true, data: balance });
} catch (error) { } catch (error) {
res.status(500).json({ success: false, error: error.message }); res.status(500).json({ success: false, error: error.message });
@@ -84,8 +84,8 @@ router.get('/me/work-reports', async (req, res) => {
const { startDate, endDate } = req.query; const { startDate, endDate } = req.query;
const db = require('../config/database'); const db = require('../config/database');
const reports = await db.query( const reports = await db.query(
'SELECT * FROM daily_work_reports WHERE worker_id = ? AND report_date BETWEEN ? AND ? ORDER BY report_date DESC', 'SELECT * FROM daily_work_reports WHERE user_id = ? AND report_date BETWEEN ? AND ? ORDER BY report_date DESC',
[req.user.worker_id, startDate, endDate] [req.user.user_id, startDate, endDate]
); );
res.json({ success: true, data: reports }); res.json({ success: true, data: reports });
} catch (error) { } catch (error) {
@@ -104,8 +104,8 @@ router.get('/me/monthly-stats', async (req, res) => {
SUM(total_work_hours) as month_hours, SUM(total_work_hours) as month_hours,
COUNT(DISTINCT record_date) as work_days COUNT(DISTINCT record_date) as work_days
FROM daily_attendance_records FROM daily_attendance_records
WHERE worker_id = ? AND YEAR(record_date) = ? AND MONTH(record_date) = ?`, WHERE user_id = ? AND YEAR(record_date) = ? AND MONTH(record_date) = ?`,
[req.user.worker_id, year, month] [req.user.user_id, year, month]
); );
res.json({ success: true, data: stats[0] || { month_hours: 0, work_days: 0 } }); res.json({ success: true, data: stats[0] || { month_hours: 0, work_days: 0 } });
} catch (error) { } catch (error) {

View File

@@ -11,10 +11,10 @@ const vacationBalanceController = require('../controllers/vacationBalanceControl
router.get('/year/:year', vacationBalanceController.getAllByYear); router.get('/year/:year', vacationBalanceController.getAllByYear);
// 특정 작업자의 휴가 잔액 조회 (특정 연도) // 특정 작업자의 휴가 잔액 조회 (특정 연도)
router.get('/worker/:workerId/year/:year', vacationBalanceController.getByWorkerAndYear); router.get('/worker/:userId/year/:year', vacationBalanceController.getByWorkerAndYear);
// 작업자의 사용 가능한 휴가 일수 조회 // 작업자의 사용 가능한 휴가 일수 조회
router.get('/worker/:workerId/year/:year/available', vacationBalanceController.getAvailableDays); router.get('/worker/:userId/year/:year/available', vacationBalanceController.getAvailableDays);
// 근속년수 기반 연차 자동 계산 및 생성 (관리자만) // 근속년수 기반 연차 자동 계산 및 생성 (관리자만)
router.post('/auto-calculate', vacationBalanceController.autoCalculateAndCreate); router.post('/auto-calculate', vacationBalanceController.autoCalculateAndCreate);

View File

@@ -96,7 +96,7 @@ router.get('/', workerController.getAllWorkers);
/** /**
* @swagger * @swagger
* /api/workers/{worker_id}: * /api/workers/{user_id}:
* get: * get:
* tags: [Workers] * tags: [Workers]
* summary: 특정 작업자 조회 * summary: 특정 작업자 조회
@@ -105,7 +105,7 @@ router.get('/', workerController.getAllWorkers);
* - bearerAuth: [] * - bearerAuth: []
* parameters: * parameters:
* - in: path * - in: path
* name: worker_id * name: user_id
* required: true * required: true
* schema: * schema:
* type: integer * type: integer
@@ -142,7 +142,7 @@ router.get('/', workerController.getAllWorkers);
* - bearerAuth: [] * - bearerAuth: []
* parameters: * parameters:
* - in: path * - in: path
* name: worker_id * name: user_id
* required: true * required: true
* schema: * schema:
* type: integer * type: integer
@@ -193,7 +193,7 @@ router.get('/', workerController.getAllWorkers);
* - bearerAuth: [] * - bearerAuth: []
* parameters: * parameters:
* - in: path * - in: path
* name: worker_id * name: user_id
* required: true * required: true
* schema: * schema:
* type: integer * type: integer
@@ -221,8 +221,8 @@ router.get('/', workerController.getAllWorkers);
* 500: * 500:
* description: 서버 오류 * description: 서버 오류
*/ */
router.get('/:worker_id', workerController.getWorkerById); router.get('/:user_id', workerController.getWorkerById);
router.put('/:worker_id', workerController.updateWorker); router.put('/:user_id', workerController.updateWorker);
router.delete('/:worker_id', workerController.removeWorker); router.delete('/:user_id', workerController.removeWorker);
module.exports = router; module.exports = router;

View File

@@ -53,7 +53,7 @@ const getDailyAttendanceStatusService = async (date) => {
/** /**
* 일일 근태 기록 조회 * 일일 근태 기록 조회
*/ */
const getDailyAttendanceRecordsService = async (date, workerId = null) => { const getDailyAttendanceRecordsService = async (date, userId = null) => {
if (!date) { if (!date) {
throw new ValidationError('날짜가 필요합니다', { throw new ValidationError('날짜가 필요합니다', {
required: ['date'], required: ['date'],
@@ -61,10 +61,10 @@ const getDailyAttendanceRecordsService = async (date, workerId = null) => {
}); });
} }
logger.info('일일 근태 기록 조회 요청', { date, workerId }); logger.info('일일 근태 기록 조회 요청', { date, userId });
try { try {
const records = await AttendanceModel.getDailyAttendanceRecords(date, workerId); const records = await AttendanceModel.getDailyAttendanceRecords(date, userId);
logger.info('일일 근태 기록 조회 성공', { date, count: records.length }); logger.info('일일 근태 기록 조회 성공', { date, count: records.length });
return records; return records;
} catch (error) { } catch (error) {
@@ -76,7 +76,7 @@ const getDailyAttendanceRecordsService = async (date, workerId = null) => {
/** /**
* 기간별 근태 기록 조회 (월별 조회용) * 기간별 근태 기록 조회 (월별 조회용)
*/ */
const getAttendanceRecordsByRangeService = async (startDate, endDate, workerId = null) => { const getAttendanceRecordsByRangeService = async (startDate, endDate, userId = null) => {
if (!startDate || !endDate) { if (!startDate || !endDate) {
throw new ValidationError('시작 날짜와 종료 날짜가 필요합니다', { throw new ValidationError('시작 날짜와 종료 날짜가 필요합니다', {
required: ['start_date', 'end_date'], required: ['start_date', 'end_date'],
@@ -84,10 +84,10 @@ const getAttendanceRecordsByRangeService = async (startDate, endDate, workerId =
}); });
} }
logger.info('기간별 근태 기록 조회 요청', { startDate, endDate, workerId }); logger.info('기간별 근태 기록 조회 요청', { startDate, endDate, userId });
try { try {
const records = await AttendanceModel.getDailyRecords(startDate, endDate, workerId); const records = await AttendanceModel.getDailyRecords(startDate, endDate, userId);
logger.info('기간별 근태 기록 조회 성공', { startDate, endDate, count: records.length }); logger.info('기간별 근태 기록 조회 성공', { startDate, endDate, count: records.length });
return records; return records;
} catch (error) { } catch (error) {
@@ -103,7 +103,7 @@ const getAttendanceRecordsByRangeService = async (startDate, endDate, workerId =
const upsertAttendanceRecordService = async (recordData) => { const upsertAttendanceRecordService = async (recordData) => {
const { const {
record_date, record_date,
worker_id, user_id,
total_work_hours, total_work_hours,
attendance_type_id, attendance_type_id,
vacation_type_id, vacation_type_id,
@@ -115,25 +115,25 @@ const upsertAttendanceRecordService = async (recordData) => {
} = recordData; } = recordData;
// 필수 필드 검증 // 필수 필드 검증
if (!record_date || !worker_id) { if (!record_date || !user_id) {
throw new ValidationError('필수 필드가 누락되었습니다', { throw new ValidationError('필수 필드가 누락되었습니다', {
required: ['record_date', 'worker_id'], required: ['record_date', 'user_id'],
received: { record_date, worker_id } received: { record_date, user_id }
}); });
} }
logger.info('근태 기록 저장 요청', { record_date, worker_id, vacation_type_id }); logger.info('근태 기록 저장 요청', { record_date, user_id, vacation_type_id });
try { try {
// 1. 기존 기록 조회 (휴가 연동을 위해) // 1. 기존 기록 조회 (휴가 연동을 위해)
const existingRecords = await AttendanceModel.getDailyAttendanceRecords(record_date, worker_id); const existingRecords = await AttendanceModel.getDailyAttendanceRecords(record_date, user_id);
const existingRecord = existingRecords.find(r => r.worker_id === worker_id); const existingRecord = existingRecords.find(r => r.user_id === user_id);
const previousVacationTypeId = existingRecord?.vacation_type_id || null; const previousVacationTypeId = existingRecord?.vacation_type_id || null;
// 2. 근태 기록 저장 // 2. 근태 기록 저장
const result = await AttendanceModel.upsertAttendanceRecord({ const result = await AttendanceModel.upsertAttendanceRecord({
record_date, record_date,
worker_id, user_id,
total_work_hours, total_work_hours,
work_attendance_type_id: attendance_type_id, work_attendance_type_id: attendance_type_id,
vacation_type_id, vacation_type_id,
@@ -150,21 +150,21 @@ const upsertAttendanceRecordService = async (recordData) => {
if (previousDays !== newDays) { if (previousDays !== newDays) {
// 이전 휴가 복구 // 이전 휴가 복구
if (previousDays > 0) { if (previousDays > 0) {
await vacationBalanceModel.restoreByPriority(worker_id, year, previousDays); await vacationBalanceModel.restoreByPriority(user_id, year, previousDays);
logger.info('휴가 잔액 복구', { worker_id, year, restored: previousDays }); logger.info('휴가 잔액 복구', { user_id, year, restored: previousDays });
} }
// 새 휴가 차감 // 새 휴가 차감
if (newDays > 0) { if (newDays > 0) {
await vacationBalanceModel.deductByPriority(worker_id, year, newDays); await vacationBalanceModel.deductByPriority(user_id, year, newDays);
logger.info('휴가 잔액 차감', { worker_id, year, deducted: newDays }); logger.info('휴가 잔액 차감', { user_id, year, deducted: newDays });
} }
} }
logger.info('근태 기록 저장 성공', { record_date, worker_id }); logger.info('근태 기록 저장 성공', { record_date, user_id });
return result; return result;
} catch (error) { } catch (error) {
logger.error('근태 기록 저장 실패', { record_date, worker_id, error: error.message }); logger.error('근태 기록 저장 실패', { record_date, user_id, error: error.message });
throw new DatabaseError('근태 기록 저장 중 데이터베이스 오류가 발생했습니다'); throw new DatabaseError('근태 기록 저장 중 데이터베이스 오류가 발생했습니다');
} }
}; };
@@ -173,28 +173,28 @@ const upsertAttendanceRecordService = async (recordData) => {
* 휴가 처리 * 휴가 처리
*/ */
const processVacationService = async (vacationData) => { const processVacationService = async (vacationData) => {
const { record_date, worker_id, vacation_type_id } = vacationData; const { record_date, user_id, vacation_type_id } = vacationData;
if (!record_date || !worker_id || !vacation_type_id) { if (!record_date || !user_id || !vacation_type_id) {
throw new ValidationError('필수 필드가 누락되었습니다', { throw new ValidationError('필수 필드가 누락되었습니다', {
required: ['record_date', 'worker_id', 'vacation_type_id'], required: ['record_date', 'user_id', 'vacation_type_id'],
received: { record_date, worker_id, vacation_type_id } received: { record_date, user_id, vacation_type_id }
}); });
} }
logger.info('휴가 처리 요청', { record_date, worker_id, vacation_type_id }); logger.info('휴가 처리 요청', { record_date, user_id, vacation_type_id });
try { try {
const result = await AttendanceModel.processVacation({ const result = await AttendanceModel.processVacation({
record_date, record_date,
worker_id, user_id,
vacation_type_id vacation_type_id
}); });
logger.info('휴가 처리 성공', { record_date, worker_id }); logger.info('휴가 처리 성공', { record_date, user_id });
return result; return result;
} catch (error) { } catch (error) {
logger.error('휴가 처리 실패', { record_date, worker_id, error: error.message }); logger.error('휴가 처리 실패', { record_date, user_id, error: error.message });
throw new DatabaseError('휴가 처리 중 데이터베이스 오류가 발생했습니다'); throw new DatabaseError('휴가 처리 중 데이터베이스 오류가 발생했습니다');
} }
}; };
@@ -203,28 +203,28 @@ const processVacationService = async (vacationData) => {
* 초과 근무 승인 * 초과 근무 승인
*/ */
const approveOvertimeService = async (overtimeData) => { const approveOvertimeService = async (overtimeData) => {
const { record_date, worker_id, overtime_approved } = overtimeData; const { record_date, user_id, overtime_approved } = overtimeData;
if (!record_date || !worker_id || overtime_approved === undefined) { if (!record_date || !user_id || overtime_approved === undefined) {
throw new ValidationError('필수 필드가 누락되었습니다', { throw new ValidationError('필수 필드가 누락되었습니다', {
required: ['record_date', 'worker_id', 'overtime_approved'], required: ['record_date', 'user_id', 'overtime_approved'],
received: { record_date, worker_id, overtime_approved } received: { record_date, user_id, overtime_approved }
}); });
} }
logger.info('초과 근무 승인 요청', { record_date, worker_id, overtime_approved }); logger.info('초과 근무 승인 요청', { record_date, user_id, overtime_approved });
try { try {
const result = await AttendanceModel.approveOvertime({ const result = await AttendanceModel.approveOvertime({
record_date, record_date,
worker_id, user_id,
overtime_approved overtime_approved
}); });
logger.info('초과 근무 승인 처리 성공', { record_date, worker_id }); logger.info('초과 근무 승인 처리 성공', { record_date, user_id });
return result; return result;
} catch (error) { } catch (error) {
logger.error('초과 근무 승인 실패', { record_date, worker_id, error: error.message }); logger.error('초과 근무 승인 실패', { record_date, user_id, error: error.message });
throw new DatabaseError('초과 근무 승인 중 데이터베이스 오류가 발생했습니다'); throw new DatabaseError('초과 근무 승인 중 데이터베이스 오류가 발생했습니다');
} }
}; };
@@ -264,22 +264,22 @@ const getVacationTypesService = async () => {
/** /**
* 작업자 휴가 잔여 일수 조회 * 작업자 휴가 잔여 일수 조회
*/ */
const getWorkerVacationBalanceService = async (workerId) => { const getWorkerVacationBalanceService = async (userId) => {
if (!workerId) { if (!userId) {
throw new ValidationError('작업자 ID가 필요합니다', { throw new ValidationError('작업자 ID가 필요합니다', {
required: ['worker_id'], required: ['user_id'],
received: { workerId } received: { userId }
}); });
} }
logger.info('휴가 잔여 일수 조회 요청', { workerId }); logger.info('휴가 잔여 일수 조회 요청', { userId });
try { try {
const balance = await AttendanceModel.getWorkerVacationBalance(workerId); const balance = await AttendanceModel.getWorkerVacationBalance(userId);
logger.info('휴가 잔여 일수 조회 성공', { workerId }); logger.info('휴가 잔여 일수 조회 성공', { userId });
return balance; return balance;
} catch (error) { } catch (error) {
logger.error('휴가 잔여 일수 조회 실패', { workerId, error: error.message }); logger.error('휴가 잔여 일수 조회 실패', { userId, error: error.message });
throw new DatabaseError('휴가 잔여 일수 조회 중 데이터베이스 오류가 발생했습니다'); throw new DatabaseError('휴가 잔여 일수 조회 중 데이터베이스 오류가 발생했습니다');
} }
}; };
@@ -287,7 +287,7 @@ const getWorkerVacationBalanceService = async (workerId) => {
/** /**
* 월별 근태 통계 조회 * 월별 근태 통계 조회
*/ */
const getMonthlyAttendanceStatsService = async (year, month, workerId = null) => { const getMonthlyAttendanceStatsService = async (year, month, userId = null) => {
if (!year || !month) { if (!year || !month) {
throw new ValidationError('연도와 월이 필요합니다', { throw new ValidationError('연도와 월이 필요합니다', {
required: ['year', 'month'], required: ['year', 'month'],
@@ -295,10 +295,10 @@ const getMonthlyAttendanceStatsService = async (year, month, workerId = null) =>
}); });
} }
logger.info('월별 근태 통계 조회 요청', { year, month, workerId }); logger.info('월별 근태 통계 조회 요청', { year, month, userId });
try { try {
const stats = await AttendanceModel.getMonthlyAttendanceStats(year, month, workerId); const stats = await AttendanceModel.getMonthlyAttendanceStats(year, month, userId);
logger.info('월별 근태 통계 조회 성공', { year, month }); logger.info('월별 근태 통계 조회 성공', { year, month });
return stats; return stats;
} catch (error) { } catch (error) {
@@ -347,21 +347,21 @@ const saveCheckinsService = async (date, checkins) => {
const results = []; const results = [];
for (const checkin of checkins) { for (const checkin of checkins) {
const { worker_id, is_present } = checkin; const { user_id, is_present } = checkin;
if (!worker_id || is_present === undefined) { if (!user_id || is_present === undefined) {
logger.warn('출근 체크 데이터 누락', { checkin }); logger.warn('출근 체크 데이터 누락', { checkin });
continue; continue;
} }
const result = await AttendanceModel.upsertCheckin({ const result = await AttendanceModel.upsertCheckin({
worker_id, user_id,
record_date: date, record_date: date,
is_present is_present
}); });
results.push({ results.push({
worker_id, user_id,
record_id: result, record_id: result,
is_present is_present
}); });

View File

@@ -57,7 +57,7 @@ const loginService = async (username, password, ipAddress, userAgent) => {
const token = jwt.sign( const token = jwt.sign(
{ user_id: user.user_id, username: user.username, role: user.role_name, role_id: user.role_id, access_level: user.access_level, worker_id: user.worker_id, name: user.name || user.username }, { user_id: user.user_id, username: user.username, role: user.role_name, role_id: user.role_id, access_level: user.access_level, name: user.name || user.username },
process.env.JWT_SECRET || 'your-secret-key', process.env.JWT_SECRET || 'your-secret-key',
{ expiresIn: process.env.JWT_EXPIRES_IN || '24h' } { expiresIn: process.env.JWT_EXPIRES_IN || '24h' }
); );
@@ -81,8 +81,7 @@ const loginService = async (username, password, ipAddress, userAgent) => {
username: user.username, username: user.username,
name: user.name || user.username, name: user.name || user.username,
role: user.role_name, role: user.role_name,
access_level: user.access_level, access_level: user.access_level
worker_id: user.worker_id
} }
} }
}; };

View File

@@ -22,23 +22,23 @@ const logger = require('../utils/logger');
* @param {string} issueData.start_time - 이슈 시작 시간 * @param {string} issueData.start_time - 이슈 시작 시간
* @param {string} issueData.end_time - 이슈 종료 시간 * @param {string} issueData.end_time - 이슈 종료 시간
* @param {number} issueData.issue_type_id - 이슈 유형 ID * @param {number} issueData.issue_type_id - 이슈 유형 ID
* @param {number[]} issueData.worker_ids - 작업자 ID 배열 * @param {number[]} issueData.user_ids - 작업자 ID 배열
* @returns {Promise<object>} 생성 결과 * @returns {Promise<object>} 생성 결과
*/ */
const createDailyIssueReportService = async (issueData) => { const createDailyIssueReportService = async (issueData) => {
const { date, project_id, start_time, end_time, issue_type_id, worker_ids } = issueData; const { date, project_id, start_time, end_time, issue_type_id, user_ids } = issueData;
// 필수 필드 검증 // 필수 필드 검증
if (!date || !project_id || !start_time || !end_time || !issue_type_id || !worker_ids) { if (!date || !project_id || !start_time || !end_time || !issue_type_id || !user_ids) {
throw new ValidationError('필수 필드가 누락되었습니다', { throw new ValidationError('필수 필드가 누락되었습니다', {
required: ['date', 'project_id', 'start_time', 'end_time', 'issue_type_id', 'worker_ids'], required: ['date', 'project_id', 'start_time', 'end_time', 'issue_type_id', 'user_ids'],
received: { date, project_id, start_time, end_time, issue_type_id, worker_ids: !!worker_ids } received: { date, project_id, start_time, end_time, issue_type_id, user_ids: !!user_ids }
}); });
} }
if (!Array.isArray(worker_ids) || worker_ids.length === 0) { if (!Array.isArray(user_ids) || user_ids.length === 0) {
throw new ValidationError('worker_ids는 최소 한 명 이상의 작업자를 포함하는 배열이어야 합니다', { throw new ValidationError('user_ids는 최소 한 명 이상의 작업자를 포함하는 배열이어야 합니다', {
received: { worker_ids, isArray: Array.isArray(worker_ids), length: worker_ids?.length } received: { user_ids, isArray: Array.isArray(user_ids), length: user_ids?.length }
}); });
} }
@@ -46,17 +46,17 @@ const createDailyIssueReportService = async (issueData) => {
date, date,
project_id, project_id,
issue_type_id, issue_type_id,
worker_count: worker_ids.length worker_count: user_ids.length
}); });
// 모델에 전달할 데이터 준비 // 모델에 전달할 데이터 준비
const reportsToCreate = worker_ids.map(worker_id => ({ const reportsToCreate = user_ids.map(user_id => ({
date, date,
project_id, project_id,
start_time, start_time,
end_time, end_time,
issue_type_id, issue_type_id,
worker_id user_id
})); }));
try { try {
@@ -75,7 +75,7 @@ const createDailyIssueReportService = async (issueData) => {
logger.error('이슈 보고서 생성 실패', { logger.error('이슈 보고서 생성 실패', {
date, date,
project_id, project_id,
worker_ids, user_ids,
error: error.message error: error.message
}); });
throw new DatabaseError('이슈 보고서 생성 중 오류가 발생했습니다'); throw new DatabaseError('이슈 보고서 생성 중 오류가 발생했습니다');

View File

@@ -17,13 +17,13 @@ const logger = require('../utils/logger');
* @returns {Promise<object>} 생성 결과 또는 에러 * @returns {Promise<object>} 생성 결과 또는 에러
*/ */
const createDailyWorkReportService = async (reportData) => { const createDailyWorkReportService = async (reportData) => {
const { report_date, worker_id, work_entries, created_by, created_by_name } = reportData; const { report_date, user_id, work_entries, created_by, created_by_name } = reportData;
// 1. 기본 유효성 검사 // 1. 기본 유효성 검사
if (!report_date || !worker_id || !work_entries) { if (!report_date || !user_id || !work_entries) {
throw new ValidationError('필수 필드가 누락되었습니다', { throw new ValidationError('필수 필드가 누락되었습니다', {
required: ['report_date', 'worker_id', 'work_entries'], required: ['report_date', 'user_id', 'work_entries'],
received: { report_date, worker_id, work_entries: !!work_entries } received: { report_date, user_id, work_entries: !!work_entries }
}); });
} }
@@ -70,7 +70,7 @@ const createDailyWorkReportService = async (reportData) => {
// 3. 모델에 전달할 데이터 준비 // 3. 모델에 전달할 데이터 준비
const modelData = { const modelData = {
report_date, report_date,
worker_id: parseInt(worker_id), user_id: parseInt(user_id),
entries: work_entries.map(entry => ({ entries: work_entries.map(entry => ({
project_id: entry.project_id, project_id: entry.project_id,
work_type_id: entry.task_id, // task_id를 work_type_id로 매핑 work_type_id: entry.task_id, // task_id를 work_type_id로 매핑
@@ -83,7 +83,7 @@ const createDailyWorkReportService = async (reportData) => {
logger.info('작업보고서 생성 요청', { logger.info('작업보고서 생성 요청', {
date: report_date, date: report_date,
worker: worker_id, worker: user_id,
creator: created_by_name, creator: created_by_name,
entries_count: modelData.entries.length entries_count: modelData.entries.length
}); });
@@ -117,7 +117,7 @@ const createDailyWorkReportService = async (reportData) => {
* @returns {Promise<Array>} 조회된 작업 보고서 배열 * @returns {Promise<Array>} 조회된 작업 보고서 배열
*/ */
const getDailyWorkReportsService = async (queryParams, userInfo) => { const getDailyWorkReportsService = async (queryParams, userInfo) => {
const { date, start_date, end_date, worker_id, created_by: requested_created_by, view_all } = queryParams; const { date, start_date, end_date, user_id, created_by: requested_created_by, view_all } = queryParams;
const { user_id: current_user_id, role } = userInfo; const { user_id: current_user_id, role } = userInfo;
// 날짜 또는 날짜 범위 중 하나는 필수 // 날짜 또는 날짜 범위 중 하나는 필수
@@ -142,8 +142,8 @@ const getDailyWorkReportsService = async (queryParams, userInfo) => {
options.end_date = end_date; options.end_date = end_date;
} }
if (worker_id) { if (user_id) {
options.worker_id = parseInt(worker_id); options.user_id = parseInt(user_id);
} }
// 최종적으로 필터링할 작성자 ID 결정 // 최종적으로 필터링할 작성자 ID 결정
@@ -285,16 +285,16 @@ const getStatisticsService = async (queryParams) => {
/** /**
* 일일 또는 작업자별 작업 요약 정보를 조회하는 비즈니스 로직을 처리합니다. * 일일 또는 작업자별 작업 요약 정보를 조회하는 비즈니스 로직을 처리합니다.
* @param {object} queryParams - 컨트롤러에서 전달된 쿼리 파라미터 (date 또는 worker_id) * @param {object} queryParams - 컨트롤러에서 전달된 쿼리 파라미터 (date 또는 user_id)
* @returns {Promise<object>} 요약 데이터 * @returns {Promise<object>} 요약 데이터
*/ */
const getSummaryService = async (queryParams) => { const getSummaryService = async (queryParams) => {
const { date, worker_id } = queryParams; const { date, user_id } = queryParams;
if (!date && !worker_id) { if (!date && !user_id) {
throw new ValidationError('날짜 또는 작업자 ID가 필요합니다', { throw new ValidationError('날짜 또는 작업자 ID가 필요합니다', {
required: 'date OR worker_id', required: 'date OR user_id',
received: { date, worker_id } received: { date, user_id }
}); });
} }
@@ -304,10 +304,10 @@ const getSummaryService = async (queryParams) => {
const result = await dailyWorkReportModel.getSummaryByDate(date); const result = await dailyWorkReportModel.getSummaryByDate(date);
logger.info('일일 요약 조회 성공', { date }); logger.info('일일 요약 조회 성공', { date });
return result; return result;
} else { // worker_id } else { // user_id
logger.info('작업자별 요약 조회 요청', { worker_id }); logger.info('작업자별 요약 조회 요청', { user_id });
const result = await dailyWorkReportModel.getSummaryByWorker(worker_id); const result = await dailyWorkReportModel.getSummaryByWorker(user_id);
logger.info('작업자별 요약 조회 성공', { worker_id }); logger.info('작업자별 요약 조회 성공', { user_id });
return result; return result;
} }
} catch (error) { } catch (error) {

View File

@@ -286,7 +286,7 @@ const optimizedQueries = {
let baseQuery = ` let baseQuery = `
SELECT w.*, d.department_name, COUNT(dwr.id) as report_count SELECT w.*, d.department_name, COUNT(dwr.id) as report_count
FROM workers w FROM workers w
LEFT JOIN daily_work_reports dwr ON w.worker_id = dwr.worker_id LEFT JOIN daily_work_reports dwr ON w.user_id = dwr.user_id
LEFT JOIN departments d ON w.department_id = d.department_id LEFT JOIN departments d ON w.department_id = d.department_id
`; `;
@@ -322,10 +322,10 @@ const optimizedQueries = {
countQuery += whereClause; countQuery += whereClause;
} }
baseQuery += ' GROUP BY w.worker_id'; baseQuery += ' GROUP BY w.user_id';
return executePagedQuery(baseQuery, countQuery, params, { return executePagedQuery(baseQuery, countQuery, params, {
page, limit, orderBy: 'w.worker_id', orderDirection: 'DESC' page, limit, orderBy: 'w.user_id', orderDirection: 'DESC'
}); });
}, },
@@ -362,7 +362,7 @@ const optimizedQueries = {
wt.name as work_type_name, wst.name as work_status_name, wt.name as work_type_name, wst.name as work_status_name,
et.name as error_type_name, u.name as created_by_name et.name as error_type_name, u.name as created_by_name
FROM daily_work_reports dwr FROM daily_work_reports dwr
LEFT JOIN workers w ON dwr.worker_id = w.worker_id LEFT JOIN workers w ON dwr.user_id = w.user_id
LEFT JOIN projects p ON dwr.project_id = p.project_id LEFT JOIN projects p ON dwr.project_id = p.project_id
LEFT JOIN work_types wt ON dwr.work_type_id = wt.id LEFT JOIN work_types wt ON dwr.work_type_id = wt.id
LEFT JOIN work_status_types wst ON dwr.work_status_id = wst.id LEFT JOIN work_status_types wst ON dwr.work_status_id = wst.id

View File

@@ -242,14 +242,14 @@ const schemas = {
password: { required: true, password: true }, password: { required: true, password: true },
name: { required: true, type: 'string', minLength: 2, maxLength: 100 }, name: { required: true, type: 'string', minLength: 2, maxLength: 100 },
access_level: { required: true, enum: ['user', 'admin', 'system'] }, access_level: { required: true, enum: ['user', 'admin', 'system'] },
worker_id: { type: 'integer' } user_id: { type: 'integer' }
}, },
// 사용자 업데이트 // 사용자 업데이트
updateUser: { updateUser: {
name: { type: 'string', minLength: 2, maxLength: 100 }, name: { type: 'string', minLength: 2, maxLength: 100 },
access_level: { enum: ['user', 'admin', 'system'] }, access_level: { enum: ['user', 'admin', 'system'] },
worker_id: { type: 'integer' } user_id: { type: 'integer' }
}, },
// 비밀번호 변경 // 비밀번호 변경
@@ -261,7 +261,7 @@ const schemas = {
// 일일 작업 보고서 생성 (배열 형태) // 일일 작업 보고서 생성 (배열 형태)
createDailyWorkReport: { createDailyWorkReport: {
report_date: { required: true, type: 'date' }, report_date: { required: true, type: 'date' },
worker_id: { required: true, type: 'integer' }, user_id: { required: true, type: 'integer' },
work_entries: { required: true, type: 'array' }, work_entries: { required: true, type: 'array' },
created_by: { type: 'integer' } created_by: { type: 'integer' }
}, },

View File

@@ -0,0 +1,318 @@
/**
* Daily Work Report - Module Loader
* 작업보고서 모듈을 초기화하고 연결하는 메인 진입점
*
* 로드 순서:
* 1. state.js - 전역 상태 관리
* 2. utils.js - 유틸리티 함수
* 3. api.js - API 클라이언트
* 4. index.js - 이 파일 (메인 컨트롤러)
*/
class DailyWorkReportController {
constructor() {
this.state = window.DailyWorkReportState;
this.api = window.DailyWorkReportAPI;
this.utils = window.DailyWorkReportUtils;
this.initialized = false;
console.log('[Controller] DailyWorkReportController 생성');
}
/**
* 초기화
*/
async init() {
if (this.initialized) {
console.log('[Controller] 이미 초기화됨');
return;
}
console.log('[Controller] 초기화 시작...');
try {
// 이벤트 리스너 설정
this.setupEventListeners();
// 기본 데이터 로드
await this.api.loadAllData();
// TBM 탭이 기본
await this.switchTab('tbm');
this.initialized = true;
console.log('[Controller] 초기화 완료');
} catch (error) {
console.error('[Controller] 초기화 실패:', error);
window.showMessage?.('초기화 중 오류가 발생했습니다: ' + error.message, 'error');
}
}
/**
* 이벤트 리스너 설정
*/
setupEventListeners() {
// 탭 버튼
const tbmBtn = document.getElementById('tbmReportTab');
const completedBtn = document.getElementById('completedReportTab');
if (tbmBtn) {
tbmBtn.addEventListener('click', () => this.switchTab('tbm'));
}
if (completedBtn) {
completedBtn.addEventListener('click', () => this.switchTab('completed'));
}
// 완료 보고서 날짜 변경
const completedDateInput = document.getElementById('completedReportDate');
if (completedDateInput) {
completedDateInput.addEventListener('change', () => this.loadCompletedReports());
}
console.log('[Controller] 이벤트 리스너 설정 완료');
}
/**
* 탭 전환
*/
async switchTab(tab) {
this.state.setCurrentTab(tab);
const tbmBtn = document.getElementById('tbmReportTab');
const completedBtn = document.getElementById('completedReportTab');
const tbmSection = document.getElementById('tbmReportSection');
const completedSection = document.getElementById('completedReportSection');
// 모든 탭 버튼 비활성화
tbmBtn?.classList.remove('active');
completedBtn?.classList.remove('active');
// 모든 섹션 숨기기
if (tbmSection) tbmSection.style.display = 'none';
if (completedSection) completedSection.style.display = 'none';
// 선택된 탭 활성화
if (tab === 'tbm') {
tbmBtn?.classList.add('active');
if (tbmSection) tbmSection.style.display = 'block';
await this.loadTbmData();
} else if (tab === 'completed') {
completedBtn?.classList.add('active');
if (completedSection) completedSection.style.display = 'block';
// 오늘 날짜로 초기화
const dateInput = document.getElementById('completedReportDate');
if (dateInput) {
dateInput.value = this.utils.getKoreaToday();
}
await this.loadCompletedReports();
}
}
/**
* TBM 데이터 로드
*/
async loadTbmData() {
try {
await this.api.loadIncompleteTbms();
await this.api.loadDailyIssuesForTbms();
// 렌더링은 기존 함수 사용 (점진적 마이그레이션)
if (typeof window.renderTbmWorkList === 'function') {
window.renderTbmWorkList();
}
} catch (error) {
console.error('[Controller] TBM 데이터 로드 오류:', error);
window.showMessage?.('TBM 데이터를 불러오는 중 오류가 발생했습니다.', 'error');
}
}
/**
* 완료 보고서 로드
*/
async loadCompletedReports() {
try {
const dateInput = document.getElementById('completedReportDate');
const date = dateInput?.value || this.utils.getKoreaToday();
const reports = await this.api.loadCompletedReports(date);
// 렌더링은 기존 함수 사용
if (typeof window.renderCompletedReports === 'function') {
window.renderCompletedReports(reports);
}
} catch (error) {
console.error('[Controller] 완료 보고서 로드 오류:', error);
window.showMessage?.('완료 보고서를 불러오는 중 오류가 발생했습니다.', 'error');
}
}
/**
* TBM 작업보고서 제출
*/
async submitTbmWorkReport(index) {
try {
const tbm = this.state.incompleteTbms[index];
if (!tbm) {
throw new Error('TBM 데이터를 찾을 수 없습니다.');
}
// 유효성 검사
const totalHoursInput = document.getElementById(`totalHours_${index}`);
const totalHours = parseFloat(totalHoursInput?.value);
if (!totalHours || totalHours <= 0) {
window.showMessage?.('작업시간을 입력해주세요.', 'warning');
return;
}
// 부적합 시간 계산
const defects = this.state.tempDefects[index] || [];
const errorHours = defects.reduce((sum, d) => sum + (parseFloat(d.defect_hours) || 0), 0);
const regularHours = totalHours - errorHours;
if (regularHours < 0) {
window.showMessage?.('부적합 시간이 총 작업시간을 초과할 수 없습니다.', 'warning');
return;
}
// API 데이터 구성
const user = this.state.getCurrentUser();
const reportData = {
tbm_session_id: tbm.session_id,
tbm_assignment_id: tbm.assignment_id,
user_id: tbm.user_id,
project_id: tbm.project_id,
work_type_id: tbm.work_type_id,
report_date: this.utils.formatDateForApi(tbm.session_date),
total_hours: totalHours,
regular_hours: regularHours,
error_hours: errorHours,
work_status_id: errorHours > 0 ? 2 : 1,
created_by: user?.user_id || user?.id,
defects: defects.map(d => ({
category_id: d.category_id,
item_id: d.item_id,
issue_report_id: d.issue_report_id,
defect_hours: d.defect_hours,
note: d.note
}))
};
const result = await this.api.submitTbmWorkReport(reportData);
window.showSaveResultModal?.(
'success',
'제출 완료',
`${tbm.worker_name}의 작업보고서가 제출되었습니다.`
);
// 목록 새로고침
await this.loadTbmData();
} catch (error) {
console.error('[Controller] 제출 오류:', error);
window.showSaveResultModal?.(
'error',
'제출 실패',
error.message || '작업보고서 제출 중 오류가 발생했습니다.'
);
}
}
/**
* 세션 일괄 제출
*/
async batchSubmitSession(sessionKey) {
const rows = document.querySelectorAll(`tr[data-session-key="${sessionKey}"][data-type="tbm"]`);
const indices = [];
rows.forEach(row => {
const index = parseInt(row.dataset.index);
const totalHoursInput = document.getElementById(`totalHours_${index}`);
if (totalHoursInput?.value && parseFloat(totalHoursInput.value) > 0) {
indices.push(index);
}
});
if (indices.length === 0) {
window.showMessage?.('제출할 항목이 없습니다. 작업시간을 입력해주세요.', 'warning');
return;
}
const confirmed = confirm(`${indices.length}건의 작업보고서를 일괄 제출하시겠습니까?`);
if (!confirmed) return;
let successCount = 0;
let failCount = 0;
for (const index of indices) {
try {
await this.submitTbmWorkReport(index);
successCount++;
} catch (error) {
failCount++;
console.error(`[Controller] 일괄 제출 오류 (index: ${index}):`, error);
}
}
if (failCount === 0) {
window.showSaveResultModal?.('success', '일괄 제출 완료', `${successCount}건이 성공적으로 제출되었습니다.`);
} else {
window.showSaveResultModal?.('warning', '일괄 제출 부분 완료', `성공: ${successCount}건, 실패: ${failCount}`);
}
}
/**
* 상태 디버그
*/
debug() {
console.log('[Controller] 상태 디버그:');
this.state.debug();
}
}
// 전역 인스턴스 생성
window.DailyWorkReportController = new DailyWorkReportController();
// 하위 호환성: 기존 전역 함수들
window.switchTab = (tab) => window.DailyWorkReportController.switchTab(tab);
window.submitTbmWorkReport = (index) => window.DailyWorkReportController.submitTbmWorkReport(index);
window.batchSubmitTbmSession = (sessionKey) => window.DailyWorkReportController.batchSubmitSession(sessionKey);
// 사용자 정보 함수
window.getUser = () => window.DailyWorkReportState.getUser();
window.getCurrentUser = () => window.DailyWorkReportState.getCurrentUser();
// 날짜 그룹 토글 (UI 함수)
window.toggleDateGroup = function(dateStr) {
const group = document.querySelector(`.date-group[data-date="${dateStr}"]`);
if (!group) return;
const isExpanded = group.classList.contains('expanded');
const content = group.querySelector('.date-group-content');
const icon = group.querySelector('.date-toggle-icon');
if (isExpanded) {
group.classList.remove('expanded');
group.classList.add('collapsed');
if (content) content.style.display = 'none';
if (icon) icon.textContent = '▶';
} else {
group.classList.remove('collapsed');
group.classList.add('expanded');
if (content) content.style.display = 'block';
if (icon) icon.textContent = '▼';
}
};
// DOMContentLoaded 이벤트에서 초기화
document.addEventListener('DOMContentLoaded', () => {
// 약간의 지연 후 초기화 (다른 스크립트 로드 대기)
setTimeout(() => {
window.DailyWorkReportController.init();
}, 100);
});
console.log('[Module] daily-work-report/index.js 로드 완료');

View File

@@ -0,0 +1,51 @@
// /js/work-report-api.js
import { apiGet, apiPost } from './api-helper.js';
/**
* 작업 보고서 작성을 위해 필요한 초기 데이터(작업자, 프로젝트, 태스크)를 가져옵니다.
* Promise.all을 사용하여 병렬로 API를 호출합니다.
* @returns {Promise<{workers: Array, projects: Array, tasks: Array}>}
*/
export async function getInitialData() {
try {
const [allWorkers, projects, tasks] = await Promise.all([
apiGet('/workers'),
apiGet('/projects'),
apiGet('/tasks')
]);
// 활성화된 작업자만 필터링
const workers = allWorkers.filter(worker => {
return worker.status === 'active' || worker.is_active === 1 || worker.is_active === true;
});
// 데이터 형식 검증
if (!Array.isArray(workers) || !Array.isArray(projects) || !Array.isArray(tasks)) {
throw new Error('서버에서 받은 데이터 형식이 올바르지 않습니다.');
}
// 작업자 목록은 ID 기준으로 정렬
workers.sort((a, b) => a.user_id - b.user_id);
return { workers, projects, tasks };
} catch (error) {
console.error('초기 데이터 로딩 중 오류 발생:', error);
// 에러를 다시 던져서 호출한 쪽에서 처리할 수 있도록 함
throw error;
}
}
/**
* 작성된 작업 보고서 데이터를 서버에 전송합니다.
* @param {Array<object>} reportData - 전송할 작업 보고서 데이터 배열
* @returns {Promise<object>} - 서버의 응답 결과
*/
export async function createWorkReport(reportData) {
try {
const result = await apiPost('/workreports', reportData);
return result;
} catch (error) {
console.error('작업 보고서 생성 요청 실패:', error);
throw error;
}
}

View File

@@ -0,0 +1,79 @@
// /js/work-report-create.js
import { renderCalendar } from './calendar.js';
import { getInitialData, createWorkReport } from './work-report-api.js';
import { initializeReportTable, getReportData } from './work-report-ui.js';
// 전역 상태 변수
let selectedDate = '';
/**
* 날짜가 선택되었을 때 실행되는 콜백 함수.
* 초기 데이터를 로드하고 테이블을 렌더링합니다.
* @param {string} date - 선택된 날짜 (YYYY-MM-DD 형식)
*/
async function onDateSelect(date) {
selectedDate = date;
const tableBody = document.getElementById('reportBody');
tableBody.innerHTML = '<tr><td colspan="8" class="text-center">데이터를 불러오는 중...</td></tr>';
try {
const initialData = await getInitialData();
initializeReportTable(initialData);
} catch (error) {
alert('데이터를 불러오는 데 실패했습니다: ' + error.message);
tableBody.innerHTML = '<tr><td colspan="8" class="text-center error">오류 발생! 데이터를 불러올 수 없습니다.</td></tr>';
}
}
/**
* '전체 등록' 버튼 클릭 시 실행되는 이벤트 핸들러.
* 폼 데이터를 서버에 전송합니다.
*/
async function handleSubmit() {
if (!selectedDate) {
alert('먼저 달력에서 날짜를 선택해주세요.');
return;
}
const reportData = getReportData();
if (!reportData) {
// getReportData 내부에서 이미 alert으로 사용자에게 알림
return;
}
// 각 항목에 선택된 날짜 추가
const payload = reportData.map(item => ({ ...item, date: selectedDate }));
const submitBtn = document.getElementById('submitBtn');
submitBtn.disabled = true;
submitBtn.textContent = '등록 중...';
try {
const result = await createWorkReport(payload);
if (result.success) {
alert('✅ 작업 보고서가 성공적으로 등록되었습니다!');
// 성공 후 폼을 다시 로드하거나, 다른 페이지로 이동 등의 로직 추가 가능
onDateSelect(selectedDate); // 현재 날짜의 폼을 다시 로드
} else {
throw new Error(result.error || '알 수 없는 오류로 등록에 실패했습니다.');
}
} catch (error) {
alert('❌ 등록 실패: ' + error.message);
} finally {
submitBtn.disabled = false;
submitBtn.textContent = '전체 등록';
}
}
/**
* 페이지 초기화 함수
*/
function initializePage() {
renderCalendar('calendar', onDateSelect);
const submitBtn = document.getElementById('submitBtn');
submitBtn.addEventListener('click', handleSubmit);
}
// DOM이 로드되면 페이지 초기화를 시작합니다.
document.addEventListener('DOMContentLoaded', initializePage);

View File

@@ -0,0 +1,141 @@
// /js/work-report-ui.js
const DEFAULT_PROJECT_ID = '13'; // 나중에는 API나 설정에서 받아오는 것이 좋음
const DEFAULT_TASK_ID = '15';
/**
* 주어진 데이터를 바탕으로 <select> 요소의 <option>들을 생성합니다.
* @param {Array<object>} items - 옵션으로 만들 데이터 배열
* @param {string} valueField - <option>의 value 속성에 사용할 필드 이름
* @param {string} textField - <option>의 텍스트에 사용할 필드 이름
* @returns {string} - 생성된 HTML 옵션 문자열
*/
function createOptions(items, valueField, textField) {
return items.map(item => `<option value="${item[valueField]}">${textField(item)}</option>`).join('');
}
/**
* 테이블의 모든 행 번호를 다시 매깁니다.
* @param {HTMLTableSectionElement} tableBody - tbody 요소
*/
function updateRowNumbers(tableBody) {
tableBody.querySelectorAll('tr').forEach((tr, index) => {
tr.cells[0].textContent = index + 1;
});
}
/**
* 하나의 작업 보고서 행(tr)을 생성합니다.
* @param {object} worker - 작업자 정보
* @param {Array} projects - 전체 프로젝트 목록
* @param {Array} tasks - 전체 태스크 목록
* @param {number} index - 행 번호
* @returns {HTMLTableRowElement} - 생성된 tr 요소
*/
function createReportRow(worker, projects, tasks, index) {
const tr = document.createElement('tr');
tr.innerHTML = `
<td>${index + 1}</td>
<td>
<input type="hidden" name="user_id" value="${worker.user_id}">
${worker.worker_name}
</td>
<td><select name="project_id">${createOptions(projects, 'project_id', p => p.project_name)}</select></td>
<td><select name="task_id">${createOptions(tasks, 'task_id', t => `${t.category}:${t.subcategory}`)}</select></td>
<td>
<select name="overtime">
<option value="">없음</option>
${[1, 2, 3, 4].map(n => `<option>${n}</option>`).join('')}
</select>
</td>
<td>
<select name="work_type">
${['근무', '연차', '유급', '반차', '반반차', '조퇴', '휴무'].map(t => `<option>${t}</option>`).join('')}
</select>
</td>
<td><input type="text" name="memo" placeholder="메모"></td>
<td><button type="button" class="remove-btn">x</button></td>
`;
// 이벤트 리스너 설정
const workTypeSelect = tr.querySelector('[name="work_type"]');
const projectSelect = tr.querySelector('[name="project_id"]');
const taskSelect = tr.querySelector('[name="task_id"]');
workTypeSelect.addEventListener('change', () => {
const isDisabled = ['연차', '휴무', '유급'].includes(workTypeSelect.value);
projectSelect.disabled = isDisabled;
taskSelect.disabled = isDisabled;
if (isDisabled) {
projectSelect.value = DEFAULT_PROJECT_ID;
taskSelect.value = DEFAULT_TASK_ID;
}
});
tr.querySelector('.remove-btn').addEventListener('click', () => {
tr.remove();
updateRowNumbers(tr.parentElement);
});
return tr;
}
/**
* 작업 보고서 테이블을 초기화하고 데이터를 채웁니다.
* @param {{workers: Array, projects: Array, tasks: Array}} initialData - 초기 데이터
*/
export function initializeReportTable(initialData) {
const tableBody = document.getElementById('reportBody');
if (!tableBody) return;
tableBody.innerHTML = ''; // 기존 내용 초기화
const { workers, projects, tasks } = initialData;
if (!workers || workers.length === 0) {
tableBody.innerHTML = '<tr><td colspan="8" class="text-center">등록할 작업자 정보가 없습니다.</td></tr>';
return;
}
workers.forEach((worker, index) => {
const row = createReportRow(worker, projects, tasks, index);
tableBody.appendChild(row);
});
}
/**
* 테이블에서 폼 데이터를 추출하여 배열로 반환합니다.
* @returns {Array<object>|null} - 추출된 데이터 배열 또는 유효성 검사 실패 시 null
*/
export function getReportData() {
const tableBody = document.getElementById('reportBody');
const rows = tableBody.querySelectorAll('tr');
if (rows.length === 0 || (rows.length === 1 && rows[0].cells.length < 2)) {
alert('등록할 내용이 없습니다.');
return null;
}
const reportData = [];
const workerIds = new Set();
for (const tr of rows) {
const workerId = tr.querySelector('[name="user_id"]').value;
if (workerIds.has(workerId)) {
alert(`오류: 작업자 '${tr.cells[1].textContent.trim()}'가 중복 등록되었습니다.`);
return null;
}
workerIds.add(workerId);
reportData.push({
user_id: workerId,
project_id: tr.querySelector('[name="project_id"]').value,
task_id: tr.querySelector('[name="task_id"]').value,
overtime_hours: tr.querySelector('[name="overtime"]').value || 0,
work_details: tr.querySelector('[name="work_type"]').value,
memo: tr.querySelector('[name="memo"]').value
});
}
return reportData;
}

View File

@@ -794,14 +794,14 @@ document.addEventListener('DOMContentLoaded', () => {
// ========== 작업자 연결 기능 ========== // // ========== 작업자 연결 기능 ========== //
let departments = []; let departments = [];
let selectedWorkerId = null; let selectedUserId = null;
// 연결된 작업자 정보 표시 업데이트 // 연결된 작업자 정보 표시 업데이트
function updateLinkedWorkerDisplay(user) { function updateLinkedWorkerDisplay(user) {
const linkedWorkerInfo = document.getElementById('linkedWorkerInfo'); const linkedWorkerInfo = document.getElementById('linkedWorkerInfo');
if (!linkedWorkerInfo) return; if (!linkedWorkerInfo) return;
if (user.worker_id && user.worker_name) { if (user.user_id && user.worker_name) {
linkedWorkerInfo.innerHTML = ` linkedWorkerInfo.innerHTML = `
<span class="worker-badge"> <span class="worker-badge">
<span class="worker-name">👤 ${user.worker_name}</span> <span class="worker-name">👤 ${user.worker_name}</span>
@@ -820,7 +820,7 @@ async function openWorkerSelectModal() {
return; return;
} }
selectedWorkerId = currentEditingUser.worker_id || null; selectedUserId = currentEditingUser.user_id || null;
// 부서 목록 로드 // 부서 목록 로드
await loadDepartmentsForSelect(); await loadDepartmentsForSelect();
@@ -833,7 +833,7 @@ window.openWorkerSelectModal = openWorkerSelectModal;
// 작업자 선택 모달 닫기 // 작업자 선택 모달 닫기
function closeWorkerSelectModal() { function closeWorkerSelectModal() {
document.getElementById('workerSelectModal').style.display = 'none'; document.getElementById('workerSelectModal').style.display = 'none';
selectedWorkerId = null; selectedUserId = null;
} }
window.closeWorkerSelectModal = closeWorkerSelectModal; window.closeWorkerSelectModal = closeWorkerSelectModal;
@@ -906,18 +906,18 @@ function renderWorkerListForSelect(workers) {
} }
// 이미 다른 계정에 연결된 작업자 확인을 위해 users 배열 사용 // 이미 다른 계정에 연결된 작업자 확인을 위해 users 배열 사용
const linkedWorkerIds = users const linkedUserIds = users
.filter(u => u.worker_id && u.user_id !== currentEditingUser?.user_id) .filter(u => u.user_id && u.user_id !== currentEditingUser?.user_id)
.map(u => u.worker_id); .map(u => u.user_id);
container.innerHTML = workers.map(worker => { container.innerHTML = workers.map(worker => {
const isSelected = selectedWorkerId === worker.worker_id; const isSelected = selectedUserId === worker.user_id;
const isLinkedToOther = linkedWorkerIds.includes(worker.worker_id); const isLinkedToOther = linkedUserIds.includes(worker.user_id);
const linkedUser = isLinkedToOther ? users.find(u => u.worker_id === worker.worker_id) : null; const linkedUser = isLinkedToOther ? users.find(u => u.user_id === worker.user_id) : null;
return ` return `
<div class="worker-select-item ${isSelected ? 'selected' : ''} ${isLinkedToOther ? 'disabled' : ''}" <div class="worker-select-item ${isSelected ? 'selected' : ''} ${isLinkedToOther ? 'disabled' : ''}"
onclick="${isLinkedToOther ? '' : `selectWorker(${worker.worker_id}, '${worker.worker_name}')`}"> onclick="${isLinkedToOther ? '' : `selectWorker(${worker.user_id}, '${worker.worker_name}')`}">
<div class="worker-avatar">${worker.worker_name.charAt(0)}</div> <div class="worker-avatar">${worker.worker_name.charAt(0)}</div>
<div class="worker-info"> <div class="worker-info">
<div class="worker-name">${worker.worker_name}</div> <div class="worker-name">${worker.worker_name}</div>
@@ -941,8 +941,8 @@ function getJobTypeName(jobType) {
} }
// 작업자 선택 // 작업자 선택
async function selectWorker(workerId, workerName) { async function selectWorker(userId, workerName) {
selectedWorkerId = workerId; selectedUserId = userId;
// UI 업데이트 // UI 업데이트
document.querySelectorAll('.worker-select-item').forEach(item => { document.querySelectorAll('.worker-select-item').forEach(item => {
@@ -950,7 +950,7 @@ async function selectWorker(workerId, workerName) {
item.querySelector('.select-indicator').textContent = ''; item.querySelector('.select-indicator').textContent = '';
}); });
const selectedItem = document.querySelector(`.worker-select-item[onclick*="${workerId}"]`); const selectedItem = document.querySelector(`.worker-select-item[onclick*="${userId}"]`);
if (selectedItem) { if (selectedItem) {
selectedItem.classList.add('selected'); selectedItem.classList.add('selected');
selectedItem.querySelector('.select-indicator').textContent = '✓'; selectedItem.querySelector('.select-indicator').textContent = '✓';
@@ -959,12 +959,12 @@ async function selectWorker(workerId, workerName) {
// 서버에 저장 // 서버에 저장
try { try {
const response = await window.apiCall(`/users/${currentEditingUser.user_id}`, 'PUT', { const response = await window.apiCall(`/users/${currentEditingUser.user_id}`, 'PUT', {
worker_id: workerId user_id: userId
}); });
if (response.success) { if (response.success) {
// currentEditingUser 업데이트 // currentEditingUser 업데이트
currentEditingUser.worker_id = workerId; currentEditingUser.user_id = userId;
currentEditingUser.worker_name = workerName; currentEditingUser.worker_name = workerName;
// 부서 정보도 업데이트 // 부서 정보도 업데이트
@@ -1003,7 +1003,7 @@ async function unlinkWorker() {
return; return;
} }
if (!currentEditingUser.worker_id) { if (!currentEditingUser.user_id) {
showToast('연결된 작업자가 없습니다.', 'warning'); showToast('연결된 작업자가 없습니다.', 'warning');
closeWorkerSelectModal(); closeWorkerSelectModal();
return; return;
@@ -1015,19 +1015,19 @@ async function unlinkWorker() {
try { try {
const response = await window.apiCall(`/users/${currentEditingUser.user_id}`, 'PUT', { const response = await window.apiCall(`/users/${currentEditingUser.user_id}`, 'PUT', {
worker_id: null user_id: null
}); });
if (response.success) { if (response.success) {
// currentEditingUser 업데이트 // currentEditingUser 업데이트
currentEditingUser.worker_id = null; currentEditingUser.user_id = null;
currentEditingUser.worker_name = null; currentEditingUser.worker_name = null;
currentEditingUser.department_name = null; currentEditingUser.department_name = null;
// users 배열 업데이트 // users 배열 업데이트
const userIndex = users.findIndex(u => u.user_id === currentEditingUser.user_id); const userIndex = users.findIndex(u => u.user_id === currentEditingUser.user_id);
if (userIndex !== -1) { if (userIndex !== -1) {
users[userIndex] = { ...users[userIndex], worker_id: null, worker_name: null, department_name: null }; users[userIndex] = { ...users[userIndex], user_id: null, worker_name: null, department_name: null };
} }
// 표시 업데이트 // 표시 업데이트

View File

@@ -322,7 +322,7 @@ async function fetchDailyWorkReports(date) {
async function updateWorkerHours(workerId, date, newHours, reason = '') { async function updateWorkerHours(workerId, date, newHours, reason = '') {
try { try {
const data = { const data = {
worker_id: workerId, user_id: workerId,
report_date: date, report_date: date,
work_hours: parseFloat(newHours), work_hours: parseFloat(newHours),
modification_reason: reason, modification_reason: reason,
@@ -412,7 +412,7 @@ async function calculateDateStatus(dateStr) {
status = 'missing'; status = 'missing';
} else { } else {
const hasDiscrepancy = workReports.some(wr => { const hasDiscrepancy = workReports.some(wr => {
const dr = dailyReports.find(d => d.worker_id === wr.worker_id); const dr = dailyReports.find(d => d.user_id === wr.user_id);
if (!dr) return true; if (!dr) return true;
const expected = calculateExpectedHours(wr.status, wr.overtime_hours); const expected = calculateExpectedHours(wr.status, wr.overtime_hours);
@@ -607,8 +607,8 @@ async function getWorkersForDate(dateStr) {
// WorkReports 데이터 추가 // WorkReports 데이터 추가
workReports.forEach(wr => { workReports.forEach(wr => {
workerMap.set(wr.worker_id, { workerMap.set(wr.user_id, {
worker_id: wr.worker_id, user_id: wr.user_id,
worker_name: wr.worker_name, worker_name: wr.worker_name,
overtime_hours: wr.overtime_hours || 0, overtime_hours: wr.overtime_hours || 0,
status: wr.status || 'normal', status: wr.status || 'normal',
@@ -621,13 +621,13 @@ async function getWorkersForDate(dateStr) {
// DailyReports 데이터 추가 // DailyReports 데이터 추가
dailyReports.forEach(dr => { dailyReports.forEach(dr => {
if (workerMap.has(dr.worker_id)) { if (workerMap.has(dr.user_id)) {
const worker = workerMap.get(dr.worker_id); const worker = workerMap.get(dr.user_id);
worker.reported_hours = dr.work_hours; worker.reported_hours = dr.work_hours;
worker.hasDailyReport = true; worker.hasDailyReport = true;
} else { } else {
workerMap.set(dr.worker_id, { workerMap.set(dr.user_id, {
worker_id: dr.worker_id, user_id: dr.user_id,
worker_name: dr.worker_name, worker_name: dr.worker_name,
overtime_hours: 0, overtime_hours: 0,
status: 'normal', status: 'normal',
@@ -739,7 +739,7 @@ async function saveEditedWork() {
showMessage('수정 중...', 'loading'); showMessage('수정 중...', 'loading');
await updateWorkerHours(editingWorker.worker_id, selectedDate, newHours, reason); await updateWorkerHours(editingWorker.user_id, selectedDate, newHours, reason);
showMessage('✅ 근무시간이 성공적으로 수정되었습니다!', 'success'); showMessage('✅ 근무시간이 성공적으로 수정되었습니다!', 'success');
closeEditModal(); closeEditModal();
@@ -767,7 +767,7 @@ async function deleteWorker(worker) {
try { try {
showMessage('삭제 중...', 'loading'); showMessage('삭제 중...', 'loading');
await deleteWorkerReport(worker.worker_id, selectedDate); await deleteWorkerReport(worker.user_id, selectedDate);
showMessage('✅ 작업 데이터가 성공적으로 삭제되었습니다!', 'success'); showMessage('✅ 작업 데이터가 성공적으로 삭제되었습니다!', 'success');
@@ -884,7 +884,7 @@ function renderWorkersList(workers) {
</div> </div>
<div> <div>
<div class="worker-name">${worker.worker_name}</div> <div class="worker-name">${worker.worker_name}</div>
<div class="worker-id">작업자 ID: ${worker.worker_id}</div> <div class="worker-id">작업자 ID: ${worker.user_id}</div>
</div> </div>
</div> </div>
<div class="status-badge">${getStatusIcon(worker.validationStatus)}</div> <div class="status-badge">${getStatusIcon(worker.validationStatus)}</div>

View File

@@ -45,7 +45,7 @@ async function fetchWorkers() {
return worker.status === 'active' || worker.is_active === 1 || worker.is_active === true; return worker.status === 'active' || worker.is_active === 1 || worker.is_active === true;
}); });
workers.sort((a, b) => a.worker_id - b.worker_id); workers.sort((a, b) => a.user_id - b.user_id);
} catch (err) { } catch (err) {
alert('작업자 불러오기 실패'); alert('작업자 불러오기 실패');
} }
@@ -93,14 +93,14 @@ function renderTable(data, year, month, lastDay) {
workers.forEach(w => { workers.forEach(w => {
// ✅ 월간 데이터 (표에 표시용) // ✅ 월간 데이터 (표에 표시용)
const recsThisMonth = data.filter(r => const recsThisMonth = data.filter(r =>
r.worker_id === w.worker_id && r.user_id === w.user_id &&
new Date(r.date).getFullYear() === +year && new Date(r.date).getFullYear() === +year &&
new Date(r.date).getMonth() + 1 === +month new Date(r.date).getMonth() + 1 === +month
); );
// ✅ 연간 데이터 (연차 계산용) // ✅ 연간 데이터 (연차 계산용)
const recsThisYear = data.filter(r => const recsThisYear = data.filter(r =>
r.worker_id === w.worker_id && r.user_id === w.user_id &&
new Date(r.date).getFullYear() === +year new Date(r.date).getFullYear() === +year
); );

View File

@@ -34,8 +34,8 @@ export async function getWorkersByDate(date) {
if (reports && reports.length > 0) { if (reports && reports.length > 0) {
const workerMap = new Map(); const workerMap = new Map();
reports.forEach(r => { reports.forEach(r => {
if (!workerMap.has(r.worker_id)) { if (!workerMap.has(r.user_id)) {
workerMap.set(r.worker_id, { worker_id: r.worker_id, worker_name: r.worker_name }); workerMap.set(r.user_id, { user_id: r.user_id, worker_name: r.worker_name });
} }
}); });
workers = Array.from(workerMap.values()); workers = Array.from(workerMap.values());

View File

@@ -54,7 +54,7 @@ export function renderWorkerList(workers) {
btn.type = 'button'; btn.type = 'button';
btn.className = 'btn'; btn.className = 'btn';
btn.textContent = worker.worker_name; btn.textContent = worker.worker_name;
btn.dataset.id = worker.worker_id; btn.dataset.id = worker.user_id;
btn.addEventListener('click', () => btn.classList.toggle('selected')); btn.addEventListener('click', () => btn.classList.toggle('selected'));
DOM.workerList.appendChild(btn); DOM.workerList.appendChild(btn);
}); });
@@ -79,7 +79,7 @@ export function getFormData() {
issue_type_id: DOM.issueTypeSelect.value, issue_type_id: DOM.issueTypeSelect.value,
start_time: DOM.timeStart.value, start_time: DOM.timeStart.value,
end_time: DOM.timeEnd.value, end_time: DOM.timeEnd.value,
worker_ids: selectedWorkers, // worker_id -> worker_ids 로 명확하게 변경 user_ids: selectedWorkers, // user_id -> user_ids 로 명확하게 변경
}; };
for (const key in data) { for (const key in data) {

View File

@@ -792,7 +792,7 @@ const MobileReport = (function() {
const reportData = { const reportData = {
tbm_assignment_id: tbm.assignment_id, tbm_assignment_id: tbm.assignment_id,
tbm_session_id: tbm.session_id, tbm_session_id: tbm.session_id,
worker_id: tbm.worker_id, user_id: tbm.user_id,
project_id: tbm.project_id, project_id: tbm.project_id,
work_type_id: tbm.task_id, work_type_id: tbm.task_id,
report_date: reportDate, report_date: reportDate,
@@ -895,7 +895,7 @@ const MobileReport = (function() {
data: { data: {
tbm_assignment_id: tbm.assignment_id, tbm_assignment_id: tbm.assignment_id,
tbm_session_id: tbm.session_id, tbm_session_id: tbm.session_id,
worker_id: tbm.worker_id, user_id: tbm.user_id,
project_id: tbm.project_id, project_id: tbm.project_id,
work_type_id: tbm.task_id, work_type_id: tbm.task_id,
report_date: reportDate, report_date: reportDate,
@@ -974,7 +974,7 @@ const MobileReport = (function() {
const state = window.DailyWorkReportState; const state = window.DailyWorkReportState;
manualCards[id] = { manualCards[id] = {
worker_id: null, user_id: null,
report_date: getKoreaToday(), report_date: getKoreaToday(),
project_id: null, project_id: null,
work_type_id: null, work_type_id: null,
@@ -1006,9 +1006,9 @@ const MobileReport = (function() {
<button class="m-manual-delete" onclick="MobileReport.removeManualCard('${id}')">&times;</button> <button class="m-manual-delete" onclick="MobileReport.removeManualCard('${id}')">&times;</button>
<div class="m-form-group"> <div class="m-form-group">
<label class="m-form-label">작업자</label> <label class="m-form-label">작업자</label>
<select class="m-form-select" id="m_manual_worker_${id}" onchange="MobileReport.updateManualField('${id}','worker_id',this.value)"> <select class="m-form-select" id="m_manual_worker_${id}" onchange="MobileReport.updateManualField('${id}','user_id',this.value)">
<option value="">작업자 선택</option> <option value="">작업자 선택</option>
${workers.map(w => `<option value="${w.worker_id}">${esc(w.worker_name)} (${esc(w.job_type || '-')})</option>`).join('')} ${workers.map(w => `<option value="${w.user_id}">${esc(w.worker_name)} (${esc(w.job_type || '-')})</option>`).join('')}
</select> </select>
</div> </div>
<div class="m-form-row"> <div class="m-form-row">
@@ -1127,7 +1127,7 @@ const MobileReport = (function() {
const mc = manualCards[id]; const mc = manualCards[id];
if (!mc) return; if (!mc) return;
const workerId = mc.worker_id || document.getElementById('m_manual_worker_' + id)?.value; const workerId = mc.user_id || document.getElementById('m_manual_worker_' + id)?.value;
const reportDate = mc.report_date || document.getElementById('m_manual_date_' + id)?.value; const reportDate = mc.report_date || document.getElementById('m_manual_date_' + id)?.value;
const projectId = mc.project_id || document.getElementById('m_manual_project_' + id)?.value; const projectId = mc.project_id || document.getElementById('m_manual_project_' + id)?.value;
const taskId = mc.task_id || document.getElementById('m_manual_task_' + id)?.value; const taskId = mc.task_id || document.getElementById('m_manual_task_' + id)?.value;
@@ -1148,7 +1148,7 @@ const MobileReport = (function() {
const reportData = { const reportData = {
report_date: reportDate, report_date: reportDate,
worker_id: parseInt(workerId), user_id: parseInt(workerId),
work_entries: [{ work_entries: [{
project_id: parseInt(projectId), project_id: parseInt(projectId),
task_id: parseInt(taskId), task_id: parseInt(taskId),

View File

@@ -581,7 +581,7 @@ window.submitTbmWorkReport = async function(index) {
const reportData = { const reportData = {
tbm_assignment_id: tbm.assignment_id, tbm_assignment_id: tbm.assignment_id,
tbm_session_id: tbm.session_id, tbm_session_id: tbm.session_id,
worker_id: tbm.worker_id, user_id: tbm.user_id,
project_id: tbm.project_id, project_id: tbm.project_id,
work_type_id: tbm.task_id, // task_id를 work_type_id 컬럼에 저장 (직접 작업보고서와 일관성 유지) work_type_id: tbm.task_id, // task_id를 work_type_id 컬럼에 저장 (직접 작업보고서와 일관성 유지)
report_date: reportDate, report_date: reportDate,
@@ -729,7 +729,7 @@ window.batchSubmitTbmSession = async function(sessionKey) {
data: { data: {
tbm_assignment_id: tbm.assignment_id, tbm_assignment_id: tbm.assignment_id,
tbm_session_id: tbm.session_id, tbm_session_id: tbm.session_id,
worker_id: tbm.worker_id, user_id: tbm.user_id,
project_id: tbm.project_id, project_id: tbm.project_id,
work_type_id: tbm.task_id, // task_id를 work_type_id 컬럼에 저장 (일관성 유지) work_type_id: tbm.task_id, // task_id를 work_type_id 컬럼에 저장 (일관성 유지)
report_date: reportDate, report_date: reportDate,
@@ -847,7 +847,7 @@ window.addManualWorkRow = function() {
<td> <td>
<select class="form-input-compact" id="worker_${manualIndex}" style="width: 120px;" required> <select class="form-input-compact" id="worker_${manualIndex}" style="width: 120px;" required>
<option value="">작업자 선택</option> <option value="">작업자 선택</option>
${workers.map(w => `<option value="${escapeHtml(String(w.worker_id))}">${escapeHtml(w.worker_name)} (${escapeHtml(w.job_type || '-')})</option>`).join('')} ${workers.map(w => `<option value="${escapeHtml(String(w.user_id))}">${escapeHtml(w.worker_name)} (${escapeHtml(w.job_type || '-')})</option>`).join('')}
</select> </select>
</td> </td>
<td> <td>
@@ -1418,7 +1418,7 @@ window.submitManualWorkReport = async function(manualIndex) {
// 주의: 서비스에서 task_id를 work_type_id 컬럼에 매핑함 // 주의: 서비스에서 task_id를 work_type_id 컬럼에 매핑함
const reportData = { const reportData = {
report_date: reportDate, report_date: reportDate,
worker_id: parseInt(workerId), user_id: parseInt(workerId),
work_entries: [{ work_entries: [{
project_id: parseInt(projectId), project_id: parseInt(projectId),
task_id: parseInt(taskId), // 서비스에서 work_type_id로 매핑됨 task_id: parseInt(taskId), // 서비스에서 work_type_id로 매핑됨
@@ -1577,7 +1577,7 @@ window.submitAllManualWorkReports = async function() {
// 서비스 레이어가 기대하는 형식으로 변환 // 서비스 레이어가 기대하는 형식으로 변환
const reportData = { const reportData = {
report_date: reportDate, report_date: reportDate,
worker_id: parseInt(workerId), user_id: parseInt(workerId),
work_entries: [{ work_entries: [{
project_id: parseInt(projectId), project_id: parseInt(projectId),
task_id: parseInt(taskId), task_id: parseInt(taskId),
@@ -2282,7 +2282,7 @@ async function loadTbmTeamForDate(date) {
// 팀 구성 조회 // 팀 구성 조회
const teamRes = await window.apiCall(`/tbm/sessions/${targetSession.session_id}/team`); const teamRes = await window.apiCall(`/tbm/sessions/${targetSession.session_id}/team`);
if (teamRes && teamRes.success && teamRes.data) { if (teamRes && teamRes.success && teamRes.data) {
const teamWorkerIds = teamRes.data.map(m => m.worker_id); const teamWorkerIds = teamRes.data.map(m => m.user_id);
console.log(`✅ TBM 팀 구성 로드 성공: ${teamWorkerIds.length}`); console.log(`✅ TBM 팀 구성 로드 성공: ${teamWorkerIds.length}`);
return teamWorkerIds; return teamWorkerIds;
} }
@@ -2334,16 +2334,16 @@ async function populateWorkerGrid() {
btn.type = 'button'; btn.type = 'button';
btn.className = 'worker-card'; btn.className = 'worker-card';
btn.textContent = worker.worker_name; btn.textContent = worker.worker_name;
btn.dataset.id = worker.worker_id; btn.dataset.id = worker.user_id;
// TBM 팀 구성에 포함된 작업자는 자동 선택 // TBM 팀 구성에 포함된 작업자는 자동 선택
if (tbmWorkerIds.includes(worker.worker_id)) { if (tbmWorkerIds.includes(worker.user_id)) {
btn.classList.add('selected'); btn.classList.add('selected');
selectedWorkers.add(worker.worker_id); selectedWorkers.add(worker.user_id);
} }
btn.addEventListener('click', () => { btn.addEventListener('click', () => {
toggleWorkerSelection(worker.worker_id, btn); toggleWorkerSelection(worker.user_id, btn);
}); });
grid.appendChild(btn); grid.appendChild(btn);
@@ -2673,12 +2673,12 @@ async function saveWorkReport() {
const failureDetails = []; const failureDetails = [];
for (const workerId of selectedWorkers) { for (const workerId of selectedWorkers) {
const workerName = workers.find(w => w.worker_id == workerId)?.worker_name || '알 수 없음'; const workerName = workers.find(w => w.user_id == workerId)?.worker_name || '알 수 없음';
// 서버가 기대하는 work_entries 배열 형태로 전송 // 서버가 기대하는 work_entries 배열 형태로 전송
const requestData = { const requestData = {
report_date: reportDate, report_date: reportDate,
worker_id: parseInt(workerId), user_id: parseInt(workerId),
work_entries: newWorkEntries.map(entry => ({ work_entries: newWorkEntries.map(entry => ({
project_id: entry.project_id, project_id: entry.project_id,
task_id: entry.work_type_id, // 서버에서 task_id로 기대 task_id: entry.work_type_id, // 서버에서 task_id로 기대

View File

@@ -96,7 +96,7 @@ class DailyWorkReportState extends BaseState {
*/ */
selectAllWorkers(select = true) { selectAllWorkers(select = true) {
if (select) { if (select) {
this.workers.forEach(w => this.selectedWorkers.add(w.worker_id)); this.workers.forEach(w => this.selectedWorkers.add(w.user_id));
} else { } else {
this.selectedWorkers.clear(); this.selectedWorkers.clear();
} }

View File

@@ -110,11 +110,11 @@ function renderWorkerList(workers) {
} }
container.innerHTML = workers.map(worker => ` container.innerHTML = workers.map(worker => `
<div class="worker-card ${selectedWorkers.has(worker.worker_id) ? 'selected' : ''}" <div class="worker-card ${selectedWorkers.has(worker.user_id) ? 'selected' : ''}"
onclick="toggleWorkerSelection(${worker.worker_id})"> onclick="toggleWorkerSelection(${worker.user_id})">
<div class="worker-info-row"> <div class="worker-info-row">
<input type="checkbox" ${selectedWorkers.has(worker.worker_id) ? 'checked' : ''} <input type="checkbox" ${selectedWorkers.has(worker.user_id) ? 'checked' : ''}
onclick="event.stopPropagation(); toggleWorkerSelection(${worker.worker_id})"> onclick="event.stopPropagation(); toggleWorkerSelection(${worker.user_id})">
<div class="worker-avatar">${worker.worker_name.charAt(0)}</div> <div class="worker-avatar">${worker.worker_name.charAt(0)}</div>
<div class="worker-details"> <div class="worker-details">
<span class="worker-name">${worker.worker_name}</span> <span class="worker-name">${worker.worker_name}</span>

View File

@@ -23,7 +23,7 @@ async function fetchDashboardStats() {
// 필요한 데이터 형태로 가공 (예시) // 필요한 데이터 형태로 가공 (예시)
return { return {
today_reports_count: stats.length, today_reports_count: stats.length,
today_workers_count: new Set(stats.map(d => d.worker_id)).size, today_workers_count: new Set(stats.map(d => d.user_id)).size,
}; };
} catch (error) { } catch (error) {
console.error('대시보드 통계 데이터 로드 실패:', error); console.error('대시보드 통계 데이터 로드 실패:', error);

View File

@@ -147,7 +147,7 @@ userForm?.addEventListener('submit', async e => {
password: document.getElementById('password').value.trim(), password: document.getElementById('password').value.trim(),
name: document.getElementById('name').value.trim(), name: document.getElementById('name').value.trim(),
access_level: document.getElementById('access_level').value, access_level: document.getElementById('access_level').value,
worker_id: document.getElementById('worker_id').value || null user_id: document.getElementById('user_id').value || null
}; };
try { try {
@@ -206,13 +206,13 @@ async function loadUsers() {
list.forEach(item => { list.forEach(item => {
item.access_level = accessLabels[item.access_level] || item.access_level; item.access_level = accessLabels[item.access_level] || item.access_level;
item.worker_id = item.worker_id || '-'; item.user_id = item.user_id || '-';
// 행 생성 // 행 생성
const tr = document.createElement('tr'); const tr = document.createElement('tr');
// 데이터 컬럼 // 데이터 컬럼
['user_id', 'username', 'name', 'access_level', 'worker_id'].forEach(key => { ['user_id', 'username', 'name', 'access_level', 'user_id'].forEach(key => {
const td = document.createElement('td'); const td = document.createElement('td');
td.textContent = item[key] || '-'; td.textContent = item[key] || '-';
tr.appendChild(td); tr.appendChild(td);
@@ -267,7 +267,7 @@ async function loadUsers() {
} }
async function loadWorkerOptions() { async function loadWorkerOptions() {
const select = document.getElementById('worker_id'); const select = document.getElementById('user_id');
if (!select) return; if (!select) return;
try { try {
@@ -289,8 +289,8 @@ async function loadWorkerOptions() {
if (Array.isArray(workers)) { if (Array.isArray(workers)) {
workers.forEach(w => { workers.forEach(w => {
const opt = document.createElement('option'); const opt = document.createElement('option');
opt.value = w.worker_id; opt.value = w.user_id;
opt.textContent = `${w.worker_name} (${w.worker_id})`; opt.textContent = `${w.worker_name} (${w.user_id})`;
select.appendChild(opt); select.appendChild(opt);
}); });
} }

View File

@@ -80,10 +80,10 @@ async function loadWorkers() {
if (Array.isArray(list)) { if (Array.isArray(list)) {
list.forEach(item => { list.forEach(item => {
const row = createRow(item, ['worker_id', 'worker_name', 'position'], async w => { const row = createRow(item, ['user_id', 'worker_name', 'position'], async w => {
if (!confirm('삭제하시겠습니까?')) return; if (!confirm('삭제하시겠습니까?')) return;
try { try {
const delRes = await fetch(`${API}/workers/${w.worker_id}`, { const delRes = await fetch(`${API}/workers/${w.user_id}`, {
method: 'DELETE', method: 'DELETE',
headers: getAuthHeaders() headers: getAuthHeaders()
}); });

View File

@@ -255,11 +255,11 @@ function analyzeDashboardData() {
// 작업자별 데이터 그룹화 // 작업자별 데이터 그룹화
const workerWorkData = {}; const workerWorkData = {};
workData.forEach(work => { workData.forEach(work => {
const workerId = work.worker_id; const userId = work.user_id;
if (!workerWorkData[workerId]) { if (!workerWorkData[userId]) {
workerWorkData[workerId] = []; workerWorkData[userId] = [];
} }
workerWorkData[workerId].push(work); workerWorkData[userId].push(work);
}); });
// 전체 통계 계산 // 전체 통계 계산
@@ -272,7 +272,7 @@ function analyzeDashboardData() {
// 작업자별 상세 분석 (개선된 버전) // 작업자별 상세 분석 (개선된 버전)
const workerAnalysis = workers.map(worker => { const workerAnalysis = workers.map(worker => {
const workerWorks = workerWorkData[worker.worker_id] || []; const workerWorks = workerWorkData[worker.user_id] || [];
const workerHours = workerWorks.reduce((sum, work) => sum + parseFloat(work.work_hours || 0), 0); const workerHours = workerWorks.reduce((sum, work) => sum + parseFloat(work.work_hours || 0), 0);
// 작업 유형 분석 (실제 이름으로) // 작업 유형 분석 (실제 이름으로)
@@ -455,7 +455,7 @@ function createWorkerRow(worker) {
<div class="update-time ${updateClass}">${updateTimeText}</div> <div class="update-time ${updateClass}">${updateTimeText}</div>
</td> </td>
<td> <td>
<button class="detail-btn" onclick="showWorkerDetailSafe('${worker.worker_id}')"> <button class="detail-btn" onclick="showWorkerDetailSafe('${worker.user_id}')">
📋 상세 📋 상세
</button> </button>
</td> </td>
@@ -479,7 +479,7 @@ function formatDateTime(date) {
// 작업자 상세 모달 표시 (안전한 버전) // 작업자 상세 모달 표시 (안전한 버전)
function showWorkerDetailSafe(workerId) { function showWorkerDetailSafe(workerId) {
// 현재 분석된 데이터에서 해당 작업자 찾기 // 현재 분석된 데이터에서 해당 작업자 찾기
const worker = filteredWorkData.find(w => w.worker_id == workerId); const worker = filteredWorkData.find(w => w.user_id == workerId);
if (!worker) { if (!worker) {
showMessage('작업자 정보를 찾을 수 없습니다.', 'error'); showMessage('작업자 정보를 찾을 수 없습니다.', 'error');
return; return;

View File

@@ -246,7 +246,7 @@ async function loadWorkData(date) {
// ========== 요약 카드 업데이트 ========== // // ========== 요약 카드 업데이트 ========== //
function updateSummaryCards() { function updateSummaryCards() {
// 오늘 작업자 수 // 오늘 작업자 수
const todayWorkersCount = new Set(workData.map(w => w.worker_id)).size; const todayWorkersCount = new Set(workData.map(w => w.user_id)).size;
updateSummaryCard(elements.todayWorkers, todayWorkersCount, '명'); updateSummaryCard(elements.todayWorkers, todayWorkersCount, '명');
// 총 작업 시간 // 총 작업 시간
@@ -326,7 +326,7 @@ function displayWorkStatus() {
// 작업자별 상황 분석 // 작업자별 상황 분석
const workerStatusList = allWorkers.map(worker => { const workerStatusList = allWorkers.map(worker => {
const todayWork = workData.filter(w => w.worker_id === worker.worker_id); const todayWork = workData.filter(w => w.user_id === worker.user_id);
const totalHours = todayWork.reduce((sum, w) => sum + parseFloat(w.work_hours || 0), 0); const totalHours = todayWork.reduce((sum, w) => sum + parseFloat(w.work_hours || 0), 0);
// 휴가/연차 제외한 실제 작업시간 계산 // 휴가/연차 제외한 실제 작업시간 계산
@@ -453,7 +453,7 @@ function displayWorkStatus() {
else if (worker.status === 'partial') iconKey = 'partial'; else if (worker.status === 'partial') iconKey = 'partial';
return ` return `
<tr data-worker-id="${worker.worker_id}"> <tr data-user-id="${worker.user_id}">
<td data-label="작업자" class="worker-info"> <td data-label="작업자" class="worker-info">
<div class="worker-avatar"> <div class="worker-avatar">
<span>${worker.worker_name.charAt(0)}</span> <span>${worker.worker_name.charAt(0)}</span>
@@ -492,7 +492,7 @@ function displayWorkStatus() {
<td data-label="액션" class="worker-actions"> <td data-label="액션" class="worker-actions">
<div class="action-buttons"> <div class="action-buttons">
<button class="action-btn btn-edit" onclick="openWorkerModal(${worker.worker_id}, '${worker.worker_name}')" title="작업입력"> <button class="action-btn btn-edit" onclick="openWorkerModal(${worker.user_id}, '${worker.worker_name}')" title="작업입력">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path> <path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path>
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path> <path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path>
@@ -500,7 +500,7 @@ function displayWorkStatus() {
<span class="action-text">작업입력</span> <span class="action-text">작업입력</span>
</button> </button>
${worker.vacationType ? ` ${worker.vacationType ? `
<button class="action-btn btn-vacation" onclick="handleVacation(${worker.worker_id}, '${worker.vacationType}')" title="${worker.vacationType === 'full' ? '연차처리' : worker.vacationType === 'half' ? '반차처리' : '반반차처리'}"> <button class="action-btn btn-vacation" onclick="handleVacation(${worker.user_id}, '${worker.vacationType}')" title="${worker.vacationType === 'full' ? '연차처리' : worker.vacationType === 'half' ? '반차처리' : '반반차처리'}">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect> <rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect>
<line x1="16" y1="2" x2="16" y2="6"></line> <line x1="16" y1="2" x2="16" y2="6"></line>
@@ -511,7 +511,7 @@ function displayWorkStatus() {
</button> </button>
` : ''} ` : ''}
${worker.status === 'overtime-warning' ? ` ${worker.status === 'overtime-warning' ? `
<button class="action-btn btn-confirm" onclick="confirmOvertime(${worker.worker_id})" title="정상확인"> <button class="action-btn btn-confirm" onclick="confirmOvertime(${worker.user_id})" title="정상확인">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="20 6 9 17 4 12"></polyline> <polyline points="20 6 9 17 4 12"></polyline>
</svg> </svg>
@@ -563,7 +563,7 @@ function displayWorkersAsCards(workers) {
elements.workersContainer.innerHTML = ` elements.workersContainer.innerHTML = `
<div class="workers-grid grid grid-cols-4"> <div class="workers-grid grid grid-cols-4">
${workers.map(worker => { ${workers.map(worker => {
const todayWork = workData.filter(w => w.worker_id === worker.worker_id); const todayWork = workData.filter(w => w.user_id === worker.user_id);
const totalHours = todayWork.reduce((sum, w) => sum + parseFloat(w.work_hours || 0), 0); const totalHours = todayWork.reduce((sum, w) => sum + parseFloat(w.work_hours || 0), 0);
// 휴가/연차 제외한 실제 작업시간 계산 // 휴가/연차 제외한 실제 작업시간 계산
@@ -631,7 +631,7 @@ function displayWorkersAsList(workers) {
</thead> </thead>
<tbody> <tbody>
${workers.map(worker => { ${workers.map(worker => {
const todayWork = workData.filter(w => w.worker_id === worker.worker_id); const todayWork = workData.filter(w => w.user_id === worker.user_id);
const totalHours = todayWork.reduce((sum, w) => sum + parseFloat(w.work_hours || 0), 0); const totalHours = todayWork.reduce((sum, w) => sum + parseFloat(w.work_hours || 0), 0);
const hasError = todayWork.some(w => w.work_status_id === 2); const hasError = todayWork.some(w => w.work_status_id === 2);
@@ -852,7 +852,7 @@ async function processVacation(workerId, vacationType, hours) {
// 휴가용 작업 보고서 생성 (특별한 작업 유형으로) // 휴가용 작업 보고서 생성 (특별한 작업 유형으로)
const vacationReport = { const vacationReport = {
report_date: selectedDate, report_date: selectedDate,
worker_id: workerId, user_id: workerId,
project_id: 1, // 기본 프로젝트 (휴가용) project_id: 1, // 기본 프로젝트 (휴가용)
work_type_id: 999, // 휴가 전용 작업 유형 (DB에 추가 필요) work_type_id: 999, // 휴가 전용 작업 유형 (DB에 추가 필요)
work_status_id: 1, // 정상 상태 work_status_id: 1, // 정상 상태
@@ -887,7 +887,7 @@ async function processOvertimeConfirmation(workerId) {
// 새로운 근태 관리 API 사용 // 새로운 근태 관리 API 사용
const overtimeData = { const overtimeData = {
worker_id: workerId, user_id: workerId,
date: selectedDate date: selectedDate
}; };
@@ -1104,7 +1104,7 @@ async function loadModalData() {
async function loadModalExistingWork() { async function loadModalExistingWork() {
try { try {
const response = await window.apiCall(`/daily-work-reports?date=${currentModalWorker.date}&worker_id=${currentModalWorker.id}`); const response = await window.apiCall(`/daily-work-reports?date=${currentModalWorker.date}&user_id=${currentModalWorker.id}`);
modalExistingWork = Array.isArray(response) ? response : (response.data || []); modalExistingWork = Array.isArray(response) ? response : (response.data || []);
} catch (error) { } catch (error) {
console.error('기존 작업 로드 오류:', error); console.error('기존 작업 로드 오류:', error);
@@ -1258,7 +1258,7 @@ async function saveModalNewWork() {
const workData = { const workData = {
report_date: currentModalWorker.date, report_date: currentModalWorker.date,
worker_id: currentModalWorker.id, user_id: currentModalWorker.id,
work_entries: [{ work_entries: [{
project_id: parseInt(projectId), project_id: parseInt(projectId),
task_id: parseInt(workTypeId), // work_type_id를 task_id로 매핑 task_id: parseInt(workTypeId), // work_type_id를 task_id로 매핑
@@ -1332,7 +1332,7 @@ async function handleModalVacation(vacationType) {
try { try {
// 새로운 근태 관리 API 사용 // 새로운 근태 관리 API 사용
const vacationData = { const vacationData = {
worker_id: currentModalWorker.id, user_id: currentModalWorker.id,
date: currentModalWorker.date, date: currentModalWorker.date,
vacation_type: vacation.code vacation_type: vacation.code
}; };

View File

@@ -71,7 +71,7 @@ function updateProfileUI(user) {
document.getElementById('username').textContent = user.username || '-'; document.getElementById('username').textContent = user.username || '-';
document.getElementById('fullName').textContent = user.name || '-'; document.getElementById('fullName').textContent = user.name || '-';
document.getElementById('accessLevel').textContent = accessLevelMap[user.access_level] || user.access_level || '-'; document.getElementById('accessLevel').textContent = accessLevelMap[user.access_level] || user.access_level || '-';
document.getElementById('workerId').textContent = user.worker_id || '연결되지 않음'; document.getElementById('workerId').textContent = user.user_id || '연결되지 않음';
// 날짜 포맷팅 // 날짜 포맷팅
if (user.created_at) { if (user.created_at) {

View File

@@ -80,7 +80,7 @@ export function updateFilterOptions(masterData) {
return html; return html;
}; };
DOM.projectFilter.innerHTML = createOptions(masterData.projects, 'project_id', 'project_name'); DOM.projectFilter.innerHTML = createOptions(masterData.projects, 'project_id', 'project_name');
DOM.workerFilter.innerHTML = createOptions(masterData.workers, 'worker_id', 'worker_name'); DOM.workerFilter.innerHTML = createOptions(masterData.workers, 'user_id', 'worker_name');
DOM.taskFilter.innerHTML = createOptions(masterData.tasks, 'task_id', 'category'); DOM.taskFilter.innerHTML = createOptions(masterData.tasks, 'task_id', 'category');
} }

View File

@@ -678,7 +678,7 @@ function showUserEditForm(user) {
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="edit-worker-id">작업자 ID</label> <label for="edit-worker-id">작업자 ID</label>
<input type="number" id="edit-worker-id" value="${user.worker_id || ''}"> <input type="number" id="edit-worker-id" value="${user.user_id || ''}">
</div> </div>
<div class="form-actions"> <div class="form-actions">
<button type="submit" class="btn btn-primary"> <button type="submit" class="btn btn-primary">
@@ -711,7 +711,7 @@ async function updateUser(userId) {
role: document.getElementById('edit-role').value, role: document.getElementById('edit-role').value,
access_level: document.getElementById('edit-role').value, access_level: document.getElementById('edit-role').value,
is_active: parseInt(document.getElementById('edit-is-active').value), is_active: parseInt(document.getElementById('edit-is-active').value),
worker_id: document.getElementById('edit-worker-id').value || null user_id: document.getElementById('edit-worker-id').value || null
}; };
const response = await apiRequest(`/api/system/users/${userId}`, 'PUT', formData); const response = await apiRequest(`/api/system/users/${userId}`, 'PUT', formData);
@@ -840,7 +840,7 @@ async function createUser() {
email: document.getElementById('create-email').value || null, email: document.getElementById('create-email').value || null,
role: document.getElementById('create-role').value, role: document.getElementById('create-role').value,
access_level: document.getElementById('create-role').value, access_level: document.getElementById('create-role').value,
worker_id: document.getElementById('create-worker-id').value || null user_id: document.getElementById('create-worker-id').value || null
}; };
const response = await apiRequest('/api/system/users', 'POST', formData); const response = await apiRequest('/api/system/users', 'POST', formData);

View File

@@ -15,8 +15,8 @@
sessionDate: null, sessionDate: null,
leaderId: null, leaderId: null,
leaderName: '', leaderName: '',
workers: new Set(), // worker_id Set workers: new Set(), // user_id Set
workerNames: {}, // { worker_id: worker_name } workerNames: {}, // { user_id: worker_name }
projectId: null, projectId: null,
projectName: '', projectName: '',
workTypeId: null, workTypeId: null,
@@ -41,10 +41,10 @@
W.sessionDate = window.TbmUtils.getTodayKST(); W.sessionDate = window.TbmUtils.getTodayKST();
var user = window.TbmState.getUser(); var user = window.TbmState.getUser();
if (user) { if (user) {
if (user.worker_id) { if (user.user_id) {
var worker = window.TbmState.allWorkers.find(function(w) { return w.worker_id === user.worker_id; }); var worker = window.TbmState.allWorkers.find(function(w) { return w.user_id === user.user_id; });
if (worker) { if (worker) {
W.leaderId = worker.worker_id; W.leaderId = worker.user_id;
W.leaderName = worker.worker_name; W.leaderName = worker.worker_name;
} else { } else {
W.leaderName = user.name || ''; W.leaderName = user.name || '';
@@ -203,7 +203,7 @@
W.todayAssignments = {}; W.todayAssignments = {};
res.data.forEach(function(a) { res.data.forEach(function(a) {
if (a.sessions && a.sessions.length > 0) { if (a.sessions && a.sessions.length > 0) {
W.todayAssignments[a.worker_id] = a; W.todayAssignments[a.user_id] = a;
} }
}); });
} else { } else {
@@ -216,14 +216,14 @@
} }
var workerCards = workers.map(function(w) { var workerCards = workers.map(function(w) {
var selected = W.workers.has(w.worker_id) ? ' selected' : ''; var selected = W.workers.has(w.user_id) ? ' selected' : '';
var assignment = W.todayAssignments[w.worker_id]; var assignment = W.todayAssignments[w.user_id];
var assigned = assignment && assignment.total_hours >= 8; var assigned = assignment && assignment.total_hours >= 8;
var partiallyAssigned = assignment && assignment.total_hours > 0 && assignment.total_hours < 8; var partiallyAssigned = assignment && assignment.total_hours > 0 && assignment.total_hours < 8;
var badgeHtml = ''; var badgeHtml = '';
var disabledClass = ''; var disabledClass = '';
var onclick = 'toggleWorker(' + w.worker_id + ')'; var onclick = 'toggleWorker(' + w.user_id + ')';
if (assigned) { if (assigned) {
// 종일 배정됨 - 선택 불가 // 종일 배정됨 - 선택 불가
@@ -238,7 +238,7 @@
return '<div class="worker-card' + selected + disabledClass + '"' + return '<div class="worker-card' + selected + disabledClass + '"' +
(onclick ? ' onclick="' + onclick + '"' : '') + (onclick ? ' onclick="' + onclick + '"' : '') +
' data-wid="' + w.worker_id + '"' + ' data-wid="' + w.user_id + '"' +
' style="' + (assigned ? 'opacity:0.5; pointer-events:none;' : '') + '">' + ' style="' + (assigned ? 'opacity:0.5; pointer-events:none;' : '') + '">' +
'<div class="worker-check">&#10003;</div>' + '<div class="worker-check">&#10003;</div>' +
'<div class="worker-info">' + '<div class="worker-info">' +
@@ -272,7 +272,7 @@
delete W.workerNames[workerId]; delete W.workerNames[workerId];
} else { } else {
W.workers.add(workerId); W.workers.add(workerId);
var w = window.TbmState.allWorkers.find(function(x) { return x.worker_id === workerId; }); var w = window.TbmState.allWorkers.find(function(x) { return x.user_id === workerId; });
if (w) W.workerNames[workerId] = w.worker_name; if (w) W.workerNames[workerId] = w.worker_name;
} }
var card = document.querySelector('[data-wid="' + workerId + '"]'); var card = document.querySelector('[data-wid="' + workerId + '"]');
@@ -284,7 +284,7 @@
window.toggleAllWorkers = function() { window.toggleAllWorkers = function() {
var workers = window.TbmState.allWorkers; var workers = window.TbmState.allWorkers;
var availableWorkers = workers.filter(function(w) { var availableWorkers = workers.filter(function(w) {
var a = W.todayAssignments && W.todayAssignments[w.worker_id]; var a = W.todayAssignments && W.todayAssignments[w.user_id];
return !(a && a.total_hours >= 8); return !(a && a.total_hours >= 8);
}); });
if (W.workers.size === availableWorkers.length) { if (W.workers.size === availableWorkers.length) {
@@ -292,8 +292,8 @@
W.workerNames = {}; W.workerNames = {};
} else { } else {
availableWorkers.forEach(function(w) { availableWorkers.forEach(function(w) {
W.workers.add(w.worker_id); W.workers.add(w.user_id);
W.workerNames[w.worker_id] = w.worker_name; W.workerNames[w.user_id] = w.worker_name;
}); });
} }
renderStepWorkers(document.getElementById('stepContainer')); renderStepWorkers(document.getElementById('stepContainer'));
@@ -522,7 +522,7 @@
// 1. TBM 세션 생성 // 1. TBM 세션 생성
var sessionData = { var sessionData = {
session_date: W.sessionDate, session_date: W.sessionDate,
leader_id: leaderId leader_user_id: leaderId
}; };
var response = await window.apiCall('/tbm/sessions', 'POST', sessionData); var response = await window.apiCall('/tbm/sessions', 'POST', sessionData);
@@ -536,7 +536,7 @@
var members = []; var members = [];
W.workers.forEach(function(wid) { W.workers.forEach(function(wid) {
members.push({ members.push({
worker_id: wid, user_id: wid,
project_id: W.projectId, project_id: W.projectId,
work_type_id: W.workTypeId, work_type_id: W.workTypeId,
task_id: null, task_id: null,

View File

@@ -124,10 +124,10 @@
function isMySession(s) { function isMySession(s) {
var userId = currentUser.user_id; var userId = currentUser.user_id;
var workerId = currentUser.worker_id; var workerId = currentUser.user_id;
var userName = currentUser.name; var userName = currentUser.name;
return (userId && String(s.created_by) === String(userId)) || return (userId && String(s.created_by) === String(userId)) ||
(workerId && String(s.leader_id) === String(workerId)) || (workerId && String(s.leader_user_id) === String(workerId)) ||
(userName && s.created_by_name === userName); (userName && s.created_by_name === userName);
} }
@@ -645,7 +645,7 @@
var wpId = document.getElementById('de_wp_' + i).value || null; var wpId = document.getElementById('de_wp_' + i).value || null;
members.push({ members.push({
worker_id: m.worker_id, user_id: m.user_id,
project_id: m.project_id || deSession.project_id || null, project_id: m.project_id || deSession.project_id || null,
work_type_id: m.work_type_id || deSession.work_type_id || null, work_type_id: m.work_type_id || deSession.work_type_id || null,
task_id: taskId ? parseInt(taskId) : null, task_id: taskId ? parseInt(taskId) : null,
@@ -798,7 +798,7 @@
if (type === 'early' && (!hours || hours <= 0)) { if (type === 'early' && (!hours || hours <= 0)) {
window.showToast(esc(completeTeamMembers[i].worker_name) + '의 근무 시간을 입력해주세요.', 'error'); return; window.showToast(esc(completeTeamMembers[i].worker_name) + '의 근무 시간을 입력해주세요.', 'error'); return;
} }
attendanceData.push({ worker_id: completeTeamMembers[i].worker_id, attendance_type: type, attendance_hours: hours }); attendanceData.push({ user_id: completeTeamMembers[i].user_id, attendance_type: type, attendance_hours: hours });
} }
var btn = document.getElementById('completeSheetBtn'); var btn = document.getElementById('completeSheetBtn');
@@ -996,7 +996,7 @@
// 1) 기존 항목: 시간만 줄이기 (프로젝트/공정 유지) // 1) 기존 항목: 시간만 줄이기 (프로젝트/공정 유지)
await window.TbmAPI.updateTeamMember(deSessionId, { await window.TbmAPI.updateTeamMember(deSessionId, {
worker_id: m.worker_id, user_id: m.user_id,
project_id: m.project_id || null, project_id: m.project_id || null,
work_type_id: m.work_type_id || null, work_type_id: m.work_type_id || null,
task_id: m.task_id || null, task_id: m.task_id || null,
@@ -1009,7 +1009,7 @@
// 2) 나머지 시간으로 새 항목 추가 (프로젝트/공정 변경 가능) // 2) 나머지 시간으로 새 항목 추가 (프로젝트/공정 변경 가능)
await window.TbmAPI.splitAssignment(deSessionId, { await window.TbmAPI.splitAssignment(deSessionId, {
worker_id: m.worker_id, user_id: m.user_id,
work_hours: remainHoursKeep, work_hours: remainHoursKeep,
project_id: newProjectId, project_id: newProjectId,
work_type_id: newWorkTypeId work_type_id: newWorkTypeId
@@ -1033,7 +1033,7 @@
// transfer API 호출 // transfer API 호출
var res = await window.TbmAPI.transfer({ var res = await window.TbmAPI.transfer({
transfer_type: 'send', transfer_type: 'send',
worker_id: m.worker_id, user_id: m.user_id,
source_session_id: deSessionId, source_session_id: deSessionId,
dest_session_id: splitTargetSessionId, dest_session_id: splitTargetSessionId,
hours: remainHours, hours: remainHours,
@@ -1094,7 +1094,7 @@
if (!myDraftSession) { if (!myDraftSession) {
btnHtml = '<button type="button" class="pull-btn" disabled title="내 TBM이 없음">내 TBM 없음</button>'; btnHtml = '<button type="button" class="pull-btn" disabled title="내 TBM이 없음">내 TBM 없음</button>';
} else { } else {
btnHtml = '<button type="button" class="pull-btn" onclick="event.stopPropagation(); startPull(' + m.worker_id + ', \'' + esc(m.worker_name).replace(/'/g, "\\'") + '\', ' + hours + ')">빼오기</button>'; btnHtml = '<button type="button" class="pull-btn" onclick="event.stopPropagation(); startPull(' + m.user_id + ', \'' + esc(m.worker_name).replace(/'/g, "\\'") + '\', ' + hours + ')">빼오기</button>';
} }
html += '<div class="pull-member-item">' + html += '<div class="pull-member-item">' +
@@ -1129,7 +1129,7 @@
}; };
window.startPull = async function(workerId, workerName, maxHours) { window.startPull = async function(workerId, workerName, maxHours) {
pullWorker = { worker_id: workerId, worker_name: workerName, max_hours: maxHours }; pullWorker = { user_id: workerId, worker_name: workerName, max_hours: maxHours };
document.getElementById('pullHoursTitle').textContent = esc(workerName) + ' 빼오기'; document.getElementById('pullHoursTitle').textContent = esc(workerName) + ' 빼오기';
document.getElementById('pullHoursSubtitle').textContent = '최대 ' + maxHours + 'h 가능'; document.getElementById('pullHoursSubtitle').textContent = '최대 ' + maxHours + 'h 가능';
document.getElementById('pullHoursInput').value = maxHours; document.getElementById('pullHoursInput').value = maxHours;
@@ -1167,7 +1167,7 @@
var pullWorkTypeId = document.getElementById('pullWorkTypeId').value || null; var pullWorkTypeId = document.getElementById('pullWorkTypeId').value || null;
var res = await window.TbmAPI.transfer({ var res = await window.TbmAPI.transfer({
transfer_type: 'pull', transfer_type: 'pull',
worker_id: pullWorker.worker_id, user_id: pullWorker.user_id,
source_session_id: pullSessionId, source_session_id: pullSessionId,
dest_session_id: myDraftSession.session_id, dest_session_id: myDraftSession.session_id,
hours: hours, hours: hours,
@@ -1230,13 +1230,13 @@
// 현재 세션 리더를 제외한 반장/그룹장 목록 // 현재 세션 리더를 제외한 반장/그룹장 목록
var leaders = workers.filter(function(w) { var leaders = workers.filter(function(w) {
return (w.job_type === 'leader' || w.job_type === '그룹장' || w.job_type === 'admin') && return (w.job_type === 'leader' || w.job_type === '그룹장' || w.job_type === 'admin') &&
w.worker_id !== handoverSession.leader_id; w.user_id !== handoverSession.leader_user_id;
}); });
var leaderSelect = document.getElementById('handoverLeaderId'); var leaderSelect = document.getElementById('handoverLeaderId');
leaderSelect.innerHTML = '<option value="">반장 선택...</option>' + leaderSelect.innerHTML = '<option value="">반장 선택...</option>' +
leaders.map(function(w) { leaders.map(function(w) {
return '<option value="' + w.worker_id + '">' + esc(w.worker_name) + ' (' + (w.job_type || '') + ')</option>'; return '<option value="' + w.user_id + '">' + esc(w.worker_name) + ' (' + (w.job_type || '') + ')</option>';
}).join(''); }).join('');
// 인계할 팀원 체크리스트 // 인계할 팀원 체크리스트
@@ -1246,7 +1246,7 @@
} else { } else {
listEl.innerHTML = team.map(function(m) { listEl.innerHTML = team.map(function(m) {
return '<label style="display:flex; align-items:center; gap:0.5rem; padding:0.5rem; cursor:pointer;">' + return '<label style="display:flex; align-items:center; gap:0.5rem; padding:0.5rem; cursor:pointer;">' +
'<input type="checkbox" class="handover-worker-cb" value="' + m.worker_id + '" checked style="width:16px; height:16px;">' + '<input type="checkbox" class="handover-worker-cb" value="' + m.user_id + '" checked style="width:16px; height:16px;">' +
'<span style="font-weight:500; font-size:0.875rem;">' + esc(m.worker_name) + '</span>' + '<span style="font-weight:500; font-size:0.875rem;">' + esc(m.worker_name) + '</span>' +
'<span style="font-size:0.75rem; color:#6b7280; margin-left:auto;">' + (m.job_type || '') + '</span>' + '<span style="font-size:0.75rem; color:#6b7280; margin-left:auto;">' + (m.job_type || '') + '</span>' +
'</label>'; '</label>';
@@ -1301,13 +1301,13 @@
var handoverData = { var handoverData = {
session_id: handoverSessionId, session_id: handoverSessionId,
from_leader_id: handoverSession.leader_id, from_leader_user_id: handoverSession.leader_user_id,
to_leader_id: toLeaderId, to_leader_user_id: toLeaderId,
handover_date: today, handover_date: today,
handover_time: now, handover_time: now,
reason: '모바일 인계', reason: '모바일 인계',
handover_notes: notes, handover_notes: notes,
worker_ids: workerIds user_ids: workerIds
}; };
var res = await window.TbmAPI.saveHandover(handoverData); var res = await window.TbmAPI.saveHandover(handoverData);

View File

@@ -394,11 +394,11 @@ function openNewTbmModal() {
} }
// 입력자 자동 설정 (readonly) // 입력자 자동 설정 (readonly)
if (currentUser && currentUser.worker_id) { if (currentUser && currentUser.user_id) {
const worker = allWorkers.find(w => w.worker_id === currentUser.worker_id); const worker = allWorkers.find(w => w.user_id === currentUser.user_id);
if (worker) { if (worker) {
document.getElementById('leaderName').textContent = worker.worker_name; document.getElementById('leaderName').textContent = worker.worker_name;
document.getElementById('leaderId').value = worker.worker_id; document.getElementById('leaderId').value = worker.user_id;
} }
} else if (currentUser && currentUser.name) { } else if (currentUser && currentUser.name) {
document.getElementById('leaderName').textContent = currentUser.name; document.getElementById('leaderName').textContent = currentUser.name;
@@ -444,7 +444,7 @@ async function renderNewTbmWorkerGrid() {
todayAssignmentsMap = {}; todayAssignmentsMap = {};
assignments.forEach(a => { assignments.forEach(a => {
if (a.sessions && a.sessions.length > 0) { if (a.sessions && a.sessions.length > 0) {
todayAssignmentsMap[a.worker_id] = a; todayAssignmentsMap[a.user_id] = a;
} }
}); });
} catch(e) { } catch(e) {
@@ -454,8 +454,8 @@ async function renderNewTbmWorkerGrid() {
} }
grid.innerHTML = allWorkers.map(w => { grid.innerHTML = allWorkers.map(w => {
const checked = selectedWorkersForNewTbm.has(w.worker_id) ? 'checked' : ''; const checked = selectedWorkersForNewTbm.has(w.user_id) ? 'checked' : '';
const assignment = todayAssignmentsMap[w.worker_id]; const assignment = todayAssignmentsMap[w.user_id];
const fullyAssigned = assignment && assignment.total_hours >= 8; const fullyAssigned = assignment && assignment.total_hours >= 8;
const partiallyAssigned = assignment && assignment.total_hours > 0 && assignment.total_hours < 8; const partiallyAssigned = assignment && assignment.total_hours > 0 && assignment.total_hours < 8;
@@ -474,9 +474,9 @@ async function renderNewTbmWorkerGrid() {
} }
return ` return `
<label class="tbm-worker-select-item ${checked ? 'selected' : ''}" data-wid="${w.worker_id}" style="${disabledStyle}"> <label class="tbm-worker-select-item ${checked ? 'selected' : ''}" data-wid="${w.user_id}" style="${disabledStyle}">
<input type="checkbox" class="new-tbm-worker-cb" data-worker-id="${w.worker_id}" ${checked} ${disabledAttr} <input type="checkbox" class="new-tbm-worker-cb" data-user-id="${w.user_id}" ${checked} ${disabledAttr}
onchange="toggleNewTbmWorker(${w.worker_id}, this.checked)"> onchange="toggleNewTbmWorker(${w.user_id}, this.checked)">
<span class="tbm-worker-name">${escapeHtml(w.worker_name)}</span> <span class="tbm-worker-name">${escapeHtml(w.worker_name)}</span>
<span class="tbm-worker-role">${escapeHtml(w.job_type || '작업자')}</span> <span class="tbm-worker-role">${escapeHtml(w.job_type || '작업자')}</span>
${badgeHtml} ${badgeHtml}
@@ -511,9 +511,9 @@ window.toggleNewTbmWorker = toggleNewTbmWorker;
function selectAllNewTbmWorkers() { function selectAllNewTbmWorkers() {
allWorkers.forEach(w => { allWorkers.forEach(w => {
const a = todayAssignmentsMap && todayAssignmentsMap[w.worker_id]; const a = todayAssignmentsMap && todayAssignmentsMap[w.user_id];
if (a && a.total_hours >= 8) return; // 종일 배정 제외 if (a && a.total_hours >= 8) return; // 종일 배정 제외
selectedWorkersForNewTbm.add(w.worker_id); selectedWorkersForNewTbm.add(w.user_id);
}); });
document.querySelectorAll('.new-tbm-worker-cb').forEach(cb => { document.querySelectorAll('.new-tbm-worker-cb').forEach(cb => {
if (!cb.disabled) cb.checked = true; if (!cb.disabled) cb.checked = true;
@@ -539,12 +539,12 @@ function populateLeaderSelect() {
if (!leaderSelect) return; if (!leaderSelect) return;
// 로그인한 사용자가 작업자와 연결되어 있는지 확인 // 로그인한 사용자가 작업자와 연결되어 있는지 확인
if (currentUser && currentUser.worker_id) { if (currentUser && currentUser.user_id) {
// 작업자와 연결된 경우: 자동으로 선택하고 비활성화 // 작업자와 연결된 경우: 자동으로 선택하고 비활성화
const worker = allWorkers.find(w => w.worker_id === currentUser.worker_id); const worker = allWorkers.find(w => w.user_id === currentUser.user_id);
if (worker) { if (worker) {
const jobTypeText = worker.job_type ? ` (${escapeHtml(worker.job_type)})` : ''; const jobTypeText = worker.job_type ? ` (${escapeHtml(worker.job_type)})` : '';
leaderSelect.innerHTML = `<option value="${escapeHtml(worker.worker_id)}" selected>${escapeHtml(worker.worker_name)}${jobTypeText}</option>`; leaderSelect.innerHTML = `<option value="${escapeHtml(worker.user_id)}" selected>${escapeHtml(worker.worker_name)}${jobTypeText}</option>`;
leaderSelect.disabled = true; leaderSelect.disabled = true;
console.log('✅ 입력자 자동 설정:', worker.worker_name); console.log('✅ 입력자 자동 설정:', worker.worker_name);
} else { } else {
@@ -553,7 +553,7 @@ function populateLeaderSelect() {
leaderSelect.disabled = true; leaderSelect.disabled = true;
} }
} else { } else {
// 관리자 계정 (worker_id가 없음): 드롭다운으로 선택 가능 // 관리자 계정 (user_id가 없음): 드롭다운으로 선택 가능
const leaders = allWorkers.filter(w => const leaders = allWorkers.filter(w =>
w.job_type === 'leader' || w.job_type === '그룹장' || w.job_type === 'admin' w.job_type === 'leader' || w.job_type === '그룹장' || w.job_type === 'admin'
); );
@@ -561,7 +561,7 @@ function populateLeaderSelect() {
leaderSelect.innerHTML = '<option value="">입력자 선택...</option>' + leaderSelect.innerHTML = '<option value="">입력자 선택...</option>' +
leaders.map(w => { leaders.map(w => {
const jobTypeText = w.job_type ? ` (${escapeHtml(w.job_type)})` : ''; const jobTypeText = w.job_type ? ` (${escapeHtml(w.job_type)})` : '';
return `<option value="${escapeHtml(w.worker_id)}">${escapeHtml(w.worker_name)}${jobTypeText}</option>`; return `<option value="${escapeHtml(w.user_id)}">${escapeHtml(w.worker_name)}${jobTypeText}</option>`;
}).join(''); }).join('');
leaderSelect.disabled = false; leaderSelect.disabled = false;
console.log('✅ 관리자: 입력자 선택 가능'); console.log('✅ 관리자: 입력자 선택 가능');
@@ -646,8 +646,8 @@ async function saveTbmSession() {
let leaderId = parseInt(document.getElementById('leaderId').value); let leaderId = parseInt(document.getElementById('leaderId').value);
if (!leaderId || isNaN(leaderId)) { if (!leaderId || isNaN(leaderId)) {
if (!currentUser.worker_id) { if (!currentUser.user_id) {
console.log('📝 관리자 계정: leader_id를 NULL로 설정'); console.log('📝 관리자 계정: leader_user_id를 NULL로 설정');
leaderId = null; leaderId = null;
} else { } else {
console.error('❌ 입력자 설정 오류'); console.error('❌ 입력자 설정 오류');
@@ -658,7 +658,7 @@ async function saveTbmSession() {
const sessionData = { const sessionData = {
session_date: document.getElementById('sessionDate').value, session_date: document.getElementById('sessionDate').value,
leader_id: leaderId leader_user_id: leaderId
}; };
if (!sessionData.session_date) { if (!sessionData.session_date) {
@@ -680,7 +680,7 @@ async function saveTbmSession() {
for (const workerData of workerTaskList) { for (const workerData of workerTaskList) {
for (const taskLine of workerData.tasks) { for (const taskLine of workerData.tasks) {
members.push({ members.push({
worker_id: workerData.worker_id, user_id: workerData.user_id,
project_id: taskLine.project_id || null, project_id: taskLine.project_id || null,
work_type_id: taskLine.work_type_id, work_type_id: taskLine.work_type_id,
task_id: taskLine.task_id, task_id: taskLine.task_id,
@@ -732,7 +732,7 @@ async function saveTbmSession() {
const members = []; const members = [];
selectedWorkersForNewTbm.forEach(workerId => { selectedWorkersForNewTbm.forEach(workerId => {
members.push({ members.push({
worker_id: workerId, user_id: workerId,
project_id: projectId, project_id: projectId,
work_type_id: workTypeId, work_type_id: workTypeId,
task_id: null, task_id: null,
@@ -894,11 +894,11 @@ function openWorkerSelectionModal() {
if (!workerCardGrid) return; if (!workerCardGrid) return;
// 이미 추가된 작업자 ID 세트 // 이미 추가된 작업자 ID 세트
const addedWorkerIds = new Set(workerTaskList.map(w => w.worker_id)); const addedWorkerIds = new Set(workerTaskList.map(w => w.user_id));
workerCardGrid.innerHTML = allWorkers.map(worker => { workerCardGrid.innerHTML = allWorkers.map(worker => {
const isAdded = addedWorkerIds.has(worker.worker_id); const isAdded = addedWorkerIds.has(worker.user_id);
const safeWorkerId = parseInt(worker.worker_id) || 0; const safeWorkerId = parseInt(worker.user_id) || 0;
return ` return `
<div id="worker-card-${safeWorkerId}" <div id="worker-card-${safeWorkerId}"
onclick="toggleWorkerSelection(${safeWorkerId})" onclick="toggleWorkerSelection(${safeWorkerId})"
@@ -923,7 +923,7 @@ window.openWorkerSelectionModal = openWorkerSelectionModal;
// 작업자 선택 토글 // 작업자 선택 토글
function toggleWorkerSelection(workerId) { function toggleWorkerSelection(workerId) {
// 이미 추가된 작업자는 선택 불가 // 이미 추가된 작업자는 선택 불가
const alreadyAdded = workerTaskList.some(w => w.worker_id === workerId); const alreadyAdded = workerTaskList.some(w => w.user_id === workerId);
if (alreadyAdded) return; if (alreadyAdded) return;
const card = document.getElementById(`worker-card-${workerId}`); const card = document.getElementById(`worker-card-${workerId}`);
@@ -945,11 +945,11 @@ window.toggleWorkerSelection = toggleWorkerSelection;
// 전체 선택 // 전체 선택
function selectAllWorkersInModal() { function selectAllWorkersInModal() {
const addedWorkerIds = new Set(workerTaskList.map(w => w.worker_id)); const addedWorkerIds = new Set(workerTaskList.map(w => w.user_id));
allWorkers.forEach(worker => { allWorkers.forEach(worker => {
if (!addedWorkerIds.has(worker.worker_id)) { if (!addedWorkerIds.has(worker.user_id)) {
selectedWorkersInModal.add(worker.worker_id); selectedWorkersInModal.add(worker.user_id);
const card = document.getElementById(`worker-card-${worker.worker_id}`); const card = document.getElementById(`worker-card-${worker.user_id}`);
if (card) { if (card) {
card.style.borderColor = '#3b82f6'; card.style.borderColor = '#3b82f6';
card.style.background = '#eff6ff'; card.style.background = '#eff6ff';
@@ -982,10 +982,10 @@ function confirmWorkerSelection() {
} }
selectedWorkersInModal.forEach(workerId => { selectedWorkersInModal.forEach(workerId => {
const worker = allWorkers.find(w => w.worker_id === workerId); const worker = allWorkers.find(w => w.user_id === workerId);
if (worker) { if (worker) {
workerTaskList.push({ workerTaskList.push({
worker_id: worker.worker_id, user_id: worker.user_id,
worker_name: worker.worker_name, worker_name: worker.worker_name,
job_type: worker.job_type, job_type: worker.job_type,
tasks: [ tasks: [
@@ -1970,16 +1970,16 @@ async function openTeamCompositionModal(sessionId) {
// 팀원별로 작업 그룹화 // 팀원별로 작업 그룹화
teamMembers.forEach(member => { teamMembers.forEach(member => {
if (!workerMap.has(member.worker_id)) { if (!workerMap.has(member.user_id)) {
workerMap.set(member.worker_id, { workerMap.set(member.user_id, {
worker_id: member.worker_id, user_id: member.user_id,
worker_name: member.worker_name, worker_name: member.worker_name,
job_type: member.job_type, job_type: member.job_type,
tasks: [] tasks: []
}); });
} }
workerMap.get(member.worker_id).tasks.push({ workerMap.get(member.user_id).tasks.push({
task_line_id: generateUUID(), task_line_id: generateUUID(),
project_id: member.project_id, project_id: member.project_id,
work_type_id: member.work_type_id, work_type_id: member.work_type_id,
@@ -2003,7 +2003,7 @@ async function openTeamCompositionModal(sessionId) {
// 입력자 표시 // 입력자 표시
if (session.leader_name) { if (session.leader_name) {
document.getElementById('leaderName').value = `${session.leader_name} (${session.leader_job_type || ''})`; document.getElementById('leaderName').value = `${session.leader_name} (${session.leader_job_type || ''})`;
document.getElementById('leaderId').value = session.leader_id; document.getElementById('leaderId').value = session.leader_user_id;
} else if (session.created_by_name) { } else if (session.created_by_name) {
document.getElementById('leaderName').value = `${session.created_by_name} (관리자)`; document.getElementById('leaderName').value = `${session.created_by_name} (관리자)`;
document.getElementById('leaderId').value = ''; document.getElementById('leaderId').value = '';
@@ -2026,7 +2026,7 @@ function updateSelectedWorkers() {
selectedWorkers.clear(); selectedWorkers.clear();
document.querySelectorAll('.worker-checkbox:checked').forEach(cb => { document.querySelectorAll('.worker-checkbox:checked').forEach(cb => {
selectedWorkers.add(parseInt(cb.dataset.workerId)); selectedWorkers.add(parseInt(cb.dataset.userId));
}); });
const selectedCount = document.getElementById('selectedCount'); const selectedCount = document.getElementById('selectedCount');
@@ -2038,7 +2038,7 @@ function updateSelectedWorkers() {
selectedList.innerHTML = '<p style="margin: 0; color: #9ca3af; font-size: 0.875rem;">작업자를 선택해주세요</p>'; selectedList.innerHTML = '<p style="margin: 0; color: #9ca3af; font-size: 0.875rem;">작업자를 선택해주세요</p>';
} else { } else {
const selectedWorkersArray = Array.from(selectedWorkers).map(id => { const selectedWorkersArray = Array.from(selectedWorkers).map(id => {
const worker = allWorkers.find(w => w.worker_id === id); const worker = allWorkers.find(w => w.user_id === id);
return worker ? ` return worker ? `
<span style="display: inline-flex; align-items: center; gap: 0.25rem; padding: 0.25rem 0.75rem; background: #3b82f6; color: white; border-radius: 9999px; font-size: 0.875rem;"> <span style="display: inline-flex; align-items: center; gap: 0.25rem; padding: 0.25rem 0.75rem; background: #3b82f6; color: white; border-radius: 9999px; font-size: 0.875rem;">
${worker.worker_name} ${worker.worker_name}
@@ -2053,7 +2053,7 @@ window.updateSelectedWorkers = updateSelectedWorkers;
// 작업자 제거 // 작업자 제거
function removeWorker(workerId) { function removeWorker(workerId) {
const checkbox = document.querySelector(`.worker-checkbox[data-worker-id="${workerId}"]`); const checkbox = document.querySelector(`.worker-checkbox[data-user-id="${workerId}"]`);
if (checkbox) { if (checkbox) {
checkbox.checked = false; checkbox.checked = false;
updateSelectedWorkers(); updateSelectedWorkers();
@@ -2094,7 +2094,7 @@ async function saveTeamComposition() {
} }
const members = Array.from(selectedWorkers).map(workerId => ({ const members = Array.from(selectedWorkers).map(workerId => ({
worker_id: workerId user_id: workerId
})); }));
try { try {
@@ -2427,7 +2427,7 @@ async function completeTbmSession() {
} }
attendanceData.push({ attendanceData.push({
worker_id: completeModalTeam[i].worker_id, user_id: completeModalTeam[i].user_id,
attendance_type: type, attendance_type: type,
attendance_hours: hours attendance_hours: hours
}); });
@@ -2527,15 +2527,15 @@ async function viewTbmSession(sessionId) {
// 작업자별로 그룹화 // 작업자별로 그룹화
const workerMap = new Map(); const workerMap = new Map();
team.forEach(member => { team.forEach(member => {
if (!workerMap.has(member.worker_id)) { if (!workerMap.has(member.user_id)) {
workerMap.set(member.worker_id, { workerMap.set(member.user_id, {
worker_name: member.worker_name, worker_name: member.worker_name,
job_type: member.job_type, job_type: member.job_type,
is_present: member.is_present, is_present: member.is_present,
tasks: [] tasks: []
}); });
} }
workerMap.get(member.worker_id).tasks.push(member); workerMap.get(member.user_id).tasks.push(member);
}); });
teamContainer.style.display = 'flex'; teamContainer.style.display = 'flex';
@@ -2692,12 +2692,12 @@ async function openHandoverModal(sessionId) {
const toLeaderSelect = document.getElementById('toLeaderId'); const toLeaderSelect = document.getElementById('toLeaderId');
const otherLeaders = allWorkers.filter(w => const otherLeaders = allWorkers.filter(w =>
(w.job_type === 'leader' || w.job_type === '그룹장' || w.job_type === 'admin') && (w.job_type === 'leader' || w.job_type === '그룹장' || w.job_type === 'admin') &&
w.worker_id !== session.leader_id w.user_id !== session.leader_user_id
); );
toLeaderSelect.innerHTML = '<option value="">인수자 선택...</option>' + toLeaderSelect.innerHTML = '<option value="">인수자 선택...</option>' +
otherLeaders.map(w => ` otherLeaders.map(w => `
<option value="${w.worker_id}">${w.worker_name} (${w.job_type || ''})</option> <option value="${w.user_id}">${w.worker_name} (${w.job_type || ''})</option>
`).join(''); `).join('');
// 인계할 팀원 목록 // 인계할 팀원 목록
@@ -2710,7 +2710,7 @@ async function openHandoverModal(sessionId) {
onmouseover="this.style.background='#f9fafb'" onmouseout="this.style.background='white'"> onmouseover="this.style.background='#f9fafb'" onmouseout="this.style.background='white'">
<input type="checkbox" <input type="checkbox"
class="handover-worker-checkbox" class="handover-worker-checkbox"
value="${member.worker_id}" value="${member.user_id}"
checked checked
style="width: 16px; height: 16px; cursor: pointer;"> style="width: 16px; height: 16px; cursor: pointer;">
<span style="font-weight: 500; font-size: 0.875rem;">${member.worker_name}</span> <span style="font-weight: 500; font-size: 0.875rem;">${member.worker_name}</span>
@@ -2771,9 +2771,9 @@ async function saveHandover() {
} }
try { try {
// 세션 정보 조회 (from_leader_id 가져오기) // 세션 정보 조회 (from_leader_user_id 가져오기)
const sessionData = await window.TbmAPI.getSession(sessionId); const sessionData = await window.TbmAPI.getSession(sessionId);
const fromLeaderId = sessionData?.leader_id; const fromLeaderId = sessionData?.leader_user_id;
if (!fromLeaderId) { if (!fromLeaderId) {
showToast('세션 정보를 찾을 수 없습니다.', 'error'); showToast('세션 정보를 찾을 수 없습니다.', 'error');
@@ -2782,13 +2782,13 @@ async function saveHandover() {
const handoverData = { const handoverData = {
session_id: sessionId, session_id: sessionId,
from_leader_id: fromLeaderId, from_leader_user_id: fromLeaderId,
to_leader_id: toLeaderId, to_leader_user_id: toLeaderId,
handover_date: handoverDate, handover_date: handoverDate,
handover_time: handoverTime, handover_time: handoverTime,
reason: reason, reason: reason,
handover_notes: handoverNotes, handover_notes: handoverNotes,
worker_ids: workerIds user_ids: workerIds
}; };
const response = await window.TbmAPI.saveHandover(handoverData); const response = await window.TbmAPI.saveHandover(handoverData);
@@ -2855,12 +2855,12 @@ async function executeSplit(memberIdx) {
} }
try { try {
await window.TbmAPI.updateTeamMember(splitModalSessionId, { await window.TbmAPI.updateTeamMember(splitModalSessionId, {
worker_id: m.worker_id, project_id: m.project_id, work_type_id: m.work_type_id, user_id: m.user_id, project_id: m.project_id, work_type_id: m.work_type_id,
task_id: m.task_id, workplace_category_id: m.workplace_category_id, workplace_id: m.workplace_id, task_id: m.task_id, workplace_category_id: m.workplace_category_id, workplace_id: m.workplace_id,
work_detail: m.work_detail, is_present: true, work_hours: splitHours work_detail: m.work_detail, is_present: true, work_hours: splitHours
}); });
await window.TbmAPI.splitAssignment(splitModalSessionId, { await window.TbmAPI.splitAssignment(splitModalSessionId, {
worker_id: m.worker_id, work_hours: currentHours - splitHours, user_id: m.user_id, work_hours: currentHours - splitHours,
project_id: m.project_id, work_type_id: m.work_type_id project_id: m.project_id, work_type_id: m.work_type_id
}); });
showToast(`${escapeHtml(m.worker_name)} 분할 완료: ${splitHours}h + ${currentHours - splitHours}h`, 'success'); showToast(`${escapeHtml(m.worker_name)} 분할 완료: ${splitHours}h + ${currentHours - splitHours}h`, 'success');
@@ -2934,8 +2934,8 @@ async function togglePullSessionMembers(sessionId, el) {
<div style="display:flex; align-items:center; justify-content:space-between; padding:0.375rem 0; border-bottom:1px solid #f9fafb;"> <div style="display:flex; align-items:center; justify-content:space-between; padding:0.375rem 0; border-bottom:1px solid #f9fafb;">
<span>${escapeHtml(m.worker_name)} <span style="font-size:0.75rem; color:#6b7280;">(${hours}h)</span></span> <span>${escapeHtml(m.worker_name)} <span style="font-size:0.75rem; color:#6b7280;">(${hours}h)</span></span>
<div style="display:flex; gap:0.25rem; align-items:center;"> <div style="display:flex; gap:0.25rem; align-items:center;">
<input type="number" id="pull_h_${sessionId}_${m.worker_id}" step="0.5" min="0.5" max="${hours}" value="${hours}" style="width:60px; padding:0.25rem; border:1px solid #d1d5db; border-radius:0.25rem; font-size:0.75rem;"> <input type="number" id="pull_h_${sessionId}_${m.user_id}" step="0.5" min="0.5" max="${hours}" value="${hours}" style="width:60px; padding:0.25rem; border:1px solid #d1d5db; border-radius:0.25rem; font-size:0.75rem;">
<button type="button" class="tbm-btn tbm-btn-primary" style="padding:0.25rem 0.5rem; font-size:0.75rem;" onclick="executePull(${sessionId}, ${m.worker_id}, '${escapeHtml(m.worker_name)}')">빼오기</button> <button type="button" class="tbm-btn tbm-btn-primary" style="padding:0.25rem 0.5rem; font-size:0.75rem;" onclick="executePull(${sessionId}, ${m.user_id}, '${escapeHtml(m.worker_name)}')">빼오기</button>
</div> </div>
</div>`; </div>`;
}).join('') || '<div style="color:#9ca3af; padding:0.25rem;">팀원 없음</div>'; }).join('') || '<div style="color:#9ca3af; padding:0.25rem;">팀원 없음</div>';
@@ -2954,7 +2954,7 @@ async function executePull(sourceSessionId, workerId, workerName) {
try { try {
const res = await window.TbmAPI.transfer({ const res = await window.TbmAPI.transfer({
transfer_type: 'pull', transfer_type: 'pull',
worker_id: workerId, user_id: workerId,
source_session_id: sourceSessionId, source_session_id: sourceSessionId,
dest_session_id: pullModalSessionId, dest_session_id: pullModalSessionId,
hours: hours hours: hours

View File

@@ -18,7 +18,7 @@ class TbmAPI {
// 현재 로그인한 사용자 정보 가져오기 // 현재 로그인한 사용자 정보 가져오기
const userInfo = JSON.parse(localStorage.getItem('sso_user') || '{}'); const userInfo = JSON.parse(localStorage.getItem('sso_user') || '{}');
this.state.currentUser = userInfo; this.state.currentUser = userInfo;
console.log('👤 로그인 사용자:', this.state.currentUser, 'worker_id:', this.state.currentUser?.worker_id); console.log('👤 로그인 사용자:', this.state.currentUser, 'user_id:', this.state.currentUser?.user_id);
// 병렬로 데이터 로드 // 병렬로 데이터 로드
await Promise.all([ await Promise.all([

View File

@@ -77,7 +77,7 @@ class TbmState extends BaseState {
*/ */
addWorkerToList(worker) { addWorkerToList(worker) {
this.workerTaskList.push({ this.workerTaskList.push({
worker_id: worker.worker_id, user_id: worker.user_id,
worker_name: worker.worker_name, worker_name: worker.worker_name,
job_type: worker.job_type, job_type: worker.job_type,
tasks: [this.createEmptyTaskLine()] tasks: [this.createEmptyTaskLine()]

View File

@@ -81,7 +81,7 @@ async function loadWorkers() {
const selectWorker = document.getElementById('individualWorker'); const selectWorker = document.getElementById('individualWorker');
workers.forEach(worker => { workers.forEach(worker => {
const option = document.createElement('option'); const option = document.createElement('option');
option.value = worker.worker_id; option.value = worker.user_id;
option.textContent = `${worker.worker_name} (${worker.employment_status === 'employed' ? '재직' : '퇴사'})`; option.textContent = `${worker.worker_name} (${worker.employment_status === 'employed' ? '재직' : '퇴사'})`;
selectWorker.appendChild(option); selectWorker.appendChild(option);
}); });
@@ -293,7 +293,7 @@ async function autoCalculateAnnualLeave() {
} }
// 작업자의 입사일 조회 // 작업자의 입사일 조회
const worker = workers.find(w => w.worker_id == workerId); const worker = workers.find(w => w.user_id == workerId);
if (!worker || !worker.hire_date) { if (!worker || !worker.hire_date) {
showToast('작업자의 입사일 정보가 없습니다', 'error'); showToast('작업자의 입사일 정보가 없습니다', 'error');
return; return;
@@ -308,7 +308,7 @@ async function autoCalculateAnnualLeave() {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}, },
body: JSON.stringify({ body: JSON.stringify({
worker_id: workerId, user_id: workerId,
hire_date: worker.hire_date, hire_date: worker.hire_date,
year: year year: year
}) })
@@ -369,7 +369,7 @@ async function submitIndividualVacation() {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}, },
body: JSON.stringify({ body: JSON.stringify({
worker_id: workerId, user_id: workerId,
vacation_type_id: typeId, vacation_type_id: typeId,
year: year, year: year,
total_days: parseFloat(totalDays), total_days: parseFloat(totalDays),
@@ -520,7 +520,7 @@ async function previewBulkAllocation() {
const hireDate = worker.hire_date; const hireDate = worker.hire_date;
if (!hireDate) { if (!hireDate) {
return { return {
worker_id: worker.worker_id, user_id: worker.user_id,
worker_name: worker.worker_name, worker_name: worker.worker_name,
hire_date: '-', hire_date: '-',
years_worked: '-', years_worked: '-',
@@ -534,7 +534,7 @@ async function previewBulkAllocation() {
const yearsWorked = calculateYearsWorked(hireDate, year); const yearsWorked = calculateYearsWorked(hireDate, year);
return { return {
worker_id: worker.worker_id, user_id: worker.user_id,
worker_name: worker.worker_name, worker_name: worker.worker_name,
hire_date: hireDate, hire_date: hireDate,
years_worked: yearsWorked, years_worked: yearsWorked,
@@ -656,7 +656,7 @@ async function submitBulkAllocation() {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}, },
body: JSON.stringify({ body: JSON.stringify({
worker_id: item.worker_id, user_id: item.user_id,
hire_date: item.hire_date, hire_date: item.hire_date,
year: year year: year
}) })

View File

@@ -445,7 +445,7 @@ class WorkAnalysisTableRenderer {
return workerData.map(worker => { return workerData.map(worker => {
// 해당 작업자의 작업 데이터 필터링 // 해당 작업자의 작업 데이터 필터링
const workerWork = recentWorkData ? const workerWork = recentWorkData ?
recentWorkData.filter(work => work.worker_id === worker.worker_id) : []; recentWorkData.filter(work => work.user_id === worker.user_id) : [];
// 프로젝트별로 그룹화 // 프로젝트별로 그룹화
const projectMap = new Map(); const projectMap = new Map();

View File

@@ -47,7 +47,7 @@ async function loadReports() {
return; return;
} }
const nameMap = Object.fromEntries(workers.map(w => [w.worker_id, w.worker_name])); const nameMap = Object.fromEntries(workers.map(w => [w.user_id, w.worker_name]));
const projMap = Object.fromEntries(projects.map(p => [p.project_id, p.project_name])); const projMap = Object.fromEntries(projects.map(p => [p.project_id, p.project_name]));
// const taskMap = Object.fromEntries(tasks.map(t => [t.task_id, `${t.category}:${t.subcategory}`])); // tasks 테이블 삭제됨 // const taskMap = Object.fromEntries(tasks.map(t => [t.task_id, `${t.category}:${t.subcategory}`])); // tasks 테이블 삭제됨
@@ -56,7 +56,7 @@ async function loadReports() {
const tr = document.createElement('tr'); const tr = document.createElement('tr');
tr.innerHTML = ` tr.innerHTML = `
<td>${i + 1}</td> <td>${i + 1}</td>
<td>${nameMap[r.worker_id] || r.worker_id}</td> <td>${nameMap[r.user_id] || r.user_id}</td>
<td><select data-id="project"> <td><select data-id="project">
${projects.map(p => ${projects.map(p =>
`<option value="${p.project_id}" ${p.project_id === r.project_id ? 'selected' : ''}>${p.project_name}</option>` `<option value="${p.project_id}" ${p.project_id === r.project_id ? 'selected' : ''}>${p.project_name}</option>`
@@ -107,7 +107,7 @@ async function loadReports() {
const payload = { const payload = {
date: formatDate(r.date), // 날짜 형식 변환 date: formatDate(r.date), // 날짜 형식 변환
worker_id: r.worker_id, // 기존 작업자 ID 유지 user_id: r.user_id, // 기존 작업자 ID 유지
project_id: Number(projectId), project_id: Number(projectId),
task_id: Number(taskId), task_id: Number(taskId),
overtime_hours: overtimeHours ? Number(overtimeHours) : null, overtime_hours: overtimeHours ? Number(overtimeHours) : null,

View File

@@ -142,8 +142,8 @@ class WorkReportReviewManager {
} }
dummyAttendance.push({ dummyAttendance.push({
id: `att_${worker.worker_id}_${dateStr}`, id: `att_${worker.user_id}_${dateStr}`,
worker_id: worker.worker_id, user_id: worker.user_id,
worker_name: worker.worker_name, worker_name: worker.worker_name,
date: dateStr, date: dateStr,
attendance_type: attendanceType, attendance_type: attendanceType,
@@ -196,7 +196,7 @@ class WorkReportReviewManager {
if (startDate) params.append('start_date', startDate); if (startDate) params.append('start_date', startDate);
if (endDate) params.append('end_date', endDate); if (endDate) params.append('end_date', endDate);
if (workerId) params.append('worker_id', workerId); if (workerId) params.append('user_id', workerId);
if (projectId) params.append('project_id', projectId); if (projectId) params.append('project_id', projectId);
// 페이지네이션 (일단 많이 가져오기) // 페이지네이션 (일단 많이 가져오기)
@@ -250,26 +250,26 @@ class WorkReportReviewManager {
// 각 보고서에 대해 근무시간 검증 // 각 보고서에 대해 근무시간 검증
this.reports.forEach(report => { this.reports.forEach(report => {
const attendance = this.attendanceData.find(att => const attendance = this.attendanceData.find(att =>
att.worker_id === report.worker_id && att.date === report.report_date att.user_id === report.user_id && att.date === report.report_date
); );
if (attendance) { if (attendance) {
const expectedHours = attendance.expected_hours; const expectedHours = attendance.expected_hours;
const actualHours = this.getWorkerDailyHours(report.worker_id, report.report_date); const actualHours = this.getWorkerDailyHours(report.user_id, report.report_date);
report.expected_hours = expectedHours; report.expected_hours = expectedHours;
report.actual_hours = actualHours; report.actual_hours = actualHours;
report.attendance_type = attendance.attendance_type; report.attendance_type = attendance.attendance_type;
report.hours_status = this.getHoursStatus(actualHours, expectedHours); report.hours_status = this.getHoursStatus(actualHours, expectedHours);
report.is_reviewed = this.reviewedReports.has(`${report.worker_id}_${report.report_date}`); report.is_reviewed = this.reviewedReports.has(`${report.user_id}_${report.report_date}`);
} else { } else {
// 휴가 정보가 없으면 기본 8시간으로 가정 // 휴가 정보가 없으면 기본 8시간으로 가정
const actualHours = this.getWorkerDailyHours(report.worker_id, report.report_date); const actualHours = this.getWorkerDailyHours(report.user_id, report.report_date);
report.expected_hours = 8; report.expected_hours = 8;
report.actual_hours = actualHours; report.actual_hours = actualHours;
report.attendance_type = 'NORMAL'; report.attendance_type = 'NORMAL';
report.hours_status = this.getHoursStatus(actualHours, 8); report.hours_status = this.getHoursStatus(actualHours, 8);
report.is_reviewed = this.reviewedReports.has(`${report.worker_id}_${report.report_date}`); report.is_reviewed = this.reviewedReports.has(`${report.user_id}_${report.report_date}`);
} }
}); });
} }
@@ -277,7 +277,7 @@ class WorkReportReviewManager {
getWorkerDailyHours(workerId, date) { getWorkerDailyHours(workerId, date) {
// 해당 작업자의 특정 날짜 총 작업시간 계산 // 해당 작업자의 특정 날짜 총 작업시간 계산
return this.reports return this.reports
.filter(r => r.worker_id === workerId && r.report_date === date) .filter(r => r.user_id === workerId && r.report_date === date)
.reduce((sum, r) => sum + (r.work_hours || 0), 0); .reduce((sum, r) => sum + (r.work_hours || 0), 0);
} }
@@ -315,9 +315,9 @@ class WorkReportReviewManager {
(Math.random() * 6 + 2); // 정상 시간 (2-8시간) (Math.random() * 6 + 2); // 정상 시간 (2-8시간)
dummyData.push({ dummyData.push({
id: 1000000 + i * 100 + worker.worker_id * 10 + j, // 고유 정수 ID id: 1000000 + i * 100 + worker.user_id * 10 + j, // 고유 정수 ID
report_date: dateStr, report_date: dateStr,
worker_id: worker.worker_id, user_id: worker.user_id,
worker_name: worker.worker_name, worker_name: worker.worker_name,
project_id: this.projects[Math.floor(Math.random() * this.projects.length)]?.project_id || 1, project_id: this.projects[Math.floor(Math.random() * this.projects.length)]?.project_id || 1,
project_name: this.projects[Math.floor(Math.random() * this.projects.length)]?.project_name || 'Unknown', project_name: this.projects[Math.floor(Math.random() * this.projects.length)]?.project_name || 'Unknown',
@@ -344,7 +344,7 @@ class WorkReportReviewManager {
if (workerSelect) { if (workerSelect) {
workerSelect.innerHTML = '<option value="">전체 작업자</option>'; workerSelect.innerHTML = '<option value="">전체 작업자</option>';
this.workers.forEach(worker => { this.workers.forEach(worker => {
workerSelect.innerHTML += `<option value="${worker.worker_id}">${worker.worker_name}</option>`; workerSelect.innerHTML += `<option value="${worker.user_id}">${worker.worker_name}</option>`;
}); });
} }
@@ -540,7 +540,7 @@ class WorkReportReviewManager {
const grouped = {}; const grouped = {};
this.filteredReports.forEach(report => { this.filteredReports.forEach(report => {
const key = `${report.worker_id}_${report.report_date}`; const key = `${report.user_id}_${report.report_date}`;
if (!grouped[key]) { if (!grouped[key]) {
grouped[key] = []; grouped[key] = [];
} }
@@ -569,7 +569,7 @@ class WorkReportReviewManager {
selectWorkerDate(workerId, date) { selectWorkerDate(workerId, date) {
// 해당 작업자의 특정 날짜 보고서들을 선택 // 해당 작업자의 특정 날짜 보고서들을 선택
const reports = this.filteredReports.filter(r => const reports = this.filteredReports.filter(r =>
r.worker_id == workerId && r.report_date === date r.user_id == workerId && r.report_date === date
); );
if (reports.length > 0) { if (reports.length > 0) {
@@ -600,7 +600,7 @@ class WorkReportReviewManager {
// const response = await fetch(`${API}/daily-work-reports/review`, { // const response = await fetch(`${API}/daily-work-reports/review`, {
// method: 'POST', // method: 'POST',
// headers: { ...getAuthHeaders(), 'Content-Type': 'application/json' }, // headers: { ...getAuthHeaders(), 'Content-Type': 'application/json' },
// body: JSON.stringify({ worker_id: workerId, report_date: date, reviewed: true }) // body: JSON.stringify({ user_id: workerId, report_date: date, reviewed: true })
// }); // });
// 현재는 로컬 상태만 업데이트 // 현재는 로컬 상태만 업데이트
@@ -608,7 +608,7 @@ class WorkReportReviewManager {
// 해당 보고서들의 검토 상태 업데이트 // 해당 보고서들의 검토 상태 업데이트
this.reports.forEach(report => { this.reports.forEach(report => {
if (report.worker_id == workerId && report.report_date === date) { if (report.user_id == workerId && report.report_date === date) {
report.is_reviewed = true; report.is_reviewed = true;
} }
}); });
@@ -627,7 +627,7 @@ class WorkReportReviewManager {
} }
getWorkerName(workerId) { getWorkerName(workerId) {
const worker = this.workers.find(w => w.worker_id == workerId); const worker = this.workers.find(w => w.user_id == workerId);
return worker ? worker.worker_name : `작업자${workerId}`; return worker ? worker.worker_name : `작업자${workerId}`;
} }
@@ -680,7 +680,7 @@ class WorkReportReviewManager {
<div class="panel-actions"> <div class="panel-actions">
${!report.is_reviewed && report.hours_status === 'NORMAL' && !allReports.some(r => r.work_status_id === 2) ? ${!report.is_reviewed && report.hours_status === 'NORMAL' && !allReports.some(r => r.work_status_id === 2) ?
`<button type="button" class="panel-btn save" onclick="workReportReview.markAsReviewed('${report.worker_id}', '${report.report_date}')"> `<button type="button" class="panel-btn save" onclick="workReportReview.markAsReviewed('${report.user_id}', '${report.report_date}')">
✅ 검토완료 처리 ✅ 검토완료 처리
</button>` : '' </button>` : ''
} }

View File

@@ -179,7 +179,7 @@ function processDayData(dateStr, works) {
works.forEach(work => { works.forEach(work => {
dayData.totalHours += parseFloat(work.work_hours || 0); dayData.totalHours += parseFloat(work.work_hours || 0);
dayData.workers.add(work.worker_name || work.worker_id); dayData.workers.add(work.worker_name || work.user_id);
}); });
const workType = classifyWorkType(dayData.totalHours); const workType = classifyWorkType(dayData.totalHours);
@@ -217,7 +217,7 @@ function renderDayInfo() {
// 작업자별 상세 정보 생성 // 작업자별 상세 정보 생성
const workerDetailsHtml = Array.from(data.workers).map(worker => { const workerDetailsHtml = Array.from(data.workers).map(worker => {
const workerWorks = data.details.filter(w => (w.worker_name || w.worker_id) === worker); const workerWorks = data.details.filter(w => (w.worker_name || w.user_id) === worker);
const workerHours = workerWorks.reduce((sum, w) => sum + parseFloat(w.work_hours || 0), 0); const workerHours = workerWorks.reduce((sum, w) => sum + parseFloat(w.work_hours || 0), 0);
const workerWorkItemsHtml = workerWorks.map(work => ` const workerWorkItemsHtml = workerWorks.map(work => `
@@ -563,7 +563,7 @@ async function deleteWorkerAllWorks(date, workerName) {
try { try {
if (!selectedDateData) return; if (!selectedDateData) return;
const workerWorks = selectedDateData.details.filter(w => (w.worker_name || w.worker_id) === workerName); const workerWorks = selectedDateData.details.filter(w => (w.worker_name || w.user_id) === workerName);
if (workerWorks.length === 0) { if (workerWorks.length === 0) {
showMessage('삭제할 작업이 없습니다.', 'error'); showMessage('삭제할 작업이 없습니다.', 'error');

View File

@@ -15,7 +15,7 @@ let existingWork = [];
function getUrlParams() { function getUrlParams() {
const urlParams = new URLSearchParams(window.location.search); const urlParams = new URLSearchParams(window.location.search);
return { return {
worker_id: urlParams.get('worker_id'), user_id: urlParams.get('user_id'),
worker_name: decodeURIComponent(urlParams.get('worker_name') || ''), worker_name: decodeURIComponent(urlParams.get('worker_name') || ''),
date: urlParams.get('date') || new Date().toISOString().split('T')[0] date: urlParams.get('date') || new Date().toISOString().split('T')[0]
}; };
@@ -88,7 +88,7 @@ async function initializePage() {
// URL 파라미터 추출 // URL 파라미터 추출
const params = getUrlParams(); const params = getUrlParams();
currentWorkerId = parseInt(params.worker_id); currentWorkerId = parseInt(params.user_id);
currentWorkerName = params.worker_name; currentWorkerName = params.worker_name;
selectedDate = params.date; selectedDate = params.date;
@@ -189,7 +189,7 @@ async function loadWorkerInfo() {
async function loadExistingWork() { async function loadExistingWork() {
try { try {
const response = await window.apiCall(`/daily-work-reports?date=${selectedDate}&worker_id=${currentWorkerId}`); const response = await window.apiCall(`/daily-work-reports?date=${selectedDate}&user_id=${currentWorkerId}`);
existingWork = Array.isArray(response) ? response : (response.data || []); existingWork = Array.isArray(response) ? response : (response.data || []);
console.log(`✅ 기존 작업 ${existingWork.length}건 로드 완료`); console.log(`✅ 기존 작업 ${existingWork.length}건 로드 완료`);
} catch (error) { } catch (error) {
@@ -398,7 +398,7 @@ async function saveNewWork() {
const workData = { const workData = {
report_date: selectedDate, report_date: selectedDate,
worker_id: currentWorkerId, user_id: currentWorkerId,
project_id: parseInt(projectId), project_id: parseInt(projectId),
work_type_id: parseInt(workTypeId), work_type_id: parseInt(workTypeId),
work_status_id: parseInt(workStatusId), work_status_id: parseInt(workStatusId),
@@ -477,7 +477,7 @@ async function handleVacationProcess(vacationType) {
// 휴가용 작업 보고서 생성 // 휴가용 작업 보고서 생성
const vacationWork = { const vacationWork = {
report_date: selectedDate, report_date: selectedDate,
worker_id: currentWorkerId, user_id: currentWorkerId,
project_id: 1, // 기본 프로젝트 (휴가용) project_id: 1, // 기본 프로젝트 (휴가용)
work_type_id: 999, // 휴가 전용 작업 유형 (DB에 추가 필요) work_type_id: 999, // 휴가 전용 작업 유형 (DB에 추가 필요)
work_status_id: 1, // 정상 상태 work_status_id: 1, // 정상 상태

View File

@@ -331,7 +331,7 @@ function renderWorkerList() {
statusText = '사무직'; statusText = '사무직';
} }
const safeWorkerId = parseInt(worker.worker_id) || 0; const safeWorkerId = parseInt(worker.user_id) || 0;
const safeWorkerName = escapeHtml(worker.worker_name || ''); const safeWorkerName = escapeHtml(worker.worker_name || '');
const firstChar = safeWorkerName ? safeWorkerName.charAt(0) : '?'; const firstChar = safeWorkerName ? safeWorkerName.charAt(0) : '?';
@@ -406,7 +406,7 @@ function openWorkerModal(workerId = null) {
} }
if (workerId) { if (workerId) {
const worker = allWorkers.find(w => w.worker_id === workerId); const worker = allWorkers.find(w => w.user_id === workerId);
if (!worker) { if (!worker) {
showToast('작업자를 찾을 수 없습니다.', 'error'); showToast('작업자를 찾을 수 없습니다.', 'error');
return; return;
@@ -416,7 +416,7 @@ function openWorkerModal(workerId = null) {
title.textContent = '작업자 정보 수정'; title.textContent = '작업자 정보 수정';
deleteBtn.style.display = 'inline-flex'; deleteBtn.style.display = 'inline-flex';
document.getElementById('workerId').value = worker.worker_id; document.getElementById('workerId').value = worker.user_id;
document.getElementById('workerName').value = worker.worker_name || ''; document.getElementById('workerName').value = worker.worker_name || '';
document.getElementById('jobType').value = worker.job_type || 'worker'; document.getElementById('jobType').value = worker.job_type || 'worker';
document.getElementById('joinDate').value = worker.join_date ? worker.join_date.split('T')[0] : ''; document.getElementById('joinDate').value = worker.join_date ? worker.join_date.split('T')[0] : '';
@@ -508,7 +508,7 @@ async function saveWorker() {
// 작업자 삭제 확인 // 작업자 삭제 확인
function confirmDeleteWorker(workerId) { function confirmDeleteWorker(workerId) {
const worker = allWorkers.find(w => w.worker_id === workerId); const worker = allWorkers.find(w => w.user_id === workerId);
if (!worker) { if (!worker) {
showToast('작업자를 찾을 수 없습니다.', 'error'); showToast('작업자를 찾을 수 없습니다.', 'error');
return; return;
@@ -524,7 +524,7 @@ function confirmDeleteWorker(workerId) {
// 작업자 삭제 (모달에서) // 작업자 삭제 (모달에서)
function deleteWorker() { function deleteWorker() {
if (currentEditingWorker) { if (currentEditingWorker) {
confirmDeleteWorker(currentEditingWorker.worker_id); confirmDeleteWorker(currentEditingWorker.user_id);
} }
} }

View File

@@ -271,7 +271,7 @@
const select = document.getElementById('workerFilter'); const select = document.getElementById('workerFilter');
workers.forEach(worker => { workers.forEach(worker => {
const option = document.createElement('option'); const option = document.createElement('option');
option.value = worker.worker_id; option.value = worker.user_id;
option.textContent = worker.worker_name; option.textContent = worker.worker_name;
select.appendChild(option); select.appendChild(option);
}); });
@@ -297,7 +297,7 @@
params: { params: {
start_date: startDate, start_date: startDate,
end_date: endDate, end_date: endDate,
worker_id: workerId || undefined user_id: workerId || undefined
} }
}); });
@@ -306,7 +306,7 @@
params: { params: {
start_date: startDate, start_date: startDate,
end_date: endDate, end_date: endDate,
worker_id: workerId || undefined user_id: workerId || undefined
} }
}); });
@@ -340,10 +340,10 @@
// 출퇴근 기록 맵핑 // 출퇴근 기록 맵핑
attendanceRecords.forEach(record => { attendanceRecords.forEach(record => {
const key = `${record.attendance_date}_${record.worker_id}`; const key = `${record.attendance_date}_${record.user_id}`;
dateWorkerMap.set(key, { dateWorkerMap.set(key, {
date: record.attendance_date, date: record.attendance_date,
worker_id: record.worker_id, user_id: record.user_id,
worker_name: record.worker_name, worker_name: record.worker_name,
attendance: record, attendance: record,
reports: [] reports: []
@@ -352,13 +352,13 @@
// 작업 보고서 맵핑 // 작업 보고서 맵핑
workReports.forEach(report => { workReports.forEach(report => {
const key = `${report.report_date}_${report.worker_id}`; const key = `${report.report_date}_${report.user_id}`;
if (dateWorkerMap.has(key)) { if (dateWorkerMap.has(key)) {
dateWorkerMap.get(key).reports.push(report); dateWorkerMap.get(key).reports.push(report);
} else { } else {
dateWorkerMap.set(key, { dateWorkerMap.set(key, {
date: report.report_date, date: report.report_date,
worker_id: report.worker_id, user_id: report.user_id,
worker_name: report.worker_name, worker_name: report.worker_name,
attendance: null, attendance: null,
reports: [report] reports: [report]

View File

@@ -499,7 +499,7 @@
function populateAssigneeDropdown() { function populateAssigneeDropdown() {
const select = document.getElementById('receiveAssignee'); const select = document.getElementById('receiveAssignee');
select.innerHTML = '<option value="">담당자 선택</option>' + select.innerHTML = '<option value="">담당자 선택</option>' +
workers.map(w => `<option value="${w.worker_id}">${w.worker_name} (${w.job_type || ''})</option>`).join(''); workers.map(w => `<option value="${w.user_id}">${w.worker_name} (${w.job_type || ''})</option>`).join('');
} }
async function loadRepairRequests() { async function loadRepairRequests() {

View File

@@ -392,7 +392,7 @@
// 데이터 정리 // 데이터 정리
vacationData = {}; vacationData = {};
workers.forEach(w => { workers.forEach(w => {
vacationData[w.worker_id] = { vacationData[w.user_id] = {
carryover: 0, carryover: 0,
annual: 0, annual: 0,
longService: 0, longService: 0,
@@ -403,9 +403,9 @@
// 잔액 데이터 매핑 // 잔액 데이터 매핑
balances.forEach(b => { balances.forEach(b => {
if (!vacationData[b.worker_id]) return; if (!vacationData[b.user_id]) return;
const code = b.type_code || ''; const code = b.type_code || '';
const data = vacationData[b.worker_id]; const data = vacationData[b.user_id];
if (code === 'CARRYOVER' || b.type_name === '이월') { if (code === 'CARRYOVER' || b.type_name === '이월') {
data.carryover = b.total_days || 0; data.carryover = b.total_days || 0;
@@ -443,7 +443,7 @@
} }
tbody.innerHTML = workers.map((w, idx) => { tbody.innerHTML = workers.map((w, idx) => {
const d = vacationData[w.worker_id] || { carryover: 0, annual: 0, longService: 0, specials: [], totalUsed: 0 }; const d = vacationData[w.user_id] || { carryover: 0, annual: 0, longService: 0, specials: [], totalUsed: 0 };
const carryover = parseFloat(d.carryover) || 0; const carryover = parseFloat(d.carryover) || 0;
const annual = parseFloat(d.annual) || 0; const annual = parseFloat(d.annual) || 0;
const longService = parseFloat(d.longService) || 0; const longService = parseFloat(d.longService) || 0;
@@ -454,29 +454,29 @@
const remainingClass = remaining > 0 ? 'positive' : remaining < 0 ? 'negative' : 'zero'; const remainingClass = remaining > 0 ? 'positive' : remaining < 0 ? 'negative' : 'zero';
return ` return `
<tr data-worker-id="${w.worker_id}"> <tr data-user-id="${w.user_id}">
<td>${idx + 1}</td> <td>${idx + 1}</td>
<td class="worker-name">${w.worker_name}</td> <td class="worker-name">${w.worker_name}</td>
<td> <td>
<input type="number" class="num-input ${carryover < 0 ? 'negative' : ''}" <input type="number" class="num-input ${carryover < 0 ? 'negative' : ''}"
value="${carryover}" step="0.5" value="${carryover}" step="0.5"
data-field="carryover" data-field="carryover"
onchange="updateField(${w.worker_id}, 'carryover', this.value)"> onchange="updateField(${w.user_id}, 'carryover', this.value)">
</td> </td>
<td> <td>
<input type="number" class="num-input" <input type="number" class="num-input"
value="${annual}" step="0.5" value="${annual}" step="0.5"
data-field="annual" data-field="annual"
onchange="updateField(${w.worker_id}, 'annual', this.value)"> onchange="updateField(${w.user_id}, 'annual', this.value)">
</td> </td>
<td> <td>
<input type="number" class="num-input" <input type="number" class="num-input"
value="${longService}" step="0.5" value="${longService}" step="0.5"
data-field="longService" data-field="longService"
onchange="updateField(${w.worker_id}, 'longService', this.value)"> onchange="updateField(${w.user_id}, 'longService', this.value)">
</td> </td>
<td> <td>
<button class="special-btn" onclick="openSpecialModal(${w.worker_id}, '${w.worker_name}')"> <button class="special-btn" onclick="openSpecialModal(${w.user_id}, '${w.worker_name}')">
${(d.specials || []).length > 0 ? `${specialTotal}` : '추가'} ${(d.specials || []).length > 0 ? `${specialTotal}` : '추가'}
${(d.specials || []).length > 0 ? `<span class="special-count">${d.specials.length}</span>` : ''} ${(d.specials || []).length > 0 ? `<span class="special-count">${d.specials.length}</span>` : ''}
</button> </button>
@@ -497,7 +497,7 @@
vacationData[workerId][field] = val; vacationData[workerId][field] = val;
// 입력 스타일 업데이트 // 입력 스타일 업데이트
const input = document.querySelector(`tr[data-worker-id="${workerId}"] input[data-field="${field}"]`); const input = document.querySelector(`tr[data-user-id="${workerId}"] input[data-field="${field}"]`);
if (input) { if (input) {
input.classList.toggle('negative', val < 0); input.classList.toggle('negative', val < 0);
} }
@@ -508,7 +508,7 @@
} }
function updateRowTotals(workerId) { function updateRowTotals(workerId) {
const row = document.querySelector(`tr[data-worker-id="${workerId}"]`); const row = document.querySelector(`tr[data-user-id="${workerId}"]`);
if (!row) return; if (!row) return;
const d = vacationData[workerId]; const d = vacationData[workerId];
@@ -668,12 +668,12 @@
// 데이터 수집 // 데이터 수집
for (const w of workers) { for (const w of workers) {
const d = vacationData[w.worker_id]; const d = vacationData[w.user_id];
if (!d) continue; if (!d) continue;
if (typeIdMap['CARRYOVER']) { if (typeIdMap['CARRYOVER']) {
balancesToSave.push({ balancesToSave.push({
worker_id: w.worker_id, user_id: w.user_id,
vacation_type_id: typeIdMap['CARRYOVER'], vacation_type_id: typeIdMap['CARRYOVER'],
year: currentYear, year: currentYear,
total_days: d.carryover total_days: d.carryover
@@ -681,7 +681,7 @@
} }
if (typeIdMap['ANNUAL']) { if (typeIdMap['ANNUAL']) {
balancesToSave.push({ balancesToSave.push({
worker_id: w.worker_id, user_id: w.user_id,
vacation_type_id: typeIdMap['ANNUAL'], vacation_type_id: typeIdMap['ANNUAL'],
year: currentYear, year: currentYear,
total_days: d.annual total_days: d.annual
@@ -689,7 +689,7 @@
} }
if (typeIdMap['LONG_SERVICE']) { if (typeIdMap['LONG_SERVICE']) {
balancesToSave.push({ balancesToSave.push({
worker_id: w.worker_id, user_id: w.user_id,
vacation_type_id: typeIdMap['LONG_SERVICE'], vacation_type_id: typeIdMap['LONG_SERVICE'],
year: currentYear, year: currentYear,
total_days: d.longService total_days: d.longService
@@ -726,7 +726,7 @@
} }
if (specialTypeId) { if (specialTypeId) {
balancesToSave.push({ balancesToSave.push({
worker_id: w.worker_id, user_id: w.user_id,
vacation_type_id: specialTypeId, vacation_type_id: specialTypeId,
year: currentYear, year: currentYear,
total_days: special.days, total_days: special.days,

View File

@@ -270,18 +270,18 @@
checkinStatus = {}; checkinStatus = {};
workers.forEach(w => { workers.forEach(w => {
const checkin = checkinList.find(c => c.worker_id === w.worker_id); const checkin = checkinList.find(c => c.user_id === w.user_id);
const record = records.find(r => r.worker_id === w.worker_id); const record = records.find(r => r.user_id === w.user_id);
if (checkin?.vacation_status === 'approved' || record?.vacation_type_id) { if (checkin?.vacation_status === 'approved' || record?.vacation_type_id) {
checkinStatus[w.worker_id] = { status: 'vacation', vacationType: checkin?.vacation_type_name || record?.vacation_type_name || '연차' }; checkinStatus[w.user_id] = { status: 'vacation', vacationType: checkin?.vacation_type_name || record?.vacation_type_name || '연차' };
} else if (record && record.is_present === 0) { } else if (record && record.is_present === 0) {
checkinStatus[w.worker_id] = { status: 'absent' }; checkinStatus[w.user_id] = { status: 'absent' };
} else if (record && record.is_present === 1) { } else if (record && record.is_present === 1) {
checkinStatus[w.worker_id] = { status: 'present' }; checkinStatus[w.user_id] = { status: 'present' };
} else { } else {
// 기록이 없으면 기본 출근 // 기록이 없으면 기본 출근
checkinStatus[w.worker_id] = { status: 'present' }; checkinStatus[w.user_id] = { status: 'present' };
} }
}); });
@@ -301,9 +301,9 @@
} }
container.innerHTML = workers.map(w => { container.innerHTML = workers.map(w => {
const s = checkinStatus[w.worker_id] || { status: 'present' }; const s = checkinStatus[w.user_id] || { status: 'present' };
const label = s.status === 'present' ? '출근' : s.status === 'absent' ? '결근' : (s.vacationType || '연차'); const label = s.status === 'present' ? '출근' : s.status === 'absent' ? '결근' : (s.vacationType || '연차');
return `<span class="worker-chip ${s.status}" onclick="toggle(${w.worker_id})"><span class="chip-dot"></span>${w.worker_name} <small style="color:#6b7280">${label}</small></span>`; return `<span class="worker-chip ${s.status}" onclick="toggle(${w.user_id})"><span class="chip-dot"></span>${w.worker_name} <small style="color:#6b7280">${label}</small></span>`;
}).join(''); }).join('');
updateSummary(); updateSummary();
@@ -318,8 +318,8 @@
function setAllPresent() { function setAllPresent() {
workers.forEach(w => { workers.forEach(w => {
if (checkinStatus[w.worker_id]?.status !== 'vacation') { if (checkinStatus[w.user_id]?.status !== 'vacation') {
checkinStatus[w.worker_id] = { status: 'present' }; checkinStatus[w.user_id] = { status: 'present' };
} }
}); });
render(); render();
@@ -327,8 +327,8 @@
function setAllAbsent() { function setAllAbsent() {
workers.forEach(w => { workers.forEach(w => {
if (checkinStatus[w.worker_id]?.status !== 'vacation') { if (checkinStatus[w.user_id]?.status !== 'vacation') {
checkinStatus[w.worker_id] = { status: 'absent' }; checkinStatus[w.user_id] = { status: 'absent' };
} }
}); });
render(); render();
@@ -374,10 +374,10 @@
// 연차가 아닌 작업자들만 체크인 데이터로 전송 // 연차가 아닌 작업자들만 체크인 데이터로 전송
const checkins = workers const checkins = workers
.filter(w => checkinStatus[w.worker_id]?.status !== 'vacation') .filter(w => checkinStatus[w.user_id]?.status !== 'vacation')
.map(w => ({ .map(w => ({
worker_id: w.worker_id, user_id: w.user_id,
is_present: checkinStatus[w.worker_id]?.status === 'present' is_present: checkinStatus[w.user_id]?.status === 'present'
})); }));
try { try {

View File

@@ -177,7 +177,7 @@
// 체크인 목록을 기준으로 출퇴근 기록 생성 (연차 정보 포함) // 체크인 목록을 기준으로 출퇴근 기록 생성 (연차 정보 포함)
attendanceRecords = checkinList.map(worker => { attendanceRecords = checkinList.map(worker => {
const existingRecord = existingRecords.find(r => r.worker_id === worker.worker_id); const existingRecord = existingRecords.find(r => r.user_id === worker.user_id);
const isOnVacation = worker.vacation_status === 'approved'; const isOnVacation = worker.vacation_status === 'approved';
// 기존 기록이 있으면 사용, 없으면 초기화 // 기존 기록이 있으면 사용, 없으면 초기화
@@ -185,7 +185,7 @@
return existingRecord; return existingRecord;
} else { } else {
return { return {
worker_id: worker.worker_id, user_id: worker.user_id,
worker_name: worker.worker_name, worker_name: worker.worker_name,
attendance_date: selectedDate, attendance_date: selectedDate,
total_hours: isOnVacation ? 0 : 8, total_hours: isOnVacation ? 0 : 8,
@@ -209,7 +209,7 @@
function initializeAttendanceRecords() { function initializeAttendanceRecords() {
const selectedDate = document.getElementById('selectedDate').value; const selectedDate = document.getElementById('selectedDate').value;
attendanceRecords = workers.map(worker => ({ attendanceRecords = workers.map(worker => ({
worker_id: worker.worker_id, user_id: worker.user_id,
worker_name: worker.worker_name, worker_name: worker.worker_name,
attendance_date: selectedDate, attendance_date: selectedDate,
total_hours: 8, total_hours: 8,
@@ -351,7 +351,7 @@
// 모든 기록을 API 형식에 맞게 변환 // 모든 기록을 API 형식에 맞게 변환
const recordsToSave = attendanceRecords.map(record => ({ const recordsToSave = attendanceRecords.map(record => ({
worker_id: record.worker_id, user_id: record.user_id,
attendance_date: selectedDate, attendance_date: selectedDate,
total_hours: record.total_hours || 0, total_hours: record.total_hours || 0,
overtime_hours: record.overtime_hours || 0, overtime_hours: record.overtime_hours || 0,
@@ -371,7 +371,7 @@
successCount++; successCount++;
} }
} catch (error) { } catch (error) {
console.error(`작업자 ${data.worker_id} 저장 오류:`, error); console.error(`작업자 ${data.user_id} 저장 오류:`, error);
errorCount++; errorCount++;
} }
} }

View File

@@ -662,7 +662,7 @@
const records = response.data.data; const records = response.data.data;
// 작업자별로 데이터 정리 // 작업자별로 데이터 정리
records.forEach(record => { records.forEach(record => {
const workerId = record.worker_id; const workerId = record.user_id;
// 날짜 형식 정규화 (다양한 형식 처리) // 날짜 형식 정규화 (다양한 형식 처리)
let dateKey = record.record_date; let dateKey = record.record_date;
if (dateKey) { if (dateKey) {
@@ -686,7 +686,7 @@
console.error('근태 데이터 조회 오류:', err); console.error('근태 데이터 조회 오류:', err);
// 작업자별 빈 데이터 초기화 // 작업자별 빈 데이터 초기화
workers.forEach(worker => { workers.forEach(worker => {
attendanceData[worker.worker_id] = {}; attendanceData[worker.user_id] = {};
}); });
} }
@@ -765,7 +765,7 @@
html += `<tbody>`; html += `<tbody>`;
workers.forEach((worker, index) => { workers.forEach((worker, index) => {
const workerRecords = attendanceData[worker.worker_id] || {}; const workerRecords = attendanceData[worker.user_id] || {};
let totalOvertimeHours = 0; let totalOvertimeHours = 0;
// 입사일 파싱 // 입사일 파싱
@@ -954,7 +954,7 @@
let totalNormalDays = 0; let totalNormalDays = 0;
workers.forEach(worker => { workers.forEach(worker => {
const records = attendanceData[worker.worker_id] || {}; const records = attendanceData[worker.user_id] || {};
Object.values(records).forEach(record => { Object.values(records).forEach(record => {
const hours = parseFloat(record.total_work_hours) || 0; const hours = parseFloat(record.total_work_hours) || 0;
totalWorkHours += hours; totalWorkHours += hours;

View File

@@ -306,13 +306,13 @@
document.getElementById('adminControls').classList.add('visible'); document.getElementById('adminControls').classList.add('visible');
await loadWorkers(); await loadWorkers();
} else { } else {
// 일반 사용자: 본인 worker_id 사용 // 일반 사용자: 본인 user_id 사용
if (currentUser?.worker_id) { if (currentUser?.user_id) {
currentWorkerId = currentUser.worker_id; currentWorkerId = currentUser.user_id;
document.getElementById('infoGrid').style.display = 'grid'; document.getElementById('infoGrid').style.display = 'grid';
await loadAllData(); await loadAllData();
} else { } else {
// worker_id가 없는 경우 // user_id가 없는 경우
document.getElementById('noWorkerMessage').style.display = 'block'; document.getElementById('noWorkerMessage').style.display = 'block';
} }
} }
@@ -352,7 +352,7 @@
const select = document.getElementById('workerSelect'); const select = document.getElementById('workerSelect');
workers.forEach(w => { workers.forEach(w => {
const opt = document.createElement('option'); const opt = document.createElement('option');
opt.value = w.worker_id; opt.value = w.user_id;
opt.textContent = w.worker_name; opt.textContent = w.worker_name;
select.appendChild(opt); select.appendChild(opt);
}); });
@@ -369,7 +369,7 @@
} }
currentWorkerId = parseInt(workerId); currentWorkerId = parseInt(workerId);
const worker = workers.find(w => w.worker_id === currentWorkerId); const worker = workers.find(w => w.user_id === currentWorkerId);
document.getElementById('workerNameDisplay').textContent = document.getElementById('workerNameDisplay').textContent =
`${worker?.worker_name || ''}님의 연차 잔여 현황 및 월간 연장근로 시간`; `${worker?.worker_name || ''}님의 연차 잔여 현황 및 월간 연장근로 시간`;
document.getElementById('infoGrid').style.display = 'grid'; document.getElementById('infoGrid').style.display = 'grid';
@@ -486,7 +486,7 @@
try { try {
// 근태 기록에서 연장근로 데이터 조회 // 근태 기록에서 연장근로 데이터 조회
const res = await axios.get(`/attendance/records?start_date=${startDate}&end_date=${endDate}&worker_id=${currentWorkerId}`); const res = await axios.get(`/attendance/records?start_date=${startDate}&end_date=${endDate}&user_id=${currentWorkerId}`);
const records = res.data.data || []; const records = res.data.data || [];
// 8시간 초과분 계산 // 8시간 초과분 계산

View File

@@ -222,7 +222,7 @@
event.preventDefault(); event.preventDefault();
const data = { const data = {
worker_id: parseInt(document.getElementById('inputWorker').value), user_id: parseInt(document.getElementById('inputWorker').value),
vacation_type_id: parseInt(document.getElementById('inputVacationType').value), vacation_type_id: parseInt(document.getElementById('inputVacationType').value),
start_date: document.getElementById('inputStartDate').value, start_date: document.getElementById('inputStartDate').value,
end_date: document.getElementById('inputEndDate').value, end_date: document.getElementById('inputEndDate').value,

View File

@@ -369,7 +369,7 @@
event.preventDefault(); event.preventDefault();
const data = { const data = {
worker_id: parseInt(document.getElementById('inputWorker').value), user_id: parseInt(document.getElementById('inputWorker').value),
vacation_type_id: parseInt(document.getElementById('inputVacationType').value), vacation_type_id: parseInt(document.getElementById('inputVacationType').value),
start_date: document.getElementById('inputStartDate').value, start_date: document.getElementById('inputStartDate').value,
end_date: document.getElementById('inputEndDate').value, end_date: document.getElementById('inputEndDate').value,

View File

@@ -155,7 +155,7 @@
try { try {
const currentUser = getCurrentUser(); const currentUser = getCurrentUser();
if (!currentUser || !currentUser.worker_id) { if (!currentUser || !currentUser.user_id) {
alert('작업자 정보가 없습니다. 관리자에게 문의하세요.'); alert('작업자 정보가 없습니다. 관리자에게 문의하세요.');
return; return;
} }
@@ -179,7 +179,7 @@
const currentUser = getCurrentUser(); const currentUser = getCurrentUser();
try { try {
const response = await axios.get(`/attendance/vacation-balance/${currentUser.worker_id}`); const response = await axios.get(`/attendance/vacation-balance/${currentUser.user_id}`);
if (response.data.success) { if (response.data.success) {
renderVacationBalance(response.data.data); renderVacationBalance(response.data.data);
} }
@@ -222,7 +222,7 @@
const currentUser = getCurrentUser(); const currentUser = getCurrentUser();
const data = { const data = {
worker_id: currentUser.worker_id, user_id: currentUser.user_id,
vacation_type_id: parseInt(document.getElementById('vacationType').value), vacation_type_id: parseInt(document.getElementById('vacationType').value),
start_date: document.getElementById('startDate').value, start_date: document.getElementById('startDate').value,
end_date: document.getElementById('endDate').value, end_date: document.getElementById('endDate').value,
@@ -251,7 +251,7 @@
if (response.data.success) { if (response.data.success) {
// 내 신청만 필터링 // 내 신청만 필터링
const myRequests = response.data.data.filter(req => const myRequests = response.data.data.filter(req =>
req.requested_by === currentUser.user_id || req.worker_id === currentUser.worker_id req.requested_by === currentUser.user_id || req.user_id === currentUser.user_id
); );
renderVacationRequests(myRequests, 'myRequestsList', true, 'delete'); renderVacationRequests(myRequests, 'myRequestsList', true, 'delete');
} }

View File

@@ -329,7 +329,7 @@
workStatus = {}; workStatus = {};
workers.forEach(w => { workers.forEach(w => {
const record = records.find(r => r.worker_id === w.worker_id); const record = records.find(r => r.user_id === w.user_id);
// 입사일 이전인지 확인 // 입사일 이전인지 확인
const joinDate = w.join_date ? w.join_date.split('T')[0] : null; const joinDate = w.join_date ? w.join_date.split('T')[0] : null;
@@ -337,7 +337,7 @@
if (isBeforeJoin) { if (isBeforeJoin) {
// 입사 전 날짜 // 입사 전 날짜
workStatus[w.worker_id] = { workStatus[w.user_id] = {
isPresent: false, isPresent: false,
type: 'not_hired', type: 'not_hired',
hours: 0, hours: 0,
@@ -396,7 +396,7 @@
const typeInfo = attendanceTypes.find(t => t.value === type); const typeInfo = attendanceTypes.find(t => t.value === type);
workStatus[w.worker_id] = { workStatus[w.user_id] = {
isPresent: record.is_present === 1 || typeInfo?.isLeave, isPresent: record.is_present === 1 || typeInfo?.isLeave,
type: type, type: type,
hours: typeInfo !== undefined ? typeInfo.hours : 8, hours: typeInfo !== undefined ? typeInfo.hours : 8,
@@ -406,7 +406,7 @@
}; };
} else { } else {
// 출근 체크 기록이 없는 경우 - 결근 상태 // 출근 체크 기록이 없는 경우 - 결근 상태
workStatus[w.worker_id] = { workStatus[w.user_id] = {
isPresent: false, isPresent: false,
type: 'normal', type: 'normal',
hours: 8, hours: 8,
@@ -435,7 +435,7 @@
} }
tbody.innerHTML = workers.map((w, idx) => { tbody.innerHTML = workers.map((w, idx) => {
const s = workStatus[w.worker_id]; const s = workStatus[w.user_id];
// 미입사 상태 처리 // 미입사 상태 처리
if (s.isNotHired) { if (s.isNotHired) {
@@ -503,7 +503,7 @@
${statusText} ${statusText}
</td> </td>
<td> <td>
<select class="type-select" onchange="updateType(${w.worker_id}, this.value)"> <select class="type-select" onchange="updateType(${w.user_id}, this.value)">
${attendanceTypes.map(t => ` ${attendanceTypes.map(t => `
<option value="${t.value}" ${s.type === t.value ? 'selected' : ''}>${t.label}</option> <option value="${t.value}" ${s.type === t.value ? 'selected' : ''}>${t.label}</option>
`).join('')} `).join('')}
@@ -513,7 +513,7 @@
<td class="hours-cell"> <td class="hours-cell">
${showOvertimeInput ? ` ${showOvertimeInput ? `
<input type="number" class="overtime-input" value="${s.overtimeHours}" min="0" max="8" step="0.5" <input type="number" class="overtime-input" value="${s.overtimeHours}" min="0" max="8" step="0.5"
onchange="updateOvertime(${w.worker_id}, this.value)"> onchange="updateOvertime(${w.user_id}, this.value)">
` : '-'} ` : '-'}
</td> </td>
<td class="hours-cell"><strong>${totalHours}h</strong></td> <td class="hours-cell"><strong>${totalHours}h</strong></td>
@@ -551,9 +551,9 @@
function setAllNormal() { function setAllNormal() {
workers.forEach(w => { workers.forEach(w => {
workStatus[w.worker_id].type = 'normal'; workStatus[w.user_id].type = 'normal';
workStatus[w.worker_id].hours = 8; workStatus[w.user_id].hours = 8;
workStatus[w.worker_id].overtimeHours = 0; workStatus[w.user_id].overtimeHours = 0;
}); });
render(); render();
updateSummary(); updateSummary();
@@ -638,14 +638,14 @@
// 미입사자 제외하고 저장할 데이터 생성 // 미입사자 제외하고 저장할 데이터 생성
const recordsToSave = workers const recordsToSave = workers
.filter(w => !workStatus[w.worker_id]?.isNotHired) .filter(w => !workStatus[w.user_id]?.isNotHired)
.map(w => { .map(w => {
const s = workStatus[w.worker_id]; const s = workStatus[w.user_id];
const totalHours = s.type === 'overtime' ? s.hours + s.overtimeHours : s.hours; const totalHours = s.type === 'overtime' ? s.hours + s.overtimeHours : s.hours;
return { return {
record_date: date, record_date: date,
worker_id: w.worker_id, user_id: w.user_id,
attendance_type_id: typeIdMap[s.type] || 1, attendance_type_id: typeIdMap[s.type] || 1,
vacation_type_id: vacationTypeIdMap[s.type] || null, vacation_type_id: vacationTypeIdMap[s.type] || null,
total_work_hours: totalHours, total_work_hours: totalHours,
@@ -674,8 +674,8 @@
alert(`${ok}명 저장 완료`); alert(`${ok}명 저장 완료`);
isAlreadySaved = true; isAlreadySaved = true;
workers.forEach(w => { workers.forEach(w => {
if (workStatus[w.worker_id]) { if (workStatus[w.user_id]) {
workStatus[w.worker_id].isSaved = true; workStatus[w.user_id].isSaved = true;
} }
}); });
render(); render();