Files
shoptime/com.twin.app.shoptime/src/actions/searchActions.js
optrader 741c4338ca [251124] fix: Log정리-5
🕐 커밋 시간: 2025. 11. 24. 12:43:58

📊 변경 통계:
  • 총 파일: 40개
  • 추가: +774줄
  • 삭제: -581줄

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/actions/appDataActions.js
  ~ com.twin.app.shoptime/src/actions/billingActions.js
  ~ com.twin.app.shoptime/src/actions/brandActions.js
  ~ com.twin.app.shoptime/src/actions/cancelActions.js
  ~ com.twin.app.shoptime/src/actions/cardActions.js
  ~ com.twin.app.shoptime/src/actions/cartActions.js
  ~ com.twin.app.shoptime/src/actions/checkoutActions.js
  ~ com.twin.app.shoptime/src/actions/commonActions.js
  ~ com.twin.app.shoptime/src/actions/convertActions.js
  ~ com.twin.app.shoptime/src/actions/couponActions.js
  ~ com.twin.app.shoptime/src/actions/deviceActions.js
  ~ com.twin.app.shoptime/src/actions/empActions.js
  ~ com.twin.app.shoptime/src/actions/eventActions.js
  ~ com.twin.app.shoptime/src/actions/forYouActions.js
  ~ com.twin.app.shoptime/src/actions/homeActions.js
  ~ com.twin.app.shoptime/src/actions/logActions.js
  ~ com.twin.app.shoptime/src/actions/mediaActions.js
  ~ com.twin.app.shoptime/src/actions/mockCartActions.js
  ~ com.twin.app.shoptime/src/actions/myPageActions.js
  ~ com.twin.app.shoptime/src/actions/onSaleActions.js
  ~ com.twin.app.shoptime/src/actions/orderActions.js
  ~ com.twin.app.shoptime/src/actions/panelActions.js
  ~ com.twin.app.shoptime/src/actions/panelNavigationActions.js
  ~ com.twin.app.shoptime/src/actions/pinCodeActions.js
  ~ com.twin.app.shoptime/src/actions/productActions.js
  ~ com.twin.app.shoptime/src/actions/queuedPanelActions.js
  ~ com.twin.app.shoptime/src/actions/searchActions.js
  ~ com.twin.app.shoptime/src/actions/shippingActions.js
  ~ com.twin.app.shoptime/src/actions/voiceActions.js
  ~ com.twin.app.shoptime/src/actions/webSpeechActions.js
  ~ com.twin.app.shoptime/src/reducers/localSettingsReducer.js
  ~ com.twin.app.shoptime/src/reducers/mediaOverlayReducer.js
  ~ com.twin.app.shoptime/src/reducers/mockCartReducer.js
  ~ com.twin.app.shoptime/src/reducers/playReducer.js
  ~ com.twin.app.shoptime/src/reducers/productReducer.js
  ~ com.twin.app.shoptime/src/reducers/videoOverlayReducer.js
  ~ com.twin.app.shoptime/src/views/UserReview/ShowUserReviews.jsx
  ~ com.twin.app.shoptime/src/views/UserReview/UserReviewPanel.jsx
  ~ com.twin.app.shoptime/src/views/UserReview/components/UserReviewsList.jsx
  ~ com.twin.app.shoptime/src/views/UserReview/components/VirtualScrollBar.jsx

🔧 함수 변경 내용:
  📊 Function-level changes summary across 40 files:
    • Functions added: 14
    • Functions modified: 34
    • Functions deleted: 18
  📋 By language:
    • javascript: 40 files, 66 function changes

🔧 주요 변경 내용:
  • 핵심 비즈니스 로직 개선
  • 로깅 시스템 개선
  • 설정 관리 시스템 개선
  • UI 컴포넌트 아키텍처 개선
2025-11-24 12:47:57 +09:00

538 lines
18 KiB
JavaScript

