ZIP 한 번에 세상이 담기기까지, 하루가 다 갔다
목차
오늘 커밋을 세어봤더니 서른다섯 개가 넘는다. 숫자만 보면 뿌듯해야 하는데, 막상 git log를 스크롤하다 보면 뿌듯함보다 "왜 이걸 이제야"가 먼저 나온다. 어제(0813) 실제로 민원 문서를 수기로 발송했던 게 직접적인 트리거였다. 수동 발송 세트를 한 번 경험하고 나니까 자동화 범위가 눈에 확 들어왔다. 그래서 오늘 하루 거의 전부를 민원 문서 자동화에 쏟아부었다.
ZIP 한 방으로 끝내기까지 - 생각보다 깊었던 구멍
시작은 단순했다. "수신처별로 공문·내용증명 다 들어간 ZIP 하나 뽑으면 되지" 싶었다. 근데 막상 파고들면 파고들수록 빠진 게 계속 나왔다.
첫 번째로 확인한 건 은행 통지문 특정 로직이었다. 사건에 물린 은행이 어디인지를 자동으로 잡아야 공문 수신처를 구성할 수 있는데, 이게 DB에서 단순 조회가 아니라 쿼리 매퍼를 뒤져야 하는 구조였다. 거기서 sqlmap 손대고, 은행 통지문 자동 특정 로직 붙이고, 그제야 ZIP 뼈대에 공문과 내용증명 두 종을 넣을 수 있었다.
영수증이 변수였다. 영수증은 건별 낱장이라 단순 PDF 합본이 아니라 개별 파일로 ZIP에 인입해야 했다. 동시에 공문 별첨도 보강해야 한다는 게 목록을 보니까 명확했다. 흐름도·수신내역·회신양식·조치내역 네 가지를 별첨으로 붙이는 작업을 하면서 별첨 번호 부여 규칙이 위저드 모드와 자동생성 모드 사이에서 살짝 달랐다는 걸 발견했다. 위저드에서 수동 항목(우체국 접수증, 계약서 발췌본)을 제외한 기준을 자동생성 모드가 따르지 않고 있었던 것. fix 두 방 연달아 나온 이유가 그거다.
문서번호 자동 채번도 그날 안에 끝냈다. 이전까지는 ZIP 안에 들어간 문서마다 '(발송 시 채번)' 안내문이 붙어 있었는데, 사실상 그냥 공란 취급이었다. ZIP 생성 시점에 채번하도록 바꾸고 나니 문서마다 실번호가 찍혀서 훨씬 완결된 느낌이 났다.
레터헤드, 워터마크, 페이지 번호 - 공문서 품질의 기본
이게 의외로 시간을 많이 잡아먹었다. 처음엔 "로고 이미지 하나 넣으면 끝 아니야"라고 생각했는데 전혀 아니었다.
레터헤드에 심볼과 워드마크 조합을 넣는 건 그렇다 치고, 은행 서식 사본에는 워터마크를 빼야 한다는 게 이미 결정된 정책이라 조건 분기를 넣어야 했다. 전 페이지 워터마크라는 것 자체가 PDF 생성 레이어에서 페이지마다 오버레이를 얹는 방식이라 페이지 번호 작업이랑 같이 묶여 있었다. n / 전체 형식 페이지 번호를 전 문서 공통으로 깔아야 하는데, 이게 합본 PDF(전체인쇄.pdf) 생성할 때 페이지 카운트 누적이 제대로 안 되면 깨진다. 두어 번 실측해서 확인했다.
전체인쇄.pdf 자체가 오늘 추가된 기능인데, 수신처 간지 + 전 PDF 병합 + 이의제기신청서 docx 인쇄용 사본까지 한 파일로 묶는 거다. 담당자가 이 파일 하나만 뽑아서 인쇄하면 전 수신처 세트가 순서대로 나오게 하는 게 목표였다. 합본 순서 로직이 간지 위치에 따라 달라지다 보니 생각보다 신경 써야 했다.
메일문안.html도 ZIP에 포함시켰다. 수신처별 제목과 본문, 복사 버튼까지. 이건 사실 당연히 들어가야 했는데 지금까지 빠져 있던 것.
이의제기신청서 버그 세 방 연속
이게 좀 창피한 부분이다. 이의제기신청서 docx를 자동 채움하는 과정에서 버그가 세 종류 나왔다.
첫째는 서명일이 2026년 8월로 하드코딩되어 있었다. 이 문서를 9월에 쓰면 8월짜리가 찍혀서 나갔을 것이다. [[작성년월]] 토큰으로 교체하면서 docx 원본도 같이 수정했다.
둘째는 토큰 단위 중복·허위 일치 단정 문제였다. 실문서를 열어서 실측해보니 토큰이 겹치는 패턴에서 replace 로직이 엉뚱한 위치를 건드리는 케이스가 있었다. 추상화된 상태에서 단위 테스트로는 잘 잡히지 않을 버그다. 실제 docx 놓고 돌려봐야 보인다.
셋째는 '전체 입금 중 통지 대상' 표기. 은행에 보내는 이의제기신청서에는 통지 건수가 전체 입금 중 몇 건인지를 대조해서 표기해야 하는데 이게 자동으로 안 채워지고 있었다. 3분기 기준으로 값을 계산해서 넣도록 했다.
AI가 법령을 지어낸다는 문제
오늘 가장 까다로웠던 부분 중 하나가 AI 초안에서 나오는 법령 인용이었다. 내용증명이나 공문에 법령 조항을 인용할 때 AI가 그럴듯하지만 없는 조문을 만들어 넣는 케이스가 실제로 발견됐다. 이걸 발견한 게 전수검증 과정에서였는데, 조항 번호가 실제로 없거나 내용이 전혀 다른 경우가 있었다. 공문에 허위 법령이 인용된 채로 나가면 말 그대로 큰일이다.
대응은 두 방향으로 했다. AI 프롬프트 레이어에서 법령 인용을 직접 생성하지 말고 반드시 실기록에서 끌어오도록 제약을 걸었고, 회신기한 날짜도 마찬가지로 AI가 기간을 자의적으로 창작하지 못하게 실기한 날짜를 직접 주입하는 방식으로 바꿨다. "~일 이내에 회신해 주십시오" 같은 말이 문서 앞부분과 뒷부분에서 다른 날짜를 가리키는 자기모순 케이스도 있었다.
KYC 빈 표 명시 처리도 같이 했다. KYC 정보가 없는 사건에서 표 자체가 날아가는 대신 명시적으로 비어있는 표를 남기도록.
조치이력 별첨 자동 합성 - codex가 지적한 네 가지
조치이력 별첨을 자동 합성하는 기능이 오늘 들어갔는데, 처음 올린 버전에서 codex 지적이 4건 나왔다.
사건별 문서번호 분리, 현재유지 최종전이 한정, 시간순 병합, NPE 가드. 하나하나 보면 다 납득 가는 지적이었다. 특히 현재유지 상태를 최종 전이로 한정하는 부분은 이력을 그냥 전부 나열하면 중간 상태가 뒤섞여서 읽기 어렵다는 문제였다. NPE 가드는 말 그대로 null check 빠진 거. 부끄럽지만 빠르게 고쳤다.
파트너 잠금, 상태전이, 문서발행까지 이력에 자동으로 묶어서 별첨으로 내보내는 게 목표였는데, 어제 0813 발송분을 수기로 보완했던 이력이 있어서 그 부분 자동화 의미가 더 컸다.
간단생성 모드 UI 숨김 - 놓치기 쉬운 디테일
민원 문서 생성 화면에는 두 가지 모드가 있다. 위저드 모드(회원 선택 → 수신처 설정 → 딸깍)와 간단생성 모드(통지문→ZIP 단순 동선). 간단생성 모드에서는 위저드 전용 UI를 보여줄 필요가 없는데 숨김 처리가 빠진 요소가 여럿 있었다. 회원 검색, 선택 상태 표시, 수신처 스텝, 은행 양식 편집기, 별첨 편집기까지. JSP 하나에서 조건 분기를 꼼꼼히 챙겨야 하는 작업인데 한 번에 다 잡지 못하고 두 커밋으로 나눠서 나왔다.
Partner API - 시작은 5종 신설, 끝은 보안 게이트
민원 쪽 작업과 병렬로 파트너 API 작업도 있었다. 대리점별 쿠폰 발송 B2B API 5종을 /api/v1/partner/dispatch에 신설하는 게 큰 줄기였다. 외부 파트너가 curl이나 Axios로 호출하는 서버 클라이언트 환경에서 403이 나는 문제가 있었는데, CSRF 필터나 세션 의존 로직이 개입하는 전형적인 패턴이었다. 필터 레이어에서 해당 경로 처리를 조정하고, 토큰 단일활성 정책도 문서에 명시했다.
Webhook 전용 연동 문서를 별도로 신설하면서 포탈 API 문서 화면을 재구성했다. 원래 발송 API 문서가 운영 도메인 기준 링크를 쓰고 있어서 파트너가 포탈 도메인으로 호출하다가 302 리다이렉트를 오해하는 상황이 있었다. www 절대경로로 고치고, 운영 Base URL 실값도 문서에 직접 명시했다.
보안 게이트는 오늘 마지막 즈음에 들어갔다. 연동 문서 페이지가 로그인 없이 접근하면 내부 모델(쿼리 구조, 파라미터 스펙)이 그대로 노출되는 게 문제였다. 게이트를 걸어서 비로그인 상태에서는 차단되도록 했다. 당연히 있어야 할 것이었는데 이제서야 들어간 셈.
dispatch/partners 엔드포인트 설명에서 키 스코프 설명도 정정했다. 자기 키로 조회하면 본인과 하위 전체가 반환되는 계층 구조인데 문서에 그 범위가 명확히 안 나와 있었다.
배치 데드락 - 새벽 정산이 잠그는 구간
이건 오늘 갑자기 터진 게 아니라 추적하던 이슈인데 오늘 수정이 들어갔다. 지표 버킷 리빌드 배치가 새벽 정산 INSERT 타이밍이랑 겹치면서 데드락이 발생하는 케이스였다. 격리수준을 REPEATABLE READ에서 READ COMMITTED로 내렸다. 리빌드 특성상 스냅샷 일관성보다 충돌 회피가 더 중요한 구간이라 타당한 판단이었다. 새벽 타이밍이라 실제 재현이 쉽지 않아서 로직으로 추론한 부분이 많았는데, 격리수준 낮추는 것 외에 다른 선택지가 마땅치 않았다.
가상계좌 은행명 누락 - 머지 후 충돌 잔해
dev 브랜치 머지 이후에 _kpBankNameByCd와 _kpBankNameByCode 두 헬퍼가 공존하는 상태가 생겼다. 제주은행 폴백이 포함된 dev 쪽 버전으로 일원화하고 중복 제거했다. 그 과정에서 가상계좌 발급 응답에 은행명과 확정 계좌번호가 누락되어 충전 화면에 '확인 필요'가 뜨는 버그가 같이 발견됐다. 충전 화면 렌더링하는 쪽에서 응답 필드 매핑이 안 된 것이었고, utl 레이어 payment 쪽도 손봤다.
오늘 전체를 돌아보면
커밋 분포를 보면 민원 문서 쪽에 압도적으로 집중되어 있다. feat이 많지만 fix가 그 사이사이에 계속 끼어 있는 패턴이 사실 좀 걸린다. 잘 만들면서 동시에 고치고 있다는 건 설계 단계에서 안 보이던 게 구현 중에 계속 드러났다는 뜻이기도 하다.
그래도 ZIP 하나 딸깍했을 때 공문, 내용증명, 영수증 낱장, 이의제기신청서, 메일문안, 전체인쇄 합본이 전부 나오는 구조가 오늘 하루 만에 완성된 건 솔직히 꽤 만족스럽다. 어제 수기로 서류 끼워 맞추던 걸 생각하면 거리가 크다.
AI 법령 환각은 계속 주시해야 한다. 프롬프트에 제약 걸었다고 100%는 아니다. 공문 나가기 전에 법령 조항은 여전히 사람이 한 번 확인해야 하는 항목이다.
PSY 자동 테스트가 오늘도 세 편 나갔다. friend-shopping-type, highway-rest-stop-type, midnight-aquarium-terror. 자동 파이프라인이라 신경 쓸 일은 없는데 커밋 목록에서 볼 때마다 이 프로젝트랑 민원 프로젝트가 같은 git 타임라인에 공존한다는 게 묘하다.
내일은 오늘 올린 것들 실측 한 번 더 돌리는 게 먼저다. 특히 전체인쇄.pdf 페이지 순서와 문서번호 채번이 실 데이터에서 깨지는 케이스가 없는지.
댓글 0
첫 댓글 달아줘.