- 공통 유틸리티 추출 (common/utils.js, common/base-state.js) - TBM 모바일 인라인 JS/CSS 외부 파일로 분리 (tbm-mobile.js, tbm-mobile.css) - 미사용 코드 삭제 (index.js, work-report-*.js 등 5개 파일) - TBM/작업보고 state.js, utils.js를 공통 모듈 기반으로 전환 - 작업보고서 SSO 인증 호환 수정 (token/user 함수) - tbmModel.js: incomplete-reports 쿼리에서 users→sso_users 조인 수정, leader_name 조인 추가 - docker-compose.yml: system1-web 볼륨 마운트 추가 - 모바일 인계(handover) 기능 추가 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
300 lines
7.8 KiB
JavaScript
300 lines
7.8 KiB
JavaScript
/**
|
||
* Daily Work Report - Utilities
|
||
* 작업보고서 관련 유틸리티 함수들 (공통 함수는 CommonUtils에 위임)
|
||
*/
|
||
|
||
class DailyWorkReportUtils {
|
||
constructor() {
|
||
this._common = window.CommonUtils;
|
||
console.log('[Utils] DailyWorkReportUtils 초기화');
|
||
}
|
||
|
||
// --- CommonUtils 위임 ---
|
||
getKoreaToday() { return this._common.getTodayKST(); }
|
||
formatDateForApi(date) { return this._common.formatDate(date); }
|
||
formatDate(date) { return this._common.formatDate(date) || '-'; }
|
||
getDayOfWeek(date) { return this._common.getDayOfWeek(date); }
|
||
isToday(date) { return this._common.isToday(date); }
|
||
generateUUID() { return this._common.generateUUID(); }
|
||
escapeHtml(text) { return this._common.escapeHtml(text); }
|
||
debounce(func, wait) { return this._common.debounce(func, wait); }
|
||
throttle(func, limit) { return this._common.throttle(func, limit); }
|
||
deepClone(obj) { return this._common.deepClone(obj); }
|
||
isEmpty(value) { return this._common.isEmpty(value); }
|
||
groupBy(array, key) { return this._common.groupBy(array, key); }
|
||
|
||
// --- 작업보고 전용 ---
|
||
|
||
/**
|
||
* 시간 포맷팅 (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);
|
||
}
|
||
|
||
/**
|
||
* 두 날짜 사이 일수 계산
|
||
*/
|
||
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));
|
||
}
|
||
|
||
/**
|
||
* 숫자 유효성 검사
|
||
*/
|
||
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;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 배열 정렬 (다중 키)
|
||
*/
|
||
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;
|
||
});
|
||
}
|
||
}
|
||
|
||
// 전역 인스턴스 생성
|
||
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');
|
||
}
|
||
}
|
||
}
|
||
};
|
||
|
||
// showToast → api-base.js 전역 사용
|
||
|
||
// 확인 다이얼로그
|
||
window.showConfirmDialog = function(message, onConfirm, onCancel) {
|
||
if (confirm(message)) {
|
||
onConfirm?.();
|
||
} else {
|
||
onCancel?.();
|
||
}
|
||
};
|