docs: 약학정보원 JSON API 정답 경로 추가 (로컬 BLOB 우회 해법)
다른 AI가 로컬 BLOB(DrugImg) 매칭키에서 막힌 부분을, 운영 POS에
실제 구현·검증한 약정원 JSON API 경로로 보강. 혼동 방지용 결정 문서.
- docs/healthkr-json-api-SOLVED.md: 정답 경로 단일 진입점.
· 연결고리 = CD_GOODS.BARCODE → CD_BARCODE.DIK_CODE (CD_GOODS.DIK
직접 14/19, 바코드 경로 18/19. 용각산(대) 실증).
· 정답 엔드포인트 = ajax_result_drug2.asp (JSON 64필드+이미지URL).
result_drug_simple.asp는 사람용 HTML, zoom.asp 파싱 불필요.
· 로컬 BLOB 안 씀 → 44자 base64 image key 문제 자체가 사라짐.
- README: 핵심결론 최상단에 SOLVED 문서 포인터.
- scripts/probe_healthkr_json.py: 정답 경로 재현 스크립트(비번 CHANGEME).
기존 drugimg-key-findings.md(BLOB 분석)는 보존 — "BLOB 쓸 때의 문제"로
관계 명시.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,11 @@ PIT3000/PM2000V6에서 확인한 **의약품 상세정보, 식별 이미지, 약
|
|||||||
|
|
||||||
## 핵심 결론
|
## 핵심 결론
|
||||||
|
|
||||||
|
> ★**이미지/약품정보를 실제로 가져오려면 [docs/healthkr-json-api-SOLVED.md](docs/healthkr-json-api-SOLVED.md) 를 보세요.**
|
||||||
|
> 로컬 BLOB(`PM_IMAGE..DrugImg`) 매칭키 분석은 막혔지만, **약학정보원 JSON API**로 평문 DIK_CODE로 안정 해결됨(운영 POS 반영).
|
||||||
|
> 정답 경로 = `CD_GOODS.BARCODE → CD_BARCODE.DIK_CODE → ajax_result_drug2.asp(JSON)`. CD_GOODS.DIK_CODE 직접·로컬 BLOB은 함정.
|
||||||
|
|
||||||
|
0. **(권장 경로)** 약품 이미지·상세는 로컬 BLOB이 아니라 **약학정보원 JSON API**에서 받는다. 위 SOLVED 문서 참조.
|
||||||
1. 화면/인쇄에 쓰이는 약품 상세정보는 로컬 DB(`PM_DRUG`, `PM_IMAGE`)와 외부 health.kr URL을 병행합니다.
|
1. 화면/인쇄에 쓰이는 약품 상세정보는 로컬 DB(`PM_DRUG`, `PM_IMAGE`)와 외부 health.kr URL을 병행합니다.
|
||||||
2. `DRUGCODE`에서 `DIK_CODE`, `SUNG_CODE`를 조회합니다.
|
2. `DRUGCODE`에서 `DIK_CODE`, `SUNG_CODE`를 조회합니다.
|
||||||
3. `SUNG_CODE`는 **성분코드**입니다. 보험코드가 아닙니다.
|
3. `SUNG_CODE`는 **성분코드**입니다. 보험코드가 아닙니다.
|
||||||
|
|||||||
86
docs/healthkr-json-api-SOLVED.md
Normal file
86
docs/healthkr-json-api-SOLVED.md
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
# ★해결: 약학정보원(health.kr) JSON API로 약품정보+이미지 가져오기
|
||||||
|
|
||||||
|
> 이 문서는 **실제 운영 POS(ANIPOS)에 구현·배포된 정답 경로**입니다.
|
||||||
|
> 로컬 BLOB(`PM_IMAGE..DrugImg`) 매칭키 분석([drugimg-key-findings.md](drugimg-key-findings.md))은
|
||||||
|
> "암호화 image key라 어렵다"에서 멈췄지만 — **애초에 로컬 BLOB을 쓸 필요가 없습니다.**
|
||||||
|
> 약품 이미지·상세정보는 **약학정보원 JSON API**에서 평문 DIK_CODE로 안정적으로 받습니다.
|
||||||
|
> 실측 환경: `192.168.0.69\PM2014` (PIT3000), 검증일 2026-06-29.
|
||||||
|
|
||||||
|
## TL;DR — 다른 AI는 이것만 보면 됨
|
||||||
|
|
||||||
|
```
|
||||||
|
POS 상품(DrugCode 또는 바코드)
|
||||||
|
→ PM_DRUG..CD_GOODS.BARCODE (마스터 바코드)
|
||||||
|
→ PM_DRUG..CD_BARCODE.DIK_CODE ★연결고리는 여기 (CD_GOODS.DIK_CODE 아님)
|
||||||
|
→ health.kr drug_cd = 평문 DIK_CODE
|
||||||
|
→ https://health.kr/searchDrug/ajax/ajax_result_drug2.asp?drug_cd={DIK}
|
||||||
|
→ JSON 배열 [{...64필드...}] : 약품명/성분/효능/이미지URL 전부
|
||||||
|
```
|
||||||
|
|
||||||
|
- **로컬 BLOB(DrugImg) 안 씀.** 44자 base64 image key 푸는 노력 불필요.
|
||||||
|
- **직접 이미지 URL 추측 금지.** `big3/{DIK}.jpg`는 대부분 404 — JSON 안의 이미지 필드를 써야 함.
|
||||||
|
|
||||||
|
## ★핵심 함정 1 — DIK_CODE 출처는 CD_BARCODE
|
||||||
|
|
||||||
|
판매상세/장바구니의 시작점은 `DrugCode`(예 `LB000000169`, `ZP00000178`, 조제약은 9자리).
|
||||||
|
거기서 DIK로 가는 경로 도달률 실측 (19개 샘플):
|
||||||
|
|
||||||
|
| 경로 | 도달 | 비고 |
|
||||||
|
|---|---|---|
|
||||||
|
| **A: `CD_GOODS.BARCODE → CD_BARCODE.DIK_CODE`** | **18/19** ✅ 정답 | |
|
||||||
|
| B: `CD_BARCODE.DRUGCODE` 직접 조인 | 0/19 | CD_BARCODE.DRUGCODE엔 우리 DrugCode 없음 |
|
||||||
|
| C: `CD_GOODS.DIK_CODE` 직접 | 14/19 | LB채번 약 누락 |
|
||||||
|
|
||||||
|
> 실증: **용각산(대) `LB000000169`** = `CD_GOODS.DIK_CODE = NULL`(C 실패)인데
|
||||||
|
> `BARCODE 8806419031329 → CD_BARCODE.DIK = A11AJJJJJ0220`(A 성공) → health.kr 이미지 `exists=True`.
|
||||||
|
> CD_GOODS.DIK 직접만 믿으면 이런 약이 통째로 빠진다.
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT TOP 1 b.DIK_CODE
|
||||||
|
FROM PM_DRUG..CD_GOODS g
|
||||||
|
JOIN PM_DRUG..CD_BARCODE b ON g.BARCODE = b.BARCODE
|
||||||
|
WHERE g.DrugCode = ? AND ISNULL(b.DIK_CODE,'') <> '';
|
||||||
|
```
|
||||||
|
|
||||||
|
CD_BARCODE.DIK_CODE 채움률 = **210,529 / 307,078 (69%)** (CD_GOODS.DIK 44%보다 높음).
|
||||||
|
조제약(ETC)도 9자리 약품코드로 동일 경로 도달 9/10 (바코드 없는 약만 누락).
|
||||||
|
|
||||||
|
## ★핵심 함정 2 — 정답 엔드포인트는 ajax_result_drug2.asp (JSON)
|
||||||
|
|
||||||
|
```
|
||||||
|
사람용 HTML : https://health.kr/searchDrug/result_drug_simple.asp?drug_cd={DIK}
|
||||||
|
정답 JSON : https://health.kr/searchDrug/ajax/ajax_result_drug2.asp?drug_cd={DIK}
|
||||||
|
```
|
||||||
|
|
||||||
|
- 응답 content-type은 `text/html`이지만 본문은 **순수 JSON 배열**(`json.loads` OK).
|
||||||
|
- `zoom.asp` 파싱조차 불필요 — JSON에 이미지 URL이 직접 들어있다.
|
||||||
|
- 헤더에 `Referer: https://health.kr/` 권장.
|
||||||
|
|
||||||
|
### JSON 주요 필드 (64개 중)
|
||||||
|
| 필드 | 내용 |
|
||||||
|
|---|---|
|
||||||
|
| `drug_name` / `drug_enm` | 한글명 / 영문명 |
|
||||||
|
| `upso_name` | `제조사\|영문\|주소\|전화\|...` (파이프 구분, 첫 토큰만) |
|
||||||
|
| `list_sunb_name` | 성분 |
|
||||||
|
| `cls_code` | 약효분류 / `drug_form` 제형 / `charact` 성상 |
|
||||||
|
| `boh` | 보험/비급여 |
|
||||||
|
| `effect`/`dosage`/`caution`/`mediguide`/`additives` | 효능/용법/주의/복약지도/첨가제 |
|
||||||
|
| `drug_pic` / `pack_img` | 낱알/포장 이미지 URL (`common.health.kr/...`) |
|
||||||
|
| `picto_img` | 픽토그램 URL들, `\|` 구분 |
|
||||||
|
|
||||||
|
⚠ 본문 텍스트(effect 등)는 줄바꿈을 리터럴 `brbr` / `<P></P>` / `</br>`로 섞어 줌 → `<br>`로 정리 필요.
|
||||||
|
⚠ 이미지 필드는 빈 값일 수 있음(약마다 커버리지 다름). URL은 실제 GET으로 image magic 확인 권장.
|
||||||
|
|
||||||
|
## 구현 (운영 반영됨)
|
||||||
|
|
||||||
|
pharmon-web POS에 구현·커밋(`3c2481a`):
|
||||||
|
- `healthkr_drug_info.py` — `extract_info(drugcode)` / `extract_many([...])`(ThreadPool 병렬).
|
||||||
|
- `drug_info_dialog.py` — 판매상세 상품명 더블클릭 → QTextBrowser 약품정보 카드
|
||||||
|
(QtWebEngine 미사용, 이미지 URL→QImage 인라인).
|
||||||
|
- OTC·ETC 상세 둘 다 적용.
|
||||||
|
|
||||||
|
## 기존 BLOB 분석과의 관계
|
||||||
|
|
||||||
|
[drugimg-key-findings.md](drugimg-key-findings.md)의 결론(로컬 DrugImg.DIK_CODE가 44자
|
||||||
|
base64 image key라 join 단정 불가)은 **맞다**. 다만 그건 **로컬 BLOB을 쓰려 할 때의 문제**일
|
||||||
|
뿐이고, 우리는 로컬 BLOB을 안 쓰고 약정원 JSON API로 우회한다 → 그 문제 자체가 사라진다.
|
||||||
83
scripts/probe_healthkr_json.py
Normal file
83
scripts/probe_healthkr_json.py
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""약학정보원 JSON API 정답 경로 재현 스크립트.
|
||||||
|
|
||||||
|
DrugCode → CD_GOODS.BARCODE → CD_BARCODE.DIK_CODE → health.kr JSON + 이미지.
|
||||||
|
실측 환경: 192.168.0.69\\PM2014 (PIT3000). DB 접속정보는 환경에 맞게 수정.
|
||||||
|
|
||||||
|
이 저장소의 다른 스크립트(probe_drugimg_keys.py)는 로컬 BLOB 매칭키 분석용.
|
||||||
|
이 스크립트는 그 BLOB을 우회하는 '정답' 경로(약정원 JSON)를 재현한다.
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import ssl
|
||||||
|
import urllib.request
|
||||||
|
|
||||||
|
try:
|
||||||
|
import pyodbc
|
||||||
|
except ImportError:
|
||||||
|
pyodbc = None
|
||||||
|
|
||||||
|
DRIVER = 'SQL Server Native Client 11.0' # 또는 'ODBC Driver 17/18 for SQL Server'
|
||||||
|
SERVER = r'192.168.0.69\PM2014'
|
||||||
|
UID, PWD = 'sa', 'CHANGEME' # ← 환경에 맞게
|
||||||
|
|
||||||
|
_CTX = ssl.create_default_context()
|
||||||
|
_CTX.check_hostname = False
|
||||||
|
_CTX.verify_mode = ssl.CERT_NONE
|
||||||
|
_HDR = {'User-Agent': 'Mozilla/5.0 Chrome/120', 'Referer': 'https://health.kr/'}
|
||||||
|
|
||||||
|
JSON_API = 'https://health.kr/searchDrug/ajax/ajax_result_drug2.asp?drug_cd={dik}'
|
||||||
|
|
||||||
|
|
||||||
|
def q(sql, params=None, db='PM_DRUG'):
|
||||||
|
cs = (f"DRIVER={{{DRIVER}}};SERVER={SERVER};DATABASE={db};UID={UID};"
|
||||||
|
f"PWD={PWD};Encrypt=no;TrustServerCertificate=yes;"
|
||||||
|
f"Connection Timeout=15;")
|
||||||
|
c = pyodbc.connect(cs)
|
||||||
|
cur = c.cursor()
|
||||||
|
cur.execute(sql, params) if params else cur.execute(sql)
|
||||||
|
cols = [d[0] for d in cur.description]
|
||||||
|
rows = [dict(zip(cols, r)) for r in cur.fetchall()]
|
||||||
|
c.close()
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_dik(drugcode):
|
||||||
|
"""DrugCode → 마스터바코드 → CD_BARCODE.DIK_CODE (정답 경로 A)."""
|
||||||
|
rows = q("""
|
||||||
|
SELECT TOP 1 b.DIK_CODE
|
||||||
|
FROM PM_DRUG..CD_GOODS g
|
||||||
|
JOIN PM_DRUG..CD_BARCODE b ON g.BARCODE = b.BARCODE
|
||||||
|
WHERE g.DrugCode = ? AND ISNULL(b.DIK_CODE,'') <> ''
|
||||||
|
""", (drugcode,))
|
||||||
|
return rows[0]['DIK_CODE'] if rows else None
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_json(dik):
|
||||||
|
url = JSON_API.format(dik=urllib.parse.quote(dik))
|
||||||
|
req = urllib.request.Request(url, headers=_HDR)
|
||||||
|
with urllib.request.urlopen(req, timeout=12, context=_CTX) as r:
|
||||||
|
body = r.read().decode('utf-8', 'replace').strip()
|
||||||
|
if not body:
|
||||||
|
return None
|
||||||
|
data = json.loads(body)
|
||||||
|
return data[0] if isinstance(data, list) and data else None
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
import urllib.parse
|
||||||
|
drugcode = sys.argv[1] if len(sys.argv) > 1 else 'LB000000169' # 용각산(대)
|
||||||
|
dik = resolve_dik(drugcode)
|
||||||
|
print(f"DrugCode={drugcode} → DIK_CODE={dik}")
|
||||||
|
if not dik:
|
||||||
|
print("DIK 없음(의약품 아님/바코드 없음).")
|
||||||
|
sys.exit()
|
||||||
|
d = fetch_json(dik)
|
||||||
|
if not d:
|
||||||
|
print("약정원 미등록.")
|
||||||
|
sys.exit()
|
||||||
|
print(f" drug_name = {d.get('drug_name')} / {d.get('drug_enm')}")
|
||||||
|
print(f" company = {(d.get('upso_name') or '').split('|')[0]}")
|
||||||
|
print(f" cls/form = {d.get('cls_code')} / {d.get('drug_form')}")
|
||||||
|
print(f" drug_pic = {d.get('drug_pic')}")
|
||||||
|
print(f" pack_img = {d.get('pack_img')}")
|
||||||
Reference in New Issue
Block a user