From 96a3df8470cbc9d70af2e79060890312db05fef4 Mon Sep 17 00:00:00 2001 From: thug0bin Date: Wed, 4 Mar 2026 14:31:21 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=A0=9C=ED=92=88=20=EA=B2=80=EC=83=89?= =?UTF-8?q?=EC=97=90=20=EC=9C=84=EC=B9=98=20=EC=BB=AC=EB=9F=BC=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20(=EC=9D=B8=EC=BD=94=EB=94=A9=20=EC=88=98=EC=A0=95)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CD_item_position.CD_NM_sale 조회 - Edit 도구로만 수정하여 인코딩 유지 - 위치 뱃지 스타일 (노란색 배경) --- backend/templates/admin_products.html | 281 +++++++++++++------------- 1 file changed, 140 insertions(+), 141 deletions(-) diff --git a/backend/templates/admin_products.html b/backend/templates/admin_products.html index af16793..fd0166e 100644 --- a/backend/templates/admin_products.html +++ b/backend/templates/admin_products.html @@ -1,9 +1,9 @@ - + - ?�품 검??- �?��?�국 + 제품 검색 - 청춘약국 @@ -16,7 +16,7 @@ color: #1e293b; } - /* ?�?� ?�더 ?�?� */ + /* ── 헤더 ── */ .header { background: linear-gradient(135deg, #7c3aed 0%, #8b5cf6 50%, #a78bfa 100%); padding: 28px 32px 24px; @@ -46,14 +46,14 @@ opacity: 0.85; } - /* ?�?� 컨텐�??�?� */ + /* ── 컨텐츠 ── */ .content { max-width: 1100px; margin: 0 auto; padding: 24px 20px 60px; } - /* ?�?� ?�로??챗봇 ?�?� */ + /* ── 플로팅 챗봇 ── */ .chatbot-panel { position: fixed; right: 24px; @@ -224,7 +224,7 @@ 30% { transform: translateY(-6px); opacity: 1; } } - /* ?�?� 챗봇 ?��? 버튼 (??�� ?�시) ?�?� */ + /* ── 챗봇 토글 버튼 (항상 표시) ── */ .chatbot-toggle { position: fixed; bottom: 24px; @@ -253,7 +253,7 @@ box-shadow: 0 4px 20px rgba(239, 68, 68, 0.4); } - /* 모바??*/ + /* 모바일 */ @media (max-width: 640px) { .chatbot-panel { right: 0; @@ -266,7 +266,7 @@ .chatbot-toggle { bottom: 16px; right: 16px; } } - /* ?�?� 검???�역 ?�?� */ + /* ── 검색 영역 ── */ .search-section { background: #fff; border-radius: 14px; @@ -320,7 +320,7 @@ margin-right: 8px; } - /* ?�?� 결과 카운???�?� */ + /* ── 결과 카운트 ── */ .result-count { margin-bottom: 16px; font-size: 14px; @@ -331,7 +331,7 @@ font-weight: 700; } - /* ?�?� ?�이�??�?� */ + /* ── 테이블 ── */ .table-wrap { background: #fff; border-radius: 14px; @@ -362,7 +362,7 @@ tbody tr:hover { background: #faf5ff; } tbody tr:last-child td { border-bottom: none; } - /* ?�?� ?�품 ?�보 ?�?� */ + /* ── 상품 정보 ── */ .product-name { font-weight: 600; color: #1e293b; @@ -377,7 +377,7 @@ font-weight: 500; } - /* ?�?� 코드/바코???�?� */ + /* ── 코드/바코드 ── */ .code { font-family: 'JetBrains Mono', monospace; font-size: 12px; @@ -407,13 +407,13 @@ font-weight: 500; } - /* ?�?� 가�??�?� */ + /* ── 가격 ── */ .price { font-weight: 600; color: #1e293b; white-space: nowrap; } - /* ?�?� ?�고 ?�?� */ + /* ── 재고 ── */ .stock { font-weight: 600; white-space: nowrap; @@ -422,7 +422,7 @@ .stock.in-stock { color: #10b981; } .stock.out-stock { color: #ef4444; } - /* ?�?� QR 버튼 ?�?� */ + /* ── QR 버튼 ── */ .btn-qr { background: #8b5cf6; color: #fff; @@ -438,7 +438,7 @@ .btn-qr:hover { background: #7c3aed; } .btn-qr:active { transform: scale(0.95); } - /* ?�?� �??�태 ?�?� */ + /* ── 빈 상태 ── */ .empty-state { text-align: center; padding: 60px 20px; @@ -452,7 +452,7 @@ font-size: 15px; } - /* ?�?� 모달 ?�?� */ + /* ── 모달 ── */ .modal-overlay { display: none; position: fixed; @@ -488,7 +488,7 @@ border-radius: 8px; } - /* ?�?� ?�량 ?�택�??�?� */ + /* ── 수량 선택기 ── */ .qty-selector { display: flex; align-items: center; @@ -556,7 +556,7 @@ .modal-btn.confirm { background: #8b5cf6; color: #fff; } .modal-btn.confirm:hover { background: #7c3aed; } - /* ?�?� ?�품 ?��?지 ?�?� */ + /* ── 제품 이미지 ── */ .product-thumb { width: 40px; height: 40px; @@ -592,7 +592,7 @@ fill: #94a3b8; } - /* ?�?� ?��?지 모달 ?�?� */ + /* ── 이미지 모달 ── */ .image-modal { display: none; position: fixed; @@ -679,7 +679,7 @@ .img-modal-btn.secondary { background: #f1f5f9; color: #64748b; } .img-modal-btn.primary { background: #8b5cf6; color: #fff; } - /* ?�?� 반응???�?� */ + /* ── 반응형 ── */ @media (max-width: 768px) { .search-box { flex-direction: column; } .table-wrap { overflow-x: auto; } @@ -690,40 +690,40 @@
-

