Skip to content

Commit 2055b7d

Browse files
authored
Merge pull request #130 from CampusTable/20260114_#128_장바구니_메뉴_수량_조절_컴포넌트_개발
20260114 #128 장바구니 메뉴 수량 조절 컴포넌트 개발
2 parents 4463fad + 0233210 commit 2055b7d

13 files changed

Lines changed: 169 additions & 11 deletions

File tree

next.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ const nextConfig: NextConfig = {
3636
remotePatterns: [
3737
{
3838
protocol: "https",
39-
hostname: "campustable-s3.s3.ap-northeast-2.amazonaws.com",
39+
hostname: "campus-table-s3.s3.ap-northeast-2.amazonaws.com",
4040
port: "",
4141
pathname: "/**",
4242
},

src/assets/icons/index.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ export { default as HomeDisableIcon } from "./home-disable.svg";
1010
export { default as HomeEnableIcon } from "./home-enable.svg";
1111
export { default as JingwanDisableIcon } from "./jingwan-disable.svg";
1212
export { default as JingwanEnableIcon } from "./jingwan-enable.svg";
13+
export { default as MinusIcon } from "./minus.svg";
1314
export { default as MyDisableIcon } from "./my-disable.svg";
1415
export { default as MyEnableIcon } from "./my-enable.svg";
15-
export { default as ShoppingBag } from "./shopping-bag.svg";
16+
export { default as PlusIcon } from "./plus.svg";
17+
export { default as PlusDisableIcon } from "./plus-disable.svg";
18+
export { default as ShoppingBagIcon } from "./shopping-bag.svg";
19+
export { default as TrashIcon } from "./trash.svg";

src/assets/icons/minus.svg

Lines changed: 3 additions & 0 deletions
Loading

src/assets/icons/plus-disable.svg

Lines changed: 3 additions & 0 deletions
Loading

src/assets/icons/plus.svg

Lines changed: 3 additions & 0 deletions
Loading

src/assets/icons/trash.svg

Lines changed: 3 additions & 0 deletions
Loading
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
.container {
2+
display: flex;
3+
justify-content: center;
4+
align-content: center;
5+
6+
padding-block: 0.44rem;
7+
padding-inline: 0.5rem;
8+
9+
border-radius: 0.25rem;
10+
border: 1px solid var(--color-gray-200);
11+
background-color: var(--color-white);
12+
}
13+
14+
.wrapper {
15+
display: flex;
16+
justify-content: space-between;
17+
align-items: center;
18+
}
19+
20+
.label {
21+
color: var(--color-black);
22+
font-size: var(--text-xs);
23+
font-style: normal;
24+
font-weight: var(--font-medium);
25+
line-height: var(--line-height-single);
26+
letter-spacing: var(--letter-spacing);
27+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
"use client";
2+
3+
import styles from "./QuantityStepper.module.css";
4+
import { MinusIcon, PlusDisableIcon, PlusIcon, TrashIcon } from "@/assets/icons";
5+
import { useCart } from "@/features/cart/hooks/useCart";
6+
import { useToast } from "@/shared/hooks/useToast";
7+
8+
interface QuantityStepperProps {
9+
menuId: number;
10+
quantity: number;
11+
}
12+
13+
export default function QuantityStepper({
14+
menuId,
15+
quantity
16+
}: QuantityStepperProps) {
17+
const { addToCart, removeFromCart, isAddingToCart } = useCart();
18+
const { showToast } = useToast();
19+
20+
const handleIncrement = () => {
21+
if (quantity >= 9) {
22+
showToast("메뉴는 최대 9개까지만 담을 수 있어요!");
23+
return;
24+
}
25+
addToCart(menuId, {
26+
onError: (message) => showToast(message)
27+
});
28+
}
29+
30+
const handleDecrement = () => {
31+
if (quantity <= 0) {
32+
return;
33+
}
34+
35+
removeFromCart(menuId, {
36+
onError: (message) => showToast(message)
37+
});
38+
}
39+
40+
const decrementIcon = quantity === 1 ? <TrashIcon /> : <MinusIcon />;
41+
const incrementIcon = quantity !== 9 ? <PlusIcon /> : <PlusDisableIcon />;
42+
const isPlusDisabled = quantity >= 9;
43+
44+
return (
45+
<div className={styles.container}>
46+
<div className={styles.wrapper}>
47+
<button
48+
onClick={handleDecrement}
49+
disabled={isAddingToCart}
50+
>
51+
{decrementIcon}
52+
</button>
53+
<div className={styles.label}>
54+
{quantity}
55+
</div>
56+
<button
57+
onClick={handleIncrement}
58+
disabled={isPlusDisabled}
59+
>
60+
{incrementIcon}
61+
</button>
62+
</div>
63+
</div>
64+
);
65+
}

src/features/cart/hooks/useCart.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ interface UseCartReturn {
1414
onSuccess?: () => void;
1515
onError?: (message: string) => void;
1616
}) => void;
17+
removeFromCart: (menuId: number, callbacks?: {
18+
onSuccess?: () => void;
19+
onError?: (message: string) => void;
20+
}) => void;
1721
isAddingToCart: boolean;
1822
}
1923

@@ -54,6 +58,7 @@ export function useCart(): UseCartReturn {
5458
/**
5559
* 메뉴 1개 추가 (기존 수량 + 1)
5660
* - 버튼 연타 방지
61+
* - 각 메뉴는 최대 9개까지
5762
*/
5863
const addToCart = useCallback((
5964
menuId: number,
@@ -97,12 +102,61 @@ export function useCart(): UseCartReturn {
97102
pendingRequests.current.set(menuId, requestPromise);
98103
}, [getMenuQuantity, upsertCartMutation]);
99104

105+
/**
106+
* 메뉴 1개 감소 (기존 수량 - 1)
107+
* - 수량이 1이면 장바구니에서 삭제 (수량 0으로 설정)
108+
* - 버튼 연타 방지
109+
*/
110+
const removeFromCart = useCallback((
111+
menuId: number,
112+
callbacks?: {
113+
onSuccess?: () => void;
114+
onError?: (message: string) => void;
115+
}
116+
) => {
117+
// 중복 요청 방지
118+
if (pendingRequests.current.has(menuId)) {
119+
return;
120+
}
121+
122+
// 특정 메뉴 수량 조회
123+
const menuQuantity = getMenuQuantity(menuId);
124+
125+
// 수량이 0이면 실행 중지
126+
if (menuQuantity === 0) {
127+
return;
128+
}
129+
130+
const newMenuQuantity = menuQuantity - 1;
131+
132+
const requestPromise = new Promise<void>((resolve, reject) => {
133+
upsertCartMutation.mutate(
134+
{ menuId, quantity: newMenuQuantity },
135+
{
136+
onSuccess: () => {
137+
pendingRequests.current.delete(menuId);
138+
callbacks?.onSuccess?.();
139+
resolve();
140+
},
141+
onError: (error) => {
142+
pendingRequests.current.delete(menuId);
143+
callbacks?.onError?.("장바구니 수정에 실패했어요. 잠시 후 다시 시도해주세요.");
144+
reject(error);
145+
},
146+
}
147+
);
148+
});
149+
150+
pendingRequests.current.set(menuId, requestPromise);
151+
}, [getMenuQuantity, upsertCartMutation]);
152+
100153
return {
101154
cartInfo,
102155
isLoading,
103156
error,
104157
getMenuQuantity,
105158
addToCart,
159+
removeFromCart,
106160
isAddingToCart: upsertCartMutation.isPending,
107161
};
108162
}

src/features/menu/components/button/CartButton.module.css

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,6 @@
1919
justify-content: center;
2020
}
2121

22-
.icon {
23-
width: 1.25rem;
24-
height: 1.25rem;
25-
}
26-
2722
.label {
2823
color: var(--color-white);
2924
font-size: var(--text-base);

0 commit comments

Comments
 (0)