일기 slecs

앱 심사 넣고 플랫폼 뜯는 하루가 있다

목차

오늘이 그런 날이었다.

아침에 404_pjt 거래 통제 화면 구조를 다시 그리면서, 동시에 PitLog build 7이 App Store 심사 큐에 올라가 있었고, FilmDev는 워치 타깃 배포 서명이 전날 밤부터 묶인 채였다. Bloomly는 독립 코드 리뷰에서 나온 파인딩 처리가 아직 덜 됐고, psy는 자동 생성 테스트 두 개가 로그로만 올라왔다. 프로젝트가 이렇게 동시에 굴러가는 날은 집중이 산산이 흩어질 것 같은데, 오늘은 각 프로젝트가 성격이 달라서 오히려 전환 비용이 덜했다. 플랫폼은 UI 구조 재설계, 모바일은 빌드 파이프라인과 심사 대응, 그 틈새에 버그 수정.

거래 통제, 전자계약, 사이트설정이 셋으로 나뉘어 있던 문제

404_pjt 오전 작업의 중심은 거래 통제 설정이 너무 여러 화면에 분산돼 있다는 걸 해소하는 것이었다. 쿨다운 설정이 사이트설정에, 전자계약 게이트가 거기 또 별도 섹션에, 실제 거래 통제 화면은 완전히 따로. 관리자가 거래 관련 뭔가를 조정하려면 화면을 두세 군데 왔다 갔다 해야 했다. 기능이 하나씩 붙으면서 자연스럽게 그렇게 됐는데, 이제는 못 참을 수준에 이른 거다.

정리 방향은 단순했다. 사이트설정에서 쿨다운·전자계약 게이트 섹션을 걷어내고 거래 통제 화면 하나로 일원화. 대신 거래 통제 화면에 설정 탭을 추가해서 사이트설정으로 나가지 않아도 in-place로 수정할 수 있게 만들었다. 메뉴도 거래 통제와 전자계약 체결 현황을 탭으로 묶어서 하나의 진입점에서 오갈 수 있게 SQL을 새로 썼다. MENU_20260824_trade_control_econtract_merge.sql이 그 결과물이고, 기존 MENU_20260821_trade_control_admin.sql에는 개발 DB 실행 완료 기록과 Feature 캐시 무효화 주의사항을 추가로 기록했다.

그런데 설정 탭 in-place 수정을 붙이고 나서 바로 400이 떴다. configSn이 빠져 있었다. 화면에서 폼 데이터를 전송할 때 사이트 설정 일련번호를 같이 넘겨야 하는데, 탭 전환 흐름에서 그 값이 누락된 거다. 처음에 URL 라우팅 문제인지 파라미터 바인딩 문제인지 헷갈렸는데 컨트롤러 로그를 찍어보니 바인딩 전에 값 자체가 없는 거라 금방 찾았다. 이런 류의 버그는 찾는 데 30초, 왜 놓쳤지 하는 자책에 3분 쓰는 패턴이다.

확약서 템플릿도 이 기회에 드롭다운으로 바꿨다. 텍스트 입력이면 오탈자가 나거나 유효하지 않은 값이 들어올 수 있어서다. 탭 배지 대비는 다크 배경에서 배지 텍스트가 거의 안 보이는 수준이었는데, 꽤 오래 방치돼 있었던 거다. 체결 현황 화면 탭바에 '차단 중' 배지도 화면 간 이동 시 사라지는 버그가 있었는데 같이 잡았다. 목록 JSP와 컨트롤러 양쪽에 손이 갔다.

DAILY 모드가 발동을 안 한 이유

전자계약 게이트 DAILY 모드 미발동 버그가 오늘 404_pjt 작업 중 제일 시간을 잡아먹었다.

증상은 명확했다. DAILY 설정 상태에서 전자계약 게이트가 열리지 않는다. 처음에는 sqlmap 쿼리 쪽 문제라고 보고 매퍼를 꽤 오래 뒤집었다. 쿼리 자체는 이상이 없었다. 서비스 레이어인가 싶어 로직을 따라가다가 OGNL 표현식에 도달했다.

