[251211] feat: FeaturedBrandsPanel , TopBannerImage Focus

🕐 커밋 시간: 2025. 12. 11. 13:46:11

📊 변경 통계:
  • 총 파일: 6개
  • 추가: +48줄
  • 삭제: -16줄

📁 추가된 파일:
  + com.twin.app.shoptime/src/components/TQRCode/TQRCodeNew.jsx
  + com.twin.app.shoptime/src/components/TQRCode/TQRCodeNew.module.less

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/views/DetailPanel/ProductAllSection/ProductAllSection.jsx
  ~ com.twin.app.shoptime/src/views/DetailPanel/ProductInfoSection/QRCode/QRCode.jsx
  ~ com.twin.app.shoptime/src/views/FeaturedBrandsPanel/TopBannerImage/TopBannerImage.jsx
  ~ com.twin.app.shoptime/src/views/FeaturedBrandsPanel/TopBannerImage/TopBannerImage.module.less

🔧 주요 변경 내용:
  • UI 컴포넌트 아키텍처 개선
  • 소규모 기능 개선
  • 모듈 구조 개선
This commit is contained in:
2025-12-11 13:46:12 +09:00
parent cf27ed3846
commit d640bb74ef
6 changed files with 141 additions and 16 deletions

View File

@@ -0,0 +1,93 @@
import React, { useEffect, useMemo, useRef, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { getDeviceAdditionInfo } from "../../actions/deviceActions";
import { scaleH, scaleW } from "../../utils/helperMethods";
export default function TQRCodeNew({
isBillingProductVisible,
ariaLabel,
text,
width = "128",
height = "128",
}) {
const qrcodeRef = useRef(null);
const deviceInfo = useSelector((state) => state.device.deviceInfo);
const { entryMenu, nowMenu } = useSelector((state) => state.common.menu);
const dispatch = useDispatch();
useEffect(() => {
if (!deviceInfo) {
dispatch(getDeviceAdditionInfo());
}
}, [deviceInfo, dispatch]);
const applyCircularMask = (scaledWidth, scaledHeight) => {
if (!qrcodeRef.current) return;
const canvas = qrcodeRef.current.querySelector('canvas');
if (!canvas) return;
const radius = scaledWidth / 2;
// 원본 canvas 저장
const tempCanvas = document.createElement('canvas');
tempCanvas.width = scaledWidth;
tempCanvas.height = scaledHeight;
const tempCtx = tempCanvas.getContext('2d');
tempCtx.drawImage(canvas, 0, 0);
// 원본 canvas 초기화
const ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, scaledWidth, scaledHeight);
// 원형 마스크 적용
ctx.beginPath();
ctx.arc(radius, radius, radius, 0, Math.PI * 2);
ctx.clip();
// 이미지 다시 그리기
ctx.drawImage(tempCanvas, 0, 0);
};
useEffect(() => {
if (typeof window === "object" && entryMenu && nowMenu) {
if (qrcodeRef.current) {
while (qrcodeRef.current.firstChild) {
qrcodeRef.current.removeChild(qrcodeRef.current.firstChild);
}
}
// nowMenu 데이터를 Base64로 인코딩
const encodedNowMenu = encodeURIComponent(nowMenu);
const encodeEntryMenu = encodeURIComponent(entryMenu);
let idx;
if (deviceInfo === null || !deviceInfo) {
idx = 0;
} else {
idx = deviceInfo?.dvcIndex;
}
const scaledWidth = scaleW(width);
const scaledHeight = scaleH(height);
const qrcode = new window.QRCode(qrcodeRef.current, {
text: isBillingProductVisible
? text
: `${text}&entryMenu=${encodeEntryMenu}&nowMenu=${encodedNowMenu}&idx=${idx}`,
width: scaledWidth,
height: scaledHeight,
correctLevel: window.QRCode.CorrectLevel.L,
});
// QR코드 생성 완료 후 원형 마스킹 적용
setTimeout(() => {
applyCircularMask(scaledWidth, scaledHeight);
}, 100);
}
}, [text, deviceInfo, entryMenu, nowMenu, isBillingProductVisible, width, height]);
return <div aria-label={ariaLabel} ref={qrcodeRef} />;
}

View File

