일기 slecs

같은 파일을 일곱 번 고쳐 올린 날

목차

오늘 커밋 로그를 닫기 전에 훑어보다가 멈췄다. fix: rewrite ATT/permission purpose strings for App Review 5.1.1 (all locales) + build bump. 이 타이틀이 일곱 번 나온다. 로케일 조합이 조금씩 다르다. de/en/es/fr/it, de/en/es/fr/id, ar/en/es/hi/id, 또 de/en/es/fr/it... 같은 파일, 같은 이유, 하루 안에 일곱 커밋. 이걸 "삽질"이라고 부르지 않으면 뭐라고 부를 수가 없다.

App Review 5.1.1이 오늘의 보스였다

5.1.1은 개인정보 수집 목적 문자열 가이드라인이다. 앱이 카메라, 마이크, 위치, 추적(ATT) 권한을 요청할 때 사용자에게 보여주는 NSUsageDescription 류의 문자열들인데, 애플 리뷰어가 이걸 "충분히 구체적이지 않다"거나 "마케팅 언어처럼 들린다"는 이유로 반려한다. 딱히 자동화된 룰이 아니라 사람이 읽고 판단하는 항목이라, 뭘 어떻게 고쳐야 한다는 명확한 기준이 없다. 오늘 그 모호함을 몸으로 때운 날이었다.

문제는 로케일이 여러 개라는 거다. Info.plist의 영어 버전 고치고, en.lproj/InfoPlist.strings 고치고, 그 내용을 독일어·스페인어·프랑스어·이탈리아어·인도네시아어·힌디어·아랍어로 다 맞춰야 한다. 번역이 자동으로 따라오지 않는다. 영어 원문을 바꾸면 나머지 로케일 파일도 그 논조에 맞게 수작업으로 조율해야 한다. 그리고 문자열을 바꿀 때마다 빌드 번호도 올려야 한다. 애플 심사 시스템이 같은 빌드 번호로 올라온 바이너리를 받지 않기 때문이다.

첫 번째 rewrite가 들어갔다. 빌드 범프, 업로드. 그리고 반려. 문구가 여전히 "마케팅 톤"이라는 거다. 두 번째 고쳤다. 이번엔 아예 기능 설명 위주로 덤덤하게. 다시 올렸다. 또 반려. 세 번째. 네 번째. 커밋 메시지가 동일하게 반복되면서 git log가 점점 무겁게 쌓여갔다. 일곱 번째 즈음에는 솔직히 뭘 더 고쳐야 할지 감이 희미해졌다. 표현 하나하나를 뒤집어 보면서 "이게 마케팅인가? 기능 설명인가?" 경계를 스스로 따지는 게 어느 순간 의미없게 느껴지기 시작했다.

그 과정에서 1.4.1도 같이 물고 들어왔다. 앱에 의료 관련 정보가 들어가 있는데, 출처(citation)를 명시하지 않아서 반려됐다는 것이다. 인앱 의료정보 및 출처 화면을 새로 만들었다. 다국어 지원이 있는 앱이니 app_en.arb부터 시작해서 app_ar.arb, app_es.arb, app_hi.arb, app_id.arb, app_ja.arb 전부. 기능 자체는 단순하다. 화면 하나, 내용 나열. 그런데 이걸 심사 통과용으로 만든다는 게 기술적으로 만드는 것과 결이 달라서 머릿속이 좀 이상한 상태로 돌아갔다.

2.3.10도 있었다. 타 플랫폼 언급 금지 조항. 앱 스토어 메타데이터에서 타 플랫폼 이름이 들어간 문구가 걸렸다. 이건 ASC(App Store Connect) PATCH로 직접 고쳐야 하는데, 문서만 수정하면 무효라는 걸 mobile-app-launch.md에 따로 기록해뒀다. 재발 방지용 플레이북 형태로. 오늘 아침 자동 반려 sweep이 있었다는 것도 문서화했다. 이런 거 안 적어놓으면 두 달 뒤에 똑같이 밟는다.

스크린샷 204장

App Review 대응 중간에, 또는 병렬로, 스토어 스샷 재생성 작업도 돌아갔다. framed 처리된 스샷 204장. frame_screenshots.py로 iOS iPad 13인치 + 다국어 조합이 생성됐고, 커밋에는 ar/01, ar/02, ar/03만 보이지만 실제로는 전 로케일이 다 재생성된 것이다. 스샷 관련 스크립트도 이날 꽤 정비됐다. asc_iap_screenshot.py(IAP 전용 리뷰 스샷), asc_ss_upload.py(업로드 보강), play_alpha_track.py(Play 알파 트랙 이관), play_screenshots.py(Play 스샷), 그리고 store/build_cap_screenshots.sh까지. 스토어 대응 인프라를 사실상 이 날 한 번 더 정비한 셈이다.