?�� ?�품 검??/h1> -

?�체 ?�품 검??· QR ?�벨 ?�쇄 · ?�� ?�물??AI ?�담

+

🔍 제품 검색

+

전체 제품 검색 · QR 라벨 인쇄 · 🐾 동물약 AI 상담

- +
- ?�시 ?�?�레?�, 벤포?�워, 8806418067510, LB000001423 + 예시 타이레놀, 벤포파워, 8806418067510, LB000001423
@@ -732,28 +732,28 @@
- - - - - + + + + + + + @@ -761,51 +761,51 @@ - +
-

?�� ?�물??AI ?�담

-

?�장?�상�? ?��?기생�? 구충????무엇?�든 물어보세??/p> +

🐾 동물약 AI 상담

+

심장사상충, 외부기생충, 구충제 등 무엇이든 물어보세요

- - - + + +
- ?�녕?�세?? ?�� ?�물???�담 AI?�니??

- 반려?�물???�장?�상�??�방, 벼룩/진드�??�방, 구충??/strong> ?�에 ?�??무엇?�든 물어보세?? + 안녕하세요! 🐾 동물약 상담 AI입니다.

+ 반려동물의 심장사상충 예방, 벼룩/진드기 예방, 구충제 등에 대해 무엇이든 물어보세요!
- -
- - + + - + @@ -819,7 +819,7 @@ function formatPrice(num) { if (!num) return '-'; - return new Intl.NumberFormat('ko-KR').format(num) + '??; + return new Intl.NumberFormat('ko-KR').format(num) + '원'; } function escapeHtml(str) { @@ -831,20 +831,20 @@ const search = document.getElementById('searchInput').value.trim(); const animalOnly = document.getElementById('animalOnly').checked; - // ?�물?�만 체크??검?�어 ?�어???�체 조회 가?? + // 동물약만 체크시 검색어 없어도 전체 조회 가능 if (!animalOnly) { if (!search) { - alert('검?�어�??�력?�세??); + alert('검색어를 입력하세요'); return; } if (search.length < 2) { - alert('2글???�상 ?�력?�세??); + alert('2글자 이상 입력하세요'); return; } } const tbody = document.getElementById('productsTableBody'); - tbody.innerHTML = ''; + tbody.innerHTML = ''; const inStockOnly = document.getElementById('inStockOnly').checked; let url = `/api/products?search=${encodeURIComponent(search)}`; @@ -859,11 +859,11 @@ document.getElementById('resultNum').textContent = productsData.length; renderTable(); } else { - tbody.innerHTML = ``; + tbody.innerHTML = ``; } }) .catch(err => { - tbody.innerHTML = ''; + tbody.innerHTML = ''; }); } @@ -871,18 +871,18 @@ const tbody = document.getElementById('productsTableBody'); if (productsData.length === 0) { - tbody.innerHTML = ''; + tbody.innerHTML = ''; return; } tbody.innerHTML = productsData.map((item, idx) => { - // 분류 뱃�? (?�물?�만) + // 분류 뱃지 (동물약만) const categoryBadge = item.category ? `${escapeHtml(item.category)}` : ''; - // ?�매???�고 ?�시 (?�물?�만) + // 도매상 재고 표시 (동물약만) const wsStock = (item.wholesaler_stock && item.wholesaler_stock > 0) - ? `(?�매 ${item.wholesaler_stock})` + ? `(도매 ${item.wholesaler_stock})` : ''; return ` @@ -896,7 +896,7 @@ - + : `없음`} + `}).join(''); } - // ?�?� QR ?�쇄 관???�?� + // ── QR 인쇄 관련 ── function adjustQty(delta) { printQty = Math.max(MIN_QTY, Math.min(MAX_QTY, printQty + delta)); updateQtyUI(); @@ -925,7 +925,7 @@ document.getElementById('qtyValue').textContent = printQty; document.getElementById('qtyMinus').disabled = printQty <= MIN_QTY; document.getElementById('qtyPlus').disabled = printQty >= MAX_QTY; - document.getElementById('printBtn').textContent = printQty > 1 ? `${printQty}???�쇄` : '?�쇄'; + document.getElementById('printBtn').textContent = printQty > 1 ? `${printQty}장 인쇄` : '인쇄'; } function printQR(idx) { @@ -936,12 +936,12 @@ const preview = document.getElementById('qrPreview'); const info = document.getElementById('qrInfo'); - preview.innerHTML = '

미리보기 로딩 �?..

'; + preview.innerHTML = '

미리보기 로딩 중...

'; info.innerHTML = ` ${escapeHtml(selectedItem.product_name)}
- 바코?? ${selectedItem.barcode || selectedItem.drug_code || 'N/A'}
- 가�? ${formatPrice(selectedItem.sale_price)} + 바코드: ${selectedItem.barcode || selectedItem.drug_code || 'N/A'}
+ 가격: ${formatPrice(selectedItem.sale_price)}
`; updateQtyUI(); @@ -962,11 +962,11 @@ if (data.success && data.image) { preview.innerHTML = `QR 미리보기`; } else { - preview.innerHTML = '

미리보기 ?�패

'; + preview.innerHTML = '

미리보기 실패

'; } }) .catch(() => { - preview.innerHTML = '

미리보기 ?�류

'; + preview.innerHTML = '

미리보기 오류

'; }); } @@ -987,7 +987,7 @@ let errorMsg = ''; for (let i = 0; i < totalQty; i++) { - btn.textContent = `?�쇄 �?.. (${i + 1}/${totalQty})`; + btn.textContent = `인쇄 중... (${i + 1}/${totalQty})`; try { const res = await fetch('/api/qr-print', { @@ -1005,7 +1005,7 @@ if (data.success) { successCount++; } else { - errorMsg = data.error || '?????�는 ?�류'; + errorMsg = data.error || '알 수 없는 오류'; break; } @@ -1022,21 +1022,21 @@ updateQtyUI(); if (successCount === totalQty) { - alert(`??QR ?�벨 ${totalQty}???�쇄 ?�료!`); + alert(`✅ QR 라벨 ${totalQty}장 인쇄 완료!`); closeQRModal(); } else if (successCount > 0) { - alert(`?�️ ${successCount}/${totalQty}???�쇄 ?�료\n?�류: ${errorMsg}`); + alert(`⚠️ ${successCount}/${totalQty}장 인쇄 완료\n오류: ${errorMsg}`); } else { - alert(`???�쇄 ?�패: ${errorMsg}`); + alert(`❌ 인쇄 실패: ${errorMsg}`); } } - // ?�이지 로드 ??검?�창 ?�커?? + // 페이지 로드 시 검색창 포커스 document.getElementById('searchInput').focus(); - // ?�═?�═?�═?�═?�═?�═?�═?�═?�═?�═?�═?�═?�═?�═?�═?�═?�═?�═?�═?�═?�═?�═?�═?�═?�═?�═?�═?�═?�═?�═?�═?�═?�═ - // ?�물??챗봇 - // ?�═?�═?�═?�═?�═?�═?�═?�═?�═?�═?�═?�═?�═?�═?�═?�═?�═?�═?�═?�═?�═?�═?�═?�═?�═?�═?�═?�═?�═?�═?�═?�═?�═ + // ══════════════════════════════════════════════════════════════════ + // 동물약 챗봇 + // ══════════════════════════════════════════════════════════════════ let chatHistory = []; let isChatLoading = false; @@ -1045,7 +1045,7 @@ const btn = document.getElementById('chatbotToggle'); const isOpen = panel.classList.toggle('open'); btn.classList.toggle('active', isOpen); - btn.innerHTML = isOpen ? '?? : '?��'; + btn.innerHTML = isOpen ? '✕' : '🐾'; if (isOpen) { document.getElementById('chatInput').focus(); } @@ -1062,14 +1062,14 @@ if (!message || isChatLoading) return; - // ?�용??메시지 ?�시 + // 사용자 메시지 표시 addChatMessage('user', message); input.value = ''; - // ?�스?�리??추�? + // 히스토리에 추가 chatHistory.push({ role: 'user', content: message }); - // 로딩 ?�시 + // 로딩 표시 isChatLoading = true; document.getElementById('chatSendBtn').disabled = true; showTypingIndicator(); @@ -1086,22 +1086,22 @@ hideTypingIndicator(); if (data.success) { - // AI ?�답 ?�시 + // AI 응답 표시 addChatMessage('assistant', data.message, data.products); - // ?�스?�리??추�? + // 히스토리에 추가 chatHistory.push({ role: 'assistant', content: data.message }); - // ?�스?�리 길이 ?�한 (최근 20�? + // 히스토리 길이 제한 (최근 20개) if (chatHistory.length > 20) { chatHistory = chatHistory.slice(-20); } } else { - addChatMessage('system', '?�️ ' + (data.message || '?�류가 발생?�습?�다')); + addChatMessage('system', '⚠️ ' + (data.message || '오류가 발생했습니다')); } } catch (error) { hideTypingIndicator(); - addChatMessage('system', '?�️ ?�트?�크 ?�류가 발생?�습?�다'); + addChatMessage('system', '⚠️ 네트워크 오류가 발생했습니다'); } isChatLoading = false; @@ -1113,19 +1113,19 @@ const msgDiv = document.createElement('div'); msgDiv.className = `chat-message ${role}`; - // 줄바�?처리 + // 줄바꿈 처리 let htmlContent = escapeHtml(content).replace(/\n/g, '
'); - // 마크?�운 굵게 처리 + // 마크다운 굵게 처리 htmlContent = htmlContent.replace(/\*\*(.+?)\*\*/g, '$1'); msgDiv.innerHTML = htmlContent; - // ?�급???�품 ?�시 (?��?지 ?�함) + // 언급된 제품 표시 (이미지 포함) if (products && products.length > 0) { const productsDiv = document.createElement('div'); productsDiv.className = 'products-mentioned'; - productsDiv.innerHTML = '?�� 관???�품:'; + productsDiv.innerHTML = '📦 관련 제품:'; const productsGrid = document.createElement('div'); productsGrid.style.cssText = 'display:flex;flex-wrap:wrap;gap:8px;margin-top:8px;'; @@ -1136,7 +1136,7 @@ card.style.cssText = 'display:flex;align-items:center;gap:8px;padding:8px;background:#f8fafc;border-radius:8px;cursor:pointer;border:1px solid #e2e8f0;'; card.onclick = () => searchProductFromChat(p.name); - // ?��?지 컨테?�너 + // 이미지 컨테이너 const imgContainer = document.createElement('div'); imgContainer.style.cssText = 'width:40px;height:40px;flex-shrink:0;'; @@ -1146,25 +1146,25 @@ img.src = p.image_url; img.alt = p.name; img.onerror = function() { - // ?��?지 로드 ?�패 ???�이콘으�??��? - imgContainer.innerHTML = '
?��
'; + // 이미지 로드 실패 시 아이콘으로 대체 + imgContainer.innerHTML = '
💊
'; }; imgContainer.appendChild(img); } else { - // ?��?지 ?�으�??�이�? - imgContainer.innerHTML = '
?��
'; + // 이미지 없으면 아이콘 + imgContainer.innerHTML = '
💊
'; } - // ?�스??(카테고리 뱃�? + ?�국/?�매 ?�고) + // 텍스트 (카테고리 뱃지 + 약국/도매 재고) const textDiv = document.createElement('div'); const pharmacyStock = p.stock || 0; const wholesalerStock = p.wholesaler_stock || 0; const stockColor = (pharmacyStock > 0) ? '#10b981' : '#ef4444'; - const pharmacyText = (pharmacyStock > 0) ? `?�국 ${pharmacyStock}` : '?�절'; - const wholesalerText = (wholesalerStock > 0) ? `?�매 ${wholesalerStock}` : ''; + const pharmacyText = (pharmacyStock > 0) ? `약국 ${pharmacyStock}` : '품절'; + const wholesalerText = (wholesalerStock > 0) ? `도매 ${wholesalerStock}` : ''; const stockDisplay = wholesalerText ? `${pharmacyText} / ${wholesalerText}` : pharmacyText; - // 카테고리 뱃�? + // 카테고리 뱃지 const categoryBadge = p.category ? `${p.category}` : ''; @@ -1200,18 +1200,18 @@ } function searchProductFromChat(productName) { - // 챗봇?�서 ?�품 ?�릭 ??검?�창???�력?�고 검?? + // 챗봇에서 제품 클릭 시 검색창에 입력하고 검색 document.getElementById('searchInput').value = productName; document.getElementById('animalOnly').checked = true; searchProducts(); - // 모바?�에??챗봇 ?�기 + // 모바일에서 챗봇 닫기 if (window.innerWidth <= 1100) { document.getElementById('chatbotPanel').classList.remove('open'); } } - // ?�?� ?��?지 ?�록 모달 ?�?� + // ── 이미지 등록 모달 ── let imgModalBarcode = null; let imgModalDrugCode = null; let imgModalName = null; @@ -1220,7 +1220,7 @@ function openImageModal(barcode, drugCode, productName) { if (!barcode && !drugCode) { - alert('?�품 코드 ?�보가 ?�습?�다'); + alert('제품 코드 정보가 없습니다'); return; } @@ -1268,7 +1268,7 @@ document.getElementById('previewBtns').style.display = 'none'; capturedImageData = null; } catch (err) { - alert('카메?�에 ?�근?????�습?�다'); + alert('카메라에 접근할 수 없습니다'); } } @@ -1309,11 +1309,11 @@ } async function submitCapturedImage() { - if (!capturedImageData) { alert('촬영???��?지가 ?�습?�다'); return; } + if (!capturedImageData) { alert('촬영된 이미지가 없습니다'); return; } const code = imgModalBarcode || imgModalDrugCode; const name = imgModalName; closeImageModal(); - showToast(`"${name}" ?��?지 ?�??�?..`); + showToast(`"${name}" 이미지 저장 중...`); try { const res = await fetch(`/api/admin/product-images/${code}/upload`, { method: 'POST', @@ -1321,19 +1321,19 @@ body: JSON.stringify({ image_data: capturedImageData, product_name: name, drug_code: imgModalDrugCode }) }); const data = await res.json(); - if (data.success) { showToast('???��?지 ?�???�료!', 'success'); searchProducts(); } - else showToast(data.error || '?�???�패', 'error'); - } catch (err) { showToast('?�류: ' + err.message, 'error'); } + if (data.success) { showToast('✅ 이미지 저장 완료!', 'success'); searchProducts(); } + else showToast(data.error || '저장 실패', 'error'); + } catch (err) { showToast('오류: ' + err.message, 'error'); } } async function submitImageUrl() { const url = document.getElementById('imgUrlInput').value.trim(); - if (!url) { alert('?��?지 URL???�력?�세??); return; } - if (!url.startsWith('http')) { alert('?�바�?URL???�력?�세??); return; } + if (!url) { alert('이미지 URL을 입력하세요'); return; } + if (!url.startsWith('http')) { alert('올바른 URL을 입력하세요'); return; } const code = imgModalBarcode || imgModalDrugCode; const name = imgModalName; closeImageModal(); - showToast(`"${name}" ?��?지 ?�운로드 �?..`); + showToast(`"${name}" 이미지 다운로드 중...`); try { const res = await fetch(`/api/admin/product-images/${code}/replace`, { method: 'POST', @@ -1341,9 +1341,9 @@ body: JSON.stringify({ image_url: url, product_name: name, drug_code: imgModalDrugCode }) }); const data = await res.json(); - if (data.success) { showToast('???��?지 ?�록 ?�료!', 'success'); searchProducts(); } - else showToast(data.error || '?�록 ?�패', 'error'); - } catch (err) { showToast('?�류: ' + err.message, 'error'); } + if (data.success) { showToast('✅ 이미지 등록 완료!', 'success'); searchProducts(); } + else showToast(data.error || '등록 실패', 'error'); + } catch (err) { showToast('오류: ' + err.message, 'error'); } } function showToast(msg, type = 'info') { @@ -1359,25 +1359,25 @@ }); - +
-

?�� ?�품 ?��?지 ?�록

+

📷 제품 이미지 등록

-
?�품�?/div> +
제품명
코드
- - + +
- +
- +
@@ -1393,15 +1393,14 @@
- +
-
?��?지?�품�?/th> - ?�품코드바코??/th> - ?�치?�고?�매가이미지상품명상품코드바코드위치재고판매가 QR
-
?��
-

?�품�? 바코?? ?�품코드�?검?�하?�요

+
🔍
+

상품명, 바코드, 상품코드로 검색하세요

검??�?..

검색 중...

?�류: ${data.error}

오류: ${data.error}

검???�패

검색 실패

?��

검??결과가 ?�습?�다

📭

검색 결과가 없습니다

${escapeHtml(item.product_name)} - ${item.is_animal_drug ? '?�� ?�물??/span>' : ''} + ${item.is_animal_drug ? '🐾 동물약' : ''} ${categoryBadge}
${escapeHtml(item.supplier) || ''}
@@ -904,18 +904,18 @@
${item.drug_code} ${item.barcode ? `${item.barcode}` - : `?�음`}${item.location ? `${escapeHtml(item.location)}` : ''}${item.location ? `${escapeHtml(item.location)}` : ''} ${item.stock || 0}${wsStock} ${formatPrice(item.sale_price)} - +