@@ -570,6 +570,21 @@ export default function ProductAllSection({
[productType, themeProductInfo, themeProducts, selectedIndex, productInfo] [productType, themeProductInfo, themeProducts, selectedIndex, productInfo]
); );
// 🆕 [251211] patnrId=21인 경우 QR 데이터 확인
useEffect(() => {
if (productData?.patnrId === 21 || productData?.patnrId === "21") {
console.log('[QR-Data] patnrId=21 QR 데이터 확인:', {
patnrId: productData?.patnrId,
prdtId: productData?.prdtId,
qrImgUrl: productData?.qrImgUrl,
qrCodeUrl: productData?.qrCodeUrl,
hasQrImgUrl: !!productData?.qrImgUrl,
hasQrCodeUrl: !!productData?.qrCodeUrl,
allData: productData,
});
}
}, [productData]);
// 단품(결제 가능 상품) - DetailPanel.backup.jsx와 동일한 로직 // 단품(결제 가능 상품) - DetailPanel.backup.jsx와 동일한 로직
const isBillingProductVisible = useMemo(() => { const isBillingProductVisible = useMemo(() => {
// API Mode: 기존 로직 100% 유지 (절대 수정 안 함) // API Mode: 기존 로직 100% 유지 (절대 수정 안 함)

View File

@@ -4,6 +4,7 @@ import classNames from 'classnames';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import TQRCode from '../../../../components/TQRCode/TQRCode'; import TQRCode from '../../../../components/TQRCode/TQRCode';
import TQRCodeNew from '../../../../components/TQRCode/TQRCodeNew';
import { getQRCodeUrl } from '../../../../utils/helperMethods'; import { getQRCodeUrl } from '../../../../utils/helperMethods';
import css from './QRCode.module.less'; import css from './QRCode.module.less';
@@ -56,13 +57,26 @@ export default function QRCode({
return detailUrl; return detailUrl;
}, [productInfo, isShopByMobile, detailUrl]); }, [productInfo, isShopByMobile, detailUrl]);
// patnrId === 21인 경우 TQRCodeNew 사용 (원형 QR코드)
const isPatnrId21 = productInfo?.patnrId === 21 || productInfo?.patnrId === "21";
return ( return (
<div className={classNames(css.qrcode, kind ? css.detailQrcode : "")}> <div className={classNames(css.qrcode, kind ? css.detailQrcode : "")}>
{/* {qrCodeUrl && <TQRCode text={qrCodeUrl} width="190" height="190" />} */} {/* {qrCodeUrl && <TQRCode text={qrCodeUrl} width="190" height="190" />} */}
{kind === "detail" ? ( {kind === "detail" ? (
<TQRCode text={qrCodeUrl} width="240" height="240" /> isPatnrId21 ? (
<TQRCodeNew text={qrCodeUrl} width="240" height="240" />
) : (
<TQRCode text={qrCodeUrl} width="240" height="240" />
)
) : ( ) : (
qrCodeUrl && <TQRCode text={qrCodeUrl} width="190" height="190" /> qrCodeUrl && (
isPatnrId21 ? (
<TQRCodeNew text={qrCodeUrl} width="190" height="190" />
) : (
<TQRCode text={qrCodeUrl} width="190" height="190" />
)
)
)} )}
{/* todo : 시나리오,UI 릴리즈 후 */} {/* todo : 시나리오,UI 릴리즈 후 */}
<div className={css.tooltip}> <div className={css.tooltip}>

View File

@@ -2,13 +2,10 @@ import React, { memo, useCallback, useState } from "react";
import { useDispatch } from "react-redux"; import { useDispatch } from "react-redux";
import { setShowPopup } from "../../../actions/commonActions"; import { setShowPopup } from "../../../actions/commonActions";
import CustomImage from "../../../components/CustomImage/CustomImage"; import CustomImage from "../../../components/CustomImage/CustomImage";
import SpotlightContainerDecorator from "@enact/spotlight/SpotlightContainerDecorator"; import Spottable from "@enact/spotlight/Spottable";
import css from "./TopBannerImage.module.less"; import css from "./TopBannerImage.module.less";
const Container = SpotlightContainerDecorator( const SpottableDiv = Spottable("div");
{ leaveFor: { left: "", right: "", down: "best-seller-spotlightId" }, enterTo: "default-element" },
"div"
);
const TopBannerImage = memo(({ banrImgUrl, banrImgNm, banrNm, pupBanrImgUrl, pupBanrImgNm, spotlightId }) => { const TopBannerImage = memo(({ banrImgUrl, banrImgNm, banrNm, pupBanrImgUrl, pupBanrImgNm, spotlightId }) => {
console.log("[TOP-BANNER-IMG] Rendering with URL:", banrImgUrl); console.log("[TOP-BANNER-IMG] Rendering with URL:", banrImgUrl);
@@ -51,10 +48,9 @@ const TopBannerImage = memo(({ banrImgUrl, banrImgNm, banrNm, pupBanrImgUrl, pup
} }
return ( return (
<Container <SpottableDiv
className={css.topBannerContainer} className={css.topBannerContainer}
spotlightId={spotlightId} spotlightId={spotlightId}
tabIndex={0}
onClick={handleClick} onClick={handleClick}
style={{ style={{
// 이미지 크기에 맞춰 컨테이너 크기 조정 // 이미지 크기에 맞춰 컨테이너 크기 조정
@@ -72,7 +68,7 @@ const TopBannerImage = memo(({ banrImgUrl, banrImgNm, banrNm, pupBanrImgUrl, pup
height: imageDimensions.height || 'auto' height: imageDimensions.height || 'auto'
}} }}
/> />
</Container> </SpottableDiv>
); );
}); });

View File

@@ -1,25 +1,32 @@
@import "../../../style/CommonStyle.module.less";
@import "../../../style/utils.module.less";
.topBannerContainer { .topBannerContainer {
position: absolute; position: absolute;
right: 60px; right: 60px;
top: 48px; top: 48px;
padding: 15px; padding: 15px;
background-color: transparent; background-color: transparent;
cursor: pointer;
// Spotlight 포커스 스타일 // Spotlight 포커스 스타일 (TItemCard 방식)
&:focus { &:focus {
&::after {
.focused(@boxShadow: 10px, @borderRadius: 4px);
}
}
// 마우스 호버 스타일
&:hover {
outline: 2px solid #fff; outline: 2px solid #fff;
outline-offset: 2px; outline-offset: 2px;
background-color: rgba(255, 255, 255, 0.1); background-color: rgba(255, 255, 255, 0.1);
} }
// Spotlight hover 효과
&[data-spotlight-id] {
cursor: pointer;
}
} }
.topBannerImage { .topBannerImage {
display: block; display: block;
pointer-events: none;
// 크기는 JavaScript에서 동적으로 설정 // 크기는 JavaScript에서 동적으로 설정
border-radius: 4px; border-radius: 4px;
} }