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