일기 slecs

수수료 규칙 하나가 하루를 통째로 끌고 다닌 날

목차

오늘 아침 첫 번째로 잡은 게 "ADMIN 발급 충전형 쿠폰 수수료 미부과 정책 반영"이었다. 운영자가 명시적으로 관리자 권한으로 발급한 충전형 쿠폰에는 이용중계수수료를 물리지 않는다는 규칙인데, 코드가 이걸 제대로 반영하지 못하고 있었다. 규칙 자체는 명확했고 작업도 깔끔할 것 같았다.

착각이었다.

규칙 하나 고쳤더니 옆 구멍이 보였다

명시적 ADMIN 발급 충전형 미부과 로직을 코드에 박아 넣고 나니까, 바로 인접한 케이스에서 다른 문제가 보였다. 발송포탈 DISPATCH 경로로 발급된 충전권 처리 흐름에서 수수료가 두 번 나가는 케이스가 있었다. 첫 시도 후 fee=0으로 재시도를 보낼 때 그 재시도에서도 수수료를 다시 잡아가는 구조였다.

두 버그가 별개처럼 보이지만 뿌리가 같다. 수수료 부과 여부를 판단하는 로직이 여러 군데 흩어져 있고, 각자 독립적으로 조건을 보다 보니 한쪽을 막으면 다른 쪽에서 뚫린다. 부과 판단을 한 지점에서 내리도록 정리하고, 쿼리 매퍼 레벨에서도 이미 처리된 건에 대한 재부과를 막는 조건을 추가했다. settlement-domain.md에 정책 문서도 같이 업데이트했다.

이 도메인이 항상 이런 식이다. 코드만 고쳐서는 안 되고 SQL도 봐야 하고 정책 문서도 맞춰야 한다. 셋이 각자 조금씩 다른 진실을 말하고 있는 상태가 생긴다. 오늘은 세 군데를 한 사이클에 같이 돌렸다.

440원이 아니라 550원이었다

거기서 끝이면 좋았을 텐데. kp-pay 가상계좌 발급비 원가가 운영 DB에 440원으로 박혀 있었다. 실제는 500원에 VAT 10%를 더한 550원이었다. 실측 확인 후 정정.

"440→550원 정정 (500+VAT, 실측 확정)"이라고 커밋 메시지에 썼는데, 이게 오늘 제일 불편한 커밋이다. 운영 DB에 박힌 원가 기준은 정산 계산에 직접 영향을 주는 값이고, 틀린 채로 얼마나 굴러갔는지 추적하기 어렵다. 어느 시점에 440이 들어갔는지도 모른다. VAT를 빼고 잘못 입력했을 수도 있고, 계약 시점 단가가 달랐을 수도 있다. 이런 경우에는 수정하고 나서도 "얼마 동안 틀렸는가"가 뒤에 남는다.

UPDATE_20260723_kppay_va_cost_550_prod.sql로 운영 DB 업데이트하고, Java 유틸 클래스에서 하드코딩된 값도 수정했다. 문서도 정정. 같은 수치를 세 군데에서 관리하고 있다는 구조적 문제는 오늘 뜯기에 여유가 없었다. 언젠가 또 이런 상황을 만들 것 같다는 느낌이 있었지만 일단 넘겼다.

계약서류 업로드가 조용히 막혀 있었다

파트너 포탈에서 계약서류 업로드가 안 된다는 게 오늘 터졌다. 원인은 appgate의 apple pathWhitelist였다. hwp/hwpx 파일을 올리는 엔드포인트가 화이트리스트에 등록이 안 돼 있어서 게이트웨이가 조용히 막고 있었던 것이다.

이 패턴은 익숙하다. 개발 환경에서는 멀쩡하게 작동하다가 운영의 게이트웨이에 막혀 조용히 실패한다. 사용자 쪽에서 보면 그냥 안 올라가는 것이고, 에러 로그도 별로 안 남는다. UPDATE_20260723_appgate_apple_whitelist.sql에 해당 경로를 등록하고, .claude/CLAUDE.md에 사고 대응 기록을 남겼다.

