정정: DrugImg 이미지키와 외부 URL 검증 추가
- DrugImg.DIK_CODE 44자 Base64 image key 가능성 문서화 - 평문 DIK_CODE 직접 join 단정 제거 - health.kr zoom 후보 URL과 실제 이미지 GET 검증 스크립트 추가
This commit is contained in:
@@ -7,8 +7,9 @@ PIT3000/PM2000V6에서 확인한 **의약품 상세정보, 식별 이미지, 약
|
||||
1. 화면/인쇄에 쓰이는 약품 상세정보는 로컬 DB(`PM_DRUG`, `PM_IMAGE`)와 외부 health.kr URL을 병행합니다.
|
||||
2. `DRUGCODE`에서 `DIK_CODE`, `SUNG_CODE`를 조회합니다.
|
||||
3. `SUNG_CODE`는 **성분코드**입니다. 보험코드가 아닙니다.
|
||||
4. 식별/약품 이미지는 `PM_IMAGE..DrugImg`, `C:\Dik20\Data\Drugimg2005.MDB`, `C:\Dik2004\Data\Drugimg.MDB` 계열에서 확인됩니다.
|
||||
5. health.kr URL은 상세 페이지/확대 이미지 페이지를 여는 보조 경로로 확인됩니다.
|
||||
4. 식별/약품 이미지는 `PM_IMAGE..DrugImg`, `C:\\Dik20\\Data\\Drugimg2005.MDB`, `C:\\Dik2004\\Data\\Drugimg.MDB` 계열에서 확인됩니다.
|
||||
5. `PM_IMAGE..DrugImg.DIK_CODE`는 일부 인스턴스에서 평문 13자리 DIK_CODE가 아니라 44자 Base64/32-byte digest 형태의 image key일 수 있습니다. 직접 join을 단정하면 안 됩니다.
|
||||
6. health.kr URL은 상세 페이지/확대 이미지 페이지를 여는 보조 경로입니다. `zoom.asp` 내부에 보이는 이미지 URL도 실제 HTTP 200/JPEG인지 별도 검증해야 합니다.
|
||||
|
||||
## 확인된 외부 URL
|
||||
|
||||
@@ -48,9 +49,11 @@ LEFT JOIN PM_DRUG..CD_SUNG AS C
|
||||
WHERE A.DRUGCODE = :drugcode;
|
||||
|
||||
-- SQL Server 이미지 테이블
|
||||
-- 주의: DrugImg.DIK_CODE는 인스턴스에 따라 평문 DIK_CODE가 아니라 44자 Base64 image key일 수 있음.
|
||||
-- 평문 join을 먼저 가정하지 말고 key 분포/DRUGIMG_USER.DRUG_CODE/legacy MDB/PIT 생성 함수를 확인한다.
|
||||
SELECT DIK_CODE, DRUG_IMG, EXPR1
|
||||
FROM PM_IMAGE..DrugImg
|
||||
WHERE DIK_CODE = :dik_code
|
||||
WHERE DIK_CODE = :image_key
|
||||
ORDER BY EXPR1 DESC;
|
||||
|
||||
-- Access MDB 계열 이미지
|
||||
|
||||
68
docs/drugimg-key-findings.md
Normal file
68
docs/drugimg-key-findings.md
Normal file
@@ -0,0 +1,68 @@
|
||||
# DrugImg.DIK_CODE 44자 이미지키 / 외부 이미지 URL 검증 메모
|
||||
|
||||
## 결론
|
||||
|
||||
`PM_IMAGE..DrugImg.DIK_CODE`는 인스턴스/버전에 따라 `PM_DRUG..CD_GOODS.DIK_CODE`의 평문 숫자값과 다를 수 있습니다.
|
||||
|
||||
VM303 PIT3000 샘플에서 확인된 `PM_IMAGE..DRUGIMG.DIK_CODE` 예시는 다음과 같습니다.
|
||||
|
||||
```text
|
||||
xiY4jUZTAZB0p8JpHEa+Zt9DEcOVQFq9sgtAxSG7GXY=
|
||||
```
|
||||
|
||||
이 값은 길이 44자의 Base64 문자열이고, strict base64 decode 시 32 bytes입니다.
|
||||
|
||||
```text
|
||||
len(base64_text) = 44
|
||||
len(base64_decode(text)) = 32
|
||||
```
|
||||
|
||||
즉 형태상 SHA-256 digest를 Base64로 인코딩한 값과 같은 길이입니다. 다만 확인한 평문 `CD_GOODS.DIK_CODE`를 단순히 UTF-8/CP949/UTF-16LE로 SHA-256한 결과와는 일치하지 않았습니다. 따라서 현재 단계의 정확한 표현은 다음입니다.
|
||||
|
||||
```text
|
||||
DrugImg.DIK_CODE = 평문 DIK_CODE가 아닐 수 있는 44자 Base64 image key
|
||||
```
|
||||
|
||||
암호화된 이미지라는 뜻은 아닙니다. 이미지 BLOB 자체는 `FF D8 FF ...` JPEG bytes로 확인되었습니다. 문제는 이미지 BLOB 매칭키입니다.
|
||||
|
||||
## 잘못된 단정
|
||||
|
||||
다음 join은 일부 구버전/일부 DB에서만 가능할 수 있고, VM303/다른 인스턴스에서는 실패할 수 있습니다.
|
||||
|
||||
```sql
|
||||
PM_DRUG..CD_GOODS.DIK_CODE = PM_IMAGE..DrugImg.DIK_CODE
|
||||
```
|
||||
|
||||
따라서 이 저장소에서는 위 join을 기본 경로가 아니라 `legacy/plain-key 가능성`으로만 취급합니다.
|
||||
|
||||
## 외부 URL 검증
|
||||
|
||||
다음 직접 이미지 URL은 일반적으로 성공한다고 보면 안 됩니다.
|
||||
|
||||
```text
|
||||
http://pharm.or.kr/images/sb_photo/big3/{DIK_CODE}.jpg
|
||||
https://www.pharm.or.kr/images/sb_photo/big3/{DIK_CODE}.jpg
|
||||
https://www.health.kr/images/sb_photo/big3/{DIK_CODE}.jpg
|
||||
```
|
||||
|
||||
테스트한 여러 일반의약품 DIK_CODE에서 직접 URL은 404였습니다.
|
||||
|
||||
반면 다음 `zoom.asp`는 HTML을 반환하고 내부 JS/img 후보 경로를 포함할 수 있습니다.
|
||||
|
||||
```text
|
||||
https://www.health.kr/drug_info/sb/zoom.asp?drug_code={DIK_CODE}
|
||||
```
|
||||
|
||||
하지만 중요한 점은, `zoom.asp` 안에 `big3/{DIK}.jpg` 또는 `marked3/{DIK}00_x.jpg` 문자열이 보이더라도 그 이미지 GET이 실제 200이라는 뜻은 아닙니다. 실제 GET/HEAD를 별도로 확인해야 합니다.
|
||||
|
||||
## 현재 권장 매칭 우선순위
|
||||
|
||||
1. `DRUGIMG_USER`처럼 `DRUG_CODE` 컬럼이 있는 테이블을 먼저 확인합니다.
|
||||
2. `PM_IMAGE..DRUGIMG*.DIK_CODE`가 44자 Base64 image key인지, 13자리 평문 DIK_CODE인지 샘플 분포를 확인합니다.
|
||||
3. PIT 코드에서 image key 생성 함수 또는 legacy MDB 매핑을 역추적합니다.
|
||||
4. 외부 health.kr/약학정보원 페이지는 보조 경로로 쓰되, 이미지 URL은 반드시 HTTP 200과 content-type/image magic을 검증합니다.
|
||||
5. 복약지도 pictogram은 별도 경로입니다: `CD_MC.DIK_CODE → CD_PICTOGRAM_SUB.PIC_CODE → CD_PICTOGRAM.PIC_IMG`.
|
||||
|
||||
## 샘플 검증 코드
|
||||
|
||||
`scripts/probe_drugimg_keys.py` 참고.
|
||||
96
scripts/probe_drugimg_keys.py
Normal file
96
scripts/probe_drugimg_keys.py
Normal file
@@ -0,0 +1,96 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Probe PIT/PM drug image key assumptions.
|
||||
|
||||
This script is intentionally read-only. It does not require production credentials in the repo;
|
||||
wire `query_rows()` to pyodbc/pymssql in your environment.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import base64
|
||||
import hashlib
|
||||
import json
|
||||
import re
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
def classify_key(value: str) -> dict:
|
||||
value = value or ''
|
||||
out = {'value': value, 'length': len(value), 'kind': 'unknown'}
|
||||
if re.fullmatch(r'\d{13}', value):
|
||||
out['kind'] = 'plain_13_digit_dik_code'
|
||||
return out
|
||||
try:
|
||||
raw = base64.b64decode(value, validate=True)
|
||||
out['base64_bytes'] = len(raw)
|
||||
out['base64_hex_prefix'] = raw[:8].hex()
|
||||
if len(value) == 44 and len(raw) == 32:
|
||||
out['kind'] = 'base64_32byte_digest_like_key'
|
||||
else:
|
||||
out['kind'] = 'base64_other'
|
||||
except Exception:
|
||||
out['kind'] = 'non_base64_text'
|
||||
return out
|
||||
|
||||
|
||||
def sha256_b64_candidates(plain: str) -> dict:
|
||||
return {
|
||||
enc: base64.b64encode(hashlib.sha256(plain.encode(enc)).digest()).decode()
|
||||
for enc in ['utf-8', 'cp949', 'utf-16le']
|
||||
}
|
||||
|
||||
|
||||
def probe_url(url: str, referer: str | None = None, timeout: int = 10) -> dict:
|
||||
headers = {'User-Agent': 'Mozilla/5.0'}
|
||||
if referer:
|
||||
headers['Referer'] = referer
|
||||
try:
|
||||
req = urllib.request.Request(url, headers=headers)
|
||||
with urllib.request.urlopen(req, timeout=timeout) as r:
|
||||
head = r.read(32)
|
||||
return {
|
||||
'url': url,
|
||||
'ok': True,
|
||||
'status': r.status,
|
||||
'content_type': r.headers.get('content-type'),
|
||||
'content_length': r.headers.get('content-length'),
|
||||
'head_hex': head.hex(),
|
||||
'is_jpeg': head.startswith(b'\xff\xd8\xff'),
|
||||
}
|
||||
except Exception as e:
|
||||
return {'url': url, 'ok': False, 'error': f'{type(e).__name__}: {e}'}
|
||||
|
||||
|
||||
def extract_zoom_image_candidates(dik_code: str) -> dict:
|
||||
url = f'https://www.health.kr/drug_info/sb/zoom.asp?drug_code={dik_code}'
|
||||
result = {'zoom_url': url, 'ok': False, 'candidates': []}
|
||||
try:
|
||||
req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'})
|
||||
with urllib.request.urlopen(req, timeout=15) as r:
|
||||
data = r.read(300000)
|
||||
# health.kr pages are usually euc-kr/cp949-ish.
|
||||
text = data.decode('euc-kr', errors='replace')
|
||||
result['ok'] = True
|
||||
imgs = re.findall(r'''(?:src|myImg\.src)\s*=\s*['\"]([^'\"]+)''', text, flags=re.I)
|
||||
paths = re.findall(r'''https?://[^'\"<>\s]+(?:sb_photo|big3|marked3)[^'\"<>\s]+''', text, flags=re.I)
|
||||
result['candidates'] = sorted(set(imgs + paths))
|
||||
except Exception as e:
|
||||
result['error'] = f'{type(e).__name__}: {e}'
|
||||
return result
|
||||
|
||||
|
||||
def demo():
|
||||
# Replace these with real rows from PM_DRUG..CD_GOODS and PM_IMAGE..DrugImg.
|
||||
plain_dik = '2015012700008'
|
||||
image_key = 'xiY4jUZTAZB0p8JpHEa+Zt9DEcOVQFq9sgtAxSG7GXY='
|
||||
print(json.dumps({
|
||||
'plain_dik': classify_key(plain_dik),
|
||||
'image_key': classify_key(image_key),
|
||||
'sha256_plain_candidates': sha256_b64_candidates(plain_dik),
|
||||
'direct_big3': probe_url(f'http://pharm.or.kr/images/sb_photo/big3/{plain_dik}.jpg'),
|
||||
'zoom_candidates': extract_zoom_image_candidates(plain_dik),
|
||||
}, ensure_ascii=False, indent=2))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
demo()
|
||||
Reference in New Issue
Block a user