## Backend Changes - Create tasks table with work_type_id FK to work_types - Add taskModel, taskController, taskRoutes for task CRUD - Update tbmModel to support work_type_id and task_id - Add migrations for tasks table and TBM integration ## Frontend Changes - Create task management admin page (tasks.html, task-management.js) - Update TBM modal to include work type (공정) and task (작업) selection - Add cascading dropdown: work type → task selection - Display work type and task info in TBM session cards - Update sidebar navigation in all admin pages ## Database Schema - tasks: task_id, work_type_id, task_name, description, is_active - tbm_sessions: add work_type_id, task_id columns with FKs - Foreign keys maintain referential integrity with work_types and tasks 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
379 lines
11 KiB
JavaScript
379 lines
11 KiB
JavaScript
// task-management.js - 작업 관리 페이지 JavaScript
|
|
|
|
// 전역 변수
|
|
let workTypes = []; // 공정 목록
|
|
let tasks = []; // 작업 목록
|
|
let currentWorkTypeId = ''; // 현재 선택된 공정 ID
|
|
let currentEditingTask = null;
|
|
|
|
// 페이지 초기화
|
|
document.addEventListener('DOMContentLoaded', async () => {
|
|
console.log('📋 작업 관리 페이지 초기화');
|
|
|
|
// API 함수가 로드될 때까지 대기
|
|
let retryCount = 0;
|
|
while (!window.apiCall && retryCount < 50) {
|
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
retryCount++;
|
|
}
|
|
|
|
if (!window.apiCall) {
|
|
showToast('시스템을 초기화할 수 없습니다. 페이지를 새로고침해주세요.', 'error');
|
|
return;
|
|
}
|
|
|
|
await loadAllData();
|
|
});
|
|
|
|
// 전체 데이터 로드
|
|
async function loadAllData() {
|
|
try {
|
|
// 공정 목록 로드 (work_types 조회 - 코드 관리 API 사용)
|
|
await loadWorkTypes();
|
|
// 작업 목록 로드
|
|
await loadTasks();
|
|
} catch (error) {
|
|
console.error('❌ 데이터 로드 오류:', error);
|
|
showToast('데이터를 불러오는 중 오류가 발생했습니다.', 'error');
|
|
}
|
|
}
|
|
window.loadAllData = loadAllData;
|
|
|
|
// 공정 목록 로드
|
|
async function loadWorkTypes() {
|
|
try {
|
|
// 작업 유형(공정) 목록 조회 - codes API 사용 또는 직접 DB 조회
|
|
// 간단하게 하드코딩으로 시작 (나중에 API로 변경 가능)
|
|
const response = await window.apiCall('/tools/work-types');
|
|
|
|
if (response && response.success) {
|
|
workTypes = response.data || [];
|
|
} else {
|
|
workTypes = [];
|
|
}
|
|
|
|
console.log('✅ 공정 목록 로드:', workTypes.length + '개');
|
|
renderWorkTypeTabs();
|
|
populateWorkTypeSelect();
|
|
} catch (error) {
|
|
console.error('❌ 공정 목록 조회 오류:', error);
|
|
// API 오류 시에도 빈 배열로 처리
|
|
workTypes = [];
|
|
renderWorkTypeTabs();
|
|
}
|
|
}
|
|
|
|
// 작업 목록 로드
|
|
async function loadTasks() {
|
|
try {
|
|
const response = await window.apiCall('/tasks');
|
|
|
|
if (response && response.success) {
|
|
tasks = response.data || [];
|
|
console.log('✅ 작업 목록 로드:', tasks.length + '개');
|
|
} else {
|
|
tasks = [];
|
|
}
|
|
|
|
renderTasks();
|
|
updateStatistics();
|
|
} catch (error) {
|
|
console.error('❌ 작업 목록 조회 오류:', error);
|
|
showToast('작업 목록을 불러오는 중 오류가 발생했습니다.', 'error');
|
|
tasks = [];
|
|
renderTasks();
|
|
}
|
|
}
|
|
|
|
// 공정 탭 렌더링
|
|
function renderWorkTypeTabs() {
|
|
const tabsContainer = document.getElementById('workTypeTabs');
|
|
|
|
let tabsHtml = `
|
|
<button class="tab-btn ${currentWorkTypeId === '' ? 'active' : ''}"
|
|
data-work-type="" onclick="switchWorkType('')">
|
|
<span class="tab-icon">📋</span>
|
|
전체 (${tasks.length})
|
|
</button>
|
|
`;
|
|
|
|
workTypes.forEach(workType => {
|
|
const count = tasks.filter(t => t.work_type_id === workType.id).length;
|
|
const isActive = currentWorkTypeId === workType.id;
|
|
|
|
tabsHtml += `
|
|
<button class="tab-btn ${isActive ? 'active' : ''}"
|
|
data-work-type="${workType.id}"
|
|
onclick="switchWorkType(${workType.id})">
|
|
<span class="tab-icon">🔧</span>
|
|
${workType.name} (${count})
|
|
</button>
|
|
`;
|
|
});
|
|
|
|
tabsContainer.innerHTML = tabsHtml;
|
|
}
|
|
|
|
// 공정 전환
|
|
function switchWorkType(workTypeId) {
|
|
currentWorkTypeId = workTypeId === '' ? '' : parseInt(workTypeId);
|
|
renderWorkTypeTabs();
|
|
renderTasks();
|
|
updateStatistics();
|
|
}
|
|
window.switchWorkType = switchWorkType;
|
|
|
|
// 작업 목록 렌더링
|
|
function renderTasks() {
|
|
const grid = document.getElementById('taskGrid');
|
|
|
|
// 현재 선택된 공정으로 필터링
|
|
let filteredTasks = tasks;
|
|
if (currentWorkTypeId !== '') {
|
|
filteredTasks = tasks.filter(t => t.work_type_id === currentWorkTypeId);
|
|
}
|
|
|
|
if (filteredTasks.length === 0) {
|
|
grid.innerHTML = `
|
|
<div class="empty-state" style="grid-column: 1 / -1;">
|
|
<div class="empty-icon">📋</div>
|
|
<h3>등록된 작업이 없습니다</h3>
|
|
<p>"작업 추가" 버튼을 눌러 새로운 작업을 등록하세요</p>
|
|
</div>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
grid.innerHTML = filteredTasks.map(task => createTaskCard(task)).join('');
|
|
}
|
|
|
|
// 작업 카드 생성
|
|
function createTaskCard(task) {
|
|
const statusBadge = task.is_active
|
|
? '<span class="badge" style="background: #dcfce7; color: #166534;">활성</span>'
|
|
: '<span class="badge" style="background: #f3f4f6; color: #6b7280;">비활성</span>';
|
|
|
|
return `
|
|
<div class="code-card" onclick="editTask(${task.task_id})">
|
|
<div class="code-card-header">
|
|
<h3 class="code-name">${task.task_name}</h3>
|
|
${statusBadge}
|
|
</div>
|
|
|
|
<div class="code-info">
|
|
<div class="info-item">
|
|
<span class="info-label">소속 공정</span>
|
|
<span class="info-value">${task.work_type_name || '-'}</span>
|
|
</div>
|
|
${task.category ? `
|
|
<div class="info-item">
|
|
<span class="info-label">카테고리</span>
|
|
<span class="info-value">${task.category}</span>
|
|
</div>
|
|
` : ''}
|
|
</div>
|
|
|
|
${task.description ? `
|
|
<div class="code-description">
|
|
${task.description}
|
|
</div>
|
|
` : ''}
|
|
|
|
<div class="code-meta">
|
|
<span>등록: ${formatDate(task.created_at)}</span>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// 통계 업데이트
|
|
function updateStatistics() {
|
|
let filteredTasks = tasks;
|
|
if (currentWorkTypeId !== '') {
|
|
filteredTasks = tasks.filter(t => t.work_type_id === currentWorkTypeId);
|
|
}
|
|
|
|
const activeCount = filteredTasks.filter(t => t.is_active).length;
|
|
|
|
document.getElementById('totalCount').textContent = filteredTasks.length;
|
|
document.getElementById('activeCount').textContent = activeCount;
|
|
}
|
|
|
|
// 새로고침
|
|
function refreshTasks() {
|
|
loadAllData();
|
|
showToast('데이터를 새로고침했습니다.', 'success');
|
|
}
|
|
window.refreshTasks = refreshTasks;
|
|
|
|
// ==================== 작업 모달 ====================
|
|
|
|
// 작업 모달 열기 (신규)
|
|
function openTaskModal() {
|
|
currentEditingTask = null;
|
|
document.getElementById('taskModalTitle').textContent = '작업 추가';
|
|
document.getElementById('taskForm').reset();
|
|
document.getElementById('taskId').value = '';
|
|
document.getElementById('taskIsActive').checked = true;
|
|
|
|
// 공정 선택 드롭다운 채우기
|
|
populateWorkTypeSelect();
|
|
|
|
// 현재 선택된 공정이 있으면 자동 선택
|
|
if (currentWorkTypeId !== '') {
|
|
document.getElementById('taskWorkTypeId').value = currentWorkTypeId;
|
|
}
|
|
|
|
document.getElementById('deleteTaskBtn').style.display = 'none';
|
|
document.getElementById('taskModal').style.display = 'flex';
|
|
document.body.style.overflow = 'hidden';
|
|
}
|
|
window.openTaskModal = openTaskModal;
|
|
|
|
// 작업 편집
|
|
async function editTask(taskId) {
|
|
try {
|
|
const response = await window.apiCall(`/tasks/${taskId}`);
|
|
|
|
if (response && response.success) {
|
|
currentEditingTask = response.data;
|
|
|
|
document.getElementById('taskModalTitle').textContent = '작업 수정';
|
|
document.getElementById('taskId').value = currentEditingTask.task_id;
|
|
document.getElementById('taskWorkTypeId').value = currentEditingTask.work_type_id || '';
|
|
document.getElementById('taskName').value = currentEditingTask.task_name;
|
|
document.getElementById('taskDescription').value = currentEditingTask.description || '';
|
|
document.getElementById('taskIsActive').checked = currentEditingTask.is_active;
|
|
|
|
document.getElementById('deleteTaskBtn').style.display = 'block';
|
|
document.getElementById('taskModal').style.display = 'flex';
|
|
document.body.style.overflow = 'hidden';
|
|
}
|
|
} catch (error) {
|
|
console.error('❌ 작업 조회 오류:', error);
|
|
showToast('작업 정보를 불러올 수 없습니다.', 'error');
|
|
}
|
|
}
|
|
window.editTask = editTask;
|
|
|
|
// 작업 모달 닫기
|
|
function closeTaskModal() {
|
|
document.getElementById('taskModal').style.display = 'none';
|
|
document.body.style.overflow = 'auto';
|
|
currentEditingTask = null;
|
|
}
|
|
window.closeTaskModal = closeTaskModal;
|
|
|
|
// 공정 선택 드롭다운 채우기
|
|
function populateWorkTypeSelect() {
|
|
const select = document.getElementById('taskWorkTypeId');
|
|
|
|
select.innerHTML = '<option value="">공정 선택...</option>' +
|
|
workTypes.map(wt => `
|
|
<option value="${wt.id}">${wt.name}${wt.category ? ' (' + wt.category + ')' : ''}</option>
|
|
`).join('');
|
|
}
|
|
|
|
// 작업 저장
|
|
async function saveTask() {
|
|
const taskId = document.getElementById('taskId').value;
|
|
const taskData = {
|
|
work_type_id: parseInt(document.getElementById('taskWorkTypeId').value) || null,
|
|
task_name: document.getElementById('taskName').value.trim(),
|
|
description: document.getElementById('taskDescription').value.trim() || null,
|
|
is_active: document.getElementById('taskIsActive').checked ? 1 : 0
|
|
};
|
|
|
|
if (!taskData.task_name) {
|
|
showToast('작업명을 입력해주세요.', 'error');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
let response;
|
|
if (taskId) {
|
|
// 수정
|
|
response = await window.apiCall(`/tasks/${taskId}`, 'PUT', taskData);
|
|
} else {
|
|
// 신규
|
|
response = await window.apiCall('/tasks', 'POST', taskData);
|
|
}
|
|
|
|
if (response && response.success) {
|
|
showToast(taskId ? '작업이 수정되었습니다.' : '작업이 추가되었습니다.', 'success');
|
|
closeTaskModal();
|
|
await loadAllData();
|
|
} else {
|
|
throw new Error(response.message || '저장에 실패했습니다.');
|
|
}
|
|
} catch (error) {
|
|
console.error('❌ 작업 저장 오류:', error);
|
|
showToast('작업 저장 중 오류가 발생했습니다.', 'error');
|
|
}
|
|
}
|
|
window.saveTask = saveTask;
|
|
|
|
// 작업 삭제
|
|
async function deleteTask() {
|
|
if (!currentEditingTask) return;
|
|
|
|
if (!confirm(`"${currentEditingTask.task_name}" 작업을 삭제하시겠습니까?`)) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await window.apiCall(`/tasks/${currentEditingTask.task_id}`, 'DELETE');
|
|
|
|
if (response && response.success) {
|
|
showToast('작업이 삭제되었습니다.', 'success');
|
|
closeTaskModal();
|
|
await loadAllData();
|
|
} else {
|
|
throw new Error(response.message || '삭제에 실패했습니다.');
|
|
}
|
|
} catch (error) {
|
|
console.error('❌ 작업 삭제 오류:', error);
|
|
showToast('작업 삭제 중 오류가 발생했습니다.', 'error');
|
|
}
|
|
}
|
|
window.deleteTask = deleteTask;
|
|
|
|
// ==================== 유틸리티 ====================
|
|
|
|
// 날짜 포맷
|
|
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 toast = document.createElement('div');
|
|
toast.className = `toast toast-${type}`;
|
|
toast.textContent = message;
|
|
toast.style.cssText = `
|
|
position: fixed;
|
|
top: 20px;
|
|
right: 20px;
|
|
padding: 1rem 1.5rem;
|
|
background: ${type === 'success' ? '#10b981' : type === 'error' ? '#ef4444' : '#3b82f6'};
|
|
color: white;
|
|
border-radius: 0.5rem;
|
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
z-index: 10000;
|
|
animation: slideIn 0.3s ease-out;
|
|
`;
|
|
|
|
document.body.appendChild(toast);
|
|
|
|
setTimeout(() => {
|
|
toast.style.animation = 'slideOut 0.3s ease-out';
|
|
setTimeout(() => toast.remove(), 300);
|
|
}, 3000);
|
|
}
|