백엔드: - proxyInputModel 전체 worker_id→user_id 전환 (작업보고서/휴가 매핑 실패 → 전부 미입력으로 표시되던 문제) 프론트: - 개별 입력 → 공통 입력 1개로 전환 프로젝트/공종/시간/부적합 한번 입력 → 선택된 전원에 적용 - 부서별 그룹핑 표시 - 적용 대상 칩 표시 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
243 lines
9.1 KiB
JavaScript
243 lines
9.1 KiB
JavaScript
/**
|
|
* 대리입력 + 일별 현황 모델
|
|
*/
|
|
const { getDb } = require('../dbPool');
|
|
|
|
const ProxyInputModel = {
|
|
/**
|
|
* 중복 배정 체크 (같은 날짜 + 같은 작업자)
|
|
*/
|
|
checkDuplicateAssignments: async (conn, sessionDate, userIds) => {
|
|
if (!userIds.length) return [];
|
|
const placeholders = userIds.map(() => '?').join(',');
|
|
const [rows] = await conn.query(`
|
|
SELECT ta.user_id, w.worker_name, ta.session_id
|
|
FROM tbm_team_assignments ta
|
|
JOIN tbm_sessions s ON ta.session_id = s.session_id
|
|
JOIN workers w ON ta.user_id = w.user_id
|
|
WHERE s.session_date = ? AND ta.user_id IN (${placeholders}) AND s.status != 'cancelled'
|
|
`, [sessionDate, ...userIds]);
|
|
return rows;
|
|
},
|
|
|
|
/**
|
|
* 작업자 존재 여부 체크
|
|
*/
|
|
validateWorkers: async (conn, userIds) => {
|
|
if (!userIds.length) return [];
|
|
const placeholders = userIds.map(() => '?').join(',');
|
|
const [rows] = await conn.query(`
|
|
SELECT user_id FROM workers WHERE user_id IN (${placeholders}) AND status = 'active'
|
|
`, [...userIds]);
|
|
return rows.map(r => r.user_id);
|
|
},
|
|
|
|
/**
|
|
* TBM 세션 생성 (대리입력)
|
|
*/
|
|
createProxySession: async (conn, data) => {
|
|
const [result] = await conn.query(`
|
|
INSERT INTO tbm_sessions (session_date, leader_user_id, status, is_proxy_input, proxy_input_by, created_by, safety_notes, work_location)
|
|
VALUES (?, ?, 'completed', 1, ?, ?, ?, ?)
|
|
`, [data.session_date, data.leader_id, data.proxy_input_by, data.created_by, data.safety_notes || '', data.work_location || '']);
|
|
return result;
|
|
},
|
|
|
|
/**
|
|
* 팀 배정 생성
|
|
*/
|
|
createTeamAssignment: async (conn, data) => {
|
|
const [result] = await conn.query(`
|
|
INSERT INTO tbm_team_assignments (session_id, user_id, project_id, work_type_id, task_id, workplace_id, work_hours, is_present)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, 1)
|
|
`, [data.session_id, data.user_id, data.project_id, data.work_type_id, data.task_id || null, data.workplace_id || null, data.work_hours]);
|
|
return result;
|
|
},
|
|
|
|
/**
|
|
* 작업보고서 생성 (accumulative)
|
|
*/
|
|
createWorkReport: async (conn, data) => {
|
|
const [result] = await conn.query(`
|
|
INSERT INTO daily_work_reports (report_date, user_id, project_id, work_type_id, task_id, work_status_id, work_hours, start_time, end_time, note, tbm_session_id, tbm_assignment_id, created_by, created_by_name, created_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW())
|
|
`, [data.report_date, data.user_id, data.project_id, data.work_type_id, data.task_id || null, data.work_status_id || 1, data.work_hours, data.start_time || null, data.end_time || null, data.note || '', data.tbm_session_id, data.tbm_assignment_id, data.created_by, data.created_by_name || '']);
|
|
return result;
|
|
},
|
|
|
|
/**
|
|
* 일별 현황 조회
|
|
*/
|
|
getDailyStatus: async (date) => {
|
|
const db = await getDb();
|
|
|
|
// 1. 활성 작업자
|
|
const [workers] = await db.query(`
|
|
SELECT w.user_id, w.worker_name, w.job_type,
|
|
COALESCE(d.department_name, '미배정') AS department_name
|
|
FROM workers w
|
|
LEFT JOIN departments d ON w.department_id = d.department_id
|
|
WHERE w.status = 'active' AND w.user_id IS NOT NULL
|
|
ORDER BY w.worker_name
|
|
`);
|
|
|
|
// 2. TBM 배정 현황
|
|
const [tbmAssignments] = await db.query(`
|
|
SELECT ta.user_id, ta.session_id, s.leader_user_id,
|
|
lu.worker_name AS leader_name, s.is_proxy_input
|
|
FROM tbm_team_assignments ta
|
|
JOIN tbm_sessions s ON ta.session_id = s.session_id
|
|
LEFT JOIN workers lu ON s.leader_user_id = lu.user_id
|
|
WHERE s.session_date = ? AND s.status != 'cancelled'
|
|
`, [date]);
|
|
|
|
// 3. 작업보고서 현황
|
|
const [reports] = await db.query(`
|
|
SELECT dwr.user_id, SUM(dwr.work_hours) AS total_hours, COUNT(*) AS entry_count
|
|
FROM daily_work_reports dwr
|
|
WHERE dwr.report_date = ?
|
|
GROUP BY dwr.user_id
|
|
`, [date]);
|
|
|
|
// 4. 해당 날짜의 연차 기록
|
|
const [vacationRecords] = await db.query(`
|
|
SELECT dar.user_id, dar.vacation_type_id,
|
|
vt.type_code AS vacation_type_code,
|
|
vt.type_name AS vacation_type_name,
|
|
vt.deduct_days
|
|
FROM daily_attendance_records dar
|
|
JOIN vacation_types vt ON dar.vacation_type_id = vt.id
|
|
WHERE dar.record_date = ? AND dar.vacation_type_id IS NOT NULL
|
|
`, [date]);
|
|
|
|
// 5. 해당 날짜가 회사 휴무일인지 확인
|
|
const [holidayRows] = await db.query(
|
|
`SELECT holiday_date, holiday_name FROM company_holidays WHERE holiday_date = ?`,
|
|
[date]
|
|
);
|
|
const isCompanyHoliday = holidayRows.length > 0;
|
|
const holidayName = isCompanyHoliday ? holidayRows[0].holiday_name : null;
|
|
const dateObj = new Date(date);
|
|
const isWeekend = dateObj.getDay() === 0 || dateObj.getDay() === 6;
|
|
|
|
// 메모리에서 조합
|
|
const tbmMap = {};
|
|
tbmAssignments.forEach(ta => {
|
|
if (!tbmMap[ta.user_id]) tbmMap[ta.user_id] = [];
|
|
tbmMap[ta.user_id].push(ta);
|
|
});
|
|
|
|
const reportMap = {};
|
|
reports.forEach(r => { reportMap[r.user_id] = r; });
|
|
|
|
const vacMap = {};
|
|
vacationRecords.forEach(v => { vacMap[v.user_id] = v; });
|
|
|
|
let tbmCompleted = 0, reportCompleted = 0, bothCompleted = 0, bothMissing = 0;
|
|
|
|
const workerList = workers.map(w => {
|
|
const hasTbm = !!tbmMap[w.user_id];
|
|
const hasReport = !!reportMap[w.user_id];
|
|
const tbmSessions = (tbmMap[w.user_id] || []).map(ta => ({
|
|
session_id: ta.session_id,
|
|
leader_name: ta.leader_name,
|
|
is_proxy_input: !!ta.is_proxy_input
|
|
}));
|
|
const totalReportHours = reportMap[w.user_id]?.total_hours || 0;
|
|
const vac = vacMap[w.user_id] || null;
|
|
|
|
let status = 'both_missing';
|
|
if (hasTbm && hasReport) { status = 'complete'; bothCompleted++; }
|
|
else if (hasTbm && !hasReport) { status = 'tbm_only'; }
|
|
else if (!hasTbm && hasReport) { status = 'report_only'; }
|
|
else { bothMissing++; }
|
|
|
|
if (hasTbm) tbmCompleted++;
|
|
if (hasReport) reportCompleted++;
|
|
|
|
return {
|
|
user_id: w.user_id, worker_name: w.worker_name, job_type: w.job_type,
|
|
department_name: w.department_name, has_tbm: hasTbm, has_report: hasReport,
|
|
tbm_sessions: tbmSessions, total_report_hours: totalReportHours, status,
|
|
vacation_type_id: vac ? vac.vacation_type_id : null,
|
|
vacation_type_code: vac ? vac.vacation_type_code : null,
|
|
vacation_type_name: vac ? vac.vacation_type_name : null,
|
|
vacation_hours: vac ? (8 - parseFloat(vac.deduct_days) * 8) : null
|
|
};
|
|
});
|
|
|
|
return {
|
|
date,
|
|
is_holiday: isWeekend || isCompanyHoliday,
|
|
holiday_name: isCompanyHoliday ? holidayName : (isWeekend ? '주말' : null),
|
|
summary: {
|
|
total_active_workers: workers.length,
|
|
tbm_completed: tbmCompleted,
|
|
tbm_missing: workers.length - tbmCompleted,
|
|
report_completed: reportCompleted,
|
|
report_missing: workers.length - reportCompleted,
|
|
both_completed: bothCompleted,
|
|
both_missing: bothMissing
|
|
},
|
|
workers: workerList
|
|
};
|
|
},
|
|
|
|
/**
|
|
* 작업자별 상세 조회
|
|
*/
|
|
getDailyStatusDetail: async (date, userId) => {
|
|
const db = await getDb();
|
|
|
|
// 작업자 정보
|
|
const [workerRows] = await db.query(`
|
|
SELECT w.user_id, w.worker_name, w.job_type,
|
|
COALESCE(d.department_name, '미배정') AS department_name
|
|
FROM workers w
|
|
LEFT JOIN departments d ON w.department_id = d.department_id
|
|
WHERE w.user_id = ?
|
|
`, [userId]);
|
|
|
|
// TBM 세션
|
|
const [tbmSessions] = await db.query(`
|
|
SELECT ta.session_id, s.status, s.is_proxy_input,
|
|
lu.worker_name AS leader_name,
|
|
pu.name AS proxy_input_by_name,
|
|
p.project_name, wt.work_type_name, ta.work_hours
|
|
FROM tbm_team_assignments ta
|
|
JOIN tbm_sessions s ON ta.session_id = s.session_id
|
|
LEFT JOIN workers lu ON s.leader_user_id = lu.user_id
|
|
LEFT JOIN sso_users pu ON s.proxy_input_by = pu.user_id
|
|
LEFT JOIN projects p ON ta.project_id = p.project_id
|
|
LEFT JOIN work_types wt ON ta.work_type_id = wt.work_type_id
|
|
WHERE s.session_date = ? AND ta.user_id = ? AND s.status != 'cancelled'
|
|
`, [date, userId]);
|
|
|
|
// 작업보고서
|
|
const [workReports] = await db.query(`
|
|
SELECT dwr.report_id, dwr.work_hours, dwr.created_at, dwr.created_by,
|
|
cu.name AS created_by_name,
|
|
p.project_name, wt.work_type_name, t.task_name,
|
|
ws.status_name AS work_status,
|
|
s.is_proxy_input
|
|
FROM daily_work_reports dwr
|
|
LEFT JOIN sso_users cu ON dwr.created_by = cu.user_id
|
|
LEFT JOIN projects p ON dwr.project_id = p.project_id
|
|
LEFT JOIN work_types wt ON dwr.work_type_id = wt.work_type_id
|
|
LEFT JOIN tasks t ON dwr.task_id = t.task_id
|
|
LEFT JOIN work_statuses ws ON dwr.work_status_id = ws.work_status_id
|
|
LEFT JOIN tbm_sessions s ON dwr.tbm_session_id = s.session_id
|
|
WHERE dwr.report_date = ? AND dwr.user_id = ?
|
|
ORDER BY dwr.created_at
|
|
`, [date, userId]);
|
|
|
|
return {
|
|
worker: workerRows[0] || null,
|
|
tbm_sessions: tbmSessions,
|
|
work_reports: workReports
|
|
};
|
|
}
|
|
};
|
|
|
|
module.exports = ProxyInputModel;
|