📌 Description
약봉투·처방전 OCR 추출이 여러 서식에서 깨지는데, 실패를 재현할 수단이 없다.
NaverOcrClient.callOcr()는 응답을 NaverOcrApiResponse로 파싱한 뒤 원본을 버린다.
OcrResult에는 raw_text(전체 텍스트를 이어붙인 문자열)만 저장되고, 좌표(boundingPoly)가
포함된 원본 응답은 어디에도 남지 않는다.
OcrParser 단위 테스트가 0개다. 서식 하나가 깨질 때마다 정규식 분기를 추가하는데,
기존 케이스가 회귀했는지 확인할 방법이 없다.
raw_text에 환자 이름·주민번호가 그대로 저장되고 있다 (마스킹 없음).
이 이슈는 회귀 테스트 기반을 만든다. 후속 이슈(표 처방전 좌표 파싱, 약봉투 다중 약 분리)는
전부 이 위에서 진행한다.
🎯 목표
- OCR 응답 원본(좌표 포함)을 저장해 실패 케이스를 그대로 재현 가능하게 한다
- 저장 시 주민번호를 마스킹한다 (이름·생년월일은 저장 허용 — 팀 합의)
OcrParser를 fixture로 돌려 약 단위 precision/recall을 뽑는 회귀 테스트를 만든다
- 응답 DTO(
OcrResultResponse) 외부 계약은 건드리지 않는다 (프론트 영향 0)
🛠️ 작업 범위
1. Naver 응답 원본 저장
NaverOcrClient.callOcr() — bodyToMono(String.class)로 원문을 받아 주입한 ObjectMapper로
파싱. 반환 타입을 List<Field> → NaverOcrCallResult(List<Field> fields, String rawResponseJson)
(신규 record)로 변경. extractFields() 검증 로직은 유지.
OcrResult 엔티티 — @Column(name = "raw_response", columnDefinition = "TEXT") String rawResponse
추가 (nullable). 빌더 파라미터 추가. ddl-auto: update라 마이그레이션 파일 불필요.
OcrCommandService.processOcr() — 반환 타입 변경 반영, rawResponse 저장.
2. 주민번호 마스킹
- 신규 유틸
global/util/PiiMasker.java
// 주민번호 형식(6자리-7자리, 뒤 첫자리 1~8)만 마스킹
public static String maskResidentNumber(String text) {
return text == null ? null
: text.replaceAll("\\d{6}\\s*-\\s*[1-8]\\d{6}", "******-*******");
}
OcrCommandService에서 raw_text·raw_response 둘 다 저장 직전에 통과.
3. 파서 회귀 테스트 하네스
src/test/resources/ocr/fixtures/*.json — 저장된 Naver 응답(NaverOcrApiResponse 형태)
- 합성 표 처방전 2~3건: 좌표 포함, 개인정보 없음 (직접 작성)
- 약봉투/영수증: 실제 사진에서 뽑아 이름 마스킹 후 커밋. 확보 전까지는 관찰된
유성온누리약국 케이스 rawText를 좌표 없는 fixture로 우선 포함
src/test/resources/ocr/expected/manifest.json — fixture명 → 기대 약 목록
[{drugName, dosagePerTime, timesPerDay, totalDays}]
src/test/java/.../ocrresult/fixture/OcrFixtureLoader.java — JSON → List<Field> 역직렬화
src/test/java/.../ocrresult/service/OcrParserRegressionTest.java
new OcrParser() (의존성 없음), fixture별 parse() 실행
- 약 단위 precision/recall 계산·콘솔 출력
- 핵심 케이스 개별 단언 (한국어
@DisplayName, Given/When/Then)
@Tag("integration") 아님 (순수 단위)
✅ To-do
📏 완료 조건 (DoD)
POST /api/ocr 호출 후 ocr_result.raw_response에 좌표 포함 응답 JSON 저장됨
- 주민번호가 있는 처방전으로 테스트 시
raw_text·raw_response 모두 ******-*******
./gradlew test --tests *OcrParser* 통과 + precision/recall 출력
OcrResultResponse 필드 변화 없음
📒 기타
- 커밋 분리 (git.md): entity → record(dto) → client → service → test
- 응답 원본에
inferConfidence는 우리 DTO에 없어 재직렬화 시 빠짐 → 원문 문자열을 그대로 저장하는 이유
- 후속: 이슈 B(표 처방전 좌표 파싱), 이슈 C(약봉투 다중 약)가 이 하네스 위에서 진행
- 로컬 테스트용 컨트롤러
userId 하드코딩(현재 워킹트리에 있음)은 이 이슈와 무관 — 커밋에 섞지 말 것
📌 Description
약봉투·처방전 OCR 추출이 여러 서식에서 깨지는데, 실패를 재현할 수단이 없다.
NaverOcrClient.callOcr()는 응답을NaverOcrApiResponse로 파싱한 뒤 원본을 버린다.OcrResult에는raw_text(전체 텍스트를 이어붙인 문자열)만 저장되고, 좌표(boundingPoly)가포함된 원본 응답은 어디에도 남지 않는다.
OcrParser단위 테스트가 0개다. 서식 하나가 깨질 때마다 정규식 분기를 추가하는데,기존 케이스가 회귀했는지 확인할 방법이 없다.
raw_text에 환자 이름·주민번호가 그대로 저장되고 있다 (마스킹 없음).이 이슈는 회귀 테스트 기반을 만든다. 후속 이슈(표 처방전 좌표 파싱, 약봉투 다중 약 분리)는
전부 이 위에서 진행한다.
🎯 목표
OcrParser를 fixture로 돌려 약 단위 precision/recall을 뽑는 회귀 테스트를 만든다OcrResultResponse) 외부 계약은 건드리지 않는다 (프론트 영향 0)🛠️ 작업 범위
1. Naver 응답 원본 저장
NaverOcrClient.callOcr()—bodyToMono(String.class)로 원문을 받아 주입한ObjectMapper로파싱. 반환 타입을
List<Field>→NaverOcrCallResult(List<Field> fields, String rawResponseJson)(신규 record)로 변경.
extractFields()검증 로직은 유지.OcrResult엔티티 —@Column(name = "raw_response", columnDefinition = "TEXT") String rawResponse추가 (nullable). 빌더 파라미터 추가.
ddl-auto: update라 마이그레이션 파일 불필요.OcrCommandService.processOcr()— 반환 타입 변경 반영,rawResponse저장.2. 주민번호 마스킹
global/util/PiiMasker.javaOcrCommandService에서raw_text·raw_response둘 다 저장 직전에 통과.3. 파서 회귀 테스트 하네스
src/test/resources/ocr/fixtures/*.json— 저장된 Naver 응답(NaverOcrApiResponse형태)유성온누리약국 케이스 rawText를 좌표 없는 fixture로 우선 포함
src/test/resources/ocr/expected/manifest.json— fixture명 → 기대 약 목록[{drugName, dosagePerTime, timesPerDay, totalDays}]src/test/java/.../ocrresult/fixture/OcrFixtureLoader.java— JSON →List<Field>역직렬화src/test/java/.../ocrresult/service/OcrParserRegressionTest.javanew OcrParser()(의존성 없음), fixture별parse()실행@DisplayName, Given/When/Then)@Tag("integration")아님 (순수 단위)✅ To-do
NaverOcrCallResultrecordNaverOcrClient원문 반환 +ObjectMapper주입PiiMasker유틸 + 단위 테스트OcrResultraw_response컬럼 + 빌더OcrCommandService저장 로직 + 마스킹 적용OcrFixtureLoader+ fixture JSON (합성 처방전 우선)manifest.json정답표OcrParserRegressionTest(precision/recall + 핵심 케이스 단언)📏 완료 조건 (DoD)
POST /api/ocr호출 후ocr_result.raw_response에 좌표 포함 응답 JSON 저장됨raw_text·raw_response모두******-*******./gradlew test --tests *OcrParser*통과 + precision/recall 출력OcrResultResponse필드 변화 없음📒 기타
inferConfidence는 우리 DTO에 없어 재직렬화 시 빠짐 → 원문 문자열을 그대로 저장하는 이유userId하드코딩(현재 워킹트리에 있음)은 이 이슈와 무관 — 커밋에 섞지 말 것