# POI 라벨 사라진 뒤 팝업 재등장 수정

**소요 시간**: 약 10분
**Context 사용량**: input ~80k / output ~3k tokens

## 문제
POI 라벨이 화면 밖으로 사라졌는데, 방금 전 위치에 POI 팝업이 다시 뜨는 문제.

## 원인
[client/src/components/overlay/StationOverlay.tsx](../../client/src/components/overlay/StationOverlay.tsx) 의
마커 그리기 루프(1114행)가 라벨이 화면(비디오 크롭) 밖으로 나가도 `cc.Zc >= CLIP_Z` 이기만 하면
매 프레임 `visStructRef` 에 계속 등록.
- 팝업 제거 로직(RAF): 라벨이 off-container 상태 4프레임이면 팝업 제거.
- 자동 sync 인터벌(150ms): `vis.has(title)` 이 아직 true → 팝업을 즉시 다시 추가.
→ 둘이 싸워 팝업이 마지막 위치에 되살아남.

## 수정
`visStructRef.set` 을 팝업 off 판정과 **동일한 화면 범위**(`px/py ∈ [-16, W+16]/[-16, H+16]`)로 제한.
`vx(nx)=offX+nx*dispW` 는 팝업 off-check 의 `sx=offX+disp.x*dispW` 와 같은 좌표계라 일치.
→ 라벨이 화면 밖이면 vis 에서도 빠져 sync 가 되살리지 않음(제거 로직과 일관).

## 추가 수정 (잔여 1프레임 번쩍임)
`visStructRef` 가드만으론 sync 인터벌이 팝업 DOM 을 **새로 mount** 하는 순간의 번쩍임이 남음:
새 팝업 요소는 React 초기 스타일(`top: sy+27`)로 **보이는 상태**로 붙고, 다음 RAF 가
'화면 밖→숨김' 판정을 내리기 전 한 프레임 stale 위치에 번쩍임.

→ 팝업 ref 콜백에서 `popupPosRef`(RAF 위치 확정 시 기록)에 아직 없는 '신규' 팝업은
`visibility:hidden` 으로 mount. RAF 가 온스크린 확인 후에만 visible 로 전환(off 면 계속 숨김→제거).
visibility 는 RAF 명령형으로만 관리(style prop 미포함)라 재렌더에도 유지됨.

## 추가 수정 2 (hidden-mount 가드 무력화 — 진짜 원인)
hidden-mount 가드는 `!popupPosRef.has(id)` 일 때만 숨긴다. 그런데 팝업 제거 경로가 여럿인데
보조 맵(popupPosRef/popupMissRef) 정리는 **RAF off-4프레임 경로만** 했음.
→ sync `toRemoveIds` / 수동(✕·ESC·빈곳클릭) 제거 시 `popupPosRef` 잔존 → 같은 title 재추가 시
`popupPosRef.has=true` → 숨김 안 됨 → React 초기 스타일(`top: sy+27`)로 **보이는 채 mount** →
라벨 없이 팝업만 stale 위치에 나타남(+ RAF 가 stale 위치에서 EMA 로 미끄러짐).

수정:
1. `infoPopups` 변경 effect 에서 사라진 id 의 보조 맵 일괄 정리(단일 출처).
2. sync `toRemoveIds` 처리 시 즉시 `popupPosRef`/`popupMissRef` 정리(재추가 전에 확실히 비움).

## 추가 수정 3 (definitive — 라벨 가시성과 완전 결합)
증상이 계속 동일해 근본 재분석: 팝업 off 판정이 라벨 **십자마커 중심** ±16px 슬랙 + 화면 안 **clamp**
조합이라, 마커가 경계 근처(예: 오른쪽 끝 밖 ~16px)면 라벨 텍스트는 화면 밖(안 보임)인데
팝업만 clamp 되어 화면 안에 뜸 → "라벨 없이 팝업만".

수정: 이번 프레임에 라벨이 **실제로 화면 안에 그려졌는지**(히트박스∩뷰포트)를 `onScreenLabels`
집합으로 만들고, 팝업 표시/visStruct 등록을 이 집합에만 의존하도록 통일.
- `off = !disp || !onScreenLabels.has(id)` (좌표 슬랙·clamp 기반 판정 제거)
- visStruct 등록도 `poiVisible`(히트박스 온스크린) 기준

## 추가 수정 4 (사라졌다 잠깐 재등장 → 완전 제거)
증상: 라벨 사라짐 → 팝업 사라짐 → 잠깐 재등장 → 사라짐. 원인은 sync(150ms 추가) ↔ RAF(제거) 충돌.
- 경계에서 라벨이 1~2프레임 깜빡이면 visStruct 재등록 → sync 가 팝업 재생성 → RAF 가 다시 제거.

