슬립큐 앱 진료비 세부산정내역서 업로드/등록 기능 개선 Tech Spec
요약 (Summary)
배경 (Background)
현재 슬립큐 앱의 진료비 세부산정내역서 업로드 기능은 현재 구현 사양에서 확인할 수 있습니다.
목표 (Goals)
목표가 아닌 것 (Non-Goals)
계획 (Plan) - 개발을 위한 세부적인 모든 내용
리팩토링 목표
- OCR 처리 로직을 범용 API 서버(
dta-wir-api-universal)로 분리 - 기존 모바일 앱의 엔드포인트 변경 최소화
- 이벤트 기반 아키텍처로 전환하여 확장성 향상
- 서비스 간 명확한 책임 분리
시스템 구성 요소 변경사항
기존 (AS-IS)
- Frontend: 모바일 앱 →
dta-wir-api-ts - Backend API:
dta-wir-api-ts(모든 OCR 처리 담당) - Chat API:
dta-wir-api-chat-ts(검토 시스템)
변경 후 (TO-BE)
- Frontend: 모바일 앱 → Load Balancer (URL rewrite) →
dta-wir-api-universal(직접 연결) - OCR Backend API:
dta-wir-api-universal(OCR 처리 전담) - Chat API:
dta-wir-api-chat-ts(검토 시스템, 유지)
📍 라우팅 방식: Load Balancer에서 URL rewrite를 통해 앱의 기존 엔드포인트(
/accesscodes/medical-statements)를dta-wir-api-universal로 직접 라우팅합니다. 이를 통해 불필요한 프록시 레이어를 제거하고 응답 속도를 개선했습니다.
전체 프로세스 플로우 다이어그램
1. OCR 업로드 단계
1.1 환자의 이미지 업로드
환자가 모바일 앱에서 진료비 세부산정내역서 이미지를 업로드합니다.
변경사항: 없음 (기존과 동일)
1.2 API 호출 및 라우팅
1.2.1 앱에서 기존 엔드포인트 호출 (변경 없음)
모바일 앱은 기존 엔드포인트를 그대로 사용합니다.
- 앱의 API 호출:
POST /accesscodes/medical-statements - 엔드포인트 URL: 변경 없음 (앱 배포 불필요)
설계 의도: 앱에서 API 엔드포인트를 변경하는 것은 배포 부담이 크므로, 인프라 레벨(Load Balancer)에서 라우팅을 변경하여 하위 호환성을 유지합니다.
1.2.2 Load Balancer의 URL Rewrite (인프라 변경)
Load Balancer에서 URL rewrite 규칙을 통해 요청을 dta-wir-api-universal로 직접 라우팅합니다.
- 라우팅 규칙:
POST /ts/v1/auth/accesscodes/medical-statements→dta-wir-api-universal로 rewrite하여/v1/auth/accesscodes/medical-statements로 처리 - 장점:
- 불필요한 프록시 레이어 제거
- 네트워크 홉(hop) 감소로 응답 속도 개선
dta-wir-api-ts의 부하 감소
URL Rewrite 설정 (Terraform):
{
priority = 8
service = "https://www.googleapis.com/compute/v1/projects/${local.project}/global/backendServices/dta-wir-api-universal"
match_rules = {
prefix_match = "/ts/v1/auth/accesscodes/medical-statements"
}
route_action = {
url_rewrite = {
path_prefix_rewrite = "/v1/auth/accesscodes/medical-statements"
}
}
}
동작 방식:
- 모바일 앱이
POST /ts/v1/auth/accesscodes/medical-statements로 요청 - Load Balancer가 prefix match 규칙에 따라 요청을 캐치
- URL rewrite를 통해
/v1/auth/accesscodes/medical-statements로 경로 변경 dta-wir-api-universal백엔드 서비스로 라우팅
1.3 서버측 처리 (dta-wir-api-universal)
dta-wir-api-universal에서 OCR 업로드의 핵심 로직을 처리합니다.
1.3.1 이미지 저장
기존과 동일하게 Cloud Storage에 이미지를 저장합니다.
- 저장 위치: Cloud Storage
- 버킷:
medical-statement-prod - 경로 패턴:
dta-wir/{year}/{month}/{filename}
1.3.2 액세스 코드 생성
데이터베이스에 액세스 코드를 생성합니다.
액세스 코드 생성 파라미터:
type: 3groupId: 1siteId: NULLregistration_channel_id: 4delivery_method: IN_APP
1.3.3 이벤트 발행
Cloud Pub/Sub의 events 토픽에 OCR 등록 완료 이벤트를 발행합니다.
- Pub/Sub 토픽:
events - 이벤트 타입:
ocr.medical-statement.created
이벤트 메시지 구조:
{
"eventType": "ocr.medical-statement.created",
"data": {
"accessCodeId": 952,
"deviceId": "Android-4fa95b2f-7b51-4cd4-8017-082c13f8f232",
"gcsUris": ["gs://medical-statement-prod/dta-wir/2025/10/1758677416465-ujzvxn.1758677415796_dee80075-6896-4274-9cb6-b3d013cf9b7b_0"],
"imageCount": 1,
"uploadedAt": "2025-10-10T12:34:56.789Z"
}
}
1.3.4 응답 반환
클라이언트(앱)에게 업로드 완료 응답을 반환합니다.
Response Body:
{
"success": true,
"accessCodeId": 952,
"uploadedAt": "2025-10-10T12:34:56.789Z",
"imageCount": 1,
"status": "UPLOADED",
"message": "이미지가 성공적으로 업로드되었습니다. 분석이 시작됩니다."
}
2. OCR 분석 및 검토 등록 단계
✨ 프로세스 통합: 기존 시스템에서는 "분석 → 검토 작업 등록 → 검토 요청 알림"이 3개의 독립적인 단계로 분리되어 있었으나, 이벤트 기반 아키텍처로 전환하면서 하나의 통합된 프로세스로 재설계했습니다. 이를 통해 불필요한 Pub/Sub 토픽 홉(hop)을 제거하고, 처리 시간을 단축하며, 데이터 일관성을 향상시킬 수 있습니다.
2.1 이벤트 기반 분석 프로세스
2.1.1 이벤트 수신 및 처리 시작
Cloud Pub/Sub의 events 토픽을 구독하여 ocr.medical-statement.created 이벤트를 수신하고 처리를 시작합니다.
- 이벤트 소스:
events토픽 (Pub/Sub) - 처리 시스템:
dta-wir-api-universal - 이벤트 타입:
ocr.medical-statement.created
처리 흐름:
events 토픽 (Pub/Sub)
↓ (이벤트 구독)
dta-wir-api-universal
↓ (이벤트 핸들러 실행)
ocr.medical-statement.created 로직
2.1.2 dta-ocr-api 호출 - 이미지 분석
dta-ocr-api를 호출하여 업로드된 진료비 세부산정내역서 이미지를 LLM 기반으로 분석합니다.
- API 경로:
dta-wir-api-universal → dta-ocr-api - 엔드포인트:
POST /analyze
2.1.3 Site ID 등록 및 환자 정보 연동
🆕 새로운 기능: 분석된 진료 정보를 바탕으로 요양기관(Site) 정보를 자동으로 등록하고, AccessCode 및 UserCycle에 환자 정보를 연동합니다. 이는 기존 시스템에서는 수동으로 처리되던 작업을 자동화하여 검토 프로세스를 개선합니다.
처리 흐름:
-
Site 조회/생성
- OCR 분석 결과에서 요양기관 정보 추출
- 기존 Site 조회 (요양기관코드 기준)
- 없으면 새로운 Site 생성
-
AccessCode 업데이트
siteId: 조회/생성된 Site IDmedicalRegistrationNumber: OCR에서 추출한 환자등록번호(병록번호)
-
UserCycle 업데이트 (조건부)
- AccessCode와 연결된 UserCycle 조회
- UserCycle이 존재하는 경우:
siteId: 조회/생성된 Site IDmedicalRegistrationNumber: OCR에서 추출한 환자등록번호(병록번호)
📌 설계 참고: UserCycle이 아직 존재하지 않는 경우(사용자가 아직 등록을 완료하지 않은 경우)에도 AccessCode에
siteId와medicalRegistrationNumber를 저장하는 것으로 충분합니다. 실제 사용자 등록(registration) 시dta-wir-api에서 AccessCode에 저장된 이 값들을 참조하여 UserCycle을 생성하기 때문에, 데이터 정합성이 자동으로 보장됩니다.
데이터베이스 업데이트 예시:
-- AccessCode 업데이트
UPDATE access_codes
SET
site_id = 123,
medical_registration_number = 'P202501234567'
WHERE id = 952;
-- UserCycle 업데이트 (존재하는 경우)
UPDATE user_cycles
SET
site_id = 123,
medical_registration_number = 'P202501234567'
WHERE access_code_id = 952;
2.1.4 분석 결과 저장
🔄 저장소 변경: 기존에는 Firestore의
review-tasks-todo컬렉션에 검토 작업을 저장했으나, 관계형 데이터베이스의user_medical_statement테이블로 변경합니다.
OCR 분석 결과를 user_medical_statement 테이블에 저장합니다.
저장 데이터:
- OCR 분석 결과 (진료 정보, 금액 등)
status:TODO- 검토 대기 상태result:PENDING- 검토 결과 대기 중
2.1.5 Slack 검토 요청 알림 전송
분석 및 등록이 완료되면 즉시 검토자에게 Slack 알림을 전송합니다.
- 알림 채널: Slack (검토자 전용 채널)
- 메시지 타입: Interactive Message (버튼 포함)
Slack 메시지 전송 시점:
- OCR 분석 완료 후 즉시 전송 (비동기)
- 전송 실패 시 재시도 로직 포함 (최대 3회)
3. OCR 검토 프로세스
💡 참고: 현재 버전에서는 OCR 분석 및 데이터 저장이 완료되면 Slack으로 검토 알림이 전송됩니다. 검토자는 Slack 메시지의 "지금 검토하기" 버튼을 통해 검토 프로세스를 시작할 수 있습니다.
주요 변경사항 요약
| 구분 | 기존 (AS-IS) | 변경 (TO-BE) | 변경 이유 |
|---|---|---|---|
| 라우팅 방식 | 앱 → `dta-wir-api-ts₩ | 앱 → LB (URL rewrite) → dta-wir-api-universal | 프록시 레이어 제거, 응답 속도 개선, 부하 분산 |
| 프로세스 구조 | 분석 → 등록 → 알림 (3단계 분리) | 분석 + 등록 + 알림 (1단계 통합) | 불필요한 Pub/Sub 홉 제거, 처리 시간 단축 |
| Site 등록 | 수동 등록 | 자동 등록 (OCR 분석 기반) | 검토 프로세스 자동화, 운영 효율 향상 |
| 환자 정보 연동 | AccessCode만 업데이트 | AccessCode + UserCycle 자동 업데이트 (siteId, medicalRegistrationNumber) | 데이터 정합성 향상, 환자-병원 연동 자동화 |
| 데이터 저장소 | Firestore (review-tasks-todo) | PostgreSQL (user_medical_statement) | 데이터 일관성, 쿼리 성능, 참조 무결성 향상 |
| Chat API 연동 | dta-wir-api-ts Tool 사용 | dta-wir-api-universal Tool 사용 | 불필요한 중간 레이어 제거, 직접 데이터 접근 |
| 이벤트 아키텍처 | Cloud Task 기반 | Pub/Sub 이벤트 기반 | 확장성 및 유연성 향상, 느슨한 결합 |