일기 slecs

수수료 테이블 뒤집고 앱 올리고, 하루가 두 개였다

목차

오늘을 한 줄로 정리하면 그렇다. 정산 쪽 DB를 두 번 갈아엎고, 모바일 앱은 비공개 테스트 빌드를 두 번 올렸다. 두 프로젝트가 같은 날 동시에 터지는 건 언제나 환영받지 못한 조합이다.


부가세 표기가 두 번 뒤집혔다

언제부터 시작됐는지 기억도 흐릿하다. 비비·순하리 쪽 정산 작업이 아침부터 이어졌다. 총판 충전·결제 요율을 0.51로 내리는 건 이미 협의가 끝난 사안이었고, SQL 한 방이면 될 줄 알았다. 그런데 등급 가드가 걸렸다. 시스템이 요율을 저장할 때 최저 등급 기준보다 낮은 값은 거부하도록 돼 있는데, 0.51이 그 아래였다. 그래서 DB에 직접 반영했고, 우회 사실을 커밋 메시지에 명시했다.

가드가 존재하는 이유가 있고, 그 가드를 정식으로 건너뛰는 경우도 사업 협의 결과로 발생한다. 나쁜 선택이 아니다. 문제는 이게 끝이 아니었다는 것.

하위 대리점 9곳이 있었다. 요율은 0.66이고 부가세는 별도 표기. 처음 SQL을 짤 때 어떤 경위에서인지 INCLUDED(포함) 표기로 들어갔다. 틀린 것이었고, 정정 커밋이 하나 더 생겼다. 그러니까 오늘 비비·순하리 하위 대리점 관련 커밋이 두 개다. 별도 표기로 전환한 기록, 그리고 INCLUDED로 잘못 들어간 걸 다시 별도로 복귀시킨 기록.

같은 파일에 커밋이 두 개 붙는 건 썩 유쾌하지 않다. 실수라기보다는 절차상 오기다. 운영 DB에서 수작업으로 반영하다 보면 이런 게 생긴다. 기록은 남겨야 하고, 남긴 기록이 틀렸으면 정정 기록도 남겨야 한다. 그냥 덮어쓰는 게 속은 편하지만 그게 더 나쁜 선택이라는 걸 이미 알고 있다.

플랫폼 floor 값도 오늘 0.385로 올라갔다. 부가세 표기 정책도 오늘 확정됐다. 대행사·하위는 별도 표기. 이게 정리되면서 정산 도메인 규칙 문서도 업데이트했다. .claude/rules/settlement-domain.md - 이 파일이 점점 두꺼워지고 있다. 정책이 바뀔 때마다 쌓인다. 나중에 이 파일을 열면 오늘 무슨 일이 있었는지 다 나올 것이다.


등급 가드를 코드로도 박았다

SQL 작업과 별개로, 코드 레벨에서도 오늘 중요한 것이 들어갔다. fee_standard_json 저장 가드. 수수료 등급 기준 JSON을 저장할 때 특정 sysId를 강제하도록 관리자 팝업에 막아둔 것이다.

관리자 UI에서 팝업을 열어 설정을 바꿀 때, 어떤 시스템 ID 컨텍스트에서 저장하는지를 명시적으로 고정해야 한다. 그렇지 않으면 A 시스템 설정을 보다가 B 시스템 값으로 저장하는 사고가 생길 수 있다. 실제로 발생했거나 발생 가능성을 감지했으니 이 가드를 넣었을 것이다. commission/web, partner/web 두 군데, 유틸 클래스 두 개. 한 줄짜리 수정이 아니라 여러 레이어를 건드린 작업이었다.


PG 거절 문구가 코드에 박혀 있었다

결제 쪽 작업도 오늘 꽤 들어갔다. kp-pay에서 특정 사용자를 거절할 때 나오는 문구가 코드에 하드코딩되어 있었다. '등록하실 수 없는 고객'이라는 표현인데, 이걸 그대로 사용자에게 노출하는 게 문제였다. 발급 불가 안내 문구로 교체했다.

그다음은 계좌인증 거부 안내. 고객센터 연락처 문구를 빼고 본인정보 확인 안내로 교체했다. 안내에 고객센터 문구가 들어가면 실제로 연락이 온다. 그 볼륨을 줄이고 싶었던 것이 맞다.

