Merge branch 'detail_v3' of http://gitlab.t-win.kr/ifheone/shoptime into detail_v3

This commit is contained in:
2025-10-29 15:24:33 +09:00
6 changed files with 143 additions and 50 deletions

View File

@@ -1,14 +1,23 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import {
useCallback,
useEffect,
useMemo,
useState,
} from 'react';
import { useDispatch } from 'react-redux';
import { getSearch, getShopperHouseSearch } from '../actions/searchActions';
import {
readSearchHistory,
writeSearchHistory,
getSearch,
getShopperHouseSearch,
} from '../actions/searchActions';
import {
addSearchHistory,
removeSearchHistory,
clearAllSearchHistory,
migrateVoiceSearchHistory,
readSearchHistory,
removeSearchHistory,
writeSearchHistory,
} from '../utils/searchHistory';
export const useSearchHistory = () => {
@@ -23,7 +32,7 @@ export const useSearchHistory = () => {
const normalSearches = useMemo(() => {
return searchHistory
.filter(item => item.type === 'normal')
.slice(0, 8); // SearchPanel에서 최대 8개 표시
.slice(0, 5); // SearchPanel에서 최대 5개 표시
}, [searchHistory]);
// 음성 검색 기록 필터링
@@ -49,6 +58,26 @@ export const useSearchHistory = () => {
setSearchHistory(newHistory);
}, []);
// 일반 검색 기록 삭제
const removeNormalSearch = useCallback((query) => {
if (!query || typeof query !== 'string') {
console.warn('[useSearchHistory] Invalid query for removal');
return;
}
const currentHistory = readSearchHistory();
const itemToRemove = currentHistory.find(
(item) => item.query === query.trim() && item.type === 'normal'
);
if (itemToRemove) {
const newHistory = removeSearchHistory(itemToRemove.timestamp);
setSearchHistory(newHistory);
} else {
console.warn(`[useSearchHistory] Normal search not found: "${query}"`);
}
}, []);
// 음성 검색 기록 추가
const addVoiceSearch = useCallback((query, searchId = null) => {
if (!query || typeof query !== 'string' || !query.trim()) {
@@ -125,6 +154,7 @@ export const useSearchHistory = () => {
// 액션 함수
addNormalSearch,
removeNormalSearch,
addVoiceSearch,
executeSearchFromHistory,
removeHistoryItem,

View File

@@ -234,25 +234,34 @@ const BuyOption = ({
// 포커스 관리 로직 (SingleOption과 유사)
useEffect(() => {
if (!isSpotlight) {
// isSpotlight이 false면 일반적인 BuyOption 포커스
console.log('[BuyOption] Component mounted - focusing BUY NOW button');
setTimeout(() => {
Spotlight.focus('buy-option-buy-now-button');
}, 100);
return;
}
// if (!isSpotlight) {
// // isSpotlight이 false면 일반적인 BuyOption 포커스
// console.log('[BuyOption] Component mounted - focusing BUY NOW button');
// isSpotlight이 true이고 SingleOption 동작이 필요한 경우
// Spotlight.focus('buy-option-buy-now-button');
// return;
// }
// isSpotlight이 true이고 SingleOption 동작이 필요한 경우 요부분
if (productInfo?.optProdYn === 'N') {
Spotlight.focus('buy-option-buy-now-button');
} else if (productOptionInfos && productOptionInfos.length > 0) {
} else if (
productOptionInfos &&
productOptionInfos.length > 1 &&
productInfo?.optProdYn === 'Y'
) {
Spotlight.focus('buy-option-first-dropdown');
if (productOptionInfos.length === 1) {
Spotlight.focus('buy-option-second-dropdown');
}
} else if (
productOptionInfos[selectedBtnOptIdx]?.prdtOptDtl &&
productOptionInfos[selectedBtnOptIdx]?.prdtOptDtl.length > 1 &&
productInfo?.optProdYn === 'Y'
) {
Spotlight.focus('buy-option-second-dropdown');
} else {
Spotlight.focus('buy-option-buy-now-button');
}
}, [productOptionInfos, productInfo, isSpotlight]);
}, [productOptionInfos, productInfo]);
// checkOutValidate 콜백 함수 (SingleOption과 동일한 로직)
function checkOutValidate(response) {

View File

@@ -41,7 +41,8 @@ const SearchInputOverlay = ({
handleClick,
}) => {
const dispatch = useDispatch();
const { addNormalSearch, normalSearches } = useSearchHistory();
const { addNormalSearch, removeNormalSearch, normalSearches } =
useSearchHistory();
// ✨ [Phase 3] Input 포커스 상태에 따른 placeholder 동적 변경
const [inputFocused, setInputFocused] = useState(false);
@@ -99,6 +100,12 @@ const SearchInputOverlay = ({
[dispatch, addNormalSearch, onClose, onSearchChange]
);
const handleRemoveSearch = (query) => {
if (query && query.trim()) {
removeNormalSearch(query.trim());
}
};
const handleKeydown = useCallback(
(e) => {
// Enter 키 또는 OK 키 처리
@@ -220,11 +227,18 @@ const SearchInputOverlay = ({
// 검색어 설정 후 검색 실행
handleClick(keyword);
}}
tabIndex={0}
spotlightId={`recent-Resultkeyword-${index}`}
>
{keyword}
</SpottableKeyword>
<SpottableKeyword className={css.closeBtn}>
<SpottableKeyword
className={css.closeBtn}
onClick={() => {
handleRemoveSearch(keyword);
}}
tabIndex={0}
>
<img src={closeImg} className={css.closeBtnImg} />
</SpottableKeyword>
</div>

View File

@@ -32,7 +32,7 @@
width: 100%;
height: 100%;
z-index: 1002;
pointer-events: none; // 빈 공간 클릭 시 dimBackground로 이벤트 전달
// pointer-events: none; // 빈 공간 클릭 시 dimBackground로 이벤트 전달
display: flex;
flex-direction: column;
align-items: center;
@@ -156,7 +156,7 @@
font-size: 24px;
font-family: 'LG Smart UI';
font-weight: 700;
line-height: 24px;
line-height: 62px;
text-align: center;
word-wrap: break-word;
//timeStamp
@@ -164,6 +164,8 @@
background-position: 15px center;
background-repeat: no-repeat;
background-size: 26px 25px;
max-width: 300px;
.elip(1);
&:hover {
background: @PRIMARY_COLOR_RED;
border: 5px solid @PRIMARY_COLOR_RED;

View File

@@ -362,13 +362,6 @@
cursor: pointer;
transition: all 0.2s ease;
height: 64px;
> * {
margin-bottom: 10px;
&:last-child {
margin-bottom: 0;
}
}
color: black;
font-size: 24px;
@@ -378,6 +371,13 @@
text-align: center;
word-wrap: break-word;
> * {
margin-bottom: 10px;
&:last-child {
margin-bottom: 0;
}
}
&:hover {
border-color: @PRIMARY_COLOR_RED;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
@@ -459,7 +459,7 @@
}
.keywordButton {
padding: 20px;
padding: 0 20px;
background: white;
border-radius: 100px;
border: 5px solid #dadada;
@@ -482,10 +482,11 @@
font-size: 24px;
font-family: 'LG Smart UI';
font-weight: 700;
line-height: 24px;
line-height: 63px;
text-align: center;
word-wrap: break-word;
max-width: 300px;
.elip(1);
&:hover {
border-color: @PRIMARY_COLOR_RED;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);

View File

@@ -1,26 +1,44 @@
// src/views/SearchPanel/SearchPanel.new.jsx
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import classNames from 'classnames';
import { useDispatch, useSelector } from 'react-redux';
import {
useDispatch,
useSelector,
} from 'react-redux';
import Spotlight from '@enact/spotlight';
import SpotlightContainerDecorator from '@enact/spotlight/SpotlightContainerDecorator';
import SpotlightContainerDecorator
from '@enact/spotlight/SpotlightContainerDecorator';
import Spottable from '@enact/spotlight/Spottable';
import micIcon from '../../../assets/images/searchpanel/image-mic.png';
import hotPicksImage from '../../../assets/images/searchpanel/img-hotpicks.png';
import hotPicksBrandImage from '../../../assets/images/searchpanel/img-search-hotpicks.png';
import { sendLogGNB, sendLogTotalRecommend } from '../../actions/logActions';
import { getMyRecommandedKeyword } from '../../actions/myPageActions';
import { popPanel, pushPanel, updatePanel } from '../../actions/panelActions';
import hotPicksBrandImage
from '../../../assets/images/searchpanel/img-search-hotpicks.png';
import {
sendLogGNB,
sendLogTotalRecommend,
} from '../../actions/logActions';
import { getMyRecommandedKeyword } from '../../actions/myPageActions';
import {
popPanel,
pushPanel,
updatePanel,
} from '../../actions/panelActions';
import {
clearShopperHouseData,
getSearch,
getSearchMain,
getShopperHouseSearch,
resetSearch,
resetVoiceSearch,
clearShopperHouseData,
getShopperHouseSearch,
} from '../../actions/searchActions';
// import {
// showErrorToast,
@@ -32,20 +50,32 @@ import {
// } from '../../actions/toastActions';
import TBody from '../../components/TBody/TBody';
import TPanel from '../../components/TPanel/TPanel';
import TVerticalPagenator from '../../components/TVerticalPagenator/TVerticalPagenator';
import TVirtualGridList from '../../components/TVirtualGridList/TVirtualGridList';
import TVerticalPagenator
from '../../components/TVerticalPagenator/TVerticalPagenator';
import TVirtualGridList
from '../../components/TVirtualGridList/TVirtualGridList';
import usePanelHistory from '../../hooks/usePanelHistory/usePanelHistory';
// import VirtualKeyboardContainer from "../../components/TToast/VirtualKeyboardContainer";
import usePrevious from '../../hooks/usePrevious';
import { useSearchHistory } from '../../hooks/useSearchHistory';
import usePanelHistory from '../../hooks/usePanelHistory/usePanelHistory';
import { LOG_CONTEXT_NAME, LOG_MENU, LOG_MESSAGE_ID, panel_names } from '../../utils/Config';
import {
LOG_CONTEXT_NAME,
LOG_MENU,
LOG_MESSAGE_ID,
panel_names,
} from '../../utils/Config';
import NoSearchResults from './NoSearchResults/NoSearchResults';
// import NoSearchResults from './NoSearchResults/NoSearchResults';
import SearchInputOverlay from './SearchInputOverlay';
import css from './SearchPanel.new.module.less';
import SearchResultsNew from './SearchResults.new.v2';
import TInputSimple, { ICONS, KINDS } from './TInput/TInputSimple';
import VoiceInputOverlay, { VOICE_MODES } from './VoiceInputOverlay/VoiceInputOverlay';
import TInputSimple, {
ICONS,
KINDS,
} from './TInput/TInputSimple';
import VoiceInputOverlay, {
VOICE_MODES,
} from './VoiceInputOverlay/VoiceInputOverlay';
/**
* ✨ Mode-Based Architecture 도입
@@ -277,7 +307,7 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
/**
* useSearchHistory Hook 적용
*/
const { normalSearches, addNormalSearch, executeSearchFromHistory } = useSearchHistory();
const { normalSearches, addNormalSearch, refreshHistory, executeSearchFromHistory } = useSearchHistory();
/**
* 🎯 [DetailPanel 복귀 감지] usePanelHistory Hook 적용
@@ -1738,6 +1768,13 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
//모드 변경시 최근 검색어 새로고침.
useEffect(() => {
if (currentMode === SEARCH_PANEL_MODES.INITIAL && isOnTop) {
refreshHistory();
}
}, [currentMode, isOnTop, refreshHistory]);
return (
<TPanel className={css.container} handleCancel={onCancel} spotlightId={spotlightId}>
{/* ✨ [Phase 2] spotlightDisabled를 currentMode로 제어 */}