feat: 캘린더 기반 작업 현황 확인 시스템 구현
- 월별 캘린더 UI로 작업 현황을 한눈에 확인 가능 - 미입력(빨강), 부분입력(주황), 확인필요(보라), 이상없음(초록) 상태 표시 - 범례 아이콘(●)을 사용한 직관적인 상태 표시 - 날짜 클릭 시 해당일 작업자별 상세 현황 모달 - 작업자 클릭 시 개별 작업 입력/수정 모달 - 휴가 처리 기능 (연차, 반차, 반반차, 조퇴) - 월별 집계 데이터 최적화로 API 호출 최소화 백엔드: - monthly_worker_status, monthly_summary 테이블 추가 - 자동 집계 stored procedure 및 trigger 구현 - 확인필요(12시간 초과) 상태 감지 로직 - 출석 관리 시스템 확장 프론트엔드: - 캘린더 그리드 UI 구현 - 상태별 색상 및 아이콘 표시 - 모달 기반 상세 정보 표시 - 반응형 디자인 적용
This commit is contained in:
306
api.hyungi.net/controllers/attendanceController.js
Normal file
306
api.hyungi.net/controllers/attendanceController.js
Normal file
@@ -0,0 +1,306 @@
|
||||
const AttendanceModel = require('../models/attendanceModel');
|
||||
|
||||
class AttendanceController {
|
||||
// 일일 근태 현황 조회 (대시보드용)
|
||||
static async getDailyAttendanceStatus(req, res) {
|
||||
try {
|
||||
const { date } = req.query;
|
||||
|
||||
if (!date) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '날짜가 필요합니다.'
|
||||
});
|
||||
}
|
||||
|
||||
const attendanceStatus = await AttendanceModel.getWorkerAttendanceStatus(date);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: attendanceStatus,
|
||||
message: '근태 현황을 성공적으로 조회했습니다.'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('근태 현황 조회 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '근태 현황 조회 중 오류가 발생했습니다.',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 일일 근태 기록 조회
|
||||
static async getDailyAttendanceRecords(req, res) {
|
||||
try {
|
||||
const { date, worker_id } = req.query;
|
||||
|
||||
if (!date) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '날짜가 필요합니다.'
|
||||
});
|
||||
}
|
||||
|
||||
const records = await AttendanceModel.getDailyAttendanceRecords(date, worker_id);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: records,
|
||||
message: '근태 기록을 성공적으로 조회했습니다.'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('근태 기록 조회 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '근태 기록 조회 중 오류가 발생했습니다.',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 근태 기록 생성/업데이트
|
||||
static async upsertAttendanceRecord(req, res) {
|
||||
try {
|
||||
const {
|
||||
record_date,
|
||||
worker_id,
|
||||
total_work_hours,
|
||||
attendance_type_id,
|
||||
vacation_type_id,
|
||||
is_vacation_processed,
|
||||
overtime_approved,
|
||||
status,
|
||||
notes
|
||||
} = req.body;
|
||||
|
||||
// 필수 필드 검증
|
||||
if (!record_date || !worker_id) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '날짜와 작업자 ID가 필요합니다.'
|
||||
});
|
||||
}
|
||||
|
||||
const recordData = {
|
||||
record_date,
|
||||
worker_id,
|
||||
total_work_hours: total_work_hours || 0,
|
||||
attendance_type_id,
|
||||
vacation_type_id,
|
||||
is_vacation_processed: is_vacation_processed || false,
|
||||
overtime_approved: overtime_approved || false,
|
||||
status: status || 'incomplete',
|
||||
notes,
|
||||
created_by: req.user.user_id
|
||||
};
|
||||
|
||||
const result = await AttendanceModel.upsertAttendanceRecord(recordData);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: result,
|
||||
message: '근태 기록이 성공적으로 저장되었습니다.'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('근태 기록 저장 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '근태 기록 저장 중 오류가 발생했습니다.',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 휴가 처리
|
||||
static async processVacation(req, res) {
|
||||
try {
|
||||
const { worker_id, date, vacation_type } = req.body;
|
||||
|
||||
// 필수 필드 검증
|
||||
if (!worker_id || !date || !vacation_type) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '작업자 ID, 날짜, 휴가 유형이 필요합니다.'
|
||||
});
|
||||
}
|
||||
|
||||
// 휴가 유형 검증
|
||||
const validVacationTypes = ['ANNUAL_FULL', 'ANNUAL_HALF', 'ANNUAL_QUARTER'];
|
||||
if (!validVacationTypes.includes(vacation_type)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '유효하지 않은 휴가 유형입니다.'
|
||||
});
|
||||
}
|
||||
|
||||
const result = await AttendanceModel.processVacation(
|
||||
worker_id,
|
||||
date,
|
||||
vacation_type,
|
||||
req.user.user_id
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: result,
|
||||
message: '휴가 처리가 성공적으로 완료되었습니다.'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('휴가 처리 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '휴가 처리 중 오류가 발생했습니다.',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 초과근무 승인
|
||||
static async approveOvertime(req, res) {
|
||||
try {
|
||||
const { worker_id, date } = req.body;
|
||||
|
||||
// 필수 필드 검증
|
||||
if (!worker_id || !date) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '작업자 ID와 날짜가 필요합니다.'
|
||||
});
|
||||
}
|
||||
|
||||
const result = await AttendanceModel.approveOvertime(
|
||||
worker_id,
|
||||
date,
|
||||
req.user.user_id
|
||||
);
|
||||
|
||||
if (result) {
|
||||
res.json({
|
||||
success: true,
|
||||
message: '초과근무가 성공적으로 승인되었습니다.'
|
||||
});
|
||||
} else {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
message: '해당 날짜의 근태 기록을 찾을 수 없습니다.'
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('초과근무 승인 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '초과근무 승인 중 오류가 발생했습니다.',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 근로 유형 목록 조회
|
||||
static async getAttendanceTypes(req, res) {
|
||||
try {
|
||||
const attendanceTypes = await AttendanceModel.getAttendanceTypes();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: attendanceTypes,
|
||||
message: '근로 유형 목록을 성공적으로 조회했습니다.'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('근로 유형 조회 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '근로 유형 조회 중 오류가 발생했습니다.',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 휴가 유형 목록 조회
|
||||
static async getVacationTypes(req, res) {
|
||||
try {
|
||||
const vacationTypes = await AttendanceModel.getVacationTypes();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: vacationTypes,
|
||||
message: '휴가 유형 목록을 성공적으로 조회했습니다.'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('휴가 유형 조회 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '휴가 유형 조회 중 오류가 발생했습니다.',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 작업자 휴가 잔여 조회
|
||||
static async getWorkerVacationBalance(req, res) {
|
||||
try {
|
||||
const { worker_id } = req.params;
|
||||
const { year } = req.query;
|
||||
|
||||
if (!worker_id) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '작업자 ID가 필요합니다.'
|
||||
});
|
||||
}
|
||||
|
||||
const balance = await AttendanceModel.getWorkerVacationBalance(
|
||||
parseInt(worker_id),
|
||||
year ? parseInt(year) : null
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: balance,
|
||||
message: '휴가 잔여 정보를 성공적으로 조회했습니다.'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('휴가 잔여 조회 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '휴가 잔여 조회 중 오류가 발생했습니다.',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 월별 근태 통계
|
||||
static async getMonthlyAttendanceStats(req, res) {
|
||||
try {
|
||||
const { year, month, worker_id } = req.query;
|
||||
|
||||
if (!year || !month) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '연도와 월이 필요합니다.'
|
||||
});
|
||||
}
|
||||
|
||||
const stats = await AttendanceModel.getMonthlyAttendanceStats(
|
||||
parseInt(year),
|
||||
parseInt(month),
|
||||
worker_id ? parseInt(worker_id) : null
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: stats,
|
||||
message: '월별 근태 통계를 성공적으로 조회했습니다.'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('월별 근태 통계 조회 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '월별 근태 통계 조회 중 오류가 발생했습니다.',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = AttendanceController;
|
||||
201
api.hyungi.net/controllers/monthlyStatusController.js
Normal file
201
api.hyungi.net/controllers/monthlyStatusController.js
Normal file
@@ -0,0 +1,201 @@
|
||||
// controllers/monthlyStatusController.js
|
||||
// 월별 작업자 상태 집계 컨트롤러
|
||||
|
||||
const MonthlyStatusModel = require('../models/monthlyStatusModel');
|
||||
|
||||
class MonthlyStatusController {
|
||||
// 월별 캘린더 데이터 조회
|
||||
static async getMonthlyCalendarData(req, res) {
|
||||
try {
|
||||
const { year, month } = req.query;
|
||||
|
||||
if (!year || !month) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '연도(year)와 월(month)이 필요합니다.'
|
||||
});
|
||||
}
|
||||
|
||||
const yearNum = parseInt(year);
|
||||
const monthNum = parseInt(month);
|
||||
|
||||
if (yearNum < 2020 || yearNum > 2030 || monthNum < 1 || monthNum > 12) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '유효하지 않은 연도 또는 월입니다.'
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`📅 월별 캘린더 데이터 조회: ${year}년 ${month}월`);
|
||||
|
||||
const summaryData = await MonthlyStatusModel.getMonthlySummary(yearNum, monthNum);
|
||||
|
||||
// 날짜별 객체로 변환 (날짜 키를 YYYY-MM-DD 형식으로 변환)
|
||||
const calendarData = {};
|
||||
summaryData.forEach(day => {
|
||||
const dateKey = day.date.toISOString().split('T')[0]; // YYYY-MM-DD 형식으로 변환
|
||||
calendarData[dateKey] = {
|
||||
totalWorkers: day.total_workers,
|
||||
workingWorkers: day.working_workers,
|
||||
hasIssues: day.has_issues,
|
||||
hasErrors: day.has_errors,
|
||||
hasOvertimeWarning: day.has_overtime_warning,
|
||||
incompleteWorkers: day.incomplete_workers,
|
||||
partialWorkers: day.partial_workers,
|
||||
errorWorkers: day.error_workers,
|
||||
overtimeWarningWorkers: day.overtime_warning_workers,
|
||||
totalHours: parseFloat(day.total_work_hours || 0),
|
||||
totalTasks: day.total_work_count,
|
||||
errorCount: day.total_error_count,
|
||||
lastUpdated: day.last_updated
|
||||
};
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: calendarData,
|
||||
message: `${year}년 ${month}월 캘린더 데이터를 성공적으로 조회했습니다.`
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('월별 캘린더 데이터 조회 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '월별 캘린더 데이터 조회 중 오류가 발생했습니다.',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 특정 날짜의 작업자별 상세 상태 조회
|
||||
static async getDailyWorkerDetails(req, res) {
|
||||
try {
|
||||
const { date } = req.query;
|
||||
|
||||
if (!date) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '날짜(date)가 필요합니다.'
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`👥 일별 작업자 상세 조회: ${date}`);
|
||||
|
||||
const workerDetails = await MonthlyStatusModel.getDailyWorkerStatus(date);
|
||||
|
||||
// 프론트엔드에서 사용하기 쉽도록 데이터 변환
|
||||
const formattedData = workerDetails.map(worker => ({
|
||||
workerId: worker.worker_id,
|
||||
workerName: worker.worker_name,
|
||||
jobType: worker.job_type,
|
||||
totalHours: parseFloat(worker.total_work_hours || 0),
|
||||
actualWorkHours: parseFloat(worker.actual_work_hours || 0),
|
||||
vacationHours: parseFloat(worker.vacation_hours || 0),
|
||||
totalWorkCount: worker.total_work_count,
|
||||
regularWorkCount: worker.regular_work_count,
|
||||
errorWorkCount: worker.error_work_count,
|
||||
status: worker.work_status,
|
||||
hasVacation: worker.has_vacation,
|
||||
hasError: worker.has_error,
|
||||
hasIssues: worker.has_issues,
|
||||
lastUpdated: worker.last_updated
|
||||
}));
|
||||
|
||||
// 요약 정보 계산
|
||||
const summary = {
|
||||
totalWorkers: formattedData.length,
|
||||
totalHours: formattedData.reduce((sum, w) => sum + w.totalHours, 0),
|
||||
totalTasks: formattedData.reduce((sum, w) => sum + w.totalWorkCount, 0),
|
||||
errorCount: formattedData.reduce((sum, w) => sum + w.errorWorkCount, 0)
|
||||
};
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
workers: formattedData,
|
||||
summary
|
||||
},
|
||||
message: `${date} 작업자 상세 정보를 성공적으로 조회했습니다.`
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('일별 작업자 상세 조회 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '일별 작업자 상세 조회 중 오류가 발생했습니다.',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 월별 집계 재계산 (관리자용)
|
||||
static async recalculateMonth(req, res) {
|
||||
try {
|
||||
const { year, month } = req.body;
|
||||
|
||||
if (!year || !month) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '연도(year)와 월(month)이 필요합니다.'
|
||||
});
|
||||
}
|
||||
|
||||
// 관리자 권한 확인
|
||||
if (req.user.role !== 'admin' && req.user.role !== 'system') {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
message: '관리자 권한이 필요합니다.'
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`🔄 월별 집계 재계산 시작: ${year}년 ${month}월`);
|
||||
|
||||
const result = await MonthlyStatusModel.recalculateMonth(parseInt(year), parseInt(month));
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: result,
|
||||
message: `${year}년 ${month}월 집계 재계산이 완료되었습니다.`
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('월별 집계 재계산 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '월별 집계 재계산 중 오류가 발생했습니다.',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 집계 테이블 상태 확인 (관리자용)
|
||||
static async getStatusInfo(req, res) {
|
||||
try {
|
||||
// 관리자 권한 확인
|
||||
if (req.user.role !== 'admin' && req.user.role !== 'system') {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
message: '관리자 권한이 필요합니다.'
|
||||
});
|
||||
}
|
||||
|
||||
const statusInfo = await MonthlyStatusModel.getStatusInfo();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: statusInfo,
|
||||
message: '집계 테이블 상태 정보를 성공적으로 조회했습니다.'
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('집계 테이블 상태 확인 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '집계 테이블 상태 확인 중 오류가 발생했습니다.',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MonthlyStatusController;
|
||||
193
api.hyungi.net/create-attendance-tables.js
Normal file
193
api.hyungi.net/create-attendance-tables.js
Normal file
@@ -0,0 +1,193 @@
|
||||
// 근태 관리 테이블 생성 스크립트
|
||||
const mysql = require('mysql2/promise');
|
||||
|
||||
async function createAttendanceTables() {
|
||||
let connection;
|
||||
|
||||
try {
|
||||
// 로컬 MySQL 연결 (기본 설정)
|
||||
connection = await mysql.createConnection({
|
||||
host: 'localhost',
|
||||
user: 'root',
|
||||
password: '', // 비밀번호가 있다면 여기에 입력
|
||||
database: 'hyungi'
|
||||
});
|
||||
|
||||
console.log('✅ MySQL 연결 성공');
|
||||
|
||||
// 1. 근로 유형 테이블 생성
|
||||
console.log('📋 근로 유형 테이블 생성 중...');
|
||||
await connection.execute(`
|
||||
CREATE TABLE IF NOT EXISTS work_attendance_types (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
type_code VARCHAR(20) NOT NULL UNIQUE COMMENT '근로 유형 코드',
|
||||
type_name VARCHAR(50) NOT NULL COMMENT '근로 유형명',
|
||||
description TEXT COMMENT '설명',
|
||||
is_active BOOLEAN DEFAULT TRUE COMMENT '활성 상태',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
) COMMENT='근로 유형 관리 테이블'
|
||||
`);
|
||||
|
||||
// 2. 휴가 유형 테이블 생성
|
||||
console.log('🏖️ 휴가 유형 테이블 생성 중...');
|
||||
await connection.execute(`
|
||||
CREATE TABLE IF NOT EXISTS vacation_types (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
type_code VARCHAR(20) NOT NULL UNIQUE COMMENT '휴가 유형 코드',
|
||||
type_name VARCHAR(50) NOT NULL COMMENT '휴가 유형명',
|
||||
hours_deduction DECIMAL(4,2) NOT NULL COMMENT '차감 시간',
|
||||
description TEXT COMMENT '설명',
|
||||
is_active BOOLEAN DEFAULT TRUE COMMENT '활성 상태',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
) COMMENT='휴가 유형 관리 테이블'
|
||||
`);
|
||||
|
||||
// 3. 일일 근태 기록 테이블 생성
|
||||
console.log('📊 일일 근태 기록 테이블 생성 중...');
|
||||
await connection.execute(`
|
||||
CREATE TABLE IF NOT EXISTS daily_attendance_records (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
record_date DATE NOT NULL COMMENT '기록 날짜',
|
||||
worker_id INT NOT NULL COMMENT '작업자 ID',
|
||||
total_work_hours DECIMAL(4,2) DEFAULT 0 COMMENT '총 작업 시간',
|
||||
attendance_type_id INT COMMENT '근로 유형 ID',
|
||||
vacation_type_id INT NULL COMMENT '휴가 유형 ID',
|
||||
is_vacation_processed BOOLEAN DEFAULT FALSE COMMENT '휴가 처리 여부',
|
||||
overtime_approved BOOLEAN DEFAULT FALSE COMMENT '초과근무 승인 여부',
|
||||
overtime_approved_by INT NULL COMMENT '초과근무 승인자 ID',
|
||||
overtime_approved_at TIMESTAMP NULL COMMENT '초과근무 승인 시간',
|
||||
status ENUM('incomplete', 'partial', 'complete', 'overtime', 'vacation', 'error') DEFAULT 'incomplete' COMMENT '상태',
|
||||
notes TEXT COMMENT '비고',
|
||||
created_by INT NOT NULL DEFAULT 1 COMMENT '생성자 ID',
|
||||
updated_by INT NULL COMMENT '수정자 ID',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
|
||||
UNIQUE KEY unique_worker_date (worker_id, record_date),
|
||||
INDEX idx_record_date (record_date),
|
||||
INDEX idx_worker_date (worker_id, record_date),
|
||||
INDEX idx_status (status)
|
||||
) COMMENT='일일 근태 기록 테이블'
|
||||
`);
|
||||
|
||||
// 4. 작업자 휴가 잔여 관리 테이블 생성
|
||||
console.log('📅 휴가 잔여 관리 테이블 생성 중...');
|
||||
await connection.execute(`
|
||||
CREATE TABLE IF NOT EXISTS worker_vacation_balance (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
worker_id INT NOT NULL COMMENT '작업자 ID',
|
||||
year YEAR NOT NULL COMMENT '연도',
|
||||
total_annual_leave DECIMAL(4,2) DEFAULT 15.0 COMMENT '연간 총 연차 (일)',
|
||||
used_annual_leave DECIMAL(4,2) DEFAULT 0 COMMENT '사용한 연차 (일)',
|
||||
remaining_annual_leave DECIMAL(4,2) GENERATED ALWAYS AS (total_annual_leave - used_annual_leave) STORED COMMENT '잔여 연차 (일)',
|
||||
notes TEXT COMMENT '비고',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
|
||||
UNIQUE KEY unique_worker_year (worker_id, year),
|
||||
INDEX idx_worker_year (worker_id, year)
|
||||
) COMMENT='작업자별 휴가 잔여 관리 테이블'
|
||||
`);
|
||||
|
||||
// 5. 기본 데이터 삽입
|
||||
console.log('📝 기본 데이터 삽입 중...');
|
||||
|
||||
// 근로 유형 기본 데이터
|
||||
await connection.execute(`
|
||||
INSERT IGNORE INTO work_attendance_types (type_code, type_name, description) VALUES
|
||||
('REGULAR', '정시근로', '8시간 정규 근무'),
|
||||
('OVERTIME', '연장근로', '8시간 초과 근무'),
|
||||
('PARTIAL', '부분근로', '8시간 미만 근무'),
|
||||
('VACATION', '휴가근로', '휴가와 함께하는 부분 근무')
|
||||
`);
|
||||
|
||||
// 휴가 유형 기본 데이터
|
||||
await connection.execute(`
|
||||
INSERT IGNORE INTO vacation_types (type_code, type_name, hours_deduction, description) VALUES
|
||||
('ANNUAL_FULL', '연차', 8.0, '하루 전체 연차'),
|
||||
('ANNUAL_HALF', '반차', 4.0, '반일 연차'),
|
||||
('ANNUAL_QUARTER', '반반차', 2.0, '1/4일 연차'),
|
||||
('SICK_FULL', '병가', 8.0, '하루 전체 병가'),
|
||||
('SICK_HALF', '반일병가', 4.0, '반일 병가'),
|
||||
('PERSONAL_FULL', '개인사유', 8.0, '개인사유로 인한 휴가'),
|
||||
('PERSONAL_HALF', '반일개인사유', 4.0, '반일 개인사유 휴가')
|
||||
`);
|
||||
|
||||
// 6. 휴가 전용 작업 유형 추가
|
||||
console.log('🏖️ 휴가 전용 작업 유형 추가 중...');
|
||||
await connection.execute(`
|
||||
INSERT IGNORE INTO work_types (name, description, is_active) VALUES
|
||||
('휴가', '연차, 반차, 병가 등 휴가 처리용', TRUE)
|
||||
`);
|
||||
|
||||
// 7. daily_work_reports 테이블에 근태 기록 연결 컬럼 추가 (이미 있으면 무시)
|
||||
try {
|
||||
await connection.execute(`
|
||||
ALTER TABLE daily_work_reports
|
||||
ADD COLUMN attendance_record_id INT NULL COMMENT '근태 기록 ID' AFTER updated_by
|
||||
`);
|
||||
console.log('✅ daily_work_reports 테이블에 attendance_record_id 컬럼 추가됨');
|
||||
} catch (error) {
|
||||
if (error.code !== 'ER_DUP_FIELDNAME') {
|
||||
console.log('⚠️ attendance_record_id 컬럼 추가 실패:', error.message);
|
||||
} else {
|
||||
console.log('✅ attendance_record_id 컬럼이 이미 존재함');
|
||||
}
|
||||
}
|
||||
|
||||
// 8. 인덱스 추가
|
||||
try {
|
||||
await connection.execute(`CREATE INDEX idx_attendance_record ON daily_work_reports(attendance_record_id)`);
|
||||
console.log('✅ attendance_record_id 인덱스 추가됨');
|
||||
} catch (error) {
|
||||
console.log('⚠️ 인덱스 추가 실패 (이미 존재할 수 있음):', error.message);
|
||||
}
|
||||
|
||||
console.log('🎉 근태 관리 DB 설정 완료!');
|
||||
console.log('');
|
||||
console.log('📋 생성된 테이블:');
|
||||
console.log(' - work_attendance_types (근로 유형)');
|
||||
console.log(' - vacation_types (휴가 유형)');
|
||||
console.log(' - daily_attendance_records (일일 근태 기록)');
|
||||
console.log(' - worker_vacation_balance (휴가 잔여 관리)');
|
||||
console.log('');
|
||||
console.log('✅ 기본 데이터도 모두 삽입되었습니다.');
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ DB 설정 중 오류 발생:', error);
|
||||
|
||||
// 다른 연결 정보로 시도
|
||||
if (error.code === 'ECONNREFUSED' || error.code === 'ER_ACCESS_DENIED_ERROR') {
|
||||
console.log('');
|
||||
console.log('💡 다른 DB 연결 정보를 시도해보세요:');
|
||||
console.log(' - host: localhost 또는 127.0.0.1');
|
||||
console.log(' - port: 3306 (기본값)');
|
||||
console.log(' - user: root 또는 다른 사용자');
|
||||
console.log(' - password: 설정된 비밀번호');
|
||||
console.log(' - database: hyungi');
|
||||
}
|
||||
|
||||
throw error;
|
||||
} finally {
|
||||
if (connection) {
|
||||
await connection.end();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 직접 실행
|
||||
if (require.main === module) {
|
||||
createAttendanceTables()
|
||||
.then(() => {
|
||||
console.log('✅ 설정 완료');
|
||||
process.exit(0);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('❌ 설정 실패:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { createAttendanceTables };
|
||||
@@ -117,6 +117,11 @@ app.get('/api/ping', (req, res) => {
|
||||
});
|
||||
});
|
||||
|
||||
// ✅ 개발용 DB 설정 엔드포인트 (임시 비활성화)
|
||||
// app.post('/api/setup-attendance-db', async (req, res) => {
|
||||
// // DB 설정 로직 임시 비활성화
|
||||
// });
|
||||
|
||||
// ✅ 서버 상태 엔드포인트
|
||||
app.get('/api/status', (req, res) => {
|
||||
console.log('📊 Status 요청 받음!');
|
||||
@@ -277,6 +282,9 @@ app.use('/api/auth/check-password-strength', loginLimiter);
|
||||
// 나머지 인증 라우트
|
||||
app.use('/api/auth', authRoutes);
|
||||
|
||||
// 🔧 DB 설정 라우트 (개발용 - 인증 없이 접근 가능)
|
||||
app.use('/api/setup', require('./routes/setupRoutes'));
|
||||
|
||||
// 🔒 일반 API 속도 제한 적용
|
||||
app.use('/api/', apiLimiter);
|
||||
|
||||
@@ -291,7 +299,14 @@ app.use('/api/*', (req, res, next) => {
|
||||
'/api/auth/check-password-strength',
|
||||
'/api/health',
|
||||
'/api/ping', // 개발용 핑
|
||||
'/api/status' // 서버 상태
|
||||
'/api/status', // 서버 상태
|
||||
'/api/setup/setup-attendance-db',
|
||||
'/api/setup/setup-monthly-status', // DB 설정 (개발용)
|
||||
'/api/setup/add-overtime-warning', // 12시간 초과 상태 추가 (개발용)
|
||||
'/api/setup/migrate-existing-data', // 기존 데이터 마이그레이션 (개발용)
|
||||
'/api/setup/check-data-status', // DB 상태 확인 (개발용)
|
||||
'/api/monthly-status/calendar', // 월별 집계 테스트용
|
||||
'/api/monthly-status/daily-details' // 일별 상세 테스트용
|
||||
];
|
||||
|
||||
// 정확한 경로 매칭 확인
|
||||
@@ -327,6 +342,8 @@ app.use('/api/daily-work-reports', dailyWorkReportRoutes);
|
||||
app.use('/api/work-analysis', workAnalysisRoutes);
|
||||
app.use('/api/analysis', analysisRoutes); // 새로운 분석 라우트 등록
|
||||
app.use('/api/daily-work-reports-analysis', require('./routes/workReportAnalysisRoutes')); // 데일리 워크 레포트 분석 라우트
|
||||
app.use('/api/attendance', require('./routes/attendanceRoutes')); // 근태 관리 라우트
|
||||
app.use('/api/monthly-status', require('./routes/monthlyStatusRoutes')); // 월별 상태 집계 라우트
|
||||
|
||||
// 📊 리포트 및 분석
|
||||
app.use('/api/workreports', workReportRoutes);
|
||||
|
||||
359
api.hyungi.net/migrations/004_add_work_attendance_tracking.sql
Normal file
359
api.hyungi.net/migrations/004_add_work_attendance_tracking.sql
Normal file
@@ -0,0 +1,359 @@
|
||||
-- 근로 및 휴가 관리를 위한 테이블 확장
|
||||
-- 작성일: 2025-11-03
|
||||
|
||||
-- 1. 근로 유형 테이블 생성
|
||||
CREATE TABLE IF NOT EXISTS work_attendance_types (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
type_code VARCHAR(20) NOT NULL UNIQUE COMMENT '근로 유형 코드',
|
||||
type_name VARCHAR(50) NOT NULL COMMENT '근로 유형명',
|
||||
description TEXT COMMENT '설명',
|
||||
is_active BOOLEAN DEFAULT TRUE COMMENT '활성 상태',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
) COMMENT='근로 유형 관리 테이블';
|
||||
|
||||
-- 2. 휴가 유형 테이블 생성
|
||||
CREATE TABLE IF NOT EXISTS vacation_types (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
type_code VARCHAR(20) NOT NULL UNIQUE COMMENT '휴가 유형 코드',
|
||||
type_name VARCHAR(50) NOT NULL COMMENT '휴가 유형명',
|
||||
hours_deduction DECIMAL(4,2) NOT NULL COMMENT '차감 시간',
|
||||
description TEXT COMMENT '설명',
|
||||
is_active BOOLEAN DEFAULT TRUE COMMENT '활성 상태',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
) COMMENT='휴가 유형 관리 테이블';
|
||||
|
||||
-- 3. 일일 근태 기록 테이블 생성
|
||||
CREATE TABLE IF NOT EXISTS daily_attendance_records (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
record_date DATE NOT NULL COMMENT '기록 날짜',
|
||||
worker_id INT NOT NULL COMMENT '작업자 ID',
|
||||
total_work_hours DECIMAL(4,2) DEFAULT 0 COMMENT '총 작업 시간',
|
||||
attendance_type_id INT COMMENT '근로 유형 ID',
|
||||
vacation_type_id INT NULL COMMENT '휴가 유형 ID',
|
||||
is_vacation_processed BOOLEAN DEFAULT FALSE COMMENT '휴가 처리 여부',
|
||||
overtime_approved BOOLEAN DEFAULT FALSE COMMENT '초과근무 승인 여부',
|
||||
overtime_approved_by INT NULL COMMENT '초과근무 승인자 ID',
|
||||
overtime_approved_at TIMESTAMP NULL COMMENT '초과근무 승인 시간',
|
||||
status ENUM('incomplete', 'partial', 'complete', 'overtime', 'vacation', 'error') DEFAULT 'incomplete' COMMENT '상태',
|
||||
notes TEXT COMMENT '비고',
|
||||
created_by INT NOT NULL COMMENT '생성자 ID',
|
||||
updated_by INT NULL COMMENT '수정자 ID',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
|
||||
-- 외래키 제약조건
|
||||
FOREIGN KEY (worker_id) REFERENCES workers(worker_id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (attendance_type_id) REFERENCES work_attendance_types(id),
|
||||
FOREIGN KEY (vacation_type_id) REFERENCES vacation_types(id),
|
||||
FOREIGN KEY (overtime_approved_by) REFERENCES users(user_id),
|
||||
FOREIGN KEY (created_by) REFERENCES users(user_id),
|
||||
FOREIGN KEY (updated_by) REFERENCES users(user_id),
|
||||
|
||||
-- 유니크 제약조건 (작업자별 날짜별 하나의 기록)
|
||||
UNIQUE KEY unique_worker_date (worker_id, record_date),
|
||||
|
||||
-- 인덱스
|
||||
INDEX idx_record_date (record_date),
|
||||
INDEX idx_worker_date (worker_id, record_date),
|
||||
INDEX idx_status (status)
|
||||
) COMMENT='일일 근태 기록 테이블';
|
||||
|
||||
-- 4. 휴가 잔여 관리 테이블 생성
|
||||
CREATE TABLE IF NOT EXISTS worker_vacation_balance (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
worker_id INT NOT NULL COMMENT '작업자 ID',
|
||||
year YEAR NOT NULL COMMENT '연도',
|
||||
total_annual_leave DECIMAL(4,2) DEFAULT 15.0 COMMENT '연간 총 연차 (일)',
|
||||
used_annual_leave DECIMAL(4,2) DEFAULT 0 COMMENT '사용한 연차 (일)',
|
||||
remaining_annual_leave DECIMAL(4,2) GENERATED ALWAYS AS (total_annual_leave - used_annual_leave) STORED COMMENT '잔여 연차 (일)',
|
||||
notes TEXT COMMENT '비고',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
|
||||
-- 외래키 제약조건
|
||||
FOREIGN KEY (worker_id) REFERENCES workers(worker_id) ON DELETE CASCADE,
|
||||
|
||||
-- 유니크 제약조건 (작업자별 연도별 하나의 기록)
|
||||
UNIQUE KEY unique_worker_year (worker_id, year),
|
||||
|
||||
-- 인덱스
|
||||
INDEX idx_worker_year (worker_id, year)
|
||||
) COMMENT='작업자별 휴가 잔여 관리 테이블';
|
||||
|
||||
-- 5. 기본 데이터 삽입
|
||||
|
||||
-- 근로 유형 기본 데이터
|
||||
INSERT INTO work_attendance_types (type_code, type_name, description) VALUES
|
||||
('REGULAR', '정시근로', '8시간 정규 근무'),
|
||||
('OVERTIME', '연장근로', '8시간 초과 근무'),
|
||||
('PARTIAL', '부분근로', '8시간 미만 근무'),
|
||||
('VACATION', '휴가근로', '휴가와 함께하는 부분 근무')
|
||||
ON DUPLICATE KEY UPDATE
|
||||
type_name = VALUES(type_name),
|
||||
description = VALUES(description);
|
||||
|
||||
-- 휴가 유형 기본 데이터
|
||||
INSERT INTO vacation_types (type_code, type_name, hours_deduction, description) VALUES
|
||||
('ANNUAL_FULL', '연차', 8.0, '하루 전체 연차'),
|
||||
('ANNUAL_HALF', '반차', 4.0, '반일 연차'),
|
||||
('ANNUAL_QUARTER', '반반차', 2.0, '1/4일 연차'),
|
||||
('SICK_FULL', '병가', 8.0, '하루 전체 병가'),
|
||||
('SICK_HALF', '반일병가', 4.0, '반일 병가'),
|
||||
('PERSONAL_FULL', '개인사유', 8.0, '개인사유로 인한 휴가'),
|
||||
('PERSONAL_HALF', '반일개인사유', 4.0, '반일 개인사유 휴가')
|
||||
ON DUPLICATE KEY UPDATE
|
||||
type_name = VALUES(type_name),
|
||||
hours_deduction = VALUES(hours_deduction),
|
||||
description = VALUES(description);
|
||||
|
||||
-- 6. daily_work_reports 테이블에 근태 기록 연결 컬럼 추가
|
||||
ALTER TABLE daily_work_reports
|
||||
ADD COLUMN attendance_record_id INT NULL COMMENT '근태 기록 ID' AFTER updated_by,
|
||||
ADD INDEX idx_attendance_record (attendance_record_id);
|
||||
|
||||
-- 외래키 제약조건 추가 (나중에 데이터 정리 후)
|
||||
-- ALTER TABLE daily_work_reports
|
||||
-- ADD FOREIGN KEY (attendance_record_id) REFERENCES daily_attendance_records(id);
|
||||
|
||||
-- 7. 휴가 전용 작업 유형 추가 (work_types 테이블에)
|
||||
INSERT INTO work_types (name, description, is_active) VALUES
|
||||
('휴가', '연차, 반차, 병가 등 휴가 처리용', TRUE)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
description = VALUES(description),
|
||||
is_active = VALUES(is_active);
|
||||
|
||||
-- 8. 뷰 생성 - 일일 근태 현황 조회용
|
||||
CREATE OR REPLACE VIEW v_daily_attendance_summary AS
|
||||
SELECT
|
||||
dar.id,
|
||||
dar.record_date,
|
||||
dar.worker_id,
|
||||
w.worker_name,
|
||||
w.job_type,
|
||||
dar.total_work_hours,
|
||||
wat.type_name as attendance_type,
|
||||
vt.type_name as vacation_type,
|
||||
dar.is_vacation_processed,
|
||||
dar.overtime_approved,
|
||||
dar.status,
|
||||
CASE
|
||||
WHEN dar.status = 'incomplete' THEN '미입력'
|
||||
WHEN dar.status = 'partial' THEN '부분입력'
|
||||
WHEN dar.status = 'complete' THEN '정시근로'
|
||||
WHEN dar.status = 'overtime' THEN '연장근로'
|
||||
WHEN dar.status = 'vacation' THEN '휴가'
|
||||
WHEN dar.status = 'error' THEN '오류'
|
||||
ELSE '알수없음'
|
||||
END as status_text,
|
||||
dar.notes,
|
||||
dar.created_at,
|
||||
dar.updated_at
|
||||
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
|
||||
ORDER BY dar.record_date DESC, w.worker_name;
|
||||
|
||||
-- 9. 트리거 생성 - daily_work_reports 변경 시 근태 기록 자동 업데이트
|
||||
DELIMITER //
|
||||
|
||||
CREATE OR REPLACE TRIGGER tr_update_attendance_on_work_report
|
||||
AFTER INSERT ON daily_work_reports
|
||||
FOR EACH ROW
|
||||
BEGIN
|
||||
DECLARE total_hours DECIMAL(4,2);
|
||||
DECLARE attendance_type INT;
|
||||
DECLARE vacation_type INT;
|
||||
DECLARE record_status VARCHAR(20);
|
||||
DECLARE existing_record_id INT;
|
||||
|
||||
-- 해당 작업자의 해당 날짜 총 작업시간 계산
|
||||
SELECT COALESCE(SUM(work_hours), 0) INTO total_hours
|
||||
FROM daily_work_reports
|
||||
WHERE worker_id = NEW.worker_id
|
||||
AND report_date = NEW.report_date;
|
||||
|
||||
-- 휴가 처리 여부 확인 (work_type_id가 휴가용인지)
|
||||
SELECT id INTO vacation_type
|
||||
FROM vacation_types
|
||||
WHERE (total_hours = 0 AND type_code = 'ANNUAL_FULL')
|
||||
OR (total_hours = 4 AND type_code = 'ANNUAL_HALF')
|
||||
OR (total_hours = 6 AND type_code = 'ANNUAL_QUARTER')
|
||||
LIMIT 1;
|
||||
|
||||
-- 근로 유형 결정
|
||||
IF total_hours = 0 THEN
|
||||
SELECT id INTO attendance_type FROM work_attendance_types WHERE type_code = 'PARTIAL';
|
||||
SET record_status = 'incomplete';
|
||||
ELSEIF total_hours < 8 THEN
|
||||
SELECT id INTO attendance_type FROM work_attendance_types WHERE type_code = 'PARTIAL';
|
||||
SET record_status = 'partial';
|
||||
ELSEIF total_hours = 8 THEN
|
||||
SELECT id INTO attendance_type FROM work_attendance_types WHERE type_code = 'REGULAR';
|
||||
SET record_status = 'complete';
|
||||
ELSE
|
||||
SELECT id INTO attendance_type FROM work_attendance_types WHERE type_code = 'OVERTIME';
|
||||
SET record_status = 'overtime';
|
||||
END IF;
|
||||
|
||||
-- 휴가 처리된 경우 상태 조정
|
||||
IF vacation_type IS NOT NULL THEN
|
||||
SET record_status = 'vacation';
|
||||
END IF;
|
||||
|
||||
-- 기존 근태 기록 확인
|
||||
SELECT id INTO existing_record_id
|
||||
FROM daily_attendance_records
|
||||
WHERE worker_id = NEW.worker_id AND record_date = NEW.report_date;
|
||||
|
||||
-- 근태 기록 업데이트 또는 생성
|
||||
IF existing_record_id IS NOT NULL THEN
|
||||
UPDATE daily_attendance_records
|
||||
SET
|
||||
total_work_hours = total_hours,
|
||||
attendance_type_id = attendance_type,
|
||||
vacation_type_id = vacation_type,
|
||||
is_vacation_processed = (vacation_type IS NOT NULL),
|
||||
status = record_status,
|
||||
updated_by = NEW.created_by,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = existing_record_id;
|
||||
ELSE
|
||||
INSERT INTO daily_attendance_records (
|
||||
record_date, worker_id, total_work_hours, attendance_type_id,
|
||||
vacation_type_id, is_vacation_processed, status, created_by
|
||||
) VALUES (
|
||||
NEW.report_date, NEW.worker_id, total_hours, attendance_type,
|
||||
vacation_type, (vacation_type IS NOT NULL), record_status, NEW.created_by
|
||||
);
|
||||
END IF;
|
||||
END//
|
||||
|
||||
DELIMITER ;
|
||||
|
||||
-- 10. 기존 데이터 마이그레이션을 위한 프로시저
|
||||
DELIMITER //
|
||||
|
||||
CREATE OR REPLACE PROCEDURE sp_migrate_existing_work_reports()
|
||||
BEGIN
|
||||
DECLARE done INT DEFAULT FALSE;
|
||||
DECLARE v_worker_id INT;
|
||||
DECLARE v_report_date DATE;
|
||||
DECLARE cur CURSOR FOR
|
||||
SELECT DISTINCT worker_id, report_date
|
||||
FROM daily_work_reports
|
||||
ORDER BY report_date DESC, worker_id;
|
||||
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;
|
||||
|
||||
OPEN cur;
|
||||
|
||||
read_loop: LOOP
|
||||
FETCH cur INTO v_worker_id, v_report_date;
|
||||
IF done THEN
|
||||
LEAVE read_loop;
|
||||
END IF;
|
||||
|
||||
-- 각 작업자별 날짜별로 근태 기록 생성/업데이트
|
||||
CALL sp_update_attendance_record(v_worker_id, v_report_date);
|
||||
END LOOP;
|
||||
|
||||
CLOSE cur;
|
||||
END//
|
||||
|
||||
CREATE OR REPLACE PROCEDURE sp_update_attendance_record(
|
||||
IN p_worker_id INT,
|
||||
IN p_report_date DATE
|
||||
)
|
||||
BEGIN
|
||||
DECLARE total_hours DECIMAL(4,2);
|
||||
DECLARE attendance_type INT;
|
||||
DECLARE vacation_type INT;
|
||||
DECLARE record_status VARCHAR(20);
|
||||
DECLARE existing_record_id INT;
|
||||
DECLARE has_vacation_work INT DEFAULT 0;
|
||||
|
||||
-- 해당 작업자의 해당 날짜 총 작업시간 계산
|
||||
SELECT COALESCE(SUM(work_hours), 0) INTO total_hours
|
||||
FROM daily_work_reports
|
||||
WHERE worker_id = p_worker_id
|
||||
AND report_date = p_report_date;
|
||||
|
||||
-- 휴가 관련 작업이 있는지 확인 (work_type_id = 999 또는 휴가 관련)
|
||||
SELECT COUNT(*) INTO has_vacation_work
|
||||
FROM daily_work_reports dwr
|
||||
JOIN work_types wt ON dwr.work_type_id = wt.id
|
||||
WHERE dwr.worker_id = p_worker_id
|
||||
AND dwr.report_date = p_report_date
|
||||
AND (wt.name LIKE '%휴가%' OR wt.name LIKE '%연차%' OR wt.name LIKE '%반차%');
|
||||
|
||||
-- 휴가 유형 결정
|
||||
IF has_vacation_work > 0 THEN
|
||||
IF total_hours = 0 THEN
|
||||
SELECT id INTO vacation_type FROM vacation_types WHERE type_code = 'ANNUAL_FULL' LIMIT 1;
|
||||
ELSEIF total_hours = 4 THEN
|
||||
SELECT id INTO vacation_type FROM vacation_types WHERE type_code = 'ANNUAL_HALF' LIMIT 1;
|
||||
ELSEIF total_hours = 6 THEN
|
||||
SELECT id INTO vacation_type FROM vacation_types WHERE type_code = 'ANNUAL_QUARTER' LIMIT 1;
|
||||
END IF;
|
||||
END IF;
|
||||
|
||||
-- 근로 유형 및 상태 결정
|
||||
IF vacation_type IS NOT NULL THEN
|
||||
SELECT id INTO attendance_type FROM work_attendance_types WHERE type_code = 'VACATION';
|
||||
SET record_status = 'vacation';
|
||||
ELSEIF total_hours = 0 THEN
|
||||
SELECT id INTO attendance_type FROM work_attendance_types WHERE type_code = 'PARTIAL';
|
||||
SET record_status = 'incomplete';
|
||||
ELSEIF total_hours < 8 THEN
|
||||
SELECT id INTO attendance_type FROM work_attendance_types WHERE type_code = 'PARTIAL';
|
||||
SET record_status = 'partial';
|
||||
ELSEIF total_hours = 8 THEN
|
||||
SELECT id INTO attendance_type FROM work_attendance_types WHERE type_code = 'REGULAR';
|
||||
SET record_status = 'complete';
|
||||
ELSE
|
||||
SELECT id INTO attendance_type FROM work_attendance_types WHERE type_code = 'OVERTIME';
|
||||
SET record_status = 'overtime';
|
||||
END IF;
|
||||
|
||||
-- 기존 근태 기록 확인
|
||||
SELECT id INTO existing_record_id
|
||||
FROM daily_attendance_records
|
||||
WHERE worker_id = p_worker_id AND record_date = p_report_date;
|
||||
|
||||
-- 근태 기록 업데이트 또는 생성
|
||||
IF existing_record_id IS NOT NULL THEN
|
||||
UPDATE daily_attendance_records
|
||||
SET
|
||||
total_work_hours = total_hours,
|
||||
attendance_type_id = attendance_type,
|
||||
vacation_type_id = vacation_type,
|
||||
is_vacation_processed = (vacation_type IS NOT NULL),
|
||||
status = record_status,
|
||||
updated_by = 1,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = existing_record_id;
|
||||
ELSE
|
||||
INSERT INTO daily_attendance_records (
|
||||
record_date, worker_id, total_work_hours, attendance_type_id,
|
||||
vacation_type_id, is_vacation_processed, status, created_by
|
||||
) VALUES (
|
||||
p_report_date, p_worker_id, total_hours, attendance_type,
|
||||
vacation_type, (vacation_type IS NOT NULL), record_status, 1
|
||||
);
|
||||
END IF;
|
||||
END//
|
||||
|
||||
DELIMITER ;
|
||||
|
||||
-- 11. 권한 및 인덱스 최적화
|
||||
-- 추가 인덱스 생성
|
||||
CREATE INDEX idx_daily_work_reports_worker_date ON daily_work_reports(worker_id, report_date);
|
||||
CREATE INDEX idx_daily_work_reports_work_type ON daily_work_reports(work_type_id);
|
||||
|
||||
-- 마이그레이션 실행 (주석 해제하여 실행)
|
||||
-- CALL sp_migrate_existing_work_reports();
|
||||
|
||||
-- 완료 메시지
|
||||
SELECT 'DB 확장 완료: 근로 및 휴가 관리 시스템이 성공적으로 구축되었습니다.' as message;
|
||||
143
api.hyungi.net/migrations/005_add_attendance_management.sql
Normal file
143
api.hyungi.net/migrations/005_add_attendance_management.sql
Normal file
@@ -0,0 +1,143 @@
|
||||
-- 근태 관리 시스템 테이블 추가
|
||||
-- 작성일: 2025-11-03
|
||||
-- 설명: 근로 유형, 휴가 유형, 일일 근태 기록, 휴가 잔여 관리 테이블
|
||||
|
||||
-- 1. 근로 유형 테이블 생성
|
||||
CREATE TABLE IF NOT EXISTS `work_attendance_types` (
|
||||
`id` INT PRIMARY KEY AUTO_INCREMENT,
|
||||
`type_code` VARCHAR(20) NOT NULL UNIQUE COMMENT '근로 유형 코드',
|
||||
`type_name` VARCHAR(50) NOT NULL COMMENT '근로 유형명',
|
||||
`description` TEXT COMMENT '설명',
|
||||
`is_active` BOOLEAN DEFAULT TRUE COMMENT '활성 상태',
|
||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='근로 유형 관리 테이블';
|
||||
|
||||
-- 2. 휴가 유형 테이블 생성
|
||||
CREATE TABLE IF NOT EXISTS `vacation_types` (
|
||||
`id` INT PRIMARY KEY AUTO_INCREMENT,
|
||||
`type_code` VARCHAR(20) NOT NULL UNIQUE COMMENT '휴가 유형 코드',
|
||||
`type_name` VARCHAR(50) NOT NULL COMMENT '휴가 유형명',
|
||||
`hours_deduction` DECIMAL(4,2) NOT NULL COMMENT '차감 시간',
|
||||
`description` TEXT COMMENT '설명',
|
||||
`is_active` BOOLEAN DEFAULT TRUE COMMENT '활성 상태',
|
||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='휴가 유형 관리 테이블';
|
||||
|
||||
-- 3. 일일 근태 기록 테이블 생성
|
||||
CREATE TABLE IF NOT EXISTS `daily_attendance_records` (
|
||||
`id` INT PRIMARY KEY AUTO_INCREMENT,
|
||||
`record_date` DATE NOT NULL COMMENT '기록 날짜',
|
||||
`worker_id` INT NOT NULL COMMENT '작업자 ID',
|
||||
`total_work_hours` DECIMAL(4,2) DEFAULT 0 COMMENT '총 작업 시간',
|
||||
`attendance_type_id` INT COMMENT '근로 유형 ID',
|
||||
`vacation_type_id` INT NULL COMMENT '휴가 유형 ID',
|
||||
`is_vacation_processed` BOOLEAN DEFAULT FALSE COMMENT '휴가 처리 여부',
|
||||
`overtime_approved` BOOLEAN DEFAULT FALSE COMMENT '초과근무 승인 여부',
|
||||
`overtime_approved_by` INT NULL COMMENT '초과근무 승인자 ID',
|
||||
`overtime_approved_at` TIMESTAMP NULL COMMENT '초과근무 승인 시간',
|
||||
`status` ENUM('incomplete', 'partial', 'complete', 'overtime', 'vacation', 'error') DEFAULT 'incomplete' COMMENT '상태',
|
||||
`notes` TEXT COMMENT '비고',
|
||||
`created_by` INT NOT NULL DEFAULT 1 COMMENT '생성자 ID',
|
||||
`updated_by` INT NULL COMMENT '수정자 ID',
|
||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
|
||||
-- 인덱스
|
||||
UNIQUE KEY `unique_worker_date` (`worker_id`, `record_date`),
|
||||
INDEX `idx_record_date` (`record_date`),
|
||||
INDEX `idx_worker_date` (`worker_id`, `record_date`),
|
||||
INDEX `idx_status` (`status`),
|
||||
|
||||
-- 외래키 (기존 테이블과의 관계)
|
||||
FOREIGN KEY (`worker_id`) REFERENCES `workers`(`worker_id`) ON DELETE CASCADE,
|
||||
FOREIGN KEY (`attendance_type_id`) REFERENCES `work_attendance_types`(`id`),
|
||||
FOREIGN KEY (`vacation_type_id`) REFERENCES `vacation_types`(`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='일일 근태 기록 테이블';
|
||||
|
||||
-- 4. 작업자 휴가 잔여 관리 테이블 생성
|
||||
CREATE TABLE IF NOT EXISTS `worker_vacation_balance` (
|
||||
`id` INT PRIMARY KEY AUTO_INCREMENT,
|
||||
`worker_id` INT NOT NULL COMMENT '작업자 ID',
|
||||
`year` YEAR NOT NULL COMMENT '연도',
|
||||
`total_annual_leave` DECIMAL(4,2) DEFAULT 15.0 COMMENT '연간 총 연차 (일)',
|
||||
`used_annual_leave` DECIMAL(4,2) DEFAULT 0 COMMENT '사용한 연차 (일)',
|
||||
`remaining_annual_leave` DECIMAL(4,2) GENERATED ALWAYS AS (`total_annual_leave` - `used_annual_leave`) STORED COMMENT '잔여 연차 (일)',
|
||||
`notes` TEXT COMMENT '비고',
|
||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
|
||||
-- 인덱스
|
||||
UNIQUE KEY `unique_worker_year` (`worker_id`, `year`),
|
||||
INDEX `idx_worker_year` (`worker_id`, `year`),
|
||||
|
||||
-- 외래키
|
||||
FOREIGN KEY (`worker_id`) REFERENCES `workers`(`worker_id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='작업자별 휴가 잔여 관리 테이블';
|
||||
|
||||
-- 5. daily_work_reports 테이블에 근태 기록 연결 컬럼 추가
|
||||
ALTER TABLE `daily_work_reports`
|
||||
ADD COLUMN IF NOT EXISTS `attendance_record_id` INT NULL COMMENT '근태 기록 ID' AFTER `updated_by`;
|
||||
|
||||
-- 인덱스 추가
|
||||
CREATE INDEX IF NOT EXISTS `idx_attendance_record` ON `daily_work_reports`(`attendance_record_id`);
|
||||
CREATE INDEX IF NOT EXISTS `idx_daily_work_reports_worker_date` ON `daily_work_reports`(`worker_id`, `report_date`);
|
||||
|
||||
-- 6. 기본 데이터 삽입
|
||||
|
||||
-- 근로 유형 기본 데이터
|
||||
INSERT IGNORE INTO `work_attendance_types` (`type_code`, `type_name`, `description`) VALUES
|
||||
('REGULAR', '정시근로', '8시간 정규 근무'),
|
||||
('OVERTIME', '연장근로', '8시간 초과 근무'),
|
||||
('PARTIAL', '부분근로', '8시간 미만 근무'),
|
||||
('VACATION', '휴가근로', '휴가와 함께하는 부분 근무');
|
||||
|
||||
-- 휴가 유형 기본 데이터
|
||||
INSERT IGNORE INTO `vacation_types` (`type_code`, `type_name`, `hours_deduction`, `description`) VALUES
|
||||
('ANNUAL_FULL', '연차', 8.0, '하루 전체 연차'),
|
||||
('ANNUAL_HALF', '반차', 4.0, '반일 연차'),
|
||||
('ANNUAL_QUARTER', '반반차', 2.0, '1/4일 연차'),
|
||||
('SICK_FULL', '병가', 8.0, '하루 전체 병가'),
|
||||
('SICK_HALF', '반일병가', 4.0, '반일 병가'),
|
||||
('PERSONAL_FULL', '개인사유', 8.0, '개인사유로 인한 휴가'),
|
||||
('PERSONAL_HALF', '반일개인사유', 4.0, '반일 개인사유 휴가');
|
||||
|
||||
-- 7. 휴가 전용 작업 유형 추가 (이미 있으면 무시)
|
||||
INSERT IGNORE INTO `work_types` (`name`, `description`, `is_active`) VALUES
|
||||
('휴가', '연차, 반차, 병가 등 휴가 처리용', TRUE);
|
||||
|
||||
-- 8. 뷰 생성 - 일일 근태 현황 조회용
|
||||
CREATE OR REPLACE VIEW `v_daily_attendance_summary` AS
|
||||
SELECT
|
||||
dar.id,
|
||||
dar.record_date,
|
||||
dar.worker_id,
|
||||
w.worker_name,
|
||||
w.job_type,
|
||||
dar.total_work_hours,
|
||||
wat.type_name as attendance_type,
|
||||
vt.type_name as vacation_type,
|
||||
dar.is_vacation_processed,
|
||||
dar.overtime_approved,
|
||||
dar.status,
|
||||
CASE
|
||||
WHEN dar.status = 'incomplete' THEN '미입력'
|
||||
WHEN dar.status = 'partial' THEN '부분입력'
|
||||
WHEN dar.status = 'complete' THEN '정시근로'
|
||||
WHEN dar.status = 'overtime' THEN '연장근로'
|
||||
WHEN dar.status = 'vacation' THEN '휴가'
|
||||
WHEN dar.status = 'error' THEN '오류'
|
||||
ELSE '알수없음'
|
||||
END as status_text,
|
||||
dar.notes,
|
||||
dar.created_at,
|
||||
dar.updated_at
|
||||
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
|
||||
ORDER BY dar.record_date DESC, w.worker_name;
|
||||
|
||||
-- 완료 메시지
|
||||
SELECT '✅ 근태 관리 시스템 테이블이 성공적으로 생성되었습니다.' as message;
|
||||
16
api.hyungi.net/migrations/006_add_description_column.sql
Normal file
16
api.hyungi.net/migrations/006_add_description_column.sql
Normal file
@@ -0,0 +1,16 @@
|
||||
-- 006_add_description_column.sql
|
||||
-- daily_work_reports 테이블에 description 컬럼 추가
|
||||
|
||||
-- description 컬럼 추가 (이미 존재하는 경우 무시)
|
||||
SET @sql = (SELECT IF(
|
||||
(SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE table_name = 'daily_work_reports'
|
||||
AND table_schema = 'hyungi'
|
||||
AND column_name = 'description') = 0,
|
||||
'ALTER TABLE daily_work_reports ADD COLUMN description TEXT COMMENT ''작업 설명'' AFTER work_hours',
|
||||
'SELECT ''description column already exists'' as message'
|
||||
));
|
||||
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
249
api.hyungi.net/migrations/007_create_monthly_worker_status.sql
Normal file
249
api.hyungi.net/migrations/007_create_monthly_worker_status.sql
Normal file
@@ -0,0 +1,249 @@
|
||||
-- 007_create_monthly_worker_status.sql
|
||||
-- 월별 작업자 상태 집계 테이블 생성
|
||||
|
||||
-- 월별 작업자 상태 집계 테이블
|
||||
CREATE TABLE IF NOT EXISTS monthly_worker_status (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
year INT NOT NULL COMMENT '연도',
|
||||
month INT NOT NULL COMMENT '월 (1-12)',
|
||||
worker_id INT NOT NULL COMMENT '작업자 ID',
|
||||
date DATE NOT NULL COMMENT '날짜',
|
||||
|
||||
-- 작업 시간 정보
|
||||
total_work_hours DECIMAL(5,2) DEFAULT 0.00 COMMENT '총 작업시간',
|
||||
actual_work_hours DECIMAL(5,2) DEFAULT 0.00 COMMENT '실제 작업시간 (휴가 제외)',
|
||||
vacation_hours DECIMAL(5,2) DEFAULT 0.00 COMMENT '휴가 시간',
|
||||
|
||||
-- 작업 건수
|
||||
total_work_count INT DEFAULT 0 COMMENT '총 작업 건수',
|
||||
regular_work_count INT DEFAULT 0 COMMENT '정규 작업 건수',
|
||||
error_work_count INT DEFAULT 0 COMMENT '오류 작업 건수',
|
||||
|
||||
-- 상태 정보
|
||||
work_status ENUM(
|
||||
'incomplete', -- 미입력 (0시간)
|
||||
'partial', -- 부분입력 (8시간 미만)
|
||||
'complete', -- 정시근로 (8시간)
|
||||
'overtime', -- 연장근로 (8시간 초과)
|
||||
'vacation-full', -- 연차 (8시간)
|
||||
'vacation-half', -- 반차 (4시간)
|
||||
'vacation-quarter',-- 반반차 (2시간)
|
||||
'vacation-half-half', -- 조퇴 (6시간)
|
||||
'error', -- 오류 발생
|
||||
'overtime-warning' -- 초과근무 확인필요 (12시간 초과)
|
||||
) NOT NULL DEFAULT 'incomplete' COMMENT '작업 상태',
|
||||
|
||||
has_vacation BOOLEAN DEFAULT FALSE COMMENT '휴가 여부',
|
||||
has_error BOOLEAN DEFAULT FALSE COMMENT '오류 여부',
|
||||
has_issues BOOLEAN DEFAULT FALSE COMMENT '문제 여부 (미입력/부분입력)',
|
||||
|
||||
-- 메타 정보
|
||||
last_updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '마지막 업데이트 시간',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
-- 인덱스
|
||||
UNIQUE KEY unique_worker_date (worker_id, date),
|
||||
KEY idx_year_month (year, month),
|
||||
KEY idx_worker_year_month (worker_id, year, month),
|
||||
KEY idx_status (work_status),
|
||||
KEY idx_has_issues (has_issues),
|
||||
KEY idx_has_error (has_error),
|
||||
|
||||
-- 외래키
|
||||
FOREIGN KEY (worker_id) REFERENCES workers(worker_id) ON DELETE CASCADE
|
||||
) COMMENT='월별 작업자 상태 집계 테이블';
|
||||
|
||||
-- 월별 집계 요약 테이블 (캘린더 최적화용)
|
||||
CREATE TABLE IF NOT EXISTS monthly_summary (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
year INT NOT NULL COMMENT '연도',
|
||||
month INT NOT NULL COMMENT '월 (1-12)',
|
||||
date DATE NOT NULL COMMENT '날짜',
|
||||
|
||||
-- 작업자 수
|
||||
total_workers INT DEFAULT 0 COMMENT '총 작업자 수',
|
||||
working_workers INT DEFAULT 0 COMMENT '작업한 작업자 수',
|
||||
|
||||
-- 상태별 작업자 수
|
||||
incomplete_workers INT DEFAULT 0 COMMENT '미입력 작업자 수',
|
||||
partial_workers INT DEFAULT 0 COMMENT '부분입력 작업자 수',
|
||||
complete_workers INT DEFAULT 0 COMMENT '완료 작업자 수',
|
||||
overtime_workers INT DEFAULT 0 COMMENT '연장근로 작업자 수',
|
||||
vacation_workers INT DEFAULT 0 COMMENT '휴가 작업자 수',
|
||||
error_workers INT DEFAULT 0 COMMENT '오류 작업자 수',
|
||||
|
||||
-- 집계 정보
|
||||
total_work_hours DECIMAL(8,2) DEFAULT 0.00 COMMENT '총 작업시간',
|
||||
total_work_count INT DEFAULT 0 COMMENT '총 작업 건수',
|
||||
total_error_count INT DEFAULT 0 COMMENT '총 오류 건수',
|
||||
|
||||
-- 상태 플래그 (캘린더 표시용)
|
||||
has_issues BOOLEAN DEFAULT FALSE COMMENT '문제 있음 (미입력/부분입력)',
|
||||
has_errors BOOLEAN DEFAULT FALSE COMMENT '오류 있음',
|
||||
|
||||
-- 메타 정보
|
||||
last_updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
-- 인덱스
|
||||
UNIQUE KEY unique_date (date),
|
||||
KEY idx_year_month (year, month),
|
||||
KEY idx_has_issues (has_issues),
|
||||
KEY idx_has_errors (has_errors)
|
||||
) COMMENT='월별 일자별 요약 테이블 (캘린더 최적화용)';
|
||||
|
||||
-- 집계 데이터 업데이트 함수
|
||||
DELIMITER $$
|
||||
|
||||
CREATE OR REPLACE PROCEDURE UpdateMonthlyWorkerStatus(
|
||||
IN p_date DATE,
|
||||
IN p_worker_id INT
|
||||
)
|
||||
BEGIN
|
||||
DECLARE v_year INT;
|
||||
DECLARE v_month INT;
|
||||
DECLARE v_total_hours DECIMAL(5,2);
|
||||
DECLARE v_actual_hours DECIMAL(5,2);
|
||||
DECLARE v_vacation_hours DECIMAL(5,2);
|
||||
DECLARE v_total_count INT;
|
||||
DECLARE v_regular_count INT;
|
||||
DECLARE v_error_count INT;
|
||||
DECLARE v_has_vacation BOOLEAN;
|
||||
DECLARE v_has_error BOOLEAN;
|
||||
DECLARE v_has_issues BOOLEAN;
|
||||
DECLARE v_status VARCHAR(20);
|
||||
|
||||
-- 연도, 월 추출
|
||||
SET v_year = YEAR(p_date);
|
||||
SET v_month = MONTH(p_date);
|
||||
|
||||
-- 해당 날짜의 작업자 데이터 집계
|
||||
SELECT
|
||||
COALESCE(SUM(work_hours), 0),
|
||||
COALESCE(SUM(CASE WHEN project_id != 13 THEN work_hours ELSE 0 END), 0),
|
||||
COALESCE(SUM(CASE WHEN project_id = 13 THEN work_hours ELSE 0 END), 0),
|
||||
COUNT(*),
|
||||
COUNT(CASE WHEN project_id != 13 AND work_status_id != 2 THEN 1 END),
|
||||
COUNT(CASE WHEN work_status_id = 2 THEN 1 END),
|
||||
MAX(CASE WHEN project_id = 13 THEN 1 ELSE 0 END),
|
||||
MAX(CASE WHEN work_status_id = 2 THEN 1 ELSE 0 END)
|
||||
INTO
|
||||
v_total_hours, v_actual_hours, v_vacation_hours,
|
||||
v_total_count, v_regular_count, v_error_count,
|
||||
v_has_vacation, v_has_error
|
||||
FROM daily_work_reports
|
||||
WHERE report_date = p_date AND worker_id = p_worker_id;
|
||||
|
||||
-- 상태 결정 로직
|
||||
IF v_has_error THEN
|
||||
SET v_status = 'error';
|
||||
SET v_has_issues = FALSE;
|
||||
ELSEIF v_total_hours > 12 THEN
|
||||
SET v_status = 'overtime-warning';
|
||||
SET v_has_issues = TRUE;
|
||||
ELSEIF v_has_vacation AND v_vacation_hours > 0 THEN
|
||||
-- 휴가 상태 결정
|
||||
CASE v_vacation_hours
|
||||
WHEN 8 THEN SET v_status = 'vacation-full';
|
||||
WHEN 6 THEN SET v_status = 'vacation-half-half';
|
||||
WHEN 4 THEN SET v_status = 'vacation-half';
|
||||
WHEN 2 THEN SET v_status = 'vacation-quarter';
|
||||
ELSE SET v_status = 'vacation-full';
|
||||
END CASE;
|
||||
SET v_has_issues = FALSE;
|
||||
ELSEIF v_total_hours > 8 THEN
|
||||
SET v_status = 'overtime';
|
||||
SET v_has_issues = FALSE;
|
||||
ELSEIF v_total_hours = 8 THEN
|
||||
SET v_status = 'complete';
|
||||
SET v_has_issues = FALSE;
|
||||
ELSEIF v_total_hours > 0 THEN
|
||||
SET v_status = 'partial';
|
||||
SET v_has_issues = TRUE;
|
||||
ELSE
|
||||
SET v_status = 'incomplete';
|
||||
SET v_has_issues = TRUE;
|
||||
END IF;
|
||||
|
||||
-- 데이터 업서트
|
||||
INSERT INTO monthly_worker_status (
|
||||
year, month, worker_id, date,
|
||||
total_work_hours, actual_work_hours, vacation_hours,
|
||||
total_work_count, regular_work_count, error_work_count,
|
||||
work_status, has_vacation, has_error, has_issues
|
||||
) VALUES (
|
||||
v_year, v_month, p_worker_id, p_date,
|
||||
v_total_hours, v_actual_hours, v_vacation_hours,
|
||||
v_total_count, v_regular_count, v_error_count,
|
||||
v_status, v_has_vacation, v_has_error, v_has_issues
|
||||
) ON DUPLICATE KEY UPDATE
|
||||
total_work_hours = v_total_hours,
|
||||
actual_work_hours = v_actual_hours,
|
||||
vacation_hours = v_vacation_hours,
|
||||
total_work_count = v_total_count,
|
||||
regular_work_count = v_regular_count,
|
||||
error_work_count = v_error_count,
|
||||
work_status = v_status,
|
||||
has_vacation = v_has_vacation,
|
||||
has_error = v_has_error,
|
||||
has_issues = v_has_issues,
|
||||
last_updated = CURRENT_TIMESTAMP;
|
||||
|
||||
-- 일별 요약도 업데이트
|
||||
CALL UpdateDailySummary(p_date);
|
||||
END$$
|
||||
|
||||
-- 일별 요약 업데이트 함수
|
||||
CREATE OR REPLACE PROCEDURE UpdateDailySummary(
|
||||
IN p_date DATE
|
||||
)
|
||||
BEGIN
|
||||
DECLARE v_year INT;
|
||||
DECLARE v_month INT;
|
||||
|
||||
SET v_year = YEAR(p_date);
|
||||
SET v_month = MONTH(p_date);
|
||||
|
||||
INSERT INTO monthly_summary (
|
||||
year, month, date,
|
||||
total_workers, working_workers,
|
||||
incomplete_workers, partial_workers, complete_workers,
|
||||
overtime_workers, vacation_workers, error_workers,
|
||||
total_work_hours, total_work_count, total_error_count,
|
||||
has_issues, has_errors
|
||||
)
|
||||
SELECT
|
||||
v_year, v_month, p_date,
|
||||
COUNT(*) as total_workers,
|
||||
COUNT(CASE WHEN work_status != 'incomplete' THEN 1 END) as working_workers,
|
||||
COUNT(CASE WHEN work_status = 'incomplete' THEN 1 END) as incomplete_workers,
|
||||
COUNT(CASE WHEN work_status = 'partial' THEN 1 END) as partial_workers,
|
||||
COUNT(CASE WHEN work_status IN ('complete') THEN 1 END) as complete_workers,
|
||||
COUNT(CASE WHEN work_status = 'overtime' THEN 1 END) as overtime_workers,
|
||||
COUNT(CASE WHEN work_status LIKE 'vacation%' THEN 1 END) as vacation_workers,
|
||||
COUNT(CASE WHEN work_status = 'error' THEN 1 END) as error_workers,
|
||||
SUM(total_work_hours) as total_work_hours,
|
||||
SUM(total_work_count) as total_work_count,
|
||||
SUM(error_work_count) as total_error_count,
|
||||
MAX(has_issues) as has_issues,
|
||||
MAX(has_error) as has_errors
|
||||
FROM monthly_worker_status
|
||||
WHERE date = p_date
|
||||
ON DUPLICATE KEY UPDATE
|
||||
total_workers = VALUES(total_workers),
|
||||
working_workers = VALUES(working_workers),
|
||||
incomplete_workers = VALUES(incomplete_workers),
|
||||
partial_workers = VALUES(partial_workers),
|
||||
complete_workers = VALUES(complete_workers),
|
||||
overtime_workers = VALUES(overtime_workers),
|
||||
vacation_workers = VALUES(vacation_workers),
|
||||
error_workers = VALUES(error_workers),
|
||||
total_work_hours = VALUES(total_work_hours),
|
||||
total_work_count = VALUES(total_work_count),
|
||||
total_error_count = VALUES(total_error_count),
|
||||
has_issues = VALUES(has_issues),
|
||||
has_errors = VALUES(has_errors),
|
||||
last_updated = CURRENT_TIMESTAMP;
|
||||
END$$
|
||||
|
||||
DELIMITER ;
|
||||
36
api.hyungi.net/migrations/008_create_update_triggers.sql
Normal file
36
api.hyungi.net/migrations/008_create_update_triggers.sql
Normal file
@@ -0,0 +1,36 @@
|
||||
-- 008_create_update_triggers.sql
|
||||
-- 작업보고서 변경 시 월별 집계 자동 업데이트 트리거
|
||||
|
||||
DELIMITER $$
|
||||
|
||||
-- 작업보고서 INSERT 트리거
|
||||
CREATE OR REPLACE TRIGGER tr_daily_work_reports_insert
|
||||
AFTER INSERT ON daily_work_reports
|
||||
FOR EACH ROW
|
||||
BEGIN
|
||||
CALL UpdateMonthlyWorkerStatus(NEW.report_date, NEW.worker_id);
|
||||
END$$
|
||||
|
||||
-- 작업보고서 UPDATE 트리거
|
||||
CREATE OR REPLACE TRIGGER tr_daily_work_reports_update
|
||||
AFTER UPDATE ON daily_work_reports
|
||||
FOR EACH ROW
|
||||
BEGIN
|
||||
-- 기존 날짜 업데이트
|
||||
CALL UpdateMonthlyWorkerStatus(OLD.report_date, OLD.worker_id);
|
||||
|
||||
-- 새 날짜가 다르면 새 날짜도 업데이트
|
||||
IF OLD.report_date != NEW.report_date OR OLD.worker_id != NEW.worker_id THEN
|
||||
CALL UpdateMonthlyWorkerStatus(NEW.report_date, NEW.worker_id);
|
||||
END IF;
|
||||
END$$
|
||||
|
||||
-- 작업보고서 DELETE 트리거
|
||||
CREATE OR REPLACE TRIGGER tr_daily_work_reports_delete
|
||||
AFTER DELETE ON daily_work_reports
|
||||
FOR EACH ROW
|
||||
BEGIN
|
||||
CALL UpdateMonthlyWorkerStatus(OLD.report_date, OLD.worker_id);
|
||||
END$$
|
||||
|
||||
DELIMITER ;
|
||||
@@ -0,0 +1,67 @@
|
||||
-- 009_add_overtime_warning_columns.sql
|
||||
-- monthly_summary 테이블에 12시간 초과(확인필요) 상태 컬럼 추가
|
||||
|
||||
-- monthly_summary 테이블에 컬럼 추가
|
||||
ALTER TABLE monthly_summary
|
||||
ADD COLUMN overtime_warning_workers INT DEFAULT 0 COMMENT '확인필요(12시간초과) 작업자 수' AFTER error_workers,
|
||||
ADD COLUMN has_overtime_warning BOOLEAN DEFAULT FALSE COMMENT '확인필요 상태 있음' AFTER has_errors;
|
||||
|
||||
-- UpdateDailySummary 프로시저 업데이트
|
||||
DROP PROCEDURE IF EXISTS UpdateDailySummary;
|
||||
DELIMITER //
|
||||
CREATE PROCEDURE UpdateDailySummary(
|
||||
IN p_date DATE
|
||||
)
|
||||
BEGIN
|
||||
DECLARE v_year INT;
|
||||
DECLARE v_month INT;
|
||||
|
||||
SET v_year = YEAR(p_date);
|
||||
SET v_month = MONTH(p_date);
|
||||
|
||||
INSERT INTO monthly_summary (
|
||||
year, month, date,
|
||||
total_workers, working_workers,
|
||||
incomplete_workers, partial_workers, complete_workers,
|
||||
overtime_workers, vacation_workers, error_workers, overtime_warning_workers,
|
||||
total_work_hours, total_work_count, total_error_count,
|
||||
has_issues, has_errors, has_overtime_warning
|
||||
)
|
||||
SELECT
|
||||
v_year, v_month, p_date,
|
||||
COUNT(*) as total_workers,
|
||||
COUNT(CASE WHEN work_status != 'incomplete' THEN 1 END) as working_workers,
|
||||
COUNT(CASE WHEN work_status = 'incomplete' THEN 1 END) as incomplete_workers,
|
||||
COUNT(CASE WHEN work_status = 'partial' THEN 1 END) as partial_workers,
|
||||
COUNT(CASE WHEN work_status IN ('complete', 'overtime', 'vacation-full', 'vacation-half', 'vacation-quarter', 'vacation-half-half') THEN 1 END) as complete_workers,
|
||||
COUNT(CASE WHEN work_status = 'overtime' THEN 1 END) as overtime_workers,
|
||||
COUNT(CASE WHEN work_status LIKE 'vacation%' THEN 1 END) as vacation_workers,
|
||||
COUNT(CASE WHEN work_status = 'error' THEN 1 END) as error_workers,
|
||||
COUNT(CASE WHEN work_status = 'overtime-warning' THEN 1 END) as overtime_warning_workers,
|
||||
SUM(total_work_hours) as total_work_hours,
|
||||
SUM(total_work_count) as total_work_count,
|
||||
SUM(error_work_count) as total_error_count,
|
||||
MAX(has_issues) as has_issues,
|
||||
MAX(has_error) as has_errors,
|
||||
MAX(CASE WHEN work_status = 'overtime-warning' THEN 1 ELSE 0 END) as has_overtime_warning
|
||||
FROM monthly_worker_status
|
||||
WHERE date = p_date
|
||||
ON DUPLICATE KEY UPDATE
|
||||
total_workers = VALUES(total_workers),
|
||||
working_workers = VALUES(working_workers),
|
||||
incomplete_workers = VALUES(incomplete_workers),
|
||||
partial_workers = VALUES(partial_workers),
|
||||
complete_workers = VALUES(complete_workers),
|
||||
overtime_workers = VALUES(overtime_workers),
|
||||
vacation_workers = VALUES(vacation_workers),
|
||||
error_workers = VALUES(error_workers),
|
||||
overtime_warning_workers = VALUES(overtime_warning_workers),
|
||||
total_work_hours = VALUES(total_work_hours),
|
||||
total_work_count = VALUES(total_work_count),
|
||||
total_error_count = VALUES(total_error_count),
|
||||
has_issues = VALUES(has_issues),
|
||||
has_errors = VALUES(has_errors),
|
||||
has_overtime_warning = VALUES(has_overtime_warning),
|
||||
last_updated = CURRENT_TIMESTAMP;
|
||||
END //
|
||||
DELIMITER ;
|
||||
303
api.hyungi.net/models/attendanceModel.js
Normal file
303
api.hyungi.net/models/attendanceModel.js
Normal file
@@ -0,0 +1,303 @@
|
||||
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 * 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 * 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 * 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 * 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;
|
||||
@@ -855,17 +855,22 @@ const getReportsWithOptions = async (options) => {
|
||||
let whereConditions = [];
|
||||
let queryParams = [];
|
||||
|
||||
// 날짜 조건 처리 (단일 날짜 또는 날짜 범위)
|
||||
if (options.date) {
|
||||
whereConditions.push('dwr.report_date = ?');
|
||||
queryParams.push(options.date);
|
||||
} else if (options.start_date && options.end_date) {
|
||||
whereConditions.push('dwr.report_date BETWEEN ? AND ?');
|
||||
queryParams.push(options.start_date, options.end_date);
|
||||
}
|
||||
|
||||
if (options.worker_id) {
|
||||
whereConditions.push('dwr.worker_id = ?');
|
||||
queryParams.push(options.worker_id);
|
||||
}
|
||||
if (options.created_by) {
|
||||
if (options.created_by_user_id) {
|
||||
whereConditions.push('dwr.created_by = ?');
|
||||
queryParams.push(options.created_by);
|
||||
queryParams.push(options.created_by_user_id);
|
||||
}
|
||||
// 필요에 따라 다른 조건 추가 가능 (project_id 등)
|
||||
|
||||
|
||||
156
api.hyungi.net/models/monthlyStatusModel.js
Normal file
156
api.hyungi.net/models/monthlyStatusModel.js
Normal file
@@ -0,0 +1,156 @@
|
||||
// models/monthlyStatusModel.js
|
||||
// 월별 작업자 상태 집계 모델
|
||||
|
||||
const { getDb } = require('../dbPool');
|
||||
|
||||
class MonthlyStatusModel {
|
||||
// 월별 일자별 요약 조회 (캘린더용)
|
||||
static async getMonthlySummary(year, month) {
|
||||
const db = await getDb();
|
||||
|
||||
try {
|
||||
const [rows] = await db.execute(`
|
||||
SELECT
|
||||
date,
|
||||
total_workers,
|
||||
working_workers,
|
||||
incomplete_workers,
|
||||
partial_workers,
|
||||
complete_workers,
|
||||
overtime_workers,
|
||||
vacation_workers,
|
||||
error_workers,
|
||||
overtime_warning_workers,
|
||||
total_work_hours,
|
||||
total_work_count,
|
||||
total_error_count,
|
||||
has_issues,
|
||||
has_errors,
|
||||
has_overtime_warning,
|
||||
last_updated
|
||||
FROM monthly_summary
|
||||
WHERE year = ? AND month = ?
|
||||
ORDER BY date ASC
|
||||
`, [year, month]);
|
||||
|
||||
return rows;
|
||||
} catch (error) {
|
||||
console.error('월별 요약 조회 오류:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 특정 날짜의 작업자별 상태 조회 (모달용)
|
||||
static async getDailyWorkerStatus(date) {
|
||||
const db = await getDb();
|
||||
|
||||
try {
|
||||
const [rows] = await db.execute(`
|
||||
SELECT
|
||||
mws.*,
|
||||
w.worker_name,
|
||||
w.job_type
|
||||
FROM monthly_worker_status mws
|
||||
JOIN workers w ON mws.worker_id = w.worker_id
|
||||
WHERE mws.date = ?
|
||||
ORDER BY w.worker_name ASC
|
||||
`, [date]);
|
||||
|
||||
return rows;
|
||||
} catch (error) {
|
||||
console.error('일별 작업자 상태 조회 오류:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 월별 집계 데이터 강제 재계산 (관리용)
|
||||
static async recalculateMonth(year, month) {
|
||||
const db = await getDb();
|
||||
|
||||
try {
|
||||
// 해당 월의 모든 날짜와 작업자 조합을 찾아서 재계산
|
||||
const [workDates] = await db.execute(`
|
||||
SELECT DISTINCT report_date, worker_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]);
|
||||
updatedCount++;
|
||||
}
|
||||
|
||||
console.log(`✅ ${year}년 ${month}월 집계 재계산 완료: ${updatedCount}건`);
|
||||
return { success: true, updatedCount };
|
||||
|
||||
} catch (error) {
|
||||
console.error('월별 집계 재계산 오류:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 특정 날짜 집계 강제 업데이트
|
||||
static async updateDateSummary(date, workerId = null) {
|
||||
const db = await getDb();
|
||||
|
||||
try {
|
||||
if (workerId) {
|
||||
// 특정 작업자만 업데이트
|
||||
await db.execute('CALL UpdateMonthlyWorkerStatus(?, ?)', [date, workerId]);
|
||||
} else {
|
||||
// 해당 날짜의 모든 작업자 업데이트
|
||||
const [workers] = await db.execute(`
|
||||
SELECT DISTINCT worker_id
|
||||
FROM daily_work_reports
|
||||
WHERE report_date = ?
|
||||
`, [date]);
|
||||
|
||||
for (const { worker_id } of workers) {
|
||||
await db.execute('CALL UpdateMonthlyWorkerStatus(?, ?)', [date, worker_id]);
|
||||
}
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('날짜별 집계 업데이트 오류:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 집계 테이블 상태 확인
|
||||
static async getStatusInfo() {
|
||||
const db = await getDb();
|
||||
|
||||
try {
|
||||
const [summaryCount] = await db.execute(`
|
||||
SELECT
|
||||
COUNT(*) as total_days,
|
||||
MIN(date) as earliest_date,
|
||||
MAX(date) as latest_date,
|
||||
MAX(last_updated) as last_update
|
||||
FROM monthly_summary
|
||||
`);
|
||||
|
||||
const [workerStatusCount] = await db.execute(`
|
||||
SELECT
|
||||
COUNT(*) as total_records,
|
||||
COUNT(DISTINCT worker_id) as unique_workers,
|
||||
COUNT(DISTINCT date) as unique_dates,
|
||||
MAX(last_updated) as last_update
|
||||
FROM monthly_worker_status
|
||||
`);
|
||||
|
||||
return {
|
||||
summary: summaryCount[0],
|
||||
workerStatus: workerStatusCount[0]
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('집계 테이블 상태 확인 오류:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MonthlyStatusModel;
|
||||
37
api.hyungi.net/routes/attendanceRoutes.js
Normal file
37
api.hyungi.net/routes/attendanceRoutes.js
Normal file
@@ -0,0 +1,37 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const AttendanceController = require('../controllers/attendanceController');
|
||||
const { verifyToken } = require('../middlewares/authMiddleware');
|
||||
|
||||
// 모든 라우트에 인증 미들웨어 적용
|
||||
router.use(verifyToken);
|
||||
|
||||
// 일일 근태 현황 조회 (대시보드용)
|
||||
router.get('/daily-status', AttendanceController.getDailyAttendanceStatus);
|
||||
|
||||
// 일일 근태 기록 조회
|
||||
router.get('/daily-records', AttendanceController.getDailyAttendanceRecords);
|
||||
|
||||
// 근태 기록 생성/업데이트
|
||||
router.post('/records', AttendanceController.upsertAttendanceRecord);
|
||||
router.put('/records', AttendanceController.upsertAttendanceRecord);
|
||||
|
||||
// 휴가 처리
|
||||
router.post('/vacation', AttendanceController.processVacation);
|
||||
|
||||
// 초과근무 승인
|
||||
router.post('/overtime/approve', AttendanceController.approveOvertime);
|
||||
|
||||
// 근로 유형 목록 조회
|
||||
router.get('/attendance-types', AttendanceController.getAttendanceTypes);
|
||||
|
||||
// 휴가 유형 목록 조회
|
||||
router.get('/vacation-types', AttendanceController.getVacationTypes);
|
||||
|
||||
// 작업자 휴가 잔여 조회
|
||||
router.get('/vacation-balance/:worker_id', AttendanceController.getWorkerVacationBalance);
|
||||
|
||||
// 월별 근태 통계
|
||||
router.get('/monthly-stats', AttendanceController.getMonthlyAttendanceStats);
|
||||
|
||||
module.exports = router;
|
||||
24
api.hyungi.net/routes/monthlyStatusRoutes.js
Normal file
24
api.hyungi.net/routes/monthlyStatusRoutes.js
Normal file
@@ -0,0 +1,24 @@
|
||||
// routes/monthlyStatusRoutes.js
|
||||
// 월별 작업자 상태 집계 라우트
|
||||
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const MonthlyStatusController = require('../controllers/monthlyStatusController');
|
||||
const { verifyToken } = require('../middlewares/authMiddleware');
|
||||
|
||||
// 모든 라우트에 인증 미들웨어 적용 (임시로 주석 처리 - 테스트용)
|
||||
// router.use(verifyToken);
|
||||
|
||||
// 월별 캘린더 데이터 조회 (캘린더 페이지용)
|
||||
router.get('/calendar', MonthlyStatusController.getMonthlyCalendarData);
|
||||
|
||||
// 특정 날짜의 작업자별 상세 상태 조회 (모달용)
|
||||
router.get('/daily-details', MonthlyStatusController.getDailyWorkerDetails);
|
||||
|
||||
// 월별 집계 재계산 (관리자용)
|
||||
router.post('/recalculate', MonthlyStatusController.recalculateMonth);
|
||||
|
||||
// 집계 테이블 상태 확인 (관리자용)
|
||||
router.get('/status', MonthlyStatusController.getStatusInfo);
|
||||
|
||||
module.exports = router;
|
||||
717
api.hyungi.net/routes/setupRoutes.js
Normal file
717
api.hyungi.net/routes/setupRoutes.js
Normal file
@@ -0,0 +1,717 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { getDb } = require('../dbPool');
|
||||
|
||||
// DB 설정 엔드포인트 (개발용 - 인증 없이 접근 가능)
|
||||
// 월별 집계 테이블 설정
|
||||
router.post('/setup-monthly-status', async (req, res) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
|
||||
console.log('📊 월별 집계 테이블 생성 중...');
|
||||
|
||||
// 1. 월별 작업자 상태 집계 테이블
|
||||
await db.execute(`
|
||||
CREATE TABLE IF NOT EXISTS monthly_worker_status (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
year INT NOT NULL COMMENT '연도',
|
||||
month INT NOT NULL COMMENT '월 (1-12)',
|
||||
worker_id INT NOT NULL COMMENT '작업자 ID',
|
||||
date DATE NOT NULL COMMENT '날짜',
|
||||
|
||||
total_work_hours DECIMAL(5,2) DEFAULT 0.00 COMMENT '총 작업시간',
|
||||
actual_work_hours DECIMAL(5,2) DEFAULT 0.00 COMMENT '실제 작업시간 (휴가 제외)',
|
||||
vacation_hours DECIMAL(5,2) DEFAULT 0.00 COMMENT '휴가 시간',
|
||||
|
||||
total_work_count INT DEFAULT 0 COMMENT '총 작업 건수',
|
||||
regular_work_count INT DEFAULT 0 COMMENT '정규 작업 건수',
|
||||
error_work_count INT DEFAULT 0 COMMENT '오류 작업 건수',
|
||||
|
||||
work_status ENUM(
|
||||
'incomplete', 'partial', 'complete', 'overtime',
|
||||
'vacation-full', 'vacation-half', 'vacation-quarter', 'vacation-half-half',
|
||||
'error', 'overtime-warning'
|
||||
) NOT NULL DEFAULT 'incomplete' COMMENT '작업 상태',
|
||||
|
||||
has_vacation BOOLEAN DEFAULT FALSE COMMENT '휴가 여부',
|
||||
has_error BOOLEAN DEFAULT FALSE COMMENT '오류 여부',
|
||||
has_issues BOOLEAN DEFAULT FALSE COMMENT '문제 여부 (미입력/부분입력)',
|
||||
|
||||
last_updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
UNIQUE KEY unique_worker_date (worker_id, date),
|
||||
KEY idx_year_month (year, month),
|
||||
KEY idx_worker_year_month (worker_id, year, month),
|
||||
KEY idx_status (work_status),
|
||||
KEY idx_has_issues (has_issues),
|
||||
KEY idx_has_error (has_error),
|
||||
|
||||
FOREIGN KEY (worker_id) REFERENCES workers(worker_id) ON DELETE CASCADE
|
||||
) COMMENT='월별 작업자 상태 집계 테이블'
|
||||
`);
|
||||
|
||||
// 2. 월별 집계 요약 테이블
|
||||
await db.execute(`
|
||||
CREATE TABLE IF NOT EXISTS monthly_summary (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
year INT NOT NULL COMMENT '연도',
|
||||
month INT NOT NULL COMMENT '월 (1-12)',
|
||||
date DATE NOT NULL COMMENT '날짜',
|
||||
|
||||
total_workers INT DEFAULT 0 COMMENT '총 작업자 수',
|
||||
working_workers INT DEFAULT 0 COMMENT '작업한 작업자 수',
|
||||
|
||||
incomplete_workers INT DEFAULT 0 COMMENT '미입력 작업자 수',
|
||||
partial_workers INT DEFAULT 0 COMMENT '부분입력 작업자 수',
|
||||
complete_workers INT DEFAULT 0 COMMENT '완료 작업자 수',
|
||||
overtime_workers INT DEFAULT 0 COMMENT '연장근로 작업자 수',
|
||||
vacation_workers INT DEFAULT 0 COMMENT '휴가 작업자 수',
|
||||
error_workers INT DEFAULT 0 COMMENT '오류 작업자 수',
|
||||
|
||||
total_work_hours DECIMAL(8,2) DEFAULT 0.00 COMMENT '총 작업시간',
|
||||
total_work_count INT DEFAULT 0 COMMENT '총 작업 건수',
|
||||
total_error_count INT DEFAULT 0 COMMENT '총 오류 건수',
|
||||
|
||||
has_issues BOOLEAN DEFAULT FALSE COMMENT '문제 있음 (미입력/부분입력)',
|
||||
has_errors BOOLEAN DEFAULT FALSE COMMENT '오류 있음',
|
||||
|
||||
last_updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
UNIQUE KEY unique_date (date),
|
||||
KEY idx_year_month (year, month),
|
||||
KEY idx_has_issues (has_issues),
|
||||
KEY idx_has_errors (has_errors)
|
||||
) COMMENT='월별 일자별 요약 테이블 (캘린더 최적화용)'
|
||||
`);
|
||||
|
||||
console.log('📊 집계 프로시저 생성 중...');
|
||||
|
||||
// 3. 집계 업데이트 프로시저
|
||||
await db.execute(`DROP PROCEDURE IF EXISTS UpdateMonthlyWorkerStatus`);
|
||||
await db.execute(`
|
||||
CREATE PROCEDURE UpdateMonthlyWorkerStatus(
|
||||
IN p_date DATE,
|
||||
IN p_worker_id INT
|
||||
)
|
||||
BEGIN
|
||||
DECLARE v_year INT;
|
||||
DECLARE v_month INT;
|
||||
DECLARE v_total_hours DECIMAL(5,2);
|
||||
DECLARE v_actual_hours DECIMAL(5,2);
|
||||
DECLARE v_vacation_hours DECIMAL(5,2);
|
||||
DECLARE v_total_count INT;
|
||||
DECLARE v_regular_count INT;
|
||||
DECLARE v_error_count INT;
|
||||
DECLARE v_has_vacation BOOLEAN;
|
||||
DECLARE v_has_error BOOLEAN;
|
||||
DECLARE v_has_issues BOOLEAN;
|
||||
DECLARE v_status VARCHAR(20);
|
||||
|
||||
SET v_year = YEAR(p_date);
|
||||
SET v_month = MONTH(p_date);
|
||||
|
||||
SELECT
|
||||
COALESCE(SUM(work_hours), 0),
|
||||
COALESCE(SUM(CASE WHEN project_id != 13 THEN work_hours ELSE 0 END), 0),
|
||||
COALESCE(SUM(CASE WHEN project_id = 13 THEN work_hours ELSE 0 END), 0),
|
||||
COUNT(*),
|
||||
COUNT(CASE WHEN project_id != 13 AND work_status_id != 2 THEN 1 END),
|
||||
COUNT(CASE WHEN work_status_id = 2 THEN 1 END),
|
||||
MAX(CASE WHEN project_id = 13 THEN 1 ELSE 0 END),
|
||||
MAX(CASE WHEN work_status_id = 2 THEN 1 ELSE 0 END)
|
||||
INTO
|
||||
v_total_hours, v_actual_hours, v_vacation_hours,
|
||||
v_total_count, v_regular_count, v_error_count,
|
||||
v_has_vacation, v_has_error
|
||||
FROM daily_work_reports
|
||||
WHERE report_date = p_date AND worker_id = p_worker_id;
|
||||
|
||||
IF v_has_error THEN
|
||||
SET v_status = 'error';
|
||||
SET v_has_issues = FALSE;
|
||||
ELSEIF v_total_hours > 12 THEN
|
||||
SET v_status = 'overtime-warning';
|
||||
SET v_has_issues = TRUE;
|
||||
ELSEIF v_has_vacation AND v_vacation_hours > 0 THEN
|
||||
CASE v_vacation_hours
|
||||
WHEN 8 THEN SET v_status = 'vacation-full';
|
||||
WHEN 6 THEN SET v_status = 'vacation-half-half';
|
||||
WHEN 4 THEN SET v_status = 'vacation-half';
|
||||
WHEN 2 THEN SET v_status = 'vacation-quarter';
|
||||
ELSE SET v_status = 'vacation-full';
|
||||
END CASE;
|
||||
SET v_has_issues = FALSE;
|
||||
ELSEIF v_total_hours > 8 THEN
|
||||
SET v_status = 'overtime';
|
||||
SET v_has_issues = FALSE;
|
||||
ELSEIF v_total_hours = 8 THEN
|
||||
SET v_status = 'complete';
|
||||
SET v_has_issues = FALSE;
|
||||
ELSEIF v_total_hours > 0 THEN
|
||||
SET v_status = 'partial';
|
||||
SET v_has_issues = TRUE;
|
||||
ELSE
|
||||
SET v_status = 'incomplete';
|
||||
SET v_has_issues = TRUE;
|
||||
END IF;
|
||||
|
||||
INSERT INTO monthly_worker_status (
|
||||
year, month, worker_id, date,
|
||||
total_work_hours, actual_work_hours, vacation_hours,
|
||||
total_work_count, regular_work_count, error_work_count,
|
||||
work_status, has_vacation, has_error, has_issues
|
||||
) VALUES (
|
||||
v_year, v_month, p_worker_id, p_date,
|
||||
v_total_hours, v_actual_hours, v_vacation_hours,
|
||||
v_total_count, v_regular_count, v_error_count,
|
||||
v_status, v_has_vacation, v_has_error, v_has_issues
|
||||
) ON DUPLICATE KEY UPDATE
|
||||
total_work_hours = v_total_hours,
|
||||
actual_work_hours = v_actual_hours,
|
||||
vacation_hours = v_vacation_hours,
|
||||
total_work_count = v_total_count,
|
||||
regular_work_count = v_regular_count,
|
||||
error_work_count = v_error_count,
|
||||
work_status = v_status,
|
||||
has_vacation = v_has_vacation,
|
||||
has_error = v_has_error,
|
||||
has_issues = v_has_issues,
|
||||
last_updated = CURRENT_TIMESTAMP;
|
||||
|
||||
CALL UpdateDailySummary(p_date);
|
||||
END
|
||||
`);
|
||||
|
||||
await db.execute(`DROP PROCEDURE IF EXISTS UpdateDailySummary`);
|
||||
await db.execute(`
|
||||
CREATE PROCEDURE UpdateDailySummary(
|
||||
IN p_date DATE
|
||||
)
|
||||
BEGIN
|
||||
DECLARE v_year INT;
|
||||
DECLARE v_month INT;
|
||||
|
||||
SET v_year = YEAR(p_date);
|
||||
SET v_month = MONTH(p_date);
|
||||
|
||||
INSERT INTO monthly_summary (
|
||||
year, month, date,
|
||||
total_workers, working_workers,
|
||||
incomplete_workers, partial_workers, complete_workers,
|
||||
overtime_workers, vacation_workers, error_workers,
|
||||
total_work_hours, total_work_count, total_error_count,
|
||||
has_issues, has_errors
|
||||
)
|
||||
SELECT
|
||||
v_year, v_month, p_date,
|
||||
COUNT(*) as total_workers,
|
||||
COUNT(CASE WHEN work_status != 'incomplete' THEN 1 END) as working_workers,
|
||||
COUNT(CASE WHEN work_status = 'incomplete' THEN 1 END) as incomplete_workers,
|
||||
COUNT(CASE WHEN work_status = 'partial' THEN 1 END) as partial_workers,
|
||||
COUNT(CASE WHEN work_status IN ('complete') THEN 1 END) as complete_workers,
|
||||
COUNT(CASE WHEN work_status = 'overtime' THEN 1 END) as overtime_workers,
|
||||
COUNT(CASE WHEN work_status LIKE 'vacation%' THEN 1 END) as vacation_workers,
|
||||
COUNT(CASE WHEN work_status = 'error' THEN 1 END) as error_workers,
|
||||
SUM(total_work_hours) as total_work_hours,
|
||||
SUM(total_work_count) as total_work_count,
|
||||
SUM(error_work_count) as total_error_count,
|
||||
MAX(has_issues) as has_issues,
|
||||
MAX(has_error) as has_errors
|
||||
FROM monthly_worker_status
|
||||
WHERE date = p_date
|
||||
ON DUPLICATE KEY UPDATE
|
||||
total_workers = VALUES(total_workers),
|
||||
working_workers = VALUES(working_workers),
|
||||
incomplete_workers = VALUES(incomplete_workers),
|
||||
partial_workers = VALUES(partial_workers),
|
||||
complete_workers = VALUES(complete_workers),
|
||||
overtime_workers = VALUES(overtime_workers),
|
||||
vacation_workers = VALUES(vacation_workers),
|
||||
error_workers = VALUES(error_workers),
|
||||
total_work_hours = VALUES(total_work_hours),
|
||||
total_work_count = VALUES(total_work_count),
|
||||
total_error_count = VALUES(total_error_count),
|
||||
has_issues = VALUES(has_issues),
|
||||
has_errors = VALUES(has_errors),
|
||||
last_updated = CURRENT_TIMESTAMP;
|
||||
END
|
||||
`);
|
||||
|
||||
console.log('📊 트리거 생성 중...');
|
||||
|
||||
// 4. 트리거 생성
|
||||
await db.execute(`DROP TRIGGER IF EXISTS tr_daily_work_reports_insert`);
|
||||
await db.execute(`
|
||||
CREATE TRIGGER tr_daily_work_reports_insert
|
||||
AFTER INSERT ON daily_work_reports
|
||||
FOR EACH ROW
|
||||
BEGIN
|
||||
CALL UpdateMonthlyWorkerStatus(NEW.report_date, NEW.worker_id);
|
||||
END
|
||||
`);
|
||||
|
||||
await db.execute(`DROP TRIGGER IF EXISTS tr_daily_work_reports_update`);
|
||||
await db.execute(`
|
||||
CREATE TRIGGER tr_daily_work_reports_update
|
||||
AFTER UPDATE ON daily_work_reports
|
||||
FOR EACH ROW
|
||||
BEGIN
|
||||
CALL UpdateMonthlyWorkerStatus(OLD.report_date, OLD.worker_id);
|
||||
|
||||
IF OLD.report_date != NEW.report_date OR OLD.worker_id != NEW.worker_id THEN
|
||||
CALL UpdateMonthlyWorkerStatus(NEW.report_date, NEW.worker_id);
|
||||
END IF;
|
||||
END
|
||||
`);
|
||||
|
||||
await db.execute(`DROP TRIGGER IF EXISTS tr_daily_work_reports_delete`);
|
||||
await db.execute(`
|
||||
CREATE TRIGGER tr_daily_work_reports_delete
|
||||
AFTER DELETE ON daily_work_reports
|
||||
FOR EACH ROW
|
||||
BEGIN
|
||||
CALL UpdateMonthlyWorkerStatus(OLD.report_date, OLD.worker_id);
|
||||
END
|
||||
`);
|
||||
|
||||
console.log('📊 기존 데이터로 집계 테이블 초기화 중...');
|
||||
|
||||
// 5. 기존 작업 데이터로 집계 테이블 초기화
|
||||
const [existingDates] = await db.execute(`
|
||||
SELECT DISTINCT report_date, worker_id
|
||||
FROM daily_work_reports
|
||||
WHERE report_date >= '2025-01-01'
|
||||
ORDER BY report_date DESC, worker_id ASC
|
||||
`);
|
||||
|
||||
let processedCount = 0;
|
||||
const batchSize = 50;
|
||||
|
||||
for (let i = 0; i < existingDates.length; i += batchSize) {
|
||||
const batch = existingDates.slice(i, i + batchSize);
|
||||
|
||||
for (const { report_date, worker_id } of batch) {
|
||||
try {
|
||||
await db.execute('CALL UpdateMonthlyWorkerStatus(?, ?)', [report_date, worker_id]);
|
||||
processedCount++;
|
||||
} catch (error) {
|
||||
console.warn(`집계 처리 실패: ${report_date}, worker ${worker_id}:`, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
if (i % 100 === 0) {
|
||||
console.log(`📊 집계 초기화 진행률: ${processedCount}/${existingDates.length}`);
|
||||
}
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '월별 집계 시스템이 성공적으로 설정되었습니다.',
|
||||
data: {
|
||||
tables_created: [
|
||||
'monthly_worker_status',
|
||||
'monthly_summary'
|
||||
],
|
||||
procedures_created: [
|
||||
'UpdateMonthlyWorkerStatus',
|
||||
'UpdateDailySummary'
|
||||
],
|
||||
triggers_created: [
|
||||
'tr_daily_work_reports_insert',
|
||||
'tr_daily_work_reports_update',
|
||||
'tr_daily_work_reports_delete'
|
||||
],
|
||||
initialized_records: processedCount,
|
||||
total_dates: existingDates.length
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 월별 집계 시스템 설정 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '월별 집계 시스템 설정 중 오류가 발생했습니다.',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/setup-attendance-db', async (req, res) => {
|
||||
try {
|
||||
console.log('🚀 근태 관리 DB 설정 API 호출됨');
|
||||
|
||||
const db = await getDb();
|
||||
|
||||
// 1. 근로 유형 테이블 생성
|
||||
console.log('📋 근로 유형 테이블 생성 중...');
|
||||
await db.execute(`
|
||||
CREATE TABLE IF NOT EXISTS work_attendance_types (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
type_code VARCHAR(20) NOT NULL UNIQUE COMMENT '근로 유형 코드',
|
||||
type_name VARCHAR(50) NOT NULL COMMENT '근로 유형명',
|
||||
description TEXT COMMENT '설명',
|
||||
is_active BOOLEAN DEFAULT TRUE COMMENT '활성 상태',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
) COMMENT='근로 유형 관리 테이블'
|
||||
`);
|
||||
|
||||
// 2. 휴가 유형 테이블 생성
|
||||
console.log('🏖️ 휴가 유형 테이블 생성 중...');
|
||||
await db.execute(`
|
||||
CREATE TABLE IF NOT EXISTS vacation_types (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
type_code VARCHAR(20) NOT NULL UNIQUE COMMENT '휴가 유형 코드',
|
||||
type_name VARCHAR(50) NOT NULL COMMENT '휴가 유형명',
|
||||
hours_deduction DECIMAL(4,2) NOT NULL COMMENT '차감 시간',
|
||||
description TEXT COMMENT '설명',
|
||||
is_active BOOLEAN DEFAULT TRUE COMMENT '활성 상태',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
) COMMENT='휴가 유형 관리 테이블'
|
||||
`);
|
||||
|
||||
// 3. 일일 근태 기록 테이블 생성
|
||||
console.log('📊 일일 근태 기록 테이블 생성 중...');
|
||||
await db.execute(`
|
||||
CREATE TABLE IF NOT EXISTS daily_attendance_records (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
record_date DATE NOT NULL COMMENT '기록 날짜',
|
||||
worker_id INT NOT NULL COMMENT '작업자 ID',
|
||||
work_attendance_type_id INT COMMENT '근로 유형 ID (정시, 연장, 부분, 휴가)',
|
||||
total_work_hours DECIMAL(4,2) DEFAULT 0.00 COMMENT '총 작업 시간',
|
||||
vacation_type_id INT COMMENT '휴가 유형 ID (연차, 반차 등)',
|
||||
is_overtime_approved BOOLEAN DEFAULT FALSE COMMENT '연장근로 승인 여부',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
UNIQUE KEY unique_worker_date (worker_id, record_date),
|
||||
FOREIGN KEY (worker_id) REFERENCES workers(worker_id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (work_attendance_type_id) REFERENCES work_attendance_types(id) ON DELETE SET NULL,
|
||||
FOREIGN KEY (vacation_type_id) REFERENCES vacation_types(id) ON DELETE SET NULL
|
||||
) COMMENT='일일 근태 기록 테이블'
|
||||
`);
|
||||
|
||||
// 4. 작업자별 휴가 잔여 관리 테이블 생성
|
||||
console.log('👥 작업자별 휴가 잔여 관리 테이블 생성 중...');
|
||||
await db.execute(`
|
||||
CREATE TABLE IF NOT EXISTS worker_vacation_balance (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
worker_id INT NOT NULL UNIQUE COMMENT '작업자 ID',
|
||||
annual_leave_total DECIMAL(5,2) DEFAULT 15.00 COMMENT '총 연차 일수',
|
||||
annual_leave_used DECIMAL(5,2) DEFAULT 0.00 COMMENT '사용 연차 일수',
|
||||
sick_leave_total DECIMAL(5,2) DEFAULT 10.00 COMMENT '총 병가 일수',
|
||||
sick_leave_used DECIMAL(5,2) DEFAULT 0.00 COMMENT '사용 병가 일수',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (worker_id) REFERENCES workers(worker_id) ON DELETE CASCADE
|
||||
) COMMENT='작업자별 휴가 잔여 관리 테이블'
|
||||
`);
|
||||
|
||||
// 5. 기본 데이터 삽입
|
||||
console.log('📝 기본 데이터 삽입 중...');
|
||||
|
||||
// 근로 유형 기본 데이터
|
||||
await db.execute(`
|
||||
INSERT IGNORE INTO work_attendance_types (type_code, type_name, description) VALUES
|
||||
('REGULAR', '정시근로', '8시간 정규 근무'),
|
||||
('OVERTIME', '연장근로', '8시간 초과 근무'),
|
||||
('PARTIAL', '부분근로', '8시간 미만 근무'),
|
||||
('VACATION', '휴가근로', '휴가와 함께하는 부분 근무')
|
||||
`);
|
||||
|
||||
// 휴가 유형 기본 데이터
|
||||
await db.execute(`
|
||||
INSERT IGNORE INTO vacation_types (type_code, type_name, hours_deduction, description) VALUES
|
||||
('ANNUAL_FULL', '연차', 8.0, '하루 전체 연차'),
|
||||
('ANNUAL_HALF', '반차', 4.0, '반일 연차'),
|
||||
('ANNUAL_QUARTER', '반반차', 2.0, '1/4일 연차'),
|
||||
('SICK_FULL', '병가', 8.0, '하루 전체 병가'),
|
||||
('SICK_HALF', '반일병가', 4.0, '반일 병가')
|
||||
`);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '근태 관리 DB 설정이 완료되었습니다.',
|
||||
data: {
|
||||
tables_created: [
|
||||
'work_attendance_types',
|
||||
'vacation_types',
|
||||
'daily_attendance_records',
|
||||
'worker_vacation_balance'
|
||||
],
|
||||
basic_data_inserted: true
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ DB 설정 API 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'DB 설정 중 오류가 발생했습니다.',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 12시간 초과 상태 컬럼 추가
|
||||
router.post('/add-overtime-warning', async (req, res) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
|
||||
console.log('⚠️ 12시간 초과 상태 컬럼 추가 중...');
|
||||
|
||||
// 1. monthly_summary 테이블에 컬럼 추가
|
||||
try {
|
||||
await db.execute(`
|
||||
ALTER TABLE monthly_summary
|
||||
ADD COLUMN overtime_warning_workers INT DEFAULT 0 COMMENT '확인필요(12시간초과) 작업자 수' AFTER error_workers
|
||||
`);
|
||||
console.log('✅ overtime_warning_workers 컬럼 추가 완료');
|
||||
} catch (error) {
|
||||
if (error.code === 'ER_DUP_FIELDNAME') {
|
||||
console.log('ℹ️ overtime_warning_workers 컬럼이 이미 존재합니다.');
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await db.execute(`
|
||||
ALTER TABLE monthly_summary
|
||||
ADD COLUMN has_overtime_warning BOOLEAN DEFAULT FALSE COMMENT '확인필요 상태 있음' AFTER has_errors
|
||||
`);
|
||||
console.log('✅ has_overtime_warning 컬럼 추가 완료');
|
||||
} catch (error) {
|
||||
if (error.code === 'ER_DUP_FIELDNAME') {
|
||||
console.log('ℹ️ has_overtime_warning 컬럼이 이미 존재합니다.');
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. UpdateDailySummary 프로시저 업데이트
|
||||
await db.execute(`DROP PROCEDURE IF EXISTS UpdateDailySummary`);
|
||||
|
||||
await db.execute(`
|
||||
CREATE PROCEDURE UpdateDailySummary(
|
||||
IN p_date DATE
|
||||
)
|
||||
BEGIN
|
||||
DECLARE v_year INT;
|
||||
DECLARE v_month INT;
|
||||
|
||||
SET v_year = YEAR(p_date);
|
||||
SET v_month = MONTH(p_date);
|
||||
|
||||
INSERT INTO monthly_summary (
|
||||
year, month, date,
|
||||
total_workers, working_workers,
|
||||
incomplete_workers, partial_workers, complete_workers,
|
||||
overtime_workers, vacation_workers, error_workers, overtime_warning_workers,
|
||||
total_work_hours, total_work_count, total_error_count,
|
||||
has_issues, has_errors, has_overtime_warning
|
||||
)
|
||||
SELECT
|
||||
v_year, v_month, p_date,
|
||||
COUNT(*) as total_workers,
|
||||
COUNT(CASE WHEN work_status != 'incomplete' THEN 1 END) as working_workers,
|
||||
COUNT(CASE WHEN work_status = 'incomplete' THEN 1 END) as incomplete_workers,
|
||||
COUNT(CASE WHEN work_status = 'partial' THEN 1 END) as partial_workers,
|
||||
COUNT(CASE WHEN work_status IN ('complete', 'overtime', 'vacation-full', 'vacation-half', 'vacation-quarter', 'vacation-half-half') THEN 1 END) as complete_workers,
|
||||
COUNT(CASE WHEN work_status = 'overtime' THEN 1 END) as overtime_workers,
|
||||
COUNT(CASE WHEN work_status LIKE 'vacation%' THEN 1 END) as vacation_workers,
|
||||
COUNT(CASE WHEN work_status = 'error' THEN 1 END) as error_workers,
|
||||
COUNT(CASE WHEN work_status = 'overtime-warning' THEN 1 END) as overtime_warning_workers,
|
||||
SUM(total_work_hours) as total_work_hours,
|
||||
SUM(total_work_count) as total_work_count,
|
||||
SUM(error_work_count) as total_error_count,
|
||||
MAX(has_issues) as has_issues,
|
||||
MAX(has_error) as has_errors,
|
||||
MAX(CASE WHEN work_status = 'overtime-warning' THEN 1 ELSE 0 END) as has_overtime_warning
|
||||
FROM monthly_worker_status
|
||||
WHERE date = p_date
|
||||
ON DUPLICATE KEY UPDATE
|
||||
total_workers = VALUES(total_workers),
|
||||
working_workers = VALUES(working_workers),
|
||||
incomplete_workers = VALUES(incomplete_workers),
|
||||
partial_workers = VALUES(partial_workers),
|
||||
complete_workers = VALUES(complete_workers),
|
||||
overtime_workers = VALUES(overtime_workers),
|
||||
vacation_workers = VALUES(vacation_workers),
|
||||
error_workers = VALUES(error_workers),
|
||||
overtime_warning_workers = VALUES(overtime_warning_workers),
|
||||
total_work_hours = VALUES(total_work_hours),
|
||||
total_work_count = VALUES(total_work_count),
|
||||
total_error_count = VALUES(total_error_count),
|
||||
has_issues = VALUES(has_issues),
|
||||
has_errors = VALUES(has_errors),
|
||||
has_overtime_warning = VALUES(has_overtime_warning),
|
||||
last_updated = CURRENT_TIMESTAMP;
|
||||
END
|
||||
`);
|
||||
console.log('✅ UpdateDailySummary 프로시저 업데이트 완료');
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '12시간 초과 상태 컬럼 추가 완료',
|
||||
columns_added: ['overtime_warning_workers', 'has_overtime_warning'],
|
||||
procedure_updated: 'UpdateDailySummary'
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 12시간 초과 상태 설정 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '12시간 초과 상태 설정 실패',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 기존 데이터를 월별 집계 테이블로 마이그레이션
|
||||
router.post('/migrate-existing-data', async (req, res) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
|
||||
console.log('🔄 기존 데이터 마이그레이션 시작...');
|
||||
|
||||
// 1. 기존 데이터 범위 확인
|
||||
const [dateRange] = await db.execute(`
|
||||
SELECT
|
||||
MIN(report_date) as min_date,
|
||||
MAX(report_date) as max_date,
|
||||
COUNT(*) as total_reports
|
||||
FROM daily_work_reports
|
||||
`);
|
||||
|
||||
if (dateRange.length === 0 || !dateRange[0].min_date) {
|
||||
return res.json({
|
||||
success: true,
|
||||
message: '마이그레이션할 데이터가 없습니다.',
|
||||
migrated_count: 0
|
||||
});
|
||||
}
|
||||
|
||||
const { min_date, max_date, total_reports } = dateRange[0];
|
||||
console.log(`📊 데이터 범위: ${min_date} ~ ${max_date} (총 ${total_reports}건)`);
|
||||
|
||||
// 2. 기존 monthly_worker_status, monthly_summary 데이터 삭제
|
||||
await db.execute('DELETE FROM monthly_summary');
|
||||
await db.execute('DELETE FROM monthly_worker_status');
|
||||
console.log('🗑️ 기존 집계 데이터 삭제 완료');
|
||||
|
||||
// 3. 날짜별로 작업자별 상태 재계산
|
||||
const [allDates] = await db.execute(`
|
||||
SELECT DISTINCT report_date, worker_id
|
||||
FROM daily_work_reports
|
||||
WHERE report_date BETWEEN ? AND ?
|
||||
ORDER BY report_date, worker_id
|
||||
`, [min_date, max_date]);
|
||||
|
||||
console.log(`🔄 ${allDates.length}개 날짜-작업자 조합 처리 중...`);
|
||||
|
||||
let processedCount = 0;
|
||||
for (const { report_date, worker_id } of allDates) {
|
||||
try {
|
||||
// UpdateMonthlyWorkerStatus 프로시저 호출
|
||||
await db.execute('CALL UpdateMonthlyWorkerStatus(?, ?)', [report_date, worker_id]);
|
||||
processedCount++;
|
||||
|
||||
if (processedCount % 50 === 0) {
|
||||
console.log(`📈 진행률: ${processedCount}/${allDates.length} (${Math.round(processedCount/allDates.length*100)}%)`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`❌ ${report_date} ${worker_id} 처리 오류:`, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 결과 확인
|
||||
const [workerStatusCount] = await db.execute('SELECT COUNT(*) as count FROM monthly_worker_status');
|
||||
const [summaryCount] = await db.execute('SELECT COUNT(*) as count FROM monthly_summary');
|
||||
|
||||
console.log(`✅ 마이그레이션 완료:`);
|
||||
console.log(` - monthly_worker_status: ${workerStatusCount[0].count}건`);
|
||||
console.log(` - monthly_summary: ${summaryCount[0].count}건`);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '기존 데이터 마이그레이션 완료',
|
||||
original_reports: total_reports,
|
||||
processed_combinations: processedCount,
|
||||
worker_status_records: workerStatusCount[0].count,
|
||||
summary_records: summaryCount[0].count,
|
||||
date_range: {
|
||||
from: min_date,
|
||||
to: max_date
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 데이터 마이그레이션 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '데이터 마이그레이션 실패',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// DB 상태 확인
|
||||
router.get('/check-data-status', async (req, res) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
|
||||
const [dailyReports] = await db.execute('SELECT COUNT(*) as count FROM daily_work_reports');
|
||||
const [workerStatus] = await db.execute('SELECT COUNT(*) as count FROM monthly_worker_status');
|
||||
const [monthlySummary] = await db.execute('SELECT COUNT(*) as count FROM monthly_summary');
|
||||
|
||||
// 최근 데이터 확인
|
||||
const [recentData] = await db.execute(`
|
||||
SELECT
|
||||
DATE(report_date) as date,
|
||||
COUNT(*) as reports
|
||||
FROM daily_work_reports
|
||||
WHERE report_date >= DATE_SUB(CURDATE(), INTERVAL 30 DAY)
|
||||
GROUP BY DATE(report_date)
|
||||
ORDER BY report_date DESC
|
||||
LIMIT 5
|
||||
`);
|
||||
|
||||
const [recentSummary] = await db.execute(`
|
||||
SELECT
|
||||
date,
|
||||
total_workers,
|
||||
has_issues,
|
||||
has_errors,
|
||||
has_overtime_warning
|
||||
FROM monthly_summary
|
||||
WHERE date >= DATE_SUB(CURDATE(), INTERVAL 30 DAY)
|
||||
ORDER BY date DESC
|
||||
LIMIT 5
|
||||
`);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
daily_work_reports: dailyReports[0].count,
|
||||
monthly_worker_status: workerStatus[0].count,
|
||||
monthly_summary: monthlySummary[0].count,
|
||||
recent_daily_reports: recentData,
|
||||
recent_summary: recentSummary,
|
||||
migration_needed: workerStatus[0].count === 0 && dailyReports[0].count > 0
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ DB 상태 확인 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'DB 상태 확인 실패',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -86,11 +86,12 @@ const createDailyWorkReportService = async (reportData) => {
|
||||
* @returns {Promise<Array>} 조회된 작업 보고서 배열
|
||||
*/
|
||||
const getDailyWorkReportsService = async (queryParams, userInfo) => {
|
||||
const { date, worker_id, created_by: requested_created_by, view_all } = queryParams;
|
||||
const { date, start_date, end_date, worker_id, created_by: requested_created_by, view_all } = queryParams;
|
||||
const { user_id: current_user_id, role } = userInfo;
|
||||
|
||||
if (!date) {
|
||||
throw new Error('조회를 위해 날짜(date)는 필수입니다.');
|
||||
// 날짜 또는 날짜 범위 중 하나는 필수
|
||||
if (!date && (!start_date || !end_date)) {
|
||||
throw new Error('조회를 위해 날짜(date) 또는 날짜 범위(start_date, end_date)가 필요합니다.');
|
||||
}
|
||||
|
||||
// 관리자 여부 확인
|
||||
@@ -98,7 +99,14 @@ const getDailyWorkReportsService = async (queryParams, userInfo) => {
|
||||
const canViewAll = isAdmin || view_all === 'true';
|
||||
|
||||
// 모델에 전달할 조회 옵션 객체 생성
|
||||
const options = { date };
|
||||
const options = {};
|
||||
|
||||
if (date) {
|
||||
options.date = date;
|
||||
} else {
|
||||
options.start_date = start_date;
|
||||
options.end_date = end_date;
|
||||
}
|
||||
|
||||
if (worker_id) {
|
||||
options.worker_id = parseInt(worker_id);
|
||||
|
||||
177
api.hyungi.net/setup-attendance-db.js
Normal file
177
api.hyungi.net/setup-attendance-db.js
Normal file
@@ -0,0 +1,177 @@
|
||||
const { getDb } = require('./dbPool');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
async function setupAttendanceDB() {
|
||||
try {
|
||||
console.log('🚀 근태 관리 DB 설정 시작...');
|
||||
|
||||
const db = await getDb();
|
||||
|
||||
// 1. 근로 유형 테이블 생성
|
||||
console.log('📋 근로 유형 테이블 생성 중...');
|
||||
await db.execute(`
|
||||
CREATE TABLE IF NOT EXISTS work_attendance_types (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
type_code VARCHAR(20) NOT NULL UNIQUE COMMENT '근로 유형 코드',
|
||||
type_name VARCHAR(50) NOT NULL COMMENT '근로 유형명',
|
||||
description TEXT COMMENT '설명',
|
||||
is_active BOOLEAN DEFAULT TRUE COMMENT '활성 상태',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
) COMMENT='근로 유형 관리 테이블'
|
||||
`);
|
||||
|
||||
// 2. 휴가 유형 테이블 생성
|
||||
console.log('🏖️ 휴가 유형 테이블 생성 중...');
|
||||
await db.execute(`
|
||||
CREATE TABLE IF NOT EXISTS vacation_types (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
type_code VARCHAR(20) NOT NULL UNIQUE COMMENT '휴가 유형 코드',
|
||||
type_name VARCHAR(50) NOT NULL COMMENT '휴가 유형명',
|
||||
hours_deduction DECIMAL(4,2) NOT NULL COMMENT '차감 시간',
|
||||
description TEXT COMMENT '설명',
|
||||
is_active BOOLEAN DEFAULT TRUE COMMENT '활성 상태',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
) COMMENT='휴가 유형 관리 테이블'
|
||||
`);
|
||||
|
||||
// 3. 일일 근태 기록 테이블 생성
|
||||
console.log('📊 일일 근태 기록 테이블 생성 중...');
|
||||
await db.execute(`
|
||||
CREATE TABLE IF NOT EXISTS daily_attendance_records (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
record_date DATE NOT NULL COMMENT '기록 날짜',
|
||||
worker_id INT NOT NULL COMMENT '작업자 ID',
|
||||
total_work_hours DECIMAL(4,2) DEFAULT 0 COMMENT '총 작업 시간',
|
||||
attendance_type_id INT COMMENT '근로 유형 ID',
|
||||
vacation_type_id INT NULL COMMENT '휴가 유형 ID',
|
||||
is_vacation_processed BOOLEAN DEFAULT FALSE COMMENT '휴가 처리 여부',
|
||||
overtime_approved BOOLEAN DEFAULT FALSE COMMENT '초과근무 승인 여부',
|
||||
overtime_approved_by INT NULL COMMENT '초과근무 승인자 ID',
|
||||
overtime_approved_at TIMESTAMP NULL COMMENT '초과근무 승인 시간',
|
||||
status ENUM('incomplete', 'partial', 'complete', 'overtime', 'vacation', 'error') DEFAULT 'incomplete' COMMENT '상태',
|
||||
notes TEXT COMMENT '비고',
|
||||
created_by INT NOT NULL COMMENT '생성자 ID',
|
||||
updated_by INT NULL COMMENT '수정자 ID',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
|
||||
UNIQUE KEY unique_worker_date (worker_id, record_date),
|
||||
INDEX idx_record_date (record_date),
|
||||
INDEX idx_worker_date (worker_id, record_date),
|
||||
INDEX idx_status (status)
|
||||
) COMMENT='일일 근태 기록 테이블'
|
||||
`);
|
||||
|
||||
// 4. 작업자 휴가 잔여 관리 테이블 생성
|
||||
console.log('📅 휴가 잔여 관리 테이블 생성 중...');
|
||||
await db.execute(`
|
||||
CREATE TABLE IF NOT EXISTS worker_vacation_balance (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
worker_id INT NOT NULL COMMENT '작업자 ID',
|
||||
year YEAR NOT NULL COMMENT '연도',
|
||||
total_annual_leave DECIMAL(4,2) DEFAULT 15.0 COMMENT '연간 총 연차 (일)',
|
||||
used_annual_leave DECIMAL(4,2) DEFAULT 0 COMMENT '사용한 연차 (일)',
|
||||
remaining_annual_leave DECIMAL(4,2) GENERATED ALWAYS AS (total_annual_leave - used_annual_leave) STORED COMMENT '잔여 연차 (일)',
|
||||
notes TEXT COMMENT '비고',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
|
||||
UNIQUE KEY unique_worker_year (worker_id, year),
|
||||
INDEX idx_worker_year (worker_id, year)
|
||||
) COMMENT='작업자별 휴가 잔여 관리 테이블'
|
||||
`);
|
||||
|
||||
// 5. 기본 데이터 삽입
|
||||
console.log('📝 기본 데이터 삽입 중...');
|
||||
|
||||
// 근로 유형 기본 데이터
|
||||
await db.execute(`
|
||||
INSERT IGNORE INTO work_attendance_types (type_code, type_name, description) VALUES
|
||||
('REGULAR', '정시근로', '8시간 정규 근무'),
|
||||
('OVERTIME', '연장근로', '8시간 초과 근무'),
|
||||
('PARTIAL', '부분근로', '8시간 미만 근무'),
|
||||
('VACATION', '휴가근로', '휴가와 함께하는 부분 근무')
|
||||
`);
|
||||
|
||||
// 휴가 유형 기본 데이터
|
||||
await db.execute(`
|
||||
INSERT IGNORE INTO vacation_types (type_code, type_name, hours_deduction, description) VALUES
|
||||
('ANNUAL_FULL', '연차', 8.0, '하루 전체 연차'),
|
||||
('ANNUAL_HALF', '반차', 4.0, '반일 연차'),
|
||||
('ANNUAL_QUARTER', '반반차', 2.0, '1/4일 연차'),
|
||||
('SICK_FULL', '병가', 8.0, '하루 전체 병가'),
|
||||
('SICK_HALF', '반일병가', 4.0, '반일 병가'),
|
||||
('PERSONAL_FULL', '개인사유', 8.0, '개인사유로 인한 휴가'),
|
||||
('PERSONAL_HALF', '반일개인사유', 4.0, '반일 개인사유 휴가')
|
||||
`);
|
||||
|
||||
// 6. 휴가 전용 작업 유형 추가
|
||||
console.log('🏖️ 휴가 전용 작업 유형 추가 중...');
|
||||
await db.execute(`
|
||||
INSERT IGNORE INTO work_types (name, description, is_active) VALUES
|
||||
('휴가', '연차, 반차, 병가 등 휴가 처리용', TRUE)
|
||||
`);
|
||||
|
||||
// 7. daily_work_reports 테이블에 근태 기록 연결 컬럼 추가 (이미 있으면 무시)
|
||||
try {
|
||||
await db.execute(`
|
||||
ALTER TABLE daily_work_reports
|
||||
ADD COLUMN attendance_record_id INT NULL COMMENT '근태 기록 ID' AFTER updated_by
|
||||
`);
|
||||
console.log('✅ daily_work_reports 테이블에 attendance_record_id 컬럼 추가됨');
|
||||
} catch (error) {
|
||||
if (error.code !== 'ER_DUP_FIELDNAME') {
|
||||
console.log('⚠️ attendance_record_id 컬럼 추가 실패 (이미 존재할 수 있음):', error.message);
|
||||
} else {
|
||||
console.log('✅ attendance_record_id 컬럼이 이미 존재함');
|
||||
}
|
||||
}
|
||||
|
||||
// 8. 인덱스 추가
|
||||
try {
|
||||
await db.execute(`CREATE INDEX idx_attendance_record ON daily_work_reports(attendance_record_id)`);
|
||||
console.log('✅ attendance_record_id 인덱스 추가됨');
|
||||
} catch (error) {
|
||||
console.log('⚠️ 인덱스 추가 실패 (이미 존재할 수 있음):', error.message);
|
||||
}
|
||||
|
||||
try {
|
||||
await db.execute(`CREATE INDEX idx_daily_work_reports_worker_date ON daily_work_reports(worker_id, report_date)`);
|
||||
console.log('✅ worker_date 인덱스 추가됨');
|
||||
} catch (error) {
|
||||
console.log('⚠️ 인덱스 추가 실패 (이미 존재할 수 있음):', error.message);
|
||||
}
|
||||
|
||||
console.log('🎉 근태 관리 DB 설정 완료!');
|
||||
console.log('');
|
||||
console.log('📋 생성된 테이블:');
|
||||
console.log(' - work_attendance_types (근로 유형)');
|
||||
console.log(' - vacation_types (휴가 유형)');
|
||||
console.log(' - daily_attendance_records (일일 근태 기록)');
|
||||
console.log(' - worker_vacation_balance (휴가 잔여 관리)');
|
||||
console.log('');
|
||||
console.log('✅ 기본 데이터도 모두 삽입되었습니다.');
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ DB 설정 중 오류 발생:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 직접 실행
|
||||
if (require.main === module) {
|
||||
setupAttendanceDB()
|
||||
.then(() => {
|
||||
console.log('✅ 설정 완료');
|
||||
process.exit(0);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('❌ 설정 실패:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { setupAttendanceDB };
|
||||
Reference in New Issue
Block a user