diff --git a/src/components/FilterBar/index.tsx b/src/components/FilterBar/index.tsx index ce30a08..70973f0 100644 --- a/src/components/FilterBar/index.tsx +++ b/src/components/FilterBar/index.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from "react"; +import { useCallback, useEffect, useState } from "react"; import icn_reset from "../../assets/svgs/icn_reset.svg"; import icn_line from "../../assets/svgs/icn_filterLine.svg"; import FilterType from "./FilterType"; @@ -19,23 +19,26 @@ export default function FilterBar({ isEvent, setSearchParams }: Props) { const [showSelectBar, setShowSelectBar] = useState(false); const [clickedFilter, setClickedFilter] = useState(""); - const getDefaultFilters = () => - isEvent - ? { - progressStatus: "ALL", - sortType: "LATEST", - } - : { - sortType: "LATEST", - }; + const getDefaultFilters = useCallback( + () => + isEvent + ? { + progressStatus: "ALL", + sortType: "LATEST", + } + : { + sortType: "LATEST", + }, + [isEvent], + ); const [filters, setFilters] = useState(getDefaultFilters()); const [selectedLocs, setSelectedLocs] = useState([]); - const handleFilterChange = (key: string, value: any) => { + const handleFilterChange = useCallback((key: string, value: any) => { setFilters((prevFilters: any) => ({ ...prevFilters, [key]: value })); - }; + }, []); useEffect(() => { const newSearchParams = new URLSearchParams(); @@ -49,25 +52,40 @@ export default function FilterBar({ isEvent, setSearchParams }: Props) { setSearchParams(newSearchParams); }, [filters, setSearchParams]); - const handleResetFilter = () => { + const handleSetFree = useCallback( + () => handleFilterChange("type", "FREE"), + [handleFilterChange], + ); + const handleSetDiscount = useCallback( + () => handleFilterChange("type", "DISCOUNT"), + [handleFilterChange], + ); + const handleClearType = useCallback( + () => handleFilterChange("type", ""), + [handleFilterChange], + ); + + const handleResetFilter = useCallback(() => { setIsModifiedOrder(false); setIsModifiedState(false); setIsModifiedRegion(false); setIsModifiedTypes(false); setFilters(getDefaultFilters()); - }; + }, [getDefaultFilters]); - const handleFilter = (type: string) => { + const handleFilter = useCallback((type: string) => { setShowSelectBar(true); setClickedFilter(type); - }; + }, []); + + const handleCloseSelectBar = useCallback(() => setShowSelectBar(false), []); return (
@@ -75,14 +93,14 @@ export default function FilterBar({ isEvent, setSearchParams }: Props) {
handleFilterChange("type", "FREE")} - setOff={() => handleFilterChange("type", "")} + setOn={handleSetFree} + setOff={handleClearType} title="무료" /> handleFilterChange("type", "DISCOUNT")} - setOff={() => handleFilterChange("type", "")} + setOn={handleSetDiscount} + setOff={handleClearType} title="할인" /> @@ -119,7 +137,7 @@ export default function FilterBar({ isEvent, setSearchParams }: Props) {
setShowSelectBar(false)} + onClick={handleCloseSelectBar} />
{ setIsClipped(data.isSaved); - }, [data]); + }, [data.isSaved]); - const handleTextLength = () => { - if (data.nickname.length < 8) return data.nickname; - return `${data.nickname.slice(0, 8)}...`; - }; + const nickname = useMemo( + () => + data.nickname.length < 8 + ? data.nickname + : `${data.nickname.slice(0, 8)}...`, + [data.nickname], + ); - const onClickPhotographer = () => { + const onClickPhotographer = useCallback(() => { window.open(`/photographer/${data.id}`); - }; + }, [data.id]); - const onClickSave = () => { - requireLogin(() => savePhotographerFunc(isClipped)); - }; + const savePhotographerFunc = useCallback( + async (clipped: boolean) => { + try { + setIsClipped(!clipped); + clipped + ? await deleteSavedPhotographer(data.id) + : await savePhotographer(data.id); + } catch (e) { + setIsClipped(clipped); + setShowLoginModal(true); + console.log(e); + } + }, + [data.id, setShowLoginModal], + ); - const savePhotographerFunc = async (isClipped: boolean) => { - try { - setIsClipped(!isClipped); - isClipped - ? await deleteSavedPhotographer(data.id) - : await savePhotographer(data.id); - } catch (e) { - setIsClipped(false); - setShowLoginModal(true); - console.log(e); - } - }; + const onClickSave = useCallback(() => { + requireLogin(() => savePhotographerFunc(isClipped)); + }, [requireLogin, savePhotographerFunc, isClipped]); if (!data) return <>; return ( @@ -62,7 +68,7 @@ export default function PhotographerBox({ data }: Props) {
onClickPhotographer()} + onClick={onClickPhotographer} >
clip onClickSave()} + onClick={onClickSave} />
-
onClickPhotographer()}> -
{handleTextLength()}
+
+
{nickname}
{data.mainPhotographyTypes.map((type, index) => (
diff --git a/src/components/PromotionBox/index.tsx b/src/components/PromotionBox/index.tsx index 22d7f3b..50bf492 100644 --- a/src/components/PromotionBox/index.tsx +++ b/src/components/PromotionBox/index.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; import { useNavigate } from "react-router-dom"; import { savePromotion, deleteSavedPromotion } from "../../api/promotion"; import { useLoginGuard } from "../../hooks/useLoginGuard"; @@ -37,37 +37,42 @@ export default function PromotionBox({ data }: Props) { useEffect(() => { setIsClipped(data.isSaved); setSaveCount(data.saveCount); - }, [data]); + }, [data.isSaved, data.saveCount]); - const onClickSave = () => { - requireLogin(() => savePromotionFunc(isClipped)); - }; + const dDayText = useMemo(() => getDDayText(data.endedAt), [data.endedAt]); + + const savePromotionFunc = useCallback( + async (clipped: boolean) => { + try { + setIsClipped(!clipped); + setSaveCount((prev) => (clipped ? prev - 1 : prev + 1)); + clipped + ? await deleteSavedPromotion(data.id) + : await savePromotion(data.id); + } catch (e) { + setIsClipped(clipped); + setSaveCount((prev) => (clipped ? prev + 1 : prev - 1)); + setShowLoginModal(true); + console.log(e); + } + }, + [data.id, setShowLoginModal], + ); - const savePromotionFunc = async (isClipped: boolean) => { - try { - setIsClipped(!isClipped); - setSaveCount(isClipped ? saveCount - 1 : saveCount + 1); - isClipped - ? await deleteSavedPromotion(data.id) - : await savePromotion(data.id); - } catch (e) { - setIsClipped(false); - setSaveCount(isClipped ? saveCount + 1 : saveCount - 1); - setShowLoginModal(true); - console.log(e); - } - }; + const onClickSave = useCallback(() => { + requireLogin(() => savePromotionFunc(isClipped)); + }, [requireLogin, savePromotionFunc, isClipped]); - const openDetailPage = () => { + const openDetailPage = useCallback(() => { isMobileDevice() ? navigate(`/event/${data.id}`) : window.open(`/event/${data.id}`); - }; + }, [data.id, navigate]); return (
-
openDetailPage()}> +
{data.title}
{data.author && !data.author.isAdmin && ( @@ -88,13 +93,13 @@ export default function PromotionBox({ data }: Props) { clip onClickSave()} + onClick={onClickSave} />
openDetailPage()} + onClick={openDetailPage} > {data.images.map((image, index) => (
@@ -110,11 +115,11 @@ export default function PromotionBox({ data }: Props) {
openDetailPage()} + onClick={openDetailPage} >
- {getDDayText(data.endedAt)} + {dDayText}
diff --git a/src/hooks/useLoginGuard.ts b/src/hooks/useLoginGuard.ts index 5d88d83..34631ab 100644 --- a/src/hooks/useLoginGuard.ts +++ b/src/hooks/useLoginGuard.ts @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useCallback, useMemo, useState } from "react"; import { useNavigate } from "react-router-dom"; interface UseLoginGuardReturn { @@ -19,25 +19,33 @@ export function useLoginGuard(): UseLoginGuardReturn { const navigate = useNavigate(); const [showLoginModal, setShowLoginModal] = useState(false); - const requireLogin = (onAuthorized: () => void) => { - if (!localStorage.getItem("accessToken")) { - setShowLoginModal(true); - return; - } - onAuthorized(); - }; + const requireLogin = useCallback( + (onAuthorized: () => void) => { + if (!localStorage.getItem("accessToken")) { + setShowLoginModal(true); + return; + } + onAuthorized(); + }, + [setShowLoginModal], + ); - const loginModalProps = { - title: ["로그인이 필요한 서비스입니다."], - content: [ - "이 기능은 로그인 후 이용하실 수 있습니다.", - "로그인 페이지로 이동하시겠습니까?", - ], - btnMsg: "로그인 하기", - align: "start" as const, - setShowModal: setShowLoginModal, - onClick: () => navigate("/login"), - }; + const handleNavigateLogin = useCallback(() => navigate("/login"), [navigate]); + + const loginModalProps = useMemo( + () => ({ + title: ["로그인이 필요한 서비스입니다."], + content: [ + "이 기능은 로그인 후 이용하실 수 있습니다.", + "로그인 페이지로 이동하시겠습니까?", + ], + btnMsg: "로그인 하기", + align: "start" as const, + setShowModal: setShowLoginModal, + onClick: handleNavigateLogin, + }), + [setShowLoginModal, handleNavigateLogin], + ); return { showLoginModal, setShowLoginModal, requireLogin, loginModalProps }; } diff --git a/src/pages/MyPage/index.tsx b/src/pages/MyPage/index.tsx index 64dc6b9..627de1d 100644 --- a/src/pages/MyPage/index.tsx +++ b/src/pages/MyPage/index.tsx @@ -27,22 +27,31 @@ export default function MyPage() { const role = user.roles.includes("PHOTOGRAPHER") ? "PHOTOGRAPHER" : "USER"; useEffect(() => { + let ignore = false; const fetchUserInfo = async () => { - try { - const res = - role === "USER" - ? await getUserInfo() - : await getPhotographerInfo(user.id); - setUserInfo(res); - const promotions = await getSavedPromotionList(); - const photographers = await getSavedPhotographerList(); - setSavedPromotions(promotions.items); - setSavedPhotographers(photographers.items); - } catch (e) { - console.log(e); + const [resResult, promotionsResult, photographersResult] = + await Promise.allSettled([ + role === "USER" ? getUserInfo() : getPhotographerInfo(user.id), + getSavedPromotionList(), + getSavedPhotographerList(), + ]); + if (ignore) return; + if (resResult.status === "fulfilled") { + setUserInfo(resResult.value); + } else { + console.error("Failed to load user profile", resResult.reason); + } + if (promotionsResult.status === "fulfilled") { + setSavedPromotions(promotionsResult.value.items); + } + if (photographersResult.status === "fulfilled") { + setSavedPhotographers(photographersResult.value.items); } }; fetchUserInfo(); + return () => { + ignore = true; + }; }, [role, user.id]); if (!userInfo) return <>;