[251023] fix: ShopperHouse API relativeQurues구현

🕐 커밋 시간: 2025. 10. 23. 17:39:17

📊 변경 통계:
  • 총 파일: 5개
  • 추가: +94줄
  • 삭제: -18줄

📝 수정된 파일:
  ~ 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

🔧 함수 변경 내용:
  📄 com.twin.app.shoptime/src/actions/searchActions.js (javascript):
    🔄 Modified: setShopperHouseError()
  📄 com.twin.app.shoptime/src/views/SearchPanel/VoiceInputOverlay/VoiceInputOverlay.jsx (javascript):
    🔄 Modified: clearAllTimers()

🔧 주요 변경 내용:
  • 핵심 비즈니스 로직 개선
This commit is contained in:
2025-10-23 17:39:18 +09:00
parent a6c74a35f6
commit eecb1c26bb
5 changed files with 188 additions and 121 deletions

View File

@@ -15,13 +15,11 @@ export const getSearch =
lastSearchedParams = params;
}
const maxResults =
startIndex === 1
? SEARCH_DATA_MAX_RESULTS_LIMIT * 2
: SEARCH_DATA_MAX_RESULTS_LIMIT;
startIndex === 1 ? SEARCH_DATA_MAX_RESULTS_LIMIT * 2 : SEARCH_DATA_MAX_RESULTS_LIMIT;
let currentKey = key;
const onSuccess = (response) => {
console.log("getSearch onSuccess: ", response.data);
console.log('getSearch onSuccess: ', response.data);
if (startIndex === 1) {
getSearchKey = new Date();
@@ -44,13 +42,13 @@ export const getSearch =
};
const onFail = (error) => {
console.error("getSearch onFail: ", error);
console.error('getSearch onFail: ', error);
};
TAxios(
dispatch,
getState,
"post",
'post',
URLS.GET_SEARCH,
{},
{ service, query, startIndex, maxResults, domain },
@@ -73,13 +71,7 @@ export const continueSearch =
//ignore search
return;
}
dispatch(
getSearch(
{ ...lastSearchedParams, domain: key },
startIndex,
getSearchKey
)
);
dispatch(getSearch({ ...lastSearchedParams, domain: key }, startIndex, getSearchKey));
};
export const resetSearch = (status) => {
@@ -112,27 +104,21 @@ export const getShopperHouseSearch =
getShopperHouseSearchKey = currentSearchKey;
console.log(
"[ShopperHouse] 🔍 [DEBUG] API 호출 시작 - key:",
'[ShopperHouse] 🔍 [DEBUG] API 호출 시작 - key:',
currentSearchKey,
"query:",
'query:',
query
);
const onSuccess = (response) => {
console.log(
"[ShopperHouse] 📥 [DEBUG] API 응답 도착 - key:",
currentSearchKey
);
console.log(
"[ShopperHouse] 🔑 [DEBUG] 현재 유효한 key:",
getShopperHouseSearchKey
);
console.log('[ShopperHouse] 📥 [DEBUG] API 응답 도착 - key:', currentSearchKey);
console.log('[ShopperHouse] 🔑 [DEBUG] 현재 유효한 key:', getShopperHouseSearchKey);
// ✨ 현재 요청이 최신 요청인지 확인
if (currentSearchKey === getShopperHouseSearchKey) {
console.log("[ShopperHouse] ✅ [DEBUG] 유효한 응답 - Redux 업데이트");
console.log('[ShopperHouse] ✅ [DEBUG] 유효한 응답 - Redux 업데이트');
console.log(
"[ShopperHouse] getShopperHouseSearch onSuccess: ",
'[ShopperHouse] getShopperHouseSearch onSuccess: ',
JSON.stringify(response.data)
);
@@ -140,9 +126,9 @@ export const getShopperHouseSearch =
const retCode = response.data?.retCode;
if (retCode !== 0) {
console.error(
"[ShopperHouse] ❌ API 실패 - retCode:",
'[ShopperHouse] ❌ API 실패 - retCode:',
retCode,
"retMsg:",
'retMsg:',
response.data?.retMsg
);
console.log('[VoiceInput] 📥 API 응답 실패');
@@ -150,7 +136,8 @@ export const getShopperHouseSearch =
console.log('[VoiceInput] └─ retMsg:', response.data?.retMsg);
// ✨ API 실패 응답을 Redux 에러 상태에 저장
dispatch(setShopperHouseError({
dispatch(
setShopperHouseError({
message: `API 실패: ${response.data?.retMsg || '알 수 없는 오류'}`,
status: 'API_ERROR',
retCode: retCode,
@@ -159,8 +146,9 @@ export const getShopperHouseSearch =
endpoint: URLS.GET_SHOPPERHOUSE_SEARCH,
query: query,
searchId: searchId,
response: response.data
}));
response: response.data,
})
);
return;
}
@@ -171,15 +159,17 @@ export const getShopperHouseSearch =
console.log('[VoiceInput] 📥 API 응답 실패 (result 데이터 없음)');
// ✨ result 데이터 없음 에러를 Redux 에러 상태에 저장
dispatch(setShopperHouseError({
dispatch(
setShopperHouseError({
message: 'API 응답에 result 데이터가 없습니다',
status: 'NO_RESULT_DATA',
timestamp: Date.now(),
endpoint: URLS.GET_SHOPPERHOUSE_SEARCH,
query: query,
searchId: searchId,
response: response.data
}));
response: response.data,
})
);
return;
}
@@ -187,15 +177,13 @@ export const getShopperHouseSearch =
// 📥 [VoiceInput] API 응답 성공 로그
const resultData = response.data.data.result;
const results = resultData.results || [];
const receivedSearchId =
results.length > 0 ? results[0].searchId : null;
const receivedSearchId = results.length > 0 ? results[0].searchId : null;
const relativeQueries = results.length > 0 ? results[0].relativeQueries : null;
console.log("[VoiceInput] 📥 API 응답 성공");
console.log(
"[VoiceInput] ├─ searchId:",
receivedSearchId || "(없음)"
);
console.log("[VoiceInput] └─ 결과 수:", results.length);
console.log('[VoiceInput] 📥 API 응답 성공');
console.log('[VoiceInput] ├─ searchId:', receivedSearchId || '(없음)');
console.log('[VoiceInput] ├─ 결과 수:', results.length);
console.log('[VoiceInput] └─ relativeQueries:', relativeQueries || '(없음)');
dispatch({
type: types.GET_SHOPPERHOUSE_SEARCH,
@@ -204,18 +192,12 @@ export const getShopperHouseSearch =
dispatch(updateSearchTimestamp());
} else {
console.log(
"[ShopperHouse] ❌ [DEBUG] 오래된 응답 무시 - Redux 업데이트 안함"
);
console.log('[ShopperHouse] ❌ [DEBUG] 오래된 응답 무시 - Redux 업데이트 안함');
}
};
const onFail = (error) => {
console.error(
"[ShopperHouse] getShopperHouseSearch onFail: ",
JSON.stringify(error)
);
console.error('[ShopperHouse] getShopperHouseSearch onFail: ', JSON.stringify(error));
// ✨ 현재 요청이 최신 요청인지 확인
if (currentSearchKey === getShopperHouseSearchKey) {
@@ -241,7 +223,11 @@ export const getShopperHouseSearch =
}
// ✅ 네트워크 연결 관련 오류 중 일시적인 오류 필터링
if (status === 0 || errorMessage?.includes('Network Error') || errorMessage?.includes('timeout')) {
if (
status === 0 ||
errorMessage?.includes('Network Error') ||
errorMessage?.includes('timeout')
) {
console.log('[VoiceInput] ⚠️ 일시적인 네트워크 오류 - 재시도 필요');
console.log('[VoiceInput] └─ TAxios 큐에 의해 자동 재시도될 수 있습니다');
// 일시적인 네트워크 오류는 Redux에 저장하지 않음
@@ -254,7 +240,8 @@ export const getShopperHouseSearch =
console.log('[VoiceInput] ├─ status:', status);
console.log('[VoiceInput] └─ error:', errorMessage || JSON.stringify(error));
dispatch(setShopperHouseError({
dispatch(
setShopperHouseError({
message: errorMessage || 'Unknown API error',
status: status,
statusText: error?.statusText,
@@ -264,8 +251,9 @@ export const getShopperHouseSearch =
endpoint: URLS.GET_SHOPPERHOUSE_SEARCH,
query: query,
searchId: searchId,
originalError: error
}));
originalError: error,
})
);
} else {
console.log('[ShopperHouse] ❌ [DEBUG] 오래된 에러 응답 무시 - Redux 업데이트 안함');
}
@@ -275,21 +263,9 @@ export const getShopperHouseSearch =
if (searchId) {
params.searchid = searchId;
}
console.log(
"[ShopperHouse] getShopperHouseSearch params: ",
JSON.stringify(params)
);
console.log('[ShopperHouse] getShopperHouseSearch params: ', JSON.stringify(params));
TAxios(
dispatch,
getState,
"post",
URLS.GET_SHOPPERHOUSE_SEARCH,
{},
params,
onSuccess,
onFail
);
TAxios(dispatch, getState, 'post', URLS.GET_SHOPPERHOUSE_SEARCH, {}, params, onSuccess, onFail);
};
// ShopperHouse API 에러 처리 액션
@@ -306,9 +282,7 @@ export const setShopperHouseError = (error) => {
// ShopperHouse 검색 데이터 초기화
export const clearShopperHouseData = () => {
// ✨ 검색 키 초기화 - 진행 중인 요청의 응답을 무시하도록
console.log(
"[ShopperHouse] 🧹 [DEBUG] clearShopperHouseData - 이전 요청 무효화"
);
console.log('[ShopperHouse] 🧹 [DEBUG] clearShopperHouseData - 이전 요청 무효화');
getShopperHouseSearchKey = null;
return {
@@ -320,7 +294,7 @@ export const clearShopperHouseData = () => {
// Search Main 조회 IF-LGSP-097
export const getSearchMain = () => (dispatch, getState) => {
const onSuccess = (response) => {
console.log("getSearchMain onSuccess: ", response.data);
console.log('getSearchMain onSuccess: ', response.data);
dispatch({
type: types.GET_SEARCH_MAIN,
@@ -329,19 +303,10 @@ export const getSearchMain = () => (dispatch, getState) => {
};
const onFail = (error) => {
console.error("getSearchMain onFail: ", error);
console.error('getSearchMain onFail: ', error);
};
TAxios(
dispatch,
getState,
"post",
URLS.GET_SEARCH_MAIN,
{},
{},
onSuccess,
onFail
);
TAxios(dispatch, getState, 'post', URLS.GET_SEARCH_MAIN, {}, {}, onSuccess, onFail);
};
// 검색 메인 데이터 초기화

View File

@@ -8,6 +8,7 @@ const initialState = {
searchTimestamp: null,
shopperHouseData: null,
shopperHouseSearchId: null,
shopperHouseRelativeQueries: null, // ✨ relativeQueries는 독립적으로 저장 (searchId와 별개)
// 🔽 검색 메인 데이터 추가
searchMainData: {
@@ -62,10 +63,25 @@ export const searchReducer = (state = initialState, action) => {
};
case types.RESET_VOICE_SEARCH:
console.log('[VoiceInput]-searchReducer-RESET_VOICE_SEARCH');
console.log(
JSON.stringify(
{
action: 'RESET_VOICE_SEARCH',
shopperHouseData_cleared: true,
shopperHouseSearchId_cleared: true,
shopperHouseRelativeQueries_preserved: state.shopperHouseRelativeQueries || '(없음)',
relativeQueries_length: state.shopperHouseRelativeQueries?.length || 0,
},
null,
2
)
);
return {
...state,
shopperHouseData: null,
shopperHouseSearchId: null,
// ✨ relativeQueries는 유지 (다음 PROMPT 모드에서 표시하기 위해)
};
case types.SET_SEARCH_INIT_PERFORMED:
@@ -91,17 +107,36 @@ export const searchReducer = (state = initialState, action) => {
const results = resultData.results || [];
// searchId 추출 (첫 번째 result에서)
// searchId와 relativeQueries 추출 (첫 번째 result에서)
const searchId = results.length > 0 ? results[0].searchId : null;
const relativeQueries = results.length > 0 ? results[0].relativeQueries : null;
// [VoiceInput] Redux에 searchId 저장 로그
console.log('[VoiceInput] 💾 Redux에 searchId 저장');
console.log('[VoiceInput] ─ searchId:', searchId || '(없음)');
// [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
)
);
return {
...state,
shopperHouseData: resultData,
shopperHouseSearchId: searchId,
shopperHouseRelativeQueries: relativeQueries, // ✨ relativeQueries 별도 저장
// ShopperHouse 검색 시 일반 검색 데이터 초기화
searchDatas: {},
totalCount: {},
@@ -120,12 +155,29 @@ export const searchReducer = (state = initialState, action) => {
};
case types.CLEAR_SHOPPERHOUSE_DATA:
console.log('[VoiceInput] 🧹 Redux shopperHouseData 초기화 (searchId는 유지)');
console.log(
'[VoiceInput] 🧹 Redux shopperHouseData 초기화 (searchId & relativeQueries는 유지)'
);
console.log('[VoiceInput]-searchReducer-CLEAR_SHOPPERHOUSE_DATA');
console.log(
JSON.stringify(
{
shopperHouseData_cleared: true,
shopperHouseSearchId_preserved: state.shopperHouseSearchId || '(없음)',
shopperHouseRelativeQueries_preserved: state.shopperHouseRelativeQueries || '(없음)',
relativeQueries_length: state.shopperHouseRelativeQueries?.length || 0,
},
null,
2
)
);
return {
...state,
shopperHouseData: null,
// ✅ searchId는 2번째 발화에서 필요하므로 유지!
// shopperHouseSearchId: null, // ❌ 제거됨 - searchId를 유지해야 2차 발화에서 searchId 포함 가능
// ✨ relativeQueries도 PROMPT 모드에서 표시하기 위해 유지!
// shopperHouseRelativeQueries는 유지
shopperHouseError: null, // 데이터 초기화 시 오류도 초기화
};

View File

@@ -353,7 +353,9 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
console.log(
'[DEBUG]-VOICE_RESULT: Clearing ShopperHouse data (searchId will be preserved for 2nd search)'
);
dispatch(clearShopperHouseData()); // ✨ shopperHouseData만 초기화, searchId 유지
console.log('[VoiceInput]-SearchPanel-onCancel-VOICE_RESULT');
console.log('[VoiceInput] 🧹 VOICE_RESULT 모드에서 ESC 누름 - clearShopperHouseData 호출');
dispatch(clearShopperHouseData()); // ✨ shopperHouseData만 초기화, searchId & relativeQuerys 유지
Spotlight.focus(SPOTLIGHT_IDS.SEARCH_INPUT_BOX);
return;
}

View File

@@ -226,10 +226,12 @@ const SearchResultsNew = ({ itemInfo, showInfo, themeInfo, shopperHouseInfo, key
[themeInfo]
);
// relativeQuerys 가져오기 (ShopperHouse API 응답)
const relativeQuerys = useMemo(() => {
if (shopperHouseInfo?.results?.[0]?.relativeQuerys) {
return shopperHouseInfo.results[0].relativeQuerys;
// relativeQueries 가져오기 (Redux에서 제공)
// Redux의 shopperHouseRelativeQueries를 사용하여 데이터 유지
const relativeQueries = useMemo(() => {
if (shopperHouseInfo?.results?.[0]?.relativeQueries) {
// Redux에서 받은 relativeQueries 사용
return shopperHouseInfo.results[0].relativeQueries;
}
// 기본값
return ['Puppy food', 'Dog toy', 'Fitness'];
@@ -239,7 +241,7 @@ const SearchResultsNew = ({ itemInfo, showInfo, themeInfo, shopperHouseInfo, key
<div className={css.searchBox}>
{/* HowAboutThese Small 버전 - 기본 인라인 표시 */}
<HowAboutTheseSmall
relativeQueries={relativeQuerys}
relativeQueries={relativeQueries}
onQueryClick={handleSmallQueryClick}
onSeeMoreClick={handleShowFullHowAboutThese}
/>
@@ -248,7 +250,7 @@ const SearchResultsNew = ({ itemInfo, showInfo, themeInfo, shopperHouseInfo, key
{howAboutTheseMode === HOW_ABOUT_THESE_MODES.FULL && (
<div className={css.howAboutTheseOverlay}>
<HowAboutThese
relativeQueries={relativeQuerys}
relativeQueries={relativeQueries}
onQueryClick={handleFullQueryClick}
onClose={handleCloseHowAboutThese}
/>

View File

@@ -314,6 +314,9 @@ const VoiceInputOverlay = ({
// Redux에서 shopperHouse 검색 결과 및 에러 가져오기 (simplified ref usage)
const shopperHouseData = useSelector((state) => state.search.shopperHouseData);
const shopperHouseSearchId = useSelector((state) => state.search.shopperHouseSearchId); // 2차 발화용 searchId
const shopperHouseRelativeQueries = useSelector(
(state) => state.search.shopperHouseRelativeQueries
); // ✨ 관련 검색어
const shopperHouseError = useSelector((state) => state.search.shopperHouseError); // API 에러 정보
const shopperHouseDataRef = useRef(null);
const isInitializingRef = useRef(false); // overlay 초기화 중 플래그
@@ -1218,20 +1221,62 @@ const VoiceInputOverlay = ({
}
switch (currentMode) {
case VOICE_MODES.PROMPT:
case VOICE_MODES.PROMPT: {
// ✨ relativeQueries가 있으면 'How about these ?' 표시, 없으면 'Try saying' 표시
const hasRelativeQueries = !!shopperHouseRelativeQueries;
const promptTitle = hasRelativeQueries ? 'How about these ?' : 'Try saying';
// relativeQueries가 있으면 사용, 없으면 searchHistory 사용
const promptSuggestions = hasRelativeQueries ? shopperHouseRelativeQueries : searchHistory;
// ✨ [DEBUG] Redux 상태 확인 로그
console.log('[VoiceInput]-shopperHouseRelativeQueries');
console.log(
JSON.stringify(
{
hasRelativeQueries: hasRelativeQueries,
relativeQueries: shopperHouseRelativeQueries,
relativeQueries_type: typeof shopperHouseRelativeQueries,
relativeQueries_isArray: Array.isArray(shopperHouseRelativeQueries),
relativeQueries_length: shopperHouseRelativeQueries?.length || 0,
promptTitle: promptTitle,
promptSuggestions: promptSuggestions,
promptSuggestions_length: promptSuggestions?.length || 0,
searchHistory_length: searchHistory?.length || 0,
shopperHouseSearchId: shopperHouseSearchId || '(없음)',
shopperHouseData_exists: !!shopperHouseData,
},
null,
2
)
);
if (DEBUG_MODE) {
console.log(
'✅ [DEBUG][VoiceInputOverlay] MODE = PROMPT | Rendering VoicePromptScreen with',
searchHistory.length,
'✅ [DEBUG][VoiceInputOverlay] MODE = PROMPT | Rendering VoicePromptScreen',
hasRelativeQueries ? '(with relativeQueries)' : '(with searchHistory)',
promptSuggestions.length,
'suggestions'
);
if (hasRelativeQueries) {
console.log(
'[DEBUG][VoiceInputOverlay] ├─ searchId:',
shopperHouseSearchId || '(없음)'
);
console.log('[DEBUG][VoiceInputOverlay] ├─ relativeQueries:', promptSuggestions);
console.log('[DEBUG][VoiceInputOverlay] └─ promptTitle:', promptTitle);
} else {
console.log('[DEBUG][VoiceInputOverlay] ├─ relativeQueries가 없음');
console.log('[DEBUG][VoiceInputOverlay] └─ searchHistory 사용:', promptSuggestions);
}
}
return (
<VoicePromptScreen
suggestions={searchHistory}
title={promptTitle}
suggestions={promptSuggestions}
onSuggestionClick={handleSuggestionClick}
/>
);
}
case VOICE_MODES.LISTENING:
if (DEBUG_MODE) {
console.log(
@@ -1318,6 +1363,7 @@ const VoiceInputOverlay = ({
restartWebSpeech,
shopperHouseError,
shopperHouseSearchId,
shopperHouseRelativeQueries, // ✨ relativeQueries 변경 감지
handleApiErrorRetry,
handleApiErrorRestart,
]);