앱 스토어 두 군데 동시에 제출하는 게 왜 이렇게 피곤하냐 하면, 각 플랫폼의 스샷 규격이 다르고, 로케일 수가 많을수록 조합이 폭발적으로 늘어나기 때문이다. iOS는 디바이스 사이즈별로 따로 받는다. Play는 또 자기 규격이 있다. 그 조합을 수작업으로 관리하다 보면 반드시 어딘가 빠진다. 스크립트로 묶어놓은 게 아니었으면 오늘처럼 반려가 쏟아지는 날에 스샷까지 다시 만들어야 한다는 걸 감당하기 어려웠을 것이다. IAP 쪽 purchase_service.dart 구매 복원 로직도 이날 보강됐다. 에러 처리가 엉성했던 부분을 잡았다.

파트너 포탈, 그나마 뭔가 완성해 가는 쪽

iOS 쪽이 "버티기"였다면, 웹 파트너 포탈 쪽은 그나마 앞으로 나가는 작업이었다.

오늘 가장 로직이 두꺼운 피처는 스코프 자동 편입이었다. 두 갈래로 나뉜다.

하나는 스코프 매니저가 파트너를 생성하거나 승인할 때 해당 파트너를 자신의 담당 스코프로 자동으로 넣는 것. 지금까지는 파트너를 만들고 나서 스코프 편입을 따로 해줘야 했는데, 그 수작업을 없앴다. 세션도 즉시 갱신된다. 이게 중요한 이유가, 스코프가 갱신됐는데 세션은 구버전 스코프를 들고 있으면 해당 파트너가 로그인 상태에서 권한 오류를 마주치기 때문이다. 세션 재적재를 묶어서 원자적으로 처리하는 게 맞다고 판단했다.

다른 하나는 초대링크로 신청한 파트너다. 초대링크를 발급한 사람(발급자) 쪽으로 신청자가 자동 편입되어야 하는데, 이걸 신청 시점에 처리하도록 배선했다. Controller 두 군데, 쿼리 매퍼 두 군데 건드렸다. .claude/rules/admin-patterns.md도 같이 업데이트됐는데, 이런 패턴이 반복될 때 일관성 있게 적용하기 위한 룰 명시다. 다음 사람이 비슷한 케이스를 만들었을 때 바퀴를 다시 발명하지 않게.

공급 웹훅(supply-webhook) 쪽도 제법 무거운 작업이 들어갔다. 계층 팬아웃이라는 게 대리점과 영업대행사를 통해 이벤트를 흘려보내는 구조인데, 회원정보와 스코프 이력을 같이 필터링해서 웹훅 페이로드에 담는다. DDL이 변경됐고(DDL_TABLES.sql), ALTER 스크립트도 들어갔다(ALTER_20260725_supply_webhook_log_source_partner.sql). api-docs.jsp 화면에 이 내용이 반영됐다는 것도 커밋에 보인다.

API 권한 쪽도 정리됐다. 파트너별로 외부 API 문서에 접근할 수 있는지를 토글로 제어하는 기능(api_enabled 플래그). 파트너 폼(form.jsp)에 토글 UI가 붙었고, api-docs, API키, 웹훅 엔드포인트 Controller에 접근 가드가 추가됐다. 없는 상태에서는 api_enabled 파트너가 아니어도 해당 엔드포인트로 들어올 수 있었다는 뜻이다. 뒤늦은 가드긴 한데 지금 막으면 된다.

CSS는 common.css와 사이트별 CSS 세 군데(SUNRISE.css, 이커머스 PG 플랫폼.css, apple.css)가 동시에 변경됐다. 토글 스타일이 공통에서 사이트별로 오버라이드되는 구조여서 한 줄 추가가 여러 파일로 퍼진 케이스다.

대시보드 카드 정렬 - 의외로 오래 걸린 것

작게 보이지만 대시보드 카드 바닥 정렬 이슈가 꽤 신경 쓰였다. KPI 카드들이 같은 줄에 늘어서 있는데, 설명 텍스트 길이가 카드마다 달라서 높이가 들쑥날쑥했다. 파트너 대시보드(dashboard.jsp), 공급사 대시보드(dashboard-supplier.jsp), 발송포탈 대시보드(dashboard-sender.jsp) 세 군데.

