[251104] feat: preShopperHouseData

🕐 커밋 시간: 2025. 11. 04. 19:09:24

📊 변경 통계:
  • 총 파일: 6개
  • 추가: +89줄
  • 삭제: -29줄

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/actions/actionTypes.js
  ~ com.twin.app.shoptime/src/actions/searchActions.js
  ~ com.twin.app.shoptime/src/reducers/searchReducer.js
  ~ com.twin.app.shoptime/src/views/SearchPanel/SearchPanel.new.v2.jsx
  ~ com.twin.app.shoptime/src/views/SearchPanel/SearchResults.new.v2.jsx
  ~ com.twin.app.shoptime/src/views/SearchPanel/VoiceInputOverlay/VoiceInputOverlay.jsx

🔧 주요 변경 내용:
  • 타입 시스템 안정성 강화
  • 핵심 비즈니스 로직 개선
  • 소규모 기능 개선
  • 모듈 구조 개선
This commit is contained in:
2025-11-04 19:09:25 +09:00
parent a3e261960b
commit b3589eb940
6 changed files with 89 additions and 29 deletions

View File

@@ -181,6 +181,7 @@ export const types = {
GET_SEARCH_MAIN: 'GET_SEARCH_MAIN',
CLEAR_SEARCH_MAIN_DATA: 'CLEAR_SEARCH_MAIN_DATA',
GET_SHOPPERHOUSE_SEARCH: 'GET_SHOPPERHOUSE_SEARCH',
BACKUP_SHOPPERHOUSE_DATA: 'BACKUP_SHOPPERHOUSE_DATA',
CLEAR_SHOPPERHOUSE_DATA: 'CLEAR_SHOPPERHOUSE_DATA',
RESET_SEARCH: 'RESET_SEARCH',
RESET_VOICE_SEARCH: 'RESET_VOICE_SEARCH',

View File

@@ -105,6 +105,21 @@ export const getShopperHouseSearch =
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';
console.log('[ShopperHouse]-DIFF shopperHouseKey:', currentKey, '| preShopperHouseKey:', preKey);
if (currentShopperHouseData) {
dispatch({
type: types.BACKUP_SHOPPERHOUSE_DATA,
payload: currentShopperHouseData,
});
}
// 이전 데이터 초기화 -> shopperHouseData만 초기화
// dispatch({ type: types.CLEAR_SHOPPERHOUSE_DATA });
@@ -379,14 +394,28 @@ export const hideShopperHouseError = () => {
};
// ShopperHouse 검색 데이터 초기화
export const clearShopperHouseData = () => {
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';
console.log('[ShopperHouse]-DIFF (before clear) shopperHouseKey:', currentKey, '| preShopperHouseKey:', preKey);
if (currentShopperHouseData) {
dispatch({
type: types.BACKUP_SHOPPERHOUSE_DATA,
payload: currentShopperHouseData,
});
}
// ✨ 검색 키 초기화 - 진행 중인 요청의 응답을 무시하도록
console.log('[ShopperHouse] 🧹 [DEBUG] clearShopperHouseData - 이전 요청 무효화');
getShopperHouseSearchKey = null;
return {
dispatch({
type: types.CLEAR_SHOPPERHOUSE_DATA,
};
});
};
// 기존 코드 뒤에 추가

View File

@@ -9,6 +9,7 @@ const initialState = {
shopperHouseData: null,
shopperHouseSearchId: null,
shopperHouseRelativeQueries: null, // ✨ relativeQueries는 독립적으로 저장 (searchId와 별개)
preShopperHouseData: null, // 🔄 이전 shopperHouseData 저장 (sortingType 변경 시 사용)
// 🔽 검색 메인 데이터 추가
searchMainData: {
@@ -64,6 +65,7 @@ export const searchReducer = (state = initialState, action) => {
// 일반 검색 시 ShopperHouse 데이터 초기화
shopperHouseData: null,
shopperHouseSearchId: null,
preShopperHouseData: null, // 🔄 이전 데이터도 초기화
};
}
@@ -86,6 +88,7 @@ export const searchReducer = (state = initialState, action) => {
action: 'RESET_VOICE_SEARCH',
shopperHouseData_cleared: true,
shopperHouseSearchId_cleared: true,
preShopperHouseData_cleared: true,
shopperHouseRelativeQueries_preserved: state.shopperHouseRelativeQueries || '(없음)',
relativeQueries_length: state.shopperHouseRelativeQueries?.length || 0,
},
@@ -97,6 +100,7 @@ export const searchReducer = (state = initialState, action) => {
...state,
shopperHouseData: null,
shopperHouseSearchId: null,
preShopperHouseData: null, // 🔄 이전 데이터도 초기화
// ✨ relativeQueries는 유지 (다음 PROMPT 모드에서 표시하기 위해)
};
@@ -112,6 +116,18 @@ export const searchReducer = (state = initialState, action) => {
searchTimestamp: Date.now(),
};
case types.BACKUP_SHOPPERHOUSE_DATA: {
const newPreKey = action.payload?.results?.[0]?.searchId || 'null';
const oldPreKey = state.preShopperHouseData?.results?.[0]?.searchId || 'null';
console.log('[ShopperHouse]-DIFF (after backup) preShopperHouseKey:', oldPreKey, '→', newPreKey);
return {
...state,
preShopperHouseData: action.payload,
};
}
case types.GET_SHOPPERHOUSE_SEARCH: {
// ✅ 안전한 데이터 접근
const resultData = action.payload?.data?.result;
@@ -127,30 +143,17 @@ export const searchReducer = (state = initialState, action) => {
const searchId = results.length > 0 ? results[0].searchId : null;
const relativeQueries = results.length > 0 ? results[0].relativeQueries : null;
// Key 정보
const newKey = searchId || 'null';
const preKey = state.preShopperHouseData?.results?.[0]?.searchId || 'null';
// [VoiceInput] Redux에 저장 로그
console.log('[VoiceInput] 💾 Redux에 저장');
console.log('[VoiceInput] ├─ searchId:', searchId || '(없음)');
console.log('[VoiceInput] ├─ relativeQueries:', relativeQueries || '(없음)');
console.log('[VoiceInput]-searchReducer-GET_SHOPPERHOUSE_SEARCH');
console.log(
JSON.stringify(
{
searchId: searchId,
relativeQueries: relativeQueries,
relativeQueries_type: typeof relativeQueries,
relativeQueries_isArray: Array.isArray(relativeQueries),
relativeQueries_length: relativeQueries?.length || 0,
results_length: results.length,
firstResult_keys: results.length > 0 ? Object.keys(results[0]) : [],
},
null,
2
)
);
console.log('[ShopperHouse]-DIFF (after API) shopperHouseKey:', newKey, '| preShopperHouseKey:', preKey);
return {
...state,
shopperHouseData: resultData,
// 🔄 preShopperHouseData는 건드리지 않음 (API 호출 전에 이미 백업됨)
shopperHouseSearchId: searchId,
shopperHouseRelativeQueries: relativeQueries, // ✨ relativeQueries 별도 저장
// ShopperHouse 검색 시 일반 검색 데이터 초기화
@@ -168,6 +171,7 @@ export const searchReducer = (state = initialState, action) => {
shopperHouseError: action.payload,
shopperHouseData: null, // 오류 발생 시 데이터 초기화
shopperHouseSearchId: null,
preShopperHouseData: null, // 🔄 이전 데이터도 초기화
};
case types.SHOW_SHOPPERHOUSE_ERROR:
@@ -203,13 +207,14 @@ export const searchReducer = (state = initialState, action) => {
new Error().stack?.split('\n')[1]?.trim() || '(스택 추적 불가)'
);
console.log(
'[VoiceInput] 🧹 Redux shopperHouseData 초기화 (searchId & relativeQueries는 유지)'
'[VoiceInput] 🧹 Redux shopperHouseData 초기화 (searchId & relativeQueries & preShopperHouseData는 유지)'
);
console.log('[VoiceInput]-searchReducer-CLEAR_SHOPPERHOUSE_DATA');
console.log(
JSON.stringify(
{
shopperHouseData_cleared: true,
preShopperHouseData_preserved: !!state.preShopperHouseData,
shopperHouseSearchId_preserved: state.shopperHouseSearchId || '(없음)',
shopperHouseRelativeQueries_preserved: state.shopperHouseRelativeQueries || '(없음)',
relativeQueries_length: state.shopperHouseRelativeQueries?.length || 0,
@@ -221,6 +226,8 @@ export const searchReducer = (state = initialState, action) => {
return {
...state,
shopperHouseData: null,
// 🔄 preShopperHouseData는 searchId처럼 유지! (다음 정렬 변경 시 사용)
// preShopperHouseData: state.preShopperHouseData, // 명시적으로 유지 (spread로 자동 유지됨)
// ✅ searchId는 2번째 발화에서 필요하므로 유지!
// shopperHouseSearchId: null, // ❌ 제거됨 - searchId를 유지해야 2차 발화에서 searchId 포함 가능
// ✨ relativeQueries도 PROMPT 모드에서 표시하기 위해 유지!

View File

@@ -187,6 +187,8 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
const shopperHouseRelativeQueries = useSelector(
(state) => state.search.shopperHouseRelativeQueries
);
// 🔄 이전 shopperHouseData (sortingType 변경 시 사용)
const preShopperHouseData = useSelector((state) => state.search.preShopperHouseData);
// 0hun: 검색 메인, Hot Picks for you 영역에 대한 전역 상태 값
const hotPicksForYou = useSelector((state) => state.search.searchMainData.hotPicksForYou);
// 0hun: 검색 메인, Popular Brands 영역에 대한 전역 상태 값
@@ -1397,6 +1399,7 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
itemInfo={searchDatas.item}
showInfo={searchDatas.show}
shopperHouseInfo={shopperHouseData}
preShopperHouseInfo={preShopperHouseData}
fallbackShopperHouseData={shopperHouseDataRef.current}
shopperHouseSearchId={shopperHouseSearchId}
shopperHouseRelativeQueries={shopperHouseRelativeQueries}

View File

@@ -218,6 +218,7 @@ const SearchResultsNew = ({
showInfo,
themeInfo,
shopperHouseInfo,
preShopperHouseInfo = null,
fallbackShopperHouseData = null,
shopperHouseSearchId = null,
shopperHouseRelativeQueries = [],
@@ -254,9 +255,28 @@ const SearchResultsNew = ({
// ShopperHouse 데이터를 ItemCard 형식으로 변환
const convertedShopperHouseItems = useMemo(() => {
// 🎯 Fallback 로직: HowAboutThese 로딩 중에만 fallbackShopperHouseData 허용
const targetData =
shopperHouseInfo || (isHowAboutTheseLoading ? fallbackShopperHouseData : null);
// 🔄 sortingType에 따른 데이터 선택 로직
let targetData;
let dataSource = '';
if (dropDownTab === 0) {
// LG_Recommended: 현재 API 응답 사용
targetData = shopperHouseInfo || (isHowAboutTheseLoading ? fallbackShopperHouseData : null);
dataSource = shopperHouseInfo ? 'shopperHouseInfo' : (isHowAboutTheseLoading ? 'fallbackShopperHouseData' : 'null');
} else {
// 다른 정렬: 이전 API 응답 사용 (없으면 현재 데이터 사용)
targetData = preShopperHouseInfo || shopperHouseInfo || (isHowAboutTheseLoading ? fallbackShopperHouseData : null);
dataSource = preShopperHouseInfo ? 'preShopperHouseInfo' : (shopperHouseInfo ? 'shopperHouseInfo' : (isHowAboutTheseLoading ? 'fallbackShopperHouseData' : 'null'));
}
console.log('[SearchResultsNew] 🔄 데이터 소스 선택:', {
dropDownTab,
dataSource,
hasPreData: !!preShopperHouseInfo,
hasCurrentData: !!shopperHouseInfo,
hasFallbackData: !!fallbackShopperHouseData,
isLoading: isHowAboutTheseLoading
});
if (!targetData || !targetData.results || targetData.results.length === 0) {
return null;
@@ -319,7 +339,7 @@ const SearchResultsNew = ({
rangeType: resultData.rangeType || '',
};
});
}, [shopperHouseInfo, fallbackShopperHouseData, isHowAboutTheseLoading]);
}, [shopperHouseInfo, preShopperHouseInfo, fallbackShopperHouseData, isHowAboutTheseLoading, dropDownTab]);
const getButtonTabList = () => {
// ShopperHouse 데이터가 있으면 그것을 사용, 없으면 기존 검색 결과 사용

View File

@@ -213,7 +213,7 @@ const VoiceInputOverlay = ({
const [isSilenceCheckActive, setIsSilenceCheckActive] = useState(false);
const [hasReached5Chars, setHasReached5Chars] = useState(false); // 처음 5글자 도달 추적
// 💬 Bubble 버튼 상태 (true: 버튼 O, false: 텍스트만)
const [isBubbleButton, setIsBubbleButton] = useState(false); // 첫 발화때는 true (Try Saying)
const [isBubbleButton, setIsBubbleButton] = useState(true); // 첫 발화때는 true (Try Saying)
// useSearchHistory Hook 적용 (음성검색 기록 관리)
const { addVoiceSearch } = useSearchHistory();