/**
* Workplace Management - Module Loader
* 작업장 관리 모듈을 초기화하고 연결하는 메인 진입점
*
* 로드 순서:
* 1. state.js - 전역 상태 관리
* 2. utils.js - 유틸리티 함수
* 3. api.js - API 클라이언트
* 4. index.js - 이 파일 (메인 컨트롤러)
*/
class WorkplaceController {
constructor() {
this.state = window.WorkplaceState;
this.api = window.WorkplaceAPI;
this.utils = window.WorkplaceUtils;
this.initialized = false;
console.log('[WorkplaceController] 생성');
}
/**
* 초기화
*/
async init() {
if (this.initialized) {
console.log('[WorkplaceController] 이미 초기화됨');
return;
}
console.log('🏗️ 작업장 관리 페이지 초기화 시작');
// API 함수가 로드될 때까지 대기
let retryCount = 0;
while (!window.apiCall && retryCount < 50) {
await new Promise(resolve => setTimeout(resolve, 100));
retryCount++;
}
if (!window.apiCall) {
window.showToast?.('시스템을 초기화할 수 없습니다. 페이지를 새로고침해주세요.', 'error');
return;
}
// 모든 데이터 로드
await this.loadAllData();
this.initialized = true;
console.log('[WorkplaceController] 초기화 완료');
}
/**
* 모든 데이터 로드
*/
async loadAllData() {
try {
await this.api.loadAllData();
this.renderCategoryTabs();
this.renderWorkplaces();
this.updateStatistics();
} catch (error) {
console.error('데이터 로딩 오류:', error);
window.showToast?.('데이터를 불러오는데 실패했습니다.', 'error');
}
}
/**
* 카테고리 탭 렌더링
*/
renderCategoryTabs() {
const tabsContainer = document.getElementById('categoryTabs');
if (!tabsContainer) return;
const categories = this.state.categories;
const workplaces = this.state.workplaces;
const currentCategoryId = this.state.currentCategoryId;
let tabsHtml = `
`;
categories.forEach(category => {
const count = workplaces.filter(w => w.category_id === category.category_id).length;
const isActive = currentCategoryId === category.category_id;
tabsHtml += `
`;
});
tabsContainer.innerHTML = tabsHtml;
}
/**
* 카테고리 전환
*/
async switchCategory(categoryId) {
this.state.setCurrentCategory(categoryId);
this.renderCategoryTabs();
this.renderWorkplaces();
const layoutMapSection = document.getElementById('layoutMapSection');
const selectedCategoryName = document.getElementById('selectedCategoryName');
if (categoryId && layoutMapSection) {
const category = this.state.getCurrentCategory();
if (category) {
layoutMapSection.style.display = 'block';
if (selectedCategoryName) {
selectedCategoryName.textContent = category.category_name;
}
await this.updateLayoutPreview(category);
}
} else if (layoutMapSection) {
layoutMapSection.style.display = 'none';
}
}
/**
* 레이아웃 미리보기 업데이트
*/
async updateLayoutPreview(category) {
const previewDiv = document.getElementById('layoutMapPreview');
if (!previewDiv) return;
if (category.layout_image) {
const fullImageUrl = this.utils.getFullImageUrl(category.layout_image);
previewDiv.innerHTML = `
클릭하여 작업장 영역을 수정하려면 "지도 설정" 버튼을 누르세요
`;
await this.loadImageWithRegions(fullImageUrl, category.category_id);
} else {
previewDiv.innerHTML = `
🗺️
이 공장의 레이아웃 이미지가 아직 등록되지 않았습니다
"지도 설정" 버튼을 눌러 레이아웃 이미지를 업로드하고 작업장 위치를 지정하세요
`;
}
}
/**
* 이미지와 영역을 캔버스에 로드
*/
async loadImageWithRegions(imageUrl, categoryId) {
const canvas = document.getElementById('previewCanvas');
if (!canvas) return;
const ctx = canvas.getContext('2d');
const img = new Image();
const self = this;
img.onload = async function() {
const maxWidth = 800;
const scale = img.width > maxWidth ? maxWidth / img.width : 1;
canvas.width = img.width * scale;
canvas.height = img.height * scale;
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
try {
const regions = await self.api.loadMapRegions(categoryId);
regions.forEach(region => {
const x1 = (region.x_start / 100) * canvas.width;
const y1 = (region.y_start / 100) * canvas.height;
const x2 = (region.x_end / 100) * canvas.width;
const y2 = (region.y_end / 100) * canvas.height;
const width = x2 - x1;
const height = y2 - y1;
ctx.strokeStyle = '#10b981';
ctx.lineWidth = 2;
ctx.strokeRect(x1, y1, width, height);
ctx.fillStyle = 'rgba(16, 185, 129, 0.15)';
ctx.fillRect(x1, y1, width, height);
if (region.workplace_name) {
ctx.font = 'bold 14px sans-serif';
const textMetrics = ctx.measureText(region.workplace_name);
const textPadding = 4;
ctx.fillStyle = 'rgba(255, 255, 255, 0.9)';
ctx.fillRect(x1 + 5, y1 + 5, textMetrics.width + textPadding * 2, 20);
ctx.fillStyle = '#10b981';
ctx.fillText(region.workplace_name, x1 + 5 + textPadding, y1 + 20);
}
});
if (regions.length > 0) {
console.log(`✅ 레이아웃 미리보기에 ${regions.length}개 영역 표시 완료`);
}
} catch (error) {
console.error('영역 로드 오류:', error);
}
};
img.onerror = function() {
console.error('이미지 로드 실패:', imageUrl);
};
img.src = imageUrl;
}
/**
* 작업장 렌더링
*/
renderWorkplaces() {
const grid = document.getElementById('workplaceGrid');
if (!grid) return;
const filtered = this.state.getFilteredWorkplaces();
if (filtered.length === 0) {
grid.innerHTML = `
🏗️
등록된 작업장이 없습니다
"작업장 추가" 버튼을 눌러 작업장을 등록해보세요
`;
return;
}
let gridHtml = '';
filtered.forEach(workplace => {
const categoryName = workplace.category_name || '미분류';
const isActive = workplace.is_active === 1 || workplace.is_active === true;
const purposeIcon = this.utils.getPurposeIcon(workplace.workplace_purpose);
gridHtml += `
${workplace.description ? `
${workplace.description}
` : ''}
등록: ${this.utils.formatDate(workplace.created_at)}
${workplace.updated_at !== workplace.created_at ? `수정: ${this.utils.formatDate(workplace.updated_at)}` : ''}
`;
});
grid.innerHTML = gridHtml;
filtered.forEach(workplace => {
if (workplace.category_id) {
this.loadWorkplaceMapThumbnail(workplace);
}
});
}
/**
* 작업장 카드에 지도 썸네일 로드
*/
async loadWorkplaceMapThumbnail(workplace) {
const thumbnailDiv = document.getElementById(`workplace-map-${workplace.workplace_id}`);
if (!thumbnailDiv) return;
if (workplace.layout_image) {
const fullImageUrl = this.utils.getFullImageUrl(workplace.layout_image);
let equipmentCount = 0;
try {
const eqResponse = await window.apiCall(`/equipments/workplace/${workplace.workplace_id}`, 'GET');
if (eqResponse && eqResponse.success && Array.isArray(eqResponse.data)) {
equipmentCount = eqResponse.data.filter(eq => eq.map_x_percent != null).length;
}
} catch (e) {
console.debug('설비 정보 로드 실패');
}
const canvasId = `layout-canvas-${workplace.workplace_id}`;
thumbnailDiv.innerHTML = `
📍 작업장 지도
${equipmentCount > 0 ? `설비 ${equipmentCount}개` : ''}
클릭하여 지도 관리
`;
await this.loadWorkplaceCanvasWithEquipments(workplace.workplace_id, fullImageUrl, canvasId);
return;
}
try {
const response = await this.api.loadWorkplaceMapRegion(workplace.workplace_id);
if (!response || (!response.success && !response.region_id)) {
thumbnailDiv.innerHTML = `
`;
return;
}
const region = response.success ? response.data : response;
if (!region || region.x_start === undefined || region.y_start === undefined ||
region.x_end === undefined || region.y_end === undefined) {
return;
}
const category = this.state.categories.find(c => c.category_id === workplace.category_id);
if (!category || !category.layout_image) return;
const fullImageUrl = this.utils.getFullImageUrl(category.layout_image);
const canvasId = `thumbnail-canvas-${workplace.workplace_id}`;
thumbnailDiv.innerHTML = `
📍 공장 지도 내 위치
클릭하여 상세 지도 설정
`;
const img = new Image();
img.onload = function() {
const canvas = document.getElementById(canvasId);
if (!canvas) return;
const ctx = canvas.getContext('2d');
const x1 = (region.x_start / 100) * img.width;
const y1 = (region.y_start / 100) * img.height;
const x2 = (region.x_end / 100) * img.width;
const y2 = (region.y_end / 100) * img.height;
const regionWidth = x2 - x1;
const regionHeight = y2 - y1;
const maxThumbWidth = 350;
const scale = regionWidth > maxThumbWidth ? maxThumbWidth / regionWidth : 1;
canvas.width = regionWidth * scale;
canvas.height = regionHeight * scale;
ctx.drawImage(
img,
x1, y1, regionWidth, regionHeight,
0, 0, canvas.width, canvas.height
);
ctx.strokeStyle = '#10b981';
ctx.lineWidth = 3;
ctx.strokeRect(0, 0, canvas.width, canvas.height);
};
img.onerror = function() {
thumbnailDiv.innerHTML = '';
};
img.src = fullImageUrl;
} catch (error) {
console.debug(`작업장 ${workplace.workplace_id}의 지도 영역 없음`);
}
}
/**
* 작업장 캔버스에 설비 영역 함께 그리기
*/
async loadWorkplaceCanvasWithEquipments(workplaceId, imageUrl, canvasId) {
const img = new Image();
img.onload = async function() {
const canvas = document.getElementById(canvasId);
if (!canvas) return;
const ctx = canvas.getContext('2d');
const maxThumbWidth = 400;
const scale = img.width > maxThumbWidth ? maxThumbWidth / img.width : 1;
canvas.width = img.width * scale;
canvas.height = img.height * scale;
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
try {
const response = await window.apiCall(`/equipments/workplace/${workplaceId}`, 'GET');
let equipments = [];
if (response && response.success && Array.isArray(response.data)) {
equipments = response.data.filter(eq => eq.map_x_percent != null);
}
equipments.forEach(eq => {
const x = (parseFloat(eq.map_x_percent) / 100) * canvas.width;
const y = (parseFloat(eq.map_y_percent) / 100) * canvas.height;
const width = (parseFloat(eq.map_width_percent || 10) / 100) * canvas.width;
const height = (parseFloat(eq.map_height_percent || 10) / 100) * canvas.height;
ctx.fillStyle = 'rgba(16, 185, 129, 0.2)';
ctx.fillRect(x, y, width, height);
ctx.strokeStyle = '#10b981';
ctx.lineWidth = 2;
ctx.strokeRect(x, y, width, height);
if (eq.equipment_code) {
ctx.font = 'bold 10px sans-serif';
const textMetrics = ctx.measureText(eq.equipment_code);
ctx.fillStyle = 'rgba(255, 255, 255, 0.9)';
ctx.fillRect(x + 2, y + 2, textMetrics.width + 6, 14);
ctx.fillStyle = '#047857';
ctx.fillText(eq.equipment_code, x + 5, y + 12);
}
});
} catch (error) {
console.debug('설비 영역 로드 실패');
}
};
img.src = imageUrl;
}
/**
* 통계 업데이트
*/
async updateStatistics() {
const stats = this.state.getStatistics();
const factoryCountEl = document.getElementById('factoryCount');
const totalCountEl = document.getElementById('totalCount');
const activeCountEl = document.getElementById('activeCount');
const equipmentCountEl = document.getElementById('equipmentCount');
if (factoryCountEl) factoryCountEl.textContent = stats.factoryTotal;
if (totalCountEl) totalCountEl.textContent = stats.total;
if (activeCountEl) activeCountEl.textContent = stats.active;
if (equipmentCountEl) {
try {
const equipments = await this.api.loadAllEquipments();
equipmentCountEl.textContent = equipments.length;
} catch (e) {
equipmentCountEl.textContent = '-';
}
}
const sectionTotalEl = document.getElementById('sectionTotalCount');
const sectionActiveEl = document.getElementById('sectionActiveCount');
if (sectionTotalEl) sectionTotalEl.textContent = stats.filteredTotal;
if (sectionActiveEl) sectionActiveEl.textContent = stats.filteredActive;
}
/**
* 전체 새로고침
*/
async refreshWorkplaces() {
const refreshBtn = document.querySelector('.btn-secondary');
if (refreshBtn) {
const originalText = refreshBtn.innerHTML;
refreshBtn.innerHTML = '⏳새로고침 중...';
refreshBtn.disabled = true;
await this.loadAllData();
refreshBtn.innerHTML = originalText;
refreshBtn.disabled = false;
} else {
await this.loadAllData();
}
window.showToast?.('데이터가 새로고침되었습니다.', 'success');
}
/**
* 디버그
*/
debug() {
console.log('[WorkplaceController] 상태 디버그:');
this.state.debug();
}
}
// 전역 인스턴스 생성
window.WorkplaceController = new WorkplaceController();
// 하위 호환성: 기존 전역 함수들
window.switchCategory = (categoryId) => window.WorkplaceController.switchCategory(categoryId);
window.renderCategoryTabs = () => window.WorkplaceController.renderCategoryTabs();
window.renderWorkplaces = () => window.WorkplaceController.renderWorkplaces();
window.updateStatistics = () => window.WorkplaceController.updateStatistics();
window.refreshWorkplaces = () => window.WorkplaceController.refreshWorkplaces();
window.loadAllData = () => window.WorkplaceController.loadAllData();
window.updateLayoutPreview = (category) => window.WorkplaceController.updateLayoutPreview(category);
// DOMContentLoaded 이벤트에서 초기화
document.addEventListener('DOMContentLoaded', () => {
setTimeout(() => {
window.WorkplaceController.init();
}, 100);
});
console.log('[Module] workplace-management/index.js 로드 완료');