그리고 PG 거절 사용자 문구 전체를 DB로 이관했다. config_json.pgUserMessages 필드에 넣는 방식. 이게 오늘 결제 쪽 작업 중 체급이 있는 것이다. 이전까지 코드 안에 박혀 있던 문자열들이 이제 DB에서 관리된다. 배포 없이 문구를 바꿀 수 있게 된다는 게 실질적인 이득이다. PG사 정책이나 운영 상황에 따라 문구를 즉각 수정해야 할 때, 매번 빌드·배포 사이클을 타지 않아도 된다. 운영 DB 실행 완료 마킹도 같은 날 커밋에 들어갔다. SQL 실행 후 해당 파일에 완료 표시를 남기는 것. 사소해 보이지만 이 습관이 나중에 "이거 반영됐어?"라는 질문을 막는다.


kp-pay가 새벽에 느리다

타임아웃 확장이 하나 있었다. kp-pay read timeout을 00:30~01:30 KST 구간에만 50초로 늘리고, 기본값은 20초 유지. 새벽 0시 반에서 1시 반 사이에 PG사 응답이 늦어진다는 데이터가 있었을 것이다. 그 시간대에만 한시적으로 타임아웃을 풀어주는 방식.

전체를 50초로 늘리지 않고 시간대 조건을 건 게 합리적인 판단이다. 타임아웃이 길면 실패 케이스에서 사용자가 기다리는 시간도 그만큼 늘어난다.


하단 탭바가 태블릿에서 사라졌다

CSS 작업이었다. 폴드폰을 펼치거나 태블릿처럼 768px 이상 폭이 되면 앱 하단 탭바, 그러니까 페이 버튼이 화면에서 사라지는 버그. SCSS 소스와 컴파일된 CSS 파일 두 곳을 모두 건드렸고, 우측 여백도 같이 수정됐다.

이 종류의 버그가 특히 짜증스러운 이유는 일반 스마트폰에서는 멀쩡한데 특정 폼팩터에서만 터지기 때문이다. 재현하려면 DevTools에서 뷰포트를 바꿔가며 확인해야 하고, 768px 분기점이 기존 미디어 쿼리와 충돌했거나 아예 빠져 있었을 것이다. 발견했으니 다행이지, 놓쳤으면 폴드 유저들이 결제 버튼을 못 누르는 사태가 났을 것이다.

예치금 송금 화면에서 하위 열람 뷰에 송금·이관 폼이 노출되던 것도 오늘 잡았다. 하위 계정이 화면을 볼 수는 있지만 실제 송금이나 이관을 실행할 수는 없어야 하는데, 폼 자체가 보이고 있었다. JSP 파일 수정으로 숨김 처리.


여기까지가 정산·결제 쪽 하루였다. 그리고 동시에 모바일 앱 프로젝트에서도 오늘이 꽤 바빴다.


앱 비공개 테스트 준비, 빌드를 두 번 올렸다

Flutter 프로젝트 쪽. Play 비공개(alpha) 테스트 트랙에 빌드를 올리는 게 오늘 모바일 쪽 메인이었다.

ADS_TEST dart-define 플래그가 오늘 들어갔다. 릴리즈 빌드인데 테스트 광고를 보여줘야 하는 상황 때문이다. 비공개 테스트 단계에서 실광고를 붙이면 테스터들의 클릭이 수익화 지표를 오염시킨다. 그래서 릴리즈 빌드임에도 dart-define으로 플래그를 넘겨서 테스트 광고를 강제하는 방식을 택했고, cas_backend.dart에 이 조건 분기가 들어갔다. ads_controller.dart에도 플래그 처리가 추가됐다.

1.1.0+7 빌드와 1.1.0+10 빌드, 두 번이나 올라갔다. CLAUDE.md에 Play alpha 트랙 배정 상태가 세 번 갱신됐다는 게 그 과정을 보여준다. 빌드 올리고 상태 업데이트하고, 뭔가 문제가 발견되고, 고치고, 다시 빌드. 이 사이클이 반복됐다.

OneDrive 인증서 경로 이동 픽스도 있었다. play_api.py에서 인증서 파일 경로를 참조하는 부분인데, 개인 계정과 헤드비온 계정 두 경로가 달라져서 반영했다. 스크립트가 인증서를 못 찾으면 Play 업로드 자체가 실패한다. 이게 막혀 있었으면 빌드 올리는 시도 자체가 죽었을 것이다.