OGNL에서 문자열 리터럴을 ==로 비교하면 Java String과 마찬가지로 참조 비교가 되는 케이스가 있다. mode == 'DAILY'처럼 쓴 게 문제였다. .equals() 호출 방식으로 교정하고 나서야 DAILY 조건이 정상 발동하기 시작했다. 찾고 나면 너무 뻔한 실수인데 이 코드가 만들어진 시점부터 이 상태였던 건지 중간에 누군가 건드린 건지 이력 추적이 안 돼서 찝찝함이 남는다. 쿼리 매퍼도 같이 수정됐으니 커밋 한 줄에 두 군데가 묶였다.

전자계약 UI는 스타일 작업도 오늘 들어갔다. 이커머스 PG 플랫폼 표준 톤으로 정렬하는 작업인데, 목록과 상세 화면 JSP 두 개에 CSS·SCSS가 같이 건드려졌다. 컴포넌트 CSS를 건드리면 의도치 않게 다른 화면에 영향 갈까봐 항상 조심스럽다.

전자계약 서명 동의 체크박스 분리도 오늘 했다. 하나로 묶여 있던 걸 두 개로 분리하면서 운영 템플릿 SQL(INSERT_20260824_econtract_templates_prod.sql)을 함께 남겼다. 화면 변경보다 템플릿 문구 조율이 더 오래 걸렸다. 서명 화면에서 어떤 항목에 동의했는지가 나중에 분쟁 방지와 직결되는 부분이라 문구 하나하나를 꽤 신경 썼다.

kp-pay 가상계좌와 1원 인증 원가 누락

kp-pay 쪽은 두 개가 묶여 있었다.

가상계좌 v2.3에서 인증 완료 후 발급 흐름이 기존 즉시 발급 흐름과 경로가 다르다. 이 둘을 동시에 지원하는 듀얼모드 대응이 첫 번째였다. wallet 컨트롤러, payment 유틸, charge JSP 두 개, payCharge JSP까지 다섯 파일이 같이 수정됐다. 인증 완료 콜백이 들어오는 시점과 기존 발급 완료 처리 시점이 달라서 상태 관리를 정리했다.

두 번째는 정산 도메인에서 나온 버그였다. 1원 인증(regcerti) 원가가 정산 기록에 누락되고 있었다. settlement-domain.md에 백필 확정치가 들어왔다 - 23,576건에 3,890,040원, 실기록 시작이 12:34:47부터. 이 수치를 기준으로 원가 누락 수정이 들어갔다. 원가 기록이 빠져 있으면 수익 구성 계산이 틀어지니까, 총괄 요약 화면의 수익 구성 표를 믿고 쓸 수가 없는 상황이었다. 수익 구성 표에 기간 직접 선택 열도 오늘 추가했는데, 데이터가 맞아야 그 표가 의미가 있다.

쿠폰 문자 이연 - 락 보유 시간 얘기

쿠폰 문자발송을 커밋 후 전용 스레드로 이연시켰다. 기능 변경이라기보다 성능 방어다. 정산 트랜잭션이 락을 보유하는 시간 안에 외부 문자 발송 API 호출이 끼어들어 있으면 락 보유 시간이 길어진다. 문자 발송이 느리거나 타임아웃 나는 상황에서는 정산 처리 전체가 묶일 수 있다는 게 문제다. 커밋 후 별도 스레드에서 처리하면 트랜잭션은 먼저 닫히고 문자 발송은 비동기로 나간다. 이게 더 나은 패턴이라는 걸 알면서 한동안 손을 못 댔다가 오늘 정리했다.


404_pjt 쪽만 해도 이 정도 분량인데, 나머지가 더 있다.

PitLog: build 7과 그 앞에 있던 것들

PitLog는 오늘 build 7을 제출했다. 핵심 수정은 탭 데이터가 갱신됐을 때 해당 탭을 리로드하는 것이었다. IndexedStack 구조에서 탭을 오가는 동안 데이터가 바뀌어도 화면이 갱신되지 않는 문제였고, app_shell.dart에서 탭 전환 시 상태를 강제 갱신하는 로직을 추가했다. fuel, garage, reminders 화면 각각에 통합 테스트도 달았다.

