feat: 3-System 분리 프로젝트 초기 코드 작성
TK-FB(공장관리+신고)와 M-Project(부적합관리)를 3개 독립 시스템으로 분리하기 위한 전체 코드 구조 작성. - SSO 인증 서비스 (bcrypt + pbkdf2 이중 해시 지원) - System 1: 공장관리 (TK-FB 기반, 신고 코드 제거) - System 2: 신고 (TK-FB에서 workIssue 코드 추출) - System 3: 부적합관리 (M-Project 기반) - Gateway 포털 (path-based 라우팅) - 통합 docker-compose.yml 및 배포 스크립트 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
470
system1-factory/web/js/daily-work-report/utils.js
Normal file
470
system1-factory/web/js/daily-work-report/utils.js
Normal file
@@ -0,0 +1,470 @@
|
||||
/**
|
||||
* 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?.();
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user