[251114] feat: ProductAllSection ProductVideo.v3

🕐 커밋 시간: 2025. 11. 14. 14:24:07

📊 변경 통계:
  • 총 파일: 13개
  • 추가: +135줄
  • 삭제: -54줄

📁 추가된 파일:
  + com.twin.app.shoptime/src/components/VideoPlayer/VideoPlayer.v3.js
  + com.twin.app.shoptime/src/components/VideoPlayer/VideoPlayer.v3.module.less
  + com.twin.app.shoptime/src/hooks/useMediaPanelController.js
  + com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/ProductVideo/ProductVideo.v3.jsx
  + com.twin.app.shoptime/src/views/MediaPanel/MediaPanel.v3.jsx
  + com.twin.app.shoptime/src/views/MediaPanel/MediaPanel.v3.module.less

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/actions/mediaActions.js
  ~ com.twin.app.shoptime/src/views/DetailPanel/DetailPanel.jsx
  ~ com.twin.app.shoptime/src/views/DetailPanel/ProductAllSection/ProductAllSection.jsx
  ~ com.twin.app.shoptime/src/views/DetailPanel/ProductAllSection/ProductAllSection.module.less
  ~ com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/ProductVideo/ProductVideo.jsx
  ~ com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/ProductVideo/ProductVideo.v2.jsx
  ~ com.twin.app.shoptime/src/views/MainView/MainView.jsx

🔧 함수 변경 내용:
  📄 com.twin.app.shoptime/src/actions/mediaActions.js (javascript):
    🔄 Modified: switchMediaToModal()
  📄 com.twin.app.shoptime/src/views/DetailPanel/ProductAllSection/ProductAllSection.jsx (javascript):
    🔄 Modified: SpotlightContainerDecorator(), extractProductMeta()
  📄 com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/ProductVideo/ProductVideo.jsx (javascript):
     Added: Spottable()
  📄 com.twin.app.shoptime/src/components/VideoPlayer/VideoPlayer.v3.js (javascript):
     Added: getControlsHandleAboveHoldConfig(), shouldJump(), calcNumberValueOfPlaybackRate(), getDurFmt(), getVideoPhoneNumberClassNames()
  📄 com.twin.app.shoptime/src/components/VideoPlayer/VideoPlayer.v3.module.less (unknown):
     Added: gradient(), focused(), position(), rotate(), applySkins(), scale()
  📄 com.twin.app.shoptime/src/hooks/useMediaPanelController.js (javascript):
     Added: MediaPanelControllerProvider(), useMediaPanelController()
  📄 com.twin.app.shoptime/src/views/MediaPanel/MediaPanel.v3.jsx (javascript):
     Added: findSelector(), getLogTpNo(), normalizeModalStyle(), parseValue(), handleVisibilityChange(), onKeyDown(), handleEvent(), propsAreEqual()

🔧 주요 변경 내용:
  • 핵심 비즈니스 로직 개선
  • UI 컴포넌트 아키텍처 개선
This commit is contained in:
2025-11-14 14:24:08 +09:00
parent a33213fb8c
commit fdb9507024
13 changed files with 6817 additions and 257 deletions

View File

