- codes.html, code-management.js 삭제 (tasks.html에서 동일 기능 제공) - 사이드바에서 코드 관리 링크 제거 - daily-work-report, tbm, workplace-management JS 모듈 분리 - common/security.js 추가 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
471 lines
11 KiB
JavaScript
471 lines
11 KiB
JavaScript
/**
|
||
* 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?.();
|
||
}
|
||
};
|