Skip to content

Commit 74ff439

Browse files
authored
Merge pull request #45 from frontend-opensource-project/URH-48/use-geolocation
[URH-48] useGeolocation 신규
2 parents 86c356e + 56d0d62 commit 74ff439

File tree

2 files changed

+183
-0
lines changed

2 files changed

+183
-0
lines changed

src/hooks/useGeolocation.test.ts

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { renderHook, waitFor } from '@testing-library/react';
2+
import useGeolocation from './useGeolocation';
3+
4+
const mockOptions = {
5+
enableHighAccuracy: false,
6+
timeout: 5000,
7+
maximumAge: 0,
8+
};
9+
10+
const mockError: GeolocationPositionError = {
11+
code: 1,
12+
message: '사용자가 위치 정보를 거부했습니다.',
13+
PERMISSION_DENIED: 1,
14+
POSITION_UNAVAILABLE: 2,
15+
TIMEOUT: 3,
16+
};
17+
18+
beforeEach(() => {
19+
const mockGeolocation = {
20+
watchPosition: jest.fn().mockImplementation((success) => {
21+
success({
22+
coords: {
23+
latitude: 35,
24+
longitude: 139,
25+
altitude: 0,
26+
accuracy: 100,
27+
altitudeAccuracy: null,
28+
heading: null,
29+
speed: null,
30+
},
31+
timestamp: Date.now(),
32+
});
33+
return 1;
34+
}),
35+
clearWatch: jest.fn(),
36+
};
37+
38+
Object.defineProperty(global.navigator, 'geolocation', {
39+
value: mockGeolocation,
40+
writable: true,
41+
});
42+
});
43+
44+
afterEach(() => {
45+
jest.clearAllMocks();
46+
});
47+
48+
describe('useGeolocation hook', () => {
49+
it('성공 후 위치 정보를 반환해야 함', async () => {
50+
const { result } = renderHook(() => useGeolocation(mockOptions));
51+
52+
waitFor(() => {
53+
expect(result.current.loading).toBe(false);
54+
expect(result.current.error).toBe(null);
55+
expect(result.current.latitude).toBe(35);
56+
expect(result.current.longitude).toBe(139);
57+
});
58+
});
59+
60+
it('오류를 처리해야 한다', async () => {
61+
global.navigator.geolocation.watchPosition = jest.fn((_, errorCallback) => {
62+
if (errorCallback) {
63+
errorCallback(mockError);
64+
}
65+
return 1;
66+
});
67+
68+
const { result } = renderHook(() =>
69+
useGeolocation({
70+
enableHighAccuracy: false,
71+
timeout: 5000,
72+
maximumAge: 0,
73+
})
74+
);
75+
76+
await waitFor(() => {
77+
expect(result.current.loading).toBe(false);
78+
expect(result.current.error).not.toBe(null);
79+
expect(result.current.latitude).toBeUndefined();
80+
expect(result.current.longitude).toBeUndefined();
81+
});
82+
});
83+
});

src/hooks/useGeolocation.ts

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import { useEffect, useState, useRef } from 'react';
2+
3+
interface UseGeolocationReturnType extends Partial<GeolocationCoordinates> {
4+
loading: boolean;
5+
error: GeolocationPositionError | null;
6+
timestamp: EpochTimeStamp | undefined;
7+
}
8+
9+
/**
10+
* 사용자의 위치 정보를 가져오는 커스텀 훅
11+
*
12+
* @param {PositionOptions} options - 위치 정보를 가져오는 옵션
13+
* @param {boolean} options.enableHighAccuracy - 위치 정보를 높은 정확도로 수집할지 여부를 지정 (기본값은 false)
14+
* @param {number} options.timeout - 위치 정보를 가져오기 위해 대기할 최대 시간 (밀리초 단위)
15+
* @param {number} options.maximumAge - 위치 정보를 캐싱할 최대 시간 (밀리초 단위)
16+
17+
* @returns {UseGeolocationReturnType} 위치 정보와 상태를 포함하는 객체를 반환
18+
* @returns {boolean} UseGeolocationReturnType.loading - 위치 정보를 가져오는 중인지 여부
19+
* @returns {GeolocationPositionError | null} UseGeolocationReturnType.error - 위치 정보를 가져오는 중에 발생한 에러
20+
* @returns {EpochTimeStamp | undefined} UseGeolocationReturnType.timestamp - 위치 정보의 타임스탬프
21+
* @returns {number | undefined} UseGeolocationReturnType.latitude - 위도 정보
22+
* @returns {number | undefined} UseGeolocationReturnType.longitude - 경도 정보
23+
* @returns {number | undefined} UseGeolocationReturnType.altitude - 고도 정보
24+
* @returns {number | undefined} UseGeolocationReturnType.accuracy - 위치 정보의 정확도
25+
* @returns {number | undefined} UseGeolocationReturnType.altitudeAccuracy - 고도 정보의 정확도
26+
* @returns {number | undefined} UseGeolocationReturnType.heading - 방향 정보
27+
* @returns {number | undefined} UseGeolocationReturnType.speed - 속도 정보
28+
*/
29+
const useGeolocation = (
30+
options: PositionOptions = {}
31+
): UseGeolocationReturnType => {
32+
const isMounted = useRef(true);
33+
34+
const [loading, setLoading] = useState(true);
35+
const [error, setError] = useState<GeolocationPositionError | null>(null);
36+
const [position, setPosition] = useState<GeolocationPosition | null>(null);
37+
38+
const { enableHighAccuracy, timeout, maximumAge } = options;
39+
40+
useEffect(() => {
41+
const handleSuccess = (position: GeolocationPosition) => {
42+
if (isMounted.current) {
43+
setPosition(position);
44+
setLoading(false);
45+
}
46+
};
47+
48+
const handleError = (err: GeolocationPositionError) => {
49+
if (isMounted.current) {
50+
setError(err);
51+
setLoading(false);
52+
}
53+
};
54+
55+
const handleReset = () => {
56+
setLoading(true);
57+
setError(null);
58+
setPosition(null);
59+
};
60+
61+
const watchId = navigator.geolocation.watchPosition(
62+
handleSuccess,
63+
handleError,
64+
options
65+
);
66+
67+
return () => {
68+
handleReset();
69+
isMounted.current = false;
70+
navigator.geolocation.clearWatch(watchId);
71+
};
72+
}, [enableHighAccuracy, timeout, maximumAge]);
73+
74+
const {
75+
latitude,
76+
longitude,
77+
altitude,
78+
accuracy,
79+
altitudeAccuracy,
80+
heading,
81+
speed,
82+
} = position?.coords || {};
83+
84+
const timestamp = position?.timestamp ?? undefined;
85+
86+
return {
87+
latitude,
88+
longitude,
89+
altitude,
90+
accuracy,
91+
altitudeAccuracy,
92+
heading,
93+
speed,
94+
timestamp,
95+
error,
96+
loading,
97+
};
98+
};
99+
100+
export default useGeolocation;

0 commit comments

Comments
 (0)