Files
GhiVideo/docs/history/2026-07-02_POI라벨사라짐-팝업재등장-수정.md
T
b23042andClaude Opus 4.8 e0ff5dd6d0 feat: 지도 나침반(OSM/위성) 추가 + 측점 검색·마커 정확도 개선 + POI/영상 수정
- 지도 기반 나침반(노스업, 현재위치, 시야 역삼각형): 호버 확대·휠 줌·클릭 위성전환, OSM/Esri 타일 서버 프록시(/api/tile)
- 스테이션 검색: 실제 측점(chain) 기준 이동, 없으면 '측점 없음' 안내
- 역 마커: 직교 투영 측점 일치 시에만 표시, 미도착 종점은 추가 방식
- POI 팝업 겹침/재등장·라벨 정합 수정
- 영상 fps 데이터 기반 자동 산출
- 기술/발표/쉬운설명 문서 추가

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 18:03:30 +09:00

104 lines
7.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 프로덕션 반영 (브라우저 강력 새로고침 필요)