build 6이 심사에 올라가지 못한 이유가 위젯 익스텐션 서명이었다. iOS 위젯 익스텐션이 App Store 배포 서명으로 묶이지 않아서 업로드 자체가 막혔다. ExportOptions.plistproject.pbxproj를 수정해서 풀었는데, pbxproj는 볼 때마다 손이 떨린다. GUID가 꼬이면 찾기가 끔찍해서.

리뷰 컴플라이언스 수정이 상당히 쌓였다. 페이월에서 무료 기능을 유료로 팔고 있던 항목, 프라이버시 정보 누락, 타이어 화면에서 읽기를 조용히 버리던 버그, DTC 파서에서 어댑터가 카운트 바이트를 보낼 때 이후 코드가 모두 한 칸씩 밀리던 버그, 위젯 카운트다운이 그리니치 동쪽에서 하루가 밀리던 버그까지. DTC 파서 버그가 제일 웃긴 케이스였다. 응답 첫 바이트가 코드 수(count byte)인 어댑터 모델이 있는데, 그 바이트를 코드로 해석하니 이후 코드가 전부 한 칸씩 뒤로 밀렸다. obd_protocol_test.dart에 테스트가 들어갔으니 재발은 없다. 위젯 카운트다운은 DueMath.swift에서 날짜 계산을 UTC로 해버려서 생긴 거였다. 서울이나 도쿄 기준으로 UTC보다 9시간 앞서니까, 하루 단위 계산에서 오프셋이 고스란히 오차가 됐다. 검증 스크립트(test_widget_dday.sh)까지 만들어서 재발 방지를 박았다.

백업이 정비 기록만 내보내던 걸 전 테이블로 확장한 것도 오늘 들어갔다. 골든 이미지는 VIN 필드 추가 때문에 4_car 여섯 개, 네 탭 쉘 변경 때문에 1_map 여섯 개를 재생성했다. 골든 diff 아티팩트들은 .gitignore에 추가해서 트래킹에서 제외했다.

PitLog 기능 목록(ELM327 OBD 리더, 직업 체크리스트, NHTSA 리콜 조회, 영수증 스캐너, 타이어 세트 관리, iOS 홈 위젯, 비용 대시보드, 연료 로그, PDF 서비스 히스토리)이 이번에 커밋 묶음으로 올라온 건, 이번 빌드 제출을 위해 준비된 것들이 한꺼번에 기록된 거다. OBD 리더는 BLE 트랜스포트가 아직 미검증이라 그 상태로 기록이 남아있다. 영수증 스캐너는 iOS 온디바이스 처리, PDF 히스토리는 한자·한글·일본어 폰트까지 따로 번들링됐다.

FilmDev: 워치 빌드가 순환이었다

오늘 가장 황당했던 빌드 문제는 FilmDev에서 나왔다.

Apple Watch 컴패니언 앱(FilmDevWatch)을 빌드 파이프라인에 추가했는데, 워치 타깃을 호스트 앱에 embed하는 페이즈가 iOS 빌드를 순환으로 깨뜨렸다. 빌드하려면 워치가 먼저 빌드돼야 하는데, 워치를 빌드하면 호스트 앱이 필요한 구조. Xcode project.pbxproj 안에서 의존성이 잘못 엮인 거다. 워치 embed 페이즈의 위치를 바꾸고 xcscheme에서 빌드 순서를 명시적으로 지정해서 풀었다. WatchBridge.swift도 같이 수정됐다.

아카이브에서만 잡히는 함정이 몇 개 더 있었다. 시뮬레이터에서는 아무 문제 없이 빌드되는데 배포용 아카이브에서 터지는 케이스들. 이게 워치 앱 배포할 때 제일 골치 아픈 부분인데, 시뮬로 검증을 다 해도 실제 배포 서명 빌드에서 새로운 게 나온다. 이번에 잡은 함정들은 CLAUDE.md에 박아뒀다.

워치 버전을 호스트 앱 버전과 같은 값으로 묶는 커밋도 별도로 들어갔다. App Store Connect가 버전 불일치를 업로드 시점에 차단하는 경우가 있어서다. 워치 앱 아이콘도 빠져 있었다. 아이콘 없으면 업로드 자체가 막히니까 Icon-1024.png 추가하고 Contents.json을 맞췄다.

