pharmacy-pos-qr-system/backend/templates/admin_products.html

1033 lines
36 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>?<3F>품 검??- <20>?<3F><>?<3F></title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Noto Sans KR', -apple-system, BlinkMacSystemFont, sans-serif;
background: #f8fafc;
-webkit-font-smoothing: antialiased;
color: #1e293b;
}
/* ?<3F>?<3F> ?<3F>더 ?<3F>?<3F> */
.header {
background: linear-gradient(135deg, #7c3aed 0%, #8b5cf6 50%, #a78bfa 100%);
padding: 28px 32px 24px;
color: #fff;
}
.header-nav {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.header-nav a {
color: rgba(255,255,255,0.8);
text-decoration: none;
font-size: 14px;
font-weight: 500;
}
.header-nav a:hover { color: #fff; }
.header h1 {
font-size: 24px;
font-weight: 700;
letter-spacing: -0.5px;
margin-bottom: 6px;
}
.header p {
font-size: 14px;
opacity: 0.85;
}
/* ?<3F>?<3F> 컨텐<ECBBA8>??<3F>?<3F> */
.content {
max-width: 1100px;
margin: 0 auto;
padding: 24px 20px 60px;
}
/* ?<3F>?<3F> ?<3F>로??챗봇 ?<3F>?<3F> */
.chatbot-panel {
position: fixed;
right: 24px;
bottom: 90px;
width: 370px;
background: #fff;
border-radius: 20px;
border: 1px solid #e2e8f0;
box-shadow: 0 8px 40px rgba(0,0,0,0.15);
display: none;
flex-direction: column;
height: 520px;
max-height: calc(100vh - 120px);
z-index: 998;
animation: chatSlideUp 0.3s ease;
}
.chatbot-panel.open {
display: flex;
}
@keyframes chatSlideUp {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
.chatbot-header {
padding: 16px 20px;
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
border-radius: 16px 16px 0 0;
color: #fff;
}
.chatbot-header h2 {
font-size: 16px;
font-weight: 700;
display: flex;
align-items: center;
gap: 8px;
}
.chatbot-header p {
font-size: 12px;
opacity: 0.85;
margin-top: 4px;
}
.chatbot-messages {
flex: 1;
overflow-y: auto;
padding: 16px;
display: flex;
flex-direction: column;
gap: 12px;
}
.chat-message {
max-width: 85%;
padding: 12px 16px;
border-radius: 16px;
font-size: 14px;
line-height: 1.5;
word-break: break-word;
}
.chat-message.user {
background: #8b5cf6;
color: #fff;
align-self: flex-end;
border-bottom-right-radius: 4px;
}
.chat-message.assistant {
background: #f1f5f9;
color: #334155;
align-self: flex-start;
border-bottom-left-radius: 4px;
}
.chat-message.system {
background: #fef3c7;
color: #92400e;
align-self: center;
font-size: 12px;
padding: 8px 16px;
}
.chat-message .products-mentioned {
margin-top: 10px;
padding-top: 10px;
border-top: 1px dashed #cbd5e1;
}
.chat-message .product-chip {
display: inline-block;
background: #10b981;
color: #fff;
padding: 4px 10px;
border-radius: 20px;
font-size: 12px;
margin: 2px 4px 2px 0;
cursor: pointer;
}
.chat-message .product-chip:hover {
background: #059669;
}
.chatbot-input {
padding: 16px;
border-top: 1px solid #e2e8f0;
display: flex;
gap: 10px;
}
.chatbot-input input {
flex: 1;
padding: 12px 16px;
border: 2px solid #e2e8f0;
border-radius: 24px;
font-size: 14px;
font-family: inherit;
transition: all 0.2s;
}
.chatbot-input input:focus {
outline: none;
border-color: #10b981;
box-shadow: 0 0 0 4px rgba(16, 185, 129, 0.1);
}
.chatbot-input button {
width: 44px;
height: 44px;
border-radius: 50%;
background: #10b981;
color: #fff;
border: none;
cursor: pointer;
font-size: 18px;
transition: all 0.2s;
display: flex;
align-items: center;
justify-content: center;
}
.chatbot-input button:hover { background: #059669; }
.chatbot-input button:disabled { background: #94a3b8; cursor: not-allowed; }
.chatbot-suggestions {
padding: 12px 16px;
background: #f8fafc;
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.suggestion-chip {
background: #e0e7ff;
color: #4338ca;
padding: 6px 12px;
border-radius: 16px;
font-size: 12px;
cursor: pointer;
border: none;
transition: all 0.2s;
}
.suggestion-chip:hover {
background: #c7d2fe;
}
.typing-indicator {
display: flex;
gap: 4px;
padding: 12px 16px;
align-self: flex-start;
}
.typing-indicator span {
width: 8px;
height: 8px;
background: #94a3b8;
border-radius: 50%;
animation: typing 1.4s infinite;
}
.typing-indicator span:nth-child(2) { animation-delay: 0.2s; }
.typing-indicator span:nth-child(3) { animation-delay: 0.4s; }
@keyframes typing {
0%, 60%, 100% { transform: translateY(0); opacity: 0.4; }
30% { transform: translateY(-6px); opacity: 1; }
}
/* ?<3F>?<3F> 챗봇 ?<3F><>? 버튼 (??<3F><> ?<3F>시) ?<3F>?<3F> */
.chatbot-toggle {
position: fixed;
bottom: 24px;
right: 24px;
width: 60px;
height: 60px;
border-radius: 50%;
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
color: #fff;
border: none;
font-size: 28px;
cursor: pointer;
box-shadow: 0 4px 20px rgba(16, 185, 129, 0.4);
z-index: 999;
display: flex;
align-items: center;
justify-content: center;
transition: transform 0.2s, box-shadow 0.2s;
}
.chatbot-toggle:hover {
transform: scale(1.1);
box-shadow: 0 6px 28px rgba(16, 185, 129, 0.5);
}
.chatbot-toggle.active {
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
box-shadow: 0 4px 20px rgba(239, 68, 68, 0.4);
}
/* 모바??*/
@media (max-width: 640px) {
.chatbot-panel {
right: 0;
bottom: 0;
left: 0;
width: 100%;
height: 75vh;
border-radius: 20px 20px 0 0;
}
.chatbot-toggle { bottom: 16px; right: 16px; }
}
/* ?<3F>?<3F> 검???<3F>역 ?<3F>?<3F> */
.search-section {
background: #fff;
border-radius: 14px;
padding: 24px;
margin-bottom: 20px;
border: 1px solid #e2e8f0;
}
.search-box {
display: flex;
gap: 12px;
}
.search-input {
flex: 1;
padding: 14px 18px;
border: 2px solid #e2e8f0;
border-radius: 12px;
font-size: 16px;
font-family: inherit;
transition: all 0.2s;
}
.search-input:focus {
outline: none;
border-color: #8b5cf6;
box-shadow: 0 0 0 4px rgba(139, 92, 246, 0.1);
}
.search-input::placeholder {
color: #94a3b8;
}
.search-btn {
background: #8b5cf6;
color: #fff;
border: none;
padding: 14px 32px;
border-radius: 12px;
font-size: 15px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.search-btn:hover { background: #7c3aed; }
.search-btn:active { transform: scale(0.98); }
.search-hint {
margin-top: 12px;
font-size: 13px;
color: #94a3b8;
}
.search-hint span {
background: #f1f5f9;
padding: 2px 8px;
border-radius: 4px;
margin-right: 8px;
}
/* ?<3F>?<3F> 결과 카운???<3F>?<3F> */
.result-count {
margin-bottom: 16px;
font-size: 14px;
color: #64748b;
}
.result-count strong {
color: #8b5cf6;
font-weight: 700;
}
/* ?<3F>?<3F> ?<3F><EFBFBD>??<3F>?<3F> */
.table-wrap {
background: #fff;
border-radius: 14px;
border: 1px solid #e2e8f0;
overflow: hidden;
}
table {
width: 100%;
border-collapse: collapse;
}
thead th {
background: #f8fafc;
padding: 14px 16px;
font-size: 12px;
font-weight: 600;
color: #64748b;
text-align: left;
border-bottom: 1px solid #e2e8f0;
white-space: nowrap;
}
tbody td {
padding: 16px;
font-size: 14px;
color: #334155;
border-bottom: 1px solid #f1f5f9;
vertical-align: middle;
}
tbody tr:hover { background: #faf5ff; }
tbody tr:last-child td { border-bottom: none; }
/* ?<3F>?<3F> ?<3F>품 ?<3F>보 ?<3F>?<3F> */
.product-name {
font-weight: 600;
color: #1e293b;
margin-bottom: 2px;
}
.product-supplier {
font-size: 12px;
color: #94a3b8;
}
.product-supplier.set {
color: #8b5cf6;
font-weight: 500;
}
/* ?<3F>?<3F> 코드/바코???<3F>?<3F> */
.code {
font-family: 'JetBrains Mono', monospace;
font-size: 12px;
padding: 4px 8px;
border-radius: 6px;
display: inline-block;
}
.code-drug {
background: #ede9fe;
color: #6d28d9;
}
.code-barcode {
background: #d1fae5;
color: #065f46;
}
.code-na {
background: #f1f5f9;
color: #94a3b8;
}
/* 가격 */
.price {
font-weight: 600;
color: #1e293b;
white-space: nowrap;
}
/* 재고 */
.stock {
font-weight: 600;
white-space: nowrap;
}
.stock.in-stock {
color: #10b981;
}
.stock.out-stock {
color: #ef4444;
}
/* QR 버튼 */
.btn-qr {
background: #8b5cf6;
color: #fff;
border: none;
padding: 8px 14px;
border-radius: 8px;
font-size: 12px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
white-space: nowrap;
}
.btn-qr:hover { background: #7c3aed; }
.btn-qr:active { transform: scale(0.95); }
/* ?<3F>?<3F> <20>??<3F>태 ?<3F>?<3F> */
.empty-state {
text-align: center;
padding: 60px 20px;
color: #94a3b8;
}
.empty-state .icon {
font-size: 48px;
margin-bottom: 16px;
}
.empty-state p {
font-size: 15px;
}
/* ?<3F>?<3F> 모달 ?<3F>?<3F> */
.modal-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.5);
z-index: 1000;
justify-content: center;
align-items: center;
}
.modal-overlay.active { display: flex; }
.modal-box {
background: #fff;
border-radius: 16px;
padding: 24px;
max-width: 400px;
width: 90%;
text-align: center;
}
.modal-title {
font-size: 18px;
font-weight: 700;
margin-bottom: 16px;
}
.modal-preview {
margin: 16px 0;
}
.modal-preview img {
max-width: 200px;
border: 1px solid #e2e8f0;
border-radius: 8px;
}
/* ?<3F>?<3F> ?<3F>량 ?<3F><EFBFBD>??<3F>?<3F> */
.qty-selector {
display: flex;
align-items: center;
justify-content: center;
gap: 0;
margin: 20px 0;
}
.qty-btn {
width: 44px;
height: 44px;
border: none;
background: #f1f5f9;
font-size: 24px;
font-weight: 600;
color: #64748b;
cursor: pointer;
transition: all 0.15s;
display: flex;
align-items: center;
justify-content: center;
user-select: none;
}
.qty-btn:first-child { border-radius: 12px 0 0 12px; }
.qty-btn:last-child { border-radius: 0 12px 12px 0; }
.qty-btn:hover { background: #e2e8f0; color: #334155; }
.qty-btn:active { transform: scale(0.95); background: #cbd5e1; }
.qty-btn:disabled { opacity: 0.4; cursor: not-allowed; }
.qty-value {
width: 64px;
height: 44px;
background: #fff;
border: 2px solid #e2e8f0;
border-left: none;
border-right: none;
font-size: 20px;
font-weight: 700;
color: #1e293b;
display: flex;
align-items: center;
justify-content: center;
}
.qty-label {
font-size: 13px;
color: #64748b;
margin-bottom: 8px;
}
.modal-btns {
display: flex;
gap: 12px;
justify-content: center;
margin-top: 20px;
}
.modal-btn {
padding: 10px 24px;
border-radius: 8px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
border: none;
transition: all 0.15s;
}
.modal-btn.cancel { background: #f1f5f9; color: #64748b; }
.modal-btn.cancel:hover { background: #e2e8f0; }
.modal-btn.confirm { background: #8b5cf6; color: #fff; }
.modal-btn.confirm:hover { background: #7c3aed; }
/* ?<3F>?<3F> 반응???<3F>?<3F> */
@media (max-width: 768px) {
.search-box { flex-direction: column; }
.table-wrap { overflow-x: auto; }
table { min-width: 700px; }
}
</style>
</head>
<body>
<div class="header">
<div class="header-nav">
<a href="/admin">??관리자 ??/a>
<div>
<a href="/admin/sales-detail" style="margin-right: 16px;">?<3F>매 조회</a>
<a href="/admin/sales">?<3F>매 ?<3F></a>
</div>
</div>
<h1>?<3F><> ?<3F>품 검??/h1>
<p>?<3F>체 ?<3F>품 검??· QR ?<3F>벨 ?<3F>쇄 · ?<3F><> ?<3F>물??AI ?<3F></p>
</div>
<div class="content">
<!-- 검??-->
<div class="search-section">
<div class="search-box">
<input type="text" class="search-input" id="searchInput"
placeholder="?<3F><EFBFBD>? 바코?? ?<3F>품코드<ECBD94>?검??.."
onkeypress="if(event.key==='Enter')searchProducts()">
<button class="search-btn" onclick="searchProducts()">?<3F><> 검??/button>
</div>
<div style="display: flex; justify-content: space-between; align-items: center; margin-top: 12px;">
<div class="search-hint">
<span>?<3F></span> ?<3F>?<3F>레?<3F>, 벤포?<3F>워, 8806418067510, LB000001423
</div>
<label style="display: flex; align-items: center; gap: 8px; cursor: pointer; font-size: 14px; color: #475569;">
<input type="checkbox" id="animalOnly" style="width: 18px; height: 18px; accent-color: #10b981; cursor: pointer;">
<span style="display: flex; align-items: center; gap: 4px;">
?<3F><> <strong style="color: #10b981;">?<3F>물?<3F></strong> 보기
</span>
</label>
</div>
</div>
<!-- 결과 -->
<div class="result-count" id="resultCount" style="display:none;">
검??결과: <strong id="resultNum">0</strong><EFBFBD>? </div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th>?<3F><EFBFBD>?/th>
<th>?<3F>품코드</th>
<th>바코??/th>
<th>?<3F></th>
<th>?<3F>매가</th>
<th>QR</th>
</tr>
</thead>
<tbody id="productsTableBody">
<tr>
<td colspan="6" class="empty-state">
<div class="icon">?<3F><></div>
<p>?<3F><EFBFBD>? 바코?? ?<3F>품코드<ECBD94>?검?<3F>하?<3F></p>
</td>
</tr>
</tbody>
</table>
</div>
</div><!-- /.content -->
<!-- ?<3F>물??챗봇 ?<3F>-->
<div class="chatbot-panel" id="chatbotPanel">
<div class="chatbot-header">
<h2>?<3F><> ?<3F>물??AI ?<3F></h2>
<p>?<3F>장?<3F><EFBFBD>? ?<3F><>?기생<EAB8B0>? 구충????무엇?<3F>든 물어보세??/p>
</div>
<div class="chatbot-suggestions">
<button class="suggestion-chip" onclick="sendSuggestion('강아지 ?<3F>장?<3F><EFBFBD>???추천?<3F>줘')">?<3F><> ?<3F>장?<3F>상충약</button>
<button class="suggestion-chip" onclick="sendSuggestion('?<3F>스가?<3F>랑 브라벡토 차이??)">?<3F>스가??vs 브라벡토</button>
<button class="suggestion-chip" onclick="sendSuggestion('고양??벼룩 ??뭐<>? 좋아?')">?<3F><> 고양??벼룩??/button>
<button class="suggestion-chip" onclick="sendSuggestion('강아지 구충??추천')">?<3F><> 구충??/button>
</div>
<div class="chatbot-messages" id="chatMessages">
<div class="chat-message assistant">
?<3F>녕?<3F>세?? ?<3F><> ?<3F>물???<3F>담 AI?<3F>니??<br><br>
반려?<3F>물??<strong>?<3F>장?<3F><EFBFBD>??<3F></strong>, <strong>벼룩/진드<ECA784>??<3F></strong>, <strong>구충??/strong> ?<3F>에 ?<3F>??무엇?<3F>든 물어보세??
</div>
</div>
<div class="chatbot-input">
<input type="text" id="chatInput" placeholder="?? 5kg 강아지 ?<3F>장?<3F><EFBFBD>???추천?<3F>줘"
onkeypress="if(event.key==='Enter')sendChat()">
<button onclick="sendChat()" id="chatSendBtn">??/button>
</div>
</div>
<!-- 챗봇 ?<3F><>? 버튼 -->
<button class="chatbot-toggle" id="chatbotToggle" onclick="toggleChatbot()">?<3F><></button>
<!-- QR ?<3F>쇄 모달 -->
<div class="modal-overlay" id="qrModal" onclick="if(event.target===this)closeQRModal()">
<div class="modal-box">
<div class="modal-title">?<3F><><EFBFBD>?QR ?<3F>벨 ?<3F></div>
<div id="qrInfo" style="margin-bottom:12px;"></div>
<div class="modal-preview" id="qrPreview">
<p style="color:#64748b;">미리보기 로딩 <20>?..</p>
</div>
<div class="qty-label">?<3F>쇄 매수</div>
<div class="qty-selector">
<button class="qty-btn" onclick="adjustQty(-1)" id="qtyMinus">??/button>
<div class="qty-value" id="qtyValue">1</div>
<button class="qty-btn" onclick="adjustQty(1)" id="qtyPlus">+</button>
</div>
<div class="modal-btns">
<button class="modal-btn cancel" onclick="closeQRModal()">취소</button>
<button class="modal-btn confirm" onclick="confirmPrintQR()" id="printBtn">?<3F></button>
</div>
</div>
</div>
<script>
let productsData = [];
let selectedItem = null;
let printQty = 1;
const MAX_QTY = 10;
const MIN_QTY = 1;
function formatPrice(num) {
if (!num) return '-';
return new Intl.NumberFormat('ko-KR').format(num) + '??;
}
function escapeHtml(str) {
if (!str) return '';
return str.replace(/[&<>"']/g, m => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[m]));
}
function searchProducts() {
const search = document.getElementById('searchInput').value.trim();
if (!search) {
alert('검?<3F><EFBFBD>??<3F>력?<3F>세??);
return;
}
if (search.length < 2) {
alert('2???<EFBFBD> ?<EFBFBD>?<EFBFBD>??);
return;
}
const tbody = document.getElementById('productsTableBody');
tbody.innerHTML = '<tr><td colspan="6" class="empty-state"><p>검??<3F>?..</p></td></tr>';
const animalOnly = document.getElementById('animalOnly').checked;
fetch(`/api/products?search=${encodeURIComponent(search)}${animalOnly ? '&animal_only=1' : ''}`)
.then(res => res.json())
.then(data => {
if (data.success) {
productsData = data.items;
document.getElementById('resultCount').style.display = 'block';
document.getElementById('resultNum').textContent = productsData.length;
renderTable();
} else {
tbody.innerHTML = `<tr><td colspan="6" class="empty-state"><p>?<3F>류: ${data.error}</p></td></tr>`;
}
})
.catch(err => {
tbody.innerHTML = '<tr><td colspan="6" class="empty-state"><p>검???<3F>패</p></td></tr>';
});
}
function renderTable() {
const tbody = document.getElementById('productsTableBody');
if (productsData.length === 0) {
tbody.innerHTML = '<tr><td colspan="6" class="empty-state"><div class="icon">?<3F><></div><p>검??결과가 ?<3F>습?<3F>다</p></td></tr>';
return;
}
tbody.innerHTML = productsData.map((item, idx) => `
<tr>
<td>
<div class="product-name">
${escapeHtml(item.product_name)}
${item.is_animal_drug ? '<span style="display:inline-block;background:#10b981;color:#fff;font-size:11px;padding:2px 6px;border-radius:4px;margin-left:6px;">?<3F><> ?<3F>물??/span>' : ''}
</div>
<div class="product-supplier ${item.is_set ? 'set' : ''}">${escapeHtml(item.supplier) || ''}</div>
</td>
<td><span class="code code-drug">${item.drug_code}</span></td>
<td>${item.barcode
? `<span class="code code-barcode">${item.barcode}</span>`
: `<span class="code code-na">?<3F>음</span>`}</td>
<td class="stock ${item.stock > 0 ? 'in-stock' : 'out-stock'}">${item.stock || 0}<EFBFBD>?/td>
<td class="price">${formatPrice(item.sale_price)}</td>
<td>
<button class="btn-qr" onclick="printQR(${idx})">?<3F><><EFBFBD>?QR</button>
</td>
</tr>
`).join('');
}
// ?<3F>?<3F> QR ?<3F>쇄 관???<3F>?<3F>
function adjustQty(delta) {
printQty = Math.max(MIN_QTY, Math.min(MAX_QTY, printQty + delta));
updateQtyUI();
}
function updateQtyUI() {
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}???<3F>` : '?<3F>쇄';
}
function printQR(idx) {
selectedItem = productsData[idx];
printQty = 1;
const modal = document.getElementById('qrModal');
const preview = document.getElementById('qrPreview');
const info = document.getElementById('qrInfo');
preview.innerHTML = '<p style="color:#64748b;">미리보기 로딩 <20>?..</p>';
info.innerHTML = `
<strong>${escapeHtml(selectedItem.product_name)}</strong><br>
<span style="color:#64748b;font-size:13px;">
바코?? ${selectedItem.barcode || selectedItem.drug_code || 'N/A'}<br>
<>? ${formatPrice(selectedItem.sale_price)}
</span>
`;
updateQtyUI();
modal.classList.add('active');
fetch('/api/qr-preview', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
drug_name: selectedItem.product_name,
barcode: selectedItem.barcode || '',
drug_code: selectedItem.drug_code || '',
sale_price: selectedItem.sale_price || 0
})
})
.then(res => res.json())
.then(data => {
if (data.success && data.image) {
preview.innerHTML = `<img src="${data.image}" alt="QR 미리보기">`;
} else {
preview.innerHTML = '<p style="color:#ef4444;">미리보기 ?<3F>패</p>';
}
})
.catch(() => {
preview.innerHTML = '<p style="color:#ef4444;">미리보기 ?<3F>류</p>';
});
}
function closeQRModal() {
document.getElementById('qrModal').classList.remove('active');
selectedItem = null;
printQty = 1;
}
async function confirmPrintQR() {
if (!selectedItem) return;
const btn = document.getElementById('printBtn');
const totalQty = printQty;
btn.disabled = true;
let successCount = 0;
let errorMsg = '';
for (let i = 0; i < totalQty; i++) {
btn.textContent = `?<3F><20>?.. (${i + 1}/${totalQty})`;
try {
const res = await fetch('/api/qr-print', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
drug_name: selectedItem.product_name,
barcode: selectedItem.barcode || '',
drug_code: selectedItem.drug_code || '',
sale_price: selectedItem.sale_price || 0
})
});
const data = await res.json();
if (data.success) {
successCount++;
} else {
errorMsg = data.error || '?????<3F>는 ?<3F>류';
break;
}
if (i < totalQty - 1) {
await new Promise(r => setTimeout(r, 500));
}
} catch (err) {
errorMsg = err.message;
break;
}
}
btn.disabled = false;
updateQtyUI();
if (successCount === totalQty) {
alert(`??QR ?<3F>${totalQty}???<3F>쇄 ?<3F>료!`);
closeQRModal();
} else if (successCount > 0) {
alert(`?<3F> ${successCount}/${totalQty}???<3F>쇄 ?<3F>\n?<3F>류: ${errorMsg}`);
} else {
alert(`???<3F>쇄 ?<3F>패: ${errorMsg}`);
}
}
// ?<3F>이지 로드 ??검?<3F>창 ?<3F>커?? document.getElementById('searchInput').focus();
// ?<3F>═?<3F>═?<3F>═?<3F>═?<3F>═?<3F>═?<3F>═?<3F>═?<3F>═?<3F>═?<3F>═?<3F>═?<3F>═?<3F>═?<3F>═?<3F>═?<3F>═?<3F>═?<3F>═?<3F>═?<3F>═?<3F>═?<3F>═?<3F>═?<3F>═?<3F>═?<3F>═?<3F>═?<3F>═?<3F>═?<3F>═?<3F>═?<3F>
// ?<3F>물??챗봇
// ?<3F>═?<3F>═?<3F>═?<3F>═?<3F>═?<3F>═?<3F>═?<3F>═?<3F>═?<3F>═?<3F>═?<3F>═?<3F>═?<3F>═?<3F>═?<3F>═?<3F>═?<3F>═?<3F>═?<3F>═?<3F>═?<3F>═?<3F>═?<3F>═?<3F>═?<3F>═?<3F>═?<3F>═?<3F>═?<3F>═?<3F>═?<3F>═?<3F>
let chatHistory = [];
let isChatLoading = false;
function toggleChatbot() {
const panel = document.getElementById('chatbotPanel');
const btn = document.getElementById('chatbotToggle');
const isOpen = panel.classList.toggle('open');
btn.classList.toggle('active', isOpen);
btn.innerHTML = isOpen ? '?? : '?<EFBFBD><EFBFBD>';
if (isOpen) {
document.getElementById('chatInput').focus();
}
}
function sendSuggestion(text) {
document.getElementById('chatInput').value = text;
sendChat();
}
async function sendChat() {
const input = document.getElementById('chatInput');
const message = input.value.trim();
if (!message || isChatLoading) return;
// ?<3F>용??메시지 ?<3F>
addChatMessage('user', message);
input.value = '';
// ?<3F>스?<3F>리??추<>?
chatHistory.push({ role: 'user', content: message });
// 로딩 ?<3F>
isChatLoading = true;
document.getElementById('chatSendBtn').disabled = true;
showTypingIndicator();
try {
const response = await fetch('/api/animal-chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ messages: chatHistory })
});
const data = await response.json();
hideTypingIndicator();
if (data.success) {
// AI ?<3F>답 ?<3F>
addChatMessage('assistant', data.message, data.products);
// ?<3F>스?<3F>리??추<>?
chatHistory.push({ role: 'assistant', content: data.message });
// ?<3F>스?<3F>리 길이 ?<3F>한 (최근 20<32>?
if (chatHistory.length > 20) {
chatHistory = chatHistory.slice(-20);
}
} else {
addChatMessage('system', '?<EFBFBD> ' + (data.message || '?<EFBFBD>류가 발생?<EFBFBD>?<EFBFBD>'));
}
} catch (error) {
hideTypingIndicator();
addChatMessage('system', '?<EFBFBD> ?<EFBFBD>?<EFBFBD> ?<EFBFBD>류가 발생?<EFBFBD>?<EFBFBD>');
}
isChatLoading = false;
document.getElementById('chatSendBtn').disabled = false;
}
function addChatMessage(role, content, products) {
const container = document.getElementById('chatMessages');
const msgDiv = document.createElement('div');
msgDiv.className = `chat-message ${role}`;
// 줄바<ECA484>?처리
let htmlContent = escapeHtml(content).replace(/\n/g, '<br>');
// 마크?<3F>운 굵게 처리
htmlContent = htmlContent.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
msgDiv.innerHTML = htmlContent;
// ?<3F>급???<3F>품 ?<3F>시 (?<3F><>?지 ?<3F>함)
if (products && products.length > 0) {
const productsDiv = document.createElement('div');
productsDiv.className = 'products-mentioned';
productsDiv.innerHTML = '<small style="color:#64748b;">?<3F><> 관???<3F>품:</small>';
const productsGrid = document.createElement('div');
productsGrid.style.cssText = 'display:flex;flex-wrap:wrap;gap:8px;margin-top:8px;';
products.forEach(p => {
const card = document.createElement('div');
card.className = 'product-card-mini';
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);
// ?<3F><>?지 컨테?<3F>
const imgContainer = document.createElement('div');
imgContainer.style.cssText = 'width:40px;height:40px;flex-shrink:0;';
if (p.image_url) {
const img = document.createElement('img');
img.style.cssText = 'width:40px;height:40px;object-fit:cover;border-radius:4px;background:#e2e8f0;';
img.src = p.image_url;
img.alt = p.name;
img.onerror = function() {
// ?<3F><>?지 로드 ?<3F>패 ???<3F>이콘으<ECBD98>??<3F><>? imgContainer.innerHTML = '<div style="width:40px;height:40px;background:#f1f5f9;border-radius:4px;display:flex;align-items:center;justify-content:center;font-size:20px;">?<3F><></div>';
};
imgContainer.appendChild(img);
} else {
// ?<3F><>?지 ?<3F><EFBFBD>??<3F><EFBFBD>? imgContainer.innerHTML = '<div style="width:40px;height:40px;background:#f1f5f9;border-radius:4px;display:flex;align-items:center;justify-content:center;font-size:20px;">?<3F><></div>';
}
// ?<3F>스?? const textDiv = document.createElement('div');
textDiv.innerHTML = `<div style="font-size:13px;font-weight:500;color:#334155;">${p.name}</div><div style="font-size:12px;color:#10b981;">${formatPrice(p.price)}</div>`;
card.appendChild(imgContainer);
card.appendChild(textDiv);
productsGrid.appendChild(card);
});
productsDiv.appendChild(productsGrid);
msgDiv.appendChild(productsDiv);
}
container.appendChild(msgDiv);
container.scrollTop = container.scrollHeight;
}
function showTypingIndicator() {
const container = document.getElementById('chatMessages');
const indicator = document.createElement('div');
indicator.className = 'typing-indicator';
indicator.id = 'typingIndicator';
indicator.innerHTML = '<span></span><span></span><span></span>';
container.appendChild(indicator);
container.scrollTop = container.scrollHeight;
}
function hideTypingIndicator() {
const indicator = document.getElementById('typingIndicator');
if (indicator) indicator.remove();
}
function searchProductFromChat(productName) {
// 챗봇?<3F>서 ?<3F>품 ?<3F>릭 ??검?<3F>창???<3F>력?<3F>고 검?? document.getElementById('searchInput').value = productName;
document.getElementById('animalOnly').checked = true;
searchProducts();
// 모바?<3F>에??챗봇 ?<3F>
if (window.innerWidth <= 1100) {
document.getElementById('chatbotPanel').classList.remove('open');
}
}
</script>
</body>
</html>