diff --git a/README.md b/README.md index 53f6413..e21eb85 100644 --- a/README.md +++ b/README.md @@ -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 계열 이미지 diff --git a/docs/drugimg-key-findings.md b/docs/drugimg-key-findings.md new file mode 100644 index 0000000..483bb6b --- /dev/null +++ b/docs/drugimg-key-findings.md @@ -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` 참고. diff --git a/scripts/probe_drugimg_keys.py b/scripts/probe_drugimg_keys.py new file mode 100644 index 0000000..a59fc8e --- /dev/null +++ b/scripts/probe_drugimg_keys.py @@ -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()