워치 UI는 8로케일로 번역이 들어갔고, 암실 적색 테마를 폰 앱과 통일했다. Localizable.xcstrings 방식이라 추가 로케일 붙이는 건 이제 깔끔하다.

Dart 쪽 워치 상태 통지(watch_sync.dart)와 네이티브 쪽(WatchBridge.swift)이 각각 커밋으로 분리돼 있다. M4라고 표기된 작업인데, 네이티브 절반이 들어오기 전에 stale 주석이 들어가 있던 걸 같이 정정했다. 이런 멀티레이어 브리지는 양쪽을 동시에 볼 수 있어야 디버그가 된다는 게 항상 교훈으로 남는다.

opus 리뷰 파인딩 7건도 오늘 반영됐다. 필름 손실 경로 차단과 워치 무력화 경로 차단이 주요 항목이었다. FilmDev에 이번에 들어간 기능 범위(현상 로그 스캔 사진 첨부, 필름 재고 관리, 약품 수명·재사용 추적, 레시피 QR 공유·가져오기, Apple Watch 햅틱 타이머 컴패니언)가 꽤 묵직한데, 스크린샷도 워치 포함 8로케일로 정리됐고 배포 서명 해결까지 오늘 다 끝냈다.

Bloomly: 심사 사이클 한복판에서

Bloomly는 지금 IN_REVIEW 상태다. 그 전까지 오늘 수정이 꽤 들어갔다.

독립 리뷰에서 나온 C1(리워드 보상 사망), C2(페이월 허위 기능)와 M1~M3 파인딩을 반영했다. 자체 점검 4축(성장 보너스 규칙, iCloud 미다운로드, 무효 트래픽, 통합테스트 릴리스 금지)도 돌렸다.

FAB heroTag 충돌 문제는 IndexedStack에서 탭들이 항상 마운트돼 있는 구조에서 생기는 거다. chemicals, inventory, recipes 화면의 FAB에 각각 고유한 heroTag를 부여해서 해결했고 테스트도 달았다. 처음에 heroTag 자체가 충돌 원인이라고 봤는데, 나중에 실제 원인이 시뮬 pasteboard XPC 동작 차이라는 걸 확인해서 CLAUDE.md를 정정했다. 틀린 원인 분석을 기록으로 남기는 것보다 교정하는 게 맞는 거다.

페이월이 안 닫히던 버그와 배너가 모달 시트 탭을 가로채던 버그가 QA 차단급으로 분류됐다가 빌드 번호 19에 반영됐다. 배너 z순서·모달 닫힘 규칙을 CLAUDE.md에 박아서 재발 방지를 했다.

iCloud 백업은 엔타이틀먼트 복원과 페이월 문구 복원으로 들어갔다. CAS 네트워크 구성 갱신도 같이. 빌드 번호 18이 재제출용이었는데 QA에서 2건이 더 잡혀서 19가 됐다.

수익화 기능 9종(스트릭 보험, 집중 타이머, 위젯 스킨 4종, 잠금화면 위젯, 식물 도감, 월간·연간 회고 카드, 정원 꾸미기 데코팩, iCloud 백업, 무료 습관 템플릿 팩 5종)과 DB 스키마 v5, 운영 함정들이 CLAUDE.md에 정리됐다.

하루 끝에 남은 것

오늘 가장 깔끔하게 끝난 건 404_pjt의 거래 통제 일원화다. 설정이 세 군데에 흩어져 있던 걸 하나로 모았고, 탭 메뉴로 묶였고, 설정 in-place 수정도 됐다. 한동안 기술 부채로 알고 있으면서 못 건드렸던 걸 오늘 한 번에 정리했다. OGNL 비교 버그처럼 한 줄 수정인데 증상이 드라마틱하게 달라지는 것도 있었고, FilmDev 워치 순환 빌드처럼 이론적으로는 단순한 의존성 문제인데 Xcode pbxproj 안에서 찾는 게 고통스러운 것도 있었다.

앱 쪽은 '완료'보다 '진행 중'이 더 많다. PitLog build 7이 올라간 것, FilmDev 워치 빌드 서명 완료, Bloomly IN_REVIEW 진입. ZipWise는 WAITING 상태.

피드백 반영 큐가 다 비워지지 않았다. 내일 이어서 볼 게 있다.

댓글 0

첫 댓글 달아줘.