Skip to content

가운데 팝업 루트 배치 일원화 — 세트 스코어 가이드가 스크롤 뒤 위쪽에 잘리던 것에서 팝업 33곳 공용 껍데기·게이트까지 (2026-09-03)

  • 기간: 2026-09-03 (세션 1개, 오너 보고 원문 "세트 상세 화면에서 아래로 드래그해서 내리면 모달이 그 위치에서 뜨는게 아니라 위쪽에 잘려서 뜸" → 오너 질문 "지금 계획이 땜질이 아니고 가장 근본적이고 구조적인 해결책이야?" → "구조적 트랙으로 가자, go")
  • 랜딩: PR #1217 (Phase 1~3, f8fa45e1) — 마이그레이션 없음, 서버 변경 없음, Vercel 배포 O(릴리스 v0.16.0 레인)
  • 설계서: 없음(수리 건). 분석·Phase 계획·예상 효과 = 이슈 #1214 댓글 1~2
  • 정본: src/react/ui/mobile/lib/Modal.tsx(UiModal — 가운데 팝업 공용 껍데기, 파일 머리 주석이 규칙)
  • 도구: 교체 스크립트(세션 스크래치, 커밋 제외 — 한 줄 opening + currentTarget 검사형은 자동, 나머지 5곳 수동) · 로그인 없는 스크래치 마운트(워크트리 scratch-1214/, 커밋 제외)
  • 게이트: tests/react/modalRootPortal.test.mjs(신설 — UiModalconfirm-scrim 직접 사용 금지) · check-typescript-unused(미사용 React import 1건 잡음)
  • 버그리포트: bug-report/bug-072-20260903.md
  • 계약: 없음(호출부 마크업·data-lg-hook 불변)

Phase 현황

Phase내용상태
Phase 1UiModal + 게이트(예외 목록) + 깨져 있던 12곳 교체(세션 상세·기록 상세·그룹 보드/라운지·손 포털 2곳 합침)✅ PR #1217
Phase 2나머지 21곳 교체, 게이트 예외 목록 비움(실패 수준), PrTools 달력을 폼 모달 안으로✅ PR #1217
Phase 3스크래치 마운트 전/후 실측 3껍데기 + 달력 중첩, npm run check, PR 1개·CI 1회✅ PR #1217 (f8fa45e1)

1. 배경

