From 4a84235ff2804d53c202ebe4aa476302915ef8bb Mon Sep 17 00:00:00 2001 From: optrader Date: Thu, 4 Sep 2025 12:56:26 +0900 Subject: [PATCH] =?UTF-8?q?[250904]=20feat:=20DetailPanel=20UserReviews=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=EB=84=A4=EC=9D=B4=EC=85=98=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ๐Ÿ• ์ปค๋ฐ‹ ์‹œ๊ฐ„: 2025. 09. 04. 12:56:09 ๐Ÿ’ฌ ์‚ฌ์šฉ์ž ๋ฉ”์‹œ์ง€: UserReviews์™€ CustomerImages์— ํŽ˜์ด์ง€๋„ค์ด์…˜ ๊ธฐ๋Šฅ ์ถ”๊ฐ€ - CustomerImages: 5๊ฐœ์”ฉ ํ‘œ์‹œํ•˜๋Š” View More ๋ฒ„ํŠผ ๊ธฐ๋Šฅ - UserReviews: ๋ชจ๋“  ๋ฆฌ๋ทฐ ๋ฐ์ดํ„ฐ ํ‘œ์‹œ๋กœ ๋ณ€๊ฒฝ - Chromium 68 ํ˜ธํ™˜์„ฑ ๊ฐœ์„  (Optional Chaining ์ œ๊ฑฐ) - API ์—”๋“œํฌ์ธํŠธ ๋ฐ Redux ์•ก์…˜/๋ฆฌ๋“€์„œ ์ถ”๊ฐ€ - 1124px ๋ ˆ์ด์•„์›ƒ ํ†ต์ผ ๋ฐ View More ๋ฒ„ํŠผ ์Šคํƒ€์ผ๋ง ๐Ÿ“Š ๋ณ€๊ฒฝ ํ†ต๊ณ„: โ€ข ์ด ํŒŒ์ผ: 57๊ฐœ โ€ข ์ถ”๊ฐ€: +1252์ค„ โ€ข ์‚ญ์ œ: -540์ค„ ๐Ÿ“ ์ถ”๊ฐ€๋œ ํŒŒ์ผ: + com.twin.app.shoptime/assets/images/icons/ic-gr-call-1.png + com.twin.app.shoptime/assets/images/image-review-sample-1.png + com.twin.app.shoptime/src/views/DetailPanel/DetailPanel.backup.jsx + com.twin.app.shoptime/src/views/DetailPanel/DetailPanel.backup.module.less + 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/ProductDescription/ProductDescription.jsx + com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/ProductDescription/ProductDescription.module.less + com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/ProductDescription/package.json + com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/ProductDetail/ProductDetail.new.jsx + com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/ProductDetail/ProductDetail.new.module.less + com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/UserReviews/CustomerImages/CustomerImages.jsx + com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/UserReviews/CustomerImages/CustomerImages.module.less + com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/UserReviews/UserReviews.jsx + com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/UserReviews/UserReviews.module.less + com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/UserReviews/UserReviewsPopup/UserReviewsPopup.figma.jsx + com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/UserReviews/UserReviewsPopup/UserReviewsPopup.jsx + com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/UserReviews/UserReviewsPopup/UserReviewsPopup.module.less + com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/UserReviews/package.json + com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/YouMayAlsoLike/YouMayAlsoLike.jsx + com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/YouMayAlsoLike/YouMayAlsoLike.module.less + com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/YouMayAlsoLike/package.json + com.twin.app.shoptime/src/views/DetailPanel/ProductInfoSection/QRCode/QRCode.jsx + com.twin.app.shoptime/src/views/DetailPanel/ProductInfoSection/QRCode/QRCode.module.less + com.twin.app.shoptime/src/views/DetailPanel/ProductInfoSection/QRCode/package.json + com.twin.app.shoptime/src/views/DetailPanel/ProductOverview/ProductOverview.jsx + com.twin.app.shoptime/src/views/DetailPanel/ProductOverview/ProductOverview.module.less + com.twin.app.shoptime/src/views/DetailPanel/ProductOverview/ProductPriceDisplay/BuyNowPriceDisplay/BuyNowPriceDisplay.jsx + com.twin.app.shoptime/src/views/DetailPanel/ProductOverview/ProductPriceDisplay/BuyNowPriceDisplay/BuyNowPriceDisplay.module.less + com.twin.app.shoptime/src/views/DetailPanel/ProductOverview/ProductPriceDisplay/BuyNowPriceDisplay/package.json + com.twin.app.shoptime/src/views/DetailPanel/ProductOverview/ProductPriceDisplay/ProductPriceDisplay.jsx + com.twin.app.shoptime/src/views/DetailPanel/ProductOverview/ProductPriceDisplay/ProductPriceDisplay.module.less + com.twin.app.shoptime/src/views/DetailPanel/ProductOverview/ProductPriceDisplay/ShopByMobilePriceDisplay/ShopByMobilePriceDisplay.jsx + com.twin.app.shoptime/src/views/DetailPanel/ProductOverview/ProductPriceDisplay/ShopByMobilePriceDisplay/ShopByMobilePriceDisplay.module.less + com.twin.app.shoptime/src/views/DetailPanel/ProductOverview/ProductPriceDisplay/ShopByMobilePriceDisplay/package.json + com.twin.app.shoptime/src/views/DetailPanel/ProductOverview/ProductPriceDisplay/package.json + com.twin.app.shoptime/src/views/DetailPanel/ProductOverview/package.json + com.twin.app.shoptime/src/views/DetailPanel/ThemeItemListOverlay/ThemeItemListOverlay.jsx + com.twin.app.shoptime/src/views/DetailPanel/ThemeItemListOverlay/ThemeItemListOverlay.module.less + com.twin.app.shoptime/src/views/DetailPanel/components/CustomScrollbar/CustomScrollbar.jsx + com.twin.app.shoptime/src/views/DetailPanel/components/CustomScrollbar/CustomScrollbar.module.less + com.twin.app.shoptime/src/views/DetailPanel/components/DetailMobileSendPopUp.jsx + com.twin.app.shoptime/src/views/DetailPanel/components/DetailMobileSendPopUp.module.less + com.twin.app.shoptime/src/views/DetailPanel/components/THeaderCustom.jsx + com.twin.app.shoptime/src/views/DetailPanel/components/THeaderCustom.module.less + com.twin.app.shoptime/src/views/DetailPanel/components/TScroller/TScrollerDetail.jsx + com.twin.app.shoptime/src/views/DetailPanel/components/TScroller/TScrollerDetail.module.less + package-lock.json ๐Ÿ“ ์ˆ˜์ •๋œ ํŒŒ์ผ: ~ com.twin.app.shoptime/src/actions/actionTypes.js ~ com.twin.app.shoptime/src/actions/productActions.js ~ com.twin.app.shoptime/src/api/apiConfig.js ~ com.twin.app.shoptime/src/reducers/productReducer.js ~ com.twin.app.shoptime/src/utils/fp.js ~ com.twin.app.shoptime/src/utils/lodash.js ~ com.twin.app.shoptime/src/utils/lodashFpEx.js ~ com.twin.app.shoptime/src/views/DetailPanel/DetailPanel.jsx ~ com.twin.app.shoptime/src/views/DetailPanel/DetailPanel.module.less ๐Ÿ”ง ์ฃผ์š” ๋ณ€๊ฒฝ ๋‚ด์šฉ: โ€ข ํƒ€์ž… ์‹œ์Šคํ…œ ์•ˆ์ •์„ฑ ๊ฐ•ํ™” โ€ข ํ•ต์‹ฌ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ๊ฐœ์„  โ€ข API ์„œ๋น„์Šค ๋ ˆ์ด์–ด ๊ฐœ์„  โ€ข ๊ณตํ†ต ์œ ํ‹ธ๋ฆฌํ‹ฐ ํ•จ์ˆ˜ ์ตœ์ ํ™” โ€ข ํ”„๋กœ์ ํŠธ ์˜์กด์„ฑ ๊ด€๋ฆฌ ๊ฐœ์„  โ€ข UI ์ปดํฌ๋„ŒํŠธ ์•„ํ‚คํ…์ฒ˜ ๊ฐœ์„  โ€ข ๋Œ€๊ทœ๋ชจ ๊ธฐ๋Šฅ ๊ฐœ๋ฐœ โ€ข ๋ชจ๋“ˆ ๊ตฌ์กฐ ๊ฐœ์„  BREAKING CHANGE: API ๋˜๋Š” ์„ค์ • ๋ณ€๊ฒฝ์œผ๋กœ ์ธํ•œ ํ˜ธํ™˜์„ฑ ์˜ํ–ฅ ๊ฐ€๋Šฅ --- .../assets/images/icons/ic-gr-call-1.png | Bin 0 -> 1748 bytes .../assets/images/image-review-sample-1.png | Bin 0 -> 84218 bytes .../src/actions/actionTypes.js | 27 +- .../src/actions/productActions.js | 315 +++++- com.twin.app.shoptime/src/api/apiConfig.js | 1 + .../src/reducers/productReducer.js | 99 +- com.twin.app.shoptime/src/utils/fp.js | 70 +- com.twin.app.shoptime/src/utils/lodash.js | 7 +- com.twin.app.shoptime/src/utils/lodashFpEx.js | 280 ++++- .../views/DetailPanel/DetailPanel.backup.jsx | 550 ++++++++++ .../DetailPanel.backup.module.less | 41 + .../src/views/DetailPanel/DetailPanel.jsx | 987 ++++++++++-------- .../views/DetailPanel/DetailPanel.module.less | 30 +- .../ProductAllSection/ProductAllSection.jsx | 431 ++++++++ .../ProductAllSection.module.less | 465 +++++++++ .../ProductDescription/ProductDescription.jsx | 76 ++ .../ProductDescription.module.less | 54 + .../ProductDescription/package.json | 6 + .../ProductDetail/ProductDetail.new.jsx | 96 ++ .../ProductDetail.new.module.less | 87 ++ .../CustomerImages/CustomerImages.jsx | 268 +++++ .../CustomerImages/CustomerImages.module.less | 97 ++ .../UserReviews/UserReviews.jsx | 240 +++++ .../UserReviews/UserReviews.module.less | 111 ++ .../UserReviewsPopup.figma.jsx | 33 + .../UserReviewsPopup/UserReviewsPopup.jsx | 104 ++ .../UserReviewsPopup.module.less | 201 ++++ .../UserReviews/package.json | 6 + .../YouMayAlsoLike/YouMayAlsoLike.jsx | 145 +++ .../YouMayAlsoLike/YouMayAlsoLike.module.less | 107 ++ .../YouMayAlsoLike/package.json | 6 + .../ProductInfoSection/QRCode/QRCode.jsx | 67 ++ .../QRCode/QRCode.module.less | 12 + .../ProductInfoSection/QRCode/package.json | 6 + .../ProductOverview/ProductOverview.jsx | 51 + .../ProductOverview.module.less | 117 +++ .../BuyNowPriceDisplay/BuyNowPriceDisplay.jsx | 61 ++ .../BuyNowPriceDisplay.module.less | 0 .../BuyNowPriceDisplay/package.json | 6 + .../ProductPriceDisplay.jsx | 91 ++ .../ProductPriceDisplay.module.less | 101 ++ .../ShopByMobilePriceDisplay.jsx | 200 ++++ .../ShopByMobilePriceDisplay.module.less | 101 ++ .../ShopByMobilePriceDisplay/package.json | 6 + .../ProductPriceDisplay/package.json | 6 + .../DetailPanel/ProductOverview/package.json | 6 + .../ThemeItemListOverlay.jsx | 200 ++++ .../ThemeItemListOverlay.module.less | 68 ++ .../CustomScrollbar/CustomScrollbar.jsx | 144 +++ .../CustomScrollbar.module.less | 51 + .../components/DetailMobileSendPopUp.jsx | 193 ++++ .../DetailMobileSendPopUp.module.less | 2 + .../DetailPanel/components/THeaderCustom.jsx | 93 ++ .../components/THeaderCustom.module.less | 49 + .../components/TScroller/TScrollerDetail.jsx | 244 +++++ .../TScroller/TScrollerDetail.module.less | 39 + 56 files changed, 6302 insertions(+), 552 deletions(-) create mode 100644 com.twin.app.shoptime/assets/images/icons/ic-gr-call-1.png create mode 100644 com.twin.app.shoptime/assets/images/image-review-sample-1.png create mode 100644 com.twin.app.shoptime/src/views/DetailPanel/DetailPanel.backup.jsx create mode 100644 com.twin.app.shoptime/src/views/DetailPanel/DetailPanel.backup.module.less create mode 100644 com.twin.app.shoptime/src/views/DetailPanel/ProductAllSection/ProductAllSection.jsx create mode 100644 com.twin.app.shoptime/src/views/DetailPanel/ProductAllSection/ProductAllSection.module.less create mode 100644 com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/ProductDescription/ProductDescription.jsx create mode 100644 com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/ProductDescription/ProductDescription.module.less create mode 100644 com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/ProductDescription/package.json create mode 100644 com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/ProductDetail/ProductDetail.new.jsx create mode 100644 com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/ProductDetail/ProductDetail.new.module.less create mode 100644 com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/UserReviews/CustomerImages/CustomerImages.jsx create mode 100644 com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/UserReviews/CustomerImages/CustomerImages.module.less create mode 100644 com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/UserReviews/UserReviews.jsx create mode 100644 com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/UserReviews/UserReviews.module.less create mode 100644 com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/UserReviews/UserReviewsPopup/UserReviewsPopup.figma.jsx create mode 100644 com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/UserReviews/UserReviewsPopup/UserReviewsPopup.jsx create mode 100644 com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/UserReviews/UserReviewsPopup/UserReviewsPopup.module.less create mode 100644 com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/UserReviews/package.json create mode 100644 com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/YouMayAlsoLike/YouMayAlsoLike.jsx create mode 100644 com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/YouMayAlsoLike/YouMayAlsoLike.module.less create mode 100644 com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/YouMayAlsoLike/package.json create mode 100644 com.twin.app.shoptime/src/views/DetailPanel/ProductInfoSection/QRCode/QRCode.jsx create mode 100644 com.twin.app.shoptime/src/views/DetailPanel/ProductInfoSection/QRCode/QRCode.module.less create mode 100644 com.twin.app.shoptime/src/views/DetailPanel/ProductInfoSection/QRCode/package.json create mode 100644 com.twin.app.shoptime/src/views/DetailPanel/ProductOverview/ProductOverview.jsx create mode 100644 com.twin.app.shoptime/src/views/DetailPanel/ProductOverview/ProductOverview.module.less create mode 100644 com.twin.app.shoptime/src/views/DetailPanel/ProductOverview/ProductPriceDisplay/BuyNowPriceDisplay/BuyNowPriceDisplay.jsx create mode 100644 com.twin.app.shoptime/src/views/DetailPanel/ProductOverview/ProductPriceDisplay/BuyNowPriceDisplay/BuyNowPriceDisplay.module.less create mode 100644 com.twin.app.shoptime/src/views/DetailPanel/ProductOverview/ProductPriceDisplay/BuyNowPriceDisplay/package.json create mode 100644 com.twin.app.shoptime/src/views/DetailPanel/ProductOverview/ProductPriceDisplay/ProductPriceDisplay.jsx create mode 100644 com.twin.app.shoptime/src/views/DetailPanel/ProductOverview/ProductPriceDisplay/ProductPriceDisplay.module.less create mode 100644 com.twin.app.shoptime/src/views/DetailPanel/ProductOverview/ProductPriceDisplay/ShopByMobilePriceDisplay/ShopByMobilePriceDisplay.jsx create mode 100644 com.twin.app.shoptime/src/views/DetailPanel/ProductOverview/ProductPriceDisplay/ShopByMobilePriceDisplay/ShopByMobilePriceDisplay.module.less create mode 100644 com.twin.app.shoptime/src/views/DetailPanel/ProductOverview/ProductPriceDisplay/ShopByMobilePriceDisplay/package.json create mode 100644 com.twin.app.shoptime/src/views/DetailPanel/ProductOverview/ProductPriceDisplay/package.json create mode 100644 com.twin.app.shoptime/src/views/DetailPanel/ProductOverview/package.json create mode 100644 com.twin.app.shoptime/src/views/DetailPanel/ThemeItemListOverlay/ThemeItemListOverlay.jsx create mode 100644 com.twin.app.shoptime/src/views/DetailPanel/ThemeItemListOverlay/ThemeItemListOverlay.module.less create mode 100644 com.twin.app.shoptime/src/views/DetailPanel/components/CustomScrollbar/CustomScrollbar.jsx create mode 100644 com.twin.app.shoptime/src/views/DetailPanel/components/CustomScrollbar/CustomScrollbar.module.less create mode 100644 com.twin.app.shoptime/src/views/DetailPanel/components/DetailMobileSendPopUp.jsx create mode 100644 com.twin.app.shoptime/src/views/DetailPanel/components/DetailMobileSendPopUp.module.less create mode 100644 com.twin.app.shoptime/src/views/DetailPanel/components/THeaderCustom.jsx create mode 100644 com.twin.app.shoptime/src/views/DetailPanel/components/THeaderCustom.module.less create mode 100644 com.twin.app.shoptime/src/views/DetailPanel/components/TScroller/TScrollerDetail.jsx create mode 100644 com.twin.app.shoptime/src/views/DetailPanel/components/TScroller/TScrollerDetail.module.less diff --git a/com.twin.app.shoptime/assets/images/icons/ic-gr-call-1.png b/com.twin.app.shoptime/assets/images/icons/ic-gr-call-1.png new file mode 100644 index 0000000000000000000000000000000000000000..e34cabe4e91225fc32255156f6e8fadebba7c41b GIT binary patch literal 1748 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�pT_ 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

