Skip to content
This repository was archived by the owner on Jul 3, 2026. It is now read-only.

Commit a4920f0

Browse files
committed
[CP-224] feat: 예약 사진 인증 시 GPS 기반 1차 검증
1 parent 73775ed commit a4920f0

3 files changed

Lines changed: 95 additions & 12 deletions

File tree

‎src/modal/reserve-rejected-detail.modal.tsx‎

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { useEffect, useState } from 'react';
2+
13
import Button from '@/components/button';
24
import { FormField } from '@/components/form/form-field';
35
import { Input } from '@/components/form/input';
@@ -6,10 +8,12 @@ import GridIconModal from '@/components/modal-container/grid-icon-modal';
68
import { ReservationResponse } from '@/api/dto/reservation';
79
import { useModalStore } from '@/store/modal.store';
810
import { useUserStore } from '@/store/user.store';
11+
import { checkLocation } from '@/util/gps-validator';
912
import { format, parse } from 'date-fns';
1013
import { ko } from 'date-fns/locale';
1114
import { TickCircle, Warning2 } from 'iconsax-react';
1215
import { Folder, Forward } from 'lucide-react';
16+
import { twMerge } from 'tailwind-merge';
1317

1418
interface ReserveRejectedDetailModalProps {
1519
reservation: ReservationResponse;
@@ -21,6 +25,15 @@ export default function ReserveRejectedDetailModal({
2125
const { user } = useUserStore();
2226
const { closeModal } = useModalStore();
2327

28+
const [validatedLocation, setValidatedLocation] = useState(false);
29+
30+
useEffect(() => {
31+
(async () => {
32+
const validated = await checkLocation();
33+
setValidatedLocation(validated);
34+
})();
35+
}, []);
36+
2437
if (!user) return null;
2538

2639
return (
@@ -55,12 +68,25 @@ export default function ReserveRejectedDetailModal({
5568
</h3>
5669
</div>
5770

58-
<div className="border-system-alarm flex cursor-pointer flex-col items-center gap-1 rounded border border-dashed py-5 hover:bg-gray-50">
59-
<div className="flex items-center gap-1.5">
60-
<Folder size={22} color="black" />
61-
<p className="subtitle2-nanum text-primary-gray-600">파일 선택</p>
62-
</div>
63-
<p className="body2-nanum text-neutral-500">파일을 여기로 끌어 놓으세요</p>
71+
<div
72+
className={twMerge(
73+
'border-system-alarm flex flex-col items-center gap-1 rounded border border-dashed py-5',
74+
validatedLocation && 'cursor-pointer hover:bg-gray-50',
75+
)}
76+
>
77+
{validatedLocation ? (
78+
<>
79+
<div className="flex items-center gap-1.5">
80+
<Folder size={22} color="black" />
81+
<p className="subtitle2-nanum text-primary-gray-600">파일 선택</p>
82+
</div>
83+
<p className="body2-nanum text-neutral-500">파일을 여기로 끌어 놓으세요</p>
84+
</>
85+
) : (
86+
<p className="body2-pretendard text-system-alarm">
87+
국민대학교 내부에서만 인증할 수 있습니다.
88+
</p>
89+
)}
6490
</div>
6591
</div>
6692

‎src/modal/reserve-success-detail.modal.tsx‎

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { useEffect, useState } from 'react';
2+
13
import Button from '@/components/button';
24
import { FormField } from '@/components/form/form-field';
35
import { Input } from '@/components/form/input';
@@ -6,10 +8,12 @@ import GridIconModal from '@/components/modal-container/grid-icon-modal';
68
import { ReservationResponse } from '@/api/dto/reservation';
79
import { useModalStore } from '@/store/modal.store';
810
import { useUserStore } from '@/store/user.store';
11+
import { checkLocation } from '@/util/gps-validator';
912
import { format, parse } from 'date-fns';
1013
import { ko } from 'date-fns/locale';
1114
import { TickCircle, Warning2 } from 'iconsax-react';
1215
import { Folder, Forward } from 'lucide-react';
16+
import { twMerge } from 'tailwind-merge';
1317

1418
interface ReserveRejectedDetailModalProps {
1519
reservation: ReservationResponse;
@@ -21,6 +25,15 @@ export default function ReserveSuccessDetailModal({
2125
const { user } = useUserStore();
2226
const { closeModal } = useModalStore();
2327

28+
const [validatedLocation, setValidatedLocation] = useState(false);
29+
30+
useEffect(() => {
31+
(async () => {
32+
const validated = await checkLocation();
33+
setValidatedLocation(validated);
34+
})();
35+
}, []);
36+
2437
if (!user) return null;
2538

2639
return (
@@ -55,12 +68,25 @@ export default function ReserveSuccessDetailModal({
5568
</h3>
5669
</div>
5770

58-
<div className="border-system-alarm3 flex cursor-pointer flex-col items-center gap-1 rounded border border-dashed py-5 hover:bg-gray-50">
59-
<div className="flex items-center gap-1.5">
60-
<Folder size={22} color="black" />
61-
<p className="subtitle2-nanum text-primary-gray-600">파일 선택</p>
62-
</div>
63-
<p className="body2-nanum text-neutral-500">파일을 여기로 끌어 놓으세요</p>
71+
<div
72+
className={twMerge(
73+
'border-system-alarm3 flex flex-col items-center gap-1 rounded border border-dashed py-5',
74+
validatedLocation && 'cursor-pointer hover:bg-gray-50',
75+
)}
76+
>
77+
{validatedLocation ? (
78+
<>
79+
<div className="flex items-center gap-1.5">
80+
<Folder size={22} color="black" />
81+
<p className="subtitle2-nanum text-primary-gray-600">파일 선택</p>
82+
</div>
83+
<p className="body2-nanum text-neutral-500">파일을 여기로 끌어 놓으세요</p>
84+
</>
85+
) : (
86+
<p className="body2-pretendard text-system-alarm3">
87+
국민대학교 내부에서만 인증할 수 있습니다.
88+
</p>
89+
)}
6490
</div>
6591
</div>
6692

‎src/util/gps-validator.ts‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
const lat = 37.611142;
2+
const lng = 126.996427;
3+
4+
const toRad = (value: number) => (value * Math.PI) / 180;
5+
6+
const getDistance = (lat1: number, lon1: number, lat2: number, lon2: number) => {
7+
const R = 6371000;
8+
const dLat = toRad(lat2 - lat1);
9+
const dLon = toRad(lon2 - lon1);
10+
const a =
11+
Math.sin(dLat / 2) ** 2 +
12+
Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLon / 2) ** 2;
13+
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
14+
return R * c;
15+
};
16+
17+
export const checkLocation = async (): Promise<boolean> => {
18+
if (!navigator.geolocation) return true;
19+
20+
return new Promise((resolve) => {
21+
navigator.geolocation.getCurrentPosition(
22+
(position) => {
23+
const { latitude, longitude } = position.coords;
24+
const distance = getDistance(latitude, longitude, lat, lng);
25+
26+
resolve(distance <= 300);
27+
},
28+
() => resolve(false),
29+
);
30+
});
31+
};

0 commit comments

Comments
 (0)