[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:
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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}>
|
||||
|
||||
Reference in New Issue
Block a user