[251022] feat: HowAboutThese

🕐 커밋 시간: 2025. 10. 22. 18:46:19

📊 변경 통계:
  • 총 파일: 9개
  • 추가: +1줄
  • 삭제: -1줄

📁 추가된 파일:
  + com.twin.app.shoptime/src/views/SearchPanel/HowAboutThese/HowAboutThese.figma.jsx
  + com.twin.app.shoptime/src/views/SearchPanel/HowAboutThese/HowAboutThese.jsx
  + com.twin.app.shoptime/src/views/SearchPanel/HowAboutThese/HowAboutThese.module.less
  + com.twin.app.shoptime/src/views/SearchPanel/HowAboutThese/HowAboutThese.small.figama.jsx
  + com.twin.app.shoptime/src/views/SearchPanel/HowAboutThese/HowAboutThese.small.jsx
  + com.twin.app.shoptime/src/views/SearchPanel/HowAboutThese/HowAboutThese.small.module.less
  + com.twin.app.shoptime/src/views/SearchPanel/SearchResults.new.v2.jsx
  + com.twin.app.shoptime/src/views/SearchPanel/SearchResults.new.v2.module.less

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/views/SearchPanel/SearchPanel.new.jsx

🔧 함수 변경 내용:
  📄 com.twin.app.shoptime/src/views/SearchPanel/HowAboutThese/HowAboutThese.jsx (javascript):
     Added: HowAboutThese()
  📄 com.twin.app.shoptime/src/views/SearchPanel/HowAboutThese/HowAboutThese.module.less (unknown):
     Added: gradient(), translateY()
  📄 com.twin.app.shoptime/src/views/SearchPanel/HowAboutThese/HowAboutThese.small.jsx (javascript):
     Added: HowAboutTheseSmall()
  📄 com.twin.app.shoptime/src/views/SearchPanel/HowAboutThese/HowAboutThese.small.module.less (unknown):
     Added: translateY(), gradient()
  📄 com.twin.app.shoptime/src/views/SearchPanel/SearchResults.new.v2.jsx (javascript):
     Added: SearchResultsNew(), getButtonTabList(), upBtnClick()
  📄 com.twin.app.shoptime/src/views/SearchPanel/SearchResults.new.v2.module.less (unknown):
     Added: translateY(), child()
This commit is contained in:
2025-10-22 18:46:22 +09:00
parent f460cbfd04
commit e6c5e27e2d
9 changed files with 1721 additions and 1 deletions

View File

@@ -0,0 +1,257 @@
<div
style={{
width: '100%',
height: '860px',
paddingTop: 32,
paddingBottom: 30,
paddingLeft: 60,
paddingRight: 60,
left: 0,
top: 0,
position: 'absolute',
background:
'linear-gradient(360deg, rgba(221.25, 221.25, 221.25, 0) 0%, rgba(221.25, 221.25, 221.25, 0.85) 57%, rgba(221.25, 221.25, 221.25, 0.90) 80%, rgba(221, 221, 221, 0.90) 100%), linear-gradient(360deg, rgba(221.25, 221.25, 221.25, 0) 35%, #DDDDDD 80%, #DDDDDD 100%)',
flexDirection: 'column',
justifyContent: 'flex-start',
alignItems: 'flex-start',
gap: 19,
display: 'inline-flex',
}}
>
<div
style={{
alignSelf: 'stretch',
justifyContent: 'flex-start',
alignItems: 'center',
display: 'inline-flex',
}}
>
<div style={{ flex: '1 1 0', height: 48, position: 'relative' }}>
<img
style={{ width: 48, height: 48, left: 0, top: 0, position: 'absolute' }}
src="https://placehold.co/48x48"
/>
<div
style={{
left: 53,
top: 10,
position: 'absolute',
borderBottom: '1px black solid',
justifyContent: 'center',
alignItems: 'center',
gap: 10,
display: 'inline-flex',
}}
>
<div
style={{
textAlign: 'center',
justifyContent: 'center',
display: 'flex',
flexDirection: 'column',
color: '#272727',
fontSize: 24,
fontFamily: 'Roboto',
fontWeight: '700',
wordWrap: 'break-word',
}}
>
How about these?
</div>
</div>
</div>
<div
style={{
flex: '1 1 0',
flexDirection: 'column',
justifyContent: 'flex-start',
alignItems: 'flex-end',
gap: 10,
display: 'inline-flex',
}}
>
<div
style={{
width: 200,
height: 60,
background: '#808080',
borderRadius: 6,
justifyContent: 'space-between',
alignItems: 'center',
display: 'inline-flex',
}}
>
<div
style={{
textAlign: 'center',
color: 'white',
fontSize: 24,
fontFamily: 'LG Smart UI',
fontWeight: '700',
lineHeight: 24,
wordWrap: 'break-word',
}}
>
SEE MORE
</div>
</div>
</div>
</div>
<div
style={{
alignSelf: 'stretch',
overflow: 'hidden',
flexDirection: 'column',
justifyContent: 'flex-start',
alignItems: 'flex-start',
gap: 15,
display: 'flex',
}}
>
<div
style={{
height: 64,
paddingLeft: 20,
paddingRight: 20,
paddingTop: 16,
paddingBottom: 16,
background: '#CE1C5E',
boxShadow: '0px 4px 4px rgba(0, 0, 0, 0.25)',
borderRadius: 30,
outline: '1px #C70850 solid',
outlineOffset: '-1px',
flexDirection: 'column',
justifyContent: 'flex-start',
alignItems: 'center',
gap: 20,
display: 'flex',
}}
>
<div
style={{
textAlign: 'center',
color: '#EAEAEA',
fontSize: 24,
fontFamily: 'LG Smart UI',
fontWeight: '700',
lineHeight: 24,
wordWrap: 'break-word',
}}
>
What are some luxury skincare products.
</div>
</div>
<div
style={{
padding: 20,
background: 'white',
borderRadius: 100,
outline: '1px #DADADA solid',
outlineOffset: '-1px',
flexDirection: 'column',
justifyContent: 'flex-start',
alignItems: 'flex-start',
gap: 10,
display: 'flex',
}}
>
<div
style={{
textAlign: 'center',
color: 'black',
fontSize: 24,
fontFamily: 'LG Smart UI',
fontWeight: '700',
lineHeight: 24,
wordWrap: 'break-word',
}}
>
Popular makeup gift sets uitable for mother.
</div>
</div>
<div
style={{
padding: 20,
background: 'white',
borderRadius: 100,
outline: '1px #DADADA solid',
outlineOffset: '-1px',
flexDirection: 'column',
justifyContent: 'flex-start',
alignItems: 'flex-start',
gap: 10,
display: 'flex',
}}
>
<div
style={{
textAlign: 'center',
color: 'black',
fontSize: 24,
fontFamily: 'LG Smart UI',
fontWeight: '700',
lineHeight: 24,
wordWrap: 'break-word',
}}
>
Which anti-aging cosmetics in 50s or 60s.
</div>
</div>
<div
style={{
padding: 20,
background: 'white',
borderRadius: 100,
outline: '1px #DADADA solid',
outlineOffset: '-1px',
flexDirection: 'column',
justifyContent: 'flex-start',
alignItems: 'flex-start',
gap: 10,
display: 'flex',
}}
>
<div
style={{
textAlign: 'center',
color: 'black',
fontSize: 24,
fontFamily: 'LG Smart UI',
fontWeight: '700',
lineHeight: 24,
wordWrap: 'break-word',
}}
>
Elegant fragrance or cosmetic bundles.
</div>
</div>
<div
style={{
padding: 20,
background: 'white',
borderRadius: 100,
outline: '1px #DADADA solid',
outlineOffset: '-1px',
flexDirection: 'column',
justifyContent: 'flex-start',
alignItems: 'flex-start',
gap: 10,
display: 'flex',
}}
>
<div
style={{
textAlign: 'center',
color: 'black',
fontSize: 24,
fontFamily: 'LG Smart UI',
fontWeight: '700',
lineHeight: 24,
wordWrap: 'break-word',
}}
>
What beauty brands offer special gift boxes.
</div>
</div>
</div>
</div>;

