From 29648e3a7d9a7801157b4f96b3e34ca53b5b0aec Mon Sep 17 00:00:00 2001 From: thug0bin Date: Mon, 2 Mar 2026 23:19:52 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20yakkok.com=20=EC=A0=9C=ED=92=88=20?= =?UTF-8?q?=EC=9D=B4=EB=AF=B8=EC=A7=80=20=ED=81=AC=EB=A1=A4=EB=9F=AC=20+?= =?UTF-8?q?=20=EC=96=B4=EB=93=9C=EB=AF=BC=20=ED=8E=98=EC=9D=B4=EC=A7=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 크롤러 (utils/yakkok_crawler.py): - yakkok.com에서 제품 검색 및 이미지 추출 - MSSQL 오늘 판매 품목 자동 조회 - base64 변환 후 SQLite 저장 - CLI 지원 (--today, --product) DB (product_images.db): - 바코드, 제품명, 이미지(base64), 상태 저장 - 크롤링 로그 테이블 어드민 페이지 (/admin/product-images): - 이미지 목록/검색/필터 - 통계 (성공/실패/대기) - 상세 보기/삭제 - 오늘 판매 제품 일괄 크롤링 API: - GET /api/admin/product-images - GET /api/admin/product-images/ - POST /api/admin/product-images/crawl-today - DELETE /api/admin/product-images/ --- backend/app.py | 163 ++++++ backend/db/product_images_schema.sql | 38 ++ backend/templates/admin_product_images.html | 575 ++++++++++++++++++++ backend/test_pg.py | 8 + backend/utils/yakkok_crawler.py | 349 ++++++++++++ docs/ANIMAL_DRUG_APC_MAPPING.html | 515 ++++++++++++++++++ 6 files changed, 1648 insertions(+) create mode 100644 backend/db/product_images_schema.sql create mode 100644 backend/templates/admin_product_images.html create mode 100644 backend/test_pg.py create mode 100644 backend/utils/yakkok_crawler.py create mode 100644 docs/ANIMAL_DRUG_APC_MAPPING.html diff --git a/backend/app.py b/backend/app.py index a8d039c..9e30191 100644 --- a/backend/app.py +++ b/backend/app.py @@ -5649,6 +5649,169 @@ def api_search_mssql_drug(): return jsonify({'success': False, 'error': str(e)}), 500 +# ============================================================ +# 제품 이미지 관리 (yakkok 크롤러) +# ============================================================ + +@app.route('/admin/product-images') +def admin_product_images(): + """제품 이미지 관리 어드민 페이지""" + return render_template('admin_product_images.html') + + +@app.route('/api/admin/product-images') +def api_product_images_list(): + """제품 이미지 목록 조회""" + import sqlite3 + try: + db_path = os.path.join(os.path.dirname(__file__), 'db', 'product_images.db') + conn = sqlite3.connect(db_path) + conn.row_factory = sqlite3.Row + cursor = conn.cursor() + + status_filter = request.args.get('status', '') + search = request.args.get('search', '') + limit = int(request.args.get('limit', 50)) + offset = int(request.args.get('offset', 0)) + + where_clauses = [] + params = [] + + if status_filter: + where_clauses.append("status = ?") + params.append(status_filter) + + if search: + where_clauses.append("(product_name LIKE ? OR barcode LIKE ?)") + params.extend([f'%{search}%', f'%{search}%']) + + where_sql = " WHERE " + " AND ".join(where_clauses) if where_clauses else "" + + # 총 개수 + cursor.execute(f"SELECT COUNT(*) FROM product_images {where_sql}", params) + total = cursor.fetchone()[0] + + # 목록 조회 + cursor.execute(f""" + SELECT id, barcode, drug_code, product_name, thumbnail_base64, + image_url, status, created_at, error_message + FROM product_images + {where_sql} + ORDER BY created_at DESC + LIMIT ? OFFSET ? + """, params + [limit, offset]) + + items = [dict(row) for row in cursor.fetchall()] + conn.close() + + return jsonify({ + 'success': True, + 'total': total, + 'items': items + }) + except Exception as e: + logging.error(f"제품 이미지 목록 조회 오류: {e}") + return jsonify({'success': False, 'error': str(e)}), 500 + + +@app.route('/api/admin/product-images/') +def api_product_image_detail(barcode): + """제품 이미지 상세 조회 (원본 base64 포함)""" + import sqlite3 + try: + db_path = os.path.join(os.path.dirname(__file__), 'db', 'product_images.db') + conn = sqlite3.connect(db_path) + conn.row_factory = sqlite3.Row + cursor = conn.cursor() + + cursor.execute("SELECT * FROM product_images WHERE barcode = ?", (barcode,)) + row = cursor.fetchone() + conn.close() + + if row: + return jsonify({'success': True, 'image': dict(row)}) + else: + return jsonify({'success': False, 'error': '이미지 없음'}), 404 + except Exception as e: + return jsonify({'success': False, 'error': str(e)}), 500 + + +@app.route('/api/admin/product-images/crawl-today', methods=['POST']) +def api_crawl_today(): + """오늘 판매 제품 크롤링""" + try: + from utils.yakkok_crawler import crawl_today_sales + result = crawl_today_sales(headless=True) + return jsonify({'success': True, 'result': result}) + except Exception as e: + logging.error(f"크롤링 오류: {e}") + return jsonify({'success': False, 'error': str(e)}), 500 + + +@app.route('/api/admin/product-images/crawl', methods=['POST']) +def api_crawl_products(): + """특정 제품 크롤링""" + try: + from utils.yakkok_crawler import crawl_products + data = request.get_json() + products = data.get('products', []) # [(barcode, drug_code, product_name), ...] + + if not products: + return jsonify({'success': False, 'error': '제품 목록 필요'}), 400 + + result = crawl_products(products, headless=True) + return jsonify({'success': True, 'result': result}) + except Exception as e: + logging.error(f"크롤링 오류: {e}") + return jsonify({'success': False, 'error': str(e)}), 500 + + +@app.route('/api/admin/product-images/', methods=['DELETE']) +def api_delete_product_image(barcode): + """제품 이미지 삭제""" + import sqlite3 + try: + db_path = os.path.join(os.path.dirname(__file__), 'db', 'product_images.db') + conn = sqlite3.connect(db_path) + cursor = conn.cursor() + cursor.execute("DELETE FROM product_images WHERE barcode = ?", (barcode,)) + conn.commit() + conn.close() + return jsonify({'success': True}) + except Exception as e: + return jsonify({'success': False, 'error': str(e)}), 500 + + +@app.route('/api/admin/product-images/stats') +def api_product_images_stats(): + """이미지 통계""" + import sqlite3 + try: + db_path = os.path.join(os.path.dirname(__file__), 'db', 'product_images.db') + conn = sqlite3.connect(db_path) + cursor = conn.cursor() + + cursor.execute(""" + SELECT status, COUNT(*) as count + FROM product_images + GROUP BY status + """) + stats = {row[0]: row[1] for row in cursor.fetchall()} + + cursor.execute("SELECT COUNT(*) FROM product_images") + total = cursor.fetchone()[0] + + conn.close() + + return jsonify({ + 'success': True, + 'total': total, + 'stats': stats + }) + except Exception as e: + return jsonify({'success': False, 'error': str(e)}), 500 + + if __name__ == '__main__': import os diff --git a/backend/db/product_images_schema.sql b/backend/db/product_images_schema.sql new file mode 100644 index 0000000..6cede58 --- /dev/null +++ b/backend/db/product_images_schema.sql @@ -0,0 +1,38 @@ +-- product_images.db 스키마 +-- yakkok.com에서 크롤링한 제품 이미지 저장 + +CREATE TABLE IF NOT EXISTS product_images ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + barcode TEXT UNIQUE NOT NULL, -- 바코드 (고유키) + drug_code TEXT, -- PIT3000 DrugCode + product_name TEXT NOT NULL, -- 제품명 + search_name TEXT, -- 검색에 사용한 이름 + image_base64 TEXT, -- 이미지 (base64) + image_url TEXT, -- 원본 URL + thumbnail_base64 TEXT, -- 썸네일 (base64, 작은 사이즈) + source TEXT DEFAULT 'yakkok', -- 출처 + status TEXT DEFAULT 'pending', -- pending/success/failed/manual/no_result + error_message TEXT, -- 실패 시 에러 메시지 + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP +); + +-- 인덱스 +CREATE INDEX IF NOT EXISTS idx_barcode ON product_images(barcode); +CREATE INDEX IF NOT EXISTS idx_status ON product_images(status); +CREATE INDEX IF NOT EXISTS idx_drug_code ON product_images(drug_code); +CREATE INDEX IF NOT EXISTS idx_created_at ON product_images(created_at); + +-- 크롤링 로그 테이블 +CREATE TABLE IF NOT EXISTS crawl_logs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + batch_id TEXT, -- 배치 ID + total_count INTEGER DEFAULT 0, -- 전체 개수 + success_count INTEGER DEFAULT 0, -- 성공 개수 + failed_count INTEGER DEFAULT 0, -- 실패 개수 + skipped_count INTEGER DEFAULT 0, -- 스킵 개수 (이미 있음) + started_at DATETIME, + finished_at DATETIME, + status TEXT DEFAULT 'running', -- running/completed/failed + error_message TEXT +); diff --git a/backend/templates/admin_product_images.html b/backend/templates/admin_product_images.html new file mode 100644 index 0000000..7995ee2 --- /dev/null +++ b/backend/templates/admin_product_images.html @@ -0,0 +1,575 @@ + + + + + + 제품 이미지 관리 - yakkok 크롤러 + + + + + +
+
+

