[#5] [안전교육 시스템] 퀴즈, 안전서약, 이수증 발급까지 마지막 흐름을 이렇게 닫았다 | 연재 4편
Series 04 · Quiz · Pledge · Certificate
퀴즈, 안전서약, 이수증 발급까지
마지막 흐름을 이렇게 닫았다
교육 화면을 만들다 보면 마지막 단계는 자주 형식으로 밀린다. 하지만 이 프로젝트에서는
오히려 마지막 단계가 더 중요했다. 영상을 다 봤다고 해서 끝이 아니고, 최소한의 이해를 확인하고,
동의와 서명을 거쳐 제출하고, 이수증까지 발급돼야 비로소 “완료됐다”는 감각이 생긴다고 봤기 때문이다.
이번 편에서는 퀴즈가 어떻게 다음 단계의 문턱이 됐는지, 안전서약과 전자서명을 어떤 흐름으로 붙였는지,
제출 뒤 이수증 발급은 어떤 식으로 구현했는지 정리한다. 기능만 보면 각각 별개처럼 보이지만,
실제로는 하나의 완료 경험으로 이어지도록 설계한 부분들이다.
마지막 단계의 목적은 체크를 더 받는 것이 아니라, 교육 이수를 시스템적으로 닫아주는 것이었다.
퀴즈는 장식이 아니라 다음 단계의 조건이었다
이 프로젝트에서 퀴즈는 있으면 좋은 부가 기능이 아니었다. 영상을 모두 시청해야 퀴즈가 열리고,
퀴즈를 통과해야 서약과 서명 단계로 넘어갈 수 있게 막아뒀다. 다시 말해 퀴즈는
“봤다”를 “이해했다” 쪽으로 한 번 더 끌어당기는 문턱이었다.
실제로는 `videosCompleted`가 참일 때만 퀴즈 섹션을 열고, `quizPassed`가 참이 돼야
마지막 단계가 진행된다. 흐름을 보면 단순하지만, 이 한 줄기 제어가 전체 교육 완성도를 꽤 많이 바꿨다.
퀴즈 구현에서는 무작위성과 다국어 대응을 같이 봤다
퀴즈는 열리기만 하면 안 되고, 매번 같은 방식으로 보이지 않는 편이 좋다고 판단했다.
그래서 문제 데이터는 초기화 시점에 불러오고, 각 문제의 보기 순서를 Fisher-Yates 방식으로 섞는다.
정답 인덱스도 섞인 보기 배열 기준으로 다시 계산해 채점한다.
다국어 지원도 그대로 이어졌다. 현재 언어가 한국어가 아니면 국제 퀴즈 데이터셋을 먼저 보고,
해당 언어 데이터가 없으면 영어로 폴백한다. 이렇게 해두면 외국인 노동자 대상 교육에서도
최소한 이해 가능한 언어로 퀴즈를 마칠 수 있다.
제한 시간도 넣었다. 10분 타이머를 두고 시간이 끝나면 자동 제출되도록 해서,
퀴즈가 무한정 열린 상태로 남지 않게 막았다.
통과 기준은 80점으로 고정했다
퀴즈 상수에서는 `QUIZ_PASS_SCORE = 8`로 잡혀 있다. 10문제 중 8문제 이상,
즉 80점 이상을 받아야 통과다. 이 기준은 너무 낮지도 너무 높지도 않게,
최소한의 이해도는 확인하겠다는 의도를 반영한다.
구현적으로는 `score >= QUIZ_PASS_SCORE`인지로 통과 여부를 판단하고,
그 결과가 상위 컴포넌트로 전달되면 `quizPassed` 상태가 바뀐다.
이후 단계의 개방 여부는 전부 이 값에 걸려 있다.
안전서약과 전자서명은 마지막 확인 단계로 두었다
영상 시청과 퀴즈를 다 거친 뒤에야 동의 체크와 서명이 열린다. 순서가 중요한 이유는 분명하다.
앞단을 거치지 않고 바로 동의와 서명으로 뛰어넘을 수 있으면, 마지막 단계는 금방 형식이 된다.
전자서명은 `react-signature-canvas`를 이용해 구현했고, 사용자가 실제로 그린 서명을
`getTrimmedCanvas().toDataURL(“image/png”)`로 잘라낸 뒤 base64 이미지로 저장한다.
서명이 비어 있지 않을 때만 제출 가능하게 두어, 서명이 흐름상 실제 입력이 되도록 했다.
서명이 끝나면 바로 최종 제출하지 않고, 한 번 더 확인 다이얼로그를 거친다.
이 사이에 `signatureDataURL`을 상태로 들고 가면서 마지막 제출 데이터에 합쳐 넣는다.
제출은 API 한 번으로 끝나지만, 준비는 여러 상태가 모여야 가능하다
최종 제출에서는 부서, 이름, 생년월일, 작업 내용, 동의 여부, 서명 이미지, 제출 시각을 하나의 JSON으로 묶어
API에 전송한다. 여기서 중요한 건 단순 POST가 아니라, 그 전에 필요한 상태가 모두 채워졌는지를
여러 단계에서 이미 검증해두었다는 점이다.
기본 정보가 비어 있거나, 영상이 완료되지 않았거나, 퀴즈를 통과하지 않았거나,
동의를 체크하지 않았거나, 서명이 없으면 각각 다른 경고를 띄우고 막는다.
즉 마지막 제출 버튼 하나에 모든 검증을 몰아넣지 않고, 단계마다 미리 걸러주는 방식이다.
제출이 성공하면 해당 작업 기준의 영상 진행 기록도 `localStorage`에서 지워 초기화한다.
이미 이수한 흐름을 다음 사람이나 다음 작업에 그대로 남기지 않기 위해서다.
이수증 발급은 html2canvas로 마무리했다
제출이 성공하면 이수증 모달을 열고, 여기서 `html2canvas`로 실제 표시 중인 이수증 레이아웃을 캡처해 PNG로 내려받게 했다.
단순 PDF 생성보다 현재 화면과 같은 결과물을 빠르게 다운로드하게 만들고 싶었다.
캡처 전에는 다운로드 버튼 영역을 잠깐 숨기고, 캡처가 끝난 뒤 다시 보여준다.
캔버스 해상도는 `scale: 2.5`로 높여 선명도를 챙겼고, CORS 옵션도 같이 켜서 외부 이미지 자산이
이수증 안에 포함되더라도 최대한 안정적으로 렌더링되게 했다.
발급일과 만료일도 훅에서 계산한다. 발급일 기준 6개월 뒤를 만료일로 잡아,
이수증이 그냥 한 장의 이미지가 아니라 운영 정책과 연결된 결과물이 되도록 했다.
마지막 구현 포인트를 정리하면
- 퀴즈는 영상 완료 이후에만 열고, 통과해야 다음 단계로 넘어가게 했다.
- 보기 셔플, 다국어 문제 분기, 10분 제한 시간, 80점 통과 기준을 함께 묶었다.
- 서명은 캔버스 입력을 PNG 데이터 URL로 변환해 최종 제출 데이터에 넣었다.
- 제출 전에는 단계별 검증을 거쳐 빈 흐름이나 우회 제출을 막았다.
- 제출 성공 뒤에는 html2canvas 기반 이수증 다운로드까지 이어지게 해 완료 경험을 닫았다.
연재를 마치며
이 프로젝트를 돌아보면 결국 한 가지로 정리된다. 외부 공사업체 안전교육을 웹으로 옮긴 것이 아니라,
시간과 장소 제약 없이도 실제로 이수될 수 있는 흐름으로 다시 설계한 작업이었다.
접근성, 언어, 상태 관리, 이해도 확인, 제출과 결과물까지 각각은 작은 기능처럼 보이지만,
모두 연결됐을 때 비로소 하나의 교육 경험이 됐다.
읽고 끝내지 않는 현장 적용
이 글의 현장 적용 패키지
작업 전 5분 체크
브라우저에서 바로 확인하고 인쇄할 수 있습니다.



