🕐 커밋 시간: 2025. 11. 24. 12:19:40 📊 변경 통계: • 총 파일: 6개 • 추가: +283줄 • 삭제: -255줄 📝 수정된 파일: ~ com.twin.app.shoptime/src/actions/mainActions.js ~ com.twin.app.shoptime/src/reducers/mainReducer.js ~ com.twin.app.shoptime/src/reducers/searchReducer.js ~ com.twin.app.shoptime/src/views/PlayerPanel/PlayerTabContents/v2/TabContainer.v2.jsx ~ com.twin.app.shoptime/src/views/SearchPanel/SearchPanel.new.v2.jsx ~ com.twin.app.shoptime/src/views/SearchPanel/VoiceInputOverlay/VoiceInputOverlay.jsx 🔧 함수 변경 내용: 📄 com.twin.app.shoptime/src/actions/mainActions.js (javascript): 🔄 Modified: clearSubCategory() 📄 com.twin.app.shoptime/src/views/PlayerPanel/PlayerTabContents/v2/TabContainer.v2.jsx (javascript): 🔄 Modified: Spottable() 📄 com.twin.app.shoptime/src/views/SearchPanel/VoiceInputOverlay/VoiceInputOverlay.jsx (javascript): ✅ Added: Spottable() 🔄 Modified: clearAllTimers() 🔧 주요 변경 내용: • 핵심 비즈니스 로직 개선
332 lines
11 KiB
JavaScript
332 lines
11 KiB
JavaScript
import { types } from '../actions/actionTypes';
|
|
import { createDebugHelpers } from '../utils/debug';
|
|
|
|
// 디버그 헬퍼 설정
|
|
const DEBUG_MODE = false;
|
|
const { dlog, dwarn, derror } = createDebugHelpers(DEBUG_MODE);
|
|
|
|
const initialState = {
|
|
searchDatas: {},
|
|
totalCount: {},
|
|
searchPerformed: false,
|
|
initPerformed: false,
|
|
searchTimestamp: null,
|
|
shopperHouseData: null,
|
|
shopperHouseSearchId: null,
|
|
shopperHouseRelativeQueries: null, // ✨ relativeQueries는 독립적으로 저장 (searchId와 별개)
|
|
preShopperHouseData: null, // 🔄 이전 shopperHouseData 저장 (sortingType 변경 시 사용)
|
|
|
|
// 🔽 검색 메인 데이터 추가
|
|
searchMainData: {
|
|
topSearchs: [],
|
|
popularBrands: [],
|
|
hotPicksForYou: [],
|
|
tsvInfo: [],
|
|
},
|
|
shopperHouseError: null, // ShopperHouse API 오류 정보 저장 (디버깅용)
|
|
shopperHouseErrorPopup: {
|
|
// ShopperHouse 에러 팝업 상태 (사용자 알림용)
|
|
message: null,
|
|
type: null, // 'API_FAILURE', 'NO_RESULTS', etc.
|
|
visible: false,
|
|
timestamp: null,
|
|
originalError: null,
|
|
},
|
|
|
|
// 🎯 [Phase 1] SearchPanel 모드 제어 명령
|
|
panelCommand: {
|
|
type: null, // 명령 타입: 'SWITCH_TO_SEARCH_INPUT', 'SWITCH_TO_VOICE_INPUT' 등
|
|
source: null, // 명령 발생 출처: 'VoiceInputOverlay', 'SearchInputOverlay' 등
|
|
timestamp: null, // 명령 발생 시간
|
|
},
|
|
};
|
|
|
|
export const searchReducer = (state = initialState, action) => {
|
|
switch (action.type) {
|
|
case types.GET_SEARCH: {
|
|
const newResults = action.payload.data.result.results;
|
|
|
|
const updatedSearchDatas = action.append ? { ...state.searchDatas } : {};
|
|
newResults.forEach(({ type, docs }) => {
|
|
if (action.append && updatedSearchDatas[type]) {
|
|
for (let i = 0; i < docs.length; i++) {
|
|
updatedSearchDatas[type][action.startIndex + i] = docs[i];
|
|
}
|
|
} else {
|
|
updatedSearchDatas[type] = docs;
|
|
}
|
|
});
|
|
const updatedTotalCount = action.append ? { ...state.totalCount } : {};
|
|
newResults.forEach(({ type, total_count }) => {
|
|
updatedTotalCount[type] = total_count;
|
|
});
|
|
|
|
return {
|
|
...state,
|
|
searchDatas: updatedSearchDatas,
|
|
totalCount: updatedTotalCount,
|
|
searchPerformed: true,
|
|
initPerformed: !action.append,
|
|
// 일반 검색 시 ShopperHouse 데이터 초기화
|
|
shopperHouseData: null,
|
|
shopperHouseSearchId: null,
|
|
preShopperHouseData: null, // 🔄 이전 데이터도 초기화
|
|
};
|
|
}
|
|
|
|
case types.RESET_SEARCH:
|
|
return {
|
|
...state,
|
|
searchDatas: {},
|
|
totalCount: {},
|
|
searchPerformed: false,
|
|
initPerformed: false,
|
|
searchTimestamp: null,
|
|
// shopperHouseData, shopperHouseSearchId 유지
|
|
};
|
|
|
|
case types.RESET_VOICE_SEARCH:
|
|
dlog('[VoiceInput]-searchReducer-RESET_VOICE_SEARCH');
|
|
dlog(
|
|
JSON.stringify(
|
|
{
|
|
action: 'RESET_VOICE_SEARCH',
|
|
shopperHouseData_cleared: true,
|
|
shopperHouseSearchId_cleared: true,
|
|
preShopperHouseData_cleared: true,
|
|
shopperHouseRelativeQueries_preserved: state.shopperHouseRelativeQueries || '(없음)',
|
|
relativeQueries_length: state.shopperHouseRelativeQueries?.length || 0,
|
|
},
|
|
null,
|
|
2
|
|
)
|
|
);
|
|
return {
|
|
...state,
|
|
shopperHouseData: null,
|
|
shopperHouseSearchId: null,
|
|
preShopperHouseData: null, // 🔄 이전 데이터도 초기화
|
|
// ✨ relativeQueries는 유지 (다음 PROMPT 모드에서 표시하기 위해)
|
|
};
|
|
|
|
case types.SET_SEARCH_INIT_PERFORMED:
|
|
return {
|
|
...state,
|
|
initPerformed: action.payload,
|
|
};
|
|
|
|
case types.UPDATE_SEARCH_TIMESTAMP:
|
|
return {
|
|
...state,
|
|
searchTimestamp: Date.now(),
|
|
};
|
|
|
|
case types.BACKUP_SHOPPERHOUSE_DATA: {
|
|
const newPreKey = action.payload?.results?.[0]?.searchId || 'null';
|
|
const oldPreKey = state.preShopperHouseData?.results?.[0]?.searchId || 'null';
|
|
|
|
dlog('[ShopperHouse]-DIFF (after backup) preShopperHouseKey:', oldPreKey, '→', newPreKey);
|
|
|
|
return {
|
|
...state,
|
|
preShopperHouseData: action.payload,
|
|
};
|
|
}
|
|
|
|
case types.GET_SHOPPERHOUSE_SEARCH: {
|
|
// ✅ 안전한 데이터 접근
|
|
const resultData = action.payload?.data?.result;
|
|
|
|
if (!resultData) {
|
|
console.error('[searchReducer] Invalid shopperHouse data structure');
|
|
return state;
|
|
}
|
|
|
|
const results = resultData.results || [];
|
|
|
|
// searchId와 relativeQueries 추출 (첫 번째 result에서)
|
|
const searchId = results.length > 0 ? results[0].searchId : null;
|
|
const relativeQueries = results.length > 0 ? results[0].relativeQueries : null;
|
|
const sortingType = results.length > 0 ? results[0].sortingType : null;
|
|
|
|
// Key 정보
|
|
const newKey = searchId || 'null';
|
|
const preKey = state.preShopperHouseData?.results?.[0]?.searchId || 'null';
|
|
const preSortingType = state.preShopperHouseData?.results?.[0]?.sortingType || 'null';
|
|
|
|
// [VoiceInput] Redux에 저장 로그
|
|
dlog(
|
|
'[ShopperHouse]-DIFF (after API) shopperHouseKey:',
|
|
newKey,
|
|
'| preShopperHouseKey:',
|
|
preKey,
|
|
'| sortingType:',
|
|
sortingType || 'null',
|
|
'| preSortingType:',
|
|
preSortingType
|
|
);
|
|
|
|
return {
|
|
...state,
|
|
shopperHouseData: resultData,
|
|
// 🔄 preShopperHouseData는 건드리지 않음 (API 호출 전에 이미 백업됨)
|
|
shopperHouseSearchId: searchId,
|
|
shopperHouseRelativeQueries: relativeQueries, // ✨ relativeQueries 별도 저장
|
|
// ShopperHouse 검색 시 일반 검색 데이터 초기화
|
|
searchDatas: {},
|
|
totalCount: {},
|
|
searchPerformed: false,
|
|
initPerformed: false,
|
|
};
|
|
}
|
|
|
|
case types.SET_SHOPPERHOUSE_ERROR:
|
|
dlog('[VoiceInput] ❌ Redux shopperHouseError 저장:', action.payload);
|
|
return {
|
|
...state,
|
|
shopperHouseError: action.payload,
|
|
shopperHouseData: null, // 오류 발생 시 데이터 초기화
|
|
shopperHouseSearchId: null,
|
|
preShopperHouseData: null, // 🔄 이전 데이터도 초기화
|
|
};
|
|
|
|
case types.SHOW_SHOPPERHOUSE_ERROR:
|
|
dlog('[ShopperHouse] 🔴 Redux shopperHouseErrorPopup 표시:', action.payload);
|
|
return {
|
|
...state,
|
|
shopperHouseErrorPopup: {
|
|
message: action.payload.message,
|
|
type: action.payload.type,
|
|
visible: action.payload.visible,
|
|
timestamp: action.payload.timestamp,
|
|
originalError: action.payload.originalError,
|
|
},
|
|
};
|
|
|
|
case types.HIDE_SHOPPERHOUSE_ERROR:
|
|
dlog('[ShopperHouse] ✅ Redux shopperHouseErrorPopup 숨김');
|
|
return {
|
|
...state,
|
|
shopperHouseErrorPopup: {
|
|
message: null,
|
|
type: null,
|
|
visible: false,
|
|
timestamp: null,
|
|
originalError: null,
|
|
},
|
|
};
|
|
|
|
case types.CLEAR_SHOPPERHOUSE_DATA:
|
|
dlog('[DEBUG] 🧹 Redux shopperHouseData 초기화 호출 - 호출 스택 추적:');
|
|
dlog('[DEBUG] 호출 위치:', new Error().stack?.split('\n')[1]?.trim() || '(스택 추적 불가)');
|
|
dlog(
|
|
'[VoiceInput] 🧹 Redux shopperHouseData 초기화 (searchId & relativeQueries & preShopperHouseData는 유지)'
|
|
);
|
|
dlog('[VoiceInput]-searchReducer-CLEAR_SHOPPERHOUSE_DATA');
|
|
dlog(
|
|
JSON.stringify(
|
|
{
|
|
shopperHouseData_cleared: true,
|
|
preShopperHouseData_preserved: !!state.preShopperHouseData,
|
|
shopperHouseSearchId_preserved: state.shopperHouseSearchId || '(없음)',
|
|
shopperHouseRelativeQueries_preserved: state.shopperHouseRelativeQueries || '(없음)',
|
|
relativeQueries_length: state.shopperHouseRelativeQueries?.length || 0,
|
|
},
|
|
null,
|
|
2
|
|
)
|
|
);
|
|
return {
|
|
...state,
|
|
shopperHouseData: null,
|
|
// 🔄 preShopperHouseData는 searchId처럼 유지! (다음 정렬 변경 시 사용)
|
|
// preShopperHouseData: state.preShopperHouseData, // 명시적으로 유지 (spread로 자동 유지됨)
|
|
// ✅ searchId는 2번째 발화에서 필요하므로 유지!
|
|
// shopperHouseSearchId: null, // ❌ 제거됨 - searchId를 유지해야 2차 발화에서 searchId 포함 가능
|
|
// ✨ relativeQueries도 PROMPT 모드에서 표시하기 위해 유지!
|
|
// shopperHouseRelativeQueries는 유지
|
|
shopperHouseError: null, // 데이터 초기화 시 오류도 초기화
|
|
};
|
|
|
|
// 🔽 검색 메인 데이터 처리
|
|
case types.GET_SEARCH_MAIN: {
|
|
dlog('🔍 [searchReducer] GET_SEARCH_MAIN 받은 payload:', action.payload);
|
|
|
|
// 여러 가능한 구조 확인
|
|
let resultData = null;
|
|
|
|
if (action.payload?.result) {
|
|
// payload.result 구조
|
|
resultData = action.payload.result;
|
|
dlog('✅ [searchReducer] payload.result 구조 확인');
|
|
} else if (action.payload?.data?.result) {
|
|
// payload.data.result 구조
|
|
resultData = action.payload.data.result;
|
|
dlog('✅ [searchReducer] payload.data.result 구조 확인');
|
|
} else if (action.payload?.data) {
|
|
// payload.data에 직접 데이터가 있는 경우
|
|
resultData = action.payload.data;
|
|
dlog('✅ [searchReducer] payload.data 직접 구조 확인');
|
|
}
|
|
|
|
if (!resultData) {
|
|
console.error('[searchReducer] ❌ Invalid searchMain data structure');
|
|
console.error('받은 payload:', JSON.stringify(action.payload, null, 2));
|
|
return state;
|
|
}
|
|
|
|
dlog('[searchReducer] ✅ GET_SEARCH_MAIN success');
|
|
|
|
return {
|
|
...state,
|
|
searchMainData: {
|
|
topSearchs: resultData.topSearchs || [],
|
|
popularBrands: resultData.popularBrands || [],
|
|
hotPicksForYou: resultData.hotPicksForYou || [],
|
|
tsvInfo: resultData.tsvInfo || [],
|
|
},
|
|
};
|
|
}
|
|
|
|
case types.CLEAR_SEARCH_MAIN_DATA:
|
|
dlog('[searchReducer] 🧹 searchMainData 초기화');
|
|
return {
|
|
...state,
|
|
searchMainData: {
|
|
topSearchs: [],
|
|
popularBrands: [],
|
|
hotPicksForYou: [],
|
|
tsvInfo: [],
|
|
},
|
|
};
|
|
|
|
// 🎯 [Phase 1] SearchPanel 모드 제어 명령
|
|
case types.SWITCH_TO_SEARCH_INPUT_OVERLAY:
|
|
dlog('[searchReducer] 🔄 SWITCH_TO_SEARCH_INPUT_OVERLAY 명령 저장', {
|
|
source: action.payload?.source,
|
|
timestamp: new Date().toISOString(),
|
|
});
|
|
return {
|
|
...state,
|
|
panelCommand: {
|
|
type: 'SWITCH_TO_SEARCH_INPUT',
|
|
source: action.payload?.source || 'unknown',
|
|
timestamp: Date.now(),
|
|
},
|
|
};
|
|
|
|
case types.CLEAR_PANEL_COMMAND:
|
|
return {
|
|
...state,
|
|
panelCommand: {
|
|
type: null,
|
|
source: null,
|
|
timestamp: null,
|
|
},
|
|
};
|
|
|
|
default:
|
|
return state;
|
|
}
|
|
};
|