🖼️ 제품 이미지 관리

+
+ + ← 어드민 +
+
+ +
+
+
-
+
전체
+
+
+
-
+
성공
+
+
+
-
+
실패
+
+
+
-
+
대기
+
+
+ +
+ + +
+ +
+
이미지 로딩 중...
+
+
+ + + + + + + diff --git a/backend/test_pg.py b/backend/test_pg.py new file mode 100644 index 0000000..e8183b1 --- /dev/null +++ b/backend/test_pg.py @@ -0,0 +1,8 @@ +from sqlalchemy import create_engine, text + +pg_engine = create_engine('postgresql://admin:trajet6640@192.168.0.87:5432/apdb_master') +with pg_engine.connect() as conn: + result = conn.execute(text("SELECT apc, product_name, company_name, main_ingredient FROM apc WHERE product_name LIKE '%아시엔로%' LIMIT 20")) + print('아시엔로 검색 결과:') + for row in result: + print(f' APC: {row[0]} | {row[1]} | {row[2]} | {row[3]}') diff --git a/backend/utils/yakkok_crawler.py b/backend/utils/yakkok_crawler.py new file mode 100644 index 0000000..a533a91 --- /dev/null +++ b/backend/utils/yakkok_crawler.py @@ -0,0 +1,349 @@ +# -*- coding: utf-8 -*- +""" +yakkok.com 제품 이미지 크롤러 +- 제품명으로 검색하여 이미지 URL 추출 +- base64로 변환하여 SQLite에 저장 +""" + +import os +import sys +import sqlite3 +import base64 +import logging +import hashlib +import re +from datetime import datetime +from urllib.parse import quote +import requests +from PIL import Image +from io import BytesIO + +# Playwright 동기 모드 +from playwright.sync_api import sync_playwright + +# 로깅 설정 +logging.basicConfig(level=logging.INFO, format='[%(levelname)s] %(message)s') +logger = logging.getLogger(__name__) + +# DB 경로 +DB_PATH = os.path.join(os.path.dirname(__file__), '..', 'db', 'product_images.db') + +# yakkok.com 설정 +YAKKOK_BASE_URL = "https://yakkok.com" +YAKKOK_SEARCH_URL = "https://yakkok.com/search?q={query}" + + +def init_db(): + """DB 초기화""" + conn = sqlite3.connect(DB_PATH) + cursor = conn.cursor() + + # 스키마 파일 실행 + schema_path = os.path.join(os.path.dirname(__file__), '..', 'db', 'product_images_schema.sql') + if os.path.exists(schema_path): + with open(schema_path, 'r', encoding='utf-8') as f: + cursor.executescript(f.read()) + conn.commit() + + conn.close() + logger.info(f"[DB] 초기화 완료: {DB_PATH}") + + +def get_existing_barcodes(): + """이미 저장된 바코드 목록 조회""" + conn = sqlite3.connect(DB_PATH) + cursor = conn.cursor() + cursor.execute("SELECT barcode FROM product_images WHERE status IN ('success', 'manual')") + barcodes = set(row[0] for row in cursor.fetchall()) + conn.close() + return barcodes + + +def save_product_image(barcode, drug_code, product_name, search_name, + image_base64, image_url, thumbnail_base64=None, + status='success', error_message=None): + """제품 이미지 저장""" + conn = sqlite3.connect(DB_PATH) + cursor = conn.cursor() + + cursor.execute(""" + INSERT OR REPLACE INTO product_images + (barcode, drug_code, product_name, search_name, image_base64, image_url, + thumbnail_base64, status, error_message, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """, (barcode, drug_code, product_name, search_name, image_base64, image_url, + thumbnail_base64, status, error_message, datetime.now().isoformat())) + + conn.commit() + conn.close() + logger.info(f"[DB] 저장 완료: {product_name} ({barcode}) - {status}") + + +def download_image_as_base64(url, max_size=500): + """이미지 다운로드 후 base64 변환 (리사이즈 포함)""" + try: + headers = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' + } + response = requests.get(url, headers=headers, timeout=10) + response.raise_for_status() + + # PIL로 이미지 열기 + img = Image.open(BytesIO(response.content)) + + # RGBA -> RGB 변환 (JPEG 저장용) + if img.mode == 'RGBA': + bg = Image.new('RGB', img.size, (255, 255, 255)) + bg.paste(img, mask=img.split()[3]) + img = bg + elif img.mode != 'RGB': + img = img.convert('RGB') + + # 리사이즈 (비율 유지) + if max(img.size) > max_size: + ratio = max_size / max(img.size) + new_size = tuple(int(dim * ratio) for dim in img.size) + img = img.resize(new_size, Image.LANCZOS) + + # base64 변환 + buffer = BytesIO() + img.save(buffer, format='JPEG', quality=85) + base64_str = base64.b64encode(buffer.getvalue()).decode('utf-8') + + return base64_str + except Exception as e: + logger.error(f"[ERROR] 이미지 다운로드 실패: {url} - {e}") + return None + + +def clean_product_name(name): + """검색용 제품명 정리""" + # 괄호 안 내용 제거 (용량 등) + name = re.sub(r'\([^)]*\)', '', name) + # 숫자+단위 제거 (100ml, 500mg 등) + name = re.sub(r'\d+\s*(ml|mg|g|kg|정|캡슐|T|t|개|EA|ea)', '', name, flags=re.IGNORECASE) + # 특수문자 제거 + name = re.sub(r'[_\-/\\]', ' ', name) + # 연속 공백 정리 + name = re.sub(r'\s+', ' ', name).strip() + return name + + +def search_yakkok(page, product_name): + """yakkok.com에서 제품 검색하여 이미지 URL 반환""" + try: + # 검색어 정리 + search_name = clean_product_name(product_name) + if not search_name: + search_name = product_name + + # 검색 페이지 접속 + search_url = YAKKOK_SEARCH_URL.format(query=quote(search_name)) + page.goto(search_url, wait_until='networkidle', timeout=15000) + + # 잠시 대기 + page.wait_for_timeout(1000) + + # 첫 번째 검색 결과의 이미지 찾기 + img_selector = 'img[alt]' + images = page.query_selector_all(img_selector) + + for img in images: + src = img.get_attribute('src') + alt = img.get_attribute('alt') or '' + + # 로고, 아이콘 등 제외 + if not src or 'logo' in src.lower() or 'icon' in src.lower(): + continue + + # 검색 아이콘 등 제외 + if alt in ['검색', '홈', '마이', '재고콕', '약콕인증', '뒤로가기']: + continue + + # 제품 이미지로 보이는 것 반환 + if src.startswith('http') or src.startswith('//'): + if src.startswith('//'): + src = 'https:' + src + return src, search_name + + return None, search_name + + except Exception as e: + logger.error(f"[ERROR] 검색 실패: {product_name} - {e}") + return None, search_name + + +def crawl_products(products, headless=True): + """ + 제품 목록 크롤링 + products: [(barcode, drug_code, product_name), ...] + """ + init_db() + existing = get_existing_barcodes() + + # 새로 크롤링할 제품만 필터 + to_crawl = [(b, d, n) for b, d, n in products if b not in existing] + + if not to_crawl: + logger.info("[INFO] 크롤링할 새 제품이 없습니다.") + return {'total': 0, 'success': 0, 'failed': 0, 'skipped': len(products)} + + logger.info(f"[INFO] 크롤링 시작: {len(to_crawl)}개 (스킵: {len(products) - len(to_crawl)}개)") + + results = {'total': len(to_crawl), 'success': 0, 'failed': 0, 'skipped': len(products) - len(to_crawl)} + + with sync_playwright() as p: + browser = p.chromium.launch(headless=headless) + context = browser.new_context( + viewport={'width': 390, 'height': 844}, # 모바일 뷰포트 + user_agent='Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15' + ) + page = context.new_page() + + for barcode, drug_code, product_name in to_crawl: + try: + logger.info(f"[CRAWL] {product_name} ({barcode})") + + # yakkok 검색 + image_url, search_name = search_yakkok(page, product_name) + + if image_url: + # 이미지 다운로드 & base64 변환 + image_base64 = download_image_as_base64(image_url) + thumbnail_base64 = download_image_as_base64(image_url, max_size=100) + + if image_base64: + save_product_image( + barcode=barcode, + drug_code=drug_code, + product_name=product_name, + search_name=search_name, + image_base64=image_base64, + image_url=image_url, + thumbnail_base64=thumbnail_base64, + status='success' + ) + results['success'] += 1 + else: + save_product_image( + barcode=barcode, + drug_code=drug_code, + product_name=product_name, + search_name=search_name, + image_base64=None, + image_url=image_url, + status='failed', + error_message='이미지 다운로드 실패' + ) + results['failed'] += 1 + else: + save_product_image( + barcode=barcode, + drug_code=drug_code, + product_name=product_name, + search_name=search_name, + image_base64=None, + image_url=None, + status='no_result', + error_message='검색 결과 없음' + ) + results['failed'] += 1 + + # 요청 간 딜레이 + page.wait_for_timeout(500) + + except Exception as e: + logger.error(f"[ERROR] {product_name}: {e}") + save_product_image( + barcode=barcode, + drug_code=drug_code, + product_name=product_name, + search_name=product_name, + image_base64=None, + image_url=None, + status='failed', + error_message=str(e) + ) + results['failed'] += 1 + + browser.close() + + logger.info(f"[DONE] 완료 - 성공: {results['success']}, 실패: {results['failed']}, 스킵: {results['skipped']}") + return results + + +def get_today_sales_products(): + """오늘 판매된 제품 목록 조회 (MSSQL)""" + try: + # 상위 폴더의 db 모듈 import + sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) + from db.dbsetup import db_manager + from sqlalchemy import text + + session = db_manager.get_session('PM_PRES') + + today = datetime.now().strftime('%Y%m%d') + + # 오늘 판매된 품목 조회 (중복 제거) + query = text(""" + SELECT DISTINCT + COALESCE(NULLIF(G.Barcode, ''), + (SELECT TOP 1 CD_CD_BARCODE FROM PM_DRUG.dbo.CD_ITEM_UNIT_MEMBER WHERE DrugCode = S.DrugCode) + ) AS barcode, + S.DrugCode AS drug_code, + ISNULL(G.GoodsName, '알수없음') AS product_name + FROM SALE_SUB S + LEFT JOIN PM_DRUG.dbo.CD_GOODS G ON S.DrugCode = G.DrugCode + WHERE S.SL_NO_order LIKE :today_pattern + AND S.DrugCode IS NOT NULL + """) + + result = session.execute(query, {'today_pattern': f'{today}%'}).fetchall() + + products = [] + for row in result: + barcode = row[0] + if barcode: # 바코드 있는 것만 + products.append((barcode, row[1], row[2])) + + logger.info(f"[MSSQL] 오늘 판매 품목: {len(products)}개") + return products + + except Exception as e: + logger.error(f"[ERROR] MSSQL 조회 실패: {e}") + return [] + + +def crawl_today_sales(headless=True): + """오늘 판매된 제품 이미지 크롤링""" + products = get_today_sales_products() + if not products: + return {'total': 0, 'success': 0, 'failed': 0, 'skipped': 0, 'message': '오늘 판매 내역 없음'} + + return crawl_products(products, headless=headless) + + +# CLI 실행 +if __name__ == '__main__': + import argparse + + parser = argparse.ArgumentParser(description='yakkok.com 제품 이미지 크롤러') + parser.add_argument('--today', action='store_true', help='오늘 판매 제품 크롤링') + parser.add_argument('--product', type=str, help='특정 제품명으로 테스트') + parser.add_argument('--visible', action='store_true', help='브라우저 표시') + + args = parser.parse_args() + + if args.today: + result = crawl_today_sales(headless=not args.visible) + print(f"\n결과: {result}") + elif args.product: + # 테스트용 단일 제품 크롤링 + test_products = [('TEST001', 'TEST', args.product)] + result = crawl_products(test_products, headless=not args.visible) + print(f"\n결과: {result}") + else: + print("사용법:") + print(" python yakkok_crawler.py --today # 오늘 판매 제품 크롤링") + print(" python yakkok_crawler.py --product 타이레놀 # 특정 제품 테스트") + print(" python yakkok_crawler.py --visible # 브라우저 표시") diff --git a/docs/ANIMAL_DRUG_APC_MAPPING.html b/docs/ANIMAL_DRUG_APC_MAPPING.html new file mode 100644 index 0000000..ae05783 --- /dev/null +++ b/docs/ANIMAL_DRUG_APC_MAPPING.html @@ -0,0 +1,515 @@ + + + + + 스마트헬스케어 사업제안서 + + + +

