feat: 다수 기능 개선 - 순찰, 출근, 작업분석, 모바일 UI 등

- 순찰/점검 기능 개선 (zone-detail 페이지 추가)
- 출근/근태 시스템 개선 (연차 조회, 근무현황)
- 작업분석 대분류 그룹화 및 마이그레이션 스크립트
- 모바일 네비게이션 UI 추가
- NAS 배포 도구 및 문서 추가

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Hyungi Ahn
2026-02-09 14:41:01 +09:00
parent 1548253f56
commit 2b1c7bfb88
633 changed files with 361224 additions and 1090 deletions

View File

@@ -8,9 +8,19 @@
*/
const AttendanceModel = require('../models/attendanceModel');
const vacationBalanceModel = require('../models/vacationBalanceModel');
const { ValidationError, NotFoundError, DatabaseError } = require('../utils/errors');
const logger = require('../utils/logger');
/**
* 휴가 사용 유형 ID를 차감 일수로 변환
* vacation_type_id: 1=연차(1일), 2=반차(0.5일), 3=반반차(0.25일)
*/
const getVacationDays = (vacationTypeId) => {
const daysMap = { 1: 1, 2: 0.5, 3: 0.25 };
return daysMap[vacationTypeId] || 0;
};
/**
* 일일 근태 현황 조회
*/
@@ -63,8 +73,32 @@ const getDailyAttendanceRecordsService = async (date, workerId = null) => {
}
};
/**
* 기간별 근태 기록 조회 (월별 조회용)
*/
const getAttendanceRecordsByRangeService = async (startDate, endDate, workerId = null) => {
if (!startDate || !endDate) {
throw new ValidationError('시작 날짜와 종료 날짜가 필요합니다', {
required: ['start_date', 'end_date'],
received: { startDate, endDate }
});
}
logger.info('기간별 근태 기록 조회 요청', { startDate, endDate, workerId });
try {
const records = await AttendanceModel.getDailyRecords(startDate, endDate, workerId);
logger.info('기간별 근태 기록 조회 성공', { startDate, endDate, count: records.length });
return records;
} catch (error) {
logger.error('기간별 근태 기록 조회 실패', { startDate, endDate, error: error.message });
throw new DatabaseError('근태 기록 조회 중 데이터베이스 오류가 발생했습니다');
}
};
/**
* 근태 기록 생성/업데이트
* - 휴가 기록 시 vacation_balance_details의 used_days 자동 연동
*/
const upsertAttendanceRecordService = async (recordData) => {
const {
@@ -88,9 +122,15 @@ const upsertAttendanceRecordService = async (recordData) => {
});
}
logger.info('근태 기록 저장 요청', { record_date, worker_id });
logger.info('근태 기록 저장 요청', { record_date, worker_id, vacation_type_id });
try {
// 1. 기존 기록 조회 (휴가 연동을 위해)
const existingRecords = await AttendanceModel.getDailyAttendanceRecords(record_date, worker_id);
const existingRecord = existingRecords.find(r => r.worker_id === worker_id);
const previousVacationTypeId = existingRecord?.vacation_type_id || null;
// 2. 근태 기록 저장
const result = await AttendanceModel.upsertAttendanceRecord({
record_date,
worker_id,
@@ -101,6 +141,26 @@ const upsertAttendanceRecordService = async (recordData) => {
created_by
});
// 3. 휴가 잔액 연동 (vacation_balance_details.used_days 업데이트)
const year = new Date(record_date).getFullYear();
const previousDays = getVacationDays(previousVacationTypeId);
const newDays = getVacationDays(vacation_type_id);
// 이전 휴가가 있었고 변경된 경우 → 복구 후 차감
if (previousDays !== newDays) {
// 이전 휴가 복구
if (previousDays > 0) {
await vacationBalanceModel.restoreByPriority(worker_id, year, previousDays);
logger.info('휴가 잔액 복구', { worker_id, year, restored: previousDays });
}
// 새 휴가 차감
if (newDays > 0) {
await vacationBalanceModel.deductByPriority(worker_id, year, newDays);
logger.info('휴가 잔액 차감', { worker_id, year, deducted: newDays });
}
}
logger.info('근태 기록 저장 성공', { record_date, worker_id });
return result;
} catch (error) {
@@ -321,6 +381,7 @@ const saveCheckinsService = async (date, checkins) => {
module.exports = {
getDailyAttendanceStatusService,
getDailyAttendanceRecordsService,
getAttendanceRecordsByRangeService,
upsertAttendanceRecordService,
processVacationService,
approveOvertimeService,