Files
TK-FB-Project/web-ui/js/workplace-management.js
Hyungi Ahn f27728b168 feat: 작업장 관리 기능 추가 (공장-작업장 계층 구조)
- 공장(카테고리) 및 작업장 CRUD API 구현
- 탭 기반 UI로 공장별 작업장 필터링
- 터치 최적화된 관리자 페이지
- DB 테이블: workplace_categories, workplaces
- 관리자 메뉴에 작업장 관리 추가

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-26 14:31:58 +09:00

552 lines
17 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 작업장 관리 페이지 JavaScript
// 전역 변수
let categories = [];
let workplaces = [];
let currentCategoryId = '';
let currentEditingCategory = null;
let currentEditingWorkplace = null;
// 페이지 초기화
document.addEventListener('DOMContentLoaded', function() {
console.log('🏗️ 작업장 관리 페이지 초기화 시작');
loadAllData();
});
// 모든 데이터 로드
async function loadAllData() {
try {
await Promise.all([
loadCategories(),
loadWorkplaces()
]);
renderCategoryTabs();
renderWorkplaces();
updateStatistics();
} catch (error) {
console.error('데이터 로딩 오류:', error);
showToast('데이터를 불러오는데 실패했습니다.', 'error');
}
}
// ==================== 카테고리(공장) 관련 ====================
// 카테고리 목록 로드
async function loadCategories() {
try {
const response = await apiCall('/workplaces/categories', 'GET');
let categoryData = [];
if (response && response.success && Array.isArray(response.data)) {
categoryData = response.data;
} else if (Array.isArray(response)) {
categoryData = response;
}
categories = categoryData;
console.log(`✅ 카테고리 ${categories.length}개 로드 완료`);
} catch (error) {
console.error('카테고리 로딩 오류:', error);
categories = [];
}
}
// 카테고리 탭 렌더링
function renderCategoryTabs() {
const tabsContainer = document.getElementById('categoryTabs');
if (!tabsContainer) return;
// 전체 탭은 항상 표시
let tabsHtml = `
<button class="tab-btn ${currentCategoryId === '' ? 'active' : ''}"
data-category=""
onclick="switchCategory('')">
<span class="tab-icon">🏗️</span>
전체 (${workplaces.length})
</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="tab-btn ${isActive ? 'active' : ''}"
data-category="${category.category_id}"
onclick="switchCategory(${category.category_id})">
<span class="tab-icon">🏭</span>
${category.category_name} (${count})
</button>
`;
});
tabsContainer.innerHTML = tabsHtml;
}
// 카테고리 전환
function switchCategory(categoryId) {
currentCategoryId = categoryId === '' ? '' : categoryId;
renderCategoryTabs();
renderWorkplaces();
}
// 카테고리 모달 열기
function openCategoryModal(categoryData = null) {
const modal = document.getElementById('categoryModal');
const modalTitle = document.getElementById('categoryModalTitle');
const deleteBtn = document.getElementById('deleteCategoryBtn');
if (!modal) return;
currentEditingCategory = categoryData;
if (categoryData) {
// 수정 모드
modalTitle.textContent = '공장 수정';
deleteBtn.style.display = 'inline-flex';
document.getElementById('categoryId').value = categoryData.category_id;
document.getElementById('categoryName').value = categoryData.category_name || '';
document.getElementById('categoryDescription').value = categoryData.description || '';
document.getElementById('categoryOrder').value = categoryData.display_order || 0;
} else {
// 신규 등록 모드
modalTitle.textContent = '공장 추가';
deleteBtn.style.display = 'none';
document.getElementById('categoryForm').reset();
document.getElementById('categoryId').value = '';
}
modal.style.display = 'flex';
document.body.style.overflow = 'hidden';
setTimeout(() => {
document.getElementById('categoryName').focus();
}, 100);
}
// 카테고리 모달 닫기
function closeCategoryModal() {
const modal = document.getElementById('categoryModal');
if (modal) {
modal.style.display = 'none';
document.body.style.overflow = '';
currentEditingCategory = null;
}
}
// 카테고리 저장
async function saveCategory() {
try {
const categoryId = document.getElementById('categoryId').value;
const categoryData = {
category_name: document.getElementById('categoryName').value.trim(),
description: document.getElementById('categoryDescription').value.trim() || null,
display_order: parseInt(document.getElementById('categoryOrder').value) || 0,
is_active: true
};
if (!categoryData.category_name) {
showToast('공장명은 필수 입력 항목입니다.', 'error');
return;
}
console.log('💾 저장할 카테고리 데이터:', categoryData);
let response;
if (categoryId) {
// 수정
response = await apiCall(`/workplaces/categories/${categoryId}`, 'PUT', categoryData);
} else {
// 신규 등록
response = await apiCall('/workplaces/categories', 'POST', categoryData);
}
if (response && (response.success || response.category_id)) {
const action = categoryId ? '수정' : '등록';
showToast(`공장이 성공적으로 ${action}되었습니다.`, 'success');
closeCategoryModal();
await loadAllData();
} else {
throw new Error(response?.message || '저장에 실패했습니다.');
}
} catch (error) {
console.error('카테고리 저장 오류:', error);
showToast(error.message || '카테고리 저장 중 오류가 발생했습니다.', 'error');
}
}
// 카테고리 삭제
async function deleteCategory() {
if (!currentEditingCategory) return;
if (!confirm(`"${currentEditingCategory.category_name}" 공장을 정말 삭제하시겠습니까?\n\n⚠️ 이 공장에 속한 모든 작업장의 공장 정보가 제거됩니다.`)) {
return;
}
try {
const response = await apiCall(`/workplaces/categories/${currentEditingCategory.category_id}`, 'DELETE');
if (response && response.success) {
showToast('공장이 성공적으로 삭제되었습니다.', 'success');
closeCategoryModal();
await loadAllData();
} else {
throw new Error(response?.message || '삭제에 실패했습니다.');
}
} catch (error) {
console.error('카테고리 삭제 오류:', error);
showToast(error.message || '카테고리 삭제 중 오류가 발생했습니다.', 'error');
}
}
// ==================== 작업장 관련 ====================
// 작업장 목록 로드
async function loadWorkplaces() {
try {
const response = await apiCall('/workplaces', 'GET');
let workplaceData = [];
if (response && response.success && Array.isArray(response.data)) {
workplaceData = response.data;
} else if (Array.isArray(response)) {
workplaceData = response;
}
workplaces = workplaceData;
console.log(`✅ 작업장 ${workplaces.length}개 로드 완료`);
} catch (error) {
console.error('작업장 로딩 오류:', error);
workplaces = [];
}
}
// 작업장 렌더링
function renderWorkplaces() {
const grid = document.getElementById('workplaceGrid');
if (!grid) return;
// 현재 카테고리별 필터링
const filtered = currentCategoryId === ''
? workplaces
: workplaces.filter(w => w.category_id == currentCategoryId);
if (filtered.length === 0) {
grid.innerHTML = `
<div class="empty-state">
<div class="empty-icon">🏗️</div>
<h3>등록된 작업장이 없습니다.</h3>
<p>"작업장 추가" 버튼을 눌러 작업장을 등록해보세요.</p>
<button class="btn btn-primary" onclick="openWorkplaceModal()">
첫 작업장 추가하기
</button>
</div>
`;
return;
}
let gridHtml = '';
filtered.forEach(workplace => {
const categoryName = workplace.category_name || '미분류';
const isActive = workplace.is_active === 1 || workplace.is_active === true;
gridHtml += `
<div class="code-card workplace-card ${isActive ? '' : 'inactive'}" onclick="editWorkplace(${workplace.workplace_id})">
<div class="code-header">
<div class="code-icon" style="background: #dbeafe;">🏗️</div>
<div class="code-info">
<h3 class="code-name">${workplace.workplace_name}</h3>
${workplace.category_id ? `<span class="code-label">🏭 ${categoryName}</span>` : ''}
</div>
<div class="code-actions">
<button class="btn-small btn-edit" onclick="event.stopPropagation(); editWorkplace(${workplace.workplace_id})" title="수정">
✏️
</button>
<button class="btn-small btn-delete" onclick="event.stopPropagation(); confirmDeleteWorkplace(${workplace.workplace_id})" title="삭제">
🗑️
</button>
</div>
</div>
${workplace.description ? `<p class="code-description">${workplace.description}</p>` : ''}
<div class="code-meta">
<span class="code-date">등록: ${formatDate(workplace.created_at)}</span>
${workplace.updated_at !== workplace.created_at ? `<span class="code-date">수정: ${formatDate(workplace.updated_at)}</span>` : ''}
</div>
</div>
`;
});
grid.innerHTML = gridHtml;
}
// 작업장 모달 열기
function openWorkplaceModal(workplaceData = null) {
const modal = document.getElementById('workplaceModal');
const modalTitle = document.getElementById('workplaceModalTitle');
const deleteBtn = document.getElementById('deleteWorkplaceBtn');
const categorySelect = document.getElementById('workplaceCategoryId');
if (!modal) return;
currentEditingWorkplace = workplaceData;
// 카테고리 선택 옵션 업데이트
let categoryOptions = '<option value="">미분류</option>';
categories.forEach(cat => {
categoryOptions += `<option value="${cat.category_id}">${cat.category_name}</option>`;
});
categorySelect.innerHTML = categoryOptions;
if (workplaceData) {
// 수정 모드
modalTitle.textContent = '작업장 수정';
deleteBtn.style.display = 'inline-flex';
document.getElementById('workplaceId').value = workplaceData.workplace_id;
document.getElementById('workplaceCategoryId').value = workplaceData.category_id || '';
document.getElementById('workplaceName').value = workplaceData.workplace_name || '';
document.getElementById('workplaceDescription').value = workplaceData.description || '';
} else {
// 신규 등록 모드
modalTitle.textContent = '작업장 추가';
deleteBtn.style.display = 'none';
document.getElementById('workplaceForm').reset();
document.getElementById('workplaceId').value = '';
// 현재 선택된 카테고리가 있으면 자동 선택
if (currentCategoryId) {
document.getElementById('workplaceCategoryId').value = currentCategoryId;
}
}
modal.style.display = 'flex';
document.body.style.overflow = 'hidden';
setTimeout(() => {
document.getElementById('workplaceName').focus();
}, 100);
}
// 작업장 모달 닫기
function closeWorkplaceModal() {
const modal = document.getElementById('workplaceModal');
if (modal) {
modal.style.display = 'none';
document.body.style.overflow = '';
currentEditingWorkplace = null;
}
}
// 작업장 편집
function editWorkplace(workplaceId) {
const workplace = workplaces.find(w => w.workplace_id === workplaceId);
if (workplace) {
openWorkplaceModal(workplace);
} else {
showToast('작업장을 찾을 수 없습니다.', 'error');
}
}
// 작업장 저장
async function saveWorkplace() {
try {
const workplaceId = document.getElementById('workplaceId').value;
const workplaceData = {
category_id: document.getElementById('workplaceCategoryId').value || null,
workplace_name: document.getElementById('workplaceName').value.trim(),
description: document.getElementById('workplaceDescription').value.trim() || null,
is_active: true
};
if (!workplaceData.workplace_name) {
showToast('작업장명은 필수 입력 항목입니다.', 'error');
return;
}
console.log('💾 저장할 작업장 데이터:', workplaceData);
let response;
if (workplaceId) {
// 수정
response = await apiCall(`/workplaces/${workplaceId}`, 'PUT', workplaceData);
} else {
// 신규 등록
response = await apiCall('/workplaces', 'POST', workplaceData);
}
if (response && (response.success || response.workplace_id)) {
const action = workplaceId ? '수정' : '등록';
showToast(`작업장이 성공적으로 ${action}되었습니다.`, 'success');
closeWorkplaceModal();
await loadAllData();
} else {
throw new Error(response?.message || '저장에 실패했습니다.');
}
} catch (error) {
console.error('작업장 저장 오류:', error);
showToast(error.message || '작업장 저장 중 오류가 발생했습니다.', 'error');
}
}
// 작업장 삭제 확인
function confirmDeleteWorkplace(workplaceId) {
const workplace = workplaces.find(w => w.workplace_id === workplaceId);
if (!workplace) {
showToast('작업장을 찾을 수 없습니다.', 'error');
return;
}
if (confirm(`"${workplace.workplace_name}" 작업장을 정말 삭제하시겠습니까?\n\n⚠️ 삭제된 작업장은 복구할 수 없습니다.`)) {
deleteWorkplaceById(workplaceId);
}
}
// 작업장 삭제 (수정 모드에서)
function deleteWorkplace() {
if (currentEditingWorkplace) {
confirmDeleteWorkplace(currentEditingWorkplace.workplace_id);
}
}
// 작업장 삭제 실행
async function deleteWorkplaceById(workplaceId) {
try {
const response = await apiCall(`/workplaces/${workplaceId}`, 'DELETE');
if (response && response.success) {
showToast('작업장이 성공적으로 삭제되었습니다.', 'success');
closeWorkplaceModal();
await loadAllData();
} else {
throw new Error(response?.message || '삭제에 실패했습니다.');
}
} catch (error) {
console.error('작업장 삭제 오류:', error);
showToast(error.message || '작업장 삭제 중 오류가 발생했습니다.', 'error');
}
}
// ==================== 유틸리티 ====================
// 전체 새로고침
async function refreshWorkplaces() {
const refreshBtn = document.querySelector('.btn-secondary');
if (refreshBtn) {
const originalText = refreshBtn.innerHTML;
refreshBtn.innerHTML = '<span class="btn-icon">⏳</span>새로고침 중...';
refreshBtn.disabled = true;
await loadAllData();
refreshBtn.innerHTML = originalText;
refreshBtn.disabled = false;
} else {
await loadAllData();
}
showToast('데이터가 새로고침되었습니다.', 'success');
}
// 통계 업데이트
function updateStatistics() {
const total = workplaces.length;
const active = workplaces.filter(w => w.is_active === 1 || w.is_active === true).length;
document.getElementById('totalCount').textContent = total;
document.getElementById('activeCount').textContent = active;
}
// 날짜 포맷팅
function formatDate(dateString) {
if (!dateString) return '';
const date = new Date(dateString);
return date.toLocaleDateString('ko-KR', {
year: 'numeric',
month: '2-digit',
day: '2-digit'
});
}
// 토스트 메시지 표시
function showToast(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);
}
// 전역 함수로 노출
window.switchCategory = switchCategory;
window.openCategoryModal = openCategoryModal;
window.closeCategoryModal = closeCategoryModal;
window.saveCategory = saveCategory;
window.deleteCategory = deleteCategory;
window.openWorkplaceModal = openWorkplaceModal;
window.closeWorkplaceModal = closeWorkplaceModal;
window.editWorkplace = editWorkplace;
window.saveWorkplace = saveWorkplace;
window.deleteWorkplace = deleteWorkplace;
window.confirmDeleteWorkplace = confirmDeleteWorkplace;
window.refreshWorkplaces = refreshWorkplaces;