동물약 APC 매핑 가이드

+
+

최종 업데이트: 2026-03-02

+
+

개요

+

POS(PIT3000)의 동물약 제품을 APDB의 APC 코드와 매핑하여 제품 정보(용법, 용량, 주의사항) 및 이미지를 표시하기 위한 작업 가이드.

+
+

현재 상태

+

매핑 현황

+ + + + + + + + + + + + + + + + + + + + + + + + + +
구분개수비율
동물약 총39개100%
APC 매핑됨7개18%
APC 미매핑32개82%
+

매핑 완료 제품

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
POS 제품명DrugCodeAPC
(판)복합개시딘LB0000031400231093520106
안텔민킹(5kg이상)LB0000031580230237810109
안텔민뽀삐(5kg이하)LB0000031570230237010107
파라캅L(5kg이상)LB0000031590230338510101
파라캅S(5kg이하)LB0000031600230347110106
세레니아정16mg(개멀미약)LB0000033530231884610109
세레니아정24mg(개멀미약)LB0000033540231884620107
+
+

매핑 구조

+

데이터베이스 연결

+
MSSQL (192.168.0.4\PM2014)          PostgreSQL (192.168.0.87:5432)
+┌─────────────────────────┐         ┌─────────────────────────┐
+│ PM_DRUG.CD_GOODS        │         │ apdb_master.apc         │
+│ - DrugCode              │         │ - apc (PK)              │
+│ - GoodsName             │         │ - product_name          │
+│ - BARCODE               │         │ - image_url1            │
+│                         │         │ - llm_pharm (JSONB)     │
+├─────────────────────────┤         └─────────────────────────┘
+│ PM_DRUG.CD_ITEM_UNIT_   │
+│ MEMBER                  │
+│ - DRUGCODE (FK)         │
+│ - CD_CD_BARCODE ◀───────┼── APC 코드 저장 (023%로 시작)
+│ - CHANGE_DATE           │
+└─────────────────────────┘
+
+

