[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:
2025-10-19 23:37:30 +09:00
parent edbc7628c7
commit f70e2b1a21
18 changed files with 746 additions and 866 deletions

View File

@@ -42,20 +42,20 @@ import Announce from '@enact/ui/AnnounceDecorator/Announce';
import ComponentOverride from '@enact/ui/ComponentOverride'; import ComponentOverride from '@enact/ui/ComponentOverride';
import { FloatingLayerDecorator } from '@enact/ui/FloatingLayer'; import { FloatingLayerDecorator } from '@enact/ui/FloatingLayer';
import { FloatingLayerContext } from '@enact/ui/FloatingLayer/FloatingLayerDecorator'; import { FloatingLayerContext } from '@enact/ui/FloatingLayer/FloatingLayerDecorator';
import Marquee from '@enact/ui/Marquee'; // import Marquee from '@enact/ui/Marquee';
import Slottable from '@enact/ui/Slottable'; import Slottable from '@enact/ui/Slottable';
import Touchable from '@enact/ui/Touchable'; import Touchable from '@enact/ui/Touchable';
import { panel_names } from '../../utils/Config'; // import { panel_names } from '../../utils/Config';
import { $L } from '../../utils/helperMethods'; import { $L } from '../../utils/helperMethods';
import { SpotlightIds } from '../../utils/SpotlightIds'; import { SpotlightIds } from '../../utils/SpotlightIds';
import Loader from '../Loader/Loader'; import Loader from '../Loader/Loader';
import { MediaControls, MediaSlider, secondsToTime, Times } from '../MediaPlayer'; import { MediaControls, MediaSlider, secondsToTime, Times } from '../MediaPlayer';
import PlayerOverlayContents from '../../views/PlayerPanel/PlayerOverlay/PlayerOverlayContents'; import PlayerOverlayContents from '../../views/PlayerPanel/PlayerOverlay/PlayerOverlayContents';
import FeedbackContent from './FeedbackContent'; import FeedbackContent from './FeedbackContent';
import FeedbackTooltip from './FeedbackTooltip'; // import FeedbackTooltip from './FeedbackTooltip';
import Media from './Media'; import Media from './Media';
import MediaTitle from './MediaTitle'; // import MediaTitle from './MediaTitle';
import Overlay from './Overlay'; import Overlay from './Overlay';
import TReactPlayer from './TReactPlayer'; import TReactPlayer from './TReactPlayer';
import Video from './Video'; import Video from './Video';
@@ -78,7 +78,7 @@ const calcNumberValueOfPlaybackRate = (rate) => {
const pbArray = String(rate).split('/'); const pbArray = String(rate).split('/');
return pbArray.length > 1 ? parseInt(pbArray[0]) / parseInt(pbArray[1]) : parseInt(rate); return pbArray.length > 1 ? parseInt(pbArray[0]) / parseInt(pbArray[1]) : parseInt(rate);
}; };
const SpottableBtn = Spottable('button'); // const SpottableBtn = Spottable('button');
const SpottableDiv = Touchable(Spottable('div')); const SpottableDiv = Touchable(Spottable('div'));
const RootContainer = SpotlightContainerDecorator( const RootContainer = SpotlightContainerDecorator(
{ {
@@ -113,7 +113,7 @@ const getDurFmt = (locale) => {
const forwardWithState = (type) => adaptEvent(call('addStateToEvent'), forwardWithPrevent(type)); const forwardWithState = (type) => adaptEvent(call('addStateToEvent'), forwardWithPrevent(type));
const forwardToggleMore = forward('onToggleMore'); // const forwardToggleMore = forward('onToggleMore');
// provide forwarding of events on media controls // provide forwarding of events on media controls
const forwardControlsAvailable = forward('onControlsAvailable'); const forwardControlsAvailable = forward('onControlsAvailable');
@@ -2001,6 +2001,20 @@ const VideoPlayerBase = class extends React.Component {
})); }));
}; };
onSpotlightFocus = () => {
this.showControls();
if (this.state.lastFocusedTarget) {
setTimeout(() => {
Spotlight.focus(this.state.lastFocusedTarget);
});
} else {
setTimeout(() => {
Spotlight.focus(SpotlightIds.PLAYER_TAB_BUTTON);
});
}
};
slider5WayPressJob = new Job(() => { slider5WayPressJob = new Job(() => {
this.setState({ slider5WayPressed: false }); this.setState({ slider5WayPressed: false });
}, 200); }, 200);
@@ -2087,9 +2101,9 @@ const VideoPlayerBase = class extends React.Component {
className, className,
modalClassName, modalClassName,
disabled, disabled,
infoComponents, // infoComponents,
backButton, // backButton,
promotionTitle, // promotionTitle,
initialJumpDelay, initialJumpDelay,
jumpDelay, jumpDelay,
@@ -2105,14 +2119,14 @@ const VideoPlayerBase = class extends React.Component {
spotlightDisabled, spotlightDisabled,
spotlightId, spotlightId,
style, style,
thumbnailComponent, // thumbnailComponent,
thumbnailSrc, // thumbnailSrc,
title, // title,
introTime, introTime,
onClickSkipIntro, // onClickSkipIntro,
onIntroDisabled, // onIntroDisabled,
videoComponent: VideoComponent, videoComponent: VideoComponent,
cameraSettingsButton, // cameraSettingsButton,
onBackButton, onBackButton,
panelInfo, panelInfo,
selectedIndex, selectedIndex,
@@ -2129,18 +2143,18 @@ const VideoPlayerBase = class extends React.Component {
disclaimer, disclaimer,
liveTotalTime, liveTotalTime,
currentLiveTimeSeconds, currentLiveTimeSeconds,
themeProductInfos, // themeProductInfos,
detailThemeProductImageLength, // detailThemeProductImageLength,
videoVerticalVisible, videoVerticalVisible,
handleIndicatorDownClick, handleIndicatorDownClick,
handleIndicatorUpClick, handleIndicatorUpClick,
orderPhnNo, // orderPhnNo,
captionEnable, captionEnable,
countryCode, countryCode,
setCurrentTime, // setCurrentTime,
setIsVODPaused, // setIsVODPaused,
qrCurrentItem, qrCurrentItem,
modalScale, // modalScale,
...mediaProps ...mediaProps
} = this.props; } = this.props;
@@ -2222,27 +2236,15 @@ const VideoPlayerBase = class extends React.Component {
} }
} }
// eslint-disable-next-line no-unused-vars
const isQRCodeVisible = playListInfo && qrCurrentItem && !thumbnailUrl && !panelInfo.modal; const isQRCodeVisible = playListInfo && qrCurrentItem && !thumbnailUrl && !panelInfo.modal;
const onSpotlightFocus = () => {
this.showControls();
if (this.state.lastFocusedTarget) {
setTimeout(() => {
Spotlight.focus(this.state.lastFocusedTarget);
});
} else {
setTimeout(() => {
Spotlight.focus(SpotlightIds.PLAYER_TAB_BUTTON);
});
}
};
// MediaPanel에서는 setIsVODPaused 사용 안 함 (PlayerPanel 전용) // MediaPanel에서는 setIsVODPaused 사용 안 함 (PlayerPanel 전용)
// if (panelInfo?.shptmBanrTpNm === 'VOD' || panelInfo?.shptmBanrTpNm === 'MEDIA') { // if (panelInfo?.shptmBanrTpNm === 'VOD' || panelInfo?.shptmBanrTpNm === 'MEDIA') {
// setIsVODPaused(this.state.paused); // setIsVODPaused(this.state.paused);
// } // }
// eslint-disable-next-line no-unused-vars
const getVideoPhoneNumberClassNames = () => { const getVideoPhoneNumberClassNames = () => {
const isQVC = const isQVC =
panelInfo?.chanId === 'USQVC' || panelInfo?.chanId === 'USQVC' ||
@@ -2393,7 +2395,7 @@ const VideoPlayerBase = class extends React.Component {
playbackRate={this.pulsedPlaybackRate || this.selectPlaybackRate(this.speedIndex)} playbackRate={this.pulsedPlaybackRate || this.selectPlaybackRate(this.speedIndex)}
playbackState={this.pulsedPlaybackState || this.prevCommand} playbackState={this.pulsedPlaybackState || this.prevCommand}
visible={this.state.miniFeedbackVisible && !noMiniFeedback} visible={this.state.miniFeedbackVisible && !noMiniFeedback}
></FeedbackContent> />
<ControlsContainer <ControlsContainer
className={classNames( className={classNames(
@@ -2453,7 +2455,7 @@ const VideoPlayerBase = class extends React.Component {
value={this.state.proportionPlayed} value={this.state.proportionPlayed}
visible={this.state.mediaSliderVisible} visible={this.state.mediaSliderVisible}
type={type} type={type}
></MediaSlider> />
)} )}
</div> </div>
)} )}
@@ -2483,11 +2485,11 @@ const VideoPlayerBase = class extends React.Component {
holdConfig={controlsHandleAboveHoldConfig} holdConfig={controlsHandleAboveHoldConfig}
onDown={this.handleControlsHandleAboveDown} onDown={this.handleControlsHandleAboveDown}
onKeyUp={this.handleControlsHandleAboveKeyUp} onKeyUp={this.handleControlsHandleAboveKeyUp}
onSpotlightDown={onSpotlightFocus} onSpotlightDown={this.onSpotlightFocus}
onSpotlightUp={onSpotlightFocus} onSpotlightUp={this.onSpotlightFocus}
onSpotlightRight={onSpotlightFocus} onSpotlightRight={this.onSpotlightFocus}
onSpotlightLeft={onSpotlightFocus} onSpotlightLeft={this.onSpotlightFocus}
onClick={onSpotlightFocus} onClick={this.onSpotlightFocus}
selectionKeys={controlsHandleAboveSelectionKeys} selectionKeys={controlsHandleAboveSelectionKeys}
spotlightDisabled={this.state.mediaControlsVisible || spotlightDisabled} spotlightDisabled={this.state.mediaControlsVisible || spotlightDisabled}
/> />

View File

@@ -8,61 +8,127 @@ const safeFp = fp || {};
export const { export const {
// 기본 함수 조합 // 기본 함수 조합
pipe, flow, curry, compose, pipe,
flow,
curry,
compose,
// 기본 컬렉션 함수들 // 기본 컬렉션 함수들
map, filter, reduce, get, set, map,
filter,
reduce,
get,
set,
// 기본 타입 체크 함수들 // 기본 타입 체크 함수들
isEmpty, isNotEmpty, isNil, isNotNil, isEmpty,
isNotEmpty,
isNil,
isNotNil,
// 비동기 함수들 // 비동기 함수들
mapAsync, reduceAsync, filterAsync, findAsync, forEachAsync, mapAsync,
reduceAsync,
filterAsync,
findAsync,
forEachAsync,
// Promise 관련 // Promise 관련
promisify, then, andThen, otherwise, catch: catchFn, finally: finallyFn, promisify,
then,
andThen,
otherwise,
catch: catchFn,
finally: finallyFn,
isPromise, isPromise,
// 조건부 실행 함수들 // 조건부 실행 함수들
when, unless, ifElse, ifT, ifF, ternary, when,
unless,
ifElse,
ifT,
ifF,
ternary,
// 디버깅 및 사이드 이펙트 // 디버깅 및 사이드 이펙트
tap, trace, tap,
trace,
// 안전한 실행 // 안전한 실행
tryCatch, safeGet, getOr, tryCatch,
safeGet,
getOr,
// 타입 체크 확장 // 타입 체크 확장
isJson, notEquals, isNotEqual, isVal, isPrimitive, isRef, isReference, isJson,
not, notIncludes, toBool, isFalsy, isTruthy, notEquals,
isNotEqual,
isVal,
isPrimitive,
isRef,
isReference,
not,
notIncludes,
toBool,
isFalsy,
isTruthy,
// 객체 변환 // 객체 변환
transformObjectKey, toCamelcase, toCamelKey, toSnakecase, toSnakeKey, transformObjectKey,
toPascalcase, pascalCase, renameKeys, toCamelcase,
toCamelKey,
toSnakecase,
toSnakeKey,
toPascalcase,
pascalCase,
renameKeys,
// 배열 유틸리티 // 배열 유틸리티
mapWhen, filterWhen, removeByIndex, removeByIdx, removeLast, mapWhen,
append, prepend, insertAt, partition, filterWhen,
removeByIndex,
removeByIdx,
removeLast,
append,
prepend,
insertAt,
partition,
// 함수 조합 유틸리티 // 함수 조합 유틸리티
ap, applyTo, juxt, converge, instanceOf, ap,
applyTo,
juxt,
converge,
instanceOf,
// 문자열 유틸리티 // 문자열 유틸리티
trimToUndefined, capitalize, isDatetimeString, trimToUndefined,
capitalize,
isDatetimeString,
// 수학 유틸리티 // 수학 유틸리티
clampTo, between, clampTo,
between,
// 기본값 처리 // 기본값 처리
defaultTo, defaultWith, elvis, defaultTo,
defaultWith,
elvis,
// 키 관련 // 키 관련
key, keyByVal, key,
mapWithKey, mapWithIdx, forEachWithKey, forEachWithIdx, keyByVal,
reduceWithKey, reduceWithIdx, mapWithKey,
mapWithIdx,
forEachWithKey,
forEachWithIdx,
reduceWithKey,
reduceWithIdx,
// 유틸리티 // 유틸리티
deepFreeze, times, lazy, deepFreeze,
times,
lazy,
} = safeFp; } = safeFp;
export default safeFp; export default safeFp;

View File

@@ -1,20 +1,18 @@
import { Job } from "@enact/core/util"; import { Job } from '@enact/core/util';
import Enact_$L from "@enact/i18n/$L";
import stringReSourceDe from "../../resources/de/strings.json"; import stringReSourceDe from '../../resources/de/strings.json';
import stringReSourceEn from "../../resources/en/strings.json"; import stringReSourceEn from '../../resources/en/strings.json';
import stringReSourceGb from "../../resources/gb/strings.json"; import stringReSourceGb from '../../resources/gb/strings.json';
import stringReSourceRu from "../../resources/ru/strings.json"; import stringReSourceRu from '../../resources/ru/strings.json';
import { getRicCode } from "../api/apiConfig"; import { ERROR_MESSAGES_GROUPS, SECRET_KEY } from './Config';
import { ERROR_MESSAGES_GROUPS, SECRET_KEY } from "./Config";
let _boundingRectCache = {}; let _boundingRectCache = {};
const BOUNDING_RECT_IGNORE_TIME = 10; const BOUNDING_RECT_IGNORE_TIME = 10;
const generateUUID = () => { const generateUUID = () => {
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
var r = (Math.random() * 16) | 0, const r = (Math.random() * 16) | 0;
v = c === "x" ? r : (r & 0x3) | 0x8; const v = c === 'x' ? r : (r & 0x3) | 0x8;
return v.toString(16); return v.toString(16);
}); });
}; };
@@ -37,10 +35,7 @@ export const getBoundingClientRect = (node) => {
const uuid = node.dataset.uuid; const uuid = node.dataset.uuid;
if (_boundingRectCache[uuid]) { if (_boundingRectCache[uuid]) {
if ( if (Date.now() - _boundingRectCache[uuid].called < BOUNDING_RECT_IGNORE_TIME) {
Date.now() - _boundingRectCache[uuid].called <
BOUNDING_RECT_IGNORE_TIME
) {
return _boundingRectCache[uuid].boundingRect; return _boundingRectCache[uuid].boundingRect;
} }
} }
@@ -63,42 +58,38 @@ const stringReSource = {
}; };
export const $L = (str) => { export const $L = (str) => {
let languageSetting = "system"; let languageSetting = 'system';
let resourceKey = ""; let resourceKey = '';
if (typeof window === "object" && window.store) { if (typeof window === 'object' && window.store) {
languageSetting = window.store.getState().localSettings.languageSetting; languageSetting = window.store.getState().localSettings.languageSetting;
if (languageSetting === "system") { if (languageSetting === 'system') {
resourceKey = window.store.getState().common.httpHeader?.cntry_cd; resourceKey = window.store.getState().common.httpHeader?.cntry_cd;
} else { } else {
resourceKey = languageSetting; resourceKey = languageSetting;
} }
} }
const resource = stringReSource[resourceKey]; const resource = stringReSource[resourceKey];
if (typeof str === "object") { if (typeof str === 'object') {
if (resource && resource[str.key]) { if (resource && resource[str.key]) {
return resource[str.key].replace(/{br}/g, "{br}"); return resource[str.key].replace(/{br}/g, '{br}');
} else { } else {
return str.value; return str.value;
} }
} else if (resource && resource[str]) { } else if (resource && resource[str]) {
return resource[str].replace(/{br}/g, "{br}"); return resource[str].replace(/{br}/g, '{br}');
} }
return str && str.replace(/{br}/g, "{br}"); return str && str.replace(/{br}/g, '{br}');
}; };
export const createQueryString = (object) => { export const createQueryString = (object) => {
const parts = []; const parts = [];
for (const key of Object.getOwnPropertyNames(object)) { for (const key of Object.getOwnPropertyNames(object)) {
if ( if (object[key] !== null && object[key] !== undefined && object[key] !== '') {
object[key] !== null &&
object[key] !== undefined &&
object[key] !== ""
) {
parts.push(`${key}=${encodeURIComponent(object[key])}`); parts.push(`${key}=${encodeURIComponent(object[key])}`);
} }
} }
return parts.join("&"); return parts.join('&');
}; };
export const wait = (time) => { export const wait = (time) => {
@@ -110,14 +101,14 @@ export const wait = (time) => {
}; };
export const scaleW = (value) => { export const scaleW = (value) => {
if (typeof window === "object") { if (typeof window === 'object') {
return value * (window.innerWidth / 1920); return value * (window.innerWidth / 1920);
} }
return value; return value;
}; };
export const scaleH = (value) => { export const scaleH = (value) => {
if (typeof window === "object") { if (typeof window === 'object') {
return value * (window.innerHeight / 1080); return value * (window.innerHeight / 1080);
} }
return value; return value;
@@ -150,14 +141,10 @@ let localLaunchParams = {
export const getLaunchParams = () => { export const getLaunchParams = () => {
let params = {}; let params = {};
if ( if (typeof window === 'object' && window.PalmSystem && window.PalmSystem.launchParams) {
typeof window === "object" &&
window.PalmSystem &&
window.PalmSystem.launchParams
) {
try { try {
params = JSON.parse(window.PalmSystem.launchParams); params = JSON.parse(window.PalmSystem.launchParams);
if (params["x-webos-app-container-launch"] === true) { if (params['x-webos-app-container-launch'] === true) {
params = params.details; params = params.details;
} }
} catch (e) { } catch (e) {
@@ -170,28 +157,24 @@ export const getLaunchParams = () => {
}; };
export const clearLaunchParams = () => { export const clearLaunchParams = () => {
console.log("common.clearLaunchParams"); console.log('common.clearLaunchParams');
if ( if (typeof window === 'object' && window.PalmSystem && window.PalmSystem.launchParams) {
typeof window === "object" && window.PalmSystem.launchParams = '';
window.PalmSystem &&
window.PalmSystem.launchParams
) {
window.PalmSystem.launchParams = "";
} else { } else {
localLaunchParams = {}; localLaunchParams = {};
} }
}; };
export const readLocalStorage = (key, defaultValue) => { export const readLocalStorage = (key, defaultValue) => {
const value = typeof window === "object" && window.localStorage.getItem(key); const value = typeof window === 'object' && window.localStorage.getItem(key);
if (!value && defaultValue !== undefined) { if (!value && defaultValue !== undefined) {
return defaultValue; return defaultValue;
} }
return value === "undefined" ? null : JSON.parse(value); return value === 'undefined' ? null : JSON.parse(value);
}; };
export const writeLocalStorage = (key, value) => { export const writeLocalStorage = (key, value) => {
if (typeof window === "object") { if (typeof window === 'object') {
window.localStorage.setItem(key, JSON.stringify(value)); window.localStorage.setItem(key, JSON.stringify(value));
} }
}; };
@@ -199,34 +182,29 @@ export const writeLocalStorage = (key, value) => {
export const convertToTimeFormat = (timeString, isIncludeDate = false) => { export const convertToTimeFormat = (timeString, isIncludeDate = false) => {
const date = new Date(timeString); const date = new Date(timeString);
let options = { hour: "numeric", minute: "2-digit", hour12: true }; let options = { hour: 'numeric', minute: '2-digit', hour12: true };
let pattern = " "; let pattern = ' ';
if (isIncludeDate) { if (isIncludeDate) {
options = { options = {
...options, ...options,
month: "2-digit", month: '2-digit',
day: "2-digit", day: '2-digit',
year: "numeric", year: 'numeric',
hour: "2-digit", hour: '2-digit',
}; };
pattern = ","; pattern = ',';
} }
return new Intl.DateTimeFormat("en-US", options) return new Intl.DateTimeFormat('en-US', options).format(date).replace(pattern, '');
.format(date)
.replace(pattern, "");
}; };
export const getTranslate3dValueByDirection = ( export const getTranslate3dValueByDirection = (element, isHorizontal = true) => {
element,
isHorizontal = true
) => {
try { try {
const transformStyle = window.getComputedStyle(element).transform; const transformStyle = window.getComputedStyle(element).transform;
if (!transformStyle || transformStyle === "none") { if (!transformStyle || transformStyle === 'none') {
throw new Error("transfrom style not found"); throw new Error('transfrom style not found');
} }
const transformMatrix = transformStyle.match(/^matrix\((.+)\)$/); const transformMatrix = transformStyle.match(/^matrix\((.+)\)$/);
@@ -234,7 +212,7 @@ export const getTranslate3dValueByDirection = (
let index, value; let index, value;
if (transformMatrix) { if (transformMatrix) {
const matrixValues = transformMatrix[1].split(", "); const matrixValues = transformMatrix[1].split(', ');
index = isHorizontal ? 4 : 5; index = isHorizontal ? 4 : 5;
@@ -248,28 +226,22 @@ export const getTranslate3dValueByDirection = (
}; };
export const formatGMTString = (date) => { export const formatGMTString = (date) => {
let string = date.toISOString().replace(/T/, " ").replace(/\..+/, ""); let string = date.toISOString().replace(/T/, ' ').replace(/\..+/, '');
return string; return string;
}; };
export const getSpottableDescendants = (containerId) => { export const getSpottableDescendants = (containerId) => {
let container = document.querySelector( let container = document.querySelector(`[data-spotlight-id="${containerId}"]`);
`[data-spotlight-id="${containerId}"]`
);
if (container) { if (container) {
return container.querySelectorAll('[class*="spottable"]'); return container.querySelectorAll('[class*="spottable"]');
} }
return []; return [];
}; };
export const isElementInContainer = ( export const isElementInContainer = (element, container, fullyVisible = true) => {
element,
container,
fullyVisible = true
) => {
// 요소와 컨테이너의 사각형 정보 가져오기 // 요소와 컨테이너의 사각형 정보 가져오기
if (typeof window === "object") { if (typeof window === 'object') {
const elementRect = getBoundingClientRect(element); const elementRect = getBoundingClientRect(element);
const containerRect = container const containerRect = container
? getBoundingClientRect(container) ? getBoundingClientRect(container)
@@ -334,9 +306,7 @@ export const getRectDiff = (element1, element2) => {
}; };
export const getFormattingCardNo = (cardNumber) => { export const getFormattingCardNo = (cardNumber) => {
return `${"*".repeat(12)}${cardNumber.slice(-4)}` return `${'*'.repeat(12)}${cardNumber.slice(-4)}`.replace(/(.{4})/g, '$1-').slice(0, -1);
.replace(/(.{4})/g, "$1-")
.slice(0, -1);
}; };
export const getQRCodeUrl = ({ export const getQRCodeUrl = ({
@@ -345,41 +315,41 @@ export const getQRCodeUrl = ({
index, index,
patnrId, patnrId,
prdtId, prdtId,
dirPurcSelYn = "Y", dirPurcSelYn = 'Y',
prdtData, prdtData,
qrType, qrType,
liveFlag = "Y", liveFlag = 'Y',
entryMenu, entryMenu,
nowMenu, nowMenu,
}) => { }) => {
if (!serverHOST) { if (!serverHOST) {
console.error("getQRCodeUrl: Not Supported, Host is missing"); console.error('getQRCodeUrl: Not Supported, Host is missing');
return {}; return {};
} }
let sdpURL = serverHOST.split(".")[0]; let sdpURL = serverHOST.split('.')[0];
let countryCode = ""; let countryCode = '';
if (sdpURL.indexOf("-") > 0) { if (sdpURL.indexOf('-') > 0) {
countryCode = sdpURL.split("-")[1]; countryCode = sdpURL.split('-')[1];
} else { } else {
countryCode = sdpURL; countryCode = sdpURL;
} }
sdpURL = sdpURL.toLowerCase(); sdpURL = sdpURL.toLowerCase();
if (serverType !== "system") { if (serverType !== 'system') {
sdpURL = serverType; sdpURL = serverType;
} }
let baseUrl = ""; let baseUrl = '';
if (sdpURL.indexOf("qt2") >= 0) { if (sdpURL.indexOf('qt2') >= 0) {
baseUrl = "https://qt2-m.shoptime.lgappstv.com/"; baseUrl = 'https://qt2-m.shoptime.lgappstv.com/';
} else if (sdpURL.indexOf("qt") >= 0) { } else if (sdpURL.indexOf('qt') >= 0) {
baseUrl = "https://qt-m.shoptime.lgappstv.com/"; baseUrl = 'https://qt-m.shoptime.lgappstv.com/';
} else { } else {
baseUrl = "https://m.shoptime.lgappstv.com/"; baseUrl = 'https://m.shoptime.lgappstv.com/';
} }
const prdtDataStr = JSON.stringify(prdtData); const prdtDataStr = JSON.stringify(prdtData);
const prdtDataBase64 = btoa(prdtDataStr); const prdtDataBase64 = btoa(prdtDataStr);
@@ -402,25 +372,25 @@ export const getQRCodeUrl = ({
// ex: JANUARY 01, 2024 // ex: JANUARY 01, 2024
export const getFormattingDate = (dateString) => { export const getFormattingDate = (dateString) => {
const date = new Date(dateString.replace(" ", "T")); const date = new Date(dateString.replace(' ', 'T'));
const monthNames = [ const monthNames = [
$L("JANUARY"), $L('JANUARY'),
$L("FEBRUARY"), $L('FEBRUARY'),
$L("MARCH"), $L('MARCH'),
$L("APRIL"), $L('APRIL'),
$L("MAY"), $L('MAY'),
$L("JUNE"), $L('JUNE'),
$L("JULY"), $L('JULY'),
$L("AUGUST"), $L('AUGUST'),
$L("SEPTEMBER"), $L('SEPTEMBER'),
$L("OCTOBER"), $L('OCTOBER'),
$L("NOVEMBER"), $L('NOVEMBER'),
$L("DECEMBER"), $L('DECEMBER'),
]; ];
const month = monthNames[date.getMonth()]; const month = monthNames[date.getMonth()];
const day = date.getDate().toString().padStart(2, "0"); const day = date.getDate().toString().padStart(2, '0');
const year = date.getFullYear(); const year = date.getFullYear();
return `${month} ${day}, ${year}`; return `${month} ${day}, ${year}`;
@@ -437,14 +407,14 @@ export const removeSpecificTags = (html) => {
let sanitizedHtml = html; let sanitizedHtml = html;
tagPatterns.forEach((pattern) => { tagPatterns.forEach((pattern) => {
sanitizedHtml = sanitizedHtml.replace(pattern, ""); sanitizedHtml = sanitizedHtml.replace(pattern, '');
}); });
return sanitizedHtml; return sanitizedHtml;
}; };
export const encryptPhoneNumber = (phoneNumber) => { export const encryptPhoneNumber = (phoneNumber) => {
if (typeof window === "object") { if (typeof window === 'object') {
return window.CryptoJS.AES.encrypt(phoneNumber, SECRET_KEY).toString(); return window.CryptoJS.AES.encrypt(phoneNumber, SECRET_KEY).toString();
} }
@@ -452,7 +422,7 @@ export const encryptPhoneNumber = (phoneNumber) => {
}; };
export const decryptPhoneNumber = (encryptedPhoneNumber) => { export const decryptPhoneNumber = (encryptedPhoneNumber) => {
if (typeof window === "object") { if (typeof window === 'object') {
const bytes = window.CryptoJS.AES.decrypt(encryptedPhoneNumber, SECRET_KEY); const bytes = window.CryptoJS.AES.decrypt(encryptedPhoneNumber, SECRET_KEY);
return bytes.toString(window.CryptoJS.enc.Utf8); return bytes.toString(window.CryptoJS.enc.Utf8);
} }
@@ -461,63 +431,54 @@ export const decryptPhoneNumber = (encryptedPhoneNumber) => {
}; };
export const formatLocalDateTime = (date) => { export const formatLocalDateTime = (date) => {
const isDate = (obj) => const isDate = (obj) => Object.prototype.toString.call(obj) === '[object Date]';
Object.prototype.toString.call(obj) === "[object Date]";
if (typeof window === "object" && isDate(date)) { if (typeof window === 'object' && isDate(date)) {
const year = date.getFullYear(); const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0"); const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, "0"); const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, "0"); const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, "0"); const minutes = String(date.getMinutes()).padStart(2, '0');
const seconds = String(date.getSeconds()).padStart(2, "0"); const seconds = String(date.getSeconds()).padStart(2, '0');
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
} }
return ""; return '';
}; };
export const parseLocalizedNumber = (numberString, countryCode) => { export const parseLocalizedNumber = (numberString, countryCode) => {
// 유럽식: 1.499,00 -> 1499.00 // 유럽식: 1.499,00 -> 1499.00
if (countryCode === "DE") { if (countryCode === 'DE') {
return parseFloat(numberString.replace(/\./g, "").replace(",", ".")); return parseFloat(numberString.replace(/\./g, '').replace(',', '.'));
} }
// 미국식: 1,499.00 -> 1499.00 // 미국식: 1,499.00 -> 1499.00
if (countryCode === "US" || countryCode === "GB") { if (countryCode === 'US' || countryCode === 'GB') {
return parseFloat(numberString.replace(/[^0-9.-]+/g, "")); return parseFloat(numberString.replace(/[^0-9.-]+/g, ''));
} }
// 러시아식: 1 499,00 -> 1499.00 // 러시아식: 1 499,00 -> 1499.00
if (countryCode === "RU") { if (countryCode === 'RU') {
return parseFloat(numberString.replace(/\s/g, "").replace(",", ".")); return parseFloat(numberString.replace(/\s/g, '').replace(',', '.'));
} }
return parseFloat(numberString); return parseFloat(numberString);
}; };
export const formatCurrencyValue = ( export const formatCurrencyValue = (value, currSign, currSignLoc, isDiscount = false) => {
value, if (value === '-' || value === 0) return '-';
currSign,
currSignLoc,
isDiscount = false
) => {
if (value === "-" || value === 0) return "-";
const numValue = parseFloat(value); const numValue = parseFloat(value);
if (isNaN(numValue)) return "-"; if (isNaN(numValue)) return '-';
const sign = isDiscount && numValue > 0 ? "- " : ""; const sign = isDiscount && numValue > 0 ? '- ' : '';
const formattedValue = parseFloat(numValue.toFixed(2)).toLocaleString( const formattedValue = parseFloat(numValue.toFixed(2)).toLocaleString('en-US', {
"en-US", minimumFractionDigits: 2,
{ maximumFractionDigits: 2,
minimumFractionDigits: 2, });
maximumFractionDigits: 2,
}
);
if (!currSign || !currSignLoc) return `${sign}${formattedValue}`; if (!currSign || !currSignLoc) return `${sign}${formattedValue}`;
return currSignLoc === "L" return currSignLoc === 'L'
? `${sign}${currSign} ${formattedValue}` ? `${sign}${currSign} ${formattedValue}`
: `${sign}${formattedValue} ${currSign}`; : `${sign}${formattedValue} ${currSign}`;
}; };
@@ -536,44 +497,31 @@ export const getTimeDifferenceByMilliseconds = (
return false; return false;
} }
const convertedStartTime = new Date(startTimeString.replace(/ /, "T") + "Z"); const convertedStartTime = new Date(startTimeString.replace(/ /, 'T') + 'Z');
const convertedEndTime = new Date(endTimeString.replace(/ /, "T") + "Z"); const convertedEndTime = new Date(endTimeString.replace(/ /, 'T') + 'Z');
const timeDifference = convertedEndTime - convertedStartTime; const timeDifference = convertedEndTime - convertedStartTime;
return timeDifference > threshold; return timeDifference > threshold;
}; };
export const getErrorMessage = ( export const getErrorMessage = (errorCode, retMsg, retDetailCode, returnBindStrings) => {
errorCode, const foundGroup = ERROR_MESSAGES_GROUPS.find((group) => group.codes.includes(Number(errorCode)));
retMsg,
retDetailCode,
returnBindStrings
) => {
const group = ERROR_MESSAGES_GROUPS.find((group) =>
group.codes.includes(Number(errorCode))
);
const errorPrefix = errorCode const errorPrefix = errorCode
? retDetailCode ? retDetailCode
? `[${errorCode}-${retDetailCode}] ` ? `[${errorCode}-${retDetailCode}] `
: `[${errorCode}] ` : `[${errorCode}] `
: ""; : '';
if (group) { if (foundGroup) {
if ( if (errorCode === 1120 && returnBindStrings && typeof returnBindStrings === 'object') {
errorCode === 1120 && return `${errorPrefix} ${foundGroup.message} (ID: ${returnBindStrings.join(', ')})`;
returnBindStrings &&
typeof returnBindStrings === "object"
) {
return `${errorPrefix} ${group.message} (ID: ${returnBindStrings.join(
", "
)})`;
} }
return errorPrefix + group.message; return errorPrefix + foundGroup.message;
} else if (retMsg) { } else if (retMsg) {
return errorPrefix + retMsg; return errorPrefix + retMsg;
} else { } else {
return errorPrefix + "An unknown error occurred. Please try again later."; return errorPrefix + 'An unknown error occurred. Please try again later.';
} }
}; };

View File

@@ -24,7 +24,7 @@ const promisify = (a, ...args) => {
const cond = fp.cond([ const cond = fp.cond([
[fp.isFunction, () => fnPromisify(a, ...args)], [fp.isFunction, () => fnPromisify(a, ...args)],
[isPromise, fp.identity], [isPromise, fp.identity],
[fp.T, (a) => Promise.resolve(a)], [fp.T, (val) => Promise.resolve(val)],
]); ]);
const result = cond(a); const result = cond(a);
@@ -75,7 +75,7 @@ const toBool = (a) =>
fp.cond([ fp.cond([
[fp.equals('true'), fp.T], [fp.equals('true'), fp.T],
[fp.equals('false'), fp.F], [fp.equals('false'), fp.F],
[fp.T, (a) => !!a], [fp.T, (val) => !!val],
])(a); ])(a);
/** /**
@@ -83,9 +83,9 @@ const toBool = (a) =>
* (isTrue가 true면 t(실행)반환, false면 f(실행)반환) * (isTrue가 true면 t(실행)반환, false면 f(실행)반환)
*/ */
const ternary = fp.curry((evaluator, trueHandler, falseHandler, a) => { const ternary = fp.curry((evaluator, trueHandler, falseHandler, a) => {
const executor = fp.curry((t, f, a, isTrue) => { const executor = fp.curry((t, f, val, isTrue) => {
const result = isTrue ? (fp.isFunction(t) ? t(a) : t) : fp.isFunction(f) ? f(a) : f; const evalResult = isTrue ? (fp.isFunction(t) ? t(val) : t) : fp.isFunction(f) ? f(val) : f;
return result; return evalResult;
}); });
const getEvaluator = (fn) => (fp.isNil(fn) ? fp.identity : fn); const getEvaluator = (fn) => (fp.isNil(fn) ? fp.identity : fn);
const result = executor(trueHandler, falseHandler, a, getEvaluator(evaluator)(a)); const result = executor(trueHandler, falseHandler, a, getEvaluator(evaluator)(a));
@@ -110,7 +110,7 @@ const pascalCase = fp.pipe(fp.camelCase, fp.upperFirst);
const mapAsync = fp.curry(async (asyncMapper, arr) => { const mapAsync = fp.curry(async (asyncMapper, arr) => {
const composer = fp.pipe( const composer = fp.pipe(
fp.flatMapDeep(fp.pipe(asyncMapper, promisify)), fp.flatMapDeep(fp.pipe(asyncMapper, promisify)),
async (a) => await Promise.all(a), async (a) => await Promise.all(a)
); );
const result = await composer(arr); const result = await composer(arr);
@@ -124,7 +124,7 @@ const mapAsync = fp.curry(async (asyncMapper, arr) => {
const filterAsync = fp.curry(async (asyncFilter, arr) => { const filterAsync = fp.curry(async (asyncFilter, arr) => {
const composer = fp.pipe( const composer = fp.pipe(
mapAsync(async (item) => ((await asyncFilter(item)) ? item : false)), mapAsync(async (item) => ((await asyncFilter(item)) ? item : false)),
then(fp.filter(fp.pipe(fp.equals(false), not))), then(fp.filter(fp.pipe(fp.equals(false), not)))
); );
const result = await composer(arr); const result = await composer(arr);
@@ -139,7 +139,7 @@ const findAsync = fp.curry(async (asyncFn, arr) => {
mapAsync(asyncFn), mapAsync(asyncFn),
then(fp.indexOf(true)), then(fp.indexOf(true)),
then((idx) => fp.get(`[${idx}]`, arr)), then((idx) => fp.get(`[${idx}]`, arr)),
otherwise(fp.always(undefined)), otherwise(fp.always(undefined))
); );
const result = await composer(arr); const result = await composer(arr);
@@ -182,8 +182,8 @@ const forEachAsync = fp.curry(async (cb, collection) => {
const key = fp.curry((a, v) => { const key = fp.curry((a, v) => {
const composer = fp.pipe( const composer = fp.pipe(
fp.entries, fp.entries,
fp.find(([k, val]) => fp.equals(v, val)), fp.find(([, val]) => fp.equals(v, val)),
fp.head, fp.head
); );
const result = composer(a); const result = composer(a);
return result; return result;
@@ -198,6 +198,14 @@ const isJson = (a) => {
return fp.isString(a) && !composer(() => JSON.parse(a)); return fp.isString(a) && !composer(() => JSON.parse(a));
}; };
/**
* Reference type 여부 체크 함수 (전방 선언)
*/
const isRef = fp.pipe(
(a) => fp.isNil(a) || fp.isBoolean(a) || fp.isNumber(a) || fp.isString(a),
not
);
/** /**
* shallow freeze 보완 * shallow freeze 보완
* (대상 object의 refence 타입의 properties까지 object.freeze 처리) * (대상 object의 refence 타입의 properties까지 object.freeze 처리)
@@ -212,31 +220,31 @@ const deepFreeze = (obj) => {
}; };
const transformObjectKey = fp.curry((transformFn, dest) => { const transformObjectKey = fp.curry((transformFn, dest) => {
const convertRecursively = (dest) => { const convertRecursively = (source) => {
const convertTo = (o) => { const convertTo = (o) => {
const composer = fp.pipe( const composer = fp.pipe(
fp.entries, fp.entries,
fp.reduce((acc, [k, v]) => { fp.reduce((acc, [k, val]) => {
const cond = fp.cond([ const cond = fp.cond([
[fp.isPlainObject, convertTo], [fp.isPlainObject, convertTo],
[fp.isArray, (v) => v.map(cond)], [fp.isArray, (arr) => arr.map(cond)],
[fp.T, (a) => a], [fp.T, (item) => item],
]); ]);
const transformedKey = transformFn(k); const transformedKey = transformFn(k);
if (!fp.has(transformedKey, acc)) { if (!fp.has(transformedKey, acc)) {
acc[transformedKey] = cond(v); acc[transformedKey] = cond(val);
return acc; return acc;
} else { } else {
throw new Error( throw new Error(
`${transformedKey} already exist. duplicated property name is not supported.`, `${transformedKey} already exist. duplicated property name is not supported.`
); );
} }
}, {}), }, {})
); );
const result = composer(o); const result = composer(o);
return result; return result;
}; };
const result = convertTo(dest); const result = convertTo(source);
return result; return result;
}; };
@@ -356,7 +364,7 @@ const append = fp.concat;
* array 인자의 (index상)앞쪽에 value인자를 추가 * array 인자의 (index상)앞쪽에 value인자를 추가
*/ */
const prepend = fp.curry((array, value) => const prepend = fp.curry((array, value) =>
fp.isArray(value) ? fp.concat(value, array) : fp.concat([value], array), fp.isArray(value) ? fp.concat(value, array) : fp.concat([value], array)
); );
/** /**
@@ -381,9 +389,9 @@ const reduceWithKey = fp.curry((f, acc, a) => fp.reduce.convert({ cap: false })(
const isVal = (a) => fp.isNil(a) || fp.isBoolean(a) || fp.isNumber(a) || fp.isString(a); const isVal = (a) => fp.isNil(a) || fp.isBoolean(a) || fp.isNumber(a) || fp.isString(a);
/** /**
* Array, Object, Function * Array, Object, Function (이미 위에서 선언됨)
*/ */
const isRef = fp.pipe(isVal, not); // const isRef = fp.pipe(isVal, not);
const isFalsy = (a) => { const isFalsy = (a) => {
return fp.isNil(a) || fp.some(fp.equals(a), [0, -0, NaN, false, '']); return fp.isNil(a) || fp.some(fp.equals(a), [0, -0, NaN, false, '']);
@@ -397,7 +405,7 @@ const isTruthy = (a) => !isFalsy(a);
* fp.getOr의 반환값이 null인 경우, 기본값 반환되게 수정한 버전 * fp.getOr의 반환값이 null인 경우, 기본값 반환되게 수정한 버전
* circular dependency 때문에 closure로 작성 * circular dependency 때문에 closure로 작성
*/ */
const getOr = (({ curry, getOr }) => { const getOr = (({ curry }) => {
const _getOr = curry((defaultValue, path, target) => { const _getOr = curry((defaultValue, path, target) => {
const val = fp.get(path, target); const val = fp.get(path, target);
return fp.isNil(val) ? defaultValue : val; return fp.isNil(val) ? defaultValue : val;
@@ -413,9 +421,7 @@ const getOr = (({ curry, getOr }) => {
* @param {Function} fn 실행할 함수 * @param {Function} fn 실행할 함수
* @param {*} value 대상 값 * @param {*} value 대상 값
*/ */
const when = fp.curry((predicate, fn, value) => const when = fp.curry((predicate, fn, value) => (predicate(value) ? fn(value) : value));
predicate(value) ? fn(value) : value
);
/** /**
* 조건이 거짓일 때만 함수를 실행, 참이면 원래 값 반환 * 조건이 거짓일 때만 함수를 실행, 참이면 원래 값 반환
@@ -423,9 +429,7 @@ const when = fp.curry((predicate, fn, value) =>
* @param {Function} fn 실행할 함수 * @param {Function} fn 실행할 함수
* @param {*} value 대상 값 * @param {*} value 대상 값
*/ */
const unless = fp.curry((predicate, fn, value) => const unless = fp.curry((predicate, fn, value) => (!predicate(value) ? fn(value) : value));
!predicate(value) ? fn(value) : value
);
/** /**
* if-else 조건부 실행 * if-else 조건부 실행
@@ -495,7 +499,9 @@ const safeGet = fp.curry((path, defaultValue, obj) => {
* @param {Array} array 대상 배열 * @param {Array} array 대상 배열
*/ */
const mapWhen = fp.curry((predicate, fn, array) => const mapWhen = fp.curry((predicate, fn, array) =>
array.map(function(item) { return predicate(item) ? fn(item) : item; }) array.map(function (item) {
return predicate(item) ? fn(item) : item;
})
); );
/** /**
@@ -514,12 +520,16 @@ const filterWhen = fp.curry((condition, predicate, array) =>
* @param {Object} obj 대상 객체 * @param {Object} obj 대상 객체
*/ */
const renameKeys = fp.curry((keyMap, obj) => const renameKeys = fp.curry((keyMap, obj) =>
fp.reduce((acc, [oldKey, newKey]) => { fp.reduce(
if (fp.has(oldKey, obj)) { (acc, [oldKey, newKey]) => {
acc[newKey] = obj[oldKey]; if (fp.has(oldKey, obj)) {
} acc[newKey] = obj[oldKey];
return acc; }
}, {}, fp.toPairs(keyMap)) return acc;
},
{},
fp.toPairs(keyMap)
)
); );
/** /**
@@ -545,7 +555,11 @@ const applyTo = fp.curry((value, fn) => fn(value));
* @param {Array} fns 함수 배열 * @param {Array} fns 함수 배열
* @param {*} value 대상 값 * @param {*} value 대상 값
*/ */
const juxt = fp.curry((fns, value) => fns.map(function(fn) { return fn(value); })); const juxt = fp.curry((fns, value) =>
fns.map(function (fn) {
return fn(value);
})
);
/** /**
* 여러 함수의 결과를 converge 함수로 조합 * 여러 함수의 결과를 converge 함수로 조합
@@ -555,7 +569,9 @@ const juxt = fp.curry((fns, value) => fns.map(function(fn) { return fn(value); }
*/ */
const converge = fp.curry((convergeFn, fns, value) => { const converge = fp.curry((convergeFn, fns, value) => {
// Chromium 68 호환성: spread 연산자 대신 apply 사용 // Chromium 68 호환성: spread 연산자 대신 apply 사용
const results = fns.map(function(fn) { return fn(value); }); const results = fns.map(function (fn) {
return fn(value);
});
return convergeFn.apply(null, results); return convergeFn.apply(null, results);
}); });
@@ -572,7 +588,7 @@ const trimToUndefined = (str) => {
* 첫 글자 대문자, 나머지 소문자 * 첫 글자 대문자, 나머지 소문자
* @param {string} str 대상 문자열 * @param {string} str 대상 문자열
*/ */
const capitalize = function(str) { const capitalize = function (str) {
if (!str || typeof str !== 'string') return str; if (!str || typeof str !== 'string') return str;
return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase(); return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
}; };
@@ -583,9 +599,7 @@ const capitalize = function(str) {
* @param {number} max 최댓값 * @param {number} max 최댓값
* @param {number} value 대상 값 * @param {number} value 대상 값
*/ */
const clampTo = fp.curry((min, max, value) => const clampTo = fp.curry((min, max, value) => Math.min(Math.max(value, min), max));
Math.min(Math.max(value, min), max)
);
/** /**
* 값이 min과 max 사이에 있는지 확인 * 값이 min과 max 사이에 있는지 확인
@@ -593,27 +607,21 @@ const clampTo = fp.curry((min, max, value) =>
* @param {number} max 최댓값 * @param {number} max 최댓값
* @param {number} value 대상 값 * @param {number} value 대상 값
*/ */
const between = fp.curry((min, max, value) => const between = fp.curry((min, max, value) => value >= min && value <= max);
value >= min && value <= max
);
/** /**
* null/undefined일 때 기본값 반환 * null/undefined일 때 기본값 반환
* @param {*} defaultValue 기본값 * @param {*} defaultValue 기본값
* @param {*} value 대상 값 * @param {*} value 대상 값
*/ */
const defaultTo = fp.curry((defaultValue, value) => const defaultTo = fp.curry((defaultValue, value) => (value == null ? defaultValue : value));
value == null ? defaultValue : value
);
/** /**
* Elvis 연산자 구현 (null safe 함수 적용) * Elvis 연산자 구현 (null safe 함수 적용)
* @param {Function} fn 적용할 함수 * @param {Function} fn 적용할 함수
* @param {*} value 대상 값 * @param {*} value 대상 값
*/ */
const elvis = fp.curry((fn, value) => const elvis = fp.curry((fn, value) => (value == null ? undefined : fn(value)));
value == null ? undefined : fn(value)
);
/** /**
* 배열을 조건에 따라 두 그룹으로 분할 * 배열을 조건에 따라 두 그룹으로 분할
@@ -622,7 +630,9 @@ const elvis = fp.curry((fn, value) =>
*/ */
const partition = fp.curry((predicate, array) => [ const partition = fp.curry((predicate, array) => [
array.filter(predicate), array.filter(predicate),
array.filter(function(item) { return !predicate(item); }) array.filter(function (item) {
return !predicate(item);
}),
]); ]);
/** /**
@@ -647,7 +657,7 @@ const lazy = (fn) => {
let cached = false; let cached = false;
let result; let result;
// Chromium 68 호환성: spread 연산자 대신 arguments 사용 // Chromium 68 호환성: spread 연산자 대신 arguments 사용
return function() { return function () {
if (!cached) { if (!cached) {
result = fn.apply(this, arguments); result = fn.apply(this, arguments);
cached = true; cached = true;
@@ -756,4 +766,4 @@ export default {
partition, partition,
times, times,
lazy, lazy,
}; };

View File

@@ -1,11 +1,11 @@
// spotlight-utils.js // spotlight-utils.js
import {getTargetByContainer} from '@enact/spotlight/src/target'; import { getTargetByContainer } from '@enact/spotlight/src/target';
import {getTargetBySelector} from '@enact/spotlight/src/target'; import { getTargetBySelector } from '@enact/spotlight/src/target';
import {getContainerConfig} from '@enact/spotlight/src/container'; import { getContainerConfig } from '@enact/spotlight/src/container';
import {isContainer, getContainerId} from '@enact/spotlight/src/container'; import { isContainer, getContainerId } from '@enact/spotlight/src/container';
import {getContainersForNode} from '@enact/spotlight/src/container'; import { getContainersForNode } from '@enact/spotlight/src/container';
import {isNavigable} from '@enact/spotlight/src/container'; import { isNavigable } from '@enact/spotlight/src/container';
import {setLastContainer} from '@enact/spotlight/src/container'; import { setLastContainer } from '@enact/spotlight/src/container';
import Spotlight from '@enact/spotlight'; import Spotlight from '@enact/spotlight';
// lodash 없이 last 함수 직접 구현 // lodash 없이 last 함수 직접 구현
@@ -15,13 +15,13 @@ const last = (array) => {
// focusElement 함수는 spotlight 내부 함수이므로, 직접 구현하거나 spotlight의 focus를 사용 // focusElement 함수는 spotlight 내부 함수이므로, 직접 구현하거나 spotlight의 focus를 사용
const focusElement = (target, containerIds) => { const focusElement = (target, containerIds) => {
console.log("focusElement called with:", target, containerIds); console.log('focusElement called with:', target, containerIds);
if (target && typeof target.focus === 'function') { if (target && typeof target.focus === 'function') {
try { try {
target.focus(); target.focus();
return true; return true;
} catch (e) { } catch (e) {
console.error("Focus failed:", e); console.error('Focus failed:', e);
return false; return false;
} }
} }
@@ -39,40 +39,40 @@ const focusElement = (target, containerIds) => {
* navigable target is found. * navigable target is found.
*/ */
export const getPredictedFocus = (elem) => { export const getPredictedFocus = (elem) => {
let target = elem; let target = elem;
if (!elem) { if (!elem) {
target = getTargetByContainer(); target = getTargetByContainer();
} else if (typeof elem === 'string') { } else if (typeof elem === 'string') {
if (getContainerConfig(elem)) { if (getContainerConfig(elem)) {
// String is a container ID // String is a container ID
target = getTargetByContainer(elem); target = getTargetByContainer(elem);
} else if (/^[\w\d-]+$/.test(elem)) { } else if (/^[\w\d-]+$/.test(elem)) {
// Support component IDs consisting of alphanumeric, dash, or underscore // Support component IDs consisting of alphanumeric, dash, or underscore
target = getTargetBySelector(`[data-spotlight-id=${elem}]`); target = getTargetBySelector(`[data-spotlight-id=${elem}]`);
} else { } else {
// Treat as a CSS selector // Treat as a CSS selector
target = getTargetBySelector(elem); target = getTargetBySelector(elem);
}
} else if (isContainer(elem)) {
// elem is a container element
target = getTargetByContainer(getContainerId(elem));
} }
} else if (isContainer(elem)) {
// Check navigability without attempting to focus // elem is a container element
const nextContainerIds = getContainersForNode(target); target = getTargetByContainer(getContainerId(elem));
const nextContainerId = last(nextContainerIds); }
if (isNavigable(target, nextContainerId, true)) { // Check navigability without attempting to focus
return target; const nextContainerIds = getContainersForNode(target);
} const nextContainerId = last(nextContainerIds);
return null; if (isNavigable(target, nextContainerId, true)) {
}; return target;
}
return null;
};
// 메인 focus 함수 // 메인 focus 함수
export const focus = (elem) => { export const focus = (elem) => {
console.log("focus test", elem); console.log('focus test', elem);
var target = elem; var target = elem;
var wasContainerId = false; var wasContainerId = false;
@@ -86,7 +86,7 @@ export const focus = (elem) => {
wasContainerId = true; wasContainerId = true;
} else if (/^[\w\d-]+$/.test(elem)) { } else if (/^[\w\d-]+$/.test(elem)) {
// 알파벳, 숫자, 대시, 언더스코어로 구성된 컴포넌트 ID 지원 // 알파벳, 숫자, 대시, 언더스코어로 구성된 컴포넌트 ID 지원
target = getTargetBySelector("[data-spotlight-id=".concat(elem, "]")); target = getTargetBySelector('[data-spotlight-id='.concat(elem, ']'));
} else { } else {
// CSS 셀렉터로 처리 // CSS 셀렉터로 처리
target = getTargetBySelector(elem); target = getTargetBySelector(elem);
@@ -123,118 +123,117 @@ export const focus = (elem) => {
/** /**
* spotlightId로 직접 포커스를 설정하는 함수 * spotlightId로 직접 포커스를 설정하는 함수
* *
* @param {String} spotlightId - data-spotlight-id 속성값 * @param {String} spotlightId - data-spotlight-id 속성값
* @param {Boolean} force - 강제 포커스 여부 (기본값: false) * @param {Boolean} force - 강제 포커스 여부 (기본값: false)
* @returns {Boolean} 포커스 성공 여부 * @returns {Boolean} 포커스 성공 여부
*/ */
export const focusById = (spotlightId, force = false) => { export const focusById = (spotlightId, force = false) => {
// spotlightId 유효성 검사 // spotlightId 유효성 검사
if (!spotlightId || typeof spotlightId !== 'string') { if (!spotlightId || typeof spotlightId !== 'string') {
console.error('[focusById] spotlightId는 반드시 문자열이어야 합니다.'); console.error('[focusById] spotlightId는 반드시 문자열이어야 합니다.');
return false;
}
try {
// data-spotlight-id 속성을 가진 요소 직접 검색
const targetElement = document.querySelector(`[data-spotlight-id="${spotlightId}"]`);
if (!targetElement) {
console.warn(`[focusById] spotlightId "${spotlightId}"를 가진 요소를 찾을 수 없습니다.`);
return false; return false;
} }
try { // 요소가 현재 보이고 활성화되어 있는지 확인
// data-spotlight-id 속성을 가진 요소 직접 검색 if (!isElementVisible(targetElement)) {
const targetElement = document.querySelector(`[data-spotlight-id="${spotlightId}"]`); console.warn(`[focusById] 요소 "${spotlightId}"가 보이지 않거나 비활성화되어 있습니다.`);
if (!force) return false;
if (!targetElement) { }
console.warn(`[focusById] spotlightId "${spotlightId}"를 가진 요소를 찾을 수 없습니다.`);
// Spotlight의 isSpottable로 포커스 가능 여부 확인
if (typeof Spotlight !== 'undefined' && Spotlight.isSpottable) {
if (!Spotlight.isSpottable(targetElement) && !force) {
console.warn(`[focusById] 요소 "${spotlightId}"가 현재 spottable하지 않습니다.`);
return false; return false;
} }
}
// 요소가 현재 보이고 활성화되어 있는지 확인
if (!isElementVisible(targetElement)) { // 직접 DOM 포커스 시도
console.warn(`[focusById] 요소 "${spotlightId}"가 보이지 않거나 비활성화되어 있습니다.`); if (force) {
if (!force) return false; // 강제 모드: DOM focus() 직접 호출
} console.log(`[focusById] 강제 포커스 모드: "${spotlightId}"`);
targetElement.focus();
// Spotlight의 isSpottable로 포커스 가능 여부 확인 return true;
if (typeof Spotlight !== 'undefined' && Spotlight.isSpottable) { } else {
if (!Spotlight.isSpottable(targetElement) && !force) { // 일반 모드: Spotlight 시스템 사용
console.warn(`[focusById] 요소 "${spotlightId}"가 현재 spottable하지 않습니다.`); console.log(`[focusById] Spotlight 포커스: "${spotlightId}"`);
return false;
} // Spotlight.focus() 사용 (선택자 형태로 전달)
} const focusResult = focus(`[data-spotlight-id="${spotlightId}"]`);
// 직접 DOM 포커스 시도 if (!focusResult) {
if (force) { // Spotlight 포커스 실패 시 직접 포커스 시도
// 강제 모드: DOM focus() 직접 호출 console.log(`[focusById] Spotlight 포커스 실패, 직접 포커스 시도: "${spotlightId}"`);
console.log(`[focusById] 강제 포커스 모드: "${spotlightId}"`);
targetElement.focus(); targetElement.focus();
return true; return document.activeElement === targetElement;
} else {
// 일반 모드: Spotlight 시스템 사용
console.log(`[focusById] Spotlight 포커스: "${spotlightId}"`);
// Spotlight.focus() 사용 (선택자 형태로 전달)
const focusResult = focus(`[data-spotlight-id="${spotlightId}"]`);
if (!focusResult) {
// Spotlight 포커스 실패 시 직접 포커스 시도
console.log(`[focusById] Spotlight 포커스 실패, 직접 포커스 시도: "${spotlightId}"`);
targetElement.focus();
return document.activeElement === targetElement;
}
return focusResult;
} }
} catch (error) { return focusResult;
console.error(`[focusById] 포커스 설정 중 오류 발생: "${spotlightId}"`, error);
return false;
} }
}; } catch (error) {
console.error(`[focusById] 포커스 설정 중 오류 발생: "${spotlightId}"`, error);
/** return false;
* 요소가 보이고 포커스 가능한 상태인지 확인하는 헬퍼 함수 }
* };
* @param {Element} element - 확인할 DOM 요소
* @returns {Boolean} 요소의 가시성 및 활성화 상태 /**
*/ * 요소가 보이고 포커스 가능한 상태인지 확인하는 헬퍼 함수
const isElementVisible = (element) => { *
if (!element) return false; * @param {Element} element - 확인할 DOM 요소
* @returns {Boolean} 요소의 가시성 및 활성화 상태
// 요소가 DOM에 연결되어 있는지 확인 */
if (!element.isConnected) return false; const isElementVisible = (element) => {
if (!element) return false;
// disabled 속성 확인
if (element.disabled) return false; // 요소가 DOM에 연결되어 있는지 확인
if (!element.isConnected) return false;
// display: none 또는 visibility: hidden 확인
const style = window.getComputedStyle(element); // disabled 속성 확인
if (style.display === 'none' || style.visibility === 'hidden') return false; if (element.disabled) return false;
// opacity가 0인지 확인 // display: none 또는 visibility: hidden 확인
if (parseFloat(style.opacity) === 0) return false; const style = window.getComputedStyle(element);
if (style.display === 'none' || style.visibility === 'hidden') return false;
// 요소의 크기가 0인지 확인
const rect = element.getBoundingClientRect(); // opacity가 0인지 확인
if (rect.width === 0 && rect.height === 0) return false; if (parseFloat(style.opacity) === 0) return false;
return true; // 요소의 크기가 0인지 확인
}; const rect = element.getBoundingClientRect();
if (rect.width === 0 && rect.height === 0) return false;
/**
* 현재 포커스된 요소의 spotlightId를 반환하는 헬퍼 함수 return true;
* };
* @returns {String|null} 현재 포커스된 요소의 spotlightId 또는 null
*/ /**
export const getCurrentSpotlightId = () => { * 현재 포커스된 요소의 spotlightId를 반환하는 헬퍼 함수
const current = document.activeElement; *
if (current && current.hasAttribute('data-spotlight-id')) { * @returns {String|null} 현재 포커스된 요소의 spotlightId 또는 null
return current.getAttribute('data-spotlight-id'); */
} export const getCurrentSpotlightId = () => {
return null; const current = document.activeElement;
}; if (current && current.hasAttribute('data-spotlight-id')) {
return current.getAttribute('data-spotlight-id');
/** }
* 특정 spotlightId를 가진 요소가 현재 포커스되어 있는지 확인하는 함수 return null;
* };
* @param {String} spotlightId - 확인할 spotlightId
* @returns {Boolean} 해당 요소가 현재 포커스되어 있는지 여부 /**
*/ * 특정 spotlightId를 가진 요소가 현재 포커스되어 있는지 확인하는 함수
export const isCurrentlyFocused = (spotlightId) => { *
return getCurrentSpotlightId() === spotlightId; * @param {String} spotlightId - 확인할 spotlightId
}; * @returns {Boolean} 해당 요소가 현재 포커스되어 있는지 여부
*/
export const isCurrentlyFocused = (spotlightId) => {
return getCurrentSpotlightId() === spotlightId;
};

View File

@@ -3,11 +3,9 @@ import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useSta
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import Spinner from '@enact/sandstone/Spinner';
import Spotlight from '@enact/spotlight'; import Spotlight from '@enact/spotlight';
import { setContainerLastFocusedElement } from '@enact/spotlight/src/container'; import { setContainerLastFocusedElement } from '@enact/spotlight/src/container';
import indicatorDefaultImage from '../../../assets/images/img-thumb-empty-144@3x.png';
import { getDeviceAdditionInfo } from '../../actions/deviceActions'; import { getDeviceAdditionInfo } from '../../actions/deviceActions';
import { getThemeCurationDetailInfo } from '../../actions/homeActions'; import { getThemeCurationDetailInfo } from '../../actions/homeActions';
import { getMainCategoryDetail, getMainYouMayLike } from '../../actions/mainActions'; import { getMainCategoryDetail, getMainYouMayLike } from '../../actions/mainActions';
@@ -22,7 +20,6 @@ import TBody from '../../components/TBody/TBody';
import TPanel from '../../components/TPanel/TPanel'; import TPanel from '../../components/TPanel/TPanel';
import { panel_names } from '../../utils/Config'; import { panel_names } from '../../utils/Config';
import fp from '../../utils/fp'; import fp from '../../utils/fp';
import { $L, getQRCodeUrl } from '../../utils/helperMethods';
import { SpotlightIds } from '../../utils/SpotlightIds'; import { SpotlightIds } from '../../utils/SpotlightIds';
import DetailPanelBackground from './components/DetailPanelBackground'; import DetailPanelBackground from './components/DetailPanelBackground';
import THeaderCustom from './components/THeaderCustom'; import THeaderCustom from './components/THeaderCustom';
@@ -51,11 +48,6 @@ export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) {
const panels = useSelector((state) => state.panels.panels); const panels = useSelector((state) => state.panels.panels);
const [selectedIndex, setSelectedIndex] = useState(0); const [selectedIndex, setSelectedIndex] = useState(0);
const localRecentItems = useSelector((state) =>
fp.pipe(() => state, fp.get('localSettings.recentItems'))()
);
const { httpHeader } = useSelector((state) => state.common);
const { popupVisible, activePopup } = useSelector((state) => state.common.popup);
const [lgCatCd, setLgCatCd] = useState(''); const [lgCatCd, setLgCatCd] = useState('');
const [themeProductInfo, setThemeProductInfo] = useState(null); const [themeProductInfo, setThemeProductInfo] = useState(null);
@@ -561,11 +553,6 @@ export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) {
} }
}, [themeData, selectedIndex]); }, [themeData, selectedIndex]);
const imageUrl = useMemo(
() => fp.pipe(() => productData, fp.get('thumbnailUrl960'))(),
[productData]
);
// 타이틀과 aria-label 메모이제이션 (성능 최적화) // 타이틀과 aria-label 메모이제이션 (성능 최적화)
const headerTitle = useMemo( const headerTitle = useMemo(
() => () =>

View File

@@ -7,39 +7,39 @@ export default function DetailPanelSkeleton() {
return ( return (
<div className={css.detailArea}> <div className={css.detailArea}>
{/* 1. Left Margin Section - 60px */} {/* 1. Left Margin Section - 60px */}
<div className={css.leftMarginSection}></div> <div className={css.leftMarginSection} />
{/* 2. Info Section - 650px (왼쪽 영역 스켈레톤) */} {/* 2. Info Section - 650px (왼쪽 영역 스켈레톤) */}
<div className={css.infoSection}> <div className={css.infoSection}>
<div className={css.leftInfoContainer}> <div className={css.leftInfoContainer}>
<div className={css.leftInfoWrapper}> <div className={css.leftInfoWrapper}>
{/* 제품 태그 스켈레톤 */} {/* 제품 태그 스켈레톤 */}
<div className={css.skeletonProductTag}></div> <div className={css.skeletonProductTag} />
{/* 제품 정보 스켈레톤 */} {/* 제품 정보 스켈레톤 */}
<div className={css.skeletonProductInfo}> <div className={css.skeletonProductInfo}>
<div className={css.skeletonTitle}></div> <div className={css.skeletonTitle} />
<div className={css.skeletonSubtitle}></div> <div className={css.skeletonSubtitle} />
<div className={css.skeletonPrice}></div> <div className={css.skeletonPrice} />
</div> </div>
{/* QR 코드 스켈레톤 */} {/* QR 코드 스켈레톤 */}
<div className={css.skeletonQrCode}></div> <div className={css.skeletonQrCode} />
{/* 버튼들 스켈레톤 */} {/* 버튼들 스켈레톤 */}
<div className={css.skeletonButtons}> <div className={css.skeletonButtons}>
<div className={css.skeletonShopButton}></div> <div className={css.skeletonShopButton} />
<div className={css.skeletonFavoriteButton}></div> <div className={css.skeletonFavoriteButton} />
</div> </div>
{/* 주문 전화 섹션 스켈레톤 */} {/* 주문 전화 섹션 스켈레톤 */}
<div className={css.skeletonCallToOrder}></div> <div className={css.skeletonCallToOrder} />
{/* 액션 버튼들 스켈레톤 */} {/* 액션 버튼들 스켈레톤 */}
<div className={css.skeletonActionButtons}> <div className={css.skeletonActionButtons}>
<div className={css.skeletonActionButton}></div> <div className={css.skeletonActionButton} />
<div className={css.skeletonActionButton}></div> <div className={css.skeletonActionButton} />
<div className={css.skeletonActionButton}></div> <div className={css.skeletonActionButton} />
</div> </div>
</div> </div>
</div> </div>
@@ -52,57 +52,57 @@ export default function DetailPanelSkeleton() {
<div className={css.scrollerOverride}> <div className={css.scrollerOverride}>
{/* 제품 이미지/비디오 스켈레톤 */} {/* 제품 이미지/비디오 스켈레톤 */}
<div className={css.skeletonProductImages}> <div className={css.skeletonProductImages}>
<div className={css.skeletonMainImage}></div> <div className={css.skeletonMainImage} />
</div> </div>
{/* 제품 설명 스켈레톤 */} {/* 제품 설명 스켈레톤 */}
<div className={css.skeletonDescription}> <div className={css.skeletonDescription}>
<div className={css.skeletonDescTitle}></div> <div className={css.skeletonDescTitle} />
<div className={css.skeletonDescLine}></div> <div className={css.skeletonDescLine} />
<div className={css.skeletonDescLine}></div> <div className={css.skeletonDescLine} />
<div className={css.skeletonDescLine}></div> <div className={css.skeletonDescLine} />
<div className={css.skeletonDescLineShort}></div> <div className={css.skeletonDescLineShort} />
</div> </div>
{/* 리뷰 섹션 스켈레톤 */} {/* 리뷰 섹션 스켈레톤 */}
<div className={css.skeletonReviews}> <div className={css.skeletonReviews}>
<div className={css.skeletonReviewTitle}></div> <div className={css.skeletonReviewTitle} />
<div className={css.skeletonReviewItem}> <div className={css.skeletonReviewItem}>
<div className={css.skeletonReviewAvatar}></div> <div className={css.skeletonReviewAvatar} />
<div className={css.skeletonReviewContent}> <div className={css.skeletonReviewContent}>
<div className={css.skeletonReviewHeader}></div> <div className={css.skeletonReviewHeader} />
<div className={css.skeletonReviewText}></div> <div className={css.skeletonReviewText} />
<div className={css.skeletonReviewTextShort}></div> <div className={css.skeletonReviewTextShort} />
</div> </div>
</div> </div>
<div className={css.skeletonReviewItem}> <div className={css.skeletonReviewItem}>
<div className={css.skeletonReviewAvatar}></div> <div className={css.skeletonReviewAvatar} />
<div className={css.skeletonReviewContent}> <div className={css.skeletonReviewContent}>
<div className={css.skeletonReviewHeader}></div> <div className={css.skeletonReviewHeader} />
<div className={css.skeletonReviewText}></div> <div className={css.skeletonReviewText} />
<div className={css.skeletonReviewTextShort}></div> <div className={css.skeletonReviewTextShort} />
</div> </div>
</div> </div>
</div> </div>
{/* 추천 상품 스켈레톤 */} {/* 추천 상품 스켈레톤 */}
<div className={css.skeletonYouMayLike}> <div className={css.skeletonYouMayLike}>
<div className={css.skeletonYouMayLikeTitle}></div> <div className={css.skeletonYouMayLikeTitle} />
<div className={css.skeletonProductGrid}> <div className={css.skeletonProductGrid}>
<div className={css.skeletonProductCard}> <div className={css.skeletonProductCard}>
<div className={css.skeletonProductImage}></div> <div className={css.skeletonProductImage} />
<div className={css.skeletonProductName}></div> <div className={css.skeletonProductName} />
<div className={css.skeletonProductPrice}></div> <div className={css.skeletonProductPrice} />
</div> </div>
<div className={css.skeletonProductCard}> <div className={css.skeletonProductCard}>
<div className={css.skeletonProductImage}></div> <div className={css.skeletonProductImage} />
<div className={css.skeletonProductName}></div> <div className={css.skeletonProductName} />
<div className={css.skeletonProductPrice}></div> <div className={css.skeletonProductPrice} />
</div> </div>
<div className={css.skeletonProductCard}> <div className={css.skeletonProductCard}>
<div className={css.skeletonProductImage}></div> <div className={css.skeletonProductImage} />
<div className={css.skeletonProductName}></div> <div className={css.skeletonProductName} />
<div className={css.skeletonProductPrice}></div> <div className={css.skeletonProductPrice} />
</div> </div>
</div> </div>
</div> </div>
@@ -112,4 +112,4 @@ export default function DetailPanelSkeleton() {
</div> </div>
</div> </div>
); );
} }

View File

@@ -7,7 +7,7 @@ import Spottable from '@enact/spotlight/Spottable';
import defaultLogoImg from '../../../../assets/images/ic-tab-partners-default@3x.png'; import defaultLogoImg from '../../../../assets/images/ic-tab-partners-default@3x.png';
import CustomImage from '../../../components/CustomImage/CustomImage'; import CustomImage from '../../../components/CustomImage/CustomImage';
import { convertUtcToLocal } from '../../../components/MediaPlayer/util'; import { convertUtcToLocal } from '../../../components/MediaPlayer/util';
import { $L, removeSpecificTags } from '../../../utils/helperMethods'; import { $L } from '../../../utils/helperMethods';
import css1 from './PlayerItemCard.module.less'; import css1 from './PlayerItemCard.module.less';
import css2 from './PlayerItemCard.v2.module.less'; import css2 from './PlayerItemCard.v2.module.less';
@@ -33,11 +33,9 @@ export const removeDotAndColon = (string) => {
}; };
export default memo(function PlayerItemCard({ export default memo(function PlayerItemCard({
children,
disabled, disabled,
imageAlt, imageAlt,
imageSource, imageSource,
imgType = IMAGETYPES.imgHorizontal,
logo, logo,
onBlur, onBlur,
onClick, onClick,
@@ -47,10 +45,8 @@ export default memo(function PlayerItemCard({
soldoutFlag, soldoutFlag,
spotlightId, spotlightId,
patnerName, patnerName,
selectedIndex,
videoVerticalVisible, videoVerticalVisible,
currentVideoVisible, currentVideoVisible,
dangerouslySetInnerHTML,
currentTime, currentTime,
liveInfo, liveInfo,
startDt, startDt,
@@ -144,6 +140,7 @@ export default memo(function PlayerItemCard({
<h3 className={css.brandName}>{patnerName}</h3> <h3 className={css.brandName}>{patnerName}</h3>
</div> </div>
{/* eslint-disable-next-line react/no-danger */}
<h3 dangerouslySetInnerHTML={productName()} className={css.title} /> <h3 dangerouslySetInnerHTML={productName()} className={css.title} />
{liveInfo && liveInfo.showType === 'live' && ( {liveInfo && liveInfo.showType === 'live' && (
<div <div

View File

@@ -37,7 +37,6 @@ function PlayerOverlayContents({
handleIndicatorDownClick, handleIndicatorDownClick,
tabContainerVersion, tabContainerVersion,
tabIndexV2, tabIndexV2,
...rest
}) { }) {
const cntry_cd = useSelector((state) => state.common.httpHeader?.cntry_cd); const cntry_cd = useSelector((state) => state.common.httpHeader?.cntry_cd);
const dispatch = useDispatch(); const dispatch = useDispatch();
@@ -66,7 +65,7 @@ function PlayerOverlayContents({
} }
setIsSubtitleActive((prev) => !prev); setIsSubtitleActive((prev) => !prev);
}, [dispatch, captionEnable]); }, [dispatch, captionEnable, setIsSubtitleActive]);
const patncLogoPath = useMemo(() => { const patncLogoPath = useMemo(() => {
let logo = playListInfo[selectedIndex]?.patncLogoPath; let logo = playListInfo[selectedIndex]?.patncLogoPath;
@@ -75,7 +74,7 @@ function PlayerOverlayContents({
} }
return logo; return logo;
}, [playListInfo, selectedIndex, panelInfo]); }, [playListInfo, selectedIndex, panelInfo, type]);
const partnerName = useMemo(() => { const partnerName = useMemo(() => {
let name = playListInfo[selectedIndex]?.patncNm; let name = playListInfo[selectedIndex]?.patncNm;
@@ -84,7 +83,7 @@ function PlayerOverlayContents({
} }
return name; return name;
}, [playListInfo, selectedIndex, panelInfo]); }, [playListInfo, selectedIndex, panelInfo, type]);
const showName = useMemo(() => { const showName = useMemo(() => {
let name = playListInfo[selectedIndex]?.showNm; let name = playListInfo[selectedIndex]?.showNm;
@@ -93,21 +92,24 @@ function PlayerOverlayContents({
} }
return name ? name.replace(/<br\s*\/?>/gi, ' ') : ''; return name ? name.replace(/<br\s*\/?>/gi, ' ') : '';
}, [playListInfo, selectedIndex, panelInfo]); }, [playListInfo, selectedIndex, panelInfo, type]);
const onSpotlightMoveTabButton = (e) => { const onSpotlightMoveTabButton = useCallback((e) => {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
Spotlight.focus(SpotlightIds.PLAYER_TAB_BUTTON); Spotlight.focus(SpotlightIds.PLAYER_TAB_BUTTON);
}; }, []);
const onSpotlightMoveMediaButton = (e) => { const onSpotlightMoveMediaButton = useCallback(
e.stopPropagation(); (e) => {
if (type === 'LIVE') { e.stopPropagation();
return Spotlight.focus('videoIndicator-down-button'); if (type === 'LIVE') {
} return Spotlight.focus('videoIndicator-down-button');
Spotlight.focus('videoPlayer_mediaControls'); }
}; Spotlight.focus('videoPlayer_mediaControls');
},
[type]
);
const onSpotlightMoveSlider = useCallback( const onSpotlightMoveSlider = useCallback(
(e) => { (e) => {
@@ -120,33 +122,36 @@ function PlayerOverlayContents({
[type] [type]
); );
const onSpotlightMoveSideTab = (e) => { const onSpotlightMoveSideTab = useCallback((e) => {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
Spotlight.focus('tab-0'); Spotlight.focus('tab-0');
}; }, []);
const onSpotlightMoveBelowTab = (e) => { const onSpotlightMoveBelowTab = useCallback(
e.stopPropagation(); (e) => {
e.preventDefault(); e.stopPropagation();
e.preventDefault();
// tabIndexV2에 따라 다른 버튼으로 포커스 이동 // tabIndexV2에 따라 다른 버튼으로 포커스 이동
if (tabIndexV2 === 0) { if (tabIndexV2 === 0) {
// ShopNow 탭: Close 버튼으로 // ShopNow 탭: Close 버튼으로
// Spotlight.focus('below-tab-close-button'); // Spotlight.focus('below-tab-close-button');
Spotlight.focus('shownow_close_button'); Spotlight.focus('shownow_close_button');
} else if (tabIndexV2 === 1) { } else if (tabIndexV2 === 1) {
// LIVE CHANNEL 탭: LIVE CHANNEL 버튼으로 // LIVE CHANNEL 탭: LIVE CHANNEL 버튼으로
Spotlight.focus('below-tab-live-channel-button'); Spotlight.focus('below-tab-live-channel-button');
} else if (tabIndexV2 === 2) { } else if (tabIndexV2 === 2) {
// ShopNowButton: ShopNowButton으로 // ShopNowButton: ShopNowButton으로
Spotlight.focus('below-tab-shop-now-button'); Spotlight.focus('below-tab-shop-now-button');
} }
}; },
[tabIndexV2]
);
const onSpotlightMoveBackButton = () => { const onSpotlightMoveBackButton = useCallback(() => {
return Spotlight.focus(SpotlightIds.PLAYER_BACK_BUTTON); return Spotlight.focus(SpotlightIds.PLAYER_BACK_BUTTON);
}; }, []);
const currentSideButtonStatus = useMemo(() => { const currentSideButtonStatus = useMemo(() => {
if ( if (
@@ -158,7 +163,7 @@ function PlayerOverlayContents({
return true; return true;
} }
return false; return false;
}, [panelInfo, sideContentsVisible, tabContainerVersion]); }, [type, panelInfo, sideContentsVisible, tabContainerVersion]);
const noLiveContentsVisible = useMemo(() => { const noLiveContentsVisible = useMemo(() => {
if (!Array.isArray(playListInfo) || playListInfo.length === 0) { if (!Array.isArray(playListInfo) || playListInfo.length === 0) {

View File

@@ -4,11 +4,9 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import classNames from 'classnames'; import classNames from 'classnames';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { Job } from '@enact/core/util';
import Spotlight from '@enact/spotlight'; import Spotlight from '@enact/spotlight';
import SpotlightContainerDecorator from '@enact/spotlight/SpotlightContainerDecorator'; import SpotlightContainerDecorator from '@enact/spotlight/SpotlightContainerDecorator';
import Spottable from '@enact/spotlight/Spottable'; import Spottable from '@enact/spotlight/Spottable';
import { setContainerLastFocusedElement } from '@enact/spotlight/src/container';
import micIcon from '../../../assets/images/searchpanel/image-mic.png'; import micIcon from '../../../assets/images/searchpanel/image-mic.png';
import hotPicksImage from '../../../assets/images/searchpanel/img-hotpicks.png'; import hotPicksImage from '../../../assets/images/searchpanel/img-hotpicks.png';
@@ -16,7 +14,7 @@ import hotPicksBrandImage from '../../../assets/images/searchpanel/img-search-ho
import { sendLogGNB, sendLogTotalRecommend } from '../../actions/logActions'; import { sendLogGNB, sendLogTotalRecommend } from '../../actions/logActions';
import { getMyRecommandedKeyword } from '../../actions/myPageActions'; import { getMyRecommandedKeyword } from '../../actions/myPageActions';
import { popPanel, updatePanel } from '../../actions/panelActions'; import { popPanel, updatePanel } from '../../actions/panelActions';
import { getSearch, resetSearch, searchMain } from '../../actions/searchActions'; import { getSearch, resetSearch } from '../../actions/searchActions';
// import { // import {
// showErrorToast, // showErrorToast,
// showInfoToast, // showInfoToast,
@@ -28,15 +26,11 @@ import { getSearch, resetSearch, searchMain } from '../../actions/searchActions'
import TBody from '../../components/TBody/TBody'; import TBody from '../../components/TBody/TBody';
import TInput, { ICONS, KINDS } from './TInput/TInput'; import TInput, { ICONS, KINDS } from './TInput/TInput';
import TPanel from '../../components/TPanel/TPanel'; import TPanel from '../../components/TPanel/TPanel';
import TScroller from '../../components/TScroller/TScroller';
import TVerticalPagenator from '../../components/TVerticalPagenator/TVerticalPagenator'; import TVerticalPagenator from '../../components/TVerticalPagenator/TVerticalPagenator';
import TVirtualGridList from '../../components/TVirtualGridList/TVirtualGridList'; import TVirtualGridList from '../../components/TVirtualGridList/TVirtualGridList';
// import VirtualKeyboardContainer from "../../components/TToast/VirtualKeyboardContainer"; // import VirtualKeyboardContainer from "../../components/TToast/VirtualKeyboardContainer";
import usePrevious from '../../hooks/usePrevious'; import usePrevious from '../../hooks/usePrevious';
import { LOG_CONTEXT_NAME, LOG_MENU, LOG_MESSAGE_ID, panel_names } from '../../utils/Config'; import { LOG_CONTEXT_NAME, LOG_MENU, LOG_MESSAGE_ID, panel_names } from '../../utils/Config';
import { SpotlightIds } from '../../utils/SpotlightIds';
import NoSearchResults from './NoSearchResults/NoSearchResults';
import RecommendedKeywords from './RecommendedKeywords/RecommendedKeywords';
import SearchInputOverlay from './SearchInpuOverlay'; import SearchInputOverlay from './SearchInpuOverlay';
import css from './SearchPanel.new.module.less'; import css from './SearchPanel.new.module.less';
import SearchResultsNew from './SearchResults.new'; import SearchResultsNew from './SearchResults.new';
@@ -54,7 +48,6 @@ const SectionContainer = SpotlightContainerDecorator({ enterTo: 'last-focused' }
const SpottableMicButton = Spottable('div'); const SpottableMicButton = Spottable('div');
const SpottableKeyword = Spottable('div'); const SpottableKeyword = Spottable('div');
const SpottableProduct = Spottable('div'); const SpottableProduct = Spottable('div');
const SpottableLi = Spottable('li');
const ITEMS_PER_PAGE = 9; const ITEMS_PER_PAGE = 9;
@@ -70,7 +63,7 @@ const SPOTLIGHT_IDS = {
SEARCH_VERTICAL_PAGENATOR: 'search_verticalPagenator', SEARCH_VERTICAL_PAGENATOR: 'search_verticalPagenator',
}; };
export default function SearchPanel({ panelInfo, isOnTop, spotlightId, scrollOptions = [] }) { export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
const dispatch = useDispatch(); const dispatch = useDispatch();
const loadingComplete = useSelector((state) => state.common?.loadingComplete); const loadingComplete = useSelector((state) => state.common?.loadingComplete);
const recommandedKeywords = useSelector( const recommandedKeywords = useSelector(
@@ -90,16 +83,15 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId, scrollOpt
const [showVirtualKeyboard, setShowVirtualKeyboard] = useState(false); const [showVirtualKeyboard, setShowVirtualKeyboard] = useState(false);
const [isVoiceOverlayVisible, setIsVoiceOverlayVisible] = useState(false); const [isVoiceOverlayVisible, setIsVoiceOverlayVisible] = useState(false);
const [isSearchOverlayVisible, setIsSearchOverlayVisible] = useState(false); const [isSearchOverlayVisible, setIsSearchOverlayVisible] = useState(false);
const [voiceMode, setVoiceMode] = useState(VOICE_MODES.PROMPT);
//인풋창 포커스 구분을 위함 //인풋창 포커스 구분을 위함
const [inputFocus, setInputFocus] = useState(false); const [inputFocus, setInputFocus] = useState(false);
const _onFocus = useCallback(() => { const _onFocus = useCallback(() => {
setInputFocus(true); setInputFocus(true);
}, [inputFocus]); }, []);
const _onBlur = useCallback(() => { const _onBlur = useCallback(() => {
setInputFocus(false); setInputFocus(false);
}, [inputFocus]); }, []);
// TInput의 입력 모드 상태 (webOS 키보드가 뜨는지 여부) // TInput의 입력 모드 상태 (webOS 키보드가 뜨는지 여부)
const [isInputModeActive, setIsInputModeActive] = useState(false); const [isInputModeActive, setIsInputModeActive] = useState(false);
@@ -132,19 +124,6 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId, scrollOpt
// 가짜 데이터 - 실제로는 Redux store나 API에서 가져와야 함 // 가짜 데이터 - 실제로는 Redux store나 API에서 가져와야 함
const recentSearches = useMemo(() => ['Puppy food', 'Dog toy', 'Fitness'], []); const recentSearches = useMemo(() => ['Puppy food', 'Dog toy', 'Fitness'], []);
const recentResultSearches = useMemo(
() => [
'Puppy food',
'Dog toy',
"Mather's Day",
'Gift',
'Easter Day',
'Royal Canin puppy food2',
'Shark',
],
[]
);
const topSearches = useMemo( const topSearches = useMemo(
() => ["Mather's Day", 'Gift', 'Easter Day', 'Royal Canin puppy food', 'Fitness', 'Parrot'], () => ["Mather's Day", 'Gift', 'Easter Day', 'Royal Canin puppy food', 'Fitness', 'Parrot'],
[] []
@@ -207,28 +186,30 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId, scrollOpt
if (loadingComplete && !recommandedKeywords) { if (loadingComplete && !recommandedKeywords) {
dispatch(getMyRecommandedKeyword()); dispatch(getMyRecommandedKeyword());
} }
}, [loadingComplete]); }, [loadingComplete, recommandedKeywords, dispatch]);
useEffect(() => { useEffect(() => {
if (isOnTop) { if (isOnTop) {
let menu; let menu;
if (!searchPerformed) menu = LOG_MENU.SEARCH_SEARCH; if (!searchPerformed) {
else { menu = LOG_MENU.SEARCH_SEARCH;
if (searchQueryRef.current) } else {
if (searchQueryRef.current) {
menu = menu =
Object.keys(searchDatas).length > 0 Object.keys(searchDatas).length > 0
? LOG_MENU.SEARCH_RESULT ? LOG_MENU.SEARCH_RESULT
: LOG_MENU.SEARCH_BEST_SELLER; : LOG_MENU.SEARCH_BEST_SELLER;
}
} }
dispatch(sendLogGNB(menu)); dispatch(sendLogGNB(menu));
} }
}, [isOnTop, searchDatas, searchPerformed]); }, [isOnTop, searchDatas, searchPerformed, dispatch, searchQueryRef]);
useEffect(() => { useEffect(() => {
if (!searchQuery) { if (!searchQuery) {
dispatch(resetSearch()); dispatch(resetSearch());
} }
}, [dispatch]); }, [dispatch, searchQuery]);
useEffect(() => { useEffect(() => {
if (recommandedKeywords) { if (recommandedKeywords) {
@@ -303,35 +284,17 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId, scrollOpt
// 검색 시 가상 키보드 숨김 // 검색 시 가상 키보드 숨김
setShowVirtualKeyboard(false); setShowVirtualKeyboard(false);
}, },
[dispatch, searchPerformed, searchDatas, searchQuery] // eslint-disable-next-line react-hooks/exhaustive-deps
[dispatch, searchPerformed]
); );
const handleNext = useCallback(() => {
if (!isOnTopRef.current) {
return;
}
setCurrentPage((prev) => prev + 1);
setPageChanged(true);
}, [currentPage]);
const handlePrev = useCallback(() => {
if (!isOnTopRef.current) {
return;
}
setCurrentPage((prev) => (prev > 1 ? prev - 1 : prev));
setPageChanged(true);
}, [currentPage]);
const hasPrevPage = currentPage > 1;
const hasNextPage = currentPage * ITEMS_PER_PAGE < recommandedKeywords?.length;
useEffect(() => { useEffect(() => {
if (panelInfo && isOnTop) { if (panelInfo && isOnTop) {
if (panelInfo.currentSpot && firstSpot) { if (panelInfo.currentSpot && firstSpot) {
Spotlight.focus(panel_names.SEARCH_PANEL); Spotlight.focus(panel_names.SEARCH_PANEL);
} }
} }
}, [panelInfo, isOnTop]); }, [panelInfo, isOnTop, firstSpot]);
// SearchPanel이 처음 열릴 때 TInput으로 포커스 설정 // SearchPanel이 처음 열릴 때 TInput으로 포커스 설정
useEffect(() => { useEffect(() => {
@@ -349,20 +312,23 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId, scrollOpt
initialFocusTimerRef.current = null; initialFocusTimerRef.current = null;
} }
}; };
}, [isOnTop]); }, [isOnTop, isOnTopRef]);
useEffect(() => { useEffect(() => {
return () => { return () => {
const currentSearchVal = searchQueryRef.current;
const currentFocusedId = focusedContainerIdRef.current;
dispatch( dispatch(
updatePanel({ updatePanel({
name: panel_names.SEARCH_PANEL, name: panel_names.SEARCH_PANEL,
panelInfo: { panelInfo: {
searchVal: searchQueryRef.current, searchVal: currentSearchVal,
focusedContainerId: focusedContainerIdRef.current, focusedContainerId: currentFocusedId,
}, },
}) })
); );
}; };
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, []);
// Cleanup all timers on component unmount // Cleanup all timers on component unmount
@@ -373,7 +339,7 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId, scrollOpt
spotlightResumeTimerRef.current = null; spotlightResumeTimerRef.current = null;
} }
}; };
}, []); }, [isOnTopRef]);
const handleKeydown = useCallback( const handleKeydown = useCallback(
(e) => { (e) => {
@@ -426,7 +392,7 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId, scrollOpt
return; return;
} }
}, },
[searchQuery, position, handleSearchSubmit, showVirtualKeyboard] [searchQuery, position, handleSearchSubmit, showVirtualKeyboard, isOnTopRef]
); );
const cursorPosition = useCallback(() => { const cursorPosition = useCallback(() => {
@@ -442,7 +408,7 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId, scrollOpt
} }
// 마이크 버튼 클릭 시 voice overlay 토글 // 마이크 버튼 클릭 시 voice overlay 토글
setIsVoiceOverlayVisible((prev) => !prev); setIsVoiceOverlayVisible((prev) => !prev);
}, []); }, [isOnTopRef]);
const onCancel = useCallback(() => { const onCancel = useCallback(() => {
if (!isOnTopRef.current) { if (!isOnTopRef.current) {
@@ -461,7 +427,7 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId, scrollOpt
dispatch(resetSearch()); dispatch(resetSearch());
Spotlight.focus(SPOTLIGHT_IDS.SEARCH_INPUT_BOX); Spotlight.focus(SPOTLIGHT_IDS.SEARCH_INPUT_BOX);
} }
}, [searchQuery, dispatch, isVoiceOverlayVisible]); }, [searchQuery, dispatch, isVoiceOverlayVisible, isOnTopRef]);
const onFocusedContainerId = useCallback( const onFocusedContainerId = useCallback(
(containerId) => { (containerId) => {
@@ -484,17 +450,9 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId, scrollOpt
}, 0); }, 0);
} }
}, },
[panelInfo, firstSpot] [panelInfo, firstSpot, panels]
); );
const panelInfoFall = useMemo(() => {
const newPanelInfo = { ...panelInfo };
if (firstSpot) {
newPanelInfo.currentSpot = null;
}
return newPanelInfo;
}, [panelInfo, firstSpot]);
// 키워드 클릭 핸들러 // 키워드 클릭 핸들러
const handleKeywordClick = useCallback( const handleKeywordClick = useCallback(
(keyword) => { (keyword) => {
@@ -505,7 +463,16 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId, scrollOpt
setIsSearchOverlayVisible(false); setIsSearchOverlayVisible(false);
setInputFocus(false); setInputFocus(false);
}, },
[handleSearchSubmit, dispatch] // eslint-disable-next-line react-hooks/exhaustive-deps
[handleSearchSubmit]
);
// 키워드 클릭 핸들러 생성 함수
const createKeywordClickHandler = useCallback(
(keyword) => {
return () => handleKeywordClick(keyword);
},
[handleKeywordClick]
); );
const handleKeywordInput = (keyword) => { const handleKeywordInput = (keyword) => {
@@ -514,10 +481,33 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId, scrollOpt
setInputFocus(false); setInputFocus(false);
}; };
// 상품 클릭 핸들러 // TInput icon click handler
const handleProductClick = useCallback((product) => { const handleInputIconClick = useCallback(() => {
// 상품 상세 페이지로 이동하는 로직 구현 if (showVirtualKeyboard) {
console.log('Product clicked:', product); handleSearchSubmit(searchQuery);
} else {
setShowVirtualKeyboard(true);
}
}, [showVirtualKeyboard, handleSearchSubmit, searchQuery]);
// Microphone button keydown handler
const handleMicKeyDown = useCallback(
(e) => {
if (e.key === 'Enter') {
onClickMic();
}
},
[onClickMic]
);
// Voice overlay close handler
const handleVoiceOverlayClose = useCallback(() => {
setIsVoiceOverlayVisible(false);
}, []);
// Search overlay close handler
const handleSearchOverlayClose = useCallback(() => {
setIsSearchOverlayVisible(false);
}, []); }, []);
// 테스트용 Toast 핸들러들 // 테스트용 Toast 핸들러들
@@ -573,7 +563,7 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId, scrollOpt
</SpottableProduct> </SpottableProduct>
); );
}, },
[] [hotPicks]
); );
useEffect(() => { useEffect(() => {
@@ -587,7 +577,7 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId, scrollOpt
return ( return (
<TPanel className={css.container} handleCancel={onCancel} spotlightId={spotlightId}> <TPanel className={css.container} handleCancel={onCancel} spotlightId={spotlightId}>
<TBody className={css.tBody} scrollable={true} spotlightDisabled={!isOnTop}> <TBody className={css.tBody} scrollable spotlightDisabled={!isOnTop}>
<ContainerBasic> <ContainerBasic>
{isOnTop && ( {isOnTop && (
<TVerticalPagenator <TVerticalPagenator
@@ -598,7 +588,7 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId, scrollOpt
onFocusedContainerId={onFocusedContainerId} onFocusedContainerId={onFocusedContainerId}
cbChangePageRef={cbChangePageRef} cbChangePageRef={cbChangePageRef}
topMargin={36} topMargin={36}
scrollable={true} scrollable
> >
{/* 검색 내용있을때 검색 부분 */} {/* 검색 내용있을때 검색 부분 */}
{/* 검색 입력 영역 - overlay 열릴 때 숨김 (visibility로 처리) */} {/* 검색 입력 영역 - overlay 열릴 때 숨김 (visibility로 처리) */}
@@ -609,7 +599,7 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId, scrollOpt
searchDatas && css.searchValue /* 이건 결과값 있을때만. 조건 추가필요 */, searchDatas && css.searchValue /* 이건 결과값 있을때만. 조건 추가필요 */,
(isVoiceOverlayVisible || isSearchOverlayVisible) && css.hidden (isVoiceOverlayVisible || isSearchOverlayVisible) && css.hidden
)} )}
data-wheel-point={true} data-wheel-point="true"
spotlightId={SPOTLIGHT_IDS.SEARCH_INPUT_LAYER} spotlightId={SPOTLIGHT_IDS.SEARCH_INPUT_LAYER}
> >
<div className={css.searchInputWrapper}> <div className={css.searchInputWrapper}>
@@ -625,19 +615,13 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId, scrollOpt
icon={ICONS.search} icon={ICONS.search}
value={searchQuery} value={searchQuery}
onChange={handleSearchChange} onChange={handleSearchChange}
onIconClick={() => { onIconClick={handleInputIconClick}
if (showVirtualKeyboard) {
handleSearchSubmit(searchQuery);
} else {
setShowVirtualKeyboard(true);
}
}}
onKeyDown={handleKeydown} onKeyDown={handleKeydown}
onKeyUp={cursorPosition} onKeyUp={cursorPosition}
spotlightId={SPOTLIGHT_IDS.SEARCH_INPUT_BOX} spotlightId={SPOTLIGHT_IDS.SEARCH_INPUT_BOX}
forcedSpotlight="recent-keyword-0" forcedSpotlight="recent-keyword-0"
tabIndex={0} tabIndex={0}
spotlightBoxDisabled={true} spotlightBoxDisabled
onFocus={_onFocus} onFocus={_onFocus}
onBlur={_onBlur} onBlur={_onBlur}
onInputModeChange={handleInputModeChange} onInputModeChange={handleInputModeChange}
@@ -647,11 +631,7 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId, scrollOpt
className={css.microphoneButton} className={css.microphoneButton}
onClick={onClickMic} onClick={onClickMic}
onFocus={onFocusMic} onFocus={onFocusMic}
onKeyDown={(e) => { onKeyDown={handleMicKeyDown}
if (e.key === 'Enter') {
onClickMic();
}
}}
spotlightId={SPOTLIGHT_IDS.MICROPHONE_BUTTON} spotlightId={SPOTLIGHT_IDS.MICROPHONE_BUTTON}
> >
<div className={css.microphoneCircle}> <div className={css.microphoneCircle}>
@@ -714,11 +694,11 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId, scrollOpt
{/* 최근 검색어 섹션 */} {/* 최근 검색어 섹션 */}
<SectionContainer <SectionContainer
className={css.section} className={css.section}
data-wheel-point={true} data-wheel-point="true"
spotlightId={SPOTLIGHT_IDS.RECENT_SEARCHES_SECTION} spotlightId={SPOTLIGHT_IDS.RECENT_SEARCHES_SECTION}
> >
<div className={css.sectionHeader}> <div className={css.sectionHeader}>
<div className={css.sectionIndicator}></div> <div className={css.sectionIndicator} />
<div className={css.sectionTitle}>Your Recent Searches</div> <div className={css.sectionTitle}>Your Recent Searches</div>
</div> </div>
<div className={css.keywordList}> <div className={css.keywordList}>
@@ -726,7 +706,7 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId, scrollOpt
<SpottableKeyword <SpottableKeyword
key={`recent-${index}`} key={`recent-${index}`}
className={css.keywordButton} className={css.keywordButton}
onClick={() => handleKeywordClick(keyword)} onClick={createKeywordClickHandler(keyword)}
spotlightId={`recent-keyword-${index}`} spotlightId={`recent-keyword-${index}`}
> >
{keyword} {keyword}
@@ -738,11 +718,11 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId, scrollOpt
{/* 인기 검색어 섹션 */} {/* 인기 검색어 섹션 */}
<SectionContainer <SectionContainer
className={css.section} className={css.section}
data-wheel-point={true} data-wheel-point="true"
spotlightId={SPOTLIGHT_IDS.TOP_SEARCHES_SECTION} spotlightId={SPOTLIGHT_IDS.TOP_SEARCHES_SECTION}
> >
<div className={css.sectionHeader}> <div className={css.sectionHeader}>
<div className={css.sectionIndicator}></div> <div className={css.sectionIndicator} />
<div className={css.sectionTitle}>Top Searches</div> <div className={css.sectionTitle}>Top Searches</div>
</div> </div>
<div className={css.keywordList}> <div className={css.keywordList}>
@@ -750,7 +730,7 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId, scrollOpt
<SpottableKeyword <SpottableKeyword
key={`top-${index}`} key={`top-${index}`}
className={css.keywordButton} className={css.keywordButton}
onClick={() => handleKeywordClick(keyword)} onClick={createKeywordClickHandler(keyword)}
spotlightId={`top-keyword-${index}`} spotlightId={`top-keyword-${index}`}
> >
{keyword} {keyword}
@@ -762,11 +742,11 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId, scrollOpt
{/* 인기 브랜드 섹션 */} {/* 인기 브랜드 섹션 */}
<SectionContainer <SectionContainer
className={css.section} className={css.section}
data-wheel-point={true} data-wheel-point="true"
spotlightId={SPOTLIGHT_IDS.POPULAR_BRANDS_SECTION} spotlightId={SPOTLIGHT_IDS.POPULAR_BRANDS_SECTION}
> >
<div className={css.sectionHeader}> <div className={css.sectionHeader}>
<div className={css.sectionIndicator}></div> <div className={css.sectionIndicator} />
<div className={css.sectionTitle}>Popular Brands</div> <div className={css.sectionTitle}>Popular Brands</div>
</div> </div>
<div className={css.keywordList}> <div className={css.keywordList}>
@@ -774,7 +754,7 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId, scrollOpt
<SpottableKeyword <SpottableKeyword
key={`brand-${index}`} key={`brand-${index}`}
className={css.keywordButton} className={css.keywordButton}
onClick={() => handleKeywordClick(brand)} onClick={createKeywordClickHandler(brand)}
spotlightId={`brand-${index}`} spotlightId={`brand-${index}`}
> >
{brand} {brand}
@@ -786,11 +766,11 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId, scrollOpt
{/* Hot Picks for You 섹션 */} {/* Hot Picks for You 섹션 */}
<SectionContainer <SectionContainer
className={css.hotpicksSection} className={css.hotpicksSection}
data-wheel-point={true} data-wheel-point="true"
spotlightId={SPOTLIGHT_IDS.HOT_PICKS_SECTION} spotlightId={SPOTLIGHT_IDS.HOT_PICKS_SECTION}
> >
<div className={css.sectionHeader}> <div className={css.sectionHeader}>
<div className={css.sectionIndicator}></div> <div className={css.sectionIndicator} />
<div className={css.sectionTitle}>Hot Picks for You</div> <div className={css.sectionTitle}>Hot Picks for You</div>
</div> </div>
<div className={css.productList}> <div className={css.productList}>
@@ -842,8 +822,8 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId, scrollOpt
{isVoiceOverlayVisible && ( {isVoiceOverlayVisible && (
<VoiceInputOverlay <VoiceInputOverlay
isVisible={isVoiceOverlayVisible} isVisible={isVoiceOverlayVisible}
onClose={() => setIsVoiceOverlayVisible(false)} onClose={handleVoiceOverlayClose}
mode={voiceMode} mode={VOICE_MODES.PROMPT}
suggestions={voiceSuggestions} suggestions={voiceSuggestions}
searchQuery={searchQuery} searchQuery={searchQuery}
onSearchChange={handleSearchChange} onSearchChange={handleSearchChange}
@@ -853,7 +833,7 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId, scrollOpt
{isSearchOverlayVisible && ( {isSearchOverlayVisible && (
<SearchInputOverlay <SearchInputOverlay
isVisible={isSearchOverlayVisible} isVisible={isSearchOverlayVisible}
onClose={() => setIsSearchOverlayVisible(false)} onClose={handleSearchOverlayClose}
searchQuery={searchQuery} searchQuery={searchQuery}
onSearchChange={handleSearchChange} onSearchChange={handleSearchChange}
onSearchSubmit={handleSearchSubmit} onSearchSubmit={handleSearchSubmit}

View File

@@ -1,27 +1,15 @@
import React, { import React, { useCallback, useMemo, useRef, useState } from 'react';
useCallback,
useMemo,
useRef,
useState,
} from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import { useDispatch } from 'react-redux';
import SpotlightContainerDecorator
from '@enact/spotlight/SpotlightContainerDecorator';
import Spottable from '@enact/spotlight/Spottable'; import Spottable from '@enact/spotlight/Spottable';
import downBtnImg from '../../../assets/images/btn/search_btn_down_arrow.png'; import downBtnImg from '../../../assets/images/btn/search_btn_down_arrow.png';
import upBtnImg from '../../../assets/images/btn/search_btn_up_arrow.png'; import upBtnImg from '../../../assets/images/btn/search_btn_up_arrow.png';
import hotPicksImage from '../../../assets/images/searchpanel/img-hotpicks.png';
import hotPicksBrandImage
from '../../../assets/images/searchpanel/img-search-hotpicks.png';
import CustomImage from '../../components/CustomImage/CustomImage'; import CustomImage from '../../components/CustomImage/CustomImage';
import TButtonTab, { LIST_TYPE } from '../../components/TButtonTab/TButtonTab'; import TButtonTab, { LIST_TYPE } from '../../components/TButtonTab/TButtonTab';
import TDropDown from '../../components/TDropDown/TDropDown'; import TDropDown from '../../components/TDropDown/TDropDown';
import TVirtualGridList import TVirtualGridList from '../../components/TVirtualGridList/TVirtualGridList';
from '../../components/TVirtualGridList/TVirtualGridList';
import { $L } from '../../utils/helperMethods'; import { $L } from '../../utils/helperMethods';
import { SpotlightIds } from '../../utils/SpotlightIds'; import { SpotlightIds } from '../../utils/SpotlightIds';
import css from './SearchResults.new.module.less'; import css from './SearchResults.new.module.less';
@@ -30,39 +18,29 @@ import ShowCard from './SearchResultsNew/ShowCard';
const ITEMS_PER_PAGE = 10; const ITEMS_PER_PAGE = 10;
const SearchResultsNew = ({ const SearchResultsNew = ({ itemInfo, showInfo, themeInfo, shopperHouseInfo, keywordClick }) => {
itemInfo,
showInfo,
themeInfo,
shopperHouseInfo,
keywordClick,
}) => {
// ShopperHouse 데이터를 ItemCard 형식으로 변환 // ShopperHouse 데이터를 ItemCard 형식으로 변환
const convertedShopperHouseItems = useMemo(() => { const convertedShopperHouseItems = useMemo(() => {
if ( if (!shopperHouseInfo || !shopperHouseInfo.results || shopperHouseInfo.results.length === 0) {
!shopperHouseInfo ||
!shopperHouseInfo.results ||
shopperHouseInfo.results.length === 0
) {
return null; return null;
} }
const docs = shopperHouseInfo.results[0].docs || []; const docs = shopperHouseInfo.results[0].docs || [];
return docs.map((doc, index) => { return docs.map((doc) => {
const contentId = doc.contentId; const contentId = doc.contentId;
const tokens = contentId.split("_"); const tokens = contentId.split('_');
const patnrId = tokens?.[4] || ""; const patnrId = tokens?.[4] || '';
const prdtId = tokens?.[5] || ""; const prdtId = tokens?.[5] || '';
return { return {
thumbnail: doc.thumnail || doc.imgPath || "", //이미지 경로 thumbnail: doc.thumnail || doc.imgPath || '', //이미지 경로
title: doc.title || doc.prdtName || "", // 제목 title: doc.title || doc.prdtName || '', // 제목
dcPrice: doc.dcPrice || doc.price || "", // 할인가격 dcPrice: doc.dcPrice || doc.price || '', // 할인가격
price: doc.orgPrice || doc.price || "", // 원가 price: doc.orgPrice || doc.price || '', // 원가
soldout: doc.soldout || false, // 품절 여부 soldout: doc.soldout || false, // 품절 여부
contentId, //콘텐트 아이디 contentId, //콘텐트 아이디
reviewGrade: doc.reviewGrade || "", //리뷰 점수 (추가 정보) reviewGrade: doc.reviewGrade || '', //리뷰 점수 (추가 정보)
partnerName: doc.partnerName || "", //파트너 네임 partnerName: doc.partnerName || '', //파트너 네임
patnrId, // 파트너 아이디 patnrId, // 파트너 아이디
prdtId, // 상품 아이디 prdtId, // 상품 아이디
}; };
@@ -70,8 +48,7 @@ const SearchResultsNew = ({
}, [shopperHouseInfo]); }, [shopperHouseInfo]);
const getButtonTabList = () => { const getButtonTabList = () => {
// ShopperHouse 데이터가 있으면 그것을 사용, 없으면 기존 검색 결과 사용 // ShopperHouse 데이터가 있으면 그것을 사용, 없으면 기존 검색 결과 사용
const itemLength = const itemLength = convertedShopperHouseItems?.length || itemInfo?.length || 0;
convertedShopperHouseItems?.length || itemInfo?.length || 0;
const showLength = showInfo?.length || 0; const showLength = showInfo?.length || 0;
return [ return [
@@ -90,7 +67,7 @@ const SearchResultsNew = ({
const [visibleCount, setVisibleCount] = useState(ITEMS_PER_PAGE); const [visibleCount, setVisibleCount] = useState(ITEMS_PER_PAGE);
const [styleChange, setStyleChange] = useState(false); const [styleChange, setStyleChange] = useState(false);
const [filterMethods, setFilterMethods] = useState([]); const filterMethods = [];
const cbChangePageRef = useRef(null); const cbChangePageRef = useRef(null);
if (!buttonTabList) { if (!buttonTabList) {
@@ -98,8 +75,7 @@ const SearchResultsNew = ({
} }
// 현재 탭의 데이터 가져오기 - ShopperHouse 데이터 우선 // 현재 탭의 데이터 가져오기 - ShopperHouse 데이터 우선
const currentData = const currentData = tab === 0 ? convertedShopperHouseItems || itemInfo : showInfo;
tab === 0 ? convertedShopperHouseItems || itemInfo : showInfo;
// 표시할 데이터 (처음부터 visibleCount 개수만큼) // 표시할 데이터 (처음부터 visibleCount 개수만큼)
const displayedData = useMemo(() => { const displayedData = useMemo(() => {
@@ -147,8 +123,8 @@ const SearchResultsNew = ({
[dropDownTab] [dropDownTab]
); );
const SpottableLi = Spottable("li"); const SpottableLi = Spottable('li');
const SpottableDiv = Spottable("div"); const SpottableDiv = Spottable('div');
// 맨 처음으로 이동 (위 버튼) // 맨 처음으로 이동 (위 버튼)
const upBtnClick = () => { const upBtnClick = () => {
@@ -167,8 +143,7 @@ const SearchResultsNew = ({
// ProductCard 컴포넌트 // ProductCard 컴포넌트
const renderItem = useCallback( const renderItem = useCallback(
({ index, ...rest }) => { ({ index, ...rest }) => {
const { bgImgPath, title, partnerLogo, partnerName, keyword } = const { bgImgPath, title, partnerLogo, partnerName, keyword } = themeInfo[index];
themeInfo[index];
return ( return (
<SpottableDiv <SpottableDiv
key={`searchProduct-${index}`} key={`searchProduct-${index}`}
@@ -181,17 +156,13 @@ const SearchResultsNew = ({
</div> </div>
<div className={css.productInfo}> <div className={css.productInfo}>
<div className={css.productBrandWrapper}> <div className={css.productBrandWrapper}>
<img <img src={partnerLogo} alt={partnerName} className={css.brandLogo} />
src={partnerLogo}
alt={partnerName}
className={css.brandLogo}
/>
</div> </div>
<div className={css.productDetails}> <div className={css.productDetails}>
{keyword && ( {keyword && (
<div className={css.brandName}> <div className={css.brandName}>
{keyword.map((item, index) => ( {keyword.map((item, keywordIndex) => (
<span key={index}># {item}</span> <span key={keywordIndex}># {item}</span>
))} ))}
</div> </div>
)} )}
@@ -209,37 +180,25 @@ const SearchResultsNew = ({
<div className={css.topBox}> <div className={css.topBox}>
<span className={css.topBoxTitle}>How about these?</span> <span className={css.topBoxTitle}>How about these?</span>
<ul className={css.topBoxList}> <ul className={css.topBoxList}>
<SpottableLi {[
className={css.topBoxListItem} { text: 'Puppy food', key: 'puppy-food' },
onClick={() => keywordClick("Puppy food")} { text: 'Dog toy', key: 'dog-toy' },
> { text: 'Fitness', key: 'fitness' },
Puppy food ].map(({ text, key }) => {
</SpottableLi> const handleClick = () => keywordClick(text);
<SpottableLi return (
className={css.topBoxListItem} <SpottableLi key={key} className={css.topBoxListItem} onClick={handleClick}>
onClick={() => keywordClick("Dog toy")} {text}
> </SpottableLi>
Dog toy );
</SpottableLi> })}
<SpottableLi
className={css.topBoxListItem}
onClick={() => keywordClick("Fitness")}
>
Fitness
</SpottableLi>
</ul> </ul>
</div> </div>
{themeInfo && themeInfo?.length > 0 && ( {themeInfo && themeInfo?.length > 0 && (
<div <div className={css.hotpicksSection} data-wheel-point="true">
className={css.hotpicksSection}
data-wheel-point={true}
spotlightId={"hot-picks-section"}
>
<div className={css.sectionHeader}> <div className={css.sectionHeader}>
<div className={css.sectionIndicator}></div> <div className={css.sectionIndicator} />
<div className={css.sectionTitle}> <div className={css.sectionTitle}>Hot Picks ({themeInfo?.length})</div>
Hot Picks ({themeInfo?.length})
</div>
</div> </div>
<div className={css.productList}> <div className={css.productList}>
<TVirtualGridList <TVirtualGridList
@@ -253,7 +212,7 @@ const SearchResultsNew = ({
</div> </div>
</div> </div>
)} )}
<div className={css.itemBox} cbChangePageRef={cbChangePageRef}> <div className={css.itemBox}>
<div className={css.tabContainer}> <div className={css.tabContainer}>
<TButtonTab <TButtonTab
contents={buttonTabList} contents={buttonTabList}
@@ -284,11 +243,11 @@ const SearchResultsNew = ({
<div className={css.buttonContainer}> <div className={css.buttonContainer}>
{hasMore && ( {hasMore && (
<SpottableDiv onClick={downBtnClick} className={css.downBtn}> <SpottableDiv onClick={downBtnClick} className={css.downBtn}>
<CustomImage className={css.btnImg} src={downBtnImg} /> <CustomImage className={css.btnImg} src={downBtnImg} alt="Down arrow" />
</SpottableDiv> </SpottableDiv>
)} )}
<SpottableDiv onClick={upBtnClick} className={css.upBtn}> <SpottableDiv onClick={upBtnClick} className={css.upBtn}>
<CustomImage className={css.btnImg} src={upBtnImg} /> <CustomImage className={css.btnImg} src={upBtnImg} alt="Up arrow" />
</SpottableDiv> </SpottableDiv>
</div> </div>
</div> </div>

View File

@@ -14,7 +14,9 @@ const ItemCard = ({ onClick, itemInfo }) => {
const _handleItemClick = useCallback( const _handleItemClick = useCallback(
(patnrId, prdtId) => (ev) => { (patnrId, prdtId) => (ev) => {
onClick && onClick(ev); if (onClick) {
onClick(ev);
}
dispatch( dispatch(
pushPanel({ pushPanel({
name: panel_names.DETAIL_PANEL, name: panel_names.DETAIL_PANEL,
@@ -25,20 +27,17 @@ const ItemCard = ({ onClick, itemInfo }) => {
}) })
); );
}, },
[onClick] [onClick, dispatch]
); );
return ( return (
<> <>
<TScroller <TScroller className={css.container} spotlightId={SpotlightIds.SEARCH_ITEM}>
className={css.container}
spotlightId={SpotlightIds.SEARCH_ITEM}
>
{itemInfo.map((item, index) => { {itemInfo.map((item, index) => {
const { thumbnail, title, dcPrice, price, soldout, contentId } = item; const { thumbnail, title, dcPrice, price, soldout, contentId } = item;
const tokens = contentId && contentId.split("_"); const tokens = contentId && contentId.split('_');
const patnrId = tokens?.[4] || ""; const patnrId = tokens?.[4] || '';
const prdtId = tokens?.[5] || ""; const prdtId = tokens?.[5] || '';
return ( return (
<TItemCardNew <TItemCardNew
key={prdtId} key={prdtId}
@@ -50,8 +49,8 @@ const ItemCard = ({ onClick, itemInfo }) => {
soldoutFlag={soldout} soldoutFlag={soldout}
dcPrice={dcPrice} dcPrice={dcPrice}
originPrice={price} originPrice={price}
spotlightId={"searchItemContents" + index} spotlightId={'searchItemContents' + index}
label={index * 1 + 1 + " of " + itemInfo.length + 1} label={index * 1 + 1 + ' of ' + itemInfo.length + 1}
lastLabel=" go to detail, button" lastLabel=" go to detail, button"
/> />
); );

View File

@@ -2,29 +2,25 @@ import React, { useCallback } from 'react';
import { useDispatch } from 'react-redux'; import { useDispatch } from 'react-redux';
import SpotlightContainerDecorator import SpotlightContainerDecorator from '@enact/spotlight/SpotlightContainerDecorator';
from '@enact/spotlight/SpotlightContainerDecorator';
import { pushPanel } from '../../../actions/panelActions'; import { pushPanel } from '../../../actions/panelActions';
import TItemCardNew, { import TItemCardNew, { TYPES } from '../../../components/TItemCard/TItemCard.new';
IMAGETYPES,
TYPES,
} from '../../../components/TItemCard/TItemCard.new';
import { panel_names } from '../../../utils/Config'; import { panel_names } from '../../../utils/Config';
import { SpotlightIds } from '../../../utils/SpotlightIds'; import { SpotlightIds } from '../../../utils/SpotlightIds';
import css from './ShowCard.module.less'; import css from './ShowCard.module.less';
const Container = SpotlightContainerDecorator({ enterTo: null }, "div"); const Container = SpotlightContainerDecorator({ enterTo: null }, 'div');
const ShowCard = ({ onClick, showInfo }) => { const ShowCard = ({ onClick, showInfo }) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const _onClick = useCallback( const handleClick = useCallback(
(contentId, thumbnail, liveFlag) => { (contentId, thumbnail, liveFlag) => () => {
const tokens = contentId && contentId.split("_"); const tokens = contentId && contentId.split('_');
const linkTpCd = tokens[1] || ""; const linkTpCd = tokens[1] || '';
const patnrId = tokens[4] || ""; const patnrId = tokens[4] || '';
const showId = tokens[5] || ""; const showId = tokens[5] || '';
if (onClick) { if (onClick) {
onClick(); onClick();
@@ -39,30 +35,20 @@ const ShowCard = ({ onClick, showInfo }) => {
chanId: showId, chanId: showId,
linkTpCd, linkTpCd,
thumbnail, thumbnail,
shptmBanrTpNm: liveFlag === "Y" ? "LIVE" : "VOD", shptmBanrTpNm: liveFlag === 'Y' ? 'LIVE' : 'VOD',
}, },
}) })
); );
}, },
[onClick] [onClick, dispatch]
); );
return ( return (
<Container className={css.container} spotlightId={SpotlightIds.SEARCH_SHOW}> <Container className={css.container} spotlightId={SpotlightIds.SEARCH_SHOW}>
{showInfo.map((item, index) => { {showInfo.map((item, index) => {
const { const { contentId, liveFlag, partnerLogo, thumbnail, title } = item;
contentId, const tokkens = contentId && contentId.split('_');
endTime, const showId = tokkens[5] || '';
liveFlag,
partnerId,
partnerLogo,
partnerName,
startTime,
thumbnail,
title,
} = item;
const tokkens = contentId && contentId.split("_");
const showId = tokkens[5] || "";
return ( return (
<TItemCardNew <TItemCardNew
type={TYPES.videoShow} type={TYPES.videoShow}
@@ -71,11 +57,9 @@ const ShowCard = ({ onClick, showInfo }) => {
imageSource={thumbnail} imageSource={thumbnail}
productName={title} productName={title}
logo={partnerLogo} logo={partnerLogo}
onClick={() => { onClick={handleClick(contentId, thumbnail, liveFlag)}
_onClick(contentId, thumbnail, liveFlag);
}}
productId={showId} productId={showId}
spotlightId={"categoryShowContents" + index} spotlightId={'categoryShowContents' + index}
data-wheel-point="true" data-wheel-point="true"
logoDisplay="true" logoDisplay="true"
/> />

View File

@@ -19,13 +19,9 @@ const Container = Spottable('div');
export default function TInput({ export default function TInput({
kind, kind,
icon, icon,
border,
color,
className, className,
spotlightDisabled, spotlightDisabled,
spotlightBoxDisabled,
spotlightId, spotlightId,
disabled,
onKeyDown, onKeyDown,
scrollTop, scrollTop,
onIconClick, onIconClick,
@@ -79,13 +75,13 @@ export default function TInput({
onFocus(); onFocus();
} }
handleScrollReset(); handleScrollReset();
}, [onFocus]); }, [onFocus, handleScrollReset]);
const _onBlur = useCallback(() => { const _onBlur = useCallback(() => {
if (onBlur) { if (onBlur) {
onBlur(); onBlur();
} }
handleStopScrolling(); handleStopScrolling();
}, [onBlur]); }, [onBlur, handleStopScrolling]);
// onActivate: 내부 input에 실제로 포커스가 가서 입력 가능한 상태 (webOS 키보드가 뜨는 시점) // onActivate: 내부 input에 실제로 포커스가 가서 입력 가능한 상태 (webOS 키보드가 뜨는 시점)
const _onActivate = useCallback(() => { const _onActivate = useCallback(() => {

View File

@@ -63,7 +63,6 @@ export const VOICE_VERSION = {
const OVERLAY_SPOTLIGHT_ID = 'voice-input-overlay-container'; const OVERLAY_SPOTLIGHT_ID = 'voice-input-overlay-container';
const INPUT_SPOTLIGHT_ID = 'voice-overlay-input-box'; const INPUT_SPOTLIGHT_ID = 'voice-overlay-input-box';
const MIC_SPOTLIGHT_ID = 'voice-overlay-mic-button'; const MIC_SPOTLIGHT_ID = 'voice-overlay-mic-button';
const MIC_WEBSPEECH_SPOTLIGHT_ID = 'voice-overlay-mic-webspeech-button';
// 🔧 실험적 기능: Wake Word Detection ("Hey Shoptime") // 🔧 실험적 기능: Wake Word Detection ("Hey Shoptime")
// false로 설정하면 이 기능은 완전히 비활성화됩니다 // false로 설정하면 이 기능은 완전히 비활성화됩니다
@@ -93,7 +92,6 @@ const VoiceInputOverlay = ({
suggestions = [], suggestions = [],
searchQuery = '', searchQuery = '',
onSearchChange, onSearchChange,
onSearchSubmit,
}) => { }) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const lastFocusedElement = useRef(null); const lastFocusedElement = useRef(null);
@@ -117,7 +115,6 @@ const VoiceInputOverlay = ({
wakeWordRestartTimerRef, wakeWordRestartTimerRef,
]; ];
const [inputFocus, setInputFocus] = useState(false);
const [micFocused, setMicFocused] = useState(false); const [micFocused, setMicFocused] = useState(false);
// 내부 모드 상태 관리 (prompt -> listening -> response -> close) // 내부 모드 상태 관리 (prompt -> listening -> response -> close)
const [currentMode, setCurrentMode] = useState(mode); const [currentMode, setCurrentMode] = useState(mode);
@@ -126,7 +123,7 @@ const VoiceInputOverlay = ({
// STT 응답 텍스트 저장 // STT 응답 텍스트 저장
const [sttResponseText, setSttResponseText] = useState(''); const [sttResponseText, setSttResponseText] = useState('');
// Voice Version (어떤 음성 시스템을 사용할지 결정) // Voice Version (어떤 음성 시스템을 사용할지 결정)
const [voiceVersion, setVoiceVersion] = useState(VOICE_VERSION.WEB_SPEECH); const voiceVersion = VOICE_VERSION.WEB_SPEECH;
// 🔊 Beep 소리 재생 함수 - zero dependencies // 🔊 Beep 소리 재생 함수 - zero dependencies
const playBeep = useCallback(() => { const playBeep = useCallback(() => {
@@ -222,16 +219,15 @@ const VoiceInputOverlay = ({
} }
}, []); }, []);
const { isListening, interimText, startListening, stopListening, error, isSupported } = const { isListening, interimText, startListening, stopListening, isSupported } = useWebSpeech(
useWebSpeech( isVisible, // Overlay가 열려있을 때만 활성화 (voiceInputMode와 무관하게 초기화)
isVisible, // Overlay가 열려있을 때만 활성화 (voiceInputMode와 무관하게 초기화) handleWebSpeechSTT,
handleWebSpeechSTT, {
{ lang: 'en-US',
lang: 'en-US', continuous: false, // 침묵 감지 후 자동 종료
continuous: false, // 침묵 감지 후 자동 종료 interimResults: true,
interimResults: true, }
} );
);
// ⛔ VUI 테스트 비활성화: VoicePanel 독립 테스트 시 충돌 방지 // ⛔ VUI 테스트 비활성화: VoicePanel 독립 테스트 시 충돌 방지
// Redux에서 voice 상태 가져오기 // Redux에서 voice 상태 가져오기
@@ -393,7 +389,7 @@ const VoiceInputOverlay = ({
stopListening(); stopListening();
} }
}; };
}, [ENABLE_WAKE_WORD, isVisible, currentMode, startListening, stopListening]); }, [isVisible, currentMode, startListening, stopListening]);
// Overlay가 열릴 때 포커스를 overlay 내부로 이동 // Overlay가 열릴 때 포커스를 overlay 내부로 이동
useEffect(() => { useEffect(() => {
@@ -459,6 +455,7 @@ const VoiceInputOverlay = ({
audioContextRef.current = null; audioContextRef.current = null;
} }
}; };
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, []);
// Suggestion 버튼 클릭 핸들러 - Input 창에 텍스트만 설정 // Suggestion 버튼 클릭 핸들러 - Input 창에 텍스트만 설정
@@ -615,15 +612,6 @@ const VoiceInputOverlay = ({
handleTalkAgain, handleTalkAgain,
]); ]);
// 입력창 포커스 핸들러
const handleInputFocus = useCallback(() => {
setInputFocus(true);
}, []);
const handleInputBlur = useCallback(() => {
setInputFocus(false);
}, []);
// 마이크 버튼 포커스 핸들러 (VUI) // 마이크 버튼 포커스 핸들러 (VUI)
const handleMicFocus = useCallback(() => { const handleMicFocus = useCallback(() => {
setMicFocused(true); setMicFocused(true);
@@ -633,52 +621,6 @@ const VoiceInputOverlay = ({
setMicFocused(false); setMicFocused(false);
}, []); }, []);
// VUI 마이크 버튼 클릭 핸들러 (voiceVersion이 VUI일 때만 작동)
const handleVUIMicClick = useCallback(
(e) => {
// voiceVersion이 VUI가 아니면 차단
if (voiceVersion !== VOICE_VERSION.VUI) return;
if (DEBUG_MODE) {
console.log('[VoiceInputOverlay.v2] handleVUIMicClick called, currentMode:', currentMode);
}
// 이벤트 전파 방지 - dim 레이어의 onClick 실행 방지
if (e && e.stopPropagation) {
e.stopPropagation();
}
if (e && e.nativeEvent && e.nativeEvent.stopImmediatePropagation) {
e.nativeEvent.stopImmediatePropagation();
}
if (currentMode === VOICE_MODES.PROMPT) {
// prompt 모드에서 클릭 시 -> VUI listening 모드로 전환
if (DEBUG_MODE) {
console.log('[VoiceInputOverlay.v2] Switching to VUI LISTENING mode');
}
setVoiceInputMode(VOICE_INPUT_MODE.VUI);
setCurrentMode(VOICE_MODES.LISTENING);
// 이 시점에서 webOS Voice Framework가 자동으로 음성인식 시작
// (이미 registerVoiceFramework()로 등록되어 있으므로)
} else if (currentMode === VOICE_MODES.LISTENING && voiceInputMode === VOICE_INPUT_MODE.VUI) {
// VUI listening 모드에서 클릭 시 -> 종료
if (DEBUG_MODE) {
console.log('[VoiceInputOverlay.v2] Closing from VUI LISTENING mode');
}
setVoiceInputMode(null);
onClose();
} else {
// 기타 모드에서는 바로 종료
if (DEBUG_MODE) {
console.log('[VoiceInputOverlay.v2] Closing from other mode');
}
setVoiceInputMode(null);
onClose();
}
},
[currentMode, voiceInputMode, voiceVersion, onClose]
);
// Overlay 닫기 핸들러 (모든 닫기 동작을 통합) // Overlay 닫기 핸들러 (모든 닫기 동작을 통합)
const handleClose = useCallback(() => { const handleClose = useCallback(() => {
if (DEBUG_MODE) { if (DEBUG_MODE) {
@@ -756,6 +698,18 @@ const VoiceInputOverlay = ({
[currentMode, handleClose, playBeep, startListening, stopListening] [currentMode, handleClose, playBeep, startListening, stopListening]
); );
// 마이크 버튼 키다운 핸들러
const handleMicKeyDown = useCallback(
(e) => {
if (e.key === 'Enter' || e.keyCode === 13) {
e.preventDefault();
e.stopPropagation();
handleWebSpeechMicClick(e);
}
},
[handleWebSpeechMicClick]
);
// Memoize microphone button rendering // Memoize microphone button rendering
const microphoneButton = useMemo(() => { const microphoneButton = useMemo(() => {
if (voiceVersion !== VOICE_VERSION.WEB_SPEECH) return null; if (voiceVersion !== VOICE_VERSION.WEB_SPEECH) return null;
@@ -771,13 +725,7 @@ const VoiceInputOverlay = ({
micFocused && css.focused micFocused && css.focused
)} )}
onClick={handleWebSpeechMicClick} onClick={handleWebSpeechMicClick}
onKeyDown={(e) => { onKeyDown={handleMicKeyDown}
if (e.key === 'Enter' || e.keyCode === 13) {
e.preventDefault();
e.stopPropagation();
handleWebSpeechMicClick(e);
}
}}
onFocus={handleMicFocus} onFocus={handleMicFocus}
onBlur={handleMicBlur} onBlur={handleMicBlur}
spotlightId={MIC_SPOTLIGHT_ID} spotlightId={MIC_SPOTLIGHT_ID}
@@ -806,6 +754,7 @@ const VoiceInputOverlay = ({
voiceInputMode, voiceInputMode,
micFocused, micFocused,
handleWebSpeechMicClick, handleWebSpeechMicClick,
handleMicKeyDown,
handleMicFocus, handleMicFocus,
handleMicBlur, handleMicBlur,
]); ]);
@@ -845,7 +794,7 @@ const VoiceInputOverlay = ({
<TFullPopup <TFullPopup
open={isVisible} open={isVisible}
onClose={handleClose} onClose={handleClose}
noAutoDismiss={true} noAutoDismiss
spotlightRestrict="self-only" spotlightRestrict="self-only"
spotlightId={OVERLAY_SPOTLIGHT_ID} spotlightId={OVERLAY_SPOTLIGHT_ID}
noAnimation={false} noAnimation={false}
@@ -877,8 +826,6 @@ const VoiceInputOverlay = ({
onKeyDown={handleInputKeyDown} onKeyDown={handleInputKeyDown}
onIconClick={handleSearchSubmit} onIconClick={handleSearchSubmit}
spotlightId={INPUT_SPOTLIGHT_ID} spotlightId={INPUT_SPOTLIGHT_ID}
onFocus={handleInputFocus}
onBlur={handleInputBlur}
/> />
{/* voiceVersion에 따라 하나의 마이크만 표시 */} {/* voiceVersion에 따라 하나의 마이크만 표시 */}

View File

@@ -16,8 +16,8 @@ const PromptContainer = SpotlightContainerDecorator(
); );
const VoicePromptScreen = ({ title = 'Try saying', suggestions = [], onSuggestionClick }) => { const VoicePromptScreen = ({ title = 'Try saying', suggestions = [], onSuggestionClick }) => {
const handleBubbleClick = (suggestion, index) => { const handleBubbleClick = (suggestion) => {
console.log(`[VoicePromptScreen] Bubble clicked: ${suggestion}`, index); console.log(`[VoicePromptScreen] Bubble clicked: ${suggestion}`);
// 부모 컴포넌트로 suggestion 텍스트 전달 (API 호출은 부모에서 처리) // 부모 컴포넌트로 suggestion 텍스트 전달 (API 호출은 부모에서 처리)
if (onSuggestionClick) { if (onSuggestionClick) {
@@ -33,16 +33,19 @@ const VoicePromptScreen = ({ title = 'Try saying', suggestions = [], onSuggestio
> >
<div className={css.title}>{title}</div> <div className={css.title}>{title}</div>
<div className={css.suggestionsContainer}> <div className={css.suggestionsContainer}>
{suggestions.map((suggestion, index) => ( {suggestions.map((suggestion, index) => {
<SpottableBubble const handleClick = () => handleBubbleClick(suggestion);
key={index} return (
className={css.bubbleMessage} <SpottableBubble
onClick={() => handleBubbleClick(suggestion, index)} key={index}
spotlightId={`voice-bubble-${index}`} className={css.bubbleMessage}
> onClick={handleClick}
<div className={css.bubbleText}>{suggestion}</div> spotlightId={`voice-bubble-${index}`}
</SpottableBubble> >
))} <div className={css.bubbleText}>{suggestion}</div>
</SpottableBubble>
);
})}
</div> </div>
</PromptContainer> </PromptContainer>
); );

View File

@@ -46,18 +46,16 @@ const VoiceResponse = ({ responseText = '', onTalkAgain }) => {
} }
}; };
const handleButtonClick = () => {
handleTalkAgainClick();
};
return ( return (
<ResponseContainer className={css.container} spotlightId="voice-response-container"> <ResponseContainer className={css.container} spotlightId="voice-response-container">
<div className={css.responseContainer}> <div className={css.responseContainer}>
<SpottableButton <SpottableButton
className={css.talkAgainButton} className={css.talkAgainButton}
onClick={handleTalkAgainClick} onClick={handleButtonClick}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.keyCode === 13) {
e.preventDefault();
handleTalkAgainClick();
}
}}
spotlightId="voice-talk-again-button" spotlightId="voice-talk-again-button"
> >
TALK AGAIN TALK AGAIN

View File

@@ -39,7 +39,7 @@ const ShowUserReviews = ({ hasVideo, launchedFromPlayer }) => {
}, },
}) })
); );
}, [dispatch, productData, reviewData, reviewListData]); }, [dispatch, productData, reviewData, reviewListData, hasVideo, launchedFromPlayer]);
const handleKeyDown = useCallback((event) => { const handleKeyDown = useCallback((event) => {
if (event.key === 'ArrowUp') { if (event.key === 'ArrowUp') {