수정:
1. 추가 히스테리시스: 라벨이 **연속 ADD_STREAK(6프레임 ~100ms)** 이상 화면 안일 때만 visStruct 등록
   (poiOnScreenStreakRef). 짧은 깜빡임으론 팝업 생성 안 됨.
2. 제거 단일화: sync 의 팝업 제거 로직 삭제 → **RAF(onScreenLabels + 4프레임 유예)가 유일한 제거 권한**.
   add/remove 충돌 제거 → 라벨 사라지면 팝업도 사라진 뒤 재생성 안 됨.

## 추가 수정 5 (진짜 리그레션 원인 — flip 왕복)
사용자 지적: 이 증상은 '하단 겹침' 수정(추가수정 없음, 첫 위치 수정) 이후 생긴 리그레션.
→ 가시성 문제가 아니라 flip(아래↔위 전환) 로직에 **히스테리시스가 없어서** 발생.
라벨이 하단 flip 임계값 부근이면 드론 흔들림/평활로 `sy` 가 미세하게 오르내릴 때마다
`ty+ph > H-2` 가 참↔거짓 왕복 → 팝업이 아래↔위로 **순간이동(bigJump 스냅)** → "사라졌다 나타났다".

수정: `popupFlipRef`(위/아래 상태) + 히스테리시스(HYST=48px). '위'로 간 뒤엔 아래에 48px 이상
여유가 생겨야만 '아래'로 복귀 → 경계 왕복 제거. flip 상태도 제거 경로에서 정리.

## 추가 수정 6 (하단 이탈 즉시 제거 — 유예 삭제)
요구: 라벨이 하단으로 벗어나면 팝업 즉시 사라지고 재등장 금지.
잔여 원인: 제거에 4프레임 '유예'가 있어, 그 사이 흔들림으로 라벨이 경계로 잠깐 되들어오면
RAF 가 아직 안 지워진 팝업을 다시 visible 로 바꿈 → "사라졌다 잠깐 나타났다".

수정: off(라벨 화면 밖) 판정 시 유예 없이 **즉시 toRemove**(+숨김). 재등장은 sync 의
ADD_STREAK(6프레임 연속 안정) 게이트가 막음 → 짧은 복귀로는 재생성 안 됨.
mid-screen 은 라벨 히트박스가 가장자리에서 멀어 off 가 안 나므로 영향 없음.

## 추가 수정 7 (하단 기준 = 영상 영역(스테이션바 위), 모니터 아님)
요구: 팝업이 스테이션바에 걸치지 않게, 라벨이 바 밑으로 내려가면 팝업 제거. 기준은 영상 재생 영역.
- 스테이션바는 영상 위 z-20 오버레이(absolute bottom-0), 팝업은 z-40 → 바 위에 걸쳐 보였음.
- 유효 하단 `HB = H − barHeight` 도입(barHeightRef, ResizeObserver 실측 ~130px).
  - `labelOnScreen` 하단 기준 `H-2` → `HB-2`: 라벨 히트박스가 바 밑이면 off → 즉시 제거.
  - 팝업 flip/clamp 하단 기준도 `H` → `HB`: 팝업이 바 위로만 배치(걸침 방지).

## 추가 수정 8 (flip 제거 + 바 뒤로 슬라이드)
요구: 팝업이 라벨 위로 flip하면 안 됨. 항상 라벨 아래에 있다가 라벨이 바 밑으로 사라질 때 같이 사라짐.
- flip/clamp 둘 다 문제였음(flip=위로 튐, clamp=바에 걸쳐 멈춤).

수정:
1. flip·하단 clamp 제거 → 팝업 `ty = sy + LABEL_HALF + GAP` (항상 라벨 아래, 자유 하강).
2. 팝업들을 클립 컨테이너로 감쌈: `absolute left-0 right-0 top-0 overflow-hidden` + `bottom: barHeight`
   (높이 = 영상영역 HB). 팝업이 라벨 따라 내려가면 바 영역에서 클립돼 '바 뒤로 슬라이드'되어 사라짐.
3. 제거는 label off(HB 기준) 그대로 → 라벨이 바 밑이면 팝업 제거.

## 배포 확인
- :55000 서빙 index.html → `assets/index-yfJyCKp1.js` (이번 빌드 해시와 일치)
- ecosystem: ghiVideo, cwd=프로젝트루트, clientDist=client/dist (정적 즉시 반영)

## 검증
- `tsc --noEmit` 통과
- `npm run build` 성공 → :55000 프로덕션 반영 (브라우저 강력 새로고침 필요)
