diff --git a/system1-factory/api/Dockerfile b/system1-factory/api/Dockerfile index 314d33e..45f8a08 100644 --- a/system1-factory/api/Dockerfile +++ b/system1-factory/api/Dockerfile @@ -17,7 +17,7 @@ RUN apk add --no-cache --virtual .build-deps python3 make g++ && \ COPY . . # 로그 디렉토리 생성 -RUN mkdir -p logs uploads +RUN mkdir -p logs uploads/issues uploads/equipments uploads/purchase_requests # 실행 권한 설정 RUN chown -R node:node /usr/src/app diff --git a/system1-factory/api/controllers/purchaseController.js b/system1-factory/api/controllers/purchaseController.js index 8ec722d..060429a 100644 --- a/system1-factory/api/controllers/purchaseController.js +++ b/system1-factory/api/controllers/purchaseController.js @@ -6,16 +6,37 @@ const PurchaseController = { // 구매 처리 (신청 → 구매) create: async (req, res) => { try { - const { request_id, item_id, vendor_id, quantity, unit_price, purchase_date, update_base_price, notes } = req.body; + const { request_id, item_id, vendor_id, quantity, unit_price, purchase_date, update_base_price, register_to_master, notes } = req.body; - if (!item_id) return res.status(400).json({ success: false, message: '소모품을 선택해주세요.' }); + // item_id가 없으면 custom item → register_to_master로 자동 등록 가능 + let effectiveItemId = item_id; + + if (!effectiveItemId && request_id) { + // 미등록 품목의 구매 처리 — 마스터 등록 처리 + const requestData = await PurchaseRequestModel.getById(request_id); + if (requestData && requestData.custom_item_name) { + if (register_to_master !== false) { + // 마스터에 등록 + const newItemId = await PurchaseModel.registerToMaster( + requestData.custom_item_name, + requestData.custom_category, + null // maker + ); + effectiveItemId = newItemId; + // purchase_requests.item_id 업데이트 + await PurchaseRequestModel.updateItemId(request_id, newItemId); + } + } + } + + if (!effectiveItemId) return res.status(400).json({ success: false, message: '소모품을 선택해주세요.' }); if (!unit_price) return res.status(400).json({ success: false, message: '구매 단가를 입력해주세요.' }); if (!purchase_date) return res.status(400).json({ success: false, message: '구매일을 입력해주세요.' }); // 구매 내역 생성 const purchaseId = await PurchaseModel.createFromRequest({ request_id: request_id || null, - item_id, + item_id: effectiveItemId, vendor_id: vendor_id || null, quantity: quantity || 1, unit_price, @@ -27,9 +48,9 @@ const PurchaseController = { // 기준가 업데이트 요청 시 if (update_base_price) { const items = await PurchaseModel.getConsumableItems(false); - const item = items.find(i => i.item_id === parseInt(item_id)); + const item = items.find(i => i.item_id === parseInt(effectiveItemId)); if (item) { - await PurchaseModel.updateBasePrice(item_id, unit_price, item.base_price, req.user.id); + await PurchaseModel.updateBasePrice(effectiveItemId, unit_price, item.base_price, req.user.id); } } @@ -37,9 +58,10 @@ const PurchaseController = { let equipmentResult = null; if (request_id) { const requestData = await PurchaseRequestModel.getById(request_id); - if (requestData && requestData.category === 'equipment') { + const category = requestData?.category || requestData?.custom_category; + if (category === 'equipment') { equipmentResult = await PurchaseModel.tryAutoRegisterEquipment({ - item_name: requestData.item_name, + item_name: requestData.item_name || requestData.custom_item_name, maker: requestData.maker, vendor_name: null, unit_price, @@ -51,7 +73,7 @@ const PurchaseController = { } else { // 직접 구매 시에도 category 확인 const items = await PurchaseModel.getConsumableItems(false); - const item = items.find(i => i.item_id === parseInt(item_id)); + const item = items.find(i => i.item_id === parseInt(effectiveItemId)); if (item && item.category === 'equipment') { const vendors = await PurchaseModel.getVendors(); const vendor = vendors.find(v => v.vendor_id === parseInt(vendor_id)); diff --git a/system1-factory/api/controllers/purchaseRequestController.js b/system1-factory/api/controllers/purchaseRequestController.js index c5f17c4..1d8de8e 100644 --- a/system1-factory/api/controllers/purchaseRequestController.js +++ b/system1-factory/api/controllers/purchaseRequestController.js @@ -1,5 +1,6 @@ const PurchaseRequestModel = require('../models/purchaseRequestModel'); const PurchaseModel = require('../models/purchaseModel'); +const { saveBase64Image } = require('../services/imageUploadService'); const logger = require('../utils/logger'); const PurchaseRequestController = { @@ -33,16 +34,34 @@ const PurchaseRequestController = { // 구매신청 생성 create: async (req, res) => { try { - const { item_id, quantity, notes } = req.body; - if (!item_id) return res.status(400).json({ success: false, message: '소모품을 선택해주세요.' }); - if (!quantity || quantity < 1) return res.status(400).json({ success: false, message: '수량은 1 이상이어야 합니다.' }); + const { item_id, custom_item_name, custom_category, quantity, notes, photo } = req.body; + + // item_id 또는 custom_item_name 중 하나 필수 + if (!item_id && !custom_item_name) { + return res.status(400).json({ success: false, message: '소모품을 선택하거나 품목명을 입력해주세요.' }); + } + if (!quantity || quantity < 1) { + return res.status(400).json({ success: false, message: '수량은 1 이상이어야 합니다.' }); + } + if (!item_id && custom_item_name && !custom_category) { + return res.status(400).json({ success: false, message: '직접 입력 시 분류를 선택해주세요.' }); + } + + // 사진 업로드 + let photo_path = null; + if (photo) { + photo_path = await saveBase64Image(photo, 'pr', 'purchase_requests'); + } const request = await PurchaseRequestModel.create({ - item_id, + item_id: item_id || null, + custom_item_name: custom_item_name || null, + custom_category: custom_category || null, quantity, requester_id: req.user.id, request_date: new Date().toISOString().substring(0, 10), - notes + notes, + photo_path }); res.status(201).json({ success: true, data: request, message: '구매신청이 등록되었습니다.' }); } catch (err) { diff --git a/system1-factory/api/models/purchaseModel.js b/system1-factory/api/models/purchaseModel.js index b014723..19d3ef5 100644 --- a/system1-factory/api/models/purchaseModel.js +++ b/system1-factory/api/models/purchaseModel.js @@ -129,6 +129,27 @@ const PurchaseModel = { return rows; }, + // 미등록 품목 → 소모품 마스터 등록 + async registerToMaster(customItemName, customCategory, maker) { + const db = await getDb(); + + // 중복 확인 + const [existing] = await db.query( + `SELECT item_id FROM consumable_items WHERE item_name = ? AND (maker = ? OR (maker IS NULL AND ? IS NULL))`, + [customItemName, maker || null, maker || null] + ); + if (existing.length > 0) { + return existing[0].item_id; + } + + // 신규 등록 (photo_path = NULL) + const [result] = await db.query( + `INSERT INTO consumable_items (item_name, maker, category, is_active) VALUES (?, ?, ?, 1)`, + [customItemName, maker || null, customCategory || 'consumable'] + ); + return result.insertId; + }, + // 가격 변동 이력 async getPriceHistory(itemId) { const db = await getDb(); diff --git a/system1-factory/api/models/purchaseRequestModel.js b/system1-factory/api/models/purchaseRequestModel.js index 92f1cc0..f6242ee 100644 --- a/system1-factory/api/models/purchaseRequestModel.js +++ b/system1-factory/api/models/purchaseRequestModel.js @@ -2,14 +2,16 @@ const { getDb } = require('../dbPool'); const PurchaseRequestModel = { - // 구매신청 목록 (소모품 정보 JOIN) + // 구매신청 목록 (소모품 정보 LEFT JOIN — item_id NULL 허용) async getAll(filters = {}) { const db = await getDb(); let sql = ` - SELECT pr.*, ci.item_name, ci.maker, ci.category, ci.base_price, ci.unit, ci.photo_path, + SELECT pr.*, ci.item_name, 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 FROM purchase_requests pr - JOIN consumable_items ci ON pr.item_id = ci.item_id + LEFT JOIN consumable_items ci ON pr.item_id = ci.item_id LEFT JOIN sso_users su ON pr.requester_id = su.user_id WHERE 1=1 `; @@ -17,7 +19,10 @@ const PurchaseRequestModel = { if (filters.status) { sql += ' AND pr.status = ?'; params.push(filters.status); } if (filters.requester_id) { sql += ' AND pr.requester_id = ?'; params.push(filters.requester_id); } - if (filters.category) { sql += ' AND ci.category = ?'; params.push(filters.category); } + if (filters.category) { + sql += ' AND (ci.category = ? OR pr.custom_category = ?)'; + params.push(filters.category, filters.category); + } if (filters.from_date) { sql += ' AND pr.request_date >= ?'; params.push(filters.from_date); } if (filters.to_date) { sql += ' AND pr.request_date <= ?'; params.push(filters.to_date); } @@ -30,10 +35,12 @@ 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, ci.photo_path, + SELECT pr.*, ci.item_name, 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 FROM purchase_requests pr - JOIN consumable_items ci ON pr.item_id = ci.item_id + LEFT JOIN consumable_items ci ON pr.item_id = ci.item_id LEFT JOIN sso_users su ON pr.requester_id = su.user_id WHERE pr.request_id = ? `, [requestId]); @@ -44,9 +51,10 @@ const PurchaseRequestModel = { async create(data) { const db = await getDb(); const [result] = await db.query( - `INSERT INTO purchase_requests (item_id, quantity, requester_id, request_date, notes) - VALUES (?, ?, ?, ?, ?)`, - [data.item_id, data.quantity || 1, data.requester_id, data.request_date, data.notes || null] + `INSERT INTO purchase_requests (item_id, custom_item_name, custom_category, quantity, requester_id, request_date, notes, photo_path) + VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, + [data.item_id || null, data.custom_item_name || null, data.custom_category || null, + data.quantity || 1, data.requester_id, data.request_date, data.notes || null, data.photo_path || null] ); return this.getById(result.insertId); }, @@ -80,6 +88,15 @@ const PurchaseRequestModel = { return this.getById(requestId); }, + // item_id 업데이트 (마스터 등록 후) + async updateItemId(requestId, itemId) { + const db = await getDb(); + await db.query( + `UPDATE purchase_requests SET item_id = ? WHERE request_id = ?`, + [itemId, requestId] + ); + }, + // 삭제 (admin only, pending 상태만) async delete(requestId) { const db = await getDb(); diff --git a/system1-factory/api/services/imageUploadService.js b/system1-factory/api/services/imageUploadService.js index e898f78..b3b9722 100644 --- a/system1-factory/api/services/imageUploadService.js +++ b/system1-factory/api/services/imageUploadService.js @@ -22,7 +22,8 @@ try { // 업로드 디렉토리 설정 (Docker 볼륨 마운트: /usr/src/app/uploads) const UPLOAD_DIRS = { issues: path.join(__dirname, '../uploads/issues'), - equipments: path.join(__dirname, '../uploads/equipments') + equipments: path.join(__dirname, '../uploads/equipments'), + purchase_requests: path.join(__dirname, '../uploads/purchase_requests') }; const UPLOAD_DIR = UPLOAD_DIRS.issues; // 기존 호환성 유지 const MAX_SIZE = { width: 1920, height: 1920 }; diff --git a/system1-factory/web/pages/purchase/request.html b/system1-factory/web/pages/purchase/request.html index 48bf1db..e4a7eb8 100644 --- a/system1-factory/web/pages/purchase/request.html +++ b/system1-factory/web/pages/purchase/request.html @@ -7,6 +7,18 @@ + +
@@ -41,12 +53,13 @@

