Skip to content

현재 사용법 (2026-09-10, 6fbaee7f): 급여명세서 공개·전자서명 연동 · 기능 마스터. 아래는 작성 당시의 변경 설명이며 현재 전체 동작·재측정 결과를 뜻하지 않습니다.

전자서명 목록 새로고침 시 상태 동기화 + 최신순 정렬 ​

관리자 앱의 전자 서명 탭에서, 페이지를 새로 열거나 헤더의 [새로고침]을 누르면 진행 중인 서명 건의 상태가 Hereby Sign 기준으로 바로 갱신되고, 목록은 최근 발송 건이 위에 오도록 바뀝니다. 이전에는 각 행의 관리 열에 있는 [새로고침] 버튼을 눌러야만 그 한 건의 상태가 갱신됐고, 목록은 오래된 건이 먼저 왔습니다.

배경 / 지금 동작 ​

전자 서명 탭은 관리자 앱(apps/admin)의 #esign 화면입니다. 이 화면은 두 종류의 데이터를 보여줍니다.

  • 레코드(ESignRecord): 관리자가 직원에게 보낸 서명 요청 한 건입니다. 우리 DB 에 저장되며 status(로컬 상태: sending/sent/signed/rejected/expired/error)와 herebyStatusCode(Hereby Sign 이 마지막으로 알려준 원격 상태 코드)를 함께 가집니다. 이하 두 상태 중 어느 쪽이든 signed/rejected/expired 또는 원격의 완료·파기 코드처럼 더 이상 바뀌지 않는 값을 종결 상태라고 씁니다.
  • Hereby Sign: 실제 서명이 이루어지는 외부 서비스입니다. 서명이 완료되거나 거절되면 웹훅으로 우리 서버에 알려주고, 서버는 그 내용을 레코드에 반영합니다.

웹훅은 놓칠 수 있습니다. 서버가 잠깐 내려가 있었거나, 로컬 환경처럼 웹훅이 도달하지 않는 경우입니다. 그래서 라우터에는 레코드 한 건을 골라 Hereby Sign 에 상태를 직접 물어보는 syncStatus 뮤테이션이 있고, 화면에서는 각 행의 관리 열에 있는 [새로고침] 버튼만 이것을 호출했습니다.

반면 페이지 로드, 탭 진입, 헤더의 [새로고침] 버튼은 모두 loadESignIntegrationStatus 를 거쳐 loadESignRecords 로 이어지며, 이 경로는 우리 DB 만 다시 읽습니다. Hereby Sign 에는 묻지 않으므로 웹훅을 놓친 레코드는 관리 열 버튼을 누르기 전까지 옛 상태로 남았습니다. 30초마다 도는 폴링(pollESignRecords)도 같은 DB 조회 경로입니다.

정렬은 별개의 원인입니다. 목록 조회 라우터는 정렬이 없으면 createdAt desc 를 쓰도록 기본값을 두었지만, 입력 검증에 쓰는 공통 paginationSchema 가 sortOrder 기본값을 asc 로 먼저 채웁니다. 관리자 앱은 정렬을 넘기지 않았으므로 라우터의 기본값은 한 번도 적용되지 않았고, 오래된 건이 먼저 왔습니다.

다이어그램 ​

변경 전

변경 후

개념 흐름별 변경 ​

1. 여러 건을 한 번에 동기화하는 API 를 추가합니다 ​

행별 syncStatus 를 화면에서 반복 호출하는 대신, id 목록을 받아 서버에서 처리하는 syncStatuses 뮤테이션을 추가합니다. 입력 스키마 syncESignStatusesSchema 는 packages/core 에 두고, 한 페이지 분량이 상한이므로 id 를 최대 100건으로 제한합니다. SDK 의 ESignService 에 같은 이름의 메서드를 추가하여 관리자 앱이 호출합니다.

서버는 받은 id 를 조직 범위로 조회한 뒤, herebyContractId 가 없거나 이미 종결 상태인 레코드는 Hereby Sign 을 호출하지 않고 건너뜁니다. 종결 상태의 판정은 e-sign.state.ts 의 새 헬퍼 needsRemoteSync 가 맡으며, 기존의 로컬·원격 종결 집합을 그대로 사용합니다. 이 판정을 서버에서 다시 하는 이유는, 화면이 보낸 id 가 그 사이 웹훅으로 종결됐을 수 있기 때문입니다.

남은 레코드는 5건씩 나누어 Promise.allSettled 로 조회합니다. 한 건이 실패해도 나머지는 계속 진행하고, 응답으로 { synced, updated, failed } 를 돌려줍니다. 실패가 있으면 서버 로그에 경고를 남깁니다. 동시 실행 수를 작게 둔 것은 Hereby Sign 의 요청 제한을 고려한 것입니다.

기존 syncStatus 가 하던 Hereby 조회, 연동 상태(health) 기록, DB 갱신, 로그 남기기는 syncRecordWithHereby 함수로 추출하여 단건·일괄 두 경로가 같은 코드를 씁니다.

2. 새로고침 경로에서 화면의 미완료 건을 동기화합니다 ​

관리자 앱의 loadESignIntegrationStatus 는 목록을 그린 뒤 새 메서드 syncVisibleESignStatuses 를 호출합니다. 이 메서드는 현재 화면의 레코드 중 herebyContractId 가 있고 종결 상태가 아닌 것의 id 를 골라 syncStatuses 를 부르고, 응답의 updated 가 1 이상일 때만 목록을 다시 읽습니다. 후보 선별은 apps/admin/src/utils/esignSync.ts 의 esignSyncCandidateIds 가 맡습니다.

목록을 먼저 그리고 동기화를 뒤에 두는 이유는 화면이 Hereby Sign 응답을 기다리며 비어 있지 않게 하기 위해서입니다. 동기화 실패는 콘솔 로그만 남기고 목록은 유지합니다. 목록은 이미 그려져 있고, 연동 상태 표시등이 장애를 별도로 알리기 때문입니다.

30초 폴링에서는 동기화를 호출하지 않습니다. 폴링에 넣으면 Hereby Sign 호출이 탭을 열어 둔 시간에 비례해 늘어나므로, 사용자가 의도한 새로고침(페이지 로드·탭 진입·헤더 버튼)에서만 외부 조회를 합니다.

3. 목록 정렬을 생성일 내림차순으로 고정합니다 ​

관리자 앱의 목록 요청에 sortBy: "createdAt", sortOrder: "desc" 를 명시합니다. 라우터의 기본값을 desc 로 두는 것만으로는 공통 스키마의 asc 기본값이 먼저 적용되어 효과가 없으므로, 호출 측에서 정렬을 보내는 방식을 택했습니다.

4. 테스트 ​

  • e-sign.state.test.ts: needsRemoteSync 가 진행 중 레코드는 동기화 대상으로, 로컬·원격 어느 쪽이든 종결된 레코드는 제외하는지 확인합니다.
  • esignSync.test.ts: esignSyncCandidateIds 가 contractId 없는 건과 종결 건을 제외하고, id 가 문자열인 건만 고르는지 확인합니다.
  • app-esign-status.test.ts: 새로고침 경로에서 정렬 파라미터가 붙는지, 미완료 건의 id 로 syncStatuses 가 호출되는지, 바뀐 건이 있을 때만 목록을 다시 읽는지, 후보가 없으면 호출을 생략하는지, 동기화가 실패해도 목록이 유지되는지, 폴링 경로에서는 호출하지 않는지를 확인합니다.