Ansimsign
안심사인
계약서를 만드는 화면부터, 서명이 끝난 문서까지.


출발점과 담당 범위
전자계약 · 신규 구축
맡은 범위
요구사항과 제품 정책, 웹 화면·API·DB의 개발, 검증과 배포, 사용자 대응을 담당했습니다. 계약 작성에서 완료 문서 조회까지 기능별로 끊지 않고 전체 사용 흐름을 맡은 프로젝트입니다.
시작할 때의 상태
전자계약 서비스를 새로 구축했습니다.
개발 범위 · 기술 · 협업 자세히 읽기
시작할 때의 상태
전자계약 서비스를 새로 구축했습니다. 계약을 관리하던 실무 경험을 바탕으로 발신자·수신자가 각각 무엇을 해야 하는지 정리하고, 운영을 시작한 뒤에도 고객 피드백과 배포를 함께 맡았습니다.
작업 개요
계약 작성·발송·본인확인·서명·PDF 봉인과 회사 공간을 하나의 서비스로 연결했습니다. 실제 사용 중 들어온 피드백을 화면 수정으로 끝내지 않고, 데이터가 저장되고 최종 문서로 남는 과정까지 따라가며 개선하고 있습니다.
개발·개선 범위
- 계약서 업로드·템플릿·필드 배치·수신자 설정과 발송 화면
- 본인확인·서명 세션·임시 저장·최종 제출 API와 데이터 모델
- PDF 필드 합성·긴 글 별지·봉인 워커·타임스탬프·파일 검증
- 사람과 회사별 소속의 분리, 공간 전환·권한·강퇴·탈퇴 정책
- 이메일·문자·알림톡 발송 연동, 회귀 검증·배포·실사용 피드백 반영
- 사용한 기술
- TypeScript · React · Vite · Node.js · Express · PostgreSQL · Knex · AWS ECS · S3 · Docker · GitHub Actions
기존 자산 · 외부 서비스 · AI 활용
Claude Code와 Codex를 조사·구현·독립 리뷰·테스트에 활용했습니다. 제품 정책과 작업 범위, 배포 여부를 결정하고 재현 결과를 확인했습니다. PASS·메시지·TSA 공급자와 PDF·문서 변환 라이브러리는 연동한 외부 구성요소입니다.
제품 화면과 구조 요약
제품 화면과 구조 요약 보기

