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:
@@ -21,7 +21,9 @@ function createTokenPayload(user) {
|
||||
id: user.user_id,
|
||||
username: user.username,
|
||||
name: user.name,
|
||||
worker_id: user.worker_id || null,
|
||||
department_id: user.department_id || null,
|
||||
department_name: user.department_name || user.department || null,
|
||||
is_production: user.is_production || false,
|
||||
department: user.department,
|
||||
role: user.role,
|
||||
access_level: user.role,
|
||||
@@ -78,8 +80,10 @@ async function login(req, res, next) {
|
||||
username: user.username,
|
||||
name: user.name,
|
||||
department: user.department,
|
||||
department_id: user.department_id || null,
|
||||
department_name: user.department_name || user.department || null,
|
||||
is_production: user.is_production || false,
|
||||
role: user.role,
|
||||
worker_id: user.worker_id || null,
|
||||
system_access: payload.system_access
|
||||
}
|
||||
});
|
||||
@@ -154,8 +158,10 @@ async function validate(req, res, next) {
|
||||
username: user.username,
|
||||
name: user.name,
|
||||
department: user.department,
|
||||
department_id: user.department_id || null,
|
||||
department_name: user.department_name || user.department || null,
|
||||
is_production: user.is_production || false,
|
||||
role: user.role,
|
||||
worker_id: user.worker_id || null,
|
||||
system_access: {
|
||||
system1: user.system1_access,
|
||||
system2: user.system2_access,
|
||||
|
||||
@@ -84,7 +84,10 @@ async function hashPassword(password) {
|
||||
async function findByUsername(username) {
|
||||
const db = getPool();
|
||||
const [rows] = await db.query(
|
||||
'SELECT * FROM sso_users WHERE username = ? AND is_active = TRUE',
|
||||
`SELECT s.*, d.department_id, d.department_name, d.is_production
|
||||
FROM sso_users s
|
||||
LEFT JOIN departments d ON s.department_id = d.department_id
|
||||
WHERE s.username = ? AND s.is_active = TRUE`,
|
||||
[username]
|
||||
);
|
||||
return rows[0] || null;
|
||||
@@ -93,7 +96,10 @@ async function findByUsername(username) {
|
||||
async function findById(userId) {
|
||||
const db = getPool();
|
||||
const [rows] = await db.query(
|
||||
'SELECT * FROM sso_users WHERE user_id = ?',
|
||||
`SELECT s.*, d.department_id, d.department_name, d.is_production
|
||||
FROM sso_users s
|
||||
LEFT JOIN departments d ON s.department_id = d.department_id
|
||||
WHERE s.user_id = ?`,
|
||||
[userId]
|
||||
);
|
||||
return rows[0] || null;
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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: '권한이 없습니다' });
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
`;
|
||||
|
||||
|
||||
@@ -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입니다');
|
||||
|
||||
@@ -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는 유지 (다른 마이그레이션에서 관리)
|
||||
};
|
||||
@@ -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);
|
||||
*/
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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];
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
// ==================== 안전 체크리스트 관련 ====================
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
@@ -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
|
||||
});
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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('이슈 보고서 생성 중 오류가 발생했습니다');
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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' }
|
||||
},
|
||||
|
||||
318
system1-factory/web/js/_deprecated/index.js
Normal file
318
system1-factory/web/js/_deprecated/index.js
Normal file
@@ -0,0 +1,318 @@
|
||||
/**
|
||||
* Daily Work Report - Module Loader
|
||||
* 작업보고서 모듈을 초기화하고 연결하는 메인 진입점
|
||||
*
|
||||
* 로드 순서:
|
||||
* 1. state.js - 전역 상태 관리
|
||||
* 2. utils.js - 유틸리티 함수
|
||||
* 3. api.js - API 클라이언트
|
||||
* 4. index.js - 이 파일 (메인 컨트롤러)
|
||||
*/
|
||||
|
||||
class DailyWorkReportController {
|
||||
constructor() {
|
||||
this.state = window.DailyWorkReportState;
|
||||
this.api = window.DailyWorkReportAPI;
|
||||
this.utils = window.DailyWorkReportUtils;
|
||||
this.initialized = false;
|
||||
|
||||
console.log('[Controller] DailyWorkReportController 생성');
|
||||
}
|
||||
|
||||
/**
|
||||
* 초기화
|
||||
*/
|
||||
async init() {
|
||||
if (this.initialized) {
|
||||
console.log('[Controller] 이미 초기화됨');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[Controller] 초기화 시작...');
|
||||
|
||||
try {
|
||||
// 이벤트 리스너 설정
|
||||
this.setupEventListeners();
|
||||
|
||||
// 기본 데이터 로드
|
||||
await this.api.loadAllData();
|
||||
|
||||
// TBM 탭이 기본
|
||||
await this.switchTab('tbm');
|
||||
|
||||
this.initialized = true;
|
||||
console.log('[Controller] 초기화 완료');
|
||||
|
||||
} catch (error) {
|
||||
console.error('[Controller] 초기화 실패:', error);
|
||||
window.showMessage?.('초기화 중 오류가 발생했습니다: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 이벤트 리스너 설정
|
||||
*/
|
||||
setupEventListeners() {
|
||||
// 탭 버튼
|
||||
const tbmBtn = document.getElementById('tbmReportTab');
|
||||
const completedBtn = document.getElementById('completedReportTab');
|
||||
|
||||
if (tbmBtn) {
|
||||
tbmBtn.addEventListener('click', () => this.switchTab('tbm'));
|
||||
}
|
||||
if (completedBtn) {
|
||||
completedBtn.addEventListener('click', () => this.switchTab('completed'));
|
||||
}
|
||||
|
||||
// 완료 보고서 날짜 변경
|
||||
const completedDateInput = document.getElementById('completedReportDate');
|
||||
if (completedDateInput) {
|
||||
completedDateInput.addEventListener('change', () => this.loadCompletedReports());
|
||||
}
|
||||
|
||||
console.log('[Controller] 이벤트 리스너 설정 완료');
|
||||
}
|
||||
|
||||
/**
|
||||
* 탭 전환
|
||||
*/
|
||||
async switchTab(tab) {
|
||||
this.state.setCurrentTab(tab);
|
||||
|
||||
const tbmBtn = document.getElementById('tbmReportTab');
|
||||
const completedBtn = document.getElementById('completedReportTab');
|
||||
const tbmSection = document.getElementById('tbmReportSection');
|
||||
const completedSection = document.getElementById('completedReportSection');
|
||||
|
||||
// 모든 탭 버튼 비활성화
|
||||
tbmBtn?.classList.remove('active');
|
||||
completedBtn?.classList.remove('active');
|
||||
|
||||
// 모든 섹션 숨기기
|
||||
if (tbmSection) tbmSection.style.display = 'none';
|
||||
if (completedSection) completedSection.style.display = 'none';
|
||||
|
||||
// 선택된 탭 활성화
|
||||
if (tab === 'tbm') {
|
||||
tbmBtn?.classList.add('active');
|
||||
if (tbmSection) tbmSection.style.display = 'block';
|
||||
await this.loadTbmData();
|
||||
} else if (tab === 'completed') {
|
||||
completedBtn?.classList.add('active');
|
||||
if (completedSection) completedSection.style.display = 'block';
|
||||
|
||||
// 오늘 날짜로 초기화
|
||||
const dateInput = document.getElementById('completedReportDate');
|
||||
if (dateInput) {
|
||||
dateInput.value = this.utils.getKoreaToday();
|
||||
}
|
||||
await this.loadCompletedReports();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TBM 데이터 로드
|
||||
*/
|
||||
async loadTbmData() {
|
||||
try {
|
||||
await this.api.loadIncompleteTbms();
|
||||
await this.api.loadDailyIssuesForTbms();
|
||||
|
||||
// 렌더링은 기존 함수 사용 (점진적 마이그레이션)
|
||||
if (typeof window.renderTbmWorkList === 'function') {
|
||||
window.renderTbmWorkList();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[Controller] TBM 데이터 로드 오류:', error);
|
||||
window.showMessage?.('TBM 데이터를 불러오는 중 오류가 발생했습니다.', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 완료 보고서 로드
|
||||
*/
|
||||
async loadCompletedReports() {
|
||||
try {
|
||||
const dateInput = document.getElementById('completedReportDate');
|
||||
const date = dateInput?.value || this.utils.getKoreaToday();
|
||||
|
||||
const reports = await this.api.loadCompletedReports(date);
|
||||
|
||||
// 렌더링은 기존 함수 사용
|
||||
if (typeof window.renderCompletedReports === 'function') {
|
||||
window.renderCompletedReports(reports);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[Controller] 완료 보고서 로드 오류:', error);
|
||||
window.showMessage?.('완료 보고서를 불러오는 중 오류가 발생했습니다.', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TBM 작업보고서 제출
|
||||
*/
|
||||
async submitTbmWorkReport(index) {
|
||||
try {
|
||||
const tbm = this.state.incompleteTbms[index];
|
||||
if (!tbm) {
|
||||
throw new Error('TBM 데이터를 찾을 수 없습니다.');
|
||||
}
|
||||
|
||||
// 유효성 검사
|
||||
const totalHoursInput = document.getElementById(`totalHours_${index}`);
|
||||
const totalHours = parseFloat(totalHoursInput?.value);
|
||||
|
||||
if (!totalHours || totalHours <= 0) {
|
||||
window.showMessage?.('작업시간을 입력해주세요.', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
// 부적합 시간 계산
|
||||
const defects = this.state.tempDefects[index] || [];
|
||||
const errorHours = defects.reduce((sum, d) => sum + (parseFloat(d.defect_hours) || 0), 0);
|
||||
const regularHours = totalHours - errorHours;
|
||||
|
||||
if (regularHours < 0) {
|
||||
window.showMessage?.('부적합 시간이 총 작업시간을 초과할 수 없습니다.', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
// API 데이터 구성
|
||||
const user = this.state.getCurrentUser();
|
||||
const reportData = {
|
||||
tbm_session_id: tbm.session_id,
|
||||
tbm_assignment_id: tbm.assignment_id,
|
||||
user_id: tbm.user_id,
|
||||
project_id: tbm.project_id,
|
||||
work_type_id: tbm.work_type_id,
|
||||
report_date: this.utils.formatDateForApi(tbm.session_date),
|
||||
total_hours: totalHours,
|
||||
regular_hours: regularHours,
|
||||
error_hours: errorHours,
|
||||
work_status_id: errorHours > 0 ? 2 : 1,
|
||||
created_by: user?.user_id || user?.id,
|
||||
defects: defects.map(d => ({
|
||||
category_id: d.category_id,
|
||||
item_id: d.item_id,
|
||||
issue_report_id: d.issue_report_id,
|
||||
defect_hours: d.defect_hours,
|
||||
note: d.note
|
||||
}))
|
||||
};
|
||||
|
||||
const result = await this.api.submitTbmWorkReport(reportData);
|
||||
|
||||
window.showSaveResultModal?.(
|
||||
'success',
|
||||
'제출 완료',
|
||||
`${tbm.worker_name}의 작업보고서가 제출되었습니다.`
|
||||
);
|
||||
|
||||
// 목록 새로고침
|
||||
await this.loadTbmData();
|
||||
|
||||
} catch (error) {
|
||||
console.error('[Controller] 제출 오류:', error);
|
||||
window.showSaveResultModal?.(
|
||||
'error',
|
||||
'제출 실패',
|
||||
error.message || '작업보고서 제출 중 오류가 발생했습니다.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 세션 일괄 제출
|
||||
*/
|
||||
async batchSubmitSession(sessionKey) {
|
||||
const rows = document.querySelectorAll(`tr[data-session-key="${sessionKey}"][data-type="tbm"]`);
|
||||
const indices = [];
|
||||
|
||||
rows.forEach(row => {
|
||||
const index = parseInt(row.dataset.index);
|
||||
const totalHoursInput = document.getElementById(`totalHours_${index}`);
|
||||
if (totalHoursInput?.value && parseFloat(totalHoursInput.value) > 0) {
|
||||
indices.push(index);
|
||||
}
|
||||
});
|
||||
|
||||
if (indices.length === 0) {
|
||||
window.showMessage?.('제출할 항목이 없습니다. 작업시간을 입력해주세요.', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
const confirmed = confirm(`${indices.length}건의 작업보고서를 일괄 제출하시겠습니까?`);
|
||||
if (!confirmed) return;
|
||||
|
||||
let successCount = 0;
|
||||
let failCount = 0;
|
||||
|
||||
for (const index of indices) {
|
||||
try {
|
||||
await this.submitTbmWorkReport(index);
|
||||
successCount++;
|
||||
} catch (error) {
|
||||
failCount++;
|
||||
console.error(`[Controller] 일괄 제출 오류 (index: ${index}):`, error);
|
||||
}
|
||||
}
|
||||
|
||||
if (failCount === 0) {
|
||||
window.showSaveResultModal?.('success', '일괄 제출 완료', `${successCount}건이 성공적으로 제출되었습니다.`);
|
||||
} else {
|
||||
window.showSaveResultModal?.('warning', '일괄 제출 부분 완료', `성공: ${successCount}건, 실패: ${failCount}건`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 상태 디버그
|
||||
*/
|
||||
debug() {
|
||||
console.log('[Controller] 상태 디버그:');
|
||||
this.state.debug();
|
||||
}
|
||||
}
|
||||
|
||||
// 전역 인스턴스 생성
|
||||
window.DailyWorkReportController = new DailyWorkReportController();
|
||||
|
||||
// 하위 호환성: 기존 전역 함수들
|
||||
window.switchTab = (tab) => window.DailyWorkReportController.switchTab(tab);
|
||||
window.submitTbmWorkReport = (index) => window.DailyWorkReportController.submitTbmWorkReport(index);
|
||||
window.batchSubmitTbmSession = (sessionKey) => window.DailyWorkReportController.batchSubmitSession(sessionKey);
|
||||
|
||||
// 사용자 정보 함수
|
||||
window.getUser = () => window.DailyWorkReportState.getUser();
|
||||
window.getCurrentUser = () => window.DailyWorkReportState.getCurrentUser();
|
||||
|
||||
// 날짜 그룹 토글 (UI 함수)
|
||||
window.toggleDateGroup = function(dateStr) {
|
||||
const group = document.querySelector(`.date-group[data-date="${dateStr}"]`);
|
||||
if (!group) return;
|
||||
|
||||
const isExpanded = group.classList.contains('expanded');
|
||||
const content = group.querySelector('.date-group-content');
|
||||
const icon = group.querySelector('.date-toggle-icon');
|
||||
|
||||
if (isExpanded) {
|
||||
group.classList.remove('expanded');
|
||||
group.classList.add('collapsed');
|
||||
if (content) content.style.display = 'none';
|
||||
if (icon) icon.textContent = '▶';
|
||||
} else {
|
||||
group.classList.remove('collapsed');
|
||||
group.classList.add('expanded');
|
||||
if (content) content.style.display = 'block';
|
||||
if (icon) icon.textContent = '▼';
|
||||
}
|
||||
};
|
||||
|
||||
// DOMContentLoaded 이벤트에서 초기화
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// 약간의 지연 후 초기화 (다른 스크립트 로드 대기)
|
||||
setTimeout(() => {
|
||||
window.DailyWorkReportController.init();
|
||||
}, 100);
|
||||
});
|
||||
|
||||
console.log('[Module] daily-work-report/index.js 로드 완료');
|
||||
51
system1-factory/web/js/_deprecated/work-report-api.js
Normal file
51
system1-factory/web/js/_deprecated/work-report-api.js
Normal file
@@ -0,0 +1,51 @@
|
||||
// /js/work-report-api.js
|
||||
import { apiGet, apiPost } from './api-helper.js';
|
||||
|
||||
/**
|
||||
* 작업 보고서 작성을 위해 필요한 초기 데이터(작업자, 프로젝트, 태스크)를 가져옵니다.
|
||||
* Promise.all을 사용하여 병렬로 API를 호출합니다.
|
||||
* @returns {Promise<{workers: Array, projects: Array, tasks: Array}>}
|
||||
*/
|
||||
export async function getInitialData() {
|
||||
try {
|
||||
const [allWorkers, projects, tasks] = await Promise.all([
|
||||
apiGet('/workers'),
|
||||
apiGet('/projects'),
|
||||
apiGet('/tasks')
|
||||
]);
|
||||
|
||||
// 활성화된 작업자만 필터링
|
||||
const workers = allWorkers.filter(worker => {
|
||||
return worker.status === 'active' || worker.is_active === 1 || worker.is_active === true;
|
||||
});
|
||||
|
||||
// 데이터 형식 검증
|
||||
if (!Array.isArray(workers) || !Array.isArray(projects) || !Array.isArray(tasks)) {
|
||||
throw new Error('서버에서 받은 데이터 형식이 올바르지 않습니다.');
|
||||
}
|
||||
|
||||
// 작업자 목록은 ID 기준으로 정렬
|
||||
workers.sort((a, b) => a.user_id - b.user_id);
|
||||
|
||||
return { workers, projects, tasks };
|
||||
} catch (error) {
|
||||
console.error('초기 데이터 로딩 중 오류 발생:', error);
|
||||
// 에러를 다시 던져서 호출한 쪽에서 처리할 수 있도록 함
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 작성된 작업 보고서 데이터를 서버에 전송합니다.
|
||||
* @param {Array<object>} reportData - 전송할 작업 보고서 데이터 배열
|
||||
* @returns {Promise<object>} - 서버의 응답 결과
|
||||
*/
|
||||
export async function createWorkReport(reportData) {
|
||||
try {
|
||||
const result = await apiPost('/workreports', reportData);
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('작업 보고서 생성 요청 실패:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
79
system1-factory/web/js/_deprecated/work-report-create.js
Normal file
79
system1-factory/web/js/_deprecated/work-report-create.js
Normal file
@@ -0,0 +1,79 @@
|
||||
// /js/work-report-create.js
|
||||
import { renderCalendar } from './calendar.js';
|
||||
import { getInitialData, createWorkReport } from './work-report-api.js';
|
||||
import { initializeReportTable, getReportData } from './work-report-ui.js';
|
||||
|
||||
// 전역 상태 변수
|
||||
let selectedDate = '';
|
||||
|
||||
/**
|
||||
* 날짜가 선택되었을 때 실행되는 콜백 함수.
|
||||
* 초기 데이터를 로드하고 테이블을 렌더링합니다.
|
||||
* @param {string} date - 선택된 날짜 (YYYY-MM-DD 형식)
|
||||
*/
|
||||
async function onDateSelect(date) {
|
||||
selectedDate = date;
|
||||
const tableBody = document.getElementById('reportBody');
|
||||
tableBody.innerHTML = '<tr><td colspan="8" class="text-center">데이터를 불러오는 중...</td></tr>';
|
||||
|
||||
try {
|
||||
const initialData = await getInitialData();
|
||||
initializeReportTable(initialData);
|
||||
} catch (error) {
|
||||
alert('데이터를 불러오는 데 실패했습니다: ' + error.message);
|
||||
tableBody.innerHTML = '<tr><td colspan="8" class="text-center error">오류 발생! 데이터를 불러올 수 없습니다.</td></tr>';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* '전체 등록' 버튼 클릭 시 실행되는 이벤트 핸들러.
|
||||
* 폼 데이터를 서버에 전송합니다.
|
||||
*/
|
||||
async function handleSubmit() {
|
||||
if (!selectedDate) {
|
||||
alert('먼저 달력에서 날짜를 선택해주세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
const reportData = getReportData();
|
||||
if (!reportData) {
|
||||
// getReportData 내부에서 이미 alert으로 사용자에게 알림
|
||||
return;
|
||||
}
|
||||
|
||||
// 각 항목에 선택된 날짜 추가
|
||||
const payload = reportData.map(item => ({ ...item, date: selectedDate }));
|
||||
|
||||
const submitBtn = document.getElementById('submitBtn');
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.textContent = '등록 중...';
|
||||
|
||||
try {
|
||||
const result = await createWorkReport(payload);
|
||||
if (result.success) {
|
||||
alert('✅ 작업 보고서가 성공적으로 등록되었습니다!');
|
||||
// 성공 후 폼을 다시 로드하거나, 다른 페이지로 이동 등의 로직 추가 가능
|
||||
onDateSelect(selectedDate); // 현재 날짜의 폼을 다시 로드
|
||||
} else {
|
||||
throw new Error(result.error || '알 수 없는 오류로 등록에 실패했습니다.');
|
||||
}
|
||||
} catch (error) {
|
||||
alert('❌ 등록 실패: ' + error.message);
|
||||
} finally {
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.textContent = '전체 등록';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 페이지 초기화 함수
|
||||
*/
|
||||
function initializePage() {
|
||||
renderCalendar('calendar', onDateSelect);
|
||||
|
||||
const submitBtn = document.getElementById('submitBtn');
|
||||
submitBtn.addEventListener('click', handleSubmit);
|
||||
}
|
||||
|
||||
// DOM이 로드되면 페이지 초기화를 시작합니다.
|
||||
document.addEventListener('DOMContentLoaded', initializePage);
|
||||
141
system1-factory/web/js/_deprecated/work-report-ui.js
Normal file
141
system1-factory/web/js/_deprecated/work-report-ui.js
Normal file
@@ -0,0 +1,141 @@
|
||||
// /js/work-report-ui.js
|
||||
|
||||
const DEFAULT_PROJECT_ID = '13'; // 나중에는 API나 설정에서 받아오는 것이 좋음
|
||||
const DEFAULT_TASK_ID = '15';
|
||||
|
||||
/**
|
||||
* 주어진 데이터를 바탕으로 <select> 요소의 <option>들을 생성합니다.
|
||||
* @param {Array<object>} items - 옵션으로 만들 데이터 배열
|
||||
* @param {string} valueField - <option>의 value 속성에 사용할 필드 이름
|
||||
* @param {string} textField - <option>의 텍스트에 사용할 필드 이름
|
||||
* @returns {string} - 생성된 HTML 옵션 문자열
|
||||
*/
|
||||
function createOptions(items, valueField, textField) {
|
||||
return items.map(item => `<option value="${item[valueField]}">${textField(item)}</option>`).join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* 테이블의 모든 행 번호를 다시 매깁니다.
|
||||
* @param {HTMLTableSectionElement} tableBody - tbody 요소
|
||||
*/
|
||||
function updateRowNumbers(tableBody) {
|
||||
tableBody.querySelectorAll('tr').forEach((tr, index) => {
|
||||
tr.cells[0].textContent = index + 1;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 하나의 작업 보고서 행(tr)을 생성합니다.
|
||||
* @param {object} worker - 작업자 정보
|
||||
* @param {Array} projects - 전체 프로젝트 목록
|
||||
* @param {Array} tasks - 전체 태스크 목록
|
||||
* @param {number} index - 행 번호
|
||||
* @returns {HTMLTableRowElement} - 생성된 tr 요소
|
||||
*/
|
||||
function createReportRow(worker, projects, tasks, index) {
|
||||
const tr = document.createElement('tr');
|
||||
|
||||
tr.innerHTML = `
|
||||
<td>${index + 1}</td>
|
||||
<td>
|
||||
<input type="hidden" name="user_id" value="${worker.user_id}">
|
||||
${worker.worker_name}
|
||||
</td>
|
||||
<td><select name="project_id">${createOptions(projects, 'project_id', p => p.project_name)}</select></td>
|
||||
<td><select name="task_id">${createOptions(tasks, 'task_id', t => `${t.category}:${t.subcategory}`)}</select></td>
|
||||
<td>
|
||||
<select name="overtime">
|
||||
<option value="">없음</option>
|
||||
${[1, 2, 3, 4].map(n => `<option>${n}</option>`).join('')}
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<select name="work_type">
|
||||
${['근무', '연차', '유급', '반차', '반반차', '조퇴', '휴무'].map(t => `<option>${t}</option>`).join('')}
|
||||
</select>
|
||||
</td>
|
||||
<td><input type="text" name="memo" placeholder="메모"></td>
|
||||
<td><button type="button" class="remove-btn">x</button></td>
|
||||
`;
|
||||
|
||||
// 이벤트 리스너 설정
|
||||
const workTypeSelect = tr.querySelector('[name="work_type"]');
|
||||
const projectSelect = tr.querySelector('[name="project_id"]');
|
||||
const taskSelect = tr.querySelector('[name="task_id"]');
|
||||
|
||||
workTypeSelect.addEventListener('change', () => {
|
||||
const isDisabled = ['연차', '휴무', '유급'].includes(workTypeSelect.value);
|
||||
projectSelect.disabled = isDisabled;
|
||||
taskSelect.disabled = isDisabled;
|
||||
if (isDisabled) {
|
||||
projectSelect.value = DEFAULT_PROJECT_ID;
|
||||
taskSelect.value = DEFAULT_TASK_ID;
|
||||
}
|
||||
});
|
||||
|
||||
tr.querySelector('.remove-btn').addEventListener('click', () => {
|
||||
tr.remove();
|
||||
updateRowNumbers(tr.parentElement);
|
||||
});
|
||||
|
||||
return tr;
|
||||
}
|
||||
|
||||
/**
|
||||
* 작업 보고서 테이블을 초기화하고 데이터를 채웁니다.
|
||||
* @param {{workers: Array, projects: Array, tasks: Array}} initialData - 초기 데이터
|
||||
*/
|
||||
export function initializeReportTable(initialData) {
|
||||
const tableBody = document.getElementById('reportBody');
|
||||
if (!tableBody) return;
|
||||
|
||||
tableBody.innerHTML = ''; // 기존 내용 초기화
|
||||
const { workers, projects, tasks } = initialData;
|
||||
|
||||
if (!workers || workers.length === 0) {
|
||||
tableBody.innerHTML = '<tr><td colspan="8" class="text-center">등록할 작업자 정보가 없습니다.</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
workers.forEach((worker, index) => {
|
||||
const row = createReportRow(worker, projects, tasks, index);
|
||||
tableBody.appendChild(row);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 테이블에서 폼 데이터를 추출하여 배열로 반환합니다.
|
||||
* @returns {Array<object>|null} - 추출된 데이터 배열 또는 유효성 검사 실패 시 null
|
||||
*/
|
||||
export function getReportData() {
|
||||
const tableBody = document.getElementById('reportBody');
|
||||
const rows = tableBody.querySelectorAll('tr');
|
||||
|
||||
if (rows.length === 0 || (rows.length === 1 && rows[0].cells.length < 2)) {
|
||||
alert('등록할 내용이 없습니다.');
|
||||
return null;
|
||||
}
|
||||
|
||||
const reportData = [];
|
||||
const workerIds = new Set();
|
||||
|
||||
for (const tr of rows) {
|
||||
const workerId = tr.querySelector('[name="user_id"]').value;
|
||||
if (workerIds.has(workerId)) {
|
||||
alert(`오류: 작업자 '${tr.cells[1].textContent.trim()}'가 중복 등록되었습니다.`);
|
||||
return null;
|
||||
}
|
||||
workerIds.add(workerId);
|
||||
|
||||
reportData.push({
|
||||
user_id: workerId,
|
||||
project_id: tr.querySelector('[name="project_id"]').value,
|
||||
task_id: tr.querySelector('[name="task_id"]').value,
|
||||
overtime_hours: tr.querySelector('[name="overtime"]').value || 0,
|
||||
work_details: tr.querySelector('[name="work_type"]').value,
|
||||
memo: tr.querySelector('[name="memo"]').value
|
||||
});
|
||||
}
|
||||
|
||||
return reportData;
|
||||
}
|
||||
@@ -794,14 +794,14 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
// ========== 작업자 연결 기능 ========== //
|
||||
let departments = [];
|
||||
let selectedWorkerId = null;
|
||||
let selectedUserId = null;
|
||||
|
||||
// 연결된 작업자 정보 표시 업데이트
|
||||
function updateLinkedWorkerDisplay(user) {
|
||||
const linkedWorkerInfo = document.getElementById('linkedWorkerInfo');
|
||||
if (!linkedWorkerInfo) return;
|
||||
|
||||
if (user.worker_id && user.worker_name) {
|
||||
if (user.user_id && user.worker_name) {
|
||||
linkedWorkerInfo.innerHTML = `
|
||||
<span class="worker-badge">
|
||||
<span class="worker-name">👤 ${user.worker_name}</span>
|
||||
@@ -820,7 +820,7 @@ async function openWorkerSelectModal() {
|
||||
return;
|
||||
}
|
||||
|
||||
selectedWorkerId = currentEditingUser.worker_id || null;
|
||||
selectedUserId = currentEditingUser.user_id || null;
|
||||
|
||||
// 부서 목록 로드
|
||||
await loadDepartmentsForSelect();
|
||||
@@ -833,7 +833,7 @@ window.openWorkerSelectModal = openWorkerSelectModal;
|
||||
// 작업자 선택 모달 닫기
|
||||
function closeWorkerSelectModal() {
|
||||
document.getElementById('workerSelectModal').style.display = 'none';
|
||||
selectedWorkerId = null;
|
||||
selectedUserId = null;
|
||||
}
|
||||
window.closeWorkerSelectModal = closeWorkerSelectModal;
|
||||
|
||||
@@ -906,18 +906,18 @@ function renderWorkerListForSelect(workers) {
|
||||
}
|
||||
|
||||
// 이미 다른 계정에 연결된 작업자 확인을 위해 users 배열 사용
|
||||
const linkedWorkerIds = users
|
||||
.filter(u => u.worker_id && u.user_id !== currentEditingUser?.user_id)
|
||||
.map(u => u.worker_id);
|
||||
const linkedUserIds = users
|
||||
.filter(u => u.user_id && u.user_id !== currentEditingUser?.user_id)
|
||||
.map(u => u.user_id);
|
||||
|
||||
container.innerHTML = workers.map(worker => {
|
||||
const isSelected = selectedWorkerId === worker.worker_id;
|
||||
const isLinkedToOther = linkedWorkerIds.includes(worker.worker_id);
|
||||
const linkedUser = isLinkedToOther ? users.find(u => u.worker_id === worker.worker_id) : null;
|
||||
const isSelected = selectedUserId === worker.user_id;
|
||||
const isLinkedToOther = linkedUserIds.includes(worker.user_id);
|
||||
const linkedUser = isLinkedToOther ? users.find(u => u.user_id === worker.user_id) : null;
|
||||
|
||||
return `
|
||||
<div class="worker-select-item ${isSelected ? 'selected' : ''} ${isLinkedToOther ? 'disabled' : ''}"
|
||||
onclick="${isLinkedToOther ? '' : `selectWorker(${worker.worker_id}, '${worker.worker_name}')`}">
|
||||
onclick="${isLinkedToOther ? '' : `selectWorker(${worker.user_id}, '${worker.worker_name}')`}">
|
||||
<div class="worker-avatar">${worker.worker_name.charAt(0)}</div>
|
||||
<div class="worker-info">
|
||||
<div class="worker-name">${worker.worker_name}</div>
|
||||
@@ -941,8 +941,8 @@ function getJobTypeName(jobType) {
|
||||
}
|
||||
|
||||
// 작업자 선택
|
||||
async function selectWorker(workerId, workerName) {
|
||||
selectedWorkerId = workerId;
|
||||
async function selectWorker(userId, workerName) {
|
||||
selectedUserId = userId;
|
||||
|
||||
// UI 업데이트
|
||||
document.querySelectorAll('.worker-select-item').forEach(item => {
|
||||
@@ -950,7 +950,7 @@ async function selectWorker(workerId, workerName) {
|
||||
item.querySelector('.select-indicator').textContent = '';
|
||||
});
|
||||
|
||||
const selectedItem = document.querySelector(`.worker-select-item[onclick*="${workerId}"]`);
|
||||
const selectedItem = document.querySelector(`.worker-select-item[onclick*="${userId}"]`);
|
||||
if (selectedItem) {
|
||||
selectedItem.classList.add('selected');
|
||||
selectedItem.querySelector('.select-indicator').textContent = '✓';
|
||||
@@ -959,12 +959,12 @@ async function selectWorker(workerId, workerName) {
|
||||
// 서버에 저장
|
||||
try {
|
||||
const response = await window.apiCall(`/users/${currentEditingUser.user_id}`, 'PUT', {
|
||||
worker_id: workerId
|
||||
user_id: userId
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
// currentEditingUser 업데이트
|
||||
currentEditingUser.worker_id = workerId;
|
||||
currentEditingUser.user_id = userId;
|
||||
currentEditingUser.worker_name = workerName;
|
||||
|
||||
// 부서 정보도 업데이트
|
||||
@@ -1003,7 +1003,7 @@ async function unlinkWorker() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!currentEditingUser.worker_id) {
|
||||
if (!currentEditingUser.user_id) {
|
||||
showToast('연결된 작업자가 없습니다.', 'warning');
|
||||
closeWorkerSelectModal();
|
||||
return;
|
||||
@@ -1015,19 +1015,19 @@ async function unlinkWorker() {
|
||||
|
||||
try {
|
||||
const response = await window.apiCall(`/users/${currentEditingUser.user_id}`, 'PUT', {
|
||||
worker_id: null
|
||||
user_id: null
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
// currentEditingUser 업데이트
|
||||
currentEditingUser.worker_id = null;
|
||||
currentEditingUser.user_id = null;
|
||||
currentEditingUser.worker_name = null;
|
||||
currentEditingUser.department_name = null;
|
||||
|
||||
// users 배열 업데이트
|
||||
const userIndex = users.findIndex(u => u.user_id === currentEditingUser.user_id);
|
||||
if (userIndex !== -1) {
|
||||
users[userIndex] = { ...users[userIndex], worker_id: null, worker_name: null, department_name: null };
|
||||
users[userIndex] = { ...users[userIndex], user_id: null, worker_name: null, department_name: null };
|
||||
}
|
||||
|
||||
// 표시 업데이트
|
||||
|
||||
@@ -322,7 +322,7 @@ async function fetchDailyWorkReports(date) {
|
||||
async function updateWorkerHours(workerId, date, newHours, reason = '') {
|
||||
try {
|
||||
const data = {
|
||||
worker_id: workerId,
|
||||
user_id: workerId,
|
||||
report_date: date,
|
||||
work_hours: parseFloat(newHours),
|
||||
modification_reason: reason,
|
||||
@@ -412,7 +412,7 @@ async function calculateDateStatus(dateStr) {
|
||||
status = 'missing';
|
||||
} else {
|
||||
const hasDiscrepancy = workReports.some(wr => {
|
||||
const dr = dailyReports.find(d => d.worker_id === wr.worker_id);
|
||||
const dr = dailyReports.find(d => d.user_id === wr.user_id);
|
||||
if (!dr) return true;
|
||||
|
||||
const expected = calculateExpectedHours(wr.status, wr.overtime_hours);
|
||||
@@ -607,8 +607,8 @@ async function getWorkersForDate(dateStr) {
|
||||
|
||||
// WorkReports 데이터 추가
|
||||
workReports.forEach(wr => {
|
||||
workerMap.set(wr.worker_id, {
|
||||
worker_id: wr.worker_id,
|
||||
workerMap.set(wr.user_id, {
|
||||
user_id: wr.user_id,
|
||||
worker_name: wr.worker_name,
|
||||
overtime_hours: wr.overtime_hours || 0,
|
||||
status: wr.status || 'normal',
|
||||
@@ -621,13 +621,13 @@ async function getWorkersForDate(dateStr) {
|
||||
|
||||
// DailyReports 데이터 추가
|
||||
dailyReports.forEach(dr => {
|
||||
if (workerMap.has(dr.worker_id)) {
|
||||
const worker = workerMap.get(dr.worker_id);
|
||||
if (workerMap.has(dr.user_id)) {
|
||||
const worker = workerMap.get(dr.user_id);
|
||||
worker.reported_hours = dr.work_hours;
|
||||
worker.hasDailyReport = true;
|
||||
} else {
|
||||
workerMap.set(dr.worker_id, {
|
||||
worker_id: dr.worker_id,
|
||||
workerMap.set(dr.user_id, {
|
||||
user_id: dr.user_id,
|
||||
worker_name: dr.worker_name,
|
||||
overtime_hours: 0,
|
||||
status: 'normal',
|
||||
@@ -739,7 +739,7 @@ async function saveEditedWork() {
|
||||
|
||||
showMessage('수정 중...', 'loading');
|
||||
|
||||
await updateWorkerHours(editingWorker.worker_id, selectedDate, newHours, reason);
|
||||
await updateWorkerHours(editingWorker.user_id, selectedDate, newHours, reason);
|
||||
|
||||
showMessage('✅ 근무시간이 성공적으로 수정되었습니다!', 'success');
|
||||
closeEditModal();
|
||||
@@ -767,7 +767,7 @@ async function deleteWorker(worker) {
|
||||
try {
|
||||
showMessage('삭제 중...', 'loading');
|
||||
|
||||
await deleteWorkerReport(worker.worker_id, selectedDate);
|
||||
await deleteWorkerReport(worker.user_id, selectedDate);
|
||||
|
||||
showMessage('✅ 작업 데이터가 성공적으로 삭제되었습니다!', 'success');
|
||||
|
||||
@@ -884,7 +884,7 @@ function renderWorkersList(workers) {
|
||||
</div>
|
||||
<div>
|
||||
<div class="worker-name">${worker.worker_name}</div>
|
||||
<div class="worker-id">작업자 ID: ${worker.worker_id}</div>
|
||||
<div class="worker-id">작업자 ID: ${worker.user_id}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="status-badge">${getStatusIcon(worker.validationStatus)}</div>
|
||||
|
||||
@@ -45,7 +45,7 @@ async function fetchWorkers() {
|
||||
return worker.status === 'active' || worker.is_active === 1 || worker.is_active === true;
|
||||
});
|
||||
|
||||
workers.sort((a, b) => a.worker_id - b.worker_id);
|
||||
workers.sort((a, b) => a.user_id - b.user_id);
|
||||
} catch (err) {
|
||||
alert('작업자 불러오기 실패');
|
||||
}
|
||||
@@ -93,14 +93,14 @@ function renderTable(data, year, month, lastDay) {
|
||||
workers.forEach(w => {
|
||||
// ✅ 월간 데이터 (표에 표시용)
|
||||
const recsThisMonth = data.filter(r =>
|
||||
r.worker_id === w.worker_id &&
|
||||
r.user_id === w.user_id &&
|
||||
new Date(r.date).getFullYear() === +year &&
|
||||
new Date(r.date).getMonth() + 1 === +month
|
||||
);
|
||||
|
||||
// ✅ 연간 데이터 (연차 계산용)
|
||||
const recsThisYear = data.filter(r =>
|
||||
r.worker_id === w.worker_id &&
|
||||
r.user_id === w.user_id &&
|
||||
new Date(r.date).getFullYear() === +year
|
||||
);
|
||||
|
||||
|
||||
@@ -34,8 +34,8 @@ export async function getWorkersByDate(date) {
|
||||
if (reports && reports.length > 0) {
|
||||
const workerMap = new Map();
|
||||
reports.forEach(r => {
|
||||
if (!workerMap.has(r.worker_id)) {
|
||||
workerMap.set(r.worker_id, { worker_id: r.worker_id, worker_name: r.worker_name });
|
||||
if (!workerMap.has(r.user_id)) {
|
||||
workerMap.set(r.user_id, { user_id: r.user_id, worker_name: r.worker_name });
|
||||
}
|
||||
});
|
||||
workers = Array.from(workerMap.values());
|
||||
|
||||
@@ -54,7 +54,7 @@ export function renderWorkerList(workers) {
|
||||
btn.type = 'button';
|
||||
btn.className = 'btn';
|
||||
btn.textContent = worker.worker_name;
|
||||
btn.dataset.id = worker.worker_id;
|
||||
btn.dataset.id = worker.user_id;
|
||||
btn.addEventListener('click', () => btn.classList.toggle('selected'));
|
||||
DOM.workerList.appendChild(btn);
|
||||
});
|
||||
@@ -79,7 +79,7 @@ export function getFormData() {
|
||||
issue_type_id: DOM.issueTypeSelect.value,
|
||||
start_time: DOM.timeStart.value,
|
||||
end_time: DOM.timeEnd.value,
|
||||
worker_ids: selectedWorkers, // worker_id -> worker_ids 로 명확하게 변경
|
||||
user_ids: selectedWorkers, // user_id -> user_ids 로 명확하게 변경
|
||||
};
|
||||
|
||||
for (const key in data) {
|
||||
|
||||
@@ -792,7 +792,7 @@ const MobileReport = (function() {
|
||||
const reportData = {
|
||||
tbm_assignment_id: tbm.assignment_id,
|
||||
tbm_session_id: tbm.session_id,
|
||||
worker_id: tbm.worker_id,
|
||||
user_id: tbm.user_id,
|
||||
project_id: tbm.project_id,
|
||||
work_type_id: tbm.task_id,
|
||||
report_date: reportDate,
|
||||
@@ -895,7 +895,7 @@ const MobileReport = (function() {
|
||||
data: {
|
||||
tbm_assignment_id: tbm.assignment_id,
|
||||
tbm_session_id: tbm.session_id,
|
||||
worker_id: tbm.worker_id,
|
||||
user_id: tbm.user_id,
|
||||
project_id: tbm.project_id,
|
||||
work_type_id: tbm.task_id,
|
||||
report_date: reportDate,
|
||||
@@ -974,7 +974,7 @@ const MobileReport = (function() {
|
||||
const state = window.DailyWorkReportState;
|
||||
|
||||
manualCards[id] = {
|
||||
worker_id: null,
|
||||
user_id: null,
|
||||
report_date: getKoreaToday(),
|
||||
project_id: null,
|
||||
work_type_id: null,
|
||||
@@ -1006,9 +1006,9 @@ const MobileReport = (function() {
|
||||
<button class="m-manual-delete" onclick="MobileReport.removeManualCard('${id}')">×</button>
|
||||
<div class="m-form-group">
|
||||
<label class="m-form-label">작업자</label>
|
||||
<select class="m-form-select" id="m_manual_worker_${id}" onchange="MobileReport.updateManualField('${id}','worker_id',this.value)">
|
||||
<select class="m-form-select" id="m_manual_worker_${id}" onchange="MobileReport.updateManualField('${id}','user_id',this.value)">
|
||||
<option value="">작업자 선택</option>
|
||||
${workers.map(w => `<option value="${w.worker_id}">${esc(w.worker_name)} (${esc(w.job_type || '-')})</option>`).join('')}
|
||||
${workers.map(w => `<option value="${w.user_id}">${esc(w.worker_name)} (${esc(w.job_type || '-')})</option>`).join('')}
|
||||
</select>
|
||||
</div>
|
||||
<div class="m-form-row">
|
||||
@@ -1127,7 +1127,7 @@ const MobileReport = (function() {
|
||||
const mc = manualCards[id];
|
||||
if (!mc) return;
|
||||
|
||||
const workerId = mc.worker_id || document.getElementById('m_manual_worker_' + id)?.value;
|
||||
const workerId = mc.user_id || document.getElementById('m_manual_worker_' + id)?.value;
|
||||
const reportDate = mc.report_date || document.getElementById('m_manual_date_' + id)?.value;
|
||||
const projectId = mc.project_id || document.getElementById('m_manual_project_' + id)?.value;
|
||||
const taskId = mc.task_id || document.getElementById('m_manual_task_' + id)?.value;
|
||||
@@ -1148,7 +1148,7 @@ const MobileReport = (function() {
|
||||
|
||||
const reportData = {
|
||||
report_date: reportDate,
|
||||
worker_id: parseInt(workerId),
|
||||
user_id: parseInt(workerId),
|
||||
work_entries: [{
|
||||
project_id: parseInt(projectId),
|
||||
task_id: parseInt(taskId),
|
||||
|
||||
@@ -581,7 +581,7 @@ window.submitTbmWorkReport = async function(index) {
|
||||
const reportData = {
|
||||
tbm_assignment_id: tbm.assignment_id,
|
||||
tbm_session_id: tbm.session_id,
|
||||
worker_id: tbm.worker_id,
|
||||
user_id: tbm.user_id,
|
||||
project_id: tbm.project_id,
|
||||
work_type_id: tbm.task_id, // task_id를 work_type_id 컬럼에 저장 (직접 작업보고서와 일관성 유지)
|
||||
report_date: reportDate,
|
||||
@@ -729,7 +729,7 @@ window.batchSubmitTbmSession = async function(sessionKey) {
|
||||
data: {
|
||||
tbm_assignment_id: tbm.assignment_id,
|
||||
tbm_session_id: tbm.session_id,
|
||||
worker_id: tbm.worker_id,
|
||||
user_id: tbm.user_id,
|
||||
project_id: tbm.project_id,
|
||||
work_type_id: tbm.task_id, // task_id를 work_type_id 컬럼에 저장 (일관성 유지)
|
||||
report_date: reportDate,
|
||||
@@ -847,7 +847,7 @@ window.addManualWorkRow = function() {
|
||||
<td>
|
||||
<select class="form-input-compact" id="worker_${manualIndex}" style="width: 120px;" required>
|
||||
<option value="">작업자 선택</option>
|
||||
${workers.map(w => `<option value="${escapeHtml(String(w.worker_id))}">${escapeHtml(w.worker_name)} (${escapeHtml(w.job_type || '-')})</option>`).join('')}
|
||||
${workers.map(w => `<option value="${escapeHtml(String(w.user_id))}">${escapeHtml(w.worker_name)} (${escapeHtml(w.job_type || '-')})</option>`).join('')}
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
@@ -1418,7 +1418,7 @@ window.submitManualWorkReport = async function(manualIndex) {
|
||||
// 주의: 서비스에서 task_id를 work_type_id 컬럼에 매핑함
|
||||
const reportData = {
|
||||
report_date: reportDate,
|
||||
worker_id: parseInt(workerId),
|
||||
user_id: parseInt(workerId),
|
||||
work_entries: [{
|
||||
project_id: parseInt(projectId),
|
||||
task_id: parseInt(taskId), // 서비스에서 work_type_id로 매핑됨
|
||||
@@ -1577,7 +1577,7 @@ window.submitAllManualWorkReports = async function() {
|
||||
// 서비스 레이어가 기대하는 형식으로 변환
|
||||
const reportData = {
|
||||
report_date: reportDate,
|
||||
worker_id: parseInt(workerId),
|
||||
user_id: parseInt(workerId),
|
||||
work_entries: [{
|
||||
project_id: parseInt(projectId),
|
||||
task_id: parseInt(taskId),
|
||||
@@ -2282,7 +2282,7 @@ async function loadTbmTeamForDate(date) {
|
||||
// 팀 구성 조회
|
||||
const teamRes = await window.apiCall(`/tbm/sessions/${targetSession.session_id}/team`);
|
||||
if (teamRes && teamRes.success && teamRes.data) {
|
||||
const teamWorkerIds = teamRes.data.map(m => m.worker_id);
|
||||
const teamWorkerIds = teamRes.data.map(m => m.user_id);
|
||||
console.log(`✅ TBM 팀 구성 로드 성공: ${teamWorkerIds.length}명`);
|
||||
return teamWorkerIds;
|
||||
}
|
||||
@@ -2334,16 +2334,16 @@ async function populateWorkerGrid() {
|
||||
btn.type = 'button';
|
||||
btn.className = 'worker-card';
|
||||
btn.textContent = worker.worker_name;
|
||||
btn.dataset.id = worker.worker_id;
|
||||
btn.dataset.id = worker.user_id;
|
||||
|
||||
// TBM 팀 구성에 포함된 작업자는 자동 선택
|
||||
if (tbmWorkerIds.includes(worker.worker_id)) {
|
||||
if (tbmWorkerIds.includes(worker.user_id)) {
|
||||
btn.classList.add('selected');
|
||||
selectedWorkers.add(worker.worker_id);
|
||||
selectedWorkers.add(worker.user_id);
|
||||
}
|
||||
|
||||
btn.addEventListener('click', () => {
|
||||
toggleWorkerSelection(worker.worker_id, btn);
|
||||
toggleWorkerSelection(worker.user_id, btn);
|
||||
});
|
||||
|
||||
grid.appendChild(btn);
|
||||
@@ -2673,12 +2673,12 @@ async function saveWorkReport() {
|
||||
const failureDetails = [];
|
||||
|
||||
for (const workerId of selectedWorkers) {
|
||||
const workerName = workers.find(w => w.worker_id == workerId)?.worker_name || '알 수 없음';
|
||||
const workerName = workers.find(w => w.user_id == workerId)?.worker_name || '알 수 없음';
|
||||
|
||||
// 서버가 기대하는 work_entries 배열 형태로 전송
|
||||
const requestData = {
|
||||
report_date: reportDate,
|
||||
worker_id: parseInt(workerId),
|
||||
user_id: parseInt(workerId),
|
||||
work_entries: newWorkEntries.map(entry => ({
|
||||
project_id: entry.project_id,
|
||||
task_id: entry.work_type_id, // 서버에서 task_id로 기대
|
||||
|
||||
@@ -96,7 +96,7 @@ class DailyWorkReportState extends BaseState {
|
||||
*/
|
||||
selectAllWorkers(select = true) {
|
||||
if (select) {
|
||||
this.workers.forEach(w => this.selectedWorkers.add(w.worker_id));
|
||||
this.workers.forEach(w => this.selectedWorkers.add(w.user_id));
|
||||
} else {
|
||||
this.selectedWorkers.clear();
|
||||
}
|
||||
|
||||
@@ -110,11 +110,11 @@ function renderWorkerList(workers) {
|
||||
}
|
||||
|
||||
container.innerHTML = workers.map(worker => `
|
||||
<div class="worker-card ${selectedWorkers.has(worker.worker_id) ? 'selected' : ''}"
|
||||
onclick="toggleWorkerSelection(${worker.worker_id})">
|
||||
<div class="worker-card ${selectedWorkers.has(worker.user_id) ? 'selected' : ''}"
|
||||
onclick="toggleWorkerSelection(${worker.user_id})">
|
||||
<div class="worker-info-row">
|
||||
<input type="checkbox" ${selectedWorkers.has(worker.worker_id) ? 'checked' : ''}
|
||||
onclick="event.stopPropagation(); toggleWorkerSelection(${worker.worker_id})">
|
||||
<input type="checkbox" ${selectedWorkers.has(worker.user_id) ? 'checked' : ''}
|
||||
onclick="event.stopPropagation(); toggleWorkerSelection(${worker.user_id})">
|
||||
<div class="worker-avatar">${worker.worker_name.charAt(0)}</div>
|
||||
<div class="worker-details">
|
||||
<span class="worker-name">${worker.worker_name}</span>
|
||||
|
||||
@@ -23,7 +23,7 @@ async function fetchDashboardStats() {
|
||||
// 필요한 데이터 형태로 가공 (예시)
|
||||
return {
|
||||
today_reports_count: stats.length,
|
||||
today_workers_count: new Set(stats.map(d => d.worker_id)).size,
|
||||
today_workers_count: new Set(stats.map(d => d.user_id)).size,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('대시보드 통계 데이터 로드 실패:', error);
|
||||
|
||||
@@ -147,7 +147,7 @@ userForm?.addEventListener('submit', async e => {
|
||||
password: document.getElementById('password').value.trim(),
|
||||
name: document.getElementById('name').value.trim(),
|
||||
access_level: document.getElementById('access_level').value,
|
||||
worker_id: document.getElementById('worker_id').value || null
|
||||
user_id: document.getElementById('user_id').value || null
|
||||
};
|
||||
|
||||
try {
|
||||
@@ -206,13 +206,13 @@ async function loadUsers() {
|
||||
|
||||
list.forEach(item => {
|
||||
item.access_level = accessLabels[item.access_level] || item.access_level;
|
||||
item.worker_id = item.worker_id || '-';
|
||||
item.user_id = item.user_id || '-';
|
||||
|
||||
// 행 생성
|
||||
const tr = document.createElement('tr');
|
||||
|
||||
// 데이터 컬럼
|
||||
['user_id', 'username', 'name', 'access_level', 'worker_id'].forEach(key => {
|
||||
['user_id', 'username', 'name', 'access_level', 'user_id'].forEach(key => {
|
||||
const td = document.createElement('td');
|
||||
td.textContent = item[key] || '-';
|
||||
tr.appendChild(td);
|
||||
@@ -267,7 +267,7 @@ async function loadUsers() {
|
||||
}
|
||||
|
||||
async function loadWorkerOptions() {
|
||||
const select = document.getElementById('worker_id');
|
||||
const select = document.getElementById('user_id');
|
||||
if (!select) return;
|
||||
|
||||
try {
|
||||
@@ -289,8 +289,8 @@ async function loadWorkerOptions() {
|
||||
if (Array.isArray(workers)) {
|
||||
workers.forEach(w => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = w.worker_id;
|
||||
opt.textContent = `${w.worker_name} (${w.worker_id})`;
|
||||
opt.value = w.user_id;
|
||||
opt.textContent = `${w.worker_name} (${w.user_id})`;
|
||||
select.appendChild(opt);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -80,10 +80,10 @@ async function loadWorkers() {
|
||||
|
||||
if (Array.isArray(list)) {
|
||||
list.forEach(item => {
|
||||
const row = createRow(item, ['worker_id', 'worker_name', 'position'], async w => {
|
||||
const row = createRow(item, ['user_id', 'worker_name', 'position'], async w => {
|
||||
if (!confirm('삭제하시겠습니까?')) return;
|
||||
try {
|
||||
const delRes = await fetch(`${API}/workers/${w.worker_id}`, {
|
||||
const delRes = await fetch(`${API}/workers/${w.user_id}`, {
|
||||
method: 'DELETE',
|
||||
headers: getAuthHeaders()
|
||||
});
|
||||
|
||||
@@ -255,11 +255,11 @@ function analyzeDashboardData() {
|
||||
// 작업자별 데이터 그룹화
|
||||
const workerWorkData = {};
|
||||
workData.forEach(work => {
|
||||
const workerId = work.worker_id;
|
||||
if (!workerWorkData[workerId]) {
|
||||
workerWorkData[workerId] = [];
|
||||
const userId = work.user_id;
|
||||
if (!workerWorkData[userId]) {
|
||||
workerWorkData[userId] = [];
|
||||
}
|
||||
workerWorkData[workerId].push(work);
|
||||
workerWorkData[userId].push(work);
|
||||
});
|
||||
|
||||
// 전체 통계 계산
|
||||
@@ -272,7 +272,7 @@ function analyzeDashboardData() {
|
||||
|
||||
// 작업자별 상세 분석 (개선된 버전)
|
||||
const workerAnalysis = workers.map(worker => {
|
||||
const workerWorks = workerWorkData[worker.worker_id] || [];
|
||||
const workerWorks = workerWorkData[worker.user_id] || [];
|
||||
const workerHours = workerWorks.reduce((sum, work) => sum + parseFloat(work.work_hours || 0), 0);
|
||||
|
||||
// 작업 유형 분석 (실제 이름으로)
|
||||
@@ -455,7 +455,7 @@ function createWorkerRow(worker) {
|
||||
<div class="update-time ${updateClass}">${updateTimeText}</div>
|
||||
</td>
|
||||
<td>
|
||||
<button class="detail-btn" onclick="showWorkerDetailSafe('${worker.worker_id}')">
|
||||
<button class="detail-btn" onclick="showWorkerDetailSafe('${worker.user_id}')">
|
||||
📋 상세
|
||||
</button>
|
||||
</td>
|
||||
@@ -479,7 +479,7 @@ function formatDateTime(date) {
|
||||
// 작업자 상세 모달 표시 (안전한 버전)
|
||||
function showWorkerDetailSafe(workerId) {
|
||||
// 현재 분석된 데이터에서 해당 작업자 찾기
|
||||
const worker = filteredWorkData.find(w => w.worker_id == workerId);
|
||||
const worker = filteredWorkData.find(w => w.user_id == workerId);
|
||||
if (!worker) {
|
||||
showMessage('작업자 정보를 찾을 수 없습니다.', 'error');
|
||||
return;
|
||||
|
||||
@@ -246,7 +246,7 @@ async function loadWorkData(date) {
|
||||
// ========== 요약 카드 업데이트 ========== //
|
||||
function updateSummaryCards() {
|
||||
// 오늘 작업자 수
|
||||
const todayWorkersCount = new Set(workData.map(w => w.worker_id)).size;
|
||||
const todayWorkersCount = new Set(workData.map(w => w.user_id)).size;
|
||||
updateSummaryCard(elements.todayWorkers, todayWorkersCount, '명');
|
||||
|
||||
// 총 작업 시간
|
||||
@@ -326,7 +326,7 @@ function displayWorkStatus() {
|
||||
|
||||
// 작업자별 상황 분석
|
||||
const workerStatusList = allWorkers.map(worker => {
|
||||
const todayWork = workData.filter(w => w.worker_id === worker.worker_id);
|
||||
const todayWork = workData.filter(w => w.user_id === worker.user_id);
|
||||
const totalHours = todayWork.reduce((sum, w) => sum + parseFloat(w.work_hours || 0), 0);
|
||||
|
||||
// 휴가/연차 제외한 실제 작업시간 계산
|
||||
@@ -453,7 +453,7 @@ function displayWorkStatus() {
|
||||
else if (worker.status === 'partial') iconKey = 'partial';
|
||||
|
||||
return `
|
||||
<tr data-worker-id="${worker.worker_id}">
|
||||
<tr data-user-id="${worker.user_id}">
|
||||
<td data-label="작업자" class="worker-info">
|
||||
<div class="worker-avatar">
|
||||
<span>${worker.worker_name.charAt(0)}</span>
|
||||
@@ -492,7 +492,7 @@ function displayWorkStatus() {
|
||||
|
||||
<td data-label="액션" class="worker-actions">
|
||||
<div class="action-buttons">
|
||||
<button class="action-btn btn-edit" onclick="openWorkerModal(${worker.worker_id}, '${worker.worker_name}')" title="작업입력">
|
||||
<button class="action-btn btn-edit" onclick="openWorkerModal(${worker.user_id}, '${worker.worker_name}')" title="작업입력">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path>
|
||||
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path>
|
||||
@@ -500,7 +500,7 @@ function displayWorkStatus() {
|
||||
<span class="action-text">작업입력</span>
|
||||
</button>
|
||||
${worker.vacationType ? `
|
||||
<button class="action-btn btn-vacation" onclick="handleVacation(${worker.worker_id}, '${worker.vacationType}')" title="${worker.vacationType === 'full' ? '연차처리' : worker.vacationType === 'half' ? '반차처리' : '반반차처리'}">
|
||||
<button class="action-btn btn-vacation" onclick="handleVacation(${worker.user_id}, '${worker.vacationType}')" title="${worker.vacationType === 'full' ? '연차처리' : worker.vacationType === 'half' ? '반차처리' : '반반차처리'}">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect>
|
||||
<line x1="16" y1="2" x2="16" y2="6"></line>
|
||||
@@ -511,7 +511,7 @@ function displayWorkStatus() {
|
||||
</button>
|
||||
` : ''}
|
||||
${worker.status === 'overtime-warning' ? `
|
||||
<button class="action-btn btn-confirm" onclick="confirmOvertime(${worker.worker_id})" title="정상확인">
|
||||
<button class="action-btn btn-confirm" onclick="confirmOvertime(${worker.user_id})" title="정상확인">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polyline points="20 6 9 17 4 12"></polyline>
|
||||
</svg>
|
||||
@@ -563,16 +563,16 @@ function displayWorkersAsCards(workers) {
|
||||
elements.workersContainer.innerHTML = `
|
||||
<div class="workers-grid grid grid-cols-4">
|
||||
${workers.map(worker => {
|
||||
const todayWork = workData.filter(w => w.worker_id === worker.worker_id);
|
||||
const todayWork = workData.filter(w => w.user_id === worker.user_id);
|
||||
const totalHours = todayWork.reduce((sum, w) => sum + parseFloat(w.work_hours || 0), 0);
|
||||
|
||||
|
||||
// 휴가/연차 제외한 실제 작업시간 계산
|
||||
const actualWorkHours = todayWork
|
||||
.filter(w => w.project_id !== 13) // 연차/휴무 프로젝트 제외
|
||||
.reduce((sum, w) => sum + parseFloat(w.work_hours || 0), 0);
|
||||
|
||||
|
||||
const hasError = todayWork.some(w => w.work_status_id === 2);
|
||||
|
||||
|
||||
// 정규 작업과 에러 작업 건수 분리
|
||||
const regularWorkCount = todayWork.filter(w => w.project_id !== 13 && w.work_status_id !== 2).length;
|
||||
const errorWorkCount = todayWork.filter(w => w.project_id !== 13 && w.work_status_id === 2).length;
|
||||
@@ -631,7 +631,7 @@ function displayWorkersAsList(workers) {
|
||||
</thead>
|
||||
<tbody>
|
||||
${workers.map(worker => {
|
||||
const todayWork = workData.filter(w => w.worker_id === worker.worker_id);
|
||||
const todayWork = workData.filter(w => w.user_id === worker.user_id);
|
||||
const totalHours = todayWork.reduce((sum, w) => sum + parseFloat(w.work_hours || 0), 0);
|
||||
const hasError = todayWork.some(w => w.work_status_id === 2);
|
||||
|
||||
@@ -852,7 +852,7 @@ async function processVacation(workerId, vacationType, hours) {
|
||||
// 휴가용 작업 보고서 생성 (특별한 작업 유형으로)
|
||||
const vacationReport = {
|
||||
report_date: selectedDate,
|
||||
worker_id: workerId,
|
||||
user_id: workerId,
|
||||
project_id: 1, // 기본 프로젝트 (휴가용)
|
||||
work_type_id: 999, // 휴가 전용 작업 유형 (DB에 추가 필요)
|
||||
work_status_id: 1, // 정상 상태
|
||||
@@ -887,10 +887,10 @@ async function processOvertimeConfirmation(workerId) {
|
||||
|
||||
// 새로운 근태 관리 API 사용
|
||||
const overtimeData = {
|
||||
worker_id: workerId,
|
||||
user_id: workerId,
|
||||
date: selectedDate
|
||||
};
|
||||
|
||||
|
||||
const response = await window.apiCall('/attendance/overtime/approve', 'POST', overtimeData);
|
||||
|
||||
if (response.success) {
|
||||
@@ -1104,7 +1104,7 @@ async function loadModalData() {
|
||||
|
||||
async function loadModalExistingWork() {
|
||||
try {
|
||||
const response = await window.apiCall(`/daily-work-reports?date=${currentModalWorker.date}&worker_id=${currentModalWorker.id}`);
|
||||
const response = await window.apiCall(`/daily-work-reports?date=${currentModalWorker.date}&user_id=${currentModalWorker.id}`);
|
||||
modalExistingWork = Array.isArray(response) ? response : (response.data || []);
|
||||
} catch (error) {
|
||||
console.error('기존 작업 로드 오류:', error);
|
||||
@@ -1258,7 +1258,7 @@ async function saveModalNewWork() {
|
||||
|
||||
const workData = {
|
||||
report_date: currentModalWorker.date,
|
||||
worker_id: currentModalWorker.id,
|
||||
user_id: currentModalWorker.id,
|
||||
work_entries: [{
|
||||
project_id: parseInt(projectId),
|
||||
task_id: parseInt(workTypeId), // work_type_id를 task_id로 매핑
|
||||
@@ -1332,7 +1332,7 @@ async function handleModalVacation(vacationType) {
|
||||
try {
|
||||
// 새로운 근태 관리 API 사용
|
||||
const vacationData = {
|
||||
worker_id: currentModalWorker.id,
|
||||
user_id: currentModalWorker.id,
|
||||
date: currentModalWorker.date,
|
||||
vacation_type: vacation.code
|
||||
};
|
||||
|
||||
@@ -71,7 +71,7 @@ function updateProfileUI(user) {
|
||||
document.getElementById('username').textContent = user.username || '-';
|
||||
document.getElementById('fullName').textContent = user.name || '-';
|
||||
document.getElementById('accessLevel').textContent = accessLevelMap[user.access_level] || user.access_level || '-';
|
||||
document.getElementById('workerId').textContent = user.worker_id || '연결되지 않음';
|
||||
document.getElementById('workerId').textContent = user.user_id || '연결되지 않음';
|
||||
|
||||
// 날짜 포맷팅
|
||||
if (user.created_at) {
|
||||
|
||||
@@ -80,7 +80,7 @@ export function updateFilterOptions(masterData) {
|
||||
return html;
|
||||
};
|
||||
DOM.projectFilter.innerHTML = createOptions(masterData.projects, 'project_id', 'project_name');
|
||||
DOM.workerFilter.innerHTML = createOptions(masterData.workers, 'worker_id', 'worker_name');
|
||||
DOM.workerFilter.innerHTML = createOptions(masterData.workers, 'user_id', 'worker_name');
|
||||
DOM.taskFilter.innerHTML = createOptions(masterData.tasks, 'task_id', 'category');
|
||||
}
|
||||
|
||||
|
||||
@@ -678,7 +678,7 @@ function showUserEditForm(user) {
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="edit-worker-id">작업자 ID</label>
|
||||
<input type="number" id="edit-worker-id" value="${user.worker_id || ''}">
|
||||
<input type="number" id="edit-worker-id" value="${user.user_id || ''}">
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
@@ -711,9 +711,9 @@ async function updateUser(userId) {
|
||||
role: document.getElementById('edit-role').value,
|
||||
access_level: document.getElementById('edit-role').value,
|
||||
is_active: parseInt(document.getElementById('edit-is-active').value),
|
||||
worker_id: document.getElementById('edit-worker-id').value || null
|
||||
user_id: document.getElementById('edit-worker-id').value || null
|
||||
};
|
||||
|
||||
|
||||
const response = await apiRequest(`/api/system/users/${userId}`, 'PUT', formData);
|
||||
|
||||
if (response.success) {
|
||||
@@ -840,7 +840,7 @@ async function createUser() {
|
||||
email: document.getElementById('create-email').value || null,
|
||||
role: document.getElementById('create-role').value,
|
||||
access_level: document.getElementById('create-role').value,
|
||||
worker_id: document.getElementById('create-worker-id').value || null
|
||||
user_id: document.getElementById('create-worker-id').value || null
|
||||
};
|
||||
|
||||
const response = await apiRequest('/api/system/users', 'POST', formData);
|
||||
|
||||
@@ -15,8 +15,8 @@
|
||||
sessionDate: null,
|
||||
leaderId: null,
|
||||
leaderName: '',
|
||||
workers: new Set(), // worker_id Set
|
||||
workerNames: {}, // { worker_id: worker_name }
|
||||
workers: new Set(), // user_id Set
|
||||
workerNames: {}, // { user_id: worker_name }
|
||||
projectId: null,
|
||||
projectName: '',
|
||||
workTypeId: null,
|
||||
@@ -41,10 +41,10 @@
|
||||
W.sessionDate = window.TbmUtils.getTodayKST();
|
||||
var user = window.TbmState.getUser();
|
||||
if (user) {
|
||||
if (user.worker_id) {
|
||||
var worker = window.TbmState.allWorkers.find(function(w) { return w.worker_id === user.worker_id; });
|
||||
if (user.user_id) {
|
||||
var worker = window.TbmState.allWorkers.find(function(w) { return w.user_id === user.user_id; });
|
||||
if (worker) {
|
||||
W.leaderId = worker.worker_id;
|
||||
W.leaderId = worker.user_id;
|
||||
W.leaderName = worker.worker_name;
|
||||
} else {
|
||||
W.leaderName = user.name || '';
|
||||
@@ -203,7 +203,7 @@
|
||||
W.todayAssignments = {};
|
||||
res.data.forEach(function(a) {
|
||||
if (a.sessions && a.sessions.length > 0) {
|
||||
W.todayAssignments[a.worker_id] = a;
|
||||
W.todayAssignments[a.user_id] = a;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
@@ -216,14 +216,14 @@
|
||||
}
|
||||
|
||||
var workerCards = workers.map(function(w) {
|
||||
var selected = W.workers.has(w.worker_id) ? ' selected' : '';
|
||||
var assignment = W.todayAssignments[w.worker_id];
|
||||
var selected = W.workers.has(w.user_id) ? ' selected' : '';
|
||||
var assignment = W.todayAssignments[w.user_id];
|
||||
var assigned = assignment && assignment.total_hours >= 8;
|
||||
var partiallyAssigned = assignment && assignment.total_hours > 0 && assignment.total_hours < 8;
|
||||
|
||||
var badgeHtml = '';
|
||||
var disabledClass = '';
|
||||
var onclick = 'toggleWorker(' + w.worker_id + ')';
|
||||
var onclick = 'toggleWorker(' + w.user_id + ')';
|
||||
|
||||
if (assigned) {
|
||||
// 종일 배정됨 - 선택 불가
|
||||
@@ -238,7 +238,7 @@
|
||||
|
||||
return '<div class="worker-card' + selected + disabledClass + '"' +
|
||||
(onclick ? ' onclick="' + onclick + '"' : '') +
|
||||
' data-wid="' + w.worker_id + '"' +
|
||||
' data-wid="' + w.user_id + '"' +
|
||||
' style="' + (assigned ? 'opacity:0.5; pointer-events:none;' : '') + '">' +
|
||||
'<div class="worker-check">✓</div>' +
|
||||
'<div class="worker-info">' +
|
||||
@@ -272,7 +272,7 @@
|
||||
delete W.workerNames[workerId];
|
||||
} else {
|
||||
W.workers.add(workerId);
|
||||
var w = window.TbmState.allWorkers.find(function(x) { return x.worker_id === workerId; });
|
||||
var w = window.TbmState.allWorkers.find(function(x) { return x.user_id === workerId; });
|
||||
if (w) W.workerNames[workerId] = w.worker_name;
|
||||
}
|
||||
var card = document.querySelector('[data-wid="' + workerId + '"]');
|
||||
@@ -284,7 +284,7 @@
|
||||
window.toggleAllWorkers = function() {
|
||||
var workers = window.TbmState.allWorkers;
|
||||
var availableWorkers = workers.filter(function(w) {
|
||||
var a = W.todayAssignments && W.todayAssignments[w.worker_id];
|
||||
var a = W.todayAssignments && W.todayAssignments[w.user_id];
|
||||
return !(a && a.total_hours >= 8);
|
||||
});
|
||||
if (W.workers.size === availableWorkers.length) {
|
||||
@@ -292,8 +292,8 @@
|
||||
W.workerNames = {};
|
||||
} else {
|
||||
availableWorkers.forEach(function(w) {
|
||||
W.workers.add(w.worker_id);
|
||||
W.workerNames[w.worker_id] = w.worker_name;
|
||||
W.workers.add(w.user_id);
|
||||
W.workerNames[w.user_id] = w.worker_name;
|
||||
});
|
||||
}
|
||||
renderStepWorkers(document.getElementById('stepContainer'));
|
||||
@@ -522,7 +522,7 @@
|
||||
// 1. TBM 세션 생성
|
||||
var sessionData = {
|
||||
session_date: W.sessionDate,
|
||||
leader_id: leaderId
|
||||
leader_user_id: leaderId
|
||||
};
|
||||
|
||||
var response = await window.apiCall('/tbm/sessions', 'POST', sessionData);
|
||||
@@ -536,7 +536,7 @@
|
||||
var members = [];
|
||||
W.workers.forEach(function(wid) {
|
||||
members.push({
|
||||
worker_id: wid,
|
||||
user_id: wid,
|
||||
project_id: W.projectId,
|
||||
work_type_id: W.workTypeId,
|
||||
task_id: null,
|
||||
|
||||
@@ -124,10 +124,10 @@
|
||||
|
||||
function isMySession(s) {
|
||||
var userId = currentUser.user_id;
|
||||
var workerId = currentUser.worker_id;
|
||||
var workerId = currentUser.user_id;
|
||||
var userName = currentUser.name;
|
||||
return (userId && String(s.created_by) === String(userId)) ||
|
||||
(workerId && String(s.leader_id) === String(workerId)) ||
|
||||
(workerId && String(s.leader_user_id) === String(workerId)) ||
|
||||
(userName && s.created_by_name === userName);
|
||||
}
|
||||
|
||||
@@ -645,7 +645,7 @@
|
||||
var wpId = document.getElementById('de_wp_' + i).value || null;
|
||||
|
||||
members.push({
|
||||
worker_id: m.worker_id,
|
||||
user_id: m.user_id,
|
||||
project_id: m.project_id || deSession.project_id || null,
|
||||
work_type_id: m.work_type_id || deSession.work_type_id || null,
|
||||
task_id: taskId ? parseInt(taskId) : null,
|
||||
@@ -798,7 +798,7 @@
|
||||
if (type === 'early' && (!hours || hours <= 0)) {
|
||||
window.showToast(esc(completeTeamMembers[i].worker_name) + '의 근무 시간을 입력해주세요.', 'error'); return;
|
||||
}
|
||||
attendanceData.push({ worker_id: completeTeamMembers[i].worker_id, attendance_type: type, attendance_hours: hours });
|
||||
attendanceData.push({ user_id: completeTeamMembers[i].user_id, attendance_type: type, attendance_hours: hours });
|
||||
}
|
||||
|
||||
var btn = document.getElementById('completeSheetBtn');
|
||||
@@ -996,7 +996,7 @@
|
||||
|
||||
// 1) 기존 항목: 시간만 줄이기 (프로젝트/공정 유지)
|
||||
await window.TbmAPI.updateTeamMember(deSessionId, {
|
||||
worker_id: m.worker_id,
|
||||
user_id: m.user_id,
|
||||
project_id: m.project_id || null,
|
||||
work_type_id: m.work_type_id || null,
|
||||
task_id: m.task_id || null,
|
||||
@@ -1009,7 +1009,7 @@
|
||||
|
||||
// 2) 나머지 시간으로 새 항목 추가 (프로젝트/공정 변경 가능)
|
||||
await window.TbmAPI.splitAssignment(deSessionId, {
|
||||
worker_id: m.worker_id,
|
||||
user_id: m.user_id,
|
||||
work_hours: remainHoursKeep,
|
||||
project_id: newProjectId,
|
||||
work_type_id: newWorkTypeId
|
||||
@@ -1033,7 +1033,7 @@
|
||||
// transfer API 호출
|
||||
var res = await window.TbmAPI.transfer({
|
||||
transfer_type: 'send',
|
||||
worker_id: m.worker_id,
|
||||
user_id: m.user_id,
|
||||
source_session_id: deSessionId,
|
||||
dest_session_id: splitTargetSessionId,
|
||||
hours: remainHours,
|
||||
@@ -1094,7 +1094,7 @@
|
||||
if (!myDraftSession) {
|
||||
btnHtml = '<button type="button" class="pull-btn" disabled title="내 TBM이 없음">내 TBM 없음</button>';
|
||||
} else {
|
||||
btnHtml = '<button type="button" class="pull-btn" onclick="event.stopPropagation(); startPull(' + m.worker_id + ', \'' + esc(m.worker_name).replace(/'/g, "\\'") + '\', ' + hours + ')">빼오기</button>';
|
||||
btnHtml = '<button type="button" class="pull-btn" onclick="event.stopPropagation(); startPull(' + m.user_id + ', \'' + esc(m.worker_name).replace(/'/g, "\\'") + '\', ' + hours + ')">빼오기</button>';
|
||||
}
|
||||
|
||||
html += '<div class="pull-member-item">' +
|
||||
@@ -1129,7 +1129,7 @@
|
||||
};
|
||||
|
||||
window.startPull = async function(workerId, workerName, maxHours) {
|
||||
pullWorker = { worker_id: workerId, worker_name: workerName, max_hours: maxHours };
|
||||
pullWorker = { user_id: workerId, worker_name: workerName, max_hours: maxHours };
|
||||
document.getElementById('pullHoursTitle').textContent = esc(workerName) + ' 빼오기';
|
||||
document.getElementById('pullHoursSubtitle').textContent = '최대 ' + maxHours + 'h 가능';
|
||||
document.getElementById('pullHoursInput').value = maxHours;
|
||||
@@ -1167,7 +1167,7 @@
|
||||
var pullWorkTypeId = document.getElementById('pullWorkTypeId').value || null;
|
||||
var res = await window.TbmAPI.transfer({
|
||||
transfer_type: 'pull',
|
||||
worker_id: pullWorker.worker_id,
|
||||
user_id: pullWorker.user_id,
|
||||
source_session_id: pullSessionId,
|
||||
dest_session_id: myDraftSession.session_id,
|
||||
hours: hours,
|
||||
@@ -1230,13 +1230,13 @@
|
||||
// 현재 세션 리더를 제외한 반장/그룹장 목록
|
||||
var leaders = workers.filter(function(w) {
|
||||
return (w.job_type === 'leader' || w.job_type === '그룹장' || w.job_type === 'admin') &&
|
||||
w.worker_id !== handoverSession.leader_id;
|
||||
w.user_id !== handoverSession.leader_user_id;
|
||||
});
|
||||
|
||||
var leaderSelect = document.getElementById('handoverLeaderId');
|
||||
leaderSelect.innerHTML = '<option value="">반장 선택...</option>' +
|
||||
leaders.map(function(w) {
|
||||
return '<option value="' + w.worker_id + '">' + esc(w.worker_name) + ' (' + (w.job_type || '') + ')</option>';
|
||||
return '<option value="' + w.user_id + '">' + esc(w.worker_name) + ' (' + (w.job_type || '') + ')</option>';
|
||||
}).join('');
|
||||
|
||||
// 인계할 팀원 체크리스트
|
||||
@@ -1246,7 +1246,7 @@
|
||||
} else {
|
||||
listEl.innerHTML = team.map(function(m) {
|
||||
return '<label style="display:flex; align-items:center; gap:0.5rem; padding:0.5rem; cursor:pointer;">' +
|
||||
'<input type="checkbox" class="handover-worker-cb" value="' + m.worker_id + '" checked style="width:16px; height:16px;">' +
|
||||
'<input type="checkbox" class="handover-worker-cb" value="' + m.user_id + '" checked style="width:16px; height:16px;">' +
|
||||
'<span style="font-weight:500; font-size:0.875rem;">' + esc(m.worker_name) + '</span>' +
|
||||
'<span style="font-size:0.75rem; color:#6b7280; margin-left:auto;">' + (m.job_type || '') + '</span>' +
|
||||
'</label>';
|
||||
@@ -1301,13 +1301,13 @@
|
||||
|
||||
var handoverData = {
|
||||
session_id: handoverSessionId,
|
||||
from_leader_id: handoverSession.leader_id,
|
||||
to_leader_id: toLeaderId,
|
||||
from_leader_user_id: handoverSession.leader_user_id,
|
||||
to_leader_user_id: toLeaderId,
|
||||
handover_date: today,
|
||||
handover_time: now,
|
||||
reason: '모바일 인계',
|
||||
handover_notes: notes,
|
||||
worker_ids: workerIds
|
||||
user_ids: workerIds
|
||||
};
|
||||
|
||||
var res = await window.TbmAPI.saveHandover(handoverData);
|
||||
|
||||
@@ -394,11 +394,11 @@ function openNewTbmModal() {
|
||||
}
|
||||
|
||||
// 입력자 자동 설정 (readonly)
|
||||
if (currentUser && currentUser.worker_id) {
|
||||
const worker = allWorkers.find(w => w.worker_id === currentUser.worker_id);
|
||||
if (currentUser && currentUser.user_id) {
|
||||
const worker = allWorkers.find(w => w.user_id === currentUser.user_id);
|
||||
if (worker) {
|
||||
document.getElementById('leaderName').textContent = worker.worker_name;
|
||||
document.getElementById('leaderId').value = worker.worker_id;
|
||||
document.getElementById('leaderId').value = worker.user_id;
|
||||
}
|
||||
} else if (currentUser && currentUser.name) {
|
||||
document.getElementById('leaderName').textContent = currentUser.name;
|
||||
@@ -444,7 +444,7 @@ async function renderNewTbmWorkerGrid() {
|
||||
todayAssignmentsMap = {};
|
||||
assignments.forEach(a => {
|
||||
if (a.sessions && a.sessions.length > 0) {
|
||||
todayAssignmentsMap[a.worker_id] = a;
|
||||
todayAssignmentsMap[a.user_id] = a;
|
||||
}
|
||||
});
|
||||
} catch(e) {
|
||||
@@ -454,8 +454,8 @@ async function renderNewTbmWorkerGrid() {
|
||||
}
|
||||
|
||||
grid.innerHTML = allWorkers.map(w => {
|
||||
const checked = selectedWorkersForNewTbm.has(w.worker_id) ? 'checked' : '';
|
||||
const assignment = todayAssignmentsMap[w.worker_id];
|
||||
const checked = selectedWorkersForNewTbm.has(w.user_id) ? 'checked' : '';
|
||||
const assignment = todayAssignmentsMap[w.user_id];
|
||||
const fullyAssigned = assignment && assignment.total_hours >= 8;
|
||||
const partiallyAssigned = assignment && assignment.total_hours > 0 && assignment.total_hours < 8;
|
||||
|
||||
@@ -474,9 +474,9 @@ async function renderNewTbmWorkerGrid() {
|
||||
}
|
||||
|
||||
return `
|
||||
<label class="tbm-worker-select-item ${checked ? 'selected' : ''}" data-wid="${w.worker_id}" style="${disabledStyle}">
|
||||
<input type="checkbox" class="new-tbm-worker-cb" data-worker-id="${w.worker_id}" ${checked} ${disabledAttr}
|
||||
onchange="toggleNewTbmWorker(${w.worker_id}, this.checked)">
|
||||
<label class="tbm-worker-select-item ${checked ? 'selected' : ''}" data-wid="${w.user_id}" style="${disabledStyle}">
|
||||
<input type="checkbox" class="new-tbm-worker-cb" data-user-id="${w.user_id}" ${checked} ${disabledAttr}
|
||||
onchange="toggleNewTbmWorker(${w.user_id}, this.checked)">
|
||||
<span class="tbm-worker-name">${escapeHtml(w.worker_name)}</span>
|
||||
<span class="tbm-worker-role">${escapeHtml(w.job_type || '작업자')}</span>
|
||||
${badgeHtml}
|
||||
@@ -511,9 +511,9 @@ window.toggleNewTbmWorker = toggleNewTbmWorker;
|
||||
|
||||
function selectAllNewTbmWorkers() {
|
||||
allWorkers.forEach(w => {
|
||||
const a = todayAssignmentsMap && todayAssignmentsMap[w.worker_id];
|
||||
const a = todayAssignmentsMap && todayAssignmentsMap[w.user_id];
|
||||
if (a && a.total_hours >= 8) return; // 종일 배정 제외
|
||||
selectedWorkersForNewTbm.add(w.worker_id);
|
||||
selectedWorkersForNewTbm.add(w.user_id);
|
||||
});
|
||||
document.querySelectorAll('.new-tbm-worker-cb').forEach(cb => {
|
||||
if (!cb.disabled) cb.checked = true;
|
||||
@@ -539,12 +539,12 @@ function populateLeaderSelect() {
|
||||
if (!leaderSelect) return;
|
||||
|
||||
// 로그인한 사용자가 작업자와 연결되어 있는지 확인
|
||||
if (currentUser && currentUser.worker_id) {
|
||||
if (currentUser && currentUser.user_id) {
|
||||
// 작업자와 연결된 경우: 자동으로 선택하고 비활성화
|
||||
const worker = allWorkers.find(w => w.worker_id === currentUser.worker_id);
|
||||
const worker = allWorkers.find(w => w.user_id === currentUser.user_id);
|
||||
if (worker) {
|
||||
const jobTypeText = worker.job_type ? ` (${escapeHtml(worker.job_type)})` : '';
|
||||
leaderSelect.innerHTML = `<option value="${escapeHtml(worker.worker_id)}" selected>${escapeHtml(worker.worker_name)}${jobTypeText}</option>`;
|
||||
leaderSelect.innerHTML = `<option value="${escapeHtml(worker.user_id)}" selected>${escapeHtml(worker.worker_name)}${jobTypeText}</option>`;
|
||||
leaderSelect.disabled = true;
|
||||
console.log('✅ 입력자 자동 설정:', worker.worker_name);
|
||||
} else {
|
||||
@@ -553,7 +553,7 @@ function populateLeaderSelect() {
|
||||
leaderSelect.disabled = true;
|
||||
}
|
||||
} else {
|
||||
// 관리자 계정 (worker_id가 없음): 드롭다운으로 선택 가능
|
||||
// 관리자 계정 (user_id가 없음): 드롭다운으로 선택 가능
|
||||
const leaders = allWorkers.filter(w =>
|
||||
w.job_type === 'leader' || w.job_type === '그룹장' || w.job_type === 'admin'
|
||||
);
|
||||
@@ -561,7 +561,7 @@ function populateLeaderSelect() {
|
||||
leaderSelect.innerHTML = '<option value="">입력자 선택...</option>' +
|
||||
leaders.map(w => {
|
||||
const jobTypeText = w.job_type ? ` (${escapeHtml(w.job_type)})` : '';
|
||||
return `<option value="${escapeHtml(w.worker_id)}">${escapeHtml(w.worker_name)}${jobTypeText}</option>`;
|
||||
return `<option value="${escapeHtml(w.user_id)}">${escapeHtml(w.worker_name)}${jobTypeText}</option>`;
|
||||
}).join('');
|
||||
leaderSelect.disabled = false;
|
||||
console.log('✅ 관리자: 입력자 선택 가능');
|
||||
@@ -646,8 +646,8 @@ async function saveTbmSession() {
|
||||
let leaderId = parseInt(document.getElementById('leaderId').value);
|
||||
|
||||
if (!leaderId || isNaN(leaderId)) {
|
||||
if (!currentUser.worker_id) {
|
||||
console.log('📝 관리자 계정: leader_id를 NULL로 설정');
|
||||
if (!currentUser.user_id) {
|
||||
console.log('📝 관리자 계정: leader_user_id를 NULL로 설정');
|
||||
leaderId = null;
|
||||
} else {
|
||||
console.error('❌ 입력자 설정 오류');
|
||||
@@ -658,7 +658,7 @@ async function saveTbmSession() {
|
||||
|
||||
const sessionData = {
|
||||
session_date: document.getElementById('sessionDate').value,
|
||||
leader_id: leaderId
|
||||
leader_user_id: leaderId
|
||||
};
|
||||
|
||||
if (!sessionData.session_date) {
|
||||
@@ -680,7 +680,7 @@ async function saveTbmSession() {
|
||||
for (const workerData of workerTaskList) {
|
||||
for (const taskLine of workerData.tasks) {
|
||||
members.push({
|
||||
worker_id: workerData.worker_id,
|
||||
user_id: workerData.user_id,
|
||||
project_id: taskLine.project_id || null,
|
||||
work_type_id: taskLine.work_type_id,
|
||||
task_id: taskLine.task_id,
|
||||
@@ -732,7 +732,7 @@ async function saveTbmSession() {
|
||||
const members = [];
|
||||
selectedWorkersForNewTbm.forEach(workerId => {
|
||||
members.push({
|
||||
worker_id: workerId,
|
||||
user_id: workerId,
|
||||
project_id: projectId,
|
||||
work_type_id: workTypeId,
|
||||
task_id: null,
|
||||
@@ -894,11 +894,11 @@ function openWorkerSelectionModal() {
|
||||
if (!workerCardGrid) return;
|
||||
|
||||
// 이미 추가된 작업자 ID 세트
|
||||
const addedWorkerIds = new Set(workerTaskList.map(w => w.worker_id));
|
||||
const addedWorkerIds = new Set(workerTaskList.map(w => w.user_id));
|
||||
|
||||
workerCardGrid.innerHTML = allWorkers.map(worker => {
|
||||
const isAdded = addedWorkerIds.has(worker.worker_id);
|
||||
const safeWorkerId = parseInt(worker.worker_id) || 0;
|
||||
const isAdded = addedWorkerIds.has(worker.user_id);
|
||||
const safeWorkerId = parseInt(worker.user_id) || 0;
|
||||
return `
|
||||
<div id="worker-card-${safeWorkerId}"
|
||||
onclick="toggleWorkerSelection(${safeWorkerId})"
|
||||
@@ -923,7 +923,7 @@ window.openWorkerSelectionModal = openWorkerSelectionModal;
|
||||
// 작업자 선택 토글
|
||||
function toggleWorkerSelection(workerId) {
|
||||
// 이미 추가된 작업자는 선택 불가
|
||||
const alreadyAdded = workerTaskList.some(w => w.worker_id === workerId);
|
||||
const alreadyAdded = workerTaskList.some(w => w.user_id === workerId);
|
||||
if (alreadyAdded) return;
|
||||
|
||||
const card = document.getElementById(`worker-card-${workerId}`);
|
||||
@@ -945,11 +945,11 @@ window.toggleWorkerSelection = toggleWorkerSelection;
|
||||
|
||||
// 전체 선택
|
||||
function selectAllWorkersInModal() {
|
||||
const addedWorkerIds = new Set(workerTaskList.map(w => w.worker_id));
|
||||
const addedWorkerIds = new Set(workerTaskList.map(w => w.user_id));
|
||||
allWorkers.forEach(worker => {
|
||||
if (!addedWorkerIds.has(worker.worker_id)) {
|
||||
selectedWorkersInModal.add(worker.worker_id);
|
||||
const card = document.getElementById(`worker-card-${worker.worker_id}`);
|
||||
if (!addedWorkerIds.has(worker.user_id)) {
|
||||
selectedWorkersInModal.add(worker.user_id);
|
||||
const card = document.getElementById(`worker-card-${worker.user_id}`);
|
||||
if (card) {
|
||||
card.style.borderColor = '#3b82f6';
|
||||
card.style.background = '#eff6ff';
|
||||
@@ -982,10 +982,10 @@ function confirmWorkerSelection() {
|
||||
}
|
||||
|
||||
selectedWorkersInModal.forEach(workerId => {
|
||||
const worker = allWorkers.find(w => w.worker_id === workerId);
|
||||
const worker = allWorkers.find(w => w.user_id === workerId);
|
||||
if (worker) {
|
||||
workerTaskList.push({
|
||||
worker_id: worker.worker_id,
|
||||
user_id: worker.user_id,
|
||||
worker_name: worker.worker_name,
|
||||
job_type: worker.job_type,
|
||||
tasks: [
|
||||
@@ -1970,16 +1970,16 @@ async function openTeamCompositionModal(sessionId) {
|
||||
|
||||
// 팀원별로 작업 그룹화
|
||||
teamMembers.forEach(member => {
|
||||
if (!workerMap.has(member.worker_id)) {
|
||||
workerMap.set(member.worker_id, {
|
||||
worker_id: member.worker_id,
|
||||
if (!workerMap.has(member.user_id)) {
|
||||
workerMap.set(member.user_id, {
|
||||
user_id: member.user_id,
|
||||
worker_name: member.worker_name,
|
||||
job_type: member.job_type,
|
||||
tasks: []
|
||||
});
|
||||
}
|
||||
|
||||
workerMap.get(member.worker_id).tasks.push({
|
||||
workerMap.get(member.user_id).tasks.push({
|
||||
task_line_id: generateUUID(),
|
||||
project_id: member.project_id,
|
||||
work_type_id: member.work_type_id,
|
||||
@@ -2003,7 +2003,7 @@ async function openTeamCompositionModal(sessionId) {
|
||||
// 입력자 표시
|
||||
if (session.leader_name) {
|
||||
document.getElementById('leaderName').value = `${session.leader_name} (${session.leader_job_type || ''})`;
|
||||
document.getElementById('leaderId').value = session.leader_id;
|
||||
document.getElementById('leaderId').value = session.leader_user_id;
|
||||
} else if (session.created_by_name) {
|
||||
document.getElementById('leaderName').value = `${session.created_by_name} (관리자)`;
|
||||
document.getElementById('leaderId').value = '';
|
||||
@@ -2026,7 +2026,7 @@ function updateSelectedWorkers() {
|
||||
selectedWorkers.clear();
|
||||
|
||||
document.querySelectorAll('.worker-checkbox:checked').forEach(cb => {
|
||||
selectedWorkers.add(parseInt(cb.dataset.workerId));
|
||||
selectedWorkers.add(parseInt(cb.dataset.userId));
|
||||
});
|
||||
|
||||
const selectedCount = document.getElementById('selectedCount');
|
||||
@@ -2038,7 +2038,7 @@ function updateSelectedWorkers() {
|
||||
selectedList.innerHTML = '<p style="margin: 0; color: #9ca3af; font-size: 0.875rem;">작업자를 선택해주세요</p>';
|
||||
} else {
|
||||
const selectedWorkersArray = Array.from(selectedWorkers).map(id => {
|
||||
const worker = allWorkers.find(w => w.worker_id === id);
|
||||
const worker = allWorkers.find(w => w.user_id === id);
|
||||
return worker ? `
|
||||
<span style="display: inline-flex; align-items: center; gap: 0.25rem; padding: 0.25rem 0.75rem; background: #3b82f6; color: white; border-radius: 9999px; font-size: 0.875rem;">
|
||||
${worker.worker_name}
|
||||
@@ -2053,7 +2053,7 @@ window.updateSelectedWorkers = updateSelectedWorkers;
|
||||
|
||||
// 작업자 제거
|
||||
function removeWorker(workerId) {
|
||||
const checkbox = document.querySelector(`.worker-checkbox[data-worker-id="${workerId}"]`);
|
||||
const checkbox = document.querySelector(`.worker-checkbox[data-user-id="${workerId}"]`);
|
||||
if (checkbox) {
|
||||
checkbox.checked = false;
|
||||
updateSelectedWorkers();
|
||||
@@ -2094,7 +2094,7 @@ async function saveTeamComposition() {
|
||||
}
|
||||
|
||||
const members = Array.from(selectedWorkers).map(workerId => ({
|
||||
worker_id: workerId
|
||||
user_id: workerId
|
||||
}));
|
||||
|
||||
try {
|
||||
@@ -2427,7 +2427,7 @@ async function completeTbmSession() {
|
||||
}
|
||||
|
||||
attendanceData.push({
|
||||
worker_id: completeModalTeam[i].worker_id,
|
||||
user_id: completeModalTeam[i].user_id,
|
||||
attendance_type: type,
|
||||
attendance_hours: hours
|
||||
});
|
||||
@@ -2527,15 +2527,15 @@ async function viewTbmSession(sessionId) {
|
||||
// 작업자별로 그룹화
|
||||
const workerMap = new Map();
|
||||
team.forEach(member => {
|
||||
if (!workerMap.has(member.worker_id)) {
|
||||
workerMap.set(member.worker_id, {
|
||||
if (!workerMap.has(member.user_id)) {
|
||||
workerMap.set(member.user_id, {
|
||||
worker_name: member.worker_name,
|
||||
job_type: member.job_type,
|
||||
is_present: member.is_present,
|
||||
tasks: []
|
||||
});
|
||||
}
|
||||
workerMap.get(member.worker_id).tasks.push(member);
|
||||
workerMap.get(member.user_id).tasks.push(member);
|
||||
});
|
||||
|
||||
teamContainer.style.display = 'flex';
|
||||
@@ -2692,12 +2692,12 @@ async function openHandoverModal(sessionId) {
|
||||
const toLeaderSelect = document.getElementById('toLeaderId');
|
||||
const otherLeaders = allWorkers.filter(w =>
|
||||
(w.job_type === 'leader' || w.job_type === '그룹장' || w.job_type === 'admin') &&
|
||||
w.worker_id !== session.leader_id
|
||||
w.user_id !== session.leader_user_id
|
||||
);
|
||||
|
||||
toLeaderSelect.innerHTML = '<option value="">인수자 선택...</option>' +
|
||||
otherLeaders.map(w => `
|
||||
<option value="${w.worker_id}">${w.worker_name} (${w.job_type || ''})</option>
|
||||
<option value="${w.user_id}">${w.worker_name} (${w.job_type || ''})</option>
|
||||
`).join('');
|
||||
|
||||
// 인계할 팀원 목록
|
||||
@@ -2710,7 +2710,7 @@ async function openHandoverModal(sessionId) {
|
||||
onmouseover="this.style.background='#f9fafb'" onmouseout="this.style.background='white'">
|
||||
<input type="checkbox"
|
||||
class="handover-worker-checkbox"
|
||||
value="${member.worker_id}"
|
||||
value="${member.user_id}"
|
||||
checked
|
||||
style="width: 16px; height: 16px; cursor: pointer;">
|
||||
<span style="font-weight: 500; font-size: 0.875rem;">${member.worker_name}</span>
|
||||
@@ -2771,9 +2771,9 @@ async function saveHandover() {
|
||||
}
|
||||
|
||||
try {
|
||||
// 세션 정보 조회 (from_leader_id 가져오기)
|
||||
// 세션 정보 조회 (from_leader_user_id 가져오기)
|
||||
const sessionData = await window.TbmAPI.getSession(sessionId);
|
||||
const fromLeaderId = sessionData?.leader_id;
|
||||
const fromLeaderId = sessionData?.leader_user_id;
|
||||
|
||||
if (!fromLeaderId) {
|
||||
showToast('세션 정보를 찾을 수 없습니다.', 'error');
|
||||
@@ -2782,13 +2782,13 @@ async function saveHandover() {
|
||||
|
||||
const handoverData = {
|
||||
session_id: sessionId,
|
||||
from_leader_id: fromLeaderId,
|
||||
to_leader_id: toLeaderId,
|
||||
from_leader_user_id: fromLeaderId,
|
||||
to_leader_user_id: toLeaderId,
|
||||
handover_date: handoverDate,
|
||||
handover_time: handoverTime,
|
||||
reason: reason,
|
||||
handover_notes: handoverNotes,
|
||||
worker_ids: workerIds
|
||||
user_ids: workerIds
|
||||
};
|
||||
|
||||
const response = await window.TbmAPI.saveHandover(handoverData);
|
||||
@@ -2855,12 +2855,12 @@ async function executeSplit(memberIdx) {
|
||||
}
|
||||
try {
|
||||
await window.TbmAPI.updateTeamMember(splitModalSessionId, {
|
||||
worker_id: m.worker_id, project_id: m.project_id, work_type_id: m.work_type_id,
|
||||
user_id: m.user_id, project_id: m.project_id, work_type_id: m.work_type_id,
|
||||
task_id: m.task_id, workplace_category_id: m.workplace_category_id, workplace_id: m.workplace_id,
|
||||
work_detail: m.work_detail, is_present: true, work_hours: splitHours
|
||||
});
|
||||
await window.TbmAPI.splitAssignment(splitModalSessionId, {
|
||||
worker_id: m.worker_id, work_hours: currentHours - splitHours,
|
||||
user_id: m.user_id, work_hours: currentHours - splitHours,
|
||||
project_id: m.project_id, work_type_id: m.work_type_id
|
||||
});
|
||||
showToast(`${escapeHtml(m.worker_name)} 분할 완료: ${splitHours}h + ${currentHours - splitHours}h`, 'success');
|
||||
@@ -2934,8 +2934,8 @@ async function togglePullSessionMembers(sessionId, el) {
|
||||
<div style="display:flex; align-items:center; justify-content:space-between; padding:0.375rem 0; border-bottom:1px solid #f9fafb;">
|
||||
<span>${escapeHtml(m.worker_name)} <span style="font-size:0.75rem; color:#6b7280;">(${hours}h)</span></span>
|
||||
<div style="display:flex; gap:0.25rem; align-items:center;">
|
||||
<input type="number" id="pull_h_${sessionId}_${m.worker_id}" step="0.5" min="0.5" max="${hours}" value="${hours}" style="width:60px; padding:0.25rem; border:1px solid #d1d5db; border-radius:0.25rem; font-size:0.75rem;">
|
||||
<button type="button" class="tbm-btn tbm-btn-primary" style="padding:0.25rem 0.5rem; font-size:0.75rem;" onclick="executePull(${sessionId}, ${m.worker_id}, '${escapeHtml(m.worker_name)}')">빼오기</button>
|
||||
<input type="number" id="pull_h_${sessionId}_${m.user_id}" step="0.5" min="0.5" max="${hours}" value="${hours}" style="width:60px; padding:0.25rem; border:1px solid #d1d5db; border-radius:0.25rem; font-size:0.75rem;">
|
||||
<button type="button" class="tbm-btn tbm-btn-primary" style="padding:0.25rem 0.5rem; font-size:0.75rem;" onclick="executePull(${sessionId}, ${m.user_id}, '${escapeHtml(m.worker_name)}')">빼오기</button>
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('') || '<div style="color:#9ca3af; padding:0.25rem;">팀원 없음</div>';
|
||||
@@ -2954,7 +2954,7 @@ async function executePull(sourceSessionId, workerId, workerName) {
|
||||
try {
|
||||
const res = await window.TbmAPI.transfer({
|
||||
transfer_type: 'pull',
|
||||
worker_id: workerId,
|
||||
user_id: workerId,
|
||||
source_session_id: sourceSessionId,
|
||||
dest_session_id: pullModalSessionId,
|
||||
hours: hours
|
||||
|
||||
@@ -18,7 +18,7 @@ class TbmAPI {
|
||||
// 현재 로그인한 사용자 정보 가져오기
|
||||
const userInfo = JSON.parse(localStorage.getItem('sso_user') || '{}');
|
||||
this.state.currentUser = userInfo;
|
||||
console.log('👤 로그인 사용자:', this.state.currentUser, 'worker_id:', this.state.currentUser?.worker_id);
|
||||
console.log('👤 로그인 사용자:', this.state.currentUser, 'user_id:', this.state.currentUser?.user_id);
|
||||
|
||||
// 병렬로 데이터 로드
|
||||
await Promise.all([
|
||||
|
||||
@@ -77,7 +77,7 @@ class TbmState extends BaseState {
|
||||
*/
|
||||
addWorkerToList(worker) {
|
||||
this.workerTaskList.push({
|
||||
worker_id: worker.worker_id,
|
||||
user_id: worker.user_id,
|
||||
worker_name: worker.worker_name,
|
||||
job_type: worker.job_type,
|
||||
tasks: [this.createEmptyTaskLine()]
|
||||
|
||||
@@ -81,7 +81,7 @@ async function loadWorkers() {
|
||||
const selectWorker = document.getElementById('individualWorker');
|
||||
workers.forEach(worker => {
|
||||
const option = document.createElement('option');
|
||||
option.value = worker.worker_id;
|
||||
option.value = worker.user_id;
|
||||
option.textContent = `${worker.worker_name} (${worker.employment_status === 'employed' ? '재직' : '퇴사'})`;
|
||||
selectWorker.appendChild(option);
|
||||
});
|
||||
@@ -293,7 +293,7 @@ async function autoCalculateAnnualLeave() {
|
||||
}
|
||||
|
||||
// 작업자의 입사일 조회
|
||||
const worker = workers.find(w => w.worker_id == workerId);
|
||||
const worker = workers.find(w => w.user_id == workerId);
|
||||
if (!worker || !worker.hire_date) {
|
||||
showToast('작업자의 입사일 정보가 없습니다', 'error');
|
||||
return;
|
||||
@@ -308,7 +308,7 @@ async function autoCalculateAnnualLeave() {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
worker_id: workerId,
|
||||
user_id: workerId,
|
||||
hire_date: worker.hire_date,
|
||||
year: year
|
||||
})
|
||||
@@ -369,7 +369,7 @@ async function submitIndividualVacation() {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
worker_id: workerId,
|
||||
user_id: workerId,
|
||||
vacation_type_id: typeId,
|
||||
year: year,
|
||||
total_days: parseFloat(totalDays),
|
||||
@@ -520,7 +520,7 @@ async function previewBulkAllocation() {
|
||||
const hireDate = worker.hire_date;
|
||||
if (!hireDate) {
|
||||
return {
|
||||
worker_id: worker.worker_id,
|
||||
user_id: worker.user_id,
|
||||
worker_name: worker.worker_name,
|
||||
hire_date: '-',
|
||||
years_worked: '-',
|
||||
@@ -534,7 +534,7 @@ async function previewBulkAllocation() {
|
||||
const yearsWorked = calculateYearsWorked(hireDate, year);
|
||||
|
||||
return {
|
||||
worker_id: worker.worker_id,
|
||||
user_id: worker.user_id,
|
||||
worker_name: worker.worker_name,
|
||||
hire_date: hireDate,
|
||||
years_worked: yearsWorked,
|
||||
@@ -656,7 +656,7 @@ async function submitBulkAllocation() {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
worker_id: item.worker_id,
|
||||
user_id: item.user_id,
|
||||
hire_date: item.hire_date,
|
||||
year: year
|
||||
})
|
||||
|
||||
@@ -445,7 +445,7 @@ class WorkAnalysisTableRenderer {
|
||||
return workerData.map(worker => {
|
||||
// 해당 작업자의 작업 데이터 필터링
|
||||
const workerWork = recentWorkData ?
|
||||
recentWorkData.filter(work => work.worker_id === worker.worker_id) : [];
|
||||
recentWorkData.filter(work => work.user_id === worker.user_id) : [];
|
||||
|
||||
// 프로젝트별로 그룹화
|
||||
const projectMap = new Map();
|
||||
|
||||
@@ -47,7 +47,7 @@ async function loadReports() {
|
||||
return;
|
||||
}
|
||||
|
||||
const nameMap = Object.fromEntries(workers.map(w => [w.worker_id, w.worker_name]));
|
||||
const nameMap = Object.fromEntries(workers.map(w => [w.user_id, w.worker_name]));
|
||||
const projMap = Object.fromEntries(projects.map(p => [p.project_id, p.project_name]));
|
||||
// const taskMap = Object.fromEntries(tasks.map(t => [t.task_id, `${t.category}:${t.subcategory}`])); // tasks 테이블 삭제됨
|
||||
|
||||
@@ -56,7 +56,7 @@ async function loadReports() {
|
||||
const tr = document.createElement('tr');
|
||||
tr.innerHTML = `
|
||||
<td>${i + 1}</td>
|
||||
<td>${nameMap[r.worker_id] || r.worker_id}</td>
|
||||
<td>${nameMap[r.user_id] || r.user_id}</td>
|
||||
<td><select data-id="project">
|
||||
${projects.map(p =>
|
||||
`<option value="${p.project_id}" ${p.project_id === r.project_id ? 'selected' : ''}>${p.project_name}</option>`
|
||||
@@ -107,7 +107,7 @@ async function loadReports() {
|
||||
|
||||
const payload = {
|
||||
date: formatDate(r.date), // 날짜 형식 변환
|
||||
worker_id: r.worker_id, // 기존 작업자 ID 유지
|
||||
user_id: r.user_id, // 기존 작업자 ID 유지
|
||||
project_id: Number(projectId),
|
||||
task_id: Number(taskId),
|
||||
overtime_hours: overtimeHours ? Number(overtimeHours) : null,
|
||||
|
||||
@@ -142,8 +142,8 @@ class WorkReportReviewManager {
|
||||
}
|
||||
|
||||
dummyAttendance.push({
|
||||
id: `att_${worker.worker_id}_${dateStr}`,
|
||||
worker_id: worker.worker_id,
|
||||
id: `att_${worker.user_id}_${dateStr}`,
|
||||
user_id: worker.user_id,
|
||||
worker_name: worker.worker_name,
|
||||
date: dateStr,
|
||||
attendance_type: attendanceType,
|
||||
@@ -196,7 +196,7 @@ class WorkReportReviewManager {
|
||||
|
||||
if (startDate) params.append('start_date', startDate);
|
||||
if (endDate) params.append('end_date', endDate);
|
||||
if (workerId) params.append('worker_id', workerId);
|
||||
if (workerId) params.append('user_id', workerId);
|
||||
if (projectId) params.append('project_id', projectId);
|
||||
|
||||
// 페이지네이션 (일단 많이 가져오기)
|
||||
@@ -250,26 +250,26 @@ class WorkReportReviewManager {
|
||||
// 각 보고서에 대해 근무시간 검증
|
||||
this.reports.forEach(report => {
|
||||
const attendance = this.attendanceData.find(att =>
|
||||
att.worker_id === report.worker_id && att.date === report.report_date
|
||||
att.user_id === report.user_id && att.date === report.report_date
|
||||
);
|
||||
|
||||
if (attendance) {
|
||||
const expectedHours = attendance.expected_hours;
|
||||
const actualHours = this.getWorkerDailyHours(report.worker_id, report.report_date);
|
||||
|
||||
const actualHours = this.getWorkerDailyHours(report.user_id, report.report_date);
|
||||
|
||||
report.expected_hours = expectedHours;
|
||||
report.actual_hours = actualHours;
|
||||
report.attendance_type = attendance.attendance_type;
|
||||
report.hours_status = this.getHoursStatus(actualHours, expectedHours);
|
||||
report.is_reviewed = this.reviewedReports.has(`${report.worker_id}_${report.report_date}`);
|
||||
report.is_reviewed = this.reviewedReports.has(`${report.user_id}_${report.report_date}`);
|
||||
} else {
|
||||
// 휴가 정보가 없으면 기본 8시간으로 가정
|
||||
const actualHours = this.getWorkerDailyHours(report.worker_id, report.report_date);
|
||||
const actualHours = this.getWorkerDailyHours(report.user_id, report.report_date);
|
||||
report.expected_hours = 8;
|
||||
report.actual_hours = actualHours;
|
||||
report.attendance_type = 'NORMAL';
|
||||
report.hours_status = this.getHoursStatus(actualHours, 8);
|
||||
report.is_reviewed = this.reviewedReports.has(`${report.worker_id}_${report.report_date}`);
|
||||
report.is_reviewed = this.reviewedReports.has(`${report.user_id}_${report.report_date}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -277,7 +277,7 @@ class WorkReportReviewManager {
|
||||
getWorkerDailyHours(workerId, date) {
|
||||
// 해당 작업자의 특정 날짜 총 작업시간 계산
|
||||
return this.reports
|
||||
.filter(r => r.worker_id === workerId && r.report_date === date)
|
||||
.filter(r => r.user_id === workerId && r.report_date === date)
|
||||
.reduce((sum, r) => sum + (r.work_hours || 0), 0);
|
||||
}
|
||||
|
||||
@@ -315,9 +315,9 @@ class WorkReportReviewManager {
|
||||
(Math.random() * 6 + 2); // 정상 시간 (2-8시간)
|
||||
|
||||
dummyData.push({
|
||||
id: 1000000 + i * 100 + worker.worker_id * 10 + j, // 고유 정수 ID
|
||||
id: 1000000 + i * 100 + worker.user_id * 10 + j, // 고유 정수 ID
|
||||
report_date: dateStr,
|
||||
worker_id: worker.worker_id,
|
||||
user_id: worker.user_id,
|
||||
worker_name: worker.worker_name,
|
||||
project_id: this.projects[Math.floor(Math.random() * this.projects.length)]?.project_id || 1,
|
||||
project_name: this.projects[Math.floor(Math.random() * this.projects.length)]?.project_name || 'Unknown',
|
||||
@@ -344,7 +344,7 @@ class WorkReportReviewManager {
|
||||
if (workerSelect) {
|
||||
workerSelect.innerHTML = '<option value="">전체 작업자</option>';
|
||||
this.workers.forEach(worker => {
|
||||
workerSelect.innerHTML += `<option value="${worker.worker_id}">${worker.worker_name}</option>`;
|
||||
workerSelect.innerHTML += `<option value="${worker.user_id}">${worker.worker_name}</option>`;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -540,7 +540,7 @@ class WorkReportReviewManager {
|
||||
const grouped = {};
|
||||
|
||||
this.filteredReports.forEach(report => {
|
||||
const key = `${report.worker_id}_${report.report_date}`;
|
||||
const key = `${report.user_id}_${report.report_date}`;
|
||||
if (!grouped[key]) {
|
||||
grouped[key] = [];
|
||||
}
|
||||
@@ -569,7 +569,7 @@ class WorkReportReviewManager {
|
||||
selectWorkerDate(workerId, date) {
|
||||
// 해당 작업자의 특정 날짜 보고서들을 선택
|
||||
const reports = this.filteredReports.filter(r =>
|
||||
r.worker_id == workerId && r.report_date === date
|
||||
r.user_id == workerId && r.report_date === date
|
||||
);
|
||||
|
||||
if (reports.length > 0) {
|
||||
@@ -600,7 +600,7 @@ class WorkReportReviewManager {
|
||||
// const response = await fetch(`${API}/daily-work-reports/review`, {
|
||||
// method: 'POST',
|
||||
// headers: { ...getAuthHeaders(), 'Content-Type': 'application/json' },
|
||||
// body: JSON.stringify({ worker_id: workerId, report_date: date, reviewed: true })
|
||||
// body: JSON.stringify({ user_id: workerId, report_date: date, reviewed: true })
|
||||
// });
|
||||
|
||||
// 현재는 로컬 상태만 업데이트
|
||||
@@ -608,7 +608,7 @@ class WorkReportReviewManager {
|
||||
|
||||
// 해당 보고서들의 검토 상태 업데이트
|
||||
this.reports.forEach(report => {
|
||||
if (report.worker_id == workerId && report.report_date === date) {
|
||||
if (report.user_id == workerId && report.report_date === date) {
|
||||
report.is_reviewed = true;
|
||||
}
|
||||
});
|
||||
@@ -627,7 +627,7 @@ class WorkReportReviewManager {
|
||||
}
|
||||
|
||||
getWorkerName(workerId) {
|
||||
const worker = this.workers.find(w => w.worker_id == workerId);
|
||||
const worker = this.workers.find(w => w.user_id == workerId);
|
||||
return worker ? worker.worker_name : `작업자${workerId}`;
|
||||
}
|
||||
|
||||
@@ -680,7 +680,7 @@ class WorkReportReviewManager {
|
||||
|
||||
<div class="panel-actions">
|
||||
${!report.is_reviewed && report.hours_status === 'NORMAL' && !allReports.some(r => r.work_status_id === 2) ?
|
||||
`<button type="button" class="panel-btn save" onclick="workReportReview.markAsReviewed('${report.worker_id}', '${report.report_date}')">
|
||||
`<button type="button" class="panel-btn save" onclick="workReportReview.markAsReviewed('${report.user_id}', '${report.report_date}')">
|
||||
✅ 검토완료 처리
|
||||
</button>` : ''
|
||||
}
|
||||
|
||||
@@ -179,7 +179,7 @@ function processDayData(dateStr, works) {
|
||||
|
||||
works.forEach(work => {
|
||||
dayData.totalHours += parseFloat(work.work_hours || 0);
|
||||
dayData.workers.add(work.worker_name || work.worker_id);
|
||||
dayData.workers.add(work.worker_name || work.user_id);
|
||||
});
|
||||
|
||||
const workType = classifyWorkType(dayData.totalHours);
|
||||
@@ -217,7 +217,7 @@ function renderDayInfo() {
|
||||
|
||||
// 작업자별 상세 정보 생성
|
||||
const workerDetailsHtml = Array.from(data.workers).map(worker => {
|
||||
const workerWorks = data.details.filter(w => (w.worker_name || w.worker_id) === worker);
|
||||
const workerWorks = data.details.filter(w => (w.worker_name || w.user_id) === worker);
|
||||
const workerHours = workerWorks.reduce((sum, w) => sum + parseFloat(w.work_hours || 0), 0);
|
||||
|
||||
const workerWorkItemsHtml = workerWorks.map(work => `
|
||||
@@ -563,7 +563,7 @@ async function deleteWorkerAllWorks(date, workerName) {
|
||||
try {
|
||||
if (!selectedDateData) return;
|
||||
|
||||
const workerWorks = selectedDateData.details.filter(w => (w.worker_name || w.worker_id) === workerName);
|
||||
const workerWorks = selectedDateData.details.filter(w => (w.worker_name || w.user_id) === workerName);
|
||||
|
||||
if (workerWorks.length === 0) {
|
||||
showMessage('삭제할 작업이 없습니다.', 'error');
|
||||
|
||||
@@ -15,7 +15,7 @@ let existingWork = [];
|
||||
function getUrlParams() {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
return {
|
||||
worker_id: urlParams.get('worker_id'),
|
||||
user_id: urlParams.get('user_id'),
|
||||
worker_name: decodeURIComponent(urlParams.get('worker_name') || ''),
|
||||
date: urlParams.get('date') || new Date().toISOString().split('T')[0]
|
||||
};
|
||||
@@ -88,7 +88,7 @@ async function initializePage() {
|
||||
|
||||
// URL 파라미터 추출
|
||||
const params = getUrlParams();
|
||||
currentWorkerId = parseInt(params.worker_id);
|
||||
currentWorkerId = parseInt(params.user_id);
|
||||
currentWorkerName = params.worker_name;
|
||||
selectedDate = params.date;
|
||||
|
||||
@@ -189,7 +189,7 @@ async function loadWorkerInfo() {
|
||||
|
||||
async function loadExistingWork() {
|
||||
try {
|
||||
const response = await window.apiCall(`/daily-work-reports?date=${selectedDate}&worker_id=${currentWorkerId}`);
|
||||
const response = await window.apiCall(`/daily-work-reports?date=${selectedDate}&user_id=${currentWorkerId}`);
|
||||
existingWork = Array.isArray(response) ? response : (response.data || []);
|
||||
console.log(`✅ 기존 작업 ${existingWork.length}건 로드 완료`);
|
||||
} catch (error) {
|
||||
@@ -398,7 +398,7 @@ async function saveNewWork() {
|
||||
|
||||
const workData = {
|
||||
report_date: selectedDate,
|
||||
worker_id: currentWorkerId,
|
||||
user_id: currentWorkerId,
|
||||
project_id: parseInt(projectId),
|
||||
work_type_id: parseInt(workTypeId),
|
||||
work_status_id: parseInt(workStatusId),
|
||||
@@ -477,7 +477,7 @@ async function handleVacationProcess(vacationType) {
|
||||
// 휴가용 작업 보고서 생성
|
||||
const vacationWork = {
|
||||
report_date: selectedDate,
|
||||
worker_id: currentWorkerId,
|
||||
user_id: currentWorkerId,
|
||||
project_id: 1, // 기본 프로젝트 (휴가용)
|
||||
work_type_id: 999, // 휴가 전용 작업 유형 (DB에 추가 필요)
|
||||
work_status_id: 1, // 정상 상태
|
||||
|
||||
@@ -331,7 +331,7 @@ function renderWorkerList() {
|
||||
statusText = '사무직';
|
||||
}
|
||||
|
||||
const safeWorkerId = parseInt(worker.worker_id) || 0;
|
||||
const safeWorkerId = parseInt(worker.user_id) || 0;
|
||||
const safeWorkerName = escapeHtml(worker.worker_name || '');
|
||||
const firstChar = safeWorkerName ? safeWorkerName.charAt(0) : '?';
|
||||
|
||||
@@ -406,7 +406,7 @@ function openWorkerModal(workerId = null) {
|
||||
}
|
||||
|
||||
if (workerId) {
|
||||
const worker = allWorkers.find(w => w.worker_id === workerId);
|
||||
const worker = allWorkers.find(w => w.user_id === workerId);
|
||||
if (!worker) {
|
||||
showToast('작업자를 찾을 수 없습니다.', 'error');
|
||||
return;
|
||||
@@ -416,7 +416,7 @@ function openWorkerModal(workerId = null) {
|
||||
title.textContent = '작업자 정보 수정';
|
||||
deleteBtn.style.display = 'inline-flex';
|
||||
|
||||
document.getElementById('workerId').value = worker.worker_id;
|
||||
document.getElementById('workerId').value = worker.user_id;
|
||||
document.getElementById('workerName').value = worker.worker_name || '';
|
||||
document.getElementById('jobType').value = worker.job_type || 'worker';
|
||||
document.getElementById('joinDate').value = worker.join_date ? worker.join_date.split('T')[0] : '';
|
||||
@@ -508,7 +508,7 @@ async function saveWorker() {
|
||||
|
||||
// 작업자 삭제 확인
|
||||
function confirmDeleteWorker(workerId) {
|
||||
const worker = allWorkers.find(w => w.worker_id === workerId);
|
||||
const worker = allWorkers.find(w => w.user_id === workerId);
|
||||
if (!worker) {
|
||||
showToast('작업자를 찾을 수 없습니다.', 'error');
|
||||
return;
|
||||
@@ -524,7 +524,7 @@ function confirmDeleteWorker(workerId) {
|
||||
// 작업자 삭제 (모달에서)
|
||||
function deleteWorker() {
|
||||
if (currentEditingWorker) {
|
||||
confirmDeleteWorker(currentEditingWorker.worker_id);
|
||||
confirmDeleteWorker(currentEditingWorker.user_id);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -271,7 +271,7 @@
|
||||
const select = document.getElementById('workerFilter');
|
||||
workers.forEach(worker => {
|
||||
const option = document.createElement('option');
|
||||
option.value = worker.worker_id;
|
||||
option.value = worker.user_id;
|
||||
option.textContent = worker.worker_name;
|
||||
select.appendChild(option);
|
||||
});
|
||||
@@ -297,7 +297,7 @@
|
||||
params: {
|
||||
start_date: startDate,
|
||||
end_date: endDate,
|
||||
worker_id: workerId || undefined
|
||||
user_id: workerId || undefined
|
||||
}
|
||||
});
|
||||
|
||||
@@ -306,7 +306,7 @@
|
||||
params: {
|
||||
start_date: startDate,
|
||||
end_date: endDate,
|
||||
worker_id: workerId || undefined
|
||||
user_id: workerId || undefined
|
||||
}
|
||||
});
|
||||
|
||||
@@ -340,10 +340,10 @@
|
||||
|
||||
// 출퇴근 기록 맵핑
|
||||
attendanceRecords.forEach(record => {
|
||||
const key = `${record.attendance_date}_${record.worker_id}`;
|
||||
const key = `${record.attendance_date}_${record.user_id}`;
|
||||
dateWorkerMap.set(key, {
|
||||
date: record.attendance_date,
|
||||
worker_id: record.worker_id,
|
||||
user_id: record.user_id,
|
||||
worker_name: record.worker_name,
|
||||
attendance: record,
|
||||
reports: []
|
||||
@@ -352,13 +352,13 @@
|
||||
|
||||
// 작업 보고서 맵핑
|
||||
workReports.forEach(report => {
|
||||
const key = `${report.report_date}_${report.worker_id}`;
|
||||
const key = `${report.report_date}_${report.user_id}`;
|
||||
if (dateWorkerMap.has(key)) {
|
||||
dateWorkerMap.get(key).reports.push(report);
|
||||
} else {
|
||||
dateWorkerMap.set(key, {
|
||||
date: report.report_date,
|
||||
worker_id: report.worker_id,
|
||||
user_id: report.user_id,
|
||||
worker_name: report.worker_name,
|
||||
attendance: null,
|
||||
reports: [report]
|
||||
|
||||
@@ -499,7 +499,7 @@
|
||||
function populateAssigneeDropdown() {
|
||||
const select = document.getElementById('receiveAssignee');
|
||||
select.innerHTML = '<option value="">담당자 선택</option>' +
|
||||
workers.map(w => `<option value="${w.worker_id}">${w.worker_name} (${w.job_type || ''})</option>`).join('');
|
||||
workers.map(w => `<option value="${w.user_id}">${w.worker_name} (${w.job_type || ''})</option>`).join('');
|
||||
}
|
||||
|
||||
async function loadRepairRequests() {
|
||||
|
||||
@@ -392,7 +392,7 @@
|
||||
// 데이터 정리
|
||||
vacationData = {};
|
||||
workers.forEach(w => {
|
||||
vacationData[w.worker_id] = {
|
||||
vacationData[w.user_id] = {
|
||||
carryover: 0,
|
||||
annual: 0,
|
||||
longService: 0,
|
||||
@@ -403,9 +403,9 @@
|
||||
|
||||
// 잔액 데이터 매핑
|
||||
balances.forEach(b => {
|
||||
if (!vacationData[b.worker_id]) return;
|
||||
if (!vacationData[b.user_id]) return;
|
||||
const code = b.type_code || '';
|
||||
const data = vacationData[b.worker_id];
|
||||
const data = vacationData[b.user_id];
|
||||
|
||||
if (code === 'CARRYOVER' || b.type_name === '이월') {
|
||||
data.carryover = b.total_days || 0;
|
||||
@@ -443,7 +443,7 @@
|
||||
}
|
||||
|
||||
tbody.innerHTML = workers.map((w, idx) => {
|
||||
const d = vacationData[w.worker_id] || { carryover: 0, annual: 0, longService: 0, specials: [], totalUsed: 0 };
|
||||
const d = vacationData[w.user_id] || { carryover: 0, annual: 0, longService: 0, specials: [], totalUsed: 0 };
|
||||
const carryover = parseFloat(d.carryover) || 0;
|
||||
const annual = parseFloat(d.annual) || 0;
|
||||
const longService = parseFloat(d.longService) || 0;
|
||||
@@ -454,29 +454,29 @@
|
||||
const remainingClass = remaining > 0 ? 'positive' : remaining < 0 ? 'negative' : 'zero';
|
||||
|
||||
return `
|
||||
<tr data-worker-id="${w.worker_id}">
|
||||
<tr data-user-id="${w.user_id}">
|
||||
<td>${idx + 1}</td>
|
||||
<td class="worker-name">${w.worker_name}</td>
|
||||
<td>
|
||||
<input type="number" class="num-input ${carryover < 0 ? 'negative' : ''}"
|
||||
value="${carryover}" step="0.5"
|
||||
data-field="carryover"
|
||||
onchange="updateField(${w.worker_id}, 'carryover', this.value)">
|
||||
onchange="updateField(${w.user_id}, 'carryover', this.value)">
|
||||
</td>
|
||||
<td>
|
||||
<input type="number" class="num-input"
|
||||
value="${annual}" step="0.5"
|
||||
data-field="annual"
|
||||
onchange="updateField(${w.worker_id}, 'annual', this.value)">
|
||||
onchange="updateField(${w.user_id}, 'annual', this.value)">
|
||||
</td>
|
||||
<td>
|
||||
<input type="number" class="num-input"
|
||||
value="${longService}" step="0.5"
|
||||
data-field="longService"
|
||||
onchange="updateField(${w.worker_id}, 'longService', this.value)">
|
||||
onchange="updateField(${w.user_id}, 'longService', this.value)">
|
||||
</td>
|
||||
<td>
|
||||
<button class="special-btn" onclick="openSpecialModal(${w.worker_id}, '${w.worker_name}')">
|
||||
<button class="special-btn" onclick="openSpecialModal(${w.user_id}, '${w.worker_name}')">
|
||||
${(d.specials || []).length > 0 ? `${specialTotal}일` : '추가'}
|
||||
${(d.specials || []).length > 0 ? `<span class="special-count">${d.specials.length}</span>` : ''}
|
||||
</button>
|
||||
@@ -497,7 +497,7 @@
|
||||
vacationData[workerId][field] = val;
|
||||
|
||||
// 입력 스타일 업데이트
|
||||
const input = document.querySelector(`tr[data-worker-id="${workerId}"] input[data-field="${field}"]`);
|
||||
const input = document.querySelector(`tr[data-user-id="${workerId}"] input[data-field="${field}"]`);
|
||||
if (input) {
|
||||
input.classList.toggle('negative', val < 0);
|
||||
}
|
||||
@@ -508,7 +508,7 @@
|
||||
}
|
||||
|
||||
function updateRowTotals(workerId) {
|
||||
const row = document.querySelector(`tr[data-worker-id="${workerId}"]`);
|
||||
const row = document.querySelector(`tr[data-user-id="${workerId}"]`);
|
||||
if (!row) return;
|
||||
|
||||
const d = vacationData[workerId];
|
||||
@@ -668,12 +668,12 @@
|
||||
|
||||
// 데이터 수집
|
||||
for (const w of workers) {
|
||||
const d = vacationData[w.worker_id];
|
||||
const d = vacationData[w.user_id];
|
||||
if (!d) continue;
|
||||
|
||||
if (typeIdMap['CARRYOVER']) {
|
||||
balancesToSave.push({
|
||||
worker_id: w.worker_id,
|
||||
user_id: w.user_id,
|
||||
vacation_type_id: typeIdMap['CARRYOVER'],
|
||||
year: currentYear,
|
||||
total_days: d.carryover
|
||||
@@ -681,7 +681,7 @@
|
||||
}
|
||||
if (typeIdMap['ANNUAL']) {
|
||||
balancesToSave.push({
|
||||
worker_id: w.worker_id,
|
||||
user_id: w.user_id,
|
||||
vacation_type_id: typeIdMap['ANNUAL'],
|
||||
year: currentYear,
|
||||
total_days: d.annual
|
||||
@@ -689,7 +689,7 @@
|
||||
}
|
||||
if (typeIdMap['LONG_SERVICE']) {
|
||||
balancesToSave.push({
|
||||
worker_id: w.worker_id,
|
||||
user_id: w.user_id,
|
||||
vacation_type_id: typeIdMap['LONG_SERVICE'],
|
||||
year: currentYear,
|
||||
total_days: d.longService
|
||||
@@ -726,7 +726,7 @@
|
||||
}
|
||||
if (specialTypeId) {
|
||||
balancesToSave.push({
|
||||
worker_id: w.worker_id,
|
||||
user_id: w.user_id,
|
||||
vacation_type_id: specialTypeId,
|
||||
year: currentYear,
|
||||
total_days: special.days,
|
||||
|
||||
@@ -270,18 +270,18 @@
|
||||
|
||||
checkinStatus = {};
|
||||
workers.forEach(w => {
|
||||
const checkin = checkinList.find(c => c.worker_id === w.worker_id);
|
||||
const record = records.find(r => r.worker_id === w.worker_id);
|
||||
const checkin = checkinList.find(c => c.user_id === w.user_id);
|
||||
const record = records.find(r => r.user_id === w.user_id);
|
||||
|
||||
if (checkin?.vacation_status === 'approved' || record?.vacation_type_id) {
|
||||
checkinStatus[w.worker_id] = { status: 'vacation', vacationType: checkin?.vacation_type_name || record?.vacation_type_name || '연차' };
|
||||
checkinStatus[w.user_id] = { status: 'vacation', vacationType: checkin?.vacation_type_name || record?.vacation_type_name || '연차' };
|
||||
} else if (record && record.is_present === 0) {
|
||||
checkinStatus[w.worker_id] = { status: 'absent' };
|
||||
checkinStatus[w.user_id] = { status: 'absent' };
|
||||
} else if (record && record.is_present === 1) {
|
||||
checkinStatus[w.worker_id] = { status: 'present' };
|
||||
checkinStatus[w.user_id] = { status: 'present' };
|
||||
} else {
|
||||
// 기록이 없으면 기본 출근
|
||||
checkinStatus[w.worker_id] = { status: 'present' };
|
||||
checkinStatus[w.user_id] = { status: 'present' };
|
||||
}
|
||||
});
|
||||
|
||||
@@ -301,9 +301,9 @@
|
||||
}
|
||||
|
||||
container.innerHTML = workers.map(w => {
|
||||
const s = checkinStatus[w.worker_id] || { status: 'present' };
|
||||
const s = checkinStatus[w.user_id] || { status: 'present' };
|
||||
const label = s.status === 'present' ? '출근' : s.status === 'absent' ? '결근' : (s.vacationType || '연차');
|
||||
return `<span class="worker-chip ${s.status}" onclick="toggle(${w.worker_id})"><span class="chip-dot"></span>${w.worker_name} <small style="color:#6b7280">${label}</small></span>`;
|
||||
return `<span class="worker-chip ${s.status}" onclick="toggle(${w.user_id})"><span class="chip-dot"></span>${w.worker_name} <small style="color:#6b7280">${label}</small></span>`;
|
||||
}).join('');
|
||||
|
||||
updateSummary();
|
||||
@@ -318,8 +318,8 @@
|
||||
|
||||
function setAllPresent() {
|
||||
workers.forEach(w => {
|
||||
if (checkinStatus[w.worker_id]?.status !== 'vacation') {
|
||||
checkinStatus[w.worker_id] = { status: 'present' };
|
||||
if (checkinStatus[w.user_id]?.status !== 'vacation') {
|
||||
checkinStatus[w.user_id] = { status: 'present' };
|
||||
}
|
||||
});
|
||||
render();
|
||||
@@ -327,8 +327,8 @@
|
||||
|
||||
function setAllAbsent() {
|
||||
workers.forEach(w => {
|
||||
if (checkinStatus[w.worker_id]?.status !== 'vacation') {
|
||||
checkinStatus[w.worker_id] = { status: 'absent' };
|
||||
if (checkinStatus[w.user_id]?.status !== 'vacation') {
|
||||
checkinStatus[w.user_id] = { status: 'absent' };
|
||||
}
|
||||
});
|
||||
render();
|
||||
@@ -374,10 +374,10 @@
|
||||
|
||||
// 연차가 아닌 작업자들만 체크인 데이터로 전송
|
||||
const checkins = workers
|
||||
.filter(w => checkinStatus[w.worker_id]?.status !== 'vacation')
|
||||
.filter(w => checkinStatus[w.user_id]?.status !== 'vacation')
|
||||
.map(w => ({
|
||||
worker_id: w.worker_id,
|
||||
is_present: checkinStatus[w.worker_id]?.status === 'present'
|
||||
user_id: w.user_id,
|
||||
is_present: checkinStatus[w.user_id]?.status === 'present'
|
||||
}));
|
||||
|
||||
try {
|
||||
|
||||
@@ -177,7 +177,7 @@
|
||||
|
||||
// 체크인 목록을 기준으로 출퇴근 기록 생성 (연차 정보 포함)
|
||||
attendanceRecords = checkinList.map(worker => {
|
||||
const existingRecord = existingRecords.find(r => r.worker_id === worker.worker_id);
|
||||
const existingRecord = existingRecords.find(r => r.user_id === worker.user_id);
|
||||
const isOnVacation = worker.vacation_status === 'approved';
|
||||
|
||||
// 기존 기록이 있으면 사용, 없으면 초기화
|
||||
@@ -185,7 +185,7 @@
|
||||
return existingRecord;
|
||||
} else {
|
||||
return {
|
||||
worker_id: worker.worker_id,
|
||||
user_id: worker.user_id,
|
||||
worker_name: worker.worker_name,
|
||||
attendance_date: selectedDate,
|
||||
total_hours: isOnVacation ? 0 : 8,
|
||||
@@ -209,7 +209,7 @@
|
||||
function initializeAttendanceRecords() {
|
||||
const selectedDate = document.getElementById('selectedDate').value;
|
||||
attendanceRecords = workers.map(worker => ({
|
||||
worker_id: worker.worker_id,
|
||||
user_id: worker.user_id,
|
||||
worker_name: worker.worker_name,
|
||||
attendance_date: selectedDate,
|
||||
total_hours: 8,
|
||||
@@ -351,7 +351,7 @@
|
||||
|
||||
// 모든 기록을 API 형식에 맞게 변환
|
||||
const recordsToSave = attendanceRecords.map(record => ({
|
||||
worker_id: record.worker_id,
|
||||
user_id: record.user_id,
|
||||
attendance_date: selectedDate,
|
||||
total_hours: record.total_hours || 0,
|
||||
overtime_hours: record.overtime_hours || 0,
|
||||
@@ -371,7 +371,7 @@
|
||||
successCount++;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`작업자 ${data.worker_id} 저장 오류:`, error);
|
||||
console.error(`작업자 ${data.user_id} 저장 오류:`, error);
|
||||
errorCount++;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -662,7 +662,7 @@
|
||||
const records = response.data.data;
|
||||
// 작업자별로 데이터 정리
|
||||
records.forEach(record => {
|
||||
const workerId = record.worker_id;
|
||||
const workerId = record.user_id;
|
||||
// 날짜 형식 정규화 (다양한 형식 처리)
|
||||
let dateKey = record.record_date;
|
||||
if (dateKey) {
|
||||
@@ -686,7 +686,7 @@
|
||||
console.error('근태 데이터 조회 오류:', err);
|
||||
// 작업자별 빈 데이터 초기화
|
||||
workers.forEach(worker => {
|
||||
attendanceData[worker.worker_id] = {};
|
||||
attendanceData[worker.user_id] = {};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -765,7 +765,7 @@
|
||||
html += `<tbody>`;
|
||||
|
||||
workers.forEach((worker, index) => {
|
||||
const workerRecords = attendanceData[worker.worker_id] || {};
|
||||
const workerRecords = attendanceData[worker.user_id] || {};
|
||||
let totalOvertimeHours = 0;
|
||||
|
||||
// 입사일 파싱
|
||||
@@ -954,7 +954,7 @@
|
||||
let totalNormalDays = 0;
|
||||
|
||||
workers.forEach(worker => {
|
||||
const records = attendanceData[worker.worker_id] || {};
|
||||
const records = attendanceData[worker.user_id] || {};
|
||||
Object.values(records).forEach(record => {
|
||||
const hours = parseFloat(record.total_work_hours) || 0;
|
||||
totalWorkHours += hours;
|
||||
|
||||
@@ -306,13 +306,13 @@
|
||||
document.getElementById('adminControls').classList.add('visible');
|
||||
await loadWorkers();
|
||||
} else {
|
||||
// 일반 사용자: 본인 worker_id 사용
|
||||
if (currentUser?.worker_id) {
|
||||
currentWorkerId = currentUser.worker_id;
|
||||
// 일반 사용자: 본인 user_id 사용
|
||||
if (currentUser?.user_id) {
|
||||
currentWorkerId = currentUser.user_id;
|
||||
document.getElementById('infoGrid').style.display = 'grid';
|
||||
await loadAllData();
|
||||
} else {
|
||||
// worker_id가 없는 경우
|
||||
// user_id가 없는 경우
|
||||
document.getElementById('noWorkerMessage').style.display = 'block';
|
||||
}
|
||||
}
|
||||
@@ -352,7 +352,7 @@
|
||||
const select = document.getElementById('workerSelect');
|
||||
workers.forEach(w => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = w.worker_id;
|
||||
opt.value = w.user_id;
|
||||
opt.textContent = w.worker_name;
|
||||
select.appendChild(opt);
|
||||
});
|
||||
@@ -369,7 +369,7 @@
|
||||
}
|
||||
|
||||
currentWorkerId = parseInt(workerId);
|
||||
const worker = workers.find(w => w.worker_id === currentWorkerId);
|
||||
const worker = workers.find(w => w.user_id === currentWorkerId);
|
||||
document.getElementById('workerNameDisplay').textContent =
|
||||
`${worker?.worker_name || ''}님의 연차 잔여 현황 및 월간 연장근로 시간`;
|
||||
document.getElementById('infoGrid').style.display = 'grid';
|
||||
@@ -486,7 +486,7 @@
|
||||
|
||||
try {
|
||||
// 근태 기록에서 연장근로 데이터 조회
|
||||
const res = await axios.get(`/attendance/records?start_date=${startDate}&end_date=${endDate}&worker_id=${currentWorkerId}`);
|
||||
const res = await axios.get(`/attendance/records?start_date=${startDate}&end_date=${endDate}&user_id=${currentWorkerId}`);
|
||||
const records = res.data.data || [];
|
||||
|
||||
// 8시간 초과분 계산
|
||||
|
||||
@@ -222,7 +222,7 @@
|
||||
event.preventDefault();
|
||||
|
||||
const data = {
|
||||
worker_id: parseInt(document.getElementById('inputWorker').value),
|
||||
user_id: parseInt(document.getElementById('inputWorker').value),
|
||||
vacation_type_id: parseInt(document.getElementById('inputVacationType').value),
|
||||
start_date: document.getElementById('inputStartDate').value,
|
||||
end_date: document.getElementById('inputEndDate').value,
|
||||
|
||||
@@ -369,7 +369,7 @@
|
||||
event.preventDefault();
|
||||
|
||||
const data = {
|
||||
worker_id: parseInt(document.getElementById('inputWorker').value),
|
||||
user_id: parseInt(document.getElementById('inputWorker').value),
|
||||
vacation_type_id: parseInt(document.getElementById('inputVacationType').value),
|
||||
start_date: document.getElementById('inputStartDate').value,
|
||||
end_date: document.getElementById('inputEndDate').value,
|
||||
|
||||
@@ -155,7 +155,7 @@
|
||||
try {
|
||||
const currentUser = getCurrentUser();
|
||||
|
||||
if (!currentUser || !currentUser.worker_id) {
|
||||
if (!currentUser || !currentUser.user_id) {
|
||||
alert('작업자 정보가 없습니다. 관리자에게 문의하세요.');
|
||||
return;
|
||||
}
|
||||
@@ -179,7 +179,7 @@
|
||||
const currentUser = getCurrentUser();
|
||||
|
||||
try {
|
||||
const response = await axios.get(`/attendance/vacation-balance/${currentUser.worker_id}`);
|
||||
const response = await axios.get(`/attendance/vacation-balance/${currentUser.user_id}`);
|
||||
if (response.data.success) {
|
||||
renderVacationBalance(response.data.data);
|
||||
}
|
||||
@@ -222,7 +222,7 @@
|
||||
|
||||
const currentUser = getCurrentUser();
|
||||
const data = {
|
||||
worker_id: currentUser.worker_id,
|
||||
user_id: currentUser.user_id,
|
||||
vacation_type_id: parseInt(document.getElementById('vacationType').value),
|
||||
start_date: document.getElementById('startDate').value,
|
||||
end_date: document.getElementById('endDate').value,
|
||||
@@ -251,7 +251,7 @@
|
||||
if (response.data.success) {
|
||||
// 내 신청만 필터링
|
||||
const myRequests = response.data.data.filter(req =>
|
||||
req.requested_by === currentUser.user_id || req.worker_id === currentUser.worker_id
|
||||
req.requested_by === currentUser.user_id || req.user_id === currentUser.user_id
|
||||
);
|
||||
renderVacationRequests(myRequests, 'myRequestsList', true, 'delete');
|
||||
}
|
||||
|
||||
@@ -329,7 +329,7 @@
|
||||
|
||||
workStatus = {};
|
||||
workers.forEach(w => {
|
||||
const record = records.find(r => r.worker_id === w.worker_id);
|
||||
const record = records.find(r => r.user_id === w.user_id);
|
||||
|
||||
// 입사일 이전인지 확인
|
||||
const joinDate = w.join_date ? w.join_date.split('T')[0] : null;
|
||||
@@ -337,7 +337,7 @@
|
||||
|
||||
if (isBeforeJoin) {
|
||||
// 입사 전 날짜
|
||||
workStatus[w.worker_id] = {
|
||||
workStatus[w.user_id] = {
|
||||
isPresent: false,
|
||||
type: 'not_hired',
|
||||
hours: 0,
|
||||
@@ -396,7 +396,7 @@
|
||||
|
||||
const typeInfo = attendanceTypes.find(t => t.value === type);
|
||||
|
||||
workStatus[w.worker_id] = {
|
||||
workStatus[w.user_id] = {
|
||||
isPresent: record.is_present === 1 || typeInfo?.isLeave,
|
||||
type: type,
|
||||
hours: typeInfo !== undefined ? typeInfo.hours : 8,
|
||||
@@ -406,7 +406,7 @@
|
||||
};
|
||||
} else {
|
||||
// 출근 체크 기록이 없는 경우 - 결근 상태
|
||||
workStatus[w.worker_id] = {
|
||||
workStatus[w.user_id] = {
|
||||
isPresent: false,
|
||||
type: 'normal',
|
||||
hours: 8,
|
||||
@@ -435,7 +435,7 @@
|
||||
}
|
||||
|
||||
tbody.innerHTML = workers.map((w, idx) => {
|
||||
const s = workStatus[w.worker_id];
|
||||
const s = workStatus[w.user_id];
|
||||
|
||||
// 미입사 상태 처리
|
||||
if (s.isNotHired) {
|
||||
@@ -503,7 +503,7 @@
|
||||
${statusText}
|
||||
</td>
|
||||
<td>
|
||||
<select class="type-select" onchange="updateType(${w.worker_id}, this.value)">
|
||||
<select class="type-select" onchange="updateType(${w.user_id}, this.value)">
|
||||
${attendanceTypes.map(t => `
|
||||
<option value="${t.value}" ${s.type === t.value ? 'selected' : ''}>${t.label}</option>
|
||||
`).join('')}
|
||||
@@ -513,7 +513,7 @@
|
||||
<td class="hours-cell">
|
||||
${showOvertimeInput ? `
|
||||
<input type="number" class="overtime-input" value="${s.overtimeHours}" min="0" max="8" step="0.5"
|
||||
onchange="updateOvertime(${w.worker_id}, this.value)">
|
||||
onchange="updateOvertime(${w.user_id}, this.value)">
|
||||
` : '-'}
|
||||
</td>
|
||||
<td class="hours-cell"><strong>${totalHours}h</strong></td>
|
||||
@@ -551,9 +551,9 @@
|
||||
|
||||
function setAllNormal() {
|
||||
workers.forEach(w => {
|
||||
workStatus[w.worker_id].type = 'normal';
|
||||
workStatus[w.worker_id].hours = 8;
|
||||
workStatus[w.worker_id].overtimeHours = 0;
|
||||
workStatus[w.user_id].type = 'normal';
|
||||
workStatus[w.user_id].hours = 8;
|
||||
workStatus[w.user_id].overtimeHours = 0;
|
||||
});
|
||||
render();
|
||||
updateSummary();
|
||||
@@ -638,14 +638,14 @@
|
||||
|
||||
// 미입사자 제외하고 저장할 데이터 생성
|
||||
const recordsToSave = workers
|
||||
.filter(w => !workStatus[w.worker_id]?.isNotHired)
|
||||
.filter(w => !workStatus[w.user_id]?.isNotHired)
|
||||
.map(w => {
|
||||
const s = workStatus[w.worker_id];
|
||||
const s = workStatus[w.user_id];
|
||||
const totalHours = s.type === 'overtime' ? s.hours + s.overtimeHours : s.hours;
|
||||
|
||||
return {
|
||||
record_date: date,
|
||||
worker_id: w.worker_id,
|
||||
user_id: w.user_id,
|
||||
attendance_type_id: typeIdMap[s.type] || 1,
|
||||
vacation_type_id: vacationTypeIdMap[s.type] || null,
|
||||
total_work_hours: totalHours,
|
||||
@@ -674,8 +674,8 @@
|
||||
alert(`${ok}명 저장 완료`);
|
||||
isAlreadySaved = true;
|
||||
workers.forEach(w => {
|
||||
if (workStatus[w.worker_id]) {
|
||||
workStatus[w.worker_id].isSaved = true;
|
||||
if (workStatus[w.user_id]) {
|
||||
workStatus[w.user_id].isSaved = true;
|
||||
}
|
||||
});
|
||||
render();
|
||||
|
||||
Reference in New Issue
Block a user