그런데 이 사건이 hwp/hwpx 업로드 기능 자체의 구현 작업을 같이 끌고왔다. hwp/hwpx를 받으면 서버에서 PDF로 자동 변환하고, 변환에 실패하면 원본을 그대로 보존하는 폴백 구조였다. Dockerfile까지 손댄 게 커밋에 찍혀 있으니 변환 도구 의존성을 컨테이너에 넣는 작업도 같이 돌아갔다. 로컬 설정 파일도 바뀌었으니 로컬 개발 환경 설정도 맞췄을 것이고, JSP는 두 곳이 바뀌었다. appgate 경로 등록 하나로 시작해서 기능 구현 전체가 딸려온 흐름이다.

신분증 사본 걷어내기

계약서류 화면을 손보면서 신분증 사본 수집 기능도 같이 제거했다. 왜 제거하는지는 커밋 메시지에 나오지 않는다. 정책 결정인지, 법적 이슈인지. 어쨌든 Java 컨트롤러에서 신분증 파일 처리 로직을 걷어내고, 유틸 클래스도 정리하고, JSP 세 곳에서 UI 요소를 빼냈다.

제거 작업이 새로 추가하는 것보다 더 신경 써야 하는 이유가 있다. 제거했다고 생각한 경로가 어딘가 숨어 있으면 나중에 예상 못 한 데서 터진다. 파일 세 곳을 같이 건드린 게 보이는 걸로 봐서 꼼꼼하게 쓸었다고 믿는다.

스코프 가드 전수 적용

담당 대리점 스코프 가드 작업도 오늘 두 번에 걸쳐 처리했다. 처음에는 대시보드 재무 카드 숨김 조건이 스코프 기준에 안 맞아서 수정했고, 바로 이어서 수수료 라인 전체에 같은 스코프 가드를 전수 적용했다. 담당 목록에 포함된 파트너는 노출해야 한다는 조건이 일관되게 적용되지 않고 있었던 것이다.

admin-patterns.md를 먼저 업데이트하고 컨트롤러 코드에 반영한 순서인지, 코드 고치면서 문서를 맞춘 순서인지는 모르겠다. 어느 방향이든 코드와 패턴 문서가 같이 바뀐 게 보인다.


백엔드 작업이 여기까지다. 근데 오늘은 이쪽만 한 날이 아니었다. 전혀.

다섯 개 앱이 동시에 굴러갔다

모바일 앱이 오늘 다섯 개가 동시에 움직였다. 회고 쓰기가 제일 어려운 날 유형이다. 각각에서 무슨 일이 있었는지 정리하려면 컨텍스트 전환을 계속 해야 하고, 각각에 실제로 얼마나 집중했는지 사실 기억이 선명하지 않다.

vidpress는 오늘 굵직한 게 두 가지 들어갔다. GIF export와 trim이 Pro 기능으로 추가됐고, integration_testgif_roundtrip_test.dartgif_save_test.dart가 새로 생겼다. 갤러리 저장 버그도 Android/iOS 양쪽에서 수정됐다. 그리고 PDF 패스워드 보호와 해제도 Pro 기능으로 추가됐는데, iOS에서 SPM 패키지가 바뀌었고 project.pbxprojPackage.resolved 두 군데가 업데이트됐다.

multi-select merge to single PDF도 오늘 들어갔는데, 이걸 포함하면 vidpress가 GIF 툴이자 PDF 툴 앱으로 성격이 점점 뚜렷해지고 있다는 게 보인다. 1.1.0 — Word/PDF→HWPX reverse conversion, consumable credits IAP, PDF tools/OCR/signature, AdMob removal이라는 커밋도 오늘에 찍혔다. HWPX 역변환에 소비형 IAP까지 들어갔고, AdMob을 제거한다는 결정도 이 버전에 들어갔다. Play 스토어 업로드 스크립트도 오늘 추가했다. scanbox 쪽에 있던 OAuth token 패턴을 공유하는 구조로.

plant 앱 반려, 그리고 재제출

식물 성장 일지 앱은 1.4.1이 반려됐다. 커밋 메시지에 1.4.1 반려→1.0.1(4) 재제출 완료 기록 + resubmit 스크립트 IAP 엔드포인트 픽스라고 요약돼 있다.

반려 사유가 커밋들에 흩어져 있다. 출처/참고문헌 화면 추가가 "App Review 1.4.1 대응"이라고 명시돼 있고, 공신력 있는 출처 12개를 8개 로케일에 걸쳐 진입점 3곳에 넣었다. lib/content/references.dart가 새로 생기고 l10n 파일 여러 개가 한 번에 바뀐 것이다. 건강이나 식물 정보 앱에서 리뷰어가 출처를 요구하는 경우가 있는데 그 케이스였던 것 같다.

