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>
613 lines
20 KiB
JavaScript
613 lines
20 KiB
JavaScript
/**
|
|
* Work Analysis Main Controller Module
|
|
* 작업 분석 페이지의 메인 컨트롤러 - 모든 모듈을 조율하고 사용자 상호작용을 처리
|
|
*/
|
|
|
|
class WorkAnalysisMainController {
|
|
constructor() {
|
|
this.api = window.WorkAnalysisAPI;
|
|
this.state = window.WorkAnalysisState;
|
|
this.dataProcessor = window.WorkAnalysisDataProcessor;
|
|
this.tableRenderer = window.WorkAnalysisTableRenderer;
|
|
this.chartRenderer = window.WorkAnalysisChartRenderer;
|
|
|
|
this.init();
|
|
}
|
|
|
|
/**
|
|
* 초기화
|
|
*/
|
|
init() {
|
|
console.log('🚀 작업 분석 메인 컨트롤러 초기화');
|
|
|
|
this.setupEventListeners();
|
|
this.setupStateListeners();
|
|
this.initializeUI();
|
|
|
|
console.log('✅ 작업 분석 메인 컨트롤러 초기화 완료');
|
|
}
|
|
|
|
/**
|
|
* 이벤트 리스너 설정
|
|
*/
|
|
setupEventListeners() {
|
|
// 기간 확정 버튼
|
|
const confirmButton = document.getElementById('confirmPeriodBtn');
|
|
if (confirmButton) {
|
|
confirmButton.addEventListener('click', () => this.handlePeriodConfirm());
|
|
}
|
|
|
|
// 분석 모드 탭
|
|
document.querySelectorAll('[data-mode]').forEach(button => {
|
|
button.addEventListener('click', (e) => {
|
|
const mode = e.target.dataset.mode;
|
|
this.handleModeChange(mode);
|
|
});
|
|
});
|
|
|
|
// 분석 탭 네비게이션
|
|
document.querySelectorAll('[data-tab]').forEach(button => {
|
|
button.addEventListener('click', (e) => {
|
|
const tabId = e.target.dataset.tab;
|
|
this.handleTabChange(tabId);
|
|
});
|
|
});
|
|
|
|
// 개별 분석 실행 버튼들
|
|
this.setupAnalysisButtons();
|
|
|
|
// 날짜 입력 필드
|
|
const startDateInput = document.getElementById('startDate');
|
|
const endDateInput = document.getElementById('endDate');
|
|
|
|
if (startDateInput && endDateInput) {
|
|
[startDateInput, endDateInput].forEach(input => {
|
|
input.addEventListener('change', () => this.handleDateChange());
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 개별 분석 버튼 설정
|
|
*/
|
|
setupAnalysisButtons() {
|
|
const buttons = [
|
|
{ selector: 'button[onclick*="analyzeWorkStatus"]', handler: () => this.analyzeWorkStatus() },
|
|
{ selector: 'button[onclick*="analyzeProjectDistribution"]', handler: () => this.analyzeProjectDistribution() },
|
|
{ selector: 'button[onclick*="analyzeWorkerPerformance"]', handler: () => this.analyzeWorkerPerformance() },
|
|
{ selector: 'button[onclick*="analyzeErrorAnalysis"]', handler: () => this.analyzeErrorAnalysis() }
|
|
];
|
|
|
|
buttons.forEach(({ selector, handler }) => {
|
|
const button = document.querySelector(selector);
|
|
if (button) {
|
|
// 기존 onclick 제거하고 새 이벤트 리스너 추가
|
|
button.removeAttribute('onclick');
|
|
button.addEventListener('click', handler);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 상태 리스너 설정
|
|
*/
|
|
setupStateListeners() {
|
|
// 기간 확정 상태 변경 시 UI 업데이트
|
|
this.state.subscribe('periodConfirmed', (newState, prevState) => {
|
|
this.updateAnalysisButtons(newState.isAnalysisEnabled);
|
|
|
|
if (newState.confirmedPeriod.confirmed && !prevState.confirmedPeriod.confirmed) {
|
|
this.showAnalysisTabs();
|
|
}
|
|
});
|
|
|
|
// 로딩 상태 변경 시 UI 업데이트
|
|
this.state.subscribe('loadingState', (newState) => {
|
|
if (newState.isLoading) {
|
|
this.showLoading(newState.loadingMessage);
|
|
} else {
|
|
this.hideLoading();
|
|
}
|
|
});
|
|
|
|
// 탭 변경 시 UI 업데이트
|
|
this.state.subscribe('tabChange', (newState) => {
|
|
this.updateActiveTab(newState.currentTab);
|
|
});
|
|
|
|
// 에러 발생 시 처리
|
|
this.state.subscribe('errorOccurred', (newState) => {
|
|
if (newState.lastError) {
|
|
this.handleError(newState.lastError);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* UI 초기화
|
|
*/
|
|
initializeUI() {
|
|
// 기본 날짜 설정
|
|
const currentState = this.state.getState();
|
|
const startDateInput = document.getElementById('startDate');
|
|
const endDateInput = document.getElementById('endDate');
|
|
|
|
if (startDateInput && currentState.confirmedPeriod.start) {
|
|
startDateInput.value = currentState.confirmedPeriod.start;
|
|
}
|
|
|
|
if (endDateInput && currentState.confirmedPeriod.end) {
|
|
endDateInput.value = currentState.confirmedPeriod.end;
|
|
}
|
|
|
|
// 분석 버튼 초기 상태 설정
|
|
this.updateAnalysisButtons(false);
|
|
|
|
// 분석 탭 숨김
|
|
this.hideAnalysisTabs();
|
|
}
|
|
|
|
// ========== 이벤트 핸들러 ==========
|
|
|
|
/**
|
|
* 기간 확정 처리
|
|
*/
|
|
async handlePeriodConfirm() {
|
|
try {
|
|
const startDate = document.getElementById('startDate').value;
|
|
const endDate = document.getElementById('endDate').value;
|
|
|
|
console.log('🔄 기간 확정 처리 시작:', startDate, '~', endDate);
|
|
|
|
this.state.confirmPeriod(startDate, endDate);
|
|
|
|
this.showToast('기간이 확정되었습니다', 'success');
|
|
|
|
console.log('✅ 기간 확정 완료 - 각 분석 버튼을 눌러서 데이터를 확인하세요');
|
|
|
|
} catch (error) {
|
|
console.error('❌ 기간 확정 처리 오류:', error);
|
|
this.state.setError(error);
|
|
this.showToast(error.message, 'error');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 분석 모드 변경 처리
|
|
*/
|
|
handleModeChange(mode) {
|
|
try {
|
|
this.state.setAnalysisMode(mode);
|
|
this.updateModeButtons(mode);
|
|
|
|
// 캐시 초기화
|
|
this.state.clearCache();
|
|
|
|
} catch (error) {
|
|
this.state.setError(error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 탭 변경 처리
|
|
*/
|
|
handleTabChange(tabId) {
|
|
this.state.setCurrentTab(tabId);
|
|
}
|
|
|
|
/**
|
|
* 날짜 변경 처리
|
|
*/
|
|
handleDateChange() {
|
|
// 날짜가 변경되면 기간 확정 상태 해제
|
|
this.state.updateState({
|
|
confirmedPeriod: {
|
|
...this.state.getState().confirmedPeriod,
|
|
confirmed: false
|
|
},
|
|
isAnalysisEnabled: false
|
|
});
|
|
|
|
this.updateAnalysisButtons(false);
|
|
this.hideAnalysisTabs();
|
|
}
|
|
|
|
// ========== 분석 실행 ==========
|
|
|
|
/**
|
|
* 기본 통계 로드
|
|
*/
|
|
async loadBasicStats() {
|
|
const currentState = this.state.getState();
|
|
const { start, end } = currentState.confirmedPeriod;
|
|
|
|
try {
|
|
console.log('📊 기본 통계 로딩 시작 - 기간:', start, '~', end);
|
|
this.state.startLoading('기본 통계를 로딩 중입니다...');
|
|
|
|
console.log('🌐 API 호출 전 - getBasicStats 호출...');
|
|
const statsResponse = await this.api.getBasicStats(start, end);
|
|
|
|
console.log('📊 기본 통계 API 응답:', statsResponse);
|
|
|
|
if (statsResponse.success && statsResponse.data) {
|
|
const stats = statsResponse.data;
|
|
|
|
// 정상/오류 시간 계산
|
|
const totalHours = stats.totalHours || 0;
|
|
const errorReports = stats.errorRate || 0;
|
|
const errorHours = Math.round(totalHours * (errorReports / 100));
|
|
const normalHours = totalHours - errorHours;
|
|
|
|
const cardData = {
|
|
totalHours: totalHours,
|
|
normalHours: normalHours,
|
|
errorHours: errorHours,
|
|
workerCount: stats.activeWorkers || stats.activeworkers || 0,
|
|
errorRate: errorReports
|
|
};
|
|
|
|
this.state.setCache('basicStats', cardData);
|
|
this.updateResultCards(cardData);
|
|
|
|
console.log('✅ 기본 통계 로딩 완료:', cardData);
|
|
} else {
|
|
// 기본값으로 카드 업데이트
|
|
const defaultData = {
|
|
totalHours: 0,
|
|
normalHours: 0,
|
|
errorHours: 0,
|
|
workerCount: 0,
|
|
errorRate: 0
|
|
};
|
|
this.updateResultCards(defaultData);
|
|
console.warn('⚠️ 기본 통계 데이터가 없어서 기본값으로 설정');
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('❌ 기본 통계 로드 실패:', error);
|
|
// 에러 시에도 기본값으로 카드 업데이트
|
|
const defaultData = {
|
|
totalHours: 0,
|
|
normalHours: 0,
|
|
errorHours: 0,
|
|
workerCount: 0,
|
|
errorRate: 0
|
|
};
|
|
this.updateResultCards(defaultData);
|
|
} finally {
|
|
this.state.stopLoading();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 기간별 작업 현황 분석
|
|
*/
|
|
async analyzeWorkStatus() {
|
|
if (!this.state.canAnalyze()) {
|
|
this.showToast('기간을 먼저 확정해주세요', 'warning');
|
|
return;
|
|
}
|
|
|
|
const currentState = this.state.getState();
|
|
const { start, end } = currentState.confirmedPeriod;
|
|
|
|
try {
|
|
this.state.startLoading('기간별 작업 현황을 분석 중입니다...');
|
|
|
|
// 실제 API 호출
|
|
const batchData = await this.api.batchCall([
|
|
{
|
|
name: 'projectWorkType',
|
|
method: 'getProjectWorkTypeAnalysis',
|
|
startDate: start,
|
|
endDate: end
|
|
},
|
|
{
|
|
name: 'workerStats',
|
|
method: 'getWorkerStats',
|
|
startDate: start,
|
|
endDate: end
|
|
},
|
|
{
|
|
name: 'recentWork',
|
|
method: 'getRecentWork',
|
|
startDate: start,
|
|
endDate: end,
|
|
limit: 2000
|
|
}
|
|
]);
|
|
|
|
console.log('🔍 기간별 작업 현황 API 응답:', batchData);
|
|
|
|
// 데이터 처리
|
|
const recentWorkData = batchData.recentWork?.success ? batchData.recentWork.data.data : [];
|
|
const workerData = batchData.workerStats?.success ? batchData.workerStats.data.data : [];
|
|
|
|
const projectData = this.dataProcessor.aggregateProjectData(recentWorkData);
|
|
|
|
// 테이블 렌더링
|
|
this.tableRenderer.renderWorkStatusTable(projectData, workerData, recentWorkData);
|
|
|
|
this.showToast('기간별 작업 현황 분석이 완료되었습니다', 'success');
|
|
|
|
} catch (error) {
|
|
console.error('❌ 기간별 작업 현황 분석 오류:', error);
|
|
this.state.setError(error);
|
|
this.showToast('기간별 작업 현황 분석에 실패했습니다', 'error');
|
|
} finally {
|
|
this.state.stopLoading();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 프로젝트별 분포 분석
|
|
*/
|
|
async analyzeProjectDistribution() {
|
|
if (!this.state.canAnalyze()) {
|
|
this.showToast('기간을 먼저 확정해주세요', 'warning');
|
|
return;
|
|
}
|
|
|
|
const currentState = this.state.getState();
|
|
const { start, end } = currentState.confirmedPeriod;
|
|
|
|
try {
|
|
this.state.startLoading('프로젝트별 분포를 분석 중입니다...');
|
|
|
|
// 실제 API 호출
|
|
const distributionData = await this.api.getProjectDistributionData(start, end);
|
|
|
|
console.log('🔍 프로젝트별 분포 API 응답:', distributionData);
|
|
|
|
// 데이터 처리
|
|
const recentWorkData = distributionData.recentWork?.success ? distributionData.recentWork.data.data : [];
|
|
const workerData = distributionData.workerStats?.success ? distributionData.workerStats.data.data : [];
|
|
|
|
const projectData = this.dataProcessor.aggregateProjectData(recentWorkData);
|
|
|
|
console.log('📊 취합된 프로젝트 데이터:', projectData);
|
|
|
|
// 테이블 렌더링
|
|
this.tableRenderer.renderProjectDistributionTable(projectData, workerData);
|
|
|
|
this.showToast('프로젝트별 분포 분석이 완료되었습니다', 'success');
|
|
|
|
} catch (error) {
|
|
console.error('❌ 프로젝트별 분포 분석 오류:', error);
|
|
this.state.setError(error);
|
|
this.showToast('프로젝트별 분포 분석에 실패했습니다', 'error');
|
|
} finally {
|
|
this.state.stopLoading();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 작업자별 성과 분석
|
|
*/
|
|
async analyzeWorkerPerformance() {
|
|
if (!this.state.canAnalyze()) {
|
|
this.showToast('기간을 먼저 확정해주세요', 'warning');
|
|
return;
|
|
}
|
|
|
|
const currentState = this.state.getState();
|
|
const { start, end } = currentState.confirmedPeriod;
|
|
|
|
try {
|
|
this.state.startLoading('작업자별 성과를 분석 중입니다...');
|
|
|
|
const workerStatsResponse = await this.api.getWorkerStats(start, end);
|
|
|
|
console.log('👤 작업자 통계 API 응답:', workerStatsResponse);
|
|
|
|
if (workerStatsResponse.success && workerStatsResponse.data) {
|
|
this.chartRenderer.renderWorkerPerformanceChart(workerStatsResponse.data);
|
|
this.showToast('작업자별 성과 분석이 완료되었습니다', 'success');
|
|
} else {
|
|
throw new Error('작업자 데이터를 가져올 수 없습니다');
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('❌ 작업자별 성과 분석 오류:', error);
|
|
this.state.setError(error);
|
|
this.showToast('작업자별 성과 분석에 실패했습니다', 'error');
|
|
} finally {
|
|
this.state.stopLoading();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 오류 분석
|
|
*/
|
|
async analyzeErrorAnalysis() {
|
|
if (!this.state.canAnalyze()) {
|
|
this.showToast('기간을 먼저 확정해주세요', 'warning');
|
|
return;
|
|
}
|
|
|
|
const currentState = this.state.getState();
|
|
const { start, end } = currentState.confirmedPeriod;
|
|
|
|
try {
|
|
this.state.startLoading('오류 분석을 진행 중입니다...');
|
|
|
|
// 병렬로 API 호출
|
|
const [recentWorkResponse, errorAnalysisResponse] = await Promise.all([
|
|
this.api.getRecentWork(start, end, 2000),
|
|
this.api.getErrorAnalysis(start, end)
|
|
]);
|
|
|
|
console.log('🔍 오류 분석 API 응답:', recentWorkResponse);
|
|
|
|
if (recentWorkResponse.success && recentWorkResponse.data) {
|
|
this.tableRenderer.renderErrorAnalysisTable(recentWorkResponse.data);
|
|
this.showToast('오류 분석이 완료되었습니다', 'success');
|
|
} else {
|
|
throw new Error('작업 데이터를 가져올 수 없습니다');
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('❌ 오류 분석 실패:', error);
|
|
this.state.setError(error);
|
|
this.showToast('오류 분석에 실패했습니다', 'error');
|
|
} finally {
|
|
this.state.stopLoading();
|
|
}
|
|
}
|
|
|
|
// ========== UI 업데이트 ==========
|
|
|
|
/**
|
|
* 결과 카드 업데이트
|
|
*/
|
|
updateResultCards(stats) {
|
|
const cards = {
|
|
totalHours: stats.totalHours || 0,
|
|
normalHours: stats.normalHours || 0,
|
|
errorHours: stats.errorHours || 0,
|
|
workerCount: stats.activeWorkers || 0,
|
|
errorRate: stats.errorRate || 0
|
|
};
|
|
|
|
Object.entries(cards).forEach(([key, value]) => {
|
|
const element = document.getElementById(key);
|
|
if (element) {
|
|
element.textContent = typeof value === 'number' ?
|
|
(key.includes('Rate') ? `${value}%` : value.toLocaleString()) : value;
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 분석 버튼 상태 업데이트
|
|
*/
|
|
updateAnalysisButtons(enabled) {
|
|
const buttons = document.querySelectorAll('.chart-analyze-btn');
|
|
buttons.forEach(button => {
|
|
button.disabled = !enabled;
|
|
button.style.opacity = enabled ? '1' : '0.5';
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 분석 탭 표시
|
|
*/
|
|
showAnalysisTabs() {
|
|
const tabNavigation = document.getElementById('analysisTabNavigation');
|
|
if (tabNavigation) {
|
|
tabNavigation.style.display = 'block';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 분석 탭 숨김
|
|
*/
|
|
hideAnalysisTabs() {
|
|
const tabNavigation = document.getElementById('analysisTabNavigation');
|
|
if (tabNavigation) {
|
|
tabNavigation.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 활성 탭 업데이트
|
|
*/
|
|
updateActiveTab(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');
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 모드 버튼 업데이트
|
|
*/
|
|
updateModeButtons(mode) {
|
|
document.querySelectorAll('[data-mode]').forEach(button => {
|
|
button.classList.remove('active');
|
|
if (button.dataset.mode === mode) {
|
|
button.classList.add('active');
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 로딩 표시
|
|
*/
|
|
showLoading(message = '분석 중입니다...') {
|
|
const loadingElement = document.getElementById('loadingState');
|
|
if (loadingElement) {
|
|
const textElement = loadingElement.querySelector('.loading-text');
|
|
if (textElement) {
|
|
textElement.textContent = message;
|
|
}
|
|
loadingElement.style.display = 'flex';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 로딩 숨김
|
|
*/
|
|
hideLoading() {
|
|
const loadingElement = document.getElementById('loadingState');
|
|
if (loadingElement) {
|
|
loadingElement.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 토스트 메시지 표시
|
|
*/
|
|
showToast(message, type = 'info') {
|
|
console.log(`📢 ${type.toUpperCase()}: ${message}`);
|
|
|
|
// 간단한 토스트 구현 (실제로는 더 정교한 토스트 라이브러리 사용 권장)
|
|
if (type === 'error') {
|
|
alert(`❌ ${message}`);
|
|
} else if (type === 'success') {
|
|
console.log(`✅ ${message}`);
|
|
} else if (type === 'warning') {
|
|
alert(`⚠️ ${message}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 에러 처리
|
|
*/
|
|
handleError(errorInfo) {
|
|
console.error('❌ 에러 발생:', errorInfo);
|
|
this.showToast(errorInfo.message, 'error');
|
|
}
|
|
|
|
// ========== 유틸리티 ==========
|
|
|
|
/**
|
|
* 컨트롤러 상태 디버그
|
|
*/
|
|
debug() {
|
|
console.log('🔍 메인 컨트롤러 상태:');
|
|
console.log('- API 클라이언트:', this.api);
|
|
console.log('- 상태 관리자:', this.state.getState());
|
|
console.log('- 차트 상태:', this.chartRenderer.getChartStatus());
|
|
}
|
|
}
|
|
|
|
// 전역 인스턴스 생성 및 초기화
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
window.WorkAnalysisMainController = new WorkAnalysisMainController();
|
|
});
|
|
|
|
// Export는 브라우저 환경에서 제거됨
|