@@ -238,10 +238,10 @@ export const switchMediaToModal = (modalContainerId, modalClassName) => (dispatc
export const minimizeModalMedia = () => (dispatch, getState) => {
const panels = getState().panels.panels;
console.log('[minimizeModalMedia] ========== Called ==========');
console.log('[minimizeModalMedia] Total panels:', panels.length);
console.log('[Minimize] ========== Called ==========');
console.log('[Minimize] Total panels:', panels.length);
console.log(
'[minimizeModalMedia] All panels:',
'[Minimize] All panels:',
JSON.stringify(
panels.map((p) => ({ name: p.name, modal: p.panelInfo?.modal })),
null,
@@ -253,15 +253,13 @@ export const minimizeModalMedia = () => (dispatch, getState) => {
(panel) => panel.name === panel_names.MEDIA_PANEL && panel.panelInfo?.modal
);
console.log('[minimizeModalMedia] Found modalMediaPanel:', !!modalMediaPanel);
console.log('[Minimize] Found modalMediaPanel:', !!modalMediaPanel);
if (modalMediaPanel) {
console.log(
'[minimizeModalMedia] modalMediaPanel.panelInfo:',
'[Minimize] modalMediaPanel.panelInfo:',
JSON.stringify(modalMediaPanel.panelInfo, null, 2)
);
console.log(
'[minimizeModalMedia] ✅ Minimizing modal MediaPanel (modal=false, isMinimized=true)'
);
console.log('[Minimize] ✅ Minimizing modal MediaPanel (modal=false, isMinimized=true)');
dispatch(
updatePanel({
name: panel_names.MEDIA_PANEL,
@@ -269,13 +267,14 @@ export const minimizeModalMedia = () => (dispatch, getState) => {
...modalMediaPanel.panelInfo,
modal: false, // fullscreen 모드로 전환
isMinimized: true, // modal-minimized 클래스 적용 (1px 크기)
shouldShrinkTo1px: true, // shrink 플래그 추가
// modalContainerId, modalClassName 등은 복원을 위해 유지
// isPaused는 변경하지 않음 - 재생은 계속됨
},
})
);
} else {
console.log('[minimizeModalMedia] ❌ No modal MediaPanel found - cannot minimize');
console.log('[Minimize] ❌ No modal MediaPanel found - cannot minimize');
}
};
@@ -286,10 +285,10 @@ export const minimizeModalMedia = () => (dispatch, getState) => {
export const restoreModalMedia = () => (dispatch, getState) => {
const panels = getState().panels.panels;
console.log('[restoreModalMedia] ========== Called ==========');
console.log('[restoreModalMedia] Total panels:', panels.length);
console.log('[Restore]] ========== Called ==========');
console.log('[Restore] Total panels:', panels.length);
console.log(
'[restoreModalMedia] All panels:',
'[Restore] All panels:',
JSON.stringify(
panels.map((p) => ({
name: p.name,
@@ -325,6 +324,7 @@ export const restoreModalMedia = () => (dispatch, getState) => {
...minimizedMediaPanel.panelInfo,
modal: true, // modal 모드로 복원 (원래 위치로 복귀)
isMinimized: false, // 최소화 해제
shouldShrinkTo1px: false, // shrink 플래그 초기화
},
})
);

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,843 @@
// VideoPlayer.module.less
//
@import "~@enact/sandstone/styles/variables.less";
@import "~@enact/sandstone/styles/mixins.less";
@import "~@enact/sandstone/styles/skin.less";
@import "../../style/utils.module.less";
@import "../../style/CommonStyle.module.less";
.videoPlayer {
// Set by counting the IconButtons inside the side components.
--liftDistance: 0px;
overflow: hidden;
padding: 2px;
box-sizing: border-box;
:focus {
outline: none !important;
border: none !important;
box-shadow: none !important;
}
.video {
height: calc(100% - 4px);
width: calc(100% - 4px);
background: #000;
}
.media {
height: calc(100% - 4px);
width: calc(100% - 4px);
background: #000;
&.mediaBackground {
&:after {
width: 560px;
height: 200px;
position: absolute;
left: 0;
bottom: 0;
content: "";
background: linear-gradient(
to top,
rgba(255, 255, 255, 1),
transparent
);
opacity: 0.2;
}
}
}
.preloadVideo {
display: none;
}
.thumbnail {
width: 100%;
height: 100%;
position: absolute;
left: 0;
top: 0;
display: flex;
justify-content: center;
align-items: center;
> img {
width: 100%;
max-height: 100%;
border-radius: 12px;
}
&.noRadiusThumbnail {
> img {
border-radius: 0;
}
}
&.verticalThumbnail {
> img {
width: auto;
height: 100%;
border-radius: 0;
}
}
&.smallThumbnail {
&::after {
.focused(@boxShadow:0, @borderRadius: 12px);
border: 6px solid @PRIMARY_COLOR_RED;
top: -4px;
right: -4px;
bottom: -4px;
left: -4px;
}
}
}
.disclaimer {
.size(@w: 100% , @h: 54px);
display: flex;
background-color: rgba(0, 0, 0, 0.9);
position: absolute;
bottom: 0;
left: 0;
align-items: center;
z-index: 1;
> span {
.size(@w: 18px , @h: 18px);
background-image: url("../../../assets/images/icons/ic-alert-20@3x.png");
background-position: center;
background-size: cover;
margin: 0 12px 0 20px;
}
> h3 {
.size(@w: 100% , @h: 54px);
color: #ffffff;
font-size: 20px;
line-height: 54px;
.elip(@clamp:1);
.marquee {
width: 100%;
transition: opacity 0.5s ease-in-out;
}
}
}
.videoOverlayWithPhoneNumberFull {
bottom: 59px;
left: 141px;
}
.videoOverlayWithPhoneNumber {
display: none;
&.ru {
display: flex;
width: 22%;
height: 12.5%;
bottom: 6.5% !important;
left: 48% !important;
padding: 4px !important;
margin-bottom: 0 !important;
> div:first-child {
font-size: 12px;
line-height: 12px;
}
> div:last-child {
margin-top: 0;
display: flex;
align-items: center;
> img {
width: 14px;
height: 14px;
}
> span {
font-size: 18px;
line-height: 18px;
height: auto;
width: auto;
}
}
}
&.us {
&.vertical {
width: 105px;
height: 66px;
bottom: 225px !important;
left: -96px !important;
}
&.horizontal {
> div:first-child {
font-size: 12px;
line-height: 1;
}
> div:last-child {
display: flex;
align-items: center;
> img {
width: 14px;
height: 14px;
}
> span {
font-size: 16px;
height: auto;
width: auto;
}
}
&.qvc {
display: flex;
width: 18%;
height: 22%;
padding: 1%;
bottom: 4% !important;
left: 4.5% !important;
> div:first-child {
font-size: 48%;
}
> div:last-child {
> img {
width: 12px;
height: 12px;
}
> span {
font-size: 48%;
}
}
}
&.hsn {
display: flex;
width: 18.5%;
height: 22%;
padding: 1%;
bottom: 4% !important;
left: 7% !important;
> div:first-child {
font-size: 48%;
}
> div:last-child {
> img {
width: 12px;
height: 12px;
}
> span {
font-size: 48%;
}
}
}
&.verticalModal {
> div:last-child {
> img {
width: 20px;
height: 20px;
}
> span {
font-size: 20px;
}
}
}
}
}
> img {
width: 102px;
height: 35px;
}
div > img {
width: 34px;
height: 34px;
}
}
.videoOverlayMedia {
bottom: 24% !important;
left: 7% !important;
&.callToOrderHide {
display: none !important;
}
&.qvc {
display: flex;
width: 23%;
height: 15%;
padding: 30px 5px;
> div:first-child {
flex: none;
font-size: 13px !important;
line-height: 13px !important;
padding: 3px 5px;
}
> div:last-child {
display: flex;
flex: none;
align-items: center;
height: 4px;
> img {
flex: none;
width: 13px;
height: 13px;
}
> span {
flex: none;
font-size: 15px;
height: auto;
width: auto;
}
}
}
&.hsn {
display: flex;
width: 23%;
height: 15%;
padding: 30px 5px;
> div:first-child {
flex: none;
font-size: 13px !important;
line-height: 13px !important;
padding: 3px 5px;
}
> div:last-child {
display: flex;
flex: none;
align-items: center;
height: 4px;
> img {
flex: none;
width: 13px;
height: 13px;
}
> span {
flex: none;
font-size: 15px;
height: auto;
width: auto;
}
}
}
}
.loaderWrap {
height: 100%;
> div {
width: 220px;
height: 220px;
position: relative;
overflow: hidden;
left: calc(50% - 110px);
top: calc(50% - 110px);
background-color: transparent;
> div {
> div {
-webkit-animation: mulShdSpinWhite 1.2s infinite ease !important;
animation: mulShdSpinWhite 1.2s infinite ease !important;
}
}
}
}
@-webkit-keyframes mulShdSpinWhite {
0%,
100% {
-webkit-box-shadow: 0em -2.6em 0em 0em #fff,
1.8em -1.8em 0 0em rgba(255, 255, 255, 0.2),
2.5em 0em 0 0em rgba(255, 255, 255, 0.2),
1.75em 1.75em 0 0em rgba(255, 255, 255, 0.2),
0em 2.5em 0 0em rgba(255, 255, 255, 0.2),
-1.8em 1.8em 0 0em rgba(255, 255, 255, 0.2),
-2.6em 0em 0 0em rgba(255, 255, 255, 0.5),
-1.8em -1.8em 0 0em rgba(255, 255, 255, 0.7);
box-shadow: 0em -2.6em 0em 0em #fff,
1.8em -1.8em 0 0em rgba(255, 255, 255, 0.2),
2.5em 0em 0 0em rgba(255, 255, 255, 0.2),
1.75em 1.75em 0 0em rgba(255, 255, 255, 0.2),
0em 2.5em 0 0em rgba(255, 255, 255, 0.2),
-1.8em 1.8em 0 0em rgba(255, 255, 255, 0.2),
-2.6em 0em 0 0em rgba(255, 255, 255, 0.5),
-1.8em -1.8em 0 0em rgba(255, 255, 255, 0.7);
}
12.5% {
-webkit-box-shadow: 0em -2.6em 0em 0em rgba(255, 255, 255, 0.7),
1.8em -1.8em 0 0em #fff, 2.5em 0em 0 0em rgba(255, 255, 255, 0.2),
1.75em 1.75em 0 0em rgba(255, 255, 255, 0.2),
0em 2.5em 0 0em rgba(255, 255, 255, 0.2),
-1.8em 1.8em 0 0em rgba(255, 255, 255, 0.2),
-2.6em 0em 0 0em rgba(255, 255, 255, 0.2),
-1.8em -1.8em 0 0em rgba(255, 255, 255, 0.5);
box-shadow: 0em -2.6em 0em 0em rgba(255, 255, 255, 0.7),
1.8em -1.8em 0 0em #fff, 2.5em 0em 0 0em rgba(255, 255, 255, 0.2),
1.75em 1.75em 0 0em rgba(255, 255, 255, 0.2),
0em 2.5em 0 0em rgba(255, 255, 255, 0.2),
-1.8em 1.8em 0 0em rgba(255, 255, 255, 0.2),
-2.6em 0em 0 0em rgba(255, 255, 255, 0.2),
-1.8em -1.8em 0 0em rgba(255, 255, 255, 0.5);
}
25% {
-webkit-box-shadow: 0em -2.6em 0em 0em rgba(255, 255, 255, 0.5),
1.8em -1.8em 0 0em rgba(255, 255, 255, 0.7), 2.5em 0em 0 0em #fff,
1.75em 1.75em 0 0em rgba(255, 255, 255, 0.2),
0em 2.5em 0 0em rgba(255, 255, 255, 0.2),
-1.8em 1.8em 0 0em rgba(255, 255, 255, 0.2),
-2.6em 0em 0 0em rgba(255, 255, 255, 0.2),
-1.8em -1.8em 0 0em rgba(255, 255, 255, 0.2);
box-shadow: 0em -2.6em 0em 0em rgba(255, 255, 255, 0.5),
1.8em -1.8em 0 0em rgba(255, 255, 255, 0.7), 2.5em 0em 0 0em #fff,
1.75em 1.75em 0 0em rgba(255, 255, 255, 0.2),
0em 2.5em 0 0em rgba(255, 255, 255, 0.2),
-1.8em 1.8em 0 0em rgba(255, 255, 255, 0.2),
-2.6em 0em 0 0em rgba(255, 255, 255, 0.2),
-1.8em -1.8em 0 0em rgba(255, 255, 255, 0.2);
}
37.5% {
-webkit-box-shadow: 0em -2.6em 0em 0em rgba(255, 255, 255, 0.2),
1.8em -1.8em 0 0em rgba(255, 255, 255, 0.5),
2.5em 0em 0 0em rgba(255, 255, 255, 0.7), 1.75em 1.75em 0 0em #fff,
0em 2.5em 0 0em rgba(255, 255, 255, 0.2),
-1.8em 1.8em 0 0em rgba(255, 255, 255, 0.2),
-2.6em 0em 0 0em rgba(255, 255, 255, 0.2),
-1.8em -1.8em 0 0em rgba(255, 255, 255, 0.2);
box-shadow: 0em -2.6em 0em 0em rgba(255, 255, 255, 0.2),
1.8em -1.8em 0 0em rgba(255, 255, 255, 0.5),
2.5em 0em 0 0em rgba(255, 255, 255, 0.7), 1.75em 1.75em 0 0em #fff,
0em 2.5em 0 0em rgba(255, 255, 255, 0.2),
-1.8em 1.8em 0 0em rgba(255, 255, 255, 0.2),
-2.6em 0em 0 0em rgba(255, 255, 255, 0.2),
-1.8em -1.8em 0 0em rgba(255, 255, 255, 0.2);
}
50% {
-webkit-box-shadow: 0em -2.6em 0em 0em rgba(255, 255, 255, 0.2),
1.8em -1.8em 0 0em rgba(255, 255, 255, 0.2),
2.5em 0em 0 0em rgba(255, 255, 255, 0.5),
1.75em 1.75em 0 0em rgba(255, 255, 255, 0.7), 0em 2.5em 0 0em #fff,
-1.8em 1.8em 0 0em rgba(255, 255, 255, 0.2),
-2.6em 0em 0 0em rgba(255, 255, 255, 0.2),
-1.8em -1.8em 0 0em rgba(255, 255, 255, 0.2);
box-shadow: 0em -2.6em 0em 0em rgba(255, 255, 255, 0.2),
1.8em -1.8em 0 0em rgba(255, 255, 255, 0.2),
2.5em 0em 0 0em rgba(255, 255, 255, 0.5),
1.75em 1.75em 0 0em rgba(255, 255, 255, 0.7), 0em 2.5em 0 0em #fff,
-1.8em 1.8em 0 0em rgba(255, 255, 255, 0.2),
-2.6em 0em 0 0em rgba(255, 255, 255, 0.2),
-1.8em -1.8em 0 0em rgba(255, 255, 255, 0.2);
}
62.5% {
-webkit-box-shadow: 0em -2.6em 0em 0em rgba(255, 255, 255, 0.2),
1.8em -1.8em 0 0em rgba(255, 255, 255, 0.2),
2.5em 0em 0 0em rgba(255, 255, 255, 0.2),
1.75em 1.75em 0 0em rgba(255, 255, 255, 0.5),
0em 2.5em 0 0em rgba(255, 255, 255, 0.7), -1.8em 1.8em 0 0em #fff,
-2.6em 0em 0 0em rgba(255, 255, 255, 0.2),
-1.8em -1.8em 0 0em rgba(255, 255, 255, 0.2);
box-shadow: 0em -2.6em 0em 0em rgba(255, 255, 255, 0.2),
1.8em -1.8em 0 0em rgba(255, 255, 255, 0.2),
2.5em 0em 0 0em rgba(255, 255, 255, 0.2),
1.75em 1.75em 0 0em rgba(255, 255, 255, 0.5),
0em 2.5em 0 0em rgba(255, 255, 255, 0.7), -1.8em 1.8em 0 0em #fff,
-2.6em 0em 0 0em rgba(255, 255, 255, 0.2),
-1.8em -1.8em 0 0em rgba(255, 255, 255, 0.2);
}
75% {
-webkit-box-shadow: 0em -2.6em 0em 0em rgba(255, 255, 255, 0.2),
1.8em -1.8em 0 0em rgba(255, 255, 255, 0.2),
2.5em 0em 0 0em rgba(255, 255, 255, 0.2),
1.75em 1.75em 0 0em rgba(255, 255, 255, 0.2),
0em 2.5em 0 0em rgba(255, 255, 255, 0.5),
-1.8em 1.8em 0 0em rgba(255, 255, 255, 0.7), -2.6em 0em 0 0em #fff,
-1.8em -1.8em 0 0em rgba(255, 255, 255, 0.2);
box-shadow: 0em -2.6em 0em 0em rgba(255, 255, 255, 0.2),
1.8em -1.8em 0 0em rgba(255, 255, 255, 0.2),
2.5em 0em 0 0em rgba(255, 255, 255, 0.2),
1.75em 1.75em 0 0em rgba(255, 255, 255, 0.2),
0em 2.5em 0 0em rgba(255, 255, 255, 0.5),
-1.8em 1.8em 0 0em rgba(255, 255, 255, 0.7), -2.6em 0em 0 0em #fff,
-1.8em -1.8em 0 0em rgba(255, 255, 255, 0.2);
}
87.5% {
-webkit-box-shadow: 0em -2.6em 0em 0em rgba(255, 255, 255, 0.2),
1.8em -1.8em 0 0em rgba(255, 255, 255, 0.2),
2.5em 0em 0 0em rgba(255, 255, 255, 0.2),
1.75em 1.75em 0 0em rgba(255, 255, 255, 0.2),
0em 2.5em 0 0em rgba(255, 255, 255, 0.2),
-1.8em 1.8em 0 0em rgba(255, 255, 255, 0.5),
-2.6em 0em 0 0em rgba(255, 255, 255, 0.7), -1.8em -1.8em 0 0em #fff;
box-shadow: 0em -2.6em 0em 0em rgba(255, 255, 255, 0.2),
1.8em -1.8em 0 0em rgba(255, 255, 255, 0.2),
2.5em 0em 0 0em rgba(255, 255, 255, 0.2),
1.75em 1.75em 0 0em rgba(255, 255, 255, 0.2),
0em 2.5em 0 0em rgba(255, 255, 255, 0.2),
-1.8em 1.8em 0 0em rgba(255, 255, 255, 0.5),
-2.6em 0em 0 0em rgba(255, 255, 255, 0.7), -1.8em -1.8em 0 0em #fff;
}
}
@keyframes mulShdSpinWhite {
0%,
100% {
-webkit-box-shadow: 0em -2.6em 0em 0em #fff,
1.8em -1.8em 0 0em rgba(255, 255, 255, 0.2),
2.5em 0em 0 0em rgba(255, 255, 255, 0.2),
1.75em 1.75em 0 0em rgba(255, 255, 255, 0.2),
0em 2.5em 0 0em rgba(255, 255, 255, 0.2),
-1.8em 1.8em 0 0em rgba(255, 255, 255, 0.2),
-2.6em 0em 0 0em rgba(255, 255, 255, 0.5),
-1.8em -1.8em 0 0em rgba(255, 255, 255, 0.7);
box-shadow: 0em -2.6em 0em 0em #fff,
1.8em -1.8em 0 0em rgba(255, 255, 255, 0.2),
2.5em 0em 0 0em rgba(255, 255, 255, 0.2),
1.75em 1.75em 0 0em rgba(255, 255, 255, 0.2),
0em 2.5em 0 0em rgba(255, 255, 255, 0.2),
-1.8em 1.8em 0 0em rgba(255, 255, 255, 0.2),
-2.6em 0em 0 0em rgba(255, 255, 255, 0.5),
-1.8em -1.8em 0 0em rgba(255, 255, 255, 0.7);
}
12.5% {
-webkit-box-shadow: 0em -2.6em 0em 0em rgba(255, 255, 255, 0.7),
1.8em -1.8em 0 0em #fff, 2.5em 0em 0 0em rgba(255, 255, 255, 0.2),
1.75em 1.75em 0 0em rgba(255, 255, 255, 0.2),
0em 2.5em 0 0em rgba(255, 255, 255, 0.2),
-1.8em 1.8em 0 0em rgba(255, 255, 255, 0.2),
-2.6em 0em 0 0em rgba(255, 255, 255, 0.2),
-1.8em -1.8em 0 0em rgba(255, 255, 255, 0.5);
box-shadow: 0em -2.6em 0em 0em rgba(255, 255, 255, 0.7),
1.8em -1.8em 0 0em #fff, 2.5em 0em 0 0em rgba(255, 255, 255, 0.2),
1.75em 1.75em 0 0em rgba(255, 255, 255, 0.2),
0em 2.5em 0 0em rgba(255, 255, 255, 0.2),
-1.8em 1.8em 0 0em rgba(255, 255, 255, 0.2),
-2.6em 0em 0 0em rgba(255, 255, 255, 0.2),
-1.8em -1.8em 0 0em rgba(255, 255, 255, 0.5);
}
25% {
-webkit-box-shadow: 0em -2.6em 0em 0em rgba(255, 255, 255, 0.5),
1.8em -1.8em 0 0em rgba(255, 255, 255, 0.7), 2.5em 0em 0 0em #fff,
1.75em 1.75em 0 0em rgba(255, 255, 255, 0.2),
0em 2.5em 0 0em rgba(255, 255, 255, 0.2),
-1.8em 1.8em 0 0em rgba(255, 255, 255, 0.2),
-2.6em 0em 0 0em rgba(255, 255, 255, 0.2),
-1.8em -1.8em 0 0em rgba(255, 255, 255, 0.2);
box-shadow: 0em -2.6em 0em 0em rgba(255, 255, 255, 0.5),
1.8em -1.8em 0 0em rgba(255, 255, 255, 0.7), 2.5em 0em 0 0em #fff,
1.75em 1.75em 0 0em rgba(255, 255, 255, 0.2),
0em 2.5em 0 0em rgba(255, 255, 255, 0.2),
-1.8em 1.8em 0 0em rgba(255, 255, 255, 0.2),
-2.6em 0em 0 0em rgba(255, 255, 255, 0.2),
-1.8em -1.8em 0 0em rgba(255, 255, 255, 0.2);
}
37.5% {
-webkit-box-shadow: 0em -2.6em 0em 0em rgba(255, 255, 255, 0.2),
1.8em -1.8em 0 0em rgba(255, 255, 255, 0.5),
2.5em 0em 0 0em rgba(255, 255, 255, 0.7), 1.75em 1.75em 0 0em #fff,
0em 2.5em 0 0em rgba(255, 255, 255, 0.2),
-1.8em 1.8em 0 0em rgba(255, 255, 255, 0.2),
-2.6em 0em 0 0em rgba(255, 255, 255, 0.2),
-1.8em -1.8em 0 0em rgba(255, 255, 255, 0.2);
box-shadow: 0em -2.6em 0em 0em rgba(255, 255, 255, 0.2),
1.8em -1.8em 0 0em rgba(255, 255, 255, 0.5),
2.5em 0em 0 0em rgba(255, 255, 255, 0.7), 1.75em 1.75em 0 0em #fff,
0em 2.5em 0 0em rgba(255, 255, 255, 0.2),
-1.8em 1.8em 0 0em rgba(255, 255, 255, 0.2),
-2.6em 0em 0 0em rgba(255, 255, 255, 0.2),
-1.8em -1.8em 0 0em rgba(255, 255, 255, 0.2);
}
50% {
-webkit-box-shadow: 0em -2.6em 0em 0em rgba(255, 255, 255, 0.2),
1.8em -1.8em 0 0em rgba(255, 255, 255, 0.2),
2.5em 0em 0 0em rgba(255, 255, 255, 0.5),
1.75em 1.75em 0 0em rgba(255, 255, 255, 0.7), 0em 2.5em 0 0em #fff,
-1.8em 1.8em 0 0em rgba(255, 255, 255, 0.2),
-2.6em 0em 0 0em rgba(255, 255, 255, 0.2),
-1.8em -1.8em 0 0em rgba(255, 255, 255, 0.2);
box-shadow: 0em -2.6em 0em 0em rgba(255, 255, 255, 0.2),
1.8em -1.8em 0 0em rgba(255, 255, 255, 0.2),
2.5em 0em 0 0em rgba(255, 255, 255, 0.5),
1.75em 1.75em 0 0em rgba(255, 255, 255, 0.7), 0em 2.5em 0 0em #fff,
-1.8em 1.8em 0 0em rgba(255, 255, 255, 0.2),
-2.6em 0em 0 0em rgba(255, 255, 255, 0.2),
-1.8em -1.8em 0 0em rgba(255, 255, 255, 0.2);
}
62.5% {
-webkit-box-shadow: 0em -2.6em 0em 0em rgba(255, 255, 255, 0.2),
1.8em -1.8em 0 0em rgba(255, 255, 255, 0.2),
2.5em 0em 0 0em rgba(255, 255, 255, 0.2),
1.75em 1.75em 0 0em rgba(255, 255, 255, 0.5),
0em 2.5em 0 0em rgba(255, 255, 255, 0.7), -1.8em 1.8em 0 0em #fff,
-2.6em 0em 0 0em rgba(255, 255, 255, 0.2),
-1.8em -1.8em 0 0em rgba(255, 255, 255, 0.2);
box-shadow: 0em -2.6em 0em 0em rgba(255, 255, 255, 0.2),
1.8em -1.8em 0 0em rgba(255, 255, 255, 0.2),
2.5em 0em 0 0em rgba(255, 255, 255, 0.2),
1.75em 1.75em 0 0em rgba(255, 255, 255, 0.5),
0em 2.5em 0 0em rgba(255, 255, 255, 0.7), -1.8em 1.8em 0 0em #fff,
-2.6em 0em 0 0em rgba(255, 255, 255, 0.2),
-1.8em -1.8em 0 0em rgba(255, 255, 255, 0.2);
}
75% {
-webkit-box-shadow: 0em -2.6em 0em 0em rgba(255, 255, 255, 0.2),
1.8em -1.8em 0 0em rgba(255, 255, 255, 0.2),
2.5em 0em 0 0em rgba(255, 255, 255, 0.2),
1.75em 1.75em 0 0em rgba(255, 255, 255, 0.2),
0em 2.5em 0 0em rgba(255, 255, 255, 0.5),
-1.8em 1.8em 0 0em rgba(255, 255, 255, 0.7), -2.6em 0em 0 0em #fff,
-1.8em -1.8em 0 0em rgba(255, 255, 255, 0.2);
box-shadow: 0em -2.6em 0em 0em rgba(255, 255, 255, 0.2),
1.8em -1.8em 0 0em rgba(255, 255, 255, 0.2),
2.5em 0em 0 0em rgba(255, 255, 255, 0.2),
1.75em 1.75em 0 0em rgba(255, 255, 255, 0.2),
0em 2.5em 0 0em rgba(255, 255, 255, 0.5),
-1.8em 1.8em 0 0em rgba(255, 255, 255, 0.7), -2.6em 0em 0 0em #fff,
-1.8em -1.8em 0 0em rgba(255, 255, 255, 0.2);
}
87.5% {
-webkit-box-shadow: 0em -2.6em 0em 0em rgba(255, 255, 255, 0.2),
1.8em -1.8em 0 0em rgba(255, 255, 255, 0.2),
2.5em 0em 0 0em rgba(255, 255, 255, 0.2),
1.75em 1.75em 0 0em rgba(255, 255, 255, 0.2),
0em 2.5em 0 0em rgba(255, 255, 255, 0.2),
-1.8em 1.8em 0 0em rgba(255, 255, 255, 0.5),
-2.6em 0em 0 0em rgba(255, 255, 255, 0.7), -1.8em -1.8em 0 0em #fff;
box-shadow: 0em -2.6em 0em 0em rgba(255, 255, 255, 0.2),
1.8em -1.8em 0 0em rgba(255, 255, 255, 0.2),
2.5em 0em 0 0em rgba(255, 255, 255, 0.2),
1.75em 1.75em 0 0em rgba(255, 255, 255, 0.2),
0em 2.5em 0 0em rgba(255, 255, 255, 0.2),
-1.8em 1.8em 0 0em rgba(255, 255, 255, 0.5),
-2.6em 0em 0 0em rgba(255, 255, 255, 0.7), -1.8em -1.8em 0 0em #fff;
}
}
.overlay {
.position(@position: absolute, @top: 0, @right: 0, @bottom: 0, @left: 0);
}
@keyframes spin {
0% {
transform: rotate(0.25turn);
}
33% {
transform: rotate(0.5turn);
}
80% {
transform: rotate(0.95turn);
}
85% {
transform: rotate(1turn);
}
100% {
transform: rotate(1.25turn);
}
}
.spinner {
background-image: url("../../../assets/images/player/icon_loading.png");
width: 100px;
height: 100px;
position: relative;
background-size: cover;
border-radius: 20.8125rem;
overflow: hidden;
// margin: 490px auto;
left: calc(50% - 50px);
top: calc(50% - 50px);
animation: none 1.25s linear infinite;
animation-name: spin;
// animation-play-state: paused;
}
.controlFeedbackBtnLayer {
position: absolute;
z-index: 50;
top: 0;
left: 0;
padding: 0;
width: 100%;
height: 94px;
display: flex;
justify-content: space-between;
&.lift {
transform: translateY(~"calc(var(--liftDistance) * -1)");
transition: transform 0.3s linear;
}
}
.fullscreen {
.miniFeedback {
position: absolute;
z-index: 50;
top: 506px;
left: 0;
padding: 0;
width: 100%;
height: 94px;
-webkit-margin-end: 0px;
pointer-events: none;
}
&.liveFullScreen {
.bottom {
bottom: 78px;
}
}
.bottom {
position: absolute;
z-index: 3; // Value assigned as part of the VideoPlayer API so layers may be inserted in-between
bottom: 70px;
// bottom: 78px;
// bottom: -18px;
left: 0;
right: 0;
height: 70px;
// left: @sand-video-player-padding-side;
// right: @sand-video-player-padding-side;
&.videoVerticalBottom {
height: 54px;
}
&.lift {
transform: translateY(~"calc(var(--liftDistance) * -1)");
transition: transform 0.3s linear;
}
&.hidden {
pointer-events: none;
.sliderContainer {
position: absolute;
left: 0;
right: 0;
}
}
.infoFrame {
display: flex;
margin-left: 64px;
}
.sliderContainer {
// display: flex;
position: relative;
align-items: center;
margin-left: 60px;
margin-right: 59px;
height: 70px;
bottom: -20px;
> *:first-child {
text-align: right;
}
.enact-locale-rtl({
direction: ltr;
});
&.videoVertical {
margin-left: 680px;
margin-right: 673px;
}
}
}
}
.controlsHandleAbove {
pointer-events: none;
.position(@position: absolute, @top: 0, @right: 0, @bottom: auto, @left: 0);
}
// Skin colors
.applySkins({
.fullscreen {
.bottom {
background-color: @sand-video-player-bottom-bg-color;
.infoFrame {
text-shadow: @sand-video-player-title-text-shadow;
}
}
}
.overlay {
z-index: 2;
&.scrim::before,
&.scrim::after {
width: 1920px;
height: 50%;
position: absolute;
left: 0;
content: "";
z-index: 4;
}
&.scrim::before {
bottom: 0;
background: linear-gradient(0deg, rgba(0, 0, 0, 1) 0%, transparent 50%);
}
&.scrim::after {
top: 0;
background: linear-gradient(180deg, rgba(0, 0, 0, 1) 0%, transparent 50%);
}
// &.scrim::after {
// background: @sand-video-player-scrim-gradient-color
// }
}
});
}
// ========== MediaPlayer.v2 Controls ==========
.controlsContainer {
position: absolute;
bottom: 0;
left: 0;
right: 0;
padding: 20px 40px 30px;
background: linear-gradient(to top, rgba(0, 0, 0, 0.9) 0%, rgba(0, 0, 0, 0.7) 60%, transparent 100%);
z-index: 10;
display: flex;
flex-direction: column;
gap: 16px;
}
.sliderContainer {
display: flex;
align-items: center;
gap: 12px;
width: 100%;
}
.times {
min-width: 80px;
text-align: center;
}
.controlsButtons {
display: flex;
gap: 20px;
justify-content: center;
align-items: center;
}
.playPauseBtn {
width: 60px;
height: 60px;
font-size: 24px;
background: rgba(255, 255, 255, 0.2);
border: 2px solid rgba(255, 255, 255, 0.6);
border-radius: 50%;
color: white;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
&:hover {
background: rgba(255, 255, 255, 0.3);
border-color: white;
}
&:active {
transform: scale(0.95);
}
}
.backBtn {
padding: 12px 24px;
font-size: 18px;
background: rgba(255, 255, 255, 0.2);
border: 2px solid rgba(255, 255, 255, 0.6);
border-radius: 8px;
color: white;
cursor: pointer;
transition: all 0.2s;
&:hover {
background: rgba(255, 255, 255, 0.3);
border-color: white;
}
&:active {
transform: scale(0.98);
}
}

View File

@@ -0,0 +1,15 @@
import React, { createContext, useContext } from 'react';
const MediaPanelControllerContext = createContext(null);
export function MediaPanelControllerProvider({ controller, children }) {
return (
<MediaPanelControllerContext.Provider value={controller}>
{children}
</MediaPanelControllerContext.Provider>
);
}
export function useMediaPanelController() {
return useContext(MediaPanelControllerContext);
}

View File

@@ -1,41 +1,22 @@
// src/views/DetailPanel/DetailPanel.new.jsx
import React, {
useCallback,
useEffect,
useLayoutEffect,
useMemo,
useRef,
useState,
} from 'react';
import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
import {
useDispatch,
useSelector,
} from 'react-redux';
import { useDispatch, useSelector } from 'react-redux';
import Spotlight from '@enact/spotlight';
import { setContainerLastFocusedElement } from '@enact/spotlight/src/container';
import { getDeviceAdditionInfo } from '../../actions/deviceActions';
import { getThemeCurationDetailInfo } from '../../actions/homeActions';
import {
getMainCategoryDetail,
getMainYouMayLike,
} from '../../actions/mainActions';
import { getMainCategoryDetail, getMainYouMayLike } from '../../actions/mainActions';
import { finishModalMediaForce } from '../../actions/mediaActions';
import {
popPanel,
updatePanel,
} from '../../actions/panelActions';
import { popPanel, updatePanel } from '../../actions/panelActions';
import {
finishVideoPreview,
pauseFullscreenVideo,
resumeFullscreenVideo,
} from '../../actions/playActions';
import {
clearProductDetail,
getProductOptionId,
} from '../../actions/productActions';
import { clearProductDetail, getProductOptionId } from '../../actions/productActions';
import { clearAllToasts } from '../../actions/toastActions';
import TBody from '../../components/TBody/TBody';
import TPanel from '../../components/TPanel/TPanel';
@@ -148,7 +129,7 @@ export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) {
};
}, [dispatch]);
const onClick = useCallback(
const onBackClick = useCallback(
(isCancelClick) => (ev) => {
fp.pipe(
() => {
@@ -195,7 +176,7 @@ export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) {
const onBackButtonFocus = useCallback(() => {
dispatch(clearAllToasts());
},[dispatch])
}, [dispatch]);
const handleScrollToSection = useCallback(
(sectionId) => {
@@ -429,13 +410,22 @@ export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) {
dispatch(
updatePanel({
name: panel_names.DETAIL_PANEL,
panelInfo: { shouldReload: false }
panelInfo: { shouldReload: false },
})
);
console.log('[DetailPanel] Reload complete');
}
}, [panelShouldReload, dispatch, panelType, panelPatnrId, panelCurationId, panelBgImgNo, panelPrdtId, panelLiveReqFlag]);
}, [
panelShouldReload,
dispatch,
panelType,
panelPatnrId,
panelCurationId,
panelBgImgNo,
panelPrdtId,
panelLiveReqFlag,
]);
// 최근 본 상품 트리거 예시:
// useEffect(() => {
@@ -730,7 +720,7 @@ export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) {
<TPanel
isTabActivated={false}
className={css.detailPanelWrap}
handleCancel={onClick(true)}
handleCancel={onBackClick(true)}
spotlightId={spotlightId}
>
<THeaderCustom
@@ -738,7 +728,7 @@ export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) {
prdtId={productData?.prdtId}
title={headerTitle}
onBackButton
onClick={onClick(false)}
onClick={onBackClick(false)}
onBackButtonFocus={onBackButtonFocus}
spotlightDisabled={isLoading}
onSpotlightUp={onSpotlightUpTButton}
@@ -747,7 +737,6 @@ export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) {
ariaLabel={ariaLabel}
logoImg={productData?.patncLogoPath}
patnrId={panelPatnrId}
/>
<TBody
className={css.tbody}

View File

@@ -1,40 +1,24 @@
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import classNames from 'classnames';
// import { throttle } from 'lodash';
import { PropTypes } from 'prop-types';
import {
useDispatch,
useSelector,
} from 'react-redux';
import { useDispatch, useSelector } from 'react-redux';
import Spotlight from '@enact/spotlight';
/* eslint-disable react/jsx-no-bind */
// src/views/DetailPanel/ProductAllSection/ProductAllSection.jsx
import SpotlightContainerDecorator
from '@enact/spotlight/SpotlightContainerDecorator';
import SpotlightContainerDecorator from '@enact/spotlight/SpotlightContainerDecorator';
// import Spottable from '@enact/spotlight/Spottable';
//image
import arrowDown
from '../../../../assets/images/icons/ic_arrow_down_3x_new.png';
import indicatorDefaultImage
from '../../../../assets/images/img-thumb-empty-144@3x.png';
import arrowDown from '../../../../assets/images/icons/ic_arrow_down_3x_new.png';
import indicatorDefaultImage from '../../../../assets/images/img-thumb-empty-144@3x.png';
// import { pushPanel } from '../../../actions/panelActions';
import { minimizeModalMedia, restoreModalMedia } from '../../../actions/mediaActions';
import { pauseFullscreenVideo } from '../../../actions/playActions';
import { resetShowAllReviews } from '../../../actions/productActions';
import {
clearAllToasts,
removeToast,
showToast,
} from '../../../actions/toastActions';
import { clearAllToasts, removeToast, showToast } from '../../../actions/toastActions';
// ProductInfoSection imports
import TButton from '../../../components/TButton/TButton';
import useReviews from '../../../hooks/useReviews/useReviews';
@@ -67,16 +51,13 @@ import StarRating from '../components/StarRating';
// ProductContentSection imports
import TScrollerDetail from '../components/TScroller/TScrollerDetail';
import DetailPanelSkeleton from '../DetailPanelSkeleton/DetailPanelSkeleton';
import ProductDescription
from '../ProductContentSection/ProductDescription/ProductDescription';
import ProductDetail
from '../ProductContentSection/ProductDetail/ProductDetail.new';
import ProductDescription from '../ProductContentSection/ProductDescription/ProductDescription';
import ProductDetail from '../ProductContentSection/ProductDetail/ProductDetail.new';
import { ProductVideoV2 } from '../ProductContentSection/ProductVideo/ProductVideo.v2.jsx';
import ProductVideo from '../ProductContentSection/ProductVideo/ProductVideo';
import ProductVideo from '../ProductContentSection/ProductVideo/ProductVideo.v3';
import UserReviews from '../ProductContentSection/UserReviews/UserReviews';
// import ViewAllReviewsButton from '../ProductContentSection/UserReviews/ViewAllReviewsButton';
import YouMayAlsoLike
from '../ProductContentSection/YouMayAlsoLike/YouMayAlsoLike';
import YouMayAlsoLike from '../ProductContentSection/YouMayAlsoLike/YouMayAlsoLike';
import QRCode from '../ProductInfoSection/QRCode/QRCode';
import ProductOverview from '../ProductOverview/ProductOverview';
// CSS imports
@@ -120,10 +101,7 @@ const HorizontalContainer = SpotlightContainerDecorator(
const getProductData = curry((productType, themeProductInfo, productInfo) =>
pipe(
when(
() =>
isVal(productType) &&
productType === 'theme' &&
isVal(themeProductInfo),
() => isVal(productType) && productType === 'theme' && isVal(themeProductInfo),
() => themeProductInfo
),
defaultTo(productInfo),
@@ -165,9 +143,7 @@ export default function ProductAllSection({
const dispatch = useDispatch();
// Redux 상태
const webOSVersion = useSelector(
(state) => state.common.appStatus.webOSVersion
);
const webOSVersion = useSelector((state) => state.common.appStatus.webOSVersion);
const groupInfos = useSelector((state) => state.product.groupInfo);
// YouMayLike 데이터는 API 응답 시간이 걸리므로 직접 구독
@@ -275,9 +251,7 @@ export default function ProductAllSection({
if (webOSVersion < '6.0') {
return (
productData?.pmtSuptYn === 'N' ||
(productData?.pmtSuptYn === 'Y' &&
productData?.grPrdtProcYn === 'N' &&
panelInfo?.prdtId)
(productData?.pmtSuptYn === 'Y' && productData?.grPrdtProcYn === 'N' && panelInfo?.prdtId)
);
}
@@ -298,10 +272,7 @@ export default function ProductAllSection({
// 여행/테마 상품 - DetailPanel.backup.jsx와 동일한 로직
const isTravelProductVisible = useMemo(() => {
return (
panelInfo?.curationId &&
(panelInfo?.type === 'theme' || panelInfo?.type === 'hotel')
);
return panelInfo?.curationId && (panelInfo?.type === 'theme' || panelInfo?.type === 'hotel');
}, [panelInfo]);
// useReviews Hook 사용 - 모든 리뷰 관련 로직을 담당
@@ -374,47 +345,50 @@ export default function ProductAllSection({
// reviewListData가 반복적으로 초기화되어 Chrome에서 진입 불가 발생
// BUY NOW 버튼 클릭 핸들러 - Toast로 BuyOption 표시
const handleBuyNowClick = useCallback((e) => {
// console.log('[BuyNow] Buy Now button clicked');
e.stopPropagation();
const handleBuyNowClick = useCallback(
(e) => {
// console.log('[BuyNow] Buy Now button clicked');
e.stopPropagation();
console.log('[ProductAllSection] 🛒 BUY NOW clicked - productData:', {
prdtId: productData?.prdtId,
patnrId: productData?.patnrId,
prdtNm: productData?.prdtNm,
hasProductData: !!productData,
});
if(openToast === false){
dispatch(
showToast({
id: productData.prdtId,
message: '',
type: 'buyOption',
duration: 0,
position: 'bottom-center',
// 🚀 BuyOption에 전달할 props 데이터
productInfo: productData,
selectedPatnrId: productData?.patnrId,
selectedPrdtId: productData?.prdtId,
// BuyOption Toast가 닫힐 때 BUY NOW 버튼으로 포커스 복구
onToastClose: () => {
setTimeout(() => {
setOpenToast(false);
Spotlight.focus('detail-buy-now-button');
}, 100);
},
})
);
console.log('[ProductAllSection] 🛒 BUY NOW clicked - productData:', {
prdtId: productData?.prdtId,
patnrId: productData?.patnrId,
prdtNm: productData?.prdtNm,
hasProductData: !!productData,
});
if (openToast === false) {
dispatch(
showToast({
id: productData.prdtId,
message: '',
type: 'buyOption',
duration: 0,
position: 'bottom-center',
// 🚀 BuyOption에 전달할 props 데이터
productInfo: productData,
selectedPatnrId: productData?.patnrId,
selectedPrdtId: productData?.prdtId,
// BuyOption Toast가 닫힐 때 BUY NOW 버튼으로 포커스 복구
onToastClose: () => {
setTimeout(() => {
setOpenToast(false);
Spotlight.focus('detail-buy-now-button');
}, 100);
},
})
);
setOpenToast(true);
}
}, [dispatch, productData, openToast]);
setOpenToast(true);
}
},
[dispatch, productData, openToast]
);
//닫히도록
const handleCloseToast = useCallback(() => {
dispatch(clearAllToasts());
setOpenToast(false);
},[dispatch])
}, [dispatch]);
// 스크롤 컨테이너의 클릭 이벤트 추적용 로깅
const handleScrollContainerClick = useCallback((e) => {
@@ -425,11 +399,14 @@ export default function ProductAllSection({
bubbles: e.bubbles,
defaultPrevented: e.defaultPrevented,
timestamp: new Date().getTime(),
eventPath: e.composedPath?.().slice(0, 5).map(el => ({
tag: el.tagName,
className: el.className,
id: el.id
}))
eventPath: e
.composedPath?.()
.slice(0, 5)
.map((el) => ({
tag: el.tagName,
className: el.className,
id: el.id,
})),
});
}, []);
@@ -441,11 +418,14 @@ export default function ProductAllSection({
currentTarget: e.currentTarget?.className,
bubbles: e.bubbles,
defaultPrevented: e.defaultPrevented,
eventPath: e.composedPath?.().slice(0, 8).map(el => ({
tag: el.tagName,
className: el.className,
id: el.id
}))
eventPath: e
.composedPath?.()
.slice(0, 8)
.map((el) => ({
tag: el.tagName,
className: el.className,
id: el.id,
})),
});
}, []);
@@ -455,10 +435,7 @@ export default function ProductAllSection({
// TODO: 장바구니 추가 로직 구현
}, []);
const { revwGrd, orderPhnNo } = useMemo(
() => extractProductMeta(productInfo),
[productInfo]
);
const { revwGrd, orderPhnNo } = useMemo(() => extractProductMeta(productInfo), [productInfo]);
const [favoriteOverride, setFavoriteOverride] = useState(null);
const favoriteFlag = useMemo(
@@ -467,8 +444,7 @@ export default function ProductAllSection({
);
const [mobileSendPopupOpen, setMobileSendPopupOpen] = useState(false);
const [isShowUserReviewsFocused, setIsShowUserReviewsFocused] =
useState(false);
const [isShowUserReviewsFocused, setIsShowUserReviewsFocused] = useState(false);
const reviewTotalCount = stats.totalReviews;
@@ -491,12 +467,9 @@ export default function ProductAllSection({
);
// User Reviews 스크롤 핸들러 추가
const handleUserReviewsClick = useCallback(
() => {
scrollToSection('scroll-marker-user-reviews');
},
[scrollToSection]
);
const handleUserReviewsClick = useCallback(() => {
scrollToSection('scroll-marker-user-reviews');
}, [scrollToSection]);
// ProductVideo V1 전용 - MediaPanel minimize 포함
const handleScrollToImagesV1 = useCallback(() => {
@@ -571,11 +544,7 @@ export default function ProductAllSection({
}
// 이미지들 추가
if (
productData &&
productData.imgUrls600 &&
productData.imgUrls600.length > 0
) {
if (productData && productData.imgUrls600 && productData.imgUrls600.length > 0) {
productData.imgUrls600.forEach((image, imgIndex) => {
items.push({
type: 'image',
@@ -595,9 +564,7 @@ export default function ProductAllSection({
// renderItems에 Video가 존재하는지 확인하는 boolean 상태
const hasVideo = useMemo(() => {
return (
renderItems &&
renderItems.length > 0 &&
renderItems.some((item) => item.type === 'video')
renderItems && renderItems.length > 0 && renderItems.some((item) => item.type === 'video')
);
}, [renderItems]);
@@ -640,10 +607,7 @@ export default function ProductAllSection({
pipe(
() => setOpenThemeItemOverlay(true),
tap(() => {
const timerId = setTimeout(
() => Spotlight.focus('theme-close-button'),
0
);
const timerId = setTimeout(() => Spotlight.focus('theme-close-button'), 0);
timersRef.current.push(timerId);
})
),
@@ -661,6 +625,9 @@ export default function ProductAllSection({
);
const scrollPositionRef = useRef(0);
const prevScrollPositionRef = useRef(0); // 이전 스크롤 위치 추적
const prevScrollTopRef = useRef(0); // HomePanel 스타일 스크롤 위치 추적
const scrollExpandTimerRef = useRef(null); // 스크롤 확장 타이머
const mediaMinimizedRef = useRef(false);
const handleArrowClickAlternative = useCallback(() => {
const currentHeight = scrollPositionRef.current;
@@ -672,8 +639,7 @@ export default function ProductAllSection({
});
// documentHeight를 활용하여 반복 계산 제거
const totalHeight =
documentHeight + (youMayAlsoLikelRef.current?.scrollHeight || 0);
const totalHeight = documentHeight + (youMayAlsoLikelRef.current?.scrollHeight || 0);
const isAtBottom = scrollPositionRef.current + 1100 >= totalHeight;
if (isAtBottom) {
@@ -681,20 +647,71 @@ export default function ProductAllSection({
}
}, [documentHeight, scrollTop]);
// const handleScroll = useCallback(
// (e) => {
// scrollPositionRef.current = e.scrollTop;
// if (documentHeight) {
// const isAtBottom =
// scrollPositionRef.current + 944 >=
// documentHeight + (youMayAlsoLikelRef.current?.scrollHeight || 0);
// if (isAtBottom !== isBottom) {
// setIsBottom(isAtBottom);
// }
// }
// },
// [documentHeight, isBottom]
// );
const handleScroll = useCallback(
(e) => {
const currentScrollTop = e.scrollTop;
const prevScrollTop = prevScrollTopRef.current;
scrollPositionRef.current = currentScrollTop;
// 기존 bottom 체크 로직 유지
if (documentHeight) {
const isAtBottom =
scrollPositionRef.current + 944 >=
documentHeight + (youMayAlsoLikelRef.current?.scrollHeight || 0);
if (isAtBottom !== isBottom) {
setIsBottom(isAtBottom);
}
}
// 🔽 ProductVideo (v3.jsx)에만 HomePanel 스타일 즉각 스크롤 로직 적용
// ProductVideo.v3.jsx는 ProductVideo로 import되어 productVideoVersion === 1일 때 사용됨
if (productVideoVersion === 1) {
const isScrollingDown = currentScrollTop > prevScrollTop;
prevScrollTopRef.current = currentScrollTop;
// 아래로 스크롤: 즉시 1px 축소 (HomePanel과 동일)
if (isScrollingDown && currentScrollTop > 0) {
console.log('🚀 [ProductVideo.v3] onScroll Down - immediate minimize');
dispatch(minimizeModalMedia());
setShouldMinimizeMedia(true);
// 기존 타이머 취소
if (scrollExpandTimerRef.current) {
clearTimeout(scrollExpandTimerRef.current);
scrollExpandTimerRef.current = null;
}
}
// 위로 스크롤 (최상단 아님): 1초 후 복구
else if (!isScrollingDown && currentScrollTop > 1) {
console.log('🚀 [ProductVideo.v3] onScroll Up - expand after 1s');
if (scrollExpandTimerRef.current) {
clearTimeout(scrollExpandTimerRef.current);
}
scrollExpandTimerRef.current = setTimeout(() => {
dispatch(restoreModalMedia());
setShouldMinimizeMedia(false);
scrollExpandTimerRef.current = null;
}, 1000);
}
// 최상단 도달: 즉시 복구 (HomePanel과 동일)
else if (currentScrollTop <= 1) {
console.log('🚀 [ProductVideo.v3] onScroll AtTop - immediate expand');
dispatch(restoreModalMedia());
setShouldMinimizeMedia(false);
if (scrollExpandTimerRef.current) {
clearTimeout(scrollExpandTimerRef.current);
scrollExpandTimerRef.current = null;
}
}
}
// v2: onScrollStop에서 처리 (기존 로직 유지)
},
[documentHeight, isBottom, productVideoVersion, dispatch]
);
// 스크롤 멈추었을 때만 호출 (성능 최적화)
const handleScrollStop = useCallback(
@@ -712,13 +729,29 @@ export default function ProductAllSection({
}
console.log('📍 [ProductAllSection] 스크롤 멈춤 - 위치:', currentScrollTop);
const shouldMinimize = currentScrollTop > 0;
console.log('📍 [ProductAllSection] setShouldMinimizeMedia 호출:', shouldMinimize);
setShouldMinimizeMedia(shouldMinimize); // state 업데이트만
setShouldMinimizeMedia((prev) => {
if (prev === shouldMinimize) return prev;
console.log('📍 [ProductAllSection] setShouldMinimizeMedia 호출:', shouldMinimize);
return shouldMinimize;
});
},
[documentHeight, isBottom]
);
useEffect(() => {
if (shouldMinimizeMedia && !mediaMinimizedRef.current) {
console.log('📍 [ProductAllSection] MediaPanel 최소화 (effect)');
dispatch(minimizeModalMedia());
mediaMinimizedRef.current = true;
} else if (!shouldMinimizeMedia && mediaMinimizedRef.current) {
console.log('📍 [ProductAllSection] MediaPanel 복원 (effect)');
dispatch(restoreModalMedia());
mediaMinimizedRef.current = false;
}
}, [shouldMinimizeMedia, dispatch]);
const handleButtonFocus = useCallback((buttonType) => {
if (activeButton !== buttonType) {
setActiveButton(buttonType);
@@ -829,6 +862,12 @@ export default function ProductAllSection({
scrollToImagesTimeoutRef.current = null;
}
// 🔽 스크롤 확장 타이머 cleanup 추가
if (scrollExpandTimerRef.current) {
clearTimeout(scrollExpandTimerRef.current);
scrollExpandTimerRef.current = null;
}
console.log('[ProductAllSection] cleanup 완료 on unmount');
};
}, []);
@@ -875,11 +914,7 @@ export default function ProductAllSection({
>
<div className={css.qrWrapper}>
{isShowQRCode ? (
<QRCode
productInfo={productData}
productType={productType}
kind={'detail'}
/>
<QRCode productInfo={productData} productType={productType} kind={'detail'} />
) : (
<div className={css.qrRollingWrap}>
<div className={css.innerText}>
@@ -929,9 +964,7 @@ export default function ProductAllSection({
onClick={handleShopByMobileOpen}
onSpotlightUp={handleSpotlightUpToBackButton}
>
<div className={css.shopByMobileText}>
{$L('SHOP BY MOBILE')}
</div>
<div className={css.shopByMobileText}>{$L('SHOP BY MOBILE')}</div>
</TButton>
{panelInfo && (
<div className={css.favoriteBtnWrapper}>
@@ -950,9 +983,7 @@ export default function ProductAllSection({
<div className={css.callToOrderSection}>
{orderPhnNo && (
<>
<div className={css.callToOrderText}>
{$L('Call to Order')}
</div>
<div className={css.callToOrderText}>{$L('Call to Order')}</div>
<div className={css.phoneSection}>
<div className={css.phoneIconContainer}>
<div className={css.phoneIcon} />
@@ -979,7 +1010,7 @@ export default function ProductAllSection({
</TButton>
{isReviewDataComplete && (
<>
{/*
{/*
{console.log('[ProductAllSection_useReviewList] 🎯 버튼 렌더링:', {
hasReviews,
reviewTotalCount,
@@ -1020,17 +1051,15 @@ export default function ProductAllSection({
})()} */}
</Container>
{panelInfo &&
panelInfo.type === 'theme' &&
!openThemeItemOverlay && (
<TButton
className={css.themeButton}
onClick={handleThemeItemButtonClick}
spotlightId="theme-open-button"
>
{$L('THEME ITEM')}
</TButton>
)}
{panelInfo && panelInfo.type === 'theme' && !openThemeItemOverlay && (
<TButton
className={css.themeButton}
onClick={handleThemeItemButtonClick}
spotlightId="theme-open-button"
>
{$L('THEME ITEM')}
</TButton>
)}
<DetailMobileSendPopUp
ismobileSendPopupOpen={mobileSendPopupOpen}
@@ -1060,7 +1089,7 @@ export default function ProductAllSection({
spotlightId="main-content-scroller"
spotlightDisabled={false}
spotlightRestrict="none"
// onScroll={handleScroll}
onScroll={handleScroll}
onScrollStop={handleScrollStop}
onClick={handleScrollContainerClick}
>
@@ -1073,36 +1102,32 @@ export default function ProductAllSection({
onBlur={handleButtonBlur}
>
{/* 비디오가 있으면 먼저 렌더링 (productVideoVersion이 3이 아닐 때만) */}
{hasVideo &&
renderItems[0].type === 'video' &&
productVideoVersion !== 3 && (
<>
{productVideoVersion === 1 ? (
<ProductVideo
key="product-video-0"
productInfo={productData}
videoUrl={renderItems[0].url}
thumbnailUrl={renderItems[0].thumbnail}
autoPlay={true}
continuousPlay={true}
onScrollToImages={handleScrollToImagesV1}
/>
) : (
<ProductVideoV2
key="product-video-v2-0"
productInfo={productData}
videoUrl={renderItems[0].url}
thumbnailUrl={renderItems[0].thumbnail}
autoPlay={true}
onScrollToImages={handleScrollToImagesV2}
/>
)}
<div
id="scroll-marker-after-video"
className={css.scrollMarker}
></div>
</>
)}
{hasVideo && renderItems[0].type === 'video' && productVideoVersion !== 3 && (
<>
{productVideoVersion === 1 ? (
<ProductVideo
key="product-video-0"
productInfo={productData}
videoUrl={renderItems[0].url}
thumbnailUrl={renderItems[0].thumbnail}
autoPlay={true}
continuousPlay={true}
onScrollToImages={handleScrollToImagesV1}
onFocus={() => console.log('[ProductVideo V1] Focused')}
/>
) : (
<ProductVideoV2
key="product-video-v2-0"
productInfo={productData}
videoUrl={renderItems[0].url}
thumbnailUrl={renderItems[0].thumbnail}
autoPlay={true}
onScrollToImages={handleScrollToImagesV2}
/>
)}
<div id="scroll-marker-after-video" className={css.scrollMarker}></div>
</>
)}
{/* 이미지들만 렌더링 (비디오 제외) */}
{renderItems.length > 0
@@ -1121,10 +1146,7 @@ export default function ProductAllSection({
))
: !hasVideo && <ProductDetail productInfo={productData} />}
</div>
<div
id="scroll-marker-product-details"
className={css.scrollMarker}
></div>
<div id="scroll-marker-product-details" className={css.scrollMarker}></div>
<div
id="product-description-section"
ref={descriptionRef}
@@ -1134,10 +1156,7 @@ export default function ProductAllSection({
<ProductDescription productInfo={productData} />
</div>
{/* 리뷰 데이터가 완전할 때만 UserReviews 섹션 표시 */}
<div
id="scroll-marker-user-reviews"
className={css.scrollMarker}
></div>
<div id="scroll-marker-user-reviews" className={css.scrollMarker}></div>
{isReviewDataComplete && (
<div
id="user-reviews-section"
@@ -1165,10 +1184,7 @@ export default function ProductAllSection({
)}
</div>
<div ref={youMayAlsoLikelRef}>
<div
id="scroll-marker-you-may-also-like"
className={css.scrollMarker}
></div>
<div id="scroll-marker-you-may-also-like" className={css.scrollMarker}></div>
{hasYouMayAlsoLike && (
<div id="you-may-also-like-section">
{/* {(() => {

View File

@@ -120,8 +120,6 @@
padding: 0 15px 0 30px;
box-sizing: border-box;
margin: 0;
overflow-y: auto;
overflow-x: hidden;
display: flex;
flex-direction: column;

View File

@@ -22,6 +22,7 @@ export default function ProductVideo({
onScrollToImages,
autoPlay = false, // 자동 재생 여부
continuousPlay = false, // 반복 재생 여부
onFocus = null, // 외부에서 전달된 포커스 핸들러
}) {
const dispatch = useDispatch();
@@ -115,8 +116,13 @@ export default function ProductVideo({
console.log('[ProductVideo] Calling restoreModalMedia');
// ProductVideo에 포커스가 돌아오면 비디오 복원
// dispatch(restoreModalMedia());
// 외부에서 전달된 onFocus 핸들러도 호출
if (onFocus) {
onFocus();
}
}
}, [canPlayVideo, dispatch]);
}, [canPlayVideo, dispatch, onFocus]);
const videoContainerOnBlur = useCallback(() => {
console.log('[ProductVideo] onBlur called - canPlayVideo:', canPlayVideo);

View File

@@ -614,12 +614,7 @@ export function ProductVideoV2({
debugLog('[Fullscreen Container] Enter key - overlay visible, allow default behavior');
}
},
[
isPlaying,
isFullscreen,
toggleOverlayVisibility,
mediaOverlayState.controls?.visible,
]
[isPlaying, isFullscreen, toggleOverlayVisibility, mediaOverlayState.controls?.visible]
);
// 마우스 다운 (클릭) 이벤트 - capture phase에서 처리
@@ -1178,6 +1173,3 @@ export function ProductVideoV2({
</>
);
}

View File

@@ -0,0 +1,246 @@
import React, { useCallback, useMemo, useState, useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import Spottable from '@enact/spotlight/Spottable';
import {
startMediaPlayer,
finishMediaPreview,
switchMediaToFullscreen,
minimizeModalMedia,
restoreModalMedia,
} from '../../../../actions/mediaActions';
import CustomImage from '../../../../components/CustomImage/CustomImage';
import { panel_names } from '../../../../utils/Config';
import playImg from '../../../../../assets/images/btn/btn-play-thumb-nor.png';
import css from './ProductVideo.module.less';
const SpottableComponent = Spottable('div');
export default function ProductVideo({
productInfo,
videoUrl,
thumbnailUrl,
onScrollToImages,
autoPlay = false, // 자동 재생 여부
continuousPlay = false, // 반복 재생 여부
onFocus = null, // 외부에서 전달된 포커스 핸들러
}) {
const dispatch = useDispatch();
// MediaPanel 상태 체크를 위한 selectors 추가
const panels = useSelector((state) => state.panels.panels);
const [isLaunchedFromPlayer, setIsLaunchedFromPlayer] = useState(false);
const [focused, setFocused] = useState(false);
const [modalState, setModalState] = useState(true); // 모달 상태 관리 추가
const [hasAutoPlayed, setHasAutoPlayed] = useState(false); // 자동 재생 완료 여부
const topPanel = panels[panels.length - 1];
// MediaPanel 상태 체크 로직 + 모달 상태 복원
useEffect(() => {
if (
topPanel &&
topPanel.name === panel_names.MEDIA_PANEL &&
topPanel.panelInfo.modal === false
) {
return; // MediaPanel이 전체화면 모드일 때는 처리하지 않음
}
// MediaPanel이 닫혔을 때 modalState를 true로 복원
if (!topPanel || topPanel.name !== panel_names.MEDIA_PANEL) {
setModalState(true);
}
}, [topPanel]);
// autoPlay 기능: 컴포넌트 마운트 시 자동으로 비디오 재생
useEffect(() => {
if (autoPlay && canPlayVideo && !hasAutoPlayed && productInfo) {
console.log('[ProductVideo] Auto-playing video');
setHasAutoPlayed(true);
// 짧은 딜레이 후 재생 시작 (컴포넌트 마운트 완료 후)
setTimeout(() => {
dispatch(
startMediaPlayer({
qrCurrentItem: productInfo,
showUrl: productInfo?.prdtMediaUrl,
showNm: productInfo?.prdtNm,
patnrNm: productInfo?.patncNm,
patncLogoPath: productInfo?.patncLogoPath,
orderPhnNo: productInfo?.orderPhnNo,
disclaimer: productInfo?.disclaimer,
subtitle: productInfo?.prdtMediaSubtitlUrl,
lgCatCd: productInfo?.catCd,
patnrId: productInfo?.patnrId,
lgCatNm: productInfo?.catNm,
prdtId: productInfo?.prdtId,
patncNm: productInfo?.patncNm,
prdtNm: productInfo?.prdtNm,
thumbnailUrl: productInfo?.thumbnailUrl960,
shptmBanrTpNm: 'MEDIA',
modal: true,
modalContainerId: 'product-video-player',
modalClassName: modalClassNameChange(),
spotlightDisable: true,
continuousPlay, // 반복 재생 옵션 전달
})
);
}, 100);
}
}, [
autoPlay,
canPlayVideo,
hasAutoPlayed,
productInfo,
dispatch,
modalClassNameChange,
continuousPlay,
]);
// 비디오 재생 가능 여부 체크
const canPlayVideo = useMemo(() => {
return Boolean(productInfo?.prdtMediaUrl);
}, [productInfo]);
// 모달 CSS 클래스 변경 로직
const modalClassNameChange = useCallback(() => {
if (focused) {
return css.videoModal;
}
return '';
}, [focused]);
// 포커스 이벤트 핸들러
const videoContainerOnFocus = useCallback(() => {
if (canPlayVideo) {
setFocused(true);
console.log('[ProductVideo] Calling restoreModalMedia');
// ProductVideo에 포커스가 돌아오면 비디오 복원
// dispatch(restoreModalMedia());
// 외부에서 전달된 onFocus 핸들러도 호출
if (onFocus) {
onFocus();
}
}
}, [canPlayVideo, dispatch, onFocus]);
const videoContainerOnBlur = useCallback(() => {
console.log('[ProductVideo] onBlur called - canPlayVideo:', canPlayVideo);
// if (canPlayVideo) {
// setFocused(false);
// // 포커스를 잃으면 모달 MediaPanel을 최소화하여 1px 상태로 유지
// dispatch(minimizeModalMedia());
// }
}, [canPlayVideo, dispatch]);
// Spotlight Down 키 핸들러 - 비디오 다음 이미지로 스크롤
const handleSpotlightDown = useCallback(
(e) => {
if (canPlayVideo && onScrollToImages) {
e.preventDefault();
e.stopPropagation();
dispatch(minimizeModalMedia());
onScrollToImages();
return true; // 이벤트 처리 완료
}
return false; // Spotlight가 기본 동작 수행
},
[canPlayVideo, onScrollToImages, dispatch]
);
// MediaPanel 비디오 클릭 핸들러 + 모달 토글 기능
const handleVideoClick = useCallback(() => {
console.log('[ProductVideo] ========== handleVideoClick 호출 ==========');
console.log('[ProductVideo] canPlayVideo:', canPlayVideo);
console.log('[ProductVideo] panels.length:', panels.length);
console.log('[ProductVideo] All panels:', JSON.stringify(panels, null, 2));
if (canPlayVideo) {
const currentTopPanel = panels[panels.length - 1];
// 현재 MediaPanel이 modal=true로 재생 중인지 확인
const isCurrentlyPlayingModal =
currentTopPanel &&
currentTopPanel.name === panel_names.MEDIA_PANEL &&
currentTopPanel.panelInfo.modal === true;
console.log('[ProductVideo] currentTopPanel:', JSON.stringify(currentTopPanel, null, 2));
console.log('[ProductVideo] isCurrentlyPlayingModal:', isCurrentlyPlayingModal);
// modal로 재생 중이면 전체화면으로 전환
if (isCurrentlyPlayingModal) {
console.log(
'[ProductVideo] *** Switching to fullscreen mode via switchMediaToFullscreen ***'
);
dispatch(switchMediaToFullscreen());
setModalState(false);
} else {
console.log('[ProductVideo] *** Starting modal MediaPanel ***');
console.log('[ProductVideo] productInfo:', JSON.stringify(productInfo, null, 2));
// 처음 재생 시작 - modal=true로 시작
dispatch(
startMediaPlayer({
qrCurrentItem: productInfo,
showUrl: productInfo?.prdtMediaUrl,
showNm: productInfo?.prdtNm,
patnrNm: productInfo?.patncNm,
patncLogoPath: productInfo?.patncLogoPath,
orderPhnNo: productInfo?.orderPhnNo,
disclaimer: productInfo?.disclaimer,
subtitle: productInfo?.prdtMediaSubtitlUrl,
lgCatCd: productInfo?.catCd,
patnrId: productInfo?.patnrId,
lgCatNm: productInfo?.catNm,
prdtId: productInfo?.prdtId,
patncNm: productInfo?.patncNm,
prdtNm: productInfo?.prdtNm,
thumbnailUrl: productInfo?.thumbnailUrl960,
shptmBanrTpNm: 'MEDIA',
modal: true,
modalContainerId: 'product-video-player',
modalClassName: modalClassNameChange(),
spotlightDisable: true,
continuousPlay, // 반복 재생 옵션 전달
})
);
}
}
if (isLaunchedFromPlayer) {
setIsLaunchedFromPlayer(false);
}
}, [
dispatch,
productInfo,
canPlayVideo,
isLaunchedFromPlayer,
modalClassNameChange,
panels,
modalState,
]);
if (!canPlayVideo) return null;
return (
<SpottableComponent
className={css.videoContainer}
onClick={handleVideoClick}
onFocus={videoContainerOnFocus}
onBlur={videoContainerOnBlur}
onSpotlightDown={handleSpotlightDown}
spotlightId="product-video-player"
aria-label={`${productInfo?.prdtNm} 동영상 재생`}
>
<div className={css.videoThumbnailWrapper}>
<CustomImage
src={thumbnailUrl}
alt={`${productInfo?.prdtNm} 동영상 썸네일`}
className={css.videoThumbnail}
/>
<div className={css.playButtonOverlay}>
<img src={playImg} alt="재생" />
</div>
</div>
</SpottableComponent>
);
}

View File

@@ -65,7 +65,7 @@ import IntroPanel from '../IntroPanel/IntroPanel.new';
import JustForYouPanel from '../JustForYouPanel/JustForYouPanel';
import JustForYouTestPanel from '../JustForYouTestPanel/JustForYouTestPanel';
import LoadingPanel from '../LoadingPanel/LoadingPanel';
import MediaPanel from '../MediaPanel/MediaPanel';
import MediaPanel from '../MediaPanel/MediaPanel.v3';
import MyPagePanel from '../MyPagePanel/MyPagePanel';
import OnSalePanel from '../OnSalePanel/OnSalePanel';
import PlayerPanel from '../PlayerPanel/PlayerPanel';
@@ -208,14 +208,19 @@ export default function MainView({ className, initService }) {
console.log(`[MainView] 🔍 Top panel name: ${topPanel?.name}`);
console.log(`[MainView] 🔍 isStandalonePanel check:`, isStandalonePanel(topPanel?.name));
console.log(`[MainView] 🔍 STANDALONE_PANELS:`, STANDALONE_PANELS);
console.log(`[MainView] 🔍 All panels:`, panels.map(p => ({ name: p.name, hasModal: !!p.panelInfo?.modal })));
console.log(
`[MainView] 🔍 All panels:`,
panels.map((p) => ({ name: p.name, hasModal: !!p.panelInfo?.modal }))
);
}
if (isStandalonePanel(topPanel?.name)) {
if (DEBUG_MODE) {
console.log(`[MainView] ✅ Standalone panel detected: ${topPanel?.name} - rendering independently`);
console.log(
`[MainView] ✅ Standalone panel detected: ${topPanel?.name} - rendering independently`
);
}
renderingPanels = [topPanel]; // 단독 패널만 단독으로 렌더링
renderingPanels = [topPanel]; // 단독 패널만 단독으로 렌더링
}
// 기존 3-layer 구조 체크: PlayerPanel + DetailPanel + MediaPanel(modal)
else {
@@ -716,7 +721,12 @@ export default function MainView({ className, initService }) {
}, [webOSVersion]);
const handleErrorPopupClose = useCallback(() => {
if (DEBUG_MODE) {
console.log('handleErrorPopupClose 호출됨! activePopup:', activePopup, 'popupData:', popupData);
console.log(
'handleErrorPopupClose 호출됨! activePopup:',
activePopup,
'popupData:',
popupData
);
}
if (popupData?.shouldPopPanel) {
dispatch(popPanel());

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,142 @@
@import "../../style/CommonStyle.module.less";
@import "../../style/utils.module.less";
@videoBackgroundColor: black;
.videoContainer {
position: absolute;
background-color: @videoBackgroundColor;
z-index: 21;
outline: none;
&:focus-visible,
&:focus {
outline: none;
}
.videoFrame {
padding: 2px;
box-sizing: border-box;
}
.videoFrame:focus-within {
outline: 2px solid #c70850;
border-radius: 6px;
}
.videoFrame video,
.videoFrame iframe {
width: 100%;
height: 100%;
}
.playButton {
.size(@w: 60px, @h: 60px);
background-image: url("../../../assets/images/btn/btn-video-play-nor@3x.png");
background-size: cover;
&:focus {
.size(@w: 60px, @h: 60px);
border-radius: 50%;
background-color: #c70850;
opacity: 0.5;
}
}
.pauseButton {
.size(@w: 60px, @h: 60px);
background-image: url("../../../assets/images/btn/btn-voc-pause-nor@3x.png");
background-size: cover;
&:focus {
.size(@w: 60px, @h: 60px);
border-radius: 50%;
background-color: #c70850;
opacity: 0.5;
}
}
.toOpenBtn {
.size(@w: 147px, @h: 243px);
min-width: 60px !important;
margin: 192px 0 136px 512px;
text-align: center;
background-image: url(../../../assets/images/btn/btn-toopen-foc.svg);
}
.videoReduce {
.size(@w:78px, @h:78px);
background: url("../../../assets/images/btn/btn-video-min-nor@3x.png")
no-repeat center center/60px 60px;
position: absolute;
right: 60px;
bottom: 150px;
z-index: 3;
&:focus {
.size(@w:78px, @h:78px);
background-color: rgba(199, 8, 80, 0.5);
border-radius: 50%;
}
}
&.shrinkTo1px {
/* 모달 비디오를 1px로 축소 */
width: 1px;
height: 1px;
left: -1px;
top: -1px;
pointer-events: none;
z-index: 1;
background-color: @videoBackgroundColor;
overflow: visible;
.tabContainer,
.arrow,
.toOpenBtn {
display: none;
}
}
&.modal,
&.modal-minimized,
&.background {
/* 실제 css */
width: 1px;
height: 1px;
left: -1px;
top: -1px;
/* //실제 css */
/* 테스트 용도 */
// width: 1920px;
// height: 1080px;
// left: 0;
// top: 0;
/* //테스트 용도 */
pointer-events: none;
z-index: 1;
background-color: @videoBackgroundColor;
overflow: visible;
.tabContainer,
.arrow,
.toOpenBtn {
display: none;
}
}
&.background-visible {
width: 1920px;
height: 1080px;
left: -1px;
top: -1px;
pointer-events: none;
z-index: 1;
background-color: @videoBackgroundColor;
overflow: visible;
.tabContainer,
.arrow,
.toOpenBtn {
display: none;
}
}
&.hideSubtitle {
video::cue {
visibility: hidden;
}
}
}