APC 매핑 방식

+
    +
  1. CD_ITEM_UNIT_MEMBER 테이블에 추가 바코드로 APC 등록
  2. +
  3. 기존 바코드는 유지, APC를 별도 레코드로 INSERT
  4. +
  5. APC 코드는 023%로 시작 (식별자)
  6. +
+
+

1:1 매핑 가능 후보

+

✅ 확실한 매핑 (1개)

+ + + + + + + + + + + + + + + + + + + +
POS 제품명DrugCodeAPCAPDB 제품명이미지
제스타제(10정)LB0000031468809720800455제스타제✅ 있음
+

⚠️ 검토 필요 (1개)

+ + + + + + + + + + + + + + + + + +
POS 제품명DrugCodeAPC 후보비고
안텔민S00000010230237800003"안텔민킹"과 "안텔민뽀삐"는 이미 별도 매핑됨. 이 제품이 무엇인지 확인 필요
+

❌ APDB에 없음 (3개)

+ + + + + + + + + + + + + + + + + + + + + +
POS 제품명사유
(판)클라펫정50(100정)APDB엔 "클라펫 정"만 있음 (함량 불일치)
넥스가드xs(2~3.5kg)사이즈별 APC 없음
캐치원캣(2.5~7.5kg)/고양이APDB에 캐치원 자체가 없음
+
+

