From 0a05bd8d76b1a737ced2a812d837370a1b3762f0 Mon Sep 17 00:00:00 2001 From: Hyungi Ahn Date: Mon, 16 Mar 2026 13:34:43 +0900 Subject: [PATCH] =?UTF-8?q?feat(consumable):=20=EC=86=8C=EB=AA=A8=ED=92=88?= =?UTF-8?q?=20=EB=A7=88=EC=8A=A4=ED=84=B0=EC=97=90=20"=EA=B7=9C=EA=B2=A9(s?= =?UTF-8?q?pec)"=20=ED=95=84=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 품목의 규격 정보(예: 4" 용접, M16)를 분리 저장할 수 있도록 spec 컬럼 추가. DB ALTER 필요: ALTER TABLE consumable_items ADD COLUMN spec VARCHAR(200) DEFAULT NULL AFTER item_name; Co-Authored-By: Claude Opus 4.6 --- system1-factory/api/models/purchaseModel.js | 4 ++-- .../api/models/purchaseRequestModel.js | 4 ++-- system1-factory/api/models/settlementModel.js | 4 ++-- .../web/pages/admin/purchase-analysis.html | 2 +- .../web/pages/purchase/request.html | 2 +- .../web/static/js/purchase-analysis.js | 4 ++-- .../web/static/js/purchase-request.js | 19 ++++++++++++------- .../api/models/consumableItemModel.js | 9 +++++---- user-management/web/index.html | 10 +++++++++- .../web/static/js/tkuser-consumables.js | 5 ++++- 10 files changed, 40 insertions(+), 23 deletions(-) diff --git a/system1-factory/api/models/purchaseModel.js b/system1-factory/api/models/purchaseModel.js index 19d3ef5..aa2f8a6 100644 --- a/system1-factory/api/models/purchaseModel.js +++ b/system1-factory/api/models/purchaseModel.js @@ -6,7 +6,7 @@ const PurchaseModel = { async getAll(filters = {}) { const db = await getDb(); let sql = ` - SELECT p.*, ci.item_name, ci.maker, ci.category, ci.unit, ci.photo_path, + SELECT p.*, ci.item_name, ci.spec, ci.maker, ci.category, ci.unit, ci.photo_path, v.vendor_name, su.name AS purchaser_name FROM purchases p JOIN consumable_items ci ON p.item_id = ci.item_id @@ -122,7 +122,7 @@ const PurchaseModel = { // 소모품 목록 (구매신청용) async getConsumableItems(activeOnly = true) { const db = await getDb(); - let sql = 'SELECT item_id, item_name, maker, category, base_price, unit, photo_path FROM consumable_items'; + let sql = 'SELECT item_id, item_name, spec, maker, category, base_price, unit, photo_path FROM consumable_items'; if (activeOnly) sql += ' WHERE is_active = 1'; sql += ' ORDER BY category, item_name'; const [rows] = await db.query(sql); diff --git a/system1-factory/api/models/purchaseRequestModel.js b/system1-factory/api/models/purchaseRequestModel.js index f6242ee..d7c2423 100644 --- a/system1-factory/api/models/purchaseRequestModel.js +++ b/system1-factory/api/models/purchaseRequestModel.js @@ -6,7 +6,7 @@ const PurchaseRequestModel = { async getAll(filters = {}) { const db = await getDb(); let sql = ` - SELECT pr.*, ci.item_name, ci.maker, ci.category, ci.base_price, ci.unit, + SELECT pr.*, ci.item_name, ci.spec, ci.maker, ci.category, ci.base_price, ci.unit, ci.photo_path AS ci_photo_path, pr.photo_path AS pr_photo_path, pr.custom_item_name, pr.custom_category, su.name AS requester_name @@ -35,7 +35,7 @@ const PurchaseRequestModel = { async getById(requestId) { const db = await getDb(); const [rows] = await db.query(` - SELECT pr.*, ci.item_name, ci.maker, ci.category, ci.base_price, ci.unit, + SELECT pr.*, ci.item_name, ci.spec, ci.maker, ci.category, ci.base_price, ci.unit, ci.photo_path AS ci_photo_path, pr.photo_path AS pr_photo_path, pr.custom_item_name, pr.custom_category, su.name AS requester_name diff --git a/system1-factory/api/models/settlementModel.js b/system1-factory/api/models/settlementModel.js index 3d0a741..e25004e 100644 --- a/system1-factory/api/models/settlementModel.js +++ b/system1-factory/api/models/settlementModel.js @@ -40,7 +40,7 @@ const SettlementModel = { async getMonthlyPurchases(yearMonth) { const db = await getDb(); const [rows] = await db.query(` - SELECT p.*, ci.item_name, ci.maker, ci.category, ci.unit, ci.base_price, ci.photo_path, + SELECT p.*, ci.item_name, ci.spec, ci.maker, ci.category, ci.unit, ci.base_price, ci.photo_path, v.vendor_name, su.name AS purchaser_name FROM purchases p JOIN consumable_items ci ON p.item_id = ci.item_id @@ -88,7 +88,7 @@ const SettlementModel = { const db = await getDb(); const [rows] = await db.query(` SELECT p.purchase_id, p.purchase_date, p.unit_price, p.quantity, - ci.item_id, ci.item_name, ci.maker, ci.category, ci.base_price, + ci.item_id, ci.item_name, ci.spec, ci.maker, ci.category, ci.base_price, v.vendor_name FROM purchases p JOIN consumable_items ci ON p.item_id = ci.item_id diff --git a/system1-factory/web/pages/admin/purchase-analysis.html b/system1-factory/web/pages/admin/purchase-analysis.html index e18685a..9110c61 100644 --- a/system1-factory/web/pages/admin/purchase-analysis.html +++ b/system1-factory/web/pages/admin/purchase-analysis.html @@ -108,6 +108,6 @@ - + diff --git a/system1-factory/web/pages/purchase/request.html b/system1-factory/web/pages/purchase/request.html index b17e54d..8ff79d3 100644 --- a/system1-factory/web/pages/purchase/request.html +++ b/system1-factory/web/pages/purchase/request.html @@ -222,6 +222,6 @@ - + diff --git a/system1-factory/web/static/js/purchase-analysis.js b/system1-factory/web/static/js/purchase-analysis.js index 1e29176..9f1ea1d 100644 --- a/system1-factory/web/static/js/purchase-analysis.js +++ b/system1-factory/web/static/js/purchase-analysis.js @@ -102,7 +102,7 @@ function renderPurchaseList(data) { return ` -
${escapeHtml(p.item_name)}
+
${escapeHtml(p.item_name)}${p.spec ? ' [' + escapeHtml(p.spec) + ']' : ''}
${escapeHtml(p.maker || '')}
${catLabel} @@ -138,7 +138,7 @@ function renderPriceChanges(data) { const arrow = diff > 0 ? '▲' : '▼'; const color = diff > 0 ? 'text-red-600' : 'text-blue-600'; return ` - ${escapeHtml(p.item_name)} ${p.maker ? '(' + escapeHtml(p.maker) + ')' : ''} + ${escapeHtml(p.item_name)}${p.spec ? ' [' + escapeHtml(p.spec) + ']' : ''} ${p.maker ? '(' + escapeHtml(p.maker) + ')' : ''} ${Number(p.base_price).toLocaleString()}원 ${Number(p.unit_price).toLocaleString()}원 ${arrow} ${Math.abs(diff).toLocaleString()}원 diff --git a/system1-factory/web/static/js/purchase-request.js b/system1-factory/web/static/js/purchase-request.js index aa123ce..23ac6ca 100644 --- a/system1-factory/web/static/js/purchase-request.js +++ b/system1-factory/web/static/js/purchase-request.js @@ -10,6 +10,8 @@ const CAT_FG = { consumable: '#1e40af', safety: '#166534', repair: '#92400e', eq const STATUS_LABELS = { pending: '대기', purchased: '구매완료', hold: '보류' }; const STATUS_COLORS = { pending: 'badge-amber', purchased: 'badge-green', hold: 'badge-gray' }; +function _fmtSpec(spec) { return spec ? ' [' + spec + ']' : ''; } + let consumableItems = []; let vendorsList = []; let requestsList = []; @@ -56,7 +58,8 @@ function initItemSearch() { const lower = query.toLowerCase(); const filtered = consumableItems.filter(item => item.item_name.toLowerCase().includes(lower) || - (item.maker && item.maker.toLowerCase().includes(lower)) + (item.maker && item.maker.toLowerCase().includes(lower)) || + (item.spec && item.spec.toLowerCase().includes(lower)) ); showDropdown(filtered, query); } @@ -71,7 +74,8 @@ function initItemSearch() { const lower = query.toLowerCase(); const filtered = consumableItems.filter(item => item.item_name.toLowerCase().includes(lower) || - (item.maker && item.maker.toLowerCase().includes(lower)) + (item.maker && item.maker.toLowerCase().includes(lower)) || + (item.spec && item.spec.toLowerCase().includes(lower)) ); showDropdown(filtered, query); } @@ -115,10 +119,11 @@ function showDropdown(items, query) { const catLabel = CAT_LABELS[item.category] || item.category; const bg = CAT_BG[item.category] || '#f3f4f6'; const fg = CAT_FG[item.category] || '#374151'; + const spec = _fmtSpec(item.spec ? escapeHtml(item.spec) : ''); const maker = item.maker ? ` (${escapeHtml(item.maker)})` : ''; html += `
${catLabel} - ${escapeHtml(item.item_name)}${maker} + ${escapeHtml(item.item_name)}${spec}${maker}
`; }); } @@ -156,7 +161,7 @@ function selectItem(itemId) { if (!item) return; const input = document.getElementById('prItemSearch'); - input.value = item.item_name + (item.maker ? ' (' + item.maker + ')' : ''); + input.value = item.item_name + _fmtSpec(item.spec) + (item.maker ? ' (' + item.maker + ')' : ''); document.getElementById('prItemId').value = item.item_id; document.getElementById('prCustomItemName').value = ''; closeDropdown(); @@ -172,7 +177,7 @@ function selectItem(itemId) { } else { photoEl.classList.add('hidden'); } - document.getElementById('prItemInfo').textContent = `${item.item_name} ${item.maker ? '(' + item.maker + ')' : ''}`; + document.getElementById('prItemInfo').textContent = `${item.item_name}${_fmtSpec(item.spec)} ${item.maker ? '(' + item.maker + ')' : ''}`; const price = item.base_price ? Number(item.base_price).toLocaleString() + '원/' + (item.unit || 'EA') : '기준가 미설정'; document.getElementById('prItemPrice').textContent = price; @@ -343,7 +348,7 @@ function renderRequests() {
${photoSrc ? `` : ''}
-
${escapeHtml(itemName)}${isCustom ? ' (직접입력)' : ''}
+
${escapeHtml(itemName)}${r.spec ? ' [' + escapeHtml(r.spec) + ']' : ''}${isCustom ? ' (직접입력)' : ''}
${escapeHtml(r.maker || '')}
@@ -373,7 +378,7 @@ function openPurchaseModal(requestId) { const basePrice = r.base_price ? Number(r.base_price).toLocaleString() + '원' : '-'; document.getElementById('purchaseModalInfo').innerHTML = ` -
${escapeHtml(itemName)} ${r.maker ? '(' + escapeHtml(r.maker) + ')' : ''}${isCustom ? ' (직접입력)' : ''}
+
${escapeHtml(itemName)}${_fmtSpec(r.spec ? escapeHtml(r.spec) : '')} ${r.maker ? '(' + escapeHtml(r.maker) + ')' : ''}${isCustom ? ' (직접입력)' : ''}
분류: ${CAT_LABELS[category] || category || '-'} | 기준가: ${basePrice} | 신청수량: ${r.quantity}
${r.pr_photo_path ? `` : ''} `; diff --git a/user-management/api/models/consumableItemModel.js b/user-management/api/models/consumableItemModel.js index 72a645b..0707410 100644 --- a/user-management/api/models/consumableItemModel.js +++ b/user-management/api/models/consumableItemModel.js @@ -8,7 +8,7 @@ async function findAll({ category, search, is_active } = {}) { const params = []; if (is_active !== undefined) { sql += ' AND is_active = ?'; params.push(is_active); } if (category) { sql += ' AND category = ?'; params.push(category); } - if (search) { sql += ' AND (item_name LIKE ? OR maker LIKE ?)'; params.push(`%${search}%`, `%${search}%`); } + if (search) { sql += ' AND (item_name LIKE ? OR maker LIKE ? OR spec LIKE ?)'; params.push(`%${search}%`, `%${search}%`, `%${search}%`); } sql += ' ORDER BY category, item_name'; const [rows] = await db.query(sql, params); return rows; @@ -23,9 +23,9 @@ async function findById(id) { async function create(data) { const db = getPool(); const [result] = await db.query( - `INSERT INTO consumable_items (item_name, maker, category, base_price, unit, photo_path) - VALUES (?, ?, ?, ?, ?, ?)`, - [data.item_name, data.maker || null, data.category, + `INSERT INTO consumable_items (item_name, spec, maker, category, base_price, unit, photo_path) + VALUES (?, ?, ?, ?, ?, ?, ?)`, + [data.item_name, data.spec || null, data.maker || null, data.category, data.base_price || 0, data.unit || 'EA', data.photo_path || null] ); return findById(result.insertId); @@ -36,6 +36,7 @@ async function update(id, data) { const fields = []; const values = []; if (data.item_name !== undefined) { fields.push('item_name = ?'); values.push(data.item_name); } + if (data.spec !== undefined) { fields.push('spec = ?'); values.push(data.spec || null); } if (data.maker !== undefined) { fields.push('maker = ?'); values.push(data.maker || null); } if (data.category !== undefined) { fields.push('category = ?'); values.push(data.category); } if (data.base_price !== undefined) { fields.push('base_price = ?'); values.push(data.base_price); } diff --git a/user-management/web/index.html b/user-management/web/index.html index 9c3cf31..d884492 100644 --- a/user-management/web/index.html +++ b/user-management/web/index.html @@ -1909,6 +1909,10 @@ +
+ + +
@@ -1959,6 +1963,10 @@
+
+ + +
@@ -2014,7 +2022,7 @@ - + diff --git a/user-management/web/static/js/tkuser-consumables.js b/user-management/web/static/js/tkuser-consumables.js index abe90ea..597088c 100644 --- a/user-management/web/static/js/tkuser-consumables.js +++ b/user-management/web/static/js/tkuser-consumables.js @@ -59,7 +59,7 @@ function renderConsumablesListTkuser() { ? `` : `
`}
-
${escHtml(item.item_name)}
+
${escHtml(item.item_name)}${item.spec ? ' [' + escHtml(item.spec) + ']' : ''}
${escHtml(item.maker) || '-'}
${catLabel} @@ -102,6 +102,7 @@ async function submitAddConsumableTkuser(e) { const fd = new FormData(); fd.append('item_name', itemName); + fd.append('spec', document.getElementById('newConsumableSpecTkuser').value.trim()); fd.append('maker', document.getElementById('newConsumableMakerTkuser').value.trim()); fd.append('category', category); fd.append('base_price', document.getElementById('newConsumablePriceTkuser').value || '0'); @@ -130,6 +131,7 @@ function openEditConsumableTkuser(id) { if (!item) return; document.getElementById('editConsumableIdTkuser').value = item.item_id; document.getElementById('editConsumableNameTkuser').value = item.item_name; + document.getElementById('editConsumableSpecTkuser').value = item.spec || ''; document.getElementById('editConsumableMakerTkuser').value = item.maker || ''; document.getElementById('editConsumableCategoryTkuser').value = item.category; document.getElementById('editConsumablePriceTkuser').value = item.base_price || ''; @@ -155,6 +157,7 @@ async function submitEditConsumableTkuser(e) { const id = document.getElementById('editConsumableIdTkuser').value; const fd = new FormData(); fd.append('item_name', document.getElementById('editConsumableNameTkuser').value.trim()); + fd.append('spec', document.getElementById('editConsumableSpecTkuser').value.trim()); fd.append('maker', document.getElementById('editConsumableMakerTkuser').value.trim()); fd.append('category', document.getElementById('editConsumableCategoryTkuser').value); fd.append('base_price', document.getElementById('editConsumablePriceTkuser').value || '0');