[251103] fix: TDropDown 필터링 기능 구현 및 복합 이벤트 처리

- 원본 TDropDown에 onChange/onSelect 이벤트 처리 추가
- 로컬 TDropDown(SearchPanel) 생성 및 Spotlight 지원 구현
- width="small" 스타일 및 spotlightId prop 지원 추가
- SearchResults.new.v2.jsx에서 로컬 TDropDown 사용 설정
- sortItems 함수 및 필터 선택 핸들러 연결

이전 필터링 이벤트가 제대로 처리되지 않는 문제를 해결했습니다.
Enact Dropdown의 onChange/onSelect 이벤트를 모두 처리하여
필터링 기능이 정상 작동하도록 수정했습니다.

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-03 16:20:28 +09:00
parent 7d40af88d2
commit dba5d4d680
4 changed files with 354 additions and 28 deletions

View File

@@ -15,6 +15,7 @@ export default memo(function TDropDown({
direction = "below",
onClose,
onOpen,
onSelect,
scrollTop,
selectedIndex,
size,
@@ -34,6 +35,12 @@ export default memo(function TDropDown({
}
}, [onClose]);
const _onChange = useCallback((event) => {
if (onSelect) {
onSelect({ selected: event.selected });
}
}, [onSelect]);
return (
<DropDown
className={classNames(
@@ -45,6 +52,8 @@ export default memo(function TDropDown({
)}
direction={direction}
selected={selectedIndex}
onChange={_onChange}
onSelect={_onChange}
onFocus={handleScrollReset}
onBlur={handleStopScrolling}
onOpen={_onOpen}

View File

@@ -13,7 +13,7 @@ import { pushPanel } from '../../actions/panelActions';
import { hideShopperHouseError } from '../../actions/searchActions';
import CustomImage from '../../components/CustomImage/CustomImage';
import TButtonTab, { LIST_TYPE } from '../../components/TButtonTab/TButtonTab';
import TDropDown from '../../components/TDropDown/TDropDown';
import TDropDown from './components/TDropDown/TDropDown';
import TVirtualGridList from '../../components/TVirtualGridList/TVirtualGridList';
import { panel_names } from '../../utils/Config';
import { $L } from '../../utils/helperMethods';
@@ -193,6 +193,30 @@ const SearchResultsNew = ({
onSearchInputFocus = false,
}) => {
const dispatch = useDispatch();
// ✅ State 정의를 먼저 선언 (hoisting 문제 해결)
//탭
const [tab, setTab] = useState(panelInfo?.tab ? panelInfo?.tab : 0);
//드롭다운
const [dropDownTab, setDropDownTab] = useState(0); // 기본값: 첫번째 옵션(API sortingType)
//표시할 아이템 개수
const [visibleCount, setVisibleCount] = useState(ITEMS_PER_PAGE);
const [styleChange, setStyleChange] = useState(false);
// HowAboutThese 모드 상태 관리
const [howAboutTheseMode, setHowAboutTheseMode] = useState(HOW_ABOUT_THESE_MODES.SMALL);
// HowAboutThese Response 로딩 상태
const [isHowAboutTheseLoading, setIsHowAboutTheseLoading] = useState(false);
const [hasPendingResponse, setHasPendingResponse] = useState(false);
const previousShopperHouseInfoRef = useRef(shopperHouseInfo);
const localChangePageRef = useRef(null);
const effectiveChangePageRef = cbChangePageRef ?? localChangePageRef;
const [pendingFocusIndex, setPendingFocusIndex] = useState(null);
// ShopperHouse 에러 팝업 상태 가져오기
const shopperHouseErrorPopup = useSelector((state) => state.search.shopperHouseErrorPopup);
// ShopperHouse 데이터를 ItemCard 형식으로 변환
const convertedShopperHouseItems = useMemo(() => {
// 🎯 Fallback 로직: HowAboutThese 로딩 중에만 fallbackShopperHouseData 허용
@@ -231,7 +255,7 @@ const SearchResultsNew = ({
rangeType: resultData.rangeType || '',
};
});
}, [shopperHouseInfo, fallbackShopperHouseData]);
}, [shopperHouseInfo, fallbackShopperHouseData, isHowAboutTheseLoading]);
const getButtonTabList = () => {
// ShopperHouse 데이터가 있으면 그것을 사용, 없으면 기존 검색 결과 사용
@@ -244,15 +268,6 @@ const SearchResultsNew = ({
].filter(Boolean);
};
//탭
const [tab, setTab] = useState(panelInfo?.tab ? panelInfo?.tab : 0);
//드롭다운
const [dropDownTab, setDropDownTab] = useState(0); // 기본값: 첫번째 옵션(API sortingType)
//표시할 아이템 개수
const [visibleCount, setVisibleCount] = useState(ITEMS_PER_PAGE);
const [styleChange, setStyleChange] = useState(false);
// 동적 정렬 옵션 배열 (인덱스 기반)
const filterOptions = useMemo(() => {
const apiSortingType = convertedShopperHouseItems?.[0]?.sortingType;
@@ -268,7 +283,7 @@ const SearchResultsNew = ({
];
}, [convertedShopperHouseItems]);
// UI에 표시되는 라벨만 추출
// UI에 표시되는 라벨만 추출 (문자열 배열)
const filterMethods = useMemo(() =>
filterOptions.map(opt => $L(opt.label)),
[filterOptions]
@@ -294,20 +309,7 @@ const SearchResultsNew = ({
setVisibleCount(ITEMS_PER_PAGE); // 표시 개수도 리셋
}, [shopperHouseInfo?.results?.[0]?.searchId]);
const localChangePageRef = useRef(null);
const effectiveChangePageRef = cbChangePageRef ?? localChangePageRef;
const [pendingFocusIndex, setPendingFocusIndex] = useState(null);
// HowAboutThese 모드 상태 관리
const [howAboutTheseMode, setHowAboutTheseMode] = useState(HOW_ABOUT_THESE_MODES.SMALL);
// HowAboutThese Response 로딩 상태
const [isHowAboutTheseLoading, setIsHowAboutTheseLoading] = useState(false);
const [hasPendingResponse, setHasPendingResponse] = useState(false);
const previousShopperHouseInfoRef = useRef(shopperHouseInfo);
// ShopperHouse 에러 팝업 상태 가져오기
const shopperHouseErrorPopup = useSelector((state) => state.search.shopperHouseErrorPopup);
// ✅ 이미 위에서 정의됨 (중복 제거)
// 자동 닫기 타이머 (3초 후 자동으로 팝업 닫기)
useEffect(() => {
@@ -430,6 +432,15 @@ const SearchResultsNew = ({
// 현재 탭의 데이터 가져오기 - ShopperHouse 데이터 우선 (정렬된 데이터 사용)
const hasShopperHouseItems = convertedShopperHouseItems?.length > 0;
const currentData = tab === 0 ? convertedShopperHouseItemsSorted || itemInfo : showInfo;
console.log('[SearchResultsNew] 📊 렌더링 상태 체크:');
console.log(' - tab:', tab);
console.log(' - hasShopperHouseItems:', hasShopperHouseItems);
console.log(' - convertedShopperHouseItems?.length:', convertedShopperHouseItems?.length);
console.log(' - filterOptions:', filterOptions);
console.log(' - filterMethods:', filterMethods);
console.log(' - dropDownTab:', dropDownTab);
console.log(' - TDropDown 렌더링 조건 (tab === 0 && hasShopperHouseItems):', tab === 0 && hasShopperHouseItems);
// 표시할 데이터 (처음부터 visibleCount 개수만큼)
const displayedData = useMemo(() => {
@@ -467,16 +478,28 @@ const SearchResultsNew = ({
//필터선택
const handleSelectFilter = useCallback(
({ selected }) => {
console.log('[SearchResultsNew] 🔔 handleSelectFilter 호출됨!');
console.log('[SearchResultsNew] - 전달받은 selected:', selected);
console.log('[SearchResultsNew] - 현재 dropDownTab:', dropDownTab);
console.log('[SearchResultsNew] - filterOptions:', filterOptions);
if (selected === dropDownTab) {
console.log('[SearchResultsNew] ⏭️ 동일한 정렬 옵션 선택 무시 - selected:', selected);
return;
}
console.log('[SearchResultsNew] 🎯 정렬 옵션 변경 -', dropDownTab, '→', selected, '(', SORTING_TYPE_MAP[selected], ')');
const selectedOption = filterOptions[selected];
console.log('[SearchResultsNew] 🎯 정렬 옵션 변경');
console.log('[SearchResultsNew] - 이전:', dropDownTab, '→ 새로운:', selected);
console.log('[SearchResultsNew] - 선택된 옵션:', selectedOption);
console.log('[SearchResultsNew] - 정렬 타입:', selectedOption?.value);
setDropDownTab(selected);
setVisibleCount(ITEMS_PER_PAGE); // 필터 변경시 표시 개수 리셋
console.log('[SearchResultsNew] ✅ dropDownTab 상태 업데이트 완료');
},
[dropDownTab]
[dropDownTab, filterOptions]
);
// Spottable 컴포넌트 캐싱하여 메모리 누수 방지
@@ -736,7 +759,10 @@ const SearchResultsNew = ({
onOpen={handleStyle}
onClose={handleStyleOut}
selectedIndex={dropDownTab}
size="small"
width="small"
spotlightId="sortingDropdown"
data-spotlight-up={SpotlightIds.SEARCH_TAB_CONTAINER}
>
{filterMethods}
</TDropDown>

View File

@@ -0,0 +1,123 @@
import React, { memo, useCallback } from "react";
import classNames from "classnames";
import DropDown from "@enact/sandstone/Dropdown";
import Spottable from "@enact/spotlight/Spottable";
import SpotlightContainerDecorator from "@enact/spotlight/SpotlightContainerDecorator";
import { countryCode } from "../../../../api/apiConfig";
import useScrollReset from "../../../../hooks/useScrollReset";
import css from "./TDropDown.module.less";
const SpottableDropDown = Spottable(DropDown);
const Container = SpotlightContainerDecorator(
{ enterTo: "last-focused" },
"div"
);
export default memo(function TDropDown({
children,
className,
color,
direction = "below",
onClose,
onOpen,
onSelect,
scrollTop,
selectedIndex,
size,
spotlightId,
width,
...rest
}) {
const { handleScrollReset, handleStopScrolling } = useScrollReset(scrollTop);
const _onOpen = useCallback(() => {
if (onOpen) {
onOpen();
}
}, [onOpen]);
const _onClose = useCallback(() => {
if (onClose) {
onClose();
}
}, [onClose]);
const _onChange = useCallback((event) => {
console.log('[TDropDown LOCAL] 🔥 onChange 이벤트 발생!');
console.log('[TDropDown LOCAL] event:', event);
console.log('[TDropDown LOCAL] event.selected:', event?.selected);
console.log('[TDropDown LOCAL] event.value:', event?.value);
console.log('[TDropDown LOCAL] onSelect 콜백:', !!onSelect);
if (onSelect) {
console.log('[TDropDown LOCAL] ✅ onSelect 호출:', { selected: event.selected });
onSelect({ selected: event.selected });
}
}, [onSelect]);
console.log('[TDropDown LOCAL] ⚠️ TDropDown 렌더링 - selectedIndex:', selectedIndex, 'onSelect:', !!onSelect, 'children:', Array.isArray(children) ? children.length : typeof children);
// Spotlight을 사용하지 않는 버전 (필터링 테스트용)
if (spotlightId) {
return (
<Container spotlightId={spotlightId}>
<SpottableDropDown
className={classNames(
css.tDropdown,
css[size],
css[color],
(selectedIndex !== undefined && selectedIndex !== null) && css.selected,
className
)}
direction={direction}
selected={selectedIndex}
onChange={_onChange}
onSelect={_onChange}
onFocus={handleScrollReset}
onBlur={handleStopScrolling}
onOpen={_onOpen}
onClose={_onClose}
style={width === 'small' ? { width: '240px', minWidth: '240px' } :
width === 'medium' ? { width: '320px', minWidth: '320px' } :
width === 'large' ? { width: '400px', minWidth: '400px' } : undefined}
aria-disabled={countryCode && countryCode !== "US"}
aria-label={children}
{...rest}
>
{children}
</SpottableDropDown>
</Container>
);
}
// Spotlight 없이 기본 DropDown 사용
return (
<DropDown
className={classNames(
css.tDropdown,
css[size],
css[color],
(selectedIndex !== undefined && selectedIndex !== null) && css.selected,
className
)}
direction={direction}
selected={selectedIndex}
onChange={_onChange}
onSelect={_onChange}
onFocus={handleScrollReset}
onBlur={handleStopScrolling}
onOpen={_onOpen}
onClose={_onClose}
style={width === 'small' ? { width: '240px', minWidth: '240px' } :
width === 'medium' ? { width: '320px', minWidth: '320px' } :
width === 'large' ? { width: '400px', minWidth: '400px' } : undefined}
aria-disabled={countryCode && countryCode !== "US"}
aria-label={children}
{...rest}
>
{children}
</DropDown>
);
});

View File

@@ -0,0 +1,168 @@
@import "../../../../style/CommonStyle.module.less";
@import "../../../../style/utils.module.less";
.tDropdown {
margin-left: 0px !important;
margin-right: 0px !important;
[role="button"] {
height: 60px !important;
border-radius: 6px !important;
padding: 20px 19px !important;
font-weight: bold;
font-size: 24px !important;
background-color: @COLOR_WHITE !important;
color: @PRIMARY_COLOR_RED !important;
margin: 0 !important;
> div:first-child {
background-color: transparent !important;
box-shadow: none !important;
}
> div:nth-child(2) > div:nth-child(1) {
margin-right: 0 !important;
margin-left: 10px !important;
font-size: 24px !important;
width: 24px !important;
color: @PRIMARY_COLOR_RED !important;
}
&:focus {
background-color: @PRIMARY_COLOR_RED !important;
color: @COLOR_WHITE !important;
> div:nth-child(2) > div:nth-child(1) {
color: @COLOR_WHITE !important;
}
}
&.selected {
background-color: @PRIMARY_COLOR_RED !important;
color: @COLOR_WHITE !important;
> div:nth-child(2) > div:nth-child(1) {
color: @COLOR_WHITE !important;
}
}
}
}
.small {
width: 240px !important;
min-width: 240px !important;
}
.medium {
width: 320px !important;
min-width: 320px !important;
}
.large {
width: 400px !important;
min-width: 400px !important;
}
[id="floatLayer"] {
--list-background-color: @COLOR_GRAY03;
--list-item-font-color: @COLOR_WHITE;
--list-item-focus-background-color: @PRIMARY_COLOR_RED;
--list-item-focus-font-color: @COLOR_WHITE;
--list-item-select-border-color: @PRIMARY_COLOR_RED;
--scroll-track-color: @COLOR_GRAY03;
--scroll-bar-color: @COLOR_GRAY04;
> div > div[id] > div:nth-child(2) {
bottom: unset !important;
transform: unset !important;
> div:nth-child(1) {
//list layout
padding: 0 !important;
margin-top: 4px !important;
box-shadow: none !important;
background-color: var(--list-background-color) !important;
border-radius: 6px !important;
> div {
padding: 0 !important;
padding-top: 14px !important;
height: auto !important;
> div > div {
> div {
padding: 0 15px !important;
> div[aria-checked="true"] {
background-color: @COLOR_WHITE !important;
border: 2px solid @PRIMARY_COLOR_RED;
> div:nth-child(2) {
> div {
> div {
color: @PRIMARY_COLOR_RED;
}
}
}
}
&:focus {
background-color: var(
--list-item-focus-background-color
) !important;
color: @COLOR_WHITE !important;
}
> div:nth-child(1) {
font-weight: bold;
color: @COLOR_WHITE;
font-size: 24px !important;
margin: 0 !important;
padding: 0 !important;
height: 63px !important;
line-height: 63px !important;
-webkit-margin-start: 0 !important;
-webkit-inline-start: 0 !important;
> div:first-child {
//list item shadow
background-color: transparent !important;
box-shadow: none !important;
}
> div:nth-child(3) {
//check icon
display: none !important;
}
}
}
}
> div:nth-child(2) > div {
// scroll track
background-color: var(--scroll-track-color) !important;
width: 4px !important;
border-radius: 20px !important;
> div {
//scroll bar
background: var(--scroll-bar-color) !important;
width: 100% !important;
border-radius: inherit !important;
}
}
> div > div {
> div {
> div[aria-checked="true"] {
&:focus {
> div:nth-child(2) {
> div {
> div {
color: @COLOR_WHITE;
}
}
}
}
}
}
}
}
}
}
}