Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 17 additions & 38 deletions week08/mission/Chap8_mission/src/hooks/useThrottle.ts
Original file line number Diff line number Diff line change
@@ -1,63 +1,42 @@
import { useEffect, useRef, useState } from "react";

// value가 true로 바뀔 때 지정한 delay 시간 동안 한 번만 true로 반영
// value가 false로 바뀌면 즉시 false로 반영
// value: boolean 상태 값 (ex: inView)
// delay: throttle 간격(ms)
// throttledValue: throttle 적용된 상태 값

function useThrottle(value: boolean, delay: number) {
// throttledValue: 실제로 외부에 반환되는 상태
function useThrottle<T>(value: T, delay: number): T {
const [throttledValue, setThrottledValue] = useState(value);

// lastExecuted: 마지막으로 throttledValue를 true로 업데이트한 시간
const lastExecuted = useRef(0);

// timer: delay 시간 후에 업데이트하기 위한 setTimeout ID
const timer = useRef<number | null>(null); // 브라우저 환경에서 setTimeout은 number 반환
const lastExecuted = useRef(Date.now());
const timer = useRef<number | null>(null);

useEffect(() => {
// value가 false이면 즉시 반영
if (!value) {
setThrottledValue(false);
const now = Date.now();
const remaining = delay - (now - lastExecuted.current);

// 이미 등록된 타이머가 있다면 취소
if (timer.current) {
clearTimeout(timer.current);
timer.current = null;
}
return; // 이후 로직 실행 X
// 기존 타이머 제거
if (timer.current) {
clearTimeout(timer.current);
timer.current = null;
}

const now = Date.now(); // 현재 시간
const remaining = delay - (now - lastExecuted.current);
// lastExecuted 기준으로 남은 delay 계산

if (remaining <= 0) {
// 마지막 실행 이후 delay 이상 지났으면 즉시 true 반영
// delay 지나면 즉시 실행
lastExecuted.current = now;
setThrottledValue(true);
setThrottledValue(value);
} else {
// delay가 남아 있으면 이전 타이머 취소 후 새 타이머 등록
if (timer.current) clearTimeout(timer.current);

// 남은 시간만큼 타이머
timer.current = window.setTimeout(() => {
lastExecuted.current = Date.now(); // 실행 시간 업데이트
setThrottledValue(true); // throttledValue 갱신
timer.current = null; // 타이머 리셋
lastExecuted.current = Date.now();
setThrottledValue(value);
timer.current = null;
}, remaining);
}

// Clean-up: effect 재실행 전 타이머 정리
return () => {
if (timer.current) {
clearTimeout(timer.current);
timer.current = null;
}
};
}, [value, delay]); // value 또는 delay 변경 시 effect 실행
}, [value, delay]);

return throttledValue; // throttle 적용된 상태 반환
return throttledValue;
}

export default useThrottle;
16 changes: 12 additions & 4 deletions week08/mission/Chap8_mission/src/pages/HomePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,22 @@ const HomePage = () => {
console.log("data:", data);

const { ref, inView } = useInView({ threshold: 0 });
const throttledInView = useThrottle(inView, 3000);

// throttledInView 감시
const shouldFetch = inView && !isFetching && hasNextPage;
const throttledInView = useThrottle(shouldFetch, 1000);

useEffect(() => {
if (throttledInView && !isFetching && hasNextPage) {
if (throttledInView) {
fetchNextPage();
}
}, [throttledInView, isFetching, hasNextPage, fetchNextPage]);
}, [throttledInView, fetchNextPage]);

// throttledInView 감시
// useEffect(() => {
// if (throttledInView && !isFetching && hasNextPage) {
// fetchNextPage();
// }
// }, [throttledInView, isFetching, hasNextPage, fetchNextPage]);

