[홈] 에너지 라벨 #1

1. 우선 홈 > 카테고리 부분에만 적용해둠.
		- TItemCard.new.jsx에 적용.
		- 노출이 어떨때 되어야하는지 아직 이야기없음.
		- api가 아직 나오지않아 이부분에 대해서는 1000달러 이상인 금액에만 나오도록 처리.
	2. 에너지 라벨 관련에 대해서 이미지 assset/energyLabel 에 추가.
	3. Config 파일에 energyPopup추가
	4. Tpopup에 스타일 추가.
This commit is contained in:
junghoon86.park
2025-09-12 14:10:43 +09:00
parent ddf7c352eb
commit c50a3c5196
15 changed files with 667 additions and 27 deletions

BIN
.DS_Store vendored

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

View File

@@ -1,6 +1,8 @@
@import "../../style/CommonStyle.module.less";
@import "../../style/utils.module.less";
// moved into scoped blocks
/* horizontal */
.horizontal {
display: flex;
@@ -10,6 +12,14 @@
border: solid 1px @COLOR_GRAY02;
background-color: @COLOR_WHITE;
.justForYouTag {
position: absolute;
width: 100px;
top: 40px;
right: 20px;
background-color: transparent;
}
.imageWrap {
position: relative;
.size(@w: 200px, @h: 200px);
@@ -117,6 +127,14 @@
border: solid 1px @COLOR_GRAY02;
background-color: @COLOR_WHITE;
.justForYouTag {
position: absolute;
width: 100px;
top: 40px;
right: 20px;
background-color: transparent;
}
.imageWrap {
position: relative;
.size(@w: 288px, @h: 288px);
@@ -154,12 +172,46 @@
font-size: 36px;
}
}
.flexBox {
display: flex;
flex-wrap: nowrap;
}
.labelImgBox {
display: flex;
flex-wrap: wrap;
align-content: flex-end;
width: 60px;
gap: 3px;
> div {
position: relative;
z-index: 100;
&:focus {
&::after {
.focused(@boxShadow: 22px, @borderRadius: 2px);
}
}
> img {
width: 60px;
height: 35px;
}
}
}
.descWrap {
.flex(@direction: column, @alignCenter: flex-start);
width: 100%;
&.labelBox {
width: calc(100% - 60px);
> p {
font-size: 27px;
&.priceInfo {
> span {
font-size: 16px;
}
}
}
}
> div.title {
width: 100%;
height: 64px;
font-weight: bold;
line-height: 1.33;
@@ -315,8 +367,7 @@
margin-top: 11px;
.size(@w: 508px, @h: 107px);
.title {
&.title {
margin-left: 15px;
.size(@w: 430px, @h: 100px);
.productNameTitle {
@@ -414,4 +465,53 @@
.size(@w: 108px, @h: 48px);
.position(@position: absolute, @top: 30px, @left: 30px);
}
.justForYouTag {
position: absolute;
width: 100px;
top: 40px;
right: 20px;
background-color: transparent;
}
}
.popupContainer {
.header {
.size(@w: 780px , @h: 102px);
.flex(@display: flex, @justifyCenter: center, @alignCenter: center, @direction: row);
background-color: #e7ebef;
> h3 {
font-size: 36px;
color: #222222;
font-weight: bold;
}
}
.qrcodeContainer {
padding: 30px 0;
display: flex;
flex-direction: column;
align-items: center;
.qrcode {
.size(@w: 360px , @h: 360px);
background-color: #ffffff;
border-radius: 12px;
box-shadow: 0 0 0 1px #dadada inset;
margin-bottom: 41px;
}
> h3 {
display: flex;
text-align: center;
word-break: break-word;
line-height: 1.27;
}
.popupBtn {
.size(@w: 300px , @h: 78px);
margin-top: 38px;
}
}
}

View File

@@ -0,0 +1,473 @@
import React, {
memo,
useCallback,
useEffect,
useMemo,
useState,
} from 'react';
import classNames from 'classnames';
import {
useDispatch,
useSelector,
} from 'react-redux';
import Spottable from '@enact/spotlight/Spottable';
import energyDetail from '../../../assets/images/energyLabel/energyDetail.png';
import energyLabelA from '../../../assets/images/energyLabel/labelgradeA.png';
import energyLabelB from '../../../assets/images/energyLabel/labelgradeB.png';
import energyLabelC from '../../../assets/images/energyLabel/labelgradeC.png';
import energyLabelD from '../../../assets/images/energyLabel/labelgradeD.png';
import energyLabelE from '../../../assets/images/energyLabel/labelgradeE.png';
import energyLabelF from '../../../assets/images/energyLabel/labelgradeF.png';
import energyLabelG from '../../../assets/images/energyLabel/labelgradeG.png';
//에너지 라벨
import qrImg from '../../../assets/images/energyLabel/qr.png';
import defaultLogoImg
from '../../../assets/images/ic-tab-partners-default@3x.png';
import defaultimgHorizontal
from '../../../assets/images/img-thumb-empty-hor@3x.png';
import defaultImageItem
from '../../../assets/images/img-thumb-empty-product@3x.png';
import defaultimgVertical
from '../../../assets/images/img-thumb-empty-ver@3x.png';
import IcLiveShow from '../../../assets/images/tag/tag-liveshow.png';
import {
setHidePopup,
setShowPopup,
} from '../../actions/commonActions';
import { sendLogTotalRecommend } from '../../actions/logActions';
import usePriceInfo from '../../hooks/usePriceInfo';
import * as Config from '../../utils/Config';
import {
$L,
getQRCodeUrl,
removeSpecificTags,
} from '../../utils/helperMethods';
import CustomImage from '../CustomImage/CustomImage';
import TPopUp from '../TPopUp/TPopUp';
import css from './TItemCard.module.less';
const SpottableComponent = Spottable("div");
const SpottableTemp = Spottable("div");
const TYPES = {
vertical: "vertical",
horizontal: "horizontal",
videoShow: "videoShow",
};
const IMAGETYPES = {
imgHorizontal: "imgHorizontal",
imgVertical: "imgVertical",
};
const STRING_CONF = {
SOLD_OUT: "SOLD OUT",
};
export const removeDotAndColon = (string) => {
return /[.:]/.test(string) ? string.replace(/[.:]/g, "") : string;
};
const parsePrice = (price) => {
return parseFloat(price?.replace(/[^0-9.-]+/g, "") || "0");
};
export default memo(function TItemCardNew({
children,
className,
disabled,
imageAlt,
imageSource,
imgType = IMAGETYPES.imgHorizontal,
logo,
logoDisplay = false,
isBestSeller = false,
isLive = false,
onBlur,
onClick,
onFocus,
onError,
offerInfo,
priceInfo,
productId,
productName,
catNm,
rank,
soldoutFlag,
spotlightId,
nonPosition = false,
type = TYPES.vertical,
firstLabel,
label,
lastLabel,
contextName,
messageId,
order,
patnerName,
brandName,
shelfId,
shelfLocation,
shelfTitle,
contentTitle,
category,
curationId,
curationTitle,
nowProductId,
nowCategory,
nowProductTitle,
contentId,
...rest
}) {
const dispatch = useDispatch();
const [defaultImage, setDefaultImage] = useState(null);
const [prdtId, setPrdtId] = useState(null);
const countryCode = useSelector((state) => state.common.httpHeader.cntry_cd);
const cursorVisible = useSelector(
(state) => state.common.appStatus.cursorVisible
);
const { activePopup, popupVisible } = useSelector(
(state) => state.common.popup
);
const serverHOST = useSelector((state) => state.common.appStatus.serverHOST);
const serverType = useSelector((state) => state.localSettings.serverType);
useEffect(() => {
if (!imageSource) {
if (type === "videoShow") {
setDefaultImage(
imgType === IMAGETYPES.imgHorizontal
? defaultimgHorizontal
: defaultimgVertical
);
} else {
setDefaultImage(defaultImageItem);
}
}
}, [imageSource, type, imgType]);
const { originalPrice, discountedPrice, discountRate } =
usePriceInfo(priceInfo) || {};
const _onBlur = useCallback(() => {
if (onBlur) {
onBlur();
}
}, [onBlur]);
const _onClick = useCallback(
(e) => {
if (disabled) {
e.stopPropagation();
return;
}
if (onClick) {
onClick(e);
if (contextName && messageId) {
const params = {
contextName: contextName,
messageId: messageId,
shelfLocation: shelfLocation,
shelfId: shelfId,
shelfTitle: shelfTitle,
productId: productId,
productTitle: productName,
nowProductId: nowProductId,
nowCategory: nowCategory,
nowProductTitle: nowProductTitle,
partner: patnerName,
brand: brandName,
price: originalPrice,
discount: discountRate,
location: order,
category: category ? category : catNm,
contentTitle: contentTitle,
curationId: curationId,
curationTitle: curationTitle,
};
dispatch(sendLogTotalRecommend(params));
}
}
},
[
onClick,
disabled,
contextName,
messageId,
shelfLocation,
shelfId,
shelfTitle,
productId,
productName,
nowProductId,
nowCategory,
nowProductTitle,
patnerName,
brandName,
originalPrice,
discountRate,
order,
category,
catNm,
contentTitle,
curationId,
curationTitle,
dispatch,
]
);
const _onFocus = useCallback(() => {
if (onFocus) {
onFocus();
}
}, [onFocus]);
const addDefaultImg = useCallback(
(e) => {
if (onError) {
onError(e);
}
},
[onError]
);
const ariaLabel = useMemo(() => {
const soldOutText = soldoutFlag === "Y" ? "Sold Out " : "";
const firstLabelText = firstLabel ? `${firstLabel} ` : "";
const discountLabel = discountRate ? `${discountRate} discount, ` : "";
const discountPriceLabel = discountRate
? `Sale price ${discountedPrice}, `
: "";
const parsedPrice = parsePrice(originalPrice);
const priceLabel =
parsedPrice === 0
? offerInfo
? ` ${offerInfo}`
: ""
: originalPrice
? ` Original price ${originalPrice}, `
: "";
const productLabel = label || "";
const lastLabelText = lastLabel || "";
return `${soldOutText}${firstLabelText}${discountLabel}${productName}${discountPriceLabel}${priceLabel}${productLabel}${lastLabelText}`;
}, [
soldoutFlag,
firstLabel,
discountRate,
productName,
discountedPrice,
originalPrice,
offerInfo,
label,
lastLabel,
]);
const productNameDangerousHTML = useMemo(() => {
const sanitizedString = removeSpecificTags(productName);
return sanitizedString;
}, [productName]);
const handleClosePopup = useCallback(() => {
dispatch(setHidePopup());
}, [dispatch]);
const { setupPinUrl } = useMemo(() => {
return getQRCodeUrl({ serverHOST, serverType });
}, [serverHOST, serverType]);
const onEnergyClick = useCallback(
(e, pId) => {
e.stopPropagation();
setPrdtId(pId);
dispatch(setShowPopup(Config.ACTIVE_POPUP.energyPopup));
},
[dispatch]
);
return (
<>
<SpottableComponent
className={classNames(
css[type],
nonPosition && css.nonPosition,
type === "videoShow" && css[imgType],
className && className
)}
onBlur={_onBlur}
onClick={_onClick}
onFocus={_onFocus}
spotlightId={
spotlightId ?? "spotlightId-" + removeDotAndColon(productId)
}
aria-label={ariaLabel}
role="button"
{...rest}
>
<div className={css.imageWrap}>
<CustomImage
alt={imageAlt}
delay={0}
src={imageSource}
fallbackSrc={
type === "videoShow"
? imgType === IMAGETYPES.imgHorizontal
? defaultimgHorizontal
: defaultimgVertical
: defaultImageItem
}
onError={addDefaultImg}
/>
{priceInfo &&
discountRate &&
Number(discountRate.replace("%", "")) > 4 && (
<span className={css.discount}>{discountRate}</span>
)}
{soldoutFlag && soldoutFlag === "Y" && (
<div
className={classNames(
css.soldout,
countryCode === "DE" && css.de
)}
>
{$L(STRING_CONF.SOLD_OUT)}
</div>
)}
{isLive && (
<img className={css.liveTag} src={IcLiveShow} alt="Live Show" />
)}
</div>
<div className={css.flexBox}>
<div
className={classNames(
css.descWrap,
catNm && css.hstNmWrap,
originalPrice &&
parseFloat(originalPrice.replace("$", "").replace(",", "")) >
1000
? css.labelBox
: ""
)}
>
{logoDisplay && logo && (
<div className={css.logo}>
<CustomImage src={logo} fallbackSrc={defaultLogoImg} />
</div>
)}
<div className={css.title}>
<h3
className={css.productNameTitle}
dangerouslySetInnerHTML={{ __html: productNameDangerousHTML }}
/>
</div>
{children}
{priceInfo ? (
<p className={css.priceInfo}>
{parseFloat(originalPrice?.replace(/[^0-9.-]+/g, "") || "0") ===
0 ? (
<strong>{offerInfo}</strong>
) : discountRate ? (
discountedPrice
) : (
originalPrice
)}
{discountRate && (
<span className={css.originalPrice}>{originalPrice}</span>
)}
</p>
) : (
<p className={css.offerInfo}>{offerInfo}</p>
)}
</div>
{originalPrice &&
parseFloat(Number(originalPrice.replace("$", "").replace(",", ""))) >
1000 ? (
<div className={css.labelImgBox}>
<SpottableTemp
spotlightDisabled={Boolean(!cursorVisible)}
onClick={(e) => onEnergyClick(e, productId)}
>
<CustomImage
alt="Energy Label A"
delay={0}
src={energyLabelA}
/>
</SpottableTemp>
<SpottableTemp
spotlightDisabled={Boolean(!cursorVisible)}
onClick={(e) => onEnergyClick(e, productId)}
>
<CustomImage
alt="Energy Label B"
delay={0}
src={energyLabelB}
/>
</SpottableTemp>
<SpottableTemp
spotlightDisabled={Boolean(!cursorVisible)}
onClick={(e) => onEnergyClick(e, productId)}
>
<CustomImage
alt="Energy Label C"
delay={0}
src={energyLabelC}
/>
</SpottableTemp>
</div>
) : null}
</div>
{isBestSeller && rank && (
<div className={css.bestSeller}>
<span>{rank}</span>
</div>
)}
</SpottableComponent>
{activePopup === Config.ACTIVE_POPUP.energyPopup &&
prdtId === productId && (
<TPopUp
kind="energyPopup"
title="Energy Efficiency"
hasText
open={popupVisible}
hasButton
button1Text={$L("CLOSE")}
onClose={handleClosePopup}
>
<div>
<span>
<CustomImage
alt="Energy Label C"
delay={0}
src={energyDetail}
/>
<div>
<CustomImage alt="Energy Label C" delay={0} src={qrImg} />
</div>
</span>
{/* <span>
<CustomImage
alt="Energy Label C"
delay={0}
src={energyDetail}
/>
<div>
<CustomImage alt="Energy Label C" delay={0} src={qrImg} />
</div>
</span> */}
</div>
</TPopUp>
)}
</>
);
});
export { IMAGETYPES, TYPES };

View File

@@ -812,3 +812,59 @@
}
}
}
.energyPopup {
.default-style(@width: 960px);
.info {
background-color: #fff;
.textLayer {
.title {
width: 100%;
}
}
> div {
text-align: center;
> span {
display: inline-flex;
padding: 30px 0px;
width: 412px;
height: 434px;
text-align: center;
margin-top: 30px;
margin-bottom: 30px;
margin-right: 10px;
background-color: #f3f3f3;
justify-content: center;
> img {
width: 185px;
}
> div {
display: flex;
align-items: flex-start;
> img {
justify-items: baseline;
width: 135px;
}
}
}
}
.text {
padding: 0 60px;
min-height: 180px;
margin: 30px 0;
}
.buttonContainer {
margin: 0 0 30px 0;
display: flex;
justify-content: center;
> div {
min-width: 240px;
height: 78px;
margin: 0 6px;
}
}
}
}

View File

@@ -1,4 +1,4 @@
import { $L } from "./helperMethods";
import { $L } from './helperMethods';
export const SUPPORT_COUNTRIES = { US: "en", DE: "de", UK: "uk", RU: "ru" };
@@ -94,6 +94,7 @@ export const ACTIVE_POPUP = {
introTermsPopup: "introTermsPopup",
toast: "toast",
optionalConfirm: "optionalConfirm",
energyPopup: "energyPopup",
};
export const DEBUG_VIDEO_SUBTITLE_TEST = false;
export const AUTO_SCROLL_DELAY = 600;

View File

@@ -1,28 +1,38 @@
import React, { memo, useCallback, useEffect, useState } from "react";
import React, {
memo,
useCallback,
useEffect,
useState,
} from 'react';
import { useDispatch, useSelector } from "react-redux";
import {
useDispatch,
useSelector,
} from 'react-redux';
import { SpotlightContainerDecorator } from "@enact/spotlight/SpotlightContainerDecorator";
import Spottable from "@enact/spotlight/Spottable";
import { setContainerLastFocusedElement } from "@enact/spotlight/src/container";
import {
SpotlightContainerDecorator,
} from '@enact/spotlight/SpotlightContainerDecorator';
import Spottable from '@enact/spotlight/Spottable';
import { setContainerLastFocusedElement } from '@enact/spotlight/src/container';
import { sendLogCuration } from "../../../actions/logActions";
import { getSubCategory } from "../../../actions/mainActions";
import { pushPanel } from "../../../actions/panelActions";
import TItemCard from "../../../components/TItemCard/TItemCard";
import TScroller from "../../../components/TScroller/TScroller";
import usePrevious from "../../../hooks/usePrevious";
import useScrollReset from "../../../hooks/useScrollReset";
import useScrollTo from "../../../hooks/useScrollTo";
import { sendLogCuration } from '../../../actions/logActions';
import { getSubCategory } from '../../../actions/mainActions';
import { pushPanel } from '../../../actions/panelActions';
import TItemCardNew from '../../../components/TItemCard/TitemCard.new';
import TScroller from '../../../components/TScroller/TScroller';
import usePrevious from '../../../hooks/usePrevious';
import useScrollReset from '../../../hooks/useScrollReset';
import useScrollTo from '../../../hooks/useScrollTo';
import {
LOG_CONTEXT_NAME,
LOG_MESSAGE_ID,
LOG_TP_NO,
panel_names,
} from "../../../utils/Config";
import { SpotlightIds } from "../../../utils/SpotlightIds";
import CategoryNav from "../../HomePanel/SubCategory/CategoryNav/CategoryNav";
import css from "../../HomePanel/SubCategory/SubCategory.module.less";
} from '../../../utils/Config';
import { SpotlightIds } from '../../../utils/SpotlightIds';
import CategoryNav from '../../HomePanel/SubCategory/CategoryNav/CategoryNav';
import css from '../../HomePanel/SubCategory/SubCategory.module.less';
const SpottableComponent = Spottable("div");
const Container = SpotlightContainerDecorator({ enterTo: null }, "div");
@@ -207,7 +217,7 @@ export default memo(function SubCategory({
handleShelfFocus();
}
}, [handleShelfFocus]);
console.log("###test pjh");
return (
<Container
spotlightId={spotlightId}
@@ -247,7 +257,7 @@ export default memo(function SubCategory({
itemIndex
) => {
return (
<TItemCard
<TItemCardNew
key={"subItem" + itemIndex}
contextName={LOG_CONTEXT_NAME.HOME}
messageId={LOG_MESSAGE_ID.SHELF_CLICK}