신규 구매신청

-
-
+
+
- + + + +
+ +
@@ -64,6 +87,21 @@
+ +
+ +
+ + + +
+
-
+
+
+ +
@@ -175,7 +221,7 @@
- - + + diff --git a/system1-factory/web/static/js/purchase-request.js b/system1-factory/web/static/js/purchase-request.js index 9938835..c0862f9 100644 --- a/system1-factory/web/static/js/purchase-request.js +++ b/system1-factory/web/static/js/purchase-request.js @@ -5,6 +5,8 @@ const TKUSER_BASE_URL = location.hostname.includes('technicalkorea.net') const CAT_LABELS = { consumable: '소모품', safety: '안전용품', repair: '수선비', equipment: '설비' }; const CAT_COLORS = { consumable: 'badge-blue', safety: 'badge-green', repair: 'badge-amber', equipment: 'badge-purple' }; +const CAT_BG = { consumable: '#dbeafe', safety: '#dcfce7', repair: '#fef3c7', equipment: '#f3e8ff' }; +const CAT_FG = { consumable: '#1e40af', safety: '#166534', repair: '#92400e', equipment: '#7e22ce' }; const STATUS_LABELS = { pending: '대기', purchased: '구매완료', hold: '보류' }; const STATUS_COLORS = { pending: 'badge-amber', purchased: 'badge-green', hold: 'badge-gray' }; @@ -14,6 +16,9 @@ let requestsList = []; let currentRequestForPurchase = null; let currentRequestForHold = null; let isAdmin = false; +let photoBase64 = null; +let searchDebounceTimer = null; +let dropdownActiveIndex = -1; async function loadInitialData() { try { @@ -23,45 +28,141 @@ async function loadInitialData() { ]); consumableItems = itemsRes.data || []; vendorsList = vendorsRes.data || []; - populateItemSelect(); populateVendorSelect(); } catch (e) { console.error('초기 데이터 로드 실패:', e); } } -function populateItemSelect() { - const sel = document.getElementById('prItemSelect'); - const groups = {}; - consumableItems.forEach(item => { - const cat = CAT_LABELS[item.category] || item.category; - if (!groups[cat]) groups[cat] = []; - groups[cat].push(item); - }); - let html = ''; - for (const [cat, items] of Object.entries(groups)) { - html += ``; - items.forEach(item => { - const maker = item.maker ? ` (${escapeHtml(item.maker)})` : ''; - html += ``; - }); - html += ''; - } - sel.innerHTML = html; -} - function populateVendorSelect() { const sel = document.getElementById('pmVendor'); sel.innerHTML = '' + vendorsList.map(v => ``).join(''); } -function onItemSelect() { - const itemId = parseInt(document.getElementById('prItemSelect').value); - const preview = document.getElementById('prItemPreview'); - const item = consumableItems.find(i => i.item_id === itemId); - if (!item) { preview.classList.add('hidden'); return; } +/* ===== 검색형 품목 선택 ===== */ +function initItemSearch() { + const input = document.getElementById('prItemSearch'); + const dropdown = document.getElementById('prItemDropdown'); + input.addEventListener('input', () => { + clearTimeout(searchDebounceTimer); + searchDebounceTimer = setTimeout(() => { + const query = input.value.trim(); + if (query.length === 0) { + // 빈 입력: 전체 목록 보여주기 + showDropdown(consumableItems.slice(0, 30), ''); + } else { + const lower = query.toLowerCase(); + const filtered = consumableItems.filter(item => + item.item_name.toLowerCase().includes(lower) || + (item.maker && item.maker.toLowerCase().includes(lower)) + ); + showDropdown(filtered, query); + } + }, 200); + }); + + input.addEventListener('focus', () => { + const query = input.value.trim(); + if (query.length === 0) { + showDropdown(consumableItems.slice(0, 30), ''); + } else { + const lower = query.toLowerCase(); + const filtered = consumableItems.filter(item => + item.item_name.toLowerCase().includes(lower) || + (item.maker && item.maker.toLowerCase().includes(lower)) + ); + showDropdown(filtered, query); + } + }); + + input.addEventListener('keydown', (e) => { + const items = dropdown.querySelectorAll('.item-dropdown-item, .item-dropdown-custom'); + if (e.key === 'ArrowDown') { + e.preventDefault(); + dropdownActiveIndex = Math.min(dropdownActiveIndex + 1, items.length - 1); + updateDropdownActive(items); + } else if (e.key === 'ArrowUp') { + e.preventDefault(); + dropdownActiveIndex = Math.max(dropdownActiveIndex - 1, 0); + updateDropdownActive(items); + } else if (e.key === 'Enter') { + e.preventDefault(); + if (dropdownActiveIndex >= 0 && dropdownActiveIndex < items.length) { + items[dropdownActiveIndex].click(); + } + } else if (e.key === 'Escape') { + closeDropdown(); + } + }); + + // 외부 클릭 시 드롭다운 닫기 + document.addEventListener('click', (e) => { + if (!e.target.closest('#prItemSearch') && !e.target.closest('#prItemDropdown')) { + closeDropdown(); + } + }); +} + +function showDropdown(items, query) { + const dropdown = document.getElementById('prItemDropdown'); + dropdownActiveIndex = -1; + + let html = ''; + if (items.length > 0) { + items.forEach((item, idx) => { + const catLabel = CAT_LABELS[item.category] || item.category; + const bg = CAT_BG[item.category] || '#f3f4f6'; + const fg = CAT_FG[item.category] || '#374151'; + const maker = item.maker ? ` (${escapeHtml(item.maker)})` : ''; + html += `
+ ${catLabel} + ${escapeHtml(item.item_name)}${maker} +
`; + }); + } + + // 직접 입력 옵션 (검색어가 있을 때만) + if (query.length > 0) { + html += `
+ + "${escapeHtml(query)}"(으)로 직접 신청 +
`; + } + + if (html) { + dropdown.innerHTML = html; + dropdown.classList.add('open'); + } else { + closeDropdown(); + } +} + +function updateDropdownActive(items) { + items.forEach((el, idx) => { + el.classList.toggle('active', idx === dropdownActiveIndex); + if (idx === dropdownActiveIndex) el.scrollIntoView({ block: 'nearest' }); + }); +} + +function closeDropdown() { + document.getElementById('prItemDropdown').classList.remove('open'); + dropdownActiveIndex = -1; +} + +function selectItem(itemId) { + const item = consumableItems.find(i => i.item_id === itemId); + if (!item) return; + + const input = document.getElementById('prItemSearch'); + input.value = item.item_name + (item.maker ? ' (' + item.maker + ')' : ''); + document.getElementById('prItemId').value = item.item_id; + document.getElementById('prCustomItemName').value = ''; + closeDropdown(); + + // 미리보기 + const preview = document.getElementById('prItemPreview'); preview.classList.remove('hidden'); const photoEl = document.getElementById('prItemPhoto'); if (item.photo_path) { @@ -74,27 +175,114 @@ function onItemSelect() { document.getElementById('prItemInfo').textContent = `${item.item_name} ${item.maker ? '(' + item.maker + ')' : ''}`; const price = item.base_price ? Number(item.base_price).toLocaleString() + '원/' + (item.unit || 'EA') : '기준가 미설정'; document.getElementById('prItemPrice').textContent = price; + + // 분류 선택 숨김 + document.getElementById('prCustomCategoryWrap').classList.add('hidden'); +} + +function selectCustomItem() { + const input = document.getElementById('prItemSearch'); + const customName = input.value.trim(); + if (!customName) return; + + document.getElementById('prItemId').value = ''; + document.getElementById('prCustomItemName').value = customName; + closeDropdown(); + + // 미리보기 숨기고 분류 선택 표시 + document.getElementById('prItemPreview').classList.add('hidden'); + document.getElementById('prCustomCategoryWrap').classList.remove('hidden'); +} + +/* ===== 사진 첨부 ===== */ +async function onPhotoSelected(inputEl) { + const file = inputEl.files[0]; + if (!file) return; + + const statusEl = document.getElementById('prPhotoStatus'); + let processFile = file; + + // HEIC/HEIF 변환 + const isHeic = file.type === 'image/heic' || file.type === 'image/heif' || + file.name.toLowerCase().endsWith('.heic') || file.name.toLowerCase().endsWith('.heif'); + if (isHeic) { + if (typeof heic2any === 'undefined') { + showToast('HEIC 변환 라이브러리를 불러오지 못했습니다.', 'error'); + return; + } + statusEl.textContent = 'HEIC 변환 중...'; + try { + const blob = await heic2any({ blob: file, toType: 'image/jpeg', quality: 0.85 }); + processFile = new File([blob], file.name.replace(/\.heic$/i, '.jpg').replace(/\.heif$/i, '.jpg'), { type: 'image/jpeg' }); + } catch (e) { + console.error('HEIC 변환 실패:', e); + showToast('HEIC 이미지 변환에 실패했습니다.', 'error'); + statusEl.textContent = ''; + return; + } + } + + // 파일 크기 확인 (10MB) + if (processFile.size > 10 * 1024 * 1024) { + showToast('파일 크기는 10MB 이하만 가능합니다.', 'error'); + return; + } + + statusEl.textContent = '처리 중...'; + + const reader = new FileReader(); + reader.onload = (e) => { + photoBase64 = e.target.result; + document.getElementById('prPhotoPreviewImg').src = photoBase64; + document.getElementById('prPhotoPreview').classList.remove('hidden'); + statusEl.textContent = ''; + }; + reader.readAsDataURL(processFile); +} + +function removePhoto() { + photoBase64 = null; + document.getElementById('prPhotoPreview').classList.add('hidden'); + document.getElementById('prPhotoInput').value = ''; + document.getElementById('prPhotoStatus').textContent = ''; } /* ===== 구매신청 제출 ===== */ async function submitPurchaseRequest() { - const item_id = document.getElementById('prItemSelect').value; + const itemId = document.getElementById('prItemId').value; + const customItemName = document.getElementById('prCustomItemName').value; const quantity = parseInt(document.getElementById('prQuantity').value) || 0; const notes = document.getElementById('prNotes').value.trim(); - if (!item_id) { showToast('소모품을 선택해주세요.', 'error'); return; } + if (!itemId && !customItemName) { showToast('소모품을 선택하거나 품목명을 입력해주세요.', 'error'); return; } if (quantity < 1) { showToast('수량은 1 이상이어야 합니다.', 'error'); return; } + const body = { quantity, notes }; + if (itemId) { + body.item_id = parseInt(itemId); + } else { + body.custom_item_name = customItemName; + body.custom_category = document.getElementById('prCustomCategory').value; + } + if (photoBase64) { + body.photo = photoBase64; + } + try { await api('/purchase-requests', { method: 'POST', - body: JSON.stringify({ item_id: parseInt(item_id), quantity, notes }) + body: JSON.stringify(body) }); showToast('구매신청이 등록되었습니다.'); - document.getElementById('prItemSelect').value = ''; + // 폼 초기화 + document.getElementById('prItemSearch').value = ''; + document.getElementById('prItemId').value = ''; + document.getElementById('prCustomItemName').value = ''; document.getElementById('prQuantity').value = '1'; document.getElementById('prNotes').value = ''; document.getElementById('prItemPreview').classList.add('hidden'); + document.getElementById('prCustomCategoryWrap').classList.add('hidden'); + removePhoto(); await loadRequests(); } catch (e) { showToast(e.message, 'error'); } } @@ -122,11 +310,22 @@ function renderRequests() { return; } tbody.innerHTML = requestsList.map(r => { - const catLabel = CAT_LABELS[r.category] || r.category; - const catColor = CAT_COLORS[r.category] || 'badge-gray'; + // 등록 품목이면 ci 데이터, 미등록이면 custom 데이터 사용 + const itemName = r.item_name || r.custom_item_name || '-'; + const category = r.category || r.custom_category; + const catLabel = CAT_LABELS[category] || category || '-'; + const catColor = CAT_COLORS[category] || 'badge-gray'; const statusLabel = STATUS_LABELS[r.status] || r.status; const statusColor = STATUS_COLORS[r.status] || 'badge-gray'; - const photoSrc = r.photo_path ? TKUSER_BASE_URL + r.photo_path : ''; + const isCustom = !r.item_id && r.custom_item_name; + + // 사진: 구매신청 첨부 사진 우선, 없으면 소모품 마스터 사진 + let photoSrc = ''; + if (r.pr_photo_path) { + photoSrc = r.pr_photo_path; + } else if (r.ci_photo_path) { + photoSrc = TKUSER_BASE_URL + r.ci_photo_path; + } let actions = ''; if (isAdmin && r.status === 'pending') { @@ -144,7 +343,7 @@ function renderRequests() {
${photoSrc ? `` : ''}
-
${escapeHtml(r.item_name)}
+
${escapeHtml(itemName)}${isCustom ? ' (직접입력)' : ''}
${escapeHtml(r.maker || '')}
@@ -168,16 +367,31 @@ function openPurchaseModal(requestId) { if (!r) return; currentRequestForPurchase = r; + const itemName = r.item_name || r.custom_item_name || '-'; + const category = r.category || r.custom_category; + const isCustom = !r.item_id && r.custom_item_name; const basePrice = r.base_price ? Number(r.base_price).toLocaleString() + '원' : '-'; + document.getElementById('purchaseModalInfo').innerHTML = ` -
${escapeHtml(r.item_name)} ${r.maker ? '(' + escapeHtml(r.maker) + ')' : ''}
-
분류: ${CAT_LABELS[r.category] || r.category} | 기준가: ${basePrice} | 신청수량: ${r.quantity}
+
${escapeHtml(itemName)} ${r.maker ? '(' + escapeHtml(r.maker) + ')' : ''}${isCustom ? ' (직접입력)' : ''}
+
분류: ${CAT_LABELS[category] || category || '-'} | 기준가: ${basePrice} | 신청수량: ${r.quantity}
+ ${r.pr_photo_path ? `` : ''} `; document.getElementById('pmUnitPrice').value = r.base_price || ''; document.getElementById('pmQuantity').value = r.quantity; document.getElementById('pmDate').value = new Date().toISOString().substring(0, 10); document.getElementById('pmNotes').value = ''; document.getElementById('pmPriceDiffArea').innerHTML = ''; + + // 마스터 등록 체크박스: 미등록 품목일 때만 표시 + const masterWrap = document.getElementById('pmMasterRegisterWrap'); + if (isCustom) { + masterWrap.classList.remove('hidden'); + document.getElementById('pmRegisterToMaster').checked = true; + } else { + masterWrap.classList.add('hidden'); + } + document.getElementById('purchaseModal').classList.remove('hidden'); showPriceDiff(); } @@ -218,14 +432,18 @@ async function submitPurchase() { if (!purchase_date) { showToast('구매일을 입력해주세요.', 'error'); return; } const updateCheckbox = document.getElementById('pmUpdateBasePrice'); + const registerCheckbox = document.getElementById('pmRegisterToMaster'); + const isCustom = !currentRequestForPurchase.item_id && currentRequestForPurchase.custom_item_name; + const body = { request_id: currentRequestForPurchase.request_id, - item_id: currentRequestForPurchase.item_id, + item_id: currentRequestForPurchase.item_id || null, vendor_id: parseInt(document.getElementById('pmVendor').value) || null, quantity: parseInt(document.getElementById('pmQuantity').value) || currentRequestForPurchase.quantity, unit_price, purchase_date, update_base_price: updateCheckbox ? updateCheckbox.checked : false, + register_to_master: isCustom ? (registerCheckbox ? registerCheckbox.checked : true) : undefined, notes: document.getElementById('pmNotes').value.trim() }; @@ -289,6 +507,7 @@ async function deleteRequest(requestId) { (async function() { if (!await initAuth()) return; isAdmin = currentUser && ['admin', 'system', 'system admin'].includes(currentUser.role); + initItemSearch(); await loadInitialData(); await loadRequests(); })();