import { URLS } from '../api/apiConfig';
import { TAxios } from '../api/TAxios';
import { SEARCH_DATA_MAX_RESULTS_LIMIT } from '../utils/Config';
import { types } from './actionTypes';
import { changeAppStatus } from './commonActions';
import { createDebugHelpers } from '../utils/debug';
// 디버그 헬퍼 설정
const DEBUG_MODE = false;
const { dlog, dwarn, derror } = createDebugHelpers(DEBUG_MODE);
// Search 통합검색 (IBS) 데이터 조회 IF-LGSP-090
let getSearchKey = null;
let lastSearchedParams = {};
export const getSearch =
(params, startIndex = 1, key) =>
(dispatch, getState) => {
const { service, query, domain } = params;
if (startIndex === 1) {
lastSearchedParams = params;
}
const maxResults =
startIndex === 1 ? SEARCH_DATA_MAX_RESULTS_LIMIT * 2 : SEARCH_DATA_MAX_RESULTS_LIMIT;
let currentKey = key;
const onSuccess = (response) => {
dlog('getSearch onSuccess: ', response.data);
if (startIndex === 1) {
getSearchKey = new Date();
currentKey = getSearchKey;
dispatch({
type: types.GET_SEARCH,
payload: response.data,
});
dispatch(updateSearchTimestamp());
} else if (getSearchKey === currentKey) {
dispatch({
type: types.GET_SEARCH,
payload: response.data,
append: true,
startIndex: startIndex - 1,
});
}
};
const onFail = (error) => {
derror('getSearch onFail: ', error);
};
TAxios(
dispatch,
getState,
'post',
URLS.GET_SEARCH,
{},
{ service, query, startIndex, maxResults, domain },
onSuccess,
onFail
);
};
export const continueSearch =
(key, startIndex = 1) =>
(dispatch, getState) => {
const searchDatas = getState().search.searchDatas;
const totalCount = getState().search.totalCount;
if (
(startIndex <= 1 && !searchDatas[key]) ||
searchDatas[key][startIndex - 1] ||
!totalCount[key] ||
totalCount[key] < startIndex
) {
//ignore search
return;
}
dispatch(getSearch({ ...lastSearchedParams, domain: key }, startIndex, getSearchKey));
};
export const resetSearch = (status) => {
getSearchKey = null;
return { type: types.RESET_SEARCH, payload: status };
};
export const resetVoiceSearch = () => {
getShopperHouseSearchKey = null;
return { type: types.RESET_VOICE_SEARCH };
};
export const setInitPerformed = (performed) => ({
type: types.SET_SEARCH_INIT_PERFORMED,
payload: performed,
});
export const updateSearchTimestamp = () => ({
type: types.UPDATE_SEARCH_TIMESTAMP,
});
// ShopperHouse 검색 조회 IF-LGSP-098
let getShopperHouseSearchKey = null;
export const getShopperHouseSearch =
(query, searchId = null, sortingType = null) =>
(dispatch, getState) => {
// ✅ 빈 query 체크 - API 호출 방지
if (!query || query.trim() === '') {
dlog('[ShopperHouse] ⚠️ 빈 쿼리 - API 호출 건너뜀');
return;
}
// 🔄 API 호출 전에 현재 데이터를 백업 (한 번만!)
const currentShopperHouseData = getState().search.shopperHouseData;
const preShopperHouseData = getState().search.preShopperHouseData;
const currentKey = currentShopperHouseData?.results?.[0]?.searchId || 'null';
const preKey = preShopperHouseData?.results?.[0]?.searchId || 'null';
dlog('[ShopperHouse]-DIFF shopperHouseKey:', currentKey, '| preShopperHouseKey:', preKey);
if (currentShopperHouseData) {
dispatch({
type: types.BACKUP_SHOPPERHOUSE_DATA,
payload: currentShopperHouseData,
});
}
// 이전 데이터 초기화 -> shopperHouseData만 초기화
// dispatch({ type: types.CLEAR_SHOPPERHOUSE_DATA });
// 새로운 검색 시작 - 고유 키 생성
const currentSearchKey = new Date().getTime();
getShopperHouseSearchKey = currentSearchKey;
dlog('[ShopperHouse] 🔍 [DEBUG] API 호출 시작 - key:', currentSearchKey, 'query:', query);
const onSuccess = (response) => {
dlog('[ShopperHouse] 📥 [DEBUG] API 응답 도착 - key:', currentSearchKey);
dlog('[ShopperHouse] 🔑 [DEBUG] 현재 유효한 key:', getShopperHouseSearchKey);
// ✨ 현재 요청이 최신 요청인지 확인
if (currentSearchKey === getShopperHouseSearchKey) {
dlog('[ShopperHouse] ✅ [DEBUG] 유효한 응답 - Redux 업데이트');
dlog('[ShopperHouse] getShopperHouseSearch onSuccess: ', JSON.stringify(response.data));
// ✅ API 성공 여부 확인
const retCode = response.data?.retCode;
if (retCode !== 0) {
derror(
'[ShopperHouse] ❌ API 실패 - retCode:',
retCode,
'retMsg:',
response.data?.retMsg
);
dlog('[VoiceInput] 📥 API 응답 실패');
dlog('[VoiceInput] ├─ retCode:', retCode);
dlog('[VoiceInput] └─ retMsg:', response.data?.retMsg);
// ✨ API 실패 응답을 Redux 에러 상태에 저장
dispatch(
setShopperHouseError({
message: `API 실패: ${response.data?.retMsg || '알 수 없는 오류'}`,
status: 'API_ERROR',
retCode: retCode,
retMsg: response.data?.retMsg,
timestamp: Date.now(),
endpoint: URLS.GET_SHOPPERHOUSE_SEARCH,
query: query,
searchId: searchId,
response: response.data,
})
);
return;
}
// ✅ result 데이터 존재 확인
if (!response.data?.data?.result) {
derror('[ShopperHouse] ❌ API 응답에 result 데이터 없음');
dlog('[VoiceInput] 📥 API 응답 실패 (result 데이터 없음)');
// ✨ result 데이터 없음 에러를 Redux 에러 상태에 저장
dispatch(
setShopperHouseError({
message: 'API 응답에 result 데이터가 없습니다',
status: 'NO_RESULT_DATA',
timestamp: Date.now(),
endpoint: URLS.GET_SHOPPERHOUSE_SEARCH,
query: query,
searchId: searchId,
response: response.data,
})
);
return;
}
// 📥 [ShopperHouseAPI] API 응답 성공 로그
const resultData = response.data.data.result;
const results = resultData.results || [];
const receivedSearchId = results.length > 0 ? results[0].searchId : null;
const relativeQueries = results.length > 0 ? results[0].relativeQueries : null;
// 상품 개수 계산: results[0].docs 배열 길이
const productCount = results.length > 0 && results[0].docs ? results[0].docs.length : 0;
const elapsedTime = ((new Date().getTime() - currentSearchKey) / 1000).toFixed(2);
dlog('*[ShopperHouseAPI] ✅ onSuccess - API 응답 성공');
dlog(
'*[ShopperHouseAPI] ├─ searchId:',
receivedSearchId === null ? '(NULL)' : receivedSearchId
);
dlog('*[ShopperHouseAPI] ├─ 상품 개수:', productCount);
dlog('*[ShopperHouseAPI] ├─ relativeQueries:', relativeQueries || '(없음)');
dlog('*[ShopperHouseAPI] ├─ 소요 시간:', elapsedTime + '초');
dlog('*[ShopperHouseAPI] └─ timestamp:', new Date().toISOString());
dispatch({
type: types.GET_SHOPPERHOUSE_SEARCH,
payload: response.data,
});
dispatch(updateSearchTimestamp());
} else {
dlog('[ShopperHouse] ❌ [DEBUG] 오래된 응답 무시 - Redux 업데이트 안함');
}
};
const onFail = (error) => {
derror('[ShopperHouse] getShopperHouseSearch onFail: ', JSON.stringify(error));
// ✨ 현재 요청이 최신 요청인지 확인
if (currentSearchKey === getShopperHouseSearchKey) {
dlog('[ShopperHouse] ❌ [DEBUG] 유효한 에러 응답 - Redux 에러 상태 업데이트');
const retCode = error?.data?.retCode;
const status = error?.status;
const errorMessage = error?.message;
// ✅ TAxios 재인증 오류 필터링 (기존 방식 그대로 활용)
if (retCode === 401) {
dlog('*[ShopperHouseAPI] ⚠️ onFail - Access Token 만료 (401)');
dlog('*[ShopperHouseAPI] └─ TAxios가 자동으로 재인증하고 재시도합니다');
// 401 에러는 Redux에 저장하지 않음 (TAxios 자동 재시도 대기)
return;
}
if (retCode === 402 || retCode === 501) {
dlog('*[ShopperHouseAPI] ⚠️ onFail - RefreshToken 만료 (' + retCode + ')');
dlog('*[ShopperHouseAPI] └─ TAxios가 자동으로 토큰 재발급하고 재시도합니다');
// 402/501 에러는 Redux에 저장하지 않음 (TAxios 자동 재시도 대기)
return;
}
// ✅ 네트워크 연결 관련 오류 중 일시적인 오류 필터링
if (
status === 0 ||
errorMessage?.includes('Network Error') ||
errorMessage?.includes('timeout')
) {
dlog('*[ShopperHouseAPI] ⚠️ onFail - 일시적인 네트워크 오류');
dlog('*[ShopperHouseAPI] ├─ status:', status);
dlog('*[ShopperHouseAPI] └─ errorMessage:', errorMessage);
// 일시적인 네트워크 오류는 Redux에 저장하지 않음
return;
}
// ✨ 그 외의 실제 API 오류들만 Redux에 저장
dlog('*[ShopperHouseAPI] ❌ onFail - 실제 API 오류 발생');
dlog('*[ShopperHouseAPI] ├─ retCode:', retCode);
dlog('*[ShopperHouseAPI] ├─ status:', status);
dlog('*[ShopperHouseAPI] ├─ errorMessage:', errorMessage);
dlog('*[ShopperHouseAPI] └─ retMsg:', error?.data?.retMsg || '(없음)');
// ✅ API 실패 시 모든 데이터 정리
dlog('*[ShopperHouseAPI] 🧹 API 실패 - shopperHouse 데이터 정리');
dispatch(clearShopperHouseData());
// ✅ 사용자에게 실패 알림 표시
dispatch(
showShopperHouseError({
message: '검색에 실패했습니다. 다시 시도해주세요.',
type: 'API_FAILURE',
originalError: {
status: status,
retCode: retCode,
retMsg: error?.data?.retMsg,
errorMessage: errorMessage,
},
})
);
// 기존 에러 정보도 저장 (디버깅용)
dispatch(
setShopperHouseError({
message: errorMessage || 'Unknown API error',
status: status,
statusText: error?.statusText,
retCode: retCode,
retMsg: error?.data?.retMsg,
timestamp: Date.now(),
endpoint: URLS.GET_SHOPPERHOUSE_SEARCH,
query: query,
searchId: searchId,
originalError: error,
})
);
} else {
dlog('[ShopperHouse] ❌ [DEBUG] 오래된 에러 응답 무시 - Redux 업데이트 안함');
}
};
const params = { query };
if (searchId) {
params.searchId = searchId;
}
if (sortingType) {
params.sortingType = sortingType;
}
dlog('*[ShopperHouseAPI] getShopperHouseSearch params: ', JSON.stringify(params));
dlog('*[ShopperHouseAPI] ├─ query:', query);
dlog('*[ShopperHouseAPI] ├─ searchId:', searchId === null ? '(NULL)' : searchId);
dlog('*[ShopperHouseAPI] ├─ sortingType:', sortingType === null ? '(NULL)' : sortingType);
dlog('*[ShopperHouseAPI] └─ timestamp:', new Date().toISOString());
// 🔧 [테스트용] API 실패 시뮬레이션 스위치
const SIMULATE_API_FAILURE = false; // ⭐ 이 값을 true로 변경하면 실패 시뮬레이션
if (SIMULATE_API_FAILURE) {
dlog('🧪 [TEST] API 실패 시뮬레이션 활성화 - 2초 후 실패 응답');
// 2초 후 실패 시뮬레이션
setTimeout(() => {
const simulatedError = {
status: 500,
retCode: 500,
retMsg: '시뮬레이션된 API 실패',
message: 'Simulated API failure for testing',
data: {
retCode: 500,
retMsg: '시뮬레이션된 서버 오류',
},
};
dlog('🧪 [TEST] 시뮬레이션된 실패 응답 전송');
onFail(simulatedError);
}, 2000); // 2초 딜레이
return; // 실제 API 호출하지 않음
}
TAxios(dispatch, getState, 'post', URLS.GET_SHOPPERHOUSE_SEARCH, {}, params, onSuccess, onFail);
};
// ShopperHouse API 에러 처리 액션
export const setShopperHouseError = (error) => {
dlog('[ShopperHouse] ❌ [DEBUG] setShopperHouseError - 에러 정보 저장');
dlog('[ShopperHouse] └─ error:', error);
return {
type: types.SET_SHOPPERHOUSE_ERROR,
payload: error,
};
};
// ShopperHouse 에러 표시 액션 (사용자에게 팝업으로 알림)
export const showShopperHouseError = (error) => {
dlog('[ShopperHouse] 🔴 [DEBUG] showShopperHouseError - 에러 팝업 표시');
dlog('[ShopperHouse] └─ error:', error);
return {
type: types.SHOW_SHOPPERHOUSE_ERROR,
payload: {
message: error.message,
type: error.type || 'API_FAILURE',
timestamp: Date.now(),
visible: true,
originalError: error.originalError || null,
},
};
};
// ShopperHouse 에러 숨김 액션 (팝업 닫기)
export const hideShopperHouseError = () => {
dlog('[ShopperHouse] ✅ [DEBUG] hideShopperHouseError - 에러 팝업 숨김');
return {
type: types.HIDE_SHOPPERHOUSE_ERROR,
};
};
// ShopperHouse 검색 데이터 초기화
export const clearShopperHouseData = () => (dispatch, getState) => {
// 🔄 초기화 전에 현재 데이터를 백업!
const currentShopperHouseData = getState().search.shopperHouseData;
const preShopperHouseData = getState().search.preShopperHouseData;
const currentKey = currentShopperHouseData?.results?.[0]?.searchId || 'null';
const preKey = preShopperHouseData?.results?.[0]?.searchId || 'null';
dlog(
'[ShopperHouse]-DIFF (before clear) shopperHouseKey:',
currentKey,
'| preShopperHouseKey:',
preKey
);
if (currentShopperHouseData) {
dispatch({
type: types.BACKUP_SHOPPERHOUSE_DATA,
payload: currentShopperHouseData,
});
}
// ✨ 검색 키 초기화 - 진행 중인 요청의 응답을 무시하도록
getShopperHouseSearchKey = null;
dispatch({
type: types.CLEAR_SHOPPERHOUSE_DATA,
});
};
// 기존 코드 뒤에 추가
// Search Main 조회 IF-LGSP-097
export const getSearchMain = () => (dispatch, getState) => {
const onSuccess = (response) => {
dlog('getSearchMain onSuccess: ', response.data);
dispatch({
type: types.GET_SEARCH_MAIN,
payload: response.data,
});
};
const onFail = (error) => {
derror('getSearchMain onFail: ', error);
};
TAxios(dispatch, getState, 'post', URLS.GET_SEARCH_MAIN, {}, {}, onSuccess, onFail);
};
// 검색 메인 데이터 초기화
export const clearSearchMainData = () => ({
type: types.CLEAR_SEARCH_MAIN_DATA,
});
// 🎯 [Phase 1] SearchPanel 모드 제어 명령 - VoiceInputOverlay에서 SearchInputOverlay로 전환
/**
* VoiceInputOverlay의 TInputSimple에서 Enter 키 또는 마우스 클릭 감지 시
* SearchInputOverlay로 전환하도록 신호를 보냅니다.
*
* 흐름:
* VoiceInputOverlay (TInputSimple)
* ↓ (Enter/Click 감지)
* dispatch(switchToSearchInputOverlay())
* ↓
* Redux state 업데이트
* ↓
* SearchPanel useSelector로 감지
* ↓
* SearchPanel이 VoiceOverlay 닫고 SearchInputOverlay 오픈
*
* @param {string} source - 명령 발생 출처 (기본값: 'VoiceInputOverlay')
* @returns {object} Redux action
*/
export const switchToSearchInputOverlay = (source = 'VoiceInputOverlay') => {
dlog('[searchActions] 🔄 switchToSearchInputOverlay 명령 발송', {
source,
timestamp: new Date().toISOString(),
});
return {
type: types.SWITCH_TO_SEARCH_INPUT_OVERLAY,
payload: {
source,
},
};
};
// 🎯 [Phase 1] SearchPanel 명령 초기화
/**
* SearchPanel에서 명령을 처리한 후 명령 상태를 초기화합니다.
* 이를 통해 다음 명령이 제대로 감지되도록 합니다.
*
* @returns {object} Redux action
*/
export const clearPanelCommand = () => {
dlog('[searchActions] 🧹 clearPanelCommand 호출 - 명령 초기화');
return {
type: types.CLEAR_PANEL_COMMAND,
};
};
// 🎯 [Phase 2] 부드러운 전환: VoiceInputOverlay → SearchInputOverlay
/**
* Promise 기반의 비동기 전환 로직
* 1. VoiceInputOverlay 닫기
* 2. 애니메이션 대기
* 3. SearchInputOverlay 열기
* 4. 렌더링 대기
* 5. Spotlight 포커스 설정
*
* @param {object} options - { setIsVoiceOverlayVisible, setIsSearchOverlayVisible, dispatch, Spotlight }
* @returns {Promise}
*/
export const transitionToSearchInputOverlay = (options) => async (dispatch) => {
const { setIsVoiceOverlayVisible, setIsSearchOverlayVisible, Spotlight } = options;
dlog('[searchActions] 🔄 transitionToSearchInputOverlay 시작');
dlog('[searchActions] ├─ Step 1: VoiceInputOverlay 닫기');
// Step 1: VoiceInputOverlay 닫기
setIsVoiceOverlayVisible(false);
// Step 2: 애니메이션 대기 (300ms - VoiceInputOverlay 닫기 애니메이션)
dlog('[searchActions] ├─ Step 2: 300ms 대기 (VoiceOverlay 애니메이션)');
await new Promise((resolve) => setTimeout(resolve, 300));
// Step 3: SearchInputOverlay 열기
dlog('[searchActions] ├─ Step 3: SearchInputOverlay 열기');
setIsSearchOverlayVisible(true);
// Step 4: 렌더링 대기 (100ms - SearchInputOverlay 렌더링 및 마운트)
dlog('[searchActions] ├─ Step 4: 100ms 대기 (SearchInputOverlay 렌더링)');
await new Promise((resolve) => setTimeout(resolve, 100));
// Step 5: Spotlight 포커스 설정
dlog('[searchActions] ├─ Step 5: Spotlight 포커스 설정 (search_overlay_input_box)');
Spotlight.focus('search_overlay_input_box');
// Step 6: 명령 초기화
dlog('[searchActions] └─ Step 6: panelCommand 초기화');
dispatch(clearPanelCommand());
dlog('[searchActions] ✅ transitionToSearchInputOverlay 완료');
};