From 77c667e1f6de7c77361dfefe6e332fc36b138695 Mon Sep 17 00:00:00 2001 From: thug0bin Date: Wed, 4 Mar 2026 16:29:23 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EB=8B=A8=EC=9C=84=EB=B0=94=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EA=B0=AF=EC=88=98=20=EB=B1=83=EC=A7=80=20+=20QR=20?= =?UTF-8?q?=EB=B0=94=EC=BD=94=EB=93=9C=20=EC=9A=B0=EC=84=A0=EC=88=9C?= =?UTF-8?q?=EC=9C=84=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 화면 표시: - 대표바코드 옆 빨간 뱃지로 단위바코드 갯수 표시 (카톡 스타일) - APC 없어도 단위바코드 있으면 POS 판매 가능함을 표시 QR 인쇄 우선순위: 1. 대표바코드 (있으면) 2. 단위바코드 첫 번째 (대표 없으면) 3. drug_code (fallback) 쿼리 추가: - UNIT_FIRST: 단위바코드 첫 번째 (조건 없이) - UNIT_CNT: 단위바코드 갯수 --- backend/app.py | 54 ++++++++++++++++++++++++--- backend/templates/admin_products.html | 31 +++++++++++++-- 2 files changed, 75 insertions(+), 10 deletions(-) diff --git a/backend/app.py b/backend/app.py index a8676aa..586ff70 100644 --- a/backend/app.py +++ b/backend/app.py @@ -3335,7 +3335,7 @@ def api_products(): # 제품 검색 쿼리 - 사용약품만 옵션에 따라 JOIN 방식 변경 if in_stock_only: # 최적화된 쿼리: 재고 있는 제품만 (IM_total INNER JOIN) - # 대표바코드만 표시 (없으면 빈값), APC는 02로 시작하는 것만 + # 대표바코드만 표시, 단위바코드 첫번째/갯수, APC(02%) products_query = text(f""" SELECT TOP {limit} G.DrugCode as drug_code, @@ -3348,7 +3348,9 @@ def api_products(): G.POS_BOON as pos_boon, IT.IM_QT_sale_debit as stock, ISNULL(POS.CD_NM_sale, '') as location, - APC.CD_CD_BARCODE as apc_code + APC.CD_CD_BARCODE as apc_code, + UNIT_FIRST.CD_CD_BARCODE as unit_barcode, + ISNULL(UNIT_CNT.cnt, 0) as unit_barcode_count FROM CD_GOODS G INNER JOIN IM_total IT ON G.DrugCode = IT.DrugCode AND IT.IM_QT_sale_debit > 0 LEFT JOIN CD_item_position POS ON G.DrugCode = POS.DrugCode @@ -3357,6 +3359,16 @@ def api_products(): FROM CD_ITEM_UNIT_MEMBER WHERE DRUGCODE = G.DrugCode AND CD_CD_BARCODE LIKE '02%' ) APC + OUTER APPLY ( + SELECT TOP 1 CD_CD_BARCODE + FROM CD_ITEM_UNIT_MEMBER + WHERE DRUGCODE = G.DrugCode AND CD_CD_BARCODE IS NOT NULL AND CD_CD_BARCODE != '' + ) UNIT_FIRST + OUTER APPLY ( + SELECT COUNT(*) as cnt + FROM CD_ITEM_UNIT_MEMBER + WHERE DRUGCODE = G.DrugCode AND CD_CD_BARCODE IS NOT NULL AND CD_CD_BARCODE != '' + ) UNIT_CNT WHERE 1=1 {animal_condition} {search_condition} @@ -3378,7 +3390,9 @@ def api_products(): G.POS_BOON as pos_boon, ISNULL(IT.IM_QT_sale_debit, 0) as stock, ISNULL(POS.CD_NM_sale, '') as location, - APC.CD_CD_BARCODE as apc_code + APC.CD_CD_BARCODE as apc_code, + UNIT_FIRST.CD_CD_BARCODE as unit_barcode, + ISNULL(UNIT_CNT.cnt, 0) as unit_barcode_count FROM CD_GOODS G LEFT JOIN IM_total IT ON G.DrugCode = IT.DrugCode LEFT JOIN CD_item_position POS ON G.DrugCode = POS.DrugCode @@ -3387,6 +3401,16 @@ def api_products(): FROM CD_ITEM_UNIT_MEMBER WHERE DRUGCODE = G.DrugCode AND CD_CD_BARCODE LIKE '02%' ) APC + OUTER APPLY ( + SELECT TOP 1 CD_CD_BARCODE + FROM CD_ITEM_UNIT_MEMBER + WHERE DRUGCODE = G.DrugCode AND CD_CD_BARCODE IS NOT NULL AND CD_CD_BARCODE != '' + ) UNIT_FIRST + OUTER APPLY ( + SELECT COUNT(*) as cnt + FROM CD_ITEM_UNIT_MEMBER + WHERE DRUGCODE = G.DrugCode AND CD_CD_BARCODE IS NOT NULL AND CD_CD_BARCODE != '' + ) UNIT_CNT WHERE G.POS_BOON = '010103' {search_condition} ORDER BY G.GoodsName @@ -3408,7 +3432,9 @@ def api_products(): G.POS_BOON as pos_boon, ISNULL(IT.IM_QT_sale_debit, 0) as stock, ISNULL(POS.CD_NM_sale, '') as location, - APC.CD_CD_BARCODE as apc_code + APC.CD_CD_BARCODE as apc_code, + UNIT_FIRST.CD_CD_BARCODE as unit_barcode, + ISNULL(UNIT_CNT.cnt, 0) as unit_barcode_count FROM CD_GOODS G LEFT JOIN IM_total IT ON G.DrugCode = IT.DrugCode LEFT JOIN CD_item_position POS ON G.DrugCode = POS.DrugCode @@ -3422,6 +3448,16 @@ def api_products(): FROM CD_ITEM_UNIT_MEMBER WHERE DRUGCODE = G.DrugCode AND CD_CD_BARCODE LIKE '02%' ) APC + OUTER APPLY ( + SELECT TOP 1 CD_CD_BARCODE + FROM CD_ITEM_UNIT_MEMBER + WHERE DRUGCODE = G.DrugCode AND CD_CD_BARCODE IS NOT NULL AND CD_CD_BARCODE != '' + ) UNIT_FIRST + OUTER APPLY ( + SELECT COUNT(*) as cnt + FROM CD_ITEM_UNIT_MEMBER + WHERE DRUGCODE = G.DrugCode AND CD_CD_BARCODE IS NOT NULL AND CD_CD_BARCODE != '' + ) UNIT_CNT WHERE 1=1 {search_condition} ORDER BY G.GoodsName @@ -3456,8 +3492,12 @@ def api_products(): # APC 코드: 쿼리에서 02%로 조회한 것만 사용 (바코드 대체 X) apc_code = getattr(row, 'apc_code', None) or '' - # PostgreSQL 조회용 APC (분류/도매재고): apc 또는 apc_code 사용 - pg_apc = apc or apc_code + # 단위바코드 (첫 번째, 갯수) + unit_barcode = getattr(row, 'unit_barcode', None) or '' + unit_barcode_count = getattr(row, 'unit_barcode_count', 0) or 0 + + # PostgreSQL 조회용 APC (분류/도매재고): apc 또는 apc_code 또는 unit_barcode + pg_apc = apc or apc_code or unit_barcode items.append({ 'drug_code': row.drug_code or '', @@ -3471,6 +3511,8 @@ def api_products(): 'stock': int(row.stock) if row.stock else 0, 'location': row.location or '', # 위치 'apc': apc_code, # UI용 APC 코드 (02로 시작하는 것만) + 'unit_barcode': unit_barcode, # 단위바코드 첫 번째 (QR용) + 'unit_barcode_count': int(unit_barcode_count), # 단위바코드 갯수 (뱃지용) '_pg_apc': pg_apc, # PostgreSQL 조회용 (내부용) 'category': None, # PostgreSQL에서 lazy fetch 'wholesaler_stock': None, diff --git a/backend/templates/admin_products.html b/backend/templates/admin_products.html index f5aabbf..8216d36 100644 --- a/backend/templates/admin_products.html +++ b/backend/templates/admin_products.html @@ -402,6 +402,21 @@ color: #dc2626; border: 1px dashed #fca5a5; } + .unit-badge { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 18px; + height: 18px; + padding: 0 5px; + margin-left: 4px; + background: #ef4444; + color: #fff; + font-size: 11px; + font-weight: 600; + border-radius: 9px; + vertical-align: middle; + } .code-na { background: #f1f5f9; color: #94a3b8; @@ -1053,7 +1068,9 @@ ${item.drug_code} ${item.is_animal_drug - ? `
${item.barcode ? `${item.barcode}` : `없음`}
+ ? `
${item.barcode + ? `${item.barcode}` + : `없음`}${item.unit_barcode_count > 0 ? `${item.unit_barcode_count}` : ''}
${item.apc ? `${item.apc}` : `APC미지정`}
` : (item.barcode ? `${item.barcode}` : `없음`)} ${item.location @@ -1089,11 +1106,14 @@ const preview = document.getElementById('qrPreview'); const info = document.getElementById('qrInfo'); + // QR에 들어갈 바코드 우선순위: 대표바코드 > 단위바코드 > 제품코드 + const qrBarcode = selectedItem.barcode || selectedItem.unit_barcode || selectedItem.drug_code || ''; + preview.innerHTML = '

미리보기 로딩 중...

'; info.innerHTML = ` ${escapeHtml(selectedItem.product_name)}
- 바코드: ${selectedItem.barcode || selectedItem.drug_code || 'N/A'}
+ 바코드: ${qrBarcode || 'N/A'}
가격: ${formatPrice(selectedItem.sale_price)}
`; @@ -1105,7 +1125,7 @@ headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ drug_name: selectedItem.product_name, - barcode: selectedItem.barcode || '', + barcode: qrBarcode, drug_code: selectedItem.drug_code || '', sale_price: selectedItem.sale_price || 0 }) @@ -1143,12 +1163,15 @@ btn.textContent = `인쇄 중... (${i + 1}/${totalQty})`; try { + // QR에 들어갈 바코드 우선순위: 대표바코드 > 단위바코드 > 제품코드 + const qrBarcode = selectedItem.barcode || selectedItem.unit_barcode || selectedItem.drug_code || ''; + 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 || '', + barcode: qrBarcode, drug_code: selectedItem.drug_code || '', sale_price: selectedItem.sale_price || 0 })