[251028] feat: HowAboutTheseResponse

🕐 커밋 시간: 2025. 10. 28. 13:31:19

📊 변경 통계:
  • 총 파일: 3개
  • 추가: +27줄

📁 추가된 파일:
  + 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/SearchResults.new.v2.jsx

🔧 주요 변경 내용:
  • 소규모 기능 개선
This commit is contained in:
2025-10-28 13:31:20 +09:00
parent 4056d1e2a1
commit d21be06b46
3 changed files with 308 additions and 0 deletions

View File

@@ -0,0 +1,143 @@
import React, { useEffect, useRef, useState } from 'react';
import PropTypes from 'prop-types';
import SpotlightContainerDecorator from '@enact/spotlight/SpotlightContainerDecorator';
import icShoptime from '../../../../assets/images/icons/ic-shoptime.png';
import css from './HowAboutThese.response.module.less';
const OverlayContainer = SpotlightContainerDecorator(
{
enterTo: 'default-element',
restrict: 'self-only',
},
'div'
);
const HowAboutTheseResponse = ({ searchId = null, isLoading = true, onClose }) => {
const [typedText, setTypedText] = useState('');
const [currentMessageIndex, setCurrentMessageIndex] = useState(0);
const typingTimerRef = useRef(null);
const stageTimerRef = useRef(null);
const MESSAGES = [
"I'm analyzing your request...",
'Preparing your answer...',
'Thank you for waiting-Just finalizing it',
];
const TYPING_SPEED = 50; // ms per character
// 타이핑 애니메이션 효과 - VoiceResponse와 동일
useEffect(() => {
if (!isLoading) {
if (typingTimerRef.current) {
clearTimeout(typingTimerRef.current);
typingTimerRef.current = null;
}
if (stageTimerRef.current) {
clearTimeout(stageTimerRef.current);
stageTimerRef.current = null;
}
setTypedText('');
setCurrentMessageIndex(0);
return;
}
const messageTimers = () => {
stageTimerRef.current = setTimeout(() => {
setCurrentMessageIndex(1);
setTypedText('');
}, 2000);
setTimeout(() => {
setCurrentMessageIndex(2);
setTypedText('');
}, 4000);
};
messageTimers();
return () => {
if (typingTimerRef.current) {
clearTimeout(typingTimerRef.current);
typingTimerRef.current = null;
}
if (stageTimerRef.current) {
clearTimeout(stageTimerRef.current);
stageTimerRef.current = null;
}
};
}, [isLoading]);
// 타이핑 애니메이션 - VoiceResponse와 동일
useEffect(() => {
if (!isLoading) return;
const currentMessage = MESSAGES[currentMessageIndex];
let currentIndex = 0;
const typeNextChar = () => {
if (currentIndex < currentMessage.length) {
setTypedText(currentMessage.substring(0, currentIndex + 1));
currentIndex++;
typingTimerRef.current = setTimeout(typeNextChar, TYPING_SPEED);
}
};
// 초기화 및 시작
setTypedText('');
typingTimerRef.current = setTimeout(typeNextChar, 300); // 300ms 지연 후 시작
// Cleanup
return () => {
if (typingTimerRef.current) {
clearTimeout(typingTimerRef.current);
typingTimerRef.current = null;
}
};
}, [isLoading, currentMessageIndex]);
return (
<OverlayContainer className={css.bgcontainer}>
<div className={css.container}>
{/* Header Section */}
<div className={css.header}>
<div className={css.headerLeft}>
<div className={css.headerContent}>
{/* 아이콘 */}
<div className={css.iconPlaceholder}>
<img src={icShoptime} alt="Shoptime" className={css.icon} />
</div>
<div className={css.titleContainer}>
<div className={css.title}>How about these?</div>
</div>
</div>
</div>
</div>
</div>
{/* Response Content Section - Loading Message with Typing Animation */}
<div className={css.responseContent}>
{isLoading && (
<div className={css.loadingMessage}>
<div className={css.typingText}>{typedText}</div>
<span className={css.cursor}>|</span>
</div>
)}
</div>
</OverlayContainer>
);
};
HowAboutTheseResponse.propTypes = {
searchId: PropTypes.string,
isLoading: PropTypes.bool,
onClose: PropTypes.func,
};
HowAboutTheseResponse.defaultProps = {
searchId: null,
isLoading: true,
onClose: null,
};
export default HowAboutTheseResponse;

View File

