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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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