먼저 설명 박스가 없는 카드 6종에 설명을 붙이는 피처가 들어갔고(dashboard.jsp), 그다음 공급사·발송포탈 KPI 카드에도 같이 붙였다. 그 다음 커밋이 정렬 fix다. 순서를 보면 "일단 넣고 보니 더 삐뚤어졌다, 그래서 바닥 정렬 잡았다"는 흐름이다. flexbox로 카드 컨테이너를 align-items: stretch나 그와 유사한 방식으로 맞췄을 것이다. 커밋 메시지에 "줄별 카드 높이 들쑥날쑥 해소"라고 명시된 걸 보면 꽤 눈에 띄었던 모양이다.

회원가입 이메일 필수 검증도 손댔다. SIGNUP_POLICY 기반으로 통일하면서 프론트 JS에 하드코딩돼 있던 이메일 필수 여부 분기를 걷어냈다. 백엔드 정책 값이 있는데 프론트에 동일한 조건이 중복으로 박혀 있으면 양쪽이 어긋날 위험이 항상 있다. register.jsp이커머스 PG 플랫폼/pages/register.jsp 두 군데에 반영됐다.

하위파트너 화면(subordinates.jsp)에 스코프 매니저 대응과 XSS 처리, 예치 모달 배선 정리가 들어갔다. 이 커밋이 같은 내용으로 두 번 올라가 있는데, 첫 번째 커밋이 불완전했거나 충돌 해소 후 재커밋한 것으로 보인다. XSS 처리는 화면에서 직접 문자열을 출력하는 곳에 이스케이프를 빠뜨린 부분을 잡은 것이고, 예치 모달 배선은 버튼을 누르면 모달이 뜨도록 이벤트가 연결되지 않은 부분을 고친 것이다.

대리점 커미션 시드

대리점(AGENCY) 기본 커미션 시드 최소치를 넣었다. 충전은 0.6% + 1,000원, 결제·구매(가상계좌)는 0.6%. 시드 데이터가 없으면 대리점 유형으로 파트너를 생성할 때 커미션이 비어서 나오는 문제가 생긴다. 수치 자체는 정책 결정이고, 그걸 코드에 반영하는 건 단순한 일이지만 누군가 명확하게 정하고 넣어야 존재하는 것들이다.

잡다한 정리들

gitignore 정리가 세 군데 들어갔다. .omc 런타임 상태 파일들, store/__pycache__, tsconfig.tsbuildinfo. 이런 게 추적 대상으로 올라가 있으면 diff가 지저분해지고 협업 시 충돌 지점이 된다. 당장 급하진 않은데 쌓이면 성가시다.

wear_test_run.sh도 갱신됐다. Wear OS 테스트 스크립트인데, 커밋 메시지가 "갱신"이라고만 돼 있어서 디테일은 모른다.

wishket에 지원서 초안 두 건 생겼다. bid_academy_codereview.html, bid_imweb_seminar_landing.html. 코드리뷰 아카데미 관련이랑 세미나 랜딩 페이지 관련. 이걸 오늘 짬을 내서 잡아둔 거다.

psy 사이트 자동 생성 테스트 3건(destiny-mirror, haunted-upstairs-apartment, listening-type)도 chore로 올라왔다. 매일 자동으로 돌아가는 것이라 특별히 손댄 건 없고.

마치며라고 쓰기엔 너무 힘든

오늘 하루를 가장 잘 나타내는 커밋은 역시 fix: rewrite ATT/permission purpose strings for App Review 5.1.1 일곱 번이다. 웹 쪽은 스코프 편입, API 가드, 대시보드 정렬, 웹훅 팬아웃까지 피처가 착착 쌓였다. 기능 단위로 보면 하루 치고 꽤 많이 나간 편이다. 그런데 iOS 쪽에서 계속 소모됐다. 애플 리뷰 시스템이 어떤 방향으로 반려를 날릴지 예측이 안 된다는 게, 기술적 문제보다 더 소진되는 종류의 문제다.

목적 문자열 하나를 두고 "이 표현이 마케팅인가 기능 설명인가"를 일곱 번 고민하다 보면, 어느 순간 판단 기준이 흔들린다. 이게 맞는 건지 틀린 건지가 아니라 "리뷰어가 이걸 어떻게 읽을까"로 관점이 완전히 옮겨간다. 코드를 쓰는 게 아니라 심리전을 하는 느낌이 드는 시점이 있는데, 오늘이 그랬다.

docs/mobile-app-launch.md에 2.3.10 재발 방지 플레이북이랑 5.1.1 목적 문자열 재작성 플로우를 기록해뒀다. 오늘 몸으로 배운 것을 문서로 남겨야 다음에 같은 고통을 줄일 수 있다. 줄어들 거라는 보장은 없지만.

댓글 0

첫 댓글 달아줘.