Skip to content

Commit d71fc30

Browse files
committed
feat: MemeDetailPage에 로딩 상태를 위한 스켈레톤 UI 추가
- MemeDetailPage에 로딩 중 상태를 처리하기 위해 MemeDetailSkeleton 컴포넌트를 추가함 - 데이터 로딩 시 스켈레톤 UI를 표시하여 사용자 경험을 개선함
1 parent 957bd10 commit d71fc30

File tree

3 files changed

+173
-56
lines changed

3 files changed

+173
-56
lines changed
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import styled from '@emotion/styled';
2+
import { keyframes } from '@emotion/react';
3+
4+
const shimmer = keyframes`
5+
0% {
6+
background-position: -200% 0;
7+
}
8+
100% {
9+
background-position: 200% 0;
10+
}
11+
`;
12+
13+
const SkeletonBase = styled.div`
14+
background: linear-gradient(
15+
90deg,
16+
${({ theme }) => theme.palette.gray['gray-9']} 25%,
17+
${({ theme }) => theme.palette.gray['gray-8']} 37%,
18+
${({ theme }) => theme.palette.gray['gray-9']} 63%
19+
);
20+
background-size: 200% 100%;
21+
animation: ${shimmer} 1.5s infinite;
22+
`;
23+
24+
export const Container = styled.div`
25+
width: 100%;
26+
height: 100%;
27+
display: flex;
28+
flex-direction: column;
29+
gap: 24px;
30+
padding: 20px;
31+
`;
32+
33+
export const ImageContainer = styled(SkeletonBase)`
34+
width: 100%;
35+
aspect-ratio: 1;
36+
border-radius: 8px;
37+
`;
38+
39+
export const ContentContainer = styled.div`
40+
display: flex;
41+
flex-direction: column;
42+
gap: 16px;
43+
`;
44+
45+
export const TitleSkeleton = styled(SkeletonBase)`
46+
width: 60%;
47+
height: 32px;
48+
border-radius: 4px;
49+
`;
50+
51+
export const HashTagsSkeleton = styled(SkeletonBase)`
52+
width: 80%;
53+
height: 24px;
54+
border-radius: 4px;
55+
`;
56+
57+
export const SectionTitleSkeleton = styled(SkeletonBase)`
58+
width: 40%;
59+
height: 24px;
60+
border-radius: 4px;
61+
margin-top: 8px;
62+
`;
63+
64+
export const SectionTextSkeleton = styled(SkeletonBase)`
65+
width: 100%;
66+
height: 80px;
67+
border-radius: 4px;
68+
`;
69+
70+
export const ButtonContainer = styled.div`
71+
position: fixed;
72+
max-width: ${({ theme }) => theme.breakpoints.mobile};
73+
margin: 0 auto;
74+
bottom: 0;
75+
left: 0;
76+
right: 0;
77+
padding: 20px;
78+
display: flex;
79+
gap: 12px;
80+
background-color: ${({ theme }) => theme.palette.gray['gray-10']};
81+
`;
82+
83+
export const ButtonSkeleton = styled(SkeletonBase)`
84+
flex: 1;
85+
height: 48px;
86+
border-radius: 8px;
87+
`;
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import * as S from './MemeDetailSkeleton.styles';
2+
3+
const MemeDetailSkeleton = () => {
4+
return (
5+
<S.Container>
6+
<S.ImageContainer />
7+
<S.ContentContainer>
8+
<S.TitleSkeleton />
9+
<S.HashTagsSkeleton />
10+
<S.SectionTitleSkeleton />
11+
<S.SectionTextSkeleton />
12+
<S.SectionTitleSkeleton />
13+
<S.SectionTextSkeleton />
14+
</S.ContentContainer>
15+
<S.ButtonContainer>
16+
<S.ButtonSkeleton />
17+
<S.ButtonSkeleton />
18+
</S.ButtonContainer>
19+
</S.Container>
20+
);
21+
};
22+
23+
export default MemeDetailSkeleton;

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

Lines changed: 63 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { useEffect, useState } from 'react';
1818
import { BridgeCommand, COMMAND_TYPE, CommandType } from '@/types/bridge';
1919
import useInAppBrowserDetect from '@/hooks/useInAppBrowserDetect';
2020
import MemeShareSheet from './components/MemeShareSheet';
21+
import MemeDetailSkeleton from './components/MemeDetailSkeleton';
2122

