Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
2 changes: 1 addition & 1 deletion next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: "/**",
},
Expand Down
6 changes: 5 additions & 1 deletion src/assets/icons/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
export { default as PlusIcon } from "./plus.svg";
export { default as PlusDisableIcon } from "./plus-disable.svg";
export { default as ShoppingBag } from "./shopping-bag.svg";
export { default as TrashIcon } from "./trash.svg";
3 changes: 3 additions & 0 deletions src/assets/icons/minus.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions src/assets/icons/plus-disable.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions src/assets/icons/plus.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions src/assets/icons/trash.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
27 changes: 27 additions & 0 deletions src/features/cart/components/button/QuantityStepper.module.css
Original file line number Diff line number Diff line change
@@ -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);
}
65 changes: 65 additions & 0 deletions src/features/cart/components/button/QuantityStepper.tsx
Original file line number Diff line number Diff line change
@@ -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 ? <TrashIcon /> : <MinusIcon />;
const incrementIcon = quantity !== 9 ? <PlusIcon /> : <PlusDisableIcon />;
const isPlusDisabled = quantity >= 9

return (
<div className={styles.container}>
<div className={styles.wrapper}>
<button
onClick={handleDecrement}
disabled={isAddingToCart}
>
{decrementIcon}
</button>
<div className={styles.label}>
{quantity}
</div>
<button
onClick={handleIncrement}
disabled={isPlusDisabled}
>
{incrementIcon}
</button>
</div>
</div>
);
}
54 changes: 54 additions & 0 deletions src/features/cart/hooks/useCart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down Expand Up @@ -54,6 +58,7 @@ export function useCart(): UseCartReturn {
/**
* 메뉴 1개 추가 (기존 수량 + 1)
* - 버튼 연타 방지
* - 각 메뉴는 최대 9개까지
*/
const addToCart = useCallback((
menuId: number,
Expand Down Expand Up @@ -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<void>((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,
};
}
5 changes: 0 additions & 5 deletions src/features/menu/components/button/CartButton.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,6 @@
justify-content: center;
}

.icon {
width: 1.25rem;
height: 1.25rem;
}

.label {
color: var(--color-white);
font-size: var(--text-base);
Expand Down
2 changes: 0 additions & 2 deletions src/features/menu/components/button/CartButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,10 @@ export default function CartButton({
className={styles.button}
>
<div className={styles.labelWrapper}>
<div className={styles.icon}>
<RoundedNumber
number={quantity}
filled={false}
/>
</div>
<div className={styles.label}>
장바구니 보기
</div>
Expand Down
3 changes: 3 additions & 0 deletions src/shared/components/number/RoundedNumber.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down