- 부적합 API 호출 형식 수정 (카테고리/아이템 추가 시) - 부적합 저장 시 내부 플래그 제거 후 백엔드 전송 - 기본 부적합 객체 구조 수정 (category_id, item_id 추가) - 날씨 API 시간대 수정 (UTC → KST 변환) - 신고 카테고리 관리 페이지 추가 (/pages/admin/issue-categories.html) - 부적합 입력 UI 개선 (대분류→소분류 캐스케이딩 선택) - 저장된 부적합 분리 표시 및 수정/삭제 기능 - 디버깅 로그 추가 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
422 lines
12 KiB
JavaScript
422 lines
12 KiB
JavaScript
/**
|
|
* 신고 카테고리 관리 JavaScript
|
|
*/
|
|
|
|
import { API, getAuthHeaders } from '/js/api-config.js';
|
|
|
|
let currentType = 'nonconformity';
|
|
let categories = [];
|
|
let items = [];
|
|
|
|
// 초기화
|
|
document.addEventListener('DOMContentLoaded', async () => {
|
|
await loadCategories();
|
|
});
|
|
|
|
/**
|
|
* 유형 탭 전환
|
|
*/
|
|
window.switchType = async function(type) {
|
|
currentType = type;
|
|
|
|
// 탭 상태 업데이트
|
|
document.querySelectorAll('.type-tab').forEach(tab => {
|
|
tab.classList.toggle('active', tab.dataset.type === type);
|
|
});
|
|
|
|
await loadCategories();
|
|
};
|
|
|
|
/**
|
|
* 카테고리 로드
|
|
*/
|
|
async function loadCategories() {
|
|
const container = document.getElementById('categoryList');
|
|
container.innerHTML = '<div class="empty-state">카테고리를 불러오는 중...</div>';
|
|
|
|
try {
|
|
const response = await fetch(`${API}/work-issues/categories/type/${currentType}`, {
|
|
headers: getAuthHeaders()
|
|
});
|
|
|
|
if (!response.ok) throw new Error('카테고리 조회 실패');
|
|
|
|
const data = await response.json();
|
|
|
|
if (data.success && data.data) {
|
|
categories = data.data;
|
|
|
|
// 항목도 로드
|
|
const itemsResponse = await fetch(`${API}/work-issues/items`, {
|
|
headers: getAuthHeaders()
|
|
});
|
|
|
|
if (itemsResponse.ok) {
|
|
const itemsData = await itemsResponse.json();
|
|
if (itemsData.success) {
|
|
items = itemsData.data || [];
|
|
}
|
|
}
|
|
|
|
renderCategories();
|
|
} else {
|
|
container.innerHTML = '<div class="empty-state">카테고리가 없습니다.</div>';
|
|
}
|
|
} catch (error) {
|
|
console.error('카테고리 로드 실패:', error);
|
|
container.innerHTML = '<div class="empty-state">카테고리를 불러오지 못했습니다.</div>';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 카테고리 렌더링
|
|
*/
|
|
function renderCategories() {
|
|
const container = document.getElementById('categoryList');
|
|
|
|
if (categories.length === 0) {
|
|
container.innerHTML = '<div class="empty-state">등록된 카테고리가 없습니다.</div>';
|
|
return;
|
|
}
|
|
|
|
const severityLabel = {
|
|
low: '낮음',
|
|
medium: '보통',
|
|
high: '높음',
|
|
critical: '심각'
|
|
};
|
|
|
|
container.innerHTML = categories.map(cat => {
|
|
const catItems = items.filter(item => item.category_id === cat.category_id);
|
|
|
|
return `
|
|
<div class="category-section" data-category-id="${cat.category_id}">
|
|
<div class="category-header" onclick="toggleCategory(${cat.category_id})">
|
|
<div class="category-name">${cat.category_name}</div>
|
|
<div class="category-badge">
|
|
<span class="severity-badge ${cat.severity || 'medium'}">${severityLabel[cat.severity] || '보통'}</span>
|
|
<span class="item-count">${catItems.length}개 항목</span>
|
|
<button class="btn btn-secondary btn-sm" onclick="event.stopPropagation(); openCategoryModal(${cat.category_id})">수정</button>
|
|
</div>
|
|
</div>
|
|
<div class="category-items">
|
|
<div class="item-list">
|
|
${catItems.length > 0 ? catItems.map(item => `
|
|
<div class="item-card">
|
|
<div class="item-info">
|
|
<div class="item-name">${item.item_name}</div>
|
|
${item.description ? `<div class="item-desc">${item.description}</div>` : ''}
|
|
</div>
|
|
<div class="item-actions">
|
|
<span class="severity-badge ${item.severity || 'medium'}">${severityLabel[item.severity] || '보통'}</span>
|
|
<button class="btn btn-secondary btn-sm" onclick="openItemModal(${cat.category_id}, ${item.item_id})">수정</button>
|
|
</div>
|
|
</div>
|
|
`).join('') : '<div class="empty-state" style="padding: 24px;">등록된 항목이 없습니다.</div>'}
|
|
</div>
|
|
<div class="add-item-form">
|
|
<input type="text" id="newItemName_${cat.category_id}" placeholder="새 항목 이름">
|
|
<button class="btn btn-primary btn-sm" onclick="quickAddItem(${cat.category_id})">추가</button>
|
|
<button class="btn btn-secondary btn-sm" onclick="openItemModal(${cat.category_id})">상세 추가</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}).join('');
|
|
}
|
|
|
|
/**
|
|
* 카테고리 토글
|
|
*/
|
|
window.toggleCategory = function(categoryId) {
|
|
const section = document.querySelector(`.category-section[data-category-id="${categoryId}"]`);
|
|
if (section) {
|
|
section.classList.toggle('expanded');
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 카테고리 모달 열기
|
|
*/
|
|
window.openCategoryModal = function(categoryId = null) {
|
|
const modal = document.getElementById('categoryModal');
|
|
const title = document.getElementById('categoryModalTitle');
|
|
const deleteBtn = document.getElementById('deleteCategoryBtn');
|
|
|
|
document.getElementById('categoryId').value = '';
|
|
document.getElementById('categoryName').value = '';
|
|
document.getElementById('categoryDescription').value = '';
|
|
document.getElementById('categorySeverity').value = 'medium';
|
|
|
|
if (categoryId) {
|
|
const category = categories.find(c => c.category_id === categoryId);
|
|
if (category) {
|
|
title.textContent = '카테고리 수정';
|
|
document.getElementById('categoryId').value = category.category_id;
|
|
document.getElementById('categoryName').value = category.category_name;
|
|
document.getElementById('categoryDescription').value = category.description || '';
|
|
document.getElementById('categorySeverity').value = category.severity || 'medium';
|
|
deleteBtn.style.display = 'block';
|
|
}
|
|
} else {
|
|
title.textContent = '새 카테고리';
|
|
deleteBtn.style.display = 'none';
|
|
}
|
|
|
|
modal.style.display = 'flex';
|
|
};
|
|
|
|
/**
|
|
* 카테고리 모달 닫기
|
|
*/
|
|
window.closeCategoryModal = function() {
|
|
document.getElementById('categoryModal').style.display = 'none';
|
|
};
|
|
|
|
/**
|
|
* 카테고리 저장
|
|
*/
|
|
window.saveCategory = async function() {
|
|
const categoryId = document.getElementById('categoryId').value;
|
|
const name = document.getElementById('categoryName').value.trim();
|
|
const description = document.getElementById('categoryDescription').value.trim();
|
|
const severity = document.getElementById('categorySeverity').value;
|
|
|
|
if (!name) {
|
|
alert('카테고리 이름을 입력하세요.');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const url = categoryId
|
|
? `${API}/work-issues/categories/${categoryId}`
|
|
: `${API}/work-issues/categories`;
|
|
|
|
const response = await fetch(url, {
|
|
method: categoryId ? 'PUT' : 'POST',
|
|
headers: {
|
|
...getAuthHeaders(),
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
category_name: name,
|
|
category_type: currentType,
|
|
description,
|
|
severity
|
|
})
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (response.ok && data.success) {
|
|
alert(categoryId ? '카테고리가 수정되었습니다.' : '카테고리가 추가되었습니다.');
|
|
closeCategoryModal();
|
|
await loadCategories();
|
|
} else {
|
|
throw new Error(data.error || '저장 실패');
|
|
}
|
|
} catch (error) {
|
|
console.error('카테고리 저장 실패:', error);
|
|
alert('카테고리 저장에 실패했습니다: ' + error.message);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 카테고리 삭제
|
|
*/
|
|
window.deleteCategory = async function() {
|
|
const categoryId = document.getElementById('categoryId').value;
|
|
|
|
if (!categoryId) return;
|
|
|
|
const catItems = items.filter(item => item.category_id == categoryId);
|
|
if (catItems.length > 0) {
|
|
alert(`이 카테고리에 ${catItems.length}개의 항목이 있습니다. 먼저 항목을 삭제하세요.`);
|
|
return;
|
|
}
|
|
|
|
if (!confirm('이 카테고리를 삭제하시겠습니까?')) return;
|
|
|
|
try {
|
|
const response = await fetch(`${API}/work-issues/categories/${categoryId}`, {
|
|
method: 'DELETE',
|
|
headers: getAuthHeaders()
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (response.ok && data.success) {
|
|
alert('카테고리가 삭제되었습니다.');
|
|
closeCategoryModal();
|
|
await loadCategories();
|
|
} else {
|
|
throw new Error(data.error || '삭제 실패');
|
|
}
|
|
} catch (error) {
|
|
console.error('카테고리 삭제 실패:', error);
|
|
alert('카테고리 삭제에 실패했습니다: ' + error.message);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 항목 모달 열기
|
|
*/
|
|
window.openItemModal = function(categoryId, itemId = null) {
|
|
const modal = document.getElementById('itemModal');
|
|
const title = document.getElementById('itemModalTitle');
|
|
const deleteBtn = document.getElementById('deleteItemBtn');
|
|
|
|
document.getElementById('itemId').value = '';
|
|
document.getElementById('itemCategoryId').value = categoryId;
|
|
document.getElementById('itemName').value = '';
|
|
document.getElementById('itemDescription').value = '';
|
|
document.getElementById('itemSeverity').value = 'medium';
|
|
|
|
if (itemId) {
|
|
const item = items.find(i => i.item_id === itemId);
|
|
if (item) {
|
|
title.textContent = '항목 수정';
|
|
document.getElementById('itemId').value = item.item_id;
|
|
document.getElementById('itemName').value = item.item_name;
|
|
document.getElementById('itemDescription').value = item.description || '';
|
|
document.getElementById('itemSeverity').value = item.severity || 'medium';
|
|
deleteBtn.style.display = 'block';
|
|
}
|
|
} else {
|
|
const category = categories.find(c => c.category_id === categoryId);
|
|
title.textContent = `새 항목 (${category?.category_name || ''})`;
|
|
deleteBtn.style.display = 'none';
|
|
}
|
|
|
|
modal.style.display = 'flex';
|
|
};
|
|
|
|
/**
|
|
* 항목 모달 닫기
|
|
*/
|
|
window.closeItemModal = function() {
|
|
document.getElementById('itemModal').style.display = 'none';
|
|
};
|
|
|
|
/**
|
|
* 항목 저장
|
|
*/
|
|
window.saveItem = async function() {
|
|
const itemId = document.getElementById('itemId').value;
|
|
const categoryId = document.getElementById('itemCategoryId').value;
|
|
const name = document.getElementById('itemName').value.trim();
|
|
const description = document.getElementById('itemDescription').value.trim();
|
|
const severity = document.getElementById('itemSeverity').value;
|
|
|
|
if (!name) {
|
|
alert('항목 이름을 입력하세요.');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const url = itemId
|
|
? `${API}/work-issues/items/${itemId}`
|
|
: `${API}/work-issues/items`;
|
|
|
|
const response = await fetch(url, {
|
|
method: itemId ? 'PUT' : 'POST',
|
|
headers: {
|
|
...getAuthHeaders(),
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
category_id: categoryId,
|
|
item_name: name,
|
|
description,
|
|
severity
|
|
})
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (response.ok && data.success) {
|
|
alert(itemId ? '항목이 수정되었습니다.' : '항목이 추가되었습니다.');
|
|
closeItemModal();
|
|
await loadCategories();
|
|
} else {
|
|
throw new Error(data.error || '저장 실패');
|
|
}
|
|
} catch (error) {
|
|
console.error('항목 저장 실패:', error);
|
|
alert('항목 저장에 실패했습니다: ' + error.message);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 항목 삭제
|
|
*/
|
|
window.deleteItem = async function() {
|
|
const itemId = document.getElementById('itemId').value;
|
|
|
|
if (!itemId) return;
|
|
if (!confirm('이 항목을 삭제하시겠습니까?')) return;
|
|
|
|
try {
|
|
const response = await fetch(`${API}/work-issues/items/${itemId}`, {
|
|
method: 'DELETE',
|
|
headers: getAuthHeaders()
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (response.ok && data.success) {
|
|
alert('항목이 삭제되었습니다.');
|
|
closeItemModal();
|
|
await loadCategories();
|
|
} else {
|
|
throw new Error(data.error || '삭제 실패');
|
|
}
|
|
} catch (error) {
|
|
console.error('항목 삭제 실패:', error);
|
|
alert('항목 삭제에 실패했습니다: ' + error.message);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 빠른 항목 추가
|
|
*/
|
|
window.quickAddItem = async function(categoryId) {
|
|
const input = document.getElementById(`newItemName_${categoryId}`);
|
|
const name = input.value.trim();
|
|
|
|
if (!name) {
|
|
alert('항목 이름을 입력하세요.');
|
|
input.focus();
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(`${API}/work-issues/items`, {
|
|
method: 'POST',
|
|
headers: {
|
|
...getAuthHeaders(),
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
category_id: categoryId,
|
|
item_name: name,
|
|
severity: 'medium'
|
|
})
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (response.ok && data.success) {
|
|
input.value = '';
|
|
await loadCategories();
|
|
// 카테고리 펼침 유지
|
|
toggleCategory(categoryId);
|
|
} else {
|
|
throw new Error(data.error || '추가 실패');
|
|
}
|
|
} catch (error) {
|
|
console.error('항목 추가 실패:', error);
|
|
alert('항목 추가에 실패했습니다: ' + error.message);
|
|
}
|
|
};
|