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 itemAliasRoutes = require('../routes/itemAliasRoutes');
|
||||
const purchaseBatchRoutes = require('../routes/purchaseBatchRoutes');
|
||||
const consumableCategoryRoutes = require('../routes/consumableCategoryRoutes');
|
||||
const scheduleRoutes = require('../routes/scheduleRoutes');
|
||||
const meetingRoutes = require('../routes/meetingRoutes');
|
||||
const proxyInputRoutes = require('../routes/proxyInputRoutes');
|
||||
@@ -170,6 +171,7 @@ function setupRoutes(app) {
|
||||
app.use('/api/settlements', settlementRoutes); // 월간 정산
|
||||
app.use('/api/item-aliases', itemAliasRoutes); // 품목 별칭
|
||||
app.use('/api/purchase-batches', purchaseBatchRoutes); // 구매 그룹
|
||||
app.use('/api/consumable-categories', consumableCategoryRoutes); // 소모품 카테고리
|
||||
app.use('/api/schedule', scheduleRoutes); // 공정표
|
||||
app.use('/api/meetings', meetingRoutes); // 생산회의록
|
||||
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;
|
||||
Reference in New Issue
Block a user