👀 깃허브 링크
😎 클론 사이트 바로가기
이슈가 있었던 부분은 제목 옆에 ❗를 붙였습니다.
- 카테고리 세분화 및 '프로필 수정', '북마크' 탭은 본인 프로필에서만 보일 수 있게 함
- '프로필 수정' 클릭 시 모달창이 활성화 되어 배경·프로필 이미지, 닉네임·자기소개 추가/변경/삭제 가능
- 가입일과 팔로잉, 팔로워 숫자 확인
- Right Bar 팔로우 추천에서의 팔로우를 다른 유저의 프로필 탭에서도 가능
- 프로필 탭에서도 로그아웃 가능
- 리트윗, 좋아요, 북마크 탭을 각 트윗과 답글로 더 구분 지었습니다.
<Route path={}>
에 경로를 배열로 지정하여 여러 경로에서 같은 컴포넌트를 보일 수 있게 했습니다.
니다.
// creatorInfo = URL에서 이메일 추출한 유저 정보
const userEmail = pathname.split("/")[3];
useEffect(() => {
const paths = {
mynweets: 1,
replies: 2,
renweets: 3,
like: 4,
bookmark: 5,
};
const selectedValue = paths[pathname.split("/")[2]];
setSelected(selectedValue);
}, [pathname, userObj.email]);
return (
// ... 생략
<SelectMenuBtn
num={3}
selected={selected}
url={"/profile/renweets/" + userEmail}
text={"리트윗"}
/>
<Switch>
// ... 생략
<Route
path={[
"/profile/renweets/" + userEmail,
"/profile/renweetsreplies/" + userEmail,
]}
>
<ProfileReNweetBox
userObj={userObj}
creatorInfo={creatorInfo}
/>
</Route>
</Switch>
)
const ProfileReNweetBox = ({ userObj, creatorInfo }) => {
const location = useLocation();
const [selected, setSelected] = useState(1);
useEffect(() => {
if (location.pathname.includes("/renweets/")) {
setSelected(1);
} else if (location.pathname.includes("/renweetsreplies/")) {
setSelected(2);
}
}, [location.pathname]);
return (
<>
<div className={styled.container}>
<div className={styled.main__container}>
<nav className={styled.categoryList}>
<SelectMenuBtn
num={1}
selected={selected}
url={
location.pathname.includes("/user/")
? "/user/renweets/" + creatorInfo.email
: "/profile/renweets/" + creatorInfo.email
}
text={"트윗"}
/>
<SelectMenuBtn
num={2}
selected={selected}
url={
location.pathname.includes("/user/")
? "/user/renweetsreplies/" + creatorInfo.email
: "/profile/renweetsreplies/" + creatorInfo.email
}
text={"답글"}
/>
</nav>
</div>
<Switch>
<Route
path={
location.pathname.includes("/user/")
? "/user/renweets/" + creatorInfo.email
: "/profile/renweets/" + creatorInfo.email
}
>
<ProfileReNweets userObj={userObj} creatorInfo={creatorInfo} />
</Route>
<Route
path={
location.pathname.includes("/user/")
? "/user/renweetsreplies/" + creatorInfo.email
: "/profile/renweetsreplies/" + creatorInfo.email
}
>
<ProfileReNweetsReplies
userObj={userObj}
creatorInfo={creatorInfo}
/>
</Route>
</Switch>
</div>
</>
);
};
export default ProfileReNweetBox;
- 모달창 활성 시 각 영역들을 추가·변경·삭제 할 수 있습니다.
- 변경사항 없을 시 프로필 수정 버튼 비활성화
- 파일의 용량이 클 때 업로드가 안 되기 때문에 이미지 크기와 사이즈를 압축(조정)해줄 수 있는
browser-image-compression
라이브러리를 사용했습니다.
// UpdateProfileModal.js 컴포넌트 內
const dispatch = useDispatch();
const currentUser = useSelector((state) => state.user.currentUser);
const [newDisplayName, setNewDisplayName] = useState(creatorInfo.displayName);
const [desc, setDesc] = useState(creatorInfo.description);
const [editAttachment, setEditAttachment] = useState(creatorInfo.photoURL);
const [editAttachmentBg, setEditAttachmentBg] = useState(creatorInfo.bgURL);
const [isDeleteProfileURL, setIsDeleteProfileURL] = useState(false);
const [isDeleteBgURL, setIsDeleteBgURL] = useState(false);
const [isAddFile, setIsAddFile] = useState(null);
const onChangeInfo = (e, type) => {
if (type === "displayName") {
setNewDisplayName(e.target.value);
} else if (type === "description") {
setDesc(e.target.value);
}
};
// 이미지 압축
const compressImage = async (image) => {
try {
const options = {
maxSizeMb: 1,
maxWidthOrHeight: 600,
};
return await imageCompression(image, options);
} catch (e) {
console.log(e);
}
};
// 이미지 URL로 바꾸는 로직 - hook 예정(useFileChange)
const onFileChange = async (e) => {
setIsAddFile(true);
const theFile = e.target.files[0]; // 파일 1개만 첨부
const compressedImage = await compressImage(theFile); // 이미지 압축
const reader = new FileReader(); // 파일 이름 읽기
reader.onloadend = (finishedEvent) => {
setEditAttachment(finishedEvent.currentTarget.result);
};
/* 파일 선택 누르고 이미지 한 개 선택 뒤 다시 파일선택 누르고 취소 누르면
Failed to execute 'readAsDataURL' on 'FileReader': parameter 1 is not of type 'Blob'. 이런 오류가 나옴.
-> if문으로 예외 처리 */
if (theFile) {
reader.readAsDataURL(compressedImage);
}
};
const onFileBgChange = async (e) => {
setIsAddFile(true);
const theFile = e.target.files[0]; // 파일 1개만 첨부
const compressedImage = await compressImage(theFile); // 이미지 압축
const reader = new FileReader(); // 파일 이름 읽기
reader.onloadend = (finishedEvent) => {
setEditAttachmentBg(finishedEvent.currentTarget.result);
};
/* 파일 선택 누르고 이미지 한 개 선택 뒤 다시 파일선택 누르고 취소 누르면
Failed to execute 'readAsDataURL' on 'FileReader': parameter 1 is not of type 'Blob'. 이런 오류가 나옴.
-> if문으로 예외 처리 */
if (theFile) {
reader.readAsDataURL(compressedImage);
}
};
const onDeleteProfileClick = async () => {
const ok = window.confirm("프로필 사진을 삭제하시겠어요?");
// 이미지 없는 글 삭제 시 렌더링 하려는 이미지 값을 찾는 에러가 떠서 예외 처리
if (ok) {
setIsDeleteProfileURL(!isDeleteProfileURL);
setEditAttachment(noneProfile);
setIsAddFile(false);
}
};
const onDeleteBgClick = async () => {
const ok = window.confirm("배경사진을 삭제하시겠어요?");
// 이미지 없는 글 삭제 시 렌더링 하려는 이미지 값을 찾는 에러가 떠서 예외 처리
if (ok) {
setIsDeleteBgURL(!isDeleteBgURL);
setEditAttachmentBg(bgImg);
setIsAddFile(false);
}
};
// 업데이트 버튼
const onSubmit = async (e) => {
e.preventDefault();
await updateDoc(doc(dbService, "users", creatorInfo.email), {
displayName: newDisplayName, // 바뀐 이름 업데이트
photoURL: editAttachment,
bgURL: editAttachmentBg,
description: desc,
});
dispatch(
setCurrentUser({
displayName: newDisplayName, // 바뀐 이름 디스패치
photoURL: editAttachment,
bgURL: editAttachmentBg,
description: desc,
...currentUser,
})
);
// 프로필 이미지 삭제 버튼 클릭 시
if (isDeleteProfileURL) {
await updateDoc(doc(dbService, "users", creatorInfo.email), {
photoURL: noneProfile,
});
dispatch(
setCurrentUser({
photoURL: noneProfile,
displayName: newDisplayName,
description: desc,
...currentUser,
})
);
}
// 배경 이미지 삭제 버튼 클릭 시
if (isDeleteBgURL) {
await updateDoc(doc(dbService, "users", creatorInfo.email), {
bgURL: bgImg,
});
dispatch(
setCurrentUser({
bgURL: bgImg,
displayName: newDisplayName,
description: desc,
...currentUser,
})
);
}
alert(`프로필이 수정되었습니다.`);
toggleEdit(false);
};
- 가입일, 팔로우 관련 숫자는
Firebase
필드의 값을 불러왔습니다.
- 유저 팔로우는
Right Bar
의팔로우 추천 영역
이 아닌다른 유저의 프로필
에서도 팔로우 할 수 있게 했습니다.
// creatorInfo = URL에서 이메일 추출한 유저 정보
const userEmail = pathname.split("/")[3];// URL 중 이메일 부분에 해당
// 본인 정보 가져오기
useEffect(() => {
const unsubscribe = onSnapshot(
doc(dbService, "users", currentUser.email),
(doc) => {
setMyInfo(doc.data());
setFbLoading((prev) => ({ ...prev, myInfo: true }));
}
);
return () => {
unsubscribe();
};
}, [currentUser.email]);
return (
<div className={styled.profile__createdAt}>
<BsCalendar3 />
<p>가입일 : {timeToString3(creatorInfo.createdAtId)}</p>
</div>
<div className={styled.profile__followInfo}>
<p>
<b>{creatorInfo.following?.length}</b> 팔로잉
</p>
<p>
<b>{creatorInfo.follower?.length}</b> 팔로워
</p>
</div>
)
const timeToString3 = (timestamp) => {
let date = new Date(timestamp);
let str =
date.getFullYear() +
"년 " +
(date.getMonth() + 1) +
"월 " +
date.getDate() +
"일 ";
return str;
};
- 프로필 업데이트 하는 로직 중 값을
Redux
의store
에dispatch
하는 코드가 있는데 새로고침하면 정보가 사라져서 렌더링이 되지 않는 현상이 있었습니다.
- 새로고침을 해도 정보를 남아있게 해주는
react-persist
를 사용해서 해결했습니다.
import { composeWithDevTools } from "redux-devtools-extension";
import { persistStore, persistReducer } from "redux-persist";
import { createStore } from "redux";
import rootReducer from "../reducer";
// localStorage에 저장하고 싶으면
import storage from "redux-persist/lib/storage";
// session Storage에 저장하고 싶으면
// import storageSession from "redux-persist/lib/storage/session";
//persist 설정
const persistConfig = {
key: "root",
storage,
// whitelist: ["특정 리듀서"] // 특정한 reducer만 localStorage에 저장하고 싶을 경우
};
// persistedReducer 생성
const persistedReducer = persistReducer(persistConfig, rootReducer);
// store, persistor를 리턴하는 함수
const configureStore = () => {
const store = createStore(persistedReducer, composeWithDevTools());
const persistor = persistStore(store);
return { store, persistor };
};
export default configureStore;
const { store, persistor } = configureStore();
root.render(
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
<App />
</PersistGate>
</Provider>
);