Skip to content

feat: OCR 응답 원본 저장 + 파서 회귀 테스트 기반 #96

Description

@kangcheolung

📌 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

  • NaverOcrCallResult record
  • NaverOcrClient 원문 반환 + ObjectMapper 주입
  • PiiMasker 유틸 + 단위 테스트
  • OcrResult raw_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 필드 변화 없음

📒 기타

  • 커밋 분리 (git.md): entity → record(dto) → client → service → test
  • 응답 원본에 inferConfidence는 우리 DTO에 없어 재직렬화 시 빠짐 → 원문 문자열을 그대로 저장하는 이유
  • 후속: 이슈 B(표 처방전 좌표 파싱), 이슈 C(약봉투 다중 약)가 이 하네스 위에서 진행
  • 로컬 테스트용 컨트롤러 userId 하드코딩(현재 워킹트리에 있음)은 이 이슈와 무관 — 커밋에 섞지 말 것

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions