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(); +})();