diff --git a/src/@components/@common/Toast/ToastProvider.tsx b/src/@components/@common/Toast/ToastProvider.tsx index 395fdeb7..84dbd08f 100644 --- a/src/@components/@common/Toast/ToastProvider.tsx +++ b/src/@components/@common/Toast/ToastProvider.tsx @@ -7,6 +7,7 @@ import * as St from "./style"; export type ToastType = { message: string; duration: number; + handleClickCancel?: () => void; }; export default function ToastProvider({ children }: PropsWithChildren) { @@ -14,8 +15,8 @@ export default function ToastProvider({ children }: PropsWithChildren) { const toastTimeout = useTimeout(); const showToast = useCallback( - async ({ message, duration }: ToastType) => { - setToast({ message, duration }); + async ({ message, duration, handleClickCancel }: ToastType) => { + setToast({ message, duration, handleClickCancel }); toastTimeout.set(() => { setToast(null); @@ -24,12 +25,17 @@ export default function ToastProvider({ children }: PropsWithChildren) { [setToast, toastTimeout], ); + const blackoutToast = useCallback(() => setToast(null), [setToast]); + return ( - + {children} {toast && ( - {toast.message} + + {toast.message} + {toast.handleClickCancel && 취소} + )} diff --git a/src/@components/@common/Toast/context.ts b/src/@components/@common/Toast/context.ts index c2bc5ffc..c5f450a9 100644 --- a/src/@components/@common/Toast/context.ts +++ b/src/@components/@common/Toast/context.ts @@ -4,10 +4,14 @@ import { ToastType } from "./ToastProvider"; interface ToastController { showToast: (toast: ToastType) => void; + blackoutToast: () => void; } export const ToastContext = React.createContext({ showToast: () => { throw new Error("Function not implemented."); }, + blackoutToast: () => { + throw new Error("Function not implemented."); + }, }); diff --git a/src/@components/@common/Toast/hooks/useTimeout.ts b/src/@components/@common/Toast/hooks/useTimeout.ts index a6978c86..419bfaee 100644 --- a/src/@components/@common/Toast/hooks/useTimeout.ts +++ b/src/@components/@common/Toast/hooks/useTimeout.ts @@ -1,7 +1,5 @@ import { useRef } from "react"; -import { ToastType } from "../ToastProvider"; - export default function useTimeout() { const timerRef = useRef | null>(null); diff --git a/src/@components/@common/Toast/hooks/useToast.ts b/src/@components/@common/Toast/hooks/useToast.ts index 0538c141..48e686e2 100644 --- a/src/@components/@common/Toast/hooks/useToast.ts +++ b/src/@components/@common/Toast/hooks/useToast.ts @@ -4,7 +4,7 @@ import { ToastContext } from "../context"; import { ToastType } from "../ToastProvider"; export default function useToast() { - const { showToast } = useContext(ToastContext); + const { showToast, blackoutToast } = useContext(ToastContext); - return (toast: ToastType) => showToast(toast); + return { showToast: (toast: ToastType) => showToast(toast), blackoutToast }; } diff --git a/src/@components/@common/Toast/style.ts b/src/@components/@common/Toast/style.ts index d502f5e1..19a16b16 100644 --- a/src/@components/@common/Toast/style.ts +++ b/src/@components/@common/Toast/style.ts @@ -12,6 +12,7 @@ export const ToastContainer = styled.div` export const ToastMessage = styled.div` display: flex; + justify-content: space-between; height: 4.8rem; padding: 0.8rem 1.6rem; @@ -24,3 +25,8 @@ export const ToastMessage = styled.div` ${({ theme }) => theme.newFonts.caption1} color: ${({ theme }) => theme.newColors.gray900}; `; + +export const CancelButton = styled.button` + ${({ theme }) => theme.newFonts.caption1} + color: ${({ theme }) => theme.newColors.gray600}; +`; diff --git a/src/@components/@common/hooks/useShowByQuery.ts b/src/@components/@common/hooks/useShowByQuery.ts new file mode 100644 index 00000000..24353765 --- /dev/null +++ b/src/@components/@common/hooks/useShowByQuery.ts @@ -0,0 +1,15 @@ +import { useEffect, useState } from "react"; + +import { LocationType } from "../../../types/cardCollection"; + +// 파라미터로 보이지 않아야할 locationType 전달 +export default function useShowByCardType(locationTypes: LocationType[]) { + const [isShow, setIsShow] = useState(false); + + useEffect(() => { + const cardType = new URLSearchParams(window.location.search.split("?")[1]).get("type"); + setIsShow(!locationTypes.includes((cardType as LocationType) || "")); + }, [locationTypes]); + + return { isShow }; +} diff --git a/src/@components/CardCollectionPage/Card/MenuModal/index.tsx b/src/@components/CardCollectionPage/Card/MenuModal/index.tsx index 3e659131..2d7fba98 100644 --- a/src/@components/CardCollectionPage/Card/MenuModal/index.tsx +++ b/src/@components/CardCollectionPage/Card/MenuModal/index.tsx @@ -1,9 +1,19 @@ +import { useEffect, useState } from "react"; + +import { LocationType } from "../../../../types/cardCollection"; +import useShowByCardType from "../../../@common/hooks/useShowByQuery"; import Modal from "../../../@common/Modal"; import useToast from "../../../@common/Toast/hooks/useToast"; +import { handleClickBlacklistType } from "../../hooks/useBlacklist"; +import { autoSlideType } from "../../hooks/useCardSwiper"; import * as St from "./style"; interface MenuModalProps { + currentCardId: string; closeHandler: () => void; + autoSlide: autoSlideType; + handleClickAddBlacklist: handleClickBlacklistType; + handleClickCancelBlacklist: handleClickBlacklistType; } type ModalItem = { @@ -14,8 +24,23 @@ type ModalItem = { }; export default function MenuModal(props: MenuModalProps) { - const { closeHandler } = props; - const showToast = useToast(); + const { currentCardId, closeHandler, autoSlide, handleClickAddBlacklist, handleClickCancelBlacklist } = props; + const { showToast, blackoutToast } = useToast(); + + const { isShow: isBlockShow } = useShowByCardType([LocationType.BEST, LocationType.MEDLEY]); + + const onSuccessAddBlacklist = () => { + closeHandler(); + showToast({ + message: "🚫 해당 대화주제가 더 이상 추천되지 않아요", + duration: 3.5, + handleClickCancel: () => { + handleClickCancelBlacklist({ _id: currentCardId, onSuccess: blackoutToast }); + autoSlide.slideUp(); + }, + }); + autoSlide.slideDown(); + }; const ModalItems: ModalItem[] = [ { @@ -31,7 +56,10 @@ export default function MenuModal(props: MenuModalProps) { title: "주제 다시 안보기", isNeedLogin: true, handleClickItem: () => { - /* todo */ + handleClickAddBlacklist({ + _id: currentCardId, + onSuccess: onSuccessAddBlacklist, + }); }, }, { @@ -47,13 +75,19 @@ export default function MenuModal(props: MenuModalProps) { return ( - {ModalItems.map(({ emoji, title, isNeedLogin, handleClickItem }, idx) => ( - - {emoji} - {title} - {isNeedLogin && 로그인 시 사용가능 합니다} - - ))} + {ModalItems.map(({ emoji, title, isNeedLogin, handleClickItem }, idx) => { + if (idx === 1 && !isBlockShow) { + return null; + } else { + return ( + + {emoji} + {title} + {isNeedLogin && 로그인 시 사용가능 합니다} + + ); + } + })} ); diff --git a/src/@components/CardCollectionPage/Card/MenuModal/style.ts b/src/@components/CardCollectionPage/Card/MenuModal/style.ts index 1ac26d87..b2ba6c76 100644 --- a/src/@components/CardCollectionPage/Card/MenuModal/style.ts +++ b/src/@components/CardCollectionPage/Card/MenuModal/style.ts @@ -6,13 +6,12 @@ export const ModalContainer = styled.aside` display: flex; flex-direction: column; - width: 36rem; + width: 100%; `; export const ModalItemWrapper = styled.div` display: flex; - width: 36rem; padding: 1.6rem; ${({ theme }) => theme.newFonts.body4}; diff --git a/src/@components/CardCollectionPage/Card/index.tsx b/src/@components/CardCollectionPage/Card/index.tsx index 3fef499b..e01dc470 100644 --- a/src/@components/CardCollectionPage/Card/index.tsx +++ b/src/@components/CardCollectionPage/Card/index.tsx @@ -1,21 +1,27 @@ import React from "react"; import { GTM_CLASS_NAME } from "../../../util/const/gtm"; +import useModal from "../../@common/hooks/useModal"; +import useBlacklist from "../hooks/useBlacklist"; +import { autoSlideType } from "../hooks/useCardSwiper"; import TagsSlider from "../TagsSlider"; import CardMenu from "./CardMenu"; +import MenuModal from "./MenuModal"; import St from "./style"; interface LoginCheckProps { - openLoginModalHandler: () => void; + autoSlide: autoSlideType; _id: string; content: string; isBookmark: boolean; tags: string[]; - toggleMenuModal: () => void; + openLoginModalHandler: () => void; } const Card = (props: LoginCheckProps) => { - const { content, tags } = props; + const { _id, content, tags, autoSlide, openLoginModalHandler } = props; + const { isModalOpen: isMenuModalOpen, toggleModal: toggleMenuModal } = useModal(); + const { getIsBlacklist, handleClickAddBlacklist, handleClickCancelBlacklist } = useBlacklist(openLoginModalHandler); return ( @@ -25,7 +31,24 @@ const Card = (props: LoginCheckProps) => { - + + + {getIsBlacklist(_id) && ( + + 다시 안보기를 설정한 주제입니다 + handleClickCancelBlacklist({ _id })}>취소하기 + + )} + + {isMenuModalOpen && ( + + )} ); }; diff --git a/src/@components/CardCollectionPage/Card/style.ts b/src/@components/CardCollectionPage/Card/style.ts index dcfce5e3..cda265da 100644 --- a/src/@components/CardCollectionPage/Card/style.ts +++ b/src/@components/CardCollectionPage/Card/style.ts @@ -37,10 +37,42 @@ const TagsWrapper = styled.div` margin-top: 1.6rem; `; +const BlockCardWrapper = styled.div` + position: absolute; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + + width: 100%; + height: 100%; + + gap: 0.8rem; + + border-radius: 0.8rem; + background: var(--blackblur, rgba(0, 0, 0, 0.6)); + backdrop-filter: blur(1.2rem); +`; + +const BlockCardText = styled.p` + color: ${({ theme }) => theme.newColors.white}; + ${({ theme }) => theme.newFonts.body4}; +`; + +const BlockCardButton = styled.button` + color: ${({ theme }) => theme.newColors.white}; + ${({ theme }) => theme.newFonts.caption1}; + text-decoration: underline; +`; + const St = { Card, Container, ContentWrapper, TagsWrapper, + BlockCardWrapper, + BlockCardText, + BlockCardButton, }; + export default St; diff --git a/src/@components/CardCollectionPage/CardSlider/index.tsx b/src/@components/CardCollectionPage/CardSlider/index.tsx index 3f800272..1166b28c 100644 --- a/src/@components/CardCollectionPage/CardSlider/index.tsx +++ b/src/@components/CardCollectionPage/CardSlider/index.tsx @@ -6,6 +6,7 @@ import { CardList } from "../../../types/cardCollection"; import { externalLinks } from "../../../util/const/externalLinks"; import Card from "../Card"; import LastCard from "../Card/LastCard"; +import useBlacklist from "../hooks/useBlacklist"; import useCardSwiper from "../hooks/useCardSwiper"; import * as St from "./style"; @@ -13,19 +14,18 @@ interface CardSliderProps { openLoginModalHandler: () => void; cardLists: CardList[]; lastCardObsvRef: React.RefObject; - toggleMenuModal: () => void; } const CardSlider = (props: CardSliderProps) => { - const { openLoginModalHandler, cardLists, lastCardObsvRef, toggleMenuModal } = props; - const { swiperSettings } = useCardSwiper(); + const { openLoginModalHandler, cardLists, lastCardObsvRef } = props; + const { swiperSettings, swiperRef, autoSlide } = useCardSwiper(); return ( - + {cardLists.map((cardList) => ( - + ))} diff --git a/src/@components/CardCollectionPage/hooks/useBlacklist.ts b/src/@components/CardCollectionPage/hooks/useBlacklist.ts new file mode 100644 index 00000000..6151ad25 --- /dev/null +++ b/src/@components/CardCollectionPage/hooks/useBlacklist.ts @@ -0,0 +1,48 @@ +import { useCallback, useState } from "react"; + +import { cardCollectionApi } from "../../../core/api/cardCollection"; +import useAuth from "../../../core/hooks/useAuth"; + +interface handleClickParams { + _id: string; + onSuccess?: () => void; +} + +export type handleClickBlacklistType = (params: handleClickParams) => void; + +const useBlacklist = (handleClickBeforeLogin: () => void) => { + const { isLogin } = useAuth(); + const [blacklist, setBlacklist] = useState([]); + + const handleClickAddBlacklist: handleClickBlacklistType = useCallback( + ({ _id, onSuccess: onSuccessAdd }) => { + switch (isLogin) { + case true: + cardCollectionApi.addBlacklist(_id); + onSuccessAdd && onSuccessAdd(); + setBlacklist((prev) => [...prev, _id]); + break; + case false: + handleClickBeforeLogin(); + break; + } + }, + [isLogin, handleClickBeforeLogin], + ); + + const handleClickCancelBlacklist: handleClickBlacklistType = useCallback(({ _id, onSuccess: onSuccessDelete }) => { + cardCollectionApi.deleteBlacklist(_id); + setBlacklist((prev) => prev.filter((id) => id !== _id)); + onSuccessDelete && onSuccessDelete(); + }, []); + + const getIsBlacklist = useCallback((_id: string) => blacklist.includes(_id), [blacklist]); + + return { + getIsBlacklist, + handleClickAddBlacklist, + handleClickCancelBlacklist, + }; +}; + +export default useBlacklist; diff --git a/src/@components/CardCollectionPage/hooks/useCardLists.ts b/src/@components/CardCollectionPage/hooks/useCardLists.ts index 6c92feac..bc1498ac 100644 --- a/src/@components/CardCollectionPage/hooks/useCardLists.ts +++ b/src/@components/CardCollectionPage/hooks/useCardLists.ts @@ -21,7 +21,7 @@ export function useCardLists() { const fetchingKeyByLocation = getSWRFetchingKeyByLocation(cardsTypeLocation); const optionsByLocation = getSWROptionsByLocation(cardsTypeLocation); - const { data } = useSWR>( + const { data, mutate: mutateCardlists } = useSWR>( fetchingKeyByLocation, realReq.GET_SWR, optionsByLocation, @@ -32,6 +32,7 @@ export function useCardLists() { return { cardLists: getReturnCardLists(data, cardsTypeLocation) ?? [], fetchCardListsWithFilter, + mutateCardlists, }; } diff --git a/src/@components/CardCollectionPage/hooks/useCardSwiper.ts b/src/@components/CardCollectionPage/hooks/useCardSwiper.ts index 0994dadf..b1bfeb7c 100644 --- a/src/@components/CardCollectionPage/hooks/useCardSwiper.ts +++ b/src/@components/CardCollectionPage/hooks/useCardSwiper.ts @@ -1,12 +1,19 @@ +import { useRef } from "react"; import { useRecoilState, useSetRecoilState } from "recoil"; import { Pagination } from "swiper"; -import { SwiperProps } from "swiper/react"; +import { SwiperProps, SwiperRef } from "swiper/react"; import { isSliderDownState, sliderIdxState } from "../../../core/atom/slider"; +export type autoSlideType = { + slideDown: () => void; + slideUp: () => void; +}; + export default function useCardSwiper() { const [sliderIdx, setSliderIdx] = useRecoilState(sliderIdxState); const setIsSliderDown = useSetRecoilState(isSliderDownState); + const swiperRef = useRef(null); const swiperSettings: SwiperProps = { slidesPerView: "auto", @@ -20,5 +27,10 @@ export default function useCardSwiper() { }, }; - return { swiperSettings }; + const autoSlide: autoSlideType = { + slideDown: () => swiperRef.current?.swiper.slideTo(sliderIdx + 1), + slideUp: () => swiperRef.current?.swiper.slideTo(sliderIdx), + }; + + return { swiperSettings, swiperRef, autoSlide }; } diff --git a/src/@components/CardCollectionPage/index.tsx b/src/@components/CardCollectionPage/index.tsx index 75de89e0..08b230b2 100644 --- a/src/@components/CardCollectionPage/index.tsx +++ b/src/@components/CardCollectionPage/index.tsx @@ -10,7 +10,6 @@ import useModal from "../@common/hooks/useModal"; import useScroll from "../@common/hooks/useScrollToTop"; import LoginModal from "../@common/LoginModal"; import SuspenseBoundary from "../@common/SuspenseBoundary"; -import MenuModal from "./Card/MenuModal"; import CardSlider from "./CardSlider"; import CoachMark from "./CoachMark"; import FilterModal from "./FilterModal"; @@ -37,7 +36,6 @@ function CardCollectionContent() { const { isModalOpen: isFilterModalOpen, toggleModal: toggleFilterModal } = useModal(); const { isModalOpen: isLoginModalOpen, toggleModal: toggleLoginModal } = useModal(); - const { isModalOpen: isMenuModalOpen, toggleModal: toggleMenuModal } = useModal(); const { isOpened: isCoachMarkOpen, handleCloseCoachMark: toggleCoachMark } = useCoachMark(); @@ -46,12 +44,8 @@ function CardCollectionContent() { return ( {isSliderDown ? :
} - + + {isVisibleCTAButton && ( )} - - {isMenuModalOpen && } ); } diff --git a/src/core/api/cardCollection.ts b/src/core/api/cardCollection.ts index 9f83b8fb..0867c5e9 100644 --- a/src/core/api/cardCollection.ts +++ b/src/core/api/cardCollection.ts @@ -6,6 +6,16 @@ function addNDeleteBookmark(cardId: string) { return realReq.PUT(`${PATH.USERS_}${PATH.USERS_BOOKMARK}`, { cardId }); } +function addBlacklist(cardId: string) { + return realReq.POST(`${PATH.USERS_}${PATH.CARDS_}${PATH.BLACKLIST}`, { cardId }); +} + +function deleteBlacklist(cardId: string) { + return realReq.DELETE(`${PATH.USERS_}${PATH.CARDS_}${PATH.BLACKLIST}/${cardId}`); +} + export const cardCollectionApi = { addNDeleteBookmark, + addBlacklist, + deleteBlacklist, }; diff --git a/src/core/api/common/axios.ts b/src/core/api/common/axios.ts index c61b9105..4385f72b 100644 --- a/src/core/api/common/axios.ts +++ b/src/core/api/common/axios.ts @@ -31,4 +31,8 @@ export const realReq = { async PATCH(path: string, body: T) { await axiosInstance.patch(path, body); }, + + async DELETE(path: string, option?: { params: string }) { + await axiosInstance.delete(path, option); + }, }; diff --git a/src/core/api/common/constants.ts b/src/core/api/common/constants.ts index a1e21c0d..e4c7880f 100644 --- a/src/core/api/common/constants.ts +++ b/src/core/api/common/constants.ts @@ -16,6 +16,7 @@ export const PATH = { CARDS_GENDER: "/cardByBookmarkedGender", NOTICES: "/notices", MEDLEY: "/medley", + BLACKLIST: "/blacklist", }; export const USER_TOKEN = "piickle-token";