- 순찰/점검 기능 개선 (zone-detail 페이지 추가) - 출근/근태 시스템 개선 (연차 조회, 근무현황) - 작업분석 대분류 그룹화 및 마이그레이션 스크립트 - 모바일 네비게이션 UI 추가 - NAS 배포 도구 및 문서 추가 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
383 lines
10 KiB
JavaScript
383 lines
10 KiB
JavaScript
/**
|
|
* Work Analysis State Manager Module
|
|
* 작업 분석 페이지의 상태 관리를 담당하는 모듈
|
|
*/
|
|
|
|
class WorkAnalysisStateManager {
|
|
constructor() {
|
|
this.state = {
|
|
// 분석 설정
|
|
analysisMode: 'period', // 'period' | 'project'
|
|
confirmedPeriod: {
|
|
start: null,
|
|
end: null,
|
|
confirmed: false
|
|
},
|
|
|
|
// UI 상태
|
|
currentTab: 'work-status',
|
|
isAnalysisEnabled: false,
|
|
isLoading: false,
|
|
|
|
// 데이터 캐시
|
|
cache: {
|
|
basicStats: null,
|
|
chartData: null,
|
|
projectDistribution: null,
|
|
errorAnalysis: null
|
|
},
|
|
|
|
// 에러 상태
|
|
lastError: null
|
|
};
|
|
|
|
this.listeners = new Map();
|
|
this.init();
|
|
}
|
|
|
|
/**
|
|
* 초기화
|
|
*/
|
|
init() {
|
|
console.log('🔧 상태 관리자 초기화');
|
|
|
|
// 기본 날짜 설정 (현재 월)
|
|
const now = new Date();
|
|
const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
|
|
const endOfMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0);
|
|
|
|
this.updateState({
|
|
confirmedPeriod: {
|
|
start: this.formatDate(startOfMonth),
|
|
end: this.formatDate(endOfMonth),
|
|
confirmed: false
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 상태 업데이트
|
|
* @param {Object} updates - 업데이트할 상태
|
|
*/
|
|
updateState(updates) {
|
|
const prevState = { ...this.state };
|
|
this.state = { ...this.state, ...updates };
|
|
|
|
console.log('🔄 상태 업데이트:', updates);
|
|
|
|
// 리스너들에게 상태 변경 알림
|
|
this.notifyListeners(prevState, this.state);
|
|
}
|
|
|
|
/**
|
|
* 상태 리스너 등록
|
|
* @param {string} key - 리스너 키
|
|
* @param {Function} callback - 콜백 함수
|
|
*/
|
|
subscribe(key, callback) {
|
|
this.listeners.set(key, callback);
|
|
}
|
|
|
|
/**
|
|
* 상태 리스너 제거
|
|
* @param {string} key - 리스너 키
|
|
*/
|
|
unsubscribe(key) {
|
|
this.listeners.delete(key);
|
|
}
|
|
|
|
/**
|
|
* 리스너들에게 알림
|
|
*/
|
|
notifyListeners(prevState, newState) {
|
|
this.listeners.forEach((callback, key) => {
|
|
try {
|
|
callback(newState, prevState);
|
|
} catch (error) {
|
|
console.error(`❌ 리스너 ${key} 오류:`, error);
|
|
}
|
|
});
|
|
}
|
|
|
|
// ========== 분석 설정 관리 ==========
|
|
|
|
/**
|
|
* 분석 모드 변경
|
|
* @param {string} mode - 분석 모드 ('period' | 'project')
|
|
*/
|
|
setAnalysisMode(mode) {
|
|
if (mode !== 'period' && mode !== 'project') {
|
|
throw new Error('유효하지 않은 분석 모드입니다.');
|
|
}
|
|
|
|
this.updateState({
|
|
analysisMode: mode,
|
|
currentTab: 'work-status' // 모드 변경 시 첫 번째 탭으로 리셋
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 기간 확정
|
|
* @param {string} startDate - 시작일
|
|
* @param {string} endDate - 종료일
|
|
*/
|
|
confirmPeriod(startDate, endDate) {
|
|
// 날짜 유효성 검사
|
|
if (!startDate || !endDate) {
|
|
throw new Error('시작일과 종료일을 모두 입력해주세요.');
|
|
}
|
|
|
|
const start = new Date(startDate);
|
|
const end = new Date(endDate);
|
|
|
|
if (start > end) {
|
|
throw new Error('시작일이 종료일보다 늦을 수 없습니다.');
|
|
}
|
|
|
|
// 최대 1년 제한
|
|
const maxDays = 365;
|
|
const daysDiff = Math.ceil((end - start) / (1000 * 60 * 60 * 24));
|
|
if (daysDiff > maxDays) {
|
|
throw new Error(`분석 기간은 최대 ${maxDays}일까지 가능합니다.`);
|
|
}
|
|
|
|
this.updateState({
|
|
confirmedPeriod: {
|
|
start: startDate,
|
|
end: endDate,
|
|
confirmed: true
|
|
},
|
|
isAnalysisEnabled: true,
|
|
// 기간 변경 시 캐시 초기화
|
|
cache: {
|
|
basicStats: null,
|
|
chartData: null,
|
|
projectDistribution: null,
|
|
errorAnalysis: null
|
|
}
|
|
});
|
|
|
|
console.log('✅ 기간 확정:', startDate, '~', endDate);
|
|
}
|
|
|
|
/**
|
|
* 현재 탭 변경
|
|
* @param {string} tabId - 탭 ID
|
|
*/
|
|
setCurrentTab(tabId) {
|
|
const validTabs = ['work-status', 'project-distribution', 'worker-performance', 'error-analysis'];
|
|
|
|
if (!validTabs.includes(tabId)) {
|
|
console.warn('유효하지 않은 탭 ID:', tabId);
|
|
return;
|
|
}
|
|
|
|
this.updateState({ currentTab: tabId });
|
|
|
|
// DOM 업데이트 직접 수행
|
|
this.updateTabDOM(tabId);
|
|
|
|
console.log('🔄 탭 전환:', tabId);
|
|
}
|
|
|
|
/**
|
|
* 탭 DOM 업데이트
|
|
* @param {string} tabId - 탭 ID
|
|
*/
|
|
updateTabDOM(tabId) {
|
|
// 탭 버튼 업데이트
|
|
document.querySelectorAll('.tab-button').forEach(button => {
|
|
button.classList.remove('active');
|
|
if (button.dataset.tab === tabId) {
|
|
button.classList.add('active');
|
|
}
|
|
});
|
|
|
|
// 탭 컨텐츠 업데이트
|
|
document.querySelectorAll('.tab-content').forEach(content => {
|
|
content.classList.remove('active');
|
|
if (content.id === `${tabId}-tab`) {
|
|
content.classList.add('active');
|
|
}
|
|
});
|
|
}
|
|
|
|
// ========== 로딩 상태 관리 ==========
|
|
|
|
/**
|
|
* 로딩 시작
|
|
* @param {string} message - 로딩 메시지
|
|
*/
|
|
startLoading(message = '분석 중입니다...') {
|
|
this.updateState({
|
|
isLoading: true,
|
|
loadingMessage: message
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 로딩 종료
|
|
*/
|
|
stopLoading() {
|
|
this.updateState({
|
|
isLoading: false,
|
|
loadingMessage: null
|
|
});
|
|
}
|
|
|
|
// ========== 데이터 캐시 관리 ==========
|
|
|
|
/**
|
|
* 캐시 데이터 저장
|
|
* @param {string} key - 캐시 키
|
|
* @param {*} data - 저장할 데이터
|
|
*/
|
|
setCache(key, data) {
|
|
this.updateState({
|
|
cache: {
|
|
...this.state.cache,
|
|
[key]: {
|
|
data,
|
|
timestamp: Date.now()
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 캐시 데이터 조회
|
|
* @param {string} key - 캐시 키
|
|
* @param {number} maxAge - 최대 유효 시간 (밀리초)
|
|
* @returns {*} 캐시된 데이터 또는 null
|
|
*/
|
|
getCache(key, maxAge = 5 * 60 * 1000) { // 기본 5분
|
|
const cached = this.state.cache[key];
|
|
|
|
if (!cached) {
|
|
return null;
|
|
}
|
|
|
|
const age = Date.now() - cached.timestamp;
|
|
if (age > maxAge) {
|
|
console.log('🗑️ 캐시 만료:', key);
|
|
return null;
|
|
}
|
|
|
|
console.log('📦 캐시 히트:', key);
|
|
return cached.data;
|
|
}
|
|
|
|
/**
|
|
* 캐시 초기화
|
|
* @param {string} key - 특정 키만 초기화 (선택사항)
|
|
*/
|
|
clearCache(key = null) {
|
|
if (key) {
|
|
this.updateState({
|
|
cache: {
|
|
...this.state.cache,
|
|
[key]: null
|
|
}
|
|
});
|
|
} else {
|
|
this.updateState({
|
|
cache: {
|
|
basicStats: null,
|
|
chartData: null,
|
|
projectDistribution: null,
|
|
errorAnalysis: null
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// ========== 에러 관리 ==========
|
|
|
|
/**
|
|
* 에러 설정
|
|
* @param {Error|string} error - 에러 객체 또는 메시지
|
|
*/
|
|
setError(error) {
|
|
const errorInfo = {
|
|
message: error instanceof Error ? error.message : error,
|
|
timestamp: Date.now(),
|
|
stack: error instanceof Error ? error.stack : null
|
|
};
|
|
|
|
this.updateState({
|
|
lastError: errorInfo,
|
|
isLoading: false
|
|
});
|
|
|
|
console.error('❌ 에러 발생:', errorInfo);
|
|
}
|
|
|
|
/**
|
|
* 에러 초기화
|
|
*/
|
|
clearError() {
|
|
this.updateState({ lastError: null });
|
|
}
|
|
|
|
// ========== 유틸리티 ==========
|
|
|
|
/**
|
|
* 날짜 포맷팅
|
|
* @param {Date} date - 날짜 객체
|
|
* @returns {string} YYYY-MM-DD 형식
|
|
*/
|
|
formatDate(date) {
|
|
return date.toISOString().split('T')[0];
|
|
}
|
|
|
|
/**
|
|
* 현재 상태 조회
|
|
* @returns {Object} 현재 상태
|
|
*/
|
|
getState() {
|
|
return { ...this.state };
|
|
}
|
|
|
|
/**
|
|
* 분석 가능 여부 확인
|
|
* @returns {boolean} 분석 가능 여부
|
|
*/
|
|
canAnalyze() {
|
|
return this.state.confirmedPeriod.confirmed &&
|
|
this.state.confirmedPeriod.start &&
|
|
this.state.confirmedPeriod.end &&
|
|
!this.state.isLoading;
|
|
}
|
|
|
|
/**
|
|
* 상태 디버그 정보 출력
|
|
*/
|
|
debug() {
|
|
console.log('🔍 현재 상태:', this.state);
|
|
console.log('👂 등록된 리스너:', Array.from(this.listeners.keys()));
|
|
}
|
|
}
|
|
|
|
// 전역 인스턴스 생성
|
|
window.WorkAnalysisState = new WorkAnalysisStateManager();
|
|
|
|
// 하위 호환성을 위한 전역 변수들
|
|
Object.defineProperty(window, 'currentAnalysisMode', {
|
|
get: () => window.WorkAnalysisState.state.analysisMode,
|
|
set: (value) => window.WorkAnalysisState.setAnalysisMode(value)
|
|
});
|
|
|
|
Object.defineProperty(window, 'confirmedStartDate', {
|
|
get: () => window.WorkAnalysisState.state.confirmedPeriod.start
|
|
});
|
|
|
|
Object.defineProperty(window, 'confirmedEndDate', {
|
|
get: () => window.WorkAnalysisState.state.confirmedPeriod.end
|
|
});
|
|
|
|
Object.defineProperty(window, 'isAnalysisEnabled', {
|
|
get: () => window.WorkAnalysisState.state.isAnalysisEnabled
|
|
});
|
|
|
|
// Export는 브라우저 환경에서 제거됨
|