diff --git a/.claude/implementations/infowindow-implementation.md b/.claude/implementations/infowindow-implementation.md
new file mode 100644
index 00000000..0b203242
--- /dev/null
+++ b/.claude/implementations/infowindow-implementation.md
@@ -0,0 +1,218 @@
+# InfoWindow Implementation
+
+## 개요
+
+네이버 지도 SDK의 InfoWindow 기능을 React Native Naver Map 라이브러리에 구현했습니다.
+InfoWindow는 마커나 특정 좌표에 부가 정보를 표시하는 말풍선 형태의 오버레이입니다.
+
+## 구현 내용
+
+### 1. TypeScript Spec 및 타입 정의
+
+**파일**: `src/spec/RNCNaverMapInfoWindowNativeComponent.ts`
+
+- Fabric Component Spec 정의
+- BaseOverlay 속성 상속
+- InfoWindow 전용 속성:
+ - `coord`: 위치 좌표
+ - `markerTag`: 연결할 마커 태그 (선택)
+ - `align`: 정렬 방향
+ - `anchor`: 앵커 포인트
+ - `offsetX`, `offsetY`: 오프셋
+ - `alpha`: 불투명도
+ - `text`: 표시 텍스트
+ - `textSize`, `textColor`: 텍스트 스타일
+ - `backgroundColor`: 배경색
+
+### 2. Android 구현
+
+#### RNCNaverMapInfoWindow.kt
+**위치**: `android/src/main/java/com/mjstudio/reactnativenavermap/overlay/infowindow/`
+
+- `InfoWindow` 래핑
+- **커스텀 어댑터 패턴 구현** (`RNCNaverMapInfoWindowAdapter`)
+ - `InfoWindow.ViewAdapter()` 상속
+ - `getView()` 메서드에서 커스텀 View 생성
+ - TextView + FrameLayout으로 구성
+ - GradientDrawable로 배경, 테두리, 라운드 코너 구현
+- 텍스트 및 스타일 동적 업데이트
+- 마커 연결 지원 (`identifier`를 통한 Marker Registry 조회)
+- 열림/닫힘 상태 제어
+
+#### RNCNaverMapInfoWindowManager.kt
+**위치**: `android/src/main/java/com/mjstudio/reactnativenavermap/overlay/infowindow/`
+
+- ViewManager 구현 (Codegen Delegate 패턴)
+- ReactProp을 통한 속성 처리
+
+#### RNCNaverMapInfoWindowManagerSpec.kt
+**위치**: `android/src/newarch/`
+
+- New Architecture Spec 클래스
+- ViewManagerDelegate 연결
+
+### 3. iOS 구현
+
+#### RNCNaverMapInfoWindow.h/mm
+**위치**: `ios/Overlay/InfoWindow/`
+
+- `NMFInfoWindow` 래핑
+- Fabric Component View 구현
+- Props 업데이트 처리
+- **커스텀 데이터 소스 구현** (`RNCNaverMapInfoWindowDataSource`)
+ - `NMFOverlayImageDataSource` 프로토콜 채택
+ - `viewWithOverlay:` 메서드로 UIView 직접 반환
+ - UILabel 기반 텍스트 렌더링
+ - CALayer를 통한 스타일링 (테두리, 라운드 코너, 배경색)
+ - 패딩 및 폰트 굵기 지원
+- 속성 변경 시 `invalidate()` 호출하여 자동 재렌더링
+- Retina 디스플레이 대응
+- `nmap::intToColor()` 함수로 색상 변환
+
+### 4. React Component
+
+**파일**: `src/component/NaverMapInfoWindow.tsx`
+
+- Props interface 정의 (`NaverMapInfoWindowProps`)
+- JSDoc 문서화
+- 사용 예제 포함
+- 기본값 설정
+- Color 처리 (`processColor`)
+- Align 변환
+
+### 5. 등록 및 Export
+
+- **Android**: `RNCNaverMapPackage.kt`에 ViewManager 등록
+- **iOS**: `RNCNaverMapViewImpl.h`에 import 추가, `insertReactSubview`에 처리 추가
+- **Export**: `src/index.tsx`에 컴포넌트 및 타입 export
+
+## 사용 예제
+
+```tsx
+import { NaverMapInfoWindow } from '@mj-studio/react-native-naver-map';
+
+// 특정 좌표에 InfoWindow 표시
+
+
+// 마커에 연결된 InfoWindow
+
+
+```
+
+## 참고 자료
+
+- [Android InfoWindow 공식 문서](https://navermaps.github.io/android-map-sdk/guide-ko/5-3.html)
+- [iOS NMFInfoWindow API](https://navermaps.github.io/ios-map-sdk/reference/Classes/NMFInfoWindow.html)
+- [iOS NMFOverlayImageDataSource 프로토콜](https://navermaps.github.io/ios-map-sdk/reference/Protocols/NMFOverlayImageDataSource.html)
+
+## 플랫폼별 스타일 지원 현황
+
+### Android ✅ (완전 지원)
+- ✅ `text`, `textSize`, `textColor`
+- ✅ `fontWeight` - Bold/Regular/Medium/Semibold (100-900)
+- ✅ `backgroundColor`
+- ✅ `borderRadius` - 둥근 모서리
+- ✅ `borderWidth`, `borderColor` - 테두리
+- ✅ `padding` - 내부 여백
+- ✅ 마커 연결 (`identifier`)
+- ✅ 열림/닫힘 제어 (`isOpen`)
+
+**구현 방식:**
+```kotlin
+// GradientDrawable로 커스텀 스타일 구현
+val drawable = GradientDrawable().apply {
+ setColor(backgroundColor)
+ cornerRadius = borderRadius
+ setStroke(borderWidth.toInt(), borderColor)
+}
+```
+
+### iOS ✅ (완전 지원)
+- ✅ `text`, `textSize`, `textColor`
+- ✅ `fontWeight` - Regular/Medium/Semibold/Bold (100-900)
+- ✅ `backgroundColor`
+- ✅ `borderRadius` - 둥근 모서리
+- ✅ `borderWidth`, `borderColor` - 테두리
+- ✅ `padding` - 내부 여백
+- ✅ 마커 연결 (`identifier`)
+- ✅ 열림/닫힘 제어 (`isOpen`)
+
+**구현 방식:**
+```objective-c
+// NMFOverlayImageDataSource 프로토콜의 viewWithOverlay: 메서드 구현
+- (UIView*)viewWithOverlay:(NMFOverlay*)overlay {
+ // UILabel과 UIView를 사용하여 커스텀 스타일 구현
+ UIView* containerView = [[UIView alloc] init];
+ containerView.backgroundColor = backgroundColor;
+ containerView.layer.cornerRadius = borderRadius;
+ containerView.layer.borderWidth = borderWidth;
+ containerView.layer.borderColor = borderColor.CGColor;
+ // ... 텍스트와 패딩 설정
+ return containerView;
+}
+```
+
+**주요 특징:**
+- Retina 디스플레이 지원
+- 동적 스타일 업데이트 (`invalidate()` 호출)
+- 빈 텍스트 처리 및 최소 크기 보장
+- Android와 동일한 모든 스타일 속성 지원
+
+## 구현 패턴
+
+이 구현은 다음 패턴을 따릅니다:
+
+- **Pattern #001**: Fabric Native Component Definition
+- **Pattern #008**: Android ViewManager with Codegen
+- **Pattern #004**: iOS Fabric Component Implementation
+
+## 테스트
+
+구현 후 다음을 확인해야 합니다:
+
+1. Codegen 정상 실행 (`pnpm codegen`)
+2. Lint 오류 없음
+3. 예제 앱에서 InfoWindow 표시 확인
+4. Android/iOS 모두에서 동작 확인
+5. 속성 변경 시 동적 업데이트 확인
+
+## 완료 상태
+
+- ✅ TypeScript Spec 및 타입 정의
+- ✅ Android 네이티브 구현 (모든 스타일 지원)
+- ✅ iOS 네이티브 구현 (모든 스타일 지원)
+- ✅ React Component 작성
+- ✅ Package 등록
+- ✅ Export 추가
+- ✅ 마커 연결 기능 (`identifier`)
+- ✅ 열림/닫힘 제어 (`isOpen`)
+- ✅ Marker Registry 구현
+- ✅ iOS 커스텀 스타일 (NMFOverlayImageDataSource 프로토콜 활용)
+
+## 사용 권장사항
+
+- **텍스트 정보 표시**: InfoWindow 사용 (양쪽 플랫폼 모두 완전 지원)
+- **커스텀 스타일**: InfoWindow 사용 (Android & iOS 모두 모든 스타일 속성 지원)
+- **복잡한 인터랙션**: 필요한 경우 Marker의 Custom View 사용 고려
+
diff --git a/android/src/main/java/com/mjstudio/reactnativenavermap/RNCNaverMapPackage.kt b/android/src/main/java/com/mjstudio/reactnativenavermap/RNCNaverMapPackage.kt
index 871f0ab9..ce2df567 100644
--- a/android/src/main/java/com/mjstudio/reactnativenavermap/RNCNaverMapPackage.kt
+++ b/android/src/main/java/com/mjstudio/reactnativenavermap/RNCNaverMapPackage.kt
@@ -8,6 +8,7 @@ import com.mjstudio.reactnativenavermap.mapview.RNCNaverMapViewManager
import com.mjstudio.reactnativenavermap.overlay.arrowheadpath.RNCNaverMapArrowheadPathManager
import com.mjstudio.reactnativenavermap.overlay.circle.RNCNaverMapCircleManager
import com.mjstudio.reactnativenavermap.overlay.ground.RNCNaverMapGroundManager
+import com.mjstudio.reactnativenavermap.overlay.infowindow.RNCNaverMapInfoWindowManager
import com.mjstudio.reactnativenavermap.overlay.marker.RNCNaverMapMarkerManager
import com.mjstudio.reactnativenavermap.overlay.multipath.RNCNaverMapMultiPathManager
import com.mjstudio.reactnativenavermap.overlay.path.RNCNaverMapPathManager
@@ -26,6 +27,7 @@ class RNCNaverMapPackage : ReactPackage {
add(RNCNaverMapMultiPathManager())
add(RNCNaverMapArrowheadPathManager())
add(RNCNaverMapGroundManager())
+ add(RNCNaverMapInfoWindowManager())
}
override fun createNativeModules(reactContext: ReactApplicationContext): List = emptyList()
diff --git a/android/src/main/java/com/mjstudio/reactnativenavermap/mapview/RNCNaverMapView.kt b/android/src/main/java/com/mjstudio/reactnativenavermap/mapview/RNCNaverMapView.kt
index 3d45df96..55926bf7 100644
--- a/android/src/main/java/com/mjstudio/reactnativenavermap/mapview/RNCNaverMapView.kt
+++ b/android/src/main/java/com/mjstudio/reactnativenavermap/mapview/RNCNaverMapView.kt
@@ -35,6 +35,9 @@ class RNCNaverMapView(
private var map: NaverMap? = null
val overlays = mutableListOf>()
+ // Marker registry for InfoWindow lookup
+ val markerRegistry = mutableMapOf()
+
private val locationOverlayImageRenderer by lazy {
RNCNaverMapTaggedImageRenderer(context)
}
@@ -162,6 +165,13 @@ class RNCNaverMapView(
is RNCNaverMapMarker -> {
child.addToMap(map)
overlays.add(index, child)
+
+ // Register marker by identifier
+ val identifier = child.overlay.tag as? String
+ if (identifier != null) {
+ markerRegistry[identifier] = child
+ }
+
val visibility: Int = child.visibility
child.visibility = INVISIBLE
(child.parent as? ViewGroup)?.removeView(child)
@@ -171,6 +181,10 @@ class RNCNaverMapView(
}
is RNCNaverMapOverlay<*> -> {
+ // If it's an InfoWindow, pass mapView reference
+ if (child is com.mjstudio.reactnativenavermap.overlay.infowindow.RNCNaverMapInfoWindow) {
+ child.setParentMapView(this)
+ }
child.addToMap(map)
overlays.add(index, child)
}
@@ -189,6 +203,7 @@ class RNCNaverMapView(
}
}
overlays.clear()
+ markerRegistry.clear()
map = null
attacherGroup = null
super.onDestroy()
@@ -198,6 +213,12 @@ class RNCNaverMapView(
withMap { map ->
when (val child = overlays.removeAt(index)) {
is RNCNaverMapMarker -> {
+ // Unregister marker
+ val identifier = child.overlay.tag as? String
+ if (identifier != null) {
+ markerRegistry.remove(identifier)
+ }
+
child.removeFromMap(map)
attacherGroup?.removeView(child)
}
diff --git a/android/src/main/java/com/mjstudio/reactnativenavermap/mapview/RNCNaverMapViewWrapper.kt b/android/src/main/java/com/mjstudio/reactnativenavermap/mapview/RNCNaverMapViewWrapper.kt
index 73bc32b4..71608826 100644
--- a/android/src/main/java/com/mjstudio/reactnativenavermap/mapview/RNCNaverMapViewWrapper.kt
+++ b/android/src/main/java/com/mjstudio/reactnativenavermap/mapview/RNCNaverMapViewWrapper.kt
@@ -6,6 +6,7 @@ import android.view.Choreographer
import android.view.Choreographer.FrameCallback
import android.view.MotionEvent
import android.view.View
+import android.view.View.MeasureSpec
import android.widget.FrameLayout
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
@@ -178,14 +179,10 @@ class RNCNaverMapViewWrapper(
when (ev?.action) {
MotionEvent.ACTION_DOWN,
MotionEvent.ACTION_MOVE,
- -> {
- parent?.requestDisallowInterceptTouchEvent(true)
- }
+ -> parent?.requestDisallowInterceptTouchEvent(true)
MotionEvent.ACTION_UP,
MotionEvent.ACTION_CANCEL,
- -> {
- parent?.requestDisallowInterceptTouchEvent(false)
- }
+ -> parent?.requestDisallowInterceptTouchEvent(false)
}
}
}
diff --git a/android/src/main/java/com/mjstudio/reactnativenavermap/overlay/infowindow/RNCNaverMapInfoWindow.kt b/android/src/main/java/com/mjstudio/reactnativenavermap/overlay/infowindow/RNCNaverMapInfoWindow.kt
new file mode 100644
index 00000000..dae0865a
--- /dev/null
+++ b/android/src/main/java/com/mjstudio/reactnativenavermap/overlay/infowindow/RNCNaverMapInfoWindow.kt
@@ -0,0 +1,206 @@
+package com.mjstudio.reactnativenavermap.overlay.infowindow
+
+import android.annotation.SuppressLint
+import android.view.View
+import android.widget.TextView
+import com.facebook.react.uimanager.ThemedReactContext
+import com.mjstudio.reactnativenavermap.overlay.RNCNaverMapOverlay
+import com.naver.maps.geometry.LatLng
+import com.naver.maps.map.NaverMap
+import com.naver.maps.map.overlay.InfoWindow
+import com.naver.maps.map.overlay.Marker
+
+@SuppressLint("ViewConstructor")
+class RNCNaverMapInfoWindow(
+ private val reactContext: ThemedReactContext,
+) : RNCNaverMapOverlay(reactContext) {
+ private var targetMarker: Marker? = null
+ private var position: LatLng? = null
+ private var customAdapter: RNCNaverMapInfoWindowAdapter? = null
+ private var markerIdentifier: String? = null
+ private var shouldBeOpen: Boolean = true
+ private var currentMap: NaverMap? = null
+ private var parentMapView: com.mjstudio.reactnativenavermap.mapview.RNCNaverMapView? = null
+
+ override val overlay: InfoWindow by lazy {
+ InfoWindow().apply {
+ customAdapter = RNCNaverMapInfoWindowAdapter(reactContext)
+ adapter = customAdapter as InfoWindow.Adapter
+ }
+ }
+
+ override fun addToMap(map: NaverMap) {
+ currentMap = map
+ updateInfoWindowState()
+ }
+
+ override fun removeFromMap(map: NaverMap) {
+ overlay.close()
+ currentMap = null
+ }
+
+ fun setParentMapView(mapView: com.mjstudio.reactnativenavermap.mapview.RNCNaverMapView) {
+ parentMapView = mapView
+ }
+
+ private fun updateInfoWindowState() {
+ if (!shouldBeOpen) {
+ overlay.close()
+ return
+ }
+
+ val map = currentMap ?: return
+
+ // Try to find marker by identifier first
+ val identifier = markerIdentifier
+ if (identifier != null) {
+ val markerView = parentMapView?.markerRegistry?.get(identifier)
+ if (markerView != null) {
+ // Open on marker (marker position is used automatically)
+ overlay.open(markerView.overlay)
+ return
+ }
+ }
+
+ // Fall back to position
+ val pos = position
+ if (pos != null) {
+ overlay.position = pos
+ overlay.map = map
+ }
+ }
+
+ override fun onDropViewInstance() {
+ overlay.close()
+ customAdapter = null
+ }
+
+ fun setPosition(latLng: LatLng) {
+ position = latLng
+ updateInfoWindowState()
+ }
+
+ fun setMarkerIdentifier(identifier: String?) {
+ markerIdentifier = identifier
+ updateInfoWindowState()
+ }
+
+ fun setIsOpen(isOpen: Boolean) {
+ shouldBeOpen = isOpen
+ updateInfoWindowState()
+ }
+
+ fun setText(text: String?) {
+ customAdapter?.text = text
+ customAdapter?.let { overlay.adapter = it as InfoWindow.Adapter }
+ }
+
+ fun setTextSize(size: Float) {
+ customAdapter?.textSize = size
+ customAdapter?.let { overlay.adapter = it as InfoWindow.Adapter }
+ }
+
+ fun setTextColor(color: Int) {
+ customAdapter?.textColor = color
+ customAdapter?.let { overlay.adapter = it as InfoWindow.Adapter }
+ }
+
+ fun setFontWeight(weight: Int) {
+ customAdapter?.fontWeight = weight
+ customAdapter?.let { overlay.adapter = it as InfoWindow.Adapter }
+ }
+
+ fun setInfoWindowBackgroundColor(color: Int) {
+ customAdapter?.backgroundColor = color
+ customAdapter?.let { overlay.adapter = it as InfoWindow.Adapter }
+ }
+
+ fun setInfoWindowBorderRadius(radius: Float) {
+ customAdapter?.borderRadius = radius
+ customAdapter?.let { overlay.adapter = it as InfoWindow.Adapter }
+ }
+
+ fun setInfoWindowBorderWidth(width: Float) {
+ customAdapter?.borderWidth = width
+ customAdapter?.let { overlay.adapter = it as InfoWindow.Adapter }
+ }
+
+ fun setInfoWindowBorderColor(color: Int) {
+ customAdapter?.borderColor = color
+ customAdapter?.let { overlay.adapter = it as InfoWindow.Adapter }
+ }
+
+ fun setInfoWindowPaddingHorizontal(padding: Float) {
+ customAdapter?.paddingHorizontal = padding
+ customAdapter?.let { overlay.adapter = it as InfoWindow.Adapter }
+ }
+
+ fun setInfoWindowPaddingVertical(padding: Float) {
+ customAdapter?.paddingVertical = padding
+ customAdapter?.let { overlay.adapter = it as InfoWindow.Adapter }
+ }
+
+ inner class RNCNaverMapInfoWindowAdapter(
+ private val context: ThemedReactContext,
+ ) : InfoWindow.ViewAdapter() {
+ var text: String? = null
+ var textSize: Float = 14f
+ var textColor: Int = android.graphics.Color.BLACK
+ var fontWeight: Int = 400
+ var backgroundColor: Int = android.graphics.Color.WHITE
+ var borderRadius: Float = 5f
+ var borderWidth: Float = 1f
+ var borderColor: Int = android.graphics.Color.parseColor("#cccccc")
+ var paddingHorizontal: Float = 10f
+ var paddingVertical: Float = 10f
+
+ override fun getView(infoWindow: InfoWindow): View {
+ val paddingHorizontalPx = this@RNCNaverMapInfoWindowAdapter.paddingHorizontal.toInt()
+ val paddingVerticalPx = this@RNCNaverMapInfoWindowAdapter.paddingVertical.toInt()
+
+ val textView = TextView(context).apply {
+ this.text = this@RNCNaverMapInfoWindowAdapter.text ?: ""
+ this.textSize = this@RNCNaverMapInfoWindowAdapter.textSize
+ this.setTextColor(this@RNCNaverMapInfoWindowAdapter.textColor)
+
+ // Font weight
+ val typeface = when {
+ this@RNCNaverMapInfoWindowAdapter.fontWeight >= 700 -> android.graphics.Typeface.BOLD
+ else -> android.graphics.Typeface.NORMAL
+ }
+ this.setTypeface(null, typeface)
+
+ // Gravity
+ this.gravity = android.view.Gravity.CENTER
+ }
+
+ // Container with border, background and padding
+ val container = android.widget.FrameLayout(context).apply {
+ // Add padding to container (horizontal and vertical separately)
+ this.setPadding(paddingHorizontalPx, paddingVerticalPx, paddingHorizontalPx, paddingVerticalPx)
+
+ // Add text view with wrap content
+ addView(
+ textView,
+ android.widget.FrameLayout.LayoutParams(
+ android.widget.FrameLayout.LayoutParams.WRAP_CONTENT,
+ android.widget.FrameLayout.LayoutParams.WRAP_CONTENT,
+ ),
+ )
+
+ // Background and border using GradientDrawable
+ val drawable = android.graphics.drawable.GradientDrawable().apply {
+ setColor(this@RNCNaverMapInfoWindowAdapter.backgroundColor)
+ cornerRadius = this@RNCNaverMapInfoWindowAdapter.borderRadius
+ setStroke(
+ this@RNCNaverMapInfoWindowAdapter.borderWidth.toInt(),
+ this@RNCNaverMapInfoWindowAdapter.borderColor,
+ )
+ }
+ background = drawable
+ }
+
+ return container
+ }
+ }
+}
diff --git a/android/src/main/java/com/mjstudio/reactnativenavermap/overlay/infowindow/RNCNaverMapInfoWindowManager.kt b/android/src/main/java/com/mjstudio/reactnativenavermap/overlay/infowindow/RNCNaverMapInfoWindowManager.kt
new file mode 100644
index 00000000..36c12947
--- /dev/null
+++ b/android/src/main/java/com/mjstudio/reactnativenavermap/overlay/infowindow/RNCNaverMapInfoWindowManager.kt
@@ -0,0 +1,238 @@
+package com.mjstudio.reactnativenavermap.overlay.infowindow
+
+import com.facebook.react.bridge.ReadableMap
+import com.facebook.react.uimanager.ThemedReactContext
+import com.facebook.react.uimanager.annotations.ReactProp
+import com.mjstudio.reactnativenavermap.RNCNaverMapInfoWindowManagerSpec
+import com.mjstudio.reactnativenavermap.util.getLatLng
+import com.mjstudio.reactnativenavermap.util.getPoint
+import com.mjstudio.reactnativenavermap.util.isValidNumber
+import com.mjstudio.reactnativenavermap.util.px
+import com.naver.maps.map.overlay.InfoWindow
+
+class RNCNaverMapInfoWindowManager : RNCNaverMapInfoWindowManagerSpec() {
+ override fun getName(): String = NAME
+
+ override fun createViewInstance(context: ThemedReactContext): RNCNaverMapInfoWindow =
+ RNCNaverMapInfoWindow(context)
+
+ override fun onDropViewInstance(view: RNCNaverMapInfoWindow) {
+ super.onDropViewInstance(view)
+ view.onDropViewInstance()
+ }
+
+ private fun RNCNaverMapInfoWindow?.withOverlay(fn: (InfoWindow) -> Unit) {
+ this?.overlay?.run(fn)
+ }
+
+ @ReactProp(name = "coord")
+ override fun setCoord(
+ view: RNCNaverMapInfoWindow?,
+ value: ReadableMap?,
+ ) {
+ value.getLatLng()?.run {
+ view?.setPosition(this)
+ }
+ }
+
+ @ReactProp(name = "zIndexValue")
+ override fun setZIndexValue(
+ view: RNCNaverMapInfoWindow?,
+ value: Int,
+ ) = view.withOverlay {
+ it.zIndex = value
+ }
+
+ @ReactProp(name = "globalZIndexValue")
+ override fun setGlobalZIndexValue(
+ view: RNCNaverMapInfoWindow?,
+ value: Int,
+ ) = view.withOverlay {
+ if (isValidNumber(value)) {
+ it.globalZIndex = value
+ }
+ }
+
+ @ReactProp(name = "isHidden")
+ override fun setIsHidden(
+ view: RNCNaverMapInfoWindow?,
+ value: Boolean,
+ ) = view.withOverlay {
+ it.isVisible = !value
+ }
+
+ @ReactProp(name = "minZoom")
+ override fun setMinZoom(
+ view: RNCNaverMapInfoWindow?,
+ value: Double,
+ ) = view.withOverlay {
+ it.minZoom = value
+ }
+
+ @ReactProp(name = "maxZoom")
+ override fun setMaxZoom(
+ view: RNCNaverMapInfoWindow?,
+ value: Double,
+ ) = view.withOverlay {
+ it.maxZoom = value
+ }
+
+ @ReactProp(name = "isMinZoomInclusive")
+ override fun setIsMinZoomInclusive(
+ view: RNCNaverMapInfoWindow?,
+ value: Boolean,
+ ) = view.withOverlay {
+ it.isMinZoomInclusive = value
+ }
+
+ @ReactProp(name = "isMaxZoomInclusive")
+ override fun setIsMaxZoomInclusive(
+ view: RNCNaverMapInfoWindow?,
+ value: Boolean,
+ ) = view.withOverlay {
+ it.isMaxZoomInclusive = value
+ }
+
+ @ReactProp(name = "align")
+ override fun setAlign(
+ view: RNCNaverMapInfoWindow?,
+ value: Int,
+ ) {
+ // InfoWindow align will be handled when opening
+ // Stored for later use when marker is attached
+ }
+
+ @ReactProp(name = "anchor")
+ override fun setAnchor(
+ view: RNCNaverMapInfoWindow?,
+ value: ReadableMap?,
+ ) = view.withOverlay {
+ value.getPoint()?.run {
+ it.anchor = this
+ }
+ }
+
+ @ReactProp(name = "offsetX")
+ override fun setOffsetX(
+ view: RNCNaverMapInfoWindow?,
+ value: Int,
+ ) = view.withOverlay {
+ it.offsetX = value.px
+ }
+
+ @ReactProp(name = "offsetY")
+ override fun setOffsetY(
+ view: RNCNaverMapInfoWindow?,
+ value: Int,
+ ) = view.withOverlay {
+ it.offsetY = value.px
+ }
+
+ @ReactProp(name = "alpha")
+ override fun setAlpha(
+ view: RNCNaverMapInfoWindow?,
+ value: Double,
+ ) = view.withOverlay {
+ it.alpha = value.toFloat()
+ }
+
+ @ReactProp(name = "text")
+ override fun setText(
+ view: RNCNaverMapInfoWindow?,
+ value: String?,
+ ) {
+ view?.setText(value)
+ }
+
+ @ReactProp(name = "textSize")
+ override fun setTextSize(
+ view: RNCNaverMapInfoWindow?,
+ value: Double,
+ ) {
+ view?.setTextSize(value.toFloat())
+ }
+
+ @ReactProp(name = "textColor")
+ override fun setTextColor(
+ view: RNCNaverMapInfoWindow?,
+ value: Int,
+ ) {
+ view?.setTextColor(value)
+ }
+
+ @ReactProp(name = "fontWeight", defaultInt = 400)
+ override fun setFontWeight(
+ view: RNCNaverMapInfoWindow?,
+ value: Int,
+ ) {
+ view?.setFontWeight(value)
+ }
+
+ @ReactProp(name = "infoWindowBackgroundColor")
+ override fun setInfoWindowBackgroundColor(
+ view: RNCNaverMapInfoWindow?,
+ value: Int,
+ ) {
+ view?.setInfoWindowBackgroundColor(value)
+ }
+
+ @ReactProp(name = "infoWindowBorderRadius", defaultFloat = 5f)
+ override fun setInfoWindowBorderRadius(
+ view: RNCNaverMapInfoWindow?,
+ value: Double,
+ ) {
+ view?.setInfoWindowBorderRadius(value.toFloat())
+ }
+
+ @ReactProp(name = "infoWindowBorderWidth", defaultFloat = 1f)
+ override fun setInfoWindowBorderWidth(
+ view: RNCNaverMapInfoWindow?,
+ value: Double,
+ ) {
+ view?.setInfoWindowBorderWidth(value.toFloat())
+ }
+
+ @ReactProp(name = "infoWindowBorderColor")
+ override fun setInfoWindowBorderColor(
+ view: RNCNaverMapInfoWindow?,
+ value: Int,
+ ) {
+ view?.setInfoWindowBorderColor(value)
+ }
+
+ @ReactProp(name = "infoWindowPaddingHorizontal", defaultFloat = 10f)
+ override fun setInfoWindowPaddingHorizontal(
+ view: RNCNaverMapInfoWindow?,
+ value: Double,
+ ) {
+ view?.setInfoWindowPaddingHorizontal(value.toFloat())
+ }
+
+ @ReactProp(name = "infoWindowPaddingVertical", defaultFloat = 10f)
+ override fun setInfoWindowPaddingVertical(
+ view: RNCNaverMapInfoWindow?,
+ value: Double,
+ ) {
+ view?.setInfoWindowPaddingVertical(value.toFloat())
+ }
+
+ @ReactProp(name = "identifier")
+ override fun setIdentifier(
+ view: RNCNaverMapInfoWindow?,
+ value: String?,
+ ) {
+ view?.setMarkerIdentifier(value)
+ }
+
+ @ReactProp(name = "isOpen", defaultBoolean = true)
+ override fun setIsOpen(
+ view: RNCNaverMapInfoWindow?,
+ value: Boolean,
+ ) {
+ view?.setIsOpen(value)
+ }
+
+ companion object {
+ const val NAME = "RNCNaverMapInfoWindow"
+ }
+}
diff --git a/android/src/main/java/com/mjstudio/reactnativenavermap/overlay/marker/RNCNaverMapMarker.kt b/android/src/main/java/com/mjstudio/reactnativenavermap/overlay/marker/RNCNaverMapMarker.kt
index 509e1ec7..6edcc402 100644
--- a/android/src/main/java/com/mjstudio/reactnativenavermap/overlay/marker/RNCNaverMapMarker.kt
+++ b/android/src/main/java/com/mjstudio/reactnativenavermap/overlay/marker/RNCNaverMapMarker.kt
@@ -29,150 +29,150 @@ class RNCNaverMapMarker(
val reactContext: ThemedReactContext,
) : RNCNaverMapImageRenderableOverlay(reactContext),
TrackableView {
- private var customView: View? = null
- private var customViewBitmap: Bitmap? = null
-
- private var isImageSetFromSubview = false
-
- private var lastCaptionKey = DEFAULT_CAPTION_KEY
- private var lastSubCaptionKey = DEFAULT_CAPTION_KEY
-
- override val overlay: Marker by lazy {
- Marker().apply {
- setOnClickListener {
- reactContext.emitEvent(id) { surfaceId, reactTag ->
- NaverMapOverlayTapEvent(
- surfaceId,
- reactTag,
- )
+ private var customView: View? = null
+ private var customViewBitmap: Bitmap? = null
+
+ private var isImageSetFromSubview = false
+
+ private var lastCaptionKey = DEFAULT_CAPTION_KEY
+ private var lastSubCaptionKey = DEFAULT_CAPTION_KEY
+
+ override val overlay: Marker by lazy {
+ Marker().apply {
+ setOnClickListener {
+ reactContext.emitEvent(id) { surfaceId, reactTag ->
+ NaverMapOverlayTapEvent(
+ surfaceId,
+ reactTag,
+ )
+ }
+ true
}
- true
}
}
- }
-
- override fun addToMap(map: NaverMap) {
- overlay.map = map
- }
- override fun removeFromMap(map: NaverMap) {
- overlay.map = null
- }
+ override fun addToMap(map: NaverMap) {
+ overlay.map = map
+ }
- override fun onDropViewInstance() {
- overlay.map = null
- overlay.onClickListener = null
- super.onDropViewInstance()
- }
+ override fun removeFromMap(map: NaverMap) {
+ overlay.map = null
+ }
- fun setCustomView(
- view: View,
- index: Int,
- ) {
- super.addView(view, index)
- isImageSetFromSubview = true
- if (view.layoutParams == null) {
- view.setLayoutParams(
- LayoutParams(
- LayoutParams.WRAP_CONTENT,
- LayoutParams.WRAP_CONTENT,
- ),
- )
+ override fun onDropViewInstance() {
+ overlay.map = null
+ overlay.onClickListener = null
+ super.onDropViewInstance()
}
- ViewChangesTracker.getInstance().addMarker(this)
- customView = view
- updateCustomView()
- overlay.alpha = 1f
- }
- fun removeCustomView(index: Int) {
- customView = null
- ViewChangesTracker.getInstance().removeMarker(this)
- if (customViewBitmap != null && !customViewBitmap!!.isRecycled) customViewBitmap!!.recycle()
- isImageSetFromSubview = false
- setImageWithLastImage()
- super.removeView(children.elementAt(index))
- }
+ fun setCustomView(
+ view: View,
+ index: Int,
+ ) {
+ super.addView(view, index)
+ isImageSetFromSubview = true
+ if (view.layoutParams == null) {
+ view.setLayoutParams(
+ LayoutParams(
+ LayoutParams.WRAP_CONTENT,
+ LayoutParams.WRAP_CONTENT,
+ ),
+ )
+ }
+ ViewChangesTracker.getInstance().addMarker(this)
+ customView = view
+ updateCustomView()
+ overlay.alpha = 1f
+ }
- override fun requestLayout() {
- super.requestLayout()
- if (isEmpty() && customView != null) {
+ fun removeCustomView(index: Int) {
customView = null
- updateCustomView()
+ ViewChangesTracker.getInstance().removeMarker(this)
+ if (customViewBitmap != null && !customViewBitmap!!.isRecycled) customViewBitmap!!.recycle()
+ isImageSetFromSubview = false
+ setImageWithLastImage()
+ super.removeView(children.elementAt(index))
}
- }
- private fun updateCustomView() {
- if (customViewBitmap == null ||
- customViewBitmap!!.isRecycled ||
- customViewBitmap?.width != overlay.width ||
- customViewBitmap?.height != overlay.height
- ) {
- customViewBitmap =
- createBitmap(max(1, overlay.width), max(1, overlay.height), Bitmap.Config.ARGB_4444)
+ override fun requestLayout() {
+ super.requestLayout()
+ if (isEmpty() && customView != null) {
+ customView = null
+ updateCustomView()
+ }
}
- if (customView != null) {
- customViewBitmap?.also { bitmap ->
- bitmap.eraseColor(Color.TRANSPARENT)
- val canvas = Canvas(bitmap)
- draw(canvas)
- setOverlayImage(OverlayImage.fromBitmap(bitmap))
+
+ private fun updateCustomView() {
+ if (customViewBitmap == null ||
+ customViewBitmap!!.isRecycled ||
+ customViewBitmap?.width != overlay.width ||
+ customViewBitmap?.height != overlay.height
+ ) {
+ customViewBitmap =
+ createBitmap(max(1, overlay.width), max(1, overlay.height), Bitmap.Config.ARGB_4444)
+ }
+ if (customView != null) {
+ customViewBitmap?.also { bitmap ->
+ bitmap.eraseColor(Color.TRANSPARENT)
+ val canvas = Canvas(bitmap)
+ draw(canvas)
+ setOverlayImage(OverlayImage.fromBitmap(bitmap))
+ }
}
}
- }
- override fun skipTryRender(): Boolean = isImageSetFromSubview
+ override fun skipTryRender(): Boolean = isImageSetFromSubview
- override fun updateCustomForTracking(): Boolean = true
+ override fun updateCustomForTracking(): Boolean = true
- override fun update() {
- updateCustomView()
- }
+ override fun update() {
+ updateCustomView()
+ }
- override fun setOverlayAlpha(alpha: Float) {
- overlay.alpha = alpha
- }
+ override fun setOverlayAlpha(alpha: Float) {
+ overlay.alpha = alpha
+ }
- override fun setOverlayImage(image: OverlayImage?) {
- overlay.icon =
- image ?: OverlayImage.fromBitmap(createBitmap(1, 1))
- }
+ override fun setOverlayImage(image: OverlayImage?) {
+ overlay.icon =
+ image ?: OverlayImage.fromBitmap(createBitmap(1, 1))
+ }
- fun updateCaption(value: ReadableMap?) {
- value?.also { map ->
- val key = map.getString("key") ?: DEFAULT_CAPTION_KEY
- if (key == lastCaptionKey) return
- lastCaptionKey = key
-
- overlay.captionText = map.getString("text") ?: ""
- overlay.captionRequestedWidth = (map.getDoubleOrNull("requestedWidth") ?: 0.0).px
- overlay.setCaptionAligns(map.getAlign("align"))
- overlay.captionOffset = (map.getDoubleOrNull("offset") ?: 0.0).px
- overlay.captionColor = map.getIntOrNull("color") ?: Color.BLACK
- overlay.captionHaloColor = map.getIntOrNull("haloColor") ?: Color.TRANSPARENT
- overlay.captionTextSize = map.getDouble("textSize").toFloat()
- overlay.captionMinZoom = map.getDouble("minZoom")
- overlay.captionMaxZoom = map.getDouble("maxZoom")
+ fun updateCaption(value: ReadableMap?) {
+ value?.also { map ->
+ val key = map.getString("key") ?: DEFAULT_CAPTION_KEY
+ if (key == lastCaptionKey) return
+ lastCaptionKey = key
+
+ overlay.captionText = map.getString("text") ?: ""
+ overlay.captionRequestedWidth = (map.getDoubleOrNull("requestedWidth") ?: 0.0).px
+ overlay.setCaptionAligns(map.getAlign("align"))
+ overlay.captionOffset = (map.getDoubleOrNull("offset") ?: 0.0).px
+ overlay.captionColor = map.getIntOrNull("color") ?: Color.BLACK
+ overlay.captionHaloColor = map.getIntOrNull("haloColor") ?: Color.TRANSPARENT
+ overlay.captionTextSize = map.getDouble("textSize").toFloat()
+ overlay.captionMinZoom = map.getDouble("minZoom")
+ overlay.captionMaxZoom = map.getDouble("maxZoom")
+ }
}
- }
- fun updateSubCaption(value: ReadableMap?) {
- value?.also { map ->
- val key = map.getString("key") ?: DEFAULT_CAPTION_KEY
- if (key == lastSubCaptionKey) return
- lastSubCaptionKey = key
-
- overlay.subCaptionText = map.getString("text") ?: ""
- overlay.subCaptionColor = map.getIntOrNull("color") ?: Color.BLACK
- overlay.subCaptionHaloColor = map.getIntOrNull("haloColor") ?: Color.TRANSPARENT
- overlay.subCaptionTextSize = map.getDouble("textSize").toFloat()
- overlay.subCaptionRequestedWidth = map.getDouble("requestedWidth").px
- overlay.subCaptionMinZoom = map.getDouble("minZoom")
- overlay.subCaptionMaxZoom = map.getDouble("maxZoom")
+ fun updateSubCaption(value: ReadableMap?) {
+ value?.also { map ->
+ val key = map.getString("key") ?: DEFAULT_CAPTION_KEY
+ if (key == lastSubCaptionKey) return
+ lastSubCaptionKey = key
+
+ overlay.subCaptionText = map.getString("text") ?: ""
+ overlay.subCaptionColor = map.getIntOrNull("color") ?: Color.BLACK
+ overlay.subCaptionHaloColor = map.getIntOrNull("haloColor") ?: Color.TRANSPARENT
+ overlay.subCaptionTextSize = map.getDouble("textSize").toFloat()
+ overlay.subCaptionRequestedWidth = map.getDouble("requestedWidth").px
+ overlay.subCaptionMinZoom = map.getDouble("minZoom")
+ overlay.subCaptionMaxZoom = map.getDouble("maxZoom")
+ }
}
- }
- companion object {
- const val DEFAULT_CAPTION_KEY = "DEFAULT"
+ companion object {
+ const val DEFAULT_CAPTION_KEY = "DEFAULT"
+ }
}
-}
diff --git a/android/src/main/java/com/mjstudio/reactnativenavermap/overlay/marker/RNCNaverMapMarkerManager.kt b/android/src/main/java/com/mjstudio/reactnativenavermap/overlay/marker/RNCNaverMapMarkerManager.kt
index 3d2f71c9..9589a5e3 100644
--- a/android/src/main/java/com/mjstudio/reactnativenavermap/overlay/marker/RNCNaverMapMarkerManager.kt
+++ b/android/src/main/java/com/mjstudio/reactnativenavermap/overlay/marker/RNCNaverMapMarkerManager.kt
@@ -238,6 +238,14 @@ class RNCNaverMapMarkerManager : RNCNaverMapMarkerManagerSpec
view?.updateSubCaption(value)
}
+ @ReactProp(name = "identifier")
+ override fun setIdentifier(
+ view: RNCNaverMapMarker?,
+ value: String?,
+ ) = view.withOverlay {
+ // Set marker tag for InfoWindow to find this marker
+ it.tag = value
+ }
companion object {
const val NAME = "RNCNaverMapMarker"
}
diff --git a/android/src/newarch/RNCNaverMapInfoWindowManagerSpec.kt b/android/src/newarch/RNCNaverMapInfoWindowManagerSpec.kt
new file mode 100644
index 00000000..2370045e
--- /dev/null
+++ b/android/src/newarch/RNCNaverMapInfoWindowManagerSpec.kt
@@ -0,0 +1,19 @@
+package com.mjstudio.reactnativenavermap
+
+import android.view.View
+import com.facebook.react.uimanager.SimpleViewManager
+import com.facebook.react.uimanager.ViewManagerDelegate
+import com.facebook.react.viewmanagers.RNCNaverMapInfoWindowManagerDelegate
+import com.facebook.react.viewmanagers.RNCNaverMapInfoWindowManagerInterface
+
+abstract class RNCNaverMapInfoWindowManagerSpec :
+ SimpleViewManager(),
+ RNCNaverMapInfoWindowManagerInterface {
+ private val mDelegate: ViewManagerDelegate
+
+ init {
+ mDelegate = RNCNaverMapInfoWindowManagerDelegate(this)
+ }
+
+ override fun getDelegate(): ViewManagerDelegate = mDelegate
+}
diff --git a/docs/content/docs/components/overlays/meta.json b/docs/content/docs/components/overlays/meta.json
index 3f7ff198..6828a5cf 100644
--- a/docs/content/docs/components/overlays/meta.json
+++ b/docs/content/docs/components/overlays/meta.json
@@ -4,6 +4,7 @@
"pages": [
"common-overlay-usage",
"naver-map-marker-overlay",
+ "naver-map-info-window",
"naver-map-circle-overlay",
"naver-map-polygon-overlay",
"naver-map-polyline-overlay",
diff --git a/docs/content/docs/components/overlays/meta.ko.json b/docs/content/docs/components/overlays/meta.ko.json
index ba2482b5..b511f43b 100644
--- a/docs/content/docs/components/overlays/meta.ko.json
+++ b/docs/content/docs/components/overlays/meta.ko.json
@@ -4,6 +4,7 @@
"pages": [
"common-overlay-usage",
"naver-map-marker-overlay",
+ "naver-map-info-window",
"naver-map-circle-overlay",
"naver-map-polygon-overlay",
"naver-map-polyline-overlay",
diff --git a/docs/content/docs/components/overlays/naver-map-info-window.ko.mdx b/docs/content/docs/components/overlays/naver-map-info-window.ko.mdx
new file mode 100644
index 00000000..1d620564
--- /dev/null
+++ b/docs/content/docs/components/overlays/naver-map-info-window.ko.mdx
@@ -0,0 +1,61 @@
+---
+title: "NaverMapInfoWindow"
+description: "마커 또는 좌표 위에 말풍선(정보창)을 표시하는 컴포넌트"
+---
+
+## Overview
+
+NaverMapInfoWindow는 지도 위에 정보 말풍선을 표시합니다.
+`identifier`로 마커에 연결할 수 있고, `latitude`/`longitude`로 임의의 좌표에 직접 표시할 수도 있습니다.
+
+플랫폼 참고:
+- Android: 폰트, 배경/테두리/패딩 등 전체 스타일 지원
+- iOS: 모든 스타일 속성 지원 (NMFOverlayImageDataSource 프로토콜 활용)
+
+## Basic Usage
+
+```tsx
+import { NaverMapView, NaverMapMarkerOverlay, NaverMapInfoWindow } from '@mj-studio/react-native-naver-map';
+
+function MapWithInfoWindow() {
+ return (
+
+ {/* 1) identifier로 마커에 연결 */}
+
+
+
+ {/* 2) 좌표에 직접 표시 */}
+
+
+ );
+}
+```
+
+## Props
+
+`NaverMapInfoWindowProps`
+
+
+
+## 비고
+
+- `identifier`가 설정되면 동일한 `identifier`를 가진 마커 위에 정보창이 열립니다.
+- `identifier`가 없으면 `latitude`와 `longitude` 좌표에 표시됩니다.
+- `align`, `anchor`, `offsetX/offsetY`로 마커 주변 위치를 세밀하게 조정할 수 있습니다.
+- Android와 iOS 모두 모든 스타일 속성을 완전히 지원합니다 (폰트, 배경색, 테두리, 패딩 등).
+
+
diff --git a/docs/content/docs/components/overlays/naver-map-info-window.mdx b/docs/content/docs/components/overlays/naver-map-info-window.mdx
new file mode 100644
index 00000000..9ed54bc5
--- /dev/null
+++ b/docs/content/docs/components/overlays/naver-map-info-window.mdx
@@ -0,0 +1,61 @@
+---
+title: "NaverMapInfoWindow"
+description: "Component for displaying info windows above markers or coordinates"
+---
+
+## Overview
+
+NaverMapInfoWindow displays an information balloon on the map.
+It can be attached to a marker via `identifier` or rendered at an arbitrary coordinate using `latitude` and `longitude`.
+
+Platform notes:
+- Android: supports full styling
+- iOS: supports full styling (using NMFOverlayImageDataSource protocol)
+
+## Basic Usage
+
+```tsx
+import { NaverMapView, NaverMapMarkerOverlay, NaverMapInfoWindow } from '@mj-studio/react-native-naver-map';
+
+function MapWithInfoWindow() {
+ return (
+
+ {/* 1) Attach to a marker via identifier */}
+
+
+
+ {/* 2) Render at a coordinate directly */}
+
+
+ );
+}
+```
+
+## Props
+
+`NaverMapInfoWindowProps`
+
+
+
+## Notes
+
+- When `identifier` is provided, the info window opens above the marker that has the same `identifier`.
+- If `identifier` is omitted, `latitude` and `longitude` must be provided and the window is shown at the coordinate.
+- `align`, `anchor`, and `offsetX/offsetY` help fine-tune placement around the marker.
+- Both Android and iOS support full styling customization (font, radius, border, background, padding).
+
+
diff --git a/example/src/App.tsx b/example/src/App.tsx
index f4702f59..d021d1e8 100644
--- a/example/src/App.tsx
+++ b/example/src/App.tsx
@@ -20,6 +20,7 @@ import { CitiesScreen } from './screens/CitiesScreen';
import { ClusteringScreen } from './screens/ClusteringScreen';
import { CommonScreen } from './screens/CommonScreen';
import { GroundScreen } from './screens/GroundScreen';
+import { InfoWindowScreen } from './screens/InfoWindowScreen';
import { LocationOverlayScreen } from './screens/LocationOverlayScreen';
import { MarkerScreen } from './screens/MarkerScreen';
import { MultiPathScreen } from './screens/MultiPathScreen';
@@ -41,6 +42,7 @@ const SCREENS = [
{ id: 'clustering', title: 'Clustering' },
{ id: 'locationOverlay', title: 'Location Overlay' },
{ id: 'cities', title: 'Cities (Performance Test)' },
+ { id: 'infowindow', title: 'InfoWindow' },
];
export default function App() {
diff --git a/example/src/screens/InfoWindowScreen.tsx b/example/src/screens/InfoWindowScreen.tsx
new file mode 100644
index 00000000..a53a6bcd
--- /dev/null
+++ b/example/src/screens/InfoWindowScreen.tsx
@@ -0,0 +1,128 @@
+import {
+ NaverMapInfoWindow,
+ NaverMapMarkerOverlay,
+} from '@mj-studio/react-native-naver-map';
+import React from 'react';
+import { Platform } from 'react-native';
+import { Header } from '../components/Header';
+import { ScreenLayout } from '../components/ScreenLayout';
+
+const Cameras = {
+ Jeju: {
+ latitude: 33.39530773,
+ longitude: 126.54656715029,
+ zoom: 8,
+ },
+};
+
+export const InfoWindowScreen = ({ onBack }: { onBack: () => void }) => {
+ return (
+ <>
+
+
+ {/* infoWindow 와 연결된 마커들 */}
+
+
+
+
+ {/* infoWindow 1: 마커와 연결된 infoWindow (font bold 사용) */}
+
+
+ {/* infoWindow 2: 마커와 연결된 infoWindow (borderRadius, backgroundColor 사용) */}
+
+
+ {/* infoWindow 3: 마커와 연결된 infoWindow (all custom styles) */}
+
+
+ {/* infoWindow 4: 좌표에 독립적으로 표시된 infoWindow */}
+
+
+ {/* infoWindow 5: 처음부터 닫힌 infoWindow */}
+
+
+ >
+ );
+};
diff --git a/ios/Overlay/InfoWindow/RNCNaverMapInfoWindow.h b/ios/Overlay/InfoWindow/RNCNaverMapInfoWindow.h
new file mode 100644
index 00000000..f214d3f3
--- /dev/null
+++ b/ios/Overlay/InfoWindow/RNCNaverMapInfoWindow.h
@@ -0,0 +1,34 @@
+//
+// RNCNaverMapInfoWindow.h
+// mj-studio-react-native-naver-map
+//
+// Created by AI Assistant
+//
+
+#import "ColorUtil.h"
+#import "FnUtil.h"
+#import "MacroUtil.h"
+#import
+#import
+#import
+#import
+#import
+
+#import "RCTFabricComponentsPlugins.h"
+#import
+#import
+#import
+#import
+#import
+
+@class RNCNaverMapViewImpl;
+@class RNCNaverMapMarker;
+
+@interface RNCNaverMapInfoWindow : RCTViewComponentView
+@property(nonatomic, strong) NMFInfoWindow* inner;
+
+- (void)setCurrentMapView:(NMFMapView*)mapView;
+- (void)setParentMapViewImpl:(RNCNaverMapViewImpl*)mapViewImpl;
+- (void)updateInfoWindowState;
+
+@end
diff --git a/ios/Overlay/InfoWindow/RNCNaverMapInfoWindow.mm b/ios/Overlay/InfoWindow/RNCNaverMapInfoWindow.mm
new file mode 100644
index 00000000..7a839c2b
--- /dev/null
+++ b/ios/Overlay/InfoWindow/RNCNaverMapInfoWindow.mm
@@ -0,0 +1,307 @@
+//
+// RNCNaverMapInfoWindow.mm
+// mj-studio-react-native-naver-map
+//
+// Created by AI Assistant
+//
+
+#import "RNCNaverMapInfoWindow.h"
+#import "RNCNaverMapMarker.h"
+#import "RNCNaverMapViewImpl.h"
+#import
+
+using namespace facebook::react;
+
+// Custom data source for InfoWindow with styling support
+@interface RNCNaverMapInfoWindowDataSource : NSObject
+@property(nonatomic, strong) NSString* text;
+@property(nonatomic, assign) CGFloat textSize;
+@property(nonatomic, strong) UIColor* textColor;
+@property(nonatomic, assign) NSInteger fontWeight;
+@property(nonatomic, strong) UIColor* backgroundColor;
+@property(nonatomic, assign) CGFloat borderRadius;
+@property(nonatomic, assign) CGFloat borderWidth;
+@property(nonatomic, strong) UIColor* borderColor;
+@property(nonatomic, assign) CGFloat paddingHorizontal;
+@property(nonatomic, assign) CGFloat paddingVertical;
+@end
+
+@implementation RNCNaverMapInfoWindowDataSource
+
+- (instancetype)init {
+ if (self = [super init]) {
+ _text = @"";
+ _textSize = 14.0;
+ _textColor = [UIColor blackColor];
+ _fontWeight = 400;
+ _backgroundColor = [UIColor whiteColor];
+ _borderRadius = 5.0;
+ _borderWidth = 1.0;
+ _borderColor = [UIColor colorWithRed:0.8 green:0.8 blue:0.8 alpha:1.0];
+ _paddingHorizontal = 10.0;
+ _paddingVertical = 10.0;
+ }
+ return self;
+}
+
+- (UIView*)viewWithOverlay:(NMFOverlay*)overlay {
+ // Return the styled view directly
+ return [self createStyledView];
+}
+
+- (UIView*)createStyledView {
+ // Create label with text
+ UILabel* label = [[UILabel alloc] init];
+ NSString* displayText = (_text && _text.length > 0) ? _text : @" ";
+ label.text = displayText;
+ label.font = [self createFont];
+ label.textColor = _textColor;
+ label.textAlignment = NSTextAlignmentCenter;
+ label.numberOfLines = 0;
+
+ // Calculate label size
+ CGSize maxSize = CGSizeMake(300, CGFLOAT_MAX);
+ CGSize labelSize = [displayText boundingRectWithSize:maxSize
+ options:NSStringDrawingUsesLineFragmentOrigin
+ attributes:@{NSFontAttributeName : label.font}
+ context:nil]
+ .size;
+
+ // Ensure minimum size for empty or very small text
+ CGFloat minWidth = 20.0;
+ CGFloat minHeight = _textSize;
+ labelSize.width = MAX(ceil(labelSize.width), minWidth);
+ labelSize.height = MAX(ceil(labelSize.height), minHeight);
+
+ // Add padding (horizontal and vertical separately)
+ CGFloat totalPaddingHorizontal = _paddingHorizontal * 2;
+ CGFloat totalPaddingVertical = _paddingVertical * 2;
+ CGSize containerSize =
+ CGSizeMake(labelSize.width + totalPaddingHorizontal, labelSize.height + totalPaddingVertical);
+
+ // Create container view
+ UIView* containerView =
+ [[UIView alloc] initWithFrame:CGRectMake(0, 0, containerSize.width, containerSize.height)];
+ containerView.backgroundColor = _backgroundColor;
+ containerView.layer.cornerRadius = _borderRadius;
+ containerView.layer.borderWidth = _borderWidth;
+ containerView.layer.borderColor = _borderColor.CGColor;
+ containerView.clipsToBounds = YES;
+
+ // Add label to container with padding
+ label.frame = CGRectMake(_paddingHorizontal, _paddingVertical, labelSize.width, labelSize.height);
+ [containerView addSubview:label];
+
+ return containerView;
+}
+
+- (UIFont*)createFont {
+ CGFloat fontSize = _textSize;
+
+ if (_fontWeight >= 700) {
+ return [UIFont boldSystemFontOfSize:fontSize];
+ } else if (_fontWeight >= 600) {
+ return [UIFont systemFontOfSize:fontSize weight:UIFontWeightSemibold];
+ } else if (_fontWeight >= 500) {
+ return [UIFont systemFontOfSize:fontSize weight:UIFontWeightMedium];
+ } else {
+ return [UIFont systemFontOfSize:fontSize weight:UIFontWeightRegular];
+ }
+}
+
+@end
+
+@interface RNCNaverMapInfoWindow ()
+
+@end
+
+@implementation RNCNaverMapInfoWindow {
+ NSString* _markerIdentifier;
+ BOOL _shouldBeOpen;
+ NMFMapView* _currentMapView;
+ RNCNaverMapViewImpl* _parentMapViewImpl;
+ RNCNaverMapInfoWindowDataSource* _customDataSource;
+}
+
+- (RCTBridge*)bridge {
+ return [RCTBridge currentBridge];
+}
+
+- (std::shared_ptr)emitter {
+ if (!_eventEmitter)
+ return nullptr;
+ return std::static_pointer_cast(_eventEmitter);
+}
+
+- (instancetype)init {
+ if ((self = [super init])) {
+ _inner = [NMFInfoWindow new];
+ _shouldBeOpen = YES; // Default isOpen = true
+
+ // Create custom data source with styling support
+ _customDataSource = [[RNCNaverMapInfoWindowDataSource alloc] init];
+ _inner.dataSource = _customDataSource;
+ }
+
+ return self;
+}
+
+- (instancetype)initWithFrame:(CGRect)frame {
+ if (self = [super initWithFrame:frame]) {
+ static const auto defaultProps = std::make_shared();
+ _props = defaultProps;
+ }
+
+ return self;
+}
+
+- (void)setCurrentMapView:(NMFMapView*)mapView {
+ _currentMapView = mapView;
+ [self updateInfoWindowState];
+}
+
+- (void)setParentMapViewImpl:(RNCNaverMapViewImpl*)mapViewImpl {
+ _parentMapViewImpl = mapViewImpl;
+ [self updateInfoWindowState];
+}
+
+- (void)updateInfoWindowState {
+ if (!_shouldBeOpen) {
+ [_inner close];
+ return;
+ }
+
+ if (!_currentMapView)
+ return;
+
+ // Try to find marker by identifier first
+ if (_markerIdentifier && _markerIdentifier.length > 0 && _parentMapViewImpl) {
+ RNCNaverMapMarker* markerView = _parentMapViewImpl.markerRegistry[_markerIdentifier];
+ if (markerView) {
+ // Open on marker (marker position is used automatically)
+ [_inner openWithMarker:markerView.inner];
+ return;
+ }
+ }
+
+ // Fall back to position
+ _inner.mapView = _currentMapView;
+}
+
+- (void)updateProps:(Props::Shared const&)props oldProps:(Props::Shared const&)oldProps {
+ const auto& prev = *std::static_pointer_cast(_props);
+ const auto& next = *std::static_pointer_cast(props);
+
+ if (!nmap::isCoordEqual(prev.coord, next.coord)) {
+ _inner.position = nmap::createLatLng(next.coord);
+ [self updateInfoWindowState];
+ }
+
+ if (prev.zIndexValue != next.zIndexValue)
+ _inner.zIndex = next.zIndexValue;
+ if (prev.globalZIndexValue != next.globalZIndexValue && isValidNumber(next.globalZIndexValue))
+ _inner.globalZIndex = next.globalZIndexValue;
+ if (prev.isHidden != next.isHidden)
+ _inner.hidden = next.isHidden;
+ if (prev.minZoom != next.minZoom)
+ _inner.minZoom = next.minZoom;
+ if (prev.maxZoom != next.maxZoom)
+ _inner.maxZoom = next.maxZoom;
+ if (prev.isMinZoomInclusive != next.isMinZoomInclusive)
+ _inner.isMinZoomInclusive = next.isMinZoomInclusive;
+ if (prev.isMaxZoomInclusive != next.isMaxZoomInclusive)
+ _inner.isMaxZoomInclusive = next.isMaxZoomInclusive;
+
+ if (!nmap::isAnchorEqual(prev.anchor, next.anchor))
+ _inner.anchor = nmap::createAnchorCGPoint(next.anchor);
+
+ if (prev.offsetX != next.offsetX)
+ _inner.offsetX = next.offsetX;
+ if (prev.offsetY != next.offsetY)
+ _inner.offsetY = next.offsetY;
+
+ if (prev.alpha != next.alpha)
+ _inner.alpha = next.alpha;
+
+ // Identifier handling
+ if (prev.identifier != next.identifier) {
+ _markerIdentifier = getNsStr(next.identifier);
+ [self updateInfoWindowState];
+ }
+
+ // IsOpen handling
+ if (prev.isOpen != next.isOpen) {
+ _shouldBeOpen = next.isOpen;
+ [self updateInfoWindowState];
+ }
+
+ // Text content and styling (now supported via custom data source!)
+ BOOL needsRedraw = NO;
+
+ if (prev.text != next.text) {
+ _customDataSource.text = getNsStr(next.text);
+ needsRedraw = YES;
+ }
+
+ if (prev.textSize != next.textSize) {
+ _customDataSource.textSize = next.textSize;
+ needsRedraw = YES;
+ }
+
+ if (prev.textColor != next.textColor) {
+ _customDataSource.textColor = nmap::intToColor(next.textColor);
+ needsRedraw = YES;
+ }
+
+ if (prev.fontWeight != next.fontWeight) {
+ _customDataSource.fontWeight = next.fontWeight;
+ needsRedraw = YES;
+ }
+
+ if (prev.infoWindowBackgroundColor != next.infoWindowBackgroundColor) {
+ _customDataSource.backgroundColor = nmap::intToColor(next.infoWindowBackgroundColor);
+ needsRedraw = YES;
+ }
+
+ if (prev.infoWindowBorderRadius != next.infoWindowBorderRadius) {
+ _customDataSource.borderRadius = next.infoWindowBorderRadius;
+ needsRedraw = YES;
+ }
+
+ if (prev.infoWindowBorderWidth != next.infoWindowBorderWidth) {
+ _customDataSource.borderWidth = next.infoWindowBorderWidth;
+ needsRedraw = YES;
+ }
+
+ if (prev.infoWindowBorderColor != next.infoWindowBorderColor) {
+ _customDataSource.borderColor = nmap::intToColor(next.infoWindowBorderColor);
+ needsRedraw = YES;
+ }
+
+ if (prev.infoWindowPaddingHorizontal != next.infoWindowPaddingHorizontal) {
+ _customDataSource.paddingHorizontal = next.infoWindowPaddingHorizontal;
+ needsRedraw = YES;
+ }
+
+ if (prev.infoWindowPaddingVertical != next.infoWindowPaddingVertical) {
+ _customDataSource.paddingVertical = next.infoWindowPaddingVertical;
+ needsRedraw = YES;
+ }
+
+ // Redraw the info window if any styling property changed
+ if (needsRedraw) {
+ [_inner invalidate];
+ }
+
+ [super updateProps:props oldProps:oldProps];
+}
+
+Class RNCNaverMapInfoWindowCls(void) {
+ return RNCNaverMapInfoWindow.class;
+}
+
++ (ComponentDescriptorProvider)componentDescriptorProvider {
+ return concreteComponentDescriptorProvider();
+}
+
+@end
diff --git a/ios/Overlay/Marker/RNCNaverMapMarker.mm b/ios/Overlay/Marker/RNCNaverMapMarker.mm
index fa122094..71ccb1ba 100644
--- a/ios/Overlay/Marker/RNCNaverMapMarker.mm
+++ b/ios/Overlay/Marker/RNCNaverMapMarker.mm
@@ -228,6 +228,11 @@ - (void)updateProps:(Props::Shared const&)props oldProps:(Props::Shared const&)o
_inner.subCaptionMaxZoom = caption.maxZoom;
}
+ // Set identifier as tag for InfoWindow lookup
+ if (prev.identifier != next.identifier) {
+ _inner.userInfo = @{@"identifier" : getNsStr(next.identifier)};
+ }
+
[super updateProps:props oldProps:oldProps];
// Ensure touch handler is properly set after marker properties are updated
diff --git a/ios/RNCNaverMapViewImpl.h b/ios/RNCNaverMapViewImpl.h
index 551ba899..739263fb 100644
--- a/ios/RNCNaverMapViewImpl.h
+++ b/ios/RNCNaverMapViewImpl.h
@@ -15,6 +15,7 @@
#import "RNCNaverMapClusterKey.h"
#import "RNCNaverMapClusterMarkerUpdater.h"
#import "RNCNaverMapGround.h"
+#import "RNCNaverMapInfoWindow.h"
#import "RNCNaverMapLeafMarkerUpdater.h"
#import "RNCNaverMapMarker.h"
#import "RNCNaverMapMultiPath.h"
@@ -47,6 +48,9 @@ using namespace facebook::react;
@property(nonatomic, assign) NSInteger animationDuration;
@property(nonatomic, assign) NSInteger animationEasing;
+// Marker registry for InfoWindow lookup
+@property(nonatomic, strong) NSMutableDictionary* markerRegistry;
+
- (void)setLocationOverlay:(const RNCNaverMapViewLocationOverlayStruct&)locationOverlay;
@end
diff --git a/ios/RNCNaverMapViewImpl.mm b/ios/RNCNaverMapViewImpl.mm
index 76d8f28b..2fafa5d2 100644
--- a/ios/RNCNaverMapViewImpl.mm
+++ b/ios/RNCNaverMapViewImpl.mm
@@ -38,6 +38,7 @@ - (RCTBridge*)bridge {
- (instancetype)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
_reactSubviews = [NSMutableArray new];
+ _markerRegistry = [NSMutableDictionary new];
[self.mapView addCameraDelegate:self];
[self.mapView setTouchDelegate:self];
@@ -70,6 +71,7 @@ - (void)callImageCancllers {
- (void)dealloc {
[_reactSubviews removeAllObjects];
+ [_markerRegistry removeAllObjects];
[self callImageCancllers];
}
@@ -87,8 +89,15 @@ - (void)insertReactSubview:(id)subview atIndex:(NSInteger)atIndex
// Our desired API is to pass up markers/overlays as children to the mapview component.
// This is where we intercept them and do the appropriate underlying mapview action.
if ([subview isKindOfClass:[RNCNaverMapMarker class]]) {
- auto marker = static_cast(subview).inner;
+ auto markerView = static_cast(subview);
+ auto marker = markerView.inner;
marker.mapView = self.mapView;
+
+ // Register marker by identifier
+ NSString* identifier = marker.userInfo[@"identifier"];
+ if (identifier && identifier.length > 0) {
+ _markerRegistry[identifier] = markerView;
+ }
} else if ([subview isKindOfClass:[RNCNaverMapCircle class]]) {
auto marker = static_cast(subview).inner;
marker.mapView = self.mapView;
@@ -113,6 +122,10 @@ - (void)insertReactSubview:(id)subview atIndex:(NSInteger)atIndex
marker.overlayImage = NMF_MARKER_IMAGE_GREEN;
}
marker.mapView = self.mapView;
+ } else if ([subview isKindOfClass:[RNCNaverMapInfoWindow class]]) {
+ auto infoWindowView = static_cast(subview);
+ [infoWindowView setCurrentMapView:self.mapView];
+ [infoWindowView setParentMapViewImpl:self];
} else {
NSArray>* childSubviews = [subview reactSubviews];
for (int i = 0; i < childSubviews.count; i++) {
@@ -126,7 +139,15 @@ - (void)removeReactSubview:(UIView*)subview {
// similarly, when the children are being removed we have to do the appropriate
// underlying mapview action here.
if ([subview isKindOfClass:[RNCNaverMapMarker class]]) {
- auto marker = static_cast(subview).inner;
+ auto markerView = static_cast(subview);
+ auto marker = markerView.inner;
+
+ // Unregister marker
+ NSString* identifier = marker.userInfo[@"identifier"];
+ if (identifier && identifier.length > 0) {
+ [_markerRegistry removeObjectForKey:identifier];
+ }
+
marker.mapView = nil;
marker.touchHandler = nil;
} else if ([subview isKindOfClass:[RNCNaverMapCircle class]]) {
diff --git a/package.json b/package.json
index 6f6eb35e..58c96092 100644
--- a/package.json
+++ b/package.json
@@ -166,6 +166,7 @@
"RNCNaverMapArrowheadPath": "RNCNaverMapArrowheadPath",
"RNCNaverMapCircle": "RNCNaverMapCircle",
"RNCNaverMapGround": "RNCNaverMapGround",
+ "RNCNaverMapInfoWindow": "RNCNaverMapInfoWindow",
"RNCNaverMapMarker": "RNCNaverMapMarker",
"RNCNaverMapMultiPath": "RNCNaverMapMultiPath",
"RNCNaverMapPath": "RNCNaverMapPath",
diff --git a/src/component/NaverMapInfoWindow.tsx b/src/component/NaverMapInfoWindow.tsx
new file mode 100644
index 00000000..7d7b5c05
--- /dev/null
+++ b/src/component/NaverMapInfoWindow.tsx
@@ -0,0 +1,244 @@
+import React, { type PropsWithChildren } from 'react';
+import { type ColorValue, processColor } from 'react-native';
+import type { Double } from 'react-native/Libraries/Types/CodegenTypes';
+import { getAlignIntValue } from '../internal/Util';
+import { Const } from '../internal/util/Const';
+import NativeNaverMapInfoWindow from '../spec/RNCNaverMapInfoWindowNativeComponent';
+import type { Align } from '../types/Align';
+import type { BaseOverlayProps } from '../types/BaseOverlayProps';
+import type { Coord } from '../types/Coord';
+
+export interface NaverMapInfoWindowProps
+ extends BaseOverlayProps,
+ Coord,
+ PropsWithChildren<{}> {
+ /**
+ * InfoWindow가 열릴 마커의 identifier
+ * 지정하면 해당 identifier를 가진 마커 위에 InfoWindow가 열립니다.
+ * identifier가 없으면 latitude, longitude 좌표에 직접 표시됩니다.
+ */
+ identifier?: string;
+
+ /**
+ * InfoWindow를 처음부터 열린 상태로 표시할지 여부
+ * @default true
+ */
+ isOpen?: boolean;
+
+ /**
+ * InfoWindow가 마커에 대해 열릴 때의 정렬 방향
+ * @default 'Top'
+ */
+ align?: Align;
+
+ /**
+ * 앵커 포인트를 지정합니다.
+ * 왼쪽 위가 (0, 0), 오른쪽 아래가 (1, 1)인 비율로 표현합니다.
+ * @default {x: 0.5, y: 1}
+ */
+ anchor?: { x: Double; y: Double };
+
+ /**
+ * X축 오프셋 (픽셀)
+ * @default 0
+ */
+ offsetX?: number;
+
+ /**
+ * Y축 오프셋 (픽셀)
+ * @default 0
+ */
+ offsetY?: number;
+
+ /**
+ * InfoWindow의 불투명도 (0~1)
+ * @default 1
+ */
+ alpha?: Double;
+
+ /**
+ * InfoWindow에 표시할 텍스트
+ */
+ text?: string;
+
+ /**
+ * 텍스트 크기
+ * @default 14
+ */
+ textSize?: Double;
+
+ /**
+ * 텍스트 색상
+ * @default 'black'
+ */
+ textColor?: ColorValue;
+
+ /**
+ * 폰트 굵기
+ * 'normal' | 'bold' | '100' | '200' | ... | '900'
+ * @default 'normal'
+ */
+ fontWeight?:
+ | 'normal'
+ | 'bold'
+ | '100'
+ | '200'
+ | '300'
+ | '400'
+ | '500'
+ | '600'
+ | '700'
+ | '800'
+ | '900';
+
+ /**
+ * 배경 색상
+ * @default 'white'
+ */
+ backgroundColor?: ColorValue;
+
+ /**
+ * 둥근 모서리 반경 (픽셀)
+ * @default 5
+ */
+ borderRadius?: number;
+
+ /**
+ * 테두리 두께 (픽셀)
+ * @default 1
+ */
+ borderWidth?: number;
+
+ /**
+ * 테두리 색상
+ * @default '#ccc'
+ */
+ borderColor?: ColorValue;
+
+ /**
+ * 수평 내부 여백 (픽셀, 좌우)
+ * @default 10
+ */
+ paddingHorizontal?: number;
+
+ /**
+ * 수직 내부 여백 (픽셀, 상하)
+ * @default 10
+ */
+ paddingVertical?: number;
+}
+
+/**
+ * 네이버 지도에 InfoWindow를 표시하는 컴포넌트입니다.
+ *
+ * InfoWindow는 마커의 위 또는 지도의 특정 지점에 부가적인 정보를 나타내기 위한 오버레이입니다.
+ * 주로 말풍선 형태로 구성되어 텍스트를 표시하는 용도로 사용합니다.
+ *
+ * **플랫폼별 스타일 지원:**
+ * - Android: 모든 스타일 속성 지원 (ViewAdapter를 통한 커스텀 뷰 렌더링)
+ * - iOS: 모든 스타일 속성 지원 (NMFOverlayImageDataSource를 통한 커스텀 이미지 렌더링)
+ *
+ * @example
+ * ```tsx
+ * // 1. 마커에 연결된 InfoWindow (권장)
+ *
+ *
+ *
+ * // 2. 특정 좌표에 InfoWindow 직접 표시
+ *
+ * ```
+ *
+ * @see https://navermaps.github.io/android-map-sdk/guide-ko/5-3.html
+ * @see https://navermaps.github.io/ios-map-sdk/guide-ko/5-3.html
+ */
+export const NaverMapInfoWindow = ({
+ latitude,
+ longitude,
+ zIndex = 0,
+ globalZIndex = Const.NULL_NUMBER,
+ isHidden = false,
+ minZoom = Const.MIN_ZOOM,
+ maxZoom = Const.MAX_ZOOM,
+ isMinZoomInclusive = true,
+ isMaxZoomInclusive = true,
+
+ identifier,
+ isOpen = true,
+ align = 'Top',
+ anchor = { x: 0.5, y: 1 },
+ offsetX = 0,
+ offsetY = 0,
+ alpha = 1,
+
+ text,
+ textSize = 14,
+ textColor = 'black',
+ fontWeight = 'normal',
+ backgroundColor = 'white',
+ borderRadius = 5,
+ borderWidth = 1,
+ borderColor = '#ccc',
+ paddingHorizontal = 10,
+ paddingVertical = 10,
+
+ children,
+}: NaverMapInfoWindowProps) => {
+ const fontWeightValue = (() => {
+ if (fontWeight === 'normal') return 400;
+ if (fontWeight === 'bold') return 700;
+ return parseInt(fontWeight, 10);
+ })();
+ return (
+
+ {children}
+
+ );
+};
diff --git a/src/component/NaverMapMarkerOverlay.tsx b/src/component/NaverMapMarkerOverlay.tsx
index 5ea55d74..f9989753 100644
--- a/src/component/NaverMapMarkerOverlay.tsx
+++ b/src/component/NaverMapMarkerOverlay.tsx
@@ -140,6 +140,11 @@ export interface NaverMapMarkerOverlayProps
extends BaseOverlayProps,
Coord,
PropsWithChildren<{}> {
+ /**
+ * 마커의 고유 식별자입니다.
+ * InfoWindow를 이 마커에 연결할 때 사용됩니다.
+ */
+ identifier?: string;
/**
* 마커의 너비입니다.
*
@@ -322,6 +327,7 @@ export const NaverMapMarkerOverlay = ({
isMinZoomInclusive,
isMaxZoomInclusive,
+ identifier,
width = Const.NULL_NUMBER,
height = Const.NULL_NUMBER,
@@ -344,7 +350,9 @@ export const NaverMapMarkerOverlay = ({
}: NaverMapMarkerOverlayProps) => {
nAssert(
Children.count(children) <= 1,
- `[NaverMapMarkerOverlay] children count should be equal or less than 1, is ${Children.count(children)}`
+ `[NaverMapMarkerOverlay] children count should be equal or less than 1, is ${Children.count(
+ children
+ )}`
);
const _caption: NativeCaptionProp = (() => {
@@ -379,6 +387,7 @@ export const NaverMapMarkerOverlay = ({
return (
;
+ minZoom: Double;
+ maxZoom: Double;
+ isMinZoomInclusive?: WithDefault;
+ isMaxZoomInclusive?: WithDefault;
+}
+
+////////////////////
+
+interface Props extends BaseOverlay, ViewProps {
+ /**
+ * InfoWindow 위치 좌표
+ * identifier가 없으면 이 좌표에 직접 표시
+ * identifier가 있으면 대체 위치로 사용
+ */
+ coord: Readonly<{
+ latitude: Double;
+ longitude: Double;
+ }>;
+ /**
+ * InfoWindow가 열릴 마커의 identifier
+ * 지정하면 해당 마커 위에 열림
+ */
+ identifier?: string;
+ /**
+ * InfoWindow를 처음부터 열린 상태로 표시할지 여부
+ * @default true
+ */
+ isOpen?: WithDefault;
+ /**
+ * InfoWindow가 마커에 대해 열릴 때의 정렬 방향
+ * Top = 0, TopLeft = 1, TopRight = 2, Right = 3,
+ * BottomRight = 4, Bottom = 5, BottomLeft = 6, Left = 7, Center = 8
+ * @default 0 (Top)
+ */
+ align?: WithDefault;
+ /**
+ * 앵커 포인트 (0~1 범위의 비율)
+ * 왼쪽 위가 (0, 0), 오른쪽 아래가 (1, 1)
+ * @default {x: 0.5, y: 1}
+ */
+ anchor?: Readonly<{ x: Double; y: Double }>;
+ /**
+ * X축 오프셋 (픽셀)
+ * @default 0
+ */
+ offsetX?: WithDefault;
+ /**
+ * Y축 오프셋 (픽셀)
+ * @default 0
+ */
+ offsetY?: WithDefault;
+ /**
+ * 불투명도 (0~1)
+ * @default 1
+ */
+ alpha?: WithDefault;
+ /**
+ * 텍스트 내용
+ */
+ text?: string;
+ /**
+ * 텍스트 크기
+ * @default 14
+ */
+ textSize?: WithDefault;
+ /**
+ * 텍스트 색상
+ */
+ textColor?: Int32;
+ /**
+ * 폰트 굵기 (100-900, 400=normal, 700=bold)
+ * @default 400
+ */
+ fontWeight?: WithDefault;
+ /**
+ * 배경 색상
+ */
+ infoWindowBackgroundColor?: Int32;
+ /**
+ * 둥근 모서리 반경 (픽셀)
+ * @default 5
+ */
+ infoWindowBorderRadius?: WithDefault;
+ /**
+ * 테두리 두께 (픽셀)
+ * @default 1
+ */
+ infoWindowBorderWidth?: WithDefault;
+ /**
+ * 테두리 색상
+ */
+ infoWindowBorderColor?: Int32;
+ /**
+ * 수평 내부 여백 (픽셀)
+ * @default 10
+ */
+ infoWindowPaddingHorizontal?: WithDefault;
+ /**
+ * 수직 내부 여백 (픽셀)
+ * @default 10
+ */
+ infoWindowPaddingVertical?: WithDefault;
+}
+
+export default codegenNativeComponent('RNCNaverMapInfoWindow');
diff --git a/src/spec/RNCNaverMapMarkerNativeComponent.ts b/src/spec/RNCNaverMapMarkerNativeComponent.ts
index b3a9e653..0370f3b4 100644
--- a/src/spec/RNCNaverMapMarkerNativeComponent.ts
+++ b/src/spec/RNCNaverMapMarkerNativeComponent.ts
@@ -55,6 +55,10 @@ export type NativeImageProp = Readonly<{
////////////////////
interface Props extends BaseOverlay, ViewProps {
+ /**
+ * 마커의 고유 식별자 (InfoWindow 연결용)
+ */
+ identifier?: string;
coord: Readonly<{
latitude: Double;
longitude: Double;