Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .claude/settings.local.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
"Bash(grep:*)",
"Bash(pnpm format:*)",
"Bash(pnpm turbo:android:*)",
"Bash(git init:*)"
"Bash(git init:*)",
"Bash(pnpm run:*)"
],
"deny": [],
"ask": []
Expand Down
8 changes: 8 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,14 @@ jobs:
- name: Install Ktlint
run: curl -sSLO https://github.com/pinterest/ktlint/releases/download/1.8.0/ktlint && chmod a+x ktlint && sudo mv ktlint /usr/local/bin/

- name: Install clang-format 21.1.8
run: |
python3 -m pip install --user clang-format==21.1.8
echo "$HOME/.local/bin" >> "$GITHUB_PATH"

- name: Verify clang-format version
run: clang-format --version

- name: Check
run: pnpm run t

Expand Down
7 changes: 0 additions & 7 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -84,10 +84,3 @@ example/ios/Secret.xcconfig

example/.watchman-*
*.bak




# Git worktrees (Crystal)
/worktrees/
/worktree-*/
162 changes: 162 additions & 0 deletions .skills/dev-patterns.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
# NaverMap Dev Patterns

This file is a quick pattern reference extracted from `CLAUDE.md`.
Use this when implementing native-spec-linked features and when you need concrete code shapes.

## TypeScript: Fabric Native Component Spec

Rules:
- Define props with codegen types (`Double`, `Int32`, `WithDefault`, `DirectEventHandler`).
- Keep component spec files in `src/spec/` as source of truth.
- Use `codegenNativeComponent<Props>()`.

```ts
import { codegenNativeComponent, type ViewProps } from 'react-native';
import type {
DirectEventHandler,
Double,
Int32,
WithDefault,
} from 'react-native/Libraries/Types/CodegenTypes';

interface Props extends ViewProps {
coord: Readonly<{ latitude: Double; longitude: Double }>;
onTapOverlay?: DirectEventHandler<Readonly<{}>>;
isHidden?: WithDefault<boolean, false>;
tintColor?: Int32;
}

export default codegenNativeComponent<Props>('RNCNaverMapMarker');
```

## TypeScript: Native Commands Spec

Rules:
- Define imperative methods via `codegenNativeCommands`.
- First arg is always `React.ElementRef<ComponentType>`.
- Keep command names aligned with native handlers.

```ts
interface NativeCommands {
animateCameraTo: (
ref: React.ElementRef<ComponentType>,
latitude: Double,
longitude: Double,
duration?: Int32
) => void;
}

export const Commands: NativeCommands = codegenNativeCommands<NativeCommands>({
supportedCommands: ['animateCameraTo'],
});
```

## TypeScript: TurboModule Spec

Rules:
- Define module interface with `TurboModule`.
- Access via `TurboModuleRegistry.getEnforcing()`.

```ts
import { type TurboModule, TurboModuleRegistry } from 'react-native';

export interface Spec extends TurboModule {
createInfoWindow(id: string): void;
}

export default TurboModuleRegistry.getEnforcing<Spec>('RNCNaverMapUtil');
```

## iOS: Fabric Component Pattern

Rules:
- Extend `RCTViewComponentView`.
- Implement `initWithFrame`, `updateProps`.
- Cast props/emitter safely.

```objc
- (void)updateProps:(Props::Shared const&)props oldProps:(Props::Shared const&)oldProps {
const auto& prev = *std::static_pointer_cast<MyProps const>(_props);
const auto& next = *std::static_pointer_cast<MyProps const>(props);

if (prev.width != next.width) {
_inner.width = next.width;
}

[super updateProps:props oldProps:oldProps];
}
```

## iOS: Command Handling

Rules:
- Implement command methods on component view.
- Route commands through generated handler.

```objc
- (void)handleCommand:(const NSString*)commandName args:(const NSArray*)args {
RCTMyComponentHandleCommand(self, commandName, args);
}
```

## Android: ViewManager + Codegen Delegate

