diff --git a/backend/templates/admin_sales_pos.html b/backend/templates/admin_sales_pos.html index c546e5d..71aedc4 100644 --- a/backend/templates/admin_sales_pos.html +++ b/backend/templates/admin_sales_pos.html @@ -380,6 +380,12 @@ border-radius: 6px; background: var(--bg-secondary); flex-shrink: 0; + cursor: pointer; + transition: transform 0.15s, box-shadow 0.15s; + } + .product-thumb:hover { + transform: scale(1.1); + box-shadow: 0 4px 12px rgba(139,92,246,0.3); } .product-thumb-placeholder { width: 36px; @@ -391,6 +397,12 @@ border-radius: 6px; flex-shrink: 0; border: 1px solid rgba(255,255,255,0.05); + cursor: pointer; + transition: transform 0.15s, border-color 0.15s; + } + .product-thumb-placeholder:hover { + transform: scale(1.1); + border-color: var(--accent-purple); } .product-thumb-placeholder svg { width: 18px; @@ -398,6 +410,117 @@ opacity: 0.3; fill: #888; } + + /* 이미지 교체 모달 */ + .image-modal { + display: none; + position: fixed; + top: 0; left: 0; right: 0; bottom: 0; + background: rgba(0,0,0,0.8); + z-index: 1000; + align-items: center; + justify-content: center; + backdrop-filter: blur(4px); + } + .image-modal.show { display: flex; } + + .image-modal-content { + background: #1a1a3e; + border-radius: 16px; + padding: 24px; + max-width: 450px; + width: 90%; + max-height: 90vh; + overflow-y: auto; + border: 1px solid rgba(139,92,246,0.3); + } + .image-modal-content h3 { + margin: 0 0 16px 0; + color: var(--accent-purple); + font-size: 18px; + } + .image-modal-tabs { + display: flex; + gap: 8px; + margin-bottom: 16px; + } + .tab-btn { + flex: 1; + padding: 10px; + border: 1px solid rgba(255,255,255,0.1); + background: transparent; + color: var(--text-secondary); + border-radius: 8px; + cursor: pointer; + transition: all 0.2s; + } + .tab-btn.active { + background: var(--accent-purple); + color: white; + border-color: var(--accent-purple); + } + .tab-content { display: none; } + .tab-content.active { display: block; } + + .image-input { + width: 100%; + padding: 12px; + background: var(--bg-secondary); + border: 1px solid rgba(255,255,255,0.1); + border-radius: 8px; + color: var(--text-primary); + margin-bottom: 12px; + } + .image-input:focus { + outline: none; + border-color: var(--accent-purple); + } + + .camera-container { + position: relative; + width: 100%; + aspect-ratio: 1; + background: #000; + border-radius: 8px; + overflow: hidden; + margin-bottom: 12px; + } + .camera-container video, + .camera-container canvas { + width: 100%; + height: 100%; + object-fit: cover; + } + .camera-guide { + position: absolute; + top: 0; left: 0; right: 0; bottom: 0; + pointer-events: none; + } + + .modal-btns { + display: flex; + gap: 8px; + justify-content: flex-end; + } + .btn-modal { + padding: 10px 20px; + border: none; + border-radius: 8px; + cursor: pointer; + font-weight: 500; + transition: all 0.2s; + } + .btn-modal.secondary { + background: var(--bg-secondary); + color: var(--text-primary); + } + .btn-modal.primary { + background: var(--accent-purple); + color: white; + } + .btn-modal:hover { + transform: translateY(-1px); + } .product-info { display: flex; flex-direction: column; @@ -831,8 +954,8 @@
${item.thumbnail - ? `` - : `
` + ? `` + : `
` }
${escapeHtml(item.product_name)} @@ -867,8 +990,8 @@
${item.thumbnail - ? `` - : `
` + ? `` + : `
` }
${escapeHtml(item.product_name)} @@ -943,6 +1066,288 @@ // 초기 로드 loadSalesData(); + + // ──────────────── 이미지 교체 모달 ──────────────── + let imgModalBarcode = null; + let imgModalDrugCode = null; + let imgModalName = null; + let cameraStream = null; + let capturedImageData = null; + + function openImageModal(barcode, drugCode, productName) { + // 바코드나 drug_code 중 하나는 있어야 함 + if (!barcode && !drugCode) { + showToast('제품 코드 정보가 없습니다', 'error'); + return; + } + + imgModalBarcode = barcode || null; + imgModalDrugCode = drugCode || null; + imgModalName = productName || (barcode || drugCode); + + document.getElementById('imgModalProductName').textContent = imgModalName; + document.getElementById('imgModalCode').textContent = barcode || drugCode; + document.getElementById('imgUrlInput').value = ''; + + // URL 탭으로 초기화 + switchImageTab('url'); + + document.getElementById('imageModal').classList.add('show'); + document.getElementById('imgUrlInput').focus(); + } + + function closeImageModal() { + stopCamera(); + document.getElementById('imageModal').classList.remove('show'); + imgModalBarcode = null; + imgModalDrugCode = null; + imgModalName = null; + capturedImageData = null; + } + + function switchImageTab(tab) { + document.querySelectorAll('.image-modal .tab-btn').forEach(btn => { + btn.classList.toggle('active', btn.dataset.tab === tab); + }); + document.querySelectorAll('.image-modal .tab-content').forEach(content => { + content.classList.toggle('active', content.id === 'tab' + tab.charAt(0).toUpperCase() + tab.slice(1)); + }); + + if (tab === 'camera') { + startCamera(); + } else { + stopCamera(); + } + } + + async function startCamera() { + try { + stopCamera(); + + const constraints = { + video: { + facingMode: { ideal: 'environment' }, + width: { ideal: 1920 }, + height: { ideal: 1920 } + }, + audio: false + }; + + cameraStream = await navigator.mediaDevices.getUserMedia(constraints); + const video = document.getElementById('cameraVideo'); + video.srcObject = cameraStream; + video.style.display = 'block'; + + document.getElementById('captureCanvas').style.display = 'none'; + document.getElementById('cameraGuide').style.display = 'block'; + document.getElementById('captureBtn').style.display = 'block'; + document.getElementById('previewBtns').style.display = 'none'; + capturedImageData = null; + + } catch (err) { + console.error('카메라 오류:', err); + showToast('카메라에 접근할 수 없습니다', 'error'); + } + } + + function stopCamera() { + if (cameraStream) { + cameraStream.getTracks().forEach(track => track.stop()); + cameraStream = null; + } + const video = document.getElementById('cameraVideo'); + if (video) video.srcObject = null; + } + + function capturePhoto() { + const video = document.getElementById('cameraVideo'); + const canvas = document.getElementById('captureCanvas'); + const ctx = canvas.getContext('2d'); + + const vw = video.videoWidth; + const vh = video.videoHeight; + const minDim = Math.min(vw, vh); + const cropSize = minDim * 0.8; + const sx = (vw - cropSize) / 2; + const sy = (vh - cropSize) / 2; + + canvas.width = 800; + canvas.height = 800; + ctx.drawImage(video, sx, sy, cropSize, cropSize, 0, 0, 800, 800); + + capturedImageData = canvas.toDataURL('image/jpeg', 0.92); + + video.style.display = 'none'; + canvas.style.display = 'block'; + document.getElementById('cameraGuide').style.display = 'none'; + document.getElementById('captureBtn').style.display = 'none'; + document.getElementById('previewBtns').style.display = 'flex'; + } + + function retakePhoto() { + const video = document.getElementById('cameraVideo'); + const canvas = document.getElementById('captureCanvas'); + + video.style.display = 'block'; + canvas.style.display = 'none'; + document.getElementById('cameraGuide').style.display = 'block'; + document.getElementById('captureBtn').style.display = 'block'; + document.getElementById('previewBtns').style.display = 'none'; + capturedImageData = null; + } + + async function submitCapturedImage() { + if (!capturedImageData) { + showToast('촬영된 이미지가 없습니다', 'error'); + return; + } + + const code = imgModalBarcode || imgModalDrugCode; + const name = imgModalName; + const imageData = capturedImageData; + + closeImageModal(); + showToast(`"${name}" 이미지 저장 중...`, 'info'); + + try { + const res = await fetch(`/api/admin/product-images/${code}/upload`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + image_data: imageData, + product_name: name, + drug_code: imgModalDrugCode + }) + }); + const data = await res.json(); + + if (data.success) { + showToast('✅ 이미지 저장 완료!', 'success'); + loadSalesData(); // 새로고침 + } else { + showToast(data.error || '저장 실패', 'error'); + } + } catch (err) { + showToast('오류: ' + err.message, 'error'); + } + } + + async function submitImageUrl() { + const imageUrl = document.getElementById('imgUrlInput').value.trim(); + + if (!imageUrl) { + showToast('이미지 URL을 입력하세요', 'error'); + return; + } + + if (!imageUrl.startsWith('http')) { + showToast('올바른 URL을 입력하세요', 'error'); + return; + } + + const code = imgModalBarcode || imgModalDrugCode; + const name = imgModalName; + + closeImageModal(); + showToast(`"${name}" 이미지 다운로드 중...`, 'info'); + + try { + const res = await fetch(`/api/admin/product-images/${code}/replace`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + image_url: imageUrl, + product_name: name, + drug_code: imgModalDrugCode + }) + }); + const data = await res.json(); + + if (data.success) { + showToast('✅ 이미지 등록 완료!', 'success'); + loadSalesData(); // 새로고침 + } else { + showToast(data.error || '등록 실패', 'error'); + } + } catch (err) { + showToast('오류: ' + err.message, 'error'); + } + } + + function showToast(message, type = 'info') { + const toast = document.createElement('div'); + toast.style.cssText = ` + position: fixed; + bottom: 24px; + left: 50%; + transform: translateX(-50%); + padding: 12px 24px; + background: ${type === 'success' ? '#10b981' : type === 'error' ? '#ef4444' : '#6366f1'}; + color: white; + border-radius: 8px; + font-size: 14px; + z-index: 2000; + animation: fadeIn 0.3s; + `; + toast.textContent = message; + document.body.appendChild(toast); + setTimeout(() => toast.remove(), 3000); + } + + // 모달 외부 클릭시 닫기 + document.getElementById('imageModal').addEventListener('click', e => { + if (e.target.id === 'imageModal') closeImageModal(); + }); + + +
+
+

📷 제품 이미지 등록

+
+
제품명
+
코드
+
+ +
+ + +
+ + +
+ + +
+ + +
+
+ + +
+ + + + + + + +
+
+ + +
+
+