[251028] feat: HowAboutTheseResponse-1
🕐 커밋 시간: 2025. 10. 28. 19:47:39 📊 변경 통계: • 총 파일: 6개 • 추가: +142줄 • 삭제: -38줄 📝 수정된 파일: ~ com.twin.app.shoptime/src/actions/searchActions.js ~ com.twin.app.shoptime/src/views/SearchPanel/HowAboutThese/HowAboutThese.response.jsx ~ com.twin.app.shoptime/src/views/SearchPanel/HowAboutThese/HowAboutThese.response.module.less ~ 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/SearchResults.new.v2.module.less 🔧 주요 변경 내용: • 핵심 비즈니스 로직 개선 • 중간 규모 기능 개선 • 모듈 구조 개선
This commit is contained in:
@@ -99,6 +99,10 @@ let getShopperHouseSearchKey = null;
|
||||
export const getShopperHouseSearch =
|
||||
(query, searchId = null) =>
|
||||
(dispatch, getState) => {
|
||||
|
||||
// 이전 데이터 초기화 -> shopperHouseData만 초기화
|
||||
dispatch({ type: types.CLEAR_SHOPPERHOUSE_DATA });
|
||||
|
||||
// 새로운 검색 시작 - 고유 키 생성
|
||||
const currentSearchKey = new Date().getTime();
|
||||
getShopperHouseSearchKey = currentSearchKey;
|
||||
|
||||
@@ -15,6 +15,10 @@ const OverlayContainer = SpotlightContainerDecorator(
|
||||
);
|
||||
|
||||
const HowAboutTheseResponse = ({ searchId = null, isLoading = true, onClose }) => {
|
||||
useEffect(() => {
|
||||
console.log('[HowAboutTheseResponse] 🎯 상태 변경', { searchId, isLoading });
|
||||
}, [searchId, isLoading]);
|
||||
|
||||
const [typedText, setTypedText] = useState('');
|
||||
const [currentMessageIndex, setCurrentMessageIndex] = useState(0);
|
||||
const typingTimerRef = useRef(null);
|
||||
|
||||
@@ -3,16 +3,18 @@
|
||||
|
||||
.bgcontainer {
|
||||
width: 100%;
|
||||
height: calc(100vh - 210px);
|
||||
height: 100vh;
|
||||
background: linear-gradient(
|
||||
360deg,
|
||||
rgba(221.25, 221.25, 221.25, 0) 2%,
|
||||
rgba(221.25, 221.25, 221.25, 0.85) 58%,
|
||||
rgba(221.25, 221.25, 221.25, 0.9) 80%,
|
||||
rgba(221, 221, 221, 0.9) 100%
|
||||
rgba(221, 221, 221, 0.15) 2%,
|
||||
rgba(221, 221, 221, 0.85) 58%,
|
||||
rgba(221, 221, 221, 0.92) 80%,
|
||||
rgba(221, 221, 221, 0.95) 100%
|
||||
);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.container {
|
||||
@@ -111,7 +113,7 @@
|
||||
|
||||
/* 타이핑 애니메이션 텍스트 - 검은색으로 변경 */
|
||||
.typingText {
|
||||
color: #272727; // 검은색 (흰색 대신)
|
||||
color: #1a1a1a;
|
||||
font-size: 46px;
|
||||
font-family: "LG Smart UI";
|
||||
font-weight: 400;
|
||||
@@ -121,7 +123,7 @@
|
||||
|
||||
/* 깜빡이는 커서 - 검은색으로 변경 */
|
||||
.cursor {
|
||||
color: #272727; // 검은색 (흰색 대신)
|
||||
color: #1a1a1a;
|
||||
font-size: 34px;
|
||||
font-family: "LG Smart UI";
|
||||
font-weight: 400;
|
||||
|
||||
@@ -144,6 +144,7 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
|
||||
const panels = useSelector((state) => state.panels.panels);
|
||||
// 0hun: 음성 검색 결과에 대한 전역 상태
|
||||
const shopperHouseData = useSelector((state) => state.search.shopperHouseData);
|
||||
const shopperHouseError = useSelector((state) => state.search.shopperHouseError);
|
||||
// 0hun: 음성 검색 searchId (Redux에서 별도 관리)
|
||||
const shopperHouseSearchId = useSelector((state) => state.search.shopperHouseSearchId);
|
||||
// 0hun: 음성 검색 relativeQueries (Redux에서 별도 관리)
|
||||
@@ -181,6 +182,7 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
|
||||
|
||||
// ✨ [Phase 1] SearchPanel의 현재 모드 상태 (VoiceInputOverlay의 VOICE_MODES와 동일한 개념)
|
||||
const [currentMode, setCurrentMode] = useState(SEARCH_PANEL_MODES.INITIAL);
|
||||
const [isShopperHousePending, setIsShopperHousePending] = useState(false);
|
||||
const [voiceOverlayMode, setVoiceOverlayMode] = useState(VOICE_MODES.PROMPT);
|
||||
const [voiceOverlayResponseText, setVoiceOverlayResponseText] = useState('');
|
||||
const [isVoiceOverlayBubbleSearch, setIsVoiceOverlayBubbleSearch] = useState(false);
|
||||
@@ -547,6 +549,7 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
|
||||
currentMode,
|
||||
searchQuery,
|
||||
hasShopperHouseData: !!shopperHouseData,
|
||||
isShopperHousePending,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -593,6 +596,7 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
|
||||
console.log('[VoiceInput] 🧹 VOICE_RESULT 모드에서 ESC 누름 - clearShopperHouseData 호출');
|
||||
}
|
||||
// 🎯 [포커스 로직 통합] 포커스는 상태 변경에 의해 자동으로 처리됨
|
||||
setIsShopperHousePending(false);
|
||||
dispatch(clearShopperHouseData()); // ✨ shopperHouseData만 초기화, searchId & relativeQuerys 유지
|
||||
return;
|
||||
}
|
||||
@@ -1027,13 +1031,21 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
|
||||
setSearchQuery(trimmedQuery);
|
||||
|
||||
// Voice overlay를 RESPONSE 모드로 전환하여 대기 화면 표시
|
||||
setVoiceOverlayResponseText(trimmedQuery);
|
||||
setVoiceOverlayMode(VOICE_MODES.RESPONSE);
|
||||
setIsVoiceOverlayBubbleSearch(true);
|
||||
setIsVoiceOverlayVisible(true);
|
||||
setShouldFocusVoiceResult(false);
|
||||
// setVoiceOverlayResponseText(trimmedQuery);
|
||||
// setVoiceOverlayMode(VOICE_MODES.RESPONSE);
|
||||
// setIsVoiceOverlayBubbleSearch(true);
|
||||
// setIsVoiceOverlayVisible(true);
|
||||
// setShouldFocusVoiceResult(false);
|
||||
|
||||
dispatch(getShopperHouseSearch(trimmedQuery, shopperHouseSearchId));
|
||||
// API 호출 전에 이전 데이터 초기화
|
||||
setIsShopperHousePending(true);
|
||||
dispatch(clearShopperHouseData());
|
||||
|
||||
// Redux state 업데이트를 위해 약간의 지연 후 API 호출
|
||||
setTimeout(() => {
|
||||
console.log('[HowAboutThese] 🔄 Redux 업데이트 후 API 호출');
|
||||
dispatch(getShopperHouseSearch(trimmedQuery, shopperHouseSearchId));
|
||||
}, 50); // 50ms 지연
|
||||
|
||||
// 🎯 [포커스 로직 통합] 검색어만 업데이트
|
||||
// 포커스는 shopperHouseData 변경에 의해 자동으로 처리됨
|
||||
@@ -1050,6 +1062,20 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
|
||||
[dispatch, shopperHouseSearchId]
|
||||
);
|
||||
|
||||
// ShopperHouse API 응답 도착 시 로딩 상태 해제
|
||||
useEffect(() => {
|
||||
if (shopperHouseData) {
|
||||
setIsShopperHousePending(false);
|
||||
}
|
||||
}, [shopperHouseData]);
|
||||
|
||||
// ShopperHouse API 오류 발생 시 로딩 상태 해제
|
||||
useEffect(() => {
|
||||
if (shopperHouseError) {
|
||||
setIsShopperHousePending(false);
|
||||
}
|
||||
}, [shopperHouseError]);
|
||||
|
||||
// ✨ [Phase 3] handleInputIconClick 제거 (돋보기 아이콘 기능 비활성화)
|
||||
// /**
|
||||
// * ✨ [Phase 3] TInput icon click handler (showVirtualKeyboard 제거)
|
||||
@@ -1355,12 +1381,13 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
|
||||
searchQuery,
|
||||
hasSearchResults: !!(
|
||||
searchDatas?.theme?.length > 0 ||
|
||||
searchDatas?.item?.length > 0 ||
|
||||
searchDatas?.show?.length > 0
|
||||
),
|
||||
isSearchOverlayVisible,
|
||||
currentMode,
|
||||
});
|
||||
searchDatas?.item?.length > 0 ||
|
||||
searchDatas?.show?.length > 0
|
||||
),
|
||||
isSearchOverlayVisible,
|
||||
isShopperHousePending,
|
||||
currentMode,
|
||||
});
|
||||
}
|
||||
|
||||
// 우선순위 1: 음성 입력 오버레이가 열려있으면 VOICE_INPUT 모드
|
||||
@@ -1371,9 +1398,12 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
|
||||
nextMode = SEARCH_PANEL_MODES.VOICE_INPUT;
|
||||
}
|
||||
// 우선순위 2: 음성 검색 결과가 있으면 VOICE_RESULT 모드
|
||||
else if (shopperHouseData) {
|
||||
else if (shopperHouseData || isShopperHousePending) {
|
||||
if (DEBUG_MODE) {
|
||||
console.log('[DEBUG]-MODE: shopperHouseData EXISTS → VOICE_RESULT');
|
||||
console.log('[DEBUG]-MODE: shopperHouseData EXISTS or pending → VOICE_RESULT', {
|
||||
hasData: !!shopperHouseData,
|
||||
isPending: isShopperHousePending,
|
||||
});
|
||||
}
|
||||
nextMode = SEARCH_PANEL_MODES.VOICE_RESULT;
|
||||
}
|
||||
@@ -1410,6 +1440,7 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
|
||||
console.log(`[DEBUG]-VOICE_RESULT 🔀 Mode changed: ${currentMode} → ${nextMode}`, {
|
||||
isVoiceOverlayVisible,
|
||||
shopperHouseData: !!shopperHouseData,
|
||||
isShopperHousePending,
|
||||
searchPerformed,
|
||||
searchQuery,
|
||||
hasSearchResults: !!(
|
||||
@@ -1430,6 +1461,7 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
|
||||
}, [
|
||||
isVoiceOverlayVisible,
|
||||
shopperHouseData,
|
||||
isShopperHousePending,
|
||||
searchPerformed,
|
||||
searchQuery,
|
||||
searchDatas,
|
||||
|
||||
@@ -167,6 +167,8 @@ const SearchResultsNew = ({
|
||||
|
||||
// HowAboutThese Response 로딩 상태
|
||||
const [isHowAboutTheseLoading, setIsHowAboutTheseLoading] = useState(false);
|
||||
const [hasPendingResponse, setHasPendingResponse] = useState(false);
|
||||
const previousShopperHouseInfoRef = useRef(shopperHouseInfo);
|
||||
|
||||
// HowAboutThese 모드 전환 핸들러
|
||||
const handleShowFullHowAboutThese = useCallback(() => {
|
||||
@@ -182,19 +184,25 @@ const SearchResultsNew = ({
|
||||
const handleCloseHowAboutTheseResponse = useCallback(() => {
|
||||
setHowAboutTheseMode(HOW_ABOUT_THESE_MODES.SMALL);
|
||||
setIsHowAboutTheseLoading(false);
|
||||
setHasPendingResponse(false);
|
||||
}, []);
|
||||
|
||||
// HowAboutThese Response 모드로 전환 핸들러
|
||||
const handleShowHowAboutTheseResponse = useCallback(() => {
|
||||
console.log('[HowAboutThese] 🚀 1. handleShowHowAboutTheseResponse 호출 - RESPONSE 모드로 전환, isLoading: true');
|
||||
console.log('[HowAboutThese] 🚀 현재 shopperHouseInfo 상태:', !!shopperHouseInfo);
|
||||
setHowAboutTheseMode(HOW_ABOUT_THESE_MODES.RESPONSE);
|
||||
setIsHowAboutTheseLoading(true);
|
||||
setHasPendingResponse(true);
|
||||
}, []);
|
||||
|
||||
// 🎯 HowAboutThese 포커스 관리 - 검색 입력 영역 포커스 감지 시 SMALL 모드로 전환
|
||||
// 단, RESPONSE 모드일 때는 우회 (API 응답 대기 중이므로)
|
||||
useEffect(() => {
|
||||
if (
|
||||
onSearchInputFocus &&
|
||||
howAboutTheseMode === HOW_ABOUT_THESE_MODES.FULL
|
||||
howAboutTheseMode === HOW_ABOUT_THESE_MODES.FULL &&
|
||||
howAboutTheseMode !== HOW_ABOUT_THESE_MODES.RESPONSE // ✅ RESPONSE 모드는 우회
|
||||
) {
|
||||
console.log(
|
||||
'[SearchResultsNew] 검색 입력 영역 포커스 감지 - HowAboutThese를 SMALL 모드로 전환'
|
||||
@@ -203,6 +211,55 @@ const SearchResultsNew = ({
|
||||
}
|
||||
}, [onSearchInputFocus, howAboutTheseMode]);
|
||||
|
||||
// HowAboutThese API 응답 받으면 RESPONSE → SMALL 모드로 전환
|
||||
useEffect(() => {
|
||||
console.log('[HowAboutThese] 🔍 2. useEffect 실행');
|
||||
console.log('[HowAboutThese] howAboutTheseMode:', howAboutTheseMode);
|
||||
console.log('[HowAboutThese] isHowAboutTheseLoading:', isHowAboutTheseLoading);
|
||||
console.log('[HowAboutThese] shopperHouseInfo 존재 여부:', !!shopperHouseInfo);
|
||||
console.log('[HowAboutThese] shopperHouseInfo.results 존재 여부:', !!(shopperHouseInfo?.results));
|
||||
console.log('[HowAboutThese] shopperHouseInfo.results.length:', shopperHouseInfo?.results?.length || 0);
|
||||
|
||||
const prevShopperHouseInfo = previousShopperHouseInfoRef.current;
|
||||
|
||||
if (
|
||||
howAboutTheseMode === HOW_ABOUT_THESE_MODES.RESPONSE &&
|
||||
isHowAboutTheseLoading &&
|
||||
shopperHouseInfo &&
|
||||
shopperHouseInfo.results &&
|
||||
shopperHouseInfo.results.length > 0
|
||||
) {
|
||||
if (hasPendingResponse && shopperHouseInfo !== prevShopperHouseInfo) {
|
||||
console.log('[HowAboutThese] ✅ 3. 모든 조건 충족 - RESPONSE → SMALL 모드로 전환');
|
||||
setHowAboutTheseMode(HOW_ABOUT_THESE_MODES.SMALL);
|
||||
setIsHowAboutTheseLoading(false);
|
||||
setHasPendingResponse(false);
|
||||
console.log('[HowAboutThese] 전환 완료: SMALL 모드, isLoading: false');
|
||||
} else {
|
||||
console.log('[HowAboutThese] ⏳ 3-1. API 응답 대기 중 - 동일 데이터 감지, RESPONSE 유지');
|
||||
}
|
||||
} else if (
|
||||
howAboutTheseMode === HOW_ABOUT_THESE_MODES.RESPONSE &&
|
||||
isHowAboutTheseLoading &&
|
||||
shopperHouseInfo &&
|
||||
(!shopperHouseInfo.results || shopperHouseInfo.results.length === 0)
|
||||
) {
|
||||
if (hasPendingResponse && shopperHouseInfo !== prevShopperHouseInfo) {
|
||||
console.log('[HowAboutThese] ✅ 3. API 응답 도착했지만 데이터가 없음 - SMALL 모드로 전환');
|
||||
setHowAboutTheseMode(HOW_ABOUT_THESE_MODES.SMALL);
|
||||
setIsHowAboutTheseLoading(false);
|
||||
setHasPendingResponse(false);
|
||||
console.log('[HowAboutThese] 전환 완료: SMALL 모드, isLoading: false');
|
||||
} else {
|
||||
console.log('[HowAboutThese] ⏳ 3-2. 동일한 빈 결과 감지 - RESPONSE 유지');
|
||||
}
|
||||
} else {
|
||||
console.log('[HowAboutThese] ❌ 3. 조건 불충족 - 상태 유지');
|
||||
}
|
||||
|
||||
previousShopperHouseInfoRef.current = shopperHouseInfo;
|
||||
}, [shopperHouseInfo, howAboutTheseMode, isHowAboutTheseLoading, hasPendingResponse]);
|
||||
|
||||
// buttonTabList 최적화 - 의존성이 변경될 때만 재계산
|
||||
const buttonTabList = useMemo(() => getButtonTabList(), [getButtonTabList]);
|
||||
|
||||
@@ -442,7 +499,7 @@ const SearchResultsNew = ({
|
||||
spotlightId="search-results-container"
|
||||
data-wheel-point="true"
|
||||
>
|
||||
{/* HowAboutThese Small 버전 - relativeQueries가 존재할 때만 표시 */}
|
||||
{/* HowAboutThese Small 버전 - relativeQueries가 존재할 때 항상 표시 (레이아웃 유지용) */}
|
||||
{relativeQueries && relativeQueries.length > 0 && (
|
||||
<div>
|
||||
{/* ✅ [251026] CHANGED: SpottableDiv wrapper 제거 - Spotlight 포커스 네비게이션 개선 */}
|
||||
@@ -467,7 +524,7 @@ const SearchResultsNew = ({
|
||||
if (onRelativeQueryClick) {
|
||||
onRelativeQueryClick(query);
|
||||
}
|
||||
handleCloseHowAboutThese();
|
||||
handleShowHowAboutTheseResponse();
|
||||
}}
|
||||
onClose={handleCloseHowAboutThese}
|
||||
/>
|
||||
@@ -475,13 +532,16 @@ const SearchResultsNew = ({
|
||||
)}
|
||||
{/* HowAboutThese Response 버전 - 로딩 메시지 표시 */}
|
||||
{howAboutTheseMode === HOW_ABOUT_THESE_MODES.RESPONSE && (
|
||||
<div className={css.howAboutTheseOverlay}>
|
||||
<HowAboutTheseResponse
|
||||
searchId={shopperHouseSearchId}
|
||||
isLoading={isHowAboutTheseLoading}
|
||||
onClose={handleCloseHowAboutTheseResponse}
|
||||
/>
|
||||
</div>
|
||||
<>
|
||||
{console.log('[HowAboutThese] 📱 4. HowAboutTheseResponse 렌더링 시작!') || null}
|
||||
<div className={css.howAboutTheseOverlay}>
|
||||
<HowAboutTheseResponse
|
||||
searchId={shopperHouseSearchId}
|
||||
isLoading={isHowAboutTheseLoading}
|
||||
onClose={handleCloseHowAboutTheseResponse}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{themeInfo && themeInfo?.length > 0 && (
|
||||
<div className={css.hotpicksSection} data-wheel-point="true">
|
||||
|
||||
@@ -379,12 +379,14 @@
|
||||
|
||||
// HowAboutThese Full 버전 오버레이
|
||||
.howAboutTheseOverlay {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 860px; // HowAboutThese의 높이
|
||||
z-index: 100;
|
||||
pointer-events: auto; // 오버레이 전체를 클릭 가능하게 (배경 클릭 방지)
|
||||
height: 100vh;
|
||||
z-index: 200;
|
||||
pointer-events: auto;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user