Skip to content
Merged
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ This file is automatically generated and maintained by the centralized workflow

# Chuseok22 Version Changelog

## [0.4.1] - 2026-01-02

🐛 **patch**: 하단 네비게이션을 통한 페이지 이동 시 replace 동작
- commit: `05a52d7`

## [0.4.0] - 2026-01-02

✨ **minor**: 학생회관 메뉴 페이지 장바구니 추가 기능 개발
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "campus-table-fe",
"version": "0.4.0",
"version": "0.4.1",
"private": true,
"scripts": {
"dev": "next dev",
Expand Down
2 changes: 1 addition & 1 deletion src/constants/version.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export const APP_VERSION: string = '0.4.0';
export const APP_VERSION: string = '0.4.1';
16 changes: 4 additions & 12 deletions src/features/menu/components/MenuPageContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { useCart } from "@/features/cart/hooks/useCart";
import OrderSummaryBar from "@/features/menu/components/bar/OrderSummaryBar";
import { ItemCount } from "@/features/menu/components/button/CartButton";
import CartToast from "@/features/menu/components/toast/CartToast";
import { useToast } from "@/shared/hooks/useToast";

interface MenuPageContainerProps {
hakgwanMenuData: HakgwanMenuData;
Expand All @@ -26,8 +27,7 @@ export default function MenuPageContainer({
return 0;
});

const [toastVisible, setToastVisible] = useState<boolean>(false);
const [toastMessage, setToastMessage] = useState<string>("");
const { visible: toastVisible, message: toastMessage, showToast } = useToast(3000);

const { cartInfo, addToCart } = useCart();

Expand All @@ -47,18 +47,10 @@ export default function MenuPageContainer({
const handleAddToCart = (menuId: number): void => {
addToCart(menuId, {
onSuccess: () => {
setToastMessage("장바구니에 쏙 담았어요!");
setToastVisible(true);
setTimeout(() => {
setToastVisible(false);
}, 3000);
showToast("장바구니에 쏙 담았어요!");
},
onError: (message) => {
setToastMessage(message);
setToastVisible(true);
setTimeout(() => {
setToastVisible(false);
}, 3000);
showToast(message);
}
});
};
Expand Down
83 changes: 83 additions & 0 deletions src/shared/hooks/useToast.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
"use client";

import { useCallback, useEffect, useRef, useState } from "react";

interface UseToastReturn {
visible: boolean;
message: string;
showToast: (message: string) => void;
}

/**
* 토스트 메시지 관리 훅
* - 타이머 충돌 방지
* - 기존 토스트가 있으면 새로운 토스트로 교체
*/
export function useToast(
duration: number = 3000,
animationDuration: number = 200,
): UseToastReturn {
const [visible, setVisible] = useState<boolean>(false);
const [message, setMessage] = useState<string>("");
const timerRef = useRef<number | null>(null);
const hideTimerRef = useRef<number | null>(null);
const visibleRef = useRef<boolean>(false);

const showToast = useCallback((message: string) => {
// 기존 타이머 정리
if (timerRef.current) {
window.clearTimeout(timerRef.current);
}
if (hideTimerRef.current) {
window.clearTimeout(hideTimerRef.current);
}

// 이미 토스트가 표시중이면 먼저 제거
if (visibleRef.current) {
// 기존 토스트 숨김
setVisible(false);
visibleRef.current = false;

// 애니메이션 완료 대기
hideTimerRef.current = window.setTimeout(() => {
setMessage(message);
setVisible(true);
visibleRef.current = true;

// duration 후 토스트 숨김
timerRef.current = window.setTimeout(() => {
setVisible(false);
visibleRef.current = false;
timerRef.current = null;
}, duration);

hideTimerRef.current = null;
}, animationDuration);
} else {
// 기존 토스트가 없는 경우
setMessage(message);
setVisible(true);
visibleRef.current = true;

timerRef.current = window.setTimeout(() => {
setVisible(false);
visibleRef.current = false;
timerRef.current = null;
}, duration);
}
Comment on lines +36 to +67
Copy link

Copilot AI Jan 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a potential race condition between the visible state and visibleRef. When showToast is called while a toast is already visible, the code sets both setVisible(false) and visibleRef.current = false at lines 38-39, then schedules state updates in a setTimeout. If showToast is called again during the animation delay (before line 42's setTimeout executes), the visibleRef check at line 36 will be false (because it was set to false at line 39), causing the else branch to execute and potentially creating a race condition with the pending setTimeout. Consider using a more robust state machine or queueing mechanism to handle rapid consecutive calls.

Copilot uses AI. Check for mistakes.
}, [duration, animationDuration]);
Comment on lines +26 to +68
Copy link

Copilot AI Jan 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The showToast callback function depends on duration and animationDuration, which means it will be recreated whenever these values change. However, since duration and animationDuration are hook parameters that could change between renders, this could cause unnecessary recreations of the callback. Consider removing duration and animationDuration from the dependency array and using refs instead, or add a comment explaining why they should trigger callback recreation.

Copilot uses AI. Check for mistakes.

// 컴포넌트 언마운트 시 타이머 정리
useEffect(() => {
return () => {
if (timerRef.current) {
window.clearTimeout(timerRef.current);
}
if (hideTimerRef.current) {
window.clearTimeout(hideTimerRef.current);
}
};
}, []);

return { visible, message, showToast };
}