feat: 판매내역 페이지에 제품 썸네일 이미지 표시
- app.py: /api/sales-detail에서 product_images.db 조회하여 thumbnail 반환
- admin_sales_pos.html: 거래별/목록 뷰에 36x36 썸네일 표시
- 이미지 없는 제품은 📦 플레이스홀더 표시
- barcode 우선, drug_code 폴백으로 이미지 매칭
This commit is contained in:
@@ -3555,7 +3555,8 @@ def api_sales_detail():
|
||||
'supplier': row.supplier or '',
|
||||
'quantity': quantity,
|
||||
'unit_price': int(unit_price),
|
||||
'total_price': int(total_price)
|
||||
'total_price': int(total_price),
|
||||
'thumbnail': None # 나중에 채워짐
|
||||
})
|
||||
|
||||
total_amount += total_price
|
||||
@@ -3563,9 +3564,57 @@ def api_sales_detail():
|
||||
barcode_count += 1
|
||||
unique_products.add(drug_code)
|
||||
|
||||
# 제품 이미지 조회 (product_images.db에서)
|
||||
try:
|
||||
images_db_path = Path(__file__).parent / 'db' / 'product_images.db'
|
||||
if images_db_path.exists():
|
||||
img_conn = sqlite3.connect(str(images_db_path))
|
||||
img_cursor = img_conn.cursor()
|
||||
|
||||
# barcode와 drug_code 수집
|
||||
barcodes = [item['barcode'] for item in items if item['barcode']]
|
||||
drug_codes = [item['drug_code'] for item in items if item['drug_code']]
|
||||
|
||||
# 이미지 조회 (barcode 또는 drug_code로 매칭)
|
||||
image_map = {}
|
||||
if barcodes:
|
||||
placeholders = ','.join(['?' for _ in barcodes])
|
||||
img_cursor.execute(f'''
|
||||
SELECT barcode, thumbnail_base64
|
||||
FROM product_images
|
||||
WHERE barcode IN ({placeholders}) AND thumbnail_base64 IS NOT NULL
|
||||
''', barcodes)
|
||||
for row in img_cursor.fetchall():
|
||||
image_map[f'bc:{row[0]}'] = row[1]
|
||||
|
||||
if drug_codes:
|
||||
placeholders = ','.join(['?' for _ in drug_codes])
|
||||
img_cursor.execute(f'''
|
||||
SELECT drug_code, thumbnail_base64
|
||||
FROM product_images
|
||||
WHERE drug_code IN ({placeholders}) AND thumbnail_base64 IS NOT NULL
|
||||
''', drug_codes)
|
||||
for row in img_cursor.fetchall():
|
||||
if f'dc:{row[0]}' not in image_map: # barcode 우선
|
||||
image_map[f'dc:{row[0]}'] = row[1]
|
||||
|
||||
img_conn.close()
|
||||
|
||||
# 아이템에 썸네일 매핑
|
||||
for item in items:
|
||||
thumb = image_map.get(f'bc:{item["barcode"]}') or image_map.get(f'dc:{item["drug_code"]}')
|
||||
if thumb:
|
||||
item['thumbnail'] = thumb
|
||||
except Exception as img_err:
|
||||
logging.warning(f"제품 이미지 조회 오류: {img_err}")
|
||||
|
||||
# 바코드 매핑률 계산
|
||||
barcode_rate = round(barcode_count / len(items) * 100, 1) if items else 0
|
||||
|
||||
# 이미지 매핑률 계산
|
||||
image_count = sum(1 for item in items if item.get('thumbnail'))
|
||||
image_rate = round(image_count / len(items) * 100, 1) if items else 0
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'items': items[:500], # 최대 500건
|
||||
@@ -3573,6 +3622,7 @@ def api_sales_detail():
|
||||
'total_count': len(items),
|
||||
'total_amount': int(total_amount),
|
||||
'barcode_rate': barcode_rate,
|
||||
'image_rate': image_rate,
|
||||
'unique_products': len(unique_products)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -369,13 +369,42 @@
|
||||
|
||||
/* 제품 셀 */
|
||||
.product-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
.product-thumb {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
object-fit: cover;
|
||||
border-radius: 6px;
|
||||
background: var(--bg-secondary);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.product-thumb-placeholder {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--bg-secondary);
|
||||
border-radius: 6px;
|
||||
font-size: 16px;
|
||||
opacity: 0.4;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.product-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
gap: 2px;
|
||||
min-width: 0;
|
||||
}
|
||||
.product-name {
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.product-supplier {
|
||||
font-size: 11px;
|
||||
@@ -796,8 +825,14 @@
|
||||
<tr>
|
||||
<td>
|
||||
<div class="product-cell">
|
||||
<span class="product-name">${escapeHtml(item.product_name)}</span>
|
||||
${item.supplier ? `<span class="product-supplier">${escapeHtml(item.supplier)}</span>` : ''}
|
||||
${item.thumbnail
|
||||
? `<img src="data:image/jpeg;base64,${item.thumbnail}" class="product-thumb" alt="">`
|
||||
: `<div class="product-thumb-placeholder">📦</div>`
|
||||
}
|
||||
<div class="product-info">
|
||||
<span class="product-name">${escapeHtml(item.product_name)}</span>
|
||||
${item.supplier ? `<span class="product-supplier">${escapeHtml(item.supplier)}</span>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>${renderCode(item)}</td>
|
||||
@@ -826,8 +861,14 @@
|
||||
<td style="color:var(--text-secondary);font-size:12px;">${item.sale_date}</td>
|
||||
<td>
|
||||
<div class="product-cell">
|
||||
<span class="product-name">${escapeHtml(item.product_name)}</span>
|
||||
${item.supplier ? `<span class="product-supplier">${escapeHtml(item.supplier)}</span>` : ''}
|
||||
${item.thumbnail
|
||||
? `<img src="data:image/jpeg;base64,${item.thumbnail}" class="product-thumb" alt="">`
|
||||
: `<div class="product-thumb-placeholder">📦</div>`
|
||||
}
|
||||
<div class="product-info">
|
||||
<span class="product-name">${escapeHtml(item.product_name)}</span>
|
||||
${item.supplier ? `<span class="product-supplier">${escapeHtml(item.supplier)}</span>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>${renderCode(item)}</td>
|
||||
|
||||
Reference in New Issue
Block a user