PjKH@Qb$6;IB*YyZ^!Lx~| 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 (src) => + reduce( + (acc, key) => + src && src[key] !== null && src[key] !== undefined + ? set(key, src[key], acc) + : acc, + {}, + keys + ); + +// Generic request thunk factory +const createRequestThunk = ({ + method, + url, + type, + params = () => ({}), + data = () => ({}), + tag, + onSuccessExtra = () => (dispatch, getState, response) => {}, + onFailExtra = () => (dispatch, getState, error) => {}, + selectPayload = (response) => get("data.data", response), +}) => + (props) => (dispatch, getState) => { + const query = params(props); + const body = data(props); + + const onSuccess = (response) => { + console.log(`${tag} onSuccess`, response.data); + dispatch({ type, payload: selectPayload(response) }); + onSuccessExtra(props, dispatch, getState, response); + }; + + const onFail = (error) => { + console.error(`${tag} onFail`, error); + onFailExtra(props, dispatch, getState, error); + }; + + TAxios( + dispatch, + getState, + method, + url, + query, + body, + onSuccess, + onFail + ); + }; + +// Specialization for GET +const createGetThunk = ({ url, type, params = () => ({}), tag }) => + createRequestThunk({ method: "get", url, type, params, tag }); + export const getBestSeller = (callback) => (dispatch, getState) => { const onSuccess = (response) => { console.log("getBestSeller onSuccess", response.data); - - dispatch({ - type: types.GET_BEST_SELLER, - payload: response.data.data, - }); + dispatch({ type: types.GET_BEST_SELLER, payload: get("data.data", response) }); dispatch(changeAppStatus({ showLoadingPanel: { show: false } })); - - if (callback) { - callback(); - } + callback && callback(); }; const onFail = (error) => { console.error("getBestSeller onFail", error); dispatch(changeAppStatus({ showLoadingPanel: { show: false } })); - - if (callback) { - callback(); - } + callback && callback(); }; TAxios( @@ -41,62 +89,227 @@ export const getBestSeller = (callback) => (dispatch, getState) => { }; // Detail ์˜ต์…˜์ƒํ’ˆ ์ •๋ณด ์กฐํšŒ IF-LGSP-319 -export const getProductGroup = (props) => (dispatch, getState) => { - const { patnrId, prdtId } = props; - - const onSuccess = (response) => { - console.log("getProductGroup onSuccess", response.data); - - dispatch({ - type: types.GET_PRODUCT_GROUP, - payload: response.data.data, - }); - }; - - const onFail = (error) => { - console.error("getProductGroup onFail", error); - }; - - TAxios( - dispatch, - getState, - "get", - URLS.GET_PRODUCT_GROUP, - { patnrId, prdtId }, - {}, - onSuccess, - onFail - ); -}; +export const getProductGroup = createGetThunk({ + url: URLS.GET_PRODUCT_GROUP, + type: types.GET_PRODUCT_GROUP, + params: pickParams(["patnrId", "prdtId"]), + tag: "getProductGroup", +}); // Detail ์˜ต์…˜์ƒํ’ˆ ์ •๋ณด ์กฐํšŒ IF-LGSP-320 -export const getProductOption = (props) => (dispatch, getState) => { - const { patnrId, prdtId } = props; +export const getProductOption = createGetThunk({ + url: URLS.GET_PRODUCT_OPTION, + type: types.GET_PRODUCT_OPTION, + params: pickParams(["patnrId", "prdtId"]), + tag: "getProductOption", +}); + +// FP: ์‹ค์ œ API ์‘๋‹ต์—์„œ data ๋ถ€๋ถ„๋งŒ ์ถ”์ถœ (๊ฐ„์†Œํ™”) +const extractReviewApiData = (apiResponse) => { + try { + console.log("[UserReviews] ๐Ÿ” extractReviewApiData - ์ „์ฒด API ์‘๋‹ต ๋ถ„์„:", { + fullApiResponse: apiResponse, + hasData: !!(apiResponse && apiResponse.data), + retCode: apiResponse && apiResponse.retCode, + retMsg: apiResponse && apiResponse.retMsg, + responseKeys: apiResponse ? Object.keys(apiResponse) : [], + dataKeys: apiResponse && apiResponse.data ? Object.keys(apiResponse.data) : [] + }); + + // ์—ฌ๋Ÿฌ ๊ฐ€๋Šฅํ•œ ๋ฐ์ดํ„ฐ ๊ฒฝ๋กœ ์‹œ๋„ + let apiData = null; + + // 1. response.data.data (์ค‘์ฒฉ ๊ตฌ์กฐ) + if (apiResponse && apiResponse.data && apiResponse.data.data) { + apiData = apiResponse.data.data; + console.log("[UserReviews] โœ… ๋ฐ์ดํ„ฐ ๊ฒฝ๋กœ 1: response.data.data ์‚ฌ์šฉ"); + } + // 2. response.data (๋‹จ์ผ ๊ตฌ์กฐ) + else if (apiResponse && apiResponse.data && (apiResponse.data.reviewList || apiResponse.data.reviewDetail)) { + apiData = apiResponse.data; + console.log("[UserReviews] โœ… ๋ฐ์ดํ„ฐ ๊ฒฝ๋กœ 2: response.data ์‚ฌ์šฉ"); + } + // 3. response ์ง์ ‘ (์ตœ์ƒ์œ„) + else if (apiResponse && (apiResponse.reviewList || apiResponse.reviewDetail)) { + apiData = apiResponse; + console.log("[UserReviews] โœ… ๋ฐ์ดํ„ฐ ๊ฒฝ๋กœ 3: response ์ง์ ‘ ์‚ฌ์šฉ"); + } + + if (!apiData) { + console.warn("[UserReviews] โŒ ๋ชจ๋“  ๋ฐ์ดํ„ฐ ๊ฒฝ๋กœ์—์„œ ์ถ”์ถœ ์‹คํŒจ:", { + hasResponseData: !!(apiResponse && apiResponse.data), + hasReviewList: !!(apiResponse && apiResponse.data && apiResponse.data.reviewList), + hasReviewDetail: !!(apiResponse && apiResponse.data && apiResponse.data.reviewDetail), + hasNestedData: !!(apiResponse && apiResponse.data && apiResponse.data.data), + fullResponse: apiResponse + }); + return null; + } + + // ์ถ”์ถœ๋œ ๋ฐ์ดํ„ฐ ๊ฒ€์ฆ + console.log("[UserReviews] ๐Ÿ“Š ์ถ”์ถœ๋œ ๋ฐ์ดํ„ฐ ๊ฒ€์ฆ:", { + hasReviewList: !!apiData.reviewList, + hasReviewDetail: !!apiData.reviewDetail, + reviewListLength: apiData.reviewList ? apiData.reviewList.length : 0, + reviewDetailKeys: apiData.reviewDetail ? Object.keys(apiData.reviewDetail) : [], + totRvwCnt: apiData.reviewDetail && apiData.reviewDetail.totRvwCnt, + totRvwAvg: apiData.reviewDetail && apiData.reviewDetail.totRvwAvg, + extractedData: apiData + }); + + return apiData; + } catch (error) { + console.error("[UserReviews] โŒ extractReviewApiData ์—๋Ÿฌ:", error); + return null; + } +}; + +// Mock ๋ฐ์ดํ„ฐ ์ƒ์„ฑ ํ•จ์ˆ˜ (์žฌ์‚ฌ์šฉ์„ฑ ์œ„ํ•ด ๋ถ„๋ฆฌ) +const createMockReviewData = () => ({ + reviewList: [ + { + rvwId: "mock-review-1", + rvwRtng: 5, + rvwCtnt: "The shoes are really stylish and comfortable for daily wear. I love the design and how lightweight they feel. However, the size runs a bit small, so I'd recommend ordering half a size up.", + rvwRgstDtt: "2024-01-15", + reviewImageList: [ + { + imgId: "mock-img-1", + imgUrl: reviewSampleImage, + imgSeq: 1 + } + ] + }, + { + rvwId: "mock-review-2", + rvwRtng: 4, + rvwCtnt: "Great value for the price! The quality is better than I expected. Shipping was fast and the product arrived in perfect condition. Would definitely recommend to others.", + rvwRgstDtt: "2024-01-10", + reviewImageList: [] + } + ], + reviewDetail: { + totRvwCnt: 2, + totRvwAvg: 4.5 + } +}); + +// ์ƒํ’ˆ๋ณ„ ์œ ์ € ๋ฆฌ๋ทฐ ๋ฆฌ์ŠคํŠธ ์กฐํšŒ : IF-LGSP-0002 +export const getUserReviews = (requestParams) => (dispatch, getState) => { + const { prdtId } = requestParams; + + console.log("[UserReviews] ๐Ÿš€ getUserReviews ์•ก์…˜ ์‹œ์ž‘:", { + requestParams, + originalPrdtId: prdtId, + willUseRandomPrdtId: true, // ์ž„์‹œ ํ…Œ์ŠคํŠธ ํ”Œ๋ž˜๊ทธ + timestamp: new Date().toISOString() + }); + + // ==================== [์ž„์‹œ ํ…Œ์ŠคํŠธ] ์‹œ์ž‘ ==================== + // ํ…Œ์ŠคํŠธ์šฉ prdtId ๋ชฉ๋ก - ์ œ๊ฑฐ ์‹œ ์ด ๋ธ”๋ก ์ „์ฒด ์‚ญ์ œ + const testProductIds = [ + "LCE3010SB", + "100QNED85AU", + "14Z90Q-K.ARW3U1", + "16Z90Q-K.AAC7U1", + "24GN600-B", + "50UT8000AUA", + "A949KTMS", + "AGF76631064", + "C5323B0", + "DLE3600V" + ]; + const randomIndex = Math.floor(Math.random() * testProductIds.length); + const randomPrdtId = testProductIds[randomIndex]; + // ==================== [์ž„์‹œ ํ…Œ์ŠคํŠธ] ๋ ==================== + + // TAxios ํŒŒ๋ผ๋ฏธํ„ฐ ์ค€๋น„ + const params = { prdtId: randomPrdtId }; // ์ž„์‹œ: randomPrdtId ์‚ฌ์šฉ, ์›๋ณธ: prdtId ์‚ฌ์šฉ + const body = {}; // GET์ด๋ฏ€๋กœ ๋นˆ ๊ฐ์ฒด + + console.log("[UserReviews] ๐Ÿ“ก TAxios ํ˜ธ์ถœ ์ค€๋น„:", { + method: "get", + url: URLS.GET_USER_REVEIW, + params, + body, + selectedRandomPrdtId: randomPrdtId, // ์ž„์‹œ: ์„ ํƒ๋œ ๋žœ๋ค ์ƒํ’ˆ ID + }); const onSuccess = (response) => { - console.log("getProductOption onSuccess", response.data); - - dispatch({ - type: types.GET_PRODUCT_OPTION, - payload: response.data.data, + console.log("[UserReviews] โœ… API ์„ฑ๊ณต ์‘๋‹ต:", { + status: response.status, + statusText: response.statusText, + headers: response.headers, + retCode: response.data && response.data.retCode, + retMsg: response.data && response.data.retMsg, + hasData: !!(response.data && response.data.data), + fullResponse: response.data }); + + if (response.data && response.data.data) { + console.log("[UserReviews] ๐Ÿ“Š API ๋ฐ์ดํ„ฐ ์ƒ์„ธ:", { + reviewListLength: response.data.data.reviewList ? response.data.data.reviewList.length : 0, + reviewDetail: response.data.data.reviewDetail, + reviewList_sample: response.data.data.reviewList && response.data.data.reviewList[0] || "empty", + totRvwCnt: response.data.data.reviewDetail && response.data.data.reviewDetail.totRvwCnt, + totRvwAvg: response.data.data.reviewDetail && response.data.data.reviewDetail.totRvwAvg + }); + } + + // ์‹ค์ œ API ์‘๋‹ต์—์„œ data ๋ถ€๋ถ„ ์ถ”์ถœ + const apiData = extractReviewApiData(response.data); + + if (apiData) { + console.log("[UserReviews] โœ… ์‹ค์ œ API ๋ฐ์ดํ„ฐ ์‚ฌ์šฉ"); + dispatch({ + type: types.GET_USER_REVIEW, + payload: apiData + }); + console.log("[UserReviews] ๐Ÿ“ฆ ์‹ค์ œ API ๋ฐ์ดํ„ฐ ๋””์ŠคํŒจ์น˜ ์™„๋ฃŒ:", apiData); + } else { + console.log("[UserReviews] โš ๏ธ API ๋ฐ์ดํ„ฐ ์ถ”์ถœ ์‹คํŒจ, Mock ๋ฐ์ดํ„ฐ ์‚ฌ์šฉ"); + const mockData = createMockReviewData(); + dispatch({ + type: types.GET_USER_REVIEW, + payload: mockData + }); + console.log("[UserReviews] ๐Ÿ“ฆ Mock ๋ฐ์ดํ„ฐ ๋””์ŠคํŒจ์น˜ ์™„๋ฃŒ:", mockData); + } }; const onFail = (error) => { - console.error("getProductOption onFail", error); + console.error("[UserReviews] โŒ API ์‹คํŒจ:", { + message: error.message, + status: error.response && error.response.status, + statusText: error.response && error.response.statusText, + responseData: error.response && error.response.data, + requestParams: requestParams, + url: URLS.GET_USER_REVEIW, + fullError: error + }); + + console.log("[UserReviews] ๐Ÿ”„ API ์‹คํŒจ๋กœ Mock ๋ฐ์ดํ„ฐ ์‚ฌ์šฉ"); + const mockData = createMockReviewData(); + dispatch({ + type: types.GET_USER_REVIEW, + payload: mockData + }); + console.log("[UserReviews] ๐Ÿ“ฆ Mock ๋ฐ์ดํ„ฐ ๋””์ŠคํŒจ์น˜ ์™„๋ฃŒ (API ์‹คํŒจ):", mockData); }; + console.log("[UserReviews] ๐Ÿ”— TAxios ํ˜ธ์ถœ ์‹คํ–‰ ์ค‘..."); TAxios( dispatch, getState, "get", - URLS.GET_PRODUCT_OPTION, - { patnrId, prdtId }, - {}, + URLS.GET_USER_REVEIW, + params, + body, onSuccess, onFail ); }; + export const getProductOptionId = (id) => (dispatch) => { dispatch({ type: types.GET_PRODUCT_OPTION_ID, payload: id }); }; diff --git a/com.twin.app.shoptime/src/api/apiConfig.js b/com.twin.app.shoptime/src/api/apiConfig.js index 1eaf60a5..6b6313e6 100644 --- a/com.twin.app.shoptime/src/api/apiConfig.js +++ b/com.twin.app.shoptime/src/api/apiConfig.js @@ -52,6 +52,7 @@ export const URLS = { GET_PRODUCT_BESTSELLER: "/lgsp/v1/product/bestSeller.lge", GET_PRODUCT_GROUP: "/lgsp/v1/product/group.lge", GET_PRODUCT_OPTION: "/lgsp/v1/product/option.lge", + GET_USER_REVEIW: "/lgsp/v1/product/reviews.lge", //my-page controller GET_MY_RECOMMANDED_KEYWORD: "/lgsp/v1/mypage/reckeyword.lge", diff --git a/com.twin.app.shoptime/src/reducers/productReducer.js b/com.twin.app.shoptime/src/reducers/productReducer.js index 09f30f66..a32edfe7 100644 --- a/com.twin.app.shoptime/src/reducers/productReducer.js +++ b/com.twin.app.shoptime/src/reducers/productReducer.js @@ -1,49 +1,68 @@ import { types } from "../actions/actionTypes"; +import { curry, get, set } from "../utils/fp"; const initialState = { bestSellerData: {}, productImageLength: 0, prdtOptInfo: {}, + reviewData: null, // ๋ฆฌ๋ทฐ ๋ฐ์ดํ„ฐ ์ถ”๊ฐ€ }; -export const productReducer = (state = initialState, action) => { - switch (action.type) { - case types.GET_BEST_SELLER: - return { - ...state, - bestSellerData: action.payload, - }; - case types.GET_PRODUCT_OPTION: - return { - ...state, - prdtOptInfo: action.payload.prdtOptInfo, - }; - case types.GET_PRODUCT_GROUP: - return { - ...state, - groupInfo: action.payload.groupInfo, - }; - case types.GET_PRODUCT_IMAGE_LENGTH: - return { - ...state, - productImageLength: action.payload + 1, - }; - case types.GET_VIDEO_INDECATOR_FOCUS: - return { - ...state, - videoIndicatorFocus: action.payload, - }; - case types.CLEAR_PRODUCT_DETAIL: - return { - ...state, - prdtOptInfo: null, - }; - case types.GET_PRODUCT_OPTION_ID: - return { - ...state, - prodOptCdCval: action.payload, - }; - default: - return state; - } +// FP: handlers map (curried), pure and immutable updates only +const handleBestSeller = curry((state, action) => + set("bestSellerData", get("payload", action), state) +); + +const handleProductOption = curry((state, action) => + set("prdtOptInfo", get(["payload", "prdtOptInfo"], action), state) +); + +const handleProductGroup = curry((state, action) => + set("groupInfo", get(["payload", "groupInfo"], action), state) +); + +const handleProductImageLength = curry((state, action) => + set( + "productImageLength", + (get("payload", action) ? get("payload", action) : 0) + 1, + state + ) +); + +const handleVideoIndicatorFocus = curry((state, action) => + set("videoIndicatorFocus", get("payload", action), state) +); + +const handleClearProductDetail = curry((state) => set("prdtOptInfo", null, state)); + +const handleProductOptionId = curry((state, action) => + set("prodOptCdCval", get("payload", action), state) +); + +// ์œ ์ € ๋ฆฌ๋ทฐ ๋ฐ์ดํ„ฐ ํ•ธ๋“ค๋Ÿฌ ์ถ”๊ฐ€ +const handleUserReview = curry((state, action) => { + const reviewData = get("payload", action); + console.log("[UserReviews] Reducer - Storing review data:", { + hasData: !!reviewData, + reviewListLength: reviewData && reviewData.reviewList ? reviewData.reviewList.length : 0, + totalCount: reviewData && reviewData.reviewDetail ? reviewData.reviewDetail.totRvwCnt : 0 + }); + return set("reviewData", reviewData, state); +}); + +const handlers = { + [types.GET_BEST_SELLER]: handleBestSeller, + [types.GET_PRODUCT_OPTION]: handleProductOption, + [types.GET_PRODUCT_GROUP]: handleProductGroup, + [types.GET_PRODUCT_IMAGE_LENGTH]: handleProductImageLength, + [types.GET_VIDEO_INDECATOR_FOCUS]: handleVideoIndicatorFocus, + [types.CLEAR_PRODUCT_DETAIL]: handleClearProductDetail, + [types.GET_PRODUCT_OPTION_ID]: handleProductOptionId, + [types.GET_USER_REVIEW]: handleUserReview, // GET_USER_REVIEW ํ•ธ๋“ค๋Ÿฌ ์ถ”๊ฐ€ +}; + +export const productReducer = (state = initialState, action = {}) => { + const type = get("type", action); + const handler = handlers[type]; + return handler ? handler(state, action) : state; }; diff --git a/com.twin.app.shoptime/src/utils/fp.js b/com.twin.app.shoptime/src/utils/fp.js index 569e44b3..fe45aff2 100644 --- a/com.twin.app.shoptime/src/utils/fp.js +++ b/com.twin.app.shoptime/src/utils/fp.js @@ -1,12 +1,68 @@ +// src/utils/fp.js // FP bootstrap: use locally-extended lodash instance // './lodash' already mixes in our custom extensions from './lodashFpEx' import fp from './lodash'; -export const { - pipe, flow, curry, compose, - map, filter, reduce, get, set, - isEmpty, isNotEmpty, isNil, isNotNil, - mapAsync, reduceAsync, filterAsync, -} = fp; +// ๐Ÿ”ฝ [FIX] fp๊ฐ€ undefined์ผ ๊ฒฝ์šฐ๋ฅผ ๋Œ€๋น„ํ•œ ๊ธฐ๋ณธ๊ฐ’ ์ œ๊ณต +const safeFp = fp || {}; -export default fp; +export const { + // ๊ธฐ๋ณธ ํ•จ์ˆ˜ ์กฐํ•ฉ + pipe, flow, curry, compose, + + // ๊ธฐ๋ณธ ์ปฌ๋ ‰์…˜ ํ•จ์ˆ˜๋“ค + map, filter, reduce, get, set, + + // ๊ธฐ๋ณธ ํƒ€์ž… ์ฒดํฌ ํ•จ์ˆ˜๋“ค + isEmpty, isNotEmpty, isNil, isNotNil, + + // ๋น„๋™๊ธฐ ํ•จ์ˆ˜๋“ค + mapAsync, reduceAsync, filterAsync, findAsync, forEachAsync, + + // Promise ๊ด€๋ จ + promisify, then, andThen, otherwise, catch: catchFn, finally: finallyFn, + isPromise, + + // ์กฐ๊ฑด๋ถ€ ์‹คํ–‰ ํ•จ์ˆ˜๋“ค + when, unless, ifElse, ifT, ifF, ternary, + + // ๋””๋ฒ„๊น… ๋ฐ ์‚ฌ์ด๋“œ ์ดํŽ™ํŠธ + tap, trace, + + // ์•ˆ์ „ํ•œ ์‹คํ–‰ + tryCatch, safeGet, getOr, + + // ํƒ€์ž… ์ฒดํฌ ํ™•์žฅ + isJson, notEquals, isNotEqual, isVal, isPrimitive, isRef, isReference, + not, notIncludes, toBool, isFalsy, isTruthy, + + // ๊ฐ์ฒด ๋ณ€ํ™˜ + transformObjectKey, toCamelcase, toCamelKey, toSnakecase, toSnakeKey, + toPascalcase, pascalCase, renameKeys, + + // ๋ฐฐ์—ด ์œ ํ‹ธ๋ฆฌํ‹ฐ + mapWhen, filterWhen, removeByIndex, removeByIdx, removeLast, + append, prepend, insertAt, partition, + + // ํ•จ์ˆ˜ ์กฐํ•ฉ ์œ ํ‹ธ๋ฆฌํ‹ฐ + ap, applyTo, juxt, converge, instanceOf, + + // ๋ฌธ์ž์—ด ์œ ํ‹ธ๋ฆฌํ‹ฐ + trimToUndefined, capitalize, isDatetimeString, + + // ์ˆ˜ํ•™ ์œ ํ‹ธ๋ฆฌํ‹ฐ + clampTo, between, + + // ๊ธฐ๋ณธ๊ฐ’ ์ฒ˜๋ฆฌ + defaultTo, defaultWith, elvis, + + // ํ‚ค ๊ด€๋ จ + key, keyByVal, + mapWithKey, mapWithIdx, forEachWithKey, forEachWithIdx, + reduceWithKey, reduceWithIdx, + + // ์œ ํ‹ธ๋ฆฌํ‹ฐ + deepFreeze, times, lazy, +} = safeFp; + +export default safeFp; \ No newline at end of file diff --git a/com.twin.app.shoptime/src/utils/lodash.js b/com.twin.app.shoptime/src/utils/lodash.js index ac1389e5..d1146ffd 100644 --- a/com.twin.app.shoptime/src/utils/lodash.js +++ b/com.twin.app.shoptime/src/utils/lodash.js @@ -1,4 +1,9 @@ +// src/utils/lodash.js import fp from 'lodash/fp'; import fpEx from './lodashFpEx'; -export default fp.mixin(fpEx); +// ๐Ÿ”ฝ [FIX] lodash/fp import ์‹คํŒจ ์‹œ ๊ธฐ๋ณธ๊ฐ’ ์ œ๊ณต +const safeFp = fp || {}; +const safeFpEx = fpEx || {}; + +export default safeFp.mixin ? safeFp.mixin(safeFpEx) : safeFp; diff --git a/com.twin.app.shoptime/src/utils/lodashFpEx.js b/com.twin.app.shoptime/src/utils/lodashFpEx.js index cb136ef3..e714731a 100644 --- a/com.twin.app.shoptime/src/utils/lodashFpEx.js +++ b/com.twin.app.shoptime/src/utils/lodashFpEx.js @@ -1,3 +1,4 @@ +// src/utils/lodashFpEx.js import fp from 'lodash/fp'; /** @@ -129,6 +130,7 @@ const filterAsync = fp.curry(async (asyncFilter, arr) => { return result; }); + /** * (collection) fp.find์˜ ๋น„๋™๊ธฐ ํ•จ์ˆ˜ */ @@ -257,6 +259,7 @@ const toPascalcase = transformObjectKey(pascalCase); * @param {string} str dateํ˜•์‹ ๋ฌธ์ž์—ด */ const isDatetimeString = (str) => isNaN(str) && !isNaN(Date.parse(str)); + /** * applicative functor pattern ๊ตฌํ˜„์ฒด * (์ฃผ๋กœ fp.pipeํ•จ์ˆ˜์—์„œ ํ•จ์ˆ˜์˜ ์ธ์ž ์ˆœ์„œ๋ฅผ ๋ณ€๊ฒฝํ•˜๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉ) @@ -400,6 +403,255 @@ const getOr = (({ curry, getOr }) => { return _getOr; })(fp); +// ========== ์ƒˆ๋กœ ์ถ”๊ฐ€๋˜๋Š” ํ•จ์ˆ˜๋“ค ========== + +/** + * ์กฐ๊ฑด์ด ์ฐธ์ผ ๋•Œ๋งŒ ํ•จ์ˆ˜๋ฅผ ์‹คํ–‰, ๊ฑฐ์ง“์ด๋ฉด ์›๋ž˜ ๊ฐ’ ๋ฐ˜ํ™˜ + * @param {Function} predicate ์กฐ๊ฑด ํ•จ์ˆ˜ + * @param {Function} fn ์‹คํ–‰ํ•  ํ•จ์ˆ˜ + * @param {*} value ๋Œ€์ƒ ๊ฐ’ + */ +const when = fp.curry((predicate, fn, value) => + predicate(value) ? fn(value) : value +); + +/** + * ์กฐ๊ฑด์ด ๊ฑฐ์ง“์ผ ๋•Œ๋งŒ ํ•จ์ˆ˜๋ฅผ ์‹คํ–‰, ์ฐธ์ด๋ฉด ์›๋ž˜ ๊ฐ’ ๋ฐ˜ํ™˜ + * @param {Function} predicate ์กฐ๊ฑด ํ•จ์ˆ˜ + * @param {Function} fn ์‹คํ–‰ํ•  ํ•จ์ˆ˜ + * @param {*} value ๋Œ€์ƒ ๊ฐ’ + */ +const unless = fp.curry((predicate, fn, value) => + !predicate(value) ? fn(value) : value +); + +/** + * if-else ์กฐ๊ฑด๋ถ€ ์‹คํ–‰ + * @param {Function} predicate ์กฐ๊ฑด ํ•จ์ˆ˜ + * @param {Function} onTrue ์ฐธ์ผ ๋•Œ ์‹คํ–‰ํ•  ํ•จ์ˆ˜ + * @param {Function} onFalse ๊ฑฐ์ง“์ผ ๋•Œ ์‹คํ–‰ํ•  ํ•จ์ˆ˜ + * @param {*} value ๋Œ€์ƒ ๊ฐ’ + */ +const ifElse = fp.curry((predicate, onTrue, onFalse, value) => + predicate(value) ? onTrue(value) : onFalse(value) +); + +/** + * ๋ถ€์ˆ˜ ํšจ๊ณผ๋ฅผ ์œ„ํ•œ ํ•จ์ˆ˜ (๊ฐ’์„ ๋ณ€๊ฒฝํ•˜์ง€ ์•Š๊ณ  ํ•จ์ˆ˜ ์‹คํ–‰) + * @param {Function} fn ์‹คํ–‰ํ•  ํ•จ์ˆ˜ + * @param {*} value ๋Œ€์ƒ ๊ฐ’ + */ +const tap = fp.curry((fn, value) => { + fn(value); + return value; +}); + +/** + * ๋””๋ฒ„๊น…์„ ์œ„ํ•œ trace ํ•จ์ˆ˜ + * @param {string} label ๋ผ๋ฒจ + * @param {*} value ๋Œ€์ƒ ๊ฐ’ + */ +const trace = fp.curry((label, value) => { + console.log(label, value); + return value; +}); + +/** + * try-catch๋ฅผ ํ•จ์ˆ˜ํ˜•์œผ๋กœ ์ฒ˜๋ฆฌ + * @param {Function} tryFn ์‹œ๋„ํ•  ํ•จ์ˆ˜ + * @param {Function} catchFn ์—๋Ÿฌ ์‹œ ์‹คํ–‰ํ•  ํ•จ์ˆ˜ + * @param {*} value ๋Œ€์ƒ ๊ฐ’ + */ +const tryCatch = fp.curry((tryFn, catchFn, value) => { + try { + return tryFn(value); + } catch (error) { + return catchFn(error, value); + } +}); + +/** + * ์•ˆ์ „ํ•œ get ํ•จ์ˆ˜ (์—๋Ÿฌ ์‹œ ๊ธฐ๋ณธ๊ฐ’ ๋ฐ˜ํ™˜) + * @param {string|Array} path ๊ฒฝ๋กœ + * @param {*} defaultValue ๊ธฐ๋ณธ๊ฐ’ + * @param {Object} obj ๋Œ€์ƒ ๊ฐ์ฒด + */ +const safeGet = fp.curry((path, defaultValue, obj) => { + try { + return fp.get(path, obj) ?? defaultValue; + } catch { + return defaultValue; + } +}); + +/** + * ์กฐ๊ฑด์— ๋”ฐ๋ฅธ map + * @param {Function} predicate ์กฐ๊ฑด ํ•จ์ˆ˜ + * @param {Function} fn ๋ณ€ํ™˜ ํ•จ์ˆ˜ + * @param {Array} array ๋Œ€์ƒ ๋ฐฐ์—ด + */ +const mapWhen = fp.curry((predicate, fn, array) => + array.map(item => predicate(item) ? fn(item) : item) +); + +/** + * ์กฐ๊ฑด์— ๋”ฐ๋ฅธ filter + * @param {boolean} condition ์กฐ๊ฑด + * @param {Function} predicate ํ•„ํ„ฐ ํ•จ์ˆ˜ + * @param {Array} array ๋Œ€์ƒ ๋ฐฐ์—ด + */ +const filterWhen = fp.curry((condition, predicate, array) => + condition ? array.filter(predicate) : array +); + +/** + * ๊ฐ์ฒด ํ‚ค ์ด๋ฆ„ ๋ณ€๊ฒฝ + * @param {Object} keyMap ํ‚ค ๋งคํ•‘ ๊ฐ์ฒด + * @param {Object} obj ๋Œ€์ƒ ๊ฐ์ฒด + */ +const renameKeys = fp.curry((keyMap, obj) => + fp.reduce((acc, [oldKey, newKey]) => { + if (fp.has(oldKey, obj)) { + acc[newKey] = obj[oldKey]; + } + return acc; + }, {}, fp.toPairs(keyMap)) +); + +/** + * ๋ฐฐ์—ด์—์„œ ํŠน์ • ์ธ๋ฑ์Šค์— ์š”์†Œ ์‚ฝ์ž… + * @param {number} index ์‚ฝ์ž…ํ•  ์ธ๋ฑ์Šค + * @param {*} item ์‚ฝ์ž…ํ•  ์š”์†Œ + * @param {Array} array ๋Œ€์ƒ ๋ฐฐ์—ด + */ +const insertAt = fp.curry((index, item, array) => [ + ...array.slice(0, index), + item, + ...array.slice(index) +]); + +/** + * ๊ฐ’์„ ์ฒซ ๋ฒˆ์งธ ์ธ์ž๋กœ ๋ฐ›๋Š” applicative + * @param {*} value ๋Œ€์ƒ ๊ฐ’ + * @param {Function} fn ์‹คํ–‰ํ•  ํ•จ์ˆ˜ + */ +const applyTo = fp.curry((value, fn) => fn(value)); + +/** + * ์—ฌ๋Ÿฌ ํ•จ์ˆ˜๋ฅผ ํ•˜๋‚˜์˜ ๊ฐ’์— ์ ์šฉํ•˜์—ฌ ๋ฐฐ์—ด๋กœ ๋ฐ˜ํ™˜ + * @param {Array} fns ํ•จ์ˆ˜ ๋ฐฐ์—ด + * @param {*} value ๋Œ€์ƒ ๊ฐ’ + */ +const juxt = fp.curry((fns, value) => fns.map(fn => fn(value))); + +/** + * ์—ฌ๋Ÿฌ ํ•จ์ˆ˜์˜ ๊ฒฐ๊ณผ๋ฅผ converge ํ•จ์ˆ˜๋กœ ์กฐํ•ฉ + * @param {Function} convergeFn ์กฐํ•ฉ ํ•จ์ˆ˜ + * @param {Array} fns ํ•จ์ˆ˜ ๋ฐฐ์—ด + * @param {*} value ๋Œ€์ƒ ๊ฐ’ + */ +const converge = fp.curry((convergeFn, fns, value) => + convergeFn(...fns.map(fn => fn(value))) +); + +/** + * ๋ฌธ์ž์—ด์„ trimํ•˜๊ณ  ๋นˆ ๋ฌธ์ž์—ด์ด๋ฉด undefined ๋ฐ˜ํ™˜ + * @param {string} str ๋Œ€์ƒ ๋ฌธ์ž์—ด + */ +const trimToUndefined = (str) => { + const trimmed = fp.trim(str); + return trimmed === '' ? undefined : trimmed; +}; + +/** + * ์ฒซ ๊ธ€์ž ๋Œ€๋ฌธ์ž, ๋‚˜๋จธ์ง€ ์†Œ๋ฌธ์ž + * @param {string} str ๋Œ€์ƒ ๋ฌธ์ž์—ด + */ +const capitalize = (str) => + str.charAt(0).toUpperCase() + str.slice(1).toLowerCase(); + +/** + * ๊ฐ’์„ min๊ณผ max ์‚ฌ์ด๋กœ ์ œํ•œ + * @param {number} min ์ตœ์†Ÿ๊ฐ’ + * @param {number} max ์ตœ๋Œ“๊ฐ’ + * @param {number} value ๋Œ€์ƒ ๊ฐ’ + */ +const clampTo = fp.curry((min, max, value) => + Math.min(Math.max(value, min), max) +); + +/** + * ๊ฐ’์ด min๊ณผ max ์‚ฌ์ด์— ์žˆ๋Š”์ง€ ํ™•์ธ + * @param {number} min ์ตœ์†Ÿ๊ฐ’ + * @param {number} max ์ตœ๋Œ“๊ฐ’ + * @param {number} value ๋Œ€์ƒ ๊ฐ’ + */ +const between = fp.curry((min, max, value) => + value >= min && value <= max +); + +/** + * null/undefined์ผ ๋•Œ ๊ธฐ๋ณธ๊ฐ’ ๋ฐ˜ํ™˜ + * @param {*} defaultValue ๊ธฐ๋ณธ๊ฐ’ + * @param {*} value ๋Œ€์ƒ ๊ฐ’ + */ +const defaultTo = fp.curry((defaultValue, value) => + value == null ? defaultValue : value +); + +/** + * Elvis ์—ฐ์‚ฐ์ž ๊ตฌํ˜„ (null safe ํ•จ์ˆ˜ ์ ์šฉ) + * @param {Function} fn ์ ์šฉํ•  ํ•จ์ˆ˜ + * @param {*} value ๋Œ€์ƒ ๊ฐ’ + */ +const elvis = fp.curry((fn, value) => + value == null ? undefined : fn(value) +); + +/** + * ๋ฐฐ์—ด์„ ์กฐ๊ฑด์— ๋”ฐ๋ผ ๋‘ ๊ทธ๋ฃน์œผ๋กœ ๋ถ„ํ•  + * @param {Function} predicate ์กฐ๊ฑด ํ•จ์ˆ˜ + * @param {Array} array ๋Œ€์ƒ ๋ฐฐ์—ด + */ +const partition = fp.curry((predicate, array) => [ + array.filter(predicate), + array.filter(item => !predicate(item)) +]); + +/** + * n๋ฒˆ ํ•จ์ˆ˜๋ฅผ ์‹คํ–‰ํ•˜์—ฌ ๊ฒฐ๊ณผ ๋ฐฐ์—ด ์ƒ์„ฑ + * @param {Function} fn ์‹คํ–‰ํ•  ํ•จ์ˆ˜ + * @param {number} n ์‹คํ–‰ ํšŸ์ˆ˜ + */ +const times = fp.curry((fn, n) => + Array.from({ length: n }, (_, i) => fn(i)) +); + +/** + * ์ง€์—ฐ ํ‰๊ฐ€ ํ•จ์ˆ˜ (memoization๊ณผ ์œ ์‚ฌ) + * @param {Function} fn ์ง€์—ฐ ์‹คํ–‰ํ•  ํ•จ์ˆ˜ + */ +const lazy = (fn) => { + let cached = false; + let result; + return (...args) => { + if (!cached) { + result = fn(...args); + cached = true; + } + return result; + }; +}; + +/** + * ํ•จ์ˆ˜๊ฐ€ ์•„๋‹Œ ๊ฐ’๋„ ๋ฐ›์„ ์ˆ˜ ์žˆ๋Š” ๋ฒ”์šฉ ๊ธฐ๋ณธ๊ฐ’ ํ•จ์ˆ˜ + * @param {*} defaultValue ๊ธฐ๋ณธ๊ฐ’ (ํ•จ์ˆ˜ ๋˜๋Š” ๊ฐ’) + * @param {*} value ๋Œ€์ƒ ๊ฐ’ + */ +const defaultWith = fp.curry((defaultValue, value) => + value == null ? (fp.isFunction(defaultValue) ? defaultValue() : defaultValue) : value +); + export default { mapAsync, filterAsync, @@ -465,4 +717,30 @@ export default { isTruthy, getOr, -}; + + // ์ƒˆ๋กœ ์ถ”๊ฐ€๋œ ํ•จ์ˆ˜๋“ค + when, + unless, + ifElse, + tap, + trace, + tryCatch, + safeGet, + mapWhen, + filterWhen, + renameKeys, + insertAt, + applyTo, + juxt, + converge, + trimToUndefined, + capitalize, + clampTo, + between, + defaultTo, + defaultWith, + elvis, + partition, + times, + lazy, +}; \ No newline at end of file diff --git a/com.twin.app.shoptime/src/views/DetailPanel/DetailPanel.backup.jsx b/com.twin.app.shoptime/src/views/DetailPanel/DetailPanel.backup.jsx new file mode 100644 index 00000000..bb453935 --- /dev/null +++ b/com.twin.app.shoptime/src/views/DetailPanel/DetailPanel.backup.jsx @@ -0,0 +1,550 @@ +import React, { + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from "react"; + +import { useDispatch, useSelector } from "react-redux"; + +import Spotlight from "@enact/spotlight"; +import { setContainerLastFocusedElement } from "@enact/spotlight/src/container"; + +import { + changeAppStatus, + changeLocalSettings, + setHidePopup, +} from "../../actions/commonActions"; +import { clearCouponInfo } from "../../actions/couponActions"; +import { getDeviceAdditionInfo } from "../../actions/deviceActions"; +import { + clearThemeDetail, + getThemeCurationDetailInfo, + getThemeHotelDetailInfo, +} from "../../actions/homeActions"; +import { + getMainCategoryDetail, + getMainYouMayLike, +} from "../../actions/mainActions"; +import { popPanel, updatePanel } from "../../actions/panelActions"; +import { finishVideoPreview } from "../../actions/playActions"; +import { + clearProductDetail, + getProductGroup, + getProductImageLength, + getProductOptionId, +} from "../../actions/productActions"; +import MobileSendPopUp from "../../components/MobileSend/MobileSendPopUp"; +import TBody from "../../components/TBody/TBody"; +import THeader from "../../components/THeader/THeader"; +import TPanel from "../../components/TPanel/TPanel"; +import * as Config from "../../utils/Config"; +import { panel_names } from "../../utils/Config"; +import { $L, getQRCodeUrl } from "../../utils/helperMethods"; +import css from "./DetailPanel.module.less"; +import GroupProduct from "./GroupProduct/GroupProduct"; +import SingleProduct from "./SingleProduct/SingleProduct"; +import ThemeProduct from "./ThemeProduct/ThemeProduct"; +import UnableProduct from "./UnableProduct/UnableProduct"; +import YouMayLike from "./YouMayLike/YouMayLike"; + +export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) { + const dispatch = useDispatch(); + + const productData = useSelector((state) => state.main.productData); + const themeData = useSelector((state) => state.home.productData); + const hotelData = useSelector((state) => state.home.hotelData); + const isLoading = useSelector( + (state) => state.common.appStatus?.showLoadingPanel?.show + ); + const themeProductInfos = useSelector( + (state) => state.home.themeCurationDetailInfoData + ); + const hotelInfos = useSelector( + (state) => state.home.themeCurationHotelDetailData + ); + const localRecentItems = useSelector( + (state) => state.localSettings?.recentItems + ); + const youmaylikeData = useSelector((state) => state.main.youmaylikeData); + const { httpHeader } = useSelector((state) => state.common); + const deviceInfo = useSelector((state) => state.device.deviceInfo); + + const serverHOST = useSelector((state) => state.common.appStatus.serverHOST); + const serverType = useSelector((state) => state.localSettings.serverType); + const { entryMenu, nowMenu } = useSelector((state) => state.common.menu); + const groupInfos = useSelector((state) => state.product.groupInfo); + const productInfo = useSelector((state) => state.main.productData); + const { popupVisible, activePopup } = useSelector( + (state) => state.common.popup + ); + const webOSVersion = useSelector( + (state) => state.common.appStatus.webOSVersion + ); + + const panels = useSelector((state) => state.panels.panels); + const [lgCatCd, setLgCatCd] = useState(""); + const [isYouMayLikeOpened, setIsYouMayLikeOpened] = useState(false); + const [selectedIndex, setSelectedIndex] = useState(0); + + const shopByMobileLogRef = useRef(null); + + useEffect(() => { + dispatch(getProductOptionId(undefined)); + if (panelInfo?.type === "hotel") { + dispatch( + getThemeHotelDetailInfo({ + patnrId: panelInfo?.patnrId, + curationId: panelInfo?.curationId, + }) + ); + } + + if (panelInfo?.type === "theme") { + dispatch( + getThemeCurationDetailInfo({ + patnrId: panelInfo?.patnrId, + curationId: panelInfo?.curationId, + bgImgNo: panelInfo?.bgImgNo, + }) + ); + } + + if (panelInfo?.prdtId && !panelInfo?.curationId) { + dispatch( + getMainCategoryDetail({ + patnrId: panelInfo?.patnrId, + prdtId: panelInfo?.prdtId, + liveReqFlag: panelInfo?.liveReqFlag ? panelInfo?.liveReqFlag : "N", + }) + ); + } + dispatch(getDeviceAdditionInfo()); + }, [ + dispatch, + panelInfo.liveReqFlag, + panelInfo.curationId, + panelInfo.prdtId, + panelInfo.type, + panelInfo.patnrId, + ]); + + useEffect(() => { + if (lgCatCd) { + dispatch( + getMainYouMayLike({ + lgCatCd: lgCatCd, + exclCurationId: panelInfo?.curationId, + exclPatnrId: panelInfo?.patnrId, + exclPrdtId: panelInfo?.prdtId, + }) + ); + } + }, [panelInfo?.curationId, panelInfo?.patnrId, panelInfo?.prdtId, lgCatCd]); + + useEffect(() => { + if (productData?.pmtSuptYn === "Y" && productData?.grPrdtProcYn === "Y") { + dispatch( + getProductGroup({ + patnrId: panelInfo?.patnrId, + prdtId: panelInfo?.prdtId, + }) + ); + } + }, [productData]); + + useEffect(() => { + if ( + themeProductInfos && + themeProductInfos.length > 0 && + panelInfo?.themePrdtId + ) { + for (let i = 0; i < themeProductInfos.length; i++) { + if (themeProductInfos[i].prdtId === panelInfo?.themePrdtId) { + setSelectedIndex(i); + } + } + } + + if (hotelInfos && hotelInfos.length > 0 && panelInfo?.themeHotelId) { + for (let i = 0; i < hotelInfos.length; i++) { + if (hotelInfos[i].hotelId === panelInfo?.themeHotelId) { + setSelectedIndex(i); + } + } + } + }, [ + themeProductInfos, + hotelInfos, + panelInfo?.themePrdtId, + panelInfo?.themeHotelId, + ]); + + const { detailUrl } = useMemo(() => { + return getQRCodeUrl({ + serverHOST, + serverType, + index: deviceInfo?.dvcIndex, + patnrId: productInfo?.patnrId, + prdtId: productInfo?.prdtId, + entryMenu: entryMenu, + nowMenu: nowMenu, + liveFlag: "Y", + qrType: "billingDetail", + }); + }, [serverHOST, serverType, deviceInfo, entryMenu, nowMenu, productInfo]); + + const onSpotlightUpTButton = (e) => { + e.stopPropagation(); + Spotlight.focus("spotlightId_backBtn"); + }; + + const onClick = useCallback( + (isCancelClick) => (ev) => { + dispatch(finishVideoPreview()); + dispatch(popPanel(panel_names.DETAIL_PANEL)); + + if (panels.length === 4 && panels[1]?.name === panel_names.PLAYER_PANEL) { + dispatch( + updatePanel({ + name: panel_names.PLAYER_PANEL, + panelInfo: { + thumbnail: panelInfo.thumbnailUrl, + }, + }) + ); + } + + if (isCancelClick) { + ev.stopPropagation(); + } + }, + [dispatch, panelInfo, panels] + ); + + const handleSMSonClose = useCallback(() => { + dispatch(setHidePopup()); + setTimeout(() => { + Spotlight.focus("spotlightId_backBtn"); + Spotlight.focus("shopbymobile_Btn"); + }, 0); + }, [dispatch]); + + const saveToLocalSettings = useCallback(() => { + let recentItems = []; + const prdtId = + themeProductInfos && + themeProductInfos.length > 0 && + panelInfo?.type === "theme" + ? themeProductInfos[selectedIndex].prdtId + : panelInfo?.prdtId; + + if (localRecentItems) { + recentItems = [...localRecentItems]; + } + + const currentDate = new Date(); + + const formattedDate = `${ + currentDate.getMonth() + 1 + }/${currentDate.getDate()}`; + + const existingProductIndex = recentItems.findIndex((item) => { + return item.prdtId === prdtId; + }); + + if (existingProductIndex !== -1) { + recentItems.splice(existingProductIndex, 1); + } + + recentItems.push({ + prdtId: prdtId, + patnrId: panelInfo?.patnrId, + date: formattedDate, + expireTime: currentDate.getTime() + 1000 * 60 * 60 * 24 * 14, + cntryCd: httpHeader["X-Device-Country"], + }); + + if (recentItems.length >= 51) { + const data = [...recentItems]; + dispatch(changeLocalSettings({ recentItems: data.slice(1) })); + } else { + dispatch(changeLocalSettings({ recentItems })); + } + }, [ + panelInfo?.prdtId, + httpHeader, + localRecentItems, + dispatch, + selectedIndex, + themeProductInfos, + ]); + + const getlgCatCd = useCallback(() => { + if (productData && !panelInfo?.curationId) { + setLgCatCd(productData.catCd); + } else if ( + themeProductInfos && + themeProductInfos[selectedIndex]?.pmtSuptYn === "N" && + panelInfo?.curationId + ) { + setLgCatCd(themeProductInfos[selectedIndex]?.catCd); + } else { + setLgCatCd(""); + } + }, [productData, themeProductInfos, selectedIndex, panelInfo?.curationId]); + + const mobileSendPopUpProductImg = useMemo(() => { + if (panelInfo?.type === "theme" && themeProductInfos) { + return themeProductInfos[selectedIndex]?.imgUrls600[0]; + } else if (panelInfo?.type === "hotel" && hotelInfos) { + return hotelInfos[selectedIndex]?.hotelImgUrl; + } else { + return productData?.imgUrls600[0]; + } + }, [ + themeProductInfos, + hotelInfos, + selectedIndex, + productData, + panelInfo?.type, + ]); + + const mobileSendPopUpSubtitle = useMemo(() => { + if (panelInfo?.type === "theme" && themeProductInfos) { + return themeProductInfos[selectedIndex]?.prdtNm; + } else if (panelInfo?.type === "hotel" && hotelInfos) { + return hotelInfos[selectedIndex]?.hotelNm; + } else { + return productData?.prdtNm; + } + }, [ + themeProductInfos, + hotelInfos, + selectedIndex, + productData, + panelInfo?.type, + ]); + + const isBillingProductVisible = useMemo(() => { + return ( + productData?.pmtSuptYn === "Y" && + productData?.grPrdtProcYn === "N" && + panelInfo?.prdtId && + webOSVersion >= "6.0" + ); + }, [productData, webOSVersion, panelInfo?.prdtId]); + + const isUnavailableProductVisible = useMemo(() => { + return ( + productData?.pmtSuptYn === "N" || + (productData?.pmtSuptYn === "Y" && + productData?.grPrdtProcYn === "N" && + webOSVersion < "6.0" && + panelInfo?.prdtId) + ); + }, [productData, webOSVersion, panelInfo?.prdtId]); + + const isGroupProductVisible = useMemo(() => { + return ( + productData?.pmtSuptYn === "Y" && + productData?.grPrdtProcYn === "Y" && + groupInfos && + groupInfos.length > 0 + ); + }, [productData, groupInfos]); + + const isTravelProductVisible = useMemo(() => { + return panelInfo?.curationId && (hotelInfos || themeData); + }, [panelInfo?.curationId, hotelInfos, themeData]); + + const Price = () => { + return ( + <> + {hotelInfos[selectedIndex]?.hotelDetailInfo.currencySign} + {hotelInfos[selectedIndex]?.hotelDetailInfo.price} + + ); + }; + + useEffect(() => { + getlgCatCd(); + }, [themeProductInfos, productData, panelInfo, selectedIndex]); + + useEffect(() => { + if (panelInfo && panelInfo?.patnrId && panelInfo?.prdtId) { + saveToLocalSettings(); + } + }, [panelInfo, selectedIndex]); + + useEffect(() => { + if ( + ((themeProductInfos && themeProductInfos.length > 0) || + (hotelInfos && hotelInfos.length > 0)) && + selectedIndex !== undefined + ) { + if (panelInfo?.type === "theme") { + const imgUrls600 = themeProductInfos[selectedIndex]?.imgUrls600 || []; + dispatch(getProductImageLength({ imageLength: imgUrls600.length })); + return; + } + if (panelInfo?.type === "hotel") { + const imgUrls600 = hotelInfos[selectedIndex]?.imgUrls600 || []; + dispatch(getProductImageLength({ imageLength: imgUrls600.length })); + } + } + }, [dispatch, themeProductInfos, selectedIndex, hotelInfos]); + + useEffect(() => { + return () => { + dispatch(clearProductDetail()); + dispatch(clearThemeDetail()); + dispatch(clearCouponInfo()); + setContainerLastFocusedElement(null, ["indicator-GridListContainer"]); + }; + }, [dispatch]); + + return ( + <> + + + + {!isLoading && ( + <> + {/* ๊ฒฐ์ œ๊ฐ€๋Šฅ์ƒํ’ˆ ์˜์—ญ */} + {isBillingProductVisible && ( + + )} + {/* ๊ตฌ๋งค๋ถˆ๊ฐ€์ƒํ’ˆ ์˜์—ญ */} + {isUnavailableProductVisible && ( + + )} + {/* ๊ทธ๋ฃน์ƒํ’ˆ ์˜์—ญ */} + {isGroupProductVisible && ( + + )} + {/* ํ…Œ๋งˆ๊ทธ๋ฃน์ƒํ’ˆ ์˜์—ญ*/} + {isTravelProductVisible && ( + + )} + + )} + + + {lgCatCd && youmaylikeData && youmaylikeData.length > 0 && isOnTop && ( + + )} + {activePopup === Config.ACTIVE_POPUP.smsPopup && ( + + )} + + ); +} diff --git a/com.twin.app.shoptime/src/views/DetailPanel/DetailPanel.backup.module.less b/com.twin.app.shoptime/src/views/DetailPanel/DetailPanel.backup.module.less new file mode 100644 index 00000000..ca94a989 --- /dev/null +++ b/com.twin.app.shoptime/src/views/DetailPanel/DetailPanel.backup.module.less @@ -0,0 +1,41 @@ +@import "../../style/CommonStyle.module.less"; +@import "../../style/utils.module.less"; + +.detailPanelWrap { + background: @COLOR_WHITE; +} + +.header { + > div { + font-weight: bold !important; + font-size: 30px !important; + .elip(@clamp: 1); + padding-left: 12px !important; + text-transform: none !important; + letter-spacing: 0 !important; + } + display: flex; + width: 100%; + height: 90px; + background-color: #f2f2f2; + align-items: center; + color: #333333; + padding: 0 0 0 60px; + position: relative; + + // > button { + // &:focus { + // &::after { + // .focused(@boxShadow: 22px, @borderRadius:0px); + // } + // } + // } +} + +.tbody { + position: relative; + display: flex; + justify-content: space-between; + + padding-left: 120px; +} diff --git a/com.twin.app.shoptime/src/views/DetailPanel/DetailPanel.jsx b/com.twin.app.shoptime/src/views/DetailPanel/DetailPanel.jsx index bb453935..7a427615 100644 --- a/com.twin.app.shoptime/src/views/DetailPanel/DetailPanel.jsx +++ b/com.twin.app.shoptime/src/views/DetailPanel/DetailPanel.jsx @@ -1,6 +1,8 @@ +// src/views/DetailPanel/DetailPanel.new.jsx import React, { useCallback, useEffect, + useLayoutEffect, useMemo, useRef, useState, @@ -11,18 +13,8 @@ import { useDispatch, useSelector } from "react-redux"; import Spotlight from "@enact/spotlight"; import { setContainerLastFocusedElement } from "@enact/spotlight/src/container"; -import { - changeAppStatus, - changeLocalSettings, - setHidePopup, -} from "../../actions/commonActions"; -import { clearCouponInfo } from "../../actions/couponActions"; import { getDeviceAdditionInfo } from "../../actions/deviceActions"; -import { - clearThemeDetail, - getThemeCurationDetailInfo, - getThemeHotelDetailInfo, -} from "../../actions/homeActions"; + import { getMainCategoryDetail, getMainYouMayLike, @@ -31,406 +23,566 @@ import { popPanel, updatePanel } from "../../actions/panelActions"; import { finishVideoPreview } from "../../actions/playActions"; import { clearProductDetail, - getProductGroup, - getProductImageLength, getProductOptionId, } from "../../actions/productActions"; -import MobileSendPopUp from "../../components/MobileSend/MobileSendPopUp"; + import TBody from "../../components/TBody/TBody"; -import THeader from "../../components/THeader/THeader"; +import THeaderCustom from "./components/THeaderCustom"; import TPanel from "../../components/TPanel/TPanel"; -import * as Config from "../../utils/Config"; + import { panel_names } from "../../utils/Config"; import { $L, getQRCodeUrl } from "../../utils/helperMethods"; +import fp from "../../utils/fp"; import css from "./DetailPanel.module.less"; -import GroupProduct from "./GroupProduct/GroupProduct"; -import SingleProduct from "./SingleProduct/SingleProduct"; -import ThemeProduct from "./ThemeProduct/ThemeProduct"; -import UnableProduct from "./UnableProduct/UnableProduct"; -import YouMayLike from "./YouMayLike/YouMayLike"; +import ProductAllSection from "./ProductAllSection/ProductAllSection"; +import { getThemeCurationDetailInfo } from "../../actions/homeActions"; +import indicatorDefaultImage from "../../../assets/images/img-thumb-empty-144@3x.png"; +import ThemeItemListOverlay from "./ThemeItemListOverlay/ThemeItemListOverlay"; +import Spinner from "@enact/sandstone/Spinner"; export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) { const dispatch = useDispatch(); const productData = useSelector((state) => state.main.productData); - const themeData = useSelector((state) => state.home.productData); - const hotelData = useSelector((state) => state.home.hotelData); - const isLoading = useSelector( - (state) => state.common.appStatus?.showLoadingPanel?.show + const isLoading = useSelector((state) => + fp.pipe(() => state, fp.get('common.appStatus.showLoadingPanel.show'))() ); - const themeProductInfos = useSelector( - (state) => state.home.themeCurationDetailInfoData - ); - const hotelInfos = useSelector( - (state) => state.home.themeCurationHotelDetailData - ); - const localRecentItems = useSelector( - (state) => state.localSettings?.recentItems - ); - const youmaylikeData = useSelector((state) => state.main.youmaylikeData); - const { httpHeader } = useSelector((state) => state.common); - const deviceInfo = useSelector((state) => state.device.deviceInfo); - - const serverHOST = useSelector((state) => state.common.appStatus.serverHOST); - const serverType = useSelector((state) => state.localSettings.serverType); - const { entryMenu, nowMenu } = useSelector((state) => state.common.menu); - const groupInfos = useSelector((state) => state.product.groupInfo); - const productInfo = useSelector((state) => state.main.productData); - const { popupVisible, activePopup } = useSelector( - (state) => state.common.popup + const themeData = useSelector((state) => + fp.pipe( + () => state, + fp.get('home.productData.themeInfo'), + (list) => (list && list[0]) + )() ); const webOSVersion = useSelector( - (state) => state.common.appStatus.webOSVersion + (state) => state.common.appStatus.webOSVersion, + ); + const panels = useSelector((state) => state.panels.panels); + + // FP ๋ฐฉ์‹์œผ๋กœ ์ƒํƒœ ๊ด€๋ฆฌ + const [selectedIndex, setSelectedIndex] = useState(0); + const localRecentItems = useSelector((state) => + fp.pipe(() => state, fp.get('localSettings.recentItems'))() + ); + const { httpHeader } = useSelector((state) => state.common); + const { popupVisible, activePopup } = useSelector( + (state) => state.common.popup, + ); + const [lgCatCd, setLgCatCd] = useState(""); + const [themeProductInfo, setThemeProductInfo] = useState(null); + + const containerRef = useRef(null); + + // FP ํŒŒ์ƒ ๊ฐ’ ๋ฉ”๋ชจ์ด์ œ์ด์…˜ (optional chaining ๋Œ€์ฒด ๋ฐ deps ์•ˆ์ •ํ™”) + const panelType = useMemo(() => fp.pipe(() => panelInfo, fp.get('type'))(), [panelInfo]); + const panelCurationId = useMemo(() => fp.pipe(() => panelInfo, fp.get('curationId'))(), [panelInfo]); + const panelPatnrId = useMemo(() => fp.pipe(() => panelInfo, fp.get('patnrId'))(), [panelInfo]); + const panelPrdtId = useMemo(() => fp.pipe(() => panelInfo, fp.get('prdtId'))(), [panelInfo]); + const panelLiveReqFlag = useMemo(() => fp.pipe(() => panelInfo, fp.get('liveReqFlag'))(), [panelInfo]); + const panelBgImgNo = useMemo(() => fp.pipe(() => panelInfo, fp.get('bgImgNo'))(), [panelInfo]); + const productPmtSuptYn = useMemo(() => fp.pipe(() => productData, fp.get('pmtSuptYn'))(), [productData]); + const productGrPrdtProcYn = useMemo(() => fp.pipe(() => productData, fp.get('grPrdtProcYn'))(), [productData]); + + // FP ๋ฐฉ์‹์œผ๋กœ ๋ฐ์ดํ„ฐ ์†Œ์Šค ๊ฒฐ์ • (๋ฉ”๋ชจ์ด์ œ์ด์…˜ ์ตœ์ ํ™”) + const productDataSource = useMemo(() => + fp.pipe( + () => panelType, + type => type === "theme" ? themeData : productData + )(), + [panelType, themeData, productData] ); - const panels = useSelector((state) => state.panels.panels); - const [lgCatCd, setLgCatCd] = useState(""); - const [isYouMayLikeOpened, setIsYouMayLikeOpened] = useState(false); - const [selectedIndex, setSelectedIndex] = useState(0); + const [productType, setProductType] = useState(null); + const [openThemeItemOverlay, setOpenThemeItemOverlay] = useState(false); - const shopByMobileLogRef = useRef(null); + // FP ๋ฐฉ์‹์œผ๋กœ ์Šคํฌ๋กค ์ƒํƒœ ๊ด€๋ฆฌ + const [scrollToSection, setScrollToSection] = useState(null); + const [pendingScrollSection, setPendingScrollSection] = useState(null); - useEffect(() => { - dispatch(getProductOptionId(undefined)); - if (panelInfo?.type === "hotel") { - dispatch( - getThemeHotelDetailInfo({ - patnrId: panelInfo?.patnrId, - curationId: panelInfo?.curationId, - }) - ); - } + // FP ๋ฐฉ์‹์œผ๋กœ ์ƒํƒœ ์—…๋ฐ์ดํŠธ ํ•จ์ˆ˜๋“ค + const updateSelectedIndex = useCallback((newIndex) => { + setSelectedIndex(fp.pipe( + () => newIndex, + index => Math.max(0, Math.min(index, 999)) // ๋ฒ”์œ„ ์ œํ•œ + )()); + }, []); - if (panelInfo?.type === "theme") { - dispatch( - getThemeCurationDetailInfo({ - patnrId: panelInfo?.patnrId, - curationId: panelInfo?.curationId, - bgImgNo: panelInfo?.bgImgNo, - }) - ); - } + const updateThemeItemOverlay = useCallback((isOpen) => { + setOpenThemeItemOverlay(fp.pipe( + () => isOpen, + Boolean + )()); + }, []); - if (panelInfo?.prdtId && !panelInfo?.curationId) { - dispatch( - getMainCategoryDetail({ - patnrId: panelInfo?.patnrId, - prdtId: panelInfo?.prdtId, - liveReqFlag: panelInfo?.liveReqFlag ? panelInfo?.liveReqFlag : "N", - }) - ); - } - dispatch(getDeviceAdditionInfo()); - }, [ - dispatch, - panelInfo.liveReqFlag, - panelInfo.curationId, - panelInfo.prdtId, - panelInfo.type, - panelInfo.patnrId, - ]); - - useEffect(() => { - if (lgCatCd) { - dispatch( - getMainYouMayLike({ - lgCatCd: lgCatCd, - exclCurationId: panelInfo?.curationId, - exclPatnrId: panelInfo?.patnrId, - exclPrdtId: panelInfo?.prdtId, - }) - ); - } - }, [panelInfo?.curationId, panelInfo?.patnrId, panelInfo?.prdtId, lgCatCd]); - - useEffect(() => { - if (productData?.pmtSuptYn === "Y" && productData?.grPrdtProcYn === "Y") { - dispatch( - getProductGroup({ - patnrId: panelInfo?.patnrId, - prdtId: panelInfo?.prdtId, - }) - ); - } - }, [productData]); - - useEffect(() => { - if ( - themeProductInfos && - themeProductInfos.length > 0 && - panelInfo?.themePrdtId - ) { - for (let i = 0; i < themeProductInfos.length; i++) { - if (themeProductInfos[i].prdtId === panelInfo?.themePrdtId) { - setSelectedIndex(i); - } - } - } - - if (hotelInfos && hotelInfos.length > 0 && panelInfo?.themeHotelId) { - for (let i = 0; i < hotelInfos.length; i++) { - if (hotelInfos[i].hotelId === panelInfo?.themeHotelId) { - setSelectedIndex(i); - } - } - } - }, [ - themeProductInfos, - hotelInfos, - panelInfo?.themePrdtId, - panelInfo?.themeHotelId, - ]); - - const { detailUrl } = useMemo(() => { - return getQRCodeUrl({ - serverHOST, - serverType, - index: deviceInfo?.dvcIndex, - patnrId: productInfo?.patnrId, - prdtId: productInfo?.prdtId, - entryMenu: entryMenu, - nowMenu: nowMenu, - liveFlag: "Y", - qrType: "billingDetail", - }); - }, [serverHOST, serverType, deviceInfo, entryMenu, nowMenu, productInfo]); - - const onSpotlightUpTButton = (e) => { + // FP ๋ฐฉ์‹์œผ๋กœ ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ ์ •์˜ + const onSpotlightUpTButton = useCallback((e) => { e.stopPropagation(); Spotlight.focus("spotlightId_backBtn"); - }; + }, []); const onClick = useCallback( (isCancelClick) => (ev) => { - dispatch(finishVideoPreview()); - dispatch(popPanel(panel_names.DETAIL_PANEL)); - - if (panels.length === 4 && panels[1]?.name === panel_names.PLAYER_PANEL) { - dispatch( - updatePanel({ - name: panel_names.PLAYER_PANEL, - panelInfo: { - thumbnail: panelInfo.thumbnailUrl, - }, - }) - ); - } + // FP ๋ฐฉ์‹์œผ๋กœ ์•ก์…˜ ๋””์ŠคํŒจ์น˜ ์ฒด์ด๋‹ + fp.pipe( + () => { + dispatch(finishVideoPreview()); + dispatch(popPanel(panel_names.DETAIL_PANEL)); + }, + () => { + // ํŒจ๋„ ์—…๋ฐ์ดํŠธ ์กฐ๊ฑด ์ฒดํฌ + const shouldUpdatePanel = fp.pipe( + () => panels, + fp.get('length'), + length => length === 4 + )() && fp.pipe( + () => panels, + fp.get('1.name'), + name => name === panel_names.PLAYER_PANEL + )(); + + if (shouldUpdatePanel) { + dispatch( + updatePanel({ + name: panel_names.PLAYER_PANEL, + panelInfo: { + thumbnail: fp.pipe( + () => panelInfo, + fp.get('thumbnailUrl') + )(), + }, + }), + ); + } + } + )(); if (isCancelClick) { ev.stopPropagation(); } }, - [dispatch, panelInfo, panels] + [dispatch, panelInfo, panels], ); - const handleSMSonClose = useCallback(() => { - dispatch(setHidePopup()); - setTimeout(() => { - Spotlight.focus("spotlightId_backBtn"); - Spotlight.focus("shopbymobile_Btn"); - }, 0); - }, [dispatch]); + // FP ๋ฐฉ์‹์œผ๋กœ ์Šคํฌ๋กค ํ•จ์ˆ˜ ํ•ธ๋“ค๋Ÿฌ + const handleScrollToSection = useCallback( + (sectionId) => { + console.log("DetailPanel: handleScrollToSection called with:", sectionId); + console.log("DetailPanel: scrollToSection function:", scrollToSection); - const saveToLocalSettings = useCallback(() => { - let recentItems = []; - const prdtId = - themeProductInfos && - themeProductInfos.length > 0 && - panelInfo?.type === "theme" - ? themeProductInfos[selectedIndex].prdtId - : panelInfo?.prdtId; + // FP ๋ฐฉ์‹์œผ๋กœ ์Šคํฌ๋กค ์ฒ˜๋ฆฌ ๋กœ์ง + const scrollAction = fp.pipe( + () => ({ scrollToSection, sectionId }), + ({ scrollToSection, sectionId }) => { + if (fp.isNotNil(scrollToSection)) { + return { action: 'execute', scrollFunction: scrollToSection, sectionId }; + } else { + return { action: 'store', sectionId }; + } + } + )(); - if (localRecentItems) { - recentItems = [...localRecentItems]; + // ์•ก์…˜์— ๋”ฐ๋ฅธ ์ฒ˜๋ฆฌ + if (scrollAction.action === 'execute') { + scrollAction.scrollFunction(scrollAction.sectionId); + } else { + console.log( + "DetailPanel: scrollToSection function is null, storing pending scroll", + ); + setPendingScrollSection(scrollAction.sectionId); + } + }, + [scrollToSection], + ); + + // FP ๋ฐฉ์‹์œผ๋กœ pending scroll ์ฒ˜๋ฆฌ (๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜ ๋ฐฉ์ง€) + useEffect(() => { + const shouldExecutePendingScroll = fp.pipe( + () => ({ scrollToSection, pendingScrollSection }), + ({ scrollToSection, pendingScrollSection }) => + fp.isNotNil(scrollToSection) && fp.isNotNil(pendingScrollSection) + )(); + + if (shouldExecutePendingScroll) { + console.log( + "DetailPanel: executing pending scroll to:", + pendingScrollSection, + ); + + // ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜ ๋ฐฉ์ง€๋ฅผ ์œ„ํ•œ cleanup ํ•จ์ˆ˜ + const timeoutId = setTimeout(() => { + if (scrollToSection) { + scrollToSection(pendingScrollSection); + } + setPendingScrollSection(null); + }, 100); + + // cleanup ํ•จ์ˆ˜ ๋ฐ˜ํ™˜์œผ๋กœ ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜ ๋ฐฉ์ง€ + return () => { + clearTimeout(timeoutId); + }; } + }, [scrollToSection, pendingScrollSection]); - const currentDate = new Date(); + // FP ๋ฐฉ์‹์œผ๋กœ ์ดˆ๊ธฐ ๋ฐ์ดํ„ฐ ๋กœ๋”ฉ ์ฒ˜๋ฆฌ (๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜ ๋ฐฉ์ง€) + useEffect(() => { + // FP ๋ฐฉ์‹์œผ๋กœ ์•ก์…˜ ๋””์ŠคํŒจ์น˜ ์ฒด์ด๋‹ + const loadInitialData = fp.pipe( + () => { + // ๊ธฐ๋ณธ ์•ก์…˜ ๋””์ŠคํŒจ์น˜ + dispatch(getProductOptionId(undefined)); + dispatch(getDeviceAdditionInfo()); + }, + () => { + // ํ…Œ๋งˆ ๋ฐ์ดํ„ฐ ๋กœ๋”ฉ + const isThemeType = panelType === "theme"; + + if (isThemeType) { + dispatch( + getThemeCurationDetailInfo({ + patnrId: panelPatnrId, + curationId: panelCurationId, + bgImgNo: panelBgImgNo, + }), + ); + } + }, + () => { + // ์ผ๋ฐ˜ ์ƒํ’ˆ ๋ฐ์ดํ„ฐ ๋กœ๋”ฉ + const hasProductId = fp.isNotNil(panelPrdtId); + const hasNoCuration = fp.isNil(panelCurationId); + + if (hasProductId && hasNoCuration) { + dispatch( + getMainCategoryDetail({ + patnrId: panelPatnrId, + prdtId: panelPrdtId, + liveReqFlag: panelLiveReqFlag || "N", + }), + ); + } + } + )(); - const formattedDate = `${ - currentDate.getMonth() + 1 - }/${currentDate.getDate()}`; - - const existingProductIndex = recentItems.findIndex((item) => { - return item.prdtId === prdtId; - }); - - if (existingProductIndex !== -1) { - recentItems.splice(existingProductIndex, 1); - } - - recentItems.push({ - prdtId: prdtId, - patnrId: panelInfo?.patnrId, - date: formattedDate, - expireTime: currentDate.getTime() + 1000 * 60 * 60 * 24 * 14, - cntryCd: httpHeader["X-Device-Country"], - }); - - if (recentItems.length >= 51) { - const data = [...recentItems]; - dispatch(changeLocalSettings({ recentItems: data.slice(1) })); - } else { - dispatch(changeLocalSettings({ recentItems })); - } + // cleanup ํ•จ์ˆ˜๋กœ ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜ ๋ฐฉ์ง€ + return () => { + // ํ•„์š”ํ•œ ๊ฒฝ์šฐ cleanup ๋กœ์ง ์ถ”๊ฐ€ + }; }, [ - panelInfo?.prdtId, - httpHeader, - localRecentItems, dispatch, - selectedIndex, - themeProductInfos, + panelLiveReqFlag, + panelCurationId, + panelPrdtId, + panelType, + panelPatnrId, + panelBgImgNo, ]); + // FP ๋ฐฉ์‹์œผ๋กœ ์ถ”์ฒœ ์ƒํ’ˆ ๋ฐ์ดํ„ฐ ๋กœ๋”ฉ (๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜ ๋ฐฉ์ง€) + useEffect(() => { + const shouldLoadRecommendations = fp.pipe( + () => lgCatCd, + fp.isNotEmpty + )(); + + if (shouldLoadRecommendations) { + dispatch( + getMainYouMayLike({ + lgCatCd: lgCatCd, + exclCurationId: panelCurationId, + exclPatnrId: panelPatnrId, + exclPrdtId: panelPrdtId, + }), + ); + } + }, [panelCurationId, panelPatnrId, panelPrdtId, lgCatCd]); + + // FP ๋ฐฉ์‹์œผ๋กœ ์นดํ…Œ๊ณ ๋ฆฌ ๊ทœ์น™ ํ—ฌํผ ํ•จ์ˆ˜๋“ค (curry ์ ์šฉ) + const categoryHelpers = useMemo(() => ({ + createCategoryRule: fp.curry((conditionFn, extractFn, data) => + conditionFn(data) ? extractFn(data) : null + ), + hasProductWithoutCuration: fp.curry((panelCurationId, productData) => + fp.isNotNil(productData) && fp.isNil(panelCurationId) + ), + hasThemeWithPaymentCondition: fp.curry((panelCurationId, themeProductInfo) => { + const hasThemeProduct = fp.isNotNil(themeProductInfo); + const equalToN = fp.curry((expected, actual) => actual === expected)("N"); + const isNoPayment = equalToN(fp.pipe(() => themeProductInfo, fp.get('pmtSuptYn'))()); + const hasCuration = fp.isNotNil(panelCurationId); + return hasThemeProduct && isNoPayment && hasCuration; + }) + }), []); + const getlgCatCd = useCallback(() => { - if (productData && !panelInfo?.curationId) { - setLgCatCd(productData.catCd); - } else if ( - themeProductInfos && - themeProductInfos[selectedIndex]?.pmtSuptYn === "N" && - panelInfo?.curationId - ) { - setLgCatCd(themeProductInfos[selectedIndex]?.catCd); - } else { - setLgCatCd(""); - } - }, [productData, themeProductInfos, selectedIndex, panelInfo?.curationId]); + // FP ๋ฐฉ์‹์œผ๋กœ ์นดํ…Œ๊ณ ๋ฆฌ ์ฝ”๋“œ ๊ฒฐ์ • - curry ์ ์šฉ์œผ๋กœ ๋” ํ•จ์ˆ˜ํ˜• ๊ฐœ์„  + const categoryRules = [ + // ์ผ๋ฐ˜ ์ƒํ’ˆ ๊ทœ์น™ (curry ํ™œ์šฉ) + () => categoryHelpers.createCategoryRule( + categoryHelpers.hasProductWithoutCuration(panelCurationId), + (data) => fp.pipe(() => data, fp.get('catCd'))(), + productData + ), + + // ํ…Œ๋งˆ ์ƒํ’ˆ ๊ทœ์น™ (curry ํ™œ์šฉ) + () => categoryHelpers.createCategoryRule( + categoryHelpers.hasThemeWithPaymentCondition(panelCurationId), + (data) => fp.pipe(() => data, fp.get('catCd'))(), + themeProductInfo + ) + ]; - const mobileSendPopUpProductImg = useMemo(() => { - if (panelInfo?.type === "theme" && themeProductInfos) { - return themeProductInfos[selectedIndex]?.imgUrls600[0]; - } else if (panelInfo?.type === "hotel" && hotelInfos) { - return hotelInfos[selectedIndex]?.hotelImgUrl; - } else { - return productData?.imgUrls600[0]; - } - }, [ - themeProductInfos, - hotelInfos, - selectedIndex, - productData, - panelInfo?.type, - ]); - - const mobileSendPopUpSubtitle = useMemo(() => { - if (panelInfo?.type === "theme" && themeProductInfos) { - return themeProductInfos[selectedIndex]?.prdtNm; - } else if (panelInfo?.type === "hotel" && hotelInfos) { - return hotelInfos[selectedIndex]?.hotelNm; - } else { - return productData?.prdtNm; - } - }, [ - themeProductInfos, - hotelInfos, - selectedIndex, - productData, - panelInfo?.type, - ]); - - const isBillingProductVisible = useMemo(() => { - return ( - productData?.pmtSuptYn === "Y" && - productData?.grPrdtProcYn === "N" && - panelInfo?.prdtId && - webOSVersion >= "6.0" + // ์ฒซ ๋ฒˆ์งธ๋กœ ๋งค์นญ๋˜๋Š” ๊ทœ์น™์˜ ๊ฒฐ๊ณผ ์‚ฌ์šฉ (curry์˜ reduce ํ™œ์šฉ) + const categoryCode = fp.reduce( + (result, value) => result || value || "", + "", + categoryRules.map(rule => rule()) ); - }, [productData, webOSVersion, panelInfo?.prdtId]); - - const isUnavailableProductVisible = useMemo(() => { - return ( - productData?.pmtSuptYn === "N" || - (productData?.pmtSuptYn === "Y" && - productData?.grPrdtProcYn === "N" && - webOSVersion < "6.0" && - panelInfo?.prdtId) - ); - }, [productData, webOSVersion, panelInfo?.prdtId]); - - const isGroupProductVisible = useMemo(() => { - return ( - productData?.pmtSuptYn === "Y" && - productData?.grPrdtProcYn === "Y" && - groupInfos && - groupInfos.length > 0 - ); - }, [productData, groupInfos]); - - const isTravelProductVisible = useMemo(() => { - return panelInfo?.curationId && (hotelInfos || themeData); - }, [panelInfo?.curationId, hotelInfos, themeData]); - - const Price = () => { - return ( - <> - {hotelInfos[selectedIndex]?.hotelDetailInfo.currencySign} - {hotelInfos[selectedIndex]?.hotelDetailInfo.price} - - ); - }; + + setLgCatCd(categoryCode); + }, [productData, selectedIndex, panelCurationId, themeProductInfo, categoryHelpers]); + // FP ๋ฐฉ์‹์œผ๋กœ ์นดํ…Œ๊ณ ๋ฆฌ ์ฝ”๋“œ ์—…๋ฐ์ดํŠธ (๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜ ๋ฐฉ์ง€) useEffect(() => { - getlgCatCd(); - }, [themeProductInfos, productData, panelInfo, selectedIndex]); + const shouldUpdateCategory = fp.pipe( + () => ({ themeProductInfo, productData, panelInfo, selectedIndex }), + ({ themeProductInfo, productData, panelInfo, selectedIndex }) => + fp.isNotNil(themeProductInfo) || fp.isNotNil(productData) || fp.isNotNil(panelInfo) + )(); - useEffect(() => { - if (panelInfo && panelInfo?.patnrId && panelInfo?.prdtId) { - saveToLocalSettings(); + if (shouldUpdateCategory) { + getlgCatCd(); } - }, [panelInfo, selectedIndex]); + }, [themeProductInfo, productData, panelInfo, selectedIndex]); - useEffect(() => { - if ( - ((themeProductInfos && themeProductInfos.length > 0) || - (hotelInfos && hotelInfos.length > 0)) && - selectedIndex !== undefined - ) { - if (panelInfo?.type === "theme") { - const imgUrls600 = themeProductInfos[selectedIndex]?.imgUrls600 || []; - dispatch(getProductImageLength({ imageLength: imgUrls600.length })); - return; - } - if (panelInfo?.type === "hotel") { - const imgUrls600 = hotelInfos[selectedIndex]?.imgUrls600 || []; - dispatch(getProductImageLength({ imageLength: imgUrls600.length })); - } - } - }, [dispatch, themeProductInfos, selectedIndex, hotelInfos]); + // ์ตœ๊ทผ ๋ณธ ์ƒํ’ˆ ์ €์žฅ์ด ํ•„์š”ํ•˜๋ฉด: + // - ์ˆœ์ˆ˜ ์œ ํ‹ธ๋กœ ๋นŒ๋“œ/์—…์„œํŠธ ํ•จ์ˆ˜ ์ž‘์„ฑ ํ›„, ์ ์ ˆํ•œ useEffect์—์„œ ํ˜ธ์ถœํ•˜์„ธ์š”. + // ์˜ˆ) saveRecentItem(panelInfo, selectedIndex) + // FP ๋ฐฉ์‹์œผ๋กœ cleanup ์ฒ˜๋ฆฌ (๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜ ๋ฐฉ์ง€) useEffect(() => { return () => { - dispatch(clearProductDetail()); - dispatch(clearThemeDetail()); - dispatch(clearCouponInfo()); - setContainerLastFocusedElement(null, ["indicator-GridListContainer"]); + // FP ๋ฐฉ์‹์œผ๋กœ cleanup ์•ก์…˜ ์‹คํ–‰ + fp.pipe( + () => { + dispatch(clearProductDetail()); + }, + () => { + setContainerLastFocusedElement(null, ["indicator-GridListContainer"]); + } + )(); }; }, [dispatch]); + // ์ตœ๊ทผ ๋ณธ ์ƒํ’ˆ ํŠธ๋ฆฌ๊ฑฐ ์˜ˆ์‹œ: + // useEffect(() => { + // if (panelInfo && panelInfo.patnrId && panelInfo.prdtId) { + // // saveRecentItem(panelInfo, selectedIndex) + // } + // }, [panelInfo, selectedIndex]) + + // ํ…Œ๋งˆ/ํ˜ธํ…” ๊ธฐ๋ฐ˜ ์ธ๋ฑ์Šค ์ดˆ๊ธฐํ™”๊ฐ€ ํ•„์š”ํ•˜๋ฉด: + // - findIndex ์œ ํ‹ธ์„ ๋งŒ๋“ค์–ด ๋งค์นญ ์ธ๋ฑ์Šค๋ฅผ ๊ณ„์‚ฐ ํ›„ setSelectedIndex์— ๋ฐ˜์˜ํ•˜์„ธ์š”. + // FP ๋ฐฉ์‹์œผ๋กœ ๋ฒ„์ „ ๋น„๊ต ํ—ฌํผ ํ•จ์ˆ˜ (curry ์ ์šฉ) + const versionComparators = useMemo(() => ({ + isVersionGTE: fp.curry((target, version) => version >= target), + isVersionLT: fp.curry((target, version) => version < target) + }), []); + + // FP ๋ฐฉ์‹์œผ๋กœ ์กฐ๊ฑด ์ฒดํฌ ํ—ฌํผ ํ•จ์ˆ˜๋“ค (curry ์ ์šฉ) + const conditionCheckers = useMemo(() => ({ + hasDataAndCondition: fp.curry((conditionFn, data) => fp.isNotNil(data) && conditionFn(data)), + equalTo: fp.curry((expected, actual) => actual === expected), + checkAllConditions: fp.curry((conditions, data) => + fp.reduce((acc, condition) => acc && condition, true, conditions.map(fn => fn(data))) + ) + }), []); + + const getProductType = useCallback(() => { + // FP ๋ฐฉ์‹์œผ๋กœ ๋ฐ์ดํ„ฐ ๊ฒ€์ฆ ๋ฐ ํƒ€์ž… ๊ฒฐ์ • - curry ์ ์šฉ์œผ๋กœ ๋” ํ•จ์ˆ˜ํ˜• ๊ฐœ์„  + const createTypeChecker = fp.curry((type, conditions, sideEffect) => + fp.pipe( + () => conditions(), + isValid => isValid ? (() => { + sideEffect && sideEffect(); + return { matched: true, type }; + })() : { matched: false } + )() + ); + + const productTypeRules = [ + // ํ…Œ๋งˆ ํƒ€์ž… ์ฒดํฌ + () => createTypeChecker( + "theme", + () => fp.pipe( + () => ({ panelCurationId, themeData }), + ({ panelCurationId, themeData }) => + fp.isNotNil(panelCurationId) && fp.isNotNil(themeData) + )(), + () => { + const themeProduct = fp.pipe( + () => themeData, + fp.get('productInfos'), + fp.get(selectedIndex.toString()) + )(); + setProductType("theme"); + setThemeProductInfo(themeProduct); + } + ), + + // Buy Now ํƒ€์ž… ์ฒดํฌ (curry ํ™œ์šฉ) + () => createTypeChecker( + "buyNow", + () => fp.pipe( + () => ({ productData, panelPrdtId, productPmtSuptYn, productGrPrdtProcYn, webOSVersion }), + ({ productData, panelPrdtId, productPmtSuptYn, productGrPrdtProcYn, webOSVersion }) => { + const conditions = [ + () => fp.isNotNil(productData), + () => conditionCheckers.equalTo("Y")(productPmtSuptYn), + () => conditionCheckers.equalTo("N")(productGrPrdtProcYn), + () => fp.isNotNil(panelPrdtId), + () => versionComparators.isVersionGTE("6.0")(webOSVersion) + ]; + return conditionCheckers.checkAllConditions(conditions)({}); + } + )(), + () => setProductType("buyNow") + ), + + // Shop By Mobile ํƒ€์ž… ์ฒดํฌ (curry ํ™œ์šฉ) + () => createTypeChecker( + "shopByMobile", + () => fp.pipe( + () => ({ productData, panelPrdtId, productPmtSuptYn, productGrPrdtProcYn, webOSVersion }), + ({ productData, panelPrdtId, productPmtSuptYn, productGrPrdtProcYn, webOSVersion }) => { + if (!productData) return false; + + const isDirectMobile = conditionCheckers.equalTo("N")(productPmtSuptYn); + const conditionalMobileConditions = [ + () => conditionCheckers.equalTo("Y")(productPmtSuptYn), + () => conditionCheckers.equalTo("N")(productGrPrdtProcYn), + () => versionComparators.isVersionLT("6.0")(webOSVersion), + () => fp.isNotNil(panelPrdtId) + ]; + const isConditionalMobile = conditionCheckers.checkAllConditions(conditionalMobileConditions)({}); + + return isDirectMobile || isConditionalMobile; + } + )(), + () => setProductType("shopByMobile") + ) + ]; + + // FP ๋ฐฉ์‹์œผ๋กœ ์ˆœ์ฐจ์  ํƒ€์ž… ์ฒดํฌ + const matchedRule = fp.reduce( + (result, rule) => result.matched ? result : rule(), + { matched: false }, + productTypeRules + ); + + // ๋งค์นญ๋˜์ง€ ์•Š์€ ๊ฒฝ์šฐ ๋””๋ฒ„๊น… ์ •๋ณด ์ถœ๋ ฅ + if (!matchedRule.matched) { + const debugInfo = fp.pipe( + () => ({ productData, panelPrdtId, productPmtSuptYn, productGrPrdtProcYn, webOSVersion }), + ({ productData, panelPrdtId, productPmtSuptYn, productGrPrdtProcYn, webOSVersion }) => ({ + pmtSuptYn: productPmtSuptYn, + grPrdtProcYn: productGrPrdtProcYn, + prdtId: panelPrdtId, + webOSVersion, + }) + )(); + + console.warn("Unknown product type:", productData); + console.warn("Product data properties:", debugInfo); + } + }, [ + panelCurationId, + themeData, + productPmtSuptYn, + productGrPrdtProcYn, + panelPrdtId, + webOSVersion, + selectedIndex, + versionComparators, + conditionCheckers, + ]); + + useEffect(() => { + // productData๊ฐ€ ๋กœ๋“œ๋œ ํ›„์—๋งŒ getProductType ์‹คํ–‰ + if (productData || (panelType === "theme" && themeData)) { + getProductType(); + } + }, [getProductType, productData, themeData, panelType]); + + const imageUrl = useMemo(() => fp.pipe(() => productData, fp.get('thumbnailUrl960'))(), [productData]); + + // FP ๋ฐฉ์‹์œผ๋กœ ํƒ€์ดํ‹€๊ณผ aria-label ๋ฉ”๋ชจ์ด์ œ์ด์…˜ (์„ฑ๋Šฅ ์ตœ์ ํ™”) + const headerTitle = useMemo(() => fp.pipe( + () => ({ panelPrdtId, productData, panelType, themeData }), + ({ panelPrdtId, productData, panelType, themeData }) => { + const productTitle = fp.pipe( + () => ({ panelPrdtId, productData }), + ({ panelPrdtId, productData }) => + fp.isNotNil(panelPrdtId) && fp.pipe(() => productData, fp.get('prdtNm'), fp.isNotNil)() + ? fp.pipe(() => productData, fp.get('prdtNm'))() + : null + )(); + + const themeTitle = fp.pipe( + () => ({ panelType, themeData }), + ({ panelType, themeData }) => + panelType === "theme" && fp.pipe(() => themeData, fp.get('curationNm'), fp.isNotNil)() + ? fp.pipe(() => themeData, fp.get('curationNm'))() + : null + )(); + + return productTitle || themeTitle || ""; + } + )(), [panelPrdtId, productData, panelType, themeData]); + + const ariaLabel = useMemo(() => fp.pipe( + () => ({ panelPrdtId, productData }), + ({ panelPrdtId, productData }) => + fp.isNotNil(panelPrdtId) && fp.pipe(() => productData, fp.get('prdtNm'), fp.isNotNil)() + ? fp.pipe(() => productData, fp.get('prdtNm'))() + : "" + )(), [panelPrdtId, productData]); + + // FP ๋ฐฉ์‹์œผ๋กœ ๋ฐฐ๊ฒฝ ์ด๋ฏธ์ง€ ์„ค์ • (๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜ ๋ฐฉ์ง€) + useLayoutEffect(() => { + const shouldSetBackground = fp.pipe( + () => ({ imageUrl, containerRef }), + ({ imageUrl, containerRef }) => + fp.isNotNil(imageUrl) && fp.isNotNil(containerRef.current) + )(); + + if (shouldSetBackground) { + containerRef.current.style.setProperty("--bg-url", `url('${imageUrl}')`); + } + }, [imageUrl]); + + console.log("productDataSource :", productDataSource); + + // ์–ธ๋งˆ์šดํŠธ ์‹œ ์ธ๋ฑ์Šค ์ดˆ๊ธฐํ™”๊ฐ€ ํ•„์š”ํ•˜๋ฉด: + // useEffect(() => () => setSelectedIndex(0), []) + return ( - <> +