세트 스코어 가이드 팝업은 2026-09-03 세션 상세에 새로 붙은 설명 팝업(#1164 문구)이다. 오너가 같은 날 세션 상세에서 스크롤한 뒤 열면 팝업 윗부분이 화면 밖으로 나가는 스크린샷을 보고했다. 팝업은 앱 전체에 30여 곳 있고, 각자 자기 자리에서 어두운 배경(.confirm-scrim)을 그리는 방식이었다.

2. 문제 제기

2-1. 팝업 배경이 스크롤 내용에 붙어 있었다

유저 A가 일지 하루 상세 → 세션 상세로 들어가 C번 종목까지 내린 뒤 세트 스코어를 누르면, 팝업이 "지금 보는 화면"이 아니라 "스크롤 전 맨 위 화면" 기준으로 그려져 이미 밀려 올라간 자리에 뜬다. 배경은 position: absolute; inset: 0으로 가장 가까운 위치 기준 조상에 붙는데, 전체 화면 오버레이 껍데기(.pg-overlay)는 위치 기준이면서 스크롤 상자라 배경이 스크롤 내용 맨 위 한 화면에 고정된다. 실측(375×812, 끝까지 스크롤 후 열기): 카드 위쪽 y = −756px.

2-2. 같은 병이 9곳, 그리고 규칙이 없었다

.pg-overlay 안에서 그리는 팝업은 세션 상세 3(가이드·운동 시간 수정·삭제 확인)·기록 상세 4(등급·누적·1RM×2)·그룹 보드/라운지 3(달력·공지 작성·멤버) — 전부 같은 증상. 나머지 20여 곳이 정상인 것은 탭 화면(.screen)이나 드로어 안에 있어 우연히 위치 기준이 스크롤 상자 밖에 있어서였다. 진행 화면의 "기준 종목 선택" 팝업은 같은 문제를 겪고 2026-08-26에 개별적으로 루트 포털을 손으로 붙여 둔 상태 — 위치 결정 지점이 3곳(손 포털 2 + 없음)으로 흩어져 있었다.

3. 해결 방안

원칙 (오너 결정, 2026-09-03)

  • D1 — "지금 계획이 땜질이 아니고 가장 근본적이고 구조적인 해결책이야?" → 첫 계획(세션 상세 3곳만 루트 포털)을 스스로 "원인은 맞지만 범위는 땜질"로 판정하고 재조사. "구조적 트랙으로 가자, go" → 33곳 전체 + 게이트 채택.

접근

내용판단
.confirm-scrimposition: fixedCSS 한 줄기각 — 시트·오버레이 애니메이션의 transform이 fixed 기준을 가로채 찌그러짐(손 포털 주석에 기록된 함정), 30여 곳 전부 영향
.pg-overlay 스크롤을 안쪽 상자로껍데기 마크업 변경기각 — 오버레이 8곳 공유, 다음 "위치 기준 + 스크롤" 상자에서 재발
깨진 9곳만 루트 포털최소 범위기각 — 게이트를 실패 수준으로 못 올려 재발 차단이 안 됨
공용 껍데기 UiModal(앱 루트 포털) + 전부 교체 + 게이트위치 결정 1곳채택 — React가 포털을 제공하는 표준 방식, 재발 경로 소멸

4. 적용한 내용

Phase 1 — UiModal + 게이트 + 깨진 12곳 (#1217)

  • lib/Modal.tsx: <UiModal onScrimTap className data-lg-hook role aria-*>.confirm-scrim.app 루트에 createPortal. 배경 탭(target === currentTarget) 닫기는 껍데기가 판단, 조건부 닫기(!busy 등)는 호출부 콜백 안. document가 없는 서버 렌더는 제자리(서버 렌더러는 포털 미지원 — renderToStaticMarkup 테스트 6개가 여기 걸린다).
  • 교체: SessionSheetBody(운동 시간 수정·삭제 확인)·IntensityGuide·RecordsDetailGroupScreenWorkoutRecord 손 포털 2(ReactDOM import·ref 제거).
  • 게이트 modalRootPortal.test.mjs: 모바일 tsx 전수 스캔, UiModalclassName="confirm-scrim" → 실패. Phase 1에서는 아직 안 옮긴 13파일 예외 목록 + "옮겼는데 목록에 남으면 실패"(느슨해짐 방지).

Phase 2 — 나머지 21곳 + 게이트 승격 (#1217)

  • AccountDeleteFlow·FeedModeration(차단 확인·정지 안내)·WorkoutResumePrompt·FeedScreen·GroupComposerProfileScreen·PrToolsSessionScreen·WorkoutExtrasWorkoutFinishWorkoutFlowWorkoutRecordWorkoutRecordParts. 예외 목록 비움.
  • PrTools 1RM 입력 폼: 달력 시트(RcCalSheet, z 72)를 폼 모달 안으로. 모달이 루트로 나가면 형제 달력이 모달 배경(z 70) 아래 깔리므로. primitives.css 선택자 .prt-man .rc-calscrim.confirm-scrim .rc-calscrim.

Phase 3 — 검증 (#1217)

  • 스크래치 마운트(워크트리 scratch-1214/index.html + main.tsx, 실제 CSS 매니페스트 + 실제 UiIntensityGuide, ?shell=session|records|group|prtools&mode=before|after): 오버레이 끝까지 스크롤 → 팝업 열기 → getBoundingClientRect 실측. 아래 5절.
  • npm run check 통과(테스트 2488), check-typescript-unused 통과, CI 1회(모바일 화면 공용 코드).

주요 결정과 그 근거

  • CSS는 손대지 않는다.confirm-scrim·.pg-overlay 그대로. 껍데기 구조를 바꾸면 공유 레이아웃 8곳에 부작용, 규칙은 "팝업이 루트에서 그려진다"가 맞다.
  • 호출부 마크업 불변 — 카드·클래스·data-lg-hook은 그대로 두고 배경 한 줄만 바꾼다(33곳 diff가 각 2줄 + import). e2e 선택자 변화 0.
  • 게이트를 두 단계로 — Phase 1 예외 목록 → Phase 2 비움. 중간 커밋도 게이트가 참이 되게.

작업 중 드러난 것

  • z-index는 루트 기준으로 다시 봐야 한다 — 팝업 70 vs 종목 픽커 시트 80. 픽커 안에서 여는 팝업 3곳은 이미 90(wfx-dtl-scrim·wfx-pickone-scrim·wfx-hist-scrim)이라 통과. 형제 관계로 팝업 위에 얹던 것(PrTools 달력 72)은 루트로 나간 팝업 아래 깔린다 — 팝업 안으로 넣어야 한다.
  • CRLF 워크트리 — 여러 줄 문자열 치환은 \r\n을 먼저 \n으로 정규화하고 되돌린다. Bash에서 sed로 스크립트의 백슬래시를 고치면 망가진다(Write 도구로 다시 씀).
  • 리베이스 뒤 인덱스가 전부 삭제(D)+미추적(??)으로 보이면 git reset(mixed)으로 인덱스만 재구성 — 작업 트리는 무사하다.
  • renderToStaticMarkup 테스트가 팝업 hook을 단언한다(workoutDraftRestorePrompt) — 서버 렌더 제자리 fallback이 없으면 깨진다.

5. 적용 결과

항목결과
세션 상세 .sj-mo.pg-overlay, 1070px 스크롤 후 세트 스코어 가이드(실제 컴포넌트) 카드 위쪽 y−756px(화면 밖) → 184px(가운데, 카드 184~628 / 화면 812)
기록 상세 .rc-det.pg-overlay, 등급 팝업−734px → 336px
그룹 .gp-full.pg-overlay, 공지 작성 팝업−801px → 319px
팝업 배경의 부모.pg-overlay.app (3껍데기 모두)
PrTools 폼 모달 + 달력달력이 카드 위(elementFromPoint = 달력), 달력 배경 탭 → 달력만 닫힘, 모달 배경 탭 → 모달 닫힘
팝업 위치 결정 지점3곳(손 포털 2 + 없음) → 1곳(UiModal)
confirm-scrim 직접 사용33곳 → 0곳, 게이트가 새 사용을 실패시킴
로컬 게이트npm run check 2488 통과, 미사용 심볼 게이트 통과
미검증실기기 시각 확인(§14 게이트 아님). z-index는 표로 점검했고 모든 화면 조합을 눌러 본 것은 아님

6. 이번 개선으로 향상된 것

스크롤한 오버레이에서 연 팝업이 화면 가운데에 뜬다

세션 상세·기록 상세·그룹 보드/라운지의 팝업 9곳이 스크롤 위치와 무관하게 보이는 화면 가운데에 뜬다.

구조적으로 남는 것

  • 계약: "가운데 팝업은 어디서 열든 앱 루트에서 그린다" — UiModal 한 곳이 담당. 새 오버레이·시트를 만들어도 팝업 위치를 신경 쓸 필요가 없다.
  • 게이트: modalRootPortal.test.mjs — 화면 코드가 .confirm-scrim을 직접 쓰면 npm run check·CI가 막는다.
  • 절차: 로그인 없는 스크래치 마운트로 오버레이 안 팝업 위치를 재는 방법(4절 Phase 3).

남은 것

  • 릴리스 v0.16.0 머지 → [v0.16.0 반영완료], 이슈 종결.
  • 아래서 올라오는 시트(.sheet-scrim)는 같은 문제가 없었지만 같은 원리(제자리 그리기)다. 오버레이 안에서 시트를 여는 화면이 생기면 같은 껍데기 방식을 검토(범위 밖).