diff --git a/src/components/Skeleton/index.tsx b/src/components/Skeleton/index.tsx new file mode 100644 index 00000000..d79b5d1b --- /dev/null +++ b/src/components/Skeleton/index.tsx @@ -0,0 +1,37 @@ +import { SkeletonContainer } from './styles'; + +interface SkeletonProps { + width?: string | number; + height?: string | number; + borderRadius?: string | number; + className?: string; + style?: React.CSSProperties; +} + +const Skeleton: React.FC = ({ + width = '100%', + height = '16px', + borderRadius = '5px', + className = '', +}) => { + // width와 height가 숫자인 경우 rem 단위를 추가 + const getSize = (size: string | number) => { + if (typeof size === 'number') { + return `${size}rem`; + } + return size; + }; + + return ( + + ); +}; + +export default Skeleton; diff --git a/src/components/Skeleton/styles.tsx b/src/components/Skeleton/styles.tsx new file mode 100644 index 00000000..14638099 --- /dev/null +++ b/src/components/Skeleton/styles.tsx @@ -0,0 +1,19 @@ +import { styled } from 'styled-components'; + +export const SkeletonContainer = styled.div` + background-color: #e0e0e0; + position: relative; + overflow: hidden; + background: linear-gradient(90deg, #e0e0e0 0%, #f0f0f0 50%, #e0e0e0 100%); + background-size: 200% 100%; + animation: shimmer 1.5s infinite; + + @keyframes shimmer { + 0% { + background-position: 200% 0; + } + 100% { + background-position: -200% 0; + } + } +`; diff --git a/src/pages/Account/AccountCancel/index.tsx b/src/pages/Account/AccountCancel/index.tsx index 17e4dd2c..61adba0f 100644 --- a/src/pages/Account/AccountCancel/index.tsx +++ b/src/pages/Account/AccountCancel/index.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useState, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; import theme from '@styles/theme'; @@ -11,6 +11,7 @@ import back from '@assets/arrow/left.svg'; import BottomButton from '@components/BottomButton/index'; import { OODDFrame } from '@components/Frame/Frame'; import Modal from '@components/Modal/index'; +import Skeleton from '@components/Skeleton'; import { StyledText } from '@components/Text/StyledText'; import TopBar from '@components/TopBar/index'; @@ -30,8 +31,15 @@ const AccountCancel: React.FC = () => { const [isChecked, setIsChecked] = useState(false); const [modalContent, setModalContent] = useState(null); const [isModalVisible, setIsModalVisible] = useState(false); + const [isLoading, setIsLoading] = useState(true); // Loading state const navigate = useNavigate(); + useEffect(() => { + setTimeout(() => { + setIsLoading(false); + }, 1000); + }, []); + const handleCheckboxChange = () => { setIsChecked(!isChecked); }; @@ -79,6 +87,23 @@ const AccountCancel: React.FC = () => { } }; + if (isLoading) { + return ( + + + navigate(-1)} /> + + + OOTD 탈퇴 전 확인하세요! + + + + + + + ); + } + return ( diff --git a/src/pages/Account/AccountSetting/index.tsx b/src/pages/Account/AccountSetting/index.tsx index 65ba1624..efadc1f0 100644 --- a/src/pages/Account/AccountSetting/index.tsx +++ b/src/pages/Account/AccountSetting/index.tsx @@ -12,8 +12,8 @@ import leave from '@assets/default/leave.svg'; import Profile_s from '@assets/default/my-page.svg'; import { OODDFrame } from '@components/Frame/Frame'; -import Loading from '@components/Loading/index'; import Modal from '@components/Modal'; +import Skeleton from '@components/Skeleton'; import { StyledText } from '@components/Text/StyledText'; import TopBar from '@components/TopBar/index'; @@ -44,8 +44,7 @@ const AccountSetting: React.FC = () => { setIsLoading(false); } }; - - getUserInfo(); + setTimeout(getUserInfo, 1000); }, []); const handleConfirmLogout = () => { @@ -68,7 +67,33 @@ const AccountSetting: React.FC = () => { }; if (isLoading) { - return ; + return ( + + + navigate(-1)} /> + + + + {' '} + + + + + + + + + + + + + + + + + + + ); } return ( diff --git a/src/pages/Account/AccountSetting/styles.tsx b/src/pages/Account/AccountSetting/styles.tsx index ef19ecf5..f4309b02 100644 --- a/src/pages/Account/AccountSetting/styles.tsx +++ b/src/pages/Account/AccountSetting/styles.tsx @@ -13,8 +13,6 @@ export const ProfilePicWrapper = styled.div` display: flex; flex-direction: column; align-items: center; - margin-bottom: 1.25rem; - margin-top: 1.5rem; `; export const ProfilePic = styled.div` @@ -24,7 +22,6 @@ export const ProfilePic = styled.div` border-radius: 50%; overflow: hidden; margin-top: 2.125rem; - margin-bottom: 1.375rem; img { width: 100%; @@ -42,7 +39,7 @@ export const Row = styled.div` justify-content: center; align-items: center; width: 100%; - margin-bottom: 0.625rem; + margin-top: 10px; ${Label} { width: auto; @@ -67,7 +64,8 @@ export const List = styled.ul` export const ListItem = styled.li` display: flex; align-items: center; - padding: 0.9375rem 1.25rem; + padding: 15px 10px; + border-bottom: 0px solid ${({ theme }) => theme.colors.background.divider}; cursor: pointer; diff --git a/src/pages/Post/index.tsx b/src/pages/Post/index.tsx index ad69bbfd..dfae0030 100644 --- a/src/pages/Post/index.tsx +++ b/src/pages/Post/index.tsx @@ -7,6 +7,7 @@ import { modifyPostRepresentativeStatusApi, deletePostApi } from '@apis/post'; import { isPostRepresentativeAtom, postIdAtom, userAtom } from '@recoil/Post/PostAtom'; import { getCurrentUserId } from '@utils/getCurrentUserId'; +import back from '@assets/arrow/left.svg'; import Delete from '@assets/default/delete.svg'; import Edit from '@assets/default/edit.svg'; import Pin from '@assets/default/pin.svg'; @@ -14,7 +15,10 @@ import Pin from '@assets/default/pin.svg'; import BottomSheet from '@components/BottomSheet'; import BottomSheetMenu from '@components/BottomSheet/BottomSheetMenu'; import OptionsBottomSheet from '@components/BottomSheet/OptionsBottomSheet'; +import { OODDFrame } from '@components/Frame/Frame'; import Modal from '@components/Modal'; +import Skeleton from '@components/Skeleton'; +import TopBar from '@components/TopBar/index'; import type { BottomSheetMenuProps } from '@components/BottomSheet/BottomSheetMenu/dto'; import type { BottomSheetProps } from '@components/BottomSheet/dto'; @@ -23,6 +27,8 @@ import type { ModalProps } from '@components/Modal/dto'; import PostBase from './PostBase/index'; +import { PicWrapper, NameWrapper, InfoWrapper, PostWrapper } from './styles'; + const Post: React.FC = () => { const user = useRecoilValue(userAtom); const postId = useRecoilValue(postIdAtom); @@ -37,6 +43,8 @@ const Post: React.FC = () => { const [modalContent, setModalContent] = useState(''); const [postPinStatus, setPostPinStatus] = useState<'지정' | '해제'>('지정'); + const [isLoading, setIsLoading] = useState(true); + const navigate = useNavigate(); const currentUserId = getCurrentUserId(); @@ -87,6 +95,10 @@ const Post: React.FC = () => { }; useEffect(() => { + setTimeout(() => { + setIsLoading(false); + }, 1000); + // 현재 게시글이 내 게시글인지 확인 if (user?.id && postId) { setIsMyPost(currentUserId === user.id); @@ -163,6 +175,25 @@ const Post: React.FC = () => { content: modalContent, }; + if (isLoading) { + return ( + + navigate(-1)} /> + + + + + + + + + + + + + ); + } + return ( <> diff --git a/src/pages/Post/styles.tsx b/src/pages/Post/styles.tsx index 5e347dfe..0c610fe8 100644 --- a/src/pages/Post/styles.tsx +++ b/src/pages/Post/styles.tsx @@ -1,28 +1,21 @@ import { styled } from 'styled-components'; -export const InputLayout = styled.div` +export const InfoWrapper = styled.div` display: flex; - flex-direction: column; - justify-content: center; - align-items: center; + flex-direction: row; + align-items: left; +`; + +export const PicWrapper = styled.div` + margin-left: 47px; +`; + +export const NameWrapper = styled.div` + margin-left: 10px; + margin-top: 10px; +`; - textarea { - display: block; - width: calc(100% - 3rem); - height: 5.75rem; - border-radius: 0.125rem; - border: 0.0625rem solid ${({ theme }) => theme.colors.gray[600]}; - margin-bottom: 5.875rem; - z-index: 2; - margin-top: -3.75rem; - outline: none; - padding: 0.8125rem 0.9375rem; - font-family: 'Pretendard Variable'; - font-size: 1rem; - font-style: normal; - font-weight: 300; - line-height: 150%; - color: ${({ theme }) => theme.colors.text.primary}; - resize: none; - } +export const PostWrapper = styled.div` + margin-top: 10px; + padding-inline: 30px; `; diff --git a/src/pages/Profile/ButtonSecondary/styles.tsx b/src/pages/Profile/ButtonSecondary/styles.tsx index 14b7e3f7..db0e8f20 100644 --- a/src/pages/Profile/ButtonSecondary/styles.tsx +++ b/src/pages/Profile/ButtonSecondary/styles.tsx @@ -2,7 +2,7 @@ import { styled } from 'styled-components'; export const Button = styled.button` width: 90%; - margin: 1rem auto; + margin: 16px auto; height: 3.1rem; text-align: center; color: ${({ theme }) => theme.colors.brand.primary}; diff --git a/src/pages/Profile/ProfileEdit/index.tsx b/src/pages/Profile/ProfileEdit/index.tsx index b9eb24a3..28676c02 100644 --- a/src/pages/Profile/ProfileEdit/index.tsx +++ b/src/pages/Profile/ProfileEdit/index.tsx @@ -15,8 +15,8 @@ import imageBasic from '@assets/default/defaultProfile.svg'; import BottomButton from '@components/BottomButton/index'; import { OODDFrame } from '@components/Frame/Frame'; -import Loading from '@components/Loading/index'; import Modal from '@components/Modal/index'; +import Skeleton from '@components/Skeleton'; import { StyledText } from '@components/Text/StyledText'; import TopBar from '@components/TopBar/index'; @@ -83,7 +83,7 @@ const ProfileEdit: React.FC = () => { } }; - getUserInfo(); + setTimeout(getUserInfo, 1000); }, []); const handleButtonClick = () => { @@ -155,7 +155,59 @@ const ProfileEdit: React.FC = () => { }; if (isLoading || uploading) { - return ; + return ( + + + navigate(-1)} /> + + + + + + + + + + + + 이름 + + + + + + 닉네임 + + + + + + 소개글 + + + + + + 전화번호 + + + + + + 생년월일 + + + + + + 이메일 + + + + + + + ); } return ( @@ -222,7 +274,7 @@ const ProfileEdit: React.FC = () => { setEmail(e.target.value)} /> - + ); diff --git a/src/pages/Profile/index.tsx b/src/pages/Profile/index.tsx index 5b8a41e6..95673dbe 100644 --- a/src/pages/Profile/index.tsx +++ b/src/pages/Profile/index.tsx @@ -1,3 +1,4 @@ +import * as React from 'react'; import { useState, useEffect } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; @@ -19,9 +20,9 @@ import button_plus from '@assets/default/plus.svg'; import CommentBottomSheet from '@components/BottomSheet/CommentBottomSheet'; import OptionsBottomSheet from '@components/BottomSheet/OptionsBottomSheet'; import { OODDFrame } from '@components/Frame/Frame'; -import Loading from '@components/Loading'; import Modal from '@components/Modal'; import NavBar from '@components/NavBar'; +import Skeleton from '@components/Skeleton'; import { StyledText } from '@components/Text/StyledText'; import TopBar from '@components/TopBar'; @@ -36,6 +37,7 @@ import UserProfile from './UserProfile/index'; import { ProfileContainer, Header, + ProfileDetail, StatsContainer, Stat, StatNumber, @@ -44,6 +46,7 @@ import { AddButton, NoPostWrapper, Button, + ButtonSkeleton, } from './styles'; const Profile: React.FC = () => { @@ -70,15 +73,19 @@ const Profile: React.FC = () => { try { const response = await getUserInfoApi(profileUserId); const postResponse = await getUserPostListApi(1, 10, profileUserId); - setUserInfo(response.data); - setPosts(postResponse.data.post); - setTotalPosts(postResponse.data.totalPostsCount); + // 1초 동안 스켈레톤 보여주기 확인용! 나중에 다 수정하고 삭제예정! + setTimeout(() => { + setUserInfo(response.data); + setPosts(postResponse.data.post); + setTotalPosts(postResponse.data.totalPostsCount); + setIsLoading(false); + }, 1000); } catch (error) { console.error('데이터 가져오기 실패:', error); - } finally { - setIsLoading(false); + setIsLoading(false); // 실패해도 로딩 상태는 끝나야 하니까 여기서도 false 처리 } }; + fetchData(); }, [profileUserId]); @@ -100,7 +107,64 @@ const Profile: React.FC = () => { setIsBottomSheetOpen(false); }; - if (isLoading) return ; + // 로딩 중일 때 스켈레톤 UI 표시 + if (isLoading) { + return ( + + + {isMyPage ? ( + + ) : ( + navigate(-1)} + onClickRightButton={() => setIsOptionsBottomSheetOpen(true)} + /> + )} + +
+ {/* 프로필 섹션 스켈레톤 */} + + + + + + +
+ + {/* 버튼 스켈레톤 */} + + + + + {/* 통계 스켈레톤 */} + + + + + {isMyPage && ( + + + + )} + + + + + + {/* 포스트 스켈레톤 */} + + {[1, 2, 3, 4].map((item) => ( + + ))} + + + {isMyPage && } +
+
+ ); + } return ( diff --git a/src/pages/Profile/styles.tsx b/src/pages/Profile/styles.tsx index 50c6463f..39880a72 100644 --- a/src/pages/Profile/styles.tsx +++ b/src/pages/Profile/styles.tsx @@ -12,6 +12,14 @@ export const ProfileContainer = styled.div` padding-top: 0rem; `; +export const ProfileDetail = styled.div` + flex: 1; + margin-left: 15px; + display: flex; + flex-direction: column; + gap: 5px; +`; + export const Header = styled.div` margin: 0.5rem 1.25rem; display: flex; @@ -35,7 +43,6 @@ export const Stat = styled.div` export const StatNumber = styled.div` color: ${({ theme }) => theme.colors.text.caption}; - //변경된 컬러시스템에서의 gray4가 800으로 나와있어서 적용해보면 색상이 다르게 나옵니다! text-align: center; font-family: 'Pretendard'; font-size: 1rem; @@ -59,7 +66,9 @@ export const PostsContainer = styled.div` justify-content: space-between; gap: 0.9375rem; margin-bottom: 100px; - padding: 1.25rem; + padding: 20px; + width: 100%; + `; export const AddButton = styled.button` @@ -94,3 +103,8 @@ export const Button = styled.button` padding: 0.625rem; background: ${({ theme }) => theme.colors.brand.gradient}; `; + +export const ButtonSkeleton = styled.button` + width: 90%; + margin: 16px auto; +`;