문제 해결 / 소속과 로그인 분리
한 회사에서 나가도, 다른 회사의 로그인은 남아야 합니다.
회사에서 나가는 범위와, 사람의 로그인 정보를 지우는 범위를 분리했습니다.
사람 하나, 회사별로 다른 소속
강퇴·탈퇴 후 쓰기 차단
늦게 도착한 수정 요청도 다시 확인공통 로그인 정보 보존
다른 소속까지 함께 지우지 않음마지막 소속이라면이전 회사에 남은 소셜 연결까지 정리
문제
한 회사의 탈퇴가 다른 회사의 로그인까지 건드릴 수 있었습니다.
바꾼 점
인증 정보와 소속을 나누고, 수정 요청이 끝나기 전에도 강퇴·탈퇴 상태를 다시 확인합니다.
확인한 것
요청 중 강퇴, 다른 소속이 남은 탈퇴, 마지막 소속의 탈퇴를 각각 확인했습니다.
조사·판단·검증 과정 읽기
마주한 문제
한 사람과 한 회사의 계정을 같은 축으로 다루던 구조에 여러 회사 공간이 필요해졌습니다. 회사에서 나가는 행동이 사람의 로그인 정보까지 지우거나, 강퇴 직전에 시작한 수정 요청이 정리한 정보를 다시 쓰는 문제가 생길 수 있었습니다.
확인한 과정
토큰 발급을 바꾸는 것과 실제 비밀번호 조회를 옮기는 것은 다른 작업이었습니다. 로그인·추가 인증·프로필 변경·소셜 연결의 읽기와 쓰기를 각각 추적했습니다. 탈퇴 시 다른 소속을 보존하는 백엔드는 이미 있는 부분도 있어, 새 삭제 기능을 만들기보다 잘못된 화면 안내와 빠진 정리 경로를 구분했습니다.
선택한 방법
사람의 인증 정보와 회사별 소속을 분리했습니다. 같은 트랜잭션에서 쓰기 사본을 맞춘 뒤 토큰 검증, 실제 자격 조회 순서로 전환했습니다. 다른 소속이 남으면 공통 로그인 정보를 보존하고, 마지막 소속이 끝나면 이전 회사에 남은 소셜 연결까지 정리했습니다. 프로필 쓰기와 소셜 콜백에도 강퇴·탈퇴 상태를 다시 확인하는 조건을 뒀습니다.
어떻게 확인했는가
요청 도중 강퇴되는 순서, 다른 회사의 소속이 남는 경우, 마지막 소속에서 탈퇴하는 경우를 나눠 확인했습니다. 쓰기 사본을 다시 켜는 과정에서는 이메일 변경과 신규 가입이 충돌하는 반례를 찾아, 기존 사본 동기화를 신규 백필보다 먼저 하도록 순서를 바꿨습니다. 운영 개방 때는 스냅샷과 백필 재실행 결과를 별도로 확인했습니다.
단계별 처리와 경계
쓰기 사본 정렬
기존 인증 경로를 유지하며 실제 갱신 결과를 같은 트랜잭션으로 옮깁니다.
토큰 검증 전환
사람·회사·소속의 관계와 각 인증 버전을 함께 확인합니다.
자격 조회 전환
비밀번호와 추가 인증이 실제로 읽는 원본까지 옮깁니다.
탈퇴 범위 확인
다른 소속이 남는 경우와 마지막 소속이 끝나는 경우를 구분합니다.
회사 공간 개방
복구 준비 후 다중 소속을 열고 백필·재실행 결과를 확인합니다.
구조도와 처리 경계 자세히 보기
공통 로그인은 사람에게, 회사 권한은 소속에게
사람의 인증 축과 회사별 권한 축을 나누고, 다른 소속의 존재 여부로 보존·정리 범위를 결정합니다.
- 처리·명시된 호출
- 비동기·별도 요청
- 데이터 읽기·참조
- 실패·제한 분기
판정·계산식
남은 소속 = 같은 person의 다른 소속 중 status NOT IN (removed, withdrawn)
- 닫힌 소속과 정지된 소속
- removed·withdrawn만 종결로 봅니다. suspended도 다른 소속 수에 포함하지만 모든 기능을 허용하지는 않습니다. 강퇴 함수가 받는 대상은 active뿐입니다.
- 마지막 회사에서 떠날 때
- B회사 탈퇴 때 B의 연결만 지우면 앞서 보존한 A의 연결이 남습니다. 마지막 정리를 person 범위로 넓히고, 소셜 인증 중 강퇴돼도 새 연결 전에 다시 검사합니다.
- 되돌릴 수 있는 전환 구간
- 쓰기 사본, 토큰 발급·검증, 자격 조회를 순차 전환했습니다. 다중 소속은 미러·v2 발급을 전제로 엽니다. 첫 공유 person 이후에는 플래그 OFF로 1:1 모델이 복원되지 않습니다.
이 구조의 한계suspended가 모든 기능에서 허용된다는 뜻은 아닙니다. 회사 탈퇴의 보존 정책과 계정·계약 증거 전체 삭제를 구분합니다.
단계별 설명 · 구조도를 글로 읽기
사람의 인증 정보
person·비밀번호·ptv · 회사 소속과 분리
- 회사별 소속 (데이터 읽기·참조)
- 로그인 자격 조회 (데이터 읽기·참조)
회사별 소속
app_user·회사·역할 · 소속별 권한 버전 mav
- 토큰 발급·대조 (데이터 읽기·참조)
- 소셜 콜백 검사 (데이터 읽기·참조)
토큰 발급·대조
person ptv·소속 mav · 종결 상태·회사 확인
- 회사 공간 전환 (데이터 읽기·참조)
- 프로필 쓰기 검사 (처리·명시된 호출)
- 강퇴·회사 탈퇴 (처리·명시된 호출)
회사 공간 전환
같은 사람의 소속 선택 · 원래 만료시각 승계
- 토큰 발급·대조 (처리·명시된 호출)
소셜 연결
person과 원래 소속 · 보존·회수 범위 분리
로그인 자격 조회
person에서 비밀번호 조회 · 소속 선택 순서는 별도
- 토큰 발급·대조 (처리·명시된 호출)
프로필 쓰기 검사
UPDATE 순간 다시 확인 · removed·withdrawn 차단
강퇴·회사 탈퇴
대상 소속·회사만 종료 · 서로 다른 DB 진입점
- 다른 소속 판정 (처리·명시된 호출)
다른 소속 판정
removed·withdrawn 제외 · suspended는 남은 소속
- 공통 신원 보존 (다른 소속 있음)
- 마지막 소속 정리 (마지막 소속)
소셜 콜백 검사
연결 도중 강퇴돼도 재검사 · 종결 소속은 신규 연결 X
- 소셜 연결 (처리·명시된 호출)
공통 신원 보존
person·소셜 연결 유지 · 종료 소속의 권한만 변경
마지막 소속 정리
사람·소속의 자격 정리 · person 범위 소셜 회수
구조 탐색과 실제 소스 대조를 바탕으로 편집한 작동도입니다. 런타임 전체의 자동 검증을 뜻하지 않습니다.
문제 해결 / 긴 글과 최종 문서
입력할 수 있는 글이라면, 완성된 계약서에서도 읽혀야 합니다.
작은 칸에서 잘리는 긴 글을, 앞뒤를 찾아 읽지 않아도 되는 전문 별지로 옮겼습니다.
칸은 작아도, 문맥은 끊기지 않도록
넘치지 않으면별지를 만들지 않음
문제
화면에서 입력한 긴 글이 최종 PDF에서는 작아지거나 잘렸습니다.
바꾼 점
칸을 넘친 필드는 나머지만 자르지 않고 전문과 원래 위치를 별지에 싣습니다.
확인한 것
DB 조회부터 PDF 출력까지 연결하고, 문단 보존과 긴 입력의 처리 시간도 확인했습니다.
조사·판단·검증 과정 읽기
마주한 문제
수신자가 긴 내용을 입력해도 최종 PDF의 작은 칸에서는 글이 줄어들거나 뒤가 잘렸습니다. 화면에서 받은 내용과 실제로 전달하는 문서가 달랐습니다.
확인한 과정
입력 길이를 더 제한하려면 PDF의 실제 칸 크기와 페이지 정보를 알아야 했고, 추가 데이터 작업뿐 아니라 정상 서명을 막는 문제도 있었습니다. 넘친 부분만 별지로 보내면 독자가 본문과 뒷장을 이어 읽어야 한다는 점도 검토했습니다.
선택한 방법
기존 한 줄 필드는 유지하고 긴 글 필드는 줄바꿈하도록 나눴습니다. 그래도 넘치는 필드는 잔여 부분이 아닌 전문을 별지에 싣고, 원래 쪽 번호와 필드 이름을 함께 표시했습니다. 첫 장의 종이 규격을 따르며, 넘친 내용이 없을 때는 별지를 만들지 않습니다.
어떻게 확인했는가
렌더 함수만 시험하면 실제 DB 조회에서 필드 이름이 빠져도 통과했습니다. 조회부터 출력까지 이어지는 통합 검증을 추가했습니다. 이후 빈 줄 정리용 AI 초안의 정규식이 긴 입력에서 워커를 오래 점유하는 반례가 나와 줄 단위 처리로 바꿨습니다. 페이지 수뿐 아니라 문단 보존·공백 처리 결과·긴 입력의 시간 상한도 검사하도록 보강했습니다.
단계별 처리와 경계
최종 입력 조회
값뿐 아니라 필드 이름·좌표·원본 쪽 정보를 함께 읽습니다.
본문 칸에 합성
한 줄 필드와 긴 글 필드의 표시 규칙을 나눕니다.
넘침 판정
칸을 넘은 긴 글의 전체 내용과 원래 위치를 별지 입력으로 넘깁니다.
전문 별지 작성
문단을 보존하며 배치하고 상한에 도달하면 안내를 남깁니다.
최종 PDF 봉인
본문과 별지가 합쳐진 문서를 이후 봉인 처리의 입력으로 사용합니다.
입력한 글이 최종 문서에 남는 과정
본문 합성, 넘침 판정, 별지 작성과 봉인까지 단계별로 살펴봅니다.
문제 해결 / 서명 완료와 봉인
서명을 다 받았다는 것과, 최종 파일이 준비됐다는 것은 다릅니다.
서명은 끝났지만 파일은 아직 준비 중인 시간을, 별도의 상태로 다뤘습니다.
완료를 두 번 구분하는 이유
모두 제출했음
같은 DB 트랜잭션에서 봉인 작업 생성- 원본 해시 대조
- 본문·별지 합성
- 외부 TSA 요청
- 작업 점유 재확인
최종 결과 확정
봉인 상태·감사 이벤트·교부 예약 기록문제
서명 완료 뒤에도 PDF 제작과 외부 타임스탬프 발급이 남아 있었습니다.
바꾼 점
봉인 작업을 워커로 넘기고, 봉인 전 감사체인 헤드와 최종 PDF 해시를 TSA에 연결합니다.
확인한 것
두 완료 상태와 해시 연결을 나눠 검사하고, 점유를 잃은 작업의 결과 확정을 차단했습니다.
조사·판단·검증 과정 읽기
마주한 문제
모든 사람이 서명한 뒤에도 PDF 합성, 저장소 쓰기, 외부 타임스탬프 발급이 남습니다. 이를 하나의 완료 상태로 다루면 아직 준비되지 않은 파일을 안내하거나, 외부 서비스 지연을 계약 제출 실패로 오해하게 됩니다.
확인한 과정
입력 임시 저장, 최종 제출, 전체 서명 완료, 봉인 작업의 경계를 나눴습니다. 워커가 오래 걸리거나 재시도할 때 작업을 더 이상 소유하지 않은 실행이 결과를 확정할 수 있는지, 타임스탬프가 정확히 어느 파일과 감사 기록을 가리키는지도 확인했습니다.
선택한 방법
최종 제출 트랜잭션에서 전체 서명 완료와 봉인 작업 생성을 처리하고, PDF 제작은 워커가 이어받도록 연결했습니다. 워커는 원본 해시를 대조하고 최종값을 합성한 뒤, 봉인 전 감사체인 헤드와 최종 PDF 해시를 묶어 TSA 타임스탬프를 요청합니다. 결과 확정 직전에도 작업 점유를 다시 확인합니다.
어떻게 확인했는가
서명 완료와 봉인 완료를 별도로 확인하고, 파일 해시·감사체인·타임스탬프의 연결 및 오래된 작업의 확정 차단을 회귀 검증했습니다. 공개 PDF 업로드 검증은 파일 자체를 검사하는 경로로, 계약 DB의 감사 기록까지 대조하는 검증과 구분했습니다.
단계별 처리와 경계
서명 최종 제출
필수값을 검증하고 임시 입력을 확정값으로 옮깁니다.
전체 서명 완료
대상자의 제출이 끝나면 같은 DB 트랜잭션에서 봉인 작업을 생성합니다.
PDF 제작
워커가 원본 해시를 확인하고 본문·별지를 합성해 최종 파일을 만듭니다.
외부 시점 연결
봉인 전 체인 헤드와 최종 PDF 해시를 묶어 TSA에 요청합니다.
봉인 결과 확정
점유를 재확인한 뒤 봉인 상태·감사 이벤트·교부 예약을 기록합니다.
구조도와 처리 경계 자세히 보기
서명 완료와 PDF 봉인은 서로 다른 상태다
입력 저장·서명 완료·PDF 봉인을 구분하고, 넘친 입력은 DB의 최종값을 조회해 별지로 연결합니다.
- 처리·명시된 호출
- 비동기·별도 요청
- 데이터 읽기·참조
- 실패·제한 분기
판정·계산식
preimage = 봉인 전 head ∥ SHA256(최종 PDF) · messageImprint = SHA256(preimage)
- 봉인본이 아직 없는 완료 상태
- 전체 제출이 끝나면 completed 변경과 봉인 작업 생성을 같은 DB 트랜잭션으로 처리합니다. 워커의 PDF·TSA 처리가 끝나 sealed로 확정되기 전에는 봉인본이 없을 수 있습니다.
- 렌더 함수 밖에서 빠진 필드 이름
- 렌더 테스트만으로는 조회에서 label이 빠지는 문제를 잡지 못했습니다. DB의 최종값·이름이 별지까지 전달되는지, 상한을 넘으면 잘림을 안내하는지 연결해서 확인했습니다.
- 외부 요청과 DB 확정의 간격
- 교부 예약은 실제 수신과 다릅니다. TSA 일시 장애는 시도를 차감하지 않고 미룹니다. DB 확정 뒤 저장소의 확정본을 다시 쓰지만, DB·저장소·TSA가 함께 롤백되지는 않습니다.
이 구조의 한계주 TSA 앵커 뒤 envelope_sealed 감사가 추가됩니다. 최종 감사 헤드 앵커 완료나 특정 PAdES 등급을 주장하지 않습니다.
단계별 설명 · 구조도를 글로 읽기
서명 세션 확인
수신자·만료 재검사 · 인증 방식별 증거
- 임시 입력 저장 (처리·명시된 호출)
임시 입력 저장
field_draft에 저장 · 세션·필드 소유 확인
- 서명 최종 확정 (데이터 읽기·참조)
서명 최종 확정
필수값 검사·값 승격 · 세션 소비·감사 기록
- 전체 서명 판정 (처리·명시된 호출)
전체 서명 판정
모든 서명자가 완료? · 미완료면 대기·다음 차수
- completed (처리·명시된 호출)
completed
봉투 완료·작업 생성 · PDF 봉인 완료와 다름
- 봉인 작업 점유 (비동기·별도 요청)
봉인 작업 점유
워커가 처리할 작업 점유 · lease·attempt 확인
- 최종 데이터 조회 (처리·명시된 호출)
최종 데이터 조회
원문값·label·좌표 · 짧은 DB 트랜잭션
- 원본 PDF 확인 (처리·명시된 호출)
원본 PDF 확인
저장소 읽기·해시 대조 · DB 잠금 밖 외부 I/O
- 본문·별지 합성 (처리·명시된 호출)
본문·별지 합성
넘친 필드의 전문 연결 · 100쪽 상한·잘림 안내
- PDF 서명 생성 (처리·명시된 호출)
PDF 서명 생성
최종 바이트·해시 생성 · PDF 내부 TSA는 조건부
- PDF 저장 (처리·명시된 호출)
PDF 저장
주 TSA 요청 전에 put · 저장소와 DB는 별개
- TSA 앵커 (처리·명시된 호출)
TSA 앵커
봉인 전 헤드+PDF 해시 · 외부 응답 후 DB 확정
- sealed 확정 (처리·명시된 호출)
- TSA 일시 지연 (일시 오류)
sealed 확정
lease 대조·교부 예약 · TSA·감사 함께 기록
- 파일 자체 검증 (파일을 업로드하면)
TSA 일시 지연
재시도 시각으로 미룸 · completed 상태로 대기
파일 자체 검증
업로드 PDF만 검증 · 계약 DB·감사 조회 없음
구조 탐색과 실제 소스 대조를 바탕으로 편집한 작동도입니다. 런타임 전체의 자동 검증을 뜻하지 않습니다.
결과와 현재의 경계
결과, 그리고 지금.
실제 계약 업무에 사용하는 서비스를 구축하고 운영 개선을 이어가고 있습니다. 입력 화면과 최종 PDF의 불일치, 여러 회사에 속한 사람의 탈퇴 범위처럼 사용 중 드러나는 문제를 웹·API·DB를 함께 바꿔 해결했습니다.
회사 업무로 만든 서비스입니다. 제품 개발·운영을 담당한 범위와 외부 인증·문서 도구의 역할을 구분합니다. 공개 사례는 구현과 당시 검증 기록을 설명하며, 특정 법적 효력이나 PAdES 등급을 보증하는 자료는 아닙니다.
작성 기준: 2026년 9월 작업 기록·소스 대조. 과거 검증 결과를 현재의 모든 동작에 대한 보증으로 사용하지 않습니다.
역할과 협업 이야기하기