Files
TK-FB-Project/web-ui/js/task-management.js
Hyungi Ahn 1fc9dff69f feat: 작업 관리 페이지에 공정 관리 기능 추가
## 추가 기능
- 공정 추가/수정/삭제 기능 구현
- 공정 관리 모달 UI 추가
- 공정 탭에 편집 버튼 추가 (✏️)

## UI 변경
- 상단에 "공정 추가" 버튼 추가
- 공정 모달: 공정명, 카테고리, 설명 입력 필드
- 각 공정 탭에 편집 아이콘 표시

## JavaScript 함수
- openWorkTypeModal(): 공정 추가 모달 열기
- editWorkType(workTypeId): 공정 수정 모달 열기
- saveWorkType(): 공정 저장 (POST/PUT)
- deleteWorkType(): 공정 삭제 (연결된 작업 확인)
- closeWorkTypeModal(): 모달 닫기

## 검증 로직
- 연결된 작업이 있는 공정은 삭제 불가
- 필수 필드(공정명) 검증

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

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

507 lines
16 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 {
// 작업 유형(공정) 목록 조회
const response = await window.apiCall('/daily-work-reports/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})"
style="position: relative; padding-right: 3rem;">
<span class="tab-icon">🔧</span>
${workType.name} (${count})
<span onclick="event.stopPropagation(); editWorkType(${workType.id});"
style="position: absolute; right: 0.5rem; padding: 0.25rem 0.5rem; opacity: 0.7; cursor: pointer; font-size: 0.75rem;"
title="공정 수정">
✏️
</span>
</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);
}
// ==================== 공정 관리 ====================
let currentEditingWorkType = null;
// 공정 모달 열기 (신규)
function openWorkTypeModal() {
currentEditingWorkType = null;
document.getElementById('workTypeModalTitle').textContent = '공정 추가';
document.getElementById('workTypeId').value = '';
document.getElementById('workTypeName').value = '';
document.getElementById('workTypeCategory').value = '';
document.getElementById('workTypeDescription').value = '';
document.getElementById('deleteWorkTypeBtn').style.display = 'none';
document.getElementById('workTypeModal').style.display = 'flex';
document.body.style.overflow = 'hidden';
}
window.openWorkTypeModal = openWorkTypeModal;
// 공정 수정 모달 열기
async function editWorkType(workTypeId) {
try {
const workType = workTypes.find(wt => wt.id === workTypeId);
if (!workType) {
showToast('공정 정보를 찾을 수 없습니다.', 'error');
return;
}
currentEditingWorkType = workType;
document.getElementById('workTypeModalTitle').textContent = '공정 수정';
document.getElementById('workTypeId').value = workType.id;
document.getElementById('workTypeName').value = workType.name || '';
document.getElementById('workTypeCategory').value = workType.category || '';
document.getElementById('workTypeDescription').value = workType.description || '';
document.getElementById('deleteWorkTypeBtn').style.display = 'block';
document.getElementById('workTypeModal').style.display = 'flex';
document.body.style.overflow = 'hidden';
} catch (error) {
console.error('❌ 공정 조회 오류:', error);
showToast('공정 정보를 불러올 수 없습니다.', 'error');
}
}
window.editWorkType = editWorkType;
// 공정 모달 닫기
function closeWorkTypeModal() {
document.getElementById('workTypeModal').style.display = 'none';
document.body.style.overflow = 'auto';
currentEditingWorkType = null;
}
window.closeWorkTypeModal = closeWorkTypeModal;
// 공정 저장
async function saveWorkType() {
const workTypeId = document.getElementById('workTypeId').value;
const workTypeData = {
name: document.getElementById('workTypeName').value.trim(),
category: document.getElementById('workTypeCategory').value.trim() || null,
description: document.getElementById('workTypeDescription').value.trim() || null
};
if (!workTypeData.name) {
showToast('공정명을 입력해주세요.', 'error');
return;
}
try {
let response;
if (workTypeId) {
// 수정
response = await window.apiCall(`/daily-work-reports/work-types/${workTypeId}`, 'PUT', workTypeData);
} else {
// 신규
response = await window.apiCall('/daily-work-reports/work-types', 'POST', workTypeData);
}
if (response && response.success) {
showToast(workTypeId ? '공정이 수정되었습니다.' : '공정이 추가되었습니다.', 'success');
closeWorkTypeModal();
await loadAllData();
} else {
throw new Error(response.message || '저장에 실패했습니다.');
}
} catch (error) {
console.error('❌ 공정 저장 오류:', error);
showToast('공정 저장 중 오류가 발생했습니다.', 'error');
}
}
window.saveWorkType = saveWorkType;
// 공정 삭제
async function deleteWorkType() {
if (!currentEditingWorkType) return;
// 이 공정에 속한 작업이 있는지 확인
const relatedTasks = tasks.filter(t => t.work_type_id === currentEditingWorkType.id);
if (relatedTasks.length > 0) {
showToast(`이 공정에 ${relatedTasks.length}개의 작업이 연결되어 있어 삭제할 수 없습니다.`, 'error');
return;
}
if (!confirm(`"${currentEditingWorkType.name}" 공정을 삭제하시겠습니까?`)) {
return;
}
try {
const response = await window.apiCall(`/daily-work-reports/work-types/${currentEditingWorkType.id}`, 'DELETE');
if (response && response.success) {
showToast('공정이 삭제되었습니다.', 'success');
closeWorkTypeModal();
await loadAllData();
} else {
throw new Error(response.message || '삭제에 실패했습니다.');
}
} catch (error) {
console.error('❌ 공정 삭제 오류:', error);
showToast('공정 삭제 중 오류가 발생했습니다.', 'error');
}
}
window.deleteWorkType = deleteWorkType;