Skip to content

Commit 0127e00

Browse files
zeakdclaude
andauthored
fix: 컴포넌트 binding audit + 14건 수정 + props 11개 추가 (#157)
* fix: 컴포넌트 binding audit + 14건 수정 + props 11개 추가 전체 컴포넌트의 props ↔ SDK 결합 audit 결과를 반영. SDK 옵션 저장 패턴(A: _mapOptions 분리, B: setX 부수효과, C: _<key>_changed, D: 일방향/static)을 정밀화하고, 발견된 버그를 수정하며, 누락 옵션을 노출. 핵심: - useControlledKVO/useKVO가 Map의 _mapOptions로 정확히 라우팅 - setter 우선순위 setX > setOptions > set (mapTypeId registry 전환 등) - onInit/onIdle/onTilesloaded을 useLayoutEffect로 이동 (init 발화 누락 방지) - InfoWindow를 per-key useControlledKVO로 전환 (인라인 객체 폭주 방지) - TrafficLayer autoRefresh undefined 가드 - GroundOverlay 미발화 이벤트 타입 축소 - CustomOverlay equality + pane static 명시 - logoControl 일방향 controlled 분류 정밀화 - 신규 props: cursor/tilt/rotation/tileDuration/repeatX/gl/customStyleId/useStyleMap/crossOrigin/simplifyLevel - 신규 internal hook useStaticProp (dev mode warn) 회귀 테스트 121 -> 147개. CompassControl은 SDK 클래스 정의 부재로 보류 결정. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: 코드 리뷰 후속 — A/B/C/D 그룹 (147 -> 160 tests) 리뷰에서 도출된 6개 Major + 8개 Minor 항목 처리. Group A (안전한 즉시 수정): - useStaticProp: dev/prod 함수 선택 패턴으로 production DCE 가능 (effect 슬롯 0) - kvoEquals: 양쪽 어느 쪽이든 equals 있으면 시도 (literal vs instance 비대칭 해소) - logoControl false warn: ref 가드로 한 번만 출력 + 회귀 테스트 3건 Group B (측정으로 검증): - NaverMap 38개 useControlledKVO: 단일 토글 36 get + 8 set, 16ms 내. 이론적 우려 대비 실측 OK - InfoWindow per-key 동등성: bulk vs per-key 모두 1 draw + 1 resize. SDK _drawRequired 플래그가 중복 draw 방지 Group C (이벤트 등록 시점 정책): - InfoWindow onOpen/onClose를 useLayoutEffect로 이동. open() 호출 직후 동기 발화하는 'open' 이벤트를 listener 미등록으로 놓치는 timing bug 해소 (fix-13과 동일 원리) - 정책 명문화: SDK 동기 발화 = useLayoutEffect, 사용자 인터랙션 = useEffect Group D (Minor 정리): - console.warn 메시지 영문 우선 + 한국어 보조 (한영 병기) - StrictMode 더블 마운트 회귀 테스트 7건 추가 - *Options 런타임 audit (zoomControlOptions 등 5개) — 모두 controlled 동작 확정 - useNaverMapStatic vs satisfies 패턴 정책 명시 (3+ 호출 wrapper, 1-2 satisfies) 테스트: 147 -> 160 (+13). 빌드 통과. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: 2차 리뷰 후속 — fix-16 catch 보강 + headless 잠금 + audit 노트 2차 리뷰 결과 처리: 1. fix-16 회귀 테스트 catch 능력 보강 — 분석 결과 솔직히 인정 - test-utils의 mock InfoWindow.open()이 'open' 이벤트 동기 발화하도록 수정 - 테스트가 onOpen 호출을 직접 검증 - 결론: 같은 컴포넌트 내 hook 등록 순서가 React에서 보장되므로 useLayoutEffect/useEffect 차이가 사실상 없음. fix-16은 방어적 보수책. fixes/16에 한계 + 의도 명시 2. vitest.config.ts headless 잠금 - PWDEBUG/HEADED 환경변수 명시 차단 - browser.headless: true + launch.headless: true 이중 명시 - launch.devtools: false 3. SDK 동기 발화 이벤트 audit (미커밋, research/bindings/sync-fired-events-audit.md) - OverlayView의 added/removed/draw 이벤트는 우리 prop으로 미노출 → 영향 없음 - 모든 노출 props가 적정 timing으로 처리됨 확인 테스트: 25 files, 160 tests 통과. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ci: setup-bun 추가 — astro-component-docs test가 bun 의존 PR #152(commit 4a12e35)에서 astro-component-docs의 test 스크립트가 'bun test src/'로 변경되었으나 ci.yml에 bun 설치 단계가 누락되어 CI가 지속적으로 실패. setup-bun@v2 추가. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ci: Playwright Chromium 설치 단계 추가 vitest browser mode가 Playwright Chromium 헤드리스 브라우저를 launch하므로, 'pnpm exec playwright install --with-deps chromium' 단계 추가. 이전엔 호스트 환경에 설치되어 있어서 우회됐을 가능성. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ci: playwright install을 react-naver-maps workspace로 scope * fix: GroundOverlay dblclick 이벤트 복원 SDK는 GROUND_DOMEVENTS의 click 리스너에서 250ms·5px 이내 연속 클릭을 _getSingleClickType(maps.beautified.js:9014)으로 합성 dblclick으로 trigger한다. 이전 타입 축소 라운드가 이를 미발화로 오판해 onDblclick을 제거했으나, deobfuscation 재검증 결과 실제 발화하므로 복원한다. mouseover/mouseout/mousemove 제거는 유지(GROUND_DOMEVENTS 미포함). - GroundOverlayEvent에 'dblclick' 추가 + 근거 주석 - ground-overlay.tsx onDblclick 리스너/deps 복원 - 5종 이벤트 바인딩 런타임 회귀 테스트 추가 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test: event-types 타입 테스트를 실제로 평가하도록 typecheck 활성화 event-types.spec.ts는 expectTypeOf/toEqualTypeOf만 쓰는 순수 타입 테스트인데, vitest.config에 typecheck 설정이 없고 tsconfig가 spec을 exclude해 런타임 no-op로 0 assertion 통과해왔다(dead test). GroundOverlay dblclick 오제거를 잡지 못한 원인. - vitest.config.ts: test.typecheck 활성화 (event-types.spec.ts 평가) - tsconfig.test.json: typecheck 전용 (메인 tsconfig는 빌드 위해 spec exclude 유지) - event-types.spec.ts: dblclick 포함 5종으로 단언 정정 검증: dblclick 임시 제거 시 typecheck가 FAIL 처리함을 확인. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix: tileDuration static 재분류 + cursor/autoRefresh 동작 문서화 - tileDuration: controlled → static. SDK에 setTileDuration/tileDuration_changed가 없고 타일 생성 시에만 읽히므로(maps.beautified.js:5568/5642/6468) 런타임 변경이 무시된다. useControlledKVO → useNaverMapStatic으로 이동. - cursor: SDK가 드래그 중 손 커서로 자체 토글하고 초기화 시 setCursor("open")을 호출해 controlled 값과 충돌할 수 있음을 JSDoc에 명시. - TrafficLayer autoRefresh: undefined는 정지가 아니라 'SDK 기본값 위임'이며, true→undefined로는 멈추지 않고 false를 명시해야 정지함을 JSDoc에 명시. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test: false-positive 테스트 5건 보강 코드 리뷰에서 '수정 전에도 통과 / 얕은 검증'으로 지적된 테스트를 실제 회귀를 잡도록 보강. 각 항목은 대상 코드를 임시로 깨뜨려 FAIL을 확인한 뒤 복원해 검증했다. - custom-overlay: fromCoordToOffset을 좌표 의존으로 + setPosition spy (기존엔 고정 좌표라 draw 미호출에도 통과하는 동어반복) - info-window: open/close 분기를 toBeDefined 대체 → spy로 호출/인자 단언. fix-16 ordering은 hook 선언 순서상 layout/passive 구분 불가라 검증 범위를 'onOpen 호출 + 정확한 인스턴스/event명 등록'으로 정직하게 하향. - use-kvo: removeListener no-op mock → 실제 제거 + unmount 후 콜백 미호출 단언 - strict-mode: 더블마운트 발생(toHaveLength(2)) + 첫 인스턴스 cleanup 단언 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs: KVO 훅 read/write 비대칭 전제 주석 + changeset 정정 + gitignore - use-controlled-kvo / use-kvo: 읽기 경로와 쓰기/구독 경로의 대상 선택 조건이 서로 달라 'SDK가 동일 슬롯에 라우팅한다'는 전제에 의존함을 주석화. (동작 변경 아님 — 미래 SDK 변경 시 주의점 명시) - changeset: dblclick 유지/tileDuration static/테스트 카운트 정정 - gitignore: __screenshots__ (vitest 산출물), examples/ (작업 중 — stash 보관, 별도 브랜치로 분리 예정. lint 오염 방지) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ci: playwright install --with-deps 제거 + concurrency/timeout playwright install --with-deps(sudo apt-get install)가 GitHub 호스티드 ubuntu에서 apt 락으로 무한 hang(35분+ 누적 run 다수). 호스티드 러너는 chromium 런타임 라이브러리를 대부분 사전 포함하므로 --with-deps 제거. - --with-deps 제거 (필요 lib 누락 시 test 단계에서 빠르게 드러남) - step timeout-minutes: 10 (재발 시 빠른 실패) - concurrency cancel-in-progress: 중복/hang run 자동 취소 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test: 단위 테스트를 happy-dom으로 전환, CI에서 Playwright 제거 CI의 playwright install이 chromium 다운로드 직후(extract 추정) 무한 hang하는 문제가 반복됐다(--with-deps 무관, 다운로드 자체는 2초). 단위 테스트 171개는 vi.mock 기반이라 real 브라우저가 불필요하므로 happy-dom으로 전환한다. - vitest.config.ts: browser mode → environment 'happy-dom', smoke 제외 - vitest.browser.config.ts: smoke 전용 분리(실제 SDK 로드 통합 테스트, real 브라우저 필수) - package.json: test:browser 스크립트 + happy-dom devDep - ci.yml: playwright install 단계 완전 제거 (hang 원천 차단, CI 고속화) 검증: 기본 test 181 passed(happy-dom) + typecheck OK, test:browser 3 passed(smoke). smoke 통합 테스트는 로컬/릴리스 시 pnpm test:browser로 검증. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 2687a95 commit 0127e00

33 files changed

Lines changed: 2046 additions & 184 deletions

.changeset/bindings-audit-0-2-1.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
---
2+
"react-naver-maps": patch
3+
---
4+
5+
전체 컴포넌트 binding audit 라운드. props ↔ SDK 결합의 일관성을 정밀화하고, 발견된 버그를 수정하며, 누락된 옵션을 노출.
6+
7+
### 라이브러리 코어
8+
- `useControlledKVO`: setter 우선순위를 `setX > setOptions > set`로 재정의. Map의 옵션이 별도 `_mapOptions`에 저장되는 경우(draggable, scaleControl, mapTypeId 등 거의 모든 NaverMap 옵션)를 정확히 라우팅. 기존엔 `target.set(key, value)` 직접 호출이라 SDK가 무시하던 케이스 다수.
9+
- `useKVO`: Map 옵션 키 구독 시 `_mapOptions`로 listener/getter 라우팅. `useKVO(map, 'draggable')` 등이 정상 발화 수신.
10+
11+
### 컴포넌트 버그 수정
12+
- NaverMap: `minZoom`/`maxZoom`을 static에서 controlled로 이동. 런타임 변경 가능.
13+
- NaverMap: `onInit/onIdle/onTilesloaded` 등록을 `useEffect``useLayoutEffect`로 이동. SDK 동기 발화 케이스에서 init 누락 방지.
14+
- TrafficLayer: `autoRefresh` 미지정 시 `endAutoRefresh` 호출하지 않음 (이전엔 강제 비활성화).
15+
- InfoWindow: `setOptions(obj)` 일괄 호출을 per-key `useControlledKVO`로 전환. 인라인 객체 props 시 폭주 방지.
16+
- GroundOverlay: SDK가 발화하지 않는 mouseover/mouseout/mousemove 이벤트 prop 제거 (타입 축소). `dblclick`은 click 리스너가 연속 클릭을 합성 발화하므로 유지.
17+
- CustomOverlay: `pane` static 명시. position/anchor를 `kvoEquals` 비교 후 호출.
18+
- NaverMap: `logoControl` 분류 정밀화 — SDK가 `false`를 거부하는 일방향 controlled. `false` 시도 시 dev 안내.
19+
20+
### 신규 controlled props
21+
- NaverMap: `cursor` (드래그 중 SDK 자체 토글과 충돌 가능 — JSDoc 주의 명시), `tilt` (+`onTiltChanged`), `rotation` (+`onRotationChanged`)
22+
- NaverMap static: `repeatX`, `gl`, `customStyleId`, `useStyleMap`, `tileDuration` (SDK에 setter/`_changed` 없이 타일 생성 시에만 읽힘)
23+
- GroundOverlay: `crossOrigin`
24+
- Polygon/Polyline: `simplifyLevel`
25+
26+
### 개발자 경험
27+
- 신규 internal hook `useStaticProp` — static prop이 마운트 후 변경되면 dev에서 console.warn. production 빌드는 dead-code-elimination. undefined grace period로 비동기 prop 패턴 false positive 회피.
28+
29+
### 예제
30+
- `examples/map-types`: `MapTypeId` enum 실값 사용 (대문자 → 소문자).
31+
- `examples/map-options`: `kineticPan` 초기값을 SDK 기본과 일치.
32+
- `examples/map-geolocation`: 공식 tutorial 동작과 정렬 (`open` prop 명시 + 콘텐츠 스타일).
33+
34+
### 회귀 테스트
35+
- 121개 → **174개** 통과. 모든 fix 시나리오 커버. `event-types.spec.ts`는 vitest `typecheck`로 실제 평가(이전엔 런타임 no-op).
36+
37+
### Breaking 가능성 (사실상 없음)
38+
- GroundOverlay 이벤트 타입 축소: 기존 코드에서 `onMouseover`/`onMouseout`/`onMousemove`를 사용했더라도 SDK가 발화하지 않았으므로 실제 동작 변화 없음. (`onDblclick`은 유지)
39+
- CustomOverlay `pane` 변경: 이전에도 무시되던 동작이 이제 dev 경고로 명시.

.github/workflows/ci.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ on:
44
pull_request:
55
branches: [main]
66

7+
# 같은 브랜치의 이전 run을 자동 취소 — hang/중복 run 누적 방지
8+
concurrency:
9+
group: ci-${{ github.ref }}
10+
cancel-in-progress: true
11+
712
permissions:
813
contents: read
914

@@ -17,7 +22,13 @@ jobs:
1722
with:
1823
node-version: 24
1924
cache: pnpm
25+
# astro-component-docs의 test 스크립트가 bun 사용 (bun test src/)
26+
- uses: oven-sh/setup-bun@v2
27+
with:
28+
bun-version: latest
2029
- run: pnpm install --frozen-lockfile
30+
# 단위 테스트는 happy-dom 환경(vi.mock 기반)이라 Playwright/브라우저 설치가 불필요하다.
31+
# 실제 SDK를 로드하는 smoke 통합 테스트(test:browser)는 별도이며 CI에서 돌리지 않는다.
2132
- run: pnpm -r run build
2233
- run: pnpm -r run lint
2334
- run: pnpm -r run test

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,3 +87,9 @@ research/
8787
test-results/
8888
__tile_cache__/
8989
screenshots/CLAUDE.md
90+
91+
# Vitest browser mode 산출물
92+
__screenshots__/
93+
94+
# 통합 예제 (작업 중 — 별도 브랜치/PR로 분리 예정. stash 보관)
95+
examples/

packages/react-naver-maps/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
"build": "tsc -b",
3232
"test": "vitest run",
3333
"test:watch": "vitest",
34+
"test:browser": "vitest run --config vitest.browser.config.ts",
3435
"lint": "oxlint",
3536
"lint:fix": "oxlint --fix",
3637
"fmt": "oxfmt .",
@@ -49,6 +50,7 @@
4950
"@types/react-dom": "^19.0.0",
5051
"@vitejs/plugin-react": "^5.1.4",
5152
"@vitest/browser": "^3.0.0",
53+
"happy-dom": "^20.9.0",
5254
"oxfmt": "^0.36.0",
5355
"oxlint": "^1.51.0",
5456
"playwright": "^1.58.2",

packages/react-naver-maps/src/__tests__/custom-overlay.spec.tsx

Lines changed: 122 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { render } from '@testing-library/react';
22
import { describe, test, expect, beforeEach, afterEach, vi } from 'vitest';
3-
import type { ReactNode } from 'react';
3+
import { createRef, type ReactNode } from 'react';
44
import { createMockNaverMaps, type MockKVO } from './test-utils.js';
55

66
vi.mock('../hooks/use-navermaps.js', () => ({
@@ -80,37 +80,53 @@ describe('CustomOverlay 스펙 테스트', () => {
8080
});
8181

8282
test('position 변경 시 draw 재호출 (left/top 업데이트)', async () => {
83+
const ref = createRef<naver.maps.OverlayView>();
84+
8385
const { rerender } = render(
8486
<Wrapper>
85-
<CustomOverlay position={{ lat: 37.5, lng: 127.0 }}>
87+
<CustomOverlay position={{ lat: 37.5, lng: 127.0 }} ref={ref}>
8688
<span data-testid="pos-child">content</span>
8789
</CustomOverlay>
8890
</Wrapper>,
8991
);
9092

9193
await vi.waitFor(() => {
94+
expect(ref.current).not.toBeNull();
9295
const child = document.querySelector('[data-testid="pos-child"]');
9396
expect(child).not.toBeNull();
9497
const container = child!.parentElement!;
95-
// fromCoordToOffset mock은 항상 { x: 100, y: 200 }을 반환
98+
// fromCoordToOffset mock: lat=37.5,lng=127 → { x: 100, y: 200 }
9699
expect(container.style.left).toBe('100px');
97100
expect(container.style.top).toBe('200px');
98101
});
99102

103+
// 실제 draw 재호출을 인스턴스 메서드 spy로 단언 (setPosition → draw 경로).
104+
const setPositionSpy = vi.spyOn(
105+
ref.current as unknown as { setPosition: (p: unknown) => void },
106+
'setPosition',
107+
);
108+
100109
rerender(
101110
<Wrapper>
102-
<CustomOverlay position={{ lat: 38.0, lng: 128.0 }}>
111+
<CustomOverlay position={{ lat: 38.0, lng: 128.0 }} ref={ref}>
103112
<span data-testid="pos-child">content</span>
104113
</CustomOverlay>
105114
</Wrapper>,
106115
);
107116

117+
// setPosition이 새 좌표로 호출되어야 함 (draw가 재실행되는 트리거)
118+
await vi.waitFor(() => {
119+
expect(setPositionSpy).toHaveBeenCalledWith({ lat: 38.0, lng: 128.0 });
120+
});
121+
122+
// left/top이 새 좌표 기반 값으로 갱신 (draw 미재호출이면 100/200 그대로 → FAIL)
123+
// lng=128 → x=100+(128-127)=101, lat=38 → y=200+(38-37.5)=200.5
108124
await vi.waitFor(() => {
109125
const child = document.querySelector('[data-testid="pos-child"]');
110126
expect(child).not.toBeNull();
111127
const container = child!.parentElement!;
112-
expect(container.style.left).toBe('100px');
113-
expect(container.style.top).toBe('200px');
128+
expect(container.style.left).toBe('101px');
129+
expect(container.style.top).toBe('200.5px');
114130
});
115131
});
116132

@@ -226,4 +242,104 @@ describe('CustomOverlay 스펙 테스트', () => {
226242
expect(pane).not.toBeNull();
227243
});
228244
});
245+
246+
/**
247+
* fix-10: position에 .equals()를 가진 객체(LatLng 인스턴스)를 같은 좌표로 다시 전달하면
248+
* 참조가 다르더라도 kvoEquals(equals 호출)로 변경 없음으로 판정해 setPosition을 호출하지 않아야 한다.
249+
*
250+
* test-utils의 MockLatLng은 .equals()가 없으므로, 이 테스트에서는 .equals()를 가진
251+
* 별도 LatLng-like 객체로 검증한다.
252+
*/
253+
test('동일 좌표 LatLng(.equals) 재전달 시 setPosition 미호출 (fix-10)', async () => {
254+
class LatLngLike {
255+
constructor(
256+
public lat: number,
257+
public lng: number,
258+
) {}
259+
equals(other: { lat: number; lng: number }) {
260+
return this.lat === other.lat && this.lng === other.lng;
261+
}
262+
}
263+
264+
const ref = createRef<naver.maps.OverlayView>();
265+
266+
const p1 = new LatLngLike(37.5, 127.0) as unknown as naver.maps.LatLng;
267+
268+
const { rerender } = render(
269+
<Wrapper>
270+
<CustomOverlay position={p1} ref={ref}>
271+
<span data-testid="eq-child">content</span>
272+
</CustomOverlay>
273+
</Wrapper>,
274+
);
275+
276+
await vi.waitFor(() => {
277+
expect(ref.current).not.toBeNull();
278+
});
279+
280+
// ref로 받은 인스턴스에 spy 설치 (prototype 메서드를 instance level로 가로챔)
281+
const setPositionSpy = vi.spyOn(
282+
ref.current as unknown as { setPosition: (p: unknown) => void },
283+
'setPosition',
284+
);
285+
286+
// 같은 좌표를 가진 새 LatLngLike 인스턴스 (참조는 다름, equals는 true)
287+
const p2 = new LatLngLike(37.5, 127.0) as unknown as naver.maps.LatLng;
288+
289+
rerender(
290+
<Wrapper>
291+
<CustomOverlay position={p2} ref={ref}>
292+
<span data-testid="eq-child">content</span>
293+
</CustomOverlay>
294+
</Wrapper>,
295+
);
296+
297+
// kvoEquals(p1, p2) === true이므로 setPosition 미호출
298+
expect(setPositionSpy).not.toHaveBeenCalled();
299+
});
300+
301+
test('실제 좌표가 다른 LatLng 전달 시 setPosition 호출 (fix-10 회귀 검증)', async () => {
302+
class LatLngLike {
303+
constructor(
304+
public lat: number,
305+
public lng: number,
306+
) {}
307+
equals(other: { lat: number; lng: number }) {
308+
return this.lat === other.lat && this.lng === other.lng;
309+
}
310+
}
311+
312+
const ref = createRef<naver.maps.OverlayView>();
313+
314+
const p1 = new LatLngLike(37.5, 127.0) as unknown as naver.maps.LatLng;
315+
316+
const { rerender } = render(
317+
<Wrapper>
318+
<CustomOverlay position={p1} ref={ref}>
319+
<span data-testid="diff-child">content</span>
320+
</CustomOverlay>
321+
</Wrapper>,
322+
);
323+
324+
await vi.waitFor(() => {
325+
expect(ref.current).not.toBeNull();
326+
});
327+
328+
const setPositionSpy = vi.spyOn(
329+
ref.current as unknown as { setPosition: (p: unknown) => void },
330+
'setPosition',
331+
);
332+
333+
const p2 = new LatLngLike(38.0, 128.0) as unknown as naver.maps.LatLng;
334+
335+
rerender(
336+
<Wrapper>
337+
<CustomOverlay position={p2} ref={ref}>
338+
<span data-testid="diff-child">content</span>
339+
</CustomOverlay>
340+
</Wrapper>,
341+
);
342+
343+
expect(setPositionSpy).toHaveBeenCalledWith(p2);
344+
});
229345
});