Rules:
- Extend generated manager spec class.
- Use generated delegate.

```kt
abstract class MyManagerSpec<T : ViewGroup> : ViewGroupManager<T>(), MyManagerInterface<T> {
private val mDelegate: ViewManagerDelegate<T> = MyManagerDelegate(this)
override fun getDelegate(): ViewManagerDelegate<T>? = mDelegate
}
```

## Android: Event Emission Pattern

Rules:
- Emit via ReactContext JS module.
- Event names must match TS `DirectEventHandler` props.

```kt
val event = Arguments.createMap().apply {
putDouble("latitude", marker.position.latitude)
putDouble("longitude", marker.position.longitude)
}

reactContext
.getJSModule(RCTEventEmitter::class.java)
.receiveEvent(view.id, "onTapOverlay", event)
```

## TypeScript: Color Prop to Native Int

Rules:
- Accept `ColorValue` on JS side.
- Convert with `processColor` before passing native prop.

```ts
import { processColor, type ColorValue } from 'react-native';

const tintColorNative = processColor(tintColor as ColorValue) as number;
```

## JSDoc Baseline

Public APIs should include:
- `@param`
- `@returns`
- `@example`
- `@default`
- `@platform` (when platform-scoped)

## Marker Image Handling (Native)

Rules:
- Support symbol, RN asset, HTTP URL, native asset, and custom view cases.
- Cancel pending image work on cleanup.
- Keep behavior parity between iOS and Android implementations.

---

For full details and extended examples, see `CLAUDE.md`.
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
package com.mjstudio.reactnativenavermap

import com.facebook.react.ReactPackage
import com.facebook.react.BaseReactPackage
import com.facebook.react.bridge.NativeModule
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.module.model.ReactModuleInfo
import com.facebook.react.module.model.ReactModuleInfoProvider
import com.facebook.react.uimanager.ViewManager
import com.mjstudio.reactnativenavermap.mapview.RNCNaverMapViewManager
import com.mjstudio.reactnativenavermap.module.RNCNaverMapUtilModule
import com.mjstudio.reactnativenavermap.overlay.arrowheadpath.RNCNaverMapArrowheadPathManager
import com.mjstudio.reactnativenavermap.overlay.circle.RNCNaverMapCircleManager
import com.mjstudio.reactnativenavermap.overlay.ground.RNCNaverMapGroundManager
Expand All @@ -14,7 +17,7 @@ import com.mjstudio.reactnativenavermap.overlay.path.RNCNaverMapPathManager
import com.mjstudio.reactnativenavermap.overlay.polygon.RNCNaverMapPolygonManager
import com.mjstudio.reactnativenavermap.overlay.polyline.RNCNaverMapPolylineManager

