✨ Feat: 산책 지오펜스(울타리) 및 실시간 위치 추적 기능 - #63
Conversation
- NCP Maps 스크립트 로더 추가 (중복 로드 방지) - 네이버 지도 전역 타입 최소 선언 - WalkMap 컴포넌트로 지도 렌더링 및 언마운트 시 정리 - env에 VITE_NAVER_MAP_CLIENT_ID 추가 `DoDo-Project#62`
- Fence REST API 타입 및 axios 함수 6종 추가 (생성/상태조회/토글/범위수정/경계조회/경계목록) - 조회·뮤테이션 react-query 훅 추가 (useFenceBoundaries/useFenceStatus/useCreateFence /useToggleFence/useUpdateFenceRange) - queryKey에 fence 키 추가 `DoDo-Project#62`
- 네이버 지도 전역 타입에 Circle 추가 - 울타리 경계 목록을 지도에 원(Circle)으로 렌더링 - 활성/비활성 상태에 따라 색상 구분, 첫 울타리로 중심 이동 `DoDo-Project#62`
- 지도 클릭으로 울타리 중심 지정 + 점선 미리보기 원 표시 - WalkMap을 props 기반(boundaries/draftCenter/onMapClick)으로 리팩터링 - FenceControlPanel 추가: 펫 선택, 이름·반경 입력, 생성/수정/토글 - naver 타입에 지도 클릭 이벤트(addListener)·LatLng 접근자 추가 - 변경 후 boundaries 재조회로 지도 갱신 `DoDo-Project#62`
- 네이버 지도 스타일 레이아웃 적용 (좌측 사이드바 + 전체 폭 지도) - 울타리 ON/OFF 토글 렌더링 정상화 및 접근성(role/aria) 보강 - 울타리 원 중심에 펫 이름 라벨(Marker) 표시 - 수정 모드에서도 울타리 이름 변경 가능하도록 입력 노출 - naver 타입에 Marker/Point 추가 `DoDo-Project#62`
- 산책 페이지에서 상단 헤더 숨기고 전체화면 지도 레이아웃 적용 - WalkSideRail 추가: 좌측 세로 레일에 로고·메뉴·프로필 배치 - 지도 위 둥근 플로팅 카드(레일+울타리 패널) 구조로 변경 - header 배럴에 useIsLoggedIn 노출 `DoDo-Project#62`
- @stomp/stompjs 설치 (WebSocket STOMP 통신용) - env에 VITE_WS_URL 추가 `DoDo-Project#62`
- createStompClient: brokerURL/자동 재연결/heartbeat 설정 - beforeConnect에서 연결 직전 최신 액세스 토큰을 헤더에 주입 `DoDo-Project#62`
- LiveLocationMessage / LiveLocationPayload 타입 추가 - 서버가 내려주는 insideFence·distanceMeter 등 포함 `DoDo-Project#62`
- STOMP 연결 후 /sub/fence/location/{petId} 구독
- 수신 payload를 location 상태로 관리, 연결 상태 노출
- petId 변경/언마운트 시 연결 해제로 누수 방지
`DoDo-Project#62`
|
@sooloin is attempting to deploy a commit to the sooloin's projects Team on Vercel. A member of the Team first needs to authorize it. |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Code Review
This pull request implements a pet walking safety fence (geofence) feature, introducing Naver Maps integration, real-time location tracking via STOMP/WebSockets, and management controls for creating, toggling, and updating fences. Feedback on the changes highlights a bug where the draft center preview is not cleared after fence creation or update, a UX issue where adjusting the radius slider forces the map to snap back to the center, and an inconsistency where the update button is not disabled when the fence name is empty.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| const handleCreate = () => { | ||
| if (selectedPetId == null || !draftCenter) return; | ||
| createFence.mutate({ | ||
| petId: selectedPetId, | ||
| centerLatitude: draftCenter.lat, | ||
| centerLongitude: draftCenter.lng, | ||
| radius, | ||
| fenceName: fenceName.trim(), | ||
| }); | ||
| }; | ||
|
|
||
| const handleUpdate = () => { | ||
| if (!existingFence) return; | ||
| updateFenceRange.mutate({ | ||
| fenceId: existingFence.fenceId, | ||
| payload: { | ||
| centerLatitude: draftCenter?.lat ?? existingFence.center.latitude, | ||
| centerLongitude: draftCenter?.lng ?? existingFence.center.longitude, | ||
| radius, | ||
| fenceName: fenceName.trim() || existingFence.fenceName, | ||
| }, | ||
| }); | ||
| }; |
There was a problem hiding this comment.
울타리를 성공적으로 생성(handleCreate)하거나 수정한 후(handleUpdate), 지도상의 주황색 점선 미리보기 원(draftCenter)이 초기화되지 않아 저장된 후에도 미리보기 원이 지도에 계속 남아있는 버그가 있습니다. 뮤테이션 성공 시 setDraftCenter(null)을 호출하여 상태를 초기화해 주어야 합니다.
const handleCreate = () => {
if (selectedPetId == null || !draftCenter) return;
createFence.mutate(
{
petId: selectedPetId,
centerLatitude: draftCenter.lat,
centerLongitude: draftCenter.lng,
radius,
fenceName: fenceName.trim(),
},
{
onSuccess: () => {
setDraftCenter(null);
},
},
);
};
const handleUpdate = () => {
if (!existingFence) return;
updateFenceRange.mutate(
{
fenceId: existingFence.fenceId,
payload: {
centerLatitude: draftCenter?.lat ?? existingFence.center.latitude,
centerLongitude: draftCenter?.lng ?? existingFence.center.longitude,
radius,
fenceName: fenceName.trim() || existingFence.fenceName,
},
},
{
onSuccess: () => {
setDraftCenter(null);
},
},
);
};
| useEffect(() => { | ||
| const map = mapInstanceRef.current; | ||
| if (!isMapReady || !map) return; | ||
|
|
||
| // 중심이 없으면 미리보기 원 제거 | ||
| if (!draftCenter) { | ||
| draftCircleRef.current?.setMap(null); | ||
| draftCircleRef.current = null; | ||
| return; | ||
| } | ||
|
|
||
| const center = new naver.maps.LatLng(draftCenter.lat, draftCenter.lng); | ||
|
|
||
| if (draftCircleRef.current) { | ||
| // 이미 있으면 위치/반경만 갱신 | ||
| draftCircleRef.current.setCenter(center); | ||
| draftCircleRef.current.setRadius(draftRadius); | ||
| } else { | ||
| draftCircleRef.current = new naver.maps.Circle({ | ||
| map, | ||
| center, | ||
| radius: draftRadius, | ||
| strokeColor: '#f59e0b', | ||
| strokeWeight: 2, | ||
| strokeStyle: 'shortdash', | ||
| fillColor: '#f59e0b', | ||
| fillOpacity: 0.12, | ||
| }); | ||
| } | ||
|
|
||
| map.setCenter(center); | ||
| }, [isMapReady, draftCenter, draftRadius]); | ||
|
|
There was a problem hiding this comment.
draftRadius(반경)가 변경될 때마다 map.setCenter(center)가 호출되어, 사용자가 반경 슬라이더를 조절할 때 지도가 계속해서 중심 좌표로 강제 이동(스냅)되는 현상이 발생합니다. 지도의 중심 이동은 draftCenter가 실제로 변경될 때만 수행하도록 로직을 분리하는 것이 좋습니다.
// 3. 미리보기(점선) 원 그리기/갱신
useEffect(() => {
const map = mapInstanceRef.current;
if (!isMapReady || !map) return;
// 중심이 없으면 미리보기 원 제거
if (!draftCenter) {
draftCircleRef.current?.setMap(null);
draftCircleRef.current = null;
return;
}
const center = new naver.maps.LatLng(draftCenter.lat, draftCenter.lng);
if (draftCircleRef.current) {
// 이미 있으면 위치/반경만 갱신
draftCircleRef.current.setCenter(center);
draftCircleRef.current.setRadius(draftRadius);
} else {
draftCircleRef.current = new naver.maps.Circle({
map,
center,
radius: draftRadius,
strokeColor: "#f59e0b",
strokeWeight: 2,
strokeStyle: "shortdash",
fillColor: "#f59e0b",
fillOpacity: 0.12,
});
}
}, [isMapReady, draftCenter, draftRadius]);
// 3-1. draftCenter 변경 시에만 지도 중심 이동
useEffect(() => {
const map = mapInstanceRef.current;
if (!isMapReady || !map || !draftCenter) return;
map.setCenter(new naver.maps.LatLng(draftCenter.lat, draftCenter.lng));
}, [isMapReady, draftCenter]);
| <button | ||
| type="button" | ||
| onClick={onUpdate} | ||
| disabled={isSubmitting} | ||
| className="inline-flex min-h-11 items-center justify-center rounded-xl bg-brand px-5 text-sm font-semibold text-brand-foreground transition-opacity hover:opacity-90 disabled:opacity-50" | ||
| > | ||
| 울타리 수정 저장 | ||
| </button> |
There was a problem hiding this comment.
울타리 생성 버튼은 이름이 비어있을 때 비활성화되지만, '울타리 수정 저장' 버튼은 이름이 비어있어도 비활성화되지 않습니다. 이 경우 수정을 시도하면 기존 이름으로 자동 대체되거나 빈 값으로 요청이 갈 수 있어 사용자에게 혼란을 줄 수 있습니다. 수정 시에도 이름이 비어있다면 버튼을 비활성화하는 것이 안전합니다.
| <button | |
| type="button" | |
| onClick={onUpdate} | |
| disabled={isSubmitting} | |
| className="inline-flex min-h-11 items-center justify-center rounded-xl bg-brand px-5 text-sm font-semibold text-brand-foreground transition-opacity hover:opacity-90 disabled:opacity-50" | |
| > | |
| 울타리 수정 저장 | |
| </button> | |
| <button | |
| type="button" | |
| onClick={onUpdate} | |
| disabled={isSubmitting || fenceName.trim().length === 0} | |
| className="inline-flex min-h-11 items-center justify-center rounded-xl bg-brand px-5 text-sm font-semibold text-brand-foreground transition-opacity hover:opacity-90 disabled:opacity-50" | |
| > | |
| 울타리 수정 저장 | |
| </button> |
- 이탈 판정에 울타리 활성(isActive) 조건 추가 - 울타리가 꺼져 있으면 알림 배너·빨강 마커 미표시 `DoDo-Project#62`
- 울타리 생성/수정 성공 시 미리보기 원(draftCenter) 초기화 - 지도 중심 이동을 draftCenter 변경 시에만 수행해 반경 조절 시 스냅 방지 - 울타리 수정 저장 버튼도 이름이 비어있으면 비활성화 `DoDo-Project#62`
📄 작업 내용 (Description)
산책 페이지에 반려동물 안전 울타리(지오펜스)와 실시간 위치 추적 기능을 구현했습니다.
지도 & 레이아웃
울타리(REST)
실시간 위치(WebSocket / STOMP)
useLiveLocation훅:/sub/fence/location/{petId}구독 → 실시간 좌표 수신🔗 관련 이슈 (Related Issues)
✅ 체크리스트 (Checklist)
Style)Test)📸 스크린샷 (Screenshots)
💬 기타 사항 (Etc)
VITE_NAVER_MAP_CLIENT_ID,VITE_WS_URL(.env설정 필요)/sub/fence/location/{petId}(앞 슬래시 포함)로 구현getFenceBoundary(단일 경계 조회),useFenceStatus는 현재 화면에선 미사용이나 추후 활용 위해 유지/walk에서만 헤더를 숨기고 전체화면으로 렌더 (다른 페이지 영향 없음)