# OTC 용법 라벨 시스템 ## 1. 시스템 개요 ### OTC 라벨이란? OTC(Over-The-Counter) 약품 판매 시 부착하는 **용법·용량 안내 라벨**입니다. 약사가 직접 설명하는 것 외에 시각적 보조 자료로, 복용 방법과 효능을 명확히 전달합니다. ### 전체 흐름 ``` 바코드 스캔 → POS 연동 → 웹 관리 페이지 → 미리보기 → Brother 프린터 출력 ``` 1. **POS에서 바코드 스캔** → URL 호출 (`?barcode=...&name=...`) 2. **관리 페이지 자동 로드** → 기존 프리셋이 있으면 채움 3. **효능/용법 입력** → 실시간 미리보기 4. **인쇄** → Brother QL-810W로 29mm 라벨 출력 5. **프리셋 저장** → 다음 번엔 바코드만 스캔하면 바로 인쇄 --- ## 2. 아키텍처 ### 2.1 시스템 구성도 ``` ┌─────────────────┐ ┌──────────────────────────────┐ │ POS (PIT3000) │────▶│ Flask 서버 (port 7001) │ │ 바코드 스캔 │ │ /admin/otc-labels │ └─────────────────┘ └───────────┬──────────────────┘ │ ┌────────────────┼────────────────┐ ▼ ▼ ▼ ┌──────────────┐ ┌────────────┐ ┌──────────────┐ │ SQLite │ │ MSSQL │ │ Brother │ │ (프리셋 저장) │ │ (약품 정보) │ │ QL-810W │ │ mileage.db │ │ PM_DRUG │ │ 192.168.0.168│ └──────────────┘ └────────────┘ └──────────────┘ ``` ### 2.2 Flask 라우트 구조 ``` /admin/otc-labels ← 관리 페이지 (HTML) /api/admin/otc-labels ← 프리셋 목록 조회 / 등록·수정 /api/admin/otc-labels/ ← 단건 조회 / 삭제 /api/admin/otc-labels/preview ← 미리보기 이미지 생성 /api/admin/otc-labels/print ← 라벨 인쇄 /api/admin/otc-labels/search-mssql ← MSSQL 약품 검색 /api/otc-label-print/ ← 외부 GET 인쇄 (CORS 지원) /api/otc-label-check ← 프리셋 존재 여부 일괄 확인 ``` ### 2.3 DB 연결 | DB | 용도 | 연결 방식 | |---|---|---| | **SQLite** | 라벨 프리셋 저장 | `db_manager.get_sqlite_connection()` | | **MSSQL** | 약품 마스터 (CD_GOODS) | `db_manager.get_session('PM_DRUG')` | - SQLite DB 경로: `backend/db/mileage.db` - MSSQL 인스턴스: `192.168.0.4\PM2014` ### 2.4 프린터 연동 | 항목 | 값 | |---|---| | 프린터 | Brother QL-810W | | IP | 192.168.0.168 | | 포트 | 9100 (TCP) | | 용지 | 29mm 연속 라벨 | | 라이브러리 | `brother_ql` | --- ## 3. API 엔드포인트 ### 3.1 관리 페이지 ``` GET /admin/otc-labels GET /admin/otc-labels?barcode=8806436003118&name=노바손크림 ``` - URL 파라미터로 바코드/이름 전달 시 자동 로드 --- ### 3.2 프리셋 목록 조회 ```http GET /api/admin/otc-labels ``` **응답 예시:** ```json { "success": true, "count": 5, "labels": [ { "id": 1, "barcode": "8806436003118", "drug_code": "DR001", "display_name": "노바손크림", "effect": "습진, 피부염", "dosage_instruction": "1일 2회, 환부에 얇게 도포", "usage_tip": "눈 주위 사용 금지", "use_wide_format": true, "print_count": 12, "last_printed_at": "2026-03-19 15:30:00", "created_at": "...", "updated_at": "..." } ] } ``` --- ### 3.3 프리셋 단건 조회 ```http GET /api/admin/otc-labels/{barcode} ``` **응답 예시:** ```json { "success": true, "exists": true, "label": { /* 프리셋 데이터 */ } } ``` **프리셋 없는 경우 (404):** ```json { "success": false, "error": "등록된 프리셋이 없습니다.", "exists": false } ``` --- ### 3.4 프리셋 등록/수정 (Upsert) ```http POST /api/admin/otc-labels Content-Type: application/json { "barcode": "8806436003118", "drug_code": "DR001", "display_name": "노바손크림", "effect": "습진, 피부염", "dosage_instruction": "1일 2회, 환부에 얇게 도포", "usage_tip": "눈 주위 사용 금지", "use_wide_format": true } ``` **필수 필드:** `barcode` **동작:** 바코드가 이미 존재하면 UPDATE, 없으면 INSERT --- ### 3.5 프리셋 삭제 ```http DELETE /api/admin/otc-labels/{barcode} ``` --- ### 3.6 미리보기 이미지 생성 ```http POST /api/admin/otc-labels/preview Content-Type: application/json { "drug_name": "노바손크림", "effect": "습진, 피부염", "dosage_instruction": "1일 2회, 환부에 얇게 도포", "usage_tip": "눈 주위 사용 금지" } ``` **응답:** ```json { "success": true, "preview_url": "data:image/png;base64,iVBORw0KGgo..." } ``` --- ### 3.7 라벨 인쇄 ```http POST /api/admin/otc-labels/print Content-Type: application/json { "barcode": "8806436003118", "drug_name": "노바손크림", "effect": "습진, 피부염", "dosage_instruction": "1일 2회", "usage_tip": "" } ``` **동작:** 인쇄 후 `print_count` 증가, `last_printed_at` 갱신 --- ### 3.8 외부 GET 인쇄 (CORS 지원) ```http GET /api/otc-label-print/{barcode} ``` - **프리셋 있음** → 해당 데이터로 즉시 인쇄 - **프리셋 없음** → 404 반환 (인쇄 안 함) - POS 등 외부 시스템에서 URL 호출로 바로 인쇄 가능 --- ### 3.9 MSSQL 약품 검색 ```http GET /api/admin/otc-labels/search-mssql?q=노바손 ``` **응답:** ```json { "success": true, "count": 3, "drugs": [ { "drug_code": "DR001", "barcode": "8806436003118", "goods_name": "노바손크림30g", "sale_price": 8500.0 } ] } ``` **쿼리 대상:** - `CD_GOODS.GoodsName` (약품명) - `CD_GOODS.Barcode` (바코드) - `CD_ITEM_UNIT_MEMBER.CD_CD_BARCODE` (포장 단위 바코드) --- ### 3.10 프리셋 존재 여부 일괄 확인 ```http GET /api/otc-label-check?barcodes=8806436003118,8806436058613 # 또는 POST /api/otc-label-check Content-Type: application/json { "barcodes": ["8806436003118", "8806436058613"] } ``` **응답:** ```json { "success": true, "total": 2, "found": 1, "results": { "8806436003118": true, "8806436058613": false } } ``` --- ## 4. DB 스키마 ### 4.1 테이블: `otc_label_presets` (SQLite) | 컬럼 | 타입 | 설명 | |---|---|---| | `id` | INTEGER | PK, 자동 증가 | | `barcode` | VARCHAR(20) | **UNIQUE**, 바코드 (실질적 PK) | | `drug_code` | VARCHAR(20) | MSSQL DrugCode (참조용) | | `display_name` | VARCHAR(100) | 표시 이름 (오버라이드) | | `effect` | VARCHAR(100) | 효능 (예: "치통, 두통") | | `dosage_instruction` | TEXT | 용법 (예: "1일 3회, 1회 1정") | | `usage_tip` | TEXT | 부가 설명 | | `use_wide_format` | BOOLEAN | 와이드 포맷 사용 여부 | | `print_count` | INTEGER | 인쇄 횟수 (통계) | | `last_printed_at` | DATETIME | 마지막 인쇄 시간 | | `created_at` | DATETIME | 생성 시간 | | `updated_at` | DATETIME | 수정 시간 | **인덱스:** - `idx_otc_label_barcode` (barcode) - `idx_otc_label_drug_code` (drug_code) --- ### 4.2 MSSQL 테이블: `CD_GOODS` (약품 마스터) 검색 시 조회하는 테이블: ```sql SELECT TOP 20 G.DrugCode, COALESCE(NULLIF(G.Barcode, ''), (SELECT TOP 1 CD_CD_BARCODE FROM CD_ITEM_UNIT_MEMBER WHERE DrugCode = G.DrugCode) ) AS Barcode, G.GoodsName, G.Saleprice FROM CD_GOODS G WHERE G.GoodsName LIKE '%검색어%' OR G.Barcode LIKE '%검색어%' OR G.DrugCode IN (SELECT DrugCode FROM CD_ITEM_UNIT_MEMBER WHERE CD_CD_BARCODE LIKE '%검색어%') ``` --- ## 5. 라벨 이미지 생성 ### 5.1 이미지 사양 | 항목 | 값 | |---|---| | 크기 | 800 × 306 px | | 색상 | 1-bit (흑백) | | 폰트 | 맑은 고딕 Bold (`malgunbd.ttf`) | ### 5.2 레이아웃 ``` ┌────────────────────────────────────────────┐ │ [효능 - 72pt, 굵게, 중앙 상단] │ │ │ │ □ 용법 - 40pt, 왼쪽 정렬 │ │ [약품명 36pt] │ │ □ 부가 설명 - 26pt ┌──────────┐ │ │ │ 청춘약국 │ │ │ └──────────┘ │ └────────────────────────────────────────────┘ ``` ### 5.3 인쇄 과정 1. PIL로 이미지 생성 (가로 800 × 세로 306) 2. 90도 회전 (Brother QL은 세로 기준) 3. `brother_ql` 라이브러리로 래스터 변환 4. TCP 9100 포트로 전송 --- ## 6. 관련 파일 목록 ### 6.1 핵심 파일 | 파일 | 역할 | |---|---| | `backend/app.py` | Flask 라우트 (7730~8200줄) | | `backend/utils/otc_label_printer.py` | 이미지 생성 & 프린터 출력 | | `backend/templates/admin_otc_labels.html` | 관리 페이지 UI | | `backend/db/mileage_schema.sql` | 테이블 스키마 | | `backend/db/mileage.db` | SQLite DB | ### 6.2 app.py 내 주요 함수 | 함수명 | 라인 | 설명 | |---|---|---| | `admin_otc_labels()` | 7735 | 관리 페이지 렌더링 | | `api_get_otc_labels()` | 7741 | 목록 조회 | | `api_get_otc_label()` | 7770 | 단건 조회 | | `api_upsert_otc_label()` | 7801 | 등록/수정 | | `api_delete_otc_label()` | 7848 | 삭제 | | `api_preview_otc_label()` | 7868 | 미리보기 | | `api_print_otc_label()` | 7900 | 인쇄 | | `api_otc_label_print_by_barcode()` | 7948 | GET 인쇄 | | `api_otc_label_check()` | 8039 | 일괄 확인 | | `api_search_mssql_drug()` | 8117 | MSSQL 검색 | ### 6.3 otc_label_printer.py 함수 | 함수명 | 설명 | |---|---| | `create_otc_label_image()` | 라벨 이미지 생성 (PIL) | | `print_otc_label()` | Brother QL로 인쇄 | | `generate_preview_image()` | Base64 미리보기 생성 | --- ## 7. 트러블슈팅 ### 프린터 연결 테스트 ```python import socket sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(3) result = sock.connect_ex(("192.168.0.168", 9100)) print("OK" if result == 0 else f"FAIL: {result}") sock.close() ``` ### 모듈 로드 실패 `OTC_LABEL_AVAILABLE = False` 로그 발생 시: - `brother_ql` 설치 확인: `pip install brother_ql` - Pillow 설치 확인: `pip install Pillow` ### 폰트 깨짐 - Windows: `C:/Windows/Fonts/malgunbd.ttf` 존재 확인 - 대체 폰트: NanumGothicBold 등 --- ## 8. 사용 예시 ### POS에서 라벨 페이지 열기 ``` https://mile.0bin.in/admin/otc-labels?barcode=8806436003118&name=노바손크림 ``` ### 외부 시스템에서 바로 인쇄 ```bash curl https://mile.0bin.in/api/otc-label-print/8806436003118 ``` ### 프리셋 일괄 등록 (스크립트) ```python import requests labels = [ {"barcode": "8806436003118", "display_name": "노바손크림", "effect": "습진", "dosage_instruction": "1일 2회"}, {"barcode": "8806436058613", "display_name": "게보린", "effect": "두통", "dosage_instruction": "1회 1정"}, ] for label in labels: requests.post("https://mile.0bin.in/api/admin/otc-labels", json=label) ``` --- *문서 작성: 2026-03-19*