[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:
2025-10-28 19:47:40 +09:00
parent 23d0b5374e
commit c95e8af4cf
6 changed files with 141 additions and 37 deletions

View File

@@ -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;

View File

@@ -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);

View File

@@ -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;

View File

@@ -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,

View File

@@ -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">

View File

@@ -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;
}
}