class RNCNaverMapPackage : ReactPackage {
class RNCNaverMapPackage : BaseReactPackage() {
override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> =
mutableListOf<ViewManager<*, *>>().apply {
add(RNCNaverMapViewManager())
Expand All @@ -28,5 +31,27 @@ class RNCNaverMapPackage : ReactPackage {
add(RNCNaverMapGroundManager())
}

override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> = emptyList()
override fun getModule(
name: String,
reactContext: ReactApplicationContext,
): NativeModule? =
when (name) {
NativeRNCNaverMapUtilSpec.NAME -> RNCNaverMapUtilModule(reactContext)
else -> null
}

override fun getReactModuleInfoProvider() =
ReactModuleInfoProvider {
mapOf(
NativeRNCNaverMapUtilSpec.NAME to
ReactModuleInfo(
name = NativeRNCNaverMapUtilSpec.NAME,
className = NativeRNCNaverMapUtilSpec.NAME,
canOverrideExistingModule = false,
needsEagerInit = false,
isCxxModule = false,
isTurboModule = true,
),
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -846,6 +846,35 @@ class RNCNaverMapViewManager : RNCNaverMapViewManagerSpec<RNCNaverMapViewWrapper
}
}

override fun showInfoWindow(
view: RNCNaverMapViewWrapper?,
infoWindowId: String?,
latitude: Double,
longitude: Double,
) = view.withMap { map ->
if (infoWindowId == null) return@withMap

val module = reactAppContext.getNativeModule(com.mjstudio.reactnativenavermap.module.RNCNaverMapUtilModule::class.java)
val infoWindow = module?.getInfoWindow(infoWindowId) ?: return@withMap

infoWindow.position = LatLng(latitude, longitude)
infoWindow.open(map)
module.markAsOpen(infoWindowId)
}

override fun hideInfoWindow(
view: RNCNaverMapViewWrapper?,
infoWindowId: String?,
) = view.withMap { map ->
if (infoWindowId == null) return@withMap

val module = reactAppContext.getNativeModule(com.mjstudio.reactnativenavermap.module.RNCNaverMapUtilModule::class.java)
val infoWindow = module?.getInfoWindow(infoWindowId) ?: return@withMap

infoWindow.close()
module.markAsClosed(infoWindowId)
}

companion object {
const val NAME = "RNCNaverMapView"
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package com.mjstudio.reactnativenavermap.module

import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReactMethod
import com.facebook.react.module.annotations.ReactModule
import com.mjstudio.reactnativenavermap.RNCNaverMapUtilSpec
import com.naver.maps.map.overlay.InfoWindow

@ReactModule(name = RNCNaverMapUtilModule.NAME)
class RNCNaverMapUtilModule(
reactContext: ReactApplicationContext,
) : RNCNaverMapUtilSpec(reactContext) {
companion object {
const val NAME = "RNCNaverMapUtil"
}

private val infoWindows = mutableMapOf<String, InfoWindow>()
private val infoWindowContents = mutableMapOf<String, InfoWindowContent>()
private val openInfoWindows = mutableSetOf<String>()

override fun getName(): String = NAME

@ReactMethod
override fun createInfoWindow(id: String) {
if (infoWindows.containsKey(id)) return

val infoWindow =
InfoWindow().apply {
adapter =
object : InfoWindow.DefaultTextAdapter(reactApplicationContext) {
override fun getText(infoWindow: InfoWindow): CharSequence {
val content = infoWindowContents[id]
return content?.let {
if (it.subtitle.isNullOrEmpty()) {
it.title
} else {
"${it.title}\n${it.subtitle}"
}
} ?: ""
}
}
// todo
// setOnClickListener {
// true
// }
}

infoWindows[id] = infoWindow
}

@ReactMethod
override fun destroyInfoWindow(id: String) {
infoWindows[id]?.let { infoWindow ->
infoWindow.close()
infoWindows.remove(id)
infoWindowContents.remove(id)
openInfoWindows.remove(id)
}
}

@ReactMethod
override fun closeInfoWindow(id: String) {
infoWindows[id]?.let { infoWindow ->
infoWindow.close()
openInfoWindows.remove(id)
}
}

@ReactMethod
override fun setInfoWindowContent(
id: String,
title: String,
subtitle: String?,
) {
infoWindowContents[id] = InfoWindowContent(title, subtitle)
val infoWindow = infoWindows[id] ?: return

// 이미 열려있다면 업데이트
if (isInfoWindowActuallyOpen(infoWindow)) {
infoWindow.invalidate()
}
}

@ReactMethod(isBlockingSynchronousMethod = true)
override fun isInfoWindowOpen(id: String): Boolean = infoWindows[id]?.let(::isInfoWindowActuallyOpen) ?: false

// ViewManager에서 접근할 내부 메서드들
fun getInfoWindow(id: String): InfoWindow? = infoWindows[id]

fun markAsOpen(id: String) {
openInfoWindows.add(id)
}

fun markAsClosed(id: String) {
openInfoWindows.remove(id)
}

private fun isInfoWindowActuallyOpen(infoWindow: InfoWindow): Boolean = infoWindow.map != null || infoWindow.marker != null

private data class InfoWindowContent(
val title: String,
val subtitle: String?,
)
}
Loading