Skip to content

[FEAT/#100] web-hook 기반 카카오 맵 연동#101

Open
taegeon2 wants to merge 1 commit into
feat/#97-map-uifrom
feat/#100-kakao-map
Open

[FEAT/#100] web-hook 기반 카카오 맵 연동#101
taegeon2 wants to merge 1 commit into
feat/#97-map-uifrom
feat/#100-kakao-map

Conversation

@taegeon2
Copy link
Copy Markdown
Contributor

📝 요약

  • 학생/관리자/제휴업체 맵 탭에 카카오맵을 통합하고, 내 위치 표시·실시간 갱신·내 위치 포커스 기능을 추가합니다.

⚙️ 작업 내용 ─

  • shared/ui/kakao-map/KakaoMap.tsx: react-native-webview 기반 카카오맵 컴포넌트 신규 추가. forwardRefpanTo(lat, lng)
    인터페이스를 노출하여 외부에서 카메라 이동 제어 가능.

  • 마커 생성 타이밍 개선: initMap 단계에서 내 위치 마커(파란 점 + 시야 cone)를 즉시 생성하도록 변경.

  • widgets/map/model/useUserLocation.ts: 위치 권한·watchPositionAsync·watchHeadingAsync 로직을 훅으로 분리. useFocusEffect로 탭 포커스 단위 구독·해제.

  • widgets/map/ui/MapLocateButton.tsx: 우측 상단 위치 포커스 버튼 신규 컴포넌트. 누르면 어디를 보고 있든 panTo로 내 위치까지
    부드럽게 카메라 이동.
    설정.

  • .gitignore.env 추가.

    🔗 관련 이슈

    ✅ 체크리스트

    • 코딩 컨벤션(Biome/Lint)을 준수하였습니다.
    • 모든 타입 에러를 해결하였습니다. (Typecheck)
    • 변경 사항에 대한 테스트를 마쳤습니다.
    • 불필요한 로그(console.log)를 제거하였습니다.

    💬 리뷰어에게

    ⚠️ 지도 회전 제스처 미지원 ⚠️

    현재 구조는 카카오맵 JavaScript SDK를 react-native-webview에 띄우는 방식이라 두 손가락 회전 제스처를 SDK 차원에서 지원하지 않습니다. 카카오맵 모바일 앱이 회전되는 건 카카오 네이티브 SDK 기반이라 가능한 것인데 이 경우 저희 빌드 과정도 달라지고 별도의 추가적인 설정이 많이 필요하여 우선 웹훅 방식으로 구현하였습니다.

    회전을 적용하려면 @react-native-kakao/map 같은 네이티브 래퍼로 마이그레이션해야 하며, 이는 단순 코드 변경이 아니라 팀 워크플로우 전환을 동반합니다:

    • Expo Go 사용 종료 → 팀 전체가 EAS dev client로 전환 (한 번 깔면 됨)
    • 카카오 디벨로퍼스 추가 설정: Native App Key 신규 발급, Android 키 해시(디버그 + EAS 빌드용 각 1개), iOS 번들 ID 등록
    • iOS 빌드는 Mac 또는 EAS 클라우드 필수

    코드 작업 자체는 수정하면 되긴 하는데, 워크플로우 전환은 팀 회의에서 결정할 사안이라 본 PR에서는 의도적으로 보류했습니다.

    기타 참고

    • 관리자/제휴 맵이 학생 대비 첫 진입에서 느려 보이는 현상이 있는데, 백그라운드에서 지도를 미리 로딩하는 개선은 별도 PR로 분리 예정입니다.
image

@taegeon2 taegeon2 self-assigned this Apr 28, 2026
@github-actions github-actions Bot added del 쓸모없는 코드나 파일 삭제 feature 새로운 기능 구현 mod 코드 수정 및 내부 파일 수정 labels Apr 28, 2026
@github-actions
Copy link
Copy Markdown

Thanks for the contribution!
I have applied any labels matching special text in your title and description.

Please review the labels and make any necessary changes.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request implements a map feature using the Kakao Maps API within a React Native WebView. It introduces a custom useUserLocation hook to track the user's coordinates and heading, a KakaoMap component that manages the map's state and overlays, and a UI button to center the map on the user's current location. The review feedback focuses on improving robustness through better error handling for environment variables and asynchronous location requests, enhancing code readability within the WebView's HTML template, and optimizing performance by memoizing the location focus handler.


export const KakaoMap = forwardRef<KakaoMapHandle, KakaoMapProps>(
function KakaoMap({ initialCenter = SOONGSIL, myLocation, heading }, ref) {
const appKey = process.env.EXPO_PUBLIC_KAKAO_JS_KEY ?? "";
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

process.env.EXPO_PUBLIC_KAKAO_JS_KEY가 설정되지 않은 경우, appKey는 빈 문자열이 되어 카카오맵 SDK가 정상적으로 동작하지 않습니다. 이 경우 WebView 내부에서 오류가 발생하여 디버깅이 어려울 수 있습니다. 환경 변수가 없을 때 즉시 오류를 발생시켜 개발 단계에서 문제를 빠르게 인지할 수 있도록 하는 것이 좋습니다.

const appKey = process.env.EXPO_PUBLIC_KAKAO_JS_KEY;
		if (!appKey) {
			throw new Error(
				"Kakao Map JavaScript API key is not set. Please set EXPO_PUBLIC_KAKAO_JS_KEY in your .env file.",
			);
		}

Comment on lines +21 to +53
(async () => {
const { status } = await Location.requestForegroundPermissionsAsync();
if (status !== "granted") {
setCenter(SOONGSIL);
return;
}

const loc = await Location.getCurrentPositionAsync({});
const userLoc = { lat: loc.coords.latitude, lng: loc.coords.longitude };
setCenter(userLoc);
setMyLocation(userLoc);

positionSub = await Location.watchPositionAsync(
{
accuracy: Location.Accuracy.High,
timeInterval: 2000,
distanceInterval: 3,
},
(l) =>
setMyLocation({ lat: l.coords.latitude, lng: l.coords.longitude }),
);

headingSub = await Location.watchHeadingAsync((hdg) => {
const rounded = Math.round(hdg.magHeading);
if (
lastHeadingRef.current === null ||
Math.abs(rounded - lastHeadingRef.current) >= HEADING_THRESHOLD_DEG
) {
lastHeadingRef.current = rounded;
setHeading(rounded);
}
});
})();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

useFocusEffect 내의 비동기 IIFE에서 오류 처리가 누락되었습니다. Location.getCurrentPositionAsync와 같은 함수는 다양한 이유(예: 타임아웃, 서비스 비활성화)로 실패하여 Promise를 reject할 수 있습니다. 이 경우 처리되지 않은 Promise rejection이 발생하여 앱이 예기치 않게 동작할 수 있습니다. try...catch 블록을 사용하여 오류를 처리하고, 오류 발생 시 적절한 폴백(fallback) 로직을 수행하는 것이 중요합니다.

			(async () => {
				try {
					const { status } = await Location.requestForegroundPermissionsAsync();
					if (status !== "granted") {
						setCenter(SOONGSIL);
						return;
					}

					const loc = await Location.getCurrentPositionAsync({});
					const userLoc = { lat: loc.coords.latitude, lng: loc.coords.longitude };
					setCenter(userLoc);
					setMyLocation(userLoc);

					positionSub = await Location.watchPositionAsync(
						{
							accuracy: Location.Accuracy.High,
							timeInterval: 2000,
							distanceInterval: 3,
						},
						(l) =>
							setMyLocation({ lat: l.coords.latitude, lng: l.coords.longitude }),
					);

					headingSub = await Location.watchHeadingAsync((hdg) => {
						const rounded = Math.round(hdg.magHeading);
						if (
							lastHeadingRef.current === null ||
							Math.abs(rounded - lastHeadingRef.current) >= HEADING_THRESHOLD_DEG
						) {
							lastHeadingRef.current = rounded;
							setHeading(rounded);
						}
					});
				} catch (error) {
					console.error("Failed to get user location:", error);
					setCenter(SOONGSIL);
				}
			})();

Comment on lines +74 to +77
try {
const data = JSON.parse(event.nativeEvent.data) as { type: string };
if (data.type === "MAP_READY") setIsMapReady(true);
} catch {}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

handleMessage 함수의 catch 블록이 비어 있어 JSON.parse 실패 시 오류가 조용히 무시됩니다. 이는 디버깅을 어렵게 만들 수 있습니다. 최소한 개발 환경에서는 오류를 콘솔에 기록하는 것이 좋습니다.

try {
				const data = JSON.parse(event.nativeEvent.data) as { type: string };
				if (data.type === "MAP_READY") setIsMapReady(true);
			} catch (error) {
				console.error("Failed to parse message from WebView:", error);
			}

Comment on lines +123 to +129
var content =
'<div style="position:relative;width:16px;height:16px;">' +
'<div id="loc-cone" style="position:absolute;bottom:8px;left:2px;width:12px;height:20px;transform-origin:6px 100%;transform:rotate(0deg);transition:transform 0.2s linear;">' +
'<svg width="12" height="20" viewBox="0 0 12 20" fill="${coneFill}"><polygon points="6,0 0,20 12,20"/></svg>' +
'</div>' +
'<div style="position:absolute;top:0;right:0;bottom:0;left:0;background:${primary};border:2.5px solid ${canvas};border-radius:50%;box-shadow:0 0 0 6px ${ring};"></div>' +
'</div>';
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

createMyLocationOverlay 함수 내에서 content 변수를 생성할 때 문자열 접합(+)을 사용하고 있습니다. 이 방식은 가독성이 떨어지고 유지보수가 어렵습니다. 전체 HTML을 생성하는 buildMapHtml 함수에서처럼 템플릿 리터럴을 사용하면 코드가 더 깔끔하고 읽기 쉬워집니다.

      var content = `
        <div style="position:relative;width:16px;height:16px;">
          <div id="loc-cone" style="position:absolute;bottom:8px;left:2px;width:12px;height:20px;transform-origin:6px 100%;transform:rotate(0deg);transition:transform 0.2s linear;">
            <svg width="12" height="20" viewBox="0 0 12 20" fill="${coneFill}"><polygon points="6,0 0,20 12,20"/></svg>
          </div>
          <div style="position:absolute;top:0;right:0;bottom:0;left:0;background:${primary};border:2.5px solid ${canvas};border-radius:50%;box-shadow:0 0 0 6px ${ring};"></div>
        </div>`;

Comment on lines +13 to +16
const handleFocusToMyLocation = () => {
if (!myLocation) return;
kakaoRef.current?.panTo(myLocation.lat, myLocation.lng);
};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

handleFocusToMyLocation 함수는 MapView 컴포넌트가 렌더링될 때마다 새로 생성됩니다. 이 함수는 myLocation 상태에 의존하므로, useCallback을 사용하여 메모이제이션하면 불필요한 함수 재생성을 방지하고 성능을 최적화할 수 있습니다. useCallbackreact에서 import해야 합니다.

Suggested change
const handleFocusToMyLocation = () => {
if (!myLocation) return;
kakaoRef.current?.panTo(myLocation.lat, myLocation.lng);
};
const handleFocusToMyLocation = useCallback(() => {
if (!myLocation) return;
kakaoRef.current?.panTo(myLocation.lat, myLocation.lng);
}, [myLocation]);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

del 쓸모없는 코드나 파일 삭제 feature 새로운 기능 구현 mod 코드 수정 및 내부 파일 수정

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant