문제 정의: 복잡하고 오류가 잦았던 세금계산서 등록 프로세스
기존 세금계산서 등록은 이미지를 업로드한 후 사용자가 직접 수기로 입력하는 구조였습니다.
하지만 이 방식은 실제 사용 과정에서 다음과 같은 심각한 비효율과 오류를 유발했습니다:
- 이미지를 회전하거나 자르지 않고 업로드 → OCR 인식 실패
- 여러 장이 포함된 사진 업로드 → 텍스트 누락 또는 잘못된 매칭
- 중복 세금계산서 업로드 → 관리자 수동 확인 필요
- OCR 결과를 그대로 등록 → 오타, 누락된 정보 발생
이로 인해 세금계산서 1건을 처리하는 데 평균 약 250분이 소요되었고, 사용자 오타율은 월 10건 이상에 달했습니다.
이를 개선하기 위해 이미지 편집부터 OCR 자동화까지 전 과정 자동화 시스템을 구축하게 되었습니다.
개선한 전체 흐름
사용자 이미지 업로드
↓
이미지 크롭, 회전, 반전 (ImageCrop 컴포넌트)
↓
변환된 이미지 Canvas로 변환 후 Base64
↓
OCR 서버에 요청
↓
- 중복인지 확인
- 반려 상태인지 확인
↓
결과에 따라 분기 처리 + Step 이동
🧩 구현 상세
📌 react-image-crop + Canvas API 기반 이미지 회전/반전/크롭 처리
- 사용자가 세금계산서 부분만 자를 수 있도록 react-image-crop을 활용해 UI를 제공했습니다.
하지만 이 라이브러리는 실제로 이미지를 자르지는 않기 때문에, Canvas API를 통해 이미지 회전·반전·크롭을 처리했습니다.
✅ Situation
사용자가 이미지를 회전/자르지 않고 업로드하면, OCR 서버가 문서를 제대로 인식하지 못했습니다.
✅ Action
- react-image-crop: 화면상 자르기 UI 제공
- Canvas API: 실제 이미지 crop, 회전, 반전 적용
ctx.translate(canvas.width / 2, canvas.height / 2); // 중심 이동
ctx.rotate((rotation * Math.PI) / 180); // 회전 적용
ctx.scale(flipX, flipY); // 좌우/상하 반전
ctx.drawImage( // 최종 출력
image,
newX, newY, newWidth, newHeight, // 원본 이미지 좌표
-newWidth / 2, -newHeight / 2, // 캔버스 위치
newWidth, newHeight // 출력 크기
);
- translate는 회전을 중심점 기준으로 하기 위한 설정
- rotate는 라디안 단위
- scale(flipX, flipY)는 반전 처리 (1 또는 -1)
- drawImage의 좌표가 회전한 뒤 기준으로 정확해야 자른 영역이 일치함
crop 정보 변환 계산
const scaleX = naturalWidth / renderedImageWidth;
const scaleY = naturalHeight / renderedImageHeight;
x *= scaleX;
y *= scaleY;
width *= scaleX;
height *= scaleY;
- 렌더된 이미지와 실제 이미지의 차이를 보정
- 정확한 픽셀 단위로 잘라 OCR 정확도를 높였습니다
✅ Result
- 이미지가 항상 정렬된 상태로 OCR 서버에 전달
- 사용자가 이미지를 손쉽게 편집 가능
- OCR 인식률 상승
📌 OCR 결과 보정 UI
OCR 결과가 도착하면 다음과 같은 정보를 자동으로 입력 필드에 반영합니다:
- 승인번호 (approvalNo)
- 공급자 등록번호 (supplier)
- 공급 받는 자 등록번호 (recipient)
- 작성일자 (date)
- 공급가액 (amount)
이 필드는 모두 수정 가능하며, 이후 "업로드" 버튼을 누르면 백엔드에 유효성 검사를 요청합니다.
데이터 흐름 관리
- location.state + query-string → Step 간 데이터 전달
- useState로 폼 상태 로컬 관리
📌 서버 캐싱 이슈: 파일명이 동일할 경우 OCR 결과가 재사용되는 문제
✅ Situation
Base64 → Blob으로 변환된 이미지를 FormData로 전송할 때, **파일명을 고정값(image.jpg 등)**으로 설정해 서버에서 동일 파일로 인식됨. 그 결과 OCR 응답이 이전 결과와 동일하게 나오는 문제 발생.
✅ Task
업로드할 파일을 매번 고유하게 식별하고, 서버가 새 파일로 처리되도록 보장해야 했습니다.
✅ Action
- uuidv4()를 사용하여 고유한 파일명을 생성
- Blob → File 변환 시 "image.jpg" → "crop_<uuid>.jpg"로 파일명 지정
- 동일한 이미지를 여러 번 올려도 매번 다른 파일처럼 인식되도록 처리
const uniqueFile = new File([blob], `crop_${uuidv4()}.jpg`, { type: 'image/jpeg' });
formData.append('file', uniqueFile);
✅ Result
- OCR 서버에서 캐시된 이미지를 재사용하지 않게 되었고
- 반복 업로드, 여러 사용자 동시 업로드 상황에서도 정확한 OCR 결과 반환
📌OCR 실패 시 사용자 UX 단절 문제
✅ Situation
OCR 실패(isSuccess: false) 응답이 오면 사용자는 왜 실패했는지 알 수 없었고, 다시 업로드해야 했지만 어떤 문제가 있었는지 힌트가 없어 불편하다는 피드백이 있었습니다.
✅ Task
OCR 실패 시, 사용자가 상황을 인지하고 즉시 대처할 수 있도록 UI/UX 측면에서 개선이 필요했습니다.
✅ Action
- 실패 원인 메시지를 기반으로 커스텀 모달 출력
- "재업로드" 버튼을 모달 내에 배치하고, 클릭 시 fileInput.click() 자동 실행
- 이전 이미지 제거 + 업로드 초기 상태로 리셋되도록 처리
setErrorMessage('파일 업로드 중 오류가 발생했어요');
setIsErrorModalOpen(true);
✅ Result
- 사용자는 더 이상 업로드 실패 후 브라우저를 새로고침하지 않음
- 평균 재업로드 시도 수가 감소, 업로드 완료율이 20% 이상 향상
🧪 예외 대응
1. OCR 실패
- isSuccess가 false일 경우 사용자에게 오류 모달 제공
- 파일 업로드 중 오류가 발생했습니다 메시지 출력 후 재업로드 유도
2. 중복 문서
- 이미 등록된 세금계산서입니다. 메시지 시, 상태값 따라 모달 처리
- 승인된 문서는 링크 안내
- 반려된 문서는 자동 삭제 후 재업로드
📈 최종 성과 정리
⏱ 평균 등록 시간 | 250분 | 2분 |
🧠 OCR 오류율 | 37% | 8% |
✏️ 오타율 | 월 10건 | 0건 |
🧑 사용자 만족도 | 불편함, 수동 중심 | 자동화, 편집 UI, 예외 대응 포함 |
'⛲ 프로젝트' 카테고리의 다른 글
이벤트 페이지 성능최적화 (LCP 37-> 2.5, CLS 0.219→0.006) (4) | 2025.08.18 |
---|---|
10,000건 테스트, MSW 기반 대용량 가상+무한 스크롤 적용 (6) | 2025.08.12 |
카카오톡에서 앱 설치 유도하기: PWA 외부 브라우저 리다이렉션 전략 (5) | 2025.08.06 |
Home 진입 40초 → 2.5초, LCP를 93% 줄인 Next.js 최적화 (3) | 2025.08.05 |
(husky) Husky와 Lint-staged를 이용한 Lint 자동화 (1) | 2024.11.23 |