[251017] fix: SearchPanel TInput
🕐 커밋 시간: 2025. 10. 17. 12:33:29 📊 변경 통계: • 총 파일: 6개 • 추가: +75줄 • 삭제: -48줄 📁 추가된 파일: + com.twin.app.shoptime/src/views/SearchPanel/TInput/TInput.jsx + com.twin.app.shoptime/src/views/SearchPanel/TInput/TInput.module.less 📝 수정된 파일: ~ com.twin.app.shoptime/src/views/SearchPanel/SearchPanel.new.jsx ~ com.twin.app.shoptime/src/views/SearchPanel/SearchPanel.new.module.less ~ com.twin.app.shoptime/src/views/SearchPanel/VoiceInputOverlay/VoiceInputOverlay.jsx ~ com.twin.app.shoptime/src/views/SearchPanel/VoiceInputOverlay/modes/VoiceNotRecognized.module.less 🔧 함수 변경 내용: 📄 com.twin.app.shoptime/src/views/SearchPanel/TInput/TInput.module.less (unknown): ✅ Added: translateY()
This commit is contained in:
@@ -17,16 +17,16 @@ import { sendLogGNB, sendLogTotalRecommend } from '../../actions/logActions';
|
|||||||
import { getMyRecommandedKeyword } from '../../actions/myPageActions';
|
import { getMyRecommandedKeyword } from '../../actions/myPageActions';
|
||||||
import { popPanel, updatePanel } from '../../actions/panelActions';
|
import { popPanel, updatePanel } from '../../actions/panelActions';
|
||||||
import { getSearch, resetSearch, searchMain } from '../../actions/searchActions';
|
import { getSearch, resetSearch, searchMain } from '../../actions/searchActions';
|
||||||
import {
|
// import {
|
||||||
showErrorToast,
|
// showErrorToast,
|
||||||
showInfoToast,
|
// showInfoToast,
|
||||||
showSearchErrorToast,
|
// showSearchErrorToast,
|
||||||
showSearchSuccessToast,
|
// showSearchSuccessToast,
|
||||||
showSuccessToast,
|
// showSuccessToast,
|
||||||
showWarningToast,
|
// showWarningToast,
|
||||||
} from '../../actions/toastActions';
|
// } from '../../actions/toastActions';
|
||||||
import TBody from '../../components/TBody/TBody';
|
import TBody from '../../components/TBody/TBody';
|
||||||
import TInput, { ICONS, KINDS } from '../../components/TInput/TInput';
|
import TInput, { ICONS, KINDS } from './TInput/TInput';
|
||||||
import TPanel from '../../components/TPanel/TPanel';
|
import TPanel from '../../components/TPanel/TPanel';
|
||||||
import TScroller from '../../components/TScroller/TScroller';
|
import TScroller from '../../components/TScroller/TScroller';
|
||||||
import TVerticalPagenator from '../../components/TVerticalPagenator/TVerticalPagenator';
|
import TVerticalPagenator from '../../components/TVerticalPagenator/TVerticalPagenator';
|
||||||
@@ -101,6 +101,16 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId, scrollOpt
|
|||||||
setInputFocus(false);
|
setInputFocus(false);
|
||||||
}, [inputFocus]);
|
}, [inputFocus]);
|
||||||
|
|
||||||
|
// TInput의 입력 모드 상태 (webOS 키보드가 뜨는지 여부)
|
||||||
|
const [isInputModeActive, setIsInputModeActive] = useState(false);
|
||||||
|
const handleInputModeChange = useCallback((isActive) => {
|
||||||
|
console.log(
|
||||||
|
'[SearchPanel] TInput 입력 모드:',
|
||||||
|
isActive ? '활성화 (키보드 표시)' : '비활성화 (키보드 숨김)'
|
||||||
|
);
|
||||||
|
setIsInputModeActive(isActive);
|
||||||
|
}, []);
|
||||||
|
|
||||||
let searchQueryRef = usePrevious(searchQuery);
|
let searchQueryRef = usePrevious(searchQuery);
|
||||||
let isOnTopRef = usePrevious(isOnTop);
|
let isOnTopRef = usePrevious(isOnTop);
|
||||||
|
|
||||||
@@ -281,7 +291,7 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId, scrollOpt
|
|||||||
);
|
);
|
||||||
|
|
||||||
// 검색 시작 알림 (선택사항)
|
// 검색 시작 알림 (선택사항)
|
||||||
dispatch(showSuccessToast(`"${query}" 검색 중...`, { duration: 2000 }));
|
// dispatch(showSuccessToast(`"${query}" 검색 중...`, { duration: 2000 }));
|
||||||
} else {
|
} else {
|
||||||
dispatch(resetSearch());
|
dispatch(resetSearch());
|
||||||
}
|
}
|
||||||
@@ -473,10 +483,10 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId, scrollOpt
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 테스트용 Toast 핸들러들
|
// 테스트용 Toast 핸들러들
|
||||||
const handleTestToasts = useCallback(() => {
|
// const handleTestToasts = useCallback(() => {
|
||||||
// 간단한 Toast 테스트
|
// // 간단한 Toast 테스트
|
||||||
dispatch(showSuccessToast('테스트 메시지입니다', { duration: 3000 }));
|
// dispatch(showSuccessToast('테스트 메시지입니다', { duration: 3000 }));
|
||||||
}, [dispatch]);
|
// }, [dispatch]);
|
||||||
|
|
||||||
// ProductCard 컴포넌트
|
// ProductCard 컴포넌트
|
||||||
const renderItem = useCallback(
|
const renderItem = useCallback(
|
||||||
@@ -565,28 +575,36 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId, scrollOpt
|
|||||||
spotlightId={SPOTLIGHT_IDS.SEARCH_INPUT_LAYER}
|
spotlightId={SPOTLIGHT_IDS.SEARCH_INPUT_LAYER}
|
||||||
>
|
>
|
||||||
<div className={css.searchInputWrapper}>
|
<div className={css.searchInputWrapper}>
|
||||||
<TInput
|
<div
|
||||||
className={css.inputBox}
|
className={classNames(
|
||||||
kind={KINDS.withIcon}
|
css.inputWrapper,
|
||||||
icon={ICONS.search}
|
isInputModeActive && css.inputWrapperActive
|
||||||
value={searchQuery}
|
)}
|
||||||
onChange={handleSearchChange}
|
>
|
||||||
onIconClick={() => {
|
<TInput
|
||||||
if (showVirtualKeyboard) {
|
className={css.inputBox}
|
||||||
handleSearchSubmit(searchQuery);
|
kind={KINDS.withIcon}
|
||||||
} else {
|
icon={ICONS.search}
|
||||||
setShowVirtualKeyboard(true);
|
value={searchQuery}
|
||||||
}
|
onChange={handleSearchChange}
|
||||||
}}
|
onIconClick={() => {
|
||||||
onKeyDown={handleKeydown}
|
if (showVirtualKeyboard) {
|
||||||
onKeyUp={cursorPosition}
|
handleSearchSubmit(searchQuery);
|
||||||
spotlightId={SPOTLIGHT_IDS.SEARCH_INPUT_BOX}
|
} else {
|
||||||
forcedSpotlight="recent-keyword-0"
|
setShowVirtualKeyboard(true);
|
||||||
tabIndex={0}
|
}
|
||||||
spotlightBoxDisabled={true}
|
}}
|
||||||
onFocus={_onFocus}
|
onKeyDown={handleKeydown}
|
||||||
onBlur={_onBlur}
|
onKeyUp={cursorPosition}
|
||||||
/>
|
spotlightId={SPOTLIGHT_IDS.SEARCH_INPUT_BOX}
|
||||||
|
forcedSpotlight="recent-keyword-0"
|
||||||
|
tabIndex={0}
|
||||||
|
spotlightBoxDisabled={true}
|
||||||
|
onFocus={_onFocus}
|
||||||
|
onBlur={_onBlur}
|
||||||
|
onInputModeChange={handleInputModeChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<SpottableMicButton
|
<SpottableMicButton
|
||||||
className={css.microphoneButton}
|
className={css.microphoneButton}
|
||||||
onClick={onClickMic}
|
onClick={onClickMic}
|
||||||
|
|||||||
@@ -70,14 +70,36 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.inputBox {
|
// TInput을 감싸는 wrapper
|
||||||
|
.inputWrapper {
|
||||||
width: 880px;
|
width: 880px;
|
||||||
height: 100px !important;
|
height: 100px;
|
||||||
|
border-radius: 1000px;
|
||||||
|
border: 5px solid #ccc; // 기본 회색 테두리
|
||||||
|
box-sizing: border-box;
|
||||||
|
transition: border 0.2s ease, box-shadow 0.2s ease;
|
||||||
|
|
||||||
|
&:focus-within {
|
||||||
|
border: 5px solid @PRIMARY_COLOR_RED;
|
||||||
|
box-shadow: 0 0 22px 0 rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 입력 모드 활성화 시 (webOS 키보드 표시됨)
|
||||||
|
.inputWrapperActive {
|
||||||
|
outline: 2px solid @PRIMARY_COLOR_RED; // 기존 5px 테두리 위에 2px 추가
|
||||||
|
outline-offset: 0px;
|
||||||
|
box-shadow: 0 0 30px 0 rgba(229, 9, 20, 0.6) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputBox {
|
||||||
|
width: 100%;
|
||||||
|
height: 100% !important;
|
||||||
padding-left: 50px;
|
padding-left: 50px;
|
||||||
padding-right: 40px;
|
padding-right: 40px;
|
||||||
background: white;
|
background: white;
|
||||||
border-radius: 1000px;
|
border-radius: 1000px;
|
||||||
border: 5px solid #ccc;
|
border: none; // wrapper에서 테두리 처리
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@@ -126,12 +148,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&:focus-within,
|
|
||||||
&:focus {
|
|
||||||
border: 5px solid @PRIMARY_COLOR_RED;
|
|
||||||
box-shadow: 0 0 22px 0 rgba(0, 0, 0, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TInput 컴포넌트 자체의 내부 포커스 스타일 제거
|
// TInput 컴포넌트 자체의 내부 포커스 스타일 제거
|
||||||
> div {
|
> div {
|
||||||
&:focus,
|
&:focus,
|
||||||
|
|||||||
151
com.twin.app.shoptime/src/views/SearchPanel/TInput/TInput.jsx
Normal file
151
com.twin.app.shoptime/src/views/SearchPanel/TInput/TInput.jsx
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
import React, { useCallback, useState } from 'react';
|
||||||
|
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
import { InputField } from '@enact/sandstone/Input';
|
||||||
|
import Spotlight from '@enact/spotlight';
|
||||||
|
import Spottable from '@enact/spotlight/Spottable';
|
||||||
|
|
||||||
|
import useScrollReset from '../../../hooks/useScrollReset';
|
||||||
|
import TIconButton from '../../../components/TIconButton/TIconButton';
|
||||||
|
import css from './TInput.module.less';
|
||||||
|
|
||||||
|
const KINDS = { withIcon: 'withIcon' };
|
||||||
|
const ICONS = { search: 'search' };
|
||||||
|
const BORDER = { none: 'none' };
|
||||||
|
const COLOR = { transparent: 'transparent' };
|
||||||
|
|
||||||
|
const Container = Spottable('div');
|
||||||
|
export default function TInput({
|
||||||
|
kind,
|
||||||
|
icon,
|
||||||
|
border,
|
||||||
|
color,
|
||||||
|
className,
|
||||||
|
spotlightDisabled,
|
||||||
|
spotlightBoxDisabled,
|
||||||
|
spotlightId,
|
||||||
|
disabled,
|
||||||
|
onKeyDown,
|
||||||
|
scrollTop,
|
||||||
|
onIconClick,
|
||||||
|
forcedSpotlight,
|
||||||
|
onFocus,
|
||||||
|
onBlur,
|
||||||
|
onInputModeChange, // 입력 모드 변경 콜백
|
||||||
|
...rest
|
||||||
|
}) {
|
||||||
|
const { handleScrollReset, handleStopScrolling } = useScrollReset(scrollTop);
|
||||||
|
|
||||||
|
// 입력 가능 모드 상태 (webOS 키보드가 뜨는 상태)
|
||||||
|
const [isInputActive, setIsInputActive] = useState(false);
|
||||||
|
|
||||||
|
const handleContainerClick = useCallback((e) => {
|
||||||
|
if (e.key === 'ArrrowLeft' || e.key === 'Left') {
|
||||||
|
Spotlight.focus('spotlight_search');
|
||||||
|
} else {
|
||||||
|
Spotlight.focus('input-field-box');
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleButtonKeyDown = useCallback((e) => {
|
||||||
|
if (e.key === 'ArrowDown' || e.key === 'Down') {
|
||||||
|
Spotlight.move('down');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.key === 'ArrowLeft' || e.key === 'Left') {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
Spotlight.focus('input-field-box');
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleBoxKeyDown = useCallback((e) => {
|
||||||
|
if (e.key === 'ArrowLeft' || e.key === 'Left') {
|
||||||
|
const currentElement = e.currentTarget;
|
||||||
|
const inputElement = currentElement.querySelector('input');
|
||||||
|
|
||||||
|
if (inputElement.value.length === 0) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
Spotlight.focus('spotlight_search');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
//focus,blur 추가
|
||||||
|
const _onFocus = useCallback(() => {
|
||||||
|
if (onFocus) {
|
||||||
|
onFocus();
|
||||||
|
}
|
||||||
|
handleScrollReset();
|
||||||
|
}, [onFocus]);
|
||||||
|
const _onBlur = useCallback(() => {
|
||||||
|
if (onBlur) {
|
||||||
|
onBlur();
|
||||||
|
}
|
||||||
|
handleStopScrolling();
|
||||||
|
}, [onBlur]);
|
||||||
|
|
||||||
|
// onActivate: 내부 input에 실제로 포커스가 가서 입력 가능한 상태 (webOS 키보드가 뜨는 시점)
|
||||||
|
const _onActivate = useCallback(() => {
|
||||||
|
console.log('[TInput] Input 모드 활성화 - webOS 키보드 표시됨, isInputActive:', true);
|
||||||
|
setIsInputActive(true);
|
||||||
|
// 부모 컴포넌트에 모드 변경 알림
|
||||||
|
if (onInputModeChange) {
|
||||||
|
onInputModeChange(true);
|
||||||
|
}
|
||||||
|
}, [onInputModeChange]);
|
||||||
|
|
||||||
|
// onDeactivate: 내부 input에서 포커스가 빠져나감 (webOS 키보드가 사라지는 시점)
|
||||||
|
const _onDeactivate = useCallback(() => {
|
||||||
|
console.log('[TInput] Input 모드 비활성화 - webOS 키보드 숨김, isInputActive:', false);
|
||||||
|
setIsInputActive(false);
|
||||||
|
// 부모 컴포넌트에 모드 변경 알림
|
||||||
|
if (onInputModeChange) {
|
||||||
|
onInputModeChange(false);
|
||||||
|
}
|
||||||
|
}, [onInputModeChange]);
|
||||||
|
|
||||||
|
console.log('[TInput Render] isInputActive:', isInputActive);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container
|
||||||
|
className={classNames(
|
||||||
|
css.container,
|
||||||
|
className ? className : null,
|
||||||
|
isInputActive && css.containerActive
|
||||||
|
)}
|
||||||
|
spotlightId={spotlightId}
|
||||||
|
onClick={handleContainerClick}
|
||||||
|
onKeyDown={handleBoxKeyDown}
|
||||||
|
>
|
||||||
|
<InputField
|
||||||
|
{...rest}
|
||||||
|
spotlightDisabled={spotlightDisabled}
|
||||||
|
className={classNames(css.input, isInputActive && css.inputActive)}
|
||||||
|
autoFocus
|
||||||
|
onFocus={_onFocus}
|
||||||
|
onBlur={_onBlur}
|
||||||
|
onActivate={_onActivate}
|
||||||
|
onDeactivate={_onDeactivate}
|
||||||
|
onKeyDown={onKeyDown}
|
||||||
|
spotlightId={'input-field-box'}
|
||||||
|
aria-label="Keyword edit box"
|
||||||
|
dismissOnEnter
|
||||||
|
/>
|
||||||
|
|
||||||
|
{kind === 'withIcon' && (
|
||||||
|
<TIconButton
|
||||||
|
className={classNames(icon && css[icon])}
|
||||||
|
onClick={onIconClick}
|
||||||
|
forcedSpotlight={forcedSpotlight}
|
||||||
|
ariaLabel="Search, button"
|
||||||
|
onKeyDown={handleButtonKeyDown}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { BORDER, COLOR, ICONS, KINDS };
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
@import "../../../style/CommonStyle.module.less";
|
||||||
|
@import "../../../style/utils.module.less";
|
||||||
|
|
||||||
|
.container {
|
||||||
|
width: fit-content;
|
||||||
|
height: fit-content;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
// 입력 가능 모드일 때 Container 스타일
|
||||||
|
&.containerActive {
|
||||||
|
.input {
|
||||||
|
background-color: #ffff00 !important; // 노란색 배경
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.input {
|
||||||
|
margin-right: 0;
|
||||||
|
margin-left: 0;
|
||||||
|
height: 100px !important;
|
||||||
|
padding: 20px 40px 20px 50px !important;
|
||||||
|
border-radius: 50px;
|
||||||
|
border: none !important; // SearchPanel의 wrapper에서 테두리 처리
|
||||||
|
background-color: #fff !important;
|
||||||
|
width: 100% !important;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
box-shadow: none !important; // SearchPanel의 wrapper에서 포커스 처리
|
||||||
|
border: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
> div {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
> input {
|
||||||
|
font-family: @baseFont;
|
||||||
|
color: @COLOR_GRAY07 !important;
|
||||||
|
height: inherit !important;
|
||||||
|
line-height: inherit !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 입력 가능 모드 (webOS 키보드가 뜨는 상태)
|
||||||
|
// .input과 함께 사용되므로 더 구체적인 선택자 필요
|
||||||
|
.input.inputActive {
|
||||||
|
background-color: #ffff00 !important; // 노란색 배경
|
||||||
|
}
|
||||||
|
|
||||||
|
.withIcon {
|
||||||
|
> div {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.search {
|
||||||
|
position: absolute;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center top;
|
||||||
|
background-size: cover;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
top: 50%;
|
||||||
|
right: 30px;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
background-image: url("../../../../assets/images/searchpanel/ico_search_submit.png");
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
width: 42px;
|
||||||
|
height: 42px;
|
||||||
|
transform: translateY(-50%) scale(1.1);
|
||||||
|
background-position: center bottom;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,7 +12,7 @@ import Spottable from '@enact/spotlight/Spottable';
|
|||||||
import micIcon from '../../../../assets/images/searchpanel/image-mic.png';
|
import micIcon from '../../../../assets/images/searchpanel/image-mic.png';
|
||||||
import { getShopperHouseSearch } from '../../../actions/searchActions';
|
import { getShopperHouseSearch } from '../../../actions/searchActions';
|
||||||
import TFullPopup from '../../../components/TFullPopup/TFullPopup';
|
import TFullPopup from '../../../components/TFullPopup/TFullPopup';
|
||||||
import TInput, { ICONS, KINDS } from '../../../components/TInput/TInput';
|
import TInput, { ICONS, KINDS } from '../TInput/TInput';
|
||||||
import { useWebSpeech } from '../../../hooks/useWebSpeech';
|
import { useWebSpeech } from '../../../hooks/useWebSpeech';
|
||||||
import VoiceListening from './modes/VoiceListening';
|
import VoiceListening from './modes/VoiceListening';
|
||||||
import VoiceNotRecognized from './modes/VoiceNotRecognized';
|
import VoiceNotRecognized from './modes/VoiceNotRecognized';
|
||||||
@@ -334,9 +334,9 @@ const VoiceInputOverlay = ({
|
|||||||
setCurrentMode(mode);
|
setCurrentMode(mode);
|
||||||
setVoiceInputMode(null);
|
setVoiceInputMode(null);
|
||||||
|
|
||||||
// Overlay 내부로 포커스 이동
|
// 마이크 버튼으로 포커스 이동
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
Spotlight.focus(OVERLAY_SPOTLIGHT_ID);
|
Spotlight.focus(MIC_SPOTLIGHT_ID);
|
||||||
}, 100);
|
}, 100);
|
||||||
} else {
|
} else {
|
||||||
// Overlay가 닫힐 때 원래 포커스 복원 및 상태 초기화
|
// Overlay가 닫힐 때 원래 포커스 복원 및 상태 초기화
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
letter-spacing: -1px;
|
letter-spacing: -1px;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
|
line-height: 1.2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user