Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: 현재 위치와 체험장소 간 거리 안내, 주소 복사 버튼 #55

Merged
merged 18 commits into from
Jul 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
a6a6045
Design: 체험상세페이지 태블릿 디자인 수정
MEGUMMY1 Jul 20, 2024
cd4c463
Design: popup 버튼 순서 변경
MEGUMMY1 Jul 20, 2024
858274f
Merge branch 'master' of https://github.com/eunji-0623/GlobalNomad in…
MEGUMMY1 Jul 20, 2024
fb37629
Merge branch 'master' of https://github.com/eunji-0623/GlobalNomad in…
MEGUMMY1 Jul 20, 2024
bd8e1ee
Fix: appKey env 추가
MEGUMMY1 Jul 20, 2024
ab1ed78
Fix: appKey env 추가
MEGUMMY1 Jul 20, 2024
daec45e
Fix: 캘린더 예약 가능 일자 표시
MEGUMMY1 Jul 20, 2024
844dc0a
Feat: 현재 위치와 체험장소 간 거리 안내, 주소 복사 버튼
MEGUMMY1 Jul 20, 2024
02cb74a
Feat: 현재 위치와 체험장소 간 거리 안내, 주소 복사 버튼
MEGUMMY1 Jul 20, 2024
f97db08
Fix: 공유 모달 링크 복사 성공 시 alert -> toast 변경
MEGUMMY1 Jul 20, 2024
2ff339b
Fix: Custom Calendar 체험 등록 오류 해결
MEGUMMY1 Jul 20, 2024
11a8859
Fix: 체험 상세 페이지 내 체험 삭제 로직 수정
MEGUMMY1 Jul 20, 2024
4fe4a82
Design: 다크모드용 공유 버튼 추가
MEGUMMY1 Jul 20, 2024
170cb01
Merge branch 'master' of https://github.com/eunji-0623/GlobalNomad in…
MEGUMMY1 Jul 20, 2024
35316ab
Refactor: 스타일 리팩토링
MEGUMMY1 Jul 20, 2024
ffa9365
Merge branch 'master' of https://github.com/eunji-0623/GlobalNomad in…
MEGUMMY1 Jul 22, 2024
9e54f1c
Refactor: 스타일 리팩토링
MEGUMMY1 Jul 22, 2024
2bc81b0
Design: 다크 모드 location 아이콘 변경
MEGUMMY1 Jul 22, 2024
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 .env
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
NEXT_PUBLIC_KAKAO_API_KEY=021dc3000bd4e8368bea279079c36944
NEXT_PUBLIC_KAKAO_API_KEY=021dc3000bd4e8368bea279079c36944
NEXT_PUBLIC_KAKAO_MAP_APP_KEY=6fdf3f59b292392fa72007a224286221
38 changes: 30 additions & 8 deletions components/ ShareButton/ShareButton.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { useModal } from '@/hooks/useModal';
import { usePopup } from '@/hooks/usePopup';
import { darkModeState } from '@/states/themeState';
import Image from 'next/image';
import { toast, ToastContainer } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
import { useRecoilValue } from 'recoil';

