[테마 아이템]

- 상품 상세에서 노출되는 테마 아이템 노출에 관련하여 처리.
 - 에너지 라벨 노출및 테마 아이템 상품 이미지 변경.
This commit is contained in:
junghoon86.park
2025-11-24 12:39:26 +09:00
parent b95628de24
commit d196b8b49e
3 changed files with 349 additions and 57 deletions

View File

@@ -1,21 +1,32 @@
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
import React, {
useCallback,
useEffect,
useMemo,
useRef,
} from 'react';
import { useDispatch } from 'react-redux';
import Spotlight from '@enact/spotlight';
import SpotlightContainerDecorator from '@enact/spotlight/SpotlightContainerDecorator';
import SpotlightContainerDecorator
from '@enact/spotlight/SpotlightContainerDecorator';
import arrowDownIcon from '../../../../assets/images/icons/ic-arrow-down.svg';
import themeImage1 from '../../../../assets/images/theme/image-1.png';
import themeImage2 from '../../../../assets/images/theme/image-2.png';
import themeImage3 from '../../../../assets/images/theme/image-3.png';
import { sendLogTotalRecommend } from '../../../actions/logActions';
import { updatePanel } from '../../../actions/panelActions';
import TButton from '../../../components/TButton/TButton';
import ThemeItemCard from './ThemeItemCard';
import { LOG_CONTEXT_NAME, LOG_MESSAGE_ID, panel_names } from '../../../utils/Config';
import usePriceInfo from '../../../hooks/usePriceInfo';
import {
LOG_CONTEXT_NAME,
LOG_MESSAGE_ID,
panel_names,
} from '../../../utils/Config';
import { $L } from '../../../utils/helperMethods';
import css from './ThemeContents.module.less';
import { sendLogTotalRecommend } from '../../../actions/logActions';
import ThemeItemCard from './ThemeItemCard';
const Container = SpotlightContainerDecorator(
{
@@ -151,18 +162,20 @@ export default function ThemeContents({
const renderItem = useCallback(
(item, index) => {
console.log("[Theme] renderItem Item",item);
console.log('[Theme] renderItem index', index, 'displayItems size', displayItems?.length);
const {
prdtId,
prdtNm,
prdtImgPath,
salePrice,
originalPrice,
patnrLogoPath,
imgUrls,
// salePrice,
// originalPrice,
// patnrLogoPath,
priceInfo,
patncNm,
showId,
catNm,
energyLabels,
euEnrgLblInfos,
} = item;
const spotlightItemId = `theme-toast-item-${index}`;
@@ -216,10 +229,9 @@ export default function ThemeContents({
key={prdtId || `theme-item-${index}`}
prdtId={prdtId}
prdtNm={prdtNm}
prdtImgPath={prdtImgPath}
salePrice={salePrice}
originalPrice={originalPrice}
energyLabels={energyLabels}
prdtImgPath={imgUrls[0]}
priceInfo={priceInfo}
energyLabels={euEnrgLblInfos}
onClick={handleItemClick}
spotlightId={spotlightItemId}
dataSpotlightDefault={index === 0}

View File

@@ -1,17 +1,48 @@
import React, { useState } from 'react';
import React, {
useCallback,
useState,
} from 'react';
import PropTypes from 'prop-types';
import {
useDispatch,
useSelector,
} from 'react-redux';
import Spotlight from '@enact/spotlight';
import Spottable from '@enact/spotlight/Spottable';
import {
setHidePopup,
setShowPopup,
} from '../../../actions/commonActions';
import {
clearConvertedImage,
convertPdfToImage,
} from '../../../actions/convertActions';
import CustomImage from '../../../components/CustomImage/CustomImage';
import TPopUp from '../../../components/TPopUp/TPopUp';
import usePriceInfo from '../../../hooks/usePriceInfo';
import * as Config from '../../../utils/Config';
import { $L } from '../../../utils/helperMethods';
import { SpotlightIds } from '../../../utils/SpotlightIds';
import css from './ThemeItemCard.module.less';
const SpottableDiv = Spottable('div');
const SpottableTemp = Spottable("div");
const STRING_CONF = {
ENERGY_LOADING: "Loading energy label...",
ENERGY_ERROR: "Failed to load energy label",
};
export default function ThemeItemCard({
prdtId,
prdtNm,
prdtImgPath,
salePrice,
originalPrice,
// salePrice,
// originalPrice,
priceInfo,
energyLabels,
onClick,
onFocus,
@@ -21,7 +52,17 @@ export default function ThemeItemCard({
dataSpotlightDefault,
onFocused,
}) {
const dispatch = useDispatch();
const [isFocused, setIsFocused] = useState(false);
const [currentPdfUrl, setCurrentPdfUrl] = useState(null);
const cursorVisible = useSelector(
(state) => state.common.appStatus.cursorVisible
);
const { activePopup, popupVisible } = useSelector(
(state) => state.common.popup
);
const convert = useSelector((state) => state.convert);
const handleFocus = (e) => {
setIsFocused(true);
@@ -34,7 +75,114 @@ export default function ThemeItemCard({
onFocused?.(false);
};
const { originalPrice, discountedPrice, discountRate } =
usePriceInfo(priceInfo) || {};
const mockEnergyLabel = [
{
"enrgLblExpsOrd": "0",
"enrgLblTpCd": "EL_TYPE_05",
"enrgLblCd": "MNLC",
"enrgClasCd": "A",
"enrgLblIcnUrl": "http://eic-ngfts.lge.com/fts/gftsDownload.lge?biz_code=LGSHOPPING&func_code=IMAGE&file_path=/lgshopping/image/gb_class_arrows_ag_a.png",
"enrgLblUrl": "https://www.lg.com/uk/lgecs.downloadFile.ldwf?DOC_ID=20241113229264&ORIGINAL_NAME_b1_a1=27U511SA EU (E).pdf&FILE_NAME=27U511SA EU (E)[20241113011401634].pdf&TC=DwnCmd&GSRI_DOC=GSRI&SPEC_DOWNLOAD=N",
"enrgShetUrl": "https://www.lg.com/uk/lgecs.downloadFile.ldwf?DOC_ID=20241125231113&ORIGINAL_NAME_b1_a1=27U511SA-W.pdf&FILE_NAME=27U511SA-W[20241125231113].pdf&TC=DwnCmd&GSRI_DOC=GSRI&SPEC_DOWNLOAD=N"
},
{
"enrgLblExpsOrd": "0",
"enrgLblTpCd": "EL_TYPE_05",
"enrgLblCd": "MNLC",
"enrgClasCd": "D",
"enrgLblIcnUrl": "http://eic-ngfts.lge.com/fts/gftsDownload.lge?biz_code=LGSHOPPING&func_code=IMAGE&file_path=/lgshopping/image/gb_class_arrows_ag_d.png",
"enrgLblUrl": "https://www.lg.com/uk/lgecs.downloadFile.ldwf?DOC_ID=20241230236057&ORIGINAL_NAME_b1_a1=27U421A EU (E).pdf&FILE_NAME=27U421A EU (E)[20241230015816192].pdf&TC=DwnCmd&GSRI_DOC=GSRI&SPEC_DOWNLOAD=N",
"enrgShetUrl": "https://www.lg.com/uk/lgecs.downloadFile.ldwf?DOC_ID=20241224235911&ORIGINAL_NAME_b1_a1=27U421A-B.pdf&FILE_NAME=27U421A-B[20241224235911].pdf&TC=DwnCmd&GSRI_DOC=GSRI&SPEC_DOWNLOAD=N"
},
{
"enrgLblExpsOrd": "0",
"enrgLblTpCd": "EL_TYPE_05",
"enrgLblCd": "MNLC",
"enrgClasCd": "D",
"enrgLblIcnUrl": "http://eic-ngfts.lge.com/fts/gftsDownload.lge?biz_code=LGSHOPPING&func_code=IMAGE&file_path=/lgshopping/image/gb_class_arrows_ag_d.png",
"enrgLblUrl": "https://www.lg.com/uk/lgecs.downloadFile.ldwf?DOC_ID=20241230236057&ORIGINAL_NAME_b1_a1=27U421A EU (E).pdf&FILE_NAME=27U421A EU (E)[20241230015816192].pdf&TC=DwnCmd&GSRI_DOC=GSRI&SPEC_DOWNLOAD=N",
"enrgShetUrl": "https://www.lg.com/uk/lgecs.downloadFile.ldwf?DOC_ID=20241224235911&ORIGINAL_NAME_b1_a1=27U421A-B.pdf&FILE_NAME=27U421A-B[20241224235911].pdf&TC=DwnCmd&GSRI_DOC=GSRI&SPEC_DOWNLOAD=N"
}
];
const setEnactFitZIndex = (zIndexValue) => {
const target = document.getElementById("floatLayer");
if (target) {
const enactFit = target.querySelector(".enact-fit");
if (enactFit) {
enactFit.style.zIndex = zIndexValue;
}
}
};
const onEnergyClick = useCallback(
(e, pdfUrl) => {
e.stopPropagation();
setCurrentPdfUrl(pdfUrl);
// PNG 이미지는 직접 표시
if (pdfUrl.endsWith(".png")) {
// console.log(`📸 [EnergyLabel] Displaying PNG directly:`, pdfUrl);
dispatch({
type: "CONVERT_PDF_TO_IMAGE_SUCCESS",
payload: { pdfUrl, imageUrl: pdfUrl },
});
dispatch(setShowPopup(Config.ACTIVE_POPUP.energyPopup));
setTimeout(() => {
Spotlight.focus(SpotlightIds.TPOPUP);
}, 250);
return;
}
// PDF 변환 시작 (최대 5회 재시도, 60초 타임아웃)
// console.log(`📄 [EnergyLabel] Starting PDF conversion:`, pdfUrl);
dispatch(
convertPdfToImage(
pdfUrl,
(error, imageUrl) => {
if (error) {
console.error(
"[EnergyLabel] 최종 변환 실패:",
error.message || error
);
// 실패해도 팝업은 열어서 에러 메시지 표시
dispatch(setShowPopup(Config.ACTIVE_POPUP.energyPopup));
setTimeout(() => {
Spotlight.focus(SpotlightIds.TPOPUP);
}, 250);
} else {
console.log(`[EnergyLabel] PDF 변환 완료, 팝업 표시`);
dispatch(setShowPopup(Config.ACTIVE_POPUP.energyPopup));
setEnactFitZIndex("10000");
setTimeout(() => {
Spotlight.focus(SpotlightIds.TPOPUP);
}, 250);
}
},
5, // 최대 5회 재시도
60000 // 60초 타임아웃
)
);
},
[dispatch]
);
const handleClosePopup = useCallback(() => {
if (convert?.convertedImage && convert.convertedImage.startsWith("blob:")) {
URL.revokeObjectURL(convert.convertedImage);
}
setEnactFitZIndex("100");
dispatch(setHidePopup());
dispatch(clearConvertedImage());
setCurrentPdfUrl(null);
}, [dispatch, convert?.convertedImage]);
return (
<>
<SpottableDiv
className={`${css.itemCard} ${isFocused ? css.focused : ''}`}
onClick={onClick}
@@ -49,10 +197,12 @@ export default function ThemeItemCard({
<div className={css.itemInfo}>
<div className={css.itemName}>{prdtNm}</div>
<div className={css.itemPrice}>
<span className={css.salePrice}>{salePrice}</span>
<span className={css.salePrice}>{discountedPrice}</span>
{discountedPrice !== originalPrice && (
<span className={css.originalPrice}>{originalPrice}</span>
)}
</div>
{energyLabels && energyLabels.length > 0 && (
{/* {energyLabels && energyLabels.length > 0 && (
<div className={css.energyLabels}>
{energyLabels.map((label, labelIndex) => (
<div key={labelIndex} className={css.energyLabel}>
@@ -60,9 +210,126 @@ export default function ThemeItemCard({
</div>
))}
</div>
)} */}
{mockEnergyLabel && mockEnergyLabel.length > 0 && (
<div className={css.energyLabels}>
{mockEnergyLabel.map((label, labelIndex) => (
<SpottableTemp
key={labelIndex}
spotlightDisabled={Boolean(!cursorVisible)}
onClick={(e) => onEnergyClick(e, label.enrgLblUrl)}
aria-label={`Energy Efficiency ${label.enrgGrade || ""}`}
>
<CustomImage
alt={`Energy Label ${label.enrgGrade || labelIndex + 1}`}
delay={0}
src={label.enrgLblIcnUrl}
/>
</SpottableTemp>
))}
</div>
)}
</div>
</SpottableDiv>
{(() => {
// 팝업이 표시되어야 하는 조건 검증
const isEnergyPopup = activePopup === Config.ACTIVE_POPUP.energyPopup;
const hasPdfUrl = !!currentPdfUrl;
const shouldShowPopup = isEnergyPopup && hasPdfUrl;
if (!shouldShowPopup) {
// console.log('[EnergyLabel] Popup not showing:', {
// isEnergyPopup,
// hasPdfUrl,
// popupVisible,
// });
return null;
}
// console.log('[EnergyLabel] Rendering popup:', {
// popupVisible,
// activePopup,
// currentPdfUrl,
// isConverting: convert?.isConverting,
// hasImage: !!convert?.convertedImage,
// hasError: !!convert?.error,
// });
return (
<TPopUp
kind="energyPopup"
title={$L("Energy Efficiency")}
hasText
open={popupVisible}
hasButton
button1Text={$L("CLOSE")}
onClose={handleClosePopup}
>
<div className={css.energyPopupContent}>
{convert ? (
<>
<div className={css.energyImagesContainer}>
{convert.convertedImage ? (
<img
alt="Energy Label"
src={convert.convertedImage}
className={css.energyImage}
/>
) : convert.error ? (
<div>
<p>{$L(STRING_CONF.ENERGY_ERROR)}</p>
<p style={{ fontSize: "0.8em", marginTop: "10px" }}>
{convert.error?.message || String(convert.error)}
</p>
</div>
) : convert.isConverting ? (
<div>
<p>{$L(STRING_CONF.ENERGY_LOADING)}</p>
<p
style={{
fontSize: "0.8em",
marginTop: "10px",
color: "#999",
}}
>
Converting PDF to image... (attempt in progress)
</p>
</div>
) : (
<div>
<p>{$L(STRING_CONF.ENERGY_ERROR)}</p>
<p
style={{
fontSize: "0.8em",
marginTop: "10px",
color: "#999",
}}
>
Unknown state - no image or error
</p>
</div>
)}
</div>
</>
) : (
<div>
<p>{$L(STRING_CONF.ENERGY_ERROR)}</p>
<p
style={{
fontSize: "0.8em",
marginTop: "10px",
color: "#999",
}}
>
Convert reducer state not found
</p>
</div>
)}
</div>
</TPopUp>
);
})()}
</>
);
}
@@ -70,8 +337,9 @@ ThemeItemCard.propTypes = {
prdtId: PropTypes.string,
prdtNm: PropTypes.string,
prdtImgPath: PropTypes.string,
salePrice: PropTypes.string,
originalPrice: PropTypes.string,
// salePrice: PropTypes.string,
// originalPrice: PropTypes.string,
priceInfo: PropTypes.string,
energyLabels: PropTypes.array,
onClick: PropTypes.func,
onFocus: PropTypes.func,

View File

@@ -38,9 +38,9 @@
z-index: 2;
}
&.focused::after,
&:focus::after,
&:global(.spotlight)::after {
&:focus::after {
opacity: 1;
}
}
@@ -64,7 +64,7 @@
display: inline-flex;
> * {
margin-bottom: 10px;
margin-bottom: 5px;
&:last-child {
margin-bottom: 0;
@@ -80,6 +80,7 @@
font-weight: 400;
line-height: 25px;
word-wrap: break-word;
.elip(2);
}
.itemPrice {
@@ -88,7 +89,7 @@
justify-content: flex-start;
align-items: center;
display: inline-flex;
height: 20px;
> * {
margin-right: 5px;
@@ -103,7 +104,6 @@
font-size: 20px;
font-family: @baseFont;
font-weight: 700;
line-height: 16.67;
word-wrap: break-word;
}
@@ -113,7 +113,7 @@
font-family: @baseFont;
font-weight: 400;
text-decoration: line-through;
line-height: 16.67;
word-wrap: break-word;
}
@@ -121,7 +121,19 @@
justify-content: flex-end;
align-items: flex-end;
display: inline-flex;
> div {
width:52px;
> img {
width:100%;
border: 4px solid transparent;
}
&:focus {
> img {
border: 4px solid @PRIMARY_COLOR_RED;
border-radius: 12px;
}
}
}
> * {
margin-right: 5px;