가운데 팝업 루트 배치 일원화 — 세트 스코어 가이드가 스크롤 뒤 위쪽에 잘리던 것에서 팝업 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(신설 —UiModal밖confirm-scrim직접 사용 금지) ·check-typescript-unused(미사용Reactimport 1건 잡음) - 버그리포트:
bug-report/bug-072-20260903.md - 계약: 없음(호출부 마크업·
data-lg-hook불변)
Phase 현황
| Phase | 내용 | 상태 |
|---|---|---|
| Phase 1 | UiModal + 게이트(예외 목록) + 깨져 있던 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-scrim을 position: fixed | CSS 한 줄 | 기각 — 시트·오버레이 애니메이션의 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·RecordsDetail4·GroupScreen3·WorkoutRecord손 포털 2(ReactDOM import·ref 제거). - 게이트
modalRootPortal.test.mjs: 모바일 tsx 전수 스캔,UiModal밖className="confirm-scrim"→ 실패. Phase 1에서는 아직 안 옮긴 13파일 예외 목록 + "옮겼는데 목록에 남으면 실패"(느슨해짐 방지).
Phase 2 — 나머지 21곳 + 게이트 승격 (#1217)
AccountDeleteFlow·FeedModeration(차단 확인·정지 안내)·WorkoutResumePrompt·FeedScreen·GroupComposer3·ProfileScreen·PrTools2·SessionScreen·WorkoutExtras2·WorkoutFinish2·WorkoutFlow4·WorkoutRecord5·WorkoutRecordParts. 예외 목록 비움.- 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)는 같은 문제가 없었지만 같은 원리(제자리 그리기)다. 오버레이 안에서 시트를 여는 화면이 생기면 같은 껍데기 방식을 검토(범위 밖).