광고 관련 버그들도 같이 처리됐다. 풀스크린 광고 백투백 가드였는데 AdState를 공유해서 90초 gap을 두는 방식이었다. 같은 커밋에 restore 피드백, clearError/ensureProduct, 페이월 구간 억제, 원자적 세션 쓰기, RTA 라벨 겹침 수정까지 한꺼번에 묶였다. 광고 관련 버그들이 리뷰 과정에서 QA 돌리다 연달아 터졌거나, 반려 수정하면서 전반적으로 뒤진 것 같다.

비공개테스트=테스트광고 강제 + 구매 트래픽 금지라는 docs 커밋에 pub-3485 밴 원인 재추정이라는 설명이 붙어 있다. 비공개 테스트 환경에서 실제 광고나 실제 구매 트래픽이 발생해서 AdMob 밴을 맞은 게 아닐까 하는 추정이다. 이게 맞다면 꽤 쓰라린 사고다. 비공개 테스트라고 해도 AdMob 입장에서는 실제 트래픽과 구분이 안 됐을 것이다.

growth-journal photo gate도 오늘 들어갔다. 무료 사용자는 식물당 사진 3장, Pro는 무제한. .fvmrc가 바뀐 게 Flutter 버전 관련 변경이 있었다는 것이고, integration_test/plant_photo_gate_test.dart가 새로 생겼다.

gwangclock reapply가 두 쌍

gwangclock은 광고를 전면 제거하고 Pro 기능을 확장하는 v1.2.0이 오늘 올라갔다. 반응속도 클릭 보정과 밀리초 정밀 표시가 Pro 전용 기능으로 들어갔다. AndroidManifest.xml, Info.plist, app_settings.dart, haptic_cue.dart 등이 바뀌었다.

커밋이 두 쌍으로 찍혀 있다는 게 특이하다. feat: v1.2.0+10 ...Reapply "feat: v1.2.0+10 ...", docs: repo CLAUDE.md 신설 ...Reapply "docs: repo CLAUDE.md 신설 ...". Reapply가 쌍으로 있다는 건 한 번 올렸다가 어떤 이유로 되돌리고 다시 적용했다는 뜻이다. cherry-pick이 꼬였거나 merge conflict를 수동 해결한 뒤 재적용한 것 같다. 내용 자체는 같을 테니 실제 변경이 달라지지는 않았겠지만 git 로그가 좀 지저분해졌다.

CLAUDE.md를 새로 신설한 것도 오늘이다. 수익화 구조(광고 제로, Pro), 빌드 운영 지식을 문서화했다. 앱이 일정 복잡도를 넘으면 이런 게 있어야 다음에 다시 봤을 때 맥락을 잃지 않는다.

scanbox 스크린샷 80장 분량

scanbox가 오늘 릴리즈 작업 분량이 가장 많았다. release: 스토어 스샷 2→5장 스윕 (RTA·톤젠·마커 컷 추가, 8로케일×2기기). 기존 2장에서 5장으로 늘리면서 8개 로케일 각각 두 가지 기기 사이즈. 계산하면 80장 분량을 교체한 것이다.

lib/screenshots_harness.dart, store/asc_replace_generic.py, store/build_cap_screenshots.sh, store/frame_screenshots.py, store/play_shots_framed.py가 바뀌었으니 자동화 파이프라인이 있긴 하다. 그래도 80장 치의 캡처 내용과 프레임 설정을 하루에 처리한 것이다.

1.1.0+8에는 딥 클린 3단계 자동 콤보와 톤 프리셋 저장이 들어갔다. tone_presets.darttone_service.dart. 이걸 제출했다가 "진상5"라고 이름 붙인 케이스에서 걸리는 문제가 발견됐고, 게이트 로직 수정 후 1.1.0+9로 재빌드해서 양 스토어에 다시 올렸다. integration_test/pro_features_test.dart도 업데이트됐다.

