Skip to content

Commit d9ca378

Browse files
committed
feat: MemeDetailPage에 공유 시트 기능 추가
- MemeDetailPage에 밈 공유를 위한 새로운 공유 시트를 추가하여 사용자 경험을 향상시킴 - Kakao SDK를 포함하여 공유 기능을 통합함
1 parent 5954f91 commit d9ca378

File tree

4 files changed

+309
-8
lines changed

4 files changed

+309
-8
lines changed

apps/web/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
/>
3434
<link rel="canonical" href="https://meme-wiki.net/" />
3535
<title>Meme Wiki - 밈 문화의 모든 것</title>
36+
<script src="https://developers.kakao.com/sdk/js/kakao.js"></script>
3637
</head>
3738
<body>
3839
<div id="root"></div>
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import styled from '@emotion/styled';
2+
import { motion } from 'motion/react';
3+
4+
const Container = styled(motion.div)`
5+
position: fixed;
6+
bottom: 0;
7+
left: 0;
8+
right: 0;
9+
background-color: ${({ theme }) => theme.palette.common.white};
10+
border-top-left-radius: 16px;
11+
border-top-right-radius: 16px;
12+
z-index: 1000;
13+
max-width: ${({ theme }) => theme.breakpoints.mobile};
14+
margin: 0 auto;
15+
`;
16+
17+
const Dimmed = styled(motion.div)`
18+
position: fixed;
19+
top: 0;
20+
left: 0;
21+
right: 0;
22+
bottom: 0;
23+
background-color: rgba(0, 0, 0, 0.5);
24+
z-index: 999;
25+
`;
26+
27+
const Content = styled.div`
28+
padding: 30px 0;
29+
`;
30+
31+
const Title = styled.h2`
32+
${({ theme }) => theme.typography.title.headline2};
33+
text-align: center;
34+
margin-bottom: 24px;
35+
`;
36+
37+
const ThumbnailContainer = styled.div`
38+
display: flex;
39+
justify-content: center;
40+
margin-bottom: 30px;
41+
`;
42+
43+
const Thumbnail = styled.img`
44+
width: 84px;
45+
height: 84px;
46+
border-radius: 8px;
47+
object-fit: cover;
48+
`;
49+
50+
const IconContainer = styled.div`
51+
display: flex;
52+
justify-content: center;
53+
gap: 40px;
54+
padding: 24px 0;
55+
border-top: 1px solid ${({ theme }) => theme.palette.gray['gray-2']};
56+
`;
57+
58+
const IconButton = styled.button`
59+
display: flex;
60+
flex-direction: column;
61+
align-items: center;
62+
gap: 10px;
63+
background: none;
64+
border: none;
65+
cursor: pointer;
66+
`;
67+
68+
interface IconCircleProps {
69+
isKakao?: boolean;
70+
}
71+
72+
const IconCircle = styled.div<IconCircleProps>`
73+
width: 48px;
74+
height: 48px;
75+
border-radius: 50%;
76+
display: flex;
77+
align-items: center;
78+
justify-content: center;
79+
background-color: ${({ theme, isKakao }) =>
80+
isKakao ? '#FFE812' : theme.palette.gray['gray-2']};
81+
border: ${({ theme, isKakao }) =>
82+
isKakao ? 'none' : `1px solid ${theme.palette.gray['gray-2']}`};
83+
`;
84+
85+
const IconText = styled.span`
86+
${({ theme }) => theme.typography.body.body1};
87+
color: ${({ theme }) => theme.palette.common.black};
88+
`;
89+
90+
const Toast = styled(motion.div)`
91+
position: absolute;
92+
left: 50%;
93+
top: 50%;
94+
transform: translate(-50%, -50%);
95+
background-color: rgba(0, 0, 0, 0.8);
96+
color: ${({ theme }) => theme.palette.common.white};
97+
padding: 10px 20px;
98+
border-radius: 6px;
99+
display: flex;
100+
align-items: center;
101+
justify-content: center;
102+
gap: 8px;
103+
z-index: 1002;
104+
white-space: nowrap;
105+
${({ theme }) => theme.typography.body.body1};
106+
width: 182px;
107+
height: 40px;
108+
`;
109+
110+
export {
111+
Container,
112+
Dimmed,
113+
Content,
114+
Title,
115+
ThumbnailContainer,
116+
Thumbnail,
117+
IconContainer,
118+
IconButton,
119+
IconCircle,
120+
IconText,
121+
Toast,
122+
};
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
import { useEffect, useState } from 'react';
2+
import { AnimatePresence } from 'motion/react';
3+
import { KakaoIcon, MoreInfo, LinkCopyIcon } from '@/assets/icons';
4+
import * as S from './MemeShareSheet.styles';
5+
6+
interface MemeShareSheetProps {
7+
isOpen: boolean;
8+
onClose: () => void;
9+
title: string;
10+
imageUrl: string;
11+
isWebview: boolean;
12+
onShareNative: () => void;
13+
}
14+
15+
const MemeShareSheet = ({
16+
isOpen,
17+
onClose,
18+
title,
19+
imageUrl,
20+
isWebview,
21+
onShareNative,
22+
}: MemeShareSheetProps) => {
23+
const [showToast, setShowToast] = useState(false);
24+
25+
useEffect(() => {
26+
// Kakao SDK 초기화
27+
if (!window.Kakao) {
28+
const script = document.createElement('script');
29+
script.src = 'https://developers.kakao.com/sdk/js/kakao.js';
30+
script.async = true;
31+
document.head.appendChild(script);
32+
33+
script.onload = () => {
34+
window.Kakao.init('05ba74b5a769929cd086247c874b60e4');
35+
};
36+
}
37+
}, []);
38+
39+
const handleKakaoShare = () => {
40+
if (window.Kakao) {
41+
window.Kakao.Share.sendDefault({
42+
objectType: 'feed',
43+
content: {
44+
title,
45+
description: '밈위키에서 더 많은 밈을 확인해보세요!',
46+
imageUrl,
47+
link: {
48+
mobileWebUrl: window.location.href,
49+
webUrl: window.location.href,
50+
},
51+
},
52+
buttons: [
53+
{
54+
title: '자세히 보기',
55+
link: {
56+
mobileWebUrl: window.location.href,
57+
webUrl: window.location.href,
58+
},
59+
},
60+
],
61+
});
62+
}
63+
};
64+
65+
const handleCopyLink = () => {
66+
navigator.clipboard.writeText(window.location.href);
67+
setShowToast(true);
68+
};
69+
70+
return (
71+
<AnimatePresence>
72+
{isOpen && (
73+
<>
74+
<S.Dimmed
75+
initial={{ opacity: 0 }}
76+
animate={{ opacity: 1 }}
77+
exit={{ opacity: 0 }}
78+
onClick={onClose}
79+
/>
80+
<S.Container
81+
initial={{ y: '100%' }}
82+
animate={{ y: 0 }}
83+
exit={{ y: '100%' }}
84+
transition={{ type: 'spring', damping: 25, stiffness: 200 }}
85+
>
86+
<S.Content>
87+
<S.Title>공유하기</S.Title>
88+
<S.ThumbnailContainer>
89+
<S.Thumbnail src={imageUrl} alt={title} />
90+
</S.ThumbnailContainer>
91+
<S.IconContainer>
92+
<S.IconButton onClick={handleKakaoShare}>
93+
<S.IconCircle isKakao>
94+
<KakaoIcon />
95+
</S.IconCircle>
96+
<S.IconText>카카오톡</S.IconText>
97+
</S.IconButton>
98+
<S.IconButton onClick={handleCopyLink}>
99+
<S.IconCircle>
100+
<LinkCopyIcon />
101+
</S.IconCircle>
102+
<S.IconText>링크 복사</S.IconText>
103+
</S.IconButton>
104+
{isWebview && (
105+
<S.IconButton onClick={onShareNative}>
106+
<S.IconCircle>
107+
<MoreInfo />
108+
</S.IconCircle>
109+
<S.IconText>더보기</S.IconText>
110+
</S.IconButton>
111+
)}
112+
</S.IconContainer>
113+
<AnimatePresence>
114+
{showToast && (
115+
<S.Toast
116+
initial={{ opacity: 0 }}
117+
animate={{ opacity: 1 }}
118+
exit={{ opacity: 0 }}
119+
transition={{ duration: 0.3 }}
120+
onAnimationComplete={() => {
121+
setTimeout(() => setShowToast(false), 2000);
122+
}}
123+
>
124+
클립보드에 복사되었습니다
125+
</S.Toast>
126+
)}
127+
</AnimatePresence>
128+
</S.Content>
129+
</S.Container>
130+
</>
131+
)}
132+
</AnimatePresence>
133+
);
134+
};
135+
136+
export default MemeShareSheet;
137+
138+
interface KakaoShare {
139+
sendDefault: (options: {
140+
objectType: string;
141+
content: {
142+
title: string;
143+
description: string;
144+
imageUrl: string;
145+
link: {
146+
mobileWebUrl: string;
147+
webUrl: string;
148+
};
149+
};
150+
buttons: Array<{
151+
title: string;
152+
link: {
153+
mobileWebUrl: string;
154+
webUrl: string;
155+
};
156+
}>;
157+
}) => void;
158+
}
159+
160+
interface KakaoSDK {
161+
init: (key: string) => void;
162+
Share: KakaoShare;
163+
}
164+
165+
declare global {
166+
interface Window {
167+
Kakao: KakaoSDK;
168+
}
169+
}

