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:
@@ -55,6 +55,7 @@ function setupRoutes(app) {
|
|||||||
const settlementRoutes = require('../routes/settlementRoutes');
|
const settlementRoutes = require('../routes/settlementRoutes');
|
||||||
const itemAliasRoutes = require('../routes/itemAliasRoutes');
|
const itemAliasRoutes = require('../routes/itemAliasRoutes');
|
||||||
const purchaseBatchRoutes = require('../routes/purchaseBatchRoutes');
|
const purchaseBatchRoutes = require('../routes/purchaseBatchRoutes');
|
||||||
|
const consumableCategoryRoutes = require('../routes/consumableCategoryRoutes');
|
||||||
const scheduleRoutes = require('../routes/scheduleRoutes');
|
const scheduleRoutes = require('../routes/scheduleRoutes');
|
||||||
const meetingRoutes = require('../routes/meetingRoutes');
|
const meetingRoutes = require('../routes/meetingRoutes');
|
||||||
const proxyInputRoutes = require('../routes/proxyInputRoutes');
|
const proxyInputRoutes = require('../routes/proxyInputRoutes');
|
||||||
@@ -170,6 +171,7 @@ function setupRoutes(app) {
|
|||||||
app.use('/api/settlements', settlementRoutes); // 월간 정산
|
app.use('/api/settlements', settlementRoutes); // 월간 정산
|
||||||
app.use('/api/item-aliases', itemAliasRoutes); // 품목 별칭
|
app.use('/api/item-aliases', itemAliasRoutes); // 품목 별칭
|
||||||
app.use('/api/purchase-batches', purchaseBatchRoutes); // 구매 그룹
|
app.use('/api/purchase-batches', purchaseBatchRoutes); // 구매 그룹
|
||||||
|
app.use('/api/consumable-categories', consumableCategoryRoutes); // 소모품 카테고리
|
||||||
app.use('/api/schedule', scheduleRoutes); // 공정표
|
app.use('/api/schedule', scheduleRoutes); // 공정표
|
||||||
app.use('/api/meetings', meetingRoutes); // 생산회의록
|
app.use('/api/meetings', meetingRoutes); // 생산회의록
|
||||||
app.use('/api/proxy-input', proxyInputRoutes); // 대리입력 + 일별현황
|
app.use('/api/proxy-input', proxyInputRoutes); // 대리입력 + 일별현황
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
const ConsumableCategoryModel = require('../models/consumableCategoryModel');
|
||||||
|
const logger = require('../utils/logger');
|
||||||
|
|
||||||
|
const ConsumableCategoryController = {
|
||||||
|
getAll: async (req, res) => {
|
||||||
|
try {
|
||||||
|
const activeOnly = req.query.all !== '1';
|
||||||
|
const rows = await ConsumableCategoryModel.getAll(activeOnly);
|
||||||
|
res.json({ success: true, data: rows });
|
||||||
|
} catch (err) {
|
||||||
|
logger.error('ConsumableCategory getAll error:', err);
|
||||||
|
res.status(500).json({ success: false, message: '서버 오류가 발생했습니다.' });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
create: async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { category_code, category_name, icon, color_bg, color_fg, sort_order } = req.body;
|
||||||
|
if (!category_code || !category_name) {
|
||||||
|
return res.status(400).json({ success: false, message: '코드와 이름을 입력해주세요.' });
|
||||||
|
}
|
||||||
|
const cat = await ConsumableCategoryModel.create({
|
||||||
|
categoryCode: category_code, categoryName: category_name,
|
||||||
|
icon, colorBg: color_bg, colorFg: color_fg, sortOrder: sort_order
|
||||||
|
});
|
||||||
|
res.status(201).json({ success: true, data: cat, message: '카테고리가 추가되었습니다.' });
|
||||||
|
} catch (err) {
|
||||||
|
if (err.code === 'ER_DUP_ENTRY') {
|
||||||
|
return res.status(400).json({ success: false, message: '이미 존재하는 코드입니다.' });
|
||||||
|
}
|
||||||
|
logger.error('ConsumableCategory create error:', err);
|
||||||
|
res.status(500).json({ success: false, message: '서버 오류가 발생했습니다.' });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
update: async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { category_name, icon, color_bg, color_fg, sort_order } = req.body;
|
||||||
|
const cat = await ConsumableCategoryModel.update(req.params.id, {
|
||||||
|
categoryName: category_name, icon, colorBg: color_bg, colorFg: color_fg, sortOrder: sort_order
|
||||||
|
});
|
||||||
|
if (!cat) return res.status(404).json({ success: false, message: '카테고리를 찾을 수 없습니다.' });
|
||||||
|
res.json({ success: true, data: cat, message: '카테고리가 수정되었습니다.' });
|
||||||
|
} catch (err) {
|
||||||
|
logger.error('ConsumableCategory update error:', err);
|
||||||
|
res.status(500).json({ success: false, message: '서버 오류가 발생했습니다.' });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
deactivate: async (req, res) => {
|
||||||
|
try {
|
||||||
|
const cat = await ConsumableCategoryModel.deactivate(req.params.id);
|
||||||
|
if (!cat) return res.status(404).json({ success: false, message: '카테고리를 찾을 수 없습니다.' });
|
||||||
|
res.json({ success: true, data: cat, message: '카테고리가 비활성화되었습니다.' });
|
||||||
|
} catch (err) {
|
||||||
|
logger.error('ConsumableCategory deactivate error:', err);
|
||||||
|
res.status(500).json({ success: false, message: '서버 오류가 발생했습니다.' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = ConsumableCategoryController;
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
-- 소모품 카테고리 테이블 분리 (ENUM → 마스터 테이블)
|
||||||
|
|
||||||
|
-- 1단계: 카테고리 마스터 테이블 생성
|
||||||
|
CREATE TABLE IF NOT EXISTS consumable_categories (
|
||||||
|
category_id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
category_code VARCHAR(30) NOT NULL UNIQUE COMMENT '코드 (consumable, safety 등)',
|
||||||
|
category_name VARCHAR(50) NOT NULL COMMENT '표시명',
|
||||||
|
icon VARCHAR(30) DEFAULT 'fa-box' COMMENT 'Font Awesome 아이콘',
|
||||||
|
color_bg VARCHAR(30) DEFAULT '#dbeafe' COMMENT '배경색',
|
||||||
|
color_fg VARCHAR(30) DEFAULT '#1e40af' COMMENT '글자색',
|
||||||
|
sort_order INT DEFAULT 0,
|
||||||
|
is_active TINYINT(1) DEFAULT 1,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 2단계: 기존 4개 시드
|
||||||
|
INSERT IGNORE INTO consumable_categories (category_code, category_name, icon, color_bg, color_fg, sort_order) VALUES
|
||||||
|
('consumable', '소모품', 'fa-box', '#dbeafe', '#1e40af', 1),
|
||||||
|
('safety', '안전용품', 'fa-hard-hat', '#dcfce7', '#166534', 2),
|
||||||
|
('repair', '수선비', 'fa-wrench', '#fef3c7', '#92400e', 3),
|
||||||
|
('equipment', '설비', 'fa-cogs', '#f3e8ff', '#7e22ce', 4);
|
||||||
|
|
||||||
|
-- 3단계: ENUM → VARCHAR 변환
|
||||||
|
ALTER TABLE consumable_items MODIFY COLUMN category VARCHAR(30) DEFAULT 'consumable';
|
||||||
|
ALTER TABLE purchase_requests MODIFY COLUMN custom_category VARCHAR(30) NULL;
|
||||||
|
ALTER TABLE purchase_batches MODIFY COLUMN category VARCHAR(30) NULL;
|
||||||
27
system1-factory/api/db/migrations/20260401_seed_aliases.sql
Normal file
27
system1-factory/api/db/migrations/20260401_seed_aliases.sql
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
-- 소모품 별칭 시드 데이터 (item_name LIKE 매칭, 데이터 없으면 무시)
|
||||||
|
|
||||||
|
-- 장갑류
|
||||||
|
INSERT IGNORE INTO item_aliases (item_id, alias_name)
|
||||||
|
SELECT item_id, '장갑' FROM consumable_items WHERE item_name LIKE '%면장갑%';
|
||||||
|
INSERT IGNORE INTO item_aliases (item_id, alias_name)
|
||||||
|
SELECT item_id, '목장갑' FROM consumable_items WHERE item_name LIKE '%면장갑%';
|
||||||
|
|
||||||
|
-- 테이프류
|
||||||
|
INSERT IGNORE INTO item_aliases (item_id, alias_name)
|
||||||
|
SELECT item_id, '테이프' FROM consumable_items WHERE item_name LIKE '%절연테이프%';
|
||||||
|
INSERT IGNORE INTO item_aliases (item_id, alias_name)
|
||||||
|
SELECT item_id, '전기테이프' FROM consumable_items WHERE item_name LIKE '%절연테이프%';
|
||||||
|
|
||||||
|
-- 연마류
|
||||||
|
INSERT IGNORE INTO item_aliases (item_id, alias_name)
|
||||||
|
SELECT item_id, '사포' FROM consumable_items WHERE item_name LIKE '%연마지%' OR item_name LIKE '%연마석%';
|
||||||
|
|
||||||
|
-- 마스크
|
||||||
|
INSERT IGNORE INTO item_aliases (item_id, alias_name)
|
||||||
|
SELECT item_id, '마스크' FROM consumable_items WHERE item_name LIKE '%방진마스크%' OR item_name LIKE '%방독마스크%';
|
||||||
|
|
||||||
|
-- 안전화
|
||||||
|
INSERT IGNORE INTO item_aliases (item_id, alias_name)
|
||||||
|
SELECT item_id, '작업화' FROM consumable_items WHERE item_name LIKE '%안전화%';
|
||||||
|
INSERT IGNORE INTO item_aliases (item_id, alias_name)
|
||||||
|
SELECT item_id, '신발' FROM consumable_items WHERE item_name LIKE '%안전화%';
|
||||||
47
system1-factory/api/models/consumableCategoryModel.js
Normal file
47
system1-factory/api/models/consumableCategoryModel.js
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
// models/consumableCategoryModel.js
|
||||||
|
const { getDb } = require('../dbPool');
|
||||||
|
|
||||||
|
const ConsumableCategoryModel = {
|
||||||
|
async getAll(activeOnly = true) {
|
||||||
|
const db = await getDb();
|
||||||
|
let sql = 'SELECT * FROM consumable_categories';
|
||||||
|
if (activeOnly) sql += ' WHERE is_active = 1';
|
||||||
|
sql += ' ORDER BY sort_order, category_name';
|
||||||
|
const [rows] = await db.query(sql);
|
||||||
|
return rows;
|
||||||
|
},
|
||||||
|
|
||||||
|
async getById(id) {
|
||||||
|
const db = await getDb();
|
||||||
|
const [rows] = await db.query('SELECT * FROM consumable_categories WHERE category_id = ?', [id]);
|
||||||
|
return rows[0] || null;
|
||||||
|
},
|
||||||
|
|
||||||
|
async create({ categoryCode, categoryName, icon, colorBg, colorFg, sortOrder }) {
|
||||||
|
const db = await getDb();
|
||||||
|
const [result] = await db.query(
|
||||||
|
`INSERT INTO consumable_categories (category_code, category_name, icon, color_bg, color_fg, sort_order)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?)`,
|
||||||
|
[categoryCode, categoryName, icon || 'fa-box', colorBg || '#dbeafe', colorFg || '#1e40af', sortOrder || 0]
|
||||||
|
);
|
||||||
|
return this.getById(result.insertId);
|
||||||
|
},
|
||||||
|
|
||||||
|
async update(id, { categoryName, icon, colorBg, colorFg, sortOrder }) {
|
||||||
|
const db = await getDb();
|
||||||
|
await db.query(
|
||||||
|
`UPDATE consumable_categories SET category_name = ?, icon = ?, color_bg = ?, color_fg = ?, sort_order = ?
|
||||||
|
WHERE category_id = ?`,
|
||||||
|
[categoryName, icon, colorBg, colorFg, sortOrder, id]
|
||||||
|
);
|
||||||
|
return this.getById(id);
|
||||||
|
},
|
||||||
|
|
||||||
|
async deactivate(id) {
|
||||||
|
const db = await getDb();
|
||||||
|
await db.query('UPDATE consumable_categories SET is_active = 0 WHERE category_id = ?', [id]);
|
||||||
|
return this.getById(id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = ConsumableCategoryModel;
|
||||||
13
system1-factory/api/routes/consumableCategoryRoutes.js
Normal file
13
system1-factory/api/routes/consumableCategoryRoutes.js
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
const ctrl = require('../controllers/consumableCategoryController');
|
||||||
|
const { createRequirePage } = require('../../../shared/middleware/pagePermission');
|
||||||
|
const { getDb } = require('../dbPool');
|
||||||
|
const requirePage = createRequirePage(getDb);
|
||||||
|
|
||||||
|
router.get('/', ctrl.getAll);
|
||||||
|
router.post('/', requirePage('factory_purchases'), ctrl.create);
|
||||||
|
router.put('/:id', requirePage('factory_purchases'), ctrl.update);
|
||||||
|
router.put('/:id/deactivate', requirePage('factory_purchases'), ctrl.deactivate);
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
@@ -1,7 +1,23 @@
|
|||||||
/* ===== 구매 분석 페이지 ===== */
|
/* ===== 구매 분석 페이지 ===== */
|
||||||
const CAT_LABELS = { consumable: '소모품', safety: '안전용품', repair: '수선비', equipment: '설비' };
|
// 카테고리 — API 동적 로드
|
||||||
const CAT_ICONS = { consumable: 'fa-box', safety: 'fa-hard-hat', repair: 'fa-wrench', equipment: 'fa-cogs' };
|
let _categories = null;
|
||||||
const CAT_BG = { consumable: 'bg-blue-50 text-blue-700', safety: 'bg-green-50 text-green-700', repair: 'bg-amber-50 text-amber-700', equipment: 'bg-purple-50 text-purple-700' };
|
async function loadCategories() {
|
||||||
|
if (_categories) return _categories;
|
||||||
|
try { const r = await api('/consumable-categories'); _categories = r.data || []; } catch(e) { _categories = []; }
|
||||||
|
return _categories;
|
||||||
|
}
|
||||||
|
function getCatLabel(code) { return (_categories || []).find(c => c.category_code === code)?.category_name || code || '-'; }
|
||||||
|
function getCatIcon(code) { return (_categories || []).find(c => c.category_code === code)?.icon || 'fa-box'; }
|
||||||
|
function getCatBgClass(code) {
|
||||||
|
const c = (_categories || []).find(x => x.category_code === code);
|
||||||
|
if (!c) return 'bg-gray-50 text-gray-700';
|
||||||
|
// Tailwind class 생성 (인라인 스타일 대신)
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
function getCatColors(code) {
|
||||||
|
const c = (_categories || []).find(x => x.category_code === code);
|
||||||
|
return c ? { bg: c.color_bg, fg: c.color_fg } : { bg: '#f3f4f6', fg: '#374151' };
|
||||||
|
}
|
||||||
const STATUS_LABELS = { received: '입고완료', returned: '반품' };
|
const STATUS_LABELS = { received: '입고완료', returned: '반품' };
|
||||||
const STATUS_COLORS = { received: 'badge-teal', returned: 'badge-red' };
|
const STATUS_COLORS = { received: 'badge-teal', returned: 'badge-red' };
|
||||||
|
|
||||||
@@ -69,20 +85,17 @@ async function loadReceivedBasis() {
|
|||||||
/* ===== 렌더링 함수들 ===== */
|
/* ===== 렌더링 함수들 ===== */
|
||||||
function renderCategorySummary(data) {
|
function renderCategorySummary(data) {
|
||||||
const el = document.getElementById('paCategorySummary');
|
const el = document.getElementById('paCategorySummary');
|
||||||
const allCategories = ['consumable', 'safety', 'repair', 'equipment'];
|
const cats = _categories || [];
|
||||||
const dataMap = {};
|
const dataMap = {};
|
||||||
data.forEach(d => { dataMap[d.category] = d; });
|
data.forEach(d => { dataMap[d.category] = d; });
|
||||||
const totalAmount = data.reduce((sum, d) => sum + Number(d.total_amount || 0), 0);
|
const totalAmount = data.reduce((sum, d) => sum + Number(d.total_amount || 0), 0);
|
||||||
|
|
||||||
el.innerHTML = allCategories.map(cat => {
|
el.innerHTML = cats.map(cat => {
|
||||||
const d = dataMap[cat] || { count: 0, total_amount: 0 };
|
const d = dataMap[cat.category_code] || { count: 0, total_amount: 0 };
|
||||||
const label = CAT_LABELS[cat];
|
|
||||||
const icon = CAT_ICONS[cat];
|
|
||||||
const bg = CAT_BG[cat];
|
|
||||||
return `<div class="bg-white rounded-xl shadow-sm p-4">
|
return `<div class="bg-white rounded-xl shadow-sm p-4">
|
||||||
<div class="flex items-center gap-2 mb-2">
|
<div class="flex items-center gap-2 mb-2">
|
||||||
<div class="w-8 h-8 rounded-lg ${bg} flex items-center justify-center"><i class="fas ${icon} text-sm"></i></div>
|
<div class="w-8 h-8 rounded-lg flex items-center justify-center" style="background:${cat.color_bg};color:${cat.color_fg}"><i class="fas ${cat.icon} text-sm"></i></div>
|
||||||
<span class="text-sm font-medium text-gray-700">${label}</span>
|
<span class="text-sm font-medium text-gray-700">${escapeHtml(cat.category_name)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-xl font-bold text-gray-800">${Number(d.total_amount || 0).toLocaleString()}<span class="text-xs font-normal text-gray-400 ml-1">원</span></div>
|
<div class="text-xl font-bold text-gray-800">${Number(d.total_amount || 0).toLocaleString()}<span class="text-xs font-normal text-gray-400 ml-1">원</span></div>
|
||||||
<div class="text-xs text-gray-500 mt-1">${d.count || 0}건</div>
|
<div class="text-xs text-gray-500 mt-1">${d.count || 0}건</div>
|
||||||
@@ -127,8 +140,8 @@ function renderPurchaseList(data) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
tbody.innerHTML = data.map(p => {
|
tbody.innerHTML = data.map(p => {
|
||||||
const catLabel = CAT_LABELS[p.category] || p.category;
|
const catLabel = getCatLabel(p.category);
|
||||||
const catColor = CAT_BG[p.category] || '';
|
const catColor = getCatBgClass(p.category);
|
||||||
const subtotal = (p.quantity || 0) * (p.unit_price || 0);
|
const subtotal = (p.quantity || 0) * (p.unit_price || 0);
|
||||||
const basePrice = Number(p.base_price || 0);
|
const basePrice = Number(p.base_price || 0);
|
||||||
const unitPrice = Number(p.unit_price || 0);
|
const unitPrice = Number(p.unit_price || 0);
|
||||||
@@ -190,8 +203,8 @@ function renderReceivedList(data) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
tbody.innerHTML = data.map(r => {
|
tbody.innerHTML = data.map(r => {
|
||||||
const catLabel = CAT_LABELS[r.category] || r.category || '-';
|
const catLabel = getCatLabel(r.category);
|
||||||
const catColor = CAT_BG[r.category] || '';
|
const catColor = getCatBgClass(r.category);
|
||||||
const statusLabel = STATUS_LABELS[r.status] || r.status;
|
const statusLabel = STATUS_LABELS[r.status] || r.status;
|
||||||
const statusColor = STATUS_COLORS[r.status] || 'badge-gray';
|
const statusColor = STATUS_COLORS[r.status] || 'badge-gray';
|
||||||
return `<tr class="hover:bg-gray-50 ${r.status === 'returned' ? 'bg-red-50' : ''}">
|
return `<tr class="hover:bg-gray-50 ${r.status === 'returned' ? 'bg-red-50' : ''}">
|
||||||
@@ -236,6 +249,7 @@ async function cancelSettlement(vendorId) {
|
|||||||
/* ===== Init ===== */
|
/* ===== Init ===== */
|
||||||
(async function() {
|
(async function() {
|
||||||
if (!await initAuth()) return;
|
if (!await initAuth()) return;
|
||||||
|
await loadCategories();
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
document.getElementById('paMonth').value = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`;
|
document.getElementById('paMonth').value = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`;
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -3,7 +3,14 @@ const TKUSER_BASE_URL = location.hostname.includes('technicalkorea.net')
|
|||||||
? 'https://tkuser.technicalkorea.net'
|
? 'https://tkuser.technicalkorea.net'
|
||||||
: location.protocol + '//' + location.hostname + ':30180';
|
: location.protocol + '//' + location.hostname + ':30180';
|
||||||
|
|
||||||
const CAT_LABELS = { consumable: '소모품', safety: '안전용품', repair: '수선비', equipment: '설비' };
|
// 카테고리 — API 동적 로드
|
||||||
|
let _categories = null;
|
||||||
|
async function loadCategories() {
|
||||||
|
if (_categories) return _categories;
|
||||||
|
try { const r = await api('/consumable-categories'); _categories = r.data || []; } catch(e) { _categories = []; }
|
||||||
|
return _categories;
|
||||||
|
}
|
||||||
|
function getCatLabel(code) { return (_categories || []).find(c => c.category_code === code)?.category_name || code || ''; }
|
||||||
const STATUS_LABELS = { pending: '대기', grouped: '구매진행중', purchased: '구매완료', received: '입고완료', cancelled: '취소', returned: '반품', hold: '보류' };
|
const STATUS_LABELS = { pending: '대기', grouped: '구매진행중', purchased: '구매완료', received: '입고완료', cancelled: '취소', returned: '반품', hold: '보류' };
|
||||||
const STATUS_COLORS = { pending: 'badge-amber', grouped: 'badge-blue', purchased: 'badge-green', received: 'badge-teal', cancelled: 'badge-red', returned: 'badge-red', hold: 'badge-gray' };
|
const STATUS_COLORS = { pending: 'badge-amber', grouped: 'badge-blue', purchased: 'badge-green', received: 'badge-teal', cancelled: 'badge-red', returned: 'badge-red', hold: 'badge-gray' };
|
||||||
const MATCH_LABELS = { exact: '정확', name: '이름', alias: '별칭', spec: '규격', chosung: '초성', chosung_alias: '초성' };
|
const MATCH_LABELS = { exact: '정확', name: '이름', alias: '별칭', spec: '규격', chosung: '초성', chosung_alias: '초성' };
|
||||||
@@ -55,7 +62,7 @@ function renderCards() {
|
|||||||
container.innerHTML = requestsList.map(r => {
|
container.innerHTML = requestsList.map(r => {
|
||||||
const itemName = r.item_name || r.custom_item_name || '-';
|
const itemName = r.item_name || r.custom_item_name || '-';
|
||||||
const category = r.category || r.custom_category;
|
const category = r.category || r.custom_category;
|
||||||
const catLabel = CAT_LABELS[category] || category || '';
|
const catLabel = getCatLabel(category);
|
||||||
const statusLabel = STATUS_LABELS[r.status] || r.status;
|
const statusLabel = STATUS_LABELS[r.status] || r.status;
|
||||||
const statusColor = STATUS_COLORS[r.status] || 'badge-gray';
|
const statusColor = STATUS_COLORS[r.status] || 'badge-gray';
|
||||||
const isCustom = !r.item_id && r.custom_item_name;
|
const isCustom = !r.item_id && r.custom_item_name;
|
||||||
@@ -96,7 +103,7 @@ function openDetail(requestId) {
|
|||||||
if (!r) return;
|
if (!r) return;
|
||||||
const itemName = r.item_name || r.custom_item_name || '-';
|
const itemName = r.item_name || r.custom_item_name || '-';
|
||||||
const category = r.category || r.custom_category;
|
const category = r.category || r.custom_category;
|
||||||
const catLabel = CAT_LABELS[category] || category || '-';
|
const catLabel = getCatLabel(category);
|
||||||
const statusLabel = STATUS_LABELS[r.status] || r.status;
|
const statusLabel = STATUS_LABELS[r.status] || r.status;
|
||||||
const statusColor = STATUS_COLORS[r.status] || 'badge-gray';
|
const statusColor = STATUS_COLORS[r.status] || 'badge-gray';
|
||||||
|
|
||||||
@@ -182,7 +189,7 @@ function renderSearchResults(items, query) {
|
|||||||
const container = document.getElementById('searchResults');
|
const container = document.getElementById('searchResults');
|
||||||
let html = '';
|
let html = '';
|
||||||
items.forEach(item => {
|
items.forEach(item => {
|
||||||
const catLabel = CAT_LABELS[item.category] || '';
|
const catLabel = getCatLabel(item.category);
|
||||||
const matchLabel = MATCH_LABELS[item._matchType] || '';
|
const matchLabel = MATCH_LABELS[item._matchType] || '';
|
||||||
const spec = item.spec ? ' [' + escapeHtml(item.spec) + ']' : '';
|
const spec = item.spec ? ' [' + escapeHtml(item.spec) + ']' : '';
|
||||||
const maker = item.maker ? ' (' + escapeHtml(item.maker) + ')' : '';
|
const maker = item.maker ? ' (' + escapeHtml(item.maker) + ')' : '';
|
||||||
@@ -288,7 +295,7 @@ function renderCart() {
|
|||||||
list.innerHTML = cartItems.map((c, idx) => {
|
list.innerHTML = cartItems.map((c, idx) => {
|
||||||
const spec = c.spec ? ' [' + escapeHtml(c.spec) + ']' : '';
|
const spec = c.spec ? ' [' + escapeHtml(c.spec) + ']' : '';
|
||||||
const maker = c.maker ? ' (' + escapeHtml(c.maker) + ')' : '';
|
const maker = c.maker ? ' (' + escapeHtml(c.maker) + ')' : '';
|
||||||
const catLabel = CAT_LABELS[c.category] || '';
|
const catLabel = getCatLabel(c.category);
|
||||||
|
|
||||||
// 사진 썸네일
|
// 사진 썸네일
|
||||||
let thumbHtml = '';
|
let thumbHtml = '';
|
||||||
@@ -478,6 +485,7 @@ function checkViewParam() {
|
|||||||
/* ===== Init ===== */
|
/* ===== Init ===== */
|
||||||
(async function() {
|
(async function() {
|
||||||
if (!await initAuth()) return;
|
if (!await initAuth()) return;
|
||||||
|
await loadCategories();
|
||||||
await loadRequests();
|
await loadRequests();
|
||||||
checkViewParam();
|
checkViewParam();
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -3,10 +3,16 @@ const TKUSER_BASE_URL = location.hostname.includes('technicalkorea.net')
|
|||||||
? 'https://tkuser.technicalkorea.net'
|
? 'https://tkuser.technicalkorea.net'
|
||||||
: location.protocol + '//' + location.hostname + ':30180';
|
: location.protocol + '//' + location.hostname + ':30180';
|
||||||
|
|
||||||
const CAT_LABELS = { consumable: '소모품', safety: '안전용품', repair: '수선비', equipment: '설비' };
|
// 카테고리 — API에서 동적 로드
|
||||||
const CAT_COLORS = { consumable: 'badge-blue', safety: 'badge-green', repair: 'badge-amber', equipment: 'badge-purple' };
|
let _categories = null;
|
||||||
const CAT_BG = { consumable: '#dbeafe', safety: '#dcfce7', repair: '#fef3c7', equipment: '#f3e8ff' };
|
async function loadCategories() {
|
||||||
const CAT_FG = { consumable: '#1e40af', safety: '#166534', repair: '#92400e', equipment: '#7e22ce' };
|
if (_categories) return _categories;
|
||||||
|
try { const r = await api('/consumable-categories'); _categories = r.data || []; } catch(e) { _categories = []; }
|
||||||
|
return _categories;
|
||||||
|
}
|
||||||
|
function getCatLabel(code) { return (_categories || []).find(c => c.category_code === code)?.category_name || code || '-'; }
|
||||||
|
function getCatBg(code) { return (_categories || []).find(c => c.category_code === code)?.color_bg || '#f3f4f6'; }
|
||||||
|
function getCatFg(code) { return (_categories || []).find(c => c.category_code === code)?.color_fg || '#374151'; }
|
||||||
const STATUS_LABELS = { pending: '대기', grouped: '구매진행중', purchased: '구매완료', received: '입고완료', cancelled: '취소', returned: '반품', hold: '보류' };
|
const STATUS_LABELS = { pending: '대기', grouped: '구매진행중', purchased: '구매완료', received: '입고완료', cancelled: '취소', returned: '반품', hold: '보류' };
|
||||||
const STATUS_COLORS = { pending: 'badge-amber', grouped: 'badge-blue', purchased: 'badge-green', received: 'badge-teal', cancelled: 'badge-red', returned: 'badge-red', hold: 'badge-gray' };
|
const STATUS_COLORS = { pending: 'badge-amber', grouped: 'badge-blue', purchased: 'badge-green', received: 'badge-teal', cancelled: 'badge-red', returned: 'badge-red', hold: 'badge-gray' };
|
||||||
|
|
||||||
@@ -125,9 +131,9 @@ function showDropdown(items, query) {
|
|||||||
let html = '';
|
let html = '';
|
||||||
if (items.length > 0) {
|
if (items.length > 0) {
|
||||||
items.forEach((item, idx) => {
|
items.forEach((item, idx) => {
|
||||||
const catLabel = CAT_LABELS[item.category] || item.category;
|
const catLabel = getCatLabel(item.category);
|
||||||
const bg = CAT_BG[item.category] || '#f3f4f6';
|
const bg = getCatBg(item.category);
|
||||||
const fg = CAT_FG[item.category] || '#374151';
|
const fg = getCatFg(item.category);
|
||||||
const spec = _fmtSpec(item.spec ? escapeHtml(item.spec) : '');
|
const spec = _fmtSpec(item.spec ? escapeHtml(item.spec) : '');
|
||||||
const maker = item.maker ? ` (${escapeHtml(item.maker)})` : '';
|
const maker = item.maker ? ` (${escapeHtml(item.maker)})` : '';
|
||||||
const photoSrc = item.photo_path ? (item.photo_path.startsWith('http') ? item.photo_path : TKUSER_BASE_URL + item.photo_path) : '';
|
const photoSrc = item.photo_path ? (item.photo_path.startsWith('http') ? item.photo_path : TKUSER_BASE_URL + item.photo_path) : '';
|
||||||
@@ -339,8 +345,8 @@ function renderRequests() {
|
|||||||
// 등록 품목이면 ci 데이터, 미등록이면 custom 데이터 사용
|
// 등록 품목이면 ci 데이터, 미등록이면 custom 데이터 사용
|
||||||
const itemName = r.item_name || r.custom_item_name || '-';
|
const itemName = r.item_name || r.custom_item_name || '-';
|
||||||
const category = r.category || r.custom_category;
|
const category = r.category || r.custom_category;
|
||||||
const catLabel = CAT_LABELS[category] || category || '-';
|
const catLabel = getCatLabel(category);
|
||||||
const catColor = CAT_COLORS[category] || 'badge-gray';
|
const catColor = 'badge-gray';
|
||||||
const statusLabel = STATUS_LABELS[r.status] || r.status;
|
const statusLabel = STATUS_LABELS[r.status] || r.status;
|
||||||
const statusColor = STATUS_COLORS[r.status] || 'badge-gray';
|
const statusColor = STATUS_COLORS[r.status] || 'badge-gray';
|
||||||
const isCustom = !r.item_id && r.custom_item_name;
|
const isCustom = !r.item_id && r.custom_item_name;
|
||||||
@@ -415,7 +421,7 @@ function openPurchaseModal(requestId) {
|
|||||||
|
|
||||||
document.getElementById('purchaseModalInfo').innerHTML = `
|
document.getElementById('purchaseModalInfo').innerHTML = `
|
||||||
<div class="font-medium">${escapeHtml(itemName)}${_fmtSpec(r.spec ? escapeHtml(r.spec) : '')} ${r.maker ? '(' + escapeHtml(r.maker) + ')' : ''}${isCustom ? ' <span class="text-orange-500 text-xs">(직접입력)</span>' : ''}</div>
|
<div class="font-medium">${escapeHtml(itemName)}${_fmtSpec(r.spec ? escapeHtml(r.spec) : '')} ${r.maker ? '(' + escapeHtml(r.maker) + ')' : ''}${isCustom ? ' <span class="text-orange-500 text-xs">(직접입력)</span>' : ''}</div>
|
||||||
<div class="text-xs text-gray-500 mt-1">분류: ${CAT_LABELS[category] || category || '-'} | 기준가: ${basePrice} | 신청수량: ${r.quantity}</div>
|
<div class="text-xs text-gray-500 mt-1">분류: ${getCatLabel(category)} | 기준가: ${basePrice} | 신청수량: ${r.quantity}</div>
|
||||||
${r.pr_photo_path ? `<img src="${r.pr_photo_path}" class="mt-2 w-20 h-20 rounded object-cover" onerror="this.style.display='none'">` : ''}
|
${r.pr_photo_path ? `<img src="${r.pr_photo_path}" class="mt-2 w-20 h-20 rounded object-cover" onerror="this.style.display='none'">` : ''}
|
||||||
`;
|
`;
|
||||||
document.getElementById('pmUnitPrice').value = r.base_price || '';
|
document.getElementById('pmUnitPrice').value = r.base_price || '';
|
||||||
@@ -773,6 +779,7 @@ async function submitReceive() {
|
|||||||
(async function() {
|
(async function() {
|
||||||
if (!await initAuth()) return;
|
if (!await initAuth()) return;
|
||||||
isAdmin = currentUser && ['admin', 'system', 'system admin'].includes(currentUser.role);
|
isAdmin = currentUser && ['admin', 'system', 'system admin'].includes(currentUser.role);
|
||||||
|
await loadCategories();
|
||||||
initItemSearch();
|
initItemSearch();
|
||||||
await loadInitialData();
|
await loadInitialData();
|
||||||
await loadRequests();
|
await loadRequests();
|
||||||
|
|||||||
@@ -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="품명/메이커 검색">
|
<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">
|
<select id="consumableFilterCategoryTkuser" class="input-field px-2 py-1.5 rounded-lg text-sm">
|
||||||
<option value="">전체 분류</option>
|
<option value="">전체 분류</option>
|
||||||
<option value="consumable">소모품</option>
|
|
||||||
<option value="safety">안전용품</option>
|
|
||||||
<option value="repair">수선비</option>
|
|
||||||
<option value="equipment">설비</option>
|
|
||||||
</select>
|
</select>
|
||||||
<select id="consumableFilterActiveTkuser" class="input-field px-2 py-1.5 rounded-lg text-sm">
|
<select id="consumableFilterActiveTkuser" class="input-field px-2 py-1.5 rounded-lg text-sm">
|
||||||
<option value="true">활성</option>
|
<option value="true">활성</option>
|
||||||
@@ -1789,6 +1785,14 @@
|
|||||||
<p class="text-gray-400 text-center py-4 text-sm">탭을 선택하면 데이터를 불러옵니다.</p>
|
<p class="text-gray-400 text-center py-4 text-sm">탭을 선택하면 데이터를 불러옵니다.</p>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
|
|
||||||
<!-- ============ 알림 수신자 탭 ============ -->
|
<!-- ============ 알림 수신자 탭 ============ -->
|
||||||
|
|||||||
@@ -2,26 +2,71 @@
|
|||||||
let consumablesLoaded = false;
|
let consumablesLoaded = false;
|
||||||
let consumablesList = [];
|
let consumablesList = [];
|
||||||
|
|
||||||
const CONSUMABLE_CATEGORIES = {
|
// 카테고리 — system1-factory API에서 동적 로드 (fallback: 기본 4개)
|
||||||
consumable: '소모품',
|
const TKFB_API = location.hostname.includes('technicalkorea.net')
|
||||||
safety: '안전용품',
|
? 'https://tkfb.technicalkorea.net/api'
|
||||||
repair: '수선비',
|
: location.protocol + '//' + location.hostname + ':30005/api';
|
||||||
equipment: '설비'
|
let _consumableCategories = null;
|
||||||
};
|
const DEFAULT_CATEGORIES = [
|
||||||
const CONSUMABLE_CAT_COLORS = {
|
{ category_code: 'consumable', category_name: '소모품', icon: 'fa-box', color_bg: '#dbeafe', color_fg: '#1e40af' },
|
||||||
consumable: 'bg-blue-50 text-blue-600',
|
{ category_code: 'safety', category_name: '안전용품', icon: 'fa-hard-hat', color_bg: '#dcfce7', color_fg: '#166534' },
|
||||||
safety: 'bg-green-50 text-green-600',
|
{ category_code: 'repair', category_name: '수선비', icon: 'fa-wrench', color_bg: '#fef3c7', color_fg: '#92400e' },
|
||||||
repair: 'bg-amber-50 text-amber-600',
|
{ category_code: 'equipment', category_name: '설비', icon: 'fa-cogs', color_bg: '#f3e8ff', color_fg: '#7e22ce' }
|
||||||
equipment: 'bg-purple-50 text-purple-600'
|
];
|
||||||
};
|
|
||||||
|
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() {
|
async function loadConsumablesTab() {
|
||||||
if (consumablesLoaded) return;
|
if (consumablesLoaded) return;
|
||||||
consumablesLoaded = true;
|
consumablesLoaded = true;
|
||||||
|
await loadConsumableCategories();
|
||||||
if (currentUser && ['admin', 'system'].includes(currentUser.role)) {
|
if (currentUser && ['admin', 'system'].includes(currentUser.role)) {
|
||||||
document.getElementById('btnAddConsumableTkuser')?.classList.remove('hidden');
|
document.getElementById('btnAddConsumableTkuser')?.classList.remove('hidden');
|
||||||
|
document.getElementById('categoryManageSection')?.classList.remove('hidden');
|
||||||
}
|
}
|
||||||
|
populateCategorySelects();
|
||||||
await loadConsumablesList();
|
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() {
|
async function loadConsumablesList() {
|
||||||
@@ -50,8 +95,8 @@ function renderConsumablesListTkuser() {
|
|||||||
const isAdmin = currentUser && ['admin', 'system'].includes(currentUser.role);
|
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">` +
|
c.innerHTML = `<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">` +
|
||||||
consumablesList.map(item => {
|
consumablesList.map(item => {
|
||||||
const catLabel = CONSUMABLE_CATEGORIES[item.category] || item.category;
|
const catLabel = getCatLabel(item.category);
|
||||||
const catColor = CONSUMABLE_CAT_COLORS[item.category] || 'bg-gray-50 text-gray-600';
|
const catStyle = getCatStyle(item.category);
|
||||||
const price = item.base_price ? Number(item.base_price).toLocaleString() + '원' : '-';
|
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">
|
return `<div class="bg-white border rounded-lg p-3 hover:shadow-md transition-shadow">
|
||||||
<div class="flex gap-3">
|
<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-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="text-xs text-gray-500 mt-0.5">${escHtml(item.maker) || '-'}</div>
|
||||||
<div class="flex items-center gap-2 mt-1">
|
<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-600 font-medium">${price}</span>
|
||||||
<span class="text-xs text-gray-400">${escHtml(item.unit) || 'EA'}</span>
|
<span class="text-xs text-gray-400">${escHtml(item.unit) || 'EA'}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -190,6 +235,47 @@ async function deactivateConsumableTkuser(id, name) {
|
|||||||
} catch (e) { showToast(e.message, 'error'); }
|
} 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', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
let searchTimeout;
|
let searchTimeout;
|
||||||
|
|||||||
Reference in New Issue
Block a user