리포트 서비스
리포트 서비스
리포트 서비스(report-svc)는 학생의 시험 결과 데이터를 수집하고, Jinja2 HTML 템플릿으로 렌더링한 뒤 Playwright Chromium을 통해 고품질 JPG 이미지로 출력합니다. RabbitMQ report.generation-request 큐를 소비하고, 결과를 report.generation.callback 라우팅 키로 백엔드에 콜백합니다. 하위 호환성을 위한 동기식 HTTP 엔드포인트도 함께 제공합니다.
RabbitMQ 메시지 계약
인바운드 큐: report.generation-request
백엔드에서 발행하는 리포트 생성 요청 메시지 형식입니다.
class ReportCommand(BaseModel):
jobId: int
examId: int
studentId: int
tenantSlug: str
reportIndex: int # 이 리포트의 배치 내 순서 (0-based)
totalReports: int # 배치 전체 리포트 수
schemaVersion: int # 기본값 1
아웃바운드 라우팅 키: report.generation.callback
성공·실패 모두 동일한 라우팅 키로 백엔드에 콜백합니다.
성공 페이로드
class SuccessCallbackPayload(BaseModel):
jobId: int
examId: int
studentId: int
tenantSlug: str
reportIndex: int
totalReports: int
success: bool = True
reportBytes: str # base64 인코딩된 JPG 바이트
실패 페이로드
class FailureCallbackPayload(BaseModel):
jobId: int | None
examId: int | None
studentId: int | None
tenantSlug: str | None
reportIndex: int | None
totalReports: int | None
success: bool = False
error: str
reportBytes: None = None
기술 스택
| 라이브러리 | 버전 | 용도 |
|---|---|---|
| Playwright | 1.50.0 | Headless Chromium 기반 HTML → JPG 변환 |
| Jinja2 | 3.1.3 | HTML 템플릿 렌더링 |
| httpx | 0.27.0 | 내부 서비스 비동기 HTTP 통신 |
| FastAPI | 0.115.0 | HTTP API 서버 |
| Uvicorn | 0.30.0 | ASGI 서버 |
| aio_pika | — | RabbitMQ 비동기 소비자 |
한국어 폰트는 Noto CJK 패밀리를 사용하며, 컨테이너에 사전 설치됩니다.
HTTP API 엔드포인트
POST /v1/reports/students/\{student_id\}/exams/\{exam_id\} — 동기식 리포트 생성
하위 호환성을 위해 유지되는 동기식 엔드포인트입니다. 특정 학생의 특정 시험에 대한 리포트를 JPG 이미지로 반환합니다.
요청 헤더
| 헤더 | 설명 |
|---|---|
X-Tenant-Slug | 테넌트 식별자 |
경로 파라미터
| 파라미터 | 설명 |
|---|---|
student_id | 학생 고유 ID (UUID) |
exam_id | 시험 고유 ID (UUID) |
응답
- Content-Type:
image/jpeg - 출력 해상도: 794×1123px (기본 뷰포트) × 4배 스케일 = 3176×4492px
데이터 수집 플로우
리포트 생성은 4단계 파이프라인으로 구성됩니다. MQ 경로와 HTTP 동기식 경로 모두 동일한 파이프라인을 사용합니다.
Phase 1: 병렬 데이터 조회
httpx를 사용한 비동기 병렬 요청으로 두 서비스를 동시에 호출합니다. 모든 호출은 HMAC 서명과 X-Tenant-Slug 헤더를 포함합니다.
| 조회 대상 | 내부 경로 | 내용 |
|---|---|---|
| 학생 기본 정보 | /internal/students/{id} | 이름, 학년, 소속 반 등 |
| 시험 통계 정보 | /internal/exams/{id}/stats | 전체 평균, 표준편차, 등급 컷 등 |
Phase 2: 순차 데이터 조회
Phase 1에서 반환된 result_id를 사용하여 문항별 세부 결과를 조회합니다. 이 단계는 Phase 1이 완료된 후에만 실행 가능합니다.
| 조회 대상 | 내부 경로 | 내용 |
|---|---|---|
| 문항별 결과 | /internal/exams/{id}/results/{result_id} | 선택 답안, 정오, 배점, 정답률 등 |
Phase 3: HTML 렌더링
수집된 데이터를 Jinja2 템플릿에 주입하여 HTML 문서를 생성합니다. 템플릿은 리포트 레이아웃, 차트 데이터, 문항 분석 영역을 포함합니다.
Phase 4: HTML → JPG 변환
Playwright의 Headless Chromium으로 렌더링된 HTML을 캡처하여 JPG로 변환합니다.
Chromium 설정
# 컨테이너 환경에서 필요한 플래그
--no-sandbox
# 뷰포트 설정
width: 794 # A4 너비 (96dpi 기준)
height: 1123 # A4 높이
device_scale_factor: 4 # 고해상도 출력
Chromium 브라우저 인스턴스는 서비스 **시작 시 미리 워밍업(pre-warm)**되어 첫 번째 요청에서도 지연 없이 응답합니다.
환경 변수
| 변수 | 기본값 | 설명 |
|---|---|---|
RABBITMQ_HOST | localhost | RabbitMQ 호스트 |
RABBITMQ_PORT | 5672 | RabbitMQ 포트 |
RABBITMQ_QUEUE | report.generation-request | 소비 큐 이름 |
RABBITMQ_PREFETCH_COUNT | 2 | 동시 처리 메시지 수 |
LUMIE_BACKEND_URL | — (필수) | 백엔드 콜백 및 데이터 조회 URL |
LUMIE_INTERNAL_HMAC_SECRET | — (필수) | HMAC 서명 비밀키 |
MINIO_ENDPOINT | localhost:9000 | MinIO 엔드포인트 |
MINIO_BUCKET | lumie | MinIO 버킷 |
Kubernetes 리소스
resources:
requests:
memory: "512Mi"
cpu: "100m"
limits:
memory: "2Gi"
Playwright Chromium은 메모리를 상당히 소비하므로 메모리 한도를 충분히 확보해야 합니다. prefetch_count=2는 Playwright Chromium 스폰 비용을 반영한 보수적인 값입니다.
컨테이너 설정 주의사항
컨테이너 환경에서는 반드시 다음 사항을 확인해야 합니다.
--no-sandbox플래그: Chromium은 컨테이너 내에서 샌드박스 없이 실행되어야 합니다. root 권한 없이도 동작하도록 설정되어 있습니다.- Noto CJK 폰트: 한국어 텍스트 렌더링을 위해 컨테이너 이미지에 Noto CJK 폰트가 포함되어야 합니다.
- Playwright 브라우저 사전 설치: Docker 빌드 시
playwright install chromium명령으로 브라우저를 설치합니다.
헬스체크
GET /health
{ "status": "ok" }