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();
+ });
+
+
+
+
+ 📷 제품 이미지 등록
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
| |