[251019] fix: Resolve warnings-1
🕐 커밋 시간: 2025. 10. 19. 23:37:25 📊 변경 통계: • 총 파일: 18개 • 추가: +347줄 • 삭제: -449줄 📝 수정된 파일: ~ com.twin.app.shoptime/src/components/VideoPlayer/MediaPlayer.jsx ~ com.twin.app.shoptime/src/utils/fp.js ~ com.twin.app.shoptime/src/utils/helperMethods.js ~ com.twin.app.shoptime/src/utils/lodashFpEx.js ~ com.twin.app.shoptime/src/utils/spotlight-utils.js ~ com.twin.app.shoptime/src/views/DetailPanel/DetailPanel.jsx ~ com.twin.app.shoptime/src/views/DetailPanel/DetailPanelSkeleton/DetailPanelSkeleton.jsx ~ com.twin.app.shoptime/src/views/PlayerPanel/PlayerItemCard/PlayerItemCard.jsx ~ com.twin.app.shoptime/src/views/PlayerPanel/PlayerOverlay/PlayerOverlayContents.jsx ~ com.twin.app.shoptime/src/views/SearchPanel/SearchPanel.new.jsx ~ com.twin.app.shoptime/src/views/SearchPanel/SearchResults.new.jsx ~ com.twin.app.shoptime/src/views/SearchPanel/SearchResultsNew/ItemCard.jsx ~ com.twin.app.shoptime/src/views/SearchPanel/SearchResultsNew/ShowCard.jsx ~ com.twin.app.shoptime/src/views/SearchPanel/TInput/TInput.jsx ~ com.twin.app.shoptime/src/views/SearchPanel/VoiceInputOverlay/VoiceInputOverlay.jsx ~ com.twin.app.shoptime/src/views/SearchPanel/VoiceInputOverlay/modes/VoicePromptScreen.jsx ~ com.twin.app.shoptime/src/views/SearchPanel/VoiceInputOverlay/modes/VoiceResponse.jsx ~ com.twin.app.shoptime/src/views/UserReview/ShowUserReviews.jsx 🔧 함수 변경 내용: 📊 Function-level changes summary across 18 files: • Functions added: 8 • Functions modified: 14 • Functions deleted: 17 📋 By language: • javascript: 18 files, 39 function changes 🔧 주요 변경 내용: • UI 컴포넌트 아키텍처 개선 • 공통 유틸리티 함수 최적화 Performance: 코드 최적화로 성능 개선 기대
This commit is contained in:
@@ -42,20 +42,20 @@ import Announce from '@enact/ui/AnnounceDecorator/Announce';
|
|||||||
import ComponentOverride from '@enact/ui/ComponentOverride';
|
import ComponentOverride from '@enact/ui/ComponentOverride';
|
||||||
import { FloatingLayerDecorator } from '@enact/ui/FloatingLayer';
|
import { FloatingLayerDecorator } from '@enact/ui/FloatingLayer';
|
||||||
import { FloatingLayerContext } from '@enact/ui/FloatingLayer/FloatingLayerDecorator';
|
import { FloatingLayerContext } from '@enact/ui/FloatingLayer/FloatingLayerDecorator';
|
||||||
import Marquee from '@enact/ui/Marquee';
|
// import Marquee from '@enact/ui/Marquee';
|
||||||
import Slottable from '@enact/ui/Slottable';
|
import Slottable from '@enact/ui/Slottable';
|
||||||
import Touchable from '@enact/ui/Touchable';
|
import Touchable from '@enact/ui/Touchable';
|
||||||
|
|
||||||
import { panel_names } from '../../utils/Config';
|
// import { panel_names } from '../../utils/Config';
|
||||||
import { $L } from '../../utils/helperMethods';
|
import { $L } from '../../utils/helperMethods';
|
||||||
import { SpotlightIds } from '../../utils/SpotlightIds';
|
import { SpotlightIds } from '../../utils/SpotlightIds';
|
||||||
import Loader from '../Loader/Loader';
|
import Loader from '../Loader/Loader';
|
||||||
import { MediaControls, MediaSlider, secondsToTime, Times } from '../MediaPlayer';
|
import { MediaControls, MediaSlider, secondsToTime, Times } from '../MediaPlayer';
|
||||||
import PlayerOverlayContents from '../../views/PlayerPanel/PlayerOverlay/PlayerOverlayContents';
|
import PlayerOverlayContents from '../../views/PlayerPanel/PlayerOverlay/PlayerOverlayContents';
|
||||||
import FeedbackContent from './FeedbackContent';
|
import FeedbackContent from './FeedbackContent';
|
||||||
import FeedbackTooltip from './FeedbackTooltip';
|
// import FeedbackTooltip from './FeedbackTooltip';
|
||||||
import Media from './Media';
|
import Media from './Media';
|
||||||
import MediaTitle from './MediaTitle';
|
// import MediaTitle from './MediaTitle';
|
||||||
import Overlay from './Overlay';
|
import Overlay from './Overlay';
|
||||||
import TReactPlayer from './TReactPlayer';
|
import TReactPlayer from './TReactPlayer';
|
||||||
import Video from './Video';
|
import Video from './Video';
|
||||||
@@ -78,7 +78,7 @@ const calcNumberValueOfPlaybackRate = (rate) => {
|
|||||||
const pbArray = String(rate).split('/');
|
const pbArray = String(rate).split('/');
|
||||||
return pbArray.length > 1 ? parseInt(pbArray[0]) / parseInt(pbArray[1]) : parseInt(rate);
|
return pbArray.length > 1 ? parseInt(pbArray[0]) / parseInt(pbArray[1]) : parseInt(rate);
|
||||||
};
|
};
|
||||||
const SpottableBtn = Spottable('button');
|
// const SpottableBtn = Spottable('button');
|
||||||
const SpottableDiv = Touchable(Spottable('div'));
|
const SpottableDiv = Touchable(Spottable('div'));
|
||||||
const RootContainer = SpotlightContainerDecorator(
|
const RootContainer = SpotlightContainerDecorator(
|
||||||
{
|
{
|
||||||
@@ -113,7 +113,7 @@ const getDurFmt = (locale) => {
|
|||||||
|
|
||||||
const forwardWithState = (type) => adaptEvent(call('addStateToEvent'), forwardWithPrevent(type));
|
const forwardWithState = (type) => adaptEvent(call('addStateToEvent'), forwardWithPrevent(type));
|
||||||
|
|
||||||
const forwardToggleMore = forward('onToggleMore');
|
// const forwardToggleMore = forward('onToggleMore');
|
||||||
|
|
||||||
// provide forwarding of events on media controls
|
// provide forwarding of events on media controls
|
||||||
const forwardControlsAvailable = forward('onControlsAvailable');
|
const forwardControlsAvailable = forward('onControlsAvailable');
|
||||||
@@ -2001,6 +2001,20 @@ const VideoPlayerBase = class extends React.Component {
|
|||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onSpotlightFocus = () => {
|
||||||
|
this.showControls();
|
||||||
|
|
||||||
|
if (this.state.lastFocusedTarget) {
|
||||||
|
setTimeout(() => {
|
||||||
|
Spotlight.focus(this.state.lastFocusedTarget);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setTimeout(() => {
|
||||||
|
Spotlight.focus(SpotlightIds.PLAYER_TAB_BUTTON);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
slider5WayPressJob = new Job(() => {
|
slider5WayPressJob = new Job(() => {
|
||||||
this.setState({ slider5WayPressed: false });
|
this.setState({ slider5WayPressed: false });
|
||||||
}, 200);
|
}, 200);
|
||||||
@@ -2087,9 +2101,9 @@ const VideoPlayerBase = class extends React.Component {
|
|||||||
className,
|
className,
|
||||||
modalClassName,
|
modalClassName,
|
||||||
disabled,
|
disabled,
|
||||||
infoComponents,
|
// infoComponents,
|
||||||
backButton,
|
// backButton,
|
||||||
promotionTitle,
|
// promotionTitle,
|
||||||
initialJumpDelay,
|
initialJumpDelay,
|
||||||
jumpDelay,
|
jumpDelay,
|
||||||
|
|
||||||
@@ -2105,14 +2119,14 @@ const VideoPlayerBase = class extends React.Component {
|
|||||||
spotlightDisabled,
|
spotlightDisabled,
|
||||||
spotlightId,
|
spotlightId,
|
||||||
style,
|
style,
|
||||||
thumbnailComponent,
|
// thumbnailComponent,
|
||||||
thumbnailSrc,
|
// thumbnailSrc,
|
||||||
title,
|
// title,
|
||||||
introTime,
|
introTime,
|
||||||
onClickSkipIntro,
|
// onClickSkipIntro,
|
||||||
onIntroDisabled,
|
// onIntroDisabled,
|
||||||
videoComponent: VideoComponent,
|
videoComponent: VideoComponent,
|
||||||
cameraSettingsButton,
|
// cameraSettingsButton,
|
||||||
onBackButton,
|
onBackButton,
|
||||||
panelInfo,
|
panelInfo,
|
||||||
selectedIndex,
|
selectedIndex,
|
||||||
@@ -2129,18 +2143,18 @@ const VideoPlayerBase = class extends React.Component {
|
|||||||
disclaimer,
|
disclaimer,
|
||||||
liveTotalTime,
|
liveTotalTime,
|
||||||
currentLiveTimeSeconds,
|
currentLiveTimeSeconds,
|
||||||
themeProductInfos,
|
// themeProductInfos,
|
||||||
detailThemeProductImageLength,
|
// detailThemeProductImageLength,
|
||||||
videoVerticalVisible,
|
videoVerticalVisible,
|
||||||
handleIndicatorDownClick,
|
handleIndicatorDownClick,
|
||||||
handleIndicatorUpClick,
|
handleIndicatorUpClick,
|
||||||
orderPhnNo,
|
// orderPhnNo,
|
||||||
captionEnable,
|
captionEnable,
|
||||||
countryCode,
|
countryCode,
|
||||||
setCurrentTime,
|
// setCurrentTime,
|
||||||
setIsVODPaused,
|
// setIsVODPaused,
|
||||||
qrCurrentItem,
|
qrCurrentItem,
|
||||||
modalScale,
|
// modalScale,
|
||||||
...mediaProps
|
...mediaProps
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
@@ -2222,27 +2236,15 @@ const VideoPlayerBase = class extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
const isQRCodeVisible = playListInfo && qrCurrentItem && !thumbnailUrl && !panelInfo.modal;
|
const isQRCodeVisible = playListInfo && qrCurrentItem && !thumbnailUrl && !panelInfo.modal;
|
||||||
|
|
||||||
const onSpotlightFocus = () => {
|
|
||||||
this.showControls();
|
|
||||||
|
|
||||||
if (this.state.lastFocusedTarget) {
|
|
||||||
setTimeout(() => {
|
|
||||||
Spotlight.focus(this.state.lastFocusedTarget);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
setTimeout(() => {
|
|
||||||
Spotlight.focus(SpotlightIds.PLAYER_TAB_BUTTON);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// MediaPanel에서는 setIsVODPaused 사용 안 함 (PlayerPanel 전용)
|
// MediaPanel에서는 setIsVODPaused 사용 안 함 (PlayerPanel 전용)
|
||||||
// if (panelInfo?.shptmBanrTpNm === 'VOD' || panelInfo?.shptmBanrTpNm === 'MEDIA') {
|
// if (panelInfo?.shptmBanrTpNm === 'VOD' || panelInfo?.shptmBanrTpNm === 'MEDIA') {
|
||||||
// setIsVODPaused(this.state.paused);
|
// setIsVODPaused(this.state.paused);
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
const getVideoPhoneNumberClassNames = () => {
|
const getVideoPhoneNumberClassNames = () => {
|
||||||
const isQVC =
|
const isQVC =
|
||||||
panelInfo?.chanId === 'USQVC' ||
|
panelInfo?.chanId === 'USQVC' ||
|
||||||
@@ -2393,7 +2395,7 @@ const VideoPlayerBase = class extends React.Component {
|
|||||||
playbackRate={this.pulsedPlaybackRate || this.selectPlaybackRate(this.speedIndex)}
|
playbackRate={this.pulsedPlaybackRate || this.selectPlaybackRate(this.speedIndex)}
|
||||||
playbackState={this.pulsedPlaybackState || this.prevCommand}
|
playbackState={this.pulsedPlaybackState || this.prevCommand}
|
||||||
visible={this.state.miniFeedbackVisible && !noMiniFeedback}
|
visible={this.state.miniFeedbackVisible && !noMiniFeedback}
|
||||||
></FeedbackContent>
|
/>
|
||||||
|
|
||||||
<ControlsContainer
|
<ControlsContainer
|
||||||
className={classNames(
|
className={classNames(
|
||||||
@@ -2453,7 +2455,7 @@ const VideoPlayerBase = class extends React.Component {
|
|||||||
value={this.state.proportionPlayed}
|
value={this.state.proportionPlayed}
|
||||||
visible={this.state.mediaSliderVisible}
|
visible={this.state.mediaSliderVisible}
|
||||||
type={type}
|
type={type}
|
||||||
></MediaSlider>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -2483,11 +2485,11 @@ const VideoPlayerBase = class extends React.Component {
|
|||||||
holdConfig={controlsHandleAboveHoldConfig}
|
holdConfig={controlsHandleAboveHoldConfig}
|
||||||
onDown={this.handleControlsHandleAboveDown}
|
onDown={this.handleControlsHandleAboveDown}
|
||||||
onKeyUp={this.handleControlsHandleAboveKeyUp}
|
onKeyUp={this.handleControlsHandleAboveKeyUp}
|
||||||
onSpotlightDown={onSpotlightFocus}
|
onSpotlightDown={this.onSpotlightFocus}
|
||||||
onSpotlightUp={onSpotlightFocus}
|
onSpotlightUp={this.onSpotlightFocus}
|
||||||
onSpotlightRight={onSpotlightFocus}
|
onSpotlightRight={this.onSpotlightFocus}
|
||||||
onSpotlightLeft={onSpotlightFocus}
|
onSpotlightLeft={this.onSpotlightFocus}
|
||||||
onClick={onSpotlightFocus}
|
onClick={this.onSpotlightFocus}
|
||||||
selectionKeys={controlsHandleAboveSelectionKeys}
|
selectionKeys={controlsHandleAboveSelectionKeys}
|
||||||
spotlightDisabled={this.state.mediaControlsVisible || spotlightDisabled}
|
spotlightDisabled={this.state.mediaControlsVisible || spotlightDisabled}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -8,61 +8,127 @@ const safeFp = fp || {};
|
|||||||
|
|
||||||
export const {
|
export const {
|
||||||
// 기본 함수 조합
|
// 기본 함수 조합
|
||||||
pipe, flow, curry, compose,
|
pipe,
|
||||||
|
flow,
|
||||||
|
curry,
|
||||||
|
compose,
|
||||||
|
|
||||||
// 기본 컬렉션 함수들
|
// 기본 컬렉션 함수들
|
||||||
map, filter, reduce, get, set,
|
map,
|
||||||
|
filter,
|
||||||
|
reduce,
|
||||||
|
get,
|
||||||
|
set,
|
||||||
|
|
||||||
// 기본 타입 체크 함수들
|
// 기본 타입 체크 함수들
|
||||||
isEmpty, isNotEmpty, isNil, isNotNil,
|
isEmpty,
|
||||||
|
isNotEmpty,
|
||||||
|
isNil,
|
||||||
|
isNotNil,
|
||||||
|
|
||||||
// 비동기 함수들
|
// 비동기 함수들
|
||||||
mapAsync, reduceAsync, filterAsync, findAsync, forEachAsync,
|
mapAsync,
|
||||||
|
reduceAsync,
|
||||||
|
filterAsync,
|
||||||
|
findAsync,
|
||||||
|
forEachAsync,
|
||||||
|
|
||||||
// Promise 관련
|
// Promise 관련
|
||||||
promisify, then, andThen, otherwise, catch: catchFn, finally: finallyFn,
|
promisify,
|
||||||
|
then,
|
||||||
|
andThen,
|
||||||
|
otherwise,
|
||||||
|
catch: catchFn,
|
||||||
|
finally: finallyFn,
|
||||||
isPromise,
|
isPromise,
|
||||||
|
|
||||||
// 조건부 실행 함수들
|
// 조건부 실행 함수들
|
||||||
when, unless, ifElse, ifT, ifF, ternary,
|
when,
|
||||||
|
unless,
|
||||||
|
ifElse,
|
||||||
|
ifT,
|
||||||
|
ifF,
|
||||||
|
ternary,
|
||||||
|
|
||||||
// 디버깅 및 사이드 이펙트
|
// 디버깅 및 사이드 이펙트
|
||||||
tap, trace,
|
tap,
|
||||||
|
trace,
|
||||||
|
|
||||||
// 안전한 실행
|
// 안전한 실행
|
||||||
tryCatch, safeGet, getOr,
|
tryCatch,
|
||||||
|
safeGet,
|
||||||
|
getOr,
|
||||||
|
|
||||||
// 타입 체크 확장
|
// 타입 체크 확장
|
||||||
isJson, notEquals, isNotEqual, isVal, isPrimitive, isRef, isReference,
|
isJson,
|
||||||
not, notIncludes, toBool, isFalsy, isTruthy,
|
notEquals,
|
||||||
|
isNotEqual,
|
||||||
|
isVal,
|
||||||
|
isPrimitive,
|
||||||
|
isRef,
|
||||||
|
isReference,
|
||||||
|
not,
|
||||||
|
notIncludes,
|
||||||
|
toBool,
|
||||||
|
isFalsy,
|
||||||
|
isTruthy,
|
||||||
|
|
||||||
// 객체 변환
|
// 객체 변환
|
||||||
transformObjectKey, toCamelcase, toCamelKey, toSnakecase, toSnakeKey,
|
transformObjectKey,
|
||||||
toPascalcase, pascalCase, renameKeys,
|
toCamelcase,
|
||||||
|
toCamelKey,
|
||||||
|
toSnakecase,
|
||||||
|
toSnakeKey,
|
||||||
|
toPascalcase,
|
||||||
|
pascalCase,
|
||||||
|
renameKeys,
|
||||||
|
|
||||||
// 배열 유틸리티
|
// 배열 유틸리티
|
||||||
mapWhen, filterWhen, removeByIndex, removeByIdx, removeLast,
|
mapWhen,
|
||||||
append, prepend, insertAt, partition,
|
filterWhen,
|
||||||
|
removeByIndex,
|
||||||
|
removeByIdx,
|
||||||
|
removeLast,
|
||||||
|
append,
|
||||||
|
prepend,
|
||||||
|
insertAt,
|
||||||
|
partition,
|
||||||
|
|
||||||
// 함수 조합 유틸리티
|
// 함수 조합 유틸리티
|
||||||
ap, applyTo, juxt, converge, instanceOf,
|
ap,
|
||||||
|
applyTo,
|
||||||
|
juxt,
|
||||||
|
converge,
|
||||||
|
instanceOf,
|
||||||
|
|
||||||
// 문자열 유틸리티
|
// 문자열 유틸리티
|
||||||
trimToUndefined, capitalize, isDatetimeString,
|
trimToUndefined,
|
||||||
|
capitalize,
|
||||||
|
isDatetimeString,
|
||||||
|
|
||||||
// 수학 유틸리티
|
// 수학 유틸리티
|
||||||
clampTo, between,
|
clampTo,
|
||||||
|
between,
|
||||||
|
|
||||||
// 기본값 처리
|
// 기본값 처리
|
||||||
defaultTo, defaultWith, elvis,
|
defaultTo,
|
||||||
|
defaultWith,
|
||||||
|
elvis,
|
||||||
|
|
||||||
// 키 관련
|
// 키 관련
|
||||||
key, keyByVal,
|
key,
|
||||||
mapWithKey, mapWithIdx, forEachWithKey, forEachWithIdx,
|
keyByVal,
|
||||||
reduceWithKey, reduceWithIdx,
|
mapWithKey,
|
||||||
|
mapWithIdx,
|
||||||
|
forEachWithKey,
|
||||||
|
forEachWithIdx,
|
||||||
|
reduceWithKey,
|
||||||
|
reduceWithIdx,
|
||||||
|
|
||||||
// 유틸리티
|
// 유틸리티
|
||||||
deepFreeze, times, lazy,
|
deepFreeze,
|
||||||
|
times,
|
||||||
|
lazy,
|
||||||
} = safeFp;
|
} = safeFp;
|
||||||
|
|
||||||
export default safeFp;
|
export default safeFp;
|
||||||
|
|||||||
@@ -1,20 +1,18 @@
|
|||||||
import { Job } from "@enact/core/util";
|
import { Job } from '@enact/core/util';
|
||||||
import Enact_$L from "@enact/i18n/$L";
|
|
||||||
|
|
||||||
import stringReSourceDe from "../../resources/de/strings.json";
|
import stringReSourceDe from '../../resources/de/strings.json';
|
||||||
import stringReSourceEn from "../../resources/en/strings.json";
|
import stringReSourceEn from '../../resources/en/strings.json';
|
||||||
import stringReSourceGb from "../../resources/gb/strings.json";
|
import stringReSourceGb from '../../resources/gb/strings.json';
|
||||||
import stringReSourceRu from "../../resources/ru/strings.json";
|
import stringReSourceRu from '../../resources/ru/strings.json';
|
||||||
import { getRicCode } from "../api/apiConfig";
|
import { ERROR_MESSAGES_GROUPS, SECRET_KEY } from './Config';
|
||||||
import { ERROR_MESSAGES_GROUPS, SECRET_KEY } from "./Config";
|
|
||||||
|
|
||||||
let _boundingRectCache = {};
|
let _boundingRectCache = {};
|
||||||
const BOUNDING_RECT_IGNORE_TIME = 10;
|
const BOUNDING_RECT_IGNORE_TIME = 10;
|
||||||
|
|
||||||
const generateUUID = () => {
|
const generateUUID = () => {
|
||||||
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
|
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
||||||
var r = (Math.random() * 16) | 0,
|
const r = (Math.random() * 16) | 0;
|
||||||
v = c === "x" ? r : (r & 0x3) | 0x8;
|
const v = c === 'x' ? r : (r & 0x3) | 0x8;
|
||||||
return v.toString(16);
|
return v.toString(16);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -37,10 +35,7 @@ export const getBoundingClientRect = (node) => {
|
|||||||
const uuid = node.dataset.uuid;
|
const uuid = node.dataset.uuid;
|
||||||
|
|
||||||
if (_boundingRectCache[uuid]) {
|
if (_boundingRectCache[uuid]) {
|
||||||
if (
|
if (Date.now() - _boundingRectCache[uuid].called < BOUNDING_RECT_IGNORE_TIME) {
|
||||||
Date.now() - _boundingRectCache[uuid].called <
|
|
||||||
BOUNDING_RECT_IGNORE_TIME
|
|
||||||
) {
|
|
||||||
return _boundingRectCache[uuid].boundingRect;
|
return _boundingRectCache[uuid].boundingRect;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -63,42 +58,38 @@ const stringReSource = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const $L = (str) => {
|
export const $L = (str) => {
|
||||||
let languageSetting = "system";
|
let languageSetting = 'system';
|
||||||
let resourceKey = "";
|
let resourceKey = '';
|
||||||
|
|
||||||
if (typeof window === "object" && window.store) {
|
if (typeof window === 'object' && window.store) {
|
||||||
languageSetting = window.store.getState().localSettings.languageSetting;
|
languageSetting = window.store.getState().localSettings.languageSetting;
|
||||||
if (languageSetting === "system") {
|
if (languageSetting === 'system') {
|
||||||
resourceKey = window.store.getState().common.httpHeader?.cntry_cd;
|
resourceKey = window.store.getState().common.httpHeader?.cntry_cd;
|
||||||
} else {
|
} else {
|
||||||
resourceKey = languageSetting;
|
resourceKey = languageSetting;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const resource = stringReSource[resourceKey];
|
const resource = stringReSource[resourceKey];
|
||||||
if (typeof str === "object") {
|
if (typeof str === 'object') {
|
||||||
if (resource && resource[str.key]) {
|
if (resource && resource[str.key]) {
|
||||||
return resource[str.key].replace(/{br}/g, "{br}");
|
return resource[str.key].replace(/{br}/g, '{br}');
|
||||||
} else {
|
} else {
|
||||||
return str.value;
|
return str.value;
|
||||||
}
|
}
|
||||||
} else if (resource && resource[str]) {
|
} else if (resource && resource[str]) {
|
||||||
return resource[str].replace(/{br}/g, "{br}");
|
return resource[str].replace(/{br}/g, '{br}');
|
||||||
}
|
}
|
||||||
return str && str.replace(/{br}/g, "{br}");
|
return str && str.replace(/{br}/g, '{br}');
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createQueryString = (object) => {
|
export const createQueryString = (object) => {
|
||||||
const parts = [];
|
const parts = [];
|
||||||
for (const key of Object.getOwnPropertyNames(object)) {
|
for (const key of Object.getOwnPropertyNames(object)) {
|
||||||
if (
|
if (object[key] !== null && object[key] !== undefined && object[key] !== '') {
|
||||||
object[key] !== null &&
|
|
||||||
object[key] !== undefined &&
|
|
||||||
object[key] !== ""
|
|
||||||
) {
|
|
||||||
parts.push(`${key}=${encodeURIComponent(object[key])}`);
|
parts.push(`${key}=${encodeURIComponent(object[key])}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return parts.join("&");
|
return parts.join('&');
|
||||||
};
|
};
|
||||||
|
|
||||||
export const wait = (time) => {
|
export const wait = (time) => {
|
||||||
@@ -110,14 +101,14 @@ export const wait = (time) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const scaleW = (value) => {
|
export const scaleW = (value) => {
|
||||||
if (typeof window === "object") {
|
if (typeof window === 'object') {
|
||||||
return value * (window.innerWidth / 1920);
|
return value * (window.innerWidth / 1920);
|
||||||
}
|
}
|
||||||
return value;
|
return value;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const scaleH = (value) => {
|
export const scaleH = (value) => {
|
||||||
if (typeof window === "object") {
|
if (typeof window === 'object') {
|
||||||
return value * (window.innerHeight / 1080);
|
return value * (window.innerHeight / 1080);
|
||||||
}
|
}
|
||||||
return value;
|
return value;
|
||||||
@@ -150,14 +141,10 @@ let localLaunchParams = {
|
|||||||
export const getLaunchParams = () => {
|
export const getLaunchParams = () => {
|
||||||
let params = {};
|
let params = {};
|
||||||
|
|
||||||
if (
|
if (typeof window === 'object' && window.PalmSystem && window.PalmSystem.launchParams) {
|
||||||
typeof window === "object" &&
|
|
||||||
window.PalmSystem &&
|
|
||||||
window.PalmSystem.launchParams
|
|
||||||
) {
|
|
||||||
try {
|
try {
|
||||||
params = JSON.parse(window.PalmSystem.launchParams);
|
params = JSON.parse(window.PalmSystem.launchParams);
|
||||||
if (params["x-webos-app-container-launch"] === true) {
|
if (params['x-webos-app-container-launch'] === true) {
|
||||||
params = params.details;
|
params = params.details;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -170,28 +157,24 @@ export const getLaunchParams = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const clearLaunchParams = () => {
|
export const clearLaunchParams = () => {
|
||||||
console.log("common.clearLaunchParams");
|
console.log('common.clearLaunchParams');
|
||||||
if (
|
if (typeof window === 'object' && window.PalmSystem && window.PalmSystem.launchParams) {
|
||||||
typeof window === "object" &&
|
window.PalmSystem.launchParams = '';
|
||||||
window.PalmSystem &&
|
|
||||||
window.PalmSystem.launchParams
|
|
||||||
) {
|
|
||||||
window.PalmSystem.launchParams = "";
|
|
||||||
} else {
|
} else {
|
||||||
localLaunchParams = {};
|
localLaunchParams = {};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const readLocalStorage = (key, defaultValue) => {
|
export const readLocalStorage = (key, defaultValue) => {
|
||||||
const value = typeof window === "object" && window.localStorage.getItem(key);
|
const value = typeof window === 'object' && window.localStorage.getItem(key);
|
||||||
if (!value && defaultValue !== undefined) {
|
if (!value && defaultValue !== undefined) {
|
||||||
return defaultValue;
|
return defaultValue;
|
||||||
}
|
}
|
||||||
return value === "undefined" ? null : JSON.parse(value);
|
return value === 'undefined' ? null : JSON.parse(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const writeLocalStorage = (key, value) => {
|
export const writeLocalStorage = (key, value) => {
|
||||||
if (typeof window === "object") {
|
if (typeof window === 'object') {
|
||||||
window.localStorage.setItem(key, JSON.stringify(value));
|
window.localStorage.setItem(key, JSON.stringify(value));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -199,34 +182,29 @@ export const writeLocalStorage = (key, value) => {
|
|||||||
export const convertToTimeFormat = (timeString, isIncludeDate = false) => {
|
export const convertToTimeFormat = (timeString, isIncludeDate = false) => {
|
||||||
const date = new Date(timeString);
|
const date = new Date(timeString);
|
||||||
|
|
||||||
let options = { hour: "numeric", minute: "2-digit", hour12: true };
|
let options = { hour: 'numeric', minute: '2-digit', hour12: true };
|
||||||
let pattern = " ";
|
let pattern = ' ';
|
||||||
|
|
||||||
if (isIncludeDate) {
|
if (isIncludeDate) {
|
||||||
options = {
|
options = {
|
||||||
...options,
|
...options,
|
||||||
month: "2-digit",
|
month: '2-digit',
|
||||||
day: "2-digit",
|
day: '2-digit',
|
||||||
year: "numeric",
|
year: 'numeric',
|
||||||
hour: "2-digit",
|
hour: '2-digit',
|
||||||
};
|
};
|
||||||
pattern = ",";
|
pattern = ',';
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Intl.DateTimeFormat("en-US", options)
|
return new Intl.DateTimeFormat('en-US', options).format(date).replace(pattern, '');
|
||||||
.format(date)
|
|
||||||
.replace(pattern, "");
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getTranslate3dValueByDirection = (
|
export const getTranslate3dValueByDirection = (element, isHorizontal = true) => {
|
||||||
element,
|
|
||||||
isHorizontal = true
|
|
||||||
) => {
|
|
||||||
try {
|
try {
|
||||||
const transformStyle = window.getComputedStyle(element).transform;
|
const transformStyle = window.getComputedStyle(element).transform;
|
||||||
|
|
||||||
if (!transformStyle || transformStyle === "none") {
|
if (!transformStyle || transformStyle === 'none') {
|
||||||
throw new Error("transfrom style not found");
|
throw new Error('transfrom style not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
const transformMatrix = transformStyle.match(/^matrix\((.+)\)$/);
|
const transformMatrix = transformStyle.match(/^matrix\((.+)\)$/);
|
||||||
@@ -234,7 +212,7 @@ export const getTranslate3dValueByDirection = (
|
|||||||
let index, value;
|
let index, value;
|
||||||
|
|
||||||
if (transformMatrix) {
|
if (transformMatrix) {
|
||||||
const matrixValues = transformMatrix[1].split(", ");
|
const matrixValues = transformMatrix[1].split(', ');
|
||||||
|
|
||||||
index = isHorizontal ? 4 : 5;
|
index = isHorizontal ? 4 : 5;
|
||||||
|
|
||||||
@@ -248,28 +226,22 @@ export const getTranslate3dValueByDirection = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const formatGMTString = (date) => {
|
export const formatGMTString = (date) => {
|
||||||
let string = date.toISOString().replace(/T/, " ").replace(/\..+/, "");
|
let string = date.toISOString().replace(/T/, ' ').replace(/\..+/, '');
|
||||||
|
|
||||||
return string;
|
return string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getSpottableDescendants = (containerId) => {
|
export const getSpottableDescendants = (containerId) => {
|
||||||
let container = document.querySelector(
|
let container = document.querySelector(`[data-spotlight-id="${containerId}"]`);
|
||||||
`[data-spotlight-id="${containerId}"]`
|
|
||||||
);
|
|
||||||
if (container) {
|
if (container) {
|
||||||
return container.querySelectorAll('[class*="spottable"]');
|
return container.querySelectorAll('[class*="spottable"]');
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isElementInContainer = (
|
export const isElementInContainer = (element, container, fullyVisible = true) => {
|
||||||
element,
|
|
||||||
container,
|
|
||||||
fullyVisible = true
|
|
||||||
) => {
|
|
||||||
// 요소와 컨테이너의 사각형 정보 가져오기
|
// 요소와 컨테이너의 사각형 정보 가져오기
|
||||||
if (typeof window === "object") {
|
if (typeof window === 'object') {
|
||||||
const elementRect = getBoundingClientRect(element);
|
const elementRect = getBoundingClientRect(element);
|
||||||
const containerRect = container
|
const containerRect = container
|
||||||
? getBoundingClientRect(container)
|
? getBoundingClientRect(container)
|
||||||
@@ -334,9 +306,7 @@ export const getRectDiff = (element1, element2) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const getFormattingCardNo = (cardNumber) => {
|
export const getFormattingCardNo = (cardNumber) => {
|
||||||
return `${"*".repeat(12)}${cardNumber.slice(-4)}`
|
return `${'*'.repeat(12)}${cardNumber.slice(-4)}`.replace(/(.{4})/g, '$1-').slice(0, -1);
|
||||||
.replace(/(.{4})/g, "$1-")
|
|
||||||
.slice(0, -1);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getQRCodeUrl = ({
|
export const getQRCodeUrl = ({
|
||||||
@@ -345,41 +315,41 @@ export const getQRCodeUrl = ({
|
|||||||
index,
|
index,
|
||||||
patnrId,
|
patnrId,
|
||||||
prdtId,
|
prdtId,
|
||||||
dirPurcSelYn = "Y",
|
dirPurcSelYn = 'Y',
|
||||||
prdtData,
|
prdtData,
|
||||||
qrType,
|
qrType,
|
||||||
liveFlag = "Y",
|
liveFlag = 'Y',
|
||||||
entryMenu,
|
entryMenu,
|
||||||
nowMenu,
|
nowMenu,
|
||||||
}) => {
|
}) => {
|
||||||
if (!serverHOST) {
|
if (!serverHOST) {
|
||||||
console.error("getQRCodeUrl: Not Supported, Host is missing");
|
console.error('getQRCodeUrl: Not Supported, Host is missing');
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
let sdpURL = serverHOST.split(".")[0];
|
let sdpURL = serverHOST.split('.')[0];
|
||||||
let countryCode = "";
|
let countryCode = '';
|
||||||
|
|
||||||
if (sdpURL.indexOf("-") > 0) {
|
if (sdpURL.indexOf('-') > 0) {
|
||||||
countryCode = sdpURL.split("-")[1];
|
countryCode = sdpURL.split('-')[1];
|
||||||
} else {
|
} else {
|
||||||
countryCode = sdpURL;
|
countryCode = sdpURL;
|
||||||
}
|
}
|
||||||
|
|
||||||
sdpURL = sdpURL.toLowerCase();
|
sdpURL = sdpURL.toLowerCase();
|
||||||
|
|
||||||
if (serverType !== "system") {
|
if (serverType !== 'system') {
|
||||||
sdpURL = serverType;
|
sdpURL = serverType;
|
||||||
}
|
}
|
||||||
|
|
||||||
let baseUrl = "";
|
let baseUrl = '';
|
||||||
|
|
||||||
if (sdpURL.indexOf("qt2") >= 0) {
|
if (sdpURL.indexOf('qt2') >= 0) {
|
||||||
baseUrl = "https://qt2-m.shoptime.lgappstv.com/";
|
baseUrl = 'https://qt2-m.shoptime.lgappstv.com/';
|
||||||
} else if (sdpURL.indexOf("qt") >= 0) {
|
} else if (sdpURL.indexOf('qt') >= 0) {
|
||||||
baseUrl = "https://qt-m.shoptime.lgappstv.com/";
|
baseUrl = 'https://qt-m.shoptime.lgappstv.com/';
|
||||||
} else {
|
} else {
|
||||||
baseUrl = "https://m.shoptime.lgappstv.com/";
|
baseUrl = 'https://m.shoptime.lgappstv.com/';
|
||||||
}
|
}
|
||||||
const prdtDataStr = JSON.stringify(prdtData);
|
const prdtDataStr = JSON.stringify(prdtData);
|
||||||
const prdtDataBase64 = btoa(prdtDataStr);
|
const prdtDataBase64 = btoa(prdtDataStr);
|
||||||
@@ -402,25 +372,25 @@ export const getQRCodeUrl = ({
|
|||||||
|
|
||||||
// ex: JANUARY 01, 2024
|
// ex: JANUARY 01, 2024
|
||||||
export const getFormattingDate = (dateString) => {
|
export const getFormattingDate = (dateString) => {
|
||||||
const date = new Date(dateString.replace(" ", "T"));
|
const date = new Date(dateString.replace(' ', 'T'));
|
||||||
|
|
||||||
const monthNames = [
|
const monthNames = [
|
||||||
$L("JANUARY"),
|
$L('JANUARY'),
|
||||||
$L("FEBRUARY"),
|
$L('FEBRUARY'),
|
||||||
$L("MARCH"),
|
$L('MARCH'),
|
||||||
$L("APRIL"),
|
$L('APRIL'),
|
||||||
$L("MAY"),
|
$L('MAY'),
|
||||||
$L("JUNE"),
|
$L('JUNE'),
|
||||||
$L("JULY"),
|
$L('JULY'),
|
||||||
$L("AUGUST"),
|
$L('AUGUST'),
|
||||||
$L("SEPTEMBER"),
|
$L('SEPTEMBER'),
|
||||||
$L("OCTOBER"),
|
$L('OCTOBER'),
|
||||||
$L("NOVEMBER"),
|
$L('NOVEMBER'),
|
||||||
$L("DECEMBER"),
|
$L('DECEMBER'),
|
||||||
];
|
];
|
||||||
|
|
||||||
const month = monthNames[date.getMonth()];
|
const month = monthNames[date.getMonth()];
|
||||||
const day = date.getDate().toString().padStart(2, "0");
|
const day = date.getDate().toString().padStart(2, '0');
|
||||||
const year = date.getFullYear();
|
const year = date.getFullYear();
|
||||||
|
|
||||||
return `${month} ${day}, ${year}`;
|
return `${month} ${day}, ${year}`;
|
||||||
@@ -437,14 +407,14 @@ export const removeSpecificTags = (html) => {
|
|||||||
|
|
||||||
let sanitizedHtml = html;
|
let sanitizedHtml = html;
|
||||||
tagPatterns.forEach((pattern) => {
|
tagPatterns.forEach((pattern) => {
|
||||||
sanitizedHtml = sanitizedHtml.replace(pattern, "");
|
sanitizedHtml = sanitizedHtml.replace(pattern, '');
|
||||||
});
|
});
|
||||||
|
|
||||||
return sanitizedHtml;
|
return sanitizedHtml;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const encryptPhoneNumber = (phoneNumber) => {
|
export const encryptPhoneNumber = (phoneNumber) => {
|
||||||
if (typeof window === "object") {
|
if (typeof window === 'object') {
|
||||||
return window.CryptoJS.AES.encrypt(phoneNumber, SECRET_KEY).toString();
|
return window.CryptoJS.AES.encrypt(phoneNumber, SECRET_KEY).toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -452,7 +422,7 @@ export const encryptPhoneNumber = (phoneNumber) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const decryptPhoneNumber = (encryptedPhoneNumber) => {
|
export const decryptPhoneNumber = (encryptedPhoneNumber) => {
|
||||||
if (typeof window === "object") {
|
if (typeof window === 'object') {
|
||||||
const bytes = window.CryptoJS.AES.decrypt(encryptedPhoneNumber, SECRET_KEY);
|
const bytes = window.CryptoJS.AES.decrypt(encryptedPhoneNumber, SECRET_KEY);
|
||||||
return bytes.toString(window.CryptoJS.enc.Utf8);
|
return bytes.toString(window.CryptoJS.enc.Utf8);
|
||||||
}
|
}
|
||||||
@@ -461,63 +431,54 @@ export const decryptPhoneNumber = (encryptedPhoneNumber) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const formatLocalDateTime = (date) => {
|
export const formatLocalDateTime = (date) => {
|
||||||
const isDate = (obj) =>
|
const isDate = (obj) => Object.prototype.toString.call(obj) === '[object Date]';
|
||||||
Object.prototype.toString.call(obj) === "[object Date]";
|
|
||||||
|
|
||||||
if (typeof window === "object" && isDate(date)) {
|
if (typeof window === 'object' && isDate(date)) {
|
||||||
const year = date.getFullYear();
|
const year = date.getFullYear();
|
||||||
const month = String(date.getMonth() + 1).padStart(2, "0");
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||||
const day = String(date.getDate()).padStart(2, "0");
|
const day = String(date.getDate()).padStart(2, '0');
|
||||||
const hours = String(date.getHours()).padStart(2, "0");
|
const hours = String(date.getHours()).padStart(2, '0');
|
||||||
const minutes = String(date.getMinutes()).padStart(2, "0");
|
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||||||
const seconds = String(date.getSeconds()).padStart(2, "0");
|
const seconds = String(date.getSeconds()).padStart(2, '0');
|
||||||
|
|
||||||
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return "";
|
return '';
|
||||||
};
|
};
|
||||||
|
|
||||||
export const parseLocalizedNumber = (numberString, countryCode) => {
|
export const parseLocalizedNumber = (numberString, countryCode) => {
|
||||||
// 유럽식: 1.499,00 -> 1499.00
|
// 유럽식: 1.499,00 -> 1499.00
|
||||||
if (countryCode === "DE") {
|
if (countryCode === 'DE') {
|
||||||
return parseFloat(numberString.replace(/\./g, "").replace(",", "."));
|
return parseFloat(numberString.replace(/\./g, '').replace(',', '.'));
|
||||||
}
|
}
|
||||||
// 미국식: 1,499.00 -> 1499.00
|
// 미국식: 1,499.00 -> 1499.00
|
||||||
if (countryCode === "US" || countryCode === "GB") {
|
if (countryCode === 'US' || countryCode === 'GB') {
|
||||||
return parseFloat(numberString.replace(/[^0-9.-]+/g, ""));
|
return parseFloat(numberString.replace(/[^0-9.-]+/g, ''));
|
||||||
}
|
}
|
||||||
// 러시아식: 1 499,00 -> 1499.00
|
// 러시아식: 1 499,00 -> 1499.00
|
||||||
if (countryCode === "RU") {
|
if (countryCode === 'RU') {
|
||||||
return parseFloat(numberString.replace(/\s/g, "").replace(",", "."));
|
return parseFloat(numberString.replace(/\s/g, '').replace(',', '.'));
|
||||||
}
|
}
|
||||||
|
|
||||||
return parseFloat(numberString);
|
return parseFloat(numberString);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const formatCurrencyValue = (
|
export const formatCurrencyValue = (value, currSign, currSignLoc, isDiscount = false) => {
|
||||||
value,
|
if (value === '-' || value === 0) return '-';
|
||||||
currSign,
|
|
||||||
currSignLoc,
|
|
||||||
isDiscount = false
|
|
||||||
) => {
|
|
||||||
if (value === "-" || value === 0) return "-";
|
|
||||||
|
|
||||||
const numValue = parseFloat(value);
|
const numValue = parseFloat(value);
|
||||||
if (isNaN(numValue)) return "-";
|
if (isNaN(numValue)) return '-';
|
||||||
|
|
||||||
const sign = isDiscount && numValue > 0 ? "- " : "";
|
const sign = isDiscount && numValue > 0 ? '- ' : '';
|
||||||
const formattedValue = parseFloat(numValue.toFixed(2)).toLocaleString(
|
const formattedValue = parseFloat(numValue.toFixed(2)).toLocaleString('en-US', {
|
||||||
"en-US",
|
minimumFractionDigits: 2,
|
||||||
{
|
maximumFractionDigits: 2,
|
||||||
minimumFractionDigits: 2,
|
});
|
||||||
maximumFractionDigits: 2,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!currSign || !currSignLoc) return `${sign}${formattedValue}`;
|
if (!currSign || !currSignLoc) return `${sign}${formattedValue}`;
|
||||||
|
|
||||||
return currSignLoc === "L"
|
return currSignLoc === 'L'
|
||||||
? `${sign}${currSign} ${formattedValue}`
|
? `${sign}${currSign} ${formattedValue}`
|
||||||
: `${sign}${formattedValue} ${currSign}`;
|
: `${sign}${formattedValue} ${currSign}`;
|
||||||
};
|
};
|
||||||
@@ -536,44 +497,31 @@ export const getTimeDifferenceByMilliseconds = (
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const convertedStartTime = new Date(startTimeString.replace(/ /, "T") + "Z");
|
const convertedStartTime = new Date(startTimeString.replace(/ /, 'T') + 'Z');
|
||||||
const convertedEndTime = new Date(endTimeString.replace(/ /, "T") + "Z");
|
const convertedEndTime = new Date(endTimeString.replace(/ /, 'T') + 'Z');
|
||||||
const timeDifference = convertedEndTime - convertedStartTime;
|
const timeDifference = convertedEndTime - convertedStartTime;
|
||||||
|
|
||||||
return timeDifference > threshold;
|
return timeDifference > threshold;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getErrorMessage = (
|
export const getErrorMessage = (errorCode, retMsg, retDetailCode, returnBindStrings) => {
|
||||||
errorCode,
|
const foundGroup = ERROR_MESSAGES_GROUPS.find((group) => group.codes.includes(Number(errorCode)));
|
||||||
retMsg,
|
|
||||||
retDetailCode,
|
|
||||||
returnBindStrings
|
|
||||||
) => {
|
|
||||||
const group = ERROR_MESSAGES_GROUPS.find((group) =>
|
|
||||||
group.codes.includes(Number(errorCode))
|
|
||||||
);
|
|
||||||
|
|
||||||
const errorPrefix = errorCode
|
const errorPrefix = errorCode
|
||||||
? retDetailCode
|
? retDetailCode
|
||||||
? `[${errorCode}-${retDetailCode}] `
|
? `[${errorCode}-${retDetailCode}] `
|
||||||
: `[${errorCode}] `
|
: `[${errorCode}] `
|
||||||
: "";
|
: '';
|
||||||
|
|
||||||
if (group) {
|
if (foundGroup) {
|
||||||
if (
|
if (errorCode === 1120 && returnBindStrings && typeof returnBindStrings === 'object') {
|
||||||
errorCode === 1120 &&
|
return `${errorPrefix} ${foundGroup.message} (ID: ${returnBindStrings.join(', ')})`;
|
||||||
returnBindStrings &&
|
|
||||||
typeof returnBindStrings === "object"
|
|
||||||
) {
|
|
||||||
return `${errorPrefix} ${group.message} (ID: ${returnBindStrings.join(
|
|
||||||
", "
|
|
||||||
)})`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return errorPrefix + group.message;
|
return errorPrefix + foundGroup.message;
|
||||||
} else if (retMsg) {
|
} else if (retMsg) {
|
||||||
return errorPrefix + retMsg;
|
return errorPrefix + retMsg;
|
||||||
} else {
|
} else {
|
||||||
return errorPrefix + "An unknown error occurred. Please try again later.";
|
return errorPrefix + 'An unknown error occurred. Please try again later.';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ const promisify = (a, ...args) => {
|
|||||||
const cond = fp.cond([
|
const cond = fp.cond([
|
||||||
[fp.isFunction, () => fnPromisify(a, ...args)],
|
[fp.isFunction, () => fnPromisify(a, ...args)],
|
||||||
[isPromise, fp.identity],
|
[isPromise, fp.identity],
|
||||||
[fp.T, (a) => Promise.resolve(a)],
|
[fp.T, (val) => Promise.resolve(val)],
|
||||||
]);
|
]);
|
||||||
const result = cond(a);
|
const result = cond(a);
|
||||||
|
|
||||||
@@ -75,7 +75,7 @@ const toBool = (a) =>
|
|||||||
fp.cond([
|
fp.cond([
|
||||||
[fp.equals('true'), fp.T],
|
[fp.equals('true'), fp.T],
|
||||||
[fp.equals('false'), fp.F],
|
[fp.equals('false'), fp.F],
|
||||||
[fp.T, (a) => !!a],
|
[fp.T, (val) => !!val],
|
||||||
])(a);
|
])(a);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -83,9 +83,9 @@ const toBool = (a) =>
|
|||||||
* (isTrue가 true면 t(실행)반환, false면 f(실행)반환)
|
* (isTrue가 true면 t(실행)반환, false면 f(실행)반환)
|
||||||
*/
|
*/
|
||||||
const ternary = fp.curry((evaluator, trueHandler, falseHandler, a) => {
|
const ternary = fp.curry((evaluator, trueHandler, falseHandler, a) => {
|
||||||
const executor = fp.curry((t, f, a, isTrue) => {
|
const executor = fp.curry((t, f, val, isTrue) => {
|
||||||
const result = isTrue ? (fp.isFunction(t) ? t(a) : t) : fp.isFunction(f) ? f(a) : f;
|
const evalResult = isTrue ? (fp.isFunction(t) ? t(val) : t) : fp.isFunction(f) ? f(val) : f;
|
||||||
return result;
|
return evalResult;
|
||||||
});
|
});
|
||||||
const getEvaluator = (fn) => (fp.isNil(fn) ? fp.identity : fn);
|
const getEvaluator = (fn) => (fp.isNil(fn) ? fp.identity : fn);
|
||||||
const result = executor(trueHandler, falseHandler, a, getEvaluator(evaluator)(a));
|
const result = executor(trueHandler, falseHandler, a, getEvaluator(evaluator)(a));
|
||||||
@@ -110,7 +110,7 @@ const pascalCase = fp.pipe(fp.camelCase, fp.upperFirst);
|
|||||||
const mapAsync = fp.curry(async (asyncMapper, arr) => {
|
const mapAsync = fp.curry(async (asyncMapper, arr) => {
|
||||||
const composer = fp.pipe(
|
const composer = fp.pipe(
|
||||||
fp.flatMapDeep(fp.pipe(asyncMapper, promisify)),
|
fp.flatMapDeep(fp.pipe(asyncMapper, promisify)),
|
||||||
async (a) => await Promise.all(a),
|
async (a) => await Promise.all(a)
|
||||||
);
|
);
|
||||||
const result = await composer(arr);
|
const result = await composer(arr);
|
||||||
|
|
||||||
@@ -124,7 +124,7 @@ const mapAsync = fp.curry(async (asyncMapper, arr) => {
|
|||||||
const filterAsync = fp.curry(async (asyncFilter, arr) => {
|
const filterAsync = fp.curry(async (asyncFilter, arr) => {
|
||||||
const composer = fp.pipe(
|
const composer = fp.pipe(
|
||||||
mapAsync(async (item) => ((await asyncFilter(item)) ? item : false)),
|
mapAsync(async (item) => ((await asyncFilter(item)) ? item : false)),
|
||||||
then(fp.filter(fp.pipe(fp.equals(false), not))),
|
then(fp.filter(fp.pipe(fp.equals(false), not)))
|
||||||
);
|
);
|
||||||
const result = await composer(arr);
|
const result = await composer(arr);
|
||||||
|
|
||||||
@@ -139,7 +139,7 @@ const findAsync = fp.curry(async (asyncFn, arr) => {
|
|||||||
mapAsync(asyncFn),
|
mapAsync(asyncFn),
|
||||||
then(fp.indexOf(true)),
|
then(fp.indexOf(true)),
|
||||||
then((idx) => fp.get(`[${idx}]`, arr)),
|
then((idx) => fp.get(`[${idx}]`, arr)),
|
||||||
otherwise(fp.always(undefined)),
|
otherwise(fp.always(undefined))
|
||||||
);
|
);
|
||||||
const result = await composer(arr);
|
const result = await composer(arr);
|
||||||
|
|
||||||
@@ -182,8 +182,8 @@ const forEachAsync = fp.curry(async (cb, collection) => {
|
|||||||
const key = fp.curry((a, v) => {
|
const key = fp.curry((a, v) => {
|
||||||
const composer = fp.pipe(
|
const composer = fp.pipe(
|
||||||
fp.entries,
|
fp.entries,
|
||||||
fp.find(([k, val]) => fp.equals(v, val)),
|
fp.find(([, val]) => fp.equals(v, val)),
|
||||||
fp.head,
|
fp.head
|
||||||
);
|
);
|
||||||
const result = composer(a);
|
const result = composer(a);
|
||||||
return result;
|
return result;
|
||||||
@@ -198,6 +198,14 @@ const isJson = (a) => {
|
|||||||
return fp.isString(a) && !composer(() => JSON.parse(a));
|
return fp.isString(a) && !composer(() => JSON.parse(a));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reference type 여부 체크 함수 (전방 선언)
|
||||||
|
*/
|
||||||
|
const isRef = fp.pipe(
|
||||||
|
(a) => fp.isNil(a) || fp.isBoolean(a) || fp.isNumber(a) || fp.isString(a),
|
||||||
|
not
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* shallow freeze 보완
|
* shallow freeze 보완
|
||||||
* (대상 object의 refence 타입의 properties까지 object.freeze 처리)
|
* (대상 object의 refence 타입의 properties까지 object.freeze 처리)
|
||||||
@@ -212,31 +220,31 @@ const deepFreeze = (obj) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const transformObjectKey = fp.curry((transformFn, dest) => {
|
const transformObjectKey = fp.curry((transformFn, dest) => {
|
||||||
const convertRecursively = (dest) => {
|
const convertRecursively = (source) => {
|
||||||
const convertTo = (o) => {
|
const convertTo = (o) => {
|
||||||
const composer = fp.pipe(
|
const composer = fp.pipe(
|
||||||
fp.entries,
|
fp.entries,
|
||||||
fp.reduce((acc, [k, v]) => {
|
fp.reduce((acc, [k, val]) => {
|
||||||
const cond = fp.cond([
|
const cond = fp.cond([
|
||||||
[fp.isPlainObject, convertTo],
|
[fp.isPlainObject, convertTo],
|
||||||
[fp.isArray, (v) => v.map(cond)],
|
[fp.isArray, (arr) => arr.map(cond)],
|
||||||
[fp.T, (a) => a],
|
[fp.T, (item) => item],
|
||||||
]);
|
]);
|
||||||
const transformedKey = transformFn(k);
|
const transformedKey = transformFn(k);
|
||||||
if (!fp.has(transformedKey, acc)) {
|
if (!fp.has(transformedKey, acc)) {
|
||||||
acc[transformedKey] = cond(v);
|
acc[transformedKey] = cond(val);
|
||||||
return acc;
|
return acc;
|
||||||
} else {
|
} else {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`${transformedKey} already exist. duplicated property name is not supported.`,
|
`${transformedKey} already exist. duplicated property name is not supported.`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}, {}),
|
}, {})
|
||||||
);
|
);
|
||||||
const result = composer(o);
|
const result = composer(o);
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
const result = convertTo(dest);
|
const result = convertTo(source);
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -356,7 +364,7 @@ const append = fp.concat;
|
|||||||
* array 인자의 (index상)앞쪽에 value인자를 추가
|
* array 인자의 (index상)앞쪽에 value인자를 추가
|
||||||
*/
|
*/
|
||||||
const prepend = fp.curry((array, value) =>
|
const prepend = fp.curry((array, value) =>
|
||||||
fp.isArray(value) ? fp.concat(value, array) : fp.concat([value], array),
|
fp.isArray(value) ? fp.concat(value, array) : fp.concat([value], array)
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -381,9 +389,9 @@ const reduceWithKey = fp.curry((f, acc, a) => fp.reduce.convert({ cap: false })(
|
|||||||
const isVal = (a) => fp.isNil(a) || fp.isBoolean(a) || fp.isNumber(a) || fp.isString(a);
|
const isVal = (a) => fp.isNil(a) || fp.isBoolean(a) || fp.isNumber(a) || fp.isString(a);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Array, Object, Function
|
* Array, Object, Function (이미 위에서 선언됨)
|
||||||
*/
|
*/
|
||||||
const isRef = fp.pipe(isVal, not);
|
// const isRef = fp.pipe(isVal, not);
|
||||||
|
|
||||||
const isFalsy = (a) => {
|
const isFalsy = (a) => {
|
||||||
return fp.isNil(a) || fp.some(fp.equals(a), [0, -0, NaN, false, '']);
|
return fp.isNil(a) || fp.some(fp.equals(a), [0, -0, NaN, false, '']);
|
||||||
@@ -397,7 +405,7 @@ const isTruthy = (a) => !isFalsy(a);
|
|||||||
* fp.getOr의 반환값이 null인 경우, 기본값 반환되게 수정한 버전
|
* fp.getOr의 반환값이 null인 경우, 기본값 반환되게 수정한 버전
|
||||||
* circular dependency 때문에 closure로 작성
|
* circular dependency 때문에 closure로 작성
|
||||||
*/
|
*/
|
||||||
const getOr = (({ curry, getOr }) => {
|
const getOr = (({ curry }) => {
|
||||||
const _getOr = curry((defaultValue, path, target) => {
|
const _getOr = curry((defaultValue, path, target) => {
|
||||||
const val = fp.get(path, target);
|
const val = fp.get(path, target);
|
||||||
return fp.isNil(val) ? defaultValue : val;
|
return fp.isNil(val) ? defaultValue : val;
|
||||||
@@ -413,9 +421,7 @@ const getOr = (({ curry, getOr }) => {
|
|||||||
* @param {Function} fn 실행할 함수
|
* @param {Function} fn 실행할 함수
|
||||||
* @param {*} value 대상 값
|
* @param {*} value 대상 값
|
||||||
*/
|
*/
|
||||||
const when = fp.curry((predicate, fn, value) =>
|
const when = fp.curry((predicate, fn, value) => (predicate(value) ? fn(value) : value));
|
||||||
predicate(value) ? fn(value) : value
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 조건이 거짓일 때만 함수를 실행, 참이면 원래 값 반환
|
* 조건이 거짓일 때만 함수를 실행, 참이면 원래 값 반환
|
||||||
@@ -423,9 +429,7 @@ const when = fp.curry((predicate, fn, value) =>
|
|||||||
* @param {Function} fn 실행할 함수
|
* @param {Function} fn 실행할 함수
|
||||||
* @param {*} value 대상 값
|
* @param {*} value 대상 값
|
||||||
*/
|
*/
|
||||||
const unless = fp.curry((predicate, fn, value) =>
|
const unless = fp.curry((predicate, fn, value) => (!predicate(value) ? fn(value) : value));
|
||||||
!predicate(value) ? fn(value) : value
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* if-else 조건부 실행
|
* if-else 조건부 실행
|
||||||
@@ -495,7 +499,9 @@ const safeGet = fp.curry((path, defaultValue, obj) => {
|
|||||||
* @param {Array} array 대상 배열
|
* @param {Array} array 대상 배열
|
||||||
*/
|
*/
|
||||||
const mapWhen = fp.curry((predicate, fn, array) =>
|
const mapWhen = fp.curry((predicate, fn, array) =>
|
||||||
array.map(function(item) { return predicate(item) ? fn(item) : item; })
|
array.map(function (item) {
|
||||||
|
return predicate(item) ? fn(item) : item;
|
||||||
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -514,12 +520,16 @@ const filterWhen = fp.curry((condition, predicate, array) =>
|
|||||||
* @param {Object} obj 대상 객체
|
* @param {Object} obj 대상 객체
|
||||||
*/
|
*/
|
||||||
const renameKeys = fp.curry((keyMap, obj) =>
|
const renameKeys = fp.curry((keyMap, obj) =>
|
||||||
fp.reduce((acc, [oldKey, newKey]) => {
|
fp.reduce(
|
||||||
if (fp.has(oldKey, obj)) {
|
(acc, [oldKey, newKey]) => {
|
||||||
acc[newKey] = obj[oldKey];
|
if (fp.has(oldKey, obj)) {
|
||||||
}
|
acc[newKey] = obj[oldKey];
|
||||||
return acc;
|
}
|
||||||
}, {}, fp.toPairs(keyMap))
|
return acc;
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
fp.toPairs(keyMap)
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -545,7 +555,11 @@ const applyTo = fp.curry((value, fn) => fn(value));
|
|||||||
* @param {Array} fns 함수 배열
|
* @param {Array} fns 함수 배열
|
||||||
* @param {*} value 대상 값
|
* @param {*} value 대상 값
|
||||||
*/
|
*/
|
||||||
const juxt = fp.curry((fns, value) => fns.map(function(fn) { return fn(value); }));
|
const juxt = fp.curry((fns, value) =>
|
||||||
|
fns.map(function (fn) {
|
||||||
|
return fn(value);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 여러 함수의 결과를 converge 함수로 조합
|
* 여러 함수의 결과를 converge 함수로 조합
|
||||||
@@ -555,7 +569,9 @@ const juxt = fp.curry((fns, value) => fns.map(function(fn) { return fn(value); }
|
|||||||
*/
|
*/
|
||||||
const converge = fp.curry((convergeFn, fns, value) => {
|
const converge = fp.curry((convergeFn, fns, value) => {
|
||||||
// Chromium 68 호환성: spread 연산자 대신 apply 사용
|
// Chromium 68 호환성: spread 연산자 대신 apply 사용
|
||||||
const results = fns.map(function(fn) { return fn(value); });
|
const results = fns.map(function (fn) {
|
||||||
|
return fn(value);
|
||||||
|
});
|
||||||
return convergeFn.apply(null, results);
|
return convergeFn.apply(null, results);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -572,7 +588,7 @@ const trimToUndefined = (str) => {
|
|||||||
* 첫 글자 대문자, 나머지 소문자
|
* 첫 글자 대문자, 나머지 소문자
|
||||||
* @param {string} str 대상 문자열
|
* @param {string} str 대상 문자열
|
||||||
*/
|
*/
|
||||||
const capitalize = function(str) {
|
const capitalize = function (str) {
|
||||||
if (!str || typeof str !== 'string') return str;
|
if (!str || typeof str !== 'string') return str;
|
||||||
return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
|
return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
|
||||||
};
|
};
|
||||||
@@ -583,9 +599,7 @@ const capitalize = function(str) {
|
|||||||
* @param {number} max 최댓값
|
* @param {number} max 최댓값
|
||||||
* @param {number} value 대상 값
|
* @param {number} value 대상 값
|
||||||
*/
|
*/
|
||||||
const clampTo = fp.curry((min, max, value) =>
|
const clampTo = fp.curry((min, max, value) => Math.min(Math.max(value, min), max));
|
||||||
Math.min(Math.max(value, min), max)
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 값이 min과 max 사이에 있는지 확인
|
* 값이 min과 max 사이에 있는지 확인
|
||||||
@@ -593,27 +607,21 @@ const clampTo = fp.curry((min, max, value) =>
|
|||||||
* @param {number} max 최댓값
|
* @param {number} max 최댓값
|
||||||
* @param {number} value 대상 값
|
* @param {number} value 대상 값
|
||||||
*/
|
*/
|
||||||
const between = fp.curry((min, max, value) =>
|
const between = fp.curry((min, max, value) => value >= min && value <= max);
|
||||||
value >= min && value <= max
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* null/undefined일 때 기본값 반환
|
* null/undefined일 때 기본값 반환
|
||||||
* @param {*} defaultValue 기본값
|
* @param {*} defaultValue 기본값
|
||||||
* @param {*} value 대상 값
|
* @param {*} value 대상 값
|
||||||
*/
|
*/
|
||||||
const defaultTo = fp.curry((defaultValue, value) =>
|
const defaultTo = fp.curry((defaultValue, value) => (value == null ? defaultValue : value));
|
||||||
value == null ? defaultValue : value
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Elvis 연산자 구현 (null safe 함수 적용)
|
* Elvis 연산자 구현 (null safe 함수 적용)
|
||||||
* @param {Function} fn 적용할 함수
|
* @param {Function} fn 적용할 함수
|
||||||
* @param {*} value 대상 값
|
* @param {*} value 대상 값
|
||||||
*/
|
*/
|
||||||
const elvis = fp.curry((fn, value) =>
|
const elvis = fp.curry((fn, value) => (value == null ? undefined : fn(value)));
|
||||||
value == null ? undefined : fn(value)
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 배열을 조건에 따라 두 그룹으로 분할
|
* 배열을 조건에 따라 두 그룹으로 분할
|
||||||
@@ -622,7 +630,9 @@ const elvis = fp.curry((fn, value) =>
|
|||||||
*/
|
*/
|
||||||
const partition = fp.curry((predicate, array) => [
|
const partition = fp.curry((predicate, array) => [
|
||||||
array.filter(predicate),
|
array.filter(predicate),
|
||||||
array.filter(function(item) { return !predicate(item); })
|
array.filter(function (item) {
|
||||||
|
return !predicate(item);
|
||||||
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -647,7 +657,7 @@ const lazy = (fn) => {
|
|||||||
let cached = false;
|
let cached = false;
|
||||||
let result;
|
let result;
|
||||||
// Chromium 68 호환성: spread 연산자 대신 arguments 사용
|
// Chromium 68 호환성: spread 연산자 대신 arguments 사용
|
||||||
return function() {
|
return function () {
|
||||||
if (!cached) {
|
if (!cached) {
|
||||||
result = fn.apply(this, arguments);
|
result = fn.apply(this, arguments);
|
||||||
cached = true;
|
cached = true;
|
||||||
@@ -756,4 +766,4 @@ export default {
|
|||||||
partition,
|
partition,
|
||||||
times,
|
times,
|
||||||
lazy,
|
lazy,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
// spotlight-utils.js
|
// spotlight-utils.js
|
||||||
import {getTargetByContainer} from '@enact/spotlight/src/target';
|
import { getTargetByContainer } from '@enact/spotlight/src/target';
|
||||||
import {getTargetBySelector} from '@enact/spotlight/src/target';
|
import { getTargetBySelector } from '@enact/spotlight/src/target';
|
||||||
import {getContainerConfig} from '@enact/spotlight/src/container';
|
import { getContainerConfig } from '@enact/spotlight/src/container';
|
||||||
import {isContainer, getContainerId} from '@enact/spotlight/src/container';
|
import { isContainer, getContainerId } from '@enact/spotlight/src/container';
|
||||||
import {getContainersForNode} from '@enact/spotlight/src/container';
|
import { getContainersForNode } from '@enact/spotlight/src/container';
|
||||||
import {isNavigable} from '@enact/spotlight/src/container';
|
import { isNavigable } from '@enact/spotlight/src/container';
|
||||||
import {setLastContainer} from '@enact/spotlight/src/container';
|
import { setLastContainer } from '@enact/spotlight/src/container';
|
||||||
import Spotlight from '@enact/spotlight';
|
import Spotlight from '@enact/spotlight';
|
||||||
|
|
||||||
// lodash 없이 last 함수 직접 구현
|
// lodash 없이 last 함수 직접 구현
|
||||||
@@ -15,13 +15,13 @@ const last = (array) => {
|
|||||||
|
|
||||||
// focusElement 함수는 spotlight 내부 함수이므로, 직접 구현하거나 spotlight의 focus를 사용
|
// focusElement 함수는 spotlight 내부 함수이므로, 직접 구현하거나 spotlight의 focus를 사용
|
||||||
const focusElement = (target, containerIds) => {
|
const focusElement = (target, containerIds) => {
|
||||||
console.log("focusElement called with:", target, containerIds);
|
console.log('focusElement called with:', target, containerIds);
|
||||||
if (target && typeof target.focus === 'function') {
|
if (target && typeof target.focus === 'function') {
|
||||||
try {
|
try {
|
||||||
target.focus();
|
target.focus();
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Focus failed:", e);
|
console.error('Focus failed:', e);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -39,40 +39,40 @@ const focusElement = (target, containerIds) => {
|
|||||||
* navigable target is found.
|
* navigable target is found.
|
||||||
*/
|
*/
|
||||||
export const getPredictedFocus = (elem) => {
|
export const getPredictedFocus = (elem) => {
|
||||||
let target = elem;
|
let target = elem;
|
||||||
|
|
||||||
if (!elem) {
|
if (!elem) {
|
||||||
target = getTargetByContainer();
|
target = getTargetByContainer();
|
||||||
} else if (typeof elem === 'string') {
|
} else if (typeof elem === 'string') {
|
||||||
if (getContainerConfig(elem)) {
|
if (getContainerConfig(elem)) {
|
||||||
// String is a container ID
|
// String is a container ID
|
||||||
target = getTargetByContainer(elem);
|
target = getTargetByContainer(elem);
|
||||||
} else if (/^[\w\d-]+$/.test(elem)) {
|
} else if (/^[\w\d-]+$/.test(elem)) {
|
||||||
// Support component IDs consisting of alphanumeric, dash, or underscore
|
// Support component IDs consisting of alphanumeric, dash, or underscore
|
||||||
target = getTargetBySelector(`[data-spotlight-id=${elem}]`);
|
target = getTargetBySelector(`[data-spotlight-id=${elem}]`);
|
||||||
} else {
|
} else {
|
||||||
// Treat as a CSS selector
|
// Treat as a CSS selector
|
||||||
target = getTargetBySelector(elem);
|
target = getTargetBySelector(elem);
|
||||||
}
|
|
||||||
} else if (isContainer(elem)) {
|
|
||||||
// elem is a container element
|
|
||||||
target = getTargetByContainer(getContainerId(elem));
|
|
||||||
}
|
}
|
||||||
|
} else if (isContainer(elem)) {
|
||||||
// Check navigability without attempting to focus
|
// elem is a container element
|
||||||
const nextContainerIds = getContainersForNode(target);
|
target = getTargetByContainer(getContainerId(elem));
|
||||||
const nextContainerId = last(nextContainerIds);
|
}
|
||||||
|
|
||||||
if (isNavigable(target, nextContainerId, true)) {
|
// Check navigability without attempting to focus
|
||||||
return target;
|
const nextContainerIds = getContainersForNode(target);
|
||||||
}
|
const nextContainerId = last(nextContainerIds);
|
||||||
|
|
||||||
return null;
|
if (isNavigable(target, nextContainerId, true)) {
|
||||||
};
|
return target;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
// 메인 focus 함수
|
// 메인 focus 함수
|
||||||
export const focus = (elem) => {
|
export const focus = (elem) => {
|
||||||
console.log("focus test", elem);
|
console.log('focus test', elem);
|
||||||
var target = elem;
|
var target = elem;
|
||||||
var wasContainerId = false;
|
var wasContainerId = false;
|
||||||
|
|
||||||
@@ -86,7 +86,7 @@ export const focus = (elem) => {
|
|||||||
wasContainerId = true;
|
wasContainerId = true;
|
||||||
} else if (/^[\w\d-]+$/.test(elem)) {
|
} else if (/^[\w\d-]+$/.test(elem)) {
|
||||||
// 알파벳, 숫자, 대시, 언더스코어로 구성된 컴포넌트 ID 지원
|
// 알파벳, 숫자, 대시, 언더스코어로 구성된 컴포넌트 ID 지원
|
||||||
target = getTargetBySelector("[data-spotlight-id=".concat(elem, "]"));
|
target = getTargetBySelector('[data-spotlight-id='.concat(elem, ']'));
|
||||||
} else {
|
} else {
|
||||||
// CSS 셀렉터로 처리
|
// CSS 셀렉터로 처리
|
||||||
target = getTargetBySelector(elem);
|
target = getTargetBySelector(elem);
|
||||||
@@ -123,118 +123,117 @@ export const focus = (elem) => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* spotlightId로 직접 포커스를 설정하는 함수
|
* spotlightId로 직접 포커스를 설정하는 함수
|
||||||
*
|
*
|
||||||
* @param {String} spotlightId - data-spotlight-id 속성값
|
* @param {String} spotlightId - data-spotlight-id 속성값
|
||||||
* @param {Boolean} force - 강제 포커스 여부 (기본값: false)
|
* @param {Boolean} force - 강제 포커스 여부 (기본값: false)
|
||||||
* @returns {Boolean} 포커스 성공 여부
|
* @returns {Boolean} 포커스 성공 여부
|
||||||
*/
|
*/
|
||||||
export const focusById = (spotlightId, force = false) => {
|
export const focusById = (spotlightId, force = false) => {
|
||||||
// spotlightId 유효성 검사
|
// spotlightId 유효성 검사
|
||||||
if (!spotlightId || typeof spotlightId !== 'string') {
|
if (!spotlightId || typeof spotlightId !== 'string') {
|
||||||
console.error('[focusById] spotlightId는 반드시 문자열이어야 합니다.');
|
console.error('[focusById] spotlightId는 반드시 문자열이어야 합니다.');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// data-spotlight-id 속성을 가진 요소 직접 검색
|
||||||
|
const targetElement = document.querySelector(`[data-spotlight-id="${spotlightId}"]`);
|
||||||
|
|
||||||
|
if (!targetElement) {
|
||||||
|
console.warn(`[focusById] spotlightId "${spotlightId}"를 가진 요소를 찾을 수 없습니다.`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
// 요소가 현재 보이고 활성화되어 있는지 확인
|
||||||
// data-spotlight-id 속성을 가진 요소 직접 검색
|
if (!isElementVisible(targetElement)) {
|
||||||
const targetElement = document.querySelector(`[data-spotlight-id="${spotlightId}"]`);
|
console.warn(`[focusById] 요소 "${spotlightId}"가 보이지 않거나 비활성화되어 있습니다.`);
|
||||||
|
if (!force) return false;
|
||||||
if (!targetElement) {
|
}
|
||||||
console.warn(`[focusById] spotlightId "${spotlightId}"를 가진 요소를 찾을 수 없습니다.`);
|
|
||||||
|
// Spotlight의 isSpottable로 포커스 가능 여부 확인
|
||||||
|
if (typeof Spotlight !== 'undefined' && Spotlight.isSpottable) {
|
||||||
|
if (!Spotlight.isSpottable(targetElement) && !force) {
|
||||||
|
console.warn(`[focusById] 요소 "${spotlightId}"가 현재 spottable하지 않습니다.`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// 요소가 현재 보이고 활성화되어 있는지 확인
|
|
||||||
if (!isElementVisible(targetElement)) {
|
// 직접 DOM 포커스 시도
|
||||||
console.warn(`[focusById] 요소 "${spotlightId}"가 보이지 않거나 비활성화되어 있습니다.`);
|
if (force) {
|
||||||
if (!force) return false;
|
// 강제 모드: DOM focus() 직접 호출
|
||||||
}
|
console.log(`[focusById] 강제 포커스 모드: "${spotlightId}"`);
|
||||||
|
targetElement.focus();
|
||||||
// Spotlight의 isSpottable로 포커스 가능 여부 확인
|
return true;
|
||||||
if (typeof Spotlight !== 'undefined' && Spotlight.isSpottable) {
|
} else {
|
||||||
if (!Spotlight.isSpottable(targetElement) && !force) {
|
// 일반 모드: Spotlight 시스템 사용
|
||||||
console.warn(`[focusById] 요소 "${spotlightId}"가 현재 spottable하지 않습니다.`);
|
console.log(`[focusById] Spotlight 포커스: "${spotlightId}"`);
|
||||||
return false;
|
|
||||||
}
|
// Spotlight.focus() 사용 (선택자 형태로 전달)
|
||||||
}
|
const focusResult = focus(`[data-spotlight-id="${spotlightId}"]`);
|
||||||
|
|
||||||
// 직접 DOM 포커스 시도
|
if (!focusResult) {
|
||||||
if (force) {
|
// Spotlight 포커스 실패 시 직접 포커스 시도
|
||||||
// 강제 모드: DOM focus() 직접 호출
|
console.log(`[focusById] Spotlight 포커스 실패, 직접 포커스 시도: "${spotlightId}"`);
|
||||||
console.log(`[focusById] 강제 포커스 모드: "${spotlightId}"`);
|
|
||||||
targetElement.focus();
|
targetElement.focus();
|
||||||
return true;
|
return document.activeElement === targetElement;
|
||||||
} else {
|
|
||||||
// 일반 모드: Spotlight 시스템 사용
|
|
||||||
console.log(`[focusById] Spotlight 포커스: "${spotlightId}"`);
|
|
||||||
|
|
||||||
// Spotlight.focus() 사용 (선택자 형태로 전달)
|
|
||||||
const focusResult = focus(`[data-spotlight-id="${spotlightId}"]`);
|
|
||||||
|
|
||||||
if (!focusResult) {
|
|
||||||
// Spotlight 포커스 실패 시 직접 포커스 시도
|
|
||||||
console.log(`[focusById] Spotlight 포커스 실패, 직접 포커스 시도: "${spotlightId}"`);
|
|
||||||
targetElement.focus();
|
|
||||||
return document.activeElement === targetElement;
|
|
||||||
}
|
|
||||||
|
|
||||||
return focusResult;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
return focusResult;
|
||||||
console.error(`[focusById] 포커스 설정 중 오류 발생: "${spotlightId}"`, error);
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
};
|
} catch (error) {
|
||||||
|
console.error(`[focusById] 포커스 설정 중 오류 발생: "${spotlightId}"`, error);
|
||||||
/**
|
return false;
|
||||||
* 요소가 보이고 포커스 가능한 상태인지 확인하는 헬퍼 함수
|
}
|
||||||
*
|
};
|
||||||
* @param {Element} element - 확인할 DOM 요소
|
|
||||||
* @returns {Boolean} 요소의 가시성 및 활성화 상태
|
/**
|
||||||
*/
|
* 요소가 보이고 포커스 가능한 상태인지 확인하는 헬퍼 함수
|
||||||
const isElementVisible = (element) => {
|
*
|
||||||
if (!element) return false;
|
* @param {Element} element - 확인할 DOM 요소
|
||||||
|
* @returns {Boolean} 요소의 가시성 및 활성화 상태
|
||||||
// 요소가 DOM에 연결되어 있는지 확인
|
*/
|
||||||
if (!element.isConnected) return false;
|
const isElementVisible = (element) => {
|
||||||
|
if (!element) return false;
|
||||||
// disabled 속성 확인
|
|
||||||
if (element.disabled) return false;
|
// 요소가 DOM에 연결되어 있는지 확인
|
||||||
|
if (!element.isConnected) return false;
|
||||||
// display: none 또는 visibility: hidden 확인
|
|
||||||
const style = window.getComputedStyle(element);
|
// disabled 속성 확인
|
||||||
if (style.display === 'none' || style.visibility === 'hidden') return false;
|
if (element.disabled) return false;
|
||||||
|
|
||||||
// opacity가 0인지 확인
|
// display: none 또는 visibility: hidden 확인
|
||||||
if (parseFloat(style.opacity) === 0) return false;
|
const style = window.getComputedStyle(element);
|
||||||
|
if (style.display === 'none' || style.visibility === 'hidden') return false;
|
||||||
// 요소의 크기가 0인지 확인
|
|
||||||
const rect = element.getBoundingClientRect();
|
// opacity가 0인지 확인
|
||||||
if (rect.width === 0 && rect.height === 0) return false;
|
if (parseFloat(style.opacity) === 0) return false;
|
||||||
|
|
||||||
return true;
|
// 요소의 크기가 0인지 확인
|
||||||
};
|
const rect = element.getBoundingClientRect();
|
||||||
|
if (rect.width === 0 && rect.height === 0) return false;
|
||||||
/**
|
|
||||||
* 현재 포커스된 요소의 spotlightId를 반환하는 헬퍼 함수
|
return true;
|
||||||
*
|
};
|
||||||
* @returns {String|null} 현재 포커스된 요소의 spotlightId 또는 null
|
|
||||||
*/
|
/**
|
||||||
export const getCurrentSpotlightId = () => {
|
* 현재 포커스된 요소의 spotlightId를 반환하는 헬퍼 함수
|
||||||
const current = document.activeElement;
|
*
|
||||||
if (current && current.hasAttribute('data-spotlight-id')) {
|
* @returns {String|null} 현재 포커스된 요소의 spotlightId 또는 null
|
||||||
return current.getAttribute('data-spotlight-id');
|
*/
|
||||||
}
|
export const getCurrentSpotlightId = () => {
|
||||||
return null;
|
const current = document.activeElement;
|
||||||
};
|
if (current && current.hasAttribute('data-spotlight-id')) {
|
||||||
|
return current.getAttribute('data-spotlight-id');
|
||||||
/**
|
}
|
||||||
* 특정 spotlightId를 가진 요소가 현재 포커스되어 있는지 확인하는 함수
|
return null;
|
||||||
*
|
};
|
||||||
* @param {String} spotlightId - 확인할 spotlightId
|
|
||||||
* @returns {Boolean} 해당 요소가 현재 포커스되어 있는지 여부
|
/**
|
||||||
*/
|
* 특정 spotlightId를 가진 요소가 현재 포커스되어 있는지 확인하는 함수
|
||||||
export const isCurrentlyFocused = (spotlightId) => {
|
*
|
||||||
return getCurrentSpotlightId() === spotlightId;
|
* @param {String} spotlightId - 확인할 spotlightId
|
||||||
};
|
* @returns {Boolean} 해당 요소가 현재 포커스되어 있는지 여부
|
||||||
|
*/
|
||||||
|
export const isCurrentlyFocused = (spotlightId) => {
|
||||||
|
return getCurrentSpotlightId() === spotlightId;
|
||||||
|
};
|
||||||
|
|||||||
@@ -3,11 +3,9 @@ import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useSta
|
|||||||
|
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
|
||||||
import Spinner from '@enact/sandstone/Spinner';
|
|
||||||
import Spotlight from '@enact/spotlight';
|
import Spotlight from '@enact/spotlight';
|
||||||
import { setContainerLastFocusedElement } from '@enact/spotlight/src/container';
|
import { setContainerLastFocusedElement } from '@enact/spotlight/src/container';
|
||||||
|
|
||||||
import indicatorDefaultImage from '../../../assets/images/img-thumb-empty-144@3x.png';
|
|
||||||
import { getDeviceAdditionInfo } from '../../actions/deviceActions';
|
import { getDeviceAdditionInfo } from '../../actions/deviceActions';
|
||||||
import { getThemeCurationDetailInfo } from '../../actions/homeActions';
|
import { getThemeCurationDetailInfo } from '../../actions/homeActions';
|
||||||
import { getMainCategoryDetail, getMainYouMayLike } from '../../actions/mainActions';
|
import { getMainCategoryDetail, getMainYouMayLike } from '../../actions/mainActions';
|
||||||
@@ -22,7 +20,6 @@ import TBody from '../../components/TBody/TBody';
|
|||||||
import TPanel from '../../components/TPanel/TPanel';
|
import TPanel from '../../components/TPanel/TPanel';
|
||||||
import { panel_names } from '../../utils/Config';
|
import { panel_names } from '../../utils/Config';
|
||||||
import fp from '../../utils/fp';
|
import fp from '../../utils/fp';
|
||||||
import { $L, getQRCodeUrl } from '../../utils/helperMethods';
|
|
||||||
import { SpotlightIds } from '../../utils/SpotlightIds';
|
import { SpotlightIds } from '../../utils/SpotlightIds';
|
||||||
import DetailPanelBackground from './components/DetailPanelBackground';
|
import DetailPanelBackground from './components/DetailPanelBackground';
|
||||||
import THeaderCustom from './components/THeaderCustom';
|
import THeaderCustom from './components/THeaderCustom';
|
||||||
@@ -51,11 +48,6 @@ export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) {
|
|||||||
const panels = useSelector((state) => state.panels.panels);
|
const panels = useSelector((state) => state.panels.panels);
|
||||||
|
|
||||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||||
const localRecentItems = useSelector((state) =>
|
|
||||||
fp.pipe(() => state, fp.get('localSettings.recentItems'))()
|
|
||||||
);
|
|
||||||
const { httpHeader } = useSelector((state) => state.common);
|
|
||||||
const { popupVisible, activePopup } = useSelector((state) => state.common.popup);
|
|
||||||
const [lgCatCd, setLgCatCd] = useState('');
|
const [lgCatCd, setLgCatCd] = useState('');
|
||||||
const [themeProductInfo, setThemeProductInfo] = useState(null);
|
const [themeProductInfo, setThemeProductInfo] = useState(null);
|
||||||
|
|
||||||
@@ -561,11 +553,6 @@ export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) {
|
|||||||
}
|
}
|
||||||
}, [themeData, selectedIndex]);
|
}, [themeData, selectedIndex]);
|
||||||
|
|
||||||
const imageUrl = useMemo(
|
|
||||||
() => fp.pipe(() => productData, fp.get('thumbnailUrl960'))(),
|
|
||||||
[productData]
|
|
||||||
);
|
|
||||||
|
|
||||||
// 타이틀과 aria-label 메모이제이션 (성능 최적화)
|
// 타이틀과 aria-label 메모이제이션 (성능 최적화)
|
||||||
const headerTitle = useMemo(
|
const headerTitle = useMemo(
|
||||||
() =>
|
() =>
|
||||||
|
|||||||
@@ -7,39 +7,39 @@ export default function DetailPanelSkeleton() {
|
|||||||
return (
|
return (
|
||||||
<div className={css.detailArea}>
|
<div className={css.detailArea}>
|
||||||
{/* 1. Left Margin Section - 60px */}
|
{/* 1. Left Margin Section - 60px */}
|
||||||
<div className={css.leftMarginSection}></div>
|
<div className={css.leftMarginSection} />
|
||||||
|
|
||||||
{/* 2. Info Section - 650px (왼쪽 영역 스켈레톤) */}
|
{/* 2. Info Section - 650px (왼쪽 영역 스켈레톤) */}
|
||||||
<div className={css.infoSection}>
|
<div className={css.infoSection}>
|
||||||
<div className={css.leftInfoContainer}>
|
<div className={css.leftInfoContainer}>
|
||||||
<div className={css.leftInfoWrapper}>
|
<div className={css.leftInfoWrapper}>
|
||||||
{/* 제품 태그 스켈레톤 */}
|
{/* 제품 태그 스켈레톤 */}
|
||||||
<div className={css.skeletonProductTag}></div>
|
<div className={css.skeletonProductTag} />
|
||||||
|
|
||||||
{/* 제품 정보 스켈레톤 */}
|
{/* 제품 정보 스켈레톤 */}
|
||||||
<div className={css.skeletonProductInfo}>
|
<div className={css.skeletonProductInfo}>
|
||||||
<div className={css.skeletonTitle}></div>
|
<div className={css.skeletonTitle} />
|
||||||
<div className={css.skeletonSubtitle}></div>
|
<div className={css.skeletonSubtitle} />
|
||||||
<div className={css.skeletonPrice}></div>
|
<div className={css.skeletonPrice} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* QR 코드 스켈레톤 */}
|
{/* QR 코드 스켈레톤 */}
|
||||||
<div className={css.skeletonQrCode}></div>
|
<div className={css.skeletonQrCode} />
|
||||||
|
|
||||||
{/* 버튼들 스켈레톤 */}
|
{/* 버튼들 스켈레톤 */}
|
||||||
<div className={css.skeletonButtons}>
|
<div className={css.skeletonButtons}>
|
||||||
<div className={css.skeletonShopButton}></div>
|
<div className={css.skeletonShopButton} />
|
||||||
<div className={css.skeletonFavoriteButton}></div>
|
<div className={css.skeletonFavoriteButton} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 주문 전화 섹션 스켈레톤 */}
|
{/* 주문 전화 섹션 스켈레톤 */}
|
||||||
<div className={css.skeletonCallToOrder}></div>
|
<div className={css.skeletonCallToOrder} />
|
||||||
|
|
||||||
{/* 액션 버튼들 스켈레톤 */}
|
{/* 액션 버튼들 스켈레톤 */}
|
||||||
<div className={css.skeletonActionButtons}>
|
<div className={css.skeletonActionButtons}>
|
||||||
<div className={css.skeletonActionButton}></div>
|
<div className={css.skeletonActionButton} />
|
||||||
<div className={css.skeletonActionButton}></div>
|
<div className={css.skeletonActionButton} />
|
||||||
<div className={css.skeletonActionButton}></div>
|
<div className={css.skeletonActionButton} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -52,57 +52,57 @@ export default function DetailPanelSkeleton() {
|
|||||||
<div className={css.scrollerOverride}>
|
<div className={css.scrollerOverride}>
|
||||||
{/* 제품 이미지/비디오 스켈레톤 */}
|
{/* 제품 이미지/비디오 스켈레톤 */}
|
||||||
<div className={css.skeletonProductImages}>
|
<div className={css.skeletonProductImages}>
|
||||||
<div className={css.skeletonMainImage}></div>
|
<div className={css.skeletonMainImage} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 제품 설명 스켈레톤 */}
|
{/* 제품 설명 스켈레톤 */}
|
||||||
<div className={css.skeletonDescription}>
|
<div className={css.skeletonDescription}>
|
||||||
<div className={css.skeletonDescTitle}></div>
|
<div className={css.skeletonDescTitle} />
|
||||||
<div className={css.skeletonDescLine}></div>
|
<div className={css.skeletonDescLine} />
|
||||||
<div className={css.skeletonDescLine}></div>
|
<div className={css.skeletonDescLine} />
|
||||||
<div className={css.skeletonDescLine}></div>
|
<div className={css.skeletonDescLine} />
|
||||||
<div className={css.skeletonDescLineShort}></div>
|
<div className={css.skeletonDescLineShort} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 리뷰 섹션 스켈레톤 */}
|
{/* 리뷰 섹션 스켈레톤 */}
|
||||||
<div className={css.skeletonReviews}>
|
<div className={css.skeletonReviews}>
|
||||||
<div className={css.skeletonReviewTitle}></div>
|
<div className={css.skeletonReviewTitle} />
|
||||||
<div className={css.skeletonReviewItem}>
|
<div className={css.skeletonReviewItem}>
|
||||||
<div className={css.skeletonReviewAvatar}></div>
|
<div className={css.skeletonReviewAvatar} />
|
||||||
<div className={css.skeletonReviewContent}>
|
<div className={css.skeletonReviewContent}>
|
||||||
<div className={css.skeletonReviewHeader}></div>
|
<div className={css.skeletonReviewHeader} />
|
||||||
<div className={css.skeletonReviewText}></div>
|
<div className={css.skeletonReviewText} />
|
||||||
<div className={css.skeletonReviewTextShort}></div>
|
<div className={css.skeletonReviewTextShort} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={css.skeletonReviewItem}>
|
<div className={css.skeletonReviewItem}>
|
||||||
<div className={css.skeletonReviewAvatar}></div>
|
<div className={css.skeletonReviewAvatar} />
|
||||||
<div className={css.skeletonReviewContent}>
|
<div className={css.skeletonReviewContent}>
|
||||||
<div className={css.skeletonReviewHeader}></div>
|
<div className={css.skeletonReviewHeader} />
|
||||||
<div className={css.skeletonReviewText}></div>
|
<div className={css.skeletonReviewText} />
|
||||||
<div className={css.skeletonReviewTextShort}></div>
|
<div className={css.skeletonReviewTextShort} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 추천 상품 스켈레톤 */}
|
{/* 추천 상품 스켈레톤 */}
|
||||||
<div className={css.skeletonYouMayLike}>
|
<div className={css.skeletonYouMayLike}>
|
||||||
<div className={css.skeletonYouMayLikeTitle}></div>
|
<div className={css.skeletonYouMayLikeTitle} />
|
||||||
<div className={css.skeletonProductGrid}>
|
<div className={css.skeletonProductGrid}>
|
||||||
<div className={css.skeletonProductCard}>
|
<div className={css.skeletonProductCard}>
|
||||||
<div className={css.skeletonProductImage}></div>
|
<div className={css.skeletonProductImage} />
|
||||||
<div className={css.skeletonProductName}></div>
|
<div className={css.skeletonProductName} />
|
||||||
<div className={css.skeletonProductPrice}></div>
|
<div className={css.skeletonProductPrice} />
|
||||||
</div>
|
</div>
|
||||||
<div className={css.skeletonProductCard}>
|
<div className={css.skeletonProductCard}>
|
||||||
<div className={css.skeletonProductImage}></div>
|
<div className={css.skeletonProductImage} />
|
||||||
<div className={css.skeletonProductName}></div>
|
<div className={css.skeletonProductName} />
|
||||||
<div className={css.skeletonProductPrice}></div>
|
<div className={css.skeletonProductPrice} />
|
||||||
</div>
|
</div>
|
||||||
<div className={css.skeletonProductCard}>
|
<div className={css.skeletonProductCard}>
|
||||||
<div className={css.skeletonProductImage}></div>
|
<div className={css.skeletonProductImage} />
|
||||||
<div className={css.skeletonProductName}></div>
|
<div className={css.skeletonProductName} />
|
||||||
<div className={css.skeletonProductPrice}></div>
|
<div className={css.skeletonProductPrice} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -112,4 +112,4 @@ export default function DetailPanelSkeleton() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import Spottable from '@enact/spotlight/Spottable';
|
|||||||
import defaultLogoImg from '../../../../assets/images/ic-tab-partners-default@3x.png';
|
import defaultLogoImg from '../../../../assets/images/ic-tab-partners-default@3x.png';
|
||||||
import CustomImage from '../../../components/CustomImage/CustomImage';
|
import CustomImage from '../../../components/CustomImage/CustomImage';
|
||||||
import { convertUtcToLocal } from '../../../components/MediaPlayer/util';
|
import { convertUtcToLocal } from '../../../components/MediaPlayer/util';
|
||||||
import { $L, removeSpecificTags } from '../../../utils/helperMethods';
|
import { $L } from '../../../utils/helperMethods';
|
||||||
import css1 from './PlayerItemCard.module.less';
|
import css1 from './PlayerItemCard.module.less';
|
||||||
import css2 from './PlayerItemCard.v2.module.less';
|
import css2 from './PlayerItemCard.v2.module.less';
|
||||||
|
|
||||||
@@ -33,11 +33,9 @@ export const removeDotAndColon = (string) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default memo(function PlayerItemCard({
|
export default memo(function PlayerItemCard({
|
||||||
children,
|
|
||||||
disabled,
|
disabled,
|
||||||
imageAlt,
|
imageAlt,
|
||||||
imageSource,
|
imageSource,
|
||||||
imgType = IMAGETYPES.imgHorizontal,
|
|
||||||
logo,
|
logo,
|
||||||
onBlur,
|
onBlur,
|
||||||
onClick,
|
onClick,
|
||||||
@@ -47,10 +45,8 @@ export default memo(function PlayerItemCard({
|
|||||||
soldoutFlag,
|
soldoutFlag,
|
||||||
spotlightId,
|
spotlightId,
|
||||||
patnerName,
|
patnerName,
|
||||||
selectedIndex,
|
|
||||||
videoVerticalVisible,
|
videoVerticalVisible,
|
||||||
currentVideoVisible,
|
currentVideoVisible,
|
||||||
dangerouslySetInnerHTML,
|
|
||||||
currentTime,
|
currentTime,
|
||||||
liveInfo,
|
liveInfo,
|
||||||
startDt,
|
startDt,
|
||||||
@@ -144,6 +140,7 @@ export default memo(function PlayerItemCard({
|
|||||||
<h3 className={css.brandName}>{patnerName}</h3>
|
<h3 className={css.brandName}>{patnerName}</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* eslint-disable-next-line react/no-danger */}
|
||||||
<h3 dangerouslySetInnerHTML={productName()} className={css.title} />
|
<h3 dangerouslySetInnerHTML={productName()} className={css.title} />
|
||||||
{liveInfo && liveInfo.showType === 'live' && (
|
{liveInfo && liveInfo.showType === 'live' && (
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -37,7 +37,6 @@ function PlayerOverlayContents({
|
|||||||
handleIndicatorDownClick,
|
handleIndicatorDownClick,
|
||||||
tabContainerVersion,
|
tabContainerVersion,
|
||||||
tabIndexV2,
|
tabIndexV2,
|
||||||
...rest
|
|
||||||
}) {
|
}) {
|
||||||
const cntry_cd = useSelector((state) => state.common.httpHeader?.cntry_cd);
|
const cntry_cd = useSelector((state) => state.common.httpHeader?.cntry_cd);
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
@@ -66,7 +65,7 @@ function PlayerOverlayContents({
|
|||||||
}
|
}
|
||||||
|
|
||||||
setIsSubtitleActive((prev) => !prev);
|
setIsSubtitleActive((prev) => !prev);
|
||||||
}, [dispatch, captionEnable]);
|
}, [dispatch, captionEnable, setIsSubtitleActive]);
|
||||||
|
|
||||||
const patncLogoPath = useMemo(() => {
|
const patncLogoPath = useMemo(() => {
|
||||||
let logo = playListInfo[selectedIndex]?.patncLogoPath;
|
let logo = playListInfo[selectedIndex]?.patncLogoPath;
|
||||||
@@ -75,7 +74,7 @@ function PlayerOverlayContents({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return logo;
|
return logo;
|
||||||
}, [playListInfo, selectedIndex, panelInfo]);
|
}, [playListInfo, selectedIndex, panelInfo, type]);
|
||||||
|
|
||||||
const partnerName = useMemo(() => {
|
const partnerName = useMemo(() => {
|
||||||
let name = playListInfo[selectedIndex]?.patncNm;
|
let name = playListInfo[selectedIndex]?.patncNm;
|
||||||
@@ -84,7 +83,7 @@ function PlayerOverlayContents({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return name;
|
return name;
|
||||||
}, [playListInfo, selectedIndex, panelInfo]);
|
}, [playListInfo, selectedIndex, panelInfo, type]);
|
||||||
|
|
||||||
const showName = useMemo(() => {
|
const showName = useMemo(() => {
|
||||||
let name = playListInfo[selectedIndex]?.showNm;
|
let name = playListInfo[selectedIndex]?.showNm;
|
||||||
@@ -93,21 +92,24 @@ function PlayerOverlayContents({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return name ? name.replace(/<br\s*\/?>/gi, ' ') : '';
|
return name ? name.replace(/<br\s*\/?>/gi, ' ') : '';
|
||||||
}, [playListInfo, selectedIndex, panelInfo]);
|
}, [playListInfo, selectedIndex, panelInfo, type]);
|
||||||
|
|
||||||
const onSpotlightMoveTabButton = (e) => {
|
const onSpotlightMoveTabButton = useCallback((e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
Spotlight.focus(SpotlightIds.PLAYER_TAB_BUTTON);
|
Spotlight.focus(SpotlightIds.PLAYER_TAB_BUTTON);
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
const onSpotlightMoveMediaButton = (e) => {
|
const onSpotlightMoveMediaButton = useCallback(
|
||||||
e.stopPropagation();
|
(e) => {
|
||||||
if (type === 'LIVE') {
|
e.stopPropagation();
|
||||||
return Spotlight.focus('videoIndicator-down-button');
|
if (type === 'LIVE') {
|
||||||
}
|
return Spotlight.focus('videoIndicator-down-button');
|
||||||
Spotlight.focus('videoPlayer_mediaControls');
|
}
|
||||||
};
|
Spotlight.focus('videoPlayer_mediaControls');
|
||||||
|
},
|
||||||
|
[type]
|
||||||
|
);
|
||||||
|
|
||||||
const onSpotlightMoveSlider = useCallback(
|
const onSpotlightMoveSlider = useCallback(
|
||||||
(e) => {
|
(e) => {
|
||||||
@@ -120,33 +122,36 @@ function PlayerOverlayContents({
|
|||||||
[type]
|
[type]
|
||||||
);
|
);
|
||||||
|
|
||||||
const onSpotlightMoveSideTab = (e) => {
|
const onSpotlightMoveSideTab = useCallback((e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
Spotlight.focus('tab-0');
|
Spotlight.focus('tab-0');
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
const onSpotlightMoveBelowTab = (e) => {
|
const onSpotlightMoveBelowTab = useCallback(
|
||||||
e.stopPropagation();
|
(e) => {
|
||||||
e.preventDefault();
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
// tabIndexV2에 따라 다른 버튼으로 포커스 이동
|
// tabIndexV2에 따라 다른 버튼으로 포커스 이동
|
||||||
if (tabIndexV2 === 0) {
|
if (tabIndexV2 === 0) {
|
||||||
// ShopNow 탭: Close 버튼으로
|
// ShopNow 탭: Close 버튼으로
|
||||||
// Spotlight.focus('below-tab-close-button');
|
// Spotlight.focus('below-tab-close-button');
|
||||||
Spotlight.focus('shownow_close_button');
|
Spotlight.focus('shownow_close_button');
|
||||||
} else if (tabIndexV2 === 1) {
|
} else if (tabIndexV2 === 1) {
|
||||||
// LIVE CHANNEL 탭: LIVE CHANNEL 버튼으로
|
// LIVE CHANNEL 탭: LIVE CHANNEL 버튼으로
|
||||||
Spotlight.focus('below-tab-live-channel-button');
|
Spotlight.focus('below-tab-live-channel-button');
|
||||||
} else if (tabIndexV2 === 2) {
|
} else if (tabIndexV2 === 2) {
|
||||||
// ShopNowButton: ShopNowButton으로
|
// ShopNowButton: ShopNowButton으로
|
||||||
Spotlight.focus('below-tab-shop-now-button');
|
Spotlight.focus('below-tab-shop-now-button');
|
||||||
}
|
}
|
||||||
};
|
},
|
||||||
|
[tabIndexV2]
|
||||||
|
);
|
||||||
|
|
||||||
const onSpotlightMoveBackButton = () => {
|
const onSpotlightMoveBackButton = useCallback(() => {
|
||||||
return Spotlight.focus(SpotlightIds.PLAYER_BACK_BUTTON);
|
return Spotlight.focus(SpotlightIds.PLAYER_BACK_BUTTON);
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
const currentSideButtonStatus = useMemo(() => {
|
const currentSideButtonStatus = useMemo(() => {
|
||||||
if (
|
if (
|
||||||
@@ -158,7 +163,7 @@ function PlayerOverlayContents({
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}, [panelInfo, sideContentsVisible, tabContainerVersion]);
|
}, [type, panelInfo, sideContentsVisible, tabContainerVersion]);
|
||||||
|
|
||||||
const noLiveContentsVisible = useMemo(() => {
|
const noLiveContentsVisible = useMemo(() => {
|
||||||
if (!Array.isArray(playListInfo) || playListInfo.length === 0) {
|
if (!Array.isArray(playListInfo) || playListInfo.length === 0) {
|
||||||
|
|||||||
@@ -4,11 +4,9 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
|
||||||
import { Job } from '@enact/core/util';
|
|
||||||
import Spotlight from '@enact/spotlight';
|
import Spotlight from '@enact/spotlight';
|
||||||
import SpotlightContainerDecorator from '@enact/spotlight/SpotlightContainerDecorator';
|
import SpotlightContainerDecorator from '@enact/spotlight/SpotlightContainerDecorator';
|
||||||
import Spottable from '@enact/spotlight/Spottable';
|
import Spottable from '@enact/spotlight/Spottable';
|
||||||
import { setContainerLastFocusedElement } from '@enact/spotlight/src/container';
|
|
||||||
|
|
||||||
import micIcon from '../../../assets/images/searchpanel/image-mic.png';
|
import micIcon from '../../../assets/images/searchpanel/image-mic.png';
|
||||||
import hotPicksImage from '../../../assets/images/searchpanel/img-hotpicks.png';
|
import hotPicksImage from '../../../assets/images/searchpanel/img-hotpicks.png';
|
||||||
@@ -16,7 +14,7 @@ import hotPicksBrandImage from '../../../assets/images/searchpanel/img-search-ho
|
|||||||
import { sendLogGNB, sendLogTotalRecommend } from '../../actions/logActions';
|
import { sendLogGNB, sendLogTotalRecommend } from '../../actions/logActions';
|
||||||
import { getMyRecommandedKeyword } from '../../actions/myPageActions';
|
import { getMyRecommandedKeyword } from '../../actions/myPageActions';
|
||||||
import { popPanel, updatePanel } from '../../actions/panelActions';
|
import { popPanel, updatePanel } from '../../actions/panelActions';
|
||||||
import { getSearch, resetSearch, searchMain } from '../../actions/searchActions';
|
import { getSearch, resetSearch } from '../../actions/searchActions';
|
||||||
// import {
|
// import {
|
||||||
// showErrorToast,
|
// showErrorToast,
|
||||||
// showInfoToast,
|
// showInfoToast,
|
||||||
@@ -28,15 +26,11 @@ import { getSearch, resetSearch, searchMain } from '../../actions/searchActions'
|
|||||||
import TBody from '../../components/TBody/TBody';
|
import TBody from '../../components/TBody/TBody';
|
||||||
import TInput, { ICONS, KINDS } from './TInput/TInput';
|
import TInput, { ICONS, KINDS } from './TInput/TInput';
|
||||||
import TPanel from '../../components/TPanel/TPanel';
|
import TPanel from '../../components/TPanel/TPanel';
|
||||||
import TScroller from '../../components/TScroller/TScroller';
|
|
||||||
import TVerticalPagenator from '../../components/TVerticalPagenator/TVerticalPagenator';
|
import TVerticalPagenator from '../../components/TVerticalPagenator/TVerticalPagenator';
|
||||||
import TVirtualGridList from '../../components/TVirtualGridList/TVirtualGridList';
|
import TVirtualGridList from '../../components/TVirtualGridList/TVirtualGridList';
|
||||||
// import VirtualKeyboardContainer from "../../components/TToast/VirtualKeyboardContainer";
|
// import VirtualKeyboardContainer from "../../components/TToast/VirtualKeyboardContainer";
|
||||||
import usePrevious from '../../hooks/usePrevious';
|
import usePrevious from '../../hooks/usePrevious';
|
||||||
import { LOG_CONTEXT_NAME, LOG_MENU, LOG_MESSAGE_ID, panel_names } from '../../utils/Config';
|
import { LOG_CONTEXT_NAME, LOG_MENU, LOG_MESSAGE_ID, panel_names } from '../../utils/Config';
|
||||||
import { SpotlightIds } from '../../utils/SpotlightIds';
|
|
||||||
import NoSearchResults from './NoSearchResults/NoSearchResults';
|
|
||||||
import RecommendedKeywords from './RecommendedKeywords/RecommendedKeywords';
|
|
||||||
import SearchInputOverlay from './SearchInpuOverlay';
|
import SearchInputOverlay from './SearchInpuOverlay';
|
||||||
import css from './SearchPanel.new.module.less';
|
import css from './SearchPanel.new.module.less';
|
||||||
import SearchResultsNew from './SearchResults.new';
|
import SearchResultsNew from './SearchResults.new';
|
||||||
@@ -54,7 +48,6 @@ const SectionContainer = SpotlightContainerDecorator({ enterTo: 'last-focused' }
|
|||||||
const SpottableMicButton = Spottable('div');
|
const SpottableMicButton = Spottable('div');
|
||||||
const SpottableKeyword = Spottable('div');
|
const SpottableKeyword = Spottable('div');
|
||||||
const SpottableProduct = Spottable('div');
|
const SpottableProduct = Spottable('div');
|
||||||
const SpottableLi = Spottable('li');
|
|
||||||
|
|
||||||
const ITEMS_PER_PAGE = 9;
|
const ITEMS_PER_PAGE = 9;
|
||||||
|
|
||||||
@@ -70,7 +63,7 @@ const SPOTLIGHT_IDS = {
|
|||||||
SEARCH_VERTICAL_PAGENATOR: 'search_verticalPagenator',
|
SEARCH_VERTICAL_PAGENATOR: 'search_verticalPagenator',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function SearchPanel({ panelInfo, isOnTop, spotlightId, scrollOptions = [] }) {
|
export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const loadingComplete = useSelector((state) => state.common?.loadingComplete);
|
const loadingComplete = useSelector((state) => state.common?.loadingComplete);
|
||||||
const recommandedKeywords = useSelector(
|
const recommandedKeywords = useSelector(
|
||||||
@@ -90,16 +83,15 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId, scrollOpt
|
|||||||
const [showVirtualKeyboard, setShowVirtualKeyboard] = useState(false);
|
const [showVirtualKeyboard, setShowVirtualKeyboard] = useState(false);
|
||||||
const [isVoiceOverlayVisible, setIsVoiceOverlayVisible] = useState(false);
|
const [isVoiceOverlayVisible, setIsVoiceOverlayVisible] = useState(false);
|
||||||
const [isSearchOverlayVisible, setIsSearchOverlayVisible] = useState(false);
|
const [isSearchOverlayVisible, setIsSearchOverlayVisible] = useState(false);
|
||||||
const [voiceMode, setVoiceMode] = useState(VOICE_MODES.PROMPT);
|
|
||||||
|
|
||||||
//인풋창 포커스 구분을 위함
|
//인풋창 포커스 구분을 위함
|
||||||
const [inputFocus, setInputFocus] = useState(false);
|
const [inputFocus, setInputFocus] = useState(false);
|
||||||
const _onFocus = useCallback(() => {
|
const _onFocus = useCallback(() => {
|
||||||
setInputFocus(true);
|
setInputFocus(true);
|
||||||
}, [inputFocus]);
|
}, []);
|
||||||
const _onBlur = useCallback(() => {
|
const _onBlur = useCallback(() => {
|
||||||
setInputFocus(false);
|
setInputFocus(false);
|
||||||
}, [inputFocus]);
|
}, []);
|
||||||
|
|
||||||
// TInput의 입력 모드 상태 (webOS 키보드가 뜨는지 여부)
|
// TInput의 입력 모드 상태 (webOS 키보드가 뜨는지 여부)
|
||||||
const [isInputModeActive, setIsInputModeActive] = useState(false);
|
const [isInputModeActive, setIsInputModeActive] = useState(false);
|
||||||
@@ -132,19 +124,6 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId, scrollOpt
|
|||||||
// 가짜 데이터 - 실제로는 Redux store나 API에서 가져와야 함
|
// 가짜 데이터 - 실제로는 Redux store나 API에서 가져와야 함
|
||||||
const recentSearches = useMemo(() => ['Puppy food', 'Dog toy', 'Fitness'], []);
|
const recentSearches = useMemo(() => ['Puppy food', 'Dog toy', 'Fitness'], []);
|
||||||
|
|
||||||
const recentResultSearches = useMemo(
|
|
||||||
() => [
|
|
||||||
'Puppy food',
|
|
||||||
'Dog toy',
|
|
||||||
"Mather's Day",
|
|
||||||
'Gift',
|
|
||||||
'Easter Day',
|
|
||||||
'Royal Canin puppy food2',
|
|
||||||
'Shark',
|
|
||||||
],
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
const topSearches = useMemo(
|
const topSearches = useMemo(
|
||||||
() => ["Mather's Day", 'Gift', 'Easter Day', 'Royal Canin puppy food', 'Fitness', 'Parrot'],
|
() => ["Mather's Day", 'Gift', 'Easter Day', 'Royal Canin puppy food', 'Fitness', 'Parrot'],
|
||||||
[]
|
[]
|
||||||
@@ -207,28 +186,30 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId, scrollOpt
|
|||||||
if (loadingComplete && !recommandedKeywords) {
|
if (loadingComplete && !recommandedKeywords) {
|
||||||
dispatch(getMyRecommandedKeyword());
|
dispatch(getMyRecommandedKeyword());
|
||||||
}
|
}
|
||||||
}, [loadingComplete]);
|
}, [loadingComplete, recommandedKeywords, dispatch]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isOnTop) {
|
if (isOnTop) {
|
||||||
let menu;
|
let menu;
|
||||||
if (!searchPerformed) menu = LOG_MENU.SEARCH_SEARCH;
|
if (!searchPerformed) {
|
||||||
else {
|
menu = LOG_MENU.SEARCH_SEARCH;
|
||||||
if (searchQueryRef.current)
|
} else {
|
||||||
|
if (searchQueryRef.current) {
|
||||||
menu =
|
menu =
|
||||||
Object.keys(searchDatas).length > 0
|
Object.keys(searchDatas).length > 0
|
||||||
? LOG_MENU.SEARCH_RESULT
|
? LOG_MENU.SEARCH_RESULT
|
||||||
: LOG_MENU.SEARCH_BEST_SELLER;
|
: LOG_MENU.SEARCH_BEST_SELLER;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
dispatch(sendLogGNB(menu));
|
dispatch(sendLogGNB(menu));
|
||||||
}
|
}
|
||||||
}, [isOnTop, searchDatas, searchPerformed]);
|
}, [isOnTop, searchDatas, searchPerformed, dispatch, searchQueryRef]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!searchQuery) {
|
if (!searchQuery) {
|
||||||
dispatch(resetSearch());
|
dispatch(resetSearch());
|
||||||
}
|
}
|
||||||
}, [dispatch]);
|
}, [dispatch, searchQuery]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (recommandedKeywords) {
|
if (recommandedKeywords) {
|
||||||
@@ -303,35 +284,17 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId, scrollOpt
|
|||||||
// 검색 시 가상 키보드 숨김
|
// 검색 시 가상 키보드 숨김
|
||||||
setShowVirtualKeyboard(false);
|
setShowVirtualKeyboard(false);
|
||||||
},
|
},
|
||||||
[dispatch, searchPerformed, searchDatas, searchQuery]
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
[dispatch, searchPerformed]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleNext = useCallback(() => {
|
|
||||||
if (!isOnTopRef.current) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setCurrentPage((prev) => prev + 1);
|
|
||||||
setPageChanged(true);
|
|
||||||
}, [currentPage]);
|
|
||||||
|
|
||||||
const handlePrev = useCallback(() => {
|
|
||||||
if (!isOnTopRef.current) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setCurrentPage((prev) => (prev > 1 ? prev - 1 : prev));
|
|
||||||
setPageChanged(true);
|
|
||||||
}, [currentPage]);
|
|
||||||
|
|
||||||
const hasPrevPage = currentPage > 1;
|
|
||||||
const hasNextPage = currentPage * ITEMS_PER_PAGE < recommandedKeywords?.length;
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (panelInfo && isOnTop) {
|
if (panelInfo && isOnTop) {
|
||||||
if (panelInfo.currentSpot && firstSpot) {
|
if (panelInfo.currentSpot && firstSpot) {
|
||||||
Spotlight.focus(panel_names.SEARCH_PANEL);
|
Spotlight.focus(panel_names.SEARCH_PANEL);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [panelInfo, isOnTop]);
|
}, [panelInfo, isOnTop, firstSpot]);
|
||||||
|
|
||||||
// SearchPanel이 처음 열릴 때 TInput으로 포커스 설정
|
// SearchPanel이 처음 열릴 때 TInput으로 포커스 설정
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -349,20 +312,23 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId, scrollOpt
|
|||||||
initialFocusTimerRef.current = null;
|
initialFocusTimerRef.current = null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [isOnTop]);
|
}, [isOnTop, isOnTopRef]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
|
const currentSearchVal = searchQueryRef.current;
|
||||||
|
const currentFocusedId = focusedContainerIdRef.current;
|
||||||
dispatch(
|
dispatch(
|
||||||
updatePanel({
|
updatePanel({
|
||||||
name: panel_names.SEARCH_PANEL,
|
name: panel_names.SEARCH_PANEL,
|
||||||
panelInfo: {
|
panelInfo: {
|
||||||
searchVal: searchQueryRef.current,
|
searchVal: currentSearchVal,
|
||||||
focusedContainerId: focusedContainerIdRef.current,
|
focusedContainerId: currentFocusedId,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Cleanup all timers on component unmount
|
// Cleanup all timers on component unmount
|
||||||
@@ -373,7 +339,7 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId, scrollOpt
|
|||||||
spotlightResumeTimerRef.current = null;
|
spotlightResumeTimerRef.current = null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, []);
|
}, [isOnTopRef]);
|
||||||
|
|
||||||
const handleKeydown = useCallback(
|
const handleKeydown = useCallback(
|
||||||
(e) => {
|
(e) => {
|
||||||
@@ -426,7 +392,7 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId, scrollOpt
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[searchQuery, position, handleSearchSubmit, showVirtualKeyboard]
|
[searchQuery, position, handleSearchSubmit, showVirtualKeyboard, isOnTopRef]
|
||||||
);
|
);
|
||||||
|
|
||||||
const cursorPosition = useCallback(() => {
|
const cursorPosition = useCallback(() => {
|
||||||
@@ -442,7 +408,7 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId, scrollOpt
|
|||||||
}
|
}
|
||||||
// 마이크 버튼 클릭 시 voice overlay 토글
|
// 마이크 버튼 클릭 시 voice overlay 토글
|
||||||
setIsVoiceOverlayVisible((prev) => !prev);
|
setIsVoiceOverlayVisible((prev) => !prev);
|
||||||
}, []);
|
}, [isOnTopRef]);
|
||||||
|
|
||||||
const onCancel = useCallback(() => {
|
const onCancel = useCallback(() => {
|
||||||
if (!isOnTopRef.current) {
|
if (!isOnTopRef.current) {
|
||||||
@@ -461,7 +427,7 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId, scrollOpt
|
|||||||
dispatch(resetSearch());
|
dispatch(resetSearch());
|
||||||
Spotlight.focus(SPOTLIGHT_IDS.SEARCH_INPUT_BOX);
|
Spotlight.focus(SPOTLIGHT_IDS.SEARCH_INPUT_BOX);
|
||||||
}
|
}
|
||||||
}, [searchQuery, dispatch, isVoiceOverlayVisible]);
|
}, [searchQuery, dispatch, isVoiceOverlayVisible, isOnTopRef]);
|
||||||
|
|
||||||
const onFocusedContainerId = useCallback(
|
const onFocusedContainerId = useCallback(
|
||||||
(containerId) => {
|
(containerId) => {
|
||||||
@@ -484,17 +450,9 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId, scrollOpt
|
|||||||
}, 0);
|
}, 0);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[panelInfo, firstSpot]
|
[panelInfo, firstSpot, panels]
|
||||||
);
|
);
|
||||||
|
|
||||||
const panelInfoFall = useMemo(() => {
|
|
||||||
const newPanelInfo = { ...panelInfo };
|
|
||||||
if (firstSpot) {
|
|
||||||
newPanelInfo.currentSpot = null;
|
|
||||||
}
|
|
||||||
return newPanelInfo;
|
|
||||||
}, [panelInfo, firstSpot]);
|
|
||||||
|
|
||||||
// 키워드 클릭 핸들러
|
// 키워드 클릭 핸들러
|
||||||
const handleKeywordClick = useCallback(
|
const handleKeywordClick = useCallback(
|
||||||
(keyword) => {
|
(keyword) => {
|
||||||
@@ -505,7 +463,16 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId, scrollOpt
|
|||||||
setIsSearchOverlayVisible(false);
|
setIsSearchOverlayVisible(false);
|
||||||
setInputFocus(false);
|
setInputFocus(false);
|
||||||
},
|
},
|
||||||
[handleSearchSubmit, dispatch]
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
[handleSearchSubmit]
|
||||||
|
);
|
||||||
|
|
||||||
|
// 키워드 클릭 핸들러 생성 함수
|
||||||
|
const createKeywordClickHandler = useCallback(
|
||||||
|
(keyword) => {
|
||||||
|
return () => handleKeywordClick(keyword);
|
||||||
|
},
|
||||||
|
[handleKeywordClick]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleKeywordInput = (keyword) => {
|
const handleKeywordInput = (keyword) => {
|
||||||
@@ -514,10 +481,33 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId, scrollOpt
|
|||||||
setInputFocus(false);
|
setInputFocus(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 상품 클릭 핸들러
|
// TInput icon click handler
|
||||||
const handleProductClick = useCallback((product) => {
|
const handleInputIconClick = useCallback(() => {
|
||||||
// 상품 상세 페이지로 이동하는 로직 구현
|
if (showVirtualKeyboard) {
|
||||||
console.log('Product clicked:', product);
|
handleSearchSubmit(searchQuery);
|
||||||
|
} else {
|
||||||
|
setShowVirtualKeyboard(true);
|
||||||
|
}
|
||||||
|
}, [showVirtualKeyboard, handleSearchSubmit, searchQuery]);
|
||||||
|
|
||||||
|
// Microphone button keydown handler
|
||||||
|
const handleMicKeyDown = useCallback(
|
||||||
|
(e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
onClickMic();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[onClickMic]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Voice overlay close handler
|
||||||
|
const handleVoiceOverlayClose = useCallback(() => {
|
||||||
|
setIsVoiceOverlayVisible(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Search overlay close handler
|
||||||
|
const handleSearchOverlayClose = useCallback(() => {
|
||||||
|
setIsSearchOverlayVisible(false);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 테스트용 Toast 핸들러들
|
// 테스트용 Toast 핸들러들
|
||||||
@@ -573,7 +563,7 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId, scrollOpt
|
|||||||
</SpottableProduct>
|
</SpottableProduct>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[]
|
[hotPicks]
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -587,7 +577,7 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId, scrollOpt
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<TPanel className={css.container} handleCancel={onCancel} spotlightId={spotlightId}>
|
<TPanel className={css.container} handleCancel={onCancel} spotlightId={spotlightId}>
|
||||||
<TBody className={css.tBody} scrollable={true} spotlightDisabled={!isOnTop}>
|
<TBody className={css.tBody} scrollable spotlightDisabled={!isOnTop}>
|
||||||
<ContainerBasic>
|
<ContainerBasic>
|
||||||
{isOnTop && (
|
{isOnTop && (
|
||||||
<TVerticalPagenator
|
<TVerticalPagenator
|
||||||
@@ -598,7 +588,7 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId, scrollOpt
|
|||||||
onFocusedContainerId={onFocusedContainerId}
|
onFocusedContainerId={onFocusedContainerId}
|
||||||
cbChangePageRef={cbChangePageRef}
|
cbChangePageRef={cbChangePageRef}
|
||||||
topMargin={36}
|
topMargin={36}
|
||||||
scrollable={true}
|
scrollable
|
||||||
>
|
>
|
||||||
{/* 검색 내용있을때 검색 부분 */}
|
{/* 검색 내용있을때 검색 부분 */}
|
||||||
{/* 검색 입력 영역 - overlay 열릴 때 숨김 (visibility로 처리) */}
|
{/* 검색 입력 영역 - overlay 열릴 때 숨김 (visibility로 처리) */}
|
||||||
@@ -609,7 +599,7 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId, scrollOpt
|
|||||||
searchDatas && css.searchValue /* 이건 결과값 있을때만. 조건 추가필요 */,
|
searchDatas && css.searchValue /* 이건 결과값 있을때만. 조건 추가필요 */,
|
||||||
(isVoiceOverlayVisible || isSearchOverlayVisible) && css.hidden
|
(isVoiceOverlayVisible || isSearchOverlayVisible) && css.hidden
|
||||||
)}
|
)}
|
||||||
data-wheel-point={true}
|
data-wheel-point="true"
|
||||||
spotlightId={SPOTLIGHT_IDS.SEARCH_INPUT_LAYER}
|
spotlightId={SPOTLIGHT_IDS.SEARCH_INPUT_LAYER}
|
||||||
>
|
>
|
||||||
<div className={css.searchInputWrapper}>
|
<div className={css.searchInputWrapper}>
|
||||||
@@ -625,19 +615,13 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId, scrollOpt
|
|||||||
icon={ICONS.search}
|
icon={ICONS.search}
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onChange={handleSearchChange}
|
onChange={handleSearchChange}
|
||||||
onIconClick={() => {
|
onIconClick={handleInputIconClick}
|
||||||
if (showVirtualKeyboard) {
|
|
||||||
handleSearchSubmit(searchQuery);
|
|
||||||
} else {
|
|
||||||
setShowVirtualKeyboard(true);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onKeyDown={handleKeydown}
|
onKeyDown={handleKeydown}
|
||||||
onKeyUp={cursorPosition}
|
onKeyUp={cursorPosition}
|
||||||
spotlightId={SPOTLIGHT_IDS.SEARCH_INPUT_BOX}
|
spotlightId={SPOTLIGHT_IDS.SEARCH_INPUT_BOX}
|
||||||
forcedSpotlight="recent-keyword-0"
|
forcedSpotlight="recent-keyword-0"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
spotlightBoxDisabled={true}
|
spotlightBoxDisabled
|
||||||
onFocus={_onFocus}
|
onFocus={_onFocus}
|
||||||
onBlur={_onBlur}
|
onBlur={_onBlur}
|
||||||
onInputModeChange={handleInputModeChange}
|
onInputModeChange={handleInputModeChange}
|
||||||
@@ -647,11 +631,7 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId, scrollOpt
|
|||||||
className={css.microphoneButton}
|
className={css.microphoneButton}
|
||||||
onClick={onClickMic}
|
onClick={onClickMic}
|
||||||
onFocus={onFocusMic}
|
onFocus={onFocusMic}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={handleMicKeyDown}
|
||||||
if (e.key === 'Enter') {
|
|
||||||
onClickMic();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
spotlightId={SPOTLIGHT_IDS.MICROPHONE_BUTTON}
|
spotlightId={SPOTLIGHT_IDS.MICROPHONE_BUTTON}
|
||||||
>
|
>
|
||||||
<div className={css.microphoneCircle}>
|
<div className={css.microphoneCircle}>
|
||||||
@@ -714,11 +694,11 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId, scrollOpt
|
|||||||
{/* 최근 검색어 섹션 */}
|
{/* 최근 검색어 섹션 */}
|
||||||
<SectionContainer
|
<SectionContainer
|
||||||
className={css.section}
|
className={css.section}
|
||||||
data-wheel-point={true}
|
data-wheel-point="true"
|
||||||
spotlightId={SPOTLIGHT_IDS.RECENT_SEARCHES_SECTION}
|
spotlightId={SPOTLIGHT_IDS.RECENT_SEARCHES_SECTION}
|
||||||
>
|
>
|
||||||
<div className={css.sectionHeader}>
|
<div className={css.sectionHeader}>
|
||||||
<div className={css.sectionIndicator}></div>
|
<div className={css.sectionIndicator} />
|
||||||
<div className={css.sectionTitle}>Your Recent Searches</div>
|
<div className={css.sectionTitle}>Your Recent Searches</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={css.keywordList}>
|
<div className={css.keywordList}>
|
||||||
@@ -726,7 +706,7 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId, scrollOpt
|
|||||||
<SpottableKeyword
|
<SpottableKeyword
|
||||||
key={`recent-${index}`}
|
key={`recent-${index}`}
|
||||||
className={css.keywordButton}
|
className={css.keywordButton}
|
||||||
onClick={() => handleKeywordClick(keyword)}
|
onClick={createKeywordClickHandler(keyword)}
|
||||||
spotlightId={`recent-keyword-${index}`}
|
spotlightId={`recent-keyword-${index}`}
|
||||||
>
|
>
|
||||||
{keyword}
|
{keyword}
|
||||||
@@ -738,11 +718,11 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId, scrollOpt
|
|||||||
{/* 인기 검색어 섹션 */}
|
{/* 인기 검색어 섹션 */}
|
||||||
<SectionContainer
|
<SectionContainer
|
||||||
className={css.section}
|
className={css.section}
|
||||||
data-wheel-point={true}
|
data-wheel-point="true"
|
||||||
spotlightId={SPOTLIGHT_IDS.TOP_SEARCHES_SECTION}
|
spotlightId={SPOTLIGHT_IDS.TOP_SEARCHES_SECTION}
|
||||||
>
|
>
|
||||||
<div className={css.sectionHeader}>
|
<div className={css.sectionHeader}>
|
||||||
<div className={css.sectionIndicator}></div>
|
<div className={css.sectionIndicator} />
|
||||||
<div className={css.sectionTitle}>Top Searches</div>
|
<div className={css.sectionTitle}>Top Searches</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={css.keywordList}>
|
<div className={css.keywordList}>
|
||||||
@@ -750,7 +730,7 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId, scrollOpt
|
|||||||
<SpottableKeyword
|
<SpottableKeyword
|
||||||
key={`top-${index}`}
|
key={`top-${index}`}
|
||||||
className={css.keywordButton}
|
className={css.keywordButton}
|
||||||
onClick={() => handleKeywordClick(keyword)}
|
onClick={createKeywordClickHandler(keyword)}
|
||||||
spotlightId={`top-keyword-${index}`}
|
spotlightId={`top-keyword-${index}`}
|
||||||
>
|
>
|
||||||
{keyword}
|
{keyword}
|
||||||
@@ -762,11 +742,11 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId, scrollOpt
|
|||||||
{/* 인기 브랜드 섹션 */}
|
{/* 인기 브랜드 섹션 */}
|
||||||
<SectionContainer
|
<SectionContainer
|
||||||
className={css.section}
|
className={css.section}
|
||||||
data-wheel-point={true}
|
data-wheel-point="true"
|
||||||
spotlightId={SPOTLIGHT_IDS.POPULAR_BRANDS_SECTION}
|
spotlightId={SPOTLIGHT_IDS.POPULAR_BRANDS_SECTION}
|
||||||
>
|
>
|
||||||
<div className={css.sectionHeader}>
|
<div className={css.sectionHeader}>
|
||||||
<div className={css.sectionIndicator}></div>
|
<div className={css.sectionIndicator} />
|
||||||
<div className={css.sectionTitle}>Popular Brands</div>
|
<div className={css.sectionTitle}>Popular Brands</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={css.keywordList}>
|
<div className={css.keywordList}>
|
||||||
@@ -774,7 +754,7 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId, scrollOpt
|
|||||||
<SpottableKeyword
|
<SpottableKeyword
|
||||||
key={`brand-${index}`}
|
key={`brand-${index}`}
|
||||||
className={css.keywordButton}
|
className={css.keywordButton}
|
||||||
onClick={() => handleKeywordClick(brand)}
|
onClick={createKeywordClickHandler(brand)}
|
||||||
spotlightId={`brand-${index}`}
|
spotlightId={`brand-${index}`}
|
||||||
>
|
>
|
||||||
{brand}
|
{brand}
|
||||||
@@ -786,11 +766,11 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId, scrollOpt
|
|||||||
{/* Hot Picks for You 섹션 */}
|
{/* Hot Picks for You 섹션 */}
|
||||||
<SectionContainer
|
<SectionContainer
|
||||||
className={css.hotpicksSection}
|
className={css.hotpicksSection}
|
||||||
data-wheel-point={true}
|
data-wheel-point="true"
|
||||||
spotlightId={SPOTLIGHT_IDS.HOT_PICKS_SECTION}
|
spotlightId={SPOTLIGHT_IDS.HOT_PICKS_SECTION}
|
||||||
>
|
>
|
||||||
<div className={css.sectionHeader}>
|
<div className={css.sectionHeader}>
|
||||||
<div className={css.sectionIndicator}></div>
|
<div className={css.sectionIndicator} />
|
||||||
<div className={css.sectionTitle}>Hot Picks for You</div>
|
<div className={css.sectionTitle}>Hot Picks for You</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={css.productList}>
|
<div className={css.productList}>
|
||||||
@@ -842,8 +822,8 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId, scrollOpt
|
|||||||
{isVoiceOverlayVisible && (
|
{isVoiceOverlayVisible && (
|
||||||
<VoiceInputOverlay
|
<VoiceInputOverlay
|
||||||
isVisible={isVoiceOverlayVisible}
|
isVisible={isVoiceOverlayVisible}
|
||||||
onClose={() => setIsVoiceOverlayVisible(false)}
|
onClose={handleVoiceOverlayClose}
|
||||||
mode={voiceMode}
|
mode={VOICE_MODES.PROMPT}
|
||||||
suggestions={voiceSuggestions}
|
suggestions={voiceSuggestions}
|
||||||
searchQuery={searchQuery}
|
searchQuery={searchQuery}
|
||||||
onSearchChange={handleSearchChange}
|
onSearchChange={handleSearchChange}
|
||||||
@@ -853,7 +833,7 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId, scrollOpt
|
|||||||
{isSearchOverlayVisible && (
|
{isSearchOverlayVisible && (
|
||||||
<SearchInputOverlay
|
<SearchInputOverlay
|
||||||
isVisible={isSearchOverlayVisible}
|
isVisible={isSearchOverlayVisible}
|
||||||
onClose={() => setIsSearchOverlayVisible(false)}
|
onClose={handleSearchOverlayClose}
|
||||||
searchQuery={searchQuery}
|
searchQuery={searchQuery}
|
||||||
onSearchChange={handleSearchChange}
|
onSearchChange={handleSearchChange}
|
||||||
onSearchSubmit={handleSearchSubmit}
|
onSearchSubmit={handleSearchSubmit}
|
||||||
|
|||||||
@@ -1,27 +1,15 @@
|
|||||||
import React, {
|
import React, { useCallback, useMemo, useRef, useState } from 'react';
|
||||||
useCallback,
|
|
||||||
useMemo,
|
|
||||||
useRef,
|
|
||||||
useState,
|
|
||||||
} from 'react';
|
|
||||||
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { useDispatch } from 'react-redux';
|
|
||||||
|
|
||||||
import SpotlightContainerDecorator
|
|
||||||
from '@enact/spotlight/SpotlightContainerDecorator';
|
|
||||||
import Spottable from '@enact/spotlight/Spottable';
|
import Spottable from '@enact/spotlight/Spottable';
|
||||||
|
|
||||||
import downBtnImg from '../../../assets/images/btn/search_btn_down_arrow.png';
|
import downBtnImg from '../../../assets/images/btn/search_btn_down_arrow.png';
|
||||||
import upBtnImg from '../../../assets/images/btn/search_btn_up_arrow.png';
|
import upBtnImg from '../../../assets/images/btn/search_btn_up_arrow.png';
|
||||||
import hotPicksImage from '../../../assets/images/searchpanel/img-hotpicks.png';
|
|
||||||
import hotPicksBrandImage
|
|
||||||
from '../../../assets/images/searchpanel/img-search-hotpicks.png';
|
|
||||||
import CustomImage from '../../components/CustomImage/CustomImage';
|
import CustomImage from '../../components/CustomImage/CustomImage';
|
||||||
import TButtonTab, { LIST_TYPE } from '../../components/TButtonTab/TButtonTab';
|
import TButtonTab, { LIST_TYPE } from '../../components/TButtonTab/TButtonTab';
|
||||||
import TDropDown from '../../components/TDropDown/TDropDown';
|
import TDropDown from '../../components/TDropDown/TDropDown';
|
||||||
import TVirtualGridList
|
import TVirtualGridList from '../../components/TVirtualGridList/TVirtualGridList';
|
||||||
from '../../components/TVirtualGridList/TVirtualGridList';
|
|
||||||
import { $L } from '../../utils/helperMethods';
|
import { $L } from '../../utils/helperMethods';
|
||||||
import { SpotlightIds } from '../../utils/SpotlightIds';
|
import { SpotlightIds } from '../../utils/SpotlightIds';
|
||||||
import css from './SearchResults.new.module.less';
|
import css from './SearchResults.new.module.less';
|
||||||
@@ -30,39 +18,29 @@ import ShowCard from './SearchResultsNew/ShowCard';
|
|||||||
|
|
||||||
const ITEMS_PER_PAGE = 10;
|
const ITEMS_PER_PAGE = 10;
|
||||||
|
|
||||||
const SearchResultsNew = ({
|
const SearchResultsNew = ({ itemInfo, showInfo, themeInfo, shopperHouseInfo, keywordClick }) => {
|
||||||
itemInfo,
|
|
||||||
showInfo,
|
|
||||||
themeInfo,
|
|
||||||
shopperHouseInfo,
|
|
||||||
keywordClick,
|
|
||||||
}) => {
|
|
||||||
// ShopperHouse 데이터를 ItemCard 형식으로 변환
|
// ShopperHouse 데이터를 ItemCard 형식으로 변환
|
||||||
const convertedShopperHouseItems = useMemo(() => {
|
const convertedShopperHouseItems = useMemo(() => {
|
||||||
if (
|
if (!shopperHouseInfo || !shopperHouseInfo.results || shopperHouseInfo.results.length === 0) {
|
||||||
!shopperHouseInfo ||
|
|
||||||
!shopperHouseInfo.results ||
|
|
||||||
shopperHouseInfo.results.length === 0
|
|
||||||
) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const docs = shopperHouseInfo.results[0].docs || [];
|
const docs = shopperHouseInfo.results[0].docs || [];
|
||||||
return docs.map((doc, index) => {
|
return docs.map((doc) => {
|
||||||
const contentId = doc.contentId;
|
const contentId = doc.contentId;
|
||||||
const tokens = contentId.split("_");
|
const tokens = contentId.split('_');
|
||||||
const patnrId = tokens?.[4] || "";
|
const patnrId = tokens?.[4] || '';
|
||||||
const prdtId = tokens?.[5] || "";
|
const prdtId = tokens?.[5] || '';
|
||||||
|
|
||||||
return {
|
return {
|
||||||
thumbnail: doc.thumnail || doc.imgPath || "", //이미지 경로
|
thumbnail: doc.thumnail || doc.imgPath || '', //이미지 경로
|
||||||
title: doc.title || doc.prdtName || "", // 제목
|
title: doc.title || doc.prdtName || '', // 제목
|
||||||
dcPrice: doc.dcPrice || doc.price || "", // 할인가격
|
dcPrice: doc.dcPrice || doc.price || '', // 할인가격
|
||||||
price: doc.orgPrice || doc.price || "", // 원가
|
price: doc.orgPrice || doc.price || '', // 원가
|
||||||
soldout: doc.soldout || false, // 품절 여부
|
soldout: doc.soldout || false, // 품절 여부
|
||||||
contentId, //콘텐트 아이디
|
contentId, //콘텐트 아이디
|
||||||
reviewGrade: doc.reviewGrade || "", //리뷰 점수 (추가 정보)
|
reviewGrade: doc.reviewGrade || '', //리뷰 점수 (추가 정보)
|
||||||
partnerName: doc.partnerName || "", //파트너 네임
|
partnerName: doc.partnerName || '', //파트너 네임
|
||||||
patnrId, // 파트너 아이디
|
patnrId, // 파트너 아이디
|
||||||
prdtId, // 상품 아이디
|
prdtId, // 상품 아이디
|
||||||
};
|
};
|
||||||
@@ -70,8 +48,7 @@ const SearchResultsNew = ({
|
|||||||
}, [shopperHouseInfo]);
|
}, [shopperHouseInfo]);
|
||||||
const getButtonTabList = () => {
|
const getButtonTabList = () => {
|
||||||
// ShopperHouse 데이터가 있으면 그것을 사용, 없으면 기존 검색 결과 사용
|
// ShopperHouse 데이터가 있으면 그것을 사용, 없으면 기존 검색 결과 사용
|
||||||
const itemLength =
|
const itemLength = convertedShopperHouseItems?.length || itemInfo?.length || 0;
|
||||||
convertedShopperHouseItems?.length || itemInfo?.length || 0;
|
|
||||||
const showLength = showInfo?.length || 0;
|
const showLength = showInfo?.length || 0;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
@@ -90,7 +67,7 @@ const SearchResultsNew = ({
|
|||||||
const [visibleCount, setVisibleCount] = useState(ITEMS_PER_PAGE);
|
const [visibleCount, setVisibleCount] = useState(ITEMS_PER_PAGE);
|
||||||
|
|
||||||
const [styleChange, setStyleChange] = useState(false);
|
const [styleChange, setStyleChange] = useState(false);
|
||||||
const [filterMethods, setFilterMethods] = useState([]);
|
const filterMethods = [];
|
||||||
const cbChangePageRef = useRef(null);
|
const cbChangePageRef = useRef(null);
|
||||||
|
|
||||||
if (!buttonTabList) {
|
if (!buttonTabList) {
|
||||||
@@ -98,8 +75,7 @@ const SearchResultsNew = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 현재 탭의 데이터 가져오기 - ShopperHouse 데이터 우선
|
// 현재 탭의 데이터 가져오기 - ShopperHouse 데이터 우선
|
||||||
const currentData =
|
const currentData = tab === 0 ? convertedShopperHouseItems || itemInfo : showInfo;
|
||||||
tab === 0 ? convertedShopperHouseItems || itemInfo : showInfo;
|
|
||||||
|
|
||||||
// 표시할 데이터 (처음부터 visibleCount 개수만큼)
|
// 표시할 데이터 (처음부터 visibleCount 개수만큼)
|
||||||
const displayedData = useMemo(() => {
|
const displayedData = useMemo(() => {
|
||||||
@@ -147,8 +123,8 @@ const SearchResultsNew = ({
|
|||||||
[dropDownTab]
|
[dropDownTab]
|
||||||
);
|
);
|
||||||
|
|
||||||
const SpottableLi = Spottable("li");
|
const SpottableLi = Spottable('li');
|
||||||
const SpottableDiv = Spottable("div");
|
const SpottableDiv = Spottable('div');
|
||||||
|
|
||||||
// 맨 처음으로 이동 (위 버튼)
|
// 맨 처음으로 이동 (위 버튼)
|
||||||
const upBtnClick = () => {
|
const upBtnClick = () => {
|
||||||
@@ -167,8 +143,7 @@ const SearchResultsNew = ({
|
|||||||
// ProductCard 컴포넌트
|
// ProductCard 컴포넌트
|
||||||
const renderItem = useCallback(
|
const renderItem = useCallback(
|
||||||
({ index, ...rest }) => {
|
({ index, ...rest }) => {
|
||||||
const { bgImgPath, title, partnerLogo, partnerName, keyword } =
|
const { bgImgPath, title, partnerLogo, partnerName, keyword } = themeInfo[index];
|
||||||
themeInfo[index];
|
|
||||||
return (
|
return (
|
||||||
<SpottableDiv
|
<SpottableDiv
|
||||||
key={`searchProduct-${index}`}
|
key={`searchProduct-${index}`}
|
||||||
@@ -181,17 +156,13 @@ const SearchResultsNew = ({
|
|||||||
</div>
|
</div>
|
||||||
<div className={css.productInfo}>
|
<div className={css.productInfo}>
|
||||||
<div className={css.productBrandWrapper}>
|
<div className={css.productBrandWrapper}>
|
||||||
<img
|
<img src={partnerLogo} alt={partnerName} className={css.brandLogo} />
|
||||||
src={partnerLogo}
|
|
||||||
alt={partnerName}
|
|
||||||
className={css.brandLogo}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className={css.productDetails}>
|
<div className={css.productDetails}>
|
||||||
{keyword && (
|
{keyword && (
|
||||||
<div className={css.brandName}>
|
<div className={css.brandName}>
|
||||||
{keyword.map((item, index) => (
|
{keyword.map((item, keywordIndex) => (
|
||||||
<span key={index}># {item}</span>
|
<span key={keywordIndex}># {item}</span>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -209,37 +180,25 @@ const SearchResultsNew = ({
|
|||||||
<div className={css.topBox}>
|
<div className={css.topBox}>
|
||||||
<span className={css.topBoxTitle}>How about these?</span>
|
<span className={css.topBoxTitle}>How about these?</span>
|
||||||
<ul className={css.topBoxList}>
|
<ul className={css.topBoxList}>
|
||||||
<SpottableLi
|
{[
|
||||||
className={css.topBoxListItem}
|
{ text: 'Puppy food', key: 'puppy-food' },
|
||||||
onClick={() => keywordClick("Puppy food")}
|
{ text: 'Dog toy', key: 'dog-toy' },
|
||||||
>
|
{ text: 'Fitness', key: 'fitness' },
|
||||||
Puppy food
|
].map(({ text, key }) => {
|
||||||
</SpottableLi>
|
const handleClick = () => keywordClick(text);
|
||||||
<SpottableLi
|
return (
|
||||||
className={css.topBoxListItem}
|
<SpottableLi key={key} className={css.topBoxListItem} onClick={handleClick}>
|
||||||
onClick={() => keywordClick("Dog toy")}
|
{text}
|
||||||
>
|
</SpottableLi>
|
||||||
Dog toy
|
);
|
||||||
</SpottableLi>
|
})}
|
||||||
<SpottableLi
|
|
||||||
className={css.topBoxListItem}
|
|
||||||
onClick={() => keywordClick("Fitness")}
|
|
||||||
>
|
|
||||||
Fitness
|
|
||||||
</SpottableLi>
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
{themeInfo && themeInfo?.length > 0 && (
|
{themeInfo && themeInfo?.length > 0 && (
|
||||||
<div
|
<div className={css.hotpicksSection} data-wheel-point="true">
|
||||||
className={css.hotpicksSection}
|
|
||||||
data-wheel-point={true}
|
|
||||||
spotlightId={"hot-picks-section"}
|
|
||||||
>
|
|
||||||
<div className={css.sectionHeader}>
|
<div className={css.sectionHeader}>
|
||||||
<div className={css.sectionIndicator}></div>
|
<div className={css.sectionIndicator} />
|
||||||
<div className={css.sectionTitle}>
|
<div className={css.sectionTitle}>Hot Picks ({themeInfo?.length})</div>
|
||||||
Hot Picks ({themeInfo?.length})
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className={css.productList}>
|
<div className={css.productList}>
|
||||||
<TVirtualGridList
|
<TVirtualGridList
|
||||||
@@ -253,7 +212,7 @@ const SearchResultsNew = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className={css.itemBox} cbChangePageRef={cbChangePageRef}>
|
<div className={css.itemBox}>
|
||||||
<div className={css.tabContainer}>
|
<div className={css.tabContainer}>
|
||||||
<TButtonTab
|
<TButtonTab
|
||||||
contents={buttonTabList}
|
contents={buttonTabList}
|
||||||
@@ -284,11 +243,11 @@ const SearchResultsNew = ({
|
|||||||
<div className={css.buttonContainer}>
|
<div className={css.buttonContainer}>
|
||||||
{hasMore && (
|
{hasMore && (
|
||||||
<SpottableDiv onClick={downBtnClick} className={css.downBtn}>
|
<SpottableDiv onClick={downBtnClick} className={css.downBtn}>
|
||||||
<CustomImage className={css.btnImg} src={downBtnImg} />
|
<CustomImage className={css.btnImg} src={downBtnImg} alt="Down arrow" />
|
||||||
</SpottableDiv>
|
</SpottableDiv>
|
||||||
)}
|
)}
|
||||||
<SpottableDiv onClick={upBtnClick} className={css.upBtn}>
|
<SpottableDiv onClick={upBtnClick} className={css.upBtn}>
|
||||||
<CustomImage className={css.btnImg} src={upBtnImg} />
|
<CustomImage className={css.btnImg} src={upBtnImg} alt="Up arrow" />
|
||||||
</SpottableDiv>
|
</SpottableDiv>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -14,7 +14,9 @@ const ItemCard = ({ onClick, itemInfo }) => {
|
|||||||
|
|
||||||
const _handleItemClick = useCallback(
|
const _handleItemClick = useCallback(
|
||||||
(patnrId, prdtId) => (ev) => {
|
(patnrId, prdtId) => (ev) => {
|
||||||
onClick && onClick(ev);
|
if (onClick) {
|
||||||
|
onClick(ev);
|
||||||
|
}
|
||||||
dispatch(
|
dispatch(
|
||||||
pushPanel({
|
pushPanel({
|
||||||
name: panel_names.DETAIL_PANEL,
|
name: panel_names.DETAIL_PANEL,
|
||||||
@@ -25,20 +27,17 @@ const ItemCard = ({ onClick, itemInfo }) => {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[onClick]
|
[onClick, dispatch]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<TScroller
|
<TScroller className={css.container} spotlightId={SpotlightIds.SEARCH_ITEM}>
|
||||||
className={css.container}
|
|
||||||
spotlightId={SpotlightIds.SEARCH_ITEM}
|
|
||||||
>
|
|
||||||
{itemInfo.map((item, index) => {
|
{itemInfo.map((item, index) => {
|
||||||
const { thumbnail, title, dcPrice, price, soldout, contentId } = item;
|
const { thumbnail, title, dcPrice, price, soldout, contentId } = item;
|
||||||
const tokens = contentId && contentId.split("_");
|
const tokens = contentId && contentId.split('_');
|
||||||
const patnrId = tokens?.[4] || "";
|
const patnrId = tokens?.[4] || '';
|
||||||
const prdtId = tokens?.[5] || "";
|
const prdtId = tokens?.[5] || '';
|
||||||
return (
|
return (
|
||||||
<TItemCardNew
|
<TItemCardNew
|
||||||
key={prdtId}
|
key={prdtId}
|
||||||
@@ -50,8 +49,8 @@ const ItemCard = ({ onClick, itemInfo }) => {
|
|||||||
soldoutFlag={soldout}
|
soldoutFlag={soldout}
|
||||||
dcPrice={dcPrice}
|
dcPrice={dcPrice}
|
||||||
originPrice={price}
|
originPrice={price}
|
||||||
spotlightId={"searchItemContents" + index}
|
spotlightId={'searchItemContents' + index}
|
||||||
label={index * 1 + 1 + " of " + itemInfo.length + 1}
|
label={index * 1 + 1 + ' of ' + itemInfo.length + 1}
|
||||||
lastLabel=" go to detail, button"
|
lastLabel=" go to detail, button"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,29 +2,25 @@ import React, { useCallback } from 'react';
|
|||||||
|
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
|
|
||||||
import SpotlightContainerDecorator
|
import SpotlightContainerDecorator from '@enact/spotlight/SpotlightContainerDecorator';
|
||||||
from '@enact/spotlight/SpotlightContainerDecorator';
|
|
||||||
|
|
||||||
import { pushPanel } from '../../../actions/panelActions';
|
import { pushPanel } from '../../../actions/panelActions';
|
||||||
import TItemCardNew, {
|
import TItemCardNew, { TYPES } from '../../../components/TItemCard/TItemCard.new';
|
||||||
IMAGETYPES,
|
|
||||||
TYPES,
|
|
||||||
} from '../../../components/TItemCard/TItemCard.new';
|
|
||||||
import { panel_names } from '../../../utils/Config';
|
import { panel_names } from '../../../utils/Config';
|
||||||
import { SpotlightIds } from '../../../utils/SpotlightIds';
|
import { SpotlightIds } from '../../../utils/SpotlightIds';
|
||||||
import css from './ShowCard.module.less';
|
import css from './ShowCard.module.less';
|
||||||
|
|
||||||
const Container = SpotlightContainerDecorator({ enterTo: null }, "div");
|
const Container = SpotlightContainerDecorator({ enterTo: null }, 'div');
|
||||||
const ShowCard = ({ onClick, showInfo }) => {
|
const ShowCard = ({ onClick, showInfo }) => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
const _onClick = useCallback(
|
const handleClick = useCallback(
|
||||||
(contentId, thumbnail, liveFlag) => {
|
(contentId, thumbnail, liveFlag) => () => {
|
||||||
const tokens = contentId && contentId.split("_");
|
const tokens = contentId && contentId.split('_');
|
||||||
|
|
||||||
const linkTpCd = tokens[1] || "";
|
const linkTpCd = tokens[1] || '';
|
||||||
const patnrId = tokens[4] || "";
|
const patnrId = tokens[4] || '';
|
||||||
const showId = tokens[5] || "";
|
const showId = tokens[5] || '';
|
||||||
|
|
||||||
if (onClick) {
|
if (onClick) {
|
||||||
onClick();
|
onClick();
|
||||||
@@ -39,30 +35,20 @@ const ShowCard = ({ onClick, showInfo }) => {
|
|||||||
chanId: showId,
|
chanId: showId,
|
||||||
linkTpCd,
|
linkTpCd,
|
||||||
thumbnail,
|
thumbnail,
|
||||||
shptmBanrTpNm: liveFlag === "Y" ? "LIVE" : "VOD",
|
shptmBanrTpNm: liveFlag === 'Y' ? 'LIVE' : 'VOD',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[onClick]
|
[onClick, dispatch]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container className={css.container} spotlightId={SpotlightIds.SEARCH_SHOW}>
|
<Container className={css.container} spotlightId={SpotlightIds.SEARCH_SHOW}>
|
||||||
{showInfo.map((item, index) => {
|
{showInfo.map((item, index) => {
|
||||||
const {
|
const { contentId, liveFlag, partnerLogo, thumbnail, title } = item;
|
||||||
contentId,
|
const tokkens = contentId && contentId.split('_');
|
||||||
endTime,
|
const showId = tokkens[5] || '';
|
||||||
liveFlag,
|
|
||||||
partnerId,
|
|
||||||
partnerLogo,
|
|
||||||
partnerName,
|
|
||||||
startTime,
|
|
||||||
thumbnail,
|
|
||||||
title,
|
|
||||||
} = item;
|
|
||||||
const tokkens = contentId && contentId.split("_");
|
|
||||||
const showId = tokkens[5] || "";
|
|
||||||
return (
|
return (
|
||||||
<TItemCardNew
|
<TItemCardNew
|
||||||
type={TYPES.videoShow}
|
type={TYPES.videoShow}
|
||||||
@@ -71,11 +57,9 @@ const ShowCard = ({ onClick, showInfo }) => {
|
|||||||
imageSource={thumbnail}
|
imageSource={thumbnail}
|
||||||
productName={title}
|
productName={title}
|
||||||
logo={partnerLogo}
|
logo={partnerLogo}
|
||||||
onClick={() => {
|
onClick={handleClick(contentId, thumbnail, liveFlag)}
|
||||||
_onClick(contentId, thumbnail, liveFlag);
|
|
||||||
}}
|
|
||||||
productId={showId}
|
productId={showId}
|
||||||
spotlightId={"categoryShowContents" + index}
|
spotlightId={'categoryShowContents' + index}
|
||||||
data-wheel-point="true"
|
data-wheel-point="true"
|
||||||
logoDisplay="true"
|
logoDisplay="true"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -19,13 +19,9 @@ const Container = Spottable('div');
|
|||||||
export default function TInput({
|
export default function TInput({
|
||||||
kind,
|
kind,
|
||||||
icon,
|
icon,
|
||||||
border,
|
|
||||||
color,
|
|
||||||
className,
|
className,
|
||||||
spotlightDisabled,
|
spotlightDisabled,
|
||||||
spotlightBoxDisabled,
|
|
||||||
spotlightId,
|
spotlightId,
|
||||||
disabled,
|
|
||||||
onKeyDown,
|
onKeyDown,
|
||||||
scrollTop,
|
scrollTop,
|
||||||
onIconClick,
|
onIconClick,
|
||||||
@@ -79,13 +75,13 @@ export default function TInput({
|
|||||||
onFocus();
|
onFocus();
|
||||||
}
|
}
|
||||||
handleScrollReset();
|
handleScrollReset();
|
||||||
}, [onFocus]);
|
}, [onFocus, handleScrollReset]);
|
||||||
const _onBlur = useCallback(() => {
|
const _onBlur = useCallback(() => {
|
||||||
if (onBlur) {
|
if (onBlur) {
|
||||||
onBlur();
|
onBlur();
|
||||||
}
|
}
|
||||||
handleStopScrolling();
|
handleStopScrolling();
|
||||||
}, [onBlur]);
|
}, [onBlur, handleStopScrolling]);
|
||||||
|
|
||||||
// onActivate: 내부 input에 실제로 포커스가 가서 입력 가능한 상태 (webOS 키보드가 뜨는 시점)
|
// onActivate: 내부 input에 실제로 포커스가 가서 입력 가능한 상태 (webOS 키보드가 뜨는 시점)
|
||||||
const _onActivate = useCallback(() => {
|
const _onActivate = useCallback(() => {
|
||||||
|
|||||||
@@ -63,7 +63,6 @@ export const VOICE_VERSION = {
|
|||||||
const OVERLAY_SPOTLIGHT_ID = 'voice-input-overlay-container';
|
const OVERLAY_SPOTLIGHT_ID = 'voice-input-overlay-container';
|
||||||
const INPUT_SPOTLIGHT_ID = 'voice-overlay-input-box';
|
const INPUT_SPOTLIGHT_ID = 'voice-overlay-input-box';
|
||||||
const MIC_SPOTLIGHT_ID = 'voice-overlay-mic-button';
|
const MIC_SPOTLIGHT_ID = 'voice-overlay-mic-button';
|
||||||
const MIC_WEBSPEECH_SPOTLIGHT_ID = 'voice-overlay-mic-webspeech-button';
|
|
||||||
|
|
||||||
// 🔧 실험적 기능: Wake Word Detection ("Hey Shoptime")
|
// 🔧 실험적 기능: Wake Word Detection ("Hey Shoptime")
|
||||||
// false로 설정하면 이 기능은 완전히 비활성화됩니다
|
// false로 설정하면 이 기능은 완전히 비활성화됩니다
|
||||||
@@ -93,7 +92,6 @@ const VoiceInputOverlay = ({
|
|||||||
suggestions = [],
|
suggestions = [],
|
||||||
searchQuery = '',
|
searchQuery = '',
|
||||||
onSearchChange,
|
onSearchChange,
|
||||||
onSearchSubmit,
|
|
||||||
}) => {
|
}) => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const lastFocusedElement = useRef(null);
|
const lastFocusedElement = useRef(null);
|
||||||
@@ -117,7 +115,6 @@ const VoiceInputOverlay = ({
|
|||||||
wakeWordRestartTimerRef,
|
wakeWordRestartTimerRef,
|
||||||
];
|
];
|
||||||
|
|
||||||
const [inputFocus, setInputFocus] = useState(false);
|
|
||||||
const [micFocused, setMicFocused] = useState(false);
|
const [micFocused, setMicFocused] = useState(false);
|
||||||
// 내부 모드 상태 관리 (prompt -> listening -> response -> close)
|
// 내부 모드 상태 관리 (prompt -> listening -> response -> close)
|
||||||
const [currentMode, setCurrentMode] = useState(mode);
|
const [currentMode, setCurrentMode] = useState(mode);
|
||||||
@@ -126,7 +123,7 @@ const VoiceInputOverlay = ({
|
|||||||
// STT 응답 텍스트 저장
|
// STT 응답 텍스트 저장
|
||||||
const [sttResponseText, setSttResponseText] = useState('');
|
const [sttResponseText, setSttResponseText] = useState('');
|
||||||
// Voice Version (어떤 음성 시스템을 사용할지 결정)
|
// Voice Version (어떤 음성 시스템을 사용할지 결정)
|
||||||
const [voiceVersion, setVoiceVersion] = useState(VOICE_VERSION.WEB_SPEECH);
|
const voiceVersion = VOICE_VERSION.WEB_SPEECH;
|
||||||
|
|
||||||
// 🔊 Beep 소리 재생 함수 - zero dependencies
|
// 🔊 Beep 소리 재생 함수 - zero dependencies
|
||||||
const playBeep = useCallback(() => {
|
const playBeep = useCallback(() => {
|
||||||
@@ -222,16 +219,15 @@ const VoiceInputOverlay = ({
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const { isListening, interimText, startListening, stopListening, error, isSupported } =
|
const { isListening, interimText, startListening, stopListening, isSupported } = useWebSpeech(
|
||||||
useWebSpeech(
|
isVisible, // Overlay가 열려있을 때만 활성화 (voiceInputMode와 무관하게 초기화)
|
||||||
isVisible, // Overlay가 열려있을 때만 활성화 (voiceInputMode와 무관하게 초기화)
|
handleWebSpeechSTT,
|
||||||
handleWebSpeechSTT,
|
{
|
||||||
{
|
lang: 'en-US',
|
||||||
lang: 'en-US',
|
continuous: false, // 침묵 감지 후 자동 종료
|
||||||
continuous: false, // 침묵 감지 후 자동 종료
|
interimResults: true,
|
||||||
interimResults: true,
|
}
|
||||||
}
|
);
|
||||||
);
|
|
||||||
|
|
||||||
// ⛔ VUI 테스트 비활성화: VoicePanel 독립 테스트 시 충돌 방지
|
// ⛔ VUI 테스트 비활성화: VoicePanel 독립 테스트 시 충돌 방지
|
||||||
// Redux에서 voice 상태 가져오기
|
// Redux에서 voice 상태 가져오기
|
||||||
@@ -393,7 +389,7 @@ const VoiceInputOverlay = ({
|
|||||||
stopListening();
|
stopListening();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [ENABLE_WAKE_WORD, isVisible, currentMode, startListening, stopListening]);
|
}, [isVisible, currentMode, startListening, stopListening]);
|
||||||
|
|
||||||
// Overlay가 열릴 때 포커스를 overlay 내부로 이동
|
// Overlay가 열릴 때 포커스를 overlay 내부로 이동
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -459,6 +455,7 @@ const VoiceInputOverlay = ({
|
|||||||
audioContextRef.current = null;
|
audioContextRef.current = null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Suggestion 버튼 클릭 핸들러 - Input 창에 텍스트만 설정
|
// Suggestion 버튼 클릭 핸들러 - Input 창에 텍스트만 설정
|
||||||
@@ -615,15 +612,6 @@ const VoiceInputOverlay = ({
|
|||||||
handleTalkAgain,
|
handleTalkAgain,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// 입력창 포커스 핸들러
|
|
||||||
const handleInputFocus = useCallback(() => {
|
|
||||||
setInputFocus(true);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleInputBlur = useCallback(() => {
|
|
||||||
setInputFocus(false);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// 마이크 버튼 포커스 핸들러 (VUI)
|
// 마이크 버튼 포커스 핸들러 (VUI)
|
||||||
const handleMicFocus = useCallback(() => {
|
const handleMicFocus = useCallback(() => {
|
||||||
setMicFocused(true);
|
setMicFocused(true);
|
||||||
@@ -633,52 +621,6 @@ const VoiceInputOverlay = ({
|
|||||||
setMicFocused(false);
|
setMicFocused(false);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// VUI 마이크 버튼 클릭 핸들러 (voiceVersion이 VUI일 때만 작동)
|
|
||||||
const handleVUIMicClick = useCallback(
|
|
||||||
(e) => {
|
|
||||||
// voiceVersion이 VUI가 아니면 차단
|
|
||||||
if (voiceVersion !== VOICE_VERSION.VUI) return;
|
|
||||||
|
|
||||||
if (DEBUG_MODE) {
|
|
||||||
console.log('[VoiceInputOverlay.v2] handleVUIMicClick called, currentMode:', currentMode);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 이벤트 전파 방지 - dim 레이어의 onClick 실행 방지
|
|
||||||
if (e && e.stopPropagation) {
|
|
||||||
e.stopPropagation();
|
|
||||||
}
|
|
||||||
if (e && e.nativeEvent && e.nativeEvent.stopImmediatePropagation) {
|
|
||||||
e.nativeEvent.stopImmediatePropagation();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentMode === VOICE_MODES.PROMPT) {
|
|
||||||
// prompt 모드에서 클릭 시 -> VUI listening 모드로 전환
|
|
||||||
if (DEBUG_MODE) {
|
|
||||||
console.log('[VoiceInputOverlay.v2] Switching to VUI LISTENING mode');
|
|
||||||
}
|
|
||||||
setVoiceInputMode(VOICE_INPUT_MODE.VUI);
|
|
||||||
setCurrentMode(VOICE_MODES.LISTENING);
|
|
||||||
// 이 시점에서 webOS Voice Framework가 자동으로 음성인식 시작
|
|
||||||
// (이미 registerVoiceFramework()로 등록되어 있으므로)
|
|
||||||
} else if (currentMode === VOICE_MODES.LISTENING && voiceInputMode === VOICE_INPUT_MODE.VUI) {
|
|
||||||
// VUI listening 모드에서 클릭 시 -> 종료
|
|
||||||
if (DEBUG_MODE) {
|
|
||||||
console.log('[VoiceInputOverlay.v2] Closing from VUI LISTENING mode');
|
|
||||||
}
|
|
||||||
setVoiceInputMode(null);
|
|
||||||
onClose();
|
|
||||||
} else {
|
|
||||||
// 기타 모드에서는 바로 종료
|
|
||||||
if (DEBUG_MODE) {
|
|
||||||
console.log('[VoiceInputOverlay.v2] Closing from other mode');
|
|
||||||
}
|
|
||||||
setVoiceInputMode(null);
|
|
||||||
onClose();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[currentMode, voiceInputMode, voiceVersion, onClose]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Overlay 닫기 핸들러 (모든 닫기 동작을 통합)
|
// Overlay 닫기 핸들러 (모든 닫기 동작을 통합)
|
||||||
const handleClose = useCallback(() => {
|
const handleClose = useCallback(() => {
|
||||||
if (DEBUG_MODE) {
|
if (DEBUG_MODE) {
|
||||||
@@ -756,6 +698,18 @@ const VoiceInputOverlay = ({
|
|||||||
[currentMode, handleClose, playBeep, startListening, stopListening]
|
[currentMode, handleClose, playBeep, startListening, stopListening]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 마이크 버튼 키다운 핸들러
|
||||||
|
const handleMicKeyDown = useCallback(
|
||||||
|
(e) => {
|
||||||
|
if (e.key === 'Enter' || e.keyCode === 13) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
handleWebSpeechMicClick(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[handleWebSpeechMicClick]
|
||||||
|
);
|
||||||
|
|
||||||
// Memoize microphone button rendering
|
// Memoize microphone button rendering
|
||||||
const microphoneButton = useMemo(() => {
|
const microphoneButton = useMemo(() => {
|
||||||
if (voiceVersion !== VOICE_VERSION.WEB_SPEECH) return null;
|
if (voiceVersion !== VOICE_VERSION.WEB_SPEECH) return null;
|
||||||
@@ -771,13 +725,7 @@ const VoiceInputOverlay = ({
|
|||||||
micFocused && css.focused
|
micFocused && css.focused
|
||||||
)}
|
)}
|
||||||
onClick={handleWebSpeechMicClick}
|
onClick={handleWebSpeechMicClick}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={handleMicKeyDown}
|
||||||
if (e.key === 'Enter' || e.keyCode === 13) {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
handleWebSpeechMicClick(e);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onFocus={handleMicFocus}
|
onFocus={handleMicFocus}
|
||||||
onBlur={handleMicBlur}
|
onBlur={handleMicBlur}
|
||||||
spotlightId={MIC_SPOTLIGHT_ID}
|
spotlightId={MIC_SPOTLIGHT_ID}
|
||||||
@@ -806,6 +754,7 @@ const VoiceInputOverlay = ({
|
|||||||
voiceInputMode,
|
voiceInputMode,
|
||||||
micFocused,
|
micFocused,
|
||||||
handleWebSpeechMicClick,
|
handleWebSpeechMicClick,
|
||||||
|
handleMicKeyDown,
|
||||||
handleMicFocus,
|
handleMicFocus,
|
||||||
handleMicBlur,
|
handleMicBlur,
|
||||||
]);
|
]);
|
||||||
@@ -845,7 +794,7 @@ const VoiceInputOverlay = ({
|
|||||||
<TFullPopup
|
<TFullPopup
|
||||||
open={isVisible}
|
open={isVisible}
|
||||||
onClose={handleClose}
|
onClose={handleClose}
|
||||||
noAutoDismiss={true}
|
noAutoDismiss
|
||||||
spotlightRestrict="self-only"
|
spotlightRestrict="self-only"
|
||||||
spotlightId={OVERLAY_SPOTLIGHT_ID}
|
spotlightId={OVERLAY_SPOTLIGHT_ID}
|
||||||
noAnimation={false}
|
noAnimation={false}
|
||||||
@@ -877,8 +826,6 @@ const VoiceInputOverlay = ({
|
|||||||
onKeyDown={handleInputKeyDown}
|
onKeyDown={handleInputKeyDown}
|
||||||
onIconClick={handleSearchSubmit}
|
onIconClick={handleSearchSubmit}
|
||||||
spotlightId={INPUT_SPOTLIGHT_ID}
|
spotlightId={INPUT_SPOTLIGHT_ID}
|
||||||
onFocus={handleInputFocus}
|
|
||||||
onBlur={handleInputBlur}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* voiceVersion에 따라 하나의 마이크만 표시 */}
|
{/* voiceVersion에 따라 하나의 마이크만 표시 */}
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ const PromptContainer = SpotlightContainerDecorator(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const VoicePromptScreen = ({ title = 'Try saying', suggestions = [], onSuggestionClick }) => {
|
const VoicePromptScreen = ({ title = 'Try saying', suggestions = [], onSuggestionClick }) => {
|
||||||
const handleBubbleClick = (suggestion, index) => {
|
const handleBubbleClick = (suggestion) => {
|
||||||
console.log(`[VoicePromptScreen] Bubble clicked: ${suggestion}`, index);
|
console.log(`[VoicePromptScreen] Bubble clicked: ${suggestion}`);
|
||||||
|
|
||||||
// 부모 컴포넌트로 suggestion 텍스트 전달 (API 호출은 부모에서 처리)
|
// 부모 컴포넌트로 suggestion 텍스트 전달 (API 호출은 부모에서 처리)
|
||||||
if (onSuggestionClick) {
|
if (onSuggestionClick) {
|
||||||
@@ -33,16 +33,19 @@ const VoicePromptScreen = ({ title = 'Try saying', suggestions = [], onSuggestio
|
|||||||
>
|
>
|
||||||
<div className={css.title}>{title}</div>
|
<div className={css.title}>{title}</div>
|
||||||
<div className={css.suggestionsContainer}>
|
<div className={css.suggestionsContainer}>
|
||||||
{suggestions.map((suggestion, index) => (
|
{suggestions.map((suggestion, index) => {
|
||||||
<SpottableBubble
|
const handleClick = () => handleBubbleClick(suggestion);
|
||||||
key={index}
|
return (
|
||||||
className={css.bubbleMessage}
|
<SpottableBubble
|
||||||
onClick={() => handleBubbleClick(suggestion, index)}
|
key={index}
|
||||||
spotlightId={`voice-bubble-${index}`}
|
className={css.bubbleMessage}
|
||||||
>
|
onClick={handleClick}
|
||||||
<div className={css.bubbleText}>{suggestion}</div>
|
spotlightId={`voice-bubble-${index}`}
|
||||||
</SpottableBubble>
|
>
|
||||||
))}
|
<div className={css.bubbleText}>{suggestion}</div>
|
||||||
|
</SpottableBubble>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</PromptContainer>
|
</PromptContainer>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -46,18 +46,16 @@ const VoiceResponse = ({ responseText = '', onTalkAgain }) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleButtonClick = () => {
|
||||||
|
handleTalkAgainClick();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ResponseContainer className={css.container} spotlightId="voice-response-container">
|
<ResponseContainer className={css.container} spotlightId="voice-response-container">
|
||||||
<div className={css.responseContainer}>
|
<div className={css.responseContainer}>
|
||||||
<SpottableButton
|
<SpottableButton
|
||||||
className={css.talkAgainButton}
|
className={css.talkAgainButton}
|
||||||
onClick={handleTalkAgainClick}
|
onClick={handleButtonClick}
|
||||||
onKeyDown={(e) => {
|
|
||||||
if (e.key === 'Enter' || e.keyCode === 13) {
|
|
||||||
e.preventDefault();
|
|
||||||
handleTalkAgainClick();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
spotlightId="voice-talk-again-button"
|
spotlightId="voice-talk-again-button"
|
||||||
>
|
>
|
||||||
TALK AGAIN
|
TALK AGAIN
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ const ShowUserReviews = ({ hasVideo, launchedFromPlayer }) => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}, [dispatch, productData, reviewData, reviewListData]);
|
}, [dispatch, productData, reviewData, reviewListData, hasVideo, launchedFromPlayer]);
|
||||||
|
|
||||||
const handleKeyDown = useCallback((event) => {
|
const handleKeyDown = useCallback((event) => {
|
||||||
if (event.key === 'ArrowUp') {
|
if (event.key === 'ArrowUp') {
|
||||||
|
|||||||
Reference in New Issue
Block a user