[250929] feat: ShopByMobile관련 컴포넌트 추가
This commit is contained in:
BIN
com.twin.app.shoptime/assets/images/image-mobile-backspace.png
Normal file
BIN
com.twin.app.shoptime/assets/images/image-mobile-backspace.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 641 B |
@@ -0,0 +1,679 @@
|
|||||||
|
import React, {
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
|
|
||||||
|
import classNames from "classnames";
|
||||||
|
import {
|
||||||
|
AsYouTypeFormatter,
|
||||||
|
PhoneNumberFormat,
|
||||||
|
PhoneNumberUtil,
|
||||||
|
} from "google-libphonenumber";
|
||||||
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
|
||||||
|
import { off, on } from "@enact/core/dispatcher";
|
||||||
|
import spotlight, { Spotlight } from "@enact/spotlight";
|
||||||
|
import { SpotlightContainerDecorator } from "@enact/spotlight/SpotlightContainerDecorator";
|
||||||
|
import { Spottable } from "@enact/spotlight/Spottable";
|
||||||
|
|
||||||
|
import defaultImage from "../../../assets/images/img-thumb-empty-144@3x.png";
|
||||||
|
import { types } from "../../actions/actionTypes";
|
||||||
|
import { clearSMS, sendSms } from "../../actions/appDataActions";
|
||||||
|
import {
|
||||||
|
changeLocalSettings,
|
||||||
|
setHidePopup,
|
||||||
|
setShowPopup,
|
||||||
|
} from "../../actions/commonActions";
|
||||||
|
import {
|
||||||
|
clearRegisterDeviceInfo,
|
||||||
|
getDeviceAdditionInfo,
|
||||||
|
registerDeviceInfo,
|
||||||
|
} from "../../actions/deviceActions";
|
||||||
|
import {
|
||||||
|
clearCurationCoupon,
|
||||||
|
setEventIssueReq,
|
||||||
|
} from "../../actions/eventActions";
|
||||||
|
import {
|
||||||
|
sendLogShopByMobile,
|
||||||
|
sendLogTotalRecommend,
|
||||||
|
} from "../../actions/logActions";
|
||||||
|
import {
|
||||||
|
ACTIVE_POPUP,
|
||||||
|
LOG_CONTEXT_NAME,
|
||||||
|
LOG_MESSAGE_ID,
|
||||||
|
LOG_TP_NO,
|
||||||
|
} from "../../utils/Config";
|
||||||
|
import {
|
||||||
|
$L,
|
||||||
|
decryptPhoneNumber,
|
||||||
|
encryptPhoneNumber,
|
||||||
|
formatLocalDateTime,
|
||||||
|
} from "../../utils/helperMethods";
|
||||||
|
import CustomImage from "../CustomImage/CustomImage";
|
||||||
|
import TButton from "../TButton/TButton";
|
||||||
|
import TPopUp from "../TPopUp/TPopUp";
|
||||||
|
import HistoryPhoneNumber from "./HistoryPhoneNumber/HistoryPhoneNumber";
|
||||||
|
import css from "./MobileSendPopUp.module.less";
|
||||||
|
import SMSNumKeyPad from "./SMSNumKeyPad";
|
||||||
|
|
||||||
|
const SECRET_KEY = "fy7BTKuM9eeTQqEC9sF3Iw5qG43Aaip";
|
||||||
|
|
||||||
|
const Container = SpotlightContainerDecorator(
|
||||||
|
{ enterTo: "last-focused" },
|
||||||
|
"div"
|
||||||
|
);
|
||||||
|
|
||||||
|
const InputContainer = SpotlightContainerDecorator(
|
||||||
|
{ enterTo: "last-focused" },
|
||||||
|
"div"
|
||||||
|
);
|
||||||
|
|
||||||
|
const SpottableComponent = Spottable("div");
|
||||||
|
|
||||||
|
export default function MobileSendPopUp({
|
||||||
|
open,
|
||||||
|
onClose,
|
||||||
|
title,
|
||||||
|
subTitle,
|
||||||
|
productImg,
|
||||||
|
productPrice,
|
||||||
|
smsTpCd,
|
||||||
|
smsText,
|
||||||
|
prdtId,
|
||||||
|
curationId,
|
||||||
|
patnrId,
|
||||||
|
evntId,
|
||||||
|
evntTpCd,
|
||||||
|
isCurationEvnt,
|
||||||
|
hotelId,
|
||||||
|
hotelNm,
|
||||||
|
hotelDtlUrl,
|
||||||
|
shopByMobileLogRef,
|
||||||
|
spotlightId,
|
||||||
|
patncNm,
|
||||||
|
}) {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const deviceInfo = useSelector((state) => state.device.deviceInfo);
|
||||||
|
const { httpHeader } = useSelector((state) => state.common);
|
||||||
|
const regDeviceInfoRetCode = useSelector(
|
||||||
|
(state) => state.device?.regDeviceInfoData?.retCode
|
||||||
|
);
|
||||||
|
const smsRetCodeResData = useSelector(
|
||||||
|
(state) => state.appData.sendSms?.retCode
|
||||||
|
);
|
||||||
|
const { phoneNumberList } = useSelector(
|
||||||
|
(state) => state.localSettings.phoneNumbers
|
||||||
|
);
|
||||||
|
const curationCouponSuccess = useSelector(
|
||||||
|
(state) => state.event.curationCouponSuccess
|
||||||
|
);
|
||||||
|
const popupVisible = useSelector((state) => state.common.popup.popupVisible);
|
||||||
|
const nowMenu = useSelector((state) => state.common.menu.nowMenu);
|
||||||
|
const entryMenu = useSelector((state) => state.common.menu.entryMenu);
|
||||||
|
const [inputDisabled, setInputDisabled] = useState(true);
|
||||||
|
const [mobileNumber, setMobileNumber] = useState("");
|
||||||
|
const [recentSentNumber, setRecentSentNumber] = useState([]);
|
||||||
|
const [keyPadOff, setKeyPadOff] = useState(false);
|
||||||
|
const [smsRetCode, setSmsRetCode] = useState(undefined);
|
||||||
|
|
||||||
|
const agreeBtnClickedRef = useRef(false);
|
||||||
|
|
||||||
|
const deviceCountryCode = httpHeader["X-Device-Country"];
|
||||||
|
|
||||||
|
const mobileSendPopUpSpotlightId = useMemo(() => {
|
||||||
|
return !keyPadOff && recentSentNumber.length <= 0
|
||||||
|
? "keypad-number-1"
|
||||||
|
: "agreeAndSend";
|
||||||
|
}, [keyPadOff, recentSentNumber]);
|
||||||
|
|
||||||
|
const getMaxNum = useCallback((_deviceCountryCode) => {
|
||||||
|
if (_deviceCountryCode === "DE" || _deviceCountryCode === "GB") {
|
||||||
|
return 11;
|
||||||
|
} else if (_deviceCountryCode === "KR") {
|
||||||
|
return 12;
|
||||||
|
} else return 10;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const MSG_SUCCESS_SENT = $L("Text Send to") + " " + mobileNumber;
|
||||||
|
const MSG_SEND_LINK = $L("Send a purchase link for this item via SMS");
|
||||||
|
|
||||||
|
const handleClickSelect = (_phoneNumber) => {
|
||||||
|
setKeyPadOff((state) => !state);
|
||||||
|
setMobileNumber(_phoneNumber);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
Spotlight.focus("keypad-number-1");
|
||||||
|
}, 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInputClick = () => {
|
||||||
|
setKeyPadOff(false);
|
||||||
|
if (recentSentNumber && recentSentNumber.length > 0 && keyPadOff) {
|
||||||
|
setMobileNumber(recentSentNumber[0]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getRawPhoneNumber = useCallback(
|
||||||
|
(key) => {
|
||||||
|
let rawPhoneNumber = `${mobileNumber}${key}`.replace(/\D/g, "");
|
||||||
|
if (rawPhoneNumber.length === getMaxNum(deviceCountryCode)) {
|
||||||
|
Spotlight.focus("agreeAndSend");
|
||||||
|
}
|
||||||
|
if (rawPhoneNumber.length > getMaxNum(deviceCountryCode)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const phoneUtil = PhoneNumberUtil.getInstance();
|
||||||
|
const asYouTypeFormatter = new AsYouTypeFormatter(deviceCountryCode);
|
||||||
|
|
||||||
|
try {
|
||||||
|
let numberProto = phoneUtil.parse(rawPhoneNumber, deviceCountryCode);
|
||||||
|
if (
|
||||||
|
phoneUtil.isValidNumber(numberProto) &&
|
||||||
|
phoneUtil.isValidNumberForRegion(numberProto, deviceCountryCode)
|
||||||
|
) {
|
||||||
|
rawPhoneNumber = phoneUtil.format(
|
||||||
|
numberProto,
|
||||||
|
PhoneNumberFormat.NATIONAL
|
||||||
|
);
|
||||||
|
if (deviceCountryCode === "RU" && rawPhoneNumber.startsWith("8")) {
|
||||||
|
rawPhoneNumber = rawPhoneNumber.substring(1);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let formattedNumber = "";
|
||||||
|
|
||||||
|
for (let i = 0; i < rawPhoneNumber.length; i++) {
|
||||||
|
formattedNumber = asYouTypeFormatter.inputDigit(
|
||||||
|
Number(rawPhoneNumber[i])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
rawPhoneNumber = formattedNumber;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
rawPhoneNumber = `${mobileNumber}${key}`;
|
||||||
|
} finally {
|
||||||
|
setMobileNumber(rawPhoneNumber);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[mobileNumber, deviceCountryCode]
|
||||||
|
);
|
||||||
|
|
||||||
|
const getBackspaceRawNumber = useCallback(() => {
|
||||||
|
let rawPhoneNumber = mobileNumber.replace(/\D/g, "").slice(0, -1);
|
||||||
|
const phoneUtil = PhoneNumberUtil.getInstance();
|
||||||
|
const asYouTypeFormatter = new AsYouTypeFormatter(deviceCountryCode);
|
||||||
|
|
||||||
|
try {
|
||||||
|
let numberProto = phoneUtil.parse(rawPhoneNumber, deviceCountryCode);
|
||||||
|
if (
|
||||||
|
phoneUtil.isValidNumber(numberProto) &&
|
||||||
|
phoneUtil.isValidNumberForRegion(numberProto, deviceCountryCode)
|
||||||
|
) {
|
||||||
|
rawPhoneNumber = phoneUtil.format(
|
||||||
|
numberProto,
|
||||||
|
PhoneNumberFormat.NATIONAL
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
let formattedNumber = "";
|
||||||
|
|
||||||
|
for (let i = 0; i < rawPhoneNumber.length; i++) {
|
||||||
|
formattedNumber = asYouTypeFormatter.inputDigit(
|
||||||
|
Number(rawPhoneNumber[i])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
rawPhoneNumber = formattedNumber;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
rawPhoneNumber = mobileNumber.slice(0, -1);
|
||||||
|
} finally {
|
||||||
|
setMobileNumber(rawPhoneNumber);
|
||||||
|
}
|
||||||
|
}, [mobileNumber, deviceCountryCode]);
|
||||||
|
|
||||||
|
const handleKeydown = useCallback(
|
||||||
|
(ev) => {
|
||||||
|
if (ev && ev.key >= 0 && ev.key <= 9) {
|
||||||
|
getRawPhoneNumber(ev.key);
|
||||||
|
} else if (ev.key === "Backspace") {
|
||||||
|
getBackspaceRawNumber();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[mobileNumber]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
on("keydown", handleKeydown);
|
||||||
|
return () => {
|
||||||
|
off("keydown", handleKeydown);
|
||||||
|
};
|
||||||
|
}, [handleKeydown]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!deviceInfo) {
|
||||||
|
dispatch(getDeviceAdditionInfo());
|
||||||
|
}
|
||||||
|
}, [deviceInfo, dispatch]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const timer = setTimeout(() =>
|
||||||
|
setInputDisabled(mobileSendPopUpSpotlightId === "keypad-number-1")
|
||||||
|
);
|
||||||
|
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}, [mobileSendPopUpSpotlightId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!agreeBtnClickedRef.current && recentSentNumber) {
|
||||||
|
if (recentSentNumber.length > 0) {
|
||||||
|
setKeyPadOff(true);
|
||||||
|
setMobileNumber(recentSentNumber[0]);
|
||||||
|
} else {
|
||||||
|
setKeyPadOff(false);
|
||||||
|
setMobileNumber("");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [recentSentNumber]);
|
||||||
|
|
||||||
|
const numKeypadClicked = useCallback(
|
||||||
|
(key) => {
|
||||||
|
if (key === "clear") {
|
||||||
|
setMobileNumber("");
|
||||||
|
} else if (key == "backspace") {
|
||||||
|
getBackspaceRawNumber();
|
||||||
|
} else {
|
||||||
|
getRawPhoneNumber(key);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[deviceCountryCode, mobileNumber]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleDelete = (selectedIndex) => {
|
||||||
|
if (recentSentNumber) {
|
||||||
|
const updateItems = recentSentNumber.filter(
|
||||||
|
(_, index) => index !== selectedIndex
|
||||||
|
);
|
||||||
|
setRecentSentNumber(updateItems);
|
||||||
|
|
||||||
|
const encryptedNumbers = updateItems.map(encryptPhoneNumber);
|
||||||
|
|
||||||
|
dispatch(
|
||||||
|
changeLocalSettings({
|
||||||
|
phoneNumbers: {
|
||||||
|
phoneNumberList: encryptedNumbers,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAgreeSendClick = useCallback(() => {
|
||||||
|
let naturalNumber = mobileNumber.replace(/\D/g, "");
|
||||||
|
|
||||||
|
if (!mobileNumber || naturalNumber.length < getMaxNum(deviceCountryCode)) {
|
||||||
|
setSmsRetCode(907);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (deviceCountryCode === "KR") {
|
||||||
|
naturalNumber = "82" + naturalNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (recentSentNumber && recentSentNumber.length > 0) {
|
||||||
|
const updatedNumbers = [...recentSentNumber];
|
||||||
|
|
||||||
|
const existingNumberIndex = updatedNumbers.findIndex(
|
||||||
|
(existingNum) => existingNum === mobileNumber
|
||||||
|
);
|
||||||
|
if (existingNumberIndex !== -1) {
|
||||||
|
updatedNumbers.splice(existingNumberIndex, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updatedNumbers.length >= 5) {
|
||||||
|
updatedNumbers.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
updatedNumbers.unshift(mobileNumber);
|
||||||
|
agreeBtnClickedRef.current = true;
|
||||||
|
setRecentSentNumber(updatedNumbers);
|
||||||
|
|
||||||
|
const encryptedNumbers = updatedNumbers.map(encryptPhoneNumber);
|
||||||
|
|
||||||
|
dispatch(
|
||||||
|
changeLocalSettings({
|
||||||
|
phoneNumbers: {
|
||||||
|
phoneNumberList: encryptedNumbers,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
setRecentSentNumber([mobileNumber]);
|
||||||
|
dispatch(
|
||||||
|
changeLocalSettings({
|
||||||
|
phoneNumbers: {
|
||||||
|
phoneNumberList: [encryptPhoneNumber(mobileNumber)],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isCurationEvnt) {
|
||||||
|
dispatch(
|
||||||
|
setEventIssueReq({
|
||||||
|
evntTpCd,
|
||||||
|
evntId,
|
||||||
|
mbphNo: naturalNumber,
|
||||||
|
cntryCd: deviceCountryCode,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (deviceInfo && smsTpCd) {
|
||||||
|
let params = {
|
||||||
|
dvcIndex: deviceInfo.dvcIndex,
|
||||||
|
mbphNo: naturalNumber,
|
||||||
|
smsTpCd,
|
||||||
|
patnrId,
|
||||||
|
evntId,
|
||||||
|
curationId,
|
||||||
|
smsText,
|
||||||
|
prdtId,
|
||||||
|
entryMenu: nowMenu,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 호텔일 경우 날려야 하는 경우
|
||||||
|
if (smsTpCd === "APP00205") {
|
||||||
|
params = { ...params, hotelId, hotelNm, hotelDtlUrl, curationId };
|
||||||
|
}
|
||||||
|
if (smsTpCd === "APP00204") {
|
||||||
|
params = { ...params, curationId };
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(sendSms(params));
|
||||||
|
}
|
||||||
|
// EVT00101 & APP00207(welcome) EVT00103 & APP00209 (welcome+Prizes) : smsTpCd 값을 받지 않음
|
||||||
|
if (evntTpCd === "EVT00101" || evntTpCd === "EVT00103") {
|
||||||
|
dispatch(
|
||||||
|
registerDeviceInfo({
|
||||||
|
evntTpCd,
|
||||||
|
evntId,
|
||||||
|
evntApplcnFlag: "Y",
|
||||||
|
entryMenu: "TermsPop",
|
||||||
|
mbphNo: naturalNumber,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
dispatch,
|
||||||
|
mobileNumber,
|
||||||
|
smsTpCd,
|
||||||
|
evntTpCd,
|
||||||
|
deviceInfo,
|
||||||
|
deviceCountryCode,
|
||||||
|
nowMenu,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const _onClose = useCallback(
|
||||||
|
(e) => {
|
||||||
|
if (e.target) {
|
||||||
|
dispatch(clearSMS());
|
||||||
|
dispatch(clearCurationCoupon());
|
||||||
|
setSmsRetCode(undefined);
|
||||||
|
}
|
||||||
|
onClose();
|
||||||
|
dispatch(setShowPopup(ACTIVE_POPUP.smsPopup));
|
||||||
|
|
||||||
|
setTimeout(() => Spotlight.focus("agreeAndSend"));
|
||||||
|
},
|
||||||
|
[dispatch, smsRetCode]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setSmsRetCode(smsRetCodeResData);
|
||||||
|
|
||||||
|
let timer;
|
||||||
|
|
||||||
|
if (
|
||||||
|
smsRetCodeResData === 0 ||
|
||||||
|
regDeviceInfoRetCode === 0 ||
|
||||||
|
curationCouponSuccess === 0
|
||||||
|
) {
|
||||||
|
const logParams = {
|
||||||
|
status: "send",
|
||||||
|
nowMenu: nowMenu,
|
||||||
|
partner: patncNm,
|
||||||
|
contextName: LOG_CONTEXT_NAME.SHOPBYMOBILE,
|
||||||
|
messageId: LOG_MESSAGE_ID.SMB,
|
||||||
|
};
|
||||||
|
dispatch(sendLogTotalRecommend(logParams));
|
||||||
|
if (shopByMobileLogRef) {
|
||||||
|
let params = {
|
||||||
|
...shopByMobileLogRef.current,
|
||||||
|
locDt: formatLocalDateTime(new Date()),
|
||||||
|
logTpNo: LOG_TP_NO.SHOP_BY_MOBILE.AGREE_AND_SEND,
|
||||||
|
mbphNoFlag: "Y",
|
||||||
|
trmsAgrFlag: "Y",
|
||||||
|
};
|
||||||
|
|
||||||
|
dispatch(sendLogShopByMobile(params));
|
||||||
|
shopByMobileLogRef.current = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (spotlightId) Spotlight.focus(spotlightId);
|
||||||
|
else Spotlight.focus();
|
||||||
|
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
shopByMobileLogRef,
|
||||||
|
smsRetCodeResData,
|
||||||
|
regDeviceInfoRetCode,
|
||||||
|
curationCouponSuccess,
|
||||||
|
spotlightId,
|
||||||
|
]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// retCode initialize
|
||||||
|
return () => {
|
||||||
|
dispatch(clearSMS());
|
||||||
|
dispatch(clearRegisterDeviceInfo());
|
||||||
|
dispatch(clearCurationCoupon());
|
||||||
|
dispatch(setHidePopup());
|
||||||
|
};
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
const getSmsErrorMsg = useMemo(() => {
|
||||||
|
const SMS_ERROR_502 = $L("The event information has not been registered");
|
||||||
|
const SMS_ERROR_903 = $L("You have exceeded the daily text limit.");
|
||||||
|
const SMS_ERROR_904 = $L(
|
||||||
|
"You have exceeded the text limit for this product."
|
||||||
|
);
|
||||||
|
const SMS_ERROR_905 = $L(
|
||||||
|
"This number is currently blocked. To receive a message, please send UNSTOP to the number below. 07860 064195"
|
||||||
|
);
|
||||||
|
const SMS_ERROR_906 = $L("Sorry. This item is sold out.");
|
||||||
|
const SMS_ERROR_600 = $L("This device had received first time coupon.");
|
||||||
|
const SMS_ERROR_601 = $L("There is no coupon.");
|
||||||
|
const SMS_ERROR_900 = $L("Failed to send text to {mobileNumber}").replace(
|
||||||
|
"{mobileNumber}",
|
||||||
|
mobileNumber
|
||||||
|
);
|
||||||
|
const SMS_ERROR_907 = $L(
|
||||||
|
"Only {length} digits is permitted. Please check again"
|
||||||
|
).replace("{length}", getMaxNum(deviceCountryCode));
|
||||||
|
|
||||||
|
switch (smsRetCode) {
|
||||||
|
case 502:
|
||||||
|
return SMS_ERROR_502;
|
||||||
|
case 600:
|
||||||
|
return SMS_ERROR_600;
|
||||||
|
case 601:
|
||||||
|
return SMS_ERROR_601;
|
||||||
|
case 900:
|
||||||
|
return SMS_ERROR_900;
|
||||||
|
case 903:
|
||||||
|
return SMS_ERROR_903;
|
||||||
|
case 904:
|
||||||
|
return SMS_ERROR_904;
|
||||||
|
case 905:
|
||||||
|
return SMS_ERROR_905;
|
||||||
|
case 906:
|
||||||
|
return SMS_ERROR_906;
|
||||||
|
case 907:
|
||||||
|
return smsRetCodeResData === 907 ? SMS_ERROR_900 : SMS_ERROR_907;
|
||||||
|
default:
|
||||||
|
return SMS_ERROR_900;
|
||||||
|
}
|
||||||
|
}, [smsRetCode, mobileNumber, smsRetCodeResData]);
|
||||||
|
|
||||||
|
const getEvntErrorMsg = useMemo(() => {
|
||||||
|
if (curationCouponSuccess === 600) {
|
||||||
|
return $L("This device had received first time coupon.");
|
||||||
|
} else if (curationCouponSuccess === 601) {
|
||||||
|
return $L("There is no coupon.");
|
||||||
|
} else {
|
||||||
|
return $L("Failed to sent text to {mobileNumber}").replace(
|
||||||
|
"{mobileNumber}",
|
||||||
|
mobileNumber
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [curationCouponSuccess, mobileNumber]);
|
||||||
|
|
||||||
|
const retCodeError =
|
||||||
|
(smsRetCode !== undefined && smsRetCode !== 0) ||
|
||||||
|
(curationCouponSuccess !== undefined && curationCouponSuccess !== 0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (phoneNumberList) {
|
||||||
|
const decryptedNumbers = phoneNumberList.map(decryptPhoneNumber);
|
||||||
|
setRecentSentNumber(decryptedNumbers);
|
||||||
|
}
|
||||||
|
}, [phoneNumberList]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{smsRetCode === undefined &&
|
||||||
|
open &&
|
||||||
|
regDeviceInfoRetCode === undefined &&
|
||||||
|
curationCouponSuccess === undefined && (
|
||||||
|
<TPopUp
|
||||||
|
kind={"mobileSendPopup"}
|
||||||
|
className={css.container}
|
||||||
|
open={open}
|
||||||
|
onClose={onClose}
|
||||||
|
hasText
|
||||||
|
spotlightId={mobileSendPopUpSpotlightId}
|
||||||
|
>
|
||||||
|
<div className={css.header}>
|
||||||
|
{productImg && (
|
||||||
|
<p className={css.productImg}>
|
||||||
|
<CustomImage
|
||||||
|
className={css.img}
|
||||||
|
src={productImg}
|
||||||
|
fallbackSrc={defaultImage}
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
css.textBox,
|
||||||
|
!productImg && css.textBoxOnly
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{title && <div className={css.smsTitle}>{title}</div>}
|
||||||
|
{subTitle && (
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
css.subTitle,
|
||||||
|
!productImg && css.subTitleOnly
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{subTitle}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{productPrice && (
|
||||||
|
<div className={css.price}>{productPrice}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={css.smsArea}>
|
||||||
|
<div className={css.smsContainer}>
|
||||||
|
<InputContainer className={css.smsNumLayer}>
|
||||||
|
<SpottableComponent
|
||||||
|
className={css.inputNum}
|
||||||
|
onClick={handleInputClick}
|
||||||
|
spotlightDisabled={inputDisabled}
|
||||||
|
>
|
||||||
|
{deviceCountryCode && deviceCountryCode === "RU" && (
|
||||||
|
<span className={css.rucInput}>{"+7 "}</span>
|
||||||
|
)}
|
||||||
|
<span>{mobileNumber}</span>
|
||||||
|
</SpottableComponent>
|
||||||
|
</InputContainer>
|
||||||
|
|
||||||
|
<Container>
|
||||||
|
{keyPadOff && recentSentNumber.length > 0 ? (
|
||||||
|
<HistoryPhoneNumber
|
||||||
|
handleClickSelect={handleClickSelect}
|
||||||
|
handleDelete={handleDelete}
|
||||||
|
recentSentNumber={recentSentNumber}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<SMSNumKeyPad onKeyDown={numKeypadClicked} />
|
||||||
|
)}
|
||||||
|
</Container>
|
||||||
|
</div>
|
||||||
|
<div className={css.instruction}>
|
||||||
|
{deviceCountryCode && (
|
||||||
|
<span
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: `${$L(
|
||||||
|
"By clicking Agree and Send button, I agree that LGE may collect and store my cell phone number to send text messages as I requested, for data analysis and for feature-enhancement purposes. By entering my cell phone number, I agree to receive messages from LGE with information on how to purchase the product I selected. Message and data rates may apply."
|
||||||
|
)}`,
|
||||||
|
}}
|
||||||
|
className={deviceCountryCode === "RU" && css.instructionRu}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Container className={css.btnContainer}>
|
||||||
|
<TButton
|
||||||
|
onClick={handleAgreeSendClick}
|
||||||
|
spotlightId={"agreeAndSend"}
|
||||||
|
>
|
||||||
|
{$L("Agree and Send")}
|
||||||
|
</TButton>
|
||||||
|
<TButton onClick={onClose}>{$L("Cancel")}</TButton>
|
||||||
|
</Container>
|
||||||
|
</TPopUp>
|
||||||
|
)}
|
||||||
|
{retCodeError && (
|
||||||
|
<TPopUp
|
||||||
|
kind="textPopup"
|
||||||
|
open={popupVisible}
|
||||||
|
hasText
|
||||||
|
text={smsTpCd ? getSmsErrorMsg : getEvntErrorMsg}
|
||||||
|
onClick={_onClose}
|
||||||
|
hasButton
|
||||||
|
button1Text={$L("OK")}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{(smsRetCode === 0 ||
|
||||||
|
regDeviceInfoRetCode === 0 ||
|
||||||
|
regDeviceInfoRetCode === 0) && (
|
||||||
|
<TPopUp
|
||||||
|
kind="textPopup"
|
||||||
|
open={popupVisible}
|
||||||
|
hasText
|
||||||
|
text={MSG_SUCCESS_SENT}
|
||||||
|
onClick={onClose}
|
||||||
|
hasButton
|
||||||
|
button1Text={$L("OK")}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
@import "../../style/CommonStyle.module.less";
|
||||||
|
@import "../../style/utils.module.less";
|
||||||
|
|
||||||
|
.container {
|
||||||
|
.header {
|
||||||
|
position: relative;
|
||||||
|
&::after {
|
||||||
|
width: 100%;
|
||||||
|
height: 85px;
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
z-index: 0;
|
||||||
|
background-color: @COLOR_SKYBLUE;
|
||||||
|
content: "";
|
||||||
|
}
|
||||||
|
display: flex;
|
||||||
|
> .productImg,
|
||||||
|
> .textBox {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
> .textBoxOnly {
|
||||||
|
width: 100%;
|
||||||
|
margin-left: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .productImg {
|
||||||
|
margin: 30px 0px 0 60px;
|
||||||
|
|
||||||
|
> .img {
|
||||||
|
.border-solid(@size:1px,@color:#cccccc);
|
||||||
|
width: 132px;
|
||||||
|
height: 132px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.smsTitle {
|
||||||
|
width: 940px;
|
||||||
|
text-align: left;
|
||||||
|
font-size: 34px;
|
||||||
|
font-weight: bold;
|
||||||
|
line-height: normal;
|
||||||
|
color: @COLOR_BLACK;
|
||||||
|
padding: 25px 30px 20px 30px;
|
||||||
|
}
|
||||||
|
.subTitle {
|
||||||
|
width: 940px;
|
||||||
|
text-align: left;
|
||||||
|
font-weight: normal;
|
||||||
|
font-size: 28px;
|
||||||
|
color: @COLOR_GRAY06;
|
||||||
|
padding: 10px 60px 0 30px;
|
||||||
|
text-align: left;
|
||||||
|
.elip(@clamp:1);
|
||||||
|
}
|
||||||
|
.subTitleOnly {
|
||||||
|
width: 100%;
|
||||||
|
line-height: 60px;
|
||||||
|
text-align: center;
|
||||||
|
padding: 10px 60px 10px 30px;
|
||||||
|
}
|
||||||
|
.price {
|
||||||
|
padding: 0 60px 20px 30px;
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: @PRIMARY_COLOR_RED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.smsArea {
|
||||||
|
border-top: 1px solid #cccccc;
|
||||||
|
.flex(@justifyCenter: space-between, @alignCenter: flex-start);
|
||||||
|
padding-top: 25px;
|
||||||
|
|
||||||
|
.smsContainer {
|
||||||
|
width: 571px;
|
||||||
|
flex: none;
|
||||||
|
.flex(@justifyCenter: space-between, @alignCenter: flex-start,@direction:column);
|
||||||
|
.inputNum {
|
||||||
|
position: relative;
|
||||||
|
padding: 18px 5px 18px 20px;
|
||||||
|
width: 438px;
|
||||||
|
.border-solid(@size:1px,#cccccc);
|
||||||
|
height: 68px;
|
||||||
|
cursor: text;
|
||||||
|
font-weight: normal;
|
||||||
|
font-size: 28px;
|
||||||
|
|
||||||
|
&.rucInput {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
&::after {
|
||||||
|
.focused();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
min-width: 437px;
|
||||||
|
max-width: 497px;
|
||||||
|
margin: 0 70px 0 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.instruction {
|
||||||
|
margin-right: 80px;
|
||||||
|
line-height: 1.3;
|
||||||
|
letter-spacing: -0.2px;
|
||||||
|
font-weight: normal;
|
||||||
|
font-size: 24px;
|
||||||
|
text-align: left;
|
||||||
|
color: @COLOR_GRAY06;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
> span {
|
||||||
|
&.instructionRu {
|
||||||
|
font-size: 21px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.btnContainer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 30px 0;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
margin-top: 2;
|
||||||
|
min-width: 340px;
|
||||||
|
height: 80px;
|
||||||
|
margin: 0 10px;
|
||||||
|
background: @COLOR_GRAY09;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
background: @PRIMARY_COLOR_RED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -56,6 +56,7 @@ import CustomImage from "../CustomImage/CustomImage";
|
|||||||
import TButton from "../TButton/TButton";
|
import TButton from "../TButton/TButton";
|
||||||
import TPopUp from "../TPopUp/TPopUp";
|
import TPopUp from "../TPopUp/TPopUp";
|
||||||
import HistoryPhoneNumber from "./HistoryPhoneNumber/HistoryPhoneNumber";
|
import HistoryPhoneNumber from "./HistoryPhoneNumber/HistoryPhoneNumber";
|
||||||
|
import PhoneInputSection from "./PhoneInputSection";
|
||||||
import css from "./MobileSendPopUp.module.less";
|
import css from "./MobileSendPopUp.module.less";
|
||||||
import SMSNumKeyPad from "./SMSNumKeyPad";
|
import SMSNumKeyPad from "./SMSNumKeyPad";
|
||||||
|
|
||||||
@@ -80,6 +81,8 @@ export default function MobileSendPopUp({
|
|||||||
subTitle,
|
subTitle,
|
||||||
productImg,
|
productImg,
|
||||||
productPrice,
|
productPrice,
|
||||||
|
brandLogo,
|
||||||
|
productId,
|
||||||
smsTpCd,
|
smsTpCd,
|
||||||
smsText,
|
smsText,
|
||||||
prdtId,
|
prdtId,
|
||||||
@@ -567,11 +570,16 @@ export default function MobileSendPopUp({
|
|||||||
hasText
|
hasText
|
||||||
spotlightId={mobileSendPopUpSpotlightId}
|
spotlightId={mobileSendPopUpSpotlightId}
|
||||||
>
|
>
|
||||||
<div className={css.header}>
|
<div className={css.titleHead}>
|
||||||
|
<div className={css.titleHead__text}>
|
||||||
|
{title || MSG_SEND_LINK}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={css.container__header}>
|
||||||
{productImg && (
|
{productImg && (
|
||||||
<p className={css.productImg}>
|
<p className={css.container__header__productImg}>
|
||||||
<CustomImage
|
<CustomImage
|
||||||
className={css.img}
|
className={css.container__header__productImg__img}
|
||||||
src={productImg}
|
src={productImg}
|
||||||
fallbackSrc={defaultImage}
|
fallbackSrc={defaultImage}
|
||||||
/>
|
/>
|
||||||
@@ -579,27 +587,41 @@ export default function MobileSendPopUp({
|
|||||||
)}
|
)}
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
css.textBox,
|
css.container__header__textBox,
|
||||||
!productImg && css.textBoxOnly
|
!productImg && css.container__header__textBoxOnly
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{title && <div className={css.smsTitle}>{title}</div>}
|
<div className={css.headerTopRow}>
|
||||||
|
{brandLogo && (
|
||||||
|
<img
|
||||||
|
className={css.headerTopRow__brandLogo}
|
||||||
|
src={brandLogo}
|
||||||
|
alt="Brand"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{productId && (
|
||||||
|
<div className={css.headerTopRow__productId}>
|
||||||
|
ID: {productId}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
{subTitle && (
|
{subTitle && (
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
css.subTitle,
|
css.container__header__subTitle,
|
||||||
!productImg && css.subTitleOnly
|
!productImg && css.container__header__subTitleOnly
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{subTitle}
|
{subTitle}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{productPrice && (
|
{productPrice && (
|
||||||
<div className={css.price}>{productPrice}</div>
|
<div className={css.container__header__price}>{productPrice}</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={css.smsArea}>
|
<div className={css.container__smsArea}>
|
||||||
|
{/* 🔄 [BACKUP] 기존 전화번호 입력 영역 - 원상복구용
|
||||||
<div className={css.smsContainer}>
|
<div className={css.smsContainer}>
|
||||||
<InputContainer className={css.smsNumLayer}>
|
<InputContainer className={css.smsNumLayer}>
|
||||||
<SpottableComponent
|
<SpottableComponent
|
||||||
@@ -638,8 +660,34 @@ export default function MobileSendPopUp({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
*/}
|
||||||
|
|
||||||
|
{/* 🆕 [NEW] PhoneInputSection + instruction 좌우 배치 */}
|
||||||
|
<div className={css.container__smsArea__smsContainer}>
|
||||||
|
<PhoneInputSection
|
||||||
|
mobileNumber={mobileNumber}
|
||||||
|
deviceCountryCode={deviceCountryCode}
|
||||||
|
onInputClick={handleInputClick}
|
||||||
|
onKeypadClick={numKeypadClicked}
|
||||||
|
spotlightId={mobileSendPopUpSpotlightId}
|
||||||
|
inputDisabled={inputDisabled}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Container className={css.btnContainer}>
|
<div className={css.container__smsArea__instruction}>
|
||||||
|
{deviceCountryCode && (
|
||||||
|
<div className={deviceCountryCode === "RU" ? css.instructionRu : ""}>
|
||||||
|
<div>
|
||||||
|
{$L("By clicking Agree and Send button, I agree that LGE may collect and store my cell phone number to send text messages as I requested, for data analysis and for feature-enhancement purposes.")}
|
||||||
|
</div>
|
||||||
|
<div style={{ height: "16px" }}></div>
|
||||||
|
<div>
|
||||||
|
{$L("By entering my cell phone number, I agree to receive messages from LGE with information on how to purchase the product I selected. Message and data rates may apply.")}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Container className={css.container__btnContainer}>
|
||||||
<TButton
|
<TButton
|
||||||
onClick={handleAgreeSendClick}
|
onClick={handleAgreeSendClick}
|
||||||
spotlightId={"agreeAndSend"}
|
spotlightId={"agreeAndSend"}
|
||||||
|
|||||||
@@ -1,8 +1,50 @@
|
|||||||
@import "../../style/CommonStyle.module.less";
|
@import "../../style/CommonStyle.module.less";
|
||||||
@import "../../style/utils.module.less";
|
@import "../../style/utils.module.less";
|
||||||
|
|
||||||
|
/* 🆕 [NEW] Figma 디자인용 타이틀 헤드 스타일 */
|
||||||
|
.titleHead {
|
||||||
|
align-self: stretch;
|
||||||
|
padding: 30px;
|
||||||
|
background: #E7EBEF;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start; // center → flex-start
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.titleHead__text {
|
||||||
|
text-align: left; // center → left
|
||||||
|
color: black;
|
||||||
|
font-size: 32px;
|
||||||
|
font-family: 'LG Smart UI';
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 42px;
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 🆕 [NEW] Figma 헤더 TopRow - brandLogo + productId */
|
||||||
|
.headerTopRow {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 5px; // TV 호환: gap 대신 margin 사용
|
||||||
|
|
||||||
|
.headerTopRow__brandLogo {
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
margin-right: 15px; // TV 호환: gap 대신 margin 사용
|
||||||
|
}
|
||||||
|
|
||||||
|
.headerTopRow__productId {
|
||||||
|
color: #808080;
|
||||||
|
font-size: 24px;
|
||||||
|
font-family: 'LG Smart UI';
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 18px;
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
.header {
|
.container__header {
|
||||||
position: relative;
|
position: relative;
|
||||||
&::after {
|
&::after {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -15,28 +57,28 @@
|
|||||||
content: "";
|
content: "";
|
||||||
}
|
}
|
||||||
display: flex;
|
display: flex;
|
||||||
> .productImg,
|
> .container__header__productImg,
|
||||||
> .textBox {
|
> .container__header__textBox {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
> .textBoxOnly {
|
> .container__header__textBoxOnly {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin-left: 30px;
|
margin-left: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
> .productImg {
|
> .container__header__productImg {
|
||||||
margin: 30px 0px 0 60px;
|
margin: 30px 0px 0 30px; // 60px → 30px로 변경
|
||||||
|
|
||||||
> .img {
|
> .container__header__productImg__img {
|
||||||
.border-solid(@size:1px,@color:#cccccc);
|
.border-solid(@size:1px,@color:#cccccc);
|
||||||
width: 132px;
|
width: 100px; // 132px → 100px (Figma 크기)
|
||||||
height: 132px;
|
height: 100px; // 132px → 100px (Figma 크기)
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.smsTitle {
|
.container__header__smsTitle {
|
||||||
width: 940px;
|
width: 940px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
font-size: 34px;
|
font-size: 34px;
|
||||||
@@ -45,39 +87,46 @@
|
|||||||
color: @COLOR_BLACK;
|
color: @COLOR_BLACK;
|
||||||
padding: 25px 30px 20px 30px;
|
padding: 25px 30px 20px 30px;
|
||||||
}
|
}
|
||||||
.subTitle {
|
.container__header__subTitle {
|
||||||
width: 940px;
|
width: 940px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
font-size: 28px;
|
font-size: 28px;
|
||||||
color: @COLOR_GRAY06;
|
color: @COLOR_GRAY06;
|
||||||
padding: 10px 60px 0 30px;
|
padding: 10px 30px 0 30px; // 60px → 30px로 변경
|
||||||
text-align: left;
|
text-align: left;
|
||||||
.elip(@clamp:1);
|
.elip(@clamp:1);
|
||||||
}
|
}
|
||||||
.subTitleOnly {
|
.container__header__subTitleOnly {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
line-height: 60px;
|
line-height: 60px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 10px 60px 10px 30px;
|
padding: 10px 30px 10px 30px; // 60px → 30px로 변경
|
||||||
}
|
}
|
||||||
.price {
|
.container__header__price {
|
||||||
padding: 0 60px 20px 30px;
|
padding: 0 30px 20px 30px; // 60px → 30px로 변경
|
||||||
font-size: 28px;
|
font-size: 28px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: @PRIMARY_COLOR_RED;
|
color: @PRIMARY_COLOR_RED;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.smsArea {
|
.container__smsArea {
|
||||||
border-top: 1px solid #cccccc;
|
border-top: 1px solid #cccccc;
|
||||||
.flex(@justifyCenter: space-between, @alignCenter: flex-start);
|
display: flex;
|
||||||
padding-top: 25px;
|
flex-direction: row; // 가로 배치
|
||||||
|
align-items: flex-start;
|
||||||
|
margin: 10px 30px 0 30px; // 위 10px, 좌우 30px
|
||||||
|
width: calc(100% - 60px); // 좌우 마진 제외한 전체 너비
|
||||||
|
height: 412px; // PhoneInputSection 높이와 동일
|
||||||
|
flex-shrink: 0; // 크기 고정
|
||||||
|
|
||||||
.smsContainer {
|
.container__smsArea__smsContainer {
|
||||||
width: 571px;
|
flex: 1; // 남은 공간을 모두 차지하도록
|
||||||
flex: none;
|
min-width: 400px; // 최소 너비 보장
|
||||||
.flex(@justifyCenter: space-between, @alignCenter: flex-start,@direction:column);
|
max-width: 639.6px; // 최대 너비 제한
|
||||||
.inputNum {
|
margin-right: 15px; // 오른쪽 15px 마진
|
||||||
|
|
||||||
|
.container__smsArea__smsContainer__inputNum {
|
||||||
position: relative;
|
position: relative;
|
||||||
padding: 18px 5px 18px 20px;
|
padding: 18px 5px 18px 20px;
|
||||||
width: 438px;
|
width: 438px;
|
||||||
@@ -97,31 +146,47 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
min-width: 437px;
|
|
||||||
max-width: 497px;
|
|
||||||
margin: 0 70px 0 60px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.instruction {
|
.container__smsArea__instruction {
|
||||||
margin-right: 80px;
|
width: 345.5px; // 고정 너비
|
||||||
line-height: 1.3;
|
height: 412px; // PhoneInputSection과 동일한 높이
|
||||||
letter-spacing: -0.2px;
|
padding: 10px 30px 0 30px; // 위 10px, 좌우 30px, 아래 0
|
||||||
font-weight: normal;
|
background: #F8F8F8;
|
||||||
font-size: 24px;
|
border-radius: 12px;
|
||||||
text-align: left;
|
display: flex;
|
||||||
color: @COLOR_GRAY06;
|
justify-content: flex-start;
|
||||||
|
align-items: flex-start;
|
||||||
|
flex-shrink: 0; // 크기 고정
|
||||||
|
|
||||||
|
> div {
|
||||||
|
flex: 1;
|
||||||
|
align-self: stretch;
|
||||||
|
color: #808080;
|
||||||
|
font-size: 20px; // 피그마 스펙대로 복원
|
||||||
|
font-family: 'LG Smart UI';
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 24px; // 피그마 스펙대로 복원
|
||||||
|
word-wrap: break-word;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: space-between;
|
justify-content: flex-start;
|
||||||
|
align-items: flex-start;
|
||||||
|
overflow: hidden; // 넘치는 내용 숨김
|
||||||
|
|
||||||
> span {
|
|
||||||
&.instructionRu {
|
&.instructionRu {
|
||||||
font-size: 21px;
|
font-size: 18px; // 피그마 스펙대로 복원
|
||||||
|
line-height: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
> div {
|
||||||
|
word-wrap: break-word;
|
||||||
|
overflow-wrap: break-word;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.btnContainer {
|
.container__btnContainer {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 30px 0;
|
padding: 30px 0;
|
||||||
|
|||||||
@@ -0,0 +1,198 @@
|
|||||||
|
import React, { useState, useEffect } from "react";
|
||||||
|
import classNames from "classnames";
|
||||||
|
|
||||||
|
import { SpotlightContainerDecorator } from "@enact/spotlight/SpotlightContainerDecorator";
|
||||||
|
import { Spottable } from "@enact/spotlight/Spottable";
|
||||||
|
|
||||||
|
import css from "./PhoneInputSection.module.less";
|
||||||
|
import backspaceIcon from "../../../assets/images/image-mobile-backspace.png";
|
||||||
|
|
||||||
|
const Container = SpotlightContainerDecorator(
|
||||||
|
{ enterTo: "last-focused", defaultElement: `[data-spotlight-id="keypad-number-5"]` },
|
||||||
|
"div"
|
||||||
|
);
|
||||||
|
|
||||||
|
const InputContainer = SpotlightContainerDecorator(
|
||||||
|
{ enterTo: "last-focused" },
|
||||||
|
"div"
|
||||||
|
);
|
||||||
|
|
||||||
|
const SpottableComponent = Spottable("div");
|
||||||
|
|
||||||
|
export default function PhoneInputSection({
|
||||||
|
mobileNumber = "",
|
||||||
|
deviceCountryCode,
|
||||||
|
onInputClick,
|
||||||
|
onKeypadClick,
|
||||||
|
onInputChange,
|
||||||
|
spotlightId,
|
||||||
|
inputDisabled = false,
|
||||||
|
}) {
|
||||||
|
// 내부 상태로 전화번호 관리
|
||||||
|
const [phoneDigits, setPhoneDigits] = useState(Array(11).fill(""));
|
||||||
|
const [currentIndex, setCurrentIndex] = useState(0);
|
||||||
|
|
||||||
|
// props로 받은 mobileNumber 초기화 (최초 1회만)
|
||||||
|
useEffect(() => {
|
||||||
|
if (mobileNumber && phoneDigits.every(digit => !digit)) {
|
||||||
|
const digits = mobileNumber.replace(/\D/g, "").split("").slice(0, 11);
|
||||||
|
const newDigits = Array(11).fill("").map((_, index) => digits[index] || "");
|
||||||
|
setPhoneDigits(newDigits);
|
||||||
|
setCurrentIndex(digits.length < 11 ? digits.length : 10);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 키패드 번호 배열
|
||||||
|
const keypadNumbers = [
|
||||||
|
[1, 2, 3],
|
||||||
|
[4, 5, 6],
|
||||||
|
[7, 8, 9],
|
||||||
|
["Clear", 0, "Delete"]
|
||||||
|
];
|
||||||
|
|
||||||
|
// 부모에게 전화번호 변경 알리기
|
||||||
|
const notifyChange = (newDigits) => {
|
||||||
|
const phoneNumber = newDigits.join("");
|
||||||
|
if (onInputChange) {
|
||||||
|
onInputChange(phoneNumber);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeypadClick = (value) => {
|
||||||
|
if (typeof value === "number") {
|
||||||
|
// 숫자 입력 처리
|
||||||
|
const newDigits = [...phoneDigits];
|
||||||
|
newDigits[currentIndex] = value.toString();
|
||||||
|
setPhoneDigits(newDigits);
|
||||||
|
|
||||||
|
// 다음 칸으로 이동 (마지막 칸이 아닌 경우)
|
||||||
|
if (currentIndex < 10) {
|
||||||
|
setCurrentIndex(currentIndex + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
notifyChange(newDigits);
|
||||||
|
} else if (value === "Clear") {
|
||||||
|
// 전체 초기화
|
||||||
|
const newDigits = Array(11).fill("");
|
||||||
|
setPhoneDigits(newDigits);
|
||||||
|
setCurrentIndex(0);
|
||||||
|
notifyChange(newDigits);
|
||||||
|
} else if (value === "Delete") {
|
||||||
|
// 삭제 처리
|
||||||
|
const newDigits = [...phoneDigits];
|
||||||
|
|
||||||
|
if (phoneDigits[currentIndex]) {
|
||||||
|
// 현재 칸에 값이 있으면 삭제하고 그 자리에 머무름
|
||||||
|
newDigits[currentIndex] = "";
|
||||||
|
setPhoneDigits(newDigits);
|
||||||
|
} else if (currentIndex > 0) {
|
||||||
|
// 현재 칸이 비어있으면 이전 칸으로 이동하고 그 칸 삭제
|
||||||
|
const newIndex = currentIndex - 1;
|
||||||
|
newDigits[newIndex] = "";
|
||||||
|
setPhoneDigits(newDigits);
|
||||||
|
setCurrentIndex(newIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
notifyChange(newDigits);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 기존 콜백도 호출
|
||||||
|
if (onKeypadClick) {
|
||||||
|
onKeypadClick(value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderPhoneDigit = (digit, index) => {
|
||||||
|
const isEmpty = !digit;
|
||||||
|
const isActive = index === currentIndex;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className={classNames(
|
||||||
|
css.phoneInput__digit,
|
||||||
|
isEmpty && css.phoneInput__digit_empty,
|
||||||
|
isActive && css.phoneInput__digit_highlighted
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{digit || ""}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderSeparator = (index) => {
|
||||||
|
return (
|
||||||
|
<div key={`sep-${index}`} className={css.phoneInput__separator} />
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderKeypadButton = (value, rowIndex, colIndex) => {
|
||||||
|
const isNumber = typeof value === "number";
|
||||||
|
const spotlightButtonId = isNumber ? `keypad-number-${value}` : `keypad-${value.toLowerCase()}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SpottableComponent
|
||||||
|
key={`${rowIndex}-${colIndex}`}
|
||||||
|
className={classNames(
|
||||||
|
css.phoneInput__button,
|
||||||
|
!isNumber && css.phoneInput__button_text
|
||||||
|
)}
|
||||||
|
onClick={() => handleKeypadClick(value)}
|
||||||
|
spotlightId={spotlightButtonId}
|
||||||
|
>
|
||||||
|
<div className={css.phoneInput__buttonText}>
|
||||||
|
{value === "Delete" ? (
|
||||||
|
<img src={backspaceIcon} alt="Delete" />
|
||||||
|
) : (
|
||||||
|
value
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</SpottableComponent>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={css.phoneInput}>
|
||||||
|
{/* 전화번호 디스플레이 영역 */}
|
||||||
|
<InputContainer className={css.phoneInput__display}>
|
||||||
|
<SpottableComponent
|
||||||
|
className={css.phoneInput__field}
|
||||||
|
onClick={onInputClick}
|
||||||
|
spotlightDisabled={inputDisabled}
|
||||||
|
spotlightId={spotlightId}
|
||||||
|
>
|
||||||
|
<div className={css.phoneInput__digits}>
|
||||||
|
{/* 국가코드 영역 (러시아 예시) */}
|
||||||
|
{deviceCountryCode === "RU" && (
|
||||||
|
<span className={css.phoneInput__countryCode}>+7 </span>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 전화번호 자리 표시 */}
|
||||||
|
{phoneDigits.slice(0, 3).map((digit, index) => renderPhoneDigit(digit, index))}
|
||||||
|
{renderSeparator(0)}
|
||||||
|
{phoneDigits.slice(3, 6).map((digit, index) => renderPhoneDigit(digit, index + 3))}
|
||||||
|
{renderSeparator(1)}
|
||||||
|
{phoneDigits.slice(6, 10).map((digit, index) => renderPhoneDigit(digit, index + 6))}
|
||||||
|
{phoneDigits.slice(10, 11).map((digit, index) => renderPhoneDigit(digit, index + 10))}
|
||||||
|
</div>
|
||||||
|
</SpottableComponent>
|
||||||
|
</InputContainer>
|
||||||
|
|
||||||
|
{/* 에러 메시지 영역 */}
|
||||||
|
<div className={css.phoneInput__error}>
|
||||||
|
Error Message Error Message
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 키패드 영역 */}
|
||||||
|
<Container className={css.phoneInput__keypad}>
|
||||||
|
<div className={css.phoneInput__keypadGrid}>
|
||||||
|
{keypadNumbers.map((row, rowIndex) =>
|
||||||
|
row.map((number, colIndex) =>
|
||||||
|
renderKeypadButton(number, rowIndex, colIndex)
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Container>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,190 @@
|
|||||||
|
@import "../../style/CommonStyle.module.less";
|
||||||
|
@import "../../style/utils.module.less";
|
||||||
|
|
||||||
|
/* BEM Block: phone-input */
|
||||||
|
.phoneInput {
|
||||||
|
width: 639.6px;
|
||||||
|
height: 412px;
|
||||||
|
padding: 30px;
|
||||||
|
background: rgba(87, 94, 102, 0.95); // var(--Color-Alert-option_panel)
|
||||||
|
border-radius: 12px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
overflow: hidden;
|
||||||
|
flex-shrink: 0; // 크기 고정
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Element: display */
|
||||||
|
.phoneInput__display {
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 27px; // TV 호환: gap 대신 margin
|
||||||
|
}
|
||||||
|
|
||||||
|
.phoneInput__field {
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.phoneInput__digits {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.phoneInput__countryCode {
|
||||||
|
color: #E6E6E6; // var(--Color-Icon&Text*-primary)
|
||||||
|
font-size: 27px;
|
||||||
|
font-family: 'LG Smart UI';
|
||||||
|
font-weight: 600;
|
||||||
|
margin-right: 6px; // TV 호환: gap 대신 margin
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Element: digit */
|
||||||
|
.phoneInput__digit {
|
||||||
|
width: 44.74px;
|
||||||
|
height: 54px;
|
||||||
|
position: relative;
|
||||||
|
background: rgba(230, 230, 230, 0.20); // var(--Color-Input-normal_search)
|
||||||
|
border-radius: 6.17px;
|
||||||
|
margin-right: 6px; // TV 호환: gap 대신 margin
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
color: #E6E6E6; // var(--Color-Icon&Text*-primary)
|
||||||
|
font-size: 27px;
|
||||||
|
font-family: 'LG Smart UI';
|
||||||
|
font-weight: 600;
|
||||||
|
word-wrap: break-word;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modifier: empty */
|
||||||
|
.phoneInput__digit_empty {
|
||||||
|
// 빈 자리 스타일
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modifier: highlighted */
|
||||||
|
.phoneInput__digit_highlighted {
|
||||||
|
background: #E6E6E6; // var(--Color-Icon&Text*-primary)
|
||||||
|
box-shadow: 0px 10.285714149475098px 6.1714301109313965px rgba(0, 0, 0, 0.30);
|
||||||
|
color: #4C5059; // var(--Color-Icon&Text*-focused)
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Element: separator */
|
||||||
|
.phoneInput__separator {
|
||||||
|
width: 7.71px;
|
||||||
|
height: 1.71px;
|
||||||
|
background: #D9D9D9;
|
||||||
|
margin-right: 6px; // TV 호환: gap 대신 margin
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Element: error */
|
||||||
|
.phoneInput__error {
|
||||||
|
width: 288px;
|
||||||
|
height: 26px;
|
||||||
|
margin-bottom: 27px; // TV 호환: gap 대신 margin
|
||||||
|
text-align: center;
|
||||||
|
color: #FB5656; // var(--Color-Fixed-error)
|
||||||
|
font-size: 22px;
|
||||||
|
font-family: 'LG Smart UI';
|
||||||
|
font-weight: 600;
|
||||||
|
word-wrap: break-word;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Element: keypad */
|
||||||
|
.phoneInput__keypad {
|
||||||
|
width: 202.43px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.phoneInput__keypadGrid {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-content: flex-start;
|
||||||
|
|
||||||
|
// 키패드 버튼 간격 처리 (TV 호환)
|
||||||
|
> * {
|
||||||
|
margin-right: 19.46px;
|
||||||
|
margin-bottom: 19.46px;
|
||||||
|
|
||||||
|
// 3번째 열 (3, 6, 9, Delete)의 오른쪽 마진 제거
|
||||||
|
&:nth-child(3n) {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 마지막 행의 하단 마진 제거
|
||||||
|
&:nth-child(n+10) {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Element: button */
|
||||||
|
.phoneInput__button {
|
||||||
|
width: 54.50px;
|
||||||
|
height: 54.50px;
|
||||||
|
position: relative;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
background: #E6E6E6; // var(--Color-Button-focused)
|
||||||
|
box-shadow: 0px 40px 24px rgba(0, 0, 0, 0.30);
|
||||||
|
border-radius: 12px;
|
||||||
|
|
||||||
|
.phoneInput__buttonText {
|
||||||
|
color: #4C5059; // var(--Color-Icon&Text*-focused)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modifier: focused */
|
||||||
|
.phoneInput__button_focused {
|
||||||
|
background: #E6E6E6; // var(--Color-Button-focused)
|
||||||
|
box-shadow: 0px 40px 24px rgba(0, 0, 0, 0.30);
|
||||||
|
border-radius: 12px;
|
||||||
|
|
||||||
|
.phoneInput__buttonText {
|
||||||
|
color: #4C5059; // var(--Color-Icon&Text*-focused)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Element: button text */
|
||||||
|
.phoneInput__buttonText {
|
||||||
|
text-align: center;
|
||||||
|
color: #E6E6E6; // var(--Color-Icon&Text*-primary)
|
||||||
|
font-size: 27px;
|
||||||
|
font-family: 'LG Smart UI';
|
||||||
|
font-weight: 600;
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modifier: text button (Clear) */
|
||||||
|
.phoneInput__button_text .phoneInput__buttonText {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Delete 버튼 이미지 */
|
||||||
|
.phoneInput__buttonText img {
|
||||||
|
width: 29.20px;
|
||||||
|
height: 29.20px;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Delete 버튼 포커스 시 이미지 색상 변경 */
|
||||||
|
.phoneInput__button:focus .phoneInput__buttonText img,
|
||||||
|
.phoneInput__button_focused .phoneInput__buttonText img {
|
||||||
|
filter: brightness(0) saturate(100%) invert(26%) sepia(8%) saturate(1090%) hue-rotate(202deg) brightness(97%) contrast(89%);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user