- - {!isLoading && ( - <> - {/* ๊ฒฐ์ œ๊ฐ€๋Šฅ์ƒํ’ˆ ์˜์—ญ */} - {isBillingProductVisible && ( - { + // FP ๋ฐฉ์‹์œผ๋กœ ๋ Œ๋”๋ง ์กฐ๊ฑด ๊ฒฐ์ • (๋ฉ”๋ชจ์ด์ œ์ด์…˜์œผ๋กœ ์ตœ์ ํ™”) + const renderStates = fp.pipe( + () => ({ isLoading, panelInfo, productDataSource, productType }), + ({ isLoading, panelInfo, productDataSource, productType }) => { + const hasRequiredData = fp.pipe( + () => [panelInfo, productDataSource, productType], + data => fp.reduce((acc, item) => acc && fp.isNotNil(item), true, data) + )(); + + return { + canRender: !isLoading && hasRequiredData, + showLoading: !isLoading && !hasRequiredData, + showNothing: isLoading + }; + } + )(); + + if (renderStates.canRender) { + return ( + - )} - {/* ๊ตฌ๋งค๋ถˆ๊ฐ€์ƒํ’ˆ ์˜์—ญ */} - {isUnavailableProductVisible && ( - - )} - {/* ๊ทธ๋ฃน์ƒํ’ˆ ์˜์—ญ */} - {isGroupProductVisible && ( - - )} - {/* ํ…Œ๋งˆ๊ทธ๋ฃน์ƒํ’ˆ ์˜์—ญ*/} - {isTravelProductVisible && ( - - )} - - )} + ); + } + + if (renderStates.showLoading) { + return ( +
+ +
+ ); + } + + return null; + }, [isLoading, panelInfo, productDataSource, productType, selectedIndex, panelPatnrId, panelPrdtId, updateSelectedIndex, openThemeItemOverlay, updateThemeItemOverlay, themeProductInfo])} -
- {lgCatCd && youmaylikeData && youmaylikeData.length > 0 && isOnTop && ( - - )} - {activePopup === Config.ACTIVE_POPUP.smsPopup && ( - - )} - + +
); } diff --git a/com.twin.app.shoptime/src/views/DetailPanel/DetailPanel.module.less b/com.twin.app.shoptime/src/views/DetailPanel/DetailPanel.module.less index ca94a989..d50c6f2f 100644 --- a/com.twin.app.shoptime/src/views/DetailPanel/DetailPanel.module.less +++ b/com.twin.app.shoptime/src/views/DetailPanel/DetailPanel.module.less @@ -2,7 +2,17 @@ @import "../../style/utils.module.less"; .detailPanelWrap { - background: @COLOR_WHITE; + background-size: cover; + background-position: center; + background-image: + linear-gradient( + 270deg, + rgba(0, 0, 0, 0) 43.07%, + rgba(0, 0, 0, 0.539) 73.73%, + rgba(0, 0, 0, 0.7) 100% + ), + linear-gradient(0deg, rgba(0, 0, 0, 0.4), rgba(0, 0, 0, 0.4)); + // var(--bg-url); } .header { @@ -14,12 +24,17 @@ text-transform: none !important; letter-spacing: 0 !important; } + + > button { + background-image: url("../../../assets/images/btn/btn-60-wh-back-nor@3x.png"); + } + display: flex; width: 100%; - height: 90px; - background-color: #f2f2f2; + height: 60px; + background-color: transparent; align-items: center; - color: #333333; + color: rgba(238, 238, 238, 1); padding: 0 0 0 60px; position: relative; @@ -37,5 +52,10 @@ display: flex; justify-content: space-between; - padding-left: 120px; + // padding-left: 120px; + + .detailArea { + .flex(); + padding-left: -60px; + } } diff --git a/com.twin.app.shoptime/src/views/DetailPanel/ProductAllSection/ProductAllSection.jsx b/com.twin.app.shoptime/src/views/DetailPanel/ProductAllSection/ProductAllSection.jsx new file mode 100644 index 00000000..11491d2d --- /dev/null +++ b/com.twin.app.shoptime/src/views/DetailPanel/ProductAllSection/ProductAllSection.jsx @@ -0,0 +1,431 @@ +/* eslint-disable react/jsx-no-bind */ +// src/views/DetailPanel/ProductAllSection/ProductAllSection.jsx +import SpotlightContainerDecorator from "@enact/spotlight/SpotlightContainerDecorator"; +import React, { useCallback, useRef, useState, useMemo, useEffect } from "react"; +import { useSelector } from "react-redux"; +import Spotlight from "@enact/spotlight"; +import { PropTypes } from "prop-types"; + +// ProductInfoSection imports +import TButton from "../../../components/TButton/TButton"; +import { $L } from "../../../utils/helperMethods"; +import { + curry, pipe, when, isVal, isNotNil, defaultTo, defaultWith, get, identity, isEmpty, isNil, andThen, tap +} from "../../../utils/fp"; +import FavoriteBtn from "../components/FavoriteBtn"; +import StarRating from "../components/StarRating"; +import ProductTag from "../components/ProductTag"; +import DetailMobileSendPopUp from "../components/DetailMobileSendPopUp"; +import { SpotlightIds } from "../../../utils/SpotlightIds"; +import QRCode from "../ProductInfoSection/QRCode/QRCode"; +import ProductOverview from "../ProductOverview/ProductOverview"; + +// ProductContentSection imports +import TScrollerDetail from "../components/TScroller/TScrollerDetail"; +import CustomScrollbar from "../components/CustomScrollbar/CustomScrollbar"; +import useScrollTo from "../../../hooks/useScrollTo"; +import ProductDetail from "../ProductContentSection/ProductDetail/ProductDetail.new"; +import UserReviews from "../ProductContentSection/UserReviews/UserReviews"; +import YouMayAlsoLike from "../ProductContentSection/YouMayAlsoLike/YouMayAlsoLike"; +import ProductDescription from "../ProductContentSection/ProductDescription/ProductDescription"; + +// CSS imports +// import infoCSS from "../ProductInfoSection/ProductInfoSection.module.less"; +// import contentCSS from "../ProductContentSection/ProductContentSection.module.less"; +import css from "./ProductAllSection.module.less"; + +const Container = SpotlightContainerDecorator( + { + enterTo: "last-focused", + preserveld: true, + leaveFor: { right: "content-scroller-container" }, + spotlightDirection: "vertical" + }, + "div", +); + +const ContentContainer = SpotlightContainerDecorator( + { + enterTo: "default-element", + preserveld: true, + leaveFor: { + left: "spotlight-product-info-section-container" + }, + restrict: "none", + spotlightDirection: "vertical" + }, + "div", +); + +const HorizontalContainer = SpotlightContainerDecorator( + { + enterTo: "last-focused", + preserveld: true, + defaultElement: "spotlight-product-info-section-container", + spotlightDirection: "horizontal" + }, + "div", +); + + +// FP: Pure function to determine product data based on type +const getProductData = curry((productType, themeProductInfo, productInfo) => + pipe( + when( + () => isVal(productType) && productType === "theme" && isVal(themeProductInfo), + () => themeProductInfo + ), + defaultTo(productInfo), + defaultTo({}) // ๋นˆ ๊ฐ์ฒด๋ผ๋„ ๋ฐ˜ํ™˜ํ•˜์—ฌ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋ Œ๋”๋ง๋˜๋„๋ก ํ•จ + )(productInfo) +); + +// FP: Pure function to derive favorite flag +const deriveFavoriteFlag = curry((favoriteOverride, productData) => + pipe( + when(isNotNil, identity), + defaultWith(() => + pipe( + get("favorYn"), + defaultTo("N") + )(productData) + ) + )(favoriteOverride) +); + +// FP: Pure function to extract review grade and order phone +const extractProductMeta = (productInfo) => ({ + revwGrd: get("revwGrd", productInfo), + orderPhnNo: get("orderPhnNo", productInfo) +}); + +// ๋ ˆ์ด์•„์›ƒ ํ™•์ธ์šฉ ์ƒ˜ํ”Œ ์ปดํฌ๋„ŒํŠธ +const LayoutSample = () => ( +
+ Layout Sample (1124px x 300px) +
+); + +export default function ProductAllSection({ + productType, + productInfo, + panelInfo, + selectedIndex, + selectedPatnrId, + selectedPrdtId, + setSelectedIndex, + openThemeItemOverlay, + setOpenThemeItemOverlay, + themeProductInfo, +}) { + const productData = useMemo(() => + getProductData(productType, themeProductInfo, productInfo), + [productType, themeProductInfo, productInfo] + ); + + // ๋””๋ฒ„๊น…: ์‹ค์ œ ์ด๋ฏธ์ง€ ๋ฐ์ดํ„ฐ ํ™•์ธ + useEffect(() => { + console.log("[ProductAllSection] Image data check:", { + hasProductData: !!productData, + imgUrls600: productData?.imgUrls600, + imgUrls600Length: productData?.imgUrls600?.length, + imgUrls600Type: Array.isArray(productData?.imgUrls600) ? 'array' : typeof productData?.imgUrls600 + }); + }, [productData]); + + const { revwGrd, orderPhnNo } = useMemo(() => + extractProductMeta(productInfo), + [productInfo] + ); + + // FP: derive favorite flag from props with local override, avoid non-I/O useEffect + const [favoriteOverride, setFavoriteOverride] = useState(null); + const favoriteFlag = useMemo(() => + deriveFavoriteFlag(favoriteOverride, productData), + [favoriteOverride, productData] + ); + + const [mobileSendPopupOpen, setMobileSendPopupOpen] = useState(false); + + // ๐Ÿ”ง [์ž„์‹œ] ๊ณ ๊ฐ ๋ฐ๋ชจ์šฉ: UserReviews ๋ฒ„ํŠผ์€ ์ˆจ๊ธฐ๊ณ  UserReviews ์„น์…˜๋งŒ ํ‘œ์‹œ + const showUserReviewsButton = false; // ์ž„์‹œ ๋ณ€๊ฒฝ - ๋ฒ„ํŠผ ์ˆจ๊น€ + const showUserReviewsSection = true; // ์ž„์‹œ ๋ณ€๊ฒฝ - ์„น์…˜์€ ํ•ญ์ƒ ํ‘œ์‹œ + + const reviewTotalCount = useSelector(pipe( + get(["product", "reviewData", "reviewDetail", "totRvwCnt"]), + defaultTo(0) + )); + + // User Reviews ์Šคํฌ๋กค ํ•ธ๋“ค๋Ÿฌ ์ถ”๊ฐ€ + const handleUserReviewsClick = useCallback( + () => scrollToSection("scroll-marker-user-reviews"), + [scrollToSection] + ); + + const scrollContainerRef = useRef(null); + const { getScrollTo, scrollTop } = useScrollTo(); + + + // FP: Pure function for mobile popup state change + const handleShopByMobileOpen = useCallback( + pipe( + () => true, + setMobileSendPopupOpen + ), + [] + ); + + // FP: Pure function for focus navigation to back button + const handleSpotlightUpToBackButton = useCallback((e) => { + e.stopPropagation(); + Spotlight.focus("spotlightId_backBtn"); + }, []); + + // FP: Pure function for favorite flag change + const onFavoriteFlagChanged = useCallback( + (newFavoriteFlag) => setFavoriteOverride(newFavoriteFlag), + [] + ); + + // FP: Pure function for theme item button click with side effects + const handleThemeItemButtonClick = useCallback( + pipe( + () => setOpenThemeItemOverlay(true), + tap(() => setTimeout(() => Spotlight.focus("theme-close-button"), 0)) + ), + [setOpenThemeItemOverlay] + ); + + // FP: Pure function for scroll to section with early returns handled functionally + const scrollToSection = curry((sectionId) => + pipe( + when(isEmpty, () => null), + andThen(() => document.getElementById(sectionId)), + when(isNil, () => null), + andThen((targetElement) => { + const targetRect = targetElement.getBoundingClientRect(); + const y = targetRect.top; + return scrollTop({ y, animate: true }); + }) + )(sectionId) + ); + + // FP: Curried scroll handlers + const handleProductDetailsClick = useCallback( + () => scrollToSection("scroll-marker-product-details"), + [scrollToSection] + ); + + const handleYouMayAlsoLikeClick = useCallback( + () => scrollToSection("scroll-marker-you-may-also-like"), + [scrollToSection] + ); + + + return ( + + {/* Left Margin Section - 60px */} +
+ + {/* Info Section - 645px */} +
+ + {productType && productData && ( +
+
+ + {revwGrd && ( + + )} +
+ + +
+ +
+
+ + + +
+ {$L("SHOP BY MOBILE")} +
+
+ {panelInfo && ( +
+ +
+ )} +
+ + {orderPhnNo && ( +
+
+ {$L("Call to Order")} +
+
+
+
+
+
{orderPhnNo}
+
+
+ )} + + + + {$L("PRODUCT DETAILS")} + + {/* ๐Ÿ”ง [์ž„์‹œ] ๊ณ ๊ฐ ๋ฐ๋ชจ์šฉ ์กฐ๊ฑด ๋ณ€๊ฒฝ: showUserReviewsButton (์›๋ž˜: reviewTotalCount > 0) */} + {showUserReviewsButton && ( + + {$L( + `USER REVIEWS (${reviewTotalCount > 100 ? "100" : reviewTotalCount || "0"})`, + )} + + )} + + {$L("YOU MAY ALSO LIKE")} + + + + {panelInfo && + panelInfo && panelInfo.type === "theme" && + !openThemeItemOverlay && ( + + {$L("THEME ITEM")} + + )} + + +
+ )} + +
+ + {/* Content Section - 1114px */} +
+ +
+ +
+
+ +
+ {productData?.imgUrls600 && productData.imgUrls600.length > 0 ? ( + productData.imgUrls600.map((image, index) => ( + + )) + ) : ( + + )} +
+
+ +
+
+ {/* Description ๋ฐ”๋กœ ์•„๋ž˜์— UserReviews ํ•ญ์ƒ ํ‘œ์‹œ (์กฐ๊ฑด ์ œ๊ฑฐ) */} +
+ +
+
+
+
+ +
+
+
+
+
+ + + ); +} + +ProductAllSection.propTypes = { + productType: PropTypes.oneOf(["buyNow", "shopByMobile", "theme"]).isRequired, +}; \ No newline at end of file diff --git a/com.twin.app.shoptime/src/views/DetailPanel/ProductAllSection/ProductAllSection.module.less b/com.twin.app.shoptime/src/views/DetailPanel/ProductAllSection/ProductAllSection.module.less new file mode 100644 index 00000000..58a69c8e --- /dev/null +++ b/com.twin.app.shoptime/src/views/DetailPanel/ProductAllSection/ProductAllSection.module.less @@ -0,0 +1,465 @@ +@import "../../../style/CommonStyle.module.less"; +@import "../../../style/utils.module.less"; + +// 1920px ํ™”๋ฉด ๊ธฐ์ค€ ์ „์ฒด ๊ตฌ์กฐ (์ ˆ๋Œ€ ์œ„์น˜๋กœ ์ •ํ™•ํžˆ ๋ฐฐ์น˜) +.detailArea { + width: 1920px; // ๋ช…์‹œ์ ์œผ๋กœ ํ™”๋ฉด ํฌ๊ธฐ ์„ค์ • + height: 100%; + padding: 0; // ๋ชจ๋“  ํŒจ๋”ฉ ์ œ๊ฑฐ + margin: 0; // ๋ชจ๋“  ๋งˆ์ง„ ์ œ๊ฑฐ + display: flex; + justify-content: flex-start; + align-items: flex-start; + position: relative; // ์ ˆ๋Œ€ ์œ„์น˜ ๊ธฐ์ค€์  + + // Spotlight ์ขŒ์šฐ ์ด๋™์„ ์œ„ํ•œ ์„ค์ • + &:focus-within { + outline: none; + } +} + +// 1. Left Margin Section - 60px +.leftMarginSection { + position: absolute; + left: 0; + top: 0; + width: 60px; + height: 100%; + padding: 0; + margin: 0; + background: transparent; +} + +// 2. Info Section - 645px +.infoSection { + position: absolute; + left: 60px; + top: 0; + width: 645px; + height: 100%; + padding: 0; + margin: 0; + display: flex; + justify-content: flex-start; + align-items: flex-start; +} + +// 3. Content Section - 1180px (1114px ์ฝ˜ํ…์ธ  + 66px ์Šคํฌ๋กค๋ฐ”) +.contentSection { + position: absolute; + left: 705px; // 60px + 645px + top: 0; + width: 1200px; // 30px ๋งˆ์ง„ + 1114px ์ฝ˜ํ…์ธ  + 66px ์Šคํฌ๋กค๋ฐ” + height: 100%; + padding: 0; + margin: 0; + display: flex; + justify-content: flex-start; + align-items: flex-start; +} + +// 4. Scroll Section - 66px (์‚ญ์ œ - contentSection์— ํฌํ•จ) +.scrollSection { + display: none; // ์‚ฌ์šฉํ•˜์ง€ ์•Š์Œ +} + +// ์™ผ์ชฝ ์˜์—ญ ์ปจํ…Œ์ด๋„ˆ (infoSection ๋‚ด๋ถ€) +.leftInfoContainer { + width: 635px; // ์‹ค์ œ ์ฝ˜ํ…์ธ  ์˜์—ญ + margin-right: 10px; // ์šฐ์ธก 10px ๊ฐ„๊ฒฉ + padding: 0; + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: flex-start; + height: 100%; +} + +// ์™ผ์ชฝ ์˜์—ญ ๋‚ด๋ถ€ ๋ž˜ํผ +.leftInfoWrapper { + width: 100%; + padding: 0; + margin: 0; + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: flex-start; + + // gap ๋Œ€์‹  margin ์‚ฌ์šฉ (Chromium 68 ํ˜ธํ™˜์„ฑ) + > * { + margin-bottom: 5px; + &:last-child { margin-bottom: 0; } + } +} + +// ์˜ค๋ฅธ์ชฝ ์˜์—ญ ์ปจํ…Œ์ด๋„ˆ (contentSection ๋‚ด๋ถ€) +.rightContentContainer { + width: 1210px; // 30px ๋งˆ์ง„ + 1114px ์ฝ˜ํ…์ธ  + 66px ์Šคํฌ๋กค๋ฐ” + height: @globalHeight - 136px; + padding: 0; + margin: 0; + overflow: hidden; + display: flex; + justify-content: flex-start; + align-items: flex-start; + + // ์Šคํฌ๋กค๋Ÿฌ ๋ž˜ํผ (contentSection ๋‚ด๋ถ€) + .scrollerWrapper { + width: 1210px; // 30px ๋งˆ์ง„ + 1114px ์ฝ˜ํ…์ธ  + 66px ์Šคํฌ๋กค๋ฐ” (์ ˆ๋Œ€๊ฐ’) + height: 100%; + padding: 0; + margin: 0; + overflow: visible; // hidden์—์„œ visible๋กœ ๋ณ€๊ฒฝ + display: flex; + justify-content: flex-start; + align-items: flex-start; + position: relative; // ์ž์‹ absolute ์š”์†Œ์˜ ๊ธฐ์ค€์  + + + // ์Šคํฌ๋กค๋Ÿฌ ์˜ค๋ฒ„๋ผ์ด๋“œ (1210px = 30px + content + ์Šคํฌ๋กค๋ฐ”) + .scrollerOverride { + width: 1210px; // ์ ˆ๋Œ€ ํฌ๊ธฐ ์ง€์ • + height: 100%; + // ์ขŒ์ธก 30px, ์šฐ์ธก 66px(์Šคํฌ๋กค๋ฐ”) ํŒจ๋”ฉ์„ ๋ช…์‹œ์ ์œผ๋กœ ์ ์šฉ + padding: 0 10px 0 30px; + box-sizing: border-box; + margin: 0; + overflow-y: auto; + overflow-x: hidden; + display: flex; + flex-direction: column; + + // ์Šคํฌ๋กค๋ฐ” ๋„ˆ๋น„๋ฅผ 6px๋กœ ๋ช…ํ™•ํ•˜๊ฒŒ ์„ค์ • + &::-webkit-scrollbar { + width: 6px; + } + + &::-webkit-scrollbar-track { + background: transparent; // ํŠธ๋ž™ ๋ฐฐ๊ฒฝ์€ ํˆฌ๋ช…ํ•˜๊ฒŒ + } + + &::-webkit-scrollbar-thumb { + background: #9C9C9C; // ์Šคํฌ๋กค๋ฐ” ์ƒ‰์ƒ + border-radius: 3px; // ์Šคํฌ๋กค๋ฐ” ๋‘ฅ๊ทผ ๋ชจ์„œ๋ฆฌ + } + + // ์Šคํฌ๋กค๋ฐ” thumb์— hover ํšจ๊ณผ ์ ์šฉ + &:hover::-webkit-scrollbar-thumb { + background: #C72054; + } + } + + + // ๋‚ด๋ถ€ ์ฝ˜ํ…์ธ ๋Š” ๋ณ„๋„ ๋„ˆ๋น„ ๊ณ„์‚ฐ ์—†์ด 100%๋ฅผ ์‚ฌ์šฉ + > div { + width: 100%; // ๋ถ€๋ชจ์˜ ํŒจ๋”ฉ์„ ์ œ์™ธํ•œ ๋‚˜๋จธ์ง€ ๊ณต๊ฐ„(1114px)์„ ๋ชจ๋‘ ์‚ฌ์šฉ + padding: 0; + margin: 0; + box-sizing: border-box; + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: flex-start; + overflow: visible; + } + + // ์Šคํฌ๋กค ์ฝ˜ํ…์ธ  ๋‚ด๋ถ€์˜ ๋ชจ๋“  ์„น์…˜๋“ค (ํŒจ๋”ฉ ์ œ๊ฑฐ - scrollerOverride์—์„œ ์ฒ˜๋ฆฌํ•จ) + #product-details-section { + width: 1124px; // ๋ถ€๋ชจ ์ฝ˜ํ…์ธ  ์˜์—ญ ์ „์ฒด ์‚ฌ์šฉ + max-width: none; + padding: 0; + margin: 0; + box-sizing: border-box; + overflow: visible; + display: flex; + flex-direction: column; + align-items: flex-start; + + // ProductDetail.new ์ปดํฌ๋„ŒํŠธ๋“ค + > div[class*="rollingWrap"] { + width: 100% !important; // ๋ถ€๋ชจ ์˜์—ญ ์ „์ฒด ์‚ฌ์šฉ + max-width: none !important; + box-sizing: border-box; + } + } + + #product-description-section, + #user-reviews-section, + #you-may-also-like-section { + width: 100%; // ๋ถ€๋ชจ ์ฝ˜ํ…์ธ  ์˜์—ญ ์ „์ฒด ์‚ฌ์šฉ + max-width: none; + padding: 0; + margin: 0; + box-sizing: border-box; + overflow: visible; + + > * { + max-width: 100% !important; // ๋ถ€๋ชจ ์˜์—ญ ์ „์ฒด ์‚ฌ์šฉ + width: 100% !important; + box-sizing: border-box; + } + + // ์ด๋ฏธ์ง€๋“ค์ด ์ปจํ…Œ์ด๋„ˆ๋ฅผ ๋„˜์ง€ ์•Š๋„๋ก + img { + max-width: 100% !important; + width: auto !important; + height: auto; + } + } + + // ์ƒํ’ˆ ์ƒ์„ธ ์˜์—ญ (๋งˆ์ง„ ํฌํ•จ ํฌ๊ธฐ) + .productDetail { + width: 1124px; // 1114px + 10px + max-width: 1124px; + height: auto; + display: flex; + flex-direction: column; + align-items: stretch; // ์ž์‹ ์š”์†Œ๋“ค์ด ์ „์ฒด ๋„ˆ๋น„ ์‚ฌ์šฉํ•˜๋„๋ก + } + + // ์Šคํฌ๋กค ๋งˆ์ปค + .scrollMarker { + height: 1px; + visibility: hidden; + } + } // .scrollerWrapper +} // .rightContentContainer + +// (์ค‘๋ณต ์ œ๊ฑฐ๋จ) .scrollerWrapper๋Š” .rightContentContainer ํ•˜์œ„๋กœ ์ค‘์ฒฉ ์ด๋™ + +// (์ค‘๋ณต ์ œ๊ฑฐ๋จ) ์ตœ์ƒ์œ„ ์Šคํฌ๋กค๋Ÿฌ/์„น์…˜ ์ •์˜๋Š” .scrollerWrapper ์ค‘์ฒฉ ๋‚ด๋ถ€๋กœ ์ด๋™ + + +// ProductDetailCard ์Šคํƒ€์ผ ์ฐธ๊ณ  - ํฌ๊ธฐ/๊ฐ„๊ฒฉ๋งŒ ์ ์šฉ + +// ํ—ค๋” ์ปจํ…์ธ  ์˜์—ญ (ํƒœ๊ทธ, ๋ณ„์ ) +.headerContent { + width: 100%; + display: flex; + align-items: center; + justify-content: space-between; +} + +// ๋ชจ๋ฐ”์ผ ์‡ผํ•‘ ์„น์…˜ (mobileSection ์ฐธ๊ณ ) +.buttonContainer { + align-self: stretch; + padding-top: 19px; + display: flex; + justify-content: flex-start; + align-items: center; + + > * { + margin-right: 6px; + &:last-child { margin-right: 0; } + } +} + +.shopByMobileButton { + flex: 1 1 0 !important; + width: auto !important; // flex๋กœ ํฌ๊ธฐ ์กฐ์ • + height: 60px !important; + background: rgba(68, 68, 68, 0.50) !important; + border-radius: 6px !important; + border: none !important; + padding: 0 !important; + margin: 0 !important; + display: flex !important; + align-items: center !important; + justify-content: center !important; + + .shopByMobileText { + color: white !important; + font-size: 25px !important; + font-family: @baseFont !important; // LG Smart ํฐํŠธ ์‚ฌ์šฉ + font-weight: 400 !important; // Bold์—์„œ Regular๋กœ ๋ณ€๊ฒฝ + line-height: 35px !important; + text-align: center !important; + } + + // ํฌ์ปค์Šค ์ƒํƒœ ์ถ”๊ฐ€ + &:focus { + background: @PRIMARY_COLOR_RED !important; // ํฌ์ปค์Šค์‹œ ๋นจ๊ฐ„์ƒ‰ ๋ฐฐ๊ฒฝ + outline: 2px solid @PRIMARY_COLOR_RED !important; + + .shopByMobileText { + color: white !important; // ํฌ์ปค์Šค์‹œ์—๋„ ํ…์ŠคํŠธ๋Š” ํฐ์ƒ‰ ์œ ์ง€ + } + } +} + +.favoriteBtnWrapper { + width: 60px; + height: 60px; + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; + // ๋ฐฐ๊ฒฝ์ƒ‰๊ณผ ๋ผ์šด๋“œ๋Š” FavoriteBtn ๋‚ด๋ถ€์—์„œ ์ฒ˜๋ฆฌํ•˜๋ฏ€๋กœ ์ œ๊ฑฐ +} + +// ์ฃผ๋ฌธ ์ „ํ™” ์„น์…˜ (callToOrderSection ์ฐธ๊ณ ) +.callToOrderSection { + align-self: stretch; + height: 40px; + padding: 17px 30px; + border-radius: 6px; + display: flex; + justify-content: space-between; + align-items: center; + + .callToOrderText { + color: #EAEAEA; + font-size: 25px; + font-family: @baseFont; // LG Smart ํฐํŠธ ์‚ฌ์šฉ + font-weight: 400; // Bold์—์„œ Regular๋กœ ๋ณ€๊ฒฝ + line-height: 35px; + } + + .phoneSection { + padding: 0 1px; + display: flex; + align-items: center; + + > * { + margin-right: 10px; + &:last-child { margin-right: 0; } + } + + .phoneIconContainer { + width: 25px; + height: 25px; + position: relative; + overflow: hidden; + + .phoneIcon { + width: 24.94px; + height: 24.97px; + position: absolute; + left: 0; + top: 0; + background: #EAEAEA; + // ์ „ํ™” ์•„์ด์ฝ˜ ์ด๋ฏธ์ง€ ๋˜๋Š” CSS๋กœ ๊ตฌํ˜„ + background-image: url("../../../../assets/images/icons/ic-gr-call-1.png"); + background-size: contain; + background-repeat: no-repeat; + background-position: center; + } + } + + .phoneNumber { + color: #EAEAEA; + font-size: 25px; + font-family: 'LG Smart UI'; + font-weight: 700; + line-height: 35px; + } + } +} + +// ์•ก์…˜ ๋ฒ„ํŠผ๋“ค (actionButtons ์ฐธ๊ณ ) +.actionButtonsWrapper { + align-self: stretch; + padding-top: 20px; + display: flex; + flex-direction: column; + + > * { + margin-bottom: 5px; + &:last-child { margin-bottom: 0; } + } +} + +// ๋ชจ๋“  ๋ฒ„ํŠผ ๊ธฐ๋ณธ ์Šคํƒ€์ผ (PRODUCT DETAILS๋Š” ๋นจ๊ฐ„์ƒ‰ ์•„๋‹˜!) +.productDetailsButton, +.userReviewsButton, +.youMayLikeButton { + align-self: stretch; + height: 60px; + background: rgba(255, 255, 255, 0.05); // ๊ธฐ๋ณธ ํšŒ์ƒ‰ ๋ฐฐ๊ฒฝ + border-radius: 6px; + display: flex; + align-items: center; + justify-content: center; + + color: #EAEAEA; + font-size: 25px; + font-family: @baseFont; // LG Smart ํฐํŠธ ์‚ฌ์šฉ + font-weight: 400; // Bold์—์„œ Regular๋กœ ๋ณ€๊ฒฝ + line-height: 35px; + + &:focus { + background: #C72054; // ํฌ์ปค์Šค์‹œ๋งŒ ๋นจ๊ฐ„์ƒ‰ + } +} + +.themeButton { + align-self: stretch; + height: 60px; + background: rgba(255, 255, 255, 0.05); + border-radius: 6px; + margin-top: 10px; + + &:focus { + background: #C72054; + } +} + +// QR ๋ž˜ํผ (imageSection ์ฐธ๊ณ  - 240px ๊ณ ์ •) +.qrWrapper { + width: 240px; + flex-shrink: 0; + display: flex; + flex-direction: column; + align-items: flex-end; + + > * { + margin-bottom: 10px; + &:last-child { margin-bottom: 0; } + } +} + +// ProductOverview ์ปจํ…Œ์ด๋„ˆ ์Šคํƒ€์ผ ์ˆ˜์ • (์ž์‹ ์š”์†Œ์— ๋งž๊ฒŒ ํฌ๊ธฐ ์กฐ์ •) +[class*="ProductOverview"] { + align-self: stretch; + padding: 0 0 5px; // ProductDetailCard mainContent์™€ ๋™์ผํ•œ ํŒจ๋”ฉ + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: flex-start; + + // ๋‚ด๋ถ€ div (productInfoWrapper) + > div { + align-self: stretch; + display: flex; + justify-content: flex-start; + align-items: flex-start; + + > * { + margin-right: 15px; // ProductDetailCard์™€ ๋™์ผํ•œ ๊ฐ„๊ฒฉ + &:last-child { margin-right: 0; } + } + + // ๊ฐ€๊ฒฉ ์„น์…˜ (flex๋กœ ๋‚จ์€ ๊ณต๊ฐ„ ์ฐจ์ง€) + > div:first-child { + flex: 1 1 0; + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: flex-start; + } + + // ์ด๋ฏธ์ง€ ์„น์…˜ (QR ํฌํ•จ, ๊ณ ์ • ํฌ๊ธฐ) + > div:last-child { + width: 240px; + flex-shrink: 0; + display: flex; + flex-direction: column; + align-items: flex-end; + + > * { + margin-bottom: 10px; + &:last-child { margin-bottom: 0; } + } + } + } +} diff --git a/com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/ProductDescription/ProductDescription.jsx b/com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/ProductDescription/ProductDescription.jsx new file mode 100644 index 00000000..8768b092 --- /dev/null +++ b/com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/ProductDescription/ProductDescription.jsx @@ -0,0 +1,76 @@ +import React, { useCallback } from "react"; +import css from "./ProductDescription.module.less"; +import { $L, removeSpecificTags } from "../../../../utils/helperMethods"; +import Spottable from "@enact/spotlight/Spottable"; +import SpotlightContainerDecorator from "@enact/spotlight/SpotlightContainerDecorator"; +import Spotlight from "@enact/spotlight"; +// TVerticalPagenator ์ œ๊ฑฐ๋จ - TScrollerNew์™€ ์ถฉ๋Œ ๋ฌธ์ œ๋กœ ์ธํ•ด + +const SpottableComponent = Spottable("div"); + +const Container = SpotlightContainerDecorator( + { + enterTo: "default-element", + leaveFor: { + left: "spotlight-product-info-section-container" + } + }, + "div" +); + +export default function ProductDescription({ productInfo }) { + const productDescription = useCallback(() => { + const sanitizedString = removeSpecificTags(productInfo?.prdtDesc); + return { __html: sanitizedString }; + }, [productInfo?.prdtDesc]); + + // ์™ผ์ชฝ ํ™”์‚ดํ‘œ ํ‚ค ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ + const handleKeyDown = useCallback((ev) => { + if (ev.keyCode === 37) { // ์™ผ์ชฝ ํ™”์‚ดํ‘œ ํ‚ค + ev.preventDefault(); + ev.stopPropagation(); + console.log("[ProductDescription] Left arrow pressed, focusing product-details-button"); + Spotlight.focus("product-details-button"); + } + }, []); + + // ProductDescription: Container ์ง์ ‘ ์‚ฌ์šฉ ํŒจํ„ด + // prdtDesc๊ฐ€ ์—†์œผ๋ฉด ๋ Œ๋”๋งํ•˜์ง€ ์•Š์Œ + if (!productInfo?.prdtDesc) { + return null; + } + + return ( + + {/* console.log("[ProductDescription] Title clicked")} + onFocus={() => console.log("[ProductDescription] Title focused")} + onBlur={() => console.log("[ProductDescription] Title blurred")} + > */} +
+
{$L("DESCRIPTION")}
+
+ + {/*
*/} + + console.log("[ProductDescription] Content clicked")} + onFocus={() => console.log("[ProductDescription] Content focused")} + onBlur={() => console.log("[ProductDescription] Content blurred")} + onKeyDown={handleKeyDown} + > +
+ + + ); +} diff --git a/com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/ProductDescription/ProductDescription.module.less b/com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/ProductDescription/ProductDescription.module.less new file mode 100644 index 00000000..d465ee52 --- /dev/null +++ b/com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/ProductDescription/ProductDescription.module.less @@ -0,0 +1,54 @@ +@import "../../../../style/CommonStyle.module.less"; +@import "../../../../style/utils.module.less"; + +.descriptionContainer { + position: relative; + top: -1px; + width: calc(100% - 5px); + margin-left: 5px; + max-width: none; + height: 100%; + box-sizing: border-box; + color: rgba(234, 234, 234, 1); +} + +.titleWrapper { + width: 100%; + padding: 10px; + border-radius: 6px; + cursor: pointer; + + &:focus { + background-color: rgba(255, 255, 255, 0.1); + outline: 2px solid @PRIMARY_COLOR_RED; + } + + .title { + .font(@fontFamily: @baseFont, @fontSize: 30px); + font-weight: 700; + margin: 90px 0 20px 0; + } +} + +.descriptionWrapper { + width: 100%; + border-radius: 12px; + cursor: pointer; + + &:focus { + outline: 6px solid @PRIMARY_COLOR_RED; + outline-offset: 2px; + background-color: rgba(255, 255, 255, 0.05); + } + + .productDescription { + width: 100%; + border-radius: 12px; + max-height: calc(100% - 150px); + .font(@fontFamily: @baseFont, @fontSize: 26px); + font-weight: 400; + padding: 30px; + background-color: rgba(51, 51, 51, 1); + overflow-y: auto; + } +} diff --git a/com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/ProductDescription/package.json b/com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/ProductDescription/package.json new file mode 100644 index 00000000..cf808315 --- /dev/null +++ b/com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/ProductDescription/package.json @@ -0,0 +1,6 @@ +{ + "main": "ProductDescription.jsx", + "styles": [ + "ProductDescription.module.less" + ] +} diff --git a/com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/ProductDetail/ProductDetail.new.jsx b/com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/ProductDetail/ProductDetail.new.jsx new file mode 100644 index 00000000..9a698ef8 --- /dev/null +++ b/com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/ProductDetail/ProductDetail.new.jsx @@ -0,0 +1,96 @@ +import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import css from "./ProductDetail.new.module.less"; +import TVirtualGridList from "../../../../components/TVirtualGridList/TVirtualGridList"; +import Spottable from "@enact/spotlight/Spottable"; +import CustomImage from "../../../../components/CustomImage/CustomImage"; +import indicatorDefaultImage from "../../../../../assets/images/img-thumb-empty-144@3x.png"; +import useScrollTo from "../../../../hooks/useScrollTo"; +import SpotlightContainerDecorator from "@enact/spotlight/SpotlightContainerDecorator"; +// TVerticalPagenator ์ œ๊ฑฐ๋จ - TScrollerNew์™€ ์ถฉ๋Œ ๋ฌธ์ œ๋กœ ์ธํ•ด +import { removeSpecificTags } from "../../../../utils/helperMethods"; +import Spotlight from "@enact/spotlight"; + +const Container = SpotlightContainerDecorator( + { + enterTo: "last-focused", + preserveld: true, + leaveFor: { + left: "spotlight-product-info-section-container" + } + }, + "div" +); + +const SpottableComponent = Spottable("div"); + +export default function ProductDetail({ productInfo }) { + const { getScrollTo, scrollTop } = useScrollTo(); + const imageRef = useRef(null); + const containerRef = useRef(null); + + // ๋‹จ์ผ ์ด๋ฏธ์ง€ ๋ชจ๋“œ: singleImage๊ฐ€ ์žˆ์œผ๋ฉด ๊ทธ๊ฒƒ๋งŒ ์‚ฌ์šฉ, ์—†์œผ๋ฉด ๊ธฐ์กด ๋ฐฉ์‹ + const listImages = useMemo(() => { + // ๋‹จ์ผ ์ด๋ฏธ์ง€ ๋ชจ๋“œ + if (productInfo?.singleImage) { + return [productInfo.singleImage]; + } + + // ๊ธฐ์กด ๋ฐฉ์‹ (ํด๋ฐฑ) + const images = [...(productInfo?.imgUrls600 || [])]; + if (images.length === 0) { + return [indicatorDefaultImage]; + } + return images; + }, [productInfo?.singleImage, productInfo?.imgUrls600]); + + // ๋ฉ”์ธ ์ด๋ฏธ์ง€ ์˜์—ญ ํฌ์ปค์Šค ํ•ธ๋“ค๋Ÿฌ + const onFocus = useCallback(() => { + const imageIndex = productInfo?.imageIndex ?? 0; + console.log(`[ProductDetail] Image ${imageIndex + 1} focused`); + }, [productInfo?.imageIndex]); + + const onBlur = useCallback(() => { + const imageIndex = productInfo?.imageIndex ?? 0; + console.log(`[ProductDetail] Image ${imageIndex + 1} blurred`); + }, [productInfo?.imageIndex]); + + // ๋‹จ์ผ ์ด๋ฏธ์ง€ ๋ Œ๋”๋ง (ํ•ญ์ƒ ํ•˜๋‚˜์˜ ์ด๋ฏธ์ง€๋งŒ) + const renderSingleImage = useCallback(() => { + const image = listImages[0] || indicatorDefaultImage; + const imageIndex = productInfo?.imageIndex ?? 0; + const totalImages = productInfo?.totalImages ?? listImages.length; + + return ( +
+ +
+ ); + }, [listImages, productInfo?.imageIndex, productInfo?.totalImages]); + + const imageIndex = productInfo?.imageIndex ?? 0; + const totalImages = productInfo?.totalImages ?? listImages.length; + + return ( + + {/* ๋ฉ”์ธ ์ด๋ฏธ์ง€ ์˜์—ญ - ๋‹จ์ผ ์ด๋ฏธ์ง€ ํ‘œ์‹œ */} + + {renderSingleImage()} + + + ); +} diff --git a/com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/ProductDetail/ProductDetail.new.module.less b/com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/ProductDetail/ProductDetail.new.module.less new file mode 100644 index 00000000..a70d66cf --- /dev/null +++ b/com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/ProductDetail/ProductDetail.new.module.less @@ -0,0 +1,87 @@ +@import "../../../../style/CommonStyle.module.less"; +@import "../../../../style/utils.module.less"; + +// ๋‹จ์ผ ์ด๋ฏธ์ง€์šฉ ProductDetail ์ปจํ…Œ์ด๋„ˆ - ๋ช…ํ™•ํ•œ 1054px ํฌ๊ธฐ +.rollingWrap { + position: relative; + width: 1124px; // ๊ณ ์ • ํฌ๊ธฐ (Description, UserReviews์™€ ๋™์ผ) + max-width: 1124px; + height: 680px; // ๊ณ ์ • ๋†’์ด๋กœ ๊ฐ ์ด๋ฏธ์ง€๊ฐ€ ํ•œ ํ™”๋ฉด ์ฐจ์ง€ + background-color: rgba(255, 255, 255, 1); + border-radius: 12px; + margin-bottom: 30px; // ๋‹ค์Œ ProductDetail๊ณผ์˜ ๊ฐ„๊ฒฉ + box-sizing: border-box; + padding: 6px; // ํฌ์ปค์Šค ํ…Œ๋‘๋ฆฌ(6px)๋ฅผ ์œ„ํ•œ ์ •ํ™•ํ•œ ๊ณต๊ฐ„ ํ™•๋ณด + + &:last-child { + margin-bottom: 0; // ๋งˆ์ง€๋ง‰ ์ด๋ฏธ์ง€๋Š” ํ•˜๋‹จ ๋งˆ์ง„ ์ œ๊ฑฐ + } + + // ๋ฉ”์ธ ์ด๋ฏธ์ง€ ์˜์—ญ - ๋‹จ์ผ ์ด๋ฏธ์ง€ ์ค‘์•™ ๋ฐฐ์น˜ + .itemBox { + width: 100%; + height: 100%; + position: relative; + text-align: center; + display: flex; + align-items: center; + justify-content: center; + + &:focus { + &::after { + .focused(@boxShadow: 22px, @borderRadius: 12px); + border: 6px solid @PRIMARY_COLOR_RED; + // ๋ถ€๋ชจ์˜ padding(6px)์— ์ •ํ™•ํžˆ ๋งž์ถ”๊ธฐ ์œ„ํ•ด ์˜คํ”„์…‹์„ -6px๋กœ ์„ค์ • + top: -6px; + right: -6px; + bottom: -6px; + left: -6px; + } + } + } + + // ํ™”์‚ดํ‘œ ๋ฒ„ํŠผ ์Šคํƒ€์ผ (RollingUnit ํŒจํ„ด, ์ƒˆ๋กœ์šด ํฌ๊ธฐ์— ๋งž์ถฐ ์กฐ์ •) + .arrow { + z-index: 9999; + .size(@w: 42px, @h: 42px); + background-size: 42px 42px; + background-position: center center; + + &.leftBtn { + .position(@position: absolute, @top: 349px, @left: 43px); // ๋†’์ด ์ค‘์•™: (740/2 - 21) = 349px, ์ขŒ์ธก: 30px + 13px = 43px + background-image: url("../../../../../assets/images/btn/btn_prev_thumb_nor.png"); + &:focus { + background-image: url("../../../../../assets/images/btn/btn_prev_thumb_foc.png"); + } + } + + &.rightBtn { + .position(@position: absolute, @top: 349px, @right: 43px); // ๋†’์ด ์ค‘์•™: 349px, ์šฐ์ธก: 30px + 13px = 43px + background-image: url("../../../../../assets/images/btn/btn_next_thumb_nor.png"); + &:focus { + background-image: url("../../../../../assets/images/btn/btn_next_thumb_foc.png"); + } + } + } + + .thumbnailWrapper { + position: relative; + width: 100%; + height: 100%; // ๋ถ€๋ชจ itemBox์˜ 680px ์ „์ฒด ์‚ฌ์šฉ + display: flex; + align-items: center; + justify-content: center; + + .productImage { + max-width: 100%; + max-height: 100%; + margin: 0; + padding: 0; + border: none; + object-fit: contain; // ๋น„์œจ ์œ ์ง€ํ•˜๋ฉฐ ์ปจํ…Œ์ด๋„ˆ์— ๋งž์ถค + background-color: @COLOR_WHITE; + border-radius: 8px; + transition: all 0.2s ease; + } + } +} diff --git a/com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/UserReviews/CustomerImages/CustomerImages.jsx b/com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/UserReviews/CustomerImages/CustomerImages.jsx new file mode 100644 index 00000000..ae624a44 --- /dev/null +++ b/com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/UserReviews/CustomerImages/CustomerImages.jsx @@ -0,0 +1,268 @@ +import React, { useCallback, useEffect, useState } from "react"; +import css from "./CustomerImages.module.less"; +import SpotlightContainerDecorator from "@enact/spotlight/SpotlightContainerDecorator"; +import { useSelector } from "react-redux"; +import Spottable from "@enact/spotlight/Spottable"; +import THeader from "../../../../../components/THeader/THeader"; +import { $L } from "../../../../../utils/helperMethods"; +import classNames from "classnames"; +import Spotlight from "@enact/spotlight"; + +const Container = SpotlightContainerDecorator( + { + enterTo: "default-element", + preserveld: true, + leaveFor: { + left: "spotlight-product-info-section-container" + } + }, + "div" +); + +const SpottableComponent = Spottable("div"); + +export default function CustomerImages({ onImageClick }) { + // Redux์—์„œ reviewData ์ „์ฒด๋ฅผ ๊ฐ€์ ธ์˜ด + const reviewData = useSelector((state) => state.product.reviewData); + const reviewListData = reviewData?.reviewList; + + const [imageList, setImageList] = useState([]); + const [selectedIndex, setSelectedIndex] = useState(null); + const [currentPage, setCurrentPage] = useState(1); + const IMAGES_PER_PAGE = 5; + + // [UserReviews] CustomerImages ๋ฐ์ดํ„ฐ ์ˆ˜์‹  ํ™•์ธ - ๊ฐœ์„ ๋œ ๋กœ๊น… + useEffect(() => { + console.log("[UserReviews] CustomerImages - Full review data received:", { + reviewData, + reviewListData, + hasData: reviewData && reviewListData && reviewListData.length > 0, + reviewCount: reviewListData?.length || 0 + }); + }, [reviewData, reviewListData]); + + // ์ด๋ฏธ์ง€ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ ๋กœ์ง ๊ฐœ์„  + useEffect(() => { + console.log("[UserReviews] CustomerImages - Processing review data:", { + reviewListData, + reviewListType: Array.isArray(reviewListData) ? 'array' : typeof reviewListData + }); + + if (!reviewListData || !Array.isArray(reviewListData)) { + console.log("[UserReviews] CustomerImages - No valid review data available"); + setImageList([]); + return; + } + + // ๊ฐ ๋ฆฌ๋ทฐ์˜ ๊ตฌ์กฐ๋ฅผ ์ž์„ธํžˆ ๋กœ๊น… + reviewListData.forEach((review, index) => { + console.log(`[UserReviews] CustomerImages - Review ${index} structure:`, { + rvwId: review.rvwId, + hasReviewImageList: !!review.reviewImageList, + reviewImageListType: Array.isArray(review.reviewImageList) ? 'array' : typeof review.reviewImageList, + reviewImageListLength: review.reviewImageList?.length || 0, + reviewImageList: review.reviewImageList + }); + }); + + // ์ด๋ฏธ์ง€๊ฐ€ ์žˆ๋Š” ๋ฆฌ๋ทฐ๋งŒ ํ•„ํ„ฐ๋ง + const imageReviews = reviewListData.filter( + (review) => review.reviewImageList && + Array.isArray(review.reviewImageList) && + review.reviewImageList.length > 0 + ); + + console.log("[UserReviews] CustomerImages - Reviews with images:", { + totalReviews: reviewListData.length, + imageReviewsCount: imageReviews.length, + imageReviews: imageReviews + }); + + const images = []; + + // ๊ฐ ๋ฆฌ๋ทฐ์˜ ์ด๋ฏธ์ง€๋“ค์„ ์ˆ˜์ง‘ + imageReviews.forEach((review, reviewIndex) => { + console.log(`[UserReviews] CustomerImages - Processing review ${reviewIndex}:`, { + rvwId: review.rvwId, + imageCount: review.reviewImageList?.length || 0 + }); + + if (review.reviewImageList && Array.isArray(review.reviewImageList)) { + review.reviewImageList.forEach((imgItem, imgIndex) => { + const { imgId, imgUrl, imgSeq } = imgItem; + console.log(`[UserReviews] CustomerImages - Adding image ${imgIndex}:`, { + imgId, + imgSeq, + imgUrl, + isValidUrl: !!imgUrl && imgUrl !== '', + urlType: typeof imgUrl, + urlLength: imgUrl?.length || 0 + }); + + // ์œ ํšจํ•œ ์ด๋ฏธ์ง€ URL๋งŒ ์ถ”๊ฐ€ + if (imgUrl && imgUrl.trim() !== '') { + images.push({ + imgId: imgId || `img-${reviewIndex}-${imgIndex}`, + imgUrl, + imgSeq: imgSeq || imgIndex + 1, + reviewId: review.rvwId + }); + } else { + console.warn(`[UserReviews] CustomerImages - Skipping invalid image URL:`, imgUrl); + } + }); + } + }); + + console.log("[UserReviews] CustomerImages - Final image list:", { + totalImages: images.length, + images: images + }); + + setImageList(images); + }, [reviewListData]); + + // ์ด๋ฏธ์ง€ ๋ชฉ๋ก์ด ๋ณ€๊ฒฝ๋˜๋ฉด ํŽ˜์ด์ง€๋ฅผ ์ดˆ๊ธฐํ™” + useEffect(() => { + setCurrentPage(1); + }, [imageList]); + + const handleReviewImageClick = (index) => { + console.log("[UserReviews] CustomerImages - Image clicked at index:", index, { + imageData: imageList[index] + }); + setSelectedIndex(index); + + // ๋ถ€๋ชจ ์ปดํฌ๋„ŒํŠธ์— ํŒ์—… ์—ด๊ธฐ ์ด๋ฒคํŠธ ์ „๋‹ฌ + if (onImageClick) { + onImageClick(index); + } + }; + + const handleViewMoreClick = () => { + console.log("[UserReviews] CustomerImages - View more clicked", { + currentPage, + totalImages: imageList.length, + totalPages: Math.ceil(imageList.length / IMAGES_PER_PAGE) + }); + setCurrentPage(prev => prev + 1); + }; + + // ํ‚ค ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ (์™ผ์ชฝ ํ™”์‚ดํ‘œ, Enter ํ‚ค) + const handleKeyDown = useCallback((ev, index) => { + if (ev.keyCode === 37) { // ์™ผ์ชฝ ํ™”์‚ดํ‘œ ํ‚ค + ev.preventDefault(); + ev.stopPropagation(); + console.log("[CustomerImages] Left arrow pressed, focusing product-details-button"); + Spotlight.focus("product-details-button"); + } else if (ev.keyCode === 13) { // Enter ํ‚ค + ev.preventDefault(); + ev.stopPropagation(); + console.log("[CustomerImages] Enter pressed on image:", index); + handleReviewImageClick(index); + } + }, [handleReviewImageClick]); + + // ์ด๋ฏธ์ง€๊ฐ€ ์—†์„ ๋•Œ๋„ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋ Œ๋”๋งํ•˜๋˜ ๋‚ด์šฉ์€ ํ‘œ์‹œํ•˜์ง€ ์•Š์Œ + return ( + <> + + + {imageList && imageList.length > 0 ? ( +
+ {(() => { + const startIndex = (currentPage - 1) * IMAGES_PER_PAGE; + const endIndex = startIndex + IMAGES_PER_PAGE; + const displayImages = imageList.slice(startIndex, endIndex); + const hasMoreImages = imageList.length > endIndex; + + console.log("[CustomerImages] Pagination debug:", { + currentPage, + IMAGES_PER_PAGE, + totalImages: imageList.length, + startIndex, + endIndex, + displayImagesCount: displayImages.length, + hasMoreImages + }); + + return ( + <> + {displayImages.map((reviewImage, displayIndex) => { + const actualIndex = startIndex + displayIndex; + const { imgId, imgUrl, reviewId } = reviewImage; + + return ( + handleReviewImageClick(actualIndex)} + onKeyDown={(ev) => handleKeyDown(ev, actualIndex)} + spotlightId={`customer-image-${actualIndex}`} + > + {`Review { + console.log(`[UserReviews] CustomerImages - Image loaded successfully:`, { + index: actualIndex, + imgUrl, + imgId + }); + }} + onError={(e) => { + console.error(`[UserReviews] CustomerImages - Image load failed:`, { + index: actualIndex, + imgUrl, + imgId, + error: e.target.error + }); + e.target.style.display = 'none'; + }} + /> + + ); + })} + + {hasMoreImages && ( + { + if (ev.keyCode === 13) { + ev.preventDefault(); + ev.stopPropagation(); + handleViewMoreClick(); + } + }} + spotlightId="customer-images-view-more" + > +
+
+View More
+
+
+ )} + + ); + })()} +
+ ) : ( +
+
+ {reviewData ? 'No customer images available' : 'Loading customer images...'} +
+
+ )} +
+ + ); +} diff --git a/com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/UserReviews/CustomerImages/CustomerImages.module.less b/com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/UserReviews/CustomerImages/CustomerImages.module.less new file mode 100644 index 00000000..da6f1108 --- /dev/null +++ b/com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/UserReviews/CustomerImages/CustomerImages.module.less @@ -0,0 +1,97 @@ +@import "../../../../../style/CommonStyle.module.less"; +@import "../../../../../style/utils.module.less"; + +.container { + width: 1124px; + height: 236px; + max-width: 1124px; + display: flex; + flex-direction: column; + + .tHeader { + .size(@w: 100%,@h:36px); + background: transparent; + margin: 0 0 10px 0; + + > div { + padding: 0; + > span { + .font(@fontFamily: @baseFont, @fontSize: 24px); + font-weight: 400; + color: rgba(255, 255, 255, 1); + text-transform: none; + } + } + } + + .wrapper { + width: 100%; + height: 190px; + display: flex; + justify-content: flex-start; + align-items: center; + background-color: rgba(51, 51, 51, 0.95); + padding: 20px; + border-radius: 12px; + overflow: visible; + + .reviewCard { + width: calc((100% - 75px) / 6); // 6๊ฐœ ์Šฌ๋กฏ(5์ด๋ฏธ์ง€+1๋ฒ„ํŠผ), margin-right 15px * 5 = 75px + height: 150px; + position: relative; + flex-shrink: 0; + margin-right: 15px; + + &:focus { + &::after { + .focused(@boxShadow:22px, @borderRadius:12px); + } + } + + .reviewImg { + width: 100%; + height: 100%; + border-radius: 12px; + object-fit: cover; + } + } + + .lastReviewImage { + margin-right: 0; // ๋งˆ์ง€๋ง‰ ์ด๋ฏธ์ง€์˜ ์˜ค๋ฅธ์ชฝ ๋งˆ์ง„ ์ œ๊ฑฐ + } + + .viewMoreButton { + width: calc((100% - 75px) / 6); // reviewCard์™€ ๋™์ผํ•œ ํฌ๊ธฐ + height: 150px; + position: relative; + flex-shrink: 0; + cursor: pointer; + + &:focus { + &::after { + .focused(@boxShadow:22px, @borderRadius:12px); + } + } + + .viewMoreContent { + width: 100%; + height: 100%; + padding: 4px; + background: linear-gradient(0deg, rgba(0, 0, 0, 0.80) 0%, rgba(0, 0, 0, 0.80) 100%); + border-radius: 12px; + display: flex; + justify-content: center; + align-items: center; + + .viewMoreText { + color: white; + font-size: 20px; + font-weight: 700; + line-height: 31px; + text-align: center; + font-family: @baseFont; + } + } + } + } +} diff --git a/com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/UserReviews/UserReviews.jsx b/com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/UserReviews/UserReviews.jsx new file mode 100644 index 00000000..e3eaba9b --- /dev/null +++ b/com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/UserReviews/UserReviews.jsx @@ -0,0 +1,240 @@ +import React, { useCallback, useEffect, useRef, useState } from "react"; +import css from "./UserReviews.module.less"; +import TScroller from "../../../../components/TScroller/TScroller"; +import useScrollTo from "../../../../hooks/useScrollTo"; +import THeader from "../../../../components/THeader/THeader"; +import { $L } from "../../../../utils/helperMethods"; +import { useMemo } from "react"; +import Spottable from "@enact/spotlight/Spottable"; +import SpotlightContainerDecorator from "@enact/spotlight/SpotlightContainerDecorator"; +import { useDispatch, useSelector } from "react-redux"; +import { getUserReviews } from "../../../../actions/productActions"; +import StarRating from "../../components/StarRating"; +import CustomerImages from "./CustomerImages/CustomerImages"; +import UserReviewsPopup from "./UserReviewsPopup/UserReviewsPopup"; + +const SpottableComponent = Spottable("div"); + +const Container = SpotlightContainerDecorator( + { + enterTo: "last-focused", + preserveld: true, + leaveFor: { + left: "spotlight-product-info-section-container" + } + }, + "div" +); + +export default function UserReviews({ productInfo, panelInfo }) { + const { getScrollTo, scrollTop } = useScrollTo(); + const dispatch = useDispatch(); + const containerRef = useRef(null); + + // ํŒ์—… ์ƒํƒœ ๊ด€๋ฆฌ + const [isPopupOpen, setIsPopupOpen] = useState(false); + const [selectedImageIndex, setSelectedImageIndex] = useState(0); + const reviewListData = useSelector( + (state) => state.product.reviewData && state.product.reviewData.reviewList + ); + const reviewTotalCount = useSelector( + (state) => { + const reviewData = state.product.reviewData; + return reviewData && reviewData.reviewDetail && reviewData.reviewDetail.totRvwCnt ? reviewData.reviewDetail.totRvwCnt : 0; + } + ); + const reviewDetailData = useSelector( + (state) => state.product.reviewData && state.product.reviewData.reviewDetail + ); + + // [UserReviews] ๋ฐ์ดํ„ฐ ์ˆ˜์‹  ํ™•์ธ ๋กœ๊ทธ + useEffect(() => { + console.log("[UserReviews] Review data received:", { + reviewListData, + reviewTotalCount, + reviewDetailData, + hasData: reviewListData && reviewListData.length > 0 + }); + }, [reviewListData, reviewTotalCount, reviewDetailData]); + + // UserReviews: Container ์ง์ ‘ ์‚ฌ์šฉ ํŒจํ„ด (TScroller ์ค‘๋ณต ์ œ๊ฑฐ) + + // ์‹ค์ œ ์ƒํ’ˆ ID๋ฅผ ์‚ฌ์šฉํ•ด์„œ ๋ฆฌ๋ทฐ ๋ฐ์ดํ„ฐ ์š”์ฒญ (panelInfo์—์„œ prdtId ์šฐ์„  ์‚ฌ์šฉ) + useEffect(() => { + const productId = (panelInfo && panelInfo.prdtId) || (productInfo && productInfo.prdtId); + + console.log("[UserReviews] useEffect triggered - Product ID check:", { + panelInfo_prdtId: panelInfo && panelInfo.prdtId, + productInfo_prdtId: productInfo && productInfo.prdtId, + finalProductId: productId, + panelInfo: panelInfo, + productInfo: productInfo + }); + + if (productId) { + console.log("[UserReviews] โœ… API ํ˜ธ์ถœ ์‹œ์ž‘:", { + prdtId: productId, + timestamp: new Date().toISOString() + }); + dispatch(getUserReviews({ prdtId: productId })); + } else { + console.error("[UserReviews] โŒ API ํ˜ธ์ถœ ์‹คํŒจ - prdtId ์—†์Œ:", { + panelInfo_exists: !!panelInfo, + productInfo_exists: !!productInfo, + panelInfo_prdtId: panelInfo && panelInfo.prdtId, + productInfo_prdtId: productInfo && productInfo.prdtId, + panelInfo_keys: panelInfo ? Object.keys(panelInfo) : [], + productInfo_keys: productInfo ? Object.keys(productInfo) : [] + }); + } + }, [dispatch, panelInfo && panelInfo.prdtId, productInfo && productInfo.prdtId]); + + const formatToYYMMDD = (dateStr) => { + const date = new Date(dateStr); + const iso = date.toISOString().slice(2, 10); + return iso.replace(/-/g, "."); + }; + + const handleReviewClick = useCallback(() => { + console.log("[UserReviews] Review item clicked"); + }, []); + + // ํŒ์—… ๊ด€๋ จ ํ•ธ๋“ค๋Ÿฌ๋“ค + const handleOpenPopup = useCallback((imageIndex = 0) => { + console.log("[UserReviews] Opening popup with image index:", imageIndex); + setSelectedImageIndex(imageIndex); + setIsPopupOpen(true); + }, []); + + const handleClosePopup = useCallback(() => { + console.log("[UserReviews] Closing popup"); + setIsPopupOpen(false); + }, []); + + const handleImageClick = useCallback((index, image) => { + console.log("[UserReviews] Popup image clicked:", { index, image }); + setSelectedImageIndex(index); + }, []); + + // ์ด๋ฏธ์ง€ ๋ฐ์ดํ„ฐ ๊ฐ€๊ณต (CustomerImages์™€ ๋™์ผํ•œ ๋กœ์ง) + const customerImages = useMemo(() => { + if (!reviewListData || !Array.isArray(reviewListData)) { + return []; + } + + const imageReviews = reviewListData.filter( + (review) => review.reviewImageList && + Array.isArray(review.reviewImageList) && + review.reviewImageList.length > 0 + ); + + const images = []; + imageReviews.forEach((review, reviewIndex) => { + if (review.reviewImageList && Array.isArray(review.reviewImageList)) { + review.reviewImageList.forEach((imgItem, imgIndex) => { + const { imgId, imgUrl, imgSeq } = imgItem; + if (imgUrl && imgUrl.trim() !== '') { + images.push({ + imgId: imgId || `img-${reviewIndex}-${imgIndex}`, + imgUrl, + imgSeq: imgSeq || imgIndex + 1, + reviewId: review.rvwId + }); + } + }); + } + }); + + return images; + }, [reviewListData]); + + return ( + + + + {reviewDetailData && reviewDetailData.totRvwAvg && ( + + )} + + +
+
+ {$L( + `Showing ${reviewListData ? reviewListData.length : 0} out of ${reviewTotalCount} reviews` + )} +
+ {reviewListData && + reviewListData.map((review, index) => { + const { reviewImageList, rvwRtng, rvwRgstDtt, rvwCtnt, rvwId, wrtrNknm, rvwWrtrId } = + review; + console.log(`[UserReviews] Rendering review ${index}:`, { rvwId, hasImages: reviewImageList && reviewImageList.length > 0 }); + return ( + + {reviewImageList && reviewImageList.length > 0 && ( + + )} +
+
+ {rvwRtng && ( + + )} + {(wrtrNknm || rvwWrtrId) && ( + + {wrtrNknm || rvwWrtrId} + + )} + {rvwRgstDtt && ( + + {formatToYYMMDD(rvwRgstDtt)} + + )} +
+ {rvwCtnt && ( +
{rvwCtnt}
+ )} +
+
+ ); + })} +
+
+ + {/* UserReviewsPopup ์ถ”๊ฐ€ */} + +
+ ); +} diff --git a/com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/UserReviews/UserReviews.module.less b/com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/UserReviews/UserReviews.module.less new file mode 100644 index 00000000..9b559131 --- /dev/null +++ b/com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/UserReviews/UserReviews.module.less @@ -0,0 +1,111 @@ +@import "../../../../style/CommonStyle.module.less"; +@import "../../../../style/utils.module.less"; + +.tScroller { + .size(@w: 1124px, @h: 100%); // ๋งˆ์ง„ ํฌํ•จ ์ „์ฒด ํฌ๊ธฐ (1054px + 60px) + max-width: 1124px; + padding: 0; + box-sizing: border-box; +} + +.userReviewsContainer { + width: 100%; +} + +.tHeader { + background: transparent; + .size(@w: 1020px, @h: 36px); // CustomerImages์™€ ์ผ์น˜ํ•˜๋„๋ก ํฌ๊ธฐ ์กฐ์ • + max-width: 1020px; + margin-bottom: 20px; + + > div { + .size(@w:100%,@h:100%); + padding: 0; + } + + .averageOverallRating { + .size(@w: 176px,@h:30px); + } + + span { + .font(@fontFamily: @baseFont, @fontSize: 30px); + font-weight: 700; + height: 36px; + color: rgba(234, 234, 234, 1); + } +} + +.reviewItem { + width: 100%; + height: 100%; + + .showReviewsText { + .size(@w:100%, @h:36px); + .font(@fontFamily: @baseFont, @fontSize: 24px); + font-weight: 400; + margin-bottom: 10px; + padding: 5px 0 0 20px; + } + + .reviewContentContainer { + .size(@w:100%, @h:168px); + background-color: rgba(51, 51, 51, 0.95); + border-radius: 12px; + margin-bottom: 10px; + .flex(@justifyCenter:flex-start); + padding: 30px; + position: relative; + + &:focus { + &::after { + .focused(@boxShadow:22px, @borderRadius:12px); + } + } + + .reviewThumbnail { + .size(@w: 108px,@h:108px); + border-radius: 12px; + margin-right: 15px; + } + + .reviewContent { + .size(@w: 100%,@h:108px); + + .reviewMeta { + .size(@w:100%, @h:31px); + display: flex; + justify-content: flex-start; + align-items: center; + + > * { + margin-right: 20px; + + &:last-child { + margin-right: 0; + } + } + + .reviewAuthor { + color: rgba(176, 176, 176, 1); + .font(@fontFamily: @baseFont, @fontSize: 22px); + font-weight: 400; + } + + .reviewDate { + color: rgba(234, 234, 234, 1); + .font(@fontFamily: @baseFont, @fontSize: 24px); + font-weight: 400; + margin-left: auto; + } + } + + .reviewText { + .font(@fontFamily: @baseFont, @fontSize: 24px); + font-weight: 400; + .elip(@clamp:2); + color: rgba(234, 234, 234, 1); + margin-top: 15px; + } + } + } +} \ No newline at end of file diff --git a/com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/UserReviews/UserReviewsPopup/UserReviewsPopup.figma.jsx b/com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/UserReviews/UserReviewsPopup/UserReviewsPopup.figma.jsx new file mode 100644 index 00000000..9d608bb9 --- /dev/null +++ b/com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/UserReviews/UserReviewsPopup/UserReviewsPopup.figma.jsx @@ -0,0 +1,33 @@ +
+
+
Customer Imagesย 
+
+
+
+
+ +
+ + + + + + + + + + +
+
+View More
+
+
+
+
+
+
+
+
+
CLOSE
+
+
+
\ No newline at end of file diff --git a/com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/UserReviews/UserReviewsPopup/UserReviewsPopup.jsx b/com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/UserReviews/UserReviewsPopup/UserReviewsPopup.jsx new file mode 100644 index 00000000..c13b80e5 --- /dev/null +++ b/com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/UserReviews/UserReviewsPopup/UserReviewsPopup.jsx @@ -0,0 +1,104 @@ +import React, { useCallback } from "react"; +import classNames from "classnames"; +import Spottable from "@enact/spotlight/Spottable"; +import SpotlightContainerDecorator from "@enact/spotlight/SpotlightContainerDecorator"; +import TNewPopUp from "../../../../../components/TPopUp/TNewPopUp"; +import TButton from "../../../../../components/TButton/TButton"; +import { $L } from "../../../../../utils/helperMethods"; +import css from "./UserReviewsPopup.module.less"; + +const SpottableImage = Spottable("div"); + +const ContentContainer = SpotlightContainerDecorator( + { + enterTo: "default-element", + preserveId: true, + defaultElement: "user-review-image-0" + }, + "div" +); + +const FooterContainer = SpotlightContainerDecorator( + { + enterTo: "default-element", + preserveId: true, + defaultElement: "close-button" + }, + "div" +); + +export default function UserReviewsPopup({ + open = false, + onClose, + images = [], + selectedImageIndex = 0, + onImageClick, + className, +}) { + const handleClose = useCallback(() => { + if (onClose) { + onClose(); + } + }, [onClose]); + + const handleImageClick = useCallback((index, image) => { + if (onImageClick) { + onImageClick(index, image); + } + }, [onImageClick]); + + // ์ตœ๋Œ€ 8๊ฐœ ์ด๋ฏธ์ง€๋งŒ ํ‘œ์‹œ + const displayImages = images.slice(0, 8); + + return ( + +
+ {/* Header */} +
+
+ {$L("Customer Images")} +
+
+ + {/* Content */} + +
+ {displayImages.map((image, index) => ( + handleImageClick(index, image)} + > + {`Customer + + ))} +
+
+ + {/* Footer */} + + + {$L("CLOSE")} + + +
+
+ ); +} \ No newline at end of file diff --git a/com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/UserReviews/UserReviewsPopup/UserReviewsPopup.module.less b/com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/UserReviews/UserReviewsPopup/UserReviewsPopup.module.less new file mode 100644 index 00000000..15205038 --- /dev/null +++ b/com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/UserReviews/UserReviewsPopup/UserReviewsPopup.module.less @@ -0,0 +1,201 @@ +@import "../../../../../style/CommonStyle.module.less"; +@import "../../../../../style/utils.module.less"; + +.userReviewsPopup { + .popupContainer { + width: 1060px; + height: 797px; + background: white; + border-radius: 12px; + overflow: hidden; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + + // Header ์˜์—ญ + .header { + align-self: stretch; + padding: 30px; + background: #E7EBEF; + display: flex; + justify-content: flex-start; + align-items: center; + + .headerTitle { + text-align: center; + color: black; + font-size: 42px; + font-family: @baseFont; + font-weight: 700; + line-height: 42px; + word-wrap: break-word; + } + } + + // Content ์˜์—ญ + .content { + align-self: stretch; + height: 557px; + padding: 0; // ํŒจ๋”ฉ ์ œ๊ฑฐ + overflow: hidden; + display: flex; + justify-content: center; // ์ค‘์•™ ์ •๋ ฌ + align-items: center; // ์ค‘์•™ ์ •๋ ฌ + position: relative; + + .imageGrid { + flex: 1; + display: flex; + justify-content: center; // ์ค‘์•™ ์ •๋ ฌ๋กœ ๋ณ€๊ฒฝ + align-items: center; + flex-wrap: wrap; + align-content: center; + overflow: hidden; // ์Šคํฌ๋กค ์™„์ „ ์ œ๊ฑฐ + max-height: 100%; + + // gap ๋Œ€์‹  margin ์‚ฌ์šฉ (TV ํ˜ธํ™˜์„ฑ) + .imageItem { + width: 226px; + height: 218px; + border-radius: 12px; + position: relative; + cursor: pointer; + margin-right: 20px; + margin-bottom: 20px; + + // 3๊ฐœ์”ฉ ๋ฐฐ์น˜ํ•˜๋ฏ€๋กœ 3๋ฒˆ์งธ๋งˆ๋‹ค margin-right ์ œ๊ฑฐ + &:nth-child(3n) { + margin-right: 0; + } + + &:focus { + outline: none; + + &::after { + content: ''; + position: absolute; + top: -2px; // ํฌ์ปค์Šค ์œ„์น˜ ๋ฏธ์„ธ ์กฐ์ • + left: -2px; // ํฌ์ปค์Šค ์œ„์น˜ ๋ฏธ์„ธ ์กฐ์ • + width: calc(100% + 4px); // ํฌ๊ธฐ ๋ฏธ์„ธ ์กฐ์ • + height: calc(100% + 4px); // ํฌ๊ธฐ ๋ฏธ์„ธ ์กฐ์ • + border: 4px solid #C70850; + border-radius: 12px; + pointer-events: none; + } + } + + .image { + width: 100%; + height: 100%; + border-radius: 12px; + object-fit: cover; + padding: 0; + box-sizing: border-box; + } + + // ์„ ํƒ๋œ ์ด๋ฏธ์ง€ ์Šคํƒ€์ผ + &.selectedImage { + &::after { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + border: 4px solid #C70850; + border-radius: 12px; + pointer-events: none; + } + } + } + + // View More ์•„์ดํ…œ + .viewMoreItem { + width: 226px; + height: 218px; + border-radius: 12px; + position: relative; + margin-right: 20px; + margin-bottom: 20px; + + &:nth-child(3n) { + margin-right: 0; + } + + .image { + width: 100%; + height: 100%; + border-radius: 12px; + object-fit: cover; + padding: 4px; + box-sizing: border-box; + } + + .viewMoreOverlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: linear-gradient(0deg, rgba(0, 0, 0, 0.80) 0%, rgba(0, 0, 0, 0.80) 100%); + border-radius: 12px; + display: flex; + justify-content: center; + align-items: center; + + .viewMoreText { + color: white; + font-size: 16px; + font-family: @baseFont; + font-weight: 700; + line-height: 31px; + word-wrap: break-word; + } + } + } + } + + // ์ปค์Šคํ…€ ์Šคํฌ๋กค๋ฐ” (์ˆจ๊น€) + .scrollbar { + display: none; // ์Šคํฌ๋กค๋ฐ” ์™„์ „ ์ˆจ๊น€ + } + } + + // Footer ์˜์—ญ + .footer { + align-self: stretch; + padding: 30px 60px; + display: flex; + justify-content: center; + align-items: center; + + .closeButton { + width: 300px !important; + height: 78px !important; + background: #7A808D !important; + border-radius: 12px !important; + border: none !important; + display: flex !important; + justify-content: center !important; + align-items: center !important; + + color: white !important; + font-size: 30px !important; + font-family: @baseFont !important; + font-weight: 700 !important; + line-height: 30px !important; + text-align: center !important; + + &:focus { + background: @PRIMARY_COLOR_RED !important; + outline: 2px solid @PRIMARY_COLOR_RED !important; + } + + &:hover { + background: lighten(#7A808D, 10%) !important; + } + } + } + } +} \ No newline at end of file diff --git a/com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/UserReviews/package.json b/com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/UserReviews/package.json new file mode 100644 index 00000000..3a2758ac --- /dev/null +++ b/com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/UserReviews/package.json @@ -0,0 +1,6 @@ +{ + "main": "UserReviews.jsx", + "styles": [ + "UserReviews.module.less" + ] +} diff --git a/com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/YouMayAlsoLike/YouMayAlsoLike.jsx b/com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/YouMayAlsoLike/YouMayAlsoLike.jsx new file mode 100644 index 00000000..765429c9 --- /dev/null +++ b/com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/YouMayAlsoLike/YouMayAlsoLike.jsx @@ -0,0 +1,145 @@ +import React, { useCallback, useEffect, useMemo, useRef } from "react"; +import css from "./YouMayAlsoLike.module.less"; +import { $L } from "../../../../utils/helperMethods"; +import TVerticalPagenator from "../../../../components/TVerticalPagenator/TVerticalPagenator"; +import TVirtualGridList from "../../../../components/TVirtualGridList/TVirtualGridList"; +import useScrollTo from "../../../../hooks/useScrollTo"; +import TItemCard from "../../../../components/TItemCard/TItemCard"; +import Spottable from "@enact/spotlight/Spottable"; +import SpotlightContainerDecorator from "@enact/spotlight/SpotlightContainerDecorator"; +import { useDispatch, useSelector } from "react-redux"; +import { + LOG_CONTEXT_NAME, + LOG_MESSAGE_ID, + panel_names, +} from "../../../../utils/Config"; +import THeader from "../../../../components/THeader/THeader"; +import { finishVideoPreview } from "../../../../actions/playActions"; +import { popPanel, pushPanel } from "../../../../actions/panelActions"; +import { clearThemeDetail } from "../../../../actions/homeActions"; +import { Job } from "@enact/core/util"; + +const SpottableComponent = Spottable("div"); + +const Container = SpotlightContainerDecorator( + { + enterTo: "last-focused", + leaveFor: { + left: "spotlight-product-info-section-container" + } + }, + "div" +); + +export default function YouMayAlsoLike({ productInfo, panelInfo }) { + const { getScrollTo, scrollLeft } = useScrollTo(); + const dispatch = useDispatch(); + const focusedContainerIdRef = useRef(null); + + const youmaylikeProductData = useSelector( + (state) => state.main.youmaylikeData + ); + const panels = useSelector((state) => state.panels.panels); + const themeProductInfos = useSelector( + (state) => state.home.themeCurationDetailInfoData + ); + + // const [focused, setFocused] = useState(false); + + const cursorOpen = useRef(new Job((func) => func(), 1000)); + + const launchedFromPlayer = useMemo(() => { + const detailPanelIndex = panels.findIndex( + ({ name }) => name === "detailpanel" + ); + const playerPanelIndex = panels.findIndex( + ({ name }) => name === "playerpanel" + ); + + return detailPanelIndex - 1 === playerPanelIndex; + }, [panels]); + + const onFocusedContainerId = useCallback((containerId) => { + focusedContainerIdRef.current = containerId; + }, []); + + return ( +
+ {youmaylikeProductData && youmaylikeProductData.length > 0 && ( + + + +
+ {youmaylikeProductData?.map((product, index) => { + const { + imgUrl, + patnrId, + prdtId, + prdtNm, + priceInfo, + offerInfo, + patncNm, + brndNm, + lgCatCd, + } = product; + + const handleItemClick = () => { + dispatch(finishVideoPreview()); + dispatch(popPanel(panel_names.DETAIL_PANEL)); + + if (themeProductInfos && themeProductInfos.length > 0) { + dispatch(clearThemeDetail()); + } + dispatch( + pushPanel({ + name: panel_names.DETAIL_PANEL, + panelInfo: { + patnrId, + prdtId, + launchedFromPlayer: launchedFromPlayer, + }, + }) + ); + cursorOpen.current.stop(); + }; + + return ( + + ); + })} +
+
+
+ )} +
+ ); +} diff --git a/com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/YouMayAlsoLike/YouMayAlsoLike.module.less b/com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/YouMayAlsoLike/YouMayAlsoLike.module.less new file mode 100644 index 00000000..25ebe9ef --- /dev/null +++ b/com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/YouMayAlsoLike/YouMayAlsoLike.module.less @@ -0,0 +1,107 @@ +@import "../../../../style/CommonStyle.module.less"; +@import "../../../../style/utils.module.less"; + +// .container { +// .size(@w: 874px,@h:500px); + +// .itemWrapper { +// .size(@w: 874px,@h:500px); +// .item { +// .size(@w: 300px,@h:300px); +// } +// } +// } + +.tVerticalPagenator { + .size(@w: 1114px, @h: auto); // ๋งˆ์ง„ ํฌํ•จ ์ „์ฒด ํฌ๊ธฐ (1054px + 60px) + max-width: 1114px; + padding-left: 30px; // ์ขŒ์ธก 30px ๋งˆ์ง„ + padding-right: 30px; // ์šฐ์ธก 30px ๋งˆ์ง„ + box-sizing: border-box; + + // .sectionTitle { + // .font(@fontFamily: @baseFont, @fontSize: 30px); + // min-height: 56px; + // font-weight: 700; + // color: rgba(234, 234, 234, 1); + // // margin: 30px 0 20px 0; + // } + .tHeader { + background: transparent; + .size(@w: 1054px, @h: 36px); // ๋งˆ์ง„ ์ œ์™ธ ์ฝ˜ํ…์ธ  ํฌ๊ธฐ + max-width: 1054px; + margin-bottom: 20px; + + > div { + .size(@w:100%,@h:100%); + padding: 0; + } + .averageOverallRating { + .size(@w: 176px,@h:30px); + } + + span { + font-size: 30px; + font-weight: 700; + height: 36px; + color: rgba(234, 234, 234, 1); + } + } + + .container { + width: 100%; + .flex(@direction:column,@alignCenter:flex-start); + flex-wrap: wrap; + margin-top: 34px; + // > div { + // margin: 0 15px 15px 0; + // } + + .renderCardContainer { + width: auto; + display: flex; + flex-wrap: wrap; + // margin-top: 34px; + > div { + /* item card */ + margin: 0 15px 15px 0; + .size(@w:300px,@h:435px); + background-color: rgba(51, 51, 51, 0.95); + border: none; + + > div:nth-child(1) { + /* img wrapper*/ + .size(@w:264px,@h:264px); + + > img { + .size(@w:100%,@h:100%); + } + } + + > div:nth-child(2) { + /* desc wrapper */ + > div > h3 { + /* title */ + color: rgba(234, 234, 234, 1); + margin-top: 15px; + .size(@w:100%,@h:62px); + line-height: 31px; + } + > p { + /* priceInfo */ + height: 43px; + line-height: 35px; + text-align: center; + + > span { + font-size: 24px; + } + } + } + // width: 100%; + // padding-left: 60px; + // overflow: unset; + } + } + } +} diff --git a/com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/YouMayAlsoLike/package.json b/com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/YouMayAlsoLike/package.json new file mode 100644 index 00000000..008e47d9 --- /dev/null +++ b/com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/YouMayAlsoLike/package.json @@ -0,0 +1,6 @@ +{ + "main": "YouMayAlsoLike.jsx", + "styles": [ + "YouMayAlsoLike.module.less" + ] +} diff --git a/com.twin.app.shoptime/src/views/DetailPanel/ProductInfoSection/QRCode/QRCode.jsx b/com.twin.app.shoptime/src/views/DetailPanel/ProductInfoSection/QRCode/QRCode.jsx new file mode 100644 index 00000000..e6d38cd9 --- /dev/null +++ b/com.twin.app.shoptime/src/views/DetailPanel/ProductInfoSection/QRCode/QRCode.jsx @@ -0,0 +1,67 @@ +import React, { useMemo } from "react"; +import css from "./QRCode.module.less"; +import { getQRCodeUrl } from "../../../../utils/helperMethods"; +import TQRCode from "../../../../components/TQRCode/TQRCode"; +import { useSelector } from "react-redux"; + +export default function QRCode({ + productType, + selectedIndex, + productInfo, + tooltipDes, + promotionTooltip, + promotionCode, + popupVisible, + onClose, + // serverHOST, + // serverType, + prdtData, + // entryMenu, + // nowMenu, + selectedPrdtId, + selectedPatnrId, + deviceInfo, +}) { + const isBuyNow = productType === "buyNow"; + const isTheme = productType === "theme"; + const isShopByMobile = productType === "shopByMobile"; + + const serverHOST = useSelector((state) => state.common.appStatus.serverHOST); + const serverType = useSelector((state) => state.localSettings.serverType); + const { entryMenu, nowMenu } = useSelector((state) => state.common.menu); + + const { detailUrl } = useMemo(() => { + return getQRCodeUrl({ + serverHOST, + serverType, + index: deviceInfo?.dvcIndex, + patnrId: productInfo?.patnrId, + prdtId: productInfo?.prdtId, + entryMenu: entryMenu, + nowMenu: nowMenu, + liveFlag: "Y", + qrType: "billingDetail", + }); + }, [serverHOST, serverType, deviceInfo, entryMenu, nowMenu, productInfo]); + + const qrCodeUrl = useMemo(() => { + if (isShopByMobile) { + return productInfo?.qrImgUrl || productInfo?.qrcodeUrl; + } + + return detailUrl; + }, [productInfo, isShopByMobile, detailUrl]); + + return ( +
+ {qrCodeUrl && } + + {/* todo : ์‹œ๋‚˜๋ฆฌ์˜ค,UI ๋ฆด๋ฆฌ์ฆˆ ํ›„ */} + {/*
+
+ {promotionCode ? promotionTooltip : tooltipDes} +
+
*/} +
+ ); +} diff --git a/com.twin.app.shoptime/src/views/DetailPanel/ProductInfoSection/QRCode/QRCode.module.less b/com.twin.app.shoptime/src/views/DetailPanel/ProductInfoSection/QRCode/QRCode.module.less new file mode 100644 index 00000000..48ecdbd7 --- /dev/null +++ b/com.twin.app.shoptime/src/views/DetailPanel/ProductInfoSection/QRCode/QRCode.module.less @@ -0,0 +1,12 @@ +@import "../../../../style/CommonStyle.module.less"; +@import "../../../../style/utils.module.less"; + +.qrcode { + > div:first-child { + width: 190px; + height: 190px; + background: @COLOR_WHITE; + border: solid 1px #dadada; + margin: 0 auto; + } +} diff --git a/com.twin.app.shoptime/src/views/DetailPanel/ProductInfoSection/QRCode/package.json b/com.twin.app.shoptime/src/views/DetailPanel/ProductInfoSection/QRCode/package.json new file mode 100644 index 00000000..f0891a0b --- /dev/null +++ b/com.twin.app.shoptime/src/views/DetailPanel/ProductInfoSection/QRCode/package.json @@ -0,0 +1,6 @@ +{ + "main": "QRCode.jsx", + "styles": [ + "QRCode.module.less" + ] +} diff --git a/com.twin.app.shoptime/src/views/DetailPanel/ProductOverview/ProductOverview.jsx b/com.twin.app.shoptime/src/views/DetailPanel/ProductOverview/ProductOverview.jsx new file mode 100644 index 00000000..b1e66f6e --- /dev/null +++ b/com.twin.app.shoptime/src/views/DetailPanel/ProductOverview/ProductOverview.jsx @@ -0,0 +1,51 @@ +import React, { useCallback, useEffect, useState } from "react"; +import css from "./ProductOverview.module.less"; +import TButton from "../../../components/TButton/TButton"; +import FavoriteBtn from "../components/FavoriteBtn"; +import { $L } from "../../../utils/helperMethods"; +import { useSelector } from "react-redux"; +import Spottable from "@enact/spotlight/Spottable"; +import { SpotlightIds } from "../../../utils/SpotlightIds"; +import SpotlightContainerDecorator from "@enact/spotlight/SpotlightContainerDecorator"; +import Spotlight from "@enact/spotlight"; +import TQRCode from "../../../components/TQRCode/TQRCode"; + +import DetailMobileSendPopUp from "../components/DetailMobileSendPopUp"; +import useWhyDidYouUpdate from "../../../hooks/useWhyDidYouUpdate"; +import ProductPriceDisplay from "./ProductPriceDisplay/ProductPriceDisplay"; + +const SpottableComponent = Spottable("div"); + +const Container = SpotlightContainerDecorator( + { enterTo: "last-focused" }, + "div" +); + +export default function ProductOverview({ + children, + panelInfo, + selectedIndex, + productInfo, + productType, +}) { + useEffect(() => { + Spotlight.focus(SpotlightIds.DETAIL_SHOPBYMOBILE); + }, []); + + return ( +
+ {/* price Info */} + {productInfo && productType && ( +
+ {/* price */} + + {/* QR */} + {children} +
+ )} +
+ ); +} diff --git a/com.twin.app.shoptime/src/views/DetailPanel/ProductOverview/ProductOverview.module.less b/com.twin.app.shoptime/src/views/DetailPanel/ProductOverview/ProductOverview.module.less new file mode 100644 index 00000000..01c97513 --- /dev/null +++ b/com.twin.app.shoptime/src/views/DetailPanel/ProductOverview/ProductOverview.module.less @@ -0,0 +1,117 @@ +@import "../../../style/CommonStyle.module.less"; +@import "../../../style/utils.module.less"; + +.container { + .size(@w:100%,@h:100%); + + .productInfoWrapper { + .flex(@justifyCenter:flex-start,@alignCenter:flex-start); + margin: 54px 0 10px 0; + // ๊ณ ์ • ๋†’์ด๋กœ ์ธํ•ด QR ์˜์—ญ๊ณผ ํ•˜๋‹จ ๋ฒ„ํŠผ ์˜์—ญ ์‚ฌ์ด์— ๊ณผ๋„ํ•œ ์—ฌ๋ฐฑ์ด ์ƒ๊น€ + // ์ฝ˜ํ…์ธ  ๋†’์ด์— ๋งž์ถฐ ์ž๋™์œผ๋กœ ๊ณ„์‚ฐ๋˜๋„๋ก ๋ณ€๊ฒฝํ•˜์—ฌ ๋ถˆํ•„์š”ํ•œ ๊ฐ„๊ฒฉ ์ œ๊ฑฐ + .size(@w:100%,@h:auto); + + .priceWrapper { + .flex(@justifyCenter:space-between,@direction:column,@alignCenter:flex-start); + .size(@w:344px,@h:216px); + + .priceName { + .font(@fontFamily: @baseFont, @fontSize: 55px); + font-weight: 700; + color: rgba(234, 234, 234, 1); + line-height: 55px; + } + + .price { + .size(@w:100%,@h:42px); + } + } + + .qrWrapper { + > div:first-child { + width: 160px; + height: 160px; + } + } + } + + .buttonWrapper { + .size(@w:470px,@h:60px); + .flex(@justifyCenter:space-between,@alignCenter:flex-end); + + .sbmButton { + width: 404px; + height: 60px; + background-color: rgba(255, 255, 255, 0.05); + color: rgba(234, 234, 234, 1); + .font(@fontFamily: @baseFont, @fontSize: 25px); + font-weight: 600; + border-radius: 6px; + text-align: center; + > div { + .size(@w:100%,@h:100%); + line-height: 60px; + } + } + + &:focus { + box-shadow: 0px 18px 28.2px 1.8px rgba(62, 59, 59, 0.4); + background-color: @PRIMARY_COLOR_RED; + color: @COLOR_WHITE; + } + } + + .orderNum { + width: 100%; + padding: 21.5px 30px; + .flex(@justifyCenter:space-between); + height: 40px; + margin: 10px 0; + span { + .font(@fontFamily: @baseFont, @fontSize: 20px); + line-height: 40ppx; + font-weight: 700; + color: rgba(234, 234, 234, 1); + } + + > div { + // width: aupx; + .flex(); + + .callIcon { + .size(@w: 25px, @h: 25px); + background-image: url("../../../../assets/images/icons/ic-gr-call-1.png"); + background-size: 25px; + background-position: center; + margin-right: 5px; + } + } + } + + .bottomBtnWrapper { + // .size(@w:100%,@h:246px); + margin-top: 10px; + .size(@w:100%,@h:220px); + .flex(@direction:column); + .button { + .size(@w:100%,@h:60px); + margin-bottom: 6px; + background-color: rgba(255, 255, 255, 0.05); + color: rgba(234, 234, 234, 1); + .font(@fontFamily: @baseFont, @fontSize: 25px); + font-weight: 600; + border-radius: 6px; + text-align: center; + > div { + .size(@w:100%,@h:100%); + line-height: 60px; + } + } + + &:focus { + box-shadow: 0px 18px 28.2px 1.8px rgba(62, 59, 59, 0.4); + background-color: @PRIMARY_COLOR_RED; + color: @COLOR_WHITE; + } + } +} diff --git a/com.twin.app.shoptime/src/views/DetailPanel/ProductOverview/ProductPriceDisplay/BuyNowPriceDisplay/BuyNowPriceDisplay.jsx b/com.twin.app.shoptime/src/views/DetailPanel/ProductOverview/ProductPriceDisplay/BuyNowPriceDisplay/BuyNowPriceDisplay.jsx new file mode 100644 index 00000000..0b238174 --- /dev/null +++ b/com.twin.app.shoptime/src/views/DetailPanel/ProductOverview/ProductPriceDisplay/BuyNowPriceDisplay/BuyNowPriceDisplay.jsx @@ -0,0 +1,61 @@ +import React, { useCallback, useMemo } from "react"; +import usePriceInfo from "../../../../../hooks/usePriceInfo"; +import { $L } from "../../../../../utils/helperMethods"; +import css from "./BuyNowPriceDisplay.module.less"; + +export default function BuyNowPriceDisplay({ + priceData, + priceInfo, + isOriginalPriceEmpty, + isDiscountedPriceEmpty, + isDiscounted, +}) { + const { + price5, + rewd, + offerInfo, + patncNm, + patnrName, + installmentMonths, + orderPhnNo, + } = priceData; + + const { + discountRate, + // rewardFlag, + discountedPrice, + discountAmount, + originalPrice, + promotionCode, + } = usePriceInfo(priceInfo) || {}; + + const renderItem = useCallback(() => { + return ( +
+ + {patncNm + ? patncNm + " " + $L("Price") + : patnrName + " " + $L("Price")} + +
+ {discountRate && Number(discountRate.replace("%", "")) > 4 && ( +
{discountRate}
+ )} + + {isDiscountedPriceEmpty ? offerInfo : discountedPrice} + + {isDiscounted && ( + + {originalPrice && isOriginalPriceEmpty + ? offerInfo + : originalPrice} + + )} +
+ {/* ํ• ๋ถ€ */} +
+ ); + }, []); + + return
{renderItem()}
; +} diff --git a/com.twin.app.shoptime/src/views/DetailPanel/ProductOverview/ProductPriceDisplay/BuyNowPriceDisplay/BuyNowPriceDisplay.module.less b/com.twin.app.shoptime/src/views/DetailPanel/ProductOverview/ProductPriceDisplay/BuyNowPriceDisplay/BuyNowPriceDisplay.module.less new file mode 100644 index 00000000..e69de29b diff --git a/com.twin.app.shoptime/src/views/DetailPanel/ProductOverview/ProductPriceDisplay/BuyNowPriceDisplay/package.json b/com.twin.app.shoptime/src/views/DetailPanel/ProductOverview/ProductPriceDisplay/BuyNowPriceDisplay/package.json new file mode 100644 index 00000000..c6f3eb23 --- /dev/null +++ b/com.twin.app.shoptime/src/views/DetailPanel/ProductOverview/ProductPriceDisplay/BuyNowPriceDisplay/package.json @@ -0,0 +1,6 @@ +{ + "main": "BuyNowPriceDisplay.jsx", + "styles": [ + "BuyNowPriceDisplay.module.less" + ] +} diff --git a/com.twin.app.shoptime/src/views/DetailPanel/ProductOverview/ProductPriceDisplay/ProductPriceDisplay.jsx b/com.twin.app.shoptime/src/views/DetailPanel/ProductOverview/ProductPriceDisplay/ProductPriceDisplay.jsx new file mode 100644 index 00000000..0d14b765 --- /dev/null +++ b/com.twin.app.shoptime/src/views/DetailPanel/ProductOverview/ProductPriceDisplay/ProductPriceDisplay.jsx @@ -0,0 +1,91 @@ +import React, { useCallback, useMemo } from "react"; +import { useSelector } from "react-redux"; +import usePriceInfo from "../../../../hooks/usePriceInfo"; +import css from "./ProductPriceDisplay.module.less"; +import { $L } from "../../../../utils/helperMethods"; +import classNames from "classnames"; +import ShopByMobilePriceDisplay from "./ShopByMobilePriceDisplay/ShopByMobilePriceDisplay"; +import BuyNowPriceDisplay from "./BuyNowPriceDisplay/BuyNowPriceDisplay"; + +export default function ProductPriceDisplay({ productType, productInfo }) { + const webOSVersion = useSelector( + (state) => state.common.appStatus.webOSVersion + ); + + const { + price5, + priceInfo, + rewd, + offerInfo, + patncNm, + patnrName, + installmentMonths, + orderPhnNo, + } = productInfo; + + const { + discountRate, + // rewardFlag, + discountedPrice, + discountAmount, + originalPrice, + promotionCode, + } = usePriceInfo(priceInfo) || {}; + + const isOriginalPriceEmpty = useMemo(() => { + return parseFloat(originalPrice.replace(/[^0-9.-]+/g, "")) === 0; + }, [originalPrice]); + + const isDiscountedPriceEmpty = useMemo(() => { + return parseFloat(discountedPrice.replace(/[^0-9.-]+/g, "")) === 0; + }, [discountedPrice]); + + const isDiscounted = useMemo(() => { + return discountedPrice !== originalPrice; + }, [discountedPrice, originalPrice]); + + const isThemeBuyNow = useMemo(() => { + return ( + productType === "theme" && + productInfo?.pmtSuptYn === "Y" && + webOSVersion >= "6.0" + ); + }, [productType, productInfo?.pmtSuptYn, webOSVersion]); + + const isThemeShopByMobile = useMemo(() => { + return ( + productType === "theme" && + (productInfo?.pmtSuptYn === "N" || webOSVersion < "6.0") + ); + }, [productType, productInfo?.pmtSuptYn, webOSVersion]); + + return ( + <> + {productType && productInfo && ( +
+ {/* shop by mobile (๊ตฌ๋งค๋ถˆ๊ฐ€) ์ƒํ’ˆ price render */} + {(productType === "shopByMobile" || isThemeShopByMobile) && ( + + )} + + {/* buy now (๊ฒฐ์ œ ๊ฐ€๋Šฅ) ์ƒํ’ˆ price render */} + {(productType === "buyNow" || isThemeBuyNow) && ( + + )} +
+ )} + + ); +} diff --git a/com.twin.app.shoptime/src/views/DetailPanel/ProductOverview/ProductPriceDisplay/ProductPriceDisplay.module.less b/com.twin.app.shoptime/src/views/DetailPanel/ProductOverview/ProductPriceDisplay/ProductPriceDisplay.module.less new file mode 100644 index 00000000..44afa451 --- /dev/null +++ b/com.twin.app.shoptime/src/views/DetailPanel/ProductOverview/ProductPriceDisplay/ProductPriceDisplay.module.less @@ -0,0 +1,101 @@ +@import "../../../../style/CommonStyle.module.less"; +@import "../../../../style/utils.module.less"; + +.wrapper { + height: 100%; + + .partnerName { + font-weight: bold; + font-size: 36px; + color: @COLOR_BLACK; + margin-bottom: 24px; + } + .flex(@alignCenter:flex-start,@direction:column); + .topLayer { + margin-bottom: 20px; + height: 42px; + .flex(); + } + .rateTag { + background: linear-gradient(309.43deg, #ef775b 23.84%, #c70850 100%); + width: 70px; + height: 42px; + padding: 6px 12px; + border-radius: 6px; + color: @COLOR_WHITE; + margin-right: 10px; + .font(@fontFamily: @baseFont, @fontSize: 24px); + font-weight: 700; + .flex(); + } + .name { + font-weight: bold; + font-size: 36px; + color: @COLOR_GRAY07; + } + .btmLayer { + .flex(); + margin-top: 14px; + } + .price { + font-weight: bold; + font-size: 60px; + color: @PRIMARY_COLOR_RED; + margin-right: 9px; + line-height: 1; + } + .offerInfo { + .elip(4); + font-size: 40px; + font-weight: bold; + line-height: 1; + color: #808080; + padding-bottom: 5px; + } + .discountedPrc { + font-size: 24px; + color: @COLOR_GRAY03; + text-decoration: line-through; + } + //rewd Layer + .rewdTopLayer { + width: 500px; + padding-bottom: 24px; + border-bottom: 1px solid @COLOR_GRAY02; + + > span { + font-weight: bold; + font-size: 36px; + color: @COLOR_GRAY03; + } + .partnerPrc { + text-decoration: line-through; + } + } + .rewdBtmLayer { + padding-top: 24px; + .flex(@direction:column,@alignCenter:flex-start); + .rewdNm { + font-weight: bold; + font-size: 36px; + color: @COLOR_BLACK; + } + + .btmPrc { + margin-top: 17px; + .flex(); + .rewdPrc { + font-size: 24px; + color: #808080; + margin-right: 10px; + padding-top: 14px; + } + .rewdRate { + font-size: 60px; + font-weight: bold; + color: @PRIMARY_COLOR_RED; + margin-top: 18px; + } + } + } +} diff --git a/com.twin.app.shoptime/src/views/DetailPanel/ProductOverview/ProductPriceDisplay/ShopByMobilePriceDisplay/ShopByMobilePriceDisplay.jsx b/com.twin.app.shoptime/src/views/DetailPanel/ProductOverview/ProductPriceDisplay/ShopByMobilePriceDisplay/ShopByMobilePriceDisplay.jsx new file mode 100644 index 00000000..29c5aa1a --- /dev/null +++ b/com.twin.app.shoptime/src/views/DetailPanel/ProductOverview/ProductPriceDisplay/ShopByMobilePriceDisplay/ShopByMobilePriceDisplay.jsx @@ -0,0 +1,200 @@ +import React, { useCallback, useMemo } from "react"; +import usePriceInfo from "../../../../../hooks/usePriceInfo"; +import { $L } from "../../../../../utils/helperMethods"; +import css from "./ShopByMobilePriceDisplay.module.less"; +import classNames from "classnames"; + +export default function ShopByMobilePriceDisplay({ + priceData, + priceInfo, + isOriginalPriceEmpty, + isDiscountedPriceEmpty, + isDiscounted, +}) { + const { + price5, + rewd, + offerInfo, + patncNm, + patnrName, + installmentMonths, + orderPhnNo, + } = priceData; + + const { + discountRate, + rewardFlag, + discountedPrice, + discountAmount, + originalPrice, + promotionCode, + } = usePriceInfo(priceInfo) || {}; + + const TYPE_CASE = useMemo( + () => ({ + case1: !isOriginalPriceEmpty && isDiscountedPriceEmpty && !price5, + case2: !isOriginalPriceEmpty && !isDiscountedPriceEmpty && !price5, + case3: !isOriginalPriceEmpty && !isDiscountedPriceEmpty && price5, + case4: !isOriginalPriceEmpty && isDiscountedPriceEmpty && price5, + case5: !isOriginalPriceEmpty && isDiscountedPriceEmpty && !price5, + case6: !isOriginalPriceEmpty && !isDiscountedPriceEmpty && !price5, + case7: !isOriginalPriceEmpty && !isDiscountedPriceEmpty && price5, + case8: !isOriginalPriceEmpty && isDiscountedPriceEmpty && price5, + case9: !!(isOriginalPriceEmpty && isDiscountedPriceEmpty && offerInfo), + }), + [isOriginalPriceEmpty, isDiscountedPriceEmpty, price5, offerInfo] + ); + + const renderPriceItem = useCallback(() => { + if (priceData && !promotionCode) { + if (rewd) { + return ( +
+
+ + {patncNm + ? patncNm + " " + $L("Price") + " " + : patnrName + " " + $L("Price") + " "} + + + {TYPE_CASE.case5 || TYPE_CASE.case8 + ? isOriginalPriceEmpty + ? offerInfo + : originalPrice + : discountedPrice} + +
+
+ {$L("Shop Time Price")} +
+ {/* TODO : rewd data*/} + {/* ๋ถ„ํ• ๊ธˆ์•ก ์กฐ๊ฑด๋ฌธ์ฒ˜๋ฆฌ ์ผ€์ด์Šค๋ณ„๋กœ */} + {discountedPrice} + {/* ๋ฆฌ์›Œ๋“œ ๊ธˆ์•ก */} + {(TYPE_CASE.case7 || + TYPE_CASE.case5 || + TYPE_CASE.case6 || + TYPE_CASE.case8) && ( + + {$L("Save") + discountAmount + discountRate} + + )} +
+
+
+ ); + } else { + if (TYPE_CASE.case1 || TYPE_CASE.case4) { + return ( +
+ + {patncNm + ? patncNm + " " + $L("Price") + : patnrName + " " + $L("Price")} + +
+ + {isOriginalPriceEmpty ? offerInfo : originalPrice} + +
+
+ ); + } else if (TYPE_CASE.case2) { + return ( +
+
+ {discountRate && Number(discountRate.replace("%", "")) > 4 && ( +
{discountRate}
+ )} + + {patncNm + ? patncNm + " " + $L("Price") + : patnrName + $L("Price")} + +
+
+ + {isDiscountedPriceEmpty ? offerInfo : discountedPrice} + + {isDiscounted && ( + + {originalPrice && isOriginalPriceEmpty + ? offerInfo + : originalPrice} + + )} +
+
+ ); + } else if (TYPE_CASE.case3) { + return ( +
+ + {patncNm + ? patncNm + " " + $L("Price") + : patnrName + " " + $L("Price")} + +
+ {discountRate && Number(discountRate.replace("%", "")) > 4 && ( +
{discountRate}
+ )} + + {isDiscountedPriceEmpty ? offerInfo : discountedPrice} + + {isDiscounted && ( + + {originalPrice && isOriginalPriceEmpty + ? offerInfo + : originalPrice} + + )} +
+ {/* ํ• ๋ถ€ */} +
+ ); + } + } + } else if (promotionCode) { + return ( +
+
+ {discountRate && Number(discountRate.replace("%", "")) > 4 && ( +
{discountRate}
+ )} + {$L("Shop Time Price")} +
+
+ {discountedPrice} + {discountedPrice !== originalPrice && ( + + {originalPrice && isOriginalPriceEmpty + ? offerInfo + : originalPrice} + + )} +
+
+ ); + } + if (TYPE_CASE.case9) { + return ( +
+ + {patncNm ? patncNm + " " + $L("Price") : patnrName + $L("Price")} + + {offerInfo} +
+ ); + } + }, [ + patnrName, + priceInfo, + isOriginalPriceEmpty, + isDiscountedPriceEmpty, + TYPE_CASE, + offerInfo, + isDiscounted, + ]); + + return
{renderPriceItem()}
; +} diff --git a/com.twin.app.shoptime/src/views/DetailPanel/ProductOverview/ProductPriceDisplay/ShopByMobilePriceDisplay/ShopByMobilePriceDisplay.module.less b/com.twin.app.shoptime/src/views/DetailPanel/ProductOverview/ProductPriceDisplay/ShopByMobilePriceDisplay/ShopByMobilePriceDisplay.module.less new file mode 100644 index 00000000..b003c935 --- /dev/null +++ b/com.twin.app.shoptime/src/views/DetailPanel/ProductOverview/ProductPriceDisplay/ShopByMobilePriceDisplay/ShopByMobilePriceDisplay.module.less @@ -0,0 +1,101 @@ +@import "../../../../../style/CommonStyle.module.less"; +@import "../../../../../style/utils.module.less"; + +.wrapper { + height: 100%; + + .partnerName { + font-weight: bold; + font-size: 36px; + color: @COLOR_BLACK; + margin-bottom: 24px; + } + .flex(@alignCenter:flex-start,@direction:column); + .topLayer { + margin-bottom: 20px; + height: 42px; + .flex(); + } + .rateTag { + background: linear-gradient(309.43deg, #ef775b 23.84%, #c70850 100%); + width: 70px; + height: 42px; + padding: 6px 12px; + border-radius: 6px; + color: @COLOR_WHITE; + margin-right: 10px; + .font(@fontFamily: @baseFont, @fontSize: 24px); + font-weight: 700; + .flex(); + } + .name { + font-weight: bold; + font-size: 36px; + color: @COLOR_GRAY07; + } + .btmLayer { + .flex(); + margin-top: 14px; + } + .price { + font-weight: bold; + font-size: 60px; + color: @PRIMARY_COLOR_RED; + margin-right: 9px; + line-height: 1; + } + .offerInfo { + .elip(4); + font-size: 40px; + font-weight: bold; + line-height: 1; + color: #808080; + padding-bottom: 5px; + } + .discountedPrc { + font-size: 24px; + color: @COLOR_GRAY03; + text-decoration: line-through; + } + //rewd Layer + .rewdTopLayer { + width: 500px; + padding-bottom: 24px; + border-bottom: 1px solid @COLOR_GRAY02; + + > span { + font-weight: bold; + font-size: 36px; + color: @COLOR_GRAY03; + } + .partnerPrc { + text-decoration: line-through; + } + } + .rewdBtmLayer { + padding-top: 24px; + .flex(@direction:column,@alignCenter:flex-start); + .rewdNm { + font-weight: bold; + font-size: 36px; + color: @COLOR_BLACK; + } + + .btmPrc { + margin-top: 17px; + .flex(); + .rewdPrc { + font-size: 24px; + color: #808080; + margin-right: 10px; + padding-top: 14px; + } + .rewdRate { + font-size: 60px; + font-weight: bold; + color: @PRIMARY_COLOR_RED; + margin-top: 18px; + } + } + } +} diff --git a/com.twin.app.shoptime/src/views/DetailPanel/ProductOverview/ProductPriceDisplay/ShopByMobilePriceDisplay/package.json b/com.twin.app.shoptime/src/views/DetailPanel/ProductOverview/ProductPriceDisplay/ShopByMobilePriceDisplay/package.json new file mode 100644 index 00000000..32c41246 --- /dev/null +++ b/com.twin.app.shoptime/src/views/DetailPanel/ProductOverview/ProductPriceDisplay/ShopByMobilePriceDisplay/package.json @@ -0,0 +1,6 @@ +{ + "main": "ShopByMobilePriceDisplay.jsx", + "styles": [ + "ShopByMobilePriceDisplay.module.less" + ] +} diff --git a/com.twin.app.shoptime/src/views/DetailPanel/ProductOverview/ProductPriceDisplay/package.json b/com.twin.app.shoptime/src/views/DetailPanel/ProductOverview/ProductPriceDisplay/package.json new file mode 100644 index 00000000..1d8ade51 --- /dev/null +++ b/com.twin.app.shoptime/src/views/DetailPanel/ProductOverview/ProductPriceDisplay/package.json @@ -0,0 +1,6 @@ +{ + "main": "ProductPriceDisplay.jsx", + "styles": [ + "ProductPriceDisplay.module.less" + ] +} diff --git a/com.twin.app.shoptime/src/views/DetailPanel/ProductOverview/package.json b/com.twin.app.shoptime/src/views/DetailPanel/ProductOverview/package.json new file mode 100644 index 00000000..31539a61 --- /dev/null +++ b/com.twin.app.shoptime/src/views/DetailPanel/ProductOverview/package.json @@ -0,0 +1,6 @@ +{ + "main": "ProductOverview.jsx", + "styles": [ + "ProductOverview.module.less" + ] +} diff --git a/com.twin.app.shoptime/src/views/DetailPanel/ThemeItemListOverlay/ThemeItemListOverlay.jsx b/com.twin.app.shoptime/src/views/DetailPanel/ThemeItemListOverlay/ThemeItemListOverlay.jsx new file mode 100644 index 00000000..882d2a46 --- /dev/null +++ b/com.twin.app.shoptime/src/views/DetailPanel/ThemeItemListOverlay/ThemeItemListOverlay.jsx @@ -0,0 +1,200 @@ +import React, { + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from "react"; +import classNames from "classnames/bind"; + +import css from "./ThemeItemListOverlay.module.less"; +import TVirtualGridList from "../../../components/TVirtualGridList/TVirtualGridList"; +import useScrollTo from "../../../hooks/useScrollTo"; +import { finishVideoPreview } from "../../../actions/playActions"; +import { panel_names } from "../../../utils/Config"; +import { setContainerLastFocusedElement } from "@enact/spotlight/src/container"; +import { useDispatch, useSelector } from "react-redux"; +import { popPanel, pushPanel } from "../../../actions/panelActions"; +import { clearThemeDetail } from "../../../actions/homeActions"; +import TItemCard from "../../../components/TItemCard/TItemCard"; +import * as Config from "../../../utils/Config"; +import SpotlightContainerDecorator from "@enact/spotlight/SpotlightContainerDecorator"; +import { $L } from "../../../utils/helperMethods"; +import TButton from "../../../components/TButton/TButton"; +import Spotlight from "@enact/spotlight"; + +const Container = SpotlightContainerDecorator( + { enterTo: "default-element" }, + "div" +); + +export default function ThemeItemListOverlay({ + productInfo, + isOpen, + panelInfo, + productType, + setSelectedIndex, + openThemeItemOverlay, + setOpenThemeItemOverlay, +}) { + const { getScrollTo, scrollLeft } = useScrollTo(); + const panels = useSelector((state) => state.panels.panels); + const themeProductInfos = useSelector( + (state) => state.home.themeCurationDetailInfoData + ); + const overlayRef = useRef(null); + const dispatch = useDispatch(); + + useEffect(() => { + function handleClickOutside(e) { + /* ์˜ค๋ฒ„๋ ˆ์ด ์ด์™ธ ์˜์—ญ ํด๋ฆญ์‹œ ์ฐฝ ๋‹ซ์Œ ์ฒ˜๋ฆฌ */ + if ( + openThemeItemOverlay && + overlayRef.current && + !overlayRef.current.contains(e.target) + ) { + setOpenThemeItemOverlay(false); + } + } + if (openThemeItemOverlay) { + document.addEventListener("mousedown", handleClickOutside); + } + return () => { + document.removeEventListener("mousedown", handleClickOutside); + }; + }, [openThemeItemOverlay, setOpenThemeItemOverlay]); + + const launchedFromPlayer = useMemo(() => { + const detailPanelIndex = panels.findIndex( + ({ name }) => name === "detailpanel" + ); + const playerPanelIndex = panels.findIndex( + ({ name }) => name === "playerpanel" + ); + + return detailPanelIndex - 1 === playerPanelIndex; + }, [panels]); + + const onKeyDown = useCallback((event) => { + if (event.key === "ArrowUp") { + setOpenThemeItemOverlay(false); + event.stopPropagation(); + event.preventDefault(); + + setTimeout(() => { + Spotlight.focus("theme-open-button"); + }, 0); + } + }, []); + + const renderItem = useCallback( + ({ index, ...rest }) => { + const { + imgUrls, + patnrId, + prdtId, + prdtNm, + priceInfo, + offerInfo, + patncNm, + brndNm, + curationNm, + // lgCatCd, + // lgCatNm, + } = productInfo.productInfos[index]; + + const handleItemClick = () => { + setSelectedIndex(index); + dispatch(finishVideoPreview()); + dispatch(popPanel(panel_names.DETAIL_PANEL)); + + // setContainerLastFocusedElement(null, ["indicator-GridListContainer"]); + if (themeProductInfos && themeProductInfos.length > 0) { + dispatch(clearThemeDetail()); + } + + dispatch( + pushPanel({ + name: panel_names.DETAIL_PANEL, + panelInfo: { + patnrId, + prdtId, + curationId: productInfo?.curationId, + curationNm: productInfo ? launchedFromPlayer : launchedFromPlayer, + type: "theme", + }, + }) + ); + setOpenThemeItemOverlay(false); + // cursorOpen.current.stop(); + }; + return ( + + ); + }, + [productInfo?.productInfos, launchedFromPlayer, setOpenThemeItemOverlay] + ); + + useEffect(() => { + if (openThemeItemOverlay) { + Spotlight.focus("theme-close-button"); + } + }, [openThemeItemOverlay]); + + const handleButtonClick = useCallback(() => { + setOpenThemeItemOverlay(false); + setTimeout(() => { + Spotlight.focus("theme-open-button"); + }, 0); + }, [setOpenThemeItemOverlay]); + + return ( +
+ {productInfo && productInfo?.productInfos?.length > 0 && isOpen && ( + + + {$L("THEME ITEM")} + + + + )} +
+ ); +} diff --git a/com.twin.app.shoptime/src/views/DetailPanel/ThemeItemListOverlay/ThemeItemListOverlay.module.less b/com.twin.app.shoptime/src/views/DetailPanel/ThemeItemListOverlay/ThemeItemListOverlay.module.less new file mode 100644 index 00000000..c69e78d1 --- /dev/null +++ b/com.twin.app.shoptime/src/views/DetailPanel/ThemeItemListOverlay/ThemeItemListOverlay.module.less @@ -0,0 +1,68 @@ +@import "../../../style/CommonStyle.module.less"; +@import "../../../style/utils.module.less"; + +.container { + .size(@w: 1920px,@h: 390px); + position: absolute; + bottom: 0; + height: 390px; + background: rgba(30, 30, 30, 0.8); + z-index: 23; + .flex(@justifyCenter:space-between,@direction:column,@alignCenter:flex-start); + padding: 60px 0; + + .button { + margin-left: 70px; + .size(@w: 470px,@h:70px); + background-color: rgba(255, 255, 255, 0.05); + + &:focus { + background: #c72054; + color: white; + box-shadow: 22px; + } + } + + .themeItemList { + .size(@w: 1920px,@h: 170px); + padding-left: 70px; + + .themeItemCard { + .flex(@justifyCenter:space-between); + background: rgba(44, 44, 44, 1); + border: none; + padding: 30px; + + .size(@w: 470px,@h: 170px); + + /* img */ + > div { + .size(@w: 110px,@h: 110px); + > img { + .size(@w: 110px,@h: 110px); + } + } + + /* description */ + > div:nth-child(2) { + .size(@w: 285px,@h: 110px); + + > div > h3 { + .font(@fontFamily: @baseFont, @fontSize: 20px); + height: 50px; + font-weight: 400; + line-height: 25px; + color: rgba(234, 234, 234, 1); + .elip(@clamp:2); + } + + > p { + .font(@fontFamily: @baseFont, @fontSize: 20px); + } + } + // > img { + // .size(@w: 110px,@h: 110px); + // } + } + } +} diff --git a/com.twin.app.shoptime/src/views/DetailPanel/components/CustomScrollbar/CustomScrollbar.jsx b/com.twin.app.shoptime/src/views/DetailPanel/components/CustomScrollbar/CustomScrollbar.jsx new file mode 100644 index 00000000..beec03ee --- /dev/null +++ b/com.twin.app.shoptime/src/views/DetailPanel/components/CustomScrollbar/CustomScrollbar.jsx @@ -0,0 +1,144 @@ +import React, { useEffect, useRef, useState, useCallback } from "react"; +import css from "./CustomScrollbar.module.less"; + +const CustomScrollbar = ({ scrollerRef, trackHeight = 100 }) => { + const [thumbHeight, setThumbHeight] = useState(60); + const [thumbTop, setThumbTop] = useState(0); + const [isVisible, setIsVisible] = useState(false); + const [isDragging, setIsDragging] = useState(false); + + const thumbRef = useRef(null); + const trackRef = useRef(null); + const dragStartY = useRef(0); + const dragStartScrollTop = useRef(0); + + // ์Šคํฌ๋กค๋ฐ” ์ƒํƒœ ์—…๋ฐ์ดํŠธ + const updateScrollbar = useCallback(() => { + if (!scrollerRef?.current) return; + + const scroller = scrollerRef.current; + const scrollHeight = scroller.scrollHeight; + const clientHeight = scroller.clientHeight; + const scrollTop = scroller.scrollTop; + + // ์Šคํฌ๋กค์ด ํ•„์š”ํ•œ์ง€ ํ™•์ธ + if (scrollHeight <= clientHeight) { + setIsVisible(false); + return; + } + + setIsVisible(true); + + // ์ธ ๋†’์ด ๊ณ„์‚ฐ (์ฝ˜ํ…์ธ  ๋น„์œจ์— ๋”ฐ๋ผ, ์ตœ์†Œ 20px) + const newThumbHeight = Math.max(20, (clientHeight / scrollHeight) * trackHeight); + setThumbHeight(newThumbHeight); + + // ์ธ ์œ„์น˜ ๊ณ„์‚ฐ + const maxScrollTop = scrollHeight - clientHeight; + const maxThumbTop = trackHeight - newThumbHeight; + const newThumbTop = maxScrollTop > 0 ? (scrollTop / maxScrollTop) * maxThumbTop : 0; + setThumbTop(newThumbTop); + }, [scrollerRef, trackHeight]); + + // ์Šคํฌ๋กค ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ + useEffect(() => { + const scroller = scrollerRef?.current; + if (!scroller) return; + + const handleScroll = () => updateScrollbar(); + scroller.addEventListener('scroll', handleScroll); + + // ์ดˆ๊ธฐ ์ƒํƒœ ์„ค์ • + const timer = setTimeout(updateScrollbar, 100); + + return () => { + scroller.removeEventListener('scroll', handleScroll); + clearTimeout(timer); + }; + }, [scrollerRef, updateScrollbar]); + + // ๋“œ๋ž˜๊ทธ ์‹œ์ž‘ + const handleMouseDown = useCallback((e) => { + if (!scrollerRef?.current) return; + + e.preventDefault(); + setIsDragging(true); + dragStartY.current = e.clientY; + dragStartScrollTop.current = scrollerRef.current.scrollTop; + }, [scrollerRef]); + + // ๋“œ๋ž˜๊ทธ ์ค‘ + const handleMouseMove = useCallback((e) => { + if (!isDragging || !scrollerRef?.current) return; + + e.preventDefault(); + const scroller = scrollerRef.current; + const deltaY = e.clientY - dragStartY.current; + const scrollRatio = scroller.scrollHeight / trackHeight; + const newScrollTop = dragStartScrollTop.current + (deltaY * scrollRatio); + + scroller.scrollTop = Math.max(0, Math.min(newScrollTop, scroller.scrollHeight - scroller.clientHeight)); + }, [isDragging, scrollerRef, trackHeight]); + + // ๋“œ๋ž˜๊ทธ ๋ + const handleMouseUp = useCallback(() => { + setIsDragging(false); + }, []); + + // ์ „์—ญ ๋งˆ์šฐ์Šค ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ + useEffect(() => { + if (isDragging) { + document.addEventListener('mousemove', handleMouseMove); + document.addEventListener('mouseup', handleMouseUp); + document.body.style.userSelect = 'none'; + + return () => { + document.removeEventListener('mousemove', handleMouseMove); + document.removeEventListener('mouseup', handleMouseUp); + document.body.style.userSelect = ''; + }; + } + }, [isDragging, handleMouseMove, handleMouseUp]); + + // ํŠธ๋ž™ ํด๋ฆญ + const handleTrackClick = useCallback((e) => { + if (!scrollerRef?.current || !trackRef.current) return; + + const track = trackRef.current; + const rect = track.getBoundingClientRect(); + const clickY = e.clientY - rect.top; + const scroller = scrollerRef.current; + + const scrollRatio = scroller.scrollHeight / trackHeight; + const newScrollTop = (clickY - thumbHeight / 2) * scrollRatio; + + scroller.scrollTop = Math.max(0, Math.min(newScrollTop, scroller.scrollHeight - scroller.clientHeight)); + }, [scrollerRef, trackHeight, thumbHeight]); + + if (!isVisible) { + return null; + } + + return ( +
+
+
+
+
+ ); +}; + +export default CustomScrollbar; \ No newline at end of file diff --git a/com.twin.app.shoptime/src/views/DetailPanel/components/CustomScrollbar/CustomScrollbar.module.less b/com.twin.app.shoptime/src/views/DetailPanel/components/CustomScrollbar/CustomScrollbar.module.less new file mode 100644 index 00000000..77d64a46 --- /dev/null +++ b/com.twin.app.shoptime/src/views/DetailPanel/components/CustomScrollbar/CustomScrollbar.module.less @@ -0,0 +1,51 @@ +@import "../../../../style/CommonStyle.module.less"; +@import "../../../../style/utils.module.less"; + +.customScrollbarArea { + position: absolute; + right: 0; + top: 0; + width: 66px; + height: calc(100% - 60px); // ํ•˜๋‹จ 60px ๋งˆ์ง„ + background: transparent; // ๋””๋ฒ„๊น… ์™„๋ฃŒ ํ›„ transparent๋กœ ๋ณ€๊ฒฝ + z-index: 1000; + padding: 0 30px 60px 30px; // ์ขŒ์šฐ 30px, ํ•˜๋‹จ 60px ๋งˆ์ง„ + box-sizing: border-box; + + // ์Šคํฌ๋กค๋ฐ” ํŠธ๋ž™ (100px ๊ณ ์ • ์˜์—ญ) + .scrollbarTrack { + width: 6px; + height: 100px; // ๊ณ ์ • ๋†’์ด + background: transparent; // ๋””๋ฒ„๊น… ์™„๋ฃŒ ํ›„ transparent๋กœ ๋ณ€๊ฒฝ + position: relative; + margin: 0 auto; // ์ค‘์•™ ์ •๋ ฌ + cursor: pointer; + + // ์‹ค์ œ ์Šคํฌ๋กค๋ฐ” ์ธ + .customScrollbar { + width: 6px; + height: 60px; // ๊ธฐ๋ณธ ๊ธธ์ด (JavaScript๋กœ ๋™์  ์กฐ์ •) + background: #9C9C9C; + border-radius: 0; + position: absolute; + top: 0; // JavaScript๋กœ ๋™์  ์กฐ์ • + left: 0; + transition: background-color 0.2s ease; + cursor: grab; + + &:hover { + background: @PRIMARY_COLOR_RED; + } + + &.dragging { + background: @PRIMARY_COLOR_RED; + cursor: grabbing; + } + + // ์Šคํฌ๋กค ์ƒํƒœ ํ‘œ์‹œ + &.active { + background: @PRIMARY_COLOR_RED; + } + } + } +} \ No newline at end of file diff --git a/com.twin.app.shoptime/src/views/DetailPanel/components/DetailMobileSendPopUp.jsx b/com.twin.app.shoptime/src/views/DetailPanel/components/DetailMobileSendPopUp.jsx new file mode 100644 index 00000000..da6fc63c --- /dev/null +++ b/com.twin.app.shoptime/src/views/DetailPanel/components/DetailMobileSendPopUp.jsx @@ -0,0 +1,193 @@ +import React, { useCallback, useEffect, useMemo, useRef } from "react"; +import { useDispatch, useSelector } from "react-redux"; +import MobileSendPopUp from "../../../components/MobileSend/MobileSendPopUp"; +import * as Config from "../../../utils/Config"; +import { setHidePopup, setShowPopup } from "../../../actions/commonActions"; +import Spotlight from "@enact/spotlight"; +import { + sendLogShopByMobile, + sendLogTotalRecommend, +} from "../../../actions/logActions"; +import { $L, formatLocalDateTime } from "../../../utils/helperMethods"; + +export default function DetailMobileSendPopUp({ + panelInfo, + ismobileSendPopupOpen, + selectedIndex, + setMobileSendPopupOpen, +}) { + const productData = useSelector((state) => state.main.productData); + const themeProductInfos = useSelector( + (state) => state.home.themeCurationDetailInfoData + ); + const { popupVisible, activePopup } = useSelector( + (state) => state.common.popup + ); + const shopByMobileLogRef = useRef(null); + const dispatch = useDispatch(); + const { entryMenu, nowMenu } = useSelector((state) => state.common.menu); + + const mobileSendPopUpProductImg = useMemo(() => { + if (panelInfo?.type === "theme" && themeProductInfos) { + return themeProductInfos[selectedIndex]?.imgUrls600[0]; + } + // else if (panelInfo?.type === "hotel" && hotelInfos) { + // return hotelInfos[selectedIndex]?.hotelImgUrl; + // } + else { + return productData?.imgUrls600[0]; + } + }, [ + themeProductInfos, + // hotelInfos, + selectedIndex, + productData, + panelInfo?.type, + ]); + + const mobileSendPopUpSubtitle = useMemo(() => { + // if (panelInfo?.type === "theme" && themeProductInfos) { + // return themeProductInfos[selectedIndex]?.prdtNm; + // } + // else if (panelInfo?.type === "hotel" && hotelInfos) { + // return hotelInfos[selectedIndex]?.hotelNm; + // } + // else { + return productData?.prdtNm; + // } + }, [ + // themeProductInfos, + // hotelInfos, + selectedIndex, + productData, + panelInfo?.type, + ]); + + const handleSMSonClose = useCallback(() => { + dispatch(setHidePopup()); + setMobileSendPopupOpen(false); + setTimeout(() => { + Spotlight.focus("spotlightId_backBtn"); + Spotlight.focus("shopbymobile_Btn"); + }, 0); + }, [dispatch]); + + // const Price = () => { + // return ( + // <> + // {hotelInfos[selectedIndex]?.hotelDetailInfo.currencySign} + // {hotelInfos[selectedIndex]?.hotelDetailInfo.price} + // + // ); + // }; + + const handleMobileSendPopupOpen = useCallback(() => { + dispatch(setShowPopup(Config.ACTIVE_POPUP.smsPopup)); + + if (productData && Object.keys(productData).length > 0) { + const { priceInfo, patncNm, prdtId, prdtNm, brndNm, catNm } = productData; + const regularPrice = priceInfo.split("|")[0]; + const discountPrice = priceInfo.split("|")[1]; + const discountRate = priceInfo.split("|")[4]; + const logParams = { + status: "open", + nowMenu: nowMenu, + partner: patncNm, + productId: prdtId, + productTitle: prdtNm, + price: discountRate ? discountPrice : regularPrice, + brand: brndNm, + discount: discountRate, + category: catNm, + contextName: Config.LOG_CONTEXT_NAME.SHOPBYMOBILE, + messageId: Config.LOG_MESSAGE_ID.SMB, + }; + dispatch(sendLogTotalRecommend(logParams)); + dispatch( + sendLogTotalRecommend({ + menu: Config.LOG_MENU.DETAIL_PAGE_BILLING_PRODUCT_DETAIL, + buttonTitle: "Shop By Mobile", + contextName: Config.LOG_CONTEXT_NAME.DETAILPAGE, + messageId: Config.LOG_MESSAGE_ID.BUTTONCLICK, + }) + ); + } + if (productData && Object.keys(productData).length > 0) { + const params = { + befPrice: productData?.priceInfo?.split("|")[0], + curationId: productData?.curationId ?? "", + curationNm: productData?.curationNm ?? "", + evntId: "", + evntNm: "", + lastPrice: productData?.priceInfo?.split("|")[1], + lgCatCd: productData?.catCd ?? "", + lgCatNm: productData?.catNm ?? "", + liveFlag: panelInfo?.liveFlag ?? "N", + locDt: formatLocalDateTime(new Date()), + logTpNo: Config.LOG_TP_NO.SHOP_BY_MOBILE.SHOP_BY_MOBILE, + mbphNoFlag: "N", + patncNm: productData?.patncNm, + patnrId: productData?.patnrId, + prdtId: productData?.prdtId, + prdtNm: productData?.prdtNm, + revwGrd: productData?.revwGrd ?? "", + rewdAplyFlag: productData?.priceInfo?.split("|")[2], + shopByMobileFlag: "Y", + shopTpNm: "product", + showId: productData?.showId ?? "", + showNm: productData?.showNm ?? "", + trmsAgrFlag: "N", + tsvFlag: productData?.todaySpclFlag ?? "", + }; + dispatch(sendLogShopByMobile(params)); + shopByMobileLogRef.current = params; + } + }, [ + panelInfo?.liveFlag, + productData, + shopByMobileLogRef, + dispatch, + ismobileSendPopupOpen, + ]); + + useEffect(() => { + console.log("ismobileSendPopupOpen @@@@ :", ismobileSendPopupOpen); + if (ismobileSendPopupOpen) { + handleMobileSendPopupOpen(); + } + }, [ismobileSendPopupOpen]); + + return ( + <> + {activePopup === Config.ACTIVE_POPUP.smsPopup && ( + + )} + + ); +} diff --git a/com.twin.app.shoptime/src/views/DetailPanel/components/DetailMobileSendPopUp.module.less b/com.twin.app.shoptime/src/views/DetailPanel/components/DetailMobileSendPopUp.module.less new file mode 100644 index 00000000..7b34bb91 --- /dev/null +++ b/com.twin.app.shoptime/src/views/DetailPanel/components/DetailMobileSendPopUp.module.less @@ -0,0 +1,2 @@ +@import "../../../style/CommonStyle.module.less";" +@import "../../../style/utils.module.less"; \ No newline at end of file diff --git a/com.twin.app.shoptime/src/views/DetailPanel/components/THeaderCustom.jsx b/com.twin.app.shoptime/src/views/DetailPanel/components/THeaderCustom.jsx new file mode 100644 index 00000000..0054fb20 --- /dev/null +++ b/com.twin.app.shoptime/src/views/DetailPanel/components/THeaderCustom.jsx @@ -0,0 +1,93 @@ +import React, { useCallback, useMemo } from "react"; + +import classNames from "classnames"; + +import { Marquee } from "@enact/sandstone/Marquee"; +import SpotlightContainerDecorator from "@enact/spotlight/SpotlightContainerDecorator"; +import Spottable from "@enact/spotlight/Spottable"; + +import { $L } from "../../../utils/helperMethods"; +import defaultLogoImg from "../../../../assets/images/ic-tab-partners-default@3x.png"; +import qvcLogoImg from "../../../../assets/images/icons/ic-partners-qvc@3x.png"; +import css from "./THeaderCustom.module.less"; + +const Container = SpotlightContainerDecorator( + { enterTo: "last-focused" }, + "div" +); +const SpottableComponent = Spottable("button"); + +export default function THeaderCustom({ + title, + className, + onBackButton, + onSpotlightUp, + onSpotlightLeft, + marqueeDisabled = true, + onClick, + ariaLabel, + children, + ...rest +}) { + const convertedTitle = useMemo(() => { + if (title && typeof title === 'string') { + const cleanedTitle = title.replace(/(\r\n|\n)/g, ""); + return $L(marqueeDisabled ? title : cleanedTitle); + } + return ''; + }, [marqueeDisabled, title]); + + const _onClick = useCallback( + (e) => { + if (onClick) { + onClick(e); + } + }, + [onClick] + ); + + const _onSpotlightUp = (e) => { + if (onSpotlightUp) { + onSpotlightUp(e); + } + }; + + const _onSpotlightLeft = (e) => { + if (onSpotlightLeft) { + onSpotlightLeft(e); + } + }; + + return ( + + {onBackButton && ( + + )} +
+ + {convertedTitle && ( + + )} + + {children} + + ); +} \ No newline at end of file diff --git a/com.twin.app.shoptime/src/views/DetailPanel/components/THeaderCustom.module.less b/com.twin.app.shoptime/src/views/DetailPanel/components/THeaderCustom.module.less new file mode 100644 index 00000000..dcb534a4 --- /dev/null +++ b/com.twin.app.shoptime/src/views/DetailPanel/components/THeaderCustom.module.less @@ -0,0 +1,49 @@ +@import "../../../style/CommonStyle.module.less"; +@import "../../../style/utils.module.less"; + +.tHeaderCustom { + align-self: stretch; + margin: 30px 0; // ์ƒํ•˜ 30px, ์ขŒ์šฐ 0px ๋งˆ์ง„ (DetailPanel์—์„œ ์ด๋ฏธ 60px padding ์ ์šฉ) + height: 60px; // ๋งˆ์ง„์„ ์ œ์™ธํ•œ ๋†’์ด 60px + display: flex; + justify-content: flex-start; + align-items: center; + background-color: transparent; // DetailPanel์—์„œ๋Š” ๋ฐฐ๊ฒฝ ํˆฌ๋ช… + + .title { + font-size: 25px; + font-weight: 600; + color: #EAEAEA; + padding-left: 0; + letter-spacing: 1px; + text-transform: uppercase; + margin-right: 20px; // Header Title ํ›„ ๊ฐ„๊ฒฉ (children๊ณผ์˜ gap) + white-space: nowrap; + overflow: hidden; + } +} + +.button { + .size(@w: 60px, @h: 60px); + background-size: 60px 60px; + background-position: center; + background-image: url("../../../../assets/images/btn/btn-60-bk-back-nor@3x.png"); + border: none; + flex-shrink: 0; + margin-right: 20px; // ๋˜๋Œ์•„๊ฐ€๊ธฐ ์•„์ด์ฝ˜ ํ›„ 20px gap + + &:focus { + border-radius: 10px; + background-image: url("../../../../assets/images/btn/btn-60-wh-back-foc@3x.png"); + box-shadow: 0px 6px 30px 0 rgba(0, 0, 0, 0.4); + } +} + +.centerImage { + .size(@w: 60px, @h: 60px); + background-size: contain; + background-position: center; + background-repeat: no-repeat; + flex-shrink: 0; + margin-right: 10px; // ํŒŒํŠธ๋„ˆ์‚ฌ ๋กœ๊ณ  ํ›„ 10px gap +} \ No newline at end of file diff --git a/com.twin.app.shoptime/src/views/DetailPanel/components/TScroller/TScrollerDetail.jsx b/com.twin.app.shoptime/src/views/DetailPanel/components/TScroller/TScrollerDetail.jsx new file mode 100644 index 00000000..dc4c9a18 --- /dev/null +++ b/com.twin.app.shoptime/src/views/DetailPanel/components/TScroller/TScrollerDetail.jsx @@ -0,0 +1,244 @@ +import React, { + useCallback, + useEffect, + useRef, + useState, + useMemo, + forwardRef, +} from "react"; + +import classNames from "classnames"; +import { useSelector } from "react-redux"; + +import { off, on } from "@enact/core/dispatcher"; +import { Job } from "@enact/core/util"; +import Scroller from "@enact/sandstone/Scroller"; + +import AutoScrollArea, { POSITION } from "../../../../components/AutoScrollArea/AutoScrollArea"; +import css from "./TScrollerDetail.module.less"; + +/** + * DetailPanel ์ „์šฉ TScroller - ์ปค์Šคํ…€ ์Šคํฌ๋กค๋ฐ” ๊ตฌํ˜„ + * onScroll* event can't use Callback dependency + */ +const TScrollerDetail = forwardRef(({ + className, + children, + verticalScrollbar = "hidden", + focusableScrollbar = false, + direction = "vertical", + horizontalScrollbar = "hidden", + scrollMode, + onScrollStart, + onScrollStop, + onScroll, + noScrollByWheel = false, + cbScrollTo, + autoScroll = direction === "horizontal", + setScrollVerticalPos, + setCheckScrollPosition, + ...rest +}, ref) => { + const { cursorVisible } = useSelector((state) => state.common.appStatus); + + const isScrolling = useRef(false); + const scrollPosition = useRef("top"); + + const scrollToRef = useRef(null); + const scrollHorizontalPos = useRef(0); + const scrollVerticalPos = useRef(0); + const actualScrollerElement = useRef(null); // ์‹ค์ œ ์Šคํฌ๋กค DOM ์š”์†Œ + + const [isMounted, setIsMounted] = useState(false); + + // ref๋ฅผ ๋‚ด๋ถ€ Scroller ์š”์†Œ์— ์—ฐ๊ฒฐ + useEffect(() => { + if (ref && isMounted) { + // DOM์—์„œ Scroller ์š”์†Œ ์ฐพ๊ธฐ + let scrollerElement = document.querySelector(`.${css.tScroller}`); + + if (!scrollerElement) { + // ๋‹ค๋ฅธ ๋ฐฉ๋ฒ•์œผ๋กœ ์ฐพ๊ธฐ + scrollerElement = document.querySelector('[data-spotlight-container="true"]'); + } + + if (!scrollerElement) { + // ์Šคํฌ๋กค ๊ฐ€๋Šฅํ•œ ์š”์†Œ ์ฐพ๊ธฐ + scrollerElement = document.querySelector('[style*="overflow"]'); + } + + if (scrollerElement) { + // ref๊ฐ€ ํ•จ์ˆ˜์ธ ๊ฒฝ์šฐ์™€ ๊ฐ์ฒด์ธ ๊ฒฝ์šฐ๋ฅผ ๋ชจ๋‘ ์ฒ˜๋ฆฌ + if (typeof ref === 'function') { + ref(scrollerElement); + } else if (ref && ref.current !== undefined) { + ref.current = scrollerElement; + } + actualScrollerElement.current = scrollerElement; // ์‹ค์ œ ์Šคํฌ๋กค ์š”์†Œ ์ €์žฅ + } + } + }, [ref, isMounted]); + + // ์Šคํฌ๋กค ์ œ์–ด ๋ฉ”์„œ๋“œ ์ถ”๊ฐ€ + const scrollToElement = useCallback((element) => { + if (actualScrollerElement.current && element) { + const scrollerRect = actualScrollerElement.current.getBoundingClientRect(); + const elementRect = element.getBoundingClientRect(); + const relativeTop = elementRect.top - scrollerRect.top; + const scrollTop = actualScrollerElement.current.scrollTop + relativeTop - 20; + + actualScrollerElement.current.scrollTo({ + top: scrollTop, + behavior: 'smooth' + }); + } + }, []); + + useEffect(() => { + setIsMounted(true); + + return () => setIsMounted(false); + }, []); + + const _onScrollStart = useCallback( + (e) => { + if (onScrollStart) { + onScrollStart(e); + } + + isScrolling.current = true; + }, + [onScrollStart] + ); + + const _onScrollStop = useCallback( + (e) => { + if (onScrollStop) { + onScrollStop(e); + } + + isScrolling.current = false; + + if (e.reachedEdgeInfo) { + if (e.reachedEdgeInfo.top) { + scrollPosition.current = "top"; + } else if (e.reachedEdgeInfo.bottom) { + scrollPosition.current = "bottom"; + } else if (e.reachedEdgeInfo.left) { + scrollPosition.current = "left"; + } else if (e.reachedEdgeInfo.right) { + scrollPosition.current = "right"; + } else { + scrollPosition.current = "middle"; + } + } else { + scrollPosition.current = "middle"; + } + + scrollHorizontalPos.current = e.scrollLeft; + scrollVerticalPos.current = e.scrollTop; + + if (setScrollVerticalPos) { + setScrollVerticalPos(scrollVerticalPos.current); + } + if (setCheckScrollPosition) { + setCheckScrollPosition(scrollPosition.current); + } + }, + [onScrollStop] + ); + + const _onScroll = useCallback( + (ev) => { + if (onScroll) { + onScroll(ev); + } + }, + [onScroll] + ); + + const _cbScrollTo = useCallback( + (ref) => { + if (cbScrollTo) { + cbScrollTo(ref); + } + + scrollToRef.current = ref; + }, + [cbScrollTo] + ); + + const relevantPositions = useMemo(() => { + switch (direction) { + case "horizontal": + return ["left", "right"]; + case "vertical": + return ["top", "bottom"]; + default: + return []; + } + }, [direction]); + + return ( +
+ + {children} + + {cursorVisible && + autoScroll && + relevantPositions.map((pos) => ( + + ))} +
+ ); +}); + +// TScrollerDetail์— ๋ฉ”์„œ๋“œ ๋…ธ์ถœ +TScrollerDetail.scrollToElement = (element) => { + // ์ด ๋ฉ”์„œ๋“œ๋Š” ref๋ฅผ ํ†ตํ•ด ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๋„๋ก ๊ตฌํ˜„ +}; + +// displayName์„ ๋ช…ํ™•ํ•˜๊ฒŒ ์„ค์ • +TScrollerDetail.displayName = 'TScrollerDetail'; + +// forwardRef๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ์ปดํฌ๋„ŒํŠธ์ž„์„ ๋ช…์‹œ +export default TScrollerDetail; \ No newline at end of file diff --git a/com.twin.app.shoptime/src/views/DetailPanel/components/TScroller/TScrollerDetail.module.less b/com.twin.app.shoptime/src/views/DetailPanel/components/TScroller/TScrollerDetail.module.less new file mode 100644 index 00000000..ec226c6c --- /dev/null +++ b/com.twin.app.shoptime/src/views/DetailPanel/components/TScroller/TScrollerDetail.module.less @@ -0,0 +1,39 @@ +@import "../../../../style/CommonStyle.module.less"; +@import "../../../../style/utils.module.less"; + +// DetailPanel ์ „์šฉ TScroller ์Šคํƒ€์ผ +.scrollerContainer { + position: relative; + width: 100%; + height: 100%; + overflow: hidden; + + .tScroller { + width: 100%; + height: 100%; + position: relative; + overflow-y: auto; + overflow-x: hidden; + + // Sandstone Scroller ๋‚ด๋ถ€ ์ฝ˜ํ…์ธ  ์ปจํ…Œ์ด๋„ˆ ์Šคํƒ€์ผ + > div:nth-child(1) { + width: 100%; + height: 100%; + padding: 0; + margin: 0; + box-sizing: border-box; + overflow: visible; + } + + // ๋‘ ๋ฒˆ์งธ ์ž์‹ ์š”์†Œ (์Šคํฌ๋กค๋ฐ” ์˜์—ญ) ์™„์ „ํžˆ ์ˆจ๊น€ + // > div:nth-child(2) { + // display: none !important; + // } + + &.preventScroll { + > div { + overflow: hidden !important; // prevent wheel event + } + } + } +} \ No newline at end of file