apps/web/src/pages/MemeDetailPage/index.tsx

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
import { useEffect, useState } from 'react';
1818
import { BridgeCommand, COMMAND_TYPE, CommandType } from '@/types/bridge';
1919
import useInAppBrowserDetect from '@/hooks/useInAppBrowserDetect';
20+
import MemeShareSheet from './components/MemeShareSheet';
2021

2122
// 전역에서 함수 정의
2223
if (typeof window !== 'undefined') {
@@ -37,6 +38,7 @@ const MemeDetailPage = () => {
3738
const { data: memeDetail } = useMemeDetailQuery(memeId!);
3839
const { mutate: shareMeme } = useShareMemeMutation();
3940
const { mutate: customMeme } = useMemeCustomMutation();
41+
const [shareSheetOpen, setShareSheetOpen] = useState(false);
4042

4143
const theme = useTheme();
4244

@@ -118,20 +120,27 @@ const MemeDetailPage = () => {
118120
// 밈 공유하기 mutation
119121
shareMeme({ id: memeId! });
120122

121-
if (isWebview) {
122-
nativeBridge.shareMeme({
123-
title: memeDetail?.success.title ?? '',
124-
image: memeDetail?.success.imgUrl ?? '',
125-
});
126-
} else {
127-
alert('밈 공유하기 클릭!');
128-
}
123+
// 공유 시트 열기
124+
setShareSheetOpen(true);
129125
}}
130126
>
131127
<ShareIcon />
132128
<span>공유하기</span>
133129
</S.ActionButton>
134130
</S.ButtonContainer>
131+
<MemeShareSheet
132+
isOpen={shareSheetOpen}
133+
onClose={() => setShareSheetOpen(false)}
134+
title={memeDetail?.success.title ?? ''}
135+
imageUrl={memeDetail?.success.imgUrl ?? ''}
136+
isWebview={isWebview}
137+
onShareNative={() => {
138+
nativeBridge.shareMeme({
139+
title: memeDetail?.success.title ?? '',
140+
image: memeDetail?.success.imgUrl ?? '',
141+
});
142+
}}
143+
/>
135144
</Layout>
136145
);
137146
};

0 commit comments

Comments
 (0)