Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions src/app/router/Router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
AroundSearch,
TourList,
TourSearch,
TourSingleInfo,
} from '@/app/router';
import { Home } from '@/pages/home';
import { GeoTrip } from '@/pages/tour/geotrip';
Expand All @@ -28,6 +29,7 @@ export default function Router() {
</Route>
<Route path="/tour" element={<Tour />}>
<Route path="geo-trip" element={<GeoTrip />} />
<Route path="single/:contentId" element={<TourSingleInfo />} />
<Route path="list" element={<TourList />} />
<Route path="search" element={<TourSearch />} />
<Route path="bookmark" element={<Bookmark />} />
Expand Down
3 changes: 3 additions & 0 deletions src/app/router/lazyRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,6 @@ export const AroundSearch = lazy(
);
export const TourList = lazy(() => import('@/pages/tour/tourList/TourList'));
export const TourSearch = lazy(() => import('@/pages/tour/search/TourSearch'));
export const TourSingleInfo = lazy(
() => import('@/pages/tour/geotrip/SingleTrip'),
);
70 changes: 70 additions & 0 deletions src/features/shared/lib/useKakaoShare.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { tourQueries } from '@/entities/tour';
import { useQuery } from '@tanstack/react-query';
import { useEffect } from 'react';

declare global {
interface Window {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Kakao: any;
}
}

interface UseKakaoShareParams {
contentId: string;
link: string;
}

const useKakaoShare = ({ contentId, link }: UseKakaoShareParams) => {
const { data } = useQuery({
...tourQueries.detailCommon(contentId),
});

useEffect(() => {
// SDK 로드
const script = document.createElement('script');
script.src = 'https://t1.kakaocdn.net/kakao_js_sdk/2.7.0/kakao.min.js';
script.async = true;
document.body.appendChild(script);

return () => {
document.body.removeChild(script);
};
}, []);

const shareKakao = () => {
if (window.Kakao) {
const kakao = window.Kakao;
if (!kakao.isInitialized()) {
kakao.init(import.meta.env.VITE_KAKAO_MAP_KEY);
}
window.Kakao.Share.sendDefault({
objectType: 'feed',
content: {
title: data?.title ?? '여행지 추천',
description: data?.overview ?? '멋진 여행지를 확인해보세요!',
imageUrl:
data?.firstimage ||
data?.firstimage2 ||
'https://developers.kakao.com/assets/img/about/logos/kakaolink/kakaolink_btn_medium.png',
link: {
mobileWebUrl: link,
webUrl: link,
},
},
buttons: [
{
title: '여행 시작하기',
link: {
mobileWebUrl: link,
webUrl: link,
},
},
],
});
}
};

return { shareKakao };
};

export default useKakaoShare;
25 changes: 25 additions & 0 deletions src/features/shared/ui/SharedButtonContainer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/* eslint-disable */
import { commonSVG } from '@/assets';
import useKakaoShare from '../lib/useKakaoShare';

interface SharedButtonContainerProps {
contentId: string;
}

export default function SharedButtonContainer({
contentId,
}: SharedButtonContainerProps) {
const link = `${window.location.origin}/tour/single/${contentId}`;

const { shareKakao } = useKakaoShare({ contentId, link });
return (
<button
aria-label="공유하기"
className="cursor-pointer"
type="button"
onClick={shareKakao}
>
<commonSVG.ShareIcon />
</button>
);
}
38 changes: 38 additions & 0 deletions src/features/tour/ui/single/Single.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { useNavigate, useParams } from 'react-router-dom';
import { useQuery } from '@tanstack/react-query';

import { tourQueries, type TourItem } from '@/entities/tour';
import { LoadingSpinner } from '@/shared/ui';
import { TourSlide } from '@/features/tourShort';

export default function Single() {
const { contentId } = useParams();
const navigate = useNavigate();

const { data: tourInfo, isLoading } = useQuery({
...tourQueries.detailCommon(contentId!),
enabled: !!contentId,
});

// 로딩 중일 때
if (isLoading) {
return (
<div className="w-full h-full flex justify-center items-center">
<LoadingSpinner />
</div>
);
}

const handleRedirectMainPage = () => {
navigate('/tour/geo-trip?distance=20000&tour-type=12');
};

return (
<div className="relative w-full h-full">
<TourSlide
tourInfo={tourInfo as TourItem}
openBottomSheet={handleRedirectMainPage}
/>
</div>
);
}
26 changes: 13 additions & 13 deletions src/features/tourList/ui/TourInfoCard.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { Suspense } from 'react';
import { commonSVG } from '@/assets';

import { TourCardImages } from '@/features/tourList';
import { SkeletonCard, StartTripButton } from '@/features/tour';
Expand All @@ -13,6 +12,7 @@ import {
} from '@/shared';

import type { TourItem } from '@/entities/tour';
import SharedButtonContainer from '@/features/shared/ui/SharedButtonContainer';
interface TourInfoCardProps {
tourInfo: TourItem;
}
Expand Down Expand Up @@ -40,9 +40,7 @@ export default function TourInfoCard({ tourInfo }: TourInfoCardProps) {
</span>
</div>
<nav className="flex gap-2.5" aria-label="카드 옵션">
<button aria-label="옵션 보기">
<commonSVG.ShareIcon />
</button>
<SharedButtonContainer contentId={tourInfo.contentid} />
<Suspense fallback={<LoadingSpinner />}>
<BookmarkButtonContainer contentId={tourInfo.contentid} />
</Suspense>
Expand All @@ -51,15 +49,17 @@ export default function TourInfoCard({ tourInfo }: TourInfoCardProps) {

{/* 상세 정보 */}
<section className="flex flex-col px-5 mt-2 text-sm">
<div className="flex items-center gap-2">
<DistanceTimeInfo
dist={tourInfo.dist}
iconFill="#FA4032"
className="text-primary-red font-bold"
/>
<address className="not-italic">
{truncate(tourInfo.addr1 ?? '', { omission: '', length: 10 })}
</address>
<div className="flex items-center justify-between gap-2">
<div className="flex gap-2">
<DistanceTimeInfo
dist={tourInfo.dist}
iconFill="#FA4032"
className="text-primary-red font-bold"
/>
<address className="not-italic">
{truncate(tourInfo.addr1 ?? '', { omission: '...', length: 15 })}
</address>
</div>
<StartTripButton
lng={tourInfo.mapx}
lat={tourInfo.mapy}
Expand Down
30 changes: 30 additions & 0 deletions src/pages/tour/geotrip/SingleTrip.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import SingleTrip from '@/features/tour/ui/single/Single';
import { LoadingSpinner, QueryErrorBoundary } from '@/shared';
import { BottomNavigationBar, Seo } from '@/widgets';
import { Suspense } from 'react';
import { useParams } from 'react-router-dom';

export default function SingleTripPage() {
const { contentId } = useParams();
return (
<>
<Seo
title="관광지 조회"
description="관광지를 단일 조회하는 페이지입니다."
canonicalUrl={`https://p-pick.com/tour/${contentId}`}
/>
<section className="flex flex-col h-full w-full">
<div className="h-full w-full relative">
<div className="absolute h-full w-full flex items-center justify-center">
<QueryErrorBoundary>
<Suspense fallback={<LoadingSpinner />}>
<SingleTrip />
</Suspense>
</QueryErrorBoundary>
</div>
</div>
<BottomNavigationBar />
</section>
</>
);
}
1 change: 1 addition & 0 deletions src/pages/tour/geotrip/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { default as GeoTrip } from './GeoTrip';
export { default as SingleTrip } from './SingleTrip';