diff --git a/src/components/AccountSelector.tsx b/src/components/AccountSelector.tsx deleted file mode 100644 index 2ee792101..000000000 --- a/src/components/AccountSelector.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import { useEffect, useState } from "react"; -import { useTranslation } from "react-i18next"; - -import theme from "tools/theme"; - -import bankNames from "static/bankNames"; - -type AccountSelectorProps = { - accountNumber: string; - setAccountNumber: (account: string) => void; -}; - -const AccountSelector = (props: AccountSelectorProps) => { - const initializeAccountState = () => { - const account = props.accountNumber.split(" "); - return [account?.[0] || bankNames[0], account?.[1] || ""]; - }; - - const { t } = useTranslation("mypage"); - const [bankName, setBankName] = useState(initializeAccountState()[0]); - const [bankNumber, setBankNumber] = useState(initializeAccountState()[1]); - - useEffect(() => { - props.setAccountNumber(bankName + " " + bankNumber); - }, [bankName, bankNumber]); - - useEffect(() => { - setBankName(initializeAccountState()[0]); - setBankNumber(initializeAccountState()[1]); - }, [props.accountNumber]); - - const styleTitle: CSS = { - display: "flex", - alignItems: "center", - ...theme.font14, - color: theme.gray_text, - whiteSpace: "nowrap", - marginTop: "10px", - }; - const styleNickname: CSS = { - width: "100%", - ...theme.font14, - border: "none", - outline: "none", - borderRadius: "6px", - padding: "6px 12px", - marginLeft: "10px", - background: theme.purple_light, - boxShadow: theme.shadow_purple_input_inset, - }; - const styleBanks: CSS = { - width: "75px", - ...theme.font14, - color: theme.purple, - fontWeight: "400", - border: "none", - outline: "none", - borderRadius: "6px", - padding: "6px 4px", - marginLeft: "10px", - background: theme.purple_light, - boxShadow: theme.shadow_purple_input_inset, - textAlign: "center", - }; - return ( -
- {t("account")} - - setBankNumber(e.target.value)} - /> -
- ); -}; - -export default AccountSelector; diff --git a/src/components/Chat/Header/SideMenu.tsx b/src/components/Chat/Header/SideMenu.tsx index 84edf30bd..6ad9f54c5 100644 --- a/src/components/Chat/Header/SideMenu.tsx +++ b/src/components/Chat/Header/SideMenu.tsx @@ -7,6 +7,7 @@ import DottedLine from "components/DottedLine"; import { ModalCallTaxi, ModalChatCancel, + ModalChatReport, ModalRoomShare, } from "components/ModalPopup"; import User from "components/User"; @@ -75,18 +76,20 @@ const SideMenu = ({ roomInfo, isOpen, setIsOpen }: SideMenuProps) => { const setAlert = useSetRecoilState(alertAtom); const [isOpenShare, setIsOpenShare] = useState(false); const [isOpenCallTaxi, setIsOpenCallTaxi] = useState(false); + const [isOpenReport, setIsOpenReport] = useState(false); const [isOpenCancel, setIsOpenCancel] = useState(false); - const isDepart = useIsTimeOver(dayServerToClient(roomInfo.time)); // 방 출발 여부 + const isDeparted = useIsTimeOver(dayServerToClient(roomInfo.time)); // 방 출발 여부 const onClikcShare = useCallback(() => setIsOpenShare(true), []); const onClickCancel = useCallback( () => - isDepart + isDeparted ? setAlert("출발 시각이 이전인 방은 탑승 취소를 할 수 없습니다.") : setIsOpenCancel(true), - [isDepart] + [isDeparted] ); const onClickCallTaxi = useCallback(() => setIsOpenCallTaxi(true), []); + const onClickReport = useCallback(() => setIsOpenReport(true), []); const styleBackground = { position: "absolute" as any, @@ -181,33 +184,32 @@ const SideMenu = ({ roomInfo, isOpen, setIsOpen }: SideMenuProps) => {
- {/* @fixme @todo 유저의 정산 정보 넘겨주나? */} {roomInfo.part.map((item) => ( - + ))}
- {/* - */} + +
탑승 취소 @@ -230,6 +232,11 @@ const SideMenu = ({ roomInfo, isOpen, setIsOpen }: SideMenuProps) => { isOpen={isOpenCallTaxi} onChangeIsOpen={setIsOpenCallTaxi} /> + ); }; diff --git a/src/components/Chat/MessageForm/Popup/PopupAccount.tsx b/src/components/Chat/MessageForm/Popup/PopupAccount.tsx deleted file mode 100644 index 7265991dd..000000000 --- a/src/components/Chat/MessageForm/Popup/PopupAccount.tsx +++ /dev/null @@ -1,116 +0,0 @@ -import { useEffect, useState } from "react"; -import { Link } from "react-router-dom"; - -import AccountSelector from "components/AccountSelector"; -import Button from "components/Button"; -import DottedLine from "components/DottedLine"; -import Modal from "components/Modal"; - -import loginInfoAtom from "atoms/loginInfo"; -import { useRecoilValue } from "recoil"; - -import regExpTest from "tools/regExpTest"; -import theme from "tools/theme"; - -import WalletIcon from "@mui/icons-material/Wallet"; - -type SendAccoundModalProps = { - popup: boolean; - onClickClose: () => void; - onClickOk: (account: string) => void; -}; - -const PopupAccount = (props: SendAccoundModalProps) => { - const loginInfo = useRecoilValue(loginInfoAtom); - const [accountNumber, setAccountNumber] = useState(loginInfo?.account || ""); - - const styleTitle = { - ...theme.font18, - display: "flex", - alignItems: "center", - }; - const styleIcon = { - fontSize: "21px", - margin: "0 4px 0 0", - }; - const styleText = { - ...theme.font14, - color: theme.gray_text, - }; - - const handleClickOk = () => { - if (regExpTest.account(accountNumber)) { - props.onClickOk(accountNumber); - } - }; - - useEffect(() => { - if (!props.popup) { - setAccountNumber(loginInfo?.account || ""); - } - }, [props.popup, loginInfo?.account]); - - return ( - -
-
- - 계좌 보내기 -
-
- 계좌를 변경하고 싶으신 경우 마이 페이지의 - “수정하기” 메뉴를 이용해주세요. -
- - -
-
- - -
-
- ); -}; - -export default PopupAccount; diff --git a/src/components/Chat/MessageForm/ToolSheet/index.tsx b/src/components/Chat/MessageForm/ToolSheet/index.tsx index 73d95f03d..03b392e35 100644 --- a/src/components/Chat/MessageForm/ToolSheet/index.tsx +++ b/src/components/Chat/MessageForm/ToolSheet/index.tsx @@ -7,11 +7,16 @@ import { useState, } from "react"; +import useAccountFromChats from "hooks/chat/useAccountFromChats"; import { useValueRecoilState } from "hooks/useFetchRecoilState"; import useIsTimeOver from "hooks/useIsTimeOver"; import AdaptiveDiv from "components/AdaptiveDiv"; -import { ModalChatPayement, ModalChatSettlement } from "components/ModalPopup"; +import { + ModalChatPayement, + ModalChatSaveAccount, + ModalChatSettlement, +} from "components/ModalPopup"; import ToolButton from "./ToolButton"; @@ -26,6 +31,7 @@ type ToolSheetProps = { isOpen: boolean; onChangeIsOpen?: (x: boolean) => void; onChangeUploadedImage?: (x: Nullable) => void; + account: ReturnType; }; const ToolSheet = ({ @@ -33,11 +39,14 @@ const ToolSheet = ({ isOpen, onChangeIsOpen, onChangeUploadedImage, + account, }: ToolSheetProps) => { const setAlert = useSetRecoilState(alertAtom); const { oid: userOid } = useValueRecoilState("loginInfo") || {}; + const [accountToSave, setAccountToSave] = useState(""); const [isOpenSettlement, setIsOpenSettlement] = useState(false); const [isOpenPayment, setIsOpenPayment] = useState(false); + const [isOpenSaveAccount, setIsOpenSaveAccount] = useState(true); const isDepart = useIsTimeOver( roomInfo ? dayServerToClient(roomInfo.time) : dayNowClient() ); // 방 출발 여부 @@ -60,28 +69,35 @@ const ToolSheet = ({ ); const onClickImage = useCallback(() => inputImageRef.current?.click(), []); const onClickSettlement = useCallback(() => { - if (!isDepart) setAlert("출발 시각 이후에 정산하기 요청을 보내주세요."); + if (!isDepart) + setAlert("출발 시각 이후부터 정산 및 송금하기가 가능합니다."); else if (settlementStatusForMe === "paid") - setAlert("정산하기 요청은 중복하여 보낼 수 없습니다."); + setAlert("정산하기는 중복하여 수행될 수 없습니다."); else if (roomInfo?.settlementTotal) setAlert( - "정산하기 요청을 한 사용자가 이미 있습니다." + - "만약 결제하지 않은 사용자가 정산하기 요청을 보냈다면 신고해주세요." + <> + 정산하기 요청을 한 사용자가 이미 있습니다. +
+ 만약 결제하지 않은 사용자가 정산하기 요청을 보냈다면 신고해주세요. + ); else setIsOpenSettlement(true); }, [isDepart, settlementStatusForMe]); const onClickPayment = useCallback(() => { - if (!isDepart) setAlert("출발 시각 이후에 송금하기 요청을 보내주세요."); - else if (settlementStatusForMe === "sent") - setAlert("송금하기 요청은 중복하여 보낼 수 없습니다."); + if (!isDepart) + setAlert("출발 시각 이후부터 정산 및 송금하기가 가능합니다."); else if (!roomInfo?.settlementTotal) - setAlert("정산하기 요청을 보낸 사용자가 없어 송금하기가 불가능합니다."); + setAlert("정산하기를 요청한 사용자가 없어 송금하기가 불가능합니다."); else setIsOpenPayment(true); }, [isDepart, settlementStatusForMe, setIsOpenPayment]); const onRecallSettlePayment = useCallback( () => onChangeIsOpen?.(false), [onChangeIsOpen] ); + const openSaveAccountModal = useCallback((account: string) => { + setAccountToSave(account); + setIsOpenSaveAccount(true); + }, []); const styleWrap = { position: "absolute" as any, @@ -131,15 +147,24 @@ const ToolSheet = ({ + {accountToSave && ( + + )} )}
diff --git a/src/components/Chat/MessageForm/index.tsx b/src/components/Chat/MessageForm/index.tsx index 1820ab759..d63cc96d7 100644 --- a/src/components/Chat/MessageForm/index.tsx +++ b/src/components/Chat/MessageForm/index.tsx @@ -1,7 +1,8 @@ import { RefObject, memo, useState } from "react"; -import type { LayoutType } from "types/chat"; +import type { Chats, LayoutType } from "types/chat"; +import useAccountFromChats from "hooks/chat/useAccountFromChats"; import useSendMessage from "hooks/chat/useSendMessage"; import InputText from "./InputText"; @@ -19,6 +20,7 @@ import theme from "tools/theme"; type MessageFormProps = { layoutType: LayoutType; roomInfo: Nullable; + chats: Chats; isDisplayNewMessage: boolean; isOpenToolSheet: boolean; onChangeIsOpenToolSheet: (x: boolean) => void; @@ -29,6 +31,7 @@ type MessageFormProps = { const MessageForm = ({ layoutType, roomInfo, + chats, isDisplayNewMessage, isOpenToolSheet, onChangeIsOpenToolSheet, @@ -37,6 +40,7 @@ const MessageForm = ({ }: MessageFormProps) => { const isVKDetected = useRecoilValue(isVirtualKeyboardDetectedAtom); const [uploadedImage, setUploadedImage] = useState>(null); // 업로드된 이미지 파일 + const account = useAccountFromChats(chats); const onClickNewMessage = () => { if (!messageBodyRef.current) return; @@ -74,6 +78,7 @@ const MessageForm = ({ isOpen={isOpenToolSheet} onChangeIsOpen={onChangeIsOpenToolSheet} onChangeUploadedImage={setUploadedImage} + account={account} />
diff --git a/src/components/Chat/MessagesBody/MessageSet/MessageAccount.tsx b/src/components/Chat/MessagesBody/MessageSet/MessageAccount.tsx index 28aa77e6d..257fa7184 100644 --- a/src/components/Chat/MessagesBody/MessageSet/MessageAccount.tsx +++ b/src/components/Chat/MessagesBody/MessageSet/MessageAccount.tsx @@ -1,6 +1,8 @@ -import { useMemo } from "react"; +import { useMemo, useState } from "react"; -import LinkCopy from "components/Link/LinkCopy"; +import { useValueRecoilState } from "hooks/useFetchRecoilState"; + +import { ModalChatPayement } from "components/ModalPopup"; import Button from "./Button"; @@ -9,14 +11,23 @@ import theme from "tools/theme"; import WalletRoundedIcon from "@mui/icons-material/WalletRounded"; type MessageAccountProps = { + roomInfo: Room; account: string; }; -const MessageAccount = ({ account }: MessageAccountProps) => { +const MessageAccount = ({ roomInfo, account }: MessageAccountProps) => { + const { oid: userOid } = useValueRecoilState("loginInfo") || {}; + const [isOpenPayment, setIsOpenPayment] = useState(false); const [bankName, accountNumber] = useMemo((): [string, string] => { const splited = account.split(" "); return [splited?.[0] || "", splited?.[1] || ""]; }, [account]); + const settlementStatusForMe = useMemo( + () => + roomInfo && + roomInfo.part.filter((user) => user._id === userOid)?.[0]?.isSettlement, + [userOid, roomInfo] + ); const style = { width: "210px" }; const styleHead = { @@ -54,12 +65,27 @@ const MessageAccount = ({ account }: MessageAccountProps) => {
- - - + {settlementStatusForMe === "paid" ? ( + + ) : // @todo: 정산현황 + settlementStatusForMe === "sent" ? ( + + ) : ( + + )}
+ ); }; diff --git a/src/components/Chat/MessagesBody/MessageSet/index.tsx b/src/components/Chat/MessagesBody/MessageSet/index.tsx index 8b298017f..e603f71ef 100644 --- a/src/components/Chat/MessagesBody/MessageSet/index.tsx +++ b/src/components/Chat/MessagesBody/MessageSet/index.tsx @@ -4,7 +4,7 @@ import type { BotChat, LayoutType, UserChat } from "types/chat"; import { useValueRecoilState } from "hooks/useFetchRecoilState"; -import ProfileImg from "components/User/ProfileImg"; +import ProfileImage from "components/User/ProfileImage"; import MessageAccount from "./MessageAccount"; import MessageImage from "./MessageImage"; @@ -21,7 +21,7 @@ import { ReactComponent as TaxiIcon } from "static/assets/TaxiAppIcon.svg"; type MessageBodyProps = { type: (UserChat | BotChat)["type"]; content: (UserChat | BotChat)["content"]; - roomInfo?: BotChat["roomInfo"]; + roomInfo: Room; color: CSS["color"]; }; @@ -35,11 +35,7 @@ const MessageBody = ({ type, content, roomInfo, color }: MessageBodyProps) => { case "settlement": return ; case "account": - return ; - } - - if (!roomInfo) return null; - switch (type) { + return ; case "share": return ; default: @@ -50,9 +46,10 @@ const MessageBody = ({ type, content, roomInfo, color }: MessageBodyProps) => { type MessageSetProps = { chats: Array; layoutType: LayoutType; + roomInfo: Room; }; -const MessageSet = ({ chats, layoutType }: MessageSetProps) => { +const MessageSet = ({ chats, layoutType, roomInfo }: MessageSetProps) => { const { oid: userOid } = useValueRecoilState("loginInfo") || {}; const authorId = chats?.[0]?.authorId; const authorProfileUrl = @@ -139,7 +136,7 @@ const MessageSet = ({ chats, layoutType }: MessageSetProps) => { {authorId === "bot" ? ( ) : ( - + )} )} @@ -156,7 +153,7 @@ const MessageSet = ({ chats, layoutType }: MessageSetProps) => { diff --git a/src/components/Chat/MessagesBody/index.tsx b/src/components/Chat/MessagesBody/index.tsx index 7c10be0c1..339b55d26 100644 --- a/src/components/Chat/MessagesBody/index.tsx +++ b/src/components/Chat/MessagesBody/index.tsx @@ -8,11 +8,12 @@ import LoadingChats from "./LoadingChats"; type MessagesBodyProps = { layoutType: LayoutType; + roomInfo: Room; chats: Chats; }; const MessagesBody = ( - { layoutType, chats: _chats }: MessagesBodyProps, + { layoutType, roomInfo, chats: _chats }: MessagesBodyProps, ref: ForwardedRef ) => (
{_chats.length <= 0 && } - {useChatsForBody(_chats, layoutType)} + {useChatsForBody(_chats, layoutType, roomInfo)}
); diff --git a/src/components/Chat/index.tsx b/src/components/Chat/index.tsx index 1a1badd37..81f14f205 100644 --- a/src/components/Chat/index.tsx +++ b/src/components/Chat/index.tsx @@ -61,12 +61,14 @@ const Chat = ({ roomId, layoutType }: ChatProps) => {
void; + className?: string; // for emotion (css props) +} & Parameters[0]; + +const syncValue2Account = (value: string): [string, string] => { + const splited = value.split(" "); + const name: string = bankNames.includes(splited?.[0]) + ? splited?.[0] + : bankNames[0]; + const number: string = (splited?.[1] || "").replace(/[^0-9]/g, ""); + return [name, number]; +}; + +const syncAccount2Value = (_name: string, _number: string): string => { + if (_number === "") return ""; + const name: string = bankNames.includes(_name) ? _name : bankNames[0]; + const number: string = _number.replace(/[^0-9]/g, ""); + return name + " " + number; +}; + +const InputAcount = ({ + value, + onChangeValue, + className, + ...inputProps +}: InputAcountProps) => { + const [_name, number] = useMemo(() => syncValue2Account(value), [value]); + const [name, setName] = useState(_name); + + useEffect(() => { + if (number !== "") setName(_name); + }, [_name, number]); + + const onChangeName = (nameAfter: string) => { + setName(nameAfter); + onChangeValue?.(syncAccount2Value(nameAfter, number)); + }; + const onChangeNumber = (numberAfter: string) => { + onChangeValue?.(syncAccount2Value(name, numberAfter)); + }; + + return ( + + + + ); +}; + +export default InputAcount; diff --git a/src/components/Input/Select.tsx b/src/components/Input/Select.tsx new file mode 100644 index 000000000..f3fc625bc --- /dev/null +++ b/src/components/Input/Select.tsx @@ -0,0 +1,62 @@ +import theme from "tools/theme"; + +import ArrowDropDownRoundedIcon from "@mui/icons-material/ArrowDropDownRounded"; + +type SelectProps = { + value: string; + options: Array<{ value: string; label: string }>; + onChangeValue?: (v: string) => void; + className?: string; // for emotion (css props) +}; + +const Select = ({ value, options, onChangeValue, className }: SelectProps) => { + return ( + + + {options.find((option) => option.value === value)?.label || ""} + + + ); +}; + +export default Select; diff --git a/src/components/Input/index.tsx b/src/components/Input/index.tsx new file mode 100644 index 000000000..47e4f9fa7 --- /dev/null +++ b/src/components/Input/index.tsx @@ -0,0 +1,34 @@ +import { HTMLProps } from "react"; + +import theme from "tools/theme"; + +type InputProps = { + value?: string; + onChangeValue?: (v: string) => void; + className?: string; // for emotion (css props) +} & HTMLProps; + +const Input = ({ + value, + onChangeValue, + className, + ...inputProps +}: InputProps) => ( + onChangeValue?.(e.target.value)} + className={className} + css={{ + border: "none", + outline: "none", + borderRadius: "6px", + padding: "6px 12px", + background: theme.purple_light, + boxShadow: theme.shadow_purple_input_inset, + ...theme.font14, + }} + {...inputProps} + /> +); + +export default Input; diff --git a/src/components/Link/LinkCopy.tsx b/src/components/Link/LinkCopy.tsx index 28c2937cc..9f9020b1c 100644 --- a/src/components/Link/LinkCopy.tsx +++ b/src/components/Link/LinkCopy.tsx @@ -23,7 +23,7 @@ const LinkCopy = ({ children, value, onCopy }: LinkCopyProps) => { return; } navigator.clipboard.writeText(value); - if (onCopy) onCopy(value); + onCopy?.(value); }, [isApp, value, setAlert, onCopy]); return {children}; }; diff --git a/src/components/Link/LinkPayment.tsx b/src/components/Link/LinkPayment.tsx new file mode 100644 index 000000000..f8ea99523 --- /dev/null +++ b/src/components/Link/LinkPayment.tsx @@ -0,0 +1,79 @@ +import { ReactNode, useMemo } from "react"; + +import LinkCopy from "./LinkCopy"; + +type LinkPaymentProps = { + children: ReactNode; + type: "kakaopay" | "toss"; + account?: string; + amount?: number; +}; + +const bankName2Code = (name: Nullable): Nullable => { + if (!name) return undefined; + const bankName2CodeMap = [ + ["농협", "011"], + ["국민", "004"], + ["카카오", "090"], + ["신한", "088"], + ["우리", "020"], + ["기업", "003"], + ["하나", "081"], + ["토스", "092"], + ["새마을", "045"], + ["부산", "032"], + ["대구", "031"], + ["케이", "089"], + ["신협", "048"], + ["우체국", "071"], + ["SC제일", "023"], + ["경남", "039"], + ["수협", "007"], + ["광주", "034"], + ["전북", "037"], + ["저축", "050"], + ["씨티", "027"], + ["제주", "035"], + ["산업", "002"], + ["산림", "064"], + ]; + return ( + bankName2CodeMap.find((item) => name.includes(item[0]))?.[1] || undefined + ); +}; + +const LinkPayment = ({ children, account, type, amount }: LinkPaymentProps) => { + const splited = account?.split(" "); + const bankName = splited?.[0]; + const bankCode = useMemo(() => bankName2Code(bankName), [bankName]); + const accountNumber = splited?.[1]; + + switch (type) { + case "kakaopay": + return ( + { + window.location.href = "kakaotalk://kakaopay/money/to/bank"; + }} + > + {children} + + ); + case "toss": + return ( + + {children} + + ); + } +}; + +export default LinkPayment; diff --git a/src/components/Modal/ModalElem.tsx b/src/components/Modal/ModalElem.tsx index e69654c7a..e8e7dc78a 100644 --- a/src/components/Modal/ModalElem.tsx +++ b/src/components/Modal/ModalElem.tsx @@ -25,7 +25,7 @@ export type ModalElemProps = { displayCloseBtn?: boolean; width?: PixelValue; padding?: Padding; - children: ReactNode; + children?: ReactNode; isAlert?: boolean; }; diff --git a/src/components/ModalPopup/Body/BodyCallTaxi.tsx b/src/components/ModalPopup/Body/BodyCallTaxi.tsx index f828d1697..09cc760cf 100644 --- a/src/components/ModalPopup/Body/BodyCallTaxi.tsx +++ b/src/components/ModalPopup/Body/BodyCallTaxi.tsx @@ -5,7 +5,7 @@ import LinkCallTaxi from "components/Link/LinkCallTaxi"; import theme from "tools/theme"; import LocationOnRoundedIcon from "@mui/icons-material/LocationOnRounded"; -import { ReactComponent as KakaoTaxiLogo } from "static/assets/KakaotTaxiLogo.svg"; +import { ReactComponent as KakaoTaxiLogo } from "static/assets/KakaoTaxiLogo.svg"; import TmoneyOndaLogo from "static/assets/TmoneyOndaLogo.png"; import { ReactComponent as UTLogo } from "static/assets/UTLogo.svg"; diff --git a/src/components/ModalPopup/Body/BodyChatReportDone.tsx b/src/components/ModalPopup/Body/BodyChatReportDone.tsx new file mode 100644 index 000000000..e56667d70 --- /dev/null +++ b/src/components/ModalPopup/Body/BodyChatReportDone.tsx @@ -0,0 +1,37 @@ +import Button from "components/Button"; + +import theme from "tools/theme"; + +type BodyChatReportDoneProps = { + onChangeIsOpen?: (isOpen: boolean) => void; +}; + +const BodyChatReportDone = ({ onChangeIsOpen }: BodyChatReportDoneProps) => { + const styleText = { + ...theme.font12, + color: theme.gray_text, + margin: "0 8px 12px", + }; + + return ( + <> +
+ 신고완료되었습니다. +
+
+ 신고 내역은 마이페이지에서 화인하실 수 있습니다. +
+ + + ); +}; + +export default BodyChatReportDone; diff --git a/src/components/ModalPopup/Body/BodyChatReportSelectType.tsx b/src/components/ModalPopup/Body/BodyChatReportSelectType.tsx new file mode 100644 index 000000000..e69c27aaa --- /dev/null +++ b/src/components/ModalPopup/Body/BodyChatReportSelectType.tsx @@ -0,0 +1,249 @@ +import { + CSSProperties, + Dispatch, + SetStateAction, + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from "react"; + +import type { Report } from "types/report"; + +import { useValueRecoilState } from "hooks/useFetchRecoilState"; +import useIsTimeOver from "hooks/useIsTimeOver"; +import { useAxios } from "hooks/useTaxiAPI"; + +import Button from "components/Button"; +import DottedLine from "components/DottedLine"; +import Select from "components/Input/Select"; +import User from "components/User"; + +import alertAtom from "atoms/alert"; +import { useSetRecoilState } from "recoil"; + +import { dayServerToClient } from "tools/day"; +import regExpTest from "tools/regExpTest"; +import theme from "tools/theme"; + +import EditRoundedIcon from "@mui/icons-material/EditRounded"; + +type BodyChatReportSelectTypeProps = { + roomInfo: Room; + reportedId: Report["reportedId"]; + clearReportedId: () => void; + setIsSubmitted: Dispatch>; +}; + +const selectOptions = [ + { value: "no-settlement", label: "송금을 하지 않음" }, + { value: "no-show", label: "택시에 동승하지 않음" }, + { value: "etc-reason", label: "기타 사유" }, +]; + +const BodyChatReportSelectType = ({ + roomInfo, + reportedId, + clearReportedId, + setIsSubmitted, +}: BodyChatReportSelectTypeProps) => { + const axios = useAxios(); + const setAlert = useSetRecoilState(alertAtom); + const { oid: userOid } = useValueRecoilState("loginInfo") || {}; + const wrapRef = useRef(null); + const textareaRef = useRef(null); + const [height, setHeight] = useState("28px"); + const [type, setType] = useState("no-settlement"); + const [etcDetail, setEtcDetail] = useState(""); + const reportedUser: Nullable = useMemo( + () => roomInfo.part.find((user) => user._id === reportedId), + [roomInfo, reportedId] + ); + const isDeparted = useIsTimeOver(dayServerToClient(roomInfo.time)); // 방 출발 여부 + const isRequesting = useRef(false); + + const inValidMessage = useMemo( + () => + type === "no-settlement" && !isDeparted + ? "출발 시각 이전에는 해당 사유로 사용자를 신고할 수 없습니다." + : type === "no-settlement" && reportedUser?.isSettlement === "paid" + ? "정산자는 송금 대상자가 아니기 때문에 해당 사유로 신고할 수 없습니다." + + " 만약 택시비를 결제하지 않았는데 정산하기를 요청한 사용자라면 기타 사유로 신고해주세요." + : type === "no-show" && !isDeparted + ? "출발 시각 이전에는 해당 사유로 사용자를 신고할 수 없습니다." + : type === "etc-reason" && etcDetail === "" + ? "기타 사유를 입력해주세요." + : type === "etc-reason" && !regExpTest.reportMsg(etcDetail) + ? "기타 사유는 1500자 까지 입력이 허용됩니다." + : userOid === reportedUser?._id + ? "나 자신은 신고할 수 없습니다." + : null, + [type, etcDetail, isDeparted, userOid, reportedUser] + ); + + const resizeEvent = useCallback(() => { + if (!wrapRef.current) return; + const cacheHeight = wrapRef.current.style.height; + wrapRef.current.style.height = "0"; + const newHeight = `${Math.max( + Math.min( + textareaRef.current ? textareaRef.current.scrollHeight : 0, + document.body.clientHeight / 3 + ), + 28 + )}px`; + wrapRef.current.style.height = cacheHeight; + setHeight(newHeight); + }, [setHeight]); + + useEffect(() => { + resizeEvent(); + window.addEventListener("resize", resizeEvent); + return () => window.removeEventListener("resize", resizeEvent); + }, []); + useEffect(resizeEvent, [etcDetail]); + + const handleSubmit = async () => { + if (isRequesting.current) return; + isRequesting.current = true; + await axios({ + url: "/reports/create", + method: "post", + data: { + reportedId, + type, + etcDetail: type === "etc-reason" ? etcDetail : undefined, + time: new Date(), + roomId: roomInfo._id, + }, + onSuccess: () => setIsSubmitted(true), + onError: () => setAlert("신고에 실패했습니다."), + }); + isRequesting.current = false; + }; + + const styleText = { + ...theme.font12, + color: theme.gray_text, + margin: "0 8px 12px", + }; + const styleSelectWrap = { + margin: "12px 8px", + display: "flex", + alignItems: "center", + color: theme.gray_text, + whiteSpace: "nowrap" as const, + ...theme.font14, + }; + const styleSelect = { + width: "100%", + marginLeft: "10px", + color: theme.black, + }; + const styleTextareaWrap = { + height, + position: "relative" as const, + overflow: "hidden", + margin: "0 8px 12px", + borderRadius: "6px", + background: theme.purple_light, + boxShadow: theme.shadow_purple_input_inset, + }; + const styleTextarea = { + width: "100%", + height: "100%", + padding: "6px 12px 6px 38px", + boxSizing: "border-box" as const, + background: "none", + border: "none", + resize: "none" as const, + outline: "none", + ...theme.font14, + color: theme.gray_text, + }; + const styleIcon = { + color: theme.black, + fontSize: "14px", + position: "absolute" as const, + top: "6px", + left: "12px", + }; + const styleButtons = { + position: "relative" as const, + display: "flex", + justifyContent: "space-between", + gap: "10px", + }; + + return ( + <> + {reportedUser && ( +
+ +
+ )} +
+ 를 어떤 사유로 신고할까요? 만약 선택지에 원하시는 사유가 없다면 + "기타 사유" 선택 후 자세히 설명해주세요. +
+ +
+ 사유 +