Files
TK-FB-Project/api.hyungi.net/models/attendanceModel.js
Hyungi Ahn bc5df77595 refactor(db): Replace SELECT * with explicit columns in models
Replaced `SELECT *` statements across multiple data models with explicit column lists to improve query performance, reduce data transfer, and increase code clarity. This is part of the Phase 2 refactoring plan.

- Refactored queries in the following models:
  - projectModel
  - toolsModel
  - attendanceModel
  - dailyIssueReportModel
  - issueTypeModel
  - workReportModel
  - userModel
  - dailyWorkReportModel

fix(api): Add missing volume mounts to docker-compose

Modified docker-compose.yml to mount the `config`, `middlewares`, `utils`, and `services` directories into the API container. This fixes a `MODULE_NOT_FOUND` error that caused the container to crash on startup.

feat(db): Add migration for missing project columns

Created a new database migration to add `is_active`, `project_status`, and `completed_date` columns to the `projects` table, resolving an inconsistency between the model code and the schema.

docs: Add deployment notes

Added a new markdown file to document the testing (macOS, Docker Desktop) and production (Synology NAS, Container Manager) environments.
2025-12-19 10:33:29 +09:00

304 lines
9.6 KiB
JavaScript

const { getDb } = require('../dbPool');
class AttendanceModel {
// 일일 근태 기록 조회
static async getDailyAttendanceRecords(date, workerId = null) {
const db = await getDb();
let query = `
SELECT
dar.*,
w.worker_name,
w.job_type,
wat.type_name as attendance_type_name,
wat.type_code as attendance_type_code,
vt.type_name as vacation_type_name,
vt.type_code as vacation_type_code,
vt.hours_deduction as vacation_hours
FROM daily_attendance_records dar
LEFT JOIN workers w ON dar.worker_id = w.worker_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 = ?
`;
const params = [date];
if (workerId) {
query += ' AND dar.worker_id = ?';
params.push(workerId);
}
query += ' ORDER BY w.worker_name';
const [rows] = await db.execute(query, params);
return rows;
}
// 근태 기록 생성 또는 업데이트
static async upsertAttendanceRecord(recordData) {
const db = await getDb();
const {
record_date,
worker_id,
total_work_hours,
work_attendance_type_id,
vacation_type_id,
is_overtime_approved,
created_by
} = recordData;
// 기존 기록 확인
const [existing] = await db.execute(
'SELECT id FROM daily_attendance_records WHERE worker_id = ? AND record_date = ?',
[worker_id, record_date]
);
if (existing.length > 0) {
// 업데이트
const [result] = await db.execute(`
UPDATE daily_attendance_records
SET
total_work_hours = ?,
work_attendance_type_id = ?,
vacation_type_id = ?,
is_overtime_approved = ?,
updated_at = CURRENT_TIMESTAMP
WHERE id = ?
`, [
total_work_hours,
work_attendance_type_id,
vacation_type_id,
is_overtime_approved,
existing[0].id
]);
return { id: existing[0].id, affected: result.affectedRows };
} else {
// 생성
const [result] = await db.execute(`
INSERT INTO daily_attendance_records (
record_date, worker_id, total_work_hours, work_attendance_type_id,
vacation_type_id, is_overtime_approved, created_by
) VALUES (?, ?, ?, ?, ?, ?, ?)
`, [
record_date,
worker_id,
total_work_hours,
work_attendance_type_id,
vacation_type_id,
is_overtime_approved,
created_by
]);
return { id: result.insertId, affected: result.affectedRows };
}
}
// 작업자별 근태 현황 조회 (대시보드용)
static async getWorkerAttendanceStatus(date) {
const db = await getDb();
// 모든 작업자와 해당 날짜의 근태 기록을 조회
const [rows] = await db.execute(`
SELECT
w.worker_id,
w.worker_name,
w.job_type,
COALESCE(dar.total_work_hours, 0) as total_work_hours,
COALESCE(dar.status, 'incomplete') as status,
dar.is_vacation_processed,
dar.overtime_approved,
wat.type_name as attendance_type_name,
wat.type_code as attendance_type_code,
vt.type_name as vacation_type_name,
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.worker_id = w.worker_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 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
ORDER BY w.worker_name
`, [date, date, date]);
return rows;
}
// 휴가 처리
static async processVacation(workerId, date, vacationType, createdBy) {
const db = await getDb();
// 휴가 유형 정보 조회
const [vacationTypes] = await db.execute(
'SELECT id, type_code, type_name, hours_deduction, description, is_active, created_at, updated_at FROM vacation_types WHERE type_code = ?',
[vacationType]
);
if (vacationTypes.length === 0) {
throw new Error('유효하지 않은 휴가 유형입니다.');
}
const vacationTypeInfo = vacationTypes[0];
// 현재 작업 시간 조회
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]);
const currentHours = parseFloat(workHours[0].total_hours);
const vacationHours = parseFloat(vacationTypeInfo.hours_deduction);
const totalHours = currentHours + vacationHours;
// 근로 유형 결정
let attendanceTypeCode = 'VACATION';
let status = 'vacation';
if (totalHours >= 8) {
attendanceTypeCode = totalHours > 8 ? 'OVERTIME' : 'REGULAR';
status = totalHours > 8 ? 'overtime' : 'complete';
}
const [attendanceTypes] = await db.execute(
'SELECT id FROM work_attendance_types WHERE type_code = ?',
[attendanceTypeCode]
);
// 휴가 작업 기록 생성 (프로젝트 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,
work_hours, description, created_by
) VALUES (?, ?, 13, 1, 1, ?, ?, ?)
`, [
date,
workerId,
vacationHours,
`${vacationTypeInfo.type_name} 처리`,
createdBy
]);
// 근태 기록 업데이트
const attendanceData = {
record_date: date,
worker_id: workerId,
total_work_hours: totalHours,
work_attendance_type_id: attendanceTypes[0]?.id,
vacation_type_id: vacationTypeInfo.id,
is_overtime_approved: false,
created_by: createdBy
};
return await this.upsertAttendanceRecord(attendanceData);
}
// 초과근무 승인
static async approveOvertime(workerId, date, approvedBy) {
const db = await getDb();
const [result] = await db.execute(`
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]);
return result.affectedRows > 0;
}
// 근로 유형 목록 조회
static async getAttendanceTypes() {
const db = await getDb();
const [rows] = await db.execute(
'SELECT id, type_code, type_name, description, is_active, created_at, updated_at FROM work_attendance_types WHERE is_active = TRUE ORDER BY id'
);
return rows;
}
// 휴가 유형 목록 조회
static async getVacationTypes() {
const db = await getDb();
const [rows] = await db.execute(
'SELECT id, type_code, type_name, hours_deduction, description, is_active, created_at, updated_at FROM vacation_types WHERE is_active = TRUE ORDER BY hours_deduction DESC'
);
return rows;
}
// 작업자 휴가 잔여 조회
static async getWorkerVacationBalance(workerId, 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]);
if (rows.length === 0) {
// 기본 연차 생성 (15일)
await db.execute(`
INSERT INTO worker_vacation_balance (worker_id, year, total_annual_leave)
VALUES (?, ?, 15.0)
`, [workerId, currentYear]);
return {
worker_id: workerId,
year: currentYear,
total_annual_leave: 15.0,
used_annual_leave: 0,
remaining_annual_leave: 15.0
};
}
return rows[0];
}
// 월별 근태 통계
static async getMonthlyAttendanceStats(year, month, workerId = null) {
const db = await getDb();
let query = `
SELECT
w.worker_id,
w.worker_name,
COUNT(CASE WHEN dar.status = 'complete' THEN 1 END) as regular_days,
COUNT(CASE WHEN dar.status = 'overtime' THEN 1 END) as overtime_days,
COUNT(CASE WHEN dar.status = 'vacation' THEN 1 END) as vacation_days,
COUNT(CASE WHEN dar.status = 'partial' THEN 1 END) as partial_days,
COUNT(CASE WHEN dar.status = 'incomplete' THEN 1 END) as incomplete_days,
SUM(dar.total_work_hours) as total_work_hours,
AVG(dar.total_work_hours) as avg_work_hours
FROM workers w
LEFT JOIN daily_attendance_records dar ON w.worker_id = dar.worker_id
AND YEAR(dar.record_date) = ? AND MONTH(dar.record_date) = ?
WHERE w.is_active = TRUE
`;
const params = [year, month];
if (workerId) {
query += ' AND w.worker_id = ?';
params.push(workerId);
}
query += ' GROUP BY w.worker_id, w.worker_name ORDER BY w.worker_name';
const [rows] = await db.execute(query, params);
return rows;
}
}
module.exports = AttendanceModel;