2223
// 전역에서 함수 정의
2324
if (typeof window !== 'undefined') {
@@ -35,7 +36,7 @@ const MemeDetailPage = () => {
3536
const [isWebview, setIsWebview] = useState(false);
3637
const { memeId } = useParams();
3738
const { moveToStore } = useInAppBrowserDetect();
38-
const { data: memeDetail } = useMemeDetailQuery(memeId!);
39+
const { data: memeDetail, isLoading } = useMemeDetailQuery(memeId!);
3940
const { mutate: shareMeme } = useShareMemeMutation();
4041
const { mutate: customMeme } = useMemeCustomMutation();
4142
const [shareSheetOpen, setShareSheetOpen] = useState(false);
@@ -71,63 +72,69 @@ const MemeDetailPage = () => {
7172
backgroundColor: theme.palette.gray['gray-10'],
7273
}}
7374
>
74-
<S.Container>
75-
<S.ImageContainer>
76-
<S.Image
77-
src={memeDetail?.success.imgUrl}
78-
alt={memeDetail?.success.title}
79-
/>
80-
</S.ImageContainer>
81-
<S.ContentContainer>
82-
<S.Title>{memeDetail?.success.title}</S.Title>
83-
<S.HashTags>
84-
{memeDetail?.success.hashtags.map((tag) => `${tag} `)}
85-
</S.HashTags>
86-
<S.SectionTitle>
87-
<SymbolTwoIcon width={18} height={18} />
88-
이럴 때 쓰세요
89-
</S.SectionTitle>
90-
<S.SectionText>{memeDetail?.success.usageContext}</S.SectionText>
91-
<S.SectionTitle>
92-
<SymbolThreeIcon width={18} height={18} />
93-
이렇게 시작됐어요
94-
</S.SectionTitle>
95-
<S.SectionText>{memeDetail?.success.origin}</S.SectionText>
96-
</S.ContentContainer>
97-
</S.Container>
98-
<S.ButtonContainer>
99-
<S.ActionButton
100-
isPrimary
101-
onClick={() => {
102-
// 밈 꾸미기 mutation
103-
customMeme({ id: memeId! });
75+
{isLoading ? (
76+
<MemeDetailSkeleton />
77+
) : (
78+
<>
79+
<S.Container>
80+
<S.ImageContainer>
81+
<S.Image
82+
src={memeDetail?.success.imgUrl}
83+
alt={memeDetail?.success.title}
84+
/>
85+
</S.ImageContainer>
86+
<S.ContentContainer>
87+
<S.Title>{memeDetail?.success.title}</S.Title>
88+
<S.HashTags>
89+
{memeDetail?.success.hashtags.map((tag) => `${tag} `)}
90+
</S.HashTags>
91+
<S.SectionTitle>
92+
<SymbolTwoIcon width={18} height={18} />
93+
이럴 때 쓰세요
94+
</S.SectionTitle>
95+
<S.SectionText>{memeDetail?.success.usageContext}</S.SectionText>
96+
<S.SectionTitle>
97+
<SymbolThreeIcon width={18} height={18} />
98+
이렇게 시작됐어요
99+
</S.SectionTitle>
100+
<S.SectionText>{memeDetail?.success.origin}</S.SectionText>
101+
</S.ContentContainer>
102+
</S.Container>
103+
<S.ButtonContainer>
104+
<S.ActionButton
105+
isPrimary
106+
onClick={() => {
107+
// 밈 꾸미기 mutation
108+
customMeme({ id: memeId! });
104109

105-
if (isWebview) {
106-
nativeBridge.customMeme({
107-
title: memeDetail?.success.title ?? '',
108-
image: memeDetail?.success.imgUrl ?? '',
109-
});
110-
} else {
111-
moveToStore();
112-
}
113-
}}
114-
>
115-
<MemeDesignPenIcon />
116-
<span>밈 꾸미기</span>
117-
</S.ActionButton>
118-
<S.ActionButton
119-
onClick={() => {
120-
// 밈 공유하기 mutation
121-
shareMeme({ id: memeId! });
110+
if (isWebview) {
111+
nativeBridge.customMeme({
112+
title: memeDetail?.success.title ?? '',
113+
image: memeDetail?.success.imgUrl ?? '',
114+
});
115+
} else {
116+
moveToStore();
117+
}
118+
}}
119+
>
120+
<MemeDesignPenIcon />
121+
<span>밈 꾸미기</span>
122+
</S.ActionButton>
123+
<S.ActionButton
124+
onClick={() => {
125+
// 밈 공유하기 mutation
126+
shareMeme({ id: memeId! });
122127

123-
// 공유 시트 열기
124-
setShareSheetOpen(true);
125-
}}
126-
>
127-
<ShareIcon />
128-
<span>공유하기</span>
129-
</S.ActionButton>
130-
</S.ButtonContainer>
128+
// 공유 시트 열기
129+
setShareSheetOpen(true);
130+
}}
131+
>
132+
<ShareIcon />
133+
<span>공유하기</span>
134+
</S.ActionButton>
135+
</S.ButtonContainer>
136+
</>
137+
)}
131138
<MemeShareSheet
132139
isOpen={shareSheetOpen}
133140
onClose={() => setShareSheetOpen(false)}

0 commit comments

Comments
 (0)