@@ -0,0 +1,138 @@
@import "../../../style/CommonStyle.module.less";
@import "../../../style/utils.module.less";
.bgcontainer {
width: 100%;
height: calc(100vh - 210px);
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%
);
display: flex;
flex-direction: column;
}
.container {
width: 100%;
height: 128px;
padding: 32px 60px;
background: #dddddd;
display: inline-flex;
justify-content: flex-start;
align-items: center;
flex-shrink: 0;
}
/* Header Section */
.header {
width: 100%;
height: 48px;
position: relative;
flex-shrink: 0;
display: flex;
align-items: center;
}
.headerLeft {
flex: 1;
height: 48px;
position: relative;
}
.headerContent {
display: flex;
align-items: center;
height: 100%;
}
.iconPlaceholder {
width: 48px;
height: 48px;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
}
.icon {
width: 48px;
height: 48px;
object-fit: contain;
}
.titleContainer {
position: absolute;
left: 53px;
top: 10px;
border-bottom: 1px solid black;
display: inline-flex;
justify-content: center;
align-items: center;
}
.title {
color: #272727;
font-size: 24px;
font-family: "Roboto", sans-serif;
font-weight: 700;
white-space: nowrap;
line-height: 1;
}
/* Response Content Section - Loading Message with Typing Animation */
.responseContent {
flex: 1;
width: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 0 60px;
position: relative;
}
/* 로딩 메시지 컨테이너 */
.loadingMessage {
display: flex;
align-items: center;
justify-content: center;
> * {
margin-right: 4px;
&:last-child {
margin-right: 0;
}
}
}
/* 타이핑 애니메이션 텍스트 - 검은색으로 변경 */
.typingText {
color: #272727; // 검은색 (흰색 대신)
font-size: 46px;
font-family: "LG Smart UI";
font-weight: 400;
line-height: 40px;
text-align: center;
}
/* 깜빡이는 커서 - 검은색으로 변경 */
.cursor {
color: #272727; // 검은색 (흰색 대신)
font-size: 34px;
font-family: "LG Smart UI";
font-weight: 400;
animation: blink 1s infinite;
}
@keyframes blink {
0%, 49% {
opacity: 1;
}
50%, 100% {
opacity: 0;
}
}

View File

@@ -27,6 +27,7 @@ import { $L } from '../../utils/helperMethods';
import { SpotlightIds } from '../../utils/SpotlightIds';
import HowAboutThese from './HowAboutThese/HowAboutThese';
import HowAboutTheseSmall from './HowAboutThese/HowAboutThese.small';
import HowAboutTheseResponse from './HowAboutThese/HowAboutThese.response';
import css from './SearchResults.new.v2.module.less';
import ItemCard from './SearchResultsNew/ItemCard';
import ShowCard from './SearchResultsNew/ShowCard';
@@ -37,6 +38,7 @@ const ITEMS_PER_PAGE = 10;
export const HOW_ABOUT_THESE_MODES = {
SMALL: 'small', // 작은 버전 (기본)
FULL: 'full', // 전체 버전 (팝업)
RESPONSE: 'response', // 응답 버전 (팝업)
};
// 메모리 누수 방지를 위한 안전한 이미지 컴포넌트
@@ -163,6 +165,9 @@ const SearchResultsNew = ({
HOW_ABOUT_THESE_MODES.SMALL
);
// HowAboutThese Response 로딩 상태
const [isHowAboutTheseLoading, setIsHowAboutTheseLoading] = useState(false);
// HowAboutThese 모드 전환 핸들러
const handleShowFullHowAboutThese = useCallback(() => {
setHowAboutTheseMode(HOW_ABOUT_THESE_MODES.FULL);
@@ -173,6 +178,18 @@ const SearchResultsNew = ({
setHowAboutTheseMode(HOW_ABOUT_THESE_MODES.SMALL);
}, []);
// HowAboutThese Response 닫기 핸들러 (Response -> Small)
const handleCloseHowAboutTheseResponse = useCallback(() => {
setHowAboutTheseMode(HOW_ABOUT_THESE_MODES.SMALL);
setIsHowAboutTheseLoading(false);
}, []);
// HowAboutThese Response 모드로 전환 핸들러
const handleShowHowAboutTheseResponse = useCallback(() => {
setHowAboutTheseMode(HOW_ABOUT_THESE_MODES.RESPONSE);
setIsHowAboutTheseLoading(true);
}, []);
// 🎯 HowAboutThese 포커스 관리 - 검색 입력 영역 포커스 감지 시 SMALL 모드로 전환
useEffect(() => {
if (
@@ -456,6 +473,16 @@ const SearchResultsNew = ({
/>
</div>
)}
{/* HowAboutThese Response 버전 - 로딩 메시지 표시 */}
{howAboutTheseMode === HOW_ABOUT_THESE_MODES.RESPONSE && (
<div className={css.howAboutTheseOverlay}>
<HowAboutTheseResponse
searchId={shopperHouseSearchId}
isLoading={isHowAboutTheseLoading}
onClose={handleCloseHowAboutTheseResponse}
/>
</div>
)}
{themeInfo && themeInfo?.length > 0 && (
<div className={css.hotpicksSection} data-wheel-point="true">
<div className={css.sectionHeader}>