if (isError) {
return <div>Error occurred while fetching data.</div>;
Expand Down
6 changes: 2 additions & 4 deletions week09/mission/Chap9_mission/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
import "./App.css";
import { Provider } from "react-redux";
import { CartList } from "./components/CartList";
import { Navbar } from "./components/Navbar";
import store from "./store/store";

function App() {
return (
<Provider store={store}>
<div>
<Navbar />
<CartList />
</Provider>
</div>
);
}

Expand Down
9 changes: 4 additions & 5 deletions week09/mission/Chap9_mission/src/components/CartItem.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,21 @@
import type { Lp } from "../types/cart";
import { useDispatch } from "../hooks/useCustomRedux";
import { decrement, increment } from "../slices/cartSlice";
import { useCartActions } from "../hooks/useCartStore";

interface CartItemProps {
lp: Lp;
}

export const CartItem = ({ lp }: CartItemProps) => {
const dispatch = useDispatch();
const { increase, decrease } = useCartActions();

const handleIncrease = () => {
// 수량 증가 액션 디스패치
dispatch(increment({ id: lp.id }));
increase(lp.id);
};

const handleDecrease = () => {
// 수량 감소 액션 디스패치
dispatch(decrement({ id: lp.id }));
decrease(lp.id);
};

return (
Expand Down
5 changes: 3 additions & 2 deletions week09/mission/Chap9_mission/src/components/CartList.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { CartItem } from "./CartItem";
import { PriceBox } from "./PriceBox";
import { ClearBtn } from "./ClearBtn";
import { useSelector } from "../hooks/useCustomRedux";
import { useCartInfo } from "../hooks/useCartStore";


export const CartList = () => {
const { cartItems } = useSelector((state) => state.cart);
const { cartItems } = useCartInfo();
return (
<div className="max-w-3xl mx-auto mt-16 mb-5 p-6 bg-white rounded-lg shadow-md">
{cartItems.map((item) => (
Expand Down
31 changes: 19 additions & 12 deletions week09/mission/Chap9_mission/src/components/ClearBtn.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,27 @@
import { useDispatch } from "../hooks/useCustomRedux";
import { clearCart } from "../slices/cartSlice";
import DeleteModal from "./DeleteModal";
import { useModalActions } from "../hooks/useModalStore";
import { useModalInfo } from "../hooks/useModalStore";

export const ClearBtn = () => {
const dispatch = useDispatch();
const { isOpen } = useModalInfo();
const { openModal } = useModalActions();

const handleClearCart = () => {
// 수량 증가 액션 디스패치
dispatch(clearCart());
const handleModalOpen = () => {
openModal();
};

return (
<button
className="mt-6 w-full bg-red-100 text-white py-3 rounded-md font-semibold hover:bg-red-600 transition-colors"
onClick={handleClearCart}
>
전체 삭제하기
</button>
<>
<button
className="mt-6 w-full bg-red-100 text-white py-3 rounded-md font-semibold hover:bg-red-600 transition-colors"
onClick={handleModalOpen}
>
전체 삭제하기
</button>
{/* 모달 표시 */}
{isOpen && (
<DeleteModal/>
)}
</>
);
};
98 changes: 98 additions & 0 deletions week09/mission/Chap9_mission/src/components/DeleteModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { useCartActions } from "../hooks/useCartStore";
import { useModalActions } from "../hooks/useModalStore";

const DeleteModal = () => {
const { clearCart } = useCartActions();
const { closeModal } = useModalActions();

const handleClearCart = () => {
// 수량 증가 액션 디스패치
clearCart();
closeModal();
};

const handleModalClose = () => {
closeModal();
};

return (
<div className="fixed inset-0 z-40 min-h-full overflow-y-auto overflow-x-hidden transition flex items-center">
{/* 오버레이 */}
<div
aria-hidden="true"
className="fixed inset-0 w-full h-full bg-black/50 cursor-pointer"
onClick={handleModalClose}
/>

{/* Modal wrapper */}
<div className="relative w-full cursor-pointer pointer-events-none transition my-auto p-4">
<div className="w-full py-2 bg-white cursor-default pointer-events-auto dark:bg-gray-800 relative rounded-xl mx-auto max-w-sm">
{/* X 버튼 */}
<button
type="button"
onClick={handleModalClose}
className="absolute top-2 right-2 rtl:right-auto rtl:left-2"
>
<svg
className="h-4 w-4 cursor-pointer text-gray-400"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fillRule="evenodd"
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
clipRule="evenodd"
></path>
</svg>
<span className="sr-only">Close</span>
</button>

{/* Body */}
<div className="space-y-2 p-2">
<div className="p-4 space-y-2 text-center dark:text-white">
<h2 className="text-xl font-bold tracking-tight">
전체 삭제하기
</h2>

<p className="text-gray-500">
장바구니의 모든 상품을 삭제하시겠습니까?
</p>
</div>
</div>

{/* Footer */}
<div className="space-y-2">
<div className="border-t border-gray-200" />

<div className="px-6 py-2">
<div className="flex justify-center w-full gap-2">
{/* Cancel */}
<button
type="button"
onClick={handleModalClose}
className="w-full inline-flex items-center justify-center py-1 gap-1 font-semibold rounded-lg bg-gray-400 hover:bg-gray-500 cursor-pointer"
>
취소
</button>

{/* Confirm */}
<button
type="button"
onClick={handleClearCart}
className="w-full inline-flex items-center justify-center py-1 gap-1 font-semibold rounded-lg text-white bg-red-600 hover:bg-red-500 cursor-pointer"
>
삭제
</button>
</div>
</div>
</div>

</div>
</div>
</div>
);
};

export default DeleteModal;
11 changes: 5 additions & 6 deletions week09/mission/Chap9_mission/src/components/Navbar.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import { useEffect } from "react";
import { useDispatch, useSelector } from "../hooks/useCustomRedux";
import { calculateTotals } from "../slices/cartSlice";
import { useCartInfo, useCartActions } from "../hooks/useCartStore";

export const Navbar = () => {
const { amount, cartItems } = useSelector((state) => state.cart);
const dispatch = useDispatch();
const { amount, cartItems } = useCartInfo();
const { calculateTotals } = useCartActions();

useEffect(() => {
dispatch(calculateTotals());
}, [dispatch, cartItems]);
calculateTotals();
}, [cartItems, calculateTotals]);

return (
<nav className="fixed top-0 left-0 w-full bg-gray-800 text-white py-3 px-4 flex items-center justify-between z-50">
Expand Down
4 changes: 2 additions & 2 deletions week09/mission/Chap9_mission/src/components/PriceBox.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useSelector } from "../hooks/useCustomRedux";
import { useCartInfo } from "../hooks/useCartStore";

export const PriceBox = () => {
const { total } = useSelector((state) => state.cart);
const { total } = useCartInfo();

return (
<h1 className="flex justify-end mt-3 text-lg font-extrabold ">
Expand Down
Loading