Pro 전용 기능 세트가 이번에 대거 들어갔다. 상세통계, 소급체크, 1년 히트맵, 백업/복원 CSV, 정원 테마, 다중 리마인더, 아카이브. l10n 파일이 여러 로케일에 걸쳐 동시에 바뀐 것이 보인다. iOS가 READY_FOR_SALE로 라이브 들어온 것도 오늘 실측 확인했고, alpha versionCode가 stale이었던 것도 실측으로 정정했다. vc3이 아니라 vc6이었다.

pub-3485 킬스위치

공통 쪽에서 광고 킬스위치 룰이 오늘 생겼다. 해지된 pub-3485 AdMob 유닛이 아직 코드에서 제거가 안 된 상태라서, 제거될 때까지 광고 전체를 강제로 꺼둔다는 것이다. src/lib/admob.ts에서 킬스위치를 OFF로 고정했다. docs/mobile-app-launch.md에도 룰이 기록됐다.

plant 앱의 비공개 테스트 밴 원인 재추정과 이 킬스위치 이야기가 이어져 있는 것 같다. 해지된 유닛이 실제 환경에서 노출됐고 그게 밴으로 이어졌을 가능성. 어느 앱에서 어떤 경로로 노출됐는지는 커밋들만 보고는 특정하기 어렵지만, 일단 해지 유닛 제거 전까지는 전면 차단이 맞다.

앱스토어 인앱매출 디스코드 자동화

인프라 쪽에서 오늘 새로 추가된 게 하나 있다. 앱스토어 인앱매출을 매일 22:45 KST에 디스코드로 리포트하는 것. cms_appstore_sales 테이블에서 읽어서 #앱스토어-매출 채널로 보낸다. _lib/appstore-sales-discord.py와 크론 설정 파일.

앱이 여러 개로 늘어나면서 수동으로 ASC 들어가서 확인하는 게 한계가 왔다는 의미다. 어떤 앱에서 어떤 IAP가 얼마나 팔렸는지 매일 수동으로 보는 건 앱이 한두 개일 때나 되는 일이다.

IAP 가격을 바꾸면 availability가 리셋된다는 트랩도 docs/mobile-app-launch.md에 기록했다. 어딘가에서 당하고 기록한 것인지 예방 차원인지는 모른다. 어느 쪽이든 기록해두는 게 맞다.

slangdict 스캐폴드, 그리고 psy 루틴

topikbox를 포크해서 slangdict 스캐폴드를 만드는 커밋도 오늘 들어갔다. pre-rename snapshot이라는 설명대로 이름 바꾸기 전 시작 상태다. .env.example, .gitignore, PROGRESS.md, API 패키지 설정.

이 바쁜 날에 새 프로젝트를 시작했다는 게 조금 웃기다. 오전부터 정산 버그 잡고, 파트너 포탈 사고 수습하고, 모바일 앱 여러 개 릴리즈 사이클 돌리면서 새 레포도 열었다.

psy 쪽 자동 생성 테스트는 오늘 루틴대로 돌았다. destiny-wind, midnight-taxi-ghost, silence-type 세 개 커버 이미지와 테스트 JSON이 들어왔다. 매일 자동으로 돌아가는 작업이라 특별히 한 게 없지만 커밋 목록에는 찍혀 있다.


오늘 커밋을 다 세면 30개가 훌쩍 넘는다. 레포도 여러 개. 정산 규칙 세 건 수정, 파트너 포탈 사고 수습, 모바일 앱 다섯 개를 각기 다른 상태로 진전, 인프라 자동화 하나 추가, 새 프로젝트 시작.

이런 날의 문제는 각각에 충분한 집중이 가지 않는다는 것이다. kp-pay 원가 정정만 해도 제대로 파고들면 이전 정산 데이터 전체를 다시 검증해야 하는 작업인데, 오늘은 실측 확인 후 수치 교정에서 끊었다. 나중에 누군가 언제부터 틀렸는지 물으면 오늘 고쳤다는 것밖에 답 못 한다.

파트너 포탈 appgate 사고도 SQL 추가로 해결했지만 왜 처음부터 경로 등록이 누락됐는지는 못 팠다. 개발 환경과 운영 게이트웨이 설정이 함께 관리되지 않는 구조적 문제인데, 오늘 고친 건 증상이다.

모바일 앱들은 다 진행이 됐다. 다 됐는데, 각각을 기억하는 깊이가 얕다.

댓글 0

첫 댓글 달아줘.