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

sso_users.user_id를 단일 식별자로 통합. JWT에서 worker_id 제거,
department_id/is_production 추가. 백엔드 15개 모델, 11개 컨트롤러,
4개 서비스, 7개 라우트, 프론트엔드 32+ JS/11+ HTML 변환.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Hyungi Ahn
2026-03-05 13:13:10 +09:00
parent 2197cdb3d5
commit abd7564e6b
90 changed files with 1790 additions and 925 deletions

View File

@@ -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