From 870fead3e13e93c8c11010b355cd311b45c32d45 Mon Sep 17 00:00:00 2001 From: sryung Date: Tue, 9 Jan 2024 18:59:39 +0900 Subject: [PATCH 1/7] =?UTF-8?q?[=F0=9F=A5=81=20:=20feat]=20=ED=94=84?= =?UTF-8?q?=EB=A1=9C=ED=95=84=20=EA=B2=BD=EB=A1=9C=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?=EB=B0=8F=20=EC=A7=84=EC=9E=85=20=EB=A7=81=ED=81=AC=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20(#2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.tsx | 2 +- src/components/home/tweet.tsx | 4 ++-- src/styles/tweet.ts | 6 ++++-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 787ff54..d84d90f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -25,7 +25,7 @@ const router = createBrowserRouter([ element: , }, { - path: '/profile', + path: '/:user', element: , }, { diff --git a/src/components/home/tweet.tsx b/src/components/home/tweet.tsx index abafff7..b7d11f2 100644 --- a/src/components/home/tweet.tsx +++ b/src/components/home/tweet.tsx @@ -57,7 +57,7 @@ export default function Tweet({ }, [userId]); return ( - + {userAvatar ? ( - {userName} + {userName} {FormatDate(createdAt)} {tweet} diff --git a/src/styles/tweet.ts b/src/styles/tweet.ts index d7491ae..b20b66f 100644 --- a/src/styles/tweet.ts +++ b/src/styles/tweet.ts @@ -1,5 +1,6 @@ import { styled } from 'styled-components'; import { grayColor, primaryColor } from '@style/global.ts'; +import { Link } from 'react-router-dom'; export const Wrapper = styled.li` position: relative; @@ -13,7 +14,7 @@ export const Wrapper = styled.li` export const Row = styled.div``; -export const Avatar = styled.div` +export const Avatar = styled(Link)` position: absolute; top: 20px; left: 10px; @@ -50,9 +51,10 @@ export const AvatarImage = styled.img` z-index: 10; `; -export const Username = styled.span` +export const Username = styled(Link)` font-weight: 600; font-size: 15px; + text-decoration: none; `; export const Date = styled.span` From 9fec024980cbcef07d20c1d40b20d9cb5665fa79 Mon Sep 17 00:00:00 2001 From: sryung Date: Wed, 10 Jan 2024 01:14:57 +0900 Subject: [PATCH 2/7] =?UTF-8?q?[=F0=9F=A5=81=20:=20feat]=20=ED=94=84?= =?UTF-8?q?=EB=A1=9C=ED=95=84=20=EA=B2=BD=EB=A1=9C=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?=EB=B0=8F=20=EB=84=A4=EB=B9=84=EA=B2=8C=EC=9D=B4=EC=85=98=20?= =?UTF-8?q?=EC=8A=A4=ED=83=80=EC=9D=BC=20=ED=99=9C=EC=84=B1=ED=99=94=20?= =?UTF-8?q?=EC=A1=B0=EA=B1=B4=20=EC=88=98=EC=A0=95=20(#2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - search와 동일한 방식인 쿼리문으로 최종 경로 수정 - Navigation의 $isActive 조건에 사용된 location.pathname은 쿼리 문자열을 포함하지 않기 때문에 location.search으로 대체해 쿼리 문자열을 비교하도록 함 --- src/App.tsx | 2 +- src/components/home/tweet.tsx | 4 ++-- src/components/left-side-menu/navigation.tsx | 9 +++++++-- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index d84d90f..df2130b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -25,7 +25,7 @@ const router = createBrowserRouter([ element: , }, { - path: '/:user', + path: '/user', element: , }, { diff --git a/src/components/home/tweet.tsx b/src/components/home/tweet.tsx index b7d11f2..30e0b55 100644 --- a/src/components/home/tweet.tsx +++ b/src/components/home/tweet.tsx @@ -57,7 +57,7 @@ export default function Tweet({ }, [userId]); return ( - + {userAvatar ? ( - {userName} + {userName} {FormatDate(createdAt)} {tweet} diff --git a/src/components/left-side-menu/navigation.tsx b/src/components/left-side-menu/navigation.tsx index 6408609..ea3cc12 100644 --- a/src/components/left-side-menu/navigation.tsx +++ b/src/components/left-side-menu/navigation.tsx @@ -1,4 +1,6 @@ import { Link, useLocation } from 'react-router-dom'; +import { useRecoilValue } from 'recoil'; +import currentUserAtom from '@atom/current-user.tsx'; import * as S from '@style/navigation.ts'; import ImageComputer from '@img/logo-small.png'; import { ReactComponent as IconUser } from '@img/i-user.svg'; @@ -6,6 +8,7 @@ import { ReactComponent as IconHome } from '@img/i-home.svg'; export default function Navigation() { const location = useLocation(); + const currentUser = useRecoilValue(currentUserAtom); return ( <> @@ -20,8 +23,10 @@ export default function Navigation() { 홈 - - + + 프로필 From 471ce4ab95e4cda30dfcb0059af597cffc2d718a Mon Sep 17 00:00:00 2001 From: sryung Date: Wed, 10 Jan 2024 01:17:59 +0900 Subject: [PATCH 3/7] =?UTF-8?q?[=F0=9F=A5=81=20:=20feat]=20=EB=82=B4=20?= =?UTF-8?q?=ED=94=84=EB=A1=9C=ED=95=84=20->=20=EB=AA=A8=EB=93=A0=20?= =?UTF-8?q?=EC=9C=A0=EC=A0=80=20=ED=94=84=EB=A1=9C=ED=95=84=EB=A1=9C=20?= =?UTF-8?q?=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EC=88=98=EC=A0=95=20(#2?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Profile을 currentUser를 이용해 현재 로그인된 유저의 정보를 보여주는 용도의 페이지에서 확장. Timeline에서 props로 내려주는 유저 데이터를 기반으로 모든 유저의 프로필 페이지로 기능을 수정함 - '프로필 수정' 버튼은 currentUser와 props user를 비교하여 일치하는 경우에만 렌더링 --- src/components/profile/user-profile.tsx | 41 +++++++++++++++--------- src/components/profile/user-timeline.tsx | 14 ++++---- src/routes/profile.tsx | 32 ++++++++++++++++-- 3 files changed, 63 insertions(+), 24 deletions(-) diff --git a/src/components/profile/user-profile.tsx b/src/components/profile/user-profile.tsx index 0315224..5b41fd4 100644 --- a/src/components/profile/user-profile.tsx +++ b/src/components/profile/user-profile.tsx @@ -1,12 +1,17 @@ import { useState } from 'react'; import { useRecoilValue } from 'recoil'; import currentUserAtom from '@atom/current-user.tsx'; +import IUser from '@type/IUser.ts'; import EditProfileForm from '@compo/profile/edit-profile-form.tsx'; import * as S from '@style/profile.ts'; import * as P from '@style/popup.ts'; import { ReactComponent as IconUser } from '@img/i-user.svg'; -export default function UserProfile() { +interface IUserProfile { + user: IUser; +} + +export default function UserProfile({ user }: IUserProfile) { const [editPopup, setEditPopup] = useState(false); const currentUser = useRecoilValue(currentUserAtom); const toggleEditPopup = () => { @@ -15,10 +20,10 @@ export default function UserProfile() { return ( - {currentUser.userAvatar ? ( + {user.userAvatar ? ( @@ -26,18 +31,22 @@ export default function UserProfile() { )} - {currentUser.userName} - - 프로필 수정 - - {editPopup ? ( - - - - - - - ) : null} + {user.userName} + {user.userId === currentUser.userId && ( + <> + + 프로필 수정 + + {editPopup && ( + + + + + + + )} + + )} ); } diff --git a/src/components/profile/user-timeline.tsx b/src/components/profile/user-timeline.tsx index 81b1a5d..8274a19 100644 --- a/src/components/profile/user-timeline.tsx +++ b/src/components/profile/user-timeline.tsx @@ -1,5 +1,4 @@ import { useEffect, useState } from 'react'; -import { useRecoilValue } from 'recoil'; import { Unsubscribe } from 'firebase/auth'; import { collection, @@ -9,21 +8,24 @@ import { query, where, } from 'firebase/firestore'; -import currentUserAtom from '@atom/current-user.tsx'; import { db } from '@/firebase.ts'; +import IUser from '@type/IUser.ts'; import ITweet from '@type/ITweet.ts'; import Tweet from '@compo/home/tweet.tsx'; import * as S from '@style/timeline.ts'; -export default function UserTimeline() { +interface IUserTimeline { + user: IUser; +} + +export default function UserTimeline({ user }: IUserTimeline) { const [tweets, setTweets] = useState([]); - const currentUser = useRecoilValue(currentUserAtom); useEffect(() => { let unsubscribe: Unsubscribe | null = null; const fetchTweets = async () => { const tweetsQuery = query( collection(db, 'tweets'), - where('userId', '==', currentUser.userId), + where('userId', '==', user.userId), orderBy('createdAt', 'desc'), limit(25), ); @@ -48,7 +50,7 @@ export default function UserTimeline() { unsubscribe(); } }; - }, [currentUser.userId]); + }, [user]); return tweets.length !== 0 ? ( {tweets.map((tweet) => ( diff --git a/src/routes/profile.tsx b/src/routes/profile.tsx index b82e147..0372416 100644 --- a/src/routes/profile.tsx +++ b/src/routes/profile.tsx @@ -1,14 +1,42 @@ +import { useEffect, useState } from 'react'; +import { useLocation } from 'react-router-dom'; import WindowTop from '@compo/window-top.tsx'; import UserProfile from '@compo/profile/user-profile.tsx'; import UserTimeline from '@compo/profile/user-timeline.tsx'; import * as W from '@style/window.ts'; +import { collection, getDocs, query, where } from 'firebase/firestore'; +import { db } from '@/firebase.ts'; +import IUser from '@type/IUser.ts'; export default function Profile() { + const location = useLocation(); + const userFromLocation = location.search.slice(7); + const [user, setUser] = useState(); + useEffect(() => { + const fetchUser = async () => { + const usersQuery = query( + collection(db, 'users'), + where('userId', '==', userFromLocation), + ); + const snapshot = await getDocs(usersQuery); + if (!snapshot.empty) { + const userData = snapshot.docs[0].data() as IUser; + setUser(userData); + } else { + console.error("can't find user data"); + } + }; + fetchUser(); + }, [userFromLocation]); return ( - - + {user && ( + <> + + + + )} ); } From 0ee2623f50376a8d01bfc01d6d5d95bc22322593 Mon Sep 17 00:00:00 2001 From: sryung Date: Thu, 11 Jan 2024 03:27:01 +0900 Subject: [PATCH 4/7] =?UTF-8?q?[=F0=9F=9B=A0=20:=20fix]=20=20auth.signOut(?= =?UTF-8?q?)=20=EC=99=84=EB=A3=8C=20=EC=9D=B4=ED=9B=84=EC=97=90=20?= =?UTF-8?q?=EC=83=81=ED=83=9C=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8=20?= =?UTF-8?q?=EB=B0=8F=20=EB=A6=AC=EB=94=94=EB=A0=89=EC=85=98=20=EC=88=98?= =?UTF-8?q?=ED=96=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 코드 리팩토링 이전에 서비스에 가입한 유저에게 버그가 일어날 것을 대비해 로그아웃(auth.signOut())과 상태를 비워주는 로직을 추가하는 작업을 5296803 에서 진행했는데, 로그아웃이 완료되기 이전에 MiniProfile에서 setCurrenUser가 호출되어(상태를 업데이트하는 부분) 이 부분이 문제됨. - Warning: Cannot update a component (`Navigation`) while rendering a different component (`ProtectedRoute`). To locate the bad setState() call inside `ProtectedRoute` - Warning: Maximum update depth exceeded. This can happen when a component calls setState inside useEffect, but useEffect either doesn't have a dependency array, or one of the dependencies changes on every render. - 비동기 함수인 auth.signOut()가 우선 진행되고나서 상태를 변경 및 리디렉션을 하도록 하면 문제가 해결됨. MiniProfile에서는 비동기로 동작하고 있기 때문에 ProtectedRoute에서 누락된 비동기 동작(then)을 추가하여 해결 --- src/components/protected-route.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/components/protected-route.tsx b/src/components/protected-route.tsx index bbe1d9a..c37eeac 100644 --- a/src/components/protected-route.tsx +++ b/src/components/protected-route.tsx @@ -13,9 +13,10 @@ export default function ProtectedRoute({ const [localStorageUser, setLocalStorageUser] = useRecoilState(currentUserAtom); if (!user || localStorageUser.userId === '') { - auth.signOut(); - setLocalStorageUser({ userId: '', userName: '', userAvatar: '' }); - return ; + auth.signOut().then(() => { + setLocalStorageUser({ userId: '', userName: '', userAvatar: '' }); + ; + }); } return children; } From e33e4bdc7f7d8dd3e750c64939b8f50a2dab6bb3 Mon Sep 17 00:00:00 2001 From: sryung Date: Thu, 11 Jan 2024 03:50:11 +0900 Subject: [PATCH 5/7] =?UTF-8?q?[=F0=9F=9B=A0=20:=20fix]=20=ED=94=84?= =?UTF-8?q?=EB=A1=9C=ED=95=84=20=EC=88=98=EC=A0=95=EC=8B=9C=20=EB=A6=AC?= =?UTF-8?q?=EB=A0=8C=EB=8D=94=EB=A7=81=20=EC=A0=81=EC=9A=A9=20(#2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - useEffect의 dependency로 유저 데이터를 넣어 상태 변화와 동시에 리렌더링할 수 있도록 함 - 이전에 리렌더링 적용했었으나 프로필 확장 작업 중 누락된 것으로 추정 --- src/routes/profile.tsx | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/routes/profile.tsx b/src/routes/profile.tsx index 0372416..122d96f 100644 --- a/src/routes/profile.tsx +++ b/src/routes/profile.tsx @@ -1,17 +1,20 @@ import { useEffect, useState } from 'react'; import { useLocation } from 'react-router-dom'; +import { useRecoilValue } from 'recoil'; +import { collection, getDocs, query, where } from 'firebase/firestore'; +import { db } from '@/firebase.ts'; import WindowTop from '@compo/window-top.tsx'; import UserProfile from '@compo/profile/user-profile.tsx'; import UserTimeline from '@compo/profile/user-timeline.tsx'; -import * as W from '@style/window.ts'; -import { collection, getDocs, query, where } from 'firebase/firestore'; -import { db } from '@/firebase.ts'; +import currentUserAtom from '@atom/current-user.tsx'; import IUser from '@type/IUser.ts'; +import * as W from '@style/window.ts'; export default function Profile() { const location = useLocation(); const userFromLocation = location.search.slice(7); const [user, setUser] = useState(); + const currentUser = useRecoilValue(currentUserAtom); useEffect(() => { const fetchUser = async () => { const usersQuery = query( @@ -27,7 +30,7 @@ export default function Profile() { } }; fetchUser(); - }, [userFromLocation]); + }, [userFromLocation, currentUser]); return ( From c438e202f1c2e6fe88ccd532063ae553b738b6f5 Mon Sep 17 00:00:00 2001 From: sryung Date: Thu, 11 Jan 2024 04:00:25 +0900 Subject: [PATCH 6/7] =?UTF-8?q?[=F0=9F=A5=81=20:=20feat]=20=ED=9A=8C?= =?UTF-8?q?=EC=9B=90=20=ED=83=88=ED=87=B4=20(#21)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 프로필 수정 버튼과 동일하게 로그인 유저에게만 회원 탈퇴 버튼 노출 - 회원 탈퇴 시 auth, firestore의 users컬렉션 문서, storage의 avatar 이미지 데이터 일괄 삭제 진행 후 /auth로 이동 --- src/components/profile/user-profile.tsx | 58 +++++++++++++++++++++++-- src/styles/popup.ts | 3 ++ src/styles/profile.ts | 15 +++++-- 3 files changed, 69 insertions(+), 7 deletions(-) diff --git a/src/components/profile/user-profile.tsx b/src/components/profile/user-profile.tsx index 5b41fd4..809d60c 100644 --- a/src/components/profile/user-profile.tsx +++ b/src/components/profile/user-profile.tsx @@ -1,5 +1,10 @@ import { useState } from 'react'; -import { useRecoilValue } from 'recoil'; +import { useNavigate } from 'react-router-dom'; +import { useRecoilState } from 'recoil'; +import { deleteUser } from 'firebase/auth'; +import { deleteDoc, doc } from 'firebase/firestore'; +import { deleteObject, ref } from 'firebase/storage'; +import { auth, db, storage } from '@/firebase.ts'; import currentUserAtom from '@atom/current-user.tsx'; import IUser from '@type/IUser.ts'; import EditProfileForm from '@compo/profile/edit-profile-form.tsx'; @@ -12,11 +17,35 @@ interface IUserProfile { } export default function UserProfile({ user }: IUserProfile) { + const navigate = useNavigate(); + const [currentUser, setCurrentUser] = useRecoilState(currentUserAtom); const [editPopup, setEditPopup] = useState(false); - const currentUser = useRecoilValue(currentUserAtom); + const [withdrawPopup, setWithdrawPopup] = useState(false); const toggleEditPopup = () => { setEditPopup(!editPopup); }; + const toggleWithdrawPopup = () => { + setWithdrawPopup(!withdrawPopup); + }; + const withdrawAccount = async () => { + const authCurrentUser = auth.currentUser; + if (!authCurrentUser) return; + try { + await deleteDoc(doc(db, 'users', authCurrentUser.uid)); + const photoRef = ref(storage, `avatars/${authCurrentUser.uid}`); + await deleteObject(photoRef); + await deleteUser(authCurrentUser); + } catch (error) { + console.log(error); + } finally { + setCurrentUser({ + userId: '', + userAvatar: '', + userName: '', + }); + navigate('/auth'); + } + }; return ( @@ -33,7 +62,7 @@ export default function UserProfile({ user }: IUserProfile) { {user.userName} {user.userId === currentUser.userId && ( - <> + 프로필 수정 @@ -45,7 +74,28 @@ export default function UserProfile({ user }: IUserProfile) { )} - + + 회원 탈퇴 + + {withdrawPopup && ( + + + + + 정말 탈퇴하시겠습니까? + + + + 예 + + + 아니요 + + + + + )} + )} ); diff --git a/src/styles/popup.ts b/src/styles/popup.ts index 9a07bd0..74cc83b 100644 --- a/src/styles/popup.ts +++ b/src/styles/popup.ts @@ -97,6 +97,9 @@ export const Text = styled.p` font-size: 18px; line-height: 30px; text-align: center; + span { + color: ${primaryColor}; + } `; export const ButtonWrapper = styled.div` diff --git a/src/styles/profile.ts b/src/styles/profile.ts index 6bc6b4e..3316de1 100644 --- a/src/styles/profile.ts +++ b/src/styles/profile.ts @@ -1,6 +1,6 @@ import { styled } from 'styled-components'; import { grayColor } from '@style/global.ts'; -import { LineButton } from '@style/button.ts'; +import { LineButton, SolidButton } from '@style/button.ts'; export const Profile = styled.article` position: relative; @@ -60,7 +60,16 @@ export const Name = styled.h2` font-size: 30px; `; -export const EditButton = styled(LineButton)` +export const Buttons = styled.div` + display: inline-block; +`; + +export const EditButton = styled(SolidButton)` + width: auto; + margin: 0 5px; +`; + +export const WithdrawButton = styled(LineButton)` width: auto; - margin: 0; + margin: 0 5px; `; From 354b1a7a963c01ea7d5bf2083955ad57f6b831a0 Mon Sep 17 00:00:00 2001 From: sryung Date: Thu, 11 Jan 2024 04:14:16 +0900 Subject: [PATCH 7/7] =?UTF-8?q?[=F0=9F=9B=A0=20:=20fix]=20=EB=88=84?= =?UTF-8?q?=EB=9D=BD=20return=EB=AC=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 0ee2623에서 Navigate return을 누락함으로써 protected 되지 않고 / 경로에 모두가 접근 가능한 문제가 발생하여 수정 --- src/components/protected-route.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/protected-route.tsx b/src/components/protected-route.tsx index c37eeac..2f1d9b8 100644 --- a/src/components/protected-route.tsx +++ b/src/components/protected-route.tsx @@ -15,8 +15,8 @@ export default function ProtectedRoute({ if (!user || localStorageUser.userId === '') { auth.signOut().then(() => { setLocalStorageUser({ userId: '', userName: '', userAvatar: '' }); - ; }); + return ; } return children; }