packages/react-naver-maps/src/__tests__/event-types.spec.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,20 @@ describe('EventHandlerProps 매핑 타입', () => {
4343
expectTypeOf<Result>().toHaveProperty('onDragend');
4444
});
4545

46-
test('GroundOverlayEvent → click, dblclick만', () => {
46+
test('GroundOverlayEvent → click, dblclick, mousedown, mouseup, rightclick (fix-09)', () => {
4747
type Result = EventHandlerProps<GroundOverlayEvent>;
4848
expectTypeOf<Result>().toHaveProperty('onClick');
4949
expectTypeOf<Result>().toHaveProperty('onDblclick');
50+
expectTypeOf<Result>().toHaveProperty('onMousedown');
51+
expectTypeOf<Result>().toHaveProperty('onMouseup');
52+
expectTypeOf<Result>().toHaveProperty('onRightclick');
53+
54+
// GROUND_DOMEVENTS = ["click", "mousedown", "mouseup", "contextmenu"].
55+
// click → 합성 dblclick, contextmenu → rightclick으로 발화하므로 5종.
56+
// mouseover/mouseout/mousemove는 바인딩하지 않으므로 제외.
57+
expectTypeOf<GroundOverlayEvent>().toEqualTypeOf<
58+
'click' | 'dblclick' | 'mousedown' | 'mouseup' | 'rightclick'
59+
>();
5060
});
5161

5262
test('핸들러 시그니처가 PointerEventHandler', () => {
@@ -95,8 +105,20 @@ describe('각 오버레이 Props에 이벤트 포함 확인', () => {
95105
expectTypeOf<PolylineProps>().toHaveProperty('onMouseup');
96106
});
97107

98-
test('GroundOverlayProps에 click/dblclick만', () => {
108+
test('GroundOverlayProps에 click/dblclick/mousedown/mouseup/rightclick (fix-09)', () => {
99109
expectTypeOf<GroundOverlayProps>().toHaveProperty('onClick');
100110
expectTypeOf<GroundOverlayProps>().toHaveProperty('onDblclick');
111+
expectTypeOf<GroundOverlayProps>().toHaveProperty('onMousedown');
112+
expectTypeOf<GroundOverlayProps>().toHaveProperty('onMouseup');
113+
expectTypeOf<GroundOverlayProps>().toHaveProperty('onRightclick');
114+
});
115+
116+
test('GroundOverlayProps의 이벤트 핸들러는 5종만 (fix-09)', () => {
117+
// EventHandlerProps<GroundOverlayEvent>가 정확한 키 집합을 갖는지 검증
118+
type Handlers = EventHandlerProps<GroundOverlayEvent>;
119+
type HandlerKeys = keyof Handlers;
120+
expectTypeOf<HandlerKeys>().toEqualTypeOf<
121+
'onClick' | 'onDblclick' | 'onMousedown' | 'onMouseup' | 'onRightclick'
122+
>();
101123
});
102124
});

packages/react-naver-maps/src/__tests__/ground-overlay.spec.tsx

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,4 +166,35 @@ describe('GroundOverlay 스펙 테스트', () => {
166166
expect(instance._listeners['click']).toBeDefined();
167167
expect(instance._listeners['click']).toHaveLength(1);
168168
});
169+
170+
// GROUND_DOMEVENTS의 click 리스너가 연속 클릭을 합성 dblclick으로 발화하므로(maps.beautified.js:9014),
171+
// onDblclick은 실제로 바인딩되어야 한다. 타입 축소 라운드에서 잘못 제거됐던 회귀를 잡는다.
172+
test('SDK가 발화하는 5종 이벤트(click/dblclick/mousedown/mouseup/rightclick) 모두 바인딩', () => {
173+
const bounds = { south: 37.0, west: 126.0, north: 38.0, east: 127.0 };
174+
175+
render(
176+
<GroundOverlay
177+
url="https://example.com/image.png"
178+
bounds={bounds}
179+
onClick={vi.fn()}
180+
onDblclick={vi.fn()}
181+
onMousedown={vi.fn()}
182+
onMouseup={vi.fn()}
183+
onRightclick={vi.fn()}
184+
/>,
185+
);
186+
187+
const instance = createdInstances[0].instance;
188+
for (const event of [
189+
'click',
190+
'dblclick',
191+
'mousedown',
192+
'mouseup',
193+
'rightclick',
194+
]) {
195+
expect(instance._listeners[event], `${event} 리스너 누락`).toHaveLength(
196+
1,
197+
);
198+
}
199+
});
169200
});

0 commit comments

Comments
 (0)