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

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 14:41:01 +09:00

471 lines
11 KiB
JavaScript
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Daily Work Report - Utilities
* 작업보고서 관련 유틸리티 함수들
*/
class DailyWorkReportUtils {
constructor() {
console.log('[Utils] DailyWorkReportUtils 초기화');
}
/**
* 한국 시간 기준 오늘 날짜 (YYYY-MM-DD)
*/
getKoreaToday() {
const today = new Date();
const year = today.getFullYear();
const month = String(today.getMonth() + 1).padStart(2, '0');
const day = String(today.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
}
/**
* 날짜를 API 형식(YYYY-MM-DD)으로 변환
*/
formatDateForApi(date) {
if (!date) return null;
let dateObj;
if (date instanceof Date) {
dateObj = date;
} else if (typeof date === 'string') {
dateObj = new Date(date);
} else {
return null;
}
const year = dateObj.getFullYear();
const month = String(dateObj.getMonth() + 1).padStart(2, '0');
const day = String(dateObj.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
}
/**
* 날짜 포맷팅 (표시용)
*/
formatDate(date) {
if (!date) return '-';
let dateObj;
if (date instanceof Date) {
dateObj = date;
} else if (typeof date === 'string') {
dateObj = new Date(date);
} else {
return '-';
}
const year = dateObj.getFullYear();
const month = String(dateObj.getMonth() + 1).padStart(2, '0');
const day = String(dateObj.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
}
/**
* 시간 포맷팅 (HH:mm)
*/
formatTime(time) {
if (!time) return '-';
if (typeof time === 'string' && time.includes(':')) {
return time.substring(0, 5);
}
return time;
}
/**
* 상태 라벨 반환
*/
getStatusLabel(status) {
const labels = {
'pending': '접수',
'in_progress': '처리중',
'resolved': '해결',
'completed': '완료',
'closed': '종료'
};
return labels[status] || status || '-';
}
/**
* 숫자 포맷팅 (천 단위 콤마)
*/
formatNumber(num) {
if (num === null || num === undefined) return '0';
return num.toLocaleString('ko-KR');
}
/**
* 소수점 자리수 포맷팅
*/
formatDecimal(num, decimals = 1) {
if (num === null || num === undefined) return '0';
return Number(num).toFixed(decimals);
}
/**
* 요일 반환
*/
getDayOfWeek(date) {
const days = ['일', '월', '화', '수', '목', '금', '토'];
const dateObj = date instanceof Date ? date : new Date(date);
return days[dateObj.getDay()];
}
/**
* 오늘인지 확인
*/
isToday(date) {
const today = this.getKoreaToday();
const targetDate = this.formatDateForApi(date);
return today === targetDate;
}
/**
* 두 날짜 사이 일수 계산
*/
daysBetween(date1, date2) {
const d1 = new Date(date1);
const d2 = new Date(date2);
const diffTime = Math.abs(d2 - d1);
return Math.ceil(diffTime / (1000 * 60 * 60 * 24));
}
/**
* 디바운스 함수
*/
debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
/**
* 쓰로틀 함수
*/
throttle(func, limit) {
let inThrottle;
return function(...args) {
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
};
}
/**
* HTML 이스케이프
*/
escapeHtml(text) {
if (!text) return '';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
/**
* 객체 깊은 복사
*/
deepClone(obj) {
return JSON.parse(JSON.stringify(obj));
}
/**
* 빈 값 확인
*/
isEmpty(value) {
if (value === null || value === undefined) return true;
if (typeof value === 'string') return value.trim() === '';
if (Array.isArray(value)) return value.length === 0;
if (typeof value === 'object') return Object.keys(value).length === 0;
return false;
}
/**
* 숫자 유효성 검사
*/
isValidNumber(value) {
return !isNaN(value) && isFinite(value);
}
/**
* 시간 유효성 검사 (0-24)
*/
isValidHours(hours) {
const num = parseFloat(hours);
return this.isValidNumber(num) && num >= 0 && num <= 24;
}
/**
* 쿼리 스트링 파싱
*/
parseQueryString(queryString) {
const params = new URLSearchParams(queryString);
const result = {};
for (const [key, value] of params) {
result[key] = value;
}
return result;
}
/**
* 쿼리 스트링 생성
*/
buildQueryString(params) {
return new URLSearchParams(params).toString();
}
/**
* 로컬 스토리지 안전하게 가져오기
*/
getLocalStorage(key, defaultValue = null) {
try {
const item = localStorage.getItem(key);
return item ? JSON.parse(item) : defaultValue;
} catch (error) {
console.error('[Utils] localStorage 읽기 오류:', error);
return defaultValue;
}
}
/**
* 로컬 스토리지 안전하게 저장하기
*/
setLocalStorage(key, value) {
try {
localStorage.setItem(key, JSON.stringify(value));
return true;
} catch (error) {
console.error('[Utils] localStorage 저장 오류:', error);
return false;
}
}
/**
* 배열 그룹화
*/
groupBy(array, key) {
return array.reduce((result, item) => {
const groupKey = typeof key === 'function' ? key(item) : item[key];
if (!result[groupKey]) {
result[groupKey] = [];
}
result[groupKey].push(item);
return result;
}, {});
}
/**
* 배열 정렬 (다중 키)
*/
sortBy(array, ...keys) {
return [...array].sort((a, b) => {
for (const key of keys) {
const direction = key.startsWith('-') ? -1 : 1;
const actualKey = key.replace(/^-/, '');
const aVal = a[actualKey];
const bVal = b[actualKey];
if (aVal < bVal) return -1 * direction;
if (aVal > bVal) return 1 * direction;
}
return 0;
});
}
/**
* UUID 생성
*/
generateUUID() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
const r = Math.random() * 16 | 0;
const v = c === 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
}
// 전역 인스턴스 생성
window.DailyWorkReportUtils = new DailyWorkReportUtils();
// 하위 호환성: 기존 함수들
window.getKoreaToday = () => window.DailyWorkReportUtils.getKoreaToday();
window.formatDateForApi = (date) => window.DailyWorkReportUtils.formatDateForApi(date);
window.formatDate = (date) => window.DailyWorkReportUtils.formatDate(date);
window.getStatusLabel = (status) => window.DailyWorkReportUtils.getStatusLabel(status);
// 메시지 표시 함수들
window.showMessage = function(message, type = 'info') {
const container = document.getElementById('message-container');
if (!container) {
console.log(`[Message] ${type}: ${message}`);
return;
}
container.innerHTML = `<div class="message ${type}">${message}</div>`;
if (type === 'success') {
setTimeout(() => window.hideMessage(), 5000);
}
};
window.hideMessage = function() {
const container = document.getElementById('message-container');
if (container) {
container.innerHTML = '';
}
};
// 저장 결과 모달
window.showSaveResultModal = function(type, title, message, details = null) {
const modal = document.getElementById('saveResultModal');
const titleElement = document.getElementById('resultModalTitle');
const contentElement = document.getElementById('resultModalContent');
if (!modal || !contentElement) {
alert(`${title}\n\n${message}`);
return;
}
const icons = {
success: '✅',
error: '❌',
warning: '⚠️',
info: ''
};
let content = `
<div class="result-icon ${type}">${icons[type] || icons.info}</div>
<h3 class="result-title ${type}">${title}</h3>
<p class="result-message">${message}</p>
`;
if (details) {
if (Array.isArray(details) && details.length > 0) {
content += `
<div class="result-details">
<h4>상세 정보:</h4>
<ul>${details.map(d => `<li>${d}</li>`).join('')}</ul>
</div>
`;
} else if (typeof details === 'string') {
content += `<div class="result-details"><p>${details}</p></div>`;
}
}
if (titleElement) titleElement.textContent = '저장 결과';
contentElement.innerHTML = content;
modal.style.display = 'flex';
// ESC 키로 닫기
const escHandler = (e) => {
if (e.key === 'Escape') {
window.closeSaveResultModal();
document.removeEventListener('keydown', escHandler);
}
};
document.addEventListener('keydown', escHandler);
// 배경 클릭으로 닫기
modal.onclick = (e) => {
if (e.target === modal) {
window.closeSaveResultModal();
}
};
};
window.closeSaveResultModal = function() {
const modal = document.getElementById('saveResultModal');
if (modal) {
modal.style.display = 'none';
}
};
// 단계 이동 함수
window.goToStep = function(stepNumber) {
const state = window.DailyWorkReportState;
for (let i = 1; i <= 3; i++) {
const step = document.getElementById(`step${i}`);
if (step) {
step.classList.remove('active', 'completed');
if (i < stepNumber) {
step.classList.add('completed');
const stepNum = step.querySelector('.step-number');
if (stepNum) stepNum.classList.add('completed');
} else if (i === stepNumber) {
step.classList.add('active');
}
}
}
window.updateProgressSteps(stepNumber);
state.currentStep = stepNumber;
};
window.updateProgressSteps = function(currentStepNumber) {
for (let i = 1; i <= 3; i++) {
const progressStep = document.getElementById(`progressStep${i}`);
if (progressStep) {
progressStep.classList.remove('active', 'completed');
if (i < currentStepNumber) {
progressStep.classList.add('completed');
} else if (i === currentStepNumber) {
progressStep.classList.add('active');
}
}
}
};
// 토스트 메시지 (간단 버전)
window.showToast = function(message, type = 'info', duration = 3000) {
console.log(`[Toast] ${type}: ${message}`);
// 기존 토스트 제거
const existingToast = document.querySelector('.toast-message');
if (existingToast) {
existingToast.remove();
}
// 새 토스트 생성
const toast = document.createElement('div');
toast.className = `toast-message toast-${type}`;
toast.textContent = message;
toast.style.cssText = `
position: fixed;
bottom: 20px;
right: 20px;
padding: 12px 20px;
border-radius: 8px;
color: white;
font-size: 14px;
z-index: 10000;
animation: slideIn 0.3s ease;
background-color: ${type === 'success' ? '#10b981' : type === 'error' ? '#ef4444' : type === 'warning' ? '#f59e0b' : '#3b82f6'};
`;
document.body.appendChild(toast);
setTimeout(() => {
toast.style.animation = 'slideOut 0.3s ease';
setTimeout(() => toast.remove(), 300);
}, duration);
};
// 확인 다이얼로그
window.showConfirmDialog = function(message, onConfirm, onCancel) {
if (confirm(message)) {
onConfirm?.();
} else {
onCancel?.();
}
};