diff --git a/src/components/Common/Article/Article.tsx b/src/components/Common/Article/Article.tsx index c11df00a..dd907694 100644 --- a/src/components/Common/Article/Article.tsx +++ b/src/components/Common/Article/Article.tsx @@ -123,6 +123,7 @@ const Article = ({ onToggleLike={toggleLikeHandler} /> {article.commentOptionFlag && ( diff --git a/src/components/Common/Article/ArticleAlone/LargerArticle.tsx b/src/components/Common/Article/ArticleAlone/LargerArticle.tsx index 0716eef6..065126be 100644 --- a/src/components/Common/Article/ArticleAlone/LargerArticle.tsx +++ b/src/components/Common/Article/ArticleAlone/LargerArticle.tsx @@ -161,6 +161,7 @@ const LargerArticle = ({ article, isModal = false }: LargerArticleProps) => { isInLargerArticle={true} comments={article.comments} postId={article.postId} + isLiked /> ` .articleMain__content { .article-likeInfo { margin-bottom: 8px; + &.notZero { + cursor: pointer; + } span { font-weight: ${(props) => props.theme.font.bold}; } @@ -58,6 +62,7 @@ interface MainProps { isInLargerArticle?: boolean; comments: PostType.CommentType[]; postId: number; + isLiked: boolean; } const ArticleMain = ({ @@ -73,6 +78,7 @@ const ArticleMain = ({ isInLargerArticle = false, comments, postId, + isLiked, }: MainProps) => { // content state const [isFullText, setIsFullText] = useState( @@ -86,6 +92,7 @@ const ArticleMain = ({ comments.length < 10 ? true : false, ); const [isCommentsFetching, setIsCommentsFetching] = useState(false); + const [isLikedPeopleModalOn, setIsLikedPeopleModalOn] = useState(false); const dispatch = useAppDispatch(); const isTextLineBreak = useMemo(() => content.includes("\n"), [content]); @@ -202,9 +209,27 @@ const ArticleMain = ({ return ( <> + {isLikedPeopleModalOn && ( + setIsLikedPeopleModalOn(true)} + onModalOff={() => setIsLikedPeopleModalOn(false)} + modalInfo={{ + type: "post", + id: postId, + }} + isLiked={isLiked} + /> + )}
{(likeOptionFlag || myUsername === memberUsername) && ( -
+
0 ? "notZero" : "" + }`} + onClick={() => + likesCount > 0 && setIsLikedPeopleModalOn(true) + } + > paragraph.replyParentObj, @@ -209,111 +211,138 @@ const Comment = ({ }; return ( - -
- {commentType !== "recent" && ( -
- - onMouseEnter(event, commentObj.member.username) - } - onMouseLeave={onMouseLeave} - /> -
- )} -
- - - onMouseEnter(event, commentObj.member.username) - } - onMouseLeave={onMouseLeave} - > - {commentObj.member.username} - -   - - + <> + {isLikedPeopleModalOn && ( + setIsLikedPeopleModalOn(true)} + onModalOff={() => setIsLikedPeopleModalOn(false)} + isLiked + /> + )} + +
{commentType !== "recent" && ( - <> -
- - {likesCount > 0 && ( - - )} - - {commentObj.member.username === myUsername && ( // '신고' 기능이 없으면 자기 댓글이 아닌 경우에 모달에 보여줄 메뉴가 없어서 아예 버튼 비활성화 - - )} -
- +
+ + onMouseEnter( + event, + commentObj.member.username, + ) + } + onMouseLeave={onMouseLeave} + /> +
)} -
-
- -
-
- {commentType !== "recent" && commentObj.repliesCount > 0 && ( -
- - {isReplyFetching && } - {isReplyOn && commentObj.replies && ( -
    - {commentObj.replies.map((replyObj) => ( - - ))} -
- )} + {commentType !== "recent" && ( + <> +
+ + {likesCount > 0 && ( + + )} + + {commentObj.member.username === + myUsername && ( // '신고' 기능이 없으면 자기 댓글이 아닌 경우에 모달에 보여줄 메뉴가 없어서 아예 버튼 비활성화 + + )} +
+ + )} +
+
+ +
- )} -
+ {commentType !== "recent" && commentObj.repliesCount > 0 && ( +
+ + {isReplyFetching && } + {isReplyOn && commentObj.replies && ( +
    + {commentObj.replies.map((replyObj) => ( + + ))} +
+ )} +
+ )} + + ); }; diff --git a/src/components/Common/Header/Header.tsx b/src/components/Common/Header/Header.tsx index 5f942a39..9a708892 100644 --- a/src/components/Common/Header/Header.tsx +++ b/src/components/Common/Header/Header.tsx @@ -20,7 +20,7 @@ const HeaderContainer = styled.nav` top: 0; width: 100%; - z-index: 101; + z-index: 98; `; const HeaderContentsWrapper = styled.div` diff --git a/src/components/Common/LikedPeopleModal/LikedPeopleModal.tsx b/src/components/Common/LikedPeopleModal/LikedPeopleModal.tsx new file mode 100644 index 00000000..36c0981e --- /dev/null +++ b/src/components/Common/LikedPeopleModal/LikedPeopleModal.tsx @@ -0,0 +1,220 @@ +import { useAppSelector } from "app/store/Hooks"; +import CloseSVG from "assets/Svgs/CloseSVG"; +import LikedPersonUnit from "components/Common/LikedPeopleModal/LikedPersonUnit"; +import Loading from "components/Common/Loading"; +import { authorizedCustomAxios } from "customAxios"; +import React, { useEffect, useState } from "react"; +import styled from "styled-components"; +import theme from "styles/theme"; +import ModalCard from "styles/UI/ModalCard"; + +export interface LikedPersonType { + following: boolean; + member: { + hasStory: boolean; + id: number; + image: { + imageUrl: string; + }; + name: string; + username: string; + }; +} + +interface PostlikedPeopleDataType { + data: { + content: LikedPersonType[]; + }; +} + +const getLikedPeople = async ( + page: number, + type: "post" | "comment", + id: number, + setLoading: (state: boolean) => void, + setArr: (arr: LikedPersonType[]) => void, + increasePage: () => void, +) => { + const config = { + params: { + page, + size: 10, + }, + }; + setLoading(true); + try { + const { + data: { + data: { content: arr }, + }, + } = await authorizedCustomAxios.get( + `/${type === "post" ? "posts" : "comments"}/${id}/likes`, + config, + ); + setArr(arr); + increasePage(); + } catch (error) { + console.log(error); + } finally { + setLoading(false); + } +}; + +const LIkedPeopleModalHeader = styled.div` + position: relative; + display: flex; + align-items: center; + height: 42px; + border-bottom: 1px solid ${(props) => props.theme.color.bd_gray}; + & > h1 { + width: 100%; + text-align: center; + font-size: 16px; + font-weight: ${(props) => props.theme.font.bold}; + } + & > button { + position: absolute; + right: 0; + width: 50px; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + } +`; + +const LikedPeopleModalMain = styled.div` + height: calc(100% - 42px); + overflow-y: auto; + & > .wrapper { + } +`; + +const getInitialLikedPeople = ( + userInfo: AuthType.UserInfo | null, + isLiked: boolean, +): LikedPersonType[] => + userInfo && isLiked + ? [ + { + following: false, + member: { + username: userInfo.memberUsername, + name: userInfo.memberName, + hasStory: false, + image: { + imageUrl: userInfo.memberImageUrl, + }, + id: -1, + }, + }, + ] + : []; + +interface LikedPeopleModalProps { + onModalOn: () => void; + onModalOff: () => void; + // 어떤 것에 대한 좋아요 정보인지 객체 형태로 전달받습니다 + modalInfo: { + type: "post" | "comment"; + id: number; + }; + isLiked: boolean; +} + +const LikedPeopleModal = ({ + onModalOn, + onModalOff, + modalInfo, + isLiked, +}: LikedPeopleModalProps) => { + const userInfo = useAppSelector((state) => state.auth.userInfo); + const [isLoading, setIsLoading] = useState(true); // axios 요청 중인가? + const [likedPeople, setLikedPeople] = useState( + getInitialLikedPeople(userInfo, isLiked), + ); + const [isModalWidthSmall, setIsModalWidthSmall] = useState( + window.innerWidth <= 735, + ); + const [currentPage, setCurrentPage] = useState(1); + + useEffect(() => { + document.body.style.overflow = "hidden"; + const resizeEventHandler = () => + setIsModalWidthSmall(window.innerWidth <= 735); + + const keydownEventHandler = (event: KeyboardEvent) => { + event.key === "Escape" && onModalOff(); + }; + window.addEventListener("resize", resizeEventHandler); + window.addEventListener("keydown", keydownEventHandler); + + getLikedPeople( + currentPage, + modalInfo.type, + modalInfo.id, + setIsLoading, + (arr) => setLikedPeople((prev) => [...prev, ...arr]), + () => setCurrentPage((prev) => prev + 1), + ); + return () => { + document.body.style.overflow = "unset"; + window.removeEventListener("resize", resizeEventHandler); + window.removeEventListener("keydown", keydownEventHandler); + }; + }, [modalInfo.id, modalInfo.type, onModalOff]); + + return ( + + +

좋아요

+ +
+ +
+ {likedPeople.map((personObj, index) => ( + + getLikedPeople( + currentPage, + modalInfo.type, + modalInfo.id, + setIsLoading, + (arr) => + setLikedPeople((prev) => [ + ...prev, + ...arr, + ]), + () => setCurrentPage((prev) => prev + 1), + ) + } + /> + ))} +
+
+ {isLoading && } +
+ ); +}; + +export default LikedPeopleModal; diff --git a/src/components/Common/LikedPeopleModal/LikedPersonUnit.tsx b/src/components/Common/LikedPeopleModal/LikedPersonUnit.tsx new file mode 100644 index 00000000..9260f63e --- /dev/null +++ b/src/components/Common/LikedPeopleModal/LikedPersonUnit.tsx @@ -0,0 +1,145 @@ +import StoryCircle from "components/Common/StoryCircle"; +import React, { memo, useEffect, useRef, useState } from "react"; +import styled from "styled-components"; +import Button from "styles/UI/Button"; +import { LikedPersonType } from "components/Common/LikedPeopleModal/LikedPeopleModal"; +import theme from "styles/theme"; +import { useAppDispatch, useAppSelector } from "app/store/Hooks"; +import Loading from "components/Common/Loading"; +import { authorizedCustomAxios } from "customAxios"; +import { authAction } from "app/store/ducks/auth/authSlice"; +import { FAIL_TO_REISSUE_MESSAGE } from "utils/constant"; +import useOnView from "hooks/useOnView"; +import { getMiniProfile } from "app/store/ducks/modal/modalThunk"; +import { modalActions } from "app/store/ducks/modal/modalSlice"; +import Username from "components/Common/Username"; + +const StyledLikedPersonUnit = styled.div` + padding: 8px 16px; + display: flex; + align-items: center; + height: 60px; + & > div:first-child { + margin-right: 12px; + } + & > div:nth-child(2) { + flex: 1; + } + & > button { + margin-left: 8px; + } +`; + +const FollowBtn = styled(Button)<{ isSmall: boolean; isFollowing?: boolean }>` + width: ${(props) => (props.isSmall ? 64 : 88)}px; + border: 1px solid + ${(props) => (props.isFollowing ? props.theme.color.bd_gray : "none")}; +`; + +interface LikedPersonUnitProps { + personObj: LikedPersonType; + isSmall: boolean; + isFourthFromLast: boolean; + onView: () => void; +} + +const LikedPersonUnit = ({ + personObj, + isSmall, + isFourthFromLast, + onView, +}: LikedPersonUnitProps) => { + const [isFollowing, setIsFollowing] = useState(personObj.following); + const [isFollowLoading, setIsFollowLoading] = useState(false); + const myUsername = useAppSelector( + (state) => state.auth.userInfo?.memberUsername, + ); + const dispatch = useAppDispatch(); + const unitRef = useRef(null); + const isOnView = useOnView(unitRef); + const followBtnClickHandler = async () => { + try { + setIsFollowLoading(true); + const { + data: { data }, + } = isFollowing + ? await authorizedCustomAxios.delete( + `/${personObj.member.username}/follow`, + ) + : await authorizedCustomAxios.post( + `/${personObj.member.username}/follow`, + ); + setIsFollowing((prev) => !prev); + return data; + } catch (error) { + error === FAIL_TO_REISSUE_MESSAGE && dispatch(authAction.logout()); + } finally { + setIsFollowLoading(false); + } + }; + + useEffect(() => { + isFourthFromLast && isOnView && onView(); + }, [isFourthFromLast, isOnView, onView]); + + const mouseEnterHandler = async ( + event: React.MouseEvent, + ) => { + if (!event) return; + const { top, bottom, left } = + event.currentTarget.getBoundingClientRect(); + await dispatch( + getMiniProfile({ + memberUsername: personObj.member.username, + modalPosition: { top, bottom, left }, + }), + ); + }; + + const mouseLeaveHandler = () => { + dispatch(modalActions.mouseNotOnHoverModal()); + setTimeout(() => dispatch(modalActions.checkMouseOnHoverModal()), 500); + }; + + return ( + + +
+ + {personObj.member.username} + +
{personObj.member.name}
+
+ {personObj.member.username !== myUsername && ( + + {isFollowLoading ? ( + + ) : isFollowing ? ( + "팔로잉" + ) : ( + "팔로우" + )} + + )} +
+ ); +}; + +export default memo(LikedPersonUnit); diff --git a/src/components/Common/LikedPeopleModal/index.js b/src/components/Common/LikedPeopleModal/index.js new file mode 100644 index 00000000..e1cee9bf --- /dev/null +++ b/src/components/Common/LikedPeopleModal/index.js @@ -0,0 +1,3 @@ +import LikedPeopleModal from "components/Common/LikedPeopleModal/LikedPeopleModal"; + +export default LikedPeopleModal; diff --git a/src/hooks/useOnView.ts b/src/hooks/useOnView.ts index e85aef92..bda5d69e 100644 --- a/src/hooks/useOnView.ts +++ b/src/hooks/useOnView.ts @@ -1,6 +1,6 @@ import { RefObject, useEffect, useMemo, useState } from "react"; -const useOnView = (ref: RefObject | null) => { +const useOnView = (ref: RefObject) => { const [isIntersecting, setIntersecting] = useState(false); const observer = useMemo( @@ -19,7 +19,7 @@ const useOnView = (ref: RefObject | null) => { return () => { observer.disconnect(); }; - }, [observer, ref]); + }, [observer, ref.current]); return isIntersecting; }; diff --git a/src/styles/UI/ModalCard/ModalCard.tsx b/src/styles/UI/ModalCard/ModalCard.tsx index 0706fe5b..d93b27b3 100644 --- a/src/styles/UI/ModalCard/ModalCard.tsx +++ b/src/styles/UI/ModalCard/ModalCard.tsx @@ -31,13 +31,14 @@ interface StyledBackDropProps { maxHeight?: number; minWidth?: number; minHeight?: number; + zIndex?: number; } const StyledBackDrop = styled.div` position: fixed; top: 0; left: 0; - z-index: 999; + z-index: ${(props) => props.zIndex || 999}; display: flex; align-items: center; justify-content: center; @@ -94,6 +95,7 @@ interface ModalProps { minWidth?: number; minHeight?: number; isArticle?: boolean; + zIndex?: number; } const ModalCard = ({ @@ -110,6 +112,7 @@ const ModalCard = ({ minHeight, minWidth, isArticle = false, + zIndex, }: ModalProps) => { const modalRef = useRef() as React.MutableRefObject; const isUpperThanHalfPosition = useMemo( @@ -153,6 +156,7 @@ const ModalCard = ({ maxHeight={maxHeight} minWidth={minWidth} minHeight={minHeight} + zIndex={zIndex} > {isWithCancelBtn && }