없는 기능이 paywall에 적혀 있었다

iCloud/Drive 백업 문구가 paywall 화면에 있었는데, 아직 배포가 안 된 기능이었다. 지워버렸다. lib/l10n/s.dart, lib/ui/paywall_screen.dart, pubspec.yaml, store/play_upload.py, test/widget_test.dart까지 손이 간 걸 보면 이 문구가 여러 곳에 참조되어 있었다.

배포 안 된 기능을 마케팅 문구에 올리면 심사 거절 사유가 된다. Apple 5.6 리뷰 관련 복구 리포트가 오늘 문서화됐고, 30앱 QA 리포트도 같이 정리됐다. 그 교훈이 이쪽에도 작용한 것이다. 론치 체크리스트에 "대량 생산 방지" 규칙을 추가하는 문서 작업도 들어갔다. docs/mobile-app-launch.md에 5.6 관련 항목이 추가됐다.

W3 론치 게이트와 관련한 paywall history cap + restore feedback 픽스도 있었다. 독일어, 영어, 스페인어, 프랑스어, 이탈리아어 ARB 파일 다섯 개가 같이 수정됐다.


7개 로케일, 양궁 용어까지

스토어 리스팅도 손을 탔다. Play 스토어에 올라가는 tallup 키워드를 90-100자로 트리밍하고 제목·부제목과 겹치는 단어를 제거했다. 키워드 필드에 제목에 이미 있는 단어를 넣으면 Google이 중복 처리하면서 오히려 손해가 된다. 이 최적화는 눈에 잘 안 보이는 작업이지만 검색 노출에는 실제로 영향을 준다.

iOS 권한 요청 문자열 현지화도 오늘 들어갔다. 일본어, 스페인어, 포르투갈어, 인도네시아어, 힌디어, 아랍어. InfoPlist.strings 파일 6개. 카메라나 위치 정보 접근 시 iOS가 보여주는 권한 요청 문구다. 현지화가 누락되거나 엉뚱한 언어로 나오면 현지 심사에서 거절 사유가 된다.

이탈리아어 문법 교정도 있었다. 양궁 관련 용어 7개 로케일 교정이었는데, 이탈리아어 쪽에 문법 오류가 발견됐다. app_it.arbapp_localizations_it.dart 두 파일을 동시에 수정했다. 이 두 파일이 이중으로 관리되는 게 번거롭다. arb가 소스고 dart 파일이 생성 결과물인데, 자동 생성이 안 됐거나 수동으로 맞춰야 하는 상황이었을 것이다.


오늘도 심리 테스트 3편

매일 자동 생성되는 심리 테스트 콘텐츠. 오늘도 3개가 들어갔다. ghost-village-navi, procrastination-type, soul-instrument. 커버 이미지 webp 3개, JSON 3개. 자동화되어 있어서 사람 손이 거의 안 들어가지만 커밋 기록에는 남는다. 매일 이게 쌓이는 게 착실하다는 느낌도 주고, 동시에 오늘 진짜 손으로 한 게 뭔지를 희석시키는 것 같기도 하다.


돌아보면 오늘 작업 갈래가 다섯 개 이상이었다. 정산 도메인 SQL 정정, 결제 문구 DB 이관, 모바일 앱 알파 빌드, 스토어 리스팅 최적화, 다국어 현지화. 이게 한 팀에서 동시에 돌아갔다.

오늘 인상에 남는 건 SQL 정정 커밋이다. 부가세 INCLUDED/별도 표기를 한 번 썼다가 틀려서 정정 커밋을 쓰는 것. 운영 DB 작업이라 롤백도 쉽지 않으니 기록을 남기는 게 최선의 감사 수단이다. 이 흔적들이 나중에 "왜 이렇게 됐지?"라는 질문에 답해준다. 그래서 SQL 파일도 버전 관리에 넣고, 완료 마킹도 파일에 남기고, 커밋 메시지에 사유를 붙인다. 귀찮아 보이지만 이게 없으면 운영은 기억에 의존해야 한다.

kp-pay 타임아웃에 시간대 조건을 건 것도 같은 맥락이다. 왜 00:30~01:30인지, 왜 50초인지. 커밋 메시지에 담겼고 코드에 남았다. 한 달 뒤 이 코드를 보는 사람이 물어볼 것들을 미리 적어둔 셈이다.

댓글 0

첫 댓글 달아줘.