- codes.html, code-management.js 삭제 (tasks.html에서 동일 기능 제공) - 사이드바에서 코드 관리 링크 제거 - daily-work-report, tbm, workplace-management JS 모듈 분리 - common/security.js 추가 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
554 lines
20 KiB
JavaScript
554 lines
20 KiB
JavaScript
/**
|
||
* 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 = `
|
||
<button class="wp-tab-btn ${currentCategoryId === '' ? 'active' : ''}"
|
||
data-category=""
|
||
onclick="switchCategory('')">
|
||
<span class="wp-tab-icon">🏗️</span>
|
||
전체
|
||
<span class="wp-tab-count">${workplaces.length}</span>
|
||
</button>
|
||
`;
|
||
|
||
categories.forEach(category => {
|
||
const count = workplaces.filter(w => w.category_id === category.category_id).length;
|
||
const isActive = currentCategoryId === category.category_id;
|
||
|
||
tabsHtml += `
|
||
<button class="wp-tab-btn ${isActive ? 'active' : ''}"
|
||
data-category="${category.category_id}"
|
||
onclick="switchCategory(${category.category_id})">
|
||
<span class="wp-tab-icon">🏭</span>
|
||
${category.category_name}
|
||
<span class="wp-tab-count">${count}</span>
|
||
</button>
|
||
`;
|
||
});
|
||
|
||
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 = `
|
||
<div style="text-align: center;">
|
||
<canvas id="previewCanvas" style="max-width: 100%; border-radius: 8px; box-shadow: 0 4px 6px rgba(0,0,0,0.1); cursor: default;"></canvas>
|
||
<p style="color: #64748b; margin-top: 12px; font-size: 14px;">
|
||
클릭하여 작업장 영역을 수정하려면 "지도 설정" 버튼을 누르세요
|
||
</p>
|
||
</div>
|
||
`;
|
||
|
||
await this.loadImageWithRegions(fullImageUrl, category.category_id);
|
||
} else {
|
||
previewDiv.innerHTML = `
|
||
<div style="padding: 40px;">
|
||
<span style="font-size: 48px;">🗺️</span>
|
||
<p style="color: #94a3b8; margin-top: 16px;">
|
||
이 공장의 레이아웃 이미지가 아직 등록되지 않았습니다
|
||
</p>
|
||
<p style="color: #cbd5e1; font-size: 14px; margin-top: 8px;">
|
||
"지도 설정" 버튼을 눌러 레이아웃 이미지를 업로드하고 작업장 위치를 지정하세요
|
||
</p>
|
||
</div>
|
||
`;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 이미지와 영역을 캔버스에 로드
|
||
*/
|
||
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 = `
|
||
<div class="wp-empty-state">
|
||
<div class="wp-empty-icon">🏗️</div>
|
||
<h3 class="wp-empty-title">등록된 작업장이 없습니다</h3>
|
||
<p class="wp-empty-description">"작업장 추가" 버튼을 눌러 작업장을 등록해보세요</p>
|
||
<button class="wp-btn wp-btn-primary" onclick="openWorkplaceModal()">
|
||
<span class="wp-btn-icon">➕</span>
|
||
첫 작업장 추가하기
|
||
</button>
|
||
</div>
|
||
`;
|
||
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 += `
|
||
<div class="wp-card ${isActive ? '' : 'inactive'}" onclick="editWorkplace(${workplace.workplace_id})">
|
||
<div class="wp-card-header">
|
||
<div class="wp-card-icon">${purposeIcon}</div>
|
||
<div class="wp-card-info">
|
||
<h3 class="wp-card-title">${workplace.workplace_name}</h3>
|
||
<div class="wp-card-tags">
|
||
${workplace.category_id ? `<span class="wp-card-tag factory">🏭 ${categoryName}</span>` : ''}
|
||
${workplace.workplace_purpose ? `<span class="wp-card-tag purpose">${workplace.workplace_purpose}</span>` : ''}
|
||
</div>
|
||
</div>
|
||
<div class="wp-card-actions">
|
||
<button class="wp-card-btn map" onclick="event.stopPropagation(); openWorkplaceMapModal(${workplace.workplace_id})" title="지도 관리">
|
||
🗺️
|
||
</button>
|
||
<button class="wp-card-btn edit" onclick="event.stopPropagation(); editWorkplace(${workplace.workplace_id})" title="수정">
|
||
✏️
|
||
</button>
|
||
<button class="wp-card-btn delete" onclick="event.stopPropagation(); confirmDeleteWorkplace(${workplace.workplace_id})" title="삭제">
|
||
🗑️
|
||
</button>
|
||
</div>
|
||
</div>
|
||
${workplace.description ? `<p class="wp-card-description">${workplace.description}</p>` : ''}
|
||
<div class="wp-card-map" id="workplace-map-${workplace.workplace_id}"></div>
|
||
<div class="wp-card-meta">
|
||
<span class="wp-card-date">등록: ${this.utils.formatDate(workplace.created_at)}</span>
|
||
${workplace.updated_at !== workplace.created_at ? `<span class="wp-card-date">수정: ${this.utils.formatDate(workplace.updated_at)}</span>` : ''}
|
||
</div>
|
||
</div>
|
||
`;
|
||
});
|
||
|
||
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 = `
|
||
<div style="text-align: center; padding: 10px; background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%); border-radius: 8px; border: 1px solid #bae6fd; cursor: pointer;" onclick="openWorkplaceMapModal(${workplace.workplace_id})">
|
||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
|
||
<span style="font-size: 12px; color: #0369a1; font-weight: 600;">📍 작업장 지도</span>
|
||
${equipmentCount > 0 ? `<span style="font-size: 11px; background: #10b981; color: white; padding: 2px 8px; border-radius: 10px;">설비 ${equipmentCount}개</span>` : ''}
|
||
</div>
|
||
<canvas id="${canvasId}" style="max-width: 100%; border-radius: 6px; box-shadow: 0 2px 8px rgba(0,0,0,0.12);"></canvas>
|
||
<div style="font-size: 11px; color: #64748b; margin-top: 8px;">클릭하여 지도 관리</div>
|
||
</div>
|
||
`;
|
||
|
||
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 = `
|
||
<div style="text-align: center; padding: 16px; background: #f9fafb; border-radius: 8px; border: 2px dashed #cbd5e1; cursor: pointer;" onclick="openWorkplaceMapModal(${workplace.workplace_id})">
|
||
<div style="font-size: 24px; margin-bottom: 8px;">🗺️</div>
|
||
<div style="font-size: 12px; color: #64748b;">클릭하여 지도 설정</div>
|
||
</div>
|
||
`;
|
||
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 = `
|
||
<div style="text-align: center; padding: 10px; background: #f9fafb; border-radius: 8px; border: 1px solid #e5e7eb; cursor: pointer;" onclick="openWorkplaceMapModal(${workplace.workplace_id})">
|
||
<div style="font-size: 12px; color: #64748b; margin-bottom: 6px; font-weight: 500;">📍 공장 지도 내 위치</div>
|
||
<canvas id="${canvasId}" style="max-width: 100%; border-radius: 6px; box-shadow: 0 2px 6px rgba(0,0,0,0.1);"></canvas>
|
||
<div style="font-size: 11px; color: #94a3b8; margin-top: 6px;">클릭하여 상세 지도 설정</div>
|
||
</div>
|
||
`;
|
||
|
||
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 = '<span class="btn-icon">⏳</span>새로고침 중...';
|
||
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 로드 완료');
|