출금가능액 공식 다섯 군데, 전부 달랐다
목차
아침에 코드를 열자마자 대시보드 KPI 카드가 눈에 밟혔다. 파트너 포탈에서 페이지를 처음 로드하면 출금가능 잔액 카드가 폴링 시작 전 초기값으로 총잔액을 뿌리고 있었다. 그 짧은 순간, 파트너 눈엔 "내 돈이 이만큼"이 찍힌다. 실제로 출금 가능한 금액이 아닌 수치를 보고 출금 신청을 넣으면 그때부터는 단순 CS가 아니라 분쟁이 된다. 돈이 엮인 화면에서 초기값 오류는 신뢰 문제로 바로 직결되기 때문에, 작은 버그처럼 보여도 그냥 넘길 수 없었다.
교정 자체는 단순했다. 초기값에 올바른 출금가능액을 넣어주면 그만이다. 근데 그 수정을 하면서 SQL 매퍼를 열었고, 거기서 제대로 터졌다.
파트너 대시보드 출금가능 쿼리가 지급보류(hold)와 예치금(deposit)을 아예 차감하지 않고 있었다. 출금 가드 백엔드 로직은 balance - hold - deposit으로 계산하는데, KPI 카드에 띄워주는 수치는 그냥 balance에 가까운 어딘가였다. 파트너가 화면에서 보는 숫자와 실제로 출금 버튼을 눌렀을 때 통과되는 금액이 달랐던 것이다.
이 지점에서 멈추고 코드베이스를 전수로 훑기 시작했다. 출금가능액을 계산하는 로직이 어디 어디 흩어져 있나. 결과가 다섯 군데였고, 다섯 군데가 다 미묘하게 달랐다. 파트너 상세 화면, 파트너 포탈 정산 화면, 대시보드 KPI, 출금 가드 백엔드, SQL 집계 쿼리. 처음 만들 때는 하나의 기준이었을 텐데 기능이 붙을 때마다 한 군데씩 수정이 쌓이면서 언젠가부터 각자 다른 길을 걷고 있었다. 오늘 하루 중 가장 긴 시간을 여기서 썼다.
balance - hold - deposit, 하나로 못 박기
공식을 통일하는 건 그냥 숫자를 바꾸는 게 아니었다. 어떤 화면에서는 deposit을 별도 카드로 빼서 보여주는 UI 구조가 있었는데, 이게 파트너 입장에서 오해를 불렀다. "예치금이라는 게 따로 내가 맡겨둔 돈이 있는 건가?"라는 혼란. 파트너 상세 화면에서 예치담보 카드를 잔액/예치금 통합 카드로 합쳐버렸다. 중복 항목 제거, 라벨 명확화. 이건 기능 변경이 아니라 판단의 영역이다. 숫자가 맞아도 화면 구조가 오해를 유발하면 결과적으로 틀린 거나 마찬가지다.
SQL 매퍼 수정으로 쿼리에서 hold와 deposit을 빼게 고치고, JSP 두 군데(파트너 상세, 포탈 정산 화면)에서 공식을 일치시켰다. settlement-domain.md에도 이 공식을 명시적으로 기록했다. 나중에 누군가 다른 화면을 만들다가 임의로 공식을 달리 쓰는 걸 방지하려면, 어딘가에 "이게 기준이다"라고 적혀 있어야 한다.
여기서 자연스럽게 연결된 게 예치 담보 자동홀드 기능이었다. 출금가능액 공식에서 deposit을 안전하게 차감하려면 예치담보 요건이 실제로 맞게 유지되는 구조가 필요하다. 전일 판매 × 비율을 계산해서 예치담보가 부족한 파트너는 판매대금 일부를 자동으로 홀드 거는 룰 기반 배치를 오늘 달았다. BalanceChangeType.java에 새 타입 추가, 배치 Job에 홀드 로직 주입, API 레퍼런스 문서 갱신까지 한 세트였다. 아침에 대시보드 초기값 버그 하나 잡으러 들어간 게 여기까지 연결될 줄은 몰랐다. 이런 날이 있다.
출금 차단 시간대(23:30~09:00) 적용도 오늘 완성됐다. 파트너 출금 차단 창은 이미 있었는데 회원 출금에는 적용이 빠져 있었다. 파트너는 막는데 회원은 새벽에 출금 신청이 들어와도 처리되는 구조였다. 막았다. withdraw-check 판정 로직에서 제출 경로가 sysId를 참조하지 않는 버그도 같이 잡았다. 멀티테넌트 환경에서 엉뚱한 테넌트 기준으로 차단 창을 판정하게 되는 문제다. 돈이 엮인 로직에서 테넌트 정합이 어긋나면 언젠가 반드시 터진다.
수수료 floor도 오늘 바꿨다. 종합 수수료 0.6%에서 0.66%로, 충전·결제 각각 0.30에서 0.33으로 인상. 정책 변경이라 건드린 파일이 많다. Java 커미션 클래스, 상세/폼 JSP, 파트너 커미션 개요 화면, 파트너 신청 폼. 이런 변경이 제일 지루한 이유는 로직이 없어서다. 빠진 데 없이 전부 찾아서 바꾸는 것뿐이다.
충전권 감사 후속, 이중지급 구멍 3건
며칠 전 충전권 신모델로 전환하면서 전수감사를 돌렸다. 오늘은 그 감사에서 발견된 이중지급 가능 경로 3건을 막는 작업이었다. Java 처리 클래스, 유틸리티, SQL 매퍼 세 레이어 모두 손을 댔다. 배치 쿼리 매퍼, 쿠폰 쿼리 매퍼까지.
코드를 보면서 "이게 왜 지금까지 안 터졌지?"라는 생각이 드는 구멍들이었다. 아마 실제 이중지급이 발생하지 않았던 건 트래픽이 아직 그 경로를 충분히 안 탔기 때문일 것이다. 라이브 물량이 제대로 붙기 전에 감사로 찾아낸 건 다행이지만, 구멍이 있었다는 사실 자체가 남는다. 뿌듯하기보다 찝찝한 마무리에 가깝다. 다음에 신모델 전환 같은 작업을 할 때는 감사를 더 촘촘하게 돌려야겠다.
기존에 발생했을 수 있는 부분은 제주은행 라이브 기준선을 0원으로 상계하는 SQL로 처리했다(CORRECTION_20260716_live_baseline_zero.sql). 이 SQL을 settlement-domain.md에 기록으로 남기는 게 왜 중요하냐면, 나중에 이 시점 이전 데이터를 보는 누군가가 "이게 왜 이렇게 생겼지?"라고 물었을 때 근거가 되어야 하기 때문이다. 기록이 없으면 버그인지 의도인지 아무도 모른다.
파트너 시스템, 오늘 하루 만에 이만큼
파트너 도메인에서도 꽤 많이 움직였다. 체계적으로 계획한 게 아니라 하나씩 연결되면서 늘어난 쪽에 가깝다.
가장 덩치 있는 작업은 발송포탈(SENDER) 동시 생성이다. 기존에는 파트너를 만들고 나서 발송포탈 계정을 따로 생성해야 했다. 수동 단계가 하나 있었다는 뜻이다. 이제 위저드와 신청 승인 경로 모두에서 파트너 생성과 동시에 SENDER 계정이 만들어지고 포탈 링크가 자동으로 매칭된다. 폼에 발송 계정 ID 중복 확인 버튼도 붙였다. 승인이나 강제승인 성공 시 발송 계정 초기 비밀번호를 모달로 보여주는 것도 같이 들어갔다. 작업 목록으로 보면 세 커밋(feat: 파트너 생성 시 발송포탈 동시 생성, feat: 발송포탈 동시생성 확장, fix: 승인 성공 시 초기비밀번호 모달)에 걸쳐 점진적으로 완성됐다. 운영하는 사람 입장에서 파트너 하나 올릴 때마다 발송포탈 따로 만들고 연결하던 단계가 사라지는 게 얼마나 큰지, 직접 운영해본 사람이라면 안다.
계약서 승인 권한 게이트도 달았다. contract_approve_yn 플래그를 관리자가 파트너에게 부여하는 구조다. SQL 매퍼 수정, Java 두 클래스, 파트너 목록 JSP, 하위파트너 계약서 JSP까지 건드렸다. 하위파트너 승인 시 신청자 입력 코드가 초기화되는 버그도 같이 잡았다. 승인 프로세스를 타면서 폼 데이터를 덮어쓰는 타이밍 문제였다.
파트너 목록 화면은 오늘 두 번 건드렸다. 먼저 계약승인 컬럼을 추가하고 나서 테이블이 잘리는 현상 - 폭 다이어트. 그리고 계층 뷰 개선. 매칭 파트너와 발송포탈 간 계층 관계를 목록에서 시각적으로 표현하고, 잠금 파트너는 기본 숨김에 토글로 노출 가능하게, 트리 접기도 붙였다. SQL 쿼리 매퍼도 수정이 필요했다. 두 커밋에 걸쳐 단계적으로 들어간 이유는 처음 커밋으로 잠금 숨김과 계층 표기를 올리고, 두 번째 커밋에서 접기와 발송포탈 하위 표시를 보강했기 때문이다.
딥링크 쪽도 상당히 손댔다. 파트너 레벨에 따라 회원가입(SIGNUP) 딥링크를 노출·생성할 수 있는 파트너와 없는 파트너를 구분하는 게이팅 로직이 들어갔다. 딥링크 목록에 상태 탭도 생겼다. 유틸리티 두 개, 쿼리 매퍼, 목록 JSP. 그리고 회원 보유 불가 레벨의 SIGNUP 딥링크를 일괄 비활성화하는 마이그레이션 SQL이 오늘 날짜로 실행됐다. 딥링크 상세 화면에서 SIGNUP 노출 가드를 보강하고 검색폼 위임 바인딩도 정리했다. 레벨 게이팅이 들어가면 기존 데이터를 소급 처리하는 SQL이 세트로 따라온다. 빠뜨리면 나중에 데이터 정합이 맞지 않는다.
전체요약 화면 순수익 라이브 갱신 버그도 잡았다. 관리자 소모 매출이 집계에서 빠지고 있었고, 출금 팝업 수수료 흐름 라벨이 모호한 것도 명확하게 정리했다.
티켓타카 오픈 준비, 피처 플래그로 전환
티켓타카 관련해서 두 커밋이 들어갔다.
먼저 기존에 prod 프로필 체크로 막던 오픈 게이트를 DB 토글(TICKETTAKA Feature 플래그)로 전환했다. 코드에 프로필 체크를 박아두면 켜고 끄려면 배포가 필요하다. DB 피처 플래그로 가면 배포 없이 실시간으로 제어할 수 있다. 마이그레이션 SQL도 같이 들어갔다. 이 전환 자체는 refactor라 기능 변화는 없고 제어 방식만 바뀐 것인데, 운영 유연성이 달라진다.
그리고 운영 오픈 전 커밍순 게이트를 달았다. 페이지 접근, 디자인 노출, 전환 API를 커밍순 화면으로 차단하는 구조다. 메인 레이아웃, 서브 레이아웃, 커밍순 전용 JSP까지 세 파일. 아직 오픈 전이라 사용자가 접근하면 커밍순 화면으로 빠지는데, 피처 플래그 하나만 켜면 즉시 라이브가 되도록 구조를 잡아뒀다. 오픈 타이밍에 배포를 다시 하지 않아도 된다. 내일 오픈 타이밍 확인이 첫 번째가 될 것 같다.
앱스토어 리젝 대응 - 라이트모드부터 409 함정까지
플랫폼 백엔드와 병렬로 앱 심사 대응도 오늘 진행됐다.
Zoothe 1.0(5) 빌드가 두 항목으로 리젝됐다. G4(라이트모드 배경 문제)와 2.3.7(free 관련 문구). lib/main.dart에서 라이트모드일 때 배경이 검정인 위에 짙은 글자가 올라가는 상태였다. 스크린샷에서 바로 티가 나는 수준이었으니 심사자가 G4로 거절한 게 당연했다. 수정하고, frame_screenshots.py에서 price 캡션도 제거했다. pubspec.yaml 버전을 올려서 1.0.0+6으로 빌드6 재제출.
스크린샷 4번도 교체했다. 설정 화면이 노출되는 게 심사 맥락에서 부적절했던 것 같다. 고양이 플레이어 화면으로 바꿨다. screenshots_harness.dart와 빌드 쉘 스크립트에서 캡처 출력 경로도 환경변수화했다. 하드코딩된 경로는 환경 바뀔 때마다 손봐야 하는 기술 부채다.
리젝 후 재제출할 때 409가 나는 함정이 있다. reviewSubmissionItems를 resolved: true로 전환해줘야 재제출이 받아지는 구조인데, 이걸 모르면 재제출 시도가 계속 막히고 이유도 직관적이지 않다. Zoothe에서 직접 겪은 케이스를 docs/mobile-app-launch.md에 남겼다. 이 문서가 없으면 다음 앱을 낼 때 또 같은 함정에 빠진다.
Flutter 스캐폴딩도 세 건이 오늘 들어갔다. Resono는 Zoothe 베이스에서 패키지 ID com.hedvion.resono로 리네임하고 콘솔 ID를 중립화했다. holdon과 SnoreWise는 기본 스캐폴드 커밋이다. 커밋 메시지가 전부 chore라 가벼워 보이지만 초기 셋업이 꼬이면 나중에 고치는 게 훨씬 번거롭다. 패키지 ID, 콘솔 ID 중립화, CLAUDE.md 초기화, .fvmrc 설정까지 한 세트로 잘 잡아두는 게 맞다.
오늘이 남긴 것
커밋 수가 서른을 넘겼다. 계획적인 결과가 아니었다. 하나를 고치면 옆에 붙은 게 보이고, 그걸 건드리면 또 다른 게 보이는 날이었다.
출금가능액 공식 통일이 특히 그 전형이었다. 대시보드 초기값 버그 하나 고치러 들어갔다가 공식이 다섯 군데서 다 달랐던 걸 발견하고, SQL 매퍼 수정으로 이어지고, 예치담보 자동홀드 배치까지 연결됐다. 정산 도메인에서 이런 발산이 생기는 건 피하기 어렵다. 처음엔 단순했던 공식이 기능이 붙을 때마다 화면 하나씩 수정이 쌓이면서 어느 시점부터 제각각이 된다. 오늘 전수 통일로 balance - hold - deposit을 단일 공식으로 코드베이스 전체에 못 박았다. settlement-domain 룰에 기록해둔 것도 같은 이유다. 결정이 흔들리지 않으려면 어딘가에 명시적으로 남아 있어야 한다.
파트너 발송포탈 동시 생성은 오늘 제일 뿌듯한 쪽이었다. 기능 자체보다 흐름이 매끄러워지는 쪽이 실제 운영에서 더 중요할 때가 있다. 충전권 이중지급 구멍 3건은 잡은 것 자체는 다행이지만 구멍이 있었다는 사실은 남는다. 다음 신모델 전환 때는 감사를 더 촘촘하게 가야겠다는 반성이다.
psy 자동 생성 테스트 세 건은 배치가 돌아서 올라온 것들이다(family-cooking-role, midnight-beach-waterghost, youtube-speed-type). 별도 시스템이 커버하는 영역이다.
피처 플래그는 다 준비됐다. 티켓타카 오픈 타이밍만 남았다.
댓글 0
첫 댓글 달아줘.