[250930] feat: cart관련 redux추가 , BuyOption 포커스 수정

This commit is contained in:
2025-09-30 12:45:28 +09:00
parent ecb8a31284
commit 1108c1c525
8 changed files with 399 additions and 13 deletions

View File

@@ -59,6 +59,13 @@ export const types = {
// shipping actions
GET_MY_INFO_SHIPPING_SEARCH: "GET_MY_INFO_SHIPPING_SEARCH",
// cart actions
GET_MY_INFO_CART_SEARCH: "GET_MY_INFO_CART_SEARCH",
ADD_TO_CART: "ADD_TO_CART",
REMOVE_FROM_CART: "REMOVE_FROM_CART",
UPDATE_CART_ITEM: "UPDATE_CART_ITEM",
CLEAR_CART: "CLEAR_CART",
// appData actions
ADD_MAIN_INDEX: "ADD_MAIN_INDEX",
SEND_SMS: "SEND_SMS",

View File

@@ -0,0 +1,193 @@
import { URLS } from "../api/apiConfig";
import { TAxios } from "../api/TAxios";
import { types } from "./actionTypes";
/**
* 회원 장바구니 정보 조회
* @param {Object} props - { mbrNo: 회원번호 }
*/
export const getMyInfoCartSearch = (props) => (dispatch, getState) => {
const { mbrNo } = props;
const onSuccess = (response) => {
console.log("getMyInfoCartSearch onSuccess: ", response.data);
dispatch({
type: types.GET_MY_INFO_CART_SEARCH,
payload: response.data.data,
});
};
const onFail = (error) => {
console.error("getMyInfoCartSearch OnFail: ", error);
// 실패 시에도 빈 데이터로 초기화
dispatch({
type: types.GET_MY_INFO_CART_SEARCH,
payload: { cartList: [] },
});
};
// API URL이 정의되어 있지 않은 경우 임시로 빈 데이터 반환
if (!URLS.GET_MY_INFO_CART_SEARCH) {
console.warn("GET_MY_INFO_CART_SEARCH URL이 정의되지 않았습니다.");
dispatch({
type: types.GET_MY_INFO_CART_SEARCH,
payload: { cartList: [] },
});
return;
}
TAxios(
dispatch,
getState,
"get",
URLS.GET_MY_INFO_CART_SEARCH,
{ mbrNo },
{},
onSuccess,
onFail
);
};
/**
* 장바구니에 상품 추가
* @param {Object} props - 장바구니 상품 정보
*/
export const addToCart = (props) => (dispatch, getState) => {
const { mbrNo, patnrId, prdtId, prodOptCdCval, prodQty } = props;
const onSuccess = (response) => {
console.log("addToCart onSuccess: ", response.data);
dispatch({
type: types.ADD_TO_CART,
payload: response.data.data,
});
// 장바구니 목록 재조회
dispatch(getMyInfoCartSearch({ mbrNo }));
};
const onFail = (error) => {
console.error("addToCart OnFail: ", error);
};
// API URL이 정의되어 있지 않은 경우 로컬 상태만 업데이트
if (!URLS.ADD_TO_CART) {
console.warn("ADD_TO_CART URL이 정의되지 않았습니다.");
dispatch({
type: types.ADD_TO_CART,
payload: { patnrId, prdtId, prodOptCdCval, prodQty },
});
return;
}
TAxios(
dispatch,
getState,
"post",
URLS.ADD_TO_CART,
{},
{ mbrNo, patnrId, prdtId, prodOptCdCval, prodQty },
onSuccess,
onFail
);
};
/**
* 장바구니에서 상품 제거
* @param {Object} props - { mbrNo, cartSno }
*/
export const removeFromCart = (props) => (dispatch, getState) => {
const { mbrNo, cartSno } = props;
const onSuccess = (response) => {
console.log("removeFromCart onSuccess: ", response.data);
dispatch({
type: types.REMOVE_FROM_CART,
payload: { cartSno },
});
// 장바구니 목록 재조회
dispatch(getMyInfoCartSearch({ mbrNo }));
};
const onFail = (error) => {
console.error("removeFromCart OnFail: ", error);
};
if (!URLS.REMOVE_FROM_CART) {
console.warn("REMOVE_FROM_CART URL이 정의되지 않았습니다.");
dispatch({
type: types.REMOVE_FROM_CART,
payload: { cartSno },
});
return;
}
TAxios(
dispatch,
getState,
"delete",
URLS.REMOVE_FROM_CART,
{ mbrNo, cartSno },
{},
onSuccess,
onFail
);
};
/**
* 장바구니 상품 수량 업데이트
* @param {Object} props - { mbrNo, cartSno, prodQty }
*/
export const updateCartItem = (props) => (dispatch, getState) => {
const { mbrNo, cartSno, prodQty } = props;
const onSuccess = (response) => {
console.log("updateCartItem onSuccess: ", response.data);
dispatch({
type: types.UPDATE_CART_ITEM,
payload: { cartSno, prodQty },
});
// 장바구니 목록 재조회
dispatch(getMyInfoCartSearch({ mbrNo }));
};
const onFail = (error) => {
console.error("updateCartItem OnFail: ", error);
};
if (!URLS.UPDATE_CART_ITEM) {
console.warn("UPDATE_CART_ITEM URL이 정의되지 않았습니다.");
dispatch({
type: types.UPDATE_CART_ITEM,
payload: { cartSno, prodQty },
});
return;
}
TAxios(
dispatch,
getState,
"put",
URLS.UPDATE_CART_ITEM,
{},
{ mbrNo, cartSno, prodQty },
onSuccess,
onFail
);
};
/**
* 장바구니 전체 비우기
*/
export const clearCart = () => (dispatch) => {
dispatch({
type: types.CLEAR_CART,
});
};

View File

@@ -19,6 +19,12 @@ export const URLS = {
// card controller
GET_MY_INFO_CARD_SEARCH: "/lgsp/v1/myinfo/card/search.lge",
// cart controller
GET_MY_INFO_CART_SEARCH: "/lgsp/v1/myinfo/cart/search.lge",
ADD_TO_CART: "/lgsp/v1/myinfo/cart/add.lge",
REMOVE_FROM_CART: "/lgsp/v1/myinfo/cart/remove.lge",
UPDATE_CART_ITEM: "/lgsp/v1/myinfo/cart/update.lge",
// shipping controller
GET_MY_INFO_SHIPPING_SEARCH: "/lgsp/v1/myinfo/address/shipping/search.lge",

View File

@@ -61,17 +61,34 @@ export default function TToastEnhanced({
// BuyOption 타입일 때 전역 포커스 감지
useEffect(() => {
if (type === 'buyOption') {
// BuyOption이 포커스를 받았는지 추적하는 플래그
let hasBuyOptionReceivedFocus = false;
const handleFocusChange = (e) => {
// BuyOption 컨테이너 외부로 포커스가 이동했는지 확인
if (buyOptionRef.current && !buyOptionRef.current.contains(e.target)) {
console.log('[TToastEnhanced] Focus moved outside BuyOption toast - closing');
// 1. BuyOption 부로 포커스가 들어온 경우 - 플래그를 true로 설정
if (buyOptionRef.current && buyOptionRef.current.contains(e.target)) {
if (!hasBuyOptionReceivedFocus) {
hasBuyOptionReceivedFocus = true;
console.log('[TToastEnhanced] BuyOption received focus - now tracking focus leaving');
}
return; // 내부에 포커스가 있으면 아무것도 하지 않음
}
// 2. BuyOption이 포커스를 받은 적이 있고, 현재 외부로 포커스가 이동한 경우 - Toast 닫기
if (hasBuyOptionReceivedFocus &&
buyOptionRef.current &&
!buyOptionRef.current.contains(e.target)) {
console.log('[TToastEnhanced] Focus left BuyOption after receiving focus - closing toast');
handleClose();
}
};
// focusin 이벤트로 포커스 변경 감지
document.addEventListener('focusin', handleFocusChange);
return () => document.removeEventListener('focusin', handleFocusChange);
return () => {
document.removeEventListener('focusin', handleFocusChange);
};
}
}, [type]);

View File

@@ -0,0 +1,88 @@
import { types } from "../actions/actionTypes";
/**
* Cart Reducer 초기 상태
*/
const initialState = {
// 장바구니 조회 데이터
getMyinfoCartSearch: {
cartList: [],
totalCount: 0,
},
// 추가/수정/삭제 결과
lastAction: null,
error: null,
};
/**
* Cart Reducer
* 장바구니 관련 상태를 관리합니다.
*/
export const cartReducer = (state = initialState, action) => {
switch (action.type) {
// 장바구니 조회
case types.GET_MY_INFO_CART_SEARCH:
return {
...state,
getMyinfoCartSearch: action.payload || {
cartList: [],
totalCount: 0,
},
error: null,
};
// 장바구니에 상품 추가
case types.ADD_TO_CART:
return {
...state,
lastAction: {
type: "add",
data: action.payload,
timestamp: Date.now(),
},
error: null,
};
// 장바구니에서 상품 제거
case types.REMOVE_FROM_CART:
return {
...state,
lastAction: {
type: "remove",
data: action.payload,
timestamp: Date.now(),
},
error: null,
};
// 장바구니 상품 수량 업데이트
case types.UPDATE_CART_ITEM:
return {
...state,
lastAction: {
type: "update",
data: action.payload,
timestamp: Date.now(),
},
error: null,
};
// 장바구니 전체 비우기
case types.CLEAR_CART:
return {
...state,
getMyinfoCartSearch: {
cartList: [],
totalCount: 0,
},
lastAction: {
type: "clear",
timestamp: Date.now(),
},
error: null,
};
default:
return state;
}
};

View File

@@ -10,6 +10,7 @@ import { billingReducer } from '../reducers/billingReducer';
import { brandReducer } from '../reducers/brandReducer';
import { cancelReducer } from '../reducers/cancelReducer';
import { cardReducer } from '../reducers/cardReducer';
import { cartReducer } from '../reducers/cartReducer';
import { checkoutReducer } from '../reducers/checkoutReducer';
import { commonReducer } from '../reducers/commonReducer';
import { couponReducer } from '../reducers/couponReducer';
@@ -48,6 +49,7 @@ const rootReducer = combineReducers({
event: eventReducer,
coupon: couponReducer,
card: cardReducer,
cart: cartReducer,
billing: billingReducer,
shipping: shippingReducer,
checkout: checkoutReducer,

View File

@@ -8,6 +8,7 @@ import {
useSelector,
} from 'react-redux';
import { getMyInfoCartSearch } from '../../actions/cartActions';
import { popPanel } from '../../actions/panelActions';
import TBody from '../../components/TBody/TBody';
import THeader from '../../components/THeader/THeader';
@@ -21,12 +22,22 @@ import CartSidebar from './CartSidebar';
export default function CartPanel({ spotlightId, scrollOptions = [] }) {
const cartData = useSelector((state) => state.cart.getMyinfoCartSearch);
const { userNumber } = useSelector(
(state) => state.common.appStatus.loginUserData
);
const dispatch = useDispatch();
const onBackClick = useCallback(() => {
dispatch(popPanel());
}, [dispatch]);
// 장바구니 데이터 로드
useEffect(() => {
if (userNumber) {
dispatch(getMyInfoCartSearch({ mbrNo: userNumber }));
}
}, [dispatch, userNumber]);
useEffect(() => {
console.log("###cartData", cartData);
}, [cartData]);

View File

@@ -14,6 +14,7 @@ import Spotlight from '@enact/spotlight';
import SpotlightContainerDecorator
from '@enact/spotlight/SpotlightContainerDecorator';
import { addToCart } from '../../../actions/cartActions';
import { getMyInfoCheckoutInfo } from '../../../actions/checkoutActions';
import {
changeAppStatus,
@@ -365,16 +366,77 @@ const BuyOption = ({
logInfo,
]);
// [임시] ADD TO CART 버튼 클릭 시 toast 닫기
const handleAddToCartClick = () => {
console.log("[BuyOption] ADD TO CART clicked - closing toast (임시)");
// ADD TO CART 버튼 클릭 핸들러
const handleAddToCartClick = useCallback(() => {
console.log("[BuyOption] ADD TO CART clicked");
// 상품 품절 체크
if (productInfo && productInfo?.soldoutFlag === "Y") {
return;
}
// 로그인 체크
if (!userNumber || userNumber === "") {
return dispatch(setShowPopup(Config.ACTIVE_POPUP.loginPopup));
}
// 장바구니에 상품 추가
if (userNumber && selectedPatnrId && selectedPrdtId && quantity) {
const { prodOptCval, priceInfo } = selectedOptions || {};
const { patncNm, brndNm, catNm, prdtNm, prdtId } = productInfo;
const regularPrice = priceInfo?.split("|")[0];
const discountPrice = priceInfo?.split("|")[1];
const discountRate = priceInfo?.split("|")[4];
// 로그 전송
dispatch(
sendLogTotalRecommend({
nowMenu: nowMenu,
productId: prdtId,
productTitle: prdtNm,
partner: patncNm,
price: discountRate ? discountPrice : regularPrice,
discount: discountRate,
brand: brndNm,
productOption: prodOptCval || "",
category: catNm,
contextName: Config.LOG_CONTEXT_NAME.DETAILPAGE,
messageId: Config.LOG_MESSAGE_ID.ADD_TO_CART,
})
);
// 장바구니에 추가
dispatch(
addToCart({
mbrNo: userNumber,
patnrId: selectedPatnrId,
prdtId: selectedPrdtId,
prodOptCdCval: selectedOptions?.prodOptCdCval || null,
prodQty: quantity,
prodOptTpCdCval: productOptionInfos[0]?.prodOptTpCdCval,
})
);
// CartPanel로 이동
dispatch(
pushPanel({
name: Config.panel_names.CART_PANEL,
})
);
}
dispatch(clearAllToasts());
dispatch(
pushPanel({
name: Config.panel_names.CART_PANEL,
})
);
};
}, [
dispatch,
userNumber,
selectedPatnrId,
selectedPrdtId,
selectedOptions,
productInfo,
productOptionInfos,
quantity,
nowMenu,
]);
// 첫번째 옵션 선택 핸들러 (SingleOption과 동일)
const handleFirstOptionSelect = (selected) => {