diff --git a/next.config.ts b/next.config.ts index cae4153..818d7bf 100644 --- a/next.config.ts +++ b/next.config.ts @@ -36,7 +36,7 @@ const nextConfig: NextConfig = { remotePatterns: [ { protocol: "https", - hostname: "campustable-s3.s3.ap-northeast-2.amazonaws.com", + hostname: "campus-table-s3.s3.ap-northeast-2.amazonaws.com", port: "", pathname: "/**", }, diff --git a/src/assets/icons/index.ts b/src/assets/icons/index.ts index d8ae768..b03098a 100644 --- a/src/assets/icons/index.ts +++ b/src/assets/icons/index.ts @@ -10,6 +10,10 @@ export { default as HomeDisableIcon } from "./home-disable.svg"; export { default as HomeEnableIcon } from "./home-enable.svg"; export { default as JingwanDisableIcon } from "./jingwan-disable.svg"; export { default as JingwanEnableIcon } from "./jingwan-enable.svg"; +export { default as MinusIcon } from "./minus.svg"; export { default as MyDisableIcon } from "./my-disable.svg"; export { default as MyEnableIcon } from "./my-enable.svg"; -export { default as ShoppingBag } from "./shopping-bag.svg"; \ No newline at end of file +export { default as PlusIcon } from "./plus.svg"; +export { default as PlusDisableIcon } from "./plus-disable.svg"; +export { default as ShoppingBagIcon } from "./shopping-bag.svg"; +export { default as TrashIcon } from "./trash.svg"; \ No newline at end of file diff --git a/src/assets/icons/minus.svg b/src/assets/icons/minus.svg new file mode 100644 index 0000000..ba960e4 --- /dev/null +++ b/src/assets/icons/minus.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/plus-disable.svg b/src/assets/icons/plus-disable.svg new file mode 100644 index 0000000..4093fcc --- /dev/null +++ b/src/assets/icons/plus-disable.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/plus.svg b/src/assets/icons/plus.svg new file mode 100644 index 0000000..9a897d0 --- /dev/null +++ b/src/assets/icons/plus.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/trash.svg b/src/assets/icons/trash.svg new file mode 100644 index 0000000..e92ec1a --- /dev/null +++ b/src/assets/icons/trash.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/features/cart/components/button/QuantityStepper.module.css b/src/features/cart/components/button/QuantityStepper.module.css new file mode 100644 index 0000000..2843538 --- /dev/null +++ b/src/features/cart/components/button/QuantityStepper.module.css @@ -0,0 +1,27 @@ +.container { + display: flex; + justify-content: center; + align-content: center; + + padding-block: 0.44rem; + padding-inline: 0.5rem; + + border-radius: 0.25rem; + border: 1px solid var(--color-gray-200); + background-color: var(--color-white); +} + +.wrapper { + display: flex; + justify-content: space-between; + align-items: center; +} + +.label { + color: var(--color-black); + font-size: var(--text-xs); + font-style: normal; + font-weight: var(--font-medium); + line-height: var(--line-height-single); + letter-spacing: var(--letter-spacing); +} \ No newline at end of file diff --git a/src/features/cart/components/button/QuantityStepper.tsx b/src/features/cart/components/button/QuantityStepper.tsx new file mode 100644 index 0000000..d73682e --- /dev/null +++ b/src/features/cart/components/button/QuantityStepper.tsx @@ -0,0 +1,65 @@ +"use client"; + +import styles from "./QuantityStepper.module.css"; +import { MinusIcon, PlusDisableIcon, PlusIcon, TrashIcon } from "@/assets/icons"; +import { useCart } from "@/features/cart/hooks/useCart"; +import { useToast } from "@/shared/hooks/useToast"; + +interface QuantityStepperProps { + menuId: number; + quantity: number; +} + +export default function QuantityStepper({ + menuId, + quantity +}: QuantityStepperProps) { + const { addToCart, removeFromCart, isAddingToCart } = useCart(); + const { showToast } = useToast(); + + const handleIncrement = () => { + if (quantity >= 9) { + showToast("메뉴는 최대 9개까지만 담을 수 있어요!"); + return; + } + addToCart(menuId, { + onError: (message) => showToast(message) + }); + } + + const handleDecrement = () => { + if (quantity <= 0) { + return; + } + + removeFromCart(menuId, { + onError: (message) => showToast(message) + }); + } + + const decrementIcon = quantity === 1 ? : ; + const incrementIcon = quantity !== 9 ? : ; + const isPlusDisabled = quantity >= 9; + + return ( +
+
+ +
+ {quantity} +
+ +
+
+ ); +} \ No newline at end of file diff --git a/src/features/cart/hooks/useCart.ts b/src/features/cart/hooks/useCart.ts index ef2dcb0..6d31f7d 100644 --- a/src/features/cart/hooks/useCart.ts +++ b/src/features/cart/hooks/useCart.ts @@ -14,6 +14,10 @@ interface UseCartReturn { onSuccess?: () => void; onError?: (message: string) => void; }) => void; + removeFromCart: (menuId: number, callbacks?: { + onSuccess?: () => void; + onError?: (message: string) => void; + }) => void; isAddingToCart: boolean; } @@ -54,6 +58,7 @@ export function useCart(): UseCartReturn { /** * 메뉴 1개 추가 (기존 수량 + 1) * - 버튼 연타 방지 + * - 각 메뉴는 최대 9개까지 */ const addToCart = useCallback(( menuId: number, @@ -97,12 +102,61 @@ export function useCart(): UseCartReturn { pendingRequests.current.set(menuId, requestPromise); }, [getMenuQuantity, upsertCartMutation]); + /** + * 메뉴 1개 감소 (기존 수량 - 1) + * - 수량이 1이면 장바구니에서 삭제 (수량 0으로 설정) + * - 버튼 연타 방지 + */ + const removeFromCart = useCallback(( + menuId: number, + callbacks?: { + onSuccess?: () => void; + onError?: (message: string) => void; + } + ) => { + // 중복 요청 방지 + if (pendingRequests.current.has(menuId)) { + return; + } + + // 특정 메뉴 수량 조회 + const menuQuantity = getMenuQuantity(menuId); + + // 수량이 0이면 실행 중지 + if (menuQuantity === 0) { + return; + } + + const newMenuQuantity = menuQuantity - 1; + + const requestPromise = new Promise((resolve, reject) => { + upsertCartMutation.mutate( + { menuId, quantity: newMenuQuantity }, + { + onSuccess: () => { + pendingRequests.current.delete(menuId); + callbacks?.onSuccess?.(); + resolve(); + }, + onError: (error) => { + pendingRequests.current.delete(menuId); + callbacks?.onError?.("장바구니 수정에 실패했어요. 잠시 후 다시 시도해주세요."); + reject(error); + }, + } + ); + }); + + pendingRequests.current.set(menuId, requestPromise); + }, [getMenuQuantity, upsertCartMutation]); + return { cartInfo, isLoading, error, getMenuQuantity, addToCart, + removeFromCart, isAddingToCart: upsertCartMutation.isPending, }; } \ No newline at end of file diff --git a/src/features/menu/components/button/CartButton.module.css b/src/features/menu/components/button/CartButton.module.css index b0870f0..90f4fce 100644 --- a/src/features/menu/components/button/CartButton.module.css +++ b/src/features/menu/components/button/CartButton.module.css @@ -19,11 +19,6 @@ justify-content: center; } -.icon { - width: 1.25rem; - height: 1.25rem; -} - .label { color: var(--color-white); font-size: var(--text-base); diff --git a/src/features/menu/components/button/CartButton.tsx b/src/features/menu/components/button/CartButton.tsx index 7d7cb55..469fbb0 100644 --- a/src/features/menu/components/button/CartButton.tsx +++ b/src/features/menu/components/button/CartButton.tsx @@ -19,12 +19,10 @@ export default function CartButton({ className={styles.button} >
-
-
장바구니 보기
diff --git a/src/shared/components/header/Header.tsx b/src/shared/components/header/Header.tsx index 3bc8f94..994cb35 100644 --- a/src/shared/components/header/Header.tsx +++ b/src/shared/components/header/Header.tsx @@ -1,4 +1,4 @@ -import { ShoppingBag } from "@/assets/icons"; +import { ShoppingBagIcon } from "@/assets/icons"; import styles from "./Header.module.css"; export default function Header() { @@ -8,7 +8,7 @@ export default function Header() {
LOGO
- +
); diff --git a/src/shared/components/number/RoundedNumber.module.css b/src/shared/components/number/RoundedNumber.module.css index 2c95bac..13daf27 100644 --- a/src/shared/components/number/RoundedNumber.module.css +++ b/src/shared/components/number/RoundedNumber.module.css @@ -3,6 +3,9 @@ justify-content: center; align-content: center; + min-width: 1.25rem; + min-height: 1.25rem; + border-radius: 0.3125rem; background-color: var(--color-white);