1:N 매핑 필요 제품 (27개)

+

사이즈별로 세분화된 제품들. 하나의 APDB APC에 여러 POS 제품을 매핑해야 함.

+

브랜드별 현황

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
브랜드POS 제품 수APDB 존재비고
다이로하트3개 (SS/S/M)다이로하트 츄어블 정
하트세이버4개 (mini/S/M/L)하트세이버 플러스 츄어블
하트웜솔루션2개 (S/M)APDB에 없음
리펠로2개 (S/M)리펠로액 (이미지 있음!)
캐치원5개 (SS/S/M/L/캣)APDB에 없음
셀라이트5개 (SS/S/M/L/XL)셀라이트 액
넥스가드2개 (xs/L)넥스가드 스펙트라
가드닐3개 (S/M/L)가드닐 액
심피드2개 (M/L)APDB에 없음
하트캅1개하트캅-츄어블 정
+
+

APDB 통계

+ + + + + + + + + + + + + + + + + + + + + + + + + +
항목수치
전체 APC16,326개
이미지 있음73개 (0.4%)
LLM 정보 있음81개 (0.5%)
동물 관련 키워드~200개
+

⚠️ 주의: APDB에 이미지가 거의 없음. 이미지 표시가 목적이라면 다른 소스 필요.

+
+

