feat: 다수 기능 개선 - 순찰, 출근, 작업분석, 모바일 UI 등
- 순찰/점검 기능 개선 (zone-detail 페이지 추가) - 출근/근태 시스템 개선 (연차 조회, 근무현황) - 작업분석 대분류 그룹화 및 마이그레이션 스크립트 - 모바일 네비게이션 UI 추가 - NAS 배포 도구 및 문서 추가 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
329
deploy/tkfb-package/web-ui/js/workplace-management/api.js
Normal file
329
deploy/tkfb-package/web-ui/js/workplace-management/api.js
Normal file
@@ -0,0 +1,329 @@
|
||||
/**
|
||||
* Workplace Management - API Client
|
||||
* 작업장 관리 관련 모든 API 호출을 관리
|
||||
*/
|
||||
|
||||
class WorkplaceAPI {
|
||||
constructor() {
|
||||
this.state = window.WorkplaceState;
|
||||
this.utils = window.WorkplaceUtils;
|
||||
console.log('[WorkplaceAPI] 초기화 완료');
|
||||
}
|
||||
|
||||
/**
|
||||
* 모든 데이터 로드
|
||||
*/
|
||||
async loadAllData() {
|
||||
try {
|
||||
await Promise.all([
|
||||
this.loadCategories(),
|
||||
this.loadWorkplaces()
|
||||
]);
|
||||
console.log('✅ 모든 데이터 로드 완료');
|
||||
} catch (error) {
|
||||
console.error('데이터 로딩 오류:', error);
|
||||
window.showToast?.('데이터를 불러오는데 실패했습니다.', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 카테고리 목록 로드
|
||||
*/
|
||||
async loadCategories() {
|
||||
try {
|
||||
const response = await window.apiCall('/workplaces/categories', 'GET');
|
||||
|
||||
let categoryData = [];
|
||||
if (response && response.success && Array.isArray(response.data)) {
|
||||
categoryData = response.data;
|
||||
} else if (Array.isArray(response)) {
|
||||
categoryData = response;
|
||||
}
|
||||
|
||||
this.state.categories = categoryData;
|
||||
console.log(`✅ 카테고리 ${this.state.categories.length}개 로드 완료`);
|
||||
return categoryData;
|
||||
} catch (error) {
|
||||
console.error('카테고리 로딩 오류:', error);
|
||||
this.state.categories = [];
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 카테고리 저장 (생성/수정)
|
||||
*/
|
||||
async saveCategory(categoryId, categoryData) {
|
||||
try {
|
||||
let response;
|
||||
if (categoryId) {
|
||||
response = await window.apiCall(`/workplaces/categories/${categoryId}`, 'PUT', categoryData);
|
||||
} else {
|
||||
response = await window.apiCall('/workplaces/categories', 'POST', categoryData);
|
||||
}
|
||||
|
||||
if (response && (response.success || response.category_id)) {
|
||||
return response;
|
||||
}
|
||||
throw new Error(response?.message || '저장에 실패했습니다.');
|
||||
} catch (error) {
|
||||
console.error('카테고리 저장 오류:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 카테고리 삭제
|
||||
*/
|
||||
async deleteCategory(categoryId) {
|
||||
try {
|
||||
const response = await window.apiCall(`/workplaces/categories/${categoryId}`, 'DELETE');
|
||||
if (response && response.success) {
|
||||
return response;
|
||||
}
|
||||
throw new Error(response?.message || '삭제에 실패했습니다.');
|
||||
} catch (error) {
|
||||
console.error('카테고리 삭제 오류:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 작업장 목록 로드
|
||||
*/
|
||||
async loadWorkplaces() {
|
||||
try {
|
||||
const response = await window.apiCall('/workplaces', 'GET');
|
||||
|
||||
let workplaceData = [];
|
||||
if (response && response.success && Array.isArray(response.data)) {
|
||||
workplaceData = response.data;
|
||||
} else if (Array.isArray(response)) {
|
||||
workplaceData = response;
|
||||
}
|
||||
|
||||
this.state.workplaces = workplaceData;
|
||||
console.log(`✅ 작업장 ${this.state.workplaces.length}개 로드 완료`);
|
||||
return workplaceData;
|
||||
} catch (error) {
|
||||
console.error('작업장 로딩 오류:', error);
|
||||
this.state.workplaces = [];
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 작업장 저장 (생성/수정)
|
||||
*/
|
||||
async saveWorkplace(workplaceId, workplaceData) {
|
||||
try {
|
||||
let response;
|
||||
if (workplaceId) {
|
||||
response = await window.apiCall(`/workplaces/${workplaceId}`, 'PUT', workplaceData);
|
||||
} else {
|
||||
response = await window.apiCall('/workplaces', 'POST', workplaceData);
|
||||
}
|
||||
|
||||
if (response && (response.success || response.workplace_id)) {
|
||||
return response;
|
||||
}
|
||||
throw new Error(response?.message || '저장에 실패했습니다.');
|
||||
} catch (error) {
|
||||
console.error('작업장 저장 오류:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 작업장 삭제
|
||||
*/
|
||||
async deleteWorkplace(workplaceId) {
|
||||
try {
|
||||
const response = await window.apiCall(`/workplaces/${workplaceId}`, 'DELETE');
|
||||
if (response && response.success) {
|
||||
return response;
|
||||
}
|
||||
throw new Error(response?.message || '삭제에 실패했습니다.');
|
||||
} catch (error) {
|
||||
console.error('작업장 삭제 오류:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 카테고리의 지도 영역 로드
|
||||
*/
|
||||
async loadMapRegions(categoryId) {
|
||||
try {
|
||||
const response = await window.apiCall(`/workplaces/categories/${categoryId}/map-regions`, 'GET');
|
||||
|
||||
let regions = [];
|
||||
if (response && response.success && Array.isArray(response.data)) {
|
||||
regions = response.data;
|
||||
} else if (Array.isArray(response)) {
|
||||
regions = response;
|
||||
}
|
||||
return regions;
|
||||
} catch (error) {
|
||||
console.error('지도 영역 로드 오류:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 작업장의 지도 영역 로드
|
||||
*/
|
||||
async loadWorkplaceMapRegion(workplaceId) {
|
||||
try {
|
||||
const response = await window.apiCall(`/workplaces/map-regions/workplace/${workplaceId}`, 'GET');
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error('작업장 지도 영역 로드 오류:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 작업장의 설비 목록 로드
|
||||
*/
|
||||
async loadWorkplaceEquipments(workplaceId) {
|
||||
try {
|
||||
const response = await window.apiCall(`/equipments/workplace/${workplaceId}`, 'GET');
|
||||
|
||||
let equipments = [];
|
||||
if (response && response.success && Array.isArray(response.data)) {
|
||||
equipments = response.data;
|
||||
} else if (Array.isArray(response)) {
|
||||
equipments = response;
|
||||
}
|
||||
|
||||
// 지도 영역이 있는 설비만 workplaceEquipmentRegions에 추가
|
||||
this.state.workplaceEquipmentRegions = equipments
|
||||
.filter(eq => eq.map_x_percent != null && eq.map_y_percent != null)
|
||||
.map(eq => ({
|
||||
equipment_id: eq.equipment_id,
|
||||
equipment_name: eq.equipment_name,
|
||||
equipment_code: eq.equipment_code,
|
||||
x_percent: parseFloat(eq.map_x_percent),
|
||||
y_percent: parseFloat(eq.map_y_percent),
|
||||
width_percent: parseFloat(eq.map_width_percent) || 10,
|
||||
height_percent: parseFloat(eq.map_height_percent) || 10
|
||||
}));
|
||||
|
||||
this.state.existingEquipments = equipments;
|
||||
|
||||
console.log(`✅ 작업장 ${workplaceId}의 설비 ${equipments.length}개 로드 완료`);
|
||||
return equipments;
|
||||
} catch (error) {
|
||||
console.error('설비 로드 오류:', error);
|
||||
this.state.workplaceEquipmentRegions = [];
|
||||
this.state.existingEquipments = [];
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 전체 설비 목록 로드
|
||||
*/
|
||||
async loadAllEquipments() {
|
||||
try {
|
||||
const response = await window.apiCall('/equipments', 'GET');
|
||||
|
||||
let equipments = [];
|
||||
if (response && response.success && Array.isArray(response.data)) {
|
||||
equipments = response.data;
|
||||
} else if (Array.isArray(response)) {
|
||||
equipments = response;
|
||||
}
|
||||
|
||||
this.state.allEquipments = equipments;
|
||||
console.log(`✅ 전체 설비 ${this.state.allEquipments.length}개 로드 완료`);
|
||||
return equipments;
|
||||
} catch (error) {
|
||||
console.error('전체 설비 로드 오류:', error);
|
||||
this.state.allEquipments = [];
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 설비 지도 위치 업데이트
|
||||
*/
|
||||
async updateEquipmentMapPosition(equipmentId, positionData) {
|
||||
try {
|
||||
const response = await window.apiCall(`/equipments/${equipmentId}/map-position`, 'PATCH', positionData);
|
||||
if (!response || !response.success) {
|
||||
throw new Error(response?.message || '위치 저장 실패');
|
||||
}
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error('설비 위치 업데이트 오류:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 새 설비 생성
|
||||
*/
|
||||
async createEquipment(equipmentData) {
|
||||
try {
|
||||
const response = await window.apiCall('/equipments', 'POST', equipmentData);
|
||||
if (!response || !response.success) {
|
||||
throw new Error(response?.message || '설비 생성 실패');
|
||||
}
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error('설비 생성 오류:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 다음 관리번호 조회
|
||||
*/
|
||||
async getNextEquipmentCode() {
|
||||
try {
|
||||
const response = await window.apiCall('/equipments/next-code', 'GET');
|
||||
if (response && response.success) {
|
||||
return response.data.next_code;
|
||||
}
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error('다음 관리번호 조회 실패:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 작업장 레이아웃 이미지 업로드
|
||||
*/
|
||||
async uploadWorkplaceLayout(workplaceId, formData) {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${this.utils.getApiBaseUrl()}/workplaces/${workplaceId}/layout-image`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${localStorage.getItem('token')}`
|
||||
},
|
||||
body: formData
|
||||
}
|
||||
);
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('레이아웃 이미지 업로드 오류:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 전역 인스턴스 생성
|
||||
window.WorkplaceAPI = new WorkplaceAPI();
|
||||
|
||||
// 하위 호환성: 기존 함수들
|
||||
window.loadCategories = () => window.WorkplaceAPI.loadCategories();
|
||||
window.loadWorkplaces = () => window.WorkplaceAPI.loadWorkplaces();
|
||||
window.loadWorkplaceEquipments = (id) => window.WorkplaceAPI.loadWorkplaceEquipments(id);
|
||||
window.loadAllEquipments = () => window.WorkplaceAPI.loadAllEquipments();
|
||||
|
||||
console.log('[Module] workplace-management/api.js 로드 완료');
|
||||
553
deploy/tkfb-package/web-ui/js/workplace-management/index.js
Normal file
553
deploy/tkfb-package/web-ui/js/workplace-management/index.js
Normal file
@@ -0,0 +1,553 @@
|
||||
/**
|
||||
* 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 로드 완료');
|
||||
284
deploy/tkfb-package/web-ui/js/workplace-management/state.js
Normal file
284
deploy/tkfb-package/web-ui/js/workplace-management/state.js
Normal file
@@ -0,0 +1,284 @@
|
||||
/**
|
||||
* Workplace Management - State Manager
|
||||
* 작업장 관리 페이지의 전역 상태 관리
|
||||
*/
|
||||
|
||||
class WorkplaceState {
|
||||
constructor() {
|
||||
// 마스터 데이터
|
||||
this.categories = [];
|
||||
this.workplaces = [];
|
||||
this.allEquipments = [];
|
||||
this.existingEquipments = [];
|
||||
|
||||
// 현재 상태
|
||||
this.currentCategoryId = '';
|
||||
this.currentEditingCategory = null;
|
||||
this.currentEditingWorkplace = null;
|
||||
this.currentWorkplaceMapId = null;
|
||||
|
||||
// 작업장 지도 관련
|
||||
this.workplaceCanvas = null;
|
||||
this.workplaceCtx = null;
|
||||
this.workplaceImage = null;
|
||||
this.workplaceIsDrawing = false;
|
||||
this.workplaceStartX = 0;
|
||||
this.workplaceStartY = 0;
|
||||
this.workplaceCurrentRect = null;
|
||||
this.workplaceEquipmentRegions = [];
|
||||
|
||||
// 전체화면 편집기 관련
|
||||
this.fsCanvas = null;
|
||||
this.fsCtx = null;
|
||||
this.fsImage = null;
|
||||
this.fsIsDrawing = false;
|
||||
this.fsStartX = 0;
|
||||
this.fsStartY = 0;
|
||||
this.fsCurrentRect = null;
|
||||
this.fsSidebarVisible = true;
|
||||
|
||||
// 리스너
|
||||
this.listeners = new Map();
|
||||
|
||||
console.log('[WorkplaceState] 초기화 완료');
|
||||
}
|
||||
|
||||
/**
|
||||
* 상태 업데이트
|
||||
*/
|
||||
update(key, value) {
|
||||
const prevValue = this[key];
|
||||
this[key] = value;
|
||||
this.notifyListeners(key, value, prevValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* 리스너 등록
|
||||
*/
|
||||
subscribe(key, callback) {
|
||||
if (!this.listeners.has(key)) {
|
||||
this.listeners.set(key, []);
|
||||
}
|
||||
this.listeners.get(key).push(callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* 리스너 알림
|
||||
*/
|
||||
notifyListeners(key, newValue, prevValue) {
|
||||
const keyListeners = this.listeners.get(key) || [];
|
||||
keyListeners.forEach(callback => {
|
||||
try {
|
||||
callback(newValue, prevValue);
|
||||
} catch (error) {
|
||||
console.error(`[WorkplaceState] 리스너 오류 (${key}):`, error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 카테고리 변경
|
||||
*/
|
||||
setCurrentCategory(categoryId) {
|
||||
const prevId = this.currentCategoryId;
|
||||
this.currentCategoryId = categoryId === '' ? '' : categoryId;
|
||||
this.notifyListeners('currentCategoryId', this.currentCategoryId, prevId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 카테고리 정보 가져오기
|
||||
*/
|
||||
getCurrentCategory() {
|
||||
if (!this.currentCategoryId) return null;
|
||||
return this.categories.find(c => c.category_id == this.currentCategoryId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 카테고리별 작업장 가져오기
|
||||
*/
|
||||
getFilteredWorkplaces() {
|
||||
if (this.currentCategoryId === '') {
|
||||
return this.workplaces;
|
||||
}
|
||||
return this.workplaces.filter(w => w.category_id == this.currentCategoryId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 작업장 지도 상태 초기화
|
||||
*/
|
||||
resetWorkplaceMapState() {
|
||||
this.workplaceCanvas = null;
|
||||
this.workplaceCtx = null;
|
||||
this.workplaceImage = null;
|
||||
this.workplaceIsDrawing = false;
|
||||
this.workplaceCurrentRect = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 전체화면 편집기 상태 초기화
|
||||
*/
|
||||
resetFullscreenState() {
|
||||
this.fsCanvas = null;
|
||||
this.fsCtx = null;
|
||||
this.fsImage = null;
|
||||
this.fsIsDrawing = false;
|
||||
this.fsCurrentRect = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 통계 계산
|
||||
*/
|
||||
getStatistics() {
|
||||
const total = this.workplaces.length;
|
||||
const active = this.workplaces.filter(w =>
|
||||
w.is_active === 1 || w.is_active === true
|
||||
).length;
|
||||
const factoryTotal = this.categories.length;
|
||||
|
||||
const filtered = this.getFilteredWorkplaces();
|
||||
const filteredActive = filtered.filter(w =>
|
||||
w.is_active === 1 || w.is_active === true
|
||||
).length;
|
||||
|
||||
return {
|
||||
total,
|
||||
active,
|
||||
factoryTotal,
|
||||
filteredTotal: filtered.length,
|
||||
filteredActive
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 상태 초기화
|
||||
*/
|
||||
reset() {
|
||||
this.currentEditingCategory = null;
|
||||
this.currentEditingWorkplace = null;
|
||||
this.currentWorkplaceMapId = null;
|
||||
this.resetWorkplaceMapState();
|
||||
this.resetFullscreenState();
|
||||
}
|
||||
|
||||
/**
|
||||
* 디버그 출력
|
||||
*/
|
||||
debug() {
|
||||
console.log('[WorkplaceState] 현재 상태:', {
|
||||
categories: this.categories.length,
|
||||
workplaces: this.workplaces.length,
|
||||
currentCategoryId: this.currentCategoryId,
|
||||
allEquipments: this.allEquipments.length,
|
||||
workplaceEquipmentRegions: this.workplaceEquipmentRegions.length
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 전역 인스턴스 생성
|
||||
window.WorkplaceState = new WorkplaceState();
|
||||
|
||||
// 하위 호환성을 위한 전역 변수 프록시
|
||||
const wpStateProxy = window.WorkplaceState;
|
||||
|
||||
Object.defineProperties(window, {
|
||||
categories: {
|
||||
get: () => wpStateProxy.categories,
|
||||
set: (v) => { wpStateProxy.categories = v; }
|
||||
},
|
||||
workplaces: {
|
||||
get: () => wpStateProxy.workplaces,
|
||||
set: (v) => { wpStateProxy.workplaces = v; }
|
||||
},
|
||||
currentCategoryId: {
|
||||
get: () => wpStateProxy.currentCategoryId,
|
||||
set: (v) => { wpStateProxy.currentCategoryId = v; }
|
||||
},
|
||||
currentEditingCategory: {
|
||||
get: () => wpStateProxy.currentEditingCategory,
|
||||
set: (v) => { wpStateProxy.currentEditingCategory = v; }
|
||||
},
|
||||
currentEditingWorkplace: {
|
||||
get: () => wpStateProxy.currentEditingWorkplace,
|
||||
set: (v) => { wpStateProxy.currentEditingWorkplace = v; }
|
||||
},
|
||||
workplaceCanvas: {
|
||||
get: () => wpStateProxy.workplaceCanvas,
|
||||
set: (v) => { wpStateProxy.workplaceCanvas = v; }
|
||||
},
|
||||
workplaceCtx: {
|
||||
get: () => wpStateProxy.workplaceCtx,
|
||||
set: (v) => { wpStateProxy.workplaceCtx = v; }
|
||||
},
|
||||
workplaceImage: {
|
||||
get: () => wpStateProxy.workplaceImage,
|
||||
set: (v) => { wpStateProxy.workplaceImage = v; }
|
||||
},
|
||||
workplaceIsDrawing: {
|
||||
get: () => wpStateProxy.workplaceIsDrawing,
|
||||
set: (v) => { wpStateProxy.workplaceIsDrawing = v; }
|
||||
},
|
||||
workplaceStartX: {
|
||||
get: () => wpStateProxy.workplaceStartX,
|
||||
set: (v) => { wpStateProxy.workplaceStartX = v; }
|
||||
},
|
||||
workplaceStartY: {
|
||||
get: () => wpStateProxy.workplaceStartY,
|
||||
set: (v) => { wpStateProxy.workplaceStartY = v; }
|
||||
},
|
||||
workplaceCurrentRect: {
|
||||
get: () => wpStateProxy.workplaceCurrentRect,
|
||||
set: (v) => { wpStateProxy.workplaceCurrentRect = v; }
|
||||
},
|
||||
workplaceEquipmentRegions: {
|
||||
get: () => wpStateProxy.workplaceEquipmentRegions,
|
||||
set: (v) => { wpStateProxy.workplaceEquipmentRegions = v; }
|
||||
},
|
||||
existingEquipments: {
|
||||
get: () => wpStateProxy.existingEquipments,
|
||||
set: (v) => { wpStateProxy.existingEquipments = v; }
|
||||
},
|
||||
allEquipments: {
|
||||
get: () => wpStateProxy.allEquipments,
|
||||
set: (v) => { wpStateProxy.allEquipments = v; }
|
||||
},
|
||||
fsCanvas: {
|
||||
get: () => wpStateProxy.fsCanvas,
|
||||
set: (v) => { wpStateProxy.fsCanvas = v; }
|
||||
},
|
||||
fsCtx: {
|
||||
get: () => wpStateProxy.fsCtx,
|
||||
set: (v) => { wpStateProxy.fsCtx = v; }
|
||||
},
|
||||
fsImage: {
|
||||
get: () => wpStateProxy.fsImage,
|
||||
set: (v) => { wpStateProxy.fsImage = v; }
|
||||
},
|
||||
fsIsDrawing: {
|
||||
get: () => wpStateProxy.fsIsDrawing,
|
||||
set: (v) => { wpStateProxy.fsIsDrawing = v; }
|
||||
},
|
||||
fsStartX: {
|
||||
get: () => wpStateProxy.fsStartX,
|
||||
set: (v) => { wpStateProxy.fsStartX = v; }
|
||||
},
|
||||
fsStartY: {
|
||||
get: () => wpStateProxy.fsStartY,
|
||||
set: (v) => { wpStateProxy.fsStartY = v; }
|
||||
},
|
||||
fsCurrentRect: {
|
||||
get: () => wpStateProxy.fsCurrentRect,
|
||||
set: (v) => { wpStateProxy.fsCurrentRect = v; }
|
||||
},
|
||||
fsSidebarVisible: {
|
||||
get: () => wpStateProxy.fsSidebarVisible,
|
||||
set: (v) => { wpStateProxy.fsSidebarVisible = v; }
|
||||
}
|
||||
});
|
||||
|
||||
// currentWorkplaceMapId를 window에도 설정
|
||||
Object.defineProperty(window, 'currentWorkplaceMapId', {
|
||||
get: () => wpStateProxy.currentWorkplaceMapId,
|
||||
set: (v) => { wpStateProxy.currentWorkplaceMapId = v; }
|
||||
});
|
||||
|
||||
console.log('[Module] workplace-management/state.js 로드 완료');
|
||||
154
deploy/tkfb-package/web-ui/js/workplace-management/utils.js
Normal file
154
deploy/tkfb-package/web-ui/js/workplace-management/utils.js
Normal file
@@ -0,0 +1,154 @@
|
||||
/**
|
||||
* Workplace Management - Utilities
|
||||
* 작업장 관리 관련 유틸리티 함수들
|
||||
*/
|
||||
|
||||
class WorkplaceUtils {
|
||||
constructor() {
|
||||
console.log('[WorkplaceUtils] 초기화 완료');
|
||||
}
|
||||
|
||||
/**
|
||||
* 날짜 포맷팅
|
||||
*/
|
||||
formatDate(dateString) {
|
||||
if (!dateString) return '';
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString('ko-KR', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* API URL 생성
|
||||
*/
|
||||
getApiBaseUrl() {
|
||||
return window.API_BASE_URL || 'http://localhost:20005/api';
|
||||
}
|
||||
|
||||
/**
|
||||
* 이미지 URL 생성
|
||||
*/
|
||||
getFullImageUrl(imagePath) {
|
||||
if (!imagePath) return null;
|
||||
if (imagePath.startsWith('http')) return imagePath;
|
||||
return `${this.getApiBaseUrl()}${imagePath}`.replace('/api/', '/');
|
||||
}
|
||||
|
||||
/**
|
||||
* 작업장 용도 아이콘 반환
|
||||
*/
|
||||
getPurposeIcon(purpose) {
|
||||
const icons = {
|
||||
'작업구역': '🔧',
|
||||
'설비': '⚙️',
|
||||
'휴게시설': '☕',
|
||||
'회의실': '💼',
|
||||
'창고': '📦',
|
||||
'기타': '📍'
|
||||
};
|
||||
return purpose ? (icons[purpose] || '📍') : '🏗️';
|
||||
}
|
||||
|
||||
/**
|
||||
* 퍼센트를 픽셀로 변환
|
||||
*/
|
||||
percentToPixel(percent, canvasSize) {
|
||||
return (percent / 100) * canvasSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* 픽셀을 퍼센트로 변환
|
||||
*/
|
||||
pixelToPercent(pixel, canvasSize) {
|
||||
return (pixel / canvasSize) * 100;
|
||||
}
|
||||
|
||||
/**
|
||||
* 영역 좌표 정규화 (음수 처리)
|
||||
*/
|
||||
normalizeRect(rect, canvasWidth, canvasHeight) {
|
||||
const xPercent = this.pixelToPercent(
|
||||
Math.min(rect.x, rect.x + rect.width),
|
||||
canvasWidth
|
||||
);
|
||||
const yPercent = this.pixelToPercent(
|
||||
Math.min(rect.y, rect.y + rect.height),
|
||||
canvasHeight
|
||||
);
|
||||
const widthPercent = this.pixelToPercent(
|
||||
Math.abs(rect.width),
|
||||
canvasWidth
|
||||
);
|
||||
const heightPercent = this.pixelToPercent(
|
||||
Math.abs(rect.height),
|
||||
canvasHeight
|
||||
);
|
||||
|
||||
return { xPercent, yPercent, widthPercent, heightPercent };
|
||||
}
|
||||
}
|
||||
|
||||
// 전역 인스턴스 생성
|
||||
window.WorkplaceUtils = new WorkplaceUtils();
|
||||
|
||||
// 하위 호환성: 기존 함수들
|
||||
window.formatDate = (dateString) => window.WorkplaceUtils.formatDate(dateString);
|
||||
|
||||
// 토스트 메시지 표시
|
||||
window.showToast = function(message, type = 'info') {
|
||||
// 기존 토스트 제거
|
||||
const existingToast = document.querySelector('.toast');
|
||||
if (existingToast) {
|
||||
existingToast.remove();
|
||||
}
|
||||
|
||||
// 새 토스트 생성
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `toast toast-${type}`;
|
||||
toast.textContent = message;
|
||||
|
||||
// 스타일 적용
|
||||
Object.assign(toast.style, {
|
||||
position: 'fixed',
|
||||
top: '20px',
|
||||
right: '20px',
|
||||
padding: '12px 24px',
|
||||
borderRadius: '8px',
|
||||
color: 'white',
|
||||
fontWeight: '500',
|
||||
zIndex: '1000',
|
||||
transform: 'translateX(100%)',
|
||||
transition: 'transform 0.3s ease'
|
||||
});
|
||||
|
||||
// 타입별 배경색
|
||||
const colors = {
|
||||
success: '#10b981',
|
||||
error: '#ef4444',
|
||||
warning: '#f59e0b',
|
||||
info: '#3b82f6'
|
||||
};
|
||||
toast.style.backgroundColor = colors[type] || colors.info;
|
||||
|
||||
document.body.appendChild(toast);
|
||||
|
||||
// 애니메이션
|
||||
setTimeout(() => {
|
||||
toast.style.transform = 'translateX(0)';
|
||||
}, 100);
|
||||
|
||||
// 자동 제거
|
||||
setTimeout(() => {
|
||||
toast.style.transform = 'translateX(100%)';
|
||||
setTimeout(() => {
|
||||
if (toast.parentNode) {
|
||||
toast.remove();
|
||||
}
|
||||
}, 300);
|
||||
}, 3000);
|
||||
};
|
||||
|
||||
console.log('[Module] workplace-management/utils.js 로드 완료');
|
||||
Reference in New Issue
Block a user