diff --git a/com.twin.app.shoptime/src/actions/actionTypes.js b/com.twin.app.shoptime/src/actions/actionTypes.js index 68a17a80..dad90bd3 100644 --- a/com.twin.app.shoptime/src/actions/actionTypes.js +++ b/com.twin.app.shoptime/src/actions/actionTypes.js @@ -57,6 +57,7 @@ export const types = { // home actions GET_HOME_TERMS: "GET_HOME_TERMS", + SET_TERMS_ID_MAP: "SET_TERMS_ID_MAP", GET_HOME_MENU: "GET_HOME_MENU", GET_HOME_LAYOUT: "GET_HOME_LAYOUT", GET_HOME_MAIN_CONTENTS: "GET_HOME_MAIN_CONTENTS", diff --git a/com.twin.app.shoptime/src/actions/homeActions.js b/com.twin.app.shoptime/src/actions/homeActions.js index 8d61cb03..bfb8c26e 100644 --- a/com.twin.app.shoptime/src/actions/homeActions.js +++ b/com.twin.app.shoptime/src/actions/homeActions.js @@ -1,5 +1,5 @@ import { URLS } from "../api/apiConfig"; -import { TAxios } from "../api/TAxios"; +import { TAxios,TAxiosPromise } from "../api/TAxios"; import { types } from "./actionTypes"; import { changeAppStatus, getTermsAgreeYn } from "./commonActions"; @@ -15,6 +15,26 @@ export const getHomeTerms = (props) => (dispatch, getState) => { type: types.GET_HOME_TERMS, payload: response.data, }); + + // 약관 ID 매핑을 별도로 생성하여 저장 + if (response.data?.data?.terms) { + const termsIdMap = {}; + response.data.data.terms.forEach(term => { + if (term.trmsTpCd && term.trmsId) { + termsIdMap[term.trmsTpCd] = term.trmsId; + } + }); + + dispatch({ + type: types.SET_TERMS_ID_MAP, + payload: termsIdMap, + }); + + if (process.env.NODE_ENV === "development") { + console.log("약관 ID 매핑 생성:", termsIdMap); + } + } + setTimeout(() => { dispatch(getTermsAgreeYn()); }, 0); @@ -58,6 +78,26 @@ export const fetchCurrentUserHomeTerms = () => (dispatch, getState) => { type: types.GET_HOME_TERMS, // 기존 GET_HOME_TERMS 타입을 재사용 payload: response.data, }); + + // 약관 ID 매핑을 별도로 생성하여 저장 + if (response.data?.data?.terms) { + const termsIdMap = {}; + response.data.data.terms.forEach(term => { + if (term.trmsTpCd && term.trmsId) { + termsIdMap[term.trmsTpCd] = term.trmsId; + } + }); + + dispatch({ + type: types.SET_TERMS_ID_MAP, + payload: termsIdMap, + }); + + if (process.env.NODE_ENV === "development") { + console.log("약관 ID 매핑 생성:", termsIdMap); + } + } + // getHomeTerms와 동일하게 getTermsAgreeYn 후속 처리 setTimeout(() => { dispatch(getTermsAgreeYn()); @@ -85,6 +125,87 @@ export const fetchCurrentUserHomeTerms = () => (dispatch, getState) => { ); }; +// 기존 TAxios 패턴과 일치하는 안전한 Redux Action +export const fetchCurrentUserHomeTermsSafe = () => async (dispatch, getState) => { + const loginUserData = getState().common.appStatus.loginUserData; + + if (!loginUserData || !loginUserData.userNumber) { + console.error("fetchCurrentUserHomeTerms: userNumber is not available"); + dispatch({ type: types.GET_TERMS_AGREE_YN_FAILURE }); + return { success: false, message: "사용자 정보가 없습니다." }; + } + + const mbrNo = loginUserData.userNumber; + const trmsTpCdList = "MST00401, MST00402, MST00405"; + + console.log("Fetching home terms for user:", mbrNo); + + // 안전한 API 호출 (기존 TAxios 패턴과 동일) + const result = await TAxiosPromise( + dispatch, + getState, + "get", + URLS.GET_HOME_TERMS, + { trmsTpCdList, mbrNo } + ); + + // 네트워크 에러인 경우 + if (!result.success) { + console.error("fetchCurrentUserHomeTerms network error:", result.error); + dispatch({ type: types.GET_TERMS_AGREE_YN_FAILURE }); + return { success: false, message: "네트워크 오류가 발생했습니다." }; + } + + // 기존 TAxios처럼 특별한 retCode들은 TAxios 내부에서 이미 처리됨 + // (401, 402, 501, 602, 603, 604 등은 TAxios에서 알아서 처리하고 onSuccess가 호출되지 않음) + + console.log("fetchCurrentUserHomeTerms response:", result.data); + + // 정상적으로 onSuccess가 호출된 경우에만 여기까지 옴 + if (result.data && result.data.retCode === 0) { + dispatch({ + type: types.GET_HOME_TERMS, + payload: result.data, + }); + + // 약관 ID 매핑을 별도로 생성하여 저장 + if (result.data?.data?.terms) { + const termsIdMap = {}; + result.data.data.terms.forEach(term => { + if (term.trmsTpCd && term.trmsId) { + termsIdMap[term.trmsTpCd] = term.trmsId; + } + }); + + dispatch({ + type: types.SET_TERMS_ID_MAP, + payload: termsIdMap, + }); + + if (process.env.NODE_ENV === "development") { + console.log("약관 ID 매핑 생성:", termsIdMap); + } + } + + // 후속 액션 호출 (기존과 동일) + setTimeout(() => { + dispatch(getTermsAgreeYn()); + }, 0); + + return { success: true, data: result.data }; + } else { + // retCode가 0이 아닌 일반적인 API 에러 + console.error("API returned non-zero retCode:", result.data?.retCode); + dispatch({ type: types.GET_TERMS_AGREE_YN_FAILURE }); + return { + success: false, + message: result.data?.retMsg || "서버 오류가 발생했습니다." + }; + } +}; + + + // 메뉴 목록 조회 IF-LGSP-044 export const getHomeMenu = () => (dispatch, getState) => { const onSuccess = (response) => { diff --git a/com.twin.app.shoptime/src/components/TButtonTab/TButtonTab.module.less b/com.twin.app.shoptime/src/components/TButtonTab/TButtonTab.module.less index 99ef1ad9..cd721a55 100644 --- a/com.twin.app.shoptime/src/components/TButtonTab/TButtonTab.module.less +++ b/com.twin.app.shoptime/src/components/TButtonTab/TButtonTab.module.less @@ -2,7 +2,7 @@ @import "../../style/utils.module.less"; .tabs { - .size(@w: 1680px, @h: 67px); + .size(@w: 1680px, @h: 67px); display: flex; position: relative; diff --git a/com.twin.app.shoptime/src/reducers/homeReducer.js b/com.twin.app.shoptime/src/reducers/homeReducer.js index cc9eeec0..3b61fa27 100644 --- a/com.twin.app.shoptime/src/reducers/homeReducer.js +++ b/com.twin.app.shoptime/src/reducers/homeReducer.js @@ -25,6 +25,7 @@ const initialState = { ownerId: null, isPaused: false, }, + termsIdMap: {}, // added new property to initialState }; export const homeReducer = (state = initialState, action) => { @@ -35,6 +36,12 @@ export const homeReducer = (state = initialState, action) => { termsData: action.payload, }; + case types.SET_TERMS_ID_MAP: + return { + ...state, + termsIdMap: action.payload, + }; + case types.GET_HOME_MENU: { let menuItems = []; diff --git a/com.twin.app.shoptime/src/views/HomePanel/HomeBanner/HomeBanner.jsx b/com.twin.app.shoptime/src/views/HomePanel/HomeBanner/HomeBanner.jsx index 5e96fdf6..aa92ce3b 100644 --- a/com.twin.app.shoptime/src/views/HomePanel/HomeBanner/HomeBanner.jsx +++ b/com.twin.app.shoptime/src/views/HomePanel/HomeBanner/HomeBanner.jsx @@ -96,6 +96,7 @@ export default function HomeBanner({ }, [dispatch]); const termsData = useSelector((state) => state.home.termsData); + const termsIdMap = useSelector((state) => state.home.termsIdMap); const optionalTermsData = useSelector((state) => state.home.termsData?.data?.terms.find( (term) => term.trmsTpCd === "MST00405", @@ -128,35 +129,76 @@ export default function HomeBanner({ }, [termsData, termsLoading, isGnbOpened]); const handleOptionalAgree = useCallback(() => { - console.log("handleAgree Click"); + if (process.env.NODE_ENV === "development") { + console.log("[HomeBanner] handleAgree Click"); + } + + if (!termsIdMap || Object.keys(termsIdMap).length === 0) { + if (process.env.NODE_ENV === "development") { + console.error("[HomeBanner] termsIdMap이 없습니다:", termsIdMap); + } + return; + } + + const requiredTermTypes = ["MST00401", "MST00402", "MST00405"]; + const missingTerms = requiredTermTypes.filter( + (type) => !termsIdMap[type], + ); + + if (missingTerms.length > 0) { + if (process.env.NODE_ENV === "development") { + console.error("[HomeBanner] 누락된 약관 타입:", missingTerms); + } + return; + } + + const termsList = []; + + if (termsIdMap["MST00401"]) { + termsList.push(termsIdMap["MST00401"]); // 개인정보처리방침 + } + if (termsIdMap["MST00402"]) { + termsList.push(termsIdMap["MST00402"]); // 이용약관 + } + if (termsIdMap["MST00405"]) { + termsList.push(termsIdMap["MST00405"]); // 선택약관 + } - // 약관 동의할 항목들 (string array) - const termsList = ["TID0000222", "TID0000223", "TID0000232"]; - // 동의하지 않을 항목들 (빈 배열) const notTermsList = []; - console.log("OptionalTermsConfirm -약관 동의 API 호출 파라미터:", { - termsList, - notTermsList, - }); + + if (process.env.NODE_ENV === "development") { + console.log("[HomeBanner] 현재 termsIdMap:", termsIdMap); + console.log("[HomeBanner] 약관 동의 API 호출 파라미터:", { + termsList, + notTermsList, + }); + } + const callback = (response) => { if (response.retCode === "000" || response.retCode === 0) { - console.log("약관 동의 성공:", response); - // 약관 정보 갱신 + if (process.env.NODE_ENV === "development") { + console.log("[HomeBanner] 약관 동의 성공:", response); + } dispatch(fetchCurrentUserHomeTerms()); } else { - console.error("약관 동의 실패:", response); + if (process.env.NODE_ENV === "development") { + console.error("[HomeBanner] 약관 동의 실패:", response); + } } }; - console.log("OptionalTermsConfirm - 약관 동의 API 호출 payload:", { - termsList, - notTermsList, - }); + if (process.env.NODE_ENV === "development") { + console.log("[HomeBanner] 약관 동의 API 호출 payload:", { + termsList, + notTermsList, + }); + } + dispatch(setMyPageTermsAgree({ termsList, notTermsList }, callback)); - }, [dispatch]); + }, [dispatch, termsIdMap]); const handleOptionalTermsClick = useCallback(() => { - console.log("약관 자세히 보기 클릭"); + console.log("[HomeBanner] 약관 자세히 보기 클릭"); setIsOptionalConfirmVisible(false); setIsOptionalTermsVisible(true); // 약관 상세 팝업을 띄우는 로직 추가 @@ -165,10 +207,10 @@ export default function HomeBanner({ const handleOptionalAgreeClick = useCallback(() => { handleOptionalAgree(); setIsOptionalConfirmVisible(false); - }, []); + }, [handleOptionalAgree]); const handleOptionalDeclineClick = useCallback(() => { - console.log("거절/다음에 하기 버튼 클릭"); + console.log("[HomeBanner] 거절/다음에 하기 버튼 클릭"); setIsOptionalConfirmVisible(false); // 거절 처리 로직 추가 }, []); @@ -182,10 +224,10 @@ export default function HomeBanner({ // 선택약관 팝업 Agree const handleTermsPopupAgree = useCallback(() => { - console.log("handleTermsPopupAgree"); + console.log("[HomeBanner] handleTermsPopupAgree"); handleOptionalAgree(); setIsOptionalTermsVisible(false); - }, []); + }, [handleOptionalAgree]); //------------------------------------------------------------------------------ const _handleShelfFocus = useCallback(() => { diff --git a/com.twin.app.shoptime/src/views/IntroPanel/IntroPanel.new.jsx b/com.twin.app.shoptime/src/views/IntroPanel/IntroPanel.new.jsx index 34f06d75..a052fee3 100644 --- a/com.twin.app.shoptime/src/views/IntroPanel/IntroPanel.new.jsx +++ b/com.twin.app.shoptime/src/views/IntroPanel/IntroPanel.new.jsx @@ -64,6 +64,7 @@ export default function IntroPanel({ const dispatch = useDispatch(); // const blurTimeout = useRef(null); const termsData = useSelector((state) => state.home.termsData); + const termsIdMap = useSelector((state) => state.home.termsIdMap); const { popupVisible, activePopup, ...popupState } = useSelector( (state) => state.common.popup, ); @@ -76,6 +77,8 @@ export default function IntroPanel({ // registerDevice API 호출 중 여부 const [isProcessing, setIsProcessing] = useState(false); const [showExitMessagePopup, setShowExitMessagePopup] = useState(false); + // race condition 방지를 위한 안전장치 + const processingTimeoutRef = useRef(null); // const [isRequiredFocused, setIsRequiredFocused] = useState(false); const { focusedItem, setFocusAsync, clearFocusAsync } = useSafeFocusState(); @@ -262,7 +265,10 @@ export default function IntroPanel({ }, [dispatch]); const handleAgree = useCallback(() => { - if (isProcessing) return; + console.log("[IntroPanel] handleAgree isProcessing=", isProcessing); + if (isProcessing){ + return; + } // 필수 약관이 체크되어 있는지 확인 // if (!termsChecked || !privacyChecked) { @@ -277,24 +283,32 @@ export default function IntroPanel({ // } setIsProcessing(true); + + // 안전장치: 30초 후 자동으로 isProcessing 해제 + processingTimeoutRef.current = setTimeout(() => { + console.warn("[IntroPanel] handleAgree 타임아웃 - isProcessing 강제 해제"); + setIsProcessing(false); + }, 30000); + // 약관 동의 처리 시작 시 로딩 상태로 설정 dispatch({ type: types.GET_TERMS_AGREE_YN_START }); - // 약관 ID 정확하게 매핑 + // Redux에서 가져온 termsIdMap을 사용하여 동적으로 약관 ID 매핑 const agreeTerms = []; - if (termsChecked) { - agreeTerms.push("TID0000222"); // MST00402 -> TID0000222 (이용약관) + if (termsChecked && termsIdMap["MST00402"]) { + agreeTerms.push(termsIdMap["MST00402"]); // 이용약관 } - if (privacyChecked) { - agreeTerms.push("TID0000223"); // MST00401 -> TID0000223 (개인정보처리방침) + if (privacyChecked && termsIdMap["MST00401"]) { + agreeTerms.push(termsIdMap["MST00401"]); // 개인정보처리방침 } - if (optionalChecked) { - agreeTerms.push("TID0000232"); // MST00405 -> TID0000232 (선택약관) + if (optionalChecked && termsIdMap["MST00405"]) { + agreeTerms.push(termsIdMap["MST00405"]); // 선택약관 } if (process.env.NODE_ENV === "development") { - console.log("최종 전송될 agreeTerms:", agreeTerms); + console.log("[IntroPanel] 현재 termsIdMap:", termsIdMap); + console.log("[IntroPanel] 최종 전송될 agreeTerms:", agreeTerms); } dispatch( @@ -334,6 +348,7 @@ export default function IntroPanel({ } dispatch(popPanel(panel_names.INTRO_PANEL)); setIsProcessing(false); + clearTimeout(processingTimeoutRef.current); // 타임아웃 정리 }), ); } else { @@ -345,6 +360,7 @@ export default function IntroPanel({ }), ); setIsProcessing(false); + clearTimeout(processingTimeoutRef.current); // 타임아웃 정리 } }, () => { @@ -356,6 +372,7 @@ export default function IntroPanel({ }), ); setIsProcessing(false); + clearTimeout(processingTimeoutRef.current); // 타임아웃 정리 }, ), ); @@ -366,6 +383,7 @@ export default function IntroPanel({ dispatch, isProcessing, webOSVersion, + termsIdMap, ]); // 실패 감지를 위한 useEffect 추가 @@ -386,9 +404,19 @@ export default function IntroPanel({ }), ); setIsProcessing(false); + clearTimeout(processingTimeoutRef.current); // 타임아웃 정리 } }, [regDeviceData, dispatch, isProcessing]); // isProcessing 의존성 추가 + // 컴포넌트 언마운트 시 타임아웃 정리 + useEffect(() => { + return () => { + if (processingTimeoutRef.current) { + clearTimeout(processingTimeoutRef.current); + } + }; + }, []); + const handleDisagree = useCallback(() => { dispatch(setShowPopup(Config.ACTIVE_POPUP.exitPopup)); dispatch(sendLogTerms({ logTpNo: Config.LOG_TP_NO.TERMS.DO_NOT_AGREE }));