View File

@@ -0,0 +1,126 @@
import React, { useCallback, useMemo } from 'react';
import PropTypes from 'prop-types';
import { useDispatch } from 'react-redux';
import Spottable from '@enact/spotlight/Spottable';
import SpotlightContainerDecorator from '@enact/spotlight/SpotlightContainerDecorator';
import { getShopperHouseSearch } from '../../../actions/searchActions';
import css from './HowAboutThese.module.less';
const OverlayContainer = SpotlightContainerDecorator(
{
enterTo: 'default-element',
restrict: 'self-only',
},
'div'
);
const SpottableBubble = Spottable('div');
const SpottableSeeMoreButton = Spottable('div');
const HowAboutThese = ({ relativeQueries = [], onQueryClick, onClose }) => {
const dispatch = useDispatch();
// 기본 relativeQueries가 없는 경우를 위한 fallback
const queries = useMemo(() => {
return relativeQueries.length > 0
? relativeQueries
: [
'What are some luxury skincare products',
'Popular makeup gift sets suitable for mother',
'Which anti-aging cosmetics in 50s or 60s',
'Elegant fragrance or cosmetic bundles',
'What beauty brands offer special gift boxes',
];
}, [relativeQueries]);
// 검색어 클릭 핸들러
const handleQueryClick = useCallback(
(query) => {
console.log('[HowAboutThese] Query clicked:', query);
// 외부에서 전달된 onQueryClick이 있으면 사용
if (onQueryClick) {
onQueryClick(query);
return;
}
// 기본적으로 ShopperHouse API를 통해 재검색
dispatch(getShopperHouseSearch(query));
// 팝업 닫기
if (onClose) {
onClose();
}
},
[dispatch, onQueryClick, onClose]
);
// "COLLAPSE" 버튼 클릭 핸들러 (Full 버전을 Small로 축소)
const handleCollapseClick = useCallback(() => {
console.log('[HowAboutThese] Collapse clicked - 축소하여 Small 버전으로 전환');
if (onClose) {
onClose();
}
}, [onClose]);
// 첫 번째 쿼리는 특별 스타일 (핑크색 버블)
const firstQuery = queries[0];
const remainingQueries = queries.slice(1);
return (
<OverlayContainer className={css.container}>
{/* Header Section */}
<div className={css.header}>
<div className={css.headerLeft}>
<div className={css.headerContent}>
{/* 아이콘 자리 - 현재는 비워둠 */}
<div className={css.iconPlaceholder} />
<div className={css.titleContainer}>
<div className={css.title}>How about these?</div>
</div>
</div>
</div>
<div className={css.headerRight}>
<SpottableSeeMoreButton className={css.seeMoreButton} onClick={handleCollapseClick}>
<span className={css.seeMoreText}>COLLAPSE</span>
</SpottableSeeMoreButton>
</div>
</div>
{/* Bubbles Section */}
<div className={css.bubblesContainer}>
{/* 첫 번째 버블 - 핑크색 */}
{firstQuery && (
<SpottableBubble
className={css.bubblePrimary}
onClick={() => handleQueryClick(firstQuery)}
>
<span className={css.bubbleText}>{firstQuery}</span>
</SpottableBubble>
)}
{/* 나머지 버블들 - 흰색 */}
{remainingQueries.map((query, index) => (
<SpottableBubble
key={`query-${index}`}
className={css.bubbleSecondary}
onClick={() => handleQueryClick(query)}
>
<span className={css.bubbleText}>{query}</span>
</SpottableBubble>
))}
</div>
</OverlayContainer>
);
};
HowAboutThese.propTypes = {
relativeQueries: PropTypes.array,
onQueryClick: PropTypes.func,
onClose: PropTypes.func,
};
export default HowAboutThese;

