[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:
2025-10-17 12:33:31 +09:00
parent b025780107
commit 0b48e43eb4
6 changed files with 308 additions and 48 deletions

View File

@@ -17,16 +17,16 @@ import { sendLogGNB, sendLogTotalRecommend } from '../../actions/logActions';
import { getMyRecommandedKeyword } from '../../actions/myPageActions';
import { popPanel, updatePanel } from '../../actions/panelActions';
import { getSearch, resetSearch, searchMain } from '../../actions/searchActions';
import {
showErrorToast,
showInfoToast,
showSearchErrorToast,
showSearchSuccessToast,
showSuccessToast,
showWarningToast,
} from '../../actions/toastActions';
// import {
// showErrorToast,
// showInfoToast,
// showSearchErrorToast,
// showSearchSuccessToast,
// showSuccessToast,
// showWarningToast,
// } from '../../actions/toastActions';
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 TScroller from '../../components/TScroller/TScroller';
import TVerticalPagenator from '../../components/TVerticalPagenator/TVerticalPagenator';
@@ -101,6 +101,16 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId, scrollOpt
setInputFocus(false);
}, [inputFocus]);
// TInput의 입력 모드 상태 (webOS 키보드가 뜨는지 여부)
const [isInputModeActive, setIsInputModeActive] = useState(false);
const handleInputModeChange = useCallback((isActive) => {
console.log(
'[SearchPanel] TInput 입력 모드:',
isActive ? '활성화 (키보드 표시)' : '비활성화 (키보드 숨김)'
);
setIsInputModeActive(isActive);
}, []);
let searchQueryRef = usePrevious(searchQuery);
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 {
dispatch(resetSearch());
}
@@ -473,10 +483,10 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId, scrollOpt
}, []);
// 테스트용 Toast 핸들러들
const handleTestToasts = useCallback(() => {
// 간단한 Toast 테스트
dispatch(showSuccessToast('테스트 메시지입니다', { duration: 3000 }));
}, [dispatch]);
// const handleTestToasts = useCallback(() => {
// // 간단한 Toast 테스트
// dispatch(showSuccessToast('테스트 메시지입니다', { duration: 3000 }));
// }, [dispatch]);
// ProductCard 컴포넌트
const renderItem = useCallback(
@@ -565,28 +575,36 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId, scrollOpt
spotlightId={SPOTLIGHT_IDS.SEARCH_INPUT_LAYER}
>
<div className={css.searchInputWrapper}>
<TInput
className={css.inputBox}
kind={KINDS.withIcon}
icon={ICONS.search}
value={searchQuery}
onChange={handleSearchChange}
onIconClick={() => {
if (showVirtualKeyboard) {
handleSearchSubmit(searchQuery);
} else {
setShowVirtualKeyboard(true);
}
}}
onKeyDown={handleKeydown}
onKeyUp={cursorPosition}
spotlightId={SPOTLIGHT_IDS.SEARCH_INPUT_BOX}
forcedSpotlight="recent-keyword-0"
tabIndex={0}
spotlightBoxDisabled={true}
onFocus={_onFocus}
onBlur={_onBlur}
/>
<div
className={classNames(
css.inputWrapper,
isInputModeActive && css.inputWrapperActive
)}
>
<TInput
className={css.inputBox}
kind={KINDS.withIcon}
icon={ICONS.search}
value={searchQuery}
onChange={handleSearchChange}
onIconClick={() => {
if (showVirtualKeyboard) {
handleSearchSubmit(searchQuery);
} else {
setShowVirtualKeyboard(true);
}
}}
onKeyDown={handleKeydown}
onKeyUp={cursorPosition}
spotlightId={SPOTLIGHT_IDS.SEARCH_INPUT_BOX}
forcedSpotlight="recent-keyword-0"
tabIndex={0}
spotlightBoxDisabled={true}
onFocus={_onFocus}
onBlur={_onBlur}
onInputModeChange={handleInputModeChange}
/>
</div>
<SpottableMicButton
className={css.microphoneButton}
onClick={onClickMic}

View File

@@ -70,14 +70,36 @@
}
}
.inputBox {
// TInput을 감싸는 wrapper
.inputWrapper {
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-right: 40px;
background: white;
border-radius: 1000px;
border: 5px solid #ccc;
border: none; // wrapper에서 테두리 처리
box-sizing: border-box;
display: flex;
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 컴포넌트 자체의 내부 포커스 스타일 제거
> div {
&:focus,

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

View File

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

View File

@@ -12,7 +12,7 @@ import Spottable from '@enact/spotlight/Spottable';
import micIcon from '../../../../assets/images/searchpanel/image-mic.png';
import { getShopperHouseSearch } from '../../../actions/searchActions';
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 VoiceListening from './modes/VoiceListening';
import VoiceNotRecognized from './modes/VoiceNotRecognized';
@@ -334,9 +334,9 @@ const VoiceInputOverlay = ({
setCurrentMode(mode);
setVoiceInputMode(null);
// Overlay 내부로 포커스 이동
// 마이크 버튼으로 포커스 이동
setTimeout(() => {
Spotlight.focus(OVERLAY_SPOTLIGHT_ID);
Spotlight.focus(MIC_SPOTLIGHT_ID);
}, 100);
} else {
// Overlay가 닫힐 때 원래 포커스 복원 및 상태 초기화

View File

@@ -26,6 +26,7 @@
font-weight: 700;
letter-spacing: -1px;
color: #fff;
line-height: 1.2;
}
}
}