From 92c4a632b166a934f903fef4491d44d46efd8943 Mon Sep 17 00:00:00 2001 From: seorinn Date: Mon, 11 May 2026 15:20:42 +0900 Subject: [PATCH 1/3] =?UTF-8?q?refactor:=20[TA-194]=20=EC=9D=B8=EB=9D=BC?= =?UTF-8?q?=EC=9D=B8=20=ED=95=A8=EC=88=98=20props=20=EC=A0=9C=EA=B1=B0?= =?UTF-8?q?=EB=A1=9C=20=EB=B6=88=ED=95=84=EC=9A=94=ED=95=9C=20=EB=A6=AC?= =?UTF-8?q?=EB=A0=8C=EB=8D=94=EB=A7=81=20=EB=B0=A9=EC=A7=80=20(useCallback?= =?UTF-8?q?/useMemo)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/FilterBar/index.tsx | 58 +++++++++++++++--------- src/components/PhotographerBox/index.tsx | 58 +++++++++++++----------- src/components/PromotionBox/index.tsx | 55 ++++++++++++---------- 3 files changed, 99 insertions(+), 72 deletions(-) diff --git a/src/components/FilterBar/index.tsx b/src/components/FilterBar/index.tsx index ce30a08..00b0219 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,38 @@ 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); - }; + }, []); return (
@@ -75,14 +91,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="할인" /> diff --git a/src/components/PhotographerBox/index.tsx b/src/components/PhotographerBox/index.tsx index 18a0083..8203780 100644 --- a/src/components/PhotographerBox/index.tsx +++ b/src/components/PhotographerBox/index.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; import { savePhotographer, deleteSavedPhotographer, @@ -30,31 +30,37 @@ export default function PhotographerBox({ data }: Props) { setIsClipped(data.isSaved); }, [data]); - 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(false); + 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..cd507c1 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"; @@ -39,35 +39,40 @@ export default function PromotionBox({ data }: Props) { setSaveCount(data.saveCount); }, [data]); - const onClickSave = () => { - requireLogin(() => savePromotionFunc(isClipped)); - }; + const dDayText = useMemo(() => getDDayText(data.endedAt), [data.endedAt]); + + const savePromotionFunc = useCallback( + async (clipped: boolean) => { + try { + setIsClipped(!clipped); + setSaveCount(clipped ? saveCount - 1 : saveCount + 1); + clipped + ? await deleteSavedPromotion(data.id) + : await savePromotion(data.id); + } catch (e) { + setIsClipped(false); + setSaveCount(clipped ? saveCount + 1 : saveCount - 1); + setShowLoginModal(true); + console.log(e); + } + }, + [data.id, saveCount, 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}
From c3abc47d20777782d762a12c6f044250ff8d6e35 Mon Sep 17 00:00:00 2001 From: seorinn Date: Mon, 11 May 2026 15:24:45 +0900 Subject: [PATCH 2/3] =?UTF-8?q?perf:=20[TA-179]=20Promise.allSettled=20+?= =?UTF-8?q?=20cleanup=EC=9C=BC=EB=A1=9C=20=EC=97=90=EB=9F=AC=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC=20=EB=B0=8F=20stale=20request=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/MyPage/index.tsx | 33 +++++++++++++++++++++------------ 1 file changed, 21 insertions(+), 12 deletions(-) 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 <>; From a8687cd61e6263c1a9d727f2fbbed005efd4449b Mon Sep 17 00:00:00 2001 From: seorinn Date: Mon, 11 May 2026 15:25:20 +0900 Subject: [PATCH 3/3] =?UTF-8?q?refactor:=20[TA-194]=20=EC=BD=94=EB=93=9C?= =?UTF-8?q?=20=EB=A6=AC=EB=B7=B0=20=EA=B0=9C=EC=84=A0=20-=20saveCount=20?= =?UTF-8?q?=ED=95=A8=EC=88=98=ED=98=95=20=EC=97=85=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=8A=B8,=20catch=20=EB=B3=B5=EC=9B=90=20=EB=B2=84=EA=B7=B8=20?= =?UTF-8?q?=EC=88=98=EC=A0=95,=20useLoginGuard=20=EB=A9=94=EB=AA=A8?= =?UTF-8?q?=EC=9D=B4=EC=A0=9C=EC=9D=B4=EC=85=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/FilterBar/index.tsx | 4 ++- src/components/PhotographerBox/index.tsx | 4 +-- src/components/PromotionBox/index.tsx | 10 +++--- src/hooks/useLoginGuard.ts | 46 ++++++++++++++---------- 4 files changed, 37 insertions(+), 27 deletions(-) diff --git a/src/components/FilterBar/index.tsx b/src/components/FilterBar/index.tsx index 00b0219..70973f0 100644 --- a/src/components/FilterBar/index.tsx +++ b/src/components/FilterBar/index.tsx @@ -78,6 +78,8 @@ export default function FilterBar({ isEvent, setSearchParams }: Props) { setClickedFilter(type); }, []); + const handleCloseSelectBar = useCallback(() => setShowSelectBar(false), []); + return (
@@ -135,7 +137,7 @@ export default function FilterBar({ isEvent, setSearchParams }: Props) {
setShowSelectBar(false)} + onClick={handleCloseSelectBar} />
{ setIsClipped(data.isSaved); - }, [data]); + }, [data.isSaved]); const nickname = useMemo( () => @@ -50,7 +50,7 @@ export default function PhotographerBox({ data }: Props) { ? await deleteSavedPhotographer(data.id) : await savePhotographer(data.id); } catch (e) { - setIsClipped(false); + setIsClipped(clipped); setShowLoginModal(true); console.log(e); } diff --git a/src/components/PromotionBox/index.tsx b/src/components/PromotionBox/index.tsx index cd507c1..50bf492 100644 --- a/src/components/PromotionBox/index.tsx +++ b/src/components/PromotionBox/index.tsx @@ -37,7 +37,7 @@ export default function PromotionBox({ data }: Props) { useEffect(() => { setIsClipped(data.isSaved); setSaveCount(data.saveCount); - }, [data]); + }, [data.isSaved, data.saveCount]); const dDayText = useMemo(() => getDDayText(data.endedAt), [data.endedAt]); @@ -45,18 +45,18 @@ export default function PromotionBox({ data }: Props) { async (clipped: boolean) => { try { setIsClipped(!clipped); - setSaveCount(clipped ? saveCount - 1 : saveCount + 1); + setSaveCount((prev) => (clipped ? prev - 1 : prev + 1)); clipped ? await deleteSavedPromotion(data.id) : await savePromotion(data.id); } catch (e) { - setIsClipped(false); - setSaveCount(clipped ? saveCount + 1 : saveCount - 1); + setIsClipped(clipped); + setSaveCount((prev) => (clipped ? prev + 1 : prev - 1)); setShowLoginModal(true); console.log(e); } }, - [data.id, saveCount, setShowLoginModal], + [data.id, setShowLoginModal], ); const onClickSave = useCallback(() => { 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 }; }