diff --git a/com.twin.app.shoptime/src/views/PlayerPanel/PlayerTabContents/TabContents/ShopNowContents.jsx b/com.twin.app.shoptime/src/views/PlayerPanel/PlayerTabContents/TabContents/ShopNowContents.jsx index 6803ca1c..1d5d04fb 100644 --- a/com.twin.app.shoptime/src/views/PlayerPanel/PlayerTabContents/TabContents/ShopNowContents.jsx +++ b/com.twin.app.shoptime/src/views/PlayerPanel/PlayerTabContents/TabContents/ShopNowContents.jsx @@ -1,61 +1,37 @@ -import React, { - useCallback, - useEffect, - useMemo, - useRef, - useState, -} from 'react'; +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { - useDispatch, - useSelector, -} from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; import { Job } from '@enact/core/util'; import Spotlight from '@enact/spotlight'; -import SpotlightContainerDecorator - from '@enact/spotlight/SpotlightContainerDecorator'; -import { - getContainerNode, - setContainerLastFocusedElement, -} from '@enact/spotlight/src/container'; +import SpotlightContainerDecorator from '@enact/spotlight/SpotlightContainerDecorator'; +import { getContainerNode, setContainerLastFocusedElement } from '@enact/spotlight/src/container'; import { sendLogTotalRecommend } from '../../../../actions/logActions'; import { pushPanel } from '../../../../actions/panelActions'; import { hidePlayerOverlays } from '../../../../actions/videoPlayActions'; import TItemCard, { TYPES } from '../../../../components/TItemCard/TItemCard'; -import TVirtualGridList - from '../../../../components/TVirtualGridList/TVirtualGridList'; +import TVirtualGridList from '../../../../components/TVirtualGridList/TVirtualGridList'; import useScrollTo from '../../../../hooks/useScrollTo'; -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 { scaleH } from '../../../../utils/helperMethods'; -import ListEmptyContents - from '../TabContents/ListEmptyContents/ListEmptyContents'; +import ListEmptyContents from '../TabContents/ListEmptyContents/ListEmptyContents'; import css1 from './ShopNowContents.module.less'; import cssV2 from './ShopNowContents.v2.module.less'; const extractPriceInfo = (priceInfo) => { - if (!priceInfo) - return { originalPrice: "", discountedPrice: "", discountRate: "" }; + if (!priceInfo) return { originalPrice: '', discountedPrice: '', discountRate: '' }; - const parts = priceInfo.split("|").map((part) => part.trim()); + const parts = priceInfo.split('|').map((part) => part.trim()); return { - originalPrice: parts[0] || "", - discountedPrice: parts[1] || "", - discountRate: parts[4] || "", + originalPrice: parts[0] || '', + discountedPrice: parts[1] || '', + discountRate: parts[4] || '', }; }; -const Container = SpotlightContainerDecorator( - { enterTo: "last-focused" }, - "div" -); +const Container = SpotlightContainerDecorator({ enterTo: 'last-focused' }, 'div'); export default function ShopNowContents({ shopNowInfo, videoVerticalVisible, @@ -65,16 +41,40 @@ export default function ShopNowContents({ panelInfo, tabTitle, version = 1, - direction = "vertical", + direction = 'vertical', }) { const css = version === 2 ? cssV2 : css1; const { getScrollTo, scrollTop } = useScrollTo(); const dispatch = useDispatch(); const paenls = useSelector((state) => state.panels.panels[1]?.panelInfo); + const youmaylikeInfos = useSelector((state) => state.main.youmaylikeInfos); const scrollTopJob = useRef(new Job((func) => func(), 0)); const [height, setHeight] = useState(); const gridStyle = useMemo(() => ({ height: `${height}px` }), [height]); + // ShopNow + YouMayLike 통합 아이템 (v2이고 shopNow < 3일 때만) + const combinedItems = useMemo(() => { + if (!shopNowInfo) return []; + + // 기본: ShopNow 아이템 + let items = shopNowInfo.map((item) => ({ + ...item, + _type: 'shopnow', + })); + + // v2 + ShopNow < 3 + YouMayLike 데이터 존재 시 통합 + if (version === 2 && shopNowInfo.length < 3 && youmaylikeInfos && youmaylikeInfos.length > 0) { + items = items.concat( + youmaylikeInfos.map((item) => ({ + ...item, + _type: 'youmaylike', + })) + ); + } + + return items; + }, [shopNowInfo, youmaylikeInfos, version]); + // 각 상품별 가격 정보를 미리 계산 const priceInfoMap = useMemo(() => { if (!shopNowInfo) return {}; @@ -90,7 +90,7 @@ export default function ShopNowContents({ useEffect(() => { return () => { - const gridListId = "playVideoShopNowBox"; + const gridListId = 'playVideoShopNowBox'; const girdList = getContainerNode(gridListId); if (girdList) setContainerLastFocusedElement(null, [gridListId]); @@ -129,6 +129,59 @@ export default function ShopNowContents({ const renderItem = useCallback( ({ index, ...rest }) => { + const item = combinedItems[index]; + + // ===== YouMayLike 아이템 처리 ===== + if (item._type === 'youmaylike') { + const { imgUrl, patnrId, prdtId, prdtNm, priceInfo, offerInfo } = item; + + // YouMayLike 시작 지점 여부 (구분선 표시) + const isYouMayLikeStart = shopNowInfo && index === shopNowInfo.length; + + const handleItemClick = () => { + dispatch( + pushPanel({ + name: panel_names.DETAIL_PANEL, + panelInfo: { + showNm: playListInfo?.showNm, + showId: playListInfo?.showId, + liveFlag: playListInfo?.liveFlag, + thumbnailUrl: playListInfo?.thumbnailUrl, + patnrId, + prdtId, + launchedFromPlayer: true, + }, + }) + ); + }; + + return ( + <> + {isYouMayLikeStart &&
} + { + if (handleItemFocus) { + handleItemFocus(LOG_MENU.FULL_YOU_MAY_LIKE); + } + }} + spotlightId={`you-may-like-item-${index}`} + type={TYPES.horizontal} + version={version} + /> + + ); + } + + // ===== ShopNow 아이템 처리 (기존 로직) ===== const { imgUrls, patnrId, @@ -140,11 +193,10 @@ export default function ShopNowContents({ patncNm, brndNm, catNm, - } = shopNowInfo[index]; + } = item; // 미리 계산된 가격 정보를 사용 - const { originalPrice, discountedPrice, discountRate } = - priceInfoMap[index] || {}; + const { originalPrice, discountedPrice, discountRate } = priceInfoMap[index] || {}; const handleItemClick = () => { const params = { @@ -173,7 +225,7 @@ export default function ShopNowContents({ showId: playListInfo?.showId, liveFlag: playListInfo?.liveFlag, thumbnailUrl: playListInfo?.thumbnailUrl, - liveReqFlag: panelInfo?.shptmBanrTpNm === "LIVE" && "Y", + liveReqFlag: panelInfo?.shptmBanrTpNm === 'LIVE' && 'Y', patnrId, prdtId, launchedFromPlayer: true, @@ -202,7 +254,7 @@ export default function ShopNowContents({ // v2에서 첫 번째 아이템일 때 위로 가면 Close 버튼으로 e.stopPropagation(); e.preventDefault(); - Spotlight.focus("shownow_close_button"); + Spotlight.focus('shownow_close_button'); } : undefined } @@ -212,7 +264,7 @@ export default function ShopNowContents({ ); }, [ - shopNowInfo, + combinedItems, videoVerticalVisible, panelInfo?.shptmBanrTpNm, priceInfoMap, @@ -221,25 +273,25 @@ export default function ShopNowContents({ playListInfo, dispatch, version, + handleItemFocus, + handleFocus, ] ); return ( <> - {shopNowInfo && shopNowInfo.length > 0 ? ( + {combinedItems && combinedItems.length > 0 ? ( diff --git a/com.twin.app.shoptime/src/views/PlayerPanel/PlayerTabContents/TabContents/ShopNowContents.v2.module.less b/com.twin.app.shoptime/src/views/PlayerPanel/PlayerTabContents/TabContents/ShopNowContents.v2.module.less index b3913ff4..1b7f8781 100644 --- a/com.twin.app.shoptime/src/views/PlayerPanel/PlayerTabContents/TabContents/ShopNowContents.v2.module.less +++ b/com.twin.app.shoptime/src/views/PlayerPanel/PlayerTabContents/TabContents/ShopNowContents.v2.module.less @@ -51,3 +51,12 @@ } } } + +// YouMayLike 시작 지점 구분선 +.youMayLikeDivider { + width: 2px !important; + height: 445px; + background: rgba(234, 234, 234, 0.3); + margin-right: 15px; + flex-shrink: 0; +} diff --git a/com.twin.app.shoptime/src/views/PlayerPanel/PlayerTabContents/v2/ShopNowContainer.module.less b/com.twin.app.shoptime/src/views/PlayerPanel/PlayerTabContents/v2/ShopNowContainer.module.less index 01c07187..3efd7c1b 100644 --- a/com.twin.app.shoptime/src/views/PlayerPanel/PlayerTabContents/v2/ShopNowContainer.module.less +++ b/com.twin.app.shoptime/src/views/PlayerPanel/PlayerTabContents/v2/ShopNowContainer.module.less @@ -12,8 +12,11 @@ border-top: 1px solid rgba(234, 234, 234, 0.8); overflow: hidden; .flex(@display: flex, @justifyCenter: flex-start, @alignCenter: flex-start, @direction: column); - gap: 40px; z-index: 5; + + > * + * { + margin-top: 40px; + } } .header { @@ -23,7 +26,10 @@ overflow: hidden; border-radius: 100px; .flex(@display: flex, @justifyCenter: flex-start, @alignCenter: center); - gap: 15px; + + > * + * { + margin-left: 15px; + } } .iconWrapper { @@ -51,11 +57,14 @@ .productList { align-self: stretch; .flex(@display: flex, @justifyCenter: flex-start, @alignCenter: flex-start); - gap: 30px; overflow-x: auto; overflow-y: hidden; - > * + * { - margin-left: 0; + > * { + margin-right: 30px; + + &:last-child { + margin-right: 0; + } } } diff --git a/com.twin.app.shoptime/src/views/PlayerPanel/PlayerTabContents/v2/ShopNowItem.module.less b/com.twin.app.shoptime/src/views/PlayerPanel/PlayerTabContents/v2/ShopNowItem.module.less index 5129332a..8de653b0 100644 --- a/com.twin.app.shoptime/src/views/PlayerPanel/PlayerTabContents/v2/ShopNowItem.module.less +++ b/com.twin.app.shoptime/src/views/PlayerPanel/PlayerTabContents/v2/ShopNowItem.module.less @@ -8,10 +8,13 @@ border-radius: 12px; border: 1px solid transparent; .flex(@display: flex, @justifyCenter: flex-start, @alignCenter: flex-start, @direction: column); - gap: 15px; flex-shrink: 0; transition: all 0.3s ease; + > * + * { + margin-top: 15px; + } + &:focus { border-color: @PRIMARY_COLOR_RED; outline: 4px @PRIMARY_COLOR_RED solid; @@ -33,7 +36,10 @@ .productInfo { align-self: stretch; .flex(@display: flex, @justifyCenter: center, @alignCenter: flex-start, @direction: column); - gap: 15px; + + > * + * { + margin-top: 15px; + } } .productName { @@ -49,13 +55,19 @@ .priceWrapper { align-self: stretch; .flex(@display: flex, @justifyCenter: flex-start, @alignCenter: flex-start); - gap: 10px; + + > * + * { + margin-left: 10px; + } } .priceContainer { padding: 4px 0; .flex(@display: flex, @justifyCenter: flex-start, @alignCenter: center); - gap: 11px; + + > * + * { + margin-left: 11px; + } } .salePrice { diff --git a/com.twin.app.shoptime/src/views/PlayerPanel/PlayerTabContents/v2/TabContainer.v2.jsx b/com.twin.app.shoptime/src/views/PlayerPanel/PlayerTabContents/v2/TabContainer.v2.jsx index 339b2c16..6e04ea46 100644 --- a/com.twin.app.shoptime/src/views/PlayerPanel/PlayerTabContents/v2/TabContainer.v2.jsx +++ b/com.twin.app.shoptime/src/views/PlayerPanel/PlayerTabContents/v2/TabContainer.v2.jsx @@ -1,35 +1,26 @@ -import React, { - useCallback, - useEffect, -} from 'react'; +import React, { useCallback, useEffect } from 'react'; import classNames from 'classnames'; +import { useSelector } from 'react-redux'; import Spotlight from '@enact/spotlight'; -import SpotlightContainerDecorator - from '@enact/spotlight/SpotlightContainerDecorator'; +import SpotlightContainerDecorator from '@enact/spotlight/SpotlightContainerDecorator'; import Spottable from '@enact/spotlight/Spottable'; // import icon_arrow_right from '../../../../../assets/images/icons'; -import icon_arrow_dwon - from '../../../../../assets/images/player/icon_tabcontainer_arrow_down.png'; -import icon_shop_now - from '../../../../../assets/images/player/icon_tabcontainer_shopnow.png'; +import icon_arrow_dwon from '../../../../../assets/images/player/icon_tabcontainer_arrow_down.png'; +import icon_shop_now from '../../../../../assets/images/player/icon_tabcontainer_shopnow.png'; import { LOG_MENU } from '../../../../utils/Config'; import { $L } from '../../../../utils/helperMethods'; import { SpotlightIds } from '../../../../utils/SpotlightIds'; import LiveChannelContents from '../TabContents/LiveChannelContents'; import ShopNowContents from '../TabContents/ShopNowContents'; -import YouMayLikeContents from '../TabContents/YouMayLikeContents'; import ShopNowButton from './ShopNowButton'; import css from './TabContainer.v2.module.less'; -const Container = SpotlightContainerDecorator( - { enterTo: "last-focused" }, - "div" -); +const Container = SpotlightContainerDecorator({ enterTo: 'last-focused' }, 'div'); -const SpottableDiv = Spottable("div"); +const SpottableDiv = Spottable('div'); export default function TabContainerV2({ panelInfo, @@ -49,11 +40,11 @@ export default function TabContainerV2({ onTabClose, // 탭 닫기 콜백 함수 tabVisible, }) { + const youmaylikeInfos = useSelector((state) => state.main.youmaylikeInfos); + const tabList = [ - $L("SHOP NOW"), - panelInfo?.shptmBanrTpNm === "LIVE" - ? $L("LIVE CHANNEL") - : $L("FEATURED SHOWS"), + $L('SHOP NOW'), + panelInfo?.shptmBanrTpNm === 'LIVE' ? $L('LIVE CHANNEL') : $L('FEATURED SHOWS'), ]; useEffect(() => { @@ -64,10 +55,8 @@ export default function TabContainerV2({ } if (tabIndex === 1) { - const isLive = panelInfo?.shptmBanrTpNm === "LIVE"; - nowMenu = isLive - ? LOG_MENU.FULL_LIVE_CHANNELS - : LOG_MENU.FULL_FEATURED_SHOWS; + const isLive = panelInfo?.shptmBanrTpNm === 'LIVE'; + nowMenu = isLive ? LOG_MENU.FULL_LIVE_CHANNELS : LOG_MENU.FULL_FEATURED_SHOWS; } if (nowMenu) { @@ -89,7 +78,7 @@ export default function TabContainerV2({ if (videoVerticalVisible) { e.stopPropagation(); e.preventDefault(); - Spotlight.focus("spotlightId-video-contaienr"); + Spotlight.focus('spotlightId-video-contaienr'); } }, [videoVerticalVisible] @@ -120,7 +109,7 @@ export default function TabContainerV2({ useEffect(() => { if (tabIndex === 2) { setTimeout(() => { - Spotlight.focus("below-tab-shop-now-button"); + Spotlight.focus('below-tab-shop-now-button'); }, 100); } }, [tabIndex]); @@ -136,45 +125,53 @@ export default function TabContainerV2({ > {tabVisible && tabIndex === 0 && ( <> -
- { - // 첫 번째 ShopNow 아이템으로 포커스 이동 - e.stopPropagation(); - e.preventDefault(); - Spotlight.focus("shop-now-item-0"); - }} - > -
- shop now icon -
-
SHOP NOW
-
- arrow down -
-
- {/* { - // 첫 번째 ShopNow 아이템으로 포커스 이동 - e.stopPropagation(); - e.preventDefault(); - Spotlight.focus("shop-now-item-0"); - }} - > - × - */} +
+
+ { + // 첫 번째 ShopNow 아이템으로 포커스 이동 + e.stopPropagation(); + e.preventDefault(); + Spotlight.focus('shop-now-item-0'); + }} + > +
+ shop now icon +
+
SHOP NOW
+
+ arrow down +
+
+ {/* { + // 첫 번째 ShopNow 아이템으로 포커스 이동 + e.stopPropagation(); + e.preventDefault(); + Spotlight.focus("shop-now-item-0"); + }} + > + × + */} +
+ + {/* YouMayAlso Like 헤더 (ShopNow 아이템 < 3 && YouMayLike 데이터 존재) */} + {shopNowInfo && + shopNowInfo.length < 3 && + youmaylikeInfos && + youmaylikeInfos.length > 0 && ( +
+
You may also like
+
+ )}
- {shopNowInfo && shopNowInfo.length < 3 && ( - - )} )} @@ -208,7 +196,7 @@ export default function TabContainerV2({ onSpotlightUp={handleSpotlightUpToBackButton} onSpotlightDown={(e) => { // 첫 번째 PlayerItem으로 포커스 이동 - Spotlight.focus("tabChannel-video-0"); + Spotlight.focus('tabChannel-video-0'); }} > LIVE CHANNEL @@ -217,7 +205,7 @@ export default function TabContainerV2({
- {panelInfo?.shptmBanrTpNm === "LIVE" && playListInfo && ( + {panelInfo?.shptmBanrTpNm === 'LIVE' && playListInfo && ( )} - {tabVisible && tabIndex === 2 && ( - - )} + {tabVisible && tabIndex === 2 && }
); } diff --git a/com.twin.app.shoptime/src/views/PlayerPanel/PlayerTabContents/v2/TabContainer.v2.module.less b/com.twin.app.shoptime/src/views/PlayerPanel/PlayerTabContents/v2/TabContainer.v2.module.less index 382248b5..7436ba63 100644 --- a/com.twin.app.shoptime/src/views/PlayerPanel/PlayerTabContents/v2/TabContainer.v2.module.less +++ b/com.twin.app.shoptime/src/views/PlayerPanel/PlayerTabContents/v2/TabContainer.v2.module.less @@ -89,14 +89,22 @@ } } +// ShopNow 헤더 컨테이너 (ShopNow + YouMayAlsoLike 헤더를 같은 라인에 배치) +.shopNowHeaderContainer { + display: flex; + align-items: center; + width: 100%; +} + // SHOP NOW 헤더 스타일 (ShopNowContainer 참고) .shopNowHeader { - width: 100%; + width: auto; height: 70px; padding: 20px 0; overflow: hidden; border-radius: 100px; .flex(@display: flex, @justifyCenter: space-between, @alignCenter: center); + flex-shrink: 0; } .shopNowIconWrapper { @@ -239,6 +247,31 @@ } } } + +// YOU MAY ALSO LIKE 헤더 스타일 +.youMayAlsoLikeHeader { + margin-left: 90px; + height: 100%; + display: flex; + align-items: center; + justify-content: flex-start; + padding: 20px 0; + overflow: hidden; + border-radius: 100px; + flex-shrink: 0; +} + +.youMayAlsoLikeText { + margin-right: 15px; + color: #EAEAEA; + font-size: 24px; + font-family: @baseFont; + font-weight: 700; + line-height: 31px; + word-wrap: break-word; + white-space: nowrap; +} + /* 애니메이션 정의 */ @keyframes slideInFromRight { from {