Files
TK-FB-Project/deploy/tkfb-package/web-ui/js/work-analysis/state-manager.js
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

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는 브라우저 환경에서 제거됨