diff --git a/src/assets/mypage/alert.svg b/src/assets/mypage/alert.svg new file mode 100644 index 00000000..da1616b4 --- /dev/null +++ b/src/assets/mypage/alert.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/mypage/charactor.svg b/src/assets/mypage/charactor.svg new file mode 100644 index 00000000..8f957739 --- /dev/null +++ b/src/assets/mypage/charactor.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/mypage/check.svg b/src/assets/mypage/check.svg new file mode 100644 index 00000000..ab4cdf72 --- /dev/null +++ b/src/assets/mypage/check.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/mypage/guide.svg b/src/assets/mypage/guide.svg new file mode 100644 index 00000000..30c3c9c4 --- /dev/null +++ b/src/assets/mypage/guide.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/assets/mypage/heart.svg b/src/assets/mypage/heart.svg new file mode 100644 index 00000000..e8c3224c --- /dev/null +++ b/src/assets/mypage/heart.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/mypage/logout.svg b/src/assets/mypage/logout.svg new file mode 100644 index 00000000..282d3200 --- /dev/null +++ b/src/assets/mypage/logout.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/mypage/notice.svg b/src/assets/mypage/notice.svg new file mode 100644 index 00000000..ed7e0eed --- /dev/null +++ b/src/assets/mypage/notice.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/assets/mypage/save.svg b/src/assets/mypage/save.svg new file mode 100644 index 00000000..2a507d5b --- /dev/null +++ b/src/assets/mypage/save.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/mypage/service.svg b/src/assets/mypage/service.svg new file mode 100644 index 00000000..ebcb78b0 --- /dev/null +++ b/src/assets/mypage/service.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/mypage/terms.svg b/src/assets/mypage/terms.svg new file mode 100644 index 00000000..2b742bae --- /dev/null +++ b/src/assets/mypage/terms.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/assets/mypage/ver.svg b/src/assets/mypage/ver.svg new file mode 100644 index 00000000..bd04d96e --- /dev/null +++ b/src/assets/mypage/ver.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/mypage/withdraw.svg b/src/assets/mypage/withdraw.svg new file mode 100644 index 00000000..3f7ee6eb --- /dev/null +++ b/src/assets/mypage/withdraw.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/src/components/Mypage/EditProfile.tsx b/src/components/Mypage/EditProfile.tsx new file mode 100644 index 00000000..e69de29b diff --git a/src/components/Mypage/MenuButton.tsx b/src/components/Mypage/MenuButton.tsx new file mode 100644 index 00000000..d3867683 --- /dev/null +++ b/src/components/Mypage/MenuButton.tsx @@ -0,0 +1,50 @@ +import { colors, typography } from '@/styles/global/global'; +import styled from '@emotion/styled'; +import rightArrow from '../../assets/common/right-Chevron.svg'; + +export interface MenuButtonProps { + src: string; + name: string; + textColor?: string; + isButton?: boolean; + onClick?: () => void; +} + +const MenuButton = ({ src, name, isButton, onClick }: MenuButtonProps) => { + return ( + + + + {name} + + {isButton && } + + ); +}; + +const Wrapper = styled.div` + display: flex; + min-width: 280px; + width: 100%; + height: 56px; + padding: 16px 12px; + justify-content: space-between; + align-items: center; + gap: 16px; + border-radius: 12px; + background-color: ${colors.darkgrey.dark}; + cursor: pointer; + + color: ${colors.white}; + font-size: ${typography.fontSize.base}; + font-weight: ${typography.fontWeight.semibold}; + line-height: 24px; + + .main { + display: flex; + flex-direction: row; + gap: 8px; + } +`; + +export default MenuButton; diff --git a/src/components/common/NavBar.tsx b/src/components/common/NavBar.tsx index 028e0471..300e88c2 100644 --- a/src/components/common/NavBar.tsx +++ b/src/components/common/NavBar.tsx @@ -69,7 +69,7 @@ const items: RouteItem[] = [ { path: '/feed', label: '피드', icon: FeedIcon, activeIcon: FeedIconActive }, { path: '/group', label: '모임', icon: GroupIcon, activeIcon: GroupIconActive }, { path: '/search', label: '검색', icon: SearchIcon, activeIcon: SearchIconActive }, - { path: '/my', label: '내 정보', icon: MyIcon, activeIcon: MyIconActive }, + { path: '/mypage', label: '내 정보', icon: MyIcon, activeIcon: MyIconActive }, ]; const NavBar = ({ src, path }: FabProps) => { diff --git a/src/components/feed/TabBar.tsx b/src/components/feed/TabBar.tsx index d13ee4fd..4e53d2b3 100644 --- a/src/components/feed/TabBar.tsx +++ b/src/components/feed/TabBar.tsx @@ -22,6 +22,7 @@ const TabButton = styled.div` justify-content: center; align-items: center; text-align: center; + width: 60px; padding: 8px 4px; font-size: var(--font-size-lg); cursor: pointer; diff --git a/src/data/bookData.ts b/src/data/bookData.ts new file mode 100644 index 00000000..6c9e61c1 --- /dev/null +++ b/src/data/bookData.ts @@ -0,0 +1,25 @@ +import type { Book } from '@/types/book'; + +export const mockBooks: Book[] = [ + { + isbn: 11234131, + title: '채식주의자', + author: '한강', + publisher: '창비', + coverUrl: 'https://image.yes24.com/goods/17122707/XL', + }, + { + isbn: 21234123, + title: '채소 마스터 클래스', + author: '백지혜', + publisher: '세미콜론', + coverUrl: 'https://image.yes24.com/goods/109378551/XL', + }, + { + isbn: 31324122, + title: '채소 식탁', + author: '김경민', + publisher: '래디시', + coverUrl: 'https://image.yes24.com/goods/117194041/XL', + }, +]; diff --git a/src/data/postData.ts b/src/data/postData.ts index b1be4139..07a2ef31 100644 --- a/src/data/postData.ts +++ b/src/data/postData.ts @@ -37,6 +37,23 @@ export const mockPosts: PostData[] = [ commentCount: 125, isPublic: false, }, + { + profileImgUrl: 'https://placehold.co/24x24', + userName: 'userName', + userId: 177, + userTitle: 'userTitle', + titleColor: '#FF8BAC', + createdAt: '12시간 전', + bookTitle: '제목입니다제목입니다제목입니다', + bookAuthor: '작가입니다', + isbn: 111152544, + postContent: + '내용입니다내용입니다내용입니다내용입니다내용입니다내용입니다내용입니다내용입니다내용입니다내용입니다내용입니다내용입니다내용입니다내용입니다내용입니다내용입니다내용입니다내용입니다내용입니다', + feedId: 58, + initialLikeCount: 125, + commentCount: 125, + isPublic: false, + }, // …다른 포스트들… ]; diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 6922bcb3..41cebc3e 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -25,6 +25,12 @@ import GroupDetail from './groupDetail/GroupDetail'; import FeedDetailPage from './feed/FeedDetailPage'; import ScrollToTop from '@/components/common/ScrollToTop'; import UserSearch from './feed/UserSearch'; +import Mypage from './mypage/Mypage'; +import SavePage from './mypage/SavePage'; +import AlertPage from './mypage/AlertPage'; +import WithdrawPage from './mypage/WithdrawPage'; +import WithdrawDonePage from './mypage/WithdrawDonePage'; +import EditPage from './mypage/EditPage'; import Notice from './notice/Notice'; const Router = () => { @@ -52,6 +58,12 @@ const Router = () => { } /> } /> } /> + } /> + } /> + } /> + } /> + } /> + } /> } /> >, ), diff --git a/src/pages/mypage/AlertPage.tsx b/src/pages/mypage/AlertPage.tsx new file mode 100644 index 00000000..e5df71d7 --- /dev/null +++ b/src/pages/mypage/AlertPage.tsx @@ -0,0 +1,148 @@ +import { useNavigate } from 'react-router-dom'; +import { useState } from 'react'; +import TitleHeader from '../../components/common/TitleHeader'; +import { usePopupActions } from '@/hooks/usePopupActions'; +import leftArrow from '../../assets/common/leftArrow.svg'; +import styled from '@emotion/styled'; +import { colors, typography } from '@/styles/global/global'; + +const AlertPage = () => { + const navigate = useNavigate(); + const { openSnackbar } = usePopupActions(); + + const [toggleStates, setToggleStates] = useState({ + push: true, + // feed: true, + // group: false, + }); + + const handleBack = () => { + navigate('/mypage'); + }; + + const handleToggle = (key: keyof typeof toggleStates, label: string) => { + const newState = !toggleStates[key]; + setToggleStates(prev => ({ + ...prev, + [key]: newState, + })); + + openSnackbar({ + message: `${label} 알림이 ${newState ? '설정되었어요.' : '해제되었어요.'}`, + variant: 'top', + onClose: () => {}, + }); + }; + + const alertSettings = [ + { key: 'push' as const, label: '푸시 알림', desc: '알림센터의 모든 알림을 포함해요' }, + // { key: 'feed' as const, label: '피드 알림', desc: '새로운 피드 소식을 알려드려요' }, + // { key: 'group' as const, label: '모임 알림', desc: '모임 관련 소식을 알려드려요' }, + ]; + + return ( + + } + onLeftClick={handleBack} + /> + + {alertSettings.map(setting => ( + + + {setting.label} + + {setting.desc} + handleToggle(setting.key, setting.label)} + > + + + + + + ))} + + + ); +}; + +export const Wrapper = styled.div` + display: flex; + position: relative; + flex-direction: column; + align-items: center; + min-width: 320px; + max-width: 767px; + height: 100vh; + margin: 0 auto; + padding-top: 76px; + background-color: #121212; +`; + +const Content = styled.div` + display: flex; + flex-direction: column; + width: 100%; + padding: 20px; + gap: 24px; +`; + +const SettingItem = styled.div` + display: flex; + justify-content: space-between; + align-items: center; + padding: 16px 0; +`; + +const SettingInfo = styled.div` + display: flex; + flex-direction: column; + gap: 4px; + flex: 1; +`; + +const SettingTitle = styled.div` + color: ${colors.white}; + font-size: ${typography.fontSize.base}; + font-weight: ${typography.fontWeight.semibold}; + line-height: 24px; +`; + +const SubSection = styled.div` + display: flex; + flex-direction: row; + justify-content: space-between; +`; + +const SettingDesc = styled.div` + color: ${colors.grey[200]}; + font-size: ${typography.fontSize.sm}; + font-weight: ${typography.fontWeight.regular}; + line-height: 20px; +`; + +const Toggle = styled.div<{ isActive: boolean }>` + width: 44px; + height: 24px; + border-radius: 12px; + background-color: ${props => (props.isActive ? colors.purple.main : colors.grey[300])}; + position: relative; + cursor: pointer; + transition: background-color 0.3s ease; +`; + +const ToggleSlider = styled.div<{ isActive: boolean }>` + width: 20px; + height: 20px; + border-radius: 50%; + background-color: ${colors.white}; + position: absolute; + top: 2px; + left: ${props => (props.isActive ? '22px' : '2px')}; + transition: left 0.3s ease; +`; + +export default AlertPage; diff --git a/src/pages/mypage/EditPage.styled.ts b/src/pages/mypage/EditPage.styled.ts new file mode 100644 index 00000000..20c01086 --- /dev/null +++ b/src/pages/mypage/EditPage.styled.ts @@ -0,0 +1,225 @@ +import styled from '@emotion/styled'; + +export const Wrapper = styled.div` + display: flex; + position: relative; + flex-direction: column; + align-items: center; + min-width: 320px; + max-width: 767px; + width: 100%; + height: 100vh; + padding: 96px 20px 0 20px; + gap: 40px; + margin: 0 auto; + background-color: #121212; +`; + +export const Container = styled.div` + position: relative; + display: flex; + flex-direction: column; + background-color: var(--color-black-main); + width: 100%; + gap: 12px; + + .title { + color: var(--color-white); + font-size: var(--font-size-lg); + font-weight: var(--font-weight-semibold); + } + + .subtitle { + color: var(--color-grey-100); + font-size: var(--font-size-sm); + font-weight: var(--font-weight-regular); + } + + .notice { + color: var(--color-neongreen); + font-size: var(--font-size-xs); + font-weight: var(--font-weight-regular); + } + + .errorMessage { + position: absolute; + bottom: -20px; + left: 0; + color: var(--color-text-warning_red, #ff9496); + font-size: var(--string-size-small03, 12px); + font-weight: var(--string-weight-regular, 400); + line-height: normal; + } + + .content { + display: flex; + flex-direction: column; + align-items: center; + gap: 76px; + padding-top: 40px; + + .userInfo { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + + .profile { + display: flex; + width: 54px; + height: 54px; + justify-content: center; + align-items: center; + border-radius: 54px; + border: 0.5px solid var(--color-grey-200); + overflow: hidden; + + img { + width: 54px; + height: 54px; + flex-shrink: 0; + } + } + + .username { + padding: 8px 0 4px 0; + color: var(--color-white); + font-size: var(--font-size-lg); + font-weight: var(--font-weight-semibold); + line-height: 24px; + letter-spacing: 0.018px; + } + + .subname { + color: var(--color-character-pink); + font-size: var(--font-size-sm); + font-weight: var(--font-weight-regular); + line-height: normal; + } + } + } + + .startBtn { + display: flex; + justify-content: center; + align-items: center; + width: 180px; + height: 44px; + padding: 10px 12px; + gap: 8px; + border-radius: 12px; + background: var(--color-purple-main); + color: var(--color-white); + font-size: var(--font-size-base); + font-weight: var(--font-weight-semibold); + line-height: 24px; + cursor: pointer; + } + + .genreGrid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(132px, 1fr)); + gap: 16px; + + .genreCard { + position: relative; + display: flex; + flex-direction: column; + overflow: hidden; + justify-content: center; + align-items: flex-end; + text-align: center; + + height: 100px; + min-width: 130px; + max-width: 180px; + padding: 29px 20px; + gap: 10px; + border-radius: 12px; + border: 1px solid #3d3d3d; + cursor: pointer; + transition: border-color 0.2s; + + .textbox { + display: flex; + flex-direction: column; + gap: 5px; + z-index: 1; + } + + img.bg { + position: absolute; + bottom: 0px; + left: 5px; + width: 80px; + height: 70px; + opacity: 0.2; + z-index: 0; + } + + .genreTitle { + color: #adadad; + font-size: 16px; + font-style: normal; + font-weight: 600; + line-height: 24px; + } + .genreSub { + text-align: center; + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: normal; + } + + &.active { + border: 1px solid var(--color-grey-200); + .genreTitle { + color: var(--color-white); + } + img.bg { + opacity: 0.8; + } + } + } + } +`; + +export const InputBox = styled.div<{ hasError?: boolean }>` + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + min-width: 320px; + width: 100%; + height: 48px; + border-radius: 12px; + padding: 12px; + background-color: var(--color-darkgrey-dark); + border: 1px solid ${props => (props.hasError ? '#FF9496' : 'none')}; + transition: border-color 0.2s ease; +`; + +export const StyledInput = styled.input<{ hasError?: boolean }>` + flex: 1; + background: none; + border: none; + outline: none; + color: var(--color-white); + font-size: var(--font-size-base); + caret-color: var(--color-neongreen); + &::placeholder { + color: var(--color-grey-300); + font-size: var(--font-size-sm); + font-weight: var(--font-weight-regular); + } +`; + +export const CharCount = styled.div` + width: 26px; + height: 24px; + color: var(--color-white); + font-size: var(--font-size-xs); + font-weight: var(--font-weight-regular); + line-height: 24px; +`; diff --git a/src/pages/mypage/EditPage.tsx b/src/pages/mypage/EditPage.tsx new file mode 100644 index 00000000..cf6900e0 --- /dev/null +++ b/src/pages/mypage/EditPage.tsx @@ -0,0 +1,112 @@ +import { useNavigate } from 'react-router-dom'; +import TitleHeader from '@/components/common/TitleHeader'; +import leftArrow from '@/assets/common/leftArrow.svg'; +import { useEffect, useState } from 'react'; +import { Wrapper, Container, InputBox, StyledInput, CharCount } from './EditPage.styled'; +import type { Genre } from '@/types/genre'; + +const EditPage = () => { + const navigate = useNavigate(); + const onLeftClick = () => { + navigate('/mypage'); + }; + const onRightClick = () => { + // 닉네임 유효성 검사 + if (nickname.trim() === '') { + setNicknameError('변경할 닉네임을 입력해주세요.'); + return; + } + + if (nickname === currentNickname) { + setNicknameError('현재 닉네임과 동일합니다.'); + return; + } + + // 에러가 없으면 서버에 요청 + setNicknameError(''); + console.log('편집완료', { nickname, genreId: selectedId }); + }; + const [nickname, setNickname] = useState(''); + const [currentNickname] = useState('문학하는고래'); // 서버에서 받아온 현재 닉네임 + const [nicknameError, setNicknameError] = useState(''); + const maxLength = 10; + + const handleInputChange = (e: React.ChangeEvent) => { + const inputValue = e.target.value; + const filteredValue = inputValue.replace(/[^ㄱ-ㅎ가-힣a-zA-Z0-9]/g, ''); + setNickname(filteredValue); + + // 입력이 변경되면 에러 상태 초기화 + if (nicknameError) { + setNicknameError(''); + } + }; + const currentGenreId = 'literature'; // 임시 하드코딩 + + const [genres, setGenres] = useState([]); + const [selectedId, setSelectedId] = useState('currentGenreId'); + + useEffect(() => { + fetch('/genres.json') + .then(res => res.json()) + .then((data: Genre[]) => { + setGenres(data); + // 서버에서 받아온 현재 장르 ID로 선택 상태 설정 + setSelectedId(currentGenreId); + }) + .catch(console.error); + }, [currentGenreId]); + + return ( + + } + onLeftClick={onLeftClick} + title="프로필 편집" + onRightClick={onRightClick} + isNextActive={true} + rightButton={완료} + /> + + 닉네임 변경 + + + + {nickname.length}/{maxLength} + + + {nicknameError && {nicknameError}} + + + 칭호편집 + 장르는 칭호와 연결돼요. + + {genres.map(g => ( + setSelectedId(g.id)} + > + + + {g.title} + + {g.subTitle} + + + + ))} + + + + ); +}; + +export default EditPage; diff --git a/src/pages/mypage/Mypage.tsx b/src/pages/mypage/Mypage.tsx new file mode 100644 index 00000000..312df30a --- /dev/null +++ b/src/pages/mypage/Mypage.tsx @@ -0,0 +1,246 @@ +import styled from '@emotion/styled'; +import { useNavigate } from 'react-router-dom'; +import { colors, typography } from '@/styles/global/global'; +import MenuButton from '@/components/Mypage/MenuButton'; +import { usePopupActions } from '@/hooks/usePopupActions'; +import alert from '../../assets/mypage/alert.svg'; +import guide from '../../assets/mypage/guide.svg'; +import save from '../../assets/mypage/save.svg'; +import service from '../../assets/mypage/service.svg'; +import ver from '../../assets/mypage/ver.svg'; +import notice from '../../assets/mypage/notice.svg'; +import terms from '../../assets/mypage/terms.svg'; +import NavBar from '@/components/common/NavBar'; + +const mockProfile = { + profileImgUrl: 'https://placehold.co/54x54', + userName: '문학하는 고래', + userTitle: '문학가', + titleColor: '#a1d5ff', +}; + +const Mypage = () => { + const { profileImgUrl, userName, userTitle, titleColor } = mockProfile; + const { openConfirm, closePopup } = usePopupActions(); + const navigate = useNavigate(); + const onClickEdit = () => {}; + + const handleLogout = () => { + openConfirm({ + title: '로그아웃', + disc: '또 THIP 해주실거죠?', + onConfirm: () => { + //실제 로그아웃 로직 구현 + console.log('로그아웃 실행'); + closePopup(); + //토큰 삭제, 메인 페이지로 이동 + }, + onClose: () => { + console.log('로그아웃 취소'); + closePopup(); + }, + }); + }; + + const handleNotice = () => { + window.open('https://www.naver.com', '_blank'); + }; + + const handleSave = () => { + navigate('/mypage/save'); + }; + + const handleAlert = () => { + navigate('/mypage/alert'); + }; + + const handleWithdraw = () => { + navigate('/mypage/withdraw'); + }; + + return ( + + 마이페이지 + + + + + + {userName} + + {userTitle} + + + + + 편집 + + + + 내 활동 + + + + + + 메뉴 + + + + + + + + + + + + 로그아웃 + + + 회원탈퇴 + + + + + + ); +}; + +const Wrapper = styled.div` + display: flex; + position: relative; + flex-direction: column; + align-items: center; + min-width: 320px; + max-width: 767px; + min-height: 100vh; + margin: 0 auto; + background-color: #121212; +`; + +const Header = styled.div` + background-color: ${colors.black.main}; + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 100; + max-width: 767px; + margin: 0 auto; + padding: 16px 20px; + display: flex; + justify-content: space-between; + + color: ${colors.white}; + font-size: ${typography.fontSize['2xl']}; + font-style: normal; + font-weight: ${typography.fontWeight.bold}; + line-height: 24px; +`; + +const UserProfile = styled.div` + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + width: 100%; + padding: 0 20px; + + .userInfo { + display: flex; + flex-direction: row; + align-items: center; + gap: 4px; + + img { + width: 54px; + height: 54px; + border-radius: 54px; + border: 0.5px solid var(--color-white); + } + + .user { + display: flex; + flex-direction: column; + gap: 4px; + + .username { + color: var(--color-white); + font-size: var(--font-size-xs); + font-weight: var(--font-weight-medium); + line-height: normal; + } + + .usertitle { + color: var(--color-text-humanities_skyblue, #a1d5ff); + font-size: var(--font-size-xs); + font-weight: var(--string-weight-regular, 400); + line-height: normal; + } + } + } + + .edit { + padding: 8px 12px; + border-radius: 20px; + border: 1px solid #888; + + color: var(--color-text-secondary_grey00, #dadada); + font-size: var(--string-size-medium01, 14px); + font-weight: var(--string-weight-medium, 500); + line-height: normal; + cursor: pointer; + } +`; + +const Container = styled.div` + display: flex; + flex-direction: column; + width: 100%; + height: 100vh; + padding-top: 76px; + gap: 40px; +`; + +const Section = styled.div``; + +const SectionTitle = styled.div` + width: 100%; + padding: 0 20px; + padding-bottom: 12px; + + color: ${colors.white}; + font-size: ${typography.fontSize.lg}; + font-weight: ${typography.fontWeight.semibold}; + line-height: 24px; +`; + +const MenuGrid = styled.div` + display: grid; + grid-template-columns: 1fr; + gap: 12px; + padding: 0 20px; + + @media (min-width: 612px) { + grid-template-columns: repeat(2, 1fr); + gap: 16px; + } +`; + +const BottomMenu = styled.div` + display: flex; + flex-direction: column; + align-items: center; + gap: 24px; + margin-top: auto; + padding-bottom: 93px; + + color: ${colors.grey[200]}; + font-size: ${typography.fontSize.sm}; + font-weight: ${typography.fontWeight.regular}; + line-height: 20px; + cursor: pointer; +`; + +export default Mypage; diff --git a/src/pages/mypage/SavePage.tsx b/src/pages/mypage/SavePage.tsx new file mode 100644 index 00000000..ef80ff3c --- /dev/null +++ b/src/pages/mypage/SavePage.tsx @@ -0,0 +1,200 @@ +import { useNavigate } from 'react-router-dom'; +import { useState, useEffect } from 'react'; +import TitleHeader from '../../components/common/TitleHeader'; +import TabBar from '@/components/feed/TabBar'; +import FeedPost from '@/components/feed/FeedPost'; +import leftArrow from '../../assets/common/leftArrow.svg'; +import save from '../../assets/feed/save.svg'; +import activeSave from '../../assets/feed/activeSave.svg'; +import styled from '@emotion/styled'; +import { colors, typography } from '@/styles/global/global'; +import { mockBooks } from '@/data/bookData'; +import { mockPosts } from '@/data/postData'; + +const tabs = ['피드', '책']; + +const SavePage = () => { + const hasPosts = mockPosts.length > 0; + const hasBooks = mockBooks.length > 0; + + const navigate = useNavigate(); + const [activeTab, setActiveTab] = useState(tabs[0]); + const [savedBooks, setSavedBooks] = useState<{ [isbn: number]: boolean }>({}); + + const handleBack = () => { + navigate('/mypage'); + }; + + useEffect(() => { + window.scrollTo(0, 0); + }, [activeTab]); + + const handleSaveToggle = (isbn: number) => { + setSavedBooks(prev => ({ + ...prev, + [isbn]: !prev[isbn], + })); + }; + + return ( + + } + onLeftClick={handleBack} + title="저장" + /> + + {activeTab === '피드' ? ( + <> + {hasPosts ? ( + + {mockPosts.map(post => ( + + ))} + + ) : ( + + 아직 저장한 피드가 없어요 + 마음에 드는 책을 THIP 해보세요! + + )} + > + ) : hasBooks ? ( + <> + + {mockBooks.map(book => ( + + + + + {book.title} + + {book.author} 저 · {book.publisher} + + + + handleSaveToggle(book.isbn)}> + + + + ))} + + > + ) : ( + + 아직 저장한 책이 없어요 + 마음에 드는 책을 THIP 해보세요! + + )} + + ); +}; + +export const Wrapper = styled.div` + display: flex; + position: relative; + flex-direction: column; + align-items: center; + padding-top: 130px; + min-width: 320px; + max-width: 767px; + min-height: 100vh; + margin: 0 auto; + background-color: #121212; +`; + +const FeedContainer = styled.div` + display: flex; + flex-direction: column; + width: 100%; +`; + +const EmptyState = styled.div` + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + width: 100%; + min-width: 320px; + max-width: 540px; + min-height: 100%; + padding: 40px 20px; + margin: 0 auto; + margin-bottom: 20px; + gap: 8px; + flex: 1; + + .title { + color: ${colors.white}; + text-align: center; + font-size: ${typography.fontSize.lg}; + font-weight: ${typography.fontWeight.semibold}; + line-height: 24px; + } + + .sub-title { + color: ${colors.grey[100]}; + text-align: center; + font-size: ${typography.fontSize.sm}; + font-weight: ${typography.fontWeight.regular}; + line-height: normal; + } +`; + +const BookList = styled.div` + display: flex; + flex-direction: column; + min-width: 320px; + max-width: 540px; + padding-top: 32px; + margin: 0 auto; + width: 100%; +`; + +const BookItem = styled.div` + display: flex; + border-bottom: 1px solid ${colors.darkgrey.dark}; + padding: 12px 20px; + justify-content: space-between; +`; + +const Cover = styled.img` + width: 80px; + height: 107px; + object-fit: cover; +`; + +const LeftSection = styled.div` + display: flex; + flex-direction: row; + gap: 12px; +`; + +const BookInfo = styled.div` + display: flex; + flex-direction: column; +`; + +const Title = styled.h3` + font-size: ${typography.fontSize.base}; + font-weight: ${typography.fontWeight.semibold}; + color: ${colors.white}; +`; + +const Subtitle = styled.span` + font-size: ${typography.fontSize.xs}; + font-weight: ${typography.fontWeight.medium}; + color: ${colors.grey[200]}; + margin-top: 8px; +`; + +const SaveIcon = styled.div` + width: 24px; + height: 24px; + cursor: pointer; +`; + +export default SavePage; diff --git a/src/pages/mypage/WithdrawDonePage.tsx b/src/pages/mypage/WithdrawDonePage.tsx new file mode 100644 index 00000000..bcbfe4f1 --- /dev/null +++ b/src/pages/mypage/WithdrawDonePage.tsx @@ -0,0 +1,67 @@ +import TitleHeader from '@/components/common/TitleHeader'; +import styled from '@emotion/styled'; +import { Wrapper } from '../../components/common/Wrapper'; +import leftArrow from '@/assets/common/leftArrow.svg'; +import charactor from '@/assets/mypage/charactor.svg'; +import { useNavigate } from 'react-router-dom'; +import { colors, typography } from '@/styles/global/global'; + +const WithdrawDonePage = () => { + const navigate = useNavigate(); + + const onLeftClick = () => { + navigate('/'); + }; + + return ( + + } onLeftClick={onLeftClick} /> + + + 탈퇴완료 + 다음에 또 만나요! + + + + + ); +}; + +const Container = styled.div` + display: flex; + flex-direction: column; + align-items: center; + gap: 40px; +`; + +const Text = styled.div` + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + width: 100%; + min-width: 360px; + max-width: 540px; + padding: 40px 20px; + margin: 0 auto; + margin-bottom: 56px; + gap: 20px; + + .title { + color: ${colors.white}; + text-align: center; + font-size: ${typography.fontSize.lg}; + font-weight: ${typography.fontWeight.semibold}; + line-height: 24px; + } + + .sub-title { + color: ${colors.grey[100]}; + text-align: center; + font-size: ${typography.fontSize.sm}; + font-weight: ${typography.fontWeight.regular}; + line-height: normal; + } +`; + +export default WithdrawDonePage; diff --git a/src/pages/mypage/WithdrawPage.tsx b/src/pages/mypage/WithdrawPage.tsx new file mode 100644 index 00000000..c1c57169 --- /dev/null +++ b/src/pages/mypage/WithdrawPage.tsx @@ -0,0 +1,180 @@ +import TitleHeader from '@/components/common/TitleHeader'; +import { useNavigate } from 'react-router-dom'; +import { useState } from 'react'; +import { usePopupActions } from '@/hooks/usePopupActions'; +import styled from '@emotion/styled'; +import { colors, typography } from '@/styles/global/global'; +import leftArrow from '../../assets/common/leftArrow.svg'; +import withdraw from '@/assets/mypage/withdraw.svg'; +import check from '@/assets/mypage/check.svg'; + +const WithdrawPage = () => { + const navigate = useNavigate(); + const { openConfirm, closePopup } = usePopupActions(); + const [isChecked, setIsChecked] = useState(false); + + const handleBack = () => { + navigate('/mypage'); + }; + + const handleCheckboxChange = () => { + setIsChecked(!isChecked); + }; + + const handleWithdraw = () => { + if (isChecked) { + openConfirm({ + title: '정말 탈퇴하시겠어요?', + disc: "'예'를 누르면 Thip에서의 모든 기록이 사라져요", + onConfirm: () => { + // 회원탈퇴 API 호출 부분 (비워둠) + console.log('회원탈퇴 API 호출'); + closePopup(); + navigate('/mypage/withdraw/done'); + }, + onClose: () => { + closePopup(); + }, + }); + } + }; + + return ( + + } + onLeftClick={handleBack} + /> + + + 회원탈퇴 주의사항 + + 회원탈퇴 시 계정정보는 복구 불가능하며 90일 이후 재가입이 가능합니다. + + 등록된 기록 및 게시물은 삭제되지 않습니다. + 등록된 기록 및 게시물은 삭제되지 않습니다. + 등록된 기록 및 게시물은 삭제되지 않습니다. + + + + 주의사항을 이해하였으며 이에 동의합니다. + {isChecked && } + + + + + + Thip 떠나기 + + + ); +}; + +const Wrapper = styled.div` + display: flex; + position: relative; + flex-direction: column; + align-items: center; + min-width: 320px; + max-width: 767px; + height: 100vh; + margin: 0 auto; + padding-top: 76px; + background-color: #121212; +`; + +const Container = styled.div` + display: flex; + flex-direction: column; + width: 100%; + min-width: 320px; + max-width: 540px; + gap: 30px; + padding: 40px 0px 105px 0px; +`; + +const Content = styled.div` + display: flex; + flex-direction: column; + gap: 40px; + padding: 16px; + border-radius: 12px; + background-color: ${colors.darkgrey.dark}; +`; + +const ContentTitle = styled.div` + color: ${colors.white}; + font-size: ${typography.fontSize.lg}; + font-weight: ${typography.fontWeight.semibold}; + line-height: 24px; +`; + +const ContentText = styled.div` + color: ${colors.grey[200]}; + font-size: ${typography.fontSize.sm}; + font-weight: ${typography.fontWeight.regular}; + line-height: 20px; +`; + +const CheckSection = styled.div` + display: flex; + flex-direction: column; + gap: 16px; +`; + +const CheckboxContainer = styled.div` + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + gap: 12px; + cursor: pointer; +`; + +const Checkbox = styled.div<{ checked: boolean }>` + width: 30px; + height: 30px; + border: 2px solid ${colors.grey[200]}; + border-radius: 8px; + background-color: transparent; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s ease; +`; + +const CheckLabel = styled.div` + color: ${colors.white}; + font-size: ${typography.fontSize.base}; + font-weight: ${typography.fontWeight.regular}; + line-height: 24px; +`; + +const WithdrawButton = styled.div<{ isActive: boolean }>` + position: fixed; + bottom: 0; + left: 0; + right: 0; + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + margin: 0 auto; + gap: 8px; + width: 100%; + max-width: 767px; + min-width: 320px; + height: 50px; + background-color: ${props => (props.isActive ? colors.purple.main : colors.grey[300])}; + cursor: ${props => (props.isActive ? 'pointer' : 'not-allowed')}; + z-index: 2000; +`; + +const ButtonText = styled.div` + color: ${colors.white}; + font-size: ${typography.fontSize.sm}; + font-weight: ${typography.fontWeight.regular}; + line-height: 24px; +`; +export default WithdrawPage; diff --git a/src/pages/signup/SignupGenre.tsx b/src/pages/signup/SignupGenre.tsx index 3c035b8c..fa9ae6da 100644 --- a/src/pages/signup/SignupGenre.tsx +++ b/src/pages/signup/SignupGenre.tsx @@ -3,14 +3,7 @@ import { useNavigate } from 'react-router-dom'; import { Container } from './Signup.styled'; import leftarrow from '../../assets/common/leftArrow.svg'; import TitleHeader from '../../components/common/TitleHeader'; - -interface Genre { - id: string; - title: string; - subTitle: string; - iconUrl: string; - color: string; -} +import type { Genre } from '@/types/genre'; const SignupGenre = () => { const [genres, setGenres] = useState([]); diff --git a/src/types/book.ts b/src/types/book.ts new file mode 100644 index 00000000..99031060 --- /dev/null +++ b/src/types/book.ts @@ -0,0 +1,7 @@ +export interface Book { + isbn: number; + title: string; + author: string; + coverUrl: string; + publisher: string; +} diff --git a/src/types/genre.ts b/src/types/genre.ts new file mode 100644 index 00000000..527f1dcf --- /dev/null +++ b/src/types/genre.ts @@ -0,0 +1,7 @@ +export interface Genre { + id: string; + title: string; + subTitle: string; + iconUrl: string; + color: string; +}