feat(purchase): 카테고리 테이블 분리 + 동적 로드 + tkuser 관리
- DB: consumable_categories 테이블 생성, ENUM→VARCHAR 변환, 시드 4개 - API: GET/POST/PUT/DEACTIVATE /api/consumable-categories - 프론트: 3개 JS 하드코딩 CAT_LABELS 제거 → API loadCategories() 동적 로드 - tkuser: 카테고리 관리 섹션 추가, select 옵션 동적 생성 - 별칭 시드 SQL (INSERT IGNORE 기반) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1774,10 +1774,6 @@
|
||||
<input type="text" id="consumableSearchTkuser" class="input-field flex-1 min-w-[160px] px-3 py-1.5 rounded-lg text-sm" placeholder="품명/메이커 검색">
|
||||
<select id="consumableFilterCategoryTkuser" class="input-field px-2 py-1.5 rounded-lg text-sm">
|
||||
<option value="">전체 분류</option>
|
||||
<option value="consumable">소모품</option>
|
||||
<option value="safety">안전용품</option>
|
||||
<option value="repair">수선비</option>
|
||||
<option value="equipment">설비</option>
|
||||
</select>
|
||||
<select id="consumableFilterActiveTkuser" class="input-field px-2 py-1.5 rounded-lg text-sm">
|
||||
<option value="true">활성</option>
|
||||
@@ -1789,6 +1785,14 @@
|
||||
<p class="text-gray-400 text-center py-4 text-sm">탭을 선택하면 데이터를 불러옵니다.</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 카테고리 관리 (admin) -->
|
||||
<div id="categoryManageSection" class="hidden bg-white rounded-xl shadow-sm p-5 mt-4">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<h3 class="text-sm font-semibold text-gray-700"><i class="fas fa-tags text-teal-500 mr-1"></i>카테고리 관리</h3>
|
||||
<button onclick="addCategory()" class="px-2 py-1 bg-teal-600 text-white rounded text-xs hover:bg-teal-700"><i class="fas fa-plus mr-1"></i>추가</button>
|
||||
</div>
|
||||
<div id="categoryList" class="space-y-2"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ============ 알림 수신자 탭 ============ -->
|
||||
|
||||
@@ -2,26 +2,71 @@
|
||||
let consumablesLoaded = false;
|
||||
let consumablesList = [];
|
||||
|
||||
const CONSUMABLE_CATEGORIES = {
|
||||
consumable: '소모품',
|
||||
safety: '안전용품',
|
||||
repair: '수선비',
|
||||
equipment: '설비'
|
||||
};
|
||||
const CONSUMABLE_CAT_COLORS = {
|
||||
consumable: 'bg-blue-50 text-blue-600',
|
||||
safety: 'bg-green-50 text-green-600',
|
||||
repair: 'bg-amber-50 text-amber-600',
|
||||
equipment: 'bg-purple-50 text-purple-600'
|
||||
};
|
||||
// 카테고리 — system1-factory API에서 동적 로드 (fallback: 기본 4개)
|
||||
const TKFB_API = location.hostname.includes('technicalkorea.net')
|
||||
? 'https://tkfb.technicalkorea.net/api'
|
||||
: location.protocol + '//' + location.hostname + ':30005/api';
|
||||
let _consumableCategories = null;
|
||||
const DEFAULT_CATEGORIES = [
|
||||
{ category_code: 'consumable', category_name: '소모품', icon: 'fa-box', color_bg: '#dbeafe', color_fg: '#1e40af' },
|
||||
{ category_code: 'safety', category_name: '안전용품', icon: 'fa-hard-hat', color_bg: '#dcfce7', color_fg: '#166534' },
|
||||
{ category_code: 'repair', category_name: '수선비', icon: 'fa-wrench', color_bg: '#fef3c7', color_fg: '#92400e' },
|
||||
{ category_code: 'equipment', category_name: '설비', icon: 'fa-cogs', color_bg: '#f3e8ff', color_fg: '#7e22ce' }
|
||||
];
|
||||
|
||||
async function loadConsumableCategories() {
|
||||
if (_consumableCategories) return _consumableCategories;
|
||||
try {
|
||||
const token = getToken();
|
||||
const res = await fetch(TKFB_API + '/consumable-categories', {
|
||||
headers: { 'Authorization': `Bearer ${token}` }
|
||||
});
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
_consumableCategories = data.data || DEFAULT_CATEGORIES;
|
||||
} else {
|
||||
_consumableCategories = DEFAULT_CATEGORIES;
|
||||
}
|
||||
} catch (e) {
|
||||
_consumableCategories = DEFAULT_CATEGORIES;
|
||||
}
|
||||
return _consumableCategories;
|
||||
}
|
||||
function getCatLabel(code) { return (_consumableCategories || DEFAULT_CATEGORIES).find(c => c.category_code === code)?.category_name || code; }
|
||||
function getCatColorClass(code) {
|
||||
const c = (_consumableCategories || DEFAULT_CATEGORIES).find(x => x.category_code === code);
|
||||
if (!c) return 'bg-gray-50 text-gray-600';
|
||||
// 인라인 스타일 반환
|
||||
return '';
|
||||
}
|
||||
function getCatStyle(code) {
|
||||
const c = (_consumableCategories || DEFAULT_CATEGORIES).find(x => x.category_code === code);
|
||||
return c ? `background:${c.color_bg};color:${c.color_fg}` : 'background:#f3f4f6;color:#6b7280';
|
||||
}
|
||||
|
||||
async function loadConsumablesTab() {
|
||||
if (consumablesLoaded) return;
|
||||
consumablesLoaded = true;
|
||||
await loadConsumableCategories();
|
||||
if (currentUser && ['admin', 'system'].includes(currentUser.role)) {
|
||||
document.getElementById('btnAddConsumableTkuser')?.classList.remove('hidden');
|
||||
document.getElementById('categoryManageSection')?.classList.remove('hidden');
|
||||
}
|
||||
populateCategorySelects();
|
||||
await loadConsumablesList();
|
||||
loadCategoryManagement();
|
||||
}
|
||||
|
||||
// 카테고리 select 옵션 동적 생성
|
||||
function populateCategorySelects() {
|
||||
const cats = _consumableCategories || DEFAULT_CATEGORIES;
|
||||
const optionsHtml = cats.map(c => `<option value="${c.category_code}">${escHtml(c.category_name)}</option>`).join('');
|
||||
['consumableFilterCategoryTkuser', 'newConsumableCategoryTkuser', 'editConsumableCategoryTkuser'].forEach(id => {
|
||||
const el = document.getElementById(id);
|
||||
if (!el) return;
|
||||
const firstOpt = el.querySelector('option:first-child');
|
||||
el.innerHTML = (firstOpt ? firstOpt.outerHTML : '<option value="">전체</option>') + optionsHtml;
|
||||
});
|
||||
}
|
||||
|
||||
async function loadConsumablesList() {
|
||||
@@ -50,8 +95,8 @@ function renderConsumablesListTkuser() {
|
||||
const isAdmin = currentUser && ['admin', 'system'].includes(currentUser.role);
|
||||
c.innerHTML = `<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">` +
|
||||
consumablesList.map(item => {
|
||||
const catLabel = CONSUMABLE_CATEGORIES[item.category] || item.category;
|
||||
const catColor = CONSUMABLE_CAT_COLORS[item.category] || 'bg-gray-50 text-gray-600';
|
||||
const catLabel = getCatLabel(item.category);
|
||||
const catStyle = getCatStyle(item.category);
|
||||
const price = item.base_price ? Number(item.base_price).toLocaleString() + '원' : '-';
|
||||
return `<div class="bg-white border rounded-lg p-3 hover:shadow-md transition-shadow">
|
||||
<div class="flex gap-3">
|
||||
@@ -62,7 +107,7 @@ function renderConsumablesListTkuser() {
|
||||
<div class="text-sm font-medium text-gray-800 truncate">${escHtml(item.item_name)}${item.spec ? ' <span class="text-gray-400">[' + escHtml(item.spec) + ']</span>' : ''}</div>
|
||||
<div class="text-xs text-gray-500 mt-0.5">${escHtml(item.maker) || '-'}</div>
|
||||
<div class="flex items-center gap-2 mt-1">
|
||||
<span class="px-1.5 py-0.5 rounded text-xs ${catColor}">${catLabel}</span>
|
||||
<span class="px-1.5 py-0.5 rounded text-xs" style="${catStyle}">${catLabel}</span>
|
||||
<span class="text-xs text-gray-600 font-medium">${price}</span>
|
||||
<span class="text-xs text-gray-400">${escHtml(item.unit) || 'EA'}</span>
|
||||
</div>
|
||||
@@ -190,6 +235,47 @@ async function deactivateConsumableTkuser(id, name) {
|
||||
} catch (e) { showToast(e.message, 'error'); }
|
||||
}
|
||||
|
||||
/* ===== 카테고리 관리 ===== */
|
||||
async function loadCategoryManagement() {
|
||||
const container = document.getElementById('categoryList');
|
||||
if (!container) return;
|
||||
const cats = _consumableCategories || DEFAULT_CATEGORIES;
|
||||
container.innerHTML = cats.map(c => `
|
||||
<div class="flex items-center gap-3 p-2 border rounded-lg">
|
||||
<div class="w-8 h-8 rounded flex items-center justify-center" style="background:${c.color_bg};color:${c.color_fg}">
|
||||
<i class="fas ${c.icon} text-sm"></i>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<span class="text-sm font-medium">${escHtml(c.category_name)}</span>
|
||||
<span class="text-xs text-gray-400 ml-1">(${escHtml(c.category_code)})</span>
|
||||
</div>
|
||||
${c.category_id ? `<button onclick="editCategory(${c.category_id})" class="text-xs text-slate-500 hover:text-slate-700"><i class="fas fa-pen"></i></button>` : ''}
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
async function addCategory() {
|
||||
const code = prompt('카테고리 코드 (영문):');
|
||||
if (!code) return;
|
||||
const name = prompt('표시 이름:');
|
||||
if (!name) return;
|
||||
try {
|
||||
const token = getToken();
|
||||
const res = await fetch(TKFB_API + '/consumable-categories', {
|
||||
method: 'POST',
|
||||
headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ category_code: code, category_name: name })
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok) throw new Error(data.message || '추가 실패');
|
||||
showToast('카테고리가 추가되었습니다');
|
||||
_consumableCategories = null;
|
||||
await loadConsumableCategories();
|
||||
populateCategorySelects();
|
||||
loadCategoryManagement();
|
||||
} catch (e) { showToast(e.message, 'error'); }
|
||||
}
|
||||
|
||||
// 검색/필터 이벤트 + 모달 폼 이벤트
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
let searchTimeout;
|
||||
|
||||
Reference in New Issue
Block a user