export function ShareButton({
type,
Expand All @@ -16,18 +20,15 @@ export function ShareButton({
const text = `${title}\n\n${description}`;
const encodedUrl = encodeURIComponent(url);
const encodedText = encodeURIComponent(text);
const isDarkMode = useRecoilValue(darkModeState);

const copyToClipboard = async (text: string) => {
try {
await navigator.clipboard.writeText(text);
alert('클립보드에 복사되었습니다.');
toast.success('클립보드에 복사되었습니다.');
} catch (error) {
console.error(error);
openPopup({
popupType: 'alert',
content: '클립 보드 복사에 실패하였습니다.',
btnName: ['확인'],
});
toast.error('클립 보드 복사에 실패하였습니다.');
}
};

Expand Down Expand Up @@ -78,10 +79,23 @@ export function ShareButton({
objectFit="cover"
className="object-cover"
/>
<ToastContainer
position="top-center"
autoClose={3000}
hideProgressBar={false}
closeOnClick
rtl={false}
pauseOnFocusLoss
draggable
pauseOnHover
theme={isDarkMode ? 'dark' : 'light'}
/>
</div>
<div className="flex flex-col gap-[10px] overflow-hidden w-full">
<p className="font-bold">{title}</p>
<p className="text-gray-700 line-clamp-3">{description}</p>
<p className="text-gray-700 line-clamp-3 dark:text-var-gray2">
{description}
</p>
</div>
<div className="flex w-full justify-between px-[10px]">
<SNSShareButton
Expand Down Expand Up @@ -123,7 +137,15 @@ export function ShareButton({
: 'h-[30px] w-[30px] relative'
}
>
<Image src="/icon/share.png" alt="공유 버튼" layout="fill" />
<Image
src={
type !== 'initial' && isDarkMode
? '/icon/share_gray.png'
: '/icon/share.png'
}
alt="공유 버튼"
layout="fill"
/>
</div>
</button>
);
Expand Down
25 changes: 10 additions & 15 deletions components/ActivityDetails/ActivityDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,12 @@ import { ShareButton } from '../ ShareButton/ShareButton';
import { ActivityDetailsPageMeta } from '../MetaData/MetaData';
import useDeleteActivity from '@/hooks/myActivity/useDeleteActivity';
import { usePopup } from '@/hooks/usePopup';
import { darkModeState } from '@/states/themeState';

export default function ActivityDetails({ id }: ActivityDetailsProps) {
const router = useRouter();
const [isOpen, setIsOpen] = useState(false);
const isDarkMode = useRecoilValue(darkModeState);
const [currentPage, setCurrentPage] = useState<number>(
router.query.page ? parseInt(router.query.page as string, 10) : 1
);
Expand Down Expand Up @@ -99,8 +101,8 @@ export default function ActivityDetails({ id }: ActivityDetailsProps) {
content: '체험을 삭제하시겠어요?',
btnName: ['아니오', '삭제하기'],
callBackFnc: () => {
deleteMyActivityMutation.mutate(id);
router.push(`/myactivity`);
deleteMyActivityMutation.mutate(id);
},
});
};
Expand Down Expand Up @@ -147,7 +149,11 @@ export default function ActivityDetails({ id }: ActivityDetailsProps) {
</div>
<div className="flex gap-1 items-center justify-center m:items-start">
<Image
src="/icon/location.svg"
src={
isDarkMode
? '/icon/location_gray.svg'
: '/icon/location.svg'
}
alt="위치 아이콘"
width={18}
height={18}
Expand Down Expand Up @@ -199,32 +205,21 @@ export default function ActivityDetails({ id }: ActivityDetailsProps) {
/>
)}
<div className="flex gap-4 m:block m:relative">
<div className="max-w-[800px] mb-20 t:w-[470px] m:w-fit m:px-[24px]">
<div className="max-w-[800px] mb-20 ipad-pro:w-[725px] t:w-full m:w-fit m:px-[24px]">
<div className="border-t-2 border-var-gray3 dark:border-var-dark4 border-solid pt-10 m:pt-6" />
<div className="flex flex-col gap-4">
<p className="text-nomad-black dark:text-var-gray2 font-bold text-xl">
체험 설명
</p>
<textarea
className="py-[16px] px-[20px] h-[200px] resize-none dark:bg-var-dark1 dark:text-var-gray2 "
className="py-[16px] px-[20px] h-[200px] resize-none custom-scrollbar dark:bg-var-dark1 dark:text-var-gray2 "
disabled
>
{activityData?.description}
</textarea>
</div>
<div className="border-t-2 border-var-gray3 dark:border-var-dark4 border-solid my-10 m:my-6" />
{activityData && <Map address={activityData.address} />}
<div className="flex gap-1 mt-2">
<Image
src="/icon/location.svg"
alt="위치 아이콘"
width={18}
height={18}
/>
<p className="text-nomad-black dark:text-var-gray2 text-sm max-w-[700px] tracking-tight">
{activityData?.address}
</p>
</div>
<div className="border-t-2 border-var-gray3 dark:border-var-dark4 border-solid my-10 m:my-6" />
<div className="flex flex-col gap-4">
<p className="text-nomad-black dark:text-var-gray2 font-bold text-xl">
Expand Down
156 changes: 145 additions & 11 deletions components/ActivityDetails/Map/Map.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,24 @@
import React, { useEffect } from 'react';
import React, { useEffect, useState } from 'react';
import Image from 'next/image';
import { MapProps } from './Map.types';
import { appKey } from '@/static/appKey';
import { toast, ToastContainer } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
import { useRecoilValue } from 'recoil';
import { darkModeState } from '@/states/themeState';

export default function Map({ address }: MapProps) {
const [distance, setDistance] = useState<string | null>(null);
const isDarkMode = useRecoilValue(darkModeState);

useEffect(() => {
const script = document.createElement('script');
script.src = `//dapi.kakao.com/v2/maps/sdk.js?appkey=${appKey}&autoload=false&libraries=services`;
script.src = `//dapi.kakao.com/v2/maps/sdk.js?appkey=${process.env.NEXT_PUBLIC_KAKAO_MAP_APP_KEY}&autoload=false&libraries=services`;
script.async = true;
document.head.appendChild(script);

script.onload = () => {
const kakao = (window as any).kakao;

kakao.maps.load(() => {
const container = document.getElementById('map');
const options = {
Expand All @@ -21,15 +29,62 @@ export default function Map({ address }: MapProps) {

const geocoder = new kakao.maps.services.Geocoder();

// 체험장소 주소를 좌표로 변환
geocoder.addressSearch(address, (result: any, status: any) => {
if (status === kakao.maps.services.Status.OK) {
const coords = new kakao.maps.LatLng(result[0].y, result[0].x);
const marker = new kakao.maps.Marker({
const destinationLat = parseFloat(result[0].y);
const destinationLon = parseFloat(result[0].x);
const destinationCoords = new kakao.maps.LatLng(
destinationLat,
destinationLon
);

// 체험장소 마커 추가
new kakao.maps.Marker({
map: map,
position: coords,
position: destinationCoords,
title: '체험장소',
});

map.setCenter(coords);
// 지도 중심을 체험장소로 이동
map.setCenter(destinationCoords);

// 현재 위치 가져오기 및 거리 계산
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(
(position) => {
const currentLat = position.coords.latitude;
const currentLon = position.coords.longitude;
const currentCoords = new kakao.maps.LatLng(
currentLat,
currentLon
);

// 현재 위치 마커 추가
new kakao.maps.Marker({
map: map,
position: currentCoords,
title: '현재 위치',
});

const distanceInMeters = calculateDistanceInMeters(
currentLat,
currentLon,
destinationLat,
destinationLon
);

setDistance(`${(distanceInMeters / 1000).toFixed(2)} km`);
},
(error) => {
console.error('현재 위치 가져오기 실패:', error);
}
);
} else {
console.error('Geolocation is not supported by this browser.');
}
} else {
console.error('주소 검색 실패:', status);
}
});
});
Expand All @@ -40,10 +95,89 @@ export default function Map({ address }: MapProps) {
};
}, [address]);

// 거리 계산 함수 (단위: 미터)
const calculateDistanceInMeters = (
lat1: number,
lon1: number,
lat2: number,
lon2: number
): number => {
const R = 6371e3;
const φ1 = (lat1 * Math.PI) / 180;
const φ2 = (lat2 * Math.PI) / 180;
const Δφ = ((lat2 - lat1) * Math.PI) / 180;
const Δλ = ((lon2 - lon1) * Math.PI) / 180;

const a =
Math.sin(Δφ / 2) * Math.sin(Δφ / 2) +
Math.cos(φ1) * Math.cos(φ2) * Math.sin(Δλ / 2) * Math.sin(Δλ / 2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));

return R * c;
};

const copyToClipboard = (text: string) => {
navigator.clipboard.writeText(text).then(
() => {
toast.success('주소가 복사되었습니다.');
},
(err) => {
toast.error('클립보드 복사 실패');
}
);
};

return (
<div
id="map"
className="w-[800px] h-[500px] rounded-2xl t:w-full t:h-[276px] m:w-full m:h-[450px]"
/>
<div>
<ToastContainer
position="top-center"
autoClose={3000}
hideProgressBar={false}
closeOnClick
rtl={false}
pauseOnFocusLoss
draggable
pauseOnHover
theme={isDarkMode ? 'dark' : 'light'}
/>
<div className="flex gap-2 mb-2 items-center">
<Image
src="/icon/distance.svg"
alt="위치 아이콘"
width={20}
height={20}
/>
{distance ? (
<p className="text-nomad-black dark:text-var-gray2 tracking-tight">
현재 위치에서 체험 장소까지의 거리는 약 {distance} 입니다.
</p>
) : (
<p className="text-nomad-black dark:text-var-gray2 tracking-tight">
거리를 계산 중입니다...
</p>
)}
</div>
<div
id="map"
className="w-[800px] h-[500px] rounded-2xl ipad-pro:h-[400px] t:w-full t:h-[276px] m:w-full m:h-[450px]"
/>
<div className="flex gap-1 mt-2 items-center">
<Image
src={isDarkMode ? '/icon/location_gray.svg' : '/icon/location.svg'}
alt="위치 아이콘"
width={18}
height={18}
/>
<p className="text-nomad-black dark:text-var-gray2 text-sm max-w-[700px] tracking-tight">
{address}
</p>
<button
onClick={() => copyToClipboard(address)}
className="text-var-blue3 underline ml-1 text-sm"
>
복사
</button>
</div>
</div>
);
}
Loading