- {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 && (
-
-
- )}
-
+ {commentType !== "recent" && commentObj.repliesCount > 0 && (
+
+
+
+
+ {isLastReply && isReplyOn
+ ? "답글 숨기기"
+ : `답글 보기(${
+ !isReplyOn
+ ? commentObj.repliesCount
+ : commentObj.repliesCount -
+ (commentObj.replies?.length ||
+ 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 && }