RUNNING SIGN119

Sign119

소방서의 훈련·출동·교육 시 종이로 받던 서명 명부를 웹으로 대체한 전자서명 수집 플랫폼.
링크·QR로 공유하면 참가자가 모바일에서 자필 서명을 남기고, 무결성 해시가 박힌 PDF로 즉시 출력된다.

공인 전자서명(PKI)이 아닌 자필 서명 이미지 수집. 참석 확인·동의 명부 용도.

Next.js 15 React 18 TypeScript PostgreSQL Prisma PDFKit Node.js Nginx Vitest Playwright
PROBLEM

이 시스템이 해결하는 문제

BEFORE

종이 명부 수기 취합·보관

교육·훈련·출동마다 종이에 서명받고 관리자가 수기로 취합·보관했다. 잃어버리면 증빙이 없고, 보관에도 공간이 필요했다.

AFTER

링크·QR 접속 → 모바일 자필 서명 제출. 완전 비대면·무지화(無紙化). 제출 즉시 관리자 화면에 실시간 반영된다.

BEFORE

증빙 문서의 사후 위변조

출력된 명부가 나중에 수정됐는지 확인할 방법이 없었다. 법적 증빙이 필요한 상황에서 원본성 보장이 어려웠다.

AFTER

sha256 해시 16자를 PDF 푸터에 박아 사후 변조 여부를 검증 가능하게 한다. 감사로그는 DB 레벨에서 UPDATE/DELETE를 차단한 append-only 설계다.

BEFORE

대리 서명·중복 제출

누군가가 다른 사람 이름으로 서명하거나, 같은 사람이 여러 번 제출하는 것을 종이 명부 방식으로는 막기 어려웠다.

AFTER

기기 핑거프린트(SHA-256 해시만 저장) + localStorage UUID 기반 디바이스 키와 DB unique 제약으로 애플리케이션·DB 양쪽에서 차단한다.

ARCHITECTURE

단일 프로세스 풀스택

Next.js 15 App Router가 관리자 화면, 서명자 화면, REST API, PDF 렌더링을 하나의 프로세스에서 처리한다. 앞단 Nginx가 TLS 종단·정적 서빙·네트워크 레벨 rate limit을 맡는다.

SIGNATURE PIPELINE

PDF PIPELINE

ENGINEERING

기술적으로 내세울 것들

개발자 관점에서 설계가 까다로웠던 지점들.

01
위변조 탐지 가능한 PDF
문서ID·제목·내용·일시·서명 목록을 sha256으로 해싱해 앞 16자를 PDF 푸터에 박는다. 나중에 같은 연산을 돌려 비교하면 사후 변조 여부를 즉시 확인할 수 있다.
createHash("sha256").update(json).digest("hex").slice(0, 16) // PDF 푸터 // json = 문서·일시·서명 목록의 canonical JSON
02
append-only 감사로그
모든 쓰기 행위(로그인, 문서 CRUD, PDF 다운로드, 이름 수정 등)를 감사로그 테이블에 기록한다. DB 레벨에서 해당 테이블에 UPDATE/DELETE 권한을 REVOKE해 애플리케이션 버그나 SQL 실수로도 삭제가 불가능하다.
03
다층 중복 서명 방지
Canvas/WebGL 핑거프린트(원본 미저장, SHA-256 해시만 보관) + localStorage UUID를 조합한 디바이스 키를 DB unique 제약과 결합한다. 시크릿 모드 등 완전 차단이 불가한 한계도 문서에 투명하게 명시했다.
04
업로드 보안 파이프라인
서명 PNG를 magic bytes 검증 후 sharp로 강제 재인코딩해 악성 청크를 제거한다. 저장 경로는 realpath prefix 검증으로 path traversal 공격을 방어한다. 서명 이미지·핑거프린트·쿠키는 pino 로그에서 자동 마스킹(redact)된다.
05
한글 PDF 실시간 생성
한글 깨짐 문제로 @react-pdf/renderer 대신 PDFKit으로 교체하고 Noto Sans KR 서브셋 TTF를 번들했다. A4 40명 기준 p95 < 3초 목표의 서버 사이드 즉시 생성. 소속 텍스트 스냅샷도 함께 저장해 조직 개편 후에도 당시 명칭을 그대로 보존한다.
PDF 생성 p95 < 3초 목표 테스트 89건 (unit + integration) E2E 7개 시나리오 · CI (GitHub Actions)
06
시간대 엄격성 + AI graceful degradation
DB는 UTC timestamptz로 저장하고 모든 UI·API·PDF는 KST 고정 표기한다. 증빙 문서의 시각 모호성을 제거하기 위해서다. 서명 완료 격려 메시지는 OpenAI(gpt-4o-mini)가 자동 생성하되, 외부 API 장애 시에도 서비스가 중단되지 않는 graceful degradation 설계다.
Next.js 15 React 18 TypeScript 5.5 Node.js 20 PostgreSQL 16 Prisma 6 PDFKit sharp iron-session Zod pino Vitest Playwright Biome Nginx PM2 OpenAI
IN THE FIELD

지금, 현장에서 쓰이고 있습니다

경기소방재난본부 성남소방서가 훈련·출동·교육의 참가자 명부를 Sign119로 수집·증빙하고 있습니다.

Sign119 운영 중인 문서 목록 화면 — 문서 제목·소속 편성은 비식별 처리됨

문서 목록

Sign119 서명부 화면 — 소속 편성·이름·서명은 비식별 처리됨

서명부

Sign119 다기관 서명부 화면 — 기관명·이름·서명은 비식별 처리됨

다기관 서명부

경기소방재난본부 성남소방서 — 훈련·출동·교육의 참가자 명부를 Sign119로 수집·증빙하고 있습니다.

* 문서 제목·소속 편성·개인 성명·서명은 비식별 처리되었습니다.
* 본 서비스는 공인 전자서명이 아닌 자필 서명 수집(참석 확인·동의 명부) 용도입니다.