diff --git a/backend/app.py b/backend/app.py
index dd7d2e5..0a40727 100644
--- a/backend/app.py
+++ b/backend/app.py
@@ -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)
}
})
diff --git a/backend/templates/admin_sales_pos.html b/backend/templates/admin_sales_pos.html
index 9c5fcdd..f392f1a 100644
--- a/backend/templates/admin_sales_pos.html
+++ b/backend/templates/admin_sales_pos.html
@@ -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 @@
- ${escapeHtml(item.product_name)}
- ${item.supplier ? ` ${escapeHtml(item.supplier)}` : ''}
+ ${item.thumbnail
+ ? `  `
+ : ` 📦 `
+ }
+
+ ${escapeHtml(item.product_name)}
+ ${item.supplier ? `${escapeHtml(item.supplier)}` : ''}
+
|
${renderCode(item)} |
@@ -826,8 +861,14 @@
${item.sale_date} |
- ${escapeHtml(item.product_name)}
- ${item.supplier ? ` ${escapeHtml(item.supplier)}` : ''}
+ ${item.thumbnail
+ ? `  `
+ : ` 📦 `
+ }
+
+ ${escapeHtml(item.product_name)}
+ ${item.supplier ? `${escapeHtml(item.supplier)}` : ''}
+
|
${renderCode(item)} |
diff --git a/check_images_db.py b/check_images_db.py
new file mode 100644
index 0000000..74ddb2a
--- /dev/null
+++ b/check_images_db.py
@@ -0,0 +1,26 @@
+import sqlite3
+
+conn = sqlite3.connect(r'C:\Users\청춘약국\source\pharmacy-pos-qr-system\backend\db\product_images.db')
+cursor = conn.cursor()
+
+# 테이블 목록
+cursor.execute('SELECT name FROM sqlite_master WHERE type="table"')
+tables = [r[0] for r in cursor.fetchall()]
+print("테이블:", tables)
+
+# 각 테이블 스키마
+for table in tables:
+ cursor.execute(f'PRAGMA table_info({table})')
+ cols = [r[1] for r in cursor.fetchall()]
+ print(f"\n{table} 컬럼: {cols}")
+
+ # 샘플 데이터
+ cursor.execute(f'SELECT * FROM {table} LIMIT 2')
+ rows = cursor.fetchall()
+ for r in rows:
+ print(f" 샘플: {r[:3]}..." if len(r) > 3 else f" 샘플: {r}")
+
+# 총 개수
+for table in tables:
+ cursor.execute(f'SELECT COUNT(*) FROM {table}')
+ print(f"\n{table} 총 {cursor.fetchone()[0]}개")
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..00d59eb
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,56 @@
+{
+ "name": "pharmacy-pos-qr-system",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "dependencies": {
+ "playwright": "^1.58.2"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
+ "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/playwright": {
+ "version": "1.58.2",
+ "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz",
+ "integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "playwright-core": "1.58.2"
+ },
+ "bin": {
+ "playwright": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "fsevents": "2.3.2"
+ }
+ },
+ "node_modules/playwright-core": {
+ "version": "1.58.2",
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz",
+ "integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==",
+ "license": "Apache-2.0",
+ "bin": {
+ "playwright-core": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ }
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..41c60dc
--- /dev/null
+++ b/package.json
@@ -0,0 +1,5 @@
+{
+ "dependencies": {
+ "playwright": "^1.58.2"
+ }
+}
diff --git a/test_print_cusetc.js b/test_print_cusetc.js
new file mode 100644
index 0000000..219ecf1
--- /dev/null
+++ b/test_print_cusetc.js
@@ -0,0 +1,39 @@
+const { chromium } = require('playwright');
+
+(async () => {
+ const browser = await chromium.launch({ headless: true });
+ const page = await browser.newPage();
+
+ // API 응답 캡처
+ let apiResponse = null;
+ page.on('response', async (response) => {
+ if (response.url().includes('/api/print/cusetc')) {
+ apiResponse = await response.json();
+ }
+ });
+
+ await page.goto('http://localhost:7001/admin');
+ console.log('✅ 관리자 페이지');
+
+ await page.click('td:has-text("김영빈")');
+ await page.waitForSelector('text=특이사항', { timeout: 5000 });
+ console.log('✅ 모달 열림');
+
+ // 인쇄 버튼 클릭
+ const printBtn = await page.$('button[onclick*="doPrintCusetc"]');
+ if (printBtn) {
+ await printBtn.click();
+ console.log('✅ 인쇄 버튼 클릭');
+
+ // 즉시 토스트 확인 (API 응답 전)
+ await page.waitForTimeout(100);
+ const toast = await page.$eval('.toast', el => el?.textContent).catch(() => null);
+ console.log('📢 즉시 피드백:', toast || '(토스트 없음)');
+
+ // API 응답 대기
+ await page.waitForTimeout(2000);
+ console.log('📡 API 응답:', apiResponse?.success ? '✅ ' + apiResponse.message : '❌ ' + (apiResponse?.error || 'no response'));
+ }
+
+ await browser.close();
+})();