From 111c173692c5ed39dc94751a1a33586872758e56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=8B=9C=EA=B3=A8=EC=95=BD=EC=82=AC?= Date: Mon, 16 Feb 2026 16:06:20 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20=ED=95=9C=EC=9D=98=EC=82=AC=EB=9E=91=20?= =?UTF-8?q?=EC=B9=B4=ED=83=88=EB=A1=9C=EA=B7=B8=20import=20=EC=8A=A4?= =?UTF-8?q?=ED=81=AC=EB=A6=BD=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 마이페이지 데이터 파싱 및 DB 저장 - supplier_product_catalog 테이블 관리 - 가격 기반 매칭 시도 기능 포함 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- import_hanisarang_catalog.py | 275 +++++++++++++++++++++++++++++++++++ 1 file changed, 275 insertions(+) create mode 100644 import_hanisarang_catalog.py diff --git a/import_hanisarang_catalog.py b/import_hanisarang_catalog.py new file mode 100644 index 0000000..c4420d5 --- /dev/null +++ b/import_hanisarang_catalog.py @@ -0,0 +1,275 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +한의사랑 카탈로그 데이터 import 및 가격 매칭 +""" + +import sqlite3 +import re + +def get_connection(): + """데이터베이스 연결""" + return sqlite3.connect('database/kdrug.db') + +def parse_catalog_data(): + """제공된 카탈로그 데이터 파싱""" + raw_data = """갈근.각5 배송중 42,000 400 0 롯데택배256733159384배송조회 +감초.1호[야생](1kg)5 배송중 110,500 0 +건강10 배송중 62,000 600 +건강.土3 배송중 77,100 750 +계지5 배송중 14,500 100 +구기자(영하)(1kg)3 배송중 53,700 510 +길경.片[특]3 배송중 15,900 0 +대추(절편)(1kg)5 배송중 100,000 1,000 +마황(1kg)5 배송중 48,000 0 +반하생강백반제(1kg)3 배송중 101,100 990 +백출.당[1kg]2 배송중 23,600 0 +복령(1kg)5 배송중 57,500 550 +석고[통포장](kg)4 배송중 18,800 160 +세신.中3 배송중 193,500 0 +숙지황(9증)(신흥.1kg)[완]5 배송중 100,000 1,000 +오미자<토매지>(1kg)2 배송중 35,000 340 +용안육.名품(1kg)3 배송중 62,100 600 +육계.YB25 배송중 36,500 350 +일당귀.中(1kg)5 배송중 64,500 600 +자소엽.土3 배송중 20,700 180 +작약(1kg)3 배송중 56,100 540 +작약주자.土[酒炙]3 배송중 36,900 360 +전호[재배]3 배송중 21,000 210 +지각3 배송중 15,000 150 +지황.건[회](1kg)1 배송중 11,500 110 +진피.비열[非熱](1kg)5 배송중 68,500 0 +창출[북창출.재배](1kg)3 배송중 40,500 0 +천궁.일<토매지>(1kg)3 배송중 35,700 330 +황기(직절.小)(1kg)3 배송중 29,700 270""" + + items = [] + for line in raw_data.split('\n'): + if not line.strip(): + continue + + # 택배 추적번호 제거 + line = re.sub(r'롯데택배\d+배송조회', '', line) + + parts = line.split('\t') + if len(parts) >= 4: + # 약재명 추출 (뒤의 수량 숫자 제거) + raw_name = re.sub(r'\d+$', '', parts[0]) + + # 가격 파싱 (콤마 제거) + total_price = int(parts[2].replace(',', '')) + + # g당 단가 + if len(parts) >= 5 and parts[4] != '0': + unit_price = int(parts[4].replace(',', '')) + else: + # g당 단가가 0이면 총액에서 계산 (1kg 기준) + if '1kg' in raw_name or 'kg' in raw_name: + unit_price = total_price / 1000 + else: + unit_price = total_price / 1000 # 기본적으로 1kg로 가정 + + items.append({ + 'raw_name': raw_name.strip(), + 'total_price': total_price, + 'unit_price': unit_price, + 'status': parts[1] if len(parts) > 1 else '배송중' + }) + + return items + +def import_to_catalog(): + """카탈로그 데이터를 DB에 저장""" + conn = get_connection() + cursor = conn.cursor() + + print("\n" + "="*80) + print("한의사랑 카탈로그 데이터 Import") + print("="*80) + + # 한의사랑 supplier_id 조회 + cursor.execute("SELECT supplier_id FROM suppliers WHERE name = '한의사랑'") + result = cursor.fetchone() + + if not result: + # 한의사랑 공급처 생성 + cursor.execute(""" + INSERT INTO suppliers (name, is_active) + VALUES ('한의사랑', 1) + """) + supplier_id = cursor.lastrowid + print(f"한의사랑 공급처 생성 (ID: {supplier_id})") + else: + supplier_id = result[0] + print(f"한의사랑 공급처 확인 (ID: {supplier_id})") + + # 기존 데이터 삭제 + cursor.execute("DELETE FROM supplier_product_catalog WHERE supplier_id = ?", (supplier_id,)) + + # 카탈로그 데이터 파싱 + items = parse_catalog_data() + + print(f"\n총 {len(items)}개 항목을 파싱했습니다.") + print("-" * 60) + + # 데이터 삽입 + for item in items: + try: + cursor.execute(""" + INSERT INTO supplier_product_catalog + (supplier_id, raw_name, unit_price, package_unit, stock_status, last_updated) + VALUES (?, ?, ?, '1kg', ?, date('now')) + """, (supplier_id, item['raw_name'], item['unit_price'], item['status'])) + + print(f"추가: {item['raw_name']:30s} | {item['unit_price']:8.1f}원/g | {item['status']}") + except sqlite3.IntegrityError: + print(f"중복: {item['raw_name']}") + + conn.commit() + + # 저장된 데이터 확인 + cursor.execute(""" + SELECT COUNT(*) FROM supplier_product_catalog WHERE supplier_id = ? + """, (supplier_id,)) + count = cursor.fetchone()[0] + + print(f"\n한의사랑 카탈로그에 {count}개 항목이 저장되었습니다.") + + conn.close() + return items + +def match_with_inventory(): + """현재 inventory_lots와 가격 매칭""" + conn = get_connection() + cursor = conn.cursor() + + print("\n" + "="*80) + print("Inventory Lots와 가격 매칭") + print("="*80) + + # 휴먼허브 inventory lots 조회 + cursor.execute(""" + SELECT + l.lot_id, + h.herb_name, + l.unit_price_per_g, + l.origin_country, + s.name as supplier_name + FROM inventory_lots l + JOIN herb_items h ON l.herb_item_id = h.herb_item_id + JOIN purchase_receipt_lines prl ON l.receipt_line_id = prl.line_id + JOIN purchase_receipts pr ON prl.receipt_id = pr.receipt_id + JOIN suppliers s ON pr.supplier_id = s.supplier_id + WHERE l.display_name IS NULL + ORDER BY h.herb_name, l.unit_price_per_g + """) + + lots = cursor.fetchall() + print(f"\ndisplay_name이 없는 로트: {len(lots)}개\n") + + matched_count = 0 + no_match = [] + + for lot in lots: + lot_id, herb_name, unit_price, origin, supplier = lot + + # 한의사랑 카탈로그에서 비슷한 가격 찾기 (±10% 허용) + cursor.execute(""" + SELECT raw_name, unit_price + FROM supplier_product_catalog spc + JOIN suppliers s ON spc.supplier_id = s.supplier_id + WHERE s.name = '한의사랑' + AND ABS(spc.unit_price - ?) / ? < 0.1 + ORDER BY ABS(spc.unit_price - ?) + LIMIT 5 + """, (unit_price, unit_price, unit_price)) + + matches = cursor.fetchall() + + if matches: + print(f"\nLot #{lot_id}: {herb_name} ({unit_price:.1f}원/g, {origin})") + print(" 매칭 후보:") + + best_match = None + for match in matches: + match_name, match_price = match + diff_percent = abs(match_price - unit_price) / unit_price * 100 + print(f" - {match_name:30s} | {match_price:8.1f}원/g | 차이: {diff_percent:.1f}%") + + # 약재명에서 핵심 단어 추출하여 매칭 + herb_core = herb_name.replace('휴먼', '').replace('신흥', '') + if herb_core in match_name or any(keyword in match_name for keyword in [herb_core[:2], herb_core[-2:]]): + if not best_match or abs(match_price - unit_price) < abs(best_match[1] - unit_price): + best_match = match + + if best_match: + # display_name 업데이트 + cursor.execute(""" + UPDATE inventory_lots + SET display_name = ? + WHERE lot_id = ? + """, (best_match[0], lot_id)) + + # lot_variants 추가/업데이트 + try: + cursor.execute(""" + INSERT INTO lot_variants + (lot_id, raw_name, parsed_at, parsed_method) + VALUES (?, ?, datetime('now'), 'catalog_price_match') + """, (lot_id, best_match[0])) + except sqlite3.IntegrityError: + cursor.execute(""" + UPDATE lot_variants + SET raw_name = ?, parsed_at = datetime('now'), parsed_method = 'catalog_price_match' + WHERE lot_id = ? + """, (best_match[0], lot_id)) + + print(f" ✓ 매칭: {best_match[0]}") + matched_count += 1 + else: + print(" ✗ 적합한 매칭 없음") + no_match.append((lot_id, herb_name, unit_price, origin)) + else: + no_match.append((lot_id, herb_name, unit_price, origin)) + + conn.commit() + + print("\n" + "="*80) + print("매칭 결과") + print("="*80) + print(f"✓ 매칭 성공: {matched_count}개") + print(f"✗ 매칭 실패: {len(no_match)}개") + + if no_match: + print("\n매칭 실패한 로트:") + for lot in no_match: + print(f" Lot #{lot[0]}: {lot[1]:20s} | {lot[2]:8.1f}원/g | {lot[3]}") + + # 최종 결과 확인 + cursor.execute(""" + SELECT COUNT(*) as total, + COUNT(display_name) as with_display + FROM inventory_lots + """) + result = cursor.fetchone() + + print(f"\n전체 로트: {result[0]}개") + print(f"display_name 설정됨: {result[1]}개") + + conn.close() + +def main(): + """메인 실행""" + print("\n한의사랑 카탈로그 데이터 Import 및 매칭") + print("="*80) + + # 1. 카탈로그 데이터 import + items = import_to_catalog() + + # 2. inventory lots와 매칭 + match_with_inventory() + + print("\n완료!") + +if __name__ == "__main__": + main() \ No newline at end of file