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

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

View File

@@ -28,8 +28,8 @@ const getDailyAttendanceStatus = asyncHandler(async (req, res) => {
* 일일 근태 기록 조회
*/
const getDailyAttendanceRecords = asyncHandler(async (req, res) => {
const { date, worker_id } = req.query;
const data = await attendanceService.getDailyAttendanceRecordsService(date, worker_id);
const { date, user_id } = req.query;
const data = await attendanceService.getDailyAttendanceRecordsService(date, user_id);
res.json({
success: true,
@@ -42,8 +42,8 @@ const getDailyAttendanceRecords = asyncHandler(async (req, res) => {
* 기간별 근태 기록 조회 (월별 조회용)
*/
const getAttendanceRecordsByRange = asyncHandler(async (req, res) => {
const { start_date, end_date, worker_id } = req.query;
const data = await attendanceService.getAttendanceRecordsByRangeService(start_date, end_date, worker_id);
const { start_date, end_date, user_id } = req.query;
const data = await attendanceService.getAttendanceRecordsByRangeService(start_date, end_date, user_id);
res.json({
success: true,
@@ -76,7 +76,7 @@ const upsertAttendanceRecord = asyncHandler(async (req, res) => {
const processVacation = asyncHandler(async (req, res) => {
const vacationData = {
record_date: req.body.date,
worker_id: req.body.worker_id,
user_id: req.body.user_id,
vacation_type_id: req.body.vacation_type,
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 overtimeData = {
record_date: req.body.date,
worker_id: req.body.worker_id,
user_id: req.body.user_id,
overtime_approved: true,
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 { worker_id } = req.params;
const data = await attendanceService.getWorkerVacationBalanceService(parseInt(worker_id));
const { user_id } = req.params;
const data = await attendanceService.getWorkerVacationBalanceService(parseInt(user_id));
res.json({
success: true,
@@ -154,11 +154,11 @@ const getWorkerVacationBalance = 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(
parseInt(year),
parseInt(month),
worker_id ? parseInt(worker_id) : null
user_id ? parseInt(user_id) : null
);
res.json({
@@ -186,7 +186,7 @@ const getCheckinList = 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);
res.json({

View File

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

View File

@@ -36,19 +36,19 @@ const createDailyWorkReport = 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) {
return res.status(400).json({ error: 'date와 worker_id가 필요합니다.' });
if (!date || !user_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 result = {
date,
worker_id,
user_id,
contributors: data,
total_contributors: data.length,
grand_total_hours: totalHours
@@ -61,13 +61,13 @@ const getContributorsSummary = asyncHandler(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;
if (!date || !worker_id) {
if (!date || !user_id) {
return res.status(400).json({
error: 'date와 worker_id가 필요합니다.',
example: 'date=2024-06-16&worker_id=1'
error: 'date와 user_id가 필요합니다.',
example: 'date=2024-06-16&user_id=1'
});
}
@@ -78,11 +78,11 @@ const getMyAccumulatedData = async (req, res) => {
}
try {
const data = await dailyWorkReportModel.getMyAccumulatedHours(date, worker_id, created_by);
const data = await dailyWorkReportModel.getMyAccumulatedHours(date, user_id, created_by);
res.json({
date,
worker_id,
user_id,
created_by,
my_data: data,
timestamp: new Date().toISOString()
@@ -187,14 +187,14 @@ const getDailyWorkReportsByDate = 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;
if (!start_date || !end_date) {
return res.status(400).json({
error: 'start_date와 end_date가 필요합니다.',
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 = {
start_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,
work_status_id: work_status_id ? parseInt(work_status_id) : null,
created_by,
@@ -377,7 +377,7 @@ const removeDailyWorkReport = 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 access_level = req.user?.access_level || req.user?.role;
@@ -397,20 +397,20 @@ const removeDailyWorkReportByDateAndWorker = async (req, res) => {
}
try {
const affectedRows = await dailyWorkReportModel.removeByDateAndWorker(date, worker_id, deleted_by);
const affectedRows = await dailyWorkReportModel.removeByDateAndWorker(date, user_id, deleted_by);
if (affectedRows === 0) {
return res.status(404).json({
error: '삭제할 작업보고서를 찾을 수 없습니다.',
date: date,
worker_id: worker_id
user_id: user_id
});
}
res.json({
message: `${date} 날짜의 작업자 ${worker_id} 작업보고서 ${affectedRows}개가 삭제되었습니다.`,
message: `${date} 날짜의 작업자 ${user_id} 작업보고서 ${affectedRows}개가 삭제되었습니다.`,
date,
worker_id,
user_id,
affected_rows: affectedRows,
deleted_by,
timestamp: new Date().toISOString()
@@ -642,21 +642,21 @@ const deleteErrorType = asyncHandler(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({
error: 'date와 worker_id가 필요합니다.',
example: 'date=2024-06-16&worker_id=1'
error: 'date와 user_id가 필요합니다.',
example: 'date=2024-06-16&user_id=1'
});
}
try {
const data = await dailyWorkReportModel.getAccumulatedReportsByDate(date, worker_id);
const data = await dailyWorkReportModel.getAccumulatedReportsByDate(date, user_id);
res.json({
date,
worker_id,
user_id,
total_entries: data.length,
accumulated_data: data,
timestamp: new Date().toISOString()
@@ -678,7 +678,7 @@ const createFromTbm = async (req, res) => {
const {
tbm_assignment_id,
tbm_session_id,
worker_id,
user_id,
project_id,
work_type_id,
report_date,
@@ -691,10 +691,10 @@ const createFromTbm = async (req, res) => {
} = 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({
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 = {
tbm_assignment_id,
tbm_session_id,
worker_id,
user_id,
project_id,
work_type_id,
report_date,

View File

@@ -101,7 +101,7 @@ const getDailyWorkerDetails = asyncHandler(async (req, res) => {
// 데이터 변환
const formattedData = workerDetails.map(worker => ({
workerId: worker.worker_id,
userId: worker.user_id,
workerName: worker.worker_name,
jobType: worker.job_type,
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 work_types wt ON t.work_type_id = wt.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 = ?
ORDER BY ts.created_at DESC
`, [categoryId, targetDate]);
@@ -433,7 +433,7 @@ const PatrolController = {
SELECT tta.assignment_id, w.worker_name, w.occupation,
tta.attendance_status, tta.signature_image
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 = ?
ORDER BY w.worker_name
`, [session.session_id]);

View File

@@ -10,7 +10,7 @@ const TbmController = {
try {
const sessionData = {
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,
work_location: req.body.work_location || null,
work_description: req.body.work_description || null,
@@ -135,7 +135,7 @@ const TbmController = {
try {
const assignmentData = {
session_id: req.params.sessionId,
worker_id: req.body.worker_id,
user_id: req.body.user_id,
assigned_role: req.body.assigned_role || null,
work_detail: req.body.work_detail || null,
is_present: req.body.is_present,
@@ -148,7 +148,7 @@ const TbmController = {
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가 필요합니다.' });
}
@@ -164,7 +164,7 @@ const TbmController = {
try {
const assignmentData = {
session_id: req.params.sessionId,
worker_id: req.body.worker_id,
user_id: req.body.user_id,
work_hours: req.body.work_hours,
project_id: req.body.project_id || null,
work_type_id: req.body.work_type_id || null,
@@ -173,7 +173,7 @@ const TbmController = {
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와 작업시간이 필요합니다.' });
}
@@ -218,8 +218,8 @@ const TbmController = {
removeTeamMember: async (req, res) => {
try {
const { sessionId, workerId } = req.params;
const result = await TbmModel.removeTeamMember(sessionId, workerId);
const { sessionId, userId } = req.params;
const result = await TbmModel.removeTeamMember(sessionId, userId);
if (result.affectedRows === 0) {
return res.status(404).json({ success: false, message: '팀원을 찾을 수 없습니다.' });
@@ -432,17 +432,17 @@ const TbmController = {
try {
const handoverData = {
session_id: req.body.session_id,
from_leader_id: req.body.from_leader_id,
to_leader_id: req.body.to_leader_id,
from_leader_user_id: req.body.from_leader_user_id,
to_leader_user_id: req.body.to_leader_user_id,
handover_date: req.body.handover_date,
handover_time: req.body.handover_time || null,
reason: req.body.reason,
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 ||
!handoverData.to_leader_id || !handoverData.handover_date || !handoverData.reason) {
if (!handoverData.session_id || !handoverData.from_leader_user_id ||
!handoverData.to_leader_user_id || !handoverData.handover_date || !handoverData.reason) {
return res.status(400).json({ success: false, message: '필수 정보가 누락되었습니다.' });
}
@@ -483,7 +483,7 @@ const TbmController = {
getMyPendingHandovers: async (req, res) => {
try {
const toLeaderId = req.user.worker_id;
const toLeaderId = req.user.user_id;
if (!toLeaderId) {
return res.status(400).json({ success: false, message: '작업자 정보를 찾을 수 없습니다.' });
}
@@ -532,10 +532,10 @@ const TbmController = {
createTransfer: async (req, res) => {
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;
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: '필수 정보가 누락되었습니다.' });
}
@@ -545,7 +545,7 @@ const TbmController = {
String(today.getDate()).padStart(2, '0');
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,
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 = {
/**
* 특정 작업자의 휴가 잔액 조회 (특정 연도)
* GET /api/vacation-balances/worker/:workerId/year/:year
* GET /api/vacation-balances/user/:userId/year/:year
*/
async getByWorkerAndYear(req, res) {
try {
const { workerId, year } = req.params;
const results = await vacationBalanceModel.getByWorkerAndYear(workerId, year);
const { userId, year } = req.params;
const results = await vacationBalanceModel.getByWorkerAndYear(userId, year);
res.json({ success: true, data: results });
} catch (error) {
logger.error('휴가 잔액 조회 오류:', error);
@@ -44,18 +44,18 @@ const vacationBalanceController = {
*/
async createBalance(req, res) {
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;
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({
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) {
return res.status(400).json({
success: false,
@@ -64,7 +64,7 @@ const vacationBalanceController = {
}
const balanceData = {
worker_id,
user_id,
vacation_type_id,
year,
total_days,
@@ -142,13 +142,13 @@ const vacationBalanceController = {
*/
async autoCalculateAndCreate(req, res) {
try {
const { worker_id, hire_date, year } = req.body;
const { user_id, hire_date, year } = req.body;
const created_by = req.user.user_id;
if (!worker_id || !hire_date || !year) {
if (!user_id || !hire_date || !year) {
return res.status(400).json({
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 existing = await vacationBalanceModel.getByWorkerTypeYear(worker_id, annualTypeId, year);
const existing = await vacationBalanceModel.getByWorkerTypeYear(user_id, annualTypeId, year);
if (existing && existing.length > 0) {
return res.status(400).json({
success: false,
@@ -172,7 +172,7 @@ const vacationBalanceController = {
}
const balanceData = {
worker_id,
user_id,
vacation_type_id: annualTypeId,
year,
total_days: annualDays,
@@ -213,9 +213,9 @@ const vacationBalanceController = {
let errorCount = 0;
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++;
continue;
}
@@ -223,7 +223,7 @@ const vacationBalanceController = {
try {
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 (?, ?, ?, ?, 0, ?, ?)
ON DUPLICATE KEY UPDATE
total_days = VALUES(total_days),
@@ -231,7 +231,7 @@ const vacationBalanceController = {
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++;
} catch (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) {
try {
const { workerId, year } = req.params;
const results = await vacationBalanceModel.getAvailableVacationDays(workerId, year);
const { userId, year } = req.params;
const results = await vacationBalanceModel.getAvailableVacationDays(userId, year);
res.json({ success: true, data: results });
} catch (error) {
logger.error('사용 가능 휴가 조회 오류:', error);

View File

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

View File

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

View File

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

View File

@@ -103,7 +103,7 @@ exports.getAllWorkers = 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)) {
throw new ValidationError('유효하지 않은 작업자 ID입니다');
@@ -126,7 +126,7 @@ exports.getWorkerById = 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)) {
throw new ValidationError('유효하지 않은 작업자 ID입니다');
@@ -252,7 +252,7 @@ exports.updateWorker = 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)) {
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 미들웨어가 먼저 실행되어야 합니다.
*
* @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.adminRoles - 관리자로 인정할 역할들 (기본값: ['admin', 'system'])
* @returns {Function} Express 미들웨어 함수
@@ -242,9 +242,9 @@ const requireMinLevel = (minLevel) => {
* resourceField: 'params.user_id'
* }), updateUser);
*
* // 요청 body의 worker_id로 체크, support_team도 관리자로 인정
* // 요청 body의 user_id로 체크, support_team도 관리자로 인정
* router.delete('/reports/:id', requireAuth, requireOwnerOrAdmin({
* resourceField: 'body.worker_id',
* resourceField: 'body.user_id',
* adminRoles: ['admin', 'system', 'support_team']
* }), deleteReport);
*/

View File

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

View File

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

View File

@@ -2,7 +2,7 @@ const { getDb } = require('../dbPool');
class AttendanceModel {
// 일일 근태 기록 조회
static async getDailyAttendanceRecords(date, workerId = null) {
static async getDailyAttendanceRecords(date, userId = null) {
const db = await getDb();
let query = `
SELECT
@@ -15,7 +15,7 @@ class AttendanceModel {
vt.type_code as vacation_type_code,
vt.deduct_days as vacation_days
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 vacation_types vt ON dar.vacation_type_id = vt.id
WHERE dar.record_date = ?
@@ -23,9 +23,9 @@ class AttendanceModel {
const params = [date];
if (workerId) {
query += ' AND dar.worker_id = ?';
params.push(workerId);
if (userId) {
query += ' AND dar.user_id = ?';
params.push(userId);
}
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();
let query = `
SELECT
@@ -48,7 +48,7 @@ class AttendanceModel {
vt.type_code as vacation_type_code,
vt.deduct_days as vacation_days
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 vacation_types vt ON dar.vacation_type_id = vt.id
WHERE dar.record_date BETWEEN ? AND ?
@@ -56,9 +56,9 @@ class AttendanceModel {
const params = [startDate, endDate];
if (workerId) {
query += ' AND dar.worker_id = ?';
params.push(workerId);
if (userId) {
query += ' AND dar.user_id = ?';
params.push(userId);
}
query += ' ORDER BY dar.record_date ASC';
@@ -68,17 +68,17 @@ class AttendanceModel {
}
// 작업 보고서와 근태 기록 동기화 (시간 합산 및 상태 업데이트)
static async syncWithWorkReports(workerId, date) {
static async syncWithWorkReports(userId, date) {
const db = await getDb();
// 1. 해당 날짜의 총 작업 시간 계산
const [reportStats] = await db.execute(`
SELECT
SELECT
COALESCE(SUM(work_hours), 0) as total_hours,
COUNT(*) as report_count
FROM daily_work_reports
WHERE worker_id = ? AND report_date = ?
`, [workerId, date]);
WHERE user_id = ? AND report_date = ?
`, [userId, date]);
const totalHours = parseFloat(reportStats[0].total_hours || 0);
const reportCount = reportStats[0].report_count;
@@ -110,8 +110,8 @@ class AttendanceModel {
// 3. 기록 업데이트 (휴가 정보는 유지)
// 기존 기록 조회
const [existing] = await db.execute(
'SELECT id, vacation_type_id FROM daily_attendance_records WHERE worker_id = ? AND record_date = ?',
[workerId, date]
'SELECT id, vacation_type_id FROM daily_attendance_records WHERE user_id = ? AND record_date = ?',
[userId, date]
);
if (existing.length > 0) {
@@ -138,9 +138,9 @@ class AttendanceModel {
// 생성자가 명확하지 않으므로 시스템(1) 또는 알 수 없음 처리
await db.execute(`
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)
`, [date, workerId, totalHours, typeId, status]);
`, [date, userId, totalHours, typeId, status]);
return { synced: true, totalHours, status, created: true };
}
@@ -152,14 +152,14 @@ class AttendanceModel {
// 1. 활성 작업자 조회
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 };
// 2. 일일 근태 레코드 일괄 생성 (이미 존재하면 무시)
// 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
// Note: mysql2 execute doesn't support nested arrays for bulk insert easily with placeholder ?
@@ -175,10 +175,10 @@ class AttendanceModel {
for (const w of workers) {
const [result] = await conn.execute(`
INSERT IGNORE INTO daily_attendance_records
(record_date, worker_id, status, created_by)
INSERT IGNORE INTO daily_attendance_records
(record_date, user_id, status, created_by)
VALUES (?, ?, 'incomplete', ?)
`, [date, w.worker_id, createdBy]);
`, [date, w.user_id, createdBy]);
insertedCount += result.affectedRows;
}
@@ -200,7 +200,7 @@ class AttendanceModel {
const {
record_date,
worker_id,
user_id,
total_work_hours = 8,
work_attendance_type_id = 1,
vacation_type_id = null,
@@ -212,8 +212,8 @@ class AttendanceModel {
// 기존 기록 확인
const [existing] = await db.execute(
'SELECT id FROM daily_attendance_records WHERE worker_id = ? AND record_date = ?',
[worker_id, record_date]
'SELECT id FROM daily_attendance_records WHERE user_id = ? AND record_date = ?',
[user_id, record_date]
);
if (existing.length > 0) {
@@ -240,12 +240,12 @@ class AttendanceModel {
// 생성
const [result] = await db.execute(`
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
) VALUES (?, ?, ?, ?, ?, ?, ?)
`, [
record_date,
worker_id,
user_id,
total_work_hours,
attendance_type_id,
vacation_type_id,
@@ -263,8 +263,8 @@ class AttendanceModel {
// 모든 작업자와 해당 날짜의 근태 기록을 조회
const [rows] = await db.execute(`
SELECT
w.worker_id,
SELECT
w.user_id,
w.worker_name,
w.job_type,
COALESCE(dar.total_work_hours, 0) as total_work_hours,
@@ -277,16 +277,16 @@ class AttendanceModel {
vt.type_code as vacation_type_code,
dar.notes,
-- 작업 건수 계산
(SELECT COUNT(*) FROM daily_work_reports dwr
WHERE dwr.worker_id = w.worker_id AND dwr.report_date = ?) as work_count,
(SELECT COUNT(*) FROM daily_work_reports dwr
WHERE dwr.user_id = w.user_id AND dwr.report_date = ?) as work_count,
-- 오류 건수 계산
(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
(SELECT COUNT(*) FROM daily_work_reports dwr
WHERE dwr.user_id = w.user_id AND dwr.report_date = ? AND dwr.work_status_id = 2) as error_count
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 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
`, [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();
// 휴가 유형 정보 조회
@@ -312,9 +312,9 @@ class AttendanceModel {
// 현재 작업 시간 조회
const [workHours] = await db.execute(`
SELECT COALESCE(SUM(work_hours), 0) as total_hours
FROM daily_work_reports
WHERE worker_id = ? AND report_date = ?
`, [workerId, date]);
FROM daily_work_reports
WHERE user_id = ? AND report_date = ?
`, [userId, date]);
const currentHours = parseFloat(workHours[0].total_hours);
// deduct_days를 시간으로 변환 (1일 = 8시간)
@@ -338,12 +338,12 @@ class AttendanceModel {
// 휴가 작업 기록 생성 (프로젝트 ID 13 = "연차/휴무", work_type_id 1 = 기본)
await db.execute(`
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
) VALUES (?, ?, 13, 1, 1, ?, ?, ?)
`, [
date,
workerId,
userId,
vacationHours,
`${vacationTypeInfo.type_name} 처리`,
createdBy
@@ -352,7 +352,7 @@ class AttendanceModel {
// 근태 기록 업데이트
const attendanceData = {
record_date: date,
worker_id: workerId,
user_id: userId,
total_work_hours: totalHours,
work_attendance_type_id: attendanceTypes[0]?.id,
vacation_type_id: vacationTypeInfo.id,
@@ -364,19 +364,19 @@ class AttendanceModel {
}
// 초과근무 승인
static async approveOvertime(workerId, date, approvedBy) {
static async approveOvertime(userId, date, approvedBy) {
const db = await getDb();
const [result] = await db.execute(`
UPDATE daily_attendance_records
SET
UPDATE daily_attendance_records
SET
overtime_approved = TRUE,
overtime_approved_by = ?,
overtime_approved_at = CURRENT_TIMESTAMP,
updated_by = ?,
updated_at = CURRENT_TIMESTAMP
WHERE worker_id = ? AND record_date = ?
`, [approvedBy, approvedBy, workerId, date]);
WHERE user_id = ? AND record_date = ?
`, [approvedBy, approvedBy, userId, date]);
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 currentYear = year || new Date().getFullYear();
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
WHERE worker_id = ? AND year = ?
`, [workerId, currentYear]);
SELECT id, user_id, year, total_annual_leave, used_annual_leave, notes, created_at, updated_at FROM worker_vacation_balance
WHERE user_id = ? AND year = ?
`, [userId, currentYear]);
if (rows.length === 0) {
// 기본 연차 생성 (15일)
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)
`, [workerId, currentYear]);
`, [userId, currentYear]);
return {
worker_id: workerId,
user_id: userId,
year: currentYear,
total_annual_leave: 15.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();
// 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(경조사)
let query = `
SELECT
w.worker_id,
w.user_id,
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.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(AVG(dar.total_work_hours), 0) as avg_work_hours
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) = ?
WHERE w.employment_status = 'employed'
`;
const params = [year, month];
if (workerId) {
query += ' AND w.worker_id = ?';
params.push(workerId);
if (userId) {
query += ' AND w.user_id = ?';
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);
return rows;
@@ -467,12 +467,12 @@ class AttendanceModel {
// 출근 체크 기록 생성 또는 업데이트
static async upsertCheckin(checkinData) {
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(
'SELECT id FROM daily_attendance_records WHERE worker_id = ? AND record_date = ?',
[worker_id, record_date]
'SELECT id FROM daily_attendance_records WHERE user_id = ? AND record_date = ?',
[user_id, record_date]
);
if (existing.length > 0) {
@@ -486,9 +486,9 @@ class AttendanceModel {
// 새로 생성 (기본값으로)
const [result] = await db.execute(
`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)`,
[worker_id, record_date, is_present]
[user_id, record_date, is_present]
);
return result.insertId;
}
@@ -499,7 +499,7 @@ class AttendanceModel {
const db = await getDb();
const query = `
SELECT
w.worker_id,
w.user_id,
w.worker_name,
w.job_type,
w.employment_status,
@@ -512,9 +512,9 @@ class AttendanceModel {
vr.days_used as vacation_days
FROM workers w
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
ON w.worker_id = vr.worker_id
ON w.user_id = vr.user_id
AND ? BETWEEN vr.start_date AND vr.end_date
AND vr.status = 'approved'
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 sql = `
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 (?, ?, ?, ?, ?, ?)
`;
for (const report of reports) {
const { date, worker_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 { date, user_id, project_id, start_time, end_time, issue_type_id } = report;
const [result] = await conn.query(sql, [date, user_id, project_id, start_time, end_time, issue_type_id]);
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,
t.category, t.subcategory, d.description
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 IssueTypes t ON d.issue_type_id = t.issue_type_id
WHERE d.date = ?
@@ -58,7 +58,7 @@ const getAllByDate = async (date) => {
*/
const getById = async (id) => {
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];
};

View File

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

View File

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

View File

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

View File

@@ -139,13 +139,13 @@ const PageAccessModel = {
u.name,
u.role_id,
r.name as role_name,
u.worker_id,
u.user_id as worker_user_id,
w.worker_name,
w.job_type,
COUNT(upa.page_id) as granted_pages_count
FROM users u
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
WHERE u.is_active = 1
AND u.role_id IN (4, 5)

View File

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

View File

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

View File

@@ -9,7 +9,7 @@ const vacationBalanceModel = {
/**
* 특정 작업자의 모든 휴가 잔액 조회 (특정 연도)
*/
async getByWorkerAndYear(workerId, year) {
async getByWorkerAndYear(userId, year) {
const db = await getDb();
const [rows] = await db.query(`
SELECT
@@ -20,16 +20,16 @@ const vacationBalanceModel = {
vt.is_special
FROM vacation_balance_details vbd
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
`, [workerId, year]);
`, [userId, year]);
return rows;
},
/**
* 특정 작업자의 특정 휴가 유형 잔액 조회
*/
async getByWorkerTypeYear(workerId, vacationTypeId, year) {
async getByWorkerTypeYear(userId, vacationTypeId, year) {
const db = await getDb();
const [rows] = await db.query(`
SELECT
@@ -38,10 +38,10 @@ const vacationBalanceModel = {
vt.type_code
FROM vacation_balance_details vbd
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.year = ?
`, [workerId, vacationTypeId, year]);
`, [userId, vacationTypeId, year]);
return rows;
},
@@ -59,7 +59,7 @@ const vacationBalanceModel = {
vt.type_code,
vt.priority
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
WHERE vbd.year = ?
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 [result] = await db.query(`
UPDATE vacation_balance_details
SET used_days = used_days + ?,
updated_at = NOW()
WHERE worker_id = ?
WHERE user_id = ?
AND vacation_type_id = ?
AND year = ?
`, [daysToDeduct, workerId, vacationTypeId, year]);
`, [daysToDeduct, userId, vacationTypeId, year]);
return result;
},
/**
* 작업자의 휴가 사용 일수 복구 (취소)
*/
async restoreDays(workerId, vacationTypeId, year, daysToRestore) {
async restoreDays(userId, vacationTypeId, year, daysToRestore) {
const db = await getDb();
const [result] = await db.query(`
UPDATE vacation_balance_details
SET used_days = GREATEST(0, used_days - ?),
updated_at = NOW()
WHERE worker_id = ?
WHERE user_id = ?
AND vacation_type_id = ?
AND year = ?
`, [daysToRestore, workerId, vacationTypeId, year]);
`, [daysToRestore, userId, vacationTypeId, year]);
return result;
},
/**
* 특정 작업자의 사용 가능한 휴가 일수 확인
*/
async getAvailableVacationDays(workerId, year) {
async getAvailableVacationDays(userId, year) {
const db = await getDb();
const [rows] = await db.query(`
SELECT
@@ -144,11 +144,11 @@ const vacationBalanceModel = {
vbd.remaining_days
FROM vacation_balance_details vbd
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.remaining_days > 0
ORDER BY vt.priority ASC
`, [workerId, year]);
`, [userId, year]);
return rows;
},
@@ -162,11 +162,11 @@ const vacationBalanceModel = {
const db = await getDb();
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 ?`;
const values = balances.map(b => [
b.worker_id,
b.user_id,
b.vacation_type_id,
b.year,
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 [balances] = await db.query(`
@@ -211,13 +211,13 @@ const vacationBalanceModel = {
vt.type_code, vt.type_name, vt.priority
FROM vacation_balance_details vbd
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
ORDER BY vt.priority ASC
`, [workerId, year]);
`, [userId, year]);
if (balances.length === 0) {
console.warn(`[VacationBalance] 작업자 ${workerId}${year}년 휴가 잔액이 없습니다`);
console.warn(`[VacationBalance] 작업자 ${userId}${year}년 휴가 잔액이 없습니다`);
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 };
},
/**
* 휴가 취소 시 우선순위 역순으로 복구
*/
async restoreByPriority(workerId, year, daysToRestore) {
async restoreByPriority(userId, year, daysToRestore) {
const db = await getDb();
const [balances] = await db.query(`
@@ -263,10 +263,10 @@ const vacationBalanceModel = {
vt.type_code, vt.type_name, vt.priority
FROM vacation_balance_details vbd
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
ORDER BY vt.priority DESC
`, [workerId, year]);
`, [userId, year]);
let remaining = daysToRestore;
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 };
},
@@ -311,7 +311,7 @@ const vacationBalanceModel = {
vt.type_name,
vt.type_code
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
WHERE vbd.id = ?
`, [id]);

View File

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

View File

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

View File

@@ -39,39 +39,60 @@ const getAll = async () => {
const [rows] = await db.query(`
SELECT
w.*,
w.user_id,
CASE WHEN w.status = 'active' THEN 1 ELSE 0 END AS is_active,
u.user_id,
su.username,
d.department_name
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
ORDER BY w.worker_id DESC
ORDER BY w.worker_name ASC
`);
return rows;
};
// 3. 단일 조회
// 3. 단일 조회 (worker_id 기준 - 하위 호환성 유지)
const getById = async (worker_id) => {
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,
u.user_id,
su.username,
d.department_name
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
WHERE w.worker_id = ?
`, [worker_id]);
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 db = await getDb();
const {
worker_id,
user_id,
worker_name,
job_type,
status,
@@ -123,12 +144,22 @@ const update = async (worker) => {
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 파라미터:', values);
console.log('실행할 SQL:', query);
console.log('SQL 파라미터:', values);
const [result] = await db.query(query, values);
@@ -192,6 +223,7 @@ module.exports = {
create,
getAll,
getById,
getByUserId,
update,
remove
};

View File

@@ -539,7 +539,7 @@
charts.workerStats = new Chart(ctx, {
type: 'bar',
data: {
labels: data.map(item => `작업자 ${item.worker_id}`),
labels: data.map(item => `작업자 ${item.user_id}`),
datasets: [{
label: '총 작업시간',
data: data.map(item => item.totalHours),
@@ -666,7 +666,7 @@
${data.map(item => `
<tr>
<td>${item.report_date}</td>
<td>작업자 ${item.worker_id}</td>
<td>작업자 ${item.user_id}</td>
<td>프로젝트 ${item.project_id}</td>
<td>유형 ${item.work_type_id}</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-balance/:worker_id', AttendanceController.getWorkerVacationBalance);
router.get('/vacation-balance/:user_id', AttendanceController.getWorkerVacationBalance);
// 월별 근태 통계
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.get('/accumulated', dailyWorkReportController.getAccumulatedReports); // ?date=2024-06-16&worker_id=1
router.get('/contributors', dailyWorkReportController.getContributorsSummary); // ?date=2024-06-16&worker_id=1
router.get('/my-data', dailyWorkReportController.getMyAccumulatedData); // ?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&user_id=1
router.get('/my-data', dailyWorkReportController.getMyAccumulatedData); // ?date=2024-06-16&user_id=1
// ✅ check-overwrite 엔드포인트 추가 (누락된 엔드포인트)
router.get('/check-overwrite', (req, res) => {
const { date, worker_id } = req.query;
if (!date || !worker_id) {
return res.status(400).json({
error: 'date와 worker_id가 필요합니다.',
example: 'date=2025-06-16&worker_id=1'
const { date, user_id } = req.query;
if (!date || !user_id) {
return res.status(400).json({
error: 'date와 user_id가 필요합니다.',
example: 'date=2025-06-16&user_id=1'
});
}
console.log(`🔍 덮어쓰기 권한 확인: 날짜=${date}, 작업자=${worker_id} (누적입력모드)`);
console.log(`🔍 덮어쓰기 권한 확인: 날짜=${date}, 작업자=${user_id} (누적입력모드)`);
// 누적입력 시스템에서는 항상 덮어쓰기 가능 (실제로는 누적만 함)
res.json({
@@ -49,7 +49,7 @@ router.get('/check-overwrite', (req, res) => {
reason: 'accumulate_mode',
message: '누적입력 모드에서는 항상 추가 가능합니다.',
date,
worker_id,
user_id,
timestamp: new Date().toISOString()
});
});
@@ -84,7 +84,7 @@ router.get('/', dailyWorkReportController.getDailyWorkReports);
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);

View File

@@ -231,7 +231,7 @@ router.post('/migrations/fix-work-type-id', async (req, res) => {
dwr.report_date
FROM daily_work_reports dwr
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
AND ta.task_id IS NOT NULL
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
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 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
ORDER BY dwr.report_date DESC
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/: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 startDate = `${year}-${String(month).padStart(2, '0')}-01`;
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 });
} catch (error) {
res.status(500).json({ success: false, error: error.message });
@@ -71,7 +71,7 @@ router.get('/me/vacation-balance', async (req, res) => {
try {
const AttendanceModel = require('../models/attendanceModel');
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 });
} catch (error) {
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 db = require('../config/database');
const reports = await db.query(
'SELECT * FROM daily_work_reports WHERE worker_id = ? AND report_date BETWEEN ? AND ? ORDER BY report_date DESC',
[req.user.worker_id, startDate, endDate]
'SELECT * FROM daily_work_reports WHERE user_id = ? AND report_date BETWEEN ? AND ? ORDER BY report_date DESC',
[req.user.user_id, startDate, endDate]
);
res.json({ success: true, data: reports });
} catch (error) {
@@ -104,8 +104,8 @@ router.get('/me/monthly-stats', async (req, res) => {
SUM(total_work_hours) as month_hours,
COUNT(DISTINCT record_date) as work_days
FROM daily_attendance_records
WHERE worker_id = ? AND YEAR(record_date) = ? AND MONTH(record_date) = ?`,
[req.user.worker_id, year, month]
WHERE user_id = ? AND YEAR(record_date) = ? AND MONTH(record_date) = ?`,
[req.user.user_id, year, month]
);
res.json({ success: true, data: stats[0] || { month_hours: 0, work_days: 0 } });
} catch (error) {

View File

@@ -11,10 +11,10 @@ const vacationBalanceController = require('../controllers/vacationBalanceControl
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);

View File

@@ -96,7 +96,7 @@ router.get('/', workerController.getAllWorkers);
/**
* @swagger
* /api/workers/{worker_id}:
* /api/workers/{user_id}:
* get:
* tags: [Workers]
* summary: 특정 작업자 조회
@@ -105,7 +105,7 @@ router.get('/', workerController.getAllWorkers);
* - bearerAuth: []
* parameters:
* - in: path
* name: worker_id
* name: user_id
* required: true
* schema:
* type: integer
@@ -142,7 +142,7 @@ router.get('/', workerController.getAllWorkers);
* - bearerAuth: []
* parameters:
* - in: path
* name: worker_id
* name: user_id
* required: true
* schema:
* type: integer
@@ -193,7 +193,7 @@ router.get('/', workerController.getAllWorkers);
* - bearerAuth: []
* parameters:
* - in: path
* name: worker_id
* name: user_id
* required: true
* schema:
* type: integer
@@ -221,8 +221,8 @@ router.get('/', workerController.getAllWorkers);
* 500:
* description: 서버 오류
*/
router.get('/:worker_id', workerController.getWorkerById);
router.put('/:worker_id', workerController.updateWorker);
router.delete('/:worker_id', workerController.removeWorker);
router.get('/:user_id', workerController.getWorkerById);
router.put('/:user_id', workerController.updateWorker);
router.delete('/:user_id', workerController.removeWorker);
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) {
throw new ValidationError('날짜가 필요합니다', {
required: ['date'],
@@ -61,10 +61,10 @@ const getDailyAttendanceRecordsService = async (date, workerId = null) => {
});
}
logger.info('일일 근태 기록 조회 요청', { date, workerId });
logger.info('일일 근태 기록 조회 요청', { date, userId });
try {
const records = await AttendanceModel.getDailyAttendanceRecords(date, workerId);
const records = await AttendanceModel.getDailyAttendanceRecords(date, userId);
logger.info('일일 근태 기록 조회 성공', { date, count: records.length });
return records;
} 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) {
throw new ValidationError('시작 날짜와 종료 날짜가 필요합니다', {
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 {
const records = await AttendanceModel.getDailyRecords(startDate, endDate, workerId);
const records = await AttendanceModel.getDailyRecords(startDate, endDate, userId);
logger.info('기간별 근태 기록 조회 성공', { startDate, endDate, count: records.length });
return records;
} catch (error) {
@@ -103,7 +103,7 @@ const getAttendanceRecordsByRangeService = async (startDate, endDate, workerId =
const upsertAttendanceRecordService = async (recordData) => {
const {
record_date,
worker_id,
user_id,
total_work_hours,
attendance_type_id,
vacation_type_id,
@@ -115,25 +115,25 @@ const upsertAttendanceRecordService = async (recordData) => {
} = recordData;
// 필수 필드 검증
if (!record_date || !worker_id) {
if (!record_date || !user_id) {
throw new ValidationError('필수 필드가 누락되었습니다', {
required: ['record_date', 'worker_id'],
received: { record_date, worker_id }
required: ['record_date', 'user_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 {
// 1. 기존 기록 조회 (휴가 연동을 위해)
const existingRecords = await AttendanceModel.getDailyAttendanceRecords(record_date, worker_id);
const existingRecord = existingRecords.find(r => r.worker_id === worker_id);
const existingRecords = await AttendanceModel.getDailyAttendanceRecords(record_date, user_id);
const existingRecord = existingRecords.find(r => r.user_id === user_id);
const previousVacationTypeId = existingRecord?.vacation_type_id || null;
// 2. 근태 기록 저장
const result = await AttendanceModel.upsertAttendanceRecord({
record_date,
worker_id,
user_id,
total_work_hours,
work_attendance_type_id: attendance_type_id,
vacation_type_id,
@@ -150,21 +150,21 @@ const upsertAttendanceRecordService = async (recordData) => {
if (previousDays !== newDays) {
// 이전 휴가 복구
if (previousDays > 0) {
await vacationBalanceModel.restoreByPriority(worker_id, year, previousDays);
logger.info('휴가 잔액 복구', { worker_id, year, restored: previousDays });
await vacationBalanceModel.restoreByPriority(user_id, year, previousDays);
logger.info('휴가 잔액 복구', { user_id, year, restored: previousDays });
}
// 새 휴가 차감
if (newDays > 0) {
await vacationBalanceModel.deductByPriority(worker_id, year, newDays);
logger.info('휴가 잔액 차감', { worker_id, year, deducted: newDays });
await vacationBalanceModel.deductByPriority(user_id, year, newDays);
logger.info('휴가 잔액 차감', { user_id, year, deducted: newDays });
}
}
logger.info('근태 기록 저장 성공', { record_date, worker_id });
logger.info('근태 기록 저장 성공', { record_date, user_id });
return result;
} catch (error) {
logger.error('근태 기록 저장 실패', { record_date, worker_id, error: error.message });
logger.error('근태 기록 저장 실패', { record_date, user_id, error: error.message });
throw new DatabaseError('근태 기록 저장 중 데이터베이스 오류가 발생했습니다');
}
};
@@ -173,28 +173,28 @@ const upsertAttendanceRecordService = async (recordData) => {
* 휴가 처리
*/
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('필수 필드가 누락되었습니다', {
required: ['record_date', 'worker_id', 'vacation_type_id'],
received: { record_date, worker_id, vacation_type_id }
required: ['record_date', 'user_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 {
const result = await AttendanceModel.processVacation({
record_date,
worker_id,
user_id,
vacation_type_id
});
logger.info('휴가 처리 성공', { record_date, worker_id });
logger.info('휴가 처리 성공', { record_date, user_id });
return result;
} catch (error) {
logger.error('휴가 처리 실패', { record_date, worker_id, error: error.message });
logger.error('휴가 처리 실패', { record_date, user_id, error: error.message });
throw new DatabaseError('휴가 처리 중 데이터베이스 오류가 발생했습니다');
}
};
@@ -203,28 +203,28 @@ const processVacationService = async (vacationData) => {
* 초과 근무 승인
*/
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('필수 필드가 누락되었습니다', {
required: ['record_date', 'worker_id', 'overtime_approved'],
received: { record_date, worker_id, overtime_approved }
required: ['record_date', 'user_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 {
const result = await AttendanceModel.approveOvertime({
record_date,
worker_id,
user_id,
overtime_approved
});
logger.info('초과 근무 승인 처리 성공', { record_date, worker_id });
logger.info('초과 근무 승인 처리 성공', { record_date, user_id });
return result;
} catch (error) {
logger.error('초과 근무 승인 실패', { record_date, worker_id, error: error.message });
logger.error('초과 근무 승인 실패', { record_date, user_id, error: error.message });
throw new DatabaseError('초과 근무 승인 중 데이터베이스 오류가 발생했습니다');
}
};
@@ -264,22 +264,22 @@ const getVacationTypesService = async () => {
/**
* 작업자 휴가 잔여 일수 조회
*/
const getWorkerVacationBalanceService = async (workerId) => {
if (!workerId) {
const getWorkerVacationBalanceService = async (userId) => {
if (!userId) {
throw new ValidationError('작업자 ID가 필요합니다', {
required: ['worker_id'],
received: { workerId }
required: ['user_id'],
received: { userId }
});
}
logger.info('휴가 잔여 일수 조회 요청', { workerId });
logger.info('휴가 잔여 일수 조회 요청', { userId });
try {
const balance = await AttendanceModel.getWorkerVacationBalance(workerId);
logger.info('휴가 잔여 일수 조회 성공', { workerId });
const balance = await AttendanceModel.getWorkerVacationBalance(userId);
logger.info('휴가 잔여 일수 조회 성공', { userId });
return balance;
} catch (error) {
logger.error('휴가 잔여 일수 조회 실패', { workerId, error: error.message });
logger.error('휴가 잔여 일수 조회 실패', { userId, error: error.message });
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) {
throw new ValidationError('연도와 월이 필요합니다', {
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 {
const stats = await AttendanceModel.getMonthlyAttendanceStats(year, month, workerId);
const stats = await AttendanceModel.getMonthlyAttendanceStats(year, month, userId);
logger.info('월별 근태 통계 조회 성공', { year, month });
return stats;
} catch (error) {
@@ -347,21 +347,21 @@ const saveCheckinsService = async (date, checkins) => {
const results = [];
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 });
continue;
}
const result = await AttendanceModel.upsertCheckin({
worker_id,
user_id,
record_date: date,
is_present
});
results.push({
worker_id,
user_id,
record_id: result,
is_present
});

View File

@@ -57,7 +57,7 @@ const loginService = async (username, password, ipAddress, userAgent) => {
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',
{ expiresIn: process.env.JWT_EXPIRES_IN || '24h' }
);
@@ -81,8 +81,7 @@ const loginService = async (username, password, ipAddress, userAgent) => {
username: user.username,
name: user.name || user.username,
role: user.role_name,
access_level: user.access_level,
worker_id: user.worker_id
access_level: user.access_level
}
}
};

View File

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

View File

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

View File

@@ -286,7 +286,7 @@ const optimizedQueries = {
let baseQuery = `
SELECT w.*, d.department_name, COUNT(dwr.id) as report_count
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
`;
@@ -322,10 +322,10 @@ const optimizedQueries = {
countQuery += whereClause;
}
baseQuery += ' GROUP BY w.worker_id';
baseQuery += ' GROUP BY w.user_id';
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,
et.name as error_type_name, u.name as created_by_name
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 work_types wt ON dwr.work_type_id = wt.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 },
name: { required: true, type: 'string', minLength: 2, maxLength: 100 },
access_level: { required: true, enum: ['user', 'admin', 'system'] },
worker_id: { type: 'integer' }
user_id: { type: 'integer' }
},
// 사용자 업데이트
updateUser: {
name: { type: 'string', minLength: 2, maxLength: 100 },
access_level: { enum: ['user', 'admin', 'system'] },
worker_id: { type: 'integer' }
user_id: { type: 'integer' }
},
// 비밀번호 변경
@@ -261,7 +261,7 @@ const schemas = {
// 일일 작업 보고서 생성 (배열 형태)
createDailyWorkReport: {
report_date: { required: true, type: 'date' },
worker_id: { required: true, type: 'integer' },
user_id: { required: true, type: 'integer' },
work_entries: { required: true, type: 'array' },
created_by: { type: 'integer' }
},