정산 구멍 메우고, 앱 하나 더 세상에 내보낸 날
목차
오늘 커밋 수를 세다가 잠깐 멈췄다. 서른 개가 넘는다. 그것도 서로 아무 관련 없는 네다섯 개 프로젝트에 걸쳐서. 혼자 오케스트라를 지휘하는 기분이라고 해야 할까. 어느 파트가 무너지면 다른 데 불이 붙는 구조. 오늘은 그 불을 전부 끄면서 동시에 새 악기를 한 대 더 무대에 올린 날이다.
가장 먼저 손댄 건 정산이었다
아침에 제일 먼저 열어본 게 audit-history 괴리 파일이었다. STEP 3, -104,686. 숫자 자체보다 그 뒤에 붙은 "상계"라는 단어가 더 무겁다. 원장에 적혀야 할 금액이 적히지 않았다는 뜻이고, 그게 이미 누적된 상태라는 뜻이다. 관리자가 쿠폰을 발급할 때, 발송 포탈이 쿠폰을 발급할 때 - 마진이 발생하는 건 맞는데 그게 원장에 쌓이지 않고 있었다. SQL 한 줄 실행으로 해결되는 문제가 아니라, 왜 안 쌓였는지를 먼저 추적해야 했다.
파고들어 보니 fix(settlement) 커밋이 보여주듯 두 군데에서 마진 적립 로직이 빠져 있었다. 외부 쿠폰 발급 쪽과 유틸리티 내부 클래스 쪽. 과거에 누군가 - 아마 나겠지 - 발급 흐름을 추가하면서 원장 적립을 따라붙이지 않은 거다. 수정하고, SQL로 과거 괴리를 상계하고, .claude/rules/settlement-domain.md에 정책을 명문화했다. "ADMIN 발급 마진은 원장 미적립" 이라는 규칙이 어디에도 안 적혀 있었으니까. 다음 사람이 - 혹은 다음 달의 내가 - 같은 구멍을 내지 않으려면 규칙이 코드 주석이 아니라 도메인 규칙 파일에 있어야 한다.
출금 팝업 PG 비용 기간 창 통일도 같이 묶었다. 사소해 보이는 UI 변경인데, 사실 이건 "어느 기간 기준으로 수수료를 보여줄 것인가"라는 정책 문제다. 화면 두 군데가 서로 다른 기간 창을 쓰고 있으면 운영팀이 헷갈린다. 통일.
그다음이 이중과금이었다. 충전 계좌 인증할 때 1원이 두 번 나가는 버그. kp 가상계좌 1원인증 키 라우팅이 잘못돼서 생긴 문제였는데, 이건 진짜 조용히 터질 뻔한 거다. 소액이라 그냥 지나쳤을 수도 있는데, 인증 흐름 들여다보다가 발견했다. 이중과금은 어떤 금액이든 나가서는 안 된다. 수정하고 나서도 한동안 찝찝했다. 얼마나 오래 이 상태였던 걸까.
미입금 가상계좌 선제 해지 금지 정책 주석도 이 흐름에서 나왔다. vactRegcerti 사후처리 - kp-pay가 거절했을 때만 해지해야 하는데, 실수로 앞에서 해지해버리면 미입금 고객이 나중에 입금해도 처리가 안 된다. 이건 주석 한 줄이지만, 운영 사고를 막는 주석이다.
BM 개편은 오늘의 메인 디쉬
정산 버그들을 수습하고 나서 본격적으로 BM 개편에 들어갔다. 판매 포탈과 발송 포탈을 1:1로 매칭하는 구조. 원래는 하나의 파트너가 양쪽을 다 가질 수 있는 구조였는데, 그게 정산 복잡도를 기하급수적으로 올리고 있었다.
플랜 문서를 두 번 썼다. PLAN_BM_PORTAL_SPLIT.md 초안을 쓰고, 오후에 다시 한 번 보강했다. 포탈 구성 3종 - 발송만, 판매만, 둘 다 - 으로 나누고 매칭은 선택(0..1)으로 가는 게 맞다. 파트너가 발송 포탈만 운영하면 판매 포탈 없이도 돌아가야 하고, 반대도 마찬가지. 억지로 1:1을 강제하면 온보딩 유연성이 죽는다. 이 판단을 내리는 데 플랜 문서를 두 번 쓰는 과정이 필요했다. 첫 번째 초안 쓰고 나서 "아, 이건 매칭을 선택으로 풀어야 하는데" 싶었다.
예치금 송금, PINCODE 출금 허용, 출금수수료 주체별 집계까지 실제 코드에 녹였다. BalanceChangeType.java에 타입이 추가됐고, 파트너 포탈 컨트롤러와 시스템 웹 레이어, kp-pay 출금 레일 전환까지. PartnerPortalControllerAdvice.java도 건드렸다. HTML 보고서도 하나 뽑았다. 이건 내가 보려고 만든 게 반, 나중에 QA나 운영팀에 설명할 때 쓰려고 만든 게 반이다.
BM 개편이 무서운 이유는 수치 하나 잘못 건드리면 실제 돈이 잘못 흐르기 때문이다. 코드 머릿속에서 다섯 번 돌리고, 문서로 정리하고, 그다음에 손을 댄다. 오늘도 그렇게 했다.
Quit Sugar: 초기화면부터 TestFlight까지 하루
이게 오늘 가장 황당한 파트다. 아침에 feat: initial Quit Sugar app 커밋이 있고, 저녁에 feat(ios): manual-signed App Store IPA built+uploaded to TestFlight (v1.0.0+1) 커밋이 있다. 하루 만에 앱을 만들어서 TestFlight에 올렸다.
설탕 끊기 스트릭 트래커. 갈망 로그, 금단 증상 타임라인. 심플한 개념인데 그 심플함을 유지하는 게 생각보다 어렵다. 처음에 UI가 너무 bloomly 같아서 한 번 뒤집었다. style: distinct clean-visual identity (not a bloomly clone) 커밋이 그 증거다. 아이콘 이미지를 전 해상도(hdpi/mdpi/xhdpi/xxhdpi/xxxhdpi)로 다시 구웠다.
Fraunces 서체를 도입한 것도 오늘이다. 세리프 디스플레이 폰트, 장부 레이아웃. 헬스케어 앱인데 왜 세리프냐고 할 수 있는데, 오히려 그게 차별점이다. 다들 산세리프에 네온 그린인데, 이건 오래된 다이어리 같은 느낌으로 가자. 폰트 파일이 assets/fonts/에 들어가고, Podfile.lock까지 갱신됐다.
인터스티셜 광고 빈도 조정, 통계/타임라인 화면 에디토리얼 톤 통일도 같이 했다. 광고 빈도는 처음부터 너무 공격적으로 세팅하면 리뷰어한테 걸릴 수 있다. 보수적으로 시작.
AdMob 실ID 6종 반영하고 vc2 재빌드. 스토어 listing 8개 로케일, 스크린샷 하네스, 마케팅 프레임, 그래픽. store/ 폴더 아래 스크립트가 한 세트 생겼다 - asc.py, asc_appinfo.py, asc_iap.py, asc_listing.py, asc_ss_upload.py, play_graphics.py. 이 스크립트들은 이제 Quit Sugar뿐 아니라 다음 앱에도 쓸 수 있다. 처음 만드는 데 시간이 걸려도 두 번째부터는 거의 공짜다.
privacy/terms ko+en SQL 시드도 심었다. store/seed-legal-cms.sql - 이걸 admin_db에 실제 적용까지 했다. 심사 제출 전에 법적 문서가 없으면 바로 리젝이다.
iOS 수동 서명은 항상 긴장된다. ExportOptions.plist, project.pbxproj 건드리고, 프로파일 맞춰서 빌드. TestFlight에 올라간 것 확인할 때까지 숨을 참는다. 올라갔다.
PainTap iOS 심사 준비: 위젯 큐 방식으로 전환
Quit Sugar 만들면서 PainTap도 병행했다. iOS 위젯 문제가 컸다. 기존에는 엔진을 경유해서 기록을 넘겼는데 그 사이에 데이터가 유실되는 케이스가 있었다. 오늘 순수 Swift 큐 방식으로 갈아탔다. 위젯이 직접 AppGroup UserDefaults에 쌓고, 앱이 열릴 때 드레인하는 구조.
PainTapLogIntent.swift, PainTapWidgetStrings.swift, PainTapWidget.swift 전부 손댔다. Arm/Confirm/Cancel 인텐트 플로우도 추가했다 - 위젯에서 버튼 누르면 "정말 기록할까요?" 확인 받고, ✓ 기록됨 피드백 주는 흐름. 이게 없으면 위젯이 뭔가 했는지 안 했는지 사용자가 모른다.
8개국어 로컬라이즈는 de/en/es/fr/ja 이상으로. InfoPlist.strings와 Localizable.strings를 각 로케일 폴더에. ATT 문구도 각 언어로 따로 썼다. 애플 리뷰어가 ATT 팝업 문구를 꼼꼼히 본다. versionCode 3으로 범프하고 i18n 픽스 포함해서 재제출.
스토어 스크린샷은 android/ko, android/de 폴더에 01~05번 이미지가 들어갔다. store_assets/에는 feature_graphic도. make_captions.py로 문구 밴드를 합성하는 스크립트도 만들었다. 스샷 160장... 은 아니지만 여러 해상도, 여러 로케일 곱하면 금방 수십 장이다.
MoneyLeaf: DailyBible 오명 사건
짧지만 황당한 버그. fastlane Fastfile에서 IPA output_name이 DailyBible로 박혀 있었다. MoneyLeaf 프로젝트에서. 아마 어느 앱 Fastfile 복사해서 쓰다가 이름만 안 바꾼 거다. 그래서 빌드는 되는데 IPA 파일 이름이 다른 앱 이름으로 나오는 상황이었다. 심사 제출 전에 발견해서 다행. 프로파일도 갱신하고 1.0.2+4, build 4로 iOS 재제출. Play alpha도 vc4.
publish-progress.py에서 관리형 DB 일시 행업(2013 에러) 방어 로직도 추가했다. 3회 재시도 + read_timeout. 이게 없으면 DB가 잠깐 멈출 때 스크립트가 그냥 죽는다. 조용히 죽으면 발행 진행 상황 추적이 끊긴다.
헤드바이온 사이트: 앱 카드 8→22
hedvion 메인 사이트에 iOS 앱 카드를 10개 더 달았다. drywave, bloomly, zoothe, prayertimes, spectrum, tingle, paintap, topik, quitsugar, mossu. 썸네일도 각각 준비해서 thumbs/ 폴더에. 카운트가 8에서 22로 뛰었다. 숫자가 커지면 그만큼 관리 포인트도 늘어나는데, 일단 지금은 있는 걸 다 보여주는 게 맞다.
ops/인프라: 조용하지만 중요한 것들
codex 계정풀 재로그인과 풀동기화 운영지식을 hedvion-CLAUDE.md에 기록했다. 코인봇 codex 전용 운영 방식도. 이런 문서 작업은 당장 티가 안 나지만 세 달 후에 처음 보는 사람이 - 혹은 기억을 잃은 내가 - 봤을 때 살아있는 기록이 된다.
디스크 자동방어 - disk-guard, journald 캡, 캐시 크론 - 구성 기록도 같이 남겼다. 서버 디스크가 가득 차면 아무것도 안 된다. 그 방어 레이어를 깔았고, 나중에 왜 이게 있는지 이해할 수 있도록 기록.
보고서 HTML 파일들을 docs/reports/로 정리했다. 루트에 흩어져 있던 산출물들. .gitignore에 *.html 예외 처리도 했다. tb_sports 전수조사 보고서는 새 경로로 재추적. 이런 정리 작업은 미루면 영원히 못 한다.
psy: 오늘의 테스트 3종
chore(psy): daily auto-generated tests (2026-07-14) - destiny-compass, midnight-mirror-type, nunchi-type 세 편. 커버 이미지도 webp로 생성됐다. 이건 자동화 파이프라인이 돌린 거라 내가 직접 쓴 건 아니지만, 돌아간다는 게 중요하다. 오늘도 문제없이 생성돼서 커밋됐다.
하루를 닫으며
정신없는 날이었다. 정산 구멍 메우는 것부터 시작해서 BM 아키텍처 결정, 새 앱 하나를 TestFlight까지 올리고, 다른 앱 심사 재제출, 또 다른 앱 버그 잡고, 사이트 업데이트, 인프라 정리, 자동 생성 파이프라인 확인.
-104,686 상계는 오늘 일 중에 가장 묵직했다. 숫자에 이름이 붙어있다는 게. 누군가 쿠폰을 발급받았고, 그 마진이 원장에 안 쌓였고, 지금 상계로 기록을 맞춘다. 코드 버그가 돈의 흐름에 미치는 영향을 다시 한 번 실감했다.
Quit Sugar가 TestFlight에 올라간 건 하루 치고는 많이 간 거다. 앱 하나가 처음 빌드부터 심사 가능 상태까지 가려면 보통 며칠은 걸린다. 오늘 몰아붙인 건 설탕 끊기 앱이라는 개념이 단순해서 결정이 빨랐기 때문이다. 복잡한 앱일수록 설계에서 막힌다.
paintap 위젯 큐 방식 전환은 잘한 결정이다. 엔진 경유로 유실이 생기는 구조를 그냥 두면 "가끔 기록이 안 된다"는 리뷰가 쌓인다. 근본 구조를 바꿔야 할 때는 바꿔야 한다. 빙 돌아가는 패치보다 비용이 더 들어도.
오늘 커밋들을 훑어보면 전부 연결돼 있다. 정산을 고치면서 BM을 다시 설계하고, 앱을 만들면서 스토어 자동화 인프라를 쌓고, 그 인프라가 다음 앱에 쓰인다. 제각각인 것 같아도 방향은 하나다. 덜 허술하게, 더 자동화되게.
댓글 0
첫 댓글 달아줘.