매핑 스크립트

+

매핑 후보 찾기

+
python backend/scripts/batch_apc_matching.py
+
+

1:1 매핑 가능 후보 추출

+
python backend/scripts/find_1to1_candidates.py
+
+

매핑 실행 (수동)

+
# backend/scripts/batch_insert_apc.py 참고
+MAPPINGS = [
+    ('제스타제(10정)', 'LB000003146', '8809720800455'),
+]
+
+

INSERT 쿼리 예시

+
INSERT INTO CD_ITEM_UNIT_MEMBER (
+    DRUGCODE, CD_CD_UNIT, CD_NM_UNIT, CD_MY_UNIT, CD_IN_UNIT,
+    CD_CD_BARCODE, CD_CD_POS, CHANGE_DATE
+) VALUES (
+    'LB000003146',  -- DrugCode
+    '015',          -- 단위코드
+    1.0,            -- 단위명
+    <기존값>,        -- CD_MY_UNIT (기존 레코드에서 복사)
+    <기존값>,        -- CD_IN_UNIT (기존 레코드에서 복사)
+    '8809720800455', -- APC 바코드
+    '',
+    '20260302'      -- 변경일자
+)
+
+
+

다음 단계

+
    +
  1. 제스타제 1:1 매핑 실행
  2. +
  3. 안텔민(S0000001) 제품 확인 후 결정
  4. +
  5. 1:N 매핑 정책 결정 (사이즈별 제품 → 동일 APC?)
  6. +
  7. 이미지 소스 대안 검토 (필요시)
  8. +
+
+

관련 파일

+
    +
  • backend/db/dbsetup.py - DB 연결 설정
  • +
  • backend/scripts/batch_apc_matching.py - 매칭 후보 찾기
  • +
  • backend/scripts/batch_insert_apc.py - 매핑 실행
  • +
  • backend/scripts/find_1to1_candidates.py - 1:1 후보 추출
  • +
  • backend/app.py - _get_animal_drugs(), _get_animal_drug_rag()
  • +
+ + \ No newline at end of file