View File

@@ -0,0 +1,209 @@
.container {
width: 100%;
height: 860px;
padding: 30px 60px;
display: inline-flex;
flex-direction: column;
justify-content: flex-start;
align-items: flex-start;
gap: 19px;
background: linear-gradient(
360deg,
rgba(221.25, 221.25, 221.25, 0) 0%,
rgba(221.25, 221.25, 221.25, 0.85) 57%,
rgba(221.25, 221.25, 221.25, 0.9) 80%,
rgba(221, 221, 221, 0.9) 100%
),
linear-gradient(
360deg,
rgba(221.25, 221.25, 221.25, 0) 35%,
#DDDDDD 80%,
#DDDDDD 100%
);
}
/* Header Section */
.header {
align-self: stretch;
display: inline-flex;
justify-content: flex-start;
align-items: center;
}
.headerLeft {
flex: 1;
height: 48px;
position: relative;
}
.headerContent {
display: flex;
align-items: center;
height: 100%;
}
.iconPlaceholder {
width: 48px;
height: 48px;
position: absolute;
left: 0;
top: 0;
/* TODO: Add icon styling when needed */
}
.titleContainer {
position: absolute;
left: 53px;
top: 10px;
border-bottom: 1px solid black;
display: inline-flex;
justify-content: center;
align-items: center;
gap: 10px;
}
.title {
color: #272727;
font-size: 24px;
font-family: 'Roboto', sans-serif;
font-weight: 700;
text-align: center;
display: flex;
justify-content: center;
flex-direction: column;
word-wrap: break-word;
}
.headerRight {
flex: 1 1 0;
display: inline-flex;
flex-direction: column;
justify-content: flex-start;
align-items: flex-end;
gap: 10px;
}
.seeMoreButton {
width: 200px;
height: 60px;
background: #808080;
border-radius: 6px;
display: inline-flex;
justify-content: center;
align-items: center;
padding: 0 20px;
box-sizing: border-box;
cursor: pointer;
transition: background-color 0.2s ease;
&:focus {
outline: 2px solid #00a0e9;
outline-offset: 2px;
}
&:hover {
background: #666666;
}
}
.seeMoreText {
color: white;
font-size: 24px;
font-family: 'LG Smart UI', sans-serif;
font-weight: 700;
line-height: 24px;
text-align: center;
width: 100%;
}
/* Bubbles Section */
.bubblesContainer {
align-self: stretch;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: flex-start;
gap: 15px;
overflow: hidden;
}
/* Primary Bubble (Pink) */
.bubblePrimary {
height: 64px;
padding: 16px 20px;
background: #CE1C5E;
border-radius: 30px;
box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25);
outline: 1px solid #C70850;
outline-offset: -1px;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: center;
gap: 20px;
cursor: pointer;
transition: all 0.2s ease;
box-sizing: border-box;
&:focus {
outline: 2px solid #00a0e9;
outline-offset: 2px;
box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25), 0 0 0 2px #00a0e9;
}
&:hover {
background: #b91a52;
transform: translateY(-1px);
}
&:active {
transform: translateY(0);
}
}
/* Secondary Bubbles (White) */
.bubbleSecondary {
padding: 20px;
background: white;
border-radius: 100px;
outline: 1px solid #DADADA;
outline-offset: -1px;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: flex-start;
gap: 10px;
cursor: pointer;
transition: all 0.2s ease;
box-sizing: border-box;
&:focus {
outline: 2px solid #00a0e9;
outline-offset: 2px;
}
&:hover {
background: #f8f8f8;
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
&:active {
transform: translateY(0);
}
}
.bubbleText {
color: #EAEAEA;
font-size: 24px;
font-family: 'LG Smart UI', sans-serif;
font-weight: 700;
line-height: 24px;
text-align: center;
word-wrap: break-word;
width: 100%;
.bubbleSecondary & {
color: black;
}
}

View File

@@ -0,0 +1,188 @@
<div
style={{
width: '100%',
height: '100%',
paddingLeft: 60,
paddingRight: 60,
paddingTop: 30,
paddingBottom: 30,
background: '#DDDDDD',
justifyContent: 'flex-start',
alignItems: 'center',
gap: 19,
display: 'inline-flex',
}}
>
<div style={{ width: 254, height: 48, position: 'relative' }}>
<img
style={{ width: 48, height: 48, left: 0, top: 0, position: 'absolute' }}
src="https://placehold.co/48x48"
/>
<div
style={{
left: 53,
top: 10,
position: 'absolute',
borderBottom: '1px black solid',
justifyContent: 'center',
alignItems: 'center',
gap: 10,
display: 'inline-flex',
}}
>
<div
style={{
textAlign: 'center',
justifyContent: 'center',
display: 'flex',
flexDirection: 'column',
color: '#272727',
fontSize: 24,
fontFamily: 'Roboto',
fontWeight: '700',
wordWrap: 'break-word',
}}
>
How about these?
</div>
</div>
</div>
<div
style={{
flex: '1 1 0',
overflow: 'hidden',
justifyContent: 'flex-start',
alignItems: 'flex-start',
gap: 15,
display: 'flex',
}}
>
<div
style={{
padding: 20,
background: 'white',
borderRadius: 100,
outline: '1px #DADADA solid',
outlineOffset: '-1px',
flexDirection: 'column',
justifyContent: 'flex-start',
alignItems: 'flex-start',
gap: 10,
display: 'inline-flex',
}}
>
<div
style={{
textAlign: 'center',
color: 'black',
fontSize: 24,
fontFamily: 'LG Smart UI',
fontWeight: '700',
lineHeight: 24,
wordWrap: 'break-word',
}}
>
What are some luxury skincare products.
</div>
</div>
<div
style={{
padding: 20,
background: 'white',
borderRadius: 100,
outline: '1px #DADADA solid',
outlineOffset: '-1px',
flexDirection: 'column',
justifyContent: 'flex-start',
alignItems: 'flex-start',
gap: 10,
display: 'inline-flex',
}}
>
<div
style={{
textAlign: 'center',
color: 'black',
fontSize: 24,
fontFamily: 'LG Smart UI',
fontWeight: '700',
lineHeight: 24,
wordWrap: 'break-word',
}}
>
Popular makeup gift sets uitable for mother.
</div>
</div>
<div style={{ width: 1031, height: 64, position: 'relative' }}>
<div
style={{
width: 508,
height: 64,
padding: 20,
left: 0,
top: 0,
position: 'absolute',
background: 'linear-gradient(90deg, black 0%, rgba(102, 102, 102, 0) 60%)',
borderRadius: 100,
border: '1px #DADADA solid',
}}
/>
<div
style={{
padding: 20,
left: 0,
top: 0,
position: 'absolute',
background: 'white',
borderRadius: 100,
outline: '1px #DADADA solid',
outlineOffset: '-1px',
flexDirection: 'column',
justifyContent: 'flex-start',
alignItems: 'flex-start',
gap: 10,
display: 'inline-flex',
}}
>
<div
style={{
textAlign: 'center',
color: 'black',
fontSize: 24,
fontFamily: 'LG Smart UI',
fontWeight: '700',
lineHeight: 24,
wordWrap: 'break-word',
}}
>
Popular makeup gift sets uitable for mother.
</div>
</div>
</div>
</div>
<div
style={{
width: 200,
height: 60,
background: '#808080',
borderRadius: 6,
justifyContent: 'space-between',
alignItems: 'center',
display: 'flex',
}}
>
<div
style={{
textAlign: 'center',
color: 'white',
fontSize: 24,
fontFamily: 'LG Smart UI',
fontWeight: '700',
lineHeight: 24,
wordWrap: 'break-word',
}}
>
SEE MORE
</div>
</div>
</div>;

View File

@@ -0,0 +1,109 @@
import React, { useCallback } from 'react';
import PropTypes from 'prop-types';
import { useDispatch } from 'react-redux';
import Spottable from '@enact/spotlight/Spottable';
import { getShopperHouseSearch } from '../../../actions/searchActions';
import css from './HowAboutThese.small.module.less';
const SpottableBubble = Spottable('div');
const SpottableSeeMoreButton = Spottable('div');
const HowAboutTheseSmall = ({ relativeQueries = [], onQueryClick, onSeeMoreClick }) => {
const dispatch = useDispatch();
// 기본 relativeQueries가 없는 경우를 위한 fallback
const queries =
relativeQueries.length > 0
? relativeQueries
: [
'What are some luxury skincare products',
'Popular makeup gift sets suitable for mother',
'Which anti-aging cosmetics in 50s or 60s',
];
// 검색어 클릭 핸들러
const handleQueryClick = useCallback(
(query) => {
console.log('[HowAboutTheseSmall] Query clicked:', query);
// 외부에서 전달된 onQueryClick이 있으면 사용
if (onQueryClick) {
onQueryClick(query);
return;
}
// 기본적으로 ShopperHouse API를 통해 재검색
dispatch(getShopperHouseSearch(query));
},
[dispatch, onQueryClick]
);
// "SEE MORE" 버튼 클릭 핸들러
const handleSeeMoreClick = useCallback(() => {
console.log('[HowAboutTheseSmall] See More clicked');
// 외부에서 전달된 onSeeMoreClick이 있으면 사용
if (onSeeMoreClick) {
onSeeMoreClick();
return;
}
// 기본 동작: 확장된 뷰 표시 (나중에 구현)
console.log('[HowAboutTheseSmall] TODO: Show expanded view');
}, [onSeeMoreClick]);
// 첫 번째 두 개의 쿼리만 표시 (small 버전)
const displayQueries = queries.slice(0, 2);
return (
<div className={css.container}>
{/* Header Section */}
<div className={css.header}>
{/* 아이콘 자리 - 현재는 비워둠 */}
<div className={css.iconPlaceholder} />
<div className={css.titleContainer}>
<div className={css.title}>How about these?</div>
</div>
</div>
{/* Bubbles Section */}
<div className={css.bubblesContainer}>
{displayQueries.map((query, index) => (
<SpottableBubble
key={`query-${index}`}
className={css.bubble}
onClick={() => handleQueryClick(query)}
>
<div className={css.bubbleText}>{query}</div>
</SpottableBubble>
))}
{/* Fade 효과를 위한 추가 요소 (피그마 디자인 참고) */}
{queries.length > 2 && (
<div className={css.fadeContainer}>
<div className={css.fadeOverlay} />
<SpottableBubble className={css.bubble}>
<div className={css.bubbleText}>{queries[2]}</div>
</SpottableBubble>
</div>
)}
</div>
{/* See More Button */}
<SpottableSeeMoreButton className={css.seeMoreButton} onClick={handleSeeMoreClick}>
<div className={css.seeMoreText}>SEE MORE</div>
</SpottableSeeMoreButton>
</div>
);
};
HowAboutTheseSmall.propTypes = {
relativeQueries: PropTypes.array,
onQueryClick: PropTypes.func,
onSeeMoreClick: PropTypes.func,
};
export default HowAboutTheseSmall;

View File

@@ -0,0 +1,151 @@
.container {
width: 100%;
height: 100%;
padding: 30px 60px; // 상하 30px, 좌우 60px
background: #DDDDDD;
display: inline-flex;
justify-content: flex-start;
align-items: center;
gap: 19px;
}
/* Header Section */
.header {
width: 254px;
height: 48px;
position: relative;
flex-shrink: 0; // 고정 너비 유지
display: flex;
align-items: center;
}
.iconPlaceholder {
width: 48px;
height: 48px;
flex-shrink: 0;
/* TODO: Add icon styling when needed */
}
.titleContainer {
margin-left: 5px; // 아이콘과 제목 사이 간격 (53 - 48 = 5)
border-bottom: 1px solid black;
display: inline-flex;
justify-content: center;
align-items: center;
gap: 10px;
white-space: nowrap; // ⭐ 1줄로 표시
}
.title {
color: #272727;
font-size: 24px;
font-family: 'Roboto', sans-serif;
font-weight: 700;
white-space: nowrap; // ⭐ 1줄로 표시
line-height: 1; // 텍스트 높이 조정
}
/* Bubbles Section */
.bubblesContainer {
flex: 1 1 0;
overflow: hidden;
display: flex; // horizontal layout
justify-content: flex-start;
align-items: flex-start;
gap: 15px;
}
.bubble {
padding: 20px;
background: white;
border-radius: 100px;
outline: 1px solid #DADADA;
outline-offset: -1px;
display: inline-flex;
flex-direction: column;
justify-content: flex-start;
align-items: flex-start;
gap: 10px;
cursor: pointer;
transition: all 0.2s ease;
flex-shrink: 0; // 버블이 줄어들지 않도록
&:focus {
outline: 2px solid #00a0e9;
outline-offset: 2px;
}
&:hover {
background: #f8f8f8;
transform: translateY(-1px);
}
}
.fadeContainer {
width: 1031px;
height: 64px;
position: relative;
flex-shrink: 0;
}
.fadeOverlay {
width: 508px;
height: 64px;
padding: 20px;
left: 0;
top: 0;
position: absolute;
background: linear-gradient(90deg, black 0%, rgba(102, 102, 102, 0) 60%);
border-radius: 100px;
border: 1px solid #DADADA;
pointer-events: none; // 클릭 방지
}
.fadeContainer .bubble {
left: 0;
top: 0;
position: absolute;
}
.bubbleText {
text-align: center;
color: black;
font-size: 24px;
font-family: 'LG Smart UI', sans-serif;
font-weight: 700;
line-height: 24px;
word-wrap: break-word;
}
/* See More Button */
.seeMoreButton {
width: 200px;
height: 60px;
background: #808080;
border-radius: 6px;
display: flex;
justify-content: center; // 피그마: center
align-items: center;
cursor: pointer;
transition: background-color 0.2s ease;
flex-shrink: 0; // 고정 너비 유지
&:focus {
outline: 2px solid #00a0e9;
outline-offset: 2px;
}
&:hover {
background: #666666;
}
}
.seeMoreText {
text-align: center;
color: white;
font-size: 24px;
font-family: 'LG Smart UI', sans-serif;
font-weight: 700;
line-height: 24px;
word-wrap: break-word;
}

View File

@@ -39,7 +39,7 @@ import usePrevious from '../../hooks/usePrevious';
import { LOG_CONTEXT_NAME, LOG_MENU, LOG_MESSAGE_ID, panel_names } from '../../utils/Config';
import SearchInputOverlay from './SearchInpuOverlay';
import css from './SearchPanel.new.module.less';
import SearchResultsNew from './SearchResults.new';
import SearchResultsNew from './SearchResults.new.v2';
import TInput, { ICONS, KINDS } from './TInput/TInput';
import VoiceInputOverlay, { VOICE_MODES } from './VoiceInputOverlay/VoiceInputOverlay';

View File

@@ -0,0 +1,318 @@
import React, { useCallback, useMemo, useRef, useState } from 'react';
import classNames from 'classnames';
import Spottable from '@enact/spotlight/Spottable';
import downBtnImg from '../../../assets/images/btn/search_btn_down_arrow.png';
import upBtnImg from '../../../assets/images/btn/search_btn_up_arrow.png';
import CustomImage from '../../components/CustomImage/CustomImage';
import TButtonTab, { LIST_TYPE } from '../../components/TButtonTab/TButtonTab';
import TDropDown from '../../components/TDropDown/TDropDown';
import TVirtualGridList from '../../components/TVirtualGridList/TVirtualGridList';
import { $L } from '../../utils/helperMethods';
import { SpotlightIds } from '../../utils/SpotlightIds';
import css from './SearchResults.new.v2.module.less';
import ItemCard from './SearchResultsNew/ItemCard';
import ShowCard from './SearchResultsNew/ShowCard';
import HowAboutThese from './HowAboutThese/HowAboutThese';
import HowAboutTheseSmall from './HowAboutThese/HowAboutThese.small';
const ITEMS_PER_PAGE = 10;
// HowAboutThese 모드 상수
export const HOW_ABOUT_THESE_MODES = {
SMALL: 'small', // 작은 버전 (기본)
FULL: 'full', // 전체 버전 (팝업)
};
const SearchResultsNew = ({ itemInfo, showInfo, themeInfo, shopperHouseInfo, keywordClick }) => {
// ShopperHouse 데이터를 ItemCard 형식으로 변환
const convertedShopperHouseItems = useMemo(() => {
if (!shopperHouseInfo || !shopperHouseInfo.results || shopperHouseInfo.results.length === 0) {
return null;
}
const resultData = shopperHouseInfo.results[0];
const docs = resultData.docs || [];
return docs.map((doc) => {
const contentId = doc.contentId;
const tokens = contentId.split('_');
const patnrId = tokens?.[4] || '';
const prdtId = tokens?.[5] || '';
return {
thumbnail: doc.thumbnail || doc.imgPath || '', // 이미지 경로 (API 필드명 수정)
title: doc.title || doc.prdtName || '', // 제목
dcPrice: doc.dcPrice || doc.price || '', // 할인가격
price: doc.price || '', // 원가
soldout: doc.soldout || 'N', // 품절 여부
contentId, // 콘텐트 아이디
reviewGrade: doc.reviewGrade || '', // 리뷰 점수
partnerName: doc.partnerName || '', // 파트너 네임
partnerLogo: doc.partnerLogo || '', // 파트너 로고 (API 명세서 추가)
rankInfo: doc.rankInfo || 0, // 랭킹 정보 (API 명세서 추가)
patnrId, // 파트너 아이디
prdtId, // 상품 아이디
// results 레벨 추가 정보
searchId: resultData.searchId || '',
sortingType: resultData.sortingType || '',
rangeType: resultData.rangeType || '',
};
});
}, [shopperHouseInfo]);
const getButtonTabList = () => {
// ShopperHouse 데이터가 있으면 그것을 사용, 없으면 기존 검색 결과 사용
const itemLength = convertedShopperHouseItems?.length || itemInfo?.length || 0;
const showLength = showInfo?.length || 0;
return [
itemLength && $L(`ITEM (${itemLength})`),
showLength && $L(`SHOWS (${showLength})`),
].filter(Boolean);
};
let buttonTabList = null;
//탭
const [tab, setTab] = useState(0);
//드롭다운
const [dropDownTab, setDropDownTab] = useState(0);
//표시할 아이템 개수
const [visibleCount, setVisibleCount] = useState(ITEMS_PER_PAGE);
const [styleChange, setStyleChange] = useState(false);
const filterMethods = [];
const cbChangePageRef = useRef(null);
// HowAboutThese 모드 상태 관리
const [howAboutTheseMode, setHowAboutTheseMode] = useState(HOW_ABOUT_THESE_MODES.SMALL);
// HowAboutThese 모드 전환 핸들러
const handleShowFullHowAboutThese = useCallback(() => {
setHowAboutTheseMode(HOW_ABOUT_THESE_MODES.FULL);
}, []);
// HowAboutThese 닫기 핸들러 (Full -> Small)
const handleCloseHowAboutThese = useCallback(() => {
setHowAboutTheseMode(HOW_ABOUT_THESE_MODES.SMALL);
}, []);
// 쿼리 클릭 핸들러 (Small 버전용)
const handleSmallQueryClick = useCallback(
(query) => {
if (keywordClick) {
keywordClick(query);
}
// 쿼리 클릭 시에는 모드 유지 (small 계속 표시)
},
[keywordClick]
);
// 쿼리 클릭 핸들러 (Full 버전용)
const handleFullQueryClick = useCallback(
(query) => {
if (keywordClick) {
keywordClick(query);
}
setHowAboutTheseMode(HOW_ABOUT_THESE_MODES.SMALL);
},
[keywordClick]
);
if (!buttonTabList) {
buttonTabList = getButtonTabList();
}
// 현재 탭의 데이터 가져오기 - ShopperHouse 데이터 우선
const currentData = tab === 0 ? convertedShopperHouseItems || itemInfo : showInfo;
// 표시할 데이터 (처음부터 visibleCount 개수만큼)
const displayedData = useMemo(() => {
if (!currentData) return [];
return currentData.slice(0, visibleCount);
}, [currentData, visibleCount]);
// 더 불러올 데이터가 있는지 확인
const hasMore = currentData && visibleCount < currentData.length;
const handleStyle = useCallback(() => {
setStyleChange(true);
}, []);
const handleStyleOut = useCallback(() => {
setStyleChange(false);
}, []);
//탭 클릭
const handleButtonTabClick = useCallback(
({ index }) => {
if (index === tab) {
return;
}
setTab(index);
setVisibleCount(ITEMS_PER_PAGE); // 탭 변경시 표시 개수 리셋
if (cbChangePageRef.current) {
cbChangePageRef.current(0, false, false);
}
},
[tab]
);
//필터선택
const handleSelectFilter = useCallback(
({ selected }) => {
if (selected === dropDownTab) {
return;
}
setDropDownTab(selected);
setVisibleCount(ITEMS_PER_PAGE); // 필터 변경시 표시 개수 리셋
},
[dropDownTab]
);
const SpottableLi = Spottable('li');
const SpottableDiv = Spottable('div');
// 맨 처음으로 이동 (위 버튼)
const upBtnClick = () => {
if (cbChangePageRef.current) {
cbChangePageRef.current(0, true);
}
};
// 10개씩 추가 로드 (아래 버튼)
const downBtnClick = useCallback(() => {
if (hasMore) {
setVisibleCount((prev) => prev + ITEMS_PER_PAGE);
}
}, [hasMore]);
// ProductCard 컴포넌트
const renderItem = useCallback(
({ index, ...rest }) => {
const { bgImgPath, title, partnerLogo, partnerName, keyword } = themeInfo[index];
return (
<SpottableDiv
key={`searchProduct-${index}`}
className={css.productCard}
spotlightId={`searchProduct-${index}`}
{...rest}
>
<div className={css.productImageWrapper}>
<img src={bgImgPath} alt={title} className={css.productImage} />
</div>
<div className={css.productInfo}>
<div className={css.productBrandWrapper}>
<img src={partnerLogo} alt={partnerName} className={css.brandLogo} />
</div>
<div className={css.productDetails}>
{keyword && (
<div className={css.brandName}>
{keyword.map((item, keywordIndex) => (
<span key={keywordIndex}># {item}</span>
))}
</div>
)}
<div className={css.productTitle}>{title}</div>
</div>
</div>
</SpottableDiv>
);
},
[themeInfo]
);
// relativeQuerys 가져오기 (ShopperHouse API 응답)
const relativeQuerys = useMemo(() => {
if (shopperHouseInfo?.results?.[0]?.relativeQuerys) {
return shopperHouseInfo.results[0].relativeQuerys;
}
// 기본값
return ['Puppy food', 'Dog toy', 'Fitness'];
}, [shopperHouseInfo]);
return (
<div className={css.searchBox}>
{/* HowAboutThese Small 버전 - 기본 인라인 표시 */}
<HowAboutTheseSmall
relativeQueries={relativeQuerys}
onQueryClick={handleSmallQueryClick}
onSeeMoreClick={handleShowFullHowAboutThese}
/>
{/* HowAboutThese Full 버전 - 오버레이로 표시 */}
{howAboutTheseMode === HOW_ABOUT_THESE_MODES.FULL && (
<div className={css.howAboutTheseOverlay}>
<HowAboutThese
relativeQueries={relativeQuerys}
onQueryClick={handleFullQueryClick}
onClose={handleCloseHowAboutThese}
/>
</div>
)}
{themeInfo && themeInfo?.length > 0 && (
<div className={css.hotpicksSection} data-wheel-point="true">
<div className={css.sectionHeader}>
<div className={css.sectionIndicator} />
<div className={css.sectionTitle}>Hot Picks ({themeInfo?.length})</div>
</div>
<div className={css.productList}>
<TVirtualGridList
dataSize={themeInfo?.length}
direction="horizontal"
renderItem={renderItem}
itemWidth={416}
itemHeight={436}
spacing={20}
/>
</div>
</div>
)}
<div className={css.itemBox}>
<div className={css.tabContainer}>
<TButtonTab
contents={buttonTabList}
onItemClick={handleButtonTabClick}
selectedIndex={tab}
listType={LIST_TYPE.medium}
spotlightId={SpotlightIds.SEARCH_TAB_CONTAINER}
/>
{/* 2025/10/17 김영진 부장과 이야기 하여 일반에서는 아직 필터 검토하지않아 빼달라고 하여 우선 일반검색에서는 미노출 처리 추후 처리 필요함 */}
{tab === 0 && !itemInfo?.length && (
<TDropDown
className={classNames(
css.dropdown,
styleChange === true ? css.categoryDropdown : null
)}
onSelect={handleSelectFilter}
onOpen={handleStyle}
onClose={handleStyleOut}
selectedIndex={dropDownTab}
width="small"
>
{filterMethods}
</TDropDown>
)}
</div>
{tab === 0 && <ItemCard itemInfo={displayedData} />}
{tab === 1 && <ShowCard showInfo={displayedData} />}
</div>
<div className={css.buttonContainer}>
{hasMore && (
<SpottableDiv onClick={downBtnClick} className={css.downBtn}>
<CustomImage className={css.btnImg} src={downBtnImg} alt="Down arrow" />
</SpottableDiv>
)}
<SpottableDiv onClick={upBtnClick} className={css.upBtn}>
<CustomImage className={css.btnImg} src={upBtnImg} alt="Up arrow" />
</SpottableDiv>
</div>
</div>
);
};
export default SearchResultsNew;

View File

@@ -0,0 +1,362 @@
@import "../../style/CommonStyle.module.less";
@import "../../style/utils.module.less";
.searchBox {
width: 100%;
height: 100%;
position: relative; // ⭐ 추가: absolute positioning을 위한 기준점
.topBox {
width: 100%;
height: 100%;
padding: 30px 60px;
background: #DDDDDD;
display: inline-flex;
justify-content: flex-start;
align-items: center;
gap: 19px;
.topBoxTitle {
width: 254px;
height: 48px;
position: relative;
.titleText {
left: 53px;
top: 10px;
position: absolute;
border-bottom: 1px black solid;
display: inline-flex;
justify-content: center;
align-items: center;
gap: 10px;
.text {
text-align: center;
justify-content: center;
display: flex;
flex-direction: column;
color: #272727;
font-size: 24px;
font-family: 'Roboto', sans-serif;
font-weight: 700;
word-wrap: break-word;
}
}
}
.topBoxList {
flex: 1 1 0;
overflow: hidden;
display: flex;
justify-content: flex-start;
align-items: flex-start;
gap: 15px;
.topBoxListItem {
padding: 20px;
background: white;
border-radius: 100px;
outline: 1px #DADADA solid;
outline-offset: -1px;
display: inline-flex;
flex-direction: column;
justify-content: flex-start;
align-items: flex-start;
gap: 10px;
cursor: pointer;
transition: all 0.2s ease;
.text {
text-align: center;
color: black;
font-size: 24px;
font-family: 'LG Smart UI', sans-serif;
font-weight: 700;
line-height: 24px;
word-wrap: break-word;
}
&:focus {
outline: 2px solid #00a0e9;
outline-offset: 2px;
}
&:hover {
background: #f8f8f8;
transform: translateY(-1px);
}
}
}
.seeMoreButton {
width: 200px;
height: 60px;
background: #808080;
border-radius: 6px;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
transition: background-color 0.2s ease;
&:focus {
outline: 2px solid #00a0e9;
outline-offset: 2px;
}
&:hover {
background: #666666;
}
.seeMoreText {
text-align: center;
color: white;
font-size: 24px;
font-family: 'LG Smart UI', sans-serif;
font-weight: 700;
line-height: 24px;
word-wrap: break-word;
}
}
}
.hotpicksSection {
padding-top: 63px;
padding-left: 60px;
width: 1800px;
height: 580px;
.sectionHeader {
width: 1800px;
height: 42px;
justify-content: flex-start;
align-items: center;
display: inline-flex;
> * {
margin-right: 12px;
&:last-child {
margin-right: 0;
}
}
.sectionIndicator {
width: 6px;
height: 36px;
background: #c70850;
}
.sectionTitle {
text-align: center;
color: black;
font-size: 42px;
font-family: "LG Smart UI";
font-weight: 700;
line-height: 42px;
word-wrap: break-word;
}
}
// 상품 리스트 스타일 (Hot Picks for You)
.productList {
padding-top: 30px;
.size(@w: 100%, @h: inherit);
> div:nth-child(1) {
.size(@w: 100%, @h: inherit);
}
.productCard {
width: 546px;
padding: 18px;
background: white;
border-radius: 12px;
border: 5px solid #dadada;
flex-direction: column;
justify-content: flex-start;
align-items: flex-start;
display: inline-flex;
cursor: pointer;
transition: all 0.2s ease;
&:hover {
border-color: @PRIMARY_COLOR_RED;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
&:focus {
border: 5px solid @PRIMARY_COLOR_RED;
box-shadow: 0 0 22px 0 rgba(0, 0, 0, 0.5);
outline: none;
}
.productImageWrapper {
align-self: stretch;
height: 287px;
position: relative;
.productImage {
width: 510px;
height: 287px;
position: absolute;
left: 0;
top: 0;
object-fit: cover;
border-radius: 8px;
}
}
.productInfo {
align-self: stretch;
padding-top: 15px;
padding-bottom: 15px;
justify-content: flex-start;
align-items: flex-start;
display: inline-flex;
> * {
margin-right: 10px;
&:last-child {
margin-right: 0;
}
}
.productBrandWrapper {
justify-content: flex-start;
align-items: center;
display: flex;
> * {
margin-right: 10px;
&:last-child {
margin-right: 0;
}
}
.brandLogo {
width: 60px;
height: 60px;
border-radius: 8px;
object-fit: cover;
}
}
.productDetails {
flex: 1;
flex-direction: column;
justify-content: center;
align-items: flex-start;
display: inline-flex;
> * {
margin-bottom: 10px;
&:last-child {
margin-bottom: 0;
}
}
.brandName {
text-align: center;
color: #808080;
font-size: 18px;
font-family: "LG Smart UI";
font-weight: 700;
line-height: 18px;
word-wrap: break-word;
}
.productTitle {
align-self: stretch;
color: black;
font-size: 24px;
font-family: "LG Smart UI";
font-weight: 400;
line-height: 24px;
word-wrap: break-word;
}
}
}
}
}
}
.itemBox {
margin-top: 60px;
width: 100%;
padding-left: 60px;
overflow: unset;
.title {
padding: 38px 0 33px 0;
}
.tabContainer {
width: -webkit-fit-content;
height: auto;
position: relative;
.dropdown {
position: absolute;
right: 0;
top: 50%;
transform: translateY(-50%);
&.categoryDropdown {
> div {
> div {
border-radius: 6px 6px 0 0 !important;
}
}
}
}
}
}
.buttonContainer {
width: 100%;
display: flex;
justify-content: center;
align-items: center;
padding: 30px 0;
.downBtn {
width: 100px;
height: 100px;
background: #4f172c;
margin-right: 10px;
border-radius: 100%;
display: flex;
justify-content: center;
align-items: center;
border: 4px solid #4f172c;
.btnImg {
width: 42px;
}
&:focus {
border: 4px solid @PRIMARY_COLOR_RED;
}
}
.upBtn {
width: 100px;
height: 100px;
background: #4f172c;
border-radius: 100%;
display: flex;
justify-content: center;
align-items: center;
border: 4px solid #4f172c;
.btnImg {
width: 42px;
}
&:focus {
border: 4px solid @PRIMARY_COLOR_RED;
}
}
}
// HowAboutThese Full 버전 오버레이
.howAboutTheseOverlay {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 860px; // HowAboutThese의 높이
z-index: 100;
pointer-events: auto; // 오버레이 전체를 클릭 가능하게 (배경 클릭 방지)
}
}